상세 컨텐츠

본문 제목

Unreal C++ 기초 8. 아이템 스폰 및 레벨 데이터 구현

Unreal C++

by hyunjunstar 2026. 4. 17. 23:59

본문

1. 랜덤 위치에 아이템 스폰하기

레벨 위에 아이템을 직접 하나하나 배치하는 방식이 아니라,

지정한 영역 내에서 랜덤 위치에 아이템이 생성되도록 구현해보았다.
이를 위해 SpawnVolume이라는 액터를 생성하고 해당 영역 안에서 아이템이 랜덤으로 스폰되도록 설정했다.

레벨 셋팅

우선 Resources - Maps 폴더에 있는 3개의 레벨을 Content - Maps 폴더로 이동 > 

Project Settings > Maps&Modes > Editor Startup Map, Game Default Map을 BasicLevel 적용

각 레벨은 난이도에 따라 구분되어 있으며, 난이도가 올라갈수록 맵 크기가 작아진다.

BasicLevel -> IntermediateLevel -> AdvancedLevel 순서

액터 생성 

상단 툴바 > Tools 클릭 > New C++ Class 클릭 > Actor 클릭 > 이름 SpawnVolume으로 지정 후 생성

생성 후 Visual Studio 코드 작성(각 코드 설명은 주석처리)

// SpawnVolume.h

#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;

    // SpawnVolume 범위 내에 랜덤 좌표 반환하는 함수
    UFUNCTION(BlueprintCallable, Category = "Spawning")
    FVector GetRandomPointInVolume() const;
    
    // 아이템 스폰 함수
    UFUNCTION(BlueprintCallable, Category = "Spawning")
    void SpawnItem(TSubclassOf<AActor> ItemClass);
};
// SpawnVolume.cpp

#include "SpawnVolume.h"
// UBoxComponent 사용을 위한 인클루드
#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
{
    // 박스의 절반 크기값을 벡터로 가져옴
    FVector BoxExtent = SpawningBox->GetScaledBoxExtent();
    // 박스의 기준 좌표이며 중심 위치값을 벡터로 가져옴
    FVector BoxOrigin = SpawningBox->GetComponentLocation();
	
    // 아이템 최종 스폰 위치
    // 중심 위치 + 랜덤 값(-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	// 기본 방향(0, 0, 0)을 기준으로 생성
    );
}

SpawnVolume 영역 지정

생성한 C++ 클래스 SpawnVolume 좌클릭 > Create Blueprint class based on SpawnVolume 클릭 >

BP_SpawnVolume으로 이름 지정후 생성

기존 레벨에 배치한 액터들 전부 삭제 후 BP_SpawnVolume를 각 레벨에 배치 

SpawnVolume 크기

BasicLevel - Location(0, 0, 100) / Scale(90, 90, 3)

IntermediateLevel - Location(0, 0, 100) / Scale(61, 61, 3)

AdvancedLevel - Location(0, 0, 100) / Scale(77, 77, 3)

2. 아이템 스폰 확률 데이터 테이블 생성 및 적용

Item Data 구조체 생성

아이템 스폰 확률을 코드에 직접 작성(하드코딩)하면 값을 수저알 때마다 빌드를 해줘야 해서 비효율적임

그래서 언리얼 엔진의 Data Table을 사용해서 아이템 정보와 확률을 외부 데이터(CSV, JSON 파일 등)로 관리해서 

엔진 안으로 임포트 하여 코드나 블루프린트에서 쉽게 사용 가능하다.

이렇게 외부 데이터를 사용하면 기획자도 쉽게 직접 수정이 가능하다.

 

상단 툴바 > Tools 클릭 > New C++ Class 클릭 > None 클릭 > 이름을 ItemSpawnRow로 지정하여 생성

// ItemSpawnRow.h

#pragma once

#include "CoreMinimal.h"
// Data Table에서 Row로 사용하기 위한 FTableRowBase 정의
#include "Engine/DataTable.h"
#include "ItemSpawnRow.generated.h"

// DataTable에서 사용할 Row 구조체이며, 
// FTableRowBase를 상속해야 DataTable로 사용 가능
USTRUCT(BlueprintType)
struct FItemSpawnRow : public FTableRowBase
{
    GENERATED_BODY()

public:
    // 아이템 이름
    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    FName ItemName;

    // 하드 레퍼런스 : TSubclassOf
    // - 클래스가 메모리에 바로 로드(빠르게 사용 가능, 소규모 프로젝트)
    // 소프트 레퍼런스 : TSoftClassPtr
    // - 경로만 저장하고 필요할 때 로드(메모리 절약, 대규모 프로젝트)

    // 스폰할 아이템 클래스(AActor를 상속한 클래스만 가능)
    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    TSubclassOf<AActor> ItemClass;

    // 스폰될 아이템 확률
    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    float Spawnchance;
};

아이템 확률 적용(CSV, JSON 임포트 및 언리얼 에디터에서 직접 수정)

CSV 파일로 임포트

엑셀 파일 실행 후 데이터 입력 > 이름 ItemSpawnTable로 지정 > CSV파일로 저장 > 언리얼 에디터로 이동 > 

  ItemName ItemClass Spawnchance
SmallCoinItem SmallCoinItem BP클래스 우클릭 > Copy Reference > 붙여넣기 > 30
BigCoinItem BigCoinItem 맨 뒤에 _C 붙여주기(아래 예시 참고) 10
MineItem MineItem /Script/Engine.Blueprint'/Game/Blueprints/BP_MineItem.BP_MineItem_C' 40
HealingItem HealingItem 위 예시처럼 카피 레퍼런스로 가져와서 뒤에 _C 꼭 붙여주기  20

데이터 테이블을 생성할 폴더로 이동 > Import to /Game/Blueprints...(경로) 클릭 > ItemSpawnTable.csv 열기 > 

아래 사진처럼 설정 후 Apply 클릭

언리얼 에디터에서 직접 수정

Blueprints 폴더로 들어가서 빈 공간 우클릭 > Miscellaneous > Data Table 검색 > 이름 ItemSpawnTable 지정후 생성 > 

팝업창에 Row Structure를 FItemSpawnRow 선택 후 생성 > 생성된 데이터 테이블 더블클릭 > 

상단 바에 +Add 클릭해서 직접 수정 가능(Item Class는 선택 해줘야함)

SpawnVolume에 확률 적용하기

// SpawnVolume.h

#pragma once

#include "CoreMinimal.h"
// 아이템 데이터 테이블 구조체 인클루드
#include "ItemSpawnRow.h"
#include "GameFramework/Actor.h"
#include "SpawnVolume.generated.h"

class UBoxComponent;

UCLASS()
class SPARTA_API ASpawnVolume : public AActor
{
    GENERATED_BODY()
	
public:	
    ASpawnVolume();

    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Spawning")
    USceneComponent* SceneComp;
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Spawning")
    UBoxComponent* SpawningBox;

    // 아이템 데이터 테이블 
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Spawning")
    UDataTable* ItemDataTable;

    // 데이터 테이블의 확률 기반으로 랜덤 아이템을 스폰하는 함수
    UFUNCTION(BlueprintCallable, Category = "Spawning")
    void SpawnRandomItem();

    // 데이터 테이블에서 확률 계산을 통해 하나의 아이템 Row(줄)를 선택하는 함수
    // 누적 확률 방식으로 랜덤 선택
    FItemSpawnRow* GetRandomItem() const;

    void SpawnItem(TSubclassOf<AActor> ItemClass);
    FVector GetRandomPointInVolume() const;
};
// SpawnVolume.cpp

#include "SpawnVolume.h"
#include "Components/BoxComponent.h"

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

    SceneComp = CreateDefaultSubobject<USceneComponent>(TEXT("SceneComponent"));
    SetRootComponent(SceneComp);

    SpawningBox = CreateDefaultSubobject<UBoxComponent>(TEXT("Spawning Box"));
    SpawningBox->SetupAttachment(SceneComp);

    // 초기값 nullptr
    ItemDataTable = nullptr;
}

void ASpawnVolume::SpawnRandomItem()
{
    // 데이터 테이블에서 확률 계산 후 아이템 선택
    // 선택한 아이템 SelectedRow에 저장
    if (FItemSpawnRow* SelectedRow = GetRandomItem())
    {
        // 선택된 아이템을 실제로 꺼냄
        // TusbclassOf -> UClass로 변환
        if (UClass* ActualClass = SelectedRow->ItemClass.Get())
        {
            // 선택된 아이템을 실제로 월드에 스폰
            SpawnItem(ActualClass);
        }
    }
}

FItemSpawnRow* ASpawnVolume::GetRandomItem() const
{
    // DataTable이 있어야 실행(없으면 바로 종료)
    if (!ItemDataTable) return nullptr;

    // 모든 아이템을 담을 배열 생성
    TArray<FItemSpawnRow*> AllRows;

    static const FString ContextString(TEXT("ItemSpawnContext"));

    // 데이터 테이블에 있는 모든 아이템 가져오기
    ItemDataTable->GetAllRows(ContextString, AllRows);

    // 데이터가 없으면 종료
    if (AllRows.IsEmpty()) return nullptr;

    // 전체 확률 합을 저장하는 변수
    float TotalChance = 0.0f;

    // 아이템 반복문 시작
    for (const FItemSpawnRow* Row : AllRows)
    {
        // 아이템 목록이 유효한지 확인
        if (Row)
        {
            // 아이템들의 확률을 더해서 총합 계산
            TotalChance += Row->Spawnchance;
        }
    }

    // 0 ~ 전체 확률 사이 랜덤값 생성
    const float RandValue = FMath::FRandRange(0.0f, TotalChance);

    // 누적 확률 계산용 변수
    float AccumulateChance = 0.0f;

    // 어떤 아이템을 선택할지 반복문 시작
    for (FItemSpawnRow* Row : AllRows)
    {
        // 누적된 확률 값 생성
        AccumulateChance += Row->Spawnchance;

        // 랜덤 값이 현재 누적 확률 구간에 포함되면 해당 아이템 선택
        if (RandValue <= AccumulateChance)
        {
            // 선택된 아이템을 반환
            return Row;
        }
    }

    return nullptr;
}

FVector ASpawnVolume::GetRandomPointInVolume() const
{
    FVector BoxExtent = SpawningBox->GetScaledBoxExtent();
    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)
    );
}


void ASpawnVolume::SpawnItem(TSubclassOf<AActor> ItemClass)
{
    if (!ItemClass) return;

    GetWorld()->SpawnActor<AActor>(
        ItemClass,
        GetRandomPointInVolume(),
        FRotator::ZeroRotator
    );
}

코드 작성 후 엑셀파일 열어서 데이터 입력후 csv 파일 3개 생성(레벨별 데이터 관리) > 

에디터로 돌아와서 Content - DataTables 폴더 생성 > 해당 폴더에 생성한 csv 파일 세개 임포트 > 

각 레벨(총 3개)별로 BP_SpawnVolume 설치 후 클릭 > 우측중단 Details 탭 > item 검색 > Spawning > Item Data Table 적용

엑셀 파일에서 데이터 확률이나 추가 등 수정한건 DataTable 더블클릭 > 좌측상단 툴바에 Reimort 클릭하면 반영

3. 이벤트 그래프 로직 설정

BP_SpawnVolume 더블클릭 > 이벤트 그래프에서

아래처럼 하면 게임 시작시 3초마다 아이템이 1개씩 생성

아래처럼 하면 게임 시작시 10개 생성

 

관련글 더보기