레벨 셋팅하기
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등에서 활용
- Copy Reference는 엔진 런타임에서 사용되는 클래스/오브젝트 경로
- 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️⃣레벨마다 다른 확률 적용하기
- 레벨별로 서로 다른 DataTable 에셋을 사용
- CSV에 각각 다른 확률을 넣고 각각에 볼륨에 알맞은 데이터 테이블을 넣는다
- 관리가 직관적이며 간단하다
- 단일 DataTable에 레벨 구분 컬럼 추가
- CSV에 Level Index(또는 Level Name)를 넣고 같은 컬럼을 하나 두고 각각에 맞는 레벨에 해당하는지 적음
- 코드 수정하여 현재 레벨이 몇 레벨인지 확이 후 필터링된 Row만 확률 계산
- 한 DataTable에 모든 레벨 정보를 한번에 관리할 수 있지만 코드가 복잡해짐
'Unreal Bootcamp > Unreal C++' 카테고리의 다른 글
16.게임 루프 설계를 통한 게임 흐름 제어 (0) | 2025.02.11 |
---|---|
15.캐릭터 체력 및 점수 관리 시스템 (0) | 2025.02.10 |
13.충돌 이벤트로 획득되는 아이템 구현 (0) | 2025.02.05 |
12.인터페이스 기반 아이템 클래스 (0) | 2025.02.04 |
11.State Machine 설계를 통한 캐릭터 동작 애니메이션 적용 (0) | 2025.01.31 |