게임 개발/디자인 패턴

[디자인패턴]4. 유니티에서 팩토리 메소드 패턴

Heesuk Lee 2021. 7. 21. 20:43

유니티에서 객체를 생성하는 일이 많습니다. 몬스터, 장애물, 유저캐릭터 등등...

 

오늘 정리할 패턴은 위와같이 객체를 생성하는 것에 있어서 확장성이 높고 의존성을 줄일 수 있는 패턴 '팩토리 메소드 패턴'입니다.

 

이런 공장인걸까나?

 

 

팩토리 메소드 패턴의 가장 큰 핵심은 어떠한 객체를 생산하는 생산자(Creator)와 어떠한 객체를 생산할지 결정하는 구상 생산자(ConcreteCreator)의 분리 입니다.

 

게임으로 예를 들면 전체 몬스터를 스폰해주는 어떠한 클래스가 생산자(Creator)입니다. 모든 몬스터는 이 클래스에서 생산이 됩니다.

그리고 생산될 몬스터를 결정해주는 클래스가 구상 생산자(ConcreteCreator)입니다. 고블린을 생산할지, 해골을 생산할지를 정해주는 클래스입니다.

 

우선 생산자 입니다. 생산자의 특징은 추상클래스로 만들고 어떤 객체를 생성할지에 대한 결정을 내리지 않습니다.

 

// 유니티에 직접 사용하기 편하게 제네릭타입과 같이 살짝 개량했습니다.
public abstract class MonsterFactory<T> : MonoBehaviour
{
    // 외부에서 호출하는 함수 - 팩토리내부에서 처리할수있는 모든 처리는 여기서하자.
    public Monster Spawn(T _type, Transform _parent)
    {
        // 서브클래서에서 선언한 create 함수 호출 - 서브클래스와 연관된 객체를 소환
        Monster monster = this.Create(_type);
        // 매개변수로 받는 transform을 부모로 설정
        monster.transform.SetParent(_parent, false);
        // 구분을 위해 랜덤한 위치에 생성
        monster.transform.localPosition = new Vector2(Random.Range(-2f,2f), Random.Range(-2f,2f));
        return monster;
    }
    
    // 타입이 다른 몬스터 상관없이 생산
    protected abstract Monster Create(T _type);
}

 

추상클래스로 구현된 생산자 MonsterFactory에는 추상메소드 Create가 있습니다.

이부분을 구현해주는 클래스가 바로 구상 생산자입니다.

 

구상 생산자는 생산자의 서브 클래스입니다. 생산자 클래스를 상속받아 추상메소드 Create만 구현해주면 끝입니다.

 

// 고블린 종류에 대한 enum
public enum MONSTER_GOBLIN
{
    NORMAL,
    BIG,
    KING,
}

// MonsterFactory 클래스를 상속받고 Create를 구현해주면 됩니다.
public class GoblinMonsterFactory : MonsterFactory<MONSTER_GOBLIN>
{
    // 고블린과 관련된 객체를 멤버변수로 가진다.
    [SerializeField]
    private GameObject goblinPrefab = null;
    [SerializeField]
    private GameObject goblinBigPrefab = null;
    [SerializeField]
    private GameObject goblinKingPrefab = null;
    
    // MonsterFactory에서 상속받은 create 함수를 꾸며준다.
    protected override Monster Create(MONSTER_GOBLIN _type)
    {        
        Goblin goblin = null;
        switch (_type)
        {
            // 매개변수로 받은 Enum변수를 기준으로 고블린 객체 생성
            case MONSTER_GOBLIN.NORMAL :
                goblin = Instantiate(this.goblinPrefab).GetComponent<Goblin>();
                break;
            case MONSTER_GOBLIN.BIG :
                goblin = Instantiate(this.goblinBigPrefab).GetComponent<Goblin>();
                break;
            case MONSTER_GOBLIN.KING :
                goblin = Instantiate(this.goblinKingPrefab).GetComponent<Goblin>();
                break;
        }
        // 여기서 아시다시피 goblin은 Monster를 상속받습니다.
        return goblin;
    }
}

 

단순히 한 스크립트에서 구현한거랑 무엇이 다르냐고 보실수 있으시겠지만 이러한 클래스의 역할 분리가 가져올 영향은 클 수 있다고 생각합니다.

 

