0% found this document useful (0 votes)
18 views54 pages

ITP4733 Ln07-08 Netcode 1030

The document outlines a comprehensive guide for setting up a Unity project with networking capabilities using Unity Netcode, including tasks such as creating a user interface, player controls, and animations. It details the necessary components, scripts, and configurations required for multiplayer functionality, as well as optional enhancements like object pooling. Each task is broken down into steps, providing code snippets and references to facilitate the development process.

Uploaded by

kittychan810c
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
18 views54 pages

ITP4733 Ln07-08 Netcode 1030

The document outlines a comprehensive guide for setting up a Unity project with networking capabilities using Unity Netcode, including tasks such as creating a user interface, player controls, and animations. It details the necessary components, scripts, and configurations required for multiplayer functionality, as well as optional enhancements like object pooling. Each task is broken down into steps, providing code snippets and references to facilitate the development process.

Uploaded by

kittychan810c
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd
You are on page 1/ 54

Contents

Task1: Project Setup and adding NetworkManager & NetworkObject........................................................2


Task2: Create UI............................................................................................................................................7
Create UI and start Host............................................................................................................................7
Assign Network Player Name.................................................................................................................14
Create Player Control..............................................................................................................................17
Task 3a Create Simple Animator.................................................................................................................20
Task3b Update PlayerControl script...........................................................................................................23
Task4a Network Physics.............................................................................................................................26
Task 4b [Enhancement, optional] Object Pooling.......................................................................................32
Task 5 Using Unity Relay Service (skipped)..............................................................................................37
Task 6 3rd-Person Camera............................................................................................................................38
Task 7 Character Punch using ClientRpc ClientRpcParams & ServerRpc.................................................41
Reference:....................................................................................................................................................52
Task1: Project Setup and adding NetworkManager &
NetworkObject
Unity Netcode (replacing MLAPI) is a mid-level networking library built for the Unity game engine to
abstract networking. This allows developers to focus on the game rather than low level protocols and
networking frameworks.

Unity Multiplayer Networking, Components, Message System


https://docs-multiplayer.unity3d.com/netcode/current/about

Key concepts: Connection Approval, Network Object, Network Behaviour, Network Variable, PRC
Remote Procedure Call….etc.

*Declare a ServerRpc & ClientRpc


Developers can declare a ServerRpc by marking a method with [ServerRpc] attribute and making sure to
have ServerRpc suffix in the method name.

[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

Getting Started with Netcode (Unity Sample Project)


https://docs-multiplayer.unity3d.com/

Steps:
1. Create A new Project (unity2022.1.0f1)

2. Import Starter Asset from Unity Store


https://assetstore.unity.com/packages/essentials/starter-assets-third-person-character-controller-
196526
3. Import Netcode for GameObjects from package manager

Optional package:
- Universal RP (UniversalAdditionalCameraData.cs for MainCamera)
- Post Processing Stack

4. Create the following game object as shown below:


GameObject Name Object Type Remark
Environment_Prefab Prefab From StarterAssets
NetworkManager Empty Object
EventSystem EventSystem

5. Open prefab PlayerArmatureNetwork and add NetworkObject component

6. Configure NetworkManager as shown below


7. Play your scene and click start Host for testing
Result
Task2: Create UI

Create UI and start Host

1. Create the following UI objects or import from given package netcode-task2-


Canvas.unitypackage

Canvas Empty GameObject Required Component


Debug TextMeshPro – Text(UI)
StartServerButton Button
StartHostButton Button
StartClientButton Button
ExecutePhysicsButton Button
PlayersInGameText TextMeshPro – Text(UI)
UIManager Empty GameObject
PlayersManager Empty GameObject NetworkObject
2. Import Core Scripts, created and shared by DilmerGames
Singleton Return singleton object of any type <T>
NetworkSingleton Return singleton object of any type <T> with NetworkBehaviour, which
requires NetworkObject
Logger Customized Logger on UI, instead of using Debug.Log

Scripts:
Logger.cs
using System.Linq;
using DilmerGames.Core.Singletons;
using TMPro;
using UnityEngine;
using System;

public class Logger : Singleton<Logger>


{
[SerializeField]
private TextMeshProUGUI debugAreaText = null;

[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";
}
}

public void LogInfo(string message)


{
ClearLines();

debugAreaText.text += $"<color=\"green\">{DateTime.Now.ToString("HH:mm:ss.fff")}
{message}</color>\n";
}

public void LogError(string message)


{
ClearLines();
debugAreaText.text += $"<color=\"red\">{DateTime.Now.ToString("HH:mm:ss.fff")}
{message}</color>\n";
}

public void LogWarning(string message)


{
ClearLines();
debugAreaText.text +=
$"<color=\"yellow\">{DateTime.Now.ToString("HH:mm:ss.fff")} {message}</color>\n";
}

private void ClearLines()


{
if (debugAreaText.text.Split('\n').Count() >= maxLines)
{
debugAreaText.text = string.Empty;
}
}
}

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;
}
}
}
}

