카테고리 없음

Unreal C++ TIL — 지뢰 피하기 게임 3-3 (DataTable과 랜덤 아이템 스폰 시스템 구현)

view29579 2026. 8. 21. 23:20

1. 오늘 학습한 내용

오늘은 기존에 구현했던 아이템 시스템에서 한 단계 더 나아가 DataTable을 이용한 아이템 데이터 관리와 랜덤 스폰 시스템을 구현해보았다.

이번 작업에서는 크게 다음 내용을 다뤘다.

  • FTableRowBase를 상속받은 아이템 데이터 구조체 생성
  • USTRUCT(BlueprintType)를 이용한 구조체의 언리얼 엔진 및 블루프린트 인식
  • TSubclassOf<AActor>를 이용한 아이템 클래스 지정
  • UBoxComponent를 이용한 스폰 영역 설정
  • 스폰 영역 내부의 랜덤 좌표 계산
  • SpawnActor()를 이용한 액터 생성
  • DataTable을 이용한 아이템 데이터 관리

2. ItemSpawnRow 구조체

먼저 DataTable의 한 행에 들어갈 데이터를 정의하기 위해 FItemSpawnRow 구조체를 만들었다.

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를 상속받음으로써 이 구조체를 DataTable의 Row 데이터로 사용할 수 있게 된다.

한 행에는 다음과 같은 정보가 들어간다.

ItemName
ItemClass
SpawnChance

즉, 아이템 하나에 대한 데이터를 하나의 행으로 관리할 수 있다.

예를 들어 DataTable에서 다음과 같이 관리할 수 있다.

Coin    → BP_Coin    → 50
Potion  → BP_Potion  → 30
Mine    → BP_Mine    → 20

이렇게 하면 아이템의 종류나 스폰 확률 같은 데이터를 코드에 직접 하나하나 작성하지 않고 DataTable에서 관리할 수 있다는 점을 배웠다.


3. TSubclassOf에 대한 이해

이번에 TSubclassOf<AActor>도 다시 사용해보았다.

TSubclassOf<AActor> ItemClass;

이것은 아무 객체나 넣는 것이 아니라 AActor를 상속받은 클래스만 지정할 수 있도록 제한하는 역할을 한다.

따라서 DataTable에서 아이템의 실제 클래스를 선택할 수 있고, 이후 SpawnActor()에 전달하여 실제 액터를 생성할 수 있다.

또한 TSubclassOf와 TSoftClassPtr의 차이도 함께 학습했다.

  • TSubclassOf → 사용할 클래스를 직접 참조
  • TSoftClassPtr → 클래스의 경로를 이용하는 소프트 참조

이번 시스템에서는 실제로 스폰할 아이템 클래스를 지정해야 하기 때문에 TSubclassOf를 사용했다.


4. SpawnVolume 구현

아이템이 생성될 영역을 만들기 위해 ASpawnVolume을 구현했다.

USceneComponent* Scene;
UBoxComponent* SpawningBox;

Scene을 루트 컴포넌트로 만들고 SpawningBox를 그 하위에 붙였다.

Scene
└── SpawningBox

이렇게 함으로써 Box 영역을 기준으로 아이템을 스폰할 수 있도록 만들었다.


5. 스폰 영역 안에서 랜덤 좌표 만들기

가장 재미있었던 부분 중 하나는 스폰 영역 안에서 랜덤한 위치를 계산하는 것이었다.

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

GetScaledBoxExtent()를 이용하여 Box의 현재 스케일이 적용된 크기를 얻고, GetComponentLocation()을 통해 Box의 중심 위치를 가져온다.

그 다음 각 축마다

-Extent ~ +Extent

범위에서 랜덤한 값을 생성하여 중심 위치에 더한다.

결과적으로 Box 내부의 임의의 위치가 만들어진다.

        Box
┌─────────────────┐
│      •          │
│   •        •    │
│        •        │
│ •            •  │
└─────────────────┘

이 과정을 통해 아이템이 매번 같은 위치가 아니라 스폰 영역 내부의 랜덤한 위치에 생성될 수 있게 된다.


6. SpawnActor를 이용한 실제 아이템 생성

랜덤 위치를 구한 뒤에는 SpawnActor()를 이용해 실제 아이템을 생성했다.

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

여기서 각각의 인자를 구분해서 이해했다.

ItemClass
→ 무엇을 생성할 것인가?

GetRandomPointInVolume()
→ 어디에 생성할 것인가?

FRotator::ZeroRotator
→ 어떤 회전으로 생성할 것인가?

즉,

아이템의 종류 + 랜덤 위치 + 기본 회전값

을 조합해서 실제 월드에 아이템을 생성한다.


7. 오늘 느낀 점

오늘은 특별히 큰 트러블슈팅이 발생하지는 않았다.

대신 오늘 작업을 하면서 이전과는 조금 다른 느낌을 받았다.

지금까지는 주로 캐릭터를 만들고 움직이게 하거나, 컴포넌트를 붙이고 애니메이션을 적용하는 등 게임의 기능을 직접 구현하는 과정에 집중했다.

그런데 이번에 DataTable과 랜덤 스폰 시스템을 만들어보면서 처음으로 "게임이 실제로 데이터를 이용해서 돌아간다"​는 느낌이 강하게 들었다.

특히 DataTable에 아이템의 이름, 클래스, 스폰 확률을 넣고 그것을 코드에서 가져와 실제 월드에 아이템으로 생성하는 과정을 보면서 게임의 시스템이 조금씩 연결되고 있다는 느낌을 받았다.

그리고 DataTable은 게임의 기획자가 데이터를 관리할 때도 활용할 수 있다는 점이 흥미로웠다.

코드를 직접 수정하지 않고 DataTable의 값을 변경하는 것만으로 아이템의 종류나 스폰 확률 등을 조정할 수 있기 때문에, 프로그래머와 기획자가 역할을 나누어 작업하는 게임 개발 과정도 조금은 이해할 수 있었다.

무엇보다 지금까지의 실습은 "기능을 하나 구현했다"는 느낌이 강했다면, 오늘은 "게임을 구성하는 시스템 하나를 만들었다"​는 느낌이 들어서 재미있었다.

앞으로 아이템 종류가 늘어나고 스폰 규칙이나 확률까지 연결된다면 DataTable을 활용하는 이유를 더 확실하게 이해할 수 있을 것 같다.