using GameKit.Dependencies.Utilities; using System; using System.Collections.Generic; using UnityEngine; using TimeManagerCls = FishNet.Managing.Timing.TimeManager; namespace FishNet.Component.Prediction { public abstract class NetworkCollider : NetworkColliderBase { /// /// Called when another collider enters this collider. /// public event Action OnEnter; /// /// Called when another collider stays in this collider. /// public event Action OnStay; /// /// Called when another collider exits this collider. /// public event Action OnExit; /// /// The colliders on this object. /// private Collider[] _colliders; /// /// The hits from the last check. /// private Collider[] _hits; /// /// Colliders which are entered for a tick, be it stay or for the first time. /// private Dictionary> _enteredColliders; protected override void Awake() { base.Awake(); _enteredColliders = CollectionCaches>.RetrieveDictionary(); _hits = CollectionCaches.RetrieveArray(); if (_hits.Length < MaximumSimultaneousHits) _hits = new Collider[MaximumSimultaneousHits]; } public override void OnStopNetwork() { base.OnStopNetwork(); StoreEnteredColliders(keepDictionary: true); _enteredColliders?.Clear(); } protected override void OnDestroy() { base.OnDestroy(); StoreEnteredColliders(keepDictionary: false); CollectionCaches.StoreAndDefault(ref _hits, _hits.Length); } /// /// Called by the PredictionManager immediately before a reconcile begins. /// protected override void PredictionManager_OnPostPhysicsTransformSync(uint clientTick, uint serverTick) { if (IsStopping) return; if (clientTick > 0) { List keysToRemove = CollectionCaches.RetrieveList(); uint maximumTick = clientTick - 2; foreach (uint enteredTick in _enteredColliders.Keys) { if (enteredTick < maximumTick) keysToRemove.Add(enteredTick); } foreach (uint tick in keysToRemove) { HashSet colliders = _enteredColliders[tick]; CollectionCaches.Store(colliders); _enteredColliders.Remove(tick); } CollectionCaches.Store(keysToRemove); } /* Call base only after removing old entries. This ensures old entries are removed * before CheckColliders is called. */ base.PredictionManager_OnPostPhysicsTransformSync(clientTick, serverTick); } /// /// Removes a client from the entered state, if present. /// /// This may be used to force OnEnter to trigger again while previously ignored, such as if collisions were time restricted. public void RemoveEnteredCollider(Collider c) { List keysToRemove = CollectionCaches.RetrieveList(); foreach (KeyValuePair> kvp in _enteredColliders) { kvp.Value.Remove(c); if (kvp.Value.Count == 0) keysToRemove.Add(kvp.Key); } foreach (uint tick in keysToRemove) { HashSet colliders = _enteredColliders[tick]; CollectionCaches.Store(colliders); _enteredColliders.Remove(tick); } CollectionCaches.Store(keysToRemove); } /// /// Checks for any collider changes; /// protected override void CheckColliders(uint localTick) { // Initial checks failed. if (!TryPrepareColliderCheck(localTick)) return; HashSet current = CollectionCaches.RetrieveHashSet(); /* Previous may not be set here if there were * no collisions during the previous tick. */ // The rotation of the object for box colliders. Quaternion rotation = transform.rotation; // Check each collider for triggers. foreach (Collider col in _colliders) { if (!col.enabled) continue; if (IsTrigger != col.isTrigger) continue; // Number of hits from the checks. int hits; if (col is SphereCollider sphereCollider) hits = GetSphereColliderHits(sphereCollider, InteractableLayers); else if (col is CapsuleCollider capsuleCollider) hits = GetCapsuleColliderHits(capsuleCollider, InteractableLayers); else if (col is BoxCollider boxCollider) hits = GetBoxColliderHits(boxCollider, rotation, InteractableLayers); else hits = 0; /* Check hits for enter/exit callbacks. */ for (int i = 0; i < hits; i++) { Collider hit = _hits[i]; if (hit == null || hit == col) continue; /* An object may be hit through several of its own colliders; it should only be * reported once. The collider with the lowest instanceId represents the object, * a stable choice across ticks so a single object never causes duplicate * enter/exit callbacks. */ // if (IsDuplicateObjectHit(hit, i, hits, col)) // continue; current.Add(hit); } /* If the colliders already exist then the tick is being * run again, which would indicate this is being run during a reconcile. * * Since this key will have its data replaced with current, store the prior collection.*/ if (_enteredColliders.TryGetValueIL2CPP(localTick, out HashSet enteredColliders)) { CollectionCaches.Store(enteredColliders); _enteredColliders.Remove(localTick); } const uint unsetLastTick = uint.MaxValue; uint lastTick = localTick > 1 ? localTick - 1 : unsetLastTick; _enteredColliders.TryGetValueIL2CPP(lastTick, out HashSet lastEnteredColliders); /* If there are entered colliders then * update enteredColliders for the tick. */ if (current.Count > 0) { _enteredColliders[localTick] = current; /* If there were no colliders last tick * then without a doubt enter should be called since * the collider could not possibly be present already. */ if (lastEnteredColliders == null) { //Invoke OnEnter for every collider in current. foreach (Collider c in current) InvokeOnEnter(c, localTick); } /* If the last collection is found then * check to invoke Enter or Stay. */ else { foreach (Collider c in current) { if (lastEnteredColliders.Contains(c)) OnStay?.Invoke(c, localTick); else InvokeOnEnter(c, localTick); } } } //If current is empty the collection can be stored. else { CollectionCaches.Store(current); } /* Check to invoke OnExit. */ if (lastEnteredColliders != null) { /* If current does not have the colliders from * the last tick, then an exit has occurred. */ foreach (Collider c in lastEnteredColliders) { if (!current.Contains(c)) OnExit?.Invoke(c, localTick); } } /* If the server is started the lastEnteredColliders can * be discarded since the server will never reconcile, and * will never need to check them again. */ if (IsServerStarted) { if (lastTick is not unsetLastTick && _enteredColliders.TryGetValueIL2CPP(lastTick, out HashSet lEnteredColliders)) { CollectionCaches.Store(lEnteredColliders); _enteredColliders.Remove(lastTick); } } } } /// /// Invokes OnEnter, and OnStay if configured. /// private void InvokeOnEnter(Collider c, uint tick) { OnEnter?.Invoke(c, tick); if (InvokeStayOnEnter) OnStay?.Invoke(c, tick); } /// /// Checks for Sphere collisions. /// /// Number of colliders hit. private int GetSphereColliderHits(SphereCollider sphereCollider, int layerMask) { sphereCollider.GetSphereOverlapParams(out Vector3 center, out float radius); radius += AdditionalSize; return gameObject.scene.GetPhysicsScene().OverlapSphere(center, radius, _hits, layerMask, QueryTriggerInteraction.UseGlobal); } /// /// Checks for Capsule collisions. /// /// Number of colliders hit. private int GetCapsuleColliderHits(CapsuleCollider capsuleCollider, int layerMask) { capsuleCollider.GetCapsuleCastParams(out Vector3 start, out Vector3 end, out float radius); radius += AdditionalSize; return gameObject.scene.GetPhysicsScene().OverlapCapsule(start, end, radius, _hits, layerMask, QueryTriggerInteraction.UseGlobal); } /// /// Checks for Box collisions. /// /// Number of colliders hit. private int GetBoxColliderHits(BoxCollider boxCollider, Quaternion rotation, int layerMask) { boxCollider.GetBoxOverlapParams(out Vector3 center, out Vector3 halfExtents); Vector3 additional = Vector3.one * AdditionalSize; halfExtents += additional; return gameObject.scene.GetPhysicsScene().OverlapBox(center, halfExtents, _hits, rotation, layerMask, QueryTriggerInteraction.UseGlobal); } /// /// Returns true when another hit from the same check belongs to the same object as and represents that /// object, meaning this hit is a duplicate and should be ignored. The collider with the lowest instanceId is the representative, /// a stable choice across ticks so one object never registers through more than one of its colliders. /// /// True if this hit should be skipped as a duplicate of the same object. private bool IsDuplicateObjectHit(Collider hit, int index, int hitCount, Collider self) { for (int i = 0; i < hitCount; i++) { if (i == index) continue; Collider other = _hits[i]; if (other == null || other == self || other == hit) continue; #if UNITY_6000_5_OR_NEWER if (IsSameObject(hit, other) && other.GetEntityId() < hit.GetEntityId()) #else if (IsSameObject(hit, other) && other.GetInstanceID() < hit.GetInstanceID()) #endif return true; } return false; } /// /// Returns whether two colliders belong to the same object, grouped by their attached rigidbody when either has one and /// otherwise by their transform root. /// /// True if both colliders are on the same object. private static bool IsSameObject(Collider a, Collider b) { Rigidbody aRigidbody = a.attachedRigidbody; Rigidbody bRigidbody = b.attachedRigidbody; if (aRigidbody != null || bRigidbody != null) return aRigidbody == bRigidbody; return a.transform.root == b.transform.root; } /// /// Finds colliders on this object to check. /// /// True to set colliders again even if already found. This action will clear stored collider states. /// True if colliders should be found again. public override bool TryFindColliders(bool force = false) { if (!base.TryFindColliders(force)) return false; ClearColliderDataHistory(invokeOnExit: true); _colliders = GetComponents(); return true; } /// /// Resets this NetworkBehaviour so that it may be added to an object pool. /// public override void ResetState(bool asServer) { ClearColliderDataHistory(invokeOnExit: true); base.ResetState(asServer); } /// /// Clears stored collider states. /// /// True to invoke OnExit if a collider is stored in the OnEntered state. When called during a reconcile this used the current ClientReplayTick, otherwise uses LocalTick. protected override void ClearColliderDataHistory(bool invokeOnExit) { if (_enteredColliders == null) return; /* Data needs to exist to iterate, and managers are needed * to get the proper tick to invoke. */ if (invokeOnExit) { uint largestTick = 0; foreach (uint tick in _enteredColliders.Keys) largestTick = Math.Max(tick, largestTick); if (_enteredColliders.TryGetValueIL2CPP(largestTick, out HashSet colliders)) { if (colliders != null) { foreach (Collider c in colliders) OnExit?.Invoke(c, TimeManagerCls.UNSET_TICK); } } } StoreEnteredColliders(keepDictionary: true); _enteredColliders.Clear(); } /// /// Stores each Collider HashSet within EnteredColliders. /// private void StoreEnteredColliders(bool keepDictionary) { foreach (HashSet colliders in _enteredColliders.Values) CollectionCaches.Store(colliders); if (!keepDictionary) CollectionCaches>.Store(_enteredColliders); } } }