
서버로 패킷을 넘길 때 최소화된 정보를 넘겨줘야 하므로 불필요한 정보는 UnCheck 해줍니다.
Player 프리팹에 Client Network Transform컴포넌트를 붙여줍니다.
Network Transform을 쓰지 않고 Client Network Transform을 붙여주는 이유는 아래와 같습니다.


정보(Data)도 동기화 잘되는지 Test 해보겠습니다. 코드를 위와 같이 작성해 주시면 됩니다.
이 스크립트로 Test를 해보면, Server 쪽에서 T키를 눌렀을 경우 문제없이 잘 작동하지만 클라이언트 쪽에서 T키를 눌렀을 경우 에러(Error)가 발생합니다.

위 코드와 같이 데이터 수정을 Write를 Owner에서 Everyone으로 해주면 됩니다.
실행 화면, 클라이언트 쪽에서 T를 눌러도 에러가 발생하지 않고 잘 작동함.

NetworkVariable 는 RPC나 Custom Message 사용 없이 서버와 클라이언트간에 값을 동기화해주는 방법입니다!
NetworkVariable 는 제너릭 형태로 값을 저장합니다!



string타입은 지원하지 않습니다! 왜냐하면 string은 C# immutable 타입이기 때문입니다!
매 Update 마다 CG allocation을 발생하기 때문에 퍼포먼스 문제가 발생합니다!
따라서, FixedString타입을 사용하여야 합니다!
public class TestFixedString : NetworkBehaviour
{
private NetworkVariable<FixedString128Bytes> m_TextString = new NetworkVariable<FixedString128Bytes>();
private string[] m_Messages ={ "This is the first message.",
"This is the second message (not like the first)",
"This is the third message (but not the last)",
"This is the fourth and last message (next will roll over to the first)"
};
private int m_MessageIndex = 0;
public override void OnNetworkSpawn()
{
if (IsServer)
{
m_TextString.Value = m_Messages[m_MessageIndex];
}
else
{
m_TextString.OnValueChanged += OnTextStringChanged;
Debug.Log($"Client-{NetworkManager.LocalClientId}'s TextString = {m_TextString.Value}");
}
}
public override void OnNetworkDespawn()
{
m_TextString.OnValueChanged -= OnTextStringChanged;
}
private void OnTextStringChanged(FixedString128Bytes previous, FixedString128Bytes current)
{
Debug.Log($"Client-{NetworkManager.LocalClientId}'s TextString = {m_TextString.Value}");
}
private void LateUpdate()
{
if (!IsServer)
{
return;
}
if (Input.GetKeyDown(KeyCode.Space))
{
m_MessageIndex++;
m_MessageIndex %= m_Messages.Length;
m_TextString.Value = m_Messages[m_MessageIndex];
Debug.Log($"Server-{NetworkManager.LocalClientId}'s TextString = {m_TextString.Value}");
}
}
}





private void Setup()
{
NetworkManager.Singleton.ConnectionApprovalCallback = ApprovalCheck;
NetworkManager.Singleton.StartHost();
}
private void ApprovalCheck(NetworkManager.ConnectionApprovalRequest request, NetworkManager.ConnectionApprovalResponse response)
{
// The client identifier to be authenticated
var clientId = request.ClientNetworkId;
// Additional connection data defined by user code
var connectionData = request.Payload;
// Your approval logic determines the following values
response.Approved = true;
response.CreatePlayerObject = true;
// The Prefab hash value of the NetworkPrefab, if null the default NetworkManager player Prefab is used
response.PlayerPrefabHash = null;
// Position to spawn the player object (if null it uses default of Vector3.zero)
response.Position = Vector3.zero;
// Rotation to spawn the player object (if null it uses the default of Quaternion.identity)
response.Rotation = Quaternion.identity;
// If response.Approved is false, you can provide a message that explains the reason why via ConnectionApprovalResponse.Reason
// On the client-side, NetworkManager.DisconnectReason will be populated with this message via DisconnectReasonMessage
response.Reason = "Some reason for not approving the client";
// If additional approval steps are needed, set this to true until the additional steps are complete
// once it transitions from true to false the connection approval response will be processed.
response.Pending = false;
}
using UnityEngine;
using Unity.Netcode;
/// <summary>
/// Connection Approval Handler Component
/// </summary>
/// <remarks>
/// This should be placed on the same GameObject as the NetworkManager.
/// It automatically declines the client connection for example purposes.
/// </remarks>
public class ConnectionApprovalHandler : MonoBehaviour
{
private NetworkManager m_NetworkManager;
private void Start()
{
m_NetworkManager = GetComponent<NetworkManager>();
if (m_NetworkManager != null)
{
m_NetworkManager.OnClientDisconnectCallback += OnClientDisconnectCallback;
m_NetworkManager.ConnectionApprovalCallback = ApprovalCheck;
}
}
private void ApprovalCheck(NetworkManager.ConnectionApprovalRequest request, NetworkManager.ConnectionApprovalResponse response)
{
response.Approved = false;
response.Reason = "Testing the declined approval message";
}
private void OnClientDisconnectCallback(ulong obj)
{
if (!m_NetworkManager.IsServer && m_NetworkManager.DisconnectReason != string.Empty)
{
Debug.Log($"Approval Declined Reason: {m_NetworkManager.DisconnectReason}");
}
}
}
using Unity.Netcode;
NetworkManager.Singleton.NetworkConfig.ConnectionData = System.Text.Encoding.ASCII.GetBytes("room password");
NetworkManager.Singleton.StartClient();

