Unity Netcode for Entities – Client / Server worlds

🔥 Client / Server worlds Setting

Netcode for Entities 1.5.0v

https://docs.unity3d.com/Packages/com.unity.netcode@1.5/manual/set-up-client-server-worlds.html

Netcode for Entities의 네트워킹 모델을 사용하여client 와 server를 설정합니다.

✅ 클라이언트 / 서버 World 네트워크 모델

https://docs.unity3d.com/Packages/com.unity.netcode@1.5/manual/client-server-worlds.html

Netcode for EntitiesClient와 Server의 로직을 각각 Client worldServer world로 분리하여 처리합니다.

월드(World)는 Unity의 ECS(Entity Component System) 개념으로, 엔티티와 시스템을 시스템 그룹(SystemGroup)으로 구성한 단위입니다.

기본적인 Client / Server 월드 외에도, 개발 중에 게임을 테스트할 수 있도록 씬 클라이언트(Thin Client) 기능도 지원합니다.


✨ 시스템 생성 및 업데이트 구성

기본적으로, 모든 시스템은 클라이언트/서버 월드의 SimulationSystemGroup에서 생성되고 업데이트됩니다.

하지만 특정 시스템을 클라이언트에만, 또는 서버에만 작동하게 만들고 싶을 때는 다음 두 가지 방법을 사용할 수 있습니다

1️⃣ 특정 시스템 그룹에 할당하기

시스템이 속할 시스템 그룹을 지정하면, 그 시스템 그룹이 존재하지 않는 월드에서는 자동으로 필터링됩니다.

[UpdateInGroup(typeof(GhostInputSystemGroup))]
public class MyInputSystem : SystemBase
{
  ...
}

📌 참고:

PresentationSystemGroup에 속한 시스템은 클라이언트 월드에서만 작동합니다.

Server / Thin client worlds에는 이 시스템 그룹이 생성되지 않기 때문입니다.

2️⃣ WorldSystemFilter 사용

시스템이 어떤 월드에서 작동할지 더 세부적으로 설정하고 싶다면 WorldSystemFilter 속성을 사용하세요.

// WorldSystemFilterFlags 옵션 
// - LocalSimulation: 오프라인 로컬 시뮬레이션 (Netcode 시스템 없음)
// - ServerSimulation: 서버 시뮬레이션
// - ClientSimulation: 클라이언트 시뮬레이션
// - ThinClientSimulation: 씬 클라이언트
// 속성을 지정하지 않으면, 기본적으로 시스템은 상위 그룹의 필터링 규칙을 따릅니다 (WorldSystemFilterFlags.Default)

[WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation)]
public class MySystem : SystemBase
{
  ...
}

✨ Client / Server worlds with bootstrapping

Netcode for Entities를 프로젝트에 추가하면, 기본 ClientServerBootstrap 클래스가 제공됩니다.

이 클래스는 게임 시작 시 클라이언트 및 서버 월드를 자동 생성합니다

public virtual bool Initialize(string defaultWorldName)
{
    CreateDefaultClientServerWorlds();
    return true;
}

자동 부트스트래핑은 에디터에서 Play 모드 진입 시 매우 유용합니다.

하지만, 실제 게임에서는 메인 메뉴와 같은 전처리 과정이 필요하므로 월드 생성을 지연시키거나 수동 제어할 필요가 있습니다.

예를 들어

  • “클라이언트-호스트 서버 실행” → 서버클라이언트 모두 생성
  • “전용 서버에 접속” → 클라이언트만 생성
0️⃣ Bootstrapping 커스터 마이징

기본 ClientServerBootstrap을 상속하여 원하는 방식으로 월드 생성을 제어할 수 있습니다

public class MyGameSpecificBootstrap : ClientServerBootstrap
{
    public override bool Initialize(string defaultWorldName)
    {
        // Netcode 없이 로컬 시뮬레이션 월드만 생성
        CreateLocalWorld(defaultWorldName);
        return true;
    }
}

버튼 클릭 등의 타이밍에 다음을 호출하여 월드 생성:

var clientWorld = ClientServerBootstrap.CreateClientWorld();
var serverWorld = ClientServerBootstrap.CreateServerWorld();

AutomaticThinClientWorldsUtility.NumThinClientsRequested = 10;
AutomaticThinClientWorldsUtility.BootstrapThinClientWorlds();

ClientServerBootstrap.CreateDefaultClientServerWorlds();

Netcode Sample https://github.com/Unity-Technologies/EntityComponentSystemSamples/blob/master/NetcodeSamples/README.md


✨ Client / Server 업데이트 주기

Netcode for Entities를 사용할 때, 서버는 항상 Fixed Timestep으로 업데이트됩니다.

이는 클라이언트 예측을 위한 기본적인 결정론 수준을 보장하고(엄격한 결정론은 아니지만), 물리 시스템의 안정성과 프레임 레이트 독립성을 보장하기 위함입니다.

또한 이 패키지는 프레임당 최대 Fixed Step 반복 횟수를 제한하여 서버가 단일 프레임을 시뮬레이션하는 데 몇 초가 걸리는 상태에 빠지지 않도록 합니다.

중요한 점은 서버의 고정 업데이트가 표준 Unity 업데이트 빈도 또 Unity 물리 시스템의 Fixed Timestep를 사용하지 않는다는 것입니다.

대신 자체적인 ClientServerTickRate.SimulationTickRate 를 사용합니다. – 고정된 틱(tick) 간격

(이것은 Unity.Physics가 사용 중이라면 정수 배수여야 합니다. ClientServerTickRate.PredictedFixedStepSimulationTickRatio를 참조하세요)

그러나 클라이언트는 동적 타임스텝으로 업데이트됩니다.

예측 코드만 예외로, 이는 항상 서버와 동일한 고정 타임스텝으로 실행되어 두 시뮬레이션 간의 결정론적 관계를 유지하려고 합니다.

전체 틱과 동기화되지 않은 리프레시 레이트에 대한 예측 처리 방법을 이해하려면 부분 틱을 참조하세요.

설정 예시:

https://docs.unity3d.com/Packages/com.unity.netcode@1.5/api/Unity.NetCode.ClientServerTickRate.html

class MyCustomClientServerBootstrap : ClientServerBootstrap
{
   override public void Initialize(string defaultWorld)
   {
       base.Initialise(defaultWorld);
       var customTickRate = new ClientServerTickRate();
       //run at 30hz
       customTickRate.simulationTickRate = 30;
       customTickRate.ResolveDefault();
       foreach(var world in World.All)
       {
           if(world.IsServer())
           {
              // 이 경우 서버에서만 생성되지만 클라이언트 세계에서도 동일한 작업을 수행할 수 있습니다
              var tickRateEntity = world.EntityManager.CreateSingleton(new ClientServerTickRate
              {
                  SimulationTickRate = 30;
              });
           }
       }
   }
}


// SimulationTickRate = 초당 몇 번의 시뮬레이션 틱을 실행할지 설정합니다. 기본값은 초당 60 틱입니다.
// NetworkTickRate = 서버가 클라이언트에게 스냅샷(snapshot)을 전송하는 빈도를 설정합니다. 기본값은 SimulationTickRate와 동일합니다.
⚠️ 성능 문제 방지

서버가 설정된 시뮬레이션 틱 레이트보다 느린 속도로 업데이트될 경우, 한 프레임에서 여러 개의 시뮬레이션 틱을 처리하려고 시도합니다.

예를 들어, 서버의 마지막 업데이트가 16ms가 아닌 50ms가 걸렸다면, 서버는 다음 프레임에서 이를 따라잡기 위해 약 3번의 시뮬레이션 스텝을 실행합니다 (16ms × 3 ≈ 50ms).

이러한 동작은 성능 저하를 악화시키는 악순환을 유발할 수 있습니다:

서버는 더 많은 시뮬레이션 스텝을 처리해야 하므로 점점 더 느려지고, 그 결과 틱 간격을 더 많이 놓치게 됩니다.

