본문 바로가기
언리얼엔진

언리얼 공부노트 : C++ 기반 탱크 슈팅 게임 시스템 설계 및 구현

flyon 2026. 4. 15.

1. 발사체 클래스 구조 및 생성 로직

1.1 발사체 기본 클래스 구축

발사체 시스템을 구현하기 위해 Projectile C++ 클래스를 생성한다. 발사체는 별도의 사용자 입력 처리가 필요하지 않으므로, 폰(Pawn)이 아닌 기본 액터(Actor) 클래스를 상속받아 구현하는 것이 적합하다. 이후 블루프린트 에디터에서 직접 편집할 수 있도록 스태틱 메시 변수를 선언하여 객체의 외형을 설정한다.

 

private:
    UPROPERTY(EditDefaultsOnly)
    UStaticMeshComponent* ProjectileMesh;

생성자 내부에서 기본 하위 객체를 생성한 뒤 이를 루트 컴포넌트로 지정한다. 발사체는 매 프레임 틱(Tick) 연산이 필요하지 않으므로 해당 속성을 비활성화하여 성능을 최적화한다. 최종적으로 이 C++ 클래스를 기반으로 BP_Projectile 블루프린트를 생성하고 스태틱 메시 프로퍼티를 구체적으로 할당한다.

 

1.2 동적 스폰 메커니즘

레벨 실행 중 발사체를 동적으로 스폰하기 위해 TSubclassOf 템플릿을 활용한다. 이는 파라미터로 지정된 특정 타입이나 그 하위 클래스를 나타내는 UClass 변수를 타입 안정성을 유지하며 저장하는 역할을 수행한다.

 

UPROPERTY(EditDefaultsOnly)
TSubclassOf<class AProjectile> ProjectileClass;

UClass는 C++ 스크립트와 블루프린트 간의 데이터 리플렉션을 지원한다. 객체를 월드에 스폰할 때는 UWorld의 SpawnActor 함수를 호출하며, 인자로 앞서 선언한 UClass 정보와 트랜스폼(위치 및 회전값) 데이터를 전달한다.

 


 

2. 컴포넌트 기반 기능 구현

 

2.1 이동 물리 컴포넌트 추가

발사체의 물리적인 비행 궤적 연산을 엔진 시스템에 위임하기 위해 전용 이동 컴포넌트를 부착한다. 이를 위해 언리얼 엔진에 내장된 ProjectileMovementComponent를 변수로 선언하고 초기화 작업을 진행한다.

private:
    UPROPERTY(VisibleAnywhere)
    class UProjectileMovementComponent* ProjectileMovementComponent;

클래스 생성자 내에서 발사체의 초기 속도와 최고 속도 값을 지정하여 실제 물리 법칙과 유사한 궤적으로 날아가도록 구성한다.

 

2.2 체력 관리 컴포넌트 설계

피격 대상의 체력 상태를 효율적으로 관리하기 위해 HealthComponent라는 독립된 클래스를 작성한다. 기능을 분리함으로써 탱크나 타워 등 체력이 필요한 다양한 액터에 자유롭게 컴포넌트 형태로 부착할 수 있어 재사용성이 높아진다.

컴포넌트 분류 역할 및 특징 트랜스폼 유무 타 컴포넌트 부착 여부
UActorComponent 시스템의 가장 기본이 되는 논리적 컴포넌트이다. X X
USceneComponent 지오메트리 렌더링 및 물리적 배치를 담당한다. O

체력 컴포넌트 내부의 프라이빗 영역에는 최대 체력과 현재 체력을 저장할 실수형 변수를 선언한다. 이어서 언리얼 엔진의 데미지 시스템과 연동할 콜백 함수를 구현하고, GetOwner 함수를 통해 컴포넌트를 소유한 액터에 접근하여 데미지 관련 델리게이트에 해당 함수를 바인딩한다.

 


 

3. 타격 이벤트와 데미지 시스템

 

3.1 멀티캐스트 델리게이트 바인딩

메시 객체가 다른 표면과 충돌하면 OnComponentHit 이벤트가 트리거된다. 이 이벤트는 여러 함수를 동시에 연결할 수 있는 멀티캐스트 델리게이트 기반으로 동작하며, 브로드캐스트가 발생하면 인보케이션 리스트에 등록된 모든 함수가 일제히 호출된다. 이때 바인딩 대상이 되는 콜백 함수는 언리얼 엔진 시스템이 정상적으로 인식할 수 있도록 반드시 UFUNCTION 매크로를 지정해야 한다.

