Unreal Engine/Functional Implementation

언리얼 엔진 파괴 가능한 오브젝트 구현 - 폭발, 파편, 범위 대미지 만들기

보별 2025. 10. 21. 20:29

FPS나 TPS 장르에서는 총격이나 폭발에 반응하는 파괴 가능한 오브젝트가 전투의 몰입감을 크게 높여준다.
단순히 메시가 사라지는 수준이 아니라, 피격에 따라 상태가 변하고, 파괴 순간에는 이펙트와 사운드, 범위 대미지, 파편 연출까지 함께 들어가야 실제 게임 플레이에서 존재감이 생긴다.

이번에는 그런 느낌을 살리기 위해, Player의 공격으로 파괴되는 자판기 오브젝트를 직접 구현한 과정을 정리해보려고 한다.
오브젝트 이름은 자판기라는 역할에 맞춰 VendingMachine으로 두었고, 아래 조건을 만족하도록 구성했다.

  • Player의 공격으로 파괴 가능
  • 일정 조건에 도달하면 불타는 상태 연출 출력
  • 파괴 시 폭발 이펙트와 사운드 출력
  • 일정 범위 내 액터에게 방사형 대미지 적용
  • Geometry Collection을 이용해 파편이 흩어지는 Fracture 연출 적용

이번 구현의 핵심은 파괴 전 상태와 파괴 후 상태를 분리해서 다룬 것이다.

 

1. 구현 목표와 전체 구조

처음 목표는 하나의 오브젝트가 평상시에는 멀쩡한 자판기로 보이다가, 피격을 거쳐 결국 폭발하면서 주변에 피해를 주고, 마지막에는 파편이 흩어지는 흐름을 만드는 것이었다.

이를 위해 오브젝트의 상태를 크게 두 단계로 나눴다.

  • 파괴 전: 일반 Static Mesh 상태
  • 파괴 후: Geometry Collection 상태

즉, 평상시에는 일반 메시를 보여주고, 폭발이 발생하면 기존 메시를 끄고 Geometry Collection을 활성화해서 실제 파편 연출을 출력하는 방식이다.

 

2. VendingMachine 클래스 구성

VendingMachine 액터는 아래 요소들을 기준으로 구성했다.

  • 원본 외형을 보여주는 StaticMeshComponent
  • 파괴 후 파편 표현용 GeometryCollectionComponent
  • 화염 이펙트 부착용 포인트
  • 폭발 이펙트와 폭발 사운드
  • 체력, 임계값, 폭발 대미지, 반경, 임펄스 같은 전투 관련 변수

즉, 하나의 액터 안에서 피격 처리, 시각 효과, 사운드 출력, 범위 대미지, 파편 물리 처리까지 함께 담당하도록 설계했다.

VendingMachine.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
#pragma once
 
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "GeometryCollection/GeometryCollectionComponent.h"
#include "VendingMachine.generated.h"
 
UCLASS()
class MyGame_API AVendingMachine : public AActor
{
    GENERATED_BODY()
    
public:    
    // Sets default values for this actor's properties
    AVendingMachine();
 
protected:
    // Called when the game starts or when spawned
    virtual void BeginPlay() override;
 
    /** 액터가 피해를 입었을 때 호출되는 함수 */
    UFUNCTION()
    void HandleAnyDamage(AActor* DamagedActor, float Damage, const class UDamageType* DamageType, class AController* InstigatedBy, AActor* DamageCauser);
 
    UFUNCTION()
    void Explode();
 
public:    
    // Called every frame
    virtual void Tick(float DeltaTime) override;
 
protected:
    UPROPERTY(VisibleAnywhere, Category = "Components")
    USceneComponent* Root;
 
protected:
    UPROPERTY(VisibleAnywhere, Category = "Components")
    USceneComponent* FXAttachPoint;
 
    /** 파괴되기 전 원본 스태틱 메시 컴포넌트 */
    UPROPERTY(VisibleAnywhere, Category = "Components")
    UStaticMeshComponent* VendingMachineMesh;
 
