커스텀 스폰 함수
원격 액션

상태 동기화(State Synchronization)

상태 동기화는 서버에서 원격 클라이언트로 수행됩니다. 로컬 클라이언트는 서버와 씬을 공유하므로 데이터가 직렬화되지 않습니다. 로컬 클라이언트로 직렬화된 데이터는 중복됩니다. 하지만 SyncVar 후크는 로컬 클라이언트에서도 호출됩니다.

데이터는 원격 클라이언트에서 서버로 동기화되지 않으며, 이는 커맨드를 통해 이루어집니다.

SyncVars

SyncVars는 서버에서 클라이언트로 동기화되는 NetworkBehaviour 스크립트의 멤버 변수입니다. 오브젝트가 스폰되거나 새로운 플레이어가 진행 중인 게임에 참여하는 경우 현재 볼 수 있는 네트워크 오브젝트의 모든 SyncVar 최신 상태를 전달받습니다. 멤버 변수는 [SyncVar] 커스텀 속성을 사용하여 SyncVar로 변환됩니다.

class Player : NetworkBehaviour
{

    [SyncVar]
    int health;

    public void TakeDamage(int amount)
    {
        if (!isServer)
            return;

        health -= amount;
    }
}

SyncVar 상태는 OnStartClient()가 호출되기 이전에 클라이언트의 오브젝트에 적용되므로, 오브젝트 상태는 OnStartClient() 내부에서는 항상 최신 상태를 유지합니다.

SyncVar는 정수, 문자열, 플로트와 같은 기본 타입이거나 Vector3나 사용자가 정의한 구조체와 같은 Unity 타입일 수 있지만, SyncVar 구조체 업데이트는 구조체 변화에서 점진적으로 이루어지는 것이 아니라 한 번에 진행되도록 전송됩니다. 하나의 NetworkBehaviour 스크립트에는 최대 32개의 SyncVar가 있을 수 있으며, 여기에는 SyncList가 포함됩니다.

SyncVar 업데이트는 SyncVar 값이 변화하면 자동으로 서버가 진행하므로, SyncVar의 경우에는 수동으로 필드를 제거할 필요는 없습니다.

SyncLists

SyncList는 SyncVar와 유사하지만 개별 값이 아닌 값의 리스트라는 점이 다릅니다. SyncList 콘텐츠는 SyncVar 상태와 함께 초기 상태 업데이트 내용에 포함됩니다. SyncList는 SyncVar 속성을 필요로 하지는 않으며 특수 클래스에 속합니다. 아래는 기본 타입인 빌트인 SyncList 타입입니다.

  • SyncListString
  • SyncListFloat
  • SyncListInt
  • SyncListUInt
  • SyncListBool

사용자가 정의한 구조체의 리스트로 사용할 수 있는 SyncListStruct도 있습니다. SyncListStruct 파생 클래스를 사용한 구조체는 기본 타입, 배열, 일반적인 Unity 타입을 멤버로 포함할 수 있습니다만, 복잡한 클래스나 일반 컨테이너를 포함할 수는 없습니다.

SyncList 는 콜백이라는 SyncListChanged 델리게이트를 통해 리스트 콘텐츠가 변경되면 클라이언트에 알릴 수 있도록 합니다. 이 델리게이트는 발생한 작업의 타입과 작업 대상이었던 아이템의 색인을 가지고 호출됩니다.

public class MyScript : NetworkBehaviour
{
    public struct Buf
    {
        public int id;
        public string name;
        public float timer;
    };
            
    public class TestBufs : SyncListStruct<Buf> {}
    TestBufs m_bufs = new TestBufs();
    
    void BufChanged(SyncListStruct<Buf>.Operation op, int itemIndex)
    {
        Debug.Log("buf changed:" + op);
    }
    
    void Start()
    {
        m_bufs.Callback = BufChanged;
    }
}

커스텀 직렬화 함수

