• Tistory
    • 태그
    • 위치로그
    • 방명록
    • 관리자
    • 글쓰기
Carousel 01

[디자인패턴] 명령 패턴, 커맨드 패턴 (Command Pattern)

Programming/디자인패턴 2020. 6. 30. 22:58

◎ 정의

커맨드 패턴(Command pattern)이란 요청을 객체의 형태로 캡슐화하여 사용자가 보낸 요청을 나중에 이용할 수 있도록 매서드 이름, 매개변수 등 요청에 필요한 정보를 저장 또는 로깅, 취소할 수 있게 하는 패턴이다.

- 출처 : 위키백과

 

- 한마디로 쉽게 말하자면 메서드의 호출을 객체로 감싸서(캡슐화) 관리하는 패턴이다.

 

- 가령 캐릭터의 조작을 위한 기능을 구현할 때 가장 간단하게는 아래와 같이 작성할 것이다.

 

< Hero.cs >

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

public class Hero : MonoBehaviour
{
    private void Update()
    {
        CheckInputKey();
    }

    // 캐릭터 이동
    void CheckInputKey()
    {
        if (Input.GetKey("a")) Attack();
        else if (Input.GetKey("s")) Defend();
        else if (Input.GetKey("d")) Jump();
        else if (Input.GetKey("f")) Dash();
    }

    void Attack()
    {
        Debug.Log("캐릭터가 공격함.");
        
        // 캐릭터 공격에 관한 처리
        //...
    }

    void Defend()
    {
        Debug.Log("캐릭터가 방어함.");

        // 캐릭터 방어에 관한 처리
        //...
    }

    void Jump()
    {
        Debug.Log("캐릭터가 점프함.");

        // 캐릭터 점프에 관한 처리
        //...
    }

    void Dash()
    {
        Debug.Log("캐릭터가 돌진함.");

        // 캐릭터 돌진에 관한 처리
        //...
    }
}

- 사용자가 입력키를 변경하는 기능을 지원하지 않는다면 위와 같이 작성해도 무방하겠지만 추후에 키 변경을

지원해야 한다면 MoveFoward() 나 MoveRight()와 같은 함수를 직접 호출하는 것이 아닌 특정 행동을 갖는

객체를 이용하여 동작하게끔 구현할 필요가 있다. 

또한 객체로 관리하므로 명령어 실행은 물론, 실행한 명령어 저장, 취소, 재실행과 같은 처리 기능의 추가도 가능해진다.


◎ 활용

 

- 요청을 큐에 저장하는 방식으로 작업큐나 스케줄러와 같은 작업에 적용 가능

- 작업 내용을 로그로 기록하여 프로그램 실행 도중 에러 발생 시 롤백하는 기능에 적용

- 명령어 실행의 저장이 가능하므로 작업이 요청된 시점과 수행되는 시점을 분리하고자 할 때 응용이 가능

 


◎ 장점 / 단점

 

○ 장점

- 특정 작업에 대한 요청과 실제 작업을 수행하는 객체가 분리되어있으므로 

시스템의 결합도가 낮아 확장성에 유리한 면을 갖는다. 즉 각 동작에 대한 객체들이 수정되어도 다른 객체가 영향을 받지 않는다.

 

○ 단점

- 각각의 명령에 대한 기능이 될 때마다 클래스를 추가해야 하므로 클래스의 개수가 무수히 늘어날 수 있다.

 


◎ 구현

 

○ 기본적인 커맨드 패턴의 적용

 

< Command.cs >

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

public class Command
{
    public virtual void Execute() { }
}

public class CommandAttack : Command
{
    public override void Execute()
    {
        Attack();
    }

    void Attack()
    {
        Debug.Log("캐릭터가 공격함.");

        // 캐릭터 공격에 관한 처리
        //...
    }
}

public class CommandDefend : Command
{
    public override void Execute()
    {
        Defend();
    }

    void Defend()
    {
        Debug.Log("캐릭터가 방어함.");

        // 캐릭터 방어에 관한 처리
        //...
    }
}

public class CommandJump : Command
{
    public override void Execute()
    {
        Jump();
    }

    void Jump()
    {
        Debug.Log("캐릭터가 점프함.");

        // 캐릭터 점프에 관한 처리
        //...
    }
}

public class CommandDash : Command
{
    public override void Execute()
    {
        Dash();
    }

    void Dash()
    {
        Debug.Log("캐릭터가 돌진함.");

        // 캐릭터 돌진에 관한 처리
        //...
    }
}

- 위의 <Hero.cs>에서 키 입력을 체크하는 부분에 캐릭터의 동작(Attack, Jump 등)을 한 번에 정의했던 구조와 달리 Command라는 부모 클래스를 선언하고 각각의 동작들은 Command 클래스를 상속받아서