    /** 파괴될 때 사용할 지오메트리 컬렉션(GC) 컴포넌트 */
    UPROPERTY(VisibleAnywhere, Category = "Components")
    UGeometryCollectionComponent* VendingMachineGC;
 
    UPROPERTY()
    UParticleSystemComponent* FireFXComponent;
 
    UPROPERTY(EditDefaultsOnly, Category = "FX")
    UParticleSystem* ExplosionFX;
 
    UPROPERTY(EditDefaultsOnly, Category = "FX")
    UParticleSystem* FireFX;
 
    UPROPERTY(EditDefaultsOnly, Category = "FX|Sound")
    USoundBase* ExplosionSound;
 
    UPROPERTY(EditDefaultsOnly, Category = "VendingMachine|Damage")
    float MaxHealth = 100.f;
 
    /** 지정 체력 이하로 내려가면 화염 이펙트를 켬 */
    UPROPERTY(EditDefaultsOnly, Category = "VendingMachine|Damage")
    float CriticalHealthThreshold = 20.f;
 
    UPROPERTY(EditDefaultsOnly, Category = "VendingMachine|Damage")
    float ExplosionDamage = 50.f;
 
    UPROPERTY(EditDefaultsOnly, Category = "VendingMachine|Damage")
    float ExplosionRadius = 300.f;
 
    UPROPERTY(EditDefaultsOnly, Category = "VendingMachine|Damage")
    float ExplosionImpulse = 1000.f;
 
    UPROPERTY(EditDefaultsOnly, Category = "VendingMachine|Damage")
    TSubclassOf<UDamageType> DamageTypeClass;
 
    UPROPERTY()
    float CurrentHealth = 0.f;
 
    /** 자판기 폭발 여부를 추적하는 플래그 */
    bool bExploded = false;
 
public:
    /** 폭발 후 지오메트리 컬렉션의 콜리전 설정 변경 함수 */
    UFUNCTION()
    void SetPostExplosionCollision();
 
protected:
    UPROPERTY(EditDefaultsOnly, Category = "Collision")
    TEnumAsByte<ECollisionChannel> BulletChannel = ECC_GameTraceChannel10;
};
cs

 

3. 시작 상태에서는 Static Mesh만 사용

게임 시작 시에는 자판기가 멀쩡한 상태여야 하므로, BeginPlay()에서 Geometry Collection은 숨기고 콜리전과 물리도 꺼둔 뒤, 원본 Static Mesh만 보이도록 했다.

이렇게 하면 플레이어 입장에서는 일반적인 월드 오브젝트처럼 보이지만, 실제로는 내부적으로 파괴 이후를 위한 GC가 이미 준비되어 있는 구조가 된다.

VendingMachine.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#include "MapObject/VendingMachine.h"
#include "Kismet/GameplayStatics.h"
#include "Particles/ParticleSystemComponent.h"
#include "DrawDebugHelpers.h"
#include "Components/StaticMeshComponent.h"
#include "Engine/World.h"
#include "Engine/Engine.h"
#include "GeometryCollection/GeometryCollectionComponent.h"
 
AVendingMachine::AVendingMachine()
{
    PrimaryActorTick.bCanEverTick = true;
 
    Root = CreateDefaultSubobject<USceneComponent>(TEXT("Root"));
    SetRootComponent(Root);
 
    // 파괴 전 원본 스태틱 메시 컴포넌트 생성 및 루트 부착
    VendingMachineMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("VendingMachineMesh"));
    VendingMachineMesh->SetupAttachment(Root);
 
    // 파괴 후 활성화될 지오메트리 컬렉션(GC) 컴포넌트 생성 및 루트 부착
    VendingMachineGC = CreateDefaultSubobject<UGeometryCollectionComponent>(TEXT("VendingMachineGC"));
    VendingMachineGC->SetupAttachment(Root);
 
    FXAttachPoint = CreateDefaultSubobject<USceneComponent>(TEXT("FXAttachPoint"));
    FXAttachPoint->SetupAttachment(VendingMachineMesh);
}
 
