커맨드패턴 (Command Pattern)

티원·2026년 1월 12일

디자인패턴

목록 보기
15/16

커맨드패턴 시연영상
시연영상은 아래 코드해석해보고 정말 모르겠을때 보기. 영상보면 시간이 너무 오래걸린다

커맨드패턴

  • 행동을 저장하거나, 순서 바꾸거나, 되돌려야 할때 커맨드 패턴을 사용(키설정변경,리플레이,Undo기능)
  • 요청을 객체의 형태로 캡슐화해서 정보를 저장 혹은 로깅.
  • 실행 주체와 요청자를 분리. 요청을 저장하고 실행/취소 재실행이 가능하도록 설계

커맨드패턴의 요소

  1. Invoker: 발생자(키보드입력,게임패드입력,버튼)
  2. Command: 입력에 의해서 발동되는 행동(공격, 이동, 점프)
  3. Receiver: 입력을 보고 행동을 하는 진짜 캐릭터 코딩

코드1 (커맨드 패턴 기본)

어떻게 사용하는지 최소 구현코드로 작성.
플레이어 하나씩만 이동시킬수있는데 인보커를 갈아끼면 특정 캐릭터마다 조종권한을 얻어서 다른 캐릭터들을 움직일 수 있는 코드 작성했음
1. PlayerCtrlBasic.cs

using UnityEngine;

//커맨드 패턴중 Receiver에 해당
[RequireComponent(typeof(Rigidbody))]
public class PlayerCtrlBasic : MonoBehaviour
{
    Rigidbody rb;
    Animator animator;

    Vector3 moveDir;

    private void Start()
    {
        rb = GetComponent<Rigidbody>();
        animator = GetComponent<Animator>();
    }

    private void Update()
    {
        if (moveDir != Vector3.zero)
        {
            transform.rotation = Quaternion.LookRotation(moveDir);
            transform.Translate(Vector3.forward * Time.deltaTime * 4f);
        }
    }
    //문제점: 이 캐릭터를 AI가 조종해야 한다면?(CC기 걸리거나 컷씬이 나와서 이 캐릭터가 공격 모션을 해야 한다면?
    //네트워크에서 다른 사람이 이 캐릭터를 조종하게 하려면? 아니면 클릭한 캐릭터를 조종하고싶다면?
    //이런 문제들 때문에 커맨드 패턴으로 "명령을 받는 형태"로 만들거임
    public void Move(Vector3 dir)
    {
        moveDir = dir;
        animator.SetFloat("MoveFloat", dir.magnitude);
    }

    public void Attack()
    {
        animator.SetTrigger("AttackTrigger");
    }
    public void Jump()
    {
        rb.AddForce(Vector3.up * 5f, ForceMode.Impulse);
    }
}
  1. CommandPatternBasic.cs
    커맨드 패턴 명령 클래스들을 모아둔 cs파일
using UnityEngine;

//커맨트패턴중 Command에 해당
//기본 커맨드 패턴 정리할것이므로 기본코드에는 ExecuteReplay빼고 정리하기
public interface ICommandBasic
{
    void Execute();
}
public class MoveCommandBasic : ICommandBasic
{
    PlayerCtrlBasic player;  //명령 수행 대상
    Vector3 direction;  //명령 수행에 필요한 정보

    public MoveCommandBasic(PlayerCtrlBasic plr, Vector3 dir)
    {
        player = plr;
        direction = dir;
    }

    public void Execute()
    {
        player.Move(direction);
    }
}

public class JumpCommandBasic : ICommandBasic
{
    PlayerCtrlBasic player;
    public JumpCommandBasic(PlayerCtrlBasic plr)
    {
        player = plr;
    }
    public void Execute()
    {
        player.Jump();
    }
}
public class AttackCommandBasic : ICommandBasic
{
    PlayerCtrlBasic player;
    public AttackCommandBasic(PlayerCtrlBasic plr)
    {
        player = plr;
    }
    public void Execute()
    {
        player.Attack();
    }
}
  1. InputHandlerBasic.cs
using UnityEngine;
using UnityEngine.InputSystem;

//커맨드 패턴중 Invoker. 키입력 등 커맨드를 찍어낼 곳
public class InputHandlerBasic : MonoBehaviour
{
    [SerializeField] PlayerCtrlBasic player; //제어를 할 캐릭터 하나

    private void Awake()
    {
        InputSystem.actions["Move"].performed += OnMove;
        InputSystem.actions["Move"].canceled += OnMove;
        InputSystem.actions["Attack"].performed += OnAttack;
        InputSystem.actions["Jump"].performed += OnJump;
    }
    private void OnDestroy()
    {
        InputSystem.actions["Move"].performed -= OnMove;
        InputSystem.actions["Move"].canceled -= OnMove;
        InputSystem.actions["Attack"].performed -= OnAttack;
        InputSystem.actions["Jump"].performed -= OnJump;
    }

