파티클과 사운드로 게임 효과 연출하기 | [언리얼 엔진 C++ (Unreal Engine C++)]

2026. 6. 12. 17:03·Unreal Engine/UE 기초

 

파티클과 사운드로 게임 효과 연출하기

 


 

파티클 시각 효과 추가하기

 

1️⃣ 파티클 시스템 (Particla System) 기본 개념 이해하기

  • 파티클 시스템이란?
    • 파티클 시스템 (Particle System)은 게임 내에서 불꽃, 연기, 폭발, 먼지 등 다양한 시각적 효과를 구현하기 위한 도구입니다.
    • 파티클은 다수의 작은 ‘입자’ (Particle)들이 모여 움직이면서 특정한 모양, 색상, 혹은 애니메이션 효과를 만들어냅니다.
    • 언리얼 엔진에서는 파티클 시스템을 사용해 효과적인 VFX (Visual Effects) 를 구현할 수 있도록 풍부한 기능을 제공합니다.
  • Cascade vs. Niagara
    • Cascade: 언리얼 엔진 3 시절부터 제공된 오래된 파티클 편집 툴입니다. 언리얼 엔진 4, 5에서도 여전히 호환되지만, 신규 기능 업데이트는 주로 Niagara 위주로 이루어지고 있습니다.
      • 초급자가 배우기에 상대적으로 간단하고 빠르게 결과를 볼 수 있음
      • 레거시 프로젝트나 기존 아티스트 툴체인에서 많이 사용
      • 복잡하거나 고급스러운 VFX 연출에는 한계
      • 아직까지 그래도 사용되는 부분이 꽤 있기 때문에 어느 정도 알고는 있어야 함.
    • Niagara: 언리얼 엔진 4 후반부터 새롭게 도입된 차세대 파티클 시스템입니다. 언리얼 엔진 5에서는 Niagara가 공식적으로 권장되는 방식입니다.
      • 모듈 단위로 다양한 파티클 동작을 정교하게 제어 가능
      • 블루프린트나 머티리얼, 스크립팅과 유기적으로 연동되어 고급 VFX를 쉽게 만들 수 있음
      • GPU 파티클, 신규 기능 업데이트가 빠르게 적용
    • 만약 언리얼 엔진을 처음 접하거나, 파티클 효과를 빠르게 만들어봐야 한다면 Cascade부터 익히는 것도 좋은 방법입니다. 하지만, 새 프로젝트를 계획 중이고 고급 이펙트를 구현하고 싶다면 Niagara 학습을 권장합니다.
  • 이름 앞에 P_ 또는 Niagara_ 와 같은 접두어를 붙여두면, 프로젝트 내에서 파티클 에셋을 쉽게 구분할 수 있습니다.

 

나이아가라 UE 공식 문서: https://dev.epicgames.com/documentation/unreal-engine/getting-started-in-niagara-effects-for-unreal-engine?lang=ko

 

Getting Started in Niagara Effects for Unreal Engine | Unreal Engine 5.7 Documentation | Epic Developer Community

This page collects all the getting started learning materials for Niagara.

dev.epicgames.com

 

캐스케이드 UE 공식 문서: https://dev.epicgames.com/documentation/unreal-engine/cascade-particle-editor-reference?application_version=4.27

 

Designing Visuals, Rendering, and Graphics with Unreal Engine | Unreal Engine 5.7 Documentation | Epic Developer Community

Rendering subsystem including lighting and shadowing, materials and textures, visual effects, and post processing in Unreal Engine.

dev.epicgames.com

 

 


 

2️⃣ 아이템 획득에 파티클 적용하기

  • 모든 아이템은 상호작용 시 ActivateItem 함수를 실행하도로 설계를 했습니다. 여기에 파티클을 붙인다면 BaseItem을 상속받는 아이템들 모두에서 파티클을 설정할 수 있습니다.
  • 또한, 파티클 이펙트가 발생한 후 2초 뒤 이펙트가 사라지도록 설정을 해야 합니다. 이 부분까지도 고려해서 꼼꼼하게 코드를 수정해 보도록 합시다.
  • 빌드를 하고 에디터로 돌아가서, 파티클을 붙여줄 아이템 Blueprint를 (예를 들어, 지뢰 아이템) 열어봅니다. Details 창을 보면 Item 카테고리 아래에 Effects라는 카테고리가 생겼고, 여기서 파티클 이펙트를 설정할 수 있습니다. 리소스에 있는 파티클 중 맘에 드는 것으로 이펙트를 할당하고 컴파일 후 저장해 줍니다.
    • 각 아이템에 파티클 & 사운드 추가해 줍시다.

 

 

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "ItemInterface.h"  // 만들어둔 인터페이스 헤더 포함

#include "BaseItem.generated.h"

class USphereComponent;

UCLASS()
class BC_CH3_ASSIGNMENT_5_API ABaseItem : public AActor, public IItemInterface
{
	GENERATED_BODY()
	
public:	
	// Sets default values for this actor's properties
	ABaseItem();

protected:
	// 아이템 유형(타입)을 편집 가능하게 지정
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item")
	FName ItemType;
	
