[The wise heart] How symbols work
In difficult times we humans need sanctuaries and images of the sacred to remind us of who we really are.  Our imagination works in symbols, like the images from our dreams. We use symbols all the time:in clothing, in gestures, in advertising, in the very letters of these words. Buddhist psychology uses these human images of buddhas and saints and enlightened ancestors as symbolic doorways, to p..
2025.02.01
no image
워크래프트3 던전 주파형 유즈맵, 용병던전
게임 공략 영상 : https://www.youtube.com/watch?v=8aNHZYGdo1g&t=1738s&pp=ygUM7Jqp67OR642Y7KCE 1. 게임 개요2. 게임 오프닝 영상 & 시놉시스3. 등장 캐릭터4. 던전5. 잘된 점6. 잘못된 점7. 교훈8. 게임 크레딧 영상 1. 게임 개요 워크래프트 3 유즈맵 중 '던전 앤 드래곤'이라는 던전 돌파형 맵이 있었다. 벨트스크롤 액션 형태의 원본 아케이드 게임을 RTS 유즈맵으로 재창작한 게임이었는데, 다수의 동료들과 던전을 헤쳐나가고 보스를 상대하는 형태가 어렸던 나의 도전의식을 자극해 몰입하며 즐겼던 기억이 있다.   이런 구성에 감명받은 적이 있던 나도 재미있는 던전 돌파 -> 보스 격파 형태의 게임을 만들고 싶었고, 총 10명의 유저가 ..
2025.01.01
no image
InWanderLand - 쿼터뷰 전략 액션 게임
깃허브 주소 : https://github.com/GAFinal-Predator/Predator게임 시연 영상 : https://youtu.be/xjCrXEDaerc?si=laKyc-V1Po2zvh5H  목차 1. 프로젝트 개요2. 프로토타입 게임 개발3. 빌드머신 구축, 빌드와 테스트의 자동화4. Yunuty 게임엔진 개선4.1. 길찾기 라이브러리 Recast Navigation 연동4.2. 물리 라이브러리 PhysX 연동4.3. 컴포넌트 코루틴 구현5. Wander 에디터6. RTS 프레임워크 개발7. UI8. 성과8.1. 게임 플레이 영상8.2. 개발 과정 영상8.3. 도쿄 게임쇼9. 느낀 점 1. 프로젝트 개요 게임 인재원 파이널 졸업 프로젝트는 1년의 시간을 두고 진행되는 장기 게임 개발 프로젝..
2024.08.14
no image
마음챙김의 네가지 규율
서양에서의 마음챙김 명상회에서는 필요한 4가지 규율을 RAIN이라는 약어로 가르친다고 한다. 규율은 다음과 같다.1. Recognition2. Acceptance3. Investigation- Body- Feelings- Mind- Dharma4. Non-identification  우리말로 옮기자면 다음과 같겠다.1. 인지2. 수용3. 탐구- 몸- 느낌- 마음- 법(다르마)4. 비동일시 1.인지 인지는 우리가 어떤 인생의 교착 상태에 빠져 있다면 정확히 어떤 문제에 빠져 있는지 직시할 용기와 성의가 필요하다는 점을 말한다. 우리가 일을 있는 그대로 보려는 의지가 있어야 자기기만의 굴레에서 벗어날 수 있다. 당뇨병 환자가 본인의 질환을 부정하거나, 경제적 문제가 있는 사람이 자신의 과소비 성향을 무시하는..
2024.08.13
no image
PBR
매끈매끈한 강철 공을 띄우고 , 이 공이 주변 환경 이미지를 반사하는 모습을 확인했다. PBR에 쓰이는 세부적인 공식들을 모두 이해하지는 못했지만, PBR은 다음과 같은 요소들을 중시한다는 점만 일단 새겨두면 될 것 같다. 1. 표면에 반사되는 굴절되는 빛의 합은 일정해야 한다. 즉, 에너지 보존 법칙을 고려해야 한다. 2. 빛의 반사율은 시선벡터가 표면의 법선 방향과 둔각을 이룰수록 높아진다는 프레넬 반사를 고려해야 한다. 3. PBR은 까끌까끌한 미세면의 존재를 의식하며, 국소면의 까끌까끌한 정도를 roughness라는 수치로 나타낸다. 표면은 roughness가 낮을수록(매끈매끈할수록) 정반사하는 성질이 생긴다.
2024.04.05
no image
Deferred Rendering에 대한 시도 : 큐브맵 적용
장면이 너무 허해서 큐브맵을 적용해 보았다. 확실히 태가 좀 살고 코딩할 맛이 더 난다. 하늘에서 내리쬐는 상수 디렉셔널 라이트를 만들어서 diffuse 색상과 N dot L 연산만 실행시켜 보았다. 지금은 탄젠트 스페이스에서 정의된 노멀 값을 그대로 가져다 썼기 때문에 제대로 된 노말 연산은 되지 않는 모습이다. 각 픽셀별 노말 값과 노말 텍스처의 값을 조합하여 제대로 된 N dot L 연산을 해본 다음 PBR 공식을 가져와 렌더링을 해봐야겠다. 모자의 머리통 부분에 계단 현상이 생기는게 아쉽다. 디퍼드 렌더링 방식은 안티얼라이싱 적용이 힘든 구조라고 하던데, 이건 먼 훗날에 개선해봐야겠다.
2024.03.18
no image
Deferred Rendering에 대한 첫번째 시도
디퍼드 렌더링은 물체들에 대한 정보들을 여러 장의 텍스처에 저장해놓고 마지막으로 출력물을 계산할 때 각 텍스처로부터 정보를 종합하여 빛 연산을 진행하는 렌더링 기법이다. 디퍼드 렌더링을 더 깎기 전에 물체 표면의 정보를 저장하는 g버퍼들의 정보를 항상 화면 상단에 띄워 확인할 수 있도록 만들었다. g버퍼들의 정보는 왼쪽에서 오른쪽으로 차례로 각 픽셀들의 위치, 노멀 방향, 알베도 색상, ambient occlusion, roughness, metallic 정보를 rgb 값으로 저장한 ARM 정보에 해당한다. 지금은 빛 연산은 배제하고 최종 출력값으로는 알베도 색상 값만 출력하게 했다.
2024.03.15
no image
맵 에디터 개발의 흔적
테스트 시연 피직스 충돌 기반 피킹 각 패널 기능 설계
2024.01.31
 In difficult times we humans need sanctuaries and images of the sacred to remind us of who we really are.

 

 Our imagination works in symbols, like the images from our dreams. We use symbols all the time:in clothing, in gestures, in advertising, in the very letters of these words. Buddhist psychology uses these human images of buddhas and saints and enlightened ancestors as symbolic doorways, to point to and evoke the qualities of love, dedication, inner beauty, and courage. On a different level, a national flag does this, a football team logo can, even a Hermes bag or, more destructively, a swastika. Knowing the power of symbols, we can recognize them as outer forms that point to our inner world.

 

 Here is an example of how such symbols work. Imagine meditating on the most peaceful image of a Buddha you have ever seen. Imagine receiving this image from a teacher who embodies the benevolent qualities of a Buddha and reminds you that you can find these highest possibilities in yourself. Picture learning to visualize this beautiful Buddha so steadily and clearly that when you close your eyes, you can see every detail. Imagine spending some hours letting yourself actually feel the energy of calm, steadiness, and clarity depicted by this Buddha. Let these feelings touch your own heart. Now imagine a step further. Sense that you can draw this Buddha inside, to fully enter and take over your own body and mind. Now you have become a Buddha. Sense how you can actually embody the calm, clarity, and compassion. Dwell in this state for a time and allow yourself to imagine how you would act as a Buddha in your very own life and how you would see the same Buddha nature in those around you. Finally, dissolve the Buddha back into emptiness, acknowledging how the mind creates and un-creates all possibilities.

' > The Wise Heart' 카테고리의 다른 글

마음챙김의 네가지 규율  (0) 2024.08.13

게임 공략 영상 : https://www.youtube.com/watch?v=8aNHZYGdo1g&t=1738s&pp=ygUM7Jqp67OR642Y7KCE

 

1. 게임 개요

2. 게임 오프닝 영상 & 시놉시스

3. 등장 캐릭터

4. 던전

5. 잘된 점

6. 잘못된 점

7. 교훈

8. 게임 크레딧 영상

 

1. 게임 개요

 워크래프트 3 유즈맵 중 '던전 앤 드래곤'이라는 던전 돌파형 맵이 있었다. 벨트스크롤 액션 형태의 원본 아케이드 게임을 RTS 유즈맵으로 재창작한 게임이었는데, 다수의 동료들과 던전을 헤쳐나가고 보스를 상대하는 형태가 어렸던 나의 도전의식을 자극해 몰입하며 즐겼던 기억이 있다.

 

좌 : 벨트 스크롤 아케이드 게임 던전 앤 드래곤, 우 : 왼쪽의 게임을 워크래프트3 유즈맵으로 재창작한 '던전 앤 드래곤' 플레이 화면

 

 이런 구성에 감명받은 적이 있던 나도 재미있는 던전 돌파 -> 보스 격파 형태의 게임을 만들고 싶었고, 총 10명의 유저가 힘을 합쳐 5개의 스테이지와 보스를 클리어하는 방식으로 게임을 구성했다.

 

2. 게임 오프닝 영상 & 시놉시스

 

 기획 의도는 '매우 다양하고 독특한 개성을 갖고 있는 캐릭터들이 조화를 이루며 던전을 클리어하는 게임을 만들어 보자.'는 것이었다. 스토리 또한 이런 의도에 맞게 '세상을 이상적인 형태로 관리하려는 초월적인 존재, '개선자'가 여러 세계를 탐색하며 찾은 뜻있는 이들을 용병으로 고용해 세상의 문제를 해결한다.'는 편리한 이야기다.

팬더같이 생긴 자는 초월적인 존재인 개선자, 이를 바라보고 있는 10명의 영웅들은 개선자가 세계를 돌며 등용한 자들로, 사회악을 청소하는 용병들이다.

 

3. 등장 캐릭터

 플레이 스타일이 뚜렷한 캐릭터들은 특별히 볼드체로 표기했다.

 

3.1. 겁 없는 촌부, 방앗간네 박씨

 부패한 정부관료에게 아내와 딸을 잃고 봉기를 일으키려 한 농부입니다. 폭발적으로 많은 소환수를 소환해 적을 숫자로 압도할 수 있습니다.

더보기
- 사용 기술

 

 

 
 
 
 

  

- 배경 이야기

 

 박씨는 모든 용병들 중 가장 무난한 삶을 살아 본 인물입니다. 그는 살아온 인생 35년 중 27년을 밭을 갈고 씨를 뿌리고 추수를 하는데에 보냈습니다. 27년간 박 씨는 커다란 자극 없이 무난하고 행복한 삶을 살았고, 그 27년간의 행복한 세월은 그의 수동적인 인생관을 단단하게 굳혔습니다.
그 27년간 같은 일을 반복하며 안정적인 생활을 통해 쌓은 무감각함의 관성 때문인지, 그는 자신이 일하는 논과 자신이 사는 마을 밖에서 일어나는 일들이 얼마나 심각한 지도 눈치채지 못했습니다.
박 씨가 살던 나라의 새로운 독재자 김택은은 특유의 정치 감각으로 쿠데타를 저지른 후 그것을 미화시켜 농민들의 지지를 얻었습니다. 하지만 이후 그의 기형적인 권력유지체계가 수많은 숙청과 학살을 야기했고 부패와 광기, 공포로 사회는 병들어갔습니다. 결국 박씨는 자신의 딸과 아내가 부패관리에게 한꺼번에 겁탈당한 후 살해당하자 졸지에 가족을 모두 잃은 처참한 피해자로 전락하는데, 그는 이런 급격한 심적인 충격을 다스리지 못하고 낫과 호미로 고을원님의 목을 따러 갑니다.
당연히 이런 무모한 돌격에는 아무런 성과가 없었고, 되레 자신의 목이 따이려는 찰나 순진하고 만만하고 흥분한 고기방패를 찾던 개선자에게 김택은을 처치할 기회를 얻는 조건으로 용병으로 고용되었습니다.

 

3.2. 잉꼬부부 에밀리와 하환백

 사별한 남편의 영혼을 활에 담아 함께 싸우는 여전사입니다. 광역피해를 입히는 것에 능한 원거리 공격수입니다.

더보기

 

- 사용 기술

 

 


 
 

- 배경 이야기

 

에밀리는 자국 외교대사의 직속 호위대장으로 일하고 있었는데, 그녀의 일솜씨는 굉장히 뛰어났지만 한편으론 난폭하고 강한 기질과 한없이 성급한 성격을 가져 에밀리는 괴팍한 악녀로 악명이 높았습니다. 물론 남성중심적 풍조의 자국 사회분위기에서 여성이 요직을 꿰찬 탓에 이야기가 과장된 것인지, 아니면 그런 남성들의 눈초리에 성미가 독해질 수밖에 없었던 것인지는 알 수 없습니다.
어느 날 인근 강대국에 조공을 바치는 행사에 호위를 맡게 된 그녀는 강대국 사절단의 호위대장 하환백과 업무상의 대화를 나누게 되었는데, 에밀리는 안 좋은 소문이 도는 약소국의 여성무관인 자신에게 한없이 친절하고 예를 지키던 하환백의 태도에 감화되었고 그 후로 오래도록 그와 정분을 나누게 됩니다. 하환백은 그녀의 거친 성격의 이면에 상처가 숨겨져 있음을 처음으로 이해해 줬으며 결국 그 둘은 혼인합니다.
하지만 극과 극의 성미를 가진 사람들의 사랑이야기로 사람들의 입에서 자주 오르내리던 그들의 관계는 오래가지 못했습니다. 그들은 양국 간의 평화를 의미하는 비공식적인 상징이 되었으나 양국의 마찰로 이익을 얻을 수 있다 판단한 부패고위관료들의 음모로 둘은 불시의 기습을 받았고 하환백은 그녀를 지키다 사망합니다. 하환백은 에밀리의 행복을 위해 그녀에게 자신이 죽으면 재혼하라는 유언을 남기지만 에밀리는 정신을 반 날려먹은 듯한 충격과 상실감을 견디지 못하고 산속에 남편을 묻은 후 옆에 오두막을 짓고 살았습니다.
나날이 늘어가는 죄책감과 남편에 대한 그리움 끝에 극단적인 선택을 하려는 찰나 개선자가 나타나 용병으로 일해준다면 에밀리의 활에 남편의 영혼을 담아 남편과 다시 함께할 기회를 주겠다는 제안을 했고, 그녀는 그 제안을 단번에 받아들였습니다.

 

3.3. 정파 무속인 라이 양

 부족장의 아들로서 강한 긍지를 가진 무속인입니다. 아군을 치유하고, 보호하며, 죽은 동료를 부활시킬 수 있습니다.

더보기

 

- 사용 기술

 

 
 
 

 

- 배경 이야기

 

라이 양은 자신이 속한 부족제사장의 장남이자 후계자입니다. 그는 동생 샤오 양과 함께 부족의 전통을 지키며 살았으며, 올곧은 행실과 성품으로 주변 사람들 사이에서 그 명망이 높았습니다.
그러던 어느 해 바다 건너 다른 미지의 대륙에서 온 푸른빛 피부의 이질적인 이민자들이 그의 고향에 정착합니다. 그 후로 얼마 지나지 않아 동생 샤오 양의 심기가 이상해지더니, 점점 그녀는 부족의 제사에 불참하고 외지인들과 놀러 다니며 이민자들의 학문에 관심을 갖는 등, 사파의 길로 새게 됩니다.
라이 양은 오빠로서 동생을 바른 길로 이끌어주기 위해 노력했지만 동생은 결코 말을 듣지 않았고 어느 날 둘이 심하게 다툰 이후로 샤오 양이 실종되자 그제야 뭔가 잘못되어 있었다는 걸 느낀 라이 양은 서둘러 샤오 양을 찾아 나서지만 샤오 양의 시신만 찾게 됩니다. 제사장의 피를 이어받은 사람들 중 강인한 정신력을 가진 사람은 죽은 사람을 부활시킬 수 있는 권능을 가지고 있었(다고 믿어졌)으나 이는 자신의 부족의 숭고한 원칙인 대순환을 역행하는 일이기에 라이 양은 동생의 시신을 수습한 후 그 앞에서 동생의 목숨과 규율 중 뭘 우선시해야 하는 것인지 고민합니다.
그때 만만하고 순진한 의무병을 찾던 개선자가 갑자기 그 앞에 등장하고, 라이 양에게 용병업을 통해 샤오 양을 살릴지 말지 고민할 시간을 주겠다고 제안합니다. 라이 양은 동생을 부활시킬지 말지 동생의 시신이 부패하기 전에 서둘러 결정을 내려야 했지만 개선자의 힘을 빌린다면 용병일을 끝내고 다시 이 시점으로 돌아올 때까지의 시간을 통해 진지하게 고민을 해 볼 수 있었기에 개선자의 제안을 받아들였습니다.

 

3.4. 정신병자 탄초

 끊임없는 불안과 자기혐오에 휩싸인 추방자입니다. 넘치는 정신적 에너지를 승화하여 아군의 마나를 회복시키고 적군을 혼란에 빠트립니다. 무작위로 동료를 부활시킬 수 있습니다.

더보기

 

- 사용 기술

 

 

- 배경 이야기

 

 탄초는 정신건강이 양호한 사람들과 마찬가지로 선천적으로는 정상적인 아기로 태어났습니다. 하지만 일에 바빠 가정을 돌볼 틈이 없었던 아버지와 화를 잘 내는 신경질적인 기질의 어머니 때문에 탄초는 유년기를 불안하고 불행하게 보냈고, 불행하고 불안했던 유년기의 영향으로 탄초는 죽 정서불안에 시달려 사회에서 둔재, 고문관, 또라이 취급을 받으며 살았습니다.
