fishnet installed

This commit is contained in:
2023-05-31 11:32:21 -04:00
parent 47b25269f1
commit a001fe1b04
1291 changed files with 126631 additions and 1 deletions

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 40310629b512873468cfaf757b6fd377
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: e5ab06c5b11d85d4688a573ad0fdefdd
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,187 @@
#if UNITY_EDITOR
using FishNet.Editing;
using System.Collections.Generic;
using UnityEditor;
using UnityEditor.Animations;
using UnityEngine;
namespace FishNet.Component.Animating.Editing
{
[CustomEditor(typeof(NetworkAnimator), true)]
[CanEditMultipleObjects]
public class NetworkAnimatorEditor : Editor
{
private SerializedProperty _animator;
private SerializedProperty _interpolation;
//private SerializedProperty _synchronizeInterval;
private SerializedProperty _smoothFloats;
private SerializedProperty _clientAuthoritative;
private SerializedProperty _sendToOwner;
private RuntimeAnimatorController _lastRuntimeAnimatorController;
private AnimatorController _lastAnimatorController;
protected virtual void OnEnable()
{
_animator = serializedObject.FindProperty("_animator");
_interpolation = serializedObject.FindProperty("_interpolation");
//_synchronizeInterval = serializedObject.FindProperty("_synchronizeInterval");
_smoothFloats = serializedObject.FindProperty("_smoothFloats");
_clientAuthoritative = serializedObject.FindProperty("_clientAuthoritative");
_sendToOwner = serializedObject.FindProperty("_sendToOwner");
}
public override void OnInspectorGUI()
{
serializedObject.Update();
NetworkAnimator na = (NetworkAnimator)target;
GUI.enabled = false;
EditorGUILayout.ObjectField("Script:", MonoScript.FromMonoBehaviour(na), typeof(NetworkAnimator), false);
GUI.enabled = true;
#pragma warning disable CS0162 // Unreachable code detected
EditorGUILayout.HelpBox(EditingConstants.PRO_ASSETS_LOCKED_TEXT, MessageType.Warning);
#pragma warning restore CS0162 // Unreachable code detected
//Animator
EditorGUILayout.LabelField("Animator", EditorStyles.boldLabel);
EditorGUI.indentLevel++;
EditorGUILayout.PropertyField(_animator, new GUIContent("Animator", "The animator component to synchronize."));
EditorGUI.indentLevel--;
EditorGUILayout.Space();
//Synchronization Processing.
EditorGUILayout.LabelField("Synchronization Processing", EditorStyles.boldLabel);
EditorGUI.indentLevel++;
EditorGUILayout.PropertyField(_interpolation);
//EditorGUILayout.PropertyField(_synchronizeInterval, new GUIContent("Synchronize Interval", "How often to synchronize this animator."));
EditorGUILayout.PropertyField(_smoothFloats, new GUIContent("Smooth Floats", "True to smooth floats on spectators rather than snap to their values immediately. Commonly set to true for smooth blend tree animations."));
EditorGUI.indentLevel--;
EditorGUILayout.Space();
//Authority.
EditorGUILayout.LabelField("Authority", EditorStyles.boldLabel);
EditorGUI.indentLevel++;
EditorGUILayout.PropertyField(_clientAuthoritative, new GUIContent("Client Authoritative", "True if using client authoritative movement."));
if (_clientAuthoritative.boolValue == false)
{
EditorGUI.indentLevel++;
EditorGUILayout.PropertyField(_sendToOwner, new GUIContent("Synchronize To Owner", "True to synchronize server results back to owner. Typically used when you are sending inputs to the server and are relying on the server response to move the transform."));
EditorGUI.indentLevel--;
}
EditorGUI.indentLevel--;
EditorGUILayout.Space();
DrawParameters(na);
serializedObject.ApplyModifiedProperties();
}
private void DrawParameters(NetworkAnimator na)
{
EditorGUILayout.LabelField("* Synchronized Parameters", EditorStyles.boldLabel);
EditorGUILayout.HelpBox("This setting allows you to optionally completely prevent the synchronization of certain parameters. Both Fish-Networking free and Pro will only synchronize changes as they occur.", MessageType.Info);
if (Application.isPlaying)
{
EditorGUILayout.HelpBox("This feature can only be configured while out of play mode.", MessageType.Info);
return;
}
if (na == null)
return;
Animator animator = na.Animator;
if (animator == null)
return;
RuntimeAnimatorController runtimeController = (animator.runtimeAnimatorController is AnimatorOverrideController aoc) ? aoc.runtimeAnimatorController : animator.runtimeAnimatorController;
if (runtimeController == null)
{
na.IgnoredParameters.Clear();
return;
}
/* If runtime controller changed
* or editor controller is null
* then get new editor controller. */
if (runtimeController != _lastRuntimeAnimatorController || _lastAnimatorController == null)
_lastAnimatorController = (AnimatorController)AssetDatabase.LoadAssetAtPath(AssetDatabase.GetAssetPath(runtimeController), typeof(AnimatorController));
_lastRuntimeAnimatorController = runtimeController;
Color defaultColor = GUI.backgroundColor;
float width = Screen.width;
float spacePerEntry = 125f;
//Buttons seem to be longer than spacePerEntry. Why, because who knows...
float extraSpaceJustBecause = 60;
float spacer = 20f;
width -= spacer;
int entriesPerWidth = Mathf.Max(1, Mathf.FloorToInt(width / (spacePerEntry + extraSpaceJustBecause)));
List<AnimatorControllerParameter> aps = new List<AnimatorControllerParameter>();
//Create a parameter detail for each parameter that can be synchronized.
int count = 0;
foreach (AnimatorControllerParameter item in _lastAnimatorController.parameters)
{
count++;
//Over 240 parameters; who would do this!?
if (count >= 240)
continue;
aps.Add(item);
}
int apsCount = aps.Count;
for (int i = 0; i < apsCount; i++)
{
using (GUILayout.HorizontalScope hs = new GUILayout.HorizontalScope())
{
GUILayout.Space(spacer);
int z = 0;
while (z < entriesPerWidth && (z + i < apsCount))
{
//If this z+i would exceed entries then break.
if (z + i >= apsCount)
break;
AnimatorControllerParameter item = aps[i + z];
string parameterName = item.name;
bool ignored = na.IgnoredParameters.Contains(parameterName);
Color c = (ignored) ? Color.gray : Color.green;
GUI.backgroundColor = c;
if (GUILayout.Button(item.name, GUILayout.Width(spacePerEntry)))
{
if (Application.isPlaying)
{
Debug.Log("Synchronized parameters may not be changed while playing.");
}
else
{
if (ignored)
na.IgnoredParameters.Remove(parameterName);
else
na.IgnoredParameters.Add(parameterName);
}
}
z++;
}
i += (z - 1);
}
GUI.backgroundColor = defaultColor;
}
}
}
}
#endif

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 65609e99a0823a347a2f615b0e6f736e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e8cac635f24954048aad3a6ff9110beb
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: bf9191e2e07d29749bca3a1ae44e4bc8, type: 3}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 63eb855bc9013d54d9ea73088d204790
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 6c1769e2b4cabd744a4b875f6849ef76
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,134 @@
#if UNITY_EDITOR
using FishNet.Editing;
using UnityEditor;
using UnityEngine;
namespace FishNet.Component.Transforming.Editing
{
[CustomEditor(typeof(NetworkTransform), true)]
[CanEditMultipleObjects]
public class NetworkTransformEditor : Editor
{
private SerializedProperty _componentConfiguration;
private SerializedProperty _synchronizeParent;
private SerializedProperty _packing;
private SerializedProperty _interpolation;
private SerializedProperty _extrapolation;
private SerializedProperty _enableTeleport;
private SerializedProperty _teleportThreshold;
private SerializedProperty _clientAuthoritative;
private SerializedProperty _sendToOwner;
private SerializedProperty _synchronizePosition;
private SerializedProperty _positionSnapping;
private SerializedProperty _synchronizeRotation;
private SerializedProperty _rotationSnapping;
private SerializedProperty _synchronizeScale;
private SerializedProperty _scaleSnapping;
protected virtual void OnEnable()
{
_componentConfiguration = serializedObject.FindProperty(nameof(_componentConfiguration));
_synchronizeParent = serializedObject.FindProperty("_synchronizeParent");
_packing = serializedObject.FindProperty("_packing");
_interpolation = serializedObject.FindProperty("_interpolation");
_extrapolation = serializedObject.FindProperty("_extrapolation");
_enableTeleport = serializedObject.FindProperty("_enableTeleport");
_teleportThreshold = serializedObject.FindProperty("_teleportThreshold");
_clientAuthoritative = serializedObject.FindProperty("_clientAuthoritative");
_sendToOwner = serializedObject.FindProperty("_sendToOwner");
_synchronizePosition = serializedObject.FindProperty("_synchronizePosition");
_positionSnapping = serializedObject.FindProperty("_positionSnapping");
_synchronizeRotation = serializedObject.FindProperty("_synchronizeRotation");
_rotationSnapping = serializedObject.FindProperty("_rotationSnapping");
_synchronizeScale = serializedObject.FindProperty("_synchronizeScale");
_scaleSnapping = serializedObject.FindProperty("_scaleSnapping");
}
public override void OnInspectorGUI()
{
serializedObject.Update();
GUI.enabled = false;
EditorGUILayout.ObjectField("Script:", MonoScript.FromMonoBehaviour((NetworkTransform)target), typeof(NetworkTransform), false);
GUI.enabled = true;
#pragma warning disable CS0162 // Unreachable code detected
EditorGUILayout.HelpBox(EditingConstants.PRO_ASSETS_LOCKED_TEXT, MessageType.Warning);
#pragma warning restore CS0162 // Unreachable code detected
//Misc.
EditorGUILayout.LabelField("Misc", EditorStyles.boldLabel);
EditorGUI.indentLevel++;
EditorGUILayout.PropertyField(_componentConfiguration);
EditorGUILayout.PropertyField(_synchronizeParent, new GUIContent("* Synchronize Parent"));
EditorGUILayout.PropertyField(_packing);
EditorGUI.indentLevel--;
EditorGUILayout.Space();
//Smoothing.
EditorGUILayout.LabelField("Smoothing", EditorStyles.boldLabel);
EditorGUI.indentLevel++;
EditorGUILayout.PropertyField(_interpolation);
EditorGUILayout.PropertyField(_extrapolation, new GUIContent("* Extrapolation"));
EditorGUILayout.PropertyField(_enableTeleport);
if (_enableTeleport.boolValue)
{
EditorGUI.indentLevel++;
EditorGUILayout.PropertyField(_teleportThreshold);
EditorGUI.indentLevel--;
}
EditorGUI.indentLevel--;
EditorGUILayout.Space();
//Authority.
EditorGUILayout.LabelField("Authority", EditorStyles.boldLabel);
EditorGUI.indentLevel++;
EditorGUILayout.PropertyField(_clientAuthoritative);
if (!_clientAuthoritative.boolValue)
{
EditorGUI.indentLevel++;
EditorGUILayout.PropertyField(_sendToOwner);
EditorGUI.indentLevel--;
}
EditorGUI.indentLevel--;
EditorGUILayout.Space();
//Synchronizing.
EditorGUILayout.LabelField("Synchronizing.", EditorStyles.boldLabel);
EditorGUI.indentLevel++;
//Position.
EditorGUILayout.PropertyField(_synchronizePosition);
if (_synchronizePosition.boolValue)
{
EditorGUI.indentLevel += 2;
EditorGUILayout.PropertyField(_positionSnapping);
EditorGUI.indentLevel -= 2;
}
//Rotation.
EditorGUILayout.PropertyField(_synchronizeRotation);
if (_synchronizeRotation.boolValue)
{
EditorGUI.indentLevel += 2;
EditorGUILayout.PropertyField(_rotationSnapping);
EditorGUI.indentLevel -= 2;
}
//Scale.
EditorGUILayout.PropertyField(_synchronizeScale);
if (_synchronizeScale.boolValue)
{
EditorGUI.indentLevel += 2;
EditorGUILayout.PropertyField(_scaleSnapping);
EditorGUI.indentLevel -= 2;
}
EditorGUI.indentLevel--;
serializedObject.ApplyModifiedProperties();
}
}
}
#endif

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3ed56c899b8ecf241a2bc3e40a2dabc1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a2836e36774ca1c4bbbee976e17b649c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: bf9191e2e07d29749bca3a1ae44e4bc8, type: 3}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,13 @@
namespace FishNet.Component.Transforming
{
public enum SynchronizedProperty : byte
{
None = 0,
Parent = 1,
Position = 2,
Rotation = 4,
Scale = 8
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3e6005ee9abfdd542ad27023114bbe04
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: e4f8fbf54adbf5d4781d2c88c0aaebd8
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: a6b05a47941365c4097d74bca5e47017
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e0b8595657415764e9d83b6d974043af
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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();
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b749f90d4c9961c4991179db1130fa4d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6bd787cd0da2e9e4ab4bd30794ff0082
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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
}
}