	// 루트 컴포넌트 (씬)
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item|Component")
	USceneComponent* Scene;
	// 충돌 컴포넌트 (플레이어 진입 범위 감지용)
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item|Component")
	USphereComponent* Collision;
	// 아이템 시각 표현용 스태틱 메시
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item|Component")
	UStaticMeshComponent* StaticMesh;
	
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item|Effects")
	UParticleSystem* PickupParticle;
	
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item|Effects")
	USoundBase* PickupSound;
	
	virtual void OnItemOverlap(
			UPrimitiveComponent* OverlappedComp,
			AActor* OtherActor,
			UPrimitiveComponent* OtherComp,
			int32 OtherBodyIndex,
			bool bFromSweep,
			const FHitResult& SweepResult) override;
	
	virtual void OnItemEndOverlap(
			UPrimitiveComponent* OverlappedComp,
			AActor* OtherActor,
			UPrimitiveComponent* OtherComp,
			int32 OtherBodyIndex) override;
	
	virtual void ActivateItem(AActor* Activator) override;
	virtual FName GetItemType() const override;
	
	// 아이템을 제거하는 공통 함수 (추가 이펙트나 로직을 넣을 수 있음)
	virtual void DestroyItem();
	
private:
	const FName CollisionProfile_OverlapAllDynamic = TEXT("OverlapAllDynamic");

};
#include "BaseItem.h"
#include "Components/SphereComponent.h"
#include "Kismet/GameplayStatics.h"
#include "Particles/ParticleSystemComponent.h"


ABaseItem::ABaseItem()
{
	PrimaryActorTick.bCanEverTick = false;

	// 루트 컴포넌트 생성 및 설정
	Scene = CreateDefaultSubobject<USceneComponent>(TEXT("Scene"));
	SetRootComponent(Scene);

	// 충돌 컴포넌트 생성 및 설정
	Collision = CreateDefaultSubobject<USphereComponent>(TEXT("Collision"));
	// 겹침만 감지하는 프로파일 설정
	//Collision->SetCollisionProfileName(TEXT("OverlapAllDynamic"));
	Collision->SetCollisionProfileName(CollisionProfile_OverlapAllDynamic);
	// 루트 컴포넌트로 설정
	Collision->SetupAttachment(Scene);

	// 스태틱 메시 컴포넌트 생성 및 설정
	StaticMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("StaticMesh"));
	StaticMesh->SetupAttachment(Collision);
	// 메시가 불필요하게 충돌을 막지 않도록 하기 위해, 별도로 NoCollision 등으로 설정할 수 있음.

	// Overlap 이벤트 바인딩
	Collision->OnComponentBeginOverlap.AddDynamic(this, &ABaseItem::OnItemOverlap);
	Collision->OnComponentEndOverlap.AddDynamic(this, &ABaseItem::OnItemEndOverlap);
}

void ABaseItem::OnItemOverlap(
	UPrimitiveComponent* OverlappedComp,
	AActor* OtherActor,
	UPrimitiveComponent* OtherComp,
	int32 OtherBodyIndex,
	bool bFromSweep,
	const FHitResult& SweepResult)
{
	/*
	추가 팁: 인터페이스(Interface) 활용하기만약 플레이어뿐만 아니라 '데미지를 입을 수 있는 모든 대상'을 판별하고 싶다면, 
	Cast 대신 언리얼 인터페이스(UInterface)를 사용해 OtherActor->Implements<UMyDamageInterface>() 형태로 판별하는 것이 아키텍처 관점에서 훨씬 깔끔합니다.
	
	// 싱글 플레이어 게임이거나 0번 로컬 플레이어를 판별할 때 가장 간단하고 직관적인 방법입니다. 충돌한 액터가 현재 월드의 플레이어 폰과 같은지 메모리 주소를 직접 비교합니다.
	// 단점: 멀티플레이어 환경(다른 클라이언트의 플레이어 캐릭터)에서는 작동하지 않을 수 있습니다.
	if (OtherActor && OtherActor == GetWorld()->GetFirstPlayerController()->GetPawn())
	{		
		GEngine->AddOnScreenDebugMessage(-1, 2.0f, FColor::Blue, FString::Printf(TEXT("플레이어가 충돌했습니다!!!")));
	}  
	
	// 멀티플레이어 환경에서 적 플레이어, 아군 플레이어 구분 없이 '유저가 잡고 있는 캐릭터'를 모두 판별해 냅니다. 
	// AI 캐릭터(IsBotControlled)는 자동으로 걸러집니다.
	// #include "GameFramework/Pawn.h" // 다른 곳에서 선언된거랑 가능한한 여기서도 선언해두자
	if (OtherActor)
	{
		// 우선 APawn으로 캐스팅 (모든 캐릭터는 폰의 일종입니다)
		APawn* TouchedPawn = Cast<APawn>(OtherActor);
		
		// 폰이 맞고, 현재 플레이어 컨트롤러가 빙의(Possess)한 상태인지 확인
		if (TouchedPawn && TouchedPawn->IsPlayerControlled())
		{
			GEngine->AddOnScreenDebugMessage(-1, 2.0f, FColor::Red, FString::Printf(TEXT("플레이어가 조종 중인 액터입니다!!!")));
		}
	}	
	
	// 플레이어와 AI를 동시에 분기 처리하기
	// 하나의 충돌 함수 안에서 플레이어와 AI를 나누어 각각 다른 로직
	// (예: 플레이어면 UI 연출, AI면 데미지만 처리)을 실행하고 싶다면 다음과 같이 작성합니다.
	
	if (OtherActor)
	{
		APawn* TouchedPawn = Cast<APawn>(OtherActor);
			if (TouchedPawn)
			{
				if (TouchedPawn->IsPlayerControlled())
				{
					// 실제 플레이어 전용 로직
					UE_LOG(LogTemp, Log, TEXT("플레이어 충돌"));
				}
				else if (TouchedPawn->IsBotControlled())
				{
					//  AI 봇 전용 로직
					UE_LOG(LogTemp, Log, TEXT("AI 캐릭터 충돌"));
				}
			}
	}

	 */


	// OtherActor가 플레이어인지 확인 ("Player" 태그 활용)
	if (OtherActor && OtherActor->ActorHasTag("Player"))
	{
		GEngine->AddOnScreenDebugMessage(-1, 2.0f, FColor::Green, FString::Printf(TEXT("Overlap!!!")));
		// 아이템 사용 (획득) 로직 호출
		ActivateItem(OtherActor);
	}
}

