게임 개발/학습 일지

언리얼 심화 - 템플릿 패턴, 샌드박스 패턴

이개 2026. 5. 1. 18:00

템플릿 패턴이란?

알고리즘의 골격은 부모 클래스에 고정하되, 세부 단계는 자식 클래스에서 구현하도록 하는 방식.

샌드박스 패턴이란?

외부 코드나 하위 시스템이 메인 시스템의 핵심 API에 직접 접근하지 못하게 하고, '제한된 인터페이스(Sandboxed API)'만을 통하도록 강제하는 전략

 

 

템플릿 패턴 구현

1. 액터로 WeaponBase 하나를 만든다.

 

 

2. 해당 액터를 상속한 C++ 클래스인 WeaponTemplateBese를 만든다.

파생 C++ 클래스 만들기
이름은 WeaponTemplateBase

 

 3. WeaponBase에 총에서 사용할 것들을 미리 넣어준다.

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "WeaponBase.generated.h"

UCLASS()
class MYPROJECT_API AWeaponBase : public AActor
{
	GENERATED_BODY()
	
public:	
	// Sets default values for this actor's properties
	AWeaponBase();

protected:
	// Called when the game starts or when spawned
	virtual void BeginPlay() override;

public:	
	// Called every frame
	virtual void Tick(float DeltaTime) override;

	UPROPERTY(VisibleAnywhere, BlueprintReadOnly)
	TObjectPtr<USceneComponent> Root; // 루트 컴포넌트

	UPROPERTY(VisibleAnywhere, BlueprintReadOnly)
	TObjectPtr<class UArrowComponent> FirePoint; // 방향만 가르쳐준다.

	UFUNCTION(BlueprintCallable)
	virtual void Fire(); // 총 쏘기

protected:
	// 소모되는 탄약 수
	UPROPERTY(EditAnywhere, BlueprintReadWrite)
	int AmmoPerFire;

	// 남은 탄약 수
	UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly)
	int CurrentAmmo;

	// 탄약 보유량
	UPROPERTY(EditAnywhere, BlueprintReadWrite)
	int MaxAmmo;

	// Rate of Fire 연사속도
	UPROPERTY(EditAnywhere, BlueprintReadWrite)
	float RoF;

	// 유효 사거리
	UPROPERTY(EditAnywhere, BlueprintReadWrite)
	float Range;

	// 데미지 양
	UPROPERTY(EditAnywhere, BlueprintReadWrite)
	float DamagePerHit;

	// 사격 가능 여부
	UPROPERTY(BlueprintReadWrite)
	bool CanFire;

	// 연사 속도 제어를 위한 핸들
	UPROPERTY(BlueprintReadWrite)
	FTimerHandle TimerFireDelay;


	// 타이머 연결할 함수
	UFUNCTION()
	void HandleFireDelay();

};

Ammo는 Ammunition의 줄임말이다.

 

4. 생성자 만들고 함수를 구현한다. 이후 WeaponBase는 건드리면 안 된다.

cpp파일에 ArrowComponent.h를 include 한다.

#include "Components/ArrowComponent.h"

 

// Fill out your copyright notice in the Description page of Project Settings.


#include "WeaponBase.h"
#include "Components/ArrowComponent.h"

// Sets default values
AWeaponBase::AWeaponBase()
{
 	// Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
	PrimaryActorTick.bCanEverTick = true;

	Root = CreateDefaultSubobject<USceneComponent>(TEXT("Root"));
	RootComponent = Root;

	FirePoint = CreateDefaultSubobject<UArrowComponent>(TEXT("FirePoint"));
	FirePoint->SetupAttachment(RootComponent);

	AmmoPerFire = 1;
	CurrentAmmo = 0;
	MaxAmmo = 12;
	RoF = 1.f;
	CanFire = true;
	Range = 1000.;
	DamagePerHit = 100.f;

}

// Called when the game starts or when spawned
void AWeaponBase::BeginPlay()
{
	Super::BeginPlay();
	
	CurrentAmmo = MaxAmmo;
}

// Called every frame
void AWeaponBase::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

}

void AWeaponBase::Fire()
{
	// 일정 시간마다 사격할 수 있게 하기
	CanFire = false;
	GetWorld()->GetTimerManager().SetTimer(TimerFireDelay, this, &AWeaponBase::HandleFireDelay, 1.f/RoF, false); // RoF는 속도 배율이 된다.
}

void AWeaponBase::HandleFireDelay()
{
	// 일정 시간마다 사격할 수 있게 하기
	GetWorld()->GetTimerManager().ClearTimer(TimerFireDelay);
	CanFire = true;
}

헤더 쉽게 찾는 방법

해당 컴포넌드 타입을 선택하고 Ctrl+T
cpp 파일을 확인한다.
해당 컴포넌트 최상단에 원하는 헤더인 Components/ArrowComponent.h가 포함되어 있다.

 

무슨 헤더를 포함시켜야 할지 모를 때는 Ctrl+T로 해당 객체를 검색하고 cpp파일을 열어 최상단의 헤더 목록을 확인한다. 

 

세로 윈도우 만들기

우클릭 후 새 세로 문서 그룹을 만든다.

 

 

 

 

5. WeaponTemplateBase.h를 작성한다.

 

자식 클래스를 만들어 cpp로 구현하는 방법과 블루프린트로 구현하는 방법 두 가지가 있다. 이번에는 블루프린트로 작성한다.

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "WeaponBase.h"
#include "WeaponTemplateBase.generated.h"

/**
 * 
 */
UCLASS()
class MYPROJECT_API AWeaponTemplateBase : public AWeaponBase
{
	GENERATED_BODY()
	
public:
	virtual void Fire() override;

	UFUNCTION(BlueprintCallable, BlueprintNativeEvent)
	void Reload();

protected:
	UFUNCTION(BlueprintNativeEvent) // C++에서 기본 구현하고 블루프린트에서 오버라이드 가능한 함수
	bool CheckAmmo();

	// 총을 어떻게 쓸지
	UFUNCTION(BlueprintImplementableEvent) // 블루프린트에서 작성하는 함수
	void ProcessFiring(); // 라인트레이스 쓰는 함수

	// 이펙트 실행
	UFUNCTION(BlueprintImplementableEvent)
	void PlayEffects();

	// 총알 업데이트
	UFUNCTION(BlueprintNativeEvent)
	void UpdateAmmo();

};

 

BlueprintImplementableEvent는 구현하면 오류가 나므로 cpp에 작성하면 안 된다.

 

BlueprintNativeEvent처럼 블루프린트와 C++ 하이브리드 함수로 구현하는 경우에는 C++ 코드 함수명에 _Implementation을 넣어줘야 한다.

// Fill out your copyright notice in the Description page of Project Settings.


#include "WeaponTemplateBase.h"
void AWeaponTemplateBase::Fire()
{
	if (!CanFire) { return; }

	if (CheckAmmo())
	{
		// 순서를 마음대로
		PlayEffects();
		ProcessFiring();
		UpdateAmmo();

		Super::Fire();
		return;
	}
}
void AWeaponTemplateBase::Reload_Implementation()
{
	CurrentAmmo = MaxAmmo;
}
bool AWeaponTemplateBase::CheckAmmo_Implementation()
{
	return AmmoPerFire <= CurrentAmmo;
}
void AWeaponTemplateBase::UpdateAmmo_Implementation()
{
	CurrentAmmo -= AmmoPerFire;
}

 

이 상태로 WeaponTemplateBase를 상속받은 C++ 클래스 TemplateWeapon_Pistol과 TemplateWeapon_Shotgun을 만든다.

 

C++ 자식 클래스 생성
블루프린트 생성

 

블루프린트와 C++ 코드 중 겹치는 함수가 있을 경우 코드가 우선이다.

 

 

Pistol 블루프린트

