'게임 프로그래밍 패턴' 교재를 학습하며 핵심 내용과 함께 개인적으로 보충한 내용을 정리해보았다.
- 명령 패턴의 정의
- 명령 패턴이란 메서드 호출을 객체로 감싸 객체로 다룰 수 있게 하는, 콜백을 객체 지향적으로 표현한 것이다.
- 콜백, 일급 함수, 함수 포인터, 클로저, 부분 적용 함수와 비슷하다. - 명령 패턴 활용 예제
<입력키 변경>
- 입력 키 변경에서 명령 패턴을 사용하는 이유?
: 대부분의 게임이 입력키를 변경할 수 있도록 구현됨.
- 왜?
1. 크로스 플랫폼을 지원하는 게임의 경우 입력키가 기기에 따라 달라질 수 있음
2. 개발 도중 입력에 대한 기획이 변경될 수도 있음
- 명령 패턴을 활용한 입력키 변경 구현의 핵심은?
: 함수를 직접 교체하는 것이 아니라 교체 가능한 객체로 만드는 것
- 구체적인 구현 방법
: 1. 게임에서 할 수 있는 행동을 실행하는 공동 상위 클래스(command)를 정의 한다. command는 액터를 제어하는 일급 객체가 되며, 이 때 제어하려는 객체를 함수의 인자로 전달할 수 있게 함으로써 파생 클래스에서 원하는 액터의 메서드를 호출할 수 있다.(ex. actor.jump();)
2. 상위 클래스를 상속받아 각 행동(ex. jump, attack...)별로 하위 클래스를 구현한다.
3. 입력 핸들러에 각 버튼별 클래스 포인터를 저장한다. (ex. command buttonx_;) 이 때 enum 타입으로 사용될 키값들을 정의하여 구분해 줄 수도 있다.
4. 입력 핸들러가 명령을 직접 실행할 수 없는 대신 명령 객체를 리턴하도록 한다. 이를 통해 입력 핸들러가 함수 호출시점을 지연시키는 추상 계층 역할을 할 수 있게된다.
5. 명령 객체를 받아서 제어하려는 객체(ex. gameActor)에 적용하는 코드를 작성한다.
- 이렇게 했을 때의 장점?
1. 커맨드 객체를 선택하는 것과 이를 실행하는 액터가 디커플링 되었기 때문에 명령을 실행 할 때 액터만 변경해주면 게임의 어느 액터라도 제어할 수 있다.
2. 앞선 이유와 마찬가지로 로직이 각각 디커플링 상태이기 때문에 코드가 더욱 유연해진다.
- 이러한 구조를 어떻게 활용할 수 있는가?
1. AI 엔진과 액터 사이의 인터페이스로 사용할 수 있다. 이 경우 AI 코드가 원하는 Command 객체를 이용할 수 있게 되므로 구체적으로 다음과 같은 상황에 대응이 가능하다.
▶ 액터마다 AI 모듈을 다르게 적용할 수 있다.
▶ 위와 비슷하게 AI마다 다양한 패턴의 공격 명령을 적용할 수 있다.
▶ 플레이어 캐릭터에 AI를 연결하여 자동으로 실행되는 데모 모드나, 자동전투를 구현할 수 있다.
- 단, 어떤 일을 하는지를 정의한 명령 객체 하나가 매번 재사용되기 때문에 이를 변형하여 <실행취소와 재실행>에서 다룬 것처럼 특정 시점에 발생될 일을 표현할 수 있다.
<명령을 큐나 스트림으로 만드는 경우> ※
- 구조는 AI → 명령 스트림 →액터가 된다.
- 입력 핸들러나 AI 같은 코드에서 명령 객체를 만들어 스트림에 밀어 넣는다. 이때 명령들을 직렬화 할 수 있게 되면 네트워크로도 전달 할 수 있다. 즉, 플레이어로부터 입력을 받아서 네트워크를 통해 상대방에게 전달해 입력을 재현할 수 있는 것이다.
- 디스패치나 액터에서는 명령 객체를 받아서 호출한다. 큐를 둘 사이에 끼워 넣음으로써, 생산자와 소비자를 디커플링 할 수 있게 된다.
<실행취소와 재실행>
- 명령 패턴에서 명령 객체가 어떤 작업을 실행할 수 있는 기능을 응용하면 실행취소(undo) 기능과 재실행 기능을 구현할 수 있다.
- 실행 취소 기능은 원치 않는 행동을 되돌릴 수 있는 전략 게임에서도 활용 되며, 게임 개발 툴에서도 레벨 에디터에 실행 취소 기능을 제공할 수 있게 해준다. (흔히 사용하는 ctrl+z)
- 구체적인 예로는 싱글플레이어 턴제 게임에서의 이동 취소 기능을 들 수 있다. 명령 패턴을 사용하면 이미 명령 객체를 이용해 입력 처리를 추상화 하기 때문에 플레이어 이동도 명령에 캡슐화되어 있다.
그렇기에 어떤 유닛을 옮기는 명령을 처리할 때, 이전 방법 처럼 명령에서 변경하려는 액터와 명령 사이를 추상화로 격리시키는 것이 아니라 이를 변형하여 이동하려는 유닛과 위치 값을 생성자에서 받아서 명령과 명시적으로 바인딩 할 수 있다.
이를 통해 특정 시점에 발생될 일을 표현할 수 있다. 구체적으로 말하자면 이전 방식과 달리 command 클래스가 일회용이 되기 때문에, 입력 핸들러 코드는 플레이어가 이동을 선택할 때마다 명령 인스턴스를 생성해야 한다.
이런 특징을 활용하면 여러 단계의 실행 취소를 지원할 수 있다. 스택에 최근 명령만 기억하는 대신, 명령 목록을 유지하고 현재 명령이 무엇인지만 알고 있으면 된다. 정리하자면 다음과 같다.
- 여러 단계의 실행 취소 기능 구현
▶ '실행 취소'시 현재 명령을 실행 취소하고 현재 명령을 가리키는 포인터를 뒤로 이동시킨다.
▶ '재실행'시 포인터를 다음으로 이동시킨 후에 해당 포인터를 실행한다.
→ 이러한 재실행은 게임에서 잘 쓰이지 않을 수도 있지만 '리플레이'기능의 구현과 동일하다. 기능 자체는 매 프레임마다 전체 게임 상태를 저장하는 식으로 구현가능하지만 이 경우 메모리가 너무 많이 필요하다는 문제가 있다.
이를 대신해 전체 개체가 실행하는 명령 모두를 매 프레임 저장하고, 게임을 '리플레이'할 때 이전에 저장한 명령들을 순서대로 실행해 게임을 시뮬레이션 하도록 구현할 수 있다.
▶ 여러번 '실행취소' 한 다음 새로운 명령을 실행한다면, 현재 명령 뒤에 새로운 명령을 추가한 뒤 그 다음에 붙어 있는 명령들을 버린다. - 명령 패턴에서의 클래스와 함수형
- C++의 경우 일급 함수를 제대로 지원하지 않으므로 명령 패턴에 클래스를 사용하지만, 클로저를 지원 하는 언어(ex. C#과 자바스크립트)의 경우에는 함수를 사용할 수 있다. - 관련자료
- 명령 패턴에서 사용되는 Command 클래스의 수가 많은 경우 구체 상위 클래스에 여러 가지 편의를 제공하는 상위 레벨 메서드를 만들어 사용하면 하위 클래스에서 바로 원하는 작동을 재정의 할 수 있게 되어 편리하다. (명령 클래스의 execute 메서드가 하위 클래스 샌드박스 패턴으로 발전하게 됨)
- 어떤 액터가 명령을 처리할지는 명시적 일 수 도 있고 아닐 수도 있다. 이처럼 객체가 명령에 반응할 수도 있고 종속 객체에 명령 처리를 떠넘길 수도 있는 경우는 책임 연쇄 패턴이라고도 볼 수 있다.
- 어떤 명령은 특정 상태 없이 순수하게 행위만 정의 되어 있을 수도 있다. 이런 클래스는 모든 인스턴스가 동일하기 때문에 여러 인스턴스를 만드는 것은 메모리 낭비다. 이는 경량 패턴을 사용하면 해결 할 수 있는 문제이다.
+)
- 일급 객체란?
: 다른 객체들에 일반적으로 적용가능한 연산을 모두 지원하는 객체를 뜻한다.
- 일급 객체의 조건
1. 변수에 할당할 수 있다.
2. 다른 함수를 인자로 전달 받는다.
3. 다른 함수의 결과로 리턴 될 수 있다.
'CS > 디자인패턴' 카테고리의 다른 글
관찰자 패턴(Observer Pattern) (0) | 2022.08.29 |
---|---|
3장. 경량 패턴(Flyweight Pattern) (0) | 2022.08.28 |
1장. 도입 (0) | 2022.08.16 |