mirror of
https://github.com/MirrorNetworking/Mirror.git
synced 2024-11-18 02:50:32 +00:00
Merged master
This commit is contained in:
commit
8e60e28ca3
74
.github/CreateRelease.csx
vendored
Normal file
74
.github/CreateRelease.csx
vendored
Normal 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
113
.github/ModPreprocessorDefine.csx
vendored
Normal 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
46
.github/workflows/CreateRelease.yml
vendored
Normal 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 }}
|
9
.github/workflows/RunUnityTests.yml
vendored
9
.github/workflows/RunUnityTests.yml
vendored
@ -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.
|
||||
|
2
.github/workflows/Semantic.yml
vendored
2
.github/workflows/Semantic.yml
vendored
@ -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
|
||||
|
22
.github/workflows/main.yml
vendored
22
.github/workflows/main.yml
vendored
@ -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
1
.gitignore
vendored
@ -16,6 +16,7 @@ UserSettings/Search.settings
|
||||
# ===================================== #
|
||||
Database.sqlite
|
||||
Database/
|
||||
Builds/
|
||||
|
||||
# ===================================== #
|
||||
# Visual Studio / MonoDevelop / Rider #
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,7 @@
|
||||
namespace Mirror.Discovery
|
||||
{
|
||||
[Serializable]
|
||||
public class ServerFoundUnityEvent : UnityEvent<ServerResponse> {};
|
||||
public class ServerFoundUnityEvent<TResponseType> : UnityEvent<TResponseType> {};
|
||||
|
||||
[DisallowMultipleComponent]
|
||||
[AddComponentMenu("Network/Network Discovery")]
|
||||
|
@ -40,12 +40,12 @@ public abstract class NetworkDiscoveryBase<Request, Response> : MonoBehaviour
|
||||
[Tooltip("Time in seconds between multi-cast messages")]
|
||||
[Range(1, 60)]
|
||||
float ActiveDiscoveryInterval = 3;
|
||||
|
||||
|
||||
[Tooltip("Transport to be advertised during discovery")]
|
||||
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()
|
||||
@ -271,7 +272,7 @@ void BeginMulticastLock()
|
||||
{
|
||||
#if UNITY_ANDROID
|
||||
if (hasMulticastLock) return;
|
||||
|
||||
|
||||
if (Application.platform == RuntimePlatform.Android)
|
||||
{
|
||||
using (AndroidJavaObject activity = new AndroidJavaClass("com.unity3d.player.UnityPlayer").GetStatic<AndroidJavaObject>("currentActivity"))
|
||||
@ -291,7 +292,7 @@ void EndpMulticastLock()
|
||||
{
|
||||
#if UNITY_ANDROID
|
||||
if (!hasMulticastLock) return;
|
||||
|
||||
|
||||
multicastLock?.Call("release");
|
||||
hasMulticastLock = false;
|
||||
#endif
|
||||
@ -348,16 +349,16 @@ public void StopDiscovery()
|
||||
/// <returns>ClientListenAsync Task</returns>
|
||||
public async Task ClientListenAsync()
|
||||
{
|
||||
// while clientUpdClient to fix:
|
||||
// while clientUpdClient to fix:
|
||||
// https://github.com/vis2k/Mirror/pull/2908
|
||||
//
|
||||
// If, you cancel discovery the clientUdpClient is set to null.
|
||||
// However, nothing cancels ClientListenAsync. If we change the if(true)
|
||||
// to check if the client is null. You can properly cancel the discovery,
|
||||
// to check if the client is null. You can properly cancel the discovery,
|
||||
// and kill the listen thread.
|
||||
//
|
||||
// Prior to this fix, if you cancel the discovery search. It crashes the
|
||||
// thread, and is super noisy in the output. As well as causes issues on
|
||||
// Prior to this fix, if you cancel the discovery search. It crashes the
|
||||
// thread, and is super noisy in the output. As well as causes issues on
|
||||
// the quest.
|
||||
while (clientUdpClient != null)
|
||||
{
|
||||
@ -392,7 +393,7 @@ public void BroadcastDiscoveryRequest()
|
||||
}
|
||||
|
||||
IPEndPoint endPoint = new IPEndPoint(IPAddress.Broadcast, serverBroadcastListenPort);
|
||||
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(BroadcastAddress))
|
||||
{
|
||||
try
|
||||
|
@ -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>();
|
||||
}
|
||||
|
@ -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>();
|
||||
}
|
||||
|
@ -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>();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 63d647500ca1bfa4a845bc1f4cff9dcc
|
||||
guid: 00ac1d0527f234939aba22b4d7cbf280
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
107
Assets/Mirror/Components/LagCompensation/HistoryCollider.cs
Normal file
107
Assets/Mirror/Components/LagCompensation/HistoryCollider.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 96fc7967f813e4960b9119d7c2118494
|
||||
guid: f5f2158d9776d4b569858f793be4da60
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
@ -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
|
||||
{
|
||||
|
30
Assets/Mirror/Components/NetworkDiagnosticsDebugger.cs
Normal file
30
Assets/Mirror/Components/NetworkDiagnosticsDebugger.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bc9f0a0fe4124424b8f9d4927795ee01
|
||||
timeCreated: 1700945893
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
3
Assets/Mirror/Components/NetworkRigidbody.meta
Normal file
3
Assets/Mirror/Components/NetworkRigidbody.meta
Normal file
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 80106690aef541a5b8e2f8fb3d5949ad
|
||||
timeCreated: 1686733778
|
@ -0,0 +1,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 330c9aab13d2d42069c6ebbe582b73ca
|
||||
guid: cb803efbe62c34d7baece46c9ffebad9
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f6928b080072948f7b2909b4025fcc79
|
||||
guid: 7ec4f7556ca1e4b55a3381fc6a02b1bc
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c7627623f2b9fad4484082517cd73e67
|
||||
guid: 3b20dc110904e47f8a154cdcf6433eae
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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:
|
@ -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
|
||||
|
@ -41,7 +41,7 @@ public class NetworkRoomPlayer : NetworkBehaviour
|
||||
/// <summary>
|
||||
/// Do not use Start - Override OnStartHost / OnStartClient instead!
|
||||
/// </summary>
|
||||
public void Start()
|
||||
public virtual void Start()
|
||||
{
|
||||
if (NetworkManager.singleton is NetworkRoomManager room)
|
||||
{
|
||||
|
@ -0,0 +1,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 {}
|
||||
}
|
@ -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
|
@ -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();
|
@ -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);
|
||||
}
|
||||
}
|
@ -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:
|
@ -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})";
|
||||
}
|
||||
}
|
3
Assets/Mirror/Components/PredictedRigidbody.meta
Normal file
3
Assets/Mirror/Components/PredictedRigidbody.meta
Normal file
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 09cc6745984c453a8cfb4cf4244d2570
|
||||
timeCreated: 1693576410
|
@ -0,0 +1,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: []
|
@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9095a4dceda11a647a2a09eb02873cf2
|
||||
guid: 411a48b4a197d4924bec3e3809bc9320
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 2100000
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
@ -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:
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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:
|
@ -133,8 +133,9 @@ void LoadPassword()
|
||||
}
|
||||
}
|
||||
|
||||
void OnValidate()
|
||||
protected override void OnValidate()
|
||||
{
|
||||
base.OnValidate();
|
||||
syncMode = SyncMode.Owner;
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -14,13 +14,13 @@ public class Unbatcher
|
||||
{
|
||||
// supporting adding multiple batches before GetNextMessage is called.
|
||||
// just in case.
|
||||
Queue<NetworkWriterPooled> batches = new Queue<NetworkWriterPooled>();
|
||||
readonly Queue<NetworkWriterPooled> batches = new Queue<NetworkWriterPooled>();
|
||||
|
||||
public int BatchesCount => batches.Count;
|
||||
|
||||
// NetworkReader is only created once,
|
||||
// then pointed to the first batch.
|
||||
NetworkReader reader = new NetworkReader(new byte[0]);
|
||||
readonly NetworkReader reader = new NetworkReader(new byte[0]);
|
||||
|
||||
// timestamp that was written into the batch remotely.
|
||||
// for the batch that our reader is currently pointed at.
|
||||
@ -48,7 +48,7 @@ public bool AddBatch(ArraySegment<byte> batch)
|
||||
// don't need to check against that.
|
||||
|
||||
// make sure we have at least 8 bytes to read for tick timestamp
|
||||
if (batch.Count < Batcher.HeaderSize)
|
||||
if (batch.Count < Batcher.TimestampSize)
|
||||
return false;
|
||||
|
||||
// put into a (pooled) writer
|
||||
@ -69,43 +69,22 @@ public bool AddBatch(ArraySegment<byte> batch)
|
||||
}
|
||||
|
||||
// get next message, unpacked from batch (if any)
|
||||
// message ArraySegment is only valid until the next call.
|
||||
// timestamp is the REMOTE time when the batch was created remotely.
|
||||
public bool GetNextMessage(out NetworkReader message, out double remoteTimeStamp)
|
||||
public bool GetNextMessage(out ArraySegment<byte> message, out double remoteTimeStamp)
|
||||
{
|
||||
// getting messages would be easy via
|
||||
// <<size, message, size, message, ...>>
|
||||
// but to save A LOT of bandwidth, we use
|
||||
// <<message, message, ...>
|
||||
// in other words, we don't know where the current message ends
|
||||
//
|
||||
// BUT: it doesn't matter!
|
||||
// -> we simply return the reader
|
||||
// * if we have one yet
|
||||
// * and if there's more to read
|
||||
// -> the caller can then read one message from it
|
||||
// -> when the end is reached, we retire the batch!
|
||||
//
|
||||
// for example:
|
||||
// while (GetNextMessage(out message))
|
||||
// ProcessMessage(message);
|
||||
//
|
||||
message = null;
|
||||
message = default;
|
||||
remoteTimeStamp = 0;
|
||||
|
||||
// do nothing if we don't have any batches.
|
||||
// otherwise the below queue.Dequeue() would throw an
|
||||
// InvalidOperationException if operating on empty queue.
|
||||
if (batches.Count == 0)
|
||||
{
|
||||
remoteTimeStamp = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
// was our reader pointed to anything yet?
|
||||
if (reader.Capacity == 0)
|
||||
{
|
||||
remoteTimeStamp = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
// no more data to read?
|
||||
if (reader.Remaining == 0)
|
||||
@ -123,19 +102,27 @@ public bool GetNextMessage(out NetworkReader message, out double remoteTimeStamp
|
||||
StartReadingBatch(next);
|
||||
}
|
||||
// otherwise there's nothing more to read
|
||||
else
|
||||
{
|
||||
remoteTimeStamp = 0;
|
||||
return false;
|
||||
}
|
||||
else return false;
|
||||
}
|
||||
|
||||
// use the current batch's remote timestamp
|
||||
// AFTER potentially moving to the next batch ABOVE!
|
||||
remoteTimeStamp = readerRemoteTimeStamp;
|
||||
|
||||
// if we got here, then we have more data to read.
|
||||
message = reader;
|
||||
// enough data to read the size prefix?
|
||||
if (reader.Remaining == 0)
|
||||
return false;
|
||||
|
||||
// read the size prefix as varint
|
||||
// see Batcher.AddMessage comments for explanation.
|
||||
int size = (int)Compression.DecompressVarUInt(reader);
|
||||
|
||||
// validate size prefix, in case attackers send malicious data
|
||||
if (reader.Remaining < size)
|
||||
return false;
|
||||
|
||||
// return the message of size
|
||||
message = reader.ReadBytesSegment(size);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
74
Assets/Mirror/Core/ConnectionQuality.cs
Normal file
74
Assets/Mirror/Core/ConnectionQuality.cs
Normal file
@ -0,0 +1,74 @@
|
||||
// standalone, Unity-independent connection-quality algorithm & enum.
|
||||
// don't need to use this directly, it's built into Mirror's NetworkClient.
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
public enum ConnectionQuality : byte
|
||||
{
|
||||
ESTIMATING, // still estimating
|
||||
POOR, // unplayable
|
||||
FAIR, // very noticeable latency, not very enjoyable anymore
|
||||
GOOD, // very playable for everyone but high level competitors
|
||||
EXCELLENT // ideal experience for high level competitors
|
||||
}
|
||||
|
||||
public enum ConnectionQualityMethod : byte
|
||||
{
|
||||
Simple, // simple estimation based on rtt and jitter
|
||||
Pragmatic // based on snapshot interpolation adjustment
|
||||
}
|
||||
|
||||
// provide different heuristics for users to choose from.
|
||||
// simple heuristics to get started.
|
||||
// this will be iterated on over time based on user feedback.
|
||||
public static class ConnectionQualityHeuristics
|
||||
{
|
||||
// convenience extension to color code Connection Quality
|
||||
public static Color ColorCode(this ConnectionQuality quality)
|
||||
{
|
||||
switch (quality)
|
||||
{
|
||||
case ConnectionQuality.POOR: return Color.red;
|
||||
case ConnectionQuality.FAIR: return new Color(1.0f, 0.647f, 0.0f);
|
||||
case ConnectionQuality.GOOD: return Color.yellow;
|
||||
case ConnectionQuality.EXCELLENT: return Color.green;
|
||||
default: return Color.gray; // ESTIMATING
|
||||
}
|
||||
}
|
||||
|
||||
// straight forward estimation
|
||||
// rtt: average round trip time in seconds.
|
||||
// jitter: average latency variance.
|
||||
public static ConnectionQuality Simple(double rtt, double jitter)
|
||||
{
|
||||
if (rtt <= 0.100 && jitter <= 0.10) return ConnectionQuality.EXCELLENT;
|
||||
if (rtt <= 0.200 && jitter <= 0.20) return ConnectionQuality.GOOD;
|
||||
if (rtt <= 0.400 && jitter <= 0.50) return ConnectionQuality.FAIR;
|
||||
return ConnectionQuality.POOR;
|
||||
}
|
||||
|
||||
// snapshot interpolation based estimation.
|
||||
// snap. interp. adjusts buffer time based on connection quality.
|
||||
// based on this, we can measure how far away we are from the ideal.
|
||||
// the returned quality will always directly correlate with gameplay.
|
||||
// => requires SnapshotInterpolation dynamicAdjustment to be enabled!
|
||||
public static ConnectionQuality Pragmatic(double targetBufferTime, double currentBufferTime)
|
||||
{
|
||||
// buffer time is set by the game developer.
|
||||
// estimating in multiples is a great way to be game independent.
|
||||
// for example, a fast paced shooter and a slow paced RTS will both
|
||||
// have poor connection if the multiplier is >10.
|
||||
double multiplier = currentBufferTime / targetBufferTime;
|
||||
|
||||
// empirically measured with Tanks demo + LatencySimulation.
|
||||
// it's not obvious to estimate on paper.
|
||||
if (multiplier <= 1.15) return ConnectionQuality.EXCELLENT;
|
||||
if (multiplier <= 1.25) return ConnectionQuality.GOOD;
|
||||
if (multiplier <= 1.50) return ConnectionQuality.FAIR;
|
||||
|
||||
// anything else is poor
|
||||
return ConnectionQuality.POOR;
|
||||
}
|
||||
}
|
||||
}
|
11
Assets/Mirror/Core/ConnectionQuality.cs.meta
Normal file
11
Assets/Mirror/Core/ConnectionQuality.cs.meta
Normal 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:
|
@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a99666a026b14cf6ba1a2b65946b1b27
|
||||
timeCreated: 1615288671
|
@ -1 +0,0 @@
|
||||
// moved into NetworkClient on 2021-03-07
|
@ -1 +0,0 @@
|
||||
// removed 2021-05-13
|
@ -1 +0,0 @@
|
||||
// removed 2021-05-13
|
@ -1 +0,0 @@
|
||||
// removed 2021-05-13
|
@ -1 +0,0 @@
|
||||
// removed 2021-05-13
|
@ -1 +0,0 @@
|
||||
// removed 2021-05-13
|
@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 70f563b7a7210ae43bbcde5cb7721a94
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -1 +0,0 @@
|
||||
// removed 2021-05-13
|
@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c7c472a3ea1bc4348bd5a0b05bf7cc3b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -1 +0,0 @@
|
||||
// removed 2021-05-13
|
@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 97501e783fc67a4459b15d10e6c63563
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -1 +0,0 @@
|
||||
// removed 2021-05-13
|
@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 43472c60a7c72e54eafe559290dd0fc6
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -1 +0,0 @@
|
||||
// removed 2021-05-13
|
@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b80b95532a9d6e8418aa676a261e4f69
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -1 +0,0 @@
|
||||
// removed 2021-05-13
|
@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 05185b973ba389a4588fc8a99c75a4f6
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -1 +0,0 @@
|
||||
// removed 2021-05-13
|
@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: dbabb497385c20346a3c8bda4ae69508
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -1 +0,0 @@
|
||||
// removed 2021-05-13
|
@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0688c0fdae5376e4ea74d5c3904eed17
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -1 +0,0 @@
|
||||
// removed 2021-05-13
|
@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6f0311899162c5b49a3c11fa9bd9c133
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -1 +0,0 @@
|
||||
// removed 2021-05-13
|
@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b6838f9df45594d48873518cbb75b329
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -1 +0,0 @@
|
||||
// removed 2021-05-13
|
@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d49649fb32cb96b46b10f013b38a4b50
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -1 +0,0 @@
|
||||
// removed 2021-05-13
|
@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a963606335eae0f47abe7ecb5fd028ea
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -1 +0,0 @@
|
||||
// removed 2021-05-13
|
@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 675f0d0fd4e82b04290c4d30c8d78ede
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -1 +0,0 @@
|
||||
// removed 2021-05-13
|
@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 457ba2df6cb6e1542996c17c715ee81b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -1 +0,0 @@
|
||||
// removed 2021-05-13
|
@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 95bebb8e810e2954485291a26324f7d5
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -1 +0,0 @@
|
||||
// removed 2021-05-13
|
@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 068feff770f710141afa4a90063a5e6c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -1 +0,0 @@
|
||||
// removed 2021-05-13
|
@ -1 +0,0 @@
|
||||
// removed 2021-05-13
|
@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 07d1ea5260bc06e4d831c4b61d494bff
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -1 +0,0 @@
|
||||
// removed 2021-05-13
|
@ -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
Loading…
Reference in New Issue
Block a user