UFUNCTION()
void OnHit(UPrimitiveComponent* HitComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, FVector NormalImpulse, const FHitResult& Hit);

AddDynamic 함수를 활용하여 런타임 환경에서 콜백 함수를 히트 이벤트 델리게이트에 동적이고 안전하게 연결한다.

 

3.2 글로벌 데미지 발생 로직

충돌 이벤트가 감지되면 타격 대상에게 피해를 적용하기 위해 UGameplayStatics 클래스의 ApplyDamage 함수를 호출한다. 데미지 처리를 정확하게 수행하려면 다음과 같은 필수 인자들을 구성하여 전달해야 한다.

  • DamagedActor: 물리적 충돌이 발생하여 피해를 입을 상대 액터 객체를 의미한다.
  • BaseDamage: 시스템에 적용될 실제 데미지 수치를 결정한다.
  • EventInstigator: 데미지를 유발한 폰(Pawn)의 소유 컨트롤러를 지칭한다.
  • DamageCauser: 실질적인 피해 원인을 제공한 액터(예: 발사체 자체)를 의미한다.
  • DamageTypeClass: 엔진 내부에 정의된 데미지의 종류를 나타내는 UClass 포인터이다.

목표물에 데미지 이벤트를 성공적으로 브로드캐스트한 직후에는 Destroy 함수를 호출하여 역할을 다한 발사체 액터를 월드에서 즉시 소멸시킨다.

 


 

4. 라이프사이클 및 게임 규칙 제어

 

4.1 게임 모드 클래스 구축

전체적인 게임의 승패 규칙과 플레이어의 초기 상태를 제어하기 위해 AGameModeBase를 상속받는 전용 게임 모드 클래스를 생성한다. 해당 C++ 클래스를 기반으로 블루프린트를 도출한 뒤, 프로젝트 세팅에서 이를 기본 게임 모드로 확정한다. 게임 모드 프로퍼티 중 Default Pawn Class 항목에 플레이어가 조종할 기본 탱크 블루프린트를 할당한다. 이후 월드에 Player Start 액터를 배치하면 게임 모드가 해당 위치를 참조하여 디폴트 폰을 자동으로 스폰한다.

 

4.2 액터 파괴 프로세스

전투 중 체력이 모두 소진된 객체를 처리하기 위해 부모 클래스인 BasePawn에 HandleDestruction 함수를 선언한다. 탱크 객체는 파괴 시 틱 연산을 비활성화하고 메시를 화면에서 숨기는 방식으로 소멸을 대체한다. 반면 타워 객체는 Destroy 함수를 호출하여 메모리상에서 완전히 제거한다. 체력 컴포넌트가 잔여 체력 0을 감지하면 즉시 게임 모드의 ActorDied 함수를 호출하며, 이 함수 내부에서 대상이 탱크인지 타워인지 판별하여 각각에 맞는 후속 처리를 수행한다.

 


 

5. 입력 제어와 커스텀 컨트롤러

 

5.1 Player Controller 세팅

사용자 입력을 중앙에서 관리할 전용 플레이어 컨트롤러(Player Controller) 클래스를 설계한다. 내부적으로 SetPlayerEnabledState라는 사용자 지정 함수를 선언하고 불리언(Boolean) 값을 인자로 받아 처리하도록 구성한다.

void AToonTanksPlayerController::SetPlayerEnabledState(bool bPlayerEnabled)
{
    if (bPlayerEnabled)
    {
        GetPawn()->EnableInput(this);
    }
    else
    {
        GetPawn()->DisableInput(this);
    }
    bShowMouseCursor = bPlayerEnabled;
}

해당 함수는 GetPawn을 통해 현재 컨트롤러가 소유 중인 캐릭터 객체를 획득한 후, 인자 값에 따라 입력 활성화 여부를 결정한다. 또한, 조준의 편의성을 높이기 위해 플레이어 컨트롤러 블루프린트 설정에서 기본 마우스 커서의 모양을 십자선(Crosshair) 형태로 변경한다.

 

