FishNet Prediction, Reconcile (2)

walnut_mr·2026년 1월 21일

Unity 개념 정리

목록 보기
6/6
post-thumbnail

FishNet 공식 문서를 참고하여 정리한 글 입니다.
https://fish-networking.gitbook.io/docs/guides/features/prediction


멀티플레이 게임에서의 캐릭터 조작 및 움직임에 대한 기본 프로세스 개념에 대한 정리 글은 아래 링크를 참고하시면 됩니다.

https://velog.io/@khkim09/FishNet-Prediction-Reconcile


1) State Forwarding 개념

결론부터 말하면 이거다.

"State Forwarding"
- 관전자(Observer) Client에 대해 Replicate(입력 기반 로직)를 직접 돌리게 할 것인가? (ON)
- Replicate 직접 돌리는 것을 금지한다. 다른 방식(NetworkTransform, NetworkAnimator)을 통해 서버 결과 동기화 해라 (OFF)


State는 그럼 뭐지?

State Buffer, Reconcile, re-run past states 등 전부 Prediction 시스템 상태를 말한다.

1) Replicate (입력 기반 Tick 시뮬)를 돌리기 위해 필요한 입력/상태 흐름
2) Tick 단위로 저장되는 Buffer
3) 오차를 고치는 Reconcile
4) 과거의 Tick ~ 현재 까지 다시 돌리는 Replay

Enable State Forwarding의 역할은 그럼 뭐지?
(왜 중요하고, 무엇이 달라지나)

FishNet 공식 문서 [클릭]에 따르면,

State forwarding ON
= 같은 입력(Inputs)이 서버뿐 아니라 모든 클라이언트에서도 실행되게” 한다.
(Owner + Server + Observer Clients)

장점:

관전자 클라도 ‘입력 기반’으로 더 동일한 로직을 돌릴 수 있어서,
“애니/이펙트/행동 동기화”가 더 일관되고 코딩이 쉬워질 수 있음.


단점/비용:

CPU/버퍼 비용 증가(상태 버퍼 유지 + reconcile + 과거 상태 재실행 필요)

State forwarding OFF
= 입력 로직은 OwnerServer만 실행한다.

그래서 관전자 클라에게 꼭 보여줘야 하는 것(총소리, 발사 이펙트, 애니메이션 트리거 등)은

"NetworkAnimator"를 쓰거나, "RPC"로 이벤트를 따로 전달해야 한다.

이동도 입력 기반으로 관전자에서 돌지 않으니,
보통 "NetworkTransform" (또는 예측용 설정) 같은 보조 동기화가 필요해질 수 있음.

< 정리 >

State forwarding은 “관전자도 입력 시뮬레이션 (Replicate)을 공유할래?”를 켜는 스위치이다.
OFF면 “결과(상태/이벤트) 전달을 위한 구현을 더 많이 따로 해줘야” 하고,
ON이면 “코드 동일성/일관성은 좋아지지만 CPU 비용이 늘어”난다.


2) Enable State Forwarding (ON/OFF) 차이

다시 한 번 말하지만, State Forwarding이라는 것 자체가 관전자 클라(Observer Clients)에게 직접 Replicate 하도록 할지 결정하는 것이기 때문에, Owner + Server는 이 기능의 ON/OFF에 대해 실행 로직에 큰 차이가 없다.

State Forwarding ON/OFF에 대해 OwnerObserver Client 입장에서 차이를 알아보자.

  1. OFF - Observer ClientReplicate 직접 금지

Owner (소유자 클라)

- 입력 생성 (Input Data)
- 자기 로컬에서 Replicate 실행(예측) - Owner Replicate
- 서버로 입력을 보냄
- 서버에서 온 Reconcile로 보정(필요 시 재실행)

Server

- owner가 보낸 입력으로 authoritative Replicate 실행 (정답 생성)
- authoritative 결과로 Owner에게 Reconcile(정답/보정 정보) 전송

Observer (관전자 클라)

- Replicate를 “입력 기반으로” 실행하지 않는다.
- 그래서 Observer가 그 오브젝트(다른 플레이어)의 움직임을 보려면, 
  서버가 움직임(위치/회전)을 따로 동기화해야 한다.
- 애니/사운드 같은 “표현”도 입력 기반으로 Observer가 재현하지 못함.

> 해결 방법 : NetworkAnimator를 쓰거나 RPC로 총소리 같은 걸 따로 보내야 한다.

< 정리 >

Enable State Forwarding - `OFF`

Owner: Replicate + Reconcile(예측/보정)
Observer: Replicate 안 함 → 서버 결과를 NetworkTransform/Animator/RPC 등으로 “받아 보기만 함”

ON - Observer Client에서도 직접 Replicate

Owner (소유자 클라)

