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 (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 () 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. /// [RequireComponent(typeof(Collider))] [DisallowMultipleComponent] public class StartMatchTrigger : MonoBehaviour { private Collider _collider; private readonly Collider[] _overlap = new Collider[16]; private bool _started; 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(); 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); } }