mirror of
https://github.com/MirrorNetworking/Mirror.git
synced 2024-11-18 02:50:32 +00:00
Merged master
This commit is contained in:
commit
f34e14acb0
1
.gitignore
vendored
1
.gitignore
vendored
@ -20,6 +20,7 @@ UserSettings/Search.settings
|
||||
# ===================================== #
|
||||
Database.sqlite
|
||||
Database/
|
||||
Builds/
|
||||
|
||||
# ===================================== #
|
||||
# Visual Studio / MonoDevelop / Rider #
|
||||
|
@ -11,22 +11,25 @@ static class PreprocessorDefine
|
||||
[InitializeOnLoadMethod]
|
||||
public static void AddDefineSymbols()
|
||||
{
|
||||
#if UNITY_2021_2_OR_NEWER
|
||||
string currentDefines = PlayerSettings.GetScriptingDefineSymbols(UnityEditor.Build.NamedBuildTarget.FromBuildTargetGroup(EditorUserBuildSettings.selectedBuildTargetGroup));
|
||||
#else
|
||||
// Deprecated in Unity 2023.1
|
||||
string currentDefines = PlayerSettings.GetScriptingDefineSymbolsForGroup(EditorUserBuildSettings.selectedBuildTargetGroup);
|
||||
#endif
|
||||
// Remove oldest when adding next month's symbol.
|
||||
// Keep a rolling 12 months of symbols.
|
||||
HashSet<string> defines = new HashSet<string>(currentDefines.Split(';'))
|
||||
{
|
||||
"MIRROR",
|
||||
"MIRROR_57_0_OR_NEWER",
|
||||
"MIRROR_58_0_OR_NEWER",
|
||||
"MIRROR_65_0_OR_NEWER",
|
||||
"MIRROR_66_0_OR_NEWER",
|
||||
"MIRROR_2022_9_OR_NEWER",
|
||||
"MIRROR_2022_10_OR_NEWER",
|
||||
"MIRROR_70_0_OR_NEWER",
|
||||
"MIRROR_71_0_OR_NEWER",
|
||||
"MIRROR_73_OR_NEWER",
|
||||
"MIRROR_78_OR_NEWER"
|
||||
// Remove oldest when adding next month's symbol.
|
||||
// Keep a rolling 12 months of symbols.
|
||||
"MIRROR_79_OR_NEWER",
|
||||
"MIRROR_81_OR_NEWER",
|
||||
"MIRROR_82_OR_NEWER",
|
||||
"MIRROR_83_OR_NEWER",
|
||||
"MIRROR_84_OR_NEWER",
|
||||
"MIRROR_85_OR_NEWER",
|
||||
"MIRROR_86_OR_NEWER",
|
||||
"MIRROR_89_OR_NEWER"
|
||||
};
|
||||
|
||||
// only touch PlayerSettings if we actually modified it,
|
||||
@ -34,7 +37,12 @@ public static void AddDefineSymbols()
|
||||
string newDefines = string.Join(";", defines);
|
||||
if (newDefines != currentDefines)
|
||||
{
|
||||
#if UNITY_2021_2_OR_NEWER
|
||||
PlayerSettings.SetScriptingDefineSymbols(UnityEditor.Build.NamedBuildTarget.FromBuildTargetGroup(EditorUserBuildSettings.selectedBuildTargetGroup), newDefines);
|
||||
#else
|
||||
// Deprecated in Unity 2023.1
|
||||
PlayerSettings.SetScriptingDefineSymbolsForGroup(EditorUserBuildSettings.selectedBuildTargetGroup, newDefines);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,7 @@
|
||||
namespace Mirror.Discovery
|
||||
{
|
||||
[Serializable]
|
||||
public class ServerFoundUnityEvent : UnityEvent<ServerResponse> {};
|
||||
public class ServerFoundUnityEvent<TResponseType> : UnityEvent<TResponseType> {};
|
||||
|
||||
[DisallowMultipleComponent]
|
||||
[AddComponentMenu("Network/Network Discovery")]
|
||||
|
@ -45,7 +45,7 @@ public abstract class NetworkDiscoveryBase<Request, Response> : MonoBehaviour
|
||||
public Transport transport;
|
||||
|
||||
[Tooltip("Invoked when a server is found")]
|
||||
public ServerFoundUnityEvent OnServerFound;
|
||||
public ServerFoundUnityEvent<Response> OnServerFound;
|
||||
|
||||
// Each game should have a random unique handshake,
|
||||
// this way you can tell if this is the same game or not
|
||||
@ -85,9 +85,10 @@ public virtual void Start()
|
||||
transport = Transport.active;
|
||||
|
||||
// Server mode? then start advertising
|
||||
#if UNITY_SERVER
|
||||
if (Utils.IsHeadless())
|
||||
{
|
||||
AdvertiseServer();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
public static long RandomLong()
|
||||
|
@ -1,9 +1,11 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror.Experimental
|
||||
{
|
||||
[AddComponentMenu("Network/ Experimental/Network Lerp Rigidbody")]
|
||||
[AddComponentMenu("")]
|
||||
[HelpURL("https://mirror-networking.gitbook.io/docs/components/network-lerp-rigidbody")]
|
||||
[Obsolete("Use the new NetworkRigidbodyReliable/Unreliable component with Snapshot Interpolation instead.")]
|
||||
public class NetworkLerpRigidbody : NetworkBehaviour
|
||||
{
|
||||
[Header("Settings")]
|
||||
@ -33,10 +35,18 @@ public class NetworkLerpRigidbody : NetworkBehaviour
|
||||
|
||||
bool ClientWithAuthority => clientAuthority && isOwned;
|
||||
|
||||
void OnValidate()
|
||||
protected override void OnValidate()
|
||||
{
|
||||
base.OnValidate();
|
||||
Reset();
|
||||
}
|
||||
|
||||
public virtual void Reset()
|
||||
{
|
||||
if (target == null)
|
||||
target = GetComponent<Rigidbody>();
|
||||
|
||||
syncDirection = SyncDirection.ClientToServer;
|
||||
}
|
||||
|
||||
void Update()
|
||||
|
@ -1,9 +1,11 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror.Experimental
|
||||
{
|
||||
[AddComponentMenu("Network/ Experimental/Network Rigidbody")]
|
||||
[AddComponentMenu("")]
|
||||
[HelpURL("https://mirror-networking.gitbook.io/docs/components/network-rigidbody")]
|
||||
[Obsolete("Use the new NetworkRigidbodyReliable/Unreliable component with Snapshot Interpolation instead.")]
|
||||
public class NetworkRigidbody : NetworkBehaviour
|
||||
{
|
||||
[Header("Settings")]
|
||||
@ -37,10 +39,18 @@ public class NetworkRigidbody : NetworkBehaviour
|
||||
/// </summary>
|
||||
readonly ClientSyncState previousValue = new ClientSyncState();
|
||||
|
||||
void OnValidate()
|
||||
protected override void OnValidate()
|
||||
{
|
||||
base.OnValidate();
|
||||
Reset();
|
||||
}
|
||||
|
||||
public virtual void Reset()
|
||||
{
|
||||
if (target == null)
|
||||
target = GetComponent<Rigidbody>();
|
||||
|
||||
syncDirection = SyncDirection.ClientToServer;
|
||||
}
|
||||
|
||||
#region Sync vars
|
||||
|
@ -1,9 +1,11 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror.Experimental
|
||||
{
|
||||
[AddComponentMenu("Network/ Experimental/Network Rigidbody 2D")]
|
||||
[AddComponentMenu("")]
|
||||
[HelpURL("https://mirror-networking.gitbook.io/docs/components/network-rigidbody")]
|
||||
[Obsolete("Use the new NetworkRigidbodyReliable/Unreliable 2D component with Snapshot Interpolation instead.")]
|
||||
public class NetworkRigidbody2D : NetworkBehaviour
|
||||
{
|
||||
[Header("Settings")]
|
||||
@ -37,10 +39,18 @@ public class NetworkRigidbody2D : NetworkBehaviour
|
||||
/// </summary>
|
||||
readonly ClientSyncState previousValue = new ClientSyncState();
|
||||
|
||||
void OnValidate()
|
||||
protected override void OnValidate()
|
||||
{
|
||||
base.OnValidate();
|
||||
Reset();
|
||||
}
|
||||
|
||||
public virtual void Reset()
|
||||
{
|
||||
if (target == null)
|
||||
target = GetComponent<Rigidbody2D>();
|
||||
|
||||
syncDirection = SyncDirection.ClientToServer;
|
||||
}
|
||||
|
||||
#region Sync vars
|
||||
|
@ -31,26 +31,37 @@ public LogEntry(string message, LogType type)
|
||||
|
||||
public class GUIConsole : MonoBehaviour
|
||||
{
|
||||
public int height = 150;
|
||||
public int height = 80;
|
||||
public int offsetY = 40;
|
||||
|
||||
// only keep the recent 'n' entries. otherwise memory would grow forever
|
||||
// and drawing would get slower and slower.
|
||||
public int maxLogCount = 50;
|
||||
|
||||
// Unity Editor has the Console window, we don't need to show it there.
|
||||
// unless for testing, so keep it as option.
|
||||
public bool showInEditor = false;
|
||||
|
||||
// log as queue so we can remove the first entry easily
|
||||
Queue<LogEntry> log = new Queue<LogEntry>();
|
||||
readonly Queue<LogEntry> log = new Queue<LogEntry>();
|
||||
|
||||
// hotkey to show/hide at runtime for easier debugging
|
||||
// (sometimes we need to temporarily hide/show it)
|
||||
// => F12 makes sense. nobody can find ^ in other games.
|
||||
public KeyCode hotKey = KeyCode.F12;
|
||||
// Default is BackQuote, because F keys are already assigned in browsers
|
||||
[Tooltip("Hotkey to show/hide the console at runtime\nBack Quote is usually on the left above Tab\nChange with caution - F keys are generally already taken in Browsers")]
|
||||
public KeyCode hotKey = KeyCode.BackQuote;
|
||||
|
||||
// GUI
|
||||
bool visible;
|
||||
Vector2 scroll = Vector2.zero;
|
||||
|
||||
// only show at runtime, or if showInEditor is enabled
|
||||
bool show => !Application.isEditor || showInEditor;
|
||||
|
||||
void Awake()
|
||||
{
|
||||
// only show at runtime, or if showInEditor is enabled
|
||||
if (show)
|
||||
Application.logMessageReceived += OnLog;
|
||||
}
|
||||
|
||||
@ -90,7 +101,7 @@ void OnLog(string message, string stackTrace, LogType type)
|
||||
|
||||
void Update()
|
||||
{
|
||||
if (Input.GetKeyDown(hotKey))
|
||||
if (show && Input.GetKeyDown(hotKey))
|
||||
visible = !visible;
|
||||
}
|
||||
|
||||
@ -98,7 +109,12 @@ void OnGUI()
|
||||
{
|
||||
if (!visible) return;
|
||||
|
||||
scroll = GUILayout.BeginScrollView(scroll, "Box", GUILayout.Width(Screen.width), GUILayout.Height(height));
|
||||
// If this offset is changed, also change width in NetworkManagerHUD::OnGUI
|
||||
int offsetX = 300 + 20;
|
||||
|
||||
GUILayout.BeginArea(new Rect(offsetX, offsetY, Screen.width - offsetX - 10, height));
|
||||
|
||||
scroll = GUILayout.BeginScrollView(scroll, "Box", GUILayout.Width(Screen.width - offsetX - 10), GUILayout.Height(height));
|
||||
foreach (LogEntry entry in log)
|
||||
{
|
||||
if (entry.type == LogType.Error || entry.type == LogType.Exception)
|
||||
@ -110,6 +126,8 @@ void OnGUI()
|
||||
GUI.color = Color.white;
|
||||
}
|
||||
GUILayout.EndScrollView();
|
||||
|
||||
GUILayout.EndArea();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -25,7 +25,7 @@ int GetVisRange(NetworkIdentity identity)
|
||||
}
|
||||
|
||||
[ServerCallback]
|
||||
public override void Reset()
|
||||
public override void ResetState()
|
||||
{
|
||||
lastRebuildTime = 0D;
|
||||
CustomRanges.Clear();
|
||||
|
@ -7,14 +7,75 @@ namespace Mirror
|
||||
[AddComponentMenu("Network/ Interest Management/ Match/Match Interest Management")]
|
||||
public class MatchInterestManagement : InterestManagement
|
||||
{
|
||||
readonly Dictionary<Guid, HashSet<NetworkIdentity>> matchObjects =
|
||||
new Dictionary<Guid, HashSet<NetworkIdentity>>();
|
||||
[Header("Diagnostics")]
|
||||
[ReadOnly, SerializeField]
|
||||
internal ushort matchCount;
|
||||
|
||||
readonly Dictionary<NetworkIdentity, Guid> lastObjectMatch =
|
||||
new Dictionary<NetworkIdentity, Guid>();
|
||||
readonly Dictionary<Guid, HashSet<NetworkMatch>> matchObjects =
|
||||
new Dictionary<Guid, HashSet<NetworkMatch>>();
|
||||
|
||||
readonly HashSet<Guid> dirtyMatches = new HashSet<Guid>();
|
||||
|
||||
// LateUpdate so that all spawns/despawns/changes are done
|
||||
[ServerCallback]
|
||||
void LateUpdate()
|
||||
{
|
||||
// Rebuild all dirty matches
|
||||
// dirtyMatches will be empty if no matches changed members
|
||||
// by spawning or destroying or changing matchId in this frame.
|
||||
foreach (Guid dirtyMatch in dirtyMatches)
|
||||
{
|
||||
// rebuild always, even if matchObjects[dirtyMatch] is empty.
|
||||
// Players might have left the match, but they may still be spawned.
|
||||
RebuildMatchObservers(dirtyMatch);
|
||||
|
||||
// clean up empty entries in the dict
|
||||
if (matchObjects[dirtyMatch].Count == 0)
|
||||
matchObjects.Remove(dirtyMatch);
|
||||
}
|
||||
|
||||
dirtyMatches.Clear();
|
||||
|
||||
matchCount = (ushort)matchObjects.Count;
|
||||
}
|
||||
|
||||
[ServerCallback]
|
||||
void RebuildMatchObservers(Guid matchId)
|
||||
{
|
||||
foreach (NetworkMatch networkMatch in matchObjects[matchId])
|
||||
if (networkMatch.netIdentity != null)
|
||||
NetworkServer.RebuildObservers(networkMatch.netIdentity, false);
|
||||
}
|
||||
|
||||
// called by NetworkMatch.matchId setter
|
||||
[ServerCallback]
|
||||
internal void OnMatchChanged(NetworkMatch networkMatch, Guid oldMatch)
|
||||
{
|
||||
// This object is in a new match so observers in the prior match
|
||||
// and the new match need to rebuild their respective observers lists.
|
||||
|
||||
// Remove this object from the hashset of the match it just left
|
||||
// Guid.Empty is never a valid matchId
|
||||
if (oldMatch != Guid.Empty)
|
||||
{
|
||||
dirtyMatches.Add(oldMatch);
|
||||
matchObjects[oldMatch].Remove(networkMatch);
|
||||
}
|
||||
|
||||
// Guid.Empty is never a valid matchId
|
||||
if (networkMatch.matchId == Guid.Empty)
|
||||
return;
|
||||
|
||||
dirtyMatches.Add(networkMatch.matchId);
|
||||
|
||||
// Make sure this new match is in the dictionary
|
||||
if (!matchObjects.ContainsKey(networkMatch.matchId))
|
||||
matchObjects[networkMatch.matchId] = new HashSet<NetworkMatch>();
|
||||
|
||||
// Add this object to the hashset of the new match
|
||||
matchObjects[networkMatch.matchId].Add(networkMatch);
|
||||
}
|
||||
|
||||
[ServerCallback]
|
||||
public override void OnSpawned(NetworkIdentity identity)
|
||||
{
|
||||
@ -22,114 +83,43 @@ public override void OnSpawned(NetworkIdentity identity)
|
||||
return;
|
||||
|
||||
Guid networkMatchId = networkMatch.matchId;
|
||||
lastObjectMatch[identity] = networkMatchId;
|
||||
|
||||
// Guid.Empty is never a valid matchId...do not add to matchObjects collection
|
||||
if (networkMatchId == Guid.Empty)
|
||||
return;
|
||||
|
||||
// Debug.Log($"MatchInterestManagement.OnSpawned({identity.name}) currentMatch: {currentMatch}");
|
||||
if (!matchObjects.TryGetValue(networkMatchId, out HashSet<NetworkIdentity> objects))
|
||||
if (!matchObjects.TryGetValue(networkMatchId, out HashSet<NetworkMatch> objects))
|
||||
{
|
||||
objects = new HashSet<NetworkIdentity>();
|
||||
objects = new HashSet<NetworkMatch>();
|
||||
matchObjects.Add(networkMatchId, objects);
|
||||
}
|
||||
|
||||
objects.Add(identity);
|
||||
objects.Add(networkMatch);
|
||||
|
||||
// Match ID could have been set in NetworkBehaviour::OnStartServer on this object.
|
||||
// Since that's after OnCheckObserver is called it would be missed, so force Rebuild here.
|
||||
// Add the current match to dirtyMatches for Update to rebuild it.
|
||||
// Add the current match to dirtyMatches for LateUpdate to rebuild it.
|
||||
dirtyMatches.Add(networkMatchId);
|
||||
}
|
||||
|
||||
[ServerCallback]
|
||||
public override void OnDestroyed(NetworkIdentity identity)
|
||||
{
|
||||
// Don't RebuildSceneObservers here - that will happen in Update.
|
||||
// Don't RebuildSceneObservers here - that will happen in LateUpdate.
|
||||
// Multiple objects could be destroyed in same frame and we don't
|
||||
// want to rebuild for each one...let Update do it once.
|
||||
// We must add the current match to dirtyMatches for Update to rebuild it.
|
||||
if (lastObjectMatch.TryGetValue(identity, out Guid currentMatch))
|
||||
// want to rebuild for each one...let LateUpdate do it once.
|
||||
// We must add the current match to dirtyMatches for LateUpdate to rebuild it.
|
||||
if (identity.TryGetComponent(out NetworkMatch currentMatch))
|
||||
{
|
||||
lastObjectMatch.Remove(identity);
|
||||
if (currentMatch != Guid.Empty && matchObjects.TryGetValue(currentMatch, out HashSet<NetworkIdentity> objects) && objects.Remove(identity))
|
||||
dirtyMatches.Add(currentMatch);
|
||||
if (currentMatch.matchId != Guid.Empty &&
|
||||
matchObjects.TryGetValue(currentMatch.matchId, out HashSet<NetworkMatch> objects) &&
|
||||
objects.Remove(currentMatch))
|
||||
dirtyMatches.Add(currentMatch.matchId);
|
||||
}
|
||||
}
|
||||
|
||||
// internal so we can update from tests
|
||||
[ServerCallback]
|
||||
internal void Update()
|
||||
{
|
||||
// for each spawned:
|
||||
// if match changed:
|
||||
// add previous to dirty
|
||||
// add new to dirty
|
||||
foreach (NetworkIdentity identity in NetworkServer.spawned.Values)
|
||||
{
|
||||
// Ignore objects that don't have a NetworkMatch component
|
||||
if (!identity.TryGetComponent(out NetworkMatch networkMatch))
|
||||
continue;
|
||||
|
||||
Guid newMatch = networkMatch.matchId;
|
||||
if (!lastObjectMatch.TryGetValue(identity, out Guid currentMatch))
|
||||
continue;
|
||||
|
||||
// Guid.Empty is never a valid matchId
|
||||
// Nothing to do if matchId hasn't changed
|
||||
if (newMatch == Guid.Empty || newMatch == currentMatch)
|
||||
continue;
|
||||
|
||||
// Mark new/old matches as dirty so they get rebuilt
|
||||
UpdateDirtyMatches(newMatch, currentMatch);
|
||||
|
||||
// This object is in a new match so observers in the prior match
|
||||
// and the new match need to rebuild their respective observers lists.
|
||||
UpdateMatchObjects(identity, newMatch, currentMatch);
|
||||
}
|
||||
|
||||
// rebuild all dirty matches
|
||||
foreach (Guid dirtyMatch in dirtyMatches)
|
||||
RebuildMatchObservers(dirtyMatch);
|
||||
|
||||
dirtyMatches.Clear();
|
||||
}
|
||||
|
||||
void UpdateDirtyMatches(Guid newMatch, Guid currentMatch)
|
||||
{
|
||||
// Guid.Empty is never a valid matchId
|
||||
if (currentMatch != Guid.Empty)
|
||||
dirtyMatches.Add(currentMatch);
|
||||
|
||||
dirtyMatches.Add(newMatch);
|
||||
}
|
||||
|
||||
void UpdateMatchObjects(NetworkIdentity netIdentity, Guid newMatch, Guid currentMatch)
|
||||
{
|
||||
// Remove this object from the hashset of the match it just left
|
||||
// Guid.Empty is never a valid matchId
|
||||
if (currentMatch != Guid.Empty)
|
||||
matchObjects[currentMatch].Remove(netIdentity);
|
||||
|
||||
// Set this to the new match this object just entered
|
||||
lastObjectMatch[netIdentity] = newMatch;
|
||||
|
||||
// Make sure this new match is in the dictionary
|
||||
if (!matchObjects.ContainsKey(newMatch))
|
||||
matchObjects.Add(newMatch, new HashSet<NetworkIdentity>());
|
||||
|
||||
// Add this object to the hashset of the new match
|
||||
matchObjects[newMatch].Add(netIdentity);
|
||||
}
|
||||
|
||||
void RebuildMatchObservers(Guid matchId)
|
||||
{
|
||||
foreach (NetworkIdentity netIdentity in matchObjects[matchId])
|
||||
if (netIdentity != null)
|
||||
NetworkServer.RebuildObservers(netIdentity, false);
|
||||
}
|
||||
|
||||
public override bool OnCheckObserver(NetworkIdentity identity, NetworkConnectionToClient newObserver)
|
||||
{
|
||||
// Never observed if no NetworkMatch component
|
||||
@ -151,24 +141,24 @@ public override bool OnCheckObserver(NetworkIdentity identity, NetworkConnection
|
||||
return identityNetworkMatch.matchId == newObserverNetworkMatch.matchId;
|
||||
}
|
||||
|
||||
[ServerCallback]
|
||||
public override void OnRebuildObservers(NetworkIdentity identity, HashSet<NetworkConnectionToClient> newObservers)
|
||||
{
|
||||
if (!identity.TryGetComponent(out NetworkMatch networkMatch))
|
||||
return;
|
||||
|
||||
Guid matchId = networkMatch.matchId;
|
||||
|
||||
// Guid.Empty is never a valid matchId
|
||||
if (matchId == Guid.Empty)
|
||||
if (networkMatch.matchId == Guid.Empty)
|
||||
return;
|
||||
|
||||
if (!matchObjects.TryGetValue(matchId, out HashSet<NetworkIdentity> objects))
|
||||
// Abort if this match hasn't been created yet by OnSpawned or OnMatchChanged
|
||||
if (!matchObjects.TryGetValue(networkMatch.matchId, out HashSet<NetworkMatch> objects))
|
||||
return;
|
||||
|
||||
// Add everything in the hashset for this object's current match
|
||||
foreach (NetworkIdentity networkIdentity in objects)
|
||||
if (networkIdentity != null && networkIdentity.connectionToClient != null)
|
||||
newObservers.Add(networkIdentity.connectionToClient);
|
||||
foreach (NetworkMatch netMatch in objects)
|
||||
if (netMatch.netIdentity != null && netMatch.netIdentity.connectionToClient != null)
|
||||
newObservers.Add(netMatch.netIdentity.connectionToClient);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,34 @@ namespace Mirror
|
||||
[HelpURL("https://mirror-networking.gitbook.io/docs/guides/interest-management")]
|
||||
public class NetworkMatch : NetworkBehaviour
|
||||
{
|
||||
Guid _matchId;
|
||||
|
||||
#pragma warning disable IDE0052 // Suppress warning for unused field...this is for debugging purposes
|
||||
[SerializeField, ReadOnly]
|
||||
[Tooltip("Match ID is shown here on server for debugging purposes.")]
|
||||
string MatchID = string.Empty;
|
||||
#pragma warning restore IDE0052
|
||||
|
||||
///<summary>Set this to the same value on all networked objects that belong to a given match</summary>
|
||||
public Guid matchId;
|
||||
public Guid matchId
|
||||
{
|
||||
get => _matchId;
|
||||
set
|
||||
{
|
||||
if (!NetworkServer.active)
|
||||
throw new InvalidOperationException("matchId can only be set at runtime on active server");
|
||||
|
||||
if (_matchId == value)
|
||||
return;
|
||||
|
||||
Guid oldMatch = _matchId;
|
||||
_matchId = value;
|
||||
MatchID = value.ToString();
|
||||
|
||||
// Only inform the AOI if this netIdentity has been spawned (isServer) and only if using a MatchInterestManagement
|
||||
if (isServer && NetworkServer.aoi is MatchInterestManagement matchInterestManagement)
|
||||
matchInterestManagement.OnMatchChanged(this, oldMatch);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,106 @@
|
||||
// Grid3D based on Grid2D
|
||||
// -> not named 'Grid' because Unity already has a Grid type. causes warnings.
|
||||
// -> struct to avoid memory indirection. it's accessed a lot.
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
// struct to avoid memory indirection. it's accessed a lot.
|
||||
public struct Grid3D<T>
|
||||
{
|
||||
// the grid
|
||||
// note that we never remove old keys.
|
||||
// => over time, HashSet<T>s will be allocated for every possible
|
||||
// grid position in the world
|
||||
// => Clear() doesn't clear them so we don't constantly reallocate the
|
||||
// entries when populating the grid in every Update() call
|
||||
// => makes the code a lot easier too
|
||||
// => this is FINE because in the worst case, every grid position in the
|
||||
// game world is filled with a player anyway!
|
||||
readonly Dictionary<Vector3Int, HashSet<T>> grid;
|
||||
|
||||
// cache a 9 x 3 neighbor grid of vector3 offsets so we can use them more easily
|
||||
readonly Vector3Int[] neighbourOffsets;
|
||||
|
||||
public Grid3D(int initialCapacity)
|
||||
{
|
||||
grid = new Dictionary<Vector3Int, HashSet<T>>(initialCapacity);
|
||||
|
||||
neighbourOffsets = new Vector3Int[9 * 3];
|
||||
int i = 0;
|
||||
for (int x = -1; x <= 1; x++)
|
||||
{
|
||||
for (int y = -1; y <= 1; y++)
|
||||
{
|
||||
for (int z = -1; z <= 1; z++)
|
||||
{
|
||||
neighbourOffsets[i] = new Vector3Int(x, y, z);
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// helper function so we can add an entry without worrying
|
||||
public void Add(Vector3Int position, T value)
|
||||
{
|
||||
// initialize set in grid if it's not in there yet
|
||||
if (!grid.TryGetValue(position, out HashSet<T> hashSet))
|
||||
{
|
||||
// each grid entry may hold hundreds of entities.
|
||||
// let's create the HashSet with a large initial capacity
|
||||
// in order to avoid resizing & allocations.
|
||||
#if !UNITY_2021_3_OR_NEWER
|
||||
// Unity 2019 doesn't have "new HashSet(capacity)" yet
|
||||
hashSet = new HashSet<T>();
|
||||
#else
|
||||
hashSet = new HashSet<T>(128);
|
||||
#endif
|
||||
grid[position] = hashSet;
|
||||
}
|
||||
|
||||
// add to it
|
||||
hashSet.Add(value);
|
||||
}
|
||||
|
||||
// helper function to get set at position without worrying
|
||||
// -> result is passed as parameter to avoid allocations
|
||||
// -> result is not cleared before. this allows us to pass the HashSet from
|
||||
// GetWithNeighbours and avoid .UnionWith which is very expensive.
|
||||
void GetAt(Vector3Int position, HashSet<T> result)
|
||||
{
|
||||
// return the set at position
|
||||
if (grid.TryGetValue(position, out HashSet<T> hashSet))
|
||||
{
|
||||
foreach (T entry in hashSet)
|
||||
result.Add(entry);
|
||||
}
|
||||
}
|
||||
|
||||
// helper function to get at position and it's 8 neighbors without worrying
|
||||
// -> result is passed as parameter to avoid allocations
|
||||
public void GetWithNeighbours(Vector3Int position, HashSet<T> result)
|
||||
{
|
||||
// clear result first
|
||||
result.Clear();
|
||||
|
||||
// add neighbours
|
||||
foreach (Vector3Int offset in neighbourOffsets)
|
||||
GetAt(position + offset, result);
|
||||
}
|
||||
|
||||
// clear: clears the whole grid
|
||||
// IMPORTANT: we already allocated HashSet<T>s and don't want to do
|
||||
// reallocate every single update when we rebuild the grid.
|
||||
// => so simply remove each position's entries, but keep
|
||||
// every position in there
|
||||
// => see 'grid' comments above!
|
||||
// => named ClearNonAlloc to make it more obvious!
|
||||
public void ClearNonAlloc()
|
||||
{
|
||||
foreach (HashSet<T> hashSet in grid.Values)
|
||||
hashSet.Clear();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b157c08313c64752b0856469b1b70771
|
||||
timeCreated: 1713533175
|
@ -0,0 +1,146 @@
|
||||
// extremely fast spatial hashing interest management based on uMMORPG GridChecker.
|
||||
// => 30x faster in initial tests
|
||||
// => scales way higher
|
||||
// checks on three dimensions (XYZ) which includes the vertical axes.
|
||||
// this is slower than XY checking for regular spatial hashing.
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
[AddComponentMenu("Network/ Interest Management/ Spatial Hash/Spatial Hashing Interest Management")]
|
||||
public class SpatialHashing3DInterestManagement : InterestManagement
|
||||
{
|
||||
[Tooltip("The maximum range that objects will be visible at.")]
|
||||
public int visRange = 30;
|
||||
|
||||
// we use a 9 neighbour grid.
|
||||
// so we always see in a distance of 2 grids.
|
||||
// for example, our own grid and then one on top / below / left / right.
|
||||
//
|
||||
// this means that grid resolution needs to be distance / 2.
|
||||
// so for example, for distance = 30 we see 2 cells = 15 * 2 distance.
|
||||
//
|
||||
// on first sight, it seems we need distance / 3 (we see left/us/right).
|
||||
// but that's not the case.
|
||||
// resolution would be 10, and we only see 1 cell far, so 10+10=20.
|
||||
public int resolution => visRange / 2; // same as XY because if XY is rotated 90 degree for 3D, it's still the same distance
|
||||
|
||||
[Tooltip("Rebuild all every 'rebuildInterval' seconds.")]
|
||||
public float rebuildInterval = 1;
|
||||
double lastRebuildTime;
|
||||
|
||||
[Header("Debug Settings")]
|
||||
public bool showSlider;
|
||||
|
||||
// the grid
|
||||
// begin with a large capacity to avoid resizing & allocations.
|
||||
Grid3D<NetworkConnectionToClient> grid = new Grid3D<NetworkConnectionToClient>(1024);
|
||||
|
||||
// project 3d world position to grid position
|
||||
Vector3Int ProjectToGrid(Vector3 position) =>
|
||||
Vector3Int.RoundToInt(position / resolution);
|
||||
|
||||
public override bool OnCheckObserver(NetworkIdentity identity, NetworkConnectionToClient newObserver)
|
||||
{
|
||||
// calculate projected positions
|
||||
Vector3Int projected = ProjectToGrid(identity.transform.position);
|
||||
Vector3Int observerProjected = ProjectToGrid(newObserver.identity.transform.position);
|
||||
|
||||
// distance needs to be at max one of the 8 neighbors, which is
|
||||
// 1 for the direct neighbors
|
||||
// 1.41 for the diagonal neighbors (= sqrt(2))
|
||||
// => use sqrMagnitude and '2' to avoid computations. same result.
|
||||
return (projected - observerProjected).sqrMagnitude <= 2; // same as XY because if XY is rotated 90 degree for 3D, it's still the same distance
|
||||
}
|
||||
|
||||
public override void OnRebuildObservers(NetworkIdentity identity, HashSet<NetworkConnectionToClient> newObservers)
|
||||
{
|
||||
// add everyone in 9 neighbour grid
|
||||
// -> pass observers to GetWithNeighbours directly to avoid allocations
|
||||
// and expensive .UnionWith computations.
|
||||
Vector3Int current = ProjectToGrid(identity.transform.position);
|
||||
grid.GetWithNeighbours(current, newObservers);
|
||||
}
|
||||
|
||||
[ServerCallback]
|
||||
public override void ResetState()
|
||||
{
|
||||
lastRebuildTime = 0D;
|
||||
}
|
||||
|
||||
// update everyone's position in the grid
|
||||
// (internal so we can update from tests)
|
||||
[ServerCallback]
|
||||
internal void Update()
|
||||
{
|
||||
// NOTE: unlike Scene/MatchInterestManagement, this rebuilds ALL
|
||||
// entities every INTERVAL. consider the other approach later.
|
||||
|
||||
// IMPORTANT: refresh grid every update!
|
||||
// => newly spawned entities get observers assigned via
|
||||
// OnCheckObservers. this can happen any time and we don't want
|
||||
// them broadcast to old (moved or destroyed) connections.
|
||||
// => players do move all the time. we want them to always be in the
|
||||
// correct grid position.
|
||||
// => note that the actual 'rebuildall' doesn't need to happen all
|
||||
// the time.
|
||||
// NOTE: consider refreshing grid only every 'interval' too. but not
|
||||
// for now. stability & correctness matter.
|
||||
|
||||
// clear old grid results before we update everyone's position.
|
||||
// (this way we get rid of destroyed connections automatically)
|
||||
//
|
||||
// NOTE: keeps allocated HashSets internally.
|
||||
// clearing & populating every frame works without allocations
|
||||
grid.ClearNonAlloc();
|
||||
|
||||
// put every connection into the grid at it's main player's position
|
||||
// NOTE: player sees in a radius around him. NOT around his pet too.
|
||||
foreach (NetworkConnectionToClient connection in NetworkServer.connections.Values)
|
||||
{
|
||||
// authenticated and joined world with a player?
|
||||
if (connection.isAuthenticated && connection.identity != null)
|
||||
{
|
||||
// calculate current grid position
|
||||
Vector3Int position = ProjectToGrid(connection.identity.transform.position);
|
||||
|
||||
// put into grid
|
||||
grid.Add(position, connection);
|
||||
}
|
||||
}
|
||||
|
||||
// rebuild all spawned entities' observers every 'interval'
|
||||
// this will call OnRebuildObservers which then returns the
|
||||
// observers at grid[position] for each entity.
|
||||
if (NetworkTime.localTime >= lastRebuildTime + rebuildInterval)
|
||||
{
|
||||
RebuildAll();
|
||||
lastRebuildTime = NetworkTime.localTime;
|
||||
}
|
||||
}
|
||||
|
||||
// OnGUI allocates even if it does nothing. avoid in release.
|
||||
#if UNITY_EDITOR || DEVELOPMENT_BUILD
|
||||
// slider from dotsnet. it's nice to play around with in the benchmark
|
||||
// demo.
|
||||
void OnGUI()
|
||||
{
|
||||
if (!showSlider) return;
|
||||
|
||||
// only show while server is running. not on client, etc.
|
||||
if (!NetworkServer.active) return;
|
||||
|
||||
int height = 30;
|
||||
int width = 250;
|
||||
GUILayout.BeginArea(new Rect(Screen.width / 2 - width / 2, Screen.height - height, width, height));
|
||||
GUILayout.BeginHorizontal("Box");
|
||||
GUILayout.Label("Radius:");
|
||||
visRange = Mathf.RoundToInt(GUILayout.HorizontalSlider(visRange, 0, 200, GUILayout.Width(150)));
|
||||
GUILayout.Label(visRange.ToString());
|
||||
GUILayout.EndHorizontal();
|
||||
GUILayout.EndArea();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 120b4d6121d94e0280cd2ec536b0ea8f
|
||||
timeCreated: 1713534045
|
@ -1,6 +1,8 @@
|
||||
// extremely fast spatial hashing interest management based on uMMORPG GridChecker.
|
||||
// => 30x faster in initial tests
|
||||
// => scales way higher
|
||||
// checks on two dimensions only(!), for example: XZ for 3D games or XY for 2D games.
|
||||
// this is faster than XYZ checking but doesn't check vertical distance.
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
@ -36,7 +38,7 @@ public enum CheckMethod
|
||||
[Tooltip("Spatial Hashing supports 3D (XZ) and 2D (XY) games.")]
|
||||
public CheckMethod checkMethod = CheckMethod.XZ_FOR_3D;
|
||||
|
||||
// debugging
|
||||
[Header("Debug Settings")]
|
||||
public bool showSlider;
|
||||
|
||||
// the grid
|
||||
@ -72,7 +74,7 @@ public override void OnRebuildObservers(NetworkIdentity identity, HashSet<Networ
|
||||
}
|
||||
|
||||
[ServerCallback]
|
||||
public override void Reset()
|
||||
public override void ResetState()
|
||||
{
|
||||
lastRebuildTime = 0D;
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
// simple component that holds team information
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
@ -8,10 +9,31 @@ namespace Mirror
|
||||
[HelpURL("https://mirror-networking.gitbook.io/docs/guides/interest-management")]
|
||||
public class NetworkTeam : NetworkBehaviour
|
||||
{
|
||||
[Tooltip("Set this to the same value on all networked objects that belong to a given team")]
|
||||
[SyncVar] public string teamId = string.Empty;
|
||||
[SerializeField]
|
||||
[Tooltip("Set teamId on Server at runtime to the same value on all networked objects that belong to a given team")]
|
||||
string _teamId;
|
||||
|
||||
public string teamId
|
||||
{
|
||||
get => _teamId;
|
||||
set
|
||||
{
|
||||
if (Application.IsPlaying(gameObject) && !NetworkServer.active)
|
||||
throw new InvalidOperationException("teamId can only be set at runtime on active server");
|
||||
|
||||
if (_teamId == value)
|
||||
return;
|
||||
|
||||
string oldTeam = _teamId;
|
||||
_teamId = value;
|
||||
|
||||
//Only inform the AOI if this netIdentity has been spawned(isServer) and only if using a TeamInterestManagement
|
||||
if (isServer && NetworkServer.aoi is TeamInterestManagement teamInterestManagement)
|
||||
teamInterestManagement.OnTeamChanged(this, oldTeam);
|
||||
}
|
||||
}
|
||||
|
||||
[Tooltip("When enabled this object is visible to all clients. Typically this would be true for player objects")]
|
||||
[SyncVar] public bool forceShown;
|
||||
public bool forceShown;
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
@ -6,126 +6,112 @@ namespace Mirror
|
||||
[AddComponentMenu("Network/ Interest Management/ Team/Team Interest Management")]
|
||||
public class TeamInterestManagement : InterestManagement
|
||||
{
|
||||
readonly Dictionary<string, HashSet<NetworkIdentity>> teamObjects = new Dictionary<string, HashSet<NetworkIdentity>>();
|
||||
readonly Dictionary<NetworkIdentity, string> lastObjectTeam = new Dictionary<NetworkIdentity, string>();
|
||||
readonly Dictionary<string, HashSet<NetworkTeam>> teamObjects =
|
||||
new Dictionary<string, HashSet<NetworkTeam>>();
|
||||
|
||||
readonly HashSet<string> dirtyTeams = new HashSet<string>();
|
||||
|
||||
// LateUpdate so that all spawns/despawns/changes are done
|
||||
[ServerCallback]
|
||||
void LateUpdate()
|
||||
{
|
||||
// Rebuild all dirty teams
|
||||
// dirtyTeams will be empty if no teams changed members
|
||||
// by spawning or destroying or changing teamId in this frame.
|
||||
foreach (string dirtyTeam in dirtyTeams)
|
||||
{
|
||||
// rebuild always, even if teamObjects[dirtyTeam] is empty.
|
||||
// Players might have left the team, but they may still be spawned.
|
||||
RebuildTeamObservers(dirtyTeam);
|
||||
|
||||
// clean up empty entries in the dict
|
||||
if (teamObjects[dirtyTeam].Count == 0)
|
||||
teamObjects.Remove(dirtyTeam);
|
||||
}
|
||||
|
||||
dirtyTeams.Clear();
|
||||
}
|
||||
|
||||
[ServerCallback]
|
||||
void RebuildTeamObservers(string teamId)
|
||||
{
|
||||
foreach (NetworkTeam networkTeam in teamObjects[teamId])
|
||||
if (networkTeam.netIdentity != null)
|
||||
NetworkServer.RebuildObservers(networkTeam.netIdentity, false);
|
||||
}
|
||||
|
||||
// called by NetworkTeam.teamId setter
|
||||
[ServerCallback]
|
||||
internal void OnTeamChanged(NetworkTeam networkTeam, string oldTeam)
|
||||
{
|
||||
// This object is in a new team so observers in the prior team
|
||||
// and the new team need to rebuild their respective observers lists.
|
||||
|
||||
// Remove this object from the hashset of the team it just left
|
||||
// Null / Empty string is never a valid teamId
|
||||
if (!string.IsNullOrWhiteSpace(oldTeam))
|
||||
{
|
||||
dirtyTeams.Add(oldTeam);
|
||||
teamObjects[oldTeam].Remove(networkTeam);
|
||||
}
|
||||
|
||||
// Null / Empty string is never a valid teamId
|
||||
if (string.IsNullOrWhiteSpace(networkTeam.teamId))
|
||||
return;
|
||||
|
||||
dirtyTeams.Add(networkTeam.teamId);
|
||||
|
||||
// Make sure this new team is in the dictionary
|
||||
if (!teamObjects.ContainsKey(networkTeam.teamId))
|
||||
teamObjects[networkTeam.teamId] = new HashSet<NetworkTeam>();
|
||||
|
||||
// Add this object to the hashset of the new team
|
||||
teamObjects[networkTeam.teamId].Add(networkTeam);
|
||||
}
|
||||
|
||||
[ServerCallback]
|
||||
public override void OnSpawned(NetworkIdentity identity)
|
||||
{
|
||||
if (!identity.TryGetComponent(out NetworkTeam identityNetworkTeam))
|
||||
if (!identity.TryGetComponent(out NetworkTeam networkTeam))
|
||||
return;
|
||||
|
||||
string networkTeamId = identityNetworkTeam.teamId;
|
||||
lastObjectTeam[identity] = networkTeamId;
|
||||
string networkTeamId = networkTeam.teamId;
|
||||
|
||||
// Null / Empty string is never a valid teamId...do not add to teamObjects collection
|
||||
if (string.IsNullOrWhiteSpace(networkTeamId))
|
||||
return;
|
||||
|
||||
//Debug.Log($"TeamInterestManagement.OnSpawned {identity.name} {networkTeamId}");
|
||||
|
||||
if (!teamObjects.TryGetValue(networkTeamId, out HashSet<NetworkIdentity> objects))
|
||||
// Debug.Log($"TeamInterestManagement.OnSpawned({identity.name}) currentTeam: {currentTeam}");
|
||||
if (!teamObjects.TryGetValue(networkTeamId, out HashSet<NetworkTeam> objects))
|
||||
{
|
||||
objects = new HashSet<NetworkIdentity>();
|
||||
objects = new HashSet<NetworkTeam>();
|
||||
teamObjects.Add(networkTeamId, objects);
|
||||
}
|
||||
|
||||
objects.Add(identity);
|
||||
objects.Add(networkTeam);
|
||||
|
||||
// Team ID could have been set in NetworkBehaviour::OnStartServer on this object.
|
||||
// Since that's after OnCheckObserver is called it would be missed, so force Rebuild here.
|
||||
// Add the current team to dirtyTeams for Update to rebuild it.
|
||||
// Add the current team to dirtyTeames for LateUpdate to rebuild it.
|
||||
dirtyTeams.Add(networkTeamId);
|
||||
}
|
||||
|
||||
[ServerCallback]
|
||||
public override void OnDestroyed(NetworkIdentity identity)
|
||||
{
|
||||
// Don't RebuildSceneObservers here - that will happen in Update.
|
||||
// Don't RebuildSceneObservers here - that will happen in LateUpdate.
|
||||
// Multiple objects could be destroyed in same frame and we don't
|
||||
// want to rebuild for each one...let Update do it once.
|
||||
// We must add the current team to dirtyTeams for Update to rebuild it.
|
||||
if (lastObjectTeam.TryGetValue(identity, out string currentTeam))
|
||||
// want to rebuild for each one...let LateUpdate do it once.
|
||||
// We must add the current team to dirtyTeames for LateUpdate to rebuild it.
|
||||
if (identity.TryGetComponent(out NetworkTeam currentTeam))
|
||||
{
|
||||
lastObjectTeam.Remove(identity);
|
||||
if (!string.IsNullOrWhiteSpace(currentTeam) && teamObjects.TryGetValue(currentTeam, out HashSet<NetworkIdentity> objects) && objects.Remove(identity))
|
||||
dirtyTeams.Add(currentTeam);
|
||||
if (!string.IsNullOrWhiteSpace(currentTeam.teamId) &&
|
||||
teamObjects.TryGetValue(currentTeam.teamId, out HashSet<NetworkTeam> objects) &&
|
||||
objects.Remove(currentTeam))
|
||||
dirtyTeams.Add(currentTeam.teamId);
|
||||
}
|
||||
}
|
||||
|
||||
// internal so we can update from tests
|
||||
[ServerCallback]
|
||||
internal void Update()
|
||||
{
|
||||
// for each spawned:
|
||||
// if team changed:
|
||||
// add previous to dirty
|
||||
// add new to dirty
|
||||
foreach (NetworkIdentity netIdentity in NetworkServer.spawned.Values)
|
||||
{
|
||||
// Ignore objects that don't have a NetworkTeam component
|
||||
if (!netIdentity.TryGetComponent(out NetworkTeam identityNetworkTeam))
|
||||
continue;
|
||||
|
||||
string networkTeamId = identityNetworkTeam.teamId;
|
||||
if (!lastObjectTeam.TryGetValue(netIdentity, out string currentTeam))
|
||||
continue;
|
||||
|
||||
// Null / Empty string is never a valid teamId
|
||||
// Nothing to do if teamId hasn't changed
|
||||
if (string.IsNullOrWhiteSpace(networkTeamId) || networkTeamId == currentTeam)
|
||||
continue;
|
||||
|
||||
// Mark new/old Teams as dirty so they get rebuilt
|
||||
UpdateDirtyTeams(networkTeamId, currentTeam);
|
||||
|
||||
// This object is in a new team so observers in the prior team
|
||||
// and the new team need to rebuild their respective observers lists.
|
||||
UpdateTeamObjects(netIdentity, networkTeamId, currentTeam);
|
||||
}
|
||||
|
||||
// rebuild all dirty teams
|
||||
foreach (string dirtyTeam in dirtyTeams)
|
||||
RebuildTeamObservers(dirtyTeam);
|
||||
|
||||
dirtyTeams.Clear();
|
||||
}
|
||||
|
||||
void UpdateDirtyTeams(string newTeam, string currentTeam)
|
||||
{
|
||||
// Null / Empty string is never a valid teamId
|
||||
if (!string.IsNullOrWhiteSpace(currentTeam))
|
||||
dirtyTeams.Add(currentTeam);
|
||||
|
||||
dirtyTeams.Add(newTeam);
|
||||
}
|
||||
|
||||
void UpdateTeamObjects(NetworkIdentity netIdentity, string newTeam, string currentTeam)
|
||||
{
|
||||
// Remove this object from the hashset of the team it just left
|
||||
// string.Empty is never a valid teamId
|
||||
if (!string.IsNullOrWhiteSpace(currentTeam))
|
||||
teamObjects[currentTeam].Remove(netIdentity);
|
||||
|
||||
// Set this to the new team this object just entered
|
||||
lastObjectTeam[netIdentity] = newTeam;
|
||||
|
||||
// Make sure this new team is in the dictionary
|
||||
if (!teamObjects.ContainsKey(newTeam))
|
||||
teamObjects.Add(newTeam, new HashSet<NetworkIdentity>());
|
||||
|
||||
// Add this object to the hashset of the new team
|
||||
teamObjects[newTeam].Add(netIdentity);
|
||||
}
|
||||
|
||||
void RebuildTeamObservers(string teamId)
|
||||
{
|
||||
foreach (NetworkIdentity netIdentity in teamObjects[teamId])
|
||||
if (netIdentity != null)
|
||||
NetworkServer.RebuildObservers(netIdentity, false);
|
||||
}
|
||||
|
||||
public override bool OnCheckObserver(NetworkIdentity identity, NetworkConnectionToClient newObserver)
|
||||
{
|
||||
// Always observed if no NetworkTeam component
|
||||
@ -135,7 +121,7 @@ public override bool OnCheckObserver(NetworkIdentity identity, NetworkConnection
|
||||
if (identityNetworkTeam.forceShown)
|
||||
return true;
|
||||
|
||||
// string.Empty is never a valid teamId
|
||||
// Null / Empty string is never a valid teamId
|
||||
if (string.IsNullOrWhiteSpace(identityNetworkTeam.teamId))
|
||||
return false;
|
||||
|
||||
@ -149,7 +135,7 @@ public override bool OnCheckObserver(NetworkIdentity identity, NetworkConnection
|
||||
|
||||
//Debug.Log($"TeamInterestManagement.OnCheckObserver {identity.name} {identityNetworkTeam.teamId} | {newObserver.identity.name} {newObserverNetworkTeam.teamId}");
|
||||
|
||||
// Observed only if teamId's match
|
||||
// Observed only if teamId's team
|
||||
return identityNetworkTeam.teamId == newObserverNetworkTeam.teamId;
|
||||
}
|
||||
|
||||
@ -173,14 +159,14 @@ public override void OnRebuildObservers(NetworkIdentity identity, HashSet<Networ
|
||||
if (string.IsNullOrWhiteSpace(networkTeam.teamId))
|
||||
return;
|
||||
|
||||
// Abort if this team hasn't been created yet by OnSpawned or UpdateTeamObjects
|
||||
if (!teamObjects.TryGetValue(networkTeam.teamId, out HashSet<NetworkIdentity> objects))
|
||||
// Abort if this team hasn't been created yet by OnSpawned or OnTeamChanged
|
||||
if (!teamObjects.TryGetValue(networkTeam.teamId, out HashSet<NetworkTeam> objects))
|
||||
return;
|
||||
|
||||
// Add everything in the hashset for this object's current team
|
||||
foreach (NetworkIdentity networkIdentity in objects)
|
||||
if (networkIdentity != null && networkIdentity.connectionToClient != null)
|
||||
newObservers.Add(networkIdentity.connectionToClient);
|
||||
foreach (NetworkTeam netTeam in objects)
|
||||
if (netTeam.netIdentity != null && netTeam.netIdentity.connectionToClient != null)
|
||||
newObservers.Add(netTeam.netIdentity.connectionToClient);
|
||||
}
|
||||
|
||||
void AddAllConnections(HashSet<NetworkConnectionToClient> newObservers)
|
||||
|
@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 44e823b93c7d2477c8796766dc364c59
|
||||
guid: 00ac1d0527f234939aba22b4d7cbf280
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
109
Assets/Mirror/Components/LagCompensation/HistoryCollider.cs
Normal file
109
Assets/Mirror/Components/LagCompensation/HistoryCollider.cs
Normal file
@ -0,0 +1,109 @@
|
||||
// Applies HistoryBounds to the physics world by projecting to a trigger Collider.
|
||||
// This way we can use Physics.Raycast on it.
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
[DisallowMultipleComponent]
|
||||
[AddComponentMenu("Network/ Lag Compensation/ History Collider")]
|
||||
public class HistoryCollider : MonoBehaviour
|
||||
{
|
||||
[Header("Components")]
|
||||
[Tooltip("The object's actual collider. We need to know where it is, and how large it is.")]
|
||||
public Collider actualCollider;
|
||||
|
||||
[Tooltip("The helper collider that the history bounds are projected onto.\nNeeds to be added to a child GameObject to counter-rotate an axis aligned Bounding Box onto it.\nThis is only used by this component.")]
|
||||
public BoxCollider boundsCollider;
|
||||
|
||||
[Header("History")]
|
||||
[Tooltip("Keep this many past bounds in the buffer. The larger this is, the further we can raycast into the past.\nMaximum time := historyAmount * captureInterval")]
|
||||
public int boundsLimit = 8;
|
||||
|
||||
[Tooltip("Gather N bounds at a time into a bucket for faster encapsulation. A factor of 2 will be twice as fast, etc.")]
|
||||
public int boundsPerBucket = 2;
|
||||
|
||||
[Tooltip("Capture bounds every 'captureInterval' seconds. Larger values will require fewer computations, but may not capture every small move.")]
|
||||
public float captureInterval = 0.100f; // 100 ms
|
||||
double lastCaptureTime = 0;
|
||||
|
||||
[Header("Debug")]
|
||||
public Color historyColor = new Color(1.0f, 0.5f, 0.0f, 1.0f);
|
||||
public Color currentColor = Color.red;
|
||||
|
||||
protected HistoryBounds history = null;
|
||||
|
||||
protected virtual void Awake()
|
||||
{
|
||||
history = new HistoryBounds(boundsLimit, boundsPerBucket);
|
||||
|
||||
// ensure colliders were set.
|
||||
// bounds collider should always be a trigger.
|
||||
if (actualCollider == null) Debug.LogError("HistoryCollider: actualCollider was not set.");
|
||||
if (boundsCollider == null) Debug.LogError("HistoryCollider: boundsCollider was not set.");
|
||||
if (boundsCollider.transform.parent != transform) Debug.LogError("HistoryCollider: boundsCollider must be a child of this GameObject.");
|
||||
if (!boundsCollider.isTrigger) Debug.LogError("HistoryCollider: boundsCollider must be a trigger.");
|
||||
}
|
||||
|
||||
// capturing and projecting onto colliders should use physics update
|
||||
protected virtual void FixedUpdate()
|
||||
{
|
||||
// capture current bounds every interval
|
||||
if (NetworkTime.localTime >= lastCaptureTime + captureInterval)
|
||||
{
|
||||
lastCaptureTime = NetworkTime.localTime;
|
||||
CaptureBounds();
|
||||
}
|
||||
|
||||
// project bounds onto helper collider
|
||||
ProjectBounds();
|
||||
}
|
||||
|
||||
protected virtual void CaptureBounds()
|
||||
{
|
||||
// grab current collider bounds
|
||||
// this is in world space coordinates, and axis aligned
|
||||
// TODO double check
|
||||
Bounds bounds = actualCollider.bounds;
|
||||
|
||||
// insert into history
|
||||
history.Insert(bounds);
|
||||
}
|
||||
|
||||
protected virtual void ProjectBounds()
|
||||
{
|
||||
// grab total collider encapsulating all of history
|
||||
Bounds total = history.total;
|
||||
|
||||
// don't assign empty bounds, this will throw a Unity warning
|
||||
if (history.boundsCount == 0) return;
|
||||
|
||||
// scale projection doesn't work yet.
|
||||
// for now, don't allow scale changes.
|
||||
if (transform.lossyScale != Vector3.one)
|
||||
{
|
||||
Debug.LogWarning($"HistoryCollider: {name}'s transform global scale must be (1,1,1).");
|
||||
return;
|
||||
}
|
||||
|
||||
// counter rotate the child collider against the gameobject's rotation.
|
||||
// we need this to always be axis aligned.
|
||||
boundsCollider.transform.localRotation = Quaternion.Inverse(transform.rotation);
|
||||
|
||||
// project world space bounds to collider's local space
|
||||
boundsCollider.center = boundsCollider.transform.InverseTransformPoint(total.center);
|
||||
boundsCollider.size = total.size; // TODO projection?
|
||||
}
|
||||
|
||||
// TODO runtime drawing for debugging?
|
||||
protected virtual void OnDrawGizmos()
|
||||
{
|
||||
// draw total bounds
|
||||
Gizmos.color = historyColor;
|
||||
Gizmos.DrawWireCube(history.total.center, history.total.size);
|
||||
|
||||
// draw current bounds
|
||||
Gizmos.color = currentColor;
|
||||
Gizmos.DrawWireCube(actualCollider.bounds.center, actualCollider.bounds.size);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f5f2158d9776d4b569858f793be4da60
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
197
Assets/Mirror/Components/LagCompensation/LagCompensator.cs
Normal file
197
Assets/Mirror/Components/LagCompensation/LagCompensator.cs
Normal file
@ -0,0 +1,197 @@
|
||||
// Add this component to a Player object with collider.
|
||||
// Automatically keeps a history for lag compensation.
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
public struct Capture3D : Capture
|
||||
{
|
||||
public double timestamp { get; set; }
|
||||
public Vector3 position;
|
||||
public Vector3 size;
|
||||
|
||||
public Capture3D(double timestamp, Vector3 position, Vector3 size)
|
||||
{
|
||||
this.timestamp = timestamp;
|
||||
this.position = position;
|
||||
this.size = size;
|
||||
}
|
||||
|
||||
public void DrawGizmo()
|
||||
{
|
||||
Gizmos.DrawWireCube(position, size);
|
||||
}
|
||||
|
||||
public static Capture3D Interpolate(Capture3D from, Capture3D to, double t) =>
|
||||
new Capture3D(
|
||||
0, // interpolated snapshot is applied directly. don't need timestamps.
|
||||
Vector3.LerpUnclamped(from.position, to.position, (float)t),
|
||||
Vector3.LerpUnclamped(from.size, to.size, (float)t)
|
||||
);
|
||||
|
||||
public override string ToString() => $"(time={timestamp} pos={position} size={size})";
|
||||
}
|
||||
|
||||
[DisallowMultipleComponent]
|
||||
[AddComponentMenu("Network/ Lag Compensation/ Lag Compensator")]
|
||||
[HelpURL("https://mirror-networking.gitbook.io/docs/manual/general/lag-compensation")]
|
||||
public class LagCompensator : NetworkBehaviour
|
||||
{
|
||||
[Header("Components")]
|
||||
[Tooltip("The collider to keep a history of.")]
|
||||
public Collider trackedCollider; // assign this in inspector
|
||||
|
||||
[Header("Settings")]
|
||||
public LagCompensationSettings lagCompensationSettings = new LagCompensationSettings();
|
||||
double lastCaptureTime;
|
||||
|
||||
// lag compensation history of <timestamp, capture>
|
||||
readonly Queue<KeyValuePair<double, Capture3D>> history = new Queue<KeyValuePair<double, Capture3D>>();
|
||||
|
||||
[Header("Debugging")]
|
||||
public Color historyColor = Color.white;
|
||||
|
||||
[ServerCallback]
|
||||
protected virtual void Update()
|
||||
{
|
||||
// capture lag compensation snapshots every interval.
|
||||
// NetworkTime.localTime because Unity 2019 doesn't have 'double' time yet.
|
||||
if (NetworkTime.localTime >= lastCaptureTime + lagCompensationSettings.captureInterval)
|
||||
{
|
||||
lastCaptureTime = NetworkTime.localTime;
|
||||
Capture();
|
||||
}
|
||||
}
|
||||
|
||||
[ServerCallback]
|
||||
protected virtual void Capture()
|
||||
{
|
||||
// capture current state
|
||||
Capture3D capture = new Capture3D(
|
||||
NetworkTime.localTime,
|
||||
trackedCollider.bounds.center,
|
||||
trackedCollider.bounds.size
|
||||
);
|
||||
|
||||
// insert into history
|
||||
LagCompensation.Insert(history, lagCompensationSettings.historyLimit, NetworkTime.localTime, capture);
|
||||
}
|
||||
|
||||
protected virtual void OnDrawGizmos()
|
||||
{
|
||||
// draw history
|
||||
Gizmos.color = historyColor;
|
||||
LagCompensation.DrawGizmos(history);
|
||||
}
|
||||
|
||||
// sampling ////////////////////////////////////////////////////////////
|
||||
// sample the sub-tick (=interpolated) history of this object for a hit test.
|
||||
// 'viewer' needs to be the player who fired!
|
||||
// for example, if A fires at B, then call B.Sample(viewer, point, tolerance).
|
||||
[ServerCallback]
|
||||
public virtual bool Sample(NetworkConnectionToClient viewer, out Capture3D sample)
|
||||
{
|
||||
// never trust the client: estimate client time instead.
|
||||
// https://developer.valvesoftware.com/wiki/Source_Multiplayer_Networking
|
||||
// the estimation is very good. the error is as low as ~6ms for the demo.
|
||||
// note that passing 'rtt' is fine: EstimateTime halves it to latency.
|
||||
double estimatedTime = LagCompensation.EstimateTime(NetworkTime.localTime, viewer.rtt, NetworkClient.bufferTime);
|
||||
|
||||
// sample the history to get the nearest snapshots around 'timestamp'
|
||||
if (LagCompensation.Sample(history, estimatedTime, lagCompensationSettings.captureInterval, out Capture3D resultBefore, out Capture3D resultAfter, out double t))
|
||||
{
|
||||
// interpolate to get a decent estimation at exactly 'timestamp'
|
||||
sample = Capture3D.Interpolate(resultBefore, resultAfter, t);
|
||||
return true;
|
||||
}
|
||||
else Debug.Log($"CmdClicked: history doesn't contain {estimatedTime:F3}");
|
||||
|
||||
sample = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
// convenience tests ///////////////////////////////////////////////////
|
||||
// there are multiple different ways to check a hit against the sample:
|
||||
// - raycasting
|
||||
// - bounds.contains
|
||||
// - increasing bounds by tolerance and checking contains
|
||||
// - threshold to bounds.closestpoint
|
||||
// let's offer a few solutions directly and see which users prefer.
|
||||
|
||||
// bounds check: checks distance to closest point on bounds in history @ -rtt.
|
||||
// 'viewer' needs to be the player who fired!
|
||||
// for example, if A fires at B, then call B.Sample(viewer, point, tolerance).
|
||||
// this is super simple and fast, but not 100% physically accurate since we don't raycast.
|
||||
[ServerCallback]
|
||||
public virtual bool BoundsCheck(
|
||||
NetworkConnectionToClient viewer,
|
||||
Vector3 hitPoint,
|
||||
float toleranceDistance,
|
||||
out float distance,
|
||||
out Vector3 nearest)
|
||||
{
|
||||
// first, sample the history at -rtt of the viewer.
|
||||
if (Sample(viewer, out Capture3D capture))
|
||||
{
|
||||
// now that we know where the other player was at that time,
|
||||
// we can see if the hit point was within tolerance of it.
|
||||
// TODO consider rotations???
|
||||
// TODO consider original collider shape??
|
||||
Bounds bounds = new Bounds(capture.position, capture.size);
|
||||
nearest = bounds.ClosestPoint(hitPoint);
|
||||
distance = Vector3.Distance(nearest, hitPoint);
|
||||
return distance <= toleranceDistance;
|
||||
}
|
||||
nearest = hitPoint;
|
||||
distance = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
// raycast check: creates a collider the sampled position and raycasts to hitPoint.
|
||||
// 'viewer' needs to be the player who fired!
|
||||
// for example, if A fires at B, then call B.Sample(viewer, point, tolerance).
|
||||
// this is physically accurate (checks against walls etc.), with the cost
|
||||
// of a runtime instantiation.
|
||||
//
|
||||
// originPoint: where the player fired the weapon.
|
||||
// hitPoint: where the player's local raycast hit.
|
||||
// tolerance: scale up the sampled collider by % in order to have a bit of a tolerance.
|
||||
// 0 means no extra tolerance, 0.05 means 5% extra tolerance.
|
||||
// layerMask: the layer mask to use for the raycast.
|
||||
[ServerCallback]
|
||||
public virtual bool RaycastCheck(
|
||||
NetworkConnectionToClient viewer,
|
||||
Vector3 originPoint,
|
||||
Vector3 hitPoint,
|
||||
float tolerancePercent,
|
||||
int layerMask,
|
||||
out RaycastHit hit)
|
||||
{
|
||||
// first, sample the history at -rtt of the viewer.
|
||||
if (Sample(viewer, out Capture3D capture))
|
||||
{
|
||||
// instantiate a real physics collider on demand.
|
||||
// TODO rotation??
|
||||
// TODO different collier types??
|
||||
GameObject temp = new GameObject("LagCompensatorTest");
|
||||
temp.transform.position = capture.position;
|
||||
BoxCollider tempCollider = temp.AddComponent<BoxCollider>();
|
||||
tempCollider.size = capture.size * (1 + tolerancePercent);
|
||||
|
||||
// raycast
|
||||
Vector3 direction = hitPoint - originPoint;
|
||||
float maxDistance = direction.magnitude * 2;
|
||||
bool result = Physics.Raycast(originPoint, direction, out hit, maxDistance, layerMask);
|
||||
|
||||
// cleanup
|
||||
Destroy(temp);
|
||||
return result;
|
||||
}
|
||||
|
||||
hit = default;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a898831dd60c4cdfbfd9a6ea5702ed01
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -13,8 +13,8 @@ namespace Mirror
|
||||
/// <para>If the object has authority on the server, then it should be animated on the server and state information will be sent to all clients. This is common for objects not related to a specific client, such as an enemy unit.</para>
|
||||
/// <para>The NetworkAnimator synchronizes all animation parameters of the selected Animator. It does not automatically synchronize triggers. The function SetTrigger can by used by an object with authority to fire an animation trigger on other clients.</para>
|
||||
/// </remarks>
|
||||
// [RequireComponent(typeof(NetworkIdentity))] disabled to allow child NetworkBehaviours
|
||||
[AddComponentMenu("Network/Network Animator")]
|
||||
[RequireComponent(typeof(NetworkIdentity))]
|
||||
[HelpURL("https://mirror-networking.gitbook.io/docs/components/network-animator")]
|
||||
public class NetworkAnimator : NetworkBehaviour
|
||||
{
|
||||
@ -31,11 +31,12 @@ public class NetworkAnimator : NetworkBehaviour
|
||||
public Animator animator;
|
||||
|
||||
/// <summary>
|
||||
/// Syncs animator.speed
|
||||
/// Syncs animator.speed.
|
||||
/// Default to 1 because Animator.speed defaults to 1.
|
||||
/// </summary>
|
||||
[SyncVar(hook = nameof(OnAnimatorSpeedChanged))]
|
||||
float animatorSpeed;
|
||||
float previousSpeed;
|
||||
float animatorSpeed = 1f;
|
||||
float previousSpeed = 1f;
|
||||
|
||||
// Note: not an object[] array because otherwise initialization is real annoying
|
||||
int[] lastIntParameters;
|
||||
@ -71,7 +72,7 @@ bool SendMessagesAllowed
|
||||
}
|
||||
}
|
||||
|
||||
void Awake()
|
||||
void Initialize()
|
||||
{
|
||||
// store the animator parameters in a variable - the "Animator.parameters" getter allocates
|
||||
// a new parameter array every time it is accessed so we should avoid doing it in a loop
|
||||
@ -87,6 +88,17 @@ void Awake()
|
||||
layerWeight = new float[animator.layerCount];
|
||||
}
|
||||
|
||||
// fix https://github.com/MirrorNetworking/Mirror/issues/2810
|
||||
// both Awake and Enable need to initialize arrays.
|
||||
// in case users call SetActive(false) -> SetActive(true).
|
||||
void Awake() => Initialize();
|
||||
void OnEnable() => Initialize();
|
||||
|
||||
public virtual void Reset()
|
||||
{
|
||||
syncDirection = SyncDirection.ClientToServer;
|
||||
}
|
||||
|
||||
void FixedUpdate()
|
||||
{
|
||||
if (!SendMessagesAllowed)
|
||||
@ -302,9 +314,18 @@ ulong NextDirtyBits()
|
||||
|
||||
bool WriteParameters(NetworkWriter writer, bool forceAll = false)
|
||||
{
|
||||
// fix: https://github.com/MirrorNetworking/Mirror/issues/2852
|
||||
// serialize parameterCount to be 100% sure we deserialize correct amount of bytes.
|
||||
// (255 parameters should be enough for everyone, write it as byte)
|
||||
byte parameterCount = (byte)parameters.Length;
|
||||
writer.WriteByte(parameterCount);
|
||||
|
||||
ulong dirtyBits = forceAll ? (~0ul) : NextDirtyBits();
|
||||
writer.WriteULong(dirtyBits);
|
||||
for (int i = 0; i < parameters.Length; i++)
|
||||
|
||||
// iterate on byte count. if it's >256, it won't break
|
||||
// serialization - just not serialize excess layers.
|
||||
for (int i = 0; i < parameterCount; i++)
|
||||
{
|
||||
if ((dirtyBits & (1ul << i)) == 0)
|
||||
continue;
|
||||
@ -331,11 +352,20 @@ bool WriteParameters(NetworkWriter writer, bool forceAll = false)
|
||||
|
||||
void ReadParameters(NetworkReader reader)
|
||||
{
|
||||
// fix: https://github.com/MirrorNetworking/Mirror/issues/2852
|
||||
// serialize parameterCount to be 100% sure we deserialize correct amount of bytes.
|
||||
// mismatch shows error to make this super easy to debug.
|
||||
byte parameterCount = reader.ReadByte();
|
||||
if (parameterCount != parameters.Length)
|
||||
{
|
||||
Debug.LogError($"NetworkAnimator: serialized parameter count={parameterCount} does not match expected parameter count={parameters.Length}. Are you changing animators at runtime?", gameObject);
|
||||
return;
|
||||
}
|
||||
|
||||
bool animatorEnabled = animator.enabled;
|
||||
// need to read values from NetworkReader even if animator is disabled
|
||||
|
||||
ulong dirtyBits = reader.ReadULong();
|
||||
for (int i = 0; i < parameters.Length; i++)
|
||||
for (int i = 0; i < parameterCount; i++)
|
||||
{
|
||||
if ((dirtyBits & (1ul << i)) == 0)
|
||||
continue;
|
||||
@ -367,23 +397,24 @@ public override void OnSerialize(NetworkWriter writer, bool initialState)
|
||||
base.OnSerialize(writer, initialState);
|
||||
if (initialState)
|
||||
{
|
||||
for (int i = 0; i < animator.layerCount; i++)
|
||||
// fix: https://github.com/MirrorNetworking/Mirror/issues/2852
|
||||
// serialize layerCount to be 100% sure we deserialize correct amount of bytes.
|
||||
// (255 layers should be enough for everyone, write it as byte)
|
||||
byte layerCount = (byte)animator.layerCount;
|
||||
writer.WriteByte(layerCount);
|
||||
|
||||
// iterate on byte count. if it's >256, it won't break
|
||||
// serialization - just not serialize excess layers.
|
||||
for (int i = 0; i < layerCount; i++)
|
||||
{
|
||||
if (animator.IsInTransition(i))
|
||||
{
|
||||
AnimatorStateInfo st = animator.GetNextAnimatorStateInfo(i);
|
||||
AnimatorStateInfo st = animator.IsInTransition(i)
|
||||
? animator.GetNextAnimatorStateInfo(i)
|
||||
: animator.GetCurrentAnimatorStateInfo(i);
|
||||
writer.WriteInt(st.fullPathHash);
|
||||
writer.WriteFloat(st.normalizedTime);
|
||||
}
|
||||
else
|
||||
{
|
||||
AnimatorStateInfo st = animator.GetCurrentAnimatorStateInfo(i);
|
||||
writer.WriteInt(st.fullPathHash);
|
||||
writer.WriteFloat(st.normalizedTime);
|
||||
}
|
||||
writer.WriteFloat(animator.GetLayerWeight(i));
|
||||
}
|
||||
WriteParameters(writer, initialState);
|
||||
WriteParameters(writer, true);
|
||||
}
|
||||
}
|
||||
|
||||
@ -392,11 +423,23 @@ public override void OnDeserialize(NetworkReader reader, bool initialState)
|
||||
base.OnDeserialize(reader, initialState);
|
||||
if (initialState)
|
||||
{
|
||||
for (int i = 0; i < animator.layerCount; i++)
|
||||
// fix: https://github.com/MirrorNetworking/Mirror/issues/2852
|
||||
// serialize layerCount to be 100% sure we deserialize correct amount of bytes.
|
||||
// mismatch shows error to make this super easy to debug.
|
||||
byte layerCount = reader.ReadByte();
|
||||
if (layerCount != animator.layerCount)
|
||||
{
|
||||
Debug.LogError($"NetworkAnimator: serialized layer count={layerCount} does not match expected layer count={animator.layerCount}. Are you changing animators at runtime?", gameObject);
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < layerCount; i++)
|
||||
{
|
||||
int stateHash = reader.ReadInt();
|
||||
float normalizedTime = reader.ReadFloat();
|
||||
animator.SetLayerWeight(i, reader.ReadFloat());
|
||||
float weight = reader.ReadFloat();
|
||||
|
||||
animator.SetLayerWeight(i, weight);
|
||||
animator.Play(stateHash, i, normalizedTime);
|
||||
}
|
||||
|
||||
@ -424,13 +467,13 @@ public void SetTrigger(int hash)
|
||||
{
|
||||
if (!isClient)
|
||||
{
|
||||
Debug.LogWarning("Tried to set animation in the server for a client-controlled animator");
|
||||
Debug.LogWarning("Tried to set animation in the server for a client-controlled animator", gameObject);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isOwned)
|
||||
{
|
||||
Debug.LogWarning("Only the client with authority can set animations");
|
||||
Debug.LogWarning("Only the client with authority can set animations", gameObject);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -444,7 +487,7 @@ public void SetTrigger(int hash)
|
||||
{
|
||||
if (!isServer)
|
||||
{
|
||||
Debug.LogWarning("Tried to set animation in the client for a server-controlled animator");
|
||||
Debug.LogWarning("Tried to set animation in the client for a server-controlled animator", gameObject);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -471,13 +514,13 @@ public void ResetTrigger(int hash)
|
||||
{
|
||||
if (!isClient)
|
||||
{
|
||||
Debug.LogWarning("Tried to reset animation in the server for a client-controlled animator");
|
||||
Debug.LogWarning("Tried to reset animation in the server for a client-controlled animator", gameObject);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isOwned)
|
||||
{
|
||||
Debug.LogWarning("Only the client with authority can reset animations");
|
||||
Debug.LogWarning("Only the client with authority can reset animations", gameObject);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -491,7 +534,7 @@ public void ResetTrigger(int hash)
|
||||
{
|
||||
if (!isServer)
|
||||
{
|
||||
Debug.LogWarning("Tried to reset animation in the client for a server-controlled animator");
|
||||
Debug.LogWarning("Tried to reset animation in the client for a server-controlled animator", gameObject);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -596,21 +639,15 @@ void RpcOnAnimationParametersClientMessage(byte[] parameters)
|
||||
HandleAnimParamsMsg(networkReader);
|
||||
}
|
||||
|
||||
[ClientRpc]
|
||||
[ClientRpc(includeOwner = false)]
|
||||
void RpcOnAnimationTriggerClientMessage(int hash)
|
||||
{
|
||||
// host/owner handles this before it is sent
|
||||
if (isServer || (clientAuthority && isOwned)) return;
|
||||
|
||||
HandleAnimTriggerMsg(hash);
|
||||
}
|
||||
|
||||
[ClientRpc]
|
||||
[ClientRpc(includeOwner = false)]
|
||||
void RpcOnAnimationResetTriggerClientMessage(int hash)
|
||||
{
|
||||
// host/owner handles this before it is sent
|
||||
if (isServer || (clientAuthority && isOwned)) return;
|
||||
|
||||
HandleAnimResetTriggerMsg(hash);
|
||||
}
|
||||
|
||||
|
31
Assets/Mirror/Components/NetworkDiagnosticsDebugger.cs
Normal file
31
Assets/Mirror/Components/NetworkDiagnosticsDebugger.cs
Normal file
@ -0,0 +1,31 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
[AddComponentMenu("Network/Network Diagnostics Debugger")]
|
||||
public class NetworkDiagnosticsDebugger : MonoBehaviour
|
||||
{
|
||||
public bool logInMessages = true;
|
||||
public bool logOutMessages = true;
|
||||
void OnInMessage(NetworkDiagnostics.MessageInfo msgInfo)
|
||||
{
|
||||
if (logInMessages)
|
||||
Debug.Log(msgInfo);
|
||||
}
|
||||
void OnOutMessage(NetworkDiagnostics.MessageInfo msgInfo)
|
||||
{
|
||||
if (logOutMessages)
|
||||
Debug.Log(msgInfo);
|
||||
}
|
||||
void OnEnable()
|
||||
{
|
||||
NetworkDiagnostics.InMessageEvent += OnInMessage;
|
||||
NetworkDiagnostics.OutMessageEvent += OnOutMessage;
|
||||
}
|
||||
void OnDisable()
|
||||
{
|
||||
NetworkDiagnostics.InMessageEvent -= OnInMessage;
|
||||
NetworkDiagnostics.OutMessageEvent -= OnOutMessage;
|
||||
}
|
||||
}
|
||||
}
|
11
Assets/Mirror/Components/NetworkDiagnosticsDebugger.cs.meta
Normal file
11
Assets/Mirror/Components/NetworkDiagnosticsDebugger.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bc9f0a0fe4124424b8f9d4927795ee01
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -21,12 +21,18 @@ void OnGUI()
|
||||
// only while client is active
|
||||
if (!NetworkClient.active) return;
|
||||
|
||||
// show rtt in bottom right corner, right aligned
|
||||
// show stats in bottom right corner, right aligned
|
||||
GUI.color = color;
|
||||
Rect rect = new Rect(Screen.width - width - padding, Screen.height - height - padding, width, height);
|
||||
GUILayout.BeginArea(rect);
|
||||
GUIStyle style = GUI.skin.GetStyle("Label");
|
||||
style.alignment = TextAnchor.MiddleRight;
|
||||
GUI.Label(rect, $"RTT: {Math.Round(NetworkTime.rtt * 1000)}ms", style);
|
||||
GUILayout.BeginHorizontal(style);
|
||||
GUILayout.Label($"RTT: {Math.Round(NetworkTime.rtt * 1000)}ms");
|
||||
GUI.color = NetworkClient.connectionQuality.ColorCode();
|
||||
GUILayout.Label($"Q: {new string('-', (int)NetworkClient.connectionQuality)}");
|
||||
GUILayout.EndHorizontal();
|
||||
GUILayout.EndArea();
|
||||
GUI.color = Color.white;
|
||||
}
|
||||
}
|
||||
|
3
Assets/Mirror/Components/NetworkRigidbody.meta
Normal file
3
Assets/Mirror/Components/NetworkRigidbody.meta
Normal file
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 80106690aef541a5b8e2f8fb3d5949ad
|
||||
timeCreated: 1686733778
|
@ -0,0 +1,112 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
// [RequireComponent(typeof(Rigidbody))] <- OnValidate ensures this is on .target
|
||||
[AddComponentMenu("Network/Network Rigidbody (Reliable)")]
|
||||
public class NetworkRigidbodyReliable : NetworkTransformReliable
|
||||
{
|
||||
bool clientAuthority => syncDirection == SyncDirection.ClientToServer;
|
||||
|
||||
Rigidbody rb;
|
||||
bool wasKinematic;
|
||||
|
||||
// cach Rigidbody and original isKinematic setting
|
||||
protected override void Awake()
|
||||
{
|
||||
// we can't overwrite .target to be a Rigidbody.
|
||||
// but we can use its Rigidbody component.
|
||||
rb = target.GetComponent<Rigidbody>();
|
||||
if (rb == null)
|
||||
{
|
||||
Debug.LogError($"{name}'s NetworkRigidbody.target {target.name} is missing a Rigidbody", this);
|
||||
return;
|
||||
}
|
||||
wasKinematic = rb.isKinematic;
|
||||
base.Awake();
|
||||
}
|
||||
|
||||
// reset forced isKinematic flag to original.
|
||||
// otherwise the overwritten value would remain between sessions forever.
|
||||
// for example, a game may run as client, set rigidbody.iskinematic=true,
|
||||
// then run as server, where .iskinematic isn't touched and remains at
|
||||
// the overwritten=true, even though the user set it to false originally.
|
||||
public override void OnStopServer() => rb.isKinematic = wasKinematic;
|
||||
public override void OnStopClient() => rb.isKinematic = wasKinematic;
|
||||
|
||||
// overwriting Construct() and Apply() to set Rigidbody.MovePosition
|
||||
// would give more jittery movement.
|
||||
|
||||
// FixedUpdate for physics
|
||||
void FixedUpdate()
|
||||
{
|
||||
// who ever has authority moves the Rigidbody with physics.
|
||||
// everyone else simply sets it to kinematic.
|
||||
// so that only the Transform component is synced.
|
||||
|
||||
// host mode
|
||||
if (isServer && isClient)
|
||||
{
|
||||
// in host mode, we own it it if:
|
||||
// clientAuthority is disabled (hence server / we own it)
|
||||
// clientAuthority is enabled and we have authority over this object.
|
||||
bool owned = !clientAuthority || IsClientWithAuthority;
|
||||
|
||||
// only set to kinematic if we don't own it
|
||||
// otherwise don't touch isKinematic.
|
||||
// the authority owner might use it either way.
|
||||
if (!owned) rb.isKinematic = true;
|
||||
}
|
||||
// client only
|
||||
else if (isClient)
|
||||
{
|
||||
// on the client, we own it only if clientAuthority is enabled,
|
||||
// and we have authority over this object.
|
||||
bool owned = IsClientWithAuthority;
|
||||
|
||||
// only set to kinematic if we don't own it
|
||||
// otherwise don't touch isKinematic.
|
||||
// the authority owner might use it either way.
|
||||
if (!owned) rb.isKinematic = true;
|
||||
}
|
||||
// server only
|
||||
else if (isServer)
|
||||
{
|
||||
// on the server, we always own it if clientAuthority is disabled.
|
||||
bool owned = !clientAuthority;
|
||||
|
||||
// only set to kinematic if we don't own it
|
||||
// otherwise don't touch isKinematic.
|
||||
// the authority owner might use it either way.
|
||||
if (!owned) rb.isKinematic = true;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnValidate()
|
||||
{
|
||||
base.OnValidate();
|
||||
|
||||
// we can't overwrite .target to be a Rigidbody.
|
||||
// but we can ensure that .target has a Rigidbody, and use it.
|
||||
if (target.GetComponent<Rigidbody>() == null)
|
||||
{
|
||||
Debug.LogWarning($"{name}'s NetworkRigidbody.target {target.name} is missing a Rigidbody", this);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnTeleport(Vector3 destination)
|
||||
{
|
||||
base.OnTeleport(destination);
|
||||
|
||||
rb.position = transform.position;
|
||||
}
|
||||
|
||||
protected override void OnTeleport(Vector3 destination, Quaternion rotation)
|
||||
{
|
||||
base.OnTeleport(destination, rotation);
|
||||
|
||||
rb.position = transform.position;
|
||||
rb.rotation = transform.rotation;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cb803efbe62c34d7baece46c9ffebad9
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -0,0 +1,112 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
// [RequireComponent(typeof(Rigidbody))] <- OnValidate ensures this is on .target
|
||||
[AddComponentMenu("Network/Network Rigidbody 2D (Reliable)")]
|
||||
public class NetworkRigidbodyReliable2D : NetworkTransformReliable
|
||||
{
|
||||
bool clientAuthority => syncDirection == SyncDirection.ClientToServer;
|
||||
|
||||
Rigidbody2D rb;
|
||||
bool wasKinematic;
|
||||
|
||||
// cach Rigidbody and original isKinematic setting
|
||||
protected override void Awake()
|
||||
{
|
||||
// we can't overwrite .target to be a Rigidbody.
|
||||
// but we can use its Rigidbody component.
|
||||
rb = target.GetComponent<Rigidbody2D>();
|
||||
if (rb == null)
|
||||
{
|
||||
Debug.LogError($"{name}'s NetworkRigidbody2D.target {target.name} is missing a Rigidbody2D", this);
|
||||
return;
|
||||
}
|
||||
wasKinematic = rb.isKinematic;
|
||||
base.Awake();
|
||||
}
|
||||
|
||||
// reset forced isKinematic flag to original.
|
||||
// otherwise the overwritten value would remain between sessions forever.
|
||||
// for example, a game may run as client, set rigidbody.iskinematic=true,
|
||||
// then run as server, where .iskinematic isn't touched and remains at
|
||||
// the overwritten=true, even though the user set it to false originally.
|
||||
public override void OnStopServer() => rb.isKinematic = wasKinematic;
|
||||
public override void OnStopClient() => rb.isKinematic = wasKinematic;
|
||||
|
||||
// overwriting Construct() and Apply() to set Rigidbody.MovePosition
|
||||
// would give more jittery movement.
|
||||
|
||||
// FixedUpdate for physics
|
||||
void FixedUpdate()
|
||||
{
|
||||
// who ever has authority moves the Rigidbody with physics.
|
||||
// everyone else simply sets it to kinematic.
|
||||
// so that only the Transform component is synced.
|
||||
|
||||
// host mode
|
||||
if (isServer && isClient)
|
||||
{
|
||||
// in host mode, we own it it if:
|
||||
// clientAuthority is disabled (hence server / we own it)
|
||||
// clientAuthority is enabled and we have authority over this object.
|
||||
bool owned = !clientAuthority || IsClientWithAuthority;
|
||||
|
||||
// only set to kinematic if we don't own it
|
||||
// otherwise don't touch isKinematic.
|
||||
// the authority owner might use it either way.
|
||||
if (!owned) rb.isKinematic = true;
|
||||
}
|
||||
// client only
|
||||
else if (isClient)
|
||||
{
|
||||
// on the client, we own it only if clientAuthority is enabled,
|
||||
// and we have authority over this object.
|
||||
bool owned = IsClientWithAuthority;
|
||||
|
||||
// only set to kinematic if we don't own it
|
||||
// otherwise don't touch isKinematic.
|
||||
// the authority owner might use it either way.
|
||||
if (!owned) rb.isKinematic = true;
|
||||
}
|
||||
// server only
|
||||
else if (isServer)
|
||||
{
|
||||
// on the server, we always own it if clientAuthority is disabled.
|
||||
bool owned = !clientAuthority;
|
||||
|
||||
// only set to kinematic if we don't own it
|
||||
// otherwise don't touch isKinematic.
|
||||
// the authority owner might use it either way.
|
||||
if (!owned) rb.isKinematic = true;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnValidate()
|
||||
{
|
||||
base.OnValidate();
|
||||
|
||||
// we can't overwrite .target to be a Rigidbody.
|
||||
// but we can ensure that .target has a Rigidbody, and use it.
|
||||
if (target.GetComponent<Rigidbody2D>() == null)
|
||||
{
|
||||
Debug.LogWarning($"{name}'s NetworkRigidbody2D.target {target.name} is missing a Rigidbody2D", this);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnTeleport(Vector3 destination)
|
||||
{
|
||||
base.OnTeleport(destination);
|
||||
|
||||
rb.position = transform.position;
|
||||
}
|
||||
|
||||
protected override void OnTeleport(Vector3 destination, Quaternion rotation)
|
||||
{
|
||||
base.OnTeleport(destination, rotation);
|
||||
|
||||
rb.position = transform.position;
|
||||
rb.rotation = transform.rotation.eulerAngles.z;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7ec4f7556ca1e4b55a3381fc6a02b1bc
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -0,0 +1,112 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
// [RequireComponent(typeof(Rigidbody))] <- OnValidate ensures this is on .target
|
||||
[AddComponentMenu("Network/Network Rigidbody (Unreliable)")]
|
||||
public class NetworkRigidbodyUnreliable : NetworkTransformUnreliable
|
||||
{
|
||||
bool clientAuthority => syncDirection == SyncDirection.ClientToServer;
|
||||
|
||||
Rigidbody rb;
|
||||
bool wasKinematic;
|
||||
|
||||
// cach Rigidbody and original isKinematic setting
|
||||
protected override void Awake()
|
||||
{
|
||||
// we can't overwrite .target to be a Rigidbody.
|
||||
// but we can use its Rigidbody component.
|
||||
rb = target.GetComponent<Rigidbody>();
|
||||
if (rb == null)
|
||||
{
|
||||
Debug.LogError($"{name}'s NetworkRigidbody.target {target.name} is missing a Rigidbody", this);
|
||||
return;
|
||||
}
|
||||
wasKinematic = rb.isKinematic;
|
||||
base.Awake();
|
||||
}
|
||||
|
||||
// reset forced isKinematic flag to original.
|
||||
// otherwise the overwritten value would remain between sessions forever.
|
||||
// for example, a game may run as client, set rigidbody.iskinematic=true,
|
||||
// then run as server, where .iskinematic isn't touched and remains at
|
||||
// the overwritten=true, even though the user set it to false originally.
|
||||
public override void OnStopServer() => rb.isKinematic = wasKinematic;
|
||||
public override void OnStopClient() => rb.isKinematic = wasKinematic;
|
||||
|
||||
// overwriting Construct() and Apply() to set Rigidbody.MovePosition
|
||||
// would give more jittery movement.
|
||||
|
||||
// FixedUpdate for physics
|
||||
void FixedUpdate()
|
||||
{
|
||||
// who ever has authority moves the Rigidbody with physics.
|
||||
// everyone else simply sets it to kinematic.
|
||||
// so that only the Transform component is synced.
|
||||
|
||||
// host mode
|
||||
if (isServer && isClient)
|
||||
{
|
||||
// in host mode, we own it it if:
|
||||
// clientAuthority is disabled (hence server / we own it)
|
||||
// clientAuthority is enabled and we have authority over this object.
|
||||
bool owned = !clientAuthority || IsClientWithAuthority;
|
||||
|
||||
// only set to kinematic if we don't own it
|
||||
// otherwise don't touch isKinematic.
|
||||
// the authority owner might use it either way.
|
||||
if (!owned) rb.isKinematic = true;
|
||||
}
|
||||
// client only
|
||||
else if (isClient)
|
||||
{
|
||||
// on the client, we own it only if clientAuthority is enabled,
|
||||
// and we have authority over this object.
|
||||
bool owned = IsClientWithAuthority;
|
||||
|
||||
// only set to kinematic if we don't own it
|
||||
// otherwise don't touch isKinematic.
|
||||
// the authority owner might use it either way.
|
||||
if (!owned) rb.isKinematic = true;
|
||||
}
|
||||
// server only
|
||||
else if (isServer)
|
||||
{
|
||||
// on the server, we always own it if clientAuthority is disabled.
|
||||
bool owned = !clientAuthority;
|
||||
|
||||
// only set to kinematic if we don't own it
|
||||
// otherwise don't touch isKinematic.
|
||||
// the authority owner might use it either way.
|
||||
if (!owned) rb.isKinematic = true;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnValidate()
|
||||
{
|
||||
base.OnValidate();
|
||||
|
||||
// we can't overwrite .target to be a Rigidbody.
|
||||
// but we can ensure that .target has a Rigidbody, and use it.
|
||||
if (target.GetComponent<Rigidbody>() == null)
|
||||
{
|
||||
Debug.LogWarning($"{name}'s NetworkRigidbody.target {target.name} is missing a Rigidbody", this);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnTeleport(Vector3 destination)
|
||||
{
|
||||
base.OnTeleport(destination);
|
||||
|
||||
rb.position = transform.position;
|
||||
}
|
||||
|
||||
protected override void OnTeleport(Vector3 destination, Quaternion rotation)
|
||||
{
|
||||
base.OnTeleport(destination, rotation);
|
||||
|
||||
rb.position = transform.position;
|
||||
rb.rotation = transform.rotation;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3b20dc110904e47f8a154cdcf6433eae
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -0,0 +1,112 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
// [RequireComponent(typeof(Rigidbody))] <- OnValidate ensures this is on .target
|
||||
[AddComponentMenu("Network/Network Rigidbody 2D (Unreliable)")]
|
||||
public class NetworkRigidbodyUnreliable2D : NetworkTransformUnreliable
|
||||
{
|
||||
bool clientAuthority => syncDirection == SyncDirection.ClientToServer;
|
||||
|
||||
Rigidbody2D rb;
|
||||
bool wasKinematic;
|
||||
|
||||
// cach Rigidbody and original isKinematic setting
|
||||
protected override void Awake()
|
||||
{
|
||||
// we can't overwrite .target to be a Rigidbody.
|
||||
// but we can use its Rigidbody component.
|
||||
rb = target.GetComponent<Rigidbody2D>();
|
||||
if (rb == null)
|
||||
{
|
||||
Debug.LogError($"{name}'s NetworkRigidbody2D.target {target.name} is missing a Rigidbody2D", this);
|
||||
return;
|
||||
}
|
||||
wasKinematic = rb.isKinematic;
|
||||
base.Awake();
|
||||
}
|
||||
|
||||
// reset forced isKinematic flag to original.
|
||||
// otherwise the overwritten value would remain between sessions forever.
|
||||
// for example, a game may run as client, set rigidbody.iskinematic=true,
|
||||
// then run as server, where .iskinematic isn't touched and remains at
|
||||
// the overwritten=true, even though the user set it to false originally.
|
||||
public override void OnStopServer() => rb.isKinematic = wasKinematic;
|
||||
public override void OnStopClient() => rb.isKinematic = wasKinematic;
|
||||
|
||||
// overwriting Construct() and Apply() to set Rigidbody.MovePosition
|
||||
// would give more jittery movement.
|
||||
|
||||
// FixedUpdate for physics
|
||||
void FixedUpdate()
|
||||
{
|
||||
// who ever has authority moves the Rigidbody with physics.
|
||||
// everyone else simply sets it to kinematic.
|
||||
// so that only the Transform component is synced.
|
||||
|
||||
// host mode
|
||||
if (isServer && isClient)
|
||||
{
|
||||
// in host mode, we own it it if:
|
||||
// clientAuthority is disabled (hence server / we own it)
|
||||
// clientAuthority is enabled and we have authority over this object.
|
||||
bool owned = !clientAuthority || IsClientWithAuthority;
|
||||
|
||||
// only set to kinematic if we don't own it
|
||||
// otherwise don't touch isKinematic.
|
||||
// the authority owner might use it either way.
|
||||
if (!owned) rb.isKinematic = true;
|
||||
}
|
||||
// client only
|
||||
else if (isClient)
|
||||
{
|
||||
// on the client, we own it only if clientAuthority is enabled,
|
||||
// and we have authority over this object.
|
||||
bool owned = IsClientWithAuthority;
|
||||
|
||||
// only set to kinematic if we don't own it
|
||||
// otherwise don't touch isKinematic.
|
||||
// the authority owner might use it either way.
|
||||
if (!owned) rb.isKinematic = true;
|
||||
}
|
||||
// server only
|
||||
else if (isServer)
|
||||
{
|
||||
// on the server, we always own it if clientAuthority is disabled.
|
||||
bool owned = !clientAuthority;
|
||||
|
||||
// only set to kinematic if we don't own it
|
||||
// otherwise don't touch isKinematic.
|
||||
// the authority owner might use it either way.
|
||||
if (!owned) rb.isKinematic = true;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnValidate()
|
||||
{
|
||||
base.OnValidate();
|
||||
|
||||
// we can't overwrite .target to be a Rigidbody.
|
||||
// but we can ensure that .target has a Rigidbody, and use it.
|
||||
if (target.GetComponent<Rigidbody2D>() == null)
|
||||
{
|
||||
Debug.LogWarning($"{name}'s NetworkRigidbody2D.target {target.name} is missing a Rigidbody2D", this);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnTeleport(Vector3 destination)
|
||||
{
|
||||
base.OnTeleport(destination);
|
||||
|
||||
rb.position = transform.position;
|
||||
}
|
||||
|
||||
protected override void OnTeleport(Vector3 destination, Quaternion rotation)
|
||||
{
|
||||
base.OnTeleport(destination, rotation);
|
||||
|
||||
rb.position = transform.position;
|
||||
rb.rotation = transform.rotation.eulerAngles.z;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1c7e12ad9b9ae443c9fdf37e9f5ecd36
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -64,13 +64,13 @@ public struct PendingPlayer
|
||||
/// </summary>
|
||||
[Tooltip("Diagnostic flag indicating all players are ready to play")]
|
||||
[FormerlySerializedAs("allPlayersReady")]
|
||||
[SerializeField] bool _allPlayersReady;
|
||||
[ReadOnly, SerializeField] bool _allPlayersReady;
|
||||
|
||||
/// <summary>
|
||||
/// These slots track players that enter the room.
|
||||
/// <para>The slotId on players is global to the game - across all players.</para>
|
||||
/// </summary>
|
||||
[Tooltip("List of Room Player objects")]
|
||||
[ReadOnly, Tooltip("List of Room Player objects")]
|
||||
public List<NetworkRoomPlayer> roomSlots = new List<NetworkRoomPlayer>();
|
||||
|
||||
public bool allPlayersReady
|
||||
@ -120,7 +120,7 @@ public override void OnValidate()
|
||||
|
||||
void SceneLoadedForPlayer(NetworkConnectionToClient conn, GameObject roomPlayer)
|
||||
{
|
||||
Debug.Log($"NetworkRoom SceneLoadedForPlayer scene: {SceneManager.GetActiveScene().path} {conn}");
|
||||
//Debug.Log($"NetworkRoom SceneLoadedForPlayer scene: {SceneManager.GetActiveScene().path} {conn}");
|
||||
|
||||
if (Utils.IsSceneActive(RoomScene))
|
||||
{
|
||||
@ -251,10 +251,11 @@ public override void OnServerDisconnect(NetworkConnectionToClient conn)
|
||||
OnRoomServerDisconnect(conn);
|
||||
base.OnServerDisconnect(conn);
|
||||
|
||||
#if UNITY_SERVER
|
||||
if (Utils.IsHeadless())
|
||||
{
|
||||
if (numPlayers < 1)
|
||||
StopServer();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
// Sequential index used in round-robin deployment of players into instances and score positioning
|
||||
@ -267,7 +268,7 @@ public override void OnServerDisconnect(NetworkConnectionToClient conn)
|
||||
/// <param name="conn">Connection from client.</param>
|
||||
public override void OnServerReady(NetworkConnectionToClient conn)
|
||||
{
|
||||
Debug.Log($"NetworkRoomManager OnServerReady {conn}");
|
||||
//Debug.Log($"NetworkRoomManager OnServerReady {conn}");
|
||||
base.OnServerReady(conn);
|
||||
|
||||
if (conn != null && conn.identity != null)
|
||||
|
@ -25,14 +25,14 @@ public class NetworkRoomPlayer : NetworkBehaviour
|
||||
/// <para>Invoke CmdChangeReadyState method on the client to set this flag.</para>
|
||||
/// <para>When all players are ready to begin, the game will start. This should not be set directly, CmdChangeReadyState should be called on the client to set it on the server.</para>
|
||||
/// </summary>
|
||||
[Tooltip("Diagnostic flag indicating whether this player is ready for the game to begin")]
|
||||
[ReadOnly, Tooltip("Diagnostic flag indicating whether this player is ready for the game to begin")]
|
||||
[SyncVar(hook = nameof(ReadyStateChanged))]
|
||||
public bool readyToBegin;
|
||||
|
||||
/// <summary>
|
||||
/// Diagnostic index of the player, e.g. Player1, Player2, etc.
|
||||
/// </summary>
|
||||
[Tooltip("Diagnostic index of the player, e.g. Player1, Player2, etc.")]
|
||||
[ReadOnly, Tooltip("Diagnostic index of the player, e.g. Player1, Player2, etc.")]
|
||||
[SyncVar(hook = nameof(IndexChanged))]
|
||||
public int index;
|
||||
|
||||
@ -41,7 +41,7 @@ public class NetworkRoomPlayer : NetworkBehaviour
|
||||
/// <summary>
|
||||
/// Do not use Start - Override OnStartHost / OnStartClient instead!
|
||||
/// </summary>
|
||||
public void Start()
|
||||
public virtual void Start()
|
||||
{
|
||||
if (NetworkManager.singleton is NetworkRoomManager room)
|
||||
{
|
||||
|
@ -0,0 +1,10 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
// DEPRECATED 2023-06-15
|
||||
[AddComponentMenu("")]
|
||||
[Obsolete("NetworkTransform was renamed to NetworkTransformUnreliable.\nYou can easily swap the component's script by going into the Unity Inspector debug mode:\n1. Click the vertical dots on the top right in the Inspector tab.\n2. Find your NetworkTransform component\n3. Drag NetworkTransformUnreliable into the 'Script' field in the Inspector.\n4. Find the three dots and return to Normal mode.")]
|
||||
public class NetworkTransform : NetworkTransformUnreliable {}
|
||||
}
|
@ -22,24 +22,23 @@
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
public enum CoordinateSpace { Local, World }
|
||||
|
||||
public abstract class NetworkTransformBase : NetworkBehaviour
|
||||
{
|
||||
// target transform to sync. can be on a child.
|
||||
// TODO this field is kind of unnecessary since we now support child NetworkBehaviours
|
||||
[Header("Target")]
|
||||
[Tooltip("The Transform component to sync. May be on on this GameObject, or on a child.")]
|
||||
public Transform target;
|
||||
|
||||
// TODO SyncDirection { ClientToServer, ServerToClient } is easier?
|
||||
// Deprecated 2022-10-25
|
||||
[Obsolete("NetworkTransform clientAuthority was replaced with syncDirection. To enable client authority, set SyncDirection to ClientToServer in the Inspector.")]
|
||||
[Header("[Obsolete]")] // Unity doesn't show obsolete warning for fields. do it manually.
|
||||
[Tooltip("Obsolete: NetworkTransform clientAuthority was replaced with syncDirection. To enable client authority, set SyncDirection to ClientToServer in the Inspector.")]
|
||||
public bool clientAuthority;
|
||||
// Is this a client with authority over this transform?
|
||||
// This component could be on the player object or any object that has been assigned authority to this client.
|
||||
protected bool IsClientWithAuthority => isClient && authority;
|
||||
public readonly SortedList<double, TransformSnapshot> clientSnapshots = new SortedList<double, TransformSnapshot>();
|
||||
public readonly SortedList<double, TransformSnapshot> serverSnapshots = new SortedList<double, TransformSnapshot>();
|
||||
|
||||
// snapshots with initial capacity to avoid early resizing & allocations: see NetworkRigidbodyBenchmark example.
|
||||
public readonly SortedList<double, TransformSnapshot> clientSnapshots = new SortedList<double, TransformSnapshot>(16);
|
||||
public readonly SortedList<double, TransformSnapshot> serverSnapshots = new SortedList<double, TransformSnapshot>(16);
|
||||
|
||||
// selective sync //////////////////////////////////////////////////////
|
||||
[Header("Selective Sync\nDon't change these at Runtime")]
|
||||
@ -47,6 +46,12 @@ public abstract class NetworkTransformBase : NetworkBehaviour
|
||||
public bool syncRotation = true; // do not change at runtime!
|
||||
public bool syncScale = false; // do not change at runtime! rare. off by default.
|
||||
|
||||
[Header("Bandwidth Savings")]
|
||||
[Tooltip("When true, changes are not sent unless greater than sensitivity values below.")]
|
||||
public bool onlySyncOnChange = true;
|
||||
[Tooltip("Apply smallest-three quaternion compression. This is lossy, you can disable it if the small rotation inaccuracies are noticeable in your project.")]
|
||||
public bool compressRotation = true;
|
||||
|
||||
// interpolation is on by default, but can be disabled to jump to
|
||||
// the destination immediately. some projects need this.
|
||||
[Header("Interpolation")]
|
||||
@ -57,10 +62,15 @@ public abstract class NetworkTransformBase : NetworkBehaviour
|
||||
[Tooltip("Set to false to remove scale smoothing. Example use-case: Instant flipping of sprites that use -X and +X for direction.")]
|
||||
public bool interpolateScale = true;
|
||||
|
||||
// CoordinateSpace ///////////////////////////////////////////////////////////
|
||||
[Header("Coordinate Space")]
|
||||
[Tooltip("Local by default. World may be better when changing hierarchy, or non-NetworkTransforms root position/rotation/scale values.")]
|
||||
public CoordinateSpace coordinateSpace = CoordinateSpace.Local;
|
||||
|
||||
[Header("Send Interval Multiplier")]
|
||||
[Tooltip("Check/Sync every multiple of Network Manager send interval (= 1 / NM Send Rate), instead of every send interval.")]
|
||||
[Tooltip("Check/Sync every multiple of Network Manager send interval (= 1 / NM Send Rate), instead of every send interval.\n(30 NM send rate, and 3 interval, is a send every 0.1 seconds)\nA larger interval means less network sends, which has a variety of upsides. The drawbacks are delays and lower accuracy, you should find a nice balance between not sending too much, but the results looking good for your particular scenario.")]
|
||||
[Range(1, 120)]
|
||||
public uint sendIntervalMultiplier = 1; // not implemented yet
|
||||
public uint sendIntervalMultiplier = 1;
|
||||
|
||||
[Header("Timeline Offset")]
|
||||
[Tooltip("Add a small timeline offset to account for decoupled arrival of NetworkTime and NetworkTransform snapshots.\nfixes: https://github.com/MirrorNetworking/Mirror/issues/3427")]
|
||||
@ -89,10 +99,8 @@ public abstract class NetworkTransformBase : NetworkBehaviour
|
||||
public Color overlayColor = new Color(0, 0, 0, 0.5f);
|
||||
|
||||
// initialization //////////////////////////////////////////////////////
|
||||
// make sure to call this when inheriting too!
|
||||
protected virtual void Awake() { }
|
||||
|
||||
protected virtual void OnValidate()
|
||||
// forcec configuration of some settings
|
||||
protected virtual void Configure()
|
||||
{
|
||||
// set target to self if none yet
|
||||
if (target == null) target = transform;
|
||||
@ -104,19 +112,69 @@ protected virtual void OnValidate()
|
||||
// actually use NetworkServer.sendInterval.
|
||||
syncInterval = 0;
|
||||
|
||||
// obsolete clientAuthority compatibility:
|
||||
// if it was used, then set the new SyncDirection automatically.
|
||||
// if it wasn't used, then don't touch syncDirection.
|
||||
#pragma warning disable CS0618
|
||||
if (clientAuthority)
|
||||
{
|
||||
syncDirection = SyncDirection.ClientToServer;
|
||||
Debug.LogWarning($"{name}'s NetworkTransform component has obsolete .clientAuthority enabled. Please disable it and set SyncDirection to ClientToServer instead.");
|
||||
// Unity doesn't support setting world scale.
|
||||
// OnValidate force disables syncScale in world mode.
|
||||
if (coordinateSpace == CoordinateSpace.World) syncScale = false;
|
||||
}
|
||||
#pragma warning restore CS0618
|
||||
|
||||
// make sure to call this when inheriting too!
|
||||
protected virtual void Awake()
|
||||
{
|
||||
// sometimes OnValidate() doesn't run before launching a project.
|
||||
// need to guarantee configuration runs.
|
||||
Configure();
|
||||
}
|
||||
|
||||
protected override void OnValidate()
|
||||
{
|
||||
base.OnValidate();
|
||||
|
||||
// configure in awake
|
||||
Configure();
|
||||
}
|
||||
|
||||
// snapshot functions //////////////////////////////////////////////////
|
||||
// get local/world position
|
||||
protected virtual Vector3 GetPosition() =>
|
||||
coordinateSpace == CoordinateSpace.Local ? target.localPosition : target.position;
|
||||
|
||||
// get local/world rotation
|
||||
protected virtual Quaternion GetRotation() =>
|
||||
coordinateSpace == CoordinateSpace.Local ? target.localRotation : target.rotation;
|
||||
|
||||
// get local/world scale
|
||||
protected virtual Vector3 GetScale() =>
|
||||
coordinateSpace == CoordinateSpace.Local ? target.localScale : target.lossyScale;
|
||||
|
||||
// set local/world position
|
||||
protected virtual void SetPosition(Vector3 position)
|
||||
{
|
||||
if (coordinateSpace == CoordinateSpace.Local)
|
||||
target.localPosition = position;
|
||||
else
|
||||
target.position = position;
|
||||
}
|
||||
|
||||
// set local/world rotation
|
||||
protected virtual void SetRotation(Quaternion rotation)
|
||||
{
|
||||
if (coordinateSpace == CoordinateSpace.Local)
|
||||
target.localRotation = rotation;
|
||||
else
|
||||
target.rotation = rotation;
|
||||
}
|
||||
|
||||
// set local/world position
|
||||
protected virtual void SetScale(Vector3 scale)
|
||||
{
|
||||
if (coordinateSpace == CoordinateSpace.Local)
|
||||
target.localScale = scale;
|
||||
// Unity doesn't support setting world scale.
|
||||
// OnValidate disables syncScale in world mode.
|
||||
// else
|
||||
// target.lossyScale = scale; // TODO
|
||||
}
|
||||
|
||||
// construct a snapshot of the current state
|
||||
// => internal for testing
|
||||
protected virtual TransformSnapshot Construct()
|
||||
@ -126,9 +184,9 @@ protected virtual TransformSnapshot Construct()
|
||||
// our local time is what the other end uses as remote time
|
||||
NetworkTime.localTime, // Unity 2019 doesn't have timeAsDouble yet
|
||||
0, // the other end fills out local time itself
|
||||
target.localPosition,
|
||||
target.localRotation,
|
||||
target.localScale
|
||||
GetPosition(),
|
||||
GetRotation(),
|
||||
GetScale()
|
||||
);
|
||||
}
|
||||
|
||||
@ -143,18 +201,23 @@ protected void AddSnapshot(SortedList<double, TransformSnapshot> snapshots, doub
|
||||
// client sends snapshot at t=10
|
||||
// then the server would assume that it's one super slow move and
|
||||
// replay it for 10 seconds.
|
||||
if (!position.HasValue) position = snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].position : target.localPosition;
|
||||
if (!rotation.HasValue) rotation = snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].rotation : target.localRotation;
|
||||
if (!scale.HasValue) scale = snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].scale : target.localScale;
|
||||
|
||||
if (!position.HasValue) position = snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].position : GetPosition();
|
||||
if (!rotation.HasValue) rotation = snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].rotation : GetRotation();
|
||||
if (!scale.HasValue) scale = snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].scale : GetScale();
|
||||
|
||||
// insert transform snapshot
|
||||
SnapshotInterpolation.InsertIfNotExists(snapshots, new TransformSnapshot(
|
||||
SnapshotInterpolation.InsertIfNotExists(
|
||||
snapshots,
|
||||
NetworkClient.snapshotSettings.bufferLimit,
|
||||
new TransformSnapshot(
|
||||
timeStamp, // arrival remote timestamp. NOT remote time.
|
||||
NetworkTime.localTime, // Unity 2019 doesn't have timeAsDouble yet
|
||||
position.Value,
|
||||
rotation.Value,
|
||||
scale.Value
|
||||
));
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// apply a snapshot to the Transform.
|
||||
@ -177,14 +240,10 @@ protected virtual void Apply(TransformSnapshot interpolated, TransformSnapshot e
|
||||
// -> but simply don't apply it. if the user doesn't want to sync
|
||||
// scale, then we should not touch scale etc.
|
||||
|
||||
if (syncPosition)
|
||||
target.localPosition = interpolatePosition ? interpolated.position : endGoal.position;
|
||||
|
||||
if (syncRotation)
|
||||
target.localRotation = interpolateRotation ? interpolated.rotation : endGoal.rotation;
|
||||
|
||||
if (syncScale)
|
||||
target.localScale = interpolateScale ? interpolated.scale : endGoal.scale;
|
||||
// interpolate parts
|
||||
if (syncPosition) SetPosition(interpolatePosition ? interpolated.position : endGoal.position);
|
||||
if (syncRotation) SetRotation(interpolateRotation ? interpolated.rotation : endGoal.rotation);
|
||||
if (syncScale) SetScale(interpolateScale ? interpolated.scale : endGoal.scale);
|
||||
}
|
||||
|
||||
// client->server teleport to force position without interpolation.
|
||||
@ -264,21 +323,29 @@ public void RpcTeleport(Vector3 destination, Quaternion rotation)
|
||||
}
|
||||
|
||||
[ClientRpc]
|
||||
void RpcReset()
|
||||
void RpcResetState()
|
||||
{
|
||||
Reset();
|
||||
ResetState();
|
||||
}
|
||||
|
||||
// common Teleport code for client->server and server->client
|
||||
protected virtual void OnTeleport(Vector3 destination)
|
||||
{
|
||||
// reset any in-progress interpolation & buffers
|
||||
Reset();
|
||||
|
||||
// set the new position.
|
||||
// interpolation will automatically continue.
|
||||
target.position = destination;
|
||||
|
||||
// reset interpolation to immediately jump to the new position.
|
||||
// do not call Reset() here, this would cause delta compression to
|
||||
// get out of sync for NetworkTransformReliable because NTReliable's
|
||||
// 'override Reset()' resets lastDe/SerializedPosition:
|
||||
// https://github.com/MirrorNetworking/Mirror/issues/3588
|
||||
// because client's next OnSerialize() will delta compress,
|
||||
// but server's last delta will have been reset, causing offsets.
|
||||
//
|
||||
// instead, simply clear snapshots.
|
||||
ResetState();
|
||||
|
||||
// TODO
|
||||
// what if we still receive a snapshot from before the interpolation?
|
||||
// it could easily happen over unreliable.
|
||||
@ -288,21 +355,29 @@ protected virtual void OnTeleport(Vector3 destination)
|
||||
// common Teleport code for client->server and server->client
|
||||
protected virtual void OnTeleport(Vector3 destination, Quaternion rotation)
|
||||
{
|
||||
// reset any in-progress interpolation & buffers
|
||||
Reset();
|
||||
|
||||
// set the new position.
|
||||
// interpolation will automatically continue.
|
||||
target.position = destination;
|
||||
target.rotation = rotation;
|
||||
|
||||
// reset interpolation to immediately jump to the new position.
|
||||
// do not call Reset() here, this would cause delta compression to
|
||||
// get out of sync for NetworkTransformReliable because NTReliable's
|
||||
// 'override Reset()' resets lastDe/SerializedPosition:
|
||||
// https://github.com/MirrorNetworking/Mirror/issues/3588
|
||||
// because client's next OnSerialize() will delta compress,
|
||||
// but server's last delta will have been reset, causing offsets.
|
||||
//
|
||||
// instead, simply clear snapshots.
|
||||
ResetState();
|
||||
|
||||
// TODO
|
||||
// what if we still receive a snapshot from before the interpolation?
|
||||
// it could easily happen over unreliable.
|
||||
// -> maybe add destination as first entry?
|
||||
}
|
||||
|
||||
public virtual void Reset()
|
||||
public virtual void ResetState()
|
||||
{
|
||||
// disabled objects aren't updated anymore.
|
||||
// so let's clear the buffers.
|
||||
@ -310,9 +385,16 @@ public virtual void Reset()
|
||||
clientSnapshots.Clear();
|
||||
}
|
||||
|
||||
public virtual void Reset()
|
||||
{
|
||||
ResetState();
|
||||
// default to ClientToServer so this works immediately for users
|
||||
syncDirection = SyncDirection.ClientToServer;
|
||||
}
|
||||
|
||||
protected virtual void OnEnable()
|
||||
{
|
||||
Reset();
|
||||
ResetState();
|
||||
|
||||
if (NetworkServer.active)
|
||||
NetworkIdentity.clientAuthorityCallback += OnClientAuthorityChanged;
|
||||
@ -320,7 +402,7 @@ protected virtual void OnEnable()
|
||||
|
||||
protected virtual void OnDisable()
|
||||
{
|
||||
Reset();
|
||||
ResetState();
|
||||
|
||||
if (NetworkServer.active)
|
||||
NetworkIdentity.clientAuthorityCallback -= OnClientAuthorityChanged;
|
||||
@ -338,8 +420,8 @@ void OnClientAuthorityChanged(NetworkConnectionToClient conn, NetworkIdentity id
|
||||
|
||||
if (syncDirection == SyncDirection.ClientToServer)
|
||||
{
|
||||
Reset();
|
||||
RpcReset();
|
||||
ResetState();
|
||||
RpcResetState();
|
||||
}
|
||||
}
|
||||
|
||||
@ -393,7 +475,7 @@ protected virtual void DrawGizmos(SortedList<double, TransformSnapshot> buffer)
|
||||
TransformSnapshot entry = buffer.Values[i];
|
||||
bool oldEnough = entry.localTime <= threshold;
|
||||
Gizmos.color = oldEnough ? oldEnoughColor : notOldEnoughColor;
|
||||
Gizmos.DrawCube(entry.position, Vector3.one);
|
||||
Gizmos.DrawWireCube(entry.position, Vector3.one);
|
||||
}
|
||||
|
||||
// extra: lines between start<->position<->goal
|
@ -8,21 +8,16 @@ namespace Mirror
|
||||
[AddComponentMenu("Network/Network Transform (Reliable)")]
|
||||
public class NetworkTransformReliable : NetworkTransformBase
|
||||
{
|
||||
[Header("Sync Only If Changed")]
|
||||
[Tooltip("When true, changes are not sent unless greater than sensitivity values below.")]
|
||||
public bool onlySyncOnChange = true;
|
||||
|
||||
uint sendIntervalCounter = 0;
|
||||
double lastSendIntervalTime = double.MinValue;
|
||||
|
||||
[Header("Additional Settings")]
|
||||
[Tooltip("If we only sync on change, then we need to correct old snapshots if more time than sendInterval * multiplier has elapsed.\n\nOtherwise the first move will always start interpolating from the last move sequence's time, which will make it stutter when starting every time.")]
|
||||
public float onlySyncOnChangeCorrectionMultiplier = 2;
|
||||
|
||||
[Header("Rotation")]
|
||||
[Tooltip("Sensitivity of changes needed before an updated state is sent over the network")]
|
||||
public float rotationSensitivity = 0.01f;
|
||||
[Tooltip("Apply smallest-three quaternion compression. This is lossy, you can disable it if the small rotation inaccuracies are noticeable in your project.")]
|
||||
public bool compressRotation = false;
|
||||
|
||||
// delta compression is capable of detecting byte-level changes.
|
||||
// if we scale float position to bytes,
|
||||
@ -318,9 +313,9 @@ protected virtual void OnClientToServerSync(Vector3? position, Quaternion? rotat
|
||||
connectionToClient.remoteTimeStamp,
|
||||
NetworkTime.localTime, // arrival remote timestamp. NOT remote timeline.
|
||||
NetworkServer.sendInterval * sendIntervalMultiplier, // Unity 2019 doesn't have timeAsDouble yet
|
||||
target.localPosition,
|
||||
target.localRotation,
|
||||
target.localScale);
|
||||
GetPosition(),
|
||||
GetRotation(),
|
||||
GetScale());
|
||||
}
|
||||
|
||||
// add a small timeline offset to account for decoupled arrival of
|
||||
@ -346,9 +341,9 @@ protected virtual void OnServerToClientSync(Vector3? position, Quaternion? rotat
|
||||
NetworkClient.connection.remoteTimeStamp, // arrival remote timestamp. NOT remote timeline.
|
||||
NetworkTime.localTime, // Unity 2019 doesn't have timeAsDouble yet
|
||||
NetworkClient.sendInterval * sendIntervalMultiplier,
|
||||
target.localPosition,
|
||||
target.localRotation,
|
||||
target.localScale);
|
||||
GetPosition(),
|
||||
GetRotation(),
|
||||
GetScale());
|
||||
}
|
||||
|
||||
// add a small timeline offset to account for decoupled arrival of
|
||||
@ -392,18 +387,25 @@ static void RewriteHistory(
|
||||
|
||||
// insert a fake one at where we used to be,
|
||||
// 'sendInterval' behind the new one.
|
||||
SnapshotInterpolation.InsertIfNotExists(snapshots, new TransformSnapshot(
|
||||
SnapshotInterpolation.InsertIfNotExists(
|
||||
snapshots,
|
||||
NetworkClient.snapshotSettings.bufferLimit,
|
||||
new TransformSnapshot(
|
||||
remoteTimeStamp - sendInterval, // arrival remote timestamp. NOT remote time.
|
||||
localTime - sendInterval, // Unity 2019 doesn't have timeAsDouble yet
|
||||
position,
|
||||
rotation,
|
||||
scale
|
||||
));
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public override void Reset()
|
||||
// reset state for next session.
|
||||
// do not ever call this during a session (i.e. after teleport).
|
||||
// calling this will break delta compression.
|
||||
public override void ResetState()
|
||||
{
|
||||
base.Reset();
|
||||
base.ResetState();
|
||||
|
||||
// reset delta
|
||||
lastSerializedPosition = Vector3Long.zero;
|
@ -0,0 +1,679 @@
|
||||
// NetworkTransform V2 by mischa (2021-07)
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
[AddComponentMenu("Network/Network Transform (Unreliable)")]
|
||||
public class NetworkTransformUnreliable : NetworkTransformBase
|
||||
{
|
||||
uint sendIntervalCounter = 0;
|
||||
double lastSendIntervalTime = double.MinValue;
|
||||
|
||||
[Header("Additional Settings")]
|
||||
// Testing under really bad network conditions, 2%-5% packet loss and 250-1200ms ping, 5 proved to eliminate any twitching, however this should not be the default as it is a rare case Developers may want to cover.
|
||||
[Tooltip("How much time, as a multiple of send interval, has passed before clearing buffers.\nA larger buffer means more delay, but results in smoother movement.\nExample: 1 for faster responses minimal smoothing, 5 covers bad pings but has noticable delay, 3 is recommended for balanced results,.")]
|
||||
public float bufferResetMultiplier = 3;
|
||||
[Tooltip("Detect and send only changed data, such as Position X and Z, not the full Vector3 of X Y Z. Lowers network data at cost of extra calculations.")]
|
||||
public bool changedDetection = true;
|
||||
|
||||
[Header("Sensitivity"), Tooltip("Sensitivity of changes needed before an updated state is sent over the network")]
|
||||
public float positionSensitivity = 0.01f;
|
||||
public float rotationSensitivity = 0.01f;
|
||||
public float scaleSensitivity = 0.01f;
|
||||
|
||||
protected bool positionChanged;
|
||||
protected bool rotationChanged;
|
||||
protected bool scaleChanged;
|
||||
|
||||
// Used to store last sent snapshots
|
||||
protected TransformSnapshot lastSnapshot;
|
||||
protected bool cachedSnapshotComparison;
|
||||
protected Changed cachedChangedComparison;
|
||||
protected bool hasSentUnchangedPosition;
|
||||
|
||||
// update //////////////////////////////////////////////////////////////
|
||||
// Update applies interpolation
|
||||
void Update()
|
||||
{
|
||||
if (isServer) UpdateServerInterpolation();
|
||||
// for all other clients (and for local player if !authority),
|
||||
// we need to apply snapshots from the buffer.
|
||||
// 'else if' because host mode shouldn't interpolate client
|
||||
else if (isClient && !IsClientWithAuthority) UpdateClientInterpolation();
|
||||
}
|
||||
|
||||
// LateUpdate broadcasts.
|
||||
// movement scripts may change positions in Update.
|
||||
// use LateUpdate to ensure changes are detected in the same frame.
|
||||
// otherwise this may run before user update, delaying detection until next frame.
|
||||
// this could cause visible jitter.
|
||||
void LateUpdate()
|
||||
{
|
||||
// if server then always sync to others.
|
||||
if (isServer) UpdateServerBroadcast();
|
||||
// client authority, and local player (= allowed to move myself)?
|
||||
// 'else if' because host mode shouldn't send anything to server.
|
||||
// it is the server. don't overwrite anything there.
|
||||
else if (isClient && IsClientWithAuthority) UpdateClientBroadcast();
|
||||
}
|
||||
|
||||
protected virtual void CheckLastSendTime()
|
||||
{
|
||||
// We check interval every frame, and then send if interval is reached.
|
||||
// So by the time sendIntervalCounter == sendIntervalMultiplier, data is sent,
|
||||
// thus we reset the counter here.
|
||||
// This fixes previous issue of, if sendIntervalMultiplier = 1, we send every frame,
|
||||
// because intervalCounter is always = 1 in the previous version.
|
||||
|
||||
// Changing == to >= https://github.com/MirrorNetworking/Mirror/issues/3571
|
||||
|
||||
if (sendIntervalCounter >= sendIntervalMultiplier)
|
||||
sendIntervalCounter = 0;
|
||||
|
||||
// timeAsDouble not available in older Unity versions.
|
||||
if (AccurateInterval.Elapsed(NetworkTime.localTime, NetworkServer.sendInterval, ref lastSendIntervalTime))
|
||||
sendIntervalCounter++;
|
||||
}
|
||||
|
||||
void UpdateServerBroadcast()
|
||||
{
|
||||
// broadcast to all clients each 'sendInterval'
|
||||
// (client with authority will drop the rpc)
|
||||
// NetworkTime.localTime for double precision until Unity has it too
|
||||
//
|
||||
// IMPORTANT:
|
||||
// snapshot interpolation requires constant sending.
|
||||
// DO NOT only send if position changed. for example:
|
||||
// ---
|
||||
// * client sends first position at t=0
|
||||
// * ... 10s later ...
|
||||
// * client moves again, sends second position at t=10
|
||||
// ---
|
||||
// * server gets first position at t=0
|
||||
// * server gets second position at t=10
|
||||
// * server moves from first to second within a time of 10s
|
||||
// => would be a super slow move, instead of a wait & move.
|
||||
//
|
||||
// IMPORTANT:
|
||||
// DO NOT send nulls if not changed 'since last send' either. we
|
||||
// send unreliable and don't know which 'last send' the other end
|
||||
// received successfully.
|
||||
//
|
||||
// Checks to ensure server only sends snapshots if object is
|
||||
// on server authority(!clientAuthority) mode because on client
|
||||
// authority mode snapshots are broadcasted right after the authoritative
|
||||
// client updates server in the command function(see above), OR,
|
||||
// since host does not send anything to update the server, any client
|
||||
// authoritative movement done by the host will have to be broadcasted
|
||||
// here by checking IsClientWithAuthority.
|
||||
// TODO send same time that NetworkServer sends time snapshot?
|
||||
CheckLastSendTime();
|
||||
|
||||
if (sendIntervalCounter == sendIntervalMultiplier && // same interval as time interpolation!
|
||||
(syncDirection == SyncDirection.ServerToClient || IsClientWithAuthority))
|
||||
{
|
||||
// send snapshot without timestamp.
|
||||
// receiver gets it from batch timestamp to save bandwidth.
|
||||
TransformSnapshot snapshot = Construct();
|
||||
|
||||
if (changedDetection)
|
||||
{
|
||||
cachedChangedComparison = CompareChangedSnapshots(snapshot);
|
||||
|
||||
if ((cachedChangedComparison == Changed.None || cachedChangedComparison == Changed.CompressRot) && hasSentUnchangedPosition && onlySyncOnChange) { return; }
|
||||
|
||||
SyncData syncData = new SyncData(cachedChangedComparison, snapshot);
|
||||
|
||||
RpcServerToClientSync(syncData);
|
||||
|
||||
if (cachedChangedComparison == Changed.None || cachedChangedComparison == Changed.CompressRot)
|
||||
{
|
||||
hasSentUnchangedPosition = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
hasSentUnchangedPosition = false;
|
||||
UpdateLastSentSnapshot(cachedChangedComparison, snapshot);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
cachedSnapshotComparison = CompareSnapshots(snapshot);
|
||||
if (cachedSnapshotComparison && hasSentUnchangedPosition && onlySyncOnChange) { return; }
|
||||
|
||||
if (compressRotation)
|
||||
{
|
||||
RpcServerToClientSyncCompressRotation(
|
||||
// only sync what the user wants to sync
|
||||
syncPosition && positionChanged ? snapshot.position : default(Vector3?),
|
||||
syncRotation && rotationChanged ? Compression.CompressQuaternion(snapshot.rotation) : default(uint?),
|
||||
syncScale && scaleChanged ? snapshot.scale : default(Vector3?)
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
RpcServerToClientSync(
|
||||
// only sync what the user wants to sync
|
||||
syncPosition && positionChanged ? snapshot.position : default(Vector3?),
|
||||
syncRotation && rotationChanged ? snapshot.rotation : default(Quaternion?),
|
||||
syncScale && scaleChanged ? snapshot.scale : default(Vector3?)
|
||||
);
|
||||
}
|
||||
|
||||
if (cachedSnapshotComparison)
|
||||
{
|
||||
hasSentUnchangedPosition = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
hasSentUnchangedPosition = false;
|
||||
|
||||
// Fixes https://github.com/MirrorNetworking/Mirror/issues/3572
|
||||
// This also fixes https://github.com/MirrorNetworking/Mirror/issues/3573
|
||||
// with the exception of Quaternion.Angle sensitivity has to be > 0.16.
|
||||
// Unity issue, we are leaving it as is.
|
||||
|
||||
if (positionChanged) lastSnapshot.position = snapshot.position;
|
||||
if (rotationChanged) lastSnapshot.rotation = snapshot.rotation;
|
||||
if (positionChanged) lastSnapshot.scale = snapshot.scale;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UpdateServerInterpolation()
|
||||
{
|
||||
// apply buffered snapshots IF client authority
|
||||
// -> in server authority, server moves the object
|
||||
// so no need to apply any snapshots there.
|
||||
// -> don't apply for host mode player objects either, even if in
|
||||
// client authority mode. if it doesn't go over the network,
|
||||
// then we don't need to do anything.
|
||||
// -> connectionToClient is briefly null after scene changes:
|
||||
// https://github.com/MirrorNetworking/Mirror/issues/3329
|
||||
if (syncDirection == SyncDirection.ClientToServer &&
|
||||
connectionToClient != null &&
|
||||
!isOwned)
|
||||
{
|
||||
if (serverSnapshots.Count == 0) return;
|
||||
|
||||
// step the transform interpolation without touching time.
|
||||
// NetworkClient is responsible for time globally.
|
||||
SnapshotInterpolation.StepInterpolation(
|
||||
serverSnapshots,
|
||||
connectionToClient.remoteTimeline,
|
||||
out TransformSnapshot from,
|
||||
out TransformSnapshot to,
|
||||
out double t);
|
||||
|
||||
// interpolate & apply
|
||||
TransformSnapshot computed = TransformSnapshot.Interpolate(from, to, t);
|
||||
Apply(computed, to);
|
||||
}
|
||||
}
|
||||
|
||||
void UpdateClientBroadcast()
|
||||
{
|
||||
// https://github.com/vis2k/Mirror/pull/2992/
|
||||
if (!NetworkClient.ready) return;
|
||||
|
||||
// send to server each 'sendInterval'
|
||||
// NetworkTime.localTime for double precision until Unity has it too
|
||||
//
|
||||
// IMPORTANT:
|
||||
// snapshot interpolation requires constant sending.
|
||||
// DO NOT only send if position changed. for example:
|
||||
// ---
|
||||
// * client sends first position at t=0
|
||||
// * ... 10s later ...
|
||||
// * client moves again, sends second position at t=10
|
||||
// ---
|
||||
// * server gets first position at t=0
|
||||
// * server gets second position at t=10
|
||||
// * server moves from first to second within a time of 10s
|
||||
// => would be a super slow move, instead of a wait & move.
|
||||
//
|
||||
// IMPORTANT:
|
||||
// DO NOT send nulls if not changed 'since last send' either. we
|
||||
// send unreliable and don't know which 'last send' the other end
|
||||
// received successfully.
|
||||
CheckLastSendTime();
|
||||
if (sendIntervalCounter == sendIntervalMultiplier) // same interval as time interpolation!
|
||||
{
|
||||
// send snapshot without timestamp.
|
||||
// receiver gets it from batch timestamp to save bandwidth.
|
||||
TransformSnapshot snapshot = Construct();
|
||||
|
||||
if (changedDetection)
|
||||
{
|
||||
cachedChangedComparison = CompareChangedSnapshots(snapshot);
|
||||
|
||||
if ((cachedChangedComparison == Changed.None || cachedChangedComparison == Changed.CompressRot) && hasSentUnchangedPosition && onlySyncOnChange) { return; }
|
||||
|
||||
SyncData syncData = new SyncData(cachedChangedComparison, snapshot);
|
||||
|
||||
CmdClientToServerSync(syncData);
|
||||
|
||||
if (cachedChangedComparison == Changed.None || cachedChangedComparison == Changed.CompressRot)
|
||||
{
|
||||
hasSentUnchangedPosition = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
hasSentUnchangedPosition = false;
|
||||
UpdateLastSentSnapshot(cachedChangedComparison, snapshot);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
cachedSnapshotComparison = CompareSnapshots(snapshot);
|
||||
if (cachedSnapshotComparison && hasSentUnchangedPosition && onlySyncOnChange) { return; }
|
||||
|
||||
if (compressRotation)
|
||||
{
|
||||
CmdClientToServerSyncCompressRotation(
|
||||
// only sync what the user wants to sync
|
||||
syncPosition && positionChanged ? snapshot.position : default(Vector3?),
|
||||
syncRotation && rotationChanged ? Compression.CompressQuaternion(snapshot.rotation) : default(uint?),
|
||||
syncScale && scaleChanged ? snapshot.scale : default(Vector3?)
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
CmdClientToServerSync(
|
||||
// only sync what the user wants to sync
|
||||
syncPosition && positionChanged ? snapshot.position : default(Vector3?),
|
||||
syncRotation && rotationChanged ? snapshot.rotation : default(Quaternion?),
|
||||
syncScale && scaleChanged ? snapshot.scale : default(Vector3?)
|
||||
);
|
||||
}
|
||||
|
||||
if (cachedSnapshotComparison)
|
||||
{
|
||||
hasSentUnchangedPosition = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
hasSentUnchangedPosition = false;
|
||||
|
||||
// Fixes https://github.com/MirrorNetworking/Mirror/issues/3572
|
||||
// This also fixes https://github.com/MirrorNetworking/Mirror/issues/3573
|
||||
// with the exception of Quaternion.Angle sensitivity has to be > 0.16.
|
||||
// Unity issue, we are leaving it as is.
|
||||
if (positionChanged) lastSnapshot.position = snapshot.position;
|
||||
if (rotationChanged) lastSnapshot.rotation = snapshot.rotation;
|
||||
if (positionChanged) lastSnapshot.scale = snapshot.scale;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UpdateClientInterpolation()
|
||||
{
|
||||
// only while we have snapshots
|
||||
if (clientSnapshots.Count == 0) return;
|
||||
|
||||
// step the interpolation without touching time.
|
||||
// NetworkClient is responsible for time globally.
|
||||
SnapshotInterpolation.StepInterpolation(
|
||||
clientSnapshots,
|
||||
NetworkTime.time, // == NetworkClient.localTimeline from snapshot interpolation
|
||||
out TransformSnapshot from,
|
||||
out TransformSnapshot to,
|
||||
out double t);
|
||||
|
||||
// interpolate & apply
|
||||
TransformSnapshot computed = TransformSnapshot.Interpolate(from, to, t);
|
||||
Apply(computed, to);
|
||||
}
|
||||
|
||||
public override void OnSerialize(NetworkWriter writer, bool initialState)
|
||||
{
|
||||
// sync target component's position on spawn.
|
||||
// fixes https://github.com/vis2k/Mirror/pull/3051/
|
||||
// (Spawn message wouldn't sync NTChild positions either)
|
||||
if (initialState)
|
||||
{
|
||||
if (syncPosition) writer.WriteVector3(GetPosition());
|
||||
if (syncRotation) writer.WriteQuaternion(GetRotation());
|
||||
if (syncScale) writer.WriteVector3(GetScale());
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnDeserialize(NetworkReader reader, bool initialState)
|
||||
{
|
||||
// sync target component's position on spawn.
|
||||
// fixes https://github.com/vis2k/Mirror/pull/3051/
|
||||
// (Spawn message wouldn't sync NTChild positions either)
|
||||
if (initialState)
|
||||
{
|
||||
if (syncPosition) SetPosition(reader.ReadVector3());
|
||||
if (syncRotation) SetRotation(reader.ReadQuaternion());
|
||||
if (syncScale) SetScale(reader.ReadVector3());
|
||||
}
|
||||
}
|
||||
|
||||
// Returns true if position, rotation AND scale are unchanged, within given sensitivity range.
|
||||
protected virtual bool CompareSnapshots(TransformSnapshot currentSnapshot)
|
||||
{
|
||||
positionChanged = Vector3.SqrMagnitude(lastSnapshot.position - currentSnapshot.position) > positionSensitivity * positionSensitivity;
|
||||
rotationChanged = Quaternion.Angle(lastSnapshot.rotation, currentSnapshot.rotation) > rotationSensitivity;
|
||||
scaleChanged = Vector3.SqrMagnitude(lastSnapshot.scale - currentSnapshot.scale) > scaleSensitivity * scaleSensitivity;
|
||||
|
||||
return (!positionChanged && !rotationChanged && !scaleChanged);
|
||||
}
|
||||
|
||||
// cmd /////////////////////////////////////////////////////////////////
|
||||
// only unreliable. see comment above of this file.
|
||||
[Command(channel = Channels.Unreliable)]
|
||||
void CmdClientToServerSync(Vector3? position, Quaternion? rotation, Vector3? scale)
|
||||
{
|
||||
OnClientToServerSync(position, rotation, scale);
|
||||
//For client authority, immediately pass on the client snapshot to all other
|
||||
//clients instead of waiting for server to send its snapshots.
|
||||
if (syncDirection == SyncDirection.ClientToServer)
|
||||
RpcServerToClientSync(position, rotation, scale);
|
||||
}
|
||||
|
||||
// cmd /////////////////////////////////////////////////////////////////
|
||||
// only unreliable. see comment above of this file.
|
||||
[Command(channel = Channels.Unreliable)]
|
||||
void CmdClientToServerSyncCompressRotation(Vector3? position, uint? rotation, Vector3? scale)
|
||||
{
|
||||
// A fix to not apply current interpolated GetRotation when receiving null/unchanged value, instead use last sent snapshot rotation.
|
||||
Quaternion newRotation;
|
||||
if (rotation.HasValue)
|
||||
{
|
||||
newRotation = Compression.DecompressQuaternion((uint)rotation);
|
||||
}
|
||||
else
|
||||
{
|
||||
newRotation = serverSnapshots.Count > 0 ? serverSnapshots.Values[serverSnapshots.Count - 1].rotation : GetRotation();
|
||||
}
|
||||
OnClientToServerSync(position, newRotation, scale);
|
||||
//For client authority, immediately pass on the client snapshot to all other
|
||||
//clients instead of waiting for server to send its snapshots.
|
||||
if (syncDirection == SyncDirection.ClientToServer)
|
||||
RpcServerToClientSyncCompressRotation(position, rotation, scale);
|
||||
}
|
||||
|
||||
// local authority client sends sync message to server for broadcasting
|
||||
protected virtual void OnClientToServerSync(Vector3? position, Quaternion? rotation, Vector3? scale)
|
||||
{
|
||||
// only apply if in client authority mode
|
||||
if (syncDirection != SyncDirection.ClientToServer) return;
|
||||
|
||||
// protect against ever growing buffer size attacks
|
||||
if (serverSnapshots.Count >= connectionToClient.snapshotBufferSizeLimit) return;
|
||||
|
||||
// only player owned objects (with a connection) can send to
|
||||
// server. we can get the timestamp from the connection.
|
||||
double timestamp = connectionToClient.remoteTimeStamp;
|
||||
|
||||
if (onlySyncOnChange)
|
||||
{
|
||||
double timeIntervalCheck = bufferResetMultiplier * sendIntervalMultiplier * NetworkClient.sendInterval;
|
||||
|
||||
if (serverSnapshots.Count > 0 && serverSnapshots.Values[serverSnapshots.Count - 1].remoteTime + timeIntervalCheck < timestamp)
|
||||
ResetState();
|
||||
}
|
||||
|
||||
AddSnapshot(serverSnapshots, connectionToClient.remoteTimeStamp + timeStampAdjustment + offset, position, rotation, scale);
|
||||
}
|
||||
|
||||
// rpc /////////////////////////////////////////////////////////////////
|
||||
// only unreliable. see comment above of this file.
|
||||
[ClientRpc(channel = Channels.Unreliable)]
|
||||
void RpcServerToClientSync(Vector3? position, Quaternion? rotation, Vector3? scale) =>
|
||||
OnServerToClientSync(position, rotation, scale);
|
||||
|
||||
// rpc /////////////////////////////////////////////////////////////////
|
||||
// only unreliable. see comment above of this file.
|
||||
[ClientRpc(channel = Channels.Unreliable)]
|
||||
void RpcServerToClientSyncCompressRotation(Vector3? position, uint? rotation, Vector3? scale)
|
||||
{
|
||||
// A fix to not apply current interpolated GetRotation when receiving null/unchanged value, instead use last sent snapshot rotation.
|
||||
Quaternion newRotation;
|
||||
if (rotation.HasValue)
|
||||
{
|
||||
newRotation = Compression.DecompressQuaternion((uint)rotation);
|
||||
}
|
||||
else
|
||||
{
|
||||
newRotation = clientSnapshots.Count > 0 ? clientSnapshots.Values[clientSnapshots.Count - 1].rotation : GetRotation();
|
||||
}
|
||||
OnServerToClientSync(position, newRotation, scale);
|
||||
}
|
||||
|
||||
// server broadcasts sync message to all clients
|
||||
protected virtual void OnServerToClientSync(Vector3? position, Quaternion? rotation, Vector3? scale)
|
||||
{
|
||||
// in host mode, the server sends rpcs to all clients.
|
||||
// the host client itself will receive them too.
|
||||
// -> host server is always the source of truth
|
||||
// -> we can ignore any rpc on the host client
|
||||
// => otherwise host objects would have ever growing clientBuffers
|
||||
// (rpc goes to clients. if isServer is true too then we are host)
|
||||
if (isServer) return;
|
||||
|
||||
// don't apply for local player with authority
|
||||
if (IsClientWithAuthority) return;
|
||||
|
||||
// on the client, we receive rpcs for all entities.
|
||||
// not all of them have a connectionToServer.
|
||||
// but all of them go through NetworkClient.connection.
|
||||
// we can get the timestamp from there.
|
||||
double timestamp = NetworkClient.connection.remoteTimeStamp;
|
||||
|
||||
if (onlySyncOnChange)
|
||||
{
|
||||
double timeIntervalCheck = bufferResetMultiplier * sendIntervalMultiplier * NetworkServer.sendInterval;
|
||||
|
||||
if (clientSnapshots.Count > 0 && clientSnapshots.Values[clientSnapshots.Count - 1].remoteTime + timeIntervalCheck < timestamp)
|
||||
ResetState();
|
||||
}
|
||||
|
||||
AddSnapshot(clientSnapshots, NetworkClient.connection.remoteTimeStamp + timeStampAdjustment + offset, position, rotation, scale);
|
||||
}
|
||||
|
||||
protected virtual void UpdateLastSentSnapshot(Changed change, TransformSnapshot currentSnapshot)
|
||||
{
|
||||
if (change == Changed.None || change == Changed.CompressRot) return;
|
||||
|
||||
if ((change & Changed.PosX) > 0) lastSnapshot.position.x = currentSnapshot.position.x;
|
||||
if ((change & Changed.PosY) > 0) lastSnapshot.position.y = currentSnapshot.position.y;
|
||||
if ((change & Changed.PosZ) > 0) lastSnapshot.position.z = currentSnapshot.position.z;
|
||||
|
||||
if (compressRotation)
|
||||
{
|
||||
if ((change & Changed.Rot) > 0) lastSnapshot.rotation = currentSnapshot.rotation;
|
||||
}
|
||||
else
|
||||
{
|
||||
Vector3 newRotation;
|
||||
newRotation.x = (change & Changed.RotX) > 0 ? currentSnapshot.rotation.eulerAngles.x : lastSnapshot.rotation.eulerAngles.x;
|
||||
newRotation.y = (change & Changed.RotY) > 0 ? currentSnapshot.rotation.eulerAngles.y : lastSnapshot.rotation.eulerAngles.y;
|
||||
newRotation.z = (change & Changed.RotZ) > 0 ? currentSnapshot.rotation.eulerAngles.z : lastSnapshot.rotation.eulerAngles.z;
|
||||
|
||||
lastSnapshot.rotation = Quaternion.Euler(newRotation);
|
||||
}
|
||||
|
||||
if ((change & Changed.Scale) > 0) lastSnapshot.scale = currentSnapshot.scale;
|
||||
}
|
||||
|
||||
// Returns true if position, rotation AND scale are unchanged, within given sensitivity range.
|
||||
// Note the sensitivity comparison are different for pos, rot and scale.
|
||||
protected virtual Changed CompareChangedSnapshots(TransformSnapshot currentSnapshot)
|
||||
{
|
||||
Changed change = Changed.None;
|
||||
|
||||
if (syncPosition)
|
||||
{
|
||||
bool positionChanged = Vector3.SqrMagnitude(lastSnapshot.position - currentSnapshot.position) > positionSensitivity * positionSensitivity;
|
||||
if (positionChanged)
|
||||
{
|
||||
if (Mathf.Abs(lastSnapshot.position.x - currentSnapshot.position.x) > positionSensitivity) change |= Changed.PosX;
|
||||
if (Mathf.Abs(lastSnapshot.position.y - currentSnapshot.position.y) > positionSensitivity) change |= Changed.PosY;
|
||||
if (Mathf.Abs(lastSnapshot.position.z - currentSnapshot.position.z) > positionSensitivity) change |= Changed.PosZ;
|
||||
}
|
||||
}
|
||||
|
||||
if (syncRotation)
|
||||
{
|
||||
if (compressRotation)
|
||||
{
|
||||
bool rotationChanged = Quaternion.Angle(lastSnapshot.rotation, currentSnapshot.rotation) > rotationSensitivity;
|
||||
if (rotationChanged)
|
||||
{
|
||||
// Here we set all Rot enum flags, to tell us if there was a change in rotation
|
||||
// when using compression. If no change, we don't write the compressed Quat.
|
||||
change |= Changed.CompressRot;
|
||||
change |= Changed.Rot;
|
||||
}
|
||||
else
|
||||
{
|
||||
change |= Changed.CompressRot;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (Mathf.Abs(lastSnapshot.rotation.eulerAngles.x - currentSnapshot.rotation.eulerAngles.x) > rotationSensitivity) change |= Changed.RotX;
|
||||
if (Mathf.Abs(lastSnapshot.rotation.eulerAngles.y - currentSnapshot.rotation.eulerAngles.y) > rotationSensitivity) change |= Changed.RotY;
|
||||
if (Mathf.Abs(lastSnapshot.rotation.eulerAngles.z - currentSnapshot.rotation.eulerAngles.z) > rotationSensitivity) change |= Changed.RotZ;
|
||||
}
|
||||
}
|
||||
|
||||
if (syncScale)
|
||||
{
|
||||
if (Vector3.SqrMagnitude(lastSnapshot.scale - currentSnapshot.scale) > scaleSensitivity * scaleSensitivity) change |= Changed.Scale;
|
||||
}
|
||||
|
||||
return change;
|
||||
}
|
||||
|
||||
[Command(channel = Channels.Unreliable)]
|
||||
void CmdClientToServerSync(SyncData syncData)
|
||||
{
|
||||
OnClientToServerSync(syncData);
|
||||
//For client authority, immediately pass on the client snapshot to all other
|
||||
//clients instead of waiting for server to send its snapshots.
|
||||
if (syncDirection == SyncDirection.ClientToServer)
|
||||
RpcServerToClientSync(syncData);
|
||||
}
|
||||
|
||||
protected virtual void OnClientToServerSync(SyncData syncData)
|
||||
{
|
||||
// only apply if in client authority mode
|
||||
if (syncDirection != SyncDirection.ClientToServer) return;
|
||||
|
||||
// protect against ever growing buffer size attacks
|
||||
if (serverSnapshots.Count >= connectionToClient.snapshotBufferSizeLimit) return;
|
||||
|
||||
// only player owned objects (with a connection) can send to
|
||||
// server. we can get the timestamp from the connection.
|
||||
double timestamp = connectionToClient.remoteTimeStamp;
|
||||
|
||||
if (onlySyncOnChange)
|
||||
{
|
||||
double timeIntervalCheck = bufferResetMultiplier * sendIntervalMultiplier * NetworkClient.sendInterval;
|
||||
|
||||
if (serverSnapshots.Count > 0 && serverSnapshots.Values[serverSnapshots.Count - 1].remoteTime + timeIntervalCheck < timestamp)
|
||||
ResetState();
|
||||
}
|
||||
|
||||
UpdateSyncData(ref syncData, serverSnapshots);
|
||||
|
||||
AddSnapshot(serverSnapshots, connectionToClient.remoteTimeStamp + timeStampAdjustment + offset, syncData.position, syncData.quatRotation, syncData.scale);
|
||||
}
|
||||
|
||||
|
||||
[ClientRpc(channel = Channels.Unreliable)]
|
||||
void RpcServerToClientSync(SyncData syncData) =>
|
||||
OnServerToClientSync(syncData);
|
||||
|
||||
protected virtual void OnServerToClientSync(SyncData syncData)
|
||||
{
|
||||
// in host mode, the server sends rpcs to all clients.
|
||||
// the host client itself will receive them too.
|
||||
// -> host server is always the source of truth
|
||||
// -> we can ignore any rpc on the host client
|
||||
// => otherwise host objects would have ever growing clientBuffers
|
||||
// (rpc goes to clients. if isServer is true too then we are host)
|
||||
if (isServer) return;
|
||||
|
||||
// don't apply for local player with authority
|
||||
if (IsClientWithAuthority) return;
|
||||
|
||||
// on the client, we receive rpcs for all entities.
|
||||
// not all of them have a connectionToServer.
|
||||
// but all of them go through NetworkClient.connection.
|
||||
// we can get the timestamp from there.
|
||||
double timestamp = NetworkClient.connection.remoteTimeStamp;
|
||||
|
||||
if (onlySyncOnChange)
|
||||
{
|
||||
double timeIntervalCheck = bufferResetMultiplier * sendIntervalMultiplier * NetworkServer.sendInterval;
|
||||
|
||||
if (clientSnapshots.Count > 0 && clientSnapshots.Values[clientSnapshots.Count - 1].remoteTime + timeIntervalCheck < timestamp)
|
||||
ResetState();
|
||||
}
|
||||
|
||||
UpdateSyncData(ref syncData, clientSnapshots);
|
||||
|
||||
AddSnapshot(clientSnapshots, NetworkClient.connection.remoteTimeStamp + timeStampAdjustment + offset, syncData.position, syncData.quatRotation, syncData.scale);
|
||||
}
|
||||
|
||||
protected virtual void UpdateSyncData(ref SyncData syncData, SortedList<double, TransformSnapshot> snapshots)
|
||||
{
|
||||
if (syncData.changedDataByte == Changed.None || syncData.changedDataByte == Changed.CompressRot)
|
||||
{
|
||||
syncData.position = snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].position : GetPosition();
|
||||
syncData.quatRotation = snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].rotation : GetRotation();
|
||||
syncData.scale = snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].scale : GetScale();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Just going to update these without checking if syncposition or not,
|
||||
// because if not syncing position, NT will not apply any position data
|
||||
// to the target during Apply().
|
||||
|
||||
syncData.position.x = (syncData.changedDataByte & Changed.PosX) > 0 ? syncData.position.x : (snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].position.x : GetPosition().x);
|
||||
syncData.position.y = (syncData.changedDataByte & Changed.PosY) > 0 ? syncData.position.y : (snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].position.y : GetPosition().y);
|
||||
syncData.position.z = (syncData.changedDataByte & Changed.PosZ) > 0 ? syncData.position.z : (snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].position.z : GetPosition().z);
|
||||
|
||||
// If compressRot is true, we already have the Quat in syncdata.
|
||||
if ((syncData.changedDataByte & Changed.CompressRot) == 0)
|
||||
{
|
||||
syncData.vecRotation.x = (syncData.changedDataByte & Changed.RotX) > 0 ? syncData.vecRotation.x : (snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].rotation.eulerAngles.x : GetRotation().eulerAngles.x);
|
||||
syncData.vecRotation.y = (syncData.changedDataByte & Changed.RotY) > 0 ? syncData.vecRotation.y : (snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].rotation.eulerAngles.y : GetRotation().eulerAngles.y); ;
|
||||
syncData.vecRotation.z = (syncData.changedDataByte & Changed.RotZ) > 0 ? syncData.vecRotation.z : (snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].rotation.eulerAngles.z : GetRotation().eulerAngles.z);
|
||||
|
||||
syncData.quatRotation = Quaternion.Euler(syncData.vecRotation);
|
||||
}
|
||||
else
|
||||
{
|
||||
syncData.quatRotation = (syncData.changedDataByte & Changed.Rot) > 0 ? syncData.quatRotation : (snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].rotation : GetRotation());
|
||||
}
|
||||
|
||||
syncData.scale = (syncData.changedDataByte & Changed.Scale) > 0 ? syncData.scale : (snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].scale : GetScale());
|
||||
}
|
||||
}
|
||||
|
||||
// This is to extract position/rotation/scale data from payload. Override
|
||||
// Construct and Deconstruct if you are implementing a different SyncData logic.
|
||||
// Note however that snapshot interpolation still requires the basic 3 data
|
||||
// position, rotation and scale, which are computed from here.
|
||||
protected virtual void DeconstructSyncData(System.ArraySegment<byte> receivedPayload, out byte? changedFlagData, out Vector3? position, out Quaternion? rotation, out Vector3? scale)
|
||||
{
|
||||
using (NetworkReaderPooled reader = NetworkReaderPool.Get(receivedPayload))
|
||||
{
|
||||
SyncData syncData = reader.Read<SyncData>();
|
||||
changedFlagData = (byte)syncData.changedDataByte;
|
||||
position = syncData.position;
|
||||
rotation = syncData.quatRotation;
|
||||
scale = syncData.scale;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a553cb17010b2403e8523b558bffbc14
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -61,5 +61,8 @@ public static TransformSnapshot Interpolate(TransformSnapshot from, TransformSna
|
||||
Vector3.LerpUnclamped(from.scale, to.scale, (float)t)
|
||||
);
|
||||
}
|
||||
|
||||
public override string ToString() =>
|
||||
$"TransformSnapshot(remoteTime={remoteTime:F2}, localTime={localTime:F2}, pos={position}, rot={rotation}, scale={scale})";
|
||||
}
|
||||
}
|
156
Assets/Mirror/Components/NetworkTransform/TransformSyncData.cs
Normal file
156
Assets/Mirror/Components/NetworkTransform/TransformSyncData.cs
Normal file
@ -0,0 +1,156 @@
|
||||
using UnityEngine;
|
||||
using System;
|
||||
using Mirror;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
[Serializable]
|
||||
public struct SyncData
|
||||
{
|
||||
public Changed changedDataByte;
|
||||
public Vector3 position;
|
||||
public Quaternion quatRotation;
|
||||
public Vector3 vecRotation;
|
||||
public Vector3 scale;
|
||||
|
||||
public SyncData(Changed _dataChangedByte, Vector3 _position, Quaternion _rotation, Vector3 _scale)
|
||||
{
|
||||
this.changedDataByte = _dataChangedByte;
|
||||
this.position = _position;
|
||||
this.quatRotation = _rotation;
|
||||
this.vecRotation = quatRotation.eulerAngles;
|
||||
this.scale = _scale;
|
||||
}
|
||||
|
||||
public SyncData(Changed _dataChangedByte, TransformSnapshot _snapshot)
|
||||
{
|
||||
this.changedDataByte = _dataChangedByte;
|
||||
this.position = _snapshot.position;
|
||||
this.quatRotation = _snapshot.rotation;
|
||||
this.vecRotation = quatRotation.eulerAngles;
|
||||
this.scale = _snapshot.scale;
|
||||
}
|
||||
|
||||
public SyncData(Changed _dataChangedByte, Vector3 _position, Vector3 _vecRotation, Vector3 _scale)
|
||||
{
|
||||
this.changedDataByte = _dataChangedByte;
|
||||
this.position = _position;
|
||||
this.vecRotation = _vecRotation;
|
||||
this.quatRotation = Quaternion.Euler(vecRotation);
|
||||
this.scale = _scale;
|
||||
}
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum Changed : byte
|
||||
{
|
||||
None = 0,
|
||||
PosX = 1 << 0,
|
||||
PosY = 1 << 1,
|
||||
PosZ = 1 << 2,
|
||||
CompressRot = 1 << 3,
|
||||
RotX = 1 << 4,
|
||||
RotY = 1 << 5,
|
||||
RotZ = 1 << 6,
|
||||
Scale = 1 << 7,
|
||||
|
||||
Pos = PosX | PosY | PosZ,
|
||||
Rot = RotX | RotY | RotZ
|
||||
}
|
||||
|
||||
|
||||
public static class SyncDataReaderWriter
|
||||
{
|
||||
public static void WriteSyncData(this NetworkWriter writer, SyncData syncData)
|
||||
{
|
||||
writer.WriteByte((byte)syncData.changedDataByte);
|
||||
|
||||
// Write position
|
||||
if ((syncData.changedDataByte & Changed.PosX) > 0)
|
||||
{
|
||||
writer.WriteFloat(syncData.position.x);
|
||||
}
|
||||
|
||||
if ((syncData.changedDataByte & Changed.PosY) > 0)
|
||||
{
|
||||
writer.WriteFloat(syncData.position.y);
|
||||
}
|
||||
|
||||
if ((syncData.changedDataByte & Changed.PosZ) > 0)
|
||||
{
|
||||
writer.WriteFloat(syncData.position.z);
|
||||
}
|
||||
|
||||
// Write rotation
|
||||
if ((syncData.changedDataByte & Changed.CompressRot) > 0)
|
||||
{
|
||||
if((syncData.changedDataByte & Changed.Rot) > 0)
|
||||
{
|
||||
writer.WriteUInt(Compression.CompressQuaternion(syncData.quatRotation));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if ((syncData.changedDataByte & Changed.RotX) > 0)
|
||||
{
|
||||
writer.WriteFloat(syncData.quatRotation.eulerAngles.x);
|
||||
}
|
||||
|
||||
if ((syncData.changedDataByte & Changed.RotY) > 0)
|
||||
{
|
||||
writer.WriteFloat(syncData.quatRotation.eulerAngles.y);
|
||||
}
|
||||
|
||||
if ((syncData.changedDataByte & Changed.RotZ) > 0)
|
||||
{
|
||||
writer.WriteFloat(syncData.quatRotation.eulerAngles.z);
|
||||
}
|
||||
}
|
||||
|
||||
// Write scale
|
||||
if ((syncData.changedDataByte & Changed.Scale) > 0)
|
||||
{
|
||||
writer.WriteVector3(syncData.scale);
|
||||
}
|
||||
}
|
||||
|
||||
public static SyncData ReadSyncData(this NetworkReader reader)
|
||||
{
|
||||
Changed changedData = (Changed)reader.ReadByte();
|
||||
|
||||
// If we have nothing to read here, let's say because posX is unchanged, then we can write anything
|
||||
// for now, but in the NT, we will need to check changedData again, to put the right values of the axis
|
||||
// back. We don't have it here.
|
||||
|
||||
Vector3 position =
|
||||
new Vector3(
|
||||
(changedData & Changed.PosX) > 0 ? reader.ReadFloat() : 0,
|
||||
(changedData & Changed.PosY) > 0 ? reader.ReadFloat() : 0,
|
||||
(changedData & Changed.PosZ) > 0 ? reader.ReadFloat() : 0
|
||||
);
|
||||
|
||||
Vector3 vecRotation = new Vector3();
|
||||
Quaternion quatRotation = new Quaternion();
|
||||
|
||||
if ((changedData & Changed.CompressRot) > 0)
|
||||
{
|
||||
quatRotation = (changedData & Changed.RotX) > 0 ? Compression.DecompressQuaternion(reader.ReadUInt()) : new Quaternion();
|
||||
}
|
||||
else
|
||||
{
|
||||
vecRotation =
|
||||
new Vector3(
|
||||
(changedData & Changed.RotX) > 0 ? reader.ReadFloat() : 0,
|
||||
(changedData & Changed.RotY) > 0 ? reader.ReadFloat() : 0,
|
||||
(changedData & Changed.RotZ) > 0 ? reader.ReadFloat() : 0
|
||||
);
|
||||
}
|
||||
|
||||
Vector3 scale = (changedData & Changed.Scale) == Changed.Scale ? reader.ReadVector3() : new Vector3();
|
||||
|
||||
SyncData _syncData = (changedData & Changed.CompressRot) > 0 ? new SyncData(changedData, position, quatRotation, scale) : new SyncData(changedData, position, vecRotation, scale);
|
||||
|
||||
return _syncData;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a1c0832ca88e749ff96fe04cebb617ef
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -1,383 +0,0 @@
|
||||
// NetworkTransform V2 by mischa (2021-07)
|
||||
// comment out the below line to quickly revert the onlySyncOnChange feature
|
||||
#define onlySyncOnChange_BANDWIDTH_SAVING
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
[AddComponentMenu("Network/Network Transform (Unreliable)")]
|
||||
public class NetworkTransform : NetworkTransformBase
|
||||
{
|
||||
// only sync when changed hack /////////////////////////////////////////
|
||||
#if onlySyncOnChange_BANDWIDTH_SAVING
|
||||
[Header("Sync Only If Changed")]
|
||||
[Tooltip("When true, changes are not sent unless greater than sensitivity values below.")]
|
||||
public bool onlySyncOnChange = true;
|
||||
|
||||
uint sendIntervalCounter = 0;
|
||||
double lastSendIntervalTime = double.MinValue;
|
||||
|
||||
// 3 was original, but testing under really bad network conditions, 2%-5% packet loss and 250-1200ms ping, 5 proved to eliminate any twitching.
|
||||
[Tooltip("How much time, as a multiple of send interval, has passed before clearing buffers.")]
|
||||
public float bufferResetMultiplier = 5;
|
||||
|
||||
[Header("Sensitivity"), Tooltip("Sensitivity of changes needed before an updated state is sent over the network")]
|
||||
public float positionSensitivity = 0.01f;
|
||||
public float rotationSensitivity = 0.01f;
|
||||
public float scaleSensitivity = 0.01f;
|
||||
|
||||
protected bool positionChanged;
|
||||
protected bool rotationChanged;
|
||||
protected bool scaleChanged;
|
||||
|
||||
// Used to store last sent snapshots
|
||||
protected TransformSnapshot lastSnapshot;
|
||||
protected bool cachedSnapshotComparison;
|
||||
protected bool hasSentUnchangedPosition;
|
||||
#endif
|
||||
|
||||
// update //////////////////////////////////////////////////////////////
|
||||
// Update applies interpolation
|
||||
void Update()
|
||||
{
|
||||
if (isServer) UpdateServerInterpolation();
|
||||
// for all other clients (and for local player if !authority),
|
||||
// we need to apply snapshots from the buffer.
|
||||
// 'else if' because host mode shouldn't interpolate client
|
||||
else if (isClient && !IsClientWithAuthority) UpdateClientInterpolation();
|
||||
}
|
||||
|
||||
// LateUpdate broadcasts.
|
||||
// movement scripts may change positions in Update.
|
||||
// use LateUpdate to ensure changes are detected in the same frame.
|
||||
// otherwise this may run before user update, delaying detection until next frame.
|
||||
// this could cause visible jitter.
|
||||
void LateUpdate()
|
||||
{
|
||||
// if server then always sync to others.
|
||||
if (isServer) UpdateServerBroadcast();
|
||||
// client authority, and local player (= allowed to move myself)?
|
||||
// 'else if' because host mode shouldn't send anything to server.
|
||||
// it is the server. don't overwrite anything there.
|
||||
else if (isClient && IsClientWithAuthority) UpdateClientBroadcast();
|
||||
}
|
||||
|
||||
protected virtual void CheckLastSendTime()
|
||||
{
|
||||
// timeAsDouble not available in older Unity versions.
|
||||
if (AccurateInterval.Elapsed(NetworkTime.localTime, NetworkServer.sendInterval, ref lastSendIntervalTime))
|
||||
{
|
||||
if (sendIntervalCounter == sendIntervalMultiplier)
|
||||
sendIntervalCounter = 0;
|
||||
sendIntervalCounter++;
|
||||
}
|
||||
}
|
||||
|
||||
void UpdateServerBroadcast()
|
||||
{
|
||||
// broadcast to all clients each 'sendInterval'
|
||||
// (client with authority will drop the rpc)
|
||||
// NetworkTime.localTime for double precision until Unity has it too
|
||||
//
|
||||
// IMPORTANT:
|
||||
// snapshot interpolation requires constant sending.
|
||||
// DO NOT only send if position changed. for example:
|
||||
// ---
|
||||
// * client sends first position at t=0
|
||||
// * ... 10s later ...
|
||||
// * client moves again, sends second position at t=10
|
||||
// ---
|
||||
// * server gets first position at t=0
|
||||
// * server gets second position at t=10
|
||||
// * server moves from first to second within a time of 10s
|
||||
// => would be a super slow move, instead of a wait & move.
|
||||
//
|
||||
// IMPORTANT:
|
||||
// DO NOT send nulls if not changed 'since last send' either. we
|
||||
// send unreliable and don't know which 'last send' the other end
|
||||
// received successfully.
|
||||
//
|
||||
// Checks to ensure server only sends snapshots if object is
|
||||
// on server authority(!clientAuthority) mode because on client
|
||||
// authority mode snapshots are broadcasted right after the authoritative
|
||||
// client updates server in the command function(see above), OR,
|
||||
// since host does not send anything to update the server, any client
|
||||
// authoritative movement done by the host will have to be broadcasted
|
||||
// here by checking IsClientWithAuthority.
|
||||
// TODO send same time that NetworkServer sends time snapshot?
|
||||
CheckLastSendTime();
|
||||
|
||||
if (sendIntervalCounter == sendIntervalMultiplier && // same interval as time interpolation!
|
||||
(syncDirection == SyncDirection.ServerToClient || IsClientWithAuthority))
|
||||
{
|
||||
// send snapshot without timestamp.
|
||||
// receiver gets it from batch timestamp to save bandwidth.
|
||||
TransformSnapshot snapshot = Construct();
|
||||
#if onlySyncOnChange_BANDWIDTH_SAVING
|
||||
cachedSnapshotComparison = CompareSnapshots(snapshot);
|
||||
if (cachedSnapshotComparison && hasSentUnchangedPosition && onlySyncOnChange) { return; }
|
||||
#endif
|
||||
|
||||
#if onlySyncOnChange_BANDWIDTH_SAVING
|
||||
RpcServerToClientSync(
|
||||
// only sync what the user wants to sync
|
||||
syncPosition && positionChanged ? snapshot.position : default(Vector3?),
|
||||
syncRotation && rotationChanged ? snapshot.rotation : default(Quaternion?),
|
||||
syncScale && scaleChanged ? snapshot.scale : default(Vector3?)
|
||||
);
|
||||
#else
|
||||
RpcServerToClientSync(
|
||||
// only sync what the user wants to sync
|
||||
syncPosition ? snapshot.position : default(Vector3?),
|
||||
syncRotation ? snapshot.rotation : default(Quaternion?),
|
||||
syncScale ? snapshot.scale : default(Vector3?)
|
||||
);
|
||||
#endif
|
||||
|
||||
#if onlySyncOnChange_BANDWIDTH_SAVING
|
||||
if (cachedSnapshotComparison)
|
||||
{
|
||||
hasSentUnchangedPosition = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
hasSentUnchangedPosition = false;
|
||||
lastSnapshot = snapshot;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
void UpdateServerInterpolation()
|
||||
{
|
||||
// apply buffered snapshots IF client authority
|
||||
// -> in server authority, server moves the object
|
||||
// so no need to apply any snapshots there.
|
||||
// -> don't apply for host mode player objects either, even if in
|
||||
// client authority mode. if it doesn't go over the network,
|
||||
// then we don't need to do anything.
|
||||
// -> connectionToClient is briefly null after scene changes:
|
||||
// https://github.com/MirrorNetworking/Mirror/issues/3329
|
||||
if (syncDirection == SyncDirection.ClientToServer &&
|
||||
connectionToClient != null &&
|
||||
!isOwned)
|
||||
{
|
||||
if (serverSnapshots.Count == 0) return;
|
||||
|
||||
// step the transform interpolation without touching time.
|
||||
// NetworkClient is responsible for time globally.
|
||||
SnapshotInterpolation.StepInterpolation(
|
||||
serverSnapshots,
|
||||
connectionToClient.remoteTimeline,
|
||||
out TransformSnapshot from,
|
||||
out TransformSnapshot to,
|
||||
out double t);
|
||||
|
||||
// interpolate & apply
|
||||
TransformSnapshot computed = TransformSnapshot.Interpolate(from, to, t);
|
||||
Apply(computed, to);
|
||||
}
|
||||
}
|
||||
|
||||
void UpdateClientBroadcast()
|
||||
{
|
||||
// https://github.com/vis2k/Mirror/pull/2992/
|
||||
if (!NetworkClient.ready) return;
|
||||
|
||||
// send to server each 'sendInterval'
|
||||
// NetworkTime.localTime for double precision until Unity has it too
|
||||
//
|
||||
// IMPORTANT:
|
||||
// snapshot interpolation requires constant sending.
|
||||
// DO NOT only send if position changed. for example:
|
||||
// ---
|
||||
// * client sends first position at t=0
|
||||
// * ... 10s later ...
|
||||
// * client moves again, sends second position at t=10
|
||||
// ---
|
||||
// * server gets first position at t=0
|
||||
// * server gets second position at t=10
|
||||
// * server moves from first to second within a time of 10s
|
||||
// => would be a super slow move, instead of a wait & move.
|
||||
//
|
||||
// IMPORTANT:
|
||||
// DO NOT send nulls if not changed 'since last send' either. we
|
||||
// send unreliable and don't know which 'last send' the other end
|
||||
// received successfully.
|
||||
CheckLastSendTime();
|
||||
if (sendIntervalCounter == sendIntervalMultiplier) // same interval as time interpolation!
|
||||
{
|
||||
// send snapshot without timestamp.
|
||||
// receiver gets it from batch timestamp to save bandwidth.
|
||||
TransformSnapshot snapshot = Construct();
|
||||
#if onlySyncOnChange_BANDWIDTH_SAVING
|
||||
cachedSnapshotComparison = CompareSnapshots(snapshot);
|
||||
if (cachedSnapshotComparison && hasSentUnchangedPosition && onlySyncOnChange) { return; }
|
||||
#endif
|
||||
|
||||
#if onlySyncOnChange_BANDWIDTH_SAVING
|
||||
CmdClientToServerSync(
|
||||
// only sync what the user wants to sync
|
||||
syncPosition && positionChanged ? snapshot.position : default(Vector3?),
|
||||
syncRotation && rotationChanged ? snapshot.rotation : default(Quaternion?),
|
||||
syncScale && scaleChanged ? snapshot.scale : default(Vector3?)
|
||||
);
|
||||
#else
|
||||
CmdClientToServerSync(
|
||||
// only sync what the user wants to sync
|
||||
syncPosition ? snapshot.position : default(Vector3?),
|
||||
syncRotation ? snapshot.rotation : default(Quaternion?),
|
||||
syncScale ? snapshot.scale : default(Vector3?)
|
||||
);
|
||||
#endif
|
||||
|
||||
#if onlySyncOnChange_BANDWIDTH_SAVING
|
||||
if (cachedSnapshotComparison)
|
||||
{
|
||||
hasSentUnchangedPosition = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
hasSentUnchangedPosition = false;
|
||||
lastSnapshot = snapshot;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
void UpdateClientInterpolation()
|
||||
{
|
||||
// only while we have snapshots
|
||||
if (clientSnapshots.Count == 0) return;
|
||||
|
||||
// step the interpolation without touching time.
|
||||
// NetworkClient is responsible for time globally.
|
||||
SnapshotInterpolation.StepInterpolation(
|
||||
clientSnapshots,
|
||||
NetworkTime.time, // == NetworkClient.localTimeline from snapshot interpolation
|
||||
out TransformSnapshot from,
|
||||
out TransformSnapshot to,
|
||||
out double t);
|
||||
|
||||
// interpolate & apply
|
||||
TransformSnapshot computed = TransformSnapshot.Interpolate(from, to, t);
|
||||
Apply(computed, to);
|
||||
}
|
||||
|
||||
public override void OnSerialize(NetworkWriter writer, bool initialState)
|
||||
{
|
||||
// sync target component's position on spawn.
|
||||
// fixes https://github.com/vis2k/Mirror/pull/3051/
|
||||
// (Spawn message wouldn't sync NTChild positions either)
|
||||
if (initialState)
|
||||
{
|
||||
if (syncPosition) writer.WriteVector3(target.localPosition);
|
||||
if (syncRotation) writer.WriteQuaternion(target.localRotation);
|
||||
if (syncScale) writer.WriteVector3(target.localScale);
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnDeserialize(NetworkReader reader, bool initialState)
|
||||
{
|
||||
// sync target component's position on spawn.
|
||||
// fixes https://github.com/vis2k/Mirror/pull/3051/
|
||||
// (Spawn message wouldn't sync NTChild positions either)
|
||||
if (initialState)
|
||||
{
|
||||
if (syncPosition) target.localPosition = reader.ReadVector3();
|
||||
if (syncRotation) target.localRotation = reader.ReadQuaternion();
|
||||
if (syncScale) target.localScale = reader.ReadVector3();
|
||||
}
|
||||
}
|
||||
|
||||
#if onlySyncOnChange_BANDWIDTH_SAVING
|
||||
// Returns true if position, rotation AND scale are unchanged, within given sensitivity range.
|
||||
protected virtual bool CompareSnapshots(TransformSnapshot currentSnapshot)
|
||||
{
|
||||
positionChanged = Vector3.SqrMagnitude(lastSnapshot.position - currentSnapshot.position) > positionSensitivity * positionSensitivity;
|
||||
rotationChanged = Quaternion.Angle(lastSnapshot.rotation, currentSnapshot.rotation) > rotationSensitivity;
|
||||
scaleChanged = Vector3.SqrMagnitude(lastSnapshot.scale - currentSnapshot.scale) > scaleSensitivity * scaleSensitivity;
|
||||
|
||||
return (!positionChanged && !rotationChanged && !scaleChanged);
|
||||
}
|
||||
#endif
|
||||
// cmd /////////////////////////////////////////////////////////////////
|
||||
// only unreliable. see comment above of this file.
|
||||
[Command(channel = Channels.Unreliable)]
|
||||
void CmdClientToServerSync(Vector3? position, Quaternion? rotation, Vector3? scale)
|
||||
{
|
||||
OnClientToServerSync(position, rotation, scale);
|
||||
//For client authority, immediately pass on the client snapshot to all other
|
||||
//clients instead of waiting for server to send its snapshots.
|
||||
if (syncDirection == SyncDirection.ClientToServer)
|
||||
{
|
||||
RpcServerToClientSync(position, rotation, scale);
|
||||
}
|
||||
}
|
||||
|
||||
// local authority client sends sync message to server for broadcasting
|
||||
protected virtual void OnClientToServerSync(Vector3? position, Quaternion? rotation, Vector3? scale)
|
||||
{
|
||||
// only apply if in client authority mode
|
||||
if (syncDirection != SyncDirection.ClientToServer) return;
|
||||
|
||||
// protect against ever growing buffer size attacks
|
||||
if (serverSnapshots.Count >= connectionToClient.snapshotBufferSizeLimit) return;
|
||||
|
||||
// only player owned objects (with a connection) can send to
|
||||
// server. we can get the timestamp from the connection.
|
||||
double timestamp = connectionToClient.remoteTimeStamp;
|
||||
#if onlySyncOnChange_BANDWIDTH_SAVING
|
||||
if (onlySyncOnChange)
|
||||
{
|
||||
double timeIntervalCheck = bufferResetMultiplier * sendIntervalMultiplier * NetworkClient.sendInterval;
|
||||
|
||||
if (serverSnapshots.Count > 0 && serverSnapshots.Values[serverSnapshots.Count - 1].remoteTime + timeIntervalCheck < timestamp)
|
||||
{
|
||||
Reset();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
AddSnapshot(serverSnapshots, connectionToClient.remoteTimeStamp + timeStampAdjustment + offset, position, rotation, scale);
|
||||
}
|
||||
|
||||
// rpc /////////////////////////////////////////////////////////////////
|
||||
// only unreliable. see comment above of this file.
|
||||
[ClientRpc(channel = Channels.Unreliable)]
|
||||
void RpcServerToClientSync(Vector3? position, Quaternion? rotation, Vector3? scale) =>
|
||||
OnServerToClientSync(position, rotation, scale);
|
||||
|
||||
// server broadcasts sync message to all clients
|
||||
protected virtual void OnServerToClientSync(Vector3? position, Quaternion? rotation, Vector3? scale)
|
||||
{
|
||||
// in host mode, the server sends rpcs to all clients.
|
||||
// the host client itself will receive them too.
|
||||
// -> host server is always the source of truth
|
||||
// -> we can ignore any rpc on the host client
|
||||
// => otherwise host objects would have ever growing clientBuffers
|
||||
// (rpc goes to clients. if isServer is true too then we are host)
|
||||
if (isServer) return;
|
||||
|
||||
// don't apply for local player with authority
|
||||
if (IsClientWithAuthority) return;
|
||||
|
||||
// on the client, we receive rpcs for all entities.
|
||||
// not all of them have a connectionToServer.
|
||||
// but all of them go through NetworkClient.connection.
|
||||
// we can get the timestamp from there.
|
||||
double timestamp = NetworkClient.connection.remoteTimeStamp;
|
||||
#if onlySyncOnChange_BANDWIDTH_SAVING
|
||||
if (onlySyncOnChange)
|
||||
{
|
||||
double timeIntervalCheck = bufferResetMultiplier * sendIntervalMultiplier * NetworkServer.sendInterval;
|
||||
|
||||
if (clientSnapshots.Count > 0 && clientSnapshots.Values[clientSnapshots.Count - 1].remoteTime + timeIntervalCheck < timestamp)
|
||||
{
|
||||
Reset();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
AddSnapshot(clientSnapshots, NetworkClient.connection.remoteTimeStamp + timeStampAdjustment + offset, position, rotation, scale);
|
||||
}
|
||||
}
|
||||
}
|
3
Assets/Mirror/Components/PredictedRigidbody.meta
Normal file
3
Assets/Mirror/Components/PredictedRigidbody.meta
Normal file
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 09cc6745984c453a8cfb4cf4244d2570
|
||||
timeCreated: 1693576410
|
@ -0,0 +1,85 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!21 &2100000
|
||||
Material:
|
||||
serializedVersion: 8
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: LocalGhostMaterial
|
||||
m_Shader: {fileID: 46, guid: 0000000000000000f000000000000000, type: 0}
|
||||
m_Parent: {fileID: 0}
|
||||
m_ModifiedSerializedProperties: 0
|
||||
m_ValidKeywords:
|
||||
- _ALPHAPREMULTIPLY_ON
|
||||
m_InvalidKeywords: []
|
||||
m_LightmapFlags: 4
|
||||
m_EnableInstancingVariants: 0
|
||||
m_DoubleSidedGI: 0
|
||||
m_CustomRenderQueue: 3000
|
||||
stringTagMap:
|
||||
RenderType: Transparent
|
||||
disabledShaderPasses: []
|
||||
m_LockedProperties:
|
||||
m_SavedProperties:
|
||||
serializedVersion: 3
|
||||
m_TexEnvs:
|
||||
- _BumpMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailAlbedoMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailMask:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailNormalMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _EmissionMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _MainTex:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _MetallicGlossMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _OcclusionMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _ParallaxMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
m_Ints: []
|
||||
m_Floats:
|
||||
- _BumpScale: 1
|
||||
- _Cutoff: 0.5
|
||||
- _DetailNormalMapScale: 1
|
||||
- _DstBlend: 10
|
||||
- _GlossMapScale: 1
|
||||
- _Glossiness: 0.92
|
||||
- _GlossyReflections: 1
|
||||
- _Metallic: 0
|
||||
- _Mode: 3
|
||||
- _OcclusionStrength: 1
|
||||
- _Parallax: 0.02
|
||||
- _SmoothnessTextureChannel: 0
|
||||
- _SpecularHighlights: 1
|
||||
- _SrcBlend: 1
|
||||
- _UVSec: 0
|
||||
- _ZWrite: 0
|
||||
m_Colors:
|
||||
- _Color: {r: 1, g: 0, b: 0.067070484, a: 0.15686275}
|
||||
- _EmissionColor: {r: 0, g: 0, b: 0, a: 1}
|
||||
m_BuildTextureStacks: []
|
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 411a48b4a197d4924bec3e3809bc9320
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 2100000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -0,0 +1,997 @@
|
||||
// PredictedRigidbody which stores & indidvidually rewinds history per Rigidbody.
|
||||
//
|
||||
// This brings significant performance savings because:
|
||||
// - if a scene has 1000 objects
|
||||
// - and a player interacts with say 3 objects at a time
|
||||
// - Physics.Simulate() would resimulate 1000 objects
|
||||
// - where as this component only resimulates the 3 changed objects
|
||||
//
|
||||
// The downside is that history rewinding is done manually via Vector math,
|
||||
// instead of real physics. It's not 100% correct - but it sure is fast!
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
public enum PredictionMode { Smooth, Fast }
|
||||
|
||||
// [RequireComponent(typeof(Rigidbody))] <- RB is moved out at runtime, can't require it.
|
||||
public class PredictedRigidbody : NetworkBehaviour
|
||||
{
|
||||
Transform tf; // this component is performance critical. cache .transform getter!
|
||||
|
||||
// Prediction sometimes moves the Rigidbody to a ghost object.
|
||||
// .predictedRigidbody is always kept up to date to wherever the RB is.
|
||||
// other components should use this when accessing Rigidbody.
|
||||
public Rigidbody predictedRigidbody;
|
||||
Transform predictedRigidbodyTransform; // predictedRigidbody.transform for performance (Get/SetPositionAndRotation)
|
||||
|
||||
Vector3 lastPosition;
|
||||
|
||||
// motion smoothing happen on-demand, because it requires moving physics components to another GameObject.
|
||||
// this only starts at a given velocity and ends when stopped moving.
|
||||
// to avoid constant on/off/on effects, it also stays on for a minimum time.
|
||||
[Header("Motion Smoothing")]
|
||||
[Tooltip("Prediction supports two different modes: Smooth and Fast:\n\nSmooth: Physics are separated from the GameObject & applied in the background. Rendering smoothly follows the physics for perfectly smooth interpolation results. Much softer, can be even too soft where sharp collisions won't look as sharp (i.e. Billiard balls avoid the wall before even hitting it).\n\nFast: Physics remain on the GameObject and corrections are applied hard. Much faster since we don't need to update a separate GameObject, a bit harsher, more precise.")]
|
||||
public PredictionMode mode = PredictionMode.Smooth;
|
||||
[Tooltip("Smoothing via Ghost-following only happens on demand, while moving with a minimum velocity.")]
|
||||
public float motionSmoothingVelocityThreshold = 0.1f;
|
||||
float motionSmoothingVelocityThresholdSqr; // ² cached in Awake
|
||||
public float motionSmoothingAngularVelocityThreshold = 5.0f; // Billiards demo: 0.1 is way too small, takes forever for IsMoving()==false
|
||||
float motionSmoothingAngularVelocityThresholdSqr; // ² cached in Awake
|
||||
public float motionSmoothingTimeTolerance = 0.5f;
|
||||
double motionSmoothingLastMovedTime;
|
||||
|
||||
// client keeps state history for correction & reconciliation.
|
||||
// this needs to be a SortedList because we need to be able to insert inbetween.
|
||||
// => RingBuffer: see prediction_ringbuffer_2 branch, but it's slower!
|
||||
[Header("State History")]
|
||||
public int stateHistoryLimit = 32; // 32 x 50 ms = 1.6 seconds is definitely enough
|
||||
readonly SortedList<double, RigidbodyState> stateHistory = new SortedList<double, RigidbodyState>();
|
||||
public float recordInterval = 0.050f;
|
||||
|
||||
[Tooltip("(Optional) performance optimization where FixedUpdate.RecordState() only inserts state into history if the state actually changed.\nThis is generally a good idea.")]
|
||||
public bool onlyRecordChanges = true;
|
||||
|
||||
[Tooltip("(Optional) performance optimization where received state is compared to the LAST recorded state first, before sampling the whole history.\n\nThis can save significant traversal overhead for idle objects with a tiny chance of missing corrections for objects which revisisted the same position in the recent history twice.")]
|
||||
public bool compareLastFirst = true;
|
||||
|
||||
[Header("Reconciliation")]
|
||||
[Tooltip("Correction threshold in meters. For example, 0.1 means that if the client is off by more than 10cm, it gets corrected.")]
|
||||
public double positionCorrectionThreshold = 0.10;
|
||||
double positionCorrectionThresholdSqr; // ² cached in Awake
|
||||
[Tooltip("Correction threshold in degrees. For example, 5 means that if the client is off by more than 5 degrees, it gets corrected.")]
|
||||
public double rotationCorrectionThreshold = 5;
|
||||
|
||||
[Tooltip("Applying server corrections one frame ahead gives much better results. We don't know why yet, so this is an option for now.")]
|
||||
public bool oneFrameAhead = true;
|
||||
|
||||
[Header("Smoothing")]
|
||||
[Tooltip("Snap to the server state directly when velocity is < threshold. This is useful to reduce jitter/fighting effects before coming to rest.\nNote this applies position, rotation and velocity(!) so it's still smooth.")]
|
||||
public float snapThreshold = 2; // 0.5 has too much fighting-at-rest, 2 seems ideal.
|
||||
|
||||
[Header("Visual Interpolation")]
|
||||
[Tooltip("After creating the visual interpolation object, keep showing the original Rigidbody with a ghost (transparent) material for debugging.")]
|
||||
public bool showGhost = true;
|
||||
[Tooltip("Physics components are moved onto a ghost object beyond this threshold. Main object visually interpolates to it.")]
|
||||
public float ghostVelocityThreshold = 0.1f;
|
||||
|
||||
[Tooltip("After creating the visual interpolation object, replace this object's renderer materials with the ghost (ideally transparent) material.")]
|
||||
public Material localGhostMaterial;
|
||||
public Material remoteGhostMaterial;
|
||||
|
||||
[Tooltip("Performance optimization: only create/destroy ghosts every n-th frame is enough.")]
|
||||
public int checkGhostsEveryNthFrame = 4;
|
||||
|
||||
[Tooltip("How fast to interpolate to the target position, relative to how far we are away from it.\nHigher value will be more jitter but sharper moves, lower value will be less jitter but a little too smooth / rounded moves.")]
|
||||
public float positionInterpolationSpeed = 15; // 10 is a little too low for billiards at least
|
||||
public float rotationInterpolationSpeed = 10;
|
||||
|
||||
[Tooltip("Teleport if we are further than 'multiplier x collider size' behind.")]
|
||||
public float teleportDistanceMultiplier = 10;
|
||||
|
||||
[Header("Bandwidth")]
|
||||
[Tooltip("Reduce sends while velocity==0. Client's objects may slightly move due to gravity/physics, so we still want to send corrections occasionally even if an object is idle on the server the whole time.")]
|
||||
public bool reduceSendsWhileIdle = true;
|
||||
|
||||
// Rigidbody & Collider are moved out into a separate object.
|
||||
// this way the visual object can smoothly follow.
|
||||
protected GameObject physicsCopy;
|
||||
// protected Transform physicsCopyTransform; // caching to avoid GetComponent
|
||||
// protected Rigidbody physicsCopyRigidbody => rb; // caching to avoid GetComponent
|
||||
// protected Collider physicsCopyCollider; // caching to avoid GetComponent
|
||||
float smoothFollowThreshold; // caching to avoid calculation in LateUpdate
|
||||
float smoothFollowThresholdSqr; // caching to avoid calculation in LateUpdate
|
||||
|
||||
// we also create one extra ghost for the exact known server state.
|
||||
protected GameObject remoteCopy;
|
||||
|
||||
// joints
|
||||
Vector3 initialPosition;
|
||||
Quaternion initialRotation;
|
||||
// Vector3 initialScale; // don't change scale for now. causes issues with parenting.
|
||||
|
||||
Color originalColor;
|
||||
|
||||
protected virtual void Awake()
|
||||
{
|
||||
tf = transform;
|
||||
predictedRigidbody = GetComponent<Rigidbody>();
|
||||
if (predictedRigidbody == null) throw new InvalidOperationException($"Prediction: {name} is missing a Rigidbody component.");
|
||||
predictedRigidbodyTransform = predictedRigidbody.transform;
|
||||
|
||||
// in fast mode, we need to force enable Rigidbody.interpolation.
|
||||
// otherwise there's not going to be any smoothing whatsoever.
|
||||
if (mode == PredictionMode.Fast)
|
||||
{
|
||||
predictedRigidbody.interpolation = RigidbodyInterpolation.Interpolate;
|
||||
}
|
||||
|
||||
// cache some threshold to avoid calculating them in LateUpdate
|
||||
float colliderSize = GetComponentInChildren<Collider>().bounds.size.magnitude;
|
||||
smoothFollowThreshold = colliderSize * teleportDistanceMultiplier;
|
||||
smoothFollowThresholdSqr = smoothFollowThreshold * smoothFollowThreshold;
|
||||
|
||||
// cache initial position/rotation/scale to be used when moving physics components (configurable joints' range of motion)
|
||||
initialPosition = tf.position;
|
||||
initialRotation = tf.rotation;
|
||||
// initialScale = tf.localScale;
|
||||
|
||||
// cache ² computations
|
||||
motionSmoothingVelocityThresholdSqr = motionSmoothingVelocityThreshold * motionSmoothingVelocityThreshold;
|
||||
motionSmoothingAngularVelocityThresholdSqr = motionSmoothingAngularVelocityThreshold * motionSmoothingAngularVelocityThreshold;
|
||||
positionCorrectionThresholdSqr = positionCorrectionThreshold * positionCorrectionThreshold;
|
||||
}
|
||||
|
||||
protected virtual void CopyRenderersAsGhost(GameObject destination, Material material)
|
||||
{
|
||||
// find the MeshRenderer component, which sometimes is on a child.
|
||||
MeshRenderer originalMeshRenderer = GetComponentInChildren<MeshRenderer>(true);
|
||||
MeshFilter originalMeshFilter = GetComponentInChildren<MeshFilter>(true);
|
||||
if (originalMeshRenderer != null && originalMeshFilter != null)
|
||||
{
|
||||
MeshFilter meshFilter = destination.AddComponent<MeshFilter>();
|
||||
meshFilter.mesh = originalMeshFilter.mesh;
|
||||
|
||||
MeshRenderer meshRenderer = destination.AddComponent<MeshRenderer>();
|
||||
meshRenderer.material = originalMeshRenderer.material;
|
||||
|
||||
// renderers often have multiple materials. copy all.
|
||||
if (originalMeshRenderer.materials != null)
|
||||
{
|
||||
Material[] materials = new Material[originalMeshRenderer.materials.Length];
|
||||
for (int i = 0; i < materials.Length; ++i)
|
||||
{
|
||||
materials[i] = material;
|
||||
}
|
||||
meshRenderer.materials = materials; // need to reassign to see it in effect
|
||||
}
|
||||
}
|
||||
// if we didn't find a renderer, show a warning
|
||||
else Debug.LogWarning($"PredictedRigidbody: {name} found no renderer to copy onto the visual object. If you are using a custom setup, please overwrite PredictedRigidbody.CreateVisualCopy().");
|
||||
}
|
||||
|
||||
// instantiate a physics-only copy of the gameobject to apply corrections.
|
||||
// this way the main visual object can smoothly follow.
|
||||
// it's best to separate the physics instead of separating the renderers.
|
||||
// some projects have complex rendering / animation setups which we can't touch.
|
||||
// besides, Rigidbody+Collider are two components, where as renders may be many.
|
||||
protected virtual void CreateGhosts()
|
||||
{
|
||||
// skip if host mode or already separated
|
||||
if (isServer || physicsCopy != null) return;
|
||||
|
||||
// Debug.Log($"Separating Physics for {name}"); // logging this allocates too much
|
||||
|
||||
// create an empty GameObject with the same name + _Physical
|
||||
// it's important to copy world position/rotation/scale, not local!
|
||||
// because the original object may be a child of another.
|
||||
//
|
||||
// for example:
|
||||
// parent (scale=1.5)
|
||||
// child (scale=0.5)
|
||||
//
|
||||
// if we copy localScale then the copy has scale=0.5, where as the
|
||||
// original would have a global scale of ~1.0.
|
||||
physicsCopy = new GameObject($"{name}_Physical");
|
||||
|
||||
// assign the same Layer for the physics copy.
|
||||
// games may use a custom physics collision matrix, layer matters.
|
||||
physicsCopy.layer = gameObject.layer;
|
||||
|
||||
// add the PredictedRigidbodyPhysical component
|
||||
PredictedRigidbodyPhysicsGhost physicsGhostRigidbody = physicsCopy.AddComponent<PredictedRigidbodyPhysicsGhost>();
|
||||
physicsGhostRigidbody.target = tf;
|
||||
|
||||
// when moving (Configurable)Joints, their range of motion is
|
||||
// relative to the initial position. if we move them after the
|
||||
// GameObject rotated, the range of motion is wrong.
|
||||
// the easiest solution is to move to initial position,
|
||||
// then move physics components, then move back.
|
||||
// => remember previous
|
||||
Vector3 position = tf.position;
|
||||
Quaternion rotation = tf.rotation;
|
||||
// Vector3 scale = tf.localScale; // don't change scale for now. causes issues with parenting.
|
||||
// => reset to initial
|
||||
physicsGhostRigidbody.transform.position = tf.position = initialPosition;
|
||||
physicsGhostRigidbody.transform.rotation = tf.rotation = initialRotation;
|
||||
physicsGhostRigidbody.transform.localScale = tf.lossyScale;// world scale! // = initialScale; // don't change scale for now. causes issues with parenting.
|
||||
// => move physics components
|
||||
PredictionUtils.MovePhysicsComponents(gameObject, physicsCopy);
|
||||
// => reset previous
|
||||
physicsGhostRigidbody.transform.position = tf.position = position;
|
||||
physicsGhostRigidbody.transform.rotation = tf.rotation = rotation;
|
||||
//physicsGhostRigidbody.transform.localScale = tf.lossyScale; // world scale! //= scale; // don't change scale for now. causes issues with parenting.
|
||||
|
||||
// show ghost by copying all renderers / materials with ghost material applied
|
||||
if (showGhost)
|
||||
{
|
||||
// one for the locally predicted rigidbody
|
||||
CopyRenderersAsGhost(physicsCopy, localGhostMaterial);
|
||||
|
||||
// one for the latest remote state for comparison
|
||||
// it's important to copy world position/rotation/scale, not local!
|
||||
// because the original object may be a child of another.
|
||||
//
|
||||
// for example:
|
||||
// parent (scale=1.5)
|
||||
// child (scale=0.5)
|
||||
//
|
||||
// if we copy localScale then the copy has scale=0.5, where as the
|
||||
// original would have a global scale of ~1.0.
|
||||
remoteCopy = new GameObject($"{name}_Remote");
|
||||
remoteCopy.transform.position = tf.position; // world position!
|
||||
remoteCopy.transform.rotation = tf.rotation; // world rotation!
|
||||
remoteCopy.transform.localScale = tf.lossyScale; // world scale!
|
||||
CopyRenderersAsGhost(remoteCopy, remoteGhostMaterial);
|
||||
}
|
||||
|
||||
// assign our Rigidbody reference to the ghost
|
||||
predictedRigidbody = physicsCopy.GetComponent<Rigidbody>();
|
||||
predictedRigidbodyTransform = predictedRigidbody.transform;
|
||||
}
|
||||
|
||||
protected virtual void DestroyGhosts()
|
||||
{
|
||||
// move the copy's Rigidbody back onto self.
|
||||
// important for scene objects which may be reused for AOI spawn/despawn.
|
||||
// otherwise next time they wouldn't have a collider anymore.
|
||||
if (physicsCopy != null)
|
||||
{
|
||||
// when moving (Configurable)Joints, their range of motion is
|
||||
// relative to the initial position. if we move them after the
|
||||
// GameObject rotated, the range of motion is wrong.
|
||||
// the easiest solution is to move to initial position,
|
||||
// then move physics components, then move back.
|
||||
// => remember previous
|
||||
Vector3 position = tf.position;
|
||||
Quaternion rotation = tf.rotation;
|
||||
Vector3 scale = tf.localScale;
|
||||
// => reset to initial
|
||||
physicsCopy.transform.position = tf.position = initialPosition;
|
||||
physicsCopy.transform.rotation = tf.rotation = initialRotation;
|
||||
physicsCopy.transform.localScale = tf.lossyScale;// = initialScale;
|
||||
// => move physics components
|
||||
PredictionUtils.MovePhysicsComponents(physicsCopy, gameObject);
|
||||
// => reset previous
|
||||
tf.position = position;
|
||||
tf.rotation = rotation;
|
||||
tf.localScale = scale;
|
||||
|
||||
// when moving components back, we need to undo the joints initial-delta rotation that we added.
|
||||
Destroy(physicsCopy);
|
||||
|
||||
// reassign our Rigidbody reference
|
||||
predictedRigidbody = GetComponent<Rigidbody>();
|
||||
predictedRigidbodyTransform = predictedRigidbody.transform;
|
||||
}
|
||||
|
||||
// simply destroy the remote copy
|
||||
if (remoteCopy != null) Destroy(remoteCopy);
|
||||
}
|
||||
|
||||
// this shows in profiler LateUpdates! need to make this as fast as possible!
|
||||
protected virtual void SmoothFollowPhysicsCopy()
|
||||
{
|
||||
// hard follow:
|
||||
// predictedRigidbodyTransform.GetPositionAndRotation(out Vector3 physicsPosition, out Quaternion physicsRotation);
|
||||
// tf.SetPositionAndRotation(physicsPosition, physicsRotation);
|
||||
|
||||
// ORIGINAL VERSION: CLEAN AND SIMPLE
|
||||
/*
|
||||
// if we are further than N colliders sizes behind, then teleport
|
||||
float colliderSize = physicsCopyCollider.bounds.size.magnitude;
|
||||
float threshold = colliderSize * teleportDistanceMultiplier;
|
||||
float distance = Vector3.Distance(tf.position, physicsCopyRigidbody.position);
|
||||
if (distance > threshold)
|
||||
{
|
||||
tf.position = physicsCopyRigidbody.position;
|
||||
tf.rotation = physicsCopyRigidbody.rotation;
|
||||
Debug.Log($"[PredictedRigidbody] Teleported because distance to physics copy = {distance:F2} > threshold {threshold:F2}");
|
||||
return;
|
||||
}
|
||||
|
||||
// smoothly interpolate to the target position.
|
||||
// speed relative to how far away we are
|
||||
float positionStep = distance * positionInterpolationSpeed;
|
||||
tf.position = Vector3.MoveTowards(tf.position, physicsCopyRigidbody.position, positionStep * Time.deltaTime);
|
||||
|
||||
// smoothly interpolate to the target rotation.
|
||||
// Quaternion.RotateTowards doesn't seem to work at all, so let's use SLerp.
|
||||
tf.rotation = Quaternion.Slerp(tf.rotation, physicsCopyRigidbody.rotation, rotationInterpolationSpeed * Time.deltaTime);
|
||||
*/
|
||||
|
||||
// FAST VERSION: this shows in profiler a lot, so cache EVERYTHING!
|
||||
tf.GetPositionAndRotation(out Vector3 currentPosition, out Quaternion currentRotation); // faster than tf.position + tf.rotation
|
||||
predictedRigidbodyTransform.GetPositionAndRotation(out Vector3 physicsPosition, out Quaternion physicsRotation); // faster than Rigidbody .position and .rotation
|
||||
float deltaTime = Time.deltaTime;
|
||||
|
||||
// slow and simple version:
|
||||
// float distance = Vector3.Distance(currentPosition, physicsPosition);
|
||||
// if (distance > smoothFollowThreshold)
|
||||
// faster version
|
||||
Vector3 delta = physicsPosition - currentPosition;
|
||||
float sqrDistance = Vector3.SqrMagnitude(delta);
|
||||
float distance = Mathf.Sqrt(sqrDistance);
|
||||
if (sqrDistance > smoothFollowThresholdSqr)
|
||||
{
|
||||
tf.SetPositionAndRotation(physicsPosition, physicsRotation); // faster than .position and .rotation manually
|
||||
Debug.Log($"[PredictedRigidbody] Teleported because distance to physics copy = {distance:F2} > threshold {smoothFollowThreshold:F2}");
|
||||
return;
|
||||
}
|
||||
|
||||
// smoothly interpolate to the target position.
|
||||
// speed relative to how far away we are.
|
||||
// => speed increases by distance² because the further away, the
|
||||
// sooner we need to catch the fuck up
|
||||
// float positionStep = (distance * distance) * interpolationSpeed;
|
||||
float positionStep = distance * positionInterpolationSpeed;
|
||||
|
||||
Vector3 newPosition = MoveTowardsCustom(currentPosition, physicsPosition, delta, sqrDistance, distance, positionStep * deltaTime);
|
||||
|
||||
// smoothly interpolate to the target rotation.
|
||||
// Quaternion.RotateTowards doesn't seem to work at all, so let's use SLerp.
|
||||
// Quaternions always need to be normalized in order to be a valid rotation after operations
|
||||
Quaternion newRotation = Quaternion.Slerp(currentRotation, physicsRotation, rotationInterpolationSpeed * deltaTime).normalized;
|
||||
|
||||
// assign position and rotation together. faster than accessing manually.
|
||||
tf.SetPositionAndRotation(newPosition, newRotation);
|
||||
}
|
||||
|
||||
// simple and slow version with MoveTowards, which recalculates delta and delta.sqrMagnitude:
|
||||
// Vector3 newPosition = Vector3.MoveTowards(currentPosition, physicsPosition, positionStep * deltaTime);
|
||||
// faster version copied from MoveTowards:
|
||||
// this increases Prediction Benchmark Client's FPS from 615 -> 640.
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
static Vector3 MoveTowardsCustom(
|
||||
Vector3 current,
|
||||
Vector3 target,
|
||||
Vector3 _delta, // pass this in since we already calculated it
|
||||
float _sqrDistance, // pass this in since we already calculated it
|
||||
float _distance, // pass this in since we already calculated it
|
||||
float maxDistanceDelta)
|
||||
{
|
||||
if (_sqrDistance == 0.0 || maxDistanceDelta >= 0.0 && _sqrDistance <= maxDistanceDelta * maxDistanceDelta)
|
||||
return target;
|
||||
|
||||
float distFactor = maxDistanceDelta / _distance; // unlike Vector3.MoveTowards, we only calculate this once
|
||||
return new Vector3(
|
||||
// current.x + (_delta.x / _distance) * maxDistanceDelta,
|
||||
// current.y + (_delta.y / _distance) * maxDistanceDelta,
|
||||
// current.z + (_delta.z / _distance) * maxDistanceDelta);
|
||||
current.x + _delta.x * distFactor,
|
||||
current.y + _delta.y * distFactor,
|
||||
current.z + _delta.z * distFactor);
|
||||
}
|
||||
|
||||
// destroy visual copy only in OnStopClient().
|
||||
// OnDestroy() wouldn't be called for scene objects that are only disabled instead of destroyed.
|
||||
public override void OnStopClient()
|
||||
{
|
||||
DestroyGhosts();
|
||||
}
|
||||
|
||||
void UpdateServer()
|
||||
{
|
||||
// bandwidth optimization while idle.
|
||||
if (reduceSendsWhileIdle)
|
||||
{
|
||||
// while moving, always sync every frame for immediate corrections.
|
||||
// while idle, only sync once per second.
|
||||
//
|
||||
// we still need to sync occasionally because objects on client
|
||||
// may still slide or move slightly due to gravity, physics etc.
|
||||
// and those still need to get corrected if not moving on server.
|
||||
//
|
||||
// TODO
|
||||
// next round of optimizations: if client received nothing for 1s,
|
||||
// force correct to last received state. then server doesn't need
|
||||
// to send once per second anymore.
|
||||
syncInterval = IsMoving() ? 0 : 1;
|
||||
}
|
||||
|
||||
// always set dirty to always serialize in next sync interval.
|
||||
SetDirty();
|
||||
}
|
||||
|
||||
// movement detection is virtual, in case projects want to use other methods.
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected virtual bool IsMoving() =>
|
||||
// straight forward implementation
|
||||
// predictedRigidbody.velocity.magnitude >= motionSmoothingVelocityThreshold ||
|
||||
// predictedRigidbody.angularVelocity.magnitude >= motionSmoothingAngularVelocityThreshold;
|
||||
// faster implementation with cached ²
|
||||
predictedRigidbody.velocity.sqrMagnitude >= motionSmoothingVelocityThresholdSqr ||
|
||||
predictedRigidbody.angularVelocity.sqrMagnitude >= motionSmoothingAngularVelocityThresholdSqr;
|
||||
|
||||
// TODO maybe merge the IsMoving() checks & callbacks with UpdateState().
|
||||
void UpdateGhosting()
|
||||
{
|
||||
// perf: enough to check ghosts every few frames.
|
||||
// PredictionBenchmark: only checking every 4th frame: 585 => 600 FPS
|
||||
if (Time.frameCount % checkGhostsEveryNthFrame != 0) return;
|
||||
|
||||
// client only uses ghosts on demand while interacting.
|
||||
// this way 1000 GameObjects don't need +1000 Ghost GameObjects all the time!
|
||||
|
||||
// no ghost at the moment
|
||||
if (physicsCopy == null)
|
||||
{
|
||||
// faster than velocity threshold? then create the ghosts.
|
||||
// with 10% buffer zone so we don't flip flop all the time.
|
||||
if (IsMoving())
|
||||
{
|
||||
CreateGhosts();
|
||||
OnBeginPrediction();
|
||||
}
|
||||
}
|
||||
// ghosting at the moment
|
||||
else
|
||||
{
|
||||
// always set last moved time while moving.
|
||||
// this way we can avoid on/off/oneffects when stopping.
|
||||
if (IsMoving())
|
||||
{
|
||||
motionSmoothingLastMovedTime = NetworkTime.time;
|
||||
}
|
||||
// slower than velocity threshold? then destroy the ghosts.
|
||||
// with a minimum time since starting to move, to avoid on/off/on effects.
|
||||
else
|
||||
{
|
||||
if (NetworkTime.time >= motionSmoothingLastMovedTime + motionSmoothingTimeTolerance)
|
||||
{
|
||||
DestroyGhosts();
|
||||
OnEndPrediction();
|
||||
physicsCopy = null; // TESTING
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// when using Fast mode, we don't create any ghosts.
|
||||
// but we still want to check IsMoving() in order to support the same
|
||||
// user callbacks.
|
||||
bool lastMoving = false;
|
||||
void UpdateState()
|
||||
{
|
||||
// perf: enough to check ghosts every few frames.
|
||||
// PredictionBenchmark: only checking every 4th frame: 770 => 800 FPS
|
||||
if (Time.frameCount % checkGhostsEveryNthFrame != 0) return;
|
||||
|
||||
bool moving = IsMoving();
|
||||
|
||||
// started moving?
|
||||
if (moving && !lastMoving)
|
||||
{
|
||||
OnBeginPrediction();
|
||||
lastMoving = true;
|
||||
}
|
||||
// stopped moving?
|
||||
else if (!moving && lastMoving)
|
||||
{
|
||||
// ensure a minimum time since starting to move, to avoid on/off/on effects.
|
||||
if (NetworkTime.time >= motionSmoothingLastMovedTime + motionSmoothingTimeTolerance)
|
||||
{
|
||||
OnEndPrediction();
|
||||
lastMoving = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
if (isServer) UpdateServer();
|
||||
if (isClientOnly)
|
||||
{
|
||||
if (mode == PredictionMode.Smooth)
|
||||
UpdateGhosting();
|
||||
else if (mode == PredictionMode.Fast)
|
||||
UpdateState();
|
||||
}
|
||||
}
|
||||
|
||||
void LateUpdate()
|
||||
{
|
||||
// only follow on client-only, not in server or host mode
|
||||
if (isClientOnly && mode == PredictionMode.Smooth && physicsCopy) SmoothFollowPhysicsCopy();
|
||||
}
|
||||
|
||||
void FixedUpdate()
|
||||
{
|
||||
// on clients (not host) we record the current state every FixedUpdate.
|
||||
// this is cheap, and allows us to keep a dense history.
|
||||
if (!isClientOnly) return;
|
||||
|
||||
// OPTIMIZATION: RecordState() is expensive because it inserts into a SortedList.
|
||||
// only record if state actually changed!
|
||||
// risks not having up to date states when correcting,
|
||||
// but it doesn't matter since we'll always compare with the 'newest' anyway.
|
||||
//
|
||||
// we check in here instead of in RecordState() because RecordState() should definitely record if we call it!
|
||||
if (onlyRecordChanges)
|
||||
{
|
||||
// TODO maybe don't reuse the correction thresholds?
|
||||
tf.GetPositionAndRotation(out Vector3 position, out Quaternion rotation);
|
||||
// clean & simple:
|
||||
// if (Vector3.Distance(lastRecorded.position, position) < positionCorrectionThreshold &&
|
||||
// Quaternion.Angle(lastRecorded.rotation, rotation) < rotationCorrectionThreshold)
|
||||
// faster:
|
||||
if ((lastRecorded.position - position).sqrMagnitude < positionCorrectionThresholdSqr &&
|
||||
Quaternion.Angle(lastRecorded.rotation, rotation) < rotationCorrectionThreshold)
|
||||
{
|
||||
// Debug.Log($"FixedUpdate for {name}: taking optimized early return instead of recording state.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
RecordState();
|
||||
}
|
||||
|
||||
// manually store last recorded so we can easily check against this
|
||||
// without traversing the SortedList.
|
||||
RigidbodyState lastRecorded;
|
||||
double lastRecordTime;
|
||||
void RecordState()
|
||||
{
|
||||
// performance optimization: only call NetworkTime.time getter once
|
||||
double networkTime = NetworkTime.time;
|
||||
|
||||
// instead of recording every fixedupdate, let's record in an interval.
|
||||
// we don't want to record every tiny move and correct too hard.
|
||||
if (networkTime < lastRecordTime + recordInterval) return;
|
||||
lastRecordTime = networkTime;
|
||||
|
||||
// NetworkTime.time is always behind by bufferTime.
|
||||
// prediction aims to be on the exact same server time (immediately).
|
||||
// use predictedTime to record state, otherwise we would record in the past.
|
||||
double predictedTime = NetworkTime.predictedTime;
|
||||
|
||||
// FixedUpdate may run twice in the same frame / NetworkTime.time.
|
||||
// for now, simply don't record if already recorded there.
|
||||
// previously we checked ContainsKey which is O(logN) for SortedList
|
||||
// if (stateHistory.ContainsKey(predictedTime))
|
||||
// return;
|
||||
// instead, simply store the last recorded time and don't insert if same.
|
||||
if (predictedTime == lastRecorded.timestamp) return;
|
||||
|
||||
// keep state history within limit
|
||||
if (stateHistory.Count >= stateHistoryLimit)
|
||||
stateHistory.RemoveAt(0);
|
||||
|
||||
// grab current position/rotation/velocity only once.
|
||||
// this is performance critical, avoid calling .transform multiple times.
|
||||
tf.GetPositionAndRotation(out Vector3 currentPosition, out Quaternion currentRotation); // faster than accessing .position + .rotation manually
|
||||
Vector3 currentVelocity = predictedRigidbody.velocity;
|
||||
Vector3 currentAngularVelocity = predictedRigidbody.angularVelocity;
|
||||
|
||||
// calculate delta to previous state (if any)
|
||||
Vector3 positionDelta = Vector3.zero;
|
||||
Vector3 velocityDelta = Vector3.zero;
|
||||
Vector3 angularVelocityDelta = Vector3.zero;
|
||||
Quaternion rotationDelta = Quaternion.identity;
|
||||
int stateHistoryCount = stateHistory.Count; // perf: only grab .Count once
|
||||
if (stateHistoryCount > 0)
|
||||
{
|
||||
RigidbodyState last = stateHistory.Values[stateHistoryCount - 1];
|
||||
positionDelta = currentPosition - last.position;
|
||||
velocityDelta = currentVelocity - last.velocity;
|
||||
// Quaternions always need to be normalized in order to be valid rotations after operations
|
||||
rotationDelta = (currentRotation * Quaternion.Inverse(last.rotation)).normalized;
|
||||
angularVelocityDelta = currentAngularVelocity - last.angularVelocity;
|
||||
|
||||
// debug draw the recorded state
|
||||
// Debug.DrawLine(last.position, currentPosition, Color.red, lineTime);
|
||||
}
|
||||
|
||||
// create state to insert
|
||||
RigidbodyState state = new RigidbodyState(
|
||||
predictedTime,
|
||||
positionDelta,
|
||||
currentPosition,
|
||||
rotationDelta,
|
||||
currentRotation,
|
||||
velocityDelta,
|
||||
currentVelocity,
|
||||
angularVelocityDelta,
|
||||
currentAngularVelocity
|
||||
);
|
||||
|
||||
// add state to history
|
||||
stateHistory.Add(predictedTime, state);
|
||||
|
||||
// manually remember last inserted state for faster .Last comparisons
|
||||
lastRecorded = state;
|
||||
}
|
||||
|
||||
// optional user callbacks, in case people need to know about events.
|
||||
protected virtual void OnSnappedIntoPlace() {}
|
||||
protected virtual void OnBeforeApplyState() {}
|
||||
protected virtual void OnCorrected() {}
|
||||
protected virtual void OnBeginPrediction() {} // when the Rigidbody moved above threshold and we created a ghost
|
||||
protected virtual void OnEndPrediction() {} // when the Rigidbody came to rest and we destroyed the ghost
|
||||
|
||||
void ApplyState(double timestamp, Vector3 position, Quaternion rotation, Vector3 velocity, Vector3 angularVelocity)
|
||||
{
|
||||
// fix rigidbodies seemingly dancing in place instead of coming to rest.
|
||||
// hard snap to the position below a threshold velocity.
|
||||
// this is fine because the visual object still smoothly interpolates to it.
|
||||
// => consider both velocity and angular velocity (in case of Rigidbodies only rotating with joints etc.)
|
||||
if (predictedRigidbody.velocity.magnitude <= snapThreshold &&
|
||||
predictedRigidbody.angularVelocity.magnitude <= snapThreshold)
|
||||
{
|
||||
// Debug.Log($"Prediction: snapped {name} into place because velocity {predictedRigidbody.velocity.magnitude:F3} <= {snapThreshold:F3}");
|
||||
|
||||
// apply server state immediately.
|
||||
// important to apply velocity as well, instead of Vector3.zero.
|
||||
// in case an object is still slightly moving, we don't want it
|
||||
// to stop and start moving again on client - slide as well here.
|
||||
predictedRigidbody.position = position;
|
||||
predictedRigidbody.rotation = rotation;
|
||||
// projects may keep Rigidbodies as kinematic sometimes. in that case, setting velocity would log an error
|
||||
if (!predictedRigidbody.isKinematic)
|
||||
{
|
||||
predictedRigidbody.velocity = velocity;
|
||||
predictedRigidbody.angularVelocity = angularVelocity;
|
||||
}
|
||||
|
||||
// clear history and insert the exact state we just applied.
|
||||
// this makes future corrections more accurate.
|
||||
stateHistory.Clear();
|
||||
stateHistory.Add(timestamp, new RigidbodyState(
|
||||
timestamp,
|
||||
Vector3.zero,
|
||||
position,
|
||||
Quaternion.identity,
|
||||
rotation,
|
||||
Vector3.zero,
|
||||
velocity,
|
||||
Vector3.zero,
|
||||
angularVelocity
|
||||
));
|
||||
|
||||
// user callback
|
||||
OnSnappedIntoPlace();
|
||||
return;
|
||||
}
|
||||
|
||||
// we have a callback for snapping into place (above).
|
||||
// we also need one for corrections without snapping into place.
|
||||
// call it before applying pos/rot/vel in case we need to set kinematic etc.
|
||||
OnBeforeApplyState();
|
||||
|
||||
// apply the state to the Rigidbody
|
||||
if (mode == PredictionMode.Smooth)
|
||||
{
|
||||
// Smooth mode separates Physics from Renderering.
|
||||
// Rendering smoothly follows Physics in SmoothFollowPhysicsCopy().
|
||||
// this allows us to be able to hard teleport to the correction.
|
||||
// which gives most accurate results since the Rigidbody can't
|
||||
// be stopped by another object when trying to correct.
|
||||
predictedRigidbody.position = position;
|
||||
predictedRigidbody.rotation = rotation;
|
||||
}
|
||||
else if (mode == PredictionMode.Fast)
|
||||
{
|
||||
// Fast mode doesn't separate physics from rendering.
|
||||
// The only smoothing we get is from Rigidbody.MovePosition.
|
||||
predictedRigidbody.MovePosition(position);
|
||||
predictedRigidbody.MoveRotation(rotation);
|
||||
}
|
||||
|
||||
// there's only one way to set velocity.
|
||||
// (projects may keep Rigidbodies as kinematic sometimes. in that case, setting velocity would log an error)
|
||||
if (!predictedRigidbody.isKinematic)
|
||||
{
|
||||
predictedRigidbody.velocity = velocity;
|
||||
predictedRigidbody.angularVelocity = angularVelocity;
|
||||
}
|
||||
}
|
||||
|
||||
// process a received server state.
|
||||
// compares it against our history and applies corrections if needed.
|
||||
void OnReceivedState(double timestamp, RigidbodyState state)//, bool sleeping)
|
||||
{
|
||||
// always update remote state ghost
|
||||
if (remoteCopy != null)
|
||||
{
|
||||
Transform remoteCopyTransform = remoteCopy.transform;
|
||||
remoteCopyTransform.SetPositionAndRotation(state.position, state.rotation); // faster than .position + .rotation setters
|
||||
remoteCopyTransform.localScale = tf.lossyScale; // world scale! see CreateGhosts comment.
|
||||
}
|
||||
|
||||
|
||||
// DO NOT SYNC SLEEPING! this cuts benchmark performance in half(!!!)
|
||||
// color code remote sleeping objects to debug objects coming to rest
|
||||
// if (showRemoteSleeping)
|
||||
// {
|
||||
// rend.material.color = sleeping ? Color.gray : originalColor;
|
||||
// }
|
||||
|
||||
// performance: get Rigidbody position & rotation only once,
|
||||
// and together via its transform
|
||||
predictedRigidbodyTransform.GetPositionAndRotation(out Vector3 physicsPosition, out Quaternion physicsRotation);
|
||||
|
||||
// OPTIONAL performance optimization when comparing idle objects.
|
||||
// even idle objects will have a history of ~32 entries.
|
||||
// sampling & traversing through them is unnecessarily costly.
|
||||
// instead, compare directly against the current rigidbody position!
|
||||
// => this is technically not 100% correct if an object runs in
|
||||
// circles where it may revisit the same position twice.
|
||||
// => but practically, objects that didn't move will have their
|
||||
// whole history look like the last inserted state.
|
||||
// => comparing against that is free and gives us a significant
|
||||
// performance saving vs. a tiny chance of incorrect results due
|
||||
// to objects running in circles.
|
||||
// => the RecordState() call below is expensive too, so we want to
|
||||
// do this before even recording the latest state. the only way
|
||||
// to do this (in case last recorded state is too old), is to
|
||||
// compare against live rigidbody.position without any recording.
|
||||
// this is as fast as it gets for skipping idle objects.
|
||||
//
|
||||
// if this ever causes issues, feel free to disable it.
|
||||
float positionToStateDistanceSqr = Vector3.SqrMagnitude(state.position - physicsPosition);
|
||||
if (compareLastFirst &&
|
||||
// Vector3.Distance(state.position, physicsPosition) < positionCorrectionThreshold && // slow comparison
|
||||
positionToStateDistanceSqr < positionCorrectionThresholdSqr && // fast comparison
|
||||
Quaternion.Angle(state.rotation, physicsRotation) < rotationCorrectionThreshold)
|
||||
{
|
||||
// Debug.Log($"OnReceivedState for {name}: taking optimized early return!");
|
||||
return;
|
||||
}
|
||||
|
||||
// we only capture state every 'interval' milliseconds.
|
||||
// so the newest entry in 'history' may be up to 'interval' behind 'now'.
|
||||
// if there's no latency, we may receive a server state for 'now'.
|
||||
// sampling would fail, if we haven't recorded anything in a while.
|
||||
// to solve this, always record the current state when receiving a server state.
|
||||
RecordState();
|
||||
|
||||
// correction requires at least 2 existing states for 'before' and 'after'.
|
||||
// if we don't have two yet, drop this state and try again next time once we recorded more.
|
||||
if (stateHistory.Count < 2) return;
|
||||
|
||||
RigidbodyState oldest = stateHistory.Values[0];
|
||||
RigidbodyState newest = stateHistory.Values[stateHistory.Count - 1];
|
||||
|
||||
// edge case: is the state older than the oldest state in history?
|
||||
// this can happen if the client gets so far behind the server
|
||||
// that it doesn't have a recored history to sample from.
|
||||
// in that case, we should hard correct the client.
|
||||
// otherwise it could be out of sync as long as it's too far behind.
|
||||
if (state.timestamp < oldest.timestamp)
|
||||
{
|
||||
// when starting, client may only have 2-3 states in history.
|
||||
// it's expected that server states would be behind those 2-3.
|
||||
// only show a warning if it's behind the full history limit!
|
||||
if (stateHistory.Count >= stateHistoryLimit)
|
||||
Debug.LogWarning($"Hard correcting client object {name} because the client is too far behind the server. History of size={stateHistory.Count} @ t={timestamp:F3} oldest={oldest.timestamp:F3} newest={newest.timestamp:F3}. This would cause the client to be out of sync as long as it's behind.");
|
||||
|
||||
// force apply the state
|
||||
ApplyState(state.timestamp, state.position, state.rotation, state.velocity, state.angularVelocity);
|
||||
return;
|
||||
}
|
||||
|
||||
// edge case: is it newer than the newest state in history?
|
||||
// this can happen if client's predictedTime predicts too far ahead of the server.
|
||||
// in that case, log a warning for now but still apply the correction.
|
||||
// otherwise it could be out of sync as long as it's too far ahead.
|
||||
//
|
||||
// for example, when running prediction on the same machine with near zero latency.
|
||||
// when applying corrections here, this looks just fine on the local machine.
|
||||
if (newest.timestamp < state.timestamp)
|
||||
{
|
||||
// the correction is for a state in the future.
|
||||
// we clamp it to 'now'.
|
||||
// but only correct if off by threshold.
|
||||
// TODO maybe we should interpolate this back to 'now'?
|
||||
// if (Vector3.Distance(state.position, physicsPosition) >= positionCorrectionThreshold) // slow comparison
|
||||
if (positionToStateDistanceSqr >= positionCorrectionThresholdSqr) // fast comparison
|
||||
{
|
||||
// this can happen a lot when latency is ~0. logging all the time allocates too much and is too slow.
|
||||
// double ahead = state.timestamp - newest.timestamp;
|
||||
// Debug.Log($"Hard correction because the client is ahead of the server by {(ahead*1000):F1}ms. History of size={stateHistory.Count} @ t={timestamp:F3} oldest={oldest.timestamp:F3} newest={newest.timestamp:F3}. This can happen when latency is near zero, and is fine unless it shows jitter.");
|
||||
ApplyState(state.timestamp, state.position, state.rotation, state.velocity, state.angularVelocity);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// find the two closest client states between timestamp
|
||||
if (!Prediction.Sample(stateHistory, timestamp, out RigidbodyState before, out RigidbodyState after, out int afterIndex, out double t))
|
||||
{
|
||||
// something went very wrong. sampling should've worked.
|
||||
// hard correct to recover the error.
|
||||
Debug.LogError($"Failed to sample history of size={stateHistory.Count} @ t={timestamp:F3} oldest={oldest.timestamp:F3} newest={newest.timestamp:F3}. This should never happen because the timestamp is within history.");
|
||||
ApplyState(state.timestamp, state.position, state.rotation, state.velocity, state.angularVelocity);
|
||||
return;
|
||||
}
|
||||
|
||||
// interpolate between them to get the best approximation
|
||||
RigidbodyState interpolated = RigidbodyState.Interpolate(before, after, (float)t);
|
||||
|
||||
// calculate the difference between where we were and where we should be
|
||||
// TODO only position for now. consider rotation etc. too later
|
||||
// float positionToInterpolatedDistance = Vector3.Distance(state.position, interpolated.position); // slow comparison
|
||||
float positionToInterpolatedDistanceSqr = Vector3.SqrMagnitude(state.position - interpolated.position); // fast comparison
|
||||
float rotationToInterpolatedDistance = Quaternion.Angle(state.rotation, interpolated.rotation);
|
||||
// Debug.Log($"Sampled history of size={stateHistory.Count} @ {timestamp:F3}: client={interpolated.position} server={state.position} difference={difference:F3} / {correctionThreshold:F3}");
|
||||
|
||||
// too far off? then correct it
|
||||
if (positionToInterpolatedDistanceSqr >= positionCorrectionThresholdSqr || // fast comparison
|
||||
//positionToInterpolatedDistance >= positionCorrectionThreshold || // slow comparison
|
||||
rotationToInterpolatedDistance >= rotationCorrectionThreshold)
|
||||
{
|
||||
// Debug.Log($"CORRECTION NEEDED FOR {name} @ {timestamp:F3}: client={interpolated.position} server={state.position} difference={difference:F3}");
|
||||
|
||||
// show the received correction position + velocity for debugging.
|
||||
// helps to compare with the interpolated/applied correction locally.
|
||||
//Debug.DrawLine(state.position, state.position + state.velocity * 0.1f, Color.white, lineTime);
|
||||
|
||||
// insert the correction and correct the history on top of it.
|
||||
// returns the final recomputed state after rewinding.
|
||||
RigidbodyState recomputed = Prediction.CorrectHistory(stateHistory, stateHistoryLimit, state, before, after, afterIndex);
|
||||
|
||||
// log, draw & apply the final position.
|
||||
// always do this here, not when iterating above, in case we aren't iterating.
|
||||
// for example, on same machine with near zero latency.
|
||||
// int correctedAmount = stateHistory.Count - afterIndex;
|
||||
// Debug.Log($"Correcting {name}: {correctedAmount} / {stateHistory.Count} states to final position from: {rb.position} to: {last.position}");
|
||||
//Debug.DrawLine(physicsCopyRigidbody.position, recomputed.position, Color.green, lineTime);
|
||||
ApplyState(recomputed.timestamp, recomputed.position, recomputed.rotation, recomputed.velocity, recomputed.angularVelocity);
|
||||
|
||||
// user callback
|
||||
OnCorrected();
|
||||
}
|
||||
}
|
||||
|
||||
// send state to clients every sendInterval.
|
||||
// reliable for now.
|
||||
// TODO we should use the one from FixedUpdate
|
||||
public override void OnSerialize(NetworkWriter writer, bool initialState)
|
||||
{
|
||||
// Time.time was at the beginning of this frame.
|
||||
// NetworkLateUpdate->Broadcast->OnSerialize is at the end of the frame.
|
||||
// as result, client should use this to correct the _next_ frame.
|
||||
// otherwise we see noticeable resets that seem off by one frame.
|
||||
//
|
||||
// to solve this, we can send the current deltaTime.
|
||||
// server is technically supposed to be at a fixed frame rate, but this can vary.
|
||||
// sending server's current deltaTime is the safest option.
|
||||
// client then applies it on top of remoteTimestamp.
|
||||
|
||||
|
||||
// FAST VERSION: this shows in profiler a lot, so cache EVERYTHING!
|
||||
tf.GetPositionAndRotation(out Vector3 position, out Quaternion rotation); // faster than tf.position + tf.rotation. server's rigidbody is on the same transform.
|
||||
|
||||
// simple but slow write:
|
||||
// writer.WriteFloat(Time.deltaTime);
|
||||
// writer.WriteVector3(position);
|
||||
// writer.WriteQuaternion(rotation);
|
||||
// writer.WriteVector3(predictedRigidbody.velocity);
|
||||
// writer.WriteVector3(predictedRigidbody.angularVelocity);
|
||||
|
||||
// performance optimization: write a whole struct at once via blittable:
|
||||
PredictedSyncData data = new PredictedSyncData(
|
||||
Time.deltaTime,
|
||||
position,
|
||||
rotation,
|
||||
predictedRigidbody.velocity,
|
||||
predictedRigidbody.angularVelocity);//,
|
||||
// DO NOT SYNC SLEEPING! this cuts benchmark performance in half(!!!)
|
||||
// predictedRigidbody.IsSleeping());
|
||||
writer.WritePredictedSyncData(data);
|
||||
}
|
||||
|
||||
// read the server's state, compare with client state & correct if necessary.
|
||||
public override void OnDeserialize(NetworkReader reader, bool initialState)
|
||||
{
|
||||
// deserialize data
|
||||
// we want to know the time on the server when this was sent, which is remoteTimestamp.
|
||||
double timestamp = NetworkClient.connection.remoteTimeStamp;
|
||||
|
||||
// simple but slow read:
|
||||
// double serverDeltaTime = reader.ReadFloat();
|
||||
// Vector3 position = reader.ReadVector3();
|
||||
// Quaternion rotation = reader.ReadQuaternion();
|
||||
// Vector3 velocity = reader.ReadVector3();
|
||||
// Vector3 angularVelocity = reader.ReadVector3();
|
||||
|
||||
// performance optimization: read a whole struct at once via blittable:
|
||||
PredictedSyncData data = reader.ReadPredictedSyncData();
|
||||
double serverDeltaTime = data.deltaTime;
|
||||
Vector3 position = data.position;
|
||||
Quaternion rotation = data.rotation;
|
||||
Vector3 velocity = data.velocity;
|
||||
Vector3 angularVelocity = data.angularVelocity;
|
||||
// DO NOT SYNC SLEEPING! this cuts benchmark performance in half(!!!)
|
||||
// bool sleeping = data.sleeping != 0;
|
||||
|
||||
// server sends state at the end of the frame.
|
||||
// parse and apply the server's delta time to our timestamp.
|
||||
// otherwise we see noticeable resets that seem off by one frame.
|
||||
timestamp += serverDeltaTime;
|
||||
|
||||
// however, adding yet one more frame delay gives much(!) better results.
|
||||
// we don't know why yet, so keep this as an option for now.
|
||||
// possibly because client captures at the beginning of the frame,
|
||||
// with physics happening at the end of the frame?
|
||||
if (oneFrameAhead) timestamp += serverDeltaTime;
|
||||
|
||||
// process received state
|
||||
OnReceivedState(timestamp, new RigidbodyState(timestamp, Vector3.zero, position, Quaternion.identity, rotation, Vector3.zero, velocity, Vector3.zero, angularVelocity));//, sleeping);
|
||||
}
|
||||
|
||||
protected override void OnValidate()
|
||||
{
|
||||
base.OnValidate();
|
||||
|
||||
// force syncDirection to be ServerToClient
|
||||
syncDirection = SyncDirection.ServerToClient;
|
||||
|
||||
// state should be synced immediately for now.
|
||||
// later when we have prediction fully dialed in,
|
||||
// then we can maybe relax this a bit.
|
||||
syncInterval = 0;
|
||||
}
|
||||
|
||||
// helper function for Physics tests to check if a Rigidbody belongs to
|
||||
// a PredictedRigidbody component (either on it, or on its ghost).
|
||||
public static bool IsPredicted(Rigidbody rb, out PredictedRigidbody predictedRigidbody)
|
||||
{
|
||||
// by default, Rigidbody is on the PredictedRigidbody GameObject
|
||||
if (rb.TryGetComponent(out predictedRigidbody))
|
||||
return true;
|
||||
|
||||
// it might be on a ghost while interacting
|
||||
if (rb.TryGetComponent(out PredictedRigidbodyPhysicsGhost ghost))
|
||||
{
|
||||
predictedRigidbody = ghost.target.GetComponent<PredictedRigidbody>();
|
||||
return true;
|
||||
}
|
||||
|
||||
// otherwise the Rigidbody does not belong to any PredictedRigidbody.
|
||||
predictedRigidbody = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
// helper function for Physics tests to check if a Collider (which may be in children) belongs to
|
||||
// a PredictedRigidbody component (either on it, or on its ghost).
|
||||
public static bool IsPredicted(Collider co, out PredictedRigidbody predictedRigidbody)
|
||||
{
|
||||
// by default, Collider is on the PredictedRigidbody GameObject or it's children.
|
||||
predictedRigidbody = co.GetComponentInParent<PredictedRigidbody>();
|
||||
if (predictedRigidbody != null)
|
||||
return true;
|
||||
|
||||
// it might be on a ghost while interacting
|
||||
PredictedRigidbodyPhysicsGhost ghost = co.GetComponentInParent<PredictedRigidbodyPhysicsGhost>();
|
||||
if (ghost != null && ghost.target != null && ghost.target.TryGetComponent(out predictedRigidbody))
|
||||
return true;
|
||||
|
||||
// otherwise the Rigidbody does not belong to any PredictedRigidbody.
|
||||
predictedRigidbody = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d38927cdc6024b9682b5fe9778b9ef99
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences:
|
||||
- localGhostMaterial: {fileID: 2100000, guid: 411a48b4a197d4924bec3e3809bc9320,
|
||||
type: 2}
|
||||
- remoteGhostMaterial: {fileID: 2100000, guid: 04f0b2088c857414393bab3b80356776,
|
||||
type: 2}
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -0,0 +1,15 @@
|
||||
// Prediction moves out the Rigidbody & Collider into a separate object.
|
||||
// this component simply points back to the owner component.
|
||||
// in case Raycasts hit it and need to know the owner, etc.
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
public class PredictedRigidbodyPhysicsGhost : MonoBehaviour
|
||||
{
|
||||
// this is performance critical, so store target's .Transform instead of
|
||||
// PredictedRigidbody, this way we don't need to call the .transform getter.
|
||||
[Tooltip("The predicted rigidbody owner.")]
|
||||
public Transform target;
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 25593abc9bf0d44878a4ad6018204061
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -0,0 +1 @@
|
||||
// removed 2024-02-09
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 62e7e9424c7e48d69b6a3517796142a1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -0,0 +1,54 @@
|
||||
// this struct exists only for OnDe/Serialize performance.
|
||||
// instead of WriteVector3+Quaternion+Vector3+Vector3,
|
||||
// we read & write the whole struct as blittable once.
|
||||
//
|
||||
// struct packing can cause odd results with blittable on different platforms,
|
||||
// so this is usually not recommended!
|
||||
//
|
||||
// in this case however, we need to squeeze everything we can out of prediction
|
||||
// to support low even devices / VR.
|
||||
using System.Runtime.InteropServices;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
// struct packing
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)] // explicitly force sequential
|
||||
public struct PredictedSyncData
|
||||
{
|
||||
public float deltaTime; // 4 bytes (word aligned)
|
||||
public Vector3 position; // 12 bytes (word aligned)
|
||||
public Quaternion rotation; // 16 bytes (word aligned)
|
||||
public Vector3 velocity; // 12 bytes (word aligned)
|
||||
public Vector3 angularVelocity; // 12 bytes (word aligned)
|
||||
// DO NOT SYNC SLEEPING! this cuts benchmark performance in half(!!!)
|
||||
// public byte sleeping; // 1 byte: bool isn't blittable
|
||||
|
||||
// constructor for convenience
|
||||
public PredictedSyncData(float deltaTime, Vector3 position, Quaternion rotation, Vector3 velocity, Vector3 angularVelocity)//, bool sleeping)
|
||||
{
|
||||
this.deltaTime = deltaTime;
|
||||
this.position = position;
|
||||
this.rotation = rotation;
|
||||
this.velocity = velocity;
|
||||
this.angularVelocity = angularVelocity;
|
||||
// DO NOT SYNC SLEEPING! this cuts benchmark performance in half(!!!)
|
||||
// this.sleeping = sleeping ? (byte)1 : (byte)0;
|
||||
}
|
||||
}
|
||||
|
||||
// NetworkReader/Writer extensions to write this struct
|
||||
public static class PredictedSyncDataReadWrite
|
||||
{
|
||||
public static void WritePredictedSyncData(this NetworkWriter writer, PredictedSyncData data)
|
||||
{
|
||||
writer.WriteBlittable(data);
|
||||
}
|
||||
|
||||
public static PredictedSyncData ReadPredictedSyncData(this NetworkReader reader)
|
||||
{
|
||||
return reader.ReadBlittable<PredictedSyncData>();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f595f112a39e4634b670d56991b23823
|
||||
timeCreated: 1710387026
|
419
Assets/Mirror/Components/PredictedRigidbody/PredictionUtils.cs
Normal file
419
Assets/Mirror/Components/PredictedRigidbody/PredictionUtils.cs
Normal file
@ -0,0 +1,419 @@
|
||||
// standalone utility functions for PredictedRigidbody component.
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
public static class PredictionUtils
|
||||
{
|
||||
// rigidbody ///////////////////////////////////////////////////////////
|
||||
// move a Rigidbody + settings from one GameObject to another.
|
||||
public static void MoveRigidbody(GameObject source, GameObject destination)
|
||||
{
|
||||
// create a new Rigidbody component on destination.
|
||||
// note that adding a Joint automatically adds a Rigidbody.
|
||||
// so first check if one was added yet.
|
||||
Rigidbody original = source.GetComponent<Rigidbody>();
|
||||
if (original == null) throw new Exception($"Prediction: attempted to move {source}'s Rigidbody to the predicted copy, but there was no component.");
|
||||
Rigidbody rigidbodyCopy;
|
||||
if (!destination.TryGetComponent(out rigidbodyCopy))
|
||||
rigidbodyCopy = destination.AddComponent<Rigidbody>();
|
||||
|
||||
// copy all properties
|
||||
rigidbodyCopy.mass = original.mass;
|
||||
rigidbodyCopy.drag = original.drag;
|
||||
rigidbodyCopy.angularDrag = original.angularDrag;
|
||||
rigidbodyCopy.useGravity = original.useGravity;
|
||||
rigidbodyCopy.isKinematic = original.isKinematic;
|
||||
rigidbodyCopy.interpolation = original.interpolation;
|
||||
rigidbodyCopy.collisionDetectionMode = original.collisionDetectionMode;
|
||||
rigidbodyCopy.constraints = original.constraints;
|
||||
rigidbodyCopy.sleepThreshold = original.sleepThreshold;
|
||||
rigidbodyCopy.freezeRotation = original.freezeRotation;
|
||||
|
||||
// moving (Configurable)Joints messes up their range of motion unless
|
||||
// we reset to initial position first (we do this in PredictedRigibody.cs).
|
||||
// so here we don't set the Rigidbody's physics position at all.
|
||||
// rigidbodyCopy.position = original.position;
|
||||
// rigidbodyCopy.rotation = original.rotation;
|
||||
|
||||
// projects may keep Rigidbodies as kinematic sometimes. in that case, setting velocity would log an error
|
||||
if (!original.isKinematic)
|
||||
{
|
||||
rigidbodyCopy.velocity = original.velocity;
|
||||
rigidbodyCopy.angularVelocity = original.angularVelocity;
|
||||
}
|
||||
|
||||
// destroy original
|
||||
GameObject.Destroy(original);
|
||||
}
|
||||
|
||||
// helper function: if a collider is on a child, copy that child first.
|
||||
// this way child's relative position/rotation/scale are preserved.
|
||||
public static GameObject CopyRelativeTransform(GameObject source, Transform sourceChild, GameObject destination)
|
||||
{
|
||||
// is this on the source root? then we want to put it on the destination root.
|
||||
if (sourceChild == source.transform) return destination;
|
||||
|
||||
// is this on a child? then create the same child with the same transform on destination.
|
||||
// note this is technically only correct for the immediate child since
|
||||
// .localPosition is relative to parent, but this is good enough.
|
||||
GameObject child = new GameObject(sourceChild.name);
|
||||
child.transform.SetParent(destination.transform, true);
|
||||
child.transform.localPosition = sourceChild.localPosition;
|
||||
child.transform.localRotation = sourceChild.localRotation;
|
||||
child.transform.localScale = sourceChild.localScale;
|
||||
|
||||
// assign the same Layer for the physics copy.
|
||||
// games may use a custom physics collision matrix, layer matters.
|
||||
child.layer = sourceChild.gameObject.layer;
|
||||
|
||||
return child;
|
||||
}
|
||||
|
||||
// colliders ///////////////////////////////////////////////////////////
|
||||
// move all BoxColliders + settings from one GameObject to another.
|
||||
public static void MoveBoxColliders(GameObject source, GameObject destination)
|
||||
{
|
||||
// colliders may be on children
|
||||
BoxCollider[] sourceColliders = source.GetComponentsInChildren<BoxCollider>();
|
||||
foreach (BoxCollider sourceCollider in sourceColliders)
|
||||
{
|
||||
// copy the relative transform:
|
||||
// if collider is on root, it returns destination root.
|
||||
// if collider is on a child, it creates and returns a child on destination.
|
||||
GameObject target = CopyRelativeTransform(source, sourceCollider.transform, destination);
|
||||
BoxCollider colliderCopy = target.AddComponent<BoxCollider>();
|
||||
colliderCopy.center = sourceCollider.center;
|
||||
colliderCopy.size = sourceCollider.size;
|
||||
colliderCopy.isTrigger = sourceCollider.isTrigger;
|
||||
colliderCopy.material = sourceCollider.material;
|
||||
GameObject.Destroy(sourceCollider);
|
||||
}
|
||||
}
|
||||
|
||||
// move all SphereColliders + settings from one GameObject to another.
|
||||
public static void MoveSphereColliders(GameObject source, GameObject destination)
|
||||
{
|
||||
// colliders may be on children
|
||||
SphereCollider[] sourceColliders = source.GetComponentsInChildren<SphereCollider>();
|
||||
foreach (SphereCollider sourceCollider in sourceColliders)
|
||||
{
|
||||
// copy the relative transform:
|
||||
// if collider is on root, it returns destination root.
|
||||
// if collider is on a child, it creates and returns a child on destination.
|
||||
GameObject target = CopyRelativeTransform(source, sourceCollider.transform, destination);
|
||||
SphereCollider colliderCopy = target.AddComponent<SphereCollider>();
|
||||
colliderCopy.center = sourceCollider.center;
|
||||
colliderCopy.radius = sourceCollider.radius;
|
||||
colliderCopy.isTrigger = sourceCollider.isTrigger;
|
||||
colliderCopy.material = sourceCollider.material;
|
||||
GameObject.Destroy(sourceCollider);
|
||||
}
|
||||
}
|
||||
|
||||
// move all CapsuleColliders + settings from one GameObject to another.
|
||||
public static void MoveCapsuleColliders(GameObject source, GameObject destination)
|
||||
{
|
||||
// colliders may be on children
|
||||
CapsuleCollider[] sourceColliders = source.GetComponentsInChildren<CapsuleCollider>();
|
||||
foreach (CapsuleCollider sourceCollider in sourceColliders)
|
||||
{
|
||||
// copy the relative transform:
|
||||
// if collider is on root, it returns destination root.
|
||||
// if collider is on a child, it creates and returns a child on destination.
|
||||
GameObject target = CopyRelativeTransform(source, sourceCollider.transform, destination);
|
||||
CapsuleCollider colliderCopy = target.AddComponent<CapsuleCollider>();
|
||||
colliderCopy.center = sourceCollider.center;
|
||||
colliderCopy.radius = sourceCollider.radius;
|
||||
colliderCopy.height = sourceCollider.height;
|
||||
colliderCopy.direction = sourceCollider.direction;
|
||||
colliderCopy.isTrigger = sourceCollider.isTrigger;
|
||||
colliderCopy.material = sourceCollider.material;
|
||||
GameObject.Destroy(sourceCollider);
|
||||
}
|
||||
}
|
||||
|
||||
// move all MeshColliders + settings from one GameObject to another.
|
||||
public static void MoveMeshColliders(GameObject source, GameObject destination)
|
||||
{
|
||||
// colliders may be on children
|
||||
MeshCollider[] sourceColliders = source.GetComponentsInChildren<MeshCollider>();
|
||||
foreach (MeshCollider sourceCollider in sourceColliders)
|
||||
{
|
||||
// when Models have Mesh->Read/Write disabled, it means that Unity
|
||||
// uploads the mesh directly to the GPU and erases it on the CPU.
|
||||
// on some platforms this makes moving a MeshCollider in builds impossible:
|
||||
//
|
||||
// "CollisionMeshData couldn't be created because the mesh has been marked as non-accessible."
|
||||
//
|
||||
// on other platforms, this works fine.
|
||||
// let's show an explicit log message so in case collisions don't
|
||||
// work at runtime, it's obvious why it happens and how to fix it.
|
||||
if (!sourceCollider.sharedMesh.isReadable)
|
||||
{
|
||||
Debug.Log($"[Prediction]: MeshCollider on {sourceCollider.name} isn't readable, which may indicate that the Mesh only exists on the GPU. If {sourceCollider.name} is missing collisions, then please select the model in the Project Area, and enable Mesh->Read/Write so it's also available on the CPU!");
|
||||
// don't early return. keep trying, it may work.
|
||||
}
|
||||
|
||||
// copy the relative transform:
|
||||
// if collider is on root, it returns destination root.
|
||||
// if collider is on a child, it creates and returns a child on destination.
|
||||
GameObject target = CopyRelativeTransform(source, sourceCollider.transform, destination);
|
||||
MeshCollider colliderCopy = target.AddComponent<MeshCollider>();
|
||||
colliderCopy.sharedMesh = sourceCollider.sharedMesh;
|
||||
colliderCopy.convex = sourceCollider.convex;
|
||||
colliderCopy.isTrigger = sourceCollider.isTrigger;
|
||||
colliderCopy.material = sourceCollider.material;
|
||||
GameObject.Destroy(sourceCollider);
|
||||
}
|
||||
}
|
||||
|
||||
// move all Colliders + settings from one GameObject to another.
|
||||
public static void MoveAllColliders(GameObject source, GameObject destination)
|
||||
{
|
||||
MoveBoxColliders(source, destination);
|
||||
MoveSphereColliders(source, destination);
|
||||
MoveCapsuleColliders(source, destination);
|
||||
MoveMeshColliders(source, destination);
|
||||
}
|
||||
|
||||
// joints //////////////////////////////////////////////////////////////
|
||||
// move all CharacterJoints + settings from one GameObject to another.
|
||||
public static void MoveCharacterJoints(GameObject source, GameObject destination)
|
||||
{
|
||||
// colliders may be on children
|
||||
CharacterJoint[] sourceJoints = source.GetComponentsInChildren<CharacterJoint>();
|
||||
foreach (CharacterJoint sourceJoint in sourceJoints)
|
||||
{
|
||||
// copy the relative transform:
|
||||
// if joint is on root, it returns destination root.
|
||||
// if joint is on a child, it creates and returns a child on destination.
|
||||
GameObject target = CopyRelativeTransform(source, sourceJoint.transform, destination);
|
||||
CharacterJoint jointCopy = target.AddComponent<CharacterJoint>();
|
||||
// apply settings, in alphabetical order
|
||||
jointCopy.anchor = sourceJoint.anchor;
|
||||
jointCopy.autoConfigureConnectedAnchor = sourceJoint.autoConfigureConnectedAnchor;
|
||||
jointCopy.axis = sourceJoint.axis;
|
||||
jointCopy.breakForce = sourceJoint.breakForce;
|
||||
jointCopy.breakTorque = sourceJoint.breakTorque;
|
||||
jointCopy.connectedAnchor = sourceJoint.connectedAnchor;
|
||||
jointCopy.connectedBody = sourceJoint.connectedBody;
|
||||
jointCopy.connectedMassScale = sourceJoint.connectedMassScale;
|
||||
jointCopy.enableCollision = sourceJoint.enableCollision;
|
||||
jointCopy.enablePreprocessing = sourceJoint.enablePreprocessing;
|
||||
jointCopy.enableProjection = sourceJoint.enableProjection;
|
||||
jointCopy.highTwistLimit = sourceJoint.highTwistLimit;
|
||||
jointCopy.lowTwistLimit = sourceJoint.lowTwistLimit;
|
||||
jointCopy.massScale = sourceJoint.massScale;
|
||||
jointCopy.projectionAngle = sourceJoint.projectionAngle;
|
||||
jointCopy.projectionDistance = sourceJoint.projectionDistance;
|
||||
jointCopy.swing1Limit = sourceJoint.swing1Limit;
|
||||
jointCopy.swing2Limit = sourceJoint.swing2Limit;
|
||||
jointCopy.swingAxis = sourceJoint.swingAxis;
|
||||
jointCopy.swingLimitSpring = sourceJoint.swingLimitSpring;
|
||||
jointCopy.twistLimitSpring = sourceJoint.twistLimitSpring;
|
||||
#if UNITY_2020_3_OR_NEWER
|
||||
jointCopy.connectedArticulationBody = sourceJoint.connectedArticulationBody;
|
||||
#endif
|
||||
|
||||
GameObject.Destroy(sourceJoint);
|
||||
}
|
||||
}
|
||||
|
||||
// move all ConfigurableJoints + settings from one GameObject to another.
|
||||
public static void MoveConfigurableJoints(GameObject source, GameObject destination)
|
||||
{
|
||||
// colliders may be on children
|
||||
ConfigurableJoint[] sourceJoints = source.GetComponentsInChildren<ConfigurableJoint>();
|
||||
foreach (ConfigurableJoint sourceJoint in sourceJoints)
|
||||
{
|
||||
// copy the relative transform:
|
||||
// if joint is on root, it returns destination root.
|
||||
// if joint is on a child, it creates and returns a child on destination.
|
||||
GameObject target = CopyRelativeTransform(source, sourceJoint.transform, destination);
|
||||
ConfigurableJoint jointCopy = target.AddComponent<ConfigurableJoint>();
|
||||
// apply settings, in alphabetical order
|
||||
jointCopy.anchor = sourceJoint.anchor;
|
||||
jointCopy.angularXLimitSpring = sourceJoint.angularXLimitSpring;
|
||||
jointCopy.angularXDrive = sourceJoint.angularXDrive;
|
||||
jointCopy.angularXMotion = sourceJoint.angularXMotion;
|
||||
jointCopy.angularYLimit = sourceJoint.angularYLimit;
|
||||
jointCopy.angularYMotion = sourceJoint.angularYMotion;
|
||||
jointCopy.angularYZDrive = sourceJoint.angularYZDrive;
|
||||
jointCopy.angularYZLimitSpring = sourceJoint.angularYZLimitSpring;
|
||||
jointCopy.angularZLimit = sourceJoint.angularZLimit;
|
||||
jointCopy.angularZMotion = sourceJoint.angularZMotion;
|
||||
jointCopy.autoConfigureConnectedAnchor = sourceJoint.autoConfigureConnectedAnchor;
|
||||
jointCopy.axis = sourceJoint.axis;
|
||||
jointCopy.breakForce = sourceJoint.breakForce;
|
||||
jointCopy.breakTorque = sourceJoint.breakTorque;
|
||||
jointCopy.configuredInWorldSpace = sourceJoint.configuredInWorldSpace;
|
||||
jointCopy.connectedAnchor = sourceJoint.connectedAnchor;
|
||||
jointCopy.connectedBody = sourceJoint.connectedBody;
|
||||
jointCopy.connectedMassScale = sourceJoint.connectedMassScale;
|
||||
jointCopy.enableCollision = sourceJoint.enableCollision;
|
||||
jointCopy.enablePreprocessing = sourceJoint.enablePreprocessing;
|
||||
jointCopy.highAngularXLimit = sourceJoint.highAngularXLimit; // moving this only works if the object is at initial position/rotation/scale, see PredictedRigidbody.cs
|
||||
jointCopy.linearLimitSpring = sourceJoint.linearLimitSpring;
|
||||
jointCopy.linearLimit = sourceJoint.linearLimit;
|
||||
jointCopy.lowAngularXLimit = sourceJoint.lowAngularXLimit; // moving this only works if the object is at initial position/rotation/scale, see PredictedRigidbody.cs
|
||||
jointCopy.massScale = sourceJoint.massScale;
|
||||
jointCopy.projectionAngle = sourceJoint.projectionAngle;
|
||||
jointCopy.projectionDistance = sourceJoint.projectionDistance;
|
||||
jointCopy.projectionMode = sourceJoint.projectionMode;
|
||||
jointCopy.rotationDriveMode = sourceJoint.rotationDriveMode;
|
||||
jointCopy.secondaryAxis = sourceJoint.secondaryAxis;
|
||||
jointCopy.slerpDrive = sourceJoint.slerpDrive;
|
||||
jointCopy.swapBodies = sourceJoint.swapBodies;
|
||||
jointCopy.targetAngularVelocity = sourceJoint.targetAngularVelocity;
|
||||
jointCopy.targetPosition = sourceJoint.targetPosition;
|
||||
jointCopy.targetRotation = sourceJoint.targetRotation;
|
||||
jointCopy.targetVelocity = sourceJoint.targetVelocity;
|
||||
jointCopy.xDrive = sourceJoint.xDrive;
|
||||
jointCopy.xMotion = sourceJoint.xMotion;
|
||||
jointCopy.yDrive = sourceJoint.yDrive;
|
||||
jointCopy.yMotion = sourceJoint.yMotion;
|
||||
jointCopy.zDrive = sourceJoint.zDrive;
|
||||
jointCopy.zMotion = sourceJoint.zMotion;
|
||||
#if UNITY_2020_3_OR_NEWER
|
||||
jointCopy.connectedArticulationBody = sourceJoint.connectedArticulationBody;
|
||||
#endif
|
||||
|
||||
GameObject.Destroy(sourceJoint);
|
||||
}
|
||||
}
|
||||
|
||||
// move all FixedJoints + settings from one GameObject to another.
|
||||
public static void MoveFixedJoints(GameObject source, GameObject destination)
|
||||
{
|
||||
// colliders may be on children
|
||||
FixedJoint[] sourceJoints = source.GetComponentsInChildren<FixedJoint>();
|
||||
foreach (FixedJoint sourceJoint in sourceJoints)
|
||||
{
|
||||
// copy the relative transform:
|
||||
// if joint is on root, it returns destination root.
|
||||
// if joint is on a child, it creates and returns a child on destination.
|
||||
GameObject target = CopyRelativeTransform(source, sourceJoint.transform, destination);
|
||||
FixedJoint jointCopy = target.AddComponent<FixedJoint>();
|
||||
// apply settings, in alphabetical order
|
||||
jointCopy.anchor = sourceJoint.anchor;
|
||||
jointCopy.autoConfigureConnectedAnchor = sourceJoint.autoConfigureConnectedAnchor;
|
||||
jointCopy.axis = sourceJoint.axis;
|
||||
jointCopy.breakForce = sourceJoint.breakForce;
|
||||
jointCopy.breakTorque = sourceJoint.breakTorque;
|
||||
jointCopy.connectedAnchor = sourceJoint.connectedAnchor;
|
||||
jointCopy.connectedBody = sourceJoint.connectedBody;
|
||||
jointCopy.connectedMassScale = sourceJoint.connectedMassScale;
|
||||
jointCopy.enableCollision = sourceJoint.enableCollision;
|
||||
jointCopy.enablePreprocessing = sourceJoint.enablePreprocessing;
|
||||
jointCopy.massScale = sourceJoint.massScale;
|
||||
#if UNITY_2020_3_OR_NEWER
|
||||
jointCopy.connectedArticulationBody = sourceJoint.connectedArticulationBody;
|
||||
#endif
|
||||
|
||||
GameObject.Destroy(sourceJoint);
|
||||
}
|
||||
}
|
||||
|
||||
// move all HingeJoints + settings from one GameObject to another.
|
||||
public static void MoveHingeJoints(GameObject source, GameObject destination)
|
||||
{
|
||||
// colliders may be on children
|
||||
HingeJoint[] sourceJoints = source.GetComponentsInChildren<HingeJoint>();
|
||||
foreach (HingeJoint sourceJoint in sourceJoints)
|
||||
{
|
||||
// copy the relative transform:
|
||||
// if joint is on root, it returns destination root.
|
||||
// if joint is on a child, it creates and returns a child on destination.
|
||||
GameObject target = CopyRelativeTransform(source, sourceJoint.transform, destination);
|
||||
HingeJoint jointCopy = target.AddComponent<HingeJoint>();
|
||||
// apply settings, in alphabetical order
|
||||
jointCopy.anchor = sourceJoint.anchor;
|
||||
jointCopy.autoConfigureConnectedAnchor = sourceJoint.autoConfigureConnectedAnchor;
|
||||
jointCopy.axis = sourceJoint.axis;
|
||||
jointCopy.breakForce = sourceJoint.breakForce;
|
||||
jointCopy.breakTorque = sourceJoint.breakTorque;
|
||||
jointCopy.connectedAnchor = sourceJoint.connectedAnchor;
|
||||
jointCopy.connectedBody = sourceJoint.connectedBody;
|
||||
jointCopy.connectedMassScale = sourceJoint.connectedMassScale;
|
||||
jointCopy.enableCollision = sourceJoint.enableCollision;
|
||||
jointCopy.enablePreprocessing = sourceJoint.enablePreprocessing;
|
||||
jointCopy.limits = sourceJoint.limits;
|
||||
jointCopy.massScale = sourceJoint.massScale;
|
||||
jointCopy.motor = sourceJoint.motor;
|
||||
jointCopy.spring = sourceJoint.spring;
|
||||
jointCopy.useLimits = sourceJoint.useLimits;
|
||||
jointCopy.useMotor = sourceJoint.useMotor;
|
||||
jointCopy.useSpring = sourceJoint.useSpring;
|
||||
#if UNITY_2020_3_OR_NEWER
|
||||
jointCopy.connectedArticulationBody = sourceJoint.connectedArticulationBody;
|
||||
#endif
|
||||
#if UNITY_2022_3_OR_NEWER
|
||||
jointCopy.extendedLimits = sourceJoint.extendedLimits;
|
||||
jointCopy.useAcceleration = sourceJoint.useAcceleration;
|
||||
#endif
|
||||
|
||||
GameObject.Destroy(sourceJoint);
|
||||
}
|
||||
}
|
||||
|
||||
// move all SpringJoints + settings from one GameObject to another.
|
||||
public static void MoveSpringJoints(GameObject source, GameObject destination)
|
||||
{
|
||||
// colliders may be on children
|
||||
SpringJoint[] sourceJoints = source.GetComponentsInChildren<SpringJoint>();
|
||||
foreach (SpringJoint sourceJoint in sourceJoints)
|
||||
{
|
||||
// copy the relative transform:
|
||||
// if joint is on root, it returns destination root.
|
||||
// if joint is on a child, it creates and returns a child on destination.
|
||||
GameObject target = CopyRelativeTransform(source, sourceJoint.transform, destination);
|
||||
SpringJoint jointCopy = target.AddComponent<SpringJoint>();
|
||||
// apply settings, in alphabetical order
|
||||
jointCopy.anchor = sourceJoint.anchor;
|
||||
jointCopy.autoConfigureConnectedAnchor = sourceJoint.autoConfigureConnectedAnchor;
|
||||
jointCopy.axis = sourceJoint.axis;
|
||||
jointCopy.breakForce = sourceJoint.breakForce;
|
||||
jointCopy.breakTorque = sourceJoint.breakTorque;
|
||||
jointCopy.connectedAnchor = sourceJoint.connectedAnchor;
|
||||
jointCopy.connectedBody = sourceJoint.connectedBody;
|
||||
jointCopy.connectedMassScale = sourceJoint.connectedMassScale;
|
||||
jointCopy.damper = sourceJoint.damper;
|
||||
jointCopy.enableCollision = sourceJoint.enableCollision;
|
||||
jointCopy.enablePreprocessing = sourceJoint.enablePreprocessing;
|
||||
jointCopy.massScale = sourceJoint.massScale;
|
||||
jointCopy.maxDistance = sourceJoint.maxDistance;
|
||||
jointCopy.minDistance = sourceJoint.minDistance;
|
||||
jointCopy.spring = sourceJoint.spring;
|
||||
jointCopy.tolerance = sourceJoint.tolerance;
|
||||
#if UNITY_2020_3_OR_NEWER
|
||||
jointCopy.connectedArticulationBody = sourceJoint.connectedArticulationBody;
|
||||
#endif
|
||||
|
||||
GameObject.Destroy(sourceJoint);
|
||||
}
|
||||
}
|
||||
|
||||
// move all Joints + settings from one GameObject to another.
|
||||
public static void MoveAllJoints(GameObject source, GameObject destination)
|
||||
{
|
||||
MoveCharacterJoints(source, destination);
|
||||
MoveConfigurableJoints(source, destination);
|
||||
MoveFixedJoints(source, destination);
|
||||
MoveHingeJoints(source, destination);
|
||||
MoveSpringJoints(source, destination);
|
||||
}
|
||||
|
||||
// all /////////////////////////////////////////////////////////////////
|
||||
// move all physics components from one GameObject to another.
|
||||
public static void MovePhysicsComponents(GameObject source, GameObject destination)
|
||||
{
|
||||
// need to move joints first, otherwise we might see:
|
||||
// 'can't move Rigidbody because a Joint depends on it'
|
||||
MoveAllJoints(source, destination);
|
||||
MoveAllColliders(source, destination);
|
||||
MoveRigidbody(source, destination);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 17cfe1beb3f94a69b94bf60afc37ef7a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -0,0 +1,85 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!21 &2100000
|
||||
Material:
|
||||
serializedVersion: 8
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: RemoteGhostMaterial
|
||||
m_Shader: {fileID: 46, guid: 0000000000000000f000000000000000, type: 0}
|
||||
m_Parent: {fileID: 0}
|
||||
m_ModifiedSerializedProperties: 0
|
||||
m_ValidKeywords:
|
||||
- _ALPHAPREMULTIPLY_ON
|
||||
m_InvalidKeywords: []
|
||||
m_LightmapFlags: 4
|
||||
m_EnableInstancingVariants: 0
|
||||
m_DoubleSidedGI: 0
|
||||
m_CustomRenderQueue: 3000
|
||||
stringTagMap:
|
||||
RenderType: Transparent
|
||||
disabledShaderPasses: []
|
||||
m_LockedProperties:
|
||||
m_SavedProperties:
|
||||
serializedVersion: 3
|
||||
m_TexEnvs:
|
||||
- _BumpMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailAlbedoMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailMask:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailNormalMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _EmissionMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _MainTex:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _MetallicGlossMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _OcclusionMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _ParallaxMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
m_Ints: []
|
||||
m_Floats:
|
||||
- _BumpScale: 1
|
||||
- _Cutoff: 0.5
|
||||
- _DetailNormalMapScale: 1
|
||||
- _DstBlend: 10
|
||||
- _GlossMapScale: 1
|
||||
- _Glossiness: 0.92
|
||||
- _GlossyReflections: 1
|
||||
- _Metallic: 0
|
||||
- _Mode: 3
|
||||
- _OcclusionStrength: 1
|
||||
- _Parallax: 0.02
|
||||
- _SmoothnessTextureChannel: 0
|
||||
- _SpecularHighlights: 1
|
||||
- _SrcBlend: 1
|
||||
- _UVSec: 0
|
||||
- _ZWrite: 0
|
||||
m_Colors:
|
||||
- _Color: {r: 0.09849727, g: 1, b: 0, a: 0.15686275}
|
||||
- _EmissionColor: {r: 0, g: 0, b: 0, a: 1}
|
||||
m_BuildTextureStacks: []
|
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 04f0b2088c857414393bab3b80356776
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 2100000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -0,0 +1,60 @@
|
||||
// PredictedRigidbody stores a history of its rigidbody states.
|
||||
using System.Runtime.CompilerServices;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
// inline everything because this is performance critical!
|
||||
public struct RigidbodyState : PredictedState
|
||||
{
|
||||
public double timestamp { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; [MethodImpl(MethodImplOptions.AggressiveInlining)] private set; }
|
||||
|
||||
// we want to store position delta (last + delta = current), and current.
|
||||
// this way we can apply deltas on top of corrected positions to get the corrected final position.
|
||||
public Vector3 positionDelta { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; [MethodImpl(MethodImplOptions.AggressiveInlining)] set; } // delta to get from last to this position
|
||||
public Vector3 position { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; [MethodImpl(MethodImplOptions.AggressiveInlining)] set; }
|
||||
|
||||
public Quaternion rotationDelta { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; [MethodImpl(MethodImplOptions.AggressiveInlining)] set; } // delta to get from last to this rotation
|
||||
public Quaternion rotation { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; [MethodImpl(MethodImplOptions.AggressiveInlining)] set; }
|
||||
|
||||
public Vector3 velocityDelta { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; [MethodImpl(MethodImplOptions.AggressiveInlining)] set; } // delta to get from last to this velocity
|
||||
public Vector3 velocity { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; [MethodImpl(MethodImplOptions.AggressiveInlining)] set; }
|
||||
|
||||
public Vector3 angularVelocityDelta { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; [MethodImpl(MethodImplOptions.AggressiveInlining)] set; } // delta to get from last to this velocity
|
||||
public Vector3 angularVelocity { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; [MethodImpl(MethodImplOptions.AggressiveInlining)] set; }
|
||||
|
||||
public RigidbodyState(
|
||||
double timestamp,
|
||||
Vector3 positionDelta,
|
||||
Vector3 position,
|
||||
Quaternion rotationDelta,
|
||||
Quaternion rotation,
|
||||
Vector3 velocityDelta,
|
||||
Vector3 velocity,
|
||||
Vector3 angularVelocityDelta,
|
||||
Vector3 angularVelocity)
|
||||
{
|
||||
this.timestamp = timestamp;
|
||||
this.positionDelta = positionDelta;
|
||||
this.position = position;
|
||||
this.rotationDelta = rotationDelta;
|
||||
this.rotation = rotation;
|
||||
this.velocityDelta = velocityDelta;
|
||||
this.velocity = velocity;
|
||||
this.angularVelocityDelta = angularVelocityDelta;
|
||||
this.angularVelocity = angularVelocity;
|
||||
}
|
||||
|
||||
public static RigidbodyState Interpolate(RigidbodyState a, RigidbodyState b, float t)
|
||||
{
|
||||
return new RigidbodyState
|
||||
{
|
||||
position = Vector3.Lerp(a.position, b.position, t),
|
||||
// Quaternions always need to be normalized in order to be a valid rotation after operations
|
||||
rotation = Quaternion.Slerp(a.rotation, b.rotation, t).normalized,
|
||||
velocity = Vector3.Lerp(a.velocity, b.velocity, t),
|
||||
angularVelocity = Vector3.Lerp(a.angularVelocity, b.angularVelocity, t)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ed0e1c0c874c4c9db6be2d5885bb7bee
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -91,7 +91,7 @@ public class RemoteStatistics : NetworkBehaviour
|
||||
|
||||
[Header("GUI")]
|
||||
public bool showGui;
|
||||
public KeyCode hotKey = KeyCode.F11;
|
||||
public KeyCode hotKey = KeyCode.BackQuote;
|
||||
Rect windowRect = new Rect(0, 0, 400, 400);
|
||||
|
||||
// password can't be stored in code or in Unity project.
|
||||
@ -133,8 +133,9 @@ void LoadPassword()
|
||||
}
|
||||
}
|
||||
|
||||
void OnValidate()
|
||||
protected override void OnValidate()
|
||||
{
|
||||
base.OnValidate();
|
||||
syncMode = SyncMode.Owner;
|
||||
}
|
||||
|
||||
|
@ -10,3 +10,4 @@
|
||||
[assembly: InternalsVisibleTo("Mirror.Tests.Performance.Editor")]
|
||||
[assembly: InternalsVisibleTo("Mirror.Tests.Performance.Runtime")]
|
||||
[assembly: InternalsVisibleTo("Mirror.Editor")]
|
||||
[assembly: InternalsVisibleTo("Mirror.Components")]
|
||||
|
@ -4,8 +4,12 @@
|
||||
namespace Mirror
|
||||
{
|
||||
/// <summary>
|
||||
/// SyncVars are used to synchronize a variable from the server to all clients automatically.
|
||||
/// <para>Value must be changed on server, not directly by clients. Hook parameter allows you to define a client-side method to be invoked when the client gets an update from the server.</para>
|
||||
/// SyncVars are used to automatically synchronize a variable between the server and all clients. The direction of synchronization depends on the Sync Direction property, ServerToClient by default.
|
||||
/// <para>
|
||||
/// When Sync Direction is equal to ServerToClient, the value should be changed on the server side and synchronized to all clients.
|
||||
/// Otherwise, the value should be changed on the client side and synchronized to server and other clients.
|
||||
/// </para>
|
||||
/// <para>Hook parameter allows you to define a method to be invoked when gets an value update. Notice that the hook method will not be called on the change side.</para>
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Field)]
|
||||
public class SyncVarAttribute : PropertyAttribute
|
||||
@ -82,4 +86,10 @@ public class SceneAttribute : PropertyAttribute {}
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Field)]
|
||||
public class ShowInInspectorAttribute : Attribute {}
|
||||
|
||||
/// <summary>
|
||||
/// Used to make a field readonly in the inspector
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Field)]
|
||||
public class ReadOnlyAttribute : PropertyAttribute {}
|
||||
}
|
||||
|
@ -29,8 +29,17 @@ public class Batcher
|
||||
// they would not contain a timestamp
|
||||
readonly int threshold;
|
||||
|
||||
// TimeStamp header size for those who need it
|
||||
public const int HeaderSize = sizeof(double);
|
||||
// TimeStamp header size. each batch has one.
|
||||
public const int TimestampSize = sizeof(double);
|
||||
|
||||
// Message header size. each message has one.
|
||||
public static int MessageHeaderSize(int messageSize) =>
|
||||
Compression.VarUIntSize((ulong)messageSize);
|
||||
|
||||
// maximum overhead for a single message.
|
||||
// useful for the outside to calculate max message sizes.
|
||||
public static int MaxMessageOverhead(int messageSize) =>
|
||||
TimestampSize + MessageHeaderSize(messageSize);
|
||||
|
||||
// full batches ready to be sent.
|
||||
// DO NOT queue NetworkMessage, it would box.
|
||||
@ -53,13 +62,17 @@ public Batcher(int threshold)
|
||||
// caller needs to make sure they are within max packet size.
|
||||
public void AddMessage(ArraySegment<byte> message, double timeStamp)
|
||||
{
|
||||
// predict the needed size, which is varint(size) + content
|
||||
int headerSize = Compression.VarUIntSize((ulong)message.Count);
|
||||
int neededSize = headerSize + message.Count;
|
||||
|
||||
// when appending to a batch in progress, check final size.
|
||||
// if it expands beyond threshold, then we should finalize it first.
|
||||
// => less than or exactly threshold is fine.
|
||||
// GetBatch() will finalize it.
|
||||
// => see unit tests.
|
||||
if (batch != null &&
|
||||
batch.Position + message.Count > threshold)
|
||||
batch.Position + neededSize > threshold)
|
||||
{
|
||||
batches.Enqueue(batch);
|
||||
batch = null;
|
||||
@ -82,6 +95,16 @@ public void AddMessage(ArraySegment<byte> message, double timeStamp)
|
||||
// -> we do allow > threshold sized messages as single batch
|
||||
// -> WriteBytes instead of WriteSegment because the latter
|
||||
// would add a size header. we want to write directly.
|
||||
//
|
||||
// include size prefix as varint!
|
||||
// -> fixes NetworkMessage serialization mismatch corrupting the
|
||||
// next message in a batch.
|
||||
// -> a _lot_ of time was wasted debugging corrupt batches.
|
||||
// no easy way to figure out which NetworkMessage has a mismatch.
|
||||
// -> this is worth everyone's sanity.
|
||||
// -> varint means we prefix with 1 byte most of the time.
|
||||
// -> the same issue in NetworkIdentity was why Mirror started!
|
||||
Compression.CompressVarUInt(batch, (ulong)message.Count);
|
||||
batch.WriteBytes(message.Array, message.Offset, message.Count);
|
||||
}
|
||||
|
||||
@ -123,5 +146,22 @@ public bool GetBatch(NetworkWriter writer)
|
||||
// nothing was written
|
||||
return false;
|
||||
}
|
||||
|
||||
// return all batches to the pool for cleanup
|
||||
public void Clear()
|
||||
{
|
||||
// return batch in progress
|
||||
if (batch != null)
|
||||
{
|
||||
NetworkWriterPool.Return(batch);
|
||||
batch = null;
|
||||
}
|
||||
|
||||
// return all queued batches
|
||||
foreach (NetworkWriterPooled queued in batches)
|
||||
NetworkWriterPool.Return(queued);
|
||||
|
||||
batches.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -14,13 +14,13 @@ public class Unbatcher
|
||||
{
|
||||
// supporting adding multiple batches before GetNextMessage is called.
|
||||
// just in case.
|
||||
Queue<NetworkWriterPooled> batches = new Queue<NetworkWriterPooled>();
|
||||
readonly Queue<NetworkWriterPooled> batches = new Queue<NetworkWriterPooled>();
|
||||
|
||||
public int BatchesCount => batches.Count;
|
||||
|
||||
// NetworkReader is only created once,
|
||||
// then pointed to the first batch.
|
||||
NetworkReader reader = new NetworkReader(new byte[0]);
|
||||
readonly NetworkReader reader = new NetworkReader(new byte[0]);
|
||||
|
||||
// timestamp that was written into the batch remotely.
|
||||
// for the batch that our reader is currently pointed at.
|
||||
@ -48,7 +48,7 @@ public bool AddBatch(ArraySegment<byte> batch)
|
||||
// don't need to check against that.
|
||||
|
||||
// make sure we have at least 8 bytes to read for tick timestamp
|
||||
if (batch.Count < Batcher.HeaderSize)
|
||||
if (batch.Count < Batcher.TimestampSize)
|
||||
return false;
|
||||
|
||||
// put into a (pooled) writer
|
||||
@ -69,43 +69,22 @@ public bool AddBatch(ArraySegment<byte> batch)
|
||||
}
|
||||
|
||||
// get next message, unpacked from batch (if any)
|
||||
// message ArraySegment is only valid until the next call.
|
||||
// timestamp is the REMOTE time when the batch was created remotely.
|
||||
public bool GetNextMessage(out NetworkReader message, out double remoteTimeStamp)
|
||||
public bool GetNextMessage(out ArraySegment<byte> message, out double remoteTimeStamp)
|
||||
{
|
||||
// getting messages would be easy via
|
||||
// <<size, message, size, message, ...>>
|
||||
// but to save A LOT of bandwidth, we use
|
||||
// <<message, message, ...>
|
||||
// in other words, we don't know where the current message ends
|
||||
//
|
||||
// BUT: it doesn't matter!
|
||||
// -> we simply return the reader
|
||||
// * if we have one yet
|
||||
// * and if there's more to read
|
||||
// -> the caller can then read one message from it
|
||||
// -> when the end is reached, we retire the batch!
|
||||
//
|
||||
// for example:
|
||||
// while (GetNextMessage(out message))
|
||||
// ProcessMessage(message);
|
||||
//
|
||||
message = null;
|
||||
message = default;
|
||||
remoteTimeStamp = 0;
|
||||
|
||||
// do nothing if we don't have any batches.
|
||||
// otherwise the below queue.Dequeue() would throw an
|
||||
// InvalidOperationException if operating on empty queue.
|
||||
if (batches.Count == 0)
|
||||
{
|
||||
remoteTimeStamp = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
// was our reader pointed to anything yet?
|
||||
if (reader.Capacity == 0)
|
||||
{
|
||||
remoteTimeStamp = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
// no more data to read?
|
||||
if (reader.Remaining == 0)
|
||||
@ -123,19 +102,27 @@ public bool GetNextMessage(out NetworkReader message, out double remoteTimeStamp
|
||||
StartReadingBatch(next);
|
||||
}
|
||||
// otherwise there's nothing more to read
|
||||
else
|
||||
{
|
||||
remoteTimeStamp = 0;
|
||||
return false;
|
||||
}
|
||||
else return false;
|
||||
}
|
||||
|
||||
// use the current batch's remote timestamp
|
||||
// AFTER potentially moving to the next batch ABOVE!
|
||||
remoteTimeStamp = readerRemoteTimeStamp;
|
||||
|
||||
// if we got here, then we have more data to read.
|
||||
message = reader;
|
||||
// enough data to read the size prefix?
|
||||
if (reader.Remaining == 0)
|
||||
return false;
|
||||
|
||||
// read the size prefix as varint
|
||||
// see Batcher.AddMessage comments for explanation.
|
||||
int size = (int)Compression.DecompressVarUInt(reader);
|
||||
|
||||
// validate size prefix, in case attackers send malicious data
|
||||
if (reader.Remaining < size)
|
||||
return false;
|
||||
|
||||
// return the message of size
|
||||
message = reader.ReadBytesSegment(size);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
74
Assets/Mirror/Core/ConnectionQuality.cs
Normal file
74
Assets/Mirror/Core/ConnectionQuality.cs
Normal file
@ -0,0 +1,74 @@
|
||||
// standalone, Unity-independent connection-quality algorithm & enum.
|
||||
// don't need to use this directly, it's built into Mirror's NetworkClient.
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
public enum ConnectionQuality : byte
|
||||
{
|
||||
ESTIMATING, // still estimating
|
||||
POOR, // unplayable
|
||||
FAIR, // very noticeable latency, not very enjoyable anymore
|
||||
GOOD, // very playable for everyone but high level competitors
|
||||
EXCELLENT // ideal experience for high level competitors
|
||||
}
|
||||
|
||||
public enum ConnectionQualityMethod : byte
|
||||
{
|
||||
Simple, // simple estimation based on rtt and jitter
|
||||
Pragmatic // based on snapshot interpolation adjustment
|
||||
}
|
||||
|
||||
// provide different heuristics for users to choose from.
|
||||
// simple heuristics to get started.
|
||||
// this will be iterated on over time based on user feedback.
|
||||
public static class ConnectionQualityHeuristics
|
||||
{
|
||||
// convenience extension to color code Connection Quality
|
||||
public static Color ColorCode(this ConnectionQuality quality)
|
||||
{
|
||||
switch (quality)
|
||||
{
|
||||
case ConnectionQuality.POOR: return Color.red;
|
||||
case ConnectionQuality.FAIR: return new Color(1.0f, 0.647f, 0.0f);
|
||||
case ConnectionQuality.GOOD: return Color.yellow;
|
||||
case ConnectionQuality.EXCELLENT: return Color.green;
|
||||
default: return Color.gray; // ESTIMATING
|
||||
}
|
||||
}
|
||||
|
||||
// straight forward estimation
|
||||
// rtt: average round trip time in seconds.
|
||||
// jitter: average latency variance.
|
||||
public static ConnectionQuality Simple(double rtt, double jitter)
|
||||
{
|
||||
if (rtt <= 0.100 && jitter <= 0.10) return ConnectionQuality.EXCELLENT;
|
||||
if (rtt <= 0.200 && jitter <= 0.20) return ConnectionQuality.GOOD;
|
||||
if (rtt <= 0.400 && jitter <= 0.50) return ConnectionQuality.FAIR;
|
||||
return ConnectionQuality.POOR;
|
||||
}
|
||||
|
||||
// snapshot interpolation based estimation.
|
||||
// snap. interp. adjusts buffer time based on connection quality.
|
||||
// based on this, we can measure how far away we are from the ideal.
|
||||
// the returned quality will always directly correlate with gameplay.
|
||||
// => requires SnapshotInterpolation dynamicAdjustment to be enabled!
|
||||
public static ConnectionQuality Pragmatic(double targetBufferTime, double currentBufferTime)
|
||||
{
|
||||
// buffer time is set by the game developer.
|
||||
// estimating in multiples is a great way to be game independent.
|
||||
// for example, a fast paced shooter and a slow paced RTS will both
|
||||
// have poor connection if the multiplier is >10.
|
||||
double multiplier = currentBufferTime / targetBufferTime;
|
||||
|
||||
// empirically measured with Tanks demo + LatencySimulation.
|
||||
// it's not obvious to estimate on paper.
|
||||
if (multiplier <= 1.15) return ConnectionQuality.EXCELLENT;
|
||||
if (multiplier <= 1.25) return ConnectionQuality.GOOD;
|
||||
if (multiplier <= 1.50) return ConnectionQuality.FAIR;
|
||||
|
||||
// anything else is poor
|
||||
return ConnectionQuality.POOR;
|
||||
}
|
||||
}
|
||||
}
|
@ -40,9 +40,5 @@ public static void InvokeOnConnected()
|
||||
//OnConnectedEvent?.Invoke(connection);
|
||||
((LocalConnectionToServer)NetworkClient.connection).QueueConnectedEvent();
|
||||
}
|
||||
|
||||
// DEPRECATED 2023-01-28
|
||||
[Obsolete("ActivateHostScene did nothing, since identities all had .isClient set in NetworkServer.SpawnObjects.")]
|
||||
public static void ActivateHostScene() {}
|
||||
}
|
||||
}
|
||||
|
@ -54,7 +54,7 @@ public override void Rebuild(NetworkIdentity identity, bool initialize)
|
||||
newObservers.Clear();
|
||||
|
||||
// not force hidden?
|
||||
if (identity.visible != Visibility.ForceHidden)
|
||||
if (identity.visibility != Visibility.ForceHidden)
|
||||
{
|
||||
OnRebuildObservers(identity, newObservers);
|
||||
}
|
||||
|
@ -9,28 +9,21 @@ namespace Mirror
|
||||
[HelpURL("https://mirror-networking.gitbook.io/docs/guides/interest-management")]
|
||||
public abstract class InterestManagementBase : MonoBehaviour
|
||||
{
|
||||
// Configures InterestManagementBase in NetworkServer/Client
|
||||
// Do NOT check for active server or client here.
|
||||
// OnEnable must always set the static aoi references.
|
||||
// make sure to call base.OnEnable when overwriting!
|
||||
// Previously used Awake()
|
||||
// initialize NetworkServer/Client .aoi.
|
||||
// previously we did this in Awake(), but that's called for disabled
|
||||
// components too. if we do it OnEnable(), then it's not set for
|
||||
// disabled components.
|
||||
protected virtual void OnEnable()
|
||||
{
|
||||
if (NetworkServer.aoi == null)
|
||||
{
|
||||
// do not check if == null or error if already set.
|
||||
// users may enabled/disable components randomly,
|
||||
// causing this to be called multiple times.
|
||||
NetworkServer.aoi = this;
|
||||
}
|
||||
else Debug.LogError($"Only one InterestManagement component allowed. {NetworkServer.aoi.GetType()} has been set up already.");
|
||||
|
||||
if (NetworkClient.aoi == null)
|
||||
{
|
||||
NetworkClient.aoi = this;
|
||||
}
|
||||
else Debug.LogError($"Only one InterestManagement component allowed. {NetworkClient.aoi.GetType()} has been set up already.");
|
||||
}
|
||||
|
||||
[ServerCallback]
|
||||
public virtual void Reset() {}
|
||||
public virtual void ResetState() {}
|
||||
|
||||
// Callback used by the visibility system to determine if an observer
|
||||
// (player) can see the NetworkIdentity. If this function returns true,
|
||||
|
3
Assets/Mirror/Core/LagCompensation.meta
Normal file
3
Assets/Mirror/Core/LagCompensation.meta
Normal file
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d2656015ded44e83a24f4c4776bafd40
|
||||
timeCreated: 1687920405
|
13
Assets/Mirror/Core/LagCompensation/Capture.cs
Normal file
13
Assets/Mirror/Core/LagCompensation/Capture.cs
Normal file
@ -0,0 +1,13 @@
|
||||
namespace Mirror
|
||||
{
|
||||
public interface Capture
|
||||
{
|
||||
// server timestamp at time of capture.
|
||||
double timestamp { get; set; }
|
||||
|
||||
// optional gizmo drawing for visual debugging.
|
||||
// history is only known on the server, which usually doesn't render.
|
||||
// showing Gizmos in the Editor is enough.
|
||||
void DrawGizmo();
|
||||
}
|
||||
}
|
11
Assets/Mirror/Core/LagCompensation/Capture.cs.meta
Normal file
11
Assets/Mirror/Core/LagCompensation/Capture.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 347e831952e943a49095cadd39a5aeb2
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
139
Assets/Mirror/Core/LagCompensation/HistoryBounds.cs
Normal file
139
Assets/Mirror/Core/LagCompensation/HistoryBounds.cs
Normal file
@ -0,0 +1,139 @@
|
||||
// HistoryBounds keeps a bounding box of all the object's bounds in the past N seconds.
|
||||
// useful to decide which objects to rollback, instead of rolling back all of them.
|
||||
// https://www.youtube.com/watch?v=zrIY0eIyqmI (37:00)
|
||||
// standalone C# implementation to be engine (and language) agnostic.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
// FakeByte: gather bounds in smaller buckets.
|
||||
// for example, bucket(t0,t1,t2), bucket(t3,t4,t5), ...
|
||||
// instead of removing old bounds t0, t1, ...
|
||||
// we remove a whole bucket every 3 times: bucket(t0,t1,t2)
|
||||
// and when building total bounds, we encapsulate a few larger buckets
|
||||
// instead of many smaller bounds.
|
||||
//
|
||||
// => a bucket is encapsulate(bounds0, bounds1, bounds2) so we don't
|
||||
// need a custom struct, simply reuse bounds but remember that each
|
||||
// entry includes N timestamps.
|
||||
//
|
||||
// => note that simply reducing capture interval is _not_ the same.
|
||||
// we want to capture in detail in case players run in zig-zag.
|
||||
// but still grow larger buckets internally.
|
||||
public class HistoryBounds
|
||||
{
|
||||
// mischa: use MinMaxBounds to avoid Unity Bounds.Encapsulate conversions.
|
||||
readonly int boundsPerBucket;
|
||||
readonly Queue<MinMaxBounds> fullBuckets;
|
||||
|
||||
// full bucket limit. older ones will be removed.
|
||||
readonly int bucketLimit;
|
||||
|
||||
// bucket in progress, contains 0..boundsPerBucket bounds encapsulated.
|
||||
MinMaxBounds? currentBucket;
|
||||
int currentBucketSize;
|
||||
|
||||
// amount of total bounds, including bounds in full buckets + current
|
||||
public int boundsCount { get; private set; }
|
||||
|
||||
// total bounds encapsulating all of the bounds history.
|
||||
// totalMinMax is used for internal calculations.
|
||||
// public total is used for Unity representation.
|
||||
MinMaxBounds totalMinMax;
|
||||
public Bounds total
|
||||
{
|
||||
get
|
||||
{
|
||||
Bounds bounds = new Bounds();
|
||||
bounds.SetMinMax(totalMinMax.min, totalMinMax.max);
|
||||
return bounds;
|
||||
}
|
||||
}
|
||||
|
||||
public HistoryBounds(int boundsLimit, int boundsPerBucket)
|
||||
{
|
||||
// bucketLimit via '/' cuts off remainder.
|
||||
// that's what we want, since we always have a 'currentBucket'.
|
||||
this.boundsPerBucket = boundsPerBucket;
|
||||
this.bucketLimit = (boundsLimit / boundsPerBucket);
|
||||
|
||||
// initialize queue with maximum capacity to avoid runtime resizing
|
||||
// capacity +1 because it makes the code easier if we insert first, and then remove.
|
||||
fullBuckets = new Queue<MinMaxBounds>(bucketLimit + 1);
|
||||
}
|
||||
|
||||
// insert new bounds into history. calculates new total bounds.
|
||||
// Queue.Dequeue() always has the oldest bounds.
|
||||
public void Insert(Bounds bounds)
|
||||
{
|
||||
// convert to MinMax representation for faster .Encapsulate()
|
||||
MinMaxBounds minmax = new MinMaxBounds
|
||||
{
|
||||
min = bounds.min,
|
||||
max = bounds.max
|
||||
};
|
||||
|
||||
// initialize 'total' if not initialized yet.
|
||||
// we don't want to call (0,0).Encapsulate(bounds).
|
||||
if (boundsCount == 0)
|
||||
{
|
||||
totalMinMax = minmax;
|
||||
}
|
||||
|
||||
// add to current bucket:
|
||||
// either initialize new one, or encapsulate into existing one
|
||||
if (currentBucket == null)
|
||||
{
|
||||
currentBucket = minmax;
|
||||
}
|
||||
else
|
||||
{
|
||||
currentBucket.Value.Encapsulate(minmax);
|
||||
}
|
||||
|
||||
// current bucket has one more bounds.
|
||||
// total bounds increased as well.
|
||||
currentBucketSize += 1;
|
||||
boundsCount += 1;
|
||||
|
||||
// always encapsulate into total immediately.
|
||||
// this is free.
|
||||
totalMinMax.Encapsulate(minmax);
|
||||
|
||||
// current bucket full?
|
||||
if (currentBucketSize == boundsPerBucket)
|
||||
{
|
||||
// move it to full buckets
|
||||
fullBuckets.Enqueue(currentBucket.Value);
|
||||
currentBucket = null;
|
||||
currentBucketSize = 0;
|
||||
|
||||
// full bucket capacity reached?
|
||||
if (fullBuckets.Count > bucketLimit)
|
||||
{
|
||||
// remove oldest bucket
|
||||
fullBuckets.Dequeue();
|
||||
boundsCount -= boundsPerBucket;
|
||||
|
||||
// recompute total bounds
|
||||
// instead of iterating N buckets, we iterate N / boundsPerBucket buckets.
|
||||
// TODO technically we could reuse 'currentBucket' before clearing instead of encapsulating again
|
||||
totalMinMax = minmax;
|
||||
foreach (MinMaxBounds bucket in fullBuckets)
|
||||
totalMinMax.Encapsulate(bucket);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
fullBuckets.Clear();
|
||||
currentBucket = null;
|
||||
currentBucketSize = 0;
|
||||
boundsCount = 0;
|
||||
totalMinMax = new MinMaxBounds();
|
||||
}
|
||||
}
|
||||
}
|
11
Assets/Mirror/Core/LagCompensation/HistoryBounds.cs.meta
Normal file
11
Assets/Mirror/Core/LagCompensation/HistoryBounds.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ca9ea58b98a34f73801b162cd5de724e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
144
Assets/Mirror/Core/LagCompensation/LagCompensation.cs
Normal file
144
Assets/Mirror/Core/LagCompensation/LagCompensation.cs
Normal file
@ -0,0 +1,144 @@
|
||||
// standalone lag compensation algorithm
|
||||
// based on the Valve Networking Model:
|
||||
// https://developer.valvesoftware.com/wiki/Source_Multiplayer_Networking
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
public static class LagCompensation
|
||||
{
|
||||
// history is of <timestamp, capture>.
|
||||
// Queue allows for fast 'remove first' and 'append last'.
|
||||
//
|
||||
// make sure to always insert in order.
|
||||
// inserting out of order like [1,2,4,3] would cause issues.
|
||||
// can't safeguard this because Queue doesn't have .Last access.
|
||||
public static void Insert<T>(
|
||||
Queue<KeyValuePair<double, T>> history,
|
||||
int historyLimit,
|
||||
double timestamp,
|
||||
T capture)
|
||||
where T : Capture
|
||||
{
|
||||
// make space according to history limit.
|
||||
// do this before inserting, to avoid resizing past capacity.
|
||||
if (history.Count >= historyLimit)
|
||||
history.Dequeue();
|
||||
|
||||
// insert
|
||||
history.Enqueue(new KeyValuePair<double, T>(timestamp, capture));
|
||||
}
|
||||
|
||||
// get the two snapshots closest to a given timestamp.
|
||||
// those can be used to interpolate the exact snapshot at that time.
|
||||
// if timestamp is newer than the newest history entry, then we extrapolate.
|
||||
// 't' will be between 1 and 2, before is second last, after is last.
|
||||
// callers should Lerp(before, after, t=1.5) to extrapolate the hit.
|
||||
// see comments below for extrapolation.
|
||||
public static bool Sample<T>(
|
||||
Queue<KeyValuePair<double, T>> history,
|
||||
double timestamp, // current server time
|
||||
double interval, // capture interval
|
||||
out T before,
|
||||
out T after,
|
||||
out double t) // interpolation factor
|
||||
where T : Capture
|
||||
{
|
||||
before = default;
|
||||
after = default;
|
||||
t = 0;
|
||||
|
||||
// can't sample an empty history
|
||||
// interpolation needs at least one entry.
|
||||
// extrapolation needs at least two entries.
|
||||
// can't Lerp(A, A, 1.5). dist(A, A) * 1.5 is always 0.
|
||||
if(history.Count < 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// older than oldest
|
||||
if (timestamp < history.Peek().Key) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// iterate through the history
|
||||
// TODO faster version: guess start index by how many 'intervals' we are behind.
|
||||
// search around that area.
|
||||
// should be O(1) most of the time, unless sampling was off.
|
||||
KeyValuePair<double, T> prev = new KeyValuePair<double, T>();
|
||||
KeyValuePair<double, T> prevPrev = new KeyValuePair<double, T>();
|
||||
foreach(KeyValuePair<double, T> entry in history) {
|
||||
// exact match?
|
||||
if (timestamp == entry.Key) {
|
||||
before = entry.Value;
|
||||
after = entry.Value;
|
||||
t = Mathd.InverseLerp(before.timestamp, after.timestamp, timestamp);
|
||||
return true;
|
||||
}
|
||||
|
||||
// did we check beyond timestamp? then return the previous two.
|
||||
if (entry.Key > timestamp) {
|
||||
before = prev.Value;
|
||||
after = entry.Value;
|
||||
t = Mathd.InverseLerp(before.timestamp, after.timestamp, timestamp);
|
||||
return true;
|
||||
}
|
||||
|
||||
// remember the last two for extrapolation.
|
||||
// Queue doesn't have access to .Last.
|
||||
prevPrev = prev;
|
||||
prev = entry;
|
||||
}
|
||||
|
||||
// newer than newest: extrapolate up to one interval.
|
||||
// let's say we capture every 100 ms:
|
||||
// 100, 200, 300, 400
|
||||
// and the server is at 499
|
||||
// if a client sends CmdFire at time 480, then there's no history entry.
|
||||
// => adding the current entry every time would be too expensive.
|
||||
// worst case we would capture at 401, 402, 403, 404, ... 100 times
|
||||
// => not extrapolating isn't great. low latency clients would be
|
||||
// punished by missing their targets since no entry at 'time' was found.
|
||||
// => extrapolation is the best solution. make sure this works as
|
||||
// expected and within limits.
|
||||
if (prev.Key < timestamp && timestamp <= prev.Key + interval) {
|
||||
// return the last two valid snapshots.
|
||||
// can't just return (after, after) because we can't extrapolate
|
||||
// if their distance is 0.
|
||||
before = prevPrev.Value;
|
||||
after = prev.Value;
|
||||
|
||||
// InverseLerp will give [after, after+interval].
|
||||
// but we return [before, after, t].
|
||||
// so add +1 for the distance from before->after
|
||||
t = 1 + Mathd.InverseLerp(after.timestamp, after.timestamp + interval, timestamp);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// never trust the client.
|
||||
// we estimate when a message was sent.
|
||||
// don't trust the client to tell us the time.
|
||||
// https://developer.valvesoftware.com/wiki/Source_Multiplayer_Networking
|
||||
// Command Execution Time = Current Server Time - Packet Latency - Client View Interpolation
|
||||
// => lag compensation demo estimation is off by only ~6ms
|
||||
public static double EstimateTime(double serverTime, double rtt, double bufferTime)
|
||||
{
|
||||
// packet latency is one trip from client to server, so rtt / 2
|
||||
// client view interpolation is the snapshot interpolation buffer time
|
||||
double latency = rtt / 2;
|
||||
return serverTime - latency - bufferTime;
|
||||
}
|
||||
|
||||
// convenience function to draw all history gizmos.
|
||||
// this should be called from OnDrawGizmos.
|
||||
public static void DrawGizmos<T>(Queue<KeyValuePair<double, T>> history)
|
||||
where T : Capture
|
||||
{
|
||||
foreach (KeyValuePair<double, T> entry in history)
|
||||
entry.Value.DrawGizmo();
|
||||
}
|
||||
}
|
||||
}
|
11
Assets/Mirror/Core/LagCompensation/LagCompensation.cs.meta
Normal file
11
Assets/Mirror/Core/LagCompensation/LagCompensation.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ad53cc7d12144d0ba3a8b0a4515e5d17
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -0,0 +1,19 @@
|
||||
// snapshot interpolation settings struct.
|
||||
// can easily be exposed in Unity inspectors.
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
// class so we can define defaults easily
|
||||
[Serializable]
|
||||
public class LagCompensationSettings
|
||||
{
|
||||
[Header("Buffering")]
|
||||
[Tooltip("Keep this many past snapshots in the buffer. The larger this is, the further we can rewind into the past.\nMaximum rewind time := historyAmount * captureInterval")]
|
||||
public int historyLimit = 6;
|
||||
|
||||
[Tooltip("Capture state every 'captureInterval' seconds. Larger values will space out the captures more, which gives a longer history but with possible gaps inbetween.\nSmaller values will have fewer gaps, with shorter history.")]
|
||||
public float captureInterval = 0.100f; // 100 ms
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fa80bec245f94bf8a28ec78777992a1c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
73
Assets/Mirror/Core/LagCompensation/MinMaxBounds.cs
Normal file
73
Assets/Mirror/Core/LagCompensation/MinMaxBounds.cs
Normal file
@ -0,0 +1,73 @@
|
||||
// Unity's Bounds struct is represented as (center, extents).
|
||||
// HistoryBounds make heavy use of .Encapsulate(), which has to convert
|
||||
// Unity's (center, extents) to (min, max) every time, and then convert back.
|
||||
//
|
||||
// It's faster to use a (min, max) representation directly instead.
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
public struct MinMaxBounds: IEquatable<Bounds>
|
||||
{
|
||||
public Vector3 min;
|
||||
public Vector3 max;
|
||||
|
||||
// encapsulate a single point
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Encapsulate(Vector3 point)
|
||||
{
|
||||
min = Vector3.Min(this.min, point);
|
||||
max = Vector3.Max(this.max, point);
|
||||
}
|
||||
|
||||
// encapsulate another bounds
|
||||
public void Encapsulate(MinMaxBounds bounds)
|
||||
{
|
||||
Encapsulate(bounds.min);
|
||||
Encapsulate(bounds.max);
|
||||
}
|
||||
|
||||
// convenience comparison with Unity's bounds, for unit tests etc.
|
||||
public static bool operator ==(MinMaxBounds lhs, Bounds rhs) =>
|
||||
lhs.min == rhs.min &&
|
||||
lhs.max == rhs.max;
|
||||
|
||||
public static bool operator !=(MinMaxBounds lhs, Bounds rhs) =>
|
||||
!(lhs == rhs);
|
||||
|
||||
public override bool Equals(object obj) =>
|
||||
obj is MinMaxBounds other &&
|
||||
min == other.min &&
|
||||
max == other.max;
|
||||
|
||||
public bool Equals(MinMaxBounds other) =>
|
||||
min.Equals(other.min) && max.Equals(other.max);
|
||||
|
||||
public bool Equals(Bounds other) =>
|
||||
min.Equals(other.min) && max.Equals(other.max);
|
||||
|
||||
#if UNITY_2021_3_OR_NEWER
|
||||
// Unity 2019/2020 don't have HashCode.Combine yet.
|
||||
// this is only to avoid reflection. without defining, it works too.
|
||||
// default generated by rider
|
||||
public override int GetHashCode() => HashCode.Combine(min, max);
|
||||
#else
|
||||
public override int GetHashCode()
|
||||
{
|
||||
// return HashCode.Combine(min, max); without using .Combine for older Unity versions
|
||||
unchecked
|
||||
{
|
||||
int hash = 17;
|
||||
hash = hash * 23 + min.GetHashCode();
|
||||
hash = hash * 23 + max.GetHashCode();
|
||||
return hash;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// tostring
|
||||
public override string ToString() => $"({min}, {max})";
|
||||
}
|
||||
}
|
11
Assets/Mirror/Core/LagCompensation/MinMaxBounds.cs.meta
Normal file
11
Assets/Mirror/Core/LagCompensation/MinMaxBounds.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4372b1e1a1cc4c669cc7bf0925f59d29
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
@ -8,18 +9,20 @@ public class LocalConnectionToClient : NetworkConnectionToClient
|
||||
{
|
||||
internal LocalConnectionToServer connectionToServer;
|
||||
|
||||
// packet queue
|
||||
internal readonly Queue<NetworkWriterPooled> queue = new Queue<NetworkWriterPooled>();
|
||||
|
||||
public LocalConnectionToClient() : base(LocalConnectionId) {}
|
||||
|
||||
public override string address => "localhost";
|
||||
|
||||
// Send stage two: serialized NetworkMessage as ArraySegment<byte>
|
||||
internal override void Send(ArraySegment<byte> segment, int channelId = Channels.Reliable)
|
||||
{
|
||||
// get a writer to copy the message into since the segment is only
|
||||
// valid until returning.
|
||||
// => pooled writer will be returned to pool when dequeuing.
|
||||
// => WriteBytes instead of WriteArraySegment because the latter
|
||||
// includes a 4 bytes header. we just want to write raw.
|
||||
// instead of invoking it directly, we enqueue and process next update.
|
||||
// this way we can simulate a similar call flow as with remote clients.
|
||||
// the closer we get to simulating host as remote, the better!
|
||||
// both directions do this, so [Command] and [Rpc] behave the same way.
|
||||
|
||||
//Debug.Log($"Enqueue {BitConverter.ToString(segment.Array, segment.Offset, segment.Count)}");
|
||||
NetworkWriterPooled writer = NetworkWriterPool.Get();
|
||||
writer.WriteBytes(segment.Array, segment.Offset, segment.Count);
|
||||
@ -29,6 +32,38 @@ internal override void Send(ArraySegment<byte> segment, int channelId = Channels
|
||||
// true because local connections never timeout
|
||||
internal override bool IsAlive(float timeout) => true;
|
||||
|
||||
// don't ping host client in host mode
|
||||
protected override void UpdatePing() {}
|
||||
|
||||
internal override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
// process internal messages so they are applied at the correct time
|
||||
while (queue.Count > 0)
|
||||
{
|
||||
// call receive on queued writer's content, return to pool
|
||||
NetworkWriterPooled writer = queue.Dequeue();
|
||||
ArraySegment<byte> message = writer.ToArraySegment();
|
||||
|
||||
// OnTransportData assumes a proper batch with timestamp etc.
|
||||
// let's make a proper batch and pass it to OnTransportData.
|
||||
Batcher batcher = GetBatchForChannelId(Channels.Reliable);
|
||||
batcher.AddMessage(message, NetworkTime.localTime);
|
||||
|
||||
using (NetworkWriterPooled batchWriter = NetworkWriterPool.Get())
|
||||
{
|
||||
// make a batch with our local time (double precision)
|
||||
if (batcher.GetBatch(batchWriter))
|
||||
{
|
||||
NetworkServer.OnTransportData(connectionId, batchWriter.ToArraySegment(), Channels.Reliable);
|
||||
}
|
||||
}
|
||||
|
||||
NetworkWriterPool.Return(writer);
|
||||
}
|
||||
}
|
||||
|
||||
internal void DisconnectInternal()
|
||||
{
|
||||
// set not ready and handle clientscene disconnect in any case
|
||||
|
@ -13,10 +13,6 @@ public class LocalConnectionToServer : NetworkConnectionToServer
|
||||
// packet queue
|
||||
internal readonly Queue<NetworkWriterPooled> queue = new Queue<NetworkWriterPooled>();
|
||||
|
||||
// Deprecated 2023-02-23
|
||||
[Obsolete("Use LocalConnectionToClient.address instead.")]
|
||||
public string address => "localhost";
|
||||
|
||||
// see caller for comments on why we need this
|
||||
bool connectedEventPending;
|
||||
bool disconnectedEventPending;
|
||||
@ -32,22 +28,15 @@ internal override void Send(ArraySegment<byte> segment, int channelId = Channels
|
||||
return;
|
||||
}
|
||||
|
||||
// OnTransportData assumes batching.
|
||||
// so let's make a batch with proper timestamp prefix.
|
||||
Batcher batcher = GetBatchForChannelId(channelId);
|
||||
batcher.AddMessage(segment, NetworkTime.localTime);
|
||||
// instead of invoking it directly, we enqueue and process next update.
|
||||
// this way we can simulate a similar call flow as with remote clients.
|
||||
// the closer we get to simulating host as remote, the better!
|
||||
// both directions do this, so [Command] and [Rpc] behave the same way.
|
||||
|
||||
// flush it to the server's OnTransportData immediately.
|
||||
// local connection to server always invokes immediately.
|
||||
using (NetworkWriterPooled writer = NetworkWriterPool.Get())
|
||||
{
|
||||
// make a batch with our local time (double precision)
|
||||
if (batcher.GetBatch(writer))
|
||||
{
|
||||
NetworkServer.OnTransportData(connectionId, writer.ToArraySegment(), channelId);
|
||||
}
|
||||
else Debug.LogError("Local connection failed to make batch. This should never happen.");
|
||||
}
|
||||
//Debug.Log($"Enqueue {BitConverter.ToString(segment.Array, segment.Offset, segment.Count)}");
|
||||
NetworkWriterPooled writer = NetworkWriterPool.Get();
|
||||
writer.WriteBytes(segment.Array, segment.Offset, segment.Count);
|
||||
connectionToClient.queue.Enqueue(writer);
|
||||
}
|
||||
|
||||
internal override void Update()
|
||||
|
@ -52,15 +52,6 @@ public struct RpcMessage : NetworkMessage
|
||||
public ArraySegment<byte> payload;
|
||||
}
|
||||
|
||||
// holds multiple buffered rpcs for the given connection.
|
||||
// more efficient than sending one message per rpc.
|
||||
public struct RpcBufferMessage : NetworkMessage
|
||||
{
|
||||
// payload contains multiple serialized RpcMessages.
|
||||
// but without the message header.
|
||||
public ArraySegment<byte> payload;
|
||||
}
|
||||
|
||||
public struct SpawnMessage : NetworkMessage
|
||||
{
|
||||
// netId of new or existing object
|
||||
@ -111,22 +102,39 @@ public struct EntityStateMessage : NetworkMessage
|
||||
public ArraySegment<byte> payload;
|
||||
}
|
||||
|
||||
// A client sends this message to the server
|
||||
// to calculate RTT and synchronize time
|
||||
// whoever wants to measure rtt, sends this to the other end.
|
||||
public struct NetworkPingMessage : NetworkMessage
|
||||
{
|
||||
public double clientTime;
|
||||
// local time is used to calculate round trip time,
|
||||
// and to calculate the predicted time offset.
|
||||
public double localTime;
|
||||
|
||||
public NetworkPingMessage(double value)
|
||||
// predicted time is sent to compare the final error, for debugging only
|
||||
public double predictedTimeAdjusted;
|
||||
|
||||
public NetworkPingMessage(double localTime, double predictedTimeAdjusted)
|
||||
{
|
||||
clientTime = value;
|
||||
this.localTime = localTime;
|
||||
this.predictedTimeAdjusted = predictedTimeAdjusted;
|
||||
}
|
||||
}
|
||||
|
||||
// The server responds with this message
|
||||
// The client can use this to calculate RTT and sync time
|
||||
// the other end responds with this message.
|
||||
// we can use this to calculate rtt.
|
||||
public struct NetworkPongMessage : NetworkMessage
|
||||
{
|
||||
public double clientTime;
|
||||
// local time is used to calculate round trip time.
|
||||
public double localTime;
|
||||
|
||||
// predicted error is used to adjust the predicted timeline.
|
||||
public double predictionErrorUnadjusted;
|
||||
public double predictionErrorAdjusted; // for debug purposes
|
||||
|
||||
public NetworkPongMessage(double localTime, double predictionErrorUnadjusted, double predictionErrorAdjusted)
|
||||
{
|
||||
this.localTime = localTime;
|
||||
this.predictionErrorUnadjusted = predictionErrorUnadjusted;
|
||||
this.predictionErrorAdjusted = predictionErrorAdjusted;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -19,8 +19,8 @@ public enum SyncMode { Observers, Owner }
|
||||
public enum SyncDirection { ServerToClient, ClientToServer }
|
||||
|
||||
/// <summary>Base class for networked components.</summary>
|
||||
// [RequireComponent(typeof(NetworkIdentity))] disabled to allow child NetworkBehaviours
|
||||
[AddComponentMenu("")]
|
||||
[RequireComponent(typeof(NetworkIdentity))]
|
||||
[HelpURL("https://mirror-networking.gitbook.io/docs/guides/networkbehaviour")]
|
||||
public abstract class NetworkBehaviour : MonoBehaviour
|
||||
{
|
||||
@ -67,13 +67,11 @@ public abstract class NetworkBehaviour : MonoBehaviour
|
||||
// for example: main player & pets are owned. monsters & npcs aren't.
|
||||
public bool isOwned => netIdentity.isOwned;
|
||||
|
||||
// Deprecated 2022-10-13
|
||||
[Obsolete(".hasAuthority was renamed to .isOwned. This is easier to understand and prepares for SyncDirection, where there is a difference betwen isOwned and authority.")]
|
||||
public bool hasAuthority => isOwned;
|
||||
|
||||
/// <summary>authority is true if we are allowed to modify this component's state. On server, it's true if SyncDirection is ServerToClient. On client, it's true if SyncDirection is ClientToServer and(!) if this object is owned by the client.</summary>
|
||||
// on the client: if owned and if clientAuthority sync direction
|
||||
// on the server: if serverAuthority sync direction
|
||||
// on the client: if Client->Server SyncDirection and owned
|
||||
// on the server: if Server->Client SyncDirection
|
||||
// on the host: if Server->Client SyncDirection (= server owns it), or if Client->Server and owned (=host client owns it)
|
||||
// in host mode: always true because either server or client always has authority, and host is both.
|
||||
//
|
||||
// for example, NetworkTransform:
|
||||
// client may modify position if ClientAuthority mode and owned
|
||||
@ -84,10 +82,20 @@ public abstract class NetworkBehaviour : MonoBehaviour
|
||||
//
|
||||
// also note that this is a per-NetworkBehaviour flag.
|
||||
// another component may not be client authoritative, etc.
|
||||
public bool authority =>
|
||||
isClient
|
||||
? syncDirection == SyncDirection.ClientToServer && isOwned
|
||||
: syncDirection == SyncDirection.ServerToClient;
|
||||
public bool authority
|
||||
{
|
||||
get
|
||||
{
|
||||
// host mode needs to be checked explicitly
|
||||
if (isClient && isServer) return syncDirection == SyncDirection.ServerToClient || isOwned;
|
||||
|
||||
// client-only
|
||||
if (isClient) return syncDirection == SyncDirection.ClientToServer && isOwned;
|
||||
|
||||
// server-only
|
||||
return syncDirection == SyncDirection.ServerToClient;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>The unique network Id of this object (unique at runtime).</summary>
|
||||
public uint netId => netIdentity.netId;
|
||||
@ -127,8 +135,9 @@ public abstract class NetworkBehaviour : MonoBehaviour
|
||||
// -> still supports dynamically sized types
|
||||
//
|
||||
// 64 bit mask, tracking up to 64 SyncVars.
|
||||
protected ulong syncVarDirtyBits { get; private set; }
|
||||
// 64 bit mask, tracking up to 64 sync collections (internal for tests).
|
||||
// protected since NB child classes read this field in the weaver generated SerializeSyncVars method
|
||||
protected ulong syncVarDirtyBits;
|
||||
// 64 bit mask, tracking up to 64 sync collections.
|
||||
// internal for tests, field for faster access (instead of property)
|
||||
// TODO 64 SyncLists are too much. consider smaller mask later.
|
||||
internal ulong syncObjectDirtyBits;
|
||||
@ -293,6 +302,34 @@ protected void InitSyncObject(SyncObject syncObject)
|
||||
};
|
||||
}
|
||||
|
||||
protected virtual void OnValidate()
|
||||
{
|
||||
// we now allow child NetworkBehaviours.
|
||||
// we can not [RequireComponent(typeof(NetworkIdentity))] anymore.
|
||||
// instead, we need to ensure a NetworkIdentity is somewhere in the
|
||||
// parents.
|
||||
// only run this in Editor. don't add more runtime overhead.
|
||||
|
||||
// GetComponentInParent(includeInactive) is needed because Prefabs are not
|
||||
// considered active, so this check requires to scan inactive.
|
||||
#if UNITY_EDITOR
|
||||
#if UNITY_2021_3_OR_NEWER // 2021 has GetComponentInParent(bool includeInactive = false)
|
||||
if (GetComponent<NetworkIdentity>() == null &&
|
||||
GetComponentInParent<NetworkIdentity>(true) == null)
|
||||
{
|
||||
Debug.LogError($"{GetType()} on {name} requires a NetworkIdentity. Please add a NetworkIdentity component to {name} or it's parents.", this);
|
||||
}
|
||||
#elif UNITY_2020_3_OR_NEWER // 2020 only has GetComponentsInParent(bool includeInactive = false), we can use this too
|
||||
NetworkIdentity[] parentsIds = GetComponentsInParent<NetworkIdentity>(true);
|
||||
int parentIdsCount = parentsIds != null ? parentsIds.Length : 0;
|
||||
if (GetComponent<NetworkIdentity>() == null && parentIdsCount == 0)
|
||||
{
|
||||
Debug.LogError($"{GetType()} on {name} requires a NetworkIdentity. Please add a NetworkIdentity component to {name} or it's parents.", this);
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
// pass full function name to avoid ClassA.Func <-> ClassB.Func collisions
|
||||
protected void SendCommandInternal(string functionFullName, int functionHashCode, NetworkWriter writer, int channelId, bool requiresAuthority = true)
|
||||
{
|
||||
@ -301,7 +338,7 @@ protected void SendCommandInternal(string functionFullName, int functionHashCode
|
||||
// to avoid Wrapper functions. a lot of people requested this.
|
||||
if (!NetworkClient.active)
|
||||
{
|
||||
Debug.LogError($"Command Function {functionFullName} called on {name} without an active client.", gameObject);
|
||||
Debug.LogError($"Command {functionFullName} called on {name} without an active client.", gameObject);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -313,7 +350,7 @@ protected void SendCommandInternal(string functionFullName, int functionHashCode
|
||||
// or client may have been set NotReady intentionally, so
|
||||
// only warn if on the reliable channel.
|
||||
if (channelId == Channels.Reliable)
|
||||
Debug.LogWarning($"Command Function {functionFullName} called on {name} while NetworkClient is not ready.\nThis may be ignored if client intentionally set NotReady.", gameObject);
|
||||
Debug.LogWarning($"Command {functionFullName} called on {name} while NetworkClient is not ready.\nThis may be ignored if client intentionally set NotReady.", gameObject);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -321,7 +358,7 @@ protected void SendCommandInternal(string functionFullName, int functionHashCode
|
||||
// other objects must have authority.
|
||||
if (!(!requiresAuthority || isLocalPlayer || isOwned))
|
||||
{
|
||||
Debug.LogWarning($"Command Function {functionFullName} called on {name} without authority.", gameObject);
|
||||
Debug.LogWarning($"Command {functionFullName} called on {name} without authority.", gameObject);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -332,7 +369,7 @@ protected void SendCommandInternal(string functionFullName, int functionHashCode
|
||||
// => see also: https://github.com/vis2k/Mirror/issues/2629
|
||||
if (NetworkClient.connection == null)
|
||||
{
|
||||
Debug.LogError($"Command Function {functionFullName} called on {name} with no client running.", gameObject);
|
||||
Debug.LogError($"Command {functionFullName} called on {name} with no client running.", gameObject);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -363,7 +400,7 @@ protected void SendRPCInternal(string functionFullName, int functionHashCode, Ne
|
||||
// this was in Weaver before
|
||||
if (!NetworkServer.active)
|
||||
{
|
||||
Debug.LogError($"RPC Function {functionFullName} called on Client.", gameObject);
|
||||
Debug.LogError($"RPC Function {functionFullName} called without an active server.", gameObject);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -398,13 +435,14 @@ protected void SendRPCInternal(string functionFullName, int functionHashCode, Ne
|
||||
{
|
||||
serialized.Write(message);
|
||||
|
||||
// add to every observer's connection's rpc buffer
|
||||
// send to every observer.
|
||||
// batching buffers this automatically.
|
||||
foreach (NetworkConnectionToClient conn in netIdentity.observers.Values)
|
||||
{
|
||||
bool isOwner = conn == netIdentity.connectionToClient;
|
||||
if ((!isOwner || includeOwner) && conn.isReady)
|
||||
{
|
||||
conn.BufferRpc(message, channelId);
|
||||
conn.Send(message, channelId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -456,10 +494,9 @@ protected void SendTargetRPCInternal(NetworkConnection conn, string functionFull
|
||||
payload = writer.ToArraySegment()
|
||||
};
|
||||
|
||||
// serialize it to the connection's rpc buffer.
|
||||
// send them all at once, instead of sending one message per rpc.
|
||||
// conn.Send(message, channelId);
|
||||
connToClient.BufferRpc(message, channelId);
|
||||
// send it to the connection.
|
||||
// batching buffers this automatically.
|
||||
conn.Send(message, channelId);
|
||||
}
|
||||
|
||||
// move the [SyncVar] generated property's .set into C# to avoid much IL
|
||||
@ -727,7 +764,6 @@ public static bool SyncVarNetworkIdentityEqual(NetworkIdentity newIdentity, uint
|
||||
// GeneratedSyncVarDeserialize(reader, ref health, null, reader.ReadInt());
|
||||
// }
|
||||
// }
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void GeneratedSyncVarDeserialize<T>(ref T field, Action<T, T> OnChanged, T value)
|
||||
{
|
||||
T previous = field;
|
||||
@ -785,7 +821,6 @@ public void GeneratedSyncVarDeserialize<T>(ref T field, Action<T, T> OnChanged,
|
||||
// GeneratedSyncVarDeserialize_GameObject(reader, ref target, OnChangedNB, ref ___targetNetId);
|
||||
// }
|
||||
// }
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void GeneratedSyncVarDeserialize_GameObject(ref GameObject field, Action<GameObject, GameObject> OnChanged, NetworkReader reader, ref uint netIdField)
|
||||
{
|
||||
uint previousNetId = netIdField;
|
||||
@ -848,7 +883,6 @@ public void GeneratedSyncVarDeserialize_GameObject(ref GameObject field, Action<
|
||||
// GeneratedSyncVarDeserialize_NetworkIdentity(reader, ref target, OnChangedNI, ref ___targetNetId);
|
||||
// }
|
||||
// }
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void GeneratedSyncVarDeserialize_NetworkIdentity(ref NetworkIdentity field, Action<NetworkIdentity, NetworkIdentity> OnChanged, NetworkReader reader, ref uint netIdField)
|
||||
{
|
||||
uint previousNetId = netIdField;
|
||||
@ -912,7 +946,6 @@ public void GeneratedSyncVarDeserialize_NetworkIdentity(ref NetworkIdentity fiel
|
||||
// GeneratedSyncVarDeserialize_NetworkBehaviour(reader, ref target, OnChangedNB, ref ___targetNetId);
|
||||
// }
|
||||
// }
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void GeneratedSyncVarDeserialize_NetworkBehaviour<T>(ref T field, Action<T, T> OnChanged, NetworkReader reader, ref NetworkBehaviourSyncVar netIdField)
|
||||
where T : NetworkBehaviour
|
||||
{
|
||||
@ -1019,7 +1052,7 @@ protected void SetSyncVarNetworkBehaviour<T>(T newBehaviour, ref T behaviourFiel
|
||||
// Debug.Log($"SetSyncVarNetworkBehaviour NetworkIdentity {GetType().Name} bit [{dirtyBit}] netIdField:{oldField}->{syncField}");
|
||||
}
|
||||
|
||||
// helper function for [SyncVar] NetworkIdentities.
|
||||
// helper function for [SyncVar] NetworkBehaviours.
|
||||
// -> ref GameObject as second argument makes OnDeserialize processing easier
|
||||
protected T GetSyncVarNetworkBehaviour<T>(NetworkBehaviourSyncVar syncNetBehaviour, ref T behaviourField) where T : NetworkBehaviour
|
||||
{
|
||||
@ -1038,6 +1071,15 @@ protected T GetSyncVarNetworkBehaviour<T>(NetworkBehaviourSyncVar syncNetBehavio
|
||||
return null;
|
||||
}
|
||||
|
||||
// ensure componentIndex is in range.
|
||||
// show explicit errors if something went wrong, instead of IndexOutOfRangeException.
|
||||
// removing components at runtime isn't allowed, yet this happened in a project so we need to check for it.
|
||||
if (syncNetBehaviour.componentIndex >= identity.NetworkBehaviours.Length)
|
||||
{
|
||||
Debug.LogError($"[SyncVar] {typeof(T)} on {name}'s {GetType()}: can't access {identity.name} NetworkBehaviour[{syncNetBehaviour.componentIndex}] because it only has {identity.NetworkBehaviours.Length} components.\nWas a NetworkBeahviour accidentally destroyed at runtime?");
|
||||
return null;
|
||||
}
|
||||
|
||||
behaviourField = identity.NetworkBehaviours[syncNetBehaviour.componentIndex] as T;
|
||||
return behaviourField;
|
||||
}
|
||||
@ -1078,7 +1120,6 @@ public virtual void OnDeserialize(NetworkReader reader, bool initialState)
|
||||
DeserializeSyncVars(reader, initialState);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
void SerializeSyncObjects(NetworkWriter writer, bool initialState)
|
||||
{
|
||||
// if initialState: write all SyncVars.
|
||||
@ -1089,7 +1130,6 @@ void SerializeSyncObjects(NetworkWriter writer, bool initialState)
|
||||
SerializeObjectsDelta(writer);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
void DeserializeSyncObjects(NetworkReader reader, bool initialState)
|
||||
{
|
||||
if (initialState)
|
||||
@ -1330,5 +1370,10 @@ public virtual void OnStartAuthority() {}
|
||||
|
||||
/// <summary>Stop event, only called for objects the client has authority over.</summary>
|
||||
public virtual void OnStopAuthority() {}
|
||||
|
||||
// Weaver injects this into inheriting classes to return true.
|
||||
// allows runtime & tests to check if a type was weaved.
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public virtual bool Weaved() => false;
|
||||
}
|
||||
}
|
||||
|
@ -33,6 +33,15 @@ public static partial class NetworkClient
|
||||
public static float sendInterval => sendRate < int.MaxValue ? 1f / sendRate : 0; // for 30 Hz, that's 33ms
|
||||
static double lastSendTime;
|
||||
|
||||
// For security, it is recommended to disconnect a player if a networked
|
||||
// action triggers an exception\nThis could prevent components being
|
||||
// accessed in an undefined state, which may be an attack vector for
|
||||
// exploits.
|
||||
//
|
||||
// However, some games may want to allow exceptions in order to not
|
||||
// interrupt the player's experience.
|
||||
public static bool exceptionsDisconnect = true; // security by default
|
||||
|
||||
// message handlers by messageId
|
||||
internal static readonly Dictionary<ushort, NetworkMessageDelegate> handlers =
|
||||
new Dictionary<ushort, NetworkMessageDelegate>();
|
||||
@ -74,10 +83,6 @@ public static partial class NetworkClient
|
||||
/// <summary>Check if client is connected (after connecting).</summary>
|
||||
public static bool isConnected => connectState == ConnectState.Connected;
|
||||
|
||||
// Deprecated 2022-12-12
|
||||
[Obsolete("NetworkClient.isHostClient was renamed to .activeHost to be more obvious")]
|
||||
public static bool isHostClient => activeHost;
|
||||
|
||||
// OnConnected / OnDisconnected used to be NetworkMessages that were
|
||||
// invoked. this introduced a bug where external clients could send
|
||||
// Connected/Disconnected messages over the network causing undefined
|
||||
@ -86,6 +91,7 @@ public static partial class NetworkClient
|
||||
public static Action OnConnectedEvent;
|
||||
public static Action OnDisconnectedEvent;
|
||||
public static Action<TransportError, string> OnErrorEvent;
|
||||
public static Action<Exception> OnTransportExceptionEvent;
|
||||
|
||||
/// <summary>Registered spawnable prefabs by assetId.</summary>
|
||||
public static readonly Dictionary<uint, GameObject> prefabs =
|
||||
@ -107,7 +113,7 @@ public static partial class NetworkClient
|
||||
internal static readonly Dictionary<ulong, NetworkIdentity> spawnableObjects =
|
||||
new Dictionary<ulong, NetworkIdentity>();
|
||||
|
||||
static Unbatcher unbatcher = new Unbatcher();
|
||||
internal static Unbatcher unbatcher = new Unbatcher();
|
||||
|
||||
// interest management component (optional)
|
||||
// only needed for SetHostVisibility
|
||||
@ -116,6 +122,21 @@ public static partial class NetworkClient
|
||||
// scene loading
|
||||
public static bool isLoadingScene;
|
||||
|
||||
// connection quality
|
||||
// this is set by a virtual function in NetworkManager,
|
||||
// which allows users to overwrite it with their own estimations.
|
||||
public static ConnectionQuality connectionQuality = ConnectionQuality.ESTIMATING;
|
||||
public static ConnectionQuality lastConnectionQuality = ConnectionQuality.ESTIMATING;
|
||||
public static ConnectionQualityMethod connectionQualityMethod = ConnectionQualityMethod.Simple;
|
||||
public static float connectionQualityInterval = 3;
|
||||
static double lastConnectionQualityUpdate;
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when connection quality changes.
|
||||
/// <para>First argument is the old quality, second argument is the new quality.</para>
|
||||
/// </summary>
|
||||
public static event Action<ConnectionQuality, ConnectionQuality> onConnectionQualityChanged;
|
||||
|
||||
// initialization //////////////////////////////////////////////////////
|
||||
static void AddTransportHandlers()
|
||||
{
|
||||
@ -130,6 +151,7 @@ static void AddTransportHandlers()
|
||||
Transport.active.OnClientDataReceived += OnTransportData;
|
||||
Transport.active.OnClientDisconnected += OnTransportDisconnected;
|
||||
Transport.active.OnClientError += OnTransportError;
|
||||
Transport.active.OnClientTransportException += OnTransportException;
|
||||
}
|
||||
|
||||
static void RemoveTransportHandlers()
|
||||
@ -139,15 +161,31 @@ static void RemoveTransportHandlers()
|
||||
Transport.active.OnClientDataReceived -= OnTransportData;
|
||||
Transport.active.OnClientDisconnected -= OnTransportDisconnected;
|
||||
Transport.active.OnClientError -= OnTransportError;
|
||||
Transport.active.OnClientTransportException -= OnTransportException;
|
||||
}
|
||||
|
||||
// connect /////////////////////////////////////////////////////////////
|
||||
// initialize is called before every connect
|
||||
static void Initialize(bool hostMode)
|
||||
{
|
||||
// safety: ensure Weaving succeded.
|
||||
// if it silently failed, we would get lots of 'writer not found'
|
||||
// and other random errors at runtime instead. this is cleaner.
|
||||
if (!WeaverFuse.Weaved())
|
||||
{
|
||||
// if it failed, throw an exception to early exit all Connect calls.
|
||||
throw new Exception("NetworkClient won't start because Weaving failed or didn't run.");
|
||||
}
|
||||
|
||||
// Debug.Log($"Client Connect: {address}");
|
||||
Debug.Assert(Transport.active != null, "There was no active transport when calling NetworkClient.Connect, If you are calling Connect manually then make sure to set 'Transport.active' first");
|
||||
|
||||
// reset unbatcher in case any batches from last session remain.
|
||||
// need to do this in Initialize() so it runs for the host as well.
|
||||
// fixes host mode scene transition receiving data from previous scene.
|
||||
// credits: BigBoxVR
|
||||
unbatcher = new Unbatcher();
|
||||
|
||||
// reset time interpolation on every new connect.
|
||||
// ensures last sessions' state is cleared before starting again.
|
||||
InitTimeInterpolation();
|
||||
@ -187,10 +225,6 @@ public static void ConnectHost()
|
||||
HostMode.SetupConnections();
|
||||
}
|
||||
|
||||
// Deprecated 2022-12-12
|
||||
[Obsolete("NetworkClient.ConnectLocalServer was moved to HostMode.InvokeOnConnected")]
|
||||
public static void ConnectLocalServer() => HostMode.InvokeOnConnected();
|
||||
|
||||
// disconnect //////////////////////////////////////////////////////////
|
||||
/// <summary>Disconnect from server.</summary>
|
||||
public static void Disconnect()
|
||||
@ -227,13 +261,11 @@ static void OnTransportConnected()
|
||||
// reset network time stats
|
||||
NetworkTime.ResetStatics();
|
||||
|
||||
// reset unbatcher in case any batches from last session remain.
|
||||
unbatcher = new Unbatcher();
|
||||
|
||||
// the handler may want to send messages to the client
|
||||
// thus we should set the connected state before calling the handler
|
||||
connectState = ConnectState.Connected;
|
||||
NetworkTime.UpdateClient();
|
||||
// ping right away after connecting so client gets new time asap
|
||||
NetworkTime.SendPing();
|
||||
OnConnectedEvent?.Invoke();
|
||||
}
|
||||
else Debug.LogError("Skipped Connect message handling because connection is null.");
|
||||
@ -290,8 +322,14 @@ internal static void OnTransportData(ArraySegment<byte> data, int channelId)
|
||||
// always process all messages in the batch.
|
||||
if (!unbatcher.AddBatch(data))
|
||||
{
|
||||
Debug.LogWarning($"NetworkClient: failed to add batch, disconnecting.");
|
||||
if (exceptionsDisconnect)
|
||||
{
|
||||
Debug.LogError($"NetworkClient: failed to add batch, disconnecting.");
|
||||
connection.Disconnect();
|
||||
}
|
||||
else
|
||||
Debug.LogWarning($"NetworkClient: failed to add batch.");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@ -306,7 +344,9 @@ internal static void OnTransportData(ArraySegment<byte> data, int channelId)
|
||||
// the next time.
|
||||
// => consider moving processing to NetworkEarlyUpdate.
|
||||
while (!isLoadingScene &&
|
||||
unbatcher.GetNextMessage(out NetworkReader reader, out double remoteTimestamp))
|
||||
unbatcher.GetNextMessage(out ArraySegment<byte> message, out double remoteTimestamp))
|
||||
{
|
||||
using (NetworkReaderPooled reader = NetworkReaderPool.Get(message))
|
||||
{
|
||||
// enough to read at least header size?
|
||||
if (reader.Remaining >= NetworkMessages.IdSize)
|
||||
@ -325,20 +365,31 @@ internal static void OnTransportData(ArraySegment<byte> data, int channelId)
|
||||
// so we need to disconnect.
|
||||
// -> return to avoid the below unbatches.count error.
|
||||
// we already disconnected and handled it.
|
||||
Debug.LogWarning($"NetworkClient: failed to unpack and invoke message. Disconnecting.");
|
||||
if (exceptionsDisconnect)
|
||||
{
|
||||
Debug.LogError($"NetworkClient: failed to unpack and invoke message. Disconnecting.");
|
||||
connection.Disconnect();
|
||||
}
|
||||
else
|
||||
Debug.LogWarning($"NetworkClient: failed to unpack and invoke message.");
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
// otherwise disconnect
|
||||
else
|
||||
{
|
||||
// WARNING, not error. can happen if attacker sends random data.
|
||||
Debug.LogWarning($"NetworkClient: received Message was too short (messages should start with message id)");
|
||||
if (exceptionsDisconnect)
|
||||
{
|
||||
Debug.LogError($"NetworkClient: received Message was too short (messages should start with message id). Disconnecting.");
|
||||
connection.Disconnect();
|
||||
}
|
||||
else
|
||||
Debug.LogWarning("NetworkClient: received Message was too short (messages should start with message id)");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if we weren't interrupted by a scene change,
|
||||
// then all batched messages should have been processed now.
|
||||
@ -395,6 +446,7 @@ internal static void OnTransportDisconnected()
|
||||
// now that everything was handled, clear the connection.
|
||||
// previously this was done in Disconnect() already, but we still
|
||||
// need it for the above OnDisconnectedEvent.
|
||||
connection?.Cleanup();
|
||||
connection = null;
|
||||
|
||||
// transport handlers are only added when connecting.
|
||||
@ -411,6 +463,14 @@ static void OnTransportError(TransportError error, string reason)
|
||||
OnErrorEvent?.Invoke(error, reason);
|
||||
}
|
||||
|
||||
static void OnTransportException(Exception exception)
|
||||
{
|
||||
// transport errors will happen. logging a warning is enough.
|
||||
// make sure the user does not panic.
|
||||
Debug.LogWarning($"Client Transport Exception: {exception}. This is fine.");
|
||||
OnTransportExceptionEvent?.Invoke(exception);
|
||||
}
|
||||
|
||||
// send ////////////////////////////////////////////////////////////////
|
||||
/// <summary>Send a NetworkMessage to the server over the given channel.</summary>
|
||||
public static void Send<T>(T message, int channelId = Channels.Reliable)
|
||||
@ -451,6 +511,7 @@ internal static void RegisterMessageHandlers(bool hostMode)
|
||||
RegisterHandler<ObjectDestroyMessage>(OnObjectDestroy);
|
||||
RegisterHandler<ObjectHideMessage>(OnObjectHide);
|
||||
RegisterHandler<NetworkPongMessage>(NetworkTime.OnClientPong, false);
|
||||
RegisterHandler<NetworkPingMessage>(NetworkTime.OnClientPing, false);
|
||||
RegisterHandler<SpawnMessage>(OnSpawn);
|
||||
RegisterHandler<ObjectSpawnStartedMessage>(OnObjectSpawnStarted);
|
||||
RegisterHandler<ObjectSpawnFinishedMessage>(OnObjectSpawnFinished);
|
||||
@ -460,7 +521,7 @@ internal static void RegisterMessageHandlers(bool hostMode)
|
||||
// These handlers are the same for host and remote clients
|
||||
RegisterHandler<TimeSnapshotMessage>(OnTimeSnapshotMessage);
|
||||
RegisterHandler<ChangeOwnerMessage>(OnChangeOwner);
|
||||
RegisterHandler<RpcBufferMessage>(OnRPCBufferMessage);
|
||||
RegisterHandler<RpcMessage>(OnRPCMessage);
|
||||
}
|
||||
|
||||
/// <summary>Register a handler for a message type T. Most should require authentication.</summary>
|
||||
@ -473,21 +534,52 @@ public static void RegisterHandler<T>(Action<T> handler, bool requireAuthenticat
|
||||
Debug.LogWarning($"NetworkClient.RegisterHandler replacing handler for {typeof(T).FullName}, id={msgType}. If replacement is intentional, use ReplaceHandler instead to avoid this warning.");
|
||||
}
|
||||
|
||||
// register Id <> Type in lookup for debugging.
|
||||
NetworkMessages.Lookup[msgType] = typeof(T);
|
||||
|
||||
// we use the same WrapHandler function for server and client.
|
||||
// so let's wrap it to ignore the NetworkConnection parameter.
|
||||
// it's not needed on client. it's always NetworkClient.connection.
|
||||
void HandlerWrapped(NetworkConnection _, T value) => handler(value);
|
||||
handlers[msgType] = NetworkMessages.WrapHandler((Action<NetworkConnection, T>)HandlerWrapped, requireAuthentication);
|
||||
handlers[msgType] = NetworkMessages.WrapHandler((Action<NetworkConnection, T>)HandlerWrapped, requireAuthentication, exceptionsDisconnect);
|
||||
}
|
||||
|
||||
/// <summary>Replace a handler for a particular message type. Should require authentication by default.</summary>
|
||||
// RegisterHandler throws a warning (as it should) if a handler is assigned twice
|
||||
// Use of ReplaceHandler makes it clear the user intended to replace the handler
|
||||
public static void ReplaceHandler<T>(Action<NetworkConnection, T> handler, bool requireAuthentication = true)
|
||||
/// <summary>Register a handler for a message type T. Most should require authentication.</summary>
|
||||
// This version passes channelId to the handler.
|
||||
public static void RegisterHandler<T>(Action<T, int> handler, bool requireAuthentication = true)
|
||||
where T : struct, NetworkMessage
|
||||
{
|
||||
ushort msgType = NetworkMessageId<T>.Id;
|
||||
handlers[msgType] = NetworkMessages.WrapHandler(handler, requireAuthentication);
|
||||
if (handlers.ContainsKey(msgType))
|
||||
{
|
||||
Debug.LogWarning($"NetworkClient.RegisterHandler replacing handler for {typeof(T).FullName}, id={msgType}. If replacement is intentional, use ReplaceHandler instead to avoid this warning.");
|
||||
}
|
||||
|
||||
// register Id <> Type in lookup for debugging.
|
||||
NetworkMessages.Lookup[msgType] = typeof(T);
|
||||
|
||||
// we use the same WrapHandler function for server and client.
|
||||
// so let's wrap it to ignore the NetworkConnection parameter.
|
||||
// it's not needed on client. it's always NetworkClient.connection.
|
||||
void HandlerWrapped(NetworkConnection _, T value, int channelId) => handler(value, channelId);
|
||||
handlers[msgType] = NetworkMessages.WrapHandler((Action<NetworkConnection, T, int>)HandlerWrapped, requireAuthentication, exceptionsDisconnect);
|
||||
}
|
||||
|
||||
// Deprecated 2024-01-21
|
||||
[Obsolete("Use ReplaceHandler without the NetworkConnection parameter instead. This version is obsolete and will be removed soon.")]
|
||||
public static void ReplaceHandler<T>(Action<NetworkConnection, T> handler, bool requireAuthentication = true)
|
||||
where T : struct, NetworkMessage
|
||||
{
|
||||
// we use the same WrapHandler function for server and client.
|
||||
// so let's wrap it to ignore the NetworkConnection parameter.
|
||||
// it's not needed on client. it's always NetworkClient.connection.
|
||||
ushort msgType = NetworkMessageId<T>.Id;
|
||||
|
||||
// register Id <> Type in lookup for debugging.
|
||||
NetworkMessages.Lookup[msgType] = typeof(T);
|
||||
|
||||
void HandlerWrapped(NetworkConnection _, T value) => handler(_, value);
|
||||
handlers[msgType] = NetworkMessages.WrapHandler((Action<NetworkConnection, T>)HandlerWrapped, requireAuthentication, exceptionsDisconnect);
|
||||
}
|
||||
|
||||
/// <summary>Replace a handler for a particular message type. Should require authentication by default.</summary>
|
||||
@ -496,7 +588,34 @@ public static void ReplaceHandler<T>(Action<NetworkConnection, T> handler, bool
|
||||
public static void ReplaceHandler<T>(Action<T> handler, bool requireAuthentication = true)
|
||||
where T : struct, NetworkMessage
|
||||
{
|
||||
ReplaceHandler((NetworkConnection _, T value) => { handler(value); }, requireAuthentication);
|
||||
// we use the same WrapHandler function for server and client.
|
||||
// so let's wrap it to ignore the NetworkConnection parameter.
|
||||
// it's not needed on client. it's always NetworkClient.connection.
|
||||
ushort msgType = NetworkMessageId<T>.Id;
|
||||
|
||||
// register Id <> Type in lookup for debugging.
|
||||
NetworkMessages.Lookup[msgType] = typeof(T);
|
||||
|
||||
void HandlerWrapped(NetworkConnection _, T value) => handler(value);
|
||||
handlers[msgType] = NetworkMessages.WrapHandler((Action<NetworkConnection, T>)HandlerWrapped, requireAuthentication, exceptionsDisconnect);
|
||||
}
|
||||
|
||||
/// <summary>Replace a handler for a particular message type. Should require authentication by default. This version passes channelId to the handler.</summary>
|
||||
// RegisterHandler throws a warning (as it should) if a handler is assigned twice
|
||||
// Use of ReplaceHandler makes it clear the user intended to replace the handler
|
||||
public static void ReplaceHandler<T>(Action<T, int> handler, bool requireAuthentication = true)
|
||||
where T : struct, NetworkMessage
|
||||
{
|
||||
// we use the same WrapHandler function for server and client.
|
||||
// so let's wrap it to ignore the NetworkConnection parameter.
|
||||
// it's not needed on client. it's always NetworkClient.connection.
|
||||
ushort msgType = NetworkMessageId<T>.Id;
|
||||
|
||||
// register Id <> Type in lookup for debugging.
|
||||
NetworkMessages.Lookup[msgType] = typeof(T);
|
||||
|
||||
void HandlerWrapped(NetworkConnection _, T value, int channelId) => handler(value, channelId);
|
||||
handlers[msgType] = NetworkMessages.WrapHandler((Action<NetworkConnection, T, int>)HandlerWrapped, requireAuthentication, exceptionsDisconnect);
|
||||
}
|
||||
|
||||
/// <summary>Unregister a message handler of type T.</summary>
|
||||
@ -534,6 +653,9 @@ static void RegisterPrefabIdentity(NetworkIdentity prefab)
|
||||
return;
|
||||
}
|
||||
|
||||
// disallow child NetworkIdentities.
|
||||
// TODO likely not necessary anymore due to the new check in
|
||||
// NetworkIdentity.OnValidate.
|
||||
NetworkIdentity[] identities = prefab.GetComponentsInChildren<NetworkIdentity>();
|
||||
if (identities.Length > 1)
|
||||
{
|
||||
@ -1167,11 +1289,12 @@ public static void PrepareToSpawnSceneObjects()
|
||||
foreach (NetworkIdentity identity in allIdentities)
|
||||
{
|
||||
// add all unspawned NetworkIdentities to spawnable objects
|
||||
// need to ensure it's not active yet because
|
||||
// need to check netId to make sure object is not spawned
|
||||
// fixes: https://github.com/MirrorNetworking/Mirror/issues/3541
|
||||
// PrepareToSpawnSceneObjects may be called multiple times in case
|
||||
// the ObjectSpawnStarted message is received multiple times.
|
||||
if (Utils.IsSceneObject(identity) &&
|
||||
!identity.gameObject.activeSelf)
|
||||
identity.netId == 0)
|
||||
{
|
||||
if (spawnableObjects.TryGetValue(identity.sceneId, out NetworkIdentity existingIdentity))
|
||||
{
|
||||
@ -1277,7 +1400,7 @@ static void OnEntityStateMessage(EntityStateMessage message)
|
||||
using (NetworkReaderPooled reader = NetworkReaderPool.Get(message.payload))
|
||||
identity.DeserializeClient(reader, false);
|
||||
}
|
||||
else Debug.LogWarning($"Did not find target for sync message for {message.netId} . Note: this can be completely normal because UDP messages may arrive out of order, so this message might have arrived after a Destroy message.");
|
||||
else Debug.LogWarning($"Did not find target for sync message for {message.netId}. Were all prefabs added to the NetworkManager's spawnable list?\nNote: this can be completely normal because UDP messages may arrive out of order, so this message might have arrived after a Destroy message.");
|
||||
}
|
||||
|
||||
static void OnRPCMessage(RpcMessage message)
|
||||
@ -1291,21 +1414,6 @@ static void OnRPCMessage(RpcMessage message)
|
||||
// Rpcs often can't be applied if interest management unspawned them
|
||||
}
|
||||
|
||||
static void OnRPCBufferMessage(RpcBufferMessage message)
|
||||
{
|
||||
// Debug.Log($"NetworkClient.OnRPCBufferMessage of {message.payload.Count} bytes");
|
||||
// parse all rpc messages from the buffer
|
||||
using (NetworkReaderPooled reader = NetworkReaderPool.Get(message.payload))
|
||||
{
|
||||
while (reader.Remaining > 0)
|
||||
{
|
||||
// read message without header
|
||||
RpcMessage rpcMessage = reader.Read<RpcMessage>();
|
||||
OnRPCMessage(rpcMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void OnObjectHide(ObjectHideMessage message) => DestroyObject(message.netId);
|
||||
|
||||
internal static void OnObjectDestroy(ObjectDestroyMessage message) => DestroyObject(message.netId);
|
||||
@ -1455,9 +1563,6 @@ static void Broadcast()
|
||||
payload = writer.ToArraySegment()
|
||||
};
|
||||
Send(message);
|
||||
|
||||
// reset dirty bits so it's not resent next time.
|
||||
identity.ClearDirtyComponentsDirtyBits();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1508,6 +1613,38 @@ internal static void NetworkLateUpdate()
|
||||
{
|
||||
Broadcast();
|
||||
}
|
||||
|
||||
UpdateConnectionQuality();
|
||||
}
|
||||
|
||||
// Connection Quality //////////////////////////////////////////////////
|
||||
// uses 'pragmatic' version based on snapshot interpolation by default.
|
||||
void UpdateConnectionQuality()
|
||||
{
|
||||
// only recalculate every few seconds
|
||||
// we don't want to fire Good->Bad->Good->Bad dozens of times per second.
|
||||
if (connectionQualityInterval > 0 && NetworkTime.time > lastConnectionQualityUpdate + connectionQualityInterval)
|
||||
{
|
||||
lastConnectionQualityUpdate = NetworkTime.time;
|
||||
|
||||
switch (connectionQualityMethod)
|
||||
{
|
||||
case ConnectionQualityMethod.Simple:
|
||||
connectionQuality = ConnectionQualityHeuristics.Simple(NetworkTime.rtt, NetworkTime.rttVariance);
|
||||
break;
|
||||
case ConnectionQualityMethod.Pragmatic:
|
||||
connectionQuality = ConnectionQualityHeuristics.Pragmatic(initialBufferTime, bufferTime);
|
||||
break;
|
||||
}
|
||||
|
||||
if (lastConnectionQuality != connectionQuality)
|
||||
{
|
||||
// Invoke the event before assigning the new value so
|
||||
// the event handler can compare old and new values.
|
||||
onConnectionQualityChanged?.Invoke(lastConnectionQuality, connectionQuality);
|
||||
lastConnectionQuality = connectionQuality;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// update connections to flush out messages _after_ broadcast
|
||||
@ -1570,7 +1707,7 @@ public static void DestroyAllClientObjects()
|
||||
// unspawned objects should be reset for reuse later.
|
||||
if (wasUnspawned)
|
||||
{
|
||||
identity.Reset();
|
||||
identity.ResetState();
|
||||
}
|
||||
// without unspawn handler, we need to disable/destroy.
|
||||
else
|
||||
@ -1579,7 +1716,7 @@ public static void DestroyAllClientObjects()
|
||||
// they always stay in the scene, we don't destroy them.
|
||||
if (identity.sceneId != 0)
|
||||
{
|
||||
identity.Reset();
|
||||
identity.ResetState();
|
||||
identity.gameObject.SetActive(false);
|
||||
}
|
||||
// spawned objects are destroyed
|
||||
@ -1615,7 +1752,7 @@ static void DestroyObject(uint netId)
|
||||
if (InvokeUnSpawnHandler(identity.assetId, identity.gameObject))
|
||||
{
|
||||
// reset object after user's handler
|
||||
identity.Reset();
|
||||
identity.ResetState();
|
||||
}
|
||||
// otherwise fall back to default Destroy
|
||||
else if (identity.sceneId == 0)
|
||||
@ -1629,7 +1766,7 @@ static void DestroyObject(uint netId)
|
||||
identity.gameObject.SetActive(false);
|
||||
spawnableObjects[identity.sceneId] = identity;
|
||||
// reset for scene objects
|
||||
identity.Reset();
|
||||
identity.ResetState();
|
||||
}
|
||||
|
||||
// remove from dictionary no matter how it is unspawned
|
||||
@ -1692,6 +1829,7 @@ public static void Shutdown()
|
||||
OnConnectedEvent = null;
|
||||
OnDisconnectedEvent = null;
|
||||
OnErrorEvent = null;
|
||||
OnTransportExceptionEvent = null;
|
||||
}
|
||||
|
||||
// GUI /////////////////////////////////////////////////////////////////
|
||||
@ -1702,7 +1840,7 @@ public static void OnGUI()
|
||||
// only if in world
|
||||
if (!ready) return;
|
||||
|
||||
GUILayout.BeginArea(new Rect(10, 5, 800, 50));
|
||||
GUILayout.BeginArea(new Rect(10, 5, 1020, 50));
|
||||
|
||||
GUILayout.BeginHorizontal("Box");
|
||||
GUILayout.Label("Snapshot Interp.:");
|
||||
@ -1715,8 +1853,10 @@ public static void OnGUI()
|
||||
GUILayout.Box($"DriftEMA: {NetworkClient.driftEma.Value:F2}");
|
||||
GUILayout.Box($"DelTimeEMA: {NetworkClient.deliveryTimeEma.Value:F2}");
|
||||
GUILayout.Box($"timescale: {localTimescale:F2}");
|
||||
GUILayout.Box($"BTM: {snapshotSettings.bufferTimeMultiplier:F2}");
|
||||
GUILayout.Box($"RTT: {NetworkTime.rtt * 1000:000}");
|
||||
GUILayout.Box($"BTM: {NetworkClient.bufferTimeMultiplier:F2}"); // current dynamically adjusted multiplier
|
||||
GUILayout.Box($"RTT: {NetworkTime.rtt * 1000:F0}ms");
|
||||
GUILayout.Box($"PredErrUNADJ: {NetworkTime.predictionErrorUnadjusted * 1000:F0}ms");
|
||||
GUILayout.Box($"PredErrADJ: {NetworkTime.predictionErrorAdjusted * 1000:F0}ms");
|
||||
GUILayout.EndHorizontal();
|
||||
|
||||
GUILayout.EndArea();
|
||||
|
@ -11,23 +11,16 @@ public static partial class NetworkClient
|
||||
// via NetMan or NetworkClientConfig or NetworkClient as component etc.
|
||||
public static SnapshotInterpolationSettings snapshotSettings = new SnapshotInterpolationSettings();
|
||||
|
||||
// obsolete snapshot settings access
|
||||
// DEPRECATED 2023-03-11
|
||||
[Obsolete("NetworkClient snapshot interpolation settings were moved to NetworkClient.snapshotSettings.*")]
|
||||
public static double bufferTimeMultiplier => snapshotSettings.bufferTimeMultiplier;
|
||||
[Obsolete("NetworkClient snapshot interpolation settings were moved to NetworkClient.snapshotSettings.*")]
|
||||
public static float catchupNegativeThreshold => snapshotSettings.catchupNegativeThreshold;
|
||||
[Obsolete("NetworkClient snapshot interpolation settings were moved to NetworkClient.snapshotSettings.*")]
|
||||
public static float catchupPositiveThreshold => snapshotSettings.catchupPositiveThreshold;
|
||||
[Obsolete("NetworkClient snapshot interpolation settings were moved to NetworkClient.snapshotSettings.*")]
|
||||
public static double catchupSpeed => snapshotSettings.catchupSpeed;
|
||||
[Obsolete("NetworkClient snapshot interpolation settings were moved to NetworkClient.snapshotSettings.*")]
|
||||
public static double slowdownSpeed => snapshotSettings.slowdownSpeed;
|
||||
[Obsolete("NetworkClient snapshot interpolation settings were moved to NetworkClient.snapshotSettings.*")]
|
||||
public static int driftEmaDuration => snapshotSettings.driftEmaDuration;
|
||||
|
||||
// snapshot interpolation runtime data /////////////////////////////////
|
||||
public static double bufferTime => NetworkServer.sendInterval * snapshotSettings.bufferTimeMultiplier;
|
||||
// buffer time is dynamically adjusted.
|
||||
// store the current multiplier here, without touching the original in settings.
|
||||
// this way we can easily reset to or compare with original where needed.
|
||||
public static double bufferTimeMultiplier;
|
||||
|
||||
// original buffer time based on the settings
|
||||
// dynamically adjusted buffer time based on dynamically adjusted multiplier
|
||||
public static double initialBufferTime => NetworkServer.sendInterval * snapshotSettings.bufferTimeMultiplier;
|
||||
public static double bufferTime => NetworkServer.sendInterval * bufferTimeMultiplier;
|
||||
|
||||
// <servertime, snaps>
|
||||
public static SortedList<double, TimeSnapshot> snapshots = new SortedList<double, TimeSnapshot>();
|
||||
@ -89,8 +82,7 @@ public static partial class NetworkClient
|
||||
static void InitTimeInterpolation()
|
||||
{
|
||||
// reset timeline, localTimescale & snapshots from last session (if any)
|
||||
// Don't reset bufferTimeMultiplier here - whatever their network condition
|
||||
// was when they disconnected, it won't have changed on immediate reconnect.
|
||||
bufferTimeMultiplier = snapshotSettings.bufferTimeMultiplier;
|
||||
localTimeline = 0;
|
||||
localTimescale = 1;
|
||||
snapshots.Clear();
|
||||
@ -126,7 +118,7 @@ public static void OnTimeSnapshot(TimeSnapshot snap)
|
||||
{
|
||||
// set bufferTime on the fly.
|
||||
// shows in inspector for easier debugging :)
|
||||
snapshotSettings.bufferTimeMultiplier = SnapshotInterpolation.DynamicAdjustment(
|
||||
bufferTimeMultiplier = SnapshotInterpolation.DynamicAdjustment(
|
||||
NetworkServer.sendInterval,
|
||||
deliveryTimeEma.StandardDeviation,
|
||||
snapshotSettings.dynamicAdjustmentTolerance
|
||||
@ -136,6 +128,7 @@ public static void OnTimeSnapshot(TimeSnapshot snap)
|
||||
// insert into the buffer & initialize / adjust / catchup
|
||||
SnapshotInterpolation.InsertAndAdjust(
|
||||
snapshots,
|
||||
snapshotSettings.bufferLimit,
|
||||
snap,
|
||||
ref localTimeline,
|
||||
ref localTimescale,
|
||||
@ -159,6 +152,9 @@ static void UpdateTimeInterpolation()
|
||||
if (snapshots.Count > 0)
|
||||
{
|
||||
// progress local timeline.
|
||||
// NetworkTime uses unscaled time and ignores Time.timeScale.
|
||||
// fixes Time.timeScale getting server & client time out of sync:
|
||||
// https://github.com/MirrorNetworking/Mirror/issues/3409
|
||||
SnapshotInterpolation.StepTime(Time.unscaledDeltaTime, ref localTimeline, localTimescale);
|
||||
|
||||
// progress local interpolation.
|
||||
|
@ -91,41 +91,29 @@ protected Batcher GetBatchForChannelId(int channelId)
|
||||
return batch;
|
||||
}
|
||||
|
||||
// validate packet size before sending. show errors if too big/small.
|
||||
// => it's best to check this here, we can't assume that all transports
|
||||
// would check max size and show errors internally. best to do it
|
||||
// in one place in Mirror.
|
||||
// => it's important to log errors, so the user knows what went wrong.
|
||||
protected static bool ValidatePacketSize(ArraySegment<byte> segment, int channelId)
|
||||
{
|
||||
int max = Transport.active.GetMaxPacketSize(channelId);
|
||||
if (segment.Count > max)
|
||||
{
|
||||
Debug.LogError($"NetworkConnection.ValidatePacketSize: cannot send packet larger than {max} bytes, was {segment.Count} bytes");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (segment.Count == 0)
|
||||
{
|
||||
// zero length packets getting into the packet queues are bad.
|
||||
Debug.LogError("NetworkConnection.ValidatePacketSize: cannot send zero bytes");
|
||||
return false;
|
||||
}
|
||||
|
||||
// good size
|
||||
return true;
|
||||
}
|
||||
|
||||
// Send stage one: NetworkMessage<T>
|
||||
/// <summary>Send a NetworkMessage to this connection over the given channel.</summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Send<T>(T message, int channelId = Channels.Reliable)
|
||||
where T : struct, NetworkMessage
|
||||
{
|
||||
using (NetworkWriterPooled writer = NetworkWriterPool.Get())
|
||||
{
|
||||
// pack message and send allocation free
|
||||
// pack message
|
||||
NetworkMessages.Pack(message, writer);
|
||||
|
||||
// validate packet size immediately.
|
||||
// we know how much can fit into one batch at max.
|
||||
// if it's larger, log an error immediately with the type <T>.
|
||||
// previously we only logged in Update() when processing batches,
|
||||
// but there we don't have type information anymore.
|
||||
int max = NetworkMessages.MaxMessageSize(channelId);
|
||||
if (writer.Position > max)
|
||||
{
|
||||
Debug.LogError($"NetworkConnection.Send: message of type {typeof(T)} with a size of {writer.Position} bytes is larger than the max allowed message size in one batch: {max}.\nThe message was dropped, please make it smaller.");
|
||||
return;
|
||||
}
|
||||
|
||||
// send allocation free
|
||||
NetworkDiagnostics.OnSend(message, channelId, writer.Position, 1);
|
||||
Send(writer.ToArraySegment(), channelId);
|
||||
}
|
||||
@ -134,6 +122,7 @@ public void Send<T>(T message, int channelId = Channels.Reliable)
|
||||
// Send stage two: serialized NetworkMessage as ArraySegment<byte>
|
||||
// internal because no one except Mirror should send bytes directly to
|
||||
// the client. they would be detected as a message. send messages instead.
|
||||
// => make sure to validate message<T> size before calling Send<byte>!
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal virtual void Send(ArraySegment<byte> segment, int channelId = Channels.Reliable)
|
||||
{
|
||||
@ -159,7 +148,6 @@ internal virtual void Send(ArraySegment<byte> segment, int channelId = Channels.
|
||||
}
|
||||
|
||||
// Send stage three: hand off to transport
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected abstract void SendToTransport(ArraySegment<byte> segment, int channelId = Channels.Reliable);
|
||||
|
||||
// flush batched messages at the end of every Update.
|
||||
@ -176,14 +164,10 @@ internal virtual void Update()
|
||||
// make a batch with our local time (double precision)
|
||||
while (kvp.Value.GetBatch(writer))
|
||||
{
|
||||
// validate packet before handing the batch to the
|
||||
// transport. this guarantees that we always stay
|
||||
// within transport's max message size limit.
|
||||
// => just in case transport forgets to check it
|
||||
// => just in case mirror miscalulated it etc.
|
||||
// message size is validated in Send<T>, with test coverage.
|
||||
// we can send directly without checking again.
|
||||
ArraySegment<byte> segment = writer.ToArraySegment();
|
||||
if (ValidatePacketSize(segment, kvp.Key))
|
||||
{
|
||||
|
||||
// send to transport
|
||||
SendToTransport(segment, kvp.Key);
|
||||
//UnityEngine.Debug.Log($"sending batch of {writer.Position} bytes for channel={kvp.Key} connId={connectionId}");
|
||||
@ -194,7 +178,6 @@ internal virtual void Update()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Check if we received a message within the last 'timeout' seconds.</summary>
|
||||
internal virtual bool IsAlive(float timeout) => Time.time - lastMessageTime < timeout;
|
||||
@ -219,6 +202,18 @@ internal virtual void Update()
|
||||
// then later the transport events will do the clean up.
|
||||
public abstract void Disconnect();
|
||||
|
||||
// cleanup is called before the connection is removed.
|
||||
// return any batches' pooled writers before the connection disappears.
|
||||
// otherwise if a connection disappears before flushing, writers would
|
||||
// never be returned to the pool.
|
||||
public virtual void Cleanup()
|
||||
{
|
||||
foreach (Batcher batcher in batches.Values)
|
||||
{
|
||||
batcher.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString() => $"connection({connectionId})";
|
||||
}
|
||||
}
|
||||
|
@ -20,10 +20,6 @@ public class NetworkConnectionToClient : NetworkConnection
|
||||
// TODO move to server's NetworkConnectionToClient?
|
||||
public readonly HashSet<NetworkIdentity> observing = new HashSet<NetworkIdentity>();
|
||||
|
||||
// Deprecated 2022-10-13
|
||||
[Obsolete(".clientOwnedObjects was renamed to .owned :)")]
|
||||
public HashSet<NetworkIdentity> clientOwnedObjects => owned;
|
||||
|
||||
// unbatcher
|
||||
public Unbatcher unbatcher = new Unbatcher();
|
||||
|
||||
@ -46,6 +42,14 @@ public class NetworkConnectionToClient : NetworkConnection
|
||||
// Snapshot Buffer size limit to avoid ever growing list memory consumption attacks from clients.
|
||||
public int snapshotBufferSizeLimit = 64;
|
||||
|
||||
// ping for rtt (round trip time)
|
||||
// useful for statistics, lag compensation, etc.
|
||||
double lastPingTime = 0;
|
||||
internal ExponentialMovingAverage _rtt = new ExponentialMovingAverage(NetworkTime.PingWindowSize);
|
||||
|
||||
/// <summary>Round trip time (in seconds) that it takes a message to go server->client->server.</summary>
|
||||
public double rtt => _rtt.Value;
|
||||
|
||||
public NetworkConnectionToClient(int networkConnectionId)
|
||||
: base(networkConnectionId)
|
||||
{
|
||||
@ -80,6 +84,7 @@ public void OnTimeSnapshot(TimeSnapshot snapshot)
|
||||
// insert into the server buffer & initialize / adjust / catchup
|
||||
SnapshotInterpolation.InsertAndAdjust(
|
||||
snapshots,
|
||||
NetworkClient.snapshotSettings.bufferLimit,
|
||||
snapshot,
|
||||
ref remoteTimeline,
|
||||
ref remoteTimescale,
|
||||
@ -115,69 +120,24 @@ public void UpdateTimeInterpolation()
|
||||
protected override void SendToTransport(ArraySegment<byte> segment, int channelId = Channels.Reliable) =>
|
||||
Transport.active.ServerSend(connectionId, segment, channelId);
|
||||
|
||||
void FlushRpcs(NetworkWriter buffer, int channelId)
|
||||
protected virtual void UpdatePing()
|
||||
{
|
||||
if (buffer.Position > 0)
|
||||
// localTime (double) instead of Time.time for accuracy over days
|
||||
if (NetworkTime.localTime >= lastPingTime + NetworkTime.PingInterval)
|
||||
{
|
||||
Send(new RpcBufferMessage { payload = buffer }, channelId);
|
||||
buffer.Position = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// helper for both channels
|
||||
void BufferRpc(RpcMessage message, NetworkWriter buffer, int channelId, int maxMessageSize)
|
||||
{
|
||||
// calculate buffer limit. we can only fit so much into a message.
|
||||
// max - message header - WriteArraySegment size header - batch header
|
||||
int bufferLimit = maxMessageSize - NetworkMessages.IdSize - sizeof(int) - Batcher.HeaderSize;
|
||||
|
||||
// remember previous valid position
|
||||
int before = buffer.Position;
|
||||
|
||||
// serialize the message without header
|
||||
buffer.Write(message);
|
||||
|
||||
// before we potentially flush out old messages,
|
||||
// let's ensure this single message can even fit the limit.
|
||||
// otherwise no point in flushing.
|
||||
int messageSize = buffer.Position - before;
|
||||
if (messageSize > bufferLimit)
|
||||
{
|
||||
Debug.LogWarning($"NetworkConnectionToClient: discarded RpcMesage for netId={message.netId} componentIndex={message.componentIndex} functionHash={message.functionHash} because it's larger than the rpc buffer limit of {bufferLimit} bytes for the channel: {channelId}");
|
||||
return;
|
||||
}
|
||||
|
||||
// too much to fit into max message size?
|
||||
// then flush first, then write it again.
|
||||
// (message + message header + 4 bytes WriteArraySegment header)
|
||||
if (buffer.Position > bufferLimit)
|
||||
{
|
||||
buffer.Position = before;
|
||||
FlushRpcs(buffer, channelId); // this resets position
|
||||
buffer.Write(message);
|
||||
}
|
||||
}
|
||||
|
||||
internal void BufferRpc(RpcMessage message, int channelId)
|
||||
{
|
||||
int maxMessageSize = Transport.active.GetMaxPacketSize(channelId);
|
||||
if (channelId == Channels.Reliable)
|
||||
{
|
||||
BufferRpc(message, reliableRpcs, Channels.Reliable, maxMessageSize);
|
||||
}
|
||||
else if (channelId == Channels.Unreliable)
|
||||
{
|
||||
BufferRpc(message, unreliableRpcs, Channels.Unreliable, maxMessageSize);
|
||||
// TODO it would be safer for the server to store the last N
|
||||
// messages' timestamp and only send a message number.
|
||||
// This way client's can't just modify the timestamp.
|
||||
// predictedTime parameter is 0 because the server doesn't predict.
|
||||
NetworkPingMessage pingMessage = new NetworkPingMessage(NetworkTime.localTime, 0);
|
||||
Send(pingMessage, Channels.Unreliable);
|
||||
lastPingTime = NetworkTime.localTime;
|
||||
}
|
||||
}
|
||||
|
||||
internal override void Update()
|
||||
{
|
||||
// send rpc buffers
|
||||
FlushRpcs(reliableRpcs, Channels.Reliable);
|
||||
FlushRpcs(unreliableRpcs, Channels.Unreliable);
|
||||
|
||||
// call base update to flush out batched messages
|
||||
UpdatePing();
|
||||
base.Update();
|
||||
}
|
||||
|
||||
@ -244,6 +204,11 @@ internal void DestroyOwnedObjects()
|
||||
{
|
||||
if (netIdentity != null)
|
||||
{
|
||||
// unspawn scene objects, destroy instantiated objects.
|
||||
// fixes: https://github.com/MirrorNetworking/Mirror/issues/3538
|
||||
if (netIdentity.sceneId != 0)
|
||||
NetworkServer.UnSpawn(netIdentity.gameObject);
|
||||
else
|
||||
NetworkServer.Destroy(netIdentity.gameObject);
|
||||
}
|
||||
}
|
||||
|
@ -93,9 +93,8 @@ public sealed class NetworkIdentity : MonoBehaviour
|
||||
// for example: main player & pets are owned. monsters & npcs aren't.
|
||||
public bool isOwned { get; internal set; }
|
||||
|
||||
// Deprecated 2022-10-13
|
||||
[Obsolete(".hasAuthority was renamed to .isOwned. This is easier to understand and prepares for SyncDirection, where there is a difference betwen isOwned and authority.")]
|
||||
public bool hasAuthority => isOwned;
|
||||
// internal so NetworkManager can reset it from StopClient.
|
||||
internal bool clientStarted;
|
||||
|
||||
/// <summary>The set of network connections (players) that can see this object.</summary>
|
||||
public readonly Dictionary<int, NetworkConnectionToClient> observers =
|
||||
@ -115,7 +114,7 @@ public sealed class NetworkIdentity : MonoBehaviour
|
||||
//
|
||||
// it's also easier to work with for serialization etc.
|
||||
// serialized and visible in inspector for easier debugging
|
||||
[SerializeField] uint _assetId;
|
||||
[SerializeField, HideInInspector] uint _assetId;
|
||||
|
||||
// The AssetId trick:
|
||||
// Ideally we would have a serialized 'Guid m_AssetId' but Unity can't
|
||||
@ -198,10 +197,18 @@ internal set
|
||||
// ForceHidden = useful to hide monsters while they respawn etc.
|
||||
// ForceShown = useful to have score NetworkIdentities that always broadcast
|
||||
// to everyone etc.
|
||||
//
|
||||
// TODO rename to 'visibility' after removing .visibility some day!
|
||||
[Tooltip("Visibility can overwrite interest management. ForceHidden can be useful to hide monsters while they respawn. ForceShown can be useful for score NetworkIdentities that should always broadcast to everyone in the world.")]
|
||||
public Visibility visible = Visibility.Default;
|
||||
[FormerlySerializedAs("visible")]
|
||||
public Visibility visibility = Visibility.Default;
|
||||
|
||||
// Deprecated 2024-01-21
|
||||
[HideInInspector]
|
||||
[Obsolete("Deprecated - Use .visibility instead. This will be removed soon.")]
|
||||
public Visibility visible
|
||||
{
|
||||
get => visibility;
|
||||
set => visibility = value;
|
||||
}
|
||||
|
||||
// broadcasting serializes all entities around a player for each player.
|
||||
// we don't want to serialize one entity twice in the same tick.
|
||||
@ -292,9 +299,12 @@ internal static void ResetServerStatics()
|
||||
// BUT internal so tests can add them after creating the NetworkIdentity
|
||||
internal void InitializeNetworkBehaviours()
|
||||
{
|
||||
// Get all NetworkBehaviours
|
||||
// (never null. GetComponents returns [] if none found)
|
||||
NetworkBehaviours = GetComponents<NetworkBehaviour>();
|
||||
// Get all NetworkBehaviour components, including children.
|
||||
// Some users need NetworkTransform on child bones, etc.
|
||||
// => Deterministic: https://forum.unity.com/threads/getcomponentsinchildren.4582/#post-33983
|
||||
// => Never null. GetComponents returns [] if none found.
|
||||
// => Include inactive. We need all child components.
|
||||
NetworkBehaviours = GetComponentsInChildren<NetworkBehaviour>(true);
|
||||
ValidateComponents();
|
||||
|
||||
// initialize each one
|
||||
@ -347,18 +357,56 @@ void OnValidate()
|
||||
hasSpawned = false;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
DisallowChildNetworkIdentities();
|
||||
SetupIDs();
|
||||
#endif
|
||||
}
|
||||
|
||||
// expose our AssetId Guid to uint mapping code in case projects need to map Guids to uint as well.
|
||||
// this way their projects won't break if we change our mapping algorithm.
|
||||
// needs to be available at runtime / builds, don't wrap in #if UNITY_EDITOR
|
||||
public static uint AssetGuidToUint(Guid guid) => (uint)guid.GetHashCode(); // deterministic
|
||||
|
||||
#if UNITY_EDITOR
|
||||
// child NetworkIdentities are not supported.
|
||||
// Disallow them and show an error for the user to fix.
|
||||
// This needs to work for Prefabs & Scene objects, so the previous check
|
||||
// in NetworkClient.RegisterPrefab is not enough.
|
||||
void DisallowChildNetworkIdentities()
|
||||
{
|
||||
#if UNITY_2020_3_OR_NEWER
|
||||
NetworkIdentity[] identities = GetComponentsInChildren<NetworkIdentity>(true);
|
||||
#else
|
||||
NetworkIdentity[] identities = GetComponentsInChildren<NetworkIdentity>();
|
||||
#endif
|
||||
if (identities.Length > 1)
|
||||
{
|
||||
// always log the next child component so it's easy to fix.
|
||||
// if there are multiple, then after removing it'll log the next.
|
||||
Debug.LogError($"'{name}' has another NetworkIdentity component on '{identities[1].name}'. There should only be one NetworkIdentity, and it must be on the root object. Please remove the other one.", this);
|
||||
}
|
||||
}
|
||||
|
||||
void AssignAssetID(string path)
|
||||
{
|
||||
// only set if not empty. fixes https://github.com/vis2k/Mirror/issues/2765
|
||||
if (!string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
// if we generate the assetId then we MUST be sure to set dirty
|
||||
// in order to save the prefab object properly. otherwise it
|
||||
// would be regenerated every time we reopen the prefab.
|
||||
// -> Undo.RecordObject is the new EditorUtility.SetDirty!
|
||||
// -> we need to call it before changing.
|
||||
//
|
||||
// to verify this, duplicate a prefab and double click to open it.
|
||||
// add a log message if "_assetId != before_".
|
||||
// without RecordObject, it'll log every time because it's not saved.
|
||||
Undo.RecordObject(this, "Assigned AssetId");
|
||||
|
||||
// uint before = _assetId;
|
||||
Guid guid = new Guid(AssetDatabase.AssetPathToGUID(path));
|
||||
assetId = (uint)guid.GetHashCode(); // deterministic
|
||||
assetId = AssetGuidToUint(guid);
|
||||
// if (_assetId != before) Debug.Log($"Assigned assetId={assetId} to {name}");
|
||||
}
|
||||
}
|
||||
|
||||
@ -670,7 +718,6 @@ internal void OnStopServer()
|
||||
}
|
||||
}
|
||||
|
||||
bool clientStarted;
|
||||
internal void OnStartClient()
|
||||
{
|
||||
if (clientStarted) return;
|
||||
@ -919,6 +966,19 @@ internal void SerializeServer(bool initialState, NetworkWriter ownerWriter, Netw
|
||||
if (ownerDirty) ownerWriter.WriteBytes(segment.Array, segment.Offset, segment.Count);
|
||||
if (observersDirty) observersWriter.WriteBytes(segment.Array, segment.Offset, segment.Count);
|
||||
}
|
||||
|
||||
// clear dirty bits for the components that we serialized.
|
||||
// do not clear for _all_ components, only the ones that
|
||||
// were dirty and had their syncInterval elapsed.
|
||||
//
|
||||
// we don't want to clear bits before the syncInterval
|
||||
// was elapsed, as then they wouldn't be synced.
|
||||
//
|
||||
// only clear for delta, not for full (spawn messages).
|
||||
// otherwise if a player joins, we serialize monster,
|
||||
// and shouldn't clear dirty bits not yet synced to
|
||||
// other players.
|
||||
if (!initialState) comp.ClearAllDirtyBits();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -968,6 +1028,14 @@ internal void SerializeClient(NetworkWriter writer)
|
||||
// serialize into writer.
|
||||
// server always knows initialState, we never need to send it
|
||||
comp.Serialize(writer, false);
|
||||
|
||||
// clear dirty bits for the components that we serialized.
|
||||
// do not clear for _all_ components, only the ones that
|
||||
// were dirty and had their syncInterval elapsed.
|
||||
//
|
||||
// we don't want to clear bits before the syncInterval
|
||||
// was elapsed, as then they wouldn't be synced.
|
||||
comp.ClearAllDirtyBits();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1066,27 +1134,6 @@ internal NetworkIdentitySerialization GetServerSerializationAtTick(int tick)
|
||||
lastSerialization.ownerWriter,
|
||||
lastSerialization.observersWriter);
|
||||
|
||||
// clear dirty bits for the components that we serialized.
|
||||
// previously we did this in NetworkServer.BroadcastToConnection
|
||||
// for every connection, for every entity.
|
||||
// but we only serialize each entity once, right here in this
|
||||
// 'lastSerialization.tick != tick' scope.
|
||||
// so only do it once.
|
||||
//
|
||||
// NOTE: not in Serializell as that should only do one
|
||||
// thing: serialize data.
|
||||
//
|
||||
//
|
||||
// NOTE: DO NOT clear ALL component's dirty bits, because
|
||||
// components can have different syncIntervals and we
|
||||
// don't want to reset dirty bits for the ones that were
|
||||
// not synced yet.
|
||||
//
|
||||
// NOTE: this used to be very important to avoid ever growing
|
||||
// SyncList changes if they had no observers, but we've
|
||||
// added SyncObject.isRecording since.
|
||||
ClearDirtyComponentsDirtyBits();
|
||||
|
||||
// set tick
|
||||
lastSerialization.tick = tick;
|
||||
//Debug.Log($"{name} (netId={netId}) serialized for tick={tickTimeStamp}");
|
||||
@ -1096,23 +1143,6 @@ internal NetworkIdentitySerialization GetServerSerializationAtTick(int tick)
|
||||
return lastSerialization;
|
||||
}
|
||||
|
||||
// Clear only dirty component's dirty bits. ignores components which
|
||||
// may be dirty but not ready to be synced yet (because of syncInterval)
|
||||
//
|
||||
// NOTE: this used to be very important to avoid ever
|
||||
// growing SyncList changes if they had no observers,
|
||||
// but we've added SyncObject.isRecording since.
|
||||
internal void ClearDirtyComponentsDirtyBits()
|
||||
{
|
||||
foreach (NetworkBehaviour comp in NetworkBehaviours)
|
||||
{
|
||||
if (comp.IsDirty())
|
||||
{
|
||||
comp.ClearAllDirtyBits();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal void AddObserver(NetworkConnectionToClient conn)
|
||||
{
|
||||
if (observers.ContainsKey(conn.connectionId))
|
||||
@ -1264,7 +1294,7 @@ public void RemoveClientAuthority()
|
||||
// the identity during destroy as people might want to be able to read
|
||||
// the members inside OnDestroy(), and we have no way of invoking reset
|
||||
// after OnDestroy is called.
|
||||
internal void Reset()
|
||||
internal void ResetState()
|
||||
{
|
||||
hasSpawned = false;
|
||||
clientStarted = false;
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user