Root와 FirePoint 컴포넌트 존재 확인
Event Reload 추가
부모 함수에대한 호출 추가까지 해줘야만 사용이 가능하다. 부모에 아무도 없더라도 호출해줘야한다.

이벤트노드를 우클릭하여 부모 노드에 대한 함수 추가를 진행한다.

 

상속받은 블루프린트는 함수를 불러올 때 블루프린트 부모를 먼저 호출하고, 그 후에 C++ 클래스 부모를 호출한다.

Event 추가

 

LineTrace의 시작점 지정
LineTraceByChannel의 도착점 지정. ForwardVector*Range + StartLocation
PlayEffects, ProcessFiring 노드를 만든다. PlayEffects에서는 사운드 재생, LineTraceByChannel에서는 발사 로직을 구현한다.
LineTraceByChannel 뒤에서 검출 여부를 Branch로 만들고, True면 ApplyDamage를 실행한다. LineTraceByChannel의 Out Hit 핀을 Break HitResult로 나눠준 후 ApplyDamage의 Damaged Actor에 연결한다. Damage Per Hit도 Get으로 가져와서 Base Damage에 연결한다.

 

BeginPlay에 Fire를 바로 연결하면 쏘아지지 않는다.
부모 함수를 호출해도 쏘아지지 않는다.

 

Delay를 3초 주면 그제서야 쏘아진다.
그 이유는 WeaponBase C++ 클래스에서 장전해주는 부모함수가 늦게 실행되기 때문이다.

 

Super::BeginPlay()의 순서를 바꿔주면 해결된다.
라인트레이스 DrawDebugType 설정을 for Duration으로 해주면 쏘아지는 것이 보인다.

Shotgun 블루프린트

Pistol 블루프린트를 그대로 복사-붙여넣기한다.

 

 

그리고 Get Forward Vector 뒤의 범위를 변경한다.

 

Random Unit Vector in Cone in Degrees 노드만 추가해주면 된다. 상수값은 20정도 넣어본다.

 

라인트레이스는 한번에 쏘는 탄환 수만큼 For Loop로 실행한다.

피스톨과 샷건의 차이를 만들어낼 수 있다.

 

샌드박스 패턴 구현

템플릿 패턴과 반대로 기능을 만들어놓고 조립하는 방식. 약간 특이한 기능을 구현하고 싶을 때 사용할 수 있다.

 

1. WeaponBase를 상속한 SandboxWeaponBase를 만든다.

 

2. BlueprintImplementableEvent를 만든다.

BlueprintImplementableEvent를 사용하면 virtual을 사용할 수 없다. 그래서 원본 클래스인 WeaponBase의 UFUNCTION 속성을 변경하면 안 된다. 대신 우회해서 사용하는 방법을 쓴다.

virtual void Fire() override;

UFUNCTION(BlueprintImplementableEvent)
void SandboxFire();

-> 같은 기능을 하는 BlueprintImplementableEvent 형식의 다른 함수를 만들고, 원래 virtual void Fire에서 호출한다.

void ASandboxWeaponBase::Fire()
{
	SandboxFire();
}

// SandboxFire는 BlueprintImplementableEvent이기 때문에 구현부를 만들면 안 된다.

이렇게 하면 원본을 바꾸지 않고 BlueprintCallable 가상함수를 BlueprintImplementableEvent로 바꿀 수 있다.

 

UFUNCTION 블루프린트 관련 옵션

BlueprintCallable

UFUNCTION(BlueprintCallable)
 

👉 블루프린트에서 함수 호출 가능

 

BlueprintPure

UFUNCTION(BlueprintPure)
 

👉 값만 반환, 실행 핀 없음 (순수 함수)

  • 상태 변경 ❌
  • getter 느낌

BlueprintImplementableEvent

UFUNCTION(BlueprintImplementableEvent)
 

👉 C++ 구현 없음, 블루프린트에서만 구현

 

BlueprintNativeEvent

UFUNCTION(BlueprintNativeEvent)
 

