원래는
NativeParrallelMultiHashMap.AsParallelWriter()를 써보자!
였는데 글자수가 부족해서 잘렸다...
일단 왜 이런 타입을 쓰게 됐는지 살펴보자...
이것은 내 게임이다.
마운트 & 블레이드에서 아쉬웠던 점 중 하나는 당시 우리 집의 컴퓨터의 빈약한 성능으로는 고작해야 200대200 싸움 밖에 구경 못했다는 것이다.
그래서 우리 엄마의 8년 묵은 LG그램으로도 수천 규모의 전투가 돌아가는 마앤블 라이크 게임을 만들려고 하는데,
공격 판정<= 이건 DOTS의 무지막지한 최적화 능력으로도 다루기 어렵다.
여기서 간단한 수학인데, 6000명의 유닛이 있다고 치자.
3000 vs 3000의 구도는 1vs 5999보다 훨씬 혼란스럽다.
컴퓨터 관점에서 생각하면, 3000vs3000에서의 연산 부하는 1vs 1800만이나 마찬가지다. 유닛 하나하나가 상대방 유닛의 위치가 자신의 공격 범위에 있는지 확인하고 공격할지 말지 결정한다고 치면, 6000개의 유닛은 상대편 3000개의 유닛을 "모두 고려하기" 때문이다.
진짜 그랬다간 cpu가 정상화 당하므로 최적화를 해야 한다.
-------------------------------------------------------------------------------------------------------
첫번째 고려할 만한 것은 모든 유닛 주위에 trigger 충돌 판정을 만드는 것이다.
적 유닛이 이 구역에 들어오면 이 충돌 판정은 trigger 이벤트를 불러일으킨다.
굉장히 좋은 방법인데, 문제는 6000개 유닛의 trigger충돌 박스를 유지한다고 쳐도, 매 프레임마다 계속 울려대는 triggerevent가 천개는 넘는 다는 것이다.
공격 범위에 적이 있다 치면, 공격하는 시간과 멍 때리는 시간의 비율은 8:2~9:1 정도로 예상하고 있다. 그리고 공격을 하는 동안에는 이 trigger충돌 박스를 꺼버리고 싶어할 것이다.
근데 이 충돌 박스를 껐다 키고 하는 것이 유니티 dots의 물리 담당인 PhysicsWorld에 상당한 스트레스를 준다.
그냥 켜두고 triggerEvent를 무시하면 되지 않음? 맞다. 하지만 그것은 우리가 이벤트 처리에서 걸러낸다는 것이지 충돌이 안 일어난다는 것은 아니다! 알람 소리를 무음으로 한다고 해서 알람이 안 울리는 것이 아니기 때문이다.
두번쨰로 고려할 만한 것은 spherecasting이다.
raycasting이라는 거리와 방향을 확인하는 레이저빔을 사방으로 쏴서 특정 구역을 탐지하는 기술이다. 각 유닛이 적 탐지를 해야하는 타이밍에, 적재적소에서 이걸 쓰게 하는 것이다.
문제는 spherecast는 unity dots의 물리 담당 api인 unity physics에서 지원해주지 않고 있다. 물론 이걸 직접 구현을 할 수 있겠지만 그러면 최적화를 장담할 수 없다.
세번째는 모종의 방식으로 후보군을 좁히고 그 중에서 단순 거리 비교를 하는 것이다.
나는 그 중에 Spatial hashing이라는 방식을 사용했다.
맵을 체스판처럼 가르면, 유닛들은 저절로 위치에 따라 특정 칸 위에 서게 된다.
그럼 각 유닛은 공격 범위 내에 적이 있는지 확인하려고 할 때, 더이상 상대편 모든 유닛들의 위치가 아닌, 자신이 서 있는 칸과 주변 칸에 있는 적 유닛들의 위치만 알면 된다. 그러기 위해서는 모든 적의 위치를 이 지도에 등록하는 것이 필요하다. 바로 여기서 NativeParrallelMultiHashMap이 나서야 한다.
보자마자 뇌가 녹아버릴 거 같은 작명 센스다.
기능적으로 보자면 Native | Parrallel | Multi | HashMap 4가지 요소라고 볼 수 있다.
Native는 이 타입이 NativeContainer라는, 유니티 DOTS의 UnManaged 타입의 자료 저장 방식이라는 것을 의미한다. 일반적인 C#의 리스트, 딕셔너리 같은 GC(가비지 컬렉션, 쓰레기 집합소가 아니라 안쓰는 메모리를 정리하는 것)가 관리(Managed)하는 데이터 타입을 유니티 DOTS환경에서는 사용할 수 없다. Native를 앞에 붙여서 이건 DOTS-friendly라는 것을 강조하는 것이다.
Parrallel는 문자 그대로 병렬 처리가 가능하다는 것이다. 이 데이터는 병렬 처리 중인 Job에 던져주면, 수많은 cpu코어가 달려들어서 서로 싸우지 않고 잘 나눠 먹는다.
Multi라는 것은 이 해시맵은 일반적인 해시맵처럼 Key-Value 쌍이 일부일처제가 아니라 일부다처제라는 것이다. 즉 하나의 key에 여러 값이 들어갈 수 있다.
HashMap은 키가 주어지면 그 키에 맞는 값을 매우 빠르게 반환해준다.
즉 NativeParrallelMultiHashMap은, 유니티 DOTS환경에서 쓸 수 있는, 병렬 처리에 특화된, 키-값 쌍이 1대 다가 될 수 있는 해시맵 자료구조다.
자 그러면 이제 유닛들의 위치 정보를 이 해시맵에 등록하면 된다.
위 그림처럼 지도를 체스판으로 만들었지만, 실제로는 체스판 같은 오브젝트를 만들지 않을 것이다.
최적화라는 것은 자진으로 컴퓨터의 기쁨조가 되겠다는 것과 마찬가지기에 인간 다운 생각으로는 불가능하며, 현실 세계에서 탈인간급의 기행에서 그 영감을 얻을 수 있다. 현실에서 멀쩡하게 체스판에서 체스를 두는 사람이 있는가하면, 체스판 없이 서로 좌표만 얘기하면서 체스를 두는 미친 사람들이 있다. 하지만 컴퓨터 관점에서는 그게 올바른 코딩법이다.
병렬 job에서 각 유닛의 localTransform을 얻어서, math.floor로 정수형으로 맞추고, 나온 값을 int2에 넣으면, x와 y는 곧 가로 세로 1유닛의 x열y행의 칸으로 취급할 수 있다.
이 칸을 Key값으로 저 HashMap에 넣으면, 체스판 오브젝트 생성 필요 없이 사람이 읽을 수 있는 key-값으로 이루어진 해시맵이 만들어진다.
NativeParrallelMultiHashMap.AsParallelWriter()를 Job에 넘겨주면, 멀티스레드에서 병렬 처리로 더욱 빨리 맵에 등록이 된다. 그러면 이제 다음 시스템으로 넘어가서, idle또는 move상태의 유닛들은 이 맵에서 자신 주위에 어떤 적 유닛들이 있는지 알 수 있다. 이것은 읽기만 하기에 병렬 처리에는 그다지 어렵지 않지만, nativeContainer의 특수한 api로 인해 NativeParallelMultiHashMapIterator<int2>라는 해시맵의 키 값을 순회하는 용도의 변수를 사용해야 한다.
해시맵의 출력 메세지. 13행30열key에 엔티티 번호가 입력됐고, 그 뒤에 같은 key에 다른 엔티티들이 입력됐다.
미리 주의를 하자면 이런 수천개의 엔티티가 있는 씬에서는 Debug.Log를 쓸 때 먼저 엔티티들을 수십 사이즈로 줄이고 하는 것을 권장한다.
unity.physics에 bvh 있던데 써보면 어떨까 - dc App
bvh의 기능을 한정적으로 쓰는 대신 가볍게 하려고 하는건데, 사격이나 마법 같은 걸 구현할 때는 bvh가 가벼울 지도? 한번 비교해봐야겠음
테스트해봤는데 큰 차이는 없는 거 같음. 다만 실제 넉백 적용, 대미지 적용에서 코드 구조가 상당히 다르고, bvh의 aabboverlap을 하면 코드가 한 스크립트 파일에 주르륵 있고, spatial hash를 쓰면 한 단계 한 단계가 따로 분리 됨. 가독성 면에서는 spatial hash가 나은데, 확장성은 bvh가 나아보임. colliderfilter를 잘 정리해놨으면 bvh, 필터 생각하기 싫고 태그 컴포넌트로만 데이터 쿼리하고 싶으면 spatial hash. 그냥 내 개인적인 소감. 사람마다 다를 수도 있음.
bvh가 좀 더 다양한 충돌 방식에 적합해서 나는 bvh로 리팩토링 했음. 방향 제시해줘서 감사감사