void ABaseItem::OnItemEndOverlap(
	UPrimitiveComponent* OverlappedComp,
	AActor* OtherActor,
	UPrimitiveComponent* OtherComp,
	int32 OtherBodyIndex)
{
}

void ABaseItem::ActivateItem(AActor* Activator)
{	
	
	//GEngine->AddOnScreenDebugMessage(-1, 2.0f, FColor::Green, FString::Printf(TEXT("Overlap!!")));

	UParticleSystemComponent* Particle = nullptr;
	
	// PickupParticle == BP 에서 지정해둔 에셋
	if (PickupParticle)
	{
		Particle = UGameplayStatics::SpawnEmitterAtLocation(
			GetWorld(),
			PickupParticle,
			GetActorLocation(),
			GetActorRotation(),
			true
		);
	}

	if (PickupSound)
	{
		UGameplayStatics::PlaySoundAtLocation(
			GetWorld(),
			PickupSound,
			GetActorLocation()
		);
		
		/*FTimerHandle DestroyParticleTimerHandle;
		TWeakObjectPtr<UParticleSystemComponent> WeakParticle = Particle;

		GetWorld()->GetTimerManager().SetTimer(
			DestroyParticleTimerHandle,
			[WeakParticle]()
			{
			     if (WeakParticle.IsValid())
				{
					WeakParticle->DestroyComponent();
							
				}
			},
			2.0f,
			false
		);*/
		
		
	}
}

FName ABaseItem::GetItemType() const
{
	return ItemType;
}

void ABaseItem::DestroyItem()
{
	Destroy();
}

 

  • 다른 아이템 클래스들의 ActivateItem 함수에 BaseItem 을 상속받는 코드도 추가해줘야 합니다.
    • Super::ActivateItem(Activator);

예:

void AMineItem::ActivateItem(AActor* Activator)
{
	Super::ActivateItem(Activator);
	
	// ExplosionDelay 후 폭발 실행
	GetWorld()->GetTimerManager().SetTimer(ExplosionTimerHandle, this, &AMineItem::Explode, ExplosionDelay);
}

 


현재 Particle 들이 생성 후 안 사라지기 때문에 따로 관리를 해줘야 합니다.

UParticleSystemComponent* Particle = nullptr;
	
	if (ExplosionParticle)
	{
		Particle = UGameplayStatics::SpawnEmitterAtLocation(
			GetWorld(),
			ExplosionParticle,
			GetActorLocation(),
			GetActorRotation(),
			//true // 여기서 true 면 auto destroy 되어야하는데 안된다면, particle이 loop 형신인거라 다른 추가 조치를 해주어야 합니다.
			false // 그래서 loop 나 세밀한 조정을 원한경우 따로 destroy 구현합니다
		);
	}
	
	if (ExplosionSound)
	{		
		UGameplayStatics::PlaySoundAtLocation(
			GetWorld(),
			ExplosionSound,
			GetActorLocation(),
			GetActorRotation(),
			true 
		);
		
	}

 

하지만 여기서 만들어진 sound는 particle 과는 다르게 따로 destroy 안 해줍니다.

UGameplayStatics::PlaySoundAtLocation()로 재생한 사운드는 보통 따로 Destroy 해주지 않아도 되는 이유는 PlaySoundAtLocation()은 Fire and Forget 방식입니다.

즉,

“이 위치에서 이 소리 한 번 재생해 줘”
라고 엔진에 요청하고, 이후 관리는 엔진이 처리해요.

그래서 아이템 획득음, 폭발음처럼 짧게 한 번 재생되는 효과음은 따로 Destroy가 필요 없습니다.

 

그럼 언제 사운드를 직접 관리하나요?