많은 경우 SyncVar를 통해서도 스크립트가 상태를 클라이언트에 직렬화하도록 할 수 있지만, 더 복잡한 직렬화 코드가 필요한 경우도 있습니다. SyncVar 직렬화에 사용되는 NetworkBehaviour 가상 함수를 개발자가 구현하여 커스텀 직렬화를 수행할 수도 있습니다. 이들 함수는 아래와 같습니다.

public virtual bool OnSerialize(NetworkWriter writer, bool initialState);
public virtual void OnDeSerialize(NetworkReader reader, bool initialState);

InitialState 플래그는 오브젝트가 처음으로 직렬화된 시점과 점진적 업데이트를 보낼 수 있는 시점을 구분하는 데 유용합니다. 오브젝트가 처음으로 클라이언트에 전송되는 시점에서는 전체 상태의 스냅샷을 포함해야 하지만, 그 이후 발생하는 업데이트의 경우 점진적 변화만을 전송하여 대역폭을 절약할 수 있습니다. SyncVar 후크 함수는 initialState가 true인 경우 호출되지 않으며, 점진적 업데이트의 경우에만 호출된다는 점을 상기해야 합니다.

어떤 클래스가 SyncVar가 있는 경우, 이들 함수의 구현이 클래스에 자동으로 추가됩니다. 따라서 SyncVar가 있는 클래스는 커스텀 직렬화 함수를 가질 수 없습니다.

OnSerialize 함수는 업데이트가 보내져야 한다는 점을 표시하기 위해 true를 반환해야 합니다. true를 반환하게 되면 스크립트의 잔류 부분이 0으로 설정되며, false를 반환하면 이는 변경되지 않습니다. 따라서 이 기능을 통해 스크립트에 대한 여러 변경점을 매 프레임마다 보내는 대신 시스템이 준비될 때까지 축적한 다음 한 번에 보낼 수 있습니다.

직렬화 과정

NetworkIdentity 컴포넌트가 있는 게임 오브젝트는 NetworkBehaviour에서 파생된 스크립트를 다수 가질 수 있습니다. 이들 오브젝트를 직렬화하는 과정은 아래와 같습니다.

서버 측:

  • 각 NetworkBehaviour는 더티 마스크가 있습니다. 이 마스크는 OnSerialize에서 syncVarDirtyBits으로서 사용할 수 있습니다.
  • NetworkBehaviour 스크립트의 각 SyncVar에 더티 마스크 비트가 하나씩 할당됩니다.
  • SyncVar의 값이 변경되면 더티 마스크에서 이 SyncVar에 해당하는 비트가 설정됩니다.
  • 또는 SetDirtyBit()를 호출하면 더티 마스크에 직접 작성할 수 있습니다.
  • 서버 업데이트 루프의 일부로서 NetworkIdentity 오브젝트가 서버에서 검사됩니다.
  • NetworkIdentity의 NetworkBehaviour 중 더티인 것이 있는 경우 해당 오브젝트에 대해 UpdateVars 패킷이 생성됩니다.
  • UpdateVars 패킷은 해당 오브젝트의 각 NetworkBehaviour에 OnSerialize를 호출함으로써 채워집니다.
  • 더티로 설정되지 않은 NetworkBehaviour는 자신의 더티 비트 패킷에 0을 작성합니다.
  • 더티로 설정된 NetworkBehaviour는 자신의 더티 마스크를 쓴 후, 변경된 SyncVars에 대한 값을 씁니다.
  • OnSerialize가 NetworkBehaviour에 대해 true를 반환하면 더티 마스크는 해당 NetworkBehaviour에 대해 초기화되며, 이 값이 변경될 때까지 다시 전송되지 않습니다.
  • 오브젝트를 찾고 있는 준비된 클라이언트들에 UpdateVars 패킷이 전송됩니다.

