using Unity.Netcode; using UnityEngine; /// /// 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 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, whose server-side avatar is a NetworkTransform-driven /// replica - CharacterController disabled (), no Rigidbody, 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()), so 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. /// /// Server-gated: pure clients query nothing. Only the authority avatar starts the match, only during /// the lobby phase, once. The trigger lives in the additively-loaded Lobby and is destroyed when the /// lobby unloads, so a re-match gets a fresh instance with its guard reset. /// [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(); 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(); 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); } }