오늘은 Wave를 구현해서 코인을 먹고 높은 점수를 쌓는 간단한 게임을 만들어서 공부하려고 한다.

과제 제출도 할 겸..

 

먼저 어떤 걸 해야할 지 고민해보았다.

 

구현해야 할 목록들

- 캐릭터
- 캐릭터 애니메이션
- 아이템 스폰
- 게임 흐름 설계
- 웨이브 설계
- UI 제작

이렇게 크게 총 5개 정도로 나눠보았다.

 

캐릭터와 애니메이션은 저번 포스팅에서 공부한 내용을 그대로 가져갈 것이다.

오늘은 아이템 스폰에 대해 자세하게 공부해보려고 한다.

 

아이템 목록

- 코인
- 피격 아이템
- 회복 아이템

일단 이렇게 3개의 종류의 아이템을 설정했다.

그리고 공부하면서 만들어놨던 ItemInterface와 BaseItem 클래스 등을 가져와서 사용할 것이다.
(링크 참고)

 

아이템의 기반이 완성되었으니, 이제 아이템을 맵에서 랜덤으로 생성하는 부분을 구현하려고 한다.

맵에서 먼저 얼마큼의 영역에서 아이템이 스폰될 지 설정해준다.

그리고 지정된 영역에서 랜덤으로 아이템을 반환하는 클래스를 작성해주었다.

#pragma once

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

class UBoxComponent;

UCLASS()
class PRACTICE_API ASpawnVolume : public AActor
{
	GENERATED_BODY()
	
public:	
	ASpawnVolume();

	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Spawning")
	USceneComponent* Scene;
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Spawning")
	UBoxComponent* SpawningBox;
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Spawning")
	UDataTable* ItemDataTable;

	UFUNCTION(BlueprintCallable, Category = "Spawning")
	AActor* SpawnRandomItem();

	FItemSpawnRow* GetRandomItem() const;
	AActor* SpawnItem(TSubclassOf<AActor> ItemClass);  // TSubclassOf<Type> - Type의 하위 클래스가 아니면 오류가 남
	FVector GetRandomPointInVolume() const;
};
#include "SpawnVolume.h"
#include "Components/BoxComponent.h"

// Sets default values
ASpawnVolume::ASpawnVolume()
{
	PrimaryActorTick.bCanEverTick = false;
	// 씬 컴포넌트
	Scene = CreateDefaultSubobject<USceneComponent>(TEXT("Scene"));
	SetRootComponent(Scene);
	// 박스 컴포넌트 생성
	SpawningBox = CreateDefaultSubobject<UBoxComponent>(TEXT("SpawningBox"));
	SpawningBox->SetupAttachment(Scene);

	ItemDataTable = nullptr;
}

FVector ASpawnVolume::GetRandomPointInVolume() const
{
	FVector BoxExtent = SpawningBox->GetScaledBoxExtent();  // GetScaledBoxExtent - Box의 딱 절반의 크기를 반환
	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)
	);
}

AActor* ASpawnVolume::SpawnRandomItem()
{
	if (FItemSpawnRow* SelectedRow = GetRandomItem())
	{
		if (UClass* ActualClass = SelectedRow->ItemClass.Get())  // TSoftClass로 받어오기 때문에 UClass 타입으로 받아옴
		{
			return SpawnItem(ActualClass);
		}
	}
	
	return nullptr;
}

FItemSpawnRow* ASpawnVolume::GetRandomItem() const
{
	if (!ItemDataTable) return nullptr;

	TArray<FItemSpawnRow*> AllRows;
	static const FString ContextString(TEXT("ItemSpawnContext"));  // DataTable의 디버깅용 로그
	ItemDataTable->GetAllRows(ContextString, AllRows);

	if (AllRows.IsEmpty()) return nullptr;

	float TotalChance = 0;
	for (const FItemSpawnRow* Row : AllRows)
	{
		if (Row)
		{
			TotalChance += Row->SpawnChance;
		}
	}

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

AActor* ASpawnVolume::SpawnItem(TSubclassOf<AActor> ItemClass)
{
	if (!ItemClass) return nullptr;

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

	return SpawnedActor;
}

 

 

마지막 말

처음에는 아이템을 랜덤으로 어떻게 생성할 지에 대해 막막했었는데, 하나씩 해보니까 조금 이해가 되는 것 같다.

그리고 각 아이템 별 확률을 설정할 수 있는데 이 부분을 코드로도 접근할 수 있는지 살펴보려고 한다.