다음 같은 경우에는 PlaySoundAtLocation()보다 AudioComponent를 사용하는 경우가 많아요.

  • 배경음악처럼 오래 재생되는 사운드
  • 중간에 Stop 해야 하는 사운드
  • 볼륨을 실시간으로 조절해야 하는 사운드
  • 루프 사운드
  • 특정 Actor에 붙어서 따라다녀야 하는 사운드

이런 경우에는 SpawnSoundAtLocation() 또는 SpawnSoundAttached()로 컴포넌트를 받아서 관리할 수 있어요.

정리

현재 코드처럼 아이템 획득 사운드라면:

cpp
UGameplayStatics::PlaySoundAtLocation(
    GetWorld(),
    PickupSound,
    GetActorLocation()
);

이렇게만 해도 충분합니다.

반대로 파티클은 특히 Looping 파티클이면 화면에 계속 남을 수 있어서, 필요하면 타이머로 DestroyComponent()를 해주는 어야 합니다.

짧은 효과음은 자동 정리
루프 파티클이나 오래 남는 컴포넌트는 직접 정리 고려

 


 

Item 관련 C++ 클래스 코드들

 

BaseItem.h

더보기
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "ItemInterface.h"  // 만들어둔 인터페이스 헤더 포함

#include "BaseItem.generated.h"

class USphereComponent;

UCLASS()
class BC_CH3_ASSIGNMENT_5_API ABaseItem : public AActor, public IItemInterface
{
	GENERATED_BODY()
	
public:	
	// Sets default values for this actor's properties
	ABaseItem();

protected:
	// 아이템 유형(타입)을 편집 가능하게 지정
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item")
	FName ItemType;
	
	// 루트 컴포넌트 (씬)
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item|Component")
	USceneComponent* Scene;
	// 충돌 컴포넌트 (플레이어 진입 범위 감지용)
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item|Component")
	USphereComponent* Collision;
	// 아이템 시각 표현용 스태틱 메시
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item|Component")
	UStaticMeshComponent* StaticMesh;
	
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item|Effects")
	UParticleSystem* PickupParticle;
	
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item|Effects")
	USoundBase* PickupSound;
	
	virtual void OnItemOverlap(
			UPrimitiveComponent* OverlappedComp,
			AActor* OtherActor,
			UPrimitiveComponent* OtherComp,
			int32 OtherBodyIndex,
			bool bFromSweep,
			const FHitResult& SweepResult) override;
	
	virtual void OnItemEndOverlap(
			UPrimitiveComponent* OverlappedComp,
			AActor* OtherActor,
			UPrimitiveComponent* OtherComp,
			int32 OtherBodyIndex) override;
	
	virtual void ActivateItem(AActor* Activator) override;
	virtual FName GetItemType() const override;
	
	// 아이템을 제거하는 공통 함수 (추가 이펙트나 로직을 넣을 수 있음)
	virtual void DestroyItem();
	
private:
	const FName CollisionProfile_OverlapAllDynamic = TEXT("OverlapAllDynamic");

};

 

BaseItem.Cpp

더보기
#include "BaseItem.h"
#include "Components/SphereComponent.h"
#include "Kismet/GameplayStatics.h"
#include "Particles/ParticleSystemComponent.h"


ABaseItem::ABaseItem()
{
	PrimaryActorTick.bCanEverTick = false;

	// 루트 컴포넌트 생성 및 설정
	Scene = CreateDefaultSubobject<USceneComponent>(TEXT("Scene"));
	SetRootComponent(Scene);

	// 충돌 컴포넌트 생성 및 설정
	Collision = CreateDefaultSubobject<USphereComponent>(TEXT("Collision"));
	// 겹침만 감지하는 프로파일 설정
	//Collision->SetCollisionProfileName(TEXT("OverlapAllDynamic"));
	Collision->SetCollisionProfileName(CollisionProfile_OverlapAllDynamic);
	// 루트 컴포넌트로 설정
	Collision->SetupAttachment(Scene);

	// 스태틱 메시 컴포넌트 생성 및 설정
	StaticMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("StaticMesh"));
	StaticMesh->SetupAttachment(Collision);
	// 메시가 불필요하게 충돌을 막지 않도록 하기 위해, 별도로 NoCollision 등으로 설정할 수 있음.

	// Overlap 이벤트 바인딩
	Collision->OnComponentBeginOverlap.AddDynamic(this, &ABaseItem::OnItemOverlap);
	Collision->OnComponentEndOverlap.AddDynamic(this, &ABaseItem::OnItemEndOverlap);
}