void AVendingMachine::BeginPlay()
{
    Super::BeginPlay();
 
    CurrentHealth = MaxHealth;
    FireFXComponent = nullptr;
 
    // OnTakeAnyDamage 델리게이트에 데미지 처리 함수(HandleAnyDamage) 바인딩
    OnTakeAnyDamage.AddDynamic(this, &AVendingMachine::HandleAnyDamage);
 
    if (VendingMachineGC)
    {
        // 게임 시작 시 GC는 보이지 않게 함
        VendingMachineGC->SetVisibility(false);
        // GC 콜리전 비활성화
        VendingMachineGC->SetCollisionEnabled(ECollisionEnabled::NoCollision);
        // GC 물리 시뮬레이션 비활성화
        VendingMachineGC->SetSimulatePhysics(false);
    }
 
    if (VendingMachineMesh)
    {
        // 게임 시작 시 원본 메시 보이게 함
        VendingMachineMesh->SetVisibility(true);
        // 원본 메시의 콜리전 설정
        VendingMachineMesh->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
    }
}
 
void AVendingMachine::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);
 
}
cs

 

4. 피격 시 체력을 감소시키고 조건에 따라 상태 변경

이 오브젝트는 OnTakeAnyDamage 델리게이트를 통해 피격을 감지한다.
피해를 받으면 현재 체력을 감소시키고, 일정 조건을 만족하면 화염 이펙트를 켜거나 폭발을 실행한다.

현재 코드 흐름은 다음과 같다.

  • 이미 폭발했거나 유효하지 않은 대미지면 무시
  • CurrentHealth에서 받은 대미지만큼 차감
  • 한 번에 큰 피해를 받으면 FireFX 출력
  • 체력이 임계값 이하가 되면 Explode() 호출

여기서 주의할 점은, 현재 코드 기준으로 화염 이펙트는 체력 임계값이 아니라 Damage >= 50.f일 때 켜진다는 점이다.
만약 진짜로 “체력이 낮아졌을 때 불타는 상태”를 원한다면, 이 조건은 CurrentHealth <= 특정 값 형태로 바꾸는 것이 더 자연스럽다.

VendingMachine.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
void AVendingMachine::HandleAnyDamage(AActor* DamagedActor, float Damage, const UDamageType* DamageType, AController* InstigatedBy, AActor* DamageCauser)
{
    if (bExploded || Damage <= 0.f) return;
 
    CurrentHealth -= Damage;
 
    if (Damage >= 50.&& !FireFXComponent && FireFX && FXAttachPoint)
    {
        FireFXComponent = UGameplayStatics::SpawnEmitterAttached(
            FireFX,
            FXAttachPoint,
            NAME_None,
            FVector::ZeroVector,
            FRotator::ZeroRotator,
            EAttachLocation::KeepRelativeOffset,
            true
        );
    }
 
    // 현재 체력이 파괴 임계치(CriticalHealthThreshold) 이하로 떨어진 경우
    if (CurrentHealth <= CriticalHealthThreshold)
    {
        // 폭발 함수 호출
        Explode();
    }
}
cs

 

5. 폭발 시 이펙트, 사운드, 범위 대미지, 파편 연출 처리

폭발 단계에서는 단순히 메시를 지우는 것이 아니라, 전투 오브젝트답게 시청각 피드백과 실제 게임플레이 영향이 함께 들어가도록 만들었다.

Explode()에서는 다음 순서로 처리한다.

  1. 중복 폭발 방지
  2. 화염 이펙트 비활성화
  3. 폭발 이펙트 출력
  4. 폭발 사운드 출력
  5. 주변에 방사형 대미지 적용
  6. 파괴 후 상태로 전환
  7. 일정 시간이 지난 뒤 액터 제거

