StartInGame()
StartScouting()이 60초 타이머를 걸어두고 있었으니 StartInGame()에서
1. 정찰 모드 끔
2. CurrentPhase를 InGame으로 전환
3. AliveHiderCount 저장
4. 300초 후 EndMatch() 예약
void AFDGameMode::StartInGame() // 본게임 시작
{
// 정찰 모드 종료 — Scouting에서 켰던 걸 대칭으로 끔
for (APlayerState* PS : GameState->PlayerArray)
{
const AFDPlayerState* FDPS = Cast<AFDPlayerState>(PS);
if (!FDPS || FDPS->RoleTag != EFDRole::Tagger) continue;
if (AFDTaggerCharacter* TaggerChar = Cast<AFDTaggerCharacter>(FDPS->GetPawn()))
{
TaggerChar->SetScoutingMode(false);
}
}
AFDGameState* FDGameState = GetGameState<AFDGameState>();
if (!FDGameState) return;
FDGameState->CurrentPhase = EMatchPhase::InGame;
UE_LOG(LogTemp, Warning, TEXT("[GameMode] InGame 단계 시작"));
// 생존한 숨는사람 수 저장해두기
int32 HiderCount = 0;
for (APlayerState* PS : GameState->PlayerArray)
{
if (const AFDPlayerState* FDPS = Cast<AFDPlayerState>(PS))
{
if (FDPS->RoleTag == EFDRole::Hider)
{
++HiderCount;
}
}
}
FDGameState->AliveHiderCount = HiderCount; // 그걸 GameState의 replication 되는 alivehidercount에 넣음
// 300초 후 게임 종료
GetWorldTimerManager().SetTimer(PhaseTimerHandle, this, &AFDGameMode::EndMatch, 300.f, false);
}
포획 판정
캐릭터 쪽엔 이미 겹침 감지 → 포획 시퀀스 로직이 짜여 있었음 (팀원 작성 로직)
void AFDTaggerCharacter::OnCaptureCollisionOverlap(...)
{
if (CapturedHider) return;
ACharacter* HiderCharacter = Cast<ACharacter>(OtherActor);
if (!HiderCharacter) return;
Internal_StartCaptureSequence(HiderCharacter);
}
근데 판정은 GameMode에서 하는 게 더 안전할 것 같아서 팀원에게 양해 구하고 수정
Overlap → GameMode->RequestCaptureJudgement → 승인되면 → StartCaptureSequence → Internal_
void AFDGameMode::RequestCaptureJudgement(AFDTaggerCharacter* TaggerCharacter, ACharacter* HiderCharacter)
{
if (!TaggerCharacter || !HiderCharacter) return;
AFDGameState* FDGameState = GetGameState<AFDGameState>();
if (!FDGameState || FDGameState->CurrentPhase != EMatchPhase::InGame) return;
// 본게임 중이 아니면 판정 자체를 시작 안 함
TaggerCharacter->StartCaptureSequence(HiderCharacter);
}
판정 종료
판정은 포획/ 봐주기/ 타이머 만료 3개로 나뉘는데 세 경로 모두 공통으로 해야 하는 일이 있음
판정 타이머 정리, 양쪽 입력 복구, GameMode에 결과 통보, CapturedHider 초기화
그래서 이 공통 로직을 하나의 함수에 묶어서 넣음
void AFDTaggerCharacter::Internal_ResolveCaptureLocally(bool bWasCaptured)
// 아웃이든 봐주기든 똑같이 실행되어야 하는건 하나로 묶음
{
// 타이머 끄기 (결과가 정해졌으니 자동 만료 타이머 필요 없음)
GetWorldTimerManager().ClearTimer(CaptureJudgeTimerHandle);
// 양쪽 입력 원복시킴
if (APlayerController* TaggerPC = Cast<APlayerController>(GetController()))
{
TaggerPC->EnableInput(TaggerPC);
}
if (APlayerController* HiderPC = Cast<APlayerController>(CapturedHider->GetController()))
{
HiderPC->EnableInput(HiderPC);
}
// 게임모드에 알림
if (AFDGameMode* FDGameMode = GetWorld()->GetAuthGameMode<AFDGameMode>())
{
FDGameMode->ResolveCapture(CapturedHider, bWasCaptured);
}
CapturedHider = nullptr;
}
void AFDTaggerCharacter::ForceOut() // 아웃 누르면 호출될 함수
{
if (!CapturedHider) return; // 판정 중인 게 없으면 무시
Internal_ResolveCaptureLocally(true);
}
void AFDTaggerCharacter::RequestSpare() // 봐주기 누르면 호출될 함수
{
if (!CapturedHider) return;
ACharacter* SparedHider = CapturedHider;
// 봐주기 버프: 속도 증가
if (UCharacterMovementComponent* Movement = SparedHider->GetCharacterMovement())
{
Movement->MaxWalkSpeed = 1200.f;
}
// 무적처리
// 10초 뒤 버프 해제(대략 적음)
FTimerHandle SpareTimerHandle;
GetWorldTimerManager().SetTimer(SpareTimerHandle, [this, SparedHider]()
{
OnSpareExpired(SparedHider);
}, 10.f, false);
Internal_ResolveCaptureLocally(false); // 봐줬으니까 capture은 false로
}
RequestSquare 함수에서 Internal_ResolveCaptureLocally를 호출하고 나면 CapturedHider가 nullptr이 되는데
봐주기 버프 해제 타이머(OnSpareExpired)는 5초 뒤에도 그 캐릭터를 알아야 함
그래서 Internal_ 호출 전에 SparedHider라는 별도 변수로 미리 캐릭터를 저장해두고 그걸 넘김
GameMode 쪽 ResolveCapture는 카운트 감소와 종료 조건 체크를 담당한다.
void AFDGameMode::ResolveCapture(ACharacter* HiderCharacter, bool bWasCaptured)
{
if (!HiderCharacter || !bWasCaptured) return; // 봐줌이면 GameMode가 할 일 없음
AFDGameState* FDGameState = GetGameState<AFDGameState>();
if (!FDGameState) return;
--FDGameState->AliveHiderCount;
if (FDGameState->AliveHiderCount <= 0)
{
EndMatch();
}
}
전체 흐름 정리
술래의 콜리전 안에 숨는 사람 들어왔을 때 공격 버튼 누름 (마우스 좌클릭)
-> Server_TryCapture() 호출
-> Server_TryCapture_Implementation() 실행
-> CaptureCollision 콜리전 켬
-> 그 사이에 하이더랑 물리적으로 겹치면 OnCaptureCollisionOverlap() 실행
-> GameMode->RequestCaptureJudgement() 호출
-> 승인되면 캐릭터의 StartCaptureSequence() 호출 -> Internal_StartCaptureSequence()
-> // 여기서부터는 UI 작업 해야 디버깅 가능해짐
Client_ShowCapturePopup() (술래 화면에 UI 팝업)
- 아웃 선택시 Server_RequestOut (Onclicked으로 함수 넣어야함) -> ForceOut() -> Internal_ResolveCaptureLocally(true)
- 봐주기 선택시 Server_RequestSpare (마찬가지) -> RequestSpare() -> Internal_ResolveCaptureLocally(false)
- 시간초과시 -> OnCaptureTimerExpired() -> Internal_ResolveCaptureLocally(true)
알게된 것
RPC는 클라이언트 -> 서버로 물리적으로 다른 프로세스 경계를 건널 때만 필요함
서버 프로세스 안에서 캐릭터가 GameMode를 부르는 건 RPC 말고 그냥 GameMode-> 이렇게 불러도 됨
'프로젝트 > Funny Or Die (멀티플레이 술래잡기 게임)' 카테고리의 다른 글
| EndMatch 구현 (게임 한 바퀴 완주) (0) | 2026.07.03 |
|---|---|
| 포획/봐주기 팝업 위젯 (0) | 2026.07.03 |
| 정찰 단계(Scouting) 구현하기 (0) | 2026.07.01 |
| Seamless Travel 이후 캐릭터 입력이 먹지 않던 문제 (0) | 2026.07.01 |
| 로비 UI 협업 (0) | 2026.06.30 |
