updated to Mirror Version 2022.9.15
This commit is contained in:
parent
1af452928e
commit
d23f61576f
1067 changed files with 25205 additions and 15260 deletions
|
|
@ -34,6 +34,12 @@ public class LRMFunctionTest : MonoBehaviour
|
|||
NetworkManager.singleton.StartHost();
|
||||
yield return new WaitUntil(() => _LRM.serverId.Length > 4);
|
||||
DisplayText($"<color=lime>Room created! ID: {_LRM.serverId}</color>");
|
||||
DisplayText("Requesting Server List...");
|
||||
_LRM.RequestServerList();
|
||||
yield return new WaitUntil(() => _serverListUpdated);
|
||||
foreach (var server in _LRM.relayServerList)
|
||||
DisplayText($"Got Server: {server.serverName}, {server.serverData}, {server.maxPlayers}");
|
||||
_serverListUpdated = false;
|
||||
DisplayText("Requesting Server Data Change...");
|
||||
_LRM.UpdateRoomName("Updated Server Name");
|
||||
_LRM.UpdateRoomData("Updated Server Data");
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@ guid: 1b2f9d254154cd942ba40b06b869b8f3
|
|||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData: ''
|
||||
assetBundleName: ''
|
||||
assetBundleVariant: ''
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
|
|||
|
|
@ -1,20 +1,23 @@
|
|||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror.Authenticators
|
||||
{
|
||||
[AddComponentMenu("Network/ Authenticators/Basic Authenticator")]
|
||||
[HelpURL("https://mirror-networking.gitbook.io/docs/components/network-authenticators/basic-authenticator")]
|
||||
public class BasicAuthenticator : NetworkAuthenticator
|
||||
{
|
||||
[Header("Custom Properties")]
|
||||
[Header("Server Credentials")]
|
||||
public string serverUsername;
|
||||
public string serverPassword;
|
||||
|
||||
// set these in the inspector
|
||||
[Header("Client Credentials")]
|
||||
public string username;
|
||||
public string password;
|
||||
|
||||
// this is set if authentication fails to prevent garbage AuthRequestMessage spam
|
||||
bool ServerAuthFailed;
|
||||
readonly HashSet<NetworkConnection> connectionsPendingDisconnect = new HashSet<NetworkConnection>();
|
||||
|
||||
#region Messages
|
||||
|
||||
|
|
@ -57,10 +60,10 @@ namespace Mirror.Authenticators
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called on server from OnServerAuthenticateInternal when a client needs to authenticate
|
||||
/// Called on server from OnServerConnectInternal when a client needs to authenticate
|
||||
/// </summary>
|
||||
/// <param name="conn">Connection to client.</param>
|
||||
public override void OnServerAuthenticate(NetworkConnection conn)
|
||||
public override void OnServerAuthenticate(NetworkConnectionToClient conn)
|
||||
{
|
||||
// do nothing...wait for AuthRequestMessage from client
|
||||
}
|
||||
|
|
@ -70,12 +73,14 @@ namespace Mirror.Authenticators
|
|||
/// </summary>
|
||||
/// <param name="conn">Connection to client.</param>
|
||||
/// <param name="msg">The message payload</param>
|
||||
public void OnAuthRequestMessage(NetworkConnection conn, AuthRequestMessage msg)
|
||||
public void OnAuthRequestMessage(NetworkConnectionToClient conn, AuthRequestMessage msg)
|
||||
{
|
||||
// Debug.LogFormat(LogType.Log, "Authentication Request: {0} {1}", msg.authUsername, msg.authPassword);
|
||||
//Debug.Log($"Authentication Request: {msg.authUsername} {msg.authPassword}");
|
||||
|
||||
if (connectionsPendingDisconnect.Contains(conn)) return;
|
||||
|
||||
// check the credentials by calling your web server, database table, playfab api, or any method appropriate.
|
||||
if (msg.authUsername == username && msg.authPassword == password)
|
||||
if (msg.authUsername == serverUsername && msg.authPassword == serverPassword)
|
||||
{
|
||||
// create and send msg to client so it knows to proceed
|
||||
AuthResponseMessage authResponseMessage = new AuthResponseMessage
|
||||
|
|
@ -91,6 +96,8 @@ namespace Mirror.Authenticators
|
|||
}
|
||||
else
|
||||
{
|
||||
connectionsPendingDisconnect.Add(conn);
|
||||
|
||||
// create and send msg to client so it knows to disconnect
|
||||
AuthResponseMessage authResponseMessage = new AuthResponseMessage
|
||||
{
|
||||
|
|
@ -104,22 +111,21 @@ namespace Mirror.Authenticators
|
|||
conn.isAuthenticated = false;
|
||||
|
||||
// disconnect the client after 1 second so that response message gets delivered
|
||||
if (!ServerAuthFailed)
|
||||
{
|
||||
// set this false so this coroutine can only be started once
|
||||
ServerAuthFailed = true;
|
||||
|
||||
StartCoroutine(DelayedDisconnect(conn, 1));
|
||||
}
|
||||
StartCoroutine(DelayedDisconnect(conn, 1f));
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerator DelayedDisconnect(NetworkConnection conn, float waitTime)
|
||||
IEnumerator DelayedDisconnect(NetworkConnectionToClient conn, float waitTime)
|
||||
{
|
||||
yield return new WaitForSeconds(waitTime);
|
||||
|
||||
// Reject the unsuccessful authentication
|
||||
ServerReject(conn);
|
||||
|
||||
yield return null;
|
||||
|
||||
// remove conn from pending connections
|
||||
connectionsPendingDisconnect.Remove(conn);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
|
@ -133,7 +139,7 @@ namespace Mirror.Authenticators
|
|||
public override void OnStartClient()
|
||||
{
|
||||
// register a handler for the authentication response we expect from server
|
||||
NetworkClient.RegisterHandler<AuthResponseMessage>((Action<AuthResponseMessage>)OnAuthResponseMessage, false);
|
||||
NetworkClient.RegisterHandler<AuthResponseMessage>(OnAuthResponseMessage, false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -147,7 +153,7 @@ namespace Mirror.Authenticators
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called on client from OnClientAuthenticateInternal when a client needs to authenticate
|
||||
/// Called on client from OnClientConnectInternal when a client needs to authenticate
|
||||
/// </summary>
|
||||
public override void OnClientAuthenticate()
|
||||
{
|
||||
|
|
@ -160,9 +166,6 @@ namespace Mirror.Authenticators
|
|||
NetworkClient.connection.Send(authRequestMessage);
|
||||
}
|
||||
|
||||
[Obsolete("Call OnAuthResponseMessage without the NetworkConnection parameter. It always points to NetworkClient.connection anyway.")]
|
||||
public void OnAuthResponseMessage(NetworkConnection conn, AuthResponseMessage msg) => OnAuthResponseMessage(msg);
|
||||
|
||||
/// <summary>
|
||||
/// Called on client when the server's AuthResponseMessage arrives
|
||||
/// </summary>
|
||||
|
|
@ -171,7 +174,7 @@ namespace Mirror.Authenticators
|
|||
{
|
||||
if (msg.code == 100)
|
||||
{
|
||||
// Debug.LogFormat(LogType.Log, "Authentication Response: {0}", msg.message);
|
||||
//Debug.Log($"Authentication Response: {msg.message}");
|
||||
|
||||
// Authentication has been accepted
|
||||
ClientAccept();
|
||||
|
|
|
|||
|
|
@ -6,6 +6,6 @@ MonoImporter:
|
|||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData: ''
|
||||
assetBundleName: ''
|
||||
assetBundleVariant: ''
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
|
|||
129
UnityProject/Assets/Mirror/Authenticators/DeviceAuthenticator.cs
Normal file
129
UnityProject/Assets/Mirror/Authenticators/DeviceAuthenticator.cs
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror.Authenticators
|
||||
{
|
||||
/// <summary>
|
||||
/// An authenticator that identifies the user by their device.
|
||||
/// <para>A GUID is used as a fallback when the platform doesn't support SystemInfo.deviceUniqueIdentifier.</para>
|
||||
/// <para>Note: deviceUniqueIdentifier can be spoofed, so security is not guaranteed.</para>
|
||||
/// <para>See https://docs.unity3d.com/ScriptReference/SystemInfo-deviceUniqueIdentifier.html for details.</para>
|
||||
/// </summary>
|
||||
[AddComponentMenu("Network/ Authenticators/Device Authenticator")]
|
||||
[HelpURL("https://mirror-networking.gitbook.io/docs/components/network-authenticators/device-authenticator")]
|
||||
public class DeviceAuthenticator : NetworkAuthenticator
|
||||
{
|
||||
#region Messages
|
||||
|
||||
public struct AuthRequestMessage : NetworkMessage
|
||||
{
|
||||
public string clientDeviceID;
|
||||
}
|
||||
|
||||
public struct AuthResponseMessage : NetworkMessage { }
|
||||
|
||||
#endregion
|
||||
|
||||
#region Server
|
||||
|
||||
/// <summary>
|
||||
/// Called on server from StartServer to initialize the Authenticator
|
||||
/// <para>Server message handlers should be registered in this method.</para>
|
||||
/// </summary>
|
||||
public override void OnStartServer()
|
||||
{
|
||||
// register a handler for the authentication request we expect from client
|
||||
NetworkServer.RegisterHandler<AuthRequestMessage>(OnAuthRequestMessage, false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called on server from StopServer to reset the Authenticator
|
||||
/// <para>Server message handlers should be registered in this method.</para>
|
||||
/// </summary>
|
||||
public override void OnStopServer()
|
||||
{
|
||||
// unregister the handler for the authentication request
|
||||
NetworkServer.UnregisterHandler<AuthRequestMessage>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called on server from OnServerConnectInternal when a client needs to authenticate
|
||||
/// </summary>
|
||||
/// <param name="conn">Connection to client.</param>
|
||||
public override void OnServerAuthenticate(NetworkConnectionToClient conn)
|
||||
{
|
||||
// do nothing, wait for client to send his id
|
||||
}
|
||||
|
||||
void OnAuthRequestMessage(NetworkConnectionToClient conn, AuthRequestMessage msg)
|
||||
{
|
||||
Debug.Log($"connection {conn.connectionId} authenticated with id {msg.clientDeviceID}");
|
||||
|
||||
// Store the device id for later reference, e.g. when spawning the player
|
||||
conn.authenticationData = msg.clientDeviceID;
|
||||
|
||||
// Send a response to client telling it to proceed as authenticated
|
||||
conn.Send(new AuthResponseMessage());
|
||||
|
||||
// Accept the successful authentication
|
||||
ServerAccept(conn);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Client
|
||||
|
||||
/// <summary>
|
||||
/// Called on client from StartClient to initialize the Authenticator
|
||||
/// <para>Client message handlers should be registered in this method.</para>
|
||||
/// </summary>
|
||||
public override void OnStartClient()
|
||||
{
|
||||
// register a handler for the authentication response we expect from server
|
||||
NetworkClient.RegisterHandler<AuthResponseMessage>(OnAuthResponseMessage, false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called on client from StopClient to reset the Authenticator
|
||||
/// <para>Client message handlers should be unregistered in this method.</para>
|
||||
/// </summary>
|
||||
public override void OnStopClient()
|
||||
{
|
||||
// unregister the handler for the authentication response
|
||||
NetworkClient.UnregisterHandler<AuthResponseMessage>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called on client from OnClientConnectInternal when a client needs to authenticate
|
||||
/// </summary>
|
||||
public override void OnClientAuthenticate()
|
||||
{
|
||||
string deviceUniqueIdentifier = SystemInfo.deviceUniqueIdentifier;
|
||||
|
||||
// Not all platforms support this, so we use a GUID instead
|
||||
if (deviceUniqueIdentifier == SystemInfo.unsupportedIdentifier)
|
||||
{
|
||||
// Get the value from PlayerPrefs if it exists, new GUID if it doesn't
|
||||
deviceUniqueIdentifier = PlayerPrefs.GetString("deviceUniqueIdentifier", Guid.NewGuid().ToString());
|
||||
|
||||
// Store the deviceUniqueIdentifier to PlayerPrefs (in case we just made a new GUID)
|
||||
PlayerPrefs.SetString("deviceUniqueIdentifier", deviceUniqueIdentifier);
|
||||
}
|
||||
|
||||
// send the deviceUniqueIdentifier to the server
|
||||
NetworkClient.connection.Send(new AuthRequestMessage { clientDeviceID = deviceUniqueIdentifier } );
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called on client when the server's AuthResponseMessage arrives
|
||||
/// </summary>
|
||||
/// <param name="msg">The message payload</param>
|
||||
public void OnAuthResponseMessage(AuthResponseMessage msg)
|
||||
{
|
||||
Debug.Log("Authentication Success");
|
||||
ClientAccept();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: f65214da13a861f4a8ae309d3daea1c6
|
||||
guid: 60960a6ba81a842deb2fdcdc93788242
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData: ''
|
||||
assetBundleName: ''
|
||||
assetBundleVariant: ''
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -2,6 +2,6 @@ fileFormatVersion: 2
|
|||
guid: e720aa64e3f58fb4880566a322584340
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData: ''
|
||||
assetBundleName: ''
|
||||
assetBundleVariant: ''
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ namespace Mirror.Authenticators
|
|||
public void Awake()
|
||||
{
|
||||
authenticator.OnServerAuthenticated.AddListener(connection => OnServerAuthenticated.Invoke(connection));
|
||||
authenticator.OnClientAuthenticated.AddListener(connection => OnClientAuthenticated.Invoke(connection));
|
||||
authenticator.OnClientAuthenticated.AddListener(OnClientAuthenticated.Invoke);
|
||||
}
|
||||
|
||||
public override void OnStartServer()
|
||||
|
|
@ -41,7 +41,7 @@ namespace Mirror.Authenticators
|
|||
authenticator.OnStopClient();
|
||||
}
|
||||
|
||||
public override void OnServerAuthenticate(NetworkConnection conn)
|
||||
public override void OnServerAuthenticate(NetworkConnectionToClient conn)
|
||||
{
|
||||
authenticator.OnServerAuthenticate(conn);
|
||||
if (timeout > 0)
|
||||
|
|
@ -62,7 +62,7 @@ namespace Mirror.Authenticators
|
|||
|
||||
if (!conn.isAuthenticated)
|
||||
{
|
||||
// Debug.Log($"Authentication Timeout {conn}");
|
||||
Debug.LogError($"Authentication Timeout - Disconnecting {conn}");
|
||||
conn.Disconnect();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,6 @@ MonoImporter:
|
|||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData: ''
|
||||
assetBundleName: ''
|
||||
assetBundleVariant: ''
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@ guid: 1f8b918bcd89f5c488b06f5574f34760
|
|||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData: ''
|
||||
assetBundleName: ''
|
||||
assetBundleVariant: ''
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
|
|||
|
|
@ -2,6 +2,6 @@ fileFormatVersion: 2
|
|||
guid: 325984b52e4128546bc7558552f8b1d2
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData: ''
|
||||
assetBundleName: ''
|
||||
assetBundleVariant: ''
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
|
|||
|
|
@ -31,7 +31,20 @@ namespace Mirror
|
|||
"MIRROR_37_0_OR_NEWER",
|
||||
"MIRROR_38_0_OR_NEWER",
|
||||
"MIRROR_39_0_OR_NEWER",
|
||||
"MIRROR_40_0_OR_NEWER"
|
||||
"MIRROR_40_0_OR_NEWER",
|
||||
"MIRROR_41_0_OR_NEWER",
|
||||
"MIRROR_42_0_OR_NEWER",
|
||||
"MIRROR_43_0_OR_NEWER",
|
||||
"MIRROR_44_0_OR_NEWER",
|
||||
"MIRROR_46_0_OR_NEWER",
|
||||
"MIRROR_47_0_OR_NEWER",
|
||||
"MIRROR_53_0_OR_NEWER",
|
||||
"MIRROR_55_0_OR_NEWER",
|
||||
"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"
|
||||
};
|
||||
|
||||
// only touch PlayerSettings if we actually modified it.
|
||||
|
|
|
|||
|
|
@ -6,6 +6,6 @@ MonoImporter:
|
|||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData: ''
|
||||
assetBundleName: ''
|
||||
assetBundleVariant: ''
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@ guid: 9bee879fbc8ef4b1a9a9f7088bfbf726
|
|||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData: ''
|
||||
assetBundleName: ''
|
||||
assetBundleVariant: ''
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
|
|||
|
|
@ -2,6 +2,9 @@ using System.Runtime.CompilerServices;
|
|||
|
||||
[assembly: InternalsVisibleTo("Mirror.Tests.Common")]
|
||||
[assembly: InternalsVisibleTo("Mirror.Tests")]
|
||||
// need to use Unity.*.CodeGen assembly name to import Unity.CompilationPipeline
|
||||
// for ILPostProcessor tests.
|
||||
[assembly: InternalsVisibleTo("Unity.Mirror.Tests.CodeGen")]
|
||||
[assembly: InternalsVisibleTo("Mirror.Tests.Generated")]
|
||||
[assembly: InternalsVisibleTo("Mirror.Tests.Runtime")]
|
||||
[assembly: InternalsVisibleTo("Mirror.Tests.Performance.Editor")]
|
||||
|
|
|
|||
|
|
@ -6,6 +6,6 @@ MonoImporter:
|
|||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData: ''
|
||||
assetBundleName: ''
|
||||
assetBundleVariant: ''
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@ guid: b5dcf9618f5e14a4eb60bff5480284a6
|
|||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData: ''
|
||||
assetBundleName: ''
|
||||
assetBundleVariant: ''
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
|
|||
|
|
@ -6,6 +6,6 @@ MonoImporter:
|
|||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData: ''
|
||||
assetBundleName: ''
|
||||
assetBundleVariant: ''
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
|
|||
|
|
@ -93,6 +93,7 @@ namespace Mirror.Discovery
|
|||
|
||||
void Shutdown()
|
||||
{
|
||||
EndpMulticastLock();
|
||||
if (serverUdpClient != null)
|
||||
{
|
||||
try
|
||||
|
|
@ -149,6 +150,7 @@ namespace Mirror.Discovery
|
|||
|
||||
public async Task ServerListenAsync()
|
||||
{
|
||||
BeginMulticastLock();
|
||||
while (true)
|
||||
{
|
||||
try
|
||||
|
|
@ -173,7 +175,7 @@ namespace Mirror.Discovery
|
|||
|
||||
UdpReceiveResult udpReceiveResult = await udpClient.ReceiveAsync();
|
||||
|
||||
using (PooledNetworkReader networkReader = NetworkReaderPool.GetReader(udpReceiveResult.Buffer))
|
||||
using (NetworkReaderPooled networkReader = NetworkReaderPool.Get(udpReceiveResult.Buffer))
|
||||
{
|
||||
long handshake = networkReader.ReadLong();
|
||||
if (handshake != secretHandshake)
|
||||
|
|
@ -204,7 +206,7 @@ namespace Mirror.Discovery
|
|||
if (info == null)
|
||||
return;
|
||||
|
||||
using (PooledNetworkWriter writer = NetworkWriterPool.GetWriter())
|
||||
using (NetworkWriterPooled writer = NetworkWriterPool.Get())
|
||||
{
|
||||
try
|
||||
{
|
||||
|
|
@ -236,6 +238,41 @@ namespace Mirror.Discovery
|
|||
/// <returns>The message to be sent back to the client or null</returns>
|
||||
protected abstract Response ProcessRequest(Request request, IPEndPoint endpoint);
|
||||
|
||||
// Android Multicast fix: https://github.com/vis2k/Mirror/pull/2887
|
||||
#if UNITY_ANDROID
|
||||
AndroidJavaObject multicastLock;
|
||||
bool hasMulticastLock;
|
||||
#endif
|
||||
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"))
|
||||
{
|
||||
using (var wifiManager = activity.Call<AndroidJavaObject>("getSystemService", "wifi"))
|
||||
{
|
||||
multicastLock = wifiManager.Call<AndroidJavaObject>("createMulticastLock", "lock");
|
||||
multicastLock.Call("acquire");
|
||||
hasMulticastLock = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void EndpMulticastLock()
|
||||
{
|
||||
#if UNITY_ANDROID
|
||||
if (!hasMulticastLock) return;
|
||||
|
||||
multicastLock?.Call("release");
|
||||
hasMulticastLock = false;
|
||||
#endif
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Client
|
||||
|
|
@ -287,7 +324,18 @@ namespace Mirror.Discovery
|
|||
/// <returns>ClientListenAsync Task</returns>
|
||||
public async Task ClientListenAsync()
|
||||
{
|
||||
while (true)
|
||||
// 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,
|
||||
// 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
|
||||
// the quest.
|
||||
while (clientUdpClient != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
|
@ -321,7 +369,7 @@ namespace Mirror.Discovery
|
|||
|
||||
IPEndPoint endPoint = new IPEndPoint(IPAddress.Broadcast, serverBroadcastListenPort);
|
||||
|
||||
using (PooledNetworkWriter writer = NetworkWriterPool.GetWriter())
|
||||
using (NetworkWriterPooled writer = NetworkWriterPool.Get())
|
||||
{
|
||||
writer.WriteLong(secretHandshake);
|
||||
|
||||
|
|
@ -358,7 +406,7 @@ namespace Mirror.Discovery
|
|||
|
||||
UdpReceiveResult udpReceiveResult = await udpClient.ReceiveAsync();
|
||||
|
||||
using (PooledNetworkReader networkReader = NetworkReaderPool.GetReader(udpReceiveResult.Buffer))
|
||||
using (NetworkReaderPooled networkReader = NetworkReaderPool.Get(udpReceiveResult.Buffer))
|
||||
{
|
||||
if (networkReader.ReadLong() != secretHandshake)
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -6,6 +6,6 @@ MonoImporter:
|
|||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData: ''
|
||||
assetBundleName: ''
|
||||
assetBundleVariant: ''
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
|
|||
|
|
@ -62,7 +62,6 @@ namespace Mirror.Discovery
|
|||
{
|
||||
discoveredServers.Clear();
|
||||
NetworkManager.singleton.StartServer();
|
||||
|
||||
networkDiscovery.AdvertiseServer();
|
||||
}
|
||||
|
||||
|
|
@ -93,6 +92,7 @@ namespace Mirror.Discovery
|
|||
if (GUILayout.Button("Stop Host"))
|
||||
{
|
||||
NetworkManager.singleton.StopHost();
|
||||
networkDiscovery.StopDiscovery();
|
||||
}
|
||||
}
|
||||
// stop client if client-only
|
||||
|
|
@ -101,6 +101,7 @@ namespace Mirror.Discovery
|
|||
if (GUILayout.Button("Stop Client"))
|
||||
{
|
||||
NetworkManager.singleton.StopClient();
|
||||
networkDiscovery.StopDiscovery();
|
||||
}
|
||||
}
|
||||
// stop server if server-only
|
||||
|
|
@ -109,6 +110,7 @@ namespace Mirror.Discovery
|
|||
if (GUILayout.Button("Stop Server"))
|
||||
{
|
||||
NetworkManager.singleton.StopServer();
|
||||
networkDiscovery.StopDiscovery();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -117,6 +119,7 @@ namespace Mirror.Discovery
|
|||
|
||||
void Connect(ServerResponse info)
|
||||
{
|
||||
networkDiscovery.StopDiscovery();
|
||||
NetworkManager.singleton.StartClient(info.uri);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,6 @@ MonoImporter:
|
|||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData: ''
|
||||
assetBundleName: ''
|
||||
assetBundleVariant: ''
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
|
|||
|
|
@ -6,6 +6,6 @@ MonoImporter:
|
|||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData: ''
|
||||
assetBundleName: ''
|
||||
assetBundleVariant: ''
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
|
|||
|
|
@ -6,6 +6,6 @@ MonoImporter:
|
|||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData: ''
|
||||
assetBundleName: ''
|
||||
assetBundleVariant: ''
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@ guid: bfbf2a1f2b300c5489dcab219ef2846e
|
|||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData: ''
|
||||
assetBundleName: ''
|
||||
assetBundleVariant: ''
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ namespace Mirror.Experimental
|
|||
target.velocity = Vector3.Lerp(target.velocity, targetVelocity, lerpVelocityAmount);
|
||||
target.position = Vector3.Lerp(target.position, targetPosition, lerpPositionAmount);
|
||||
// add velocity to position as position would have moved on server at that velocity
|
||||
targetPosition += target.velocity * Time.fixedDeltaTime;
|
||||
target.position += target.velocity * Time.fixedDeltaTime;
|
||||
|
||||
// TODO does this also need to sync acceleration so and update velocity?
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,6 @@ MonoImporter:
|
|||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData: ''
|
||||
assetBundleName: ''
|
||||
assetBundleVariant: ''
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ namespace Mirror.Experimental
|
|||
[SerializeField] internal Rigidbody target = null;
|
||||
|
||||
[Tooltip("Set to true if moves come from owner client, set to false if moves always come from server")]
|
||||
[SerializeField] bool clientAuthority = false;
|
||||
public bool clientAuthority = false;
|
||||
|
||||
[Header("Velocity")]
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,6 @@ MonoImporter:
|
|||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData: ''
|
||||
assetBundleName: ''
|
||||
assetBundleVariant: ''
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ namespace Mirror.Experimental
|
|||
[SerializeField] internal Rigidbody2D target = null;
|
||||
|
||||
[Tooltip("Set to true if moves come from owner client, set to false if moves always come from server")]
|
||||
[SerializeField] bool clientAuthority = false;
|
||||
public bool clientAuthority = false;
|
||||
|
||||
[Header("Velocity")]
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,6 @@ MonoImporter:
|
|||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData: ''
|
||||
assetBundleName: ''
|
||||
assetBundleVariant: ''
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
|
|||
|
|
@ -1,12 +0,0 @@
|
|||
using UnityEngine;
|
||||
|
||||
namespace Mirror.Experimental
|
||||
{
|
||||
[DisallowMultipleComponent]
|
||||
[AddComponentMenu("Network/Experimental/NetworkTransformExperimental")]
|
||||
[HelpURL("https://mirror-networking.gitbook.io/docs/components/network-transform")]
|
||||
public class NetworkTransform : NetworkTransformBase
|
||||
{
|
||||
protected override Transform targetTransform => transform;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,529 +0,0 @@
|
|||
// vis2k:
|
||||
// base class for NetworkTransform and NetworkTransformChild.
|
||||
// New method is simple and stupid. No more 1500 lines of code.
|
||||
//
|
||||
// Server sends current data.
|
||||
// Client saves it and interpolates last and latest data points.
|
||||
// Update handles transform movement / rotation
|
||||
// FixedUpdate handles rigidbody movement / rotation
|
||||
//
|
||||
// Notes:
|
||||
// * Built-in Teleport detection in case of lags / teleport / obstacles
|
||||
// * Quaternion > EulerAngles because gimbal lock and Quaternion.Slerp
|
||||
// * Syncs XYZ. Works 3D and 2D. Saving 4 bytes isn't worth 1000 lines of code.
|
||||
// * Initial delay might happen if server sends packet immediately after moving
|
||||
// just 1cm, hence we move 1cm and then wait 100ms for next packet
|
||||
// * Only way for smooth movement is to use a fixed movement speed during
|
||||
// interpolation. interpolation over time is never that good.
|
||||
//
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror.Experimental
|
||||
{
|
||||
public abstract class NetworkTransformBase : NetworkBehaviour
|
||||
{
|
||||
// target transform to sync. can be on a child.
|
||||
protected abstract Transform targetTransform { get; }
|
||||
|
||||
[Header("Authority")]
|
||||
|
||||
[Tooltip("Set to true if moves come from owner client, set to false if moves always come from server")]
|
||||
[SyncVar]
|
||||
public bool clientAuthority;
|
||||
|
||||
[Tooltip("Set to true if updates from server should be ignored by owner")]
|
||||
[SyncVar]
|
||||
public bool excludeOwnerUpdate = true;
|
||||
|
||||
[Header("Synchronization")]
|
||||
|
||||
[Tooltip("Set to true if position should be synchronized")]
|
||||
[SyncVar]
|
||||
public bool syncPosition = true;
|
||||
|
||||
[Tooltip("Set to true if rotation should be synchronized")]
|
||||
[SyncVar]
|
||||
public bool syncRotation = true;
|
||||
|
||||
[Tooltip("Set to true if scale should be synchronized")]
|
||||
[SyncVar]
|
||||
public bool syncScale = true;
|
||||
|
||||
[Header("Interpolation")]
|
||||
|
||||
[Tooltip("Set to true if position should be interpolated")]
|
||||
[SyncVar]
|
||||
public bool interpolatePosition = true;
|
||||
|
||||
[Tooltip("Set to true if rotation should be interpolated")]
|
||||
[SyncVar]
|
||||
public bool interpolateRotation = true;
|
||||
|
||||
[Tooltip("Set to true if scale should be interpolated")]
|
||||
[SyncVar]
|
||||
public bool interpolateScale = true;
|
||||
|
||||
// Sensitivity is added for VR where human players tend to have micro movements so this can quiet down
|
||||
// the network traffic. Additionally, rigidbody drift should send less traffic, e.g very slow sliding / rolling.
|
||||
[Header("Sensitivity")]
|
||||
|
||||
[Tooltip("Changes to the transform must exceed these values to be transmitted on the network.")]
|
||||
[SyncVar]
|
||||
public float localPositionSensitivity = .01f;
|
||||
|
||||
[Tooltip("If rotation exceeds this angle, it will be transmitted on the network")]
|
||||
[SyncVar]
|
||||
public float localRotationSensitivity = .01f;
|
||||
|
||||
[Tooltip("Changes to the transform must exceed these values to be transmitted on the network.")]
|
||||
[SyncVar]
|
||||
public float localScaleSensitivity = .01f;
|
||||
|
||||
[Header("Diagnostics")]
|
||||
|
||||
// server
|
||||
public Vector3 lastPosition;
|
||||
public Quaternion lastRotation;
|
||||
public Vector3 lastScale;
|
||||
|
||||
// client
|
||||
// use local position/rotation for VR support
|
||||
[Serializable]
|
||||
public struct DataPoint
|
||||
{
|
||||
public float timeStamp;
|
||||
public Vector3 localPosition;
|
||||
public Quaternion localRotation;
|
||||
public Vector3 localScale;
|
||||
public float movementSpeed;
|
||||
|
||||
public bool isValid => timeStamp != 0;
|
||||
}
|
||||
|
||||
// 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.
|
||||
bool IsOwnerWithClientAuthority => hasAuthority && clientAuthority;
|
||||
|
||||
// interpolation start and goal
|
||||
public DataPoint start = new DataPoint();
|
||||
public DataPoint goal = new DataPoint();
|
||||
|
||||
// We need to store this locally on the server so clients can't request Authority when ever they like
|
||||
bool clientAuthorityBeforeTeleport;
|
||||
|
||||
void FixedUpdate()
|
||||
{
|
||||
// if server then always sync to others.
|
||||
// let the clients know that this has moved
|
||||
if (isServer && HasEitherMovedRotatedScaled())
|
||||
{
|
||||
ServerUpdate();
|
||||
}
|
||||
|
||||
if (isClient)
|
||||
{
|
||||
// send to server if we have local authority (and aren't the server)
|
||||
// -> only if connectionToServer has been initialized yet too
|
||||
if (IsOwnerWithClientAuthority)
|
||||
{
|
||||
ClientAuthorityUpdate();
|
||||
}
|
||||
else if (goal.isValid)
|
||||
{
|
||||
ClientRemoteUpdate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ServerUpdate()
|
||||
{
|
||||
RpcMove(targetTransform.localPosition, Compression.CompressQuaternion(targetTransform.localRotation), targetTransform.localScale);
|
||||
}
|
||||
|
||||
void ClientAuthorityUpdate()
|
||||
{
|
||||
if (!isServer && HasEitherMovedRotatedScaled())
|
||||
{
|
||||
// serialize
|
||||
// local position/rotation for VR support
|
||||
// send to server
|
||||
CmdClientToServerSync(targetTransform.localPosition, Compression.CompressQuaternion(targetTransform.localRotation), targetTransform.localScale);
|
||||
}
|
||||
}
|
||||
|
||||
void ClientRemoteUpdate()
|
||||
{
|
||||
// teleport or interpolate
|
||||
if (NeedsTeleport())
|
||||
{
|
||||
// local position/rotation for VR support
|
||||
ApplyPositionRotationScale(goal.localPosition, goal.localRotation, goal.localScale);
|
||||
|
||||
// reset data points so we don't keep interpolating
|
||||
start = new DataPoint();
|
||||
goal = new DataPoint();
|
||||
}
|
||||
else
|
||||
{
|
||||
// local position/rotation for VR support
|
||||
ApplyPositionRotationScale(InterpolatePosition(start, goal, targetTransform.localPosition),
|
||||
InterpolateRotation(start, goal, targetTransform.localRotation),
|
||||
InterpolateScale(start, goal, targetTransform.localScale));
|
||||
}
|
||||
}
|
||||
|
||||
// moved or rotated or scaled since last time we checked it?
|
||||
bool HasEitherMovedRotatedScaled()
|
||||
{
|
||||
// Save last for next frame to compare only if change was detected, otherwise
|
||||
// slow moving objects might never sync because of C#'s float comparison tolerance.
|
||||
// See also: https://github.com/vis2k/Mirror/pull/428)
|
||||
bool changed = HasMoved || HasRotated || HasScaled;
|
||||
if (changed)
|
||||
{
|
||||
// local position/rotation for VR support
|
||||
if (syncPosition) lastPosition = targetTransform.localPosition;
|
||||
if (syncRotation) lastRotation = targetTransform.localRotation;
|
||||
if (syncScale) lastScale = targetTransform.localScale;
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
// local position/rotation for VR support
|
||||
// SqrMagnitude is faster than Distance per Unity docs
|
||||
// https://docs.unity3d.com/ScriptReference/Vector3-sqrMagnitude.html
|
||||
|
||||
bool HasMoved => syncPosition && Vector3.SqrMagnitude(lastPosition - targetTransform.localPosition) > localPositionSensitivity * localPositionSensitivity;
|
||||
bool HasRotated => syncRotation && Quaternion.Angle(lastRotation, targetTransform.localRotation) > localRotationSensitivity;
|
||||
bool HasScaled => syncScale && Vector3.SqrMagnitude(lastScale - targetTransform.localScale) > localScaleSensitivity * localScaleSensitivity;
|
||||
|
||||
// teleport / lag / stuck detection
|
||||
// - checking distance is not enough since there could be just a tiny fence between us and the goal
|
||||
// - checking time always works, this way we just teleport if we still didn't reach the goal after too much time has elapsed
|
||||
bool NeedsTeleport()
|
||||
{
|
||||
// calculate time between the two data points
|
||||
float startTime = start.isValid ? start.timeStamp : Time.time - Time.fixedDeltaTime;
|
||||
float goalTime = goal.isValid ? goal.timeStamp : Time.time;
|
||||
float difference = goalTime - startTime;
|
||||
float timeSinceGoalReceived = Time.time - goalTime;
|
||||
return timeSinceGoalReceived > difference * 5;
|
||||
}
|
||||
|
||||
// local authority client sends sync message to server for broadcasting
|
||||
[Command(channel = Channels.Unreliable)]
|
||||
void CmdClientToServerSync(Vector3 position, uint packedRotation, Vector3 scale)
|
||||
{
|
||||
// Ignore messages from client if not in client authority mode
|
||||
if (!clientAuthority)
|
||||
return;
|
||||
|
||||
// deserialize payload
|
||||
SetGoal(position, Compression.DecompressQuaternion(packedRotation), scale);
|
||||
|
||||
// server-only mode does no interpolation to save computations, but let's set the position directly
|
||||
if (isServer && !isClient)
|
||||
ApplyPositionRotationScale(goal.localPosition, goal.localRotation, goal.localScale);
|
||||
|
||||
RpcMove(position, packedRotation, scale);
|
||||
}
|
||||
|
||||
[ClientRpc(channel = Channels.Unreliable)]
|
||||
void RpcMove(Vector3 position, uint packedRotation, Vector3 scale)
|
||||
{
|
||||
if (hasAuthority && excludeOwnerUpdate) return;
|
||||
|
||||
if (!isServer)
|
||||
SetGoal(position, Compression.DecompressQuaternion(packedRotation), scale);
|
||||
}
|
||||
|
||||
// serialization is needed by OnSerialize and by manual sending from authority
|
||||
void SetGoal(Vector3 position, Quaternion rotation, Vector3 scale)
|
||||
{
|
||||
// put it into a data point immediately
|
||||
DataPoint temp = new DataPoint
|
||||
{
|
||||
// deserialize position
|
||||
localPosition = position,
|
||||
localRotation = rotation,
|
||||
localScale = scale,
|
||||
timeStamp = Time.time
|
||||
};
|
||||
|
||||
// movement speed: based on how far it moved since last time has to be calculated before 'start' is overwritten
|
||||
temp.movementSpeed = EstimateMovementSpeed(goal, temp, targetTransform, Time.fixedDeltaTime);
|
||||
|
||||
// reassign start wisely
|
||||
// first ever data point? then make something up for previous one so that we can start interpolation without waiting for next.
|
||||
if (start.timeStamp == 0)
|
||||
{
|
||||
start = new DataPoint
|
||||
{
|
||||
timeStamp = Time.time - Time.fixedDeltaTime,
|
||||
// local position/rotation for VR support
|
||||
localPosition = targetTransform.localPosition,
|
||||
localRotation = targetTransform.localRotation,
|
||||
localScale = targetTransform.localScale,
|
||||
movementSpeed = temp.movementSpeed
|
||||
};
|
||||
}
|
||||
// second or nth data point? then update previous
|
||||
// but: we start at where ever we are right now, so that it's perfectly smooth and we don't jump anywhere
|
||||
//
|
||||
// example if we are at 'x':
|
||||
//
|
||||
// A--x->B
|
||||
//
|
||||
// and then receive a new point C:
|
||||
//
|
||||
// A--x--B
|
||||
// |
|
||||
// |
|
||||
// C
|
||||
//
|
||||
// then we don't want to just jump to B and start interpolation:
|
||||
//
|
||||
// x
|
||||
// |
|
||||
// |
|
||||
// C
|
||||
//
|
||||
// we stay at 'x' and interpolate from there to C:
|
||||
//
|
||||
// x..B
|
||||
// \ .
|
||||
// \.
|
||||
// C
|
||||
//
|
||||
else
|
||||
{
|
||||
float oldDistance = Vector3.Distance(start.localPosition, goal.localPosition);
|
||||
float newDistance = Vector3.Distance(goal.localPosition, temp.localPosition);
|
||||
|
||||
start = goal;
|
||||
|
||||
// local position/rotation for VR support
|
||||
// teleport / lag / obstacle detection: only continue at current position if we aren't too far away
|
||||
// XC < AB + BC (see comments above)
|
||||
if (Vector3.Distance(targetTransform.localPosition, start.localPosition) < oldDistance + newDistance)
|
||||
{
|
||||
start.localPosition = targetTransform.localPosition;
|
||||
start.localRotation = targetTransform.localRotation;
|
||||
start.localScale = targetTransform.localScale;
|
||||
}
|
||||
}
|
||||
|
||||
// set new destination in any case. new data is best data.
|
||||
goal = temp;
|
||||
}
|
||||
|
||||
// try to estimate movement speed for a data point based on how far it moved since the previous one
|
||||
// - if this is the first time ever then we use our best guess:
|
||||
// - delta based on transform.localPosition
|
||||
// - elapsed based on send interval hoping that it roughly matches
|
||||
static float EstimateMovementSpeed(DataPoint from, DataPoint to, Transform transform, float sendInterval)
|
||||
{
|
||||
Vector3 delta = to.localPosition - (from.localPosition != transform.localPosition ? from.localPosition : transform.localPosition);
|
||||
float elapsed = from.isValid ? to.timeStamp - from.timeStamp : sendInterval;
|
||||
|
||||
// avoid NaN
|
||||
return elapsed > 0 ? delta.magnitude / elapsed : 0;
|
||||
}
|
||||
|
||||
// set position carefully depending on the target component
|
||||
void ApplyPositionRotationScale(Vector3 position, Quaternion rotation, Vector3 scale)
|
||||
{
|
||||
// local position/rotation for VR support
|
||||
if (syncPosition) targetTransform.localPosition = position;
|
||||
if (syncRotation) targetTransform.localRotation = rotation;
|
||||
if (syncScale) targetTransform.localScale = scale;
|
||||
}
|
||||
|
||||
// where are we in the timeline between start and goal? [0,1]
|
||||
Vector3 InterpolatePosition(DataPoint start, DataPoint goal, Vector3 currentPosition)
|
||||
{
|
||||
if (!interpolatePosition)
|
||||
return currentPosition;
|
||||
|
||||
if (start.movementSpeed != 0)
|
||||
{
|
||||
// Option 1: simply interpolate based on time, but stutter will happen, it's not that smooth.
|
||||
// This is especially noticeable if the camera automatically follows the player
|
||||
// - Tell SonarCloud this isn't really commented code but actual comments and to stfu about it
|
||||
// - float t = CurrentInterpolationFactor();
|
||||
// - return Vector3.Lerp(start.position, goal.position, t);
|
||||
|
||||
// Option 2: always += speed
|
||||
// speed is 0 if we just started after idle, so always use max for best results
|
||||
float speed = Mathf.Max(start.movementSpeed, goal.movementSpeed);
|
||||
return Vector3.MoveTowards(currentPosition, goal.localPosition, speed * Time.deltaTime);
|
||||
}
|
||||
|
||||
return currentPosition;
|
||||
}
|
||||
|
||||
Quaternion InterpolateRotation(DataPoint start, DataPoint goal, Quaternion defaultRotation)
|
||||
{
|
||||
if (!interpolateRotation)
|
||||
return defaultRotation;
|
||||
|
||||
if (start.localRotation != goal.localRotation)
|
||||
{
|
||||
float t = CurrentInterpolationFactor(start, goal);
|
||||
return Quaternion.Slerp(start.localRotation, goal.localRotation, t);
|
||||
}
|
||||
|
||||
return defaultRotation;
|
||||
}
|
||||
|
||||
Vector3 InterpolateScale(DataPoint start, DataPoint goal, Vector3 currentScale)
|
||||
{
|
||||
if (!interpolateScale)
|
||||
return currentScale;
|
||||
|
||||
if (start.localScale != goal.localScale)
|
||||
{
|
||||
float t = CurrentInterpolationFactor(start, goal);
|
||||
return Vector3.Lerp(start.localScale, goal.localScale, t);
|
||||
}
|
||||
|
||||
return currentScale;
|
||||
}
|
||||
|
||||
static float CurrentInterpolationFactor(DataPoint start, DataPoint goal)
|
||||
{
|
||||
if (start.isValid)
|
||||
{
|
||||
float difference = goal.timeStamp - start.timeStamp;
|
||||
|
||||
// the moment we get 'goal', 'start' is supposed to start, so elapsed time is based on:
|
||||
float elapsed = Time.time - goal.timeStamp;
|
||||
|
||||
// avoid NaN
|
||||
return difference > 0 ? elapsed / difference : 1;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
#region Server Teleport (force move player)
|
||||
|
||||
/// <summary>
|
||||
/// This method will override this GameObject's current Transform.localPosition to the specified Vector3 and update all clients.
|
||||
/// <para>NOTE: position must be in LOCAL space if the transform has a parent</para>
|
||||
/// </summary>
|
||||
/// <param name="localPosition">Where to teleport this GameObject</param>
|
||||
[Server]
|
||||
public void ServerTeleport(Vector3 localPosition)
|
||||
{
|
||||
Quaternion localRotation = targetTransform.localRotation;
|
||||
ServerTeleport(localPosition, localRotation);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This method will override this GameObject's current Transform.localPosition and Transform.localRotation
|
||||
/// to the specified Vector3 and Quaternion and update all clients.
|
||||
/// <para>NOTE: localPosition must be in LOCAL space if the transform has a parent</para>
|
||||
/// <para>NOTE: localRotation must be in LOCAL space if the transform has a parent</para>
|
||||
/// </summary>
|
||||
/// <param name="localPosition">Where to teleport this GameObject</param>
|
||||
/// <param name="localRotation">Which rotation to set this GameObject</param>
|
||||
[Server]
|
||||
public void ServerTeleport(Vector3 localPosition, Quaternion localRotation)
|
||||
{
|
||||
// To prevent applying the position updates received from client (if they have ClientAuth) while being teleported.
|
||||
// clientAuthorityBeforeTeleport defaults to false when not teleporting, if it is true then it means that teleport
|
||||
// was previously called but not finished therefore we should keep it as true so that 2nd teleport call doesn't clear authority
|
||||
clientAuthorityBeforeTeleport = clientAuthority || clientAuthorityBeforeTeleport;
|
||||
clientAuthority = false;
|
||||
|
||||
DoTeleport(localPosition, localRotation);
|
||||
|
||||
// tell all clients about new values
|
||||
RpcTeleport(localPosition, Compression.CompressQuaternion(localRotation), clientAuthorityBeforeTeleport);
|
||||
}
|
||||
|
||||
void DoTeleport(Vector3 newLocalPosition, Quaternion newLocalRotation)
|
||||
{
|
||||
targetTransform.localPosition = newLocalPosition;
|
||||
targetTransform.localRotation = newLocalRotation;
|
||||
|
||||
// Since we are overriding the position we don't need a goal and start.
|
||||
// Reset them to null for fresh start
|
||||
goal = new DataPoint();
|
||||
start = new DataPoint();
|
||||
lastPosition = newLocalPosition;
|
||||
lastRotation = newLocalRotation;
|
||||
}
|
||||
|
||||
[ClientRpc(channel = Channels.Unreliable)]
|
||||
void RpcTeleport(Vector3 newPosition, uint newPackedRotation, bool isClientAuthority)
|
||||
{
|
||||
DoTeleport(newPosition, Compression.DecompressQuaternion(newPackedRotation));
|
||||
|
||||
// only send finished if is owner and is ClientAuthority on server
|
||||
if (hasAuthority && isClientAuthority)
|
||||
CmdTeleportFinished();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This RPC will be invoked on server after client finishes overriding the position.
|
||||
/// </summary>
|
||||
/// <param name="initialAuthority"></param>
|
||||
[Command(channel = Channels.Unreliable)]
|
||||
void CmdTeleportFinished()
|
||||
{
|
||||
if (clientAuthorityBeforeTeleport)
|
||||
{
|
||||
clientAuthority = true;
|
||||
|
||||
// reset value so doesn't effect future calls, see note in ServerTeleport
|
||||
clientAuthorityBeforeTeleport = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning("Client called TeleportFinished when clientAuthority was false on server", this);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Debug Gizmos
|
||||
|
||||
// draw the data points for easier debugging
|
||||
void OnDrawGizmos()
|
||||
{
|
||||
// draw start and goal points and a line between them
|
||||
if (start.localPosition != goal.localPosition)
|
||||
{
|
||||
DrawDataPointGizmo(start, Color.yellow);
|
||||
DrawDataPointGizmo(goal, Color.green);
|
||||
DrawLineBetweenDataPoints(start, goal, Color.cyan);
|
||||
}
|
||||
}
|
||||
|
||||
static void DrawDataPointGizmo(DataPoint data, Color color)
|
||||
{
|
||||
// use a little offset because transform.localPosition might be in the ground in many cases
|
||||
Vector3 offset = Vector3.up * 0.01f;
|
||||
|
||||
// draw position
|
||||
Gizmos.color = color;
|
||||
Gizmos.DrawSphere(data.localPosition + offset, 0.5f);
|
||||
|
||||
// draw forward and up like unity move tool
|
||||
Gizmos.color = Color.blue;
|
||||
Gizmos.DrawRay(data.localPosition + offset, data.localRotation * Vector3.forward);
|
||||
Gizmos.color = Color.green;
|
||||
Gizmos.DrawRay(data.localPosition + offset, data.localRotation * Vector3.up);
|
||||
}
|
||||
|
||||
static void DrawLineBetweenDataPoints(DataPoint data1, DataPoint data2, Color color)
|
||||
{
|
||||
Gizmos.color = color;
|
||||
Gizmos.DrawLine(data1.localPosition, data2.localPosition);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: ea7c690c4fbf8c4439726f4c62eda6d3
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData: ''
|
||||
assetBundleName: ''
|
||||
assetBundleVariant: ''
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
using UnityEngine;
|
||||
|
||||
namespace Mirror.Experimental
|
||||
{
|
||||
/// <summary>
|
||||
/// A component to synchronize the position of child transforms of networked objects.
|
||||
/// <para>There must be a NetworkTransform on the root object of the hierarchy. There can be multiple NetworkTransformChild components on an object. This does not use physics for synchronization, it simply synchronizes the localPosition and localRotation of the child transform and lerps towards the received values.</para>
|
||||
/// </summary>
|
||||
[AddComponentMenu("Network/Experimental/NetworkTransformChildExperimentalExperimental")]
|
||||
[HelpURL("https://mirror-networking.gitbook.io/docs/components/network-transform-child")]
|
||||
public class NetworkTransformChild : NetworkTransformBase
|
||||
{
|
||||
[Header("Target")]
|
||||
public Transform target;
|
||||
|
||||
protected override Transform targetTransform => target;
|
||||
}
|
||||
}
|
||||
|
|
@ -60,14 +60,17 @@ namespace Mirror
|
|||
void OnLog(string message, string stackTrace, LogType type)
|
||||
{
|
||||
// is this important?
|
||||
bool isImportant = type == LogType.Error || type == LogType.Exception;
|
||||
// => always show exceptions & errors
|
||||
// => usually a good idea to show warnings too, otherwise it's too
|
||||
// easy to miss OnDeserialize warnings etc. in builds
|
||||
bool isImportant = type == LogType.Error || type == LogType.Exception || type == LogType.Warning;
|
||||
|
||||
// use stack trace only if important
|
||||
// (otherwise users would have to find and search the log file.
|
||||
// seeing it in the console directly is way easier to deal with.)
|
||||
// => only add \n if stack trace is available (only in debug builds)
|
||||
if (isImportant && !string.IsNullOrWhiteSpace(stackTrace))
|
||||
message += "\n" + stackTrace;
|
||||
message += $"\n{stackTrace}";
|
||||
|
||||
// add to queue
|
||||
log.Enqueue(new LogEntry(message, type));
|
||||
|
|
|
|||
|
|
@ -6,6 +6,6 @@ MonoImporter:
|
|||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData: ''
|
||||
assetBundleName: ''
|
||||
assetBundleVariant: ''
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@ guid: c66f27e006ab94253b39a55a3b213651
|
|||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData: ''
|
||||
assetBundleName: ''
|
||||
assetBundleVariant: ''
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
fileFormatVersion: 2
|
||||
fileFormatVersion: 2
|
||||
guid: fa4cbc6b9c584db4971985cb9f369077
|
||||
timeCreated: 1613110605
|
||||
|
|
@ -4,23 +4,38 @@ using UnityEngine;
|
|||
|
||||
namespace Mirror
|
||||
{
|
||||
[AddComponentMenu("Network/ Interest Management/ Distance/Distance Interest Management")]
|
||||
public class DistanceInterestManagement : InterestManagement
|
||||
{
|
||||
[Tooltip("The maximum range that objects will be visible at.")]
|
||||
[Tooltip("The maximum range that objects will be visible at. Add DistanceInterestManagementCustomRange onto NetworkIdentities for custom ranges.")]
|
||||
public int visRange = 10;
|
||||
|
||||
[Tooltip("Rebuild all every 'rebuildInterval' seconds.")]
|
||||
public float rebuildInterval = 1;
|
||||
double lastRebuildTime;
|
||||
|
||||
public override bool OnCheckObserver(NetworkIdentity identity, NetworkConnection newObserver)
|
||||
// helper function to get vis range for a given object, or default.
|
||||
int GetVisRange(NetworkIdentity identity)
|
||||
{
|
||||
return Vector3.Distance(identity.transform.position, newObserver.identity.transform.position) <= visRange;
|
||||
return identity.TryGetComponent(out DistanceInterestManagementCustomRange custom) ? custom.visRange : visRange;
|
||||
}
|
||||
|
||||
public override void OnRebuildObservers(NetworkIdentity identity, HashSet<NetworkConnection> newObservers, bool initialize)
|
||||
[ServerCallback]
|
||||
public override void Reset()
|
||||
{
|
||||
// 'transform.' calls GetComponent, only do it once
|
||||
lastRebuildTime = 0D;
|
||||
}
|
||||
|
||||
public override bool OnCheckObserver(NetworkIdentity identity, NetworkConnectionToClient newObserver)
|
||||
{
|
||||
int range = GetVisRange(identity);
|
||||
return Vector3.Distance(identity.transform.position, newObserver.identity.transform.position) < range;
|
||||
}
|
||||
|
||||
public override void OnRebuildObservers(NetworkIdentity identity, HashSet<NetworkConnectionToClient> newObservers)
|
||||
{
|
||||
// cache range and .transform because both call GetComponent.
|
||||
int range = GetVisRange(identity);
|
||||
Vector3 position = identity.transform.position;
|
||||
|
||||
// brute force distance check
|
||||
|
|
@ -36,7 +51,7 @@ namespace Mirror
|
|||
if (conn != null && conn.isAuthenticated && conn.identity != null)
|
||||
{
|
||||
// check distance
|
||||
if (Vector3.Distance(conn.identity.transform.position, position) < visRange)
|
||||
if (Vector3.Distance(conn.identity.transform.position, position) < range)
|
||||
{
|
||||
newObservers.Add(conn);
|
||||
}
|
||||
|
|
@ -44,16 +59,15 @@ namespace Mirror
|
|||
}
|
||||
}
|
||||
|
||||
void Update()
|
||||
// internal so we can update from tests
|
||||
[ServerCallback]
|
||||
internal void Update()
|
||||
{
|
||||
// only on server
|
||||
if (!NetworkServer.active) return;
|
||||
|
||||
// rebuild all spawned NetworkIdentity's observers every interval
|
||||
if (NetworkTime.time >= lastRebuildTime + rebuildInterval)
|
||||
if (NetworkTime.localTime >= lastRebuildTime + rebuildInterval)
|
||||
{
|
||||
RebuildAll();
|
||||
lastRebuildTime = NetworkTime.time;
|
||||
lastRebuildTime = NetworkTime.localTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,6 @@ MonoImporter:
|
|||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData: ''
|
||||
assetBundleName: ''
|
||||
assetBundleVariant: ''
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
// add this to NetworkIdentities for custom range if needed.
|
||||
// only works with DistanceInterestManagement.
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
[DisallowMultipleComponent]
|
||||
[AddComponentMenu("Network/ Interest Management/ Distance/Distance Custom Range")]
|
||||
[HelpURL("https://mirror-networking.gitbook.io/docs/guides/interest-management")]
|
||||
public class DistanceInterestManagementCustomRange : NetworkBehaviour
|
||||
{
|
||||
[Tooltip("The maximum range that objects will be visible at.")]
|
||||
public int visRange = 20;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 741bbe11f5357b44593b15c0d11b16bd
|
||||
guid: b2e242ee38a14076a39934172a19079b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData: ''
|
||||
assetBundleName: ''
|
||||
assetBundleVariant: ''
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 5eca5245ae6bb460e9a92f7e14d5493a
|
||||
timeCreated: 1622649517
|
||||
|
|
@ -0,0 +1,160 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
[AddComponentMenu("Network/ Interest Management/ Match/Match Interest Management")]
|
||||
public class MatchInterestManagement : InterestManagement
|
||||
{
|
||||
readonly Dictionary<Guid, HashSet<NetworkIdentity>> matchObjects =
|
||||
new Dictionary<Guid, HashSet<NetworkIdentity>>();
|
||||
|
||||
readonly Dictionary<NetworkIdentity, Guid> lastObjectMatch =
|
||||
new Dictionary<NetworkIdentity, Guid>();
|
||||
|
||||
readonly HashSet<Guid> dirtyMatches = new HashSet<Guid>();
|
||||
|
||||
public override void OnSpawned(NetworkIdentity identity)
|
||||
{
|
||||
if (!identity.TryGetComponent<NetworkMatch>(out NetworkMatch networkMatch))
|
||||
return;
|
||||
|
||||
Guid currentMatch = networkMatch.matchId;
|
||||
lastObjectMatch[identity] = currentMatch;
|
||||
|
||||
// Guid.Empty is never a valid matchId...do not add to matchObjects collection
|
||||
if (currentMatch == Guid.Empty)
|
||||
return;
|
||||
|
||||
// Debug.Log($"MatchInterestManagement.OnSpawned({identity.name}) currentMatch: {currentMatch}");
|
||||
if (!matchObjects.TryGetValue(currentMatch, out HashSet<NetworkIdentity> objects))
|
||||
{
|
||||
objects = new HashSet<NetworkIdentity>();
|
||||
matchObjects.Add(currentMatch, objects);
|
||||
}
|
||||
|
||||
objects.Add(identity);
|
||||
}
|
||||
|
||||
public override void OnDestroyed(NetworkIdentity identity)
|
||||
{
|
||||
lastObjectMatch.TryGetValue(identity, out Guid currentMatch);
|
||||
lastObjectMatch.Remove(identity);
|
||||
if (currentMatch != Guid.Empty && matchObjects.TryGetValue(currentMatch, out HashSet<NetworkIdentity> objects) && objects.Remove(identity))
|
||||
RebuildMatchObservers(currentMatch);
|
||||
}
|
||||
|
||||
// internal so we can update from tests
|
||||
[ServerCallback]
|
||||
internal void Update()
|
||||
{
|
||||
// for each spawned:
|
||||
// if match changed:
|
||||
// add previous to dirty
|
||||
// add new to dirty
|
||||
foreach (NetworkIdentity netIdentity in NetworkServer.spawned.Values)
|
||||
{
|
||||
// Ignore objects that don't have a NetworkMatch component
|
||||
if (!netIdentity.TryGetComponent<NetworkMatch>(out NetworkMatch networkMatch))
|
||||
continue;
|
||||
|
||||
Guid newMatch = networkMatch.matchId;
|
||||
lastObjectMatch.TryGetValue(netIdentity, out Guid currentMatch);
|
||||
|
||||
// Guid.Empty is never a valid matchId
|
||||
// Nothing to do if matchId hasn't changed
|
||||
if (newMatch == Guid.Empty || newMatch == currentMatch)
|
||||
continue;
|
||||
|
||||
// Mark new/old matches as dirty so they get rebuilt
|
||||
UpdateDirtyMatches(newMatch, currentMatch);
|
||||
|
||||
// This object is in a new match so observers in the prior match
|
||||
// and the new match need to rebuild their respective observers lists.
|
||||
UpdateMatchObjects(netIdentity, newMatch, currentMatch);
|
||||
}
|
||||
|
||||
// rebuild all dirty matches
|
||||
foreach (Guid dirtyMatch in dirtyMatches)
|
||||
RebuildMatchObservers(dirtyMatch);
|
||||
|
||||
dirtyMatches.Clear();
|
||||
}
|
||||
|
||||
void UpdateDirtyMatches(Guid newMatch, Guid currentMatch)
|
||||
{
|
||||
// Guid.Empty is never a valid matchId
|
||||
if (currentMatch != Guid.Empty)
|
||||
dirtyMatches.Add(currentMatch);
|
||||
|
||||
dirtyMatches.Add(newMatch);
|
||||
}
|
||||
|
||||
void UpdateMatchObjects(NetworkIdentity netIdentity, Guid newMatch, Guid currentMatch)
|
||||
{
|
||||
// Remove this object from the hashset of the match it just left
|
||||
// Guid.Empty is never a valid matchId
|
||||
if (currentMatch != Guid.Empty)
|
||||
matchObjects[currentMatch].Remove(netIdentity);
|
||||
|
||||
// Set this to the new match this object just entered
|
||||
lastObjectMatch[netIdentity] = newMatch;
|
||||
|
||||
// Make sure this new match is in the dictionary
|
||||
if (!matchObjects.ContainsKey(newMatch))
|
||||
matchObjects.Add(newMatch, new HashSet<NetworkIdentity>());
|
||||
|
||||
// Add this object to the hashset of the new match
|
||||
matchObjects[newMatch].Add(netIdentity);
|
||||
}
|
||||
|
||||
void RebuildMatchObservers(Guid matchId)
|
||||
{
|
||||
foreach (NetworkIdentity netIdentity in matchObjects[matchId])
|
||||
if (netIdentity != null)
|
||||
NetworkServer.RebuildObservers(netIdentity, false);
|
||||
}
|
||||
|
||||
public override bool OnCheckObserver(NetworkIdentity identity, NetworkConnectionToClient newObserver)
|
||||
{
|
||||
// Never observed if no NetworkMatch component
|
||||
if (!identity.TryGetComponent<NetworkMatch>(out NetworkMatch identityNetworkMatch))
|
||||
return false;
|
||||
|
||||
// Guid.Empty is never a valid matchId
|
||||
if (identityNetworkMatch.matchId == Guid.Empty)
|
||||
return false;
|
||||
|
||||
// Never observed if no NetworkMatch component
|
||||
if (!newObserver.identity.TryGetComponent<NetworkMatch>(out NetworkMatch newObserverNetworkMatch))
|
||||
return false;
|
||||
|
||||
// Guid.Empty is never a valid matchId
|
||||
if (newObserverNetworkMatch.matchId == Guid.Empty)
|
||||
return false;
|
||||
|
||||
return identityNetworkMatch.matchId == newObserverNetworkMatch.matchId;
|
||||
}
|
||||
|
||||
public override void OnRebuildObservers(NetworkIdentity identity, HashSet<NetworkConnectionToClient> newObservers)
|
||||
{
|
||||
if (!identity.TryGetComponent<NetworkMatch>(out NetworkMatch networkMatch))
|
||||
return;
|
||||
|
||||
Guid matchId = networkMatch.matchId;
|
||||
|
||||
// Guid.Empty is never a valid matchId
|
||||
if (matchId == Guid.Empty)
|
||||
return;
|
||||
|
||||
if (!matchObjects.TryGetValue(matchId, out HashSet<NetworkIdentity> objects))
|
||||
return;
|
||||
|
||||
// Add everything in the hashset for this object's current match
|
||||
foreach (NetworkIdentity networkIdentity in objects)
|
||||
if (networkIdentity != null && networkIdentity.connectionToClient != null)
|
||||
newObservers.Add(networkIdentity.connectionToClient);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: d09f5c8bf2f4747b7a9284ef5d9ce2a7
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
// simple component that holds match information
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
[DisallowMultipleComponent]
|
||||
[AddComponentMenu("Network/ Interest Management/ Match/Network Match")]
|
||||
[HelpURL("https://mirror-networking.gitbook.io/docs/guides/interest-management")]
|
||||
public class NetworkMatch : NetworkBehaviour
|
||||
{
|
||||
///<summary>Set this to the same value on all networked objects that belong to a given match</summary>
|
||||
public Guid matchId;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 5d17e718851449a6879986e45c458fb7
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 7655d309a46a4bd4860edf964228b3f6
|
||||
timeCreated: 1622649517
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
[AddComponentMenu("Network/ Interest Management/ Scene/Scene Interest Management")]
|
||||
public class SceneInterestManagement : InterestManagement
|
||||
{
|
||||
// Use Scene instead of string scene.name because when additively
|
||||
// loading multiples of a subscene the name won't be unique
|
||||
readonly Dictionary<Scene, HashSet<NetworkIdentity>> sceneObjects =
|
||||
new Dictionary<Scene, HashSet<NetworkIdentity>>();
|
||||
|
||||
readonly Dictionary<NetworkIdentity, Scene> lastObjectScene =
|
||||
new Dictionary<NetworkIdentity, Scene>();
|
||||
|
||||
HashSet<Scene> dirtyScenes = new HashSet<Scene>();
|
||||
|
||||
public override void OnSpawned(NetworkIdentity identity)
|
||||
{
|
||||
Scene currentScene = identity.gameObject.scene;
|
||||
lastObjectScene[identity] = currentScene;
|
||||
// Debug.Log($"SceneInterestManagement.OnSpawned({identity.name}) currentScene: {currentScene}");
|
||||
if (!sceneObjects.TryGetValue(currentScene, out HashSet<NetworkIdentity> objects))
|
||||
{
|
||||
objects = new HashSet<NetworkIdentity>();
|
||||
sceneObjects.Add(currentScene, objects);
|
||||
}
|
||||
|
||||
objects.Add(identity);
|
||||
}
|
||||
|
||||
public override void OnDestroyed(NetworkIdentity identity)
|
||||
{
|
||||
Scene currentScene = lastObjectScene[identity];
|
||||
lastObjectScene.Remove(identity);
|
||||
if (sceneObjects.TryGetValue(currentScene, out HashSet<NetworkIdentity> objects) && objects.Remove(identity))
|
||||
RebuildSceneObservers(currentScene);
|
||||
}
|
||||
|
||||
// internal so we can update from tests
|
||||
[ServerCallback]
|
||||
internal void Update()
|
||||
{
|
||||
// for each spawned:
|
||||
// if scene changed:
|
||||
// add previous to dirty
|
||||
// add new to dirty
|
||||
foreach (NetworkIdentity identity in NetworkServer.spawned.Values)
|
||||
{
|
||||
Scene currentScene = lastObjectScene[identity];
|
||||
Scene newScene = identity.gameObject.scene;
|
||||
if (newScene == currentScene)
|
||||
continue;
|
||||
|
||||
// Mark new/old scenes as dirty so they get rebuilt
|
||||
dirtyScenes.Add(currentScene);
|
||||
dirtyScenes.Add(newScene);
|
||||
|
||||
// This object is in a new scene so observers in the prior scene
|
||||
// and the new scene need to rebuild their respective observers lists.
|
||||
|
||||
// Remove this object from the hashset of the scene it just left
|
||||
sceneObjects[currentScene].Remove(identity);
|
||||
|
||||
// Set this to the new scene this object just entered
|
||||
lastObjectScene[identity] = newScene;
|
||||
|
||||
// Make sure this new scene is in the dictionary
|
||||
if (!sceneObjects.ContainsKey(newScene))
|
||||
sceneObjects.Add(newScene, new HashSet<NetworkIdentity>());
|
||||
|
||||
// Add this object to the hashset of the new scene
|
||||
sceneObjects[newScene].Add(identity);
|
||||
}
|
||||
|
||||
// rebuild all dirty scenes
|
||||
foreach (Scene dirtyScene in dirtyScenes)
|
||||
RebuildSceneObservers(dirtyScene);
|
||||
|
||||
dirtyScenes.Clear();
|
||||
}
|
||||
|
||||
void RebuildSceneObservers(Scene scene)
|
||||
{
|
||||
foreach (NetworkIdentity netIdentity in sceneObjects[scene])
|
||||
if (netIdentity != null)
|
||||
NetworkServer.RebuildObservers(netIdentity, false);
|
||||
}
|
||||
|
||||
public override bool OnCheckObserver(NetworkIdentity identity, NetworkConnectionToClient newObserver)
|
||||
{
|
||||
return identity.gameObject.scene == newObserver.identity.gameObject.scene;
|
||||
}
|
||||
|
||||
public override void OnRebuildObservers(NetworkIdentity identity, HashSet<NetworkConnectionToClient> newObservers)
|
||||
{
|
||||
if (!sceneObjects.TryGetValue(identity.gameObject.scene, out HashSet<NetworkIdentity> objects))
|
||||
return;
|
||||
|
||||
// Add everything in the hashset for this object's current scene
|
||||
foreach (NetworkIdentity networkIdentity in objects)
|
||||
if (networkIdentity != null && networkIdentity.connectionToClient != null)
|
||||
newObservers.Add(networkIdentity.connectionToClient);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: b979f26c95d34324ba005bfacfa9c4fc
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
fileFormatVersion: 2
|
||||
fileFormatVersion: 2
|
||||
guid: cfa12b73503344d49b398b01bcb07967
|
||||
timeCreated: 1613110634
|
||||
|
|
@ -6,6 +6,6 @@ MonoImporter:
|
|||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData: ''
|
||||
assetBundleName: ''
|
||||
assetBundleVariant: ''
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
|
|||
|
|
@ -6,13 +6,23 @@ using UnityEngine;
|
|||
|
||||
namespace Mirror
|
||||
{
|
||||
[AddComponentMenu("Network/ Interest Management/ Spatial Hash/Spatial Hashing Interest Management")]
|
||||
public class SpatialHashingInterestManagement : InterestManagement
|
||||
{
|
||||
[Tooltip("The maximum range that objects will be visible at.")]
|
||||
public int visRange = 30;
|
||||
|
||||
// if we see 8 neighbors then 1 entry is visRange/3
|
||||
public int resolution => visRange / 3;
|
||||
// we use a 9 neighbour grid.
|
||||
// so we always see in a distance of 2 grids.
|
||||
// for example, our own grid and then one on top / below / left / right.
|
||||
//
|
||||
// this means that grid resolution needs to be distance / 2.
|
||||
// so for example, for distance = 30 we see 2 cells = 15 * 2 distance.
|
||||
//
|
||||
// on first sight, it seems we need distance / 3 (we see left/us/right).
|
||||
// but that's not the case.
|
||||
// resolution would be 10, and we only see 1 cell far, so 10+10=20.
|
||||
public int resolution => visRange / 2;
|
||||
|
||||
[Tooltip("Rebuild all every 'rebuildInterval' seconds.")]
|
||||
public float rebuildInterval = 1;
|
||||
|
|
@ -30,7 +40,7 @@ namespace Mirror
|
|||
public bool showSlider;
|
||||
|
||||
// the grid
|
||||
Grid2D<NetworkConnection> grid = new Grid2D<NetworkConnection>();
|
||||
Grid2D<NetworkConnectionToClient> grid = new Grid2D<NetworkConnectionToClient>();
|
||||
|
||||
// project 3d world position to grid position
|
||||
Vector2Int ProjectToGrid(Vector3 position) =>
|
||||
|
|
@ -38,7 +48,7 @@ namespace Mirror
|
|||
? Vector2Int.RoundToInt(new Vector2(position.x, position.z) / resolution)
|
||||
: Vector2Int.RoundToInt(new Vector2(position.x, position.y) / resolution);
|
||||
|
||||
public override bool OnCheckObserver(NetworkIdentity identity, NetworkConnection newObserver)
|
||||
public override bool OnCheckObserver(NetworkIdentity identity, NetworkConnectionToClient newObserver)
|
||||
{
|
||||
// calculate projected positions
|
||||
Vector2Int projected = ProjectToGrid(identity.transform.position);
|
||||
|
|
@ -51,7 +61,7 @@ namespace Mirror
|
|||
return (projected - observerProjected).sqrMagnitude <= 2;
|
||||
}
|
||||
|
||||
public override void OnRebuildObservers(NetworkIdentity identity, HashSet<NetworkConnection> newObservers, bool initialize)
|
||||
public override void OnRebuildObservers(NetworkIdentity identity, HashSet<NetworkConnectionToClient> newObservers)
|
||||
{
|
||||
// add everyone in 9 neighbour grid
|
||||
// -> pass observers to GetWithNeighbours directly to avoid allocations
|
||||
|
|
@ -60,12 +70,19 @@ namespace Mirror
|
|||
grid.GetWithNeighbours(current, newObservers);
|
||||
}
|
||||
|
||||
[ServerCallback]
|
||||
public override void Reset()
|
||||
{
|
||||
lastRebuildTime = 0D;
|
||||
}
|
||||
|
||||
// update everyone's position in the grid
|
||||
// (internal so we can update from tests)
|
||||
[ServerCallback]
|
||||
internal void Update()
|
||||
{
|
||||
// only on server
|
||||
if (!NetworkServer.active) return;
|
||||
// NOTE: unlike Scene/MatchInterestManagement, this rebuilds ALL
|
||||
// entities every INTERVAL. consider the other approach later.
|
||||
|
||||
// IMPORTANT: refresh grid every update!
|
||||
// => newly spawned entities get observers assigned via
|
||||
|
|
@ -103,13 +120,15 @@ namespace Mirror
|
|||
// rebuild all spawned entities' observers every 'interval'
|
||||
// this will call OnRebuildObservers which then returns the
|
||||
// observers at grid[position] for each entity.
|
||||
if (NetworkTime.time >= lastRebuildTime + rebuildInterval)
|
||||
if (NetworkTime.localTime >= lastRebuildTime + rebuildInterval)
|
||||
{
|
||||
RebuildAll();
|
||||
lastRebuildTime = NetworkTime.time;
|
||||
lastRebuildTime = NetworkTime.localTime;
|
||||
}
|
||||
}
|
||||
|
||||
// OnGUI allocates even if it does nothing. avoid in release.
|
||||
#if UNITY_EDITOR || DEVELOPMENT_BUILD
|
||||
// slider from dotsnet. it's nice to play around with in the benchmark
|
||||
// demo.
|
||||
void OnGUI()
|
||||
|
|
@ -129,5 +148,6 @@ namespace Mirror
|
|||
GUILayout.EndHorizontal();
|
||||
GUILayout.EndArea();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,6 @@ MonoImporter:
|
|||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData: ''
|
||||
assetBundleName: ''
|
||||
assetBundleVariant: ''
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 0995e08af14888348b42ecaa6eb21544
|
||||
guid: 2d418e60072433b4bbebbf5f3a7de1bb
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
// simple component that holds team information
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
[DisallowMultipleComponent]
|
||||
[AddComponentMenu("Network/ Interest Management/ Team/Network Team")]
|
||||
[HelpURL("https://mirror-networking.gitbook.io/docs/guides/interest-management")]
|
||||
public class NetworkTeam : NetworkBehaviour
|
||||
{
|
||||
[Tooltip("Set this to the same value on all networked objects that belong to a given team")]
|
||||
public string teamId = string.Empty;
|
||||
|
||||
[Tooltip("When enabled this object is visible to all clients. Typically this would be true for player objects")]
|
||||
public bool forceShown;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 2576730625b1632468cbcbfe5e721f88
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,191 @@
|
|||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
[AddComponentMenu("Network/ Interest Management/ Team/Team Interest Management")]
|
||||
public class TeamInterestManagement : InterestManagement
|
||||
{
|
||||
readonly Dictionary<string, HashSet<NetworkIdentity>> teamObjects =
|
||||
new Dictionary<string, HashSet<NetworkIdentity>>();
|
||||
|
||||
readonly Dictionary<NetworkIdentity, string> lastObjectTeam =
|
||||
new Dictionary<NetworkIdentity, string>();
|
||||
|
||||
readonly HashSet<string> dirtyTeams = new HashSet<string>();
|
||||
|
||||
public override void OnSpawned(NetworkIdentity identity)
|
||||
{
|
||||
if (!identity.TryGetComponent<NetworkTeam>(out NetworkTeam networkTeam))
|
||||
return;
|
||||
|
||||
string currentTeam = networkTeam.teamId;
|
||||
lastObjectTeam[identity] = currentTeam;
|
||||
|
||||
// Null / Empty string is never a valid teamId...do not add to teamObjects collection
|
||||
if (string.IsNullOrWhiteSpace(currentTeam))
|
||||
return;
|
||||
|
||||
// Debug.Log($"MatchInterestManagement.OnSpawned({identity.name}) currentMatch: {currentTeam}");
|
||||
if (!teamObjects.TryGetValue(currentTeam, out HashSet<NetworkIdentity> objects))
|
||||
{
|
||||
objects = new HashSet<NetworkIdentity>();
|
||||
teamObjects.Add(currentTeam, objects);
|
||||
}
|
||||
|
||||
objects.Add(identity);
|
||||
}
|
||||
|
||||
public override void OnDestroyed(NetworkIdentity identity)
|
||||
{
|
||||
if (lastObjectTeam.TryGetValue(identity, out string currentTeam))
|
||||
{
|
||||
lastObjectTeam.Remove(identity);
|
||||
if (!string.IsNullOrWhiteSpace(currentTeam)
|
||||
&& teamObjects.TryGetValue(currentTeam, out HashSet<NetworkIdentity> objects)
|
||||
&& objects.Remove(identity))
|
||||
RebuildTeamObservers(currentTeam);
|
||||
}
|
||||
}
|
||||
|
||||
// internal so we can update from tests
|
||||
[ServerCallback]
|
||||
internal void Update()
|
||||
{
|
||||
// for each spawned:
|
||||
// if team changed:
|
||||
// add previous to dirty
|
||||
// add new to dirty
|
||||
foreach (NetworkIdentity netIdentity in NetworkServer.spawned.Values)
|
||||
{
|
||||
// Ignore objects that don't have a NetworkTeam component
|
||||
if (!netIdentity.TryGetComponent<NetworkTeam>(out NetworkTeam networkTeam))
|
||||
continue;
|
||||
|
||||
string newTeam = networkTeam.teamId;
|
||||
if (!lastObjectTeam.TryGetValue(netIdentity, out string currentTeam))
|
||||
continue;
|
||||
|
||||
// Null / Empty string is never a valid teamId
|
||||
// Nothing to do if teamId hasn't changed
|
||||
if (string.IsNullOrWhiteSpace(newTeam) || newTeam == currentTeam)
|
||||
continue;
|
||||
|
||||
// Mark new/old Teams as dirty so they get rebuilt
|
||||
UpdateDirtyTeams(newTeam, currentTeam);
|
||||
|
||||
// This object is in a new team so observers in the prior team
|
||||
// and the new team need to rebuild their respective observers lists.
|
||||
UpdateTeamObjects(netIdentity, newTeam, currentTeam);
|
||||
}
|
||||
|
||||
// rebuild all dirty teams
|
||||
foreach (string dirtyTeam in dirtyTeams)
|
||||
RebuildTeamObservers(dirtyTeam);
|
||||
|
||||
dirtyTeams.Clear();
|
||||
}
|
||||
|
||||
void UpdateDirtyTeams(string newTeam, string currentTeam)
|
||||
{
|
||||
// Null / Empty string is never a valid teamId
|
||||
if (!string.IsNullOrWhiteSpace(currentTeam))
|
||||
dirtyTeams.Add(currentTeam);
|
||||
|
||||
dirtyTeams.Add(newTeam);
|
||||
}
|
||||
|
||||
void UpdateTeamObjects(NetworkIdentity netIdentity, string newTeam, string currentTeam)
|
||||
{
|
||||
// Remove this object from the hashset of the team it just left
|
||||
// string.Empty is never a valid teamId
|
||||
if (!string.IsNullOrWhiteSpace(currentTeam))
|
||||
teamObjects[currentTeam].Remove(netIdentity);
|
||||
|
||||
// Set this to the new team this object just entered
|
||||
lastObjectTeam[netIdentity] = newTeam;
|
||||
|
||||
// Make sure this new team is in the dictionary
|
||||
if (!teamObjects.ContainsKey(newTeam))
|
||||
teamObjects.Add(newTeam, new HashSet<NetworkIdentity>());
|
||||
|
||||
// Add this object to the hashset of the new team
|
||||
teamObjects[newTeam].Add(netIdentity);
|
||||
}
|
||||
|
||||
void RebuildTeamObservers(string teamId)
|
||||
{
|
||||
foreach (NetworkIdentity netIdentity in teamObjects[teamId])
|
||||
if (netIdentity != null)
|
||||
NetworkServer.RebuildObservers(netIdentity, false);
|
||||
}
|
||||
|
||||
public override bool OnCheckObserver(NetworkIdentity identity, NetworkConnectionToClient newObserver)
|
||||
{
|
||||
// Always observed if no NetworkTeam component
|
||||
if (!identity.TryGetComponent<NetworkTeam>(out NetworkTeam identityNetworkTeam))
|
||||
return true;
|
||||
|
||||
if (identityNetworkTeam.forceShown)
|
||||
return true;
|
||||
|
||||
// string.Empty is never a valid teamId
|
||||
if (string.IsNullOrWhiteSpace(identityNetworkTeam.teamId))
|
||||
return false;
|
||||
|
||||
// Always observed if no NetworkTeam component
|
||||
if (!newObserver.identity.TryGetComponent<NetworkTeam>(out NetworkTeam newObserverNetworkTeam))
|
||||
return true;
|
||||
|
||||
if (newObserverNetworkTeam.forceShown)
|
||||
return true;
|
||||
|
||||
// Null / Empty string is never a valid teamId
|
||||
if (string.IsNullOrWhiteSpace(newObserverNetworkTeam.teamId))
|
||||
return false;
|
||||
|
||||
// Observed only if teamId's match
|
||||
return identityNetworkTeam.teamId == newObserverNetworkTeam.teamId;
|
||||
}
|
||||
|
||||
public override void OnRebuildObservers(NetworkIdentity identity, HashSet<NetworkConnectionToClient> newObservers)
|
||||
{
|
||||
// If this object doesn't have a NetworkTeam then it's visible to all clients
|
||||
if (!identity.TryGetComponent<NetworkTeam>(out NetworkTeam networkTeam))
|
||||
{
|
||||
AddAllConnections(newObservers);
|
||||
return;
|
||||
}
|
||||
|
||||
// If this object has NetworkTeam and forceShown == true then it's visible to all clients
|
||||
if (networkTeam.forceShown)
|
||||
{
|
||||
AddAllConnections(newObservers);
|
||||
return;
|
||||
}
|
||||
|
||||
// Null / Empty string is never a valid teamId
|
||||
if (string.IsNullOrWhiteSpace(networkTeam.teamId))
|
||||
return;
|
||||
|
||||
// Abort if this team hasn't been created yet by OnSpawned or UpdateTeamObjects
|
||||
if (!teamObjects.TryGetValue(networkTeam.teamId, out HashSet<NetworkIdentity> objects))
|
||||
return;
|
||||
|
||||
// Add everything in the hashset for this object's current team
|
||||
foreach (NetworkIdentity networkIdentity in objects)
|
||||
if (networkIdentity != null && networkIdentity.connectionToClient != null)
|
||||
newObservers.Add(networkIdentity.connectionToClient);
|
||||
}
|
||||
|
||||
void AddAllConnections(HashSet<NetworkConnectionToClient> newObservers)
|
||||
{
|
||||
foreach (NetworkConnectionToClient conn in NetworkServer.connections.Values)
|
||||
{
|
||||
// authenticated and joined world with a player?
|
||||
if (conn != null && conn.isAuthenticated && conn.identity != null)
|
||||
newObservers.Add(conn);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: dceb9a7085758fd4590419ff5b14b636
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -2,6 +2,6 @@ fileFormatVersion: 2
|
|||
guid: 72872094b21c16e48b631b2224833d49
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData: ''
|
||||
assetBundleName: ''
|
||||
assetBundleVariant: ''
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
|
|||
|
|
@ -30,7 +30,6 @@ namespace Mirror
|
|||
[Tooltip("Animator that will have parameters synchronized")]
|
||||
public Animator animator;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Syncs animator.speed
|
||||
/// </summary>
|
||||
|
|
@ -48,7 +47,7 @@ namespace Mirror
|
|||
int[] animationHash;
|
||||
int[] transitionHash;
|
||||
float[] layerWeight;
|
||||
float nextSendTime;
|
||||
double nextSendTime;
|
||||
|
||||
bool SendMessagesAllowed
|
||||
{
|
||||
|
|
@ -107,7 +106,7 @@ namespace Mirror
|
|||
continue;
|
||||
}
|
||||
|
||||
using (PooledNetworkWriter writer = NetworkWriterPool.GetWriter())
|
||||
using (NetworkWriterPooled writer = NetworkWriterPool.Get())
|
||||
{
|
||||
WriteParameters(writer);
|
||||
SendAnimationMessage(stateHash, normalizedTime, i, layerWeight[i], writer.ToArray());
|
||||
|
|
@ -134,13 +133,6 @@ namespace Mirror
|
|||
}
|
||||
}
|
||||
|
||||
void CmdSetAnimatorSpeed(float newSpeed)
|
||||
{
|
||||
// set animator
|
||||
animator.speed = newSpeed;
|
||||
animatorSpeed = newSpeed;
|
||||
}
|
||||
|
||||
void OnAnimatorSpeedChanged(float _, float value)
|
||||
{
|
||||
// skip if host or client with authority
|
||||
|
|
@ -196,12 +188,12 @@ namespace Mirror
|
|||
|
||||
void CheckSendRate()
|
||||
{
|
||||
float now = Time.time;
|
||||
double now = NetworkTime.localTime;
|
||||
if (SendMessagesAllowed && syncInterval >= 0 && now > nextSendTime)
|
||||
{
|
||||
nextSendTime = now + syncInterval;
|
||||
|
||||
using (PooledNetworkWriter writer = NetworkWriterPool.GetWriter())
|
||||
using (NetworkWriterPooled writer = NetworkWriterPool.Get())
|
||||
{
|
||||
if (WriteParameters(writer))
|
||||
SendAnimationParametersMessage(writer.ToArray());
|
||||
|
|
@ -532,10 +524,10 @@ namespace Mirror
|
|||
if (!clientAuthority)
|
||||
return;
|
||||
|
||||
// Debug.Log("OnAnimationMessage for netId=" + netId);
|
||||
//Debug.Log($"OnAnimationMessage for netId {netId}");
|
||||
|
||||
// handle and broadcast
|
||||
using (PooledNetworkReader networkReader = NetworkReaderPool.GetReader(parameters))
|
||||
using (NetworkReaderPooled networkReader = NetworkReaderPool.Get(parameters))
|
||||
{
|
||||
HandleAnimMsg(stateHash, normalizedTime, layerId, weight, networkReader);
|
||||
RpcOnAnimationClientMessage(stateHash, normalizedTime, layerId, weight, parameters);
|
||||
|
|
@ -550,7 +542,7 @@ namespace Mirror
|
|||
return;
|
||||
|
||||
// handle and broadcast
|
||||
using (PooledNetworkReader networkReader = NetworkReaderPool.GetReader(parameters))
|
||||
using (NetworkReaderPooled networkReader = NetworkReaderPool.Get(parameters))
|
||||
{
|
||||
HandleAnimParamsMsg(networkReader);
|
||||
RpcOnAnimationParametersClientMessage(parameters);
|
||||
|
|
@ -593,6 +585,14 @@ namespace Mirror
|
|||
RpcOnAnimationResetTriggerClientMessage(hash);
|
||||
}
|
||||
|
||||
[Command]
|
||||
void CmdSetAnimatorSpeed(float newSpeed)
|
||||
{
|
||||
// set animator
|
||||
animator.speed = newSpeed;
|
||||
animatorSpeed = newSpeed;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region client message handlers
|
||||
|
|
@ -600,14 +600,14 @@ namespace Mirror
|
|||
[ClientRpc]
|
||||
void RpcOnAnimationClientMessage(int stateHash, float normalizedTime, int layerId, float weight, byte[] parameters)
|
||||
{
|
||||
using (PooledNetworkReader networkReader = NetworkReaderPool.GetReader(parameters))
|
||||
using (NetworkReaderPooled networkReader = NetworkReaderPool.Get(parameters))
|
||||
HandleAnimMsg(stateHash, normalizedTime, layerId, weight, networkReader);
|
||||
}
|
||||
|
||||
[ClientRpc]
|
||||
void RpcOnAnimationParametersClientMessage(byte[] parameters)
|
||||
{
|
||||
using (PooledNetworkReader networkReader = NetworkReaderPool.GetReader(parameters))
|
||||
using (NetworkReaderPooled networkReader = NetworkReaderPool.Get(parameters))
|
||||
HandleAnimParamsMsg(networkReader);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,6 @@ MonoImporter:
|
|||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData: ''
|
||||
assetBundleName: ''
|
||||
assetBundleVariant: ''
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
|
|||
|
|
@ -6,6 +6,6 @@ MonoImporter:
|
|||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData: ''
|
||||
assetBundleName: ''
|
||||
assetBundleVariant: ''
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
|
|||
|
|
@ -6,6 +6,6 @@ MonoImporter:
|
|||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData: ''
|
||||
assetBundleName: ''
|
||||
assetBundleVariant: ''
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
|
|||
|
|
@ -1,144 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
/// <summary>
|
||||
/// Component that controls visibility of networked objects based on match id.
|
||||
/// <para>Any object with this component on it will only be visible to other objects in the same match.</para>
|
||||
/// <para>This would be used to isolate players to their respective matches within a single game server instance. </para>
|
||||
/// </summary>
|
||||
[Obsolete(NetworkVisibilityObsoleteMessage.Message)]
|
||||
[DisallowMultipleComponent]
|
||||
[AddComponentMenu("Network/NetworkMatchChecker")]
|
||||
[RequireComponent(typeof(NetworkIdentity))]
|
||||
[HelpURL("https://mirror-networking.gitbook.io/docs/components/network-match-checker")]
|
||||
public class NetworkMatchChecker : NetworkVisibility
|
||||
{
|
||||
// internal for tests
|
||||
internal static readonly Dictionary<Guid, HashSet<NetworkIdentity>> matchPlayers =
|
||||
new Dictionary<Guid, HashSet<NetworkIdentity>>();
|
||||
|
||||
// internal for tests
|
||||
internal Guid currentMatch = Guid.Empty;
|
||||
|
||||
[Header("Diagnostics")]
|
||||
[SyncVar]
|
||||
public string currentMatchDebug;
|
||||
|
||||
/// <summary>
|
||||
/// Set this to the same value on all networked objects that belong to a given match
|
||||
/// </summary>
|
||||
public Guid matchId
|
||||
{
|
||||
get { return currentMatch; }
|
||||
set
|
||||
{
|
||||
if (currentMatch == value) return;
|
||||
|
||||
// cache previous match so observers in that match can be rebuilt
|
||||
Guid previousMatch = currentMatch;
|
||||
|
||||
// Set this to the new match this object just entered ...
|
||||
currentMatch = value;
|
||||
// ... and copy the string for the inspector because Unity can't show Guid directly
|
||||
currentMatchDebug = currentMatch.ToString();
|
||||
|
||||
if (previousMatch != Guid.Empty)
|
||||
{
|
||||
// Remove this object from the hashset of the match it just left
|
||||
matchPlayers[previousMatch].Remove(netIdentity);
|
||||
|
||||
// RebuildObservers of all NetworkIdentity's in the match this object just left
|
||||
RebuildMatchObservers(previousMatch);
|
||||
}
|
||||
|
||||
if (currentMatch != Guid.Empty)
|
||||
{
|
||||
// Make sure this new match is in the dictionary
|
||||
if (!matchPlayers.ContainsKey(currentMatch))
|
||||
matchPlayers.Add(currentMatch, new HashSet<NetworkIdentity>());
|
||||
|
||||
// Add this object to the hashset of the new match
|
||||
matchPlayers[currentMatch].Add(netIdentity);
|
||||
|
||||
// RebuildObservers of all NetworkIdentity's in the match this object just entered
|
||||
RebuildMatchObservers(currentMatch);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Not in any match now...RebuildObservers will clear and add self
|
||||
netIdentity.RebuildObservers(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnStartServer()
|
||||
{
|
||||
if (currentMatch == Guid.Empty) return;
|
||||
|
||||
if (!matchPlayers.ContainsKey(currentMatch))
|
||||
matchPlayers.Add(currentMatch, new HashSet<NetworkIdentity>());
|
||||
|
||||
matchPlayers[currentMatch].Add(netIdentity);
|
||||
|
||||
// No need to rebuild anything here.
|
||||
// identity.RebuildObservers is called right after this from NetworkServer.SpawnObject
|
||||
}
|
||||
|
||||
public override void OnStopServer()
|
||||
{
|
||||
if (currentMatch == Guid.Empty) return;
|
||||
|
||||
if (matchPlayers.ContainsKey(currentMatch) && matchPlayers[currentMatch].Remove(netIdentity))
|
||||
RebuildMatchObservers(currentMatch);
|
||||
}
|
||||
|
||||
void RebuildMatchObservers(Guid specificMatch)
|
||||
{
|
||||
foreach (NetworkIdentity networkIdentity in matchPlayers[specificMatch])
|
||||
if (networkIdentity != null)
|
||||
networkIdentity.RebuildObservers(false);
|
||||
}
|
||||
|
||||
#region Observers
|
||||
|
||||
/// <summary>
|
||||
/// Callback used by the visibility system to determine if an observer (player) can see this object.
|
||||
/// <para>If this function returns true, the network connection will be added as an observer.</para>
|
||||
/// </summary>
|
||||
/// <param name="conn">Network connection of a player.</param>
|
||||
/// <returns>True if the player can see this object.</returns>
|
||||
public override bool OnCheckObserver(NetworkConnection conn)
|
||||
{
|
||||
// Not Visible if not in a match
|
||||
if (matchId == Guid.Empty)
|
||||
return false;
|
||||
|
||||
NetworkMatchChecker networkMatchChecker = conn.identity.GetComponent<NetworkMatchChecker>();
|
||||
|
||||
if (networkMatchChecker == null)
|
||||
return false;
|
||||
|
||||
return networkMatchChecker.matchId == matchId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Callback used by the visibility system to (re)construct the set of observers that can see this object.
|
||||
/// <para>Implementations of this callback should add network connections of players that can see this object to the observers set.</para>
|
||||
/// </summary>
|
||||
/// <param name="observers">The new set of observers for this object.</param>
|
||||
/// <param name="initialize">True if the set of observers is being built for the first time.</param>
|
||||
public override void OnRebuildObservers(HashSet<NetworkConnection> observers, bool initialize)
|
||||
{
|
||||
if (currentMatch == Guid.Empty) return;
|
||||
|
||||
foreach (NetworkIdentity networkIdentity in matchPlayers[currentMatch])
|
||||
if (networkIdentity != null && networkIdentity.connectionToClient != null)
|
||||
observers.Add(networkIdentity.connectionToClient);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
using System;
|
||||
using UnityEngine;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
/// <summary>
|
||||
/// Component that limits visibility of networked objects to the authority client.
|
||||
/// <para>Any object with this component on it will only be visible to the client that has been assigned authority for it.</para>
|
||||
/// <para>This would be used for spawning a non-player networked object for single client to interact with, e.g. in-game puzzles.</para>
|
||||
/// </summary>
|
||||
[Obsolete(NetworkVisibilityObsoleteMessage.Message)]
|
||||
[DisallowMultipleComponent]
|
||||
[AddComponentMenu("Network/NetworkOwnerChecker")]
|
||||
[RequireComponent(typeof(NetworkIdentity))]
|
||||
[HelpURL("https://mirror-networking.gitbook.io/docs/components/network-owner-checker")]
|
||||
public class NetworkOwnerChecker : NetworkVisibility
|
||||
{
|
||||
/// <summary>
|
||||
/// Callback used by the visibility system to determine if an observer (player) can see this object.
|
||||
/// <para>If this function returns true, the network connection will be added as an observer.</para>
|
||||
/// </summary>
|
||||
/// <param name="conn">Network connection of a player.</param>
|
||||
/// <returns>True if the client is the owner of this object.</returns>
|
||||
public override bool OnCheckObserver(NetworkConnection conn)
|
||||
{
|
||||
// Debug.Log($"OnCheckObserver {netIdentity.connectionToClient} {conn}");
|
||||
|
||||
return (netIdentity.connectionToClient == conn);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Callback used by the visibility system to (re)construct the set of observers that can see this object.
|
||||
/// </summary>
|
||||
/// <param name="observers">The new set of observers for this object.</param>
|
||||
/// <param name="initialize">True if the set of observers is being built for the first time.</param>
|
||||
public override void OnRebuildObservers(HashSet<NetworkConnection> observers, bool initialize)
|
||||
{
|
||||
// Do nothing here because the authority client is always added as an observer internally.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -13,8 +13,8 @@ namespace Mirror
|
|||
{
|
||||
public Color color = Color.white;
|
||||
public int padding = 2;
|
||||
int width = 150;
|
||||
int height = 25;
|
||||
public int width = 150;
|
||||
public int height = 25;
|
||||
|
||||
void OnGUI()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -6,6 +6,6 @@ MonoImporter:
|
|||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData: ''
|
||||
assetBundleName: ''
|
||||
assetBundleVariant: ''
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
|
|||
|
|
@ -1,104 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
/// <summary>
|
||||
/// Component that controls visibility of networked objects for players.
|
||||
/// <para>Any object with this component on it will not be visible to players more than a (configurable) distance away.</para>
|
||||
/// </summary>
|
||||
[Obsolete(NetworkVisibilityObsoleteMessage.Message)]
|
||||
[AddComponentMenu("Network/NetworkProximityChecker")]
|
||||
[RequireComponent(typeof(NetworkIdentity))]
|
||||
[HelpURL("https://mirror-networking.gitbook.io/docs/components/network-proximity-checker")]
|
||||
public class NetworkProximityChecker : NetworkVisibility
|
||||
{
|
||||
/// <summary>
|
||||
/// The maximum range that objects will be visible at.
|
||||
/// </summary>
|
||||
[Tooltip("The maximum range that objects will be visible at.")]
|
||||
public int visRange = 10;
|
||||
|
||||
/// <summary>
|
||||
/// How often (in seconds) that this object should update the list of observers that can see it.
|
||||
/// </summary>
|
||||
[Tooltip("How often (in seconds) that this object should update the list of observers that can see it.")]
|
||||
public float visUpdateInterval = 1;
|
||||
|
||||
/// <summary>
|
||||
/// Flag to force this object to be hidden for players.
|
||||
/// <para>If this object is a player object, it will not be hidden for that player.</para>
|
||||
/// </summary>
|
||||
// Deprecated 2021-02-17
|
||||
[Obsolete("Use NetworkIdentity.visible mode instead of forceHidden!")]
|
||||
public bool forceHidden
|
||||
{
|
||||
get => netIdentity.visible == Visibility.ForceHidden;
|
||||
set => netIdentity.visible = value ? Visibility.ForceHidden : Visibility.Default;
|
||||
}
|
||||
|
||||
public override void OnStartServer()
|
||||
{
|
||||
InvokeRepeating(nameof(RebuildObservers), 0, visUpdateInterval);
|
||||
}
|
||||
public override void OnStopServer()
|
||||
{
|
||||
CancelInvoke(nameof(RebuildObservers));
|
||||
}
|
||||
|
||||
void RebuildObservers()
|
||||
{
|
||||
netIdentity.RebuildObservers(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Callback used by the visibility system to determine if an observer (player) can see this object.
|
||||
/// <para>If this function returns true, the network connection will be added as an observer.</para>
|
||||
/// </summary>
|
||||
/// <param name="conn">Network connection of a player.</param>
|
||||
/// <returns>True if the player can see this object.</returns>
|
||||
public override bool OnCheckObserver(NetworkConnection conn)
|
||||
{
|
||||
if (forceHidden)
|
||||
return false;
|
||||
|
||||
return Vector3.Distance(conn.identity.transform.position, transform.position) < visRange;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Callback used by the visibility system to (re)construct the set of observers that can see this object.
|
||||
/// <para>Implementations of this callback should add network connections of players that can see this object to the observers set.</para>
|
||||
/// </summary>
|
||||
/// <param name="observers">The new set of observers for this object.</param>
|
||||
/// <param name="initialize">True if the set of observers is being built for the first time.</param>
|
||||
public override void OnRebuildObservers(HashSet<NetworkConnection> observers, bool initialize)
|
||||
{
|
||||
// if force hidden then return without adding any observers.
|
||||
if (forceHidden)
|
||||
return;
|
||||
|
||||
// 'transform.' calls GetComponent, only do it once
|
||||
Vector3 position = transform.position;
|
||||
|
||||
// brute force distance check
|
||||
// -> only player connections can be observers, so it's enough if we
|
||||
// go through all connections instead of all spawned identities.
|
||||
// -> compared to UNET's sphere cast checking, this one is orders of
|
||||
// magnitude faster. if we have 10k monsters and run a sphere
|
||||
// cast 10k times, we will see a noticeable lag even with physics
|
||||
// layers. but checking to every connection is fast.
|
||||
foreach (NetworkConnectionToClient conn in NetworkServer.connections.Values)
|
||||
{
|
||||
if (conn != null && conn.identity != null)
|
||||
{
|
||||
// check distance
|
||||
if (Vector3.Distance(conn.identity.transform.position, position) < visRange)
|
||||
{
|
||||
observers.Add(conn);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
using UnityEngine.Serialization;
|
||||
|
||||
namespace Mirror
|
||||
|
|
@ -19,7 +21,7 @@ namespace Mirror
|
|||
{
|
||||
public struct PendingPlayer
|
||||
{
|
||||
public NetworkConnection conn;
|
||||
public NetworkConnectionToClient conn;
|
||||
public GameObject roomPlayer;
|
||||
}
|
||||
|
||||
|
|
@ -148,9 +150,9 @@ namespace Mirror
|
|||
/// <para>The default implementation of this function calls NetworkServer.SetClientReady() to continue the network setup process.</para>
|
||||
/// </summary>
|
||||
/// <param name="conn">Connection from client.</param>
|
||||
public override void OnServerReady(NetworkConnection conn)
|
||||
public override void OnServerReady(NetworkConnectionToClient conn)
|
||||
{
|
||||
Debug.Log("NetworkRoomManager OnServerReady");
|
||||
Debug.Log($"NetworkRoomManager OnServerReady {conn}");
|
||||
base.OnServerReady(conn);
|
||||
|
||||
if (conn != null && conn.identity != null)
|
||||
|
|
@ -163,9 +165,9 @@ namespace Mirror
|
|||
}
|
||||
}
|
||||
|
||||
void SceneLoadedForPlayer(NetworkConnection conn, GameObject roomPlayer)
|
||||
void SceneLoadedForPlayer(NetworkConnectionToClient conn, GameObject roomPlayer)
|
||||
{
|
||||
// Debug.LogFormat(LogType.Log, "NetworkRoom SceneLoadedForPlayer scene: {0} {1}", SceneManager.GetActiveScene().path, conn);
|
||||
Debug.Log($"NetworkRoom SceneLoadedForPlayer scene: {SceneManager.GetActiveScene().path} {conn}");
|
||||
|
||||
if (IsSceneActive(RoomScene))
|
||||
{
|
||||
|
|
@ -243,7 +245,7 @@ namespace Mirror
|
|||
/// <para>Unity calls this on the Server when a Client connects to the Server. Use an override to tell the NetworkManager what to do when a client connects to the server.</para>
|
||||
/// </summary>
|
||||
/// <param name="conn">Connection from client.</param>
|
||||
public override void OnServerConnect(NetworkConnection conn)
|
||||
public override void OnServerConnect(NetworkConnectionToClient conn)
|
||||
{
|
||||
if (numPlayers >= maxConnections)
|
||||
{
|
||||
|
|
@ -267,7 +269,7 @@ namespace Mirror
|
|||
/// <para>This is called on the Server when a Client disconnects from the Server. Use an override to decide what should happen when a disconnection is detected.</para>
|
||||
/// </summary>
|
||||
/// <param name="conn">Connection from client.</param>
|
||||
public override void OnServerDisconnect(NetworkConnection conn)
|
||||
public override void OnServerDisconnect(NetworkConnectionToClient conn)
|
||||
{
|
||||
if (conn.identity != null)
|
||||
{
|
||||
|
|
@ -312,7 +314,7 @@ namespace Mirror
|
|||
/// <para>The default implementation for this function creates a new player object from the playerPrefab.</para>
|
||||
/// </summary>
|
||||
/// <param name="conn">Connection from client.</param>
|
||||
public override void OnServerAddPlayer(NetworkConnection conn)
|
||||
public override void OnServerAddPlayer(NetworkConnectionToClient conn)
|
||||
{
|
||||
// increment the index before adding the player, so first player starts at 1
|
||||
clientIndex++;
|
||||
|
|
@ -324,7 +326,7 @@ namespace Mirror
|
|||
|
||||
allPlayersReady = false;
|
||||
|
||||
// Debug.LogFormat(LogType.Log, "NetworkRoomManager.OnServerAddPlayer playerPrefab:{0}", roomPlayerPrefab.name);
|
||||
//Debug.Log("NetworkRoomManager.OnServerAddPlayer playerPrefab: {roomPlayerPrefab.name}");
|
||||
|
||||
GameObject newRoomGameObject = OnRoomServerCreateRoomPlayer(conn);
|
||||
if (newRoomGameObject == null)
|
||||
|
|
@ -403,13 +405,13 @@ namespace Mirror
|
|||
/// </summary>
|
||||
public override void OnStartServer()
|
||||
{
|
||||
if (string.IsNullOrEmpty(RoomScene))
|
||||
if (string.IsNullOrWhiteSpace(RoomScene))
|
||||
{
|
||||
Debug.LogError("NetworkRoomManager RoomScene is empty. Set the RoomScene in the inspector for the NetworkRoomManager");
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(GameplayScene))
|
||||
if (string.IsNullOrWhiteSpace(GameplayScene))
|
||||
{
|
||||
Debug.LogError("NetworkRoomManager PlayScene is empty. Set the PlayScene in the inspector for the NetworkRoomManager");
|
||||
return;
|
||||
|
|
@ -468,22 +470,20 @@ namespace Mirror
|
|||
/// Called on the client when connected to a server.
|
||||
/// <para>The default implementation of this function sets the client as ready and adds a player. Override the function to dictate what happens when the client connects.</para>
|
||||
/// </summary>
|
||||
/// <param name="conn">Connection to the server.</param>
|
||||
public override void OnClientConnect(NetworkConnection conn)
|
||||
public override void OnClientConnect()
|
||||
{
|
||||
OnRoomClientConnect(conn);
|
||||
base.OnClientConnect(conn);
|
||||
OnRoomClientConnect();
|
||||
base.OnClientConnect();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called on clients when disconnected from a server.
|
||||
/// <para>This is called on the client when it disconnects from the server. Override this function to decide what happens when the client disconnects.</para>
|
||||
/// </summary>
|
||||
/// <param name="conn">Connection to the server.</param>
|
||||
public override void OnClientDisconnect(NetworkConnection conn)
|
||||
public override void OnClientDisconnect()
|
||||
{
|
||||
OnRoomClientDisconnect(conn);
|
||||
base.OnClientDisconnect(conn);
|
||||
OnRoomClientDisconnect();
|
||||
base.OnClientDisconnect();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -500,8 +500,7 @@ namespace Mirror
|
|||
/// Called on clients when a scene has completed loaded, when the scene load was initiated by the server.
|
||||
/// <para>Scene changes can cause player objects to be destroyed. The default implementation of OnClientSceneChanged in the NetworkManager is to add a player object for the connection if no player object exists.</para>
|
||||
/// </summary>
|
||||
/// <param name="conn">Connection of the client</param>
|
||||
public override void OnClientSceneChanged(NetworkConnection conn)
|
||||
public override void OnClientSceneChanged()
|
||||
{
|
||||
if (IsSceneActive(RoomScene))
|
||||
{
|
||||
|
|
@ -511,8 +510,8 @@ namespace Mirror
|
|||
else
|
||||
CallOnClientExitRoom();
|
||||
|
||||
base.OnClientSceneChanged(conn);
|
||||
OnRoomClientSceneChanged(conn);
|
||||
base.OnClientSceneChanged();
|
||||
OnRoomClientSceneChanged();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
|
@ -543,13 +542,13 @@ namespace Mirror
|
|||
/// This is called on the server when a new client connects to the server.
|
||||
/// </summary>
|
||||
/// <param name="conn">The new connection.</param>
|
||||
public virtual void OnRoomServerConnect(NetworkConnection conn) {}
|
||||
public virtual void OnRoomServerConnect(NetworkConnectionToClient conn) {}
|
||||
|
||||
/// <summary>
|
||||
/// This is called on the server when a client disconnects.
|
||||
/// </summary>
|
||||
/// <param name="conn">The connection that disconnected.</param>
|
||||
public virtual void OnRoomServerDisconnect(NetworkConnection conn) {}
|
||||
public virtual void OnRoomServerDisconnect(NetworkConnectionToClient conn) {}
|
||||
|
||||
/// <summary>
|
||||
/// This is called on the server when a networked scene finishes loading.
|
||||
|
|
@ -563,7 +562,7 @@ namespace Mirror
|
|||
/// </summary>
|
||||
/// <param name="conn">The connection the player object is for.</param>
|
||||
/// <returns>The new room-player object.</returns>
|
||||
public virtual GameObject OnRoomServerCreateRoomPlayer(NetworkConnection conn)
|
||||
public virtual GameObject OnRoomServerCreateRoomPlayer(NetworkConnectionToClient conn)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
|
@ -575,7 +574,7 @@ namespace Mirror
|
|||
/// <param name="conn">The connection the player object is for.</param>
|
||||
/// <param name="roomPlayer">The room player object for this connection.</param>
|
||||
/// <returns>A new GamePlayer object.</returns>
|
||||
public virtual GameObject OnRoomServerCreateGamePlayer(NetworkConnection conn, GameObject roomPlayer)
|
||||
public virtual GameObject OnRoomServerCreateGamePlayer(NetworkConnectionToClient conn, GameObject roomPlayer)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
|
@ -583,10 +582,10 @@ namespace Mirror
|
|||
/// <summary>
|
||||
/// This allows customization of the creation of the GamePlayer object on the server.
|
||||
/// <para>This is only called for subsequent GamePlay scenes after the first one.</para>
|
||||
/// <para>See <see cref="OnRoomServerCreateGamePlayer(NetworkConnection, GameObject)">OnRoomServerCreateGamePlayer(NetworkConnection, GameObject)</see> to customize the player object for the initial GamePlay scene.</para>
|
||||
/// <para>See <see cref="OnRoomServerCreateGamePlayer(NetworkConnectionToClient, GameObject)">OnRoomServerCreateGamePlayer(NetworkConnection, GameObject)</see> to customize the player object for the initial GamePlay scene.</para>
|
||||
/// </summary>
|
||||
/// <param name="conn">The connection the player object is for.</param>
|
||||
public virtual void OnRoomServerAddPlayer(NetworkConnection conn)
|
||||
public virtual void OnRoomServerAddPlayer(NetworkConnectionToClient conn)
|
||||
{
|
||||
base.OnServerAddPlayer(conn);
|
||||
}
|
||||
|
|
@ -600,7 +599,7 @@ namespace Mirror
|
|||
/// <param name="roomPlayer">The room player object.</param>
|
||||
/// <param name="gamePlayer">The game player object.</param>
|
||||
/// <returns>False to not allow this player to replace the room player.</returns>
|
||||
public virtual bool OnRoomServerSceneLoadedForPlayer(NetworkConnection conn, GameObject roomPlayer, GameObject gamePlayer)
|
||||
public virtual bool OnRoomServerSceneLoadedForPlayer(NetworkConnectionToClient conn, GameObject roomPlayer, GameObject gamePlayer)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
|
@ -638,19 +637,16 @@ namespace Mirror
|
|||
/// <summary>
|
||||
/// This is called on the client when it connects to server.
|
||||
/// </summary>
|
||||
/// <param name="conn">The connection that connected.</param>
|
||||
public virtual void OnRoomClientConnect(NetworkConnection conn) {}
|
||||
public virtual void OnRoomClientConnect() {}
|
||||
|
||||
/// <summary>
|
||||
/// This is called on the client when disconnected from a server.
|
||||
/// </summary>
|
||||
/// <param name="conn">The connection that disconnected.</param>
|
||||
public virtual void OnRoomClientDisconnect(NetworkConnection conn) {}
|
||||
public virtual void OnRoomClientDisconnect() {}
|
||||
|
||||
/// <summary>
|
||||
/// This is called on the client when a client is started.
|
||||
/// </summary>
|
||||
/// <param name="roomClient">The connection for the room.</param>
|
||||
public virtual void OnRoomStartClient() {}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -661,8 +657,7 @@ namespace Mirror
|
|||
/// <summary>
|
||||
/// This is called on the client when the client is finished loading a new networked scene.
|
||||
/// </summary>
|
||||
/// <param name="conn">The connection that finished loading a new networked scene.</param>
|
||||
public virtual void OnRoomClientSceneChanged(NetworkConnection conn) {}
|
||||
public virtual void OnRoomClientSceneChanged() {}
|
||||
|
||||
/// <summary>
|
||||
/// Called on the client when adding a player to the room fails.
|
||||
|
|
|
|||
|
|
@ -6,6 +6,6 @@ MonoImporter:
|
|||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData: ''
|
||||
assetBundleName: ''
|
||||
assetBundleVariant: ''
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
|
|||
|
|
@ -126,7 +126,7 @@ namespace Mirror
|
|||
#region Optional UI
|
||||
|
||||
/// <summary>
|
||||
/// Render a UI for the room. Override to provide your on UI
|
||||
/// Render a UI for the room. Override to provide your own UI
|
||||
/// </summary>
|
||||
public virtual void OnGUI()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -6,6 +6,6 @@ MonoImporter:
|
|||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData: ''
|
||||
assetBundleName: ''
|
||||
assetBundleVariant: ''
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
|
|||
|
|
@ -1,121 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
/// <summary>
|
||||
/// Component that controls visibility of networked objects between scenes.
|
||||
/// <para>Any object with this component on it will only be visible to other objects in the same scene</para>
|
||||
/// <para>This would be used when the server has multiple additive subscenes loaded to isolate players to their respective subscenes</para>
|
||||
/// </summary>
|
||||
[Obsolete(NetworkVisibilityObsoleteMessage.Message)]
|
||||
[DisallowMultipleComponent]
|
||||
[AddComponentMenu("Network/NetworkSceneChecker")]
|
||||
[RequireComponent(typeof(NetworkIdentity))]
|
||||
[HelpURL("https://mirror-networking.gitbook.io/docs/components/network-scene-checker")]
|
||||
public class NetworkSceneChecker : NetworkVisibility
|
||||
{
|
||||
/// <summary>
|
||||
/// Flag to force this object to be hidden from all observers.
|
||||
/// <para>If this object is a player object, it will not be hidden for that client.</para>
|
||||
/// </summary>
|
||||
[Tooltip("Enable to force this object to be hidden from all observers.")]
|
||||
public bool forceHidden;
|
||||
|
||||
// Use Scene instead of string scene.name because when additively loading multiples of a subscene the name won't be unique
|
||||
static readonly Dictionary<Scene, HashSet<NetworkIdentity>> sceneCheckerObjects = new Dictionary<Scene, HashSet<NetworkIdentity>>();
|
||||
|
||||
Scene currentScene;
|
||||
|
||||
[ServerCallback]
|
||||
void Awake()
|
||||
{
|
||||
currentScene = gameObject.scene;
|
||||
// Debug.Log($"NetworkSceneChecker.Awake currentScene: {currentScene}");
|
||||
}
|
||||
|
||||
public override void OnStartServer()
|
||||
{
|
||||
if (!sceneCheckerObjects.ContainsKey(currentScene))
|
||||
sceneCheckerObjects.Add(currentScene, new HashSet<NetworkIdentity>());
|
||||
|
||||
sceneCheckerObjects[currentScene].Add(netIdentity);
|
||||
}
|
||||
|
||||
public override void OnStopServer()
|
||||
{
|
||||
if (sceneCheckerObjects.ContainsKey(currentScene) && sceneCheckerObjects[currentScene].Remove(netIdentity))
|
||||
RebuildSceneObservers();
|
||||
}
|
||||
|
||||
[ServerCallback]
|
||||
void Update()
|
||||
{
|
||||
if (currentScene == gameObject.scene)
|
||||
return;
|
||||
|
||||
// This object is in a new scene so observers in the prior scene
|
||||
// and the new scene need to rebuild their respective observers lists.
|
||||
|
||||
// Remove this object from the hashset of the scene it just left
|
||||
sceneCheckerObjects[currentScene].Remove(netIdentity);
|
||||
|
||||
// RebuildObservers of all NetworkIdentity's in the scene this object just left
|
||||
RebuildSceneObservers();
|
||||
|
||||
// Set this to the new scene this object just entered
|
||||
currentScene = gameObject.scene;
|
||||
|
||||
// Make sure this new scene is in the dictionary
|
||||
if (!sceneCheckerObjects.ContainsKey(currentScene))
|
||||
sceneCheckerObjects.Add(currentScene, new HashSet<NetworkIdentity>());
|
||||
|
||||
// Add this object to the hashset of the new scene
|
||||
sceneCheckerObjects[currentScene].Add(netIdentity);
|
||||
|
||||
// RebuildObservers of all NetworkIdentity's in the scene this object just entered
|
||||
RebuildSceneObservers();
|
||||
}
|
||||
|
||||
void RebuildSceneObservers()
|
||||
{
|
||||
foreach (NetworkIdentity networkIdentity in sceneCheckerObjects[currentScene])
|
||||
if (networkIdentity != null)
|
||||
networkIdentity.RebuildObservers(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Callback used by the visibility system to determine if an observer (player) can see this object.
|
||||
/// <para>If this function returns true, the network connection will be added as an observer.</para>
|
||||
/// </summary>
|
||||
/// <param name="conn">Network connection of a player.</param>
|
||||
/// <returns>True if the player can see this object.</returns>
|
||||
public override bool OnCheckObserver(NetworkConnection conn)
|
||||
{
|
||||
if (forceHidden)
|
||||
return false;
|
||||
|
||||
return conn.identity.gameObject.scene == gameObject.scene;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Callback used by the visibility system to (re)construct the set of observers that can see this object.
|
||||
/// <para>Implementations of this callback should add network connections of players that can see this object to the observers set.</para>
|
||||
/// </summary>
|
||||
/// <param name="observers">The new set of observers for this object.</param>
|
||||
/// <param name="initialize">True if the set of observers is being built for the first time.</param>
|
||||
public override void OnRebuildObservers(HashSet<NetworkConnection> observers, bool initialize)
|
||||
{
|
||||
// If forceHidden then return without adding any observers.
|
||||
if (forceHidden)
|
||||
return;
|
||||
|
||||
// Add everything in the hashset for this object's current scene
|
||||
foreach (NetworkIdentity networkIdentity in sceneCheckerObjects[currentScene])
|
||||
if (networkIdentity != null && networkIdentity.connectionToClient != null)
|
||||
observers.Add(networkIdentity.connectionToClient);
|
||||
}
|
||||
}
|
||||
}
|
||||
194
UnityProject/Assets/Mirror/Components/NetworkStatistics.cs
Normal file
194
UnityProject/Assets/Mirror/Components/NetworkStatistics.cs
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
/// <summary>
|
||||
/// Shows Network messages and bytes sent and received per second.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>Add this component to the same object as Network Manager.</para>
|
||||
/// </remarks>
|
||||
[AddComponentMenu("Network/Network Statistics")]
|
||||
[DisallowMultipleComponent]
|
||||
[HelpURL("https://mirror-networking.gitbook.io/docs/components/network-statistics")]
|
||||
public class NetworkStatistics : MonoBehaviour
|
||||
{
|
||||
// update interval
|
||||
double intervalStartTime;
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
// CLIENT
|
||||
// long bytes to support >2GB
|
||||
int clientIntervalReceivedPackets;
|
||||
long clientIntervalReceivedBytes;
|
||||
int clientIntervalSentPackets;
|
||||
long clientIntervalSentBytes;
|
||||
|
||||
// results from last interval
|
||||
// long bytes to support >2GB
|
||||
int clientReceivedPacketsPerSecond;
|
||||
long clientReceivedBytesPerSecond;
|
||||
int clientSentPacketsPerSecond;
|
||||
long clientSentBytesPerSecond;
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
// SERVER
|
||||
// capture interval
|
||||
// long bytes to support >2GB
|
||||
int serverIntervalReceivedPackets;
|
||||
long serverIntervalReceivedBytes;
|
||||
int serverIntervalSentPackets;
|
||||
long serverIntervalSentBytes;
|
||||
|
||||
// results from last interval
|
||||
// long bytes to support >2GB
|
||||
int serverReceivedPacketsPerSecond;
|
||||
long serverReceivedBytesPerSecond;
|
||||
int serverSentPacketsPerSecond;
|
||||
long serverSentBytesPerSecond;
|
||||
|
||||
// NetworkManager sets Transport.activeTransport in Awake().
|
||||
// so let's hook into it in Start().
|
||||
void Start()
|
||||
{
|
||||
// find available transport
|
||||
Transport transport = Transport.activeTransport;
|
||||
if (transport != null)
|
||||
{
|
||||
transport.OnClientDataReceived += OnClientReceive;
|
||||
transport.OnClientDataSent += OnClientSend;
|
||||
transport.OnServerDataReceived += OnServerReceive;
|
||||
transport.OnServerDataSent += OnServerSend;
|
||||
}
|
||||
else Debug.LogError($"NetworkStatistics: no available or active Transport found on this platform: {Application.platform}");
|
||||
}
|
||||
|
||||
void OnDestroy()
|
||||
{
|
||||
// remove transport hooks
|
||||
Transport transport = Transport.activeTransport;
|
||||
if (transport != null)
|
||||
{
|
||||
transport.OnClientDataReceived -= OnClientReceive;
|
||||
transport.OnClientDataSent -= OnClientSend;
|
||||
transport.OnServerDataReceived -= OnServerReceive;
|
||||
transport.OnServerDataSent -= OnServerSend;
|
||||
}
|
||||
}
|
||||
|
||||
void OnClientReceive(ArraySegment<byte> data, int channelId)
|
||||
{
|
||||
++clientIntervalReceivedPackets;
|
||||
clientIntervalReceivedBytes += data.Count;
|
||||
}
|
||||
|
||||
void OnClientSend(ArraySegment<byte> data, int channelId)
|
||||
{
|
||||
++clientIntervalSentPackets;
|
||||
clientIntervalSentBytes += data.Count;
|
||||
}
|
||||
|
||||
void OnServerReceive(int connectionId, ArraySegment<byte> data, int channelId)
|
||||
{
|
||||
++serverIntervalReceivedPackets;
|
||||
serverIntervalReceivedBytes += data.Count;
|
||||
}
|
||||
|
||||
void OnServerSend(int connectionId, ArraySegment<byte> data, int channelId)
|
||||
{
|
||||
++serverIntervalSentPackets;
|
||||
serverIntervalSentBytes += data.Count;
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
// calculate results every second
|
||||
if (NetworkTime.localTime >= intervalStartTime + 1)
|
||||
{
|
||||
if (NetworkClient.active) UpdateClient();
|
||||
if (NetworkServer.active) UpdateServer();
|
||||
|
||||
intervalStartTime = NetworkTime.localTime;
|
||||
}
|
||||
}
|
||||
|
||||
void UpdateClient()
|
||||
{
|
||||
clientReceivedPacketsPerSecond = clientIntervalReceivedPackets;
|
||||
clientReceivedBytesPerSecond = clientIntervalReceivedBytes;
|
||||
clientSentPacketsPerSecond = clientIntervalSentPackets;
|
||||
clientSentBytesPerSecond = clientIntervalSentBytes;
|
||||
|
||||
clientIntervalReceivedPackets = 0;
|
||||
clientIntervalReceivedBytes = 0;
|
||||
clientIntervalSentPackets = 0;
|
||||
clientIntervalSentBytes = 0;
|
||||
}
|
||||
|
||||
void UpdateServer()
|
||||
{
|
||||
serverReceivedPacketsPerSecond = serverIntervalReceivedPackets;
|
||||
serverReceivedBytesPerSecond = serverIntervalReceivedBytes;
|
||||
serverSentPacketsPerSecond = serverIntervalSentPackets;
|
||||
serverSentBytesPerSecond = serverIntervalSentBytes;
|
||||
|
||||
serverIntervalReceivedPackets = 0;
|
||||
serverIntervalReceivedBytes = 0;
|
||||
serverIntervalSentPackets = 0;
|
||||
serverIntervalSentBytes = 0;
|
||||
}
|
||||
|
||||
void OnGUI()
|
||||
{
|
||||
// only show if either server or client active
|
||||
if (NetworkClient.active || NetworkServer.active)
|
||||
{
|
||||
// create main GUI area
|
||||
// 105 is below NetworkManager HUD in all cases.
|
||||
GUILayout.BeginArea(new Rect(10, 105, 215, 300));
|
||||
|
||||
// show client / server stats if active
|
||||
if (NetworkClient.active) OnClientGUI();
|
||||
if (NetworkServer.active) OnServerGUI();
|
||||
|
||||
// end of GUI area
|
||||
GUILayout.EndArea();
|
||||
}
|
||||
}
|
||||
|
||||
void OnClientGUI()
|
||||
{
|
||||
// background
|
||||
GUILayout.BeginVertical("Box");
|
||||
GUILayout.Label("<b>Client Statistics</b>");
|
||||
|
||||
// sending ("msgs" instead of "packets" to fit larger numbers)
|
||||
GUILayout.Label($"Send: {clientSentPacketsPerSecond} msgs @ {Utils.PrettyBytes(clientSentBytesPerSecond)}/s");
|
||||
|
||||
// receiving ("msgs" instead of "packets" to fit larger numbers)
|
||||
GUILayout.Label($"Recv: {clientReceivedPacketsPerSecond} msgs @ {Utils.PrettyBytes(clientReceivedBytesPerSecond)}/s");
|
||||
|
||||
// end background
|
||||
GUILayout.EndVertical();
|
||||
}
|
||||
|
||||
void OnServerGUI()
|
||||
{
|
||||
// background
|
||||
GUILayout.BeginVertical("Box");
|
||||
GUILayout.Label("<b>Server Statistics</b>");
|
||||
|
||||
// sending ("msgs" instead of "packets" to fit larger numbers)
|
||||
GUILayout.Label($"Send: {serverSentPacketsPerSecond} msgs @ {Utils.PrettyBytes(serverSentBytesPerSecond)}/s");
|
||||
|
||||
// receiving ("msgs" instead of "packets" to fit larger numbers)
|
||||
GUILayout.Label($"Recv: {serverReceivedPacketsPerSecond} msgs @ {Utils.PrettyBytes(serverReceivedBytesPerSecond)}/s");
|
||||
|
||||
// end background
|
||||
GUILayout.EndVertical();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 6d7da4e566d24ea7b0e12178d934b648
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
[DisallowMultipleComponent]
|
||||
[AddComponentMenu("Network/NetworkTransform")]
|
||||
[HelpURL("https://mirror-networking.gitbook.io/docs/components/network-transform")]
|
||||
public class NetworkTransform : NetworkTransformBase
|
||||
{
|
||||
protected override Transform targetComponent => transform;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 12f903db684732e45b130ad56f7c86c1
|
||||
guid: 44e823b93c7d2477c8796766dc364c59
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
// ʻOumuamua's light curve, assuming little systematic error, presents its
|
||||
// motion as tumbling, rather than smoothly rotating, and moving sufficiently
|
||||
// fast relative to the Sun.
|
||||
//
|
||||
// A small number of astronomers suggested that ʻOumuamua could be a product of
|
||||
// alien technology, but evidence in support of this hypothesis is weak.
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
[DisallowMultipleComponent]
|
||||
[AddComponentMenu("Network/Network Transform")]
|
||||
public class NetworkTransform : NetworkTransformBase
|
||||
{
|
||||
protected override Transform targetComponent => transform;
|
||||
}
|
||||
}
|
||||
|
|
@ -6,6 +6,6 @@ MonoImporter:
|
|||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData: ''
|
||||
assetBundleName: ''
|
||||
assetBundleVariant: ''
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,869 @@
|
|||
// NetworkTransform V2 aka project Oumuamua by vis2k (2021-07)
|
||||
// Snapshot Interpolation: https://gafferongames.com/post/snapshot_interpolation/
|
||||
//
|
||||
// Base class for NetworkTransform and NetworkTransformChild.
|
||||
// => simple unreliable sync without any interpolation for now.
|
||||
// => which means we don't need teleport detection either
|
||||
//
|
||||
// NOTE: several functions are virtual in case someone needs to modify a part.
|
||||
//
|
||||
// Channel: uses UNRELIABLE at all times.
|
||||
// -> out of order packets are dropped automatically
|
||||
// -> it's better than RELIABLE for several reasons:
|
||||
// * head of line blocking would add delay
|
||||
// * resending is mostly pointless
|
||||
// * bigger data race:
|
||||
// -> if we use a Cmd() at position X over reliable
|
||||
// -> client gets Cmd() and X at the same time, but buffers X for bufferTime
|
||||
// -> for unreliable, it would get X before the reliable Cmd(), still
|
||||
// buffer for bufferTime but end up closer to the original time
|
||||
// comment out the below line to quickly revert the onlySyncOnChange feature
|
||||
#define onlySyncOnChange_BANDWIDTH_SAVING
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
public abstract class NetworkTransformBase : NetworkBehaviour
|
||||
{
|
||||
// TODO SyncDirection { CLIENT_TO_SERVER, SERVER_TO_CLIENT } is easier?
|
||||
[Header("Authority")]
|
||||
[Tooltip("Set to true if moves come from owner client, set to false if moves always come from server")]
|
||||
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 => hasAuthority && clientAuthority;
|
||||
|
||||
// target transform to sync. can be on a child.
|
||||
protected abstract Transform targetComponent { get; }
|
||||
|
||||
[Header("Synchronization")]
|
||||
[Tooltip("Send N snapshots per second. Multiples of frame rate make sense.")]
|
||||
public int sendRate = 30; // in Hz. easier to work with as int for EMA. easier to display '30' than '0.333333333'
|
||||
public float sendInterval => 1f / sendRate;
|
||||
|
||||
// decrease bufferTime at runtime to see the catchup effect.
|
||||
// increase to see slowdown.
|
||||
// 'double' so we can have very precise dynamic adjustment without rounding
|
||||
[Header("Buffering")]
|
||||
[Tooltip("Local simulation is behind by sendInterval * multiplier seconds.\n\nThis guarantees that we always have enough snapshots in the buffer to mitigate lags & jitter.\n\nIncrease this if the simulation isn't smooth. By default, it should be around 2.")]
|
||||
public double bufferTimeMultiplier = 2;
|
||||
public double bufferTime => sendInterval * bufferTimeMultiplier;
|
||||
|
||||
[Tooltip("Buffer size limit to avoid ever growing list memory consumption attacks.")]
|
||||
public int bufferSizeLimit = 64;
|
||||
|
||||
// catchup /////////////////////////////////////////////////////////////
|
||||
// catchup thresholds in 'frames'.
|
||||
// half a frame might be too aggressive.
|
||||
[Header("Catchup / Slowdown")]
|
||||
[Tooltip("Slowdown begins when the local timeline is moving too fast towards remote time. Threshold is in frames worth of snapshots.\n\nThis needs to be negative.\n\nDon't modify unless you know what you are doing.")]
|
||||
public float catchupNegativeThreshold = -1; // careful, don't want to run out of snapshots
|
||||
|
||||
[Tooltip("Catchup begins when the local timeline is moving too slow and getting too far away from remote time. Threshold is in frames worth of snapshots.\n\nThis needs to be positive.\n\nDon't modify unless you know what you are doing.")]
|
||||
public float catchupPositiveThreshold = 1;
|
||||
|
||||
[Tooltip("Local timeline acceleration in % while catching up.")]
|
||||
[Range(0, 1)]
|
||||
public double catchupSpeed = 0.01f; // 1%
|
||||
|
||||
[Tooltip("Local timeline slowdown in % while slowing down.")]
|
||||
[Range(0, 1)]
|
||||
public double slowdownSpeed = 0.01f; // 1%
|
||||
|
||||
[Tooltip("Catchup/Slowdown is adjusted over n-second exponential moving average.")]
|
||||
public int driftEmaDuration = 1; // shouldn't need to modify this, but expose it anyway
|
||||
|
||||
// we use EMA to average the last second worth of snapshot time diffs.
|
||||
// manually averaging the last second worth of values with a for loop
|
||||
// would be the same, but a moving average is faster because we only
|
||||
// ever add one value.
|
||||
ExponentialMovingAverage serverDriftEma;
|
||||
ExponentialMovingAverage clientDriftEma;
|
||||
|
||||
// dynamic buffer time adjustment //////////////////////////////////////
|
||||
// dynamically adjusts bufferTimeMultiplier for smooth results.
|
||||
// to understand how this works, try this manually:
|
||||
//
|
||||
// - disable dynamic adjustment
|
||||
// - set jitter = 0.2 (20% is a lot!)
|
||||
// - notice some stuttering
|
||||
// - disable interpolation to see just how much jitter this really is(!)
|
||||
// - enable interpolation again
|
||||
// - manually increase bufferTimeMultiplier to 3-4
|
||||
// ... the cube slows down (blue) until it's smooth
|
||||
// - with dynamic adjustment enabled, it will set 4 automatically
|
||||
// ... the cube slows down (blue) until it's smooth as well
|
||||
//
|
||||
// note that 20% jitter is extreme.
|
||||
// for this to be perfectly smooth, set the safety tolerance to '2'.
|
||||
// but realistically this is not necessary, and '1' is enough.
|
||||
[Header("Dynamic Adjustment")]
|
||||
[Tooltip("Automatically adjust bufferTimeMultiplier for smooth results.\nSets a low multiplier on stable connections, and a high multiplier on jittery connections.")]
|
||||
public bool dynamicAdjustment = true;
|
||||
|
||||
[Tooltip("Safety buffer that is always added to the dynamic bufferTimeMultiplier adjustment.")]
|
||||
public float dynamicAdjustmentTolerance = 1; // 1 is realistically just fine, 2 is very very safe even for 20% jitter. can be half a frame too. (see above comments)
|
||||
|
||||
[Tooltip("Dynamic adjustment is computed over n-second exponential moving average standard deviation.")]
|
||||
public int deliveryTimeEmaDuration = 2; // 1-2s recommended to capture average delivery time
|
||||
|
||||
ExponentialMovingAverage serverDeliveryTimeEma; // average delivery time (standard deviation gives average jitter)
|
||||
ExponentialMovingAverage clientDeliveryTimeEma; // average delivery time (standard deviation gives average jitter)
|
||||
|
||||
// buffers & time //////////////////////////////////////////////////////
|
||||
// snapshots sorted by timestamp
|
||||
// in the original article, glenn fiedler drops any snapshots older than
|
||||
// the last received snapshot.
|
||||
// -> instead, we insert into a sorted buffer
|
||||
// -> the higher the buffer information density, the better
|
||||
// -> we still drop anything older than the first element in the buffer
|
||||
// => internal for testing
|
||||
//
|
||||
// IMPORTANT: of explicit 'NTSnapshot' type instead of 'Snapshot'
|
||||
// interface because List<interface> allocates through boxing
|
||||
internal SortedList<double, NTSnapshot> serverSnapshots = new SortedList<double, NTSnapshot>();
|
||||
internal SortedList<double, NTSnapshot> clientSnapshots = new SortedList<double, NTSnapshot>();
|
||||
|
||||
// only convert the static Interpolation function to Func<T> once to
|
||||
// avoid allocations
|
||||
Func<NTSnapshot, NTSnapshot, double, NTSnapshot> Interpolate = NTSnapshot.Interpolate;
|
||||
|
||||
// for smooth interpolation, we need to interpolate along server time.
|
||||
// any other time (arrival on client, client local time, etc.) is not
|
||||
// going to give smooth results.
|
||||
double serverTimeline;
|
||||
double serverTimescale;
|
||||
|
||||
// catchup / slowdown adjustments are applied to timescale,
|
||||
// to be adjusted in every update instead of when receiving messages.
|
||||
double clientTimeline;
|
||||
double clientTimescale;
|
||||
|
||||
// only sync when changed hack /////////////////////////////////////////
|
||||
#if onlySyncOnChange_BANDWIDTH_SAVING
|
||||
[Header("Sync Only If Changed")]
|
||||
[Tooltip("When true, changes are not sent unless greater than sensitivity values below.")]
|
||||
public bool onlySyncOnChange = true;
|
||||
|
||||
// 3 was original, but testing under really bad network conditions, 2%-5% packet loss and 250-1200ms ping, 5 proved to eliminate any twitching.
|
||||
[Tooltip("How much time, as a multiple of send interval, has passed before clearing buffers.")]
|
||||
public float bufferResetMultiplier = 5;
|
||||
|
||||
[Header("Sensitivity"), Tooltip("Sensitivity of changes needed before an updated state is sent over the network")]
|
||||
public float positionSensitivity = 0.01f;
|
||||
public float rotationSensitivity = 0.01f;
|
||||
public float scaleSensitivity = 0.01f;
|
||||
|
||||
protected bool positionChanged;
|
||||
protected bool rotationChanged;
|
||||
protected bool scaleChanged;
|
||||
|
||||
// Used to store last sent snapshots
|
||||
protected NTSnapshot lastSnapshot;
|
||||
protected bool cachedSnapshotComparison;
|
||||
protected bool hasSentUnchangedPosition;
|
||||
#endif
|
||||
// selective sync //////////////////////////////////////////////////////
|
||||
[Header("Selective Sync & interpolation")]
|
||||
public bool syncPosition = true;
|
||||
public bool syncRotation = true;
|
||||
public bool syncScale = false; // rare. off by default.
|
||||
|
||||
double lastClientSendTime;
|
||||
double lastServerSendTime;
|
||||
|
||||
// debugging ///////////////////////////////////////////////////////////
|
||||
[Header("Debug")]
|
||||
public bool showGizmos;
|
||||
public bool showOverlay;
|
||||
public Color overlayColor = new Color(0, 0, 0, 0.5f);
|
||||
|
||||
// initialization //////////////////////////////////////////////////////
|
||||
// make sure to call this when inheriting too!
|
||||
protected virtual void Awake()
|
||||
{
|
||||
// initialize EMA with 'emaDuration' seconds worth of history.
|
||||
// 1 second holds 'sendRate' worth of values.
|
||||
// multiplied by emaDuration gives n-seconds.
|
||||
serverDriftEma = new ExponentialMovingAverage(sendRate * driftEmaDuration);
|
||||
clientDriftEma = new ExponentialMovingAverage(sendRate * driftEmaDuration);
|
||||
serverDeliveryTimeEma = new ExponentialMovingAverage(sendRate * deliveryTimeEmaDuration);
|
||||
clientDeliveryTimeEma = new ExponentialMovingAverage(sendRate * deliveryTimeEmaDuration);
|
||||
}
|
||||
|
||||
// snapshot functions //////////////////////////////////////////////////
|
||||
// construct a snapshot of the current state
|
||||
// => internal for testing
|
||||
protected virtual NTSnapshot ConstructSnapshot()
|
||||
{
|
||||
// NetworkTime.localTime for double precision until Unity has it too
|
||||
return new NTSnapshot(
|
||||
// our local time is what the other end uses as remote time
|
||||
NetworkTime.localTime,
|
||||
// the other end fills out local time itself
|
||||
0,
|
||||
targetComponent.localPosition,
|
||||
targetComponent.localRotation,
|
||||
targetComponent.localScale
|
||||
);
|
||||
}
|
||||
|
||||
// apply a snapshot to the Transform.
|
||||
// -> start, end, interpolated are all passed in caes they are needed
|
||||
// -> a regular game would apply the 'interpolated' snapshot
|
||||
// -> a board game might want to jump to 'goal' directly
|
||||
// (it's easier to always interpolate and then apply selectively,
|
||||
// instead of manually interpolating x, y, z, ... depending on flags)
|
||||
// => internal for testing
|
||||
//
|
||||
// NOTE: stuck detection is unnecessary here.
|
||||
// we always set transform.position anyway, we can't get stuck.
|
||||
protected virtual void ApplySnapshot(NTSnapshot interpolated)
|
||||
{
|
||||
// local position/rotation for VR support
|
||||
//
|
||||
// if syncPosition/Rotation/Scale is disabled then we received nulls
|
||||
// -> current position/rotation/scale would've been added as snapshot
|
||||
// -> we still interpolated
|
||||
// -> but simply don't apply it. if the user doesn't want to sync
|
||||
// scale, then we should not touch scale etc.
|
||||
if (syncPosition)
|
||||
targetComponent.localPosition = interpolated.position;
|
||||
|
||||
if (syncRotation)
|
||||
targetComponent.localRotation = interpolated.rotation;
|
||||
|
||||
if (syncScale)
|
||||
targetComponent.localScale = interpolated.scale;
|
||||
}
|
||||
|
||||
#if onlySyncOnChange_BANDWIDTH_SAVING
|
||||
// Returns true if position, rotation AND scale are unchanged, within given sensitivity range.
|
||||
protected virtual bool CompareSnapshots(NTSnapshot currentSnapshot)
|
||||
{
|
||||
positionChanged = Vector3.SqrMagnitude(lastSnapshot.position - currentSnapshot.position) > positionSensitivity * positionSensitivity;
|
||||
rotationChanged = Quaternion.Angle(lastSnapshot.rotation, currentSnapshot.rotation) > rotationSensitivity;
|
||||
scaleChanged = Vector3.SqrMagnitude(lastSnapshot.scale - currentSnapshot.scale) > scaleSensitivity * scaleSensitivity;
|
||||
|
||||
return (!positionChanged && !rotationChanged && !scaleChanged);
|
||||
}
|
||||
#endif
|
||||
// cmd /////////////////////////////////////////////////////////////////
|
||||
// only unreliable. see comment above of this file.
|
||||
[Command(channel = Channels.Unreliable)]
|
||||
void CmdClientToServerSync(Vector3? position, Quaternion? rotation, Vector3? scale)
|
||||
{
|
||||
OnClientToServerSync(position, rotation, scale);
|
||||
//For client authority, immediately pass on the client snapshot to all other
|
||||
//clients instead of waiting for server to send its snapshots.
|
||||
if (clientAuthority)
|
||||
{
|
||||
RpcServerToClientSync(position, rotation, scale);
|
||||
}
|
||||
}
|
||||
|
||||
// local authority client sends sync message to server for broadcasting
|
||||
protected virtual void OnClientToServerSync(Vector3? position, Quaternion? rotation, Vector3? scale)
|
||||
{
|
||||
// only apply if in client authority mode
|
||||
if (!clientAuthority) return;
|
||||
|
||||
// protect against ever growing buffer size attacks
|
||||
if (serverSnapshots.Count >= bufferSizeLimit) return;
|
||||
|
||||
// only player owned objects (with a connection) can send to
|
||||
// server. we can get the timestamp from the connection.
|
||||
double timestamp = connectionToClient.remoteTimeStamp;
|
||||
#if onlySyncOnChange_BANDWIDTH_SAVING
|
||||
if (onlySyncOnChange)
|
||||
{
|
||||
double timeIntervalCheck = bufferResetMultiplier * sendInterval;
|
||||
|
||||
if (serverSnapshots.Count > 0 && serverSnapshots.Values[serverSnapshots.Count - 1].remoteTime + timeIntervalCheck < timestamp)
|
||||
{
|
||||
Reset();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
// position, rotation, scale can have no value if same as last time.
|
||||
// saves bandwidth.
|
||||
// but we still need to feed it to snapshot interpolation. we can't
|
||||
// just have gaps in there if nothing has changed. for example, if
|
||||
// client sends snapshot at t=0
|
||||
// client sends nothing for 10s because not moved
|
||||
// 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 = serverSnapshots.Count > 0 ? serverSnapshots.Values[serverSnapshots.Count - 1].position : targetComponent.localPosition;
|
||||
if (!rotation.HasValue) rotation = serverSnapshots.Count > 0 ? serverSnapshots.Values[serverSnapshots.Count - 1].rotation : targetComponent.localRotation;
|
||||
if (!scale.HasValue) scale = serverSnapshots.Count > 0 ? serverSnapshots.Values[serverSnapshots.Count - 1].scale : targetComponent.localScale;
|
||||
|
||||
// construct snapshot with batch timestamp to save bandwidth
|
||||
NTSnapshot snapshot = new NTSnapshot(
|
||||
timestamp,
|
||||
NetworkTime.localTime,
|
||||
position.Value, rotation.Value, scale.Value
|
||||
);
|
||||
|
||||
// (optional) dynamic adjustment
|
||||
if (dynamicAdjustment)
|
||||
{
|
||||
// set bufferTime on the fly.
|
||||
// shows in inspector for easier debugging :)
|
||||
bufferTimeMultiplier = SnapshotInterpolation.DynamicAdjustment(
|
||||
sendInterval,
|
||||
serverDeliveryTimeEma.StandardDeviation,
|
||||
dynamicAdjustmentTolerance
|
||||
);
|
||||
// Debug.Log($"[Server]: {name} delivery std={serverDeliveryTimeEma.StandardDeviation} bufferTimeMult := {bufferTimeMultiplier} ");
|
||||
}
|
||||
|
||||
// insert into the server buffer & initialize / adjust / catchup
|
||||
SnapshotInterpolation.Insert(
|
||||
serverSnapshots,
|
||||
snapshot,
|
||||
ref serverTimeline,
|
||||
ref serverTimescale,
|
||||
sendInterval,
|
||||
bufferTime,
|
||||
catchupSpeed,
|
||||
slowdownSpeed,
|
||||
ref serverDriftEma,
|
||||
catchupNegativeThreshold,
|
||||
catchupPositiveThreshold,
|
||||
ref serverDeliveryTimeEma
|
||||
);
|
||||
}
|
||||
|
||||
// rpc /////////////////////////////////////////////////////////////////
|
||||
// only unreliable. see comment above of this file.
|
||||
[ClientRpc(channel = Channels.Unreliable)]
|
||||
void RpcServerToClientSync(Vector3? position, Quaternion? rotation, Vector3? scale) =>
|
||||
OnServerToClientSync(position, rotation, scale);
|
||||
|
||||
// server broadcasts sync message to all clients
|
||||
protected virtual void OnServerToClientSync(Vector3? position, Quaternion? rotation, Vector3? scale)
|
||||
{
|
||||
// in host mode, the server sends rpcs to all clients.
|
||||
// the host client itself will receive them too.
|
||||
// -> host server is always the source of truth
|
||||
// -> we can ignore any rpc on the host client
|
||||
// => otherwise host objects would have ever growing clientBuffers
|
||||
// (rpc goes to clients. if isServer is true too then we are host)
|
||||
if (isServer) return;
|
||||
|
||||
// don't apply for local player with authority
|
||||
if (IsClientWithAuthority) return;
|
||||
|
||||
// protect against ever growing buffer size attacks
|
||||
if (clientSnapshots.Count >= bufferSizeLimit) return;
|
||||
|
||||
// on the client, we receive rpcs for all entities.
|
||||
// not all of them have a connectionToServer.
|
||||
// but all of them go through NetworkClient.connection.
|
||||
// we can get the timestamp from there.
|
||||
double timestamp = NetworkClient.connection.remoteTimeStamp;
|
||||
#if onlySyncOnChange_BANDWIDTH_SAVING
|
||||
if (onlySyncOnChange)
|
||||
{
|
||||
double timeIntervalCheck = bufferResetMultiplier * sendInterval;
|
||||
|
||||
if (clientSnapshots.Count > 0 && clientSnapshots.Values[clientSnapshots.Count - 1].remoteTime + timeIntervalCheck < timestamp)
|
||||
{
|
||||
Reset();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
// position, rotation, scale can have no value if same as last time.
|
||||
// saves bandwidth.
|
||||
// but we still need to feed it to snapshot interpolation. we can't
|
||||
// just have gaps in there if nothing has changed. for example, if
|
||||
// client sends snapshot at t=0
|
||||
// client sends nothing for 10s because not moved
|
||||
// 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 = clientSnapshots.Count > 0 ? clientSnapshots.Values[clientSnapshots.Count - 1].position : targetComponent.localPosition;
|
||||
if (!rotation.HasValue) rotation = clientSnapshots.Count > 0 ? clientSnapshots.Values[clientSnapshots.Count - 1].rotation : targetComponent.localRotation;
|
||||
if (!scale.HasValue) scale = clientSnapshots.Count > 0 ? clientSnapshots.Values[clientSnapshots.Count - 1].scale : targetComponent.localScale;
|
||||
|
||||
// construct snapshot with batch timestamp to save bandwidth
|
||||
NTSnapshot snapshot = new NTSnapshot(
|
||||
timestamp,
|
||||
NetworkTime.localTime,
|
||||
position.Value, rotation.Value, scale.Value
|
||||
);
|
||||
|
||||
// (optional) dynamic adjustment
|
||||
if (dynamicAdjustment)
|
||||
{
|
||||
// set bufferTime on the fly.
|
||||
// shows in inspector for easier debugging :)
|
||||
bufferTimeMultiplier = SnapshotInterpolation.DynamicAdjustment(
|
||||
sendInterval,
|
||||
clientDeliveryTimeEma.StandardDeviation,
|
||||
dynamicAdjustmentTolerance
|
||||
);
|
||||
// Debug.Log($"[Client]: {name} delivery std={clientDeliveryTimeEma.StandardDeviation} bufferTimeMult := {bufferTimeMultiplier} ");
|
||||
}
|
||||
|
||||
// insert into the client buffer & initialize / adjust / catchup
|
||||
SnapshotInterpolation.Insert(
|
||||
clientSnapshots,
|
||||
snapshot,
|
||||
ref clientTimeline,
|
||||
ref clientTimescale,
|
||||
sendInterval,
|
||||
bufferTime,
|
||||
catchupSpeed,
|
||||
slowdownSpeed,
|
||||
ref clientDriftEma,
|
||||
catchupNegativeThreshold,
|
||||
catchupPositiveThreshold,
|
||||
ref clientDeliveryTimeEma
|
||||
);
|
||||
}
|
||||
|
||||
// update //////////////////////////////////////////////////////////////
|
||||
void UpdateServer()
|
||||
{
|
||||
// broadcast to all clients each 'sendInterval'
|
||||
// (client with authority will drop the rpc)
|
||||
// NetworkTime.localTime for double precision until Unity has it too
|
||||
//
|
||||
// IMPORTANT:
|
||||
// snapshot interpolation requires constant sending.
|
||||
// DO NOT only send if position changed. for example:
|
||||
// ---
|
||||
// * client sends first position at t=0
|
||||
// * ... 10s later ...
|
||||
// * client moves again, sends second position at t=10
|
||||
// ---
|
||||
// * server gets first position at t=0
|
||||
// * server gets second position at t=10
|
||||
// * server moves from first to second within a time of 10s
|
||||
// => would be a super slow move, instead of a wait & move.
|
||||
//
|
||||
// IMPORTANT:
|
||||
// DO NOT send nulls if not changed 'since last send' either. we
|
||||
// send unreliable and don't know which 'last send' the other end
|
||||
// received successfully.
|
||||
//
|
||||
// Checks to ensure server only sends snapshots if object is
|
||||
// on server authority(!clientAuthority) mode because on client
|
||||
// authority mode snapshots are broadcasted right after the authoritative
|
||||
// client updates server in the command function(see above), OR,
|
||||
// since host does not send anything to update the server, any client
|
||||
// authoritative movement done by the host will have to be broadcasted
|
||||
// here by checking IsClientWithAuthority.
|
||||
if (NetworkTime.localTime >= lastServerSendTime + sendInterval &&
|
||||
(!clientAuthority || IsClientWithAuthority))
|
||||
{
|
||||
// send snapshot without timestamp.
|
||||
// receiver gets it from batch timestamp to save bandwidth.
|
||||
NTSnapshot snapshot = ConstructSnapshot();
|
||||
#if onlySyncOnChange_BANDWIDTH_SAVING
|
||||
cachedSnapshotComparison = CompareSnapshots(snapshot);
|
||||
if (cachedSnapshotComparison && hasSentUnchangedPosition && onlySyncOnChange) { return; }
|
||||
#endif
|
||||
|
||||
#if onlySyncOnChange_BANDWIDTH_SAVING
|
||||
RpcServerToClientSync(
|
||||
// only sync what the user wants to sync
|
||||
syncPosition && positionChanged ? snapshot.position : default(Vector3?),
|
||||
syncRotation && rotationChanged ? snapshot.rotation : default(Quaternion?),
|
||||
syncScale && scaleChanged ? snapshot.scale : default(Vector3?)
|
||||
);
|
||||
#else
|
||||
RpcServerToClientSync(
|
||||
// only sync what the user wants to sync
|
||||
syncPosition ? snapshot.position : default(Vector3?),
|
||||
syncRotation ? snapshot.rotation : default(Quaternion?),
|
||||
syncScale ? snapshot.scale : default(Vector3?)
|
||||
);
|
||||
#endif
|
||||
|
||||
lastServerSendTime = NetworkTime.localTime;
|
||||
#if onlySyncOnChange_BANDWIDTH_SAVING
|
||||
if (cachedSnapshotComparison)
|
||||
{
|
||||
hasSentUnchangedPosition = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
hasSentUnchangedPosition = false;
|
||||
lastSnapshot = snapshot;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// apply buffered snapshots IF client authority
|
||||
// -> in server authority, server moves the object
|
||||
// so no need to apply any snapshots there.
|
||||
// -> don't apply for host mode player objects either, even if in
|
||||
// client authority mode. if it doesn't go over the network,
|
||||
// then we don't need to do anything.
|
||||
if (clientAuthority && !hasAuthority)
|
||||
{
|
||||
if (serverSnapshots.Count > 0)
|
||||
{
|
||||
// compute snapshot interpolation & apply if any was spit out
|
||||
if (SnapshotInterpolation.Step(
|
||||
serverSnapshots,
|
||||
Time.unscaledDeltaTime,
|
||||
ref serverTimeline,
|
||||
serverTimescale,
|
||||
Interpolate,
|
||||
out NTSnapshot computed))
|
||||
{
|
||||
ApplySnapshot(computed);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UpdateClient()
|
||||
{
|
||||
// client authority, and local player (= allowed to move myself)?
|
||||
if (IsClientWithAuthority)
|
||||
{
|
||||
// https://github.com/vis2k/Mirror/pull/2992/
|
||||
if (!NetworkClient.ready) return;
|
||||
|
||||
// send to server each 'sendInterval'
|
||||
// NetworkTime.localTime for double precision until Unity has it too
|
||||
//
|
||||
// IMPORTANT:
|
||||
// snapshot interpolation requires constant sending.
|
||||
// DO NOT only send if position changed. for example:
|
||||
// ---
|
||||
// * client sends first position at t=0
|
||||
// * ... 10s later ...
|
||||
// * client moves again, sends second position at t=10
|
||||
// ---
|
||||
// * server gets first position at t=0
|
||||
// * server gets second position at t=10
|
||||
// * server moves from first to second within a time of 10s
|
||||
// => would be a super slow move, instead of a wait & move.
|
||||
//
|
||||
// IMPORTANT:
|
||||
// DO NOT send nulls if not changed 'since last send' either. we
|
||||
// send unreliable and don't know which 'last send' the other end
|
||||
// received successfully.
|
||||
if (NetworkTime.localTime >= lastClientSendTime + sendInterval)
|
||||
{
|
||||
// send snapshot without timestamp.
|
||||
// receiver gets it from batch timestamp to save bandwidth.
|
||||
NTSnapshot snapshot = ConstructSnapshot();
|
||||
#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
|
||||
|
||||
lastClientSendTime = NetworkTime.localTime;
|
||||
#if onlySyncOnChange_BANDWIDTH_SAVING
|
||||
if (cachedSnapshotComparison)
|
||||
{
|
||||
hasSentUnchangedPosition = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
hasSentUnchangedPosition = false;
|
||||
lastSnapshot = snapshot;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
// for all other clients (and for local player if !authority),
|
||||
// we need to apply snapshots from the buffer
|
||||
else
|
||||
{
|
||||
if (clientSnapshots.Count > 0)
|
||||
{
|
||||
// compute snapshot interpolation & apply if any was spit out
|
||||
if (SnapshotInterpolation.Step(
|
||||
clientSnapshots,
|
||||
Time.unscaledDeltaTime,
|
||||
ref clientTimeline,
|
||||
clientTimescale,
|
||||
Interpolate,
|
||||
out NTSnapshot computed))
|
||||
{
|
||||
ApplySnapshot(computed);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
// if server then always sync to others.
|
||||
if (isServer) UpdateServer();
|
||||
// 'else if' because host mode shouldn't send anything to server.
|
||||
// it is the server. don't overwrite anything there.
|
||||
else if (isClient) UpdateClient();
|
||||
}
|
||||
|
||||
// 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.
|
||||
targetComponent.position = destination;
|
||||
|
||||
// TODO
|
||||
// what if we still receive a snapshot from before the interpolation?
|
||||
// it could easily happen over unreliable.
|
||||
// -> maybe add destination as first entry?
|
||||
}
|
||||
|
||||
// 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.
|
||||
targetComponent.position = destination;
|
||||
targetComponent.rotation = rotation;
|
||||
|
||||
// TODO
|
||||
// what if we still receive a snapshot from before the interpolation?
|
||||
// it could easily happen over unreliable.
|
||||
// -> maybe add destination as first entry?
|
||||
}
|
||||
|
||||
// server->client teleport to force position without interpolation.
|
||||
// otherwise it would interpolate to a (far away) new position.
|
||||
// => manually calling Teleport is the only 100% reliable solution.
|
||||
[ClientRpc]
|
||||
public void RpcTeleport(Vector3 destination)
|
||||
{
|
||||
// NOTE: even in client authority mode, the server is always allowed
|
||||
// to teleport the player. for example:
|
||||
// * CmdEnterPortal() might teleport the player
|
||||
// * Some people use client authority with server sided checks
|
||||
// so the server should be able to reset position if needed.
|
||||
|
||||
// TODO what about host mode?
|
||||
OnTeleport(destination);
|
||||
}
|
||||
|
||||
// server->client teleport to force position and rotation without interpolation.
|
||||
// otherwise it would interpolate to a (far away) new position.
|
||||
// => manually calling Teleport is the only 100% reliable solution.
|
||||
[ClientRpc]
|
||||
public void RpcTeleport(Vector3 destination, Quaternion rotation)
|
||||
{
|
||||
// NOTE: even in client authority mode, the server is always allowed
|
||||
// to teleport the player. for example:
|
||||
// * CmdEnterPortal() might teleport the player
|
||||
// * Some people use client authority with server sided checks
|
||||
// so the server should be able to reset position if needed.
|
||||
|
||||
// TODO what about host mode?
|
||||
OnTeleport(destination, rotation);
|
||||
}
|
||||
|
||||
// client->server teleport to force position without interpolation.
|
||||
// otherwise it would interpolate to a (far away) new position.
|
||||
// => manually calling Teleport is the only 100% reliable solution.
|
||||
[Command]
|
||||
public void CmdTeleport(Vector3 destination)
|
||||
{
|
||||
// client can only teleport objects that it has authority over.
|
||||
if (!clientAuthority) return;
|
||||
|
||||
// TODO what about host mode?
|
||||
OnTeleport(destination);
|
||||
|
||||
// if a client teleports, we need to broadcast to everyone else too
|
||||
// TODO the teleported client should ignore the rpc though.
|
||||
// otherwise if it already moved again after teleporting,
|
||||
// the rpc would come a little bit later and reset it once.
|
||||
// TODO or not? if client ONLY calls Teleport(pos), the position
|
||||
// would only be set after the rpc. unless the client calls
|
||||
// BOTH Teleport(pos) and targetComponent.position=pos
|
||||
RpcTeleport(destination);
|
||||
}
|
||||
|
||||
// client->server teleport to force position and rotation without interpolation.
|
||||
// otherwise it would interpolate to a (far away) new position.
|
||||
// => manually calling Teleport is the only 100% reliable solution.
|
||||
[Command]
|
||||
public void CmdTeleport(Vector3 destination, Quaternion rotation)
|
||||
{
|
||||
// client can only teleport objects that it has authority over.
|
||||
if (!clientAuthority) return;
|
||||
|
||||
// TODO what about host mode?
|
||||
OnTeleport(destination, rotation);
|
||||
|
||||
// if a client teleports, we need to broadcast to everyone else too
|
||||
// TODO the teleported client should ignore the rpc though.
|
||||
// otherwise if it already moved again after teleporting,
|
||||
// the rpc would come a little bit later and reset it once.
|
||||
// TODO or not? if client ONLY calls Teleport(pos), the position
|
||||
// would only be set after the rpc. unless the client calls
|
||||
// BOTH Teleport(pos) and targetComponent.position=pos
|
||||
RpcTeleport(destination, rotation);
|
||||
}
|
||||
|
||||
public virtual void Reset()
|
||||
{
|
||||
// disabled objects aren't updated anymore.
|
||||
// so let's clear the buffers.
|
||||
serverSnapshots.Clear();
|
||||
clientSnapshots.Clear();
|
||||
|
||||
// reset interpolation time too so we start at t=0 next time
|
||||
serverTimeline = 0;
|
||||
serverTimescale = 0;
|
||||
clientTimeline = 0;
|
||||
clientTimescale = 0;
|
||||
}
|
||||
|
||||
protected virtual void OnDisable() => Reset();
|
||||
protected virtual void OnEnable() => Reset();
|
||||
|
||||
protected virtual void OnValidate()
|
||||
{
|
||||
// thresholds need to be <0 and >0
|
||||
catchupNegativeThreshold = Math.Min(catchupNegativeThreshold, 0);
|
||||
catchupPositiveThreshold = Math.Max(catchupPositiveThreshold, 0);
|
||||
|
||||
// buffer limit should be at least multiplier to have enough in there
|
||||
bufferSizeLimit = Mathf.Max((int)bufferTimeMultiplier, bufferSizeLimit);
|
||||
}
|
||||
|
||||
public override bool OnSerialize(NetworkWriter writer, bool initialState)
|
||||
{
|
||||
// sync target component's position on spawn.
|
||||
// fixes https://github.com/vis2k/Mirror/pull/3051/
|
||||
// (Spawn message wouldn't sync NTChild positions either)
|
||||
if (initialState)
|
||||
{
|
||||
if (syncPosition) writer.WriteVector3(targetComponent.localPosition);
|
||||
if (syncRotation) writer.WriteQuaternion(targetComponent.localRotation);
|
||||
if (syncScale) writer.WriteVector3(targetComponent.localScale);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public override void OnDeserialize(NetworkReader reader, bool initialState)
|
||||
{
|
||||
// sync target component's position on spawn.
|
||||
// fixes https://github.com/vis2k/Mirror/pull/3051/
|
||||
// (Spawn message wouldn't sync NTChild positions either)
|
||||
if (initialState)
|
||||
{
|
||||
if (syncPosition) targetComponent.localPosition = reader.ReadVector3();
|
||||
if (syncRotation) targetComponent.localRotation = reader.ReadQuaternion();
|
||||
if (syncScale) targetComponent.localScale = reader.ReadVector3();
|
||||
}
|
||||
}
|
||||
|
||||
// OnGUI allocates even if it does nothing. avoid in release.
|
||||
#if UNITY_EDITOR || DEVELOPMENT_BUILD
|
||||
// debug ///////////////////////////////////////////////////////////////
|
||||
protected virtual void OnGUI()
|
||||
{
|
||||
if (!showOverlay) return;
|
||||
|
||||
// show data next to player for easier debugging. this is very useful!
|
||||
// IMPORTANT: this is basically an ESP hack for shooter games.
|
||||
// DO NOT make this available with a hotkey in release builds
|
||||
if (!Debug.isDebugBuild) return;
|
||||
|
||||
// project position to screen
|
||||
Vector3 point = Camera.main.WorldToScreenPoint(targetComponent.position);
|
||||
|
||||
// enough alpha, in front of camera and in screen?
|
||||
if (point.z >= 0 && Utils.IsPointInScreen(point))
|
||||
{
|
||||
GUI.color = overlayColor;
|
||||
GUILayout.BeginArea(new Rect(point.x, Screen.height - point.y, 200, 100));
|
||||
|
||||
// always show both client & server buffers so it's super
|
||||
// obvious if we accidentally populate both.
|
||||
if (serverSnapshots.Count > 0)
|
||||
{
|
||||
GUILayout.Label($"Server Buffer:{serverSnapshots.Count}");
|
||||
GUILayout.Label($"Server Timescale:{serverTimescale * 100:F2}%");
|
||||
}
|
||||
|
||||
if (clientSnapshots.Count > 0)
|
||||
{
|
||||
GUILayout.Label($"Client Buffer:{clientSnapshots.Count}");
|
||||
GUILayout.Label($"Client Timescale:{clientTimescale * 100:F2}%");
|
||||
}
|
||||
|
||||
GUILayout.EndArea();
|
||||
GUI.color = Color.white;
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void DrawGizmos(SortedList<double, NTSnapshot> buffer)
|
||||
{
|
||||
// only draw if we have at least two entries
|
||||
if (buffer.Count < 2) return;
|
||||
|
||||
// calculate threshold for 'old enough' snapshots
|
||||
double threshold = NetworkTime.localTime - bufferTime;
|
||||
Color oldEnoughColor = new Color(0, 1, 0, 0.5f);
|
||||
Color notOldEnoughColor = new Color(0.5f, 0.5f, 0.5f, 0.3f);
|
||||
|
||||
// draw the whole buffer for easier debugging.
|
||||
// it's worth seeing how much we have buffered ahead already
|
||||
for (int i = 0; i < buffer.Count; ++i)
|
||||
{
|
||||
// color depends on if old enough or not
|
||||
NTSnapshot entry = buffer.Values[i];
|
||||
bool oldEnough = entry.localTime <= threshold;
|
||||
Gizmos.color = oldEnough ? oldEnoughColor : notOldEnoughColor;
|
||||
Gizmos.DrawCube(entry.position, Vector3.one);
|
||||
}
|
||||
|
||||
// extra: lines between start<->position<->goal
|
||||
Gizmos.color = Color.green;
|
||||
Gizmos.DrawLine(buffer.Values[0].position, targetComponent.position);
|
||||
Gizmos.color = Color.white;
|
||||
Gizmos.DrawLine(targetComponent.position, buffer.Values[1].position);
|
||||
}
|
||||
|
||||
protected virtual void OnDrawGizmos()
|
||||
{
|
||||
// This fires in edit mode but that spams NRE's so check isPlaying
|
||||
if (!Application.isPlaying) return;
|
||||
if (!showGizmos) return;
|
||||
|
||||
if (isServer) DrawGizmos(serverSnapshots);
|
||||
if (isClient) DrawGizmos(clientSnapshots);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
|
@ -6,6 +6,6 @@ MonoImporter:
|
|||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData: ''
|
||||
assetBundleName: ''
|
||||
assetBundleVariant: ''
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
// A component to synchronize the position of child transforms of networked objects.
|
||||
// There must be a NetworkTransform on the root object of the hierarchy. There can be multiple NetworkTransformChild components on an object. This does not use physics for synchronization, it simply synchronizes the localPosition and localRotation of the child transform and lerps towards the recieved values.
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
[AddComponentMenu("Network/Network Transform Child")]
|
||||
public class NetworkTransformChild : NetworkTransformBase
|
||||
{
|
||||
[Header("Target")]
|
||||
public Transform target;
|
||||
protected override Transform targetComponent => target;
|
||||
}
|
||||
}
|
||||
|
|
@ -6,6 +6,6 @@ MonoImporter:
|
|||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData: ''
|
||||
assetBundleName: ''
|
||||
assetBundleVariant: ''
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
// snapshot for snapshot interpolation
|
||||
// https://gafferongames.com/post/snapshot_interpolation/
|
||||
// position, rotation, scale for compatibility for now.
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
// NetworkTransform Snapshot
|
||||
public struct NTSnapshot : Snapshot
|
||||
{
|
||||
// time or sequence are needed to throw away older snapshots.
|
||||
//
|
||||
// glenn fiedler starts with a 16 bit sequence number.
|
||||
// supposedly this is meant as a simplified example.
|
||||
// in the end we need the remote timestamp for accurate interpolation
|
||||
// and buffering over time.
|
||||
//
|
||||
// note: in theory, IF server sends exactly(!) at the same interval then
|
||||
// the 16 bit ushort timestamp would be enough to calculate the
|
||||
// remote time (sequence * sendInterval). but Unity's update is
|
||||
// not guaranteed to run on the exact intervals / do catchup.
|
||||
// => remote timestamp is better for now
|
||||
//
|
||||
// [REMOTE TIME, NOT LOCAL TIME]
|
||||
// => DOUBLE for long term accuracy & batching gives us double anyway
|
||||
public double remoteTime { get; set; }
|
||||
// the local timestamp (when we received it)
|
||||
// used to know if the first two snapshots are old enough to start.
|
||||
public double localTime { get; set; }
|
||||
|
||||
public Vector3 position;
|
||||
public Quaternion rotation;
|
||||
public Vector3 scale;
|
||||
|
||||
public NTSnapshot(double remoteTime, double localTime, Vector3 position, Quaternion rotation, Vector3 scale)
|
||||
{
|
||||
this.remoteTime = remoteTime;
|
||||
this.localTime = localTime;
|
||||
this.position = position;
|
||||
this.rotation = rotation;
|
||||
this.scale = scale;
|
||||
}
|
||||
|
||||
public static NTSnapshot Interpolate(NTSnapshot from, NTSnapshot to, double t)
|
||||
{
|
||||
// NOTE:
|
||||
// Vector3 & Quaternion components are float anyway, so we can
|
||||
// keep using the functions with 't' as float instead of double.
|
||||
return new NTSnapshot(
|
||||
// interpolated snapshot is applied directly. don't need timestamps.
|
||||
0, 0,
|
||||
// lerp position/rotation/scale unclamped in case we ever need
|
||||
// to extrapolate. atm SnapshotInterpolation never does.
|
||||
Vector3.LerpUnclamped(from.position, to.position, (float)t),
|
||||
// IMPORTANT: LerpUnclamped(0, 60, 1.5) extrapolates to ~86.
|
||||
// SlerpUnclamped(0, 60, 1.5) extrapolates to 90!
|
||||
// (0, 90, 1.5) is even worse. for Lerp.
|
||||
// => Slerp works way better for our euler angles.
|
||||
Quaternion.SlerpUnclamped(from.rotation, to.rotation, (float)t),
|
||||
Vector3.LerpUnclamped(from.scale, to.scale, (float)t)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: d3dae77b43dc4e1dbb2012924b2da79c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -1,563 +0,0 @@
|
|||
// vis2k:
|
||||
// base class for NetworkTransform and NetworkTransformChild.
|
||||
// New method is simple and stupid. No more 1500 lines of code.
|
||||
//
|
||||
// Server sends current data.
|
||||
// Client saves it and interpolates last and latest data points.
|
||||
// Update handles transform movement / rotation
|
||||
// FixedUpdate handles rigidbody movement / rotation
|
||||
//
|
||||
// Notes:
|
||||
// * Built-in Teleport detection in case of lags / teleport / obstacles
|
||||
// * Quaternion > EulerAngles because gimbal lock and Quaternion.Slerp
|
||||
// * Syncs XYZ. Works 3D and 2D. Saving 4 bytes isn't worth 1000 lines of code.
|
||||
// * Initial delay might happen if server sends packet immediately after moving
|
||||
// just 1cm, hence we move 1cm and then wait 100ms for next packet
|
||||
// * Only way for smooth movement is to use a fixed movement speed during
|
||||
// interpolation. interpolation over time is never that good.
|
||||
//
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
public abstract class NetworkTransformBase : NetworkBehaviour
|
||||
{
|
||||
[Header("Authority")]
|
||||
[Tooltip("Set to true if moves come from owner client, set to false if moves always come from server")]
|
||||
public bool clientAuthority;
|
||||
|
||||
/// <summary>
|
||||
/// We need to store this locally on the server so clients can't request Authority when ever they like
|
||||
/// </summary>
|
||||
bool clientAuthorityBeforeTeleport;
|
||||
|
||||
// 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.
|
||||
bool IsClientWithAuthority => hasAuthority && clientAuthority;
|
||||
|
||||
// Sensitivity is added for VR where human players tend to have micro movements so this can quiet down
|
||||
// the network traffic. Additionally, rigidbody drift should send less traffic, e.g very slow sliding / rolling.
|
||||
[Header("Sensitivity")]
|
||||
[Tooltip("Changes to the transform must exceed these values to be transmitted on the network.")]
|
||||
public float localPositionSensitivity = .01f;
|
||||
[Tooltip("If rotation exceeds this angle, it will be transmitted on the network")]
|
||||
public float localRotationSensitivity = .01f;
|
||||
[Tooltip("Changes to the transform must exceed these values to be transmitted on the network.")]
|
||||
public float localScaleSensitivity = .01f;
|
||||
|
||||
[Header("Compression")]
|
||||
[Tooltip("Enables smallest-three quaternion compression, which is lossy. Great for 3D, not great for 2D where minimal sprite rotations would look wobbly.")]
|
||||
public bool compressRotation; // disabled by default to not break 2D projects
|
||||
|
||||
[Header("Interpolation")]
|
||||
[Tooltip("Set to true if scale should be interpolated, false is ideal for instant sprite flipping.")]
|
||||
public bool interpolateScale = true;
|
||||
[Tooltip("Set to true if rotation should be interpolated, false is ideal for instant turning, common in retro 2d style games")]
|
||||
public bool interpolateRotation = true;
|
||||
[Tooltip("Set to true if position should be interpolated, false is ideal for grid bassed movement")]
|
||||
public bool interpolatePosition = true;
|
||||
|
||||
[Header("Synchronization")]
|
||||
// It should be very rare cases that people want to continuously sync scale, true by default to not break previous projects that use it
|
||||
// Users in most scenarios are best to send change of scale via cmd/rpc, syncvar/hooks, only once, and when required. Saves instant 12 bytes (25% of NT bandwidth!)
|
||||
[Tooltip("Set to false to not continuously send scale data, and save bandwidth.")]
|
||||
public bool syncScale = true;
|
||||
|
||||
// target transform to sync. can be on a child.
|
||||
protected abstract Transform targetComponent { get; }
|
||||
|
||||
// server
|
||||
Vector3 lastPosition;
|
||||
Quaternion lastRotation;
|
||||
Vector3 lastScale;
|
||||
|
||||
// client
|
||||
public class DataPoint
|
||||
{
|
||||
public float timeStamp;
|
||||
// use local position/rotation for VR support
|
||||
public Vector3 localPosition;
|
||||
public Quaternion localRotation;
|
||||
public Vector3 localScale;
|
||||
public float movementSpeed;
|
||||
}
|
||||
// interpolation start and goal
|
||||
DataPoint start;
|
||||
DataPoint goal;
|
||||
|
||||
// local authority send time
|
||||
float lastClientSendTime;
|
||||
|
||||
// serialization is needed by OnSerialize and by manual sending from authority
|
||||
// public only for tests
|
||||
public static void SerializeIntoWriter(NetworkWriter writer, Vector3 position, Quaternion rotation, Vector3 scale, bool compressRotation, bool syncScale)
|
||||
{
|
||||
// serialize position, rotation, scale
|
||||
// => compress rotation from 4*4=16 to 4 bytes
|
||||
// => less bandwidth = better CCU tests / scale
|
||||
writer.WriteVector3(position);
|
||||
if (compressRotation)
|
||||
{
|
||||
// smalles three compression for 3D
|
||||
writer.WriteUInt(Compression.CompressQuaternion(rotation));
|
||||
}
|
||||
else
|
||||
{
|
||||
// uncompressed for 2D
|
||||
writer.WriteQuaternion(rotation);
|
||||
}
|
||||
if (syncScale) { writer.WriteVector3(scale); }
|
||||
}
|
||||
|
||||
public override bool OnSerialize(NetworkWriter writer, bool initialState)
|
||||
{
|
||||
// use local position/rotation/scale for VR support
|
||||
SerializeIntoWriter(writer, targetComponent.localPosition, targetComponent.localRotation, targetComponent.localScale, compressRotation, syncScale);
|
||||
return true;
|
||||
}
|
||||
|
||||
// try to estimate movement speed for a data point based on how far it
|
||||
// moved since the previous one
|
||||
// => if this is the first time ever then we use our best guess:
|
||||
// -> delta based on transform.localPosition
|
||||
// -> elapsed based on send interval hoping that it roughly matches
|
||||
static float EstimateMovementSpeed(DataPoint from, DataPoint to, Transform transform, float sendInterval)
|
||||
{
|
||||
Vector3 delta = to.localPosition - (from != null ? from.localPosition : transform.localPosition);
|
||||
float elapsed = from != null ? to.timeStamp - from.timeStamp : sendInterval;
|
||||
// avoid NaN
|
||||
return elapsed > 0 ? delta.magnitude / elapsed : 0;
|
||||
}
|
||||
|
||||
// serialization is needed by OnSerialize and by manual sending from authority
|
||||
void DeserializeFromReader(NetworkReader reader)
|
||||
{
|
||||
// put it into a data point immediately
|
||||
DataPoint temp = new DataPoint
|
||||
{
|
||||
// deserialize position, rotation, scale
|
||||
// (rotation is optionally compressed)
|
||||
localPosition = reader.ReadVector3(),
|
||||
localRotation = compressRotation
|
||||
? Compression.DecompressQuaternion(reader.ReadUInt())
|
||||
: reader.ReadQuaternion(),
|
||||
// use current target scale, so we can check boolean and reader later, to see if the data is actually sent.
|
||||
localScale = targetComponent.localScale,
|
||||
timeStamp = Time.time
|
||||
};
|
||||
|
||||
if (syncScale)
|
||||
{
|
||||
// Reader length is checked here, 12 is used as thats the current Vector3 (3 floats) amount.
|
||||
// In rare cases people may do mis-matched builds, log useful warning message, and then do not process missing scale data.
|
||||
if (reader.Length >= 12) { temp.localScale = reader.ReadVector3(); }
|
||||
else { Debug.LogWarning("Reader length does not contain enough data for a scale, please check that both server and client builds syncScale booleans match.", this); }
|
||||
}
|
||||
|
||||
// movement speed: based on how far it moved since last time
|
||||
// has to be calculated before 'start' is overwritten
|
||||
temp.movementSpeed = EstimateMovementSpeed(goal, temp, targetComponent, syncInterval);
|
||||
|
||||
// reassign start wisely
|
||||
// -> first ever data point? then make something up for previous one
|
||||
// so that we can start interpolation without waiting for next.
|
||||
if (start == null)
|
||||
{
|
||||
start = new DataPoint
|
||||
{
|
||||
timeStamp = Time.time - syncInterval,
|
||||
// local position/rotation for VR support
|
||||
localPosition = targetComponent.localPosition,
|
||||
localRotation = targetComponent.localRotation,
|
||||
localScale = targetComponent.localScale,
|
||||
movementSpeed = temp.movementSpeed
|
||||
};
|
||||
}
|
||||
// -> second or nth data point? then update previous, but:
|
||||
// we start at where ever we are right now, so that it's
|
||||
// perfectly smooth and we don't jump anywhere
|
||||
//
|
||||
// example if we are at 'x':
|
||||
//
|
||||
// A--x->B
|
||||
//
|
||||
// and then receive a new point C:
|
||||
//
|
||||
// A--x--B
|
||||
// |
|
||||
// |
|
||||
// C
|
||||
//
|
||||
// then we don't want to just jump to B and start interpolation:
|
||||
//
|
||||
// x
|
||||
// |
|
||||
// |
|
||||
// C
|
||||
//
|
||||
// we stay at 'x' and interpolate from there to C:
|
||||
//
|
||||
// x..B
|
||||
// \ .
|
||||
// \.
|
||||
// C
|
||||
//
|
||||
else
|
||||
{
|
||||
float oldDistance = Vector3.Distance(start.localPosition, goal.localPosition);
|
||||
float newDistance = Vector3.Distance(goal.localPosition, temp.localPosition);
|
||||
|
||||
start = goal;
|
||||
|
||||
// teleport / lag / obstacle detection: only continue at current
|
||||
// position if we aren't too far away
|
||||
//
|
||||
// local position/rotation for VR support
|
||||
if (Vector3.Distance(targetComponent.localPosition, start.localPosition) < oldDistance + newDistance)
|
||||
{
|
||||
start.localPosition = targetComponent.localPosition;
|
||||
start.localRotation = targetComponent.localRotation;
|
||||
start.localScale = targetComponent.localScale;
|
||||
}
|
||||
}
|
||||
|
||||
// set new destination in any case. new data is best data.
|
||||
goal = temp;
|
||||
}
|
||||
|
||||
public override void OnDeserialize(NetworkReader reader, bool initialState)
|
||||
{
|
||||
// deserialize
|
||||
DeserializeFromReader(reader);
|
||||
}
|
||||
|
||||
// local authority client sends sync message to server for broadcasting
|
||||
[Command(channel = Channels.Unreliable)]
|
||||
void CmdClientToServerSync(ArraySegment<byte> payload)
|
||||
{
|
||||
// Ignore messages from client if not in client authority mode
|
||||
if (!clientAuthority)
|
||||
return;
|
||||
|
||||
// deserialize payload
|
||||
using (PooledNetworkReader networkReader = NetworkReaderPool.GetReader(payload))
|
||||
DeserializeFromReader(networkReader);
|
||||
|
||||
// server-only mode does no interpolation to save computations,
|
||||
// but let's set the position directly
|
||||
if (isServer && !isClient)
|
||||
ApplyPositionRotationScale(goal.localPosition, goal.localRotation, goal.localScale);
|
||||
|
||||
// set dirty so that OnSerialize broadcasts it
|
||||
SetDirtyBit(1UL);
|
||||
}
|
||||
|
||||
// where are we in the timeline between start and goal? [0,1]
|
||||
static float CurrentInterpolationFactor(DataPoint start, DataPoint goal)
|
||||
{
|
||||
if (start != null)
|
||||
{
|
||||
float difference = goal.timeStamp - start.timeStamp;
|
||||
|
||||
// the moment we get 'goal', 'start' is supposed to
|
||||
// start, so elapsed time is based on:
|
||||
float elapsed = Time.time - goal.timeStamp;
|
||||
// avoid NaN
|
||||
return difference > 0 ? elapsed / difference : 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
Vector3 InterpolatePosition(DataPoint start, DataPoint goal, Vector3 currentPosition)
|
||||
{
|
||||
if (!interpolatePosition)
|
||||
{
|
||||
return goal.localPosition;
|
||||
}
|
||||
else if (start != null)
|
||||
{
|
||||
// Option 1: simply interpolate based on time. but stutter
|
||||
// will happen, it's not that smooth. especially noticeable if
|
||||
// the camera automatically follows the player
|
||||
// float t = CurrentInterpolationFactor();
|
||||
// return Vector3.Lerp(start.position, goal.position, t);
|
||||
|
||||
// Option 2: always += speed
|
||||
// -> speed is 0 if we just started after idle, so always use max
|
||||
// for best results
|
||||
float speed = Mathf.Max(start.movementSpeed, goal.movementSpeed);
|
||||
return Vector3.MoveTowards(currentPosition, goal.localPosition, speed * Time.deltaTime);
|
||||
}
|
||||
return currentPosition;
|
||||
}
|
||||
|
||||
Quaternion InterpolateRotation(DataPoint start, DataPoint goal, Quaternion defaultRotation)
|
||||
{
|
||||
if (!interpolateRotation)
|
||||
{
|
||||
return goal.localRotation;
|
||||
}
|
||||
else if (start != null)
|
||||
{
|
||||
float t = CurrentInterpolationFactor(start, goal);
|
||||
return Quaternion.Slerp(start.localRotation, goal.localRotation, t);
|
||||
}
|
||||
return defaultRotation;
|
||||
}
|
||||
|
||||
Vector3 InterpolateScale(DataPoint start, DataPoint goal, Vector3 currentScale)
|
||||
{
|
||||
if (!syncScale)
|
||||
{
|
||||
return currentScale;
|
||||
}
|
||||
else if (!interpolateScale)
|
||||
{
|
||||
return goal.localScale;
|
||||
}
|
||||
else if (interpolateScale && start != null )
|
||||
{
|
||||
float t = CurrentInterpolationFactor(start, goal);
|
||||
return Vector3.Lerp(start.localScale, goal.localScale, t);
|
||||
}
|
||||
else
|
||||
{
|
||||
return currentScale;
|
||||
}
|
||||
}
|
||||
|
||||
// teleport / lag / stuck detection
|
||||
// -> checking distance is not enough since there could be just a tiny
|
||||
// fence between us and the goal
|
||||
// -> checking time always works, this way we just teleport if we still
|
||||
// didn't reach the goal after too much time has elapsed
|
||||
bool NeedsTeleport()
|
||||
{
|
||||
// calculate time between the two data points
|
||||
float startTime = start != null ? start.timeStamp : Time.time - syncInterval;
|
||||
float goalTime = goal != null ? goal.timeStamp : Time.time;
|
||||
float difference = goalTime - startTime;
|
||||
float timeSinceGoalReceived = Time.time - goalTime;
|
||||
return timeSinceGoalReceived > difference * 5;
|
||||
}
|
||||
|
||||
// moved since last time we checked it?
|
||||
bool HasEitherMovedRotatedScaled()
|
||||
{
|
||||
// moved or rotated or scaled?
|
||||
// local position/rotation/scale for VR support
|
||||
bool moved = Vector3.Distance(lastPosition, targetComponent.localPosition) > localPositionSensitivity;
|
||||
bool scaled = Vector3.Distance(lastScale, targetComponent.localScale) > localScaleSensitivity;
|
||||
bool rotated = Quaternion.Angle(lastRotation, targetComponent.localRotation) > localRotationSensitivity;
|
||||
|
||||
// save last for next frame to compare
|
||||
// (only if change was detected. otherwise slow moving objects might
|
||||
// never sync because of C#'s float comparison tolerance. see also:
|
||||
// https://github.com/vis2k/Mirror/pull/428)
|
||||
bool change = moved || rotated || scaled;
|
||||
if (change)
|
||||
{
|
||||
// local position/rotation for VR support
|
||||
lastPosition = targetComponent.localPosition;
|
||||
lastRotation = targetComponent.localRotation;
|
||||
lastScale = targetComponent.localScale;
|
||||
}
|
||||
return change;
|
||||
}
|
||||
|
||||
// set position carefully depending on the target component
|
||||
void ApplyPositionRotationScale(Vector3 position, Quaternion rotation, Vector3 scale)
|
||||
{
|
||||
// local position/rotation for VR support
|
||||
targetComponent.localPosition = position;
|
||||
targetComponent.localRotation = rotation;
|
||||
targetComponent.localScale = scale;
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
// if server then always sync to others.
|
||||
if (isServer)
|
||||
{
|
||||
// just use OnSerialize via SetDirtyBit only sync when position
|
||||
// changed. set dirty bits 0 or 1
|
||||
SetDirtyBit(HasEitherMovedRotatedScaled() ? 1UL : 0UL);
|
||||
}
|
||||
|
||||
// no 'else if' since host mode would be both
|
||||
if (isClient)
|
||||
{
|
||||
// send to server if we have local authority (and aren't the server)
|
||||
// -> only if connectionToServer has been initialized yet too
|
||||
if (!isServer && IsClientWithAuthority)
|
||||
{
|
||||
// check only each 'syncInterval'
|
||||
if (Time.time - lastClientSendTime >= syncInterval)
|
||||
{
|
||||
if (HasEitherMovedRotatedScaled())
|
||||
{
|
||||
// serialize
|
||||
// local position/rotation for VR support
|
||||
using (PooledNetworkWriter writer = NetworkWriterPool.GetWriter())
|
||||
{
|
||||
SerializeIntoWriter(writer, targetComponent.localPosition, targetComponent.localRotation, targetComponent.localScale, compressRotation, syncScale);
|
||||
|
||||
// send to server
|
||||
CmdClientToServerSync(writer.ToArraySegment());
|
||||
}
|
||||
}
|
||||
lastClientSendTime = Time.time;
|
||||
}
|
||||
}
|
||||
|
||||
// apply interpolation on client for all players
|
||||
// unless this client has authority over the object. could be
|
||||
// himself or another object that he was assigned authority over
|
||||
if (!IsClientWithAuthority)
|
||||
{
|
||||
// received one yet? (initialized?)
|
||||
if (goal != null)
|
||||
{
|
||||
// teleport or interpolate
|
||||
if (NeedsTeleport())
|
||||
{
|
||||
// local position/rotation for VR support
|
||||
ApplyPositionRotationScale(goal.localPosition, goal.localRotation, goal.localScale);
|
||||
|
||||
// reset data points so we don't keep interpolating
|
||||
start = null;
|
||||
goal = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
// local position/rotation for VR support
|
||||
ApplyPositionRotationScale(InterpolatePosition(start, goal, targetComponent.localPosition),
|
||||
InterpolateRotation(start, goal, targetComponent.localRotation),
|
||||
InterpolateScale(start, goal, targetComponent.localScale));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#region Server Teleport (force move player)
|
||||
/// <summary>
|
||||
/// Server side teleportation.
|
||||
/// This method will override this GameObject's current Transform.Position to the Vector3 you have provided
|
||||
/// and send it to all other Clients to override it at their side too.
|
||||
/// </summary>
|
||||
/// <param name="position">Where to teleport this GameObject</param>
|
||||
[Server]
|
||||
public void ServerTeleport(Vector3 position)
|
||||
{
|
||||
Quaternion rotation = transform.rotation;
|
||||
ServerTeleport(position, rotation);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Server side teleportation.
|
||||
/// This method will override this GameObject's current Transform.Position and Transform.Rotation
|
||||
/// to the Vector3 you have provided
|
||||
/// and send it to all other Clients to override it at their side too.
|
||||
/// </summary>
|
||||
/// <param name="position">Where to teleport this GameObject</param>
|
||||
/// <param name="rotation">Which rotation to set this GameObject</param>
|
||||
[Server]
|
||||
public void ServerTeleport(Vector3 position, Quaternion rotation)
|
||||
{
|
||||
// To prevent applying the position updates received from client (if they have ClientAuth) while being teleported.
|
||||
|
||||
// clientAuthorityBeforeTeleport defaults to false when not teleporting, if it is true then it means that teleport was previously called but not finished
|
||||
// therefore we should keep it as true so that 2nd teleport call doesn't clear authority
|
||||
clientAuthorityBeforeTeleport = clientAuthority || clientAuthorityBeforeTeleport;
|
||||
clientAuthority = false;
|
||||
|
||||
DoTeleport(position, rotation);
|
||||
|
||||
// tell all clients about new values
|
||||
RpcTeleport(position, rotation, clientAuthorityBeforeTeleport);
|
||||
}
|
||||
|
||||
void DoTeleport(Vector3 newPosition, Quaternion newRotation)
|
||||
{
|
||||
transform.position = newPosition;
|
||||
transform.rotation = newRotation;
|
||||
|
||||
// Since we are overriding the position we don't need a goal and start.
|
||||
// Reset them to null for fresh start
|
||||
goal = null;
|
||||
start = null;
|
||||
lastPosition = newPosition;
|
||||
lastRotation = newRotation;
|
||||
}
|
||||
|
||||
[ClientRpc]
|
||||
void RpcTeleport(Vector3 newPosition, Quaternion newRotation, bool isClientAuthority)
|
||||
{
|
||||
DoTeleport(newPosition, newRotation);
|
||||
|
||||
// only send finished if is owner and is ClientAuthority on server
|
||||
if (hasAuthority && isClientAuthority)
|
||||
CmdTeleportFinished();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This RPC will be invoked on server after client finishes overriding the position.
|
||||
/// </summary>
|
||||
/// <param name="initialAuthority"></param>
|
||||
[Command]
|
||||
void CmdTeleportFinished()
|
||||
{
|
||||
if (clientAuthorityBeforeTeleport)
|
||||
{
|
||||
clientAuthority = true;
|
||||
|
||||
// reset value so doesn't effect future calls, see note in ServerTeleport
|
||||
clientAuthorityBeforeTeleport = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning("Client called TeleportFinished when clientAuthority was false on server", this);
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
static void DrawDataPointGizmo(DataPoint data, Color color)
|
||||
{
|
||||
// use a little offset because transform.localPosition might be in
|
||||
// the ground in many cases
|
||||
Vector3 offset = Vector3.up * 0.01f;
|
||||
|
||||
// draw position
|
||||
Gizmos.color = color;
|
||||
Gizmos.DrawSphere(data.localPosition + offset, 0.5f);
|
||||
|
||||
// draw forward and up
|
||||
// like unity move tool
|
||||
Gizmos.color = Color.blue;
|
||||
Gizmos.DrawRay(data.localPosition + offset, data.localRotation * Vector3.forward);
|
||||
|
||||
// like unity move tool
|
||||
Gizmos.color = Color.green;
|
||||
Gizmos.DrawRay(data.localPosition + offset, data.localRotation * Vector3.up);
|
||||
}
|
||||
|
||||
static void DrawLineBetweenDataPoints(DataPoint data1, DataPoint data2, Color color)
|
||||
{
|
||||
Gizmos.color = color;
|
||||
Gizmos.DrawLine(data1.localPosition, data2.localPosition);
|
||||
}
|
||||
|
||||
// draw the data points for easier debugging
|
||||
void OnDrawGizmos()
|
||||
{
|
||||
// draw start and goal points
|
||||
if (start != null) DrawDataPointGizmo(start, Color.gray);
|
||||
if (goal != null) DrawDataPointGizmo(goal, Color.white);
|
||||
|
||||
// draw line between them
|
||||
if (start != null && goal != null) DrawLineBetweenDataPoints(start, goal, Color.cyan);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
/// <summary>
|
||||
/// A component to synchronize the position of child transforms of networked objects.
|
||||
/// <para>There must be a NetworkTransform on the root object of the hierarchy. There can be multiple NetworkTransformChild components on an object. This does not use physics for synchronization, it simply synchronizes the localPosition and localRotation of the child transform and lerps towards the received values.</para>
|
||||
/// </summary>
|
||||
[AddComponentMenu("Network/NetworkTransformChild")]
|
||||
[HelpURL("https://mirror-networking.gitbook.io/docs/components/network-transform-child")]
|
||||
public class NetworkTransformChild : NetworkTransformBase
|
||||
{
|
||||
[Header("Target")]
|
||||
public Transform target;
|
||||
|
||||
protected override Transform targetComponent => target;
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,6 @@ guid: 2539267b6934a4026a505690a1e1eda2
|
|||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData: ''
|
||||
assetBundleName: ''
|
||||
assetBundleVariant: ''
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
|
|||
113
UnityProject/Assets/Mirror/Editor/AndroidManifestHelper.cs
Normal file
113
UnityProject/Assets/Mirror/Editor/AndroidManifestHelper.cs
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
// Android NetworkDiscovery Multicast fix
|
||||
// https://github.com/vis2k/Mirror/pull/2887
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using UnityEditor.Build;
|
||||
using UnityEditor.Build.Reporting;
|
||||
using System.Xml;
|
||||
using System.IO;
|
||||
#if UNITY_ANDROID
|
||||
using UnityEditor.Android;
|
||||
#endif
|
||||
|
||||
|
||||
[InitializeOnLoad]
|
||||
public class AndroidManifestHelper : IPreprocessBuildWithReport, IPostprocessBuildWithReport
|
||||
#if UNITY_ANDROID
|
||||
, IPostGenerateGradleAndroidProject
|
||||
#endif
|
||||
{
|
||||
public int callbackOrder { get { return 99999; } }
|
||||
|
||||
#if UNITY_ANDROID
|
||||
public void OnPostGenerateGradleAndroidProject(string path)
|
||||
{
|
||||
string manifestFolder = Path.Combine(path, "src/main");
|
||||
string sourceFile = manifestFolder + "/AndroidManifest.xml";
|
||||
// Load android manifest file
|
||||
XmlDocument doc = new XmlDocument();
|
||||
doc.Load(sourceFile);
|
||||
|
||||
string androidNamepsaceURI;
|
||||
XmlElement element = (XmlElement)doc.SelectSingleNode("/manifest");
|
||||
if (element == null)
|
||||
{
|
||||
UnityEngine.Debug.LogError("Could not find manifest tag in android manifest.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Get android namespace URI from the manifest
|
||||
androidNamepsaceURI = element.GetAttribute("xmlns:android");
|
||||
if (string.IsNullOrEmpty(androidNamepsaceURI))
|
||||
{
|
||||
UnityEngine.Debug.LogError("Could not find Android Namespace in manifest.");
|
||||
return;
|
||||
}
|
||||
AddOrRemoveTag(doc,
|
||||
androidNamepsaceURI,
|
||||
"/manifest",
|
||||
"uses-permission",
|
||||
"android.permission.CHANGE_WIFI_MULTICAST_STATE",
|
||||
true,
|
||||
false);
|
||||
AddOrRemoveTag(doc,
|
||||
androidNamepsaceURI,
|
||||
"/manifest",
|
||||
"uses-permission",
|
||||
"android.permission.INTERNET",
|
||||
true,
|
||||
false);
|
||||
doc.Save(sourceFile);
|
||||
}
|
||||
#endif
|
||||
|
||||
static void AddOrRemoveTag(XmlDocument doc, string @namespace, string path, string elementName, string name, bool required, bool modifyIfFound, params string[] attrs) // name, value pairs
|
||||
{
|
||||
var nodes = doc.SelectNodes(path + "/" + elementName);
|
||||
XmlElement element = null;
|
||||
foreach (XmlElement e in nodes)
|
||||
{
|
||||
if (name == null || name == e.GetAttribute("name", @namespace))
|
||||
{
|
||||
element = e;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (required)
|
||||
{
|
||||
if (element == null)
|
||||
{
|
||||
var parent = doc.SelectSingleNode(path);
|
||||
element = doc.CreateElement(elementName);
|
||||
element.SetAttribute("name", @namespace, name);
|
||||
parent.AppendChild(element);
|
||||
}
|
||||
|
||||
for (int i = 0; i < attrs.Length; i += 2)
|
||||
{
|
||||
if (modifyIfFound || string.IsNullOrEmpty(element.GetAttribute(attrs[i], @namespace)))
|
||||
{
|
||||
if (attrs[i + 1] != null)
|
||||
{
|
||||
element.SetAttribute(attrs[i], @namespace, attrs[i + 1]);
|
||||
}
|
||||
else
|
||||
{
|
||||
element.RemoveAttribute(attrs[i], @namespace);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (element != null && modifyIfFound)
|
||||
{
|
||||
element.ParentNode.RemoveChild(element);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void OnPostprocessBuild(BuildReport report) {}
|
||||
public void OnPreprocessBuild(BuildReport report) {}
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
fileFormatVersion: 2
|
||||
guid: b7b9e2c091c3d42439840a02fe700252
|
||||
guid: 80cc70189403d7444bbffd185ca28462
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
|
|
@ -10,8 +10,8 @@ namespace Mirror
|
|||
{
|
||||
string typeName = typeof(T).Name;
|
||||
|
||||
string[] guidsFound = AssetDatabase.FindAssets($"t:Script " + typeName);
|
||||
if (guidsFound.Length >= 1 && !string.IsNullOrEmpty(guidsFound[0]))
|
||||
string[] guidsFound = AssetDatabase.FindAssets($"t:Script {typeName}");
|
||||
if (guidsFound.Length >= 1 && !string.IsNullOrWhiteSpace(guidsFound[0]))
|
||||
{
|
||||
if (guidsFound.Length > 1)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -6,6 +6,6 @@ MonoImporter:
|
|||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData: ''
|
||||
assetBundleName: ''
|
||||
assetBundleVariant: ''
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@ guid: 62c8dc5bb12bbc6428bb66ccbac57000
|
|||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData: ''
|
||||
assetBundleName: ''
|
||||
assetBundleVariant: ''
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
// removed 2021-12-12
|
||||
|
|
@ -6,6 +6,6 @@ MonoImporter:
|
|||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData: ''
|
||||
assetBundleName: ''
|
||||
assetBundleVariant: ''
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -6,6 +6,6 @@ MonoImporter:
|
|||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData: ''
|
||||
assetBundleName: ''
|
||||
assetBundleVariant: ''
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@ guid: 4d97731cd74ac8b4b8aad808548ef9cd
|
|||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData: ''
|
||||
assetBundleName: ''
|
||||
assetBundleVariant: ''
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
|
|||
|
|
@ -6,6 +6,6 @@ MonoImporter:
|
|||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData: ''
|
||||
assetBundleName: ''
|
||||
assetBundleVariant: ''
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue