C++과 Unreal Engine으로 3D 게임 개발 8

김여울·2025년 7월 16일

내일배움캠프

목록 보기
44/139

📍 3주차 3강

7. 아이템 랜덤 스폰 및 레벨 별 각 아이템 확률 설정

각 아이템마다 확률을 설정해서 맵의 난이도가 올라갈 때마다 아이템 수를 늘린다거나 해서 난이도 조정 가능

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

a. 레벨 세팅

레벨 크기 : BasicLevel > IntermediateLevel > Advanced Level

b. 콜리전 컴포넌트로 스폰 영역 지정

Actor에 Box Collision Component 추가해서 레벨 크기에 맞춰 배치 → 스폰 영역

콜리전 컴포넌트의 장점

  • 레벨 디자이너들이 직관적 제어 가능
  • 사각형이라 위치 계산하기 쉬움움
  • 충돌, 트리거까지 계산 가능한 컴포넌트
    → 캐릭터가 콜리전 컴포넌트 안으로 들어오면 몬스터 소환 등 구현 가능
  • 가벼움 → 최적화

c. 만들기

1️⃣ Actor 상속 받은 C++ 클래스 생성

SpawnActor.h

#pragma once

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

// Component 미리 선언
class UBoxComponent;

UCLASS()
class SPARTAPROJECT_API ASpawnVolume : public AActor
{
	GENERATED_BODY()

public:
	ASpawnVolume();

	// Component 만들기
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Spawning")
	USceneComponent* Scene;
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Spawning")
	UBoxComponent* SpawningBox;

	// SpawnVolume 안에서 무작위 좌표 얻기
	UFUNCTION(BlueprintCallable, Category = "Spawning")
	FVector GetRandomPointInVolume() const;	

	// 지정된 class의 아이템을 스폰하는 함수
	UFUNCTION(BlueprintCallable, Category = "Spawning")
	void SpawnItem(TSubclassOf<AActor> ItemClass);	// 이 액터의 하위클래스가 아니면 무조건 오류남
};

SpawnActor.cpp

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

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

	// component 초기화
	Scene = CreateDefaultSubobject<USceneComponent>(TEXT("Scene"));
	SetRootComponent(Scene);

	SpawningBox = CreateDefaultSubobject<UBoxComponent>(TEXT("Scene"));
	SpawningBox->SetupAttachment(Scene);
}

FVector ASpawnVolume::GetRandomPointInVolume() const
{
	// 랜덤 위치 알기 위해 컴포넌트 크기 계산하기
	// size (200, 100, 50) scale (2, 1, 1) -> (400, 10, 10) 이렇게 적용된 반지름 값을 반환함
	// Extent : 중심 ~ 끝 거리
	FVector BoxExtent = SpawningBox->GetScaledBoxExtent();	// BoxComponent 크기 갖고 오기

	// 중심 좌표 = 컴포넌트가 위치한 값
	FVector BoxOrigin = SpawningBox->GetComponentLocation();

	// 어떤 랜덤한 위치(X, Y, Z축)에 소환됨
	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;

	// Actor의 하위클래스까지 적용 가능
	GetWorld()->SpawnActor<AActor>(
		ItemClass,
		GetRandomPointInVolume(),	// 위치 (랜덤 값 갖고 옴)
		FRotator::ZeroRotator	// 회전은 안하게
	);
}

2️⃣ 블루프린트 클래스로 상속

BP_SpawVolume

3️⃣ 레벨과 볼륨 크기 맞추기

  • 안 맞추면 아이템이 레벨 밖으로 생성될 수도 있음
  • 레벨 3개 모두 스폰 볼륨 맞추기


d. 아이템 랜덤 스폰 테스트

▶ 플레이 하면 아이템이 랜덤 위치에 생성됨

💥 Spawn Item 노드 속 Item Class 블루프린트 없음
해당 클래스의 헤더 파일에서 부모클래스가 Actor인지 확인

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h" // ← 반드시 이걸 상속해야 함
#include "BigCoinItem.generated.h"

