🎮 구현 목표
플레이어가 스폰 테이블과 상호작용하면 재료 액터를 동적으로 생성합니다. 재료는 타입(토마토, 양파 등)에 따라 다른 메시와 데이터를 가지며, 모든 플레이어에게 동일하게 보여야 합니다.
💭 구현 방법
재료 시스템을 어떻게 설계할 것인가?
재료마다 고유한 메시, 아이콘, 조리 상태가 필요한데, 이를 어떻게 관리할지 두 가지 방안을 고민했습니다.
A안: BP 클래스를 여러 개 만들기
BP_Tomato (메시, 아이콘, 상태 모두 내장)
BP_Onion (메시, 아이콘, 상태 모두 내장)
BP_Lettuce (메시, 아이콘, 상태 모두 내장)
...
장점:
- 구현 간단: BP에서 모든 값 설정
- 에디터에서 바로 확인 가능
단점:
- ❌ 재료마다 BP 클래스 추가 (30종 → 30개 BP)
- ❌ 공통 로직 수정 시 모든 BP 수정 필요
- ❌ 스폰 테이블도 재료 수만큼 매칭 필요
// 스폰 테이블마다 하드코딩 if (TableType == ESpawnTableType::Tomato) SpawnActor(BP_Tomato); else if (TableType == ESpawnTableType::Onion) SpawnActor(BP_Onion); // 30개 분기문...
B안: 데이터 기반 설계
재료 클래스: 1개 (껍데기, Type과 State만 가짐)
DataTable: 재료 정보 모두 등록 (메시, 아이콘, 조리 데이터)
장점:
- ✅ 재료 추가 시 DataTable에 Row 추가만
- ✅ 밸런싱/수정이 엑셀 작업처럼 간편
- ✅ 코드 수정 없이 기획 변경 대응
단점:
- 초기 구현 복잡도 높음
- 런타임에 DataTable 조회 필요
그럼에도 데이터 기반으로 설계를 하는 것이 추후 확장성을 고려했을 때 더 적절한 판단이라고 생각했습니다.
🚨 문제 상황
데이터 기반 설계의 함정
// 설계: 재료는 Type만 가지고, 나머지는 DataTable에서 조회
class AIngredient
{
UPROPERTY(Replicated)
EIngredientType IngredientType; // Enum만 복제
};
// BeginPlay에서 DataTable 조회
void AIngredient::BeginPlay()
{
FIngredientData* Data = DataTable->FindRow(IngredientType);
SetMesh(Data->Mesh);
SetTexture(Data->Icon);
}
스폰 흐름:
1. 스폰 테이블 상호작용 (클라이언트)
2. Server RPC 호출
3. 서버가 재료 스폰 (SpawnActor)
4. 재료의 Type 설정
5. BeginPlay 호출 → DataTable 조회
발생한 버그:
스폰을 요청한 클라이언트 화면에만 재료가 보이고, 다른 플레이어들은 안 보임.
디버깅 결과, 다른 클라이언트에서는 IngredientType == None으로 조회되어 메시가 설정되지 않았습니다.
🔧 구현
네트워크 복제 타이밍 문제
원인:
서버: SpawnActor → Type 설정 → BeginPlay → DataTable 조회 ✅
클라: SpawnActor → BeginPlay → Type 복제 (아직 안 됨!) ❌
클라이언트는 BeginPlay가 먼저 호출되고, Type은 그 이후에 복제되므로 DataTable 조회가 None으로 실패합니다.
해결: SpawnActorDeferred
"BeginPlay 전에 프로퍼티를 먼저 설정"할 수 있는 지연 스폰을 사용:
AIngredient* USpawnManageComponent::SpawnIngredientActor(
TSubclassOf IngredientClass,
EIngredientType Type)
{
// 1. 메모리만 할당 (BeginPlay 호출 안 함)
FTransform SpawnTransform;
AIngredient* Ingredient = GetWorld()->SpawnActorDeferred(
IngredientClass,
SpawnTransform
);
// 2. BeginPlay 전에 Type 설정 (Replicated 변수)
Ingredient->SetType(Type);
// 3. 이제 BeginPlay 호출 → 모든 클라에 Type이 이미 복제됨
Ingredient->FinishSpawning(SpawnTransform);
return Ingredient;
}
실행 흐름:
서버: SpawnDeferred → SetType → FinishSpawning → BeginPlay ✅
클라: (복제 대기) → Type 복제 완료 → BeginPlay ✅
FinishSpawning 호출 전에 SetType으로 Replicated 변수를 설정하면, BeginPlay가 호출될 때 이미 모든 클라이언트에 Type이 복제된 상태입니다.
// Ingredient.cpp
void AIngredient::SetType_Implementation(EIngredientType Type)
{
// Replicated 변수 설정 (BeginPlay 전)
IngredientType = Type;
}
void AIngredient::BeginPlay()
{
// 이제 모든 클라에서 IngredientType이 올바르게 설정됨
FIngredientData* Data = DataTable->FindRow(IngredientType);
SetMesh(Data->Mesh);
SetTexture(Data->Icon);
}
✅ 결과
✅ 모든 클라이언트에 재료 정상 표시
✅ 데이터 기반 설계 유지: 재료 BP 클래스 1개 + DataTable로 모든 재료 타입 관리
✅ 네트워크 복제 타이밍 이해: BeginPlay 전후의 실행 순서 파악
일반 SpawnActor는 즉시 BeginPlay를 호출하므로, Replicated 변수 설정이 복제보다 늦을 수 있습니다. SpawnActorDeferred를 사용하면:
- 메모리 할당
- 프로퍼티 설정 (복제 준비)
FinishSpawning→ BeginPlay 호출
이 순서로 실행되어 BeginPlay 시점에 이미 모든 데이터가 복제된 상태를 보장할 수 있습니다. 동적 스폰이 필요한 멀티플레이어 게임에서 필수 패턴입니다.
'프로젝트 회고' 카테고리의 다른 글
| [Dedicated Server] SeamlessTravel: 레벨 전환 시 데이터 유실 방지 (0) | 2025.12.02 |
|---|---|
| [Dedicated Server] 비동기 폴링 기반 서버 접속 시도 (0) | 2025.12.02 |
| [UE5 액션] 델리게이트(Delegate)를 활용한 유연한 인벤토리 시스템 (0) | 2025.12.02 |
| [UE5 액션] 데이터 에셋 기반 무기별 전투 스타일 관리 (0) | 2025.12.02 |
| [UE5 액션] 루트 모션 기반 자연스러운 대시 구현 Motion Warping (0) | 2025.12.02 |
