Unreal Engine - 아이템 스폰 및 캐릭터와 연동

2025. 2. 7. 19:25·Unreal Engine

이전에 아이템 생성한 글과 연결되는 글입니다.

https://gbleem.tistory.com/66

 

Unreal Engine - 아이템 만들기 (+충돌 처리)

1. 목표게임상에서 캐릭터와 아이템 간의 충돌이 발생하면 특정 이벤트가 발생하도록 구현해야 한다.구현해야 할 것은 아래와 같다.아이템 클래스와 그것을 상속받은 여러 자식 아이템 클래스

gbleem.tistory.com

1. 아이템 스폰


아이템은 스폰이 가능한 지역(범위)을 만들어 둔 후, 해당 지역 안에 랜덤한 위치에 아이템을 스폰되도록 구현할 것이다.

구현해야 할 것은 크게 가지 이다.

  • 랜덤한 스폰 위치 구하기
  • 아이템 마다 랜덤한 확률 부여하기
  • 랜덤한 위치에 스폰하기

1 - 1. 랜덤한 스폰 위치 구하기

콜리젼 컴포넌트를 만든 후 임의의 좌표를 뽑아서, 액터를 스폰할 것입니다.

  • 콜리젼 컴포넌트에서 랜덤한 좌표 뽑아내기
    • GetScaledBoxExtent 함수를 통해서 중점에서 끝까지의 거리를 구하고
    • RandRange를 통해 랜덤한 값을 만들었다.

BoxExtent 값의 의미

  • 코드
FVector ASpawnVolume::GetRandomPointInVolume() const
{
	FVector BoxExtent = SpawningBox->GetScaledBoxExtent();
	FVector BoxOrigin = SpawningBox->GetComponentLocation();

	return BoxOrigin + FVector(
		FMath::RandRange(-BoxExtent.X, BoxExtent.X)
		, FMath::RandRange(-BoxExtent.Y, BoxExtent.Y)
		, FMath::RandRange(-BoxExtent.Z, BoxExtent.Z)
	);
}

 

1 - 2. 아이템 마다 랜덤한 확률 부여하기

Data Table을 통해서 아이템 마다 스폰되는 확률을 지정해두고, 그 값을 가져와서 스폰하도록 구현할 것이다.

  • Data Table 만들기
    • 이전에 관련 내용을 정리한 글도 참고하기
    • 추가 내용
      • 구조체 만들 때 None으로된 클래스를 만들고, 필요 없는 것들을 지운 후 구현하면 된다.
      • Engine/DataTable 을 include 해주고,
      • 구조체 이름도 아래와 같이 간단하게 만들면 된다. (앞에 USTRUCT랑 F는 꼭 붙이기)
#include "CoreMinimal.h"
#include "Engine/DataTable.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;
};
  • Data Table 사용하기
    • 스폰을 하기 위한 로직은 아래와 같다. 
      1. Data Table에서 모든 행을 순환하면서, 우리가 저장했던 값인 생성 확률(SpawnChance) 을 가져와서 모두 더해서 Total 확률을 구한다.
      2. RandRange를 통해서 Total 확률 중 임의의 값을 뽑는다.
      3. 누적 랜덤값 뽑기 알고리즘을 통해서 현재 스폰할 아이템의 정보(Row값) 을 return 해준다.
      4. 해당 Row값에서 원하는 Class 를 가져온 후 아이템을 스폰한다. (GetWorld()->SpawnActor 함수 사용)
      5. 이때 스폰하는 위치는 위에서 구현한 GetRandomPointInVolume 함수를 사용하여 구한다.
    • 누적 랜덤값 뽑기 알고리즘
      • 예를 들어 A, B, C의 발생 확률이 각각 50%, 30%, 20% 라고 하자. 
      • 아래와 같이 누적 확률을 구할 수 있다. 
        • A(50%) : 0~50
        • B(30%) : 50~80
        • C(20%) : 80~100
      • Total 확률은 100%이고, Random한 값으로 70을 뽑았다고 하면 우리가 스폰해야할 아이템은 B가 되는 알고리즘이다. 

아래의 코드를 보면 어떤 의미인지 이해할 수 있다.

  •  전체적인 흐름에 대한 코드
void ASpawnVolume::SpawnRandomItem()
{
    if (FItemSpawnRow* SelectedRow = GetRandomItem())
    {
        if (UClass* ActualClass = SelectedRow->ItemClass.Get())
        {
            SpawnItem(ActualClass);
        }
    }
}
  • 어떤 아이템을 스폰할 것인지 고르는 코드 (Data Table에서 값 가져와서 확률 계산 후 스폰할 Row 리턴하기)
FItemSpawnRow* ASpawnVolume::GetRandomItem() const
{
	if (!ItemDataTable)
		return nullptr;
	
	TArray<FItemSpawnRow*> AllRows; //모든 Row를 저장하는 TArray
	static const FString ContextString(TEXT("ItemSpawnContext")); //디버깅 정보 위해서 존재
	
	//Data Table을 돌면서 모든 Row 값을 AllRows에 저장
	ItemDataTable->GetAllRows(ContextString, AllRows);

	if (AllRows.IsEmpty())
		return nullptr;
	
	//Row들 중에서 SpawnChance에 해당하는 값만 가져와서 Total Chance 구하기
	float TotalChance = 0.f;
	for (const FItemSpawnRow* Row : AllRows)
	{
		if (Row)
		{
			TotalChance += Row->SpawnChance;
		}
	}

	//누적 랜덤값 알고리즘
	const float RandValue = FMath::FRandRange(0.f, TotalChance);

	//RandValue(누적 랜덤값 알고리즘으로 구한 값) 보다 
	//기존에 지정한 확률 값이 커지는 순간 Row를 return 하기
	float AccumulateChance = 0.f;
	for (FItemSpawnRow* Row : AllRows)
	{
		AccumulateChance += Row->SpawnChance;
		if (RandValue <= AccumulateChance)
		{
			return Row;
		}
	}
	return nullptr;
}

 

1 - 3. 랜덤한 위치에 스폰하기

  • Spawn Actor를 통해 스폰하는 코드
void ASpawnVolume::SpawnItem(TSubclassOf<AActor> ItemClass)
{
    if (!ItemClass) return;

    GetWorld()->SpawnActor<AActor>(
        ItemClass,
        GetRandomPointInVolume(),
        FRotator::ZeroRotator
    );
}
  • 랜덤한 위치에 랜덤한 아이템이 스폰된 모습

 

 

2. 캐릭터와 아이템의 연동


포션, 동전, 지뢰 세가지 종류의 아이템이 있고, 각각 조금씩 다른 방식을 통해서 캐릭터와 연동을 할 것이다.

2 - 1. PlayerState 

언리얼 엔진에는 Player State라는 플레이어 각각의 정보를 저장하는 클래스가 존재한다. 그러나 우리가 개발하고 있는 게임은 멀티플레이 게임이 아니기 때문에 캐릭터 클래스에 직접 구현해도 충분하다.

단 멀티플레이 게임이라면 PlayerState를 사용하여 동기화 해주는 것이 중요하다.

 

2 - 2. 체력 관련 시스템 

캐릭터 클래스에 체력과 관련된 정보들을 넣어주었다.

UCLASS()
class SCC_PROJECT_API ASpartaCharacter : public ACharacter
{
	GENERATED_BODY()
public:

	UFUNCTION(BlueprintPure, Category = "Health")
	float GetHealth() const;
	UFUNCTION(BlueprintCallable, Category = "Health")
	void AddHealth(float Amount);
	void OnDeath();
		
        ...
        
protected:
	virtual float TakeDamage(
		float DamageAmount
		, struct FDamageEvent const& DamageEvent
		, AController* EventInstigator
		, AActor* DamageCauser) override;
		