UCLASS()
class SPARTAPROJECT_API ABigCoinItem : public AActor // ← 이거 꼭 확인!
{
    GENERATED_BODY()
};

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

a. Item Data 구조체 생성

아이템 확률 설정 - Data Table 활용

  • 엑셀(.csv) 또는 JSON 파일로 아이템 데이터 관리
  • 언리얼에서 DataTable로 임포트해서 사용
  • 코드/블루프린트 둘 다 쉽게 접근 가능

TSubclassOf vs TSoftClassPtr

  • 클래스 타입을 가리키는 데이터 구조
  • 포인터가 아니라 클래스 자체를 참조하는 메타데이터
  • 소프트 레퍼런스는 메모리 아끼기 위한 선택
  • 클래스 많은 게임일수록 SoftClassPtr이 유리함
항목TSubclassOfTSoftClassPtr
참조 방식하드 레퍼런스 (직접 참조)소프트 레퍼런스 (경로만 저장)
메모리 관리항상 메모리에 있음필요할 때만 로드 (지연 로딩)
사용 시점바로 사용 가능LoadSynchronous 등 별도 처리 필요
상황 예시스폰할 클래스가 몇 개 안 될 때클래스 종류가 수십~수백 개일 때
// 하드 레퍼런스
UPROPERTY(EditAnywhere)
TSubclassOf<AWeapon> WeaponClass;

// 소프트 레퍼런스
UPROPERTY(EditAnywhere)
TSoftClassPtr<AWeapon> WeaponClassSoft;

b. 만들기

1️⃣ 행(Row) -> 구조체를 만들기

구조체는 class가 아니기 때문에 None으로 C++ 클래스 만들기

ItemSpawnRow.h

#pragma once

#include "CoreMinimal.h"
#include "ItemSpawnRow.generated.h"

// 구조체 선언
USTRUCT(BlueprintType)   // 블루프린트에서 사용 가능 (이 구조체를 변수로 만듦)
struct FItemSpawnRow : public FTableRowBase   // 데이터 테이블의 행으로 얘 사용
{
    GENERATED_BODY()

public:
    // 행 하나에 세 가지 정보
    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    FName ItemName;   // 이름은 가볍게 FName
    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    TSubclassOf<AActor> ItemClass;   // 아이템 클래스 가져오기
    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    float Spawnchance;   // 아이템 확률
};

ItemSpawnRow.cpp

#include "ItemSpawnRow.h"
// 구조체니까 비워두기

2️⃣ DataTable 만들기 (ItemSpawnTable)


📌
확률은 아이템 다 합해서 100이어야 함
Row Name은 중복 불가능

7.3 데이터 테이블을 사용해 스폰 로직 구현

SpawnVolume.h

#pragma once

#include "CoreMinimal.h"
#include "ItemSpawnRow.h"
#include "GameFramework/Actor.h"
#include "SpawnVolume.generated.h"

// Component 미리 선언
class UBoxComponent;

UCLASS()
class SPARTAPROJECT_API ASpawnVolume : public AActor
{
	GENERATED_BODY()

public:
	ASpawnVolume();

	// Component 만들기
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Spawning")
	USceneComponent* Scene;
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Spawning")
	UBoxComponent* SpawningBox;
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Spawning")	// 객체 바꿔야하니까 EditAnywhere
	UDataTable* ItemDataTable;

	// 테스트 할 수 있으니까 에디터에 노출
	// 랜덤 아이템을 스폰시키는 함수
	UFUNCTION(BlueprintCallable, Category = "Spawning")
	void SpawnRandomItem();

	// 에디터에 굳이 노출 안 해도 됨
	// SpawnVolume 안에서 랜덤 좌표 얻는 함수
	FVector GetRandomPointInVolume() const;	
	// 지정된 class의 아이템을 스폰하는 함수
	// 아이템 랜덤으로 갖고 오는 함수
	FItemSpawnRow* GetRandomItem() const;
	void SpawnItem(TSubclassOf<AActor> ItemClass);	// 이 액터의 하위클래스가 아니면 무조건 오류남

};

SpawnVolume.cpp

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

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

	// component 초기화
	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);
		}
	}
}

