World of tanks game engine. BigWorld Engine - Game engines - Files for game makers - Game creation. How everything worked at that time

  • Genre focus: 3D MMO of any genre;
  • Platform: PC, PS3, Xbox 360, iOS (iPad), Web;
  • Programming language: C++, Python;
  • License: indie and commercial;
  • Open Source: not provided or provided at increased cost;
  • Multiplayer: client-server;
  • Advantages: powerful, supports all the latest technologies, optimized, supports iOS, cheap for such capabilities;
  • Flaws: not provided free of charge;
  • Engine developers: BigWorld Tech, Inc.

    BigWorld Engine is the most advanced 3D engine for creating MMO games. Games such as "World of Tanks", "Pealm of the Titans" from Wargaming.net and other games from other global game development companies were made on it. More than 15 MMO games were made on this engine. It is being developed by BigWorld Technology.

    The optimization of the engine allows you to create low-demand games with stunning graphics. The engine allows you to port games to iOS. It is written in the C++ programming language, and the game logic is implemented in it using the convenient Python scripting language. There are powerful tools and a client-server engine. The FMOD library is supported for sound, and any other libraries can be connected via the plug-in system. Works with XML and MySQL databases. The toolkit includes the powerful World Editor, Model Editor and Particle Editor.

    It is very affordable. BigWorld: Indie Edition costs just $299 to build; BigWorld: Indie Source Edition - $2,999; BigWorld: Commercial Edition – negotiated individually.

    This advanced engine is not inferior in capabilities to other global engines of its type. The engine is available in Russian, Korean, American and Japanese. There is documentation, it can work on browsers. In general, you can start working if you have the knowledge and diligence.

    No longer available for third party licensing because... Wargaming has decided to stop distributing its engine.

    Official site: http://www.bigworldtech.com





    The BigWorld Technology tool chain provides a complete, end-to-end MMOG content creation system that will enhance the quality and timeliness of your game. All tools are designed for cooperative production of game assets in a large team environment, ensuring effective use of resources and a smooth content pipeline.

  • It's time to buy new video cards or the announcement of a new WOT engine.

    The release of completely new tanks was announced at WG Fest. In fact, this is a completely different game, since new engine, sounds, HD maps. Despite the visual improvements, the developers promise that performance will not drop. If you are currently playing at 30 FPS, then it will be the same on the new engine.

    So far only one trailer is available:

    What is known so far?

    • The update will be released in March 2018.

    • A new engine called Core. (the current WOT engine) is slowly becoming a thing of the past.
      • Of the existing engines, none could satisfy the developers, so it was decided to do everything from scratch ourselves.
      • It took 4 years to develop: 3 years for the engine itself and another year for creating content (maps).
    • To evaluate performance, you can now download special software enCore, which will conduct a test, based on the results of which you can evaluate how suitable your PC is for the new game.
      • enCore can be run in three graphics modes: minimal, medium and ultra.
      • The test lasts three minutes and shows a replay of a staged battle demonstrating new graphical capabilities and effects.


    enCore World of Tanks test result.

    • HD maps - all locations have been converted to high definition. And this applies to all aspects: landscape, textures, lighting, sounds, effects, environment, etc.


    Full list of maps converted to HD.

    • The soundtrack has been completely rewritten. Each map will now have its own music, conveying the unique atmosphere of the location.
    • They promise that the work on optimization has been done well and there should not be a significant loss of FPS.
    This story began more than three years ago. Our small company DAVA became part of Wargaming, and we began to think about what projects to do next. To remind you what mobile was like three years ago, I will say that then there was no Clash Of Clans, no Puzzle & Dragons, or many very well-known projects today. Mid-core was just beginning then. The market was several times smaller than today.

    Initially, everyone thought that it would be a very good idea to make several small games that would attract new users to large “tanks”. After a series of experiments it turned out that this does not work. Despite excellent conversions in mobile applications, transfer from mobile phone to PC turned out to be an abyss for users.

    Back then we had several games in development. One of them had the working title “Sniper”. The main gameplay idea was shooting at sniper mode from a defensive tank, against other tanks controlled by AI and which could attack in response.

    At some point it seemed to us that standing tank- this is very boring, and within a week we made a multiplayer prototype, where tanks could already drive and attack each other.

    Since this all started!

    When we started developing Sniper, we looked at the technologies that were then available for mobile platforms. At that time, Unity was still at a fairly early stage of its development: in fact, the technologies we needed did not yet exist.

    The main thing we were missing was landscape rendering with dynamic detail, which is vital for creating a game with open spaces. There were several third-party libraries for Unity, but their quality left much to be desired.

    We also understood that in C# we would not be able to get the most out of the devices we were developing for, and would always be limited.
    Unreal Engine 3 was also not suitable for a number of similar reasons.

    As a result, we decided to improve our engine!

    At that time it had already been used in our previous casual projects. The engine had a fairly well-written low level of work with platforms and supported iOS, PC, Mac, plus work had begun on Android. A lot of functionality has been written for creating 2D games. That is, there was a good UI and a lot of things for working with 2D. It was the first steps into the 3D part, since one of our games was completely 3D.

    What we had in the 3D part of the engine:

    • The simplest scene graph.
    • Ability to draw static meshes.
    • Ability to draw animated meshes with skeletal animation.
    • Exporting objects and animations from Collada format.
    In general, if we talk about the functionality of a serious modern engine, it had very little.

    Beginning of work

    It all started with proving the ability to draw landscapes on mobile devices: then it was the iPhone 4 and iPad 1.

    After several days of work, we got a fully functional dynamic landscape that worked quite well, required about 8MB of memory and gave 60fps on these devices. After that, we began full development of the game.

    About six months passed, and the small mini-project turned into what Blitz is now. Completely new requirements appeared: MMO, AAA quality and other requirements that the engine in its original form could no longer provide at that time. But the work was in full swing. The game worked and worked well. However, performance was average, there were few objects on the maps, and, in fact, there were many other limitations.

    At this stage, we began to understand that the foundation that we had laid in the engine would not withstand the pressure of a real project.

    How everything worked at that time
    All scene rendering was based on a simple Scene Graph concept.

    The main concept was two classes:

    • Scene - the scene container within which all actions took place
    • above the stage.
    • SceneNode - the base class of the scene node, from which all classes that were in the scene were inherited:
    • MeshInstanceNode - class for drawing meshes.
    • LodNode - class for switching lods.
    • SwitchNode - class for switching switch objects.
    • about 15 more classes of SceneNode descendants.
    The SceneNode class allowed you to override the set virtual methods, to implement some custom functionality:
    The main functions that could be overridden are:
    • Update - a function that was called for each node in order to make an Update scene.
    • Draw is a function that was called for each node in order to draw this node.
    The main problems we faced.

    First, performance:

    • When the number of nodes in the level reached 5000, it turned out that simply going through all the empty Update functions took about 3ms.
    • Similar time was spent on empty nodes that did not require Draw.
    • A huge number of cache misses, since the work was always carried out with different types of data.
    • Inability to parallelize work across multiple cores.
    Secondly, unpredictability:
    • Changing the code in the base classes affected the entire system, that is, every change to SceneNode::Update could break anything, anywhere. The dependencies became more and more complex, and every change within the engine was almost guaranteed to require testing of all related functionality.
    • It was impossible to make a local change, for example in transformations, without affecting the rest of the scene. Very often, the slightest changes in the LodNode (node ​​for switching lods) broke something in the game.

    First steps to improve the situation

    To begin with, we decided to fix the performance problems and do it quickly.

    Actually, we did this by introducing an additional NEED_UPDATE flag in each node. It determined whether such a node needed to call Update. This did improve productivity, but it created a whole bunch of problems. In fact, the code for the Update function looked like this:

    Void SceneNode::Update(float timeElapsed) ( if (!(flags & NEED_UPDATE))return; // the rest of the update function // process children )
    This returned some of our productivity, but a lot of logical problems began where they were not expected.

    LodNode and SwitchNode - nodes responsible, respectively, for switching lods (by distance) and switching objects (for example, destroyed and undestroyed) - began to break down regularly.

    From time to time, those who tried to fix the problems did the following: they disabled NEED_UPDATE in the base class (after all, it was a simple solution), and completely unnoticeably FPS dropped again.

    When the code checking the NEED_UPDATE flag was commented out three times, we decided to make radical changes. We understood that we would not be able to do everything at once, so we decided to act in stages.

    The very first step was to lay down an architecture that would allow us to solve all the problems that arise in the future.

    Goals
    • Minimizing dependencies between independent subsystems.
    • Changes in transformations should not break the lod system, and vice versa
    • Possibility to put code on multi-core.
    • So that there are no Update or similar functions in which heterogeneous independent code is executed. Easy system expandability with new functionality without completely retesting the old one. Changes in some subsystems do not affect others. Maximum independence of subsystems.
    • Ability to arrange data linearly in memory for maximum performance.
    The main goal at the first stage was to redesign the architecture so that all these goals could be achieved.

    Combining component and data-driven approaches

    The solution to this problem was a component approach combined with a data-driven approach. Further in the text I will use a data-driven approach, since I have not found a successful translation.

    In general, many people have very different understandings of the component approach. The same goes for data-driven.

    In my mind, component approach- this is when some necessary functionality is built on the basis of independent components. The simplest example is electronics. There are chips, each chip has inputs and outputs. If the chips fit together, they can be connected. The entire electronics industry is built on this approach. There are thousands of different components: by combining them with each other, you can get completely different things.

    The main advantages of this approach are that each component is isolated and, to a greater extent, independent. I don't take into account the fact that you can feed the component the wrong data and the board will burn out. The advantages of this approach are obvious. Today you can take a huge number of ready-made chips and assemble a new device.

    What is it data-driven. In my understanding, this is a design approach software, when data rather than logic is taken as the basis for the flow of program execution.

    In our example, imagine the following class hierarchy:

    Class SceneNode ( // Data responsible for hierarchical transformations Matrix4 localTransform; Matrix4 worldTransform; virtual void Update(); virtual void Draw(); Vector children; ) class LodNode ( // Data specific for calculating lods LodDistance lods; virtual void Update(); // the Update method is overridden so that at the moment of switching lods, turn on or off some of its children virtual void Draw(); / / draw only the current active log ); class MeshNode ( RenderMesh * mesh; virtual void Draw(); // draw the mesh );
    The code for traversing this hierarchy hierarchically looks like this:

    Main Loop: rootNode->Update(); rootNode->Draw();
    In this C++ inheritance hierarchy we have three different independent data streams:

    • Transformations
    Nodes only combine them into a hierarchy, but it is important to understand that it is better to process each data stream sequentially. The practical need for processing by hierarchy is only needed for transformations.

    Let's imagine what this should look like in a data-driven approach. I’ll write it in pseudocode to make the idea clear:

    // 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 = renderObject; ) // Mesh Render Data Loop: for (each renderObject in renderObjectArray) ( RenderMesh(renderObject); )
    Essentially, we expanded the program's work cycles in such a way that everything was driven by data.

    Data in a data-driven approach is key element programs. Logic is just data processing mechanisms.

    New architecture

    At some point, it became clear that we needed to move towards an Entity-based approach to organizing the scene, where Entity was an entity consisting of many independent components. I wanted the components to be completely arbitrary and easily combined with each other.

    While reading about this topic, I came across the T-Machine blog.

    He gave me many answers to my questions, but the main answer was the following:

    Entity doesn't contain any logic, it's just an ID (or pointer).
    Entity only knows the ID of the components that belong to it (or a pointer).
    A component is just data, that is. the component does not contain any logic.
    A system is a code that can process a certain set of data and produce another set of data as output.

    If you develop in Java, I highly recommend checking it out. A very simple and conceptually correct Framework. Today it has been translated into a bunch of languages.

    What Artemis is today is called ECS (Entity Component System). There are quite a lot of options for organizing a scene based on Entity, components and data-driven, but in the end we came to the ECS architecture. It is difficult to say how generally accepted the term is, but ECS means that there are the following entities: Entity, Component, System.

    The most important difference from other approaches is: Mandatory lack of behavioral logic in components, and separation of code into systems.

    This point is very important in the “Orthodox” component approach. If you violate the first principle, a lot of temptations will appear. One of the first is to make component inheritance.

    Despite its flexibility, it usually ends in pasta.

    Initially, it seems that with this approach it will be possible to make many components that behave in a similar way, but slightly differently. Common component interfaces. In general, you can again fall into the inheritance trap. Yes, this will be slightly better than classical inheritance, but try not to fall into this trap.

    ECS is a cleaner approach and solves more problems.

    To see an example of how this works in Artemis, you can take a look.

    I will show you with an example how it works for us.

    The main container class is Entity. This is a class that contains an array of components.

    The second class is Component. In our case, it's just data.

    Here is a list of components used in our engine today:

    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_COM PONENT, // 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, W IND_COMPONENT, WAVE_COMPONENT, SKELETON_COMPONENT, / /debug components - note that everything below won't be serialized DEBUG_COMPONENTS, STATIC_OCCLUSION_DEBUG_DRAW_COMPONENT, COMPONENT_COUNT );
    The third class is 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 entity entity we"ve just added */ virtual void RegisterEntity(Entity * entity); /** \brief This function is called when any entity is unregistered from scene. It sorts out is entity has all necessary components and we need to call RemoveEntity. \param entity entity we"ve just removed */ virtual void UnregisterEntity(Entity * entity);
    The RegisterEntity and UnregisterEntity functions are called for all systems in the scene when we add or remove an Entity from the scene.

    /** \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 entity entity we added component to. \param 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 entity entity we removed component from. \param component component we"ve just removed from entity. */ virtual void UnregisterComponent(Entity * entity, Component * component);
    The RegisterComponent, UnregisterComponent functions are called for all systems in the scene when we add or remove a Component to an Entity in the scene.
    There are also two more functions for convenience:

    /** \brief This function is called only when entity has all required components. \param entity entity we want to add. */ virtual void AddEntity(Entity * entity); /** \brief This function is called only when the entity had all required components, and don"t have them anymore. \param entity entity we want to remove. */ virtual void RemoveEntity(Entity * entity);
    These functions are called when the ordered set of components has already been created using the SetRequiredComponents function.

    For example, we can order to receive only those Entities that have ACTION_COMPONENT and SOUND_COMPONENT. I pass this to SetRequiredComponents and voila.

    To understand how this works, I’ll give examples of what systems we have:

    • TransformSystem - a system that is responsible for the hierarchy of transformations.
    • SwitchSystem is a system that is responsible for switching objects that can be in several states, such as destroyed and undestroyed.
    • LodSystem - a system that is responsible for switching lods by distance.
    • ParticleEffectSystem - a system that updates particle effects.
    • RenderUpdateSystem - a system that updates render objects from the scene graph.
    • LightUpdateSystem - a system that updates light sources from the scene graph.
    • ActionUpdateSystem - a system that updates actions.
    • SoundUpdateSystem - a system that updates sounds, their position and orientation.
    • UpdateSystem - a system that causes custom user updates.
    • StaticOcclusionSystem - system for applying static occlusion.
    • StaticOcclusionBuildSystem - system for building static occlusion.
    • SpeedTreeUpdateSystem - Speed ​​Tree update system.
    • WindSystem - wind calculation system.
    • WaveSystem - a system for calculating vibrations from explosions.
    • FolliageSystem - system for calculating vegetation over the landscape.
    The most important result that we have achieved is a high decomposition of the code responsible for heterogeneous things. Now, in the TransformSystem::Process function, all the code that concerns transformations is clearly localized. It's very simple. It can be easily decomposed into several nuclei. And most importantly, it is difficult to break something in another system by making a logical change in the transformation system.

    On almost any system the code looks like this:

    For (a specific set of objects) ( // get the necessary components // perform actions on these objects // write data to the components )
    Systems can be classified by how they process objects:

    • Processing of all objects that are in the system is required:
      • Physics
      • Collisions
    • Only processing of marked objects is required:
      • Transformation system
      • Actions system
      • Sound processing system
      • Particle processing system
    • Working with your specially optimized data structure:
      • Static Occlusion System
    With this approach, in addition to the fact that it is very easy to process objects in several cores, it is very easy to do what is quite difficult to do in the usual polymorphism paradigm. For example, you can easily take and process not all lod switches per frame. If there are VERY many lod objects in a large open world, you can make sure that each frame, for example, a third of the objects are processed. However, this does not affect other systems.

    Bottom line

    • We greatly increased FPS because with the component approach things became more independent and we were able to decouple and optimize them individually.
    • The architecture has become simpler and more understandable.
    • It became easy to expand the engine without breaking neighboring systems.
    • There are fewer bugs from the series “by doing something to the LODs, the switches were broken,” and vice versa
    • It became possible to parallelize all this across several cores.
    • At the moment, we are already working to ensure that all systems run on all available cores.
    Our engine code is in Open Source. The engine as it is used in World of Tanks Blitz. This story began more than three years ago. Our small company DAVA became part of Wargaming, and we began to think about what projects to do next. To remind you what mobile was like three years ago, I will say that then there was no Clash Of Clans, no Puzzle & Dragons, or many very well-known projects today. Mid-core was just beginning then. The market was several times smaller than today.

    Initially, everyone thought that it would be a very good idea to make several small games that would attract new users to large “tanks”. After a series of experiments it turned out that this does not work. Despite excellent conversions in mobile applications, the transition from mobile to PC was a chasm for users.

    Back then we had several games in development. One of them had the working title “Sniper”. The main gameplay idea was to shoot in sniper mode from a defensive tank at other tanks controlled by AI and which could attack in response.

    At some point it seemed to us that a standing tank was very boring, and within a week we made a multiplayer prototype, where tanks could already drive and attack each other.

    Since this all started!

    When we started developing Sniper, we looked at the technologies that were then available for mobile platforms. At that time, Unity was still at a fairly early stage of its development: in fact, the technologies we needed did not yet exist.

    The main thing we were missing was landscape rendering with dynamic detail, which is vital for creating a game with open spaces. There were several third-party libraries for Unity, but their quality left much to be desired.

    We also understood that in C# we would not be able to get the most out of the devices we were developing for, and would always be limited.
    Unreal Engine 3 was also not suitable for a number of similar reasons.

    As a result, we decided to improve our engine!

    At that time it had already been used in our previous casual projects. The engine had a fairly well-written low level of work with platforms and supported iOS, PC, Mac, plus work had begun on Android. A lot of functionality has been written for creating 2D games. That is, there was a good UI and a lot of things for working with 2D. It was the first steps into the 3D part, since one of our games was completely 3D.

    What we had in the 3D part of the engine:

    • The simplest scene graph.
    • Ability to draw static meshes.
    • Ability to draw animated meshes with skeletal animation.
    • Exporting objects and animations from Collada format.
    In general, if we talk about the functionality of a serious modern engine, it had very little.

    Beginning of work

    It all started with proving the ability to draw landscapes on mobile devices: then it was the iPhone 4 and iPad 1.

    After several days of work, we got a fully functional dynamic landscape that worked quite well, required about 8MB of memory and gave 60fps on these devices. After that, we began full development of the game.

    About six months passed, and the small mini-project turned into what Blitz is now. Completely new requirements appeared: MMO, AAA quality and other requirements that the engine in its original form could no longer provide at that time. But the work was in full swing. The game worked and worked well. However, performance was average, there were few objects on the maps, and, in fact, there were many other limitations.

    At this stage, we began to understand that the foundation that we had laid in the engine would not withstand the pressure of a real project.

    How everything worked at that time
    All scene rendering was based on a simple Scene Graph concept.

    The main concept was two classes:

    • Scene - the scene container within which all actions took place
    • above the stage.
    • SceneNode - the base class of the scene node, from which all classes that were in the scene were inherited:
    • MeshInstanceNode - class for drawing meshes.
    • LodNode - class for switching lods.
    • SwitchNode - class for switching switch objects.
    • about 15 more classes of SceneNode descendants.
    The SceneNode class allowed you to override a set of virtual methods to implement some custom functionality:
    The main functions that could be overridden are:
    • Update - a function that was called for each node in order to make an Update scene.
    • Draw is a function that was called for each node in order to draw this node.
    The main problems we faced.

    First, performance:

    • When the number of nodes in the level reached 5000, it turned out that simply going through all the empty Update functions took about 3ms.
    • Similar time was spent on empty nodes that did not require Draw.
    • A huge number of cache misses, since the work was always carried out with different types of data.
    • Inability to parallelize work across multiple cores.
    Secondly, unpredictability:
    • Changing the code in the base classes affected the entire system, that is, every change to SceneNode::Update could break anything, anywhere. The dependencies became more and more complex, and every change within the engine was almost guaranteed to require testing of all related functionality.
    • It was impossible to make a local change, for example in transformations, without affecting the rest of the scene. Very often, the slightest changes in the LodNode (node ​​for switching lods) broke something in the game.

    First steps to improve the situation

    To begin with, we decided to fix the performance problems and do it quickly.

    Actually, we did this by introducing an additional NEED_UPDATE flag in each node. It determined whether such a node needed to call Update. This did improve productivity, but it created a whole bunch of problems. In fact, the code for the Update function looked like this:

    Void SceneNode::Update(float timeElapsed) ( if (!(flags & NEED_UPDATE))return; // the rest of the update function // process children )
    This returned some of our productivity, but a lot of logical problems began where they were not expected.

    LodNode and SwitchNode - nodes responsible, respectively, for switching lods (by distance) and switching objects (for example, destroyed and undestroyed) - began to break down regularly.

    From time to time, those who tried to fix the problems did the following: they disabled NEED_UPDATE in the base class (after all, it was a simple solution), and completely unnoticeably FPS dropped again.

    When the code checking the NEED_UPDATE flag was commented out three times, we decided to make radical changes. We understood that we would not be able to do everything at once, so we decided to act in stages.

    The very first step was to lay down an architecture that would allow us to solve all the problems that arise in the future.

    Goals
    • Minimizing dependencies between independent subsystems.
    • Changes in transformations should not break the lod system, and vice versa
    • Possibility to put code on multi-core.
    • So that there are no Update or similar functions in which heterogeneous independent code is executed. Easy system expandability with new functionality without completely retesting the old one. Changes in some subsystems do not affect others. Maximum independence of subsystems.
    • Ability to arrange data linearly in memory for maximum performance.
    The main goal at the first stage was to redesign the architecture so that all these goals could be achieved.

    Combining component and data-driven approaches

    The solution to this problem was a component approach combined with a data-driven approach. Further in the text I will use a data-driven approach, since I have not found a successful translation.

    In general, many people have very different understandings of the component approach. The same goes for data-driven.

    In my mind, component approach- this is when some necessary functionality is built on the basis of independent components. The simplest example is electronics. There are chips, each chip has inputs and outputs. If the chips fit together, they can be connected. The entire electronics industry is built on this approach. There are thousands of different components: by combining them with each other, you can get completely different things.

    The main advantages of this approach are that each component is isolated and, to a greater extent, independent. I don't take into account the fact that you can feed the component the wrong data and the board will burn out. The advantages of this approach are obvious. Today you can take a huge number of ready-made chips and assemble a new device.

    What is it data-driven. In my understanding, this is an approach to software design where data, rather than logic, is taken as the basis for the flow of a program.

    In our example, imagine the following class hierarchy:

    Class SceneNode ( // Data responsible for hierarchical transformations Matrix4 localTransform; Matrix4 worldTransform; virtual void Update(); virtual void Draw(); Vector children; ) class LodNode ( // Data specific for calculating lods LodDistance lods; virtual void Update(); // the Update method is overridden so that at the moment of switching lods, turn on or off some of its children virtual void Draw(); / / draw only the current active log ); class MeshNode ( RenderMesh * mesh; virtual void Draw(); // draw the mesh );
    The code for traversing this hierarchy hierarchically looks like this:

    Main Loop: rootNode->Update(); rootNode->Draw();
    In this C++ inheritance hierarchy we have three different independent data streams:

    • Transformations
    Nodes only combine them into a hierarchy, but it is important to understand that it is better to process each data stream sequentially. The practical need for processing by hierarchy is only needed for transformations.

    Let's imagine what this should look like in a data-driven approach. I’ll write it in pseudocode to make the idea clear:

    // 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 = renderObject; ) // Mesh Render Data Loop: for (each renderObject in renderObjectArray) ( RenderMesh(renderObject); )
    Essentially, we expanded the program's work cycles in such a way that everything was driven by data.

    Data in a data-driven approach is a key element of the program. Logic is just data processing mechanisms.

    New architecture

    At some point, it became clear that we needed to move towards an Entity-based approach to organizing the scene, where Entity was an entity consisting of many independent components. I wanted the components to be completely arbitrary and easily combined with each other.

    While reading about this topic, I came across the T-Machine blog.

    He gave me many answers to my questions, but the main answer was the following:

    Entity doesn't contain any logic, it's just an ID (or pointer).
    Entity only knows the ID of the components that belong to it (or a pointer).
    A component is just data, that is. the component does not contain any logic.
    A system is a code that can process a certain set of data and produce another set of data as output.

    If you develop in Java, I highly recommend checking it out. A very simple and conceptually correct Framework. Today it has been translated into a bunch of languages.

    What Artemis is today is called ECS (Entity Component System). There are quite a lot of options for organizing a scene based on Entity, components and data-driven, but in the end we came to the ECS architecture. It is difficult to say how generally accepted the term is, but ECS means that there are the following entities: Entity, Component, System.

    The most important difference from other approaches is: Mandatory lack of behavioral logic in components, and separation of code into systems.

    This point is very important in the “Orthodox” component approach. If you violate the first principle, a lot of temptations will appear. One of the first is to make component inheritance.

    Despite its flexibility, it usually ends in pasta.

    Initially, it seems that with this approach it will be possible to make many components that behave in a similar way, but slightly differently. Common component interfaces. In general, you can again fall into the inheritance trap. Yes, this will be slightly better than classical inheritance, but try not to fall into this trap.

    ECS is a cleaner approach and solves more problems.

    To see an example of how this works in Artemis, you can take a look.

    I will show you with an example how it works for us.

    The main container class is Entity. This is a class that contains an array of components.

    The second class is Component. In our case, it's just data.

    Here is a list of components used in our engine today:

    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_COM PONENT, // 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, W IND_COMPONENT, WAVE_COMPONENT, SKELETON_COMPONENT, / /debug components - note that everything below won't be serialized DEBUG_COMPONENTS, STATIC_OCCLUSION_DEBUG_DRAW_COMPONENT, COMPONENT_COUNT );
    The third class is 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 entity entity we"ve just added */ virtual void RegisterEntity(Entity * entity); /** \brief This function is called when any entity is unregistered from scene. It sorts out is entity has all necessary components and we need to call RemoveEntity. \param entity entity we"ve just removed */ virtual void UnregisterEntity(Entity * entity);
    The RegisterEntity and UnregisterEntity functions are called for all systems in the scene when we add or remove an Entity from the scene.

    /** \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 entity entity we added component to. \param 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 entity entity we removed component from. \param component component we"ve just removed from entity. */ virtual void UnregisterComponent(Entity * entity, Component * component);
    The RegisterComponent, UnregisterComponent functions are called for all systems in the scene when we add or remove a Component to an Entity in the scene.
    There are also two more functions for convenience:

    /** \brief This function is called only when entity has all required components. \param entity entity we want to add. */ virtual void AddEntity(Entity * entity); /** \brief This function is called only when the entity had all required components, and don"t have them anymore. \param entity entity we want to remove. */ virtual void RemoveEntity(Entity * entity);
    These functions are called when the ordered set of components has already been created using the SetRequiredComponents function.

    For example, we can order to receive only those Entities that have ACTION_COMPONENT and SOUND_COMPONENT. I pass this to SetRequiredComponents and voila.

    To understand how this works, I’ll give examples of what systems we have:

    • TransformSystem - a system that is responsible for the hierarchy of transformations.
    • SwitchSystem is a system that is responsible for switching objects that can be in several states, such as destroyed and undestroyed.
    • LodSystem - a system that is responsible for switching lods by distance.
    • ParticleEffectSystem - a system that updates particle effects.
    • RenderUpdateSystem - a system that updates render objects from the scene graph.
    • LightUpdateSystem - a system that updates light sources from the scene graph.
    • ActionUpdateSystem - a system that updates actions.
    • SoundUpdateSystem - a system that updates sounds, their position and orientation.
    • UpdateSystem - a system that causes custom user updates.
    • StaticOcclusionSystem - system for applying static occlusion.
    • StaticOcclusionBuildSystem - system for building static occlusion.
    • SpeedTreeUpdateSystem - Speed ​​Tree update system.
    • WindSystem - wind calculation system.
    • WaveSystem - a system for calculating vibrations from explosions.
    • FolliageSystem - system for calculating vegetation over the landscape.
    The most important result that we have achieved is a high decomposition of the code responsible for heterogeneous things. Now, in the TransformSystem::Process function, all the code that concerns transformations is clearly localized. It's very simple. It can be easily decomposed into several nuclei. And most importantly, it is difficult to break something in another system by making a logical change in the transformation system.

    On almost any system the code looks like this:

    For (a specific set of objects) ( // get the necessary components // perform actions on these objects // write data to the components )
    Systems can be classified by how they process objects:

    • Processing of all objects that are in the system is required:
      • Physics
      • Collisions
    • Only processing of marked objects is required:
      • Transformation system
      • Actions system
      • Sound processing system
      • Particle processing system
    • Working with your specially optimized data structure:
      • Static Occlusion System
    With this approach, in addition to the fact that it is very easy to process objects in several cores, it is very easy to do what is quite difficult to do in the usual polymorphism paradigm. For example, you can easily take and process not all lod switches per frame. If there are a LOT of lod objects in a large open world, you can make it so that, for example, a third of the objects are processed each frame. However, this does not affect other systems.

    Bottom line

    • We greatly increased FPS because with the component approach things became more independent and we were able to decouple and optimize them individually.
    • The architecture has become simpler and more understandable.
    • It became easy to expand the engine without breaking neighboring systems.
    • There are fewer bugs from the series “by doing something to the LODs, the switches were broken,” and vice versa
    • It became possible to parallelize all this across several cores.
    • At the moment, we are already working to ensure that all systems run on all available cores.
    Our engine code is Open Source. The engine as it is used in World of Tanks Blitz, garbage removal Noginsk

    Billiards