    private void OnMove(InputAction.CallbackContext ctx)
    {
        Vector2 input = ctx.ReadValue<Vector2>();
        Vector3 dir = new Vector3(input.x, 0, input.y);

        //커맨드 패턴을 가져옴 (아래 3줄 주석된거 record개념 추가전에 있던 코드, 커맨드패턴)
        ICommandBasic moveCommand = new MoveCommandBasic(player, dir);
        moveCommand.Execute();
    }
    private void OnAttack(InputAction.CallbackContext ctx)
    {
        if (!ctx.performed) return;
        ICommandBasic attackCommand = new AttackCommandBasic(player);
        attackCommand.Execute();
    }
    private void OnJump(InputAction.CallbackContext ctx)
    {
        if (!ctx.performed) return;
        ICommandBasic jumpCommand = new JumpCommandBasic(player);
        jumpCommand.Execute();
    }
}

코드2 (record 기능. timestamp)

응용(TimeStamp 이용해서 record기능을 추가한 코드도 만들자)

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

class TimedCommad
{
    public float time;
    public ICommandBasic command;
}

//커맨드 패턴중 Invoker. 키입력 등 커맨드를 찍어낼 곳
public class InputHandler : MonoBehaviour
{
    [SerializeField] PlayerCtrlBasic player; //제어를 할 캐릭터 하나

    //timeStamp
    float _recordStartTime = 0f;
    Vector3 _initPosition;
    List<float> timeList = new List<float>();

    //실제 TimeStamp관련 사용할거라면 아래처럼
    bool isRecording = false;
    List<TimedCommad> commandRecord = new List<TimedCommad>();
    private void Awake()
    {
        InputSystem.actions["Move"].performed += OnMove;
        InputSystem.actions["Move"].canceled += OnMove;
        InputSystem.actions["Attack"].performed += OnAttack;
        InputSystem.actions["Jump"].performed += OnJump;
    }
    private void OnDestroy()
    {
        InputSystem.actions["Move"].performed -= OnMove;
        InputSystem.actions["Move"].canceled -= OnMove;
        InputSystem.actions["Attack"].performed -= OnAttack;
        InputSystem.actions["Jump"].performed -= OnJump;
    }

    private void OnMove(InputAction.CallbackContext ctx)
    {
        Vector2 input = ctx.ReadValue<Vector2>();
        Vector3 dir = new Vector3(input.x, 0, input.y);

        //커맨드 패턴을 가져옴 (아래 3줄 주석된거 record개념 추가전에 있던 코드, 커맨드패턴)
        ICommandBasic moveCommand = new MoveCommandBasic(player, dir);
        moveCommand.Execute();
        //기록
        if(isRecording)
        {
            TimedCommad timedCommad = new TimedCommad();
            timedCommad.time = Time.time;
            timedCommad.command = moveCommand;
            commandRecord.Add(timedCommad);
        }
        
    }
    private void OnAttack(InputAction.CallbackContext ctx)
    {
        if (!ctx.performed) return;
        ICommandBasic attackCommand = new AttackCommandBasic(player);
        attackCommand.Execute();
        //기록
        if (isRecording)
        {
            TimedCommad timedCommad = new TimedCommad();
            timedCommad.time = Time.time;
            timedCommad.command = attackCommand;
            commandRecord.Add(timedCommad);
        } 
    }
    private void OnJump(InputAction.CallbackContext ctx)
    {
        if (!ctx.performed) return;
        ICommandBasic jumpCommand = new JumpCommandBasic(player);
        jumpCommand.Execute();
        //기록
        if (isRecording)
        {
            TimedCommad timedCommad = new TimedCommad();
            timedCommad.time = Time.time;
            timedCommad.command = jumpCommand;
            commandRecord.Add(timedCommad);
        }
    }

    //시작 버튼 클릭하면 시간과 위치 기록(버튼으로 등록)
    public void StartRecordingUseTimesTamp()
    {
        isRecording = true;
        _recordStartTime = Time.time;
        _initPosition = player.transform.position;
        Debug.Log(_recordStartTime);
    }
    //Stop
    public void StopRecordingUseTimesTamp()
    {
        isRecording = false;
    }
    //재생버튼. 재생하면 코루틴으로 시간에 따라 리플레이처럼 실행한다
    public void PlayRecordionUseTimesTamp()
    {
        isRecording = false;
        StartCoroutine(playerRecordCor());
    }
    IEnumerator playerRecordCor()
    {
        player.transform.position = _initPosition;
        //실행시킨 시간
        for(int i=0; i< commandRecord.Count; i++)
        {
            float delayTime = 0;
            if (i == 0) delayTime = commandRecord[i].time;
            else delayTime = commandRecord[i].time - commandRecord[i - 1].time;
            yield return new WaitForSeconds(delayTime);
            //기다린후 실행
            Debug.Log("동작 실행");
            commandRecord[i].command.Execute();
        }
    }
}

0개의 댓글