오브젝트 동기화의 구조

sync와 stateMachine 룸에서 오브젝트가 어떻게 동기화되고, 누가 무엇을 변경할 수 있는지

sync와 stateMachine 룸에서 오브젝트가 어떻게 동기화되고, 누가 무엇을 변경할 수 있는지를 설명합니다. 이 페이지의 내용은 게임 엔진과 관계없이 공통입니다. 엔진별 설정과 API는 Unity 클라이언트와 Godot 클라이언트를 참조해 주세요.

전체 구조

용어의미적이 10체 있는 경우의 예
공유 오브젝트룸이 만들고, 참가자 전원이 같은 상태를 참조하는 대상10체의 적
플레이어 오브젝트플레이어가 생성하고 소유하는 오브젝트. 소유자가 값을 변경한다각 플레이어의 캐릭터
오브젝트 ID룸 내의 하나의 실체를 나타내는 ID. 서버가 발행한다적마다 다른 10개의 ID
Prefab ID오브젝트의 종류를 나타내는 ID. 클라이언트에서 표시할 Prefab(Godot에서는 씬)을 선택하는 데 사용한다10체 모두 같은 ID
동기화 프로퍼티(필드)참가자에게 배포하는 값위치, HP

오브젝트의 종류에 따라 값을 변경할 수 있는 사람이 정해집니다.

종류값을 변경하는 사람예
플레이어 오브젝트소유자각 플레이어의 캐릭터
조작 권한을 대여하는 공유 오브젝트조작 권한을 가진 참가자(조작 권한)들고 옮기는 상자, 패들
서버 관리 공유 오브젝트룰만. 참가자는 조작을 전송한다적, 문
Simulated 오브젝트룰만. 소유자는 입력을 보낸다(틱)입력으로 움직이는 캐릭터

데이터는 다음 순서로 도착합니다.

  1. 룸에 참가하면 그 시점의 모든 오브젝트와 값(스냅샷)이 도착하고, 클라이언트가 표시용 인스턴스를 생성합니다.
  2. 이후에는 변경된 값만 도착하여 표시가 갱신됩니다.

전송 순서

RPC·생성·이벤트·프로퍼티 등의 전송은 접속 후라면 언제 호출해도 됩니다. 스냅샷 반영 전이나 생성 알림 안에서 호출한 것은 보낼 수 있게 된 시점에 호출한 순서대로 전송됩니다. 전송 순서는 모든 오브젝트와 룸 전체의 조작에 걸쳐 유지됩니다.

수신한 메시지에는 룸 서버의 시각이 붙어 있습니다. 클라이언트는 이를 사용하여 참가자에게 공통인 시각을 가지며, 다른 플레이어의 오브젝트를 수신 시각에 맞춰 부드럽게 움직입니다.

위치·회전

위치·회전은 표준 필드가 있어, 동기화 스키마가 없어도 동기화할 수 있습니다. 서버 측에서 동기화를 허가한 축만 클라이언트에서 선택합니다.

서버의 선언동기화할 수 있는 축단위 간격(표준)
RoomSync.Position위치 X / Y / Z0.01(게임의 거리 단위)
RoomSync.Rotation회전 X / Y / Z360 / 4096도
RoomSync.Transform위의 양쪽 모두위와 동일
  • 위치는 월드 좌표, 회전은 오일러 각으로 다룹니다. Scale은 동기화하지 않습니다.
  • 다른 클라이언트에서는 조금 과거(기본값 0.1초 전)의 자세를, 전후로 수신한 갱신 사이에서 보간하여 표시합니다. 일정 거리보다 떨어진 위치로는 보간하지 않고 이동시킬 수도 있습니다(리스폰 등).
  • 갱신이 멈추면 마지막으로 수신한 자세에서 멈춥니다. 앞으로의 위치를 예측하지는 않습니다.
  • 값은 단위 간격으로 반올림하여 전송합니다. 단위 간격보다 작은 흔들림은 전송되지 않습니다.

독자적인 값

