랜덤 스폰
Data Table
TSubclassOf / TSoftClassPtr
PlayerState
BluePrintPure
플레이어 체력 구현
지뢰 피해 구현
힐링아이템 효과 구현

특정 범위 안에서 아이템을 랜덤으로 스폰하는 방법은 여러가지가 있지만, 간단하면서도 쉬운 방법으로 Box Collision을 이용함
Box Collision의 영역 = 스폰 영역
Collision 컴포넌트를 사용하는 이유
// 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가 아니면 오류가 남. 타입이 맞는지 런타임에도 확인하기에 안전하게 사용하기 위해 이용// 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만큼 회전시켜 생성확률적으로 아이템을 소환할 때, 각 아이템의 확률을 하드코딩하여 사용하면, 확률을 수정할 때마다 빌드를 새로 해야하기에 비효율적
그래서 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;
};
블루프린트에서도 사용할 것이니까 리플렉션에 등록해야 함
#include "ItemSpawnRow.generated.h" USTRUCTGENERATED_BODY()BlueprintType 선언구조체이니까 이름 앞에 F붙이기 (FItemSpawnRow)
FTableRowBase : 이 구조체를 상속 받아야 Data Table의 행 구조체 사용 가능
멤버 변수 역시 수정가능하게 리플렉션에 등록 및 설정
TSubclassOf
하드 레퍼런스(참조)로, 클래스가 항상 메모리에 load된 상태에서 접근
내부적으로 UClass*로 받아 저장하므로 사용하기 편리
이전에 작성한 대로, 하위액터만 사용 가능하도록 해주는 안정성도 제공
TSoftClassPtr
소프트 레퍼런스로, 그냥 클래스의 경로 참조를 받음
나중에 클래스가 필요해지면 그 때 메모리에 로드하여 사용
Data Table처럼 DataClass가 엄청 많으면 다 메모리에 load하면 비효율적이어서,
TSofClassPtr 사용
.Get()을 이용해 UClass*를 반환받아 사용 가능
![]() | ![]() |
|---|

Row Name : 해당 열을 찾는 key값으로 다른 열과 중복되지 않는 고유값이어야 함
ItemClass에서 소환할 아이템의 객체 선택. 경로 끝에 C가 붙는데 참조를 의미
// 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)각 플레이어의 상태 정보를 관리하는 Actor 클래스
점수, 닉네임, 팀 정보, 체력 등 캐릭터의 정보를 담당
멀티플레이 환경에서 각 플레이어 간 데이터 동기화를 위해 사용
싱글플레이 게임에서는 동기화가 필요 없으므로, 캐릭터 클래스자체에서 관리해도 무방
UFUNCTION(BluePrintPure) : Get 함수 전용 매크로. 블루프린트 호출 시 실행/입력 핀 없음. 값만 반환데미지를 발생시키는 함수
공격자가 데미지 줄 대상 액터와 데미지 양, 데미지를 유발한 주체 등을 인자로 넘겨 호출
대상 액터의 TakeDamage() 함수를 호출
Static이라 객체 생성없이 사용 가능
#include "Kismet/GameplayStatics.h" 헤더파일 필요데미지를 받는 함수
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);
}
#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 BaseDamageAController* EventInstigator : 데미지를 유발한 주체의 컨트롤러 . 없음AActor* DamageCauser : 데미지 유발한 주체 TSubclassOf<UDamageType> DamageTypeClass : 데미지 유형. 기본값 사용AddHealth를 호출해주면 된다
게임 전체적으로 공유되는 전역 정보를 저장 (점수, 시간, 몬스터 수 등)
멀티플레이에서는 서버가 관리하고, 클라이언트는 이를 받아 동기화
기본적으로 “레벨당 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);
};

// 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();
}
}