mirror of
https://github.com/smyalygames/monopoly.git
synced 2025-05-17 22:04:11 +02:00
Added multiplayer plugin
This commit is contained in:
parent
9cb342dd42
commit
f64cf54803
1
.idea/.idea.Monopoly/.idea/indexLayout.xml
generated
1
.idea/.idea.Monopoly/.idea/indexLayout.xml
generated
@ -20,7 +20,6 @@
|
|||||||
<Path>.vs</Path>
|
<Path>.vs</Path>
|
||||||
<Path>Library</Path>
|
<Path>Library</Path>
|
||||||
<Path>Logs</Path>
|
<Path>Logs</Path>
|
||||||
<Path>Temp</Path>
|
|
||||||
<Path>UserSettings</Path>
|
<Path>UserSettings</Path>
|
||||||
<Path>obj</Path>
|
<Path>obj</Path>
|
||||||
</explicitExcludes>
|
</explicitExcludes>
|
||||||
|
8
Assets/Mirror.meta
Normal file
8
Assets/Mirror.meta
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 5cf8eb36be0834b3da408c694a41cb88
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
8
Assets/Mirror/Authenticators.meta
Normal file
8
Assets/Mirror/Authenticators.meta
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 1b2f9d254154cd942ba40b06b869b8f3
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
161
Assets/Mirror/Authenticators/BasicAuthenticator.cs
Normal file
161
Assets/Mirror/Authenticators/BasicAuthenticator.cs
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
using System.Collections;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace Mirror.Authenticators
|
||||||
|
{
|
||||||
|
[AddComponentMenu("Network/Authenticators/BasicAuthenticator")]
|
||||||
|
public class BasicAuthenticator : NetworkAuthenticator
|
||||||
|
{
|
||||||
|
static readonly ILogger logger = LogFactory.GetLogger(typeof(BasicAuthenticator));
|
||||||
|
|
||||||
|
[Header("Custom Properties")]
|
||||||
|
|
||||||
|
// set these in the inspector
|
||||||
|
public string username;
|
||||||
|
public string password;
|
||||||
|
|
||||||
|
#region Messages
|
||||||
|
|
||||||
|
public struct AuthRequestMessage : NetworkMessage
|
||||||
|
{
|
||||||
|
// use whatever credentials make sense for your game
|
||||||
|
// for example, you might want to pass the accessToken if using oauth
|
||||||
|
public string authUsername;
|
||||||
|
public string authPassword;
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct AuthResponseMessage : NetworkMessage
|
||||||
|
{
|
||||||
|
public byte code;
|
||||||
|
public string message;
|
||||||
|
}
|
||||||
|
|
||||||
|
#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 OnServerAuthenticateInternal when a client needs to authenticate
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="conn">Connection to client.</param>
|
||||||
|
public override void OnServerAuthenticate(NetworkConnection conn)
|
||||||
|
{
|
||||||
|
// do nothing...wait for AuthRequestMessage from client
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called on server when the client's AuthRequestMessage arrives
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="conn">Connection to client.</param>
|
||||||
|
/// <param name="msg">The message payload</param>
|
||||||
|
public void OnAuthRequestMessage(NetworkConnection conn, AuthRequestMessage msg)
|
||||||
|
{
|
||||||
|
if (logger.LogEnabled()) logger.LogFormat(LogType.Log, "Authentication Request: {0} {1}", msg.authUsername, msg.authPassword);
|
||||||
|
|
||||||
|
// check the credentials by calling your web server, database table, playfab api, or any method appropriate.
|
||||||
|
if (msg.authUsername == username && msg.authPassword == password)
|
||||||
|
{
|
||||||
|
// create and send msg to client so it knows to proceed
|
||||||
|
AuthResponseMessage authResponseMessage = new AuthResponseMessage
|
||||||
|
{
|
||||||
|
code = 100,
|
||||||
|
message = "Success"
|
||||||
|
};
|
||||||
|
|
||||||
|
conn.Send(authResponseMessage);
|
||||||
|
|
||||||
|
// Accept the successful authentication
|
||||||
|
ServerAccept(conn);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// create and send msg to client so it knows to disconnect
|
||||||
|
AuthResponseMessage authResponseMessage = new AuthResponseMessage
|
||||||
|
{
|
||||||
|
code = 200,
|
||||||
|
message = "Invalid Credentials"
|
||||||
|
};
|
||||||
|
|
||||||
|
conn.Send(authResponseMessage);
|
||||||
|
|
||||||
|
// must set NetworkConnection isAuthenticated = false
|
||||||
|
conn.isAuthenticated = false;
|
||||||
|
|
||||||
|
// disconnect the client after 1 second so that response message gets delivered
|
||||||
|
StartCoroutine(DelayedDisconnect(conn, 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IEnumerator DelayedDisconnect(NetworkConnection conn, float waitTime)
|
||||||
|
{
|
||||||
|
yield return new WaitForSeconds(waitTime);
|
||||||
|
|
||||||
|
// Reject the unsuccessful authentication
|
||||||
|
ServerReject(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 OnClientAuthenticateInternal when a client needs to authenticate
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="conn">Connection of the client.</param>
|
||||||
|
public override void OnClientAuthenticate(NetworkConnection conn)
|
||||||
|
{
|
||||||
|
AuthRequestMessage authRequestMessage = new AuthRequestMessage
|
||||||
|
{
|
||||||
|
authUsername = username,
|
||||||
|
authPassword = password
|
||||||
|
};
|
||||||
|
|
||||||
|
conn.Send(authRequestMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called on client when the server's AuthResponseMessage arrives
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="conn">Connection to client.</param>
|
||||||
|
/// <param name="msg">The message payload</param>
|
||||||
|
public void OnAuthResponseMessage(NetworkConnection conn, AuthResponseMessage msg)
|
||||||
|
{
|
||||||
|
if (msg.code == 100)
|
||||||
|
{
|
||||||
|
if (logger.LogEnabled()) logger.LogFormat(LogType.Log, "Authentication Response: {0}", msg.message);
|
||||||
|
|
||||||
|
// Authentication has been accepted
|
||||||
|
ClientAccept(conn);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
logger.LogFormat(LogType.Error, "Authentication Response: {0}", msg.message);
|
||||||
|
|
||||||
|
// Authentication has been rejected
|
||||||
|
ClientReject(conn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
}
|
11
Assets/Mirror/Authenticators/BasicAuthenticator.cs.meta
Normal file
11
Assets/Mirror/Authenticators/BasicAuthenticator.cs.meta
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 28496b776660156428f00cf78289c1ec
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
14
Assets/Mirror/Authenticators/Mirror.Authenticators.asmdef
Normal file
14
Assets/Mirror/Authenticators/Mirror.Authenticators.asmdef
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"name": "Mirror.Authenticators",
|
||||||
|
"references": [
|
||||||
|
"Mirror"
|
||||||
|
],
|
||||||
|
"optionalUnityReferences": [],
|
||||||
|
"includePlatforms": [],
|
||||||
|
"excludePlatforms": [],
|
||||||
|
"allowUnsafeCode": false,
|
||||||
|
"overrideReferences": false,
|
||||||
|
"precompiledReferences": [],
|
||||||
|
"autoReferenced": true,
|
||||||
|
"defineConstraints": []
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: e720aa64e3f58fb4880566a322584340
|
||||||
|
AssemblyDefinitionImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
64
Assets/Mirror/Authenticators/TimeoutAuthenticator.cs
Normal file
64
Assets/Mirror/Authenticators/TimeoutAuthenticator.cs
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
using System.Collections;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace Mirror.Authenticators
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// An authenticator that disconnects connections if they don't
|
||||||
|
/// authenticate within a specified time limit.
|
||||||
|
/// </summary>
|
||||||
|
[AddComponentMenu("Network/Authenticators/TimeoutAuthenticator")]
|
||||||
|
public class TimeoutAuthenticator : NetworkAuthenticator
|
||||||
|
{
|
||||||
|
static readonly ILogger logger = LogFactory.GetLogger(typeof(TimeoutAuthenticator));
|
||||||
|
|
||||||
|
public NetworkAuthenticator authenticator;
|
||||||
|
|
||||||
|
[Range(0, 600), Tooltip("Timeout to auto-disconnect in seconds. Set to 0 for no timeout.")]
|
||||||
|
public float timeout = 60;
|
||||||
|
|
||||||
|
public void Awake()
|
||||||
|
{
|
||||||
|
authenticator.OnClientAuthenticated.AddListener(connection => OnClientAuthenticated.Invoke(connection));
|
||||||
|
authenticator.OnServerAuthenticated.AddListener(connection => OnServerAuthenticated.Invoke(connection));
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void OnStartClient()
|
||||||
|
{
|
||||||
|
authenticator.OnStartClient();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void OnStartServer()
|
||||||
|
{
|
||||||
|
authenticator.OnStartServer();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void OnClientAuthenticate(NetworkConnection conn)
|
||||||
|
{
|
||||||
|
authenticator.OnClientAuthenticate(conn);
|
||||||
|
if (timeout > 0)
|
||||||
|
StartCoroutine(BeginAuthentication(conn));
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void OnServerAuthenticate(NetworkConnection conn)
|
||||||
|
{
|
||||||
|
authenticator.OnServerAuthenticate(conn);
|
||||||
|
if (timeout > 0)
|
||||||
|
StartCoroutine(BeginAuthentication(conn));
|
||||||
|
}
|
||||||
|
|
||||||
|
IEnumerator BeginAuthentication(NetworkConnection conn)
|
||||||
|
{
|
||||||
|
if (logger.LogEnabled()) logger.Log($"Authentication countdown started {conn} {timeout}");
|
||||||
|
|
||||||
|
yield return new WaitForSecondsRealtime(timeout);
|
||||||
|
|
||||||
|
if (!conn.isAuthenticated)
|
||||||
|
{
|
||||||
|
if (logger.LogEnabled()) logger.Log($"Authentication Timeout {conn}");
|
||||||
|
|
||||||
|
conn.Disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
11
Assets/Mirror/Authenticators/TimeoutAuthenticator.cs.meta
Normal file
11
Assets/Mirror/Authenticators/TimeoutAuthenticator.cs.meta
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 24d8269a07b8e4edfa374753a91c946e
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
8
Assets/Mirror/Cloud.meta
Normal file
8
Assets/Mirror/Cloud.meta
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 73a9bb2dacafa8141bce8feef34e33a7
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
61
Assets/Mirror/Cloud/ApiConnector.cs
Normal file
61
Assets/Mirror/Cloud/ApiConnector.cs
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
using Mirror.Cloud.ListServerService;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace Mirror.Cloud
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Used to requests and responses from the mirror api
|
||||||
|
/// </summary>
|
||||||
|
public interface IApiConnector
|
||||||
|
{
|
||||||
|
ListServer ListServer { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Used to requests and responses from the mirror api
|
||||||
|
/// </summary>
|
||||||
|
[DisallowMultipleComponent]
|
||||||
|
[AddComponentMenu("Network/CloudServices/ApiConnector")]
|
||||||
|
[HelpURL("https://mirror-networking.com/docs/CloudServices/ApiConnector.html")]
|
||||||
|
public class ApiConnector : MonoBehaviour, IApiConnector, ICoroutineRunner
|
||||||
|
{
|
||||||
|
#region Inspector
|
||||||
|
[Header("Settings")]
|
||||||
|
|
||||||
|
[Tooltip("Base URL of api, including https")]
|
||||||
|
[SerializeField] string ApiAddress = "";
|
||||||
|
|
||||||
|
[Tooltip("Api key required to access api")]
|
||||||
|
[SerializeField] string ApiKey = "";
|
||||||
|
|
||||||
|
[Header("Events")]
|
||||||
|
|
||||||
|
[Tooltip("Triggered when server list updates")]
|
||||||
|
[SerializeField] ServerListEvent _onServerListUpdated = new ServerListEvent();
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
IRequestCreator requestCreator;
|
||||||
|
|
||||||
|
public ListServer ListServer { get; private set; }
|
||||||
|
|
||||||
|
void Awake()
|
||||||
|
{
|
||||||
|
requestCreator = new RequestCreator(ApiAddress, ApiKey, this);
|
||||||
|
|
||||||
|
InitListServer();
|
||||||
|
}
|
||||||
|
|
||||||
|
void InitListServer()
|
||||||
|
{
|
||||||
|
IListServerServerApi serverApi = new ListServerServerApi(this, requestCreator);
|
||||||
|
IListServerClientApi clientApi = new ListServerClientApi(this, requestCreator, _onServerListUpdated);
|
||||||
|
ListServer = new ListServer(serverApi, clientApi);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnDestroy()
|
||||||
|
{
|
||||||
|
ListServer?.ServerApi.Shutdown();
|
||||||
|
ListServer?.ClientApi.Shutdown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
11
Assets/Mirror/Cloud/ApiConnector.cs.meta
Normal file
11
Assets/Mirror/Cloud/ApiConnector.cs.meta
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 8bdb99a29e179d14cb0acc43f175d9ad
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
8
Assets/Mirror/Cloud/Core.meta
Normal file
8
Assets/Mirror/Cloud/Core.meta
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 3f34c32971e65984c93a15376ec11c65
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
25
Assets/Mirror/Cloud/Core/BaseApi.cs
Normal file
25
Assets/Mirror/Cloud/Core/BaseApi.cs
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Mirror.Cloud
|
||||||
|
{
|
||||||
|
public interface IBaseApi
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Cleans up any data created by the instance
|
||||||
|
/// <para>For Example: removing server from list</para>
|
||||||
|
/// </summary>
|
||||||
|
void Shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract class BaseApi
|
||||||
|
{
|
||||||
|
protected readonly ICoroutineRunner runner;
|
||||||
|
protected readonly IRequestCreator requestCreator;
|
||||||
|
|
||||||
|
protected BaseApi(ICoroutineRunner runner, IRequestCreator requestCreator)
|
||||||
|
{
|
||||||
|
this.runner = runner ?? throw new ArgumentNullException(nameof(runner));
|
||||||
|
this.requestCreator = requestCreator ?? throw new ArgumentNullException(nameof(requestCreator));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
11
Assets/Mirror/Cloud/Core/BaseApi.cs.meta
Normal file
11
Assets/Mirror/Cloud/Core/BaseApi.cs.meta
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 70f563b7a7210ae43bbcde5cb7721a94
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
12
Assets/Mirror/Cloud/Core/Events.cs
Normal file
12
Assets/Mirror/Cloud/Core/Events.cs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
using System;
|
||||||
|
using Mirror.Cloud.ListServerService;
|
||||||
|
using UnityEngine.Events;
|
||||||
|
|
||||||
|
namespace Mirror.Cloud
|
||||||
|
{
|
||||||
|
[Serializable]
|
||||||
|
public class ServerListEvent : UnityEvent<ServerCollectionJson> { }
|
||||||
|
|
||||||
|
[Serializable]
|
||||||
|
public class MatchFoundEvent : UnityEvent<ServerJson> { }
|
||||||
|
}
|
11
Assets/Mirror/Cloud/Core/Events.cs.meta
Normal file
11
Assets/Mirror/Cloud/Core/Events.cs.meta
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: c7c472a3ea1bc4348bd5a0b05bf7cc3b
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
12
Assets/Mirror/Cloud/Core/Extensions.cs
Normal file
12
Assets/Mirror/Cloud/Core/Extensions.cs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
using UnityEngine.Networking;
|
||||||
|
|
||||||
|
namespace Mirror.Cloud
|
||||||
|
{
|
||||||
|
public static class Extensions
|
||||||
|
{
|
||||||
|
public static bool IsOk(this UnityWebRequest webRequest)
|
||||||
|
{
|
||||||
|
return 200 <= webRequest.responseCode && webRequest.responseCode <= 299;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
11
Assets/Mirror/Cloud/Core/Extensions.cs.meta
Normal file
11
Assets/Mirror/Cloud/Core/Extensions.cs.meta
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 97501e783fc67a4459b15d10e6c63563
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
12
Assets/Mirror/Cloud/Core/ICoroutineRunner.cs
Normal file
12
Assets/Mirror/Cloud/Core/ICoroutineRunner.cs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
using System.Collections;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace Mirror.Cloud
|
||||||
|
{
|
||||||
|
public interface ICoroutineRunner : IUnityEqualCheck
|
||||||
|
{
|
||||||
|
Coroutine StartCoroutine(IEnumerator routine);
|
||||||
|
void StopCoroutine(IEnumerator routine);
|
||||||
|
void StopCoroutine(Coroutine routine);
|
||||||
|
}
|
||||||
|
}
|
11
Assets/Mirror/Cloud/Core/ICoroutineRunner.cs.meta
Normal file
11
Assets/Mirror/Cloud/Core/ICoroutineRunner.cs.meta
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 43472c60a7c72e54eafe559290dd0fc6
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
42
Assets/Mirror/Cloud/Core/IRequestCreator.cs
Normal file
42
Assets/Mirror/Cloud/Core/IRequestCreator.cs
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
using System.Collections;
|
||||||
|
using UnityEngine.Networking;
|
||||||
|
|
||||||
|
namespace Mirror.Cloud
|
||||||
|
{
|
||||||
|
public delegate void RequestSuccess(string responseBody);
|
||||||
|
|
||||||
|
public delegate void RequestFail(string responseBody);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Objects that can be sent to the Api must have this interface
|
||||||
|
/// </summary>
|
||||||
|
public interface ICanBeJson { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Methods to create and send UnityWebRequest
|
||||||
|
/// </summary>
|
||||||
|
public interface IRequestCreator
|
||||||
|
{
|
||||||
|
UnityWebRequest Delete(string page);
|
||||||
|
UnityWebRequest Get(string page);
|
||||||
|
UnityWebRequest Patch<T>(string page, T json) where T : struct, ICanBeJson;
|
||||||
|
UnityWebRequest Post<T>(string page, T json) where T : struct, ICanBeJson;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sends Request to api and invokes callback when finished
|
||||||
|
/// <para>Starts Coroutine of SendRequestEnumerator</para>
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request"></param>
|
||||||
|
/// <param name="onSuccess"></param>
|
||||||
|
/// <param name="onFail"></param>
|
||||||
|
void SendRequest(UnityWebRequest request, RequestSuccess onSuccess = null, RequestFail onFail = null);
|
||||||
|
/// <summary>
|
||||||
|
/// Sends Request to api and invokes callback when finished
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request"></param>
|
||||||
|
/// <param name="onSuccess"></param>
|
||||||
|
/// <param name="onFail"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
IEnumerator SendRequestEnumerator(UnityWebRequest request, RequestSuccess onSuccess = null, RequestFail onFail = null);
|
||||||
|
}
|
||||||
|
}
|
11
Assets/Mirror/Cloud/Core/IRequestCreator.cs.meta
Normal file
11
Assets/Mirror/Cloud/Core/IRequestCreator.cs.meta
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: b80b95532a9d6e8418aa676a261e4f69
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
26
Assets/Mirror/Cloud/Core/IUnityEqualCheck.cs
Normal file
26
Assets/Mirror/Cloud/Core/IUnityEqualCheck.cs
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace Mirror.Cloud
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Adds Extension to check if unity object is null.
|
||||||
|
/// <para>Use these methods to stop MissingReferenceException</para>
|
||||||
|
/// </summary>
|
||||||
|
public interface IUnityEqualCheck
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class UnityEqualCheckExtension
|
||||||
|
{
|
||||||
|
public static bool IsNull(this IUnityEqualCheck obj)
|
||||||
|
{
|
||||||
|
return (obj as Object) == null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsNotNull(this IUnityEqualCheck obj)
|
||||||
|
{
|
||||||
|
return (obj as Object) != null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
11
Assets/Mirror/Cloud/Core/IUnityEqualCheck.cs.meta
Normal file
11
Assets/Mirror/Cloud/Core/IUnityEqualCheck.cs.meta
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 05185b973ba389a4588fc8a99c75a4f6
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
24
Assets/Mirror/Cloud/Core/JsonStructs.cs
Normal file
24
Assets/Mirror/Cloud/Core/JsonStructs.cs
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Mirror.Cloud
|
||||||
|
{
|
||||||
|
[Serializable]
|
||||||
|
public struct CreatedIdJson : ICanBeJson
|
||||||
|
{
|
||||||
|
public string id;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Serializable]
|
||||||
|
public struct ErrorJson : ICanBeJson
|
||||||
|
{
|
||||||
|
public string code;
|
||||||
|
public string message;
|
||||||
|
|
||||||
|
public int HtmlCode => int.Parse(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Serializable]
|
||||||
|
public struct EmptyJson : ICanBeJson
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
11
Assets/Mirror/Cloud/Core/JsonStructs.cs.meta
Normal file
11
Assets/Mirror/Cloud/Core/JsonStructs.cs.meta
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 0688c0fdae5376e4ea74d5c3904eed17
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
72
Assets/Mirror/Cloud/Core/Logger.cs
Normal file
72
Assets/Mirror/Cloud/Core/Logger.cs
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
using System;
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.Networking;
|
||||||
|
|
||||||
|
namespace Mirror.Cloud
|
||||||
|
{
|
||||||
|
public static class Logger
|
||||||
|
{
|
||||||
|
public static bool VerboseLogging = false;
|
||||||
|
static readonly ILogger logger = LogFactory.GetLogger("MirrorCloudServices");
|
||||||
|
|
||||||
|
public static void LogRequest(string page, string method, bool hasJson, string json)
|
||||||
|
{
|
||||||
|
if (hasJson)
|
||||||
|
{
|
||||||
|
logger.LogFormat(LogType.Log, "Request: {0} {1} {2}", method, page, json);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
logger.LogFormat(LogType.Log, "Request: {0} {1}", method, page);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void LogResponse(UnityWebRequest statusRequest)
|
||||||
|
{
|
||||||
|
long code = statusRequest.responseCode;
|
||||||
|
LogType logType = statusRequest.IsOk()
|
||||||
|
? LogType.Log
|
||||||
|
: LogType.Error;
|
||||||
|
|
||||||
|
string format = "Response: {0} {1} {2} {3}";
|
||||||
|
if (logger.IsLogTypeAllowed(logType))
|
||||||
|
{
|
||||||
|
// we split path like this to make sure api key doesn't leak
|
||||||
|
Uri uri = new Uri(statusRequest.url);
|
||||||
|
string path = string.Join("", uri.Segments);
|
||||||
|
string msg = string.Format(format, statusRequest.method, code, path, statusRequest.downloadHandler.text);
|
||||||
|
logger.Log(logType, msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(statusRequest.error))
|
||||||
|
{
|
||||||
|
string msg = string.Format("WEB REQUEST ERROR: {0}", statusRequest.error);
|
||||||
|
logger.Log(LogType.Error, msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static void Log(string msg)
|
||||||
|
{
|
||||||
|
if (logger.LogEnabled())
|
||||||
|
logger.Log(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static void LogWarning(string msg)
|
||||||
|
{
|
||||||
|
if (logger.WarnEnabled())
|
||||||
|
logger.LogWarning(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static void LogError(string msg)
|
||||||
|
{
|
||||||
|
if (logger.ErrorEnabled())
|
||||||
|
logger.LogError(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static void Verbose(string msg)
|
||||||
|
{
|
||||||
|
if (VerboseLogging && logger.LogEnabled())
|
||||||
|
logger.Log(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
11
Assets/Mirror/Cloud/Core/Logger.cs.meta
Normal file
11
Assets/Mirror/Cloud/Core/Logger.cs.meta
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 457ba2df6cb6e1542996c17c715ee81b
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
144
Assets/Mirror/Cloud/Core/RequestCreator.cs
Normal file
144
Assets/Mirror/Cloud/Core/RequestCreator.cs
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections;
|
||||||
|
using System.Text;
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.Networking;
|
||||||
|
|
||||||
|
namespace Mirror.Cloud
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Methods to create and send UnityWebRequest
|
||||||
|
/// </summary>
|
||||||
|
public class RequestCreator : IRequestCreator
|
||||||
|
{
|
||||||
|
const string GET = "GET";
|
||||||
|
const string POST = "POST";
|
||||||
|
const string PATCH = "PATCH";
|
||||||
|
const string DELETE = "DELETE";
|
||||||
|
|
||||||
|
public readonly string baseAddress;
|
||||||
|
public readonly string apiKey;
|
||||||
|
readonly ICoroutineRunner runner;
|
||||||
|
|
||||||
|
public RequestCreator(string baseAddress, string apiKey, ICoroutineRunner coroutineRunner)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(baseAddress))
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(baseAddress));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(apiKey))
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(apiKey));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.baseAddress = baseAddress;
|
||||||
|
this.apiKey = apiKey;
|
||||||
|
|
||||||
|
runner = coroutineRunner ?? throw new ArgumentNullException(nameof(coroutineRunner));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Uri CreateUri(string page)
|
||||||
|
{
|
||||||
|
return new Uri(string.Format("{0}/{1}?key={2}", baseAddress, page, apiKey));
|
||||||
|
}
|
||||||
|
|
||||||
|
UnityWebRequest CreateWebRequest(string page, string method, string json = null)
|
||||||
|
{
|
||||||
|
bool hasJson = !string.IsNullOrEmpty(json);
|
||||||
|
Logger.LogRequest(page, method, hasJson, json);
|
||||||
|
|
||||||
|
UnityWebRequest request = new UnityWebRequest(CreateUri(page));
|
||||||
|
request.method = method;
|
||||||
|
if (hasJson)
|
||||||
|
{
|
||||||
|
request.SetRequestHeader("Content-Type", "application/json");
|
||||||
|
}
|
||||||
|
|
||||||
|
request.downloadHandler = new DownloadHandlerBuffer();
|
||||||
|
|
||||||
|
byte[] bodyRaw = hasJson
|
||||||
|
? Encoding.UTF8.GetBytes(json)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
request.uploadHandler = new UploadHandlerRaw(bodyRaw);
|
||||||
|
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create Get Request to page
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="page"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public UnityWebRequest Get(string page)
|
||||||
|
{
|
||||||
|
return CreateWebRequest(page, GET);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates Post Request to page with Json body
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T"></typeparam>
|
||||||
|
/// <param name="page"></param>
|
||||||
|
/// <param name="json"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public UnityWebRequest Post<T>(string page, T json) where T : struct, ICanBeJson
|
||||||
|
{
|
||||||
|
string jsonString = JsonUtility.ToJson(json);
|
||||||
|
return CreateWebRequest(page, POST, jsonString);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates Patch Request to page with Json body
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T"></typeparam>
|
||||||
|
/// <param name="page"></param>
|
||||||
|
/// <param name="json"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public UnityWebRequest Patch<T>(string page, T json) where T : struct, ICanBeJson
|
||||||
|
{
|
||||||
|
string jsonString = JsonUtility.ToJson(json);
|
||||||
|
return CreateWebRequest(page, PATCH, jsonString);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create Delete Request to page
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="page"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public UnityWebRequest Delete(string page)
|
||||||
|
{
|
||||||
|
return CreateWebRequest(page, DELETE);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void SendRequest(UnityWebRequest request, RequestSuccess onSuccess = null, RequestFail onFail = null)
|
||||||
|
{
|
||||||
|
runner.StartCoroutine(SendRequestEnumerator(request, onSuccess, onFail));
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerator SendRequestEnumerator(UnityWebRequest request, RequestSuccess onSuccess = null, RequestFail onFail = null)
|
||||||
|
{
|
||||||
|
using (UnityWebRequest webRequest = request)
|
||||||
|
{
|
||||||
|
yield return webRequest.SendWebRequest();
|
||||||
|
Logger.LogResponse(webRequest);
|
||||||
|
|
||||||
|
string text = webRequest.downloadHandler.text;
|
||||||
|
Logger.Verbose(text);
|
||||||
|
if (webRequest.IsOk())
|
||||||
|
{
|
||||||
|
onSuccess?.Invoke(text);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
onFail?.Invoke(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
11
Assets/Mirror/Cloud/Core/RequestCreator.cs.meta
Normal file
11
Assets/Mirror/Cloud/Core/RequestCreator.cs.meta
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: cfaa626443cc7c94eae138a2e3a04d7c
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
8
Assets/Mirror/Cloud/ListServer.meta
Normal file
8
Assets/Mirror/Cloud/ListServer.meta
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: c4c4be148a492b143a881cd08bf7e320
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
66
Assets/Mirror/Cloud/ListServer/ListServer.cs
Normal file
66
Assets/Mirror/Cloud/ListServer/ListServer.cs
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
using System;
|
||||||
|
using UnityEngine.Events;
|
||||||
|
|
||||||
|
namespace Mirror.Cloud.ListServerService
|
||||||
|
{
|
||||||
|
public sealed class ListServer
|
||||||
|
{
|
||||||
|
public readonly IListServerServerApi ServerApi;
|
||||||
|
public readonly IListServerClientApi ClientApi;
|
||||||
|
|
||||||
|
public ListServer(IListServerServerApi serverApi, IListServerClientApi clientApi)
|
||||||
|
{
|
||||||
|
ServerApi = serverApi ?? throw new ArgumentNullException(nameof(serverApi));
|
||||||
|
ClientApi = clientApi ?? throw new ArgumentNullException(nameof(clientApi));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface IListServerServerApi : IBaseApi
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Has a server been added to the list with this connection
|
||||||
|
/// </summary>
|
||||||
|
bool ServerInList { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// Add a server to the list
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="server"></param>
|
||||||
|
void AddServer(ServerJson server);
|
||||||
|
/// <summary>
|
||||||
|
/// Update the current server
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="newPlayerCount"></param>
|
||||||
|
void UpdateServer(int newPlayerCount);
|
||||||
|
/// <summary>
|
||||||
|
/// Update the current server
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="server"></param>
|
||||||
|
void UpdateServer(ServerJson server);
|
||||||
|
/// <summary>
|
||||||
|
/// Removes the current server
|
||||||
|
/// </summary>
|
||||||
|
void RemoveServer();
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface IListServerClientApi : IBaseApi
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Called when the server list is updated
|
||||||
|
/// </summary>
|
||||||
|
event UnityAction<ServerCollectionJson> onServerListUpdated;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the server list once
|
||||||
|
/// </summary>
|
||||||
|
void GetServerList();
|
||||||
|
/// <summary>
|
||||||
|
/// Start getting the server list every interval
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="interval"></param>
|
||||||
|
void StartGetServerListRepeat(int interval);
|
||||||
|
/// <summary>
|
||||||
|
/// Stop getting the server list
|
||||||
|
/// </summary>
|
||||||
|
void StopGetServerListRepeat();
|
||||||
|
}
|
||||||
|
}
|
11
Assets/Mirror/Cloud/ListServer/ListServer.cs.meta
Normal file
11
Assets/Mirror/Cloud/ListServer/ListServer.cs.meta
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 6f0311899162c5b49a3c11fa9bd9c133
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
9
Assets/Mirror/Cloud/ListServer/ListServerBaseApi.cs
Normal file
9
Assets/Mirror/Cloud/ListServer/ListServerBaseApi.cs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
namespace Mirror.Cloud.ListServerService
|
||||||
|
{
|
||||||
|
public abstract class ListServerBaseApi : BaseApi
|
||||||
|
{
|
||||||
|
protected ListServerBaseApi(ICoroutineRunner runner, IRequestCreator requestCreator) : base(runner, requestCreator)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
11
Assets/Mirror/Cloud/ListServer/ListServerBaseApi.cs.meta
Normal file
11
Assets/Mirror/Cloud/ListServer/ListServerBaseApi.cs.meta
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: b6838f9df45594d48873518cbb75b329
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
70
Assets/Mirror/Cloud/ListServer/ListServerClientApi.cs
Normal file
70
Assets/Mirror/Cloud/ListServer/ListServerClientApi.cs
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
using System.Collections;
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.Events;
|
||||||
|
using UnityEngine.Networking;
|
||||||
|
|
||||||
|
namespace Mirror.Cloud.ListServerService
|
||||||
|
{
|
||||||
|
public sealed class ListServerClientApi : ListServerBaseApi, IListServerClientApi
|
||||||
|
{
|
||||||
|
readonly ServerListEvent _onServerListUpdated;
|
||||||
|
|
||||||
|
Coroutine getServerListRepeatCoroutine;
|
||||||
|
|
||||||
|
public event UnityAction<ServerCollectionJson> onServerListUpdated
|
||||||
|
{
|
||||||
|
add => _onServerListUpdated.AddListener(value);
|
||||||
|
remove => _onServerListUpdated.RemoveListener(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ListServerClientApi(ICoroutineRunner runner, IRequestCreator requestCreator, ServerListEvent onServerListUpdated) : base(runner, requestCreator)
|
||||||
|
{
|
||||||
|
_onServerListUpdated = onServerListUpdated;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Shutdown()
|
||||||
|
{
|
||||||
|
StopGetServerListRepeat();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void GetServerList()
|
||||||
|
{
|
||||||
|
runner.StartCoroutine(getServerList());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void StartGetServerListRepeat(int interval)
|
||||||
|
{
|
||||||
|
getServerListRepeatCoroutine = runner.StartCoroutine(GetServerListRepeat(interval));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void StopGetServerListRepeat()
|
||||||
|
{
|
||||||
|
// if runner is null it has been destroyed and will alraedy be null
|
||||||
|
if (runner.IsNotNull() && getServerListRepeatCoroutine != null)
|
||||||
|
{
|
||||||
|
runner.StopCoroutine(getServerListRepeatCoroutine);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IEnumerator GetServerListRepeat(int interval)
|
||||||
|
{
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
yield return getServerList();
|
||||||
|
|
||||||
|
yield return new WaitForSeconds(interval);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
IEnumerator getServerList()
|
||||||
|
{
|
||||||
|
UnityWebRequest request = requestCreator.Get("servers");
|
||||||
|
yield return requestCreator.SendRequestEnumerator(request, onSuccess);
|
||||||
|
|
||||||
|
void onSuccess(string responseBody)
|
||||||
|
{
|
||||||
|
ServerCollectionJson serverlist = JsonUtility.FromJson<ServerCollectionJson>(responseBody);
|
||||||
|
_onServerListUpdated?.Invoke(serverlist);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
11
Assets/Mirror/Cloud/ListServer/ListServerClientApi.cs.meta
Normal file
11
Assets/Mirror/Cloud/ListServer/ListServerClientApi.cs.meta
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: d49649fb32cb96b46b10f013b38a4b50
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
207
Assets/Mirror/Cloud/ListServer/ListServerJson.cs
Normal file
207
Assets/Mirror/Cloud/ListServer/ListServerJson.cs
Normal file
@ -0,0 +1,207 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace Mirror.Cloud.ListServerService
|
||||||
|
{
|
||||||
|
[Serializable]
|
||||||
|
public struct ServerCollectionJson : ICanBeJson
|
||||||
|
{
|
||||||
|
public ServerJson[] servers;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Serializable]
|
||||||
|
public struct ServerJson : ICanBeJson
|
||||||
|
{
|
||||||
|
public string protocol;
|
||||||
|
public int port;
|
||||||
|
public int playerCount;
|
||||||
|
public int maxPlayerCount;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// optional
|
||||||
|
/// </summary>
|
||||||
|
public string displayName;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Uri string of the ip and port of the server.
|
||||||
|
/// <para>The ip is calculated by the request to the API</para>
|
||||||
|
/// <para>This is returns from the api, any incoming address fields will be ignored</para>
|
||||||
|
/// </summary>
|
||||||
|
public string address;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Can be used to set custom uri
|
||||||
|
/// <para>optional</para>
|
||||||
|
/// </summary>
|
||||||
|
public string customAddress;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Array of custom data, use SetCustomData to set values
|
||||||
|
/// <para>optional</para>
|
||||||
|
/// </summary>
|
||||||
|
public KeyValue[] customData;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Uri from address field
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public Uri GetServerUri() => new Uri(address);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Uri from customAddress field
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public Uri GetCustomUri() => new Uri(customAddress);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates the customData array
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="data"></param>
|
||||||
|
public void SetCustomData(Dictionary<string, string> data)
|
||||||
|
{
|
||||||
|
if (data == null)
|
||||||
|
{
|
||||||
|
customData = null;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
customData = data.ToKeyValueArray();
|
||||||
|
CustomDataHelper.ValidateCustomData(customData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Validate()
|
||||||
|
{
|
||||||
|
CustomDataHelper.ValidateCustomData(customData);
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(protocol))
|
||||||
|
{
|
||||||
|
Logger.LogError("ServerJson should not have empty protocol");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (port == 0)
|
||||||
|
{
|
||||||
|
Logger.LogError("ServerJson should not have port equal 0");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (maxPlayerCount == 0)
|
||||||
|
{
|
||||||
|
Logger.LogError("ServerJson should not have maxPlayerCount equal 0");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Serializable]
|
||||||
|
public struct PartialServerJson : ICanBeJson
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// optional
|
||||||
|
/// </summary>
|
||||||
|
public int playerCount;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// optional
|
||||||
|
/// </summary>
|
||||||
|
public int maxPlayerCount;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// optional
|
||||||
|
/// </summary>
|
||||||
|
public string displayName;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Array of custom data, use SetCustomData to set values
|
||||||
|
/// <para>optional</para>
|
||||||
|
/// </summary>
|
||||||
|
public KeyValue[] customData;
|
||||||
|
|
||||||
|
|
||||||
|
public void SetCustomData(Dictionary<string, string> data)
|
||||||
|
{
|
||||||
|
if (data == null)
|
||||||
|
{
|
||||||
|
customData = null;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
customData = data.ToKeyValueArray();
|
||||||
|
CustomDataHelper.ValidateCustomData(customData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Validate()
|
||||||
|
{
|
||||||
|
CustomDataHelper.ValidateCustomData(customData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class CustomDataHelper
|
||||||
|
{
|
||||||
|
const int MaxCustomData = 16;
|
||||||
|
|
||||||
|
public static Dictionary<string, string> ToDictionary(this KeyValue[] keyValues)
|
||||||
|
{
|
||||||
|
return keyValues.ToDictionary(x => x.key, x => x.value);
|
||||||
|
}
|
||||||
|
public static KeyValue[] ToKeyValueArray(this Dictionary<string, string> dictionary)
|
||||||
|
{
|
||||||
|
return dictionary.Select(kvp => new KeyValue(kvp.Key, kvp.Value)).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void ValidateCustomData(KeyValue[] customData)
|
||||||
|
{
|
||||||
|
if (customData == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (customData.Length > MaxCustomData)
|
||||||
|
{
|
||||||
|
Logger.LogError($"There can only be {MaxCustomData} custom data but there was {customData.Length} values given");
|
||||||
|
Array.Resize(ref customData, MaxCustomData);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (KeyValue item in customData)
|
||||||
|
{
|
||||||
|
item.Validate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Serializable]
|
||||||
|
public struct KeyValue
|
||||||
|
{
|
||||||
|
const int MaxKeySize = 32;
|
||||||
|
const int MaxValueSize = 256;
|
||||||
|
|
||||||
|
public string key;
|
||||||
|
public string value;
|
||||||
|
|
||||||
|
public KeyValue(string key, string value)
|
||||||
|
{
|
||||||
|
this.key = key;
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Validate()
|
||||||
|
{
|
||||||
|
if (key.Length > MaxKeySize)
|
||||||
|
{
|
||||||
|
Logger.LogError($"Custom Data must have key with length less than {MaxKeySize}");
|
||||||
|
key = key.Substring(0, MaxKeySize);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.Length > MaxValueSize)
|
||||||
|
{
|
||||||
|
Logger.LogError($"Custom Data must have value with length less than {MaxValueSize}");
|
||||||
|
value = value.Substring(0, MaxValueSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
11
Assets/Mirror/Cloud/ListServer/ListServerJson.cs.meta
Normal file
11
Assets/Mirror/Cloud/ListServer/ListServerJson.cs.meta
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: a963606335eae0f47abe7ecb5fd028ea
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
219
Assets/Mirror/Cloud/ListServer/ListServerServerApi.cs
Normal file
219
Assets/Mirror/Cloud/ListServer/ListServerServerApi.cs
Normal file
@ -0,0 +1,219 @@
|
|||||||
|
using System.Collections;
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.Networking;
|
||||||
|
|
||||||
|
namespace Mirror.Cloud.ListServerService
|
||||||
|
{
|
||||||
|
public sealed class ListServerServerApi : ListServerBaseApi, IListServerServerApi
|
||||||
|
{
|
||||||
|
const int PingInterval = 20;
|
||||||
|
const int MaxPingFails = 15;
|
||||||
|
|
||||||
|
ServerJson currentServer;
|
||||||
|
string serverId;
|
||||||
|
|
||||||
|
Coroutine _pingCoroutine;
|
||||||
|
/// <summary>
|
||||||
|
/// If the server has already been added
|
||||||
|
/// </summary>
|
||||||
|
bool added;
|
||||||
|
/// <summary>
|
||||||
|
/// if a request is currently sending
|
||||||
|
/// </summary>
|
||||||
|
bool sending;
|
||||||
|
/// <summary>
|
||||||
|
/// If an update request was recently sent
|
||||||
|
/// </summary>
|
||||||
|
bool skipNextPing;
|
||||||
|
/// <summary>
|
||||||
|
/// How many failed pings in a row
|
||||||
|
/// </summary>
|
||||||
|
int pingFails = 0;
|
||||||
|
|
||||||
|
public bool ServerInList => added;
|
||||||
|
|
||||||
|
public ListServerServerApi(ICoroutineRunner runner, IRequestCreator requestCreator) : base(runner, requestCreator)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Shutdown()
|
||||||
|
{
|
||||||
|
stopPingCoroutine();
|
||||||
|
if (added)
|
||||||
|
{
|
||||||
|
removeServerWithoutCoroutine();
|
||||||
|
}
|
||||||
|
added = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddServer(ServerJson server)
|
||||||
|
{
|
||||||
|
if (added) { Logger.LogWarning("AddServer called when server was already adding or added"); return; }
|
||||||
|
bool valid = server.Validate();
|
||||||
|
if (!valid) { return; }
|
||||||
|
|
||||||
|
runner.StartCoroutine(addServer(server));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateServer(int newPlayerCount)
|
||||||
|
{
|
||||||
|
if (!added) { Logger.LogWarning("UpdateServer called when before server was added"); return; }
|
||||||
|
|
||||||
|
currentServer.playerCount = newPlayerCount;
|
||||||
|
UpdateServer(currentServer);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateServer(ServerJson server)
|
||||||
|
{
|
||||||
|
// TODO, use PartialServerJson as Arg Instead
|
||||||
|
if (!added) { Logger.LogWarning("UpdateServer called when before server was added"); return; }
|
||||||
|
|
||||||
|
PartialServerJson partialServer = new PartialServerJson
|
||||||
|
{
|
||||||
|
displayName = server.displayName,
|
||||||
|
playerCount = server.playerCount,
|
||||||
|
maxPlayerCount = server.maxPlayerCount,
|
||||||
|
customData = server.customData,
|
||||||
|
};
|
||||||
|
partialServer.Validate();
|
||||||
|
|
||||||
|
runner.StartCoroutine(updateServer(partialServer));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveServer()
|
||||||
|
{
|
||||||
|
if (!added) { return; }
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(serverId))
|
||||||
|
{
|
||||||
|
Logger.LogWarning("Can not remove server because serverId was empty");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
stopPingCoroutine();
|
||||||
|
runner.StartCoroutine(removeServer());
|
||||||
|
}
|
||||||
|
|
||||||
|
void stopPingCoroutine()
|
||||||
|
{
|
||||||
|
if (_pingCoroutine != null)
|
||||||
|
{
|
||||||
|
runner.StopCoroutine(_pingCoroutine);
|
||||||
|
_pingCoroutine = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IEnumerator addServer(ServerJson server)
|
||||||
|
{
|
||||||
|
added = true;
|
||||||
|
sending = true;
|
||||||
|
currentServer = server;
|
||||||
|
|
||||||
|
UnityWebRequest request = requestCreator.Post("servers", currentServer);
|
||||||
|
yield return requestCreator.SendRequestEnumerator(request, onSuccess, onFail);
|
||||||
|
sending = false;
|
||||||
|
|
||||||
|
void onSuccess(string responseBody)
|
||||||
|
{
|
||||||
|
CreatedIdJson created = JsonUtility.FromJson<CreatedIdJson>(responseBody);
|
||||||
|
serverId = created.id;
|
||||||
|
|
||||||
|
// Start ping to keep server alive
|
||||||
|
_pingCoroutine = runner.StartCoroutine(ping());
|
||||||
|
}
|
||||||
|
void onFail(string responseBody)
|
||||||
|
{
|
||||||
|
added = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IEnumerator updateServer(PartialServerJson server)
|
||||||
|
{
|
||||||
|
// wait to not be sending
|
||||||
|
while (sending)
|
||||||
|
{
|
||||||
|
yield return new WaitForSeconds(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// We need to check added incase Update is called soon after Add, and add failed
|
||||||
|
if (!added) { Logger.LogWarning("UpdateServer called when before server was added"); yield break; }
|
||||||
|
|
||||||
|
sending = true;
|
||||||
|
UnityWebRequest request = requestCreator.Patch("servers/" + serverId, server);
|
||||||
|
yield return requestCreator.SendRequestEnumerator(request, onSuccess);
|
||||||
|
sending = false;
|
||||||
|
|
||||||
|
void onSuccess(string responseBody)
|
||||||
|
{
|
||||||
|
skipNextPing = true;
|
||||||
|
|
||||||
|
if (_pingCoroutine == null)
|
||||||
|
{
|
||||||
|
_pingCoroutine = runner.StartCoroutine(ping());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Keeps server alive in database
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
IEnumerator ping()
|
||||||
|
{
|
||||||
|
while (pingFails <= MaxPingFails)
|
||||||
|
{
|
||||||
|
yield return new WaitForSeconds(PingInterval);
|
||||||
|
if (skipNextPing)
|
||||||
|
{
|
||||||
|
skipNextPing = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
sending = true;
|
||||||
|
UnityWebRequest request = requestCreator.Patch("servers/" + serverId, new EmptyJson());
|
||||||
|
yield return requestCreator.SendRequestEnumerator(request, onSuccess, onFail);
|
||||||
|
sending = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.LogWarning("Max ping fails reached, stoping to ping server");
|
||||||
|
_pingCoroutine = null;
|
||||||
|
|
||||||
|
|
||||||
|
void onSuccess(string responseBody)
|
||||||
|
{
|
||||||
|
pingFails = 0;
|
||||||
|
}
|
||||||
|
void onFail(string responseBody)
|
||||||
|
{
|
||||||
|
pingFails++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IEnumerator removeServer()
|
||||||
|
{
|
||||||
|
sending = true;
|
||||||
|
UnityWebRequest request = requestCreator.Delete("servers/" + serverId);
|
||||||
|
yield return requestCreator.SendRequestEnumerator(request);
|
||||||
|
sending = false;
|
||||||
|
|
||||||
|
added = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void removeServerWithoutCoroutine()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(serverId))
|
||||||
|
{
|
||||||
|
Logger.LogWarning("Can not remove server becuase serverId was empty");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
UnityWebRequest request = requestCreator.Delete("servers/" + serverId);
|
||||||
|
UnityWebRequestAsyncOperation operation = request.SendWebRequest();
|
||||||
|
|
||||||
|
operation.completed += (op) =>
|
||||||
|
{
|
||||||
|
Logger.LogResponse(request);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
11
Assets/Mirror/Cloud/ListServer/ListServerServerApi.cs.meta
Normal file
11
Assets/Mirror/Cloud/ListServer/ListServerServerApi.cs.meta
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 675f0d0fd4e82b04290c4d30c8d78ede
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
14
Assets/Mirror/Cloud/Mirror.Cloud.asmdef
Normal file
14
Assets/Mirror/Cloud/Mirror.Cloud.asmdef
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"name": "Mirror.Cloud",
|
||||||
|
"references": [
|
||||||
|
"Mirror"
|
||||||
|
],
|
||||||
|
"optionalUnityReferences": [],
|
||||||
|
"includePlatforms": [],
|
||||||
|
"excludePlatforms": [],
|
||||||
|
"allowUnsafeCode": false,
|
||||||
|
"overrideReferences": false,
|
||||||
|
"precompiledReferences": [],
|
||||||
|
"autoReferenced": true,
|
||||||
|
"defineConstraints": []
|
||||||
|
}
|
7
Assets/Mirror/Cloud/Mirror.Cloud.asmdef.meta
Normal file
7
Assets/Mirror/Cloud/Mirror.Cloud.asmdef.meta
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: c21ba7b8c3183cb47b7fe3b3799d49c4
|
||||||
|
AssemblyDefinitionImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
152
Assets/Mirror/Cloud/README.md
Normal file
152
Assets/Mirror/Cloud/README.md
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
# Mirror Cloud Services
|
||||||
|
|
||||||
|
## Mirror List Server
|
||||||
|
|
||||||
|
Example has an API key which can be used as a demo.
|
||||||
|
|
||||||
|
To get an API key to use within your game you can subscribe on the [Mirror Networking Website](https://mirror-networking.com/list-server/)
|
||||||
|
|
||||||
|
### Key features
|
||||||
|
|
||||||
|
- The Cloud Service works via https so it is secure and can be used from any platform.
|
||||||
|
- It runs on Google Cloud so there is no worry about server downtime.
|
||||||
|
- It scales really well. Default quota is 1000 API requests per minute. If you have high demands, contact us and we can increase that limit.
|
||||||
|
|
||||||
|
## List Server Examples
|
||||||
|
|
||||||
|
An example for this can be found in [Mirror/Examples/Cloud/](https://github.com/vis2k/Mirror/tree/master/Assets/Mirror/Examples/Cloud)
|
||||||
|
|
||||||
|
*Note: you cannot connect to your own public IP address, you will need at least one other person to test this*
|
||||||
|
|
||||||
|
## How to use
|
||||||
|
|
||||||
|
Add `ApiConnector` component to an object in your game. It is probably best to put this on the same object as your NetworkManager. Once it has been added set the `ApiAddress` and `ApiKey` fields.
|
||||||
|
|
||||||
|
To use `ApiConnector` either directly reference it in an inspector field or get it when your script awakes
|
||||||
|
```cs
|
||||||
|
ApiConnector connector;
|
||||||
|
|
||||||
|
void Awake()
|
||||||
|
{
|
||||||
|
connector = FindObjectOfType<ApiConnector>();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
The Api calls are grouped into objects. `connector.ListServer.ServerApi` has the Server Api calls like `AddServer`. `connector.ListServer.ClientApi` has the Client Api calls like `GetServerList`.
|
||||||
|
|
||||||
|
### Server Api Example
|
||||||
|
|
||||||
|
Example of how to add server
|
||||||
|
```cs
|
||||||
|
void AddServer(int playerCount)
|
||||||
|
{
|
||||||
|
Transport transport = Transport.activeTransport;
|
||||||
|
|
||||||
|
Uri uri = transport.ServerUri();
|
||||||
|
int port = uri.Port;
|
||||||
|
string protocol = uri.Scheme;
|
||||||
|
|
||||||
|
connector.ListServer.ServerApi.AddServer(new ServerJson
|
||||||
|
{
|
||||||
|
displayName = "Fun game!!!",
|
||||||
|
protocol = protocol,
|
||||||
|
port = port,
|
||||||
|
maxPlayerCount = NetworkManager.singleton.maxConnections,
|
||||||
|
playerCount = playerCount
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Client Api Example
|
||||||
|
Example of how to list servers
|
||||||
|
|
||||||
|
```cs
|
||||||
|
ApiConnector connector;
|
||||||
|
|
||||||
|
void Awake()
|
||||||
|
{
|
||||||
|
connector = FindObjectOfType<ApiConnector>();
|
||||||
|
// add listener to event that will update UI when Server list is refreshed
|
||||||
|
connector.ListServer.ClientApi.onServerListUpdated += onServerListUpdated;
|
||||||
|
|
||||||
|
// add listen to button so that player can refresh server list
|
||||||
|
refreshButton.onClick.AddListener(RefreshButtonHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RefreshButtonHandler()
|
||||||
|
{
|
||||||
|
connector.ListServer.ClientApi.GetServerList();
|
||||||
|
}
|
||||||
|
|
||||||
|
void onServerListUpdated()
|
||||||
|
{
|
||||||
|
// Update UI here
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Debug
|
||||||
|
|
||||||
|
If something doesn't seem to be working then here are some tips to help solve the problem
|
||||||
|
|
||||||
|
### Check logs
|
||||||
|
|
||||||
|
Enable `showDebugMessages` on your NetworkManager or use the log level window to enable logging for the cloud scripts
|
||||||
|
|
||||||
|
Below are some example logs to look for to check things are working.
|
||||||
|
|
||||||
|
#### Add Server
|
||||||
|
|
||||||
|
The add request is sent to add a server to the list server
|
||||||
|
|
||||||
|
```
|
||||||
|
Request: POST servers {"protocol":"tcp4","port":7777,"playerCount":0,"maxPlayerCount":4,"displayName":"Tanks Game 521","address":"","customAddress":"","customData":[]}
|
||||||
|
```
|
||||||
|
```
|
||||||
|
Response: POST 200 /servers {"id":"BI6bQQ2TbNiqhdp1D7UB"}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Update Server
|
||||||
|
|
||||||
|
The object sent in update request maybe be empty, this is sent to keep the server record alive so it shows up.
|
||||||
|
|
||||||
|
The update request can also be used to change info. For example the player count when someone joins or leaves
|
||||||
|
|
||||||
|
```
|
||||||
|
Request: PATCH servers/BI6bQQ2TbNiqhdp1D7UB {}
|
||||||
|
```
|
||||||
|
```
|
||||||
|
Response: PATCH 204 /servers/BI6bQQ2TbNiqhdp1D7UB
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Remove Server
|
||||||
|
|
||||||
|
The remove request is sent to remove a server from the list server. This is automatically called when the ApiConnection is destroyed.
|
||||||
|
|
||||||
|
```
|
||||||
|
Request: DELETE servers/BI6bQQ2TbNiqhdp1D7UB
|
||||||
|
```
|
||||||
|
```
|
||||||
|
Response: DELETE 204 /servers/BI6bQQ2TbNiqhdp1D7UB
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
#### Get Servers
|
||||||
|
|
||||||
|
The get request is sent in order to get the list of servers.
|
||||||
|
|
||||||
|
The example below shows an array of 2 servers, one with name `Tanks Game 521` and the other with name `Tanks Game 212`
|
||||||
|
|
||||||
|
```
|
||||||
|
Request: GET servers
|
||||||
|
```
|
||||||
|
```
|
||||||
|
Response: GET 200 /servers {"servers":[{"address":"tcp4://xx.xx.xx.xx:7777","displayName":"Tanks Game 521","port":7777,"protocol":"tcp4","playerCount":0,"maxPlayerCount":4,"customAddress":"","customData":[]},{"address":"tcp4://xx.xx.xx.xx:7777","displayName":"Tanks Game 212","port":7777,"protocol":"tcp4","playerCount":0,"maxPlayerCount":4,"customData":[]}]}
|
||||||
|
```
|
||||||
|
*xx.xx.xx.xx will be the IP address for the server*
|
||||||
|
|
||||||
|
|
||||||
|
### Use the QuickListServerDebug
|
||||||
|
|
||||||
|
The QuickListServerDebug script uses `OnGUI` to show the list of servers. This script can be used to check the server list without using Canvas UI.
|
7
Assets/Mirror/Cloud/README.md.meta
Normal file
7
Assets/Mirror/Cloud/README.md.meta
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 04945d14ccbed964597a1ee00805c059
|
||||||
|
TextScriptImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
1
Assets/Mirror/Cloud/version.txt
Normal file
1
Assets/Mirror/Cloud/version.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
MirrorCloudServices v0.1.0
|
7
Assets/Mirror/Cloud/version.txt.meta
Normal file
7
Assets/Mirror/Cloud/version.txt.meta
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: bf81e376b88e68e48a47531b8bfeb0f4
|
||||||
|
TextScriptImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
8
Assets/Mirror/CompilerSymbols.meta
Normal file
8
Assets/Mirror/CompilerSymbols.meta
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 1f8b918bcd89f5c488b06f5574f34760
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
14
Assets/Mirror/CompilerSymbols/Mirror.CompilerSymbols.asmdef
Normal file
14
Assets/Mirror/CompilerSymbols/Mirror.CompilerSymbols.asmdef
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"name": "Mirror.CompilerSymbols",
|
||||||
|
"references": [],
|
||||||
|
"optionalUnityReferences": [],
|
||||||
|
"includePlatforms": [
|
||||||
|
"Editor"
|
||||||
|
],
|
||||||
|
"excludePlatforms": [],
|
||||||
|
"allowUnsafeCode": false,
|
||||||
|
"overrideReferences": false,
|
||||||
|
"precompiledReferences": [],
|
||||||
|
"autoReferenced": true,
|
||||||
|
"defineConstraints": []
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 325984b52e4128546bc7558552f8b1d2
|
||||||
|
AssemblyDefinitionImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
49
Assets/Mirror/CompilerSymbols/PreprocessorDefine.cs
Normal file
49
Assets/Mirror/CompilerSymbols/PreprocessorDefine.cs
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using UnityEditor;
|
||||||
|
|
||||||
|
namespace Mirror
|
||||||
|
{
|
||||||
|
static class PreprocessorDefine
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Add define symbols as soon as Unity gets done compiling.
|
||||||
|
/// </summary>
|
||||||
|
[InitializeOnLoadMethod]
|
||||||
|
public static void AddDefineSymbols()
|
||||||
|
{
|
||||||
|
string currentDefines = PlayerSettings.GetScriptingDefineSymbolsForGroup(EditorUserBuildSettings.selectedBuildTargetGroup);
|
||||||
|
HashSet<string> defines = new HashSet<string>(currentDefines.Split(';'))
|
||||||
|
{
|
||||||
|
"MIRROR",
|
||||||
|
"MIRROR_1726_OR_NEWER",
|
||||||
|
"MIRROR_3_0_OR_NEWER",
|
||||||
|
"MIRROR_3_12_OR_NEWER",
|
||||||
|
"MIRROR_4_0_OR_NEWER",
|
||||||
|
"MIRROR_5_0_OR_NEWER",
|
||||||
|
"MIRROR_6_0_OR_NEWER",
|
||||||
|
"MIRROR_7_0_OR_NEWER",
|
||||||
|
"MIRROR_8_0_OR_NEWER",
|
||||||
|
"MIRROR_9_0_OR_NEWER",
|
||||||
|
"MIRROR_10_0_OR_NEWER",
|
||||||
|
"MIRROR_11_0_OR_NEWER",
|
||||||
|
"MIRROR_12_0_OR_NEWER",
|
||||||
|
"MIRROR_13_0_OR_NEWER",
|
||||||
|
"MIRROR_14_0_OR_NEWER",
|
||||||
|
"MIRROR_15_0_OR_NEWER",
|
||||||
|
"MIRROR_16_0_OR_NEWER",
|
||||||
|
"MIRROR_17_0_OR_NEWER",
|
||||||
|
"MIRROR_18_0_OR_NEWER",
|
||||||
|
"MIRROR_24_0_OR_NEWER",
|
||||||
|
"MIRROR_26_0_OR_NEWER"
|
||||||
|
};
|
||||||
|
|
||||||
|
// only touch PlayerSettings if we actually modified it.
|
||||||
|
// otherwise it shows up as changed in git each time.
|
||||||
|
string newDefines = string.Join(";", defines);
|
||||||
|
if (newDefines != currentDefines)
|
||||||
|
{
|
||||||
|
PlayerSettings.SetScriptingDefineSymbolsForGroup(EditorUserBuildSettings.selectedBuildTargetGroup, newDefines);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
11
Assets/Mirror/CompilerSymbols/PreprocessorDefine.cs.meta
Normal file
11
Assets/Mirror/CompilerSymbols/PreprocessorDefine.cs.meta
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: f1d66fe74ec6f42dd974cba37d25d453
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
8
Assets/Mirror/Components.meta
Normal file
8
Assets/Mirror/Components.meta
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 9bee879fbc8ef4b1a9a9f7088bfbf726
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
8
Assets/Mirror/Components/Discovery.meta
Normal file
8
Assets/Mirror/Components/Discovery.meta
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: b5dcf9618f5e14a4eb60bff5480284a6
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
114
Assets/Mirror/Components/Discovery/NetworkDiscovery.cs
Normal file
114
Assets/Mirror/Components/Discovery/NetworkDiscovery.cs
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
using System;
|
||||||
|
using System.Net;
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.Events;
|
||||||
|
|
||||||
|
namespace Mirror.Discovery
|
||||||
|
{
|
||||||
|
[Serializable]
|
||||||
|
public class ServerFoundUnityEvent : UnityEvent<ServerResponse> { };
|
||||||
|
|
||||||
|
[DisallowMultipleComponent]
|
||||||
|
[AddComponentMenu("Network/NetworkDiscovery")]
|
||||||
|
public class NetworkDiscovery : NetworkDiscoveryBase<ServerRequest, ServerResponse>
|
||||||
|
{
|
||||||
|
#region Server
|
||||||
|
|
||||||
|
public long ServerId { get; private set; }
|
||||||
|
|
||||||
|
[Tooltip("Transport to be advertised during discovery")]
|
||||||
|
public Transport transport;
|
||||||
|
|
||||||
|
[Tooltip("Invoked when a server is found")]
|
||||||
|
public ServerFoundUnityEvent OnServerFound;
|
||||||
|
|
||||||
|
public override void Start()
|
||||||
|
{
|
||||||
|
ServerId = RandomLong();
|
||||||
|
|
||||||
|
// active transport gets initialized in awake
|
||||||
|
// so make sure we set it here in Start() (after awakes)
|
||||||
|
// Or just let the user assign it in the inspector
|
||||||
|
if (transport == null)
|
||||||
|
transport = Transport.activeTransport;
|
||||||
|
|
||||||
|
base.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Process the request from a client
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Override if you wish to provide more information to the clients
|
||||||
|
/// such as the name of the host player
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="request">Request comming from client</param>
|
||||||
|
/// <param name="endpoint">Address of the client that sent the request</param>
|
||||||
|
/// <returns>The message to be sent back to the client or null</returns>
|
||||||
|
protected override ServerResponse ProcessRequest(ServerRequest request, IPEndPoint endpoint)
|
||||||
|
{
|
||||||
|
// In this case we don't do anything with the request
|
||||||
|
// but other discovery implementations might want to use the data
|
||||||
|
// in there, This way the client can ask for
|
||||||
|
// specific game mode or something
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// this is an example reply message, return your own
|
||||||
|
// to include whatever is relevant for your game
|
||||||
|
return new ServerResponse
|
||||||
|
{
|
||||||
|
serverId = ServerId,
|
||||||
|
uri = transport.ServerUri()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (NotImplementedException)
|
||||||
|
{
|
||||||
|
Debug.LogError($"Transport {transport} does not support network discovery");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Client
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a message that will be broadcasted on the network to discover servers
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Override if you wish to include additional data in the discovery message
|
||||||
|
/// such as desired game mode, language, difficulty, etc... </remarks>
|
||||||
|
/// <returns>An instance of ServerRequest with data to be broadcasted</returns>
|
||||||
|
protected override ServerRequest GetRequest() => new ServerRequest();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Process the answer from a server
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// A client receives a reply from a server, this method processes the
|
||||||
|
/// reply and raises an event
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="response">Response that came from the server</param>
|
||||||
|
/// <param name="endpoint">Address of the server that replied</param>
|
||||||
|
protected override void ProcessResponse(ServerResponse response, IPEndPoint endpoint)
|
||||||
|
{
|
||||||
|
// we received a message from the remote endpoint
|
||||||
|
response.EndPoint = endpoint;
|
||||||
|
|
||||||
|
// although we got a supposedly valid url, we may not be able to resolve
|
||||||
|
// the provided host
|
||||||
|
// However we know the real ip address of the server because we just
|
||||||
|
// received a packet from it, so use that as host.
|
||||||
|
UriBuilder realUri = new UriBuilder(response.uri)
|
||||||
|
{
|
||||||
|
Host = response.EndPoint.Address.ToString()
|
||||||
|
};
|
||||||
|
response.uri = realUri.Uri;
|
||||||
|
|
||||||
|
OnServerFound.Invoke(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
}
|
11
Assets/Mirror/Components/Discovery/NetworkDiscovery.cs.meta
Normal file
11
Assets/Mirror/Components/Discovery/NetworkDiscovery.cs.meta
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: c761308e733c51245b2e8bb4201f46dc
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
360
Assets/Mirror/Components/Discovery/NetworkDiscoveryBase.cs
Normal file
360
Assets/Mirror/Components/Discovery/NetworkDiscoveryBase.cs
Normal file
@ -0,0 +1,360 @@
|
|||||||
|
using System;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Sockets;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
// Based on https://github.com/EnlightenedOne/MirrorNetworkDiscovery
|
||||||
|
// forked from https://github.com/in0finite/MirrorNetworkDiscovery
|
||||||
|
// Both are MIT Licensed
|
||||||
|
|
||||||
|
namespace Mirror.Discovery
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Base implementation for Network Discovery. Extend this component
|
||||||
|
/// to provide custom discovery with game specific data
|
||||||
|
/// <see cref="NetworkDiscovery">NetworkDiscovery</see> for a sample implementation
|
||||||
|
/// </summary>
|
||||||
|
[DisallowMultipleComponent]
|
||||||
|
[HelpURL("https://mirror-networking.com/docs/Components/NetworkDiscovery.html")]
|
||||||
|
public abstract class NetworkDiscoveryBase<Request, Response> : MonoBehaviour
|
||||||
|
where Request : NetworkMessage
|
||||||
|
where Response : NetworkMessage
|
||||||
|
{
|
||||||
|
public static bool SupportedOnThisPlatform { get { return Application.platform != RuntimePlatform.WebGLPlayer; } }
|
||||||
|
|
||||||
|
// each game should have a random unique handshake, this way you can tell if this is the same game or not
|
||||||
|
[HideInInspector]
|
||||||
|
public long secretHandshake;
|
||||||
|
|
||||||
|
[SerializeField]
|
||||||
|
[Tooltip("The UDP port the server will listen for multi-cast messages")]
|
||||||
|
protected int serverBroadcastListenPort = 47777;
|
||||||
|
|
||||||
|
[SerializeField]
|
||||||
|
[Tooltip("Time in seconds between multi-cast messages")]
|
||||||
|
[Range(1, 60)]
|
||||||
|
float ActiveDiscoveryInterval = 3;
|
||||||
|
|
||||||
|
protected UdpClient serverUdpClient;
|
||||||
|
protected UdpClient clientUdpClient;
|
||||||
|
|
||||||
|
#if UNITY_EDITOR
|
||||||
|
void OnValidate()
|
||||||
|
{
|
||||||
|
if (secretHandshake == 0)
|
||||||
|
{
|
||||||
|
secretHandshake = RandomLong();
|
||||||
|
UnityEditor.Undo.RecordObject(this, "Set secret handshake");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
public static long RandomLong()
|
||||||
|
{
|
||||||
|
int value1 = UnityEngine.Random.Range(int.MinValue, int.MaxValue);
|
||||||
|
int value2 = UnityEngine.Random.Range(int.MinValue, int.MaxValue);
|
||||||
|
return value1 + ((long)value2 << 32);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// virtual so that inheriting classes' Start() can call base.Start() too
|
||||||
|
/// </summary>
|
||||||
|
public virtual void Start()
|
||||||
|
{
|
||||||
|
// Server mode? then start advertising
|
||||||
|
#if UNITY_SERVER
|
||||||
|
AdvertiseServer();
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the ports are cleared no matter when Game/Unity UI exits
|
||||||
|
void OnApplicationQuit()
|
||||||
|
{
|
||||||
|
Shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Shutdown()
|
||||||
|
{
|
||||||
|
if (serverUdpClient != null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
serverUdpClient.Close();
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
// it is just close, swallow the error
|
||||||
|
}
|
||||||
|
|
||||||
|
serverUdpClient = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clientUdpClient != null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
clientUdpClient.Close();
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
// it is just close, swallow the error
|
||||||
|
}
|
||||||
|
|
||||||
|
clientUdpClient = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
CancelInvoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Server
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Advertise this server in the local network
|
||||||
|
/// </summary>
|
||||||
|
public void AdvertiseServer()
|
||||||
|
{
|
||||||
|
if (!SupportedOnThisPlatform)
|
||||||
|
throw new PlatformNotSupportedException("Network discovery not supported in this platform");
|
||||||
|
|
||||||
|
StopDiscovery();
|
||||||
|
|
||||||
|
// Setup port -- may throw exception
|
||||||
|
serverUdpClient = new UdpClient(serverBroadcastListenPort)
|
||||||
|
{
|
||||||
|
EnableBroadcast = true,
|
||||||
|
MulticastLoopback = false
|
||||||
|
};
|
||||||
|
|
||||||
|
// listen for client pings
|
||||||
|
_ = ServerListenAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ServerListenAsync()
|
||||||
|
{
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await ReceiveRequestAsync(serverUdpClient);
|
||||||
|
}
|
||||||
|
catch (ObjectDisposedException)
|
||||||
|
{
|
||||||
|
// socket has been closed
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async Task ReceiveRequestAsync(UdpClient udpClient)
|
||||||
|
{
|
||||||
|
// only proceed if there is available data in network buffer, or otherwise Receive() will block
|
||||||
|
// average time for UdpClient.Available : 10 us
|
||||||
|
|
||||||
|
UdpReceiveResult udpReceiveResult = await udpClient.ReceiveAsync();
|
||||||
|
|
||||||
|
using (PooledNetworkReader networkReader = NetworkReaderPool.GetReader(udpReceiveResult.Buffer))
|
||||||
|
{
|
||||||
|
long handshake = networkReader.ReadInt64();
|
||||||
|
if (handshake != secretHandshake)
|
||||||
|
{
|
||||||
|
// message is not for us
|
||||||
|
throw new ProtocolViolationException("Invalid handshake");
|
||||||
|
}
|
||||||
|
|
||||||
|
Request request = networkReader.Read<Request>();
|
||||||
|
|
||||||
|
ProcessClientRequest(request, udpReceiveResult.RemoteEndPoint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reply to the client to inform it of this server
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Override if you wish to ignore server requests based on
|
||||||
|
/// custom criteria such as language, full server game mode or difficulty
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="request">Request comming from client</param>
|
||||||
|
/// <param name="endpoint">Address of the client that sent the request</param>
|
||||||
|
protected virtual void ProcessClientRequest(Request request, IPEndPoint endpoint)
|
||||||
|
{
|
||||||
|
Response info = ProcessRequest(request, endpoint);
|
||||||
|
|
||||||
|
if (info == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
using (PooledNetworkWriter writer = NetworkWriterPool.GetWriter())
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
writer.WriteInt64(secretHandshake);
|
||||||
|
|
||||||
|
writer.Write(info);
|
||||||
|
|
||||||
|
ArraySegment<byte> data = writer.ToArraySegment();
|
||||||
|
// signature matches
|
||||||
|
// send response
|
||||||
|
serverUdpClient.Send(data.Array, data.Count, endpoint);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Debug.LogException(ex, this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Process the request from a client
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Override if you wish to provide more information to the clients
|
||||||
|
/// such as the name of the host player
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="request">Request comming from client</param>
|
||||||
|
/// <param name="endpoint">Address of the client that sent the request</param>
|
||||||
|
/// <returns>The message to be sent back to the client or null</returns>
|
||||||
|
protected abstract Response ProcessRequest(Request request, IPEndPoint endpoint);
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Client
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Start Active Discovery
|
||||||
|
/// </summary>
|
||||||
|
public void StartDiscovery()
|
||||||
|
{
|
||||||
|
if (!SupportedOnThisPlatform)
|
||||||
|
throw new PlatformNotSupportedException("Network discovery not supported in this platform");
|
||||||
|
|
||||||
|
StopDiscovery();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Setup port
|
||||||
|
clientUdpClient = new UdpClient(0)
|
||||||
|
{
|
||||||
|
EnableBroadcast = true,
|
||||||
|
MulticastLoopback = false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
// Free the port if we took it
|
||||||
|
Shutdown();
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = ClientListenAsync();
|
||||||
|
|
||||||
|
InvokeRepeating(nameof(BroadcastDiscoveryRequest), 0, ActiveDiscoveryInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stop Active Discovery
|
||||||
|
/// </summary>
|
||||||
|
public void StopDiscovery()
|
||||||
|
{
|
||||||
|
Shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Awaits for server response
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>ClientListenAsync Task</returns>
|
||||||
|
public async Task ClientListenAsync()
|
||||||
|
{
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await ReceiveGameBroadcastAsync(clientUdpClient);
|
||||||
|
}
|
||||||
|
catch (ObjectDisposedException)
|
||||||
|
{
|
||||||
|
// socket was closed, no problem
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Debug.LogException(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sends discovery request from client
|
||||||
|
/// </summary>
|
||||||
|
public void BroadcastDiscoveryRequest()
|
||||||
|
{
|
||||||
|
if (clientUdpClient == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
IPEndPoint endPoint = new IPEndPoint(IPAddress.Broadcast, serverBroadcastListenPort);
|
||||||
|
|
||||||
|
using (PooledNetworkWriter writer = NetworkWriterPool.GetWriter())
|
||||||
|
{
|
||||||
|
writer.WriteInt64(secretHandshake);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Request request = GetRequest();
|
||||||
|
|
||||||
|
writer.Write(request);
|
||||||
|
|
||||||
|
ArraySegment<byte> data = writer.ToArraySegment();
|
||||||
|
|
||||||
|
clientUdpClient.SendAsync(data.Array, data.Count, endPoint);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
// It is ok if we can't broadcast to one of the addresses
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a message that will be broadcasted on the network to discover servers
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Override if you wish to include additional data in the discovery message
|
||||||
|
/// such as desired game mode, language, difficulty, etc... </remarks>
|
||||||
|
/// <returns>An instance of ServerRequest with data to be broadcasted</returns>
|
||||||
|
protected virtual Request GetRequest() => default;
|
||||||
|
|
||||||
|
async Task ReceiveGameBroadcastAsync(UdpClient udpClient)
|
||||||
|
{
|
||||||
|
// only proceed if there is available data in network buffer, or otherwise Receive() will block
|
||||||
|
// average time for UdpClient.Available : 10 us
|
||||||
|
|
||||||
|
UdpReceiveResult udpReceiveResult = await udpClient.ReceiveAsync();
|
||||||
|
|
||||||
|
using (PooledNetworkReader networkReader = NetworkReaderPool.GetReader(udpReceiveResult.Buffer))
|
||||||
|
{
|
||||||
|
if (networkReader.ReadInt64() != secretHandshake)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Response response = networkReader.Read<Response>();
|
||||||
|
|
||||||
|
ProcessResponse(response, udpReceiveResult.RemoteEndPoint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Process the answer from a server
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// A client receives a reply from a server, this method processes the
|
||||||
|
/// reply and raises an event
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="response">Response that came from the server</param>
|
||||||
|
/// <param name="endpoint">Address of the server that replied</param>
|
||||||
|
protected abstract void ProcessResponse(Response response, IPEndPoint endpoint);
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: b9971d60ce61f4e39b07cd9e7e0c68fa
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
95
Assets/Mirror/Components/Discovery/NetworkDiscoveryHUD.cs
Normal file
95
Assets/Mirror/Components/Discovery/NetworkDiscoveryHUD.cs
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace Mirror.Discovery
|
||||||
|
{
|
||||||
|
[DisallowMultipleComponent]
|
||||||
|
[AddComponentMenu("Network/NetworkDiscoveryHUD")]
|
||||||
|
[HelpURL("https://mirror-networking.com/docs/Components/NetworkDiscovery.html")]
|
||||||
|
[RequireComponent(typeof(NetworkDiscovery))]
|
||||||
|
public class NetworkDiscoveryHUD : MonoBehaviour
|
||||||
|
{
|
||||||
|
readonly Dictionary<long, ServerResponse> discoveredServers = new Dictionary<long, ServerResponse>();
|
||||||
|
Vector2 scrollViewPos = Vector2.zero;
|
||||||
|
|
||||||
|
public NetworkDiscovery networkDiscovery;
|
||||||
|
|
||||||
|
#if UNITY_EDITOR
|
||||||
|
void OnValidate()
|
||||||
|
{
|
||||||
|
if (networkDiscovery == null)
|
||||||
|
{
|
||||||
|
networkDiscovery = GetComponent<NetworkDiscovery>();
|
||||||
|
UnityEditor.Events.UnityEventTools.AddPersistentListener(networkDiscovery.OnServerFound, OnDiscoveredServer);
|
||||||
|
UnityEditor.Undo.RecordObjects(new Object[] { this, networkDiscovery }, "Set NetworkDiscovery");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
void OnGUI()
|
||||||
|
{
|
||||||
|
if (NetworkManager.singleton == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (NetworkServer.active || NetworkClient.active)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!NetworkClient.isConnected && !NetworkServer.active && !NetworkClient.active)
|
||||||
|
DrawGUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DrawGUI()
|
||||||
|
{
|
||||||
|
GUILayout.BeginHorizontal();
|
||||||
|
|
||||||
|
if (GUILayout.Button("Find Servers"))
|
||||||
|
{
|
||||||
|
discoveredServers.Clear();
|
||||||
|
networkDiscovery.StartDiscovery();
|
||||||
|
}
|
||||||
|
|
||||||
|
// LAN Host
|
||||||
|
if (GUILayout.Button("Start Host"))
|
||||||
|
{
|
||||||
|
discoveredServers.Clear();
|
||||||
|
NetworkManager.singleton.StartHost();
|
||||||
|
networkDiscovery.AdvertiseServer();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dedicated server
|
||||||
|
if (GUILayout.Button("Start Server"))
|
||||||
|
{
|
||||||
|
discoveredServers.Clear();
|
||||||
|
NetworkManager.singleton.StartServer();
|
||||||
|
|
||||||
|
networkDiscovery.AdvertiseServer();
|
||||||
|
}
|
||||||
|
|
||||||
|
GUILayout.EndHorizontal();
|
||||||
|
|
||||||
|
// show list of found server
|
||||||
|
|
||||||
|
GUILayout.Label($"Discovered Servers [{discoveredServers.Count}]:");
|
||||||
|
|
||||||
|
// servers
|
||||||
|
scrollViewPos = GUILayout.BeginScrollView(scrollViewPos);
|
||||||
|
|
||||||
|
foreach (ServerResponse info in discoveredServers.Values)
|
||||||
|
if (GUILayout.Button(info.EndPoint.Address.ToString()))
|
||||||
|
Connect(info);
|
||||||
|
|
||||||
|
GUILayout.EndScrollView();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Connect(ServerResponse info)
|
||||||
|
{
|
||||||
|
NetworkManager.singleton.StartClient(info.uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnDiscoveredServer(ServerResponse info)
|
||||||
|
{
|
||||||
|
// Note that you can check the versioning to decide if you can connect to the server or not using this method
|
||||||
|
discoveredServers[info.serverId] = info;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 88c37d3deca7a834d80cfd8d3cfcc510
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
4
Assets/Mirror/Components/Discovery/ServerRequest.cs
Normal file
4
Assets/Mirror/Components/Discovery/ServerRequest.cs
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
namespace Mirror.Discovery
|
||||||
|
{
|
||||||
|
public struct ServerRequest : NetworkMessage { }
|
||||||
|
}
|
11
Assets/Mirror/Components/Discovery/ServerRequest.cs.meta
Normal file
11
Assets/Mirror/Components/Discovery/ServerRequest.cs.meta
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: ea7254bf7b9454da4adad881d94cd141
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
18
Assets/Mirror/Components/Discovery/ServerResponse.cs
Normal file
18
Assets/Mirror/Components/Discovery/ServerResponse.cs
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
using System;
|
||||||
|
using System.Net;
|
||||||
|
|
||||||
|
namespace Mirror.Discovery
|
||||||
|
{
|
||||||
|
public struct ServerResponse : NetworkMessage
|
||||||
|
{
|
||||||
|
// The server that sent this
|
||||||
|
// this is a property so that it is not serialized, but the
|
||||||
|
// client fills this up after we receive it
|
||||||
|
public IPEndPoint EndPoint { get; set; }
|
||||||
|
|
||||||
|
public Uri uri;
|
||||||
|
|
||||||
|
// Prevent duplicate server appearance when a connection can be made via LAN on multiple NICs
|
||||||
|
public long serverId;
|
||||||
|
}
|
||||||
|
}
|
11
Assets/Mirror/Components/Discovery/ServerResponse.cs.meta
Normal file
11
Assets/Mirror/Components/Discovery/ServerResponse.cs.meta
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 36f97227fdf2d7a4e902db5bfc43039c
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
8
Assets/Mirror/Components/Experimental.meta
Normal file
8
Assets/Mirror/Components/Experimental.meta
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: bfbf2a1f2b300c5489dcab219ef2846e
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
@ -0,0 +1,93 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace Mirror.Experimental
|
||||||
|
{
|
||||||
|
[AddComponentMenu("Network/Experimental/NetworkLerpRigidbody")]
|
||||||
|
[HelpURL("https://mirror-networking.com/docs/Components/NetworkLerpRigidbody.html")]
|
||||||
|
public class NetworkLerpRigidbody : NetworkBehaviour
|
||||||
|
{
|
||||||
|
[Header("Settings")]
|
||||||
|
[SerializeField] internal Rigidbody target = null;
|
||||||
|
[Tooltip("How quickly current velocity approaches target velocity")]
|
||||||
|
[SerializeField] float lerpVelocityAmount = 0.5f;
|
||||||
|
[Tooltip("How quickly current position approaches target position")]
|
||||||
|
[SerializeField] float lerpPositionAmount = 0.5f;
|
||||||
|
|
||||||
|
[Tooltip("Set to true if moves come from owner client, set to false if moves always come from server")]
|
||||||
|
[SerializeField] bool clientAuthority = false;
|
||||||
|
|
||||||
|
float nextSyncTime;
|
||||||
|
|
||||||
|
|
||||||
|
[SyncVar()]
|
||||||
|
Vector3 targetVelocity;
|
||||||
|
|
||||||
|
[SyncVar()]
|
||||||
|
Vector3 targetPosition;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ignore value if is host or client with Authority
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
bool IgnoreSync => isServer || ClientWithAuthority;
|
||||||
|
|
||||||
|
bool ClientWithAuthority => clientAuthority && hasAuthority;
|
||||||
|
|
||||||
|
void OnValidate()
|
||||||
|
{
|
||||||
|
if (target == null)
|
||||||
|
{
|
||||||
|
target = GetComponent<Rigidbody>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Update()
|
||||||
|
{
|
||||||
|
if (isServer)
|
||||||
|
{
|
||||||
|
SyncToClients();
|
||||||
|
}
|
||||||
|
else if (ClientWithAuthority)
|
||||||
|
{
|
||||||
|
SendToServer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SyncToClients()
|
||||||
|
{
|
||||||
|
targetVelocity = target.velocity;
|
||||||
|
targetPosition = target.position;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SendToServer()
|
||||||
|
{
|
||||||
|
float now = Time.time;
|
||||||
|
if (now > nextSyncTime)
|
||||||
|
{
|
||||||
|
nextSyncTime = now + syncInterval;
|
||||||
|
CmdSendState(target.velocity, target.position);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Command]
|
||||||
|
private void CmdSendState(Vector3 velocity, Vector3 position)
|
||||||
|
{
|
||||||
|
target.velocity = velocity;
|
||||||
|
target.position = position;
|
||||||
|
targetVelocity = velocity;
|
||||||
|
targetPosition = position;
|
||||||
|
}
|
||||||
|
|
||||||
|
void FixedUpdate()
|
||||||
|
{
|
||||||
|
if (IgnoreSync) { return; }
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
// TODO does this also need to sync acceleration so and update velocity?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 7f032128052c95a46afb0ddd97d994cc
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
363
Assets/Mirror/Components/Experimental/NetworkRigidbody.cs
Normal file
363
Assets/Mirror/Components/Experimental/NetworkRigidbody.cs
Normal file
@ -0,0 +1,363 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace Mirror.Experimental
|
||||||
|
{
|
||||||
|
[AddComponentMenu("Network/Experimental/NetworkRigidbody")]
|
||||||
|
[HelpURL("https://mirror-networking.com/docs/Components/NetworkRigidbody.html")]
|
||||||
|
public class NetworkRigidbody : NetworkBehaviour
|
||||||
|
{
|
||||||
|
static readonly ILogger logger = LogFactory.GetLogger(typeof(NetworkRigidbody));
|
||||||
|
|
||||||
|
[Header("Settings")]
|
||||||
|
[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;
|
||||||
|
|
||||||
|
[Header("Velocity")]
|
||||||
|
|
||||||
|
[Tooltip("Syncs Velocity every SyncInterval")]
|
||||||
|
[SerializeField] bool syncVelocity = true;
|
||||||
|
|
||||||
|
[Tooltip("Set velocity to 0 each frame (only works if syncVelocity is false")]
|
||||||
|
[SerializeField] bool clearVelocity = false;
|
||||||
|
|
||||||
|
[Tooltip("Only Syncs Value if distance between previous and current is great than sensitivity")]
|
||||||
|
[SerializeField] float velocitySensitivity = 0.1f;
|
||||||
|
|
||||||
|
|
||||||
|
[Header("Angular Velocity")]
|
||||||
|
|
||||||
|
[Tooltip("Syncs AngularVelocity every SyncInterval")]
|
||||||
|
[SerializeField] bool syncAngularVelocity = true;
|
||||||
|
|
||||||
|
[Tooltip("Set angularVelocity to 0 each frame (only works if syncAngularVelocity is false")]
|
||||||
|
[SerializeField] bool clearAngularVelocity = false;
|
||||||
|
|
||||||
|
[Tooltip("Only Syncs Value if distance between previous and current is great than sensitivity")]
|
||||||
|
[SerializeField] float angularVelocitySensitivity = 0.1f;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Values sent on client with authoirty after they are sent to the server
|
||||||
|
/// </summary>
|
||||||
|
readonly ClientSyncState previousValue = new ClientSyncState();
|
||||||
|
|
||||||
|
void OnValidate()
|
||||||
|
{
|
||||||
|
if (target == null)
|
||||||
|
{
|
||||||
|
target = GetComponent<Rigidbody>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#region Sync vars
|
||||||
|
[SyncVar(hook = nameof(OnVelocityChanged))]
|
||||||
|
Vector3 velocity;
|
||||||
|
|
||||||
|
[SyncVar(hook = nameof(OnAngularVelocityChanged))]
|
||||||
|
Vector3 angularVelocity;
|
||||||
|
|
||||||
|
[SyncVar(hook = nameof(OnIsKinematicChanged))]
|
||||||
|
bool isKinematic;
|
||||||
|
|
||||||
|
[SyncVar(hook = nameof(OnUseGravityChanged))]
|
||||||
|
bool useGravity;
|
||||||
|
|
||||||
|
[SyncVar(hook = nameof(OnuDragChanged))]
|
||||||
|
float drag;
|
||||||
|
|
||||||
|
[SyncVar(hook = nameof(OnAngularDragChanged))]
|
||||||
|
float angularDrag;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ignore value if is host or client with Authority
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
bool IgnoreSync => isServer || ClientWithAuthority;
|
||||||
|
|
||||||
|
bool ClientWithAuthority => clientAuthority && hasAuthority;
|
||||||
|
|
||||||
|
void OnVelocityChanged(Vector3 _, Vector3 newValue)
|
||||||
|
{
|
||||||
|
if (IgnoreSync)
|
||||||
|
return;
|
||||||
|
|
||||||
|
target.velocity = newValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void OnAngularVelocityChanged(Vector3 _, Vector3 newValue)
|
||||||
|
{
|
||||||
|
if (IgnoreSync)
|
||||||
|
return;
|
||||||
|
|
||||||
|
target.angularVelocity = newValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnIsKinematicChanged(bool _, bool newValue)
|
||||||
|
{
|
||||||
|
if (IgnoreSync)
|
||||||
|
return;
|
||||||
|
|
||||||
|
target.isKinematic = newValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnUseGravityChanged(bool _, bool newValue)
|
||||||
|
{
|
||||||
|
if (IgnoreSync)
|
||||||
|
return;
|
||||||
|
|
||||||
|
target.useGravity = newValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnuDragChanged(float _, float newValue)
|
||||||
|
{
|
||||||
|
if (IgnoreSync)
|
||||||
|
return;
|
||||||
|
|
||||||
|
target.drag = newValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnAngularDragChanged(float _, float newValue)
|
||||||
|
{
|
||||||
|
if (IgnoreSync)
|
||||||
|
return;
|
||||||
|
|
||||||
|
target.angularDrag = newValue;
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
|
internal void Update()
|
||||||
|
{
|
||||||
|
if (isServer)
|
||||||
|
{
|
||||||
|
SyncToClients();
|
||||||
|
}
|
||||||
|
else if (ClientWithAuthority)
|
||||||
|
{
|
||||||
|
SendToServer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void FixedUpdate()
|
||||||
|
{
|
||||||
|
if (clearAngularVelocity && !syncAngularVelocity)
|
||||||
|
{
|
||||||
|
target.angularVelocity = Vector3.zero;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clearVelocity && !syncVelocity)
|
||||||
|
{
|
||||||
|
target.velocity = Vector3.zero;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates sync var values on server so that they sync to the client
|
||||||
|
/// </summary>
|
||||||
|
[Server]
|
||||||
|
void SyncToClients()
|
||||||
|
{
|
||||||
|
// only update if they have changed more than Sensitivity
|
||||||
|
|
||||||
|
Vector3 currentVelocity = syncVelocity ? target.velocity : default;
|
||||||
|
Vector3 currentAngularVelocity = syncAngularVelocity ? target.angularVelocity : default;
|
||||||
|
|
||||||
|
bool velocityChanged = syncVelocity && ((previousValue.velocity - currentVelocity).sqrMagnitude > velocitySensitivity * velocitySensitivity);
|
||||||
|
bool angularVelocityChanged = syncAngularVelocity && ((previousValue.angularVelocity - currentAngularVelocity).sqrMagnitude > angularVelocitySensitivity * angularVelocitySensitivity);
|
||||||
|
|
||||||
|
if (velocityChanged)
|
||||||
|
{
|
||||||
|
velocity = currentVelocity;
|
||||||
|
previousValue.velocity = currentVelocity;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (angularVelocityChanged)
|
||||||
|
{
|
||||||
|
angularVelocity = currentAngularVelocity;
|
||||||
|
previousValue.angularVelocity = currentAngularVelocity;
|
||||||
|
}
|
||||||
|
|
||||||
|
// other rigidbody settings
|
||||||
|
isKinematic = target.isKinematic;
|
||||||
|
useGravity = target.useGravity;
|
||||||
|
drag = target.drag;
|
||||||
|
angularDrag = target.angularDrag;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Uses Command to send values to server
|
||||||
|
/// </summary>
|
||||||
|
[Client]
|
||||||
|
void SendToServer()
|
||||||
|
{
|
||||||
|
if (!hasAuthority)
|
||||||
|
{
|
||||||
|
logger.LogWarning("SendToServer called without authority");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SendVelocity();
|
||||||
|
SendRigidBodySettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Client]
|
||||||
|
void SendVelocity()
|
||||||
|
{
|
||||||
|
float now = Time.time;
|
||||||
|
if (now < previousValue.nextSyncTime)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Vector3 currentVelocity = syncVelocity ? target.velocity : default;
|
||||||
|
Vector3 currentAngularVelocity = syncAngularVelocity ? target.angularVelocity : default;
|
||||||
|
|
||||||
|
bool velocityChanged = syncVelocity && ((previousValue.velocity - currentVelocity).sqrMagnitude > velocitySensitivity * velocitySensitivity);
|
||||||
|
bool angularVelocityChanged = syncAngularVelocity && ((previousValue.angularVelocity - currentAngularVelocity).sqrMagnitude > angularVelocitySensitivity * angularVelocitySensitivity);
|
||||||
|
|
||||||
|
// if angularVelocity has changed it is likely that velocity has also changed so just sync both values
|
||||||
|
// however if only velocity has changed just send velocity
|
||||||
|
if (angularVelocityChanged)
|
||||||
|
{
|
||||||
|
CmdSendVelocityAndAngular(currentVelocity, currentAngularVelocity);
|
||||||
|
previousValue.velocity = currentVelocity;
|
||||||
|
previousValue.angularVelocity = currentAngularVelocity;
|
||||||
|
}
|
||||||
|
else if (velocityChanged)
|
||||||
|
{
|
||||||
|
CmdSendVelocity(currentVelocity);
|
||||||
|
previousValue.velocity = currentVelocity;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// only update syncTime if either has changed
|
||||||
|
if (angularVelocityChanged || velocityChanged)
|
||||||
|
{
|
||||||
|
previousValue.nextSyncTime = now + syncInterval;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Client]
|
||||||
|
void SendRigidBodySettings()
|
||||||
|
{
|
||||||
|
// These shouldn't change often so it is ok to send in their own Command
|
||||||
|
if (previousValue.isKinematic != target.isKinematic)
|
||||||
|
{
|
||||||
|
CmdSendIsKinematic(target.isKinematic);
|
||||||
|
previousValue.isKinematic = target.isKinematic;
|
||||||
|
}
|
||||||
|
if (previousValue.useGravity != target.useGravity)
|
||||||
|
{
|
||||||
|
CmdSendUseGravity(target.useGravity);
|
||||||
|
previousValue.useGravity = target.useGravity;
|
||||||
|
}
|
||||||
|
if (previousValue.drag != target.drag)
|
||||||
|
{
|
||||||
|
CmdSendDrag(target.drag);
|
||||||
|
previousValue.drag = target.drag;
|
||||||
|
}
|
||||||
|
if (previousValue.angularDrag != target.angularDrag)
|
||||||
|
{
|
||||||
|
CmdSendAngularDrag(target.angularDrag);
|
||||||
|
previousValue.angularDrag = target.angularDrag;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called when only Velocity has changed on the client
|
||||||
|
/// </summary>
|
||||||
|
[Command]
|
||||||
|
void CmdSendVelocity(Vector3 velocity)
|
||||||
|
{
|
||||||
|
// Ignore messages from client if not in client authority mode
|
||||||
|
if (!clientAuthority)
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.velocity = velocity;
|
||||||
|
target.velocity = velocity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called when angularVelocity has changed on the client
|
||||||
|
/// </summary>
|
||||||
|
[Command]
|
||||||
|
void CmdSendVelocityAndAngular(Vector3 velocity, Vector3 angularVelocity)
|
||||||
|
{
|
||||||
|
// Ignore messages from client if not in client authority mode
|
||||||
|
if (!clientAuthority)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (syncVelocity)
|
||||||
|
{
|
||||||
|
this.velocity = velocity;
|
||||||
|
|
||||||
|
target.velocity = velocity;
|
||||||
|
|
||||||
|
}
|
||||||
|
this.angularVelocity = angularVelocity;
|
||||||
|
target.angularVelocity = angularVelocity;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Command]
|
||||||
|
void CmdSendIsKinematic(bool isKinematic)
|
||||||
|
{
|
||||||
|
// Ignore messages from client if not in client authority mode
|
||||||
|
if (!clientAuthority)
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.isKinematic = isKinematic;
|
||||||
|
target.isKinematic = isKinematic;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Command]
|
||||||
|
void CmdSendUseGravity(bool useGravity)
|
||||||
|
{
|
||||||
|
// Ignore messages from client if not in client authority mode
|
||||||
|
if (!clientAuthority)
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.useGravity = useGravity;
|
||||||
|
target.useGravity = useGravity;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Command]
|
||||||
|
void CmdSendDrag(float drag)
|
||||||
|
{
|
||||||
|
// Ignore messages from client if not in client authority mode
|
||||||
|
if (!clientAuthority)
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.drag = drag;
|
||||||
|
target.drag = drag;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Command]
|
||||||
|
void CmdSendAngularDrag(float angularDrag)
|
||||||
|
{
|
||||||
|
// Ignore messages from client if not in client authority mode
|
||||||
|
if (!clientAuthority)
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.angularDrag = angularDrag;
|
||||||
|
target.angularDrag = angularDrag;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// holds previously synced values
|
||||||
|
/// </summary>
|
||||||
|
public class ClientSyncState
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Next sync time that velocity will be synced, based on syncInterval.
|
||||||
|
/// </summary>
|
||||||
|
public float nextSyncTime;
|
||||||
|
public Vector3 velocity;
|
||||||
|
public Vector3 angularVelocity;
|
||||||
|
public bool isKinematic;
|
||||||
|
public bool useGravity;
|
||||||
|
public float drag;
|
||||||
|
public float angularDrag;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 83392ae5c1b731446909f252fd494ae4
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
362
Assets/Mirror/Components/Experimental/NetworkRigidbody2D.cs
Normal file
362
Assets/Mirror/Components/Experimental/NetworkRigidbody2D.cs
Normal file
@ -0,0 +1,362 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace Mirror.Experimental
|
||||||
|
{
|
||||||
|
[AddComponentMenu("Network/Experimental/NetworkRigidbody2D")]
|
||||||
|
public class NetworkRigidbody2D : NetworkBehaviour
|
||||||
|
{
|
||||||
|
static readonly ILogger logger = LogFactory.GetLogger(typeof(NetworkRigidbody2D));
|
||||||
|
|
||||||
|
[Header("Settings")]
|
||||||
|
[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;
|
||||||
|
|
||||||
|
[Header("Velocity")]
|
||||||
|
|
||||||
|
[Tooltip("Syncs Velocity every SyncInterval")]
|
||||||
|
[SerializeField] bool syncVelocity = true;
|
||||||
|
|
||||||
|
[Tooltip("Set velocity to 0 each frame (only works if syncVelocity is false")]
|
||||||
|
[SerializeField] bool clearVelocity = false;
|
||||||
|
|
||||||
|
[Tooltip("Only Syncs Value if distance between previous and current is great than sensitivity")]
|
||||||
|
[SerializeField] float velocitySensitivity = 0.1f;
|
||||||
|
|
||||||
|
|
||||||
|
[Header("Angular Velocity")]
|
||||||
|
|
||||||
|
[Tooltip("Syncs AngularVelocity every SyncInterval")]
|
||||||
|
[SerializeField] bool syncAngularVelocity = true;
|
||||||
|
|
||||||
|
[Tooltip("Set angularVelocity to 0 each frame (only works if syncAngularVelocity is false")]
|
||||||
|
[SerializeField] bool clearAngularVelocity = false;
|
||||||
|
|
||||||
|
[Tooltip("Only Syncs Value if distance between previous and current is great than sensitivity")]
|
||||||
|
[SerializeField] float angularVelocitySensitivity = 0.1f;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Values sent on client with authoirty after they are sent to the server
|
||||||
|
/// </summary>
|
||||||
|
readonly ClientSyncState previousValue = new ClientSyncState();
|
||||||
|
|
||||||
|
void OnValidate()
|
||||||
|
{
|
||||||
|
if (target == null)
|
||||||
|
{
|
||||||
|
target = GetComponent<Rigidbody2D>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#region Sync vars
|
||||||
|
[SyncVar(hook = nameof(OnVelocityChanged))]
|
||||||
|
Vector2 velocity;
|
||||||
|
|
||||||
|
[SyncVar(hook = nameof(OnAngularVelocityChanged))]
|
||||||
|
float angularVelocity;
|
||||||
|
|
||||||
|
[SyncVar(hook = nameof(OnIsKinematicChanged))]
|
||||||
|
bool isKinematic;
|
||||||
|
|
||||||
|
[SyncVar(hook = nameof(OnGravityScaleChanged))]
|
||||||
|
float gravityScale;
|
||||||
|
|
||||||
|
[SyncVar(hook = nameof(OnuDragChanged))]
|
||||||
|
float drag;
|
||||||
|
|
||||||
|
[SyncVar(hook = nameof(OnAngularDragChanged))]
|
||||||
|
float angularDrag;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ignore value if is host or client with Authority
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
bool IgnoreSync => isServer || ClientWithAuthority;
|
||||||
|
|
||||||
|
bool ClientWithAuthority => clientAuthority && hasAuthority;
|
||||||
|
|
||||||
|
void OnVelocityChanged(Vector2 _, Vector2 newValue)
|
||||||
|
{
|
||||||
|
if (IgnoreSync)
|
||||||
|
return;
|
||||||
|
|
||||||
|
target.velocity = newValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void OnAngularVelocityChanged(float _, float newValue)
|
||||||
|
{
|
||||||
|
if (IgnoreSync)
|
||||||
|
return;
|
||||||
|
|
||||||
|
target.angularVelocity = newValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnIsKinematicChanged(bool _, bool newValue)
|
||||||
|
{
|
||||||
|
if (IgnoreSync)
|
||||||
|
return;
|
||||||
|
|
||||||
|
target.isKinematic = newValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnGravityScaleChanged(float _, float newValue)
|
||||||
|
{
|
||||||
|
if (IgnoreSync)
|
||||||
|
return;
|
||||||
|
|
||||||
|
target.gravityScale = newValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnuDragChanged(float _, float newValue)
|
||||||
|
{
|
||||||
|
if (IgnoreSync)
|
||||||
|
return;
|
||||||
|
|
||||||
|
target.drag = newValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnAngularDragChanged(float _, float newValue)
|
||||||
|
{
|
||||||
|
if (IgnoreSync)
|
||||||
|
return;
|
||||||
|
|
||||||
|
target.angularDrag = newValue;
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
|
internal void Update()
|
||||||
|
{
|
||||||
|
if (isServer)
|
||||||
|
{
|
||||||
|
SyncToClients();
|
||||||
|
}
|
||||||
|
else if (ClientWithAuthority)
|
||||||
|
{
|
||||||
|
SendToServer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void FixedUpdate()
|
||||||
|
{
|
||||||
|
if (clearAngularVelocity && !syncAngularVelocity)
|
||||||
|
{
|
||||||
|
target.angularVelocity = 0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clearVelocity && !syncVelocity)
|
||||||
|
{
|
||||||
|
target.velocity = Vector2.zero;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates sync var values on server so that they sync to the client
|
||||||
|
/// </summary>
|
||||||
|
[Server]
|
||||||
|
void SyncToClients()
|
||||||
|
{
|
||||||
|
// only update if they have changed more than Sensitivity
|
||||||
|
|
||||||
|
Vector2 currentVelocity = syncVelocity ? target.velocity : default;
|
||||||
|
float currentAngularVelocity = syncAngularVelocity ? target.angularVelocity : default;
|
||||||
|
|
||||||
|
bool velocityChanged = syncVelocity && ((previousValue.velocity - currentVelocity).sqrMagnitude > velocitySensitivity * velocitySensitivity);
|
||||||
|
bool angularVelocityChanged = syncAngularVelocity && ((previousValue.angularVelocity - currentAngularVelocity) > angularVelocitySensitivity);
|
||||||
|
|
||||||
|
if (velocityChanged)
|
||||||
|
{
|
||||||
|
velocity = currentVelocity;
|
||||||
|
previousValue.velocity = currentVelocity;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (angularVelocityChanged)
|
||||||
|
{
|
||||||
|
angularVelocity = currentAngularVelocity;
|
||||||
|
previousValue.angularVelocity = currentAngularVelocity;
|
||||||
|
}
|
||||||
|
|
||||||
|
// other rigidbody settings
|
||||||
|
isKinematic = target.isKinematic;
|
||||||
|
gravityScale = target.gravityScale;
|
||||||
|
drag = target.drag;
|
||||||
|
angularDrag = target.angularDrag;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Uses Command to send values to server
|
||||||
|
/// </summary>
|
||||||
|
[Client]
|
||||||
|
void SendToServer()
|
||||||
|
{
|
||||||
|
if (!hasAuthority)
|
||||||
|
{
|
||||||
|
logger.LogWarning("SendToServer called without authority");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SendVelocity();
|
||||||
|
SendRigidBodySettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Client]
|
||||||
|
void SendVelocity()
|
||||||
|
{
|
||||||
|
float now = Time.time;
|
||||||
|
if (now < previousValue.nextSyncTime)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Vector2 currentVelocity = syncVelocity ? target.velocity : default;
|
||||||
|
float currentAngularVelocity = syncAngularVelocity ? target.angularVelocity : default;
|
||||||
|
|
||||||
|
bool velocityChanged = syncVelocity && ((previousValue.velocity - currentVelocity).sqrMagnitude > velocitySensitivity * velocitySensitivity);
|
||||||
|
bool angularVelocityChanged = syncAngularVelocity && previousValue.angularVelocity != currentAngularVelocity;//((previousValue.angularVelocity - currentAngularVelocity).sqrMagnitude > angularVelocitySensitivity * angularVelocitySensitivity);
|
||||||
|
|
||||||
|
// if angularVelocity has changed it is likely that velocity has also changed so just sync both values
|
||||||
|
// however if only velocity has changed just send velocity
|
||||||
|
if (angularVelocityChanged)
|
||||||
|
{
|
||||||
|
CmdSendVelocityAndAngular(currentVelocity, currentAngularVelocity);
|
||||||
|
previousValue.velocity = currentVelocity;
|
||||||
|
previousValue.angularVelocity = currentAngularVelocity;
|
||||||
|
}
|
||||||
|
else if (velocityChanged)
|
||||||
|
{
|
||||||
|
CmdSendVelocity(currentVelocity);
|
||||||
|
previousValue.velocity = currentVelocity;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// only update syncTime if either has changed
|
||||||
|
if (angularVelocityChanged || velocityChanged)
|
||||||
|
{
|
||||||
|
previousValue.nextSyncTime = now + syncInterval;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Client]
|
||||||
|
void SendRigidBodySettings()
|
||||||
|
{
|
||||||
|
// These shouldn't change often so it is ok to send in their own Command
|
||||||
|
if (previousValue.isKinematic != target.isKinematic)
|
||||||
|
{
|
||||||
|
CmdSendIsKinematic(target.isKinematic);
|
||||||
|
previousValue.isKinematic = target.isKinematic;
|
||||||
|
}
|
||||||
|
if (previousValue.gravityScale != target.gravityScale)
|
||||||
|
{
|
||||||
|
CmdChangeGravityScale(target.gravityScale);
|
||||||
|
previousValue.gravityScale = target.gravityScale;
|
||||||
|
}
|
||||||
|
if (previousValue.drag != target.drag)
|
||||||
|
{
|
||||||
|
CmdSendDrag(target.drag);
|
||||||
|
previousValue.drag = target.drag;
|
||||||
|
}
|
||||||
|
if (previousValue.angularDrag != target.angularDrag)
|
||||||
|
{
|
||||||
|
CmdSendAngularDrag(target.angularDrag);
|
||||||
|
previousValue.angularDrag = target.angularDrag;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called when only Velocity has changed on the client
|
||||||
|
/// </summary>
|
||||||
|
[Command]
|
||||||
|
void CmdSendVelocity(Vector2 velocity)
|
||||||
|
{
|
||||||
|
// Ignore messages from client if not in client authority mode
|
||||||
|
if (!clientAuthority)
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.velocity = velocity;
|
||||||
|
target.velocity = velocity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called when angularVelocity has changed on the client
|
||||||
|
/// </summary>
|
||||||
|
[Command]
|
||||||
|
void CmdSendVelocityAndAngular(Vector2 velocity, float angularVelocity)
|
||||||
|
{
|
||||||
|
// Ignore messages from client if not in client authority mode
|
||||||
|
if (!clientAuthority)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (syncVelocity)
|
||||||
|
{
|
||||||
|
this.velocity = velocity;
|
||||||
|
|
||||||
|
target.velocity = velocity;
|
||||||
|
|
||||||
|
}
|
||||||
|
this.angularVelocity = angularVelocity;
|
||||||
|
target.angularVelocity = angularVelocity;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Command]
|
||||||
|
void CmdSendIsKinematic(bool isKinematic)
|
||||||
|
{
|
||||||
|
// Ignore messages from client if not in client authority mode
|
||||||
|
if (!clientAuthority)
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.isKinematic = isKinematic;
|
||||||
|
target.isKinematic = isKinematic;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Command]
|
||||||
|
void CmdChangeGravityScale(float gravityScale)
|
||||||
|
{
|
||||||
|
// Ignore messages from client if not in client authority mode
|
||||||
|
if (!clientAuthority)
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.gravityScale = gravityScale;
|
||||||
|
target.gravityScale = gravityScale;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Command]
|
||||||
|
void CmdSendDrag(float drag)
|
||||||
|
{
|
||||||
|
// Ignore messages from client if not in client authority mode
|
||||||
|
if (!clientAuthority)
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.drag = drag;
|
||||||
|
target.drag = drag;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Command]
|
||||||
|
void CmdSendAngularDrag(float angularDrag)
|
||||||
|
{
|
||||||
|
// Ignore messages from client if not in client authority mode
|
||||||
|
if (!clientAuthority)
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.angularDrag = angularDrag;
|
||||||
|
target.angularDrag = angularDrag;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// holds previously synced values
|
||||||
|
/// </summary>
|
||||||
|
public class ClientSyncState
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Next sync time that velocity will be synced, based on syncInterval.
|
||||||
|
/// </summary>
|
||||||
|
public float nextSyncTime;
|
||||||
|
public Vector2 velocity;
|
||||||
|
public float angularVelocity;
|
||||||
|
public bool isKinematic;
|
||||||
|
public float gravityScale;
|
||||||
|
public float drag;
|
||||||
|
public float angularDrag;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: ab2cbc52526ea384ba280d13cd1a57b9
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
12
Assets/Mirror/Components/Experimental/NetworkTransform.cs
Normal file
12
Assets/Mirror/Components/Experimental/NetworkTransform.cs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace Mirror.Experimental
|
||||||
|
{
|
||||||
|
[DisallowMultipleComponent]
|
||||||
|
[AddComponentMenu("Network/Experimental/NetworkTransformExperimental")]
|
||||||
|
[HelpURL("https://mirror-networking.com/docs/Components/NetworkTransform.html")]
|
||||||
|
public class NetworkTransform : NetworkTransformBase
|
||||||
|
{
|
||||||
|
protected override Transform targetTransform => transform;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 741bbe11f5357b44593b15c0d11b16bd
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
529
Assets/Mirror/Components/Experimental/NetworkTransformBase.cs
Normal file
529
Assets/Mirror/Components/Experimental/NetworkTransformBase.cs
Normal file
@ -0,0 +1,529 @@
|
|||||||
|
// 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, targetTransform.localRotation, targetTransform.localScale);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ClientAuthorityUpdate()
|
||||||
|
{
|
||||||
|
if (!isServer && HasEitherMovedRotatedScaled())
|
||||||
|
{
|
||||||
|
// serialize
|
||||||
|
// local position/rotation for VR support
|
||||||
|
// send to server
|
||||||
|
CmdClientToServerSync(targetTransform.localPosition, 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]
|
||||||
|
void CmdClientToServerSync(Vector3 position, Quaternion rotation, Vector3 scale)
|
||||||
|
{
|
||||||
|
// Ignore messages from client if not in client authority mode
|
||||||
|
if (!clientAuthority)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// deserialize payload
|
||||||
|
SetGoal(position, rotation, 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, rotation, scale);
|
||||||
|
}
|
||||||
|
|
||||||
|
[ClientRpc]
|
||||||
|
void RpcMove(Vector3 position, Quaternion rotation, Vector3 scale)
|
||||||
|
{
|
||||||
|
if (hasAuthority && excludeOwnerUpdate) return;
|
||||||
|
|
||||||
|
if (!isServer)
|
||||||
|
SetGoal(position, rotation, 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, 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]
|
||||||
|
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 doesnt 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
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: ea7c690c4fbf8c4439726f4c62eda6d3
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
@ -0,0 +1,18 @@
|
|||||||
|
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 recieved values.</para>
|
||||||
|
/// </summary>
|
||||||
|
[AddComponentMenu("Network/Experimental/NetworkTransformChildExperimentalExperimental")]
|
||||||
|
[HelpURL("https://mirror-networking.com/docs/Components/NetworkTransformChild.html")]
|
||||||
|
public class NetworkTransformChild : NetworkTransformBase
|
||||||
|
{
|
||||||
|
[Header("Target")]
|
||||||
|
public Transform target;
|
||||||
|
|
||||||
|
protected override Transform targetTransform => target;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: f65214da13a861f4a8ae309d3daea1c6
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
14
Assets/Mirror/Components/Mirror.Components.asmdef
Normal file
14
Assets/Mirror/Components/Mirror.Components.asmdef
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"name": "Mirror.Components",
|
||||||
|
"references": [
|
||||||
|
"Mirror"
|
||||||
|
],
|
||||||
|
"optionalUnityReferences": [],
|
||||||
|
"includePlatforms": [],
|
||||||
|
"excludePlatforms": [],
|
||||||
|
"allowUnsafeCode": false,
|
||||||
|
"overrideReferences": false,
|
||||||
|
"precompiledReferences": [],
|
||||||
|
"autoReferenced": true,
|
||||||
|
"defineConstraints": []
|
||||||
|
}
|
7
Assets/Mirror/Components/Mirror.Components.asmdef.meta
Normal file
7
Assets/Mirror/Components/Mirror.Components.asmdef.meta
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 72872094b21c16e48b631b2224833d49
|
||||||
|
AssemblyDefinitionImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
636
Assets/Mirror/Components/NetworkAnimator.cs
Normal file
636
Assets/Mirror/Components/NetworkAnimator.cs
Normal file
@ -0,0 +1,636 @@
|
|||||||
|
using System.Linq;
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.Serialization;
|
||||||
|
|
||||||
|
namespace Mirror
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A component to synchronize Mecanim animation states for networked objects.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>The animation of game objects can be networked by this component. There are two models of authority for networked movement:</para>
|
||||||
|
/// <para>If the object has authority on the client, then it should be animated locally on the owning client. The animation state information will be sent from the owning client to the server, then broadcast to all of the other clients. This is common for player objects.</para>
|
||||||
|
/// <para>If the object has authority on the server, then it should be animated on the server and state information will be sent to all clients. This is common for objects not related to a specific client, such as an enemy unit.</para>
|
||||||
|
/// <para>The NetworkAnimator synchronizes all animation parameters of the selected Animator. It does not automatically sychronize triggers. The function SetTrigger can by used by an object with authority to fire an animation trigger on other clients.</para>
|
||||||
|
/// </remarks>
|
||||||
|
[AddComponentMenu("Network/NetworkAnimator")]
|
||||||
|
[RequireComponent(typeof(NetworkIdentity))]
|
||||||
|
[HelpURL("https://mirror-networking.com/docs/Components/NetworkAnimator.html")]
|
||||||
|
public class NetworkAnimator : NetworkBehaviour
|
||||||
|
{
|
||||||
|
static readonly ILogger logger = LogFactory.GetLogger(typeof(NetworkAnimator));
|
||||||
|
|
||||||
|
[Header("Authority")]
|
||||||
|
[Tooltip("Set to true if animations come from owner client, set to false if animations always come from server")]
|
||||||
|
public bool clientAuthority;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The animator component to synchronize.
|
||||||
|
/// </summary>
|
||||||
|
[FormerlySerializedAs("m_Animator")]
|
||||||
|
[Header("Animator")]
|
||||||
|
[Tooltip("Animator that will have parameters synchronized")]
|
||||||
|
public Animator animator;
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Syncs animator.speed
|
||||||
|
/// </summary>
|
||||||
|
[SyncVar(hook = nameof(onAnimatorSpeedChanged))]
|
||||||
|
float animatorSpeed;
|
||||||
|
float previousSpeed;
|
||||||
|
|
||||||
|
// Note: not an object[] array because otherwise initialization is real annoying
|
||||||
|
int[] lastIntParameters;
|
||||||
|
float[] lastFloatParameters;
|
||||||
|
bool[] lastBoolParameters;
|
||||||
|
AnimatorControllerParameter[] parameters;
|
||||||
|
|
||||||
|
// multiple layers
|
||||||
|
int[] animationHash;
|
||||||
|
int[] transitionHash;
|
||||||
|
float[] layerWeight;
|
||||||
|
float nextSendTime;
|
||||||
|
|
||||||
|
bool SendMessagesAllowed
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (isServer)
|
||||||
|
{
|
||||||
|
if (!clientAuthority)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
// This is a special case where we have client authority but we have not assigned the client who has
|
||||||
|
// authority over it, no animator data will be sent over the network by the server.
|
||||||
|
//
|
||||||
|
// So we check here for a connectionToClient and if it is null we will
|
||||||
|
// let the server send animation data until we receive an owner.
|
||||||
|
if (netIdentity != null && netIdentity.connectionToClient == null)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (hasAuthority && clientAuthority);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Awake()
|
||||||
|
{
|
||||||
|
// store the animator parameters in a variable - the "Animator.parameters" getter allocates
|
||||||
|
// a new parameter array every time it is accessed so we should avoid doing it in a loop
|
||||||
|
parameters = animator.parameters
|
||||||
|
.Where(par => !animator.IsParameterControlledByCurve(par.nameHash))
|
||||||
|
.ToArray();
|
||||||
|
lastIntParameters = new int[parameters.Length];
|
||||||
|
lastFloatParameters = new float[parameters.Length];
|
||||||
|
lastBoolParameters = new bool[parameters.Length];
|
||||||
|
|
||||||
|
animationHash = new int[animator.layerCount];
|
||||||
|
transitionHash = new int[animator.layerCount];
|
||||||
|
layerWeight = new float[animator.layerCount];
|
||||||
|
}
|
||||||
|
|
||||||
|
void FixedUpdate()
|
||||||
|
{
|
||||||
|
if (!SendMessagesAllowed)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!animator.enabled)
|
||||||
|
return;
|
||||||
|
|
||||||
|
CheckSendRate();
|
||||||
|
|
||||||
|
for (int i = 0; i < animator.layerCount; i++)
|
||||||
|
{
|
||||||
|
int stateHash;
|
||||||
|
float normalizedTime;
|
||||||
|
if (!CheckAnimStateChanged(out stateHash, out normalizedTime, i))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
using (PooledNetworkWriter writer = NetworkWriterPool.GetWriter())
|
||||||
|
{
|
||||||
|
WriteParameters(writer);
|
||||||
|
SendAnimationMessage(stateHash, normalizedTime, i, layerWeight[i], writer.ToArray());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CheckSpeed();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CheckSpeed()
|
||||||
|
{
|
||||||
|
float newSpeed = animator.speed;
|
||||||
|
if (Mathf.Abs(previousSpeed - newSpeed) > 0.001f)
|
||||||
|
{
|
||||||
|
previousSpeed = newSpeed;
|
||||||
|
if (isServer)
|
||||||
|
{
|
||||||
|
animatorSpeed = newSpeed;
|
||||||
|
}
|
||||||
|
else if (ClientScene.readyConnection != null)
|
||||||
|
{
|
||||||
|
CmdSetAnimatorSpeed(newSpeed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CmdSetAnimatorSpeed(float newSpeed)
|
||||||
|
{
|
||||||
|
// set animator
|
||||||
|
animator.speed = newSpeed;
|
||||||
|
animatorSpeed = newSpeed;
|
||||||
|
}
|
||||||
|
|
||||||
|
void onAnimatorSpeedChanged(float _, float value)
|
||||||
|
{
|
||||||
|
// skip if host or client with authoity
|
||||||
|
// they will have already set the speed so dont set again
|
||||||
|
if (isServer || (hasAuthority && clientAuthority))
|
||||||
|
return;
|
||||||
|
|
||||||
|
animator.speed = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CheckAnimStateChanged(out int stateHash, out float normalizedTime, int layerId)
|
||||||
|
{
|
||||||
|
bool change = false;
|
||||||
|
stateHash = 0;
|
||||||
|
normalizedTime = 0;
|
||||||
|
|
||||||
|
float lw = animator.GetLayerWeight(layerId);
|
||||||
|
if (Mathf.Abs(lw - layerWeight[layerId]) > 0.001f)
|
||||||
|
{
|
||||||
|
layerWeight[layerId] = lw;
|
||||||
|
change = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (animator.IsInTransition(layerId))
|
||||||
|
{
|
||||||
|
AnimatorTransitionInfo tt = animator.GetAnimatorTransitionInfo(layerId);
|
||||||
|
if (tt.fullPathHash != transitionHash[layerId])
|
||||||
|
{
|
||||||
|
// first time in this transition
|
||||||
|
transitionHash[layerId] = tt.fullPathHash;
|
||||||
|
animationHash[layerId] = 0;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return change;
|
||||||
|
}
|
||||||
|
|
||||||
|
AnimatorStateInfo st = animator.GetCurrentAnimatorStateInfo(layerId);
|
||||||
|
if (st.fullPathHash != animationHash[layerId])
|
||||||
|
{
|
||||||
|
// first time in this animation state
|
||||||
|
if (animationHash[layerId] != 0)
|
||||||
|
{
|
||||||
|
// came from another animation directly - from Play()
|
||||||
|
stateHash = st.fullPathHash;
|
||||||
|
normalizedTime = st.normalizedTime;
|
||||||
|
}
|
||||||
|
transitionHash[layerId] = 0;
|
||||||
|
animationHash[layerId] = st.fullPathHash;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return change;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CheckSendRate()
|
||||||
|
{
|
||||||
|
float now = Time.time;
|
||||||
|
if (SendMessagesAllowed && syncInterval >= 0 && now > nextSendTime)
|
||||||
|
{
|
||||||
|
nextSendTime = now + syncInterval;
|
||||||
|
|
||||||
|
using (PooledNetworkWriter writer = NetworkWriterPool.GetWriter())
|
||||||
|
{
|
||||||
|
if (WriteParameters(writer))
|
||||||
|
SendAnimationParametersMessage(writer.ToArray());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SendAnimationMessage(int stateHash, float normalizedTime, int layerId, float weight, byte[] parameters)
|
||||||
|
{
|
||||||
|
if (isServer)
|
||||||
|
{
|
||||||
|
RpcOnAnimationClientMessage(stateHash, normalizedTime, layerId, weight, parameters);
|
||||||
|
}
|
||||||
|
else if (ClientScene.readyConnection != null)
|
||||||
|
{
|
||||||
|
CmdOnAnimationServerMessage(stateHash, normalizedTime, layerId, weight, parameters);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SendAnimationParametersMessage(byte[] parameters)
|
||||||
|
{
|
||||||
|
if (isServer)
|
||||||
|
{
|
||||||
|
RpcOnAnimationParametersClientMessage(parameters);
|
||||||
|
}
|
||||||
|
else if (ClientScene.readyConnection != null)
|
||||||
|
{
|
||||||
|
CmdOnAnimationParametersServerMessage(parameters);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void HandleAnimMsg(int stateHash, float normalizedTime, int layerId, float weight, NetworkReader reader)
|
||||||
|
{
|
||||||
|
if (hasAuthority && clientAuthority)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// usually transitions will be triggered by parameters, if not, play anims directly.
|
||||||
|
// NOTE: this plays "animations", not transitions, so any transitions will be skipped.
|
||||||
|
// NOTE: there is no API to play a transition(?)
|
||||||
|
if (stateHash != 0 && animator.enabled)
|
||||||
|
{
|
||||||
|
animator.Play(stateHash, layerId, normalizedTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
animator.SetLayerWeight(layerId, weight);
|
||||||
|
|
||||||
|
ReadParameters(reader);
|
||||||
|
}
|
||||||
|
|
||||||
|
void HandleAnimParamsMsg(NetworkReader reader)
|
||||||
|
{
|
||||||
|
if (hasAuthority && clientAuthority)
|
||||||
|
return;
|
||||||
|
|
||||||
|
ReadParameters(reader);
|
||||||
|
}
|
||||||
|
|
||||||
|
void HandleAnimTriggerMsg(int hash)
|
||||||
|
{
|
||||||
|
if (animator.enabled)
|
||||||
|
animator.SetTrigger(hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
void HandleAnimResetTriggerMsg(int hash)
|
||||||
|
{
|
||||||
|
if (animator.enabled)
|
||||||
|
animator.ResetTrigger(hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
ulong NextDirtyBits()
|
||||||
|
{
|
||||||
|
ulong dirtyBits = 0;
|
||||||
|
for (int i = 0; i < parameters.Length; i++)
|
||||||
|
{
|
||||||
|
AnimatorControllerParameter par = parameters[i];
|
||||||
|
bool changed = false;
|
||||||
|
if (par.type == AnimatorControllerParameterType.Int)
|
||||||
|
{
|
||||||
|
int newIntValue = animator.GetInteger(par.nameHash);
|
||||||
|
changed = newIntValue != lastIntParameters[i];
|
||||||
|
if (changed)
|
||||||
|
lastIntParameters[i] = newIntValue;
|
||||||
|
}
|
||||||
|
else if (par.type == AnimatorControllerParameterType.Float)
|
||||||
|
{
|
||||||
|
float newFloatValue = animator.GetFloat(par.nameHash);
|
||||||
|
changed = Mathf.Abs(newFloatValue - lastFloatParameters[i]) > 0.001f;
|
||||||
|
// only set lastValue if it was changed, otherwise value could slowly drift within the 0.001f limit each frame
|
||||||
|
if (changed)
|
||||||
|
lastFloatParameters[i] = newFloatValue;
|
||||||
|
}
|
||||||
|
else if (par.type == AnimatorControllerParameterType.Bool)
|
||||||
|
{
|
||||||
|
bool newBoolValue = animator.GetBool(par.nameHash);
|
||||||
|
changed = newBoolValue != lastBoolParameters[i];
|
||||||
|
if (changed)
|
||||||
|
lastBoolParameters[i] = newBoolValue;
|
||||||
|
}
|
||||||
|
if (changed)
|
||||||
|
{
|
||||||
|
dirtyBits |= 1ul << i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dirtyBits;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool WriteParameters(NetworkWriter writer, bool forceAll = false)
|
||||||
|
{
|
||||||
|
ulong dirtyBits = forceAll ? (~0ul) : NextDirtyBits();
|
||||||
|
writer.WritePackedUInt64(dirtyBits);
|
||||||
|
for (int i = 0; i < parameters.Length; i++)
|
||||||
|
{
|
||||||
|
if ((dirtyBits & (1ul << i)) == 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
AnimatorControllerParameter par = parameters[i];
|
||||||
|
if (par.type == AnimatorControllerParameterType.Int)
|
||||||
|
{
|
||||||
|
int newIntValue = animator.GetInteger(par.nameHash);
|
||||||
|
writer.WritePackedInt32(newIntValue);
|
||||||
|
}
|
||||||
|
else if (par.type == AnimatorControllerParameterType.Float)
|
||||||
|
{
|
||||||
|
float newFloatValue = animator.GetFloat(par.nameHash);
|
||||||
|
writer.WriteSingle(newFloatValue);
|
||||||
|
}
|
||||||
|
else if (par.type == AnimatorControllerParameterType.Bool)
|
||||||
|
{
|
||||||
|
bool newBoolValue = animator.GetBool(par.nameHash);
|
||||||
|
writer.WriteBoolean(newBoolValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dirtyBits != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ReadParameters(NetworkReader reader)
|
||||||
|
{
|
||||||
|
bool animatorEnabled = animator.enabled;
|
||||||
|
// need to read values from NetworkReader even if animator is disabled
|
||||||
|
|
||||||
|
ulong dirtyBits = reader.ReadPackedUInt64();
|
||||||
|
for (int i = 0; i < parameters.Length; i++)
|
||||||
|
{
|
||||||
|
if ((dirtyBits & (1ul << i)) == 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
AnimatorControllerParameter par = parameters[i];
|
||||||
|
if (par.type == AnimatorControllerParameterType.Int)
|
||||||
|
{
|
||||||
|
int newIntValue = reader.ReadPackedInt32();
|
||||||
|
if (animatorEnabled)
|
||||||
|
animator.SetInteger(par.nameHash, newIntValue);
|
||||||
|
}
|
||||||
|
else if (par.type == AnimatorControllerParameterType.Float)
|
||||||
|
{
|
||||||
|
float newFloatValue = reader.ReadSingle();
|
||||||
|
if (animatorEnabled)
|
||||||
|
animator.SetFloat(par.nameHash, newFloatValue);
|
||||||
|
}
|
||||||
|
else if (par.type == AnimatorControllerParameterType.Bool)
|
||||||
|
{
|
||||||
|
bool newBoolValue = reader.ReadBoolean();
|
||||||
|
if (animatorEnabled)
|
||||||
|
animator.SetBool(par.nameHash, newBoolValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Custom Serialization
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="writer"></param>
|
||||||
|
/// <param name="initialState"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public override bool OnSerialize(NetworkWriter writer, bool initialState)
|
||||||
|
{
|
||||||
|
bool changed = base.OnSerialize(writer, initialState);
|
||||||
|
if (initialState)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < animator.layerCount; i++)
|
||||||
|
{
|
||||||
|
if (animator.IsInTransition(i))
|
||||||
|
{
|
||||||
|
AnimatorStateInfo st = animator.GetNextAnimatorStateInfo(i);
|
||||||
|
writer.WriteInt32(st.fullPathHash);
|
||||||
|
writer.WriteSingle(st.normalizedTime);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
AnimatorStateInfo st = animator.GetCurrentAnimatorStateInfo(i);
|
||||||
|
writer.WriteInt32(st.fullPathHash);
|
||||||
|
writer.WriteSingle(st.normalizedTime);
|
||||||
|
}
|
||||||
|
writer.WriteSingle(animator.GetLayerWeight(i));
|
||||||
|
}
|
||||||
|
WriteParameters(writer, initialState);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Custom Deserialization
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="reader"></param>
|
||||||
|
/// <param name="initialState"></param>
|
||||||
|
public override void OnDeserialize(NetworkReader reader, bool initialState)
|
||||||
|
{
|
||||||
|
base.OnDeserialize(reader, initialState);
|
||||||
|
if (initialState)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < animator.layerCount; i++)
|
||||||
|
{
|
||||||
|
int stateHash = reader.ReadInt32();
|
||||||
|
float normalizedTime = reader.ReadSingle();
|
||||||
|
animator.SetLayerWeight(i, reader.ReadSingle());
|
||||||
|
animator.Play(stateHash, i, normalizedTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
ReadParameters(reader);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Causes an animation trigger to be invoked for a networked object.
|
||||||
|
/// <para>If local authority is set, and this is called from the client, then the trigger will be invoked on the server and all clients. If not, then this is called on the server, and the trigger will be called on all clients.</para>
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="triggerName">Name of trigger.</param>
|
||||||
|
public void SetTrigger(string triggerName)
|
||||||
|
{
|
||||||
|
SetTrigger(Animator.StringToHash(triggerName));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Causes an animation trigger to be invoked for a networked object.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="hash">Hash id of trigger (from the Animator).</param>
|
||||||
|
public void SetTrigger(int hash)
|
||||||
|
{
|
||||||
|
if (clientAuthority)
|
||||||
|
{
|
||||||
|
if (!isClient)
|
||||||
|
{
|
||||||
|
logger.LogWarning("Tried to set animation in the server for a client-controlled animator");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasAuthority)
|
||||||
|
{
|
||||||
|
logger.LogWarning("Only the client with authority can set animations");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ClientScene.readyConnection != null)
|
||||||
|
CmdOnAnimationTriggerServerMessage(hash);
|
||||||
|
|
||||||
|
// call on client right away
|
||||||
|
HandleAnimTriggerMsg(hash);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (!isServer)
|
||||||
|
{
|
||||||
|
logger.LogWarning("Tried to set animation in the client for a server-controlled animator");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
HandleAnimTriggerMsg(hash);
|
||||||
|
RpcOnAnimationTriggerClientMessage(hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Causes an animation trigger to be reset for a networked object.
|
||||||
|
/// <para>If local authority is set, and this is called from the client, then the trigger will be reset on the server and all clients. If not, then this is called on the server, and the trigger will be reset on all clients.</para>
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="triggerName">Name of trigger.</param>
|
||||||
|
public void ResetTrigger(string triggerName)
|
||||||
|
{
|
||||||
|
ResetTrigger(Animator.StringToHash(triggerName));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Causes an animation trigger to be reset for a networked object.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="hash">Hash id of trigger (from the Animator).</param>
|
||||||
|
public void ResetTrigger(int hash)
|
||||||
|
{
|
||||||
|
if (clientAuthority)
|
||||||
|
{
|
||||||
|
if (!isClient)
|
||||||
|
{
|
||||||
|
logger.LogWarning("Tried to reset animation in the server for a client-controlled animator");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasAuthority)
|
||||||
|
{
|
||||||
|
logger.LogWarning("Only the client with authority can reset animations");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ClientScene.readyConnection != null)
|
||||||
|
CmdOnAnimationResetTriggerServerMessage(hash);
|
||||||
|
|
||||||
|
// call on client right away
|
||||||
|
HandleAnimResetTriggerMsg(hash);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (!isServer)
|
||||||
|
{
|
||||||
|
logger.LogWarning("Tried to reset animation in the client for a server-controlled animator");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
HandleAnimResetTriggerMsg(hash);
|
||||||
|
RpcOnAnimationResetTriggerClientMessage(hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#region server message handlers
|
||||||
|
|
||||||
|
[Command]
|
||||||
|
void CmdOnAnimationServerMessage(int stateHash, float normalizedTime, int layerId, float weight, byte[] parameters)
|
||||||
|
{
|
||||||
|
// Ignore messages from client if not in client authority mode
|
||||||
|
if (!clientAuthority)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (logger.LogEnabled()) logger.Log("OnAnimationMessage for netId=" + netId);
|
||||||
|
|
||||||
|
// handle and broadcast
|
||||||
|
using (PooledNetworkReader networkReader = NetworkReaderPool.GetReader(parameters))
|
||||||
|
{
|
||||||
|
HandleAnimMsg(stateHash, normalizedTime, layerId, weight, networkReader);
|
||||||
|
RpcOnAnimationClientMessage(stateHash, normalizedTime, layerId, weight, parameters);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Command]
|
||||||
|
void CmdOnAnimationParametersServerMessage(byte[] parameters)
|
||||||
|
{
|
||||||
|
// Ignore messages from client if not in client authority mode
|
||||||
|
if (!clientAuthority)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// handle and broadcast
|
||||||
|
using (PooledNetworkReader networkReader = NetworkReaderPool.GetReader(parameters))
|
||||||
|
{
|
||||||
|
HandleAnimParamsMsg(networkReader);
|
||||||
|
RpcOnAnimationParametersClientMessage(parameters);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Command]
|
||||||
|
void CmdOnAnimationTriggerServerMessage(int hash)
|
||||||
|
{
|
||||||
|
// Ignore messages from client if not in client authority mode
|
||||||
|
if (!clientAuthority)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// handle and broadcast
|
||||||
|
// host should have already the trigger
|
||||||
|
bool isHostOwner = isClient && hasAuthority;
|
||||||
|
if (!isHostOwner)
|
||||||
|
{
|
||||||
|
HandleAnimTriggerMsg(hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
RpcOnAnimationTriggerClientMessage(hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Command]
|
||||||
|
void CmdOnAnimationResetTriggerServerMessage(int hash)
|
||||||
|
{
|
||||||
|
// Ignore messages from client if not in client authority mode
|
||||||
|
if (!clientAuthority)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// handle and broadcast
|
||||||
|
// host should have already the trigger
|
||||||
|
bool isHostOwner = isClient && hasAuthority;
|
||||||
|
if (!isHostOwner)
|
||||||
|
{
|
||||||
|
HandleAnimResetTriggerMsg(hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
RpcOnAnimationResetTriggerClientMessage(hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region client message handlers
|
||||||
|
|
||||||
|
[ClientRpc]
|
||||||
|
void RpcOnAnimationClientMessage(int stateHash, float normalizedTime, int layerId, float weight, byte[] parameters)
|
||||||
|
{
|
||||||
|
using (PooledNetworkReader networkReader = NetworkReaderPool.GetReader(parameters))
|
||||||
|
HandleAnimMsg(stateHash, normalizedTime, layerId, weight, networkReader);
|
||||||
|
}
|
||||||
|
|
||||||
|
[ClientRpc]
|
||||||
|
void RpcOnAnimationParametersClientMessage(byte[] parameters)
|
||||||
|
{
|
||||||
|
using (PooledNetworkReader networkReader = NetworkReaderPool.GetReader(parameters))
|
||||||
|
HandleAnimParamsMsg(networkReader);
|
||||||
|
}
|
||||||
|
|
||||||
|
[ClientRpc]
|
||||||
|
void RpcOnAnimationTriggerClientMessage(int hash)
|
||||||
|
{
|
||||||
|
// host/owner handles this before it is sent
|
||||||
|
if (isServer || (clientAuthority && hasAuthority)) return;
|
||||||
|
|
||||||
|
HandleAnimTriggerMsg(hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
[ClientRpc]
|
||||||
|
void RpcOnAnimationResetTriggerClientMessage(int hash)
|
||||||
|
{
|
||||||
|
// host/owner handles this before it is sent
|
||||||
|
if (isServer || (clientAuthority && hasAuthority)) return;
|
||||||
|
|
||||||
|
HandleAnimResetTriggerMsg(hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
}
|
11
Assets/Mirror/Components/NetworkAnimator.cs.meta
Normal file
11
Assets/Mirror/Components/NetworkAnimator.cs.meta
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 7f6f3bf89aa97405989c802ba270f815
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
18
Assets/Mirror/Components/NetworkLobbyManager.cs
Normal file
18
Assets/Mirror/Components/NetworkLobbyManager.cs
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
using System;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace Mirror
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// This is a specialized NetworkManager that includes a networked lobby.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>The lobby has slots that track the joined players, and a maximum player count that is enforced. It requires that the NetworkLobbyPlayer component be on the lobby player objects.</para>
|
||||||
|
/// <para>NetworkLobbyManager is derived from NetworkManager, and so it implements many of the virtual functions provided by the NetworkManager class. To avoid accidentally replacing functionality of the NetworkLobbyManager, there are new virtual functions on the NetworkLobbyManager that begin with "OnLobby". These should be used on classes derived from NetworkLobbyManager instead of the virtual functions on NetworkManager.</para>
|
||||||
|
/// <para>The OnLobby*() functions have empty implementations on the NetworkLobbyManager base class, so the base class functions do not have to be called.</para>
|
||||||
|
/// </remarks>
|
||||||
|
[AddComponentMenu("Network/NetworkLobbyManager")]
|
||||||
|
[HelpURL("https://mirror-networking.com/docs/Components/NetworkRoomManager.html")]
|
||||||
|
[Obsolete("Use / inherit from NetworkRoomManager instead")]
|
||||||
|
public class NetworkLobbyManager : NetworkRoomManager { }
|
||||||
|
}
|
11
Assets/Mirror/Components/NetworkLobbyManager.cs.meta
Normal file
11
Assets/Mirror/Components/NetworkLobbyManager.cs.meta
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: a4c96e6dd99826849ab1431f94547141
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
15
Assets/Mirror/Components/NetworkLobbyPlayer.cs
Normal file
15
Assets/Mirror/Components/NetworkLobbyPlayer.cs
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
using System;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace Mirror
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// This component works in conjunction with the NetworkLobbyManager to make up the multiplayer lobby system.
|
||||||
|
/// <para>The LobbyPrefab object of the NetworkLobbyManager must have this component on it. This component holds basic lobby player data required for the lobby to function. Game specific data for lobby players can be put in other components on the LobbyPrefab or in scripts derived from NetworkLobbyPlayer.</para>
|
||||||
|
/// </summary>
|
||||||
|
[DisallowMultipleComponent]
|
||||||
|
[AddComponentMenu("Network/NetworkLobbyPlayer")]
|
||||||
|
[HelpURL("https://mirror-networking.com/docs/Components/NetworkRoomPlayer.html")]
|
||||||
|
[Obsolete("Use / inherit from NetworkRoomPlayer instead")]
|
||||||
|
public class NetworkLobbyPlayer : NetworkRoomPlayer { }
|
||||||
|
}
|
11
Assets/Mirror/Components/NetworkLobbyPlayer.cs.meta
Normal file
11
Assets/Mirror/Components/NetworkLobbyPlayer.cs.meta
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 777a368af85f2e84da7ea5666581921b
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
132
Assets/Mirror/Components/NetworkMatchChecker.cs
Normal file
132
Assets/Mirror/Components/NetworkMatchChecker.cs
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
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>
|
||||||
|
[DisallowMultipleComponent]
|
||||||
|
[AddComponentMenu("Network/NetworkMatchChecker")]
|
||||||
|
[RequireComponent(typeof(NetworkIdentity))]
|
||||||
|
[HelpURL("https://mirror-networking.com/docs/Components/NetworkMatchChecker.html")]
|
||||||
|
public class NetworkMatchChecker : NetworkVisibility
|
||||||
|
{
|
||||||
|
static readonly Dictionary<Guid, HashSet<NetworkIdentity>> matchPlayers = new Dictionary<Guid, HashSet<NetworkIdentity>>();
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
11
Assets/Mirror/Components/NetworkMatchChecker.cs.meta
Normal file
11
Assets/Mirror/Components/NetworkMatchChecker.cs.meta
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 1020a74962faada4b807ac5dc053a4cf
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
41
Assets/Mirror/Components/NetworkPingDisplay.cs
Normal file
41
Assets/Mirror/Components/NetworkPingDisplay.cs
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace Mirror
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Component that will display the clients ping in milliseconds
|
||||||
|
/// </summary>
|
||||||
|
[DisallowMultipleComponent]
|
||||||
|
[AddComponentMenu("Network/NetworkPingDisplay")]
|
||||||
|
[HelpURL("https://mirror-networking.com/docs/Components/NetworkPingDisplay.html")]
|
||||||
|
public class NetworkPingDisplay : MonoBehaviour
|
||||||
|
{
|
||||||
|
[SerializeField] bool showPing = true;
|
||||||
|
[SerializeField] Vector2 position = new Vector2(200, 0);
|
||||||
|
[SerializeField] int fontSize = 24;
|
||||||
|
[SerializeField] Color textColor = new Color32(255, 255, 255, 80);
|
||||||
|
|
||||||
|
GUIStyle style;
|
||||||
|
|
||||||
|
void Awake()
|
||||||
|
{
|
||||||
|
style = new GUIStyle();
|
||||||
|
style.alignment = TextAnchor.UpperLeft;
|
||||||
|
style.fontSize = fontSize;
|
||||||
|
style.normal.textColor = textColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnGUI()
|
||||||
|
{
|
||||||
|
if (!showPing) { return; }
|
||||||
|
|
||||||
|
string text = string.Format("{0}ms", (int)(NetworkTime.rtt * 1000));
|
||||||
|
|
||||||
|
int width = Screen.width;
|
||||||
|
int height = Screen.height;
|
||||||
|
Rect rect = new Rect(position.x, position.y, width - 200, height * 2 / 100);
|
||||||
|
|
||||||
|
GUI.Label(rect, text, style);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
11
Assets/Mirror/Components/NetworkPingDisplay.cs.meta
Normal file
11
Assets/Mirror/Components/NetworkPingDisplay.cs.meta
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: bc654f29862fc2643b948f772ebb9e68
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
98
Assets/Mirror/Components/NetworkProximityChecker.cs
Normal file
98
Assets/Mirror/Components/NetworkProximityChecker.cs
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
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>
|
||||||
|
[AddComponentMenu("Network/NetworkProximityChecker")]
|
||||||
|
[RequireComponent(typeof(NetworkIdentity))]
|
||||||
|
[HelpURL("https://mirror-networking.com/docs/Components/NetworkProximityChecker.html")]
|
||||||
|
public class NetworkProximityChecker : NetworkVisibility
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The maximim 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>
|
||||||
|
[Tooltip("Enable to force this object to be hidden from players.")]
|
||||||
|
public bool forceHidden;
|
||||||
|
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
11
Assets/Mirror/Components/NetworkProximityChecker.cs.meta
Normal file
11
Assets/Mirror/Components/NetworkProximityChecker.cs.meta
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 1731d8de2d0c84333b08ebe1e79f4118
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
697
Assets/Mirror/Components/NetworkRoomManager.cs
Normal file
697
Assets/Mirror/Components/NetworkRoomManager.cs
Normal file
@ -0,0 +1,697 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.SceneManagement;
|
||||||
|
using UnityEngine.Serialization;
|
||||||
|
|
||||||
|
namespace Mirror
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// This is a specialized NetworkManager that includes a networked room.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>The room has slots that track the joined players, and a maximum player count that is enforced. It requires that the NetworkRoomPlayer component be on the room player objects.</para>
|
||||||
|
/// <para>NetworkRoomManager is derived from NetworkManager, and so it implements many of the virtual functions provided by the NetworkManager class. To avoid accidentally replacing functionality of the NetworkRoomManager, there are new virtual functions on the NetworkRoomManager that begin with "OnRoom". These should be used on classes derived from NetworkRoomManager instead of the virtual functions on NetworkManager.</para>
|
||||||
|
/// <para>The OnRoom*() functions have empty implementations on the NetworkRoomManager base class, so the base class functions do not have to be called.</para>
|
||||||
|
/// </remarks>
|
||||||
|
[AddComponentMenu("Network/NetworkRoomManager")]
|
||||||
|
[HelpURL("https://mirror-networking.com/docs/Components/NetworkRoomManager.html")]
|
||||||
|
public class NetworkRoomManager : NetworkManager
|
||||||
|
{
|
||||||
|
static readonly ILogger logger = LogFactory.GetLogger(typeof(NetworkRoomManager));
|
||||||
|
|
||||||
|
public struct PendingPlayer
|
||||||
|
{
|
||||||
|
public NetworkConnection conn;
|
||||||
|
public GameObject roomPlayer;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Header("Room Settings")]
|
||||||
|
|
||||||
|
[FormerlySerializedAs("m_ShowRoomGUI")]
|
||||||
|
[SerializeField]
|
||||||
|
[Tooltip("This flag controls whether the default UI is shown for the room")]
|
||||||
|
internal bool showRoomGUI = true;
|
||||||
|
|
||||||
|
[FormerlySerializedAs("m_MinPlayers")]
|
||||||
|
[SerializeField]
|
||||||
|
[Tooltip("Minimum number of players to auto-start the game")]
|
||||||
|
protected int minPlayers = 1;
|
||||||
|
|
||||||
|
[FormerlySerializedAs("m_RoomPlayerPrefab")]
|
||||||
|
[SerializeField]
|
||||||
|
[Tooltip("Prefab to use for the Room Player")]
|
||||||
|
protected NetworkRoomPlayer roomPlayerPrefab;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The scene to use for the room. This is similar to the offlineScene of the NetworkManager.
|
||||||
|
/// </summary>
|
||||||
|
[Scene]
|
||||||
|
public string RoomScene;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The scene to use for the playing the game from the room. This is similar to the onlineScene of the NetworkManager.
|
||||||
|
/// </summary>
|
||||||
|
[Scene]
|
||||||
|
public string GameplayScene;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// List of players that are in the Room
|
||||||
|
/// </summary>
|
||||||
|
[FormerlySerializedAs("m_PendingPlayers")]
|
||||||
|
public List<PendingPlayer> pendingPlayers = new List<PendingPlayer>();
|
||||||
|
|
||||||
|
[Header("Diagnostics")]
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// True when all players have submitted a Ready message
|
||||||
|
/// </summary>
|
||||||
|
[Tooltip("Diagnostic flag indicating all players are ready to play")]
|
||||||
|
[FormerlySerializedAs("allPlayersReady")]
|
||||||
|
[SerializeField] bool _allPlayersReady;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// These slots track players that enter the room.
|
||||||
|
/// <para>The slotId on players is global to the game - across all players.</para>
|
||||||
|
/// </summary>
|
||||||
|
[Tooltip("List of Room Player objects")]
|
||||||
|
public List<NetworkRoomPlayer> roomSlots = new List<NetworkRoomPlayer>();
|
||||||
|
|
||||||
|
public bool allPlayersReady
|
||||||
|
{
|
||||||
|
get => _allPlayersReady;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
bool wasReady = _allPlayersReady;
|
||||||
|
bool nowReady = value;
|
||||||
|
|
||||||
|
if (wasReady != nowReady)
|
||||||
|
{
|
||||||
|
_allPlayersReady = value;
|
||||||
|
|
||||||
|
if (nowReady)
|
||||||
|
{
|
||||||
|
OnRoomServerPlayersReady();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
OnRoomServerPlayersNotReady();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void OnValidate()
|
||||||
|
{
|
||||||
|
// always >= 0
|
||||||
|
maxConnections = Mathf.Max(maxConnections, 0);
|
||||||
|
|
||||||
|
// always <= maxConnections
|
||||||
|
minPlayers = Mathf.Min(minPlayers, maxConnections);
|
||||||
|
|
||||||
|
// always >= 0
|
||||||
|
minPlayers = Mathf.Max(minPlayers, 0);
|
||||||
|
|
||||||
|
if (roomPlayerPrefab != null)
|
||||||
|
{
|
||||||
|
NetworkIdentity identity = roomPlayerPrefab.GetComponent<NetworkIdentity>();
|
||||||
|
if (identity == null)
|
||||||
|
{
|
||||||
|
roomPlayerPrefab = null;
|
||||||
|
logger.LogError("RoomPlayer prefab must have a NetworkIdentity component.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
base.OnValidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void ReadyStatusChanged()
|
||||||
|
{
|
||||||
|
int CurrentPlayers = 0;
|
||||||
|
int ReadyPlayers = 0;
|
||||||
|
|
||||||
|
foreach (NetworkRoomPlayer item in roomSlots)
|
||||||
|
{
|
||||||
|
if (item != null)
|
||||||
|
{
|
||||||
|
CurrentPlayers++;
|
||||||
|
if (item.readyToBegin)
|
||||||
|
ReadyPlayers++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (CurrentPlayers == ReadyPlayers)
|
||||||
|
CheckReadyToBegin();
|
||||||
|
else
|
||||||
|
allPlayersReady = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called on the server when a client is ready.
|
||||||
|
/// <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)
|
||||||
|
{
|
||||||
|
logger.Log("NetworkRoomManager OnServerReady");
|
||||||
|
base.OnServerReady(conn);
|
||||||
|
|
||||||
|
if (conn != null && conn.identity != null)
|
||||||
|
{
|
||||||
|
GameObject roomPlayer = conn.identity.gameObject;
|
||||||
|
|
||||||
|
// if null or not a room player, dont replace it
|
||||||
|
if (roomPlayer != null && roomPlayer.GetComponent<NetworkRoomPlayer>() != null)
|
||||||
|
SceneLoadedForPlayer(conn, roomPlayer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SceneLoadedForPlayer(NetworkConnection conn, GameObject roomPlayer)
|
||||||
|
{
|
||||||
|
if (logger.LogEnabled()) logger.LogFormat(LogType.Log, "NetworkRoom SceneLoadedForPlayer scene: {0} {1}", SceneManager.GetActiveScene().path, conn);
|
||||||
|
|
||||||
|
if (IsSceneActive(RoomScene))
|
||||||
|
{
|
||||||
|
// cant be ready in room, add to ready list
|
||||||
|
PendingPlayer pending;
|
||||||
|
pending.conn = conn;
|
||||||
|
pending.roomPlayer = roomPlayer;
|
||||||
|
pendingPlayers.Add(pending);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
GameObject gamePlayer = OnRoomServerCreateGamePlayer(conn, roomPlayer);
|
||||||
|
if (gamePlayer == null)
|
||||||
|
{
|
||||||
|
// get start position from base class
|
||||||
|
Transform startPos = GetStartPosition();
|
||||||
|
gamePlayer = startPos != null
|
||||||
|
? Instantiate(playerPrefab, startPos.position, startPos.rotation)
|
||||||
|
: Instantiate(playerPrefab, Vector3.zero, Quaternion.identity);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!OnRoomServerSceneLoadedForPlayer(conn, roomPlayer, gamePlayer))
|
||||||
|
return;
|
||||||
|
|
||||||
|
// replace room player with game player
|
||||||
|
NetworkServer.ReplacePlayerForConnection(conn, gamePlayer, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// CheckReadyToBegin checks all of the players in the room to see if their readyToBegin flag is set.
|
||||||
|
/// <para>If all of the players are ready, then the server switches from the RoomScene to the PlayScene, essentially starting the game. This is called automatically in response to NetworkRoomPlayer.CmdChangeReadyState.</para>
|
||||||
|
/// </summary>
|
||||||
|
public void CheckReadyToBegin()
|
||||||
|
{
|
||||||
|
if (!IsSceneActive(RoomScene))
|
||||||
|
return;
|
||||||
|
|
||||||
|
int numberOfReadyPlayers = NetworkServer.connections.Count(conn => conn.Value != null && conn.Value.identity.gameObject.GetComponent<NetworkRoomPlayer>().readyToBegin);
|
||||||
|
bool enoughReadyPlayers = minPlayers <= 0 || numberOfReadyPlayers >= minPlayers;
|
||||||
|
if (enoughReadyPlayers)
|
||||||
|
{
|
||||||
|
pendingPlayers.Clear();
|
||||||
|
allPlayersReady = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
allPlayersReady = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void CallOnClientEnterRoom()
|
||||||
|
{
|
||||||
|
OnRoomClientEnter();
|
||||||
|
foreach (NetworkRoomPlayer player in roomSlots)
|
||||||
|
if (player != null)
|
||||||
|
{
|
||||||
|
player.OnClientEnterRoom();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void CallOnClientExitRoom()
|
||||||
|
{
|
||||||
|
OnRoomClientExit();
|
||||||
|
foreach (NetworkRoomPlayer player in roomSlots)
|
||||||
|
if (player != null)
|
||||||
|
{
|
||||||
|
player.OnClientExitRoom();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#region server handlers
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called on the server when a new client connects.
|
||||||
|
/// <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)
|
||||||
|
{
|
||||||
|
if (numPlayers >= maxConnections)
|
||||||
|
{
|
||||||
|
conn.Disconnect();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// cannot join game in progress
|
||||||
|
if (!IsSceneActive(RoomScene))
|
||||||
|
{
|
||||||
|
conn.Disconnect();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
base.OnServerConnect(conn);
|
||||||
|
OnRoomServerConnect(conn);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called on the server when a client disconnects.
|
||||||
|
/// <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)
|
||||||
|
{
|
||||||
|
if (conn.identity != null)
|
||||||
|
{
|
||||||
|
NetworkRoomPlayer roomPlayer = conn.identity.GetComponent<NetworkRoomPlayer>();
|
||||||
|
|
||||||
|
if (roomPlayer != null)
|
||||||
|
roomSlots.Remove(roomPlayer);
|
||||||
|
|
||||||
|
foreach (NetworkIdentity clientOwnedObject in conn.clientOwnedObjects)
|
||||||
|
{
|
||||||
|
roomPlayer = clientOwnedObject.GetComponent<NetworkRoomPlayer>();
|
||||||
|
if (roomPlayer != null)
|
||||||
|
roomSlots.Remove(roomPlayer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
allPlayersReady = false;
|
||||||
|
|
||||||
|
foreach (NetworkRoomPlayer player in roomSlots)
|
||||||
|
{
|
||||||
|
if (player != null)
|
||||||
|
player.GetComponent<NetworkRoomPlayer>().readyToBegin = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsSceneActive(RoomScene))
|
||||||
|
RecalculateRoomPlayerIndices();
|
||||||
|
|
||||||
|
OnRoomServerDisconnect(conn);
|
||||||
|
base.OnServerDisconnect(conn);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sequential index used in round-robin deployment of players into instances and score positioning
|
||||||
|
public int clientIndex;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called on the server when a client adds a new player with ClientScene.AddPlayer.
|
||||||
|
/// <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)
|
||||||
|
{
|
||||||
|
// increment the index before adding the player, so first player starts at 1
|
||||||
|
clientIndex++;
|
||||||
|
|
||||||
|
if (IsSceneActive(RoomScene))
|
||||||
|
{
|
||||||
|
if (roomSlots.Count == maxConnections)
|
||||||
|
return;
|
||||||
|
|
||||||
|
allPlayersReady = false;
|
||||||
|
|
||||||
|
if (logger.LogEnabled()) logger.LogFormat(LogType.Log, "NetworkRoomManager.OnServerAddPlayer playerPrefab:{0}", roomPlayerPrefab.name);
|
||||||
|
|
||||||
|
GameObject newRoomGameObject = OnRoomServerCreateRoomPlayer(conn);
|
||||||
|
if (newRoomGameObject == null)
|
||||||
|
newRoomGameObject = Instantiate(roomPlayerPrefab.gameObject, Vector3.zero, Quaternion.identity);
|
||||||
|
|
||||||
|
NetworkServer.AddPlayerForConnection(conn, newRoomGameObject);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
OnRoomServerAddPlayer(conn);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Server]
|
||||||
|
public void RecalculateRoomPlayerIndices()
|
||||||
|
{
|
||||||
|
if (roomSlots.Count > 0)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < roomSlots.Count; i++)
|
||||||
|
{
|
||||||
|
roomSlots[i].index = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This causes the server to switch scenes and sets the networkSceneName.
|
||||||
|
/// <para>Clients that connect to this server will automatically switch to this scene. This is called autmatically if onlineScene or offlineScene are set, but it can be called from user code to switch scenes again while the game is in progress. This automatically sets clients to be not-ready. The clients must call NetworkClient.Ready() again to participate in the new scene.</para>
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="newSceneName"></param>
|
||||||
|
public override void ServerChangeScene(string newSceneName)
|
||||||
|
{
|
||||||
|
if (newSceneName == RoomScene)
|
||||||
|
{
|
||||||
|
foreach (NetworkRoomPlayer roomPlayer in roomSlots)
|
||||||
|
{
|
||||||
|
if (roomPlayer == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// find the game-player object for this connection, and destroy it
|
||||||
|
NetworkIdentity identity = roomPlayer.GetComponent<NetworkIdentity>();
|
||||||
|
|
||||||
|
if (NetworkServer.active)
|
||||||
|
{
|
||||||
|
// re-add the room object
|
||||||
|
roomPlayer.GetComponent<NetworkRoomPlayer>().readyToBegin = false;
|
||||||
|
NetworkServer.ReplacePlayerForConnection(identity.connectionToClient, roomPlayer.gameObject);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
allPlayersReady = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
base.ServerChangeScene(newSceneName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called on the server when a scene is completed loaded, when the scene load was initiated by the server with ServerChangeScene().
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="sceneName">The name of the new scene.</param>
|
||||||
|
public override void OnServerSceneChanged(string sceneName)
|
||||||
|
{
|
||||||
|
if (sceneName != RoomScene)
|
||||||
|
{
|
||||||
|
// call SceneLoadedForPlayer on any players that become ready while we were loading the scene.
|
||||||
|
foreach (PendingPlayer pending in pendingPlayers)
|
||||||
|
SceneLoadedForPlayer(pending.conn, pending.roomPlayer);
|
||||||
|
|
||||||
|
pendingPlayers.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
OnRoomServerSceneChanged(sceneName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This is invoked when a server is started - including when a host is started.
|
||||||
|
/// <para>StartServer has multiple signatures, but they all cause this hook to be called.</para>
|
||||||
|
/// </summary>
|
||||||
|
public override void OnStartServer()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(RoomScene))
|
||||||
|
{
|
||||||
|
logger.LogError("NetworkRoomManager RoomScene is empty. Set the RoomScene in the inspector for the NetworkRoomMangaer");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(GameplayScene))
|
||||||
|
{
|
||||||
|
logger.LogError("NetworkRoomManager PlayScene is empty. Set the PlayScene in the inspector for the NetworkRoomMangaer");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
OnRoomStartServer();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This is invoked when a host is started.
|
||||||
|
/// <para>StartHost has multiple signatures, but they all cause this hook to be called.</para>
|
||||||
|
/// </summary>
|
||||||
|
public override void OnStartHost()
|
||||||
|
{
|
||||||
|
OnRoomStartHost();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This is called when a server is stopped - including when a host is stopped.
|
||||||
|
/// </summary>
|
||||||
|
public override void OnStopServer()
|
||||||
|
{
|
||||||
|
roomSlots.Clear();
|
||||||
|
OnRoomStopServer();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This is called when a host is stopped.
|
||||||
|
/// </summary>
|
||||||
|
public override void OnStopHost()
|
||||||
|
{
|
||||||
|
OnRoomStopHost();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region client handlers
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This is invoked when the client is started.
|
||||||
|
/// </summary>
|
||||||
|
public override void OnStartClient()
|
||||||
|
{
|
||||||
|
if (roomPlayerPrefab == null || roomPlayerPrefab.gameObject == null)
|
||||||
|
logger.LogError("NetworkRoomManager no RoomPlayer prefab is registered. Please add a RoomPlayer prefab.");
|
||||||
|
else
|
||||||
|
ClientScene.RegisterPrefab(roomPlayerPrefab.gameObject);
|
||||||
|
|
||||||
|
if (playerPrefab == null)
|
||||||
|
logger.LogError("NetworkRoomManager no GamePlayer prefab is registered. Please add a GamePlayer prefab.");
|
||||||
|
|
||||||
|
OnRoomStartClient();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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)
|
||||||
|
{
|
||||||
|
OnRoomClientConnect(conn);
|
||||||
|
base.OnClientConnect(conn);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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)
|
||||||
|
{
|
||||||
|
OnRoomClientDisconnect(conn);
|
||||||
|
base.OnClientDisconnect(conn);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This is called when a client is stopped.
|
||||||
|
/// </summary>
|
||||||
|
public override void OnStopClient()
|
||||||
|
{
|
||||||
|
OnRoomStopClient();
|
||||||
|
CallOnClientExitRoom();
|
||||||
|
roomSlots.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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)
|
||||||
|
{
|
||||||
|
if (IsSceneActive(RoomScene))
|
||||||
|
{
|
||||||
|
if (NetworkClient.isConnected)
|
||||||
|
CallOnClientEnterRoom();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
CallOnClientExitRoom();
|
||||||
|
|
||||||
|
base.OnClientSceneChanged(conn);
|
||||||
|
OnRoomClientSceneChanged(conn);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region room server virtuals
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This is called on the host when a host is started.
|
||||||
|
/// </summary>
|
||||||
|
public virtual void OnRoomStartHost() { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This is called on the host when the host is stopped.
|
||||||
|
/// </summary>
|
||||||
|
public virtual void OnRoomStopHost() { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This is called on the server when the server is started - including when a host is started.
|
||||||
|
/// </summary>
|
||||||
|
public virtual void OnRoomStartServer() { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This is called on the server when the server is started - including when a host is stopped.
|
||||||
|
/// </summary>
|
||||||
|
public virtual void OnRoomStopServer() { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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) { }
|
||||||
|
|
||||||
|
/// <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) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This is called on the server when a networked scene finishes loading.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="sceneName">Name of the new scene.</param>
|
||||||
|
public virtual void OnRoomServerSceneChanged(string sceneName) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This allows customization of the creation of the room-player object on the server.
|
||||||
|
/// <para>By default the roomPlayerPrefab is used to create the room-player, but this function allows that behaviour to be customized.</para>
|
||||||
|
/// </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)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This allows customization of the creation of the GamePlayer object on the server.
|
||||||
|
/// <para>By default the gamePlayerPrefab is used to create the game-player, but this function allows that behaviour to be customized. The object returned from the function will be used to replace the room-player on the connection.</para>
|
||||||
|
/// </summary>
|
||||||
|
/// <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)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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>
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="conn">The connection the player object is for.</param>
|
||||||
|
public virtual void OnRoomServerAddPlayer(NetworkConnection conn)
|
||||||
|
{
|
||||||
|
base.OnServerAddPlayer(conn);
|
||||||
|
}
|
||||||
|
|
||||||
|
// for users to apply settings from their room player object to their in-game player object
|
||||||
|
/// <summary>
|
||||||
|
/// This is called on the server when it is told that a client has finished switching from the room scene to a game player scene.
|
||||||
|
/// <para>When switching from the room, the room-player is replaced with a game-player object. This callback function gives an opportunity to apply state from the room-player to the game-player object.</para>
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="conn">The connection of the player</param>
|
||||||
|
/// <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)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This is called on the server when all the players in the room are ready.
|
||||||
|
/// <para>The default implementation of this function uses ServerChangeScene() to switch to the game player scene. By implementing this callback you can customize what happens when all the players in the room are ready, such as adding a countdown or a confirmation for a group leader.</para>
|
||||||
|
/// </summary>
|
||||||
|
public virtual void OnRoomServerPlayersReady()
|
||||||
|
{
|
||||||
|
// all players are readyToBegin, start the game
|
||||||
|
ServerChangeScene(GameplayScene);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This is called on the server when CheckReadyToBegin finds that players are not ready
|
||||||
|
/// <para>May be called multiple times while not ready players are joining</para>
|
||||||
|
/// </summary>
|
||||||
|
public virtual void OnRoomServerPlayersNotReady() { }
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region room client virtuals
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This is a hook to allow custom behaviour when the game client enters the room.
|
||||||
|
/// </summary>
|
||||||
|
public virtual void OnRoomClientEnter() { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This is a hook to allow custom behaviour when the game client exits the room.
|
||||||
|
/// </summary>
|
||||||
|
public virtual void OnRoomClientExit() { }
|
||||||
|
|
||||||
|
/// <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) { }
|
||||||
|
|
||||||
|
/// <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) { }
|
||||||
|
|
||||||
|
/// <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>
|
||||||
|
/// This is called on the client when the client stops.
|
||||||
|
/// </summary>
|
||||||
|
public virtual void OnRoomStopClient() { }
|
||||||
|
|
||||||
|
/// <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) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called on the client when adding a player to the room fails.
|
||||||
|
/// <para>This could be because the room is full, or the connection is not allowed to have more players.</para>
|
||||||
|
/// </summary>
|
||||||
|
public virtual void OnRoomClientAddPlayerFailed() { }
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region optional UI
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// virtual so inheriting classes can roll their own
|
||||||
|
/// </summary>
|
||||||
|
public virtual void OnGUI()
|
||||||
|
{
|
||||||
|
if (!showRoomGUI)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (NetworkServer.active && IsSceneActive(GameplayScene))
|
||||||
|
{
|
||||||
|
GUILayout.BeginArea(new Rect(Screen.width - 150f, 10f, 140f, 30f));
|
||||||
|
if (GUILayout.Button("Return to Room"))
|
||||||
|
ServerChangeScene(RoomScene);
|
||||||
|
GUILayout.EndArea();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsSceneActive(RoomScene))
|
||||||
|
GUI.Box(new Rect(10f, 180f, 520f, 150f), "PLAYERS");
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
}
|
11
Assets/Mirror/Components/NetworkRoomManager.cs.meta
Normal file
11
Assets/Mirror/Components/NetworkRoomManager.cs.meta
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 615e6c6589cf9e54cad646b5a11e0529
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
198
Assets/Mirror/Components/NetworkRoomPlayer.cs
Normal file
198
Assets/Mirror/Components/NetworkRoomPlayer.cs
Normal file
@ -0,0 +1,198 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace Mirror
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// This component works in conjunction with the NetworkRoomManager to make up the multiplayer room system.
|
||||||
|
/// <para>The RoomPrefab object of the NetworkRoomManager must have this component on it. This component holds basic room player data required for the room to function. Game specific data for room players can be put in other components on the RoomPrefab or in scripts derived from NetworkRoomPlayer.</para>
|
||||||
|
/// </summary>
|
||||||
|
[DisallowMultipleComponent]
|
||||||
|
[AddComponentMenu("Network/NetworkRoomPlayer")]
|
||||||
|
[HelpURL("https://mirror-networking.com/docs/Components/NetworkRoomPlayer.html")]
|
||||||
|
public class NetworkRoomPlayer : NetworkBehaviour
|
||||||
|
{
|
||||||
|
static readonly ILogger logger = LogFactory.GetLogger(typeof(NetworkRoomPlayer));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This flag controls whether the default UI is shown for the room player.
|
||||||
|
/// <para>As this UI is rendered using the old GUI system, it is only recommended for testing purposes.</para>
|
||||||
|
/// </summary>
|
||||||
|
[Tooltip("This flag controls whether the default UI is shown for the room player")]
|
||||||
|
public bool showRoomGUI = true;
|
||||||
|
|
||||||
|
[Header("Diagnostics")]
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Diagnostic flag indicating whether this player is ready for the game to begin.
|
||||||
|
/// <para>Invoke CmdChangeReadyState method on the client to set this flag.</para>
|
||||||
|
/// <para>When all players are ready to begin, the game will start. This should not be set directly, CmdChangeReadyState should be called on the client to set it on the server.</para>
|
||||||
|
/// </summary>
|
||||||
|
[Tooltip("Diagnostic flag indicating whether this player is ready for the game to begin")]
|
||||||
|
[SyncVar(hook = nameof(ReadyStateChanged))]
|
||||||
|
public bool readyToBegin;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Diagnostic index of the player, e.g. Player1, Player2, etc.
|
||||||
|
/// </summary>
|
||||||
|
[Tooltip("Diagnostic index of the player, e.g. Player1, Player2, etc.")]
|
||||||
|
[SyncVar(hook = nameof(IndexChanged))]
|
||||||
|
public int index;
|
||||||
|
|
||||||
|
#region Unity Callbacks
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Do not use Start - Override OnStartrHost / OnStartClient instead!
|
||||||
|
/// </summary>
|
||||||
|
public void Start()
|
||||||
|
{
|
||||||
|
if (NetworkManager.singleton is NetworkRoomManager room)
|
||||||
|
{
|
||||||
|
// NetworkRoomPlayer object must be set to DontDestroyOnLoad along with NetworkRoomManager
|
||||||
|
// in server and all clients, otherwise it will be respawned in the game scene which would
|
||||||
|
// have undesireable effects.
|
||||||
|
if (room.dontDestroyOnLoad)
|
||||||
|
DontDestroyOnLoad(gameObject);
|
||||||
|
|
||||||
|
room.roomSlots.Add(this);
|
||||||
|
|
||||||
|
if (NetworkServer.active)
|
||||||
|
room.RecalculateRoomPlayerIndices();
|
||||||
|
|
||||||
|
if (NetworkClient.active)
|
||||||
|
room.CallOnClientEnterRoom();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
logger.LogError("RoomPlayer could not find a NetworkRoomManager. The RoomPlayer requires a NetworkRoomManager object to function. Make sure that there is one in the scene.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual void OnDisable()
|
||||||
|
{
|
||||||
|
if (NetworkClient.active && NetworkManager.singleton is NetworkRoomManager room)
|
||||||
|
{
|
||||||
|
// only need to call this on client as server removes it before object is destroyed
|
||||||
|
room.roomSlots.Remove(this);
|
||||||
|
|
||||||
|
room.CallOnClientExitRoom();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Commands
|
||||||
|
|
||||||
|
[Command]
|
||||||
|
public void CmdChangeReadyState(bool readyState)
|
||||||
|
{
|
||||||
|
readyToBegin = readyState;
|
||||||
|
NetworkRoomManager room = NetworkManager.singleton as NetworkRoomManager;
|
||||||
|
if (room != null)
|
||||||
|
{
|
||||||
|
room.ReadyStatusChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region SyncVar Hooks
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This is a hook that is invoked on clients when the index changes.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="oldIndex">The old index value</param>
|
||||||
|
/// <param name="newIndex">The new index value</param>
|
||||||
|
public virtual void IndexChanged(int oldIndex, int newIndex) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This is a hook that is invoked on clients when a RoomPlayer switches between ready or not ready.
|
||||||
|
/// <para>This function is called when the a client player calls CmdChangeReadyState.</para>
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="newReadyState">New Ready State</param>
|
||||||
|
public virtual void ReadyStateChanged(bool oldReadyState, bool newReadyState) { }
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Room Client Virtuals
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This is a hook that is invoked on clients for all room player objects when entering the room.
|
||||||
|
/// <para>Note: isLocalPlayer is not guaranteed to be set until OnStartLocalPlayer is called.</para>
|
||||||
|
/// </summary>
|
||||||
|
public virtual void OnClientEnterRoom() { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This is a hook that is invoked on clients for all room player objects when exiting the room.
|
||||||
|
/// </summary>
|
||||||
|
public virtual void OnClientExitRoom() { }
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Optional UI
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Render a UI for the room. Override to provide your on UI
|
||||||
|
/// </summary>
|
||||||
|
public virtual void OnGUI()
|
||||||
|
{
|
||||||
|
if (!showRoomGUI)
|
||||||
|
return;
|
||||||
|
|
||||||
|
NetworkRoomManager room = NetworkManager.singleton as NetworkRoomManager;
|
||||||
|
if (room)
|
||||||
|
{
|
||||||
|
if (!room.showRoomGUI)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!NetworkManager.IsSceneActive(room.RoomScene))
|
||||||
|
return;
|
||||||
|
|
||||||
|
DrawPlayerReadyState();
|
||||||
|
DrawPlayerReadyButton();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DrawPlayerReadyState()
|
||||||
|
{
|
||||||
|
GUILayout.BeginArea(new Rect(20f + (index * 100), 200f, 90f, 130f));
|
||||||
|
|
||||||
|
GUILayout.Label($"Player [{index + 1}]");
|
||||||
|
|
||||||
|
if (readyToBegin)
|
||||||
|
GUILayout.Label("Ready");
|
||||||
|
else
|
||||||
|
GUILayout.Label("Not Ready");
|
||||||
|
|
||||||
|
if (((isServer && index > 0) || isServerOnly) && GUILayout.Button("REMOVE"))
|
||||||
|
{
|
||||||
|
// This button only shows on the Host for all players other than the Host
|
||||||
|
// Host and Players can't remove themselves (stop the client instead)
|
||||||
|
// Host can kick a Player this way.
|
||||||
|
GetComponent<NetworkIdentity>().connectionToClient.Disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
GUILayout.EndArea();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DrawPlayerReadyButton()
|
||||||
|
{
|
||||||
|
if (NetworkClient.active && isLocalPlayer)
|
||||||
|
{
|
||||||
|
GUILayout.BeginArea(new Rect(20f, 300f, 120f, 20f));
|
||||||
|
|
||||||
|
if (readyToBegin)
|
||||||
|
{
|
||||||
|
if (GUILayout.Button("Cancel"))
|
||||||
|
CmdChangeReadyState(false);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (GUILayout.Button("Ready"))
|
||||||
|
CmdChangeReadyState(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
GUILayout.EndArea();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
}
|
11
Assets/Mirror/Components/NetworkRoomPlayer.cs.meta
Normal file
11
Assets/Mirror/Components/NetworkRoomPlayer.cs.meta
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 79874ac94d5b1314788ecf0e86bd23fd
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
115
Assets/Mirror/Components/NetworkSceneChecker.cs
Normal file
115
Assets/Mirror/Components/NetworkSceneChecker.cs
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
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>
|
||||||
|
[DisallowMultipleComponent]
|
||||||
|
[AddComponentMenu("Network/NetworkSceneChecker")]
|
||||||
|
[RequireComponent(typeof(NetworkIdentity))]
|
||||||
|
[HelpURL("https://mirror-networking.com/docs/Components/NetworkSceneChecker.html")]
|
||||||
|
public class NetworkSceneChecker : NetworkVisibility
|
||||||
|
{
|
||||||
|
static readonly ILogger logger = LogFactory.GetLogger(typeof(NetworkSceneChecker));
|
||||||
|
|
||||||
|
/// <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;
|
||||||
|
if (logger.LogEnabled()) logger.Log($"NetworkSceneChecker.Awake currentScene: {currentScene}");
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void OnStartServer()
|
||||||
|
{
|
||||||
|
if (!sceneCheckerObjects.ContainsKey(currentScene))
|
||||||
|
sceneCheckerObjects.Add(currentScene, new HashSet<NetworkIdentity>());
|
||||||
|
|
||||||
|
sceneCheckerObjects[currentScene].Add(netIdentity);
|
||||||
|
}
|
||||||
|
|
||||||
|
[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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
11
Assets/Mirror/Components/NetworkSceneChecker.cs.meta
Normal file
11
Assets/Mirror/Components/NetworkSceneChecker.cs.meta
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: b7fdb599e1359924bad6255660370252
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
12
Assets/Mirror/Components/NetworkTransform.cs
Normal file
12
Assets/Mirror/Components/NetworkTransform.cs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace Mirror
|
||||||
|
{
|
||||||
|
[DisallowMultipleComponent]
|
||||||
|
[AddComponentMenu("Network/NetworkTransform")]
|
||||||
|
[HelpURL("https://mirror-networking.com/docs/Components/NetworkTransform.html")]
|
||||||
|
public class NetworkTransform : NetworkTransformBase
|
||||||
|
{
|
||||||
|
protected override Transform targetComponent => transform;
|
||||||
|
}
|
||||||
|
}
|
11
Assets/Mirror/Components/NetworkTransform.cs.meta
Normal file
11
Assets/Mirror/Components/NetworkTransform.cs.meta
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 2f74aedd71d9a4f55b3ce499326d45fb
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user