Version: 2017.2
네트워크 관리자 사용(Using the Network Manager)
커스텀 스폰 함수

오브젝트 스포닝

Unity에서 Instantiate()로 새로운 게임 오브젝트를 생성하는 것을 “스포닝”이라고 부르기도 합니다. 네트워크 HLAPI에서 “스폰”은 더 세부적인 의미를 가집니다. 네트워크 HLAPI의 서버 권한 모델에서, 서버에서 오브젝트를 “스폰” 하게되면 오브젝트가 서버에 연결된 클라이언트에서 생성되어야 하며 이들 오브젝트는 스포닝 시스템이 관리하게 된다는 의미입니다. 오브젝트가 스포닝 시스템에 속하게 된 후에는 오브젝트가 서버에서 변화가 있으면 클라이언트로 상태 업데이트가 전송됩니다. 오브젝트가 서버에서 제거되면 클라이언트에서도 동일하게 제거됩니다. 스폰된 오브젝트는 서버가 관리하는 네트워크 오브젝트 집합에도 추가되므로, 추후에 다른 클라이언트가 게임에 참여하더라도 오브젝트가 동일하게 해당 클라이언트에서도 스폰됩니다. 이러한 오브젝트는 각각 서버와 클라이언트에서 동일한 “netId”라고 하는 고유 네트워크 인스턴스 ID를 가집니다. 이 ID를 통해 오브젝트에 메시지를 보내고 각각 식별할 수 있습니다.

NetworkIdentity 오브젝트가 클라이언트에서 스폰될 때에는 서버상의 해당 오브젝트의 현재 상태를 따르면서 생성됩니다. 이는 오브젝트의 변환, 이동 상태, 동기화된 값들에도 동일하게 적용됩니다. 따라서 클라이언트 오브젝트는 생성되는 시점에서 항상 최신 상태가 됩니다. 이를 통해 오브젝트가 잘못된 초기 위치에 스폰되고 상태 업데이트 패킷이 도착하는 시점에서 올바른 포지션으로 갑자기 이동해버리는 현상 등을 방지할 수 있습니다.

겉보기에는 좋아보이지만, 바로 떠오르는 의문점이 있을 수 있습니다. 오브젝트는 어떻게 클라이언트에서 생성되며, 만일 스폰되는 시점과 다른 클라이언트가 연결되는 시점 사이에서 오브젝트가 변경되면 어떤 현상이 발생하는지, 또는 그러한 경우 새로운 클라이언트의 입장에서는 어떤 오브젝트가 생성될 지 등이 있습니다.

클라이언트에서 오브젝트를 스폰하면 서버의 NetworkServer.Spawn에 전달된 오브젝트 프리팹으로부터 클라이언트 오브젝트를 인스턴스화합니다. NetworkIdentity 인스펙터 프리뷰 패널은 NetworkIdentity의 에셋 ID를 표시합니다. ID는 클라이언트가 오브젝트를 생성할 수 있도록 프리팹을 식별하는 값입니다. 이 과정이 올바르게 수행되려면 클라이언트는 ClientScene.RegisterPrefab를 호출하여 클라이언트 오브젝트가 생성될 에셋이 무엇인지 시스템에 전달하는 과정을 거쳐야 합니다.

스폰 프리팹 등록은 에디터의 NetworkManager에서 가장 쉽게 할 수 있습니다. NetworkManager의 “스폰 정보” 섹션을 활용하면 코드를 작성하지 않고도 프리팹을 등록할 수 있습니다. 코드를 통해 등록하려면 NetworkClient이 생성되는 시점에서 하면 됩니다. 아래는 그 예제입니다.

using UnityEngine;
using UnityEngine.Networking;

public class MyNetworkManager : MonoBehaviour 
{
    public GameObject alienPrefab;
    
    NetworkClient myClient;

    // Create a client and connect to the server port
    public void SetupClient()
    {
        ClientScene.RegisterPrefab(alienPrefab);

        myClient = new NetworkClient();

        myClient.RegisterHandler(MsgType.Connect, OnConnected);
        myClient.Connect("127.0.0.1", 4444);
    }
}

위의 예제에서 사용자는 MyNetworkManager 스크립트의 alienPrefab 슬롯에 프리팹 에셋을 드래그하면 됩니다. 외부 오브젝트가 서버에서 스폰되는 시점에서 동일한 유형의 오브젝트가 클라이언트에서도 생성됩니다. 에셋을 이렇게 등록하면 씬과 동시에 로드되도록 하게되며 에셋이 생성되는 시점에서 로드할 필요가 없게 됩니다. 오브젝트 풀이나 동적 생성 에셋과 같은 고급 기능을 사용하려면, 클라이언트측 스포닝에 콜백 함수를 등록할 수 있도록 ClientScene.RegisterSpawnHandler를 사용하면 됩니다.