OFF와 동일: 입력 생성 → 로컬 Replicate → 서버 전송 → Reconcile 보정

Server

OFF와 동일: authoritative Replicate → (정답 상태) → (필요한 대상에게) 전달

Observer (관전자 클라)

Observer도 그 오브젝트에 대해 Replicate를 실행할 수 있게 된다. (Inputs Run 가능)

그런데 Observer는 그 오브젝트의 입력을 “로컬에서 만들어내는 주체 (=Owner)”가 아니잖아?

그래서 서버가 Observer에게 예측에 필요한 state(입력/정정/버퍼 흐름)를
전달(forward) 해줘야 Observer가 Replicate를 돌릴 수 있는 것!!

즉, observer 쪽에서도 “state buffer를 유지”해야 하고
“reconcile로 수정”하고, “past states를 다시 실행”하는 비용이 생긴다.

→ CPU 비용 증가

< 정리 >

Enable State Forwarding - `ON`

Owner: Replicate + Reconcile
Observer: (서버가 forward 해준 state/입력 흐름을 바탕으로) Replicate도 실행
			+ 필요 시 Reconcile/재실행으로 보정

3) 표로 간단 비교 정리

항목State Forwarding OFFState Forwarding ON
OwnerReplicate 실행 + 서버 Reconcile로 보정동일
ObserverReplicate 안 함
→ NetworkTransform/Animator/RPC로 “결과” 받음
서버가 forward한 state로 Replicate 실행 가능.
Buffer/Reconcile/재실행 비용 발생
Movement Sync"movement not forwarded”
→ NetworkTransform 필요
입력 기반으로 관전자도 같은 로직 실행 가능
표현(애니/총소리 등)따로 forward 필요(NetworkAnimator/RPC)입력 기반으로 동일 코드 실행 가능
(단 결과 기반 FX는 여전히 서버 이벤트 필요)

4) 구현

Prediction은 ReplicateReconcile method 구현으로 끝이다.


데이터 구조체 구현

public struct ReplicateData : IReplicateData
{
    public bool Jump;
    public float Horizontal;
    public float Vertical;

    private uint _tick;
    public void Dispose() { }
    public uint GetTick() => _tick;
    public void SetTick(uint value) => _tick = value;
}

public struct ReconcileData : IReconcileData
{
    public PredictionRigidbody PRB;

    private uint _tick;
    public void Dispose() { }
    public uint GetTick() => _tick;
    public void SetTick(uint value) => _tick = value;
}

이와 같이 Server-Client 간 데이터 통신을 위한 Replicate DataReconcile Data 생성이 필요하다.

Prediction Method 호출 준비

1) PredictionRigidbody 생성 및 초기화

// FishNet 공식 문서 코드
private PredictionRigidbody prb;

private void Awake()
{
	prb = ObjectCaches<PredictionRigidbody>.Retrieve();
	prb.Initialize(GetComponent<Rigidbody>());
}

// ObjectCaches 사용 불가 시 코드
private PredictionRigidbody prb;

private void Awake()
{
	rb = GetComponent<Rigidbody>();
	prb = new PredictionRigidbody();
	prb.Initialize(rb);
}

Prediction을 이용하기 위해서는 이 PredictionRigidbody가 반드시 필요하다.
FishNet 공식문서에서는 ObjectCachesPredictionRigidbody에 접근하지만, ObjectCaches에 접근하지 못하는 문제로, new ()를 통해 직접 접근했다.

2) TimeManager.OnTick, OnPostTick 구현

다음 작업은 TimeManager의 API를 이용해서 Tick단위로 Replicate를 호출, Reconcile을 전송하는 단계이다. Replicate를 Tick 단위로 실행시키고, Reconcile을 전송하기 위해서는 OnTick과 OnPostTick에 구현한 함수 연결이 필요하다.

// 네트워크 연결 시 Tick 기반 연결
public override void OnStartNetwork()
{
    base.OnStartNetwork();
    TimeManager.OnTick += OnTick;
    TimeManager.OnPostTick += OnPostTick;
}

public override void OnStopNetwork()
{
    TimeManager.OnTick -= OnTick;
    TimeManager.OnPostTick -= OnPostTick;
}

Rigidbody와 같은 물리 객체를 사용할 경우, 물리 시뮬레이션이 완료된 후 state를 전달하기 위해 OnPostTick에서 Reconcile(보정)하는 것이 좋다. 물리 객체를 사용하지 않을 경우, OnTickReconcile 데이터를 보내도 좋다. (물리 시뮬레이션의 결과를 기다릴 필요가 없기 때문)


Prediction Method 호출

1) OnTickReplicate Data

