
파티클과 사운드로 게임 효과 연출하기
파티클 시각 효과 추가하기
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 학습을 권장합니다.
- Cascade: 언리얼 엔진 3 시절부터 제공된 오래된 파티클 편집 툴입니다. 언리얼 엔진 4, 5에서도 여전히 호환되지만, 신규 기능 업데이트는 주로 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()로 컴포넌트를 받아서 관리할 수 있어요.
정리
현재 코드처럼 아이템 획득 사운드라면:
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++)]