본문 바로가기
게임 개발(유니티)/멋쟁이 사자처럼 3기_회고록

유니티 디자인패턴_Command 패턴

by goraku97 2025. 1. 8.

Command 패턴

 

행위(Behavioral) 디자인 패턴 중 하나로, 작업 요청을 캡슐화하여

요청자(Invoker)와 수행자(Receiver)를 분리하는 것을 목표. 명령객체(중개자)를 적극적으로 사용.

- 요청자 : 일을 해달라고 명령을 내리는 역할(ex. 리모컨의 버튼)

- 수행자 : 실제로 일을 처리하는 역할(ex. TV본체)

- 명령객체(중개자) : "볼륨을 올려라"라는 메시지를 담고 있는 캡슐화된 객체

어째서 분리를 할까?

- 보통은 요청자(Invoker)가 수행자(Receiver)에게 직접 명령을 내림.

문제점 :

- 하지만, 요청자와 수행자가 너무 긴밀하게 연결되어 있으면,

수행자의 내부 구현(: TV가 어떻게 작동하는지)이 바뀌면 요청자도 수정이 필요

- 요청자가 수행자와 직접 연결되어 있으면, 같은 요청을 다른 수행자로 보내거나

작업을 취소/재실행하는 기능을 추가하기 어려움.

 

해결법 :

작업 요청을 캡슐화한다는 건 명령을 하나의 독립된 객체로 만드는 것

이 명령 객체는 요청자와 수행자 사이의 "중개자" 역할을 수행.

- 요청자는 명령 객체(중개자)를 통해 작업을 요청

- 명령 객체(중재자)는 수행자를 알고 있으며, 요청자가 어떤 일을 하라고 했는지

   구체적인 정보를 가지고 있음.

 

이렇게 하면 요청자와 수행자가 직접 연결되지 않으므로

서로 독립적으로 변경이 가능

 

스크립트 해석:

PlayerController.cs
0.00MB

 

PlayerController 스크립트

( Invoker 역할을 수행)

 

- static : 게임 전체에서 공통적으로 사용하는 데이터를 저장할때 또는

              변하지 않는 값은의할때 사용. 이는 싱글톤 패턴을 구현하는 데 사용

// static 키워드로 선언된 instance는 클래스 레벨에서 하나의 공유 인스턴스를 가짐
// 싱글톤은 이 클래스의 인스턴스를 전역적으로 접근 가능하게 하며, PlayerController가 게임 내에서 하나만 존재하도록 보장
public static PlayerController instance;

 

Dictionary 의 기본 문법 구조

Dictionary<T Key, T Value> dictionaryName = new Dictionary<TKey, TValue>

// key 에는 데이터 타입((: string, int, float )) ,

   Value 에는 값의 데이터 타입(: KeyCode, string, int )을 지정.

 이동 키를 관리하기 위한 딕셔너리
// 키(문자열)와 Unity의 KeyCode 값을 연결
private Dictionary<string, KeyCode> moveKeys = new Dictionary<string, KeyCode>
{
    { "Forward", KeyCode.W },
    { "Back", KeyCode.S },
    { "Right", KeyCode.D },
    { "Left", KeyCode.A },
    
};

 

Awake, this

// 유니티 생명주기(Lifecycle)에서 객체가 활성화될 때 가장 먼저 호출(초기화에 사용)
void Awake()
{
    // PlayerController의 전역 인스턴스를 설정합니다. 다른 클래스에서 PlayerController.instance로 접근할 수 있게 함
    // this는 메서드가 속한 현재 객체를 의미. Awake 메서드는 PlayerController 클래스의 인스턴스에서 실행되므로, this는 현재 PlayerController 객체 자체를 참조
    instance = this;
}

 

SetCommand() 메서드는 현재 실행할 명령을 설정.

ExecuteCommand() 메서드는 설정된 명령을 실행.

// SetCommand() 메서드는 현재 실행할 명령을 설정
// 생성된 명령 객체(moveCommand)를 CommandInvoker에 전달
commandInvoker.SetCommand(moveCommand);
// commandInvoker가 설정된 명령(moveCommand)을 실행
commandInvoker.ExecuteCommand();

 

SetKeys() 메서드는 외부에서 새로운 키를 설정하려고 할 때 사용

GetKeys() 메서드는 외부에서 현재 키 설정을 확일할 때 사용

// 새로운 키 매핑을 설정
    // 외부에서 호출하여 WASD 대신 다른 키를 사용할 수 있게 함.
    // newKeys는 새로운 딕셔너리로, 이 메서드가 호출될 때 이 딕셔너리를 moveKeys에 설정
    public void SetKeys(Dictionary<string, KeyCode> newKeys)
    {
        // moveKeys는 클래스 내에서 정의된 딕셔너리
        // moveKeys에 newKeys를 할당
        // moveKeys 를 새로운 키 설정으로 대체
        moveKeys = newKeys;
    }
    // 현재 키 매핑을 반환
    // 외부에서 현재 키 설정을 확인할 때 사용
    public Dictionary<string, KeyCode> GetKeys()
    {
        // moveKeys 라는 필드에 저장된 딕셔너리를 반환
        // 이를 통해 외부에서 moveKeys의 내용을 가져와서 사용할 수 있음
        return moveKeys;
    }
}

 

CommandInvoker.cs
0.00MB

 

CommandInvoker 스크립트

(중개자 역할을 수행)

 

SetCommand : 실행할 명령을 설정

ExecuteCommand: 설정한 명령을 실행

// SetCommand : 실행할 명령을 설정
// 매개변수인 newCommand 는 Command 인터페이스를 구현한 구체적인 명령 객체
// 이 메서드를 호출하면 CommandInvoker는 내부적으로 어떤 명령을 실행할지 준비를 함.
public void SetCommand(Command newCommand)
{
   command = newCommand;
}

// ExecuteCommand: 설정한 명령을 실행
public void ExecuteCommand()
{
   // command.Execute()를 호출하여 명령 객체가 가진 구체적인 동작을 수행
   command.Execute();
}

 

Command.cs
0.00MB

 

Command 스크립트

 

abstract : C#에서 추상적인 개념을 나타내며, 클래스 또는 메서드에 사용할 수 있는 예약어

추상 클래스 (abstract class) : 다른 클래스에서 상속받을 수 있는 기반 클래스.

                                                자체적으로는 객체를 생성 할 수 없음.

추상 메서드 (abstract method): 메서드의 구체적인 구현부가 없는 메서드

 

// 명령 패턴(Command Pattern)의 핵심 요소 중 하나인 추상 클래스 Command를 정의
// abstract : C#에서 추상적인 개념을 나타내며, 클래스 또는 메서드에 사용할 수 있는 예약어
// 추상 클래스 (abstract class) : 다른 클래스에서 상속받을 수 있는 기반 클래스. 자체적으로는 객체를 생성 할 수 없음.
// 상속받는 클래스가 반드시 구현해야 하는 추상 메서드를 정의할 수 있음
public abstract class Command
{
    // 추상 메서드 (abstract method): 메서드의 구체적인 구현부가 없는 메서드
    // 반드시 추상 클래스 내에 선언되며, 이를 상속받은 클래스에서 재정의(override) 해야 함.
    // 명령의 실행 동작을 정의하는 역활을 수행.(각 구체적인 명령(예: MoveCommand, AttackCommand)에서 고유한 실행 동작을 제공
    public abstract void Execute();
}

 

 

MoveCommand.cs
0.00MB

 

MoveCommand 스크립트

(Command 클래스를 상속받음. Receiver 역할을 수행)

 

Translate() : 객체의 위치를 이동시키는 데 사용

