Merged master

This commit is contained in:
MrGadget 2024-04-10 14:53:56 -04:00
commit 8e873e5a1d
299 changed files with 17502 additions and 1870 deletions

View File

@ -13,9 +13,9 @@ jobs:
unityVersion:
- 2019.4.40f1
- 2020.3.48f1
- 2021.3.33f1
- 2022.3.14f1
- 2023.2.2f1
- 2021.3.36f1
- 2022.3.22f1
- 2023.2.16f1
steps:
- name: Checkout repository
@ -31,7 +31,7 @@ jobs:
# key: Library-${{ matrix.unityVersion }}
- name: Run editor Tests
uses: game-ci/unity-test-runner@main
uses: game-ci/unity-test-runner@v4.0.0
# We can use the same license for all Unity versions
env:
@ -47,13 +47,15 @@ jobs:
customParameters: -stackTraceLogType None
- name: Archive test results
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
if: always()
with:
name: Test Results ${{ matrix.unityVersion }}
path: artifacts
- name: Publish test results
uses: MirrorNetworking/nunit-reporter@master
if: always()
with:
reportTitle: Test Report ${{ matrix.unityVersion }}
path: "artifacts/*.xml"

View File

@ -11,7 +11,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
fetch-depth: 0
@ -32,7 +32,7 @@ jobs:
- name: Package
run: unity-packer pack Mirror.unitypackage Assets/Mirror Assets/Mirror Assets/ScriptTemplates Assets/ScriptTemplates LICENSE Assets/Mirror/LICENSE
- uses: actions/upload-artifact@v3
- uses: actions/upload-artifact@v4
with:
name: Mirror.unitypackage
path: Mirror.unitypackage

View File

@ -16,7 +16,7 @@ jobs:
unityVersion: 2019.4.40f1
- name: Upload License Request
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: ${{ steps.getManualLicenseFile.outputs.filePath }}
path: ${{ steps.getManualLicenseFile.outputs.filePath }}

View File

@ -22,17 +22,14 @@ public static void AddDefineSymbols()
HashSet<string> defines = new HashSet<string>(currentDefines.Split(';'))
{
"MIRROR",
"MIRROR_70_OR_NEWER",
"MIRROR_71_OR_NEWER",
"MIRROR_73_OR_NEWER",
"MIRROR_78_OR_NEWER",
"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_86_OR_NEWER",
"MIRROR_89_OR_NEWER"
};
// only touch PlayerSettings if we actually modified it,

View File

@ -38,8 +38,15 @@ public class NetworkLerpRigidbody : NetworkBehaviour
protected override void OnValidate()
{
base.OnValidate();
Reset();
}
public virtual void Reset()
{
if (target == null)
target = GetComponent<Rigidbody>();
syncDirection = SyncDirection.ClientToServer;
}
void Update()

View File

@ -42,8 +42,15 @@ public class NetworkRigidbody : NetworkBehaviour
protected override void OnValidate()
{
base.OnValidate();
Reset();
}
public virtual void Reset()
{
if (target == null)
target = GetComponent<Rigidbody>();
syncDirection = SyncDirection.ClientToServer;
}
#region Sync vars

View File

@ -40,8 +40,15 @@ public class NetworkRigidbody2D : NetworkBehaviour
protected override void OnValidate()
{
base.OnValidate();
Reset();
}
public virtual void Reset()
{
if (target == null)
target = GetComponent<Rigidbody2D>();
syncDirection = SyncDirection.ClientToServer;
}
#region Sync vars

View File

@ -25,7 +25,7 @@ int GetVisRange(NetworkIdentity identity)
}
[ServerCallback]
public override void Reset()
public override void ResetState()
{
lastRebuildTime = 0D;
CustomRanges.Clear();

View File

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

View File

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

View File

@ -72,7 +72,7 @@ public override void OnRebuildObservers(NetworkIdentity identity, HashSet<Networ
}
[ServerCallback]
public override void Reset()
public override void ResetState()
{
lastRebuildTime = 0D;
}

View File

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

View File

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

View File

@ -0,0 +1,196 @@
// 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]
[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;
}
}
}

View File

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

View File

@ -71,7 +71,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 +87,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 +313,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 +351,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 +396,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);
writer.WriteInt(st.fullPathHash);
writer.WriteFloat(st.normalizedTime);
}
else
{
AnimatorStateInfo st = animator.GetCurrentAnimatorStateInfo(i);
writer.WriteInt(st.fullPathHash);
writer.WriteFloat(st.normalizedTime);
}
AnimatorStateInfo st = animator.IsInTransition(i)
? animator.GetNextAnimatorStateInfo(i)
: animator.GetCurrentAnimatorStateInfo(i);
writer.WriteInt(st.fullPathHash);
writer.WriteFloat(st.normalizedTime);
writer.WriteFloat(animator.GetLayerWeight(i));
}
WriteParameters(writer, initialState);
WriteParameters(writer, true);
}
}
@ -392,11 +422,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 +466,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 +486,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 +513,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 +533,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 +638,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);
}

View File

@ -13,7 +13,7 @@ public class NetworkPingDisplay : MonoBehaviour
{
public Color color = Color.white;
public int padding = 2;
public int width = 100;
public int width = 150;
public int height = 25;
void OnGUI()

View File

@ -92,5 +92,20 @@ protected override void OnValidate()
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;
}
}
}

View File

@ -92,5 +92,20 @@ protected override void OnValidate()
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;
}
}
}

View File

@ -92,5 +92,20 @@ protected override void OnValidate()
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;
}
}
}

View File

@ -92,5 +92,20 @@ protected override void OnValidate()
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;
}
}
}

View File

@ -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))
{
@ -268,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)

View File

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

View File

@ -46,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")]
@ -305,9 +311,9 @@ public void RpcTeleport(Vector3 destination, Quaternion rotation)
}
[ClientRpc]
void RpcReset()
void RpcResetState()
{
Reset();
ResetState();
}
// common Teleport code for client->server and server->client
@ -361,7 +367,7 @@ protected virtual void OnTeleport(Vector3 destination, Quaternion rotation)
// -> 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.
@ -369,9 +375,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;
@ -379,7 +392,7 @@ protected virtual void OnEnable()
protected virtual void OnDisable()
{
Reset();
ResetState();
if (NetworkServer.active)
NetworkIdentity.clientAuthorityCallback -= OnClientAuthorityChanged;
@ -397,8 +410,8 @@ void OnClientAuthorityChanged(NetworkConnectionToClient conn, NetworkIdentity id
if (syncDirection == SyncDirection.ClientToServer)
{
Reset();
RpcReset();
ResetState();
RpcResetState();
}
}

View File

@ -8,10 +8,6 @@ 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;
@ -21,8 +17,6 @@ public class NetworkTransformReliable : NetworkTransformBase
[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,
@ -408,9 +402,9 @@ static void RewriteHistory(
// 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 Reset()
public override void ResetState()
{
base.Reset();
base.ResetState();
// reset delta
lastSerializedPosition = Vector3Long.zero;

View File

@ -1,4 +1,5 @@
// NetworkTransform V2 by mischa (2021-07)
using System.Collections.Generic;
using UnityEngine;
namespace Mirror
@ -6,18 +7,14 @@ namespace Mirror
[AddComponentMenu("Network/Network Transform (Unreliable)")]
public class NetworkTransformUnreliable : NetworkTransformBase
{
[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;
uint sendIntervalCounter = 0;
double lastSendIntervalTime = double.MinValue;
// 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;
@ -31,6 +28,7 @@ public class NetworkTransformUnreliable : NetworkTransformBase
// Used to store last sent snapshots
protected TransformSnapshot lastSnapshot;
protected bool cachedSnapshotComparison;
protected Changed cachedChangedComparison;
protected bool hasSentUnchangedPosition;
// update //////////////////////////////////////////////////////////////
@ -67,7 +65,9 @@ protected virtual void CheckLastSendTime()
// This fixes previous issue of, if sendIntervalMultiplier = 1, we send every frame,
// because intervalCounter is always = 1 in the previous version.
if (sendIntervalCounter == sendIntervalMultiplier)
// Changing == to >= https://github.com/MirrorNetworking/Mirror/issues/3571
if (sendIntervalCounter >= sendIntervalMultiplier)
sendIntervalCounter = 0;
// timeAsDouble not available in older Unity versions.
@ -115,36 +115,68 @@ void UpdateServerBroadcast()
// send snapshot without timestamp.
// receiver gets it from batch timestamp to save bandwidth.
TransformSnapshot snapshot = Construct();
cachedSnapshotComparison = CompareSnapshots(snapshot);
if (cachedSnapshotComparison && hasSentUnchangedPosition && onlySyncOnChange) { return; }
if (compressRotation)
if (changedDetection)
{
RpcServerToClientSyncCompressRotation(
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 ? Compression.CompressQuaternion(snapshot.rotation) : default(uint?),
syncRotation && rotationChanged ? snapshot.rotation : default(Quaternion?),
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;
lastSnapshot = snapshot;
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;
}
}
}
}
@ -211,36 +243,67 @@ void UpdateClientBroadcast()
// send snapshot without timestamp.
// receiver gets it from batch timestamp to save bandwidth.
TransformSnapshot snapshot = Construct();
cachedSnapshotComparison = CompareSnapshots(snapshot);
if (cachedSnapshotComparison && hasSentUnchangedPosition && onlySyncOnChange) { return; }
if (compressRotation)
if (changedDetection)
{
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?)
);
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
{
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?)
);
}
cachedSnapshotComparison = CompareSnapshots(snapshot);
if (cachedSnapshotComparison && hasSentUnchangedPosition && onlySyncOnChange) { return; }
if (cachedSnapshotComparison)
{
hasSentUnchangedPosition = true;
}
else
{
hasSentUnchangedPosition = false;
lastSnapshot = snapshot;
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;
}
}
}
}
@ -317,7 +380,17 @@ void CmdClientToServerSync(Vector3? position, Quaternion? rotation, Vector3? sca
[Command(channel = Channels.Unreliable)]
void CmdClientToServerSyncCompressRotation(Vector3? position, uint? rotation, Vector3? scale)
{
OnClientToServerSync(position, rotation.HasValue ? Compression.DecompressQuaternion((uint)rotation) : target.rotation, 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)
@ -342,7 +415,7 @@ protected virtual void OnClientToServerSync(Vector3? position, Quaternion? rotat
double timeIntervalCheck = bufferResetMultiplier * sendIntervalMultiplier * NetworkClient.sendInterval;
if (serverSnapshots.Count > 0 && serverSnapshots.Values[serverSnapshots.Count - 1].remoteTime + timeIntervalCheck < timestamp)
Reset();
ResetState();
}
AddSnapshot(serverSnapshots, connectionToClient.remoteTimeStamp + timeStampAdjustment + offset, position, rotation, scale);
@ -357,8 +430,20 @@ void RpcServerToClientSync(Vector3? position, Quaternion? rotation, Vector3? sca
// rpc /////////////////////////////////////////////////////////////////
// only unreliable. see comment above of this file.
[ClientRpc(channel = Channels.Unreliable)]
void RpcServerToClientSyncCompressRotation(Vector3? position, uint? rotation, Vector3? scale) =>
OnServerToClientSync(position, rotation.HasValue ? Compression.DecompressQuaternion((uint)rotation) : target.rotation, scale);
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)
@ -385,10 +470,209 @@ protected virtual void OnServerToClientSync(Vector3? position, Quaternion? rotat
double timeIntervalCheck = bufferResetMultiplier * sendIntervalMultiplier * NetworkServer.sendInterval;
if (clientSnapshots.Count > 0 && clientSnapshots.Values[clientSnapshots.Count - 1].remoteTime + timeIntervalCheck < timestamp)
Reset();
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;
}
}
}
}

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

View File

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

View File

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

View File

@ -4,7 +4,10 @@ MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences:
- ghostMaterial: {fileID: 2100000, guid: 411a48b4a197d4924bec3e3809bc9320, type: 2}
- localGhostMaterial: {fileID: 2100000, guid: 411a48b4a197d4924bec3e3809bc9320,
type: 2}
- remoteGhostMaterial: {fileID: 2100000, guid: 04f0b2088c857414393bab3b80356776,
type: 2}
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:

View File

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

View File

@ -0,0 +1 @@
// removed 2024-02-09

View File

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

View File

