Iteration 4 over Assets/Scripts (no component/file splits, no Unity assets touched): Dedup: - Route inline cursor-release, material guards, Ctrl-held and undo/redo input through existing InputUtil / SetMatFloat helpers - Add ungated InputUtil.KeyPressedThisFrame; GameplayKeyPressed builds on it - Unify FindDeep/FindDeepChild into GeometryUtil.FindDeep Dead code / stale docs: - Remove unused code-gen bake path (HsvColorPicker.Build, PaintHud factory) - Drop unreferenced symbols (SubPhaseChanged event, LocalPhaseDuration, IsLocalSceneMain, MouseSensitivity.Changed, IsPaused, InSession, NetworkControlled, PreviousPose, JointCount, voxel-count accessors) - Fix incorrect PlayerRole / PaintHud / Pick / PlayerPose docs Perf / robustness (behaviour-neutral): - ConcealmentMeter.Evaluate: reuse vertex/topology buffers, two-pointer crossing count - BuildMeshCaches: alloc-free vertex copy, cached topology/UVs - Guard all PlayerPaintSync LogSync sites behind logSync - Cache LocalViewerIsHunter / controller / CanShoot / TypingGuard lookups; dedup LocalIsHunter with MatchHud - Change-guard RefreshEmissionColor and HeroCutout.Disarm - Cache scene/countdown string comparisons per frame - Grow NonAlloc buffers and log-once on saturation Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
83 lines
4.0 KiB
C#
83 lines
4.0 KiB
C#
using Unity.Netcode;
|
|
using UnityEngine;
|
|
|
|
/// <summary>
|
|
/// Sits on the Lobby's "Start Match" trigger volume (a BoxCollider with isTrigger on). When the match
|
|
/// AUTHORITY's avatar stands inside it, it asks the <see cref="MatchManager"/> to start the match.
|
|
///
|
|
/// Detection POLLS a server-side physics overlap every frame instead of relying on OnTriggerEnter.
|
|
/// Why: the authority can be a REMOTE client (a dedicated server with no host player hands authority to
|
|
/// the first client). On the server that client's avatar is a NetworkTransform-driven replica - its
|
|
/// CharacterController is disabled (<see cref="NetworkPlayerSetup"/>) and it carries no Rigidbody, so it
|
|
/// is moved by direct transform writes. Unity only sends OnTriggerEnter when one of the two colliders has
|
|
/// a Rigidbody (or is a CharacterController moving via Move()); a transform-moved non-Rigidbody collider
|
|
/// against a static trigger fires nothing. That is exactly why a host's own (Move()-driven) avatar tripped
|
|
/// the trigger but a remote authority never did. An explicit overlap query finds the avatar's (baked)
|
|
/// collider regardless of HOW it is moved, so start works for a host and a dedicated server alike.
|
|
///
|
|
/// Server-gated: pure clients query nothing. Only the authority avatar starts the match; a passing
|
|
/// non-authority avatar is ignored. Runs only while in the lobby phase and stops once it has started a
|
|
/// match. The trigger lives in the additively-loaded Lobby and is destroyed when the lobby unloads, so a
|
|
/// re-match (back in the lobby) gets a fresh instance with its guard reset.
|
|
/// </summary>
|
|
[RequireComponent(typeof(Collider))]
|
|
[DisallowMultipleComponent]
|
|
public class StartMatchTrigger : MonoBehaviour
|
|
{
|
|
private Collider _collider;
|
|
private readonly Collider[] _overlap = new Collider[32];
|
|
private bool _started;
|
|
private bool _overlapSaturated; // log-once: a full buffer could silently drop the authority's avatar
|
|
|
|
private void Awake() => _collider = GetComponent<Collider>();
|
|
|
|
private void Update()
|
|
{
|
|
if (_started) return;
|
|
|
|
var nm = NetworkManager.Singleton;
|
|
if (!nm || !nm.IsServer) return; // detection + start are server-side
|
|
var match = MatchManager.Instance;
|
|
if (!match || match.Phase != MatchManager.MatchPhase.Lobby) return;
|
|
|
|
int count = OverlapTrigger();
|
|
if (count == _overlap.Length && !_overlapSaturated)
|
|
{
|
|
_overlapSaturated = true;
|
|
Debug.LogWarning($"StartMatchTrigger: overlap buffer saturated ({count}); the authority avatar may be missed.", this);
|
|
}
|
|
for (int i = 0; i < count; i++)
|
|
{
|
|
var netObj = _overlap[i].GetComponentInParent<NetworkObject>();
|
|
if (!netObj || !netObj.IsPlayerObject) continue; // must be a player avatar
|
|
if (!match.IsMatchAuthority(netObj.OwnerClientId)) continue; // ...the authority's
|
|
|
|
_started = true;
|
|
match.StartMatch();
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Overlaps the trigger's own volume. Uses the oriented box for a BoxCollider (the authored shape);
|
|
// falls back to the collider's world AABB for any other collider type. Triggers are ignored so the
|
|
// query skips this volume itself and reports only solid avatar colliders (e.g. the baked mesh).
|
|
private int OverlapTrigger()
|
|
{
|
|
if (_collider is BoxCollider box)
|
|
{
|
|
Vector3 center = transform.TransformPoint(box.center);
|
|
Vector3 scale = transform.lossyScale;
|
|
Vector3 half = new Vector3(
|
|
Mathf.Abs(box.size.x * scale.x),
|
|
Mathf.Abs(box.size.y * scale.y),
|
|
Mathf.Abs(box.size.z * scale.z)) * 0.5f;
|
|
return Physics.OverlapBoxNonAlloc(
|
|
center, half, _overlap, transform.rotation, ~0, QueryTriggerInteraction.Ignore);
|
|
}
|
|
|
|
Bounds b = _collider.bounds;
|
|
return Physics.OverlapBoxNonAlloc(
|
|
b.center, b.extents, _overlap, Quaternion.identity, ~0, QueryTriggerInteraction.Ignore);
|
|
}
|
|
}
|