Execute()를 재정의하여 각각의 기능이 실행되는 구조로 바꾸었다. 

 

< Hero.cs >

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

public class Hero : MonoBehaviour
{
    // 사용할 각각의 버튼 객체
    Command button_A, button_S, button_D, button_F;

    private void Start()
    {
        InitCommand();
    }

    // 각 버튼의 객체에 적용할 클래스(기능)를 할당
    void InitCommand()
    {
        button_A = new CommandAttack(); 
        button_S = new CommandDefend(); 
        button_D = new CommandJump(); 
        button_F = new CommandDash(); 
    }

    private void Update()
    {
        // InitCommand()에서 할당 된 버튼객체의 Execute 메서드를 실행
        if (Input.GetKey("a")) button_A.Execute();
        else if (Input.GetKey("s")) button_S.Execute();
        else if (Input.GetKey("d")) button_D.Execute();
        else if (Input.GetKey("f")) button_F.Execute();
    }
}

- 여기까지 커맨드 패턴을 적용시킬 경우 잘동작 하지만 해당 조작 기능을 적용시키는 주체가 고정되어있기 때문에

AttackCommand나 JumpCommand는 오로지 지정한 캐릭터만 동작하게 할 수 있으므로  Command 클래스의 유용성이 떨어진다. 

이런 제약에서 벗어나기 위해서 제어하려는 객체를 함수의 파라미터로 받아서 연결시켜주는 작업이 필요하다.

 

 

○ 기본적인 커맨드 패턴 + Actor 추가

 

< Hero.cs >

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

public class Actor
{
    public void Attack()
    {
        Debug.Log("캐릭터가 공격함.");

        // 캐릭터 공격에 관한 처리
        //...
    }

    public void Defend()
    {
        Debug.Log("캐릭터가 방어함.");

        // 캐릭터 방어에 관한 처리
        //...
    }

    public void Jump()
    {
        Debug.Log("캐릭터가 점프함.");

        // 캐릭터 점프에 관한 처리
        //...
    }

    public void Dash()
    {
        Debug.Log("캐릭터가 돌진함.");

        // 캐릭터 돌진에 관한 처리
        //...
    }
}

public class Hero : MonoBehaviour
{
    // 사용할 각각의 버튼 객체
    Command button_A, button_S, button_D, button_F;
    // 액터 추가
    Actor actor;

    private void Start()
    {
        actor = new Actor();
        InitCommand();
    }

    // 각 버튼의 객체에 적용할 클래스(기능)를 할당
    void InitCommand()
    {
        button_A = new CommandAttack(); 
        button_S = new CommandDefend(); 
        button_D = new CommandJump(); 
        button_F = new CommandDash(); 
    }

    Command GetCommand()
    {
        // InitCommand()에서 할당 된 커맨드 객체를 반환
        if (Input.GetKey("a")) return button_A;
        else if (Input.GetKey("s")) return button_S;
        else if (Input.GetKey("d")) return button_D;
        else if (Input.GetKey("f")) return button_F;
        else return null;
    }

    private void Update()
    {
        Command command = GetCommand();
        if(command != null)
        {
            command.Execute(actor);
        }
    }   
}

 

< Command.cs >

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

public class Command
{
    public virtual void Execute(Actor actor) { }
}

public class CommandAttack : Command
{
    public override void Execute(Actor actor)
    {
        actor.Attack();
    }
}

public class CommandDefend : Command
{
    public override void Execute(Actor actor)
    {
        actor.Defend();
    }
}

public class CommandJump : Command
{
    public override void Execute(Actor actor)
    {
        actor.Jump();
    }
}

public class CommandDash : Command
{
    public override void Execute(Actor actor)
    {
        actor.Dash();
    }
}

- Actor 클래스를 추가하고 GetCommand()에서 각각의 버튼에 할당된 Command 객체를 반환시키고 Command 클래스를 상속받은 자식 클래스 안에 재정의 된 Execute 함수에서 파라미터로 넘겨받은 Actor 객체를 이용하여 원하는 기능을 가진 함수를 호출한다.

 

- Actor 클래스의 추가로 Actor만 바꾸면 유저가 키 조작으로 어떤 Actor든 조작이 가능하도록 되었다. 또한

유저가 조작하지 않는 AI 캐릭터의 조작도 같은 명령패턴으로 AI코드에서 원하는 Command 객체를 사용하도록 응용이

가능하다. 가령 AI 캐릭터에게 HP가 일정수준으로 줄어들 경우 더 공격적인 광폭화 모드를 추가하고 싶을 경우 해당 

공격명령을 추가해서 사용하면 된다.

 

- 더 나아가서 플레이어 캐릭터에 AI를 연결하여 자동실행되는 데모 모드 구현도 응용이 가능할 것이다.

 

 