위와 같은 상황을 방지하기 위해 ClientServerTickRate에서는 다음과 같은 설정을 제공합니다

  • MaxSimulationStepsPerFrame
    서버가 한 프레임 내에 수행할 수 있는 최대 시뮬레이션 스텝 수를 제한합니다.
  • MaxSimulationStepBatchSize
    여러 틱을 하나의 배치(batch) 로 묶어 처리하도록 지시합니다.
    이때는 델타 타임(delta time)을 곱한 값으로 단일 스텝을 실행합니다.
    예: 2개의 스텝을 실행하는 대신, 1개의 스텝만 실행하되 델타 타임을 2배로 설정하여 처리합니다.

🚫 주의:
MaxSimulationStepBatchSize 에 의한 배칭 처리는 특정 조건에서만 작동하며, 자체적인 제한 사항이 있습니다.
게임 로직에서 “한 시뮬레이션 스텝 = 하나의 틱“이라고 가정하지 않도록 하며, TimeData.DeltaTime 값을 하드코딩하지 마십시오.
배칭이 발생하면 서버와 클라이언트 간의 시뮬레이션 정밀도가 달라져 예측 오류가 발생할 수 있습니다.

✏️ 프레임 속도 유지 방식 설정

서버가 유휴 상태일 때 프레임 속도를 어떻게 유지할지도 설정할 수 있습니다.

TargetFrameRateMode는 서버가 틱 레이트를 유지하기 위한 유휴 시간 소비 방식을 제어합니다.

  • BusyWait
    가능한 한 빠른 속도로 계속 실행 (CPU 사용량 높음)
  • Sleep
    Application.targetFrameRate에 맞춰 대기하여 CPU 부하를 줄임
  • Auto
    헤드리스(콘솔 없는) 서버에서는 Sleep을 사용하고, 그 외에는 BusyWait을 사용

✨ 월드 마이그레이션 (World Migration)

현재 사용 중인 월드를 파괴(destroy) 하고, 연결 상태를 유지한 채로 새로운 월드로 전환하고자 할 경우, DriverMigrationSystem을 사용할 수 있습니다.

이 시스템은 네트워크 전송(transport) 관련 정보를 저장 및 불러올 수 있게 해주며, 매끄러운 월드 전환을 가능하게 합니다.

public World MigrateWorld(World sourceWorld)
{
    DriverMigrationSystem migrationSystem = default;
    foreach (var world in World.All)
    {
        if ((migrationSystem = world.GetExistingSystem<DriverMigrationSystem>()) != null)
            break;
    }

    var ticket = migrationSystem.StoreWorld(sourceWorld);
    sourceWorld.Dispose();

    var newWorld = migrationSystem.LoadWorld(ticket);

    // NOTE: LoadWorld는 반드시 새 월드에 필요한 시스템을 추가하기 전에 호출해야 합니다!
    // 이유: LoadWorld는 NetworkStreamReceiveSystem이 올바른 드라이버를 불러오기 위해 필요한 
    // `MigrationTicket` 컴포넌트를 생성하기 때문입니다.

    return ClientServerBootstrap.CreateServerWorld(DefaultWorld, newWorld.Name, newWorld);
}

이 기능은 예를 들어 로비에서 인게임으로 넘어가거나, 씬을 바꾸면서도 연결을 유지해야 하는 경우에 매우 유용합니다.

➕ Additional resources


✅ 네트워크 프로토콜 검사

https://docs.unity3d.com/Packages/com.unity.netcode@1.5/manual/network-protocol-checks.html

클라이언트가 서버에 연결할 때, Netcode 프로토콜 버전(NetworkProtocolVersion) 을 주고받습니다.

이 프로토콜에는 다음과 같은 정보가 포함되어 있습니다:

  • Netcode 버전 (netcode version)
  • 게임 버전 (game version)
  • RPC 컬렉션 (RPC collection)
  • 직렬화된 컴포넌트 컬렉션 (erialized component collections)

