빠른 시작 가이드
소개
Unreal Engine 5용 Quantum SDK는 결정적 예측-롤백 엔진인 Quantum C#을 완전히 재구상하고 C++로 재구현한 버전입니다.
결정적 예측/롤백 네트코드는 두 가지 기본 원리에 기반합니다:
- 동일한 입력이 주어졌을 때 항상 동일한 결과를 생성하는 크로스플랫폼 결정적 엔진 구성 요소(물리, 내비게이션, 수학 라이브러리 등). 특히 서버에서 확인된 입력(서버가 타이밍과 입력 스트림을 제어함)을 사용할 때 동일한 결과가 보장되어야 합니다.
- 로컬 게임 상태(Local Game State) 를 미리 예측하고, 서버에서 확인된 입력을 기반으로 이전의 검증된 상태로 빠르게 롤백 한 후, 안전하게 앞으로 진행할 수 있는 능력.
이 원칙들은 언어나 엔진에 종속되지 않기 때문에 Unreal용 Quantum에서도 동일하게 유지됩니다.
다만 Unreal의 개발 규칙과 패턴에 맞추기 위해 (Quantum C#/Unity 버전과 비교했을 때) 몇 가지 중요한 변경 사항이 추가되었습니다.
또한 부분 틱 예측(Partial Tick Prediction)과 같은, C# 버전에는 없던 고유한 기능도 새롭게 추가되었습니다.
(뷰 보간을 위한 1틱 인터폴레이션을 사용할 필요가 없습니다.)
이 문서는 Unreal 5 프로젝트에서 Quantum을 사용하여 개발을 시작하기 위한 빠른 참조 가이드입니다.
설정
Quantum Unreal을 새 프로젝트에 설정하려면:
- Unreal 5에서 Unreal Project Browser를 열고 C++ 프로젝트를 생성합니다.
- Unreal 프로젝트 폴더 안에
Plugins폴더를 만듭니다. - Quantum Unreal Plugin을 다운로드합니다.
- Quantum Unreal Plugin의 압축을 풀고,
PhotonQuantum폴더를 새로 만든Plugins폴더 안으로 복사합니다. - 프로젝트의
.uproject파일을 텍스트 편집기로 엽니다. Modules섹션에 Quantum에 대한 의존성을 추가합니다:
"Modules": [
{
"Name": "MyTestProject",
"Type": "Runtime",
"LoadingPhase": "Default",
"AdditionalDependencies": [
"PhotonQuantum
]
}
]
- 프로젝트 설정에서
UQuantumGameInstance를 게임 인스턴스로 설정합니다.
아래 영상에서는 새 프로젝트에서 Quantum Unreal을 설정하는 과정을 단계별로 보여줍니다. 일부 항목은 최신 버전과 다를 수 있지만, 위의 단계는 항상 최신 상태로 유지됩니다.
기본 액터 또는 ActorComponent
Unreal용 Quantum에서 예측-롤백 게임 상태 및 로직을 추가하는 진입점은 결정적 액터 입니다.
결정적 액터는 AActor를 상속한 커스텀 클래스이거나, 이 커스텀 액터 또는 미리 정의된 ADeterministicActor 클래스를 상속한 블루프린트 클래스일 수 있습니다.
모든 Quantum 액터는 QuantumObjectInterface.h에 정의된 인터페이스를 구현하기 위한 코드를 Unreal Header Tool(UHT)이 자동 생성합니다.
이 인터페이스는 필요한 API와 핵심 빌딩 블록을 제공합니다.
또한 동일한 인터페이스를 구현하는 커스텀 컴포넌트도 만들 수 있습니다.
롤백 가능한 게임 상태 는 QuantumObjectInterface를 구현한 모든 액터와, 동일한 인터페이스를 구현하는 하위 컴포넌트들로 구성됩니다.
아래는 최소한의 Quantum 액터 컴포넌트 C++ 예시입니다.
(아직 커스텀 게임 상태나 결정적 콜백은 포함되어 있지 않습니다.)
C++
#pragma once
#include "CoreMinimal.h"
#include "QuantumObjectInterface.h"
#include "TestComponent.quantum.h"
#include "TestComponent.generated.h"
UCLASS()
class QUANTUMPLUGIN_API UTestComponent : public UActorComponent, public IQuantumObjectInterface
{
GENERATED_BODY()
QGENERATED_BODY(UTestComponent)
};
예시에서 주목할 점
#include "FILENAME.generated.h"바로 위에#include "FILENAME.quantum.h"를 반드시 포함해야 합니다.- Unreal의
GENERATED_BODY()바로 아래에QGENERATED_BODY(TYPE)을 반드시 선언해야 합니다.
이 규칙을 지켜야 Quantum UHT 애드온이 각 타입에 필요한 코드를 올바르게 생성할 수 있습니다.
예측-롤백 게임 상태
결정적 시뮬레이션의 핵심은, 로컬 입력으로 상태를 미리 예측하고,
필요할 때 서버의 검증된 상태로 빠르고 정확하게 롤백 하는 능력입니다.
그 후 동일한 시뮬레이션 코드를 재사용하여, 서버의 확인된 입력으로 게임 상태를 다시 앞으로 진행시킵니다.
(필요하다면 추가 틱의 예측을 수행하거나, 새로운 로컬 입력을 수집합니다.)
이를 위해 Quantum은 자체적으로 빠르게 복사 가능한 Heap을 사용합니다.
변경 가능한 게임 플레이 관련 데이터는 모두 롤백을 지원할 수 있도록 특별한 방식으로 선언되어야 합니다.
각 액터나 컴포넌트별로 Quantum은 지정된 게임 상태 멤버를 포함하는 USTRUCT를 코드로 생성하며,
이를 QState라는 이름의 포인터로 선언합니다.
QState에 멤버를 추가하는 방법은 다음 세 가지입니다:
QREPLICATEQPROPERTY(일반 UPROPERTY를 확장한 형태)- 루트
QPROPERTY로 사용하는 USTRUCT 전체
옵션 1: QREPLICATE
QREPLICATE 매크로를 사용하면 가장 간단하게 상태를 추가할 수 있습니다:
C++
// ommited for clarity
UCLASS()
class QUANTUMPLUGIN_API UTestComponent : public UActorComponent, public IQuantumObjectInterface
{
GENERATED_BODY()
QGENERATED_BODY(UTestComponent)
QREPLICATE(FCVector3, MyVector)
QREPLICATE(int, MyNumber)
};
이 선언은 Quantum UHT 애드온이 다음과 같은 구조체를 자동 생성합니다:
C++
// code-generated by Quantum's UHT add-on, do not edit
struct FQuantumState
{
FCVector3 MyVector;
int MyNumber;
};
그리고 클래스의 public 선언부에 다음이 추가됩니다:
C++
// code-generated by Quantum's UHT add-on, do not edit
FQuantumState* QState = nullptr;
이 상태 데이터는 Quantum의 heap 내에 저장되며,
개발자 코드 실행 시 적절한 사본(검증된 버전 또는 예측 중 버전)이 자동으로 제공됩니다.
옵션 2: QPROPERTY
위 방법은 코드 중심이라 블루프린트 에디터에서 초깃값을 설정할 수 없습니다.
디자이너가 수정할 수 있도록 하려면 QPROPERTY를 사용합니다.
C++
// ommited for clarity
UCLASS()
class QUANTUMPLUGIN_API UTestComponent : public UActorComponent, public IQuantumObjectInterface
{
GENERATED_BODY()
QGENERATED_BODY(UTestComponent)
QPROPERTY(EditAnywhere, Category = "Quantum State")
FCVector3 MyVectorPrototype;
QPROPERTY(EditAnywhere, Category = "Quantum State")
int MyNumberPrototype;
};
이 컴포넌트를 Quantum 액터에 추가하면,
QState 멤버의 초깃값을 디테일 패널에서 편집할 수 있습니다:
옵션 3: QState를 커스텀 USTRUCT로 완전 제어하기
개발자가 전체 상태 구조체를 직접 제어하고 싶다면,
일반 USTRUCT를 선언하여 Quantum이 이를 루트 상태로 사용하게 할 수 있습니다.
C++
USTRUCT(BlueprintType)
struct QUANTUMPLUGIN_API FMyQuantumState {
GENERATED_BODY()
QGENERATED_BODY(FMyQuantumState)
public:
UPROPERTY(EditAnywhere)
FCVector3 MyVector;
UPROPERTY(EditAnywhere)
int MyNumber;
};
클래스에서는 이렇게 선언합니다:
C++
QPROPERTY(EditAnywhere, Category = "Quantum Root State", Meta = (QStateRoot))
FMyQuantumState StatePrototype;
주의할 점:
- 루트 상태 구조체는 일반 USTRUCT입니다.
- 멤버는 일반 UPROPERTY를 사용하며
Prototype접미사가 필요 없습니다. - QPROPERTY는 오직 루트 구조체를 가리키는 하나만 존재해야 합니다.
일반 UPROPERTY와의 관계
UPROPERTY는 Quantum 상태 외에도 사용할 수 있지만,
롤백이나 스냅샷 직렬화에는 포함되지 않습니다.
따라서 다음 용도로 적합합니다:
- 인스턴스마다 다른 읽기 전용 설정
- 뷰/UI용 속성 (예: VFX/SFX 참조)
- 게임 플레이에 영향을 주지 않는 보조 데이터
요약:
- 변경 가능한 상태는 반드시
QState에 포함되어야 합니다. - 세 가지 선언 방식 존재:
① 코드 기반 QREPLICATE
② 에디터 편집 가능한 QPROPERTY
③ 루트 구조체 기반 QPROPERTY - 일반 UPROPERTY는 뷰 전용이나 설정값에만 사용합니다.
결정적 로직 (콜백)
예측-롤백 게임 로직의 두 번째 주요 진입점은 DeterministicTick 콜백입니다.
이는 일반적인 단일 플레이어 게임에서 Unreal의 Tick 콜백이 담당하던 역할을 대체합니다.
DeterministicTick을 사용하면, QState 포인터가 항상 올바른 데이터 사본(서버에서 확인된 상태 또는 예측 중인 상태)을 가리키도록 보장됩니다.
이 메서드는 로컬 입력 기반 예측과 서버 확정 입력 기반 시뮬레이션 모두에 대해 실행됩니다.
함수 시그니처는 다음과 같습니다:
C++
virtual void DeterministicTick(const FDeterministicTickInfo& TickInfo) override;
TickInfo 구조체에는 구현 시 유용한 정보가 포함되어 있습니다. 주요 항목은 다음과 같습니다:
FCReal DeltaTime: 고정소수점(fixed-point)으로 표현된 델타 타임.
일반 틱에서는 1/FixedUpdateRate이며, 부분 틱에서는 0과 그 값 사이의 값입니다.int Tick: 현재 계산 중인 정확한 틱 번호. (첫 번째 시뮬레이션 틱은 롤백 윈도우 설정값과 동일)bool Verified: 서버에서 확인된 입력으로 진행되는 틱인지 여부. (이 경우 해당 틱은 최종 확정 상태입니다.)bool Partial: 렌더링 프레임과 맞추기 위해 불완전한 델타 타임으로 시뮬레이션되는 부분 틱 여부.
(고정 시뮬레이션 속도와 렌더링 속도 간의 작은 오차를 보정하기 위함입니다.)TickInputSet* InputSet: 현재 틱에 대한 전체 입력 세트. (로컬 예측 입력, 원격 플레이어 입력, 서버 확정 입력 포함)
UI, 애니메이션, VFX/SFX 등의 렌더링 관련 갱신은 모든 Quantum 객체의 DeterministicTick 이후 한 번만 호출되는 별도 콜백에서 처리할 수 있습니다.
C++
virtual void UpdateView() override;
이 콜백이 호출될 때 QState는 DeterministicTick의 마지막 부분 틱 업데이트 결과를 반영한 최신 데이터 상태를 가집니다.
Quantum 액터 추가
Quantum 시뮬레이션에 IQuantumObjectInterface를 구현한 액터를 추가하는 방법은 두 가지입니다:
- Unreal 맵 에 직접 배치하여 Quantum 시작 시 자동 등록
- 시뮬레이션 도중 코드에서 동적으로 스폰
맵에 액터 추가
Quantum 시작 전 맵에 로드된 IQuantumObjectInterface 구현 액터는 Quantum이 시작될 때 자동으로 등록됩니다.
(join이 서브시스템에서 호출될 때 자동 감지됩니다.)
예시:
ADeterministicActor (Quantum 기본 제공 액터 클래스)를 맵에 배치하고 Quantum 관련 컴포넌트를 추가할 수 있습니다.
만약 맵에 UFixedMeshCollider3dComponent를 가진 메시가 포함되어 있다면, 해당 메시 데이터는 데이터 에셋으로 베이크(bake) 되어야 합니다.
이를 위해 UMapBaker 컴포넌트를 액터에 추가하면 자동으로 모든 고정 메시 콜라이더와 랜드스케이프를 베이크 합니다.
MapBaker 컴포넌트를 선택한 상태에서 “Bake Collision Data” 버튼을 눌러 수동으로 다시 베이크 할 수도 있습니다.
모든 Quantum 메시 콜라이더는 런타임 시 MapBaker 참조를 필요로 하므로, 베이크 후에도 해당 컴포넌트를 삭제하지 않아야 합니다.
동적 액터 생성
IQuantumObjectInterface를 구현한 액터는 예측-롤백 시뮬레이션 중에도 동적으로 생성할 수 있습니다.
단, 다음 규칙을 따라야 합니다:
- 생성 호출은 반드시
DeterministicTick(또는 그 내부 호출 스택)에서 이루어져야 합니다.
예를 들어 “spawner” 역할의 액터를 맵에 미리 배치할 수 있습니다. - 생성은 Quantum이 제공하는 API
SpawnActor를 사용해야 합니다. - 생성되는 액터는 반드시
IQuantumObjectInterface를 구현해야 합니다.
함수 시그니처 (CoriumGame.h):
C++
AActor* = SpawnActor(UClass* type);
예시 (DeterministicTick 또는 OnPlayerAdded 등에서):
C++
// somewhere in the .h
UPROPERTY(EditAnywhere, Category = "PlayerAvatar")
TSubclassOf<ADeterministicActor> MyAvatarType;
// spawn snippet in the .cpp
auto avatar = GetQuantumGame()->SpawnActor(MyAvatarType.Get());
작동 방식:
예측 틱 중에는
nullptr이 반환됩니다. (확정 틱에서만 생성 가능)확정 틱 중에는 다음 단계를 수행합니다:
- 대상 클래스 인스턴스를 생성 (
BeginTick호출됨) - Quantum에 등록 (초기 프로토타입 값 적용)
- 생성된 액터 포인터 반환
- 대상 클래스 인스턴스를 생성 (
플레이어 연결 시 생성
새 클라이언트가 Quantum 세션에 연결하면, 로컬에서 AddLocalPlayer를 호출할 수 있습니다.
서버에서 해당 요청을 승인하면, 모든 클라이언트(관전자 포함) 에서 OnPlayerAdded 콜백이 확정 틱 에 호출됩니다.
이 시점은 플레이어 캐릭터를 생성하기에 이상적입니다.
필요시 AddLocalPlayerData를 사용해 커스텀 데이터를 함께 전달할 수도 있습니다.
OnPlayerAdded는 항상 확정 프레임에서 호출되므로 nullptr 체크가 필요 없습니다.
다음은 에디터에서 지정한 블루프린트 참조를 사용해 차량을 생성하는 예시입니다.
헤더 (QuantumPlayerSpawner.h):
C++
UPROPERTY(EditAnywhere)
TSubclassOf<AQuantumVehicle> PlayerVehicleClass;
virtual void OnPlayerAdded(ExitGames::Common::JArray<nByte>* data, int playerIndex) override;
구현 (QuantumPlayerSpawner.cpp):
C++
void UQuantumPlayerSpawner::OnPlayerAdded(ExitGames::Common::JArray<nByte>* data, int playerIndex) {
auto vehicle = Cast<AQuantumVehicle>(GetQuantumGame()->SpawnActor(PlayerClass.Get()));
vehicle->_transform->QState->Value.position = FCVector3(0, playerIndex * 400, 200);
// assigning control to this specific player
vehicle->QState->PlayerIndex = playerIndex;
vehicle->InitUI();
}
Quantum 액터 제거
WIP: 기능 테스트 완료 후 문서가 추가될 예정입니다.
입력과 명령
TODO
현재는 Discord 커뮤니티나 이전 스트림을 참고하면 됩니다.
이벤트
TODO
현재는 Discord 커뮤니티나 이전 스트림을 참고하면 됩니다.
미리 구성된 Quantum 액터 및 컴포넌트 (Prebuilt Quantum Actors and Components)
ADeterministicActor: 컴포넌트 기반 로직 또는 맵 콜라이더 구성을 위해 사용.UFixedTransform3D: Unreal 루트 트랜스폼을 FCReal 기반으로 저장 및 업데이트.UFixedCollider3D(하위 타입:UFixedLandscapeCollider3D,UFixedBoxCollider3D,UFixedSphereCollider3D,UFixedCompoundCollider3D):
Quantum의 예측-롤백 물리 엔진에서 사용할 콜라이더 데이터를 베이크 함.UFixedPhysicsBody3D: 동적 물리 데이터(질량, 중력, 드래그 등)를 관리.
AddForce,AddTorqueAPI 포함.AQuantumVehicle: 3D 차량 물리(스프링/댐퍼 서스펜션, 안티롤백, 조향, 타이어 마찰 등)를 구현한 Quantum 액터.
차량 조립용 기본 컴포넌트(UFixedTransform3D,UFixedCollider3D,UFixedPhysicsBody3D) 포함.- 프로토타입 액터: 정적/동적 기본 예시(박스, 스피어 등).
기타 물리 기능:
- Raycast 및 ShapeOverlap 쿼리
- Hit/Overlap 콜백(begin/end 이벤트 포함)
- PhysicsMaterial DataAsset (Chaos Physics와 유사한 구조)
- Layers/Collision Type 시스템 (Chaos와 동일한 표준)
- 완전한 Solver 및 설정 구성 가능
Quantum 게임 인스턴스
Quantum은 Unreal의 Game Instance로 동작하며, Photon Cloud에 연결된 시점부터 Tick이 실행됩니다.
프로젝트 설정에서 Game Instance를 UQuantumGameInstance로 지정해야 합니다.
게임 인스턴스 API
다음 API 함수들은 C++ 및 블루프린트 모두에서 사용할 수 있습니다:
C++
UFUNCTION(BlueprintPure, Category = "QuantumGameInstance", meta = (WorldContext = "WorldContextObject") )
static const UQuantumGameInstance* GetQuantumGameInstance(const UObject* WorldContextObject);
UFUNCTION(BlueprintCallable, Category = "QuantumGameInstance")
bool IsLocalPlayer(int playerIndex);
UFUNCTION(BlueprintCallable, Category = "QuantumGameInstance")
void AddLocalPlayer(int localSlot);
UFUNCTION(BlueprintCallable, Category = "QuantumGameInstance")
void AddLocalPlayerData(int localSlot, ExitGames::Common::JArray<nByte>& data);
UFUNCTION(BlueprintCallable, Category = "QuantumGameInstance")
void Join(FString roomName);
UFUNCTION(BlueprintCallable, Category = "QuantumGameInstance")
void Connect(bool local, FString userID, FString appID);
GetQuantumGameInstance는 현재 Quantum Game Instance 포인터를 반환합니다.
Connect와 Join은 빠른 테스트용 진입점입니다.
Connect: Quantum App ID와 고유한userID를 지정하여 서버에 연결합니다.
(Quantum Version 3 서버 플러그인을 사용하는 로컬 서버에서도 동작 가능)Join: 지정한 이름의 Photon 서버 룸을 “JoinOrCreate” 방식으로 연결하며,
로컬 Quantum 세션을 즉시 시작합니다. (서버 세션도 자동 시작됨)
AddLocalPlayer는 세션 시작 후 서버에 새로운 플레이어 슬롯을 요청합니다.
성공 시 OnPlayerAdded 콜백이 모든 클라이언트에서 호출됩니다.
IsLocalPlayer는 특정 글로벌 플레이어 인덱스가 로컬 인스턴스에서 제어 중인지 확인합니다.
(예: 로컬 플레이어가 조종하는 캐릭터 카메라 설정에 사용)
추가 예시
아래 블루프린트 예시는 앞서 설명한 OnPlayerAdded 예시에서 생성된 차량에 카메라를 부착하는 코드입니다.
이 코드는 차량이 로컬 플레이어가 조종하는 경우에만 스프링암 카메라를 설정합니다.
허용되는 결정적 타입
QState 정의 시 다음 타입과 이들로 구성된 USTRUCT만이 직렬화 및 예측/롤백 코드 생성을 지원합니다:
FCReal: 고정소수점(Q48.16) 타입 (float/double 대신 사용)FCVector2,FCVector3: FVector2, FVector3에 해당FCQuat: FQuat에 해당uint32_t,int32_t(8, 16, 64비트 변형 포함)bool- 위 타입으로 구성된 임의의 USTRUCT
- 다른 Quantum 액터/컴포넌트에 대한 포인터 (
TObjectPtr<Type>) - DataAsset 포인터 (
TDataAssetPtr<AssetType>)
로드맵 요약
단기~중기 계획된 누락 기능
물리(Physics):
- CCD 및 조인트 지원 (중기)
기타(Misc):
- FCReal 수학 라이브러리 함수의 블루프린트 완전 노출