482 lines
18 KiB
C#
482 lines
18 KiB
C#
using FishNet.Managing;
|
|
using FishNet.Object;
|
|
using GameKit.Dependencies.Utilities;
|
|
using GameKit.Dependencies.Utilities.Types;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Runtime.CompilerServices;
|
|
using UnityEngine;
|
|
using TimeManagerCls = FishNet.Managing.Timing.TimeManager;
|
|
|
|
namespace FishNet.Component.Prediction
|
|
{
|
|
|
|
public abstract class NetworkCollider : NetworkBehaviour
|
|
{
|
|
#if !PREDICTION_1
|
|
#region Types.
|
|
private struct ColliderData : IResettable
|
|
{
|
|
/// <summary>
|
|
/// Tick which the collisions happened.
|
|
/// </summary>
|
|
public uint Tick;
|
|
/// <summary>
|
|
/// Hits for Tick.
|
|
/// </summary>
|
|
public HashSet<Collider> Hits;
|
|
|
|
public ColliderData(uint tick, HashSet<Collider> hits)
|
|
{
|
|
Tick = tick;
|
|
Hits = hits;
|
|
}
|
|
|
|
public void InitializeState() { }
|
|
public void ResetState()
|
|
{
|
|
Tick = TimeManagerCls.UNSET_TICK;
|
|
CollectionCaches<Collider>.StoreAndDefault(ref Hits);
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
/// <summary>
|
|
/// Called when another collider enters this collider.
|
|
/// </summary>
|
|
public event Action<Collider> OnEnter;
|
|
/// <summary>
|
|
/// Called when another collider stays in this collider.
|
|
/// </summary>
|
|
public event Action<Collider> OnStay;
|
|
/// <summary>
|
|
/// Called when another collider exits this collider.
|
|
/// </summary>
|
|
public event Action<Collider> OnExit;
|
|
/// <summary>
|
|
/// True to run collisions for colliders which are triggers, false to run collisions for colliders which are not triggers.
|
|
/// </summary>
|
|
[HideInInspector]
|
|
protected bool IsTrigger;
|
|
/// <summary>
|
|
/// The maximum number of simultaneous hits to check for.
|
|
/// </summary>
|
|
[SerializeField]
|
|
private ushort _maximumSimultaneousHits = 16;
|
|
|
|
/// <summary>
|
|
/// The duration of the history.
|
|
/// </summary>
|
|
[SerializeField]
|
|
private float _historyDuration = 0.5f;
|
|
|
|
/// <summary>
|
|
/// The colliders on this object.
|
|
/// </summary>
|
|
private Collider[] _colliders;
|
|
/// <summary>
|
|
/// The hits from the last check.
|
|
/// </summary>
|
|
private Collider[] _hits;
|
|
/// <summary>
|
|
/// The history of collider data.
|
|
/// </summary>
|
|
private ResettableRingBuffer<ColliderData> _colliderDataHistory;
|
|
/// <summary>
|
|
/// True if colliders have been searched for at least once.
|
|
/// We cannot check the null state on _colliders because Unity has a habit of initializing collections on it's own.
|
|
/// </summary>
|
|
private bool _collidersFound;
|
|
/// <summary>
|
|
/// True to cache collision histories for comparing start and exits.
|
|
/// </summary>
|
|
private bool _useCache => (OnEnter != null || OnExit != null);
|
|
/// <summary>
|
|
/// Last layer of the gameObject.
|
|
/// </summary>
|
|
private int _lastGameObjectLayer = -1;
|
|
/// <summary>
|
|
/// Interactable layers for the layer of this gameObject.
|
|
/// </summary>
|
|
private int _interactableLayers;
|
|
|
|
protected virtual void Awake()
|
|
{
|
|
_colliderDataHistory = ResettableCollectionCaches<ColliderData>.RetrieveRingBuffer();
|
|
_hits = CollectionCaches<Collider>.RetrieveArray();
|
|
if (_hits.Length < _maximumSimultaneousHits)
|
|
_hits = new Collider[_maximumSimultaneousHits];
|
|
}
|
|
|
|
private void OnDestroy()
|
|
{
|
|
ResettableCollectionCaches<ColliderData>.StoreAndDefault(ref _colliderDataHistory);
|
|
CollectionCaches<Collider>.StoreAndDefault(ref _hits, -_hits.Length);
|
|
}
|
|
|
|
public override void OnStartNetwork()
|
|
{
|
|
FindColliders();
|
|
|
|
//Initialize the ringbuffer. Server only needs 1 tick worth of history.
|
|
uint historyTicks = (base.IsServerStarted) ? 1 : TimeManager.TimeToTicks(_historyDuration);
|
|
_colliderDataHistory.Initialize((int)historyTicks);
|
|
|
|
//Events needed by server and client.
|
|
TimeManager.OnPostPhysicsSimulation += TimeManager_OnPostPhysicsSimulation;
|
|
}
|
|
|
|
public override void OnStartClient()
|
|
{
|
|
//Events only needed by the client.
|
|
PredictionManager.OnPostReplicateReplay += PredictionManager_OnPostReplicateReplay;
|
|
}
|
|
|
|
public override void OnStopClient()
|
|
{
|
|
//Events only needed by the client.
|
|
PredictionManager.OnPostReplicateReplay -= PredictionManager_OnPostReplicateReplay;
|
|
|
|
}
|
|
|
|
public override void OnStopNetwork()
|
|
{
|
|
TimeManager.OnPostPhysicsSimulation -= TimeManager_OnPostPhysicsSimulation;
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// When using TimeManager for physics timing, this is called immediately after the physics simulation has occured for the tick.
|
|
/// While using Unity for physics timing, this is called during Update, only if a physics frame.
|
|
/// This may be useful if you wish to run physics differently for stacked scenes.
|
|
private void TimeManager_OnPostPhysicsSimulation(float delta)
|
|
{
|
|
CheckColliders(TimeManager.LocalTick, false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called after physics is simulated when replaying a replicate method.
|
|
/// </summary>
|
|
private void PredictionManager_OnPostReplicateReplay(uint clientTick, uint serverTick)
|
|
{
|
|
CheckColliders(clientTick, true);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Cleans history up to, while excluding tick.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private void CleanHistory(uint tick)
|
|
{
|
|
if (_useCache)
|
|
{
|
|
int removeCount = 0;
|
|
int historyCount = _colliderDataHistory.Count;
|
|
for (int i = 0; i < historyCount; i++)
|
|
{
|
|
if (_colliderDataHistory[i].Tick >= tick)
|
|
break;
|
|
removeCount++;
|
|
}
|
|
|
|
for (int i = 0; i < removeCount; i++)
|
|
_colliderDataHistory[i].ResetState();
|
|
_colliderDataHistory.RemoveRange(true, removeCount);
|
|
}
|
|
//Cache is not used.
|
|
else
|
|
{
|
|
ClearColliderDataHistory();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Units to extend collision traces by. This is used to prevent missed overlaps when colliders do not intersect enough.
|
|
/// </summary>
|
|
public virtual float GetAdditionalSize() => 0f;
|
|
|
|
/// <summary>
|
|
/// Checks for any trigger changes;
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private void CheckColliders(uint tick, bool replay)
|
|
{
|
|
//Should not be possible as tick always starts on 1.
|
|
if (tick == TimeManagerCls.UNSET_TICK)
|
|
return;
|
|
|
|
const int INVALID_HISTORY_VALUE = -1;
|
|
|
|
HashSet<Collider> current = CollectionCaches<Collider>.RetrieveHashSet();
|
|
HashSet<Collider> previous = null;
|
|
|
|
int previousHitsIndex = INVALID_HISTORY_VALUE;
|
|
/* Server only keeps 1 history so
|
|
* if server is started then
|
|
* simply clean one. When the server is
|
|
* started replay will never be true, so this
|
|
* will only call once per tick. */
|
|
if (base.IsServerStarted && tick > 0)
|
|
CleanHistory(tick - 1);
|
|
|
|
if (_useCache)
|
|
{
|
|
if (replay)
|
|
{
|
|
previousHitsIndex = GetHistoryIndex(tick - 1, false);
|
|
if (previousHitsIndex != -1)
|
|
previous = _colliderDataHistory[previousHitsIndex].Hits;
|
|
}
|
|
//Not replaying.
|
|
else
|
|
{
|
|
if (_colliderDataHistory.Count > 0)
|
|
{
|
|
ColliderData cd = _colliderDataHistory[_colliderDataHistory.Count - 1];
|
|
/* If the hit tick one before current then it can be used, otherwise
|
|
* use a new collection for previous. */
|
|
if (cd.Tick == (tick - 1))
|
|
previous = cd.Hits;
|
|
}
|
|
}
|
|
}
|
|
//Not using history, clear it all.
|
|
else
|
|
{
|
|
ClearColliderDataHistory();
|
|
}
|
|
|
|
/* 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;
|
|
|
|
//If layer changed then get new interactableLayers.
|
|
if (_lastGameObjectLayer != gameObject.layer)
|
|
{
|
|
_lastGameObjectLayer = gameObject.layer;
|
|
_interactableLayers = Layers.GetInteractableLayersValue(_lastGameObjectLayer);
|
|
}
|
|
|
|
// 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 the hits for triggers.
|
|
for (int i = 0; i < hits; i++)
|
|
{
|
|
Collider hit = _hits[i];
|
|
if (hit == null || hit == col)
|
|
continue;
|
|
|
|
/* If not in previous then add and
|
|
* invoke enter. */
|
|
if (previous == null || !previous.Contains(hit))
|
|
OnEnter?.Invoke(hit);
|
|
|
|
//Also add to current hits.
|
|
current.Add(hit);
|
|
OnStay?.Invoke(hit);
|
|
}
|
|
}
|
|
|
|
if (previous != null)
|
|
{
|
|
//Check for stays and exits.
|
|
foreach (Collider col in previous)
|
|
{
|
|
//If it was in previous but not current, it has exited.
|
|
if (!current.Contains(col))
|
|
OnExit?.Invoke(col);
|
|
}
|
|
}
|
|
|
|
//If not using the cache then clean up collections.
|
|
if (_useCache)
|
|
{
|
|
//If not replaying add onto the end. */
|
|
if (!replay)
|
|
{
|
|
AddToEnd();
|
|
}
|
|
/* If a replay then set current colliders
|
|
* to one entry past historyIndex. If the next entry
|
|
* beyond historyIndex is for the right tick it can be
|
|
* updated, otherwise a result has to be inserted. */
|
|
else
|
|
{
|
|
/* Previous hits was not found in history so we
|
|
* cannot assume current results go right after the previousIndex.
|
|
* Find whichever index is the closest to tick and return it.
|
|
*
|
|
* If an exact match is not found for tick then the entry just after
|
|
* tick will be returned. This will let us insert current hits right
|
|
* before that entry. */
|
|
if (previousHitsIndex == -1)
|
|
{
|
|
int currentIndex = GetHistoryIndex(tick, true);
|
|
AddDataToIndex(currentIndex);
|
|
}
|
|
//If previous hits are known then the index to update is right after previous index.
|
|
else
|
|
{
|
|
int insertIndex = (previousHitsIndex + 1);
|
|
/* InsertIndex is out of bounds which means
|
|
* to add onto the end. */
|
|
if (insertIndex >= _colliderDataHistory.Count)
|
|
AddToEnd();
|
|
//Not the last entry to insert in the middle.
|
|
else
|
|
AddDataToIndex(insertIndex);
|
|
}
|
|
|
|
/* Adds data to an index. If the tick
|
|
* matches on index with the current tick then
|
|
* replace the entry. Otherwise insert to the
|
|
* correct location. */
|
|
void AddDataToIndex(int index)
|
|
{
|
|
ColliderData colliderData = new ColliderData(tick, current);
|
|
/* If insertIndex is the same tick then replace, otherwise
|
|
* put in front of. */
|
|
//Replace.
|
|
if (_colliderDataHistory[index].Tick == tick)
|
|
{
|
|
_colliderDataHistory[index].ResetState();
|
|
_colliderDataHistory[index] = colliderData;
|
|
}
|
|
//Insert before.
|
|
else
|
|
{
|
|
_colliderDataHistory.Insert(index, colliderData);
|
|
}
|
|
}
|
|
}
|
|
|
|
void AddToEnd()
|
|
{
|
|
ColliderData colliderData = new ColliderData(tick, current);
|
|
_colliderDataHistory.Add(colliderData);
|
|
}
|
|
|
|
}
|
|
/* If not using caching then store results from this run. */
|
|
else
|
|
{
|
|
CollectionCaches<Collider>.Store(current);
|
|
}
|
|
|
|
//Returns history index for a tick.
|
|
/* GetClosest will return the closest match which is
|
|
* past lTick if lTick could not be found. */
|
|
int GetHistoryIndex(uint lTick, bool getClosest)
|
|
{
|
|
for (int i = 0; i < _colliderDataHistory.Count; i++)
|
|
{
|
|
uint localTick = _colliderDataHistory[i].Tick;
|
|
if (localTick == lTick)
|
|
return i;
|
|
/* Tick is too high, any further results
|
|
* will also be too high. */
|
|
if (localTick > tick)
|
|
{
|
|
if (getClosest)
|
|
return i;
|
|
else
|
|
return INVALID_HISTORY_VALUE;
|
|
}
|
|
}
|
|
|
|
//Fall through.
|
|
return INVALID_HISTORY_VALUE;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks for Sphere collisions.
|
|
/// </summary>
|
|
/// <returns>Number of colliders hit.</returns>
|
|
private int GetSphereColliderHits(SphereCollider sphereCollider, int layerMask)
|
|
{
|
|
sphereCollider.GetSphereOverlapParams(out Vector3 center, out float radius);
|
|
radius += GetAdditionalSize();
|
|
return Physics.OverlapSphereNonAlloc(center, radius, _hits, layerMask);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks for Capsule collisions.
|
|
/// </summary>
|
|
/// <returns>Number of colliders hit.</returns>
|
|
private int GetCapsuleColliderHits(CapsuleCollider capsuleCollider, int layerMask)
|
|
{
|
|
capsuleCollider.GetCapsuleCastParams(out Vector3 start, out Vector3 end, out float radius);
|
|
radius += GetAdditionalSize();
|
|
return Physics.OverlapCapsuleNonAlloc(start, end, radius, _hits, layerMask);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks for Box collisions.
|
|
/// </summary>
|
|
/// <returns>Number of colliders hit.</returns>
|
|
private int GetBoxColliderHits(BoxCollider boxCollider, Quaternion rotation, int layerMask)
|
|
{
|
|
|
|
boxCollider.GetBoxOverlapParams(out Vector3 center, out Vector3 halfExtents);
|
|
Vector3 additional = (Vector3.one * GetAdditionalSize());
|
|
halfExtents += additional;
|
|
return Physics.OverlapBoxNonAlloc(center, halfExtents, _hits, rotation, layerMask);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Finds colliders to use.
|
|
/// <paramref name="rebuild"/>True to rebuild the colliders even if they are already populated.
|
|
/// </summary>
|
|
public void FindColliders(bool rebuild = false)
|
|
{
|
|
if (_collidersFound && !rebuild)
|
|
return;
|
|
_collidersFound = true;
|
|
|
|
_colliders = GetComponents<Collider>();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resets this NetworkBehaviour so that it may be added to an object pool.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public override void ResetState(bool asServer)
|
|
{
|
|
base.ResetState(asServer);
|
|
ClearColliderDataHistory();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resets datas in collider data history and clears collection.
|
|
/// </summary>
|
|
private void ClearColliderDataHistory()
|
|
{
|
|
foreach (ColliderData cd in _colliderDataHistory)
|
|
cd.ResetState();
|
|
_colliderDataHistory.Clear();
|
|
}
|
|
#endif
|
|
}
|
|
|
|
|
|
} |