1. 랜덤 위치에 아이템 스폰하기
1) 레벨 세팅하기
: Resources 폴더에 Maps 폴더에는 3개의 레벨이 이미 존재한다.
각각 난이도에 따라서 크기가 다른 맵이고, BasicLevel - Intermediate Level - Advanced Level 순서로 크기가 작아진다.

- 위 3개의 레벨을 Content - Maps 폴더로 이동 시킨 후, 기존에 사용하던 MainLevel에서 BasicLevel을 기본 레벨로 지정한다.
: Edit - Project Settings - Maps & Modes 에서 Default Maps를 BasicLevel로 변경
- MainLevel 은 삭제 한다.
2) 콜리전 컴포넌트로 스폰 영역 지정하기
- 언리얼 엔진에서는 직접 지정한 박스(상자), 구체, 캡슐 등 원하는 형태의 '콜리전 컴포넌트' 내부에서 임의의 좌표를 뽑아 아이템(액터)을 스폰할 수 있다.
- 가장 간단한 방법으로, 콜리전 컴포넌트인 UBoxComponent를 활용할 것이다.
- 우선, 새로운 C++ 클래스 Actor를 상속한 클래스로, SpawnVolume 이라는 이름으로 클래스를 하나 생성한다.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "SpawnVolume.generated.h"
class UBoxComponent;
UCLASS()
class SPARTAPROJECT_API ASpawnVolume : public AActor
{
GENERATED_BODY()
public:
ASpawnVolume();
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Spawning")
USceneComponent* Scene;
// 스폰 영역을 담당할 박스 컴포넌트
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Spawning")
UBoxComponent* SpawningBox;
// 스폰 볼륨 내부에서 무작위 좌표를 얻어오는 함수
UFUNCTION(BlueprintCallable, Category="Spawning")
FVector GetRandomPointInVolume() const;
// 특정 아이템 클래스를 스폰하는 함수
UFUNCTION(BlueprintCallable, Category="Spawning")
void SpawnItem(TSubclassOf<AActor> ItemClass);
};
SpawnVolume.h
- UBoxComponent* SpawningBox;
: 이 컴포넌트가 박스 형태의 콜리전 영역을 나타낸다.
언리얼 엔진의 UBoxComponent는 박스 내부에서 오버랩(Overlap)이나 충돌(Collision)을 감지할 수 있는 컴포넌트이다.
실제로 눈에 보이는 3D 메시는 아니며, "박스 형태의 충돌 범위" 만 제공한다.
- GetRandomPointInVolume()
: SpawningBox 범위 내부에서 랜덤 좌표 (FVector)를 리턴한다.
이 좌표를 이용해 아이템을 생성하면 "직육면체 형태 범위 안에서" 임의의 위치에 아이템이 뜨는 효과를 낼 수 있다.
- SpawnItem()
: 파라미터로 받은 아이템 클래스를 SpawnVolume 내부의 랜덤 위치에 생성 (SpawnActor) 하는 함수이다.
#include "SpawnVolume.h"
#include "Components/BoxComponent.h"
#include "Engine/World.h"
#include "GameFramework/Actor.h"
ASpawnVolume::ASpawnVolume()
{
PrimaryActorTick.bCanEverTick = false;
// 박스 컴포넌트를 생성하고, 이 액터의 루트로 설정
Scene = CreateDefaultSubobject<USceneComponent>(TEXT("Scene"));
SetRootComponent(Scene);
SpawningBox = CreateDefaultSubobject<UBoxComponent>(TEXT("SpawningBox"));
SpawningBox->SetupAttachment(Scene);
}
FVector ASpawnVolume::GetRandomPointInVolume() const
{
// 1) 박스 컴포넌트의 스케일된 Extent, 즉 x/y/z 방향으로 반지름(절반 길이)을 구함
FVector BoxExtent = SpawningBox->GetScaledBoxExtent();
// 2) 박스 중심 위치
FVector BoxOrigin = SpawningBox->GetComponentLocation();
// 3) 각 축별로 -Extent ~ +Extent 범위의 무작위 값 생성
return BoxOrigin + FVector
(
FMath::FRandRange(-BoxExtent.X, BoxExtent.X),
FMath::FRandRange(-BoxExtent.Y, BoxExtent.Y),
FMath::FRandRange(-BoxExtent.Z, BoxExtent.Z)
);
}
void ASpawnVolume::SpawnItem(TSubclassOf<AActor> ItemClass)
{
if (!ItemClass) return;
GetWorld()->SpawnActor<AActor>
(
ItemClass,
GetRandomPointInVolume(),
FRotator::ZeroRotator
);
}
SpawnVolume.cpp
- GetScaleBoxExtent()
: 박스 컴포넌트의 실제 "가로/세로/높이 절반 길이(Extent)" 를 반환한다.
에디터에서 Scale을 조절하면 여기에도 반영된다.
- FRandRange(a, b)
: a ~ b 사이의 임의의 float 값을 반환한다.
여기서는 X, Y, Z 좌표마다 랜덤 값을 생성하여 BoxOrigin에 더해준다.
3) 아이템 랜덤 스폰 테스트하기
- 빌드를 하고 언리얼 에디터로 돌아와서 이 SpawnVolume 클래스를 상속받은 BP_SpawnVolume Blueprint 클래스를 생성한다.
- BP_SpawnVolume 을 레벨에 드래그해 놓고, 적적히 위치와 스케일을 조정한다.

- BP_SpawnVolume의 이벤트 그래프에서 Begin Play 노드를 연결하고, 그 다음에 C++의 SpawnItem() 함수를 호츨하는 Blueprint 노드를 추가한다.

2. 아이템 스폰 확률 데이터 테이블 만들기
1) Item Data 구조체 만들기
- 어떤 아이템이 몇 % 확률로 스폰되는지를 코드로 직접 하드코딩하면, 매번 수정할 때마다 빌드를 해줘야 해서 번거롭다.
- 언리얼 엔진의 데이터 테이블을 사용하면, 이를 엑셀 (CSV)이나 JSON 파일로 관리해서 엔진 안으로 임포트하고, 코드나 블루프린트에서 쉽게 불러 쓸 수 있다.
기획자나 디자이너도 엑셀에서 숫자만 바꾸면 되므로 매우 편리하다.
- 우선, 데이터 테이블의 각 "행(Row)" 을 C++ 구조체로 매핑해야 한다.
언리얼 엔진은 FTableRowBase라는 기본 구조체를 제공하며, 이를 상속한 구조체를 만들면, 각 CSV(또는 JSON) 행을 FItemSpanwRow 구조체에서 정해준 형태로 받아올 수 있게 된다.
- 이러한 구조는 클래스가 아니기 때문에, 부모 클래스를 선택할 수 있는 옵션에 나타나지 않는다.
따라서 None을 선택해서 ItemSpawnRow C++ 파일을 만들어 준다.
#pragma once
#include "CoreMinimal.h"
#include "Engine/DataTable.h" // FTableRowBase 정의가 들어있는 헤더
#include "ItemSpawnRow.generated.h"
USTRUCT(BlueprintType)
struct FItemSpawnRow : public FTableRowBase
{
GENERATED_BODY()
public:
// 아이템 이름
UPROPERTY(EditAnywhere, BlueprintReadWrite)
FName ItemName;
// 어떤 아이템 클래스를 스폰할지
UPROPERTY(EditAnywhere, BlueprintReadWrite)
TSubclassOf<AActor> ItemClass;
// 이 아이템의 스폰 확률
UPROPERTY(EditAnywhere, BlueprintReadWrite)
float SpawnChance;
};
SpawnVolume.cpp
- FTableRowBase
: 언리얼 엔진에서 "이 구조체는 데이터 테이블로 쓸 수 있다" 라고 인식하게 해주는 베이스 구조체이다.
- TSubclassOf
: 하드 래퍼런스로, 클래스가 항상 로드된 상태에서 바로 접근한다.
- TSoftClassPtr<AActor>
: 소프트 래퍼런스로, 클래스를 바로 로드하지 않고도 경로만 기억해둘 수 있다.
필요할 때 Get() 을 통해 실제 UClass* 를 얻어 인스턴스를 생성할 수 있다.
2) CSV 파일 작성 및 임포트하기 (방법 1)
- 액셀을 열고 2열부터 Column 이름을 작성한다.
- 1열은 "RowName" 으로 인식된다.
Key 값으로 사용할 값을 입력해줘야 한다.
- 2열부터는 각 아이템(데이터) 정보를 한 줄씩 기록한다.
- 경로는 각 BP 클래스 우클릭 -> Copy Reference 로 정확히 가져온다.
그리고 블루프린트의 경우 "_C" 가 붙어야 BP 클래스로 인식되니까 마지막에 반드시 붙여줘야 한다.


- Copy Reference는 언리얼 엔진 런타임에서 사용되는 "클래스/오브젝트" 경로이다.
: 블루프린트/C++ 로 자산 로드 시 ConstructorHelpers::FClassFinder 등에서 활용한다.
- CSV 로 저장
: 언리얼 콘텐츠 폴더가 아니라 본인이 편한 로컬 위치에 파일명 ItemSpawnTable.csv 로 CSV UTF-8 (쉼표로 분리)로 저장한다.
- 언리얼 에디터로 돌아와, Content Browser에서 Blueprints 폴더에서 우클릭 -> Import to /Game/ Blueprints 를 선택, 아래와 같이 옵션을 설정하고 Apply를 클릭한다.

- 그러면 다음과 같이 성공적으로 DataTable이 만들어진 것을 볼 수 있다.

3) 언리얼 에디터에서 직접 데이터 테이블 수정하기 (방법 2)
: CSV를 쓰는 대신, 언리얼 에디터 내에서 직접 편집하는 방법도 있다.
1. Content Browser 창에서 우클릭 -> Miscellaneous -> Data Table 선택
2. 팝업창에서 Row Structure로 FItemSpawnRow를 선택하고 OK
3. 새로 생성된 DataTable 에셋을 열면, 에디터 UI 상에서 아래처럼 Row 정보를 바로 편집 가능하다.
- Add Row 버튼을 눌러 행을 추가하고, Row Name, ItemName, ItemClass, SpawnChance 등을 원하는 값으로 넣는다.
- 하지만 이런 방식은 Excel처럼 대량의 데이터를 한 번에 편집하기는 불편할 수 있다.
- 상황에 따라 에디터 직접 편집 / CSV 임포트 모두 필요에 따라 병행할 수도 있다.
3. 스폰에 확률 적용하기
1) 데이터 테이블을 사용하여 스폰 로직 구현하기
: SpawnVolume 클래스로 돌아와서, 확률 로직을 "어떤 아이템을 스폰할지" 결정하는 코드로 다시 수정을 한다.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "ItemSpawnRow.h" // 우리가 정의한 구조체
#include "SpawnVolume.generated.h"
class UBoxComponent;
UCLASS()
class SPARTAPROJECT_API ASpawnVolume : public AActor
{
GENERATED_BODY()
public:
ASpawnVolume();
UFUNCTION(BlueprintCallable, Category = "Spawning")
void SpawnRandomItem();
protected:
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Spawning")
USceneComponent* Scene;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Spawning")
UBoxComponent* SpawningBox;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Spawning")
UDataTable* ItemDataTable;
FVector GetRandomPointInVolume() const;
FItemSpawnRow* GetRandomItem() const;
void SpawnItem(TSubclassOf<AActor> ItemClass);
};
SpawnVolume.h
#include "SpawnVolume.h"
#include "Components/BoxComponent.h"
#include "Engine/World.h"
ASpawnVolume::ASpawnVolume()
{
PrimaryActorTick.bCanEverTick = false;
Scene = CreateDefaultSubobject<USceneComponent>(TEXT("Scene"));
SetRootComponent(Scene);
SpawningBox = CreateDefaultSubobject<UBoxComponent>(TEXT("SpawningBox"));
SpawningBox->SetupAttachment(Scene);
ItemDataTable = nullptr;
}
void ASpawnVolume::SpawnRandomItem()
{
if (FItemSpawnRow* SelectedRow = GetRandomItem())
{
if (UClass* ActualClass = SelectedRow->ItemClass.Get())
{
SpawnItem(ActualClass);
}
}
}
FVector ASpawnVolume::GetRandomPointInVolume() const
{
const FVector BoxExtent = SpawningBox->GetScaledBoxExtent();
const FVector BoxOrigin = SpawningBox->GetComponentLocation();
return BoxOrigin + FVector(
FMath::FRandRange(-BoxExtent.X, BoxExtent.X),
FMath::FRandRange(-BoxExtent.Y, BoxExtent.Y),
FMath::FRandRange(-BoxExtent.Z, BoxExtent.Z)
);
}
FItemSpawnRow* ASpawnVolume::GetRandomItem() const
{
if (!ItemDataTable) return nullptr;
// 1) 모든 Row(행) 가져오기
TArray<FItemSpawnRow*> AllRows;
static const FString ContextString(TEXT("ItemSpawnContext"));
ItemDataTable->GetAllRows(ContextString, AllRows);
if (AllRows.IsEmpty()) return nullptr;
// 2) 전체 확률 합 구하기
float TotalChance = 0.0f; // 초기화
for (const FItemSpawnRow* Row : AllRows) // AllRows 배열의 각 Row를 순회
{
if (Row) // Row가 유효한지 확인
{
TotalChance += Row->SpawnChance; // SpawnChance 값을 TotalChance에 더하기
}
}
// 3) 0 ~ TotalChance 사이 랜덤 값
const float RandValue = FMath::FRandRange(0.0f, TotalChance);
float AccumulateChance = 0.0f;
// 4) 누적 확률로 아이템 선택
for (FItemSpawnRow* Row : AllRows)
{
AccumulateChance += Row->SpawnChance;
if (RandValue <= AccumulateChance)
{
return Row;
}
}
return nullptr;
}
void ASpawnVolume::SpawnItem(TSubclassOf<AActor> ItemClass)
{
if (!ItemClass) return;
GetWorld()->SpawnActor<AActor>(
ItemClass,
GetRandomPointInVolume(),
FRotator::ZeroRotator
);
}
SpawnVolume.cpp
- SpawnRandomItem()
: "데이터 테이블에서 확률 계산으로 Row 하나 선택" + "스폰" 을 분리해놓아 코드가 깔끔해진다.
- GetRandomItem()
: 테이블의 모든 Row를 가져와 확률합을 구하고, 확률에 따라 한 개를 뽑아 리턴한다.
- Algo::Accumulate
: 언리얼 엔진에서 추가한 C++ 템플릿 함수로, STL의 std::accumulate 와 유사하게 작동한다.
각 Row의 SpawnChance를 계속 더해 총 합을 구한다.
"배열 안에 있는 모든 값을 하나로 누적해서 합쳐주는" 함수이다.
- 빌드 후, BP_SpawnVolume 로 돌아가서, 속성에서 Item Data Table에도 기존에 만들어 둔 데이터 테이블인 ItemSpawnTable 에셋을 할당한다.

- 이벤트 그래프 SpawnRandomItem() 함수를 스크린샷과 같이 10번의 호출을 하도록 로직 노드를 생성한다.

2) 레벨마다 다른 확률 데이터 적용하기
: 원래 기획 취지에 의하면 레벨별로 다른 확률을 가져야 한다.
이를 구현하는 방법은 여러 가지가 있다.
1. 레벨별로 서로 다른 DataTable 에셋을 사용
- CSV(또는 JSON)에 각각 다른 확률을 넣고, 레벨 1 맵에서는 BP_SpawnVolume 가 ItemSpawnTable_Level1 을 바라보도록 설정.
- 레벨 2 맵에서는 ItemSpawnTable_Level2 할당... 등등
- 관리가 직관적이며 간단하다.
2. 단일 DataTable에 "레벨 구분" 컬럼 추가
- 예 : CSV에 LevelIndex (또는 LevelName) 같은 컬럼을 하나 더 두고, 각각 어느 레벨에 해당하는지 적는다.
- 코드를 수정하여, 현재 레벨이 1인지 2인지 확인 후 "필터링된 Row" 만 확률 계산.
- 한 DataTable에서 모든 레벨 정보를 한 번에 관리할 수 있는 장점이 있으나, 코드가 약간 복잡해진다.
- 실무나 개인 프로젝트에서 상황에 맞춰 자유롭게 선택하면 된다.
'본캠프 TIL' 카테고리의 다른 글
| 06.24 Unreal 본캠프)게임 루프 설계를 통한 게임 흐름 제어하기 (0) | 2026.06.24 |
|---|---|
| 06.24 Unreal 본캠프) 캐릭터 체력 및 점수 관리 시스템 구현하기 (0) | 2026.06.24 |
| 06.23 Unreal 본캠프) 충돌 이벤트로 획득되는 아이템 구현하기 (0) | 2026.06.23 |
| 06.22 Unreal 본캠프) 인터페이스 기반 아이템 클래스 설계하기 (0) | 2026.06.22 |
| 06.17 Unreal 본캠프) 4번 과제 Pawn 클래스 3D 캐릭터 만들기 (2) (0) | 2026.06.17 |