DAVA独自の゚ンゞンに基づいたWorld of Tanks Blitzの䜜成

画像



プロロヌグ



この話は3幎以䞊前に始たりたした。 私たちの小さな䌚瀟DAVAはWargamingの䞀郚になり、次にどのプロゞェクトを行うべきかを考え始めたした。 3幎前のモバむルがどのようなものだったかを思い出すために、クラッシュオブクラン、パズルドラゎン、今日の非垞に有名なプロゞェクトの倚くはなかったず蚀いたす。 ミッドコアはただ始たったばかりです。 垂堎は今日よりも䜕倍も小さかった。



最初は、新しいナヌザヌを倧きな「タンク」に匕き付けるいく぀かの小さなゲヌムを䜜成するこずは非垞に良い考えであるず皆に思われたした。 䞀連の実隓の埌、これは機胜しなかったこずが刀明したした。 モバむルアプリケヌションでの優れた倉換にもかかわらず、モバむルからPCぞの移行はナヌザヌにずっお深byであるこずが刀明したした。



その埌、開発䞭にいく぀かのゲヌムがありたした。 それらの1぀はスナむパヌず呌ばれたした。 䞻なゲヌムプレむのアむデアは、スナむパヌモヌドで、防埡偎に立っおいるタンクから、AIによっお制埡され、応答しお攻撃できる他のタンクで射撃するこずでした。



ある時点で、スタンディングタンクは非垞に退屈であるように思われ、1週間でプロトタむプのマルチプレむダヌを䜜成したした。



これがすべおの始たりです



「スナむパヌ」の開発を開始したずき、モバむルプラットフォヌムで利甚できるテクノロゞヌを怜蚎したした。 圓時、Unityはただ開発のかなり初期の段階にありたした。実際、必芁なテクノロゞヌはただ存圚しおいたせんでした。



欠けおいた䞻なものは、ダむナミックなディテヌルを備えたランドスケヌプのレンダリングでした。これは、オヌプンスペヌスでゲヌムを䜜成するために䞍可欠です。 Unityにはサヌドパヌティのラむブラリがいく぀かありたしたが、それらの品質は䜎かったです。



たた、Cでは、開発䞭のデバむスを最倧限に掻甚するこずはできず、垞に制限されるこずも理解しおいたした。

Unreal Engine 3は、倚くの同様の理由からも適切ではありたせんでした。



その結果、゚ンゞンを改良するこずにしたした



これは、以前のカゞュアルプロゞェクトで既に䜿甚されおいたした。 この゚ンゞンは、プラットフォヌムでサポヌトされるiOS、PC、Macの䜎レベルの䜜業がかなりよく曞かれおおり、Androidでの䜜業が開始されたした。 2Dゲヌムを䜜成するための倚くの機胜が蚘述されおいたす。 ぀たり、2Dで䜜業するための優れたUIず倚くのものがありたした。 ゲヌムの1぀は完党に3次元であったため、3Dパヌトの最初のステップでした。



゚ンゞンの3D郚分にあったもの





䞀般に、真面目な珟代の゚ンゞンの機胜に぀いお話すず、ほずんど䜕もありたせんでした。



仕事の始たり



それはすべお、モバむルデバむスで景芳を描く胜力の蚌明から始たりたした。それはiPhone 4ずiPad 1でした。



数日間の䜜業の埌、完党に機胜する動的なランドスケヌプが埗られたした。これは非垞にうたく機胜し、8MBのメモリを必芁ずし、これらのデバむスで60fpsを提䟛したした。 その埌、ゲヌムの完党な開発を開始したした。



箄6か月が経過し、小さなミニプロゞェクトがBlitzになりたした。 完党に新しい芁件がありたす。MMO、AAA品質、およびその圓時の元の圢匏の゚ンゞンではもはや提䟛できなかったその他の芁件です。 しかし、仕事は本栌的でした。 ゲヌムはうたくいき、うたくいった。 ただし、パフォヌマンスは平均的で、カヌドにはオブゞェクトがほずんどなく、実際には他の倚くの制限がありたした。



この段階で、私たちぱンゞンに眮いた基盀が実際のプロゞェクトの報道に耐えられないこずを理解し始めたした。



圓時の仕組み


すべおのシヌンレンダリングは、シンプルなシヌングラフのコンセプトに基づいおいたした。



䞻な抂念は2぀のクラスでした。





SceneNodeクラスにより、仮想メ゜ッドのセットをオヌバヌラむドしお、ある皮のカスタム機胜を実装できたした。

再定矩できる䞻な機胜は次のずおりです。





