Merged master

This commit is contained in:
MrGadget1024 2023-12-13 14:42:18 -05:00
commit 8e60e28ca3
1308 changed files with 76229 additions and 6429 deletions

74
.github/CreateRelease.csx vendored Normal file
View File

@ -0,0 +1,74 @@
using System;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
// Modify PreprocessorDefine.cs
string path = "Assets/Mirror/CompilerSymbols/PreprocessorDefine.cs";
string text = File.ReadAllText(path);
// Find the whole line of the first define ending with "MIRROR_n_OR_NEWER,"
string pattern = @"\s+\""(MIRROR_(\d+)_OR_NEWER)\""\,\n";
Match match = Regex.Matches(text, pattern).First();
// Remove the first define
text = text.Replace(match.Value, "");
// Find the highest version number entry, not having a comma at the end
pattern = @"\""(MIRROR_(\d+)_OR_NEWER)\""\n";
MatchCollection matches = Regex.Matches(text, pattern);
int maxVersion = matches.Max(m => int.Parse(m.Groups[2].Value));
// Find the last define ending with "MIRROR_n_OR_NEWER"
pattern = @"(\s+)\""(MIRROR_(\d+)_OR_NEWER)\""";
matches = Regex.Matches(text, pattern);
Match lastMatch = matches.Last();
// Add a new define for the next full version, used here and in ProjectSettings and version.txt
string newDefine = $"MIRROR_{maxVersion + 1}_OR_NEWER";
// Add the new define to the end of the hashset entries, with a comma after the previous entry and properly indented
text = text.Insert(lastMatch.Index + lastMatch.Length, $",\n{match.Groups[1].Value}\"{newDefine}\"");
File.WriteAllText(path, text);
// Modify ProjectSettings.asset
path = "ProjectSettings/ProjectSettings.asset";
text = File.ReadAllText(path);
// Define a regular expression pattern for finding the sections
pattern = @"(Server|Standalone|WebGL):(.+?)(?=(Server|Standalone|WebGL)|$)";
MatchCollection sectionMatches = Regex.Matches(text, pattern, RegexOptions.Singleline);
if (sectionMatches.Count > 0)
{
foreach (Match sectionMatch in sectionMatches)
{
string sectionName = sectionMatch.Groups[1].Value.Trim();
string sectionContent = sectionMatch.Groups[2].Value.Trim();
// Now, you can work with sectionName and sectionContent
// to locate and update the defines within each section.
// For example, you can use Regex to modify defines within sectionContent.
// For simplicity, let's assume you want to add the newDefine to the end of each section.
pattern = @"(MIRROR_(\d+)_OR_NEWER);";
MatchCollection defineMatches = Regex.Matches(sectionContent, pattern);
if (defineMatches.Count > 0)
{
Match lastDefineMatch = defineMatches[defineMatches.Count - 1];
int lastIndex = lastDefineMatch.Index + lastDefineMatch.Length;
sectionContent = sectionContent.Insert(lastIndex, $";{newDefine}");
}
// Replace the section in the original text with the modified section content
text = text.Remove(sectionMatch.Index, sectionMatch.Length);
text = text.Insert(sectionMatch.Index, $"{sectionName}:{sectionContent}");
}
}
File.WriteAllText(path, text);
// Update version.txt with newDefine, e.g. MIRROR_84_OR_NEWER, replacing _ with .
File.WriteAllText("Assets/Mirror/version.txt", newDefine.Replace("_", "."));

113
.github/ModPreprocessorDefine.csx vendored Normal file
View File

@ -0,0 +1,113 @@
using System;
using System.IO;
using System.Text.RegularExpressions;
Console.WriteLine("ModPreprocessorDefine Started");
// Console.Out.Flush();
ModPreprocessorDefine.DoSomething();
Console.WriteLine("ModPreprocessorDefine Finished");
// Console.Out.Flush();
class ModPreprocessorDefine
{
public static void DoSomething()
{
// Define the path to the PreprocessorDefine.cs file
string filePath = "Assets/Mirror/CompilerSymbols/PreprocessorDefine.cs";
// Read the contents of the file
string fileContents = File.ReadAllText(filePath);
Console.WriteLine("ModPreprocessorDefine File read");
// Console.Out.Flush();
// Find and remove the first entry ending with "_OR_NEWER"
fileContents = RemoveFirstOrNewerEntry(fileContents);
Console.WriteLine("ModPreprocessorDefine Old entry removed");
// Console.Out.Flush();
// Find the last entry and capture the version number
string versionNumber = GetLastVersionNumber(fileContents);
Console.WriteLine($"ModPreprocessorDefine current version {versionNumber}");
// Console.Out.Flush();
// Append a new entry with the correct indentation and next version number
fileContents = AppendNewEntry(fileContents, versionNumber);
Console.WriteLine("ModPreprocessorDefine New entry appended");
// Console.Out.Flush();
// Write the modified contents back to the file
File.WriteAllText(filePath, fileContents);
}
static string RemoveFirstOrNewerEntry(string input)
{
// Regex pattern to match the first entry ending with "_OR_NEWER"
string pattern = @"^\s*""[^""]*_OR_NEWER""\s*,\s*$";
// Find the first match
Match match = Regex.Match(input, pattern, RegexOptions.Multiline);
// If a match is found, remove the entire line
if (match.Success)
{
input = input.Remove(match.Index, match.Length);
}
return input;
}
static string GetLastVersionNumber(string input)
{
// Regex pattern to match the last entry and capture the version number
string pattern = @"^\s*""([^""]*)_OR_NEWER""\s*,\s*$";
// Find all matches
MatchCollection matches = Regex.Matches(input, pattern, RegexOptions.Multiline);
// Capture the version number from the last match
string versionNumber = matches.Count > 0 ? matches[matches.Count - 1].Groups[1].Value : "";
return versionNumber;
}
static string AppendNewEntry(string input, string versionNumber)
{
// Calculate the next version number (increment by 1)
int nextVersion = int.TryParse(versionNumber, out int currentVersion) ? currentVersion + 1 : 1;
// Get the indentation of the "HashSet<string> defines = new HashSet<string>" line
string indentation = GetHashSetIndentation(input);
// Create the new entry with the correct indentation and next version number
string newEntry = indentation + $" \"MIRROR_{nextVersion}_OR_NEWER\"";
Console.WriteLine($"New entry: {newEntry}");
// Find the position of the "defines" HashSet and insert the new entry into it
int definesStartIndex = input.IndexOf("HashSet<string> defines = new HashSet<string>");
int definesEndIndex = input.IndexOf("};", definesStartIndex) + 1;
// Insert the comma and new entry into the "defines" HashSet
input = input.Remove(definesEndIndex - 2, 2); // Remove the trailing "};"
input = input.Insert(definesEndIndex - 2, $",\n{newEntry}\n{indentation}}};");
Console.WriteLine(input);
return input;
}
static string GetHashSetIndentation(string input)
{
// Regex pattern to match the indentation of "HashSet<string> defines = new HashSet<string>"
string pattern = @"^\s*HashSet<string> defines = new HashSet<string>";
// Find the first match
Match match = Regex.Match(input, pattern, RegexOptions.Multiline);
// If a match is found, capture the indentation and add 4 spaces
string indentation = match.Success ? Regex.Match(match.Value, @"^\s*").Value : "";
return indentation;
}
}

46
.github/workflows/CreateRelease.yml vendored Normal file
View File

@ -0,0 +1,46 @@
name: Create Release
on:
workflow_dispatch:
jobs:
CreateRelease:
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Merge Master into AssetStoreRelease
uses: devmasx/merge-branch@master
with:
type: now
from_branch: master
target_branch: AssetStoreRelease
message: "Merged master"
github_token: ${{ secrets.GITHUB_TOKEN }}
- name: Checkout AssetStoreRelease
run: |
git checkout -b AssetStoreRelease
git pull origin AssetStoreRelease
- name: Set up .NET Core
uses: actions/setup-dotnet@v3
- name: Install dotnet-script
run: |
dotnet tool install -g dotnet-script
dotnet script --version
- name: Run ModPreprocessorDefine.csx
run: dotnet script .github/ModPreprocessorDefine.csx
- name: Commit and Push
run: |
git config user.name ${{ secrets.COMMITTER_NAME }}
git config user.email ${{ secrets.COMMITTER_EMAIL }}
git commit -m "release!: Asset Store Release" -a
git push origin AssetStoreRelease
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@ -12,13 +12,14 @@ jobs:
matrix:
unityVersion:
- 2019.4.40f1
- 2020.3.46f1
- 2021.3.22f1
- 2022.2.13f1
- 2020.3.48f1
- 2021.3.33f1
- 2022.3.14f1
- 2023.2.2f1
steps:
- name: Checkout repository
uses: actions/checkout@v3
uses: actions/checkout@v4
# Do Not Enable Caching --- Library needs to be recompiled every time because Weaver
# Leaving this here for posterity to ensure we never turn this on.

View File

@ -38,7 +38,7 @@ jobs:
path: Mirror.unitypackage
- name: Release
uses: cycjimmy/semantic-release-action@v3
uses: cycjimmy/semantic-release-action@v4
with:
extra_plugins: |
@semantic-release/exec

View File

@ -3,6 +3,20 @@ name: Main
on:
workflow_dispatch:
pull_request:
branches:
- master
paths-ignore:
- 'Packages/**'
- 'ProjectSettings/**'
- '.github/**'
- '.gitattributes'
- '.gitignore'
- '.editorconfig'
- 'LICENSE'
- '**.md'
- '**.yml'
- '**.txt'
- '**.ps1'
push:
branches:
- master
@ -14,10 +28,10 @@ on:
- '.gitignore'
- '.editorconfig'
- 'LICENSE'
- '*.md'
- '*.yml'
- '*.txt'
- '*.ps1'
- '**.md'
- '**.yml'
- '**.txt'
- '**.ps1'
jobs:
RunUnityTests:

1
.gitignore vendored
View File

@ -16,6 +16,7 @@ UserSettings/Search.settings
# ===================================== #
Database.sqlite
Database/
Builds/
# ===================================== #
# Visual Studio / MonoDevelop / Rider #

View File

@ -11,22 +11,28 @@ static class PreprocessorDefine
[InitializeOnLoadMethod]
public static void AddDefineSymbols()
{
#if UNITY_2021_2_OR_NEWER
string currentDefines = PlayerSettings.GetScriptingDefineSymbols(UnityEditor.Build.NamedBuildTarget.FromBuildTargetGroup(EditorUserBuildSettings.selectedBuildTargetGroup));
#else
// Deprecated in Unity 2023.1
string currentDefines = PlayerSettings.GetScriptingDefineSymbolsForGroup(EditorUserBuildSettings.selectedBuildTargetGroup);
#endif
// Remove oldest when adding next month's symbol.
// Keep a rolling 12 months of symbols.
HashSet<string> defines = new HashSet<string>(currentDefines.Split(';'))
{
"MIRROR",
"MIRROR_57_0_OR_NEWER",
"MIRROR_58_0_OR_NEWER",
"MIRROR_65_0_OR_NEWER",
"MIRROR_66_0_OR_NEWER",
"MIRROR_2022_9_OR_NEWER",
"MIRROR_2022_10_OR_NEWER",
"MIRROR_70_0_OR_NEWER",
"MIRROR_71_0_OR_NEWER",
"MIRROR_70_OR_NEWER",
"MIRROR_71_OR_NEWER",
"MIRROR_73_OR_NEWER",
"MIRROR_78_OR_NEWER"
// Remove oldest when adding next month's symbol.
// Keep a rolling 12 months of symbols.
"MIRROR_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"
};
// only touch PlayerSettings if we actually modified it,
@ -34,7 +40,12 @@ public static void AddDefineSymbols()
string newDefines = string.Join(";", defines);
if (newDefines != currentDefines)
{
#if UNITY_2021_2_OR_NEWER
PlayerSettings.SetScriptingDefineSymbols(UnityEditor.Build.NamedBuildTarget.FromBuildTargetGroup(EditorUserBuildSettings.selectedBuildTargetGroup), newDefines);
#else
// Deprecated in Unity 2023.1
PlayerSettings.SetScriptingDefineSymbolsForGroup(EditorUserBuildSettings.selectedBuildTargetGroup, newDefines);
#endif
}
}
}

View File

@ -6,7 +6,7 @@
namespace Mirror.Discovery
{
[Serializable]
public class ServerFoundUnityEvent : UnityEvent<ServerResponse> {};
public class ServerFoundUnityEvent<TResponseType> : UnityEvent<TResponseType> {};
[DisallowMultipleComponent]
[AddComponentMenu("Network/Network Discovery")]

View File

@ -45,7 +45,7 @@ public abstract class NetworkDiscoveryBase<Request, Response> : MonoBehaviour
public Transport transport;
[Tooltip("Invoked when a server is found")]
public ServerFoundUnityEvent OnServerFound;
public ServerFoundUnityEvent<Response> OnServerFound;
// Each game should have a random unique handshake,
// this way you can tell if this is the same game or not
@ -85,9 +85,10 @@ public virtual void Start()
transport = Transport.active;
// Server mode? then start advertising
#if UNITY_SERVER
AdvertiseServer();
#endif
if (Utils.IsHeadless())
{
AdvertiseServer();
}
}
public static long RandomLong()

View File

@ -1,9 +1,11 @@
using System;
using UnityEngine;
namespace Mirror.Experimental
{
[AddComponentMenu("Network/ Experimental/Network Lerp Rigidbody")]
[HelpURL("https://mirror-networking.gitbook.io/docs/components/network-lerp-rigidbody")]
[Obsolete("Use the new NetworkRigidbodyReliable/Unreliable component with Snapshot Interpolation instead.")]
public class NetworkLerpRigidbody : NetworkBehaviour
{
[Header("Settings")]
@ -33,8 +35,9 @@ public class NetworkLerpRigidbody : NetworkBehaviour
bool ClientWithAuthority => clientAuthority && isOwned;
void OnValidate()
protected override void OnValidate()
{
base.OnValidate();
if (target == null)
target = GetComponent<Rigidbody>();
}

View File

@ -1,9 +1,11 @@
using System;
using UnityEngine;
namespace Mirror.Experimental
{
[AddComponentMenu("Network/ Experimental/Network Rigidbody")]
[HelpURL("https://mirror-networking.gitbook.io/docs/components/network-rigidbody")]
[Obsolete("Use the new NetworkRigidbodyReliable/Unreliable component with Snapshot Interpolation instead.")]
public class NetworkRigidbody : NetworkBehaviour
{
[Header("Settings")]
@ -37,8 +39,9 @@ public class NetworkRigidbody : NetworkBehaviour
/// </summary>
readonly ClientSyncState previousValue = new ClientSyncState();
void OnValidate()
protected override void OnValidate()
{
base.OnValidate();
if (target == null)
target = GetComponent<Rigidbody>();
}

View File

@ -37,8 +37,9 @@ public class NetworkRigidbody2D : NetworkBehaviour
/// </summary>
readonly ClientSyncState previousValue = new ClientSyncState();
void OnValidate()
protected override void OnValidate()
{
base.OnValidate();
if (target == null)
target = GetComponent<Rigidbody2D>();
}

View File

@ -31,27 +31,38 @@ public LogEntry(string message, LogType type)
public class GUIConsole : MonoBehaviour
{
public int height = 150;
public int height = 80;
public int offsetY = 40;
// only keep the recent 'n' entries. otherwise memory would grow forever
// and drawing would get slower and slower.
public int maxLogCount = 50;
// Unity Editor has the Console window, we don't need to show it there.
// unless for testing, so keep it as option.
public bool showInEditor = false;
// log as queue so we can remove the first entry easily
Queue<LogEntry> log = new Queue<LogEntry>();
readonly Queue<LogEntry> log = new Queue<LogEntry>();
// hotkey to show/hide at runtime for easier debugging
// (sometimes we need to temporarily hide/show it)
// => F12 makes sense. nobody can find ^ in other games.
public KeyCode hotKey = KeyCode.F12;
// Default is BackQuote, because F keys are already assigned in browsers
[Tooltip("Hotkey to show/hide the console at runtime\nBack Quote is usually on the left above Tab\nChange with caution - F keys are generally already taken in Browsers")]
public KeyCode hotKey = KeyCode.BackQuote;
// GUI
bool visible;
Vector2 scroll = Vector2.zero;
// only show at runtime, or if showInEditor is enabled
bool show => !Application.isEditor || showInEditor;
void Awake()
{
Application.logMessageReceived += OnLog;
// only show at runtime, or if showInEditor is enabled
if (show)
Application.logMessageReceived += OnLog;
}
// OnLog logs everything, even Debug.Log messages in release builds
@ -90,7 +101,7 @@ void OnLog(string message, string stackTrace, LogType type)
void Update()
{
if (Input.GetKeyDown(hotKey))
if (show && Input.GetKeyDown(hotKey))
visible = !visible;
}
@ -98,7 +109,12 @@ void OnGUI()
{
if (!visible) return;
scroll = GUILayout.BeginScrollView(scroll, "Box", GUILayout.Width(Screen.width), GUILayout.Height(height));
// If this offset is changed, also change width in NetworkManagerHUD::OnGUI
int offsetX = 300 + 20;
GUILayout.BeginArea(new Rect(offsetX, offsetY, Screen.width - offsetX - 10, height));
scroll = GUILayout.BeginScrollView(scroll, "Box", GUILayout.Width(Screen.width - offsetX - 10), GUILayout.Height(height));
foreach (LogEntry entry in log)
{
if (entry.type == LogType.Error || entry.type == LogType.Exception)
@ -110,6 +126,8 @@ void OnGUI()
GUI.color = Color.white;
}
GUILayout.EndScrollView();
GUILayout.EndArea();
}
}
}

View File

@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 63d647500ca1bfa4a845bc1f4cff9dcc
guid: 00ac1d0527f234939aba22b4d7cbf280
folderAsset: yes
DefaultImporter:
externalObjects: {}

View File

@ -0,0 +1,107 @@
// Applies HistoryBounds to the physics world by projecting to a trigger Collider.
// This way we can use Physics.Raycast on it.
using UnityEngine;
namespace Mirror
{
public class HistoryCollider : MonoBehaviour
{
[Header("Components")]
[Tooltip("The object's actual collider. We need to know where it is, and how large it is.")]
public Collider actualCollider;
[Tooltip("The helper collider that the history bounds are projected onto.\nNeeds to be added to a child GameObject to counter-rotate an axis aligned Bounding Box onto it.\nThis is only used by this component.")]
public BoxCollider boundsCollider;
[Header("History")]
[Tooltip("Keep this many past bounds in the buffer. The larger this is, the further we can raycast into the past.\nMaximum time := historyAmount * captureInterval")]
public int boundsLimit = 8;
[Tooltip("Gather N bounds at a time into a bucket for faster encapsulation. A factor of 2 will be twice as fast, etc.")]
public int boundsPerBucket = 2;
[Tooltip("Capture bounds every 'captureInterval' seconds. Larger values will require fewer computations, but may not capture every small move.")]
public float captureInterval = 0.100f; // 100 ms
double lastCaptureTime = 0;
[Header("Debug")]
public Color historyColor = new Color(1.0f, 0.5f, 0.0f, 1.0f);
public Color currentColor = Color.red;
protected HistoryBounds history = null;
protected virtual void Awake()
{
history = new HistoryBounds(boundsLimit, boundsPerBucket);
// ensure colliders were set.
// bounds collider should always be a trigger.
if (actualCollider == null) Debug.LogError("HistoryCollider: actualCollider was not set.");
if (boundsCollider == null) Debug.LogError("HistoryCollider: boundsCollider was not set.");
if (boundsCollider.transform.parent != transform) Debug.LogError("HistoryCollider: boundsCollider must be a child of this GameObject.");
if (!boundsCollider.isTrigger) Debug.LogError("HistoryCollider: boundsCollider must be a trigger.");
}
// capturing and projecting onto colliders should use physics update
protected virtual void FixedUpdate()
{
// capture current bounds every interval
if (NetworkTime.localTime >= lastCaptureTime + captureInterval)
{
lastCaptureTime = NetworkTime.localTime;
CaptureBounds();
}
// project bounds onto helper collider
ProjectBounds();
}
protected virtual void CaptureBounds()
{
// grab current collider bounds
// this is in world space coordinates, and axis aligned
// TODO double check
Bounds bounds = actualCollider.bounds;
// insert into history
history.Insert(bounds);
}
protected virtual void ProjectBounds()
{
// grab total collider encapsulating all of history
Bounds total = history.total;
// don't assign empty bounds, this will throw a Unity warning
if (history.boundsCount == 0) return;
// scale projection doesn't work yet.
// for now, don't allow scale changes.
if (transform.lossyScale != Vector3.one)
{
Debug.LogWarning($"HistoryCollider: {name}'s transform global scale must be (1,1,1).");
return;
}
// counter rotate the child collider against the gameobject's rotation.
// we need this to always be axis aligned.
boundsCollider.transform.localRotation = Quaternion.Inverse(transform.rotation);
// project world space bounds to collider's local space
boundsCollider.center = boundsCollider.transform.InverseTransformPoint(total.center);
boundsCollider.size = total.size; // TODO projection?
}
// TODO runtime drawing for debugging?
protected virtual void OnDrawGizmos()
{
// draw total bounds
Gizmos.color = historyColor;
Gizmos.DrawWireCube(history.total.center, history.total.size);
// draw current bounds
Gizmos.color = currentColor;
Gizmos.DrawWireCube(actualCollider.bounds.center, actualCollider.bounds.size);
}
}
}

View File

@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 96fc7967f813e4960b9119d7c2118494
guid: f5f2158d9776d4b569858f793be4da60
MonoImporter:
externalObjects: {}
serializedVersion: 2

View File

@ -13,8 +13,8 @@ namespace Mirror
/// <para>If the object has authority on the server, then it should be animated on the server and state information will be sent to all clients. This is common for objects not related to a specific client, such as an enemy unit.</para>
/// <para>The NetworkAnimator synchronizes all animation parameters of the selected Animator. It does not automatically synchronize triggers. The function SetTrigger can by used by an object with authority to fire an animation trigger on other clients.</para>
/// </remarks>
// [RequireComponent(typeof(NetworkIdentity))] disabled to allow child NetworkBehaviours
[AddComponentMenu("Network/Network Animator")]
[RequireComponent(typeof(NetworkIdentity))]
[HelpURL("https://mirror-networking.gitbook.io/docs/components/network-animator")]
public class NetworkAnimator : NetworkBehaviour
{

View File

@ -0,0 +1,30 @@
using UnityEngine;
namespace Mirror
{
public class NetworkDiagnosticsDebugger : MonoBehaviour
{
public bool logInMessages = true;
public bool logOutMessages = true;
void OnInMessage(NetworkDiagnostics.MessageInfo msgInfo)
{
if (logInMessages)
Debug.Log(msgInfo);
}
void OnOutMessage(NetworkDiagnostics.MessageInfo msgInfo)
{
if (logOutMessages)
Debug.Log(msgInfo);
}
void OnEnable()
{
NetworkDiagnostics.InMessageEvent += OnInMessage;
NetworkDiagnostics.OutMessageEvent += OnOutMessage;
}
void OnDisable()
{
NetworkDiagnostics.InMessageEvent -= OnInMessage;
NetworkDiagnostics.OutMessageEvent -= OnOutMessage;
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: bc9f0a0fe4124424b8f9d4927795ee01
timeCreated: 1700945893

View File

@ -13,7 +13,7 @@ public class NetworkPingDisplay : MonoBehaviour
{
public Color color = Color.white;
public int padding = 2;
public int width = 150;
public int width = 100;
public int height = 25;
void OnGUI()
@ -21,12 +21,18 @@ void OnGUI()
// only while client is active
if (!NetworkClient.active) return;
// show rtt in bottom right corner, right aligned
// show stats in bottom right corner, right aligned
GUI.color = color;
Rect rect = new Rect(Screen.width - width - padding, Screen.height - height - padding, width, height);
GUILayout.BeginArea(rect);
GUIStyle style = GUI.skin.GetStyle("Label");
style.alignment = TextAnchor.MiddleRight;
GUI.Label(rect, $"RTT: {Math.Round(NetworkTime.rtt * 1000)}ms", style);
GUILayout.BeginHorizontal(style);
GUILayout.Label($"RTT: {Math.Round(NetworkTime.rtt * 1000)}ms");
GUI.color = NetworkClient.connectionQuality.ColorCode();
GUILayout.Label($"Q: {new string('-', (int)NetworkClient.connectionQuality)}");
GUILayout.EndHorizontal();
GUILayout.EndArea();
GUI.color = Color.white;
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 80106690aef541a5b8e2f8fb3d5949ad
timeCreated: 1686733778

View File

@ -0,0 +1,96 @@
using UnityEngine;
namespace Mirror
{
// [RequireComponent(typeof(Rigidbody))] <- OnValidate ensures this is on .target
public class NetworkRigidbodyReliable : NetworkTransformReliable
{
bool clientAuthority => syncDirection == SyncDirection.ClientToServer;
Rigidbody rb;
bool wasKinematic;
// cach Rigidbody and original isKinematic setting
protected override void Awake()
{
// we can't overwrite .target to be a Rigidbody.
// but we can use its Rigidbody component.
rb = target.GetComponent<Rigidbody>();
if (rb == null)
{
Debug.LogError($"{name}'s NetworkRigidbody.target {target.name} is missing a Rigidbody", this);
return;
}
wasKinematic = rb.isKinematic;
base.Awake();
}
// reset forced isKinematic flag to original.
// otherwise the overwritten value would remain between sessions forever.
// for example, a game may run as client, set rigidbody.iskinematic=true,
// then run as server, where .iskinematic isn't touched and remains at
// the overwritten=true, even though the user set it to false originally.
public override void OnStopServer() => rb.isKinematic = wasKinematic;
public override void OnStopClient() => rb.isKinematic = wasKinematic;
// overwriting Construct() and Apply() to set Rigidbody.MovePosition
// would give more jittery movement.
// FixedUpdate for physics
void FixedUpdate()
{
// who ever has authority moves the Rigidbody with physics.
// everyone else simply sets it to kinematic.
// so that only the Transform component is synced.
// host mode
if (isServer && isClient)
{
// in host mode, we own it it if:
// clientAuthority is disabled (hence server / we own it)
// clientAuthority is enabled and we have authority over this object.
bool owned = !clientAuthority || IsClientWithAuthority;
// only set to kinematic if we don't own it
// otherwise don't touch isKinematic.
// the authority owner might use it either way.
if (!owned) rb.isKinematic = true;
}
// client only
else if (isClient)
{
// on the client, we own it only if clientAuthority is enabled,
// and we have authority over this object.
bool owned = IsClientWithAuthority;
// only set to kinematic if we don't own it
// otherwise don't touch isKinematic.
// the authority owner might use it either way.
if (!owned) rb.isKinematic = true;
}
// server only
else if (isServer)
{
// on the server, we always own it if clientAuthority is disabled.
bool owned = !clientAuthority;
// only set to kinematic if we don't own it
// otherwise don't touch isKinematic.
// the authority owner might use it either way.
if (!owned) rb.isKinematic = true;
}
}
protected override void OnValidate()
{
base.OnValidate();
// we can't overwrite .target to be a Rigidbody.
// but we can ensure that .target has a Rigidbody, and use it.
if (target.GetComponent<Rigidbody>() == null)
{
Debug.LogWarning($"{name}'s NetworkRigidbody.target {target.name} is missing a Rigidbody", this);
}
}
}
}

View File

@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 330c9aab13d2d42069c6ebbe582b73ca
guid: cb803efbe62c34d7baece46c9ffebad9
MonoImporter:
externalObjects: {}
serializedVersion: 2

View File

@ -0,0 +1,96 @@
using UnityEngine;
namespace Mirror
{
// [RequireComponent(typeof(Rigidbody))] <- OnValidate ensures this is on .target
public class NetworkRigidbodyReliable2D : NetworkTransformReliable
{
bool clientAuthority => syncDirection == SyncDirection.ClientToServer;
Rigidbody2D rb;
bool wasKinematic;
// cach Rigidbody and original isKinematic setting
protected override void Awake()
{
// we can't overwrite .target to be a Rigidbody.
// but we can use its Rigidbody component.
rb = target.GetComponent<Rigidbody2D>();
if (rb == null)
{
Debug.LogError($"{name}'s NetworkRigidbody2D.target {target.name} is missing a Rigidbody2D", this);
return;
}
wasKinematic = rb.isKinematic;
base.Awake();
}
// reset forced isKinematic flag to original.
// otherwise the overwritten value would remain between sessions forever.
// for example, a game may run as client, set rigidbody.iskinematic=true,
// then run as server, where .iskinematic isn't touched and remains at
// the overwritten=true, even though the user set it to false originally.
public override void OnStopServer() => rb.isKinematic = wasKinematic;
public override void OnStopClient() => rb.isKinematic = wasKinematic;
// overwriting Construct() and Apply() to set Rigidbody.MovePosition
// would give more jittery movement.
// FixedUpdate for physics
void FixedUpdate()
{
// who ever has authority moves the Rigidbody with physics.
// everyone else simply sets it to kinematic.
// so that only the Transform component is synced.
// host mode
if (isServer && isClient)
{
// in host mode, we own it it if:
// clientAuthority is disabled (hence server / we own it)
// clientAuthority is enabled and we have authority over this object.
bool owned = !clientAuthority || IsClientWithAuthority;
// only set to kinematic if we don't own it
// otherwise don't touch isKinematic.
// the authority owner might use it either way.
if (!owned) rb.isKinematic = true;
}
// client only
else if (isClient)
{
// on the client, we own it only if clientAuthority is enabled,
// and we have authority over this object.
bool owned = IsClientWithAuthority;
// only set to kinematic if we don't own it
// otherwise don't touch isKinematic.
// the authority owner might use it either way.
if (!owned) rb.isKinematic = true;
}
// server only
else if (isServer)
{
// on the server, we always own it if clientAuthority is disabled.
bool owned = !clientAuthority;
// only set to kinematic if we don't own it
// otherwise don't touch isKinematic.
// the authority owner might use it either way.
if (!owned) rb.isKinematic = true;
}
}
protected override void OnValidate()
{
base.OnValidate();
// we can't overwrite .target to be a Rigidbody.
// but we can ensure that .target has a Rigidbody, and use it.
if (target.GetComponent<Rigidbody2D>() == null)
{
Debug.LogWarning($"{name}'s NetworkRigidbody2D.target {target.name} is missing a Rigidbody2D", this);
}
}
}
}

View File

@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: f6928b080072948f7b2909b4025fcc79
guid: 7ec4f7556ca1e4b55a3381fc6a02b1bc
MonoImporter:
externalObjects: {}
serializedVersion: 2

View File

@ -0,0 +1,96 @@
using UnityEngine;
namespace Mirror
{
// [RequireComponent(typeof(Rigidbody))] <- OnValidate ensures this is on .target
public class NetworkRigidbodyUnreliable : NetworkTransformUnreliable
{
bool clientAuthority => syncDirection == SyncDirection.ClientToServer;
Rigidbody rb;
bool wasKinematic;
// cach Rigidbody and original isKinematic setting
protected override void Awake()
{
// we can't overwrite .target to be a Rigidbody.
// but we can use its Rigidbody component.
rb = target.GetComponent<Rigidbody>();
if (rb == null)
{
Debug.LogError($"{name}'s NetworkRigidbody.target {target.name} is missing a Rigidbody", this);
return;
}
wasKinematic = rb.isKinematic;
base.Awake();
}
// reset forced isKinematic flag to original.
// otherwise the overwritten value would remain between sessions forever.
// for example, a game may run as client, set rigidbody.iskinematic=true,
// then run as server, where .iskinematic isn't touched and remains at
// the overwritten=true, even though the user set it to false originally.
public override void OnStopServer() => rb.isKinematic = wasKinematic;
public override void OnStopClient() => rb.isKinematic = wasKinematic;
// overwriting Construct() and Apply() to set Rigidbody.MovePosition
// would give more jittery movement.
// FixedUpdate for physics
void FixedUpdate()
{
// who ever has authority moves the Rigidbody with physics.
// everyone else simply sets it to kinematic.
// so that only the Transform component is synced.
// host mode
if (isServer && isClient)
{
// in host mode, we own it it if:
// clientAuthority is disabled (hence server / we own it)
// clientAuthority is enabled and we have authority over this object.
bool owned = !clientAuthority || IsClientWithAuthority;
// only set to kinematic if we don't own it
// otherwise don't touch isKinematic.
// the authority owner might use it either way.
if (!owned) rb.isKinematic = true;
}
// client only
else if (isClient)
{
// on the client, we own it only if clientAuthority is enabled,
// and we have authority over this object.
bool owned = IsClientWithAuthority;
// only set to kinematic if we don't own it
// otherwise don't touch isKinematic.
// the authority owner might use it either way.
if (!owned) rb.isKinematic = true;
}
// server only
else if (isServer)
{
// on the server, we always own it if clientAuthority is disabled.
bool owned = !clientAuthority;
// only set to kinematic if we don't own it
// otherwise don't touch isKinematic.
// the authority owner might use it either way.
if (!owned) rb.isKinematic = true;
}
}
protected override void OnValidate()
{
base.OnValidate();
// we can't overwrite .target to be a Rigidbody.
// but we can ensure that .target has a Rigidbody, and use it.
if (target.GetComponent<Rigidbody>() == null)
{
Debug.LogWarning($"{name}'s NetworkRigidbody.target {target.name} is missing a Rigidbody", this);
}
}
}
}

View File

@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: c7627623f2b9fad4484082517cd73e67
guid: 3b20dc110904e47f8a154cdcf6433eae
MonoImporter:
externalObjects: {}
serializedVersion: 2

View File

@ -0,0 +1,96 @@
using UnityEngine;
namespace Mirror
{
// [RequireComponent(typeof(Rigidbody))] <- OnValidate ensures this is on .target
public class NetworkRigidbodyUnreliable2D : NetworkTransformUnreliable
{
bool clientAuthority => syncDirection == SyncDirection.ClientToServer;
Rigidbody2D rb;
bool wasKinematic;
// cach Rigidbody and original isKinematic setting
protected override void Awake()
{
// we can't overwrite .target to be a Rigidbody.
// but we can use its Rigidbody component.
rb = target.GetComponent<Rigidbody2D>();
if (rb == null)
{
Debug.LogError($"{name}'s NetworkRigidbody2D.target {target.name} is missing a Rigidbody2D", this);
return;
}
wasKinematic = rb.isKinematic;
base.Awake();
}
// reset forced isKinematic flag to original.
// otherwise the overwritten value would remain between sessions forever.
// for example, a game may run as client, set rigidbody.iskinematic=true,
// then run as server, where .iskinematic isn't touched and remains at
// the overwritten=true, even though the user set it to false originally.
public override void OnStopServer() => rb.isKinematic = wasKinematic;
public override void OnStopClient() => rb.isKinematic = wasKinematic;
// overwriting Construct() and Apply() to set Rigidbody.MovePosition
// would give more jittery movement.
// FixedUpdate for physics
void FixedUpdate()
{
// who ever has authority moves the Rigidbody with physics.
// everyone else simply sets it to kinematic.
// so that only the Transform component is synced.
// host mode
if (isServer && isClient)
{
// in host mode, we own it it if:
// clientAuthority is disabled (hence server / we own it)
// clientAuthority is enabled and we have authority over this object.
bool owned = !clientAuthority || IsClientWithAuthority;
// only set to kinematic if we don't own it
// otherwise don't touch isKinematic.
// the authority owner might use it either way.
if (!owned) rb.isKinematic = true;
}
// client only
else if (isClient)
{
// on the client, we own it only if clientAuthority is enabled,
// and we have authority over this object.
bool owned = IsClientWithAuthority;
// only set to kinematic if we don't own it
// otherwise don't touch isKinematic.
// the authority owner might use it either way.
if (!owned) rb.isKinematic = true;
}
// server only
else if (isServer)
{
// on the server, we always own it if clientAuthority is disabled.
bool owned = !clientAuthority;
// only set to kinematic if we don't own it
// otherwise don't touch isKinematic.
// the authority owner might use it either way.
if (!owned) rb.isKinematic = true;
}
}
protected override void OnValidate()
{
base.OnValidate();
// we can't overwrite .target to be a Rigidbody.
// but we can ensure that .target has a Rigidbody, and use it.
if (target.GetComponent<Rigidbody2D>() == null)
{
Debug.LogWarning($"{name}'s NetworkRigidbody2D.target {target.name} is missing a Rigidbody2D", this);
}
}
}
}

View File

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

View File

@ -251,10 +251,11 @@ public override void OnServerDisconnect(NetworkConnectionToClient conn)
OnRoomServerDisconnect(conn);
base.OnServerDisconnect(conn);
#if UNITY_SERVER
if (numPlayers < 1)
StopServer();
#endif
if (Utils.IsHeadless())
{
if (numPlayers < 1)
StopServer();
}
}
// Sequential index used in round-robin deployment of players into instances and score positioning

View File

@ -41,7 +41,7 @@ public class NetworkRoomPlayer : NetworkBehaviour
/// <summary>
/// Do not use Start - Override OnStartHost / OnStartClient instead!
/// </summary>
public void Start()
public virtual void Start()
{
if (NetworkManager.singleton is NetworkRoomManager room)
{

View File

@ -0,0 +1,8 @@
using System;
namespace Mirror
{
// DEPRECATED 2023-06-15
[Obsolete("NetworkTransform was renamed to NetworkTransformUnreliable.\nYou can easily swap the component's script by going into the Unity Inspector debug mode:\n1. Click the vertical dots on the top right in the Inspector tab.\n2. Find your NetworkTransform component\n3. Drag NetworkTransformUnreliable into the 'Script' field in the Inspector.\n4. Find the three dots and return to Normal mode.")]
public class NetworkTransform : NetworkTransformUnreliable {}
}

View File

@ -22,24 +22,23 @@
namespace Mirror
{
public enum CoordinateSpace { Local, World }
public abstract class NetworkTransformBase : NetworkBehaviour
{
// target transform to sync. can be on a child.
// TODO this field is kind of unnecessary since we now support child NetworkBehaviours
[Header("Target")]
[Tooltip("The Transform component to sync. May be on on this GameObject, or on a child.")]
public Transform target;
// TODO SyncDirection { ClientToServer, ServerToClient } is easier?
// Deprecated 2022-10-25
[Obsolete("NetworkTransform clientAuthority was replaced with syncDirection. To enable client authority, set SyncDirection to ClientToServer in the Inspector.")]
[Header("[Obsolete]")] // Unity doesn't show obsolete warning for fields. do it manually.
[Tooltip("Obsolete: NetworkTransform clientAuthority was replaced with syncDirection. To enable client authority, set SyncDirection to ClientToServer in the Inspector.")]
public bool clientAuthority;
// Is this a client with authority over this transform?
// This component could be on the player object or any object that has been assigned authority to this client.
protected bool IsClientWithAuthority => isClient && authority;
public readonly SortedList<double, TransformSnapshot> clientSnapshots = new SortedList<double, TransformSnapshot>();
public readonly SortedList<double, TransformSnapshot> serverSnapshots = new SortedList<double, TransformSnapshot>();
// snapshots with initial capacity to avoid early resizing & allocations: see NetworkRigidbodyBenchmark example.
public readonly SortedList<double, TransformSnapshot> clientSnapshots = new SortedList<double, TransformSnapshot>(16);
public readonly SortedList<double, TransformSnapshot> serverSnapshots = new SortedList<double, TransformSnapshot>(16);
// selective sync //////////////////////////////////////////////////////
[Header("Selective Sync\nDon't change these at Runtime")]
@ -57,6 +56,36 @@ public abstract class NetworkTransformBase : NetworkBehaviour
[Tooltip("Set to false to remove scale smoothing. Example use-case: Instant flipping of sprites that use -X and +X for direction.")]
public bool interpolateScale = true;
// CoordinateSpace ///////////////////////////////////////////////////////////
[Header("Coordinate Space")]
[Tooltip("Local by default. World may be better when changing hierarchy, or non-NetworkTransforms root position/rotation/scale values.")]
public CoordinateSpace coordinateSpace = CoordinateSpace.Local;
[Header("Send Interval Multiplier")]
[Tooltip("Check/Sync every multiple of Network Manager send interval (= 1 / NM Send Rate), instead of every send interval.\n(30 NM send rate, and 3 interval, is a send every 0.1 seconds)\nA larger interval means less network sends, which has a variety of upsides. The drawbacks are delays and lower accuracy, you should find a nice balance between not sending too much, but the results looking good for your particular scenario.")]
[Range(1, 120)]
public uint sendIntervalMultiplier = 1;
[Header("Timeline Offset")]
[Tooltip("Add a small timeline offset to account for decoupled arrival of NetworkTime and NetworkTransform snapshots.\nfixes: https://github.com/MirrorNetworking/Mirror/issues/3427")]
public bool timelineOffset = false;
// Ninja's Notes on offset & mulitplier:
//
// In a no multiplier scenario:
// 1. Snapshots are sent every frame (frame being 1 NM send interval).
// 2. Time Interpolation is set to be 'behind' by 2 frames times.
// In theory where everything works, we probably have around 2 snapshots before we need to interpolate snapshots. From NT perspective, we should always have around 2 snapshots ready, so no stutter.
//
// In a multiplier scenario:
// 1. Snapshots are sent every 10 frames.
// 2. Time Interpolation remains 'behind by 2 frames'.
// When everything works, we are receiving NT snapshots every 10 frames, but start interpolating after 2.
// Even if I assume we had 2 snapshots to begin with to start interpolating (which we don't), by the time we reach 13th frame, we are out of snapshots, and have to wait 7 frames for next snapshot to come. This is the reason why we absolutely need the timestamp adjustment. We are starting way too early to interpolate.
//
protected double timeStampAdjustment => NetworkServer.sendInterval * (sendIntervalMultiplier - 1);
protected double offset => timelineOffset ? NetworkServer.sendInterval * sendIntervalMultiplier : 0;
// debugging ///////////////////////////////////////////////////////////
[Header("Debug")]
public bool showGizmos;
@ -67,8 +96,10 @@ public abstract class NetworkTransformBase : NetworkBehaviour
// make sure to call this when inheriting too!
protected virtual void Awake() { }
protected virtual void OnValidate()
protected override void OnValidate()
{
base.OnValidate();
// set target to self if none yet
if (target == null) target = transform;
@ -79,19 +110,53 @@ protected virtual void OnValidate()
// actually use NetworkServer.sendInterval.
syncInterval = 0;
// obsolete clientAuthority compatibility:
// if it was used, then set the new SyncDirection automatically.
// if it wasn't used, then don't touch syncDirection.
#pragma warning disable CS0618
if (clientAuthority)
{
syncDirection = SyncDirection.ClientToServer;
Debug.LogWarning($"{name}'s NetworkTransform component has obsolete .clientAuthority enabled. Please disable it and set SyncDirection to ClientToServer instead.");
}
#pragma warning restore CS0618
// Unity doesn't support setting world scale.
// OnValidate force disables syncScale in world mode.
if (coordinateSpace == CoordinateSpace.World) syncScale = false;
}
// snapshot functions //////////////////////////////////////////////////
// get local/world position
protected virtual Vector3 GetPosition() =>
coordinateSpace == CoordinateSpace.Local ? target.localPosition : target.position;
// get local/world rotation
protected virtual Quaternion GetRotation() =>
coordinateSpace == CoordinateSpace.Local ? target.localRotation : target.rotation;
// get local/world scale
protected virtual Vector3 GetScale() =>
coordinateSpace == CoordinateSpace.Local ? target.localScale : target.lossyScale;
// set local/world position
protected virtual void SetPosition(Vector3 position)
{
if (coordinateSpace == CoordinateSpace.Local)
target.localPosition = position;
else
target.position = position;
}
// set local/world rotation
protected virtual void SetRotation(Quaternion rotation)
{
if (coordinateSpace == CoordinateSpace.Local)
target.localRotation = rotation;
else
target.rotation = rotation;
}
// set local/world position
protected virtual void SetScale(Vector3 scale)
{
if (coordinateSpace == CoordinateSpace.Local)
target.localScale = scale;
// Unity doesn't support setting world scale.
// OnValidate disables syncScale in world mode.
// else
// target.lossyScale = scale; // TODO
}
// construct a snapshot of the current state
// => internal for testing
protected virtual TransformSnapshot Construct()
@ -101,9 +166,9 @@ protected virtual TransformSnapshot Construct()
// our local time is what the other end uses as remote time
NetworkTime.localTime, // Unity 2019 doesn't have timeAsDouble yet
0, // the other end fills out local time itself
target.localPosition,
target.localRotation,
target.localScale
GetPosition(),
GetRotation(),
GetScale()
);
}
@ -118,18 +183,23 @@ protected void AddSnapshot(SortedList<double, TransformSnapshot> snapshots, doub
// client sends snapshot at t=10
// then the server would assume that it's one super slow move and
// replay it for 10 seconds.
if (!position.HasValue) position = snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].position : target.localPosition;
if (!rotation.HasValue) rotation = snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].rotation : target.localRotation;
if (!scale.HasValue) scale = snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].scale : target.localScale;
if (!position.HasValue) position = snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].position : GetPosition();
if (!rotation.HasValue) rotation = snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].rotation : GetRotation();
if (!scale.HasValue) scale = snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].scale : GetScale();
// insert transform snapshot
SnapshotInterpolation.InsertIfNotExists(snapshots, new TransformSnapshot(
timeStamp, // arrival remote timestamp. NOT remote time.
NetworkTime.localTime, // Unity 2019 doesn't have timeAsDouble yet
position.Value,
rotation.Value,
scale.Value
));
SnapshotInterpolation.InsertIfNotExists(
snapshots,
NetworkClient.snapshotSettings.bufferLimit,
new TransformSnapshot(
timeStamp, // arrival remote timestamp. NOT remote time.
NetworkTime.localTime, // Unity 2019 doesn't have timeAsDouble yet
position.Value,
rotation.Value,
scale.Value
)
);
}
// apply a snapshot to the Transform.
@ -152,14 +222,10 @@ protected virtual void Apply(TransformSnapshot interpolated, TransformSnapshot e
// -> but simply don't apply it. if the user doesn't want to sync
// scale, then we should not touch scale etc.
if (syncPosition)
target.localPosition = interpolatePosition ? interpolated.position : endGoal.position;
if (syncRotation)
target.localRotation = interpolateRotation ? interpolated.rotation : endGoal.rotation;
if (syncScale)
target.localScale = interpolateScale ? interpolated.scale : endGoal.scale;
// interpolate parts
if (syncPosition) SetPosition(interpolatePosition ? interpolated.position : endGoal.position);
if (syncRotation) SetRotation(interpolateRotation ? interpolated.rotation : endGoal.rotation);
if (syncScale) SetScale(interpolateScale ? interpolated.scale : endGoal.scale);
}
// client->server teleport to force position without interpolation.
@ -247,13 +313,22 @@ void RpcReset()
// common Teleport code for client->server and server->client
protected virtual void OnTeleport(Vector3 destination)
{
// reset any in-progress interpolation & buffers
Reset();
// set the new position.
// interpolation will automatically continue.
target.position = destination;
// reset interpolation to immediately jump to the new position.
// do not call Reset() here, this would cause delta compression to
// get out of sync for NetworkTransformReliable because NTReliable's
// 'override Reset()' resets lastDe/SerializedPosition:
// https://github.com/MirrorNetworking/Mirror/issues/3588
// because client's next OnSerialize() will delta compress,
// but server's last delta will have been reset, causing offsets.
//
// instead, simply clear snapshots.
serverSnapshots.Clear();
clientSnapshots.Clear();
// TODO
// what if we still receive a snapshot from before the interpolation?
// it could easily happen over unreliable.
@ -263,14 +338,23 @@ protected virtual void OnTeleport(Vector3 destination)
// common Teleport code for client->server and server->client
protected virtual void OnTeleport(Vector3 destination, Quaternion rotation)
{
// reset any in-progress interpolation & buffers
Reset();
// set the new position.
// interpolation will automatically continue.
target.position = destination;
target.rotation = rotation;
// reset interpolation to immediately jump to the new position.
// do not call Reset() here, this would cause delta compression to
// get out of sync for NetworkTransformReliable because NTReliable's
// 'override Reset()' resets lastDe/SerializedPosition:
// https://github.com/MirrorNetworking/Mirror/issues/3588
// because client's next OnSerialize() will delta compress,
// but server's last delta will have been reset, causing offsets.
//
// instead, simply clear snapshots.
serverSnapshots.Clear();
clientSnapshots.Clear();
// TODO
// what if we still receive a snapshot from before the interpolation?
// it could easily happen over unreliable.
@ -368,7 +452,7 @@ protected virtual void DrawGizmos(SortedList<double, TransformSnapshot> buffer)
TransformSnapshot entry = buffer.Values[i];
bool oldEnough = entry.localTime <= threshold;
Gizmos.color = oldEnough ? oldEnoughColor : notOldEnoughColor;
Gizmos.DrawCube(entry.position, Vector3.one);
Gizmos.DrawWireCube(entry.position, Vector3.one);
}
// extra: lines between start<->position<->goal

View File

@ -18,11 +18,6 @@ public class NetworkTransformReliable : NetworkTransformBase
[Tooltip("If we only sync on change, then we need to correct old snapshots if more time than sendInterval * multiplier has elapsed.\n\nOtherwise the first move will always start interpolating from the last move sequence's time, which will make it stutter when starting every time.")]
public float onlySyncOnChangeCorrectionMultiplier = 2;
[Header("Send Interval Multiplier")]
[Tooltip("Check/Sync every multiple of Network Manager send interval (= 1 / NM Send Rate), instead of every send interval.")]
[Range(1, 120)]
public uint sendIntervalMultiplier = 3;
[Header("Rotation")]
[Tooltip("Sensitivity of changes needed before an updated state is sent over the network")]
public float rotationSensitivity = 0.01f;
@ -42,26 +37,6 @@ public class NetworkTransformReliable : NetworkTransformBase
[Range(0.00_01f, 1f)] // disallow 0 division. 1mm to 1m precision is enough range.
public float scalePrecision = 0.01f; // 1 cm
[Header("Snapshot Interpolation")]
[Tooltip("Add a small timeline offset to account for decoupled arrival of NetworkTime and NetworkTransform snapshots.\nfixes: https://github.com/MirrorNetworking/Mirror/issues/3427")]
public bool timelineOffset = false;
// Ninja's Notes on offset & mulitplier:
//
// In a no multiplier scenario:
// 1. Snapshots are sent every frame (frame being 1 NM send interval).
// 2. Time Interpolation is set to be 'behind' by 2 frames times.
// In theory where everything works, we probably have around 2 snapshots before we need to interpolate snapshots. From NT perspective, we should always have around 2 snapshots ready, so no stutter.
//
// In a multiplier scenario:
// 1. Snapshots are sent every 10 frames.
// 2. Time Interpolation remains 'behind by 2 frames'.
// When everything works, we are receiving NT snapshots every 10 frames, but start interpolating after 2.
// Even if I assume we had 2 snapshots to begin with to start interpolating (which we don't), by the time we reach 13th frame, we are out of snapshots, and have to wait 7 frames for next snapshot to come. This is the reason why we absolutely need the timestamp adjustment. We are starting way too early to interpolate.
//
double timeStampAdjustment => NetworkServer.sendInterval * (sendIntervalMultiplier - 1);
double offset => timelineOffset ? NetworkServer.sendInterval * sendIntervalMultiplier : 0;
// delta compression needs to remember 'last' to compress against
protected Vector3Long lastSerializedPosition = Vector3Long.zero;
protected Vector3Long lastDeserializedPosition = Vector3Long.zero;
@ -343,9 +318,9 @@ protected virtual void OnClientToServerSync(Vector3? position, Quaternion? rotat
connectionToClient.remoteTimeStamp,
NetworkTime.localTime, // arrival remote timestamp. NOT remote timeline.
NetworkServer.sendInterval * sendIntervalMultiplier, // Unity 2019 doesn't have timeAsDouble yet
target.localPosition,
target.localRotation,
target.localScale);
GetPosition(),
GetRotation(),
GetScale());
}
// add a small timeline offset to account for decoupled arrival of
@ -371,9 +346,9 @@ protected virtual void OnServerToClientSync(Vector3? position, Quaternion? rotat
NetworkClient.connection.remoteTimeStamp, // arrival remote timestamp. NOT remote timeline.
NetworkTime.localTime, // Unity 2019 doesn't have timeAsDouble yet
NetworkClient.sendInterval * sendIntervalMultiplier,
target.localPosition,
target.localRotation,
target.localScale);
GetPosition(),
GetRotation(),
GetScale());
}
// add a small timeline offset to account for decoupled arrival of
@ -417,15 +392,22 @@ static void RewriteHistory(
// insert a fake one at where we used to be,
// 'sendInterval' behind the new one.
SnapshotInterpolation.InsertIfNotExists(snapshots, new TransformSnapshot(
remoteTimeStamp - sendInterval, // arrival remote timestamp. NOT remote time.
localTime - sendInterval, // Unity 2019 doesn't have timeAsDouble yet
position,
rotation,
scale
));
SnapshotInterpolation.InsertIfNotExists(
snapshots,
NetworkClient.snapshotSettings.bufferLimit,
new TransformSnapshot(
remoteTimeStamp - sendInterval, // arrival remote timestamp. NOT remote time.
localTime - sendInterval, // Unity 2019 doesn't have timeAsDouble yet
position,
rotation,
scale
)
);
}
// 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()
{
base.Reset();

View File

@ -1,22 +1,23 @@
// NetworkTransform V2 by mischa (2021-07)
// comment out the below line to quickly revert the onlySyncOnChange feature
#define onlySyncOnChange_BANDWIDTH_SAVING
using UnityEngine;
namespace Mirror
{
[AddComponentMenu("Network/Network Transform (Unreliable)")]
public class NetworkTransform : NetworkTransformBase
public class NetworkTransformUnreliable : NetworkTransformBase
{
// only sync when changed hack /////////////////////////////////////////
#if onlySyncOnChange_BANDWIDTH_SAVING
[Header("Sync Only If Changed")]
[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;
// 3 was original, but testing under really bad network conditions, 2%-5% packet loss and 250-1200ms ping, 5 proved to eliminate any twitching.
[Tooltip("How much time, as a multiple of send interval, has passed before clearing buffers.")]
public float bufferResetMultiplier = 5;
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;
[Header("Sensitivity"), Tooltip("Sensitivity of changes needed before an updated state is sent over the network")]
public float positionSensitivity = 0.01f;
@ -31,35 +32,6 @@ public class NetworkTransform : NetworkTransformBase
protected TransformSnapshot lastSnapshot;
protected bool cachedSnapshotComparison;
protected bool hasSentUnchangedPosition;
#endif
double lastClientSendTime;
double lastServerSendTime;
[Header("Send Interval Multiplier")]
[Tooltip("Check/Sync every multiple of Network Manager send interval (= 1 / NM Send Rate), instead of every send interval.")]
[Range(1, 120)]
const uint sendIntervalMultiplier = 1; // not implemented yet
[Header("Snapshot Interpolation")]
[Tooltip("Add a small timeline offset to account for decoupled arrival of NetworkTime and NetworkTransform snapshots.\nfixes: https://github.com/MirrorNetworking/Mirror/issues/3427")]
public bool timelineOffset = false;
// Ninja's Notes on offset & mulitplier:
//
// In a no multiplier scenario:
// 1. Snapshots are sent every frame (frame being 1 NM send interval).
// 2. Time Interpolation is set to be 'behind' by 2 frames times.
// In theory where everything works, we probably have around 2 snapshots before we need to interpolate snapshots. From NT perspective, we should always have around 2 snapshots ready, so no stutter.
//
// In a multiplier scenario:
// 1. Snapshots are sent every 10 frames.
// 2. Time Interpolation remains 'behind by 2 frames'.
// When everything works, we are receiving NT snapshots every 10 frames, but start interpolating after 2.
// Even if I assume we had 2 snapshots to begin with to start interpolating (which we don't), by the time we reach 13th frame, we are out of snapshots, and have to wait 7 frames for next snapshot to come. This is the reason why we absolutely need the timestamp adjustment. We are starting way too early to interpolate.
//
double timeStampAdjustment => NetworkServer.sendInterval * (sendIntervalMultiplier - 1);
double offset => timelineOffset ? NetworkServer.sendInterval * sendIntervalMultiplier : 0;
// update //////////////////////////////////////////////////////////////
// Update applies interpolation
@ -87,6 +59,22 @@ void LateUpdate()
else if (isClient && IsClientWithAuthority) UpdateClientBroadcast();
}
protected virtual void CheckLastSendTime()
{
// We check interval every frame, and then send if interval is reached.
// So by the time sendIntervalCounter == sendIntervalMultiplier, data is sent,
// thus we reset the counter here.
// This fixes previous issue of, if sendIntervalMultiplier = 1, we send every frame,
// because intervalCounter is always = 1 in the previous version.
if (sendIntervalCounter == sendIntervalMultiplier)
sendIntervalCounter = 0;
// timeAsDouble not available in older Unity versions.
if (AccurateInterval.Elapsed(NetworkTime.localTime, NetworkServer.sendInterval, ref lastSendIntervalTime))
sendIntervalCounter++;
}
void UpdateServerBroadcast()
{
// broadcast to all clients each 'sendInterval'
@ -119,35 +107,36 @@ void UpdateServerBroadcast()
// authoritative movement done by the host will have to be broadcasted
// here by checking IsClientWithAuthority.
// TODO send same time that NetworkServer sends time snapshot?
if (NetworkTime.localTime >= lastServerSendTime + NetworkServer.sendInterval && // same interval as time interpolation!
CheckLastSendTime();
if (sendIntervalCounter == sendIntervalMultiplier && // same interval as time interpolation!
(syncDirection == SyncDirection.ServerToClient || IsClientWithAuthority))
{
// send snapshot without timestamp.
// receiver gets it from batch timestamp to save bandwidth.
TransformSnapshot snapshot = Construct();
#if onlySyncOnChange_BANDWIDTH_SAVING
cachedSnapshotComparison = CompareSnapshots(snapshot);
if (cachedSnapshotComparison && hasSentUnchangedPosition && onlySyncOnChange) { return; }
#endif
#if onlySyncOnChange_BANDWIDTH_SAVING
RpcServerToClientSync(
if (compressRotation)
{
RpcServerToClientSyncCompressRotation(
// only sync what the user wants to sync
syncPosition && positionChanged ? snapshot.position : default(Vector3?),
syncRotation && rotationChanged ? Compression.CompressQuaternion(snapshot.rotation) : default(uint?),
syncScale && scaleChanged ? snapshot.scale : default(Vector3?)
);
}
else
{
RpcServerToClientSync(
// only sync what the user wants to sync
syncPosition && positionChanged ? snapshot.position : default(Vector3?),
syncRotation && rotationChanged ? snapshot.rotation : default(Quaternion?),
syncScale && scaleChanged ? snapshot.scale : default(Vector3?)
);
#else
RpcServerToClientSync(
// only sync what the user wants to sync
syncPosition ? snapshot.position : default(Vector3?),
syncRotation ? snapshot.rotation : default(Quaternion?),
syncScale ? snapshot.scale : default(Vector3?)
);
#endif
);
}
lastServerSendTime = NetworkTime.localTime;
#if onlySyncOnChange_BANDWIDTH_SAVING
if (cachedSnapshotComparison)
{
hasSentUnchangedPosition = true;
@ -157,7 +146,6 @@ void UpdateServerBroadcast()
hasSentUnchangedPosition = false;
lastSnapshot = snapshot;
}
#endif
}
}
@ -217,34 +205,34 @@ void UpdateClientBroadcast()
// DO NOT send nulls if not changed 'since last send' either. we
// send unreliable and don't know which 'last send' the other end
// received successfully.
if (NetworkTime.localTime >= lastClientSendTime + NetworkClient.sendInterval) // same interval as time interpolation!
CheckLastSendTime();
if (sendIntervalCounter == sendIntervalMultiplier) // same interval as time interpolation!
{
// send snapshot without timestamp.
// receiver gets it from batch timestamp to save bandwidth.
TransformSnapshot snapshot = Construct();
#if onlySyncOnChange_BANDWIDTH_SAVING
cachedSnapshotComparison = CompareSnapshots(snapshot);
if (cachedSnapshotComparison && hasSentUnchangedPosition && onlySyncOnChange) { return; }
#endif
#if onlySyncOnChange_BANDWIDTH_SAVING
CmdClientToServerSync(
// only sync what the user wants to sync
syncPosition && positionChanged ? snapshot.position : default(Vector3?),
syncRotation && rotationChanged ? snapshot.rotation : default(Quaternion?),
syncScale && scaleChanged ? snapshot.scale : default(Vector3?)
);
#else
CmdClientToServerSync(
// only sync what the user wants to sync
syncPosition ? snapshot.position : default(Vector3?),
syncRotation ? snapshot.rotation : default(Quaternion?),
syncScale ? snapshot.scale : default(Vector3?)
);
#endif
if (compressRotation)
{
CmdClientToServerSyncCompressRotation(
// only sync what the user wants to sync
syncPosition && positionChanged ? snapshot.position : default(Vector3?),
syncRotation && rotationChanged ? Compression.CompressQuaternion(snapshot.rotation) : default(uint?),
syncScale && scaleChanged ? snapshot.scale : default(Vector3?)
);
}
else
{
CmdClientToServerSync(
// only sync what the user wants to sync
syncPosition && positionChanged ? snapshot.position : default(Vector3?),
syncRotation && rotationChanged ? snapshot.rotation : default(Quaternion?),
syncScale && scaleChanged ? snapshot.scale : default(Vector3?)
);
}
lastClientSendTime = NetworkTime.localTime;
#if onlySyncOnChange_BANDWIDTH_SAVING
if (cachedSnapshotComparison)
{
hasSentUnchangedPosition = true;
@ -254,7 +242,6 @@ void UpdateClientBroadcast()
hasSentUnchangedPosition = false;
lastSnapshot = snapshot;
}
#endif
}
}
@ -284,9 +271,9 @@ public override void OnSerialize(NetworkWriter writer, bool initialState)
// (Spawn message wouldn't sync NTChild positions either)
if (initialState)
{
if (syncPosition) writer.WriteVector3(target.localPosition);
if (syncRotation) writer.WriteQuaternion(target.localRotation);
if (syncScale) writer.WriteVector3(target.localScale);
if (syncPosition) writer.WriteVector3(GetPosition());
if (syncRotation) writer.WriteQuaternion(GetRotation());
if (syncScale) writer.WriteVector3(GetScale());
}
}
@ -297,13 +284,12 @@ public override void OnDeserialize(NetworkReader reader, bool initialState)
// (Spawn message wouldn't sync NTChild positions either)
if (initialState)
{
if (syncPosition) target.localPosition = reader.ReadVector3();
if (syncRotation) target.localRotation = reader.ReadQuaternion();
if (syncScale) target.localScale = reader.ReadVector3();
if (syncPosition) SetPosition(reader.ReadVector3());
if (syncRotation) SetRotation(reader.ReadQuaternion());
if (syncScale) SetScale(reader.ReadVector3());
}
}
#if onlySyncOnChange_BANDWIDTH_SAVING
// Returns true if position, rotation AND scale are unchanged, within given sensitivity range.
protected virtual bool CompareSnapshots(TransformSnapshot currentSnapshot)
{
@ -313,7 +299,7 @@ protected virtual bool CompareSnapshots(TransformSnapshot currentSnapshot)
return (!positionChanged && !rotationChanged && !scaleChanged);
}
#endif
// cmd /////////////////////////////////////////////////////////////////
// only unreliable. see comment above of this file.
[Command(channel = Channels.Unreliable)]
@ -323,9 +309,19 @@ void CmdClientToServerSync(Vector3? position, Quaternion? rotation, Vector3? sca
//For client authority, immediately pass on the client snapshot to all other
//clients instead of waiting for server to send its snapshots.
if (syncDirection == SyncDirection.ClientToServer)
{
RpcServerToClientSync(position, rotation, scale);
}
}
// cmd /////////////////////////////////////////////////////////////////
// only unreliable. see comment above of this file.
[Command(channel = Channels.Unreliable)]
void CmdClientToServerSyncCompressRotation(Vector3? position, uint? rotation, Vector3? scale)
{
OnClientToServerSync(position, rotation.HasValue ? Compression.DecompressQuaternion((uint)rotation) : target.rotation, scale);
//For client authority, immediately pass on the client snapshot to all other
//clients instead of waiting for server to send its snapshots.
if (syncDirection == SyncDirection.ClientToServer)
RpcServerToClientSyncCompressRotation(position, rotation, scale);
}
// local authority client sends sync message to server for broadcasting
@ -340,17 +336,15 @@ protected virtual void OnClientToServerSync(Vector3? position, Quaternion? rotat
// only player owned objects (with a connection) can send to
// server. we can get the timestamp from the connection.
double timestamp = connectionToClient.remoteTimeStamp;
#if onlySyncOnChange_BANDWIDTH_SAVING
if (onlySyncOnChange)
{
double timeIntervalCheck = bufferResetMultiplier * NetworkClient.sendInterval;
double timeIntervalCheck = bufferResetMultiplier * sendIntervalMultiplier * NetworkClient.sendInterval;
if (serverSnapshots.Count > 0 && serverSnapshots.Values[serverSnapshots.Count - 1].remoteTime + timeIntervalCheck < timestamp)
{
Reset();
}
}
#endif
AddSnapshot(serverSnapshots, connectionToClient.remoteTimeStamp + timeStampAdjustment + offset, position, rotation, scale);
}
@ -360,6 +354,12 @@ protected virtual void OnClientToServerSync(Vector3? position, Quaternion? rotat
void RpcServerToClientSync(Vector3? position, Quaternion? rotation, Vector3? scale) =>
OnServerToClientSync(position, rotation, scale);
// rpc /////////////////////////////////////////////////////////////////
// only unreliable. see comment above of this file.
[ClientRpc(channel = Channels.Unreliable)]
void RpcServerToClientSyncCompressRotation(Vector3? position, uint? rotation, Vector3? scale) =>
OnServerToClientSync(position, rotation.HasValue ? Compression.DecompressQuaternion((uint)rotation) : target.rotation, scale);
// server broadcasts sync message to all clients
protected virtual void OnServerToClientSync(Vector3? position, Quaternion? rotation, Vector3? scale)
{
@ -379,17 +379,15 @@ protected virtual void OnServerToClientSync(Vector3? position, Quaternion? rotat
// but all of them go through NetworkClient.connection.
// we can get the timestamp from there.
double timestamp = NetworkClient.connection.remoteTimeStamp;
#if onlySyncOnChange_BANDWIDTH_SAVING
if (onlySyncOnChange)
{
double timeIntervalCheck = bufferResetMultiplier * NetworkServer.sendInterval;
double timeIntervalCheck = bufferResetMultiplier * sendIntervalMultiplier * NetworkServer.sendInterval;
if (clientSnapshots.Count > 0 && clientSnapshots.Values[clientSnapshots.Count - 1].remoteTime + timeIntervalCheck < timestamp)
{
Reset();
}
}
#endif
AddSnapshot(clientSnapshots, NetworkClient.connection.remoteTimeStamp + timeStampAdjustment + offset, position, rotation, scale);
}
}

View File

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

View File

@ -29,9 +29,9 @@ public struct TransformSnapshot : Snapshot
// used to know if the first two snapshots are old enough to start.
public double localTime { get; set; }
public Vector3 position;
public Vector3 position;
public Quaternion rotation;
public Vector3 scale;
public Vector3 scale;
public TransformSnapshot(double remoteTime, double localTime, Vector3 position, Quaternion rotation, Vector3 scale)
{
@ -61,5 +61,8 @@ public static TransformSnapshot Interpolate(TransformSnapshot from, TransformSna
Vector3.LerpUnclamped(from.scale, to.scale, (float)t)
);
}
public override string ToString() =>
$"TransformSnapshot(remoteTime={remoteTime:F2}, localTime={localTime:F2}, pos={position}, rot={rotation}, scale={scale})";
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 09cc6745984c453a8cfb4cf4244d2570
timeCreated: 1693576410

View File

@ -0,0 +1,82 @@
%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: GhostMaterial
m_Shader: {fileID: 46, guid: 0000000000000000f000000000000000, type: 0}
m_ValidKeywords:
- _ALPHAPREMULTIPLY_ON
m_InvalidKeywords: []
m_LightmapFlags: 4
m_EnableInstancingVariants: 0
m_DoubleSidedGI: 0
m_CustomRenderQueue: 3000
stringTagMap:
RenderType: Transparent
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: 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: 1, b: 1, a: 0.11764706}
- _EmissionColor: {r: 0, g: 0, b: 0, a: 1}
m_BuildTextureStacks: []

View File

@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 9095a4dceda11a647a2a09eb02873cf2
guid: 411a48b4a197d4924bec3e3809bc9320
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 2100000

View File

@ -0,0 +1,457 @@
// make sure to use a reasonable sync interval.
// for example, correcting every 100ms seems reasonable.
using System;
using System.Collections.Generic;
using UnityEngine;
namespace Mirror
{
struct RigidbodyState : PredictedState
{
public double timestamp { get; 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; // delta to get from last to this position
public Vector3 position;
public Quaternion rotation; // TODO delta rotation?
public Vector3 velocityDelta; // delta to get from last to this velocity
public Vector3 velocity;
public RigidbodyState(
double timestamp,
Vector3 positionDelta, Vector3 position,
Quaternion rotation,
Vector3 velocityDelta, Vector3 velocity)
{
this.timestamp = timestamp;
this.positionDelta = positionDelta;
this.position = position;
this.rotation = rotation;
this.velocityDelta = velocityDelta;
this.velocity = velocity;
}
// adjust the deltas after inserting a correction between this one and the previous one.
public void AdjustDeltas(float multiplier)
{
positionDelta = Vector3.Lerp(Vector3.zero, positionDelta, multiplier);
// TODO if we have have a rotation delta, then scale it here too
velocityDelta = Vector3.Lerp(Vector3.zero, velocityDelta, multiplier);
}
public static RigidbodyState Interpolate(RigidbodyState a, RigidbodyState b, float t)
{
return new RigidbodyState
{
position = Vector3.Lerp(a.position, b.position, t),
rotation = Quaternion.Slerp(a.rotation, b.rotation, t),
velocity = Vector3.Lerp(a.velocity, b.velocity, t)
};
}
}
public enum CorrectionMode
{
Set, // rigidbody.position/rotation = ...
Move, // rigidbody.MovePosition/Rotation
}
[Obsolete("Prediction is under development, do not use this yet.")]
[RequireComponent(typeof(Rigidbody))]
public class PredictedRigidbody : NetworkBehaviour
{
Rigidbody rb;
Vector3 lastPosition;
// [Tooltip("Broadcast changes if position changed by more than ... meters.")]
// public float positionSensitivity = 0.01f;
// client keeps state history for correction & reconciliation
[Header("State History")]
public int stateHistoryLimit = 32; // 32 x 50 ms = 1.6 seconds is definitely enough
readonly SortedList<double, RigidbodyState> stateHistory = new SortedList<double, RigidbodyState>();
[Header("Reconciliation")]
[Tooltip("Correction threshold in meters. For example, 0.1 means that if the client is off by more than 10cm, it gets corrected.")]
public double correctionThreshold = 0.10;
[Tooltip("Applying server corrections one frame ahead gives much better results. We don't know why yet, so this is an option for now.")]
public bool oneFrameAhead = true;
[Header("Smoothing")]
[Tooltip("Configure how to apply the corrected state.")]
public CorrectionMode correctionMode = CorrectionMode.Move;
[Header("Visual Interpolation")]
[Tooltip("After creating the visual interpolation object, keep showing the original Rigidbody with a ghost (transparent) material for debugging.")]
public bool showGhost = true;
[Tooltip("After creating the visual interpolation object, replace this object's renderer materials with the ghost (ideally transparent) material.")]
public Material ghostMaterial;
[Tooltip("How fast to interpolate to the target position, relative to how far we are away from it.\nHigher value will be more jitter but sharper moves, lower value will be less jitter but a little too smooth / rounded moves.")]
public float interpolationSpeed = 15; // 10 is a little too low for billiards at least
[Tooltip("Teleport if we are further than 'multiplier x collider size' behind.")]
public float teleportDistanceMultiplier = 10;
[Header("Debugging")]
public float lineTime = 10;
// visually interpolated GameObject copy for smoothing
protected GameObject visualCopy;
void Awake()
{
rb = GetComponent<Rigidbody>();
}
// instantiate a visually-only copy of the gameobject to apply smoothing.
// on clients, where players are watching.
// create & destroy methods are virtual so games with a different
// rendering setup / hierarchy can inject their own copying code here.
protected virtual void CreateVisualCopy()
{
// create an empty GameObject with the same name + _Visual
visualCopy = new GameObject($"{name}_Visual");
visualCopy.transform.position = transform.position;
visualCopy.transform.rotation = transform.rotation;
visualCopy.transform.localScale = transform.localScale;
// add the PredictedRigidbodyVisual component
PredictedRigidbodyVisual visualRigidbody = visualCopy.AddComponent<PredictedRigidbodyVisual>();
visualRigidbody.target = this;
visualRigidbody.interpolationSpeed = interpolationSpeed;
visualRigidbody.teleportDistanceMultiplier = teleportDistanceMultiplier;
// copy the rendering components
if (GetComponent<MeshRenderer>() != null)
{
MeshFilter meshFilter = visualCopy.AddComponent<MeshFilter>();
meshFilter.mesh = GetComponent<MeshFilter>().mesh;
MeshRenderer meshRenderer = visualCopy.AddComponent<MeshRenderer>();
meshRenderer.material = GetComponent<MeshRenderer>().material;
}
// if we didn't find a renderer, show a warning
else Debug.LogWarning($"PredictedRigidbody: {name} found no renderer to copy onto the visual object. If you are using a custom setup, please overwrite PredictedRigidbody.CreateVisualCopy().");
// replace this renderer's materials with the ghost (if enabled)
foreach (Renderer rend in GetComponentsInChildren<Renderer>())
{
if (showGhost)
{
rend.material = ghostMaterial;
}
else
{
rend.enabled = false;
}
}
}
protected virtual void DestroyVisualCopy()
{
if (visualCopy != null) Destroy(visualCopy);
}
// creater visual copy only on clients, where players are watching.
public override void OnStartClient() => CreateVisualCopy();
// destroy visual copy only in OnStopClient().
// OnDestroy() wouldn't be called for scene objects that are only disabled instead of destroyed.
public override void OnStopClient() => DestroyVisualCopy();
void UpdateServer()
{
// to save bandwidth, we only serialize when position changed
// if (Vector3.Distance(transform.position, lastPosition) >= positionSensitivity)
// {
// lastPosition = transform.position;
// SetDirty();
// }
// always set dirty to always serialize.
// fixes issues where an object was idle and stopped serializing on server,
// even though it was still moving on client.
// hence getting totally out of sync.
SetDirty();
}
void Update()
{
if (isServer) UpdateServer();
}
void FixedUpdate()
{
// record client state every FixedUpdate
if (isClient) RecordState();
}
void ApplyState(Vector3 position, Quaternion rotation, Vector3 velocity)
{
// Rigidbody .position teleports, while .MovePosition interpolates
// TODO is this a good idea? what about next capture while it's interpolating?
if (correctionMode == CorrectionMode.Move)
{
rb.MovePosition(position);
rb.MoveRotation(rotation);
}
else if (correctionMode == CorrectionMode.Set)
{
rb.position = position;
rb.rotation = rotation;
}
rb.velocity = velocity;
}
// record state at NetworkTime.time on client
void RecordState()
{
// NetworkTime.time is always behind by bufferTime.
// prediction aims to be on the exact same server time (immediately).
// use predictedTime to record state, otherwise we would record in the past.
double predictedTime = NetworkTime.predictedTime;
// TODO FixedUpdate may run twice in the same frame / NetworkTime.time.
// for now, simply don't record if already recorded there.
if (stateHistory.ContainsKey(predictedTime))
return;
// keep state history within limit
if (stateHistory.Count >= stateHistoryLimit)
stateHistory.RemoveAt(0);
// calculate delta to previous state (if any)
Vector3 positionDelta = Vector3.zero;
Vector3 velocityDelta = Vector3.zero;
if (stateHistory.Count > 0)
{
RigidbodyState last = stateHistory.Values[stateHistory.Count - 1];
positionDelta = rb.position - last.position;
velocityDelta = rb.velocity - last.velocity;
// debug draw the recorded state
Debug.DrawLine(last.position, rb.position, Color.red, lineTime);
}
// add state to history
stateHistory.Add(
predictedTime,
new RigidbodyState(
predictedTime,
positionDelta, rb.position,
rb.rotation,
velocityDelta, rb.velocity)
);
}
void ApplyCorrection(RigidbodyState corrected, RigidbodyState before, RigidbodyState after)
{
// TODO merge this with CompareState iteration!
// first, remember the delta between last recorded state and current live state.
// before we potentially correct 'last' in history.
// TODO we always record the current state in CompareState now.
// applying live delta may not be necessary anymore.
// this should always be '0' now.
// RigidbodyState newest = stateHistory.Values[stateHistory.Count - 1];
// Vector3 livePositionDelta = rb.position - newest.position;
// Vector3 liveVelocityDelta = rb.velocity - newest.velocity;
// TODO rotation delta?
// insert the corrected state and adjust 'after.delta' to the inserted.
Prediction.InsertCorrection(stateHistory, stateHistoryLimit, corrected, before, after);
// show the received correction position + velocity for debugging.
// helps to compare with the interpolated/applied correction locally.
// TODO don't hardcode length?
Debug.DrawLine(corrected.position, corrected.position + corrected.velocity * 0.1f, Color.white, lineTime);
// now go through the history:
// 1. skip all states before the inserted / corrected entry
// 3. apply all deltas after timestamp
// 4. recalculate corrected position based on inserted + sum(deltas)
// 5. apply rigidbody correction
RigidbodyState last = corrected;
int correctedCount = 0; // for debugging
for (int i = 0; i < stateHistory.Count; ++i)
{
double key = stateHistory.Keys[i];
RigidbodyState entry = stateHistory.Values[i];
// skip all states before (and including) the corrected entry
// TODO InsertCorrection() above should return the inserted index to skip faster.
if (key <= corrected.timestamp)
continue;
// this state is after the inserted state.
// correct it's absolute position based on last + delta.
entry.position = last.position + entry.positionDelta;
// TODO rotation
entry.velocity = last.velocity + entry.velocityDelta;
// save the corrected entry into history.
// if we don't, then corrections for [i+1] would compare the
// uncorrected state and attempt to correct again, resulting in
// noticeable jitter and displacements.
//
// not saving it would also result in objects flying towards
// infinity when using sendInterval = 0.
stateHistory[entry.timestamp] = entry;
// debug draw the corrected state
// Debug.DrawLine(last.position, entry.position, Color.cyan, lineTime);
// save last
last = entry;
correctedCount += 1;
}
// log, draw & apply the final position.
// always do this here, not when iterating above, in case we aren't iterating.
// for example, on same machine with near zero latency.
Debug.Log($"Correcting {name}: {correctedCount} / {stateHistory.Count} states to final position from: {rb.position} to: {last.position}");
Debug.DrawLine(rb.position, last.position, Color.green, lineTime);
ApplyState(last.position, last.rotation, last.velocity);
}
// compare client state with server state at timestamp.
// apply correction if necessary.
void CompareState(double timestamp, RigidbodyState state)
{
// we only capture state every 'interval' milliseconds.
// so the newest entry in 'history' may be up to 'interval' behind 'now'.
// if there's no latency, we may receive a server state for 'now'.
// sampling would fail, if we haven't recorded anything in a while.
// to solve this, always record the current state when receiving a server state.
RecordState();
// find the two closest client states between timestamp
if (!Prediction.Sample(stateHistory, timestamp, out RigidbodyState before, out RigidbodyState after, out double t))
{
// if we failed to sample, that could indicate a problem.
// first, if the client didn't record 'limit' entries yet, then
// let it keep recording. it'll be fine.
if (stateHistory.Count < stateHistoryLimit) return;
// if we are already at the recording limit and still can't
// sample, then that's a problem.
// there are two cases to consider.
RigidbodyState oldest = stateHistory.Values[0];
RigidbodyState newest = stateHistory.Values[stateHistory.Count - 1];
// is the state older than the oldest state in history?
// this can happen if the client gets so far behind the server
// that it doesn't have a recored history to sample from.
// in that case, we should hard correct the client.
// otherwise it could be out of sync as long as it's too far behind.
if (state.timestamp < oldest.timestamp)
{
Debug.LogWarning($"Hard correcting client because the client is too far behind the server. History of size={stateHistory.Count} @ t={timestamp:F3} oldest={oldest.timestamp:F3} newest={newest.timestamp:F3}. This would cause the client to be out of sync as long as it's behind.");
ApplyCorrection(state, state, state);
}
// is it newer than the newest state in history?
// this can happen if client's predictedTime predicts too far ahead of the server.
// in that case, log a warning for now but still apply the correction.
// otherwise it could be out of sync as long as it's too far ahead.
//
// for example, when running prediction on the same machine with near zero latency.
// when applying corrections here, this looks just fine on the local machine.
else if (newest.timestamp < state.timestamp)
{
// the correction is for a state in the future.
// we clamp it to 'now'.
// but only correct if off by threshold.
// TODO maybe we should interpolate this back to 'now'?
if (Vector3.Distance(state.position, rb.position) >= correctionThreshold)
{
double ahead = state.timestamp - newest.timestamp;
Debug.Log($"Hard correction because the client is ahead of the server by {(ahead*1000):F1}ms. History of size={stateHistory.Count} @ t={timestamp:F3} oldest={oldest.timestamp:F3} newest={newest.timestamp:F3}. This can happen when latency is near zero, and is fine unless it shows jitter.");
ApplyCorrection(state, state, state);
}
}
// otherwise something went very wrong. sampling should've worked.
// hard correct to recover the error.
else
{
// TODO
Debug.LogError($"Failed to sample history of size={stateHistory.Count} @ t={timestamp:F3} oldest={oldest.timestamp:F3} newest={newest.timestamp:F3}. This should never happen because the timestamp is within history.");
ApplyCorrection(state, state, state);
}
// either way, nothing more to do here
return;
}
// interpolate between them to get the best approximation
RigidbodyState interpolated = RigidbodyState.Interpolate(before, after, (float)t);
// calculate the difference between where we were and where we should be
// TODO only position for now. consider rotation etc. too later
float difference = Vector3.Distance(state.position, interpolated.position);
// Debug.Log($"Sampled history of size={stateHistory.Count} @ {timestamp:F3}: client={interpolated.position} server={state.position} difference={difference:F3} / {correctionThreshold:F3}");
// too far off? then correct it
if (difference >= correctionThreshold)
{
// Debug.Log($"CORRECTION NEEDED FOR {name} @ {timestamp:F3}: client={interpolated.position} server={state.position} difference={difference:F3}");
ApplyCorrection(state, before, after);
}
}
// send state to clients every sendInterval.
// reliable for now.
// TODO we should use the one from FixedUpdate
public override void OnSerialize(NetworkWriter writer, bool initialState)
{
// Time.time was at the beginning of this frame.
// NetworkLateUpdate->Broadcast->OnSerialize is at the end of the frame.
// as result, client should use this to correct the _next_ frame.
// otherwise we see noticeable resets that seem off by one frame.
//
// to solve this, we can send the current deltaTime.
// server is technically supposed to be at a fixed frame rate, but this can vary.
// sending server's current deltaTime is the safest option.
// client then applies it on top of remoteTimestamp.
writer.WriteFloat(Time.deltaTime);
writer.WriteVector3(rb.position);
writer.WriteQuaternion(rb.rotation);
writer.WriteVector3(rb.velocity);
}
// read the server's state, compare with client state & correct if necessary.
public override void OnDeserialize(NetworkReader reader, bool initialState)
{
// deserialize data
// we want to know the time on the server when this was sent, which is remoteTimestamp.
double timestamp = NetworkClient.connection.remoteTimeStamp;
// server send state at the end of the frame.
// parse and apply the server's delta time to our timestamp.
// otherwise we see noticeable resets that seem off by one frame.
double serverDeltaTime = reader.ReadFloat();
timestamp += serverDeltaTime;
// however, adding yet one more frame delay gives much(!) better results.
// we don't know why yet, so keep this as an option for now.
// possibly because client captures at the beginning of the frame,
// with physics happening at the end of the frame?
if (oneFrameAhead) timestamp += serverDeltaTime;
// parse state
Vector3 position = reader.ReadVector3();
Quaternion rotation = reader.ReadQuaternion();
Vector3 velocity = reader.ReadVector3();
// compare state without deltas
CompareState(timestamp, new RigidbodyState(timestamp, Vector3.zero, position, rotation, Vector3.zero, velocity));
}
}
}

View File

@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: d38927cdc6024b9682b5fe9778b9ef99
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences:
- ghostMaterial: {fileID: 2100000, guid: 411a48b4a197d4924bec3e3809bc9320, type: 2}
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:

View File

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

View File

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

View File

@ -133,8 +133,9 @@ void LoadPassword()
}
}
void OnValidate()
protected override void OnValidate()
{
base.OnValidate();
syncMode = SyncMode.Owner;
}

View File

@ -29,8 +29,17 @@ public class Batcher
// they would not contain a timestamp
readonly int threshold;
// TimeStamp header size for those who need it
public const int HeaderSize = sizeof(double);
// TimeStamp header size. each batch has one.
public const int TimestampSize = sizeof(double);
// Message header size. each message has one.
public static int MessageHeaderSize(int messageSize) =>
Compression.VarUIntSize((ulong)messageSize);
// maximum overhead for a single message.
// useful for the outside to calculate max message sizes.
public static int MaxMessageOverhead(int messageSize) =>
TimestampSize + MessageHeaderSize(messageSize);
// full batches ready to be sent.
// DO NOT queue NetworkMessage, it would box.
@ -53,13 +62,17 @@ public Batcher(int threshold)
// caller needs to make sure they are within max packet size.
public void AddMessage(ArraySegment<byte> message, double timeStamp)
{
// predict the needed size, which is varint(size) + content
int headerSize = Compression.VarUIntSize((ulong)message.Count);
int neededSize = headerSize + message.Count;
// when appending to a batch in progress, check final size.
// if it expands beyond threshold, then we should finalize it first.
// => less than or exactly threshold is fine.
// GetBatch() will finalize it.
// => see unit tests.
if (batch != null &&
batch.Position + message.Count > threshold)
batch.Position + neededSize > threshold)
{
batches.Enqueue(batch);
batch = null;
@ -82,6 +95,16 @@ public void AddMessage(ArraySegment<byte> message, double timeStamp)
// -> we do allow > threshold sized messages as single batch
// -> WriteBytes instead of WriteSegment because the latter
// would add a size header. we want to write directly.
//
// include size prefix as varint!
// -> fixes NetworkMessage serialization mismatch corrupting the
// next message in a batch.
// -> a _lot_ of time was wasted debugging corrupt batches.
// no easy way to figure out which NetworkMessage has a mismatch.
// -> this is worth everyone's sanity.
// -> varint means we prefix with 1 byte most of the time.
// -> the same issue in NetworkIdentity was why Mirror started!
Compression.CompressVarUInt(batch, (ulong)message.Count);
batch.WriteBytes(message.Array, message.Offset, message.Count);
}

View File

@ -14,13 +14,13 @@ public class Unbatcher
{
// supporting adding multiple batches before GetNextMessage is called.
// just in case.
Queue<NetworkWriterPooled> batches = new Queue<NetworkWriterPooled>();
readonly Queue<NetworkWriterPooled> batches = new Queue<NetworkWriterPooled>();
public int BatchesCount => batches.Count;
// NetworkReader is only created once,
// then pointed to the first batch.
NetworkReader reader = new NetworkReader(new byte[0]);
readonly NetworkReader reader = new NetworkReader(new byte[0]);
// timestamp that was written into the batch remotely.
// for the batch that our reader is currently pointed at.
@ -48,7 +48,7 @@ public bool AddBatch(ArraySegment<byte> batch)
// don't need to check against that.
// make sure we have at least 8 bytes to read for tick timestamp
if (batch.Count < Batcher.HeaderSize)
if (batch.Count < Batcher.TimestampSize)
return false;
// put into a (pooled) writer
@ -69,43 +69,22 @@ public bool AddBatch(ArraySegment<byte> batch)
}
// get next message, unpacked from batch (if any)
// message ArraySegment is only valid until the next call.
// timestamp is the REMOTE time when the batch was created remotely.
public bool GetNextMessage(out NetworkReader message, out double remoteTimeStamp)
public bool GetNextMessage(out ArraySegment<byte> message, out double remoteTimeStamp)
{
// getting messages would be easy via
// <<size, message, size, message, ...>>
// but to save A LOT of bandwidth, we use
// <<message, message, ...>
// in other words, we don't know where the current message ends
//
// BUT: it doesn't matter!
// -> we simply return the reader
// * if we have one yet
// * and if there's more to read
// -> the caller can then read one message from it
// -> when the end is reached, we retire the batch!
//
// for example:
// while (GetNextMessage(out message))
// ProcessMessage(message);
//
message = null;
message = default;
remoteTimeStamp = 0;
// do nothing if we don't have any batches.
// otherwise the below queue.Dequeue() would throw an
// InvalidOperationException if operating on empty queue.
if (batches.Count == 0)
{
remoteTimeStamp = 0;
return false;
}
// was our reader pointed to anything yet?
if (reader.Capacity == 0)
{
remoteTimeStamp = 0;
return false;
}
// no more data to read?
if (reader.Remaining == 0)
@ -123,19 +102,27 @@ public bool GetNextMessage(out NetworkReader message, out double remoteTimeStamp
StartReadingBatch(next);
}
// otherwise there's nothing more to read
else
{
remoteTimeStamp = 0;
return false;
}
else return false;
}
// use the current batch's remote timestamp
// AFTER potentially moving to the next batch ABOVE!
remoteTimeStamp = readerRemoteTimeStamp;
// if we got here, then we have more data to read.
message = reader;
// enough data to read the size prefix?
if (reader.Remaining == 0)
return false;
// read the size prefix as varint
// see Batcher.AddMessage comments for explanation.
int size = (int)Compression.DecompressVarUInt(reader);
// validate size prefix, in case attackers send malicious data
if (reader.Remaining < size)
return false;
// return the message of size
message = reader.ReadBytesSegment(size);
return true;
}
}

View File

@ -0,0 +1,74 @@
// standalone, Unity-independent connection-quality algorithm & enum.
// don't need to use this directly, it's built into Mirror's NetworkClient.
using UnityEngine;
namespace Mirror
{
public enum ConnectionQuality : byte
{
ESTIMATING, // still estimating
POOR, // unplayable
FAIR, // very noticeable latency, not very enjoyable anymore
GOOD, // very playable for everyone but high level competitors
EXCELLENT // ideal experience for high level competitors
}
public enum ConnectionQualityMethod : byte
{
Simple, // simple estimation based on rtt and jitter
Pragmatic // based on snapshot interpolation adjustment
}
// provide different heuristics for users to choose from.
// simple heuristics to get started.
// this will be iterated on over time based on user feedback.
public static class ConnectionQualityHeuristics
{
// convenience extension to color code Connection Quality
public static Color ColorCode(this ConnectionQuality quality)
{
switch (quality)
{
case ConnectionQuality.POOR: return Color.red;
case ConnectionQuality.FAIR: return new Color(1.0f, 0.647f, 0.0f);
case ConnectionQuality.GOOD: return Color.yellow;
case ConnectionQuality.EXCELLENT: return Color.green;
default: return Color.gray; // ESTIMATING
}
}
// straight forward estimation
// rtt: average round trip time in seconds.
// jitter: average latency variance.
public static ConnectionQuality Simple(double rtt, double jitter)
{
if (rtt <= 0.100 && jitter <= 0.10) return ConnectionQuality.EXCELLENT;
if (rtt <= 0.200 && jitter <= 0.20) return ConnectionQuality.GOOD;
if (rtt <= 0.400 && jitter <= 0.50) return ConnectionQuality.FAIR;
return ConnectionQuality.POOR;
}
// snapshot interpolation based estimation.
// snap. interp. adjusts buffer time based on connection quality.
// based on this, we can measure how far away we are from the ideal.
// the returned quality will always directly correlate with gameplay.
// => requires SnapshotInterpolation dynamicAdjustment to be enabled!
public static ConnectionQuality Pragmatic(double targetBufferTime, double currentBufferTime)
{
// buffer time is set by the game developer.
// estimating in multiples is a great way to be game independent.
// for example, a fast paced shooter and a slow paced RTS will both
// have poor connection if the multiplier is >10.
double multiplier = currentBufferTime / targetBufferTime;
// empirically measured with Tanks demo + LatencySimulation.
// it's not obvious to estimate on paper.
if (multiplier <= 1.15) return ConnectionQuality.EXCELLENT;
if (multiplier <= 1.25) return ConnectionQuality.GOOD;
if (multiplier <= 1.50) return ConnectionQuality.FAIR;
// anything else is poor
return ConnectionQuality.POOR;
}
}
}

View File

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

View File

@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: a99666a026b14cf6ba1a2b65946b1b27
timeCreated: 1615288671

View File

@ -1 +0,0 @@
// moved into NetworkClient on 2021-03-07

View File

@ -1 +0,0 @@
// removed 2021-05-13

View File

@ -1 +0,0 @@
// removed 2021-05-13

View File

@ -1 +0,0 @@
// removed 2021-05-13

View File

@ -1 +0,0 @@
// removed 2021-05-13

View File

@ -1 +0,0 @@
// removed 2021-05-13

View File

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

View File

@ -1 +0,0 @@
// removed 2021-05-13

View File

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

View File

@ -1 +0,0 @@
// removed 2021-05-13

View File

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

View File

@ -1 +0,0 @@
// removed 2021-05-13

View File

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

View File

@ -1 +0,0 @@
// removed 2021-05-13

View File

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

View File

@ -1 +0,0 @@
// removed 2021-05-13

View File

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

View File

@ -1 +0,0 @@
// removed 2021-05-13

View File

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

View File

@ -1 +0,0 @@
// removed 2021-05-13

View File

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

View File

@ -1 +0,0 @@
// removed 2021-05-13

View File

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

View File

@ -1 +0,0 @@
// removed 2021-05-13

View File

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

View File

@ -1 +0,0 @@
// removed 2021-05-13

View File

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

View File

@ -1 +0,0 @@
// removed 2021-05-13

View File

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

View File

@ -1 +0,0 @@
// removed 2021-05-13

View File

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

View File

@ -1 +0,0 @@
// removed 2021-05-13

View File

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

View File

@ -1 +0,0 @@
// removed 2021-05-13

View File

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

View File

@ -1 +0,0 @@
// removed 2021-05-13

View File

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

View File

@ -1 +0,0 @@
// removed 2021-05-13

View File

@ -1 +0,0 @@
// removed 2021-05-13

View File

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

View File

@ -1 +0,0 @@
// removed 2021-05-13

View File

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

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