[Unity] 유니티 커맨드 패턴 심화 ( Command Pattern ) (입력키 변경, 객체 넘기기, 실행 취소)

TNT·2023년 12월 30일
0

유니티 디자인패턴

목록 보기
11/14
post-thumbnail

이번엔 커맨드 패턴 심화 버전으로 여러가지 예시를 이용해서 보여줄 예정이다.
책에 나오는 c++ 형식을 c#으로 변경 했다.

명령 패턴은 보통 데이터를 캡슐화 해서 다른 객체로 전달 한다.
커맨드 패턴은 메서드 호출을 실체화 한것이다.

1.입력키 변경

보통 게임을 하다보면 콘솔 이나 pc 게임 같은경우는
키보드나 패드로 입력을 받는다 그리고 이경우 설정 창에서
또 자신이 원하는 스타일로 셋팅 할수도있다.

진짜 간단하게 키입력을 구현 해보자면 플레이어 컨트롤러에 구현해보자면

void Update()
{
   if (Input.GetKeyDown(KeyCode.X)) Jump();
   else if (Input.GetKeyDown(KeyCode.Y)) FireGun();
   else if (Input.GetKeyDown(KeyCode.A)) SwapWeapon();
   else if (Input.GetKeyDown(KeyCode.B)) LurchIneffectively();
}

함수는 없는 함수지만 일단 이런 느낌으로 구현될꺼다
이러게 입력 받아서 점프 공격 스왑 등등
행동을 하게된다 이런 함수는 매프레임 호출 된다.
하지만 보통 게임은 버튼을 교체 할수있다.
그러면 키변경을 지원 하려면 Jump()같은 함수를 직접 호출이 아니고
교체가능하게 바꿔야한다.

여기서 보통 명령 패턴을 이용한다.

게임에서 사용할수있는 행돌을 실행하는 공통 상위 패턴 클래스부터 만들어보자.

Command.cs

public abstract class Command
{
    public abstract void Execute();
}

이걸 받아서 처리 해줄 하위 클래스도 4개 만들자.

public class FireCommand : Command
{
    public override void Execute()
    {
        Debug.Log("FireCommand");
    }
}
public class JumpCommand : Command
{
    public override void Execute()
    {
        Debug.Log("JumpCommand");
    }
}
public class SwapCommand : Command
{
    public override void Execute()
    {
        Debug.Log("SwapCommand");
    }
}
public class LurchCommand : Command
{
    public override void Execute()
    {
        Debug.Log("LurchCommand");
    }
}

이제 입력 을 받는 플레이어 또는 입력 핸들러 코드에 각버튼 별로
Command 클래스 을 저장한다.
(C++ 에선 클래스 포인터로 저장했다.)

TestInputHandler.cs

public class TestInputHandler : MonoBehaviour
{
    private Command buttonX_;
    private Command buttonY_;
    private Command buttonA_;
    private Command buttonB_;

    void Start()
    {
        buttonX_ = new JumpCommand();
        buttonY_ = new FireCommand();
        buttonA_ = new SwapCommand();
        buttonB_ = new LurchCommand();
    }
    void Update()
    {
        if (Input.GetKeyDown(KeyCode.X)) buttonX_.Execute();
        else if (Input.GetKeyDown(KeyCode.Y)) buttonY_.Execute();
        else if (Input.GetKeyDown(KeyCode.A)) buttonA_.Execute();
        else if (Input.GetKeyDown(KeyCode.B)) buttonB_.Execute();
    }
}

이후에 입력 받을때 설정에서 바꾼다면 그냥 커맨드 를 새로 재할당 해주면
된다. 그러면 바뀐 버튼으로 입력이 될것이다.

2.오브젝트에게 지시 하기

방금 정의했던 Command 클래스는 지금은 동작에 문제 없지만
Jump FireGun등등 함수가 플레이어 캐릭터를 암시적으로 찾아서
움칙인다는 가정하에 하는것이라서 제한적이다.
이렇게 커플링이 가정에 깔려있다보니 유용성이 좀 떨어지는데
JumpCommand 는 플레이어면 가능하다.
기런 제약을 유연하게 해주려면 제어하려는 객체를 함수에서 직접 찾지말고
인자로 밖에서 전달 해주면 된다.