아래 예제에서는 나뭇잎의 개수가 무작위인 나무를 생성합니다.

class Tree : NetworkBehaviour
{
    [SyncVar]
    public int numLeaves;
}

class MySpawner : NetworkBehaviour
{
    public GameObject treePrefab;

    public void Spawn()
    {
        GameObject tree = (GameObject)Instantiate(treePrefab, transform.position, transform.rotation);
        tree.GetComponent<Tree>().numLeaves = Random.Range(10,200);
        NetworkServer.Spawn(tree);
    }
}

위 예제를 완성하려면 프로젝트에 Tree 스크립트와 NetworkIdentity 컴포넌트가 있는 나무 프리팹 에셋이 있어야 합니다. 그래야만 씬의 MySpawner 인스턴스에서 treePrefab 슬롯이 나무 프리팹 에셋으로 채워지게 됩니다. 또한, 나무 프리팹은 스폰 가능한 오브젝트로 등록하여야 합니다. 이는 NetworkManager UI를 사용하거나 코드에서 ClientScene.RegisterPrefab()를 사용하면 됩니다.

코드가 실행되면 클라이언트에서 생성된 나무 오브젝트는 서버의 numLeaves 값과 동일한 값을 가지게 됩니다.

제약(Constraints)

  • NetworkIdentity는 스폰 가능한 프리팹의 루트 게임 오브젝트에 존재해야 합니다.
  • NetworkBehaviour 스크립트는 NetworkIdentity와 동일한 게임 오브젝트에 있어야 하며, 해당 오브젝트의 자식 오브젝트에 있어서는 안됩니다.
  • 프리팹은 해당 루트 오브젝트에 NetworkIdentity가 없는 한 NetworkManager에 등록할 수 없습니다.

오브젝트 생성 과정

실제 스포닝 작업 과정은 아래와 같습니다.

  • NetworkIdentity 컴포넌트가 있는 프리팹이 스폰 가능한 오브젝트로 등록됩니다.
  • 게임 오브젝트가 서버의 프리팹에서 인스턴스화됩니다.
  • 게임 코드가 인스턴스의 초기 값을 설정합니다. 여기서 적용된 3D 물리 힘은 즉시 반영되지 않습니다.
  • NetworkServer.Spawn()이 인스턴스와 호출됩니다.
  • NetworkBehaviour 컴포넌트의 OnSerialize() 호출에 의해 서버에 있는 인스턴스의 SyncVar 상태가 수집됩니다.
  • 연결된 클라이언트에게 MsgType.ObjectSpawn 타입 네트워크 메시지가 SyncVar 데이터를 포함하여 전송됩니다.
  • OnStartServer()가 서버의 해당 인스턴스에서 호출되고, isServer가 true로 설정됩니다.
  • 클라이언트는 ObjectSpawn 메시지를 수신하고 등록된 프리팹에서 새로운 인스턴스를 생성합니다.
  • SyncVar 데이터는 NetworkBehaviour 컴포넌트에서 OnDeserialize()를 호출하는 방법으로 클라이언트의 새로운 인스턴스에 적용됩니다.
  • 각 클라이언트의 인스턴스에서 OnStartClient()가 호출되고, isClient가 true로 설정됩니다.
  • 계속 게임이 진행되며, SyncVar 값이 변하면 자동으로 클라이언트에 동기화됩니다. 이 과정은 게임이 끝날 때까지 계속됩니다.
  • 서버의 인스턴스에서 NetworkServer.Destroy()가 호출됩니다.
  • MsgType.ObjectDestroy 타입 네트워크 메시지가 각 클라이언트로 전송됩니다.
  • 클라이언트의 인스턴스에서 OnNetworkDestroy()가 호출된 후 인스턴스가 제거됩니다.

플레이어 오브젝트(Player Objects)

네트워크 HLAPI에서 플레이어 오브젝트는 특별하게 취급됩니다. NetworkManager를 통해 플레이어 오브젝트를 스폰하는 과정은 아래와 같습니다.

  • 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가 설정되어 있어야 합니다.

아래는 플레이어가 오브젝트를 스폰하고 제어할 수 있도록 하는 예제입니다.

[Command]
void CmdSpawn()
{
    var go = (GameObject)Instantiate(
       otherPrefab, 
       transform.position + new Vector3(0,1,0), 
       Quaternion.identity);
       
    NetworkServer.SpawnWithClientAuthority(go, connectionToClient);
}
네트워크 관리자 사용(Using the Network Manager)
커스텀 스폰 함수