ITP4733 Ln07-08 Netcode 1030
ITP4733 Ln07-08 Netcode 1030
Key concepts: Connection Approval, Network Object, Network Behaviour, Network Variable, PRC
Remote Procedure Call….etc.
[ServerRpc]
void PingServerRpc(int somenumber, string sometext) { /* ... */ }
[ClientRpc]
void PongClientRpc(int somenumber, string sometext) { /* ... */ }
ref: https://docs-multiplayer.unity3d.com/netcode/current/advanced-topics/messaging-system
Steps:
1. Create A new Project (unity2022.1.0f1)
Optional package:
- Universal RP (UniversalAdditionalCameraData.cs for MainCamera)
- Post Processing Stack
Scripts:
Logger.cs
using System.Linq;
using DilmerGames.Core.Singletons;
using TMPro;
using UnityEngine;
using System;
[SerializeField]
private bool enableDebug = false;
[SerializeField]
private int maxLines = 15;
void Awake()
{
if (debugAreaText == null)
{
debugAreaText = GetComponent<TextMeshProUGUI>();
}
debugAreaText.text = string.Empty;
}
void OnEnable()
{
debugAreaText.enabled = enableDebug;
enabled = enableDebug;
if (enabled)
{
debugAreaText.text +=
$"<color=\"white\">{DateTime.Now.ToString("HH:mm:ss.fff")} {this.GetType().Name}
enabled</color>\n";
}
}
debugAreaText.text += $"<color=\"green\">{DateTime.Now.ToString("HH:mm:ss.fff")}
{message}</color>\n";
}
NetworkSingleton.cs
using Unity.Netcode;
using UnityEngine;
namespace DilmerGames.Core.Singletons
{
public class NetworkSingleton<T> : NetworkBehaviour
where T : Component
{
private static T _instance;
public static T Instance
{
get
{
if (_instance == null)
{
var objs = FindObjectsOfType(typeof(T)) as T[];
if (objs.Length > 0)
_instance = objs[0];
if (objs.Length > 1)
{
Debug.LogError("There is more than one " + typeof(T).Name + " in
the scene.");
}
if (_instance == null)
{
GameObject obj = new GameObject();
obj.name = string.Format("_{0}", typeof(T).Name);
_instance = obj.AddComponent<T>();
}
}
return _instance;
}
}
}
}
void Start()
{
NetworkManager.Singleton.OnClientConnectedCallback += (id) =>
{
if (IsServer)
{
Logger.Instance.LogInfo($"{id} just connected...");
playersInGame.Value++;
}
};
[SerializeField]
private Button startHostButton;
[SerializeField]
private Button startClientButton;
[SerializeField]
private TextMeshProUGUI playersInGameText;
void Update()
{
playersInGameText.text = $"Players in game:
{PlayersManager.Instance.PlayersInGame}";
}
void Start()
{
// START SERVER
startServerButton?.onClick.AddListener(() =>
{
if (NetworkManager.Singleton.StartServer())
Logger.Instance.LogInfo("Server started...");
else
Logger.Instance.LogInfo("Unable to start server...");
});
// START HOST
startHostButton?.onClick.AddListener(async () =>
{
if (NetworkManager.Singleton.StartHost())
Logger.Instance.LogInfo("Host started...");
else
Logger.Instance.LogInfo("Unable to start host...");
});
// START CLIENT
startClientButton?.onClick.AddListener(async () =>
{
if (NetworkManager.Singleton.StartClient())
Logger.Instance.LogInfo("Client started...");
else
Logger.Instance.LogInfo("Unable to start client...");
});
}
}
public void SetOverlay() //to update character model's child textmesh component
{
var localPlayerOverlay = gameObject.GetComponentInChildren<TextMeshProUGUI>();
localPlayerOverlay.text = $"{playerNetworkName.Value}";
}
PlayerControl.cs
using Unity.Netcode;
using UnityEngine;
[RequireComponent(typeof(NetworkObject))]
public class PlayerControl : NetworkBehaviour
{
[SerializeField]
private float walkSpeed = 0.1f;
[SerializeField]
private Vector2 defaultPositionRange = new Vector2(-4, 4);
[SerializeField]
private NetworkVariable<float> leftRightPosition = new NetworkVariable<float>();
//client caching
private float oldForwardBackPosition;
private float oldLeftRightPosition;
void Start()
{
transform.position = new Vector3(Random.Range(defaultPositionRange.x,
defaultPositionRange.y), 0,
Random.Range(defaultPositionRange.x, defaultPositionRange.y));
}
void Update()
{
if (IsServer)
{
UpdateServer();
}
if (Input.GetKey(KeyCode.S) || Input.GetKey(KeyCode.DownArrow))
{
forwardBackward -= walkSpeed;
}
if (Input.GetKey(KeyCode.A) || Input.GetKey(KeyCode.LeftArrow))
{
leftRight -= walkSpeed;
if (Input.GetKey(KeyCode.D) || Input.GetKey(KeyCode.RightArrow))
{
leftRight += walkSpeed;
}
}
10. Test your scene
*Note:You need to delete existing SimpleAnimator from imported from previous step and create from
scratch for your animator to work without error.
Walk State
Transition conditions
Idle -> Walk Walk > 0
Walk -> Idle Walk < 1
Walk -> Reverse Walk Walk < 0
Reverse Walk -> Walk Walk > 0
Idle -> Reverse Walk Walk < 0
Reverse Walk -> Idle Walk < 1
Walk > -1
3. Add Component Animator and NetworkAnimator to Prefab PlayerArmatureNetwork; then set
value as shown below:
[RequireComponent(typeof(NetworkObject))]
public class PlayerControl : NetworkBehaviour
{
public enum PlayerState
{
Idle,
Walk,
ReverseWalk
}
[SerializeField]
private float speed = 3.5f;
[SerializeField]
private float rotationSpeed = 2.0f;
[SerializeField]
private Vector2 defaultInitialPlanePosition = new Vector2(-4, 4);
[SerializeField]
//check API options for NetworkVariableReadPermission {Everyone|OwnerOnly}
private NetworkVariable<Vector3> networkPositionDirection = new
NetworkVariable<Vector3>(); //default permission
[SerializeField]
private NetworkVariable<Vector3> networkRotationDirection = new
NetworkVariable<Vector3>();
[SerializeField]
private NetworkVariable<PlayerState> networkPlayerState = new
NetworkVariable<PlayerState>();
void Start()
{
if (IsClient && IsOwner)
{
transform.position = new Vector3(Random.Range(defaultInitialPlanePosition.x,
defaultInitialPlanePosition.y), 0,
Random.Range(defaultInitialPlanePosition.x,
defaultInitialPlanePosition.y));
}
}
void Update()
{
if (IsClient && IsOwner)
{
ClientInput();
}
ClientMoveAndRotate();
ClientVisuals();
}
private void ClientMoveAndRotate()
{
if (networkPositionDirection.Value != Vector3.zero)
{
characterController.SimpleMove(networkPositionDirection.Value);
}
if (networkRotationDirection.Value != Vector3.zero)
{
transform.Rotate(networkRotationDirection.Value, Space.World);
}
}
//UpdateClientPositionAndRotationServerRpc(inputPosition * walkSpeed,
inputRotation * rotationSpeed);
UpdateClientPositionAndRotationServerRpc(inputPosition, inputRotation);
}
if (forwardInput > 0)
{
UpdatePlayerStateServerRpc(PlayerState.Walk);
}
else if (forwardInput < 0)
{
UpdatePlayerStateServerRpc(PlayerState.ReverseWalk);
}
else
{
UpdatePlayerStateServerRpc(PlayerState.Idle);
}
}
[ServerRpc]
public void UpdateClientPositionAndRotationServerRpc(Vector3 newPosition, Vector3
newRotation)
{
networkPositionDirection.Value = newPosition;
networkRotationDirection.Value = newRotation;
}
[ServerRpc]
public void UpdatePlayerStateServerRpc(PlayerState newState)
{
networkPlayerState.Value = newState;
}
}
[SerializeField]
private int maxObjectInstanceCount = 3;
}
}
}
2. Update UIManger.cs
using DilmerGames.Core.Singletons;
using TMPro;
using Unity.Netcode;
using UnityEngine;
using UnityEngine.UI;
[SerializeField]
private Button startHostButton;
[SerializeField]
private Button startClientButton;
[SerializeField]
private TextMeshProUGUI playersInGameText;
//Task4
[SerializeField]
private Button executePhysicsButton;
private bool hasServerStarted;
private void Awake()
{
Cursor.visible = true;
}
void Update()
{
playersInGameText.text = $"Players in game:
{PlayersManager.Instance.PlayersInGame}";
}
void Start()
{
// START SERVER
startServerButton?.onClick.AddListener(() =>
{
if (NetworkManager.Singleton.StartServer())
Logger.Instance.LogInfo("Server started...");
else
Logger.Instance.LogInfo("Unable to start server...");
});
// START HOST
startHostButton?.onClick.AddListener(async () =>
{
if (NetworkManager.Singleton.StartHost())
Logger.Instance.LogInfo("Host started...");
else
Logger.Instance.LogInfo("Unable to start host...");
});
// START CLIENT
startClientButton?.onClick.AddListener(async () =>
{
if (NetworkManager.Singleton.StartClient())
Logger.Instance.LogInfo("Client started...");
else
Logger.Instance.LogInfo("Unable to start client...");
});
// Task4 debug
NetworkManager.Singleton.OnClientConnectedCallback += (id) =>
{
Logger.Instance.LogInfo($"{id} just connected...");
};
NetworkManager.Singleton.OnServerStarted += () =>
{
hasServerStarted = true;
};
executePhysicsButton?.onClick.AddListener(async () =>
{
if (!hasServerStarted)
{
Logger.Instance.LogWarning("Server has not started...");
return;
}
SpawnerControl. Instance.SpawnObjects();
});
}
}
Smooth Movement: The component buffers incoming updates and applies them gradually, resulting in
smoother transitions. This means that even if multiple updates are received at once due to network
latency, they will be processed in a way that appears fluid to the player.
Visual Stability: Players experience less jitter and fewer abrupt changes in position. This is
particularly beneficial in fast-paced games where smooth visuals are crucial for gameplay.
Buffering Mechanism: Incoming state updates are queued and applied over time, which helps
mitigate the effects of lag by creating a more seamless visual experience.
Interpolation Disabled
When Interpolate is turned off:
Immediate Updates: Transform changes are applied instantly as they are received, which can lead to a
more responsive feel. However, this comes at the cost of visual smoothness.
Jitter and Jumping: Without interpolation, clients may experience jittery or "jumping" movements,
especially if there are fluctuations in network latency. This can make characters appear to teleport
rather than move smoothly across the screen.
Less Buffering: The absence of buffering means that if multiple updates arrive simultaneously due to
lag, they will be rendered all at once without any smoothing effect, leading to noticeable visual
artifacts25.
Summary
Choosing whether to enable or disable interpolation in the NetworkTransform component depends on the
specific needs of your game:
Enable Interpolation for smoother visuals and a more polished experience, especially in games where
aesthetics matter.
Disable Interpolation for a more responsive feel at the potential cost of visual smoothness, suitable
for scenarios where immediate feedback is prioritized over appearance.
using DilmerGames.Core.Singletons;
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Unity.Netcode;
using UnityEngine;
using UnityEngine.Assertions;
[SerializeField]
List<PoolConfigObject> PooledPrefabsList;
/// <summary>
/// Gets an instance of the given prefab from the pool. The prefab must be
registered to the pool.
/// </summary>
/// <param name="prefab"></param>
/// <returns></returns>
public NetworkObject GetNetworkObject(GameObject prefab)
{
return GetNetworkObjectInternal(prefab, Vector3.zero, Quaternion.identity);
}
/// <summary>
/// Gets an instance of the given prefab from the pool. The prefab must be
registered to the pool.
/// </summary>
/// <param name="prefab"></param>
/// <param name="position">The position to spawn the object at.</param>
/// <param name="rotation">The rotation to spawn the object with.</param>
/// <returns></returns>
public NetworkObject GetNetworkObject(GameObject prefab, Vector3 position,
Quaternion rotation)
{
return GetNetworkObjectInternal(prefab, position, rotation);
}
/// <summary>
/// Return an object to the pool (and reset them).
/// </summary>
public void ReturnNetworkObject(NetworkObject networkObject, GameObject prefab)
{
var go = networkObject.gameObject;
// In this simple example pool we just disable objects while they are in the
pool. But we could call a function on the object here for more flexibility.
go.SetActive(false);
//go.transform.SetParent(transform);
pooledObjects[prefab].Enqueue(networkObject);
}
/// <summary>
/// Adds a prefab to the list of spawnable prefabs.
/// </summary>
/// <param name="prefab">The prefab to add.</param>
/// <param name="prewarmCount"></param>
public void AddPrefab(GameObject prefab, int prewarmCount = 0)
{
var networkObject = prefab.GetComponent<NetworkObject>();
RegisterPrefabInternal(prefab, prewarmCount);
}
/// <summary>
/// Builds up the cache for a prefab.
/// </summary>
private void RegisterPrefabInternal(GameObject prefab, int prewarmCount)
{
prefabs.Add(prefab);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private GameObject CreateInstance(GameObject prefab)
{
return Instantiate(prefab);
}
/// <summary>
/// This matches the signature of <see
cref="NetworkSpawnManager.SpawnHandlerDelegate"/>
/// </summary>
/// <param name="prefabHash"></param>
/// <param name="position"></param>
/// <param name="rotation"></param>
/// <returns></returns>
private NetworkObject GetNetworkObjectInternal(GameObject prefab, Vector3 position,
Quaternion rotation)
{
var queue = pooledObjects[prefab];
NetworkObject networkObject;
if (queue.Count > 0)
{
networkObject = queue.Dequeue();
}
else
{
networkObject = CreateInstance(prefab).GetComponent<NetworkObject>();
}
go.transform.position = position;
go.transform.rotation = rotation;
return networkObject;
}
/// <summary>
/// Registers all objects in <see cref="PooledPrefabsList"/> to the cache.
/// </summary>
public void InitializePool()
{
foreach (var configObject in PooledPrefabsList)
{
RegisterPrefabInternal(configObject.Prefab, configObject.PrewarmCount);
}
}
}
[Serializable]
struct PoolConfigObject
{
public GameObject Prefab;
public int PrewarmCount;
}
11. Create Empty GameObject NetworkObjectPool and update component value as shown below
Update SpawnerControl.cs to use NetworkObjectPool to spawn network objects instead
using DilmerGames.Core.Singletons;
using Unity.Netcode;
using UnityEngine;
[SerializeField]
private int maxObjectInstanceCount = 3;
NetworkManager.Singleton.OnServerStarted += () =>
{
NetworkObjectPool.Instance.InitializePool();
};
}
}
}
}
using Cinemachine;
using DilmerGames.Core.Singletons;
using UnityEngine;
cinemachineVirtualCamera.Follow = transform;
}
}
void Start()
{
if (IsClient && IsOwner)
{
transform.position = new Vector3(Random.Range(defaultInitialPlanePosition.x,
defaultInitialPlanePosition.y), 0,
Random.Range(defaultInitialPlanePosition.x,
defaultInitialPlanePosition.y));
PlayerCameraFollow.Instance.FollowPlayer(transform.Find("PlayerCameraRoot")); //task6
}
}
4. Test your scene and experiment with CinemachineVirtualCamera Component Value as shown
below:
5. Modify PlayerCameraFollow to setup Noise using Basic Mulit Channel perlin as shown below
6. Update PlayerCameraFollow.cs
using Cinemachine;
using DilmerGames.Core.Singletons;
using UnityEngine;
[SerializeField]
private float frequencyGain = 0.5f;
cinemachineVirtualCamera.Follow = transform;
var perlin =
cinemachineVirtualCamera.GetCinemachineComponent<CinemachineBasicMultiChannelPerlin>();
//configure CinemachineVirtualCamera prior to use this code
perlin.m_AmplitudeGain = amplitudeGain;
perlin.m_FrequencyGain = frequencyGain;
}
}
7. Test your scene
PlayerWithRaycastControl.cs
using Unity.Netcode;
using Unity.Netcode.Components;
using UnityEngine;
[RequireComponent(typeof(NetworkTransform))]
[RequireComponent(typeof(NetworkObject))]
public class PlayerWithRaycastControl : NetworkBehaviour
{
[SerializeField]
private float walkSpeed = 3.5f;
[SerializeField]
private float runSpeedOffset = 2.0f;
[SerializeField]
private float rotationSpeed = 3.5f;
[SerializeField]
private Vector2 defaultInitialPositionOnPlane = new Vector2(-4, 4);
[SerializeField]
private NetworkVariable<Vector3> networkPositionDirection = new
NetworkVariable<Vector3>();
[SerializeField]
private NetworkVariable<Vector3> networkRotationDirection = new
NetworkVariable<Vector3>();
//Task 7
[SerializeField]
private NetworkVariable<PlayerState> networkPlayerState = new
NetworkVariable<PlayerState>();
[SerializeField]
private NetworkVariable<float> networkPlayerHealth = new
NetworkVariable<float>(1000);
[SerializeField]
private NetworkVariable<float> networkPlayerPunchBlend = new
NetworkVariable<float>();
[SerializeField]
private GameObject leftHand;
[SerializeField]
private GameObject rightHand;
[SerializeField]
private float minPunchDistance = 1.0f;
void Start()
{
if (IsClient && IsOwner)
{
transform.position = new
Vector3(Random.Range(defaultInitialPositionOnPlane.x, defaultInitialPositionOnPlane.y),
0,
Random.Range(defaultInitialPositionOnPlane.x,
defaultInitialPositionOnPlane.y));
PlayerCameraFollow.Instance.FollowPlayer(transform.Find("PlayerCameraRoot"));
}
}
void Update()
{
if (IsClient && IsOwner)
{
ClientInput();
}
ClientMoveAndRotate();
ClientVisuals();
}
//Task 7.1
private void FixedUpdate()
{
if (IsClient && IsOwner)
{
if (networkPlayerState.Value == PlayerState.Punch && ActivePunchActionKey())
//get Punchkey from your inputkey
{
CheckPunch(leftHand.transform, Vector3.up);
CheckPunch(rightHand.transform, Vector3.down);
}
}
}
//Task 7.2
private void CheckPunch(Transform hand, Vector3 aimDirection)
{
RaycastHit hit;
if (Physics.Raycast(hand.position,
hand.transform.TransformDirection(aimDirection), out hit, minPunchDistance, layerMask))
{
Debug.DrawRay(hand.position, hand.transform.TransformDirection(aimDirection)
* minPunchDistance, Color.yellow);
//Task 7.0.2
private void ClientVisuals()
{
if (oldPlayerState != networkPlayerState.Value)
{
oldPlayerState = networkPlayerState.Value;
animator.SetTrigger($"{networkPlayerState.Value}");
if (networkPlayerState.Value == PlayerState.Punch)
{
animator.SetFloat($"{networkPlayerState.Value}Blend",
networkPlayerPunchBlend.Value); //to set Left or Right Punch animation
}
}
}
//Task 7.0.1
// change fighting states
if (ActivePunchActionKey() && forwardInput == 0)
{
UpdatePlayerStateServerRpc(PlayerState.Punch);
return;
}
//Task 7
private static bool ActivePunchActionKey()
{
return Input.GetKey(KeyCode.Space);
}
[ServerRpc]
public void UpdateClientPositionAndRotationServerRpc(Vector3 newPosition, Vector3
newRotation)
{
networkPositionDirection.Value = newPosition;
networkRotationDirection.Value = newRotation;
}
//Task 7.3
[ServerRpc]
public void UpdateHealthServerRpc(int takeAwayPoint, ulong clientId)
{
var clientWithDamaged = NetworkManager.Singleton.ConnectedClients[clientId]
//tell server to updateHealth of specific client
.PlayerObject.GetComponent<PlayerWithRaycastControl>();
//Task 7.4
[ClientRpc]
public void NotifyHealthChangedClientRpc(int takeAwayPoint, ClientRpcParams
clientRpcParams = default)
{
if (IsOwner) return;
//Task 7.0.2
[ServerRpc]
public void UpdatePlayerStateServerRpc(PlayerState state)
{
networkPlayerState.Value = state;
if (state == PlayerState.Punch)
{
networkPlayerPunchBlend.Value = Random.Range(0.0f, 1.0f); //random number
for Right or Left Punch
}
}
}
5. Code Explanation
//Task7.0.x Flow Sequence for checking client attack
//Task7.1 to 7.4 Flow Sequence for updating server and specific client got Punch
10. Test Your scene and review the usage of getting random PunchBlend Value for Left or right punch
animation.
11. Build and Test your scene (remember to add new scene in build setting)
a.
Reference:
How To Make A Game With Netcode in Unity 2022.x.x ( using Netcode, not MLAPI!)
https://www.youtube.com/watch?v=d1FpS5hYlVE&list=PLQMQNmwN3FvyyeI1-bDcBPmZiSaDMbFTi