void ABaseItem::OnItemOverlap(
	UPrimitiveComponent* OverlappedComp,
	AActor* OtherActor,
	UPrimitiveComponent* OtherComp,
	int32 OtherBodyIndex,
	bool bFromSweep,
	const FHitResult& SweepResult)
{
	/*
	추가 팁: 인터페이스(Interface) 활용하기만약 플레이어뿐만 아니라 '데미지를 입을 수 있는 모든 대상'을 판별하고 싶다면, 
	Cast 대신 언리얼 인터페이스(UInterface)를 사용해 OtherActor->Implements<UMyDamageInterface>() 형태로 판별하는 것이 아키텍처 관점에서 훨씬 깔끔합니다.
	
	// 싱글 플레이어 게임이거나 0번 로컬 플레이어를 판별할 때 가장 간단하고 직관적인 방법입니다. 충돌한 액터가 현재 월드의 플레이어 폰과 같은지 메모리 주소를 직접 비교합니다.
	// 단점: 멀티플레이어 환경(다른 클라이언트의 플레이어 캐릭터)에서는 작동하지 않을 수 있습니다.
	if (OtherActor && OtherActor == GetWorld()->GetFirstPlayerController()->GetPawn())
	{		
		GEngine->AddOnScreenDebugMessage(-1, 2.0f, FColor::Blue, FString::Printf(TEXT("플레이어가 충돌했습니다!!!")));
	}  
	
	// 멀티플레이어 환경에서 적 플레이어, 아군 플레이어 구분 없이 '유저가 잡고 있는 캐릭터'를 모두 판별해 냅니다. 
	// AI 캐릭터(IsBotControlled)는 자동으로 걸러집니다.
	// #include "GameFramework/Pawn.h" // 다른 곳에서 선언된거랑 가능한한 여기서도 선언해두자
	if (OtherActor)
	{
		// 우선 APawn으로 캐스팅 (모든 캐릭터는 폰의 일종입니다)
		APawn* TouchedPawn = Cast<APawn>(OtherActor);
		
		// 폰이 맞고, 현재 플레이어 컨트롤러가 빙의(Possess)한 상태인지 확인
		if (TouchedPawn && TouchedPawn->IsPlayerControlled())
		{
			GEngine->AddOnScreenDebugMessage(-1, 2.0f, FColor::Red, FString::Printf(TEXT("플레이어가 조종 중인 액터입니다!!!")));
		}
	}	
	
	// 플레이어와 AI를 동시에 분기 처리하기
	// 하나의 충돌 함수 안에서 플레이어와 AI를 나누어 각각 다른 로직
	// (예: 플레이어면 UI 연출, AI면 데미지만 처리)을 실행하고 싶다면 다음과 같이 작성합니다.
	
	if (OtherActor)
	{
		APawn* TouchedPawn = Cast<APawn>(OtherActor);
			if (TouchedPawn)
			{
				if (TouchedPawn->IsPlayerControlled())
				{
					// 실제 플레이어 전용 로직
					UE_LOG(LogTemp, Log, TEXT("플레이어 충돌"));
				}
				else if (TouchedPawn->IsBotControlled())
				{
					//  AI 봇 전용 로직
					UE_LOG(LogTemp, Log, TEXT("AI 캐릭터 충돌"));
				}
			}
	}

	 */


	// OtherActor가 플레이어인지 확인 ("Player" 태그 활용)
	if (OtherActor && OtherActor->ActorHasTag("Player"))
	{
		GEngine->AddOnScreenDebugMessage(-1, 2.0f, FColor::Green, FString::Printf(TEXT("Overlap!!!")));
		// 아이템 사용 (획득) 로직 호출
		ActivateItem(OtherActor);
	}
}

void ABaseItem::OnItemEndOverlap(
	UPrimitiveComponent* OverlappedComp,
	AActor* OtherActor,
	UPrimitiveComponent* OtherComp,
	int32 OtherBodyIndex)
{
}

void ABaseItem::ActivateItem(AActor* Activator)
{	
	
	//GEngine->AddOnScreenDebugMessage(-1, 2.0f, FColor::Green, FString::Printf(TEXT("Overlap!!")));

	
	UParticleSystemComponent* Particle = nullptr;
	
	// PickupParticle == BP 에서 지정해둔 에셋
	if (PickupParticle)
	{
		Particle = UGameplayStatics::SpawnEmitterAtLocation(
			GetWorld(),
			PickupParticle,
			GetActorLocation(),
			GetActorRotation(),
			true
		);
	}

	if (PickupSound)
	{
		UGameplayStatics::PlaySoundAtLocation(
			GetWorld(),
			PickupSound,
			GetActorLocation()
		);

	}
	
	if (Particle)
	{
		if (GEngine)
		GEngine->AddOnScreenDebugMessage(-1, 2.0f, FColor::Red, FString::Printf(TEXT("ActivateItem from Base Item")));
		
		FTimerHandle DestroyParticleTimerHandle;
		TWeakObjectPtr<UParticleSystemComponent> WeakParticle = Particle;

		GetWorld()->GetTimerManager().SetTimer(
			DestroyParticleTimerHandle,
			[WeakParticle]()
			{
				 if (WeakParticle.IsValid())
				 {
					 WeakParticle->DestroyComponent();
							
				 }
			 },
			 2.0f,
			 false
		 );
	}
}

FName ABaseItem::GetItemType() const
{
	return ItemType;
}

void ABaseItem::DestroyItem()
{
	Destroy();
	
	/*if (PickupParticle)
	{
		FTimerHandle DestroyParticleTimerHandle;
		
		GetWorld()->GetTimerManager().SetTimer(
			DestroyParticleTimerHandle,
			// lamda: 익명 함수 (이름이 없는 함수)
			// [ ] 일종의 캡쳐리스트 람다 싱행시 [ ] 안에 있는 (현재는 Particle) 변수를 바깥 스코프에서 값을 가져다가 
			// 사용할 수 있게 만드는 것.
			[Particle]()
			{
				Particle->DestroyComponent();
			},
			1.0f, // destroy time in sec
			false
		);
	}*/
}

 


MineItem.h

더보기
#pragma once

#include "CoreMinimal.h"
#include "BaseItem.h"
#include "MineItem.generated.h"


UCLASS()
class BC_CH3_ASSIGNMENT_5_API AMineItem : public ABaseItem
{
	GENERATED_BODY()
	
public:
	AMineItem();
	
protected:
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Item|Component")
	USphereComponent* ExplosionCollision;
	
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item|Effects")
	UParticleSystem* ExplosionParticle;
	
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item|Effects")
	USoundBase* ExplosionSound;
	
	// 폭발까지 걸리는 시간
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Mine")
	float ExplosionDelay;
	
	// 폭발 범위
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Mine")
	float ExplosionRadius;

	// 폭발 데미지
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Mine")
	int ExplosionDamage;
	
	bool bHasExploded;
	// 지뢰 발동 여부
	FTimerHandle ExplosionTimerHandle;

	virtual void ActivateItem(AActor* Activator) override;
	
	void Explode();
	

private:
	
	const FName CollisionProfile_OverlapAllDynamic = TEXT("OverlapAllDynamic");
};

 

MineItem.cpp

더보기
#include "MineItem.h"
#include "Components/SphereComponent.h"
#include "Kismet/GameplayStatics.h"
#include "Particles/ParticleSystemComponent.h"

AMineItem::AMineItem()
{
	ExplosionDelay = 5.0f;
	ExplosionRadius = 300.0f;
	ExplosionDamage = 30.0f;
	ItemType = "Mine";
	bHasExploded = false;
    
	ExplosionCollision = CreateDefaultSubobject<USphereComponent>(TEXT("ExplosionCollision"));
	ExplosionCollision->InitSphereRadius(ExplosionRadius);
	//ExplosionCollision->SetCollisionProfileName(TEXT("OverlapAllDynamic")); 
	ExplosionCollision->SetCollisionProfileName(CollisionProfile_OverlapAllDynamic); 
	ExplosionCollision->SetupAttachment(Scene);
}

void AMineItem::ActivateItem(AActor* Activator)
{
	if (bHasExploded) return;
	
	
	Super::ActivateItem(Activator);
	
	// ExplosionDelay 후 폭발 실행
	GetWorld()->GetTimerManager().SetTimer(ExplosionTimerHandle, this, &AMineItem::Explode, ExplosionDelay);
	
	bHasExploded = true;
}

void AMineItem::Explode()
{
	UParticleSystemComponent* Particle = nullptr;
	
	if (GEngine)
		GEngine->AddOnScreenDebugMessage(-1, 2.0f, FColor::Red, FString::Printf(TEXT("ActivateItem Explode")));
	
	if (ExplosionParticle)
	{
		Particle = UGameplayStatics::SpawnEmitterAtLocation(
			GetWorld(),
			ExplosionParticle,
			GetActorLocation(),
			GetActorRotation(),
			//true // 여기서 true 면 auto destroy 되어야하는데 안된다면, particle이 loop 형신인거라 다른 추가 조치를 해주어야 합니다.
			false // 그래서 loop 나 세밀한 조정을 원한경우 따로 destroy 구현합니다
		);
	}
	
	if (ExplosionSound)
	{		
		UGameplayStatics::PlaySoundAtLocation(
			GetWorld(),
			ExplosionSound,
			GetActorLocation(),
			GetActorRotation(),
			true 
		);
		
	}
	
	TArray<AActor*> OverlappingActors;
	ExplosionCollision->GetOverlappingActors(OverlappingActors);

	for (AActor* Actor : OverlappingActors)
	{
		if (Actor && Actor->ActorHasTag("Player"))
		{
			GEngine->AddOnScreenDebugMessage(-1, 2.0f, FColor::Red, FString::Printf(TEXT("Player damaged %d by MineItem"), ExplosionDamage));
			
			// 데미지를 발생시켜 Actor->TakeDamage()가 실행되도록 함
			UGameplayStatics::ApplyDamage(
				Actor,                      // 데미지를 받을 액터
				ExplosionDamage,            // 데미지 양
				nullptr,                    // 데미지를 유발한 주체 (지뢰를 설치한 캐릭터가 없으므로 nullptr)
				this,                       // 데미지를 유발한 오브젝트(지뢰)
				UDamageType::StaticClass()  // 기본 데미지 유형
			);
			
			
		}
	}

	// 지뢰 제거
	DestroyItem();
	
	if (Particle)
	{
		if (GEngine)
		{
			GEngine->AddOnScreenDebugMessage(-1, 2.0f, FColor::Red, FString::Printf(TEXT("Particle destroyed wid delay %f"), ExplosionDelay));
		}
		
		FTimerHandle DestroyParticleTimerHandle;
		
		GetWorld()->GetTimerManager().SetTimer(
			DestroyParticleTimerHandle,
			// lamda: 익명 함수 (이름이 없는 함수)
			// [ ] 일종의 캡쳐리스트 람다 싱행시 [ ] 안에 있는 (현재는 Particle) 변수를 바깥 스코프에서 값을 가져다가 
			// 사용할 수 있게 만드는 것.
			[Particle]()
			{
				Particle->DestroyComponent();
			},
			
			
			ExplosionDelay,
			//1.0f, // destroy time in sec
			false
		);
	}
}

 