@ -1,61 +0,0 @@
using System;
using UnityEngine;
namespace Mirror
{
[Obsolete("Prediction is under development, do not use this yet.")]
public class PredictedRigidbodyVisual : MonoBehaviour
{
[Tooltip("The predicted rigidbody to follow.")]
public PredictedRigidbody target;
Rigidbody targetRigidbody;
// settings are applied in the other PredictedRigidbody component and then copied here.
[HideInInspector] public float interpolationSpeed = 15; // 10 is a little too low for billiards at least
[HideInInspector] public float teleportDistanceMultiplier = 10;
// we add this component manually from PredictedRigidbody.
// so assign this in Start. target isn't set in Awake yet.
void Start()
{
targetRigidbody = target.GetComponent<Rigidbody>();
}
// always follow in late update, after update modified positions
void LateUpdate()
{
// if target gets network destroyed for any reason, destroy visual
if (targetRigidbody == null || target.gameObject == null)
{
Destroy(gameObject);
return;
}
// hard follow:
// transform.position = targetRigidbody.position;
// transform.rotation = targetRigidbody.rotation;
// if we are further than N colliders sizes behind, then teleport
float colliderSize = target.GetComponent<Collider>().bounds.size.magnitude;
float threshold = colliderSize * teleportDistanceMultiplier;
float distance = Vector3.Distance(transform.position, targetRigidbody.position);
if (distance > threshold)
{
transform.position = targetRigidbody.position;
transform.rotation = targetRigidbody.rotation;
Debug.Log($"[PredictedRigidbodyVisual] Teleported because distance {distance:F2} > threshold {threshold:F2}");
return;
}
// smoothly interpolate to the target position.
// speed relative to how far away we are
float step = distance * interpolationSpeed;
// 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 step = (distance * distance) * interpolationSpeed;
transform.position = Vector3.MoveTowards(transform.position, targetRigidbody.position, step * Time.deltaTime);
transform.rotation = Quaternion.RotateTowards(transform.rotation, targetRigidbody.rotation, step * Time.deltaTime);
}
}
}

View File

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

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: f595f112a39e4634b670d56991b23823
timeCreated: 1710387026

View 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);
}
}
}

View File

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

View File

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

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 04f0b2088c857414393bab3b80356776
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 2100000
userData:
assetBundleName:
assetBundleVariant:

View File

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

View File

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

View File

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

View File

@ -10,3 +10,4 @@
[assembly: InternalsVisibleTo("Mirror.Tests.Performance.Editor")]
[assembly: InternalsVisibleTo("Mirror.Tests.Performance.Runtime")]
[assembly: InternalsVisibleTo("Mirror.Editor")]
[assembly: InternalsVisibleTo("Mirror.Components")]

View File

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

View File

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

View File

@ -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)
{
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.");
// 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;
NetworkClient.aoi = this;
}
[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,

View File

@ -135,8 +135,9 @@ public bool authority
// -> 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;
@ -312,18 +313,18 @@ protected virtual void OnValidate()
// 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 GetComponentInParents(active)
#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.");
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 GetComponentsInParents(active), we can use this too
#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.");
Debug.LogError($"{GetType()} on {name} requires a NetworkIdentity. Please add a NetworkIdentity component to {name} or it's parents.", this);
}
#endif
#endif

View File

@ -261,7 +261,8 @@ static void OnTransportConnected()
// 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.");
@ -318,8 +319,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.");
connection.Disconnect();
if (exceptionsDisconnect)
{
Debug.LogError($"NetworkClient: failed to add batch, disconnecting.");
connection.Disconnect();
}
else
Debug.LogWarning($"NetworkClient: failed to add batch.");
return;
}
@ -355,17 +362,27 @@ 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.");
connection.Disconnect();
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)");
connection.Disconnect();
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;
}
}
@ -515,14 +532,42 @@ public static void RegisterHandler<T>(Action<T> handler, bool requireAuthenticat
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, exceptionsDisconnect);
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>
@ -531,7 +576,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>
@ -1613,7 +1685,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
@ -1622,7 +1694,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
@ -1658,7 +1730,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)
@ -1672,7 +1744,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

View File

@ -122,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)
{

View File

@ -93,8 +93,8 @@ public sealed class NetworkIdentity : MonoBehaviour
// for example: main player & pets are owned. monsters & npcs aren't.
public bool isOwned { get; internal set; }
// public so NetworkManager can reset it from StopClient.
public bool clientStarted;
// 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 =
@ -197,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.
@ -375,7 +383,7 @@ void DisallowChildNetworkIdentities()
{
// 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.");
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);
}
}
@ -1286,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;

View File