HP나 개폐 상태 등의 독자적인 값은 룰에서 동기화 스키마로 선언하고, 생성된 .realtimeschema를 클라이언트에서 사용합니다. 선언 작성 방법은 동기화 스키마를 참조해 주세요.

  • 쓰는 측이 값을 읽으면 자신이 마지막으로 쓴 값(전송 전이라도)이 반환됩니다. 예를 들어 HP에 1을 더하는 쓰기를 전송 사이에 2번 하면 2가 증가합니다.
  • 룸에서 수신한 값만 읽는 방법(confirmed)도 있습니다.
  • 수신한 값을 표시에 전달하는 구조(Unity의 Property bindings, Godot의 property_bindings)는 표시를 바꿀 뿐입니다. 서버의 값은 바뀌지 않습니다. 서버가 관리하는 값은 조작을 전송하는 것으로 변경합니다.

조작 권한

서버가 「조작 권한을 대여한다」고 선언한 오브젝트는 조작 권한을 얻은 참가자만 값을 전송할 수 있습니다. 상자를 들고 옮기거나, 패들을 움직이는 등의 용도에 사용합니다.

  • 취득 요청은 비동기로 처리되므로, 요청한 직후에 조작할 수 있다고는 할 수 없습니다.
  • 다른 참가자가 조작 권한을 가지고 있을 때는 LeaseBusy로 거부됩니다. 거부된 경우에는 자동으로 재요청하지 않습니다.
  • 조작 권한에는 기한이 있으며, 클라이언트는 기한이 만료되기 전에 자동으로 갱신할 수 있습니다.
  • 조작할 수 있는 동안에만 이동이나 입력 처리를 활성화할 수 있습니다. 공유 오브젝트는 조작 권한이 있는 동안, 플레이어 오브젝트와 Simulated 오브젝트는 소유하고 있는 동안입니다.
  • 조작 권한을 기다리는 요청의 결과는 다음 중 하나입니다.
결과의미
성공조작할 수 있게 되었다
거부LeaseBusy: 다른 참가자가 조작 권한을 가지고 있다. Forbidden: 조작 권한을 취득할 수 없는 오브젝트(서버 관리 오브젝트나 다른 플레이어의 오브젝트)
철회요청을 철회했다(조작 권한의 반환, 표시의 비활성화, 오브젝트의 삭제)
  • 표시용 인스턴스가 비활성화되면 조작 권한은 자동으로 반환됩니다.
  • 조작 권한을 수동으로 취득·연장·해제하는 API(Acquire·Renew·Release)도 있습니다. 이는 룸의 응답을 그대로 반환할 뿐이며, 처리의 활성화나 자동 갱신은 하지 않습니다.
  • 조작 권한 보유자가 바뀔 때마다(획득·반환·기한 만료) 새 기간이 시작됩니다. 이전 기간에 보낸 값(UDP로 늦게 도착한 값)은 반영되지 않으며 오류도 되지 않습니다.

서버에 조작 전송하기

서버가 관리하는 오브젝트(적의 HP 등)는 클라이언트에서 값을 변경할 수 없습니다. 대신 「이 오브젝트에 method의 조작을 한다」는 요청(Interact)을 전송합니다. 요청은 룰의 Interact(GDScript에서는 on_interact)에 전달되고, 룰이 검증하여 값을 갱신합니다.

