언리얼 엔진 5/개발 일지

[UE5] 블루아카이브 TPS게임 개발일지 (54) - EX 스킬 제작 2 + LSP

ciel45 2024. 2. 2. 22:33

바로 이전 포스팅에서 ExProjectile을 임시로 만들었는데, ExProjectile도 제대로 C++ 클래스를 만들어 코드 상으로 관리하고자 한다.

 

ExProjectile은 Projectile과 굉장히 비슷하다. Projectile의 거의 모든 특징을 그대로 가진다.

 

 

일반적인 생각으로는, ExProjectile은 Projectile의 일종이다. 즉, is-a 관계가 성립하여, ExProjectile은 Projectile을 상속받도록 하는 것이 적절하다.

 

다만, 여기서 LSP를 고려해볼 필요가 있다.

LSP란 리스코프 치환 법칙을 의미한다. 그 의미는 간단히 하자면 다음과 같다.

 

자식 클래스는 부모 클래스를 대체할 수 있어야한다.

 

더 직관적으로 표현하자면, 부모 자리에 자식이 들어가도 지장이 없어야 한다는 의미이다.

 

 

Projectile을 상속하여 ExProjectile을 만들었다면, 다형성에 의해 ExProjectile은 Projectile 타입으로써 다뤄질 수 있다.

이렇게 실제로는 ExProjectile인 인스턴스를 Projectile 타입으로 간주하고 다룰 때, 어떤 문제가 있을까?

 

Projectile은 기본적으로 적이 쏘는 탄환이라고 가정하고 만들어놓았다.

현재 Projectile의 Damage 함수에서는 부딪힌 액터가 KannaCharacter인지 판별하여, 맞을 시에만 데미지를 주고 있다.

반면에 ExProjectile은 칸나가 쏘는 탄환이고, 부딪힌 액터가 Enemy일 때만 데미지를 줄 것이다.

 

ExProjectile은 엄연히 Projectile과 다른 기능을 수행하며, ExProjectile은 Projectile인 척 할 수 없다.

이렇게 자식클래스가 부모클래스의 기능과 방향성이 다르게 되는 경우, 혹은 부모클래스의 기능을 nullify하게되는 경우에는 상속을 사용하는 것은 적절하지 않다.

 

겉보기에는 is-a 관계이므로 상속을 쓰면 좋을 것 같지만, 실제로는 그렇지 않은 것이다.

 

 

 

하지만 상속을 사용하지 않자니, 두 클래스의 코드가 많이 중복될 것 같다. 

따라서 코드의 재사용성을 위해, 두 클래스에서 공통 영역을 뽑아내어 추상화시켜, 그것을 ProjectileBase 클래스로 만들 것이다.

 

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

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

class UProjectileMovementComponent;

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

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

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

	/* 순수 가상함수 */
	virtual void Damage(
		UPrimitiveComponent* HitComponent, 
		AActor* OtherActor, 
		UPrimitiveComponent* OtherComp, 
		FVector NormalImpulse,
		const FHitResult& Hit) PURE_VIRTUAL (AProjectileBase::Damage,);

	UPROPERTY(EditDefaultsOnly)
	UStaticMeshComponent* Projectile;

	UPROPERTY(EditDefaultsOnly)
	UProjectileMovementComponent* ProjectileMovement;
};

 

ProjectileBase는 월드에 배치할 수 없도록, 그리고 추후의 확장성을 고려하여 추상 클래스로 만들었다.

 

Damage함수는 Projectile과 ExProjectile의 가장 큰 차이점이다. 따라서 해당 함수는 순수 가상함수로 만들었다.

 

순수 가상함수는 부모클래스에서는 구현을 제공하지 않아, 자식클래스에서 반드시 오버라이딩 해야하는 함수이다.

C#의 abstract 함수와 같다.

 

일반적인 C++ 순수 가상함수 문법과 달리 PURE_VIRTUAL 매크로를 사용하는 다소 해괴한 문법인데, 저것이 언리얼 엔진에서 순수 가상함수를 사용하는 방법이다.

 

 

Projectile, ExProjectile에서 Damage는 각각 다음과 같이 오버라이딩 하였다.

void AProjectile::Damage(UPrimitiveComponent* HitComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, FVector NormalImpulse, const FHitResult& Hit)
{
	//맞은 액터가 칸나였을 경우
	if (Cast<AKannaCharacter>(OtherActor))
	{
		UGameplayStatics::ApplyDamage(OtherActor, 20.f, GetInstigator()->GetController(), this, UDamageType::StaticClass());
	}

	Destroy();
}
void AExProjectile::Damage(UPrimitiveComponent* HitComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, FVector NormalImpulse, const FHitResult& Hit)
{
	//맞은 액터가 AEnemy였을 경우
	if (Cast<AEnemy>(OtherActor))
	{
		UGameplayStatics::ApplyDamage(OtherActor, 100.f, GetInstigator()->GetController(), this, UDamageType::StaticClass());
	}

	Destroy();
}

 

 

이렇게 한 뒤 BP_ExProjectile의 부모 클래스를 AExProjectile로 재설정해주고, Pistol의 ExProjectile에 할당해주었다.

 

 

다음 포스팅에서는 EX스킬을 누를 시 총구로 빛이 모이는 효과를 넣을 것이다.