mirror of
https://github.com/MirrorNetworking/Mirror.git
synced 2024-11-18 02:50:32 +00:00
Merged master
This commit is contained in:
commit
22fc1a814f
1
.gitignore
vendored
1
.gitignore
vendored
@ -42,6 +42,7 @@ Source/Source.sln
|
||||
Output/
|
||||
bin/
|
||||
.vs/
|
||||
.vscode/
|
||||
.idea/
|
||||
Mirror/packages
|
||||
.mfractor
|
||||
|
@ -14,27 +14,6 @@ public class NetworkDiscovery : NetworkDiscoveryBase<ServerRequest, ServerRespon
|
||||
{
|
||||
#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.active;
|
||||
|
||||
base.Start();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Process the request from a client
|
||||
/// </summary>
|
||||
@ -68,9 +47,11 @@ protected override ServerResponse ProcessRequest(ServerRequest request, IPEndPoi
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Client
|
||||
|
||||
/// <summary>
|
||||
/// Create a message that will be broadcasted on the network to discover servers
|
||||
/// </summary>
|
||||
@ -106,6 +87,7 @@ protected override void ProcessResponse(ServerResponse response, IPEndPoint endp
|
||||
|
||||
OnServerFound.Invoke(response);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
@ -23,33 +23,46 @@ public abstract class NetworkDiscoveryBase<Request, Response> : MonoBehaviour
|
||||
{
|
||||
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("If true, broadcasts a discovery request every ActiveDiscoveryInterval seconds")]
|
||||
public bool enableActiveDiscovery = true;
|
||||
|
||||
// broadcast address needs to be configurable on iOS:
|
||||
// https://github.com/vis2k/Mirror/pull/3255
|
||||
[Tooltip("iOS may require LAN IP address here (e.g. 192.168.x.x), otherwise leave blank.")]
|
||||
public string BroadcastAddress = "";
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("The UDP port the server will listen for multi-cast messages")]
|
||||
protected int serverBroadcastListenPort = 47777;
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("If true, broadcasts a discovery request every ActiveDiscoveryInterval seconds")]
|
||||
public bool enableActiveDiscovery = true;
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("Time in seconds between multi-cast messages")]
|
||||
[Range(1, 60)]
|
||||
float ActiveDiscoveryInterval = 3;
|
||||
|
||||
// broadcast address needs to be configurable on iOS:
|
||||
// https://github.com/vis2k/Mirror/pull/3255
|
||||
public string BroadcastAddress = "";
|
||||
[Tooltip("Transport to be advertised during discovery")]
|
||||
public Transport transport;
|
||||
|
||||
[Tooltip("Invoked when a server is found")]
|
||||
public ServerFoundUnityEvent OnServerFound;
|
||||
|
||||
// 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;
|
||||
|
||||
public long ServerId { get; private set; }
|
||||
|
||||
protected UdpClient serverUdpClient;
|
||||
protected UdpClient clientUdpClient;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
void OnValidate()
|
||||
public virtual void OnValidate()
|
||||
{
|
||||
if (transport == null)
|
||||
transport = GetComponent<Transport>();
|
||||
|
||||
if (secretHandshake == 0)
|
||||
{
|
||||
secretHandshake = RandomLong();
|
||||
@ -58,24 +71,32 @@ void OnValidate()
|
||||
}
|
||||
#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()
|
||||
{
|
||||
ServerId = RandomLong();
|
||||
|
||||
// active transport gets initialized in Awake
|
||||
// so make sure we set it here in Start() after Awake
|
||||
// Or just let the user assign it in the inspector
|
||||
if (transport == null)
|
||||
transport = Transport.active;
|
||||
|
||||
// Server mode? then start advertising
|
||||
#if UNITY_SERVER
|
||||
AdvertiseServer();
|
||||
#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);
|
||||
}
|
||||
|
||||
// Ensure the ports are cleared no matter when Game/Unity UI exits
|
||||
void OnApplicationQuit()
|
||||
{
|
||||
@ -166,9 +187,7 @@ public async Task ServerListenAsync()
|
||||
// socket has been closed
|
||||
break;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
catch (Exception) {}
|
||||
}
|
||||
}
|
||||
|
||||
@ -247,6 +266,7 @@ protected virtual void ProcessClientRequest(Request request, IPEndPoint endpoint
|
||||
AndroidJavaObject multicastLock;
|
||||
bool hasMulticastLock;
|
||||
#endif
|
||||
|
||||
void BeginMulticastLock()
|
||||
{
|
||||
#if UNITY_ANDROID
|
||||
|
@ -39,17 +39,22 @@ public override void OnSpawned(NetworkIdentity identity)
|
||||
|
||||
// Match ID could have been set in NetworkBehaviour::OnStartServer on this object.
|
||||
// Since that's after OnCheckObserver is called it would be missed, so force Rebuild here.
|
||||
RebuildMatchObservers(networkMatchId);
|
||||
// Add the current match to dirtyMatches for Update to rebuild it.
|
||||
dirtyMatches.Add(networkMatchId);
|
||||
}
|
||||
|
||||
[ServerCallback]
|
||||
public override void OnDestroyed(NetworkIdentity identity)
|
||||
{
|
||||
// Don't RebuildSceneObservers here - that will happen in Update.
|
||||
// Multiple objects could be destroyed in same frame and we don't
|
||||
// want to rebuild for each one...let Update do it once.
|
||||
// We must add the current match to dirtyMatches for Update to rebuild it.
|
||||
if (lastObjectMatch.TryGetValue(identity, out Guid currentMatch))
|
||||
{
|
||||
lastObjectMatch.Remove(identity);
|
||||
if (currentMatch != Guid.Empty && matchObjects.TryGetValue(currentMatch, out HashSet<NetworkIdentity> objects) && objects.Remove(identity))
|
||||
RebuildMatchObservers(currentMatch);
|
||||
dirtyMatches.Add(currentMatch);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -35,11 +35,15 @@ public override void OnSpawned(NetworkIdentity identity)
|
||||
[ServerCallback]
|
||||
public override void OnDestroyed(NetworkIdentity identity)
|
||||
{
|
||||
// Don't RebuildSceneObservers here - that will happen in Update.
|
||||
// Multiple objects could be destroyed in same frame and we don't
|
||||
// want to rebuild for each one...let Update do it once.
|
||||
// We must add the current scene to dirtyScenes for Update to rebuild it.
|
||||
if (lastObjectScene.TryGetValue(identity, out Scene currentScene))
|
||||
{
|
||||
lastObjectScene.Remove(identity);
|
||||
if (sceneObjects.TryGetValue(currentScene, out HashSet<NetworkIdentity> objects) && objects.Remove(identity))
|
||||
RebuildSceneObservers(currentScene);
|
||||
dirtyScenes.Add(currentScene);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -35,17 +35,22 @@ public override void OnSpawned(NetworkIdentity identity)
|
||||
|
||||
// Team ID could have been set in NetworkBehaviour::OnStartServer on this object.
|
||||
// Since that's after OnCheckObserver is called it would be missed, so force Rebuild here.
|
||||
RebuildTeamObservers(networkTeamId);
|
||||
// Add the current team to dirtyTeams for Update to rebuild it.
|
||||
dirtyTeams.Add(networkTeamId);
|
||||
}
|
||||
|
||||
[ServerCallback]
|
||||
public override void OnDestroyed(NetworkIdentity identity)
|
||||
{
|
||||
// Don't RebuildSceneObservers here - that will happen in Update.
|
||||
// Multiple objects could be destroyed in same frame and we don't
|
||||
// want to rebuild for each one...let Update do it once.
|
||||
// We must add the current team to dirtyTeams for Update to rebuild it.
|
||||
if (lastObjectTeam.TryGetValue(identity, out string currentTeam))
|
||||
{
|
||||
lastObjectTeam.Remove(identity);
|
||||
if (!string.IsNullOrWhiteSpace(currentTeam) && teamObjects.TryGetValue(currentTeam, out HashSet<NetworkIdentity> objects) && objects.Remove(identity))
|
||||
RebuildTeamObservers(currentTeam);
|
||||
dirtyTeams.Add(currentTeam);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -118,47 +118,6 @@ public override void OnValidate()
|
||||
}
|
||||
}
|
||||
|
||||
public 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(NetworkConnectionToClient conn)
|
||||
{
|
||||
Debug.Log($"NetworkRoomManager OnServerReady {conn}");
|
||||
base.OnServerReady(conn);
|
||||
|
||||
if (conn != null && conn.identity != null)
|
||||
{
|
||||
GameObject roomPlayer = conn.identity.gameObject;
|
||||
|
||||
// if null or not a room player, don't replace it
|
||||
if (roomPlayer != null && roomPlayer.GetComponent<NetworkRoomPlayer>() != null)
|
||||
SceneLoadedForPlayer(conn, roomPlayer);
|
||||
}
|
||||
}
|
||||
|
||||
void SceneLoadedForPlayer(NetworkConnectionToClient conn, GameObject roomPlayer)
|
||||
{
|
||||
Debug.Log($"NetworkRoom SceneLoadedForPlayer scene: {SceneManager.GetActiveScene().path} {conn}");
|
||||
@ -190,6 +149,26 @@ void SceneLoadedForPlayer(NetworkConnectionToClient conn, GameObject roomPlayer)
|
||||
NetworkServer.ReplacePlayerForConnection(conn, gamePlayer, true);
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
/// <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>
|
||||
@ -215,26 +194,6 @@ public void CheckReadyToBegin()
|
||||
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>
|
||||
@ -301,6 +260,26 @@ public override void OnServerDisconnect(NetworkConnectionToClient 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 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(NetworkConnectionToClient conn)
|
||||
{
|
||||
Debug.Log($"NetworkRoomManager OnServerReady {conn}");
|
||||
base.OnServerReady(conn);
|
||||
|
||||
if (conn != null && conn.identity != null)
|
||||
{
|
||||
GameObject roomPlayer = conn.identity.gameObject;
|
||||
|
||||
// if null or not a room player, don't replace it
|
||||
if (roomPlayer != null && roomPlayer.GetComponent<NetworkRoomPlayer>() != null)
|
||||
SceneLoadedForPlayer(conn, roomPlayer);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called on the server when a client adds a new player with NetworkClient.AddPlayer.
|
||||
/// <para>The default implementation for this function creates a new player object from the playerPrefab.</para>
|
||||
@ -597,6 +576,30 @@ public virtual bool OnRoomServerSceneLoadedForPlayer(NetworkConnectionToClient c
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This is called on server from NetworkRoomPlayer.CmdChangeReadyState when client indicates change in Ready status.
|
||||
/// </summary>
|
||||
public virtual 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>
|
||||
/// 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>
|
||||
|
@ -147,8 +147,8 @@ void OnGUI()
|
||||
if (NetworkClient.active || NetworkServer.active)
|
||||
{
|
||||
// create main GUI area
|
||||
// 105 is below NetworkManager HUD in all cases.
|
||||
GUILayout.BeginArea(new Rect(10, 105, 215, 300));
|
||||
// 120 is below NetworkManager HUD in all cases.
|
||||
GUILayout.BeginArea(new Rect(10, 120, 215, 300));
|
||||
|
||||
// show client / server stats if active
|
||||
if (NetworkClient.active) OnClientGUI();
|
||||
|
@ -45,7 +45,7 @@ public abstract class NetworkTransformBase : NetworkBehaviour
|
||||
[Header("Selective Sync\nDon't change these at Runtime")]
|
||||
public bool syncPosition = true; // do not change at runtime!
|
||||
public bool syncRotation = true; // do not change at runtime!
|
||||
public bool syncScale = false; // do not change at runtime! rare. off by default.
|
||||
public bool syncScale = false; // do not change at runtime! rare. off by default.
|
||||
|
||||
// interpolation is on by default, but can be disabled to jump to
|
||||
// the destination immediately. some projects need this.
|
||||
@ -60,12 +60,12 @@ public abstract class NetworkTransformBase : NetworkBehaviour
|
||||
// debugging ///////////////////////////////////////////////////////////
|
||||
[Header("Debug")]
|
||||
public bool showGizmos;
|
||||
public bool showOverlay;
|
||||
public bool showOverlay;
|
||||
public Color overlayColor = new Color(0, 0, 0, 0.5f);
|
||||
|
||||
// initialization //////////////////////////////////////////////////////
|
||||
// make sure to call this when inheriting too!
|
||||
protected virtual void Awake() {}
|
||||
protected virtual void Awake() { }
|
||||
|
||||
protected virtual void OnValidate()
|
||||
{
|
||||
@ -82,13 +82,13 @@ protected virtual void OnValidate()
|
||||
// obsolete clientAuthority compatibility:
|
||||
// if it was used, then set the new SyncDirection automatically.
|
||||
// if it wasn't used, then don't touch syncDirection.
|
||||
#pragma warning disable CS0618
|
||||
#pragma warning disable CS0618
|
||||
if (clientAuthority)
|
||||
{
|
||||
syncDirection = SyncDirection.ClientToServer;
|
||||
Debug.LogWarning($"{name}'s NetworkTransform component has obsolete .clientAuthority enabled. Please disable it and set SyncDirection to ClientToServer instead.");
|
||||
}
|
||||
#pragma warning restore CS0618
|
||||
#pragma warning restore CS0618
|
||||
}
|
||||
|
||||
// snapshot functions //////////////////////////////////////////////////
|
||||
@ -99,13 +99,8 @@ protected virtual TransformSnapshot Construct()
|
||||
// NetworkTime.localTime for double precision until Unity has it too
|
||||
return new TransformSnapshot(
|
||||
// our local time is what the other end uses as remote time
|
||||
#if !UNITY_2020_3_OR_NEWER
|
||||
NetworkTime.localTime, // Unity 2019 doesn't have timeAsDouble yet
|
||||
#else
|
||||
Time.timeAsDouble,
|
||||
#endif
|
||||
// the other end fills out local time itself
|
||||
0,
|
||||
0, // the other end fills out local time itself
|
||||
target.localPosition,
|
||||
target.localRotation,
|
||||
target.localScale
|
||||
@ -125,16 +120,12 @@ protected void AddSnapshot(SortedList<double, TransformSnapshot> snapshots, doub
|
||||
// replay it for 10 seconds.
|
||||
if (!position.HasValue) position = snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].position : target.localPosition;
|
||||
if (!rotation.HasValue) rotation = snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].rotation : target.localRotation;
|
||||
if (!scale.HasValue) scale = snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].scale : target.localScale;
|
||||
if (!scale.HasValue) scale = snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].scale : target.localScale;
|
||||
|
||||
// insert transform snapshot
|
||||
SnapshotInterpolation.InsertIfNotExists(snapshots, new TransformSnapshot(
|
||||
timeStamp, // arrival remote timestamp. NOT remote time.
|
||||
#if !UNITY_2020_3_OR_NEWER
|
||||
NetworkTime.localTime, // Unity 2019 doesn't have timeAsDouble yet
|
||||
#else
|
||||
Time.timeAsDouble,
|
||||
#endif
|
||||
position.Value,
|
||||
rotation.Value,
|
||||
scale.Value
|
||||
|
@ -11,9 +11,18 @@ public class NetworkTransformReliable : NetworkTransformBase
|
||||
[Header("Sync Only If Changed")]
|
||||
[Tooltip("When true, changes are not sent unless greater than sensitivity values below.")]
|
||||
public bool onlySyncOnChange = true;
|
||||
|
||||
uint sendIntervalCounter = 0;
|
||||
double lastSendIntervalTime = double.MinValue;
|
||||
|
||||
[Tooltip("If we only sync on change, then we need to correct old snapshots if more time than sendInterval * multiplier has elapsed.\n\nOtherwise the first move will always start interpolating from the last move sequence's time, which will make it stutter when starting every time.")]
|
||||
public float onlySyncOnChangeCorrectionMultiplier = 2;
|
||||
|
||||
[Header("Send Interval Multiplier")]
|
||||
[Tooltip("Check/Sync every multiple of Network Manager send interval (= 1 / NM Send Rate), instead of every send interval.")]
|
||||
[Range(1, 120)]
|
||||
public uint sendIntervalMultiplier = 3;
|
||||
|
||||
[Header("Rotation")]
|
||||
[Tooltip("Sensitivity of changes needed before an updated state is sent over the network")]
|
||||
public float rotationSensitivity = 0.01f;
|
||||
@ -31,31 +40,67 @@ public class NetworkTransformReliable : NetworkTransformBase
|
||||
[Range(0.00_01f, 1f)] // disallow 0 division. 1mm to 1m precision is enough range.
|
||||
public float positionPrecision = 0.01f; // 1 cm
|
||||
[Range(0.00_01f, 1f)] // disallow 0 division. 1mm to 1m precision is enough range.
|
||||
public float scalePrecision = 0.01f; // 1 cm
|
||||
public float scalePrecision = 0.01f; // 1 cm
|
||||
|
||||
[Header("Snapshot Interpolation")]
|
||||
[Tooltip("Add a small timeline offset to account for decoupled arrival of NetworkTime and NetworkTransform snapshots.\nfixes: https://github.com/MirrorNetworking/Mirror/issues/3427")]
|
||||
public bool timelineOffset = false;
|
||||
|
||||
// Ninja's Notes on offset & mulitplier:
|
||||
//
|
||||
// In a no multiplier scenario:
|
||||
// 1. Snapshots are sent every frame (frame being 1 NM send interval).
|
||||
// 2. Time Interpolation is set to be 'behind' by 2 frames times.
|
||||
// In theory where everything works, we probably have around 2 snapshots before we need to interpolate snapshots. From NT perspective, we should always have around 2 snapshots ready, so no stutter.
|
||||
//
|
||||
// In a multiplier scenario:
|
||||
// 1. Snapshots are sent every 10 frames.
|
||||
// 2. Time Interpolation remains 'behind by 2 frames'.
|
||||
// When everything works, we are receiving NT snapshots every 10 frames, but start interpolating after 2.
|
||||
// Even if I assume we had 2 snapshots to begin with to start interpolating (which we don't), by the time we reach 13th frame, we are out of snapshots, and have to wait 7 frames for next snapshot to come. This is the reason why we absolutely need the timestamp adjustment. We are starting way too early to interpolate.
|
||||
//
|
||||
double timeStampAdjustment => NetworkServer.sendInterval * (sendIntervalMultiplier - 1);
|
||||
double offset => timelineOffset ? NetworkServer.sendInterval * sendIntervalMultiplier : 0;
|
||||
|
||||
// delta compression needs to remember 'last' to compress against
|
||||
protected Vector3Long lastSerializedPosition = Vector3Long.zero;
|
||||
protected Vector3Long lastSerializedPosition = Vector3Long.zero;
|
||||
protected Vector3Long lastDeserializedPosition = Vector3Long.zero;
|
||||
|
||||
protected Vector3Long lastSerializedScale = Vector3Long.zero;
|
||||
protected Vector3Long lastDeserializedScale = Vector3Long.zero;
|
||||
protected Vector3Long lastSerializedScale = Vector3Long.zero;
|
||||
protected Vector3Long lastDeserializedScale = Vector3Long.zero;
|
||||
|
||||
// Used to store last sent snapshots
|
||||
protected TransformSnapshot last;
|
||||
|
||||
int lastClientCount = 0;
|
||||
protected int lastClientCount = 1;
|
||||
|
||||
// update //////////////////////////////////////////////////////////////
|
||||
void Update()
|
||||
{
|
||||
// if server then always sync to others.
|
||||
if (isServer) UpdateServer();
|
||||
if (isServer) UpdateServer();
|
||||
// 'else if' because host mode shouldn't send anything to server.
|
||||
// it is the server. don't overwrite anything there.
|
||||
else if (isClient) UpdateClient();
|
||||
}
|
||||
|
||||
void UpdateServer()
|
||||
void LateUpdate()
|
||||
{
|
||||
// set dirty to trigger OnSerialize. either always, or only if changed.
|
||||
// It has to be checked in LateUpdate() for onlySyncOnChange to avoid
|
||||
// the possibility of Update() running first before the object's movement
|
||||
// script's Update(), which then causes NT to send every alternate frame
|
||||
// instead.
|
||||
if (isServer || (IsClientWithAuthority && NetworkClient.ready))
|
||||
{
|
||||
if (sendIntervalCounter == sendIntervalMultiplier && (!onlySyncOnChange || Changed(Construct())))
|
||||
SetDirty();
|
||||
|
||||
CheckLastSendTime();
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void UpdateServer()
|
||||
{
|
||||
// apply buffered snapshots IF client authority
|
||||
// -> in server authority, server moves the object
|
||||
@ -85,39 +130,16 @@ void UpdateServer()
|
||||
Apply(computed, to);
|
||||
}
|
||||
}
|
||||
|
||||
// set dirty to trigger OnSerialize. either always, or only if changed.
|
||||
// technically snapshot interpolation requires constant sending.
|
||||
// however, with reliable it should be fine without constant sends.
|
||||
//
|
||||
// detect changes _after_ all changes were applied above.
|
||||
if (!onlySyncOnChange || Changed(Construct()))
|
||||
SetDirty();
|
||||
}
|
||||
|
||||
void UpdateClient()
|
||||
protected virtual void UpdateClient()
|
||||
{
|
||||
// client authority, and local player (= allowed to move myself)?
|
||||
if (IsClientWithAuthority)
|
||||
if (!IsClientWithAuthority)
|
||||
{
|
||||
// https://github.com/vis2k/Mirror/pull/2992/
|
||||
if (!NetworkClient.ready) return;
|
||||
|
||||
// set dirty to trigger OnSerialize. either always, or only if changed.
|
||||
// technically snapshot interpolation requires constant sending.
|
||||
// however, with reliable it should be fine without constant sends.
|
||||
if (!onlySyncOnChange || Changed(Construct()))
|
||||
SetDirty();
|
||||
}
|
||||
// for all other clients (and for local player if !authority),
|
||||
// we need to apply snapshots from the buffer
|
||||
else
|
||||
{
|
||||
|
||||
// only while we have snapshots
|
||||
if (clientSnapshots.Count > 0)
|
||||
{
|
||||
|
||||
// step the interpolation without touching time.
|
||||
// NetworkClient is responsible for time globally.
|
||||
SnapshotInterpolation.StepInterpolation(
|
||||
@ -130,23 +152,23 @@ void UpdateClient()
|
||||
// interpolate & apply
|
||||
TransformSnapshot computed = TransformSnapshot.Interpolate(from, to, t);
|
||||
Apply(computed, to);
|
||||
|
||||
}
|
||||
|
||||
// 'only sync if moved'
|
||||
// explain..
|
||||
// from 1 snap to next snap..
|
||||
// it'll be old...
|
||||
if (lastClientCount > 1 && clientSnapshots.Count == 1)
|
||||
{
|
||||
// this is it. snapshots are down to '1'.
|
||||
// does this cause stuck?
|
||||
}
|
||||
|
||||
lastClientCount = clientSnapshots.Count;
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void CheckLastSendTime()
|
||||
{
|
||||
// timeAsDouble not available in older Unity versions.
|
||||
if (AccurateInterval.Elapsed(NetworkTime.localTime, NetworkServer.sendInterval, ref lastSendIntervalTime))
|
||||
{
|
||||
if (sendIntervalCounter == sendIntervalMultiplier)
|
||||
sendIntervalCounter = 0;
|
||||
sendIntervalCounter++;
|
||||
}
|
||||
}
|
||||
|
||||
// check if position / rotation / scale changed since last sync
|
||||
protected virtual bool Changed(TransformSnapshot current) =>
|
||||
// position is quantized and delta compressed.
|
||||
@ -191,6 +213,16 @@ public override void OnSerialize(NetworkWriter writer, bool initialState)
|
||||
// initial
|
||||
if (initialState)
|
||||
{
|
||||
// If there is a last serialized snapshot, we use it.
|
||||
// This prevents the new client getting a snapshot that is different
|
||||
// from what the older clients last got. If this happens, and on the next
|
||||
// regular serialisation the delta compression will get wrong values.
|
||||
// Notes:
|
||||
// 1. Interestingly only the older clients have it wrong, because at the end
|
||||
// of this function, last = snapshot which is the initial state's snapshot
|
||||
// 2. Regular NTR gets by this bug because it sends every frame anyway so initialstate
|
||||
// snapshot constructed would have been the same as the last anyway.
|
||||
if (last.remoteTime > 0) snapshot = last;
|
||||
if (syncPosition) writer.WriteVector3(snapshot.position);
|
||||
if (syncRotation)
|
||||
{
|
||||
@ -200,7 +232,7 @@ public override void OnSerialize(NetworkWriter writer, bool initialState)
|
||||
else
|
||||
writer.WriteQuaternion(snapshot.rotation);
|
||||
}
|
||||
if (syncScale) writer.WriteVector3(snapshot.scale);
|
||||
if (syncScale) writer.WriteVector3(snapshot.scale);
|
||||
}
|
||||
// delta
|
||||
else
|
||||
@ -227,14 +259,11 @@ public override void OnSerialize(NetworkWriter writer, bool initialState)
|
||||
Compression.ScaleToLong(snapshot.scale, scalePrecision, out Vector3Long quantized);
|
||||
DeltaCompression.Compress(writer, lastSerializedScale, quantized);
|
||||
}
|
||||
|
||||
// int written = writer.Position - before;
|
||||
// Debug.Log($"{name} compressed to {written} bytes");
|
||||
}
|
||||
|
||||
// save serialized as 'last' for next delta compression
|
||||
if (syncPosition) Compression.ScaleToLong(snapshot.position, positionPrecision, out lastSerializedPosition);
|
||||
if (syncScale) Compression.ScaleToLong(snapshot.scale, scalePrecision, out lastSerializedScale);
|
||||
if (syncScale) Compression.ScaleToLong(snapshot.scale, scalePrecision, out lastSerializedScale);
|
||||
|
||||
// set 'last'
|
||||
last = snapshot;
|
||||
@ -242,9 +271,9 @@ public override void OnSerialize(NetworkWriter writer, bool initialState)
|
||||
|
||||
public override void OnDeserialize(NetworkReader reader, bool initialState)
|
||||
{
|
||||
Vector3? position = null;
|
||||
Vector3? position = null;
|
||||
Quaternion? rotation = null;
|
||||
Vector3? scale = null;
|
||||
Vector3? scale = null;
|
||||
|
||||
// initial
|
||||
if (initialState)
|
||||
@ -258,7 +287,7 @@ public override void OnDeserialize(NetworkReader reader, bool initialState)
|
||||
else
|
||||
rotation = reader.ReadQuaternion();
|
||||
}
|
||||
if (syncScale) scale = reader.ReadVector3();
|
||||
if (syncScale) scale = reader.ReadVector3();
|
||||
}
|
||||
// delta
|
||||
else
|
||||
@ -286,12 +315,12 @@ public override void OnDeserialize(NetworkReader reader, bool initialState)
|
||||
|
||||
// handle depending on server / client / host.
|
||||
// server has priority for host mode.
|
||||
if (isServer) OnClientToServerSync(position, rotation, scale);
|
||||
if (isServer) OnClientToServerSync(position, rotation, scale);
|
||||
else if (isClient) OnServerToClientSync(position, rotation, scale);
|
||||
|
||||
// save deserialized as 'last' for next delta compression
|
||||
if (syncPosition) Compression.ScaleToLong(position.Value, positionPrecision, out lastDeserializedPosition);
|
||||
if (syncScale) Compression.ScaleToLong(scale.Value, scalePrecision, out lastDeserializedScale);
|
||||
if (syncScale) Compression.ScaleToLong(scale.Value, scalePrecision, out lastDeserializedScale);
|
||||
}
|
||||
|
||||
// sync ////////////////////////////////////////////////////////////////
|
||||
@ -307,20 +336,24 @@ protected virtual void OnClientToServerSync(Vector3? position, Quaternion? rotat
|
||||
|
||||
// 'only sync on change' needs a correction on every new move sequence.
|
||||
if (onlySyncOnChange &&
|
||||
NeedsCorrection(serverSnapshots, connectionToClient.remoteTimeStamp, NetworkServer.sendInterval, onlySyncOnChangeCorrectionMultiplier))
|
||||
NeedsCorrection(serverSnapshots, connectionToClient.remoteTimeStamp, NetworkServer.sendInterval * sendIntervalMultiplier, onlySyncOnChangeCorrectionMultiplier))
|
||||
{
|
||||
RewriteHistory(
|
||||
serverSnapshots,
|
||||
connectionToClient.remoteTimeStamp,
|
||||
NetworkTime.localTime, // arrival remote timestamp. NOT remote timeline.
|
||||
NetworkServer.sendInterval, // Unity 2019 doesn't have timeAsDouble yet
|
||||
NetworkTime.localTime, // arrival remote timestamp. NOT remote timeline.
|
||||
NetworkServer.sendInterval * sendIntervalMultiplier, // Unity 2019 doesn't have timeAsDouble yet
|
||||
target.localPosition,
|
||||
target.localRotation,
|
||||
target.localScale);
|
||||
// Debug.Log($"{name}: corrected history on server to fix initial stutter after not sending for a while.");
|
||||
}
|
||||
|
||||
AddSnapshot(serverSnapshots, connectionToClient.remoteTimeStamp, position, rotation, scale);
|
||||
// add a small timeline offset to account for decoupled arrival of
|
||||
// NetworkTime and NetworkTransform snapshots.
|
||||
// needs to be sendInterval. half sendInterval doesn't solve it.
|
||||
// https://github.com/MirrorNetworking/Mirror/issues/3427
|
||||
// remove this after LocalWorldState.
|
||||
AddSnapshot(serverSnapshots, connectionToClient.remoteTimeStamp + timeStampAdjustment + offset, position, rotation, scale);
|
||||
}
|
||||
|
||||
// server broadcasts sync message to all clients
|
||||
@ -331,20 +364,24 @@ protected virtual void OnServerToClientSync(Vector3? position, Quaternion? rotat
|
||||
|
||||
// 'only sync on change' needs a correction on every new move sequence.
|
||||
if (onlySyncOnChange &&
|
||||
NeedsCorrection(clientSnapshots, NetworkClient.connection.remoteTimeStamp, NetworkClient.sendInterval, onlySyncOnChangeCorrectionMultiplier))
|
||||
NeedsCorrection(clientSnapshots, NetworkClient.connection.remoteTimeStamp, NetworkClient.sendInterval * sendIntervalMultiplier, onlySyncOnChangeCorrectionMultiplier))
|
||||
{
|
||||
RewriteHistory(
|
||||
clientSnapshots,
|
||||
NetworkClient.connection.remoteTimeStamp, // arrival remote timestamp. NOT remote timeline.
|
||||
NetworkTime.localTime, // Unity 2019 doesn't have timeAsDouble yet
|
||||
NetworkClient.sendInterval,
|
||||
NetworkClient.connection.remoteTimeStamp, // arrival remote timestamp. NOT remote timeline.
|
||||
NetworkTime.localTime, // Unity 2019 doesn't have timeAsDouble yet
|
||||
NetworkClient.sendInterval * sendIntervalMultiplier,
|
||||
target.localPosition,
|
||||
target.localRotation,
|
||||
target.localScale);
|
||||
// Debug.Log($"{name}: corrected history on client to fix initial stutter after not sending for a while.");
|
||||
}
|
||||
|
||||
AddSnapshot(clientSnapshots, NetworkClient.connection.remoteTimeStamp, position, rotation, scale);
|
||||
// add a small timeline offset to account for decoupled arrival of
|
||||
// NetworkTime and NetworkTransform snapshots.
|
||||
// needs to be sendInterval. half sendInterval doesn't solve it.
|
||||
// https://github.com/MirrorNetworking/Mirror/issues/3427
|
||||
// remove this after LocalWorldState.
|
||||
AddSnapshot(clientSnapshots, NetworkClient.connection.remoteTimeStamp + timeStampAdjustment + offset, position, rotation, scale);
|
||||
}
|
||||
|
||||
// only sync on change /////////////////////////////////////////////////
|
||||
@ -394,11 +431,14 @@ public override void Reset()
|
||||
base.Reset();
|
||||
|
||||
// reset delta
|
||||
lastSerializedPosition = Vector3Long.zero;
|
||||
lastSerializedPosition = Vector3Long.zero;
|
||||
lastDeserializedPosition = Vector3Long.zero;
|
||||
|
||||
lastSerializedScale = Vector3Long.zero;
|
||||
lastSerializedScale = Vector3Long.zero;
|
||||
lastDeserializedScale = Vector3Long.zero;
|
||||
|
||||
// reset 'last' for delta too
|
||||
last = new TransformSnapshot(0, 0, Vector3.zero, Quaternion.identity, Vector3.zero);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ public class NetworkTransform : NetworkTransformBase
|
||||
[Header("Sensitivity"), Tooltip("Sensitivity of changes needed before an updated state is sent over the network")]
|
||||
public float positionSensitivity = 0.01f;
|
||||
public float rotationSensitivity = 0.01f;
|
||||
public float scaleSensitivity = 0.01f;
|
||||
public float scaleSensitivity = 0.01f;
|
||||
|
||||
protected bool positionChanged;
|
||||
protected bool rotationChanged;
|
||||
@ -29,24 +29,65 @@ public class NetworkTransform : NetworkTransformBase
|
||||
|
||||
// Used to store last sent snapshots
|
||||
protected TransformSnapshot lastSnapshot;
|
||||
protected bool cachedSnapshotComparison;
|
||||
protected bool hasSentUnchangedPosition;
|
||||
protected bool cachedSnapshotComparison;
|
||||
protected bool hasSentUnchangedPosition;
|
||||
#endif
|
||||
|
||||
double lastClientSendTime;
|
||||
double lastServerSendTime;
|
||||
|
||||
[Header("Send Interval Multiplier")]
|
||||
[Tooltip("Check/Sync every multiple of Network Manager send interval (= 1 / NM Send Rate), instead of every send interval.")]
|
||||
[Range(1, 120)]
|
||||
const uint sendIntervalMultiplier = 1; // not implemented yet
|
||||
|
||||
[Header("Snapshot Interpolation")]
|
||||
[Tooltip("Add a small timeline offset to account for decoupled arrival of NetworkTime and NetworkTransform snapshots.\nfixes: https://github.com/MirrorNetworking/Mirror/issues/3427")]
|
||||
public bool timelineOffset = false;
|
||||
|
||||
// Ninja's Notes on offset & mulitplier:
|
||||
//
|
||||
// In a no multiplier scenario:
|
||||
// 1. Snapshots are sent every frame (frame being 1 NM send interval).
|
||||
// 2. Time Interpolation is set to be 'behind' by 2 frames times.
|
||||
// In theory where everything works, we probably have around 2 snapshots before we need to interpolate snapshots. From NT perspective, we should always have around 2 snapshots ready, so no stutter.
|
||||
//
|
||||
// In a multiplier scenario:
|
||||
// 1. Snapshots are sent every 10 frames.
|
||||
// 2. Time Interpolation remains 'behind by 2 frames'.
|
||||
// When everything works, we are receiving NT snapshots every 10 frames, but start interpolating after 2.
|
||||
// Even if I assume we had 2 snapshots to begin with to start interpolating (which we don't), by the time we reach 13th frame, we are out of snapshots, and have to wait 7 frames for next snapshot to come. This is the reason why we absolutely need the timestamp adjustment. We are starting way too early to interpolate.
|
||||
//
|
||||
double timeStampAdjustment => NetworkServer.sendInterval * (sendIntervalMultiplier - 1);
|
||||
double offset => timelineOffset ? NetworkServer.sendInterval * sendIntervalMultiplier : 0;
|
||||
|
||||
// update //////////////////////////////////////////////////////////////
|
||||
// Update applies interpolation
|
||||
void Update()
|
||||
{
|
||||
// if server then always sync to others.
|
||||
if (isServer) UpdateServer();
|
||||
// 'else if' because host mode shouldn't send anything to server.
|
||||
// it is the server. don't overwrite anything there.
|
||||
else if (isClient) UpdateClient();
|
||||
if (isServer) UpdateServerInterpolation();
|
||||
// for all other clients (and for local player if !authority),
|
||||
// we need to apply snapshots from the buffer.
|
||||
// 'else if' because host mode shouldn't interpolate client
|
||||
else if (isClient && !IsClientWithAuthority) UpdateClientInterpolation();
|
||||
}
|
||||
|
||||
void UpdateServer()
|
||||
// LateUpdate broadcasts.
|
||||
// movement scripts may change positions in Update.
|
||||
// use LateUpdate to ensure changes are detected in the same frame.
|
||||
// otherwise this may run before user update, delaying detection until next frame.
|
||||
// this could cause visible jitter.
|
||||
void LateUpdate()
|
||||
{
|
||||
// if server then always sync to others.
|
||||
if (isServer) UpdateServerBroadcast();
|
||||
// client authority, and local player (= allowed to move myself)?
|
||||
// 'else if' because host mode shouldn't send anything to server.
|
||||
// it is the server. don't overwrite anything there.
|
||||
else if (isClient && IsClientWithAuthority) UpdateClientBroadcast();
|
||||
}
|
||||
|
||||
void UpdateServerBroadcast()
|
||||
{
|
||||
// broadcast to all clients each 'sendInterval'
|
||||
// (client with authority will drop the rpc)
|
||||
@ -118,7 +159,10 @@ void UpdateServer()
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
void UpdateServerInterpolation()
|
||||
{
|
||||
// apply buffered snapshots IF client authority
|
||||
// -> in server authority, server moves the object
|
||||
// so no need to apply any snapshots there.
|
||||
@ -131,115 +175,108 @@ void UpdateServer()
|
||||
connectionToClient != null &&
|
||||
!isOwned)
|
||||
{
|
||||
if (serverSnapshots.Count > 0)
|
||||
{
|
||||
// step the transform interpolation without touching time.
|
||||
// NetworkClient is responsible for time globally.
|
||||
SnapshotInterpolation.StepInterpolation(
|
||||
serverSnapshots,
|
||||
connectionToClient.remoteTimeline,
|
||||
out TransformSnapshot from,
|
||||
out TransformSnapshot to,
|
||||
out double t);
|
||||
if (serverSnapshots.Count == 0) return;
|
||||
|
||||
// interpolate & apply
|
||||
TransformSnapshot computed = TransformSnapshot.Interpolate(from, to, t);
|
||||
Apply(computed, to);
|
||||
}
|
||||
// step the transform interpolation without touching time.
|
||||
// NetworkClient is responsible for time globally.
|
||||
SnapshotInterpolation.StepInterpolation(
|
||||
serverSnapshots,
|
||||
connectionToClient.remoteTimeline,
|
||||
out TransformSnapshot from,
|
||||
out TransformSnapshot to,
|
||||
out double t);
|
||||
|
||||
// interpolate & apply
|
||||
TransformSnapshot computed = TransformSnapshot.Interpolate(from, to, t);
|
||||
Apply(computed, to);
|
||||
}
|
||||
}
|
||||
|
||||
void UpdateClient()
|
||||
void UpdateClientBroadcast()
|
||||
{
|
||||
// client authority, and local player (= allowed to move myself)?
|
||||
if (IsClientWithAuthority)
|
||||
{
|
||||
// https://github.com/vis2k/Mirror/pull/2992/
|
||||
if (!NetworkClient.ready) return;
|
||||
// https://github.com/vis2k/Mirror/pull/2992/
|
||||
if (!NetworkClient.ready) return;
|
||||
|
||||
// send to server each 'sendInterval'
|
||||
// NetworkTime.localTime for double precision until Unity has it too
|
||||
//
|
||||
// IMPORTANT:
|
||||
// snapshot interpolation requires constant sending.
|
||||
// DO NOT only send if position changed. for example:
|
||||
// ---
|
||||
// * client sends first position at t=0
|
||||
// * ... 10s later ...
|
||||
// * client moves again, sends second position at t=10
|
||||
// ---
|
||||
// * server gets first position at t=0
|
||||
// * server gets second position at t=10
|
||||
// * server moves from first to second within a time of 10s
|
||||
// => would be a super slow move, instead of a wait & move.
|
||||
//
|
||||
// IMPORTANT:
|
||||
// DO NOT send nulls if not changed 'since last send' either. we
|
||||
// send unreliable and don't know which 'last send' the other end
|
||||
// received successfully.
|
||||
if (NetworkTime.localTime >= lastClientSendTime + NetworkClient.sendInterval) // same interval as time interpolation!
|
||||
{
|
||||
// send snapshot without timestamp.
|
||||
// receiver gets it from batch timestamp to save bandwidth.
|
||||
TransformSnapshot snapshot = Construct();
|
||||
// send to server each 'sendInterval'
|
||||
// NetworkTime.localTime for double precision until Unity has it too
|
||||
//
|
||||
// IMPORTANT:
|
||||
// snapshot interpolation requires constant sending.
|
||||
// DO NOT only send if position changed. for example:
|
||||
// ---
|
||||
// * client sends first position at t=0
|
||||
// * ... 10s later ...
|
||||
// * client moves again, sends second position at t=10
|
||||
// ---
|
||||
// * server gets first position at t=0
|
||||
// * server gets second position at t=10
|
||||
// * server moves from first to second within a time of 10s
|
||||
// => would be a super slow move, instead of a wait & move.
|
||||
//
|
||||
// IMPORTANT:
|
||||
// DO NOT send nulls if not changed 'since last send' either. we
|
||||
// send unreliable and don't know which 'last send' the other end
|
||||
// received successfully.
|
||||
if (NetworkTime.localTime >= lastClientSendTime + NetworkClient.sendInterval) // same interval as time interpolation!
|
||||
{
|
||||
// send snapshot without timestamp.
|
||||
// receiver gets it from batch timestamp to save bandwidth.
|
||||
TransformSnapshot snapshot = Construct();
|
||||
#if onlySyncOnChange_BANDWIDTH_SAVING
|
||||
cachedSnapshotComparison = CompareSnapshots(snapshot);
|
||||
if (cachedSnapshotComparison && hasSentUnchangedPosition && onlySyncOnChange) { return; }
|
||||
cachedSnapshotComparison = CompareSnapshots(snapshot);
|
||||
if (cachedSnapshotComparison && hasSentUnchangedPosition && onlySyncOnChange) { return; }
|
||||
#endif
|
||||
|
||||
#if onlySyncOnChange_BANDWIDTH_SAVING
|
||||
CmdClientToServerSync(
|
||||
// only sync what the user wants to sync
|
||||
syncPosition && positionChanged ? snapshot.position : default(Vector3?),
|
||||
syncRotation && rotationChanged ? snapshot.rotation : default(Quaternion?),
|
||||
syncScale && scaleChanged ? snapshot.scale : default(Vector3?)
|
||||
);
|
||||
CmdClientToServerSync(
|
||||
// only sync what the user wants to sync
|
||||
syncPosition && positionChanged ? snapshot.position : default(Vector3?),
|
||||
syncRotation && rotationChanged ? snapshot.rotation : default(Quaternion?),
|
||||
syncScale && scaleChanged ? snapshot.scale : default(Vector3?)
|
||||
);
|
||||
#else
|
||||
CmdClientToServerSync(
|
||||
// only sync what the user wants to sync
|
||||
syncPosition ? snapshot.position : default(Vector3?),
|
||||
syncRotation ? snapshot.rotation : default(Quaternion?),
|
||||
syncScale ? snapshot.scale : default(Vector3?)
|
||||
);
|
||||
CmdClientToServerSync(
|
||||
// only sync what the user wants to sync
|
||||
syncPosition ? snapshot.position : default(Vector3?),
|
||||
syncRotation ? snapshot.rotation : default(Quaternion?),
|
||||
syncScale ? snapshot.scale : default(Vector3?)
|
||||
);
|
||||
#endif
|
||||
|
||||
lastClientSendTime = NetworkTime.localTime;
|
||||
lastClientSendTime = NetworkTime.localTime;
|
||||
#if onlySyncOnChange_BANDWIDTH_SAVING
|
||||
if (cachedSnapshotComparison)
|
||||
{
|
||||
hasSentUnchangedPosition = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
hasSentUnchangedPosition = false;
|
||||
lastSnapshot = snapshot;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
// for all other clients (and for local player if !authority),
|
||||
// we need to apply snapshots from the buffer
|
||||
else
|
||||
{
|
||||
// only while we have snapshots
|
||||
if (clientSnapshots.Count > 0)
|
||||
if (cachedSnapshotComparison)
|
||||
{
|
||||
// step the interpolation without touching time.
|
||||
// NetworkClient is responsible for time globally.
|
||||
SnapshotInterpolation.StepInterpolation(
|
||||
clientSnapshots,
|
||||
NetworkTime.time, // == NetworkClient.localTimeline from snapshot interpolation
|
||||
out TransformSnapshot from,
|
||||
out TransformSnapshot to,
|
||||
out double t);
|
||||
|
||||
// interpolate & apply
|
||||
TransformSnapshot computed = TransformSnapshot.Interpolate(from, to, t);
|
||||
Apply(computed, to);
|
||||
hasSentUnchangedPosition = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
hasSentUnchangedPosition = false;
|
||||
lastSnapshot = snapshot;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
void UpdateClientInterpolation()
|
||||
{
|
||||
// only while we have snapshots
|
||||
if (clientSnapshots.Count == 0) return;
|
||||
|
||||
// step the interpolation without touching time.
|
||||
// NetworkClient is responsible for time globally.
|
||||
SnapshotInterpolation.StepInterpolation(
|
||||
clientSnapshots,
|
||||
NetworkTime.time, // == NetworkClient.localTimeline from snapshot interpolation
|
||||
out TransformSnapshot from,
|
||||
out TransformSnapshot to,
|
||||
out double t);
|
||||
|
||||
// interpolate & apply
|
||||
TransformSnapshot computed = TransformSnapshot.Interpolate(from, to, t);
|
||||
Apply(computed, to);
|
||||
}
|
||||
|
||||
public override void OnSerialize(NetworkWriter writer, bool initialState)
|
||||
{
|
||||
// sync target component's position on spawn.
|
||||
@ -249,7 +286,7 @@ public override void OnSerialize(NetworkWriter writer, bool initialState)
|
||||
{
|
||||
if (syncPosition) writer.WriteVector3(target.localPosition);
|
||||
if (syncRotation) writer.WriteQuaternion(target.localRotation);
|
||||
if (syncScale) writer.WriteVector3(target.localScale);
|
||||
if (syncScale) writer.WriteVector3(target.localScale);
|
||||
}
|
||||
}
|
||||
|
||||
@ -262,7 +299,7 @@ public override void OnDeserialize(NetworkReader reader, bool initialState)
|
||||
{
|
||||
if (syncPosition) target.localPosition = reader.ReadVector3();
|
||||
if (syncRotation) target.localRotation = reader.ReadQuaternion();
|
||||
if (syncScale) target.localScale = reader.ReadVector3();
|
||||
if (syncScale) target.localScale = reader.ReadVector3();
|
||||
}
|
||||
}
|
||||
|
||||
@ -314,7 +351,7 @@ protected virtual void OnClientToServerSync(Vector3? position, Quaternion? rotat
|
||||
}
|
||||
}
|
||||
#endif
|
||||
AddSnapshot(serverSnapshots, timestamp, position, rotation, scale);
|
||||
AddSnapshot(serverSnapshots, connectionToClient.remoteTimeStamp + timeStampAdjustment + offset, position, rotation, scale);
|
||||
}
|
||||
|
||||
// rpc /////////////////////////////////////////////////////////////////
|
||||
@ -353,7 +390,7 @@ protected virtual void OnServerToClientSync(Vector3? position, Quaternion? rotat
|
||||
}
|
||||
}
|
||||
#endif
|
||||
AddSnapshot(clientSnapshots, timestamp, position, rotation, scale);
|
||||
AddSnapshot(clientSnapshots, NetworkClient.connection.remoteTimeStamp + timeStampAdjustment + offset, position, rotation, scale);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -209,6 +209,7 @@ void UpdateServer()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
void UpdateClient()
|
||||
{
|
||||
if (Input.GetKeyDown(hotKey))
|
||||
|
@ -38,7 +38,7 @@ public class Batcher
|
||||
// it would allocate too many writers.
|
||||
// https://github.com/vis2k/Mirror/pull/3127
|
||||
// => best to build batches on the fly.
|
||||
Queue<NetworkWriterPooled> batches = new Queue<NetworkWriterPooled>();
|
||||
readonly Queue<NetworkWriterPooled> batches = new Queue<NetworkWriterPooled>();
|
||||
|
||||
// current batch in progress
|
||||
NetworkWriterPooled batch;
|
||||
|
@ -14,7 +14,6 @@ public abstract class InterestManagement : InterestManagementBase
|
||||
readonly HashSet<NetworkConnectionToClient> newObservers =
|
||||
new HashSet<NetworkConnectionToClient>();
|
||||
|
||||
|
||||
// rebuild observers for the given NetworkIdentity.
|
||||
// Server will automatically spawn/despawn added/removed ones.
|
||||
// newObservers: cached hashset to put the result into
|
||||
|
@ -1,7 +1,6 @@
|
||||
// interest management component for custom solutions like
|
||||
// distance based, spatial hashing, raycast based, etc.
|
||||
// low level base class allows for low level spatial hashing etc., which is 3-5x faster.
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
@ -10,11 +9,12 @@ namespace Mirror
|
||||
[HelpURL("https://mirror-networking.gitbook.io/docs/guides/interest-management")]
|
||||
public abstract class InterestManagementBase : MonoBehaviour
|
||||
{
|
||||
// Awake configures InterestManagementBase in NetworkServer/Client
|
||||
// Configures InterestManagementBase in NetworkServer/Client
|
||||
// Do NOT check for active server or client here.
|
||||
// Awake must always set the static aoi references.
|
||||
// make sure to call base.Awake when overwriting!
|
||||
protected virtual void Awake()
|
||||
// OnEnable must always set the static aoi references.
|
||||
// make sure to call base.OnEnable when overwriting!
|
||||
// Previously used Awake()
|
||||
protected virtual void OnEnable()
|
||||
{
|
||||
if (NetworkServer.aoi == null)
|
||||
{
|
||||
|
@ -13,7 +13,9 @@ public class LocalConnectionToServer : NetworkConnectionToServer
|
||||
// packet queue
|
||||
internal readonly Queue<NetworkWriterPooled> queue = new Queue<NetworkWriterPooled>();
|
||||
|
||||
public override string address => "localhost";
|
||||
// Deprecated 2023-02-23
|
||||
[Obsolete("Use LocalConnectionToClient.address instead.")]
|
||||
public string address => "localhost";
|
||||
|
||||
// see caller for comments on why we need this
|
||||
bool connectedEventPending;
|
||||
|
@ -128,6 +128,5 @@ public NetworkPingMessage(double value)
|
||||
public struct NetworkPongMessage : NetworkMessage
|
||||
{
|
||||
public double clientTime;
|
||||
public double serverTime;
|
||||
}
|
||||
}
|
||||
|
@ -36,9 +36,14 @@ public abstract class NetworkBehaviour : MonoBehaviour
|
||||
/// <summary>sync interval for OnSerialize (in seconds)</summary>
|
||||
// hidden because NetworkBehaviourInspector shows it only if has OnSerialize.
|
||||
// [0,2] should be enough. anything >2s is too laggy anyway.
|
||||
//
|
||||
// NetworkServer & NetworkClient broadcast() are behind a sendInterval timer now.
|
||||
// it makes sense to keep every component's syncInterval setting at '0' by default.
|
||||
// otherwise, the overlapping timers could introduce unexpected latency.
|
||||
// careful: default of '0.1' may
|
||||
[Tooltip("Time in seconds until next change is synchronized to the client. '0' means send immediately if changed. '0.5' means only send changes every 500ms.\n(This is for state synchronization like SyncVars, SyncLists, OnSerialize. Not for Cmds, Rpcs, etc.)")]
|
||||
[Range(0, 2)]
|
||||
[HideInInspector] public float syncInterval = 0.1f;
|
||||
[HideInInspector] public float syncInterval = 0;
|
||||
internal double lastSyncTime;
|
||||
|
||||
/// <summary>True if this object is on the server and has been spawned.</summary>
|
||||
@ -384,21 +389,21 @@ protected void SendRPCInternal(string functionFullName, int functionHashCode, Ne
|
||||
// NetworkServer.SendToReadyObservers(netIdentity, message, includeOwner, channelId);
|
||||
|
||||
// safety check used to be in SendToReadyObservers. keep it for now.
|
||||
if (netIdentity.observers != null && netIdentity.observers.Count > 0)
|
||||
{
|
||||
// serialize the message only once
|
||||
using (NetworkWriterPooled serialized = NetworkWriterPool.Get())
|
||||
{
|
||||
serialized.Write(message);
|
||||
if (netIdentity.observers == null || netIdentity.observers.Count == 0)
|
||||
return;
|
||||
|
||||
// add to every observer's connection's rpc buffer
|
||||
foreach (NetworkConnectionToClient conn in netIdentity.observers.Values)
|
||||
// serialize the message only once
|
||||
using (NetworkWriterPooled serialized = NetworkWriterPool.Get())
|
||||
{
|
||||
serialized.Write(message);
|
||||
|
||||
// add to every observer's connection's rpc buffer
|
||||
foreach (NetworkConnectionToClient conn in netIdentity.observers.Values)
|
||||
{
|
||||
bool isOwner = conn == netIdentity.connectionToClient;
|
||||
if ((!isOwner || includeOwner) && conn.isReady)
|
||||
{
|
||||
bool isOwner = conn == netIdentity.connectionToClient;
|
||||
if ((!isOwner || includeOwner) && conn.isReady)
|
||||
{
|
||||
conn.BufferRpc(message, channelId);
|
||||
}
|
||||
conn.BufferRpc(message, channelId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -59,10 +59,6 @@ public static partial class NetworkClient
|
||||
// NetworkClient state
|
||||
internal static ConnectState connectState = ConnectState.None;
|
||||
|
||||
/// <summary>IP address of the connection to server.</summary>
|
||||
// empty if the client has not connected yet.
|
||||
public static string serverIp => connection.address;
|
||||
|
||||
/// <summary>active is true while a client is connecting/connected either as standalone or as host client.</summary>
|
||||
// (= while the network is active)
|
||||
public static bool active => connectState == ConnectState.Connecting ||
|
||||
@ -1706,7 +1702,7 @@ public static void OnGUI()
|
||||
// only if in world
|
||||
if (!ready) return;
|
||||
|
||||
GUILayout.BeginArea(new Rect(10, 5, 500, 50));
|
||||
GUILayout.BeginArea(new Rect(10, 5, 800, 50));
|
||||
|
||||
GUILayout.BeginHorizontal("Box");
|
||||
GUILayout.Label("Snapshot Interp.:");
|
||||
@ -1716,8 +1712,11 @@ public static void OnGUI()
|
||||
else GUI.color = Color.white;
|
||||
GUILayout.Box($"timeline: {localTimeline:F2}");
|
||||
GUILayout.Box($"buffer: {snapshots.Count}");
|
||||
GUILayout.Box($"DriftEMA: {NetworkClient.driftEma.Value:F2}");
|
||||
GUILayout.Box($"DelTimeEMA: {NetworkClient.deliveryTimeEma.Value:F2}");
|
||||
GUILayout.Box($"timescale: {localTimescale:F2}");
|
||||
GUILayout.Box($"BTM: {bufferTimeMultiplier:F2}");
|
||||
GUILayout.Box($"BTM: {snapshotSettings.bufferTimeMultiplier:F2}");
|
||||
GUILayout.Box($"RTT: {NetworkTime.rtt * 1000:000}");
|
||||
GUILayout.EndHorizontal();
|
||||
|
||||
GUILayout.EndArea();
|
||||
|
@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
@ -5,16 +6,28 @@ namespace Mirror
|
||||
{
|
||||
public static partial class NetworkClient
|
||||
{
|
||||
// snapshot interpolation settings /////////////////////////////////////
|
||||
// TODO expose the settings to the user later.
|
||||
// via NetMan or NetworkClientConfig or NetworkClient as component etc.
|
||||
public static SnapshotInterpolationSettings snapshotSettings = new SnapshotInterpolationSettings();
|
||||
|
||||
// decrease bufferTime at runtime to see the catchup effect.
|
||||
// increase to see slowdown.
|
||||
// 'double' so we can have very precise dynamic adjustment without rounding
|
||||
[Header("Snapshot Interpolation: Buffering")]
|
||||
[Tooltip("Local simulation is behind by sendInterval * multiplier seconds.\n\nThis guarantees that we always have enough snapshots in the buffer to mitigate lags & jitter.\n\nIncrease this if the simulation isn't smooth. By default, it should be around 2.")]
|
||||
public static double bufferTimeMultiplier = 2;
|
||||
public static double bufferTime => NetworkServer.sendInterval * bufferTimeMultiplier;
|
||||
// obsolete snapshot settings access
|
||||
// DEPRECATED 2023-03-11
|
||||
[Obsolete("NetworkClient snapshot interpolation settings were moved to NetworkClient.snapshotSettings.*")]
|
||||
public static double bufferTimeMultiplier => snapshotSettings.bufferTimeMultiplier;
|
||||
[Obsolete("NetworkClient snapshot interpolation settings were moved to NetworkClient.snapshotSettings.*")]
|
||||
public static float catchupNegativeThreshold => snapshotSettings.catchupNegativeThreshold;
|
||||
[Obsolete("NetworkClient snapshot interpolation settings were moved to NetworkClient.snapshotSettings.*")]
|
||||
public static float catchupPositiveThreshold => snapshotSettings.catchupPositiveThreshold;
|
||||
[Obsolete("NetworkClient snapshot interpolation settings were moved to NetworkClient.snapshotSettings.*")]
|
||||
public static double catchupSpeed => snapshotSettings.catchupSpeed;
|
||||
[Obsolete("NetworkClient snapshot interpolation settings were moved to NetworkClient.snapshotSettings.*")]
|
||||
public static double slowdownSpeed => snapshotSettings.slowdownSpeed;
|
||||
[Obsolete("NetworkClient snapshot interpolation settings were moved to NetworkClient.snapshotSettings.*")]
|
||||
public static int driftEmaDuration => snapshotSettings.driftEmaDuration;
|
||||
|
||||
// snapshot interpolation runtime data /////////////////////////////////
|
||||
public static double bufferTime => NetworkServer.sendInterval * snapshotSettings.bufferTimeMultiplier;
|
||||
|
||||
// <servertime, snaps>
|
||||
public static SortedList<double, TimeSnapshot> snapshots = new SortedList<double, TimeSnapshot>();
|
||||
@ -33,25 +46,7 @@ public static partial class NetworkClient
|
||||
internal static double localTimescale = 1;
|
||||
|
||||
// catchup /////////////////////////////////////////////////////////////
|
||||
// catchup thresholds in 'frames'.
|
||||
// half a frame might be too aggressive.
|
||||
[Header("Snapshot Interpolation: Catchup / Slowdown")]
|
||||
[Tooltip("Slowdown begins when the local timeline is moving too fast towards remote time. Threshold is in frames worth of snapshots.\n\nThis needs to be negative.\n\nDon't modify unless you know what you are doing.")]
|
||||
public static float catchupNegativeThreshold = -1; // careful, don't want to run out of snapshots
|
||||
|
||||
[Tooltip("Catchup begins when the local timeline is moving too slow and getting too far away from remote time. Threshold is in frames worth of snapshots.\n\nThis needs to be positive.\n\nDon't modify unless you know what you are doing.")]
|
||||
public static float catchupPositiveThreshold = 1;
|
||||
|
||||
[Tooltip("Local timeline acceleration in % while catching up.")]
|
||||
[Range(0, 1)]
|
||||
public static double catchupSpeed = 0.01f; // 1%
|
||||
|
||||
[Tooltip("Local timeline slowdown in % while slowing down.")]
|
||||
[Range(0, 1)]
|
||||
public static double slowdownSpeed = 0.01f; // 1%
|
||||
|
||||
[Tooltip("Catchup/Slowdown is adjusted over n-second exponential moving average.")]
|
||||
public static int driftEmaDuration = 1; // shouldn't need to modify this, but expose it anyway
|
||||
|
||||
// we use EMA to average the last second worth of snapshot time diffs.
|
||||
// manually averaging the last second worth of values with a for loop
|
||||
@ -103,8 +98,8 @@ static void InitTimeInterpolation()
|
||||
// initialize EMA with 'emaDuration' seconds worth of history.
|
||||
// 1 second holds 'sendRate' worth of values.
|
||||
// multiplied by emaDuration gives n-seconds.
|
||||
driftEma = new ExponentialMovingAverage(NetworkServer.sendRate * driftEmaDuration);
|
||||
deliveryTimeEma = new ExponentialMovingAverage(NetworkServer.sendRate * deliveryTimeEmaDuration);
|
||||
driftEma = new ExponentialMovingAverage(NetworkServer.sendRate * snapshotSettings.driftEmaDuration);
|
||||
deliveryTimeEma = new ExponentialMovingAverage(NetworkServer.sendRate * snapshotSettings.deliveryTimeEmaDuration);
|
||||
}
|
||||
|
||||
// server sends TimeSnapshotMessage every sendInterval.
|
||||
@ -117,12 +112,8 @@ static void OnTimeSnapshotMessage(TimeSnapshotMessage _)
|
||||
// before calling OnDeserialize so components can use
|
||||
// NetworkTime.time and NetworkTime.timeStamp.
|
||||
|
||||
#if !UNITY_2020_3_OR_NEWER
|
||||
// Unity 2019 doesn't have Time.timeAsDouble yet
|
||||
OnTimeSnapshot(new TimeSnapshot(connection.remoteTimeStamp, NetworkTime.localTime));
|
||||
#else
|
||||
OnTimeSnapshot(new TimeSnapshot(connection.remoteTimeStamp, Time.timeAsDouble));
|
||||
#endif
|
||||
}
|
||||
|
||||
// see comments at the top of this file
|
||||
@ -131,14 +122,14 @@ public static void OnTimeSnapshot(TimeSnapshot snap)
|
||||
// Debug.Log($"NetworkClient: OnTimeSnapshot @ {snap.remoteTime:F3}");
|
||||
|
||||
// (optional) dynamic adjustment
|
||||
if (dynamicAdjustment)
|
||||
if (snapshotSettings.dynamicAdjustment)
|
||||
{
|
||||
// set bufferTime on the fly.
|
||||
// shows in inspector for easier debugging :)
|
||||
bufferTimeMultiplier = SnapshotInterpolation.DynamicAdjustment(
|
||||
snapshotSettings.bufferTimeMultiplier = SnapshotInterpolation.DynamicAdjustment(
|
||||
NetworkServer.sendInterval,
|
||||
deliveryTimeEma.StandardDeviation,
|
||||
dynamicAdjustmentTolerance
|
||||
snapshotSettings.dynamicAdjustmentTolerance
|
||||
);
|
||||
}
|
||||
|
||||
@ -150,11 +141,11 @@ public static void OnTimeSnapshot(TimeSnapshot snap)
|
||||
ref localTimescale,
|
||||
NetworkServer.sendInterval,
|
||||
bufferTime,
|
||||
catchupSpeed,
|
||||
slowdownSpeed,
|
||||
snapshotSettings.catchupSpeed,
|
||||
snapshotSettings.slowdownSpeed,
|
||||
ref driftEma,
|
||||
catchupNegativeThreshold,
|
||||
catchupPositiveThreshold,
|
||||
snapshotSettings.catchupNegativeThreshold,
|
||||
snapshotSettings.catchupPositiveThreshold,
|
||||
ref deliveryTimeEma);
|
||||
|
||||
// Debug.Log($"inserted TimeSnapshot remote={snap.remoteTime:F2} local={snap.localTime:F2} total={snapshots.Count}");
|
||||
|
@ -27,9 +27,6 @@ public abstract class NetworkConnection
|
||||
// state.
|
||||
public bool isReady;
|
||||
|
||||
/// <summary>IP address of the connection. Can be useful for game master IP bans etc.</summary>
|
||||
public abstract string address { get; }
|
||||
|
||||
/// <summary>Last time a message was received for this connection. Includes system and user messages.</summary>
|
||||
public float lastMessageTime;
|
||||
|
||||
|
@ -14,8 +14,7 @@ public class NetworkConnectionToClient : NetworkConnection
|
||||
readonly NetworkWriter reliableRpcs = new NetworkWriter();
|
||||
readonly NetworkWriter unreliableRpcs = new NetworkWriter();
|
||||
|
||||
public override string address =>
|
||||
Transport.active.ServerGetClientAddress(connectionId);
|
||||
public virtual string address => Transport.active.ServerGetClientAddress(connectionId);
|
||||
|
||||
/// <summary>NetworkIdentities that this connection can see</summary>
|
||||
// TODO move to server's NetworkConnectionToClient?
|
||||
@ -53,11 +52,11 @@ public NetworkConnectionToClient(int networkConnectionId)
|
||||
// initialize EMA with 'emaDuration' seconds worth of history.
|
||||
// 1 second holds 'sendRate' worth of values.
|
||||
// multiplied by emaDuration gives n-seconds.
|
||||
driftEma = new ExponentialMovingAverage(NetworkServer.sendRate * NetworkClient.driftEmaDuration);
|
||||
deliveryTimeEma = new ExponentialMovingAverage(NetworkServer.sendRate * NetworkClient.deliveryTimeEmaDuration);
|
||||
driftEma = new ExponentialMovingAverage(NetworkServer.sendRate * NetworkClient.snapshotSettings.driftEmaDuration);
|
||||
deliveryTimeEma = new ExponentialMovingAverage(NetworkServer.sendRate * NetworkClient.snapshotSettings.deliveryTimeEmaDuration);
|
||||
|
||||
// buffer limit should be at least multiplier to have enough in there
|
||||
snapshotBufferSizeLimit = Mathf.Max((int)NetworkClient.bufferTimeMultiplier, snapshotBufferSizeLimit);
|
||||
snapshotBufferSizeLimit = Mathf.Max((int)NetworkClient.snapshotSettings.bufferTimeMultiplier, snapshotBufferSizeLimit);
|
||||
}
|
||||
|
||||
public void OnTimeSnapshot(TimeSnapshot snapshot)
|
||||
@ -66,14 +65,14 @@ public void OnTimeSnapshot(TimeSnapshot snapshot)
|
||||
if (snapshots.Count >= snapshotBufferSizeLimit) return;
|
||||
|
||||
// (optional) dynamic adjustment
|
||||
if (NetworkClient.dynamicAdjustment)
|
||||
if (NetworkClient.snapshotSettings.dynamicAdjustment)
|
||||
{
|
||||
// set bufferTime on the fly.
|
||||
// shows in inspector for easier debugging :)
|
||||
bufferTimeMultiplier = SnapshotInterpolation.DynamicAdjustment(
|
||||
NetworkServer.sendInterval,
|
||||
deliveryTimeEma.StandardDeviation,
|
||||
NetworkClient.dynamicAdjustmentTolerance
|
||||
NetworkClient.snapshotSettings.dynamicAdjustmentTolerance
|
||||
);
|
||||
// Debug.Log($"[Server]: {name} delivery std={serverDeliveryTimeEma.StandardDeviation} bufferTimeMult := {bufferTimeMultiplier} ");
|
||||
}
|
||||
@ -86,11 +85,11 @@ public void OnTimeSnapshot(TimeSnapshot snapshot)
|
||||
ref remoteTimescale,
|
||||
NetworkServer.sendInterval,
|
||||
bufferTime,
|
||||
NetworkClient.catchupSpeed,
|
||||
NetworkClient.slowdownSpeed,
|
||||
NetworkClient.snapshotSettings.catchupSpeed,
|
||||
NetworkClient.snapshotSettings.slowdownSpeed,
|
||||
ref driftEma,
|
||||
NetworkClient.catchupNegativeThreshold,
|
||||
NetworkClient.catchupPositiveThreshold,
|
||||
NetworkClient.snapshotSettings.catchupNegativeThreshold,
|
||||
NetworkClient.snapshotSettings.catchupPositiveThreshold,
|
||||
ref deliveryTimeEma
|
||||
);
|
||||
}
|
||||
@ -120,7 +119,7 @@ void FlushRpcs(NetworkWriter buffer, int channelId)
|
||||
{
|
||||
if (buffer.Position > 0)
|
||||
{
|
||||
Send(new RpcBufferMessage{ payload = buffer }, channelId);
|
||||
Send(new RpcBufferMessage { payload = buffer }, channelId);
|
||||
buffer.Position = 0;
|
||||
}
|
||||
}
|
||||
|
@ -5,8 +5,6 @@ namespace Mirror
|
||||
{
|
||||
public class NetworkConnectionToServer : NetworkConnection
|
||||
{
|
||||
public override string address => "";
|
||||
|
||||
// Send stage three: hand off to transport
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void SendToTransport(ArraySegment<byte> segment, int channelId = Channels.Reliable) =>
|
||||
|
@ -10,8 +10,8 @@
|
||||
|
||||
#if UNITY_2021_2_OR_NEWER
|
||||
using UnityEditor.SceneManagement;
|
||||
#elif UNITY_2018_3_OR_NEWER
|
||||
using UnityEditor.Experimental.SceneManagement;
|
||||
#else
|
||||
using UnityEditor.Experimental.SceneManagement;
|
||||
#endif
|
||||
#endif
|
||||
|
||||
|
@ -99,6 +99,15 @@ internal static bool AddToPlayerLoop(PlayerLoopSystem.UpdateFunction function, T
|
||||
//foreach (PlayerLoopSystem sys in playerLoop.subSystemList)
|
||||
// Debug.Log($" ->{sys.type}");
|
||||
|
||||
// make sure the function wasn't added yet.
|
||||
// with domain reload disabled, it would otherwise be added twice:
|
||||
// fixes: https://github.com/MirrorNetworking/Mirror/issues/3392
|
||||
if (Array.FindIndex(playerLoop.subSystemList, (s => s.updateDelegate == function)) != -1)
|
||||
{
|
||||
// loop contains the function, so return true.
|
||||
return true;
|
||||
}
|
||||
|
||||
// resize & expand subSystemList to fit one more entry
|
||||
int oldListLength = (playerLoop.subSystemList != null) ? playerLoop.subSystemList.Length : 0;
|
||||
Array.Resize(ref playerLoop.subSystemList, oldListLength + 1);
|
||||
@ -174,6 +183,10 @@ static void RuntimeInitializeOnLoad()
|
||||
|
||||
static void NetworkEarlyUpdate()
|
||||
{
|
||||
// loop functions run in edit mode and in play mode.
|
||||
// however, we only want to call NetworkServer/Client in play mode.
|
||||
if (!Application.isPlaying) return;
|
||||
|
||||
//Debug.Log($"NetworkEarlyUpdate {Time.time}");
|
||||
NetworkServer.NetworkEarlyUpdate();
|
||||
NetworkClient.NetworkEarlyUpdate();
|
||||
@ -183,6 +196,10 @@ static void NetworkEarlyUpdate()
|
||||
|
||||
static void NetworkLateUpdate()
|
||||
{
|
||||
// loop functions run in edit mode and in play mode.
|
||||
// however, we only want to call NetworkServer/Client in play mode.
|
||||
if (!Application.isPlaying) return;
|
||||
|
||||
//Debug.Log($"NetworkLateUpdate {Time.time}");
|
||||
// invoke event before mirror does its final late updating.
|
||||
OnLateUpdate?.Invoke();
|
||||
|
@ -118,6 +118,9 @@ public class NetworkManager : MonoBehaviour
|
||||
public static List<Transform> startPositions = new List<Transform>();
|
||||
public static int startPositionIndex;
|
||||
|
||||
[Header("Snapshot Interpolation")]
|
||||
public SnapshotInterpolationSettings snapshotSettings = new SnapshotInterpolationSettings();
|
||||
|
||||
[Header("Debug")]
|
||||
public bool timeInterpolationGui = false;
|
||||
|
||||
@ -219,7 +222,7 @@ public virtual void Start()
|
||||
StartServer();
|
||||
}
|
||||
// only start server or client, never both
|
||||
else if(autoConnectClientBuild)
|
||||
else if (autoConnectClientBuild)
|
||||
{
|
||||
StartClient();
|
||||
}
|
||||
@ -257,6 +260,7 @@ bool IsServerOnlineSceneChangeNeeded() =>
|
||||
void ApplyConfiguration()
|
||||
{
|
||||
NetworkServer.tickRate = sendRate;
|
||||
NetworkClient.snapshotSettings = snapshotSettings;
|
||||
}
|
||||
|
||||
// full server setup code, without spawning objects yet
|
||||
@ -536,14 +540,6 @@ void FinishStartHost()
|
||||
public void StopHost()
|
||||
{
|
||||
OnStopHost();
|
||||
|
||||
// calling OnTransportDisconnected was needed to fix
|
||||
// https://github.com/vis2k/Mirror/issues/1515
|
||||
// so that the host client receives a DisconnectMessage
|
||||
// TODO reevaluate if this is still needed after all the disconnect
|
||||
// fixes, and try to put this into LocalConnection.Disconnect!
|
||||
NetworkServer.OnTransportDisconnected(NetworkConnection.LocalConnectionId);
|
||||
|
||||
StopClient();
|
||||
StopServer();
|
||||
}
|
||||
@ -596,6 +592,12 @@ public void StopClient()
|
||||
if (mode == NetworkManagerMode.Offline)
|
||||
return;
|
||||
|
||||
// For Host client, call OnServerDisconnect before NetworkClient.Disconnect
|
||||
// because we need NetworkServer.localConnection to not be null
|
||||
// NetworkClient.Disconnect will set it null.
|
||||
if (mode == NetworkManagerMode.Host)
|
||||
OnServerDisconnect(NetworkServer.localConnection);
|
||||
|
||||
// ask client -> transport to disconnect.
|
||||
// handle voluntary and involuntary disconnects in OnClientDisconnect.
|
||||
//
|
||||
@ -1267,7 +1269,7 @@ void OnClientSceneInternal(SceneMessage msg)
|
||||
}
|
||||
|
||||
/// <summary>Called on the server when a new client connects.</summary>
|
||||
public virtual void OnServerConnect(NetworkConnectionToClient conn) {}
|
||||
public virtual void OnServerConnect(NetworkConnectionToClient conn) { }
|
||||
|
||||
/// <summary>Called on the server when a client disconnects.</summary>
|
||||
// Called by NetworkServer.OnTransportDisconnect!
|
||||
@ -1306,22 +1308,14 @@ public virtual void OnServerAddPlayer(NetworkConnectionToClient conn)
|
||||
NetworkServer.AddPlayerForConnection(conn, player);
|
||||
}
|
||||
|
||||
// Deprecated 2022-05-12
|
||||
[Obsolete("OnServerError(conn, Exception) was changed to OnServerError(conn, TransportError, string)")]
|
||||
public virtual void OnServerError(NetworkConnectionToClient conn, Exception exception) {}
|
||||
/// <summary>Called on server when transport raises an exception. NetworkConnection may be null.</summary>
|
||||
public virtual void OnServerError(NetworkConnectionToClient conn, TransportError error, string reason)
|
||||
{
|
||||
#pragma warning disable CS0618
|
||||
OnServerError(conn, new Exception(reason));
|
||||
#pragma warning restore CS0618
|
||||
}
|
||||
public virtual void OnServerError(NetworkConnectionToClient conn, TransportError error, string reason) { }
|
||||
|
||||
/// <summary>Called from ServerChangeScene immediately before SceneManager.LoadSceneAsync is executed</summary>
|
||||
public virtual void OnServerChangeScene(string newSceneName) {}
|
||||
public virtual void OnServerChangeScene(string newSceneName) { }
|
||||
|
||||
/// <summary>Called on server after a scene load with ServerChangeScene() is completed.</summary>
|
||||
public virtual void OnServerSceneChanged(string sceneName) {}
|
||||
public virtual void OnServerSceneChanged(string sceneName) { }
|
||||
|
||||
/// <summary>Called on the client when connected to a server. By default it sets client as ready and adds a player.</summary>
|
||||
public virtual void OnClientConnect()
|
||||
@ -1342,25 +1336,17 @@ public virtual void OnClientConnect()
|
||||
}
|
||||
|
||||
/// <summary>Called on clients when disconnected from a server.</summary>
|
||||
public virtual void OnClientDisconnect() {}
|
||||
public virtual void OnClientDisconnect() { }
|
||||
|
||||
// Deprecated 2022-05-12
|
||||
[Obsolete("OnClientError(Exception) was changed to OnClientError(TransportError, string)")]
|
||||
public virtual void OnClientError(Exception exception) {}
|
||||
/// <summary>Called on client when transport raises an exception.</summary>
|
||||
public virtual void OnClientError(TransportError error, string reason)
|
||||
{
|
||||
#pragma warning disable CS0618
|
||||
OnClientError(new Exception(reason));
|
||||
#pragma warning restore CS0618
|
||||
}
|
||||
public virtual void OnClientError(TransportError error, string reason) { }
|
||||
|
||||
/// <summary>Called on clients when a servers tells the client it is no longer ready, e.g. when switching scenes.</summary>
|
||||
public virtual void OnClientNotReady() {}
|
||||
public virtual void OnClientNotReady() { }
|
||||
|
||||
/// <summary>Called from ClientChangeScene immediately before SceneManager.LoadSceneAsync is executed</summary>
|
||||
// customHandling: indicates if scene loading will be handled through overrides
|
||||
public virtual void OnClientChangeScene(string newSceneName, SceneOperation sceneOperation, bool customHandling) {}
|
||||
public virtual void OnClientChangeScene(string newSceneName, SceneOperation sceneOperation, bool customHandling) { }
|
||||
|
||||
/// <summary>Called on clients when a scene has completed loaded, when the scene load was initiated by the server.</summary>
|
||||
// Scene changes can cause player objects to be destroyed. The default
|
||||
@ -1385,22 +1371,22 @@ public virtual void OnClientSceneChanged()
|
||||
// from all versions, so users only need to implement this one case.
|
||||
|
||||
/// <summary>This is invoked when a host is started.</summary>
|
||||
public virtual void OnStartHost() {}
|
||||
public virtual void OnStartHost() { }
|
||||
|
||||
/// <summary>This is invoked when a server is started - including when a host is started.</summary>
|
||||
public virtual void OnStartServer() {}
|
||||
public virtual void OnStartServer() { }
|
||||
|
||||
/// <summary>This is invoked when the client is started.</summary>
|
||||
public virtual void OnStartClient() {}
|
||||
public virtual void OnStartClient() { }
|
||||
|
||||
/// <summary>This is called when a server is stopped - including when a host is stopped.</summary>
|
||||
public virtual void OnStopServer() {}
|
||||
public virtual void OnStopServer() { }
|
||||
|
||||
/// <summary>This is called when a client is stopped.</summary>
|
||||
public virtual void OnStopClient() {}
|
||||
public virtual void OnStopClient() { }
|
||||
|
||||
/// <summary>This is called when a host is stopped.</summary>
|
||||
public virtual void OnStopHost() {}
|
||||
public virtual void OnStopHost() { }
|
||||
|
||||
// keep OnGUI even in builds. useful to debug snap interp.
|
||||
void OnGUI()
|
||||
|
@ -129,14 +129,17 @@ public static ArraySegment<byte> ReadBytesAndSizeSegment(this NetworkReader read
|
||||
public static Quaternion ReadQuaternion(this NetworkReader reader) => reader.ReadBlittable<Quaternion>();
|
||||
public static Quaternion? ReadQuaternionNullable(this NetworkReader reader) => reader.ReadBlittableNullable<Quaternion>();
|
||||
|
||||
public static Rect ReadRect(this NetworkReader reader) => reader.ReadBlittable<Rect>();
|
||||
public static Rect? ReadRectNullable(this NetworkReader reader) => reader.ReadBlittableNullable<Rect>();
|
||||
// Rect is a struct with properties instead of fields
|
||||
public static Rect ReadRect(this NetworkReader reader) => new Rect(reader.ReadVector2(), reader.ReadVector2());
|
||||
public static Rect? ReadRectNullable(this NetworkReader reader) => reader.ReadBool() ? ReadRect(reader) : default(Rect?);
|
||||
|
||||
public static Plane ReadPlane(this NetworkReader reader) => reader.ReadBlittable<Plane>();
|
||||
public static Plane? ReadPlaneNullable(this NetworkReader reader) => reader.ReadBlittableNullable<Plane>();
|
||||
// Plane is a struct with properties instead of fields
|
||||
public static Plane ReadPlane(this NetworkReader reader) => new Plane(reader.ReadVector3(), reader.ReadFloat());
|
||||
public static Plane? ReadPlaneNullable(this NetworkReader reader) => reader.ReadBool() ? ReadPlane(reader) : default(Plane?);
|
||||
|
||||
public static Ray ReadRay(this NetworkReader reader) => reader.ReadBlittable<Ray>();
|
||||
public static Ray? ReadRayNullable(this NetworkReader reader) => reader.ReadBlittableNullable<Ray>();
|
||||
// Ray is a struct with properties instead of fields
|
||||
public static Ray ReadRay(this NetworkReader reader) => new Ray(reader.ReadVector3(), reader.ReadVector3());
|
||||
public static Ray? ReadRayNullable(this NetworkReader reader) => reader.ReadBool() ? ReadRay(reader) : default(Ray?);
|
||||
|
||||
public static Matrix4x4 ReadMatrix4x4(this NetworkReader reader) => reader.ReadBlittable<Matrix4x4>();
|
||||
public static Matrix4x4? ReadMatrix4x4Nullable(this NetworkReader reader) => reader.ReadBlittableNullable<Matrix4x4>();
|
||||
@ -335,11 +338,7 @@ public static Sprite ReadSprite(this NetworkReader reader)
|
||||
return Sprite.Create(texture, reader.ReadRect(), reader.ReadVector2());
|
||||
}
|
||||
|
||||
public static DateTime ReadDateTime(this NetworkReader reader)
|
||||
{
|
||||
return DateTime.FromOADate(reader.ReadDouble());
|
||||
}
|
||||
|
||||
public static DateTime ReadDateTime(this NetworkReader reader) => DateTime.FromOADate(reader.ReadDouble());
|
||||
public static DateTime? ReadDateTimeNullable(this NetworkReader reader) => reader.ReadBool() ? ReadDateTime(reader) : default(DateTime?);
|
||||
}
|
||||
}
|
||||
|
@ -334,12 +334,12 @@ static void OnEntityStateMessage(NetworkConnectionToClient connection, EntitySta
|
||||
}
|
||||
}
|
||||
}
|
||||
// an attacker may attempt to modify another connection's entity
|
||||
// An attacker may attempt to modify another connection's entity
|
||||
// This could also be a race condition of message in flight when
|
||||
// RemoveClientAuthority is called, so not malicious.
|
||||
// Don't disconnect, just log the warning.
|
||||
else
|
||||
{
|
||||
Debug.LogWarning($"Connection {connection.connectionId} attempted to modify {identity} which is not owned by the connection. Disconnecting the connection.");
|
||||
connection.Disconnect();
|
||||
}
|
||||
Debug.LogWarning($"EntityStateMessage from {connection} for {identity} without authority.");
|
||||
}
|
||||
// no warning. don't spam server logs.
|
||||
// else Debug.LogWarning($"Did not find target for sync message for {message.netId} . Note: this can be completely normal because UDP messages may arrive out of order, so this message might have arrived after a Destroy message.");
|
||||
@ -359,12 +359,8 @@ static void OnTimeSnapshotMessage(NetworkConnectionToClient connection, TimeSnap
|
||||
// maybe we shouldn't allow timeline to deviate more than a certain %.
|
||||
// for now, this is only used for client authority movement.
|
||||
|
||||
#if !UNITY_2020_3_OR_NEWER
|
||||
// Unity 2019 doesn't have Time.timeAsDouble yet
|
||||
connection.OnTimeSnapshot(new TimeSnapshot(connection.remoteTimeStamp, NetworkTime.localTime));
|
||||
#else
|
||||
connection.OnTimeSnapshot(new TimeSnapshot(connection.remoteTimeStamp, Time.timeAsDouble));
|
||||
#endif
|
||||
}
|
||||
|
||||
// connections /////////////////////////////////////////////////////////
|
||||
@ -1810,16 +1806,9 @@ internal static void NetworkLateUpdate()
|
||||
// also important for syncInterval=0 components like
|
||||
// NetworkTransform, so they can sync on same interval as time
|
||||
// snapshots _but_ not every single tick.
|
||||
if (!Application.isPlaying ||
|
||||
#if !UNITY_2020_3_OR_NEWER
|
||||
// Unity 2019 doesn't have Time.timeAsDouble yet
|
||||
AccurateInterval.Elapsed(NetworkTime.localTime, sendInterval, ref lastSendTime))
|
||||
#else
|
||||
AccurateInterval.Elapsed(Time.timeAsDouble, sendInterval, ref lastSendTime))
|
||||
#endif
|
||||
{
|
||||
// Unity 2019 doesn't have Time.timeAsDouble yet
|
||||
if (!Application.isPlaying || AccurateInterval.Elapsed(NetworkTime.localTime, sendInterval, ref lastSendTime))
|
||||
Broadcast();
|
||||
}
|
||||
}
|
||||
|
||||
// process all outgoing messages after updating the world
|
||||
|
@ -19,11 +19,11 @@ public static class NetworkTime
|
||||
public static float PingFrequency = 2;
|
||||
|
||||
/// <summary>Average out the last few results from Ping</summary>
|
||||
public static int PingWindowSize = 10;
|
||||
public static int PingWindowSize = 6;
|
||||
|
||||
static double lastPingTime;
|
||||
|
||||
static ExponentialMovingAverage _rtt = new ExponentialMovingAverage(10);
|
||||
static ExponentialMovingAverage _rtt = new ExponentialMovingAverage(PingWindowSize);
|
||||
|
||||
/// <summary>Returns double precision clock time _in this system_, unaffected by the network.</summary>
|
||||
#if UNITY_2020_3_OR_NEWER
|
||||
@ -74,7 +74,7 @@ public static double time
|
||||
public static void ResetStatics()
|
||||
{
|
||||
PingFrequency = 2;
|
||||
PingWindowSize = 10;
|
||||
PingWindowSize = 6;
|
||||
lastPingTime = 0;
|
||||
_rtt = new ExponentialMovingAverage(PingWindowSize);
|
||||
#if !UNITY_2020_3_OR_NEWER
|
||||
@ -102,7 +102,6 @@ internal static void OnServerPing(NetworkConnectionToClient conn, NetworkPingMes
|
||||
NetworkPongMessage pongMessage = new NetworkPongMessage
|
||||
{
|
||||
clientTime = message.clientTime,
|
||||
serverTime = localTime
|
||||
};
|
||||
conn.Send(pongMessage, Channels.Unreliable);
|
||||
}
|
||||
|
@ -166,14 +166,44 @@ public static void WriteArraySegment<T>(this NetworkWriter writer, ArraySegment<
|
||||
public static void WriteQuaternion(this NetworkWriter writer, Quaternion value) => writer.WriteBlittable(value);
|
||||
public static void WriteQuaternionNullable(this NetworkWriter writer, Quaternion? value) => writer.WriteBlittableNullable(value);
|
||||
|
||||
public static void WriteRect(this NetworkWriter writer, Rect value) => writer.WriteBlittable(value);
|
||||
public static void WriteRectNullable(this NetworkWriter writer, Rect? value) => writer.WriteBlittableNullable(value);
|
||||
// Rect is a struct with properties instead of fields
|
||||
public static void WriteRect(this NetworkWriter writer, Rect value)
|
||||
{
|
||||
writer.WriteVector2(value.position);
|
||||
writer.WriteVector2(value.size);
|
||||
}
|
||||
public static void WriteRectNullable(this NetworkWriter writer, Rect? value)
|
||||
{
|
||||
writer.WriteBool(value.HasValue);
|
||||
if (value.HasValue)
|
||||
writer.WriteRect(value.Value);
|
||||
}
|
||||
|
||||
public static void WritePlane(this NetworkWriter writer, Plane value) => writer.WriteBlittable(value);
|
||||
public static void WritePlaneNullable(this NetworkWriter writer, Plane? value) => writer.WriteBlittableNullable(value);
|
||||
// Plane is a struct with properties instead of fields
|
||||
public static void WritePlane(this NetworkWriter writer, Plane value)
|
||||
{
|
||||
writer.WriteVector3(value.normal);
|
||||
writer.WriteFloat(value.distance);
|
||||
}
|
||||
public static void WritePlaneNullable(this NetworkWriter writer, Plane? value)
|
||||
{
|
||||
writer.WriteBool(value.HasValue);
|
||||
if (value.HasValue)
|
||||
writer.WritePlane(value.Value);
|
||||
}
|
||||
|
||||
public static void WriteRay(this NetworkWriter writer, Ray value) => writer.WriteBlittable(value);
|
||||
public static void WriteRayNullable(this NetworkWriter writer, Ray? value) => writer.WriteBlittableNullable(value);
|
||||
// Ray is a struct with properties instead of fields
|
||||
public static void WriteRay(this NetworkWriter writer, Ray value)
|
||||
{
|
||||
writer.WriteVector3(value.origin);
|
||||
writer.WriteVector3(value.direction);
|
||||
}
|
||||
public static void WriteRayNullable(this NetworkWriter writer, Ray? value)
|
||||
{
|
||||
writer.WriteBool(value.HasValue);
|
||||
if (value.HasValue)
|
||||
writer.WriteRay(value.Value);
|
||||
}
|
||||
|
||||
public static void WriteMatrix4x4(this NetworkWriter writer, Matrix4x4 value) => writer.WriteBlittable(value);
|
||||
public static void WriteMatrix4x4Nullable(this NetworkWriter writer, Matrix4x4? value) => writer.WriteBlittableNullable(value);
|
||||
@ -228,6 +258,22 @@ public static void WriteNetworkBehaviour(this NetworkWriter writer, NetworkBehav
|
||||
writer.WriteUInt(0);
|
||||
return;
|
||||
}
|
||||
|
||||
// users might try to use unspawned / prefab NetworkBehaviours in
|
||||
// rpcs/cmds/syncvars/messages. they would be null on the other
|
||||
// end, and it might not be obvious why. let's make it obvious.
|
||||
// https://github.com/vis2k/Mirror/issues/2060
|
||||
// and more recently https://github.com/MirrorNetworking/Mirror/issues/3399
|
||||
//
|
||||
// => warning (instead of exception) because we also use a warning
|
||||
// when writing an unspawned NetworkIdentity
|
||||
if (value.netId == 0)
|
||||
{
|
||||
Debug.LogWarning($"Attempted to serialize unspawned NetworkBehaviour: of type {value.GetType()} on GameObject {value.name}. Prefabs and unspawned GameObjects would always be null on the other side. Please spawn it before using it in [SyncVar]s/Rpcs/Cmds/NetworkMessages etc.");
|
||||
writer.WriteUInt(0);
|
||||
return;
|
||||
}
|
||||
|
||||
writer.WriteUInt(value.netId);
|
||||
writer.WriteByte(value.ComponentIndex);
|
||||
}
|
||||
|
@ -35,13 +35,13 @@ public static double Timescale(
|
||||
double drift, // how far we are off from bufferTime
|
||||
double catchupSpeed, // in % [0,1]
|
||||
double slowdownSpeed, // in % [0,1]
|
||||
double catchupNegativeThreshold, // in % of sendInteral (careful, we may run out of snapshots)
|
||||
double catchupPositiveThreshold) // in % of sendInterval)
|
||||
double absoluteCatchupNegativeThreshold, // in seconds (careful, we may run out of snapshots)
|
||||
double absoluteCatchupPositiveThreshold) // in seconds
|
||||
{
|
||||
// if the drift time is too large, it means we are behind more time.
|
||||
// so we need to speed up the timescale.
|
||||
// note the threshold should be sendInterval * catchupThreshold.
|
||||
if (drift > catchupPositiveThreshold)
|
||||
if (drift > absoluteCatchupPositiveThreshold)
|
||||
{
|
||||
// localTimeline += 0.001; // too simple, this would ping pong
|
||||
return 1 + catchupSpeed; // n% faster
|
||||
@ -50,7 +50,7 @@ public static double Timescale(
|
||||
// if the drift time is too small, it means we are ahead of time.
|
||||
// so we need to slow down the timescale.
|
||||
// note the threshold should be sendInterval * catchupThreshold.
|
||||
if (drift < catchupNegativeThreshold)
|
||||
if (drift < absoluteCatchupNegativeThreshold)
|
||||
{
|
||||
// localTimeline -= 0.001; // too simple, this would ping pong
|
||||
return 1 - slowdownSpeed; // n% slower
|
||||
@ -106,6 +106,32 @@ public static bool InsertIfNotExists<T>(
|
||||
return buffer.Count > before;
|
||||
}
|
||||
|
||||
// clamp timeline for cases where it gets too far behind.
|
||||
// for example, a client app may go into the background and get updated
|
||||
// with 1hz for a while. by the time it's back it's at least 30 frames
|
||||
// behind, possibly more if the transport also queues up. In this
|
||||
// scenario, at 1% catch up it took around 20+ seconds to finally catch
|
||||
// up. For these kinds of scenarios it will be better to snap / clamp.
|
||||
//
|
||||
// to reproduce, try snapshot interpolation demo and press the button to
|
||||
// simulate the client timeline at multiple seconds behind. it'll take
|
||||
// a long time to catch up if the timeline is a long time behind.
|
||||
public static double TimelineClamp(
|
||||
double localTimeline,
|
||||
double bufferTime,
|
||||
double latestRemoteTime)
|
||||
{
|
||||
// we want local timeline to always be 'bufferTime' behind remote.
|
||||
double targetTime = latestRemoteTime - bufferTime;
|
||||
|
||||
// we define a boundary of 'bufferTime' around the target time.
|
||||
// this is where catchup / slowdown will happen.
|
||||
// outside of the area, we clamp.
|
||||
double lowerBound = targetTime - bufferTime; // how far behind we can get
|
||||
double upperBound = targetTime + bufferTime; // how far ahead we can get
|
||||
return Mathd.Clamp(localTimeline, lowerBound, upperBound);
|
||||
}
|
||||
|
||||
// call this for every received snapshot.
|
||||
// adds / inserts it to the list & initializes local time if needed.
|
||||
public static void InsertAndAdjust<T>(
|
||||
@ -158,7 +184,7 @@ public static void InsertAndAdjust<T>(
|
||||
//
|
||||
// in practice, scramble is rare and won't make much difference
|
||||
double previousLocalTime = buffer.Values[buffer.Count - 2].localTime;
|
||||
double lastestLocalTime = buffer.Values[buffer.Count - 1].localTime;
|
||||
double lastestLocalTime = buffer.Values[buffer.Count - 1].localTime;
|
||||
|
||||
// this is the delivery time since last snapshot
|
||||
double localDeliveryTime = lastestLocalTime - previousLocalTime;
|
||||
@ -178,7 +204,12 @@ public static void InsertAndAdjust<T>(
|
||||
// snapshots may arrive out of order, we can not use last-timeline.
|
||||
// we need to use the inserted snapshot's time - timeline.
|
||||
double latestRemoteTime = snapshot.remoteTime;
|
||||
double timeDiff = latestRemoteTime - localTimeline;
|
||||
|
||||
// ensure timeline stays within a reasonable bound behind/ahead.
|
||||
localTimeline = TimelineClamp(localTimeline, bufferTime, latestRemoteTime);
|
||||
|
||||
// calculate timediff after localTimeline override changes
|
||||
double timeDiff = latestRemoteTime - localTimeline;
|
||||
|
||||
// next, calculate average of a few seconds worth of timediffs.
|
||||
// this gives smoother results.
|
||||
@ -196,6 +227,11 @@ public static void InsertAndAdjust<T>(
|
||||
// than we sould have access to in our buffer :)
|
||||
driftEma.Add(timeDiff);
|
||||
|
||||
// timescale depends on driftEma.
|
||||
// driftEma only changes when inserting.
|
||||
// therefore timescale only needs to be calculated when inserting.
|
||||
// saves CPU cycles in Update.
|
||||
|
||||
// next up, calculate how far we are currently away from bufferTime
|
||||
double drift = driftEma.Value - bufferTime;
|
||||
|
||||
@ -235,7 +271,7 @@ public static void Sample<T>(
|
||||
for (int i = 0; i < buffer.Count - 1; ++i)
|
||||
{
|
||||
// is local time between these two?
|
||||
T first = buffer.Values[i];
|
||||
T first = buffer.Values[i];
|
||||
T second = buffer.Values[i + 1];
|
||||
if (localTimeline >= first.remoteTime &&
|
||||
localTimeline <= second.remoteTime)
|
||||
@ -308,7 +344,7 @@ public static void StepInterpolation<T>(
|
||||
|
||||
// save from/to
|
||||
fromSnapshot = buffer.Values[from];
|
||||
toSnapshot = buffer.Values[to];
|
||||
toSnapshot = buffer.Values[to];
|
||||
|
||||
// remove older snapshots that we definitely don't need anymore.
|
||||
// after(!) using the indices.
|
||||
|
@ -0,0 +1,68 @@
|
||||
// snapshot interpolation settings struct.
|
||||
// can easily be exposed in Unity inspectors.
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mirror
|
||||
{
|
||||
// class so we can define defaults easily
|
||||
[Serializable]
|
||||
public class SnapshotInterpolationSettings
|
||||
{
|
||||
// decrease bufferTime at runtime to see the catchup effect.
|
||||
// increase to see slowdown.
|
||||
// 'double' so we can have very precise dynamic adjustment without rounding
|
||||
[Header("Buffering")]
|
||||
[Tooltip("Local simulation is behind by sendInterval * multiplier seconds.\n\nThis guarantees that we always have enough snapshots in the buffer to mitigate lags & jitter.\n\nIncrease this if the simulation isn't smooth. By default, it should be around 2.")]
|
||||
public double bufferTimeMultiplier = 2;
|
||||
|
||||
// catchup /////////////////////////////////////////////////////////////
|
||||
// catchup thresholds in 'frames'.
|
||||
// half a frame might be too aggressive.
|
||||
[Header("Catchup / Slowdown")]
|
||||
[Tooltip("Slowdown begins when the local timeline is moving too fast towards remote time. Threshold is in frames worth of snapshots.\n\nThis needs to be negative.\n\nDon't modify unless you know what you are doing.")]
|
||||
public float catchupNegativeThreshold = -1; // careful, don't want to run out of snapshots
|
||||
|
||||
[Tooltip("Catchup begins when the local timeline is moving too slow and getting too far away from remote time. Threshold is in frames worth of snapshots.\n\nThis needs to be positive.\n\nDon't modify unless you know what you are doing.")]
|
||||
public float catchupPositiveThreshold = 1;
|
||||
|
||||
[Tooltip("Local timeline acceleration in % while catching up.")]
|
||||
[Range(0, 1)]
|
||||
public double catchupSpeed = 0.02f; // see snap interp demo. 1% is too slow.
|
||||
|
||||
[Tooltip("Local timeline slowdown in % while slowing down.")]
|
||||
[Range(0, 1)]
|
||||
public double slowdownSpeed = 0.04f; // slow down a little faster so we don't encounter empty buffer (= jitter)
|
||||
|
||||
[Tooltip("Catchup/Slowdown is adjusted over n-second exponential moving average.")]
|
||||
public int driftEmaDuration = 1; // shouldn't need to modify this, but expose it anyway
|
||||
|
||||
// dynamic buffer time adjustment //////////////////////////////////////
|
||||
// dynamically adjusts bufferTimeMultiplier for smooth results.
|
||||
// to understand how this works, try this manually:
|
||||
//
|
||||
// - disable dynamic adjustment
|
||||
// - set jitter = 0.2 (20% is a lot!)
|
||||
// - notice some stuttering
|
||||
// - disable interpolation to see just how much jitter this really is(!)
|
||||
// - enable interpolation again
|
||||
// - manually increase bufferTimeMultiplier to 3-4
|
||||
// ... the cube slows down (blue) until it's smooth
|
||||
// - with dynamic adjustment enabled, it will set 4 automatically
|
||||
// ... the cube slows down (blue) until it's smooth as well
|
||||
//
|
||||
// note that 20% jitter is extreme.
|
||||
// for this to be perfectly smooth, set the safety tolerance to '2'.
|
||||
// but realistically this is not necessary, and '1' is enough.
|
||||
[Header("Dynamic Adjustment")]
|
||||
[Tooltip("Automatically adjust bufferTimeMultiplier for smooth results.\nSets a low multiplier on stable connections, and a high multiplier on jittery connections.")]
|
||||
public bool dynamicAdjustment = true;
|
||||
|
||||
[Tooltip("Safety buffer that is always added to the dynamic bufferTimeMultiplier adjustment.")]
|
||||
public float dynamicAdjustmentTolerance = 1; // 1 is realistically just fine, 2 is very very safe even for 20% jitter. can be half a frame too. (see above comments)
|
||||
|
||||
[Tooltip("Dynamic adjustment is computed over n-second exponential moving average standard deviation.")]
|
||||
public int deliveryTimeEmaDuration = 2; // 1-2s recommended to capture average delivery time
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f955b76b7956417088c03992b3622dc9
|
||||
timeCreated: 1678507210
|
@ -1,3 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: afe2b5ed49634971a2aec720ad74e5cd
|
||||
timeCreated: 1666288442
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
@ -119,11 +119,13 @@ public override void OnSerializeDelta(NetworkWriter writer)
|
||||
switch (change.operation)
|
||||
{
|
||||
case Operation.OP_ADD:
|
||||
case Operation.OP_REMOVE:
|
||||
case Operation.OP_SET:
|
||||
writer.Write(change.key);
|
||||
writer.Write(change.item);
|
||||
break;
|
||||
case Operation.OP_REMOVE:
|
||||
writer.Write(change.key);
|
||||
break;
|
||||
case Operation.OP_CLEAR:
|
||||
break;
|
||||
}
|
||||
@ -204,15 +206,15 @@ public override void OnDeserializeDelta(NetworkReader reader)
|
||||
|
||||
case Operation.OP_REMOVE:
|
||||
key = reader.Read<TKey>();
|
||||
item = reader.Read<TValue>();
|
||||
if (apply)
|
||||
{
|
||||
if (objects.Remove(key))
|
||||
if (objects.TryGetValue(key, out item))
|
||||
{
|
||||
// add dirty + changes.
|
||||
// ClientToServer needs to set dirty in server OnDeserialize.
|
||||
// no access check: server OnDeserialize can always
|
||||
// write, even for ClientToServer (for broadcasting).
|
||||
objects.Remove(key);
|
||||
AddOperation(Operation.OP_REMOVE, key, item, false);
|
||||
}
|
||||
}
|
||||
|
@ -34,53 +34,53 @@ public static class AccurateInterval
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool Elapsed(double time, double interval, ref double lastTime)
|
||||
{
|
||||
if (time >= lastTime + interval)
|
||||
{
|
||||
// naive implementation:
|
||||
//lastTime = time;
|
||||
// enough time elapsed?
|
||||
if (time < lastTime + interval)
|
||||
return false;
|
||||
|
||||
// accurate but doesn't handle heavy load situations:
|
||||
//lastTime += interval;
|
||||
// naive implementation:
|
||||
//lastTime = time;
|
||||
|
||||
// heavy load edge case:
|
||||
// * interval is 100ms
|
||||
// * server is under heavy load, Updates slow down to 1/s
|
||||
// * Elapsed(1.000) returns true.
|
||||
// technically 10 intervals have elapsed.
|
||||
// * server recovers to normal, Updates are every 10ms again
|
||||
// * Elapsed(1.010) should return false again until 1.100.
|
||||
//
|
||||
// increasing lastTime by interval would require 10 more calls
|
||||
// to ever catch up again:
|
||||
// lastTime += interval
|
||||
//
|
||||
// as result, the next 10 calls to Elapsed would return true.
|
||||
// Elapsed(1.001) => true
|
||||
// Elapsed(1.002) => true
|
||||
// Elapsed(1.003) => true
|
||||
// ...
|
||||
// even though technically the delta was not >= interval.
|
||||
//
|
||||
// this would keep the server under heavy load, and it may never
|
||||
// catch-up. this is not ideal for large virtual worlds.
|
||||
//
|
||||
// instead, we want to skip multiples of 'interval' and only
|
||||
// keep the remainder.
|
||||
//
|
||||
// see also: AccurateIntervalTests.Slowdown()
|
||||
// accurate but doesn't handle heavy load situations:
|
||||
//lastTime += interval;
|
||||
|
||||
// easy to understand:
|
||||
//double elapsed = time - lastTime;
|
||||
//double remainder = elapsed % interval;
|
||||
//lastTime = time - remainder;
|
||||
// heavy load edge case:
|
||||
// * interval is 100ms
|
||||
// * server is under heavy load, Updates slow down to 1/s
|
||||
// * Elapsed(1.000) returns true.
|
||||
// technically 10 intervals have elapsed.
|
||||
// * server recovers to normal, Updates are every 10ms again
|
||||
// * Elapsed(1.010) should return false again until 1.100.
|
||||
//
|
||||
// increasing lastTime by interval would require 10 more calls
|
||||
// to ever catch up again:
|
||||
// lastTime += interval
|
||||
//
|
||||
// as result, the next 10 calls to Elapsed would return true.
|
||||
// Elapsed(1.001) => true
|
||||
// Elapsed(1.002) => true
|
||||
// Elapsed(1.003) => true
|
||||
// ...
|
||||
// even though technically the delta was not >= interval.
|
||||
//
|
||||
// this would keep the server under heavy load, and it may never
|
||||
// catch-up. this is not ideal for large virtual worlds.
|
||||
//
|
||||
// instead, we want to skip multiples of 'interval' and only
|
||||
// keep the remainder.
|
||||
//
|
||||
// see also: AccurateIntervalTests.Slowdown()
|
||||
|
||||
// easier: set to rounded multiples of interval (fholm).
|
||||
// long to match double time.
|
||||
long multiplier = (long)(time / interval);
|
||||
lastTime = multiplier * interval;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
// easy to understand:
|
||||
//double elapsed = time - lastTime;
|
||||
//double remainder = elapsed % interval;
|
||||
//lastTime = time - remainder;
|
||||
|
||||
// easier: set to rounded multiples of interval (fholm).
|
||||
// long to match double time.
|
||||
long multiplier = (long)(time / interval);
|
||||
lastTime = multiplier * interval;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ namespace Mirror
|
||||
{
|
||||
public struct ExponentialMovingAverage
|
||||
{
|
||||
readonly float alpha;
|
||||
readonly double alpha;
|
||||
bool initialized;
|
||||
|
||||
public double Value;
|
||||
@ -17,7 +17,7 @@ public struct ExponentialMovingAverage
|
||||
public ExponentialMovingAverage(int n)
|
||||
{
|
||||
// standard N-day EMA alpha calculation
|
||||
alpha = 2.0f / (n + 1);
|
||||
alpha = 2.0 / (n + 1);
|
||||
initialized = false;
|
||||
Value = 0;
|
||||
Variance = 0;
|
||||
@ -41,5 +41,13 @@ public void Add(double newValue)
|
||||
initialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
initialized = false;
|
||||
Value = 0;
|
||||
Variance = 0;
|
||||
StandardDeviation = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,15 +5,20 @@ namespace Mirror
|
||||
{
|
||||
public static class Mathd
|
||||
{
|
||||
// Unity 2020 doesn't have Math.Clamp yet.
|
||||
/// <summary>Clamps value between 0 and 1 and returns value.</summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static double Clamp01(double value)
|
||||
public static double Clamp(double value, double min, double max)
|
||||
{
|
||||
if (value < 0.0)
|
||||
return 0;
|
||||
return value > 1 ? 1 : value;
|
||||
if (value < min) return min;
|
||||
if (value > max) return max;
|
||||
return value;
|
||||
}
|
||||
|
||||
/// <summary>Clamps value between 0 and 1 and returns value.</summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static double Clamp01(double value) => Clamp(value, 0, 1);
|
||||
|
||||
/// <summary>Calculates the linear parameter t that produces the interpolant value within the range [a, b].</summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static double InverseLerp(double a, double b, double value) =>
|
||||
|
@ -33,9 +33,7 @@ public static void OnPostProcessScene()
|
||||
// if we had a [ConflictComponent] attribute that would be better than this check.
|
||||
// also there is no context about which scene this is in.
|
||||
if (identity.GetComponent<NetworkManager>() != null)
|
||||
{
|
||||
Debug.LogError("NetworkManager has a NetworkIdentity component. This will cause the NetworkManager object to be disabled, so it is not recommended.");
|
||||
}
|
||||
|
||||
// not spawned before?
|
||||
// OnPostProcessScene is called after additive scene loads too,
|
||||
@ -88,22 +86,12 @@ static void PrepareSceneObject(NetworkIdentity identity)
|
||||
identity.gameObject.SetActive(false);
|
||||
|
||||
// safety check for prefabs with more than one NetworkIdentity
|
||||
#if UNITY_2018_2_OR_NEWER
|
||||
GameObject prefabGO = PrefabUtility.GetCorrespondingObjectFromSource(identity.gameObject);
|
||||
#else
|
||||
GameObject prefabGO = PrefabUtility.GetPrefabParent(identity.gameObject);
|
||||
#endif
|
||||
if (prefabGO)
|
||||
{
|
||||
#if UNITY_2018_3_OR_NEWER
|
||||
GameObject prefabRootGO = prefabGO.transform.root.gameObject;
|
||||
#else
|
||||
GameObject prefabRootGO = PrefabUtility.FindPrefabRoot(prefabGO);
|
||||
#endif
|
||||
if (prefabRootGO != null && prefabRootGO.GetComponentsInChildren<NetworkIdentity>().Length > 1)
|
||||
{
|
||||
Debug.LogWarning($"Prefab {prefabRootGO.name} has several NetworkIdentity components attached to itself or its children, this is not supported.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -57,11 +57,7 @@ public static void WeaveExistingAssemblies()
|
||||
}
|
||||
}
|
||||
|
||||
#if UNITY_2019_3_OR_NEWER
|
||||
EditorUtility.RequestScriptReload();
|
||||
#else
|
||||
UnityEditorInternal.InternalEditorUtility.RequestScriptReload();
|
||||
#endif
|
||||
}
|
||||
|
||||
static Assembly FindCompilationPipelineAssembly(string assemblyName) =>
|
||||
|
@ -11,6 +11,7 @@ public enum ClientState
|
||||
Connected = 2,
|
||||
Disconnecting = 3,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Client used to control websockets
|
||||
/// <para>Base class used by WebSocketClientWebGl and WebSocketClientStandAlone</para>
|
||||
@ -90,6 +91,8 @@ public void ProcessMessageQueue(MonoBehaviour behaviour)
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (receiveQueue.Count > 0)
|
||||
Debug.LogWarning($"SimpleWebClient ProcessMessageQueue has {receiveQueue.Count} remaining.");
|
||||
}
|
||||
|
||||
public abstract void Connect(Uri serverAddress);
|
||||
|
@ -19,9 +19,7 @@ public bool TryHandshake(Connection conn, Uri uri)
|
||||
|
||||
byte[] keyBuffer = new byte[16];
|
||||
using (RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider())
|
||||
{
|
||||
rng.GetBytes(keyBuffer);
|
||||
}
|
||||
|
||||
string key = Convert.ToBase64String(keyBuffer);
|
||||
string keySum = key + Constants.HandshakeGUID;
|
||||
@ -52,20 +50,29 @@ public bool TryHandshake(Connection conn, Uri uri)
|
||||
|
||||
if (!lengthOrNull.HasValue)
|
||||
{
|
||||
Log.Error("[SimpleWebTransport] Connected closed before handshake");
|
||||
Log.Error("[SimpleWebTransport] Connection closed before handshake");
|
||||
return false;
|
||||
}
|
||||
|
||||
string responseString = Encoding.ASCII.GetString(responseBuffer, 0, lengthOrNull.Value);
|
||||
Log.Verbose($"[SimpleWebTransport] Handshake Response {responseString}");
|
||||
|
||||
string acceptHeader = "Sec-WebSocket-Accept: ";
|
||||
int startIndex = responseString.IndexOf(acceptHeader, StringComparison.InvariantCultureIgnoreCase) + acceptHeader.Length;
|
||||
int startIndex = responseString.IndexOf(acceptHeader, StringComparison.InvariantCultureIgnoreCase);
|
||||
|
||||
if (startIndex < 0)
|
||||
{
|
||||
Log.Error($"[SimpleWebTransport] Unexpected Handshake Response {responseString}");
|
||||
return false;
|
||||
}
|
||||
|
||||
startIndex += acceptHeader.Length;
|
||||
int endIndex = responseString.IndexOf("\r\n", startIndex);
|
||||
string responseKey = responseString.Substring(startIndex, endIndex - startIndex);
|
||||
|
||||
if (responseKey != expectedResponse)
|
||||
{
|
||||
Log.Error($"[SimpleWebTransport] Response key incorrect, Response:{responseKey} Expected:{expectedResponse}");
|
||||
Log.Error($"[SimpleWebTransport] Response key incorrect\nResponse:{responseKey}\nExpected:{expectedResponse}");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -60,7 +60,7 @@ void ConnectAndReceiveLoop(Uri serverAddress)
|
||||
bool success = sslHelper.TryCreateStream(conn, serverAddress);
|
||||
if (!success)
|
||||
{
|
||||
Log.Warn("[SimpleWebTransport] Failed to create Stream");
|
||||
Log.Warn($"[SimpleWebTransport] Failed to create Stream with {serverAddress}");
|
||||
conn.Dispose();
|
||||
return;
|
||||
}
|
||||
@ -68,12 +68,12 @@ void ConnectAndReceiveLoop(Uri serverAddress)
|
||||
success = handshake.TryHandshake(conn, serverAddress);
|
||||
if (!success)
|
||||
{
|
||||
Log.Warn("[SimpleWebTransport] Failed Handshake");
|
||||
Log.Warn($"[SimpleWebTransport] Failed Handshake with {serverAddress}");
|
||||
conn.Dispose();
|
||||
return;
|
||||
}
|
||||
|
||||
Log.Info("[SimpleWebTransport] HandShake Successful");
|
||||
Log.Info($"[SimpleWebTransport] HandShake Successful with {serverAddress}");
|
||||
|
||||
state = ClientState.Connected;
|
||||
|
||||
@ -121,14 +121,11 @@ public override void Disconnect()
|
||||
{
|
||||
state = ClientState.Disconnecting;
|
||||
Log.Info("[SimpleWebTransport] Disconnect Called");
|
||||
|
||||
if (conn == null)
|
||||
{
|
||||
state = ClientState.NotConnected;
|
||||
}
|
||||
else
|
||||
{
|
||||
conn?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public override void Send(ArraySegment<byte> segment)
|
||||
|
@ -5,6 +5,7 @@
|
||||
namespace Mirror.SimpleWeb
|
||||
{
|
||||
#if !UNITY_2021_3_OR_NEWER
|
||||
|
||||
// Unity 2019 doesn't have ArraySegment.ToArray() yet.
|
||||
public static class Extensions
|
||||
{
|
||||
@ -75,6 +76,7 @@ public override void Send(ArraySegment<byte> segment)
|
||||
{
|
||||
if (ConnectingSendQueue == null)
|
||||
ConnectingSendQueue = new Queue<byte[]>();
|
||||
|
||||
ConnectingSendQueue.Enqueue(segment.ToArray());
|
||||
}
|
||||
}
|
||||
@ -91,6 +93,7 @@ void onOpen()
|
||||
byte[] next = ConnectingSendQueue.Dequeue();
|
||||
SimpleWebJSLib.Send(index, next, 0, next.Length);
|
||||
}
|
||||
|
||||
ConnectingSendQueue = null;
|
||||
}
|
||||
}
|
||||
|
@ -63,7 +63,8 @@ public void Dispose()
|
||||
|
||||
public void CopyTo(byte[] target, int offset)
|
||||
{
|
||||
if (count > (target.Length + offset)) throw new ArgumentException($"{nameof(count)} was greater than {nameof(target)}.length", nameof(target));
|
||||
if (count > (target.Length + offset))
|
||||
throw new ArgumentException($"{nameof(count)} was greater than {nameof(target)}.length", nameof(target));
|
||||
|
||||
Buffer.BlockCopy(array, 0, target, offset, count);
|
||||
}
|
||||
@ -75,7 +76,8 @@ public void CopyFrom(ArraySegment<byte> segment)
|
||||
|
||||
public void CopyFrom(byte[] source, int offset, int length)
|
||||
{
|
||||
if (length > array.Length) throw new ArgumentException($"{nameof(length)} was greater than {nameof(array)}.length", nameof(length));
|
||||
if (length > array.Length)
|
||||
throw new ArgumentException($"{nameof(length)} was greater than {nameof(array)}.length", nameof(length));
|
||||
|
||||
count = length;
|
||||
Buffer.BlockCopy(source, offset, array, 0, length);
|
||||
@ -83,24 +85,20 @@ public void CopyFrom(byte[] source, int offset, int length)
|
||||
|
||||
public void CopyFrom(IntPtr bufferPtr, int length)
|
||||
{
|
||||
if (length > array.Length) throw new ArgumentException($"{nameof(length)} was greater than {nameof(array)}.length", nameof(length));
|
||||
if (length > array.Length)
|
||||
throw new ArgumentException($"{nameof(length)} was greater than {nameof(array)}.length", nameof(length));
|
||||
|
||||
count = length;
|
||||
Marshal.Copy(bufferPtr, array, 0, length);
|
||||
}
|
||||
|
||||
public ArraySegment<byte> ToSegment()
|
||||
{
|
||||
return new ArraySegment<byte>(array, 0, count);
|
||||
}
|
||||
public ArraySegment<byte> ToSegment() => new ArraySegment<byte>(array, 0, count);
|
||||
|
||||
[Conditional("UNITY_ASSERTIONS")]
|
||||
internal void Validate(int arraySize)
|
||||
{
|
||||
if (array.Length != arraySize)
|
||||
{
|
||||
Log.Error("[SimpleWebTransport] Buffer that was returned had an array of the wrong size");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -124,9 +122,7 @@ public ArrayBuffer Take()
|
||||
{
|
||||
IncrementCreated();
|
||||
if (buffers.TryDequeue(out ArrayBuffer buffer))
|
||||
{
|
||||
return buffer;
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Verbose($"[SimpleWebTransport] BufferBucket({arraySize}) create new");
|
||||
@ -186,17 +182,13 @@ public BufferPool(int bucketCount, int smallest, int largest)
|
||||
if (smallest < 1) throw new ArgumentException("Smallest must be at least 1");
|
||||
if (largest < smallest) throw new ArgumentException("Largest must be greater than smallest");
|
||||
|
||||
|
||||
this.bucketCount = bucketCount;
|
||||
this.smallest = smallest;
|
||||
this.largest = largest;
|
||||
|
||||
|
||||
// split range over log scale (more buckets for smaller sizes)
|
||||
|
||||
double minLog = Math.Log(this.smallest);
|
||||
double maxLog = Math.Log(this.largest);
|
||||
|
||||
double range = maxLog - minLog;
|
||||
double each = range / (bucketCount - 1);
|
||||
|
||||
@ -235,16 +227,12 @@ public BufferPool(int bucketCount, int smallest, int largest)
|
||||
void Validate()
|
||||
{
|
||||
if (buckets[0].arraySize != smallest)
|
||||
{
|
||||
Log.Error($"[SimpleWebTransport] BufferPool Failed to create bucket for smallest. bucket:{buckets[0].arraySize} smallest{smallest}");
|
||||
}
|
||||
|
||||
int largestBucket = buckets[bucketCount - 1].arraySize;
|
||||
// rounded using Ceiling, so allowed to be 1 more that largest
|
||||
if (largestBucket != largest && largestBucket != largest + 1)
|
||||
{
|
||||
Log.Error($"[SimpleWebTransport] BufferPool Failed to create bucket for largest. bucket:{largestBucket} smallest{largest}");
|
||||
}
|
||||
}
|
||||
|
||||
public ArrayBuffer Take(int size)
|
||||
@ -252,12 +240,8 @@ public ArrayBuffer Take(int size)
|
||||
if (size > largest) { throw new ArgumentException($"Size ({size}) is greatest that largest ({largest})"); }
|
||||
|
||||
for (int i = 0; i < bucketCount; i++)
|
||||
{
|
||||
if (size <= buckets[i].arraySize)
|
||||
{
|
||||
return buckets[i].Take();
|
||||
}
|
||||
}
|
||||
|
||||
throw new ArgumentException($"Size ({size}) is greatest that largest ({largest})");
|
||||
}
|
||||
|
@ -40,15 +40,15 @@ public void Dispose()
|
||||
Log.Verbose($"[SimpleWebTransport] Dispose {ToString()}");
|
||||
|
||||
// check hasDisposed first to stop ThreadInterruptedException on lock
|
||||
if (hasDisposed) { return; }
|
||||
if (hasDisposed) return;
|
||||
|
||||
Log.Info($"[SimpleWebTransport] Connection Close: {ToString()}");
|
||||
|
||||
|
||||
lock (disposedLock)
|
||||
{
|
||||
// check hasDisposed again inside lock to make sure no other object has called this
|
||||
if (hasDisposed) { return; }
|
||||
if (hasDisposed) return;
|
||||
|
||||
hasDisposed = true;
|
||||
|
||||
// stop threads first so they don't try to use disposed objects
|
||||
@ -72,9 +72,7 @@ public void Dispose()
|
||||
|
||||
// release all buffers in send queue
|
||||
while (sendQueue.TryDequeue(out ArrayBuffer buffer))
|
||||
{
|
||||
buffer.Release();
|
||||
}
|
||||
|
||||
onDispose.Invoke(this);
|
||||
}
|
||||
@ -83,14 +81,17 @@ public void Dispose()
|
||||
public override string ToString()
|
||||
{
|
||||
if (hasDisposed)
|
||||
{
|
||||
return $"[Conn:{connId}, Disposed]";
|
||||
}
|
||||
else
|
||||
{
|
||||
System.Net.EndPoint endpoint = client?.Client?.RemoteEndPoint;
|
||||
return $"[Conn:{connId}, endPoint:{endpoint}]";
|
||||
}
|
||||
try
|
||||
{
|
||||
System.Net.EndPoint endpoint = client?.Client?.RemoteEndPoint;
|
||||
return $"[Conn:{connId}, endPoint:{endpoint}]";
|
||||
}
|
||||
catch (SocketException)
|
||||
{
|
||||
return $"[Conn:{connId}, endPoint:n/a]";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -13,29 +13,21 @@ public static class MessageProcessor
|
||||
public static bool NeedToReadShortLength(byte[] buffer)
|
||||
{
|
||||
byte lenByte = FirstLengthByte(buffer);
|
||||
|
||||
return lenByte == Constants.UshortPayloadLength;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool NeedToReadLongLength(byte[] buffer)
|
||||
{
|
||||
byte lenByte = FirstLengthByte(buffer);
|
||||
|
||||
return lenByte == Constants.UlongPayloadLength;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static int GetOpcode(byte[] buffer)
|
||||
{
|
||||
return buffer[0] & 0b0000_1111;
|
||||
}
|
||||
public static int GetOpcode(byte[] buffer) => buffer[0] & 0b0000_1111;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static int GetPayloadLength(byte[] buffer)
|
||||
{
|
||||
byte lenByte = FirstLengthByte(buffer);
|
||||
return GetMessageLength(buffer, 0, lenByte);
|
||||
}
|
||||
public static int GetPayloadLength(byte[] buffer) => GetMessageLength(buffer, 0, FirstLengthByte(buffer));
|
||||
|
||||
/// <summary>
|
||||
/// Has full message been sent
|
||||
@ -43,10 +35,7 @@ public static int GetPayloadLength(byte[] buffer)
|
||||
/// <param name="buffer"></param>
|
||||
/// <returns></returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool Finished(byte[] buffer)
|
||||
{
|
||||
return (buffer[0] & 0b1000_0000) != 0;
|
||||
}
|
||||
public static bool Finished(byte[] buffer) => (buffer[0] & 0b1000_0000) != 0;
|
||||
|
||||
public static void ValidateHeader(byte[] buffer, int maxLength, bool expectMask, bool opCodeContinuation = false)
|
||||
{
|
||||
@ -114,9 +103,8 @@ static int GetMessageLength(byte[] buffer, int offset, byte lenByte)
|
||||
value |= ((ulong)buffer[offset + 9] << 0);
|
||||
|
||||
if (value > int.MaxValue)
|
||||
{
|
||||
throw new NotSupportedException($"Can't receive payloads larger that int.max: {int.MaxValue}");
|
||||
}
|
||||
|
||||
return (int)value;
|
||||
}
|
||||
else // is less than 126
|
||||
@ -130,9 +118,7 @@ static int GetMessageLength(byte[] buffer, int offset, byte lenByte)
|
||||
static void ThrowIfMaskNotExpected(bool hasMask, bool expectMask)
|
||||
{
|
||||
if (hasMask != expectMask)
|
||||
{
|
||||
throw new InvalidDataException($"Message expected mask to be {expectMask} but was {hasMask}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <exception cref="InvalidDataException"></exception>
|
||||
@ -174,9 +160,7 @@ static void ThrowIfBadOpCode(int opcode, bool finished, bool opCodeContinuation)
|
||||
static void ThrowIfLengthZero(int msglen)
|
||||
{
|
||||
if (msglen == 0)
|
||||
{
|
||||
throw new InvalidDataException("Message length was zero");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -185,9 +169,7 @@ static void ThrowIfLengthZero(int msglen)
|
||||
public static void ThrowIfMsgLengthTooLong(int msglen, int maxLength)
|
||||
{
|
||||
if (msglen > maxLength)
|
||||
{
|
||||
throw new InvalidDataException("Message length is greater than max length");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -20,9 +20,8 @@ public static int Read(Stream stream, byte[] outBuffer, int outOffset, int lengt
|
||||
{
|
||||
int read = stream.Read(outBuffer, outOffset + received, length - received);
|
||||
if (read == 0)
|
||||
{
|
||||
throw new ReadHelperException("returned 0");
|
||||
}
|
||||
|
||||
received += read;
|
||||
}
|
||||
}
|
||||
@ -36,9 +35,7 @@ public static int Read(Stream stream, byte[] outBuffer, int outOffset, int lengt
|
||||
}
|
||||
|
||||
if (received != length)
|
||||
{
|
||||
throw new ReadHelperException("returned not equal to length");
|
||||
}
|
||||
|
||||
return outOffset + received;
|
||||
}
|
||||
@ -96,15 +93,11 @@ public static bool TryRead(Stream stream, byte[] outBuffer, int outOffset, int l
|
||||
endIndex++;
|
||||
// when all is match return with read length
|
||||
if (endIndex >= endLength)
|
||||
{
|
||||
return read;
|
||||
}
|
||||
}
|
||||
// if n not match reset to 0
|
||||
else
|
||||
{
|
||||
endIndex = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (IOException e)
|
||||
@ -125,8 +118,6 @@ public class ReadHelperException : Exception
|
||||
{
|
||||
public ReadHelperException(string message) : base(message) { }
|
||||
|
||||
protected ReadHelperException(SerializationInfo info, StreamingContext context) : base(info, context)
|
||||
{
|
||||
}
|
||||
protected ReadHelperException(SerializationInfo info, StreamingContext context) : base(info, context) { }
|
||||
}
|
||||
}
|
||||
|
@ -60,9 +60,7 @@ public static void Loop(Config config)
|
||||
TcpClient client = conn.client;
|
||||
|
||||
while (client.Connected)
|
||||
{
|
||||
ReadOneMessage(config, readBuffer);
|
||||
}
|
||||
|
||||
Log.Info($"[SimpleWebTransport] {conn} Not Connected");
|
||||
}
|
||||
@ -105,7 +103,6 @@ public static void Loop(Config config)
|
||||
finally
|
||||
{
|
||||
Profiler.EndThreadProfiling();
|
||||
|
||||
conn.Dispose();
|
||||
}
|
||||
}
|
||||
@ -183,22 +180,16 @@ static Header ReadHeader(Config config, byte[] buffer, bool opCodeContinuation =
|
||||
Log.Verbose($"[SimpleWebTransport] Message From {conn}");
|
||||
|
||||
if (MessageProcessor.NeedToReadShortLength(buffer))
|
||||
{
|
||||
header.offset = ReadHelper.Read(stream, buffer, header.offset, Constants.ShortLength);
|
||||
}
|
||||
if (MessageProcessor.NeedToReadLongLength(buffer))
|
||||
{
|
||||
header.offset = ReadHelper.Read(stream, buffer, header.offset, Constants.LongLength);
|
||||
}
|
||||
|
||||
Log.DumpBuffer($"[SimpleWebTransport] Raw Header", buffer, 0, header.offset);
|
||||
|
||||
MessageProcessor.ValidateHeader(buffer, maxMessageSize, expectMask, opCodeContinuation);
|
||||
|
||||
if (expectMask)
|
||||
{
|
||||
header.offset = ReadHelper.Read(stream, buffer, header.offset, Constants.MaskSize);
|
||||
}
|
||||
|
||||
header.opcode = MessageProcessor.GetOpcode(buffer);
|
||||
header.payloadLength = MessageProcessor.GetPayloadLength(buffer);
|
||||
@ -232,9 +223,7 @@ static ArrayBuffer CopyMessageToBuffer(BufferPool bufferPool, bool expectMask, b
|
||||
MessageProcessor.ToggleMask(buffer, msgOffset, arrayBuffer, payloadLength, buffer, maskOffset);
|
||||
}
|
||||
else
|
||||
{
|
||||
arrayBuffer.CopyFrom(buffer, msgOffset, payloadLength);
|
||||
}
|
||||
|
||||
return arrayBuffer;
|
||||
}
|
||||
@ -251,20 +240,15 @@ static void HandleCloseMessage(Config config, byte[] buffer, int msgOffset, int
|
||||
|
||||
// dump after mask off
|
||||
Log.DumpBuffer($"[SimpleWebTransport] Message", buffer, msgOffset, payloadLength);
|
||||
|
||||
Log.Info($"[SimpleWebTransport] Close: {GetCloseCode(buffer, msgOffset)} message:{GetCloseMessage(buffer, msgOffset, payloadLength)}");
|
||||
|
||||
conn.Dispose();
|
||||
}
|
||||
|
||||
static string GetCloseMessage(byte[] buffer, int msgOffset, int payloadLength)
|
||||
{
|
||||
return Encoding.UTF8.GetString(buffer, msgOffset + 2, payloadLength - 2);
|
||||
}
|
||||
=> Encoding.UTF8.GetString(buffer, msgOffset + 2, payloadLength - 2);
|
||||
|
||||
static int GetCloseCode(byte[] buffer, int msgOffset)
|
||||
{
|
||||
return buffer[msgOffset + 0] << 8 | buffer[msgOffset + 1];
|
||||
}
|
||||
=> buffer[msgOffset + 0] << 8 | buffer[msgOffset + 1];
|
||||
}
|
||||
}
|
||||
|
@ -59,9 +59,8 @@ public static void Loop(Config config)
|
||||
conn.sendPending.Wait();
|
||||
// wait for 1ms for mirror to send other messages
|
||||
if (SendLoopConfig.sleepBeforeSend)
|
||||
{
|
||||
Thread.Sleep(1);
|
||||
}
|
||||
|
||||
conn.sendPending.Reset();
|
||||
|
||||
if (SendLoopConfig.batchSend)
|
||||
@ -194,9 +193,7 @@ public static int WriteHeader(byte[] buffer, int startOffset, int msgLength, boo
|
||||
}
|
||||
|
||||
if (setMask)
|
||||
{
|
||||
buffer[startOffset + 1] |= 0b1000_0000;
|
||||
}
|
||||
|
||||
return sendLength + startOffset;
|
||||
}
|
||||
|
@ -128,7 +128,6 @@ static void AppendGuid(byte[] keyBuffer)
|
||||
byte[] CreateHash(byte[] keyBuffer)
|
||||
{
|
||||
Log.Verbose($"[SimpleWebTransport] Handshake Hashing {Encoding.ASCII.GetString(keyBuffer, 0, MergedKeyLength)}", false);
|
||||
|
||||
return sha1.ComputeHash(keyBuffer, 0, MergedKeyLength);
|
||||
}
|
||||
|
||||
|
@ -29,9 +29,17 @@ internal class ServerSslHelper
|
||||
|
||||
public ServerSslHelper(SslConfig sslConfig)
|
||||
{
|
||||
Console.Clear();
|
||||
|
||||
config = sslConfig;
|
||||
if (config.enabled)
|
||||
{
|
||||
certificate = new X509Certificate2(config.certPath, config.certPassword);
|
||||
|
||||
Console.ForegroundColor = ConsoleColor.Cyan;
|
||||
Console.WriteLine($"[SimpleWebTransport] SSL Certificate {certificate.Subject} loaded with expiration of {certificate.GetExpirationDateString()}");
|
||||
Console.ResetColor();
|
||||
}
|
||||
}
|
||||
|
||||
internal bool TryCreateStream(Connection conn)
|
||||
@ -46,7 +54,10 @@ internal bool TryCreateStream(Connection conn)
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error($"[SimpleWebTransport] Create SSLStream Failed: {e}", false);
|
||||
Console.ForegroundColor = ConsoleColor.Red;
|
||||
Console.WriteLine($"[SimpleWebTransport] Create SSLStream Failed: {e.Message}");
|
||||
Console.ResetColor();
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -65,10 +76,7 @@ Stream CreateStream(NetworkStream stream)
|
||||
return sslStream;
|
||||
}
|
||||
|
||||
bool acceptClient(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
|
||||
{
|
||||
// always accept client
|
||||
return true;
|
||||
}
|
||||
// always accept client
|
||||
bool acceptClient(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) => true;
|
||||
}
|
||||
}
|
||||
|
@ -6,28 +6,26 @@ namespace Mirror.SimpleWeb
|
||||
{
|
||||
public class SimpleWebServer
|
||||
{
|
||||
readonly int maxMessagesPerTick;
|
||||
public event Action<int> onConnect;
|
||||
public event Action<int> onDisconnect;
|
||||
public event Action<int, ArraySegment<byte>> onData;
|
||||
public event Action<int, Exception> onError;
|
||||
|
||||
readonly int maxMessagesPerTick;
|
||||
readonly WebSocketServer server;
|
||||
readonly BufferPool bufferPool;
|
||||
|
||||
public bool Active { get; private set; }
|
||||
|
||||
public SimpleWebServer(int maxMessagesPerTick, TcpConfig tcpConfig, int maxMessageSize, int handshakeMaxSize, SslConfig sslConfig)
|
||||
{
|
||||
this.maxMessagesPerTick = maxMessagesPerTick;
|
||||
// use max because bufferpool is used for both messages and handshake
|
||||
int max = Math.Max(maxMessageSize, handshakeMaxSize);
|
||||
bufferPool = new BufferPool(5, 20, max);
|
||||
|
||||
server = new WebSocketServer(tcpConfig, maxMessageSize, handshakeMaxSize, sslConfig, bufferPool);
|
||||
}
|
||||
|
||||
public bool Active { get; private set; }
|
||||
|
||||
public event Action<int> onConnect;
|
||||
public event Action<int> onDisconnect;
|
||||
public event Action<int, ArraySegment<byte>> onData;
|
||||
public event Action<int, Exception> onError;
|
||||
|
||||
public void Start(ushort port)
|
||||
{
|
||||
server.Listen(port);
|
||||
@ -48,28 +46,19 @@ public void SendAll(List<int> connectionIds, ArraySegment<byte> source)
|
||||
|
||||
// make copy of array before for each, data sent to each client is the same
|
||||
foreach (int id in connectionIds)
|
||||
{
|
||||
server.Send(id, buffer);
|
||||
}
|
||||
}
|
||||
|
||||
public void SendOne(int connectionId, ArraySegment<byte> source)
|
||||
{
|
||||
ArrayBuffer buffer = bufferPool.Take(source.Count);
|
||||
buffer.CopyFrom(source);
|
||||
|
||||
server.Send(connectionId, buffer);
|
||||
}
|
||||
|
||||
public bool KickClient(int connectionId)
|
||||
{
|
||||
return server.CloseConnection(connectionId);
|
||||
}
|
||||
public bool KickClient(int connectionId) => server.CloseConnection(connectionId);
|
||||
|
||||
public string GetClientAddress(int connectionId)
|
||||
{
|
||||
return server.GetClientAddress(connectionId);
|
||||
}
|
||||
public string GetClientAddress(int connectionId) => server.GetClientAddress(connectionId);
|
||||
|
||||
/// <summary>
|
||||
/// Processes all new messages
|
||||
@ -114,6 +103,13 @@ public void ProcessMessageQueue(MonoBehaviour behaviour)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (server.receiveQueue.Count > 0)
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.Yellow;
|
||||
Console.WriteLine($"SimpleWebServer ProcessMessageQueue has {server.receiveQueue.Count} remaining.");
|
||||
Console.ResetColor();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -37,7 +37,9 @@ public void Listen(int port)
|
||||
listener = TcpListener.Create(port);
|
||||
listener.Start();
|
||||
|
||||
Log.Info($"[SimpleWebTransport] Server has started on port {port}", false);
|
||||
Console.ForegroundColor = ConsoleColor.Green;
|
||||
Console.WriteLine($"[SimpleWebTransport] Server Started on {port}!");
|
||||
Console.ResetColor();
|
||||
|
||||
acceptThread = new Thread(acceptLoop);
|
||||
acceptThread.IsBackground = true;
|
||||
@ -53,7 +55,8 @@ public void Stop()
|
||||
listener?.Stop();
|
||||
acceptThread = null;
|
||||
|
||||
Log.Info("[SimpleWebTransport] Server stopped, Closing all connections...", false);
|
||||
Console.WriteLine($"[SimpleWebTransport] Server stopped...closing all connections.");
|
||||
|
||||
// make copy so that foreach doesn't break if values are removed
|
||||
Connection[] connectionsCopy = connections.Values.ToArray();
|
||||
foreach (Connection conn in connectionsCopy)
|
||||
@ -73,12 +76,11 @@ void acceptLoop()
|
||||
TcpClient client = listener.AcceptTcpClient();
|
||||
tcpConfig.ApplyTo(client);
|
||||
|
||||
|
||||
// TODO keep track of connections before they are in connections dictionary
|
||||
// this might not be a problem as HandshakeAndReceiveLoop checks for stop
|
||||
// and returns/disposes before sending message to queue
|
||||
Connection conn = new Connection(client, AfterConnectionDisposed);
|
||||
Log.Info($"[SimpleWebTransport] A client connected {conn}", false);
|
||||
Console.WriteLine($"[SimpleWebTransport] A client connected {conn}", false);
|
||||
|
||||
// handshake needs its own thread as it needs to wait for message from client
|
||||
Thread receiveThread = new Thread(() => HandshakeAndReceiveLoop(conn));
|
||||
@ -108,7 +110,9 @@ void HandshakeAndReceiveLoop(Connection conn)
|
||||
bool success = sslHelper.TryCreateStream(conn);
|
||||
if (!success)
|
||||
{
|
||||
Log.Error($"[SimpleWebTransport] Failed to create SSL Stream {conn}, false");
|
||||
Console.ForegroundColor = ConsoleColor.Red;
|
||||
Console.WriteLine($"[SimpleWebTransport] Failed to create SSL Stream {conn}");
|
||||
Console.ResetColor();
|
||||
conn.Dispose();
|
||||
return;
|
||||
}
|
||||
@ -116,12 +120,12 @@ void HandshakeAndReceiveLoop(Connection conn)
|
||||
success = handShake.TryHandshake(conn);
|
||||
|
||||
if (success)
|
||||
{
|
||||
Log.Info($"[SimpleWebTransport] Sent Handshake {conn}, false");
|
||||
}
|
||||
Console.WriteLine($"[SimpleWebTransport] Sent Handshake {conn}, false");
|
||||
else
|
||||
{
|
||||
Log.Error($"[SimpleWebTransport] Handshake Failed {conn}, false");
|
||||
Console.ForegroundColor = ConsoleColor.Red;
|
||||
Console.WriteLine($"[SimpleWebTransport] Handshake Failed {conn}");
|
||||
Console.ResetColor();
|
||||
conn.Dispose();
|
||||
return;
|
||||
}
|
||||
@ -129,7 +133,7 @@ void HandshakeAndReceiveLoop(Connection conn)
|
||||
// check if Stop has been called since accepting this client
|
||||
if (serverStopped)
|
||||
{
|
||||
Log.Info("[SimpleWebTransport] Server stops after successful handshake", false);
|
||||
Console.WriteLine("[SimpleWebTransport] Server stops after successful handshake", false);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -150,7 +154,7 @@ void HandshakeAndReceiveLoop(Connection conn)
|
||||
|
||||
conn.sendThread = sendThread;
|
||||
sendThread.IsBackground = true;
|
||||
sendThread.Name = $"[SimpleWebTransport] SendLoop {conn.connId}, false";
|
||||
sendThread.Name = $"SendThread {conn.connId}";
|
||||
sendThread.Start();
|
||||
|
||||
ReceiveLoop.Config receiveConfig = new ReceiveLoop.Config(
|
||||
@ -162,9 +166,24 @@ void HandshakeAndReceiveLoop(Connection conn)
|
||||
|
||||
ReceiveLoop.Loop(receiveConfig);
|
||||
}
|
||||
catch (ThreadInterruptedException e) { Log.InfoException(e); }
|
||||
catch (ThreadAbortException e) { Log.InfoException(e); }
|
||||
catch (Exception e) { Log.Exception(e); }
|
||||
catch (ThreadInterruptedException e)
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.Red;
|
||||
Console.WriteLine($"[SimpleWebTransport] Handshake ThreadInterruptedException {e.Message}");
|
||||
Console.ResetColor();
|
||||
}
|
||||
catch (ThreadAbortException e)
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.Red;
|
||||
Console.WriteLine($"[SimpleWebTransport] Handshake ThreadAbortException {e.Message}");
|
||||
Console.ResetColor();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.Red;
|
||||
Console.WriteLine($"[SimpleWebTransport] Handshake Exception {e.Message}");
|
||||
Console.ResetColor();
|
||||
}
|
||||
finally
|
||||
{
|
||||
// close here in case connect fails
|
||||
@ -190,7 +209,9 @@ public void Send(int id, ArrayBuffer buffer)
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Warn($"[SimpleWebTransport] Cant send message to {id} because connection was not found in dictionary. Maybe it disconnected.", false);
|
||||
Console.ForegroundColor = ConsoleColor.Yellow;
|
||||
Console.WriteLine($"[SimpleWebTransport] Cannot send message to {id} because connection was not found in dictionary. Maybe it disconnected.");
|
||||
Console.ResetColor();
|
||||
}
|
||||
}
|
||||
|
||||
@ -198,13 +219,17 @@ public bool CloseConnection(int id)
|
||||
{
|
||||
if (connections.TryGetValue(id, out Connection conn))
|
||||
{
|
||||
Log.Info($"[SimpleWebTransport] Kicking connection {id}", false);
|
||||
Console.ForegroundColor = ConsoleColor.Magenta;
|
||||
Console.WriteLine($"[SimpleWebTransport] Kicking connection {id}");
|
||||
Console.ResetColor();
|
||||
conn.Dispose();
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Warn($"[SimpleWebTransport] Failed to kick {id} because id not found", false);
|
||||
Console.ForegroundColor = ConsoleColor.Yellow;
|
||||
Console.WriteLine($"[SimpleWebTransport] Failed to kick {id} because id not found.");
|
||||
Console.ResetColor();
|
||||
|
||||
return false;
|
||||
}
|
||||
@ -213,12 +238,13 @@ public bool CloseConnection(int id)
|
||||
public string GetClientAddress(int id)
|
||||
{
|
||||
if (connections.TryGetValue(id, out Connection conn))
|
||||
{
|
||||
return conn.client.Client.RemoteEndPoint.ToString();
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Error($"[SimpleWebTransport] Cant get address of connection {id} because connection was not found in dictionary", false);
|
||||
Console.ForegroundColor = ConsoleColor.Yellow;
|
||||
Console.WriteLine($"[SimpleWebTransport] Cannot get address of connection {id} because connection was not found in dictionary.");
|
||||
Console.ResetColor();
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3b5390adca4e2bb4791cb930316d6f3e
|
||||
guid: 4e70cf4af4e02304383ce86f48b66aea
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
|
@ -41,24 +41,28 @@ public class SimpleWebTransport : Transport
|
||||
[Tooltip("Groups messages in queue before calling Stream.Send")]
|
||||
public bool batchSend = true;
|
||||
|
||||
[Tooltip("Waits for 1ms before grouping and sending messages. " +
|
||||
"This gives time for mirror to finish adding message to queue so that less groups need to be made. " +
|
||||
[Tooltip("Waits for 1ms before grouping and sending messages.\n" +
|
||||
"This gives time for mirror to finish adding message to queue so that less groups need to be made.\n" +
|
||||
"If WaitBeforeSend is true then BatchSend Will also be set to true")]
|
||||
public bool waitBeforeSend = false;
|
||||
public bool waitBeforeSend = true;
|
||||
|
||||
[Header("Ssl Settings")]
|
||||
[Tooltip("Sets connect scheme to wss. Useful when client needs to connect using wss when TLS is outside of transport, NOTE: if sslEnabled is true clientUseWss is also true")]
|
||||
[Tooltip("Sets connect scheme to wss. Useful when client needs to connect using wss when TLS is outside of transport.\nNOTE: if sslEnabled is true clientUseWss is also true")]
|
||||
public bool clientUseWss;
|
||||
|
||||
[Tooltip("Requires wss connections on server, only to be used with SSL cert.json, never with reverse proxy.\nNOTE: if sslEnabled is true clientUseWss is also true")]
|
||||
public bool sslEnabled;
|
||||
[Tooltip("Path to json file that contains path to cert and its password\n\nUse Json file so that cert password is not included in client builds\n\nSee cert.example.Json")]
|
||||
|
||||
[Tooltip("Path to json file that contains path to cert and its password\nUse Json file so that cert password is not included in client builds\nSee Assets/Mirror/Transports/.cert.example.Json")]
|
||||
public string sslCertJson = "./cert.json";
|
||||
|
||||
[Tooltip("Protocols that SSL certificate is created to support.")]
|
||||
public SslProtocols sslProtocols = SslProtocols.Tls12;
|
||||
|
||||
[Header("Debug")]
|
||||
[Tooltip("Log functions uses ConditionalAttribute which will effect which log methods are allowed. DEBUG allows warn/error, SIMPLEWEB_LOG_ENABLED allows all")]
|
||||
[FormerlySerializedAs("logLevels")]
|
||||
[SerializeField] Log.Levels _logLevels = Log.Levels.none;
|
||||
[SerializeField] Log.Levels _logLevels = Log.Levels.info;
|
||||
|
||||
/// <summary>
|
||||
/// <para>Gets _logLevels field</para>
|
||||
@ -74,29 +78,25 @@ public Log.Levels LogLevels
|
||||
}
|
||||
}
|
||||
|
||||
void OnValidate()
|
||||
{
|
||||
Log.level = _logLevels;
|
||||
}
|
||||
|
||||
SimpleWebClient client;
|
||||
SimpleWebServer server;
|
||||
|
||||
TcpConfig TcpConfig => new TcpConfig(noDelay, sendTimeout, receiveTimeout);
|
||||
|
||||
public override bool Available()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
public override int GetMaxPacketSize(int channelId = 0)
|
||||
{
|
||||
return maxMessageSize;
|
||||
}
|
||||
|
||||
void Awake()
|
||||
{
|
||||
Log.level = _logLevels;
|
||||
}
|
||||
|
||||
void OnValidate()
|
||||
{
|
||||
Log.level = _logLevels;
|
||||
}
|
||||
|
||||
public override bool Available() => true;
|
||||
|
||||
public override int GetMaxPacketSize(int channelId = 0) => maxMessageSize;
|
||||
|
||||
public override void Shutdown()
|
||||
{
|
||||
client?.Disconnect();
|
||||
@ -108,7 +108,7 @@ public override void Shutdown()
|
||||
#region Client
|
||||
|
||||
string GetClientScheme() => (sslEnabled || clientUseWss) ? SecureScheme : NormalScheme;
|
||||
string GetServerScheme() => sslEnabled ? SecureScheme : NormalScheme;
|
||||
|
||||
public override bool ClientConnected()
|
||||
{
|
||||
// not null and not NotConnected (we want to return true if connecting or disconnecting)
|
||||
@ -117,13 +117,6 @@ public override bool ClientConnected()
|
||||
|
||||
public override void ClientConnect(string hostname)
|
||||
{
|
||||
// connecting or connected
|
||||
if (ClientConnected())
|
||||
{
|
||||
Debug.LogError("[SimpleWebTransport] Already Connected");
|
||||
return;
|
||||
}
|
||||
|
||||
UriBuilder builder = new UriBuilder
|
||||
{
|
||||
Scheme = GetClientScheme(),
|
||||
@ -131,8 +124,21 @@ public override void ClientConnect(string hostname)
|
||||
Port = port
|
||||
};
|
||||
|
||||
ClientConnect(builder.Uri);
|
||||
}
|
||||
|
||||
public override void ClientConnect(Uri uri)
|
||||
{
|
||||
// connecting or connected
|
||||
if (ClientConnected())
|
||||
{
|
||||
Debug.LogError("[SimpleWebTransport] Already Connected");
|
||||
return;
|
||||
}
|
||||
|
||||
client = SimpleWebClient.Create(maxMessageSize, clientMaxMessagesPerTick, TcpConfig);
|
||||
if (client == null) { return; }
|
||||
if (client == null)
|
||||
return;
|
||||
|
||||
client.onConnect += OnClientConnected.Invoke;
|
||||
|
||||
@ -152,7 +158,7 @@ public override void ClientConnect(string hostname)
|
||||
ClientDisconnect();
|
||||
};
|
||||
|
||||
client.Connect(builder.Uri);
|
||||
client.Connect(uri);
|
||||
}
|
||||
|
||||
public override void ClientDisconnect()
|
||||
@ -197,6 +203,19 @@ public override void ClientEarlyUpdate()
|
||||
|
||||
#region Server
|
||||
|
||||
string GetServerScheme() => sslEnabled ? SecureScheme : NormalScheme;
|
||||
|
||||
public override Uri ServerUri()
|
||||
{
|
||||
UriBuilder builder = new UriBuilder
|
||||
{
|
||||
Scheme = GetServerScheme(),
|
||||
Host = Dns.GetHostName(),
|
||||
Port = port
|
||||
};
|
||||
return builder.Uri;
|
||||
}
|
||||
|
||||
public override bool ServerActive()
|
||||
{
|
||||
return server != null && server.Active;
|
||||
@ -205,9 +224,7 @@ public override bool ServerActive()
|
||||
public override void ServerStart()
|
||||
{
|
||||
if (ServerActive())
|
||||
{
|
||||
Debug.LogError("[SimpleWebTransport] Server Already Started");
|
||||
}
|
||||
|
||||
SslConfig config = SslConfigLoader.Load(sslEnabled, sslCertJson, sslProtocols);
|
||||
server = new SimpleWebServer(serverMaxMessagesPerTick, TcpConfig, maxMessageSize, handshakeMaxSize, config);
|
||||
@ -226,9 +243,7 @@ public override void ServerStart()
|
||||
public override void ServerStop()
|
||||
{
|
||||
if (!ServerActive())
|
||||
{
|
||||
Debug.LogError("[SimpleWebTransport] Server Not Active");
|
||||
}
|
||||
|
||||
server.Stop();
|
||||
server = null;
|
||||
@ -237,9 +252,7 @@ public override void ServerStop()
|
||||
public override void ServerDisconnect(int connectionId)
|
||||
{
|
||||
if (!ServerActive())
|
||||
{
|
||||
Debug.LogError("[SimpleWebTransport] Server Not Active");
|
||||
}
|
||||
|
||||
server.KickClient(connectionId);
|
||||
}
|
||||
@ -270,21 +283,7 @@ public override void ServerSend(int connectionId, ArraySegment<byte> segment, in
|
||||
OnServerDataSent?.Invoke(connectionId, segment, Channels.Reliable);
|
||||
}
|
||||
|
||||
public override string ServerGetClientAddress(int connectionId)
|
||||
{
|
||||
return server.GetClientAddress(connectionId);
|
||||
}
|
||||
|
||||
public override Uri ServerUri()
|
||||
{
|
||||
UriBuilder builder = new UriBuilder
|
||||
{
|
||||
Scheme = GetServerScheme(),
|
||||
Host = Dns.GetHostName(),
|
||||
Port = port
|
||||
};
|
||||
return builder.Uri;
|
||||
}
|
||||
public override string ServerGetClientAddress(int connectionId) => server.GetClientAddress(connectionId);
|
||||
|
||||
// messages should always be processed in early update
|
||||
public override void ServerEarlyUpdate()
|
||||
|
@ -36,15 +36,12 @@ internal static Cert LoadCertJson(string certJsonPath)
|
||||
Cert cert = JsonUtility.FromJson<Cert>(json);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(cert.path))
|
||||
{
|
||||
throw new InvalidDataException("Cert Json didn't not contain \"path\"");
|
||||
}
|
||||
|
||||
// don't use IsNullOrWhiteSpace here because whitespace could be a valid password for a cert
|
||||
// password can also be empty
|
||||
if (string.IsNullOrEmpty(cert.password))
|
||||
{
|
||||
// password can be empty
|
||||
cert.password = string.Empty;
|
||||
}
|
||||
|
||||
return cert;
|
||||
}
|
||||
|
@ -45,13 +45,21 @@ public class #SCRIPTNAME# : NetworkAuthenticator
|
||||
public void OnAuthRequestMessage(NetworkConnectionToClient conn, AuthRequestMessage msg)
|
||||
{
|
||||
AuthResponseMessage authResponseMessage = new AuthResponseMessage();
|
||||
|
||||
conn.Send(authResponseMessage);
|
||||
|
||||
// Accept the successful authentication
|
||||
ServerAccept(conn);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when server stops, used to unregister message handlers if needed.
|
||||
/// </summary>
|
||||
public override void OnStopServer()
|
||||
{
|
||||
// Unregister the handler for the authentication request
|
||||
NetworkServer.UnregisterHandler<AuthRequestMessage>();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Client
|
||||
@ -72,7 +80,6 @@ public class #SCRIPTNAME# : NetworkAuthenticator
|
||||
public override void OnClientAuthenticate()
|
||||
{
|
||||
AuthRequestMessage authRequestMessage = new AuthRequestMessage();
|
||||
|
||||
NetworkClient.Send(authRequestMessage);
|
||||
}
|
||||
|
||||
@ -86,5 +93,14 @@ public class #SCRIPTNAME# : NetworkAuthenticator
|
||||
ClientAccept();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when client stops, used to unregister message handlers if needed.
|
||||
/// </summary>
|
||||
public override void OnStopClient()
|
||||
{
|
||||
// Unregister the handler for the authentication response
|
||||
NetworkClient.UnregisterHandler<AuthResponseMessage>();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
@ -105,6 +105,14 @@ public class #SCRIPTNAME# : NetworkRoomManager
|
||||
return base.OnRoomServerSceneLoadedForPlayer(conn, roomPlayer, gamePlayer);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This is called on server from NetworkRoomPlayer.CmdChangeReadyState when client indicates change in Ready status.
|
||||
/// </summary>
|
||||
public override void ReadyStatusChanged()
|
||||
{
|
||||
base.ReadyStatusChanged();
|
||||
}
|
||||
|
||||
/// <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>
|
||||
|
@ -9,18 +9,34 @@ using Mirror.Discovery;
|
||||
|
||||
public struct DiscoveryRequest : NetworkMessage
|
||||
{
|
||||
// Add properties for whatever information you want sent by clients
|
||||
// in their broadcast messages that servers will consume.
|
||||
// Add public fields (not properties) for whatever information you want
|
||||
// sent by clients in their broadcast messages that servers will use.
|
||||
}
|
||||
|
||||
public struct DiscoveryResponse : NetworkMessage
|
||||
{
|
||||
// Add properties for whatever information you want the server to return to
|
||||
// clients for them to display or consume for establishing a connection.
|
||||
// Add public fields (not properties) for whatever information you want the server
|
||||
// to return to clients for them to display or use for establishing a connection.
|
||||
}
|
||||
|
||||
public class #SCRIPTNAME# : NetworkDiscoveryBase<DiscoveryRequest, DiscoveryResponse>
|
||||
{
|
||||
#region Unity Callbacks
|
||||
|
||||
#if UNITY_EDITOR
|
||||
public override void OnValidate()
|
||||
{
|
||||
base.OnValidate();
|
||||
}
|
||||
#endif
|
||||
|
||||
public override void Start()
|
||||
{
|
||||
base.Start();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Server
|
||||
|
||||
/// <summary>
|
||||
|
@ -2,8 +2,8 @@
|
||||
"dependencies": {
|
||||
"com.unity.2d.sprite": "1.0.0",
|
||||
"com.unity.2d.tilemap": "1.0.0",
|
||||
"com.unity.ide.rider": "3.0.16",
|
||||
"com.unity.ide.visualstudio": "2.0.16",
|
||||
"com.unity.ide.rider": "3.0.18",
|
||||
"com.unity.ide.visualstudio": "2.0.17",
|
||||
"com.unity.ide.vscode": "1.2.5",
|
||||
"com.unity.test-framework": "1.1.31",
|
||||
"com.unity.testtools.codecoverage": "1.2.2",
|
||||
|
@ -20,7 +20,7 @@
|
||||
"url": "https://packages.unity.com"
|
||||
},
|
||||
"com.unity.ide.rider": {
|
||||
"version": "3.0.16",
|
||||
"version": "3.0.18",
|
||||
"depth": 0,
|
||||
"source": "registry",
|
||||
"dependencies": {
|
||||
@ -29,7 +29,7 @@
|
||||
"url": "https://packages.unity.com"
|
||||
},
|
||||
"com.unity.ide.visualstudio": {
|
||||
"version": "2.0.16",
|
||||
"version": "2.0.17",
|
||||
"depth": 0,
|
||||
"source": "registry",
|
||||
"dependencies": {
|
||||
|
@ -579,9 +579,9 @@ PlayerSettings:
|
||||
webGLDecompressionFallback: 0
|
||||
webGLPowerPreference: 2
|
||||
scriptingDefineSymbols:
|
||||
Server: MIRROR;MIRROR_55_0_OR_NEWER;MIRROR_57_0_OR_NEWER;MIRROR_58_0_OR_NEWER;MIRROR_65_0_OR_NEWER;MIRROR_66_0_OR_NEWER;MIRROR_2022_9_OR_NEWER;MIRROR_2022_10_OR_NEWER;MIRROR_70_0_OR_NEWER;MIRROR_71_0_OR_NEWER;MIRROR_73_OR_NEWER
|
||||
Standalone: MIRROR;MIRROR_55_0_OR_NEWER;MIRROR_57_0_OR_NEWER;MIRROR_58_0_OR_NEWER;MIRROR_65_0_OR_NEWER;MIRROR_66_0_OR_NEWER;MIRROR_2022_9_OR_NEWER;MIRROR_2022_10_OR_NEWER;MIRROR_70_0_OR_NEWER;MIRROR_71_0_OR_NEWER;MIRROR_73_OR_NEWER
|
||||
WebGL: MIRROR;MIRROR_55_0_OR_NEWER;MIRROR_57_0_OR_NEWER;MIRROR_58_0_OR_NEWER;MIRROR_65_0_OR_NEWER;MIRROR_66_0_OR_NEWER;MIRROR_2022_9_OR_NEWER;MIRROR_2022_10_OR_NEWER;MIRROR_70_0_OR_NEWER;MIRROR_71_0_OR_NEWER;MIRROR_73_OR_NEWER
|
||||
Server: MIRROR;MIRROR_57_0_OR_NEWER;MIRROR_58_0_OR_NEWER;MIRROR_65_0_OR_NEWER;MIRROR_66_0_OR_NEWER;MIRROR_2022_9_OR_NEWER;MIRROR_2022_10_OR_NEWER;MIRROR_70_0_OR_NEWER;MIRROR_71_0_OR_NEWER;MIRROR_73_OR_NEWER
|
||||
Standalone: MIRROR;MIRROR_57_0_OR_NEWER;MIRROR_58_0_OR_NEWER;MIRROR_65_0_OR_NEWER;MIRROR_66_0_OR_NEWER;MIRROR_2022_9_OR_NEWER;MIRROR_2022_10_OR_NEWER;MIRROR_70_0_OR_NEWER;MIRROR_71_0_OR_NEWER;MIRROR_73_OR_NEWER
|
||||
WebGL: MIRROR;MIRROR_57_0_OR_NEWER;MIRROR_58_0_OR_NEWER;MIRROR_65_0_OR_NEWER;MIRROR_66_0_OR_NEWER;MIRROR_2022_9_OR_NEWER;MIRROR_2022_10_OR_NEWER;MIRROR_70_0_OR_NEWER;MIRROR_71_0_OR_NEWER;MIRROR_73_OR_NEWER
|
||||
additionalCompilerArguments: {}
|
||||
platformArchitecture: {}
|
||||
scriptingBackend:
|
||||
|
@ -1,2 +1,2 @@
|
||||
m_EditorVersion: 2021.3.9f1
|
||||
m_EditorVersionWithRevision: 2021.3.9f1 (ad3870b89536)
|
||||
m_EditorVersion: 2021.3.21f1
|
||||
m_EditorVersionWithRevision: 2021.3.21f1 (1b156197d683)
|
||||
|
263
README.md
263
README.md
@ -1,263 +0,0 @@
|
||||
![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/N9QVxbM)
|
||||
[![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)
|
||||
|
||||
**It's only the dreamers who ever move mountains.**
|
||||
|
||||
<img src="https://user-images.githubusercontent.com/16416509/119117854-3e4e2b80-ba5c-11eb-8236-ce6cfd2b6b07.png" title="Original Concept Art for Games that made us dream. Copyright Blizzard, Blizzard, Riot Games, Joymax in that order."/>
|
||||
|
||||
## Mirror Networking
|
||||
The **#1** free **open source** game networking library for **Unity 2019 / 2020 / 2021**.
|
||||
|
||||
Used **in production** by major hits like [**Population: ONE**](https://www.populationonevr.com/) and many [more](#made-with-mirror).
|
||||
|
||||
Originally based on [**UNET**](https://blog.unity.com/technology/announcing-unet-new-unity-multiplayer-technology): battle tested **since 2014** for 8 years and counting!
|
||||
|
||||
Mirror is **[stable](https://mirror-networking.gitbook.io/docs/general/tests)**, [**modular**](#low-level-transports) & **[easy to use](https://mirror-networking.gitbook.io/)** for all types of games, even small [MMORPGs](#made-with-mirror) 🎮.
|
||||
|
||||
**Made in 🇩🇪🇺🇸🇬🇧🇸🇬🇹🇼 with ❤️**.
|
||||
|
||||
---
|
||||
## Architecture
|
||||
The **Server & Client** are **ONE project** in order to achieve maximum productivity.
|
||||
|
||||
Simply use **NetworkBehaviour** instead of **MonoBehaviour**.
|
||||
|
||||
Making multiplayer games this way is fun & easy:
|
||||
|
||||
```cs
|
||||
public class Player : NetworkBehaviour
|
||||
{
|
||||
// synced automatically
|
||||
[SyncVar] public int health = 100;
|
||||
|
||||
// lists, dictionaries, sets too
|
||||
SyncList<Item> inventory = new SyncList<Item>();
|
||||
|
||||
// server/client-only code
|
||||
[Server] void LevelUp() {}
|
||||
[Client] void Animate() {}
|
||||
|
||||
void Update()
|
||||
{
|
||||
// isServer/isClient for runtime checks
|
||||
if (isServer) Heal();
|
||||
if (isClient) Move();
|
||||
}
|
||||
|
||||
// zero overhead remote calls
|
||||
[Command] void CmdUseItem(int slot) {} // client to server
|
||||
[Rpc] void RpcRespawn() {} // server to all clients
|
||||
[TargetRpc] void Hello() {} // server to one client
|
||||
}
|
||||
```
|
||||
|
||||
There's also **NetworkServer** & **NetworkClient**. And that's about it 🤩.
|
||||
|
||||
---
|
||||
## Free, Open & Community Funded
|
||||
Mirror is **free & open source** (MIT Licensed).
|
||||
|
||||
"Free" as in free beer, and freedom to use it any way you like.
|
||||
|
||||
- Run [Dedicated Servers](https://mirror-networking.gitbook.io/docs/guides/server-hosting) anywhere.
|
||||
- Free player hosted games thanks to [Epic Relay](https://github.com/FakeByte/EpicOnlineTransport)!
|
||||
|
||||
Mirror is funded by [**Donations**](https://github.com/sponsors/vis2k) from our [fantastic community](https://discordapp.com/invite/N9QVxbM) of over 14,000 users!
|
||||
|
||||
<img src="https://user-images.githubusercontent.com/16416509/195067704-5577b581-b829-4c9f-80d0-b6270a3a59e7.png" title="Fitzcarraldo"/>
|
||||
|
||||
_The top quote is from Fitzcarraldo, which is quite reminiscent of this project._
|
||||
|
||||
---
|
||||
## Getting Started
|
||||
Get **Unity 2019 / 2020 / 2021 LTS**, [Download Mirror](https://assetstore.unity.com/packages/tools/network/mirror-129321), open one of the examples & press Play!
|
||||
|
||||
Check out our [Documentation](https://mirror-networking.gitbook.io/) to learn how it all works.
|
||||
|
||||
If you are migrating from UNET, then please check out our [Migration Guide](https://mirror-networking.gitbook.io/docs/general/migration-guide).
|
||||
|
||||
---
|
||||
## Mirror LTS (Long Term Support)
|
||||
Mirror LTS is available on the [Asset Store](https://assetstore.unity.com/packages/tools/network/mirror-lts-102631).
|
||||
|
||||
Mirror LTS gives you peace of mind to run your game in production.
|
||||
Without any breaking changes, ever!
|
||||
|
||||
* **Bug fixes** only.
|
||||
* **Consistent API**: update any time, without any breaking features.
|
||||
* Lives along side **Unity 2019/2020/2021** LTS.
|
||||
* Supported for two years at a time.
|
||||
|
||||
---
|
||||
## Made with Mirror
|
||||
### [Population: ONE](https://www.populationonevr.com/)
|
||||
[![Population: ONE](https://user-images.githubusercontent.com/16416509/178141286-9494c3a8-a4a5-4b06-af2b-b05b66162201.png)](https://www.populationonevr.com/)
|
||||
The [BigBoxVR](https://www.bigboxvr.com/) team started using Mirror in February 2019 for what eventually became one of the most popular Oculus Rift games.
|
||||
|
||||
In addition to [24/7 support](https://github.com/sponsors/vis2k) from the Mirror team, BigBoxVR also hired one of our engineers.
|
||||
|
||||
**Population: ONE** was [acquired by Facebook](https://uploadvr.com/population-one-facebook-bigbox-acquire/) in June 2021, and they've just released a new [Sandbox](https://www.youtube.com/watch?v=jcI0h8dn9tA) addon in 2022!
|
||||
|
||||
### [Nimoyd](https://www.nimoyd.com/)
|
||||
[![nimoyd_smaller](https://user-images.githubusercontent.com/16416509/178142672-340bac2c-628a-4610-bbf1-8f718cb5b033.jpg)](https://www.nimoyd.com/)
|
||||
Nudge Nudge Games' first title: the colorful, post-apocalyptic open world sandbox game [Nimoyd](https://store.steampowered.com/app/1313210/Nimoyd__Survival_Sandbox/) is being developed with Mirror.
|
||||
|
||||
_Soon to be released for PC & mobile!_
|
||||
|
||||
### [Dinkum](https://store.steampowered.com/app/1062520/Dinkum/)
|
||||
[![dinkum](https://user-images.githubusercontent.com/16416509/180051810-50c9ebfd-973b-4f2f-8448-d599443d9ce3.jpg)](https://store.steampowered.com/app/1062520/Dinkum/)
|
||||
Set in the Australian Outback, Dinkum is a relaxing farming & survival game. Made by just one developer, Dinkum already reached 1000+ "Overwhelmingly Positive" reviews 1 week after its early access release.
|
||||
|
||||
James Bendon initially made the game with UNET, and then [switched to Mirror](https://www.playdinkum.com/blog/2019/1/11/devlog-13-biomes-and-traps) in 2019.
|
||||
|
||||
### [A Glimpse of Luna](https://www.glimpse-luna.com/)
|
||||
[![a glimpse of luna](https://user-images.githubusercontent.com/16416509/178148229-5b619655-055a-4583-a1d3-18455bde631f.jpg)](https://www.glimpse-luna.com/)
|
||||
[A Glimpse of Luna](https://www.glimpse-luna.com/) - a tactical multiplayer card battle game with the most beautiful concept art & soundtrack.
|
||||
|
||||
Made with Mirror by two brothers with [no prior game development](https://www.youtube.com/watch?v=5J2wj8l4pFA&start=12) experience.
|
||||
|
||||
### [Sun Haven](https://store.steampowered.com/app/1432860/Sun_Haven/)
|
||||
[![sun haven](https://user-images.githubusercontent.com/16416509/185836661-2bfd6cd0-523a-4af4-bac7-c202ed01de7d.jpg)](https://store.steampowered.com/app/1432860/Sun_Haven/)
|
||||
[Sun Haven](https://store.steampowered.com/app/1432860/Sun_Haven/) - A beautiful human town, a hidden elven village, and a monster city filled with farming, magic, dragons, and adventure.
|
||||
|
||||
After their successful [Kickstarter](https://www.kickstarter.com/projects/sunhaven/sunhaven/description), Sun Haven was released on Steam in 2021 and later on ported to Mirror in 2022.
|
||||
|
||||
### [Inferna](https://inferna.net/)
|
||||
[![Inferna MMORPG](https://user-images.githubusercontent.com/16416509/178148768-5ba9ea5b-bcf1-4ace-ad7e-591f2185cbd5.jpg)](https://inferna.net/)
|
||||
One of the first MMORPGs made with Mirror, released in 2019.
|
||||
|
||||
An open world experience with over 1000 CCU during its peak, spread across multiple server instances.
|
||||
|
||||
### [Samutale](https://www.samutale.com/)
|
||||
[![samutale](https://user-images.githubusercontent.com/16416509/178149040-b54e0fa1-3c41-4925-8428-efd0526f8d44.jpg)](https://www.samutale.com/)
|
||||
A sandbox survival samurai MMORPG, originally released in September 2016.
|
||||
|
||||
Later on, the Netherlands based Maple Media switched their netcode to Mirror.
|
||||
|
||||
### [Untamed Isles](https://store.steampowered.com/app/1823300/Untamed_Isles/)
|
||||
[![Untamed Isles](https://user-images.githubusercontent.com/16416509/178143679-1c325b54-0938-4e84-97b6-b59db62a51e7.jpg)](https://store.steampowered.com/app/1823300/Untamed_Isles/)
|
||||
The turn based, monster taming **MMORPG** [Untamed Isles](https://store.steampowered.com/app/1823300/Untamed_Isles/) is currently being developed by [Phat Loot Studios](https://untamedisles.com/about/).
|
||||
|
||||
After their successful [Kickstarter](https://www.kickstarter.com/projects/untamedisles/untamed-isles), the New Zealand based studio is aiming for a 2022 release date.
|
||||
|
||||
### [Zooba](https://play.google.com/store/apps/details?id=com.wildlife.games.battle.royale.free.zooba&gl=US)
|
||||
[![Zooba](https://user-images.githubusercontent.com/16416509/178141846-60805ad5-5a6e-4840-8744-5194756c2a6d.jpg)](https://play.google.com/store/apps/details?id=com.wildlife.games.battle.royale.free.zooba&gl=US)
|
||||
[Wildlife Studio's](https://wildlifestudios.com/) hit Zooba made it to rank #5 of the largest battle royal shooters in the U.S. mobile market.
|
||||
|
||||
The game has over **50 million** downloads on [Google Play](https://play.google.com/store/apps/details?id=com.wildlife.games.battle.royale.free.zooba&gl=US), with Wildlife Studios as one of the top 10 largest mobile gaming companies in the world.
|
||||
|
||||
### [Portals](https://theportal.to/)
|
||||
[![Portals](https://user-images.githubusercontent.com/9826063/209373815-8e6288ba-22fc-4cee-8867-19f587188827.png)](https://theportal.to/)
|
||||
Animal Crossing meets Yakuza meets Minecraft — a city builder with a multiplayer central hub. Gather, trade and build — all in the browser!
|
||||
|
||||
### [SCP: Secret Laboratory](https://scpslgame.com/)
|
||||
[![scp - secret laboratory_smaller](https://user-images.githubusercontent.com/16416509/178142224-413b3455-cdff-472e-b918-4246631af12f.jpg)](https://scpslgame.com/)
|
||||
[Northwood Studios'](https://store.steampowered.com/developer/NWStudios/about/) first title: the multiplayer horror game SCP: Secret Laboratory was one of Mirror's early adopters.
|
||||
|
||||
Released in December 2017, today it has more than **140,000** reviews on [Steam](https://store.steampowered.com/app/700330/SCP_Secret_Laboratory/?curator_clanid=33782778).
|
||||
|
||||
### [Naïca Online](https://naicaonline.com/)
|
||||
[![Naica Online](https://user-images.githubusercontent.com/16416509/178147710-8ed83bbd-1bce-4e14-8465-edfb40af7c7f.png)](https://naicaonline.com/)
|
||||
[Naïca](https://naicaonline.com/) is a beautiful, free to play 2D pixel art MMORPG.
|
||||
|
||||
The [France based team](https://naicaonline.com/en/news/view/1) was one of Mirror's early adopters, releasing their first public beta in November 2020.
|
||||
|
||||
### [Laurum Online](https://laurum.online/)
|
||||
[![Laurum Online](https://user-images.githubusercontent.com/16416509/178149616-3852d198-6fc9-44d5-9f63-da4e52f5546a.jpg)](https://laurum.online/)
|
||||
[Laurum Online](https://play.google.com/store/apps/details?id=com.project7.project7beta) - a 2D retro mobile MMORPG with over 500,000 downloads on Google Play.
|
||||
|
||||
### [Empires Mobile](https://knightempire.online/)
|
||||
[![Empires Mobile](https://user-images.githubusercontent.com/16416509/207028553-c646f12c-c164-47d3-a1fc-ff79409c04fa.jpg)](https://knightempire.online/)
|
||||
[Empires Mobile](https://knightempire.online/) - Retro mobile MMORPG for Android and iOS, reaching 5000 CCU at times. Check out their [video](https://www.youtube.com/watch?v=v69lW9aWb-w) for some _early MMORPG_ nostalgia.
|
||||
|
||||
### [Castaways](https://www.castaways.com/)
|
||||
[![Castaways](https://user-images.githubusercontent.com/16416509/207313082-e6b95590-80c6-4685-b0d1-f1c39c236316.png)](https://www.castaways.com/)
|
||||
[Castaways](https://www.castaways.com/) is a sandbox game where you are castaway to a small remote island where you must work with others to survive and build a thriving new civilization.
|
||||
|
||||
Castaway runs in the Browser, thanks to Mirror's WebGL support.
|
||||
|
||||
### And many more...
|
||||
<a href="https://store.steampowered.com/app/719200/The_Wall/"><img src="https://cdn.akamai.steamstatic.com/steam/apps/719200/header.jpg?t=1588105839" height="100" title="The wall"/></a>
|
||||
<a href="https://store.steampowered.com/app/535630/One_More_Night/"><img src="https://cdn.akamai.steamstatic.com/steam/apps/535630/header.jpg?t=1584831320" height="100" title="One more night"/></a>
|
||||
<img src="https://i.ytimg.com/vi/D_f_MntrLVE/maxresdefault.jpg" height="100" title="Block story"/>
|
||||
<a href="https://nightz.io"><img src="https://user-images.githubusercontent.com/16416509/130729336-9c4e95d9-69bc-4410-b894-b2677159a472.jpg" height="100" title="Nightz.io"/></a>
|
||||
<a href="https://store.steampowered.com/app/1016030/Wawa_United/"><img src="https://user-images.githubusercontent.com/16416509/162982300-c29d89bc-210a-43ef-8cce-6e5555bb09bc.png" height="100" title="Wawa united"/></a>
|
||||
<a href="https://store.steampowered.com/app/1745640/MACE_Mapinguaris_Temple/"><img src="https://user-images.githubusercontent.com/16416509/166089837-bbecf190-0f06-4c88-910d-1ce87e2f171d.png" title="MACE" height="100"/></a>
|
||||
<a href="https://www.adversator.com/"><img src="https://user-images.githubusercontent.com/16416509/178641128-37dc270c-bedf-4891-8284-33573d1776b9.jpg" title="Adversator" height="100"/></a>
|
||||
<a href="https://store.steampowered.com/app/670260/Solace_Crafting/"><img src="https://user-images.githubusercontent.com/16416509/197175819-1c2720b6-97e6-4844-80b5-2197a7f22839.png" title="Solace Crafting" height="100"/></a>
|
||||
<a href="https://www.unitystation.org"><img src="https://user-images.githubusercontent.com/57072365/204021428-0c621067-d580-4c88-b551-3ac70f9da39d.jpg" title="UnityStation" height="100"/></a>
|
||||
|
||||
## Modular Transports
|
||||
Mirror uses **KCP** (reliable UDP) by default, but you may use any of our community transports for low level packet sending:
|
||||
* (built in) [KCP](https://github.com/vis2k/kcp2k): reliable UDP
|
||||
* (built in) [Telepathy](https://github.com/vis2k/Telepathy): TCP
|
||||
* (built in) [Websockets](https://github.com/MirrorNetworking/SimpleWebTransport): Websockets
|
||||
* [Ignorance](https://github.com/SoftwareGuy/Ignorance/): ENET UDP
|
||||
* [LiteNetLib](https://github.com/MirrorNetworking/LiteNetLibTransport/) UDP
|
||||
* [FizzySteam](https://github.com/Chykary/FizzySteamworks/): SteamNetwork
|
||||
* [FizzyFacepunch](https://github.com/Chykary/FizzyFacepunch/): SteamNetwork
|
||||
* [Epic Relay](https://github.com/FakeByte/EpicOnlineTransport): Epic Online Services
|
||||
* [Bubble](https://github.com/Squaresweets/BubbleTransport): Apple GameCenter
|
||||
* [Light Reflective Mirror](https://github.com/Derek-R-S/Light-Reflective-Mirror): Self-Hosted Relay
|
||||
* [Oculus P2P](https://github.com/hyferg/MirrorOculusP2P): Oculus Platform Service
|
||||
|
||||
## Benchmarks
|
||||
* [2022] mischa [400-800 CCU](https://discord.com/channels/343440455738064897/1007519701603205150/1019879180592238603) tests
|
||||
* [2021] [Jesus' Benchmarks](https://docs.google.com/document/d/1GMxcWAz3ePt3RioK8k4erpVSpujMkYje4scOuPwM8Ug/edit?usp=sharing)
|
||||
* [2019] [uMMORPG 480 CCU](https://youtu.be/mDCNff1S9ZU) (worst case)
|
||||
|
||||
## Development & Contributing
|
||||
Mirror is used **in production** by everything from small indie projects to million dollar funded games that will run for a decade or more.
|
||||
|
||||
We prefer to work slow & thoroughly in order to not break everyone's games 🐌.
|
||||
|
||||
Therefore, we need to [KISS](https://en.wikipedia.org/wiki/KISS_principle) 😗.
|
||||
|
||||
---
|
||||
# Bug Bounty
|
||||
<img src="https://user-images.githubusercontent.com/16416509/110572995-718b5900-8195-11eb-802c-235c82a03bf7.png">
|
||||
|
||||
A lot of projects use Mirror in production. If you found a critical bug / exploit in Mirror core, please reach out to us in private.
|
||||
Depending on the severity of the exploit, we offer $50 - $500 for now.
|
||||
Rewards based on Mirror's [donations](https://github.com/sponsors/vis2k), capped at amount of donations we received that month.
|
||||
|
||||
**Specifically we are looking for:**
|
||||
* Ways to crash a Mirror server
|
||||
* Ways to exploit a Mirror server
|
||||
* Ways to leave a Mirror server in undefined state
|
||||
|
||||
We are **not** looking for DOS/DDOS attacks. The exploit should be possible with just a couple of network packets, and it should be reproducible.
|
||||
|
||||
**Credits / past findings / fixes:**
|
||||
* 2020, fholm: fuzzing ConnectMessage to stop further connects [[#2397](https://github.com/vis2k/Mirror/pull/2397)]
|
||||
|
||||
---
|
||||
# Credits & Thanks 🙏
|
||||
🪞 **Alexey Abramychev** (UNET)<br/>
|
||||
🪞 **Alan**<br/>
|
||||
🪞 **c6burns** <br/>
|
||||
🪞 **Coburn** <br/>
|
||||
🪞 **cooper** <br/>
|
||||
🪞 **FakeByte** <br/>
|
||||
🪞 **fholm**<br/>
|
||||
🪞 **Gabe** (BigBoxVR)<br/>
|
||||
🪞 **imer** <br/>
|
||||
🪞 **James Frowen** <br/>
|
||||
🪞 **JesusLuvsYooh** <br/>
|
||||
🪞 **Mischa** <br/>
|
||||
🪞 **Mr. Gadget**<br/>
|
||||
🪞 **NinjaKickja** <br/>
|
||||
🪞 **Paul Pacheco**<br/>
|
||||
🪞 **Sean Riley** (UNET)<br/>
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user