Command.cs

public abstract class Command
{
    public abstract void Execute(GameObject obj);
}

public class FireCommand : Command
{
    public override void Execute(GameObject obj)
    {
        obj.GetComponent<PlayerCnt>().Fire();
    }
}
public class JumpCommand : Command
{
    public override void Execute(GameObject obj)
    {
        obj.GetComponent<PlayerCnt>().Jump();
    }
}
public class SwapCommand : Command
{
    public override void Execute(GameObject obj)
    {
        obj.GetComponent<PlayerCnt>().Swap();
    }
}
public class LurchCommand : Command
{
    public override void Execute(GameObject obj)
    {
        obj.GetComponent<PlayerCnt>().Lurch();
    }
}

기존코드를 이렇게 커맨드 전달 할때 자기 오브젝트를 넣어서 준다.
여기서 GetComponent 도 싫다 하면 PlayerCnt 를 넣고 주면 된다.
하지만 유용성은 좀 떨어질수있다.

입력 받는 곳도 약간에 수정을 했다.

TestInputHandler.cs

public class TestInputHandler : MonoBehaviour
{
    private Command buttonX_;
    private Command buttonY_;
    private Command buttonA_;
    private Command buttonB_;
    
    void Start()
    {
        buttonX_ = new JumpCommand();
        buttonY_ = new FireCommand();
        buttonA_ = new SwapCommand();
        buttonB_ = new LurchCommand();
    }
    void Update()
    {
        Command command = HandleInput();
        if (command != null)
            command.Execute(gameObject);
    }

    Command HandleInput()
    {
        if (Input.GetKeyDown(KeyCode.X)) return buttonX_;
        else if (Input.GetKeyDown(KeyCode.Y)) return buttonY_;
        else if (Input.GetKeyDown(KeyCode.A)) return buttonA_;
        else if (Input.GetKeyDown(KeyCode.B)) return buttonB_;

        return null;
    }
}

이런방향으로 코드를 수정해봤다.
같이 TestInputHandler이랑 PlayerCnt이랑 같은 오브젝트에게 넣고
유니티에서 실행해서 테스트 해보자.

잘 동작하는걸 확인할수있다.
입력을 해서 오브젝트에게 변화를 줄때 이러한 커맨드 패턴을 이용해서 처리를 해줄수있다.

또한 이제 명령과 객체 사이에 한단계 둔 덕분에 명령을 실행할때
객체만 바꾸면 플레이어가 스크립트 달린곳이면 어떤 객체라도 플레이어처럼 사용 할수있다
만약에 PlayerCnt 자체도 부모로 두고 아래에 또 자식 으로 여러 타입으로 나눠서
사용한다면 더 다양하게 처리 해줄수도있다.

이런 명령 패턴은 또 ai 제어에도 효과적이다.
ai와 이를 실행하는 객체를 디커플링함으로써 코드를 더욱 유연하게 짤수있다.

또는 플레이어 자체도 Ai를 연결해서 데모플레이다 혼란디버프 처럼 알아서 공격하는
시스템도 가능 해진것이다.

3.실행 취소와 재실행

저번 포스트 에서 했던 예시지만 다시 한번 가지고 와봤다.
그래도 가지고 온 이유는 명령 패턴 사용 예시 중이에서 제일 잘 알려진 방식이라서 그렇다
명령 하는 객체가 어떤 작업을 실행할수 있다면 이실행 하는걸 다시 취소 하는것도 가능하다.

보면 전략 게임 이나 카드 게임에서 명령을 하거나 이미 한 명령을 실행 버튼을 누르지 않는한 다시 취소 하건 하기도 한다.

Command.cs에 기존 내용은 지우고 다시 만들어주자.
이유는 이제 사용 안할껀데 있으면 코드에 혼란을 줄수도 있다.
같이 못만드는건 아니니 만들어볼 사람은 같이 만들어도 좋다.

아래에 추가로 MoveUnitCommand 클래스를 상속 받아서 만들자.
Command.cs

