KLingo Step1(Read) - Quest System Implementation Spec
프로젝트 정보 (Project Information)
개요 (Overview)
당신은 Unreal Engine 5.4 + C++ 기반 멀티플레이 게임 Onepiece 프로젝트의 아키텍트 겸 시니어 엔지니어입니다.
목표: Step1(Read) 퀘스트 시스템 + UI 위젯 + 판정 로직을 UE C++ 코드로 구현합니다.
프로젝트 환경 및 제약 (Environment & Constraints)
기술 스택 (Tech Stack)
- 엔진: Unreal Engine 5.4
- 언어: C++17
- 네트워크: Unreal 멀티플레이 (Dedicated / Listen 모두 지원)
- UI: UMG (C++ 기반 UUserWidget 확장)
- 모듈:
ONEPIECE_API매크로 사용 - 로그:
LogOnepiece카테고리 사용
네트워크 권한 분리 (Network Authority)
- GameMode: 서버 전용 (Authority), 게임 룰 및 스테이지 진행 관리
- GameState: 복제 데이터 (Replicated), 모든 클라이언트가 공유하는 게임 상태
- PlayerState: 플레이어별 데이터 (Replicated), 각 플레이어의 선택 상태
- Widget: 클라이언트 전용, UI 표시 및 입력 처리
RPC 규칙 (RPC Rules)
- 서버에서만 변경 가능한 값:
Server RPC→GameState/PlayerState Replicated 변수 - UI 입력:
클라이언트→Server RPC→GameState/PlayerState 반영→OnRep→위젯 업데이트
기존 코드베이스 (Existing Codebase)
이미 존재하는 클래스 (Existing Classes)
1. ALingoGameState
위치: Source/Onepiece/Game/Public/ALingoGameState.h
주요 멤버:
// 타이머 관련
UPROPERTY(Replicated)
float RemainMissionTime;
UPROPERTY(Replicated)
bool bIsTimerActive;
// 시나리오 데이터
UPROPERTY(Replicated)
int32 ScenarioIndex;
UPROPERTY(Replicated)
int32 StageIndex;
UPROPERTY(Replicated)
int32 ScenarioLevel;
UPROPERTY(Replicated)
FResponseScenario CurScenarioData;
주요 메서드:
void SetStageData(int InStageIndex, const FResponseScenario& InResponseData)void StartMissionTimer(float TimeLimit)void StopMissionTimer()float GetRemainMissionTime()
2. ALingoPlayerState
위치: Source/Onepiece/Game/Public/ALingoPlayerState.h
현재 상태: 매우 기본적인 구조 (Token, UserName만 존재)
필요한 확장:
- Step1(Read)에서 플레이어의 선택 상태 (Symbol, Color)
- 싱글/멀티 플레이 시 역할 (Role) 구분
- 정답/오답 판정 결과
3. ALingoGameMode
위치: Source/Onepiece/Game/Public/ALingoGameMode.h
현재 상태: 거의 비어있음
필요한 확장:
- Step1(Read) 시작 로직
- 캐리어 선택 처리 (정답/오답 판정)
- 타이머 패널티 적용
4. APlayerActor
위치: Source/Onepiece/Character/Public/APlayerActor.h
주요 컴포넌트:
UInteractionSystem* InteractionSystem: 캐리어와의 상호작용에 활용UMainWidget* MainWidget: 메인 UI 위젯
5. UMainWidget
위치: Source/Onepiece/UI/Public/UMainWidget.h
주요 컴포넌트:
UPlayTimer* PlayTimer: 타이머 표시 위젯UStateWidget* StateWidget: 플레이어 상태 표시 위젯ALingoGameState* CachedGameState: GameState 참조
데이터 구조 (Data Structures)
기존 데이터 구조 (Existing Structures)
위치: Source/Onepiece/Network/Public/NetworkData.h
1. FWordData
USTRUCT(BlueprintType)
struct FWordData
{
GENERATED_BODY()
UPROPERTY(BlueprintReadWrite, Category = "Word")
FString Kor; // 한국어
UPROPERTY(BlueprintReadWrite, Category = "Word")
FString Eng; // 영어
UPROPERTY(BlueprintReadWrite, Category = "Word")
FString Pronunciation; // 발음
};
2. FScenarioTargetData
USTRUCT(BlueprintType)
struct FScenarioTargetData
{
GENERATED_BODY()
UPROPERTY(BlueprintReadWrite, Category = "Scenario")
FString symbol; // 심볼 (문제1)
UPROPERTY(BlueprintReadWrite, Category = "Scenario")
FString color; // 색상 (문제2)
};
3. FResponseScenario
USTRUCT(BlueprintType)
struct FResponseScenario
{
GENERATED_BODY()
UPROPERTY(BlueprintReadWrite, Category = "Scenario")
int32 index;
UPROPERTY(BlueprintReadWrite, Category = "Scenario")
int32 dificulity;
UPROPERTY(BlueprintReadWrite, Category = "Scenario")
TArray target_data; // 선택지 풀
UPROPERTY(BlueprintReadWrite, Category = "Scenario")
int32 correct_answer_index;
UPROPERTY(BlueprintReadWrite, Category = "Scenario")
FWordData word_data1; // 문제1 정답
UPROPERTY(BlueprintReadWrite, Category = "Scenario")
FWordData word_data2; // 문제2 정답
UPROPERTY(BlueprintReadWrite, Category = "Scenario")
FWordData full_data; // 전체 정답 (문제1 + 문제2)
};
추가 필요 데이터 구조 (New Structures Needed)
1. EReadQuestRole (플레이어 역할)
UENUM(BlueprintType)
enum class EReadQuestRole : uint8
{
Both UMETA(DisplayName = "Both"), // 싱글: 문제1, 2 모두 조작
OnlyQuestion1 UMETA(DisplayName = "OnlyQuestion1"), // 멀티: 문제1만 조작
OnlyQuestion2 UMETA(DisplayName = "OnlyQuestion2") // 멀티: 문제2만 조작
};
2. FReadQuestResult (플레이어 기록)
USTRUCT(BlueprintType)
struct FReadQuestResult
{
GENERATED_BODY()
UPROPERTY(BlueprintReadWrite, Category = "Quest")
bool bSuccess = false; // 성공 여부
UPROPERTY(BlueprintReadWrite, Category = "Quest")
float RemainTime = 0.f; // 남은 시간
UPROPERTY(BlueprintReadWrite, Category = "Quest")
int32 AttemptCount = 0; // 시도 횟수
UPROPERTY(BlueprintReadWrite, Category = "Quest")
FString SelectedSymbol; // 선택한 심볼
UPROPERTY(BlueprintReadWrite, Category = "Quest")
FString SelectedColor; // 선택한 색상
};
게임 시나리오 (Game Scenario)
Step1(Read) 퀘스트 흐름 (Quest Flow)
1. 퀘스트 시작 (Quest Start)
- 플레이어는 "ReadQuest 미션 가이드 팝업"을 봅니다.
- 팝업에는 다음 정보가 표시됩니다:
- 문제1 선택 영역 (심볼 리스트)
- 문제2 선택 영역 (색상 리스트)
- 남은 시간
2. 문제 선택 (Answer Selection)
문제1 (심볼 선택):
FScenarioTargetData배열에서symbol필드가 존재하는 항목만 필터링symbol문자열 기준으로 중복 제거- 세로 스크롤 뷰(ScrollBox)에 버튼/엔트리로 노출
- 플레이어가 하나 선택하면:
- 로컬 UI 하이라이트
- 서버 RPC 호출 → PlayerState에
SelectedSymbol저장 - Replicated 변수 동기화
문제2 (색상 선택):
FScenarioTargetData배열에서color필드가 존재하는 항목만 필터링color문자열 기준으로 중복 제거- 세로 스크롤 뷰에 버튼/엔트리로 노출
- 플레이어가 하나 선택하면:
- 로컬 UI 하이라이트
- 서버 RPC 호출 → PlayerState에
SelectedColor저장 - Replicated 변수 동기화
3. 캐리어 선택 (Carrier Interaction)
- 플레이어가 맵에 배치된 캐리어(수하물 -
Aluggage)를 선택합니다. APlayerActor의InteractionSystem을 활용하여 상호작용합니다.- 캐리어 선택 시 서버 RPC 호출 → GameMode에서 정답 판정
4. 정답 판정 (Answer Validation)
정답 조건:
- 플레이어가 선택한
SelectedSymbol== 캐리어의Symbol - 플레이어가 선택한
SelectedColor== 캐리어의Color - 두 조건 모두 만족 시 정답
오답 조건:
- 하나라도 다르면 오답
싱글 vs 멀티 플레이 규칙 (Single vs Multiplayer Rules)
싱글 플레이 (Single Player)
- 플레이어1: 문제1과 문제2를 모두 조작 가능
- Role:
EReadQuestRole::Both
멀티 플레이 (Multiplayer)
- 플레이어1:
word_data1 (문제1)만 조작 가능- Role:
EReadQuestRole::OnlyQuestion1
- Role:
- 플레이어2:
word_data2 (문제2)만 조작 가능- Role:
EReadQuestRole::OnlyQuestion2
- Role:
구현 방법 (Implementation)
ALingoPlayerState에EReadQuestRole필드 추가- 위젯에서 Role에 따라 버튼 활성/비활성 제어
- 서버 RPC에서 Role 검증 (권한이 없는 선택 차단)
정답 처리 로직 (Success Logic)
서버 처리 (Server-side)
- 남은 시간을
ALingoGameState::RemainMissionTime에서 가져옵니다. FReadQuestResult구조체에 다음 정보를 저장합니다:bSuccess = trueRemainTime = 현재 남은 시간AttemptCount = 시도 횟수SelectedSymbol = 선택한 심볼SelectedColor = 선택한 색상
- GameState에 성공 플래그를 Replicated 합니다.
UBroadcastManager를 통해 성공 이벤트를 브로드캐스트합니다.
클라이언트 처리 (Client-side)
- OnRep 콜백에서 성공 플래그를 감지합니다.
- 위젯에 "정답입니다" 팝업을 표시합니다.
- 다음 Step 버튼을 활성화합니다.
오답 처리 로직 (Failure Logic)
서버 처리 (Server-side)
1. 타이머 패널티
// 남은 시간에서 30초 감소
float Penalty = 30.f;
float NewTime = FMath::Max(0.f, RemainMissionTime - Penalty);
GameState->RemainMissionTime = NewTime;
2. 틀린 항목 판정
bool bSymbolCorrect = (PlayerState->SelectedSymbol == Carrier->Symbol);
bool bColorCorrect = (PlayerState->SelectedColor == Carrier->Color);
if (!bSymbolCorrect)
{
// 심볼이 틀렸으므로 선택 해제
PlayerState->SelectedSymbol = TEXT("");
PlayerState->bSymbolWrong = true;
}
if (!bColorCorrect)
{
// 색상이 틀렸으므로 선택 해제
PlayerState->SelectedColor = TEXT("");
PlayerState->bColorWrong = true;
}
3. 시도 횟수 증가
PlayerState->AttemptCount++;
클라이언트 처리 (Client-side)
1. OnRep 콜백
// PlayerState의 OnRep_SelectedSymbol, OnRep_SelectedColor 등에서 UI 업데이트
UFUNCTION()
void OnRep_SelectedSymbol();
UFUNCTION()
void OnRep_SelectedColor();
UFUNCTION()
void OnRep_SymbolWrong();
UFUNCTION()
void OnRep_ColorWrong();
2. UI 반영
- 틀린 항목:
- 이전에 선택되어 있던 엔트리에 "오답" 스타일 적용 (빨간색, X 표시 등)
- 선택 상태 해제
- 맞은 항목:
- 기존 선택 상태 유지
- 플레이어는 다시 틀렸던 항목만 선택할 수 있게 됩니다.
필수 설계 요소 (Required Design Elements)
1. 클래스 확장 (Class Extensions)
ALingoGameState 확장
새로 추가할 멤버:
// Step1(Read) 기록
UPROPERTY(Replicated, BlueprintReadOnly, Category = "Quest")
FReadQuestResult QuestResult;
// 성공/실패 플래그
UPROPERTY(ReplicatedUsing = OnRep_QuestSuccess, BlueprintReadOnly, Category = "Quest")
bool bQuestSuccess = false;
UFUNCTION()
void OnRep_QuestSuccess();
ALingoPlayerState 확장
새로 추가할 멤버:
// 플레이어 역할
UPROPERTY(Replicated, BlueprintReadOnly, Category = "Quest")
EReadQuestRole QuestRole = EReadQuestRole::Both;
// 선택 상태
UPROPERTY(ReplicatedUsing = OnRep_SelectedSymbol, BlueprintReadOnly, Category = "Quest")
FString SelectedSymbol;
UPROPERTY(ReplicatedUsing = OnRep_SelectedColor, BlueprintReadOnly, Category = "Quest")
FString SelectedColor;
// 오답 플래그
UPROPERTY(ReplicatedUsing = OnRep_SymbolWrong, BlueprintReadOnly, Category = "Quest")
bool bSymbolWrong = false;
UPROPERTY(ReplicatedUsing = OnRep_ColorWrong, BlueprintReadOnly, Category = "Quest")
bool bColorWrong = false;
// 시도 횟수
UPROPERTY(Replicated, BlueprintReadOnly, Category = "Quest")
int32 AttemptCount = 0;
// Server RPC
UFUNCTION(Server, Reliable, WithValidation)
void ServerSetSelectedSymbol(const FString& Symbol);
UFUNCTION(Server, Reliable, WithValidation)
void ServerSetSelectedColor(const FString& Color);
// OnRep 콜백
UFUNCTION()
void OnRep_SelectedSymbol();
UFUNCTION()
void OnRep_SelectedColor();
UFUNCTION()
void OnRep_SymbolWrong();
UFUNCTION()
void OnRep_ColorWrong();
ALingoGameMode 확장
새로 추가할 메서드:
// Step1(Read) 시작
UFUNCTION(BlueprintCallable, Category = "Quest")
void StartReadQuest();
// 캐리어 선택 처리
UFUNCTION()
void HandleCarrierSelection(APlayerState* Player, Aluggage* Carrier);
// 정답 판정
UFUNCTION()
bool ValidateAnswer(ALingoPlayerState* Player, Aluggage* Carrier);
// 정답 처리
UFUNCTION()
void HandleCorrectAnswer(ALingoPlayerState* Player);
// 오답 처리
UFUNCTION()
void HandleWrongAnswer(ALingoPlayerState* Player, bool bSymbolCorrect, bool bColorCorrect);
2. 기존 클래스 확장 (Existing Class Extension)
Aluggage (Luggage 액터 확장)
위치: Source/Onepiece/Interactable/Public/luggage.h
현재 구현:
UCLASS()
class ONEPIECE_API Aluggage : public AActor
{
GENERATED_BODY()
public:
Aluggage();
public:
UPROPERTY(EditDefaultsOnly, BlueprintReadWrite)
TObjectPtr Mesh;
UPROPERTY(VisibleAnywhere)
TObjectPtr InteractableComp;
};
추가 필요 멤버:
/// @brief 캐리어의 심볼 (문제1 정답)
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Quest")
FString Symbol;
/// @brief 캐리어의 색상 (문제2 정답)
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Quest")
FString Color;
/// @brief 플레이어가 캐리어를 선택했을 때 호출됩니다.
UFUNCTION(BlueprintCallable, Category = "Interaction")
void OnInteract(AActor* Interactor);
/// @brief 서버에 캐리어 선택을 알립니다.
UFUNCTION(Server, Reliable, WithValidation)
void ServerNotifySelection(APlayerState* Player);
UReadQuestWidget (Read 퀘스트 위젯)
위치: Source/Onepiece/UI/Public/UReadQuestWidget.h
주요 멤버:
UCLASS()
class ONEPIECE_API UReadQuestWidget : public UUserWidget
{
GENERATED_BODY()
public:
UReadQuestWidget(const FObjectInitializer& ObjectInitializer);
protected:
virtual void NativeConstruct() override;
virtual void NativeTick(const FGeometry& MyGeometry, float InDeltaTime) override;
public:
/// @brief 문제1 선택 스크롤 박스 (BindWidget)
UPROPERTY(meta = (BindWidget), BlueprintReadOnly)
class UScrollBox* SymbolScrollBox;
/// @brief 문제2 선택 스크롤 박스 (BindWidget)
UPROPERTY(meta = (BindWidget), BlueprintReadOnly)
class UScrollBox* ColorScrollBox;
/// @brief 미션 설명 텍스트 (BindWidget)
UPROPERTY(meta = (BindWidget), BlueprintReadOnly)
class UTextBlock* MissionDescriptionText;
/// @brief 타이머 텍스트 (BindWidget)
UPROPERTY(meta = (BindWidget), BlueprintReadOnly)
class UTextBlock* TimerText;
protected:
/// @brief GameState 참조 캐싱
UPROPERTY()
TObjectPtr CachedGameState;
/// @brief PlayerState 참조 캐싱
UPROPERTY()
TObjectPtr CachedPlayerState;
/// @brief 선택지 엔트리 위젯 클래스
UPROPERTY(EditDefaultsOnly, Category = "UI")
TSubclassOf EntryWidgetClass;
/// @brief 현재 선택된 심볼 엔트리
UPROPERTY()
TObjectPtr SelectedSymbolEntry;
/// @brief 현재 선택된 색상 엔트리
UPROPERTY()
TObjectPtr SelectedColorEntry;
public:
/// @brief 퀘스트 위젯을 초기화합니다.
UFUNCTION(BlueprintCallable, Category = "Quest")
void InitializeQuest();
/// @brief 심볼 리스트를 빌드합니다.
UFUNCTION(BlueprintCallable, Category = "Quest")
void BuildSymbolList();
/// @brief 색상 리스트를 빌드합니다.
UFUNCTION(BlueprintCallable, Category = "Quest")
void BuildColorList();
/// @brief 심볼 선택 처리
UFUNCTION()
void OnSymbolSelected(const FString& Symbol, UReadQuestEntryWidget* EntryWidget);
/// @brief 색상 선택 처리
UFUNCTION()
void OnColorSelected(const FString& Color, UReadQuestEntryWidget* EntryWidget);
/// @brief 타이머 업데이트
UFUNCTION()
void UpdateTimer();
/// @brief PlayerState 업데이트 처리
UFUNCTION()
void OnPlayerStateUpdated();
};
UReadQuestEntryWidget (선택지 엔트리 위젯)
위치: Source/Onepiece/UI/Public/UReadQuestEntryWidget.h
주요 멤버:
UCLASS()
class ONEPIECE_API UReadQuestEntryWidget : public UUserWidget
{
GENERATED_BODY()
public:
/// @brief 선택 버튼 (BindWidget)
UPROPERTY(meta = (BindWidget), BlueprintReadOnly)
class UButton* SelectButton;
/// @brief 선택지 텍스트 (BindWidget)
UPROPERTY(meta = (BindWidget), BlueprintReadOnly)
class UTextBlock* ChoiceText;
/// @brief 상태 이미지 (BindWidget) - 정답/오답 표시
UPROPERTY(meta = (BindWidget), BlueprintReadOnly)
class UImage* StateImage;
protected:
/// @brief 선택지 값
UPROPERTY(BlueprintReadOnly, Category = "Quest")
FString ChoiceValue;
/// @brief 선택 상태
UPROPERTY(BlueprintReadOnly, Category = "Quest")
bool bIsSelected = false;
/// @brief 오답 상태
UPROPERTY(BlueprintReadOnly, Category = "Quest")
bool bIsWrong = false;
public:
/// @brief 엔트리를 초기화합니다.
UFUNCTION(BlueprintCallable, Category = "Quest")
void InitializeEntry(const FString& Value, bool bEnabled);
/// @brief 선택 상태를 설정합니다.
UFUNCTION(BlueprintCallable, Category = "Quest")
void SetSelected(bool bSelected);
/// @brief 오답 상태를 설정합니다.
UFUNCTION(BlueprintCallable, Category = "Quest")
void SetWrong(bool bWrong);
/// @brief 버튼 클릭 콜백
UFUNCTION()
void OnButtonClicked();
/// @brief 선택 델리게이트
DECLARE_MULTICAST_DELEGATE_TwoParams(FOnEntrySelected, const FString&, UReadQuestEntryWidget*);
FOnEntrySelected OnEntrySelected;
};
예시 흐름 (Example Flow)
싱글 플레이 흐름 (Single Player Flow)
1. Step1 시작
1. GameMode::StartReadQuest() 호출
↓
2. GameState에 FResponseScenario 세팅
↓
3. PlayerState에 Role = EReadQuestRole::Both 설정
↓
4. GameState::StartMissionTimer() 호출
↓
5. UBroadcastManager를 통해 "QuatStarted" 이벤트 브로드캐스트
↓
6. 클라이언트: ReadQuest 팝업 표시
↓
7. ReadQuestWidget::InitializeQuest() 호출
↓
8. BuildSymbolList(), BuildColorList() 호출
2. 문제 선택
1. 플레이어가 심볼 버튼 클릭
↓
2. ReadQuestWidget::OnSymbolSelected() 호출
↓
3. 로컬 UI 하이라이트 업데이트
↓
4. PlayerState::ServerSetSelectedSymbol(Symbol) RPC 호출
↓
5. 서버: PlayerState::SelectedSymbol = Symbol
↓
6. Replicated 변수 동기화
↓
7. 클라이언트: PlayerState::OnRep_SelectedSymbol() 콜백
↓
8. 위젯 업데이트
(색상 선택도 동일한 흐름)
3. 캐리어 선택
1. 플레이어가 InteractionSystem으로 캐리어(Aluggage) 상호작용
↓
2. Aluggage::OnInteract(Player) 호출
↓
3. Aluggage::ServerNotifySelection(PlayerState) RPC 호출
↓
4. 서버: GameMode::HandleCarrierSelection(PlayerState, Carrier) 호출
↓
5. GameMode::ValidateAnswer() 호출
↓
6. 정답/오답 분기
4-A. 정답 처리
1. GameMode::HandleCorrectAnswer(PlayerState) 호출
↓
2. GameState::QuestResult 업데이트
↓
3. GameState::bQuestSuccess = true
↓
4. GameState::StopMissionTimer() 호출
↓
5. UBroadcastManager를 통해 "QuestSuccess" 이벤트 브로드캐스트
↓
6. 클라이언트: GameState::OnRep_QuestSuccess() 콜백
↓
7. 위젯에 "정답입니다" 팝업 표시
↓
8. 다음 Step 버튼 활성화
4-B. 오답 처리
1. GameMode::HandleWrongAnswer(PlayerState, bSymbolCorrect, bColorCorrect) 호출
↓
2. GameState::RemainMissionTime -= 30.f (패널티)
↓
3. 틀린 항목 판정:
- bSymbolCorrect == false → PlayerState::SelectedSymbol = "", bSymbolWrong = true
- bColorCorrect == false → PlayerState::SelectedColor = "", bColorWrong = true
↓
4. PlayerState::AttemptCount++
↓
5. Replicated 변수 동기화
↓
6. 클라이언트: OnRep 콜백
↓
7. 위젯 업데이트:
- 틀린 항목: 오답 스타일 적용, 선택 해제
- 맞은 항목: 선택 상태 유지
↓
8. 플레이어는 다시 틀렸던 항목만 선택 가능
멀티 플레이 흐름 (Multiplayer Flow)
1. Step1 시작
1. GameMode::StartReadQuest() 호출
↓
2. GameState에 FResponseScenario 세팅
↓
3. PlayerState 역할 할당:
- Player1: Role = EReadQuestRole::OnlyQuestion1
- Player2: Role = EReadQuestRole::OnlyQuestion2
↓
4. 나머지는 싱글 플레이와 동일
2. 문제 선택
1. Player1이 심볼 버튼 클릭
↓
2. ReadQuestWidget::OnSymbolSelected() 호출
↓
3. PlayerState::ServerSetSelectedSymbol(Symbol) RPC 호출
↓
4. 서버: Role 검증
- Player1.Role == OnlyQuestion1 → 허용
- Player2.Role == OnlyQuestion2 → 차단
↓
5. 나머지는 싱글 플레이와 동일
(Player2는 색상만 선택 가능)
3. 캐리어 선택
1. Player1 또는 Player2가 캐리어 선택
↓
2. 서버: 두 플레이어의 선택 상태를 모두 확인
- Player1.SelectedSymbol
- Player2.SelectedColor
↓
3. 정답 판정:
- 두 조건 모두 만족 → 정답
- 하나라도 틀림 → 오답
↓
4. 나머지는 싱글 플레이와 동일
스타일 가이드 (Style Guide)
코딩 컨벤션 (Coding Conventions)
1. 언리얼 엔진 규칙 준수
- 클래스 접두사: 클래스 타입에 따라 적절한 접두사 사용
A: Actor 클래스 (예:ACarrierActor,ALingoGameMode)U: UObject 클래스 (예:UReadQuestWidget,ULingoGameHelper)F: 구조체 (예:FReadQuestResult,FWordData)E: Enum (예:EReadQuestRole)I: Interface (예:IControllable)
2. 네이밍 규칙
- 변수: PascalCase, 접두사 사용
bool→bIsSelected,bQuestSuccessint32→AttemptCount,ScenarioIndexfloat→RemainTime,PenaltyTObjectPtr→CachedGameState,InteractionSystem
- 함수: PascalCase, 동사로 시작
InitializeQuest(),BuildSymbolList(),OnSymbolSelected()
- RPC 함수:
Server,Client,Multicast접두사ServerSetSelectedSymbol(),ClientShowPopup(),MulticastBroadcastResult()
3. UPROPERTY 메타 지정자
// Replicated 변수
UPROPERTY(Replicated, BlueprintReadOnly, Category = "Quest")
float RemainTime;
// ReplicatedUsing 변수
UPROPERTY(ReplicatedUsing = OnRep_QuestSuccess, BlueprintReadOnly, Category = "Quest")
bool bQuestSuccess;
// BindWidget
UPROPERTY(meta = (BindWidget), BlueprintReadOnly)
class UScrollBox* SymbolScrollBox;
// EditAnywhere (디자이너가 설정 가능)
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Carrier")
FString Symbol;
4. UFUNCTION 메타 지정자
// Server RPC
UFUNCTION(Server, Reliable, WithValidation)
void ServerSetSelectedSymbol(const FString& Symbol);
// Client RPC
UFUNCTION(Client, Reliable)
void ClientShowPopup(const FString& Message);
// OnRep 콜백
UFUNCTION()
void OnRep_QuestSuccess();
// BlueprintCallable
UFUNCTION(BlueprintCallable, Category = "Quest")
void InitializeQuest();
Doxygen 주석 스타일
1. 파일 헤더
// Copyright (c) 2025 Doppleddiggong. All rights reserved.
/// @file luggage.h
/// @brief 수하물(Luggage) 액터를 정의합니다.
2. 클래스 주석
/// @brief 상호작용 가능한 수하물 액터
/// @details 플레이어가 선택할 수 있는 수하물 오브젝트입니다.
/// Read 퀘스트에서는 Symbol과 Color 정보를 추가로 가지며, 정답 판정에 사용됩니다.
UCLASS()
class ONEPIECE_API Aluggage : public AActor
{
GENERATED_BODY()
};
3. 함수 주석
/// @brief 캐리어를 선택했을 때 호출됩니다.
/// @param Interactor [in] 상호작용을 시도한 액터 (플레이어)
/// @return 없음
UFUNCTION(BlueprintCallable, Category = "Interaction")
void OnInteract(AActor* Interactor);
4. 변수 주석
/// @brief 캐리어의 심볼 (문제1 정답)
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Carrier")
FString Symbol;
모듈 및 로그
1. 모듈 매크로
// 모든 public 클래스에 ONEPIECE_API 매크로 사용
class ONEPIECE_API Aluggage : public AActor
2. 로그 카테고리
// GameLogging.h에 정의된 로그 매크로 사용
PRINTLOG(TEXT("[ReadQuest] Symbol selected: %s"), *Symbol);
블루프린트 호환성
- 가능한 모든 함수에
BlueprintCallable또는BlueprintPure지정 - 구조체는
USTRUCT(BlueprintType)사용 - Enum은
UENUM(BlueprintType)사용
구현 체크리스트 (Implementation Checklist)
Phase 1: 데이터 구조 및 GameState/PlayerState 확장
- [ ]
EReadQuestRoleEnum 정의 - [ ]
FReadQuestResult구조체 정의 - [ ]
ALingoGameState확장- [ ]
FReadQuestResult QuestResult추가 - [ ]
bool bQuestSuccess추가 - [ ]
OnRep_QuestSuccess()구현
- [ ]
- [ ]
ALingoPlayerState확장- [ ]
EReadQuestRole QuestRole추가 - [ ]
FString SelectedSymbol추가 - [ ]
FString SelectedColor추가 - [ ]
bool bSymbolWrong추가 - [ ]
bool bColorWrong추가 - [ ]
int32 AttemptCount추가 - [ ]
ServerSetSelectedSymbol()RPC 구현 - [ ]
ServerSetSelectedColor()RPC 구현 - [ ] OnRep 콜백들 구현
- [ ]
Phase 2: GameMode 로직 구현
- [ ]
ALingoGameMode확장- [ ]
StartReadQuest()구현 - [ ]
HandleCarrierSelection()구현 - [ ]
ValidateAnswer()구현 - [ ]
HandleCorrectAnswer()구현 - [ ]
HandleWrongAnswer()구현
- [ ]
Phase 3: Luggage 액터 확장
- [ ]
Aluggage클래스 확장- [ ]
Symbol,Color프로퍼티 추가 (Quest 데이터) - [ ]
OnInteract()메서드 추가 - [ ]
ServerNotifySelection()RPC 구현 - [ ] 기존
InteractableComp와 연동
- [ ]
Phase 4: UI 위젯 구현
- [ ]
UReadQuestWidget클래스 생성- [ ] BindWidget 프로퍼티들 추가
- [ ]
InitializeQuest()구현 - [ ]
BuildSymbolList()구현 - [ ]
BuildColorList()구현 - [ ]
OnSymbolSelected()구현 - [ ]
OnColorSelected()구현 - [ ]
UpdateTimer()구현 - [ ]
OnPlayerStateUpdated()구현
- [ ]
UReadQuestEntryWidget클래스 생성- [ ] BindWidget 프로퍼티들 추가
- [ ]
InitializeEntry()구현 - [ ]
SetSelected()구현 - [ ]
SetWrong()구현 - [ ]
OnButtonClicked()구현
Phase 5: 블루프린트 작업
- [ ] ReadQuestWidget UMG 블루프린트 생성
- [ ] 레이아웃 구성 (ScrollBox, TextBlock 등)
- [ ] ReadQuestEntryWidget UMG 블루프린트 생성
- [ ] 버튼, 텍스트, 이미지 구성
- [ ] Aluggage 블루프린트 생성 (BP_QuestLuggage 등)
- [ ] 기존 메시 활용
- [ ] Symbol, Color 프로퍼티 설정 가능하도록 구성
Phase 6: 테스트 및 디버깅
- [ ] 싱글 플레이 테스트
- [ ] 정답 케이스
- [ ] 오답 케이스 (심볼만 틀림)
- [ ] 오답 케이스 (색상만 틀림)
- [ ] 오답 케이스 (둘 다 틀림)
- [ ] 타이머 패널티 확인
- [ ] 멀티 플레이 테스트
- [ ] Player1이 문제1만 조작 가능한지 확인
- [ ] Player2가 문제2만 조작 가능한지 확인
- [ ] 두 플레이어가 동시에 선택했을 때 정답 판정
- [ ] 오답 시 각 플레이어의 UI 업데이트 확인
참고 문서 (References)
프로젝트 문서
AgentRule/Project/Onepiece/AGENT_GUIDE.md: Onepiece 프로젝트 공통 가이드AgentRule/Project/ue_coding_conventions.md: 언리얼 엔진 코딩 컨벤션AgentRule/Project/Onepiece/DOXYGEN_SETUP.md: Doxygen 문서화 가이드
언리얼 엔진 문서
- Unreal Engine Multiplayer Programming
- Unreal Engine Replicated Properties
- Unreal Engine RPC
- Unreal Engine UMG
최종 노트 (Final Notes)
설계 원칙
- 책임 분리: GameMode는 게임 로직, GameState는 공유 상태, PlayerState는 플레이어 상태, Widget은 UI만 담당
- 네트워크 안정성: 모든 중요한 상태는 Replicated 변수로 관리, RPC는 최소화
- 확장성: 다른 Step (Listen, Write, Speak)을 구현할 때 재사용 가능한 구조
- 디버깅 용이성: 적절한 로그 메시지와 디버그 정보 출력
주의사항
- RPC Validation: 모든 Server RPC에
WithValidation추가하고_Validate()함수 구현 - Role 검증: 플레이어가 권한이 없는 선택을 하지 못하도록 서버에서 검증
- 타이머 동기화: 타이머는 서버에서만 업데이트, 클라이언트는 Replicated 변수 참조
- UI 업데이트: OnRep 콜백에서만 UI 업데이트 수행 (직접 수정 금지)
다음 단계
이 문서를 바탕으로 단계별로 구현을 진행하세요. 각 Phase를 완료한 후 체크리스트를 확인하고, 다음 Phase로 넘어가세요.
구현 중 질문이나 문제가 발생하면 이 문서와 프로젝트 가이드를 참조하세요.