이전 글에서 Terrain에 자유문맥에 해당하는 내용만 담음으로 우리는 지형을 각 종류별로 1번씩만 만들고 Terrain 객체의 포인터를 받아오는 것으로 World를 만들 수 있게됐습니다.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
이렇게 지형 종류가 같은 타일들은 모두 같은 Terrain 인스턴스 포인터를 받아오기만 하면 됩니다. 그림으로 표현하면 다음과 같습니다.
(Terrain 객체를 재사용하는 타일들)
Terrain 인스턴스가 여러 곳에서 사용되다보니 동적으로 할당하면 생명주기를 관리하기가 좀 더 어렵습니다. 따라서 World 클래스에 저장합니다.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
이제 지형 속성 값을 World 의 메서드 대신 Terrain 객체에서 바로 얻을 수 있습니다.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
경량패턴은 앞선 글에서 봤듯이 어떤 객체의 개수가 너무 많아서 좀 더 가볍게 만들고 싶을때 사용합니다.
인스턴스 렌더링에서는 메모리 크기보다는 렌더링할 나무 데이터를 하나씩 GPU버스로 보내는 데 걸리는 시간이 중요하지만, 기본 개념은 경량패턴과 같습니다.
이런 문제를 해결하기 위해 경량 패턴은 객체 데이터를 두 종류로 나눈다는 것이 특징입니다.
먼저 모든객체의 데이터 값이 갚아서 공유할 수 있는 데이터를 모읍니다. 이런 데이터를 GoF에서는 고유상태(intrinsic state) 라고 부르고, 필자가 참고하고 있는 game programming pattern 교재에서는 자유문맥(context-free) 상태 라고 부릅니다. 이전 글에서 다루었던 나무 형태가(geometry)나 텍스쳐가 이에 해당합니다.
그리고 나머지 데이터는 인스턴스별로 값이 다른 외부 상태(extrinsic state)에 해당합니다.
예제로 살펴봤던 Tree Class에서 나무의 위치, 크기, 색 등이 이에 해당합니다.
이전 글의 예제코드에서 확인한 것과 같이. 경량 패턴은 한 개의 고유 상태를 다른 객체에서 공유하게 만들어 메모리 사용량을 줄 이는 방식입니다.
여기까지만 확인한 걸로는 기초적인 자원 공유 기법이지 패턴이라 부를만한 내용은 충족하지 않는 것 처럼 보입니다. TreeModel 클래스로 깔끔하게 분리가 될 수 있었기에 그렇게 보이는 측면도 있지만, 실제로 공유 객체가 명확하지 않은 경우 경량 패턴은 잘 드러나 보이지 않습니다.
해서 이번엔 위 예제에 이어서 지형 정보(타일같은...)를 이용해서 경량 패턴을 쓰임을 확인해보도록 하겠습니다.
나무를 심을 땅도 게임에서 표현을 해야하는데, 보통 이런 땅들은 흙, 언덕, 호수, 강 같은 다양한 지형을 이어 붙여서 만들게 됩니다. 여기에서 이 땅들을 보통 타일 기반으로 만들게 될텐데요.
이 타일들은 게임플레이에 영향을 주는 여러 속성들이 들어있을 수 있습니다.
플레이어가 얼마나 빠르게 이동할 수 있는지를 결정하는 이동 비용 값
강이나 바다처럼 보트로 건널 수 있는 곳인지 여부
렌더링할 때 사용할 텍스쳐
이런 속성을 지형 타일마다 따로 저장하는 일은 있을 수 없습니다. 이런 지형 종류에 대해서는 아래와 같이 열거형을 사용하는게 일반적이죠.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
하지만 타일마다 매번 Terrain 인스턴스를 생성하는 것은 결과적으로 똑같이 많은 비용을 발생시키게 됩니다. 따라서, Terrain 클래스에는 타일위치와 관련된 내용은 전혀 없는 것을 볼 수 있습니다. 즉. 경량패턴식으로 이야기하자면 모든 지형 상태는 '고유'하다(context) 자유 문맥에 해당한다는 것을 알 수 있습니다.
3D 게임에서 숲을 구현하고자 할 때, 수 천 그루가 넘는 나무마다 각각 수천 폴리곤의 형태로 표현을 해야합니다. 이 때 메모리가 충분하다하여도 숲을 그리기 위해 전체 데이터를 CPU에서 GPU로 버스를 통해 전달해야합니다. 이때 들어가는 비용은 매우 적지않을거라는 것을 쉽게 추측할 수 있습니다.
이때 나무마다 필요한 데이터들은 일반적으로 다음과 같습니다.
- 줄기, 가지, 잎의 형태를 나타내는 폴리곤 메시
- 나무 겁질과 잎사귀 텍스쳐
- 숲에서의 위치와 방향
- 각각의 나무가 다르게 보이도록 크기와 음영같은 값을 조절할 수 있는 매게변수
위 항목들을 코드로 나열해보면
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
위에서 보듯이 나열되는 항목들의 데이터들이 적지 않은데다가, 메시와 텍스쳐는 크기도 큽니다.
해서 이렇게 많은 객체들(나무)이 있는 숲 전체는 1Frame에 GPU로 모두 전달하기에는 양이 너무 많습니다.
이런 문제에 대해서 다행히, 검증된 해결법으로 아래와 같이 시도하면 됩니다.
핵심은 숲에 나무가 수천그루가 있다고 해도 대부분 비슷해보인다는 점 입니다.
따라서, 모든 나무를 같은 메시와 텍스쳐로 표현할 수 있을거라 가정하고 나무 객체에 들어가는 데이터는 인스턴스별로 크게 다르지 않을 거라는 예상이 보입니다.
이런 가정하에 먼저 객체를 반으로 쪼개어 명시적으로 모델링을 데이터를 나눌 수 있습니다.
모든 나무가 같이 사용하는 데이터를 뽑아내 새로운 클래스에 모아봅니다.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
이렇게하면 게임내에서 같은 메시와 텍스쳐를 여러번 메모리에 올릴 필요가 없기 때문에 TreeModel 객체는 하나만 존재하게 됩니다. 이제 각 나무 인스턴스는 공유 객체인 TreeModel을 참조하기만 하면 됩니다.
Tree 클래스에는 인스턴스별로 다르게 표기되어야 하는 데이터들만 아래와 같이 남겨둡니다.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
어떤 명령 객체가 실행할 수 있다면, 이를 실행취소 할 수 있게 만드는것도 어렵지 않습니다.
게임개발 툴 같은곳에서는 필수적으로 지원이 되야하는 기능입니다. 만약 이 기능이 지원되지 않는다면....
(기획자가 당신을 죽이려들지도...)
그냥 실행취소 기능을 구현하려면 굉장히 어렵습니다. 그러나 명령패턴을 활용하면 이를 쉽게 만드는게 가능합니다. 예를 들어서 턴제 게임에서 이동 취소 기능을 추가한다고 하겠습니다.
이미 명령 객체를 2편에서 명령 객체를 이용해서 입력 처리를 추상화해둔 덕분에, 플레이어 이동도 명령에 캡슐화되어 있습니다. 이제 어떤 유닛을 옮기는 명령을 생각해보도록 하겠습니다.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
이전 에졔에서는 명령에서 변경하려는 액터와 명령 사이를 추상화로 격리시켰지만 이번에는 이동하려는 유닛과 위치 값을 생성자에서 받아서 명령과 명시적으로 바인드되어 있습니다.
MoveUnitCommand 명령 인스턴스는 '무엇인가를 움직이는' 보편적인(언제든지 써먹을 수 있는) 작업이 아니라 게임에서의 구체적인 실제 이동을 담고있습니다.
2편에서 다룬 에졔 같은 경우, 어떤 일을 하는지를 정의한 명령 객체 하나가 매번 재사용 됩니다.
입력 핸들러 코드에서는 특정 버튼이 눌릴 때마다 여기에 연결된 명령 객체의 execute() 를 호출 했었습니다.
이번에 만든 명령 클래스는 특정 시점에 발생될 일을 표현한다는 점에서 더 구체적입니다.
이를 테면, 입력 핸들러 코드는 플레이어가 이동을 선택할 때마다 명령 인스턴스를 생성해야합니다.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Command 클래스가 일회용이라는 게 장점이라는 걸 이제 명령을 취소할 수 있도록 순수가상함수 undo() 를 정의하여 보면 알 수 있습니다.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Undo( ) 에서는 execute( ) 에서 변경하는 게임상태를 반대로 바꿔주면 됩니다. 이제 MoveUnitCommand 클래스에 실행취소 기능을 넣어보겠습니다.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
이전 글에서 정리한 내용은 순전히 Command 클래스로 만든 Jump() 나 FireGun() 과 같은 전역 함수가 플레이어 캐릭터 객체가 사용할 것임을 암시적으로 찾아서 활용해야 한다는 점이 가정에 깔려 있기 때문에, 플레이어만 사용할 수 있다라는 제한이 있습니다.
그러나 꼭 플레이어만 아닌 어떤 몬스터나 캐릭터나 AI가 움직이는 NPC들도
Jump() 나 FireGun() 과 같은 동작을 한다면??
일일이 모든 캐릭터에게 위와같은 Command 클래스를 넣어줘야 한다면 그건 그것나름대로 동작은 가능하지만 불편할 것 같습니다.
그래서 행동 자체를 실체화 시킨 것에서 행동할 액터를 받아오게끔 만드는 방법이 있습니다.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
위에서 GameActor는 게임 월드를 돌아다니는 캐릭터를 대표하는 '게임 객체' 클래스라 보시면 됩니다. 이제 Command를 상속받은 클래스는 execute()가 호출될 때 GameActor 객체를 인수로 받기 때문에 원하는 액터의 메서드를 호출 할 수 있습니다.
위 말은 이제 원하는 액터의 메서드를 호출 하게끔 다음과 같이 Command 클래스가 이용가능해진다는 뜻 입니다.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
코드를 보는것처럼 이제 JumpCommand 클래스 하나로 게임에 등장하는 어떤 캐릭터라도 점프를 할 수 있게끔 가능해졌습니다.
이제 입력 핸들러에서 입력을 받아 적당한 객체의 메서드를 호출하는 명령 객체를 연결하는 것을 만들 차례입니다. 먼저 InputHandler()에서 명령 객체를 반환하도록 변경합니다.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
다음으로 명령 객체를 받아서 플레이어를 대표하는 GameActor 객체에 적용하는 코드가 필요합니다.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
요청자체를 캡슐화하는 것 입니다. 이를 통해 요청과 서로 다른 사용자를 매게변수로 만들고, 요청을 대기시키거나 로깅하며, 되돌릴 수 있는 연산을 지원합니다.(GoF의 디자인 패턴. 311쪽)
위 말만 들어서는 잘 이해가 안가는데? 하고 궁금해할 수 있습니다.
그래서 명령 패턴을 한 줄로 다시 요약해보자면,
명령 패턴은 메서드 호출을 실체화(reify)한 것이다.
라고 할 수 있습니다. 대체 메서드 호출을 실체화 했다라는게 무슨말일까요?
바로 메서드 자체를 어떤 한 객체로 만들었다는 뜻 입니다. 메서드는 우리가 알다시피 어떤 함수형태로 행위를 하는 녀석이지만, 이 행위의 내용을 바꿀수는 없습니다. 그러나 이 행위 자체를 어떤 객체로 만들어서 실체화 시키면 행위의 내용 자체를 다른 것으로 바꿔서 가능하게끔 하는 것이 바로 명령 패턴입니다.
1. 입력키 변경
그럼 명령 패턴을 활용하는 첫 번째 예를 보겠습니다.
보통 모든 게임은 입력(Input)을 통해 어떤 출력(Output)이 나오게 되는데요.
가장 보편적인 입력 방식인 키보드와 마우스를 누르는 코드가 다음과 같이 있다고 치겠습니다.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
그러나 만약 사용자가 입력키에 대한 변경을 하려면 위의 키가 교체 가능하도록 키입력에 대한 어떤 객체가 필요하게 됩니다. 이 때 명령 패턴을 넣어서 한번 키입력을 변경하도록 만들어보겠습니다.
먼저, 게임에서 할 수 있는 행동을 실행할 수 있는 공통 상위 클래스를 먼저 정의합니다.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters