TIL_042: 아이템 스폰/효과, Data Table, TSubclassOf / TSoftClassPtr, GameState/PlayerState, 언리얼 데미지 시스템

김펭귄·2025년 10월 2일

Today What I Learned (TIL)

목록 보기
42/91

오늘 학습 키워드

  • 랜덤 스폰

  • Data Table

  • TSubclassOf / TSoftClassPtr

  • PlayerState

  • BluePrintPure

  • 플레이어 체력 구현

  • 지뢰 피해 구현

  • 힐링아이템 효과 구현

1. 랜덤 스폰을 위한 Box Collision

  • 특정 범위 안에서 아이템을 랜덤으로 스폰하는 방법은 여러가지가 있지만, 간단하면서도 쉬운 방법으로 Box Collision을 이용함

  • Box Collision의 영역 = 스폰 영역

  • Collision 컴포넌트를 사용하는 이유

    1. 시각적으로 범위를 확인하기 쉬워, 레벨 디자이너가 설정하기 편리함
    2. Collision 특성 상 충돌, overlap, trigger 등의 기능을 이용하여 스폰 가능
      (캐릭터 입장 시 적 생성)
    3. 여러 기능을 제공하면서도 최적화되어 가벼운 컴포넌트

2. C++로 Spawn Volume 생성

헤더

// SpawnVolume.h
class UBoxComponent;

UCLASS()
class SPARTPROJECT_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 GetRandomPointVolume() const;	// 범위 내 랜덤 좌표 반환
	UFUNCTION(BlueprintCallable, Category = "Spawning")
	void SpawnItem(TSubclassOf<AActor> ItemClass);	// 아이템 생성
};
  • TSubclassOf<type> : type의 subclass가 아니면 오류가 남. 타입이 맞는지 런타임에도 확인하기에 안전하게 사용하기 위해 이용

cpp

// SpawnVolume.cpp
// 생성자에선 컴포넌트 초기화만 하였으므로 생략
FVector ASpawnVolume::GetRandomPointVolume() const
{
	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)
	);
}

void ASpawnVolume::SpawnItem(TSubclassOf<AActor> ItemClass)
{
	if (!ItemClass) return;
	GetWorld()->SpawnActor<AActor>(
		ItemClass,
		GetRandomPointVolume(),
		FRotator::ZeroRotator
	);
}
  • GetScaledBoxExtent() : 박스 컴포넌트의 스케일이 적용된(확대/축소) 크기(Extents)를 FVector로 반환. 실제 게임 내에서 Box각 면 길이의 절반
GetScaledBoxExtent : FVector(3, 2.5f, 3.5f)
  • SpawnActor<type> : type객체를 월드에 생성
    • ItemClass : 생성할 액터의 클래스 타입
    • GetRandomPointVolume() : 해당 위치에 생성
    • FRotator::ZeroRotator : 해당 FRotator만큼 회전시켜 생성

3. Data Table

  • 확률적으로 아이템을 소환할 때, 각 아이템의 확률을 하드코딩하여 사용하면, 확률을 수정할 때마다 빌드를 새로 해야하기에 비효율적

  • 그래서 Data Table을 생성하고 import하여 코드/블루프린트에서 사용

  • 게임에서 사용하는 숫자(확률, data)들을 Data Table로 사용하면, 기획자/디자이너들도 사용하기 편리

행 구조체 만들기

  • 각 행을 구조체로 만들어야함

  • 구조체는 클래스가 아니므로 None을 선택

헤더

// 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;
	UPROPERTY(EditAnywhere, BlueprintReadWrite)
	TSubclassOf<AActor> ItemClass;
	UPROPERTY(EditAnywhere, BlueprintReadWrite)
	float SpawnChance;
};
  • 블루프린트에서도 사용할 것이니까 리플렉션에 등록해야 함

    1. #include "ItemSpawnRow.generated.h"
    2. USTRUCT
    3. GENERATED_BODY()
    4. 블루프린트에서 변수로 사용할 수 있으므로 BlueprintType 선언
  • 구조체이니까 이름 앞에 F붙이기 (FItemSpawnRow)

  • FTableRowBase : 이 구조체를 상속 받아야 Data Table의 행 구조체 사용 가능

  • 멤버 변수 역시 수정가능하게 리플렉션에 등록 및 설정

TSubclassOf / TSoftClassPtr

  • 클래스를 참조하기 위한 데이터 구조

TSubclassOf

  • 하드 레퍼런스(참조)로, 클래스가 항상 메모리에 load된 상태에서 접근

  • 내부적으로 UClass*로 받아 저장하므로 사용하기 편리

  • 이전에 작성한 대로, 하위액터만 사용 가능하도록 해주는 안정성도 제공

