C++과 Unreal Engine으로 3D 게임 개발 14

김여울·2025년 7월 22일

내일배움캠프

목록 보기
50/139

📍 4주차 4강

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

13.1 파티클 시각 효과 추가

a. 파티클 시스템 (Particla System) 기본 개념

파티클 시스템

  • 불꽃, 연기, 폭발 등 시각적 효과를 구현하는 도구
  • 여러 개의 작은 입자들이 모여 특정한 모양이나 애니메이션을 생성

b. Cascade vs. Niagara

  • Cascade
    • 언리얼 엔진 3 ~ : 오래된 파티클 툴
    • 초급자가 배우기 쉽고 빠르게 결과를 보기 가능
    • 복잡한 VFX엔 무리
  • Niagara
    • 언리얼 엔진 4 후반 ~ : 도입된 차세대 파티클 시스템
    • 고급 VFX 연출이 가능하고, 블루프린트, 머티리얼과 연동되어 정교한 제어 가능
    • GPU 파티클을 지원, 최신 기능이 빠르게 적용

파티클 에셋 구분

  • 이름 앞에 P_ 또는 Niagara_접두어를 사용하여 구분

13.2 사운드 효과 추가

a. 언리얼 엔진 사운드 효과의 종류

사운드 웨이브(Sound Wave)

  • 단일 오디오 파일(주로 .wav)
  • 게임 내에서 기본 오디오 데이터를 재생하는 데 사용
  • 간단한 효과음이나 UI 사운드 등 단일 음원(싱글 트랙) 재생용
  • 재생 시 추가 효과나 제어가 어려움

사운드 큐(Sound Cue)

  • 여러 사운드 웨이브를 조합하거나, 랜덤/시퀀셜 재생, 믹싱, 페이드 등을 설정할 수 있는 오디오 편집 그래프
  • 동적 사운드(구간 전환, 발걸음 소리 랜덤 재생 등)와 복잡한 사운드 로직을 구성할 때 사용
  • Sound Cue Editor에서 시각적 노드를 통해 사운드 로직을 제어 가능

b. 사운드 웨이브 vs 사운드 큐

둘 다 SoundBase의 상속을 받음

  • 사운드 웨이브
    • 단일 오디오 파일을 그대로 재생할 때 사용
      (간단한 UI 효과음, 짧은 효과음 등)
  • 사운드 큐
    • 여러 사운드를 조합하거나, 복잡한 동적 사운드 로직을 구성할 때 사용
      (랜덤화, 믹싱 등 고급 기능 필요시)

13.3 아이템에 파티클, 사운드 적용

a. 아이템 획득에 파티클 적용

나머지 아이템들은 BaseItem의 상속을 받았기 때문에 BaseItem에만 해도 됨

// BaseItem.h
UParticleSystem* PickupParticle;	// 파티클 시스템 추가
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item|Effects")
USoundBase* PickupSound;	// 사운드 추가

// BaseItem.cpp
#include "Kismet/GameplayStatics.h"	// 파티클, 사운드

void ABaseItem::ActivateItem(AActor* Activator)
{
	// 지정된 위치랑 회전에서 파티클 재생해 ~
	if (PickupParticle)
	{
		// 지정된 위치나 회전에 따라 파티클 효과 생성하는 함수
		UGameplayStatics::SpawnEmitterAtLocation(
			GetWorld(),	// 현재 게임이 실행되고 있는 월드 객체 가져옴<-파티클 생성한 월드 정보 필요
			PickupParticle,	// 지정해놓은 파티클 에셋
			GetActorLocation(),	// 이 아이템의 월드 위치 가져옴
			GetActorRotation(),	// 이 아이템의 회전 위치 가져옴
			true	// AutoDistroy : 파티클 효과 끝난 이후에 메모리에서 자동 제거 되도록 설정
		);
	}

	// 지정된 위치에서 사운드 효과 재생해 ~
	if (PickupSound)
	{
		UGameplayStatics::PlaySoundAtLocation(
			GetWorld(),
			PickupSound,
			GetActorLocation()
		);
	}
}

b. BP에 파티클, 사운드 적용

c. 부모클래스의 ActivateItem을 불러오기

지금은 그냥 override만 해놓고 부모 거 안 부름!

// MineItem.cpp
void AMineItem::ActivateItem(AActor* Activator)	// 활성화되고 5초 후에 폭발
{
	Super::ActivateItem(Activator);	// → 다른 아이템들에도 추가하기
	// ... 
}

d. 파티클 2초 뒤에 사라지게 하기

파티클은 아이템에 붙어 있는 게 아니라 자체적인 라이프 사이클이니까
따로 삭제 해줘야 함 → false로

// MineItem.h
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item|Effects")
UParticleSystem* ExplosionParticle;	// 파티클 시스템 추가
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item|Effects")
USoundBase* ExplosionSound;	// 사운드 추가

// MineItem.cpp
#include "MineItem.h"
#include "Components/SphereComponent.h"
#include "Kismet/GameplayStatics.h"
#include "SpartaCharacter.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);  // 300.0f 넣기
	ExplosionCollision->SetCollisionProfileName(TEXT("OverlapAllDynamic")); // 모든 동적 오버랩 허용
	ExplosionCollision->SetupAttachment(Scene);	// 루트 컴포넌트인 씬 컴포넌트에 부착

}

void AMineItem::ActivateItem(AActor* Activator)	// 활성화되고 5초 후에 폭발
{
    if (bHasExploded) return;

	Super::ActivateItem(Activator);
	// 게임 월드에는 각자의 타이머를 관리하는 타이머 매니저가 있음
	// 타이머 핸들러 : 각자의 타이머를 갖고 있음

	// 월드에서 타이머 매니저 부름
	GetWorld()->GetTimerManager().SetTimer(
		ExplosionTimerHandle,	// 타이머 달아줌
		this,  // 타이머가 끝나면 호출할 대상 : 이 객체에
		&AMineItem::Explode,  // 호출할 함수
		ExplosionDelay,  // 지연 시간
		false  // 반복 여부 (false면 한 번만 실행)
	);

    bHasExploded = true;
}

void AMineItem::Explode()
{
    // 이미 선언된 'Particle' 변수를 재사용
    UParticleSystemComponent* Particle = nullptr;  // 다시 초기화

    if (ExplosionParticle)
    {
        // 이미 선언된 변수 'Particle'을 재사용
        Particle = UGameplayStatics::SpawnEmitterAtLocation(
            GetWorld(),
            ExplosionParticle,
            GetActorLocation(), // GetActorLocation() 함수 호출
            GetActorRotation(), // GetActorRotation() 함수 호출
            false
        );
    }

    if (ExplosionSound)
    {
        UGameplayStatics::PlaySoundAtLocation(
            GetWorld(),
            ExplosionSound,
            GetActorLocation()
        );
    }

    TArray<AActor*> OverlappingActors;  // 범위 내 겹치는 액터 검색
    ExplosionCollision->GetOverlappingActors(OverlappingActors);

    // 범위 내 돌면서 태그 확인
    for (AActor* Actor : OverlappingActors)
    {
        if (Cast<ASpartaCharacter>(Actor))
        {
            UGameplayStatics::ApplyDamage(
                Actor,	// 데미지를 받을 액터
                ExplosionDamage,	// 데미지의 양
                nullptr,	// 데미지를 유발한 주체
                this,	// 데미지를 입힌 액터
                UDamageType::StaticClass()	// 데미지의 유형
            );
        }
    }

    DestroyItem();

    if (Particle)
    {
        FTimerHandle DestroyParticleTimerHandle;
        // 람다 함수 - 이름이 없다?
        // 직접 구현하긴 뭐하고 간단하게 함수같이 쓰고 싶을 때 람다 쓰기
        GetWorld()->GetTimerManager().SetTimer(
            DestroyParticleTimerHandle,
            [Particle]()
            {
                Particle->DestroyComponent();
            },
            2.0f,
            false
        );
    }
}

BaseItem도 적용해주기
근데 이건 너무 길어서 그냥 나중에 코드 보기...

0개의 댓글