@ -38,7 +38,7 @@ public class NetworkManager : MonoBehaviour
public bool editorAutoStart;
/// <summary>Server Update frequency, per second. Use around 60Hz for fast paced games like Counter-Strike to minimize latency. Use around 30Hz for games like WoW to minimize computations. Use around 1-10Hz for slow paced games like EVE.</summary>
[Tooltip("Server & Client send rate per second. Use 60-100Hz for fast paced games like Counter-Strike to minimize latency. Use around 30Hz for games like WoW to minimize computations. Use around 1-10Hz for slow paced games like EVE.")]
[Tooltip("Server / Client send rate per second.\nUse 60-100Hz for fast paced games like Counter-Strike to minimize latency.\nUse around 30Hz for games like WoW to minimize computations.\nUse around 1-10Hz for slow paced games like EVE.")]
[FormerlySerializedAs("serverTickRate")]
public int sendRate = 60;
@ -586,11 +586,9 @@ void FinishStartHost()
// client will do things before the server is even fully started.
//Debug.Log("StartHostClient called");
SetupClient();
networkAddress = "localhost";
RegisterClientMessages();
// call OnConencted needs to be called AFTER RegisterClientMessages
// InvokeOnConnected needs to be called AFTER RegisterClientMessages
// (https://github.com/vis2k/Mirror/pull/1249/)
HostMode.InvokeOnConnected();
@ -786,7 +784,8 @@ void RegisterClientMessages()
NetworkClient.OnConnectedEvent = OnClientConnectInternal;
NetworkClient.OnDisconnectedEvent = OnClientDisconnectInternal;
NetworkClient.OnErrorEvent = OnClientError;
NetworkClient.RegisterHandler<NotReadyMessage>(OnClientNotReadyMessageInternal);
// Don't require authentication because server may send NotReadyMessage from ServerChangeScene
NetworkClient.RegisterHandler<NotReadyMessage>(OnClientNotReadyMessageInternal, false);
NetworkClient.RegisterHandler<SceneMessage>(OnClientSceneInternal, false);
if (playerPrefab != null)
@ -849,6 +848,14 @@ public virtual void ServerChangeScene(string newSceneName)
return;
}
// Throw error if called from client
// Allow changing scene while stopping the server
if (!NetworkServer.active && newSceneName != offlineScene)
{
Debug.LogError("ServerChangeScene can only be called on an active server.");
return;
}
// Debug.Log($"ServerChangeScene {newSceneName}");
NetworkServer.SetAllClientsNotReady();
networkSceneName = newSceneName;
@ -1434,7 +1441,7 @@ public virtual void OnConnectionQualityChanged(ConnectionQuality previous, Conne
{
// logging the change is very useful to track down user's lag reports.
// we want to include as much detail as possible for debugging.
Debug.Log($"[Mirror] Connection Quality changed from {previous} to {current}:\n rtt={(NetworkTime.rtt * 1000):F1}ms\n rttVar={(NetworkTime.rttVariance * 1000):F1}ms\n bufferTime={(NetworkClient.bufferTime * 1000):F1}ms");
//Debug.Log($"[Mirror] Connection Quality changed from {previous} to {current}:\n rtt={(NetworkTime.rtt * 1000):F1}ms\n rttVar={(NetworkTime.rttVariance * 1000):F1}ms\n bufferTime={(NetworkClient.bufferTime * 1000):F1}ms");
}
/// <summary>Called on client when transport raises an exception.</summary>

View File

@ -161,7 +161,7 @@ static void Initialize()
// reset Interest Management so that rebuild intervals
// start at 0 when starting again.
if (aoi != null) aoi.Reset();
if (aoi != null) aoi.ResetState();
// reset NetworkTime
NetworkTime.ResetStatics();
@ -244,7 +244,7 @@ public static void Shutdown()
OnDisconnectedEvent = null;
OnErrorEvent = null;
if (aoi != null) aoi.Reset();
if (aoi != null) aoi.ResetState();
}
static void RemoveTransportHandlers()
@ -266,21 +266,8 @@ static void CleanupSpawned()
{
if (identity != null)
{
// scene object
if (identity.sceneId != 0)
{
// spawned scene objects are unspawned and reset.
// afterwards we disable them again.
// (they always stay in the scene, we don't destroy them)
DestroyObject(identity, DestroyMode.Reset);
identity.gameObject.SetActive(false);
}
// spawned prefabs
else
{
// spawned prefabs are unspawned and destroyed.
DestroyObject(identity, DestroyMode.Destroy);
}
// NetworkServer.Destroy resets if scene object, destroys if prefab.
Destroy(identity.gameObject);
}
}
@ -315,7 +302,18 @@ static void OnCommandMessage(NetworkConnectionToClient conn, CommandMessage msg,
// Ignore commands that may have been in flight before client received NotReadyMessage message.
// Unreliable messages may be out of order, so don't spam warnings for those.
if (channelId == Channels.Reliable)
{
// Attempt to identify the target object, component, and method to narrow down the cause of the error.
if (spawned.TryGetValue(msg.netId, out NetworkIdentity netIdentity))
if (msg.componentIndex < netIdentity.NetworkBehaviours.Length && netIdentity.NetworkBehaviours[msg.componentIndex] is NetworkBehaviour component)
if (RemoteProcedureCalls.GetFunctionMethodName(msg.functionHash, out string methodName))
{
Debug.LogWarning($"Command {methodName} received for {netIdentity.name} [netId={msg.netId}] component {component.name} [index={msg.componentIndex}] when client not ready.\nThis may be ignored if client intentionally set NotReady.");
return;
}
Debug.LogWarning("Command received while client is not ready.\nThis may be ignored if client intentionally set NotReady.");
}
return;
}
@ -326,7 +324,7 @@ static void OnCommandMessage(NetworkConnectionToClient conn, CommandMessage msg,
// for example, NetworkTransform.
// let's not spam the console for unreliable out of order messages.
if (channelId == Channels.Reliable)
Debug.LogWarning($"Spawned object not found when handling Command message [netId={msg.netId}]");
Debug.LogWarning($"Spawned object not found when handling Command message netId={msg.netId}");
return;
}
@ -336,7 +334,15 @@ static void OnCommandMessage(NetworkConnectionToClient conn, CommandMessage msg,
bool requiresAuthority = RemoteProcedureCalls.CommandRequiresAuthority(msg.functionHash);
if (requiresAuthority && identity.connectionToClient != conn)
{
Debug.LogWarning($"Command for object without authority [netId={msg.netId}]");
// Attempt to identify the component and method to narrow down the cause of the error.
if (msg.componentIndex < identity.NetworkBehaviours.Length && identity.NetworkBehaviours[msg.componentIndex] is NetworkBehaviour component)
if (RemoteProcedureCalls.GetFunctionMethodName(msg.functionHash, out string methodName))
{
Debug.LogWarning($"Command {methodName} received for {identity.name} [netId={msg.netId}] component {component.name} [index={msg.componentIndex}] without authority");
return;
}
Debug.LogWarning($"Command received for {identity.name} [netId={msg.netId}] without authority");
return;
}
@ -365,8 +371,13 @@ static void OnEntityStateMessage(NetworkConnectionToClient connection, EntitySta
// failure to deserialize disconnects to prevent exploits.
if (!identity.DeserializeServer(reader))
{
Debug.LogWarning($"Server failed to deserialize client state for {identity.name} with netId={identity.netId}, Disconnecting.");
connection.Disconnect();
if (exceptionsDisconnect)
{
Debug.LogError($"Server failed to deserialize client state for {identity.name} with netId={identity.netId}, Disconnecting.");
connection.Disconnect();
}
else
Debug.LogWarning($"Server failed to deserialize client state for {identity.name} with netId={identity.netId}.");
}
}
}
@ -375,7 +386,7 @@ static void OnEntityStateMessage(NetworkConnectionToClient connection, EntitySta
// RemoveClientAuthority is called, so not malicious.
// Don't disconnect, just log the warning.
else
Debug.LogWarning($"EntityStateMessage from {connection} for {identity} without authority.");
Debug.LogWarning($"EntityStateMessage from {connection} for {identity.name} without authority.");
}
// no warning. don't spam server logs.
// 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.");
@ -480,6 +491,18 @@ public static void SendToAll<T>(T message, int channelId = Channels.Reliable, bo
NetworkMessages.Pack(message, writer);
ArraySegment<byte> segment = writer.ToArraySegment();
// 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($"NetworkServer.SendToAll: 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;
}
// filter and then send to all internet connections at once
// -> makes code more complicated, but is HIGHLY worth it to
// avoid allocations, allow for multicast, etc.
@ -526,6 +549,18 @@ static void SendToObservers<T>(NetworkIdentity identity, T message, int channelI
NetworkMessages.Pack(message, writer);
ArraySegment<byte> segment = writer.ToArraySegment();
// 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($"NetworkServer.SendToObservers: 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;
}
foreach (NetworkConnectionToClient conn in identity.observers.Values)
{
conn.Send(segment, channelId);
@ -550,6 +585,18 @@ public static void SendToReadyObservers<T>(NetworkIdentity identity, T message,
NetworkMessages.Pack(message, writer);
ArraySegment<byte> segment = writer.ToArraySegment();
// 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($"NetworkServer.SendToReadyObservers: 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;
}
int count = 0;
foreach (NetworkConnectionToClient conn in identity.observers.Values)
{
@ -670,8 +717,14 @@ internal static void OnTransportData(int connectionId, ArraySegment<byte> data,
// always process all messages in the batch.
if (!connection.unbatcher.AddBatch(data))
{
Debug.LogWarning($"NetworkServer: received Message was too short (messages should start with message id)");
connection.Disconnect();
if (exceptionsDisconnect)
{
Debug.LogError($"NetworkServer: received message from connectionId:{connectionId} was too short (messages should start with message id). Disconnecting.");
connection.Disconnect();
}
else
Debug.LogWarning($"NetworkServer: received message from connectionId:{connectionId} was too short (messages should start with message id).");
return;
}
@ -707,17 +760,28 @@ internal static void OnTransportData(int connectionId, ArraySegment<byte> data,
// so we need to disconnect.
// -> return to avoid the below unbatches.count error.
// we already disconnected and handled it.
Debug.LogWarning($"NetworkServer: failed to unpack and invoke message. Disconnecting {connectionId}.");
connection.Disconnect();
if (exceptionsDisconnect)
{
Debug.LogError($"NetworkServer: failed to unpack and invoke message. Disconnecting {connectionId}.");
connection.Disconnect();
}
else
Debug.LogWarning($"NetworkServer: failed to unpack and invoke message from connectionId:{connectionId}.");
return;
}
}
// otherwise disconnect
else
{
// WARNING, not error. can happen if attacker sends random data.
Debug.LogWarning($"NetworkServer: received Message was too short (messages should start with message id). Disconnecting {connectionId}");
connection.Disconnect();
if (exceptionsDisconnect)
{
Debug.LogError($"NetworkServer: received message from connectionId:{connectionId} was too short (messages should start with message id). Disconnecting.");
connection.Disconnect();
}
else
Debug.LogWarning($"NetworkServer: received message from connectionId:{connectionId} was too short (messages should start with message id).");
return;
}
}
@ -851,6 +915,22 @@ public static void ReplaceHandler<T>(Action<NetworkConnectionToClient, T> handle
where T : struct, NetworkMessage
{
ushort msgType = NetworkMessageId<T>.Id;
// register Id <> Type in lookup for debugging.
NetworkMessages.Lookup[msgType] = typeof(T);
handlers[msgType] = NetworkMessages.WrapHandler(handler, requireAuthentication, exceptionsDisconnect);
}
/// <summary>Replace a handler for message type T. Most should require authentication.</summary>
public static void ReplaceHandler<T>(Action<NetworkConnectionToClient, T, int> handler, bool requireAuthentication = true)
where T : struct, NetworkMessage
{
ushort msgType = NetworkMessageId<T>.Id;
// register Id <> Type in lookup for debugging.
NetworkMessages.Lookup[msgType] = typeof(T);
handlers[msgType] = NetworkMessages.WrapHandler(handler, requireAuthentication, exceptionsDisconnect);
}
@ -946,7 +1026,7 @@ public static bool AddPlayerForConnection(NetworkConnectionToClient conn, GameOb
{
if (!player.TryGetComponent(out NetworkIdentity identity))
{
Debug.LogWarning($"AddPlayer: playerGameObject has no NetworkIdentity. Please add a NetworkIdentity to {player}");
Debug.LogWarning($"AddPlayer: player GameObject has no NetworkIdentity. Please add a NetworkIdentity to {player}");
return false;
}
@ -1054,7 +1134,7 @@ public static bool ReplacePlayerForConnection(NetworkConnectionToClient conn, Ga
/// <summary>Removes the player object from the connection</summary>
// destroyServerObject: Indicates whether the server object should be destroyed
public static void RemovePlayerForConnection(NetworkConnection conn, bool destroyServerObject)
public static void RemovePlayerForConnection(NetworkConnectionToClient conn, bool destroyServerObject)
{
if (conn.identity != null)
{
@ -1121,17 +1201,17 @@ static void SpawnObserversForConnection(NetworkConnectionToClient conn)
// first!
// ForceShown: add no matter what
if (identity.visible == Visibility.ForceShown)
if (identity.visibility == Visibility.ForceShown)
{
identity.AddObserver(conn);
}
// ForceHidden: don't show no matter what
else if (identity.visible == Visibility.ForceHidden)
else if (identity.visibility == Visibility.ForceHidden)
{
// do nothing
}
// default: legacy system / new system / no system support
else if (identity.visible == Visibility.Default)
else if (identity.visibility == Visibility.Default)
{
// aoi system
if (aoi != null)
@ -1415,7 +1495,7 @@ static void SpawnObject(GameObject obj, NetworkConnection ownerConnection)
// https://github.com/MirrorNetworking/Mirror/pull/3205
if (spawned.ContainsKey(identity.netId))
{
Debug.LogWarning($"{identity} with netId={identity.netId} was already spawned.", identity.gameObject);
Debug.LogWarning($"{identity.name} [netId={identity.netId}] was already spawned.", identity.gameObject);
return;
}
@ -1426,6 +1506,10 @@ static void SpawnObject(GameObject obj, NetworkConnection ownerConnection)
if (ownerConnection is LocalConnectionToClient)
identity.isOwned = true;
// NetworkServer.Unspawn sets object as inactive.
// NetworkServer.Spawn needs to set them active again in case they were previously unspawned / inactive.
identity.gameObject.SetActive(true);
// only call OnStartServer if not spawned yet.
// check used to be in NetworkIdentity. may not be necessary anymore.
if (!identity.isServer && identity.netId == 0)
@ -1471,43 +1555,26 @@ static void SpawnObject(GameObject obj, NetworkConnection ownerConnection)
// Unlike when calling NetworkServer.Destroy(), on the server the object
// will NOT be destroyed. This allows the server to re-use the object,
// even spawn it again later.
public static void UnSpawn(GameObject obj) => DestroyObject(obj, DestroyMode.Reset);
// destroy /////////////////////////////////////////////////////////////
// sometimes we want to GameObject.Destroy it.
// sometimes we want to just unspawn on clients and .Reset() it on server.
// => 'bool destroy' isn't obvious enough. it's really destroy OR reset!
enum DestroyMode { Destroy, Reset }
/// <summary>Destroys this object and corresponding objects on all clients.</summary>
// In some cases it is useful to remove an object but not delete it on
// the server. For that, use NetworkServer.UnSpawn() instead of
// NetworkServer.Destroy().
public static void Destroy(GameObject obj) => DestroyObject(obj, DestroyMode.Destroy);
static void DestroyObject(GameObject obj, DestroyMode mode)
{
if (obj == null)
{
Debug.Log("NetworkServer DestroyObject is null");
return;
}
if (GetNetworkIdentity(obj, out NetworkIdentity identity))
{
DestroyObject(identity, mode);
}
}
static void DestroyObject(NetworkIdentity identity, DestroyMode mode)
public static void UnSpawn(GameObject obj)
{
// Debug.Log($"DestroyObject instance:{identity.netId}");
// NetworkServer.Destroy should only be called on server or host.
// NetworkServer.Unspawn should only be called on server or host.
// on client, show a warning to explain what it does.
if (!active)
{
Debug.LogWarning("NetworkServer.Destroy() called without an active server. Servers can only destroy while active, clients can only ask the server to destroy (for example, with a [Command]), after which the server may decide to destroy the object and broadcast the change to all clients.");
Debug.LogWarning("NetworkServer.Unspawn() called without an active server. Servers can only destroy while active, clients can only ask the server to destroy (for example, with a [Command]), after which the server may decide to destroy the object and broadcast the change to all clients.");
return;
}
if (obj == null)
{
Debug.Log("NetworkServer.Unspawn(): object is null");
return;
}
if (!GetNetworkIdentity(obj, out NetworkIdentity identity))
{
return;
}
@ -1561,31 +1628,59 @@ static void DestroyObject(NetworkIdentity identity, DestroyMode mode)
// we are on the server. call OnStopServer.
identity.OnStopServer();
// are we supposed to GameObject.Destroy() it completely?
if (mode == DestroyMode.Destroy)
// finally reset the state and deactivate it
identity.ResetState();
identity.gameObject.SetActive(false);
}
// destroy /////////////////////////////////////////////////////////////
/// <summary>Destroys this object and corresponding objects on all clients.</summary>
// In some cases it is useful to remove an object but not delete it on
// the server. For that, use NetworkServer.UnSpawn() instead of
// NetworkServer.Destroy().
public static void Destroy(GameObject obj)
{
// NetworkServer.Destroy should only be called on server or host.
// on client, show a warning to explain what it does.
if (!active)
{
Debug.LogWarning("NetworkServer.Destroy() called without an active server. Servers can only destroy while active, clients can only ask the server to destroy (for example, with a [Command]), after which the server may decide to destroy the object and broadcast the change to all clients.");
return;
}
if (obj == null)
{
Debug.Log("NetworkServer.Destroy(): object is null");
return;
}
// first, we unspawn it on clients and server
UnSpawn(obj);
// additionally, if it's a prefab then we destroy it completely.
// we never destroy scene objects on server or on client, since once
// they are gone, they are gone forever and can't be instantiate again.
// for example, server may Destroy() a scene object and once a match
// restarts, the scene objects would be gone from the new match.
if (GetNetworkIdentity(obj, out NetworkIdentity identity) &&
identity.sceneId == 0)
{
identity.destroyCalled = true;
// Destroy if application is running
if (Application.isPlaying)
{
UnityEngine.Object.Destroy(identity.gameObject);
UnityEngine.Object.Destroy(obj);
}
// Destroy can't be used in Editor during tests. use DestroyImmediate.
else
{
GameObject.DestroyImmediate(identity.gameObject);
GameObject.DestroyImmediate(obj);
}
}
// otherwise simply .Reset() and set inactive again
else if (mode == DestroyMode.Reset)
{
identity.Reset();
}
}
// interest management /////////////////////////////////////////////////
// Helper function to add all server connections as observers.
// This is used if none of the components provides their own
// OnRebuildObservers function.
@ -1597,7 +1692,7 @@ static void RebuildObserversDefault(NetworkIdentity identity, bool initialize)
if (initialize)
{
// not force hidden?
if (identity.visible != Visibility.ForceHidden)
if (identity.visibility != Visibility.ForceHidden)
{
AddAllReadyServerConnectionsToObservers(identity);
}
@ -1640,7 +1735,7 @@ public static void RebuildObservers(NetworkIdentity identity, bool initialize)
{
// if there is no interest management system,
// or if 'force shown' then add all connections
if (aoi == null || identity.visible == Visibility.ForceShown)
if (aoi == null || identity.visibility == Visibility.ForceShown)
{
RebuildObserversDefault(identity, initialize);
}

View File

@ -141,17 +141,21 @@ internal static void UpdateClient()
{
// localTime (double) instead of Time.time for accuracy over days
if (localTime >= lastPingTime + PingInterval)
{
// send raw predicted time without the offset applied yet.
// we then apply the offset to it after.
NetworkPingMessage pingMessage = new NetworkPingMessage
(
localTime,
predictedTime
);
NetworkClient.Send(pingMessage, Channels.Unreliable);
lastPingTime = localTime;
}
SendPing();
}
// Separate method so we can call it from NetworkClient directly.
internal static void SendPing()
{
// send raw predicted time without the offset applied yet.
// we then apply the offset to it after.
NetworkPingMessage pingMessage = new NetworkPingMessage
(
localTime,
predictedTime
);
NetworkClient.Send(pingMessage, Channels.Unreliable);
lastPingTime = localTime;
}
// client rtt calculation //////////////////////////////////////////////

View File

@ -10,29 +10,37 @@ public interface PredictedState
{
double timestamp { get; }
// predicted states should have absolute and delta values, for example:
// Vector3 position;
// Vector3 positionDelta; // from last to here
// when inserting a correction between this one and the one before,
// we need to adjust the delta:
// positionDelta *= multiplier;
void AdjustDeltas(float multiplier);
// use Vector3 for both Rigidbody3D and Rigidbody2D, that's fine
Vector3 position { get; set; }
Vector3 positionDelta { get; set; }
Quaternion rotation { get; set; }
Quaternion rotationDelta { get; set; }
Vector3 velocity { get; set; }
Vector3 velocityDelta { get; set; }
Vector3 angularVelocity { get; set; }
Vector3 angularVelocityDelta { get; set; }
}
public static class Prediction
{
// get the two states closest to a given timestamp.
// those can be used to interpolate the exact state at that time.
// => RingBuffer: see prediction_ringbuffer_2 branch, but it's slower!
public static bool Sample<T>(
SortedList<double, T> history,
double timestamp, // current server time
out T before,
out T after,
out int afterIndex,
out double t) // interpolation factor
{
before = default;
after = default;
t = 0;
afterIndex = -1;
// can't sample an empty history
// interpolation needs at least two entries.
@ -50,51 +58,74 @@ public static bool Sample<T>(
// TODO this needs to be faster than O(N)
// search around that area.
// should be O(1) most of the time, unless sampling was off.
int index = 0; // manually count when iterating. easier than for-int loop.
KeyValuePair<double, T> prev = new KeyValuePair<double, T>();
foreach (KeyValuePair<double, T> entry in history) {
// SortedList foreach iteration allocates a LOT. use for-int instead.
// foreach (KeyValuePair<double, T> entry in history) {
for (int i = 0; i < history.Count; ++i)
{
double key = history.Keys[i];
T value = history.Values[i];
// exact match?
if (timestamp == entry.Key) {
before = entry.Value;
after = entry.Value;
t = Mathd.InverseLerp(entry.Key, entry.Key, timestamp);
if (timestamp == key)
{
before = value;
after = value;
afterIndex = index;
t = Mathd.InverseLerp(key, key, timestamp);
return true;
}
// did we check beyond timestamp? then return the previous two.
if (entry.Key > timestamp) {
if (key > timestamp)
{
before = prev.Value;
after = entry.Value;
t = Mathd.InverseLerp(prev.Key, entry.Key, timestamp);
after = value;
afterIndex = index;
t = Mathd.InverseLerp(prev.Key, key, timestamp);
return true;
}
// remember the last
prev = entry;
prev = new KeyValuePair<double, T>(key, value);
index += 1;
}
return false;
}
// when receiving a correction from the server, we want to insert it
// into the client's state history.
// -> if there's already a state at timestamp, replace
// -> otherwise insert and adjust the next state's delta
// TODO test coverage
public static void InsertCorrection<T>(
SortedList<double, T> stateHistory,
// inserts a server state into the client's history.
// readjust the deltas of the states after the inserted one.
// returns the corrected final position.
// => RingBuffer: see prediction_ringbuffer_2 branch, but it's slower!
public static T CorrectHistory<T>(
SortedList<double, T> history,
int stateHistoryLimit,
T corrected, // corrected state with timestamp
T before, // state in history before the correction
T after) // state in history after the correction
T corrected, // corrected state with timestamp
T before, // state in history before the correction
T after, // state in history after the correction
int afterIndex) // index of the 'after' value so we don't need to find it again here
where T: PredictedState
{
// respect the limit
// TODO unit test to check if it respects max size
if (stateHistory.Count >= stateHistoryLimit)
stateHistory.RemoveAt(0);
if (history.Count >= stateHistoryLimit)
{
history.RemoveAt(0);
afterIndex -= 1; // we removed the first value so all indices are off by one now
}
// insert the corrected state into the history, or overwrite if already exists
stateHistory[corrected.timestamp] = corrected;
// PERFORMANCE OPTIMIZATION: avoid O(N) insertion, only readjust all values after.
// the end result is the same since after.delta and after.position are both recalculated.
// it's technically not correct if we were to reconstruct final position from 0..after..end but
// we never do, we only ever iterate from after..end!
//
// insert the corrected state into the history, or overwrite if already exists
// SortedList insertions are O(N)!
// history[corrected.timestamp] = corrected;
// afterIndex += 1; // we inserted the corrected value before the previous index
// the entry behind the inserted one still has the delta from (before, after).
// we need to correct it to (corrected, after).
@ -121,13 +152,44 @@ public static void InsertCorrection<T>(
//
double previousDeltaTime = after.timestamp - before.timestamp; // 3.0 - 1.0 = 2.0
double correctedDeltaTime = after.timestamp - corrected.timestamp; // 3.0 - 2.5 = 0.5
double multiplier = correctedDeltaTime / previousDeltaTime; // 0.5 / 2.0 = 0.25
// fix multiplier becoming NaN if previousDeltaTime is 0:
// double multiplier = correctedDeltaTime / previousDeltaTime;
double multiplier = previousDeltaTime != 0 ? correctedDeltaTime / previousDeltaTime : 0; // 0.5 / 2.0 = 0.25
// recalculate 'after.delta' with the multiplier
after.AdjustDeltas((float)multiplier);
after.positionDelta = Vector3.Lerp(Vector3.zero, after.positionDelta, (float)multiplier);
after.velocityDelta = Vector3.Lerp(Vector3.zero, after.velocityDelta, (float)multiplier);
after.angularVelocityDelta = Vector3.Lerp(Vector3.zero, after.angularVelocityDelta, (float)multiplier);
// Quaternions always need to be normalized in order to be a valid rotation after operations
after.rotationDelta = Quaternion.Slerp(Quaternion.identity, after.rotationDelta, (float)multiplier).normalized;
// write the adjusted 'after' value into the history buffer
stateHistory[after.timestamp] = after;
// changes aren't saved until we overwrite them in the history
history[after.timestamp] = after;
// second step: readjust all absolute values by rewinding client's delta moves on top of it.
T last = corrected;
for (int i = afterIndex; i < history.Count; ++i)
{
double key = history.Keys[i];
T value = history.Values[i];
// correct absolute position based on last + delta.
value.position = last.position + value.positionDelta;
value.velocity = last.velocity + value.velocityDelta;
value.angularVelocity = last.angularVelocity + value.angularVelocityDelta;
// Quaternions always need to be normalized in order to be a valid rotation after operations
value.rotation = (value.rotationDelta * last.rotation).normalized; // quaternions add delta by multiplying in this order
// save the corrected entry into history.
history[key] = value;
// save last
last = value;
}
// third step: return the final recomputed state.
return last;
}
}
}

View File

@ -30,6 +30,8 @@ public bool AreEqual(Type componentType, RemoteCallType remoteCallType, RemoteCa
/// <summary>Used to help manage remote calls for NetworkBehaviours</summary>
public static class RemoteProcedureCalls
{
public const string InvokeRpcPrefix = "InvokeUserCode_";
// one lookup for all remote calls.
// allows us to easily add more remote call types without duplicating code.
// note: do not clear those with [RuntimeInitializeOnLoad]
@ -99,6 +101,17 @@ public static void RegisterRpc(Type componentType, string functionFullName, Remo
internal static void RemoveDelegate(ushort hash) =>
remoteCallDelegates.Remove(hash);
internal static bool GetFunctionMethodName(ushort functionHash, out string methodName)
{
if (remoteCallDelegates.TryGetValue(functionHash, out Invoker invoker))
{
methodName = invoker.function.GetMethodName().Replace(InvokeRpcPrefix, "");
return true;
}
methodName = "";
return false;
}
// note: no need to throw an error if not found.
// an attacker might just try to call a cmd with an rpc's hash etc.
// returning false is enough.

View File

@ -1,3 +1,4 @@
using System;
using System.Collections;
using System.Collections.Generic;
@ -5,13 +6,31 @@ namespace Mirror
{
public class SyncIDictionary<TKey, TValue> : SyncObject, IDictionary<TKey, TValue>, IReadOnlyDictionary<TKey, TValue>
{
public delegate void SyncDictionaryChanged(Operation op, TKey key, TValue item);
/// <summary>This is called after the item is added with TKey</summary>
public Action<TKey> OnAdd;
/// <summary>This is called after the item is changed with TKey. TValue is the OLD item</summary>
public Action<TKey, TValue> OnSet;
/// <summary>This is called after the item is removed with TKey. TValue is the OLD item</summary>
public Action<TKey, TValue> OnRemove;
/// <summary>This is called before the data is cleared</summary>
public Action OnClear;
// Deprecated 2024-03-22
[Obsolete("Use individual Actions, which pass OLD values where appropriate, instead.")]
public Action<Operation, TKey, TValue> Callback;
protected readonly IDictionary<TKey, TValue> objects;
public SyncIDictionary(IDictionary<TKey, TValue> objects)
{
this.objects = objects;
}
public int Count => objects.Count;
public bool IsReadOnly => !IsWritable();
public event SyncDictionaryChanged Callback;
public enum Operation : byte
{
@ -30,7 +49,7 @@ struct Change
// list of changes.
// -> insert/delete/clear is only ONE change
// -> changing the same slot 10x caues 10 changes.
// -> changing the same slot 10x causes 10 changes.
// -> note that this grows until next sync(!)
// TODO Dictionary<key, change> to avoid ever growing changes / redundant changes!
readonly List<Change> changes = new List<Change>();
@ -41,13 +60,6 @@ struct Change
// so we need to skip them
int changesAhead;
public override void Reset()
{
changes.Clear();
changesAhead = 0;
objects.Clear();
}
public ICollection<TKey> Keys => objects.Keys;
public ICollection<TValue> Values => objects.Values;
@ -56,38 +68,6 @@ public override void Reset()
IEnumerable<TValue> IReadOnlyDictionary<TKey, TValue>.Values => objects.Values;
// throw away all the changes
// this should be called after a successful sync
public override void ClearChanges() => changes.Clear();
public SyncIDictionary(IDictionary<TKey, TValue> objects)
{
this.objects = objects;
}
void AddOperation(Operation op, TKey key, TValue item, bool checkAccess)
{
if (checkAccess && IsReadOnly)
{
throw new System.InvalidOperationException("SyncDictionaries can only be modified by the owner.");
}
Change change = new Change
{
operation = op,
key = key,
item = item
};
if (IsRecording())
{
changes.Add(change);
OnDirty?.Invoke();
}
Callback?.Invoke(op, key, item);
}
public override void OnSerializeAll(NetworkWriter writer)
{
// if init, write the full list content
@ -179,15 +159,15 @@ public override void OnDeserializeDelta(NetworkReader reader)
// ClientToServer needs to set dirty in server OnDeserialize.
// no access check: server OnDeserialize can always
// write, even for ClientToServer (for broadcasting).
if (ContainsKey(key))
if (objects.TryGetValue(key, out TValue oldItem))
{
objects[key] = item; // assign after ContainsKey check
AddOperation(Operation.OP_SET, key, item, false);
objects[key] = item; // assign after TryGetValue
AddOperation(Operation.OP_SET, key, item, oldItem, false);
}
else
{
objects[key] = item; // assign after ContainsKey check
AddOperation(Operation.OP_ADD, key, item, false);
objects[key] = item; // assign after TryGetValue
AddOperation(Operation.OP_ADD, key, item, default, false);
}
}
break;
@ -195,12 +175,14 @@ public override void OnDeserializeDelta(NetworkReader reader)
case Operation.OP_CLEAR:
if (apply)
{
objects.Clear();
// add dirty + changes.
// ClientToServer needs to set dirty in server OnDeserialize.
// no access check: server OnDeserialize can always
// write, even for ClientToServer (for broadcasting).
AddOperation(Operation.OP_CLEAR, default, default, false);
AddOperation(Operation.OP_CLEAR, default, default, default, false);
// clear after invoking the callback so users can iterate the dictionary
// and take appropriate action on the items before they are wiped.
objects.Clear();
}
break;
@ -208,14 +190,14 @@ public override void OnDeserializeDelta(NetworkReader reader)
key = reader.Read<TKey>();
if (apply)
{
if (objects.TryGetValue(key, out item))
if (objects.TryGetValue(key, out TValue oldItem))
{
// add dirty + changes.
// ClientToServer needs to set dirty in server OnDeserialize.
// no access check: server OnDeserialize can always
// write, even for ClientToServer (for broadcasting).
objects.Remove(key);
AddOperation(Operation.OP_REMOVE, key, item, false);
AddOperation(Operation.OP_REMOVE, key, oldItem, oldItem, false);
}
}
break;
@ -229,22 +211,15 @@ public override void OnDeserializeDelta(NetworkReader reader)
}
}
public void Clear()
// throw away all the changes
// this should be called after a successful sync
public override void ClearChanges() => changes.Clear();
public override void Reset()
{
changes.Clear();
changesAhead = 0;
objects.Clear();
AddOperation(Operation.OP_CLEAR, default, default, true);
}
public bool ContainsKey(TKey key) => objects.ContainsKey(key);
public bool Remove(TKey key)
{
if (objects.TryGetValue(key, out TValue item) && objects.Remove(key))
{
AddOperation(Operation.OP_REMOVE, key, item, true);
return true;
}
return false;
}
public TValue this[TKey i]
@ -254,42 +229,31 @@ public TValue this[TKey i]
{
if (ContainsKey(i))
{
TValue oldItem = objects[i];
objects[i] = value;
AddOperation(Operation.OP_SET, i, value, true);
AddOperation(Operation.OP_SET, i, value, oldItem, true);
}
else
{
objects[i] = value;
AddOperation(Operation.OP_ADD, i, value, true);
AddOperation(Operation.OP_ADD, i, value, default, true);
}
}
}
public bool TryGetValue(TKey key, out TValue value) => objects.TryGetValue(key, out value);
public void Add(TKey key, TValue value)
{
objects.Add(key, value);
AddOperation(Operation.OP_ADD, key, value, true);
}
public bool ContainsKey(TKey key) => objects.ContainsKey(key);
public void Add(KeyValuePair<TKey, TValue> item) => Add(item.Key, item.Value);
public bool Contains(KeyValuePair<TKey, TValue> item)
{
return TryGetValue(item.Key, out TValue val) && EqualityComparer<TValue>.Default.Equals(val, item.Value);
}
public bool Contains(KeyValuePair<TKey, TValue> item) => TryGetValue(item.Key, out TValue val) && EqualityComparer<TValue>.Default.Equals(val, item.Value);
public void CopyTo(KeyValuePair<TKey, TValue>[] array, int arrayIndex)
{
if (arrayIndex < 0 || arrayIndex > array.Length)
{
throw new System.ArgumentOutOfRangeException(nameof(arrayIndex), "Array Index Out of Range");
}
if (array.Length - arrayIndex < Count)
{
throw new System.ArgumentException("The number of items in the SyncDictionary is greater than the available space from arrayIndex to the end of the destination array");
}
int i = arrayIndex;
foreach (KeyValuePair<TKey, TValue> item in objects)
@ -299,16 +263,80 @@ public void CopyTo(KeyValuePair<TKey, TValue>[] array, int arrayIndex)
}
}
public void Add(KeyValuePair<TKey, TValue> item) => Add(item.Key, item.Value);
public void Add(TKey key, TValue value)
{
objects.Add(key, value);
AddOperation(Operation.OP_ADD, key, value, default, true);
}
public bool Remove(TKey key)
{
if (objects.TryGetValue(key, out TValue oldItem) && objects.Remove(key))
{
AddOperation(Operation.OP_REMOVE, key, oldItem, oldItem, true);
return true;
}
return false;
}
public bool Remove(KeyValuePair<TKey, TValue> item)
{
bool result = objects.Remove(item.Key);
if (result)
{
AddOperation(Operation.OP_REMOVE, item.Key, item.Value, true);
}
AddOperation(Operation.OP_REMOVE, item.Key, item.Value, item.Value, true);
return result;
}
public void Clear()
{
AddOperation(Operation.OP_CLEAR, default, default, default, true);
// clear after invoking the callback so users can iterate the dictionary
// and take appropriate action on the items before they are wiped.
objects.Clear();
}
void AddOperation(Operation op, TKey key, TValue item, TValue oldItem, bool checkAccess)
{
if (checkAccess && IsReadOnly)
throw new InvalidOperationException("SyncDictionaries can only be modified by the owner.");
Change change = new Change
{
operation = op,
key = key,
item = item
};
if (IsRecording())
{
changes.Add(change);
OnDirty?.Invoke();
}
switch (op)
{
case Operation.OP_ADD:
OnAdd?.Invoke(key);
break;
case Operation.OP_SET:
OnSet?.Invoke(key, oldItem);
break;
case Operation.OP_REMOVE:
OnRemove?.Invoke(key, oldItem);
break;
case Operation.OP_CLEAR:
OnClear?.Invoke();
break;
}
#pragma warning disable CS0618 // Type or member is obsolete
Callback?.Invoke(op, key, item);
#pragma warning restore CS0618 // Type or member is obsolete
}
public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator() => objects.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => objects.GetEnumerator();
@ -316,9 +344,9 @@ public bool Remove(KeyValuePair<TKey, TValue> item)
public class SyncDictionary<TKey, TValue> : SyncIDictionary<TKey, TValue>
{
public SyncDictionary() : base(new Dictionary<TKey, TValue>()) {}
public SyncDictionary(IEqualityComparer<TKey> eq) : base(new Dictionary<TKey, TValue>(eq)) {}
public SyncDictionary(IDictionary<TKey, TValue> d) : base(new Dictionary<TKey, TValue>(d)) {}
public SyncDictionary() : base(new Dictionary<TKey, TValue>()) { }
public SyncDictionary(IEqualityComparer<TKey> eq) : base(new Dictionary<TKey, TValue>(eq)) { }
public SyncDictionary(IDictionary<TKey, TValue> d) : base(new Dictionary<TKey, TValue>(d)) { }
public new Dictionary<TKey, TValue>.ValueCollection Values => ((Dictionary<TKey, TValue>)objects).Values;
public new Dictionary<TKey, TValue>.KeyCollection Keys => ((Dictionary<TKey, TValue>)objects).Keys;
public new Dictionary<TKey, TValue>.Enumerator GetEnumerator() => ((Dictionary<TKey, TValue>)objects).GetEnumerator();

View File

@ -6,23 +6,39 @@ namespace Mirror
{
public class SyncList<T> : SyncObject, IList<T>, IReadOnlyList<T>
{
public delegate void SyncListChanged(Operation op, int itemIndex, T oldItem, T newItem);
public enum Operation : byte
{
OP_ADD,
OP_SET,
OP_INSERT,
OP_REMOVEAT,
OP_CLEAR
}
/// <summary>This is called after the item is added with index</summary>
public Action<int> OnAdd;
/// <summary>This is called after the item is inserted with inedx</summary>
public Action<int> OnInsert;
/// <summary>This is called after the item is set with index and OLD Value</summary>
public Action<int, T> OnSet;
/// <summary>This is called after the item is removed with index and OLD Value</summary>
public Action<int, T> OnRemove;
/// <summary>This is called before the list is cleared so the list can be iterated</summary>
public Action OnClear;
// Deprecated 2024-03-23
[Obsolete("Use individual Actions, which pass OLD values where appropriate, instead.")]
public Action<Operation, int, T, T> Callback;
readonly IList<T> objects;
readonly IEqualityComparer<T> comparer;
public int Count => objects.Count;
public bool IsReadOnly => !IsWritable();
public event SyncListChanged Callback;
public enum Operation : byte
{
OP_ADD,
OP_CLEAR,
OP_INSERT,
OP_REMOVEAT,
OP_SET
}
struct Change
{
@ -43,7 +59,7 @@ struct Change
// so we need to skip them
int changesAhead;
public SyncList() : this(EqualityComparer<T>.Default) {}
public SyncList() : this(EqualityComparer<T>.Default) { }
public SyncList(IEqualityComparer<T> comparer)
{
@ -71,9 +87,7 @@ public override void Reset()
void AddOperation(Operation op, int itemIndex, T oldItem, T newItem, bool checkAccess)
{
if (checkAccess && IsReadOnly)
{
throw new InvalidOperationException("Synclists can only be modified by the owner.");
}
Change change = new Change
{
@ -88,7 +102,28 @@ void AddOperation(Operation op, int itemIndex, T oldItem, T newItem, bool checkA
OnDirty?.Invoke();
}
switch (op)
{
case Operation.OP_ADD:
OnAdd?.Invoke(itemIndex);
break;
case Operation.OP_INSERT:
OnInsert?.Invoke(itemIndex);
break;
case Operation.OP_SET:
OnSet?.Invoke(itemIndex, oldItem);
break;
case Operation.OP_REMOVEAT:
OnRemove?.Invoke(itemIndex, oldItem);
break;
case Operation.OP_CLEAR:
OnClear?.Invoke();
break;
}
#pragma warning disable CS0618 // Type or member is obsolete
Callback?.Invoke(op, itemIndex, oldItem, newItem);
#pragma warning restore CS0618 // Type or member is obsolete
}
public override void OnSerializeAll(NetworkWriter writer)
@ -195,12 +230,14 @@ public override void OnDeserializeDelta(NetworkReader reader)
case Operation.OP_CLEAR:
if (apply)
{
objects.Clear();
// add dirty + changes.
// ClientToServer needs to set dirty in server OnDeserialize.
// no access check: server OnDeserialize can always
// write, even for ClientToServer (for broadcasting).
AddOperation(Operation.OP_CLEAR, 0, default, default, false);
// clear after invoking the callback so users can iterate the list
// and take appropriate action on the items before they are wiped.
objects.Clear();
}
break;
@ -265,15 +302,15 @@ public void Add(T item)
public void AddRange(IEnumerable<T> range)
{
foreach (T entry in range)
{
Add(entry);
}
}
public void Clear()
{
objects.Clear();
AddOperation(Operation.OP_CLEAR, 0, default, default, true);
// clear after invoking the callback so users can iterate the list
// and take appropriate action on the items before they are wiped.
objects.Clear();
}
public bool Contains(T item) => IndexOf(item) >= 0;
@ -331,9 +368,8 @@ public bool Remove(T item)
int index = IndexOf(item);
bool result = index >= 0;
if (result)
{
RemoveAt(index);
}
return result;
}
@ -352,9 +388,7 @@ public int RemoveAll(Predicate<T> match)
toRemove.Add(objects[i]);
foreach (T entry in toRemove)
{
Remove(entry);
}
return toRemove.Count;
}
@ -393,6 +427,7 @@ public struct Enumerator : IEnumerator<T>
{
readonly SyncList<T> list;
int index;
public T Current { get; private set; }
public Enumerator(SyncList<T> list)
@ -405,16 +440,15 @@ public Enumerator(SyncList<T> list)
public bool MoveNext()
{
if (++index >= list.Count)
{
return false;
}
Current = list[index];
return true;
}
public void Reset() => index = -1;
object IEnumerator.Current => Current;
public void Dispose() {}
public void Dispose() { }
}
}
}

View File

@ -6,19 +6,29 @@ namespace Mirror
{
public class SyncSet<T> : SyncObject, ISet<T>
{
public delegate void SyncSetChanged(Operation op, T item);
/// <summary>This is called after the item is added. T is the new item.</summary>
public Action<T> OnAdd;
/// <summary>This is called after the item is removed. T is the OLD item</summary>
public Action<T> OnRemove;
/// <summary>This is called BEFORE the data is cleared</summary>
public Action OnClear;
// Deprecated 2024-03-22
[Obsolete("Use individual Actions, which pass OLD value where appropriate, instead.")]
public Action<Operation, T> Callback;
protected readonly ISet<T> objects;
public int Count => objects.Count;
public bool IsReadOnly => !IsWritable();
public event SyncSetChanged Callback;
public enum Operation : byte
{
OP_ADD,
OP_CLEAR,
OP_REMOVE
OP_REMOVE,
OP_CLEAR
}
struct Change
@ -59,9 +69,7 @@ public override void Reset()
void AddOperation(Operation op, T item, bool checkAccess)
{
if (checkAccess && IsReadOnly)
{
throw new InvalidOperationException("SyncSets can only be modified by the owner.");
}
Change change = new Change
{
@ -75,7 +83,22 @@ void AddOperation(Operation op, T item, bool checkAccess)
OnDirty?.Invoke();
}
switch (op)
{
case Operation.OP_ADD:
OnAdd?.Invoke(item);
break;
case Operation.OP_REMOVE:
OnRemove?.Invoke(item);
break;
case Operation.OP_CLEAR:
OnClear?.Invoke();
break;
}
#pragma warning disable CS0618 // Type or member is obsolete
Callback?.Invoke(op, item);
#pragma warning restore CS0618 // Type or member is obsolete
}
void AddOperation(Operation op, bool checkAccess) => AddOperation(op, default, checkAccess);
@ -86,9 +109,7 @@ public override void OnSerializeAll(NetworkWriter writer)
writer.WriteUInt((uint)objects.Count);
foreach (T obj in objects)
{
writer.Write(obj);
}
// all changes have been applied already
// thus the client will need to skip all the pending changes
@ -112,13 +133,11 @@ public override void OnSerializeDelta(NetworkWriter writer)
case Operation.OP_ADD:
writer.Write(change.item);
break;
case Operation.OP_CLEAR:
break;
case Operation.OP_REMOVE:
writer.Write(change.item);
break;
case Operation.OP_CLEAR:
break;
}
}
}
@ -171,18 +190,6 @@ public override void OnDeserializeDelta(NetworkReader reader)
}
break;
case Operation.OP_CLEAR:
if (apply)
{
objects.Clear();
// add dirty + changes.
// ClientToServer needs to set dirty in server OnDeserialize.
// no access check: server OnDeserialize can always
// write, even for ClientToServer (for broadcasting).
AddOperation(Operation.OP_CLEAR, false);
}
break;
case Operation.OP_REMOVE:
item = reader.Read<T>();
if (apply)
@ -195,6 +202,20 @@ public override void OnDeserializeDelta(NetworkReader reader)
AddOperation(Operation.OP_REMOVE, item, false);
}
break;
case Operation.OP_CLEAR:
if (apply)
{
// add dirty + changes.
// ClientToServer needs to set dirty in server OnDeserialize.
// no access check: server OnDeserialize can always
// write, even for ClientToServer (for broadcasting).
AddOperation(Operation.OP_CLEAR, false);
// clear after invoking the callback so users can iterate the set
// and take appropriate action on the items before they are wiped.
objects.Clear();
}
break;
}
if (!apply)
@ -218,15 +239,15 @@ public bool Add(T item)
void ICollection<T>.Add(T item)
{
if (objects.Add(item))
{
AddOperation(Operation.OP_ADD, item, true);
}
}
public void Clear()
{
objects.Clear();
AddOperation(Operation.OP_CLEAR, true);
// clear after invoking the callback so users can iterate the set
// and take appropriate action on the items before they are wiped.
objects.Clear();
}
public bool Contains(T item) => objects.Contains(item);
@ -257,17 +278,13 @@ public void ExceptWith(IEnumerable<T> other)
// remove every element in other from this
foreach (T element in other)
{
Remove(element);
}
}
public void IntersectWith(IEnumerable<T> other)
{
if (other is ISet<T> otherSet)
{
IntersectWithSet(otherSet);
}
else
{
HashSet<T> otherAsSet = new HashSet<T>(other);
@ -280,12 +297,8 @@ void IntersectWithSet(ISet<T> otherSet)
List<T> elements = new List<T>(objects);
foreach (T element in elements)
{
if (!otherSet.Contains(element))
{
Remove(element);
}
}
}
public bool IsProperSubsetOf(IEnumerable<T> other) => objects.IsProperSubsetOf(other);
@ -304,38 +317,26 @@ void IntersectWithSet(ISet<T> otherSet)
public void SymmetricExceptWith(IEnumerable<T> other)
{
if (other == this)
{
Clear();
}
else
{
foreach (T element in other)
{
if (!Remove(element))
{
Add(element);
}
}
}
}
// custom implementation so we can do our own Clear/Add/Remove for delta
public void UnionWith(IEnumerable<T> other)
{
if (other != this)
{
foreach (T element in other)
{
Add(element);
}
}
}
}
public class SyncHashSet<T> : SyncSet<T>
{
public SyncHashSet() : this(EqualityComparer<T>.Default) {}
public SyncHashSet(IEqualityComparer<T> comparer) : base(new HashSet<T>(comparer ?? EqualityComparer<T>.Default)) {}
public SyncHashSet() : this(EqualityComparer<T>.Default) { }
public SyncHashSet(IEqualityComparer<T> comparer) : base(new HashSet<T>(comparer ?? EqualityComparer<T>.Default)) { }
// allocation free enumerator
public new HashSet<T>.Enumerator GetEnumerator() => ((HashSet<T>)objects).GetEnumerator();
@ -343,8 +344,8 @@ public SyncHashSet(IEqualityComparer<T> comparer) : base(new HashSet<T>(comparer
public class SyncSortedSet<T> : SyncSet<T>
{
public SyncSortedSet() : this(Comparer<T>.Default) {}
public SyncSortedSet(IComparer<T> comparer) : base(new SortedSet<T>(comparer ?? Comparer<T>.Default)) {}
public SyncSortedSet() : this(Comparer<T>.Default) { }
public SyncSortedSet(IComparer<T> comparer) : base(new SortedSet<T>(comparer ?? Comparer<T>.Default)) { }
// allocation free enumerator
public new SortedSet<T>.Enumerator GetEnumerator() => ((SortedSet<T>)objects).GetEnumerator();

View File

@ -87,20 +87,23 @@ static void OnLateUpdate()
{
switch (entry.type)
{
// add [Thread#] prefix to make it super obvious where this log message comes from.
// some projects may see unexpected messages that were previously hidden,
// since Unity wouldn't log them without ThreadLog.cs.
case LogType.Log:
Debug.Log($"[T{entry.threadId}] {entry.message}\n{entry.stackTrace}");
Debug.Log($"[Thread{entry.threadId}] {entry.message}\n{entry.stackTrace}");
break;
case LogType.Warning:
Debug.LogWarning($"[T{entry.threadId}] {entry.message}\n{entry.stackTrace}");
Debug.LogWarning($"[Thread{entry.threadId}] {entry.message}\n{entry.stackTrace}");
break;
case LogType.Error:
Debug.LogError($"[T{entry.threadId}] {entry.message}\n{entry.stackTrace}");
Debug.LogError($"[Thread{entry.threadId}] {entry.message}\n{entry.stackTrace}");
break;
case LogType.Exception:
Debug.LogError($"[T{entry.threadId}] {entry.message}\n{entry.stackTrace}");
Debug.LogError($"[Thread{entry.threadId}] {entry.message}\n{entry.stackTrace}");
break;
case LogType.Assert:
Debug.LogAssertion($"[T{entry.threadId}] {entry.message}\n{entry.stackTrace}");
Debug.LogAssertion($"[Thread{entry.threadId}] {entry.message}\n{entry.stackTrace}");
break;
}
}

View File

@ -2,6 +2,7 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using UnityEngine;
namespace Mirror
{
@ -98,5 +99,14 @@ public static void Clear<T>(this ConcurrentQueue<T> source)
}
}
#endif
#if !UNITY_2021_3_OR_NEWER
// Unity 2021.2 and earlier don't have transform.GetPositionAndRotation which we use for performance in some places
public static void GetPositionAndRotation(this Transform transform, out Vector3 position, out Quaternion rotation)
{
position = transform.position;
rotation = transform.rotation;
}
#endif
}
}

View File

@ -36,6 +36,12 @@ public abstract class Transport : MonoBehaviour
/// <summary>Is this transport available in the current platform?</summary>
public abstract bool Available();
/// <summary>Is this transported encrypted for secure communication?</summary>
public virtual bool IsEncrypted => false;
/// <summary>If encrypted, which cipher is used?</summary>
public virtual string EncryptionCipher => "";
// client //////////////////////////////////////////////////////////////
/// <summary>Called by Transport when the client connected to the server.</summary>
public Action OnClientConnected;

View File

@ -0,0 +1,14 @@
using UnityEditor;
namespace Mirror
{
[CustomEditor(typeof(LagCompensator))]
public class LagCompensatorInspector : Editor
{
public override void OnInspectorGUI()
{
EditorGUILayout.HelpBox("Preview Component - Feedback appreciated on GitHub or Discord!", MessageType.Warning);
DrawDefaultInspector();
}
}
}

View File

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

View File

@ -3,6 +3,7 @@
"rootNamespace": "",
"references": [
"GUID:30817c1a0e6d646d99c048fc403f5979",
"GUID:72872094b21c16e48b631b2224833d49",
"GUID:1d0b9d21c3ff546a4aa32399dfd33474"
],
"includePlatforms": [

View File

@ -126,7 +126,9 @@ float DrawNetworkIdentityInfo(NetworkIdentity identity, float initialX, float Y)
Vector2 maxValueLabelSize = GetMaxNameLabelSize(infos);
Rect labelRect = new Rect(initialX, Y, maxNameLabelSize.x, maxNameLabelSize.y);
Rect idLabelRect = new Rect(maxNameLabelSize.x, Y, maxValueLabelSize.x, maxValueLabelSize.y);
// height needs a +1 to line up nicely
Rect idLabelRect = new Rect(maxNameLabelSize.x, Y, maxValueLabelSize.x, maxValueLabelSize.y + 1);
foreach (NetworkIdentityInfo info in infos)
{

View File

@ -0,0 +1,19 @@
using UnityEngine;
using UnityEditor;
namespace Mirror
{
[CustomPropertyDrawer(typeof(ReadOnlyAttribute))]
public class ReadOnlyDrawer : PropertyDrawer
{
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
// Cache the current GUI enabled state
bool prevGuiEnabledState = GUI.enabled;
GUI.enabled = false;
EditorGUI.PropertyField(position, property, label, true);
GUI.enabled = prevGuiEnabledState;
}
}
}

View File

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

View File

@ -98,7 +98,13 @@ public AssemblyDefinition Resolve(AssemblyNameReference name, ReaderParameters p
// let's make it obvious why we returned null for easier debugging.
// NOTE: if this fails for "System.Private.CoreLib":
// ILPostProcessorReflectionImporter fixes it!
Log.Warning($"ILPostProcessorAssemblyResolver.Resolve: Failed to find file for {name}");
// the fix for #2503 started showing this warning for Bee.BeeDriver on mac,
// which is for compilation. we can ignore that one.
if (!name.Name.StartsWith("Bee.BeeDriver"))
{
Log.Warning($"ILPostProcessorAssemblyResolver.Resolve: Failed to find file for {name}");
}
return null;
}

View File

@ -84,7 +84,7 @@ protected static void InvokeCmdCmdThrust(NetworkBehaviour obj, NetworkReader rea
*/
public static MethodDefinition ProcessCommandInvoke(WeaverTypes weaverTypes, Readers readers, Logger Log, TypeDefinition td, MethodDefinition method, MethodDefinition cmdCallFunc, ref bool WeavingFailed)
{
string cmdName = Weaver.GenerateMethodName(Weaver.InvokeRpcPrefix, method);
string cmdName = Weaver.GenerateMethodName(RemoteCalls.RemoteProcedureCalls.InvokeRpcPrefix, method);
MethodDefinition cmd = new MethodDefinition(cmdName,
MethodAttributes.Family | MethodAttributes.Static | MethodAttributes.HideBySig,

View File

@ -504,7 +504,7 @@ void GenerateSerialization(ref bool WeavingFailed)
worker.Emit(OpCodes.Ldarg_1);
// base
worker.Emit(OpCodes.Ldarg_0);
worker.Emit(OpCodes.Call, weaverTypes.NetworkBehaviourDirtyBitsReference);
worker.Emit(OpCodes.Ldfld, weaverTypes.NetworkBehaviourDirtyBitsReference);
MethodReference writeUint64Func = writers.GetWriteFunc(weaverTypes.Import<ulong>(), ref WeavingFailed);
worker.Emit(OpCodes.Call, writeUint64Func);
@ -524,7 +524,7 @@ void GenerateSerialization(ref bool WeavingFailed)
// Generates: if ((base.get_syncVarDirtyBits() & 1uL) != 0uL)
// base
worker.Emit(OpCodes.Ldarg_0);
worker.Emit(OpCodes.Call, weaverTypes.NetworkBehaviourDirtyBitsReference);
worker.Emit(OpCodes.Ldfld, weaverTypes.NetworkBehaviourDirtyBitsReference);
// 8 bytes = long
worker.Emit(OpCodes.Ldc_I8, 1L << dirtyBit);
worker.Emit(OpCodes.And);

View File

@ -1,4 +1,5 @@
// finds all readers and writers and register them
using System.Collections.Generic;
using System.Linq;
using Mono.CecilX;
using Mono.CecilX.Cil;
@ -17,6 +18,21 @@ public static bool Process(AssemblyDefinition CurrentAssembly, IAssemblyResolver
// otherwise Unity crashes when running tests
ProcessMirrorAssemblyClasses(CurrentAssembly, resolver, Log, writers, readers, ref WeavingFailed);
// process dependencies first, this way weaver can process types of other assemblies properly.
// fixes: https://github.com/MirrorNetworking/Mirror/issues/2503
//
// find NetworkReader/Writer extensions in referenced assemblies
// save a copy of the collection enumerator since it appears to be modified at some point during iteration
IEnumerable<AssemblyNameReference> assemblyReferences = CurrentAssembly.MainModule.AssemblyReferences.ToList();
foreach (AssemblyNameReference assemblyNameReference in assemblyReferences)
{
AssemblyDefinition referencedAssembly = resolver.Resolve(assemblyNameReference);
if (referencedAssembly != null)
{
ProcessAssemblyClasses(CurrentAssembly, referencedAssembly, writers, readers, ref WeavingFailed);
}
}
// find readers/writers in the assembly we are in right now.
return ProcessAssemblyClasses(CurrentAssembly, CurrentAssembly, writers, readers, ref WeavingFailed);
}

View File

@ -8,7 +8,7 @@ public static class RpcProcessor
{
public static MethodDefinition ProcessRpcInvoke(WeaverTypes weaverTypes, Writers writers, Readers readers, Logger Log, TypeDefinition td, MethodDefinition md, MethodDefinition rpcCallFunc, ref bool WeavingFailed)
{
string rpcName = Weaver.GenerateMethodName(Weaver.InvokeRpcPrefix, md);
string rpcName = Weaver.GenerateMethodName(RemoteCalls.RemoteProcedureCalls.InvokeRpcPrefix, md);
MethodDefinition rpc = new MethodDefinition(rpcName, MethodAttributes.Family | MethodAttributes.Static | MethodAttributes.HideBySig,
weaverTypes.Import(typeof(void)));

View File

@ -25,7 +25,7 @@ static bool ProcessSiteMethod(WeaverTypes weaverTypes, Logger Log, MethodDefinit
{
if (md.Name == ".cctor" ||
md.Name == NetworkBehaviourProcessor.ProcessedFunctionName ||
md.Name.StartsWith(Weaver.InvokeRpcPrefix))
md.Name.StartsWith(RemoteCalls.RemoteProcedureCalls.InvokeRpcPrefix))
return false;
if (md.IsAbstract)

View File

@ -52,7 +52,7 @@ static void ProcessMethod(Logger Log, SyncVarAccessLists syncVarAccessLists, Met
// skip static constructor, "MirrorProcessed", "InvokeUserCode_"
if (md.Name == ".cctor" ||
md.Name == NetworkBehaviourProcessor.ProcessedFunctionName ||
md.Name.StartsWith(Weaver.InvokeRpcPrefix))
md.Name.StartsWith(RemoteCalls.RemoteProcedureCalls.InvokeRpcPrefix))
return;
// skip abstract
@ -99,7 +99,7 @@ static int ProcessInstruction(Logger Log, SyncVarAccessLists syncVarAccessLists,
// we can not control the order.
// instead, Log an error to suggest adding a SetSyncVar(value) function.
// this is a very easy solution for a very rare edge case.
Log.Error($"'[SyncVar] {opFieldstRef.Name}' in '{md.Module.Name}' is modified by '{md.FullName}' in '{field.Module.Name}'. Modifying a [SyncVar] from another assembly is not supported. Please add a: 'public void Set{opFieldstRef.Name}(value) {{ this.{opFieldstRef.Name} = value; }}' function in '{opFieldstRef.DeclaringType.Name}' and call this function from '{md.FullName}' instead.");
Log.Error($"'[SyncVar] {opFieldstRef.DeclaringType.Name}.{opFieldstRef.Name}' in '{field.Module.Name}' is modified by '{md.FullName}' in '{md.Module.Name}'. Modifying a [SyncVar] from another assembly is not supported. Please add a: 'public void Set{opFieldstRef.Name}(value) {{ this.{opFieldstRef.Name} = value; }}' method in '{opFieldstRef.DeclaringType.Name}' and call this function from '{md.FullName}' instead.");
}
}
}

View File

@ -471,13 +471,6 @@ public void ProcessSyncVar(TypeDefinition td, FieldDefinition fd, Dictionary<Fie
continue;
}
if (fd.FieldType.IsArray)
{
Log.Error($"{fd.Name} has invalid type. Use SyncLists instead of arrays", fd);
WeavingFailed = true;
continue;
}
if (SyncObjectInitializer.ImplementsSyncObject(fd.FieldType))
{
Log.Warning($"{fd.Name} has [SyncVar] attribute. SyncLists should not be marked with SyncVar", fd);

View File

@ -23,7 +23,7 @@ public static bool HasNetworkConnectionParameter(MethodDefinition md)
public static MethodDefinition ProcessTargetRpcInvoke(WeaverTypes weaverTypes, Readers readers, Logger Log, TypeDefinition td, MethodDefinition md, MethodDefinition rpcCallFunc, ref bool WeavingFailed)
{
string trgName = Weaver.GenerateMethodName(Weaver.InvokeRpcPrefix, md);
string trgName = Weaver.GenerateMethodName(RemoteCalls.RemoteProcedureCalls.InvokeRpcPrefix, md);
MethodDefinition rpc = new MethodDefinition(trgName, MethodAttributes.Family |
MethodAttributes.Static |

View File

@ -42,6 +42,38 @@ public static MethodReference ResolveMethod(TypeReference t, AssemblyDefinition
return null;
}
public static FieldReference ResolveField(TypeReference tr, AssemblyDefinition assembly, Logger Log, string name, ref bool WeavingFailed)
{
if (tr == null)
{
Log.Error($"Cannot resolve Field {name} without a class");
WeavingFailed = true;
return null;
}
FieldReference field = ResolveField(tr, assembly, Log, m => m.Name == name, ref WeavingFailed);
if (field == null)
{
Log.Error($"Field not found with name {name} in type {tr.Name}", tr);
WeavingFailed = true;
}
return field;
}
public static FieldReference ResolveField(TypeReference t, AssemblyDefinition assembly, Logger Log, System.Func<FieldDefinition, bool> predicate, ref bool WeavingFailed)
{
foreach (FieldDefinition fieldRef in t.Resolve().Fields)
{
if (predicate(fieldRef))
{
return assembly.MainModule.ImportReference(fieldRef);
}
}
Log.Error($"Field not found in type {t.Name}", t);
WeavingFailed = true;
return null;
}
public static MethodReference TryResolveMethodInParents(TypeReference tr, AssemblyDefinition assembly, string name)
{
if (tr == null)

View File

@ -10,8 +10,6 @@ namespace Mirror.Weaver
// not static, because ILPostProcessor is multithreaded
internal class Weaver
{
public const string InvokeRpcPrefix = "InvokeUserCode_";
// generated code class
public const string GeneratedCodeNamespace = "Mirror";
public const string GeneratedCodeClassName = "GeneratedNetworkCode";

View File

@ -10,7 +10,7 @@ public class WeaverTypes
{
public MethodReference ScriptableObjectCreateInstanceMethod;
public MethodReference NetworkBehaviourDirtyBitsReference;
public FieldReference NetworkBehaviourDirtyBitsReference;
public MethodReference GetWriterReference;
public MethodReference ReturnWriterReference;
@ -90,7 +90,7 @@ public WeaverTypes(AssemblyDefinition assembly, Logger Log, ref bool WeavingFail
TypeReference NetworkBehaviourType = Import<NetworkBehaviour>();
NetworkBehaviourDirtyBitsReference = Resolvers.ResolveProperty(NetworkBehaviourType, assembly, "syncVarDirtyBits");
NetworkBehaviourDirtyBitsReference = Resolvers.ResolveField(NetworkBehaviourType, assembly, Log, "syncVarDirtyBits", ref WeavingFailed);
generatedSyncVarSetter = Resolvers.ResolveMethod(NetworkBehaviourType, assembly, Log, "GeneratedSyncVarSetter", ref WeavingFailed);
generatedSyncVarSetter_GameObject = Resolvers.ResolveMethod(NetworkBehaviourType, assembly, Log, "GeneratedSyncVarSetter_GameObject", ref WeavingFailed);

View File

@ -25,13 +25,12 @@ Transform:
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1724664263041697580}
serializedVersion: 2
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0.39999998, z: 0.5}
m_LocalScale: {x: 0.5, y: 0.1, z: 0.2}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 962190737825349125}
m_RootOrder: 0
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!33 &3866048407219963700
MeshFilter:
@ -52,12 +51,10 @@ MeshRenderer:
m_CastShadows: 1
m_ReceiveShadows: 1
m_DynamicOccludee: 1
m_StaticShadowCaster: 0
m_MotionVectors: 1
m_LightProbeUsage: 1
m_ReflectionProbeUsage: 1
m_RayTracingMode: 2
m_RayTraceProcedural: 0
m_RenderingLayerMask: 4294967295
m_RendererPriority: 0
m_Materials:
@ -82,7 +79,6 @@ MeshRenderer:
m_SortingLayerID: 0
m_SortingLayer: 0
m_SortingOrder: 0
m_AdditionalVertexStreams: {fileID: 0}
--- !u!1 &5620029719931856626
GameObject:
m_ObjectHideFlags: 0
@ -108,14 +104,13 @@ Transform:
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5620029719931856626}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 7513987664611104733}
m_Father: {fileID: 464867598898769114}
m_RootOrder: 0
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!33 &8168211270351413936
MeshFilter:
@ -136,12 +131,10 @@ MeshRenderer:
m_CastShadows: 1
m_ReceiveShadows: 1
m_DynamicOccludee: 1
m_StaticShadowCaster: 0
m_MotionVectors: 1
m_LightProbeUsage: 1
m_ReflectionProbeUsage: 1
m_RayTracingMode: 2
m_RayTraceProcedural: 0
m_RenderingLayerMask: 1
m_RendererPriority: 0
m_Materials:
@ -166,7 +159,6 @@ MeshRenderer:
m_SortingLayerID: 0
m_SortingLayer: 0
m_SortingOrder: 0
m_AdditionalVertexStreams: {fileID: 0}
--- !u!1 &7619140271685878370
GameObject:
m_ObjectHideFlags: 0
@ -198,14 +190,13 @@ Transform:
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7619140271685878370}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 1.08, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 962190737825349125}
m_Father: {fileID: 0}
m_RootOrder: 0
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!114 &5674344380471455553
MonoBehaviour:
@ -221,7 +212,7 @@ MonoBehaviour:
m_EditorClassIdentifier:
clientStarted: 0
sceneId: 0
_assetId: 1702147074
_assetId: 793773322
serverOnly: 0
visible: 0
hasSpawned: 0
@ -256,7 +247,7 @@ MonoBehaviour:
onlySyncOnChange: 1
onlySyncOnChangeCorrectionMultiplier: 2
rotationSensitivity: 0.01
compressRotation: 0
compressRotation: 1
positionPrecision: 0.01
scalePrecision: 0.01
--- !u!114 &-903079073849018483
@ -313,17 +304,9 @@ CharacterController:
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7619140271685878370}
m_Material: {fileID: 0}
m_IncludeLayers:
serializedVersion: 2
m_Bits: 0
m_ExcludeLayers:
serializedVersion: 2
m_Bits: 0
m_LayerOverridePriority: 0
m_IsTrigger: 0
m_ProvidesContacts: 0
m_Enabled: 0
serializedVersion: 3
serializedVersion: 2
m_Height: 2
m_Radius: 0.5
m_SlopeLimit: 45
@ -339,17 +322,8 @@ CapsuleCollider:
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7619140271685878370}
m_Material: {fileID: 0}
m_IncludeLayers:
serializedVersion: 2
m_Bits: 0
m_ExcludeLayers:
serializedVersion: 2
m_Bits: 0
m_LayerOverridePriority: 0
m_IsTrigger: 0
m_ProvidesContacts: 0
m_Enabled: 1
serializedVersion: 2
m_Radius: 0.5
m_Height: 2
m_Direction: 1
@ -361,21 +335,10 @@ Rigidbody:
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7619140271685878370}
serializedVersion: 4
serializedVersion: 2
m_Mass: 1
m_Drag: 0
m_AngularDrag: 0.05
m_CenterOfMass: {x: 0, y: 0, z: 0}
m_InertiaTensor: {x: 1, y: 1, z: 1}
m_InertiaRotation: {x: 0, y: 0, z: 0, w: 1}
m_IncludeLayers:
serializedVersion: 2
m_Bits: 0
m_ExcludeLayers:
serializedVersion: 2
m_Bits: 0
m_ImplicitCom: 1
m_ImplicitTensor: 1
m_UseGravity: 1
m_IsKinematic: 1
m_Interpolate: 0

View File

@ -1,4 +1,4 @@
using System.Collections;
using System.Collections;
using UnityEngine;
using UnityEngine.SceneManagement;
@ -7,17 +7,7 @@ namespace Mirror.Examples.AdditiveLevels
[AddComponentMenu("")]
public class AdditiveLevelsNetworkManager : NetworkManager
{
public static new AdditiveLevelsNetworkManager singleton { get; private set; }
/// <summary>
/// Runs on both Server and Client
/// Networking is NOT initialized when this fires
/// </summary>
public override void Awake()
{
base.Awake();
singleton = this;
}
public static new AdditiveLevelsNetworkManager singleton => (AdditiveLevelsNetworkManager)NetworkManager.singleton;
[Header("Additive Scenes - First is start scene")]

View File

@ -31,28 +31,28 @@ public enum GroundState : byte { Jumping, Falling, Grounded }
[Range(0.1f, 1f)]
public float jumpDelta = 0.2f;
[Header("Diagnostics - Do Not Modify")]
public GroundState groundState = GroundState.Grounded;
[Header("Diagnostics")]
[ReadOnly, SerializeField] GroundState groundState = GroundState.Grounded;
[Range(-1f, 1f)]
public float horizontal;
[Range(-1f, 1f)]
public float vertical;
[ReadOnly, SerializeField, Range(-1f, 1f)]
float horizontal;
[ReadOnly, SerializeField, Range(-1f, 1f)]
float vertical;
[Range(-200f, 200f)]
public float turnSpeed;
[ReadOnly, SerializeField, Range(-200f, 200f)]
float turnSpeed;
[Range(-10f, 10f)]
public float jumpSpeed;
[ReadOnly, SerializeField, Range(-10f, 10f)]
float jumpSpeed;
[Range(-1.5f, 1.5f)]
public float animVelocity;
[ReadOnly, SerializeField, Range(-1.5f, 1.5f)]
float animVelocity;
[Range(-1.5f, 1.5f)]
public float animRotation;
[ReadOnly, SerializeField, Range(-1.5f, 1.5f)]
float animRotation;
public Vector3Int velocity;
public Vector3 direction;
[ReadOnly, SerializeField] Vector3Int velocity;
[ReadOnly, SerializeField] Vector3 direction;
protected override void OnValidate()
{

View File

@ -25,14 +25,13 @@ Transform:
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4415124803507263412}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 3254954141432383832}
m_Father: {fileID: 5328458565928408179}
m_RootOrder: 0
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!33 &662729490405160656
MeshFilter:
@ -53,12 +52,10 @@ MeshRenderer:
m_CastShadows: 1
m_ReceiveShadows: 1
m_DynamicOccludee: 1
m_StaticShadowCaster: 0
m_MotionVectors: 1
m_LightProbeUsage: 1
m_ReflectionProbeUsage: 1
m_RayTracingMode: 2
m_RayTraceProcedural: 0
m_RenderingLayerMask: 1
m_RendererPriority: 0
m_Materials:
@ -83,7 +80,6 @@ MeshRenderer:
m_SortingLayerID: 0
m_SortingLayer: 0
m_SortingOrder: 0
m_AdditionalVertexStreams: {fileID: 0}
--- !u!1 &5815001218983416211
GameObject:
m_ObjectHideFlags: 0
@ -109,13 +105,12 @@ Transform:
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5815001218983416211}
serializedVersion: 2
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0.39999998, z: 0.5}
m_LocalScale: {x: 0.5, y: 0.1, z: 0.2}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 9057824595171805708}
m_RootOrder: 0
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!33 &1800893346221236401
MeshFilter:
@ -136,12 +131,10 @@ MeshRenderer:
m_CastShadows: 1
m_ReceiveShadows: 1
m_DynamicOccludee: 1
m_StaticShadowCaster: 0
m_MotionVectors: 1
m_LightProbeUsage: 1
m_ReflectionProbeUsage: 1
m_RayTracingMode: 2
m_RayTraceProcedural: 0
m_RenderingLayerMask: 4294967295
m_RendererPriority: 0
m_Materials:
@ -166,7 +159,6 @@ MeshRenderer:
m_SortingLayerID: 0
m_SortingLayer: 0
m_SortingOrder: 0
m_AdditionalVertexStreams: {fileID: 0}
--- !u!1 &8872462076811691049
GameObject:
m_ObjectHideFlags: 0
@ -199,14 +191,13 @@ Transform:
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8872462076811691049}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 1.08, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 9057824595171805708}
m_Father: {fileID: 0}
m_RootOrder: 0
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!114 &8537344390966522168
MonoBehaviour:
@ -222,7 +213,7 @@ MonoBehaviour:
m_EditorClassIdentifier:
clientStarted: 0
sceneId: 0
_assetId: 2014258290
_assetId: 4222099193
serverOnly: 0
visible: 0
hasSpawned: 0
@ -257,7 +248,7 @@ MonoBehaviour:
onlySyncOnChange: 1
onlySyncOnChangeCorrectionMultiplier: 2
rotationSensitivity: 0.01
compressRotation: 0
compressRotation: 1
positionPrecision: 0.01
scalePrecision: 0.01
--- !u!114 &-2082299755652640335
@ -314,17 +305,9 @@ CharacterController:
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8872462076811691049}
m_Material: {fileID: 0}
m_IncludeLayers:
serializedVersion: 2
m_Bits: 0
m_ExcludeLayers:
serializedVersion: 2
m_Bits: 0
m_LayerOverridePriority: 0
m_IsTrigger: 0
m_ProvidesContacts: 0
m_Enabled: 0
serializedVersion: 3
serializedVersion: 2
m_Height: 2
m_Radius: 0.5
m_SlopeLimit: 45
@ -340,17 +323,8 @@ CapsuleCollider:
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8872462076811691049}
m_Material: {fileID: 0}
m_IncludeLayers:
serializedVersion: 2
m_Bits: 0
m_ExcludeLayers:
serializedVersion: 2
m_Bits: 0
m_LayerOverridePriority: 0
m_IsTrigger: 0
m_ProvidesContacts: 0
m_Enabled: 1
serializedVersion: 2
m_Radius: 0.5
m_Height: 2
m_Direction: 1
@ -362,21 +336,10 @@ Rigidbody:
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8872462076811691049}
serializedVersion: 4
serializedVersion: 2
m_Mass: 1
m_Drag: 0
m_AngularDrag: 0.05
m_CenterOfMass: {x: 0, y: 0, z: 0}
m_InertiaTensor: {x: 1, y: 1, z: 1}
m_InertiaRotation: {x: 0, y: 0, z: 0, w: 1}
m_IncludeLayers:
serializedVersion: 2
m_Bits: 0
m_ExcludeLayers:
serializedVersion: 2
m_Bits: 0
m_ImplicitCom: 1
m_ImplicitTensor: 1
m_UseGravity: 0
m_IsKinematic: 1
m_Interpolate: 0

View File

@ -14,18 +14,6 @@ public class AdditiveNetworkManager : NetworkManager
[Tooltip("Add all sub-scenes to this list")]
public string[] subScenes;
public static new AdditiveNetworkManager singleton { get; private set; }
/// <summary>
/// Runs on both Server and Client
/// Networking is NOT initialized when this fires
/// </summary>
public override void Awake()
{
base.Awake();
singleton = this;
}
public override void OnStartServer()
{
base.OnStartServer();

View File

@ -31,28 +31,28 @@ public enum GroundState : byte { Jumping, Falling, Grounded }
[Range(0.1f, 1f)]
public float jumpDelta = 0.2f;
[Header("Diagnostics - Do Not Modify")]
public GroundState groundState = GroundState.Grounded;
[Header("Diagnostics")]
[ReadOnly, SerializeField] GroundState groundState = GroundState.Grounded;
[Range(-1f, 1f)]
public float horizontal;
[Range(-1f, 1f)]
public float vertical;
[ReadOnly, SerializeField, Range(-1f, 1f)]
float horizontal;
[ReadOnly, SerializeField, Range(-1f, 1f)]
float vertical;
[Range(-200f, 200f)]
public float turnSpeed;
[ReadOnly, SerializeField, Range(-200f, 200f)]
float turnSpeed;
[Range(-10f, 10f)]
public float jumpSpeed;
[ReadOnly, SerializeField, Range(-10f, 10f)]
float jumpSpeed;
[Range(-1.5f, 1.5f)]
public float animVelocity;
[ReadOnly, SerializeField, Range(-1.5f, 1.5f)]
float animVelocity;
[Range(-1.5f, 1.5f)]
public float animRotation;
[ReadOnly, SerializeField, Range(-1.5f, 1.5f)]
float animRotation;
public Vector3Int velocity;
public Vector3 direction;
[ReadOnly, SerializeField] Vector3Int velocity;
[ReadOnly, SerializeField] Vector3 direction;
protected override void OnValidate()
{

View File

@ -5,18 +5,6 @@ namespace Mirror.Examples.Basic
[AddComponentMenu("")]
public class BasicNetManager : NetworkManager
{
public static new BasicNetManager singleton { get; private set; }
/// <summary>
/// Runs on both Server and Client
/// Networking is NOT initialized when this fires
/// </summary>
public override void Awake()
{
base.Awake();
singleton = this;
}
/// <summary>
/// Called on the server when a client adds a new player with NetworkClient.AddPlayer.
/// <para>The default implementation for this function creates a new player object from the playerPrefab.</para>

View File

@ -366,7 +366,7 @@ MonoBehaviour:
deliveryTimeEmaDuration: 2
connectionQualityInterval: 3
timeInterpolationGui: 0
spawnAmount: 50000
spawnAmount: 10000
interleave: 2
spawnPrefab: {fileID: 449802645721213856, guid: 0ea79775d59804682a8cdd46b3811344,
type: 3}

View File

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

View File

@ -0,0 +1,80 @@
%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: BallMaterial
m_Shader: {fileID: 46, guid: 0000000000000000f000000000000000, type: 0}
m_ValidKeywords: []
m_InvalidKeywords: []
m_LightmapFlags: 4
m_EnableInstancingVariants: 0
m_DoubleSidedGI: 0
m_CustomRenderQueue: -1
stringTagMap: {}
disabledShaderPasses: []
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: 0
- _GlossMapScale: 1
- _Glossiness: 1
- _GlossyReflections: 1
- _Metallic: 1
- _Mode: 0
- _OcclusionStrength: 1
- _Parallax: 0.02
- _SmoothnessTextureChannel: 0
- _SpecularHighlights: 1
- _SrcBlend: 1
- _UVSec: 0
- _ZWrite: 1
m_Colors:
- _Color: {r: 0, g: 0, b: 0, a: 1}
- _EmissionColor: {r: 0, g: 0, b: 0, a: 1}
m_BuildTextureStacks: []

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 09fe33013804145e8a4ba1d18f834dcf
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 2100000
userData:
assetBundleName:
assetBundleVariant:

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 26e96d86a94c2451d85dcabf4aff3551
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,49 @@
using UnityEngine;
namespace Mirror.Examples.PredictionBenchmark
{
public class NetworkManagerPredictionBenchmark : NetworkManager
{
[Header("Spawns")]
public int spawnAmount = 1000;
public GameObject spawnPrefab;
public Bounds spawnArea = new Bounds(new Vector3(0, 2.5f, 0), new Vector3(10f, 5f, 10f));
public override void Awake()
{
base.Awake();
// ensure vsync is disabled for the benchmark, otherwise results are capped
QualitySettings.vSyncCount = 0;
}
void SpawnAll()
{
// spawn randomly inside the cage
for (int i = 0; i < spawnAmount; ++i)
{
// choose a random point within the cage
float x = Random.Range(spawnArea.min.x, spawnArea.max.x);
float y = Random.Range(spawnArea.min.y, spawnArea.max.y);
float z = Random.Range(spawnArea.min.z, spawnArea.max.z);
Vector3 position = new Vector3(x, y, z);
// spawn & position
GameObject go = Instantiate(spawnPrefab);
go.transform.position = position;
NetworkServer.Spawn(go);
}
}
public override void OnStartServer()
{
base.OnStartServer();
SpawnAll();
// disable rendering on server to reduce noise in profiling.
// keep enabled in host mode though.
if (mode == NetworkManagerMode.ServerOnly)
Camera.main.enabled = false;
}
}
}

View File

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

View File

@ -0,0 +1,51 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!1 &6080703956733773953
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 5774152995658786670}
- component: {fileID: 4958697633604052194}
m_Layer: 0
m_Name: PlayerSpectator
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &5774152995658786670
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6080703956733773953}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_RootOrder: 0
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!114 &4958697633604052194
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6080703956733773953}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 9b91ecbcc199f4492b9a91e820070131, type: 3}
m_Name:
m_EditorClassIdentifier:
sceneId: 0
_assetId: 0
serverOnly: 0
visibility: 0
hasSpawned: 0

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: feea51e51b4564f06a38482bbebac8fa
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

Some files were not shown because too many files have changed in this diff Show More