게임 개발/디자인 패턴

[디자인패턴] 8. 유니티에서의 커맨드 패턴

Heesuk Lee 2021. 9. 23. 16:59

꿀같은 추석시즌동안 너무 행복하게 쉬었답니다.

추석이다ㅎㅎ

추석이 전부 가버리기 전에 지난주에 공부했던 커맨드 패턴에 대해 정리해보겠습니다.

 

커맨드 패턴 핵심

요청하는 객체와 요청을 수행하는 객체를 분리한다.

커맨드 객체의 Execute()를 호출한다.

작업취소기능도 지원할 수 있다. (구현할때는 사용하지 않았습니다.)

아래 객체지향의 원칙을 따른다.

 

객체지향의 원칙

바뀌는 부분을 캡슐화한다.

상속보다는 구성을 활용한다.

구현이 아닌 인터페이스에 맞춰서 프로그래밍한다.

서로 상호작용하는 객체사이에서 가능하면 느슨하게 결합하는 디자인을 사용한다.

확장에는 열려있지만 변경에는 닫혀있어야한다.

추상화된것에 의존하라 구상클래스에 의존하지 않도록 한다.

 

유니티에서 커맨드 패턴 사용

유니티에서 커맨드 패턴을 사용하기 적절한 곳은 아무래도 캐릭터 행동을 제어하는 부분이지 않을까 생각했습니다.

키보드를 눌러 행동을 제어하는 부분에서 행동을 캡슐화하여 좀더 확장성있게 변화에 열린 코드가 되는것이 최종형태가 될 것 같습니다.

 

// 실제 개발할 코드기반에 적용을 해서 일부분만 적용해서 사용하는 등 다소 불친절한 코드가 되어버렸습니다ㅠ

첫번째로 명령을 담당하는 커맨드 인터페이스와 상속받을 커맨드들을 구현해보겠습니다.

// 커맨드 인터페이스
public interface Command
{
    void Execute();
}

// 구현된 커맨드들 - 공격, 이동, 점프
public class CommandAttack : Command
{
    private Actor actor = null;
    
    public CommandAttack(Actor _actor)
    {
        this.actor = _actor;
    }
    public void Execute()
    {
        this.actor.Attack();
    }
}

public class CommandMove : Command
{
    private Actor actor = null;
    private Vector2 direction = new Vector2();
    private string directionText = string.Empty;
    
    public CommandMove(Actor _actor, DIRECTION _direction)
    {
        this.actor = _actor;
        switch (_direction)
        {
            case DIRECTION.UP:
                this.directionText = "UP";
                this.direction = Vector2.up;
                break;
            case DIRECTION.DOWN:
                this.directionText = "DOWN";
                this.direction = Vector2.down;
                break;
            case DIRECTION.LEFT:
                this.directionText = "LEFT";
                this.direction = Vector2.left;
                break;
            case DIRECTION.RIGHT:
                this.directionText = "RIGHT";
                this.direction = Vector2.right;
                break;
            default:
                this.directionText = "NONE";
                this.direction = Vector2.zero;
                break;
        }
    }
    public void Execute()
    {
        this.actor.transform.Translate(direction * this.actor.moveSpeed * Time.deltaTime);
    }
}

public class CommandJump :Command
{
    private Actor actor = null;
    
    public CommandJump(Actor _actor)
    {
        this.actor = _actor;
    }
    public void Execute()
    {
        this.actor.GetRigid().AddForce(Vector2.up * this.actor.jumpSpeed, ForceMode2D.Impulse);
    }
}

 

다음은 위에 구현된 명령, 커맨드를 받을 대상 actor를 구현하고 actor를 상속받을 Player를 구현합니다.

// 움직이는 대상이 공통적으로 가지고있는 요소 제어
public abstract class Actor : MonoBehaviour
{
    private Collider2D bodyCollider = null;
    private Rigidbody2D bodyRigid = null;

    public float jumpSpeed = 100f;
    public float moveSpeed = 0.1f;
    
    private void Awake()
    {
        this.bodyRigid = this.GetComponent<Rigidbody2D>();
        this.bodyCollider = this.GetComponent<Collider2D>();
    }
    public Collider2D GetCollider()
    {
        return this.bodyCollider;
    }
    public Rigidbody2D GetRigid()
    {
        return this.bodyRigid;
    }

    // 변동의 요소가 있는 함수는 확장성을 가지도록 구현
    public abstract void Attack();
    public abstract void PowerAttack();
}

// 실제 움직이는 대상 - Player
public class Player : Actor
{
    public override void Attack()
    {
        // 미구현 상태
    }

    public override void PowerAttack()
    {
        // 미구현 상태
    }
}

 

 

위에 커맨드들과 명령을 받을 대상이 구현이 되었으니 이제 실제 구현부 PlayerController를 살펴보겠습니다.

// 유니티에서 키값 받는 타입 구분
public enum GET_KEY_TYPE
{
    PRESSED,
    DOWN,
    UP,
    NONE,
}

// 사용될 키값 구분
public enum COMMAND_KEY
{
    ARROW_UP,
    ARROW_DOWN,
    ARROW_LEFT,
    ARROW_RIGHT,
    Z,
    X,
    C,
    A,
    S,
    D,
    NONE,
}

// Player의 행동을 제어하는 클래스
public class PlayerController : MonoBehaviour
{
	// 배열,리스트 보다 편하게 작성이 가능할 것 같아서 딕셔너리 사용
    [SerializeField]
    private Dictionary<COMMAND_KEY, Command> commands = new Dictionary<COMMAND_KEY, Command>();
    [SerializeField]
    private Actor player = null;

    private void Awake()
    {
        this.InitCommand();
    }

    private void InitCommand()
    {
        // 커맨드 키 초기화 후 입력 초기화
        this.commands = new Dictionary<COMMAND_KEY, Command>();
        // 이동 커맨드 추가
        this.commands.Add(COMMAND_KEY.ARROW_UP, new CommandMove(this.player, DIRECTION.UP));
        this.commands.Add(COMMAND_KEY.ARROW_DOWN, new CommandMove(this.player, DIRECTION.DOWN));
        this.commands.Add(COMMAND_KEY.ARROW_LEFT, new CommandMove(this.player, DIRECTION.LEFT));
        this.commands.Add(COMMAND_KEY.ARROW_RIGHT, new CommandMove(this.player, DIRECTION.RIGHT));
        // 공격 커맨드 추가
        this.commands.Add(COMMAND_KEY.Z, new CommandAttack(this.player));
        // 막기 커맨드 추가
        this.commands.Add(COMMAND_KEY.X, new CommandBlock(this.player));
        // 점프 커맨드 추가
        this.commands.Add(COMMAND_KEY.C, new CommandJump(this.player));
    }

    private void Update()
    {
    	// 키가 눌리고 있지않으면 반환
        if(!Input.anyKey) return;
        // 등록한 모든키 체크
        this.InputGetDownKey(KeyCode.UpArrow, GET_KEY_TYPE.PRESSED);
        this.InputGetDownKey(KeyCode.DownArrow, GET_KEY_TYPE.PRESSED);
        this.InputGetDownKey(KeyCode.LeftArrow, GET_KEY_TYPE.PRESSED);
        this.InputGetDownKey(KeyCode.RightArrow, GET_KEY_TYPE.PRESSED);
        this.InputGetDownKey(KeyCode.Z, GET_KEY_TYPE.DOWN);
        this.InputGetDownKey(KeyCode.X, GET_KEY_TYPE.DOWN);
        this.InputGetDownKey(KeyCode.C, GET_KEY_TYPE.DOWN);
        this.InputGetDownKey(KeyCode.A, GET_KEY_TYPE.DOWN);
        this.InputGetDownKey(KeyCode.S, GET_KEY_TYPE.DOWN);
        this.InputGetDownKey(KeyCode.D, GET_KEY_TYPE.DOWN);
    }

