중요: UNet은 지원이 중단된 솔루션이며, 새로운 멀티플레이어 및 네트워킹 솔루션(게임 오브젝트용 넷코드)이 개발 중입니다. 자세한 내용과 다음 단계는 게임 오브젝트용 Unity 넷코드 웹사이트에 있는 정보를 참조하십시오. |
Unity에서는 일반적으로 Instantiate()
를 사용하여 새 게임 오브젝트를 “스폰”(즉, 생성)합니다. 하지만 멀티플레이어 고수준 API에서 “스폰”은 좀 더 세분화됩니다. HLAPI의 서버 권한 HLAPI 모델에서는 서버에 게임 오브젝트를 “스폰”한다는 의미는 게임 오브젝트가 서버와 연결된 클라이언트에 생성되고 스폰 시스템에 의해 관리된다는 의미입니다.
이 시스템을 통해 게임 오브젝트가 스폰되면, 서버의 게임 오브젝트가 변경될 때마다 상태 업데이트가 클라이언트에 전송됩니다. Unity가 서버의 게임 오브젝트를 삭제하면 클라이언트에서도 삭제됩니다. 서버는 다른 모든 네트워크로 연결된 게임 오브젝트와 함께 스폰된 게임 오브젝트를 관리합니다. 따라서 나중에 다른 클라이언트가 게임에 참가하는 경우 서버는 해당 클라이언트에 게임 오브젝트를 스폰할 수 있습니다. 이렇게 스폰된 게임 오브젝트는 “netId”라고 불리는 고유한 네트워크 인스턴스 ID를 가집니다. 이 ID는 각 게임 오브젝트의 서버와 클라이언트에서 동일합니다. 이러한 고유 네트워크 인스턴스 ID를 통해 네트워크에서 설정된 메시지를 게임 오브젝트로 라우팅하고 게임 오브젝트를 식별합니다.
서버가 Network Identity** 컴포넌트를 사용하여 게임 오브젝트를 스폰하면 클라이언트에 스폰된 게임 오브젝트도 동일한 “상태”가 됩니다. 즉, 서버의 게임 오브젝트와 동일하며. 동일한 트랜스폼, 이동 상태, 그리고 (NetworkTransform 및 SyncVars가 사용된 경우) 동기화된 변수를 가집니다. 따라서 Unity가 게임 오브젝트를 만들면 클라이언트 게임 오브젝트는 항상 최신 상태를 유지합니다. 이를 통해 잘못된 시작 위치에 게임 오브젝트를 스폰한 후 상태 업데이트가 도착할 때 올바른 위치에 다시 나타나는 문제 등을 피할 수 있습니다.
네트워크 관리자는 등록된 프리팹의 게임 오브젝트만 스폰하고 동기화할 수 있으므로, 게임 도중 스폰하고 싶은 특정 게임 오브젝트 프리팹이 있으면 네트워크 관리자에 등록해야 합니다. 네트워크 관리자는 Network Identity 컴포넌트가 연결되어 있는 게임 오브젝트 프리팹만 수락할 수 있습니다. 따라서 프리팹을 네트워크 관리자에 등록하기 전에 Network Identity 컴포넌트를 추가해야 합니다.
에디터에서 프리팹을 네트워크 관리자에 등록하려면 네트워크 관리자 게임 오브젝트를 선택한 후 인스펙터에서 Network Manager 컴포넌트로 이동합니다. Spawn Info 옆에 있는 삼각형을 클릭하고, Registered Spawnable Prefabs에서 더하기(+) 버튼을 클릭합니다. 프리팹을 빈 필드에 드래그 앤 드롭하여 리스트에 할당합니다.
고급 사용자의 경우 NetworkManager 컴포넌트를 사용하지 않고 프리팹을 등록하고 게임 오브젝트를 스폰할 수도 있습니다.
네트워크 관리자를 사용하지 않고 게임 오브젝트를 스폰하려면 스크립트를 통해 프리팹 등록을 직접 처리하십시오. ClientScene.RegisterPrefab 메서드를 사용하여 네트워크 관리자에 프리팹을 등록할 수 있습니다.
using UnityEngine;
using UnityEngine.Networking;
public class MyNetworkManager : MonoBehaviour
{
public GameObject treePrefab;
NetworkClient myClient;
// Create a client and connect to the server port
public void ClientConnect() {
ClientScene.RegisterPrefab(treePrefab);
myClient = new NetworkClient();
myClient.RegisterHandler(MsgType.Connect, OnClientConnect);
myClient.Connect("127.0.0.1", 4444);
}
void OnClientConnect(NetworkMessage msg) {
Debug.Log("Connected to server: " + msg.conn);
}
}
이 예제에서는 네트워크 관리자로 동작할 빈 게임 오브젝트를 생성한 후 위의 MyNetworkManager
스크립트를 생성하여 해당 게임 오브젝트에 연결합니다. Network Identity 컴포넌트가 연결된 프리팹을 생성하여 인스펙터의 MyNetworkManager 컴포넌트에 있는 treePrefab 슬롯에 끌어다 놓으십시오. 이렇게 하면 서버가 나무 게임 오브젝트를 스폰할 때 클라이언트에도 같은 종류의 게임 오브젝트가 생성됩니다.
프리팹을 등록하면 에셋이 씬에 로드되므로 에셋 생성을 위한 지연 또는 로딩 시간이 필요하지 않습니다.
하지만 스크립트가 동작하려면 서버에 대한 코드도 추가해야 합니다. 다음을 MyNetworkManager 스크립트에 추가하십시오.
public void ServerListen() {
NetworkServer.RegisterHandler(MsgType.Connect, OnServerConnect);
NetworkServer.RegisterHandler(MsgType.Ready, OnClientReady);
if (NetworkServer.Listen(4444))
Debug.Log("Server started listening on port 4444");
}
// When client is ready spawn a few trees
void OnClientReady(NetworkMessage msg) {
Debug.Log("Client is ready to start: " + msg.conn);
NetworkServer.SetClientReady(msg.conn);
SpawnTrees();
}
void SpawnTrees() {
int x = 0;
for (int i = 0; i < 5; ++i) {
var treeGo = Instantiate(treePrefab, new Vector3(x++, 0, 0), Quaternion.identity);
NetworkServer.Spawn(treeGo);
}
}
void OnServerConnect(NetworkMessage msg) {
Debug.Log("New client connected: " + msg.conn);
}
서버는 어떤 게임 오브젝트가 스폰되는지 알고 에셋 ID가 스폰 메시지로 전송되기 때문에 아무것도 등록할 필요가 없습니다. 클라이언트는 게임 오브젝트를 검색하여 클라이언트에 등록할 수 있어야 합니다.
고유 네트워크 관리자를 작성할 때는 서버에서 스폰 커맨드를 호출하기 전에 클라이언트가 상태 업데이트를 받을 준비가 되어 있어야 합니다. 그렇지 않으면 상태 업데이트가 전송되지 않습니다. Unity의 빌트인 Network Manager 컴포넌트를 사용하면 이 작업이 자동으로 수행됩니다.
오브젝트 풀 또는 동적으로 생성된 에셋 등과 같은 고급 사용법의 경우 ClientScene.RegisterSpawnHandler 메서드를 사용하십시오. 이렇게 하면 콜백 함수가 클라이언트 쪽 스포닝을 위해 등록됩니다. 이에 관한 예제는 커스텀 스폰 함수에 대한 문서를 참조하십시오.
게임 오브젝트에 동기화된 변수 같은 네트워크 상태가 있는 경우 해당 상태는 스폰 메시지와 동기화됩니다. 다음 예제에서는 이 스크립트가 나무 프리팹에 연결되어 있습니다.
using UnityEngine;
using UnityEngine.Networking;
class Tree : NetworkBehaviour {
[SyncVar]
public int numLeaves;
public override void OnStartClient() {
Debug.Log("Tree spawned with leaf count " + numLeaves);
}
}
이 스크립트를 연결하면 numLeaves
변수를 수정하고 SpawnTrees
함수를 수정하여 클라이언트에 정확하게 반영되는지 확인할 수 있습니다.
void SpawnTrees() {
int x = 0;
for (int i = 0; i < 5; ++i) {
var treeGo = Instantiate(treePrefab, new Vector3(x++, 0, 0), Quaternion.identity);
var tree = treeGo.GetComponent<Tree>();
tree.numLeaves = Random.Range(10,200);
Debug.Log("Spawning leaf with leaf count " + tree.numLeaves);
NetworkServer.Spawn(treeGo);
}
}
실제 동작 방식을 보려면 Tree
스크립트를 앞서 생성한 treePrefab
스크립트에 연결하십시오.
NetworkIdentity는 스폰 가능한 프리팹의 루트 게임 오브젝트에 있어야 합니다. 그러지 않으면 네트워크 관리자가 프리팹을 등록할 수 없습니다.
NetworkBehaviour 스크립트는 NetworkIdentity와 동일한 게임 오브젝트에 있어야 하며, 해당 오브젝트의 자식 오브젝트에 있어서는 안됩니다.
게임 오브젝트를 스폰하기 위한 내부 작업 과정은 아래와 같습니다.
Network Identity 컴포넌트가 있는 프리팹이 스폰 가능한 오브젝트로 등록됩니다.
게임 오브젝트가 서버의 프리팹에서 인스턴스화됩니다.
게임 코드가 인스턴스의 초기 값을 설정합니다. 여기서 적용된 3D 물리 힘은 즉시 반영되지 않습니다.
NetworkServer.Spawn()
이 인스턴스와 호출됩니다.
Network Behaviour 컴포넌트의 OnSerialize()
호출에 의해 서버에 있는 인스턴스의 SyncVars 상태가 수집됩니다.
연결된 클라이언트에게 MsgType.ObjectSpawn
타입 네트워크 메시지가 SyncVar 데이터를 포함하여 전송됩니다.
OnStartServer()
가 서버의 해당 인스턴스에서 호출되고, isServer
가 true
로 설정됩니다.
클라이언트는 ObjectSpawn
메시지를 수신하고 등록된 프리팹에서 새로운 인스턴스를 생성합니다.
SyncVar 데이터는 Network Behaviour 컴포넌트에서 OnDeserialize()를 호출하는 방법으로 클라이언트의 새로운 인스턴스에 적용됩니다.
각 클라이언트의 인스턴스에서 OnStartClient()
가 호출되고, isClient
가 true
로 설정됩니다.
계속 게임플레이가 진행되며, SyncVar 값이 변하면 자동으로 클라이언트에 동기화됩니다. 이 과정은 게임이 끝날 때까지 계속됩니다.
서버의 인스턴스에서 NetworkServer.Destroy()
가 호출됩니다.
MsgType.ObjectDestroy
타입 네트워크 메시지가 각 클라이언트로 전송됩니다.
클라이언트의 인스턴스에서 OnNetworkDestroy()
가 호출된 후 인스턴스가 제거됩니다.
HLAPI에서 플레이어 게임 오브젝트는 비플레이어 게임 오브젝트와 약간 다르게 동작합니다. 네트워크 관리자에서 플레이어 게임 오브젝트의 생성 플로는 다음과 같습니다.
NetworkIdentity
가 있는 프리팹이 PlayerPrefab
으로 등록됩니다.
클라이언트가 서버에 연결됩니다.
클라이언트는 AddPlayer()
를 호출하고 MsgType.AddPlayer
타입 네트워크 메시지가 서버에 전송됩니다.
서버는 메시지를 수신하고 NetworkManager.OnServerAddPlayer()
를 호출합니다.
게임 오브젝트가 서버의 PlayerPrefab에서 인스턴스화됩니다.
서버의 새 플레이어 인스턴스와 함께 NetworkManager.AddPlayerForConnection()
이 호출됩니다.
플레이어 인스턴스가 스폰됩니다. 플레이어 인스턴스를 위해 NetworkServer.Spawn()
을 호출할 필요가 없습니다. 일반 스폰에서와 동일하게, 스폰 메시지가 모든 클라이언트에 전송됩니다.
플레이어를 추가한 클라이언트에 대해서만 MsgType.Owner
타입 네트워크 메시지가 전송됩니다.
기존 클라이언트가 네트워크 메시지를 수신합니다.
기존 클라이언트의 플레이어 인스턴스에서 OnStartLocalPlayer()
가 호출되고, isLocalPlayer
가 true로 설정됩니다.
플레이어 게임 오브젝트가 스폰된 이후에 소유권 메시지가 서버에서 전달되는 시점에서만 OnStartLocalPlayer()
가 발생하므로, OnStartClient()
이후에만 호출된다는 점을 상기해야 합니다. 따라서 isLocalPlayer
는 OnStartClient()
에서 설정되지 않습니다.
OnStartLocalPlayer
는 클라이언트의 로컬 플레이어 게임 오브젝트에 대해서만 호출되므로, 로컬 플레이어에 대한 초기화를 진행하기 적합합니다. 예를 들면, 입력 프로세싱 활성화, 플레이어 게임 오브젝트의 카메라 트래킹 활성화 등이 있습니다.
게임 오브젝트를 스폰한 후 해당 게임 오브젝트의 권한을 특정 클라이언트에 할당하려면 권한을 할당받는 클라이언트의 NetworkConnection
을 인수로 취하는 NetworkServer.SpawnWithClientAuthority를 사용하십시오.
이 게임 오브젝트의 경우 hasAuthority
프로퍼티는 권한이 있는 클라이언트에서는 true가 되며 권한이 있는 클라이언트에서 OnStartAuthority()
가 호출됩니다. 해당 클라이언트는 해당 게임 오브젝트에 대한 커맨드를 내릴 수 있게 됩니다. 다른 클라이언트와 호스트에서 hasAuthority
는 false입니다.
클라이언트 권한을 가지고 스폰된 오브젝트는 반드시 NetworkIdentity
에 LocalPlayerAuthority
가 설정되어 있어야 합니다.
예를 들어, 위의 나무 스폰 예제를 수정하여 나무가 아래와 같이 클라이언트 권한을 가지도록 허용할 수 있습니다. 단, 이제는 소유 클라이언트 연결을 위해 NetworkConnection 게임 오브젝트를 전달해야 합니다.
void SpawnTrees(NetworkConnection conn) {
int x = 0;
for (int i = 0; i < 5; ++i)
{
var treeGo = Instantiate(treePrefab, new Vector3(x++, 0, 0), Quaternion.identity);
var tree = treeGo.GetComponent<Tree>();
tree.numLeaves = Random.Range(10,200);
Debug.Log("Spawning leaf with leaf count " + tree.numLeaves);
NetworkServer.SpawnWithClientAuthority(treeGo, conn);
}
}
다음과 같이 나무 스크립트를 수정하여 서버에 커맨드를 보낼 수 있습니다.
public override void OnStartAuthority() {
CmdMessageFromTree("Tree with " + numLeaves + " reporting in");
}
[Command]
void CmdMessageFromTree(string msg) {
Debug.Log("Client sent a tree message: " + msg);
}
단, CmdMessageFromTree
호출을 OnStartClient
에 추가할 수는 없습니다. 해당 시점에서는 권한이 아직 설정되지 않았기 때문에 호출이 실패합니다.