특히 ApplyRadialDamage()를 사용해 주변 일정 범위 안의 액터에게 대미지를 주도록 했고, 이후 SetPostExplosionCollision()을 호출해 실제 파편 상태로 전환했다.

VendingMachine.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
void AVendingMachine::Explode()
{
    if (bExploded)
        return;
 
    bExploded = true;
 
    if (FireFXComponent)
    {
        FireFXComponent->DeactivateSystem();
        FireFXComponent = nullptr;
    }
 
    const FVector FXLoc = FXAttachPoint ? FXAttachPoint->GetComponentLocation() : GetActorLocation();
    
    if (ExplosionFX)
    {
        UGameplayStatics::SpawnEmitterAtLocation(GetWorld(), ExplosionFX, FXLoc, FRotator::ZeroRotator);
    }
 
    if (ExplosionSound)
    {
        UGameplayStatics::PlaySoundAtLocation(this, ExplosionSound, FXLoc);
    }
 
    // 주변에 방사형 데미지를 적용
    UGameplayStatics::ApplyRadialDamage(
        GetWorld(), // 데미지를 적용할 월드
        ExplosionDamage, // 데미지 양
        GetActorLocation(), // 데미지 중심 위치
        ExplosionRadius, // 데미지 반경
        DamageTypeClass, // 데미지 타입
        TArray<AActor*>(), // 데미지를 무시할 액터 배열 (없음)
        this, // 데미지 유발 액터 (자판기 자신)
        GetInstigatorController(), // 데미지 유발 컨트롤러
        true // 장애물에 막히지 않고 전체 데미지를 줌 (FullDamage)
    );
 
    // 폭발 후 콜리전 및 메시 상태를 설정하는 함수를 호출
    SetPostExplosionCollision();
 
#if WITH_EDITOR
    DrawDebugSphere(GetWorld(), GetActorLocation(), ExplosionRadius, 32, FColor::Red, false3.0f);
#endif
 
    // 2.5초 후에 이 자판기 액터를 월드에서 제거
    SetLifeSpan(2.5f);
}
cs

 

6. 파괴 이후에는 Static Mesh를 끄고 Geometry Collection을 활성화

파괴 순간에는 기존 메시를 그대로 두면 오브젝트가 중복으로 보이거나 충돌이 꼬일 수 있다.
그래서 폭발 후에는 원본 Static Mesh를 비활성화하고, Geometry Collection 쪽을 활성화하는 식으로 전환했다.

이 단계에서 처리하는 내용은 아래와 같다.

  • 더 이상 대미지를 받지 않도록 설정
  • OnTakeAnyDamage 바인딩 해제
  • 원본 메시 숨김 및 콜리전 제거
  • Geometry Collection 표시
  • GC 콜리전과 물리 활성화
  • 방사형 임펄스를 가해 파편이 바깥으로 흩어지도록 처리

즉, 실제 폭발 연출의 핵심은 GC를 켜는 것보다도, 적절한 콜리전 설정과 물리 임펄스를 함께 적용하는 것에 있다.