3. Create PlayersManager.cs and add to PlayerManager gameobject


using DilmerGames.Core.Singletons;
using Unity.Netcode; //to reference NetworkManager

public class PlayersManager : NetworkSingleton<PlayersManager>


{
private NetworkVariable<int> playersInGame = new NetworkVariable<int>();

public int PlayersInGame


{
get
{
return playersInGame.Value;
}
}

void Start()
{
NetworkManager.Singleton.OnClientConnectedCallback += (id) =>
{
if (IsServer)
{
Logger.Instance.LogInfo($"{id} just connected...");
playersInGame.Value++;
}
};

NetworkManager.Singleton.OnClientDisconnectCallback += (id) =>


{
if (IsServer)
{
Logger.Instance.LogInfo($"{id} just disconnected...");
playersInGame.Value--;
}
};
}
}

4. Create UIManagers.cs and add to UIManager gameobject


using DilmerGames.Core.Singletons;
using TMPro;
using Unity.Netcode;
using UnityEngine;
using UnityEngine.UI;

public class UIManager : Singleton<UIManager>


{
[SerializeField]
private Button startServerButton;

[SerializeField]
private Button startHostButton;

[SerializeField]
private Button startClientButton;

[SerializeField]
private TextMeshProUGUI playersInGameText;

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...");
});

}
}

*Assignment UIManger component as shown

*Check if any missing script reference for imported package, such as

5. Test your Scene and click Start Host


*Note: if you cannot click any button, check Project setting | Player
Set Active Input Handling = Both
Assign Network Player Name
6. Create shared script for networking data
NetworkString.cs
using Unity.Collections;
using Unity.Netcode;

public struct NetworkString : INetworkSerializable


{
private FixedString32Bytes info; //netcode network data size
public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T :
IReaderWriter
{
serializer.SerializeValue(ref info);
}

public override string ToString()


{
return info.ToString();
}

public static implicit operator string(NetworkString s) => s.ToString();


public static implicit operator NetworkString(string s) => new NetworkString()
{ info = new FixedString32Bytes(s) };
}

7. Create PlayerHud.cs and add to PlayerArmatureNetwork prefab


using TMPro;
using Unity.Netcode;
using UnityEngine;

public class PlayerHud : NetworkBehaviour


{
[SerializeField]
private NetworkVariable<NetworkString> playerNetworkName = new
NetworkVariable<NetworkString>();

private bool overlaySet = false;

public override void OnNetworkSpawn()


{
if (IsServer)
{
playerNetworkName.Value = $"Player {OwnerClientId}"; //Player ##
}
}

public void SetOverlay() //to update character model's child textmesh component
{
var localPlayerOverlay = gameObject.GetComponentInChildren<TextMeshProUGUI>();
localPlayerOverlay.text = $"{playerNetworkName.Value}";
}

public void Update()


{
if (!overlaySet && !string.IsNullOrEmpty(playerNetworkName.Value))
{
SetOverlay();
overlaySet = true;
}
}
}
*create Canvas for PlayerArmatureNetwork as shown below
8. Test your scene

Create Player Control