5.2 타이머 델리게이트 활용

게임 시작 직후 플레이어에게 주어지는 초기 대기 시간을 구현하기 위해 월드 타이머 매니저(World Timer Manager)를 활용한다. 단순한 함수 포인터 대신, 인풋 파라미터를 함께 전달할 수 있는 FTimerDelegate 구조체를 동적으로 생성하여 사용한다.

FTimerDelegate PlayerEnableTimerDelegate = FTimerDelegate::CreateUObject(ToonTanksPlayerController, &AToonTanksPlayerController::SetPlayerEnabledState, true);
GetWorldTimerManager().SetTimer(PlayerEnableTimerHandle, PlayerEnableTimerDelegate, StartDelay, false);

이 로직을 통해 게임 진입 시점에 플레이어의 입력을 우선 차단하고, 설정해 둔 지연 시간이 경과하면 타이머에 의해 자동으로 입력 처리가 복구되도록 시스템을 구축한다.

 


 

6. 게임 시작 위젯 설정과 블루프린트 이벤트

 

게임 시작을 알리는 텍스트와 카운트다운 타이머를 화면에 표시하기 위해 BlueprintImplementableEvent를 활용한다. 이 이벤트 지정자를 사용하면 C++에서 함수 호출을 담당하고 실제 세부 로직은 블루프린트에서 시각적으로 구현할 수 있다. 먼저 ToonTanksGameMode 헤더 파일의 protected 섹션에 함수를 선언하고 UFUNCTION 매크로에 해당 지정자를 추가한다.

protected:
    UFUNCTION(BlueprintImplementableEvent)
    void StartGame();

 

C++ 측에서는 별도의 함수 구현부를 작성할 필요가 없으며, HandleGameStart 함수 내에서 StartGame을 호출하기만 하면 된다. 이후 에디터의 BP_ToonTanksGameMode 블루프린트에서 해당 이벤트를 구체화한다.

UI 구성을 위해 User Interface 메뉴에서 Widget Blueprint를 생성하고 이름을 WBP_StartGameWidget으로 지정한다. 캔버스 패널 하위에 텍스트 블록을 추가한 뒤, 앵커를 정중앙으로 설정하고 Position X, Y를 0, Alignment를 0.5로 맞추어 화면 중앙에 정렬한다. 완성된 위젯을 화면에 띄우기 위해 블루프린트의 Event Start Game 노드 실행선을 Create Widget 노드에 연결하고 위젯 클래스를 지정한다. 최종적으로 반환된 위젯 객체를 Add to Viewport 노드에 연결하여 화면 출력을 완료한다.

 


 

7. 카운트다운 타이머 로직과 화면 표시

 

위젯 블루프린트의 이벤트 그래프에서 Event Tick 노드를 활용하여 매 프레임 시간을 차감하는 타이머 로직을 구현한다. Float 타입의 Countdown 변수를 생성하여 초기값을 3으로 설정한다. 매 틱마다 주어지는 DeltaTime을 Subtract 노드를 통해 기존 Countdown 값에서 차감하고, 그 결과를 Set Countdown 노드로 다시 갱신한다. 남은 시간을 정수로 깔끔하게 표시하기 위해 Ceil 노드를 사용하여 소수점을 올림 처리한다.

 

이후 Switch on Int 노드를 사용하여 남은 시간에 따라 다른 텍스트가 출력되도록 분기 처리를 진행한다. 디자이너 탭에서 텍스트 위젯의 Is Variable 속성을 체크하여 변수로 승격시킨 뒤, 이 변수를 그래프로 가져와 SetText 노드를 통해 동적으로 텍스트를 변경한다.

 


 

8. 승패 조건 판별 및 결과 HUD 출력

이 게임의 승리 조건은 월드 내의 모든 적 탑(Tower)을 파괴하는 것이며, 패배 조건은 플레이어의 탱크가 파괴되는 것이다. 승패에 따른 게임 종료 처리를 위해 ToonTanksGameMode 클래스에 블루프린트 구현 가능 이벤트를 선언한다.

UFUNCTION(BlueprintImplementableEvent)
void GameOver(bool bWonGame);

