mirror of
https://github.com/MirrorNetworking/Mirror.git
synced 2024-11-17 18:40:33 +00:00
Merged master
This commit is contained in:
commit
79309c6a05
30
Assets/Mirror/Components/NetworkDiagnosticsDebugger.cs
Normal file
30
Assets/Mirror/Components/NetworkDiagnosticsDebugger.cs
Normal file
@ -0,0 +1,30 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
public class NetworkDiagnosticsDebugger : MonoBehaviour
|
||||
{
|
||||
public bool logInMessages = true;
|
||||
public bool logOutMessages = true;
|
||||
void OnInMessage(NetworkDiagnostics.MessageInfo msgInfo)
|
||||
{
|
||||
if (logInMessages)
|
||||
Debug.Log(msgInfo);
|
||||
}
|
||||
void OnOutMessage(NetworkDiagnostics.MessageInfo msgInfo)
|
||||
{
|
||||
if (logOutMessages)
|
||||
Debug.Log(msgInfo);
|
||||
}
|
||||
void OnEnable()
|
||||
{
|
||||
NetworkDiagnostics.InMessageEvent += OnInMessage;
|
||||
NetworkDiagnostics.OutMessageEvent += OnOutMessage;
|
||||
}
|
||||
void OnDisable()
|
||||
{
|
||||
NetworkDiagnostics.InMessageEvent -= OnInMessage;
|
||||
NetworkDiagnostics.OutMessageEvent -= OnOutMessage;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bc9f0a0fe4124424b8f9d4927795ee01
|
||||
timeCreated: 1700945893
|
@ -1703,7 +1703,7 @@ public static void OnGUI()
|
||||
// only if in world
|
||||
if (!ready) return;
|
||||
|
||||
GUILayout.BeginArea(new Rect(10, 5, 1000, 50));
|
||||
GUILayout.BeginArea(new Rect(10, 5, 1020, 50));
|
||||
|
||||
GUILayout.BeginHorizontal("Box");
|
||||
GUILayout.Label("Snapshot Interp.:");
|
||||
|
@ -9,6 +9,7 @@ namespace Mirror
|
||||
{
|
||||
public enum PlayerSpawnMethod { Random, RoundRobin }
|
||||
public enum NetworkManagerMode { Offline, ServerOnly, ClientOnly, Host }
|
||||
public enum HeadlessStartOptions { DoNothing, AutoStartServer, AutoStartClient }
|
||||
|
||||
[DisallowMultipleComponent]
|
||||
[AddComponentMenu("Network/Network Manager")]
|
||||
@ -29,18 +30,31 @@ public class NetworkManager : MonoBehaviour
|
||||
|
||||
/// <summary>Should the server auto-start when 'Server Build' is checked in build settings</summary>
|
||||
[Header("Headless Builds")]
|
||||
[Tooltip("Should the server auto-start when 'Dedicated Server' platform is selected, or 'Server Build' is checked in build settings.")]
|
||||
[FormerlySerializedAs("startOnHeadless")]
|
||||
public bool autoStartServerBuild = true;
|
||||
|
||||
[Tooltip("Automatically connect the client in headless builds. Useful for CCU tests with bot clients.\n\nAddress may be passed as command line argument.\n\nMake sure that only 'autostartServer' or 'autoconnectClient' is enabled, not both!")]
|
||||
public bool autoConnectClientBuild;
|
||||
[Tooltip("Choose whether Server or Client should auto-start in headless builds")]
|
||||
public HeadlessStartOptions headlessStartMode = HeadlessStartOptions.DoNothing;
|
||||
|
||||
/// <summary>Server Update frequency, per second. Use around 60Hz for fast paced games like Counter-Strike to minimize latency. Use around 30Hz for games like WoW to minimize computations. Use around 1-10Hz for slow paced games like EVE.</summary>
|
||||
[Tooltip("Server & Client send rate per second. Use 60-100Hz for fast paced games like Counter-Strike to minimize latency. Use around 30Hz for games like WoW to minimize computations. Use around 1-10Hz for slow paced games like EVE.")]
|
||||
[FormerlySerializedAs("serverTickRate")]
|
||||
public int sendRate = 60;
|
||||
|
||||
// Deprecated 2023-11-25
|
||||
// Using SerializeField and HideInInspector to self-correct for being
|
||||
// replaced by headlessStartMode. This can be removed in the future.
|
||||
// See OnValidate() for how we handle this.
|
||||
[Obsolete("Deprecated - Use headlessStartMode instead.")]
|
||||
[FormerlySerializedAs("autoStartServerBuild"), SerializeField, HideInInspector]
|
||||
public bool autoStartServerBuild = true;
|
||||
|
||||
// Deprecated 2023-11-25
|
||||
// Using SerializeField and HideInInspector to self-correct for being
|
||||
// replaced by headlessStartMode. This can be removed in the future.
|
||||
// See OnValidate() for how we handle this.
|
||||
[Obsolete("Deprecated - Use headlessStartMode instead.")]
|
||||
[FormerlySerializedAs("autoConnectClientBuild"), SerializeField, HideInInspector]
|
||||
public bool autoConnectClientBuild;
|
||||
|
||||
// client send rate follows server send rate to avoid errors for now
|
||||
/// <summary>Client Update frequency, per second. Use around 60Hz for fast paced games like Counter-Strike to minimize latency. Use around 30Hz for games like WoW to minimize computations. Use around 1-10Hz for slow paced games like EVE.</summary>
|
||||
// [Tooltip("Client broadcasts 'sendRate' times per second. Use around 60Hz for fast paced games like Counter-Strike to minimize latency. Use around 30Hz for games like WoW to minimize computations. Use around 1-10Hz for slow paced games like EVE.")]
|
||||
@ -157,6 +171,24 @@ public class NetworkManager : MonoBehaviour
|
||||
// virtual so that inheriting classes' OnValidate() can call base.OnValidate() too
|
||||
public virtual void OnValidate()
|
||||
{
|
||||
#pragma warning disable 618
|
||||
// autoStartServerBuild and autoConnectClientBuild are now obsolete, but to avoid
|
||||
// a breaking change we'll set headlessStartMode to what the user had set before.
|
||||
//
|
||||
// headlessStartMode defaults to DoNothing, so if the user had neither of these
|
||||
// set, then it will remain as DoNothing, and if they set headlessStartMode to
|
||||
// any selection in the inspector it won't get changed back.
|
||||
if (autoStartServerBuild)
|
||||
headlessStartMode = HeadlessStartOptions.AutoStartServer;
|
||||
else if (autoConnectClientBuild)
|
||||
headlessStartMode = HeadlessStartOptions.AutoStartClient;
|
||||
|
||||
// Setting both to false here prevents this code from fighting with user
|
||||
// selection in the inspector, and they're both SerialisedField's.
|
||||
autoStartServerBuild = false;
|
||||
autoConnectClientBuild = false;
|
||||
#pragma warning restore 618
|
||||
|
||||
// always >= 0
|
||||
maxConnections = Mathf.Max(maxConnections, 0);
|
||||
|
||||
@ -215,25 +247,24 @@ public virtual void Awake()
|
||||
// virtual so that inheriting classes' Start() can call base.Start() too
|
||||
public virtual void Start()
|
||||
{
|
||||
// headless mode? then start the server
|
||||
// can't do this in Awake because Awake is for initialization.
|
||||
// some transports might not be ready until Start.
|
||||
// Auto-start headless server or client.
|
||||
//
|
||||
// (tick rate is applied in StartServer!)
|
||||
// We can't do this in Awake because Awake is for initialization
|
||||
// and some transports might not be ready until Start.
|
||||
//
|
||||
// don't auto start in editor where we have a UI, only in builds.
|
||||
// otherwise if we switch to 'Dedicated Server' target and press
|
||||
// Play, it would auto start the server every time.
|
||||
if (Utils.IsHeadless() && !Application.isEditor)
|
||||
if (Utils.IsHeadless())
|
||||
{
|
||||
if (autoStartServerBuild)
|
||||
switch (headlessStartMode)
|
||||
{
|
||||
StartServer();
|
||||
}
|
||||
// only start server or client, never both
|
||||
else if (autoConnectClientBuild)
|
||||
{
|
||||
StartClient();
|
||||
case HeadlessStartOptions.AutoStartServer:
|
||||
StartServer();
|
||||
break;
|
||||
case HeadlessStartOptions.AutoStartClient:
|
||||
StartClient();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -398,6 +429,8 @@ void SetupClient()
|
||||
/// <summary>Starts the client, connects it to the server with networkAddress.</summary>
|
||||
public void StartClient()
|
||||
{
|
||||
// Do checks and short circuits before setting anything up.
|
||||
// If / when we retry, we won't have conflict issues.
|
||||
if (NetworkClient.active)
|
||||
{
|
||||
Debug.LogWarning("Client already started.");
|
||||
|
@ -378,6 +378,30 @@ async void BuildAndPushServer()
|
||||
string imageName = _containerImageRepo;
|
||||
string tag = _containerImageTag;
|
||||
|
||||
// MIRROR CHANGE ///////////////////////////////////////////////
|
||||
// registry, repository and tag can not contain whitespaces.
|
||||
// otherwise the docker command will throw an error:
|
||||
// "ERROR: "docker buildx build" requires exactly 1 argument."
|
||||
// catch this early and notify the user immediately.
|
||||
if (registry.Contains(" "))
|
||||
{
|
||||
onError($"Container Registry is not allowed to contain whitespace: '{registry}'");
|
||||
return;
|
||||
}
|
||||
|
||||
if (imageName.Contains(" "))
|
||||
{
|
||||
onError($"Image Repository is not allowed to contain whitespace: '{imageName}'");
|
||||
return;
|
||||
}
|
||||
|
||||
if (tag.Contains(" "))
|
||||
{
|
||||
onError($"Tag is not allowed to contain whitespace: '{tag}'");
|
||||
return;
|
||||
}
|
||||
// END MIRROR CHANGE ///////////////////////////////////////////
|
||||
|
||||
// increment tag for quicker iteration
|
||||
if (_autoIncrementTag)
|
||||
{
|
||||
@ -615,9 +639,16 @@ void SyncObjectWithForm()
|
||||
_appName = _appNameInput.value;
|
||||
_appVersionName = _appVersionNameInput.value;
|
||||
|
||||
_containerRegistry = _containerRegistryInput.value;
|
||||
_containerImageTag = _containerImageTagInput.value;
|
||||
_containerImageRepo = _containerImageRepoInput.value;
|
||||
// MIRROR CHANGE ///////////////////////////////////////////////////
|
||||
// registry, repository and tag can not contain whitespaces.
|
||||
// otherwise it'll throw an error:
|
||||
// "ERROR: "docker buildx build" requires exactly 1 argument."
|
||||
// trim whitespace in case users accidentally added some.
|
||||
_containerRegistry = _containerRegistryInput.value.Trim();
|
||||
_containerImageTag = _containerImageTagInput.value.Trim();
|
||||
_containerImageRepo = _containerImageRepoInput.value.Trim();
|
||||
// END MIRROR CHANGE ///////////////////////////////////////////////
|
||||
|
||||
_autoIncrementTag = _autoIncrementTagInput.value;
|
||||
}
|
||||
|
||||
|
@ -133,7 +133,7 @@ protected virtual void Awake()
|
||||
if (statisticsLog)
|
||||
InvokeRepeating(nameof(OnLogStatistics), 1, 1);
|
||||
|
||||
Debug.Log("KcpTransport initialized!");
|
||||
Log.Info("KcpTransport initialized!");
|
||||
}
|
||||
|
||||
protected virtual void OnValidate()
|
||||
@ -344,7 +344,7 @@ protected virtual void OnLogStatistics()
|
||||
log += $" ReceiveQueue: {GetTotalReceiveQueue()}\n";
|
||||
log += $" SendBuffer: {GetTotalSendBuffer()}\n";
|
||||
log += $" ReceiveBuffer: {GetTotalReceiveBuffer()}\n\n";
|
||||
Debug.Log(log);
|
||||
Log.Info(log);
|
||||
}
|
||||
|
||||
if (ClientConnected())
|
||||
@ -356,7 +356,7 @@ protected virtual void OnLogStatistics()
|
||||
log += $" ReceiveQueue: {client.ReceiveQueueCount}\n";
|
||||
log += $" SendBuffer: {client.SendBufferCount}\n";
|
||||
log += $" ReceiveBuffer: {client.ReceiveBufferCount}\n\n";
|
||||
Debug.Log(log);
|
||||
Log.Info(log);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -122,7 +122,7 @@ protected override void Awake()
|
||||
// it'll be used by the created thread immediately.
|
||||
base.Awake();
|
||||
|
||||
Debug.Log("ThreadedKcpTransport initialized!");
|
||||
Log.Info("ThreadedKcpTransport initialized!");
|
||||
}
|
||||
|
||||
protected virtual void OnValidate()
|
||||
@ -300,7 +300,7 @@ protected virtual void OnLogStatistics()
|
||||
log += $" ReceiveQueue: {GetTotalReceiveQueue()}\n";
|
||||
log += $" SendBuffer: {GetTotalSendBuffer()}\n";
|
||||
log += $" ReceiveBuffer: {GetTotalReceiveBuffer()}\n\n";
|
||||
Debug.Log(log);
|
||||
Log.Info(log);
|
||||
}
|
||||
|
||||
if (ClientConnected())
|
||||
@ -312,7 +312,7 @@ protected virtual void OnLogStatistics()
|
||||
log += $" ReceiveQueue: {client.peer.ReceiveQueueCount}\n";
|
||||
log += $" SendBuffer: {client.peer.SendBufferCount}\n";
|
||||
log += $" ReceiveBuffer: {client.peer.ReceiveBufferCount}\n\n";
|
||||
Debug.Log(log);
|
||||
Log.Info(log);
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
@ -337,9 +337,10 @@ public override void Shutdown()
|
||||
public override string ToString()
|
||||
{
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.Append("Multiplexer:");
|
||||
|
||||
foreach (Transport transport in transports)
|
||||
builder.AppendLine(transport.ToString());
|
||||
builder.Append($" {transport}");
|
||||
|
||||
return builder.ToString().Trim();
|
||||
}
|
||||
|
@ -29,8 +29,6 @@ internal class ServerSslHelper
|
||||
|
||||
public ServerSslHelper(SslConfig sslConfig)
|
||||
{
|
||||
Console.Clear();
|
||||
|
||||
config = sslConfig;
|
||||
if (config.enabled)
|
||||
{
|
||||
|
@ -37,7 +37,7 @@ public void Listen(int port)
|
||||
listener = TcpListener.Create(port);
|
||||
listener.Start();
|
||||
|
||||
Log.Info($"[SWT-WebSocketServer]: Server Started on {port}!", ConsoleColor.Green);
|
||||
Log.Verbose($"[SWT-WebSocketServer]: Server Started on {port}");
|
||||
|
||||
acceptThread = new Thread(acceptLoop);
|
||||
acceptThread.IsBackground = true;
|
||||
@ -53,7 +53,7 @@ public void Stop()
|
||||
listener?.Stop();
|
||||
acceptThread = null;
|
||||
|
||||
Log.Info($"[SWT-WebSocketServer]: Server stopped...closing all connections.");
|
||||
Log.Verbose($"[SWT-WebSocketServer]: Server stopped...closing all connections.");
|
||||
|
||||
// make copy so that foreach doesn't break if values are removed
|
||||
Connection[] connectionsCopy = connections.Values.ToArray();
|
||||
|
@ -52,7 +52,31 @@ public class SimpleWebTransport : Transport, PortTransport
|
||||
|
||||
[Tooltip("Port to use for server")]
|
||||
public ushort port = 7778;
|
||||
public ushort Port { get => port; set => port = value; }
|
||||
public ushort Port
|
||||
{
|
||||
get
|
||||
{
|
||||
#if UNITY_WEBGL
|
||||
if (clientWebsocketSettings.ClientPortOption == WebsocketPortOption.SpecifyPort)
|
||||
return clientWebsocketSettings.CustomClientPort;
|
||||
else
|
||||
return port;
|
||||
#else
|
||||
return port;
|
||||
#endif
|
||||
}
|
||||
set
|
||||
{
|
||||
#if UNITY_WEBGL
|
||||
if (clientWebsocketSettings.ClientPortOption == WebsocketPortOption.SpecifyPort)
|
||||
clientWebsocketSettings.CustomClientPort = value;
|
||||
else
|
||||
port = value;
|
||||
#else
|
||||
port = value;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
[Tooltip("Groups messages in queue before calling Stream.Send")]
|
||||
public bool batchSend = true;
|
||||
@ -144,7 +168,7 @@ public override void ClientConnect(string hostname)
|
||||
// https://github.com/MirrorNetworking/Mirror/pull/3477
|
||||
break;
|
||||
default: // default case handles ClientWebsocketPortOption.DefaultSameAsServerPort
|
||||
builder.Port = Port;
|
||||
builder.Port = port;
|
||||
break;
|
||||
}
|
||||
|
||||
@ -176,11 +200,28 @@ public override void ClientConnect(Uri uri)
|
||||
|
||||
client.onData += (ArraySegment<byte> data) => OnClientDataReceived.Invoke(data, Channels.Reliable);
|
||||
|
||||
client.onError += (Exception e) =>
|
||||
// We will not invoke OnClientError if minLogLevel is set to None
|
||||
// We only send the full exception if minLogLevel is set to Verbose
|
||||
switch (Log.minLogLevel)
|
||||
{
|
||||
OnClientError.Invoke(TransportError.Unexpected, e.ToString());
|
||||
ClientDisconnect();
|
||||
};
|
||||
case Log.Levels.Flood:
|
||||
case Log.Levels.Verbose:
|
||||
client.onError += (Exception e) =>
|
||||
{
|
||||
OnClientError.Invoke(TransportError.Unexpected, e.ToString());
|
||||
ClientDisconnect();
|
||||
};
|
||||
break;
|
||||
case Log.Levels.Info:
|
||||
case Log.Levels.Warn:
|
||||
case Log.Levels.Error:
|
||||
client.onError += (Exception e) =>
|
||||
{
|
||||
OnClientError.Invoke(TransportError.Unexpected, e.Message);
|
||||
ClientDisconnect();
|
||||
};
|
||||
break;
|
||||
}
|
||||
|
||||
client.Connect(uri);
|
||||
}
|
||||
@ -256,7 +297,29 @@ public override void ServerStart()
|
||||
server.onConnect += OnServerConnected.Invoke;
|
||||
server.onDisconnect += OnServerDisconnected.Invoke;
|
||||
server.onData += (int connId, ArraySegment<byte> data) => OnServerDataReceived.Invoke(connId, data, Channels.Reliable);
|
||||
server.onError += (connId, exception) => OnServerError(connId, TransportError.Unexpected, exception.ToString());
|
||||
|
||||
// We will not invoke OnServerError if minLogLevel is set to None
|
||||
// We only send the full exception if minLogLevel is set to Verbose
|
||||
switch (Log.minLogLevel)
|
||||
{
|
||||
case Log.Levels.Flood:
|
||||
case Log.Levels.Verbose:
|
||||
server.onError += (connId, exception) =>
|
||||
{
|
||||
OnServerError(connId, TransportError.Unexpected, exception.ToString());
|
||||
ServerDisconnect(connId);
|
||||
};
|
||||
break;
|
||||
case Log.Levels.Info:
|
||||
case Log.Levels.Warn:
|
||||
case Log.Levels.Error:
|
||||
server.onError += (connId, exception) =>
|
||||
{
|
||||
OnServerError(connId, TransportError.Unexpected, exception.Message);
|
||||
ServerDisconnect(connId);
|
||||
};
|
||||
break;
|
||||
}
|
||||
|
||||
SendLoopConfig.batchSend = batchSend || waitBeforeSend;
|
||||
SendLoopConfig.sleepBeforeSend = waitBeforeSend;
|
||||
|
@ -250,25 +250,16 @@ public override int GetMaxPacketSize(int channelId)
|
||||
return serverMaxMessageSize;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
if (server != null && server.Active && server.listener != null)
|
||||
{
|
||||
// printing server.listener.LocalEndpoint causes an Exception
|
||||
// in UWP + Unity 2019:
|
||||
// Exception thrown at 0x00007FF9755DA388 in UWF.exe:
|
||||
// Microsoft C++ exception: Il2CppExceptionWrapper at memory
|
||||
// location 0x000000E15A0FCDD0. SocketException: An address
|
||||
// incompatible with the requested protocol was used at
|
||||
// System.Net.Sockets.Socket.get_LocalEndPoint ()
|
||||
// so let's use the regular port instead.
|
||||
return $"Telepathy Server port: {port}";
|
||||
}
|
||||
else if (client != null && (client.Connecting || client.Connected))
|
||||
{
|
||||
return $"Telepathy Client port: {port}";
|
||||
}
|
||||
return "Telepathy (inactive/disconnected)";
|
||||
}
|
||||
// Keep it short and simple so it looks nice in the HUD.
|
||||
//
|
||||
// printing server.listener.LocalEndpoint causes an Exception
|
||||
// in UWP + Unity 2019:
|
||||
// Exception thrown at 0x00007FF9755DA388 in UWF.exe:
|
||||
// Microsoft C++ exception: Il2CppExceptionWrapper at memory
|
||||
// location 0x000000E15A0FCDD0. SocketException: An address
|
||||
// incompatible with the requested protocol was used at
|
||||
// System.Net.Sockets.Socket.get_LocalEndPoint ()
|
||||
// so just use the regular port instead.
|
||||
public override string ToString() => $"Telepathy [{port}]";
|
||||
}
|
||||
}
|
||||
|
23
README.md
23
README.md
@ -1,15 +1,16 @@
|
||||
![Mirror Logo](https://user-images.githubusercontent.com/16416509/119120944-6db26780-ba5f-11eb-9cdd-fc8500207f4d.png)
|
||||
|
||||
[![Download](https://img.shields.io/badge/asset_store-brightgreen.svg)](https://assetstore.unity.com/packages/tools/network/mirror-129321)
|
||||
[![Documentation](https://img.shields.io/badge/docs-brightgreen.svg)](https://mirror-networking.gitbook.io/)
|
||||
[![Showcase](https://img.shields.io/badge/showcase-brightgreen.svg)](https://mirror-networking.com/showcase/)
|
||||
[![Video Tutorials](https://img.shields.io/badge/video_tutorial-brightgreen.svg)](https://mirror-networking.gitbook.io/docs/community-guides/video-tutorials)
|
||||
[![Forum](https://img.shields.io/badge/forum-brightgreen.svg)](https://forum.unity.com/threads/mirror-networking-for-unity-aka-hlapi-community-edition.425437/)
|
||||
[![Build](https://img.shields.io/appveyor/ci/vis2k73562/hlapi-community-edition/Mirror.svg)](https://ci.appveyor.com/project/vis2k73562/hlapi-community-edition/branch/mirror)
|
||||
[![Discord](https://img.shields.io/discord/343440455738064897.svg)](https://discordapp.com/invite/xVW4nU4C34)
|
||||
[![release](https://img.shields.io/github/release/vis2k/Mirror.svg)](https://github.com/vis2k/Mirror/releases/latest)
|
||||
[![License: MIT](https://img.shields.io/badge/License-MIT-brightgreen.svg)](https://github.com/vis2k/Mirror/blob/master/LICENSE)
|
||||
[![Roadmap](https://img.shields.io/badge/roadmap-blue.svg)](https://trello.com/b/fgAE7Tud)
|
||||
<p align="center">
|
||||
<a href="https://assetstore.unity.com/packages/tools/network/mirror-129321"><img src="https://img.shields.io/badge/download-brightgreen.svg?style=for-the-badge&logo=unity&colorA=363a4f&colorB=f5a97f" alt="Download"></a>
|
||||
<a href="https://github.com/MirrorNetworking/Mirror#made-with-mirror"><img src="https://img.shields.io/badge/showcase-brightgreen.svg?style=for-the-badge&logo=github&colorA=363a4f&colorB=f5a97f" alt="Showcase"></a>
|
||||
<a href="https://mirror-networking.gitbook.io/"><img src="https://img.shields.io/badge/docs-brightgreen.svg?style=for-the-badge&logo=gitbook&logoColor=white&colorA=363a4f&colorB=f5a97f" alt="Documentation"></a>
|
||||
<a href="https://forum.unity.com/threads/mirror-networking-for-unity-aka-hlapi-community-edition.425437/"><img src="https://img.shields.io/badge/forum-brightgreen.svg?style=for-the-badge&logo=unity&colorA=363a4f&colorB=f5a97f" alt="Forum"></a>
|
||||
<a href="https://trello.com/b/fgAE7Tud"><img src="https://img.shields.io/badge/roadmap-brightgreen.svg?style=for-the-badge&logo=trello&colorA=363a4f&colorB=f5a97f" alt="Roadmap"></a>
|
||||
<br>
|
||||
<a href="https://github.com/vis2k/Mirror/blob/master/LICENSE"><img src="https://img.shields.io/badge/License-MIT-brightgreen.svg?style=for-the-badge&colorA=363a4f&colorB=b7bdf8" alt="License: MIT"></a>
|
||||
<a href="https://ci.appveyor.com/project/vis2k73562/hlapi-community-edition/branch/mirror"><img src="https://img.shields.io/appveyor/ci/vis2k73562/hlapi-community-edition/Mirror.svg?style=for-the-badge&colorA=363a4f&colorB=b7bdf8" alt="Build"></a>
|
||||
<a href="https://github.com/vis2k/Mirror/releases/latest"><img src="https://img.shields.io/github/release/vis2k/Mirror.svg?style=for-the-badge&colorA=363a4f&colorB=b7bdf8" alt="release"></a>
|
||||
<a href="https://discordapp.com/invite/xVW4nU4C34"><img src="https://img.shields.io/discord/343440455738064897.svg?style=for-the-badge&colorA=363a4f&colorB=b7bdf8" alt="Discord"></a>
|
||||
</p>
|
||||
|
||||
**It's only the dreamers who ever move mountains.**
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user