탄초가 30대가 되던 해에, 인근 국가에서 종교개혁이 일어나 그 영향이 사방으로 번지기 시작했는데, 그 종교개혁의 주 내용은 온전히 사회를 지키기 위해서는 마녀, 흑염소, 악마의 숙주를 있는 대로 색출해 화형, 혹은 정화해야 한다는 것이었고, 말과 행동이 어눌했던 탄초는 그중 '악마의 숙주' 혐의를 받아 입에 끓는 납물을 붓는 신성치료를 선고받고 감옥에 갇히게 되었습니다.
축축한 감옥에 갇힌 채 단념한 채로 최대한 안 아프게 자살하는 방법을 고민하던 탄초 앞에 갑자기 개선자가 나타났고, 개선자는 많은 세계를 쏘다니며 얻은 지식을 바탕으로 그를 신경증환자로 진단하며, 그에게 협조의 대가로 정신질환을 진지하게 받아들이는 문명으로의 이민을 약속하며 용병업을 제안합니다.
그저 자신의 성격과 행동양식을 운명적인 저주로 인식하고 자책하던 탄초에게 있어서, 자신을 도움이 필요한 병자로 정의하는 '정신병'이란 단어는 자신의 인생에 개선의 여지가 있으리란 희망을 줬습니다. 개선자의 제안은 탄초에게 완벽히 이상적인 두 번째 기회로 다가왔고 그는 흔쾌히 제안을 수락했습니다.

 

3.5. 퇴역군인 가롯 보울

 불명예스럽게 은퇴한 늙은 퇴역군인이었지만, 개선자의 힘으로 회춘해 찬란한 명예를 되찾고 싶어 합니다. 다른 캐릭터와 연계해 광역 피해를 입힐 수 있는 근접 공격수입니다. 

더보기

 

- 사용 기술

 

 

 

- 배경 설명

 

가롯은 자신의 세계에서 망치 할아범으로 불리는 괴짜 외톨이였습니다. 하지만 젊었을 적의 그는 전쟁영웅이었고 온 국민의 존경을 한 몸에 받는 나라의 아이콘이었습니다.
중년이 된 가롯은 군인의 신분에서 내려와 그동안 모아둔 돈으로 프랜차이즈 맥주사업을 하지만, 갑작스레 나라를 휩쓴 혁명이 가롯이 치른 전쟁을 비롯한 역사를 재조명했고, 순식간에 가롯은 전쟁영웅에서 영아살해자의 오명을 뒤집어쓰게 됩니다.
이런 불명예를 잠시도 견디지 못했던 가롯은 이전 이름을 버리고 현재의 가롯 보울이란 이름으로 개명한 후 외딴 벽지로 내려가 살지만, 모든 부와 명예를 잃어버린 가롯은 영웅으로 추앙받던 과거의 잔영에 집착하게 되고, 온갖 기행을 일삼는 괴짜 할아범으로 추하게 늙습니다.
남은 건 과거에 대한 그리움뿐인 가롯을 지켜본 개선자는 그에게 남은 재능과 갱생의 가능성을 함께 본 후, 가롯에게 용병으로 일 해준다면 젊음을 돌려줄 것이라 제안했고, 가롯은 왕년의 영광을 되찾을 수 있을지도 모른다는 생각이 들자 자세한 이야기를 끝까지 듣지도 않은 채 개선자의 제안을 승낙했습니다.

 

3.6. 미술가 논시팟사

 괴짜 미술가입니다. 전장 위에 물감을 뿌려 버프, 디버프 영역을 지정해 아군을 보조합니다.

더보기

- 사용 기술

 

 
 
 

 

- 배경 이야기

 

논시팟사의 가문은 대대로 유명한 화가, 조각가, 행위예술가들을 배출해 온 예술가가문입니다. 논시팟사는 그중 가장 독특한 화가인데, 도화지에 굵은 선을 몇 개 그리고 색칠한 후 그 그림을 그대로 보존하는, 이 채 5분도 걸리지 않는 작업 끝에 나온 결과물을 예술작품이라 주장했기 때문입니다.
수세식 소변기에 샘이란 이름표를 단 것이 예술작품의 반열에 들어가는 오묘 신비한 현대예술을 당신은 아마 한 번쯤 접해봤기에 논시팟사를 '독특하다, 창의적이다'라고 생각해 볼 수도 있겠죠. 하지만 논시팟사가 사는 세계는 그렇게 열린 세계가 아니었습니다. 도화지에 장난질을 해 놓은 것을 가지고 예술이라 억지를 부린다며 논시팟사의 가문에서는 그를 가문의 수치로 여겼고, 평론가들은 '그는 작품을 만드는 사람이 아닌 배설하는 사람이다.'라며 시도 때도 없이 조롱합니다.
소통하지 못하는 예술가 논시팟사는 자신만의 세계에서 산 셈이죠. 그렇게 집에서 은둔생활을 하던 논시팟사의 눈앞에 만만한 보조용병을 찾던 개선자가 나타나 논시팟사의 예술을 사람들이 이해할 수 있는 것으로 만들어주겠다며 용병업을 제안합니다.
그 제안을 어떻게 거절할 수 있었겠습니까? 문밖에는 자신을 이해 못 하고 싫어하는 사람들밖에 없었는데요.

 

3.7. 의적 히르만

 경박함 속에 스스로를 숨긴 협객입니다. 매우 민첩한 기동력을 갖고 있으며, 적의 뒤로 잠입하는 데에 성공하면 막대한 피해를 입힐 수 있습니다.

더보기

- 사용 기술

 


 

 

- 배경 이야기

 

히르만은 개선자의 마력 없이도 강한 전투력을 가지는 몇 안 되는 용병 중 하나입니다. 그는 젊은 나이에도 불구하고 상당히 복잡 다난하고 기구한 인생을 살아 10대 양아치와 같은 건들거리는 외모 안에 환갑노인의 관록과 같은 지혜를 숨기고 있습니다.
그는 빈민가 출신으로 어릴 적부터 특유의 날렵한 몸짓과 잔머리로 동네꼬마 패거리들을 평정하며 골목대장에서 건달두목으로 성장했습니다. 하지만 그는 사람을 때리고 겁먹이고 윽박지르는 일상과 지조 없고 기회주의에 찌든 근시안적인 부하들에게 오래전부터 환멸을 느끼고 있었습니다. 결국 히르만의 이름을 내세우며 패악질을 일삼던 부하들이 빈민가의 꼬마를 죽인 일을 계기로 히르만은 자기 손으로 자신의 패거리들을 모두 불구자로 만든 후 자신이 진정 원하는 삶, 진정으로 따를 가치가 있는 숭고한 가치를 찾으려 애씁니다.
아쉽게도, 그가 사는 세계는 영웅을 낳을 여건조차 되지 않던 소말리아 뺨치는 피폐한 세계였습니다. 히르만이 찾은 뭔가 숭고한 가치를 추구하던 사람들은 그저 사기꾼으로만 드러났습니다. 뭔가 숭고한 가치를 찾고자 하던 히르만의 순수함은 날이 갈수록 수없이 많은 배신과 기만과 망신을 당하며 빛이 바래갔고, 오히려 살벌한 사회에서 살아남기 위해 히르만이 직접 배신과 기만을 일삼게 되는 지경에까지 이릅니다. 그 후로 죄책감과 자기부정, 허무주의가 차례로 그의 머릿속을 차지하자, 그는 오직 안정된 삶만을 꿈꾸게 되었고 개선자가 그를 찾기 전까지 히르만은 호떡장사를 하며 살았습니다.
어느 날 남창을 모집하던 포주가 우연히 이 번들번들한 호떡장사꾼을 눈여겨보게 되고, 무력으로 히르만을 포획할 계획을 세웁니다. 진작 이상한 낌새를 눈치챈 히르만은 야반도주를 하다 포주의 강력한 졸개들에 의해 궁지에 몰리게 됐는데, 이때 개선자가 히르만의 의식 속에 나타나 강력한 힘을 주겠다며 히르만을 설득합니다. 히르만은 개선자를 그다지 믿지 않았지만, 일단 제안을 받아들이면 당장의 곤경에서는 벗어날 수 있을 것이라 생각했기에 밑져도 본전이란 식으로 용병이 됐습니다.

 

3.8. 사파 무속인 샤오 양

 오랜 기간 몸담은 부족의 전통과 신념에 등 돌린 사파 무속인입니다. 적의 시체로부터 영혼을 수집해 동료를 부활시킬 수 있습니다.

더보기

- 사용 기술

 

 


 

 

- 배경 이야기

 

 샤오 양은 한때는 부족의 전통과 신념에 강한 믿음을 가지고 있었습니다. 또 부족의 의녀로서, 또한 무녀로서, 그녀는 오래전부터 고향에 만연해있던 돌림병을 전통적인 방법대로 병자의 영혼정화와 영물의 축복을 담은 문신을 통해 치료하려 했습니다.
그러던 어느 해 바다 건너 다른 미지의 대륙에서 온 푸른빛 피부의 이질적인 이민자들이 샤오 양의 고향에 정착합니다. 그 후로 얼마 지나지 않아 샤오 양은 인생에 다시없을 충격적인 상황을 보게 되는데, 자신의 부족이 수십 년간 극복하지 못했던 지독하고 악랄했던 돌림병을 신대륙을 밟은 지 수개월도 되지 않았던 이민자들이 백신주사를 자체개발해 면역력을 갖춰 정복해 버린 것입니다. 그 후로 샤오 양은 부족이 가진 전통과 사상의 가치를 의심하기 시작하고 이민자들과 잦은 교류를 가지며 점점 새로운 세계에 눈을 뜨게 됩니다.
물론, 제사장의 딸이자 부족대표무녀, 의녀가 스스로 부족의 근간을 의심하고 외지인과 어울리는 모습을 고깝게 본 사람들의 수는 결코 적지 않았습니다. 오빠인 라이 양은 끊임없이 동생에게 자중하라 충고했지만, 동생은 결코 고집을 꺾지 않았고, 하루는 둘이 심하게 다툰 어느 날 이후로 샤오 양은 부족을 떠나 연락도 끊고 라이 양을 피합니다. 그제야 뭔가 잘못되어 있는 것을 느낀 라이 양은 다시 동생을 챙기기 위해 그 뒤를 쫓지만 어느새 샤오 양은 데소나 독도마뱀에게 물려 싸늘한 시체가 되어 있었고, 라이 양은 있을 수 없는 일이라며 절규하며 샤오 양의 시신을 수습했습니다.
하지만 어찌 된 일인지, 시간이 얼마 흐르자 샤오 양은 자리를 툭툭 털며 어떤 음침한 동굴에서 깨어났습니다. 그리고 그 앞에 나타난 개선자가 샤오 양의 내적인 가치갈등과 오빠와의 외적인 갈등을 동시에 해결해 주겠다는 조건을 걸고 샤오 양에게 용병업을 제안합니다. 샤오 양은 개선자가 자신의 목숨을 구해줬다고 생각했으며, 또 그의 분위기가 자신이 친하게 지내던 이민자들의 가볍고 자유로운 느낌과 흡사하다 느꼈습니다. 개선자에게 좋은 인상을 받은 샤오 양은 그 제안을 수락했습니다.

 

3.9. 능욕자 메스가탄

 악마의 군대를 타고난 재치로 농락하면서 싸워온 대담한 마법사입니다. 적들을 함정에 빠트리고 일거에 소탕하는 것에 능합니다.

더보기

 

- 사용 기술

 

 

 

 

- 배경 이야기

 

메스가탄의 고향세계에서는 악마와 인간사이의 전쟁이 치열하게 벌어지고 있었습니다. 미운 정이라는 말이 이때 쓰는 말일까요? 전쟁이 장기전으로 흘러가자 악마의 힘인 마력을 인간들도 쓸 수 있게 됐는데, 메스가탄은 특히 악마의 힘이 체질에 맞았던 모양입니다. 그는 마력을 적극적으로 활용해 유황지옥불을 유출시켜 악마들을 혼내줬고 차원문주술을 익혀 수많은 악마들을 낙사, 익사시키며 음란마귀의 페로몬분비샘을 활용해 대악마 함정을 만드는 등 악마들을 조롱능욕하는 데에 일가견을 보입니다. 이런 인재를 놓칠 수 없었던 개선자는 어느 날 메스가탄의 눈앞에 나타나 그의 도움을 요청합니다. 메스가탄은 당연히 고향방위를 이유로 거절하려 했지만, 개선자가 더 강력한 힘을 미끼로 꾀어내 결국 용병이 되었습니다.

 

3.10. 빙검술가 옐라르

 무뚝뚝한 인외의 존재입니다. 적을 얼려 무력화시키거나 약화시키는 것에 특화되어 있습니다.

더보기

 

- 사용 기술

 

 


 
 
 

 

- 배경 이야기

 

 옐라르라는 이름은 개선자가 직접 옐라르에게 지어준 이름인데, 그 이름에 별 뜻은 없습니다. 그전까지 옐라르에게는 이름이 없었기에 뭐라고 부를 이름이 필요했고, 개선자가 ㅖ와 ㄹ의 발음이 부드럽다고 생각해 옐라르라는 혀에 착착 감기는 이름을 만들어준 것뿐입니다. 실제로 그의 이름을 발음하면 혀가 입천장을 가볍게 간지럽히는 느낌과 함께 옥구슬이 은쟁반 위를 구르는 소리가 납니다.
옐라르는 다른 용병들이 합류하기 이전부터 비서처럼 개선자의 일을 도와줬습니다. 그 비서가 이번엔 용병일을 하는 거죠.
그의 체질은 보통 사람들과 굉장히 다른데, 그는 체온부터가 영하인 괴물이며 대부분의 물질대사를 자신만의 고유한 방식으로 처리합니다. 이런 걸 봐서는 애초에 그는 고향의 다른 주민들과 같은 종에 속하지도 않을 가능성도 있습니다.
개선자에게 고용되기 전, 고향에서 옐라르는 냉기를 끊임없이 강하게 내뿜는 탓에 푸른 도깨비라는 가명으로 불리며 주민들이 모두 두려워했습니다. 본명이 없을법한 이유죠. 아무도 가까이 갈 생각을 못했으니까요. 그런 옐라르에게 처음으로 말을 건 이가 개선자인데, 개선자는 옐라르의 성품을 살펴본 후 옐라르에게 자신의 체질을 조절할 수 있는 힘을 주겠다는 조건으로 그에게 비서직을 제안합니다. 사실 그 당시 옐라르는 그 제안의 내용에 크게 귀를 기울이지는 않았고, 그저 처음으로 누가 자신에게 말을 건 기념으로 제안을 받아들입니다. 도중에 일을 때려치우지 않고 계속하는 걸 보면 하는 일에 큰 불만은 없는 것 같습니다.
그의 피부가 지금은 붉은 이유는 자신의 냉기가 동료들을 해치지는 않을까 걱정한 옐라르가 자신의 냉기를 적당히 완화시키기 위한 노력의 일환으로 파란 파장의 강한 빛 에너지는 주로 흡수하려 하고 있기 때문입니다.

 

4. 던전

 던전은 튜토리얼 던전 하나와 수평적으로 배치되어 유저가 원하는 순서대로 진행할 수 있는 던전 셋, 최종 던전 하나로 총 5 종류의 던전을 만들었다.

던전을 하나 클리어할때마다 나오는 던전 선택화면, 3 종류의 던전을 호스트 유저가 정하는 순서대로 도전한다. 세 던전을 모두 클리어하고 나면 마지막 던전이 해금된다.

 

4.1. 마약왕 낙스

 

 울창한 숲 속에서 숨어 마약을 제조하는 카르텔을 소탕하는 스테이지다. 현지인들이 제공한 벌목차량을 이용해 나무를 무너뜨리고 길을 뚫어 마약왕의 은신처를 습격해야 한다. 

 

벌목 차량으로 숲을 뚫으며 비밀기지를 찾는 구간

 

 

 

카르텔의 비밀기지 잠입

 

 

 

스테이지 보스 마약왕 낙스

 

 

 

4.2. 독재자 김택은

 

 

 나라를 무자비하게 다스리는 잔혹무도한 독재자 김택은을 징벌하기 위해 정치범 수용소를 해방시키고 수도를 습격하는 스테이지다.

 

정치범 수용소 해방

 

 

수도 시가전

 

스테이지 보스 김택은

 

모티브는 북한이다.

 

4.3. 나찰녀

 

 기후변화를 일으켜 극지방의 원주민들을 핍박하는 나찰녀를 무찌르고 파초선을 빼앗아 환경을 안정화시키는 것이 목표다. 나찰녀의 폭정으로 성정이 포악해진 원주민들도 상대하면서 나아가야 한다.

 

 

포악한 성정을 가진 원주민과의 교전

 

나찰녀와 손잡고 이상기후를 일으켜 원주민을 핍박하는 불의세력과의 전투

 

 

 

스테이지 보스 나찰녀

 

 보스의 모티브는 서유기에서 나오는 우마왕의 아내, 나찰녀에서 따왔다. 이 여자도 화산의 불길을 통제하며 화염산의 불길을 선선하게 해주는 대가로 지역 주민들에게 삥을 뜯는 악한이다.

 

5. 잘된 점

- 개성 있는 플레이어 캐릭터의 스킬셋

 전통적인 탱/딜/힐의 역할배분을 벗어나 적군보다 더 많은 물량의 소환수로 던전을 압도하는 캐릭터, 적의 뒤통수를 노리며 잠입과 교란을 일삼는 캐릭터 등 고유한 플레이 스타일을 만들어내는 것을 시도했다. 이런 특색 있는 캐릭터 구성을 매력적이라 생각하는 유저들이 있었다.

 

- 근본 있는 게임 구조

 "팀원들과 힘을 합쳐 적들이 득실대는 던전을 주파하고, 보스를 잡는다."는 왕도적인 전개 방식에 충실하게 게임을 만들었다. 2014년 당시에 이런 접근을 취한 맵들은 많지 않았기에 이 맵을 오래도록 플레이하는 고정 팬층이 있었던 것 같다.

 

- 디테일

 게임 인트로 컷신, 크레딧 영상, 캐릭터별 조작 안내 튜토리얼 등 게임의 완성도를 높이기 위해 디테일한 요소들을 신경 써서 만들었다.