이제 OnTick을 사용하여 Replicate Data를 생성해야 한다. (CreateReplicate()라는 별도의 method는 데이터 생성에는 필요하지 않지만, 더 효율적인 구성을 위한 작업. - Observer, Owner 무관하게 모두 같은 파이프라인으로 들어가도록)

private void OnTick()
{
	RunInputs(CreateReplicateData());
}

private ReplicateData CreateReplicateData()
{
	// Observer일 경우 default 반환
    if (!base.IsOwner) return default;

    float horizontal = Input.GetAxisRaw("Horizontal");
    float vertical = Input.GetAxisRaw("Vertical");
    ReplicateData md = new ReplicateData(_jump, horizontal, vertical);
    _jump = false;

    return md;
}

관전자 (Observer) Client일 경우 객체의 소유자가 아니기 때문에 return default;를 통해 반드시 기본값을 반환해야 한다.


2) Replicate method 구현

[Replicate]
private void RunInputs(ReplicateData data, ReplicateState state = ReplicateState.Invalid, Channel channel = Channel.Unreliable)
{
    // Be sure to always apply and set velocities using PredictionRigidbody
    // and never on the rigidbody itself; this includes if also accessing from
    // another script.
    Vector3 forces = new Vector3(data.Horizontal, 0f, data.Vertical) * _moveRate;
    prb.AddForce(forces);

    if (data.Jump)
    {
        Vector3 jmpFrc = new Vector3(0f, _jumpForce, 0f);
        prb.AddForce(jmpFrc, ForceMode.Impulse);
    }
    
    // 중력을 강하게 하면 더 빠르게 낙하
    prb.AddForce(Physics.gravity * 3f);
    prb.Simulate();
}

Replicate method를 필요에 맞게 구현해준다. 매개변수는 전달되는 값 (ReplicateData), 나머지는 run-time에 설정되는 값들이다. 공식 문서에도 나와있듯이 반드시 Rigidbody가 아닌 PredictionRigidbody로 접근하여 속도를 조정해야 한다. 모든 물리 작업을 마친 후 Simulate()를 호출하여 실제로 시뮬레이션하라는 명령을 내려줘야 한다.


3) OnPostTickReconcile Data
이제 수정 작업을 위해 ClientReconcile할 내용을 보내야 한다. 실제로 Reconcile을 보내는 주체는 Server이지만, Owner, Server, Observer Client 관계없이 반드시 CreateReconcile()을 호출해야한다.

CreateReplicateData()와 달리 CreateReconcile() 호출은 선택 사항이 아닌 필수이다!!

private void TimeManager_OnPostTick()
{
    CreateReconcile();
}

public override void CreateReconcile()
{
    ReconcileData rd = new ReconcileData(prb);
    ReconcileState(rd);
}

**4) Reconcile method 구현**
[Reconcile]
private void ReconcileState(ReconcileData data, Channel channel = Channel.Unreliable)
{
    prb.Reconcile(data.prb);
}

Reconcile 구현은 매우 간단하다.

5) Components

Enable PredictionPrediction Type
- Enable Prediction: On
- Prediction Type은 본인 캐릭터의 타입에 맞게 설정
- Enable State Forwarding: On   -  Observer도 Input에 대해 Replicate)
                           OFF  -  NetworkTransform, NetworkAnimator 사용 / RPC

만약 OFF를 원할 경우, NetworkAnimator, NetworkTransform을 이용하여 전송 / RPC

  • NetworkTransform의 Authority 옵션은 "Tick Replay/Reconcile"을 사용하지 않고, Transform Snapshot(상태)를 누가 만들어서 누구에게 전송할지를 정하는 기능
- ON:  "Owner"가 로컬에서 Transform을 바꾸면 > "Server"로 올라가고 > "Observer Clients"에게 동기화
		= "Owner"가 만든다.
- OFF: "Server"가 Transform을 바꿀 때만 그 변화가 "Client"에 전송
		= "Server"가 만든다.

따라서, 추천 조합은

1) Prediction + State Forwarding: ON

- NetworkTransform이 필요없음 (Observer도 Replicate, Reconcile run)
- NetworkTransform 붙일 시 관할이 이중으로 잡히기 때문에 (Replicate, NetworkTransform) 주의

2) Prediction + State Forwarding: OFF + @

@ = "NetworkTransform" / "RPC" / "보간 직접 구현"
- 1) NetworkTransform 사용
	- 플레이어 이동이 단순, 관전자 부드럽게만 보이면 OK.
    	- Client Authoritative: ON                        (플레이어 직접 조작 시)
        - Client Authoritative: OFF + Send To Owner: ON   (서버 권위 이동 시)
- 2) Snapshot RPC/버퍼 보간 직접 구현
	- Observer에겐 kinematic + Snapshot Queue 보간으로 부드럽게 처리.
profile
https://github.com/khkim09

0개의 댓글