월드에 배치된 탑의 총 개수를 추적하기 위해 TargetTowers 변수와 GetTargetTowerCount 함수를 추가한다. GetAllActorsOfClass 함수를 호출하여 모든 ATower 액터의 포인터를 TArray 배열에 수집하고, Num 함수로 배열의 크기를 반환받아 TargetTowers의 초기값으로 설정한다. 게임 진행 중 탑이 파괴될 때마다 변수 값을 1씩 차감하며, 남은 개수가 0이 되면 GameOver 함수에 true를 전달해 승리로 처리한다. 반대로 플레이어 탱크가 먼저 파괴되면 false를 전달해 패배로 처리한다.

 

게임 오버 결과를 화면에 표시하기 위해 WBP_EndGameWidget을 생성하고 게임 모드 블루프린트 로직에 추가한다. Select 노드의 Index 핀에 승패 결과를 담은 불리언 변수를 연결하여, true일 경우 "You Won!"을, false일 경우 "You Lost!" 텍스트를 반환하게 한 뒤 이를 SetText 노드에 연결하여 최종 HUD를 구성한다.

 


 

9. 파티클 시스템을 이용한 특수 효과

 

발사체가 목표물에 적중하는 순간 시각적인 타격 효과를 주기 위해 발사체 헤더 파일에 UParticleSystem 포인터 변수를 추가한다.

UPROPERTY(EditAnywhere)
UParticleSystem* HitParticles;

OnHit 함수 내부에서 발사체가 파괴되기 직전, SpawnEmitterAtLocation 함수를 호출하여 충돌 위치와 회전값을 기반으로 타격 파티클을 생성한다. 발사체가 날아가는 궤적에 연기 효과를 더하기 위해서는 UParticleSystemComponent 포인터 변수를 선언하고, 생성자에서 CreateDefaultSubobject를 통해 컴포넌트를 부착한다. 이후 블루프린트 에디터에서 해당 컴포넌트의 Template에 궤적 파티클 에셋을 할당한다.

 

또한 탱크와 탑이 파괴될 때의 강렬한 폭발 효과를 구현하기 위해 BasePawn 헤더 파일에 DeathParticles 변수를 선언하고, 객체 파괴를 담당하는 HandleDestruction 함수 내부에서 SpawnEmitterAtLocation을 호출하여 폭발 파티클을 재생한다.

 


10. 사운드 효과 설정

 

게임의 타격감을 높이기 위해 발사체 발사음, 피격음, 객체 파괴음을 담당할 USoundBase 포인터 변수들을 선언한다.

UPROPERTY(EditAnywhere)
USoundBase* LaunchSound;

UPROPERTY(EditAnywhere)
USoundBase* HitSound;

선언된 변수들에 대한 사운드 에셋은 블루프린트 디테일 패널에서 각각 매핑한다. 런타임에 사운드를 재생할 때는 PlaySoundAtLocation 함수를 활용한다. 발사체의 BeginPlay 시점에는 LaunchSound를, 객체와 충돌하는 OnHit 시점에는 HitSound를 재생한다. 베이스 폰의 HandleDestruction 함수에서는 객체가 파괴되는 시점에 맞춰 DeathSound를 출력하도록 구성한다.

 


 

11. 카메라 셰이크 구현

타격 및 폭발 상황에서 화면이 흔들리는 역동적인 연출을 추가하기 위해 Camera Shake Base클래스를 상속받는 블루프린트를 생성한다.

카메라 셰이크 진동 속성 비교표

설정 항목 타격 카메라 셰이크 폭발 카메라 셰이크
경과시간 0.25 0.35
블렌드 인 및 아웃 0.05 0.1
진폭 X 및 Y축 10 50
진폭 Z축 100 -
주파수 X 및 Y축 10 20

C++ 코드에서 지정된 카메라 셰이크를 실행하기 위해 언리얼 최신 버전에 맞춰 UMatineeCameraShake 클래스의 TSubclassOf 변수를 선언한다. 흔들림 효과를 적용하려면 플레이어 컨트롤러의 참조가 필요하므로, GetWorld를 통해 얻은 월드 객체에서 GetFirstPlayerController 함수를 호출하여 참조를 가져온 뒤 ClientStartCameraShake 함수를 실행하여 연출을 완성한다.

 

profile
작심삼일을 무한으로 반복하는 지식세포 키우기
✏️ ⚙️