일반적으로 Time.deltaTime과 함께 사용하여 프레임 속도에 관계없이 일정한 속도로 움직이게 함.

public override void Execute()
{
    // PlayerController 클래스의 싱글톤 인스턴스에서 이동 속도(moveSpeed) 값을 가져오고 speed 에 할당
    float speed = PlayerController.instance.moveSpeed;
    
    // Translate() 는 Transform의 위치를 변경하는 Unity 메서드
    charTransform.Translate(direction * speed * Time.deltaTime);
}

ChangeKeyCommand.cs
0.00MB

 

 

ChangeKeyCommand 스크립트

(Command 클래스를 상속받음. Receiver 역할을 수행)

 

UIManager.cs
0.01MB

 

 

UIManager 스크립트

( Invoker 역할을 수행)

 

onValidateInput : 입력 검증(validation) 이벤트로, 사용자가 입력 필드에 문자를 입력할 때 호출

+= : 이 코드에서는, 이벤트에 새로운 검증 로직을 추가하는 연산자

char.ToUpper() , 문자열을 대문자로 만드는 함수

// 입력 필드(keyInputs)에서 사용자가 입력하는 모든 문자를 대문자로 자동 변환
private void InitUpper()
{
    // keyInputs 은 TMP_InputField 타입의 배열로, 사용자 입력을 받을 수 있는 여러 입력 필드를 나타냄
    // input 은, 배열 내 각 입력 필드 객체를 가리키는 변수
    foreach (var input in keyInputs)
    {
        // 해당 이벤트를 통해 입력된 문자가 유효한지 확인하거나 변환할 수 있음.
        // 검증 로직 : text: 입력 필드에 현재까지 입력된 문자열.
        //           charIndex: 추가될 문자의 인덱스 위치.
        //           addChar: 새로 입력된 문자.
        // char.ToUpper(addChar)는 새로 입력된 문자(addChar)를 대문자로 변환. 변환된 대문자는 입력 필드에 반영
        input.onValidateInput += (text, charIndex, addChar) => char.ToUpper(addChar);
    }
}

 

ToString()은 객체를 문자열로 변환하는 .NET의 메서드

// TMP_InputField 배열의 i번째 요소로, 사용자가 키 값을 입력하거나 수정할 수 있는 입력 필드
// KeyCode 값을 문자열로 변환하여 입력 필드에 표시(KeyCode.W는 "W"로 변환)
// ToString()은 객체를 문자열로 변환하는 .NET의 메서드
keyInputs[i].text = pair.Value.ToString();

 

Enum.TryParse 는 변환이 성공하면 true를 반환하고, 변환된 값(KeyCode)을  변수에 저장

변환이 실패하면 false를 반환하며, 변수는 기본값

.Split(':'): 문자열을 특정 구분자로 나눔.

// Enum.TryParse 는 변환이 성공하면 true를 반환하고, 변환된 값(KeyCode)을 newkey 변수에 저장
// 변환이 실패하면 false를 반환하며, newkey는 기본값
// 이 구문은 사용자가 유효한 키 값을 입력했는지 검증하는 역할
// 사용자가 입력한 텍스트(keyInputs[i].text)를 KeyCode Enum 타입으로 변환
// out 키워드는 메서드가 호출된 후, 해당 메서드가 값을 반환하는 추가적인 방법을 제공
if (Enum.TryParse(keyInputs[i].text, out KeyCode newkey))
{
    // keyTexts 배열에서 해당 키 이름을 가져옴.
    // .Split(':'): 문자열을 특정 구분자로 나눔.
    // .Split(':')[0]은 문자열을 ':'를 기준으로 나누고 첫 번째 부분("Forward")을 가져옴. 만약 .Split(':')[1] 이면 물자열(W, A...)를 가져옴
    // 새로 변환된 newkey 값을 newKeys Dictionary에 추가
    newKeys[keyTexts[i].text.Split(':')[0]] = newkey;
}