“MSW X Super Hackathon 2022” 에 참가하여 개발한 프로젝트 중 맡은 개발 기능 구현을 진행하며 정리한 포스트입니다.
사용 플랫폼 : MSW
사용 언어: LUA
👉 맡은 파트 - 건물 거래 시스템 개발
❗ 필요한 기능
1) 건물 구매
2) 건물 강화
3) 접속 종료 전 후 건물 거래 상태 유지
우선, 건물 거래 시스템 개발을 위해 필요한 스크립트들은 다음과 같다.
BuildingInteractor의 경우 모든 빌딩에 공통적으로 적용되는 상황들을 다루고자 logic을 사용했다. 건물 정보나 강화 UI창의 경우 한 건물에 대해서 확인을 하기 때문에 building 컴포넌트 스크립트는 개별 정보들을 담아두고, 개별 정보를 interactor에 넘겨서 그에 맞는 UI 업데이트나 구매 처리, 강화 처리 등 모든 building에 공통적으로 적용해야 하는 기능들을 해당 스크립트에서 구현하였다.
건물 구매는 다음과 같은 과정으로 이루어진다.
1. 건물 클릭
2. 건물 구매창 생성
3. 건물 구매 시도 시 현재 가지고 있는 돈 확인
3-1. 현재 가진 돈이 건물 가격 보다 많은 경우 4. 진행
3-2. 현재 가진 돈이 건물 가격 보다 적은 경우 구매 실패 메시지
4. 현재 돈 차감 후 건물 구매 처리 -> Data Storage에도 반영하여 정보 저장
5. 다음 건물 클릭 시 부터 건물 강화와 관련된 UI popup
🔸 건물 클릭 이벤트 처리
먼저 건물 별로 클릭이 들어왔다는 것을 처리하기 위해 각 빌딩이 가지는 컴포넌트인 Building script에 다음 event handler를 추가한다. 해당 스크립트는 현재 건물이 구매된 상태인지 아닌지를 확인해 다음 UI 창을 선택해준다.
1) 건물이 구매되지 않은 상태이면 건물 구매 UI창
2) 건물이 구매된 상태이면 건물 강화 UI창
🔸 건물 구매창 생성
구매되지 않은 건물을 클릭하면 구매창이 생성된다. Building 컴포넌트에서 BuildingInteractor의 OnClickBuilding을 호출하면 building 컴포넌트의 기본 정보를 기반으로 ui 정보를 변경해준다.
1) UIPopup의 경우 popup창이 동시에 여러 개를 띄우는 것을 막기 위해 state로 관리 (Popup으로 update해 다른 popup이 뜨는 것을 막음)
2) 하단의 경우 UIEntity들을 OnBeginPlay에서 경로로 지정한 후, 관련된 Text component의 text에 접근해 building 컴포넌트에서 넘겨 받은 정보로 업데이트
건물 구매 UI 창위에 건물 구매 UI에서 취소하기 버튼을 누르면 entity를 다시 비활성화 시켜서 화면에서 제거한 후 UI state를 Idle로 변경해 다른 popup이 켜질 수 있도록 변경한다.
🔸 건물 구매 시도
유저가 건물 구매를 시도하면 유저의 현재 자산을 확인 후 조건에 맞게 처리한다.
1) UI 창을 보이게 한 후 MoneySystem에서 현재 유저의 돈을 가져와 building 가격과 비교한다. 구매가 가능하다면 현재 돈을 update하고 구매 처리를 한다.
2) 보유한 돈이 더 부족한 경우 구매 실패 메시지로 text를 설정한다.
3) Data Storage에도 구매 반영을 해야한다. 이 부분은 User logic에서 담당하게 되는데 자세한 사항은 뒤에서 다룬다.
유저의 현재 돈이 더 많은 경우 성공 메시지를 띄운다. 건물 구매 완료 UI 이후 건물을 클릭할 때부터는 강화를 묻는 UI 창으로 변경된다. (이는 isBought라는 boolean 값을 building 컴포넌트가 들고 있는데, 해당 boolean의 값에 따라 결정된다.)
확인 UI 창확인 UI 창에서 확인하기 버튼은 다음 function과 연결되어 있다.
🔸 건물 강화 UI
건물을 구매한 후에는 해당 buidling 컴포넌트의 isBought 값을 true로 업데이트 하게 된다. 따라서, 앞에 로직에 따라 구매한 건물을 클릭하면 강화 UI 창이 뜨게 된다. 이때 강화 UI창은 builingInteractor의 GetBuildingInfo를 통해 업데이트 된다.
1) UI state Popup으로 변경 후 panel을 띄운다. building으로부터 받은 데이터들을 기반으로 UI 관련 text, image 등을 업데이트 한다.
2) building의 경우 업그레이드가 가능한 것과 아닌 것 두 종류로 나뉜다. 따라서, 바로 밑에서 canUpgrade라는 값이 true인 경우에 대해서만 버튼을 활성화 해두었다.
3) 업그레이드가 되는 경우 버튼 클릭은 다음 function을 연결시킨다. 구매와 동일한 방식으로 유저가 보유한 돈과 비교 후 업그레이드를 진행할 지 말지 결정하게 된다.
유저가 업그레이드를 하게 되면 강화된 건물의 수익을 적용해야 한다. 따라서, 강화 전 건물과 강화 후 건물 간의 수익 차이(profitDiff)를 계산해 현재 수익에 추가시키는 방식으로 진행한다. 해당 부분 업데이트는 User logic에서 진행한다.
건물 강화 UI창건물 강화와 관련해 UI 변경 및 property들 업데이트 처리는 해당 entity의 building 컴포넌트에 접근해서 진행하게 된다.
1) 빌딩 정보들 담긴 dataset에 접근해서 해당되는 행을 가져와 정보들을 업데이트 한다.
2) 현재 building Component에 저장되어 있는 profit과 row로 찾은 upgrade해야할 빌딩 profit의 차이를 return 한다.
유저가 게임을 재접속 했을 때 유저가 구매한 건물 정보들을 유지해야한다. 해당 부분은 MSW에서 제공하는 Global Data Storage를 사용한다. (Global Data Storage에 관한 정리는 다른 포스트에서 진행 예정)
💡 첫 번째 시도
처음 시도는 table(다른 언어 기준 list)에 건물들의 Entity id들을 저장해서 Data Storage에 저장할려고 했다.
현재 MSW에서 제공하는 Data Storage는 저장할 수 있는 형식이 string이나 int형 뿐이다. 그래서 찾아보다 table형태를 string으로 변환하는 것이 있어 Data Storage에 저장할 때는 string으로 변환하고 불러올 때는 다시 table로 변환해서 들고오도록 구현했다.
Table -> String String -> Table💦 문제점
전반적인 기능을 다 구현하고 테스트 하는 과정에서 3번째 건물부터 구매 처리가 되지 않는 문제가 발생했다. Stirng이 가질 수 있는 길이가 한정적이라서 테이블에서 string으로 변환하는 과정에서 정보 손실이 일어나는 것으로 보였다.
💡 두 번째 시도
테이블 자체를 string으로 변환하는 방법 대신 id 값을 dataset에 primary key 형태로 둔 후 접근하는 식으로 진행했다. 즉, table에 상대적으로 긴 entity id들을 저장하지 않고, 개별 string 값 (i.e. a, b, c..) 을 붙여서 저장하는 방식으로 구현을 변경했다.
예를 들어 빨간 버섯집의 id가 a, 노란 버섯 집의 id가 b이면 Data Storage에 ab를 저장하는 식으로 진행하였다.
리스트 대신 string으로만 저장하는 상황에서 더 좋은 방식이 있나 고민해보았는데 이 방법밖에 떠오르지 않았다..
게임에 넣을 건물 개수를 생각했을 때 string 길이에 충분히 id를 (alphabet) 으로 넣을 수 있겠다는 판단이 들어 우선 이 방식을 채택했다.
+) 팀원분들과 상의해봤는데 대문자, 숫자, 소문자 다 하면 충분하다라고 결론이 나서 이렇게 진행하기로 했다 ㅎㅎ
혹시나 건물이 더 많아져서 id 값 자체가 한 character로도 구분할 수 없는 상황이 온다면 upgrade용 building과 일반 구매 building 데이터셋 자체를 나눠서 접근하면 id 개수가 충분하지 않을까 싶다.
Data Storage와 관련해서는 User logic에서 처리한다. 우선 처음 플레이가 시작 될 때 이전에 구매 또는 강화한 적이 있는 건물들에 대한 정보를 받아야 한다.
현재 로직 상 한 entity에 대해 이게 upgrade 된 것이냐 bought 된 것이냐로 접근하게 된다. (즉, 노란 버섯집의 업그레이드 버전이 주황 버섯집이라면 dataset 자체는 두 건물 정보 모두 같은 entity를 가리키게 된다. 그리고 entity 자체가 upgrade 되었는 지 아닌 지에 따라 해당 entity의 building 컴포넌트의 정보를 결정하게 된다.)
따라서, upgrade와 관련된 Data Storage와 구매된 빌딩과 관련된 Data Storage를 둔다. 그리고 먼저 upgrade 정보를 읽으면서 해당 건물 entity의 IsUpgraded 값을 갱신한다.
1) 빌딩 정보들을 모아둔 데이터 파일 (BuildingDataSet)에 접근할 수 있도록 받고, Upgrade Data Storage 정보도 받아온다.
2) 현재 업그레이드된 건물의 string id(하나의 character)들이 Data Storage에 저장되어 있기 때문에 한 글자씩 끊어 보기 위해 string.sub()을 사용한다.
3) 2)에서 받은 id를 기반으로 BuildingDataset에서 해당 id를 가지는 행을 찾은 후 entity Id column을 받아온다.
4) 3)에서 받은 entity Id를 기반으로 entity를 찾아서 building component에 접근하여 isUpgraded를 갱신한다.
이제 upgrade 된 정보를 갱신한 상태이다. 그 다음으로는 구매된 빌딩 정보들을 받아와서 데이터에 저장된 건물들을 구매 처리해줘야한다.
1) 먼저 Building Data Storage에서 구매했던 건물들의 정보를 받아온다.
2) upgrade와 동일한 방식으로 한 string 씩 끊어서 id를 확인해 entity까지 받아온다.
3) 그 다음 해당 entity의 building component에 접근하여 IsUpgraded의 값에 따라 building을 강화해야 하는 지 말지 결정한다.
4) 그 다음 건물 구매처리를 하고 현재 profit 값을 업데이트 한다.
InitializeVariables는 building 컴포넌트에 정의되어 있다. isUpgraded 여부를 확인한 후 앞의 건물 강화에서 사용한 함수를 사용해 building 강화를 적용한다.
다음은 User logic에 있는 함수들이다. 게임 중 유저가 진행한 상황에 따라 Data Storage에 정보들을 저장함과 동시에 현재 게임 상태에도 바로 반영을 해줘야 한다.
🔹 건물 구매 정보 갱신
1) 현재 건물 정보들을 담고 있는 string 값 (self.buildingList)에 구매한 building id를 붙인다. 추가로 현재 게임에서 구매되어있는 빌딩 값을 받아오기 위한 table(list<string>))인 self.currentBuildings에 구매한 building의 id를 갱신해준다. (이건 다음에 구현할 보유 건물 정보 UI 갱신을 위해 필요하다. )
lua에서는 ..을 통해 string을 붙이더라..!
2) 구입한 건물의 수익을 현재 수익에 더해준다.
3) DataStorage에도 현재 구매한 building 정보를 업데이트 해줘야 한다. 이는 서버에서 처리해줘야 하기 때문에 execution space를 분리해서 함수를 작성하였다. UpdateBuildingInformation에서 Data Storage에 저장(SetAsync)하는 작업을 한다.
🔹 건물 강화 정보 갱신
건물 구매랑 비슷한 방식으로 진행된다.
1) 강화 건물 정보를 담고 있는 string 타입의 upgradeList에 강화된 건물의 id를 저장한다. (이건 entity id가 아니라 강화된 건물의 고유한 id (primary key) 값이다.)
2) 동일하게 서버에서 DataStorage에 저장 처리 하고, 앞서 설명했던 것처럼 현재 profit에 (강화된 건물 profit - 기존 건물 profit)을 더해 수익을 증가시킨다.
정리해보니 지금 data 넘어가는 부분이 헷갈리게 설명되어 있는 것 같은데 (upgrade와 구매 관련) 다시 읽어 보면서 정리해봐야겠다..
+) 맵 포탈타고 넘어가니 구매가 반영 안되는 등 여러 문제점 발생 -> 해커톤 기간 수정함 (update soon)