VendingMachine.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
void AVendingMachine::SetPostExplosionCollision()
{
    // 더 이상 데미지를 받지 않도록 설정
    SetCanBeDamaged(false);
 
    // OnTakeAnyDamage 델리게이트 바인딩 해제
    OnTakeAnyDamage.RemoveDynamic(this, &AVendingMachine::HandleAnyDamage);
 
    if (VendingMachineMesh)
    {
        // 물리 법칙 비활성화
        VendingMachineMesh->SetSimulatePhysics(false);
        // 메시를 보이지 않게 함
        VendingMachineMesh->SetVisibility(false);
        // 콜리전 비활성화
        VendingMachineMesh->SetCollisionEnabled(ECollisionEnabled::NoCollision);
        // 오버랩 이벤트도 생성하지 않도록 함
        VendingMachineMesh->SetGenerateOverlapEvents(false);
    }
 
    if (!VendingMachineGC) return;
 
    // GC 보이게 설정
    VendingMachineGC->SetVisibility(true);
    // GC 콜리전 활성화
    VendingMachineGC->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
    // GC 오브젝트 타입을 'WorldDynamic' 설정
    VendingMachineGC->SetCollisionObjectType(ECC_WorldDynamic);
    // 모든 채널에 대해 'Block' 설정
    VendingMachineGC->SetCollisionResponseToAllChannels(ECR_Block);
    // 'BulletChannel' 채널에 대해서도 'Block' 설정
    VendingMachineGC->SetCollisionResponseToChannel(BulletChannel, ECR_Block);
    // 'Pawn' 채널에 대해서도 'Block' 설정
    VendingMachineGC->SetCollisionResponseToChannel(ECC_Pawn, ECR_Block);
    // 오버랩 이벤트 생성하지 않도록 함
    VendingMachineGC->SetGenerateOverlapEvents(false);
    // GC 물리 법칙 활성화
    VendingMachineGC->SetSimulatePhysics(true);
    // 물리 충돌 시 알림(Notify)을 받도록 설정
    VendingMachineGC->BodyInstance.bNotifyRigidBodyCollision = true;
 
    // GC에 방사형 충격(임펄스)을 가하여 파편이 폭발 지점으로부터 밖으로 흩어지게 함
    VendingMachineGC->AddRadialImpulse(
        GetActorLocation(), // 충격 중심 위치
        ExplosionRadius, // 충격 반경
        ExplosionImpulse, // 충격량
        ERadialImpulseFalloff::RIF_Linear, // 충격 감쇠 방식 (선형)
        true // 속도 변경으로 적용 (질량 무시)
    );
}
cs

 

7. Geometry Collection 하나만으로 구현하지 않은 이유

처음에는 Static Mesh 없이 Geometry Collection 하나만으로 자판기의 외형과 파괴 상태를 모두 처리하려고 했다.
즉, 평상시에도 같은 GC를 보여주고, 파괴 시 내부 Impulse만 넣어서 자연스럽게 파편이 흩어지도록 만들고 싶었다.

그런데 실제로 테스트해 보니 원하는 방식대로 동작하지 않았다.
오브젝트가 파괴될 때 바로 파편이 흩어지는 것이 아니라, 플레이어가 오브젝트에 직접 부딪혀야 파편이 반응하는 현상이 발생했다.

그래서 이번 구현에서는 불가피하게 구조를 나눴다.

  • 파괴 전 외형은 Static Mesh
  • 파괴 후 파편 연출은 Geometry Collection

이 방식은 컴포넌트가 하나 더 필요하다는 점에서는 조금 번거롭지만, 실제 플레이에서는 파괴 전 상태와 파괴 후 상태를 훨씬 안정적으로 제어할 수 있다는 장점이 있었다.

 

이번에 구현한 VendingMachine은 단순히 맞으면 사라지는 오브젝트가 아니라, 피격 → 상태 변화 → 폭발 → 범위 대미지 → 파편 연출까지 이어지는 파괴형 오브젝트를 목표로 만든 구조다.

정리하면 다음과 같다.

  • 평상시에는 Static Mesh로 일반 오브젝트처럼 표시
  • 피해를 받으면 체력을 감소시키고 조건에 따라 화염 이펙트 출력
  • 임계 상태가 되면 폭발 이펙트와 사운드 출력
  • 주변에 방사형 대미지 적용
  • Geometry Collection을 활성화해 파편 연출 처리

포트폴리오 관점에서도 이번 구현은 단순한 오브젝트 배치가 아니라, 대미지 처리, 이펙트 연동, 사운드 피드백, 범위 공격 처리, Geometry Collection 활용까지 함께 보여줄 수 있다는 점에서 의미가 있다고 생각한다.

다만 앞서 말했듯이, 현재 코드 기준으로 화염 이펙트는 체력 임계값이 아니라 큰 피해량을 받을 때 켜지도록 되어 있다.
이 부분만 의도에 맞게 정리하면, 글과 코드의 완성도가 더 좋아질 것이다.