이전에 아이템 생성한 글과 연결되는 글입니다.
Unreal Engine - 아이템 만들기 (+충돌 처리)
1. 목표게임상에서 캐릭터와 아이템 간의 충돌이 발생하면 특정 이벤트가 발생하도록 구현해야 한다.구현해야 할 것은 아래와 같다.아이템 클래스와 그것을 상속받은 여러 자식 아이템 클래스
gbleem.tistory.com
1. 아이템 스폰
아이템은 스폰이 가능한 지역(범위)을 만들어 둔 후, 해당 지역 안에 랜덤한 위치에 아이템을 스폰되도록 구현할 것이다.
구현해야 할 것은 크게 가지 이다.
- 랜덤한 스폰 위치 구하기
- 아이템 마다 랜덤한 확률 부여하기
- 랜덤한 위치에 스폰하기
1 - 1. 랜덤한 스폰 위치 구하기
콜리젼 컴포넌트를 만든 후 임의의 좌표를 뽑아서, 액터를 스폰할 것입니다.
- 콜리젼 컴포넌트에서 랜덤한 좌표 뽑아내기
- GetScaledBoxExtent 함수를 통해서 중점에서 끝까지의 거리를 구하고
- RandRange를 통해 랜덤한 값을 만들었다.
- 코드
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 사용하기
- 스폰을 하기 위한 로직은 아래와 같다.
- Data Table에서 모든 행을 순환하면서, 우리가 저장했던 값인 생성 확률(SpawnChance) 을 가져와서 모두 더해서 Total 확률을 구한다.
- RandRange를 통해서 Total 확률 중 임의의 값을 뽑는다.
- 누적 랜덤값 뽑기 알고리즘을 통해서 현재 스폰할 아이템의 정보(Row값) 을 return 해준다.
- 해당 Row값에서 원하는 Class 를 가져온 후 아이템을 스폰한다. (GetWorld()->SpawnActor 함수 사용)
- 이때 스폰하는 위치는 위에서 구현한 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 : 데미지 유형
- 특이한 구현이 아니기에 기본 데미지 유형을 사용했다.
- 파생 클래스를 만들어서 속성 공격과 같은 유형을 정의할 수 있다.
- DamagedActor : 데미지를 받을 액터
- 구현부 코드
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 |