서로 호환되지 않는 게임 버전 간의 연결을 사전에 차단하여 예측할 수 없는 동작을 방지합니다.

RPC 컬렉션은 모든 로드된 어셈블리에서 컴파일된 RPC들의 타입과 멤버를 기반으로 해시를 계산합니다.

Serialized Component 컬렉션 역시 Netcode for Entities가 수집한 고스트(Ghost) 컴포넌트를 기반으로 해시를 계산합니다.

이러한 타입 정보와 멤버들을 기반으로 계산된 해시값이 프로토콜의 일부로 사용됩니다.

Netcode for Entities는 기본적으로 이 해시값이 완전히 동일해야 통신이 가능하도록 설정되어 있습니다.

이렇게 하면 중간에 예외가 발생하는 것을 방지하고, 대역폭 최적화가 가능합니다.


✨ 개발 중 발생할 수 있는 문제

개발 중에는 호환 가능한 빌드조차도 이 검사 때문에 부적합한 버전으로 잘못 판단되는 경우가 있습니다.

예를 들어:

  • 독립 실행형 빌드와 Unity Editor에서 실행 중인 월드를 연결하려는 경우
  • Editor는 테스트용 어셈블리를 포함하고 있는데, 빌드에는 포함되어 있지 않을 경우
  • 이로 인해 해시가 일치하지 않아 연결이 끊어짐

이러한 엄격한 프로토콜 검사는 개발 중 테스트에 방해가 될 수 있으므로, 비활성화할 수 있습니다.

이 프로토콜 버전 오류가 발생하면 각 피어는 사용자 코드가 읽을 수 있는 를 통해 원격 피어와의 연결을 끊고 

NetworkStreamDisconnectReason.BadProtocolVersion, 이를 사용하여 플레이어에게 해당 빌드가 대상 원격 피어와 호환되지 않음을 알립니다.

개발 빌드에서는 패키지가 로컬 피어에 로드된 RPC 및 고스트 유형의 전체 목록과 정렬된 목록을 나타내는 오류 로그도 출력합니다.

유형 불일치 문제를 해결하려면 이러한 로그를 원격 피어에서 발생한 로그와 상호 참조하세요.

⚠️ 프로토콜 검사 해제

검사를 비활성화하려면, 다음과 같이 RpcCollection.DynamicAssemblyListtrue로 설정합니다:

[BurstCompile] // 선택 사항
[WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation | WorldSystemFilterFlags.ServerSimulation | WorldSystemFilterFlags.ThinClientSimulation)]
[UpdateInGroup(typeof(InitializationSystemGroup))]
[CreateAfter(typeof(RpcSystem))]
public partial struct SetRpcSystemDynamicAssemblyListSystem : ISystem
{
    public void OnCreate(ref SystemState state)
    {
        SystemAPI.GetSingletonRW<RpcCollection>().ValueRW.DynamicAssemblyList = true;
        state.Enabled = false;
    }
}

🚫 주의:

이 설정은 RpcSystem.OnUpdate가 실행되기 전에 적용되어야 하며,

RpcSystem.OnCreate 이후에 설정되어야 합니다.

클라이언트와 서버 모두 이 플래그 값을 동일하게 설정해야 연결이 가능합니다. (값이 다르면 프로토콜 해석 방식이 달라지기 때문)

이 기능을 활성화하면 다음과 같은 단점이 있습니다:

  • 각 RPC마다 6바이트의 오버헤드가 발생합니다 (ushort 인덱스 대신 해시값 전체를 전송)
  • 게임 도중, 클라이언트가 알 수 없는 RPC나 고스트 타입을 수신하게 되면
    → 런타임 오류 발생 후 즉시 연결이 끊어짐
    → 즉, 연결 시점이 아닌 게임 중간에 강제 종료될 수 있습니다

따라서 이 설정은 개발 중 테스트 목적으로 사용하는 것이 좋으며, 출시용 빌드에서는 권장되지 않습니다.

➕ Additional resources

댓글 달기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다

위로 스크롤