        ...
        
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Health")
	float MaxHealth;
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Health")
	float Health;
};
  • TakeDamage 함수
    • Actor의 가상 함수로 언리얼에서 미리 구현해 둔 함수로, 이 함수를 override해서 사용했다.
    • 매개변수
      • DamageAmount : 데미지 양
      • DamageEvent : 데미지를 받은 유형
        • 데미지의 추가적인 정보
        • 스킬 등
      • EventInstigator : 데미지를 유발한 주체
        • 상대 플레이어, 몬스터
        • 지뢰 아이템의 경우는 지뢰를 심은 사람 같은 느낌인데, 그런건 실제로 없기 때문에 안쓰는 파라메터이다.
      • DamageCause : 데미지를 입힌 오브젝트
        • 총알, 폭발물 등
        • 우리의 구현에서는 지뢰를 뜻한다.
      • return 값 : 실제 데미지
        • DamageAmount에서 추가적인 처리를 통해 실제로 캐릭터에게 적용될(계산된) 데미지를 리턴해준다.
    • 구현부 코드
float ASpartaCharacter::TakeDamage(float DamageAmount, FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser)
{
	float ActualDamage = Super::TakeDamage(DamageAmount, DamageEvent, EventInstigator, DamageCauser);

	Health = FMath::Clamp(Health - DamageAmount, 0.0f, MaxHealth);
	UE_LOG(LogTemp, Warning, TEXT("health decrease to %f"), Health);

	if (Health <= 0.f)
	{
		OnDeath();
	}

	return ActualDamage;
}

 

  • ApplyDamage 함수
    • TakeDamage와 쌍이 되는 함수이다.
    • 예를 들어, 지뢰 액터에서 Apply Damage를 호출하면 데미지를 입을 캐릭터에서 TakeDamage가 실행되는 로직을 통해 동작한다.
    • Kismet/GameplayStatics.h 를 include 하고 UGameplayStatics::ApplyDamage() 를 쓰면 된다.
    • 매개변수
      • DamagedActor : 데미지를 받을 액터
        • 이번 구현에서는 캐릭터 액터
      • BaseDamage : 데미지 양
      • EventInstigator : 데미지를 유발한 주체
        • TakeDamage의 EventInstigator와 같은 의미
        • 지뢰 액터에서는 이 주체는 없다
        • 예를 들어 멀티 게임에서 A 플레이어가 지뢰를 설치했다면 A플레이어를 뜻한다.
      • DamageCauser : 데미지를 유발한 오브젝트
        • TakeDamage의 DamageCauser 와 같은 의미
        • 지뢰액터에서는 자기 자신을 뜻한다.
      • DamageTypeClass : 데미지 유형
        • 특이한 구현이 아니기에 기본 데미지 유형을 사용했다.
        • 파생 클래스를 만들어서 속성 공격과 같은 유형을 정의할 수 있다.
    • 구현부 코드
void AMineItem::Explode()
{
	TArray<AActor*> OverlappingActors;
	ExplosionCollisionComp->GetOverlappingActors(OverlappingActors);

	for (AActor* Actor : OverlappingActors)
	{
		if (Actor && Actor->ActorHasTag("Player"))
		{
			UGameplayStatics::ApplyDamage(
				Actor
				,ExplosionDamage
				,nullptr
				,this
				,UDamageType::StaticClass()
			);
		}
	}
	DestroyItem();
}
  • 캐스팅을 통한 함수 호출
    • 체력을 올려주는 함수 같은 경우 GetOverlappingActors를 통해 가져온 Actor를 Casting 해서
    • Player의 함수를 호출해 주는 식으로 간단하게 구현할 수도 있다.
    • 코드
void AHealingItem::ActivateItem(AActor* Activator)
{
	if (Activator && Activator->ActorHasTag("Player"))
	{
		ASpartaCharacter* PlayerCharacter = Cast<ASpartaCharacter>(Activator);
		if (PlayerCharacter)
		{
			PlayerCharacter->AddHealth(HealAmount);
		}

		DestroyItem();
	}
}

 

2 - 3. 점수 관련 시스템

점수 관련 시스템은 GameState를 사용해서 구현하였다. (게임 내에서 코인 아이템을 먹은 경우 점수를 올려주는 시스템)

  • GameState
    • 게임 내의 전역 정보를 저장하는 클래스이다. 기본적으로 레벨당 1개가 존재한다.
    • 게임의 단계나, 스폰된 아이템 갯수, 점수 등
  • 참고) GameMode
    • 게임의 규칙을 정의하는 클래스
    • 어떤 캐릭터를 스폰할지, 플레이어의 사망처리 
  • GameState 코드
    • GameStateBase를 상속받아서 아래와 같이 점수와 관련된 함수를 만들어 주었다.
//GameStateBase.h

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/GameStateBase.h"
#include "SpartaGameStateBase.generated.h"

UCLASS()
class SCC_PROJECT_API ASpartaGameStateBase : public AGameStateBase
{
	GENERATED_BODY()
	
public:
	ASpartaGameStateBase();

	UFUNCTION(BlueprintPure, Category = "Score")
	int32 GetScore() const;
	UFUNCTION(BlueprintCallable, Category = "Score")
	void AddScore(int32 Amount);

public:
	UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "Score")
	int32 Score;	
};
  • 코인 아이템 코드
    • UWorld를 사용하기 위해서 Engine/World.h 헤더를 include
    • GetGameState 를 사용해서 GameState를 가져왔다.
void ACoinItem::ActivateItem(AActor* Activator)
{
	if (Activator && Activator->ActorHasTag("Player"))
	{
		if (UWorld* World = GetWorld())
		{
			if (ASpartaGameStateBase* GameState = World->GetGameState<ASpartaGameStateBase>())
			{
				GameState->AddScore(PointValue);
			}
		}
		DestroyItem();
	}
}

3. 결론


  • 게임 내 아이템과 캐릭터를 연동할 때 다양한 방법을 사용할 수 있다.
    • 전역으로 관리하는 데이터는 GameState를 통해서 관리하며, GetGameState를 통해서 함수를 Call한다.
    • 데미지 관련 로직은 TakeDamge와 ApplyDamage 함수를 통해서 구현
    • 간단한 로직의 경우 GetOverlappingActors 함수를 통해 얻어온 Actor를 casting해서 바로 캐릭터의 함수를 call 하는 방식으로 구현
  • 많이 구현해보면서 좋은 방식을 찾아보는 것이 좋을 것이다.

'Unreal Engine' 카테고리의 다른 글

UE5 Issues : Look Action (bUsePawnControlRotation)  (0) 2025.02.11
Unreal Engine - Game Loop  (0) 2025.02.09
Unreal Engine - 아이템 만들기 (+충돌 처리)  (1) 2025.02.06
Unreal Engine - Simple Niagara (with cpp)  (0) 2025.02.04
Unreal Engine - Data Table  (0) 2025.02.03
'Unreal Engine' 카테고리의 다른 글
  • UE5 Issues : Look Action (bUsePawnControlRotation)
  • Unreal Engine - Game Loop
  • Unreal Engine - 아이템 만들기 (+충돌 처리)
  • Unreal Engine - Simple Niagara (with cpp)
gbleem
gbleem
gbleem 님의 블로그 입니다.
  • gbleem
    gbleem 님의 블로그
    gbleem
  • 전체
    오늘
    어제
    • 분류 전체보기 (176)
      • Unreal Engine (66)
      • C++ (19)
      • 알고리즘(코딩테스트) (26)
      • TIL (60)
      • CS (4)
      • 툴 (1)
  • 블로그 메뉴

    • 홈
    • 카테고리
  • 링크

    • 과제용 깃허브
    • 깃허브
    • velog
  • 공지사항

  • 인기 글

  • 태그

    cin함수
    motion matching
    addonscreendebugmessage
    템플릿
    blend pose
    map을 vector로 복사
    상속
    싱글턴
    BFS
    DP
    actor 클래스
    Vector
    character animation
    additive animation
    applydamage
    enhanced input system
    매크로 지정자
    C++
    const
    gamestate
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
gbleem
Unreal Engine - 아이템 스폰 및 캐릭터와 연동
상단으로

티스토리툴바