9. Create PlayerControl.cs and NetworkTransform component to prefab PlayerArmatureNetwork

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] //indicate show this field in inspector


private NetworkVariable<float> forwardBackPosition = new NetworkVariable<float>();

[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 (IsClient && IsOwner)


{
UpdateClient();
}
}
private void UpdateServer()
{
transform.position = new Vector3(transform.position.x + leftRightPosition.Value,
transform.position.y, transform.position.z + forwardBackPosition.Value);
}

private void UpdateClient()


{
float forwardBackward = 0;
float leftRight = 0;
if (Input.GetKey(KeyCode.W) || Input.GetKey(KeyCode.UpArrow))
{
forwardBackward += walkSpeed;
}

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;
}

if( (oldForwardBackPosition != forwardBackward) || (oldLeftRightPosition !=


leftRight) ) {
oldForwardBackPosition = forwardBackward;
oldLeftRightPosition = leftRight;
//update server
updateClientPositionServerRpc(forwardBackward, leftRight); //adding rpc
postfix to indicate a client will call server's method
}
}

[ServerRpc] //specify below method running on server


public void updateClientPositionServerRpc(float forwardBackward, float leftRight)
{
forwardBackPosition.Value = forwardBackward; //NetworkVariable<float>
leftRightPosition.Value = leftRight;
}

}
10. Test your scene

Task 3a Create Simple Animator


1. Create SimpleAnimator and the following state use of Walk_N animation for both Walk and
Reverse Walk state

*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

Reverse Walk State; set Speed = -1

2. Configure Transition as shown below

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:

4. Test your scene


Task3b Update PlayerControl script
5. Update PlayerControl scripts
using Unity.Netcode;
using UnityEngine;

[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>();

private CharacterController characterController;


private Animator animator;

// client caches positions


private Vector3 oldInputPosition = Vector3.zero;
private Vector3 oldInputRotation = Vector3.zero;
private PlayerState oldPlayerState = PlayerState.Idle;

private void Awake()


{
characterController = GetComponent<CharacterController>();
animator = GetComponent<Animator>();
}

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);
}
}

private void ClientVisuals()


{
if (networkPlayerState.Value == PlayerState.Walk)
{
animator.SetFloat("Walk", 1);
}
else if (networkPlayerState.Value == PlayerState.ReverseWalk)
{
animator.SetFloat("Walk", -1);
} else
{
animator.SetFloat("Walk", 0);
}
}

private void ClientInput()


{
// left & right rotation
Vector3 inputRotation = new Vector3(0, Input.GetAxis("Horizontal"), 0);

// forward & backward direction


Vector3 direction = transform.TransformDirection(Vector3.forward);
float forwardInput = Input.GetAxis("Vertical");
Vector3 inputPosition = direction * forwardInput;

// let server know about position and rotation client changes


if (oldInputPosition != inputPosition ||
oldInputRotation != inputRotation)
{
oldInputPosition = inputPosition;
oldInputRotation = inputRotation;

//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;
}
}

6. Test your Scene

Task4a Network Physics


1. Create SpawnerControl.cs
using DilmerGames.Core.Singletons;
using Unity.Netcode;
using UnityEngine;

public class SpawnerControl : NetworkSingleton<SpawnerControl>


{
[SerializeField]
private GameObject objectPrefab;

[SerializeField]
private int maxObjectInstanceCount = 3;

public void SpawnObjects()


{
if (!IsServer) return;

for (int i = 0; i < maxObjectInstanceCount; i++)


{
GameObject go = Instantiate(objectPrefab,
new Vector3(Random.Range(-10, 10), 10.0f, Random.Range(-10, 10)),
Quaternion.identity);

go.GetComponent<Rigidbody>().isKinematic = false; //if checked, need to


turn off
go.GetComponent<NetworkObject>().Spawn(); //sync object across network

}
}
}

2. Update UIManger.cs

using DilmerGames.Core.Singletons;
using TMPro;
using Unity.Netcode;
using UnityEngine;
using UnityEngine.UI;

public class UIManager : Singleton<UIManager>


{
[SerializeField]
private Button startServerButton;

[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();
});
}
}