View File

@ -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:

View File

@ -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();
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8b3bc1d8919cdf749971d3d4d44b198f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6bb311b5dc9602a4f8e4a8c6e64b21cb
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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;
}
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9536377524ca5db43aae431f983ab21f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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;
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 05dbb585c2bc6bf4dbbc592bea73d2fe
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,12 @@
namespace FishNet.Component.Prediction
{
/// <summary>
/// Type of prediction movement being used.
/// </summary>
public enum RigidbodyType : byte
{
Rigidbody = 0,
Rigidbody2D = 1
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7cf2d3fc2ff7a9042b4b7618db15b482
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: c4ff15ab60e92f14499393c8b415ea2f
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,159 @@
using FishNet.Connection;
using FishNet.Managing;
using FishNet.Object;
using System;
using UnityEngine;
using UnityEngine.Serialization;
namespace FishNet.Component.Spawning
{
/// <summary>
/// Spawns a player object for clients when they connect.
/// Must be placed on or beneath the NetworkManager object.
/// </summary>
[AddComponentMenu("FishNet/Component/PlayerSpawner")]
public class PlayerSpawner : MonoBehaviour
{
#region Public.
/// <summary>
/// Called on the server when a player is spawned.
/// </summary>
public event Action<NetworkObject> OnSpawned;
#endregion
#region Serialized.
/// <summary>
/// Prefab to spawn for the player.
/// </summary>
[Tooltip("Prefab to spawn for the player.")]
[SerializeField]
private NetworkObject _playerPrefab;
/// <summary>
/// True to add player to the active scene when no global scenes are specified through the SceneManager.
/// </summary>
[Tooltip("True to add player to the active scene when no global scenes are specified through the SceneManager.")]
[SerializeField]
private bool _addToDefaultScene = true;
/// <summary>
/// Areas in which players may spawn.
/// </summary>
[Tooltip("Areas in which players may spawn.")]
[FormerlySerializedAs("_spawns")]
public Transform[] Spawns = new Transform[0];
#endregion
#region Private.
/// <summary>
/// NetworkManager on this object or within this objects parents.
/// </summary>
private NetworkManager _networkManager;
/// <summary>
/// Next spawns to use.
/// </summary>
private int _nextSpawn;
#endregion
private void Start()
{
InitializeOnce();
}
private void OnDestroy()
{
if (_networkManager != null)
_networkManager.SceneManager.OnClientLoadedStartScenes -= SceneManager_OnClientLoadedStartScenes;
}
/// <summary>
/// Initializes this script for use.
/// </summary>
private void InitializeOnce()
{
_networkManager = InstanceFinder.NetworkManager;
if (_networkManager == null)
{
Debug.LogWarning($"PlayerSpawner on {gameObject.name} cannot work as NetworkManager wasn't found on this object or within parent objects.");
return;
}
_networkManager.SceneManager.OnClientLoadedStartScenes += SceneManager_OnClientLoadedStartScenes;
}
/// <summary>
/// Called when a client loads initial scenes after connecting.
/// </summary>
private void SceneManager_OnClientLoadedStartScenes(NetworkConnection conn, bool asServer)
{
if (!asServer)
return;
if (_playerPrefab == null)
{
Debug.LogWarning($"Player prefab is empty and cannot be spawned for connection {conn.ClientId}.");
return;
}
Vector3 position;
Quaternion rotation;
SetSpawn(_playerPrefab.transform, out position, out rotation);
NetworkObject nob = _networkManager.GetPooledInstantiated(_playerPrefab, true);
nob.transform.SetPositionAndRotation(position, rotation);
_networkManager.ServerManager.Spawn(nob, conn);
//If there are no global scenes
if (_addToDefaultScene)
_networkManager.SceneManager.AddOwnerToDefaultScene(nob);
OnSpawned?.Invoke(nob);
}
/// <summary>
/// Sets a spawn position and rotation.
/// </summary>
/// <param name="pos"></param>
/// <param name="rot"></param>
private void SetSpawn(Transform prefab, out Vector3 pos, out Quaternion rot)
{
//No spawns specified.
if (Spawns.Length == 0)
{
SetSpawnUsingPrefab(prefab, out pos, out rot);
return;
}
Transform result = Spawns[_nextSpawn];
if (result == null)
{
SetSpawnUsingPrefab(prefab, out pos, out rot);
}
else
{
pos = result.position;
rot = result.rotation;
}
//Increase next spawn and reset if needed.
_nextSpawn++;
if (_nextSpawn >= Spawns.Length)
_nextSpawn = 0;
}
/// <summary>
/// Sets spawn using values from prefab.
/// </summary>
/// <param name="prefab"></param>
/// <param name="pos"></param>
/// <param name="rot"></param>
private void SetSpawnUsingPrefab(Transform prefab, out Vector3 pos, out Quaternion rot)
{
pos = prefab.position;
rot = prefab.rotation;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 211a9f6ec51ddc14f908f5acc0cd0423
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: bf9191e2e07d29749bca3a1ae44e4bc8, type: 3}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: f4c64a37139b4b946b6f5daf3a7ae3da
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,121 @@
using FishNet.Connection;
using FishNet.Managing;
using FishNet.Managing.Logging;
using FishNet.Managing.Server;
using FishNet.Object;
using FishNet.Object.Synchronizing;
using UnityEngine;
namespace FishNet.Component.Ownership
{
/// <summary>
/// Adding this component allows any client to take ownership of the object and begin modifying it immediately.
/// </summary>
public class PredictedOwner : NetworkBehaviour
{
#region Public.
/// <summary>
/// True if the local client used TakeOwnership and is awaiting an ownership change.
/// </summary>
public bool TakingOwnership { get; private set; }
/// <summary>
/// Owner on client prior to taking ownership. This can be used to reverse a failed ownership attempt.
/// </summary>
public NetworkConnection PreviousOwner { get; private set; } = NetworkManager.EmptyConnection;
#endregion
#region Serialized.
/// <summary>
/// True if to enable this component.
/// </summary>
[Tooltip("True if to enable this component.")]
[SyncVar(SendRate = 0f)]
[SerializeField]
private bool _allowTakeOwnership = true;
/// <summary>
/// Sets the next value for AllowTakeOwnership and synchronizes it.
/// Only the server may use this method.
/// </summary>
/// <param name="value">Next value to use.</param>
[Server]
public void SetAllowTakeOwnership(bool value) => _allowTakeOwnership = value;
#endregion
/// <summary>
/// Called on the client after gaining or losing ownership.
/// </summary>
/// <param name="prevOwner">Previous owner of this object.</param>
public override void OnOwnershipClient(NetworkConnection prevOwner)
{
base.OnOwnershipClient(prevOwner);
/* Unset taken ownership either way.
* If the new owner it won't be used,
* if no longer owner then another client
* took it. */
TakingOwnership = false;
PreviousOwner = base.Owner;
}
/// <summary>
/// Takes ownership of this object to the local client and allows immediate control.
/// </summary>
[Client]
public virtual void TakeOwnership()
{
if (!_allowTakeOwnership)
return;
//Already owner.
if (base.IsOwner)
return;
NetworkConnection c = base.ClientManager.Connection;
TakingOwnership = true;
//If not server go through the server.
if (!base.IsServer)
{
base.NetworkObject.SetLocalOwnership(c);
ServerTakeOwnership();
}
//Otherwise take directly without rpcs.
else
{
OnTakeOwnership(c);
}
}
/// <summary>
/// Takes ownership of this object.
/// </summary>
[ServerRpc(RequireOwnership = false)]
private void ServerTakeOwnership(NetworkConnection caller = null)
{
OnTakeOwnership(caller);
}
/// <summary>
/// Called on the server when a client tries to take ownership of this object.
/// </summary>
/// <param name="caller">Connection trying to take ownership.</param>
[Server]
protected virtual void OnTakeOwnership(NetworkConnection caller)
{
//Client somehow disconnected between here and there.
if (!caller.IsActive)
return;
//Feature is not enabled.
if (!_allowTakeOwnership)
return;
//Already owner.
if (caller == base.Owner)
return;
base.GiveOwnership(caller);
/* No need to send a response back because an ownershipchange will handle changes.
* Although if you were to override with this your own behavior
* you could send responses for approved/denied. */
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 03002f7d324007e41b10a9dc87ed3c38
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: bf9191e2e07d29749bca3a1ae44e4bc8, type: 3}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,96 @@
using FishNet.Connection;
using FishNet.Object;
using UnityEngine;
namespace FishNet.Component.Ownership
{
/// <summary>
/// Adding this component allows any client to use predictive spawning on this prefab.
/// </summary>
public class PredictedSpawn : NetworkBehaviour
{
#region Serialized.
/// <summary>
/// True to allow clients to predicted spawn this object.
/// </summary>
public bool GetAllowSpawning() => _allowSpawning;
/// <summary>
/// Sets to allow predicted spawning. This must be set on client and server.
/// </summary>
/// <param name="value">New value.</param>
public void SetAllowSpawning(bool value) => _allowSpawning = value;
[Tooltip("True to allow clients to predicted spawn this object.")]
[SerializeField]
private bool _allowSpawning = true;
/// <summary>
/// True to allow clients to predicted despawn this object.
/// </summary>
public bool GetAllowDespawning() => _allowDespawning;
/// <summary>
/// Sets to allow predicted despawning. This must be set on client and server.
/// </summary>
/// <param name="value">New value.</param>
public void SetAllowDespawning(bool value) => _allowDespawning = value;
[Tooltip("True to allow clients to predicted despawn this object.")]
[SerializeField]
private bool _allowDespawning = true;
/// <summary>
///
/// </summary>
[Tooltip("True to allow clients to predicted set syncTypes prior to spawning the item. Set values will be applied on the server and sent to other clients.")]
[SerializeField]
private bool _allowSyncTypes = true;
/// <summary>
/// True to allow clients to predicted set syncTypes prior to spawning the item. Set values will be applied on the server and sent to other clients.
/// </summary>
public bool GetAllowSyncTypes() => _allowSyncTypes;
/// <summary>
/// Sets to allow syncTypes. This must be set on client and server.
/// </summary>
/// <param name="value">New value.</param>
public void SetAllowSyncTypes(bool value) => _allowSyncTypes = value;
#endregion
/// <summary>
/// Called on the client when trying to predicted spawn this object.
/// </summary>
/// <param name="owner">Owner specified to spawn with.</param>
/// <returns>True if able to spawn.</returns>
public virtual bool OnTrySpawnClient(NetworkConnection owner = null)
{
return GetAllowSpawning();
}
/// <summary>
/// Called on the server when a client tries to predicted spawn this object.
/// </summary>
/// <param name="spawner">Connection trying to predicted spawn this object.</param>
/// <param name="owner">Owner specified to spawn with.</param>
/// <returns>True if able to spawn.</returns>
public virtual bool OnTrySpawnServer(NetworkConnection spawner, NetworkConnection owner = null)
{
return GetAllowSpawning();
}
/// <summary>
/// Called on the client when trying to predicted spawn this object.
/// </summary>
/// <returns>True if able to despawn.</returns>
public virtual bool OnTryDespawnClient()
{
return GetAllowDespawning();
}
/// <summary>
/// Called on the server when a client tries to predicted despawn this object.
/// </summary>
/// <param name="despawner">Connection trying to predicted despawn this object.</param>
/// <returns>True if able to despawn.</returns>
public virtual bool OnTryDepawnServer(NetworkConnection despawner)
{
return GetAllowDespawning();
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e2b597e1828355a4d994a69cbb11ef85
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: bf9191e2e07d29749bca3a1ae44e4bc8, type: 3}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 4fbf39de0f731b64e97742bcbfa213d3
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,192 @@
using FishNet.Managing.Statistic;
using UnityEngine;
namespace FishNet.Component.Utility
{
/// <summary>
/// Add to any object to display current ping(round trip time).
/// </summary>
[AddComponentMenu("FishNet/Component/BandwidthDisplay")]
public class BandwidthDisplay : MonoBehaviour
{
#region Types.
private enum Corner
{
TopLeft,
TopRight,
BottomLeft,
BottomRight
}
#endregion
#region Serialized.
/// <summary>
/// Color for text.
/// </summary>
[Tooltip("Color for text.")]
[SerializeField]
private Color _color = Color.white;
/// <summary>
/// Which corner to display network statistics in.
/// </summary>
[Tooltip("Which corner to display network statistics in.")]
[SerializeField]
private Corner _placement = Corner.TopRight;
/// <summary>
/// rue to show outgoing data bytes.
/// </summary>
[Tooltip("True to show outgoing data bytes.")]
[SerializeField]
private bool _showOutgoing = true;
/// <summary>
/// Sets ShowOutgoing value.
/// </summary>
/// <param name="value"></param>
public void SetShowOutgoing(bool value) => _showOutgoing = value;
/// <summary>
/// True to show incoming data bytes.
/// </summary>
[Tooltip("True to show incoming data bytes.")]
[SerializeField]
private bool _showIncoming = true;
/// <summary>
/// Sets ShowIncoming value.
/// </summary>
/// <param name="value"></param>
public void SetShowIncoming(bool value) => _showIncoming = value;
#endregion
#region Private.
/// <summary>
/// Style for drawn ping.
/// </summary>
private GUIStyle _style = new GUIStyle();
/// <summary>
/// Text to show for client in/out data.
/// </summary>
private string _clientText;
/// <summary>
/// Text to show for server in/out data.
/// </summary>
private string _serverText;
/// <summary>
/// First found NetworkTrafficStatistics.
/// </summary>
private NetworkTraficStatistics _networkTrafficStatistics;
#endregion
private void Start()
{
_networkTrafficStatistics = InstanceFinder.NetworkManager.StatisticsManager.NetworkTraffic;
//Subscribe to both traffic updates.
_networkTrafficStatistics.OnClientNetworkTraffic += NetworkTraffic_OnClientNetworkTraffic;
_networkTrafficStatistics.OnServerNetworkTraffic += NetworkTraffic_OnServerNetworkTraffic;
if (!_networkTrafficStatistics.UpdateClient && !_networkTrafficStatistics.UpdateServer)
Debug.LogWarning($"StatisticsManager.NetworkTraffic is not updating for client nor server. To see results ensure your NetworkManager has a StatisticsManager component added with the NetworkTraffic values configured.");
}
private void OnDestroy()
{
if (_networkTrafficStatistics != null)
{
_networkTrafficStatistics.OnClientNetworkTraffic -= NetworkTraffic_OnClientNetworkTraffic;
_networkTrafficStatistics.OnServerNetworkTraffic -= NetworkTraffic_OnClientNetworkTraffic;
}
}
/// <summary>
/// Called when client network traffic is updated.
/// </summary>
private void NetworkTraffic_OnClientNetworkTraffic(NetworkTrafficArgs obj)
{
string nl = System.Environment.NewLine;
string result = string.Empty;
if (_showIncoming)
result += $"Client In: {NetworkTraficStatistics.FormatBytesToLargest(obj.FromServerBytes)}/s{nl}";
if (_showOutgoing)
result += $"Client Out: {NetworkTraficStatistics.FormatBytesToLargest(obj.ToServerBytes)}/s{nl}";
_clientText = result;
}
/// <summary>
/// Called when client network traffic is updated.
/// </summary>
private void NetworkTraffic_OnServerNetworkTraffic(NetworkTrafficArgs obj)
{
string nl = System.Environment.NewLine;
string result = string.Empty;
if (_showIncoming)
result += $"Server In: {NetworkTraficStatistics.FormatBytesToLargest(obj.ToServerBytes)}/s{nl}";
if (_showOutgoing)
result += $"Server Out: {NetworkTraficStatistics.FormatBytesToLargest(obj.FromServerBytes)}/s{nl}";
_serverText = result;
}
private void OnGUI()
{
//No need to perform these actions on server.
#if !UNITY_EDITOR && UNITY_SERVER
return;
#endif
_style.normal.textColor = _color;
_style.fontSize = 15;
float width = 100f;
float height = 0f;
if (_showIncoming)
height += 15f;
if (_showOutgoing)
height += 15f;
bool isClient = InstanceFinder.IsClient;
bool isServer = InstanceFinder.IsServer;
if (!isClient)
_clientText = string.Empty;
if (!isServer)
_serverText = string.Empty;
if (isServer && isClient)
height *= 2f;
float edge = 10f;
float horizontal;
float vertical;
if (_placement == Corner.TopLeft)
{
horizontal = 10f;
vertical = 10f;
_style.alignment = TextAnchor.UpperLeft;
}
else if (_placement == Corner.TopRight)
{
horizontal = Screen.width - width - edge;
vertical = 10f;
_style.alignment = TextAnchor.UpperRight;
}
else if (_placement == Corner.BottomLeft)
{
horizontal = 10f;
vertical = Screen.height - height - edge;
_style.alignment = TextAnchor.LowerLeft;
}
else
{
horizontal = Screen.width - width - edge;
vertical = Screen.height - height - edge;
_style.alignment = TextAnchor.LowerRight;
}
GUI.Label(new Rect(horizontal, vertical, width, height), (_clientText + _serverText), _style);
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8bc8f0363ddc75946a958043c5e49a83
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: bf9191e2e07d29749bca3a1ae44e4bc8, type: 3}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,239 @@
using FishNet.Connection;
using FishNet.Managing;
using FishNet.Managing.Logging;
using FishNet.Managing.Scened;
using FishNet.Transporting;
using FishNet.Utility;
using System.IO;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnitySceneManager = UnityEngine.SceneManagement.SceneManager;
/// <summary>
/// Add to a NetworkManager object to change between Online and Offline scene based on connection states of the server or client.
/// </summary>
[AddComponentMenu("FishNet/Component/DefaultScene")]
public class DefaultScene : MonoBehaviour
{
#region Serialized.
[Tooltip("True to load the online scene as global, false to load it as connection.")]
[SerializeField]
private bool _useGlobalScenes = true;
/// <summary>
/// True to replace all scenes with the offline scene immediately.
/// </summary>
[Tooltip("True to replace all scenes with the offline scene immediately.")]
[SerializeField]
private bool _startInOffline;
/// <summary>
///
/// </summary>
[Tooltip("Scene to load when disconnected. Server and client will load this scene.")]
[SerializeField, Scene]
private string _offlineScene;
/// <summary>
/// Sets which offline scene to use.
/// </summary>
/// <param name="sceneName">Scene name to use as the offline scene.</param>
public void SetOfflineScene(string sceneName) => _offlineScene = sceneName;
/// <summary>
/// Scene to load when disconnected. Server and client will load this scene.
/// </summary>
/// <returns></returns>
public string GetOfflineScene() => _offlineScene;
/// <summary>
///
/// </summary>
[Tooltip("Scene to load when connected. Server and client will load this scene.")]
[SerializeField, Scene]
private string _onlineScene;
/// <summary>
/// Sets which online scene to use.
/// </summary>
/// <param name="sceneName">Scene name to use as the online scene.</param>
public void SetOnlineScene(string sceneName) => _onlineScene = sceneName;
/// <summary>
/// Scene to load when connected. Server and client will load this scene.
/// </summary>
/// <returns></returns>
public string GetOnlineScene() => _onlineScene;
/// <summary>
/// Which scenes to replace when loading into OnlineScene.
/// </summary>
[Tooltip("Which scenes to replace when loading into OnlineScene.")]
[SerializeField]
private ReplaceOption _replaceScenes = ReplaceOption.All;
#endregion
#region Private.
/// <summary>
/// NetworkManager for this component.
/// </summary>
private NetworkManager _networkManager;
#endregion
private void Awake()
{
InitializeOnce();
}
private void OnDestroy()
{
if (!ApplicationState.IsQuitting() && _networkManager != null && _networkManager.Initialized)
{
_networkManager.ClientManager.OnClientConnectionState -= ClientManager_OnClientConnectionState;
_networkManager.ServerManager.OnServerConnectionState -= ServerManager_OnServerConnectionState;
_networkManager.SceneManager.OnLoadEnd -= SceneManager_OnLoadEnd;
_networkManager.ServerManager.OnAuthenticationResult -= ServerManager_OnAuthenticationResult;
}
}
/// <summary>
/// Initializes this script for use.
/// </summary>
private void InitializeOnce()
{
_networkManager = GetComponentInParent<NetworkManager>();
if (_networkManager == null)
{
NetworkManager.StaticLogError($"NetworkManager not found on {gameObject.name} or any parent objects. DefaultScene will not work.");
return;
}
//A NetworkManager won't be initialized if it's being destroyed.
if (!_networkManager.Initialized)
return;
if (_onlineScene == string.Empty || _offlineScene == string.Empty)
{
NetworkManager.StaticLogWarning("Online or Offline scene is not specified. Default scenes will not load.");
return;
}
_networkManager.ClientManager.OnClientConnectionState += ClientManager_OnClientConnectionState;
_networkManager.ServerManager.OnServerConnectionState += ServerManager_OnServerConnectionState;
_networkManager.SceneManager.OnLoadEnd += SceneManager_OnLoadEnd;
_networkManager.ServerManager.OnAuthenticationResult += ServerManager_OnAuthenticationResult;
if (_startInOffline)
LoadOfflineScene();
}
/// <summary>
/// Called when a scene load ends.
/// </summary>
private void SceneManager_OnLoadEnd(SceneLoadEndEventArgs obj)
{
bool onlineLoaded = false;
foreach (Scene s in obj.LoadedScenes)
{
if (s.name == GetSceneName(_onlineScene))
{
onlineLoaded = true;
break;
}
}
//If online scene was loaded then unload offline.
if (onlineLoaded)
UnloadOfflineScene();
}
/// <summary>
/// Called after the local server connection state changes.
/// </summary>
private void ServerManager_OnServerConnectionState(ServerConnectionStateArgs obj)
{
/* When server starts load online scene as global.
* Since this is a global scene clients will automatically
* join it when connecting. */
if (obj.ConnectionState == LocalConnectionState.Started)
{
/* If not exactly one server is started then
* that means either none are started, which isnt true because
* we just got a started callback, or two+ are started.
* When a server has already started there's no reason to load
* scenes again. */
if (!_networkManager.ServerManager.OneServerStarted())
return;
//If here can load scene.
SceneLoadData sld = new SceneLoadData(GetSceneName(_onlineScene));
sld.ReplaceScenes = _replaceScenes;
if (_useGlobalScenes)
_networkManager.SceneManager.LoadGlobalScenes(sld);
else
_networkManager.SceneManager.LoadConnectionScenes(sld);
}
//When server stops load offline scene.
else if (obj.ConnectionState == LocalConnectionState.Stopped)
{
LoadOfflineScene();
}
}
/// <summary>
/// Called after the local client connection state changes.
/// </summary>
private void ClientManager_OnClientConnectionState(ClientConnectionStateArgs obj)
{
if (obj.ConnectionState == LocalConnectionState.Stopped)
{
//Only load offline scene if not also server.
if (!_networkManager.IsServer)
LoadOfflineScene();
}
}
/// <summary>
/// Called when a client completes authentication.
/// </summary>
private void ServerManager_OnAuthenticationResult(NetworkConnection arg1, bool authenticated)
{
/* This is only for loading connection scenes.
* If using global there is no need to continue. */
if (_useGlobalScenes)
return;
if (!authenticated)
return;
SceneLoadData sld = new SceneLoadData(GetSceneName(_onlineScene));
_networkManager.SceneManager.LoadConnectionScenes(arg1, sld);
}
/// <summary>
/// Loads offlineScene as single.
/// </summary>
private void LoadOfflineScene()
{
//Already in offline scene.
if (UnitySceneManager.GetActiveScene().name == GetSceneName(_offlineScene))
return;
//Only use scene manager if networking scenes. I may add something in later to do both local and networked.
UnitySceneManager.LoadScene(_offlineScene);
}
/// <summary>
/// Unloads the offline scene.
/// </summary>
private void UnloadOfflineScene()
{
Scene s = UnitySceneManager.GetSceneByName(GetSceneName(_offlineScene));
if (string.IsNullOrEmpty(s.name))
return;
UnitySceneManager.UnloadSceneAsync(s);
}
/// <summary>
/// Returns a scene name from fullPath.
/// </summary>
/// <param name="fullPath"></param>
/// <returns></returns>
private string GetSceneName(string fullPath)
{
return Path.GetFileNameWithoutExtension(fullPath);
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 57ce8bbb58966cb45a7140f32da5327a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: bf9191e2e07d29749bca3a1ae44e4bc8, type: 3}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,118 @@
using FishNet.Managing.Timing;
using UnityEngine;
namespace FishNet.Component.Utility
{
/// <summary>
/// Add to any object to display current ping(round trip time).
/// </summary>
[AddComponentMenu("FishNet/Component/PingDisplay")]
public class PingDisplay : MonoBehaviour
{
#region Types.
private enum Corner
{
TopLeft,
TopRight,
BottomLeft,
BottomRight
}
#endregion
#region Serialized.
/// <summary>
/// Color for text.
/// </summary>
[Tooltip("Color for text.")]
[SerializeField]
private Color _color = Color.white;
/// <summary>
/// Which corner to display ping in.
/// </summary>
[Tooltip("Which corner to display ping in.")]
[SerializeField]
private Corner _placement = Corner.TopRight;
/// <summary>
/// True to show the real ping. False to include tick rate latency within the ping.
/// </summary>
[Tooltip("True to show the real ping. False to include tick rate latency within the ping.")]
[SerializeField]
private bool _hideTickRate = true;
#endregion
#region Private.
/// <summary>
/// Style for drawn ping.
/// </summary>
private GUIStyle _style = new GUIStyle();
#endregion
private void OnGUI()
{
//No need to perform these actions on server.
#if !UNITY_EDITOR && UNITY_SERVER
return;
#endif
//Only clients can see pings.
if (!InstanceFinder.IsClient)
return;
_style.normal.textColor = _color;
_style.fontSize = 15;
float width = 85f;
float height = 15f;
float edge = 10f;
float horizontal;
float vertical;
if (_placement == Corner.TopLeft)
{
horizontal = 10f;
vertical = 10f;
}
else if (_placement == Corner.TopRight)
{
horizontal = Screen.width - width - edge;
vertical = 10f;
}
else if (_placement == Corner.BottomLeft)
{
horizontal = 10f;
vertical = Screen.height - height - edge;
}
else
{
horizontal = Screen.width - width - edge;
vertical = Screen.height - height - edge;
}
long ping;
TimeManager tm = InstanceFinder.TimeManager;
if (tm == null)
{
ping = 0;
}
else
{
ping = tm.RoundTripTime;
long deduction = 0;
if (_hideTickRate)
{
deduction = (long)(tm.TickDelta * 1000d);
/* If host subtract two ticks, if client only subtract one tick.
* This will reflect the users real ping without the tick rate latency. */
if (InstanceFinder.IsHost)
deduction *= 2;
}
ping = (long)Mathf.Max(0, ping - deduction);
}
GUI.Label(new Rect(horizontal, vertical, width, height), $"Ping: {ping}ms", _style);
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f9b6b565cd9533c4ebc18003f0fc18a2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: bf9191e2e07d29749bca3a1ae44e4bc8, type: 3}
userData:
assetBundleName:
assetBundleVariant: