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