Debugging

 

문제: 게임 완성 후 테스팅 플레이 해봤는데 갑자기 게임이 크래쉬 해버렸다.

제일 큰 문제가 바로 플레이 중 갑자기 크래쉬 해버린다는 거다.. 그래서 무엇 때문인지 감을 못 잡았다.

해결 방법: 많은 Null Check

예상하기는 파티클 생성이랑 그거 해제 시 메모리 접근이 잘못돼서 그런 거 같다. 그래서 그런 쪽 코드에 다 Safety? (null check)을 걸었더니 해결되었다.

 

느낀 점: 조심해서 코드를 짜자!

확실히 언리얼 엔진이 무겁고 더 많이 신경 써서 다뤄야겠다고 배웠다.

유니티 엔진이서 이런 식으로 크래쉬 난적은 없었던 거 같은데 이게 내가 아직 C++ 이랑 언리얼이 익숙하지 못해서 그런 건가...

근데 이전에 언리얼 블루프린트 만들 때는 로직 고민 거의 없이 구현해보고 싶은 것들 생각대로 구현되고, 크래쉬도 안 나서 조금 들뜨기도 하고 쉽게 생각했는데... 정신 차리자...

그런데 걱정은 이렇게 작은 프로젝트에서 크래쉬 나는데... 조금 큰 거는 크래쉬 나면 디버깅 어떻게 하지....


 

 

 

완성된 모습

 

Youtube:https://youtu.be/DBJt9eXNTGU

 

현재 게임로직

  • 총 3개의 스테이지(Level) 존재
  • 아이템 랜덤 생성: Health Potion, Big & Small Coin, Mine
  • 각 스테이지에 생성된 모든 Coin 획득 시 다음  스테이지로 이동
  • Level 올라갈 시 체력 Restore
  • 시간 초과 혹은 체력이 0 이 되면 Game Over
  • 3개의 스테이즈 다 클리어시 Game Complete

 

Github Link: https://github.com/devcol-main/BC_Ch3_Assignment_5/blob/main/README.md

 

BC_Ch3_Assignment_5/README.md at main · devcol-main/BC_Ch3_Assignment_5

Contribute to devcol-main/BC_Ch3_Assignment_5 development by creating an account on GitHub.

github.com

 


 

추천

 

나이아가라 UE 공식 문서: https://dev.epicgames.com/documentation/unreal-engine/getting-started-in-niagara-effects-for-unreal-engine?lang=ko

 

Getting Started in Niagara Effects for Unreal Engine | Unreal Engine 5.7 Documentation | Epic Developer Community

This page collects all the getting started learning materials for Niagara.

dev.epicgames.com

 

캐스케이드 UE 공식 문서: https://dev.epicgames.com/documentation/unreal-engine/cascade-particle-editor-reference?application_version=4.27

 

Designing Visuals, Rendering, and Graphics with Unreal Engine | Unreal Engine 5.7 Documentation | Epic Developer Community

Rendering subsystem including lighting and shadowing, materials and textures, visual effects, and post processing in Unreal Engine.

dev.epicgames.com

 


[Unreal Engine/UE 기초] - 인터페이스 기반 아이템 클래스 설계하기 | [언리얼 엔진 C++ (Unreal Engine C++)]

 

인터페이스 기반 아이템 클래스 설계하기 | [언리얼 엔진 C++ (Unreal Engine C++)]

인터페이스 기반 아이템 클래스 설계하기 인터페이스 이해하기1️⃣ 인터페이스란?인터페이스 (Interface)란 클래스 (또는 오브젝트)가 반드시 구현해야 할 함수 목록만을 미리 정의해 두고, 실제

devcol.tistory.com

 

[Unreal Engine/UE 기초] - 충돌 이벤트로 획득되는 아이템 구현하기 | [언리얼 엔진 C++ (Unreal Engine C++)]

 

충돌 이벤트로 획득되는 아이템 구현하기 | [언리얼 엔진 C++ (Unreal Engine C++)]