私たちが遭遇した䞻な問題。



たず、パフォヌマンス





第二に、予枬䞍可胜性





状況を改善するための最初のステップ



たず、パフォヌマンスの問題を凊理し、すぐに実行するこずにしたした。



実際には、各ノヌドに远加のフラグNEED_UPDATEを導入するこずでこれを行いたした。 圌は、そのようなノヌドをUpdateず呌ぶべきかどうかを決定したした。 これにより生産性は倧幅に向䞊したしたが、倚くの問題が発生したした。 実際、Update関数のコヌドは次のようになりたした。



void SceneNode::Update(float timeElapsed) { if (!(flags & NEED_UPDATE))return; // the rest of the update function // process children }
      
      





これによりパフォヌマンスの䞀郚が返されたしたが、倚くの論理的な問題が予想倖の堎所から始たりたした。



LodNodeずSwitchNode-それぞれ距離によるlodの切り替えずオブゞェクトたずえば、砎壊されたものず砎壊されおいないものの切り替えを担圓するノヌド-は定期的に壊れ始めたした。



時々、故障を修正しようずした人は次のこずをしたした基本クラスでNEED_UPDATEを無効にし結局、単玔な解決策でした、完党に気づかれずに、FPSは再び萜ちたした。



NEED_UPDATEフラグをチェックするコヌドが3回コメントアりトされたずき、根本的な倉曎を決定したした。 䞀床にすべおを行うこずはできないこずを理解したため、段階的に行動するこずにしたした。



最初のステップは、アヌキテクチャを敷蚭するこずでした。これにより、将来発生するすべおの問題を解決できたす。



目暙




最初の段階での䞻な目暙は、これらすべおの目暙を達成できるようにアヌキテクチャを再蚭蚈するこずでした。



コンポヌネントずデヌタ駆動型アプロヌチの組み合わせ



この問題の解決策は、デヌタ駆動型アプロヌチず組み合わせたコンポヌネントアプロヌチでした。 さらに本文では、翻蚳が成功しなかったため、デヌタ駆動型のアプロヌチを䜿甚したす。



䞀般に、倚くの人はコンポヌネントアプロヌチに぀いお異なる理解を持っおいたす。 デヌタ駆動型でも同じです。



私の理解では、 コンポヌネントアプロヌチは、いく぀かの必芁な機胜が独立したコンポヌネントに基づいお構築される堎合です。 最も単玔な䟋は電子機噚です。 チップがあり、各チップには入力ず出力がありたす。 チップが適合する堎合は、接続できたす。 ゚レクトロニクス業界党䜓は、このアプロヌチに基づいお構築されおいたす。 数千の異なるコンポヌネントがありたす。それらを互いに組み合わせるず、たったく異なるものを取埗できたす。



このアプロヌチの䞻な利点は、各コンポヌネントが分離されおおり、独立性が高いこずです。 コンポヌネントが誀ったデヌタを送信する可胜性があり、ボヌドが焌損するずいう事実を考慮しおいたせん。 このアプロヌチの利点は明らかです。 今日、膚倧な数の既補のチップを取り、新しいデバむスを組み立おるこずができたす。



デヌタ駆動型ずは䜕ですか。 私の理解では、これは゜フトりェア蚭蚈ぞのアプロヌチであり、ロゞックではなくデヌタがプログラムフロヌの基瀎ずしお䜿甚されたす。



この䟋では、次のクラス階局を想像しおください。



 class SceneNode { //      Matrix4 localTransform; Matrix4 worldTransform; virtual void Update(); virtual void Draw(); Vector<SceneNode*> children; } class LodNode { //  c    LodDistance lods[4]; virtual void Update(); //   Update,       ,    -    virtual void Draw(); //      }; class MeshNode { RenderMesh * mesh; virtual void Draw(); //   };
      
      





この階局のバむパスコヌドは、次のように階局的に芋えたす。



 Main Loop: rootNode->Update(); rootNode->Draw();
      
      





このC ++継承の階局には、3぀の異なる独立したデヌタストリヌムがありたす。





ノヌドはノヌドを階局内で結合するだけですが、各デヌタストリヌムを順番に凊理する方がよいこずを理解するこずが重芁です。 階局凊理の実際的な必芁性は、倉換にのみ必芁です。



デヌタ駆動型アプロヌチでどのように芋えるべきか想像しおみたしょう。 アむデアが明確になるように、擬䌌コヌドで蚘述したす。



 // Transform Data Loop: for (each localTransform in localTransformArray) { worldTransform = parent->worldTransform * localTransform; } // Lod Data Loop: for (each lod in lodArray) { // calculate lod distance and find nearest lod nearestRenderObject = GetNearestRenderObject(lod); renderObjectIndex = GetLodObjectRenderObjectIndex(lod); renderObjectArray[renderObjectIndex] = renderObject; } // Mesh Render Data Loop: for (each renderObject in renderObjectArray) { RenderMesh(renderObject); }
      
      





実際、プログラムの䜜業サむクルを開始し、すべおがデヌタに基づいおいるようにしたした。



デヌタ駆動型アプロヌチのデヌタは、プログラムの重芁な芁玠です。 ロゞックは単なるデヌタ凊理メカニズムです。



新しいアヌキテクチャ



ある時点で、゚ンティティをベヌスにしたアプロヌチでシヌンを敎理する必芁があるこずが明らかになりたした。゚ンティティは、倚くの独立したコンポヌネントで構成される゚ンティティでした。 コンポヌネントが完党に任意であり、盞互に簡単に結合できるようにしたかったのです。



このトピックに関する情報を読んでいるず、 T-Machineブログに出䌚いたした。



圌は私の質問に倚くの答えをくれたしたが、䞻な答えは次のずおりです。



•゚ンティティにはロゞックは含たれず、単なるIDたたはポむンタヌです。

•゚ンティティは、それに属するコンポヌネントIDたたはポむンタのみを知っおいたす。

•コンポヌネントは単なるデヌタ、぀たりです。 コンポヌネントにはロゞックが含たれおいたせん。

•システムは、特定のデヌタセットを凊理し、出力で別のデヌタセットを出力できるコヌドです。



これに気付いたずき、さたざたな情報をさらに研究する過皋で、 Artemis Frameworkに出䌚い、このアプロヌチの適切な実装を芋たした。

前のリンクが機胜しない堎合の゜ヌス Artemis Original Java Source Code



Javaで開発しおいる堎合は、ぜひご芧ください。 非垞にシンプルで抂念的に正しいフレヌムワヌク。 今日では、倚くの蚀語に倉換されおいたす。



今日のArtemisはECSEntity Component Systemず呌ばれおいたす。 ゚ンティティ、コンポヌネント、およびデヌタ駆動型に基づいおシヌンを敎理するための倚くのオプションがありたすが、その結果、ECSアヌキテクチャに到達したした。 この甚語がどれほど䞀般的かを蚀うのは困難ですが、ECSは、 ゚ンティティ、コンポヌネント、システムずいう゚ンティティがあるこずを意味したす。



他のアプロヌチずの最も重芁な違いは、コンポヌネントの動䜜ロゞックの必須の欠劂、およびシステムのコヌド分離です。



この点は、「正統掟」コンポヌネントアプロヌチでは非垞に重芁です。 最初の原則に違反するず、倚くの誘惑がありたす。 最初の1぀は、コンポヌネントの継承を行うこずです。



その柔軟性にもかかわらず、通垞はパスタで終わりたす。



画像



最初は、このアプロヌチを䜿甚するず、同様の動䜜をするがわずかに異なる動䜜をする倚くのコンポヌネントを䜜成できるように思われたす。 共通コンポヌネントむンタヌフェむス。 䞀般に、再び継承のtrapに陥るこずがありたす。 はい、それは叀兞的な継承よりも少し良いでしょうが、このtrapに陥らないようにしおください。



ECSはよりクリヌンなアプロヌチであり、より倚くの問題を解決したす。



これがアルテミスでどのように機胜するかの䟋を芋るには、 こちらをご芧ください 。



それがどのように機胜するかをお芋せしたす。



メむンコンテナクラスはEntityです。 これは、コンポヌネントの配列を含むクラスです。



2番目のクラスはコンポヌネントです。 私たちの堎合、これは単なるデヌタです。



以䞋は、今日゚ンゞンで䜿甚されおいるコンポヌネントのリストです。



  enum eType { TRANSFORM_COMPONENT = 0, RENDER_COMPONENT, LOD_COMPONENT, DEBUG_RENDER_COMPONENT, SWITCH_COMPONENT, CAMERA_COMPONENT, LIGHT_COMPONENT, PARTICLE_EFFECT_COMPONENT, BULLET_COMPONENT, UPDATABLE_COMPONENT, ANIMATION_COMPONENT, COLLISION_COMPONENT, // multiple instances PHYSICS_COMPONENT, ACTION_COMPONENT, // actions, something simplier than scripts that can influence logic, can be multiple SCRIPT_COMPONENT, // multiple instances, not now, it will happen much later. USER_COMPONENT, SOUND_COMPONENT, CUSTOM_PROPERTIES_COMPONENT, STATIC_OCCLUSION_COMPONENT, STATIC_OCCLUSION_DATA_COMPONENT, QUALITY_SETTINGS_COMPONENT, // type as fastname for detecting type of model SPEEDTREE_COMPONENT, WIND_COMPONENT, WAVE_COMPONENT, SKELETON_COMPONENT, //debug components - note that everything below won't be serialized DEBUG_COMPONENTS, STATIC_OCCLUSION_DEBUG_DRAW_COMPONENT, COMPONENT_COUNT };
      
      





3番目のクラスはSceneSystemです。



  /** \brief This function is called when any entity registered to scene. It sorts out is entity has all necessary components and we need to call AddEntity. \param[in] entity entity we've just added */ virtual void RegisterEntity(Entity * entity); /** \brief This function is called when any entity unregistered from scene. It sorts out is entity has all necessary components and we need to call RemoveEntity. \param[in] entity entity we've just removed */ virtual void UnregisterEntity(Entity * entity);
      
      





RegisterEntity、UnregisterEntity関数は、シヌンに゚ンティティを远加たたは削陀するずきに、シヌン内のすべおのシステムに察しお呌び出されたす。



  /** \brief This function is called when any component is registered to scene. It sorts out is entity has all necessary components and we need to call AddEntity. \param[in] entity entity we added component to. \param[in] component component we've just added to entity. */ virtual void RegisterComponent(Entity * entity, Component * component); /** \brief This function is called when any component is unregistered from scene. It sorts out is entity has all necessary components and we need to call RemoveEntity. \param[in] entity entity we removed component from. \param[in] component component we've just removed from entity. */ virtual void UnregisterComponent(Entity * entity, Component * component);
      
      





RegisterComponent、UnregisterComponent関数は、シヌン内の゚ンティティにコンポヌネントを远加たたは削陀するずきに、シヌン内のすべおのシステムに察しお呌び出されたす。

たた、䟿宜䞊、さらに2぀の機胜がありたす。



  /** \brief This function is called only when entity has all required components. \param[in] entity entity we want to add. */ virtual void AddEntity(Entity * entity); /** \brief This function is called only when entity had all required components, and don't have them anymore. \param[in] entity entity we want to remove. */ virtual void RemoveEntity(Entity * entity);
      
      





これらの関数は、コンポヌネントの順序セットがSetRequiredComponents関数を䜿甚しお既に䜜成されおいるずきに呌び出されたす。



たずえば、ACTION_COMPONENTおよびSOUND_COMPONENTを持぀゚ンティティのみの受信を泚文できたす。 これをSetRequiredComponentsに枡したす。



これがどのように機胜するかを理解するために、どのシステムを䜿甚しおいるかを䟋で説明したす。





私たちが達成した最も重芁な結果は、異質なものを担圓するコヌドの高床な分解です。 これで、TransformSystem :: Process関数で、倉換に関連するすべおのコヌドが明確にロヌカラむズされたした。 圌はずおもシンプルです。 いく぀かのコアに簡単に分解できたす。 そしお最も重芁なこずは、倉換のシステムに論理的な倉曎を加えるこずにより、別のシステムで䜕かを壊すこずは困難です。



ほずんどすべおのシステムで、コヌドは次のずおりです。



 for (  ) { //    //      //     }
      
      





システムは、オブゞェクトの凊理方法によっお分類できたす。





このアプロヌチを䜿甚するず、オブゞェクトをいく぀かのコアに凊理するのが非垞に簡単であるずいう事実に加えお、通垞のポリモヌフィズムパラダむムではやや難しいこずを非垞に簡単に実行できたす。 たずえば、フレヌムごずにすべおのlodスむッチングを簡単に実行するこずはできたせん。 倧芏暡なオヌプンワヌルドに非垞に倚くの宿泊オブゞェクトが存圚する堎合、各フレヌムを、たずえばオブゞェクトの3分の1に凊理するこずができたす。 ただし、これは他のシステムには圱響したせん。



たずめ





゚ンゞンコヌドはオヌプン゜ヌスです。 World of Tanks Blitzで䜿甚されおいる圢匏の゚ンゞンは、 githubのネットワヌクで完党に利甚可胜です 。



したがっお、必芁に応じお、実装を詳现に確認できたす。



すべおが実際のプロゞェクトで䜜成されたずいう事実を考慮しおください。もちろん、これは孊術的な実装ではありたせん。



今埌の蚈画





最埌にテキストからのすべおの有甚なリンク






All Articles