3. Update UIManager Component Value

4. Create prefab PhysicsObject from Sphere

Update prefab PhysicsObject component value as shown below


 BouncingBall (Physic Material)
 Rigidbody
 NetworkObject
 Network Transform
 NetworkRigidBody.cs (Netcode built-in script)
 Toggle off NetworkTransform | Inerpolate

*create BouncingBall (physic) as shown below


Note: Interpolation Enabled
When Interpolate is turned on:

 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.

 In general, enabling interpolation is recommended unless specific gameplay mechanics dictate


otherwise.

7. Update NetworkManager component value as shown

8. Create Empty Object SpawnerControl and update below component


 NetworkObject
 SpawnerControl.cs
9. Test your scene
 Start Host and Client respectively,
 Click Execute Physics button
Task 4b [Enhancement, optional] Object Pooling
10. Import given Script NetworkObjectPool.cs

using DilmerGames.Core.Singletons;
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Unity.Netcode;
using UnityEngine;
using UnityEngine.Assertions;

public class NetworkObjectPool : Singleton<NetworkObjectPool>


{
[SerializeField]
NetworkManager m_NetworkManager;

[SerializeField]
List<PoolConfigObject> PooledPrefabsList;

HashSet<GameObject> prefabs = new HashSet<GameObject>();

Dictionary<GameObject, Queue<NetworkObject>> pooledObjects = new


Dictionary<GameObject, Queue<NetworkObject>>();

public void OnValidate()


{
for (var i = 0; i < PooledPrefabsList.Count; i++)
{
var prefab = PooledPrefabsList[i].Prefab;
if (prefab != null)
{
Assert.IsNotNull(prefab.GetComponent<NetworkObject>(),
$"{nameof(NetworkObjectPool)}: Pooled prefab \"{prefab.name}\" at index {i.ToString()}
has no {nameof(NetworkObject)} component.");
}
}
}

/// <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>();

Assert.IsNotNull(networkObject, $"{nameof(prefab)} must have


{nameof(networkObject)} component.");
Assert.IsFalse(prefabs.Contains(prefab), $"Prefab {prefab.name} is already
registered in the pool.");

RegisterPrefabInternal(prefab, prewarmCount);
}

/// <summary>
/// Builds up the cache for a prefab.
/// </summary>
private void RegisterPrefabInternal(GameObject prefab, int prewarmCount)
{
prefabs.Add(prefab);

var prefabQueue = new Queue<NetworkObject>();


pooledObjects[prefab] = prefabQueue;

for (int i = 0; i < prewarmCount; i++)


{
var go = CreateInstance(prefab);
ReturnNetworkObject(go.GetComponent<NetworkObject>(), prefab);
}

// Register MLAPI Spawn handlers


m_NetworkManager.PrefabHandler.AddHandler(prefab, new
DummyPrefabInstanceHandler(prefab, this));
}

[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>();
}

// Here we must reverse the logic in ReturnNetworkObject.


var go = networkObject.gameObject;
go.transform.SetParent(null);
go.SetActive(true);

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;
}

class DummyPrefabInstanceHandler : INetworkPrefabInstanceHandler


{
GameObject m_Prefab;
NetworkObjectPool m_Pool;

public DummyPrefabInstanceHandler(GameObject prefab, NetworkObjectPool pool)


{
m_Prefab = prefab;
m_Pool = pool;
}

public NetworkObject Instantiate(ulong ownerClientId, Vector3 position, Quaternion


rotation)
{
return m_Pool.GetNetworkObject(m_Prefab, position, rotation);
}

public void Destroy(NetworkObject networkObject)


{
m_Pool.ReturnNetworkObject(networkObject, m_Prefab);
}
}

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;

public class SpawnerControl : NetworkSingleton<SpawnerControl>


{
[SerializeField]
private GameObject objectPrefab;

[SerializeField]
private int maxObjectInstanceCount = 3;

private void Awake()


{

NetworkManager.Singleton.OnServerStarted += () =>
{
NetworkObjectPool.Instance.InitializePool();
};
}

public void SpawnObjects()


{
if (!IsServer) return;

for (int i = 0; i < maxObjectInstanceCount; i++)


{
/*
GameObject go = Instantiate(objectPrefab,
new Vector3(Random.Range(-10, 10), 10.0f, Random.Range(-10, 10)),
Quaternion.identity);

go.GetComponent<Rigidbody>().isKinematic = false; //if checked, need to


turn off
go.GetComponent<NetworkObject>().Spawn(); //sync object across network
*/
GameObject go =
NetworkObjectPool.Instance.GetNetworkObject(objectPrefab).gameObject;
go.transform.position = new Vector3(Random.Range(-10, 10), 10.0f,
Random.Range(-10, 10));
go.GetComponent<Rigidbody>().isKinematic = false; //if checked, need to
turn off
go.GetComponent<NetworkObject>().Spawn();

}
}
}

12. Test your scene

Task 5 Using Unity Relay Service (skipped)


1. This task is optional, you can refer to below link about using Relay Service
How To Make A Game With Unity Multiplayer Netcode | Relay Service Setup
https://www.youtube.com/watch?v=82Lbho7S0OA&list=PLQMQNmwN3FvyyeI1-
bDcBPmZiSaDMbFTi&index=6
Task 6 3rd-Person Camera
1. Add CinemachineBrain and CinemachineVirtualCamera to MainCamera

2. Create script PlayerCameraFollow.cs and add to Main Camera

using Cinemachine;
using DilmerGames.Core.Singletons;
using UnityEngine;

public class PlayerCameraFollow : Singleton<PlayerCameraFollow>


{
private CinemachineVirtualCamera cinemachineVirtualCamera;

private void Awake()


{
cinemachineVirtualCamera = GetComponent<CinemachineVirtualCamera>();
}

public void FollowPlayer(Transform transform)


{
// not all scenes have a cinemachine virtual camera so return in that's the case
if (cinemachineVirtualCamera == null) return;

cinemachineVirtualCamera.Follow = transform;

}
}

3. Update the start() of PlayerControl.cs

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;

public class PlayerCameraFollow : Singleton<PlayerCameraFollow>


{
[SerializeField]
private float amplitudeGain = 0.5f;

[SerializeField]
private float frequencyGain = 0.5f;

private CinemachineVirtualCamera cinemachineVirtualCamera;

private void Awake()


{
cinemachineVirtualCamera = GetComponent<CinemachineVirtualCamera>();
}

public void FollowPlayer(Transform transform)


{
// not all scenes have a cinemachine virtual camera so return in that's the case
if (cinemachineVirtualCamera == null) return;

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

Task 7 Character Punch using ClientRpc


ClientRpcParams & ServerRpc
1. Use given AdvancedAnimator

2. Review the Blend tree of each animation States


3. Use given prefab PlayerWithRaycastArmatureNetwork, which is duplicate from
PlayerArmatureNetwork but using AdvancedAnimator in previous step.

4. Create Script PlayerWithRaycastControl.cs and add to prefab


PlayerWithRaycastArmatureNetwork

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;

private CharacterController characterController;

// client caches positions


private Vector3 oldInputPosition = Vector3.zero;
private Vector3 oldInputRotation = Vector3.zero;
private PlayerState oldPlayerState = PlayerState.Idle;

private Animator animator;

private void Awake()


{
characterController = GetComponent<CharacterController>();
animator = GetComponent<Animator>();
}

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;

int layerMask = LayerMask.GetMask("Player");

if (Physics.Raycast(hand.position,
hand.transform.TransformDirection(aimDirection), out hit, minPunchDistance, layerMask))
{
Debug.DrawRay(hand.position, hand.transform.TransformDirection(aimDirection)
* minPunchDistance, Color.yellow);

var playerHit = hit.transform.GetComponent<NetworkObject>();


if (playerHit != null)
{
UpdateHealthServerRpc(1, playerHit.OwnerClientId);
}
}
else
{
Debug.DrawRay(hand.position, hand.transform.TransformDirection(aimDirection)
* minPunchDistance, Color.red);
}
}

private void ClientMoveAndRotate()


{
if (networkPositionDirection.Value != Vector3.zero)
{
characterController.SimpleMove(networkPositionDirection.Value);
}
if (networkRotationDirection.Value != Vector3.zero)
{
transform.Rotate(networkRotationDirection.Value, Space.World);
}
}

//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
}
}
}

private void ClientInput()


{
// left & right rotation
Vector3 inputRotation = new Vector3(0, Input.GetAxis("Horizontal"), 0);

// forward & backward direction


Vector3 direction = transform.TransformDirection(Vector3.forward);
float forwardInput = Input.GetAxis("Vertical");
Vector3 inputPosition = direction * forwardInput;

//Task 7.0.1
// change fighting states
if (ActivePunchActionKey() && forwardInput == 0)
{
UpdatePlayerStateServerRpc(PlayerState.Punch);
return;
}

// change motion states


if (forwardInput == 0)
UpdatePlayerStateServerRpc(PlayerState.Idle);
else if (!ActiveRunningActionKey() && forwardInput > 0 && forwardInput <= 1)
UpdatePlayerStateServerRpc(PlayerState.Walk);
else if (ActiveRunningActionKey() && forwardInput > 0 && forwardInput <= 1)
{
inputPosition = direction * runSpeedOffset;
UpdatePlayerStateServerRpc(PlayerState.Run);
}
else if (forwardInput < 0)
UpdatePlayerStateServerRpc(PlayerState.ReverseWalk);

// let server know about position and rotation client changes


if (oldInputPosition != inputPosition ||
oldInputRotation != inputRotation)
{
oldInputPosition = inputPosition;
oldInputRotation = inputRotation;
UpdateClientPositionAndRotationServerRpc(inputPosition * walkSpeed,
inputRotation * rotationSpeed);
}
}