캐릭터 중 조작법이 난해한 캐릭터 '의적 히르만'의 조작법을 안내하는 튜토리얼 스테이지

 

6. 잘못된 점

- 감당이 힘들 정도로 중구난방이었던 스킬셋 구성

 메커니즘이 독특한 캐릭터들이 너무 많았기 때문에, 캐릭터의 능력치와 스킬 수치를 조정하는 것에 사실상 실패했다.


- 과한 플레이 인원

 예상 플레이 인원은 10명이었는데, 막상 게임은 중요 캐릭터 한 명만 나가도 진행이 위태로워지는 구성이었기 때문에 게임을 재미있게 즐길 수 있는 환경이 안정적으로 유지되기 힘들었다.

 

- 탈진의 흔적

 게임을 기획하다가 탈진한 나머지 성의 없는 아이디어로 빈칸을 메운 부분이 종종 보인다. 괜찮은 기획을 떠올리기 힘들었다면 잠깐 쉬었다가 다시 돌아와도 좋았을 텐데, 빨리 작업을 해치우고 다음으로 넘어가야 한다는 집착에 경솔한 판단을 내려버린 경우가 많다. 플레이어블 캐릭터 '가롯 보울'의 성의 없는 스킬셋, 사파 무속인 샤오 양의 뜬금없는 단일대상 공격 기술 Q가 대표적인 사례다. 다른 유저들과 같이 게임을 할 때 '가롯 보울'을 선택한 유저가 지루함을 느끼고 바로 나가버리거나, 사파 무속인 '샤오 양'을 선택한 유저가 "스킬셋에 왜 이렇게 일관성이 없냐?"는 실망감을 드러냈을 때 내 무성의했던 마음이 들통난 것 같아 창피했고, 미안했다.

 

7. 교훈

 '항상 자기 자신에게 솔직해야 한다.'는 것이 이 프로젝트를 통해 얻은 가장 큰 교훈이다.

 

 이 게임을 만들 때 내가 진심으로 성의를 다해 개발한 부분도 많지만, 대충 구색만 맞추어 만들고 치운 부분도 많았다. 캐릭터 '가롯 보울'을 기획할 때가 딱 그랬다.

 노망 난 퇴역군인이라는 배경설정을 가진 플레이어블 캐릭터, '가롯 보울'은 영화 포레스트 검프의 등장인물인 댄 중위로부터 모티브를 얻은 캐릭터다. 상실감에 시달리며 현실을 놓치고 사는 사람이 구원을 얻기 위해 노력한다는 배경설정을 넣고 싶었고, 그에 부합하는 캐릭터인 댄 중위의 서사를 가롯 보울이라는 캐릭터에 넣었다.

'포레스트 검프'의 등장인물 댄 중위, 어수룩한 주인공에게 따뜻하고 헌신적인 장교였으나, 베트남전에서 다리와 부하, 삶의 목표를 잃고 폐인이 된다.

 

 이어, 퇴역군인이라는 컨셉을 캐릭터의 스킬 구성에 녹이기 위해 상실감을 고함으로 내뱉어 공격하는 범위공격 스킬, 부하들의 환영을 소환하는 소환 스킬로 스킬셋을 구상했다. 하지만 세 번째 스킬을 구상할 때 즈음에는 완전히 탈진이 와버린 나머지, '워크래프트 3의 영웅 스킬 중 다른 스킬을 강화하는 패시브 스킬이 있는데, 대충 그거 넣고 끝내면 안 되나?'라는 마음으로 무성의한 패시브 스킬을 넣어 구색만 맞췄다.

 

성난 노인의 호통같은 Q 스킬, 왕년의 용사들을 소환하는 W스킬, 늙은 퇴역군인이라는 컨셉에는 맞지만, 사실상 이 캐릭터는 이 두 스킬이 전부다.

 

 나는 스스로 '캐릭터 컨셉을 잘 잡았고, 스킬도 컨셉에 맞게 2개 잘 뽑았으니 나는 이 캐릭터에게 충분한 성의를 보였다.'라고 되뇌며 피로와 싫증에 뿌리를 둔 내 동기를 외면했다. 피곤하면 그냥 쉬었다가 작업하면 될 것을, 서둘러 일을 끝내려는 집착에 사로잡힌 나머지 이 퇴역군인 캐릭터를 매우 단순하고 재미없게 만들어버렸다. 이 캐릭터를 기획했을 때 나는 유저가 상실감에 시달리는 소외된 노인의 입장에 서서 게임에 몰입해보길 바랐지만, 실제 유저의 플레이 경험은 같은 말, 같은 행동만 반복하는 노인의 뒤치닥꺼리를 해주는 느낌을 받았을 거다.

 만약 내가 게임을 만드는 순간순간마다 항상 스스로의 마음에 솔직할 수 있었다면, 한 순간 올라온 마음속 권태로움이 작업물에까지 영향을 미치는 일은 없었을 것이다. 유저들은 귀신이다. 내가 소홀하게 개발한 콘텐츠를 경험한 유저들은 바로 '이 캐릭터를 재미있으라고 만든 거냐.', '제작자가 스킬 이름 짓다가 지쳤나 보다.', '진짜 대충 만들었네'와 같은 신랄한 말을 하며 콘텐츠 너머에 있는 내 과거의 모습을 꿰뚫어 보더라. 이 게임에 실망한 유저들이 남긴 이 같은 피드백들은 지금까지도 스스로의 마음상태와 의도에 항상 깨어있는 것이 창작자에게 얼마나 중요한지 경각심을 남기고 있다.

 

 

8. 게임 크레딧 영상

 

Warcraft 3 Custom Models

Download custom models for Warcraft 3.