TSoftClassPtr

  • 소프트 레퍼런스로, 그냥 클래스의 경로 참조를 받음

  • 나중에 클래스가 필요해지면 그 때 메모리에 로드하여 사용

  • Data Table처럼 DataClass가 엄청 많으면 다 메모리에 load하면 비효율적이어서,
    TSofClassPtr 사용

  • .Get()을 이용해 UClass*를 반환받아 사용 가능

Data Table 생성

  • Row Name : 해당 열을 찾는 key값으로 다른 열과 중복되지 않는 고유값이어야 함

  • ItemClass에서 소환할 아이템의 객체 선택. 경로 끝에 C가 붙는데 참조를 의미

4. Data Table 이용한 아이템 스폰

// SpawnVolume.h
#pragma once

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

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

public:
	// ... //
    
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Spawning")
	UDataTable* ItemDataTable;

	UFUNCTION(BlueprintCallable, Category = "Spawning")
	void SpawnRandomItem();

	FVector GetRandomPointVolume() const;
	void SpawnItem(TSubclassOf<AActor> ItemClass);
	FItemSpawnRow* GetRandomItem() const;
};
  • DataTable은 에디터에서 객체를 설정해줘야하므로 EditAnywhere로 설정
  • SpawnRandomItem을 호출해 작동하는지 확인하기 위해 이 함수만 리플렉션에 등록
void ASpawnVolume::SpawnRandomItem()
{
	
	if (FItemSpawnRow* SelectedRow = GetRandomItem())
	{
    	// `ItemClass`가 `TSubclassOf`이므로 `UClass*`로 바로 반환하기에,
    	// `Get()`안 써도 되지만 써줘도 상관없음
		if (UClass* ActualClass = SelectedRow->ItemClass.Get()) {
			SpawnItem(ActualClass);
		}
	}
}

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

	TArray<FItemSpawnRow*> AllRows;

	static const FString ContextString(TEXT("ItemSpawnContext"));
	ItemDataTable->GetAllRows(ContextString, AllRows);

	if (AllRows.IsEmpty()) return nullptr;

	float TotalChance = 0.f;
	for (const FItemSpawnRow* Row : AllRows) {
		if (Row) 
		{
			TotalChance += Row->SpawnChance;
		}
	}
	
    // 아이템 뽑는 확률은 누적 확률을 이용해서 사용
    // A확률(0.5), B(0.3), C(0.2)일 때 0.7이 랜덤으로 나온다면 B가 뽑히는 방식
	const float RandValue = FMath::FRandRange(0.f, TotalChance);
	float AccumulateChance = 0.f;
	for (FItemSpawnRow* Row : AllRows) {
		if (Row)
		{
			AccumulateChance += Row->SpawnChance;
			if (AccumulateChance >= RandValue) {
				return Row;
			}
		}
	}
	
	return nullptr;
}
  • GetAllRows(const FString& ContextString, TArray<T*>& Array)
    DataTable의 모든 행을 가져와 Array에 넣어줌
    ContextString은 함수 실행 중 오류가 발생할 경우, 에러 메시지에 포함되어 디버깅에 도움을 줌

5. PlayerState

  • 각 플레이어의 상태 정보를 관리하는 Actor 클래스

  • 점수, 닉네임, 팀 정보, 체력 등 캐릭터의 정보를 담당

  • 멀티플레이 환경에서 각 플레이어 간 데이터 동기화를 위해 사용

  • 싱글플레이 게임에서는 동기화가 필요 없으므로, 캐릭터 클래스자체에서 관리해도 무방

6. BluePrintPure

  • UFUNCTION(BluePrintPure) : Get 함수 전용 매크로. 블루프린트 호출 시 실행/입력 핀 없음. 값만 반환

7. 캐릭터 체력 구현

UGameplayStatics::ApplyDamage

  • 데미지를 발생시키는 함수

  • 공격자가 데미지 줄 대상 액터와 데미지 양, 데미지를 유발한 주체 등을 인자로 넘겨 호출

  • 대상 액터의 TakeDamage() 함수를 호출

  • Static이라 객체 생성없이 사용 가능

  • #include "Kismet/GameplayStatics.h" 헤더파일 필요

AActor::TakeDamage

  • 데미지를 받는 함수

  • Actor의 가상함수로써, 모든 액터가 이 함수를 가지며, 자식 클래스에서 오버라이드하여 사용

  • 체력 감소 또는 특수한 데미지 처리 로직을 이 안에서 구현

구현

// MyCharacter.h

virtual float TakeDamage(
	float DamageAmount,
	struct FDamageEvent const& DamageEvent,
	AController* EventInstigator,
	AActor* DamageCauser) override;
  • DamageAmount : 들어온 데미지 양
  • DamageEvent : 데미지에 대한 추가 정보 (피격 위치, 데미지 종류)
  • EventInstigator : 데미지 입힌 주체의 컨트롤러
  • DamageCauser : 데미지를 가한 주체
// MyCharacter.cpp
float AMyCharacter::TakeDamage(
	float DamageAmount, 
	FDamageEvent const& DamageEvent,
	AController* EventInstigator, 
	AActor* DamageCauser)
{
	float ActualDamage = Super::TakeDamage(DamageAmount, 
    										DamageEvent, 
                                            EventInstigator, 
                                            DamageCauser);

	Health = FMath::Clamp(Health - ActualDamage, 0.f, 100.0f);
	UE_LOG(LogTemp, Warning, TEXT("HP decreased to : %f"), Health);
	if (Health <= 0.f)
	{
		OnDeath();
	}

	return ActualDamage;
}
  • 먼저 부모클래스의 함수를 실행시켜 동작하게 하고 ActualDamage를 받음

  • 들어온 데미지와 방어력 등에 의한 요인으로 받는 피해량은 달라지므로 새로 받는다

  • Clamp(x, a, b) : x를 반환하는데, x<a일 경우 a를, x>b일 경우 b를 반환

체력 회복 구현

void AMyCharacter::AddHealth(float Amount)
{
	Health = FMath::Clamp(Health + Amount, 0.f, 100.0f);
}

8. 지뢰 피해 구현

#include "Kismet/GameplayStatics.h" // ApplyDamage 사용 위한 헤더파일
void AMineItem::Explode()
{
	TArray<AActor*> OverlappingActors;
	ExplosionCollision->GetOverlappingActors(OverlappingActors);

	for (AActor* Actor : OverlappingActors) {
		if (Actor && Actor->ActorHasTag("Player")) {
			UGameplayStatics::ApplyDamage(
				Actor,
				ExplosionDamage,
				nullptr,
				this,
				UDamageType::StaticClass()
			);
		}
	}

	DestroyItem();
}
  • 이전에 작성한 코드에서 출력부분만 데미지 발생시키도록 변경

  • ApplyDamage : 아래는 인자 순서

    • AActor* DamagedActor : 피해 받을 대상
    • float BaseDamage : 데미지 float BaseDamage
    • AController* EventInstigator : 데미지를 유발한 주체의 컨트롤러 . 없음
    • AActor* DamageCauser : 데미지 유발한 주체
    • TSubclassOf<UDamageType> DamageTypeClass : 데미지 유형. 기본값 사용

9. 힐링 아이템 효과 구현

  • 마찬가지로 플레이어 객체의 AddHealth를 호출해주면 된다

10. 게임 점수 구현

GameState

  • 게임 전체적으로 공유되는 전역 정보를 저장 (점수, 시간, 몬스터 수 등)

  • 멀티플레이에서는 서버가 관리하고, 클라이언트는 이를 받아 동기화

  • 기본적으로 “레벨당 1개” 존재

  • GameStateBase 또는 GameState클래스를 상속받아 구현

UCLASS()
class SPARTPROJECT_API AMyGameStateBase : public AGameStateBase
{
	GENERATED_BODY()

public:
	AMyGameStateBase();

	UPROPERTY(VisibleAnyWhere, BluePrintReadWrite, Category = "Score")
	int32 Score;
	
	UFUNCTION(BlueprintPure, Category = "Score")
	int32 GetScore() const;
	UFUNCTION(BlueprintCallable, Category = "Score")
	void AddScore(int32 Amount);
};
  • 구현해주고, GameMode에서 GameState로 설정

Coin

// CoinItem.cpp
#include "CoinItem.h"
#include "Engine/World.h"	// World 관련 함수 사용 가능
#include "MyGameStateBase.h"

void ACoinItem::ActivateItem(AActor* Activator)
{
	if (Activator && Activator->ActorHasTag("Player")) {
		if (UWorld* World = GetWorld())	// 월드 가져오고
		{
        	// 현재 월드의 GameStateBase 잘 가져왔으면
			if (AMyGameStateBase* GameState = World->GetGameState<AMyGameStateBase>()) {
            	// 전역 변수인 점수 추가
				GameState->AddScore(PointValue);
			}
		}

		DestroyItem();
	}
}
  • 현재 GameMode와 GameStateBase를 사용하였는데, 원래 싱글게임에선 GameModeBase-GameStateBase를 / 멀티는 GameMode-GameState를 사용해야함

11. 결과 영상

profile
반갑습니다

0개의 댓글