KLingo Speak Quest 기능 구현 요청
프로젝트 개요:
Onepiece 프로젝트 내에서 플레이어가 NPC와 상호작용하여 음성으로 질문에 답하는 'SpeakQuest' 콘텐츠를 구현합니다. 이 기능의 핵심 액터는 ANPCExaminer이며, 모든 로직은 Documents/DevLog/AgentLog/KLingo_SpeakScenario.md에 명시된 서버 권한(Server-Authoritative) 및 턴 기반(Turn-Based) 멀티플레이 아키텍처를 따라야 합니다.
프로젝트 코딩 컨벤션:
- 모든 코드는
AgentRule/Project/Onepiece/CODING_CONVENTIONS.md준수 - 파일 헤더: 저작권 표시 필수
- 변수명: camelCase (bool은 bPascalCase)
- Doxygen 주석:
/// @brief,/// @param스타일 사용 - 포인터:
TObjectPtr<>사용 - 네트워크 로깅:
NETWORK_LOG매크로 사용 - RPC 명명:
ServerRPC_,ClientRPC_접두사 사용
기존 시스템 활용:
APlayerActor: InteractionSystem, VoiceConversationSystem 내장APlayerControl: Enhanced Input System 사용ALingoPlayerState: 퀘스트 데이터 저장UKLingoNetworkSystem: HTTP/WebSocket 통신ANPCExaminer: InteractComponent 기반 상호작용
요구사항: 아래 설명된 기능들을 C++ 코드로 구현해 주세요. 기존 시스템을 최대한 활용하고 중복 구현을 피해주세요.
1. ANPCExaminer 액터 구현
ACharacter를 상속받는 ANPCExaminer 클래스에 다음 기능들을 구현합니다.
1.1. 주요 프로퍼티
// ANPCExaminer.h
// Copyright (c) 2025 Doppleddiggong. All rights reserved.
protected:
/// @brief 현재 상호작용 또는 심사 중인 플레이어 (서버에서만 유효)
UPROPERTY()
TObjectPtr targetPlayer;
/// @brief 심사관이 현재 다른 플레이어와 상호작용 중인지 여부 (모든 클라이언트에 복제)
UPROPERTY(Transient, ReplicatedUsing = OnRep_bIsBusy)
bool bIsBusy;
/// @brief 현재 심사 중인 플레이어의 이름 (UI 표기용, 모든 클라이언트에 복제)
UPROPERTY(Transient, Replicated)
FString busyPlayerName;
/// @brief 플레이어를 밀어내는 힘이 적용될 영역을 나타내는 Sphere Collision
/// @note 이미 PlayerDetectSphereComp가 존재하므로 이를 활용하거나 별도 생성
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "SpeakQuest")
TObjectPtr exclusionSphere;
/// @brief InteractionSystem 연동을 위한 상호작용 컴포넌트
/// @note KLingo_SpeakScenario.md의 InteractComponent 기반 상호작용 패턴 준수
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Interaction")
TObjectPtr interactComponent;
1.2. 상호작용 및 상태 관리
중요: KLingo_SpeakScenario.md의 InteractComponent 기반 아키텍처를 준수합니다.
OnPlayerInteract(APlayerActor* InteractingPlayer)함수interactComponent->OnInteracted델리게이트에 바인딩되는 함수입니다.- 플레이어의
InteractionSystem이 이 NPC의InteractComponent를 감지하면 자동 호출됩니다. bIsBusy가true이면:InteractingPlayer의 컨트롤러(APlayerControl)에게ClientRPC_ShowToast를 호출하여 "현재 {busyPlayerName} 유저의 심사 진행중입니다" 메시지를 띄웁니다.
bIsBusy가false이면:- 연결된
SpeakStage->RequestJoinConversation(InteractingPlayer)를 호출하여 플레이어를 대화 큐에 추가합니다. SpeakStageActor가 턴 기반 순서를 관리하므로 NPC는 직접bIsBusy를 설정하지 않습니다.
- 연결된
OnRep_bIsBusy()함수bIsBusy상태가 변경될 때 모든 클라이언트에서 호출됩니다.true일 경우: NPC 외형에 "심사 중"임을 나타내는 시각적 표시 (예: 머티리얼 변경, 이펙트 활성화, 머리 위 위젯 표시)를 활성화합니다.false일 경우: 시각적 표시를 비활성화합니다.- NPC의 상태(예: Idle, Busy)에 따른 애니메이션 전환 로직을 여기에 추가합니다.
1.3. NPC 시선 처리 및 디버깅 (Tick)
Tick함수 내에서 현재 발화자를 바라보도록 구현합니다.SpeakStage의GetCurrentSpeaker()를 통해 현재 발화 중인 플레이어를 가져옵니다.- 현재 발화자가 유효할 때:
- NPC의 머리가 발화자를 부드럽게 바라보도록
FindLookAtRotation과RInterpTo를 사용해ControlRotation을 갱신합니다. #if ENABLE_DRAW_DEBUG전처리기 내부에서, NPC의 머리 소켓에서 발화자의 머리 위치까지 녹색DrawDebugLine을 그려 시각적으로 누구를 보고 있는지 표시합니다.
- NPC의 머리가 발화자를 부드럽게 바라보도록
1.4. 다른 플레이어 밀어내기 (서버 로직)
Tick함수 또는 0.5초 주기의 타이머에서 서버(HasAuthority())에서만 실행합니다.SpeakStage의GetCurrentSpeaker()를 통해 현재 발화자를 가져옵니다.- 현재 발화자가 있을 때:
exclusionSphere내에 현재 발화자가 아닌 다른 플레이어가 있는지 감지합니다.- 감지된 다른 플레이어가 있다면,
LaunchCharacter함수 등을 사용해 영역 바깥으로 부드럽게 밀어냅니다.
2. ALingoPlayerState 데이터 확장
중요: 기존 ALingoPlayerState는 ReadQuest 데이터를 관리하므로, SpeakQuest 데이터를 별도로 추가합니다.
// ALingoPlayerState.h
// Copyright (c) 2025 Doppleddiggong. All rights reserved.
/// @brief API로부터 받아온 SpeakQuest 질문/답변 데이터 구조체
USTRUCT(BlueprintType)
struct FSpeakStageQuestion
{
GENERATED_BODY()
/// @brief 질문 텍스트
UPROPERTY(BlueprintReadOnly)
FString questionText;
/// @brief 질문 음성 파일 URL (TTS 생성 또는 사전 녹음)
UPROPERTY(BlueprintReadOnly)
FString questionVoiceURL;
/// @brief 허용 가능한 답변 목록 (채점용)
UPROPERTY(BlueprintReadOnly)
TArray acceptableAnswers;
};
// ALingoPlayerState 클래스에 추가:
public:
//--------------------------------------------------------------//
// Speak Quest Functions
//--------------------------------------------------------------//
/// @brief SpeakQuest 질문 목록
/// @note SpeakStageActor에서 관리하는 방식도 고려 가능
UPROPERTY(Transient, Replicated, BlueprintReadOnly, Category = "SpeakQuest")
TArray speakQuestData;
/// @brief 현재 진행 중인 질문 인덱스
UPROPERTY(Transient, Replicated, BlueprintReadOnly, Category = "SpeakQuest")
int32 currentSpeakQuestStep;
대안: ASpeakStageActor에서 플레이어별 진행 상태를 TMap 형태로 관리하는 것도 가능합니다.
3. SpeakQuest 진행 로직
중요: UKLingoNetworkSystem을 활용하여 HTTP 통신을 수행합니다.
3.1. ServerRPC_BeginSpeakQuest(APlayerActor* Player)
ASpeakStageActor또는ANPCExaminer에 구현된 서버 전용 함수입니다.네트워크 시스템 활용:
UKLingoNetworkSystem* NetworkSystem = UKLingoNetworkSystem::Get(GetWorld()); if (!NetworkSystem) return; NETWORK_LOG(ENetworkLogType::GET, TEXT("[SpeakQuest] Requesting question data")); // HTTP 요청 콜백 auto OnResponse = [this, Player](bool bSuccess, const FResponseSpeakQuest& Response) { if (bSuccess) { // PlayerState에 데이터 저장 if (ALingoPlayerState* PS = Player->GetPlayerState()) { PS->speakQuestData = Response.questions; PS->currentSpeakQuestStep = 0; } // PlayerController에 첫 질문 전달 if (APlayerControl* PC = Cast (Player->GetController())) { PC->ClientRPC_AskQuestion(Response.questions[0]); } } else { NETWORK_LOG(ENetworkLogType::ERROR, TEXT("[SpeakQuest] Failed to load questions")); } }; NetworkSystem->RequestSpeakQuestData(OnResponse);
3.2. ClientRPC_AskQuestion(const FSpeakStageQuestion& Question)
APlayerControl에 구현된 클라이언트 RPC입니다.기존 시스템 활용:
/// @brief 클라이언트에 질문을 전달하고 음성 재생 /// @param Question [in] 질문 데이터 UFUNCTION(Client, Reliable) void ClientRPC_AskQuestion(const FSpeakStageQuestion& Question); void APlayerControl::ClientRPC_AskQuestion_Implementation(const FSpeakStageQuestion& Question) { // UI 업데이트 (MainWidget 활용) APlayerActor* Player = Cast(GetPawn()); if (!Player) return; UMainWidget* MainWidget = Player->GetMainWidget(); if (MainWidget) { MainWidget->ShowSpeakQuestion(Question.questionText); } // VoiceConversationSystem 통한 음성 재생 if (Player->VoiceConversationSystem) { Player->VoiceConversationSystem->PlayQuestionAudio(Question.questionVoiceURL); } }
4. 플레이어 컨트롤러 (APlayerControl) 로직
중요: 기존 IA_Record InputAction을 활용합니다.
4.1. 퀘스트 중 입력 처리
질문 다시 듣기 (새 InputAction 추가 권장:
IA_Replay)- 새 InputAction
IA_Replay를 추가하거나 UI 버튼을 통해 요청합니다. ServerRPC_RequestReplayQuestion()RPC를ASpeakStageActor에 호출합니다.- 서버는 현재 플레이어의
currentSpeakQuestStep에 맞는 질문 데이터를 다시ClientRPC_AskQuestion으로 보내줍니다.
- 새 InputAction
음성 녹음 및 전송 (기존
IA_Record활용)- 이미 구현된
OnRecordPressed/OnRecordReleased핸들러를 확장합니다. - 누를 때:
VoiceConversationSystem->StartRecording()호출- UI에 "녹음 중" 표시
- 뗄 때:
VoiceConversationSystem->StopRecording()호출- 녹음된 오디오 데이터를 가져와
ServerRPC_SubmitAnswer(const TArrayRPC 호출& VoiceData)
- 이미 구현된
4.2. 답변 제출 및 결과 처리
중요: UKLingoNetworkSystem을 활용하여 STT 및 채점 API 통신을 수행합니다.
ServerRPC_SubmitAnswer(const TArray& VoiceData) APlayerControl또는ASpeakStageActor에서 호출하는 서버 RPC입니다.- 네트워크 시스템 활용: ```cpp UKLingoNetworkSystem* NetworkSystem = UKLingoNetworkSystem::Get(GetWorld()); if (!NetworkSystem) return;
NETWORK_LOG(ENetworkLogType::POST, TEXT("[SpeakQuest] Submitting answer"));
auto OnResponse = this, Player {
if (bSuccess) { // 채점 결과를 클라이언트에 전달 if (APlayerControl* PC = Cast(Player->GetController())) { PC->ClientRPC_ShowAnswerFeedback(Response.bIsCorrect, Response.feedbackMessage); } } else { NETWORK_LOG(ENetworkLogType::ERROR, TEXT("[SpeakQuest] Evaluation failed")); } };
NetworkSystem->RequestSpeakEvaluation(VoiceData, OnResponse); ```
ClientRPC_ShowAnswerFeedback(bool bIsCorrect, const FString& FeedbackMessage)APlayerControl에 구현된 클라이언트 RPC입니다.- 기존 토스트 시스템 활용:
void APlayerControl::ClientRPC_ShowAnswerFeedback_Implementation(bool bIsCorrect, const FString& FeedbackMessage) { // 기존 Client_ToastMessage 활용 Client_ToastMessage(FeedbackMessage); }
다음 단계로 넘어가기
- 피드백 확인 후, 플레이어가 "다음" 버튼을 클릭하거나 특정 키를 누르면
ASpeakStageActor->NotifyAnswerComplete(Player)RPC를 호출합니다. KLingo_SpeakScenario.md의 턴 기반 아키텍처에 따라SpeakStageActor가 단계 진행을 관리합니다.
- 피드백 확인 후, 플레이어가 "다음" 버튼을 클릭하거나 특정 키를 누르면
4.3. 퀘스트 종료
중요: KLingo_SpeakScenario.md의 턴 기반 아키텍처에 따라 ASpeakStageActor가 퀘스트 종료를 관리합니다.
ASpeakStageActor::AdvanceToNextPlayer():- 현재 플레이어의 모든 단계가 완료되면 자동 호출됩니다.
PlayerQueue에서 현재 플레이어를 제거하고 다음 플레이어로 전환합니다.- 현재 플레이어의 컨트롤러에게
ClientRPC_ShowResultScreen()RPC를 호출합니다.
APlayerControl::ClientRPC_ShowResultScreen():- 최종 결과 UI를 화면에 표시합니다.
기존 MainWidget 활용:
void APlayerControl::ClientRPC_ShowResultScreen_Implementation() { APlayerActor* Player = Cast(GetPawn()); if (!Player) return; UMainWidget* MainWidget = Player->GetMainWidget(); if (MainWidget) { MainWidget->ShowSpeakQuestResult(); } }
추가 권장 사항
A. 네트워크 응답 구조체 정의
NetworkData.h에 다음 구조체를 추가하세요:
/// @brief SpeakQuest 질문 데이터 API 응답
USTRUCT(BlueprintType)
struct FResponseSpeakQuest
{
GENERATED_BODY()
UPROPERTY(BlueprintReadOnly)
TArray questions;
void SetFromHttpResponse(const FString& JsonString);
void PrintData() const;
};
/// @brief 음성 평가 결과 API 응답
USTRUCT(BlueprintType)
struct FResponseEvaluation
{
GENERATED_BODY()
UPROPERTY(BlueprintReadOnly)
bool bIsCorrect;
UPROPERTY(BlueprintReadOnly)
FString feedbackMessage;
UPROPERTY(BlueprintReadOnly)
float score;
void SetFromHttpResponse(const FString& JsonString);
void PrintData() const;
};
B. UKLingoNetworkSystem 확장
// UKLingoNetworkSystem.h
/// @brief SpeakQuest 질문 데이터 요청
/// @param Callback [in] 응답 콜백
void RequestSpeakQuestData(TFunction Callback);
/// @brief 음성 답변 평가 요청
/// @param VoiceData [in] 녹음된 음성 데이터
/// @param Callback [in] 응답 콜백
void RequestSpeakEvaluation(const TArray& VoiceData, TFunction Callback);
C. VoiceConversationSystem 확장
// UVoiceConversationSystem.h
/// @brief 질문 음성 재생
/// @param AudioURL [in] 음성 파일 URL
void PlayQuestionAudio(const FString& AudioURL);
/// @brief 음성 녹음 시작
void StartRecording();
/// @brief 음성 녹음 중지 및 데이터 반환
/// @return 녹음된 오디오 데이터
TArray StopRecording();
최종 체크리스트
필수 확인 사항
- [ ] 모든 파일 헤더에 저작권 표시 추가
- [ ] 변수명 camelCase 컨벤션 준수 (bool은 bPascalCase)
- [ ]
TObjectPtr<>사용으로 포인터 선언 - [ ] Doxygen 주석 스타일(
/// @brief,/// @param) 적용 - [ ] RPC 함수명
ServerRPC_,ClientRPC_접두사 사용 - [ ]
NETWORK_LOG매크로로 네트워크 로깅 - [ ]
KLingo_SpeakScenario.md아키텍처 준수
기존 시스템 통합
- [ ]
APlayerActor::InteractionSystem활용 - [ ]
APlayerActor::VoiceConversationSystem활용 - [ ]
APlayerControl::IA_RecordInputAction 활용 - [ ]
APlayerControl::Client_ToastMessage활용 - [ ]
UMainWidgetUI 시스템 활용 - [ ]
UKLingoNetworkSystemHTTP/WebSocket 통신 활용 - [ ]
ANPCExaminer::InteractComponent상호작용 활용
새로 구현할 컴포넌트
- [ ]
ASpeakStageActor(턴 기반 대화 관리자) - [ ]
FSpeakStageQuestion구조체 - [ ]
FResponseSpeakQuest응답 구조체 - [ ]
FResponseEvaluation응답 구조체 - [ ]
UKLingoNetworkSystem::RequestSpeakQuestData() - [ ]
UKLingoNetworkSystem::RequestSpeakEvaluation() - [ ]
UVoiceConversationSystem::PlayQuestionAudio() - [ ]
UVoiceConversationSystem::StartRecording() - [ ]
UVoiceConversationSystem::StopRecording()
구현 우선순위
Phase 1: 기반 구조 (High Priority)
ASpeakStageActor클래스 생성 및 턴 기반 아키텍처 구현FSpeakStageQuestion구조체 정의ANPCExaminerInteractComponent 연동ALingoPlayerStateSpeakQuest 데이터 추가
Phase 2: 네트워크 통합 (High Priority)
FResponseSpeakQuest응답 구조체 추가FResponseEvaluation응답 구조체 추가UKLingoNetworkSystemAPI 메서드 확장- 네트워크 로깅 통합
Phase 3: 음성 시스템 (Medium Priority)
VoiceConversationSystem확장APlayerControl녹음 입력 핸들러 확장- 음성 재생 및 STT 통합
Phase 4: UI 및 피드백 (Medium Priority)
UMainWidgetSpeakQuest UI 추가- 진행률 표시 위젯
- 토스트 메시지 통합
Phase 5: 테스트 및 최적화 (Low Priority)
- 단일/멀티플레이어 테스트
- 턴 전환 검증
- 네트워크 지연 시뮬레이션
- 디버그 시각화
주요 변경 사항 요약
| 항목 | 변경 전 (프롬프트 원본) | 변경 후 (프로젝트 적합) |
|---|---|---|
| PlayerController | AKPlayerController |
APlayerControl |
| PlayerState | KPlayerState |
ALingoPlayerState |
| PlayerCharacter | APlayerCharacter |
APlayerActor |
| 구조체 명명 | FSpeakQuestQuestion |
FSpeakStageQuestion |
| RPC 명명 | Client_ShowToast |
ClientRPC_ShowToast |
| 상호작용 | 커스텀 OnInteract RPC |
InteractComponent 델리게이트 |
| 음성 시스템 | 새로운 VoiceCapture | 기존 VoiceConversationSystem |
| 네트워크 | FHttpModule 직접 사용 |
UKLingoNetworkSystem 활용 |
| 로깅 | 일반 로깅 | NETWORK_LOG 매크로 |
| 변수명 | PascalCase | camelCase (bool 제외) |
참고 문서
- 아키텍처:
Documents/DevLog/AgentLog/KLingo_SpeakScenario.md - 코딩 컨벤션:
AgentRule/Project/Onepiece/CODING_CONVENTIONS.md - UE 기본 규칙:
AgentRule/Project/ue_coding_conventions.md - 프로젝트 개요:
AgentRule/Project/Onepiece/PROJECT_OVERVIEW.md
문서 버전: v2.0 (프로젝트 적합성 개선) 마지막 수정: 2025-12-11 수정자: Claude Agent