fishnet installed
This commit is contained in:
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a6b05a47941365c4097d74bca5e47017
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -0,0 +1,152 @@
|
||||
#if UNITY_EDITOR
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using static FishNet.Component.Prediction.PredictedObject;
|
||||
|
||||
namespace FishNet.Component.Prediction
|
||||
{
|
||||
|
||||
|
||||
[CustomEditor(typeof(PredictedObject), true)]
|
||||
[CanEditMultipleObjects]
|
||||
public class PredictedObjectEditor : Editor
|
||||
{
|
||||
private SerializedProperty _implementsPredictionMethods;
|
||||
private SerializedProperty _graphicalObject;
|
||||
private SerializedProperty _ownerSmoothPosition;
|
||||
private SerializedProperty _ownerSmoothRotation;
|
||||
private SerializedProperty _ownerInterpolation;
|
||||
private SerializedProperty _enableTeleport;
|
||||
private SerializedProperty _teleportThreshold;
|
||||
private SerializedProperty _predictionType;
|
||||
|
||||
private SerializedProperty _rigidbody;
|
||||
private SerializedProperty _rigidbody2d;
|
||||
private SerializedProperty _spectatorSmoothPosition;
|
||||
private SerializedProperty _spectatorSmoothRotation;
|
||||
private SerializedProperty _spectatorSmoothingType;
|
||||
private SerializedProperty _customSmoothingData;
|
||||
private SerializedProperty _preconfiguredSmoothingDataPreview;
|
||||
private SerializedProperty _maintainedVelocity;
|
||||
private SerializedProperty _resendType;
|
||||
private SerializedProperty _resendInterval;
|
||||
|
||||
private SerializedProperty _networkTransform;
|
||||
|
||||
protected virtual void OnEnable()
|
||||
{
|
||||
_implementsPredictionMethods = serializedObject.FindProperty(nameof(_implementsPredictionMethods));
|
||||
_graphicalObject = serializedObject.FindProperty(nameof(_graphicalObject));
|
||||
_ownerSmoothPosition = serializedObject.FindProperty(nameof(_ownerSmoothPosition));
|
||||
_ownerSmoothRotation = serializedObject.FindProperty(nameof(_ownerSmoothRotation));
|
||||
_ownerInterpolation = serializedObject.FindProperty(nameof(_ownerInterpolation));
|
||||
_enableTeleport = serializedObject.FindProperty(nameof(_enableTeleport));
|
||||
_teleportThreshold = serializedObject.FindProperty(nameof(_teleportThreshold));
|
||||
_predictionType = serializedObject.FindProperty(nameof(_predictionType));
|
||||
|
||||
_rigidbody = serializedObject.FindProperty(nameof(_rigidbody));
|
||||
_rigidbody2d = serializedObject.FindProperty(nameof(_rigidbody2d));
|
||||
_spectatorSmoothPosition = serializedObject.FindProperty(nameof(_spectatorSmoothPosition));
|
||||
_spectatorSmoothRotation = serializedObject.FindProperty(nameof(_spectatorSmoothRotation));
|
||||
_spectatorSmoothingType = serializedObject.FindProperty(nameof(_spectatorSmoothingType));
|
||||
_customSmoothingData = serializedObject.FindProperty(nameof(_customSmoothingData));
|
||||
_preconfiguredSmoothingDataPreview = serializedObject.FindProperty(nameof(_preconfiguredSmoothingDataPreview));
|
||||
|
||||
_maintainedVelocity = serializedObject.FindProperty(nameof(_maintainedVelocity));
|
||||
_resendType = serializedObject.FindProperty(nameof(_resendType));
|
||||
_resendInterval = serializedObject.FindProperty(nameof(_resendInterval));
|
||||
|
||||
_networkTransform = serializedObject.FindProperty(nameof(_networkTransform));
|
||||
|
||||
}
|
||||
|
||||
public override void OnInspectorGUI()
|
||||
{
|
||||
serializedObject.Update();
|
||||
|
||||
GUI.enabled = false;
|
||||
EditorGUILayout.ObjectField("Script:", MonoScript.FromMonoBehaviour((PredictedObject)target), typeof(PredictedObject), false);
|
||||
GUI.enabled = true;
|
||||
|
||||
EditorGUILayout.PropertyField(_implementsPredictionMethods);
|
||||
EditorGUILayout.PropertyField(_graphicalObject);
|
||||
EditorGUILayout.PropertyField(_enableTeleport);
|
||||
if (_enableTeleport.boolValue)
|
||||
{
|
||||
EditorGUI.indentLevel++;
|
||||
EditorGUILayout.PropertyField(_teleportThreshold);
|
||||
EditorGUI.indentLevel--;
|
||||
}
|
||||
|
||||
EditorGUILayout.LabelField("Owner Settings");
|
||||
EditorGUI.indentLevel++;
|
||||
EditorGUILayout.PropertyField(_ownerSmoothPosition, new GUIContent("Smooth Position"));
|
||||
EditorGUILayout.PropertyField(_ownerSmoothRotation, new GUIContent("Smooth Rotation"));
|
||||
EditorGUILayout.PropertyField(_ownerInterpolation, new GUIContent("Interpolation"));
|
||||
EditorGUI.indentLevel--;
|
||||
|
||||
EditorGUILayout.PropertyField(_predictionType);
|
||||
PredictedObject.PredictionType movementType = (PredictedObject.PredictionType)_predictionType.intValue;
|
||||
if (movementType != PredictedObject.PredictionType.Other)
|
||||
{
|
||||
EditorGUI.indentLevel++;
|
||||
EditorGUILayout.HelpBox("When using physics prediction do not include a NetworkTransform; this component will synchronize instead.", MessageType.Info);
|
||||
if (movementType == PredictedObject.PredictionType.Rigidbody)
|
||||
EditorGUILayout.PropertyField(_rigidbody);
|
||||
else
|
||||
EditorGUILayout.PropertyField(_rigidbody2d, new GUIContent("Rigidbody2D", "Rigidbody2D to predict."));
|
||||
|
||||
EditorGUILayout.LabelField("Spectator Settings");
|
||||
EditorGUI.indentLevel++;
|
||||
EditorGUILayout.LabelField("Smoothing");
|
||||
EditorGUI.indentLevel++;
|
||||
EditorGUILayout.PropertyField(_spectatorSmoothPosition, new GUIContent("Smooth Position"));
|
||||
EditorGUILayout.PropertyField(_spectatorSmoothRotation, new GUIContent("Smooth Rotation"));
|
||||
EditorGUILayout.PropertyField(_spectatorSmoothingType, new GUIContent("Smoothing Type"));
|
||||
//Custom.
|
||||
if ((SpectatorSmoothingType)_spectatorSmoothingType.intValue == SpectatorSmoothingType.Custom)
|
||||
{
|
||||
EditorGUI.indentLevel++;
|
||||
EditorGUILayout.PropertyField(_customSmoothingData);
|
||||
EditorGUI.indentLevel--;
|
||||
}
|
||||
//Preconfigured.
|
||||
else
|
||||
{
|
||||
EditorGUI.indentLevel++;
|
||||
GUI.enabled = false;
|
||||
EditorGUILayout.PropertyField(_preconfiguredSmoothingDataPreview);
|
||||
GUI.enabled = true;
|
||||
EditorGUI.indentLevel--;
|
||||
}
|
||||
EditorGUI.indentLevel--;
|
||||
EditorGUILayout.PropertyField(_maintainedVelocity);
|
||||
|
||||
EditorGUILayout.PropertyField(_resendType);
|
||||
PredictedObject.ResendType resendType = (PredictedObject.ResendType)_resendType.intValue;
|
||||
if (resendType == PredictedObject.ResendType.Interval)
|
||||
{
|
||||
EditorGUI.indentLevel++;
|
||||
EditorGUILayout.PropertyField(_resendInterval, new GUIContent("Interval"));
|
||||
EditorGUI.indentLevel--;
|
||||
}
|
||||
EditorGUI.indentLevel--;
|
||||
|
||||
EditorGUI.indentLevel--;
|
||||
}
|
||||
else
|
||||
{
|
||||
EditorGUI.indentLevel++;
|
||||
EditorGUILayout.HelpBox("When other is selected another component, such as NetworkTransform, must be used to synchronize.", MessageType.Info);
|
||||
EditorGUILayout.PropertyField(_networkTransform);
|
||||
EditorGUI.indentLevel--;
|
||||
}
|
||||
|
||||
EditorGUILayout.Space();
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e0b8595657415764e9d83b6d974043af
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -0,0 +1,130 @@
|
||||
using FishNet.Managing.Predicting;
|
||||
using FishNet.Managing.Timing;
|
||||
using FishNet.Object;
|
||||
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>
|
||||
/// GraphicalObject to unparent when pausing.
|
||||
/// </summary>
|
||||
private Transform _graphicalObject;
|
||||
/// <summary>
|
||||
/// Sets GraphicalObject.
|
||||
/// </summary>
|
||||
/// <param name="value"></param>
|
||||
public void SetGraphicalObject(Transform value)
|
||||
{
|
||||
_graphicalObject = value;
|
||||
UpdateRigidbodies();
|
||||
}
|
||||
/// <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 RigidbodyPauser();
|
||||
/// <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 TimeManager 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, _graphicalObject);
|
||||
}
|
||||
|
||||
/// <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(NetworkBehaviour obj)
|
||||
{
|
||||
//Make rbs all kinematic/!simulated before reconciling, which would also result in replays.
|
||||
_rigidbodyPauser.Pause();
|
||||
}
|
||||
|
||||
private void _predictionManager_OnPostReconcile(NetworkBehaviour obj)
|
||||
{
|
||||
_rigidbodyPauser.Unpause();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b749f90d4c9961c4991179db1130fa4d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6bd787cd0da2e9e4ab4bd30794ff0082
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -0,0 +1,605 @@
|
||||
using FishNet.Component.Transforming;
|
||||
using FishNet.Utility.Extension;
|
||||
using FishNet.Connection;
|
||||
using FishNet.Managing;
|
||||
using FishNet.Object;
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace FishNet.Component.Prediction
|
||||
{
|
||||
[AddComponentMenu("FishNet/Component/PredictedObject")]
|
||||
public partial class PredictedObject : NetworkBehaviour
|
||||
{
|
||||
#region Types.
|
||||
/// <summary>
|
||||
/// How to favor smoothing for predicted objects.
|
||||
/// </summary>
|
||||
public enum SpectatorSmoothingType
|
||||
{
|
||||
/// <summary>
|
||||
/// Favor accurate collisions. With fast moving objects this may result in some jitter with higher latencies.
|
||||
/// </summary>
|
||||
Accuracy = 0,
|
||||
/// <summary>
|
||||
/// A mix between Accuracy and Smoothness.
|
||||
/// </summary>
|
||||
Mixed = 1,
|
||||
/// <summary>
|
||||
/// Prefer smooth movement and corrections. Fast moving objects may collide before the graphical representation catches up.
|
||||
/// </summary>
|
||||
Gradual = 2,
|
||||
/// <summary>
|
||||
/// Configure values to your preference.
|
||||
/// </summary>
|
||||
Custom = 3,
|
||||
}
|
||||
/// <summary>
|
||||
/// State of this object in a collision.
|
||||
/// </summary>
|
||||
private enum CollectionState : byte
|
||||
{
|
||||
Unset = 0,
|
||||
Added = 1,
|
||||
Removed = 2,
|
||||
}
|
||||
/// <summary>
|
||||
/// Type of prediction movement being used.
|
||||
/// </summary>
|
||||
internal enum PredictionType : byte
|
||||
{
|
||||
Other = 0,
|
||||
Rigidbody = 1,
|
||||
Rigidbody2D = 2
|
||||
}
|
||||
internal enum ResendType : byte
|
||||
{
|
||||
Disabled = 0,
|
||||
Interval = 1,
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Public.
|
||||
/// <summary>
|
||||
/// True if the prediction type is for a rigidbody.
|
||||
/// </summary>
|
||||
public bool IsRigidbodyPrediction => (_predictionType == PredictionType.Rigidbody || _predictionType == PredictionType.Rigidbody2D);
|
||||
#endregion
|
||||
|
||||
#region Serialized.
|
||||
/// <summary>
|
||||
/// True if this object implements replicate and reconcile methods.
|
||||
/// </summary>
|
||||
[Tooltip("True if this object implements replicate and reconcile methods.")]
|
||||
[SerializeField]
|
||||
private bool _implementsPredictionMethods = true;
|
||||
/// <summary>
|
||||
/// Transform which holds the graphical features of this object. This transform will be smoothed when desynchronizations occur.
|
||||
/// </summary>
|
||||
[Tooltip("Transform which holds the graphical features of this object. This transform will be smoothed when desynchronizations occur.")]
|
||||
[SerializeField]
|
||||
private Transform _graphicalObject;
|
||||
/// <summary>
|
||||
/// Gets GraphicalObject.
|
||||
/// </summary>
|
||||
public Transform GetGraphicalObject() => _graphicalObject;
|
||||
/// <summary>
|
||||
/// Sets GraphicalObject.
|
||||
/// </summary>
|
||||
/// <param name="value"></param>
|
||||
public void SetGraphicalObject(Transform value)
|
||||
{
|
||||
_graphicalObject = value;
|
||||
SetInstantiatedOffsetValues();
|
||||
_spectatorSmoother?.SetGraphicalObject(value);
|
||||
_ownerSmoother?.SetGraphicalObject(value);
|
||||
}
|
||||
/// <summary>
|
||||
/// True to enable teleport threshhold.
|
||||
/// </summary>
|
||||
[Tooltip("True to enable teleport threshhold.")]
|
||||
[SerializeField]
|
||||
private bool _enableTeleport;
|
||||
/// <summary>
|
||||
/// How far the transform must travel in a single update to cause a teleport rather than smoothing. Using 0f will teleport every update.
|
||||
/// </summary>
|
||||
[Tooltip("How far the transform must travel in a single update to cause a teleport rather than smoothing. Using 0f will teleport every update.")]
|
||||
[Range(0f, 200f)] //Unity bug? Values ~over 200f lose decimal display within inspector.
|
||||
[SerializeField]
|
||||
private float _teleportThreshold = 1f;
|
||||
/// <summary>
|
||||
/// Gets the value for SmoothTicks.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[Obsolete("No longer used. This setting has been replaced by Smoothing Type.")]//Remove on 2023/06/01
|
||||
public bool GetSmoothTicks() => true;
|
||||
/// <summary>
|
||||
/// Sets the value for SmoothTicks.
|
||||
/// </summary>
|
||||
/// <param name="value"></param>
|
||||
/// <returns></returns>
|
||||
[Obsolete("No longer used. This setting has been replaced by Smoothing Type.")] //Remove on 2023/06/01
|
||||
public void SetSmoothTicks(bool value) { }
|
||||
/// <summary>
|
||||
/// True to smooth position on owner objects.
|
||||
/// </summary>
|
||||
[Tooltip("True to smooth position on owner objects.")]
|
||||
[SerializeField]
|
||||
private bool _ownerSmoothPosition = true;
|
||||
/// <summary>
|
||||
/// True to smooth rotation on owner objects.
|
||||
/// </summary>
|
||||
[Tooltip("True to smooth rotation on owner objects.")]
|
||||
[SerializeField]
|
||||
private bool _ownerSmoothRotation = true;
|
||||
/// <summary>
|
||||
/// How far in the past to keep the graphical object when owner. Using a value of 0 will disable interpolation.
|
||||
/// </summary>
|
||||
[Tooltip("How far in the past to keep the graphical object when owner. Using a value of 0 will disable interpolation.")]
|
||||
[Range(0, 255)]
|
||||
[SerializeField]
|
||||
private byte _ownerInterpolation = 1;
|
||||
/// <summary>
|
||||
/// Gets the iterpolation value to use when the owner of this object.
|
||||
/// </summary>
|
||||
/// <param name="asOwner">True to get the interpolation for when owner, false to get the interpolation for when a spectator.</param>
|
||||
[Obsolete("No longer used. This setting has been replaced by Smoothing Type.")]//Remove on 2023/06/01
|
||||
public byte GetInterpolation(bool asOwner) => 0;
|
||||
/// <summary>
|
||||
/// Sets the interpolation value to use when the owner of this object.
|
||||
/// </summary>
|
||||
/// <param name="value"></param>
|
||||
/// <param name="asOwner">True to set the interpolation for when owner, false to set interpolation for when a spectator.</param>
|
||||
[Obsolete("No longer used. This setting has been replaced by Smoothing Type.")]//Remove on 2023/06/01
|
||||
public void SetInterpolation(byte value, bool asOwner)
|
||||
{
|
||||
//if (asOwner)
|
||||
//{
|
||||
// _ownerInterpolation = value;
|
||||
// _ownerSmoother?.SetInterpolation(value);
|
||||
//}
|
||||
//else
|
||||
//{
|
||||
// _spectatorInterpolation = value;
|
||||
// _spectatorSmoother?.SetInterpolation(value);
|
||||
//}
|
||||
}
|
||||
/// <summary>
|
||||
/// Type of prediction movement which is being used.
|
||||
/// </summary>
|
||||
[Tooltip("Type of prediction movement which is being used.")]
|
||||
[SerializeField]
|
||||
private PredictionType _predictionType;
|
||||
/// <summary>
|
||||
/// Rigidbody to predict.
|
||||
/// </summary>
|
||||
[Tooltip("Rigidbody to predict.")]
|
||||
[SerializeField]
|
||||
private Rigidbody _rigidbody;
|
||||
/// <summary>
|
||||
/// Rigidbody2D to predict.
|
||||
/// </summary>
|
||||
[Tooltip("Rigidbody2D to predict.")]
|
||||
[SerializeField]
|
||||
private Rigidbody2D _rigidbody2d;
|
||||
/// <summary>
|
||||
/// True to smooth position on spectated objects.
|
||||
/// </summary>
|
||||
[Tooltip("True to smooth position on spectated objects.")]
|
||||
[SerializeField]
|
||||
private bool _spectatorSmoothPosition = true;
|
||||
/// <summary>
|
||||
/// True to smooth rotation on spectated objects.
|
||||
/// </summary>
|
||||
[Tooltip("True to smooth rotation on spectated objects.")]
|
||||
[SerializeField]
|
||||
private bool _spectatorSmoothRotation = true;
|
||||
/// <summary>
|
||||
/// How to favor smoothing for predicted objects.
|
||||
/// </summary>
|
||||
[Tooltip("How to favor smoothing for predicted objects.")]
|
||||
[SerializeField]
|
||||
private SpectatorSmoothingType _spectatorSmoothingType = SpectatorSmoothingType.Mixed;
|
||||
/// <summary>
|
||||
/// Custom settings for smoothing data.
|
||||
/// </summary>
|
||||
[Tooltip("Custom settings for smoothing data.")]
|
||||
[SerializeField]
|
||||
private SmoothingData _customSmoothingData = _mixedSmoothingData;
|
||||
/// <summary>
|
||||
/// Preview of selected preconfigured smoothing data. This is only used for the inspector.
|
||||
/// </summary>
|
||||
[SerializeField]
|
||||
private SmoothingData _preconfiguredSmoothingDataPreview = _mixedSmoothingData;
|
||||
/// <summary>
|
||||
/// Sets SpectactorSmoothingType value.
|
||||
/// </summary>
|
||||
/// <param name="value">Value to use.</param>
|
||||
public void SetSpectatorSmoothingType(SpectatorSmoothingType value)
|
||||
{
|
||||
if (base.IsSpawned)
|
||||
base.NetworkManager.LogWarning($"Spectator smoothing type may only be set before the object is spawned, such as after instantiating but before spawning.");
|
||||
else
|
||||
_spectatorSmoothingType = value;
|
||||
}
|
||||
|
||||
///// <summary>
|
||||
///// How far in the past to keep the graphical object when not owner. Using a value of 0 will disable interpolation.
|
||||
///// </summary>
|
||||
//[Tooltip("How far in the past to keep the graphical object when not owner. Using a value of 0 will disable interpolation.")]
|
||||
//[Range(0, 255)]
|
||||
//[SerializeField]
|
||||
//private byte _spectatorInterpolation = 4;
|
||||
///// <summary>
|
||||
///// Multiplier to apply to movement speed when buffer is over interpolation.
|
||||
///// </summary>
|
||||
//[Tooltip("Multiplier to apply to movement speed when buffer is over interpolation.")]
|
||||
//[Range(0f, 5f)]
|
||||
//[SerializeField]
|
||||
//private float _overflowMultiplier = 0.1f;
|
||||
/// <summary>
|
||||
/// Multiplier applied to difference in velocity between ticks.
|
||||
/// Positive values will result in more velocity while lowers will result in less.
|
||||
/// A value of 1f will prevent any velocity from being lost between ticks, unless indicated by the server.
|
||||
/// </summary>
|
||||
[Tooltip("Multiplier applied to difference in velocity between ticks. Positive values will result in more velocity while lowers will result in less. A value of 1f will prevent any velocity from being lost between ticks, unless indicated by the server.")]
|
||||
[Range(-10f, 10f)]
|
||||
[SerializeField]
|
||||
private float _maintainedVelocity = 0f;
|
||||
/// <summary>
|
||||
/// How often to resend current values regardless if the state has changed. Using this value will consume more bandwidth but may be preferred if you want to force synchronization the object move on the client but not on the server.
|
||||
/// </summary>
|
||||
[Tooltip("How often to resend current values regardless if the state has changed. Using this value will consume more bandwidth but may be preferred if you want to force synchronization the object move on the client but not on the server.")]
|
||||
[SerializeField]
|
||||
private ResendType _resendType = ResendType.Disabled;
|
||||
/// <summary>
|
||||
/// How often in ticks to resend values.
|
||||
/// </summary>
|
||||
[Tooltip("How often in ticks to resend values.")]
|
||||
[SerializeField]
|
||||
private ushort _resendInterval = 30;
|
||||
/// <summary>
|
||||
/// NetworkTransform to configure.
|
||||
/// </summary>
|
||||
[Tooltip("NetworkTransform to configure.")]
|
||||
[SerializeField]
|
||||
private NetworkTransform _networkTransform;
|
||||
#endregion
|
||||
|
||||
#region Private.
|
||||
/// <summary>
|
||||
/// True if client subscribed to events.
|
||||
/// </summary>
|
||||
private bool _clientSubscribed;
|
||||
/// <summary>
|
||||
/// True if this PredictedObject has been registered with the PredictionManager.
|
||||
/// </summary>
|
||||
private bool _registered;
|
||||
/// <summary>
|
||||
/// GraphicalObject position difference from this object when this is instantiated.
|
||||
/// </summary>
|
||||
private Vector3 _graphicalInstantiatedOffsetPosition;
|
||||
/// <summary>
|
||||
/// GraphicalObject rotation difference from this object when this is instantiated.
|
||||
/// </summary>
|
||||
private Quaternion _graphicalInstantiatedOffsetRotation;
|
||||
/// <summary>
|
||||
/// Cached localtick for performance.
|
||||
/// </summary>
|
||||
private uint _localTick;
|
||||
/// <summary>
|
||||
/// Smoothing component for this object when not owner.
|
||||
/// </summary>
|
||||
private PredictedObjectSpectatorSmoother _spectatorSmoother;
|
||||
/// <summary>
|
||||
/// Smoothing component for this object when owner.
|
||||
/// This component is also used for non-owned objects when as server.
|
||||
/// </summary>
|
||||
private PredictedObjectOwnerSmoother _ownerSmoother;
|
||||
#endregion
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
SetInstantiatedOffsetValues();
|
||||
}
|
||||
|
||||
public override void OnStartNetwork()
|
||||
{
|
||||
base.OnStartNetwork();
|
||||
|
||||
/* If host then initialize owner smoother.
|
||||
* Host will use owner smoothing settings for more
|
||||
* accurate results. */
|
||||
if (base.IsHost)
|
||||
InitializeSmoother(true);
|
||||
|
||||
UpdateRigidbodiesCount(true);
|
||||
ConfigureRigidbodies();
|
||||
ConfigureNetworkTransform();
|
||||
base.TimeManager.OnPostTick += TimeManager_OnPostTick;
|
||||
}
|
||||
|
||||
public override void OnSpawnServer(NetworkConnection connection)
|
||||
{
|
||||
base.OnSpawnServer(connection);
|
||||
Rigidbodies_OnSpawnServer(connection);
|
||||
}
|
||||
|
||||
public override void OnStartClient()
|
||||
{
|
||||
base.OnStartClient();
|
||||
ChangeSubscriptions(true);
|
||||
Rigidbodies_OnStartClient();
|
||||
}
|
||||
|
||||
public override void OnOwnershipClient(NetworkConnection prevOwner)
|
||||
{
|
||||
base.OnOwnershipClient(prevOwner);
|
||||
/* If owner or host then use the
|
||||
* owner smoother. The owner smoother
|
||||
* is not predictive and is preferred
|
||||
* for more real time graphical results. */
|
||||
if (base.IsOwner && !base.IsServer)
|
||||
{
|
||||
/* If has prediction methods implement for owner,
|
||||
* otherwise implement for spectator. */
|
||||
InitializeSmoother(_implementsPredictionMethods);
|
||||
/* Also set spectator smoothing if does not implement
|
||||
* prediction methods as the spectator smoother is used
|
||||
* for these scenarios. */
|
||||
if (!_implementsPredictionMethods)
|
||||
SetTargetSmoothing(base.TimeManager.RoundTripTime, true);
|
||||
}
|
||||
//Not owner nor server, initialize spectator smoother if using rigidbodies.
|
||||
else if (_predictionType != PredictionType.Other)
|
||||
{
|
||||
InitializeSmoother(false);
|
||||
SetTargetSmoothing(base.TimeManager.RoundTripTime, true);
|
||||
}
|
||||
|
||||
Rigidbodies_OnOwnershipClient(prevOwner);
|
||||
}
|
||||
|
||||
public override void OnStopNetwork()
|
||||
{
|
||||
base.OnStopNetwork();
|
||||
|
||||
ChangeSubscriptions(false);
|
||||
UpdateRigidbodiesCount(false);
|
||||
base.TimeManager.OnPostTick -= TimeManager_OnPostTick;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates Rigidbodies count on the PredictionManager.
|
||||
/// </summary>
|
||||
/// <param name="add"></param>
|
||||
private void UpdateRigidbodiesCount(bool add)
|
||||
{
|
||||
if (_registered == add)
|
||||
return;
|
||||
if (_predictionType == PredictionType.Other)
|
||||
return;
|
||||
|
||||
NetworkManager nm = base.NetworkManager;
|
||||
if (nm == null)
|
||||
return;
|
||||
|
||||
_registered = add;
|
||||
|
||||
if (add)
|
||||
{
|
||||
nm.PredictionManager.AddRigidbodyCount(this);
|
||||
nm.PredictionManager.OnPreServerReconcile += PredictionManager_OnPreServerReconcile;
|
||||
}
|
||||
else
|
||||
{
|
||||
nm.PredictionManager.RemoveRigidbodyCount(this);
|
||||
nm.PredictionManager.OnPreServerReconcile -= PredictionManager_OnPreServerReconcile;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets instantiated offset values for the graphical object.
|
||||
/// </summary>
|
||||
private void SetInstantiatedOffsetValues()
|
||||
{
|
||||
transform.SetTransformOffsets(_graphicalObject, ref _graphicalInstantiatedOffsetPosition, ref _graphicalInstantiatedOffsetRotation);
|
||||
}
|
||||
|
||||
private void TimeManager_OnUpdate()
|
||||
{
|
||||
_spectatorSmoother?.ManualUpdate();
|
||||
_ownerSmoother?.ManualUpdate();
|
||||
}
|
||||
|
||||
private void TimeManager_OnPreTick()
|
||||
{
|
||||
_localTick = base.TimeManager.LocalTick;
|
||||
_spectatorSmoother?.OnPreTick();
|
||||
_ownerSmoother?.OnPreTick();
|
||||
}
|
||||
|
||||
protected void TimeManager_OnPostTick()
|
||||
{
|
||||
_spectatorSmoother?.OnPostTick();
|
||||
_ownerSmoother?.OnPostTick();
|
||||
Rigidbodies_TimeManager_OnPostTick();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Subscribes to events needed to function.
|
||||
/// </summary>
|
||||
/// <param name="subscribe"></param>
|
||||
private void ChangeSubscriptions(bool subscribe)
|
||||
{
|
||||
if (base.TimeManager == null)
|
||||
return;
|
||||
if (subscribe == _clientSubscribed)
|
||||
return;
|
||||
|
||||
if (subscribe)
|
||||
{
|
||||
base.TimeManager.OnUpdate += TimeManager_OnUpdate;
|
||||
base.TimeManager.OnPreTick += TimeManager_OnPreTick;
|
||||
//Only client will use these events.
|
||||
if (!base.IsServer)
|
||||
{
|
||||
base.PredictionManager.OnPreReplicateReplay += PredictionManager_OnPreReplicateReplay;
|
||||
base.PredictionManager.OnPostReplicateReplay += PredictionManager_OnPostReplicateReplay;
|
||||
base.PredictionManager.OnPreReconcile += PredictionManager_OnPreReconcile;
|
||||
base.PredictionManager.OnPostReconcile += PredictionManager_OnPostReconcile;
|
||||
base.TimeManager.OnRoundTripTimeUpdated += TimeManager_OnRoundTripTimeUpdated;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
base.TimeManager.OnUpdate -= TimeManager_OnUpdate;
|
||||
base.TimeManager.OnPreTick -= TimeManager_OnPreTick;
|
||||
//Only client will use these events.
|
||||
if (!base.IsServer)
|
||||
{
|
||||
base.PredictionManager.OnPreReplicateReplay -= PredictionManager_OnPreReplicateReplay;
|
||||
base.PredictionManager.OnPostReplicateReplay -= PredictionManager_OnPostReplicateReplay;
|
||||
base.PredictionManager.OnPreReconcile -= PredictionManager_OnPreReconcile;
|
||||
base.PredictionManager.OnPostReconcile -= PredictionManager_OnPostReconcile;
|
||||
base.TimeManager.OnRoundTripTimeUpdated -= TimeManager_OnRoundTripTimeUpdated;
|
||||
}
|
||||
|
||||
//Also some resets
|
||||
_lastStateLocalTick = 0;
|
||||
_rigidbodyStates.Clear();
|
||||
_rigidbody2dStates.Clear();
|
||||
}
|
||||
|
||||
_clientSubscribed = subscribe;
|
||||
}
|
||||
|
||||
private void TimeManager_OnRoundTripTimeUpdated(long obj)
|
||||
{
|
||||
Rigidbodies_OnRoundTripTimeUpdated(obj);
|
||||
}
|
||||
|
||||
private void PredictionManager_OnPreServerReconcile(NetworkBehaviour obj)
|
||||
{
|
||||
SendRigidbodyState(obj);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called before physics is simulated when replaying a replicate method.
|
||||
/// Contains the PhysicsScene and PhysicsScene2D which was simulated.
|
||||
/// </summary>
|
||||
protected virtual void PredictionManager_OnPreReplicateReplay(uint tick, PhysicsScene ps, PhysicsScene2D ps2d)
|
||||
{
|
||||
_spectatorSmoother?.OnPreReplay(tick);
|
||||
Rigidbodies_PredictionManager_OnPreReplicateReplay(tick, ps, ps2d);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called after physics is simulated when replaying a replicate method.
|
||||
/// Contains the PhysicsScene and PhysicsScene2D which was simulated.
|
||||
/// </summary>
|
||||
private void PredictionManager_OnPostReplicateReplay(uint tick, PhysicsScene ps, PhysicsScene2D ps2d)
|
||||
{
|
||||
_spectatorSmoother?.OnPostReplay(tick);
|
||||
Rigidbodies_PredictionManager_OnPostReplicateReplay(tick, ps, ps2d);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called before performing a reconcile on NetworkBehaviour.
|
||||
/// </summary>
|
||||
private void PredictionManager_OnPreReconcile(NetworkBehaviour nb)
|
||||
{
|
||||
Rigidbodies_TimeManager_OnPreReconcile(nb);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called after performing a reconcile on NetworkBehaviour.
|
||||
/// </summary>
|
||||
private void PredictionManager_OnPostReconcile(NetworkBehaviour nb)
|
||||
{
|
||||
Rigidbodies_TimeManager_OnPostReconcile(nb);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a smoother with configured values.
|
||||
/// </summary>
|
||||
private void InitializeSmoother(bool ownerSmoother)
|
||||
{
|
||||
ResetGraphicalTransform();
|
||||
|
||||
if (ownerSmoother)
|
||||
{
|
||||
_ownerSmoother = new PredictedObjectOwnerSmoother();
|
||||
float teleportThreshold = (_enableTeleport) ? _teleportThreshold : -1f;
|
||||
_ownerSmoother.Initialize(this, _graphicalInstantiatedOffsetPosition, _graphicalInstantiatedOffsetRotation, _graphicalObject, _ownerSmoothPosition, _ownerSmoothRotation, _ownerInterpolation, teleportThreshold);
|
||||
}
|
||||
else
|
||||
{
|
||||
_spectatorSmoother = new PredictedObjectSpectatorSmoother();
|
||||
RigidbodyType rbType = (_predictionType == PredictionType.Rigidbody) ?
|
||||
RigidbodyType.Rigidbody : RigidbodyType.Rigidbody2D;
|
||||
float teleportThreshold = (_enableTeleport) ? _teleportThreshold : -1f;
|
||||
_spectatorSmoother.Initialize(this, rbType, _rigidbody, _rigidbody2d, _graphicalObject, _spectatorSmoothPosition, _spectatorSmoothRotation, teleportThreshold);
|
||||
}
|
||||
|
||||
void ResetGraphicalTransform()
|
||||
{
|
||||
_graphicalObject.position = (transform.position + _graphicalInstantiatedOffsetPosition);
|
||||
_graphicalObject.rotation = (_graphicalInstantiatedOffsetRotation * transform.rotation);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configures RigidbodyPauser with settings.
|
||||
/// </summary>
|
||||
private void ConfigureRigidbodies()
|
||||
{
|
||||
if (!IsRigidbodyPrediction)
|
||||
return;
|
||||
|
||||
_rigidbodyPauser = new RigidbodyPauser();
|
||||
if (_predictionType == PredictionType.Rigidbody)
|
||||
{
|
||||
_rigidbody.collisionDetectionMode = CollisionDetectionMode.Continuous;
|
||||
_rigidbodyPauser.UpdateRigidbodies(transform, RigidbodyType.Rigidbody, true, _graphicalObject);
|
||||
}
|
||||
else
|
||||
{
|
||||
_rigidbody2d.collisionDetectionMode = CollisionDetectionMode2D.Continuous;
|
||||
_rigidbodyPauser.UpdateRigidbodies(transform, RigidbodyType.Rigidbody2D, true, _graphicalObject);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configures NetworkTransform for prediction.
|
||||
/// </summary>
|
||||
private void ConfigureNetworkTransform()
|
||||
{
|
||||
if (!IsRigidbodyPrediction)
|
||||
_networkTransform?.ConfigureForCSP();
|
||||
}
|
||||
|
||||
|
||||
#if UNITY_EDITOR
|
||||
protected override void OnValidate()
|
||||
{
|
||||
if (Application.isPlaying)
|
||||
{
|
||||
InitializeSmoother(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_spectatorSmoothingType == SpectatorSmoothingType.Accuracy)
|
||||
_preconfiguredSmoothingDataPreview = _accurateSmoothingData;
|
||||
else if (_spectatorSmoothingType == SpectatorSmoothingType.Mixed)
|
||||
_preconfiguredSmoothingDataPreview = _mixedSmoothingData;
|
||||
else if (_spectatorSmoothingType == SpectatorSmoothingType.Gradual)
|
||||
_preconfiguredSmoothingDataPreview = _gradualSmoothingData;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 402926ef33e0a894d9fec352693988ac
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: bf9191e2e07d29749bca3a1ae44e4bc8, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -0,0 +1,299 @@
|
||||
using FishNet.Utility.Extension;
|
||||
using FishNet.Object;
|
||||
using System.Runtime.CompilerServices;
|
||||
using UnityEngine;
|
||||
|
||||
namespace FishNet.Component.Prediction
|
||||
{
|
||||
internal class PredictedObjectOwnerSmoother
|
||||
{
|
||||
#region Serialized.
|
||||
/// <summary>
|
||||
/// Transform which holds the graphical features of this object. This transform will be smoothed when desynchronizations occur.
|
||||
/// </summary>
|
||||
private Transform _graphicalObject;
|
||||
/// <summary>
|
||||
/// Sets GraphicalObject.
|
||||
/// </summary>
|
||||
/// <param name="value"></param>
|
||||
public void SetGraphicalObject(Transform value)
|
||||
{
|
||||
_graphicalObject = value;
|
||||
_networkBehaviour.transform.SetTransformOffsets(value, ref _graphicalInstantiatedOffsetPosition, ref _graphicalInstantiatedOffsetRotation);
|
||||
}
|
||||
/// <summary>
|
||||
/// NetworkBehaviour which is using this object.
|
||||
/// </summary>
|
||||
private NetworkBehaviour _networkBehaviour;
|
||||
/// <summary>
|
||||
/// How far the transform must travel in a single update to cause a teleport rather than smoothing. Using 0f will teleport every update.
|
||||
/// </summary>
|
||||
private float _teleportThreshold = 1f;
|
||||
/// <summary>
|
||||
/// How far in the past to keep the graphical object when owner.
|
||||
/// </summary>
|
||||
private byte _interpolation = 1;
|
||||
/// <summary>
|
||||
/// Sets the interpolation value to use when the owner of this object.
|
||||
/// </summary>
|
||||
/// <param name="value"></param>
|
||||
public void SetInterpolation(byte value) => _interpolation = value;
|
||||
#endregion
|
||||
|
||||
#region Private.
|
||||
/// <summary>
|
||||
/// World position before transform was predicted or reset.
|
||||
/// </summary>
|
||||
private Vector3 _graphicalStartPosition;
|
||||
/// <summary>
|
||||
/// World rotation before transform was predicted or reset.
|
||||
/// </summary>
|
||||
private Quaternion _graphicalStartRotation;
|
||||
/// <summary>
|
||||
/// GraphicalObject position difference from the PredictedObject when this is initialized.
|
||||
/// </summary>
|
||||
private Vector3 _graphicalInstantiatedOffsetPosition;
|
||||
/// <summary>
|
||||
/// How quickly to move towards TargetPosition.
|
||||
/// </summary>
|
||||
private float _positionMoveRate = -2;
|
||||
/// <summary>
|
||||
/// GraphicalObject rotation difference from the PredictedObject when this is initialized.
|
||||
/// </summary>
|
||||
private Quaternion _graphicalInstantiatedOffsetRotation;
|
||||
/// <summary>
|
||||
/// How quickly to move towards TargetRotation.
|
||||
/// </summary>
|
||||
private float _rotationMoveRate = -2;
|
||||
/// <summary>
|
||||
/// True if OnPreTick was received this frame.
|
||||
/// </summary>
|
||||
private bool _preTickReceived;
|
||||
/// <summary>
|
||||
/// True to move towards position goals.
|
||||
/// </summary>
|
||||
private bool _smoothPosition;
|
||||
/// <summary>
|
||||
/// True to move towards rotation goals.
|
||||
/// </summary>
|
||||
private bool _smoothRotation;
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Initializes this script for use.
|
||||
/// </summary>
|
||||
public void Initialize(NetworkBehaviour nb, Vector3 instantiatedOffsetPosition, Quaternion instantiatedOffsetRotation, Transform graphicalObject
|
||||
, bool smoothPosition, bool smoothRotation, byte interpolation, float teleportThreshold)
|
||||
{
|
||||
_networkBehaviour = nb;
|
||||
_graphicalInstantiatedOffsetPosition = instantiatedOffsetPosition;
|
||||
_graphicalInstantiatedOffsetRotation = instantiatedOffsetRotation;
|
||||
_graphicalObject = graphicalObject;
|
||||
|
||||
_smoothPosition = smoothPosition;
|
||||
_smoothRotation = smoothRotation;
|
||||
|
||||
_interpolation = interpolation;
|
||||
_teleportThreshold = teleportThreshold;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called every frame.
|
||||
/// </summary>
|
||||
public void ManualUpdate()
|
||||
{
|
||||
MoveToTarget();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when the TimeManager invokes OnPreTick.
|
||||
/// </summary>
|
||||
public void OnPreTick()
|
||||
{
|
||||
if (CanSmooth())
|
||||
{
|
||||
_preTickReceived = true;
|
||||
/* Only snap to destination if interpolation is 1.
|
||||
* This ensures the graphics will be at the proper location
|
||||
* before the next movement rates are calculated. */
|
||||
if (_interpolation == 1)
|
||||
ResetGraphicalToInstantiatedProperties(true, true);
|
||||
|
||||
SetGraphicalPreviousProperties();
|
||||
}
|
||||
}
|
||||
|
||||
public void OnPostTick()
|
||||
{
|
||||
if (CanSmooth() && _preTickReceived)
|
||||
{
|
||||
_preTickReceived = false;
|
||||
ResetGraphicalToPreviousProperties();
|
||||
SetGraphicalMoveRates();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if prediction can be used on this rigidbody.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private bool CanSmooth()
|
||||
{
|
||||
if (_interpolation == 0)
|
||||
return false;
|
||||
//Only owner needs smoothing.
|
||||
if (!_networkBehaviour.IsOwner && !_networkBehaviour.IsHost)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Moves transform to target values.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void MoveToTarget()
|
||||
{
|
||||
//Not set, meaning movement doesnt need to happen or completed.
|
||||
if (_positionMoveRate == -2f && _rotationMoveRate == -2f)
|
||||
return;
|
||||
|
||||
Vector3 posGoal = GetGraphicalGoalPosition();
|
||||
Quaternion rotGoal = GetGraphicalGoalRotation();
|
||||
|
||||
/* Only try to update properties if they have a valid move rate.
|
||||
* Properties may have 0f move rate if they did not change. */
|
||||
Transform t = _graphicalObject;
|
||||
float delta = Time.deltaTime;
|
||||
|
||||
//Position.
|
||||
if (SmoothPosition())
|
||||
{
|
||||
if (_positionMoveRate == -1f)
|
||||
ResetGraphicalToInstantiatedProperties(true, false);
|
||||
else if (_positionMoveRate > 0f)
|
||||
t.position = Vector3.MoveTowards(t.position, posGoal, _positionMoveRate * delta);
|
||||
}
|
||||
|
||||
//Rotation.
|
||||
if (SmoothRotation())
|
||||
{
|
||||
if (_rotationMoveRate == -1f)
|
||||
ResetGraphicalToInstantiatedProperties(false, true);
|
||||
else if (_rotationMoveRate > 0f)
|
||||
t.rotation = Quaternion.RotateTowards(t.rotation, rotGoal, _rotationMoveRate * delta);
|
||||
}
|
||||
|
||||
if (GraphicalObjectMatches(posGoal, rotGoal))
|
||||
{
|
||||
_positionMoveRate = -2f;
|
||||
_rotationMoveRate = -2f;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if this transform matches arguments.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private bool GraphicalObjectMatches(Vector3 position, Quaternion rotation)
|
||||
{
|
||||
bool positionMatches = (!_smoothPosition || (_graphicalObject.position == position));
|
||||
bool rotationMatches = (!_smoothRotation || (_graphicalObject.rotation == rotation));
|
||||
return (positionMatches && rotationMatches);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// True to smooth position. When false the graphicalObjects property will not be updated.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private bool SmoothPosition() => (_smoothPosition && (_networkBehaviour.IsOwner || _networkBehaviour.IsHost));
|
||||
/// <summary>
|
||||
/// True to smooth rotation. When false the graphicalObjects property will not be updated.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private bool SmoothRotation() => (_smoothRotation && (_networkBehaviour.IsOwner || _networkBehaviour.IsHost));
|
||||
|
||||
/// <summary>
|
||||
/// Sets Position and Rotation move rates to reach Target datas.
|
||||
/// </summary>
|
||||
private void SetGraphicalMoveRates()
|
||||
{
|
||||
float delta = ((float)_networkBehaviour.TimeManager.TickDelta * _interpolation);
|
||||
|
||||
float distance;
|
||||
distance = Vector3.Distance(_graphicalObject.position, GetGraphicalGoalPosition());
|
||||
//If qualifies for teleporting.
|
||||
if (_teleportThreshold != -1f && distance >= _teleportThreshold)
|
||||
{
|
||||
_positionMoveRate = -1f;
|
||||
_rotationMoveRate = -1f;
|
||||
}
|
||||
//Smoothing.
|
||||
else
|
||||
{
|
||||
_positionMoveRate = (distance / delta);
|
||||
distance = Quaternion.Angle(_graphicalObject.rotation, GetGraphicalGoalRotation());
|
||||
if (distance > 0f)
|
||||
_rotationMoveRate = (distance / delta);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a goal position for the graphical object.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private Vector3 GetGraphicalGoalPosition()
|
||||
{
|
||||
if (SmoothPosition())
|
||||
return (_networkBehaviour.transform.position + _graphicalInstantiatedOffsetPosition);
|
||||
else
|
||||
return _graphicalObject.position;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a goal rotation for the graphical object.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private Quaternion GetGraphicalGoalRotation()
|
||||
{
|
||||
if (SmoothRotation())
|
||||
return (_graphicalInstantiatedOffsetRotation * _networkBehaviour.transform.rotation);
|
||||
else
|
||||
return _graphicalObject.rotation;
|
||||
}
|
||||
/// <summary>
|
||||
/// Caches the graphical object' current position and rotation.
|
||||
/// </summary>
|
||||
private void SetGraphicalPreviousProperties()
|
||||
{
|
||||
_graphicalStartPosition = _graphicalObject.position;
|
||||
_graphicalStartRotation = _graphicalObject.rotation;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets the graphical object to cached position and rotation of the transform.
|
||||
/// </summary>
|
||||
private void ResetGraphicalToPreviousProperties()
|
||||
{
|
||||
_graphicalObject.SetPositionAndRotation(_graphicalStartPosition, _graphicalStartRotation);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets the graphical object to it's transform offsets during instantiation.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void ResetGraphicalToInstantiatedProperties(bool position, bool rotation)
|
||||
{
|
||||
if (position)
|
||||
_graphicalObject.position = GetGraphicalGoalPosition();
|
||||
if (rotation)
|
||||
_graphicalObject.rotation = GetGraphicalGoalRotation();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8b3bc1d8919cdf749971d3d4d44b198f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -0,0 +1,884 @@
|
||||
using FishNet.Object;
|
||||
using FishNet.Transporting;
|
||||
using FishNet.Utility.Extension;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using UnityEngine;
|
||||
|
||||
|
||||
namespace FishNet.Component.Prediction
|
||||
{
|
||||
internal class PredictedObjectSpectatorSmoother
|
||||
{
|
||||
#region Types.
|
||||
/// <summary>
|
||||
/// Data on a goal to move towards.
|
||||
/// </summary>
|
||||
private class GoalData
|
||||
{
|
||||
public bool IsActive;
|
||||
/// <summary>
|
||||
/// LocalTick this data is for.
|
||||
/// </summary>
|
||||
public uint LocalTick;
|
||||
/// <summary>
|
||||
/// Data on how fast to move to transform values.
|
||||
/// </summary>
|
||||
public RateData Rates = new RateData();
|
||||
/// <summary>
|
||||
/// Transform values to move towards.
|
||||
/// </summary>
|
||||
public TransformData Transforms = new TransformData();
|
||||
|
||||
public GoalData() { }
|
||||
/// <summary>
|
||||
/// Resets values for re-use.
|
||||
/// </summary>
|
||||
public void Reset()
|
||||
{
|
||||
LocalTick = 0;
|
||||
Transforms.Reset();
|
||||
Rates.Reset();
|
||||
IsActive = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates values using a GoalData.
|
||||
/// </summary>
|
||||
public void Update(GoalData gd)
|
||||
{
|
||||
LocalTick = gd.LocalTick;
|
||||
Rates.Update(gd.Rates);
|
||||
Transforms.Update(gd.Transforms);
|
||||
IsActive = true;
|
||||
}
|
||||
|
||||
public void Update(uint localTick, RateData rd, TransformData td)
|
||||
{
|
||||
LocalTick = localTick;
|
||||
Rates = rd;
|
||||
Transforms = td;
|
||||
IsActive = true;
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// How fast to move to values.
|
||||
/// </summary>
|
||||
private class RateData
|
||||
{
|
||||
/// <summary>
|
||||
/// Rate for position after smart calculations.
|
||||
/// </summary>
|
||||
public float Position;
|
||||
/// <summary>
|
||||
/// Rate for rotation after smart calculations.
|
||||
/// </summary>
|
||||
public float Rotation;
|
||||
/// <summary>
|
||||
/// Number of ticks the rates are calculated for.
|
||||
/// If TickSpan is 2 then the rates are calculated under the assumption the transform changed over 2 ticks.
|
||||
/// </summary>
|
||||
public uint TickSpan;
|
||||
/// <summary>
|
||||
/// Time remaining until transform is expected to reach it's goal.
|
||||
/// </summary>
|
||||
internal float TimeRemaining;
|
||||
|
||||
public RateData() { }
|
||||
|
||||
/// <summary>
|
||||
/// Resets values for re-use.
|
||||
/// </summary>
|
||||
public void Reset()
|
||||
{
|
||||
Position = 0f;
|
||||
Rotation = 0f;
|
||||
TickSpan = 0;
|
||||
TimeRemaining = 0f;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Update(RateData rd)
|
||||
{
|
||||
Update(rd.Position, rd.Rotation, rd.TickSpan, rd.TimeRemaining);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates rates.
|
||||
/// </summary>
|
||||
public void Update(float position, float rotation, uint tickSpan, float timeRemaining)
|
||||
{
|
||||
Position = position;
|
||||
Rotation = rotation;
|
||||
TickSpan = tickSpan;
|
||||
TimeRemaining = timeRemaining;
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// Data about where a transform should move towards.
|
||||
/// </summary>
|
||||
private class TransformData
|
||||
{
|
||||
/// <summary>
|
||||
/// Position of the transform.
|
||||
/// </summary>
|
||||
public Vector3 Position;
|
||||
/// <summary>
|
||||
/// Rotation of the transform.
|
||||
/// </summary>
|
||||
public Quaternion Rotation;
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
Position = Vector3.zero;
|
||||
Rotation = Quaternion.identity;
|
||||
}
|
||||
/// <summary>
|
||||
/// Updates this data.
|
||||
/// </summary>
|
||||
public void Update(TransformData copy)
|
||||
{
|
||||
Update(copy.Position, copy.Rotation);
|
||||
}
|
||||
/// <summary>
|
||||
/// Updates this data.
|
||||
/// </summary>
|
||||
public void Update(Vector3 position, Quaternion rotation)
|
||||
{
|
||||
Position = position;
|
||||
Rotation = rotation;
|
||||
}
|
||||
/// <summary>
|
||||
/// Updates this data.
|
||||
/// </summary>
|
||||
public void Update(Rigidbody rigidbody)
|
||||
{
|
||||
Position = rigidbody.transform.position;
|
||||
Rotation = rigidbody.transform.rotation;
|
||||
}
|
||||
/// <summary>
|
||||
/// Updates this data.
|
||||
/// </summary>
|
||||
public void Update(Rigidbody2D rigidbody)
|
||||
{
|
||||
Position = rigidbody.transform.position;
|
||||
Rotation = rigidbody.transform.rotation;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Private.
|
||||
/// <summary>
|
||||
/// Current GoalData being used.
|
||||
/// </summary>
|
||||
private GoalData _currentGoalData = new GoalData();
|
||||
/// <summary>
|
||||
/// Object to smooth.
|
||||
/// </summary>
|
||||
private Transform _graphicalObject;
|
||||
/// <summary>
|
||||
/// Sets GraphicalObject.
|
||||
/// </summary>
|
||||
/// <param name="value"></param>
|
||||
public void SetGraphicalObject(Transform value) => _graphicalObject = value;
|
||||
/// <summary>
|
||||
/// True to move towards position goals.
|
||||
/// </summary>
|
||||
private bool _smoothPosition;
|
||||
/// <summary>
|
||||
/// True to move towards rotation goals.
|
||||
/// </summary>
|
||||
private bool _smoothRotation;
|
||||
/// <summary>
|
||||
/// How far in the past to keep the graphical object.
|
||||
/// </summary>
|
||||
private uint _interpolation = 4;
|
||||
/// <summary>
|
||||
/// Sets the interpolation value to use when the owner of this object.
|
||||
/// </summary>
|
||||
/// <param name="value"></param>
|
||||
public void SetInterpolation(uint value) => _interpolation = value;
|
||||
/// <summary>
|
||||
/// GoalDatas to move towards.
|
||||
/// </summary>
|
||||
private List<GoalData> _goalDatas = new List<GoalData>();
|
||||
/// <summary>
|
||||
/// Rigidbody to use.
|
||||
/// </summary>
|
||||
private Rigidbody _rigidbody;
|
||||
/// <summary>
|
||||
/// Rigidbody2D to use.
|
||||
/// </summary>
|
||||
private Rigidbody2D _rigidbody2d;
|
||||
/// <summary>
|
||||
/// Transform state during PreTick.
|
||||
/// </summary>
|
||||
private TransformData _preTickTransformdata = new TransformData();
|
||||
/// <summary>
|
||||
/// Type of rigidbody being used.
|
||||
/// </summary>
|
||||
private RigidbodyType _rigidbodyType;
|
||||
/// <summary>
|
||||
/// Last tick which a reconcile occured. This is reset at the end of a tick.
|
||||
/// </summary>
|
||||
private long _reconcileLocalTick = -1;
|
||||
/// <summary>
|
||||
/// Called when this frame receives OnPreTick.
|
||||
/// </summary>
|
||||
private bool _preTickReceived;
|
||||
/// <summary>
|
||||
/// Start position for graphicalObject at the beginning of the tick.
|
||||
/// </summary>
|
||||
private Vector3 _graphicalStartPosition;
|
||||
/// <summary>
|
||||
/// Start rotation for graphicalObject at the beginning of the tick.
|
||||
/// </summary>
|
||||
private Quaternion _graphicalStartRotation;
|
||||
/// <summary>
|
||||
/// How far a distance change must exceed to teleport the graphical object. -1f indicates teleport is not enabled.
|
||||
/// </summary>
|
||||
private float _teleportThreshold;
|
||||
/// <summary>
|
||||
/// PredictedObject which is using this object.
|
||||
/// </summary>
|
||||
private PredictedObject _predictedObject;
|
||||
/// <summary>
|
||||
/// Cache of GoalDatas to prevent allocations.
|
||||
/// </summary>
|
||||
private static Stack<GoalData> _goalDataCache = new Stack<GoalData>();
|
||||
/// <summary>
|
||||
/// Cached localtick for performance.
|
||||
/// </summary>
|
||||
private uint _localTick;
|
||||
/// <summary>
|
||||
/// Number of ticks to ignore when replaying.
|
||||
/// </summary>
|
||||
private uint _ignoredTicks;
|
||||
/// <summary>
|
||||
/// Start position of the graphical object in world space.
|
||||
/// </summary>
|
||||
private Vector3 _startWorldPosition;
|
||||
#endregion
|
||||
|
||||
#region Const.
|
||||
/// <summary>
|
||||
/// Multiplier to apply to movement speed when buffer is over interpolation.
|
||||
/// </summary>
|
||||
private const float OVERFLOW_MULTIPLIER = 0.1f;
|
||||
/// <summary>
|
||||
/// Multiplier to apply to movement speed when buffer is under interpolation.
|
||||
/// </summary>
|
||||
private const float UNDERFLOW_MULTIPLIER = 0.02f;
|
||||
#endregion
|
||||
|
||||
public void SetIgnoredTicks(uint value) => _ignoredTicks = value;
|
||||
/// <summary>
|
||||
/// Initializes this for use.
|
||||
/// </summary>
|
||||
internal void Initialize(PredictedObject po, RigidbodyType rbType, Rigidbody rb, Rigidbody2D rb2d, Transform graphicalObject
|
||||
, bool smoothPosition, bool smoothRotation, float teleportThreshold)
|
||||
{
|
||||
_predictedObject = po;
|
||||
_rigidbodyType = rbType;
|
||||
|
||||
_rigidbody = rb;
|
||||
_rigidbody2d = rb2d;
|
||||
_graphicalObject = graphicalObject;
|
||||
_startWorldPosition = _graphicalObject.position;
|
||||
_smoothPosition = smoothPosition;
|
||||
_smoothRotation = smoothRotation;
|
||||
_teleportThreshold = teleportThreshold;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <summary>
|
||||
/// Called every frame.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void ManualUpdate()
|
||||
{
|
||||
if (CanSmooth())
|
||||
MoveToTarget();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when the TimeManager invokes OnPreTick.
|
||||
/// </summary>
|
||||
public void OnPreTick()
|
||||
{
|
||||
if (CanSmooth())
|
||||
{
|
||||
_localTick = _predictedObject.TimeManager.LocalTick;
|
||||
if (!_preTickReceived)
|
||||
{
|
||||
uint tick = _predictedObject.TimeManager.LocalTick - 1;
|
||||
CreateGoalData(tick, false);
|
||||
}
|
||||
_preTickReceived = true;
|
||||
|
||||
if (_rigidbodyType == RigidbodyType.Rigidbody)
|
||||
_preTickTransformdata.Update(_rigidbody);
|
||||
else
|
||||
_preTickTransformdata.Update(_rigidbody2d);
|
||||
|
||||
_graphicalStartPosition = _graphicalObject.position;
|
||||
_graphicalStartRotation = _graphicalObject.rotation;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when the TimeManager invokes OnPostTick.
|
||||
/// </summary>
|
||||
public void OnPostTick()
|
||||
{
|
||||
if (CanSmooth())
|
||||
{
|
||||
if (!_preTickReceived)
|
||||
{
|
||||
/* During test the Z value for applyImmediately is 5.9.
|
||||
* Then increased 1 unit per tick: 6.9, 7.9.
|
||||
*
|
||||
* When the spectator smoother initializes 5.9 is shown.
|
||||
* Before first starting smoothing the transform needs to be set
|
||||
* back to that.
|
||||
*
|
||||
* The second issue is the first addition to goal datas seems
|
||||
* to occur at 7.9. This would need to be 6.9 to move from the
|
||||
* proper 5.9 starting point. It's probably because pretick is not received
|
||||
* when OnPostTick is called at the 6.9 position.
|
||||
*
|
||||
* Have not validated the above yet but that's the most likely situation since
|
||||
* we know this was initialized at 5.9, which means it would be assumed pretick would
|
||||
* call at 6.9. Perhaps the following is happening....
|
||||
*
|
||||
* - Pretick.
|
||||
* - Client gets spawn+applyImmediately.
|
||||
* - This also initializes this script at 5.9.
|
||||
* - Simulation moves object to 6.9.
|
||||
* - PostTick.
|
||||
* - This script does not run because _preTickReceived is not set yet.
|
||||
*
|
||||
* - Pretick. Sets _preTickReceived.
|
||||
* - Simulation moves object to 7.9.
|
||||
* - PostTick.
|
||||
* - The first goalData is created for 7.9.
|
||||
*
|
||||
* In writing the theory checks out.
|
||||
* Perhaps the solution could be simple as creating a goal
|
||||
* during pretick if _preTickReceived is being set for
|
||||
* the first time. Might need to reduce tick by 1
|
||||
* when setting goalData for this; not sure yet.
|
||||
*/
|
||||
_graphicalObject.SetPositionAndRotation(_startWorldPosition, Quaternion.identity);
|
||||
return;
|
||||
}
|
||||
|
||||
_graphicalObject.SetPositionAndRotation(_graphicalStartPosition, _graphicalStartRotation);
|
||||
CreateGoalData(_predictedObject.TimeManager.LocalTick, true);
|
||||
}
|
||||
}
|
||||
|
||||
public void OnPreReplay(uint tick)
|
||||
{
|
||||
if (!_preTickReceived)
|
||||
{
|
||||
if (CanSmooth())
|
||||
{
|
||||
//if (_localTick - tick < _ignoredTicks)
|
||||
// return;
|
||||
|
||||
CreateGoalData(tick, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called after a reconcile runs a replay.
|
||||
/// </summary>
|
||||
public void OnPostReplay(uint tick)
|
||||
{
|
||||
if (CanSmooth())
|
||||
{
|
||||
if (_reconcileLocalTick == -1)
|
||||
return;
|
||||
|
||||
CreateGoalData(tick, false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if the graphics can be smoothed.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private bool CanSmooth()
|
||||
{
|
||||
if (_interpolation == 0)
|
||||
return false;
|
||||
if (_predictedObject.IsPredictingOwner() || _predictedObject.IsServer)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Sets the last tick a reconcile occurred.
|
||||
/// </summary>
|
||||
/// <param name="value"></param>
|
||||
public void SetLocalReconcileTick(long value)
|
||||
{
|
||||
_reconcileLocalTick = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Caches a GoalData.
|
||||
/// </summary>
|
||||
/// <param name="gd"></param>
|
||||
private void StoreGoalData(GoalData gd)
|
||||
{
|
||||
gd.Reset();
|
||||
_goalDataCache.Push(gd);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if this transform matches arguments.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private bool GraphicalObjectMatches(Vector3 localPosition, Quaternion localRotation)
|
||||
{
|
||||
bool positionMatches = (!_smoothPosition || _graphicalObject.position == localPosition);
|
||||
bool rotationMatches = (!_smoothRotation || _graphicalObject.rotation == localRotation);
|
||||
return (positionMatches && rotationMatches);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if there is any change between two datas.
|
||||
/// </summary>
|
||||
private bool HasChanged(TransformData a, TransformData b)
|
||||
{
|
||||
return (a.Position != b.Position) ||
|
||||
(a.Rotation != b.Rotation);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if the transform differs from td.
|
||||
/// </summary>
|
||||
private bool HasChanged(TransformData td)
|
||||
{
|
||||
Transform rigidbodyTransform;
|
||||
|
||||
if (_rigidbodyType == RigidbodyType.Rigidbody)
|
||||
rigidbodyTransform = _rigidbody.transform;
|
||||
else
|
||||
rigidbodyTransform = _rigidbody2d.transform;
|
||||
|
||||
bool changed = (td.Position != rigidbodyTransform.position) || (td.Rotation != rigidbodyTransform.rotation);
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets CurrentGoalData to the next in queue.
|
||||
/// </summary>
|
||||
private void SetCurrentGoalData(bool afterMove)
|
||||
{
|
||||
if (_goalDatas.Count == 0)
|
||||
{
|
||||
_currentGoalData.IsActive = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
//if (!afterMove && _goalDatas.Count < _interpolation)
|
||||
// return;
|
||||
|
||||
//Update current to next.
|
||||
_currentGoalData.Update(_goalDatas[0]);
|
||||
//Store old and remove it.
|
||||
StoreGoalData(_goalDatas[0]);
|
||||
_goalDatas.RemoveAt(0);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Moves to a GoalData. Automatically determins if to use data from server or client.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void MoveToTarget(float deltaOverride = -1f)
|
||||
{
|
||||
/* If the current goal data is not active then
|
||||
* try to set a new one. If none are available
|
||||
* it will remain inactive. */
|
||||
if (!_currentGoalData.IsActive)
|
||||
{
|
||||
SetCurrentGoalData(false);
|
||||
//If still inactive then it could not be updated.
|
||||
if (!_currentGoalData.IsActive)
|
||||
return;
|
||||
}
|
||||
|
||||
float delta = (deltaOverride != -1f) ? deltaOverride : Time.deltaTime;
|
||||
/* Once here it's safe to assume the object will be moving.
|
||||
* Any checks which would stop it from moving be it client
|
||||
* auth and owner, or server controlled and server, ect,
|
||||
* would have already been run. */
|
||||
TransformData td = _currentGoalData.Transforms;
|
||||
RateData rd = _currentGoalData.Rates;
|
||||
|
||||
int queueCount = _goalDatas.Count;
|
||||
/* Begin moving even if interpolation buffer isn't
|
||||
* met to provide more real-time interactions but
|
||||
* speed up when buffer is too large. This should
|
||||
* provide a good balance of accuracy. */
|
||||
|
||||
float multiplier;
|
||||
int countOverInterpolation = (queueCount - (int)_interpolation);
|
||||
if (countOverInterpolation > 0)
|
||||
{
|
||||
float overflowMultiplier = (!_predictedObject.IsOwner) ? OVERFLOW_MULTIPLIER : (OVERFLOW_MULTIPLIER * 1f);
|
||||
multiplier = 1f + overflowMultiplier;
|
||||
}
|
||||
else if (countOverInterpolation < 0)
|
||||
{
|
||||
float value = (UNDERFLOW_MULTIPLIER * Mathf.Abs(countOverInterpolation));
|
||||
const float maximum = 0.9f;
|
||||
if (value > maximum)
|
||||
value = maximum;
|
||||
multiplier = 1f - value;
|
||||
}
|
||||
else
|
||||
{
|
||||
multiplier = 1f;
|
||||
}
|
||||
|
||||
//Rate to update. Changes per property.
|
||||
float rate;
|
||||
Transform t = _graphicalObject;
|
||||
|
||||
//Position.
|
||||
if (_smoothPosition)
|
||||
{
|
||||
rate = rd.Position;
|
||||
Vector3 posGoal = td.Position;
|
||||
if (rate == -1f)
|
||||
t.position = td.Position;
|
||||
else if (rate > 0f)
|
||||
t.position = Vector3.MoveTowards(t.position, posGoal, rate * delta * multiplier);
|
||||
}
|
||||
|
||||
//Rotation.
|
||||
if (_smoothRotation)
|
||||
{
|
||||
rate = rd.Rotation;
|
||||
if (rate == -1f)
|
||||
t.rotation = td.Rotation;
|
||||
else if (rate > 0f)
|
||||
t.rotation = Quaternion.RotateTowards(t.rotation, td.Rotation, rate * delta);
|
||||
}
|
||||
|
||||
//Subtract time remaining for movement to complete.
|
||||
if (rd.TimeRemaining > 0f)
|
||||
{
|
||||
float subtractionAmount = (delta * multiplier);
|
||||
float timeRemaining = rd.TimeRemaining - subtractionAmount;
|
||||
rd.TimeRemaining = timeRemaining;
|
||||
}
|
||||
|
||||
//If movement shoudl be complete.
|
||||
if (rd.TimeRemaining <= 0f)
|
||||
{
|
||||
float leftOver = Mathf.Abs(rd.TimeRemaining);
|
||||
//Set to next goal data if available.
|
||||
SetCurrentGoalData(true);
|
||||
|
||||
//New data was set.
|
||||
if (_currentGoalData.IsActive)
|
||||
{
|
||||
if (leftOver > 0f)
|
||||
MoveToTarget(leftOver);
|
||||
}
|
||||
//No more in buffer, see if can extrapolate.
|
||||
else
|
||||
{
|
||||
/* Everything should line up when
|
||||
* time remaining is <= 0f but incase it's not,
|
||||
* such as if the user manipulated the grapihc object
|
||||
* somehow, then set goaldata active again to continue
|
||||
* moving it until it lines up with the goal. */
|
||||
if (!GraphicalObjectMatches(td.Position, td.Rotation))
|
||||
_currentGoalData.IsActive = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#region Rates.
|
||||
/// <summary>
|
||||
/// Sets move rates which will occur instantly.
|
||||
/// </summary>
|
||||
private void SetInstantRates(RateData rd)
|
||||
{
|
||||
rd.Update(-1f, -1f, 1, -1f);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets move rates which will occur over time.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void SetCalculatedRates(GoalData prevGoalData, GoalData nextGoalData, Channel channel)
|
||||
{
|
||||
/* Only update rates if data has changed.
|
||||
* When data comes in reliably for eventual consistency
|
||||
* it's possible that it will be the same as the last
|
||||
* unreliable packet. When this happens no change has occurred
|
||||
* and the distance of change woudl also be 0; this prevents
|
||||
* the NT from moving. Only need to compare data if channel is reliable. */
|
||||
TransformData nextTd = nextGoalData.Transforms;
|
||||
if (channel == Channel.Reliable && HasChanged(prevGoalData.Transforms, nextTd))
|
||||
{
|
||||
nextGoalData.Rates.Update(prevGoalData.Rates);
|
||||
return;
|
||||
}
|
||||
|
||||
uint lastTick = prevGoalData.LocalTick;
|
||||
/* How much time has passed between last update and current.
|
||||
* If set to 0 then that means the transform has
|
||||
* settled. */
|
||||
if (lastTick == 0)
|
||||
lastTick = (nextGoalData.LocalTick - 1);
|
||||
|
||||
uint tickDifference = (nextGoalData.LocalTick - lastTick);
|
||||
float timePassed = (float)_predictedObject.TimeManager.TicksToTime(tickDifference);
|
||||
RateData nextRd = nextGoalData.Rates;
|
||||
|
||||
//Distance between properties.
|
||||
float distance;
|
||||
//Position.
|
||||
Vector3 lastPosition = prevGoalData.Transforms.Position;
|
||||
distance = Vector3.Distance(lastPosition, nextTd.Position);
|
||||
//If distance teleports assume rest do.
|
||||
if (_teleportThreshold >= 0f && distance >= _teleportThreshold)
|
||||
{
|
||||
SetInstantRates(nextRd);
|
||||
return;
|
||||
}
|
||||
|
||||
//Position distance already calculated.
|
||||
float positionRate = (distance / timePassed);
|
||||
//Rotation.
|
||||
distance = prevGoalData.Transforms.Rotation.Angle(nextTd.Rotation, true);
|
||||
float rotationRate = (distance / timePassed);
|
||||
|
||||
/* If no speed then snap just in case.
|
||||
* 0f could be from floating errors. */
|
||||
if (positionRate == 0f)
|
||||
positionRate = -1f;
|
||||
if (rotationRate == 0f)
|
||||
rotationRate = -1f;
|
||||
|
||||
nextRd.Update(positionRate, rotationRate, tickDifference, timePassed);
|
||||
}
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new goal data for tick. The result will be placed into the goalDatas queue at it's proper position.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void CreateGoalData(uint tick, bool postTick)
|
||||
{
|
||||
/* It's possible a removed entry would void further
|
||||
* logic so remove excess entires first. */
|
||||
|
||||
/* Remove entries which are excessive to the buffer.
|
||||
* This could create a starting jitter but it will ensure
|
||||
* the buffer does not fill too much. The buffer next should
|
||||
* actually get unreasonably high but rather safe than sorry. */
|
||||
int maximumBufferAllowance = ((int)_interpolation * 8);
|
||||
int removedBufferCount = (_goalDatas.Count - maximumBufferAllowance);
|
||||
//If there are some to remove.
|
||||
if (removedBufferCount > 0)
|
||||
{
|
||||
for (int i = 0; i < removedBufferCount; i++)
|
||||
StoreGoalData(_goalDatas[0 + i]);
|
||||
_goalDatas.RemoveRange(0, removedBufferCount);
|
||||
}
|
||||
|
||||
uint currentGoalDataTick = _currentGoalData.LocalTick;
|
||||
//Tick has already been interpolated past, no reason to process it.
|
||||
if (tick <= currentGoalDataTick)
|
||||
return;
|
||||
|
||||
//GoalData from previous calculation.
|
||||
GoalData prevGoalData;
|
||||
int datasCount = _goalDatas.Count;
|
||||
/* Where to insert next data. This could have value
|
||||
* somewhere in the middle of goalDatas if the tick
|
||||
* is a replay rather than post tick. */
|
||||
int injectionIndex = datasCount + 1;
|
||||
//If being added at the end of a tick rather than from replay.
|
||||
if (postTick)
|
||||
{
|
||||
//Becomes true if transform differs from previous data.
|
||||
bool changed;
|
||||
|
||||
//If there is no goal data then create one using pretick data.
|
||||
if (datasCount == 0)
|
||||
{
|
||||
prevGoalData = MakeGoalDataFromPreTickTransform();
|
||||
changed = HasChanged(prevGoalData.Transforms);
|
||||
}
|
||||
//If there's goal datas grab the last, it will always be the tick before.
|
||||
else
|
||||
{
|
||||
prevGoalData = _goalDatas[datasCount - 1];
|
||||
/* If the tick is not exactly 1 past the last
|
||||
* then there's gaps in the saved values. This can
|
||||
* occur if the transform went idle and the buffer
|
||||
* hasn't emptied out yet. When this occurs use the
|
||||
* preTick data to calculate differences. */
|
||||
if (tick - prevGoalData.LocalTick != 1)
|
||||
prevGoalData = MakeGoalDataFromPreTickTransform();
|
||||
|
||||
changed = HasChanged(prevGoalData.Transforms);
|
||||
}
|
||||
|
||||
//Nothing has changed so no further action is required.
|
||||
if (!changed)
|
||||
{
|
||||
if (datasCount > 0 && prevGoalData != _goalDatas[datasCount - 1])
|
||||
StoreGoalData(prevGoalData);
|
||||
return;
|
||||
}
|
||||
}
|
||||
//Not post tick so it's from a replay.
|
||||
else
|
||||
{
|
||||
int prevIndex = -1;
|
||||
/* If the tick is 1 past current goalData
|
||||
* then it's the next in line for smoothing
|
||||
* from the current.
|
||||
* When this occurs use currentGoalData as
|
||||
* the previous. */
|
||||
if (tick == (currentGoalDataTick + 1))
|
||||
{
|
||||
prevGoalData = _currentGoalData;
|
||||
injectionIndex = 0;
|
||||
}
|
||||
//When not the next in line find out where to place data.
|
||||
else
|
||||
{
|
||||
if (tick > 0)
|
||||
prevGoalData = GetGoalData(tick - 1, out prevIndex);
|
||||
//Cannot find prevGoalData if tick is 0.
|
||||
else
|
||||
prevGoalData = null;
|
||||
}
|
||||
|
||||
//If previous goalData was found then inject just past the previous value.
|
||||
if (prevIndex != -1)
|
||||
injectionIndex = prevIndex + 1;
|
||||
|
||||
/* Should previous goalData be null then it could not be found.
|
||||
* Create a new previous goal data based on rigidbody state
|
||||
* during pretick. */
|
||||
if (prevGoalData == null)
|
||||
{
|
||||
//Create a goaldata based on information. If it differs from pretick then throw.
|
||||
GoalData gd = RetrieveGoalData();
|
||||
gd.Transforms.Update(_preTickTransformdata);
|
||||
|
||||
if (HasChanged(gd.Transforms))
|
||||
{
|
||||
prevGoalData = gd;
|
||||
}
|
||||
else
|
||||
{
|
||||
StoreGoalData(gd);
|
||||
return;
|
||||
}
|
||||
}
|
||||
/* Previous goal data is not active.
|
||||
* This should not be possible but this
|
||||
* is here as a sanity check anyway. */
|
||||
else if (!prevGoalData.IsActive)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
//Begin building next goal data.
|
||||
GoalData nextGoalData = RetrieveGoalData();
|
||||
nextGoalData.LocalTick = tick;
|
||||
//Set next transform data.
|
||||
TransformData nextTd = nextGoalData.Transforms;
|
||||
if (_rigidbodyType == RigidbodyType.Rigidbody)
|
||||
nextTd.Update(_rigidbody);
|
||||
else
|
||||
nextTd.Update(_rigidbody2d);
|
||||
|
||||
/* Reset properties if smoothing is not enabled
|
||||
* for them. It's less checks and easier to do it
|
||||
* after the nextGoalData is populated. */
|
||||
if (!_smoothPosition)
|
||||
nextTd.Position = _graphicalStartPosition;
|
||||
if (!_smoothRotation)
|
||||
nextTd.Rotation = _graphicalStartRotation;
|
||||
|
||||
//Calculate rates for prev vs next data.
|
||||
SetCalculatedRates(prevGoalData, nextGoalData, Channel.Unreliable);
|
||||
/* If injectionIndex would place at the end
|
||||
* then add. to goalDatas. */
|
||||
if (injectionIndex >= _goalDatas.Count)
|
||||
_goalDatas.Add(nextGoalData);
|
||||
//Otherwise insert into the proper location.
|
||||
else
|
||||
_goalDatas[injectionIndex].Update(nextGoalData);
|
||||
|
||||
//Makes previous goal data from transforms pretick values.
|
||||
GoalData MakeGoalDataFromPreTickTransform()
|
||||
{
|
||||
GoalData gd = RetrieveGoalData();
|
||||
//RigidbodyData contains the data from preTick.
|
||||
gd.Transforms.Update(_preTickTransformdata);
|
||||
//No need to update rates because this is just a starting point reference for interpolation.
|
||||
return gd;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the GoalData at tick.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private GoalData GetGoalData(uint tick, out int index)
|
||||
{
|
||||
index = -1;
|
||||
if (tick == 0)
|
||||
return null;
|
||||
|
||||
for (int i = 0; i < _goalDatas.Count; i++)
|
||||
{
|
||||
if (_goalDatas[i].LocalTick == tick)
|
||||
{
|
||||
index = i;
|
||||
return _goalDatas[i];
|
||||
}
|
||||
}
|
||||
|
||||
//Not found.
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Returns a GoalData from the cache.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private GoalData RetrieveGoalData()
|
||||
{
|
||||
GoalData result = (_goalDataCache.Count > 0) ? _goalDataCache.Pop() : new GoalData();
|
||||
result.IsActive = true;
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6bb311b5dc9602a4f8e4a8c6e64b21cb
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -0,0 +1,361 @@
|
||||
using FishNet.Managing;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
|
||||
namespace FishNet.Component.Prediction
|
||||
{
|
||||
/// <summary>
|
||||
/// Pauses and unpauses rigidbodies. While paused rigidbodies cannot be interacted with or simulated.
|
||||
/// </summary>
|
||||
public class RigidbodyPauser
|
||||
{
|
||||
#region Types.
|
||||
/// <summary>
|
||||
/// Data for a rigidbody before being set kinematic.
|
||||
/// </summary>
|
||||
private struct RigidbodyData
|
||||
{
|
||||
/// <summary>
|
||||
/// Rigidbody for data.
|
||||
/// </summary>
|
||||
public Rigidbody Rigidbody;
|
||||
/// <summary>
|
||||
/// Cached velocity when being set kinematic.
|
||||
/// </summary>
|
||||
public Vector3 Velocity;
|
||||
/// <summary>
|
||||
/// Cached velocity when being set kinematic.
|
||||
/// </summary>
|
||||
public Vector3 AngularVelocity;
|
||||
/// <summary>
|
||||
/// Scene of this rigidbody when being set kinematic.
|
||||
/// </summary>
|
||||
public Scene SimulatedScene;
|
||||
/// <summary>
|
||||
/// True if the rigidbody was kinematic prior to being paused.
|
||||
/// </summary>
|
||||
public bool IsKinematic;
|
||||
|
||||
public RigidbodyData(Rigidbody rb)
|
||||
{
|
||||
Rigidbody = rb;
|
||||
Rigidbody.collisionDetectionMode = CollisionDetectionMode.Continuous;
|
||||
Velocity = Vector3.zero;
|
||||
AngularVelocity = Vector3.zero;
|
||||
SimulatedScene = rb.gameObject.scene;
|
||||
IsKinematic = rb.isKinematic;
|
||||
}
|
||||
|
||||
public void Update(Rigidbody rb)
|
||||
{
|
||||
Velocity = rb.velocity;
|
||||
AngularVelocity = rb.angularVelocity;
|
||||
SimulatedScene = rb.gameObject.scene;
|
||||
IsKinematic = rb.isKinematic;
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// Data for a rigidbody2d before being set kinematic.
|
||||
/// </summary>
|
||||
private struct Rigidbody2DData
|
||||
{
|
||||
/// <summary>
|
||||
/// Rigidbody for data.
|
||||
/// </summary>
|
||||
public Rigidbody2D Rigidbody2d;
|
||||
/// <summary>
|
||||
/// Cached velocity when being set kinematic.
|
||||
/// </summary>
|
||||
public Vector2 Velocity;
|
||||
/// <summary>
|
||||
/// Cached velocity when being set kinematic.
|
||||
/// </summary>
|
||||
public float AngularVelocity;
|
||||
/// <summary>
|
||||
/// Scene of this rigidbody when being set kinematic.
|
||||
/// </summary>
|
||||
public Scene SimulatedScene;
|
||||
/// <summary>
|
||||
/// True if the rigidbody was simulated prior to being paused.
|
||||
/// </summary>
|
||||
public bool Simulated;
|
||||
|
||||
public Rigidbody2DData(Rigidbody2D rb)
|
||||
{
|
||||
Rigidbody2d = rb;
|
||||
Rigidbody2d.collisionDetectionMode = CollisionDetectionMode2D.Continuous;
|
||||
Velocity = Vector2.zero;
|
||||
AngularVelocity = 0f;
|
||||
SimulatedScene = rb.gameObject.scene;
|
||||
Simulated = rb.simulated;
|
||||
}
|
||||
|
||||
public void Update(Rigidbody2D rb)
|
||||
{
|
||||
Velocity = rb.velocity;
|
||||
AngularVelocity = rb.angularVelocity;
|
||||
SimulatedScene = rb.gameObject.scene;
|
||||
Simulated = rb.simulated;
|
||||
}
|
||||
}
|
||||
#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 List<RigidbodyData>();
|
||||
/// <summary>
|
||||
/// Rigidbody2D datas for found rigidbodies;
|
||||
/// </summary>
|
||||
private List<Rigidbody2DData> _rigidbody2dDatas = new List<Rigidbody2DData>();
|
||||
/// <summary>
|
||||
/// Type of prediction movement which is being used.
|
||||
/// </summary>
|
||||
private RigidbodyType _rigidbodyType;
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
private static Scene _kinematicSceneCache;
|
||||
/// <summary>
|
||||
/// Scene used to simulate kinematic rigidbodies.
|
||||
/// </summary>
|
||||
private static Scene _kinematicScene
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!_kinematicSceneCache.IsValid())
|
||||
_kinematicSceneCache = SceneManager.CreateScene("RigidbodyPauser_Kinematic", new CreateSceneParameters(LocalPhysicsMode.Physics2D | LocalPhysicsMode.Physics3D));
|
||||
return _kinematicSceneCache;
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// Parent of GraphicalObject prior to unparenting.
|
||||
/// </summary>
|
||||
private Transform _graphicalParent;
|
||||
/// <summary>
|
||||
/// GraphicalObject to unparent when pausing.
|
||||
/// </summary>
|
||||
private Transform _graphicalObject;
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Assigns rigidbodies.
|
||||
/// </summary>
|
||||
/// <param name="rbs">Rigidbodies2D to use.</param>
|
||||
public void UpdateRigidbodies(Transform t, RigidbodyType rbType, bool getInChildren, Transform graphicalObject)
|
||||
{
|
||||
_rigidbodyType = rbType;
|
||||
_rigidbodyDatas.Clear();
|
||||
_rigidbody2dDatas.Clear();
|
||||
|
||||
//3D.
|
||||
if (rbType == RigidbodyType.Rigidbody)
|
||||
{
|
||||
if (getInChildren)
|
||||
{
|
||||
Rigidbody[] rbs = t.GetComponentsInChildren<Rigidbody>();
|
||||
for (int i = 0; i < rbs.Length; i++)
|
||||
_rigidbodyDatas.Add(new RigidbodyData(rbs[i]));
|
||||
}
|
||||
else
|
||||
{
|
||||
Rigidbody rb = t.GetComponent<Rigidbody>();
|
||||
if (rb != null)
|
||||
_rigidbodyDatas.Add(new RigidbodyData(rb));
|
||||
}
|
||||
|
||||
//Make sure all added datas are not the graphical object.
|
||||
for (int i = 0; i < _rigidbodyDatas.Count; i++)
|
||||
{
|
||||
if (_rigidbodyDatas[i].Rigidbody.transform == graphicalObject)
|
||||
{
|
||||
NetworkManager.StaticLogError($"GameObject {t.name} has it's GraphicalObject as a child or on the same object as a Rigidbody object. The GraphicalObject must be a child of root, and not sit beneath or on any rigidbodies.");
|
||||
graphicalObject = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
//2D.
|
||||
else
|
||||
{
|
||||
if (getInChildren)
|
||||
{
|
||||
Rigidbody2D[] rbs = t.GetComponentsInChildren<Rigidbody2D>();
|
||||
for (int i = 0; i < rbs.Length; i++)
|
||||
_rigidbody2dDatas.Add(new Rigidbody2DData(rbs[i]));
|
||||
}
|
||||
else
|
||||
{
|
||||
Rigidbody2D rb = t.GetComponent<Rigidbody2D>();
|
||||
if (rb != null)
|
||||
_rigidbody2dDatas.Add(new Rigidbody2DData(rb));
|
||||
}
|
||||
|
||||
//Make sure all added datas are not the graphical object.
|
||||
for (int i = 0; i < _rigidbody2dDatas.Count; i++)
|
||||
{
|
||||
if (_rigidbody2dDatas[i].Rigidbody2d.transform == graphicalObject)
|
||||
{
|
||||
NetworkManager.StaticLogError($"GameObject {t.name} has it's GraphicalObject as a child or on the same object as a Rigidbody object. The GraphicalObject must be a child of root, and not sit beneath or on any rigidbodies.");
|
||||
graphicalObject = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (graphicalObject != null)
|
||||
{
|
||||
_graphicalObject = graphicalObject;
|
||||
_graphicalParent = graphicalObject.parent;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unpauses rigidbodies allowing them to interact normally.
|
||||
/// </summary>
|
||||
public void Unpause()
|
||||
{
|
||||
if (!Paused)
|
||||
return;
|
||||
Paused = false;
|
||||
|
||||
//3D.
|
||||
if (_rigidbodyType == RigidbodyType.Rigidbody)
|
||||
{
|
||||
for (int i = 0; i < _rigidbodyDatas.Count; i++)
|
||||
{
|
||||
if (!UnpauseRigidbody(i))
|
||||
{
|
||||
_rigidbodyDatas.RemoveAt(i);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
|
||||
//Sets isKinematic status and returns if successful.
|
||||
bool UnpauseRigidbody(int index)
|
||||
{
|
||||
RigidbodyData rbData = _rigidbodyDatas[index];
|
||||
Rigidbody rb = rbData.Rigidbody;
|
||||
if (rb == null)
|
||||
return false;
|
||||
|
||||
rb.velocity = rbData.Velocity;
|
||||
rb.angularVelocity = rbData.AngularVelocity;
|
||||
rb.isKinematic = rbData.IsKinematic;
|
||||
SceneManager.MoveGameObjectToScene(rb.transform.root.gameObject, rbData.SimulatedScene);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
//2D.
|
||||
else
|
||||
{
|
||||
for (int i = 0; i < _rigidbody2dDatas.Count; i++)
|
||||
{
|
||||
if (!UnpauseRigidbody(i))
|
||||
{
|
||||
_rigidbody2dDatas.RemoveAt(i);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
|
||||
//Sets isKinematic status and returns if successful.
|
||||
bool UnpauseRigidbody(int index)
|
||||
{
|
||||
Rigidbody2DData rbData = _rigidbody2dDatas[index];
|
||||
Rigidbody2D rb = rbData.Rigidbody2d;
|
||||
if (rb == null)
|
||||
return false;
|
||||
|
||||
rb.velocity = rbData.Velocity;
|
||||
rb.angularVelocity = rbData.AngularVelocity;
|
||||
rb.simulated = rbData.Simulated;
|
||||
rb.isKinematic = !rbData.Simulated;
|
||||
SceneManager.MoveGameObjectToScene(rb.transform.root.gameObject, rbData.SimulatedScene);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
//Parent went null, then graphicalObject needs to be destroyed.
|
||||
if (_graphicalParent == null && _graphicalObject != null)
|
||||
MonoBehaviour.Destroy(_graphicalObject.gameObject);
|
||||
else
|
||||
_graphicalObject?.SetParent(_graphicalParent);
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pauses rigidbodies preventing them from interacting.
|
||||
/// </summary>
|
||||
public void Pause()
|
||||
{
|
||||
if (Paused)
|
||||
return;
|
||||
Paused = true;
|
||||
|
||||
_graphicalObject?.SetParent(null);
|
||||
Scene kinematicScene = _kinematicScene;
|
||||
|
||||
//3D.
|
||||
if (_rigidbodyType == RigidbodyType.Rigidbody)
|
||||
{
|
||||
for (int i = 0; i < _rigidbodyDatas.Count; i++)
|
||||
{
|
||||
if (!PauseRigidbody(i))
|
||||
{
|
||||
_rigidbodyDatas.RemoveAt(i);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
|
||||
//Sets isKinematic status and returns if successful.
|
||||
bool PauseRigidbody(int index)
|
||||
{
|
||||
RigidbodyData rbData = _rigidbodyDatas[index];
|
||||
Rigidbody rb = rbData.Rigidbody;
|
||||
if (rb == null)
|
||||
return false;
|
||||
|
||||
rbData.Update(rb);
|
||||
_rigidbodyDatas[index] = rbData;
|
||||
SceneManager.MoveGameObjectToScene(rb.transform.root.gameObject, kinematicScene);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
//2D.
|
||||
else
|
||||
{
|
||||
for (int i = 0; i < _rigidbody2dDatas.Count; i++)
|
||||
{
|
||||
if (!PauseRigidbody(i))
|
||||
{
|
||||
_rigidbody2dDatas.RemoveAt(i);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
|
||||
//Sets isKinematic status and returns if successful.
|
||||
bool PauseRigidbody(int index)
|
||||
{
|
||||
Rigidbody2DData rbData = _rigidbody2dDatas[index];
|
||||
Rigidbody2D rb = rbData.Rigidbody2d;
|
||||
if (rb == null)
|
||||
return false;
|
||||
|
||||
rbData.Update(rb);
|
||||
_rigidbody2dDatas[index] = rbData;
|
||||
SceneManager.MoveGameObjectToScene(rb.transform.root.gameObject, kinematicScene);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9536377524ca5db43aae431f983ab21f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -0,0 +1,140 @@
|
||||
using FishNet.Component.Prediction;
|
||||
using FishNet.Serializing;
|
||||
using UnityEngine;
|
||||
|
||||
namespace FishNet.Component.Prediction
|
||||
{
|
||||
public struct RigidbodyState
|
||||
{
|
||||
public uint LocalTick;
|
||||
public Vector3 Position;
|
||||
public Quaternion Rotation;
|
||||
public bool IsKinematic;
|
||||
public Vector3 Velocity;
|
||||
public Vector3 AngularVelocity;
|
||||
|
||||
public RigidbodyState(Rigidbody rb, bool isKinematic, uint tick) : this(rb, tick)
|
||||
{
|
||||
Position = rb.transform.position;
|
||||
Rotation = rb.transform.rotation;
|
||||
IsKinematic = isKinematic;
|
||||
Velocity = rb.velocity;
|
||||
AngularVelocity = rb.angularVelocity;
|
||||
LocalTick = tick;
|
||||
|
||||
}
|
||||
|
||||
public RigidbodyState(Rigidbody rb, uint tick)
|
||||
{
|
||||
Position = rb.transform.position;
|
||||
Rotation = rb.transform.rotation;
|
||||
IsKinematic = rb.isKinematic;
|
||||
Velocity = rb.velocity;
|
||||
AngularVelocity = rb.angularVelocity;
|
||||
LocalTick = tick;
|
||||
}
|
||||
}
|
||||
|
||||
public struct Rigidbody2DState
|
||||
{
|
||||
public uint LocalTick;
|
||||
public Vector3 Position;
|
||||
public Quaternion Rotation;
|
||||
public bool Simulated;
|
||||
public Vector2 Velocity;
|
||||
public float AngularVelocity;
|
||||
|
||||
public Rigidbody2DState(Rigidbody2D rb, bool simulated, uint tick)
|
||||
{
|
||||
Simulated = simulated;
|
||||
Position = rb.transform.position;
|
||||
Rotation = rb.transform.rotation;
|
||||
Velocity = rb.velocity;
|
||||
AngularVelocity = rb.angularVelocity;
|
||||
LocalTick = tick;
|
||||
}
|
||||
|
||||
public Rigidbody2DState(Rigidbody2D rb, uint tick)
|
||||
{
|
||||
Simulated = rb.simulated;
|
||||
Position = rb.transform.position;
|
||||
Rotation = rb.transform.rotation;
|
||||
Velocity = rb.velocity;
|
||||
AngularVelocity = rb.angularVelocity;
|
||||
LocalTick = tick;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class RigidbodyStateSerializers
|
||||
{
|
||||
|
||||
public static void WriteRigidbodyState(this Writer writer, RigidbodyState value)
|
||||
{
|
||||
writer.WriteUInt32(value.LocalTick, AutoPackType.Unpacked);
|
||||
writer.WriteVector3(value.Position);
|
||||
writer.WriteQuaternion(value.Rotation);
|
||||
writer.WriteBoolean(value.IsKinematic);
|
||||
if (!value.IsKinematic)
|
||||
{
|
||||
writer.WriteVector3(value.Velocity);
|
||||
writer.WriteVector3(value.AngularVelocity);
|
||||
}
|
||||
}
|
||||
|
||||
public static RigidbodyState ReadRigidbodyState(this Reader reader)
|
||||
{
|
||||
RigidbodyState state = new RigidbodyState()
|
||||
{
|
||||
LocalTick = reader.ReadUInt32(AutoPackType.Unpacked),
|
||||
Position = reader.ReadVector3(),
|
||||
Rotation = reader.ReadQuaternion(),
|
||||
IsKinematic = reader.ReadBoolean()
|
||||
};
|
||||
|
||||
if (!state.IsKinematic)
|
||||
{
|
||||
state.Velocity = reader.ReadVector3();
|
||||
state.AngularVelocity = reader.ReadVector3();
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
|
||||
public static void WriteRigidbody2DState(this Writer writer, Rigidbody2DState value)
|
||||
{
|
||||
writer.WriteUInt32(value.LocalTick, AutoPackType.Unpacked);
|
||||
writer.WriteVector3(value.Position);
|
||||
writer.WriteQuaternion(value.Rotation);
|
||||
writer.WriteBoolean(value.Simulated);
|
||||
|
||||
if (value.Simulated)
|
||||
{
|
||||
writer.WriteVector2(value.Velocity);
|
||||
writer.WriteSingle(value.AngularVelocity);
|
||||
}
|
||||
}
|
||||
|
||||
public static Rigidbody2DState ReadRigidbody2DState(this Reader reader)
|
||||
{
|
||||
Rigidbody2DState state = new Rigidbody2DState()
|
||||
{
|
||||
LocalTick = reader.ReadUInt32(AutoPackType.Unpacked),
|
||||
Position = reader.ReadVector3(),
|
||||
Rotation = reader.ReadQuaternion(),
|
||||
Simulated = reader.ReadBoolean(),
|
||||
};
|
||||
|
||||
if (state.Simulated)
|
||||
{
|
||||
state.Velocity = reader.ReadVector2();
|
||||
state.AngularVelocity = reader.ReadSingle();
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 05dbb585c2bc6bf4dbbc592bea73d2fe
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -0,0 +1,12 @@
|
||||
namespace FishNet.Component.Prediction
|
||||
{
|
||||
/// <summary>
|
||||
/// Type of prediction movement being used.
|
||||
/// </summary>
|
||||
public enum RigidbodyType : byte
|
||||
{
|
||||
Rigidbody = 0,
|
||||
Rigidbody2D = 1
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7cf2d3fc2ff7a9042b4b7618db15b482
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
Reference in New Issue
Block a user