개요
Rampage 프로젝트를 진행할때 미리 추진계획서를 작성하지 않아, 개발에 대한 전체적인 틀이나 합의를 거치지 않았었다. 그로 인해, 개발이 점차 진행되면서 객체 간의 상호작용이 많아졌지만 개발 방식이 제각각이라 점차 스파게티 코드가 되었고, 기존 구현 내용을 유지하면서 앞으로 타격감 표현기법들 같은 개발할 것들을 쉽게 합칠 수 있는 확장성 좋은 개발이 필요하게 되었다. 팀원들과의 회의를 통해 관찰자 패턴에서 아이디어를 얻어 이미 개발이 어느정도 진행된 프로젝트 상황에 맞게 개량해보기로 했다.
관찰자 패턴을 간단히 설명해보면 어떤 객체의 상태가 변화했을 때, 이 변화에 관심이 있는 다른 객체들한테 객체가 변화했음을 알려줄 수 있는 디자인 패턴이라고 할 수 있다. 여기서 아이디어를 얻어, 나는 게임 내의 어떤 객체가 변화했다고 했을 때, 변화했음을 메시지 전달자 클래스인 CMessageDispatcher에게 그 정보를 전달하고 그 변화에 관심있는 객체들에게 모두 변화했음을 BroadCast 방식으로 전달하기로 했다. 이때, BroadCast 방식이 아니라 특정 객체에게만 변화를 전달하고 싶어지는 경우도 생길 수 있을 것이라 판단하고 특정 객체에게만 변화했음을 알릴 수 있게 구현해보고자 했다. 요약해보면 다음과 같다.
- 객체가 변화했을 경우 CMessageDispatcher에 변화했음을 전달.
- 특정 변화에 관심이 있는 객체들은 CMessageDispatcher를 통해 변화가 발생할 경우 메시지를 전달받음.
- BroadCast 방식 뿐만 아니라 특정 객체에게만 메시지를 전달할 수도 있게끔 구현
MessageType
enum class MessageType은 다음과 같이 플레이어 사망, 플레이어 공격과 같은 특정 변화를 의미한다.
Message
메시지를 의미하는 Message 클래스는 MessageType 한개를 멤버 변수로 가지며 이를 리턴하는 getType() 함수가 있다.
IMessageListener
IMessageListener 인터페이스 클래스는 활성화 여부를 뜻하는 m_bEnable 변수와 여러 파라미터에 대한 가상함수를 가진다. 메시지 수신 클래스들은 IMessageListener 인터페이스 클래스를 상속 받아 구현된다.
다음은 플레이어 사망 메시지를 수신하는 PlayerDeadListener 클래스 예시이다.
PlayerDeadListener라는 이름의 수신자 객체는 PLAYER_DEAD라는 메시지가 발생했을 때, 수신자 객체를 가지고 있는 씬 객체의 메시지 처리 함수인 HandlePlayerDeadMessage() 함수를 호출해 플레이어가 사망했을 때, UI를 띄우는 등의 작업을 하게 한다.
ListenerInfo
ListenerInfo 구조체는 수신자 객체인 IMessageListener의 포인터와 특정 객체로의 수신을 구현하기 위한 filterObject라는 이름의 void*를 가진다.
CMessageDispatcher
먼저, 메시지 전달자 클래스인 CMessageDispatcher 클래스의 m_listeners 배열은 ListenerInfo 구조체를 저장하는 벡터들을 의미한다. m_listeners 배열의 크기는 메시지 타입 개수만큼 존재해 각 메시지 타입별로 ListenerInfo 구조체를 저장한다.
CMessageDispatcher는 수신자 등록을 위한 RegisterListener(MessageType, IMessageListener*, void*) 함수와 메시지 전달을 위한 Dispatch_Message(MessageType, Params, void*) 함수를 가지며 각각은 다음과 같다.
RegisterListener(MessageType, IMessageListener*, void*)는 전달 받은 인자로 수신자 구조체인 ListenerInfo를 생성, 메시지 타입에 맞게 m_listeners 벡터에 저장한다.
CMessageDispatcher::Dispatch_Message(MessageType, Params, void*)는 메시지 타입에 해당하는 벡터를 순회하면서 수신자의 filterobject 변수가 인자로 넘어온 sourceobject 변수와 일치하거나 filterobject 변수가 nullptr로 필터링할 오브젝트가 없는 수신자에게 메시지와 파라미터를 이용해 메시지를 처리할 수 있도록 HandleMessage 함수를 호출해준다.
filterobject를 비교함으로서 BroadCast 방식 뿐만 아니라 특정 객체에게만 메시지를 전달할 수도 있게끔 구현하였다.
예시
메시지 수신이 필요한 객체들의 클래스에는 위와 같이 m_pListeners라는 수신자 포인터 벡터를 가지고 객체가 생성될 때, 생성자에서 수신자를 생성해 CMessageDispatcher::Dispatch_Message(MessageType, Params, void*)를 이용해 수신자를 등록한 뒤, 저장한다.
위와 같이 메시지 송신이 필요할 때, CMessageDispatcher::Dispatch_Message(MessageType, Params, void*) 함수를 이용해 메시지를 특정 객체에게 전달하거나 모든 객체에게 전달한다.
아쉬운점.....
이미 어느정도 구현이 되어있는 프로젝트에 급하게 확장성을 위한 시스템을 넣으려다보니, 다시봐도 좋은 구조가 아니라고 생각한다. 혼자 사용한 것도 아니라 팀원들과 사용하다보니 생각보다 메시지 타입이 많아지기도 했고, 억지로 특정 객체에게 메시지도 전달할 수 있게 만드려고 하다보니 원래 생각했던 코드에서 많은 변화가 생기기도 했다. 소스 없던 생 스파게티가 그래도 소스랑 잘 어우러진 스파게티가 된 느낌...? 정도의 개선밖에 되지 않는 느낌이다. 그래도 다음에 프로젝트를 진행할 수 있게된다면 팀원들과의 커뮤니케이션을 통해 구현에 대해 합의를 하고, 개발을 진행하는 방향을 해야한다는 좋은 깨달음을 얻었고, 좋은 경험이였다고 생각한다.
'DirectX 12 프로젝트 > Rampage' 카테고리의 다른 글
Rampage - 충돌 처리(2) (0) | 2023.10.18 |
---|---|
Rampage - 충돌 처리(1) (0) | 2023.10.12 |
Rampage - Dear ImGui를 이용한 시뮬레이터 UI(5) (0) | 2023.10.12 |
Rampage - Dear ImGui를 이용한 시뮬레이터 UI(4) (0) | 2023.10.12 |
Rampage - Dear ImGui를 이용한 시뮬레이터 UI(3) (0) | 2023.10.12 |