👉 C++ 기본 구현 + 블루프린트 override 가능

 

 

3. 함수를 작성한다.

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "WeaponBase.h"
#include "SandboxWeaponBase.generated.h"

/**
 * 
 */
UCLASS()
class MYPROJECT_API ASandboxWeaponBase : public AWeaponBase
{
	GENERATED_BODY()
	
public:
	virtual void Fire() override;

	UFUNCTION(BlueprintImplementableEvent)
	void SandboxFire();

	UFUNCTION(BlueprintCallable)
	void Reload();

protected:
	// 총알 체크
	UFUNCTION(BlueprintCallable)
	bool CheckAmmo();

	// 총쏘기
	UFUNCTION(BlueprintCallable)
	void LinetraceOneShot(FVector Direction);
	
	// 사운드
	UFUNCTION(BlueprintCallable)
	void PlaySound(USoundBase* Sound);

	// 총알 업데이트
	UFUNCTION(BlueprintCallable)
	void UpdateAmmo();
};
// Fill out your copyright notice in the Description page of Project Settings.


#include "SandboxWeaponBase.h"
#include "Kismet/GameplayStatics.h"
#include "Components/ArrowComponent.h"

void ASandboxWeaponBase::Fire()
{
	SandboxFire();
}

void ASandboxWeaponBase::Reload()
{
	CurrentAmmo = MaxAmmo;
	HandleFireDelay();
}

bool ASandboxWeaponBase::CheckAmmo()
{
	return AmmoPerFire <= CurrentAmmo;
}

void ASandboxWeaponBase::LinetraceOneShot(FVector Direction)
{
	FHitResult Hit(ForceInit); // ForceInit으로 강제 초기화 가능
	FVector Start = FirePoint->GetComponentLocation();
	FVector End = Start + (Direction * Range);
	UKismetSystemLibrary::LineTraceSingle(
		this,
		Start,
		End,
		UEngineTypes::ConvertToTraceType(ECC_Visibility),
		false,
		{this, GetOwner()},
		EDrawDebugTrace::ForDuration,
		Hit,
		true,
		FLinearColor::Red,
		FLinearColor::Green,
		5.f
		);

}
void ASandboxWeaponBase::PlaySound(USoundBase* Sound)
{
	UGameplayStatics::PlaySoundAtLocation(this, Sound, GetActorLocation());
}

void ASandboxWeaponBase::UpdateAmmo()
{
	CurrentAmmo -= AmmoPerFire;
}

 

4. SandboxWeaponBase를 상속받은 Pistol, Shoutgun 블루프린트를 만들어준다.

블루프린트 생성

 

Pistol

BlueprintImplementableEvent 함수를 구현한다.

CanFire와 CheckAmmo를 실행하여 정상인지 확인한다.
템플릿 패턴의 코드로 구현했던 것처럼 PlaySound, LineTraceOneShot, UpdateAmmo 함수를 호출한다. PlaySound에서는 Sound Asset을 지정해준다.
마찬가지로 Set Timer by Function Name을 연결하고, Function Name에는 만들어둔 함수명 HandleFireDelay를 입력한다. Time에는 1/RoF를 연결한다.
출력 결과를 TimerFireDelay에 넣어 리셋해준다.

 

CheckAmmo가 false이면 Reload 되도록 한다. Sound Asset도 추가한다.

 

Fire대신 SandboxFire를 호출하고 있으므로, Fire에서 진행하던 CanFire 전환을 추가한다
BeginPlay에 Fire를 연결하여 테스트한다.
전체 노드 모양

 

Shotgun 블루프린트

Pistol의 노드들을 그대로 복사해넣는다.
s
탄환 여러 개가 나가도록 For Loop문을 추가한다. 방사형으로 발사되도록 Random Unit Vector in Cone in Degrees 노드를 넣고 값을 세팅한다. UpdateAmmo를 Completed에 넣어준다.
전체 노드
Pistol과 Shotgun 모두 잘 발사가 되는 것을 확인할 수 있다.