public abstract class Command
{
    public abstract void Execute();
    public abstract void Undo();
}

public class MoveUnitCommand : Command
{
    GameObject obj;
    float x;
    float y;
    float xBefore;
    float yBefore;

    public MoveUnitCommand(GameObject obj_, float x_, float y_)
    {
        obj = obj_;
        x = x_;
        y = y_;
        xBefore = 0;
        yBefore = 0;
    }

    public override void Execute()
    {
        xBefore = x;
        yBefore = y;
         obj.GetComponent<PlayerCnt>().MoveTo(x, y);
    }
    
    public override void Undo()
    {
        obj.GetComponent<PlayerCnt>().MoveTo(xBefore, yBefore);
    }

}

다른쪽도 다시 수정해주자
달라진 점이 보인다 기존에선 추상화로 격리 시켰는데 이번에는 생성 할때 값을 받아서 클래스 에서 보관하고 그값으로
이동 하는 걸로 바인드했다.
이렇게 한다면 명형 패턴은 또 변형으로 여러가지로 사용 할수있다.
추가적으로 Undo 함수가 생겼다.

PlayerCnt.cs

public class PlayerCnt : MonoBehaviour
{
    public void MoveTo(float x_, float y_)
    {
        transform.position = new Vector3(x_, y_);
    }
}

여기에선 그냥 이동하고싶은 x y 값을 받아서 처리 했다.

TestInputHandler.cs

public class TestInputHandler : MonoBehaviour
{
    public Queue<Command> moveQueue;
    
    void Start()
    {
        moveQueue = new Queue<Command>();
    }
    void Update()
    {
        Command command = HandleInput();
        if (command != null)
        {
            moveQueue.Enqueue(command);
            command.Execute();
        }
    }

    Command HandleInput()
    {
        if (Input.GetKeyDown(KeyCode.W))
        {
            float destY = transform.position.y + 1;
            return new MoveUnitCommand(gameObject, transform.position.x, destY);
        }
        else if (Input.GetKeyDown(KeyCode.S))
        {
            float destY = transform.position.y - 1;
            return new MoveUnitCommand(gameObject, transform.position.x, destY);
        }
        else if (Input.GetKeyDown(KeyCode.Escape) && moveQueue.Count > 0)
        {
            moveQueue.Dequeue().Undo();
        }
        
        return null;
    }
}

플레이어가 입력받는 값으로 w s 위아래 이동하면 그값을 Queue 담아서 저장하고
Esc누르면 다시이전값으로 돌아가는 로직이다
지금은 위 아래만 했지만 추가만 한다면 양옆도 충분히 가능하다.
Queue 가 아닌 배열로 해서 내 현재 명령을 위치를 알고있고 값을 저장 할수있다면
재실행도 충분히 구현 가능할것이다.

4.클래스만 사용하고 함수로는 못사용하나?

앞에서 사용한 코드에서는 클래스로만 이용해서 구현했다.
하지만 함수로도 사용할수있다 예시만 그런거기 때문에 구현하는 과정에서 더 편한 방식이있다면
구현 하고 공유해주면 좋겠다..
여기서 책에서는 C++이용했기때문에 일급 함수를 제대로 지원해주지 않아서 그렇다.
상태 저장이 어려워서 그렇고 람다로 하려면 메모리 관리 까지 해줘야해서 더 복잡해진다.
클로저를 제대로 지원 해준다면 사용하는것도 좋다.
만약에 자바스크립트 로 한다고 하면

function makeMoveUnitCommand(unit, x, y) {
	return function() {
      unit.moveTo(x,y);
    }
}

또한 실행취소도

function makeMoveUnitCommand(unit, x, y) {
  var xBefore, yBefore;
	return function() {
      xBefore = unit.x();
      yBefore = unit.y();
      unit.moveTo(x,y);
    },
    undo: function() {
      unit.moveTo(xBefore, yBefore);
    }
}

뭐 이런 느낌이 될꺼같이 그냥 이건 이해안해도 상관없이 넘어가자

profile
개발

0개의 댓글