클라이언트 측:

  • 어떤 오브젝트에 대한 UpdateVars 패킷을 수신합니다.
  • 해당 오브젝트의 각 NetworkBehaviour 스크립트에 대해 OnDeserialize 함수가 호출됩니다.
  • 해당 오브젝트의 각 NetworkBehaviour 스크립트는 더티 마스크를 읽습니다.
  • NetworkBehaviour의 더티 마스크가 0 이면 OnDeserialize 함수는 더 이상 읽지 않고 반환합니다.
  • 더티 마스크가 0이 아닌 값이면 OnDeserialize 함수는 설정된 더티 비트에 대응하는 SyncVar 값을 읽습니다.
  • SyncVar 후크 함수가 있을 경우, 스트림으로부터 읽은 값을 가지고 이 함수를 호출합니다.

따라서 이 스크립트의 경우에는 다음과 같게 됩니다.

public class data : NetworkBehaviour
{

    [SyncVar]
    public int int1 = 66;

    [SyncVar]
    public int int2 = 23487;

    [SyncVar]
    public string MyString = "esfdsagsdfgsdgdsfg";
}

생성된 OnSerialize 함수는 아래와 같습니다.

public override bool OnSerialize(NetworkWriter writer, bool forceAll)
{
    if (forceAll)
    {
        // the first time an object is sent to a client, send all the data (and no dirty bits)
        writer.WritePackedUInt32((uint)this.int1);
        writer.WritePackedUInt32((uint)this.int2);
        writer.Write(this.MyString);
        return true;
    }
    bool wroteSyncVar = false;
    if ((base.get_syncVarDirtyBits() & 1u) != 0u)
    {
        if (!wroteSyncVar)
        {
            // write dirty bits if this is the first SyncVar written
            writer.WritePackedUInt32(base.get_syncVarDirtyBits());
            wroteSyncVar = true;
        }
        writer.WritePackedUInt32((uint)this.int1);
    }
    if ((base.get_syncVarDirtyBits() & 2u) != 0u)
    {
        if (!wroteSyncVar)
        {
            // write dirty bits if this is the first SyncVar written
            writer.WritePackedUInt32(base.get_syncVarDirtyBits());
            wroteSyncVar = true;
        }
        writer.WritePackedUInt32((uint)this.int2);
    }
    if ((base.get_syncVarDirtyBits() & 4u) != 0u)
    {
        if (!wroteSyncVar)
        {
            // write dirty bits if this is the first SyncVar written
            writer.WritePackedUInt32(base.get_syncVarDirtyBits());
            wroteSyncVar = true;
        }
        writer.Write(this.MyString);
    }

    if (!wroteSyncVar)
    {
        // write zero dirty bits if no SyncVars were written
        writer.WritePackedUInt32(0);
    }
    return wroteSyncVar;
}

그리고 OnDeserialize 함수는 아래와 같습니다.

public override void OnDeserialize(NetworkReader reader, bool initialState)
{
    if (initialState)
    {
        this.int1 = (int)reader.ReadPackedUInt32();
        this.int2 = (int)reader.ReadPackedUInt32();
        this.MyString = reader.ReadString();
        return;
    }
    int num = (int)reader.ReadPackedUInt32();
    if ((num & 1) != 0)
    {
        this.int1 = (int)reader.ReadPackedUInt32();
    }
    if ((num & 2) != 0)
    {
        this.int2 = (int)reader.ReadPackedUInt32();
    }
    if ((num & 4) != 0)
    {
        this.MyString = reader.ReadString();
    }
}

NetworkBehaviour에 마찬가지로 직렬화 함수가 있는 베이스 클래스가 있다면 베이스 클래스 함수 역시 호출되어야 합니다.

오브젝트 상태 업데이트를 위해 생성된 UpdateVar 패킷은 클라이언트로 전송되기 이전 버퍼에 축적될 수 있다는 점을 상기해야 합니다. 따라서 한 개의 전송 레이어 패킷에는 여러 오브젝트에 대한 업데이트를 포함할 수 있습니다.

커스텀 스폰 함수
원격 액션