2026-07-06 TIL (91일차)
2026-07-06 TIL (91일차)
[UE5 C++] 멀티플레이어 러너 게임: 데미지 판정 및 델리게이트 실행 순서 트러블슈팅 (Dedicated Server)
1. 장애물 충돌 시 1초 무적 및 무적 관통(예외) 처리
문제 상황
- 플레이어가 장애물(Collision Box) 영역에 계속 머물면
Tick단위로 피격 판정이 들어가 순식간에 사망함. - 이를 막기 위해 1초 무적(
bIsInvincible)을 부여했으나, 에너미(FlyingEnemy)의 특수 공격(시간 초과 시 발동)까지 무적에 씹히는 문제 발생.
해결 방법
ApplyPlayerDamage 함수에 무적 판정을 무시할지 결정하는 매개변수(bIgnoreInvincibility)를 추가하여 공격 타입에 따라 분기 처리.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// SharedRailCharacter.h
void ApplyPlayerDamage(float Damage, AActor* DamageCauser, bool bIgnoreInvincibility = false);
// SharedRailCharacter.cpp
void ASharedRailCharacter::ApplyPlayerDamage(float Damage, AActor* DamageCauser, bool bIgnoreInvincibility)
{
// 무적 무시 옵션이 꺼져있고(&&) 현재 무적 상태라면 데미지 무시
if (!bIgnoreInvincibility && bIsInvincible) return;
CurrentPlayerHP = FMath::Max(0.f, CurrentPlayerHP - Damage);
// 일반 장애물일 때만 1초 무적 부여
if (!bIgnoreInvincibility)
{
bIsInvincible = true;
GetWorldTimerManager().SetTimer(InvincibilityTimerHandle, this, &ASharedRailCharacter::ResetInvincibility, 1.0f, false);
}
}
1. 데미지 처리 방식 세분화 (무적 프레임 vs 무적 관통)
- 장애물 충돌 시:
ApplyPlayerDamage(Damage, Obstacle, false);(무적 시간 적용) - 에너미 공격 시:
ApplyPlayerDamage(Damage, Enemy, true);(무적 시간 무시 및 관통)
2. 하드코딩된 데미지를 데이터 기반(Data-Driven)으로 변경
- 문제 상황: 에너미는 블루프린트에서
AttackDamage를 설정해 델리게이트로 넘겨주고 있었으나, 장애물(RunnerObstacle)에는 데미지 변수가 존재하지 않아 매니저 클래스 내부에서 하드코딩된 값(1.0f)으로 체력을 깎고 있었습니다. - 해결 방법: 기존 델리게이트 서명(Signature)을 수정하여 복잡도를 높이는 대신, 장애물 클래스 자체에 데미지 프로퍼티와
Getter함수를 추가했습니다. 매니저는 넘겨받은 장애물의 포인터를 통해 데미지 값을 직접 조회(Getter)하여 적용하도록 구현했습니다.
1
2
3
4
5
6
// RunnerObstacle.h
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Obstacle|Damage")
float ObstacleDamage = 1.0f;
UFUNCTION(BlueprintPure, Category = "Obstacle|Damage")
float GetObstacleDamage() const { return ObstacleDamage; }
1
2
3
4
5
6
7
8
9
// SharedCharacterManager.cpp (장애물 충돌 수신부)
void ASharedCharacterManager::HandlePlayerHitObstacle(ARunnerObstacle* Obstacle, ...)
{
if (SharedCharacter && IsValid(Obstacle))
{
// 하드코딩(1.0f) 대신 장애물 객체에서 데미지 값을 가져옴
SharedCharacter->ApplyPlayerDamage(Obstacle->GetObstacleDamage(), Obstacle, false);
}
}
3. 캐릭터 사망 시 서버 자원 낭비 방지 (스포너 및 타이머 정지)
- 문제 상황: 플레이어의 체력이 0이 되어 사망하더라도(
CurrentHP <= 0), 맵에 배치된 스포너는 계속해서 몬스터를 생성하고, 비행 몬스터는 3초마다 허공에 공격 타이머를 작동시켜 Dedicated Server의 연산 자원을 낭비하고 있었습니다. - 해결 방법: 플레이어 사망 시 매니저를 통해 사망 이벤트를 발송(
Broadcast)하고, 이를 수신한 매니저가 맵 상의 모든 스포너와 활성화된 적들의 공격 타이머를 강제로 정지시키고 파괴(HandleRunnerTurnSucceeded(0.0f))하도록 처리했습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// SharedCharacterManager.cpp
void ASharedCharacterManager::HandlePlayerDeathAndStopGameplay()
{
// 모든 적 스포너 정지 및 기존 적 제거
TArray<AActor*> EnemySpawners;
UGameplayStatics::GetAllActorsOfClass(GetWorld(), AFlyingEnemySpawner::StaticClass(), EnemySpawners);
for (AActor* Actor : EnemySpawners)
{
if (AFlyingEnemySpawner* Spawner = Cast<AFlyingEnemySpawner>(Actor))
{
Spawner->StopSpawning();
Spawner->HandleRunnerTurnSucceeded(0.0f); // 즉시 삭제
}
}
// 장애물 스포너 정지 로직 병행...
}
4. 델리게이트 발송은 되는데 수신이 안 되는 문제 해결 (BeginPlay 실행 순서 이슈)
- 문제 상황: 장애물 충돌 로그에서는
"[OBSTACLE] 닿은 액터가 플레이어로 판정됨! 델리게이트 발송."이라는 메시지가 정상적으로 출력되지만, 매니저가 이를 수신하지 못해 데미지 처리가 누락되는 현상이 발생했습니다. - 원인 분석 (Execution Order 엇갈림):
RunnerObstacleSpawner가BeginPlay를 실행하며 맵에 장애물을 즉시 생성(bGenerateOnBeginPlay = true).- 장애물 생성 직후 델리게이트(
OnObstacleSpawned)를 발송하지만, 이때 매니저(SharedCharacterManager)는 아직 초기화(BeginPlay)되지 않은 상태. - 한발 늦게 실행된 매니저가 스포너에 바인딩을 시도하지만, 이미 맵에 스폰되어버린 과거의 장애물들과는 연결고리가 형성되지 않음.
- 해결 방법 (투트랙 바인딩 구조): 매니저의
BeginPlay시점에 다음 두 가지 작업을 모두 수행하여 완벽한 그물망 구조를 구축했습니다.- 미래에 스폰될 장애물 바인딩: 스포너를 구독.
- 이미 스폰된 과거의 장애물 바인딩: 맵에 존재하는 모든 장애물을 전수조사(
GetAllActorsOfClass)하여 개별 바인딩.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// SharedCharacterManager.cpp (BeginPlay)
// 1. 미래 대비: 스포너에 바인딩 (게임 도중 추가 스폰이나 트리거 스폰 대비)
TArray<AActor*> ObstacleSpawners;
UGameplayStatics::GetAllActorsOfClass(GetWorld(), ARunnerObstacleSpawner::StaticClass(), ObstacleSpawners);
for (AActor* Actor : ObstacleSpawners) { ... AddDynamic ... }
// 2. 과거 싹쓸이: 이미 맵에 존재해버린 장애물들을 직접 찾아 바인딩 [핵심 해결책]
TArray<AActor*> ExistingObstacles;
UGameplayStatics::GetAllActorsOfClass(GetWorld(), ARunnerObstacle::StaticClass(), ExistingObstacles);
for (AActor* Actor : ExistingObstacles)
{
if (ARunnerObstacle* Obstacle = Cast<ARunnerObstacle>(Actor))
{
// 멱살 잡고 직접 연결
Obstacle->OnPlayerHitObstacle.AddUniqueDynamic(this, &ASharedCharacterManager::HandlePlayerHitObstacle);
}
}
결과: 언리얼 엔진 특유의 불규칙한 액터 초기화(BeginPlay) 순서가 발생하더라도 절대 바인딩 누락이 발생하지 않는 견고한 구조를 완성했습니다.
This post is licensed under CC BY 4.0 by the author.