Pantropy ― Massive Performance Boost by Using NetworkArrays Photon Bolt
이 기사는 인디 게임 스튜디오 Brain Stone에서 Pantropy의 주임 개발자인 Julian Kaulitzki 씨에 의한 이야기 입니다.
Pantropy는 파벌 싸움을 베이스로한 PC용 SF 메카 게임입니다. 이 기사를 쓰고 있는 시점에서는 클로즈드 알파버전이 공개되어 있습니다. Pantropy는, 하나의 조인트 세션으로 128 플레이어까지 허용 가능할 것으로 예상됩니다. 플레이에서는 외계인의 세계 정복, 탄광 발굴, 기지의 건축 등을 즐길 수 있습니다. 또한 무기나 이동 수단 등의 아이템을 제작하여 다른 파벌 플레이어 및 AI로 제어중인 집단으로부터 공격을 막을 수도 있습니다.
이 게임의 더 자세한 정보는 Pantropy Kick-starter Campaign 을 확인해 주세요.
Pre α버전에서의 게임 퍼포먼스 문제
우리는 2016년 후반에 플레이어가 게임의 여러 기능을 테스트 할 수 있게끔 Pre α 버전을 공개했습니다. 기능의 하나로써 토대, 벽, 바닥, 발전기, 용광로 등 다양한 파트를 사용해 기지를 구축하는 기능이 포함되어 있습니다. Pre α 버전에서는 서버와 클라이언트의 퍼포먼스가 서서히 저하되는 것을 알 수 있었고 때때로 퍼포먼스가 너무 저하되어 서버를 재기동 할 필요가 있을 정도였습니다.
디버깅을 해 본 결과, Frame rate가 서버 상에서 액티브한 BoltEntity의 양과 비례한다는 것을 알게되었습니다. 그렇기 때문에 예를 들어 10명의 플레이어가 온라인에 있을 경우 각 플레이어의 기지에 포함되는 파트 수가 100개 이상 존재하고, 각 파트에 BoltEntity가 붙어있을 경우에는 결과적으로 동시에 액티브화되는 BoltEntity가 1,000이상이 되는 것입니다. 굉장이 많은 숫자죠.
(간략한 면책 사항: 이 디버깅 때에는 scoping/freezing/idling/priority calculation은 사용하지 않았습니다.)
솔루션
게임 플레이와 기지의 사이즈에 영향을 주지 않으면서, 더욱 적은 BoltEntity 로 가동하는 시스템이 필요했습니다. 기지의 구축에 사용하는 파트는 크게 2개의 그룹으로 나뉩니다:
수동 파트 ― 이 파트들은 네트워크상에서 다음 값만을 동기화 합니다. :위치, 회전, 구조 통합성, 건강, (health), PrefabID
파트 예: 토대, 기둥, 벽, 바닥 등
능동 파트 ― 이 파트들은 상기 이외의 값도 동기화 할 필요가 있습니다. 발전기, 용광로, 보관함, 공구 장비 등의 파트는 아이템, 전기 관련 등 그 외의 값도 보유하기 때문입니다.
기지의 95%는 수동 파트로 만들어지고 이 수동 파트의 동기에 필요한 정보는 아주 적기 때문에, 우리는 이 데이터를 개별 BoltEntity가 아닌 NetworkArray_Objects에 보존하기로 했습니다.
NetworkArray_Object는 스테이트 상에서 BoltObjects를 배열로 저장하는 Bolt의 클래스입니다.
이 경우 NetworkArray는 BaseElement_Obj 타입의 BoltObject를 저장합니다.
각 BoltObject는 기지의 수동 파트를 표현하기 때문에 BaseElement_Obj에는 아래의 값이 저장됩니다.
Position (Vector3) - World Space내에서의 BasePart 위치.
Rotation (Quaternion) - World Space에서의 BasePart 방향.
Structural Integrity (float) -이 파트의 안정성을 표시합니다.
Health (int) - 이 파트가 어느정도의 건강(health)를 보유하고 있는지를 표시합니다.
PrefabID (int) - 흔히 PrefabID라 불리는 Bolt에 구축되어 있는 구조체와 혼동하지 않도록 주의가 필요합니다. 이 값은 파트의 종류를 식별하는데에 사용딥니다.
IsSpawned (bool) - NetworkArray의 엔트리가 미사용 중인지를 판별합니다.
Bolt는 각 스테이트에서의 최대 프로퍼티 수를 1,024로 제한하기 때문에 배열의 최대 엔트리 수는 170이 됩니다.
BaseElementsManager는 Bolt의 EntityBehavior에서 이어지는 스크립트입니다. 이 스크립트는 자신의 스테이트에 "BaseElements"라 불리는 프로퍼티를 소유합니다. 이 프로퍼티는 BaseElement_Obj 타입의
NetworkArray입니다(상기 참조).
아래는 중요한 사항입니다:
이것의 중요한 기능은 수동 파트의 데이터를 보관하고 이 데이터의 프록시를 처리하는 것입니다.
프록시는 BoltEntity를 갖지 않는 순수한 GameObject로, NetworkArray에서의 엔트리를 표시합니다.
그렇기 때문에 "IsSpawned = True"라고 마크된 모든 엔트리에는 플레이어가 상호 작용할 수 있는 세계에서의 GameObject(프록시)가 존재합니다. 이 경우 프록시는 기지의 파트입니다.
기지는 하나 이상의 BaseElementsManager로 만들어지기 때문에 모두를 관리하는 별도의 스크립트가 필요합니다. BaseManager스크립트는 수동 파트를 추가/삭제/변경하는 의뢰를 처리하고, 기지의BaseElementsManager를 관리합니다. 예를 들어 기지에 새로운 파트를 추가하고 싶은데 그 기지에 이미 170개의 수동 파트가 있을 경우, 현재의 BaseElementsManager의 NetworkArray는 모두 채워져 있기 때문에 새로운 BaseElementsManager가 작성되어 새 파트의 데이터를 보관하는데 사용됩니다.
또한 NetworkArray "BaseElements"가 변경되기 때문에 BaseElementsManage 스크립트 내의 콜 백 OnBaseElementsStateArrayChanged가 호출됩니다. 이어서, 콜백 내에서 변경된 엔트리의 arrayIndex를 전달함으로써 HandleBaseElement 메소드가 호출됩니다. 그리고 변경된 엔트리가 "IsSpawned = True"라고 마크되었는지 HandleBaseElement 메소드가 확인하여, 마크된 경우에는 그 엔트리에 대해 프록시가 이미 존재하는가를 확인합니다. 엔트리에 프록시가 없는 경우에는 프록시가 생성됩니다.
단, 그 엔트리가 "IsSpawned = False"로 마크되고 엔트리에 프록시가 있는 경우에는 그 프록시는 삭제됩니다.。
private void OnBaseElementsStateArrayChanged(IState s, string path, ArrayIndices indices)
{
int ArrayIndex = indices[0];
HandleBaseElement(ArrayIndex);
}
private void HandleBaseElement(int arrayIndex)
{
// state.BaseElements is the NetworkArray of type BaseElement_Obj
BaseElement_Obj element_Obj = state.BaseElements[arrayIndex];
if(element_Obj.IsSpawned)
{
// ProxyBaseElements is an array of type BaseElement with a length equivalent to the NetworkArray
if (ProxyBaseElements[arrayIndex] == null)
SpawnBaseElementProxy(element_Obj, arrayIndex);
}else
{
if (ProxyBaseElements[arrayIndex] != null)
RemoveBaseElementProxy(ProxyBaseElements[arrayIndex], arrayIndex);
}
}
엔트리의 값이 변경되었을 때는 항상 NetworkArray에의 콜백이 호출되는 점에 주의해 주십시오.
NetworkArray내의 BaseElement_Obj의 건강(health)가 변경된 경우에는 콜백이 호출됩니다.
힌트
메모리 누수를 막기 위해, 프록시는 풀링(pool)되어 있습니다. 각 BaseElementsManager상에서 실행되는 루틴에서는 플레이어가 가까이에 있는지를 확인합니다. 플레이어가 가까이 있을 경우에는 프록시를 생성할 필요가 있고 플레이어가 가까이 없을 경우에는 이미 생성된 프록시가 Pool 상태로 돌아와 다른 기지가 그 프록시를 사용할 수 있게 됩니다.
새로운 파트가 추가/삭제된 경우에는 항상 모든 수동 파트의 평균 위치가 계산되어 기지의 중심점이 산출된 후, BaseManager와 BaseElementsManagers가 그 중심점까지 이동됩니다. 결과적으로 BaseManager와 BaseElementsManagers 의 위치를 scoping/freezing/idling/priority calculation에 사용할 수 있게 됩니다.
결론
scoping/freezing/idling/priority calculation을 하면서 프록시를 사용함으로써 수 천개의 BoltEntity를 관리하는 것 보다 효율적으로, 용이하게 NetworkArray내에 보관된 데이터를 표시할 수 있습니다.
이 시스템을 이용하여 수동 파트의 BoltEntity 총 수를 10진수로 1700에서 10까지 줄일 수가 있게 되었습니다!
또한, 이 시스템은 게임에서 사용되는 광석에도 도움이 되었습니다. 게임 내 세계에는 5,000개의 광석이 분산되어 있고 각 광석은 원래 개별의 BoltEntity 였습니다. 현재는 15개의 OreNodesManager를 사용하여 각 OreNodesManager에는 하나의 BoltEntity가 부속되어 있어 340개의 광석 노드를 관리하고 있습니다.
이 시스템으로 하나의 씨앗으로 전 세계에 나무를 분산시키는 것도 가능합니다. 또 하나의 BoltEntity로 500그루의 나무를 관리하는 것도 가능합니다.
프록시 시스템에 의해 얼마나 퍼포먼스가 향상되었는지 수치로 알려드리겠습니다.
하나의 예로 3,380개의 파트로 이루어진 정육면체를 작성하기 위한 간단한 스크립트를 기술했습니다.
크기는 세로13×가로13×높이20입니다.
처음에 실행한 테스트에서는 각 파트에 BoltEntity가 붙어있었습니다. 각 엔티티는 액티브한 상태로, 인액티브나 프리징하지 않는 상태입니다. 결과적으로, 오버헤드는 Bolt의 BoltPoll.FixedUpdate 루프에 의해 32.20ms로 나타났습니다 (아래 스크린샷을 참조).
2번째 테스트는 프록시 시스템을 이용하여 실행했습니다.
이 시스템에는 25개의 BaseElementsManagers가 있고, 각각의 BaseElementsManager는 140개의 프록시를 관리하고 있어 정육면체는 불과 25개의 BoltEntity로 만들어집니다.
각 엔티티는 액티브한 상태로, 인액티브나 프리징하지 않는 상태입니다. 결과적으로, Bolt의 BoltPoll.FixedUpdate 루프는 0.69ms로 나타났습니다(이하 스크린 샷 참조).
상기의 결과물인 게임을 참고하고 싶으신 분, 궁금한 점이 있으신 분은 Kickstarter를 확인해 주세요:
https://www.kickstarter.com/projects/2124684123/pantropy?ref=card
파벌간의 슈팅을 벌이는 SF 메카 멀티플레이어 게임입니다. 게임 내에서 건축 및 제작을 실행할 수 있는 것이 특징입니다.
출처 : Photon Blog by ExitGames (독일) |