private static bool ActiveRunningActionKey()


{
return Input.GetKey(KeyCode.LeftShift) || Input.GetKey(KeyCode.RightShift);
}

//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>();

if (clientWithDamaged != null && clientWithDamaged.networkPlayerHealth.Value >


0)
{
clientWithDamaged.networkPlayerHealth.Value -= takeAwayPoint;
}

// execute method on a client getting punch (victim)


NotifyHealthChangedClientRpc(takeAwayPoint, new ClientRpcParams
{
Send = new ClientRpcSendParams
{
TargetClientIds = new ulong[] { clientId }
}
});
}

//Task 7.4
[ClientRpc]
public void NotifyHealthChangedClientRpc(int takeAwayPoint, ClientRpcParams
clientRpcParams = default)
{
if (IsOwner) return;

Logger.Instance.LogInfo($"Client got punch {takeAwayPoint}");


}

//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

1. ClientInput -> UpdatePlayerStateServerRpc(…)


2. UpdatePlayerStateServerRpc(…) -> networkPlayerPunchBlend.Value (for UpdateVisual use)
3. ClientVisuals(…)

//Task7.1 to 7.4 Flow Sequence for updating server and specific client got Punch

1. FixedUpdate() -> CheckPunch


2. CheckPunch() -> UpdateHealthServerRpc(…)
3. UpdateHealthServerRpc -> NotifyHealthChangedClientRpc(…)
4. NotifyHealthChangedClientRpc(…)

6. Updated PlayerState in Enum.cs

public enum PlayerState


{
Idle,
Walk,
Run,
Punch,
ReverseWalk,
}
7. Add Player Layer

8. Update prefab PlayerWithRaycastArmatureNetwork component value as shown below


a. Assign controlled Left & Right Hand
b. Set Layer Player
9. Update NetworkManager Player prefab

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

-----below are obsolete but for reference


How To Make A Multiplayer Game In Unity 2021.1 (using MLAPI)
https://www.youtube.com/watch?v=4Mf81GdEDU8&list=PLS6sInD7ThM2_N9a1kN2oM4zZ-U-NtT2E

MLAPI Tutorial Series


https://www.youtube.com/playlist?list=PLbxeTux6kwSAseRmJeCyvkANHsI16PoM6

You might also like