📍 3주차 3강
각 아이템마다 확률을 설정해서 맵의 난이도가 올라갈 때마다 아이템 수를 늘린다거나 해서 난이도 조정 가능
레벨 크기 : BasicLevel > IntermediateLevel > Advanced Level
Actor에 Box Collision Component 추가해서 레벨 크기에 맞춰 배치 → 스폰 영역

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 // 회전은 안하게
);
}
BP_SpawVolume


▶ 플레이 하면 아이템이 랜덤 위치에 생성됨
💥 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()
};
| 항목 | TSubclassOf | TSoftClassPtr |
|---|---|---|
| 참조 방식 | 하드 레퍼런스 (직접 참조) | 소프트 레퍼런스 (경로만 저장) |
| 메모리 관리 | 항상 메모리에 있음 | 필요할 때만 로드 (지연 로딩) |
| 사용 시점 | 바로 사용 가능 | LoadSynchronous 등 별도 처리 필요 |
| 상황 예시 | 스폰할 클래스가 몇 개 안 될 때 | 클래스 종류가 수십~수백 개일 때 |
// 하드 레퍼런스
UPROPERTY(EditAnywhere)
TSubclassOf<AWeapon> WeaponClass;
// 소프트 레퍼런스
UPROPERTY(EditAnywhere)
TSoftClassPtr<AWeapon> WeaponClassSoft;
구조체는 class가 아니기 때문에 None으로 C++ 클래스 만들기
#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; // 아이템 확률
};
#include "ItemSpawnRow.h"
// 구조체니까 비워두기



📌
확률은 아이템 다 합해서 100이어야 함
Row Name은 중복 불가능
#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); // 이 액터의 하위클래스가 아니면 무조건 오류남
};
#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 // 회전은 안하게
);
}

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


스폰 볼륨은 땅에 파묻힐 수 있으니까 볼륨을 공중에 띄우는 게 좋음
BasicItemSpawnTable, IntermediateItemSpawnTable, AdvancedItemSpawnTable
| 아이템 | Basic | Intermediate | Advanced |
|---|---|---|---|
| SmallCoinItem | 40% | 30% | 20% |
| BigCoinItem | 30% | 25% | 20% |
| MineItem | 10% | 25% | 40% |
| HealingItem | 20% | 20% | 10% |

.upproject 수동 실행은 되는데 비주얼 스튜디오에서 실행하니까 71%에서 멈춤
| 항목 | 입력 값 |
|---|---|
| 구성 | Development_Editor |
| 명령 | C:\Program Files\Epic Games\UE_5.5\Engine\Binaries\Win64\UnrealEditor.exe |
| 명령 인수 | "C:\Unreal Projects\SpartaProject\SpartaProject.uproject" -skipcompile |
| 작업 디렉터리 | $(ProjectDir) |
이미 만들어진 블루프린트도 부모 클래스 변경 (Reparent Blueprint)