본문 바로가기
프로그래밍/디자인패턴

유니티(C#)로 사용해 보는 디자인 패턴_Command Pattern_커맨드 패턴

by 리뷰하는 (게임)프로그래머_리프TV 2020. 2. 23.

* 코딩 스터디 관련 내용은 매주 토요일 작성됩니다 *

 

유니티에 Command Pattern을 적용 시켜 보았습니다.

 

 

보통 커맨드 패턴은 유저의 입력하는 값과

실제 처리하는 과정을 나누어서 

 

유저가 원하는 입력값과 실행값을 매칭 시켜 주는 용도로 많이 사용합니다.

실제 예제에서도 그렇구요.

 

물론 이 예제에서는 여러 입력값을 쓰고있지는 않아서 그 의미전달이 조금 부족할 수도있지만,

구조는 다음과 같습니다.

 

 

* 결과 설명 및 화면

방향키, U key, D key를 누르면 오브젝트가 이동하거나 확대, 축소

Z key, R key를 통해 되돌리기, 다시 돌리기 기능이 실행 되는 예제

 

Unit.cs

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

public class Unit : MonoBehaviour
{
    int posX = 0;
    int posY = 0;
    float scaleX = 1.0f;
    float scaleY = 1.0f;

    public void MoveTo(int x, int y)
    {
        posX = x;
        posY = y;

        transform.localPosition = new Vector3(posX, 0, posY);
    }

    public void ScaleTo(float x, float y)
    {
        scaleX = x;
        scaleY = y;

        transform.localScale = new Vector3(scaleX, scaleY, 1.0f);
    }
}

 

이 유닛이라는 클래스는 실제 화면에서 오브젝트에 Add될 Script라고 생각했을 때

간단한 이동과 확대, 축소가 가능한 오브젝트입니다.

 

이 행동을 관리할 커맨트 패턴입니다.

ExecuteAction() = 실행

UndoAction() = 되돌리기

RedoAction() = 다시 돌리기

 

이렇게 메인 커맨드 패턴을 가상 함수로 세팅해놓고

여러 행동들에 대해서 CommandPattern Class를 상속받게 수정합니다.

 

예제에서는 MoveUnitCommand와 ScaleUnitCommand로 나누어봤습니다.

각각의 Command에서는 unit정보를 들고 있어야 하는 문제가 있기는 합니다만,

이 부분은 꼭 들고 있어야 하는 것은 아닙니다.

 

하지만 undo와 redo를 위해서 현재 값은 기억해 두어야 합니다.

 

CommandPattern 파일은 다음과 같습니다.

 

CommandPattern.cs

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

public class CommandPattern
{
    public virtual void ExecuteAction()
    {
        
    }

    public virtual void UndoAction()
    {

    }

    public virtual void RedoAction()
    {

    }
}

public class MoveUnitCommand : CommandPattern
{
    public int x, y;
    public int undoX, undoY;
    public Unit unit = null;

    public MoveUnitCommand(Unit _unit, int _x, int _y)
    {
        x = _x;
        y = _y;

        unit = _unit;
    }

    public override void ExecuteAction()
    {
        undoX = unit.posX;
        undoY = unit.posY;
        unit.MoveTo(x, y);
    }

    public override void UndoAction()
    {
        unit.posX = undoX;
        unit.posY = undoY;
        unit.MoveTo(undoX, undoY);
    }

    public override void RedoAction()
    {
        unit.MoveTo(x, y);
    }
}

public class ScaleUnitCommand : CommandPattern
{
    public float x, y;
    public float undoX, undoY;
    public Unit unit = null;

    public ScaleUnitCommand(Unit _unit, float _x, float _y)
    {
        x = _x;
        y = _y;

        unit = _unit;
    }

    public override void ExecuteAction()
    {
        undoX = unit.scaleX;
        undoY = unit.scaleY;
        unit.ScaleTo(x, y);
    }

    public override void UndoAction()
    {
        unit.scaleX = undoX;
        unit.scaleY = undoY;
        unit.ScaleTo(undoX, undoY);
    }

    public override void RedoAction()
    {
        unit.ScaleTo(x, y);
    }
}

 

이제 유닛과 커맨드 패턴이 준비된 상태에서

실제로 사용을 해보겠습니다.

 

CommandPattern을 List로 들고 있어서 

입력이 이루어질때 마다 하나씩 내용이 증가합니다.

이 부분은 undo, redo를 위한 리스트입니다.

 

이 부분에서 가장 중요한 함수는

InputKey 입니다. 

 

입력값이 들어 오면 호출되면서 ExecuteAction이 실행됩니다.

액션이 수행될때 마다 new 하기 때문에 

많은 액션을 기록하게 되면 점점 List가 커지는 단점이 있습니다.

이는 redo, undo는 단순히 활용일 뿐 커맨트 패턴 자체가 이를 이야기 하는 것은 아닙니다.

 

핵심은 사용할 액션에 대해 Command를 추가 하여

각각의 Command에 목적과 행동을 지정하는 것입니다.

 

그렇게 되면 유저가 원하는 입력값과 그 액션을 각각 매칭시켜

분리시킬 수 있는 장점이 있습니다.

 

 

GameManager.cs

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

public class GameManager : MonoBehaviour
{
    public Unit unit = null;
    List<CommandPattern> commandList = new List<CommandPattern>();
    int currentMoveIndex = 0;

    public void InputKey(CommandPattern pattern)
    {
        if (currentMoveIndex + 1 < commandList.Count)
        {
            commandList.RemoveRange(currentMoveIndex, commandList.Count - currentMoveIndex);
        }

        commandList.Add(pattern);
        commandList[currentMoveIndex].ExecuteAction();
        ++currentMoveIndex;
    }

    void Update()
    {
        if (unit != null)
        {
            if (Input.GetKeyDown(KeyCode.LeftArrow))
            {
                InputKey(new MoveUnitCommand(unit, unit.posX - 1, unit.posY));
            }
            else if (Input.GetKeyDown(KeyCode.UpArrow))
            {
                InputKey(new MoveUnitCommand(unit, unit.posX, unit.posY + 1));
            }
            else if (Input.GetKeyDown(KeyCode.RightArrow))
            {
                InputKey(new MoveUnitCommand(unit, unit.posX + 1, unit.posY));
            }
            else if (Input.GetKeyDown(KeyCode.DownArrow))
            {
                InputKey(new MoveUnitCommand(unit, unit.posX, unit.posY - 1));
            }
            else if (Input.GetKeyDown(KeyCode.U))
            {
                InputKey(new ScaleUnitCommand(unit, unit.scaleX + 1.0f, unit.scaleY + 1.0f));
            }
            else if (Input.GetKeyDown(KeyCode.D))
            {
                InputKey(new ScaleUnitCommand(unit, unit.scaleX - 1.0f, unit.scaleY - 1.0f));
            }
            else if (Input.GetKeyDown(KeyCode.Z))
            {
                if (currentMoveIndex > 0)
                {
                    var moveUnit = commandList[--currentMoveIndex] as CommandPattern;
                    moveUnit.undoAction();
                }
            }
            else if (Input.GetKeyDown(KeyCode.R))
            {
                if (currentMoveIndex < commandList.Count)
                {
                    var moveUnit = commandList[currentMoveIndex++] as CommandPattern;
                    moveUnit.redoAction();
                }
            }
        }
    }
}

 

 

 

* 최근 회사에서 진행하는 스터디에 복습같은 느낌으로 접근하고 있습니다. *