팩토리메소드패턴의 특징은 여기서부터 나옵니다. 고블린을 만들었으니 이제 해골 타입의 몬스터를 만들고 싶습니다. 그리고 계속 종족이 추가된다고 생각한다면 MonsterFactory를 상속받은 0000MonsterFactory 클래스를 생성하여 Create메소드만 구현해주면 됩니다.

스켈레톤 팩토리를 만들어보겠습니다.

 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public enum MONSTER_SKELETON
{
    NORMAL,
    ARCHER,
    KING
}

public class SkeletonMonsterFactory : MonsterFactory<MONSTER_SKELETON>
{
    // 스켈레톤과 관련된 객체를 멤버변수로 가진다.
    [SerializeField]
    private GameObject skeletonPrefab = null;
    [SerializeField]
    private GameObject skeletonArcherPrefab = null;
    [SerializeField]
    private GameObject skeletonKingPrefab = null;
    
    // MonsterFactory에서 상속받은 create 함수를 꾸며준다.
    protected override Monster Create(MONSTER_SKELETON _type)
    {
        Skeleton skeleton = null;
        switch (_type)
        {
            // 매개변수로 받은 Enum변수를 기준으로 스켈레톤 객체 생성
            case MONSTER_SKELETON.NORMAL :
                skeleton = Instantiate(this.skeletonPrefab).GetComponent<Skeleton>();
                break;
            case MONSTER_SKELETON.ARCHER :
                skeleton = Instantiate(this.skeletonArcherPrefab).GetComponent<Skeleton>();
                break;
            case MONSTER_SKELETON.KING :
                skeleton = Instantiate(this.skeletonKingPrefab).GetComponent<Skeleton>();
                break;
        }
        return skeleton;
    }
}

 

깔끔하게 스켈레톤을 생산할수있는 클래스가 생겼습니다.

 

또하나 좋은점이 있다면 이러한 분리로 인해 특정 몬스터 생성에 관련해서 버그가 발생했을때 해당몬스터에 관련된 구상 생산자 클래스만 확인해도 되어 시간을 많이 단축 시킬 수 있을 것이라고 생각됩니다.

 

유니티에 빈오브젝트로 만들어 필요한 프리팹을 넣었습니다. 이제 생산만 해주면 끝입니다.

 

고블린 소환은 고블린 타입 몬스터를 랜덤하게 생산하고, 해골소환은 스켈레톤 타입 몬스터를 랜덤하게 생산합니다. 

 

실제 선언부도 간단합니다.

 

public class IngameUI : MonoBehaviour
{
    [SerializeField]
    private Transform world = null;

    [SerializeField]
    private GoblinMonsterFactory goblinMonsterFactory = null;
    [SerializeField]
    private SkeletonMonsterFactory skeletonMonsterFactory = null;
    
    public void OnButtonSpawnGoblin()
    {
    	// 고블린 소환 버튼 - 랜덤하게 고블린 타입 3종류 중 하나 소환
        int randomType = Random.Range(0, 3);
        this.goblinMonsterFactory.Spawn((MONSTER_GOBLIN)randomType, this.world);
    }
    public void OnButtonSpawnSkeleton()
    {
    	// 스켈레톤 소환 버튼 - 랜덤하게 스켈레톤 타입 3종류 중 하나 소환
        int randomType = Random.Range(0, 3);
        this.skeletonMonsterFactory.Spawn((MONSTER_SKELETON)randomType, this.world);
    }
}

 

 

실제 게임에서 더 유용하고 강력하게 사용하려면 다른 패턴과 섞거나 좀더 개량을 해도 좋을 것 같습니다.

 

다시한번 정리하자면 팩토리 메소드 패턴의 핵심은 '역할의 분리'입니다. 객체를 생성하는 메소드와 생성할 객체를 결정하는 메소드의 분리로 인해 확장성과 유연성을 높이는 것이 중요하다고 생각했습니다.

 

위와 같이 예시를 구현하면서 아예 다른 타입의 몬스터그룹을 추가 구현하기도 어렵지 않고 생성과 관련해서 변경되는 어떠한 것이 생겨도 어렵지 않게 대응이 가능할 것 입니다.

 

 

사실은 같이 소개했어야할 팩토리 메소드 패턴의 친구 추상팩토리도 함께 소개해드리고 싶었지만 아직은 이해도가 낮아서 좀 더 정리한 후에 글을 올리겠습니다. 펙토리 메소드 패턴과 추상 팩토리의 차이점도 명확하게 정리해보겠습니다. 

반응형