Showing

[디자인 패턴] 생태계 조성에 유리한 추상 팩토리 Abstract Factory 본문

Design Pattern

[디자인 패턴] 생태계 조성에 유리한 추상 팩토리 Abstract Factory

RabbitCode 2023. 12. 11. 16:32

 

현실 혹은 게임 생태계 내에는 하나의 종, 단일 개체군만 있는 것은 아니다.

수없이 많은 개체군들이 상호 작용을 하면서 살아가고 있으며, 이러한 개체군들의 모임을 군집이라고도 한다.

 

생물 군집은 일반적으로 생산자, 소비자, 분해자로 이루어져 있다. 생산자는 광합성을 통해 유기물을 합성하며 소비자는 생산자가 합성한 유기물을 먹고 산다. 마지막으로 분해자는 생산자나 소비자의 사체를 분해한다. 이들 사이에는 서로 먹고 먹히는 먹이 사슬이 형성되고, 한 포식자가 여러 종류의 먹이를 먹기 때문에 먹이 사슬이 더욱 복잡하게 얽혀 먹이 그물이 된다. 군집 내에서 각 개체군의 위치를 생태적 지위라고 한다. 각 먹이 사슬 별로 매우 다양한 개체가 존재한다.

참고: https://janghoa.tistory.com/169

 

위의 생물학 개념을 첨부한 이유는 추상 팩토리 패턴을 학습하는데 있어 종, 개체군, 군집의 개념을 적용한다면 보다 효과적으로 이해하기 쉽다고 생각하였기 때문이다.

구축하고자 하는 생태계가 제 아무리 복잡하게 그 관계가 얽혀있다고 하여도, 합의된 정의에 따라 종에 대한 (혹은 편의에 따라 개체군, 군집으로 공통 개념을 만들 수 있음) 통합 개념을 만들어놓고 그 개념을 상속해서 저 마다의 개체마다 다른 성질과 능력을 부여해서 찍어낼 수 있다면 제아무리 대규모의 생태계라도 소프트웨어 상에서 빠르게 구축할 수 있을 것이다.

 

추상 팩토리 패턴을 이용해서 캐릭터를 찍어 생성하는 예저를 아래에 작성해보도록 한다.

 

캐릭터 별로 actor을 만드는 것은 추후 프로젝트가 방대해질수록 비효율적일 것이다.

추상화 : 실질적인 데이터는 없는 대신에 공장처럼 찍어낼 수 있다면?

 

추상 팩토리 패턴이란 객체 생성을 추상화하여 관련 있는 객체들의 팩토리를 생성하는 디자인 패턴이다.

 

객체 생성을 추상화(껍데기만)하여 객체 생성의 구체적인 클래스를 분리하면 된다.

규격만 추상적으로 짜놓고 실질적인 구현은 다른데서 이루어진다. 유연성과 확장성을 갖출 수 있다.

 

인벤토리를 만들고자 할 때 인벤토리 아이템 별 col 정보들을 struct 멤버 변수화해놓고, 이 struct 기반으로 data table에서 실질적인 내용물들을 채우는 예시를 들 수 있다.

 

아래와 같이 산리오 몬스터즈를 만들고자 할 때 한마리씩 클래스를 생성하는 대신에 프렌즈 공통 설계를 세워놓고 이를 기반으로 공장처럼 찍어내면 될 것이다.

Unit을 만들고, 그것을 상속한 Enemy, 그것을 기반으로 폼폼푸린 클래스를 만들어본다고 가정해보자.

Unit : PURE_VIRTUAL 가상 함수만 구현해서, 구조만 이룸

위와 같이 만들 때, Enemy부터 무조건  PURE_VIRTUAL 가상 함수들을 구현해야한다.(단순 메쉬 변경만 이루어질 것이라면 블루프린트를 적극활용하는 것이 개발 비용이 저렴할 것이다.)

구조만 짜여져 있는 추상 클래스(그 안에는 가상 함수들이 있다)
이를 상속하는 자식 클래스들이 실체화될 틀이다.

추상 클래스에 가까울수록 hp, mp, 스켈레탈메쉬, 콜리전 박스 등 필수적인 요소가 들어갈 것이고,

자식 클래스일수록 컴포넌트 패턴으로 만든 다양한 컴포넌트를 자유롭게 붙였다 떼면서 커스텀 할 수 있을 것이다.

 

 

Actor Unit을 만들어보도록 한다.

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

#pragma once

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

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

	UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "Unit")
	void Attack(); 

	virtual void Attack_Implementation() PURE_VIRTUAL(AUnit::Attack_Implementation);
	/* VIRTUAL : 가상의, 껍데기인 : 사람이 편하기 위한 가상함수; 실질적으로 컴파일러를 위한 것이 아님
	UFUNCTION(BlueprintNativeEvent)
	이 매크로는 Unreal Engine에서 블루프린트와 상호작용하는 함수로 정의
	이 함수는 네이티브(c++) 구현과 블루프린트 구현 모두를 가질 수 있음
	블루프린트에서 이 함수를 오버라이드하거나 호출할 수 있음
	*/
	UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "Unit")
	void Defend();
	virtual void Defend_Implementation() PURE_VIRTUAL(AUnit::Defend_Implementation);
	/*
	* PURE_VIRTUAL(AUnit::Defend_Implementation) :  이 부분은 함수의 가상 함수 구현을 정의
	* PURE_VIRTUAL 매크로는 이 함수가 순수 가상 함수임을 나타냄
	* 순수 가상 함수는 구현이 없으며, 파생 클래스에서 반드시(무조건) 오버라이드 해야함.
	* 따라서, AUnit 클래스를 상속하는 클래스는 Defend_Implementation() 함수를 반드시 구현해야함.
	*/
	int32 Hp;
	int32 Mp;
protected:
	// Called when the game starts or when spawned
	virtual void BeginPlay() override;

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

 

순수 가상 함수의 구현은 파생 클래스에서 작성해주어야 한다.

따라서 헤더만 덩그러니 있고, cpp는 빈 상태일 것이다.

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


#include "Unit.h"

// Sets default values
AUnit::AUnit()
{
 	// Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
	PrimaryActorTick.bCanEverTick = true;

}

// Called when the game starts or when spawned
void AUnit::BeginPlay()
{
	Super::BeginPlay();
	
}

// Called every frame
void AUnit::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

}

텅비어 있을 것이다.

Unreal c++ 문법에 맞게 채워준다. Unreal의 각종 예약 클래스들이 어떻게 만들어졌는지 짐작을 할 수 있을 것이다.

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

#pragma once

#include "CoreMinimal.h"
#include "Unit.h"
#include "Enemy.generated.h"

/**
 * 
 */
UCLASS()
class THIRDPERSONTEMPLATE_API AEnemy : public AUnit
{
	GENERATED_BODY()
public:
	AEnemy();
	// 유닛 클래스의 추상 함수를 오버라이드하여 구현
	virtual void Attack_Implementation() override;
	virtual void Defend_Implementation() override;
};
// Fill out your copyright notice in the Description page of Project Settings.


#include "Enemy.h"

AEnemy::AEnemy()
{
}

void AEnemy::Attack_Implementation()
{
	// 적의 공격 기능 구현
	UE_LOG(LogTemp, Warning, TEXT("Enemy is Attacking!"))
}

void AEnemy::Defend_Implementation()
{
	UE_LOG(LogTemp, Warning, TEXT("Enemy is Defending!"))
}

이제 적 혹은 캐릭터들을 공장처럼 찍어낼 준비가 된 것이다.

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

#pragma once

#include "CoreMinimal.h"
#include "Enemy.h"
#include "PompomPurin.generated.h"

/**
 * 
 */
UCLASS()
class THIRDPERSONTEMPLATE_API APompomPurin : public AEnemy
{
	GENERATED_BODY()
	APompomPurin();
	// 적 클래스의 추상 함수를 오버라이드하여 구현
	virtual void Attack_Implementation() override;
	virtual void Defend_Implementation() override;
};

 

#include "PompomPurin.h"
APompomPurin::APompomPurin()
{
}
void APompomPurin::Attack_Implementation()
{
}

void APompomPurin::Defend_Implementation()
{
}