    private void InputGetDownKey(KeyCode _keyCode, GET_KEY_TYPE _keyType)
    {
        // 어떤 키값 호출 분기
        COMMAND_KEY commandKey = COMMAND_KEY.NONE;
        switch (_keyCode)
        {
            case KeyCode.UpArrow:
                commandKey = COMMAND_KEY.ARROW_UP;
                break;
            case KeyCode.DownArrow:
                commandKey = COMMAND_KEY.ARROW_DOWN;
                break;
            case KeyCode.LeftArrow:
                commandKey = COMMAND_KEY.ARROW_LEFT;
                break;
            case KeyCode.RightArrow:
                commandKey = COMMAND_KEY.ARROW_RIGHT;
                break;
            case KeyCode.Z:
                commandKey = COMMAND_KEY.Z;
                break;
            case KeyCode.X:
                commandKey = COMMAND_KEY.X;
                break;
            case KeyCode.C:
                commandKey = COMMAND_KEY.C;
                break;
            case KeyCode.A:
                commandKey = COMMAND_KEY.A;
                break;
            case KeyCode.S:
                commandKey = COMMAND_KEY.S;
                break;
            case KeyCode.D:
                commandKey = COMMAND_KEY.D;
                break;
            default:
                commandKey = COMMAND_KEY.NONE;
                break;
        }

        // 키 활성화 타입 분기
        bool isEnabledKey = false;
        switch (_keyType)
        {
            case GET_KEY_TYPE.PRESSED:
                isEnabledKey = Input.GetKey(_keyCode);
                break;
            case GET_KEY_TYPE.DOWN:
                isEnabledKey = Input.GetKeyDown(_keyCode);
                break;
            case GET_KEY_TYPE.UP:
                isEnabledKey = Input.GetKeyUp(_keyCode);
                break;
            default:
                isEnabledKey = false;
                break;
        }

        if(isEnabledKey && commandKey != COMMAND_KEY.NONE && this.commands.ContainsKey(commandKey))
        {
        	// 커맨드 execute 호출
            this.commands[commandKey].Execute();
        }
    }

    private void OnDestroy()
    {
        // 비활성화
        this.commands.Clear();
    }
}

 

 

다음은 위에 코드를 적용시켜 간단하게 명령을 받는 형태를 실행시킨 모습입니다.

이동과 점프만 구현했다..ㅎㅎ

 

실제로 연습중인 토이프로젝트에 적용하느라 다소 불친절한 코드가 됐다는부분이 조금 아쉬웠습니다.

 

예를들면 위에 코드에서 완전하게 커맨드패턴의 특징을 따르지 못한 부분이 있었는데 바로 CommandJump와 CommandMove이다.

패턴의 특징중 하나인 요청을 하는 객체와 요청을 수행하는 객체의 분리를 다른 커맨드와는 다르게 Jump, Move같은 경우에 커맨드에 수행하는 객체를 참조하도록 되어있는것을 볼 수 있습니다.

 

이부분에서는 다른 행동은 각 Actor마다 다르게 적용될 가능성이 있어 열어두었지만,

이동과 점프에 대해서는 절대적으로 가지고있다고 생각되어서 좀더 편하고자 혹은 파악하기 쉽고자 변형해서 구현했습니다.

이와같이 패턴의 이론적인 부분과 패턴을 실제 사용할때에는 갭이 존재할 수 있다는 것을 새삼 깨닫게되는 패턴이었습니다.

 

너무 패턴에 강하게 의존하지 않고 느슨하고 유연하게 사용 할 수 있어야겠다는 생각을 했습니다.

이것마저 느슨한 결합을?

 

그러면 오늘은 여기까지 정리를 마치겠습니다~

다음주에 또 만나요

 

반응형