method는 룰에서 조작의 종류를 구분하기 위한 번호입니다. 룰이 [RoomAction] enum(GDScript에서는 ## @room_action)으로 이름을 붙였다면, 생성된 상수를 그대로 전달할 수 있습니다. 룰로 동작하는 룸에는 추가 인수를 전송할 수 없습니다.

조작 요청(과 조작 권한의 취득·연장·해제)은 한 번만 완료됩니다. 완료 상태는 다음 중 하나입니다.

상태의미
성공(Succeeded)수락되었다
거부(Refused)거부되었다. 사유는 결과 코드(아래 표)
미전송(NotSent)전송할 수 없었다
철회(Abandoned)응답이 오지 않게 되었다(접속이 종료되었거나, 재접속으로 룸에 다시 들어갔다)

대상을 없애는 조작(아이템 획득 등)도 룸이 오브젝트를 없앤 뒤 결과를 반환하므로 성공으로 완료됩니다.

결과 코드값의미
Success0수리되어 변경이 전원에게 배포되었다
MissingObject1대상 오브젝트가 없다
Forbidden2이 오브젝트에는 허가되지 않은 조작
LeaseBusy3다른 참가자가 조작 권한을 가지고 있다
StaleEpoch4조작 권한 상태가 바뀐 후의 오래된 요청
InvalidOperation5현재 상태에서는 받아들이지 않는 조작, 또는 룰의 실행 오류
AlreadyApplied6룰이 이미 적용 완료로 반환했다. 클라이언트에서는 성공으로 취급한다
StaleRequest7같은 요청 ID가 이미 처리 완료
룰의 거부 이유100 이상룰이 Step.Ignore로 반환한 값

룰이 Step.Ignore로 거부한 경우에는 그 이유의 수치가 그대로 전달됩니다. 룸의 결과와 구별할 수 있도록 룰 고유의 거부 이유에는 100 이상의 값을 사용해 주세요(오류 enum).

플레이어 오브젝트

플레이어가 조작하는 캐릭터 등, 플레이어가 소유하는 오브젝트는 클라이언트에서 생성을 요청하여 만듭니다. 생성 방법은 2가지입니다.

방법동작
즉시 만들기(Unity의 world.Instantiate, Godot의 world.instantiate)인스턴스를 즉시 반환하며, 자신의 소유로 사용할 수 있다. 이동이나 RPC는 룸이 생성을 확정할 때까지 보류되며, 확정 후에 전송된다. 확정될 때까지 움직인 위치는 유지된다. 동기화 값은 확정 후에 사용할 수 있다
확정을 기다려 만들기(world.Spawn / world.spawn)룸의 확정을 기다린 후 인스턴스를 만든다

룸이 생성을 거부하면(소유 수의 상한 64 초과나, 오브젝트 카탈로그를 설정한 룸에서 등록되지 않은 Prefab) 거부가 통지되고, 즉시 만든 인스턴스는 삭제됩니다.

  • 소유권은 다른 참가자에게 넘길 수 있습니다(플레이어 오브젝트만). 오브젝트 카탈로그에서 "ownership": "takeover"로 한 오브젝트는 누구나 소유권을 취득할 수 있습니다.
  • 소유자가 퇴장하면 그 플레이어 오브젝트는 삭제됩니다. 오브젝트 카탈로그에서 "keepOnLeave": true로 한 오브젝트는 마스터 클라이언트에게 인계됩니다.
  • 소유자가 바뀔 때마다 소유 기간이 새로 시작됩니다. 이전 소유 기간에 보낸 값(UDP로 늦게 도착한 값)은 같은 참가자가 소유권을 되찾은 뒤에도 반영되지 않습니다.

RPC

오브젝트의 메서드를 다른 클라이언트에서 호출합니다.

대상전달되는 상대
All호출한 플레이어를 포함한 전원
Others호출한 플레이어 이외
Owner오브젝트의 소유자
AllBuffered / OthersBuffered위에 더해, 나중에 참가한 플레이어에게도 전달된다. 오브젝트 삭제나 호출한 플레이어의 퇴장으로 파기된다
1명 지정지정한 접속 ID의 참가자
  • RPC의 ID는 메서드 이름으로부터 정해집니다. 메서드 이름을 바꾸면 ID도 바뀌므로, 오브젝트 카탈로그와 클라이언트를 맞추어 갱신해 주세요.
  • 누가 어떤 RPC를 호출할 수 있는지 제한하려면 오브젝트 카탈로그를 설정합니다. 설정하면 methods에 등록한 ID는 소유자만 호출할 수 있게 되며, 다른 플레이어의 오브젝트에도 호출하는 RPC는 publicMethods에 등록합니다.
  • 인수는 합계 4096바이트까지입니다. 마지막 인수로 호출자의 접속 ID 등을 받을 수 있습니다.
  • 수신한 RPC는 도착한 순서대로, 수신 처리를 마친 후에 호출됩니다. RPC 안에서 RPC를 전송하거나 값을 변경할 수 있습니다.
  • 공유 오브젝트에는 RPC를 전송할 수 없습니다. 조작 전송을 사용합니다.

룸 이벤트

오브젝트에 연결되지 않은 알림은 룸 이벤트로 전송합니다. 참가자라면 누구나 전송할 수 있습니다. 대상은 All / Others와 각각의 Buffered, 또는 1명입니다. Buffered 이벤트는 전송한 플레이어가 퇴장하면 파기됩니다.

커스텀 프로퍼티

룸이나 플레이어에 붙이는 값입니다. 나중에 참가한 플레이어에게도 전달됩니다.

  • 값으로 사용할 수 있는 타입은 RPC의 인수와 같습니다. null을 쓰면 삭제합니다.
  • 쓰기는 룸을 경유하여 전원에게 도착한 시점에 반영됩니다. 쓴 플레이어의 로컬에서도 마찬가지이므로, 전원이 같은 순서로 값을 봅니다.
  • 플레이어의 프로퍼티는 자신의 것만 쓸 수 있습니다. 플레이어가 퇴장하면 그 플레이어의 프로퍼티는 삭제됩니다.
  • 키는 64바이트까지, 룸 전체에서 1024건까지입니다.

마스터 클라이언트

룸은 참가자 중 1명을 마스터 클라이언트로 합니다. 처음 참가한 플레이어가 마스터가 되며, 마스터가 퇴장하면 남은 플레이어 중 가장 먼저 참가한 플레이어로 넘어갑니다. 마스터는 다른 참가자를 마스터로 지명할 수 있습니다.

게임 진행을 서버에서 결정하고 싶은 경우에는 마스터 클라이언트가 아니라 stateMachine 룰을 사용하면 클라이언트 개조의 영향을 받지 않습니다.

Interest Group

오브젝트를 그룹으로 나누어, 구독하고 있는 플레이어에게만 갱신을 전달합니다. 그룹은 소유자가 정하며, 그룹 0은 전원에게 전달됩니다.

  • 그룹으로 필터링되는 것은 값의 갱신과, 그 오브젝트에 대한 All / Others 대상 RPC입니다. 생성·삭제·소유자나 그룹의 변경은 전원에게 전달됩니다.
  • 구독하면 그 그룹 오브젝트의 현재 값이 즉시 도착합니다.
  • 구독은 재접속해도 유지됩니다. 1접속당 256그룹까지입니다.

틱과 예측

룸의 룰이 틱과 Simulated 오브젝트를 정의하는 경우, 그 종류의 오브젝트는 서버가 입력으로부터 움직입니다(틱). 클라이언트는 위치를 쓰지 않고 입력을 보냅니다.

  • 입력은 32개 정수 슬롯의 “현재 상태"입니다. 룸의 틱마다 전송됩니다. 범위를 지정한 슬롯은 룸이 범위 안으로 제한해서 읽습니다.
  • 자신이 소유한 Simulated 오브젝트가 있는 동안에만 보냅니다. 1회 전송에 직전 3틱 분량의 입력을 넣으므로, UDP에서 하나가 유실되어도 보완할 수 있습니다.
  • 클라이언트는 입력이 룸의 처리보다 조금(기본 2틱) 먼저 도착하도록, 입력에 붙이는 틱 번호를 룸보다 앞서 진행시킵니다. 룸에서 오는 시각을 보고 진행 속도를 ±10% 이내에서 자동 조정합니다.
  • Simulated 오브젝트는 소유자라도 값을 쓰거나 삭제할 수 없습니다. 삭제는 룰의 Despawn이나 소유자의 퇴장으로 이루어집니다.

룰에 예측 처리(예측)가 있으면 컴파일러가 클라이언트용 예측 코드를 출력합니다. 등록하면 다음과 같이 동작합니다.

  • 자신이 소유한 Simulated 오브젝트는 입력한 틱의 결과를 바로 표시합니다. 왕복 통신 시간을 기다리지 않습니다.
  • 룸의 계산 결과가 도착할 때마다 그 결과에서 현재 틱까지 다시 계산합니다(롤백). 예측이 빗나갔을 경우에는 차이를 약 0.1초에 걸쳐 눈에 띄지 않게 좁힙니다.
  • 다른 플레이어의 캐릭터는 룸의 결과를 보간하여 표시합니다.
  • 예측 안에서 생성된 오브젝트(예측 스폰)는 먼저 표시되며, 룸의 오브젝트가 도착하면 같은 인스턴스로 인계됩니다. 예측으로 삭제된 오브젝트는 룸이 삭제할 때까지 그려지지 않습니다.
  • 입력에는 그때 다른 오브젝트가 화면에서 얼마나 늦게 표시되고 있었는지가 붙습니다. 룸은 이것을 사용하여 쏜 쪽의 화면대로 판정합니다(지연 보상). 정확하게 계산하려면 클라이언트에 서버 시각을 전달하고, 표시 지연(View Delay)을 위치 보간 지연과 같은 값(기본 0.1초)으로 합니다.

UDP

위치 등의 정기적인 갱신을 UDP로 전송하면 통신이 막혔을 때의 지연을 줄일 수 있습니다. 생성·RPC·이벤트·프로퍼티 등 도착하지 않으면 곤란한 것은 WebSocket 그대로입니다.

  • 클라이언트는 룸으로의 등록과 등록 유지를 수행합니다. 룸으로부터 응답이 있으면 UDP를 사용하고, 10초간 응답이 없으면 WebSocket으로 돌아갑니다.
  • UDP로 전송한 값은 룸에서 보낸 값이든 클라이언트에서 보낸 값이든 누락에 대비하여 1초마다 WebSocket으로 재전송됩니다. UDP가 한동안 도착하지 않더라도 통신이 회복되면 약 1초 이내에 최신 값이 됩니다.
  • 소유권을 넘기거나 조작 권한을 반환할 때는 그 전에 UDP로 보낸 값을 WebSocket으로 재전송합니다. 새로운 쓰기 주체는 최신 상태를 WebSocket으로 받은 뒤 쓰기를 시작합니다.
  • 상태 업데이트에는 보낸 순서의 번호가 붙습니다. UDP와 WebSocket으로 도착하는 순서가 바뀌더라도 오래된 값이 새 값을 덮어쓰지 않습니다. 예측에 사용하는 룸의 상태도 도착 순서가 아니라 이 번호로 구성합니다.
  • RPC 등의 직전에 전송하는 값은 WebSocket으로 전송하므로 RPC보다 먼저 도착합니다.
  • Web 브라우저(Unity의 WebGL, Godot의 Web 버전)에서는 UDP를 사용할 수 없습니다. WebSocket만으로 동작합니다.

오브젝트 카탈로그

룸이 플레이어 오브젝트의 종류, 필드, RPC, 소유권 요청을 검사하게 하려면 Realtime 네임스페이스 설정의 syncSetting.objectCatalog에 검사 기준을 JSON으로 설정합니다. 키는 Prefab ID입니다.

설정하지 않으면 룸은 이것들을 검사하지 않습니다. 등록되지 않은 Prefab도 생성 시 선언한 필드를 가진 플레이어 오브젝트로 생성할 수 있고, 어떤 RPC든 누구나 호출할 수 있으며, 누구나 소유권을 취득할 수 있습니다. 설정하면 등록한 Prefab, 필드, RPC만 통과합니다.

{
  "1": {
    "fields": 63,
    "mode": "player",
    "methods": [1],
    "publicMethods": [2],
    "ownership": "takeover",
    "keepOnLeave": true
  }
}
항목내용
fields동기화할 필드의 비트 마스크. 위치 X/Y/Z가 1/2/4, 회전 X/Y/Z가 8/16/32
mode플레이어 오브젝트는 "player"
methods소유자가 호출할 수 있는 RPC의 ID
publicMethods참가자 누구나 다른 플레이어의 오브젝트에 대해 호출할 수 있는 RPC의 ID
ownership"fixed"(기본값) 또는 "takeover"(누구나 소유권을 취득할 수 있다)
keepOnLeavetrue라면 소유자 퇴장 시 삭제하지 않고 마스터 클라이언트에게 인계한다. 카탈로그에 등록한 Prefab에서만 사용할 수 있다

직접 작성하는 대신 에디터에서 카탈로그의 JSON을 생성할 수 있습니다(Unity·Godot). 룰이 정의하는 Prefab(공유 오브젝트와 Simulated 오브젝트)은 생성되는 JSON에 포함되지 않습니다. 카탈로그와 룰 양쪽에서 같은 Prefab ID를 정의하면 룸의 초기화가 실패합니다.

오브젝트의 수명과 재접속

대상접속이 끊겼을 때재접속했을 때
서버가 만든 공유 오브젝트룸에 남는다스냅샷으로 그 시점의 값 그대로 다시 표시된다
자신의 플레이어 오브젝트삭제된다(keepOnLeave라면 마스터에게 인계된다)필요하면 다시 만든다
자신의 플레이어 프로퍼티·Buffered RPC와 이벤트삭제된다필요하면 다시 설정한다
룸의 프로퍼티룸에 남는다스냅샷으로 도착한다
가지고 있던 조작 권한반환된다필요하면 다시 취득한다
자신의 접속 ID무효가 된다새로운 ID가 발행된다

재접속할 때마다 도착한 스냅샷으로부터 표시를 다시 만듭니다. 클라이언트에서 룰이나 조작 이력을 다시 실행할 필요는 없습니다. 재접속 전에 기다리던 요청은 철회(Abandoned)로 완료됩니다.

같은 userId로 다시 접속하면 이전 접속은 퇴장한 것으로 취급됩니다.

룸 종료 조건과의 관계

룸은 참가자 전원으로부터 1분간 통신이 없으면 종료됩니다. sync와 stateMachine 룸에서는 접속을 유지하기 위한 Ping은 이 통신에 포함되지 않습니다.

클라이언트는 값이 바뀌었을 때만 전송합니다. UDP 등록 유지도 이 통신에 포함되지 않습니다. 따라서 전원이 조작하지 않고 기다리는 장면이 있는 게임에서는 그동안에도 룸이 유지되도록 정기적으로 조작을 전송하는 등의 설계가 필요합니다.

전송량 기준

동기화 데이터는 필드 이름을 전송하지 않고, 변경된 필드의 번호와 정수값만 전송합니다. 클라이언트는 일정 간격(기본값 20회/초)으로 변경을 모아서 전송합니다. 같은 값을 여러 번 쓰거나 이전에 전송한 값으로 되돌아가도 전송량은 늘어나지 않습니다.

위치의 단위 간격과 동기화할 축을 필요한 범위로 좁히면 작은 흔들림으로 인한 전송을 줄일 수 있습니다.

잘 동작하지 않을 때

엔진에 공통인 확인 사항입니다. 엔진별 확인 사항은 Unity 클라이언트와 Godot 클라이언트를 참조해 주세요.

증상확인할 사항
오브젝트가 표시되지 않는다접속 후에 스냅샷을 수신하고 있는지. 도착하는 Prefab ID가 클라이언트의 카탈로그에 등록되어 있는지
값의 크기나 위치가 어긋난다룰(GSL)과 동기화 스키마를 같은 룰 정의로부터 동시에 생성한 것으로 맞추었는지. 맞지 않으면 클라이언트가 “generated from other rules” 경고를 출력한다
조작이 거부된다거부 알림으로 거부된 커맨드, 대상 ID, 사유(Forbidden: 권한 없음, MissingObject: 오브젝트 없음, LimitReached: 상한, MethodNotAllowed: 카탈로그에 없는 RPC 등)를 확인한다
공격 등의 조작이 반영되지 않는다요청의 결과를 확인한다. 결과 코드 목록을 참조
RPC가 도착하지 않는다오브젝트 카탈로그를 설정한 경우, RPC의 ID가 카탈로그의 methods(다른 플레이어로부터 호출한다면 publicMethods)에 있는지. 메서드 이름을 바꾸면 ID도 바뀐다
생성한 오브젝트가 사라진다생성 거부의 결과를 확인한다. 소유 수의 상한(64)을 초과하지 않았는지. 오브젝트 카탈로그를 설정한 경우, Prefab이 "mode": "player"로 등록되어 있는지
UDP가 사용되지 않는다룸의 포트가 UDP로 열려 있는지. Web 브라우저에서는 UDP를 사용할 수 없다