Commit f6ed3c6f authored by cann-alberto's avatar cann-alberto
Browse files

first project commit

parent 4487b14f
fileFormatVersion: 2
guid: 7ec4f7556ca1e4b55a3381fc6a02b1bc
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:
using UnityEngine;
namespace Mirror
{
// [RequireComponent(typeof(Rigidbody))] <- OnValidate ensures this is on .target
[AddComponentMenu("Network/Network Rigidbody (Unreliable)")]
public class NetworkRigidbodyUnreliable : NetworkTransformUnreliable
{
bool clientAuthority => syncDirection == SyncDirection.ClientToServer;
Rigidbody rb;
bool wasKinematic;
// cach Rigidbody and original isKinematic setting
protected override void Awake()
{
// we can't overwrite .target to be a Rigidbody.
// but we can use its Rigidbody component.
rb = target.GetComponent<Rigidbody>();
if (rb == null)
{
Debug.LogError($"{name}'s NetworkRigidbody.target {target.name} is missing a Rigidbody", this);
return;
}
wasKinematic = rb.isKinematic;
base.Awake();
}
// reset forced isKinematic flag to original.
// otherwise the overwritten value would remain between sessions forever.
// for example, a game may run as client, set rigidbody.iskinematic=true,
// then run as server, where .iskinematic isn't touched and remains at
// the overwritten=true, even though the user set it to false originally.
public override void OnStopServer() => rb.isKinematic = wasKinematic;
public override void OnStopClient() => rb.isKinematic = wasKinematic;
// overwriting Construct() and Apply() to set Rigidbody.MovePosition
// would give more jittery movement.
// FixedUpdate for physics
void FixedUpdate()
{
// who ever has authority moves the Rigidbody with physics.
// everyone else simply sets it to kinematic.
// so that only the Transform component is synced.
// host mode
if (isServer && isClient)
{
// in host mode, we own it it if:
// clientAuthority is disabled (hence server / we own it)
// clientAuthority is enabled and we have authority over this object.
bool owned = !clientAuthority || IsClientWithAuthority;
// only set to kinematic if we don't own it
// otherwise don't touch isKinematic.
// the authority owner might use it either way.
if (!owned) rb.isKinematic = true;
}
// client only
else if (isClient)
{
// on the client, we own it only if clientAuthority is enabled,
// and we have authority over this object.
bool owned = IsClientWithAuthority;
// only set to kinematic if we don't own it
// otherwise don't touch isKinematic.
// the authority owner might use it either way.
if (!owned) rb.isKinematic = true;
}
// server only
else if (isServer)
{
// on the server, we always own it if clientAuthority is disabled.
bool owned = !clientAuthority;
// only set to kinematic if we don't own it
// otherwise don't touch isKinematic.
// the authority owner might use it either way.
if (!owned) rb.isKinematic = true;
}
}
protected override void OnValidate()
{
base.OnValidate();
// we can't overwrite .target to be a Rigidbody.
// but we can ensure that .target has a Rigidbody, and use it.
if (target.GetComponent<Rigidbody>() == null)
{
Debug.LogWarning($"{name}'s NetworkRigidbody.target {target.name} is missing a Rigidbody", this);
}
}
protected override void OnTeleport(Vector3 destination)
{
base.OnTeleport(destination);
rb.position = transform.position;
}
protected override void OnTeleport(Vector3 destination, Quaternion rotation)
{
base.OnTeleport(destination, rotation);
rb.position = transform.position;
rb.rotation = transform.rotation;
}
}
}
fileFormatVersion: 2
guid: 3b20dc110904e47f8a154cdcf6433eae
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:
using UnityEngine;
namespace Mirror
{
// [RequireComponent(typeof(Rigidbody))] <- OnValidate ensures this is on .target
[AddComponentMenu("Network/Network Rigidbody 2D (Unreliable)")]
public class NetworkRigidbodyUnreliable2D : NetworkTransformUnreliable
{
bool clientAuthority => syncDirection == SyncDirection.ClientToServer;
Rigidbody2D rb;
bool wasKinematic;
// cach Rigidbody and original isKinematic setting
protected override void Awake()
{
// we can't overwrite .target to be a Rigidbody.
// but we can use its Rigidbody component.
rb = target.GetComponent<Rigidbody2D>();
if (rb == null)
{
Debug.LogError($"{name}'s NetworkRigidbody2D.target {target.name} is missing a Rigidbody2D", this);
return;
}
wasKinematic = rb.isKinematic;
base.Awake();
}
// reset forced isKinematic flag to original.
// otherwise the overwritten value would remain between sessions forever.
// for example, a game may run as client, set rigidbody.iskinematic=true,
// then run as server, where .iskinematic isn't touched and remains at
// the overwritten=true, even though the user set it to false originally.
public override void OnStopServer() => rb.isKinematic = wasKinematic;
public override void OnStopClient() => rb.isKinematic = wasKinematic;
// overwriting Construct() and Apply() to set Rigidbody.MovePosition
// would give more jittery movement.
// FixedUpdate for physics
void FixedUpdate()
{
// who ever has authority moves the Rigidbody with physics.
// everyone else simply sets it to kinematic.
// so that only the Transform component is synced.
// host mode
if (isServer && isClient)
{
// in host mode, we own it it if:
// clientAuthority is disabled (hence server / we own it)
// clientAuthority is enabled and we have authority over this object.
bool owned = !clientAuthority || IsClientWithAuthority;
// only set to kinematic if we don't own it
// otherwise don't touch isKinematic.
// the authority owner might use it either way.
if (!owned) rb.isKinematic = true;
}
// client only
else if (isClient)
{
// on the client, we own it only if clientAuthority is enabled,
// and we have authority over this object.
bool owned = IsClientWithAuthority;
// only set to kinematic if we don't own it
// otherwise don't touch isKinematic.
// the authority owner might use it either way.
if (!owned) rb.isKinematic = true;
}
// server only
else if (isServer)
{
// on the server, we always own it if clientAuthority is disabled.
bool owned = !clientAuthority;
// only set to kinematic if we don't own it
// otherwise don't touch isKinematic.
// the authority owner might use it either way.
if (!owned) rb.isKinematic = true;
}
}
protected override void OnValidate()
{
base.OnValidate();
// we can't overwrite .target to be a Rigidbody.
// but we can ensure that .target has a Rigidbody, and use it.
if (target.GetComponent<Rigidbody2D>() == null)
{
Debug.LogWarning($"{name}'s NetworkRigidbody2D.target {target.name} is missing a Rigidbody2D", this);
}
}
protected override void OnTeleport(Vector3 destination)
{
base.OnTeleport(destination);
rb.position = transform.position;
}
protected override void OnTeleport(Vector3 destination, Quaternion rotation)
{
base.OnTeleport(destination, rotation);
rb.position = transform.position;
rb.rotation = transform.rotation.eulerAngles.z;
}
}
}
fileFormatVersion: 2
guid: 1c7e12ad9b9ae443c9fdf37e9f5ecd36
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.Serialization;
namespace Mirror
{
/// <summary>
/// This is a specialized NetworkManager that includes a networked room.
/// </summary>
/// <remarks>
/// <para>The room has slots that track the joined players, and a maximum player count that is enforced. It requires that the NetworkRoomPlayer component be on the room player objects.</para>
/// <para>NetworkRoomManager is derived from NetworkManager, and so it implements many of the virtual functions provided by the NetworkManager class. To avoid accidentally replacing functionality of the NetworkRoomManager, there are new virtual functions on the NetworkRoomManager that begin with "OnRoom". These should be used on classes derived from NetworkRoomManager instead of the virtual functions on NetworkManager.</para>
/// <para>The OnRoom*() functions have empty implementations on the NetworkRoomManager base class, so the base class functions do not have to be called.</para>
/// </remarks>
[AddComponentMenu("Network/Network Room Manager")]
[HelpURL("https://mirror-networking.gitbook.io/docs/components/network-room-manager")]
public class NetworkRoomManager : NetworkManager
{
public struct PendingPlayer
{
public NetworkConnectionToClient conn;
public GameObject roomPlayer;
}
[Header("Room Settings")]
[FormerlySerializedAs("m_ShowRoomGUI")]
[SerializeField]
[Tooltip("This flag controls whether the default UI is shown for the room")]
public bool showRoomGUI = true;
[FormerlySerializedAs("m_MinPlayers")]
[SerializeField]
[Tooltip("Minimum number of players to auto-start the game")]
public int minPlayers = 1;
[FormerlySerializedAs("m_RoomPlayerPrefab")]
[SerializeField]
[Tooltip("Prefab to use for the Room Player")]
public NetworkRoomPlayer roomPlayerPrefab;
/// <summary>
/// The scene to use for the room. This is similar to the offlineScene of the NetworkManager.
/// </summary>
[Scene]
public string RoomScene;
/// <summary>
/// The scene to use for the playing the game from the room. This is similar to the onlineScene of the NetworkManager.
/// </summary>
[Scene]
public string GameplayScene;
/// <summary>
/// List of players that are in the Room
/// </summary>
[FormerlySerializedAs("m_PendingPlayers")]
public HashSet<PendingPlayer> pendingPlayers = new HashSet<PendingPlayer>();
[Header("Diagnostics")]
/// <summary>
/// True when all players have submitted a Ready message
/// </summary>
[Tooltip("Diagnostic flag indicating all players are ready to play")]
[FormerlySerializedAs("allPlayersReady")]
[ReadOnly, SerializeField] bool _allPlayersReady;
/// <summary>
/// These slots track players that enter the room.
/// <para>The slotId on players is global to the game - across all players.</para>
/// </summary>
[ReadOnly, Tooltip("List of Room Player objects")]
public HashSet<NetworkRoomPlayer> roomSlots = new HashSet<NetworkRoomPlayer>();
public bool allPlayersReady
{
get => _allPlayersReady;
set
{
bool wasReady = _allPlayersReady;
bool nowReady = value;
if (wasReady != nowReady)
{
_allPlayersReady = value;
if (nowReady)
{
OnRoomServerPlayersReady();
}
else
{
OnRoomServerPlayersNotReady();
}
}
}
}
public override void OnValidate()
{
base.OnValidate();
// always <= maxConnections
minPlayers = Mathf.Min(minPlayers, maxConnections);
// always >= 0
minPlayers = Mathf.Max(minPlayers, 0);
if (roomPlayerPrefab != null)
{
NetworkIdentity identity = roomPlayerPrefab.GetComponent<NetworkIdentity>();
if (identity == null)
{
roomPlayerPrefab = null;
Debug.LogError("RoomPlayer prefab must have a NetworkIdentity component.");
}
}
}
void SceneLoadedForPlayer(NetworkConnectionToClient conn, GameObject roomPlayer)
{
//Debug.Log($"NetworkRoom SceneLoadedForPlayer scene: {SceneManager.GetActiveScene().path} {conn}");
if (Utils.IsSceneActive(RoomScene))
{
// cant be ready in room, add to ready list
PendingPlayer pending;
pending.conn = conn;
pending.roomPlayer = roomPlayer;
pendingPlayers.Add(pending);
return;
}
GameObject gamePlayer = OnRoomServerCreateGamePlayer(conn, roomPlayer);
if (gamePlayer == null)
{
// get start position from base class
Transform startPos = GetStartPosition();
gamePlayer = startPos != null
? Instantiate(playerPrefab, startPos.position, startPos.rotation)
: Instantiate(playerPrefab, Vector3.zero, Quaternion.identity);
}
if (!OnRoomServerSceneLoadedForPlayer(conn, roomPlayer, gamePlayer))
return;
// replace room player with game player
NetworkServer.ReplacePlayerForConnection(conn, gamePlayer, true);
}
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>
/// </summary>
public void CheckReadyToBegin()
{
if (!Utils.IsSceneActive(RoomScene))
return;
int numberOfReadyPlayers = NetworkServer.connections.Count(conn =>
conn.Value != null &&
conn.Value.identity != null &&
conn.Value.identity.TryGetComponent(out NetworkRoomPlayer nrp) &&
nrp.readyToBegin);
bool enoughReadyPlayers = minPlayers <= 0 || numberOfReadyPlayers >= minPlayers;
if (enoughReadyPlayers)
{
pendingPlayers.Clear();
allPlayersReady = true;
}
else
allPlayersReady = false;
}
#region server handlers
/// <summary>
/// Called on the server when a new client connects.
/// <para>Unity calls this on the Server when a Client connects to the Server. Use an override to tell the NetworkManager what to do when a client connects to the server.</para>
/// </summary>
/// <param name="conn">Connection from client.</param>
public override void OnServerConnect(NetworkConnectionToClient conn)
{
// cannot join game in progress
if (!Utils.IsSceneActive(RoomScene))
{
Debug.Log($"Not in Room scene...disconnecting {conn}");
conn.Disconnect();
return;
}
base.OnServerConnect(conn);
OnRoomServerConnect(conn);
}
/// <summary>
/// Called on the server when a client disconnects.
/// <para>This is called on the Server when a Client disconnects from the Server. Use an override to decide what should happen when a disconnection is detected.</para>
/// </summary>
/// <param name="conn">Connection from client.</param>
public override void OnServerDisconnect(NetworkConnectionToClient conn)
{
if (conn.identity != null)
{
NetworkRoomPlayer roomPlayer = conn.identity.GetComponent<NetworkRoomPlayer>();
if (roomPlayer != null)
roomSlots.Remove(roomPlayer);
foreach (NetworkIdentity clientOwnedObject in conn.owned)
{
roomPlayer = clientOwnedObject.GetComponent<NetworkRoomPlayer>();
if (roomPlayer != null)
roomSlots.Remove(roomPlayer);
}
}
allPlayersReady = false;
foreach (NetworkRoomPlayer player in roomSlots)
{
if (player != null)
player.GetComponent<NetworkRoomPlayer>().readyToBegin = false;
}
if (Utils.IsSceneActive(RoomScene))
RecalculateRoomPlayerIndices();
OnRoomServerDisconnect(conn);
base.OnServerDisconnect(conn);
// Restart the server if we're headless and no players are connected.
// This will send server to offline scene, where auto-start will run.
if (Utils.IsHeadless() && numPlayers < 1)
StopServer();
}
// 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>
/// </summary>
/// <param name="conn">Connection from client.</param>
public override void OnServerAddPlayer(NetworkConnectionToClient conn)
{
// increment the index before adding the player, so first player starts at 1
clientIndex++;
if (Utils.IsSceneActive(RoomScene))
{
allPlayersReady = false;
//Debug.Log("NetworkRoomManager.OnServerAddPlayer playerPrefab: {roomPlayerPrefab.name}");
GameObject newRoomGameObject = OnRoomServerCreateRoomPlayer(conn);
if (newRoomGameObject == null)
newRoomGameObject = Instantiate(roomPlayerPrefab.gameObject, Vector3.zero, Quaternion.identity);
NetworkServer.AddPlayerForConnection(conn, newRoomGameObject);
}
else
{
// Late joiners not supported...should've been kicked by OnServerDisconnect
Debug.Log($"Not in Room scene...disconnecting {conn}");
conn.Disconnect();
}
}
[Server]
public void RecalculateRoomPlayerIndices()
{
if (roomSlots.Count > 0)
{
int i = 0;
foreach (NetworkRoomPlayer player in roomSlots)
player.index = i++;
}
}
/// <summary>
/// This causes the server to switch scenes and sets the networkSceneName.
/// <para>Clients that connect to this server will automatically switch to this scene. This is called automatically if onlineScene or offlineScene are set, but it can be called from user code to switch scenes again while the game is in progress. This automatically sets clients to be not-ready. The clients must call NetworkClient.Ready() again to participate in the new scene.</para>
/// </summary>
/// <param name="newSceneName"></param>
public override void ServerChangeScene(string newSceneName)
{
if (newSceneName == RoomScene)
{
foreach (NetworkRoomPlayer roomPlayer in roomSlots)
{
if (roomPlayer == null)
continue;
// find the game-player object for this connection, and destroy it
NetworkIdentity identity = roomPlayer.GetComponent<NetworkIdentity>();
if (NetworkServer.active)
{
// re-add the room object
roomPlayer.GetComponent<NetworkRoomPlayer>().readyToBegin = false;
NetworkServer.ReplacePlayerForConnection(identity.connectionToClient, roomPlayer.gameObject);
}
}
allPlayersReady = false;
}
base.ServerChangeScene(newSceneName);
}
/// <summary>
/// Called on the server when a scene is completed loaded, when the scene load was initiated by the server with ServerChangeScene().
/// </summary>
/// <param name="sceneName">The name of the new scene.</param>
public override void OnServerSceneChanged(string sceneName)
{
if (sceneName != RoomScene)
{
// call SceneLoadedForPlayer on any players that become ready while we were loading the scene.
foreach (PendingPlayer pending in pendingPlayers)
SceneLoadedForPlayer(pending.conn, pending.roomPlayer);
pendingPlayers.Clear();
}
OnRoomServerSceneChanged(sceneName);
}
/// <summary>
/// This is invoked when a server is started - including when a host is started.
/// <para>StartServer has multiple signatures, but they all cause this hook to be called.</para>
/// </summary>
public override void OnStartServer()
{
if (string.IsNullOrWhiteSpace(RoomScene))
{
Debug.LogError("NetworkRoomManager RoomScene is empty. Set the RoomScene in the inspector for the NetworkRoomManager");
return;
}
if (string.IsNullOrWhiteSpace(GameplayScene))
{
Debug.LogError("NetworkRoomManager PlayScene is empty. Set the PlayScene in the inspector for the NetworkRoomManager");
return;
}
OnRoomStartServer();
}
/// <summary>
/// This is invoked when a host is started.
/// <para>StartHost has multiple signatures, but they all cause this hook to be called.</para>
/// </summary>
public override void OnStartHost()
{
OnRoomStartHost();
}
/// <summary>
/// This is called when a server is stopped - including when a host is stopped.
/// </summary>
public override void OnStopServer()
{
roomSlots.Clear();
OnRoomStopServer();
}
/// <summary>
/// This is called when a host is stopped.
/// </summary>
public override void OnStopHost()
{
OnRoomStopHost();
}
#endregion
#region client handlers
/// <summary>
/// This is invoked when the client is started.
/// </summary>
public override void OnStartClient()
{
if (roomPlayerPrefab == null || roomPlayerPrefab.gameObject == null)
Debug.LogError("NetworkRoomManager no RoomPlayer prefab is registered. Please add a RoomPlayer prefab.");
else
NetworkClient.RegisterPrefab(roomPlayerPrefab.gameObject);
if (playerPrefab == null)
Debug.LogError("NetworkRoomManager no GamePlayer prefab is registered. Please add a GamePlayer prefab.");
OnRoomStartClient();
}
/// <summary>
/// Called on the client when connected to a server.
/// <para>The default implementation of this function sets the client as ready and adds a player. Override the function to dictate what happens when the client connects.</para>
/// </summary>
public override void OnClientConnect()
{
OnRoomClientConnect();
base.OnClientConnect();
}
/// <summary>
/// Called on clients when disconnected from a server.
/// <para>This is called on the client when it disconnects from the server. Override this function to decide what happens when the client disconnects.</para>
/// </summary>
public override void OnClientDisconnect()
{
OnRoomClientDisconnect();
base.OnClientDisconnect();
}
/// <summary>
/// This is called when a client is stopped.
/// </summary>
public override void OnStopClient()
{
OnRoomStopClient();
CallOnClientExitRoom();
roomSlots.Clear();
}
/// <summary>
/// Called on clients when a scene has completed loaded, when the scene load was initiated by the server.
/// <para>Scene changes can cause player objects to be destroyed. The default implementation of OnClientSceneChanged in the NetworkManager is to add a player object for the connection if no player object exists.</para>
/// </summary>
public override void OnClientSceneChanged()
{
if (Utils.IsSceneActive(RoomScene))
{
if (NetworkClient.isConnected)
CallOnClientEnterRoom();
}
else
CallOnClientExitRoom();
base.OnClientSceneChanged();
OnRoomClientSceneChanged();
}
#endregion
#region room server virtuals
/// <summary>
/// This is called on the host when a host is started.
/// </summary>
public virtual void OnRoomStartHost() {}
/// <summary>
/// This is called on the host when the host is stopped.
/// </summary>
public virtual void OnRoomStopHost() {}
/// <summary>
/// This is called on the server when the server is started - including when a host is started.
/// </summary>
public virtual void OnRoomStartServer() {}
/// <summary>
/// This is called on the server when the server is started - including when a host is stopped.
/// </summary>
public virtual void OnRoomStopServer() {}
/// <summary>
/// This is called on the server when a new client connects to the server.
/// </summary>
/// <param name="conn">The new connection.</param>
public virtual void OnRoomServerConnect(NetworkConnectionToClient conn) {}
/// <summary>
/// This is called on the server when a client disconnects.
/// </summary>
/// <param name="conn">The connection that disconnected.</param>
public virtual void OnRoomServerDisconnect(NetworkConnectionToClient conn) {}
/// <summary>
/// This is called on the server when a networked scene finishes loading.
/// </summary>
/// <param name="sceneName">Name of the new scene.</param>
public virtual void OnRoomServerSceneChanged(string sceneName) {}
/// <summary>
/// This allows customization of the creation of the room-player object on the server.
/// <para>By default the roomPlayerPrefab is used to create the room-player, but this function allows that behaviour to be customized.</para>
/// </summary>
/// <param name="conn">The connection the player object is for.</param>
/// <returns>The new room-player object.</returns>
public virtual GameObject OnRoomServerCreateRoomPlayer(NetworkConnectionToClient conn)
{
return null;
}
/// <summary>
/// This allows customization of the creation of the GamePlayer object on the server.
/// <para>By default the gamePlayerPrefab is used to create the game-player, but this function allows that behaviour to be customized. The object returned from the function will be used to replace the room-player on the connection.</para>
/// </summary>
/// <param name="conn">The connection the player object is for.</param>
/// <param name="roomPlayer">The room player object for this connection.</param>
/// <returns>A new GamePlayer object.</returns>
public virtual GameObject OnRoomServerCreateGamePlayer(NetworkConnectionToClient conn, GameObject roomPlayer)
{
return null;
}
/// <summary>
/// This allows customization of the creation of the GamePlayer object on the server.
/// <para>This is only called for subsequent GamePlay scenes after the first one.</para>
/// <para>See <see cref="OnRoomServerCreateGamePlayer(NetworkConnectionToClient, GameObject)">OnRoomServerCreateGamePlayer(NetworkConnection, GameObject)</see> to customize the player object for the initial GamePlay scene.</para>
/// </summary>
/// <param name="conn">The connection the player object is for.</param>
public virtual void OnRoomServerAddPlayer(NetworkConnectionToClient conn)
{
base.OnServerAddPlayer(conn);
}
// for users to apply settings from their room player object to their in-game player object
/// <summary>
/// This is called on the server when it is told that a client has finished switching from the room scene to a game player scene.
/// <para>When switching from the room, the room-player is replaced with a game-player object. This callback function gives an opportunity to apply state from the room-player to the game-player object.</para>
/// </summary>
/// <param name="conn">The connection of the player</param>
/// <param name="roomPlayer">The room player object.</param>
/// <param name="gamePlayer">The game player object.</param>
/// <returns>False to not allow this player to replace the room player.</returns>
public virtual bool OnRoomServerSceneLoadedForPlayer(NetworkConnectionToClient conn, GameObject roomPlayer, GameObject gamePlayer)
{
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>
/// </summary>
public virtual void OnRoomServerPlayersReady()
{
// all players are readyToBegin, start the game
ServerChangeScene(GameplayScene);
}
/// <summary>
/// This is called on the server when CheckReadyToBegin finds that players are not ready
/// <para>May be called multiple times while not ready players are joining</para>
/// </summary>
public virtual void OnRoomServerPlayersNotReady() {}
#endregion
#region room client virtuals
/// <summary>
/// This is a hook to allow custom behaviour when the game client enters the room.
/// </summary>
public virtual void OnRoomClientEnter() {}
/// <summary>
/// This is a hook to allow custom behaviour when the game client exits the room.
/// </summary>
public virtual void OnRoomClientExit() {}
/// <summary>
/// This is called on the client when it connects to server.
/// </summary>
public virtual void OnRoomClientConnect() {}
/// <summary>
/// This is called on the client when disconnected from a server.
/// </summary>
public virtual void OnRoomClientDisconnect() {}
/// <summary>
/// This is called on the client when a client is started.
/// </summary>
public virtual void OnRoomStartClient() {}
/// <summary>
/// This is called on the client when the client stops.
/// </summary>
public virtual void OnRoomStopClient() {}
/// <summary>
/// This is called on the client when the client is finished loading a new networked scene.
/// </summary>
public virtual void OnRoomClientSceneChanged() {}
#endregion
#region optional UI
/// <summary>
/// virtual so inheriting classes can roll their own
/// </summary>
public virtual void OnGUI()
{
if (!showRoomGUI)
return;
if (NetworkServer.active && Utils.IsSceneActive(GameplayScene))
{
GUILayout.BeginArea(new Rect(Screen.width - 150f, 10f, 140f, 30f));
if (GUILayout.Button("Return to Room"))
ServerChangeScene(RoomScene);
GUILayout.EndArea();
}
if (Utils.IsSceneActive(RoomScene))
GUI.Box(new Rect(10f, 180f, 520f, 150f), "PLAYERS");
}
#endregion
}
}
fileFormatVersion: 2
guid: 615e6c6589cf9e54cad646b5a11e0529
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:
using UnityEngine;
namespace Mirror
{
/// <summary>
/// This component works in conjunction with the NetworkRoomManager to make up the multiplayer room system.
/// <para>The RoomPrefab object of the NetworkRoomManager must have this component on it. This component holds basic room player data required for the room to function. Game specific data for room players can be put in other components on the RoomPrefab or in scripts derived from NetworkRoomPlayer.</para>
/// </summary>
[DisallowMultipleComponent]
[AddComponentMenu("Network/Network Room Player")]
[HelpURL("https://mirror-networking.gitbook.io/docs/components/network-room-player")]
public class NetworkRoomPlayer : NetworkBehaviour
{
/// <summary>
/// This flag controls whether the default UI is shown for the room player.
/// <para>As this UI is rendered using the old GUI system, it is only recommended for testing purposes.</para>
/// </summary>
[Tooltip("This flag controls whether the default UI is shown for the room player")]
public bool showRoomGUI = true;
[Header("Diagnostics")]
/// <summary>
/// Diagnostic flag indicating whether this player is ready for the game to begin.
/// <para>Invoke CmdChangeReadyState method on the client to set this flag.</para>
/// <para>When all players are ready to begin, the game will start. This should not be set directly, CmdChangeReadyState should be called on the client to set it on the server.</para>
/// </summary>
[ReadOnly, Tooltip("Diagnostic flag indicating whether this player is ready for the game to begin")]
[SyncVar(hook = nameof(ReadyStateChanged))]
public bool readyToBegin;
/// <summary>
/// Diagnostic index of the player, e.g. Player1, Player2, etc.
/// </summary>
[ReadOnly, Tooltip("Diagnostic index of the player, e.g. Player1, Player2, etc.")]
[SyncVar(hook = nameof(IndexChanged))]
public int index;
#region Unity Callbacks
/// <summary>
/// Do not use Start - Override OnStartHost / OnStartClient instead!
/// </summary>
public virtual void Start()
{
if (NetworkManager.singleton is NetworkRoomManager room)
{
// NetworkRoomPlayer object must be set to DontDestroyOnLoad along with NetworkRoomManager
// in server and all clients, otherwise it will be respawned in the game scene which would
// have undesirable effects.
if (room.dontDestroyOnLoad)
DontDestroyOnLoad(gameObject);
room.roomSlots.Add(this);
if (NetworkServer.active)
room.RecalculateRoomPlayerIndices();
if (NetworkClient.active)
room.CallOnClientEnterRoom();
}
else Debug.LogError("RoomPlayer could not find a NetworkRoomManager. The RoomPlayer requires a NetworkRoomManager object to function. Make sure that there is one in the scene.");
}
public virtual void OnDisable()
{
if (NetworkClient.active && NetworkManager.singleton is NetworkRoomManager room)
{
// only need to call this on client as server removes it before object is destroyed
room.roomSlots.Remove(this);
room.CallOnClientExitRoom();
}
}
#endregion
#region Commands
[Command]
public void CmdChangeReadyState(bool readyState)
{
readyToBegin = readyState;
NetworkRoomManager room = NetworkManager.singleton as NetworkRoomManager;
if (room != null)
{
room.ReadyStatusChanged();
}
}
#endregion
#region SyncVar Hooks
/// <summary>
/// This is a hook that is invoked on clients when the index changes.
/// </summary>
/// <param name="oldIndex">The old index value</param>
/// <param name="newIndex">The new index value</param>
public virtual void IndexChanged(int oldIndex, int newIndex) {}
/// <summary>
/// This is a hook that is invoked on clients when a RoomPlayer switches between ready or not ready.
/// <para>This function is called when the a client player calls CmdChangeReadyState.</para>
/// </summary>
/// <param name="newReadyState">New Ready State</param>
public virtual void ReadyStateChanged(bool oldReadyState, bool newReadyState) {}
#endregion
#region Room Client Virtuals
/// <summary>
/// This is a hook that is invoked on clients for all room player objects when entering the room.
/// <para>Note: isLocalPlayer is not guaranteed to be set until OnStartLocalPlayer is called.</para>
/// </summary>
public virtual void OnClientEnterRoom() {}
/// <summary>
/// This is a hook that is invoked on clients for all room player objects when exiting the room.
/// </summary>
public virtual void OnClientExitRoom() {}
#endregion
#region Optional UI
/// <summary>
/// Render a UI for the room. Override to provide your own UI
/// </summary>
public virtual void OnGUI()
{
if (!showRoomGUI)
return;
NetworkRoomManager room = NetworkManager.singleton as NetworkRoomManager;
if (room)
{
if (!room.showRoomGUI)
return;
if (!Utils.IsSceneActive(room.RoomScene))
return;
DrawPlayerReadyState();
DrawPlayerReadyButton();
}
}
void DrawPlayerReadyState()
{
GUILayout.BeginArea(new Rect(20f + (index * 100), 200f, 90f, 130f));
GUILayout.Label($"Player [{index + 1}]");
if (readyToBegin)
GUILayout.Label("Ready");
else
GUILayout.Label("Not Ready");
if (((isServer && index > 0) || isServerOnly) && GUILayout.Button("REMOVE"))
{
// This button only shows on the Host for all players other than the Host
// Host and Players can't remove themselves (stop the client instead)
// Host can kick a Player this way.
GetComponent<NetworkIdentity>().connectionToClient.Disconnect();
}
GUILayout.EndArea();
}
void DrawPlayerReadyButton()
{
if (NetworkClient.active && isLocalPlayer)
{
GUILayout.BeginArea(new Rect(20f, 300f, 120f, 20f));
if (readyToBegin)
{
if (GUILayout.Button("Cancel"))
CmdChangeReadyState(false);
}
else
{
if (GUILayout.Button("Ready"))
CmdChangeReadyState(true);
}
GUILayout.EndArea();
}
}
#endregion
}
}
fileFormatVersion: 2
guid: 79874ac94d5b1314788ecf0e86bd23fd
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:
using System;
using UnityEngine;
namespace Mirror
{
/// <summary>
/// Shows Network messages and bytes sent and received per second.
/// </summary>
/// <remarks>
/// <para>Add this component to the same object as Network Manager.</para>
/// </remarks>
[AddComponentMenu("Network/Network Statistics")]
[DisallowMultipleComponent]
[HelpURL("https://mirror-networking.gitbook.io/docs/components/network-statistics")]
public class NetworkStatistics : MonoBehaviour
{
// update interval
double intervalStartTime;
// ---------------------------------------------------------------------
// CLIENT (public fields for other components to grab statistics)
// long bytes to support >2GB
[HideInInspector] public int clientIntervalReceivedPackets;
[HideInInspector] public long clientIntervalReceivedBytes;
[HideInInspector] public int clientIntervalSentPackets;
[HideInInspector] public long clientIntervalSentBytes;
// results from last interval
// long bytes to support >2GB
[HideInInspector] public int clientReceivedPacketsPerSecond;
[HideInInspector] public long clientReceivedBytesPerSecond;
[HideInInspector] public int clientSentPacketsPerSecond;
[HideInInspector] public long clientSentBytesPerSecond;
// ---------------------------------------------------------------------
// SERVER (public fields for other components to grab statistics)
// capture interval
// long bytes to support >2GB
[HideInInspector] public int serverIntervalReceivedPackets;
[HideInInspector] public long serverIntervalReceivedBytes;
[HideInInspector] public int serverIntervalSentPackets;
[HideInInspector] public long serverIntervalSentBytes;
// results from last interval
// long bytes to support >2GB
[HideInInspector] public int serverReceivedPacketsPerSecond;
[HideInInspector] public long serverReceivedBytesPerSecond;
[HideInInspector] public int serverSentPacketsPerSecond;
[HideInInspector] public long serverSentBytesPerSecond;
// NetworkManager sets Transport.active in Awake().
// so let's hook into it in Start().
void Start()
{
// find available transport
Transport transport = Transport.active;
if (transport != null)
{
transport.OnClientDataReceived += OnClientReceive;
transport.OnClientDataSent += OnClientSend;
transport.OnServerDataReceived += OnServerReceive;
transport.OnServerDataSent += OnServerSend;
}
else Debug.LogError($"NetworkStatistics: no available or active Transport found on this platform: {Application.platform}");
}
void OnDestroy()
{
// remove transport hooks
Transport transport = Transport.active;
if (transport != null)
{
transport.OnClientDataReceived -= OnClientReceive;
transport.OnClientDataSent -= OnClientSend;
transport.OnServerDataReceived -= OnServerReceive;
transport.OnServerDataSent -= OnServerSend;
}
}
void OnClientReceive(ArraySegment<byte> data, int channelId)
{
++clientIntervalReceivedPackets;
clientIntervalReceivedBytes += data.Count;
}
void OnClientSend(ArraySegment<byte> data, int channelId)
{
++clientIntervalSentPackets;
clientIntervalSentBytes += data.Count;
}
void OnServerReceive(int connectionId, ArraySegment<byte> data, int channelId)
{
++serverIntervalReceivedPackets;
serverIntervalReceivedBytes += data.Count;
}
void OnServerSend(int connectionId, ArraySegment<byte> data, int channelId)
{
++serverIntervalSentPackets;
serverIntervalSentBytes += data.Count;
}
void Update()
{
// calculate results every second
if (NetworkTime.localTime >= intervalStartTime + 1)
{
if (NetworkClient.active) UpdateClient();
if (NetworkServer.active) UpdateServer();
intervalStartTime = NetworkTime.localTime;
}
}
void UpdateClient()
{
clientReceivedPacketsPerSecond = clientIntervalReceivedPackets;
clientReceivedBytesPerSecond = clientIntervalReceivedBytes;
clientSentPacketsPerSecond = clientIntervalSentPackets;
clientSentBytesPerSecond = clientIntervalSentBytes;
clientIntervalReceivedPackets = 0;
clientIntervalReceivedBytes = 0;
clientIntervalSentPackets = 0;
clientIntervalSentBytes = 0;
}
void UpdateServer()
{
serverReceivedPacketsPerSecond = serverIntervalReceivedPackets;
serverReceivedBytesPerSecond = serverIntervalReceivedBytes;
serverSentPacketsPerSecond = serverIntervalSentPackets;
serverSentBytesPerSecond = serverIntervalSentBytes;
serverIntervalReceivedPackets = 0;
serverIntervalReceivedBytes = 0;
serverIntervalSentPackets = 0;
serverIntervalSentBytes = 0;
}
void OnGUI()
{
// only show if either server or client active
if (NetworkClient.active || NetworkServer.active)
{
// create main GUI area
// 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();
if (NetworkServer.active) OnServerGUI();
// end of GUI area
GUILayout.EndArea();
}
}
void OnClientGUI()
{
// background
GUILayout.BeginVertical("Box");
GUILayout.Label("<b>Client Statistics</b>");
// sending ("msgs" instead of "packets" to fit larger numbers)
GUILayout.Label($"Send: {clientSentPacketsPerSecond} msgs @ {Utils.PrettyBytes(clientSentBytesPerSecond)}/s");
// receiving ("msgs" instead of "packets" to fit larger numbers)
GUILayout.Label($"Recv: {clientReceivedPacketsPerSecond} msgs @ {Utils.PrettyBytes(clientReceivedBytesPerSecond)}/s");
// end background
GUILayout.EndVertical();
}
void OnServerGUI()
{
// background
GUILayout.BeginVertical("Box");
GUILayout.Label("<b>Server Statistics</b>");
// sending ("msgs" instead of "packets" to fit larger numbers)
GUILayout.Label($"Send: {serverSentPacketsPerSecond} msgs @ {Utils.PrettyBytes(serverSentBytesPerSecond)}/s");
// receiving ("msgs" instead of "packets" to fit larger numbers)
GUILayout.Label($"Recv: {serverReceivedPacketsPerSecond} msgs @ {Utils.PrettyBytes(serverReceivedBytesPerSecond)}/s");
// end background
GUILayout.EndVertical();
}
}
}
fileFormatVersion: 2
guid: 6d7da4e566d24ea7b0e12178d934b648
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:
fileFormatVersion: 2
guid: 36de72d9255741659bcbd1971ed29822
timeCreated: 1668358590
\ No newline at end of file
using System;
using UnityEngine;
namespace Mirror
{
// DEPRECATED 2023-06-15
[AddComponentMenu("")]
[Obsolete("NetworkTransform was renamed to NetworkTransformUnreliable.\nYou can easily swap the component's script by going into the Unity Inspector debug mode:\n1. Click the vertical dots on the top right in the Inspector tab.\n2. Find your NetworkTransform component\n3. Drag NetworkTransformUnreliable into the 'Script' field in the Inspector.\n4. Find the three dots and return to Normal mode.")]
public class NetworkTransform : NetworkTransformUnreliable {}
}
fileFormatVersion: 2
guid: 2f74aedd71d9a4f55b3ce499326d45fb
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:
// Snapshot Interpolation: https://gafferongames.com/post/snapshot_interpolation/
//
// Base class for NetworkTransform and NetworkTransformChild.
// => simple unreliable sync without any interpolation for now.
// => which means we don't need teleport detection either
//
// NOTE: several functions are virtual in case someone needs to modify a part.
//
// Channel: uses UNRELIABLE at all times.
// -> out of order packets are dropped automatically
// -> it's better than RELIABLE for several reasons:
// * head of line blocking would add delay
// * resending is mostly pointless
// * bigger data race:
// -> if we use a Cmd() at position X over reliable
// -> client gets Cmd() and X at the same time, but buffers X for bufferTime
// -> for unreliable, it would get X before the reliable Cmd(), still
// buffer for bufferTime but end up closer to the original time
using System;
using System.Collections.Generic;
using UnityEngine;
namespace Mirror
{
public enum CoordinateSpace { Local, World }
public abstract class NetworkTransformBase : NetworkBehaviour
{
// target transform to sync. can be on a child.
// TODO this field is kind of unnecessary since we now support child NetworkBehaviours
[Header("Target")]
[Tooltip("The Transform component to sync. May be on on this GameObject, or on a child.")]
public Transform target;
// Is this a client with authority over this transform?
// This component could be on the player object or any object that has been assigned authority to this client.
protected bool IsClientWithAuthority => isClient && authority;
// snapshots with initial capacity to avoid early resizing & allocations: see NetworkRigidbodyBenchmark example.
public readonly SortedList<double, TransformSnapshot> clientSnapshots = new SortedList<double, TransformSnapshot>(16);
public readonly SortedList<double, TransformSnapshot> serverSnapshots = new SortedList<double, TransformSnapshot>(16);
// selective sync //////////////////////////////////////////////////////
[Header("Selective Sync\nDon't change these at Runtime")]
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.
[Header("Bandwidth Savings")]
[Tooltip("When true, changes are not sent unless greater than sensitivity values below.")]
public bool onlySyncOnChange = true;
[Tooltip("Apply smallest-three quaternion compression. This is lossy, you can disable it if the small rotation inaccuracies are noticeable in your project.")]
public bool compressRotation = true;
// interpolation is on by default, but can be disabled to jump to
// the destination immediately. some projects need this.
[Header("Interpolation")]
[Tooltip("Set to false to have a snap-like effect on position movement.")]
public bool interpolatePosition = true;
[Tooltip("Set to false to have a snap-like effect on rotations.")]
public bool interpolateRotation = true;
[Tooltip("Set to false to remove scale smoothing. Example use-case: Instant flipping of sprites that use -X and +X for direction.")]
public bool interpolateScale = true;
// CoordinateSpace ///////////////////////////////////////////////////////////
[Header("Coordinate Space")]
[Tooltip("Local by default. World may be better when changing hierarchy, or non-NetworkTransforms root position/rotation/scale values.")]
public CoordinateSpace coordinateSpace = CoordinateSpace.Local;
[Header("Send Interval Multiplier")]
[Tooltip("Check/Sync every multiple of Network Manager send interval (= 1 / NM Send Rate), instead of every send interval.\n(30 NM send rate, and 3 interval, is a send every 0.1 seconds)\nA larger interval means less network sends, which has a variety of upsides. The drawbacks are delays and lower accuracy, you should find a nice balance between not sending too much, but the results looking good for your particular scenario.")]
[Range(1, 120)]
public uint sendIntervalMultiplier = 1;
[Header("Timeline Offset")]
[Tooltip("Add a small timeline offset to account for decoupled arrival of NetworkTime and NetworkTransform snapshots.\nfixes: https://github.com/MirrorNetworking/Mirror/issues/3427")]
public bool timelineOffset = false;
// Ninja's Notes on offset & mulitplier:
//
// In a no multiplier scenario:
// 1. Snapshots are sent every frame (frame being 1 NM send interval).
// 2. Time Interpolation is set to be 'behind' by 2 frames times.
// In theory where everything works, we probably have around 2 snapshots before we need to interpolate snapshots. From NT perspective, we should always have around 2 snapshots ready, so no stutter.
//
// In a multiplier scenario:
// 1. Snapshots are sent every 10 frames.
// 2. Time Interpolation remains 'behind by 2 frames'.
// When everything works, we are receiving NT snapshots every 10 frames, but start interpolating after 2.
// Even if I assume we had 2 snapshots to begin with to start interpolating (which we don't), by the time we reach 13th frame, we are out of snapshots, and have to wait 7 frames for next snapshot to come. This is the reason why we absolutely need the timestamp adjustment. We are starting way too early to interpolate.
//
protected double timeStampAdjustment => NetworkServer.sendInterval * (sendIntervalMultiplier - 1);
protected double offset => timelineOffset ? NetworkServer.sendInterval * sendIntervalMultiplier : 0;
// debugging ///////////////////////////////////////////////////////////
[Header("Debug")]
public bool showGizmos;
public bool showOverlay;
public Color overlayColor = new Color(0, 0, 0, 0.5f);
// initialization //////////////////////////////////////////////////////
// forcec configuration of some settings
protected virtual void Configure()
{
// set target to self if none yet
if (target == null) target = transform;
// time snapshot interpolation happens globally.
// value (transform) happens in here.
// both always need to be on the same send interval.
// force the setting to '0' in OnValidate to make it obvious that we
// actually use NetworkServer.sendInterval.
syncInterval = 0;
// Unity doesn't support setting world scale.
// OnValidate force disables syncScale in world mode.
if (coordinateSpace == CoordinateSpace.World) syncScale = false;
}
// make sure to call this when inheriting too!
protected virtual void Awake()
{
// sometimes OnValidate() doesn't run before launching a project.
// need to guarantee configuration runs.
Configure();
}
protected override void OnValidate()
{
base.OnValidate();
// configure in awake
Configure();
}
// snapshot functions //////////////////////////////////////////////////
// get local/world position
protected virtual Vector3 GetPosition() =>
coordinateSpace == CoordinateSpace.Local ? target.localPosition : target.position;
// get local/world rotation
protected virtual Quaternion GetRotation() =>
coordinateSpace == CoordinateSpace.Local ? target.localRotation : target.rotation;
// get local/world scale
protected virtual Vector3 GetScale() =>
coordinateSpace == CoordinateSpace.Local ? target.localScale : target.lossyScale;
// set local/world position
protected virtual void SetPosition(Vector3 position)
{
if (coordinateSpace == CoordinateSpace.Local)
target.localPosition = position;
else
target.position = position;
}
// set local/world rotation
protected virtual void SetRotation(Quaternion rotation)
{
if (coordinateSpace == CoordinateSpace.Local)
target.localRotation = rotation;
else
target.rotation = rotation;
}
// set local/world position
protected virtual void SetScale(Vector3 scale)
{
if (coordinateSpace == CoordinateSpace.Local)
target.localScale = scale;
// Unity doesn't support setting world scale.
// OnValidate disables syncScale in world mode.
// else
// target.lossyScale = scale; // TODO
}
// construct a snapshot of the current state
// => internal for testing
protected virtual TransformSnapshot Construct()
{
// 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
NetworkTime.localTime, // Unity 2019 doesn't have timeAsDouble yet
0, // the other end fills out local time itself
GetPosition(),
GetRotation(),
GetScale()
);
}
protected void AddSnapshot(SortedList<double, TransformSnapshot> snapshots, double timeStamp, Vector3? position, Quaternion? rotation, Vector3? scale)
{
// position, rotation, scale can have no value if same as last time.
// saves bandwidth.
// but we still need to feed it to snapshot interpolation. we can't
// just have gaps in there if nothing has changed. for example, if
// client sends snapshot at t=0
// client sends nothing for 10s because not moved
// client sends snapshot at t=10
// then the server would assume that it's one super slow move and
// replay it for 10 seconds.
if (!position.HasValue) position = snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].position : GetPosition();
if (!rotation.HasValue) rotation = snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].rotation : GetRotation();
if (!scale.HasValue) scale = snapshots.Count > 0 ? snapshots.Values[snapshots.Count - 1].scale : GetScale();
// insert transform snapshot
SnapshotInterpolation.InsertIfNotExists(
snapshots,
NetworkClient.snapshotSettings.bufferLimit,
new TransformSnapshot(
timeStamp, // arrival remote timestamp. NOT remote time.
NetworkTime.localTime, // Unity 2019 doesn't have timeAsDouble yet
position.Value,
rotation.Value,
scale.Value
)
);
}
// apply a snapshot to the Transform.
// -> start, end, interpolated are all passed in caes they are needed
// -> a regular game would apply the 'interpolated' snapshot
// -> a board game might want to jump to 'goal' directly
// (it's easier to always interpolate and then apply selectively,
// instead of manually interpolating x, y, z, ... depending on flags)
// => internal for testing
//
// NOTE: stuck detection is unnecessary here.
// we always set transform.position anyway, we can't get stuck.
protected virtual void Apply(TransformSnapshot interpolated, TransformSnapshot endGoal)
{
// local position/rotation for VR support
//
// if syncPosition/Rotation/Scale is disabled then we received nulls
// -> current position/rotation/scale would've been added as snapshot
// -> we still interpolated
// -> but simply don't apply it. if the user doesn't want to sync
// scale, then we should not touch scale etc.
// interpolate parts
if (syncPosition) SetPosition(interpolatePosition ? interpolated.position : endGoal.position);
if (syncRotation) SetRotation(interpolateRotation ? interpolated.rotation : endGoal.rotation);
if (syncScale) SetScale(interpolateScale ? interpolated.scale : endGoal.scale);
}
// client->server teleport to force position without interpolation.
// otherwise it would interpolate to a (far away) new position.
// => manually calling Teleport is the only 100% reliable solution.
[Command]
public void CmdTeleport(Vector3 destination)
{
// client can only teleport objects that it has authority over.
if (syncDirection != SyncDirection.ClientToServer) return;
// TODO what about host mode?
OnTeleport(destination);
// if a client teleports, we need to broadcast to everyone else too
// TODO the teleported client should ignore the rpc though.
// otherwise if it already moved again after teleporting,
// the rpc would come a little bit later and reset it once.
// TODO or not? if client ONLY calls Teleport(pos), the position
// would only be set after the rpc. unless the client calls
// BOTH Teleport(pos) and target.position=pos
RpcTeleport(destination);
}
// client->server teleport to force position and rotation without interpolation.
// otherwise it would interpolate to a (far away) new position.
// => manually calling Teleport is the only 100% reliable solution.
[Command]
public void CmdTeleport(Vector3 destination, Quaternion rotation)
{
// client can only teleport objects that it has authority over.
if (syncDirection != SyncDirection.ClientToServer) return;
// TODO what about host mode?
OnTeleport(destination, rotation);
// if a client teleports, we need to broadcast to everyone else too
// TODO the teleported client should ignore the rpc though.
// otherwise if it already moved again after teleporting,
// the rpc would come a little bit later and reset it once.
// TODO or not? if client ONLY calls Teleport(pos), the position
// would only be set after the rpc. unless the client calls
// BOTH Teleport(pos) and target.position=pos
RpcTeleport(destination, rotation);
}
// server->client teleport to force position without interpolation.
// otherwise it would interpolate to a (far away) new position.
// => manually calling Teleport is the only 100% reliable solution.
[ClientRpc]
public void RpcTeleport(Vector3 destination)
{
// NOTE: even in client authority mode, the server is always allowed
// to teleport the player. for example:
// * CmdEnterPortal() might teleport the player
// * Some people use client authority with server sided checks
// so the server should be able to reset position if needed.
// TODO what about host mode?
OnTeleport(destination);
}
// server->client teleport to force position and rotation without interpolation.
// otherwise it would interpolate to a (far away) new position.
// => manually calling Teleport is the only 100% reliable solution.
[ClientRpc]
public void RpcTeleport(Vector3 destination, Quaternion rotation)
{
// NOTE: even in client authority mode, the server is always allowed
// to teleport the player. for example:
// * CmdEnterPortal() might teleport the player
// * Some people use client authority with server sided checks
// so the server should be able to reset position if needed.
// TODO what about host mode?
OnTeleport(destination, rotation);
}
[ClientRpc]
void RpcResetState()
{
ResetState();
}
// common Teleport code for client->server and server->client
protected virtual void OnTeleport(Vector3 destination)
{
// set the new position.
// interpolation will automatically continue.
target.position = destination;
// reset interpolation to immediately jump to the new position.
// do not call Reset() here, this would cause delta compression to
// get out of sync for NetworkTransformReliable because NTReliable's
// 'override Reset()' resets lastDe/SerializedPosition:
// https://github.com/MirrorNetworking/Mirror/issues/3588
// because client's next OnSerialize() will delta compress,
// but server's last delta will have been reset, causing offsets.
//
// instead, simply clear snapshots.
ResetState();
// TODO
// what if we still receive a snapshot from before the interpolation?
// it could easily happen over unreliable.
// -> maybe add destination as first entry?
}
// common Teleport code for client->server and server->client
protected virtual void OnTeleport(Vector3 destination, Quaternion rotation)
{
// set the new position.
// interpolation will automatically continue.
target.position = destination;
target.rotation = rotation;
// reset interpolation to immediately jump to the new position.
// do not call Reset() here, this would cause delta compression to
// get out of sync for NetworkTransformReliable because NTReliable's
// 'override Reset()' resets lastDe/SerializedPosition:
// https://github.com/MirrorNetworking/Mirror/issues/3588
// because client's next OnSerialize() will delta compress,
// but server's last delta will have been reset, causing offsets.
//
// instead, simply clear snapshots.
ResetState();
// TODO
// what if we still receive a snapshot from before the interpolation?
// it could easily happen over unreliable.
// -> maybe add destination as first entry?
}
public virtual void ResetState()
{
// disabled objects aren't updated anymore.
// so let's clear the buffers.
serverSnapshots.Clear();
clientSnapshots.Clear();
}
public virtual void Reset()
{
ResetState();
// default to ClientToServer so this works immediately for users
syncDirection = SyncDirection.ClientToServer;
}
protected virtual void OnEnable()
{
ResetState();
if (NetworkServer.active)
NetworkIdentity.clientAuthorityCallback += OnClientAuthorityChanged;
}
protected virtual void OnDisable()
{
ResetState();
if (NetworkServer.active)
NetworkIdentity.clientAuthorityCallback -= OnClientAuthorityChanged;
}
[ServerCallback]
void OnClientAuthorityChanged(NetworkConnectionToClient conn, NetworkIdentity identity, bool authorityState)
{
if (identity != netIdentity) return;
// If server gets authority or syncdirection is server to client,
// we don't reset buffers.
// This is because if syncdirection is S to C, we will never have
// snapshot issues since there is only ever 1 source.
if (syncDirection == SyncDirection.ClientToServer)
{
ResetState();
RpcResetState();
}
}
// OnGUI allocates even if it does nothing. avoid in release.
#if UNITY_EDITOR || DEVELOPMENT_BUILD
// debug ///////////////////////////////////////////////////////////////
protected virtual void OnGUI()
{
if (!showOverlay) return;
if (!Camera.main) return;
// show data next to player for easier debugging. this is very useful!
// IMPORTANT: this is basically an ESP hack for shooter games.
// DO NOT make this available with a hotkey in release builds
if (!Debug.isDebugBuild) return;
// project position to screen
Vector3 point = Camera.main.WorldToScreenPoint(target.position);
// enough alpha, in front of camera and in screen?
if (point.z >= 0 && Utils.IsPointInScreen(point))
{
GUI.color = overlayColor;
GUILayout.BeginArea(new Rect(point.x, Screen.height - point.y, 200, 100));
// always show both client & server buffers so it's super
// obvious if we accidentally populate both.
GUILayout.Label($"Server Buffer:{serverSnapshots.Count}");
GUILayout.Label($"Client Buffer:{clientSnapshots.Count}");
GUILayout.EndArea();
GUI.color = Color.white;
}
}
protected virtual void DrawGizmos(SortedList<double, TransformSnapshot> buffer)
{
// only draw if we have at least two entries
if (buffer.Count < 2) return;
// calculate threshold for 'old enough' snapshots
double threshold = NetworkTime.localTime - NetworkClient.bufferTime;
Color oldEnoughColor = new Color(0, 1, 0, 0.5f);
Color notOldEnoughColor = new Color(0.5f, 0.5f, 0.5f, 0.3f);
// draw the whole buffer for easier debugging.
// it's worth seeing how much we have buffered ahead already
for (int i = 0; i < buffer.Count; ++i)
{
// color depends on if old enough or not
TransformSnapshot entry = buffer.Values[i];
bool oldEnough = entry.localTime <= threshold;
Gizmos.color = oldEnough ? oldEnoughColor : notOldEnoughColor;
Gizmos.DrawWireCube(entry.position, Vector3.one);
}
// extra: lines between start<->position<->goal
Gizmos.color = Color.green;
Gizmos.DrawLine(buffer.Values[0].position, target.position);
Gizmos.color = Color.white;
Gizmos.DrawLine(target.position, buffer.Values[1].position);
}
protected virtual void OnDrawGizmos()
{
// This fires in edit mode but that spams NRE's so check isPlaying
if (!Application.isPlaying) return;
if (!showGizmos) return;
if (isServer) DrawGizmos(serverSnapshots);
if (isClient) DrawGizmos(clientSnapshots);
}
#endif
}
}
fileFormatVersion: 2
guid: 7c44135fde488424eaf28566206ce473
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:
// A component to synchronize the position of child transforms of networked objects.
// There must be a NetworkTransform on the root object of the hierarchy. There can be multiple NetworkTransformChild components on an object. This does not use physics for synchronization, it simply synchronizes the localPosition and localRotation of the child transform and lerps towards the recieved values.
using System;
using UnityEngine;
namespace Mirror
{
// Deprecated 2022-10-25
[AddComponentMenu("")]
[Obsolete("NetworkTransformChild is not needed anymore. The .target is now exposed in NetworkTransform itself. Note you can open the Inspector in debug view and replace the source script instead of reassigning everything.")]
public class NetworkTransformChild : NetworkTransform {}
}
fileFormatVersion: 2
guid: 734b48bea0b204338958ee3d885e11f0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:
// NetworkTransform V3 (reliable) by mischa (2022-10)
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using UnityEngine;
namespace Mirror
{
[AddComponentMenu("Network/Network Transform (Reliable)")]
public class NetworkTransformReliable : NetworkTransformBase
{
uint sendIntervalCounter = 0;
double lastSendIntervalTime = double.MinValue;
[Header("Additional Settings")]
[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("Rotation")]
[Tooltip("Sensitivity of changes needed before an updated state is sent over the network")]
public float rotationSensitivity = 0.01f;
// delta compression is capable of detecting byte-level changes.
// if we scale float position to bytes,
// then small movements will only change one byte.
// this gives optimal bandwidth.
// benchmark with 0.01 precision: 130 KB/s => 60 KB/s
// benchmark with 0.1 precision: 130 KB/s => 30 KB/s
[Header("Precision")]
[Tooltip("Position is rounded in order to drastically minimize bandwidth.\n\nFor example, a precision of 0.01 rounds to a centimeter. In other words, sub-centimeter movements aren't synced until they eventually exceeded an actual centimeter.\n\nDepending on how important the object is, a precision of 0.01-0.10 (1-10 cm) is recommended.\n\nFor example, even a 1cm precision combined with delta compression cuts the Benchmark demo's bandwidth in half, compared to sending every tiny change.")]
[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
// delta compression needs to remember 'last' to compress against
protected Vector3Long lastSerializedPosition = Vector3Long.zero;
protected Vector3Long lastDeserializedPosition = Vector3Long.zero;
protected Vector3Long lastSerializedScale = Vector3Long.zero;
protected Vector3Long lastDeserializedScale = Vector3Long.zero;
// Used to store last sent snapshots
protected TransformSnapshot last;
protected int lastClientCount = 1;
// update //////////////////////////////////////////////////////////////
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();
}
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
// so no need to apply any snapshots there.
// -> don't apply for host mode player objects either, even if in
// client authority mode. if it doesn't go over the network,
// then we don't need to do anything.
// -> connectionToClient is briefly null after scene changes:
// https://github.com/MirrorNetworking/Mirror/issues/3329
if (syncDirection == SyncDirection.ClientToServer &&
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);
// interpolate & apply
TransformSnapshot computed = TransformSnapshot.Interpolate(from, to, t);
Apply(computed, to);
}
}
}
protected virtual void UpdateClient()
{
// client authority, and local player (= allowed to move myself)?
if (!IsClientWithAuthority)
{
// only while we have snapshots
if (clientSnapshots.Count > 0)
{
// 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);
}
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.
// only consider it changed if the quantized representation is changed.
// careful: don't use 'serialized / deserialized last'. as it depends on sync mode etc.
QuantizedChanged(last.position, current.position, positionPrecision) ||
// rotation isn't quantized / delta compressed.
// check with sensitivity.
Quaternion.Angle(last.rotation, current.rotation) > rotationSensitivity ||
// scale is quantized and delta compressed.
// only consider it changed if the quantized representation is changed.
// careful: don't use 'serialized / deserialized last'. as it depends on sync mode etc.
QuantizedChanged(last.scale, current.scale, scalePrecision);
// helper function to compare quantized representations of a Vector3
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected bool QuantizedChanged(Vector3 u, Vector3 v, float precision)
{
Compression.ScaleToLong(u, precision, out Vector3Long uQuantized);
Compression.ScaleToLong(v, precision, out Vector3Long vQuantized);
return uQuantized != vQuantized;
}
// NT may be used on client/server/host to Owner/Observers with
// ServerToClient or ClientToServer.
// however, OnSerialize should always delta against last.
public override void OnSerialize(NetworkWriter writer, bool initialState)
{
// get current snapshot for broadcasting.
TransformSnapshot snapshot = Construct();
// ClientToServer optimization:
// for interpolated client owned identities,
// always broadcast the latest known snapshot so other clients can
// interpolate immediately instead of catching up too
// TODO dirty mask? [compression is very good w/o it already]
// each vector's component is delta compressed.
// an unchanged component would still require 1 byte.
// let's use a dirty bit mask to filter those out as well.
// 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)
{
// (optional) smallest three compression for now. no delta.
if (compressRotation)
writer.WriteUInt(Compression.CompressQuaternion(snapshot.rotation));
else
writer.WriteQuaternion(snapshot.rotation);
}
if (syncScale) writer.WriteVector3(snapshot.scale);
}
// delta
else
{
// int before = writer.Position;
if (syncPosition)
{
// quantize -> delta -> varint
Compression.ScaleToLong(snapshot.position, positionPrecision, out Vector3Long quantized);
DeltaCompression.Compress(writer, lastSerializedPosition, quantized);
}
if (syncRotation)
{
// (optional) smallest three compression for now. no delta.
if (compressRotation)
writer.WriteUInt(Compression.CompressQuaternion(snapshot.rotation));
else
writer.WriteQuaternion(snapshot.rotation);
}
if (syncScale)
{
// quantize -> delta -> varint
Compression.ScaleToLong(snapshot.scale, scalePrecision, out Vector3Long quantized);
DeltaCompression.Compress(writer, lastSerializedScale, quantized);
}
}
// 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);
// set 'last'
last = snapshot;
}
public override void OnDeserialize(NetworkReader reader, bool initialState)
{
Vector3? position = null;
Quaternion? rotation = null;
Vector3? scale = null;
// initial
if (initialState)
{
if (syncPosition) position = reader.ReadVector3();
if (syncRotation)
{
// (optional) smallest three compression for now. no delta.
if (compressRotation)
rotation = Compression.DecompressQuaternion(reader.ReadUInt());
else
rotation = reader.ReadQuaternion();
}
if (syncScale) scale = reader.ReadVector3();
}
// delta
else
{
// varint -> delta -> quantize
if (syncPosition)
{
Vector3Long quantized = DeltaCompression.Decompress(reader, lastDeserializedPosition);
position = Compression.ScaleToFloat(quantized, positionPrecision);
}
if (syncRotation)
{
// (optional) smallest three compression for now. no delta.
if (compressRotation)
rotation = Compression.DecompressQuaternion(reader.ReadUInt());
else
rotation = reader.ReadQuaternion();
}
if (syncScale)
{
Vector3Long quantized = DeltaCompression.Decompress(reader, lastDeserializedScale);
scale = Compression.ScaleToFloat(quantized, scalePrecision);
}
}
// handle depending on server / client / host.
// server has priority for host mode.
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);
}
// sync ////////////////////////////////////////////////////////////////
// local authority client sends sync message to server for broadcasting
protected virtual void OnClientToServerSync(Vector3? position, Quaternion? rotation, Vector3? scale)
{
// only apply if in client authority mode
if (syncDirection != SyncDirection.ClientToServer) return;
// protect against ever growing buffer size attacks
if (serverSnapshots.Count >= connectionToClient.snapshotBufferSizeLimit) return;
// 'only sync on change' needs a correction on every new move sequence.
if (onlySyncOnChange &&
NeedsCorrection(serverSnapshots, connectionToClient.remoteTimeStamp, NetworkServer.sendInterval * sendIntervalMultiplier, onlySyncOnChangeCorrectionMultiplier))
{
RewriteHistory(
serverSnapshots,
connectionToClient.remoteTimeStamp,
NetworkTime.localTime, // arrival remote timestamp. NOT remote timeline.
NetworkServer.sendInterval * sendIntervalMultiplier, // Unity 2019 doesn't have timeAsDouble yet
GetPosition(),
GetRotation(),
GetScale());
}
// 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
protected virtual void OnServerToClientSync(Vector3? position, Quaternion? rotation, Vector3? scale)
{
// don't apply for local player with authority
if (IsClientWithAuthority) return;
// 'only sync on change' needs a correction on every new move sequence.
if (onlySyncOnChange &&
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 * sendIntervalMultiplier,
GetPosition(),
GetRotation(),
GetScale());
}
// 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 /////////////////////////////////////////////////
// snap interp. needs a continous flow of packets.
// 'only sync on change' interrupts it while not changed.
// once it restarts, snap interp. will interp from the last old position.
// this will cause very noticeable stutter for the first move each time.
// the fix is quite simple.
// 1. detect if the remaining snapshot is too old from a past move.
static bool NeedsCorrection(
SortedList<double, TransformSnapshot> snapshots,
double remoteTimestamp,
double bufferTime,
double toleranceMultiplier) =>
snapshots.Count == 1 &&
remoteTimestamp - snapshots.Keys[0] >= bufferTime * toleranceMultiplier;
// 2. insert a fake snapshot at current position,
// exactly one 'sendInterval' behind the newly received one.
static void RewriteHistory(
SortedList<double, TransformSnapshot> snapshots,
// timestamp of packet arrival, not interpolated remote time!
double remoteTimeStamp,
double localTime,
double sendInterval,
Vector3 position,
Quaternion rotation,
Vector3 scale)
{
// clear the previous snapshot
snapshots.Clear();
// insert a fake one at where we used to be,
// 'sendInterval' behind the new one.
SnapshotInterpolation.InsertIfNotExists(
snapshots,
NetworkClient.snapshotSettings.bufferLimit,
new TransformSnapshot(
remoteTimeStamp - sendInterval, // arrival remote timestamp. NOT remote time.
localTime - sendInterval, // Unity 2019 doesn't have timeAsDouble yet
position,
rotation,
scale
)
);
}
// reset state for next session.
// do not ever call this during a session (i.e. after teleport).
// calling this will break delta compression.
public override void ResetState()
{
base.ResetState();
// reset delta
lastSerializedPosition = Vector3Long.zero;
lastDeserializedPosition = Vector3Long.zero;
lastSerializedScale = Vector3Long.zero;
lastDeserializedScale = Vector3Long.zero;
// reset 'last' for delta too
last = new TransformSnapshot(0, 0, Vector3.zero, Quaternion.identity, Vector3.zero);
}
}
}
fileFormatVersion: 2
guid: 8ff3ba0becae47b8b9381191598957c8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment