레벨 셋팅하기

1️⃣레벨 셋팅

  • Resources 폴더에 Map 폴더에 3가지 레벨이 이미 존재하는데 이는 난이도에 따라 크기가 다른 맵으로 이 맵을 우리가 만든 Map폴더로 옮겨와주고 Default Level을 BasicLevel로 지정한다

2️⃣콜리전 컴포넌트로 스폰 영역 지정

  • 랜덤으로 액터를 Spawn시킬때 특정 범위안에서 이뤄져야 하기 때문에 수학적 알고리즘등을 이용해야 한다
  • 하지만 지금 만드는 간단한 게임에서는 액터의 Collision을 이용해서 작업해도 되기 때문에 이를 이용할 것이다
  • Actor Class를 상속받아 새로운 클래스를 만들어 이를 사용할 것이다
#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);
};
  • UBoxComponent
    • 이 컴포넌트가 박스 형태의 콜리전 영역을 나타낼 것이다
    • 언리얼 엔진의 UBoxCollision는 박스 내부에서 오버랩이나 충돌을 감지할 수 있는 컴포넌트이다
    • 실제로 보이는 3D메시는 아니며 박스 형태의 충돌 범위만 제공한다
  • GetRandomPointInVolum()
    • SpawninBox 범위 내부에서 랜덤 좌표를 리턴한다
    • 이 좌표를 통해 아이탬을 생성하면 Collision안에 임의의 위치에 아이템이 뜨는 효과를 나타낼 수 있다
  • SpawnItem()
    • 파라미터로 받은 아이템 클래스를 SpawnVolume 내부의 랜덤 위치에 생성하는 함수
  • TSubclassOf<T>
    • 하드 래퍼런스 : 클래스가 항상 메모리에 로드된 상태에서 바로 접근
#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
    );
}
  • GetScaleBoxExternt()
    • 박스 컴포넌트의 실제 가로,세로,높이의 절반 길이를 반환함
    • 에디터에서 Scale을 조정하면 여기에도 반영됨
  • FRandRange(a,b)
    • a~b사이의 임의의 float값을 리턴함
    • 여기서는 x,y,z좌표마다 랜덤 값을 생성하여 BoxOrigin에 더해줌

3️⃣아이템 랜덤 스폰 테스트

  • 빌드 후 에디터로 돌아와 SpawnVolume을 상속받은 BP클래스를만든 후 레벨에 배치하여 준다
  • EventBeginPlay에 SpawnItem을 연결하고 아무 아이탬 하나를 Item Class에 넣어준다
  • 시작을 해보면 박스 내부에 랜덤한 위치에 해당 아이템이 하나 생성되는 것을 볼 수 있다
  • 지속적으로 스폰하고 싶으면 Tick함수에 연결하거나, 타이머를 사용해 일정 간격으로 SpawnItem함스를 호출해주면 된다
  • 이렇게 3개의 레벨에 모두 BP클래스를 배치해준다

아이템 스폰 확률 데이터 테이블 만들기

1️⃣Item Data 구조체 만들기

  • 어떤 아이템이 몇프로 확률로 스폰되는지 코드로 직접 하드코딩하면, 매번 수정할 때 마다 빌드를 해야하기 때문에 번거롭다
  • 언리얼 엔진에서는 데이터 테이블을 사용하면 블루프린트에서 쉽게 관리할 수 있다
  • 우선, 데이터 테이블의 각 행(Row)을 C++ 구조체에 매핑하여야 하므로 엔진에 FTableRowBase라는 기본 구조체를 제공하며, 이를 상속한 구조체를 만들면, 각 CSV(또는 JSON)행을 FItemSpawnRow 구조체에 정의해준 형태로 받을 수 있다
#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;
};
  • FTableRowBase
    • 엔진에서 이 구조체는 데이터 테이블로 사용할 수 있게 해주는 베이스 구조체
  • TSoftClassPtr<T>
    • 소프트 래퍼런스
      • 클래스의 경로만 유지하여 해당 클래스가 필요한 상황에 로드함 
    • 소프트 레퍼런스로, 클래스를 바로 로드하지 않고도 경로만 기억해둘 수 있다
    • 필요할 때 Get을 통해 UClass*를 얻어 인스턴스를 생성할 수 있다
    • TSubclassOf말고 이를 권장

2️⃣CSV 파일 작성 및 임포트

  • 엑셀을 열고 2열부터 Column 이름을 작성합니다.
  • 1열은 “RowName”으로 인식됩니다. Key 값으로 사용할 값을 입력해줘야 합니다.
  • 2열부터는 각 아이템 (데이터) 정보를 한 줄씩 기록합니다.
  • 경로는 각 BP 클래스 우클릭 → Copy Reference 로 정확히 가져옵니다. 그리고 블루프린트의 경우 뒤에 "_C"가 붙어야 BP 클래스로 인식되니까 마지막에 반드시 붙여주세요.

    • Copy Reference는 엔진 런타임에서 사용되는 클래스/오브젝트 경로
      • 블프/C++로 자산 로드시 ConstructorHelpers::FClassFinder등에서 활용
  • CSV로 저장
    • 언리얼 콘텐츠 폴더가 아니라 본인이 편한 로컬 위치에 CSV UTF-8로 저장한다
    • 언리얼 에디터에서 우클릭 후 Import to/Game/Bluprint를 선택, 아래와 같은 옵션으로 설정 후 Apply해준다
  •  완성된 DataTable을 볼 수 있다

3️⃣에디터에서 직접 데이터 테이블 수정

 


Spawn 확률 적용

1️⃣데이터 테이블을 사용해 스폰 로직 구현

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);
};

.cpp

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
    );
}
  • SpawnRandomItem()
    • 데이터 테이블에서 확률 계산으로 Row 하나 선택 + 스폰 을 분리해놓아 코드가 깔끔해짐
  • GetRandomItem()
    • 테이블의 모든 데이터를 가져와 확률을 구하고 확률에 따라 한개를 뽑아 리턴함
  • Algo::Accumulate
    • 언리얼 엔진에서 추가한 C++ 템플릿 함수로, STL의 std::accumulate와유사하게 작동
    • 각 Row의 SpawnChance를 계속 더해 초우 합을 구함
  • 빌드 후, BP_SpawnVolume로 돌아가 속성에서 ItemDataTable에 기존에 만들어 둔 데이터 테이블을 할당해준다
  • 이벤트 그래프 SpawnRandomItem()함수를 스크린 샷 처렴 10번 호출하도록 한다
  • 이후 플래이 해보면 확률에 따라 10개의 아이템이 스폰되는 것을 볼 수 있다

2️⃣레벨마다 다른 확률 적용하기

  1. 레벨별로 서로 다른 DataTable 에셋을 사용
    • CSV에 각각 다른 확률을 넣고 각각에 볼륨에 알맞은 데이터 테이블을 넣는다
    • 관리가 직관적이며 간단하다
  2. 단일 DataTable에 레벨 구분 컬럼 추가
    • CSV에 Level Index(또는 Level Name)를 넣고 같은 컬럼을 하나 두고 각각에 맞는 레벨에 해당하는지 적음
    • 코드 수정하여 현재 레벨이 몇 레벨인지 확이 후 필터링된 Row만 확률 계산
    • 한 DataTable에 모든 레벨 정보를 한번에 관리할 수 있지만 코드가 복잡해짐