// 랜덤한 행 갖고 오는 함수
FItemSpawnRow* ASpawnVolume::GetRandomItem() const
{	
	// 데이터 테이블이 유효한지 확인
	if (!ItemDataTable) return nullptr;	// 유효하지 않으면 nullptr 반환, 이 행을 반환
	// 테이블에서 모든 행 가져오기 (배열 형태로)
	// 이 모든 FItemSpawnRow의 포인터를 담을 배열 생성
	TArray<FItemSpawnRow*> AllRows;
	static const FString ContextString(TEXT("ItemSpawnContext"));	// 데이터 테이블에서 디버깅 역할
	ItemDataTable->GetAllRows(ContextString, AllRows);	// 모든 행을 AllRows에 저장

	// 비어있는 상황이면 nullptr 리턴하자
	if (AllRows.IsEmpty()) return nullptr;

	// 전체 확률의 합 구하기
	float TotalChance = 0.0f;	// 합 초기화
	for (const FItemSpawnRow* Row : AllRows)
	{
		if (Row)
		{
			TotalChance += Row->SpawnChance;	// 값 누적
		}
	}
	
	// 누적 확률 방식의 랜덤 뽑기
	// 0 ~ 총합 사이 랜덤값 뽑기 -> FMath::FRandRange 사용
	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
{
	// 랜덤 위치 알기 위해 컴포넌트 크기 계산하기
	// size (200, 100, 50) scale (2, 1, 1) -> (400, 10, 10) 이렇게 적용된 반지름 값을 반환함
	// Extent : 중심 ~ 끝 거리
	FVector BoxExtent = SpawningBox->GetScaledBoxExtent();	// BoxComponent 크기 갖고 오기

	// 중심 좌표 = 컴포넌트가 위치한 값
	FVector BoxOrigin = SpawningBox->GetComponentLocation();

	// 어떤 랜덤한 위치(X, Y, Z축)에 소환됨
	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;

	// Actor의 하위클래스까지 적용 가능
	GetWorld()->SpawnActor<AActor>(
		ItemClass,
		GetRandomPointInVolume(),	// 위치 (랜덤 값 갖고 옴)
		FRotator::ZeroRotator	// 회전은 안하게
	);
}

7.4 DataTable을 언리얼 에디터에 적용하기


에디터에서 만들어둔 데이터 테이블 적용하기

- test

스폰 볼륨은 땅에 파묻힐 수 있으니까 볼륨을 공중에 띄우는 게 좋음

7.5 레벨별로 다른 확률 데이터 적용하기

a. 데이터 테이블 각각 생성하기

BasicItemSpawnTable, IntermediateItemSpawnTable, AdvancedItemSpawnTable

아이템BasicIntermediateAdvanced
SmallCoinItem40%30%20%
BigCoinItem30%25%20%
MineItem10%25%40%
HealingItem20%20%10%

b. 레벨별로 맞는 데이터 테이블 적용하기


VS에서 실행할 때 언리얼 에디어 안 열림

.upproject 수동 실행은 되는데 비주얼 스튜디오에서 실행하니까 71%에서 멈춤

1️⃣ 디버깅 대상 확인 - “SpartaProject 디버그 속성”

항목입력 값
구성Development_Editor
명령C:\Program Files\Epic Games\UE_5.5\Engine\Binaries\Win64\UnrealEditor.exe
명령 인수"C:\Unreal Projects\SpartaProject\SpartaProject.uproject" -skipcompile
작업 디렉터리$(ProjectDir)

2️⃣ DerivedDataCache Intermediate / Binaries / .vs 삭제

3️⃣ sln 파일 다시 생성

4️⃣ 리빌드


부모클래스 변경하기

이미 만들어진 블루프린트도 부모 클래스 변경 (Reparent Blueprint)

1️⃣ 블루프린트 열기 (예: BP_BigCoinItem)

2️⃣ File → Reparent Blueprint 클릭

3️⃣ 팝업창에서 원하는 부모 클래스 검색

4️⃣ 저장 후 다시 열기

0개의 댓글