Add FishNet network library

This commit is contained in:
2026-09-17 20:29:57 +08:00
parent b0e708d54c
commit 668121e35c
2015 changed files with 213270 additions and 0 deletions
@@ -0,0 +1,409 @@
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
{
/// <summary>
/// Called when another collider enters this collider.
/// </summary>
public event Action<Collider, uint> OnEnter;
/// <summary>
/// Called when another collider stays in this collider.
/// </summary>
public event Action<Collider, uint> OnStay;
/// <summary>
/// Called when another collider exits this collider.
/// </summary>
public event Action<Collider, uint> OnExit;
/// <summary>
/// The colliders on this object.
/// </summary>
private Collider[] _colliders;
/// <summary>
/// The hits from the last check.
/// </summary>
private Collider[] _hits;
/// <summary>
/// Colliders which are entered for a tick, be it stay or for the first time.
/// </summary>
private Dictionary<uint, HashSet<Collider>> _enteredColliders;
protected override void Awake()
{
base.Awake();
_enteredColliders = CollectionCaches<uint, HashSet<Collider>>.RetrieveDictionary();
_hits = CollectionCaches<Collider>.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<Collider>.StoreAndDefault(ref _hits, _hits.Length);
}
/// <summary>
/// Called by the PredictionManager immediately before a reconcile begins.
/// </summary>
protected override void PredictionManager_OnPostPhysicsTransformSync(uint clientTick, uint serverTick)
{
if (IsStopping)
return;
if (clientTick > 0)
{
List<uint> keysToRemove = CollectionCaches<uint>.RetrieveList();
uint maximumTick = clientTick - 2;
foreach (uint enteredTick in _enteredColliders.Keys)
{
if (enteredTick < maximumTick)
keysToRemove.Add(enteredTick);
}
foreach (uint tick in keysToRemove)
{
HashSet<Collider> colliders = _enteredColliders[tick];
CollectionCaches<Collider>.Store(colliders);
_enteredColliders.Remove(tick);
}
CollectionCaches<uint>.Store(keysToRemove);
}
/* Call base only after removing old entries. This ensures old entries are removed
* before CheckColliders is called. */
base.PredictionManager_OnPostPhysicsTransformSync(clientTick, serverTick);
}
/// <summary>
/// Removes a client from the entered state, if present.
/// </summary>
/// <remarks>This may be used to force OnEnter to trigger again while previously ignored, such as if collisions were time restricted.</remarks>
public void RemoveEnteredCollider(Collider c)
{
List<uint> keysToRemove = CollectionCaches<uint>.RetrieveList();
foreach (KeyValuePair<uint, HashSet<Collider>> kvp in _enteredColliders)
{
kvp.Value.Remove(c);
if (kvp.Value.Count == 0)
keysToRemove.Add(kvp.Key);
}
foreach (uint tick in keysToRemove)
{
HashSet<Collider> colliders = _enteredColliders[tick];
CollectionCaches<Collider>.Store(colliders);
_enteredColliders.Remove(tick);
}
CollectionCaches<uint>.Store(keysToRemove);
}
/// <summary>
/// Checks for any collider changes;
/// </summary>
protected override void CheckColliders(uint localTick)
{
// Initial checks failed.
if (!TryPrepareColliderCheck(localTick))
return;
HashSet<Collider> current = CollectionCaches<Collider>.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<Collider> enteredColliders))
{
CollectionCaches<Collider>.Store(enteredColliders);
_enteredColliders.Remove(localTick);
}
const uint unsetLastTick = uint.MaxValue;
uint lastTick = localTick > 1 ? localTick - 1 : unsetLastTick;
_enteredColliders.TryGetValueIL2CPP(lastTick, out HashSet<Collider> 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<Collider>.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<Collider> lEnteredColliders))
{
CollectionCaches<Collider>.Store(lEnteredColliders);
_enteredColliders.Remove(lastTick);
}
}
}
}
/// <summary>
/// Invokes OnEnter, and OnStay if configured.
/// </summary>
private void InvokeOnEnter(Collider c, uint tick)
{
OnEnter?.Invoke(c, tick);
if (InvokeStayOnEnter)
OnStay?.Invoke(c, tick);
}
/// <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 += AdditionalSize;
return gameObject.scene.GetPhysicsScene().OverlapSphere(center, radius, _hits, layerMask, QueryTriggerInteraction.UseGlobal);
}
/// <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 += AdditionalSize;
return gameObject.scene.GetPhysicsScene().OverlapCapsule(start, end, radius, _hits, layerMask, QueryTriggerInteraction.UseGlobal);
}
/// <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 * AdditionalSize;
halfExtents += additional;
return gameObject.scene.GetPhysicsScene().OverlapBox(center, halfExtents, _hits, rotation, layerMask, QueryTriggerInteraction.UseGlobal);
}
/// <summary>
/// Returns true when another hit from the same check belongs to the same object as <paramref name = "hit"/> 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.
/// </summary>
/// <returns>True if this hit should be skipped as a duplicate of the same object.</returns>
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;
}
/// <summary>
/// Returns whether two colliders belong to the same object, grouped by their attached rigidbody when either has one and
/// otherwise by their transform root.
/// </summary>
/// <returns>True if both colliders are on the same object.</returns>
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;
}
/// <summary>
/// Finds colliders on this object to check.
/// </summary>
/// <param name = "force">True to set colliders again even if already found. This action will clear stored collider states.</param>
/// <returns>True if colliders should be found again.</returns>
public override bool TryFindColliders(bool force = false)
{
if (!base.TryFindColliders(force))
return false;
ClearColliderDataHistory(invokeOnExit: true);
_colliders = GetComponents<Collider>();
return true;
}
/// <summary>
/// Resets this NetworkBehaviour so that it may be added to an object pool.
/// </summary>
public override void ResetState(bool asServer)
{
ClearColliderDataHistory(invokeOnExit: true);
base.ResetState(asServer);
}
/// <summary>
/// Clears stored collider states.
/// </summary>
/// <param name = "invokeOnExit">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.</param>
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<Collider> colliders))
{
if (colliders != null)
{
foreach (Collider c in colliders)
OnExit?.Invoke(c, TimeManagerCls.UNSET_TICK);
}
}
}
StoreEnteredColliders(keepDictionary: true);
_enteredColliders.Clear();
}
/// <summary>
/// Stores each Collider HashSet within EnteredColliders.
/// </summary>
private void StoreEnteredColliders(bool keepDictionary)
{
foreach (HashSet<Collider> colliders in _enteredColliders.Values)
CollectionCaches<Collider>.Store(colliders);
if (!keepDictionary)
CollectionCaches<uint, HashSet<Collider>>.Store(_enteredColliders);
}
}
}
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 4f977181495644345a4d0d821c31579f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 207815
packageName: 'FishNet: Networking Evolved'
packageVersion: 4.7.3R
assetPath: Assets/FishNet/Runtime/Generated/Component/Prediction/NetworkCollider.cs
uploadId: 987062
@@ -0,0 +1,396 @@
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 NetworkCollider2D : NetworkColliderBase
{
/// <summary>
/// Called when another collider enters this collider.
/// </summary>
public event Action<Collider2D, uint> OnEnter;
/// <summary>
/// Called when another collider stays in this collider.
/// </summary>
public event Action<Collider2D, uint> OnStay;
/// <summary>
/// Called when another collider exits this collider.
/// </summary>
public event Action<Collider2D, uint> OnExit;
/// <summary>
/// The colliders on this object.
/// </summary>
private Collider2D[] _colliders;
/// <summary>
/// The hits from the last check.
/// </summary>
private Collider2D[] _hits;
/// <summary>
/// Colliders which are entered for a tick, be it stay or for the first time.
/// </summary>
private Dictionary<uint, HashSet<Collider2D>> _enteredColliders;
protected override void Awake()
{
base.Awake();
_enteredColliders = CollectionCaches<uint, HashSet<Collider2D>>.RetrieveDictionary();
_hits = CollectionCaches<Collider2D>.RetrieveArray();
if (_hits.Length < MaximumSimultaneousHits)
_hits = new Collider2D[MaximumSimultaneousHits];
}
public override void OnStopNetwork()
{
base.OnStopNetwork();
StoreEnteredColliders(keepDictionary: true);
_enteredColliders?.Clear();
}
protected override void OnDestroy()
{
base.OnDestroy();
CollectionCaches<uint, HashSet<Collider2D>>.StoreAndDefault(ref _enteredColliders);
CollectionCaches<Collider2D>.StoreAndDefault(ref _hits, _hits.Length);
}
/// <summary>
/// Called by the PredictionManager immediately before a reconcile begins.
/// </summary>
protected override void PredictionManager_OnPostPhysicsTransformSync(uint clientTick, uint serverTick)
{
if (IsStopping)
return;
if (clientTick > 0)
{
List<uint> keysToRemove = CollectionCaches<uint>.RetrieveList();
uint maximumTick = clientTick - 2;
foreach (uint enteredTick in _enteredColliders.Keys)
{
if (enteredTick < maximumTick)
keysToRemove.Add(enteredTick);
}
foreach (uint tick in keysToRemove)
{
HashSet<Collider2D> colliders = _enteredColliders[tick];
CollectionCaches<Collider2D>.Store(colliders);
_enteredColliders.Remove(tick);
}
CollectionCaches<uint>.Store(keysToRemove);
}
/* Call base only after removing old entries. This ensures old entries are removed
* before CheckColliders is called. */
base.PredictionManager_OnPostPhysicsTransformSync(clientTick, serverTick);
}
/// <summary>
/// Removes a client from the entered state, if present.
/// </summary>
/// <remarks>This may be used to force OnEnter to trigger again while previously ignored, such as if collisions were time restricted.</remarks>
public void RemoveEnteredCollider(Collider2D c)
{
List<uint> keysToRemove = CollectionCaches<uint>.RetrieveList();
foreach (KeyValuePair<uint, HashSet<Collider2D>> kvp in _enteredColliders)
{
kvp.Value.Remove(c);
if (kvp.Value.Count == 0)
keysToRemove.Add(kvp.Key);
}
foreach (uint tick in keysToRemove)
{
HashSet<Collider2D> colliders = _enteredColliders[tick];
CollectionCaches<Collider2D>.Store(colliders);
_enteredColliders.Remove(tick);
}
CollectionCaches<uint>.Store(keysToRemove);
}
/// <summary>
/// Checks for any collider changes;
/// </summary>
protected override void CheckColliders(uint localTick)
{
// Initial checks failed.
if (!TryPrepareColliderCheck(localTick))
return;
HashSet<Collider2D> current = CollectionCaches<Collider2D>.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 (Collider2D col in _colliders)
{
if (!col.enabled)
continue;
if (IsTrigger != col.isTrigger)
continue;
// Number of hits from the checks.
// Number of hits from the checks.
int hits;
if (col is CircleCollider2D circleCollider)
hits = GetCircleCollider2DHits(circleCollider, InteractableLayers);
else if (col is BoxCollider2D boxCollider)
hits = GetBoxCollider2DHits(boxCollider, rotation, InteractableLayers);
else
hits = PhysicsScene2D.OverlapCollider(col, _hits, InteractableLayers);
/* Check hits for enter/exit callbacks. */
for (int i = 0; i < hits; i++)
{
Collider2D 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<Collider2D> enteredColliders))
{
CollectionCaches<Collider2D>.Store(enteredColliders);
_enteredColliders.Remove(localTick);
}
const uint unsetLastTick = uint.MaxValue;
uint lastTick = localTick > 1 ? localTick - 1 : unsetLastTick;
_enteredColliders.TryGetValueIL2CPP(lastTick, out HashSet<Collider2D> 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 (Collider2D c in current)
InvokeOnEnter(c, localTick);
}
/* If the last collection is found then
* check to invoke Enter or Stay. */
else
{
foreach (Collider2D 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<Collider2D>.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 (Collider2D 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<Collider2D> lEnteredColliders))
{
CollectionCaches<Collider2D>.Store(lEnteredColliders);
_enteredColliders.Remove(lastTick);
}
}
}
}
/// <summary>
/// Invokes OnEnter, and OnStay if configured.
/// </summary>
private void InvokeOnEnter(Collider2D c, uint localTick)
{
OnEnter?.Invoke(c, localTick);
if (InvokeStayOnEnter)
OnStay?.Invoke(c, localTick);
}
/// <summary>
/// Checks for circle collisions.
/// </summary>
/// <returns>Number of colliders hit.</returns>
private int GetCircleCollider2DHits(CircleCollider2D circleCollider, int layerMask)
{
circleCollider.GetCircleOverlapParams(out Vector3 center, out float radius);
radius += AdditionalSize;
return gameObject.scene.GetPhysicsScene2D().OverlapCircle(center, radius, _hits, layerMask);
}
/// <summary>
/// Checks for Box collisions.
/// </summary>
/// <returns>Number of colliders hit.</returns>
private int GetBoxCollider2DHits(BoxCollider2D boxCollider, Quaternion rotation, int layerMask)
{
boxCollider.GetBox2DOverlapParams(out Vector3 center, out Vector3 halfExtents);
Vector3 additional = Vector3.one * AdditionalSize;
halfExtents += additional;
return gameObject.scene.GetPhysicsScene2D().OverlapBox(center, halfExtents, rotation.z, _hits, layerMask);
}
/// <summary>
/// Returns true when another hit from the same check belongs to the same object as <paramref name = "hit"/> 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.
/// </summary>
/// <returns>True if this hit should be skipped as a duplicate of the same object.</returns>
private bool IsDuplicateObjectHit(Collider2D hit, int index, int hitCount, Collider2D self)
{
for (int i = 0; i < hitCount; i++)
{
if (i == index)
continue;
Collider2D 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;
}
/// <summary>
/// Returns whether two colliders belong to the same object, grouped by their attached rigidbody when either has one and
/// otherwise by their transform root.
/// </summary>
/// <returns>True if both colliders are on the same object.</returns>
private static bool IsSameObject(Collider2D a, Collider2D b)
{
Rigidbody2D aRigidbody = a.attachedRigidbody;
Rigidbody2D bRigidbody = b.attachedRigidbody;
if (aRigidbody != null || bRigidbody != null)
return aRigidbody == bRigidbody;
return a.transform.root == b.transform.root;
}
/// <summary>
/// Finds colliders on this object to check.
/// </summary>
/// <param name = "force">True to set colliders again even if already found. This action will clear stored collider states.</param>
/// <returns>True if colliders should be found again.</returns>
public override bool TryFindColliders(bool force = false)
{
if (!base.TryFindColliders(force))
return false;
ClearColliderDataHistory(invokeOnExit: true);
_colliders = GetComponents<Collider2D>();
return true;
}
/// <summary>
/// Resets this NetworkBehaviour so that it may be added to an object pool.
/// </summary>
public override void ResetState(bool asServer)
{
ClearColliderDataHistory(invokeOnExit: true);
base.ResetState(asServer);
}
/// <summary>
/// Clears stored collider states.
/// </summary>
/// <param name = "invokeOnExit">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.</param>
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<Collider2D> colliders))
{
if (colliders != null)
{
foreach (Collider2D c in colliders)
OnExit?.Invoke(c, TimeManagerCls.UNSET_TICK);
}
}
}
StoreEnteredColliders(keepDictionary: true);
_enteredColliders.Clear();
}
/// <summary>
/// Stores each Collider HashSet within EnteredColliders.
/// </summary>
private void StoreEnteredColliders(bool keepDictionary)
{
foreach (HashSet<Collider2D> colliders in _enteredColliders.Values)
CollectionCaches<Collider2D>.Store(colliders);
if (!keepDictionary)
CollectionCaches<uint, HashSet<Collider2D>>.Store(_enteredColliders);
}
}
}
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: df45081143314fd469a062670a0fe1ea
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 207815
packageName: 'FishNet: Networking Evolved'
packageVersion: 4.7.3R
assetPath: Assets/FishNet/Runtime/Generated/Component/Prediction/NetworkCollider2D.cs
uploadId: 987062
@@ -0,0 +1,175 @@
using System;
using FishNet.Object;
using GameKit.Dependencies.Utilities;
using UnityEngine;
using UnityEngine.Serialization;
using TimeManagerCls = FishNet.Managing.Timing.TimeManager;
namespace FishNet.Component.Prediction
{
public abstract class NetworkColliderBase : NetworkBehaviour
{
/// <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>
/// Maximum number of simultaneous hits to check for. Larger values decrease performance but allow detection to work for more overlapping colliders. Typically the default value of 16 is more than sufficient.
/// </summary>
[FormerlySerializedAs("_maximumSimultaneousHits")]
[Tooltip("Maximum number of simultaneous hits to check for. Larger values decrease performance but allow detection to work for more overlapping colliders. Typically the default value of 16 is more than sufficient.")]
[SerializeField]
protected ushort MaximumSimultaneousHits = 16;
/// <summary>
/// Units to extend collision traces by. This is used to prevent missed overlaps when colliders do not intersect enough.
/// </summary>
[FormerlySerializedAs("_additionalSize")]
[Tooltip("Units to extend collision traces by. This is used to prevent missed overlaps when colliders do not intersect enough.")]
[Range(0f, 100f)]
[SerializeField]
protected float AdditionalSize = 0.1f;
/// <summary>
/// Layers to trace on. This is used when value is not nothing.
/// </summary>
[FormerlySerializedAs("_layers")]
[Tooltip("Layers to trace on. This is used when value is not nothing.")]
[SerializeField]
protected LayerMask Layers = (LayerMask)0;
/// <summary>
/// True to invoke OnStay when OnEnter fires.
/// </summary>
[Tooltip("True to invoke OnStay when OnEnter fires.")]
[SerializeField]
protected bool InvokeStayOnEnter;
/// <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>
/// Last layer of the gameObject.
/// </summary>
private int _lastGameObjectLayer = -1;
/// <summary>
/// Interactable layers for the layer of this gameObject.
/// </summary>
[HideInInspector]
protected int InteractableLayers;
/// <summary>
/// True if stopping or destroying.
/// </summary>
[HideInInspector]
protected bool IsStopping;
protected virtual void Awake()
{
TryFindColliders(force: true);
}
public override void OnStartNetwork()
{
IsStopping = false;
// Events needed by server and client.
TimeManager.OnPostPhysicsSimulation += TimeManager_OnPostPhysicsSimulation;
}
public override void OnStartClient()
{
// Events only needed by the client.
PredictionManager.OnPostPhysicsTransformSync += PredictionManager_OnPostPhysicsTransformSync;
}
public override void OnStopClient()
{
// Events only needed by the client.
PredictionManager.OnPostPhysicsTransformSync -= PredictionManager_OnPostPhysicsTransformSync;
}
public override void OnStopNetwork()
{
IsStopping = true;
TimeManager.OnPostPhysicsSimulation -= TimeManager_OnPostPhysicsSimulation;
}
protected virtual void OnDestroy()
{
IsStopping = true;
if (TimeManager != null)
TimeManager.OnPostPhysicsSimulation -= TimeManager_OnPostPhysicsSimulation;
if (PredictionManager != null)
PredictionManager.OnPostPhysicsTransformSync -= PredictionManager_OnPostPhysicsTransformSync;
}
protected virtual void PredictionManager_OnPostPhysicsTransformSync(uint clientTick, uint serverTick)
{
CheckColliders(clientTick);
}
/// <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)
{
uint tick = IsServerStarted || !PredictionManager.IsReconciling ? TimeManager.LocalTick : PredictionManager.ClientReplayTick;
CheckColliders(tick);
}
/// <summary>
/// Returns if colliders should be checked. If colliders can be checked data needed by all collider checks (2D and 3D) is set.
/// </summary>
/// <returns>True if collision checking should proceed, false if not.</returns>
protected bool TryPrepareColliderCheck(uint tick)
{
if (IsStopping)
return false;
// Should not be possible as tick always starts on 1.
if (tick == TimeManagerCls.UNSET_TICK)
return false;
/* Previous may not be set here if there were
* no collisions during the previous tick. */
// If layers are specified then do not use GOs layers, use specified.
if (Layers != (LayerMask)0)
{
InteractableLayers = Layers;
}
// Use GOs layers.
else
{
int currentLayer = gameObject.layer;
if (_lastGameObjectLayer != currentLayer)
{
_lastGameObjectLayer = currentLayer;
InteractableLayers = GameKit.Dependencies.Utilities.Layers.GetInteractableLayersValue(currentLayer);
}
}
return true;
}
/// <summary>
/// Implement collider checking logic within this method.
/// </summary>
protected abstract void CheckColliders(uint localTick);
/// <summary>
/// Clears stored collider states.
/// </summary>
/// <param name = "invokeOnExit">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.</param>
protected abstract void ClearColliderDataHistory(bool invokeOnExit);
/// <summary>
/// Finds colliders on this object to check.
/// </summary>
/// <param name = "force">True to set colliders again even if already found. This action will clear stored collider states.</param>
/// <returns>True if colliders should be found again.</returns>
public virtual bool TryFindColliders(bool force = false) => !_collidersFound || force;
}
}
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 4fa87b975da969046b596d2669737932
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 207815
packageName: 'FishNet: Networking Evolved'
packageVersion: 4.7.3R
assetPath: Assets/FishNet/Runtime/Generated/Component/Prediction/NetworkColliderBase.cs
uploadId: 987062
@@ -0,0 +1,13 @@
using UnityEngine;
namespace FishNet.Component.Prediction
{
public sealed class NetworkCollision : NetworkCollider
{
protected override void Awake()
{
IsTrigger = false;
base.Awake();
}
}
}
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 40bcde17fa5e03f44b87f9963ac281b3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 207815
packageName: 'FishNet: Networking Evolved'
packageVersion: 4.7.3R
assetPath: Assets/FishNet/Runtime/Generated/Component/Prediction/NetworkCollision.cs
uploadId: 987062
@@ -0,0 +1,13 @@
// using UnityEngine;
//
// namespace FishNet.Component.Prediction
// {
// public sealed class NetworkCollision2D : NetworkCollider2D
// {
// protected override void Awake()
// {
// IsTrigger = false;
// base.Awake();
// }
// }
// }
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: be3cb2952c351714c963b0766f53cef3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 207815
packageName: 'FishNet: Networking Evolved'
packageVersion: 4.7.3R
assetPath: Assets/FishNet/Runtime/Generated/Component/Prediction/NetworkCollision2D.cs
uploadId: 987062
@@ -0,0 +1,11 @@
namespace FishNet.Component.Prediction
{
public sealed class NetworkTrigger : NetworkCollider
{
protected override void Awake()
{
IsTrigger = true;
base.Awake();
}
}
}
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 3dc97a8b9e628044e83004c93095c14d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 207815
packageName: 'FishNet: Networking Evolved'
packageVersion: 4.7.3R
assetPath: Assets/FishNet/Runtime/Generated/Component/Prediction/NetworkTrigger.cs
uploadId: 987062
@@ -0,0 +1,11 @@
namespace FishNet.Component.Prediction
{
public sealed class NetworkTrigger2D : NetworkCollider2D
{
protected override void Awake()
{
IsTrigger = true;
base.Awake();
}
}
}
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: a60424afcf0b5984dbe878a1f21c10b2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 207815
packageName: 'FishNet: Networking Evolved'
packageVersion: 4.7.3R
assetPath: Assets/FishNet/Runtime/Generated/Component/Prediction/NetworkTrigger2D.cs
uploadId: 987062
@@ -0,0 +1,108 @@
using FishNet.Managing.Predicting;
using UnityEngine;
namespace FishNet.Component.Prediction
{
public partial class OfflineRigidbody : MonoBehaviour
{
#region Serialized.
/// <summary>
/// Type of prediction movement which is being used.
/// </summary>
[Tooltip("Type of prediction movement which is being used.")]
[SerializeField]
private RigidbodyType _rigidbodyType;
/// <summary>
/// True to also get rigidbody components within children.
/// </summary>
[Tooltip("True to also get rigidbody components within children.")]
[SerializeField]
private bool _getInChildren;
#endregion
#region Private.
/// <summary>
/// Pauser for rigidbodies.
/// </summary>
private RigidbodyPauser _rigidbodyPauser = new();
/// <summary>
/// TimeManager subscribed to.
/// </summary>
private PredictionManager _predictionManager;
#endregion
private void Awake()
{
InitializeOnce();
}
private void OnDestroy()
{
ChangeSubscription(false);
}
/// <summary>
/// Initializes this script for use.
/// </summary>
private void InitializeOnce()
{
_predictionManager = InstanceFinder.PredictionManager;
UpdateRigidbodies();
ChangeSubscription(true);
}
/// <summary>
/// Sets a new PredictionManager to use.
/// </summary>
/// <param name = "tm"></param>
public void SetPredictionManager(PredictionManager pm)
{
if (pm == _predictionManager)
return;
// Unsub from current.
ChangeSubscription(false);
// Sub to newest.
_predictionManager = pm;
ChangeSubscription(true);
}
/// <summary>
/// Finds and assigns rigidbodie using configured settings.
/// </summary>
public void UpdateRigidbodies()
{
_rigidbodyPauser.UpdateRigidbodies(transform, _rigidbodyType, _getInChildren);
}
/// <summary>
/// Changes the subscription to the TimeManager.
/// </summary>
private void ChangeSubscription(bool subscribe)
{
if (_predictionManager == null)
return;
if (subscribe)
{
_predictionManager.OnPreReconcile += _predictionManager_OnPreReconcile;
_predictionManager.OnPostReconcile += _predictionManager_OnPostReconcile;
}
else
{
_predictionManager.OnPreReconcile -= _predictionManager_OnPreReconcile;
_predictionManager.OnPostReconcile -= _predictionManager_OnPostReconcile;
}
}
private void _predictionManager_OnPreReconcile(uint clientTick, uint serverTick)
{
_rigidbodyPauser.Pause();
}
private void _predictionManager_OnPostReconcile(uint clientTick, uint serverTick)
{
_rigidbodyPauser.Unpause();
}
}
}
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: b749f90d4c9961c4991179db1130fa4d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 207815
packageName: 'FishNet: Networking Evolved'
packageVersion: 4.7.3R
assetPath: Assets/FishNet/Runtime/Generated/Component/Prediction/OfflineRigidbody.cs
uploadId: 987062
@@ -0,0 +1,535 @@
using FishNet.Managing;
using GameKit.Dependencies.Utilities;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using UnityEngine;
namespace FishNet.Component.Prediction
{
/// <summary>
/// Pauses and unpauses rigidbodies. While paused rigidbodies cannot be interacted with or simulated.
/// </summary>
public class RigidbodyPauser : IResettable
{
#region Types.
/// <summary>
/// Data for a rigidbody, including its transform and simulation state.
/// </summary>
public struct RigidbodyData
{
/// <summary>
/// Rigidbody for data.
/// </summary>
public Rigidbody Rigidbody;
/// <summary>
/// Cached velocity.
/// </summary>
public Vector3 Velocity;
/// <summary>
/// Cached angular velocity.
/// </summary>
public Vector3 AngularVelocity;
/// <summary>
/// True if the rigidbody was kinematic.
/// </summary>
public bool IsKinematic;
/// <summary>
/// True if the rigidbody was detecting collisions.
/// </summary>
public bool DetectCollisions;
/// <summary>
/// Detection mode of the Rigidbody.
/// </summary>
public CollisionDetectionMode CollisionDetectionMode;
/// <summary>
/// World position of the rigidbody's transform.
/// </summary>
public Vector3 Position;
/// <summary>
/// World rotation of the rigidbody's transform.
/// </summary>
public Quaternion Rotation;
public RigidbodyData(Rigidbody rb)
{
Rigidbody = rb;
Velocity = Vector3.zero;
AngularVelocity = Vector3.zero;
IsKinematic = rb.isKinematic;
DetectCollisions = rb.detectCollisions;
CollisionDetectionMode = rb.collisionDetectionMode;
Position = rb.transform.position;
Rotation = rb.transform.rotation;
}
public void Update(Rigidbody rb)
{
#if UNITY_6000_1_OR_NEWER
Velocity = rb.linearVelocity;
#else
Velocity = rb.velocity;
#endif
AngularVelocity = rb.angularVelocity;
IsKinematic = rb.isKinematic;
DetectCollisions = rb.detectCollisions;
CollisionDetectionMode = rb.collisionDetectionMode;
Position = rb.transform.position;
Rotation = rb.transform.rotation;
}
}
/// <summary>
/// Data for a rigidbody2d, including its transform and simulation state.
/// </summary>
public struct Rigidbody2DData
{
/// <summary>
/// Rigidbody for data.
/// </summary>
public Rigidbody2D Rigidbody2d;
/// <summary>
/// Cached velocity.
/// </summary>
public Vector2 Velocity;
/// <summary>
/// Cached angular velocity.
/// </summary>
public float AngularVelocity;
/// <summary>
/// True if the rigidbody was kinematic.
/// </summary>
public bool IsKinematic;
/// <summary>
/// True if the rigidbody was simulated.
/// </summary>
public bool Simulated;
/// <summary>
/// Detection mode of the rigidbody.
/// </summary>
public CollisionDetectionMode2D CollisionDetectionMode;
/// <summary>
/// World position of the rigidbody's transform.
/// </summary>
public Vector2 Position;
/// <summary>
/// World rotation of the rigidbody's transform, in degrees.
/// </summary>
public float Rotation;
public Rigidbody2DData(Rigidbody2D rb)
{
Rigidbody2d = rb;
Velocity = Vector2.zero;
AngularVelocity = 0f;
Simulated = rb.simulated;
#if UNITY_6000_1_OR_NEWER
IsKinematic = rb.bodyType == RigidbodyType2D.Kinematic;
#else
IsKinematic = rb.isKinematic;
#endif
CollisionDetectionMode = rb.collisionDetectionMode;
Position = rb.position;
Rotation = rb.rotation;
}
public void Update(Rigidbody2D rb)
{
#if UNITY_6000_1_OR_NEWER
Velocity = rb.linearVelocity;
#else
Velocity = rb.velocity;
#endif
AngularVelocity = rb.angularVelocity;
Simulated = rb.simulated;
#if UNITY_6000_1_OR_NEWER
IsKinematic = rb.bodyType == RigidbodyType2D.Kinematic;
#else
IsKinematic = rb.isKinematic;
#endif
CollisionDetectionMode = rb.collisionDetectionMode;
Position = rb.position;
Rotation = rb.rotation;
}
}
#endregion
#region Public.
/// <summary>
/// True if the rigidbodies are considered paused.
/// </summary>
public bool Paused { get; private set; }
#endregion
#region Private.
/// <summary>
/// Rigidbody datas for found rigidbodies.
/// </summary>
private List<RigidbodyData> _rigidbodyDatas = new();
/// <summary>
/// Rigidbody2D datas for found rigidbodies;
/// </summary>
private List<Rigidbody2DData> _rigidbody2dDatas = new();
/// <summary>
/// Snapshot taken when pausing, applied when unpausing.
/// </summary>
private List<RigidbodyData> _pauseSnapshot = new();
/// <summary>
/// Snapshot taken when pausing 2D rigidbodies, applied when unpausing.
/// </summary>
private List<Rigidbody2DData> _pauseSnapshot2d = new();
/// <summary>
/// True to get rigidbodies in children of transform.
/// </summary>
private bool _getInChildren;
/// <summary>
/// Transform to get rigidbodies on.
/// </summary>
private Transform _transform;
/// <summary>
/// Type of prediction movement which is being used.
/// </summary>
private RigidbodyType _rigidbodyType;
/// <summary>
/// True if initialized at least once.
/// </summary>
private bool _initialized;
#endregion
/// <summary>
/// Assigns rigidbodies using initialized settings.
/// </summary>
public void UpdateRigidbodies()
{
if (!_initialized)
{
InstanceFinder.NetworkManager.LogError($"T{GetType().Name} has not been initialized yet. This method cannot be used.");
return;
}
UpdateRigidbodies(_transform, _rigidbodyType, _getInChildren);
}
/// <summary>
/// Assigns rigidbodies manually and initializes component.
/// </summary>
public void UpdateRigidbodies(Rigidbody[] rbs)
{
List<Rigidbody> rigidbodies = CollectionCaches<Rigidbody>.RetrieveList();
foreach (Rigidbody rb in rbs)
rigidbodies.Add(rb);
UpdateRigidbodies(rigidbodies);
CollectionCaches<Rigidbody>.Store(rigidbodies);
}
/// <summary>
/// Assigns rigidbodies manually and initializes component.
/// </summary>
private void UpdateRigidbodies(List<Rigidbody> rbs)
{
_rigidbodyDatas.Clear();
foreach (Rigidbody rb in rbs)
_rigidbodyDatas.Add(new(rb));
_initialized = true;
}
/// <summary>
/// Assigns rigidbodies manually and initializes component.
/// </summary>
public void UpdateRigidbodies2D(Rigidbody2D[] rbs)
{
List<Rigidbody2D> rigidbodies = CollectionCaches<Rigidbody2D>.RetrieveList();
foreach (Rigidbody2D rb in rbs)
rigidbodies.Add(rb);
UpdateRigidbodies2D(rigidbodies);
CollectionCaches<Rigidbody2D>.Store(rigidbodies);
}
/// <summary>
/// Assigns rigidbodies manually and initializes component.
/// </summary>
private void UpdateRigidbodies2D(List<Rigidbody2D> rbs)
{
_rigidbody2dDatas.Clear();
foreach (Rigidbody2D rb in rbs)
_rigidbody2dDatas.Add(new(rb));
_initialized = true;
}
/// <summary>
/// Assigns rigidbodies.
/// </summary>
/// <param name = "rbs">Rigidbodies2D to use.</param>
public void UpdateRigidbodies(Transform t, RigidbodyType rbType, bool getInChildren)
{
_rigidbodyType = rbType;
_getInChildren = getInChildren;
// 3D.
if (rbType == RigidbodyType.Rigidbody)
{
List<Rigidbody> rigidbodies = CollectionCaches<Rigidbody>.RetrieveList();
if (getInChildren)
{
Rigidbody[] rbs = t.GetComponentsInChildren<Rigidbody>();
for (int i = 0; i < rbs.Length; i++)
rigidbodies.Add(rbs[i]);
}
else
{
Rigidbody rb = t.GetComponent<Rigidbody>();
if (rb != null)
rigidbodies.Add(rb);
}
UpdateRigidbodies(rigidbodies);
CollectionCaches<Rigidbody>.Store(rigidbodies);
}
// 2D.
else
{
List<Rigidbody2D> rigidbodies = CollectionCaches<Rigidbody2D>.RetrieveList();
if (getInChildren)
{
Rigidbody2D[] rbs = t.GetComponentsInChildren<Rigidbody2D>();
for (int i = 0; i < rbs.Length; i++)
rigidbodies.Add(rbs[i]);
}
else
{
Rigidbody2D rb = t.GetComponent<Rigidbody2D>();
if (rb != null)
rigidbodies.Add(rb);
}
UpdateRigidbodies2D(rigidbodies);
CollectionCaches<Rigidbody2D>.Store(rigidbodies);
}
}
/// <summary>
/// Fills a list with a snapshot of the current transform and simulation state of every tracked rigidbody.
/// </summary>
/// <param name = "snapshot">List to populate; it is cleared first.</param>
public void GetSnapshot(List<RigidbodyData> snapshot)
{
snapshot.Clear();
for (int i = 0; i < _rigidbodyDatas.Count; i++)
{
Rigidbody rb = _rigidbodyDatas[i].Rigidbody;
if (rb == null)
continue;
RigidbodyData data = new(rb);
data.Update(rb);
snapshot.Add(data);
}
}
/// <summary>
/// Fills a list with a snapshot of the current transform and simulation state of every tracked 2D rigidbody.
/// </summary>
/// <param name = "snapshot">List to populate; it is cleared first.</param>
public void GetSnapshot(List<Rigidbody2DData> snapshot)
{
snapshot.Clear();
for (int i = 0; i < _rigidbody2dDatas.Count; i++)
{
Rigidbody2D rb = _rigidbody2dDatas[i].Rigidbody2d;
if (rb == null)
continue;
Rigidbody2DData data = new(rb);
data.Update(rb);
snapshot.Add(data);
}
}
/// <summary>
/// Applies a snapshot, setting each rigidbody's transform (and optionally its simulation state) back to the stored values.
/// </summary>
/// <param name = "snapshot">Snapshot to apply. Null is ignored.</param>
/// <param name = "restoreSimulationState">True to restore isKinematic, detectCollisions and collisionDetectionMode (used when unpausing).
/// False to leave those as they are and only restore transform and velocity — the kinematic state is owned by other systems and must not be forced by a reconcile correction.</param>
public void ApplySnapshot(List<RigidbodyData> snapshot, bool restoreSimulationState = true)
{
if (snapshot == null)
return;
for (int i = 0; i < snapshot.Count; i++)
{
RigidbodyData data = snapshot[i];
Rigidbody rb = data.Rigidbody;
if (rb == null)
continue;
rb.position = data.Position;
rb.rotation = data.Rotation;
if (restoreSimulationState)
{
rb.isKinematic = data.IsKinematic;
rb.detectCollisions = data.DetectCollisions;
rb.collisionDetectionMode = data.CollisionDetectionMode;
}
//Velocities cannot be set on a kinematic rigidbody; check the actual current state.
if (!rb.isKinematic)
{
#if UNITY_6000_1_OR_NEWER
rb.linearVelocity = data.Velocity;
#else
rb.velocity = data.Velocity;
#endif
rb.angularVelocity = data.AngularVelocity;
}
}
}
/// <summary>
/// Applies a snapshot, setting each 2D rigidbody's transform (and optionally its simulation state) back to the stored values.
/// </summary>
/// <param name = "snapshot">Snapshot to apply. Null is ignored.</param>
/// <param name = "restoreSimulationState">True to restore bodyType, simulated and collisionDetectionMode (used when unpausing).
/// False to leave those as they are and only restore transform and velocity — the kinematic state is owned by other systems and must not be forced by a reconcile correction.</param>
public void ApplySnapshot(List<Rigidbody2DData> snapshot, bool restoreSimulationState = true)
{
if (snapshot == null)
return;
for (int i = 0; i < snapshot.Count; i++)
{
Rigidbody2DData data = snapshot[i];
Rigidbody2D rb = data.Rigidbody2d;
if (rb == null)
continue;
rb.position = data.Position;
rb.rotation = data.Rotation;
if (restoreSimulationState)
{
#if UNITY_6000_1_OR_NEWER
rb.bodyType = data.IsKinematic ? RigidbodyType2D.Kinematic : RigidbodyType2D.Dynamic;
#else
rb.isKinematic = data.IsKinematic;
#endif
rb.simulated = data.Simulated;
rb.collisionDetectionMode = data.CollisionDetectionMode;
}
//Velocities can only be set on a dynamic body; check the actual current state.
#if UNITY_6000_1_OR_NEWER
bool canSetVelocity = rb.bodyType == RigidbodyType2D.Dynamic;
#else
bool canSetVelocity = !rb.isKinematic;
#endif
if (canSetVelocity)
{
#if UNITY_6000_1_OR_NEWER
rb.linearVelocity = data.Velocity;
#else
rb.velocity = data.Velocity;
#endif
rb.angularVelocity = data.AngularVelocity;
}
}
}
/// <summary>
/// Pauses rigidbodies preventing them from interacting. The pre-pause state is snapshotted so Unpause can restore it.
/// </summary>
public void Pause()
{
if (Paused)
return;
Paused = true;
// 3D.
if (_rigidbodyType == RigidbodyType.Rigidbody)
{
//Snapshot the current state, then freeze every rigidbody.
GetSnapshot(_pauseSnapshot);
for (int i = 0; i < _rigidbodyDatas.Count; i++)
{
Rigidbody rb = _rigidbodyDatas[i].Rigidbody;
if (rb == null)
{
_rigidbodyDatas.RemoveAt(i);
i--;
continue;
}
rb.collisionDetectionMode = CollisionDetectionMode.Discrete;
rb.isKinematic = true;
rb.detectCollisions = false;
}
}
// 2D.
else
{
GetSnapshot(_pauseSnapshot2d);
for (int i = 0; i < _rigidbody2dDatas.Count; i++)
{
Rigidbody2D rb = _rigidbody2dDatas[i].Rigidbody2d;
if (rb == null)
{
_rigidbody2dDatas.RemoveAt(i);
i--;
continue;
}
rb.collisionDetectionMode = CollisionDetectionMode2D.Discrete;
#if UNITY_6000_1_OR_NEWER
rb.bodyType = RigidbodyType2D.Kinematic;
#else
rb.isKinematic = true;
#endif
rb.simulated = false;
}
}
}
/// <summary>
/// Unpauses rigidbodies allowing them to interact normally, restoring the state snapshotted during Pause.
/// </summary>
public void Unpause()
{
if (!Paused)
return;
Paused = false;
if (_rigidbodyType == RigidbodyType.Rigidbody)
ApplySnapshot(_pauseSnapshot);
else
ApplySnapshot(_pauseSnapshot2d);
}
public void ResetState()
{
_rigidbodyDatas.Clear();
_rigidbody2dDatas.Clear();
_pauseSnapshot.Clear();
_pauseSnapshot2d.Clear();
_getInChildren = default;
_transform = default;
_rigidbodyType = default;
_initialized = default;
Paused = default;
}
public void InitializeState() { }
}
}
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 9536377524ca5db43aae431f983ab21f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 207815
packageName: 'FishNet: Networking Evolved'
packageVersion: 4.7.3R
assetPath: Assets/FishNet/Runtime/Generated/Component/Prediction/RigidbodyPauser.cs
uploadId: 987062
@@ -0,0 +1,226 @@
using FishNet.CodeGenerating;
using FishNet.Serializing;
using UnityEngine;
using UnityEngine.Scripting;
namespace FishNet.Component.Prediction
{
[UseGlobalCustomSerializer]
[Preserve]
public struct RigidbodyState
{
public Vector3 Position;
public Quaternion Rotation;
[ExcludeSerialization]
public readonly AutoPackType RotationPacking;
public bool IsKinematic;
public Vector3 Velocity;
public Vector3 AngularVelocity;
public RigidbodyState(Rigidbody rb, AutoPackType rotationPacking = AutoPackType.Packed)
{
Position = rb.transform.localPosition;
Rotation = rb.transform.localRotation;
IsKinematic = rb.isKinematic;
#if UNITY_6000_1_OR_NEWER
Velocity = rb.linearVelocity;
#else
Velocity = rb.velocity;
#endif
AngularVelocity = rb.angularVelocity;
RotationPacking = rotationPacking;
}
}
[UseGlobalCustomSerializer]
[Preserve]
public struct Rigidbody2DState
{
public Vector3 Position;
public Quaternion Rotation;
[ExcludeSerialization]
public readonly AutoPackType RotationPacking;
public Vector2 Velocity;
public float AngularVelocity;
public bool Simulated;
public bool IsKinematic;
public Rigidbody2DState(Rigidbody2D rb, AutoPackType rotationPacking = AutoPackType.Packed)
{
Position = rb.transform.localPosition;
Rotation = rb.transform.localRotation;
RotationPacking = rotationPacking;
#if UNITY_6000_1_OR_NEWER
Velocity = rb.linearVelocity;
#else
Velocity = rb.velocity;
#endif
AngularVelocity = rb.angularVelocity;
Simulated = rb.simulated;
#if UNITY_6000_1_OR_NEWER
IsKinematic = rb.bodyType == RigidbodyType2D.Kinematic;
#else
IsKinematic = rb.isKinematic;
#endif
}
}
[Preserve]
public static class RigidbodyStateSerializers
{
public static void WriteRigidbodyState(this Writer writer, RigidbodyState value)
{
writer.WriteVector3(value.Position);
writer.WriteAutoPackType(value.RotationPacking);
writer.WriteQuaternion(value.Rotation, value.RotationPacking);
writer.WriteBoolean(value.IsKinematic);
if (!value.IsKinematic)
{
writer.WriteVector3(value.Velocity);
writer.WriteVector3(value.AngularVelocity);
}
}
public static RigidbodyState ReadRigidbodyState(this Reader reader)
{
Vector3 position = reader.ReadVector3();
AutoPackType rotationPacking = reader.ReadAutoPackType();
Quaternion rotation = reader.ReadQuaternion(rotationPacking);
bool isKinematic = reader.ReadBoolean();
RigidbodyState state = new()
{
Position = position,
Rotation = rotation,
IsKinematic = isKinematic,
};
if (!state.IsKinematic)
{
state.Velocity = reader.ReadVector3();
state.AngularVelocity = reader.ReadVector3();
}
return state;
}
public static void WriteRigidbody2DState(this Writer writer, Rigidbody2DState value)
{
writer.WriteVector3(value.Position);
writer.WriteAutoPackType(value.RotationPacking);
writer.WriteQuaternion(value.Rotation, value.RotationPacking);
writer.WriteBoolean(value.Simulated);
writer.WriteBoolean(value.IsKinematic);
if (value.Simulated)
{
writer.WriteVector2(value.Velocity);
writer.WriteSingle(value.AngularVelocity);
}
}
public static Rigidbody2DState ReadRigidbody2DState(this Reader reader)
{
Vector3 position = reader.ReadVector3();
AutoPackType rotationPacking = reader.ReadAutoPackType();
Quaternion rotation = reader.ReadQuaternion(rotationPacking);
bool simulated = reader.ReadBoolean();
bool isKinematic = reader.ReadBoolean();
Rigidbody2DState state = new()
{
Position = position,
Rotation = rotation,
Simulated = simulated,
IsKinematic = isKinematic,
};
if (state.Simulated)
{
state.Velocity = reader.ReadVector2();
state.AngularVelocity = reader.ReadSingle();
}
return state;
}
}
[Preserve]
public static class RigidbodyStateExtensions
{
/// <summary>
/// Gets a RigidbodyState.
/// </summary>
public static RigidbodyState GetState(this Rigidbody rb, AutoPackType rotationPacking = AutoPackType.Packed)
{
return new(rb, rotationPacking);
}
/// <summary>
/// Sets a state to a rigidbody.
/// </summary>
public static void SetState(this Rigidbody rb, RigidbodyState state)
{
Transform t = rb.transform;
t.localPosition = state.Position;
t.localRotation = state.Rotation;
rb.isKinematic = state.IsKinematic;
if (!state.IsKinematic)
{
#if UNITY_6000_1_OR_NEWER
rb.linearVelocity = state.Velocity;
#else
rb.velocity = state.Velocity;
#endif
rb.angularVelocity = state.AngularVelocity;
}
}
/// <summary>
/// Gets a Rigidbody2DState.
/// </summary>
public static Rigidbody2DState GetState(this Rigidbody2D rb, AutoPackType rotationPacking = AutoPackType.Packed)
{
return new(rb, rotationPacking);
}
/// <summary>
/// Sets a state to a rigidbody.
/// </summary>
public static void SetState(this Rigidbody2D rb, Rigidbody2DState state)
{
Transform t = rb.transform;
t.position = state.Position;
t.rotation = state.Rotation;
#if UNITY_6000_1_OR_NEWER
rb.bodyType = state.IsKinematic ? RigidbodyType2D.Kinematic : RigidbodyType2D.Dynamic;
#else
rb.isKinematic = state.IsKinematic;
#endif
if (!state.IsKinematic)
{
#if UNITY_6000_1_OR_NEWER
rb.linearVelocity = state.Velocity;
#else
rb.velocity = state.Velocity;
#endif
rb.angularVelocity = state.AngularVelocity;
}
}
}
}
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 05dbb585c2bc6bf4dbbc592bea73d2fe
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 207815
packageName: 'FishNet: Networking Evolved'
packageVersion: 4.7.3R
assetPath: Assets/FishNet/Runtime/Generated/Component/Prediction/RigidbodyState.cs
uploadId: 987062
@@ -0,0 +1,11 @@
namespace FishNet.Component.Prediction
{
/// <summary>
/// Type of prediction movement being used.
/// </summary>
public enum RigidbodyType : byte
{
Rigidbody = 0,
Rigidbody2D = 1
}
}
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 7cf2d3fc2ff7a9042b4b7618db15b482
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 207815
packageName: 'FishNet: Networking Evolved'
packageVersion: 4.7.3R
assetPath: Assets/FishNet/Runtime/Generated/Component/Prediction/RigidbodyType.cs
uploadId: 987062