mirror of
https://github.com/MirrorNetworking/Mirror.git
synced 2024-11-18 02:50:32 +00:00
Merged master
This commit is contained in:
commit
8e873e5a1d
12
.github/workflows/RunUnityTests.yml
vendored
12
.github/workflows/RunUnityTests.yml
vendored
@ -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"
|
||||
|
4
.github/workflows/Semantic.yml
vendored
4
.github/workflows/Semantic.yml
vendored
@ -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
|
||||
|
2
.github/workflows/activation.yml
vendored
2
.github/workflows/activation.yml
vendored
@ -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 }}
|
||||
|
@ -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,
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -25,7 +25,7 @@ int GetVisRange(NetworkIdentity identity)
|
||||
}
|
||||
|
||||
[ServerCallback]
|
||||
public override void Reset()
|
||||
public override void ResetState()
|
||||
{
|
||||
lastRebuildTime = 0D;
|
||||
CustomRanges.Clear();
|
||||
|
@ -7,14 +7,75 @@ namespace Mirror
|
||||
[AddComponentMenu("Network/ Interest Management/ Match/Match Interest Management")]
|
||||
public class MatchInterestManagement : InterestManagement
|
||||
{
|
||||
readonly Dictionary<Guid, HashSet<NetworkIdentity>> matchObjects =
|
||||
new Dictionary<Guid, HashSet<NetworkIdentity>>();
|
||||
[Header("Diagnostics")]
|
||||
[ReadOnly, SerializeField]
|
||||
internal ushort matchCount;
|
||||
|
||||
readonly Dictionary<NetworkIdentity, Guid> lastObjectMatch =
|
||||
new Dictionary<NetworkIdentity, Guid>();
|
||||
readonly Dictionary<Guid, HashSet<NetworkMatch>> matchObjects =
|
||||
new Dictionary<Guid, HashSet<NetworkMatch>>();
|
||||
|
||||
readonly HashSet<Guid> dirtyMatches = new HashSet<Guid>();
|
||||
|
||||
// LateUpdate so that all spawns/despawns/changes are done
|
||||
[ServerCallback]
|
||||
void LateUpdate()
|
||||
{
|
||||
// Rebuild all dirty matches
|
||||
// dirtyMatches will be empty if no matches changed members
|
||||
// by spawning or destroying or changing matchId in this frame.
|
||||
foreach (Guid dirtyMatch in dirtyMatches)
|
||||
{
|
||||
// rebuild always, even if matchObjects[dirtyMatch] is empty.
|
||||
// Players might have left the match, but they may still be spawned.
|
||||
RebuildMatchObservers(dirtyMatch);
|
||||
|
||||
// clean up empty entries in the dict
|
||||
if (matchObjects[dirtyMatch].Count == 0)
|
||||
matchObjects.Remove(dirtyMatch);
|
||||
}
|
||||
|
||||
dirtyMatches.Clear();
|
||||
|
||||
matchCount = (ushort)matchObjects.Count;
|
||||
}
|
||||
|
||||
[ServerCallback]
|
||||
void RebuildMatchObservers(Guid matchId)
|
||||
{
|
||||
foreach (NetworkMatch networkMatch in matchObjects[matchId])
|
||||
if (networkMatch.netIdentity != null)
|
||||
NetworkServer.RebuildObservers(networkMatch.netIdentity, false);
|
||||
}
|
||||
|
||||
// called by NetworkMatch.matchId setter
|
||||
[ServerCallback]
|
||||
internal void OnMatchChanged(NetworkMatch networkMatch, Guid oldMatch)
|
||||
{
|
||||
// This object is in a new match so observers in the prior match
|
||||
// and the new match need to rebuild their respective observers lists.
|
||||
|
||||
// Remove this object from the hashset of the match it just left
|
||||
// Guid.Empty is never a valid matchId
|
||||
if (oldMatch != Guid.Empty)
|
||||
{
|
||||
dirtyMatches.Add(oldMatch);
|
||||
matchObjects[oldMatch].Remove(networkMatch);
|
||||
}
|
||||
|
||||
// Guid.Empty is never a valid matchId
|
||||
if (networkMatch.matchId == Guid.Empty)
|
||||
return;
|
||||
|
||||
dirtyMatches.Add(networkMatch.matchId);
|
||||
|
||||
// Make sure this new match is in the dictionary
|
||||
if (!matchObjects.ContainsKey(networkMatch.matchId))
|
||||
matchObjects[networkMatch.matchId] = new HashSet<NetworkMatch>();
|
||||
|
||||
// Add this object to the hashset of the new match
|
||||
matchObjects[networkMatch.matchId].Add(networkMatch);
|
||||
}
|
||||
|
||||
[ServerCallback]
|
||||
public override void OnSpawned(NetworkIdentity identity)
|
||||
{
|
||||
@ -22,114 +83,43 @@ public override void OnSpawned(NetworkIdentity identity)
|
||||
return;
|
||||
|
||||
Guid networkMatchId = networkMatch.matchId;
|
||||
lastObjectMatch[identity] = networkMatchId;
|
||||
|
||||
// Guid.Empty is never a valid matchId...do not add to matchObjects collection
|
||||
if (networkMatchId == Guid.Empty)
|
||||
return;
|
||||
|
||||
// Debug.Log($"MatchInterestManagement.OnSpawned({identity.name}) currentMatch: {currentMatch}");
|
||||
if (!matchObjects.TryGetValue(networkMatchId, out HashSet<NetworkIdentity> objects))
|
||||
if (!matchObjects.TryGetValue(networkMatchId, out HashSet<NetworkMatch> objects))
|
||||
{
|
||||
objects = new HashSet<NetworkIdentity>();
|
||||
objects = new HashSet<NetworkMatch>();
|
||||
matchObjects.Add(networkMatchId, objects);
|
||||
}
|
||||
|
||||
objects.Add(identity);
|
||||
objects.Add(networkMatch);
|
||||
|
||||
// Match ID could have been set in NetworkBehaviour::OnStartServer on this object.
|
||||
// Since that's after OnCheckObserver is called it would be missed, so force Rebuild here.
|
||||
// Add the current match to dirtyMatches for Update to rebuild it.
|
||||
// Add the current match to dirtyMatches for LateUpdate to rebuild it.
|
||||
dirtyMatches.Add(networkMatchId);
|
||||
}
|
||||
|
||||
[ServerCallback]
|
||||
public override void OnDestroyed(NetworkIdentity identity)
|
||||
{
|
||||
// Don't RebuildSceneObservers here - that will happen in Update.
|
||||
// Don't RebuildSceneObservers here - that will happen in LateUpdate.
|
||||
// Multiple objects could be destroyed in same frame and we don't
|
||||
// want to rebuild for each one...let Update do it once.
|
||||
// We must add the current match to dirtyMatches for Update to rebuild it.
|
||||
if (lastObjectMatch.TryGetValue(identity, out Guid currentMatch))
|
||||
// want to rebuild for each one...let LateUpdate do it once.
|
||||
// We must add the current match to dirtyMatches for LateUpdate to rebuild it.
|
||||
if (identity.TryGetComponent(out NetworkMatch currentMatch))
|
||||
{
|
||||
lastObjectMatch.Remove(identity);
|
||||
if (currentMatch != Guid.Empty && matchObjects.TryGetValue(currentMatch, out HashSet<NetworkIdentity> objects) && objects.Remove(identity))
|
||||
dirtyMatches.Add(currentMatch);
|
||||
if (currentMatch.matchId != Guid.Empty &&
|
||||
matchObjects.TryGetValue(currentMatch.matchId, out HashSet<NetworkMatch> objects) &&
|
||||
objects.Remove(currentMatch))
|
||||
dirtyMatches.Add(currentMatch.matchId);
|
||||
}
|
||||
}
|
||||
|
||||
// internal so we can update from tests
|
||||
[ServerCallback]
|
||||
internal void Update()
|
||||
{
|
||||
// for each spawned:
|
||||
// if match changed:
|
||||
// add previous to dirty
|
||||
// add new to dirty
|
||||
foreach (NetworkIdentity identity in NetworkServer.spawned.Values)
|
||||
{
|
||||
// Ignore objects that don't have a NetworkMatch component
|
||||
if (!identity.TryGetComponent(out NetworkMatch networkMatch))
|
||||
continue;
|
||||
|
||||
Guid newMatch = networkMatch.matchId;
|
||||
if (!lastObjectMatch.TryGetValue(identity, out Guid currentMatch))
|
||||
continue;
|
||||
|
||||
// Guid.Empty is never a valid matchId
|
||||
// Nothing to do if matchId hasn't changed
|
||||
if (newMatch == Guid.Empty || newMatch == currentMatch)
|
||||
continue;
|
||||
|
||||
// Mark new/old matches as dirty so they get rebuilt
|
||||
UpdateDirtyMatches(newMatch, currentMatch);
|
||||
|
||||
// This object is in a new match so observers in the prior match
|
||||
// and the new match need to rebuild their respective observers lists.
|
||||
UpdateMatchObjects(identity, newMatch, currentMatch);
|
||||
}
|
||||
|
||||
// rebuild all dirty matches
|
||||
foreach (Guid dirtyMatch in dirtyMatches)
|
||||
RebuildMatchObservers(dirtyMatch);
|
||||
|
||||
dirtyMatches.Clear();
|
||||
}
|
||||
|
||||
void UpdateDirtyMatches(Guid newMatch, Guid currentMatch)
|
||||
{
|
||||
// Guid.Empty is never a valid matchId
|
||||
if (currentMatch != Guid.Empty)
|
||||
dirtyMatches.Add(currentMatch);
|
||||
|
||||
dirtyMatches.Add(newMatch);
|
||||
}
|
||||
|
||||
void UpdateMatchObjects(NetworkIdentity netIdentity, Guid newMatch, Guid currentMatch)
|
||||
{
|
||||
// Remove this object from the hashset of the match it just left
|
||||
// Guid.Empty is never a valid matchId
|
||||
if (currentMatch != Guid.Empty)
|
||||
matchObjects[currentMatch].Remove(netIdentity);
|
||||
|
||||
// Set this to the new match this object just entered
|
||||
lastObjectMatch[netIdentity] = newMatch;
|
||||
|
||||
// Make sure this new match is in the dictionary
|
||||
if (!matchObjects.ContainsKey(newMatch))
|
||||
matchObjects.Add(newMatch, new HashSet<NetworkIdentity>());
|
||||
|
||||
// Add this object to the hashset of the new match
|
||||
matchObjects[newMatch].Add(netIdentity);
|
||||
}
|
||||
|
||||
void RebuildMatchObservers(Guid matchId)
|
||||
{
|
||||
foreach (NetworkIdentity netIdentity in matchObjects[matchId])
|
||||
if (netIdentity != null)
|
||||
NetworkServer.RebuildObservers(netIdentity, false);
|
||||
}
|
||||
|
||||
public override bool OnCheckObserver(NetworkIdentity identity, NetworkConnectionToClient newObserver)
|
||||
{
|
||||
// Never observed if no NetworkMatch component
|
||||
@ -151,24 +141,24 @@ public override bool OnCheckObserver(NetworkIdentity identity, NetworkConnection
|
||||
return identityNetworkMatch.matchId == newObserverNetworkMatch.matchId;
|
||||
}
|
||||
|
||||
[ServerCallback]
|
||||
public override void OnRebuildObservers(NetworkIdentity identity, HashSet<NetworkConnectionToClient> newObservers)
|
||||
{
|
||||
if (!identity.TryGetComponent(out NetworkMatch networkMatch))
|
||||
return;
|
||||
|
||||
Guid matchId = networkMatch.matchId;
|
||||
|
||||
// Guid.Empty is never a valid matchId
|
||||
if (matchId == Guid.Empty)
|
||||
if (networkMatch.matchId == Guid.Empty)
|
||||
return;
|
||||
|
||||
if (!matchObjects.TryGetValue(matchId, out HashSet<NetworkIdentity> objects))
|
||||
// Abort if this match hasn't been created yet by OnSpawned or OnMatchChanged
|
||||
if (!matchObjects.TryGetValue(networkMatch.matchId, out HashSet<NetworkMatch> objects))
|
||||
return;
|
||||
|
||||
// Add everything in the hashset for this object's current match
|
||||
foreach (NetworkIdentity networkIdentity in objects)
|
||||
if (networkIdentity != null && networkIdentity.connectionToClient != null)
|
||||
newObservers.Add(networkIdentity.connectionToClient);
|
||||
foreach (NetworkMatch netMatch in objects)
|
||||
if (netMatch.netIdentity != null && netMatch.netIdentity.connectionToClient != null)
|
||||
newObservers.Add(netMatch.netIdentity.connectionToClient);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,34 @@ namespace Mirror
|
||||
[HelpURL("https://mirror-networking.gitbook.io/docs/guides/interest-management")]
|
||||
public class NetworkMatch : NetworkBehaviour
|
||||
{
|
||||
Guid _matchId;
|
||||
|
||||
#pragma warning disable IDE0052 // Suppress warning for unused field...this is for debugging purposes
|
||||
[SerializeField, ReadOnly]
|
||||
[Tooltip("Match ID is shown here on server for debugging purposes.")]
|
||||
string MatchID = string.Empty;
|
||||
#pragma warning restore IDE0052
|
||||
|
||||
///<summary>Set this to the same value on all networked objects that belong to a given match</summary>
|
||||
public Guid matchId;
|
||||
public Guid matchId
|
||||
{
|
||||
get => _matchId;
|
||||
set
|
||||
{
|
||||
if (!NetworkServer.active)
|
||||
throw new InvalidOperationException("matchId can only be set at runtime on active server");
|
||||
|
||||
if (_matchId == value)
|
||||
return;
|
||||
|
||||
Guid oldMatch = _matchId;
|
||||
_matchId = value;
|
||||
MatchID = value.ToString();
|
||||
|
||||
// Only inform the AOI if this netIdentity has been spawned (isServer) and only if using a MatchInterestManagement
|
||||
if (isServer && NetworkServer.aoi is MatchInterestManagement matchInterestManagement)
|
||||
matchInterestManagement.OnMatchChanged(this, oldMatch);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -72,7 +72,7 @@ public override void OnRebuildObservers(NetworkIdentity identity, HashSet<Networ
|
||||
}
|
||||
|
||||
[ServerCallback]
|
||||
public override void Reset()
|
||||
public override void ResetState()
|
||||
{
|
||||
lastRebuildTime = 0D;
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
// simple component that holds team information
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
@ -8,10 +9,31 @@ namespace Mirror
|
||||
[HelpURL("https://mirror-networking.gitbook.io/docs/guides/interest-management")]
|
||||
public class NetworkTeam : NetworkBehaviour
|
||||
{
|
||||
[Tooltip("Set this to the same value on all networked objects that belong to a given team")]
|
||||
[SyncVar] public string teamId = string.Empty;
|
||||
[SerializeField]
|
||||
[Tooltip("Set teamId on Server at runtime to the same value on all networked objects that belong to a given team")]
|
||||
string _teamId;
|
||||
|
||||
public string teamId
|
||||
{
|
||||
get => _teamId;
|
||||
set
|
||||
{
|
||||
if (Application.IsPlaying(gameObject) && !NetworkServer.active)
|
||||
throw new InvalidOperationException("teamId can only be set at runtime on active server");
|
||||
|
||||
if (_teamId == value)
|
||||
return;
|
||||
|
||||
string oldTeam = _teamId;
|
||||
_teamId = value;
|
||||
|
||||
//Only inform the AOI if this netIdentity has been spawned(isServer) and only if using a TeamInterestManagement
|
||||
if (isServer && NetworkServer.aoi is TeamInterestManagement teamInterestManagement)
|
||||
teamInterestManagement.OnTeamChanged(this, oldTeam);
|
||||
}
|
||||
}
|
||||
|
||||
[Tooltip("When enabled this object is visible to all clients. Typically this would be true for player objects")]
|
||||
[SyncVar] public bool forceShown;
|
||||
public bool forceShown;
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
@ -6,126 +6,112 @@ namespace Mirror
|
||||
[AddComponentMenu("Network/ Interest Management/ Team/Team Interest Management")]
|
||||
public class TeamInterestManagement : InterestManagement
|
||||
{
|
||||
readonly Dictionary<string, HashSet<NetworkIdentity>> teamObjects = new Dictionary<string, HashSet<NetworkIdentity>>();
|
||||
readonly Dictionary<NetworkIdentity, string> lastObjectTeam = new Dictionary<NetworkIdentity, string>();
|
||||
readonly Dictionary<string, HashSet<NetworkTeam>> teamObjects =
|
||||
new Dictionary<string, HashSet<NetworkTeam>>();
|
||||
|
||||
readonly HashSet<string> dirtyTeams = new HashSet<string>();
|
||||
|
||||
// LateUpdate so that all spawns/despawns/changes are done
|
||||
[ServerCallback]
|
||||
void LateUpdate()
|
||||
{
|
||||
// Rebuild all dirty teams
|
||||
// dirtyTeams will be empty if no teams changed members
|
||||
// by spawning or destroying or changing teamId in this frame.
|
||||
foreach (string dirtyTeam in dirtyTeams)
|
||||
{
|
||||
// rebuild always, even if teamObjects[dirtyTeam] is empty.
|
||||
// Players might have left the team, but they may still be spawned.
|
||||
RebuildTeamObservers(dirtyTeam);
|
||||
|
||||
// clean up empty entries in the dict
|
||||
if (teamObjects[dirtyTeam].Count == 0)
|
||||
teamObjects.Remove(dirtyTeam);
|
||||
}
|
||||
|
||||
dirtyTeams.Clear();
|
||||
}
|
||||
|
||||
[ServerCallback]
|
||||
void RebuildTeamObservers(string teamId)
|
||||
{
|
||||
foreach (NetworkTeam networkTeam in teamObjects[teamId])
|
||||
if (networkTeam.netIdentity != null)
|
||||
NetworkServer.RebuildObservers(networkTeam.netIdentity, false);
|
||||
}
|
||||
|
||||
// called by NetworkTeam.teamId setter
|
||||
[ServerCallback]
|
||||
internal void OnTeamChanged(NetworkTeam networkTeam, string oldTeam)
|
||||
{
|
||||
// This object is in a new team so observers in the prior team
|
||||
// and the new team need to rebuild their respective observers lists.
|
||||
|
||||
// Remove this object from the hashset of the team it just left
|
||||
// Null / Empty string is never a valid teamId
|
||||
if (!string.IsNullOrWhiteSpace(oldTeam))
|
||||
{
|
||||
dirtyTeams.Add(oldTeam);
|
||||
teamObjects[oldTeam].Remove(networkTeam);
|
||||
}
|
||||
|
||||
// Null / Empty string is never a valid teamId
|
||||
if (string.IsNullOrWhiteSpace(networkTeam.teamId))
|
||||
return;
|
||||
|
||||
dirtyTeams.Add(networkTeam.teamId);
|
||||
|
||||
// Make sure this new team is in the dictionary
|
||||
if (!teamObjects.ContainsKey(networkTeam.teamId))
|
||||
teamObjects[networkTeam.teamId] = new HashSet<NetworkTeam>();
|
||||
|
||||
// Add this object to the hashset of the new team
|
||||
teamObjects[networkTeam.teamId].Add(networkTeam);
|
||||
}
|
||||
|
||||
[ServerCallback]
|
||||
public override void OnSpawned(NetworkIdentity identity)
|
||||
{
|
||||
if (!identity.TryGetComponent(out NetworkTeam identityNetworkTeam))
|
||||
if (!identity.TryGetComponent(out NetworkTeam networkTeam))
|
||||
return;
|
||||
|
||||
string networkTeamId = identityNetworkTeam.teamId;
|
||||
lastObjectTeam[identity] = networkTeamId;
|
||||
string networkTeamId = networkTeam.teamId;
|
||||
|
||||
// Null / Empty string is never a valid teamId...do not add to teamObjects collection
|
||||
if (string.IsNullOrWhiteSpace(networkTeamId))
|
||||
return;
|
||||
|
||||
//Debug.Log($"TeamInterestManagement.OnSpawned {identity.name} {networkTeamId}");
|
||||
|
||||
if (!teamObjects.TryGetValue(networkTeamId, out HashSet<NetworkIdentity> objects))
|
||||
// Debug.Log($"TeamInterestManagement.OnSpawned({identity.name}) currentTeam: {currentTeam}");
|
||||
if (!teamObjects.TryGetValue(networkTeamId, out HashSet<NetworkTeam> objects))
|
||||
{
|
||||
objects = new HashSet<NetworkIdentity>();
|
||||
objects = new HashSet<NetworkTeam>();
|
||||
teamObjects.Add(networkTeamId, objects);
|
||||
}
|
||||
|
||||
objects.Add(identity);
|
||||
objects.Add(networkTeam);
|
||||
|
||||
// Team ID could have been set in NetworkBehaviour::OnStartServer on this object.
|
||||
// Since that's after OnCheckObserver is called it would be missed, so force Rebuild here.
|
||||
// Add the current team to dirtyTeams for Update to rebuild it.
|
||||
// Add the current team to dirtyTeames for LateUpdate to rebuild it.
|
||||
dirtyTeams.Add(networkTeamId);
|
||||
}
|
||||
|
||||
[ServerCallback]
|
||||
public override void OnDestroyed(NetworkIdentity identity)
|
||||
{
|
||||
// Don't RebuildSceneObservers here - that will happen in Update.
|
||||
// Don't RebuildSceneObservers here - that will happen in LateUpdate.
|
||||
// Multiple objects could be destroyed in same frame and we don't
|
||||
// want to rebuild for each one...let Update do it once.
|
||||
// We must add the current team to dirtyTeams for Update to rebuild it.
|
||||
if (lastObjectTeam.TryGetValue(identity, out string currentTeam))
|
||||
// want to rebuild for each one...let LateUpdate do it once.
|
||||
// We must add the current team to dirtyTeames for LateUpdate to rebuild it.
|
||||
if (identity.TryGetComponent(out NetworkTeam currentTeam))
|
||||
{
|
||||
lastObjectTeam.Remove(identity);
|
||||
if (!string.IsNullOrWhiteSpace(currentTeam) && teamObjects.TryGetValue(currentTeam, out HashSet<NetworkIdentity> objects) && objects.Remove(identity))
|
||||
dirtyTeams.Add(currentTeam);
|
||||
if (!string.IsNullOrWhiteSpace(currentTeam.teamId) &&
|
||||
teamObjects.TryGetValue(currentTeam.teamId, out HashSet<NetworkTeam> objects) &&
|
||||
objects.Remove(currentTeam))
|
||||
dirtyTeams.Add(currentTeam.teamId);
|
||||
}
|
||||
}
|
||||
|
||||
// internal so we can update from tests
|
||||
[ServerCallback]
|
||||
internal void Update()
|
||||
{
|
||||
// for each spawned:
|
||||
// if team changed:
|
||||
// add previous to dirty
|
||||
// add new to dirty
|
||||
foreach (NetworkIdentity netIdentity in NetworkServer.spawned.Values)
|
||||
{
|
||||
// Ignore objects that don't have a NetworkTeam component
|
||||
if (!netIdentity.TryGetComponent(out NetworkTeam identityNetworkTeam))
|
||||
continue;
|
||||
|
||||
string networkTeamId = identityNetworkTeam.teamId;
|
||||
if (!lastObjectTeam.TryGetValue(netIdentity, out string currentTeam))
|
||||
continue;
|
||||
|
||||
// Null / Empty string is never a valid teamId
|
||||
// Nothing to do if teamId hasn't changed
|
||||
if (string.IsNullOrWhiteSpace(networkTeamId) || networkTeamId == currentTeam)
|
||||
continue;
|
||||
|
||||
// Mark new/old Teams as dirty so they get rebuilt
|
||||
UpdateDirtyTeams(networkTeamId, currentTeam);
|
||||
|
||||
// This object is in a new team so observers in the prior team
|
||||
// and the new team need to rebuild their respective observers lists.
|
||||
UpdateTeamObjects(netIdentity, networkTeamId, currentTeam);
|
||||
}
|
||||
|
||||
// rebuild all dirty teams
|
||||
foreach (string dirtyTeam in dirtyTeams)
|
||||
RebuildTeamObservers(dirtyTeam);
|
||||
|
||||
dirtyTeams.Clear();
|
||||
}
|
||||
|
||||
void UpdateDirtyTeams(string newTeam, string currentTeam)
|
||||
{
|
||||
// Null / Empty string is never a valid teamId
|
||||
if (!string.IsNullOrWhiteSpace(currentTeam))
|
||||
dirtyTeams.Add(currentTeam);
|
||||
|
||||
dirtyTeams.Add(newTeam);
|
||||
}
|
||||
|
||||
void UpdateTeamObjects(NetworkIdentity netIdentity, string newTeam, string currentTeam)
|
||||
{
|
||||
// Remove this object from the hashset of the team it just left
|
||||
// string.Empty is never a valid teamId
|
||||
if (!string.IsNullOrWhiteSpace(currentTeam))
|
||||
teamObjects[currentTeam].Remove(netIdentity);
|
||||
|
||||
// Set this to the new team this object just entered
|
||||
lastObjectTeam[netIdentity] = newTeam;
|
||||
|
||||
// Make sure this new team is in the dictionary
|
||||
if (!teamObjects.ContainsKey(newTeam))
|
||||
teamObjects.Add(newTeam, new HashSet<NetworkIdentity>());
|
||||
|
||||
// Add this object to the hashset of the new team
|
||||
teamObjects[newTeam].Add(netIdentity);
|
||||
}
|
||||
|
||||
void RebuildTeamObservers(string teamId)
|
||||
{
|
||||
foreach (NetworkIdentity netIdentity in teamObjects[teamId])
|
||||
if (netIdentity != null)
|
||||
NetworkServer.RebuildObservers(netIdentity, false);
|
||||
}
|
||||
|
||||
public override bool OnCheckObserver(NetworkIdentity identity, NetworkConnectionToClient newObserver)
|
||||
{
|
||||
// Always observed if no NetworkTeam component
|
||||
@ -135,7 +121,7 @@ public override bool OnCheckObserver(NetworkIdentity identity, NetworkConnection
|
||||
if (identityNetworkTeam.forceShown)
|
||||
return true;
|
||||
|
||||
// string.Empty is never a valid teamId
|
||||
// Null / Empty string is never a valid teamId
|
||||
if (string.IsNullOrWhiteSpace(identityNetworkTeam.teamId))
|
||||
return false;
|
||||
|
||||
@ -149,7 +135,7 @@ public override bool OnCheckObserver(NetworkIdentity identity, NetworkConnection
|
||||
|
||||
//Debug.Log($"TeamInterestManagement.OnCheckObserver {identity.name} {identityNetworkTeam.teamId} | {newObserver.identity.name} {newObserverNetworkTeam.teamId}");
|
||||
|
||||
// Observed only if teamId's match
|
||||
// Observed only if teamId's team
|
||||
return identityNetworkTeam.teamId == newObserverNetworkTeam.teamId;
|
||||
}
|
||||
|
||||
@ -173,14 +159,14 @@ public override void OnRebuildObservers(NetworkIdentity identity, HashSet<Networ
|
||||
if (string.IsNullOrWhiteSpace(networkTeam.teamId))
|
||||
return;
|
||||
|
||||
// Abort if this team hasn't been created yet by OnSpawned or UpdateTeamObjects
|
||||
if (!teamObjects.TryGetValue(networkTeam.teamId, out HashSet<NetworkIdentity> objects))
|
||||
// Abort if this team hasn't been created yet by OnSpawned or OnTeamChanged
|
||||
if (!teamObjects.TryGetValue(networkTeam.teamId, out HashSet<NetworkTeam> objects))
|
||||
return;
|
||||
|
||||
// Add everything in the hashset for this object's current team
|
||||
foreach (NetworkIdentity networkIdentity in objects)
|
||||
if (networkIdentity != null && networkIdentity.connectionToClient != null)
|
||||
newObservers.Add(networkIdentity.connectionToClient);
|
||||
foreach (NetworkTeam netTeam in objects)
|
||||
if (netTeam.netIdentity != null && netTeam.netIdentity.connectionToClient != null)
|
||||
newObservers.Add(netTeam.netIdentity.connectionToClient);
|
||||
}
|
||||
|
||||
void AddAllConnections(HashSet<NetworkConnectionToClient> newObservers)
|
||||
|
196
Assets/Mirror/Components/LagCompensation/LagCompensator.cs
Normal file
196
Assets/Mirror/Components/LagCompensation/LagCompensator.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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:
|
@ -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);
|
||||
AnimatorStateInfo st = animator.IsInTransition(i)
|
||||
? animator.GetNextAnimatorStateInfo(i)
|
||||
: animator.GetCurrentAnimatorStateInfo(i);
|
||||
writer.WriteInt(st.fullPathHash);
|
||||
writer.WriteFloat(st.normalizedTime);
|
||||
}
|
||||
else
|
||||
{
|
||||
AnimatorStateInfo st = animator.GetCurrentAnimatorStateInfo(i);
|
||||
writer.WriteInt(st.fullPathHash);
|
||||
writer.WriteFloat(st.normalizedTime);
|
||||
}
|
||||
writer.WriteFloat(animator.GetLayerWeight(i));
|
||||
}
|
||||
WriteParameters(writer, initialState);
|
||||
WriteParameters(writer, true);
|
||||
}
|
||||
}
|
||||
|
||||
@ -392,11 +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);
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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,6 +115,29 @@ void UpdateServerBroadcast()
|
||||
// send snapshot without timestamp.
|
||||
// receiver gets it from batch timestamp to save bandwidth.
|
||||
TransformSnapshot snapshot = Construct();
|
||||
|
||||
if (changedDetection)
|
||||
{
|
||||
cachedChangedComparison = CompareChangedSnapshots(snapshot);
|
||||
|
||||
if ((cachedChangedComparison == Changed.None || cachedChangedComparison == Changed.CompressRot) && hasSentUnchangedPosition && onlySyncOnChange) { return; }
|
||||
|
||||
SyncData syncData = new SyncData(cachedChangedComparison, snapshot);
|
||||
|
||||
RpcServerToClientSync(syncData);
|
||||
|
||||
if (cachedChangedComparison == Changed.None || cachedChangedComparison == Changed.CompressRot)
|
||||
{
|
||||
hasSentUnchangedPosition = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
hasSentUnchangedPosition = false;
|
||||
UpdateLastSentSnapshot(cachedChangedComparison, snapshot);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
cachedSnapshotComparison = CompareSnapshots(snapshot);
|
||||
if (cachedSnapshotComparison && hasSentUnchangedPosition && onlySyncOnChange) { return; }
|
||||
|
||||
@ -144,7 +167,16 @@ void UpdateServerBroadcast()
|
||||
else
|
||||
{
|
||||
hasSentUnchangedPosition = false;
|
||||
lastSnapshot = snapshot;
|
||||
|
||||
// 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,6 +243,29 @@ void UpdateClientBroadcast()
|
||||
// send snapshot without timestamp.
|
||||
// receiver gets it from batch timestamp to save bandwidth.
|
||||
TransformSnapshot snapshot = Construct();
|
||||
|
||||
if (changedDetection)
|
||||
{
|
||||
cachedChangedComparison = CompareChangedSnapshots(snapshot);
|
||||
|
||||
if ((cachedChangedComparison == Changed.None || cachedChangedComparison == Changed.CompressRot) && hasSentUnchangedPosition && onlySyncOnChange) { return; }
|
||||
|
||||
SyncData syncData = new SyncData(cachedChangedComparison, snapshot);
|
||||
|
||||
CmdClientToServerSync(syncData);
|
||||
|
||||
if (cachedChangedComparison == Changed.None || cachedChangedComparison == Changed.CompressRot)
|
||||
{
|
||||
hasSentUnchangedPosition = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
hasSentUnchangedPosition = false;
|
||||
UpdateLastSentSnapshot(cachedChangedComparison, snapshot);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
cachedSnapshotComparison = CompareSnapshots(snapshot);
|
||||
if (cachedSnapshotComparison && hasSentUnchangedPosition && onlySyncOnChange) { return; }
|
||||
|
||||
@ -240,7 +295,15 @@ void UpdateClientBroadcast()
|
||||
else
|
||||
{
|
||||
hasSentUnchangedPosition = false;
|
||||
lastSnapshot = snapshot;
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
156
Assets/Mirror/Components/NetworkTransform/TransformSyncData.cs
Normal file
156
Assets/Mirror/Components/NetworkTransform/TransformSyncData.cs
Normal file
@ -0,0 +1,156 @@
|
||||
using UnityEngine;
|
||||
using System;
|
||||
using Mirror;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
[Serializable]
|
||||
public struct SyncData
|
||||
{
|
||||
public Changed changedDataByte;
|
||||
public Vector3 position;
|
||||
public Quaternion quatRotation;
|
||||
public Vector3 vecRotation;
|
||||
public Vector3 scale;
|
||||
|
||||
public SyncData(Changed _dataChangedByte, Vector3 _position, Quaternion _rotation, Vector3 _scale)
|
||||
{
|
||||
this.changedDataByte = _dataChangedByte;
|
||||
this.position = _position;
|
||||
this.quatRotation = _rotation;
|
||||
this.vecRotation = quatRotation.eulerAngles;
|
||||
this.scale = _scale;
|
||||
}
|
||||
|
||||
public SyncData(Changed _dataChangedByte, TransformSnapshot _snapshot)
|
||||
{
|
||||
this.changedDataByte = _dataChangedByte;
|
||||
this.position = _snapshot.position;
|
||||
this.quatRotation = _snapshot.rotation;
|
||||
this.vecRotation = quatRotation.eulerAngles;
|
||||
this.scale = _snapshot.scale;
|
||||
}
|
||||
|
||||
public SyncData(Changed _dataChangedByte, Vector3 _position, Vector3 _vecRotation, Vector3 _scale)
|
||||
{
|
||||
this.changedDataByte = _dataChangedByte;
|
||||
this.position = _position;
|
||||
this.vecRotation = _vecRotation;
|
||||
this.quatRotation = Quaternion.Euler(vecRotation);
|
||||
this.scale = _scale;
|
||||
}
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum Changed : byte
|
||||
{
|
||||
None = 0,
|
||||
PosX = 1 << 0,
|
||||
PosY = 1 << 1,
|
||||
PosZ = 1 << 2,
|
||||
CompressRot = 1 << 3,
|
||||
RotX = 1 << 4,
|
||||
RotY = 1 << 5,
|
||||
RotZ = 1 << 6,
|
||||
Scale = 1 << 7,
|
||||
|
||||
Pos = PosX | PosY | PosZ,
|
||||
Rot = RotX | RotY | RotZ
|
||||
}
|
||||
|
||||
|
||||
public static class SyncDataReaderWriter
|
||||
{
|
||||
public static void WriteSyncData(this NetworkWriter writer, SyncData syncData)
|
||||
{
|
||||
writer.WriteByte((byte)syncData.changedDataByte);
|
||||
|
||||
// Write position
|
||||
if ((syncData.changedDataByte & Changed.PosX) > 0)
|
||||
{
|
||||
writer.WriteFloat(syncData.position.x);
|
||||
}
|
||||
|
||||
if ((syncData.changedDataByte & Changed.PosY) > 0)
|
||||
{
|
||||
writer.WriteFloat(syncData.position.y);
|
||||
}
|
||||
|
||||
if ((syncData.changedDataByte & Changed.PosZ) > 0)
|
||||
{
|
||||
writer.WriteFloat(syncData.position.z);
|
||||
}
|
||||
|
||||
// Write rotation
|
||||
if ((syncData.changedDataByte & Changed.CompressRot) > 0)
|
||||
{
|
||||
if((syncData.changedDataByte & Changed.Rot) > 0)
|
||||
{
|
||||
writer.WriteUInt(Compression.CompressQuaternion(syncData.quatRotation));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if ((syncData.changedDataByte & Changed.RotX) > 0)
|
||||
{
|
||||
writer.WriteFloat(syncData.quatRotation.eulerAngles.x);
|
||||
}
|
||||
|
||||
if ((syncData.changedDataByte & Changed.RotY) > 0)
|
||||
{
|
||||
writer.WriteFloat(syncData.quatRotation.eulerAngles.y);
|
||||
}
|
||||
|
||||
if ((syncData.changedDataByte & Changed.RotZ) > 0)
|
||||
{
|
||||
writer.WriteFloat(syncData.quatRotation.eulerAngles.z);
|
||||
}
|
||||
}
|
||||
|
||||
// Write scale
|
||||
if ((syncData.changedDataByte & Changed.Scale) > 0)
|
||||
{
|
||||
writer.WriteVector3(syncData.scale);
|
||||
}
|
||||
}
|
||||
|
||||
public static SyncData ReadSyncData(this NetworkReader reader)
|
||||
{
|
||||
Changed changedData = (Changed)reader.ReadByte();
|
||||
|
||||
// If we have nothing to read here, let's say because posX is unchanged, then we can write anything
|
||||
// for now, but in the NT, we will need to check changedData again, to put the right values of the axis
|
||||
// back. We don't have it here.
|
||||
|
||||
Vector3 position =
|
||||
new Vector3(
|
||||
(changedData & Changed.PosX) > 0 ? reader.ReadFloat() : 0,
|
||||
(changedData & Changed.PosY) > 0 ? reader.ReadFloat() : 0,
|
||||
(changedData & Changed.PosZ) > 0 ? reader.ReadFloat() : 0
|
||||
);
|
||||
|
||||
Vector3 vecRotation = new Vector3();
|
||||
Quaternion quatRotation = new Quaternion();
|
||||
|
||||
if ((changedData & Changed.CompressRot) > 0)
|
||||
{
|
||||
quatRotation = (changedData & Changed.RotX) > 0 ? Compression.DecompressQuaternion(reader.ReadUInt()) : new Quaternion();
|
||||
}
|
||||
else
|
||||
{
|
||||
vecRotation =
|
||||
new Vector3(
|
||||
(changedData & Changed.RotX) > 0 ? reader.ReadFloat() : 0,
|
||||
(changedData & Changed.RotY) > 0 ? reader.ReadFloat() : 0,
|
||||
(changedData & Changed.RotZ) > 0 ? reader.ReadFloat() : 0
|
||||
);
|
||||
}
|
||||
|
||||
Vector3 scale = (changedData & Changed.Scale) == Changed.Scale ? reader.ReadVector3() : new Vector3();
|
||||
|
||||
SyncData _syncData = (changedData & Changed.CompressRot) > 0 ? new SyncData(changedData, position, quatRotation, scale) : new SyncData(changedData, position, vecRotation, scale);
|
||||
|
||||
return _syncData;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a1c0832ca88e749ff96fe04cebb617ef
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -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: []
|
File diff suppressed because it is too large
Load Diff
@ -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:
|
||||
|
@ -0,0 +1,15 @@
|
||||
// Prediction moves out the Rigidbody & Collider into a separate object.
|
||||
// this component simply points back to the owner component.
|
||||
// in case Raycasts hit it and need to know the owner, etc.
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
public class PredictedRigidbodyPhysicsGhost : MonoBehaviour
|
||||
{
|
||||
// this is performance critical, so store target's .Transform instead of
|
||||
// PredictedRigidbody, this way we don't need to call the .transform getter.
|
||||
[Tooltip("The predicted rigidbody owner.")]
|
||||
public Transform target;
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
// removed 2024-02-09
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 62e7e9424c7e48d69b6a3517796142a1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
// this struct exists only for OnDe/Serialize performance.
|
||||
// instead of WriteVector3+Quaternion+Vector3+Vector3,
|
||||
// we read & write the whole struct as blittable once.
|
||||
//
|
||||
// struct packing can cause odd results with blittable on different platforms,
|
||||
// so this is usually not recommended!
|
||||
//
|
||||
// in this case however, we need to squeeze everything we can out of prediction
|
||||
// to support low even devices / VR.
|
||||
using System.Runtime.InteropServices;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
// struct packing
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)] // explicitly force sequential
|
||||
public struct PredictedSyncData
|
||||
{
|
||||
public float deltaTime; // 4 bytes (word aligned)
|
||||
public Vector3 position; // 12 bytes (word aligned)
|
||||
public Quaternion rotation; // 16 bytes (word aligned)
|
||||
public Vector3 velocity; // 12 bytes (word aligned)
|
||||
public Vector3 angularVelocity; // 12 bytes (word aligned)
|
||||
// DO NOT SYNC SLEEPING! this cuts benchmark performance in half(!!!)
|
||||
// public byte sleeping; // 1 byte: bool isn't blittable
|
||||
|
||||
// constructor for convenience
|
||||
public PredictedSyncData(float deltaTime, Vector3 position, Quaternion rotation, Vector3 velocity, Vector3 angularVelocity)//, bool sleeping)
|
||||
{
|
||||
this.deltaTime = deltaTime;
|
||||
this.position = position;
|
||||
this.rotation = rotation;
|
||||
this.velocity = velocity;
|
||||
this.angularVelocity = angularVelocity;
|
||||
// DO NOT SYNC SLEEPING! this cuts benchmark performance in half(!!!)
|
||||
// this.sleeping = sleeping ? (byte)1 : (byte)0;
|
||||
}
|
||||
}
|
||||
|
||||
// NetworkReader/Writer extensions to write this struct
|
||||
public static class PredictedSyncDataReadWrite
|
||||
{
|
||||
public static void WritePredictedSyncData(this NetworkWriter writer, PredictedSyncData data)
|
||||
{
|
||||
writer.WriteBlittable(data);
|
||||
}
|
||||
|
||||
public static PredictedSyncData ReadPredictedSyncData(this NetworkReader reader)
|
||||
{
|
||||
return reader.ReadBlittable<PredictedSyncData>();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f595f112a39e4634b670d56991b23823
|
||||
timeCreated: 1710387026
|
419
Assets/Mirror/Components/PredictedRigidbody/PredictionUtils.cs
Normal file
419
Assets/Mirror/Components/PredictedRigidbody/PredictionUtils.cs
Normal file
@ -0,0 +1,419 @@
|
||||
// standalone utility functions for PredictedRigidbody component.
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
public static class PredictionUtils
|
||||
{
|
||||
// rigidbody ///////////////////////////////////////////////////////////
|
||||
// move a Rigidbody + settings from one GameObject to another.
|
||||
public static void MoveRigidbody(GameObject source, GameObject destination)
|
||||
{
|
||||
// create a new Rigidbody component on destination.
|
||||
// note that adding a Joint automatically adds a Rigidbody.
|
||||
// so first check if one was added yet.
|
||||
Rigidbody original = source.GetComponent<Rigidbody>();
|
||||
if (original == null) throw new Exception($"Prediction: attempted to move {source}'s Rigidbody to the predicted copy, but there was no component.");
|
||||
Rigidbody rigidbodyCopy;
|
||||
if (!destination.TryGetComponent(out rigidbodyCopy))
|
||||
rigidbodyCopy = destination.AddComponent<Rigidbody>();
|
||||
|
||||
// copy all properties
|
||||
rigidbodyCopy.mass = original.mass;
|
||||
rigidbodyCopy.drag = original.drag;
|
||||
rigidbodyCopy.angularDrag = original.angularDrag;
|
||||
rigidbodyCopy.useGravity = original.useGravity;
|
||||
rigidbodyCopy.isKinematic = original.isKinematic;
|
||||
rigidbodyCopy.interpolation = original.interpolation;
|
||||
rigidbodyCopy.collisionDetectionMode = original.collisionDetectionMode;
|
||||
rigidbodyCopy.constraints = original.constraints;
|
||||
rigidbodyCopy.sleepThreshold = original.sleepThreshold;
|
||||
rigidbodyCopy.freezeRotation = original.freezeRotation;
|
||||
|
||||
// moving (Configurable)Joints messes up their range of motion unless
|
||||
// we reset to initial position first (we do this in PredictedRigibody.cs).
|
||||
// so here we don't set the Rigidbody's physics position at all.
|
||||
// rigidbodyCopy.position = original.position;
|
||||
// rigidbodyCopy.rotation = original.rotation;
|
||||
|
||||
// projects may keep Rigidbodies as kinematic sometimes. in that case, setting velocity would log an error
|
||||
if (!original.isKinematic)
|
||||
{
|
||||
rigidbodyCopy.velocity = original.velocity;
|
||||
rigidbodyCopy.angularVelocity = original.angularVelocity;
|
||||
}
|
||||
|
||||
// destroy original
|
||||
GameObject.Destroy(original);
|
||||
}
|
||||
|
||||
// helper function: if a collider is on a child, copy that child first.
|
||||
// this way child's relative position/rotation/scale are preserved.
|
||||
public static GameObject CopyRelativeTransform(GameObject source, Transform sourceChild, GameObject destination)
|
||||
{
|
||||
// is this on the source root? then we want to put it on the destination root.
|
||||
if (sourceChild == source.transform) return destination;
|
||||
|
||||
// is this on a child? then create the same child with the same transform on destination.
|
||||
// note this is technically only correct for the immediate child since
|
||||
// .localPosition is relative to parent, but this is good enough.
|
||||
GameObject child = new GameObject(sourceChild.name);
|
||||
child.transform.SetParent(destination.transform, true);
|
||||
child.transform.localPosition = sourceChild.localPosition;
|
||||
child.transform.localRotation = sourceChild.localRotation;
|
||||
child.transform.localScale = sourceChild.localScale;
|
||||
|
||||
// assign the same Layer for the physics copy.
|
||||
// games may use a custom physics collision matrix, layer matters.
|
||||
child.layer = sourceChild.gameObject.layer;
|
||||
|
||||
return child;
|
||||
}
|
||||
|
||||
// colliders ///////////////////////////////////////////////////////////
|
||||
// move all BoxColliders + settings from one GameObject to another.
|
||||
public static void MoveBoxColliders(GameObject source, GameObject destination)
|
||||
{
|
||||
// colliders may be on children
|
||||
BoxCollider[] sourceColliders = source.GetComponentsInChildren<BoxCollider>();
|
||||
foreach (BoxCollider sourceCollider in sourceColliders)
|
||||
{
|
||||
// copy the relative transform:
|
||||
// if collider is on root, it returns destination root.
|
||||
// if collider is on a child, it creates and returns a child on destination.
|
||||
GameObject target = CopyRelativeTransform(source, sourceCollider.transform, destination);
|
||||
BoxCollider colliderCopy = target.AddComponent<BoxCollider>();
|
||||
colliderCopy.center = sourceCollider.center;
|
||||
colliderCopy.size = sourceCollider.size;
|
||||
colliderCopy.isTrigger = sourceCollider.isTrigger;
|
||||
colliderCopy.material = sourceCollider.material;
|
||||
GameObject.Destroy(sourceCollider);
|
||||
}
|
||||
}
|
||||
|
||||
// move all SphereColliders + settings from one GameObject to another.
|
||||
public static void MoveSphereColliders(GameObject source, GameObject destination)
|
||||
{
|
||||
// colliders may be on children
|
||||
SphereCollider[] sourceColliders = source.GetComponentsInChildren<SphereCollider>();
|
||||
foreach (SphereCollider sourceCollider in sourceColliders)
|
||||
{
|
||||
// copy the relative transform:
|
||||
// if collider is on root, it returns destination root.
|
||||
// if collider is on a child, it creates and returns a child on destination.
|
||||
GameObject target = CopyRelativeTransform(source, sourceCollider.transform, destination);
|
||||
SphereCollider colliderCopy = target.AddComponent<SphereCollider>();
|
||||
colliderCopy.center = sourceCollider.center;
|
||||
colliderCopy.radius = sourceCollider.radius;
|
||||
colliderCopy.isTrigger = sourceCollider.isTrigger;
|
||||
colliderCopy.material = sourceCollider.material;
|
||||
GameObject.Destroy(sourceCollider);
|
||||
}
|
||||
}
|
||||
|
||||
// move all CapsuleColliders + settings from one GameObject to another.
|
||||
public static void MoveCapsuleColliders(GameObject source, GameObject destination)
|
||||
{
|
||||
// colliders may be on children
|
||||
CapsuleCollider[] sourceColliders = source.GetComponentsInChildren<CapsuleCollider>();
|
||||
foreach (CapsuleCollider sourceCollider in sourceColliders)
|
||||
{
|
||||
// copy the relative transform:
|
||||
// if collider is on root, it returns destination root.
|
||||
// if collider is on a child, it creates and returns a child on destination.
|
||||
GameObject target = CopyRelativeTransform(source, sourceCollider.transform, destination);
|
||||
CapsuleCollider colliderCopy = target.AddComponent<CapsuleCollider>();
|
||||
colliderCopy.center = sourceCollider.center;
|
||||
colliderCopy.radius = sourceCollider.radius;
|
||||
colliderCopy.height = sourceCollider.height;
|
||||
colliderCopy.direction = sourceCollider.direction;
|
||||
colliderCopy.isTrigger = sourceCollider.isTrigger;
|
||||
colliderCopy.material = sourceCollider.material;
|
||||
GameObject.Destroy(sourceCollider);
|
||||
}
|
||||
}
|
||||
|
||||
// move all MeshColliders + settings from one GameObject to another.
|
||||
public static void MoveMeshColliders(GameObject source, GameObject destination)
|
||||
{
|
||||
// colliders may be on children
|
||||
MeshCollider[] sourceColliders = source.GetComponentsInChildren<MeshCollider>();
|
||||
foreach (MeshCollider sourceCollider in sourceColliders)
|
||||
{
|
||||
// when Models have Mesh->Read/Write disabled, it means that Unity
|
||||
// uploads the mesh directly to the GPU and erases it on the CPU.
|
||||
// on some platforms this makes moving a MeshCollider in builds impossible:
|
||||
//
|
||||
// "CollisionMeshData couldn't be created because the mesh has been marked as non-accessible."
|
||||
//
|
||||
// on other platforms, this works fine.
|
||||
// let's show an explicit log message so in case collisions don't
|
||||
// work at runtime, it's obvious why it happens and how to fix it.
|
||||
if (!sourceCollider.sharedMesh.isReadable)
|
||||
{
|
||||
Debug.Log($"[Prediction]: MeshCollider on {sourceCollider.name} isn't readable, which may indicate that the Mesh only exists on the GPU. If {sourceCollider.name} is missing collisions, then please select the model in the Project Area, and enable Mesh->Read/Write so it's also available on the CPU!");
|
||||
// don't early return. keep trying, it may work.
|
||||
}
|
||||
|
||||
// copy the relative transform:
|
||||
// if collider is on root, it returns destination root.
|
||||
// if collider is on a child, it creates and returns a child on destination.
|
||||
GameObject target = CopyRelativeTransform(source, sourceCollider.transform, destination);
|
||||
MeshCollider colliderCopy = target.AddComponent<MeshCollider>();
|
||||
colliderCopy.sharedMesh = sourceCollider.sharedMesh;
|
||||
colliderCopy.convex = sourceCollider.convex;
|
||||
colliderCopy.isTrigger = sourceCollider.isTrigger;
|
||||
colliderCopy.material = sourceCollider.material;
|
||||
GameObject.Destroy(sourceCollider);
|
||||
}
|
||||
}
|
||||
|
||||
// move all Colliders + settings from one GameObject to another.
|
||||
public static void MoveAllColliders(GameObject source, GameObject destination)
|
||||
{
|
||||
MoveBoxColliders(source, destination);
|
||||
MoveSphereColliders(source, destination);
|
||||
MoveCapsuleColliders(source, destination);
|
||||
MoveMeshColliders(source, destination);
|
||||
}
|
||||
|
||||
// joints //////////////////////////////////////////////////////////////
|
||||
// move all CharacterJoints + settings from one GameObject to another.
|
||||
public static void MoveCharacterJoints(GameObject source, GameObject destination)
|
||||
{
|
||||
// colliders may be on children
|
||||
CharacterJoint[] sourceJoints = source.GetComponentsInChildren<CharacterJoint>();
|
||||
foreach (CharacterJoint sourceJoint in sourceJoints)
|
||||
{
|
||||
// copy the relative transform:
|
||||
// if joint is on root, it returns destination root.
|
||||
// if joint is on a child, it creates and returns a child on destination.
|
||||
GameObject target = CopyRelativeTransform(source, sourceJoint.transform, destination);
|
||||
CharacterJoint jointCopy = target.AddComponent<CharacterJoint>();
|
||||
// apply settings, in alphabetical order
|
||||
jointCopy.anchor = sourceJoint.anchor;
|
||||
jointCopy.autoConfigureConnectedAnchor = sourceJoint.autoConfigureConnectedAnchor;
|
||||
jointCopy.axis = sourceJoint.axis;
|
||||
jointCopy.breakForce = sourceJoint.breakForce;
|
||||
jointCopy.breakTorque = sourceJoint.breakTorque;
|
||||
jointCopy.connectedAnchor = sourceJoint.connectedAnchor;
|
||||
jointCopy.connectedBody = sourceJoint.connectedBody;
|
||||
jointCopy.connectedMassScale = sourceJoint.connectedMassScale;
|
||||
jointCopy.enableCollision = sourceJoint.enableCollision;
|
||||
jointCopy.enablePreprocessing = sourceJoint.enablePreprocessing;
|
||||
jointCopy.enableProjection = sourceJoint.enableProjection;
|
||||
jointCopy.highTwistLimit = sourceJoint.highTwistLimit;
|
||||
jointCopy.lowTwistLimit = sourceJoint.lowTwistLimit;
|
||||
jointCopy.massScale = sourceJoint.massScale;
|
||||
jointCopy.projectionAngle = sourceJoint.projectionAngle;
|
||||
jointCopy.projectionDistance = sourceJoint.projectionDistance;
|
||||
jointCopy.swing1Limit = sourceJoint.swing1Limit;
|
||||
jointCopy.swing2Limit = sourceJoint.swing2Limit;
|
||||
jointCopy.swingAxis = sourceJoint.swingAxis;
|
||||
jointCopy.swingLimitSpring = sourceJoint.swingLimitSpring;
|
||||
jointCopy.twistLimitSpring = sourceJoint.twistLimitSpring;
|
||||
#if UNITY_2020_3_OR_NEWER
|
||||
jointCopy.connectedArticulationBody = sourceJoint.connectedArticulationBody;
|
||||
#endif
|
||||
|
||||
GameObject.Destroy(sourceJoint);
|
||||
}
|
||||
}
|
||||
|
||||
// move all ConfigurableJoints + settings from one GameObject to another.
|
||||
public static void MoveConfigurableJoints(GameObject source, GameObject destination)
|
||||
{
|
||||
// colliders may be on children
|
||||
ConfigurableJoint[] sourceJoints = source.GetComponentsInChildren<ConfigurableJoint>();
|
||||
foreach (ConfigurableJoint sourceJoint in sourceJoints)
|
||||
{
|
||||
// copy the relative transform:
|
||||
// if joint is on root, it returns destination root.
|
||||
// if joint is on a child, it creates and returns a child on destination.
|
||||
GameObject target = CopyRelativeTransform(source, sourceJoint.transform, destination);
|
||||
ConfigurableJoint jointCopy = target.AddComponent<ConfigurableJoint>();
|
||||
// apply settings, in alphabetical order
|
||||
jointCopy.anchor = sourceJoint.anchor;
|
||||
jointCopy.angularXLimitSpring = sourceJoint.angularXLimitSpring;
|
||||
jointCopy.angularXDrive = sourceJoint.angularXDrive;
|
||||
jointCopy.angularXMotion = sourceJoint.angularXMotion;
|
||||
jointCopy.angularYLimit = sourceJoint.angularYLimit;
|
||||
jointCopy.angularYMotion = sourceJoint.angularYMotion;
|
||||
jointCopy.angularYZDrive = sourceJoint.angularYZDrive;
|
||||
jointCopy.angularYZLimitSpring = sourceJoint.angularYZLimitSpring;
|
||||
jointCopy.angularZLimit = sourceJoint.angularZLimit;
|
||||
jointCopy.angularZMotion = sourceJoint.angularZMotion;
|
||||
jointCopy.autoConfigureConnectedAnchor = sourceJoint.autoConfigureConnectedAnchor;
|
||||
jointCopy.axis = sourceJoint.axis;
|
||||
jointCopy.breakForce = sourceJoint.breakForce;
|
||||
jointCopy.breakTorque = sourceJoint.breakTorque;
|
||||
jointCopy.configuredInWorldSpace = sourceJoint.configuredInWorldSpace;
|
||||
jointCopy.connectedAnchor = sourceJoint.connectedAnchor;
|
||||
jointCopy.connectedBody = sourceJoint.connectedBody;
|
||||
jointCopy.connectedMassScale = sourceJoint.connectedMassScale;
|
||||
jointCopy.enableCollision = sourceJoint.enableCollision;
|
||||
jointCopy.enablePreprocessing = sourceJoint.enablePreprocessing;
|
||||
jointCopy.highAngularXLimit = sourceJoint.highAngularXLimit; // moving this only works if the object is at initial position/rotation/scale, see PredictedRigidbody.cs
|
||||
jointCopy.linearLimitSpring = sourceJoint.linearLimitSpring;
|
||||
jointCopy.linearLimit = sourceJoint.linearLimit;
|
||||
jointCopy.lowAngularXLimit = sourceJoint.lowAngularXLimit; // moving this only works if the object is at initial position/rotation/scale, see PredictedRigidbody.cs
|
||||
jointCopy.massScale = sourceJoint.massScale;
|
||||
jointCopy.projectionAngle = sourceJoint.projectionAngle;
|
||||
jointCopy.projectionDistance = sourceJoint.projectionDistance;
|
||||
jointCopy.projectionMode = sourceJoint.projectionMode;
|
||||
jointCopy.rotationDriveMode = sourceJoint.rotationDriveMode;
|
||||
jointCopy.secondaryAxis = sourceJoint.secondaryAxis;
|
||||
jointCopy.slerpDrive = sourceJoint.slerpDrive;
|
||||
jointCopy.swapBodies = sourceJoint.swapBodies;
|
||||
jointCopy.targetAngularVelocity = sourceJoint.targetAngularVelocity;
|
||||
jointCopy.targetPosition = sourceJoint.targetPosition;
|
||||
jointCopy.targetRotation = sourceJoint.targetRotation;
|
||||
jointCopy.targetVelocity = sourceJoint.targetVelocity;
|
||||
jointCopy.xDrive = sourceJoint.xDrive;
|
||||
jointCopy.xMotion = sourceJoint.xMotion;
|
||||
jointCopy.yDrive = sourceJoint.yDrive;
|
||||
jointCopy.yMotion = sourceJoint.yMotion;
|
||||
jointCopy.zDrive = sourceJoint.zDrive;
|
||||
jointCopy.zMotion = sourceJoint.zMotion;
|
||||
#if UNITY_2020_3_OR_NEWER
|
||||
jointCopy.connectedArticulationBody = sourceJoint.connectedArticulationBody;
|
||||
#endif
|
||||
|
||||
GameObject.Destroy(sourceJoint);
|
||||
}
|
||||
}
|
||||
|
||||
// move all FixedJoints + settings from one GameObject to another.
|
||||
public static void MoveFixedJoints(GameObject source, GameObject destination)
|
||||
{
|
||||
// colliders may be on children
|
||||
FixedJoint[] sourceJoints = source.GetComponentsInChildren<FixedJoint>();
|
||||
foreach (FixedJoint sourceJoint in sourceJoints)
|
||||
{
|
||||
// copy the relative transform:
|
||||
// if joint is on root, it returns destination root.
|
||||
// if joint is on a child, it creates and returns a child on destination.
|
||||
GameObject target = CopyRelativeTransform(source, sourceJoint.transform, destination);
|
||||
FixedJoint jointCopy = target.AddComponent<FixedJoint>();
|
||||
// apply settings, in alphabetical order
|
||||
jointCopy.anchor = sourceJoint.anchor;
|
||||
jointCopy.autoConfigureConnectedAnchor = sourceJoint.autoConfigureConnectedAnchor;
|
||||
jointCopy.axis = sourceJoint.axis;
|
||||
jointCopy.breakForce = sourceJoint.breakForce;
|
||||
jointCopy.breakTorque = sourceJoint.breakTorque;
|
||||
jointCopy.connectedAnchor = sourceJoint.connectedAnchor;
|
||||
jointCopy.connectedBody = sourceJoint.connectedBody;
|
||||
jointCopy.connectedMassScale = sourceJoint.connectedMassScale;
|
||||
jointCopy.enableCollision = sourceJoint.enableCollision;
|
||||
jointCopy.enablePreprocessing = sourceJoint.enablePreprocessing;
|
||||
jointCopy.massScale = sourceJoint.massScale;
|
||||
#if UNITY_2020_3_OR_NEWER
|
||||
jointCopy.connectedArticulationBody = sourceJoint.connectedArticulationBody;
|
||||
#endif
|
||||
|
||||
GameObject.Destroy(sourceJoint);
|
||||
}
|
||||
}
|
||||
|
||||
// move all HingeJoints + settings from one GameObject to another.
|
||||
public static void MoveHingeJoints(GameObject source, GameObject destination)
|
||||
{
|
||||
// colliders may be on children
|
||||
HingeJoint[] sourceJoints = source.GetComponentsInChildren<HingeJoint>();
|
||||
foreach (HingeJoint sourceJoint in sourceJoints)
|
||||
{
|
||||
// copy the relative transform:
|
||||
// if joint is on root, it returns destination root.
|
||||
// if joint is on a child, it creates and returns a child on destination.
|
||||
GameObject target = CopyRelativeTransform(source, sourceJoint.transform, destination);
|
||||
HingeJoint jointCopy = target.AddComponent<HingeJoint>();
|
||||
// apply settings, in alphabetical order
|
||||
jointCopy.anchor = sourceJoint.anchor;
|
||||
jointCopy.autoConfigureConnectedAnchor = sourceJoint.autoConfigureConnectedAnchor;
|
||||
jointCopy.axis = sourceJoint.axis;
|
||||
jointCopy.breakForce = sourceJoint.breakForce;
|
||||
jointCopy.breakTorque = sourceJoint.breakTorque;
|
||||
jointCopy.connectedAnchor = sourceJoint.connectedAnchor;
|
||||
jointCopy.connectedBody = sourceJoint.connectedBody;
|
||||
jointCopy.connectedMassScale = sourceJoint.connectedMassScale;
|
||||
jointCopy.enableCollision = sourceJoint.enableCollision;
|
||||
jointCopy.enablePreprocessing = sourceJoint.enablePreprocessing;
|
||||
jointCopy.limits = sourceJoint.limits;
|
||||
jointCopy.massScale = sourceJoint.massScale;
|
||||
jointCopy.motor = sourceJoint.motor;
|
||||
jointCopy.spring = sourceJoint.spring;
|
||||
jointCopy.useLimits = sourceJoint.useLimits;
|
||||
jointCopy.useMotor = sourceJoint.useMotor;
|
||||
jointCopy.useSpring = sourceJoint.useSpring;
|
||||
#if UNITY_2020_3_OR_NEWER
|
||||
jointCopy.connectedArticulationBody = sourceJoint.connectedArticulationBody;
|
||||
#endif
|
||||
#if UNITY_2022_3_OR_NEWER
|
||||
jointCopy.extendedLimits = sourceJoint.extendedLimits;
|
||||
jointCopy.useAcceleration = sourceJoint.useAcceleration;
|
||||
#endif
|
||||
|
||||
GameObject.Destroy(sourceJoint);
|
||||
}
|
||||
}
|
||||
|
||||
// move all SpringJoints + settings from one GameObject to another.
|
||||
public static void MoveSpringJoints(GameObject source, GameObject destination)
|
||||
{
|
||||
// colliders may be on children
|
||||
SpringJoint[] sourceJoints = source.GetComponentsInChildren<SpringJoint>();
|
||||
foreach (SpringJoint sourceJoint in sourceJoints)
|
||||
{
|
||||
// copy the relative transform:
|
||||
// if joint is on root, it returns destination root.
|
||||
// if joint is on a child, it creates and returns a child on destination.
|
||||
GameObject target = CopyRelativeTransform(source, sourceJoint.transform, destination);
|
||||
SpringJoint jointCopy = target.AddComponent<SpringJoint>();
|
||||
// apply settings, in alphabetical order
|
||||
jointCopy.anchor = sourceJoint.anchor;
|
||||
jointCopy.autoConfigureConnectedAnchor = sourceJoint.autoConfigureConnectedAnchor;
|
||||
jointCopy.axis = sourceJoint.axis;
|
||||
jointCopy.breakForce = sourceJoint.breakForce;
|
||||
jointCopy.breakTorque = sourceJoint.breakTorque;
|
||||
jointCopy.connectedAnchor = sourceJoint.connectedAnchor;
|
||||
jointCopy.connectedBody = sourceJoint.connectedBody;
|
||||
jointCopy.connectedMassScale = sourceJoint.connectedMassScale;
|
||||
jointCopy.damper = sourceJoint.damper;
|
||||
jointCopy.enableCollision = sourceJoint.enableCollision;
|
||||
jointCopy.enablePreprocessing = sourceJoint.enablePreprocessing;
|
||||
jointCopy.massScale = sourceJoint.massScale;
|
||||
jointCopy.maxDistance = sourceJoint.maxDistance;
|
||||
jointCopy.minDistance = sourceJoint.minDistance;
|
||||
jointCopy.spring = sourceJoint.spring;
|
||||
jointCopy.tolerance = sourceJoint.tolerance;
|
||||
#if UNITY_2020_3_OR_NEWER
|
||||
jointCopy.connectedArticulationBody = sourceJoint.connectedArticulationBody;
|
||||
#endif
|
||||
|
||||
GameObject.Destroy(sourceJoint);
|
||||
}
|
||||
}
|
||||
|
||||
// move all Joints + settings from one GameObject to another.
|
||||
public static void MoveAllJoints(GameObject source, GameObject destination)
|
||||
{
|
||||
MoveCharacterJoints(source, destination);
|
||||
MoveConfigurableJoints(source, destination);
|
||||
MoveFixedJoints(source, destination);
|
||||
MoveHingeJoints(source, destination);
|
||||
MoveSpringJoints(source, destination);
|
||||
}
|
||||
|
||||
// all /////////////////////////////////////////////////////////////////
|
||||
// move all physics components from one GameObject to another.
|
||||
public static void MovePhysicsComponents(GameObject source, GameObject destination)
|
||||
{
|
||||
// need to move joints first, otherwise we might see:
|
||||
// 'can't move Rigidbody because a Joint depends on it'
|
||||
MoveAllJoints(source, destination);
|
||||
MoveAllColliders(source, destination);
|
||||
MoveRigidbody(source, destination);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 17cfe1beb3f94a69b94bf60afc37ef7a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -0,0 +1,85 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!21 &2100000
|
||||
Material:
|
||||
serializedVersion: 8
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: RemoteGhostMaterial
|
||||
m_Shader: {fileID: 46, guid: 0000000000000000f000000000000000, type: 0}
|
||||
m_Parent: {fileID: 0}
|
||||
m_ModifiedSerializedProperties: 0
|
||||
m_ValidKeywords:
|
||||
- _ALPHAPREMULTIPLY_ON
|
||||
m_InvalidKeywords: []
|
||||
m_LightmapFlags: 4
|
||||
m_EnableInstancingVariants: 0
|
||||
m_DoubleSidedGI: 0
|
||||
m_CustomRenderQueue: 3000
|
||||
stringTagMap:
|
||||
RenderType: Transparent
|
||||
disabledShaderPasses: []
|
||||
m_LockedProperties:
|
||||
m_SavedProperties:
|
||||
serializedVersion: 3
|
||||
m_TexEnvs:
|
||||
- _BumpMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailAlbedoMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailMask:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailNormalMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _EmissionMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _MainTex:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _MetallicGlossMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _OcclusionMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _ParallaxMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
m_Ints: []
|
||||
m_Floats:
|
||||
- _BumpScale: 1
|
||||
- _Cutoff: 0.5
|
||||
- _DetailNormalMapScale: 1
|
||||
- _DstBlend: 10
|
||||
- _GlossMapScale: 1
|
||||
- _Glossiness: 0.92
|
||||
- _GlossyReflections: 1
|
||||
- _Metallic: 0
|
||||
- _Mode: 3
|
||||
- _OcclusionStrength: 1
|
||||
- _Parallax: 0.02
|
||||
- _SmoothnessTextureChannel: 0
|
||||
- _SpecularHighlights: 1
|
||||
- _SrcBlend: 1
|
||||
- _UVSec: 0
|
||||
- _ZWrite: 0
|
||||
m_Colors:
|
||||
- _Color: {r: 0.09849727, g: 1, b: 0, a: 0.15686275}
|
||||
- _EmissionColor: {r: 0, g: 0, b: 0, a: 1}
|
||||
m_BuildTextureStacks: []
|
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 04f0b2088c857414393bab3b80356776
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 2100000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -0,0 +1,60 @@
|
||||
// PredictedRigidbody stores a history of its rigidbody states.
|
||||
using System.Runtime.CompilerServices;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
// inline everything because this is performance critical!
|
||||
public struct RigidbodyState : PredictedState
|
||||
{
|
||||
public double timestamp { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; [MethodImpl(MethodImplOptions.AggressiveInlining)] private set; }
|
||||
|
||||
// we want to store position delta (last + delta = current), and current.
|
||||
// this way we can apply deltas on top of corrected positions to get the corrected final position.
|
||||
public Vector3 positionDelta { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; [MethodImpl(MethodImplOptions.AggressiveInlining)] set; } // delta to get from last to this position
|
||||
public Vector3 position { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; [MethodImpl(MethodImplOptions.AggressiveInlining)] set; }
|
||||
|
||||
public Quaternion rotationDelta { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; [MethodImpl(MethodImplOptions.AggressiveInlining)] set; } // delta to get from last to this rotation
|
||||
public Quaternion rotation { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; [MethodImpl(MethodImplOptions.AggressiveInlining)] set; }
|
||||
|
||||
public Vector3 velocityDelta { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; [MethodImpl(MethodImplOptions.AggressiveInlining)] set; } // delta to get from last to this velocity
|
||||
public Vector3 velocity { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; [MethodImpl(MethodImplOptions.AggressiveInlining)] set; }
|
||||
|
||||
public Vector3 angularVelocityDelta { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; [MethodImpl(MethodImplOptions.AggressiveInlining)] set; } // delta to get from last to this velocity
|
||||
public Vector3 angularVelocity { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; [MethodImpl(MethodImplOptions.AggressiveInlining)] set; }
|
||||
|
||||
public RigidbodyState(
|
||||
double timestamp,
|
||||
Vector3 positionDelta,
|
||||
Vector3 position,
|
||||
Quaternion rotationDelta,
|
||||
Quaternion rotation,
|
||||
Vector3 velocityDelta,
|
||||
Vector3 velocity,
|
||||
Vector3 angularVelocityDelta,
|
||||
Vector3 angularVelocity)
|
||||
{
|
||||
this.timestamp = timestamp;
|
||||
this.positionDelta = positionDelta;
|
||||
this.position = position;
|
||||
this.rotationDelta = rotationDelta;
|
||||
this.rotation = rotation;
|
||||
this.velocityDelta = velocityDelta;
|
||||
this.velocity = velocity;
|
||||
this.angularVelocityDelta = angularVelocityDelta;
|
||||
this.angularVelocity = angularVelocity;
|
||||
}
|
||||
|
||||
public static RigidbodyState Interpolate(RigidbodyState a, RigidbodyState b, float t)
|
||||
{
|
||||
return new RigidbodyState
|
||||
{
|
||||
position = Vector3.Lerp(a.position, b.position, t),
|
||||
// Quaternions always need to be normalized in order to be a valid rotation after operations
|
||||
rotation = Quaternion.Slerp(a.rotation, b.rotation, t).normalized,
|
||||
velocity = Vector3.Lerp(a.velocity, b.velocity, t),
|
||||
angularVelocity = Vector3.Lerp(a.angularVelocity, b.angularVelocity, t)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ed0e1c0c874c4c9db6be2d5885bb7bee
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -91,7 +91,7 @@ public class RemoteStatistics : NetworkBehaviour
|
||||
|
||||
[Header("GUI")]
|
||||
public bool showGui;
|
||||
public KeyCode hotKey = KeyCode.F11;
|
||||
public KeyCode hotKey = KeyCode.BackQuote;
|
||||
Rect windowRect = new Rect(0, 0, 400, 400);
|
||||
|
||||
// password can't be stored in code or in Unity project.
|
||||
|
@ -10,3 +10,4 @@
|
||||
[assembly: InternalsVisibleTo("Mirror.Tests.Performance.Editor")]
|
||||
[assembly: InternalsVisibleTo("Mirror.Tests.Performance.Runtime")]
|
||||
[assembly: InternalsVisibleTo("Mirror.Editor")]
|
||||
[assembly: InternalsVisibleTo("Mirror.Components")]
|
||||
|
@ -4,8 +4,12 @@
|
||||
namespace Mirror
|
||||
{
|
||||
/// <summary>
|
||||
/// SyncVars are used to synchronize a variable from the server to all clients automatically.
|
||||
/// <para>Value must be changed on server, not directly by clients. Hook parameter allows you to define a client-side method to be invoked when the client gets an update from the server.</para>
|
||||
/// SyncVars are used to automatically synchronize a variable between the server and all clients. The direction of synchronization depends on the Sync Direction property, ServerToClient by default.
|
||||
/// <para>
|
||||
/// When Sync Direction is equal to ServerToClient, the value should be changed on the server side and synchronized to all clients.
|
||||
/// Otherwise, the value should be changed on the client side and synchronized to server and other clients.
|
||||
/// </para>
|
||||
/// <para>Hook parameter allows you to define a method to be invoked when gets an value update. Notice that the hook method will not be called on the change side.</para>
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Field)]
|
||||
public class SyncVarAttribute : PropertyAttribute
|
||||
@ -82,4 +86,10 @@ public class SceneAttribute : PropertyAttribute {}
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Field)]
|
||||
public class ShowInInspectorAttribute : Attribute {}
|
||||
|
||||
/// <summary>
|
||||
/// Used to make a field readonly in the inspector
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Field)]
|
||||
public class ReadOnlyAttribute : PropertyAttribute {}
|
||||
}
|
||||
|
@ -54,7 +54,7 @@ public override void Rebuild(NetworkIdentity identity, bool initialize)
|
||||
newObservers.Clear();
|
||||
|
||||
// not force hidden?
|
||||
if (identity.visible != Visibility.ForceHidden)
|
||||
if (identity.visibility != Visibility.ForceHidden)
|
||||
{
|
||||
OnRebuildObservers(identity, newObservers);
|
||||
}
|
||||
|
@ -9,28 +9,21 @@ namespace Mirror
|
||||
[HelpURL("https://mirror-networking.gitbook.io/docs/guides/interest-management")]
|
||||
public abstract class InterestManagementBase : MonoBehaviour
|
||||
{
|
||||
// Configures InterestManagementBase in NetworkServer/Client
|
||||
// Do NOT check for active server or client here.
|
||||
// OnEnable must always set the static aoi references.
|
||||
// make sure to call base.OnEnable when overwriting!
|
||||
// Previously used Awake()
|
||||
// initialize NetworkServer/Client .aoi.
|
||||
// previously we did this in Awake(), but that's called for disabled
|
||||
// components too. if we do it OnEnable(), then it's not set for
|
||||
// disabled components.
|
||||
protected virtual void OnEnable()
|
||||
{
|
||||
if (NetworkServer.aoi == null)
|
||||
{
|
||||
// do not check if == null or error if already set.
|
||||
// users may enabled/disable components randomly,
|
||||
// causing this to be called multiple times.
|
||||
NetworkServer.aoi = this;
|
||||
}
|
||||
else Debug.LogError($"Only one InterestManagement component allowed. {NetworkServer.aoi.GetType()} has been set up already.");
|
||||
|
||||
if (NetworkClient.aoi == null)
|
||||
{
|
||||
NetworkClient.aoi = this;
|
||||
}
|
||||
else Debug.LogError($"Only one InterestManagement component allowed. {NetworkClient.aoi.GetType()} has been set up already.");
|
||||
}
|
||||
|
||||
[ServerCallback]
|
||||
public virtual void Reset() {}
|
||||
public virtual void ResetState() {}
|
||||
|
||||
// Callback used by the visibility system to determine if an observer
|
||||
// (player) can see the NetworkIdentity. If this function returns true,
|
||||
|
@ -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
|
||||
|
@ -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.");
|
||||
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.");
|
||||
if (exceptionsDisconnect)
|
||||
{
|
||||
Debug.LogError($"NetworkClient: failed to unpack and invoke message. Disconnecting.");
|
||||
connection.Disconnect();
|
||||
}
|
||||
else
|
||||
Debug.LogWarning($"NetworkClient: failed to unpack and invoke message.");
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
// otherwise disconnect
|
||||
else
|
||||
{
|
||||
// WARNING, not error. can happen if attacker sends random data.
|
||||
Debug.LogWarning($"NetworkClient: received Message was too short (messages should start with message id)");
|
||||
if (exceptionsDisconnect)
|
||||
{
|
||||
Debug.LogError($"NetworkClient: received Message was too short (messages should start with message id). Disconnecting.");
|
||||
connection.Disconnect();
|
||||
}
|
||||
else
|
||||
Debug.LogWarning("NetworkClient: received Message was too short (messages should start with message id)");
|
||||
return;
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
|
@ -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,9 +371,14 @@ 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.");
|
||||
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}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
// An attacker may attempt to modify another connection's entity
|
||||
@ -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)");
|
||||
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}.");
|
||||
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}");
|
||||
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);
|
||||
}
|
||||
|
@ -141,6 +141,11 @@ internal static void UpdateClient()
|
||||
{
|
||||
// localTime (double) instead of Time.time for accuracy over days
|
||||
if (localTime >= lastPingTime + PingInterval)
|
||||
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.
|
||||
@ -152,7 +157,6 @@ internal static void UpdateClient()
|
||||
NetworkClient.Send(pingMessage, Channels.Unreliable);
|
||||
lastPingTime = localTime;
|
||||
}
|
||||
}
|
||||
|
||||
// client rtt calculation //////////////////////////////////////////////
|
||||
// executed at the server when we receive a ping message
|
||||
|
@ -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 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
|
||||
}
|
||||
|
||||
// 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
|
||||
stateHistory[corrected.timestamp] = corrected;
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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();
|
||||
|
@ -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() { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,13 +297,9 @@ 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();
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
14
Assets/Mirror/Editor/LagCompensatorInspector.cs
Normal file
14
Assets/Mirror/Editor/LagCompensatorInspector.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
11
Assets/Mirror/Editor/LagCompensatorInspector.cs.meta
Normal file
11
Assets/Mirror/Editor/LagCompensatorInspector.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 703e39b5385ae2e479987ff4ec0707a1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -3,6 +3,7 @@
|
||||
"rootNamespace": "",
|
||||
"references": [
|
||||
"GUID:30817c1a0e6d646d99c048fc403f5979",
|
||||
"GUID:72872094b21c16e48b631b2224833d49",
|
||||
"GUID:1d0b9d21c3ff546a4aa32399dfd33474"
|
||||
],
|
||||
"includePlatforms": [
|
||||
|
@ -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)
|
||||
{
|
||||
|
19
Assets/Mirror/Editor/ReadOnlyDrawer.cs
Normal file
19
Assets/Mirror/Editor/ReadOnlyDrawer.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
11
Assets/Mirror/Editor/ReadOnlyDrawer.cs.meta
Normal file
11
Assets/Mirror/Editor/ReadOnlyDrawer.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 22f17bdd21f104c41bc175937fefbdec
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -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!
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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)));
|
||||
|
@ -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)
|
||||
|
@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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 |
|
||||
|
@ -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)
|
||||
|
@ -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";
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
@ -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")]
|
||||
|
||||
|
@ -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()
|
||||
{
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
|
@ -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()
|
||||
{
|
||||
|
@ -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>
|
||||
|
@ -366,7 +366,7 @@ MonoBehaviour:
|
||||
deliveryTimeEmaDuration: 2
|
||||
connectionQualityInterval: 3
|
||||
timeInterpolationGui: 0
|
||||
spawnAmount: 50000
|
||||
spawnAmount: 10000
|
||||
interleave: 2
|
||||
spawnPrefab: {fileID: 449802645721213856, guid: 0ea79775d59804682a8cdd46b3811344,
|
||||
type: 3}
|
||||
|
8
Assets/Mirror/Examples/BenchmarkPrediction.meta
Normal file
8
Assets/Mirror/Examples/BenchmarkPrediction.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7e90270b475f740d69548d4ed4ef5f7a
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
80
Assets/Mirror/Examples/BenchmarkPrediction/BallMaterial.mat
Normal file
80
Assets/Mirror/Examples/BenchmarkPrediction/BallMaterial.mat
Normal 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: []
|
@ -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
@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 26e96d86a94c2451d85dcabf4aff3551
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f96c236d30fd94a75a172a7642242637
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -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
|
@ -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
Loading…
Reference in New Issue
Block a user