충돌 이벤트로 획득되는 아이템 구현하기 이전 포스팅에서는 클래스 구조만 짜고 로직은 비여있던 상태.이전 포스팅: [Unreal Engine/UE 기초] - 인터페이스 기반 아이템 클래스 설계하기 | [언리얼

devcol.tistory.com

 

[Unreal Engine/UE 기초] - 아이템 스폰 및 레벨 데이터 관리하기 | [언리얼 엔진 C++ (Unreal Engine C++)]

 

아이템 스폰 및 레벨 데이터 관리하기 | [언리얼 엔진 C++ (Unreal Engine C++)]

아이템 스폰 및 레벨 데이터 관리하기 랜덤 위치에 아이템 스폰하기 1️⃣ 레벨 셋팅하기Resources 폴더에 Maps 폴더에는 3개의 레벨이 이미 존재합니다. 각각 난이도에 따라서 크기가 다른 맵이고,

devcol.tistory.com

 

[Unreal Engine/UE 기초] - 캐릭터 체력 및 점수 관리 시스템 구현하기 | [언리얼 엔진 C++ (Unreal Engine C++)]

 

캐릭터 체력 및 점수 관리 시스템 구현하기 | [언리얼 엔진 C++ (Unreal Engine C++)]

캐릭터 체력 및 점수 관리 시스템 구현하기 캐릭터 체력 시스템 구현하기 1️⃣ 캐릭터 클래스에 체력 변수 및 함수 선언PlayerState를 쓰지 않는 이유PlayerState: 각 플레이어마다의 어떤 정보를 관

devcol.tistory.com

 

[Unreal Engine/UE 기초] - 게임 루프 설계를 통한 게임 흐름 제어하기 | [언리얼 엔진 C++ (Unreal Engine C++)]

 

게임 루프 설계를 통한 게임 흐름 제어하기 | [언리얼 엔진 C++ (Unreal Engine C++)]

게임 루프 설계를 통한 게임 흐름 제어하기 GameState를 이용한 게임 루프 구현하기 게임루프: 보통 게임의 핵심적인 흐름을 얘기합니다. 즉 게임이 시작할 때부터 종료까지 수행하는 단계들게임

devcol.tistory.com

 


[Unreal Engine/UE 기초] - UI 위젯 설계와 실시간 데이터 연동하기 | [언리얼 엔진 C++ (Unreal Engine C++)]

 

UI 위젯 설계와 실시간 데이터 연동하기 | [언리얼 엔진 C++ (Unreal Engine C++)]

UI 위젯 설계와 실시간 데이터 연동하기 | [언리얼 엔진 C++ (Unreal Engine C++)] UMG (User Widget) 위젯 기초 디자인 이해하기 1️⃣ HUD (Heads-Up Display)란?HUD는 게임 내에서 플레이어에게 정보를 제공하기 위

devcol.tistory.com

 

[Unreal Engine/UE 기초] - 게임 흐름에 맞춘 메뉴 UI 구현하기 | [언리얼 엔진 C++ (Unreal Engine C++)]

<div class="og-image" style="background-image: url('https://scrap.kakaocdn.net/dn/cnypNL/dJMb8VND
저작자표시 동일조건 (새창열림)
'Unreal Engine/UE 기초' 카테고리의 다른 글
  • UI 애니메이션 효과 및 3D 위젯 UI 구현하기 | [언리얼 엔진 C++ (Unreal Engine C++)]
  • 게임 흐름에 맞춘 메뉴 UI 구현하기 | [언리얼 엔진 C++ (Unreal Engine C++)]
  • UI 위젯 설계와 실시간 데이터 연동하기 | [언리얼 엔진 C++ (Unreal Engine C++)]
  • 게임 루프 설계를 통한 게임 흐름 제어하기 | [언리얼 엔진 C++ (Unreal Engine C++)]
DevCol
DevCol
DevCol (Development Collaboration). 함께 개발 & 공부 & IT 정보 나눔장소
  • DevCol
    DevCol (Development Collaboration)
    DevCol
  • 블로그 메뉴

    • Unreal Engine
    • TIL
    • 게임국가기술자격검정 게임프로그래밍전문가 [한국콘텐츠진흥원]
    • 분류 전체보기 (73) N
      • Unreal Engine (31) N
        • Project (2) N
        • Dev Log (0)
        • Debugging (2) N
        • Blueprint (1)
        • UE 기초 (25) N
        • UE 심화 (0)
        • TA (1) N
      • Programming Language (0)
        • C++ (0)
        • C# (0)
      • Unity Engine (0)
      • 자격증 (3)
        • 게임국가기술자격검정 [한국콘텐츠진흥원] (3)
      • Coding Test | 코딩테스트 (0)
        • 프로그래머스 기초 (0)
        • 프로그래머스 입문 (0)
      • TIL (38) N
        • Boot Camp (32) N
      • Git & Github (1)
  • 링크

    • Youtube
    • GitHub
    • itch.io
    • Blog (En)
  • 공지사항

  • 인기 글

  • 태그

    Game Dev
    UE5
    Programming
    기초
    내일배움캠프
    Devlog
    til
    프로그래밍
    언리얼 엔진
    c
    cpp
    게임개발
    Code Kata
    Unreal engine
    Boot Camp
    코드 카타
    코드카타
    UE
    게임 개발
    C++
  • 최근 글

  • GitHub Youtube itch
  • hELLO · Designed By 정상우.v4.10.6
  • DevCol
    파티클과 사운드로 게임 효과 연출하기 | [언리얼 엔진 C++ (Unreal Engine C++)]
    상단으로

    티스토리툴바