2024.12.23(월)
오늘은 8주 프로젝트 중간 발표를 하였다. 매우 떨렸지만, 13분의 시간에 맞게 발표하였다. 다음은, 팀원들이 준 발표자 노트의 내용을 나의 언어로 풀어서 바꾼 대본이다.
Docs.Presentation: 13조 중간 발표 PPT
[시작]
안녕하세요 13조 사철남의 중간발표를 맡게 된 이영근입니다. 발표 시작하겠습니다.
[목차]
다음은 이번 발표의 목차입니다.
프로젝트 소개
시연영상
기술적 의사 결정
트러블 슈팅
중간지점에서의 소회 와 같은 순서로 진행될 예정입니다.
프로젝트 소개입니다.
[농담 씬]
저희 프로젝트는 플레이어가 직접 지휘관이 되서 전쟁을 지휘한다면 어떤 느낌일까? 라는 생각에서 출발하였습니다. 중세 시대의 전쟁에서는 지휘관 회의에서 정립한 여러 전략들을 북이나 깃발, 뿔피리 같은 여러 신호 전달 수단을 통해 전달하여 전쟁을 수행합니다. 이런 실제 전쟁 상황을 고려해, 플레이어가 전쟁에 나가기 전에 직접 다양한 전략을 수립하고 전쟁에서 준비한 전략들을 이용해 전쟁을 펼치게 하여 플레이어에게 지휘관으로서 전쟁을 수행하는 경험을 제공하고자 하였습니다.
[레퍼런스 씬]
프로젝트에 들어서며 팀 회의를 통해 정해진 저희의 게임은 영지 경영과 실시간 전투 시뮬레이션을 결합한 턴제 경영 시뮬레이션 장르입니다.
플레이어는 턴을 진행하며 영지를 경영하고, 여러 이벤트들을 통해 다른 영지와의 전쟁을 경험할 수 있습니다.
다음은 시연영상입니다. 시연영상 보시고 가겠습니다.
다음은 기술적 의사결정입니다.
[데이터 관리]
첫 번재로, 게임 데이터를 어떻게 관리할 것인가? 라는 고민을 하였는데, 저희는 JSON 형식으로 데이터를 저장해서 사용하기로 결정했습니다.
[JSON을 사용하는 정확한 이유?]
저희 데이터의 간단한 ERD입니다. 영지, 즉, Domain ID를 기준으로 그 영지에 소속되어있는 기사, 병사들과 영지의 전략들을 가져올 수 있게 하였습니다.
저희는 입력하거나 수정된 데이터를 바로 공유할 수 있고 함께 작업하기 좋은 구글 스프레드 시트를 이용해서 데이터 베이스를 작성하였고, 작성된 시트를 이용해 JSON으로 변경해서 사용하였습니다.
각 데이터 테이블에 맞게 클래스를 만들었고, 다양한 데이터 타입에 대응하여 JSON파일을 불러올 수 있게 제네릭 프로그래밍을 통해 여러 클래스들에 대응할 수 있는 시스템을 구축하였습니다.
[시간의 흐름과 턴을 어떻게 구현해야 하는가]
다음은 기술적 의사 결정의 2번째입니다. 저희 게임인 영지경영과 실시간 전쟁 시뮬레이션의 시간의 흐름과 턴을 어떻게 구현해야 하는가에 대한 파트입니다.
[시간의 흐름 구상]
막상 처음에 전략 게임을 만드려고 하니 막막했습니다. Time.deltaTime으로 시간 흐름을 잡아야 하나?
그런 고민들을 하다가, 전투과 영지경영의 시간 흐름을 분리시켜서 전투만 실시간으로 진행하고, 영지경영을 턴제로 진행하기로 하였습니다.
보통의 턴제 전략 게임의 경우, 시간이 정량적으로 흐릅니다. 턴 한번을 누르면 진행되는 시간의 길이가 일정 길이로 정해져 있습니다.
예를 들어, 문명의 한턴의 길이는 bc4000년부터 시작해 2150년까지를, 정해진 턴 총량만큼을 나눈 값입니다.
삼국지14와 같은 경우는 한 턴의 길이가 10일로 정해져 있습니다.
그러나, 풋볼매니저에서는, 한 턴은 하루일 때도 있지만, 어쩔 땐 이틀, 어떤 경우엔 며칠을 훅 건너 뛰기도 합니다.
이렇게 진행되는 이유는, 풋볼매니저는 한 턴 진행이 이벤트 중심으로 설계가 되어 있기 때문입니다.
저희 프로젝트의 기획도 이와 비슷합니다.
저희 시간의 흐름 아이디어는 이렇습니다. 각 의사결정 주체들, 즉, 이벤트 발생 주체들이 이벤트를 발생 시키면, 해당 이벤트를 대기열 컬렉션에 집어넣습니다.
각 주체들이 넣은 이벤트들은 대기열에 쌓여있을 것입니다. 그 중에 가장 종료예정일이 짧은 이벤트를 기준으로 종료예정일이 가장 가까운 날짜로 시간을 이동시킵니다.
그래서 한 턴의 길이는 이벤트들 간의 종료일 간격이 됩니다.
[턴 진행 구현]
지금까지는 시간의 흐름을 어떻게 표현할 지에 대해 정했습니다. 그렇다면, 턴 진행을 어떻게 구현해야 할까요?
사실 시간의 흐름과 턴은 관련이 있지만서도 별개의 구분되는 개념입니다. 플레이어는 시간이 연속적으로 흐른다고 느끼지만, 내부 설계에서는 시간이 턴 진행에 따라 워프하는 형식으로 진행됩니다.
또한, 플레이어가 체감하는 한 턴은, 사실 자신의 턴 뿐만 아니라, AI의 턴과 월드 턴을 포함하고 있습니다. 다음과 같이 내턴, 행동결정, 결정행동 대기열 등록, ai턴, 침공여부결정, 대기열 등록, 월드턴, 이벤트 결정, 결정행동 대기열등록, 다시 내 턴, 이런 식으로 진행됩니다.
따라서 실제로 턴의 흐름은 그림과 같습니다. 어디선가 본 구조같지 않나요? 뭔가 FSM같지 않나요? 맞습니다. 각 주체들의 턴을 State로 간주할 수 있겠다라는 생각에 상태패턴을 활용해 턴을 구현했습니다.
그래서 결과적으로 턴매니저는 턴진행흐름을 관리하고, 타임매니저는 시간의흐름을 계산하거나 이벤트 대기열을 관리하는 역할을 맡음으로서 시간과 턴을 관리할 수 있었습니다.
다음은 트러블 슈팅입니다.
[이벤트 결과 출력메서드에 문제가 있다!]
트러블 슈팅 첫 번째 문제입니다. 이벤트 결과 출력메서드에 문제가 있었습니다.
이벤트가 종료되면 그 이벤트들의 결과를 업데이트하는 메서드자체는 문제가 없지만 유지보수 측면에서 문제가 있다고 봤습니다.
나중에 게임 볼륨을 늘릴때 이벤트를 마구마구 늘리게 될텐데, 그럼 이벤트 하나가 추가될 때 마다 else if를 계~속 추가하게 되어 유지보수와 확장성 측면에서 문제가 생깁니다.
매일 저녁 자기 업무진행을 보고하는 스크럼에서 이영근팀장이 과거 자기 작업을 경험으로 코드 개선점을 제시해줬고, 충분히 좋은 개선방향이라고 느꼈습니다.
이영근팀장의 개선방향에 더불어 전략패턴까지 잘 어우러질 수 있겠다는 생각이 들었습니다.
결과적으로 이제 이벤트들이 추가될 때마다 else if를 추가하면서 해당 메서드를 매번수정하지 않게 되었습니다.
이벤트가 추가될 때 마다, 생성자 한줄 추가와
IEventHandler를 상속받는 새로운 클래스만 추가해주면 됩니다.
[Drag이벤트 간에 문제가 있다!]
다음 트러블 슈팅은 드래그 이벤트 간에 생긴 문제입니다.
UI에서 드래그 앤 드롭을 구현할 때, Unity 지원하는 인터페이스들을 이용해서 구현할 수 있습니다.
DraggableUI라는 드래그 앤 드롭을 할 대상을 만들어, 스크립트에 해당 인터페이스들을 상속받게 되면, EventSystem가 자동으로 OnBeginDrag, OnDrag, OnEndDrag 메서드들을 호출해서 드래그 동작을 관리합니다.
그런데, 여기서 문제가 생기는 점은, ScrollView도 똑같은 인터페이스들을 사용한다는 것입니다.
결과적으로 DraggableUI와 ScrollView의 같은 인터페이스와 메서드를 사용하기 때문에, EventSystem이 가장 먼저 호출된 메서드만 처리하여, 다음과 같이 드래그를 하였을 때, 드래그 앤 드롭 시스템만 작동을 하고, 스크롤 뷰가 작동을 하지 않는 현상이 발생합니다.
이 문제의 수정 방향성은 이렇습니다.
보유기사 탭의 ScrollView 영역에서는 ScrollView의 유니티 Drag 이벤트 동작을 수행하고, ScrollView가 아닌 곳에서는 DraggableUI의 유니티 드래그 이벤트 동작을 수행하도록 수정하였습니다.
[폭력적인 script 파트]
기존 DraggbleUI의 OnBeginDrag가 호출될 때, ScrollView의 영역에 있는지 확인하고, ScrollView의 영역이면, ScrollView의 OnBeginDrag를 실행합니다.
마찬가지로, OnDrag 중일 때, ScrollView 영역인지 확인하고, 바깥이면, OnEndDrop으로 ScrollView의 드래그를 끝내고, 드래그 앤 드랍의 드래그를 시작하고, ScrollView의 바깥에서 안으로 들어온 상황이면, 드래그 앤 드롭을 끝내고, ScrollView의 OnBeginDrag를 시작합니다.
마지막으로 OnEndDrag에서 현재 상태가 ScrollView였으면, ScrollView의 OnEndDrag로 드래그를 종료합니다.
수정후엔 다음과 같이 스크롤 뷰 내부에선 스크롤 동작을, 바깥에선 드래그 앤 드롭 동작을 하는 것을 볼 수 있습니다.
[JsonUtility에 문제가 있다!]
다음 트러블 슈팅은 JsonUtility에 문제가 있던 것입니다.
Json 데이터가 배열로 저장되어있을 때, 배열을 수정하면, Json데이터가 싹 날라가버리는 문제가 있었습니다.
JsonUtility를 사용하여 Json데이터를 변경시에 배열값을 변환을 재대로 해주지 못하는 경우가 있어서, 그것을 해결하기 위해서 뉴톤소프트 패키지를 통해 Json을 변환하는것으로 문제를 해결하였습니다.
[프리펨 설정에 문제가 있다!]
마지막 트러블 슈팅입니다. 프리펩 설정에 문제가 있었습니다.
UI를 동적으로 생성하는 과정에서 뒤쪽의 큰 창과 앞쪽의 작은 창이 각각 다른 UI 프리팹으로 구성되어 있었습니다.
이로 인해 동일한 버튼 프리팹을 사용하려 할 때, Inspector(인스펙터)에서 위치를 수동으로 설정하는 방식이 제대로 적용되지 않는 문제가 발생했습니다.
이를 해결하기 위해 새로운 버튼 프리팹을 만들어 사용할 수도 있었지만,
이 방식은 생성된 버튼의 정보를 다른 버튼에 계속 전달해야 하는 번거로움을 가져올 수 있었습니다.
따라서 같은 버튼 프리팹을 재사용하는 것이 더 효율적이고, 유지보수 측면에서도 적합한 방법이라고 판단했습니다.
FindGameObject로 특정 오브젝트의 Transform에 접근해서 버튼을 넣어줍니다.
동일한 데이터를 활용해 다른 UI에 두 번째 버튼도 생성합니다.
부모 오브젝트가 없을 경우, GameObject.Find를 통해 동적으로 탐색하도록 설계하였습니다.
이 버튼은 클릭 시 클릭하는 위치에 따라서 주어진 기능이 달라집니다.
위에 박스는 버튼 클릭시에 LoadCustomUI를 보이게 하는 용도, 아래 박스는 버튼 클릭시에 임무를 기사에게 할당하는 기능을 합니다.
결과적으로 이와 같이 버튼이 UI에 동시에 생성되고 두 버튼 다 같은 정보를 가지고 있지만, 각각 다른 역할을 할 수 있게 되었습니다.
마지막으로, 중간 지점에서의 각자의 소회입니다.
저희 팀원들이 중간 지점까지 진행하면서 각각 느낀 점들을 적어보았습니다.
이상으로 13조 사철남의 중간 발표를 마치겠습니다.
감사합니다.