Deeper Learning

[UE5] 언리얼 엔진 클래스 선택 및 설계 본문

Game Development/Unreal Engine

[UE5] 언리얼 엔진 클래스 선택 및 설계

Dlaiml 2024. 5. 22. 08:02

언리얼 엔진을 처음 공부할 때, 추가하고 싶은 기능을 구현하려면 어떤 클래스를 상속받아야 하는지 선택하는 것이 쉽지 않았다. 총 4622개의 Object를 제공하는데 각 Object에 어떤 기능이 있고 상속관계가 어떻게 되는지에 대해 아예 알지 못하였기 때문이다. 이번에 강의를 들으면서 만든 예제를 통해 간단한 게임을 만들 때 선택한 Object에 대해 설명하려 한다.

 

 

 

만들려는 게임에서 필요한 요소를 크게 그룹 지으면 다음과 같다.

 

- 플레이어 탱크 / 적 타워

- 포탄 투사체

- 게임의 승리, 패배 조건 설정

 

 

BasePawn, Tank, Tower - Pawn

PlayerController가 Possess 할 수 있는 Actor인 Pawn이라는 클래스를 탱크, 타워를 구현할 클래스로 선택하였다.

이전 게임에서는 mesh, collision, movement logic이 포함된 Pawn을 상속받아 만들어진 Character 클래스를 선택하였으나 이번에는 그 중 movement logic을 따로 사용하지 않을 것이기 때문에 Pawn을 선택하였다.

 

우선 플레이어가 컨트롤 할 탱크 / 적 타워에서 공통적으로 구현이 필요한 부분이 있다.

Pawn을 Base 클래스로 하는 ABasePawn 클래스에는 아래 항목의 구현이 필요하다.

 

- 포탄과의 충돌을 감지할 Collision 

- Pawn의 시각적 요소를 담당하는 폴리곤으로 이루어진 Mesh

- 포탄 투사체 발사

- 포신이 포함된 탱크, 타워의 윗부분 회전

- 탱크의 상태 및 탱크의 상태와 관련된 특수효과 (피격으로 인한 폭발 파티클, 피격 카메라 효과)

 

// ABasePawn.h

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Pawn.h"
#include "BasePawn.generated.h"


UCLASS()
class TOONTANKS_API ABasePawn : public APawn
{
	GENERATED_BODY()

public:
	// Sets default values for this pawn's properties
	ABasePawn();
	void HandleDestruction();

protected:
	void RotateTurret(FVector LookAtTarget);
	void Fire();

private:
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components", meta = (AllowPrivateAccess = true))
	class UCapsuleComponent* CapsuleComp;

	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components", meta = (AllowPrivateAccess = true))

	UStaticMeshComponent* BaseMesh;

	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components", meta = (AllowPrivateAccess = true))

	UStaticMeshComponent* TurretMesh;

	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components", meta = (AllowPrivateAccess = true))

	USceneComponent* ProjectileSpawnPoint;

	UPROPERTY(EditDefaultsOnly, Category="Combat")
	TSubclassOf<class AProjectile> ProjectileClass;

	UPROPERTY(EditAnywhere, Category="Particles")
	UParticleSystem* DeadParticles;

	UPROPERTY(EditAnywhere, Category="Combat")
	USoundBase* DeathSound;

	UPROPERTY(EditAnywhere, Category = "Combat")
	TSubclassOf<UCameraShakeBase> DeathCameraShake;

};

 

다음으로 위에서 만든 BasePawn 클래스를 Base 클래스로 하는 Player가 직접 컨트롤하는 Tank 클래스에 구현이 필요한 부분은 다음과 같다.

 

- 이동, 회전 등 플레이어의 조작

- 카메라 

 

// Tank.h

#pragma once

#include "CoreMinimal.h"
#include "BasePawn.h"
#include "Tank.generated.h"

/**
 * 
 */
UCLASS()
class TOONTANKS_API ATank : public ABasePawn
{
	GENERATED_BODY()

public:
	ATank();

	// Called to bind functionality to input
	virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
	
	// Called every frame
	virtual void Tick(float DeltaTime) override;
	void HandleDestruction();

	APlayerController* GetTankPlayerController() const { return TankPlayerController;}

	bool bAlive = true;

protected:
	// Called when the game starts or when spawned
	virtual void BeginPlay() override;

private:


	UPROPERTY(VisibleAnywhere, Category="Components")
	class USpringArmComponent* SpringArm;

	UPROPERTY(VisibleAnywhere, Category="Components")
	class UCameraComponent* Camera;

	UPROPERTY(EditAnywhere, Category="Movement")
	float Speed = 200.0f;

	UPROPERTY(EditAnywhere, Category = "Movement")
	float TurnRate = 45.0f;

	void Move(float Value);
	void Turn(float Value);

	APlayerController* TankPlayerController;
	
};

 

적 Pawn인 Tower 클래스도 BasePawn 클래스를 상속받으며 필요한 구현은

- 플레이어 탱크 탐지 및 공격 로직 (간단한 로직이라서 AIController, Behavior Tree를 사용하지 않았다)

 

// Tower.h

#pragma once

#include "CoreMinimal.h"
#include "BasePawn.h"
#include "Tower.generated.h"

/**
 * 
 */
UCLASS()
class TOONTANKS_API ATower : public ABasePawn
{
	GENERATED_BODY()
	
public:
	// Called every frame
	virtual void Tick(float DeltaTime) override;
	void HandleDestruction();

protected:
	// Called when the game starts or when spawned
	virtual void BeginPlay() override;

private:
	class ATank* Tank;

	UPROPERTY(EditAnywhere, Category="Fire", meta=(AllowPrivateAccess=true))
	float FireRange = 300.f;

	FTimerHandle FireRateTimerHandle;
	float FireRate = 2.0f;
	void CheckFireCondition(); 
	bool InFireRange();
};

 

이렇게 Pawn 하위에 있는 클래스의 구현을 마쳤다.

 

Projectile - Actor

다음으로는 Tank, Tower가 Fire() 할 때 Spawn되는 포탄과 관련된 로직을 담고 있는 Projectile 클래스를 구현하려 한다.

Player가 Possess할 필요가 없으며 Spawn되어 Level에 배치되어야 하기 때문에 Actor 클래스를 Base 클래스로 구현하는 것이 적합하다.

 

구현이 필요한 것들을 생각해 보면

- 포탄의 Mesh(Collision 포함)

- 포탄의 이동 (탱크나 타워가 Spawn 하고 나서의 이동)

- 포탄 명중 관련 로직 (Actor에게 대미지를 적용하는 ApplyDamage 함수 Call)

- VFX, SFX

 

// Projectile.h

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Projectile.generated.h"

UCLASS()
class TOONTANKS_API AProjectile : public AActor
{
	GENERATED_BODY()
	
public:	
	// Sets default values for this actor's properties
	AProjectile();

protected:
	// Called when the game starts or when spawned
	virtual void BeginPlay() override;

private:
	UPROPERTY(EditDefaultsOnly, Category="Combat")
	UStaticMeshComponent* ProjectileMesh;

	UPROPERTY(VisibleAnywhere, Category = "Movement")
	class UProjectileMovementComponent* ProjectileMovement;

	UPROPERTY(EditAnywhere, Category = "Combat")
	class UParticleSystem* HitParticles;

	UPROPERTY(VisibleAnywhere, Category = "Combat")
	class UParticleSystemComponent* SmokeTrail;

	UPROPERTY(EditAnywhere, Category = "Combat")
	USoundBase* LaunchSound;
	
	UPROPERTY(EditAnywhere, Category = "Combat")
	USoundBase* HitSound;

	UPROPERTY(EditAnywhere, Category = "Combat")
	TSubclassOf<UCameraShakeBase> HitCameraShake;
	
	UPROPERTY(EditAnywhere, Category = "Combat")
	float Damage = 50.f;

	UFUNCTION()
	void OnHit(UPrimitiveComponent* HitComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, FVector NormalImpulse, const FHitResult& Hit);

	



public:	
	// Called every frame
	virtual void Tick(float DeltaTime) override;

};

 

ToonTanksGameMode - GameModeBase

다음으로 게임의 승패 조건의 구현이 필요하다. 이는 게임의 규칙, 점수 등을 정의하는 GameModeBase를 Base 클래스로 하는 것이 적합하다.

The GameModeBase defines the game being played. It governs the game rules, scoring, what actors are allowed to exist in this game type, and who may enter the game. A GameModeBase actor is instantiated when the level is initialized for gameplay in C++ UGameEngine::LoadMap().

 

- 승리 패배 조건 (상대 타워 모두 파괴 / 플레이어의 탱크 소멸)

- 게임 시작, 게임 끝 이후 동작 관리 (게임 시작 후 카운트다운 및 UI widget blueprint를 Create / 게임 끝나고 마우스 비활성화 및 Tower 공격 중지)

 

// AToonTanksGameMode

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/GameModeBase.h"
#include "ToonTanksGameMode.generated.h"

/**
 * 
 */
UCLASS()
class TOONTANKS_API AToonTanksGameMode : public AGameModeBase
{
	GENERATED_BODY()
	
public:
	void ActorDied(AActor* DeadActor);

protected:
	virtual void BeginPlay() override;

	UFUNCTION(BlueprintImplementableEvent)
	void StartGame();

	UFUNCTION(BlueprintImplementableEvent)
	void GameOver(bool bWonGame);


private:
	class ATank* Tank;
	class AToonTanksPlayerController* ToonTanksPlayerController;

	float StartDelay = 3.f;
	void HandleGameStart();

	int32 TargetTowers = 0;
	int32 GetTargetTowerCount();

};

 

Others

 

플레이어 사망 후 Input을 Disalbe, Enable 하는 부분은 PlayerController와 관련된 기능으로 PlayerController를 Base 클래스로 하여 클래스를 만들고 내부에 구현하였다. (GameMode의 게임시작, 게임종료에서 호출)

 

탱크, 타워의 체력과 관련된 함수들 (Actor 체력 초기화, Actor가 대미지를 입었을 때 체력이 0 이하인지 확인하는 Callback 함)은 Spawn 되거나 Level에 독립적으로 배치될 필요가 없기 때문에 ActorComponent를 Base 클래스로 하였으며 이를 Tank, Tower 클래스의 Component로 추가하였다.

 

카메라의 흔들림은 흔들리는 패턴, 강도, 간격 등을 설정할 수 있는 CameraShakeBase를 Base 클래스로 하였으며 피격 시 흔들림을 위해서 포탄의 충돌 관련 로직을 담당하는 Projectile, 사망 시 흔들림을 위해 탱크, 타워의 상태(사망, 게임시작)를 담당하는 BasePawn 클래스에 멤버로 선언되어 호출된다.

 

Reference

[0] https://docs.unrealengine.com/4.27/en-US/API/Runtime/Engine/GameFramework/AGameModeBase/

 

AGameModeBase

The GameModeBase defines the game being played.

docs.unrealengine.com

[1] Udemy - Unreal Engine 5 C++ Developer: Learn C++ & Make Video Games

 

 

 

 

 

Comments