www.hiveworkshop.com

 이 맵을 제작 하는 데에 쓰인 3d 모델과 텍스처들 중 많은 내용들은 워크래프트 3 맵 제작 커뮤니티 하이브 워크샵(https://www.hiveworkshop.com/repositories/models.530/)에서 다운로드한 것이다.

 사용된 커스텀 아트 리소스와 해당 창작물을 만들어 주신 아티스트 분들의 닉네임을 함께 배치해 구성한 게임 크레딧 영상이다.

 

깃허브 주소 : https://github.com/GAFinal-Predator/Predator

게임 시연 영상 : https://youtu.be/xjCrXEDaerc?si=laKyc-V1Po2zvh5H

 

 목차

 

1. 프로젝트 개요

2. 프로토타입 게임 개발

3. 빌드머신 구축, 빌드와 테스트의 자동화

4. Yunuty 게임엔진 개선

4.1. 길찾기 라이브러리 Recast Navigation 연동

4.2. 물리 라이브러리 PhysX 연동

4.3. 컴포넌트 코루틴 구현

5. Wander 에디터

6. RTS 프레임워크 개발

7. UI

8. 성과

8.1. 게임 플레이 영상

8.2. 개발 과정 영상

8.3. 도쿄 게임쇼

9. 느낀 점


 

1. 프로젝트 개요

 게임 인재원 파이널 졸업 프로젝트는 1년의 시간을 두고 진행되는 장기 게임 개발 프로젝트였다. 모든 팀원이 서로 열심히 노력해서 개발한 결과 게임 인재원 우수 프로젝트로 지정되어 도쿄 게임쇼에 출품하는 영광을 얻을 수 있었다.

 기획된 게임은 여러 캐릭터들을 동시에 조종하는 실시간 쿼터뷰 액션 게임이었다. 게임 플레이 엔진은 내가 여러 프로젝트를 거치며 만들어 온 Yunuty 엔진을 사용하기로 했고, 그래픽스 엔진은 DirectX11 기반으로 팀원이 만들기로 했다.

팀 구성  
프로그래밍 팀장 ( 게임 엔진 )
프로그래밍 팀원 ( 맵 툴 ) A
프로그래밍 팀원 ( 그래픽스 엔진 ) B
프로그래밍 팀원 ( 콘텐츠 프로그래밍 ) C
아트 팀 3인
기획 팀 2인

 

프로젝트 발표에 쓰인 기획 프레젠테이션

 

2. 프로토타입 게임 개발

( 게임 플레이 기획의 영감을 얻었다는 두 게임, '트랜지스터'와 '드래곤 에이지 인퀴지션'. )

 

 프로젝트를 시작하기 이전에 기획자로부터 어떤 게임을 만들 생각인지 대강의 설명을 들었다. 쿼터뷰 시점의 다중 유닛 컨트롤 게임이자, 실시간 전투와 시간정지 플레이가 혼재된 액션 게임이라고 설명을 들었다. 그렇지만 내가 이해한 바가 기획자의 생각과 일치하는지가 여전히 모호했다. 앞으로 장기간 협업하게 될 테니 서로 생각하고 있는 게임의 형태를 처음부터 확실하게 공유하고 싶었다. 마침 게임의 형태가 RTS 게임과 비슷했고, 나는 RTS 게임 워크래프트 3의 맵 에디터 기능을 능숙하게 다룰 수 있었기 때문에 짧은 시간 안에 프로토타입 게임을 먼저 만들었다.

( 재미 검증을 위한 프로토타입 게임, 스킬과 전술모드, 적군 웨이브가 구현되었다. )
( 밸런싱 작업을 위한 프로토타입 게임의 수치 테이블 )

 

 프로토타입 게임의 목적은 단지 팀이 같은 비전을 공유하는 것만이 아니었다. 우리 게임만을 위한 맵 에디터와 그래픽스 엔진, RTS 코드를 완전히 처음부터 만드는 만큼, 실질적으로 구동되는 게임을 기획자가 받아보는 시점은 매우 늦어질 예정이었다. 그렇다고 기획자의 레벨 디자인과 수치 밸런싱 작업을 그때까지 마냥 미룰 수는 없었다. 이런 작업들은 병렬적으로 진행될 필요가 있었다. 기획자는 이 프로토타입을 통해 그러한 수치들을 적용하고 확인할 수 있었다. 자체 맵 에디터를 워크래프트3 에디터와 유사하게 만들 계획이었기 때문에, 기획자가 에디터의 활용에 미리 익숙해지는 효과도 기대할 수 있었다.

( 프로토타입의 지형 배치, 이 배치가 그대로 게임 레벨디자인에 적용되었다. )

 

 

 

 

 

 

3. 빌드머신 구축, 빌드와 테스트의 자동화

 프로토타입을 전달해 기획자의 작업이 병렬적으로 수행될 수 있게 되었으니, 이제 우리는 본격적인 프로그램 개발에만 집중하면 됐다. 프로젝트가 긴 시간 동안 진행될 예정이었기 때문에, 코드 작성에 앞서 개발 프로세스에 대해 신중히 고민하는 시간을 가졌다. 장기 프로젝트에서는 잘못된 커밋이 쌓여 버그가 발생할 경우, 문제의 원인을 찾는 것이 마치 볏짚 속에서 바늘을 찾는 것처럼 어려워질 수 있었다. 이를 방지하기 위해 테스트 주도 개발 방식을 도입했다. 우리는 주요한 기능을 구현할때마다 그 기능의 정상작동을 검증하는 단위 테스트 코드도 짝을 지어 만들기로 했다. 그리고 빌드머신을 세팅해 커밋이 들어올 때마다 테스트들을 진행시켜  모두 통과해야 빌드를 성공으로 표시하게 만들었다. 테스트 자동화 시스템 덕분에 잘못된 커밋이 들어오면 즉각적으로 해당 커밋이 어떤 문제를 일으키는지 특정할 수 있었다.

 

링크 : 게임개발 프로세스에 테스트 자동화 환경을 구축한 이야기

 

 

 

 

 

 

4. Yunuty 게임엔진의 개선

 RTS 게임을 만들기 위해 게임 엔진에서 지원해야 할 기능들이 있었다. 하나는 길찾기 기능, 하나는 3차원 충돌 기능이었다. 자체 개발한 길찾기 기능과 2D 충돌 기능이 준비되어 있었지만, 최적화와 예외처리에서 미심쩍은 부분들이 많았다. 때문에 안정성이 확보된 오픈소스 라이브러리인 RecastNavigation과 PhysX를 게임엔진과 연동시켜 사용했다.

 

 

4.1. 길찾기 라이브러리 Recast Navigation 연동

좌 : Recast Navigation Demo 프로그램의 모습, 우 : Recast Navigation이 게임엔진에 연동되어 동작하는 모습

 

 라이브러리의 데모 프로젝트의 코드를 참고해 게임엔진에서 NavigationField와 NavigationAgent라는 각각의 클래스로 래핑했다. 게임 엔진 사용자가 최대한 간단하게 기능을 사용할 수 있도록 인터페이스를 뚫어주고 싶었다. NavigationField는 버텍스 좌표들의 리스트만 받으면 이동가능한 지형을 생성할 수 있게 만들었고, NavigationAgent는 이동속도 설정, 충돌 크기 설정, 이동 등의 기능만 사용할 수 있게 만들었다.

 

링크 : 자체 게임 엔진에 RecastNavigation 이식하기

길찾기 기능을 래핑하기 위한 인터페이스 설계

 

4.2. 물리 라이브러리 PhysX 연동

 PhysX 라이브러리의 기능을 연동하고 래핑했다. 물체의 충돌범위 판정 속성과 강체로서의 속성은 따로 구분해 Collider와 Rigidbody의 두 클래스로 만들었다.

Collider와 Rigidbody의 설계

PhysX 라이브러리를 Yunuty 엔진에서 래핑한 코드

더보기

- Collider 클래스 헤더

#pragma once
#include <Windows.h>
#include <vector>
#include <unordered_set>
#include "Graphic.h"
#include "Vector2.h"
#include "Vector3.h"
#include "Rect.h"

#ifdef YUNUTY_EXPORTS
#define YUNUTY_API __declspec(dllexport)
#else
#define YUNUTY_API __declspec(dllimport)
#endif

using namespace std;
namespace yunutyEngine
{
    namespace physics
    {
        class RigidBody;
        class YUNUTY_API Collider : virtual public Component
        {
        public:
            class Impl;
            Collider(Impl* impl);
            virtual ~Collider();
            //bool IsTrigger();
            //void SetAsTrigger(bool isTrigger);
            bool IsUsingCCD();
            // Continuous Collision Detection 기능을 활성화하면 한 프레임에서 다음 프레임까지의 충돌을 연속적으로 체크합니다.
            // CCD 기능이 활성화되면 Bullet through paper 현상이 발생하지 않습니다.
            void EnableCCD(bool enable);
            // 콜라이더의 크기가 0이면 터짐, 콜라이더 크기가 매우 작을때 적용되는 최소한의 스케일
        protected:
            virtual void Start()override;
            virtual void Update()override;
            virtual void OnTransformUpdate() override;
            // 피직스 객체의 월드스케일이 달라졌을 때 이를 어떻게 반영할지 결정합니다.
            virtual void ApplyScale(const Vector3d& worldScale) = 0;
            virtual void OnEnable()override;
            virtual void OnDisable()override;
            RigidBody* rigidBody;

            Impl* impl;
        private:
            bool WasPxActorInitialized();
            Vector3d cachedScale;
            friend RigidBody;
#ifdef _DEBUG
            Vector3d firstLocation;
            Quaternion firstRotation;
#endif
        };
    }
}

 

-Rigidbody 클래스 헤더

#pragma once
#include <Windows.h>
#include <vector>
#include <unordered_set>
#include "Graphic.h"
#include "Vector2.h"
#include "Vector3.h"
#include "Rect.h"
#include "YunutyForceType.h"
#include "YunutyPhysicsMaterialInfo.h"

#ifdef YUNUTY_EXPORTS
#define YUNUTY_API __declspec(dllexport)
#else
#define YUNUTY_API __declspec(dllimport)
#endif

// RigidBody는 콜라이더에 강체 속성을 부여하고 싶을때 사용됩니다. 콜라이더 없이는 RigidBody의 코드가 제대로 동작할 수 없습니다.
using namespace std;
namespace yunutyEngine
{
    namespace physics
    {
        class Collider;
        class YUNUTY_API RigidBody : public Component
        {
        public:
            RigidBody();
            virtual ~RigidBody();
            bool GetIsStatic();
            void SetAsStatic(bool isStatic);
            void SetAsKinematic(bool isKinematic);
            bool GetIsKinematic();

            //void SetMaterialInfo();

            void LockTranslation(bool x, bool y, bool z);
            void LockRotation(bool x, bool y, bool z);
            void SetMass(float mass);
            void AddForce(const Vector3f& forceVector, ForceType forceType);
            void AddTorque(const Vector3f& forceVector, ForceType torqueType);
        protected:
            virtual void Start() override;
            virtual void OnEnable()override;
            virtual void OnDisable()override;
        private:
            std::vector<std::function<void()>> onStartCallbacks;
            Collider* attachedCollider;
            bool isStatic{ false };
            friend Collider;
        };
    }
}

 

이식된 물리 기능, 애석하게도 게임에서 물리 시뮬레이션 기능이 쓰이는 일은 없었다.

4.3. 컴포넌트 코루틴 구현

 유니티에는 StartCoroutine과 같은 함수를 통해 컴포넌트에서 코루틴 객체를 붙여 비동기적 동작을 실행하는 기능이 있다. 내 엔진에도 같은 기능이 있으면 좋을것 같아 유니티와 유사하게 만들어 보았다. 유니티는 코루틴 함수에서 YieldInstruction 객체를 반환할 수 있게 만들어 코루틴이 재활성화되는 시점을 손쉽게 조절할 수 있도록 만들었다. 나도 유사한 클래스를 만들어 '이 코루틴은 yield 후 n초 후 다시 동작'과 같은 편의성을 제공했다.

 

관련 코드

더보기

- YunutyCoroutine.h

#pragma once

#ifdef YUNUTY_EXPORTS
#define YUNUTY_API __declspec(dllexport)
#else
#define YUNUTY_API __declspec(dllimport)
#endif

#include <vector>
#include <functional>

namespace yunutyEngine
{
    class Component;
    class YunutyCycle;
    namespace coroutine
    {
        class YieldInstruction;
        struct YUNUTY_API Coroutine
        {
            struct Coroutine_Handler;
            struct promise_type {
                YieldInstruction* yield = nullptr;
                std::vector<std::function<void()>> destroyCallBack;
                Coroutine get_return_object()
                {
                    return Coroutine(std::make_shared<Coroutine_Handler>(std::coroutine_handle<promise_type>::from_promise(*this)));
                }
                std::suspend_always initial_suspend() { return std::suspend_always{}; }
                std::suspend_always final_suspend() noexcept { return std::suspend_always{}; }
                void return_void() {}
                /*void return_value(YieldInstruction& yield) { this->yield = &yield; }
                void return_value(YieldInstruction&& yield) { this->yield = &yield; }*/
                std::suspend_always yield_value(std::suspend_always) { return {}; }
                std::suspend_always yield_value(YieldInstruction& yield) { this->yield = &yield; return {}; }
                std::suspend_always yield_value(YieldInstruction&& yield) { this->yield = &yield; return {}; }
                void unhandled_exception() 
                {
                    std::rethrow_exception(std::current_exception());
                }
            };
            explicit Coroutine(const std::shared_ptr<Coroutine_Handler>& h) : handle(h) {}
            ~Coroutine()
            {
                if (handle)
                {
                    for (auto each : handle->promise().destroyCallBack)
                    {
                        each();
                    }
                }
            }
            Coroutine(Coroutine const&) = delete;
            Coroutine(Coroutine&& other) noexcept
            {
                deathWish = other.deathWish;
                handle = other.handle;
                other.handle.reset();
            };
            void PushDestroyCallBack(const std::function<void()>& callBack)
            {
                handle->promise().destroyCallBack.push_back(callBack);
            }
            Coroutine& operator=(Coroutine const&) = delete;
            void resume() { handle->resume(); }
            YieldInstruction* GetLastYield() { return handle->promise().yield; };
            bool Done() { return handle->done(); }

            class Coroutine_Handler
            {
                friend struct Coroutine;
            public:
                Coroutine_Handler(const std::coroutine_handle<promise_type>& handle)
                    : primitive_handle(handle)
                {

                }

                ~Coroutine_Handler()
                {
                    if (primitive_handle)
                    {
                        primitive_handle.destroy();
                    }
                }

                auto& promise()
                {
                    return primitive_handle.promise();
                }
                
                void resume()
                {
                    primitive_handle.resume();
                }

                bool done()
                {
                    return primitive_handle.done();
                }

            private:
                std::coroutine_handle<promise_type> primitive_handle;
            };

            std::shared_ptr<Coroutine_Handler> handle;
        private:
            bool deathWish = false;
            friend yunutyEngine::Component;
            friend yunutyEngine::YunutyCycle;
        };
    }
}

( 코루틴 클래스의 정의 )

 

- YunutyYieldInstruction.h

#pragma once

namespace yunutyEngine
{
    namespace coroutine
    {
        class YieldInstruction
        {
        public:
            virtual bool ShouldResume() const = 0;
            virtual void Update() = 0;
        };
    }
}

( 코루틴 함수에서 반환할수 있는 객체 YieldInstruction의 정의 )

- YunutyWaitForSeconds.h

#pragma once
#include "YunutyYieldInstruction.h"

#ifdef YUNUTY_EXPORTS
#define YUNUTY_API __declspec(dllexport)
#else
#define YUNUTY_API __declspec(dllimport)
#endif

namespace yunutyEngine
{
    namespace coroutine
    {
        class YUNUTY_API WaitForSeconds :public YieldInstruction
        {
        private:
            float elapsed{ 0 };
            float seconds{ 0 };
            bool isRealTime{ false };
        public:
            WaitForSeconds(float seconds, bool isRealTime = false);
            virtual void Update() override;
            virtual bool ShouldResume() const override;
            float GetExceededTime() const;
        };
    }
}

( 코루틴이 n초동안 쉬었다가 다시 동작을 재개하게 만드는 YieldInstruction의 한 형태인 WaitForSeconds )

 

- YunutyCycle.cpp

...
...
for (unsigned int i = 0; i < updateTargetComponentsSize; i++)
{
    if (!updateTargetComponents[i]->GetActive() || !updateTargetComponents[i]->GetGameObject()->GetActive())
        continue;
    UpdateComponent(updateTargetComponents[i]);
    for (auto coroutine : updateTargetComponents[i]->coroutines)
    {
        if (coroutine->Done() || coroutine->deathWish)
        {
            continue;
        }
        if (auto yield = coroutine->GetLastYield(); yield)
        {
            yield->Update();
            if (yield->ShouldResume())
            {
                coroutine->handle->promise().yield = nullptr;
                coroutine->resume();
            }
        }
        else
        {
            coroutine->resume();
        }
    }
    if (!updateTargetComponents[i]->coroutines.empty())
    {
        std::erase_if(updateTargetComponents[i]->coroutines, [](std::shared_ptr<yunutyEngine::coroutine::Coroutine> coroutine) {return coroutine->Done() || coroutine->deathWish; });

        if (!updateTargetComponents[i]->coroutines.empty())
        {
            updateTargetComponents[i]->GetGameObject()->HandleComponentUpdateState(updateTargetComponents[i]);
        }
    }
}
...
...

( 컴포넌트 객체를 Update할 때 Coroutine도 같이 업데이트하는 부분 )

 

 

 

 

 

 

5. RTS 프레임워크 개발

InWanderLand Contents.drawio
0.01MB
콘텐츠 코드의 전체 설계

 

 

 

 유닛, 유닛 컨트롤러, 투사체, 스킬, 상태이상과 같은 RTS 프레임워크 코드를 설계하고 구현했다. 이 중 가장 핵심이 되는 클래스는 유닛이었다. 이동, 공격, 스킬 시전과 같은 유닛의 상태는 행동 트리로 관리했다.

InWanderLand Unit Behaviour Tree.drawio
0.02MB
유닛의 상태를 관리하기 위한 행동 트리의 설계도

 

행동 트리와 관련된 코드

더보기

BehaviourTree.h

#include <vector>
#include <functional>
#include <unordered_set>
#include <unordered_map>

class BehaviourTree
{
public:
    struct Node
    {
    private:
        int nodeKey;
        std::unordered_map<int, Node> children;
        std::vector<Node*> childrenInOrder;
    public:
        // 재심청구 플래그, 이 플래그가 true이면 노드의 onExit 주기를 실행한 후 다시 조건을 확인한다.
        int GetNodeKey() const { return nodeKey; };
        std::function<void()> onEnter = []() {};
        std::function<void()> onExit = []() {};
        std::function<void()> onUpdate = []() {};
        std::function<bool()> enteringCondtion = []() {return false; };
        Node& operator[](int key);
        friend BehaviourTree;
    };
    const std::vector<const Node*>& GetActiveNodes()const { return activeOnCurrentUpdate; };
    void Update(int reassessCount = 0);
    static constexpr int maxReassessCount = 3;
    Node& operator[](int key);
    bool reAssessFlag = false;
private:
    std::vector<const Node*> activeOnLastUpdate;
    std::vector<const Node*> activeOnCurrentUpdate;
    std::unordered_map<int, Node> rootChildren;
    std::vector<Node*> rootChildrenInOrder;
};
BehaviourTree::Node& BehaviourTree::Node::operator[](int key)
{
    if (!children.contains(key))
    {
        children[key] = Node{};
        children[key].nodeKey = key;
        childrenInOrder.push_back(&children[key]);
    }
    return children[key];
}
void BehaviourTree::Update(int reassessCount)
{
    if (reassessCount > maxReassessCount)
        return;
    reAssessFlag = false;
    activeOnCurrentUpdate.clear();
    const auto* nodeChildren = &rootChildrenInOrder;
    while (nodeChildren && !nodeChildren->empty())
    {
        auto nodeChildrenBefore = nodeChildren;
        for (const auto& node : *nodeChildren)
        {
            if (node->enteringCondtion())
            {
                activeOnCurrentUpdate.push_back(node);
                nodeChildren = &node->childrenInOrder;
                break;
            }
        }
        assert(nodeChildrenBefore != nodeChildren, "behaviour node can't reach the leaf node.");
    }
    // 먼저 Exit를 순차적으로 부르고 다음 Enter들을 부른다.
    for (int i = activeOnLastUpdate.size() - 1; i >= 0 && reassessCount == 0; i--)
    {
        if (activeOnCurrentUpdate.size() - 1 < i || activeOnLastUpdate[i] != activeOnCurrentUpdate[i])
        {
            activeOnLastUpdate[i]->onExit();
        }
    }
    // Exit를 부른 상태에서 재심청구가 들어왔다면, 다시 Update문을 실행한다.
    if (reAssessFlag)
    {
        Update(reassessCount + 1);
        return;
    }
    for (int i = 0; i < activeOnCurrentUpdate.size(); i++)
    {
        if (((int)activeOnLastUpdate.size() - 1 < i) || (activeOnLastUpdate[i] != activeOnCurrentUpdate[i]))
        {
            activeOnCurrentUpdate[i]->onEnter();
        }
        activeOnCurrentUpdate[i]->onUpdate();
    }
    activeOnLastUpdate = activeOnCurrentUpdate;
}
BehaviourTree::Node& BehaviourTree::operator[](int key)
{
    if (!rootChildren.contains(key))
    {
        rootChildren[key] = Node{};
        rootChildren[key].nodeKey = key;
        rootChildrenInOrder.push_back(&rootChildren[key]);
    }
    return rootChildren[key];
}

 

 

Unit.cpp

...
...
unitBehaviourTree[UnitBehaviourTree::Hold][UnitBehaviourTree::Stop].enteringCondtion = [this]()
    {
        return true;
    };
unitBehaviourTree[UnitBehaviourTree::Hold][UnitBehaviourTree::Stop].onEnter = [this]()
    {
        OnStateEngage<UnitBehaviourTree::Stop>();
    };
unitBehaviourTree[UnitBehaviourTree::Hold][UnitBehaviourTree::Stop].onExit = [this]()
    {
        OnStateExit<UnitBehaviourTree::Stop>();
    };
unitBehaviourTree[UnitBehaviourTree::Move].enteringCondtion = [this]()
    {
        return CanProcessOrder<UnitOrderType::Move>();
    };
unitBehaviourTree[UnitBehaviourTree::Move].onEnter = [this]()
    {
        currentOrderType = pendingOrderType;
        pendingOrderType = UnitOrderType::None;
        OnStateEngage<UnitBehaviourTree::Move>();
    };
unitBehaviourTree[UnitBehaviourTree::Move].onExit = [this]()
    {
        OnStateExit<UnitBehaviourTree::Move>();
        if (pendingOrderType == UnitOrderType::None)
        {
            OrderHold();
            unitBehaviourTree.reAssessFlag = true;
        }
    };
unitBehaviourTree[UnitBehaviourTree::Move].onUpdate = [this]()
    {
        OnStateUpdate<UnitBehaviourTree::Move>();
    };
 ...
 ...

( 유닛의 초기화 코드 중 유닛이 가진 행동 트리의 각 노드의 진입조건, 진입시 실행할 함수, 해당 노드가 활성화되어있는 동안 실행할 업데이트 동작 등 노드의 속성을 지정하는 코드 )

( 빨간색 구는 유닛의 공격 사거리 안에 속한 적을 탐지하기 위한 충돌체, 회색 구는 포착 거리 내의 적을 탐지하기 위한 충돌체다. )

 

 유닛의 적 추적 범위, 사거리와 같이 일정 범위 안의 유닛을 탐색해야 하는 기능은 구형의 충돌체를 유닛의 위치에 붙이는 방식으로 구현했다.

 

 유닛을 탐색하는 데에 쓰이는 UnitAcquisitionCollider 클래스의 코드

더보기

UnitAcquistionCollider.h

#pragma once
#include "YunutyEngine.h"
#include "UnitAcquisitionCollider.h"

class Unit;
class UnitAcquisitionCollider : virtual public Component
{
public:
    // nullable
    std::weak_ptr<Unit> owner;
    int teamIndex{ 0 };
    // 콜라이더에 닿은 모든 유닛과 owner 기준 적, 아군 유닛들의 목록
    const std::unordered_set<Unit*>& GetUnits() { return units; }
    const std::unordered_set<Unit*>& GetEnemies() { return enemies; }
    const std::unordered_set<Unit*>& GetFriends() { return friends; }
    bool includeDeadUnits = false;
    bool includeInvulnerableUnits = false;
protected:
    virtual void Update() override;
    int GetTeamIndex();

    // 유닛이면 일단 집어넣는 리스트
    std::unordered_set<Unit*> unitsWhatSoEver;

    std::unordered_set<Unit*> units;
    std::unordered_set<Unit*> enemies;
    std::unordered_set<Unit*> friends;

    GameObject* debugMesh = nullptr;
private:
    bool ShouldContain(Unit* unit);
    virtual void OnTriggerEnter(physics::Collider* other) override;
    virtual void OnTriggerExit(physics::Collider* other) override;
};

 

UnitAcquistionCollider.cpp

#include "UnitAcquisitionCollider.h"
#include "InWanderLand.h"

bool UnitAcquisitionCollider::ShouldContain(Unit* unit)
{
    return (includeDeadUnits || unit->IsAlive()) && (includeInvulnerableUnits || !unit->IsInvulenerable());
}

void UnitAcquisitionCollider::OnTriggerEnter(physics::Collider* other)
{
    if (auto collider = other->GetGameObject()->GetComponent<UnitCollider>(); collider)
    {
        auto unit = collider->owner.lock().get();
        unitsWhatSoEver.insert(unit);
        bool includeDeadUnits = false;
        bool includeInvulnerableUnits = false;
        if (ShouldContain(unit))
        {
            units.insert(unit);
            int teamIndex = GetTeamIndex();
            int otherTeamIndex = unit->GetTeamIndex();
            if (teamIndex != otherTeamIndex)
            {
                enemies.insert(unit);
            }
            else
            {
                friends.insert(unit);
            }
        }
    }
}

void UnitAcquisitionCollider::OnTriggerExit(physics::Collider* other)
{
    if (!other) return;
    if (auto collider = other->GetGameObject()->GetComponent<UnitCollider>(); collider)
    {
        auto unit = collider->owner.lock().get();
        unitsWhatSoEver.erase(unit);
        units.erase(unit);
        enemies.erase(unit);
        friends.erase(unit);
    }
}

void UnitAcquisitionCollider::Update()
{
    units.clear();
    friends.clear();
    enemies.clear();
    std::copy_if(unitsWhatSoEver.begin(), unitsWhatSoEver.end(), std::inserter(units, units.end()), [this](Unit* const unit) { return ShouldContain(unit); });
    std::copy_if(units.begin(), units.end(), std::inserter(friends, friends.end()), [this](Unit* const unit) {return unit->GetTeamIndex() == GetTeamIndex(); });
    std::copy_if(units.begin(), units.end(), std::inserter(enemies, enemies.end()), [this](Unit* const unit) {return unit->GetTeamIndex() != GetTeamIndex(); });
}

int UnitAcquisitionCollider::GetTeamIndex()
{
    if (owner.expired())
        return teamIndex;
    else
        return owner.lock()->GetTeamIndex();
}

 

Unit 클래스의 멤버 변수로 쓰이며 각각 유닛의 공격범위, 유닛의 적 탐색 범위 역할을 수행하는 UnitAcquisitionCollider

...
...
    // 공격범위와 적 포착범위
    std::weak_ptr<UnitAcquisitionSphereCollider> attackRange;
    std::weak_ptr<UnitAcquisitionSphereCollider> acquisitionRange;
    std::weak_ptr<yunutyEngine::graphics::Animator> animatorComponent;
 ...
 ...

 

( 지면에서 문어발을 소환해 유닛을 중심으로 끌어모으는 스킬 )

 

 유닛의 스킬은 코루틴으로 구현했다.

 

코드 보기

더보기

지면에서 문어발을 소환해 유닛을 중심으로 끌어모으는 스킬의 코드

 

UrsulaParalysisSkill.cpp

coroutine::Coroutine UrsulaParalysisSkill::operator()()
{
    colliderEffectRatio = 3.0f * 0.5f;
    auto blockFollowingNavigation = owner.lock()->referenceBlockFollowingNavAgent.Acquire();
    auto blockAnimLoop = owner.lock()->referenceBlockAnimLoop.Acquire();
    auto disableNavAgent = owner.lock()->referenceDisableNavAgent.Acquire();
    animator = owner.lock()->GetAnimator();
    paralysisAnim = wanderResources::GetAnimation(owner.lock()->GetFBXName(), UnitAnimType::Skill2);

    owner.lock()->SetDefaultAnimation(UnitAnimType::None);
    owner.lock()->PlayAnimation(UnitAnimType::Skill2);

    effectColliderCoroutine = owner.lock()->StartCoroutine(SpawningFieldEffect(dynamic_pointer_cast<UrsulaParalysisSkill>(selfWeakPtr.lock())));
    effectColliderCoroutine.lock()->PushDestroyCallBack([this]()
        {
            tentacleAnimator->GetGI().SetPlaySpeed(1);
            waveAnimator->GetGI().SetPlaySpeed(1);
            waveVFXAnimator.lock()->SetSpeed(1);

            FBXPool::Instance().Return(tentacleObject);
            FBXPool::Instance().Return(waveObject);
            UnitAcquisitionSphereColliderPool::Instance().Return(damageCollider);
            UnitAcquisitionSphereColliderPool::Instance().Return(knockBackCollider);
        });

    while (!animator.lock()->IsDone())
    {
        co_await std::suspend_always{};
    }

    disableNavAgent.reset();
    blockFollowingNavigation.reset();
    owner.lock()->Relocate(owner.lock()->GetTransform()->GetWorldPosition());
    if (owner.lock()->GetPendingOrderType() == UnitOrderType::None)
        owner.lock()->OrderAttackMove();
    co_return;
}

 

 Unit 클래스에서 스킬 코루틴 객체를 실행하고 coroutineSkill 변수에 집어넣는 코드

 

Unit.cpp

template<>
void Unit::OnStateEngage<UnitBehaviourTree::SkillCasting>()
{
    assert(pendingSkill.get() != nullptr);
    SetDesiredRotation(pendingSkill.get()->targetPos - GetTransform()->GetWorldPosition());
    onGoingSkill = std::move(pendingSkill);
    coroutineSkill = StartCoroutine(onGoingSkill.get()->operator()());
    onSkillActivation(onGoingSkill);
}

 

 스킬 코루틴이 현재 진행중이라면 유닛 행동 트리의 SkillOnGoing 노드가 활성화된다.

 

Unit.cpp

...
...
  unitBehaviourTree[UnitBehaviourTree::Skill][UnitBehaviourTree::SkillOnGoing].enteringCondtion = [this]()
        {
            return !coroutineSkill.expired();
        };
...
...

유닛과 투사체같이 재활용할 수 있는 객체들은 오브젝트 풀에서 관리되도록 만들었다.

 

오브젝트 풀 관련 코드 보기

더보기

GameObjectPool.h

#pragma once
#include "Component.h"
#include "GameObject.h"
#include "Scene.h"
#include "SingletonClass.h"
#include <functional>
#include <unordered_set>
#include <deque>
#include <vector>

#ifdef YUNUTY_EXPORTS
#define YUNUTY_API __declspec(dllexport)
#else
#define YUNUTY_API __declspec(dllimport)
#endif

using namespace std;
using namespace yunutyEngine;
namespace yunutyEngine
{
    // 오브젝트가 필요하면 생성하고, 생성된 오브젝트의 용도가 다 끝나면 폐기하는 대신 비활성화만 시켜놨다가,
    // 다시 오브젝트에 대한 요청이 들어오면 재활성화 시키는 오브젝트 풀 객체입니다. 
    template<typename RepresenstativeComponent>
    class GameObjectPool
    {
        static_assert(std::is_base_of<Component, RepresenstativeComponent>::value, "only derived classes from component are allowed");
    public:
        virtual GameObject* GameObjectInitializer() { return Scene::getCurrentScene()->AddGameObject(); };
        // 빌려줄 오브젝트가 단 하나도 없을 경우, 활성화된 씬에서 게임 오브젝트를 생성한 뒤 RepresentativeComponent를 붙여줍니다.
        // 이 작업 이후 추가적으로 실행할 초기화 함수를 정의합니다.
        virtual void ObjectInitializer(std::weak_ptr<RepresenstativeComponent> comp) = 0;
        // 오브젝트를 하나 빌렸을 때 실행될 함수를 정의합니다. 굳이 정의 안해도 됩니다.
        virtual void OnBorrow(std::weak_ptr<RepresenstativeComponent> comp) {};
        // 게임 오브젝트 풀에 저장된 게임 오브젝트를 활성화합니다.
        std::weak_ptr<RepresenstativeComponent> Borrow();
        // 게임 오브젝트 풀에서 관리하는 게임 오브젝트를 되돌려 줍니다.
        void Return(std::weak_ptr<RepresenstativeComponent>);
        int poolObjectsSize() { return poolObjects.size(); };
        int expendableObjectsSize() { return expendableObjects.size(); };
        void Clear();

    protected:
        const deque<std::weak_ptr<RepresenstativeComponent>>& GetPoolObjects() {
            return poolObjects;
        }
    protected:
        std::deque<std::weak_ptr<RepresenstativeComponent>> poolObjects;
        std::deque<std::weak_ptr<RepresenstativeComponent>> expendableObjects;
        std::unordered_set<RepresenstativeComponent*> expendableObjectSet;
    };

    template<typename RepresenstativeComponent>
    std::weak_ptr<RepresenstativeComponent> GameObjectPool<RepresenstativeComponent>::Borrow()
    {
        if (expendableObjects.empty())
        {
            auto gameObject = GameObjectInitializer();
            auto component = gameObject->AddComponentAsWeakPtr<RepresenstativeComponent>();
            ObjectInitializer(component);
            poolObjects.push_back(component);
            expendableObjects.push_back(component);
        }
        auto target = expendableObjects.front();
        expendableObjects.pop_front();
        expendableObjectSet.erase(target.lock().get());
        OnBorrow(target);
        target.lock()->GetGameObject()->SetSelfActive(true);
        return target;
    }

    template<typename RepresenstativeComponent>
    void GameObjectPool<RepresenstativeComponent>::Return(std::weak_ptr<RepresenstativeComponent> obj)
    {
        if (expendableObjectSet.contains(obj.lock().get()))
        {
            return;
        }
        expendableObjects.push_back(obj);
        expendableObjectSet.insert(obj.lock().get());
        obj.lock()->GetGameObject()->SetSelfActive(false);
    }
    template<typename RepresenstativeComponent>
    inline void GameObjectPool<RepresenstativeComponent>::Clear()
    {
        for (auto each : poolObjects)
        {
            Scene::getCurrentScene()->DestroyGameObject(each.lock()->GetGameObject());
        }
        poolObjects.clear();
        expendableObjects.clear();
        expendableObjectSet.clear();
    }
}

 

UnitPool.h

#pragma once
#include "YunutyEngine.h"

namespace application
{
    namespace editor
    {
        class Unit_TemplateData;
        class UnitData;
    }
}
class Unit;

class UnitPool : public SingletonClass<UnitPool>
{
public:
    /// Editor 에서 Map 이 바뀔 때, 초기화하는 함수입니다.
    void Reset();
    std::weak_ptr<Unit> Borrow(application::editor::UnitData* data);
    std::weak_ptr<Unit> Borrow(application::editor::Unit_TemplateData* td, const Vector3d& position, float rotation);
    std::weak_ptr<Unit> Borrow(application::editor::Unit_TemplateData* td, const Vector3d& position, const Quaternion& rotation);
    void Return(std::weak_ptr<Unit>);
private:
    std::weak_ptr<Unit> Borrow(application::editor::Unit_TemplateData* td);
    class PoolByTemplate : public GameObjectPool<Unit>
    {
    public:
        application::editor::Unit_TemplateData* templateData{ nullptr };
        virtual void ObjectInitializer(std::weak_ptr<Unit> unit) override;
        virtual void OnBorrow(std::weak_ptr<Unit> unit) override;
    };
    std::unordered_map<const application::editor::Unit_TemplateData*, std::shared_ptr<PoolByTemplate>> poolsByTemplate;
};

 

ProjectilePool.h

#pragma once
#include "YunutyEngine.h"
#include "Projectile.h"
#include "ProjectileType.h"
#include "InWanderLand.h"
#include <DirectXMath.h>

class Projectile;
struct ProjectileType;
struct ProjectileHoming;
// ProjectilePool은 fbx 이름별로 여러가지 풀들을 갖고 있다.
// ProjectilePool은 풀 집합체와 같다고 할 수 있다.
//template <typename T>
class ProjectilePool : public SingletonClass<ProjectilePool>
{
public:
    std::weak_ptr<Projectile> Borrow(std::weak_ptr<Unit> owner, std::weak_ptr<Unit> opponent);
    void Return(std::weak_ptr<Projectile>);
private:
    class PoolByMesh : public GameObjectPool<Projectile>
    {
    public:
        string fbxname;
        virtual void ObjectInitializer(std::weak_ptr<Projectile> projectile) override;
    };
    std::unordered_map<string, std::shared_ptr<PoolByMesh>> poolsByFBX;
    std::unordered_map<Projectile*, std::weak_ptr<PoolByMesh>> poolsByProjectile;
};

 

 

 

 

 

 

6. Wander 맵 에디터

 게임에 쓰이는 맵 에디터는 워크래프트 3의 맵 에디터와 유사하게 개발했다.

( 통행 가능한 지형 마킹, 좌 : 워크래프트3 에디터, 우 : Wander 맵 에디터 )

 

( 밸런스 수치 변경 )
( 장식물 배치 )
( 유닛 배치 )
( 트리거 볼륨 역할을 하는 지역 배치 )


 워크래프트3 에디터에서 특히나 유용하게 참고한 기능은 블록 코딩 기반 스크립팅 기능이었다. 이를 통해 레벨 디자이너가 게임의 특수 이벤트와 컷신을 쉽게 만들 수 있었다.

워크래프트 3 에디터의 블록 코딩 스크립트로 구현한 컷신

 

( Wander 에디터의 블록 코딩 스크립트로 구현한 컷신 )

 

 또 우리 게임에 필요한 기능으로 유니티에서 배치된 장식물들의 트랜스폼 정보와 라이트맵 정보를 임포트하는 기능, 특정 지역에 진입하면 투명하게 처리할 장식물들을 지정하는 기능, 적군 웨이브를 배치하는 기능을 구현했다.

 [ WanderEditor가 Import Unreal Data를 통해 장식물의 배치와 라이트맵 정보를 들이는 모습 ]

( 에디터에서 지역별로 비활성화시킬 장식물들을 지정해두면 인게임에서 주인공 일행이 해당 지역에 진입할 때 지정된 장식물들이 투명처리된다. )
( 에디터에서 웨이브 유닛들을 시간별로 배치해두면 인게임 웨이브가 진행되면서 유닛들이 순차로 등장한다. )

 

 

 

 

 

 

7. UI

( UI는 유니티에서 배치된 UI 객체들의 정보를 임포트하는 방식으로 구현했다. )

 

 유니티에서 배치된 UI 정보들을 게임에서 임포트할 수 있도록 만들었다. 또한 UI 객체에 속성을 부여할수 있게 만들어 UI 창이 등장하거나 사라질 때 다양한 연출을 넣을 수 있도록 했다. UI 객체를 특정할 수 있어야 하는 경우, Enum으로 ID를 부여했다.

 

UI 익스포트 관련 코드 보기

더보기

JsonUIData.h

#pragma once
#include "Storable.h"
#include "JsonUIFloatType.h"

struct JsonUIData
{
    // string imageName;
    std::string uiName;
    std::array<float, JsonUIFloatType::NUM> floats;
    int uiIndex;
    // 부모가 없다면 부모를 1920 * 1080 크기의 스크린 스페이스로 가정하게 된다.
    int parentUIIndex = -1;
    std::string imagePath;
    std::string koreanImagePath;
    std::string englishImagePath;
    std::string japaneseImagePath;
    int imagePriority;
    // 만약 플래그에 openingButton이 있다면 버튼을 눌렀을 때 활성화시킬 UI 창을 의미한다.
    std::vector<int> openTargets;
    // 만약 플래그에 diablingButton이 있다면 버튼을 눌렀을 때 비활성화시킬 UI 창을 의미한다.
    std::vector<int> closeTargets;
    std::vector<int> disablePropagationTargets;
    std::vector<int> enablePropagationTargets;
    std::vector<int> hoverEnableTargets;
    std::vector<float> color;
    std::vector<float> pivot;
    std::vector<float> anchor;
    std::vector<float> anchoredPosition;
    std::vector<float> enableOffset;
    std::vector<float> disableOffset;
    bool popUpX, popUpY, popUpZ;
    bool popDownX, popDownY, popDownZ;
    // 숫자를 표현할 때, 각 자릿수별로 숫자 폰트 이미지를 갈아치울 UI 객체들을 의미합니다.
    std::vector<int> numberDigits;
    // 숫자를 표현할 때 사용할 숫자 폰트 이미지 세트를 의미합니다.
    int numberFontSet;
    // 0~9까지의 숫자 폰트 이미지를 매핑합니다.
    std::vector<int> numberFontSetImages;
    // 숫자를 표현할 때, 올림처리하면 true, 내림처리하면 false입니다.
    bool numberCeil;
    // 0을 표시할지 여부입니다.
    bool numberShowZero;
    std::string soundOnClick;
    std::string soundOnHover;
    std::string soundOnEnable;
    std::string soundOnDisable;
    int enableCurveType;
    int disableCurveType;
    // 업그레이드 버튼의 경우, 활성화하기 위해 필요한 다른 버튼의 인덱스를 의미합니다.
    int dependentUpgrade{ -1 };
    vector<float> linearClipOnEnableStart;
    vector<float> linearClipOnEnableDir;
    int linearClipOnEnableCurveType;
    vector<float> linearClipOnDisableStart;
    vector<float> linearClipOnDisableDir;
    int linearClipOnDisableCurveType;
    vector<float> colorTintOnEnableStart;
    vector<float> colorTintOnEnableEnd;
    int colorTintOnEnableCurveType;
    vector<float> colorTintOnDisableStart;
    vector<float> colorTintOnDisableEnd;
    int colorTintOnDisableCurveType;
    // 전체 셀의 갯수
    int barCells_CellNumber;
    // 임의로 사용하게 될 사용자 플래그
    bool disableOnStartEdtior;
    bool disableOnStartExe;
    string musicPlayOnEnable_musicClip;
    string musicPlayOnDisable_musicClip;
    std::vector<int> exclusiveEnableGroup;
    string animatedSpriteFolderPath;
    bool animatedSpriteIsRepeat;
    bool duplicate_poolable;
    string videoPath1;
    string videoPath2;
    bool videoUnscaledDeltaTime;
    // 숫자를 가운데 정렬할지의 여부입니다.
    bool centerAlign;
    bool reAlignOnDisable;

    int customFlags;
    int customFlags2;
    // UI의 고유한 EnumID
    int enumID;
    // 사운드 매핑을 위한 ID
    int soundEnumID;
    FROM_JSON(JsonUIData);
};

(Json 형식으로 저장되는 UI 객체 하나의 정보)

 

UIExportFlag.h

enum class UIExportFlag
{
    None = 0,
    // 마우스가 올라갔을 때, 마우스가 클릭했을 때 등의 상호작용을 받습니다.
    IsButton = 1 << 0,
    // 특정 UI들을 닫는 용도로 쓰입니다.
    CloseButton = 1 << 1,
    // 특정 UI들을 여는 용도로 쓰입니다.
    OpeningButton = 1 << 2,
    // 비활성화시 그레이 스케일 적용된 이미지를 출력합니다.
    GrayScaleDisable = 1 << 4,
    // 활성화될때 팝업 효과를 줍니다.
    IsPoppingUp = 1 << 5,
    // 비활성화될때 사이즈를 줄입니다.
    IsPoppingDown = 1 << 6,
    // 버튼 위에 마우스 포인터가 올라왔을 때 자식 오브젝트들을 보여줍니다.
    IsIncludingTooltips = 1 << 7,
    // 게임을 시작할때 비활성화된 채로 시작합니다.
    DisableOnStart = 1 << 8,
    // 버튼이 토글 동작을 할때 사용됩니다.
    IsToggle = 1 << 9,
    // UI 이미지가 활성화될때 트랜슬레이션 오프셋 애니메이션을 재생합니다.
    IsTranslatingOnEnable = 1 << 10,
    // UI 이미지가 비활성화될때 트랜슬레이션 오프셋 애니메이션을 재생합니다.
    IsTranslatingOnDisable = 1 << 11,
    // 체력, 마나와 같은 값을 70 / 100과 같은 형태로 표시하는 텍스트입니다.
    // Adjuster에 들어가는 실수 값을 분자로 사용하며, 최대값이 갱신되면 분모도 갱신됩니다.
    //IsGuageText = 1 << 12,
    // 0~1 사이의 값으로 UI 이미지의 세로 크기를 조절할 수 있습니다.
    CanAdjustHeight = 1 << 13,
    // 0~1 사이의 값으로 UI 이미지의 가로 크기를 조절할 수 있습니다.
    CanAdjustWidth = 1 << 14,
    // 0~1 사이의 값으로 UI 이미지의 Radial Fill 각을 조절할 수 있습니다.
    CanAdjustRadialFill = 1 << 15,
    // 원본 이미지가 그저 사각형 흰색 이미지인지 나타냅니다.
    IsBlankImage = 1 << 16,
    // 이 이미지 아래에 깔린 버튼은 클릭이 가능합니다.
    NoOverlaying = 1 << 17,
    // 이미지가 커졌다가 작아지는 애니메이션을 반복적으로 재생합니다.
    IsPulsing = 1 << 18,
    ///////////////////////////
    // 여러 자리의 숫자를 표현합니다. 기본적으로 내림으로 연산이 되나, 0~1 사이의 값을 올림처리할지, 내림처리할지는 속성값에 따라 결정됩니다.
    IsNumber = 1 << 19,
    // 클릭할 때 효과음을 재생합니다.
    PlaySoundOnClick = 1 << 20,
    // 마우스가 올라갔을 때 효과음을 재생합니다.
    PlaySoundOnHover = 1 << 21,
    // UI 객체가 활성화될때 효과음을 재생합니다.
    PlaySoundOnEnable = 1 << 22,
    // UI 객체가 비활성화될때 효과음을 재생합니다.
    PlaySoundOnDisable = 1 << 23,
    // 자식 게임 객체들의 위치를 인덱스별로 저장해뒀다가, 활성화된 자식 객체들을 인덱스별로 저장된 위치에 자식 인덱스 순서대로 배치합니다.
    PriorityLayout = 1 << 24,
    // 0~9까지의 숫자 폰트를 의미하는 폰트 이미지 세트를 자식으로 가집니다. 이 이미지 세트는 IsNumber 속성을 가진 UI객체가 폰트 이미지를 바꾸는 데에 사용됩니다.
    IsDigitFont = 1 << 25,
    // 스킬 업그레이드에 쓰이는 버튼입니다. 선행 업그레이드를 가질 수 있습니다.
    IsSkillUpgrade = 1 << 26,
    // UI의 크기가 화면의 해상도에 따라 달라집니다.
    // 1920, 1080 해상도에서 만들어진 UI 이미지를 1280, 720 해상도에서 사용할 때, 이미지의 크기를 0.6667배로 조정합니다.
    ScaledByResolution = 1 << 27,
    // UI가 활성화될때 시간을 멈춥니다.
    TimeStopOnEnable = 1 << 28,
    // UI가 비활성화될때 시간정지를 해제합니다.
    TimeContinueOnDisable = 1 << 29,
    // UI가 활성화될때 투명도 애니메이션을 재생합니다.
    ColorTintOnEnable = 1 << 30,
    // UI가 비활성화될때 투명도 애니메이션을 재생합니다.
    ColorTintOnDisable = 1 << 31,

};
enum class UIExportFlag2
{
    None = 0,
    LinearClipOnEnable = 1 << 0,
    LinearClipOnDisable = 1 << 1,
    Duplicatable = 1 << 2,
    // 체력바, 마나 바 등 연속적인 데이터의 수치를 추정하기 위해 게이지의 일정단위마다 셀을 끊어 표시하고 싶을때 사용됩니다.
    IsBarCells = 1 << 3,
    // 0일때 하나도 클립 안함, 1일때 완전히 클립함
    AdjustLinearClip = 1 << 4,
    PlayMusicOnEnable = 1 << 5,
    PlayMusicOnDisable = 1 << 6,
    PauseMusicOnEnable = 1 << 7,
    UnPauseMusicOnDisable = 1 << 8,
    MultiplyMusicVolumeOnEnableDisable = 1 << 9,
    ExclusiveEnable = 1 << 10,
    DisableAfterEnable = 1 << 11,
    Dialogue_Manual = 1 << 12,
    Dialogue_Timed = 1 << 13,
    RedundantEnable = 1 << 14,
    AnimatedSprite = 1 << 15,
    ScriptUI = 1 << 16,
    EnumSound = 1 << 17,
    Rotating = 1 << 18,
    StartGameButton = 1 << 19,
    ReturnToTitleButton = 1 << 20,
    Video = 1 << 21,
    PropagateEnable = 1 << 22,
    PropagateDisable = 1 << 23,
    CapsuleClip = 1 << 24,
    DisableOnlyOnImport = 1 << 25,
    MultiLanguage = 1 << 26,
};

( UI 객체의 속성을 나타내는 플래그 )

 

UIEnumID

#pragma once
#include "PodStructs.h"

enum class UIEnumID
{
    // 아무것도 아님.
    None = 0,
    // 로빈, 우르술라, 헨젤의 초상화, 초상화 위의 이름 태그, 초상화를 가리는 적색 부상 오버레이, 체력바, 체력을 표시하는 텍스트 UI
    CharInfo_Robin,
    CharInfo_Ursula,
    CharInfo_Hansel,
    CharInfo_Robin_Left,
    CharInfo_Ursula_Left,
    CharInfo_Hansel_Left,
    CharInfo_Portrait,
    CharInfo_PortraitBloodOverlay,
    CharInfo_NameTag,
    CharInfo_HP_Fill,
    CharInfo_HP_Number_Current,
    CharInfo_HP_Number_Max,
    CharInfo_Buff_Bleeding,
    CharInfo_Buff_Blinding,
    CharInfo_Buff_Paralysis,
    CharInfo_Buff_KnockBack,
    CharInfo_Buff_Taunted,
    CharInfo_Buff_UrsulaSelf,
    CharInfo_Buff_HanselBuff,
    CharInfo_Buff_HanselDebuff,
    CharInfo_Skill_Use_Q,
    CharInfo_Skill_Use_Q_Overlay,
    CharInfo_Skill_Use_Q_Cooltime_Number,
    CharInfo_Skill_Use_W,
    CharInfo_Skill_Use_W_Overlay,
    CharInfo_Skill_Use_W_Cooltime_Number,
    // 전술모드 토글버튼
    Toggle_TacticMode,
    // 전술모드 토글버튼 흑백 오버레이
    Toggle_TacticMode_Overlay,
    Toggle_TacticMode_Cooltime_Number,
    // 화면 하단 마나 게이지
    ManaBar1,
    // 전술 모드 마나 게이지
    ManaBar2,
    // 화면 하단 마나 게이지 중 사용 대상이 되는 마나 오버레이
    ManaBarSpendOverlay1,
    // 전술 모드 마나 게이지 중 사용 대상이 되는 마나 오버레이
    ManaBarSpendOverlay2,
    // 현재 마나량을 나타내는 텍스트
    Mana_Text_CurrentMP,
    Mana_Text_CurrentMP_Tactic,
    // 최대 마나량을 나타내는 텍스트
    Mana_Text_MaxMP,
    Mana_Text_MaxMP_Tactic,
    // 사운드가 켜진 상태에서 표시할 UI버튼
    Sound_On,
    // 사운드가 꺼진 상태에서 표시할 UI버튼
    Sound_Off,
    // 음악이 켜진 상태에서 표시할 UI버튼
    Music_On,
    // 음악이 꺼진 상태에서 표시할 UI버튼
    Music_Off,
    // 인게임에서 사용되는 캐릭터 상태창, 전술모드 진입 버튼을 포함하는 하단 레이아웃
    Ingame_Bottom_Layout,
    // 인게임에서 사용되는 메뉴버튼
    Ingame_MenuButton,
    // 콤보 횟수를 표시함
    Ingame_Combo_Number,
    // "Combo"라는 문자를 표시함
    Ingame_Combo_Text,
    Ingame_Combo_DescriptionTitleImg,
    // 어떤 콤보를 달성해야 하는지 설명하는 텍스트, 몇 콤보를 달성해야 하는지에 대한 표시, 콤보 달성을 표시하는 체크박스의 V자 문양
    Ingame_Combo_Description1,
    Ingame_Combo_DescriptionImageUnfinished1,
    Ingame_Combo_DescriptionImageFinished1,
    Ingame_Combo_TargetNumUnfinished1,
    Ingame_Combo_TargetNumFinished1,
    Ingame_Combo_Check1,
    Ingame_Combo_Description2,
    Ingame_Combo_DescriptionImageUnFinished2,
    Ingame_Combo_DescriptionImageFinished2,
    Ingame_Combo_TargetNumUnfinished2,
    Ingame_Combo_TargetNumFinished2,
    Ingame_Combo_Check2,
    Ingame_Combo_Description3,
    Ingame_Combo_DescriptionImageUnFinished3,
    Ingame_Combo_DescriptionImageFinished3,
    Ingame_Combo_TargetNumUnfinished3,
    Ingame_Combo_TargetNumFinished3,
    Ingame_Combo_Check3,
    Ingame_Vinetting,
    BlackMask_Alpha,
    BlackMask_RightToLeft,
    BlackMask_TopToBottom,
    BlackMask_LeftToRight,
    BlackMask_BottomToTop,
    // 게임을 시작할 때, 메인화면으로 돌아갈 때에 사용되는 블랙마스크, 단순 연출용이 아니라 실제 게임로드, 종료를 위한 기능이 있다.
    BlackMask_GameLoad,
    BlackMask_GameEnd,
    LetterBox_Top,
    LetterBox_Bottom,
    // 인게임 하단에서 클릭할 시 스킬트리 메뉴를 여는 메뉴버튼
    InGame_SkiltreeMenu_Active,
    InGame_SkiltreeMenu_InActive,
    SkillPoint_Number,
    SkillUpgradeButtonRobin00,
    SkillUpgradeButtonRobin11,
    SkillUpgradeButtonRobin12,
    SkillUpgradeButtonRobin21,
    SkillUpgradeButtonRobin22,
    SkillUpgradeButtonUrsula00,
    SkillUpgradeButtonUrsula11,
    SkillUpgradeButtonUrsula12,
    SkillUpgradeButtonUrsula21,
    SkillUpgradeButtonUrsula22,
    SkillUpgradeButtonHansel00,
    SkillUpgradeButtonHansel11,
    SkillUpgradeButtonHansel12,
    SkillUpgradeButtonHansel21,
    SkillUpgradeButtonHansel22,
    SkillUpgradeButton_UpgradedImage,
    SkillUpgradeButton_Upgradable,
    SkillUpgradeButton_InUpgradableImage,
    PopUpMessage_NotEnoughSP,
    PopUpMessage_RequirementNotMet,
    PopUpMessage_PermissionForUpgrade,
    PopUpMessage_PermissionForUpgradeProceedButton,
    PopUpMessage_WarningForRestart,
    PopUpMessage_WarningForRestart_ProceedButton,
    StatusBar_Elite,
    StatusBar_MeleeEnemy,
    StatusBar_Hero_Robin,
    StatusBar_Hero_Ursula,
    StatusBar_Hero_Hansel,
    StatusBar_Boss,
    StatusBar_LeftDoor,
    StatusBar_RightDoor,
    StatusBar_Boss_Tactic,
    StatusBar_LeftDoor_Tactic,
    StatusBar_RightDoor_Tactic,
    StatusBar_HP_Number_Current,
    StatusBar_HP_Number_Max,
    StatusBar_HP_Cells,
    StatusBar_HP_Fill,
    StatusBar_SelectionName,
    TitleRoot,
    Quit_Proceed,
    MouseCursor,
    MouseCursor_Free,
    MouseCursor_Skill,
    MouseCursor_AttackMove,
    MouseCursor_OnButton,
    MouseCursor_OnEnemy,
    MouseCursor_OnAlly,
    MoveTargetFeedbackAnimSprites,
    AttackMoveTargetFeedbackAnimSprites,
    ErrorPopup_NoMana,
    ErrorPopup_Cooltime,
    ErrorPopup_CantLand,
    ErrorPopup_TacticQueueFull,
    VictoryPage,
    DefeatPage,
    TacticModeIngameUI,
    TacticModeRevertButton_Active,
    TacticModeRevertButton_InActive,
    TacticModeCommandIcon1,
    TacticModeCommandIcon2,
    TacticModeCommandIcon3,
    TacticModeCommandIcon4,
    TacticModeCommandIcon5,
    TacticModeCommandIcon6,
    TacticModeCommandIcon7,
    TacticModeCommandIcon8,
    TacticModeCommandIcon9,
    TacticModeCommandIcon10,
    TacticModeCommandIcon11,
    TacticModeCommandIcon12,
    BeaconOutside_Robin,
    BeaconOutside_Ursula,
    BeaconOutside_Hansel,
    BeaconOutside_Arrow,
    // 일반 잡졸들에 적용되는 데미지 표시기
    DamageIndicator_Default,
    DamageIndicator_Critical,
    DamageIndicator_Default_RedFont,
    DamageIndicator_Critical_RedFont,
    DamageIndicator_Default_BlueFont,
    DamageIndicator_Critical_BlueFont,
    DamageIndicator_Default_Cyan,
    DamageIndicator_Critical_Cyan,
    DamageIndicator_Default_BlackWhiteFont,
    DamageIndicator_Critical_BlackWhiteFont,
    DamageIndicator_Missed,
    DamageIndicator_Number,
    // 보스전때에만 출력되는 UI들
    BossUI_Default,
    BossUI_Tactic,
    CheckPointReached,
    LoadCheckPointButton1,
    LoadCheckPointButton2,
    InGameMenu,
    Localization,
};

( 특정 UI들의 고유한 ID를 나타내는 Enum )

 

UIYutility.cs, 유니티에서 UI 데이터를 내보내는데 쓰이는 클래스

using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEditor;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System.Collections.Generic;
using System.IO;
using UnityEngine.Windows.Speech;
using Newtonsoft.Json.Serialization;
using UnityEngine.UI;
using UnityEngine.Assertions;
using JetBrains.Annotations;
using System;
using System.Linq;

public class UIYutility : MonoBehaviour
{
    [Serializable]
    public class UIDataList : List<UIExportJsonData>
    {
        public List<UIExportJsonData> dataList = new List<UIExportJsonData>();
    }
    [MenuItem("Yutility/Export ui data")]
    private static void ExportUIData()
    {
        string path = EditorUtility.SaveFilePanel("Save file as...", "", "new iwui data", "iwui");
        if (path == "")
        {
            return;
        }
        Dictionary<GameObject, int> uiIDDictanary = new Dictionary<GameObject, int>();
        HashSet<string> foundManualDialogues = new HashSet<string>();
        HashSet<string> foundTimedDialogues = new HashSet<string>();
        HashSet<string> foundScriptUI = new HashSet<string>();
        HashSet<SoundEnumComp.SoundEnumID> foundSounds = new HashSet<SoundEnumComp.SoundEnumID>();
        HashSet<UIEnumIDComp.UIEnumID> foundUIIds = new HashSet<UIEnumIDComp.UIEnumID>();
        UIDataList uilist = new UIDataList();
        //List<UIExportJsonData> uIExportJsonDatas = new List<UIExportJsonData>();
        bool failedAlready = false;
        string jsonString = "";
        List<GameObject> roots = new List<GameObject>();
        List<GameObject> taggedObjects = new List<GameObject>();
        UnityEngine.SceneManagement.Scene currentScene = SceneManager.GetActiveScene();
        currentScene.GetRootGameObjects(roots);
        foreach (var each in roots)
        {
            DFS(each, "UIExportTarget", taggedObjects, uiIDDictanary);
        }
        foreach (var each in taggedObjects)
        {
            try
            {
                each.transform.localScale = Vector3.one;
                UIExportJsonData data = new UIExportJsonData();
                data.floats = new float[(int)JsonUIFloatType.NUM];
                data.uiName = each.name;
                data.uiIndex = uiIDDictanary[each];
                if (each.transform.parent && each.transform.parent.tag == "UIExportTarget")
                {
                    data.parentUIIndex = uiIDDictanary[each.transform.parent.gameObject];
                }
                var imageComp = each.GetComponent<UnityEngine.UI.Image>();
                if (imageComp)
                {
                    data.imagePath = AssetDatabase.GetAssetPath(imageComp.sprite);
                    data.imagePath = data.imagePath.Replace("Assets/Resources/", "");
                    if (data.imagePath == "")
                    {
                        data.customFlags |= (int)UIExportFlag.IsBlankImage;
                    }
                    data.color = new float[] { imageComp.color.r, imageComp.color.g, imageComp.color.b, imageComp.color.a };
                }
                var rectTransform = each.GetComponent<RectTransform>();
                Assert.IsTrue(rectTransform.anchorMin == rectTransform.anchorMax);
                data.pivot = new float[] { rectTransform.pivot.x, rectTransform.pivot.y };
                data.anchor = new float[] { rectTransform.anchorMax.x, rectTransform.anchorMax.y };
                data.anchoredPosition = new float[] { rectTransform.anchoredPosition.x, rectTransform.anchoredPosition.y };
                data.floats[(int)JsonUIFloatType.rotation] = rectTransform.localRotation.eulerAngles.z;
                data.floats[(int)JsonUIFloatType.width] = rectTransform.sizeDelta.x;
                data.floats[(int)JsonUIFloatType.height] = rectTransform.sizeDelta.y;
                // 임의로 사용하게 될 사용자 플래그


                if (each.GetComponent<AnchoredPosMustBeZero>())
                {
                    data.anchoredPosition[0] = 0;
                    data.anchoredPosition[1] = 0;
                }
                if (each.GetComponent<UIDoesPopup>())
                {
                    var comp = each.GetComponent<UIDoesPopup>();
                    data.customFlags |= (int)UIExportFlag.IsPoppingUp;
                    data.floats[(int)JsonUIFloatType.popUpDuration] = comp.popUpDuration;
                    data.floats[(int)JsonUIFloatType.popUpFrom] = comp.popUpFrom;
                    data.floats[(int)JsonUIFloatType.popUpTo] = comp.popUpTo;
                    data.popUpX = comp.popUpX;
                    data.popUpY = comp.popUpY;
                    data.popUpZ = comp.popUpZ;
                }
                if (each.GetComponent<UIDoesPopDown>())
                {
                    var comp = each.GetComponent<UIDoesPopDown>();
                    data.customFlags |= (int)UIExportFlag.IsPoppingDown;
                    data.floats[(int)JsonUIFloatType.popDownDuration] = comp.popDownDuration;
                    data.floats[(int)JsonUIFloatType.popDownFrom] = comp.popDownFrom;
                    data.floats[(int)JsonUIFloatType.popDownTo] = comp.popDownTo;
                    data.popDownX = comp.popDownX;
                    data.popDownY = comp.popDownY;
                    data.popDownZ = comp.popDownZ;
                }
                if (each.GetComponent<UIIncludingTooltips>())
                {
                    var comp = each.GetComponent<UIIncludingTooltips>();
                    data.customFlags |= (int)UIExportFlag.IsButton;
                    data.customFlags |= (int)UIExportFlag.IsIncludingTooltips;
                    data.hoverEnableTargets = comp.tooltips.Select(each => uiIDDictanary[each]).ToArray();
                }
                if (each.GetComponent<UIIsOpeningButton>())
                {
                    var comp = each.GetComponent<UIIsOpeningButton>();
                    data.customFlags |= (int)UIExportFlag.IsButton;
                    data.customFlags |= (int)UIExportFlag.OpeningButton;
                    if (each.GetComponent<UIIsOpeningButton>().canToggle)
                    {
                        data.customFlags |= (int)UIExportFlag.IsToggle;
                    }
                    if (comp.targets != null)
                    {
                        foreach (var eachOpenTarget in comp.targets)
                        {
                            if (eachOpenTarget == null)
                            {
                                Debug.LogError("null target at " + comp.gameObject.name, comp.gameObject);
                            }
                        }
                        data.openTargets = comp.targets.Where(each => each != null).Select(each => uiIDDictanary[each]).ToArray();
                    }
                }
                if (each.GetComponent<UICloseButton>())
                {
                    var comp = each.GetComponent<UICloseButton>();
                    data.customFlags |= (int)UIExportFlag.IsButton;
                    data.customFlags |= (int)UIExportFlag.CloseButton;
                    if (comp.targets != null)
                    {
                        foreach (var eachCloseTarget in comp.targets)
                        {
                            if (eachCloseTarget == null)
                            {
                                Debug.LogError("null target at " + comp.gameObject.name, comp.gameObject);
                            }
                        }
                        data.closeTargets = comp.targets.Select(each => uiIDDictanary[each]).ToArray();
                    }
                }
                if (each.GetComponent<PropagateDisable>())
                {
                    var comp = each.GetComponent<PropagateDisable>();
                    data.customFlags2 |= (int)UIExportFlag2.PropagateDisable;
                    if (comp.targets != null)
                    {
                        data.disablePropagationTargets = comp.targets.Select(each => uiIDDictanary[each]).ToArray();
                    }
                }
                if (each.GetComponent<PropagateEnable>())
                {
                    var comp = each.GetComponent<PropagateEnable>();
                    data.customFlags2 |= (int)UIExportFlag2.PropagateEnable;
                    if (comp.targets != null)
                    {
                        data.enablePropagationTargets = comp.targets.Select(each => uiIDDictanary[each]).ToArray();
                    }
                }
                if (each.GetComponent<UITranslatingOnEnable>())
                {
                    var comp = each.GetComponent<UITranslatingOnEnable>();
                    data.customFlags |= (int)UIExportFlag.IsTranslatingOnEnable;
                    data.floats[(int)JsonUIFloatType.enableDelay] = comp.delayTime;
                    data.floats[(int)JsonUIFloatType.enableDuration] = comp.transitioningDuration;
                    data.enableOffset = new float[] { comp.offset.x, comp.offset.y };
                    data.enableCurveType = (int)comp.curveType;
                }
                if (each.GetComponent<UITranslatingOnDisable>())
                {
                    var comp = each.GetComponent<UITranslatingOnDisable>();
                    data.customFlags |= (int)UIExportFlag.IsTranslatingOnDisable;
                    data.floats[(int)JsonUIFloatType.disableDelay] = comp.delayTime;
                    data.floats[(int)JsonUIFloatType.disableDuration] = comp.transitioningDuration;
                    data.disableOffset = new float[] { comp.offset.x, comp.offset.y };
                    data.disableCurveType = (int)comp.curveType;
                }
                if (each.GetComponent<CanAdjustHeight>())
                {
                    data.customFlags |= (int)UIExportFlag.CanAdjustHeight;
                    data.floats[(int)JsonUIFloatType.adjustingRate] = each.GetComponent<CanAdjustHeight>().adjustingRate;
                }
                if (each.GetComponent<CanAdjustWidth>())
                {
                    data.customFlags |= (int)UIExportFlag.CanAdjustWidth;
                    data.floats[(int)JsonUIFloatType.adjustingRate] = each.GetComponent<CanAdjustWidth>().adjustingRate;
                }
                if (each.GetComponent<CanAdjustRadialFill>())
                {
                    data.customFlags |= (int)UIExportFlag.CanAdjustRadialFill;
                    data.floats[(int)JsonUIFloatType.adjustingRate] = each.GetComponent<CanAdjustRadialFill>().adjustingRate;
                }
                if (each.GetComponent<DisableOnStart>())
                {
                    data.customFlags |= (int)UIExportFlag.DisableOnStart;
                    data.disableOnStartEdtior = each.GetComponent<DisableOnStart>().disableOnStartEditor;
                    data.disableOnStartExe = each.GetComponent<DisableOnStart>().disableOnStartExe;
                    if (each.GetComponent<DisableOnStart>().disableOnlyOnImport)
                    {
                        data.customFlags2 |= (int)UIExportFlag2.DisableOnlyOnImport;
                    }
                }
                if (each.GetComponent<UINoOverlaying>())
                {
                    data.customFlags |= (int)UIExportFlag.NoOverlaying;
                }
                if (each.GetComponent<CapsuleClip>())
                {
                    data.customFlags2 |= (int)UIExportFlag2.CapsuleClip;
                }
                if (each.GetComponent<IsPulsing>())
                {
                    var comp = each.GetComponent<IsPulsing>();
                    data.customFlags |= (int)UIExportFlag.IsPulsing;
                    data.floats[(int)JsonUIFloatType.pulsingMax] = comp.pulsingSizeMax;
                    data.floats[(int)JsonUIFloatType.pulsingMin] = comp.pulsingSizeMin;
                    data.floats[(int)JsonUIFloatType.pulsingPeriod] = comp.pulsingPeriod;
                }
                if (each.GetComponent<PlaySoundOnClick>())
                {
                    data.customFlags |= (int)UIExportFlag.PlaySoundOnClick;
                    data.soundOnClick = AssetDatabase.GetAssetPath(each.GetComponent<PlaySoundOnClick>().sound);
                    data.soundOnClick = data.soundOnClick.Replace("Assets/Resources/", "");
                }
                if (each.GetComponent<PlaySoundOnHover>())
                {
                    data.customFlags |= (int)UIExportFlag.PlaySoundOnHover;
                    data.soundOnHover = AssetDatabase.GetAssetPath(each.GetComponent<PlaySoundOnHover>().sound);
                    data.soundOnHover = data.soundOnHover.Replace("Assets/Resources/", "");
                }
                if (each.GetComponent<PlaySoundOnEnable>())
                {
                    data.customFlags |= (int)UIExportFlag.PlaySoundOnEnable;
                    data.soundOnEnable = AssetDatabase.GetAssetPath(each.GetComponent<PlaySoundOnEnable>().sound);
                    data.soundOnEnable = data.soundOnEnable.Replace("Assets/Resources/", "");
                }
                if (each.GetComponent<PlaySoundOnDisable>())
                {
                    data.customFlags |= (int)UIExportFlag.PlaySoundOnDisable;
                    data.soundOnDisable = AssetDatabase.GetAssetPath(each.GetComponent<PlaySoundOnDisable>().sound);
                    data.soundOnDisable = data.soundOnDisable.Replace("Assets/Resources/", "");
                }
                if (each.GetComponent<IsNumber>())
                {
                    var comp = each.GetComponent<IsNumber>();
                    data.customFlags |= (int)UIExportFlag.IsNumber;
                    data.numberCeil = comp.ceil;
                    data.numberShowZero = comp.showZero;
                    try
                    {
                        data.numberDigits = comp.digits.Select(each => uiIDDictanary[each.gameObject]).ToArray();
                    }
                    catch (Exception e)
                    {
                        Debug.LogError(e.Message, each.gameObject);
                        throw e;
                    }
                    data.numberFontSet = uiIDDictanary[comp.font.gameObject];
                }
                if (each.GetComponent<PriorityLayout>())
                {
                    var comp = each.GetComponent<PriorityLayout>();
                    data.customFlags |= (int)UIExportFlag.PriorityLayout;
                    data.floats[(int)JsonUIFloatType.layoutNormalizingTime] = comp.normalizingTime;
                    data.centerAlign = comp.centerAlign;
                    data.reAlignOnDisable = comp.reAlignOnDisable;
                }
                if (each.GetComponent<SkillUpgradeButton>())
                {
                    var comp = each.GetComponent<SkillUpgradeButton>();
                    data.customFlags |= (int)UIExportFlag.IsSkillUpgrade;
                    if (comp.dependentButton)
                    {
                        data.dependentUpgrade = uiIDDictanary[comp.dependentButton.gameObject];
                    }
                    else
                    {
                        data.dependentUpgrade = -1;
                    }
                }
                if (each.GetComponent<DigitFont>())
                {
                    data.customFlags |= (int)UIExportFlag.IsDigitFont;
                    data.numberFontSetImages = each.GetComponentsInChildren<Image>().Select(each => uiIDDictanary[each.gameObject]).ToArray();
                }
                if (each.GetComponent<ImagePriority>())
                {
                    var comp = each.GetComponent<ImagePriority>();
                    data.imagePriority = comp.priority;
                }
                if (each.GetComponent<TimeStopOnEnable>())
                {
                    var comp = each.GetComponent<TimeStopOnEnable>();
                    data.customFlags |= (int)UIExportFlag.TimeStopOnEnable;
                    data.floats[(int)JsonUIFloatType.timeStoppingDuration] = comp.timeStoppingDuration;
                }
                if (each.GetComponent<TimeContinueOnDisable>())
                {
                    data.customFlags |= (int)UIExportFlag.TimeContinueOnDisable;
                }
                if (each.GetComponent<ColorTintOnEnable>())
                {
                    var comp = each.GetComponent<ColorTintOnEnable>();
                    data.customFlags |= (int)UIExportFlag.ColorTintOnEnable;
                    data.colorTintOnEnableStart = new float[] { comp.startColor.r, comp.startColor.g, comp.startColor.b, comp.startColor.a };
                    data.colorTintOnEnableEnd = new float[] { comp.endColor.r, comp.endColor.g, comp.endColor.b, comp.endColor.a };
                    data.colorTintOnEnableCurveType = (int)comp.curveType;
                    data.floats[(int)JsonUIFloatType.colorTintOnEnableDuration] = comp.duration;
                }
                if (each.GetComponent<ColorTintOnDisable>())
                {
                    var comp = each.GetComponent<ColorTintOnDisable>();
                    data.customFlags |= (int)UIExportFlag.ColorTintOnDisable;
                    data.colorTintOnDisableStart = new float[] { comp.startColor.r, comp.startColor.g, comp.startColor.b, comp.startColor.a };
                    data.colorTintOnDisableEnd = new float[] { comp.endColor.r, comp.endColor.g, comp.endColor.b, comp.endColor.a };
                    data.colorTintOnDisableCurveType = (int)comp.curveType;
                    data.floats[(int)JsonUIFloatType.colorTintOnDisableDuration] = comp.duration;
                }
                if (each.GetComponent<LinearClipOnEnable>())
                {
                    var comp = each.GetComponent<LinearClipOnEnable>();
                    data.customFlags2 |= (int)UIExportFlag2.LinearClipOnEnable;
                    data.linearClipOnEnableStart = new float[] { comp.startPos.x, comp.startPos.y };
                    data.linearClipOnEnableDir = new float[] { comp.clipDirection.x, comp.clipDirection.y };
                    data.floats[(int)JsonUIFloatType.linearClipOnEnableDuration] = comp.clipDuration;
                    data.linearClipOnEnableCurveType = (int)comp.curveType;
                }
                if (each.GetComponent<LinearClipOnDisable>())
                {
                    var comp = each.GetComponent<LinearClipOnDisable>();
                    data.customFlags2 |= (int)UIExportFlag2.LinearClipOnDisable;
                    data.linearClipOnDisableStart = new float[] { comp.startPos.x, comp.startPos.y };
                    data.linearClipOnDisableDir = new float[] { comp.clipDirection.x, comp.clipDirection.y };
                    data.floats[(int)JsonUIFloatType.linearClipOnDisableDuration] = comp.clipDuration;
                    data.linearClipOnDisableCurveType = (int)comp.curveType;
                }
                if (each.GetComponent<IsDuplicatable>())
                {
                    var comp = each.GetComponent<IsDuplicatable>();
                    data.customFlags2 |= (int)UIExportFlag2.Duplicatable;
                    data.duplicate_poolable = comp.poolable;
                }
                if (each.GetComponent<BarCells>())
                {
                    var comp = each.GetComponent<BarCells>();
                    data.customFlags2 |= (int)UIExportFlag2.IsBarCells;
                    data.floats[(int)JsonUIFloatType.barCells_BarWidth] = comp.gaugeFill.rectTransform.sizeDelta.x;
                    data.floats[(int)JsonUIFloatType.barCells_BarHeight] = comp.gaugeFill.rectTransform.sizeDelta.y;
                    data.barCells_CellNumber = comp.cellCount;
                    data.floats[(int)JsonUIFloatType.barCells_GaugePerCell] = comp.gaugePerCell;
                }
                if (each.GetComponent<AdjustLinearClip>())
                {
                    var comp = each.GetComponent<AdjustLinearClip>();
                    data.customFlags2 |= (int)UIExportFlag2.AdjustLinearClip;
                    data.floats[(int)JsonUIFloatType.adjustLinearClipDirectionX] = comp.clipDirection.x;
                    data.floats[(int)JsonUIFloatType.adjustLinearClipDirectionY] = comp.clipDirection.y;
                    data.floats[(int)JsonUIFloatType.adjustLinearClipStartX] = comp.clipStart.x;
                    data.floats[(int)JsonUIFloatType.adjustLinearClipStartY] = comp.clipStart.y;
                    data.floats[(int)JsonUIFloatType.adjustLinearClipAdjustingRate] = comp.adjustingRate;
                }
                if (each.GetComponent<UIPlayMusicOnEnable>())
                {
                    var comp = each.GetComponent<UIPlayMusicOnEnable>();
                    data.customFlags2 |= (int)UIExportFlag2.MusicPlayOnEnable;
                    data.floats[(int)JsonUIFloatType.musicPlayOnEnable_fadeIn] = comp.fadeInTime;
                    data.floats[(int)JsonUIFloatType.musicPlayOnEnable_fadeOut] = comp.fadeOutTime;
                    if (comp.clip != null)
                    {
                        data.musicPlayOnEnable_musicClip = AssetDatabase.GetAssetPath(comp.clip);
                        data.musicPlayOnEnable_musicClip = data.musicPlayOnEnable_musicClip.Replace("Assets/Resources/", "");
                    }
                }
                if (each.GetComponent<UIPlayMusicOnDisable>())
                {
                    var comp = each.GetComponent<UIPlayMusicOnDisable>();
                    data.customFlags2 |= (int)UIExportFlag2.MusicPlayOnDisable;
                    data.floats[(int)JsonUIFloatType.musicPlayOnDisable_fadeIn] = comp.fadeInTime;
                    data.floats[(int)JsonUIFloatType.musicPlayOnDisable_fadeOut] = comp.fadeOutTime;
                    if (comp.clip != null)
                    {
                        data.musicPlayOnDisable_musicClip = AssetDatabase.GetAssetPath(comp.clip);
                        data.musicPlayOnDisable_musicClip = data.musicPlayOnDisable_musicClip.Replace("Assets/Resources/", "");
                    }
                }
                if (each.GetComponent<MusicUnpauseOnDisable>())
                {
                    data.customFlags2 |= (int)UIExportFlag2.MusicUnpauseOnDisable;
                }
                if (each.GetComponent<MusicPauseOnEnable>())
                {
                    data.customFlags2 |= (int)UIExportFlag2.MusicPauseOnEnable;
                }
                if (each.GetComponent<MusicMultiplyVolumeOnEnableDisable>())
                {
                    var comp = each.GetComponent<MusicMultiplyVolumeOnEnableDisable>();
                    data.customFlags2 |= (int)UIExportFlag2.MusicMultiplyVolumeOnEnableDisable;
                    data.floats[(int)JsonUIFloatType.musicMultiplyVolumeOnEnableDisable_enableFactor] = comp.enableFactor;
                    data.floats[(int)JsonUIFloatType.musicMultiplyVolumeOnEnableDisable_fadeDuration] = comp.fadeDuration;
                }
                //        ExclusiveEnable = 1 << 10,
                //DisableAfterEnable = 1 << 11,
                //Dialogue_Manual = 1 << 12,
                //Dialogue_Timed = 1 << 13,
                //RedundantEnable = 1 << 14,
                if (each.GetComponent<ExclusiveEnable>())
                {
                    var comp = each.GetComponent<ExclusiveEnable>();
                    data.customFlags2 |= (int)UIExportFlag2.ExclusiveEnable;
                    data.exclusiveEnableGroup = comp.exclusiveGroup.Select(each => uiIDDictanary[each]).ToArray();
                }
                if (each.GetComponent<DisableAfterEnable>())
                {
                    var comp = each.GetComponent<DisableAfterEnable>();
                    data.customFlags2 |= (int)UIExportFlag2.DisableAfterEnable;
                    data.floats[(int)JsonUIFloatType.disableAfterEnable_delayUntilDisable] = comp.delayUntilDisable;
                }
                if (each.GetComponent<Dialogue_Manual>())
                {
                    var comp = each.GetComponent<Dialogue_Manual>();
                    if (foundManualDialogues.Contains(comp.gameObject.name))
                    {
                        Debug.LogError("Dialogue_Manual이 중복됩니다! : ", each);
                        failedAlready = true;
                        break;
                    }
                    foundManualDialogues.Add(comp.gameObject.name);
                    data.customFlags2 |= (int)UIExportFlag2.Dialogue_Manual;
                }
                if (each.GetComponent<Dialogue_Timed>())
                {
                    var comp = each.GetComponent<Dialogue_Timed>();
                    if (foundTimedDialogues.Contains(comp.gameObject.name))
                    {
                        Debug.LogError("Dialogue_Timed이 중복됩니다! : ", each);
                        failedAlready = true;
                        break;
                    }
                    foundTimedDialogues.Add(comp.gameObject.name);
                    data.customFlags2 |= (int)UIExportFlag2.Dialogue_Timed;
                }
                if (each.GetComponent<ScriptUI>())
                {
                    var comp = each.GetComponent<ScriptUI>();
                    if (foundScriptUI.Contains(comp.gameObject.name))
                    {
                        Debug.LogError("ScriptUI의 이름이 중복됩니다! : ", each);
                        failedAlready = true;
                        break;
                    }
                    foundScriptUI.Add(comp.gameObject.name);
                    data.customFlags2 |= (int)UIExportFlag2.ScriptUI;
                }
                if (each.GetComponent<SoundEnumComp>() && each.GetComponent<SoundEnumComp>().soundEnumID != SoundEnumComp.SoundEnumID.None)
                {
                    var comp = each.GetComponent<SoundEnumComp>();
                    if (foundSounds.Contains(comp.soundEnumID))
                    {
                        Debug.LogError("sound id가 중복됩니다! : ", each);
                        failedAlready = true;
                        break;
                    }
                    foundSounds.Add(comp.soundEnumID);
                    data.soundEnumID = (int)comp.soundEnumID;
                    data.customFlags2 |= (int)UIExportFlag2.EnumSound;
                }
                if (each.GetComponent<RedundantEnable>())
                {
                    var comp = each.GetComponent<RedundantEnable>();
                    data.customFlags2 |= (int)UIExportFlag2.RedundantEnable;
                }
                if (each.GetComponent<RotatingUI>())
                {
                    var comp = each.GetComponent<RotatingUI>();
                    data.customFlags2 |= (int)UIExportFlag2.Rotating;
                    data.floats[(int)JsonUIFloatType.rotatingSpeed] = comp.rotateSpeed;
                    data.floats[(int)JsonUIFloatType.rotatingInitialRotation] = comp.transform.localRotation.eulerAngles.z;
                }
                if (each.GetComponent<StartGameButton>())
                {
                    var comp = each.GetComponent<StartGameButton>();
                    data.customFlags2 |= (int)UIExportFlag2.StartGameButton;
                }
                if (each.GetComponent<ReturnToTitleButton>())
                {
                    var comp = each.GetComponent<ReturnToTitleButton>();
                    data.customFlags2 |= (int)UIExportFlag2.ReturnToTitleButton;
                }
                if (each.GetComponent<Video>())
                {
                    var comp = each.GetComponent<Video>();
                    data.customFlags2 |= (int)UIExportFlag2.Video;
                    data.floats[(int)JsonUIFloatType.videoDuration1] = comp.videoDuration1;
                    data.videoUnscaledDeltaTime = comp.isDeltatimeUnscaled;
                    if (comp.video1 != null)
                    {
                        data.videoPath1 = AssetDatabase.GetAssetPath(comp.video1);
                        data.videoPath1 = data.videoPath1.Replace("Assets/Resources/", "");
                    }
                    if (comp.video2 != null)
                    {
                        data.videoPath2 = AssetDatabase.GetAssetPath(comp.video2);
                        data.videoPath2 = data.videoPath2.Replace("Assets/Resources/", "");
                    }
                }
                if (each.GetComponent<AnimatedSprite>())
                {
                    var comp = each.GetComponent<AnimatedSprite>();
                    data.customFlags2 |= (int)UIExportFlag2.AnimatedSprite;
                    data.animatedSpriteFolderPath = AssetDatabase.GetAssetPath(comp.Sprite);
                    data.animatedSpriteFolderPath = Path.GetDirectoryName(data.animatedSpriteFolderPath);
                    data.animatedSpriteFolderPath = data.animatedSpriteFolderPath.Replace("\\", "/");
                    data.animatedSpriteFolderPath = data.animatedSpriteFolderPath.Replace("Assets/Resources/", "");
                    data.animatedSpriteIsRepeat = comp.repeat;
                    Debug.Log(data.animatedSpriteFolderPath);
                }
                if (each.GetComponent<MultiLanguageImage>())
                {
                    var comp = each.GetComponent<MultiLanguageImage>();
                    data.customFlags2 |= (int)UIExportFlag2.MultiLanguage;
                    data.koreanImagePath = AssetDatabase.GetAssetPath(comp.koreanImg);
                    data.koreanImagePath = data.koreanImagePath.Replace("Assets/Resources/", "");
                    data.englishImagePath = AssetDatabase.GetAssetPath(comp.englishImg);
                    data.englishImagePath = data.englishImagePath.Replace("Assets/Resources/", "");
                    data.japaneseImagePath = AssetDatabase.GetAssetPath(comp.japaneseImg);
                    data.japaneseImagePath = data.japaneseImagePath.Replace("Assets/Resources/", "");
                }
                if (each.GetComponent<UIEnumIDComp>())
                {
                    var id = each.GetComponent<UIEnumIDComp>().uiEnumID;
                    data.enumID = (int)id;

                    if (id != UIEnumIDComp.UIEnumID.None && foundUIIds.Contains(id))
                    {
                        Debug.LogError("UI Enum ID가 다음 객체에서 중복됩니다! : ", each);
                    }
                    var parentUI = each.transform.parent;
                    bool skip = false;
                    while (parentUI)
                    {
                        if (parentUI.GetComponent<IsDuplicatable>())
                        {
                            skip = true;
                            break;
                        }
                        parentUI = parentUI.transform.parent;
                    }
                    if (!skip)
                    {
                        foundUIIds.Add(id);
                    }
                }
                uilist.dataList.Add(data);
            }
            catch (Exception e)
            {
                Debug.LogError(e.Message, each);
                failedAlready = true;
                break;
            }
        }
        if (!failedAlready)
        {
            jsonString = JsonUtility.ToJson(uilist, true);
            System.IO.File.WriteAllText(path, jsonString);
        }
    }
    private static void DFS(GameObject gameObject, string tagName, List<GameObject> list, Dictionary<GameObject, int> uiIDDict)
    {
        if (gameObject.tag == tagName)
        {
            list.Add(gameObject);
            uiIDDict.Add(gameObject, uiIDDict.Count);
        }
        foreach (Transform each in gameObject.transform)
        {
            DFS(each.gameObject, tagName, list, uiIDDict);
        }
    }
}

 

 

 

 

 

8. 성과

 

8.1. 게임 플레이 영상

 

8.2. 개발 과정 영상

 

8.3. 도쿄 게임 쇼

Tokyo Game Show 2024 행사에 게임이 전시된 모습

 

9. 느낀 점

 팀원이 지속가능한 코드를 개발할 수 있는 역량이 있는지 냉정하게 판단하는 것이 중요하다. 기술적 문제를 자존심 문제와 별개로 생각해야 한다. 타인의 기분을 배려해야 한다는 명분으로 필요한 판단을 제때 내리지 못하면 이는 나중에 더 큰 재앙이 되어 돌아온다.

 내 말과 권위에 주눅드는 사람, 순응하는 사람보다는 나에게 따지고 들고, 추궁하는 사람이 낫다. 프로젝트에 진지하게 임하려는 사람만이 미주알 고주알을 따진다.

  일에 가시적인 성과가 보이지 않는 채로 프로젝트 기간이 늘어지면 모두가 지치고, 지친 팀원들은 유지보수가능한 코드를 짤 수 없다. 유지보수가 힘든 코드가 쌓이다 보면 결국 프로젝트 자체가 돈좌된다. 사람의 근성과 열정은 유한하다. 팀원의 역량을 객관적으로 파악하고, 공세종말점을 잘 계산한 다음 일정을 세워야 한다.

 항상 정신적인 여유를 가지고 판단력을 유지해야 한다. 운동 + 독서 + 명상 + 개발이 순수 개발보다 효율이 더 좋다. 마음이 욕심, 분노, 어리석음으로 가득 차 있었기 떄문에 일을 그르칠 뻔 했던 적이 한두번이 아니다.

 분할 컴파일, 분할 테스팅에 대한 이해가 확실히 필요하다. 매번 게임을 테스트할 때마다 어마어마한 빌드 시간 + 로딩 시간을 거치니 정신이 나태해지고 업무 효율이 떨어진다.

 테스트 자동화는 테스트케이스들도 계속 업데이트해줘야 하는 단점이 극명하게 있다.

 서양에서의 마음챙김 명상회에서는 필요한 4가지 규율을 RAIN이라는 약어로 가르친다고 한다. 규율은 다음과 같다.

1. Recognition

2. Acceptance

3. Investigation

- Body

- Feelings

- Mind

- Dharma

4. Non-identification

 

 우리말로 옮기자면 다음과 같겠다.

1. 인지

2. 수용

3. 탐구

- 몸

- 느낌

- 마음

- 법(다르마)

4. 비동일시

 

1.인지

 인지는 우리가 어떤 인생의 교착 상태에 빠져 있다면 정확히 어떤 문제에 빠져 있는지 직시할 용기와 성의가 필요하다는 점을 말한다. 우리가 일을 있는 그대로 보려는 의지가 있어야 자기기만의 굴레에서 벗어날 수 있다. 당뇨병 환자가 본인의 질환을 부정하거나, 경제적 문제가 있는 사람이 자신의 과소비 성향을 무시하는 것, 사회가 가난과 불의의 문제를 모른척하는 것 모두 상황을 있는 그대로 인지할 의지가 없는 것에서 비롯된다. 만약 내가 마음속에 있는 불만족감, 분노, 괴로움, 야심이 끼치는 영향을 직시하지 못하고 나의 가치, 신념, 소망, 선함을 알아차리지 못한다면 나는 반드시 괴로움에 빠질 것이다.

 우리가 인지를 잘 다듬을 수 있다면 알아차림은 마음의 친절한 주인이 될 수 있다. '아, 슬픔이구나, 그리고 이건 흥분이구나, 갈등이 왔구나, 긴장이 일어났구나, 고통이 찾아왔구나, 이번에는 분별심이 일어났구나.' 이렇게 인지력을 잘 닦아 놓으면 어떤 감정이든 친절하게 마음의 손님으로 맞이할 수 있다는 것이다. 인지는 수행자를 망상과 무지로부터 벗어나 자유에 이르게 한다.

 

2. 수용

 수용은 주어진 상황 앞에 이완된 채로 열려 있을 수 있는 자세를 말한다. 수용의 자세가 없다면 괴로운 감정을 인지해도 저항과 혐오가 뒤따라 올 수 있다. 수용은 현재의 상황에 안주하겠다는 수동적인 자세가 아니라 지금의 상황과 환경을 있는 그대로 이해하겠다는 마음가짐이다. 마음과 관계맺는 법을 변화시키기 위한 용기이다.

 있는 그대로 사물을 받아들이는 수용과 존중의 마음을 기를 수 있다면 당최 감당할 수 없던 난제들도 활로가 트이곤 한다. 어떤 사람은 자신이 기르는 개에게 건강에 좋은 생선 기름을 먹이기 위해 매일 개와 씨름하며 억지로 개를 고정시키고 입을 벌려 기름을 흘려넣었다고 한다. 어느날 하루는 개가 몸부림을 치다가 생선 기름을 바닥에 떨어뜨리고 도망을 쳤는데, 놀랍게도 그 개는 나중에 돌아와 자의로 땅바닥의 기름을 핥았다. 개는 사실 기름이 싫었던 것이 아니라 기름을 먹이려는 주인의 강압적인 방식이 싫었던 것이다. 우리의 마음을 다스리는 것도 비슷할 수 있다. 불만족감, 분노, 괴로움은 그 자체로 괴로운 것이 아니라 이를 경멸하거나 강압적으로 다스리려는 우리의 태도 때문에 괴로운 것일 수 있다는 것이다. 수용과 존중의 태도는 우리가 감정들과 관계맺는 방식을 극적으로 뒤바꿔 놓을 수 있다.

 

3. 탐구

 틱낫한 스님은 탐구의 규율을 '깊이 봄(Seeing Deeply)'이라고 불렀다고 한다. 인지와 수용의 마음가짐으로 우리는 경험을 회피하지 않고 있는 그대로 직면할 수 있다. 탐구의 규율은 이렇게 담아낸 경험을 더 깊이, 더 세세하게 관찰해보라는 것이다. 경험을 관찰할 때는 다음 네가지 기반, 몸, 감정, 마음, 법(다르마)를 살펴보라고 한다.

 

- 몸

 먼저 내 몸에 무슨 일이 일어나고 있는지 알아차릴 필요가 있다. 스트레스 상황은  근육의 경직, 경련, 열감, 욱신거림, 저림, 두통 등 다양한 형태로 몸에 영향을 줄 수 있다. 개인적으로 하루종일 업무에 시간을 들여도 작은 일 하나도 이루지 못할 때가 종종 있었는데, 그 때 가끔씩 마음챙김 명상을 해보면 내 감정문제가 상반신 전체를 아우르는 열감, 위장의 팽창감과 복근의 긴장, 두통과 어지러움, 목 위의 경련으로 드러나는 것을 확인할 수 있었다. 몸이 이런 긴장 상태에 있어서야 한 줄의 코드도 짤 수 없는 것이 당연했다. 이런 신체적인 신호들에 내가 알아차림과 수용의 자세로 열려 있는지, 아니면 강압적인 방식으로 신체적인 증상들을 억누르려 하는지 확인해야한다. 그리고 이런 반응들을 수용할 때 신체적 증상들이 심화되는지, 전이되는지, 확장하는지, 축소하는지, 반복되는지, 사라지는지, 아니면 다른 증상으로 변하진 않는지 관찰할 필요가 있다.

 

- 감정

 다음으로 어떤 감정들이 올라와 있는지 관찰해야 한다. 대체적으로 즐거운 감정인가? 아니면 불쾌한 감정인가? 그도 아니면 중립적인가? 올라오는 감정들에 그저 반응하고 있는가, 아니면 알아차림의 자세로 접근하고 있는가? 핵심 감정에 따라오는 부차적인 감정들은 없는가? 보통 감정들은 별자리와 같이 무리를 지어 등장한다. 이혼을 회상하는 돌싱남의 경우 슬픔, 분노, 질투, 상실감, 두려움, 외로움이 한묶음으로 떠올랐다고 한다. 조카의 마약중독 문제를 제때 도와주지 못한 여자의 경우 갈망, 혐오감, 죄책감, 공허함, 무가치함이 같이 올라왔다고 한다. 마음챙김의 힘을 잘 닦아 놓은 수행자라면 이런 감정들을 각각 제대로 인지하고 수용할 수 있다. 그리고 각 감정이 신체의 어느 부위에서 드러나는지, 그리고 알아차림을 통해 감정을 가만히 관조했을 때 감정의 흐름에 어떤 변화가 생기는지 관찰할 수 있다.

 

- 마음

 마음을 관찰하면 어떤 생각과 이미지들이 아른거리고 있는지 알 수 있다. 우리가 어떤 서사에 혼이 팔렸는지, 무슨 잣대로 어떻게 남을 판단하고 있는지, 어떤 믿음하에 행동하고 있는지 자각할 수 있다는 것이다. 여기서 더 자세히 들여다 보면 우리가 가진 관념 중 어떤 것이 특히 편향적이고 경직되어 있는지, 얼마나 습관적으로 생각하는지 확인할 수 있다. 그리고 여기서 더 나아가면 이런 것들은 그저 익숙한 이야기에 불과할 뿐, 굳이 이것들로 스스로를 옭아맬 필요가 없다는 것을 볼 수 있게 된다.

 

- 법, 다르마, 도

 다르마는 여러가지 뜻을 품을 수 있는 단어라고 한다. 다르마라는 말 하나가 진실, 불교의 가르침, 현상의 뒤에 숨은 원리라는 다양한 뜻을 포괄할 수 있다. 불교 수행자의 측면에서 바라봤을 때 이 규율은 불교의 무상과 무아의 가르침에 비추어 스스로에게 질문을 던지라고 말하는 것 같다. 내가 관찰하고 있는 경험이 정말 체감되는것만큼이나 정적인가? (Is the experience actually as solid as it appears?) 이 경험이 정말로 불변하는 성질을 갖고 있는가, 아니면 계속 형태를 바꾸는가? 이 경험은 내 통제하에 있는가, 아니면 스스로의 주기에 따라 저절로 생멸하는가? 경험이 밖에서 왔는가, 내 안에서 만들어졌는가? 내가 경험과 관계맺는 방법이 내 괴로움에 기여하는가, 행복에 기여하는가? 그리고, 내가 이 경험과 나를 동일시하고 있는가?

 

4. 비동일시

 비동일시는 경험을 곧 '나', 혹은 '내 것'이라고 간주하지 말라는 것이다. 동일시의 경험을 관찰하면 이것이 어떻게 의존심, 불안, 집착, 괴리감을 일으키는지 확인할 수 있다. 비동일시의 규율을 몸에 익히는 것은 어렵지 않다. 몸과 마음의 모든 상태에, 나의 모든 경험에, 내가 스스로에게 들려주는 모든 이야기에 다음과 같은 의문을 가지는 것이다. '이것을 진정한 나라고 할 수 있는가?'. 애초에 경험을 취사선택하여 굳혀낸 우리의 정체성 자체가 지극히 임의적이라는 것을 깨달으면 동일시에서 비롯된 괴로움으로부터 자유로워질 수 있다.(We see the tentativeness of this identity. Then we are free to let go and rest in awareness itself. This is the culmination of releasing difficulty throguh RAIN.) 

 

' > The Wise Heart' 카테고리의 다른 글

[The wise heart] How symbols work  (0) 2025.02.01

PBR

yunu95
|2024. 4. 5. 19:55

 

 매끈매끈한 강철 공을 띄우고 , 이 공이 주변 환경 이미지를 반사하는 모습을 확인했다.

 PBR에 쓰이는 세부적인 공식들을 모두 이해하지는 못했지만, PBR은 다음과 같은 요소들을 중시한다는 점만 일단 새겨두면 될 것 같다.

 

1. 표면에 반사되는 굴절되는 빛의 합은 일정해야 한다. 즉, 에너지 보존 법칙을 고려해야 한다.

2. 빛의 반사율은 시선벡터가 표면의 법선 방향과 둔각을 이룰수록 높아진다는 프레넬 반사를 고려해야 한다.

3. PBR은 까끌까끌한 미세면의 존재를 의식하며, 국소면의 까끌까끌한 정도를 roughness라는 수치로 나타낸다. 표면은 roughness가 낮을수록(매끈매끈할수록) 정반사하는 성질이 생긴다.

 

 장면이 너무 허해서 큐브맵을 적용해 보았다. 확실히 태가 좀 살고 코딩할 맛이 더 난다.

 하늘에서 내리쬐는 상수 디렉셔널 라이트를 만들어서 diffuse 색상과 N dot L 연산만 실행시켜 보았다. 지금은 탄젠트 스페이스에서 정의된 노멀 값을 그대로 가져다 썼기 때문에 제대로 된 노말 연산은 되지 않는 모습이다. 각 픽셀별 노말 값과 노말 텍스처의 값을 조합하여 제대로 된 N dot L 연산을 해본 다음 PBR 공식을 가져와 렌더링을 해봐야겠다.

 

 모자의 머리통 부분에 계단 현상이 생기는게 아쉽다. 디퍼드 렌더링 방식은 안티얼라이싱 적용이 힘든 구조라고 하던데, 이건 먼 훗날에 개선해봐야겠다.

'개발의 편린' 카테고리의 다른 글

PBR  (0) 2024.04.05
Deferred Rendering에 대한 첫번째 시도  (0) 2024.03.15
맵 에디터 개발의 흔적  (0) 2024.01.31
게임 오브젝트 갯수 메모리 스트레스 테스트  (0) 2024.01.18

 

 디퍼드 렌더링은 물체들에 대한 정보들을 여러 장의 텍스처에 저장해놓고 마지막으로 출력물을 계산할 때 각 텍스처로부터 정보를 종합하여 빛 연산을 진행하는 렌더링 기법이다. 디퍼드 렌더링을 더 깎기 전에 물체 표면의 정보를 저장하는 g버퍼들의 정보를 항상 화면 상단에 띄워 확인할 수 있도록 만들었다. g버퍼들의 정보는 왼쪽에서 오른쪽으로 차례로 각 픽셀들의 위치, 노멀 방향, 알베도 색상, ambient occlusion, roughness, metallic 정보를 rgb 값으로 저장한 ARM 정보에 해당한다. 지금은 빛 연산은 배제하고 최종 출력값으로는 알베도 색상 값만 출력하게 했다.

테스트 시연

 

피직스 충돌 기반 피킹

각 패널 기능 설계