public class ClientConnectionHandler : NetworkBehaviour
{
public List<uint> AlternatePlayerPrefabs;
public void SetClientPlayerPrefab(int index)
{
if (index > AlternatePlayerPrefabs.Count)
{
Debug.LogError($"Trying to assign player Prefab index of {index} when there are onlky {AlternatePlayerPrefabs.Count} entries!");
return;
}
if (NetworkManager.IsListening || IsSpawned)
{
Debug.LogError("This needs to be set this before connecting!");
return;
}
NetworkManager.NetworkConfig.ConnectionData = System.BitConverter.GetBytes(index);
}
public override void OnNetworkSpawn()
{
if (IsServer)
{
NetworkManager.ConnectionApprovalCallback = ConnectionApprovalCallback;
}
}
private void ConnectionApprovalCallback(NetworkManager.ConnectionApprovalRequest request, NetworkManager.ConnectionApprovalResponse response)
{
var playerPrefabIndex = System.BitConverter.ToInt32(request.Payload);
if (AlternatePlayerPrefabs.Count < playerPrefabIndex)
{
response.PlayerPrefabHash = AlternatePlayerPrefabs[playerPrefabIndex];
}
else
{
Debug.LogError($"Client provided player Prefab index of {playerPrefabIndex} when there are onlky {AlternatePlayerPrefabs.Count} entries!");
return;
}
// Continue filling out the response
}
}


AlternatePlayerPrefabs List에 채우기 위해
씬 안에 놓여 있는 ConnectoinApprovalCallback이 붙어 있는 NetworkObject가 포함된 Scene 엽니다!
List에 추가되길 바라는 Alternate Player Prefab을 찾고, 프리팹으로 선택합니다, 그리고 GlobalObjectIdHash 값을 복사합니다!
AlternatePlayerPrefabs List안에 새로운 List item으로 복사된 GlobalObjectIdHash 값을 붙여줍니다!





GetComponent<NetworkObject>().Spawn();
GetComponent<NetworkObject>().SpawnWithOwnership(clientId);
GetComponent<NetworkObject>().ChangeOwnership(clientId);
GetComponent<NetworkObject>().RemoveOwnership();
GetComponent<NetworkObject>().SpawnAsPlayerObject(clientId);

NetworkManager.Singleton.ConnectedClients[clientId].PlayerObject

런타임동안 생성되는 NetworkObject들은 Start함수가 호출되기 전에 OnNetworkSpawn함수가 호출됩니다!
따라서!!!! 런타임에 생성되는 NetworkObject에 Start함수에서 상태를 초기화 하면 OnNetworkSpawn함수에 초기화한 값이 설정되지 않기 때문에 주의해야 합니다!

using UnityEngine;
using Unity.Netcode;
public class Spawner : MonoBehaviour
{
public GameObject healthObjectPrefab;
void Update()
{
if (IsServer && Input.GetKeyDown(KeyCode.Space))
{
GameObject obj = Instantiate(healthObjectPrefab, Vector3.zero, Quaternion.identity);
obj.GetComponent<NetworkObject>().Spawn();
Debug.Log("[Server] Spawned HealthObject");
}
}
private bool IsServer => NetworkManager.Singleton.IsServer;
}
using Unity.Netcode;
using UnityEngine;
public class HealthObject : NetworkBehaviour
{
public NetworkVariable<int> Health = new NetworkVariable<int>(100);
private string objectName = "MonsterA";
public override void OnNetworkSpawn()
{
if (IsServer)
{
Debug.Log($"[Server] Spawned with health = {Health.Value}");
}
else
{
Debug.Log($"[Client] Spawned with health = {Health.Value}");
}
}
// 서버 → 클라이언트로 objectName 전송
public override void OnSynchronize<T>(T writer)
{
if (IsServer)
{
writer.WriteValueSafe(objectName);
}
}
// 클라이언트에서 수신한 objectName 처리
public override void OnDeserialize<T>(T reader)
{
if (!IsServer)
{
objectName = reader.ReadValueSafe<string>();
Debug.Log($"[Client] Received objectName = {objectName}");
}
}
}
obj.GetComponent <NetworkObject>().Spawn()
이 코드는 서버가 오브젝트를 네트워크에 "등록"하고, CreateObjectMessage를 생성해 클라이언트들에게 전파합니다.
이 코드는 서버에서 실행되어야 하고, 서버가 클라이언트들에게 이 오브젝트의 생성 사실을 알리는 역할


즉, 클라이언트 입장에서, 아래 두 로그가 출력되었다.
Debug.Log($"[Client] Spawned with health = {Health.Value}");
Debug.Log($"[Client] Received objectName = {objectName}");

서버는 생성 → 상태 직렬화 → 메시지 생성 → 메시지 전송
클라이언트는 메시지 수신 → 객체 생성 → 상태 역직렬화 → 네트워크 스폰 완료 콜백 실행