○ 기본적인 커맨드 패턴 + Actor 추가 + Undo(실행취소) 기능 추가

 

- 커맨드 패턴 사용 예 중에서도 가장 잘 알려져 있는 Undo기능을 추가해보고자 한다. 

 

< Command.cs >

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

public class Command
{
    public virtual void Execute(Actor actor, Vector3 movePos) { }
    public virtual void Undo(Actor actor) { }   // Undo  추가
}

public class CommandMove : Command
{
    Vector3 prevPos = Vector3.zero;
    public override void Execute(Actor actor, Vector3 movePos)
    {
        prevPos = actor.transform.position;
        actor.Move(movePos);
        Debug.Log("캐릭터 이동 좌표 --> " + movePos);
    }

    public override void Undo(Actor actor)
    {
        actor.Move(prevPos);
    }
}

 

< Hero.cs >

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

public class Actor
{
    public Vector3 pos = new Vector3(0, 0, 0);
    public Transform transform;

    public Actor(Transform tr)
    {
        transform = tr;
    }

    // 캐릭터를 이동시키는 함수
    public void Move(Vector3 pos)
    {
        transform.position = pos;
    }
}

public class Hero : MonoBehaviour
{
    // 사용할 각각의 버튼 객체
    Command button_A, button_S, button_D, button_F;
    // 액터 추가
    Actor actor;
    // 액터의 Command를 담는 스택 선언
    Stack<Command> stack = new Stack<Command>();
    // Undo 키 체크용도 Flag
    bool isPushUndoKey = false;

    private void Start()
    {
        actor = new Actor(gameObject.transform);
    }

    Command GetCommand()
    {
        // Undo 키를 눌렀을 때 처리
        if (Input.GetKeyDown("z"))
        {
            isPushUndoKey = true;
            if(stack.Count > 0)
            {
                return stack.Pop();
            }
        }

        // Undo 키가 눌리지 않았다면 이동 처리
        if (Input.GetKeyDown("w"))
        {
            Command command = new CommandMove();
            stack.Push(command);
            return command;
        }

        return null;

    }

    private void Update()
    {
        isPushUndoKey = false;
        Command command = GetCommand();
        if(command != null)
        {
            if (isPushUndoKey)
            {
                command.Undo(actor);
            }
            else
            {
                command.Execute(actor, new Vector3(0, Random.Range(0, 10f), 0));
            }
        }
    }   
}

- 스택에  실행했던 Command 객체를 저장하고 Undo 할 때 하나씩 꺼내서 처리하면 커맨드 패턴을 이용한 Undo의

기능 적용이 가능하다.

 

- Undo 기능을 응용하여 Redo(재실행) 기능도 구현이 가능하다. 사용자가 실행한 명령 리스트를 저장하고 Undo나 Redo 요청에

따라 현재 표시할 명령 목록으로 이동하면 된다.

 

- Redo 기능은 게임에서 리플레이 시스템에도 응용이 가능하다.

리플레이 시스템을 구현할 때 매 프레임마다 게임상황 전체를 저장하기엔 메모리가 너무 많이 필요하므로 대신 전체 개체가 실행하는 명령 모두를 저장하고 리플레이 시에 저장한 명령들을 순차적으로 실행하는 방식으로 구현이 가능하다.

'Programming > 디자인패턴' 카테고리의 다른 글

[디자인패턴] 관찰자 패턴 (Observer Pattern)  (0) 2020.07.02
[디자인패턴] 경량 패턴, 플라이웨이트 패턴 (Flyweight Pattern)  (0) 2020.07.01
블로그 이미지

BlackTopaz

e-mail : vluebear@naver.com

,

카테고리

  • 분류 전체보기 (80)
    • Programming (3)
      • C# (0)
      • Unity (0)
      • 디자인패턴 (3)
    • 프로그래밍 문제 풀이 (75)
      • C# (49)
      • MySQL (26)
    • 개발 일지 (0)
      • Project_EDM (0)
    • 디버깅 & 오류 해결 로그 (2)
      • Unity (2)

태그목록

  • 프로그래머스
  • C#
  • Unity Error
  • 유니티
  • 프로그래밍 문제 풀이
  • Unity
  • 게임프로그래밍
  • 유니티 에러
  • mysql
  • 디자인패턴

글 보관함

달력

«   2025/07   »
일 월 화 수 목 금 토
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 31

링크

BlackTopaz

블로그 이미지

e-mail : vluebear@naver.com

LATEST FROM OUR BLOG

RSS 구독하기

LATEST COMMENTS

BLOG VISITORS

  • Total :
  • Today :
  • Yesterday :

Copyright © 2015 Socialdev. All Rights Reserved.

티스토리툴바