포스팅 레퍼런스:
로버트 나이스트롬의 게임 프로그래밍 패턴: http://www.aladin.co.kr/shop/wproduct.aspx?ItemId=84101386
GoF의 디자인 패턴: http://www.aladin.co.kr/shop/wproduct.aspx?ItemId=56051596
Stackoverflow에서 삽질한 경험
위키피디아 북의 컴퓨터 과학 디자인 패턴: https://en.wikibooks.org/wiki/Computer_Science_Design_Patterns
요즘 머리가 퇴화하고 있는 것 같다. 평소 익혀뒀던 디자인 패턴을 복습하기 위한 포스트다.
패턴의 정의와 요약은 외부 레퍼런스에서 그대로 가져온 것도 있다. 모든 주요 패턴이 있는 것은 아니다. (까먹지 않는다면)생각 날때마다 추가할 것이다.
예제는 직접 짰다. 우아함보다는 이해하기 쉬움을 우선했다. 고상한 코드가 아니여도 이해해 달라.
싱글톤 Singleton 패턴
- 단 하나의 인스턴스만 생성하게 한다.
- 해당 인스턴스에 대한 정적 접근 방법을 제공한다.
캡슐화된 전역변수다.
유니티C#에서 싱글톤을 짜는 방법은 여러가지 있는데 본인은 보통 이렇게 짠다.
(인스턴스 수가 하나 초과인 경우 나머지를 파괴하는 코드를 넣는 경우도 있다.)
싱글톤의 장점
- 사용하는 시점에 인스턴스화 된다.
- 사용하지 않을때 불필요하게 미리 인스턴스화가 되지 않는다.
- 정적 접근자를 통해 코드 어디에서나 쉽게 접근 가능하다.
- 단 하나의 인스턴스만 보장하기에 공유되는 자원을 관리하는 역할에 알맞다.
하지만 싱글톤은 상당이 남용되는 패턴이다.
유니티 프로그래밍에서 싱글톤은 가장 처음으로 배우게되는 패턴인데, 그 맛(?)을 알게된 초보 프로그래머들의 프로젝트를 열어보면 열에 아홉은 LevelManager, ScoreManager, MonsterManager, DialogueManager... 등 Manager 클래스를 무지막지하게 남용하는 광경을 목격할수 있다.
그래도 싱글톤이 무작정 좋은 것이라면 상관없겠지만, 다시 싱글톤의 정의를 보자.
싱글톤은 단 하나의 인스턴스만 만들도록하고, 손쉬운 접근자를 제공한다.
- 인스턴스는 단 하나만
- 쉽게 접근가능
이 두 가지 조건을 모두 만족해야할 필요가 있을때 쓰는 것이다. 하지만 대부분은 쉽고 빠른 전역 접근을 원하는데, "정적은 최소화한다" 라는 규칙을 우회해할수 있다는 속편한 양심상의 이유로 싱글톤은 남용된다.
하지만 둘 중 하나의 조건만 만족하면되는 상황에서는 더 나은 대안이 존재한다.
싱글톤이 좋지 않은 이유는 (참고로 여기서 말하는 빠른이란, 성능이 빠른이 아니라, 충분한 고민없이 바로 사용가능 하다는 뜻이다)
- 쉽게 접근가능한 전역 접근자
- 코드 아무곳에서나 접근 가능하므로 어디선가 싱글톤 인스턴스의 내부를 바보같이 바꾸어 에러를 낸 경우, 흩어져있는 수많은 싱글톤 접근자 중에서 어디서 그랬는지 찾기 힘들다.
- 멀티 스레드의 경우 다른 스레드가 쉽게 접근하여 데드락이 발생할수 있다.
- 다른 클래스에서 자신과 전혀 상관없는 싱글톤 인스턴스가 쉽게 접근가능하므로, 지저분한 커플링을 조장한다.
- 인스턴스를 하나로 한정
- 싱글톤을 남용하는 경우, 대부분 쉽고 빠르게 쓸수 있는 접근자를 원해서이다. 싱글톤 패턴을 사용하면서 여기에 자동적으로 인스턴스는 하나만 허락하는 구조가 강제된다. 물론 하나만 존재하는 인스턴스는 관리하기 편해보인다.
결국 인스턴스가 하나만 강제되야할 필요성이 진짜 있다기보다는, 개발 초기에는 '그렇게 하는게 편해보인다'는 생각에 쓰게 된다.예를 들어 유저 점수를 가진 Score를 Score.Instane.records 라는 식으로 접근해왔다고 하자.
하지만 진짜 점수라는게 한가지만 존재할까? 개발 도중에 점수의 종류도, 여러종류가 되어 킬 스코어, 방어 스코어, 어시스트 스코어... 이렇게 여러 스코어를 따로따로 인스턴스를 만들어 관리하는게 더 나은 상황이 온다.
하지만 싱글톤으로 구조를 제한 시켜놓았기 때문에 그러한 상황에서도 Score.Instance... 로 접근해서 한 인스턴스 내부에 여러 종류의 점수를 우겨넣어야 할것이다.
- 싱글톤을 남용하는 경우, 대부분 쉽고 빠르게 쓸수 있는 접근자를 원해서이다. 싱글톤 패턴을 사용하면서 여기에 자동적으로 인스턴스는 하나만 허락하는 구조가 강제된다. 물론 하나만 존재하는 인스턴스는 관리하기 편해보인다.
- 게으른 초기화
- 게으른 초기화는 호출 시점에 인스턴스화를 함으로서 최초 호출시 게임 도중에 프리징을 유발할수 있다.
- 메모리 단편화를 막기 위해서는 적절한 인스턴스화 시점을 찾아야 하는데, 코드 어디에서라도 접근자를 사용할수 있고 접근 순간 인스턴스화가 이루어지므로, 인스턴스화 시점을 제어할 수 없다.
- 물론 처음부터 정적 인스턴스를 생성할수 있다. 하지만 이렇게 할시 필요가 없어졌을때도 메모리 해제가 불가능하다. 이렇게 런타임에서 생성과 해제를 제어할수 없다면, 그냥 정적 클래스를 쓰는게 낫지 않나? (C++에서 static myClass* instance대신 static myClass instance로 바꾸는 경우)
어떻게 할까?
- Manager... 클래스의 남용
- 대부분의 허접한 싱글톤 패턴은 Monster의 경우 MonsterManager.. 같은 식으로 해당 클래스를 관리하는 경우가 많다. 이런 경우들에서 사실 필요한 MonsterManager의 인스턴스는 1개가 아니라 0개이다. MonsterManager에 들어갈 코드가 사실 Monster 자체에 포함되어야 하지 않았나 반성하고, OOP 개념을 다시 떠올려보자.
객체는 스스로를 스스로가 챙긴다. 각 클래스마다 죄다 ~Manager 클래스를 하나씩 더 만드는 짓은 하지 말자.
- 대부분의 허접한 싱글톤 패턴은 Monster의 경우 MonsterManager.. 같은 식으로 해당 클래스를 관리하는 경우가 많다. 이런 경우들에서 사실 필요한 MonsterManager의 인스턴스는 1개가 아니라 0개이다. MonsterManager에 들어갈 코드가 사실 Monster 자체에 포함되어야 하지 않았나 반성하고, OOP 개념을 다시 떠올려보자.
- 인스턴스가 하나만 필요
- 인스턴스가 하나만 필요한것이지, 전역 접근자가 필요한 경우가 아닐때, 굳이 싱글톤을 사용하여 구조를 취약하게 만들 필요는 없다.
private static myClass instance; 로 단 하나의 인스턴스를 위한 공간을 선언한다. 그리고 생성자 내부에서 단순이 if문으로 instance의 null여부를 체크한다.
이미 존재하는 인스턴스가 있으면 단언문assert을 사용해 인스턴스화를 막아버리고, 그렇지 않다면 새 인스턴스를 생성후, private으로 선언된 instance에도 할당해준다.
- 인스턴스가 하나만 필요한것이지, 전역 접근자가 필요한 경우가 아닐때, 굳이 싱글톤을 사용하여 구조를 취약하게 만들 필요는 없다.
- 인스턴스에 쉽게 접근하고자 할때
- 부지런이 객체를 메개변수로 함수에 넘겨주기
- 상위 클래스를 통해 접근.
- 해당 클래스에서 쉽게 접근해야할 인스턴스는 부모 클래스의 protected로 선언된 접근자를 통해 접근 가능하게 한다. 이러면 다른 상관없는 클래스에서 마구잡이로 접근할수 없다.
- 기존 전역 변수에 빌붙기
- 서비스 중계자 사용
NULL 패턴
모든 객체가 연결되어 있다고 가정하고 null체크를 하지 않는 대신, 새로운 포인터가 선언될때 내용이 비어있는 객체를 가리키게 하는것.
C# 빈 델리게이트 패턴
public event EventHandler MyButtonClick = delegate { };
위와 원리가 같은 패턴이다.
C#에서 델리게이트에 아무의미 없는 빈 델리게이트를 넣어 초기화 하는 패턴이 있다.
별거 없어 보이지만, 멀티 스레드 환경에서 null체크를 한 다음 델리게이트를 invoke 하는 것은 번거롭기도 하고 예외상황이 생기기 때문에 invoke시 null 에러를 막는데 효과적이다.
그리고 딱 한줄만 쓰면 되니까 좋다.
+ 멀티스레드에서 null체크시 에러를 피하는 간단한 랩핑
public static void SafeInvoke(this EventHandler handler, object sender, EventArgs e) { if (handler != null) { handler(sender, e); } } public virtual void OnMyButtonClick(EventArgs e) { MyButtonClick.SafeInvoke(this, e); }
델리게이트는 mutual 형이고 C# 3.0의 메서드 확장 기능은 사실 static으로서 호출되는 것임을 이용했다.
MyButtonClick.SafeInvoke 를 호출한 순간, 사실 EventHandler.SafeInvoke(MyButtonClick,this,e)가 호출된다. 그리고 MyButtonClick은 mutual 형이기 때문에 딥카피 되서 메소드 내부에 사용된다.
따라서 if문 체크를 통과한 직후에 원본이 스레드에 의해 날아가도 내부에서 실제로 사용하는건 레퍼를 공유하지 않는 딥카피된 델리게이트이기 때문에 무사히 Invoke된다.
명령 Command 패턴
메소드의 동작 그 자체를 객체화 한것.
즉 메소드를 가리키는 대리자를 통해서, 메소드의 동작을 실체화한다.
이는 명령의 실행을 자료형 처럼 묶어서 꺼내 사용할 수 있게 해준다.
즉 명령어의 실행을 지연시키고, 자료형에 넣어 재조합하여 명령들의 체인을 내가 직접 제어할 수 있는 것이다.
클래스나 구조체가 execute() 혹은 invoke() 만 있다던가, 혹은 델리게이트 감싼 심플한 wrapper 의 형태를 띈 인터페이스라면 바로 명령 패턴인걸 알수 있다.
이 패턴을 사용하여 스택에 실체화된 명령을 쌓아둔자 하자.
C#의 델리게이트를 사용해도 되지만 보편적인 예를 인터페이스로 설명하자면,
public interface ICommnad { void execute(); void undo(); } public class Move: ICommand { var beforePositionState; void execute() { beforePositionState = currentPositionState; // some move codes } void undo() { // some undo move codes to get back to beforePositionState } } public class Draw: ICommand { var beforeCanvasState; void execute() { beforeCanvasState = currentCanvasState; // some draw codes } void undo() { // some undo draw codes to get back to beforeCanvasState } } /* ... 실행부 ... */ Stack<ICommand> commands = new Stack<ICommand>(); public void Execute(ICommand command) { command.execute(); commands.Add(command); } public void Undo() { commands.Pop().undo(); }
명령들을 이러한 인터페이스를 상속받아 구현한다음, 이 실체화된 명령들이 실행될때 마다 ICommand 형 스택에 쌓아둔다. 이를 통해 여러가지 종류의 에디터에 사용되는 편집 되돌리기 기능을 쉽게 구현한다. 명령을 되돌릴때는 execute의 반대 작용을 구현해둔 undo를 실행하고, 편집 다시 실행하기를 할때는 execute를 실행하면서 명령 스택을 날리거나 다시 쌓으면 되는 것이다.
이는 또한 기획자용 스크립트 툴과 컴파일러를 제작할 실력이 되지 않거나, 게임에서 흔히 말하는 비주얼 노벨 = 게임 중 대화 파트용 기획자 스크립트 툴을 준비해야 할때 빠르게 사용할 수 있다.
Regex를 통해 기획자용 스크립트를 긁은 다음, 동적으로 스크립트 명령어당 각각의 명령 객체를 생성하여 스택에 쌓은 다음, 유저가 마우스를 클릭할때 마다 해당 명령 객체가 invoke 되도록.. 물론 이건 야매인 방법이다.
클로저와 이를 포함하는 람다함수를 지원하는 언어에서도, 구조체나 클래스를 사용한 명령 패턴은 사용할 가치가 있다.
클로저는 해당 명령의 실행 당시 환경을 기억하고 있지만, 이를 편하게 쓸수 있도록 자동적으로 랩핑 해주기 때문에, 프로그래머가 당시 실행환경의 정보를 알고 거기에 반대로 제어해서 undo 명령을 포함한 여러 제어를 함께 구현하기에는 조금 불편하다.
경량 Flyweight 패턴
수많은 인스턴스들 사이에 공통된 요소를, 하나의 객체로 정의한다. 수많은 인스턴스들은 동작할때 이제 스스로의 자원을 제각각 인스턴스화 할 필요업이, 이미 인스턴스화된 구성 요소 객체 하나를 여럿이서 공유하면서 매개변수를 통해 다르게 표현하면 된다.
이를 통해 공통된 자원을 여러번 중복 인스턴스화 하는데 들이는 비용을 절감한다.
현대 그래픽 카드들은 모두 인스턴스 랜더링을 지원한다. 두가지 스트림을 전달하면 된다. 하나는 메시, 텍스쳐, 마테리얼 등 그래픽 오브젝트들이 공유하는 자원. 다른 하나는 이들을 각각 다르게 그리기 위한 위치, 스케일 등의 매개변수.
멀리가지 않아도 유니티3D의 기초 튜토리얼에서 조차, 하나의 마테리얼로 여러 인스턴스를 동시에 그려 비용을 많이 아낄 수 있음을 입문자들에게 강조하여 가르친다.
구현
경량 패턴에서는 객체 데이터를 두가지로 나눈다.
- 값이 고정되어 공유할수 있는 자원, '고유 상태'
- 인스턴스 별로 값이 다른, '외부 상태'
"게임 프로그래밍 패턴" 책에서는 고유 상태, 혹은 저자가 쓰는 표현인 자유문맥을, 값이 고정되어 공유가능한 상태라 정의했다.
내 의견으로는 좀더 구체적으로,
- 열거했을때 종류가 한정적이다.
- 초기화 단계이후 변할 필요가 없는 값이다.
로 표현하는게 더 와닿는다. 어찌보면 객체를 열거형처럼 사용하여 더 상위의 객체 내부에 집어넣어 활용하는 느낌이다.
아래 예제는 이해하기 쉽도록 구리게 짰다.
public class MonsterBlood { private bool _isMale; private Mesh _skinMesh; private string _bloodName; public bool IsMale { return _isMale; } public Mesh SkinMesh { return _skinMesh; } public string BloodName { return _bloodName; } MonsterBlood(bool isMale, Mesh skinMesh, string bloodName) { this.isMale = isMale; this.skinMesh = skinMesh; this.bloodName = bloodName; } } public class MonsterBloods { /* ...생략... */ private static MonsterBlood _ghost = new MonsterBlood(true,ghostMesh,"유령"); private static MonsterBlood _dragon = new MonsterBlood(false,dragonMesh,"드래곤"); private static MonsterBlood _zombie = new MonsterBlood(true,zombieMesh,"좀비"); public static MonsterBlood Ghost{ get{ return _ghost } } public static MonsterBlood Dragon{ get{ return _dragon } } public static MonsterBlood Zombie{ get{ return _zombie } } } public class Monster { /* ...생략... */ Monster(MonsterBlood blood, MonsterData otherData) { this._blood = blood; this._monsterData = otherData; //....생략 } private MonsterBlood _blood; public MonsterBlood Blood{ return _blood; } } /** 새로 몬스터를 인스턴스화 할때, 종족과 관련된 정보와 메쉬를 그때 그때 생성활 필요가 없다. **/ Monster newZombie0 = new Monster(MonsterBloods.Zombie,somedatas0); Monster newZombie1 = new Monster(MonsterBloods.Zombie,somedatas1); Monster newZombie2 = new Monster(MonsterBloods.Zombie,somedatas2); Monster newZombie3 = new Monster(MonsterBloods.Zombie,somedatas3); Monster newZombie4 = new Monster(MonsterBloods.Zombie,somedatas4); Monster newZombie5 = new Monster(MonsterBloods.Zombie,somedatas5); Monster newZombie6 = new Monster(MonsterBloods.Zombie,somedatas6); Monster newZombie7 = new Monster(MonsterBloods.Zombie,somedatas7);
newZombie0와 newZombie1은 는 서로 다른 객체지만, 이 두개의 객체는 하나의 종족정보 MonstersBloods.Zombie를 공유한다. 즉 위 코드에서 7개의 좀비를 만들기 위해 MonstersBloods.Zombie를 일곱번 인스턴스화할 필요가 없다.
그리고 newZombie0.Blood.BloodName; 를 통해 혹은 MonsterBloods.Ghost.BloodName 을 통해 종족 정보를 알수 있다. 즉 Monster 클래스 내부의 처리를 거칠 필요 없이, MonsterBlood 인스턴스에서바로 종족 정보를 확인 가능하며, Monster와 MonsterBlood는 서로 커플링 되지 않기 때문에 종족정보를 수정하는데 있어 자유롭다. 종족 정보를 수정할때 Monster 클래스에 직접적인 영향을 주지 않는다.
관찰자 Observer 패턴
모델-뷰-컨트롤러(MVC) 구조의 기반이다.
누가받든 상관 없는 알림. 알림을 받는 바깥의 관찰자observer가 무엇을 하는지 알림을 보내는 측, 대상subject은 전혀 몰라도 된다.
그냥 이건 말보다 코드가 빠르니 코드를 보자.
/** 사실 C#은 event와 delegate키워드가 있기 때문에 실제로 C#에선 이보다 매우 간단하다. **/ public enum GameEvent { DEATH,HIT,ATTACK }; public Interface IObserver { void OnNotify(const Object entity, GameEvent gameEvent); } public class AchievementManger: IObserver { void OnNotify(const Object entity, GameEvent gameEvent) { if(gameEvent == GameEvent.DEATH) { Console.WriteLine(entity.ToString() + "이 죽었습니다! \"넌 뒤져따\" 도전과제 해제!"); } } } public class Subject { protected List<IObserver> observers = new List<IObserver>(); public void AddObserver(IObserver observer) { observers.Add(observer); } public void ClearObservers() { observers.Clear(); } protected void Notify(const Object entity, GameEvent gameEvent) { foreach(var observer in observers) { if(observer != null) { observer.OnNotify(); } else { observers.Remove(observer); } } } } public class Player: Subject { /* ...김대기와 같은 적절한 생략을 적절하게... */ private void OnDeath() { //적절한 죽음과 관련된 코드 OnNotify(this,GameEvent.DEATH); } } /** 런타임에서 **/ myPlayer.AddObserver(achievementManager); /* ... */ if(myPlayer.hp <= 0) { myPlayer.OnDeath(); }
위 코드를 보면 Player 인스턴스는 AddObserver 메소드를 통해 자신의 죽음을 (나의 죽음을 알리지 마라!..와는 반대로) 들어줄 관찰자들을 추가할수 있다.
여기서 Player 인스턴스는 관찰자들에게 알림을 보내지만, 관찰자들이 알림을 받아 어떤 동작을 하는지는 전혀 알수 없으며, 알 필요도 없는 상태라는게 중요하다.
여기서는 도전과제 관리자가 플레이어가 죽자 "넌 뒤져따!" 라는 도전과제를 해제한다. 중요한건 관찰자 패턴을 통해 도전과제를 언락하는 코드가 독립적이라는 사실이다.
즉 도전과제를 해제하기 위해 achievement.UnlockDeathAchievenment(); 같은 코드를 if(myPlayer.hp <= 0) 내부 블록에 구태여 집어넣어 코드를 지저분하게 만들지 않아도 된다.
주의점: 관찰자는 동기적이다. 모든 관찰자가 작업 수행을 끝나기 전까지 다음 작업이 불가능하다. 만약 관찰자들의 작업이 무거운 것이라면, 다른 스레드로 관찰자들의 작업을 넘기거나 작업 큐를 활용한다.
다만 언제나 그렇듯이 멀티스레드는 정말 주의해서 사용한다. 관찰자가 대상을 락을 한 상태로 처리를 지연시키고 있다면 재앙이 올수 있다.
+ Subject 내부에서 관찰자를 동적할당하는 방법을 피하고 싶을 경우
- 링크드 리스트/ intrusive 링크드 리스트 (관찰자가 여러 대상을 관찰하고 싶은 경우)
GC가 존재하는 언어의 경우, 관찰자를 대상의 관찰자 리스트에서 제거하지 않은 상태로 다른곳에서의 레퍼런스만 해제하면, GC가 수거해가지 않는다. 당연하게도 대상에 등록된 관찰자 인스턴스는 계속 불어난다. 관찰자를 해제하여 lapsed listener 문제가 발생하지 않게 한다.
관찰자 패턴은 이벤트를 통해 간접적으로 통신하는 방법이다. 이는 대상과 관찰자를 분리시켜 깔끔한 코드를 만든다. 하지만 만약 대상과 관찰자가 서로 기능이 관련이 있는 코드라면, 둘을 명시적으로 분리시켜버리는 것은 기능을 잘라내서 코드 이곳저곳에 흩뿌려 놓는 격이다.
관찰자는 간접적인 체인이다. 그래서 IDE상으로, 관찰자와 대상 간의 명령 체인은 런타임에서밖에 확인할 수 없다. 따라서 코드간의 상호작용을 양쪽에서 동시에 확인해야 하는 것들, 즉 기능에 서로 밀접한 관계를 가진 코드끼리는 관찰자 패턴 사용이 정말 필요한지 다시 생각해봐야 한다.
프로토타입 Prototype 패턴
객체가 자신과 비슷한 다른 객체를 인스턴스화 하는 주체이다. 자가복제 인셈.
public class GameCharacter { public virtual GameCharacter Clone(); } public class Hunter : GameCharacter { //어떤 코드들 public GameCharacter Clone() { return new Hunter(attackPoint, defensePoint, speed); } } public class Warrior : GameCharacter { //어떤 코드들 public GameCharacter Clone() { return new GameCharacter(attackPoint, defensePoint, speed); } } //새로 객체를 만들고 싶으면 원본 프로토 객체로 부터 복사 생성한다. //원본 GameCharacter prototypicalWarrior = new Warrior(100,20,10); //새로운 객체를 원형 인스턴스로부터 생성. 그리고 이것이 또다른 객체들의 원형이 될수도 있다. GameCharacter prototypicalFastWarrior = prototypicalWarrior.Clone(); prototypicalFastWarrior.speed = 30; // fastWarrior0 = prototypicalFastWarrior.Clone(); fastWarrior1 = prototypicalFastWarrior.Clone(); commonWarrior0 = prototypicalWarrior.Clone(); commonWarrior1 = prototypicalWarrior.Clone();
굉장이 간단해서 본 순간 바로 잊어버릴수도 없고, 컴포넌트 형 패턴을 이름을 몰라도 다들 채화된 요즘에 와서 왜 쓰지 싶기도 한 패턴인데, 사실 그 말이 맞다.
실제 사용되는 코드 중 프로토타입 패턴의 이념을 완벽하고 추상적으로 따르는 코드는 거의 없을 것이다.
자바 스크립트 사용자라면 친근한 개념일 수 있다.
자바 스크립트는 프로토타입 패턴에서 출발한 언어로, 동작과 필드를 묶은 메서드 그 자체가 인스턴스이며, 인스턴스를 찍어내는 주체이다.
사실 두번째는 최근까지는 아니었다가 new 키워드가 추가되면서 복제가 가능해졌다.
프로토타입 패턴의 원형인 언어 Self 와 같이 자바 스크립트에서 인스턴스가 호출하려는 메서드가 자기 내부에 없으면, 자신을 복제생성했던 원본 객체인 프로토타입으로 접근하여 메서드를 호출한다. 즉 인스턴스에는 필드가 존재하고, 클래스에는 메서드가 존재하여 인스턴스가 클래스를 참조하여 메서드를 호출하는 OOP적 방식과 사실 크게 다를것이 없기에, 자바 스크립트도 객체 지향에 가까운 언어라고 생각한다.
게임 데이터 구조에서 풍부한 바리에이션을 좀더 적은 라인으로 나타낼때 요긴하게 사용할수 있겠다.
//게임 프로그래밍 패턴 책을 배낀-_-ㅋ 예제. BaseHunter { "이름": "사냥꾼", "기본 스킬": ["베기", "자르기"], "체력" : 50, "공격력" : 40, "방어력" : 20, } FastHunter { "프로토타입": "BaseHunter", "이름": "빠른 사냥꾼", "속도": 120, "추가 스킬" : ["발도","류요 와가 테키오 쿠라에"] } StrongHunter { "프로토타입": "BaseHunter", "이름": "힘쎈 사냥꾼", "힘": 100, "공격력": 60, "추가 스킬" : ["발도","류진노 켄오 쿠라에"] }
JSON은 원형 클래스를 정의하는 기능이 없다. 해당 JSON 파일에서, BaseHunter의 파생형 캐릭터인 FastHunter와 StrongHunter 에 대한 체력과 기본 스킬을 죄다 줄줄이 나열할수 있겠지만, 프로토타입 패턴을 활용하여, 자신의 원형이 되는 자료형을 프로토타입으로 명시해서 자신에게 없는 데이터는 프로토타입에서 참조하게 하여 쓸데없이 중복되는 데이터 양을 줄였다.
상태 State 패턴 (Finite State Machine, Hierarchical Machine, Pushdown Automata)
2D 플레이어를 조작하는 코드를 아래와 같이 짰다고 하자.
해당 캐릭터는 점프, 달리기, 슬라이딩, 앉기,, 를 할수 있다고 하자.
void Update() { if(Input.RShift) { if(!m_isRun && !m_isAir && !m_isSlide) { player.TryRun(); } } else if (Input.Space) { if(!m_isAir) { player.TryJump(); } } else if (Input.Crouch) { if(!m_isAir && !m_isRun && !m_isSlide) { player.TryCrouch(); } else if (!m_isRun) { player.TrySlide(); } } }
위 코드에서 각 입력에 대해 어떤 처리를 해야하는지 현재 상태를 확인하기 위해 m_isRun, m_isSlide, m_isCrouch, m_isAir 같은 bool형 변수를 행위가 하나 추가될때 마다 이들이 계속 추가되어야 한다.
특정한 상황으로 제한하기 위해 해당 bool 형 변수들을 !와 &&, || 같은 논리 연산자로 if문을 계속 묶는 과정에서 실수가 연발되게 되고, 예를 들면 (m_isRun && m_isCrouch) 처럼 앉아 있으면서도 달리고 있는 경우처럼 실재로 존재하지 않는 상태에서 플레이어가 행위를 하게 되는 실수도 무지막지하게 생길수 있다.
이 경우 오토마타 이론 중 가장 간단하고 손쉬운 FSM이 빛을 발한다.
유한상태머신은 아래 규칙을 가진다.
- 주체가 가질수 있는 상태의 종류는 유한하다.
- 주체는 한번에 단 한가지 상태State만 가질수 있다.
- 입력과 이벤트가 기계를 통해 들어온다.
- 한 상태에서 다른 상태로 전이Transition를 통해 이동할수 있다.
위 코드의 문제는 유요하지 않은 조합을 낼수 있다는 것이다. 전력달리기 m_isRun과 포복하기 m_isCrouch는 동시에 이루어질 수 없다. 이렇게 여러 플래그중 하나만 참인 경우, 열거형enumeration과 switch문을 통해 좀더 깔끔하게 구현할 수 있다.
public enum STATE {IDLE,RUN,CROUCH,SLIDE,AIR}; STATE m_playerState = STATE.IDLE: void Update() { switch(m_platerState) { case STATE.AIR: //공중에 있을시 점프 불가능 break; case STATE:IDLE: if(Input.Run) player.TryRun(); //...... break; /** 나머지 부분은 다들 어떻게 될지 알것이다 **/ } }
이제 각각의 독립된 상태State로 들어가고, 다음 Update가 호출되기 전까지는 무조건 해당 State 내부에 갖혀서 처리가 이루어지기 때문에, 존재하지 않는 상태에서 처리가 이루어지거나, 다른 상태로 곧바로 Jump하게 되는 불상사에 대해 걱정할 필요가 없어졌다.
상태 State 패턴
위는 if와 switch만으로는 충분한 경우다. 하지만 FPS에서 총을 쏘고 달리고, 슬라이딩하는 등, 그 상태 그 자체에 (예를 들어 달리기 상태라면 얼마나 오래 달렸는가, 기를 모으는 상태라면 에너지를 얼마나 모았는지) 자신만의 정보를 필요로 하면 OOP적으로 해결하는게 좋다.