Evolutionary Game hierarchy-rebuilding your game entities with components
Until recently, game programmers have used classes with deep structures to represent game entities. The current trend is gradually changing from a deep structure to simply taking the game entity object as an aggregate component. This article explains what these changes mean and explores the benefits and practical use of this method. I will describe some of my personal experiences, how to implement this system in large projects, and of course how to sell your solutions to other programmers and managers.
Game entity
Different games have different needs, just like what a game entity needs. However, in most games, the concepts of entities are very similar. A game entity is an object in the game world. It is usually visible to players and can be moved around.
Some entity examples:
L bullets
L car
L Tank
L grenade
L gun
L hero
L pedestrian
L aliens
L jet aircraft
L medical package
L stone
Entities can usually do many things. Here are some things you may want to do:
L run a script
L mobile
L behave like a rigid thing
L emit particles
L play a specific sound
L players can be placed in a backpack
L can be put on by players
L explosion
L magnetic
L targeted by players
L follow a path
L Animation
Traditional deep-level structure
The traditional way of representing a set of entity sets is like decomposing the entity set we want to remove tables. The intention to do this is usually good, but with the development progress of the game, these things often need to change-especially when a game engine is used again by different games. We usually final design like a graph B-1, but actually there are more class hierarchies than the nodes in the graph.
Figure B-1
As development progresses, we usually need to add many different functions to entities. Objects must either encapsulate their own encapsulated functions or inherit from other objects with that function. Regular Functions are loaded on the Root Node close to the class hierarchy, such as the centity class. One benefit of doing so is that all derived classes can have those functions. But the bad thing is that these classes bring related overhead.
Even a very simple object, such as a stone or a grenade, has a large number of additional functions (related member variables, or member functions that are not necessary to be executed) at the end ). Traditional game object hierarchies often end with something called the blob. As one of the classic anti-pattern, the fat ball is represented as a huge single class (or has a large number of branches on the class hierarchy) and has a large number of complex intertwined functions.
When the fat ball anti-pattern often appears near the root node of the object hierarchy, it is displayed on the leaf node ). The most likely candidate is the player class. Because a game is usually programmed for a single role, it indicates that the role object often has a large number of functions. This is often implemented in a class, such as the cplayer class, with a large number of member functions.
The result of implementing such a function near the root node of the hierarchy is to overload a large number of leaf objects that do not require functionality. However, using the opposite implementation method to implement a large number of features on the leaf node is also unfortunate. The function is now broken down, so it can only be used by a specific function specifically programmed for that object. Programmers often copy the same code to mirror functions implemented by different objects. In the end, we need to re-organize the hierarchy of classes and perform dirty refactoring to move and combine functions.
Let's take an example. An object is physically functional as a rigid body. Not all objects need to do this. As you can see in the graph B-1, we just let the crock and cgrenade classes derive from the crigid class. What will happen if we want to apply this function to the car? You don't want to move the crigid class to the top of the hierarchy to make it more like the heavy fat model at the root we saw previously, all functions are chained into a narrow chain of classes starting from the first inherited classes.
Aggregation component
The component method is more and more recognized by current game development. It is a way to separate different functions into different components independent from other components. Traditional object hierarchies are removed, and an object is now created as an aggregation (accumulation of things) of an independent component ).
Each object now has only the functions it needs. Any different new colons are implemented to add a component.
An object system composed of aggregation components can be implemented in three ways, which can be seen as transferring the fat ball object hierarchy to different stages of a combination object. The following describes the three phases.
Object as a fat ball
A common method to reconstruct a fat ball object is to distribute its functions to different sub-objects and reference it by the first object. In the end, the parent fat ball object is replaced by a series of pointers pointing to other objects. Finally, the member functions of the fat ball object program the interface functions of these sub-objects.
This may actually be a reasonable solution if the features in your game objects are within a suitable small range, or if time is limited. You can easily aggregate arbitrary objects by allowing some sub-objects to be null (assign them a null pointer ). If there are not many sub-objects, this still allows you to have a lightweight pseudo-composite object that does not implement a composite framework for managing this object.
The disadvantage is that it is still essentially a fat ball. All functions are encapsulated in a large object. It's not like you completely break down a fat ball object to a pure sub-object, so you still have some important overhead and will make your light object heavier. You still have to constantly check the idle pointer to see whether the overhead needs to be updated.
Object as a component container
The next stage is to break down each component (the "sub-object" in the previous example) to share a common base class object. Therefore, we can store a list of objects in the object.
This is an excessive solution, and we still have the root "object" that represents the game entity ". In any case, it should be a reasonable solution, or it is indeed a feasible solution in practice, if a majority of the Code libraries need game objects of this concept as a specific object.
Your game object then becomes an interface object that acts as a bridge between the legacy code in your game and is also a composite object of the new system. If time permits, you will eventually eliminate the concept of a game entity object as an integral object. On the contrary, the access object is more and more directly through its component. In the end, you can convert it to pure aggregation.
Object as pure Aggregation
In the final layout diagram, an object is the sum of all parts. The graph B-2 shows a scheme where each object is made up of many different components. There is no "game entity object" here ". Each column represents the same component in the icon, and each row can represent an object. Components can also be seen as independent from the objects that make up them.
Figure B-2
Practical experience
The first combination system of objects I implemented with components was implemented when I played the Tony Hawk series games at Neversoft. Our game Object System has been evolving along with three continuous releases of the game, guiding us to a game object hierarchy to restructure the fat ball anti-pattern I mentioned earlier. It suffers from all the same problems: objects tend to be heavyweight. The object has unnecessary data and functions. Sometimes unnecessary features make the game slower. Features are sometimes repeated on different tree branches.
I have heard of this new invention about the "Object-based component" SYSTEM IN THE sweng-gamedev email list. I think that sounds like a good idea. I started to reorganize the code and completed it two years later.
Why is it so long? Because, first of all, we are making Tony Hawk games at a hard rate every year, so there is only a small amount of time for us to invest in refactoring. Second, I mistakenly calculated the scale of the problem. A three-year code group already contains a large amount of code. A large amount of code has gradually become some inflexible code in a year. Because the Code depends on the game object to become the game object, especially some game objects. That means there is a lot of work to be done to make everything work in a component-based way.
Expected Resistance
The first problem I encountered was how to explain the system to other programmers. If you are not particularly familiar with object combination and aggregation, it will be considered unintentional, complex, and unnecessary work, which will put you under a blow. Programmers have been working on the object hierarchies of traditional systems for many years and are very accustomed to that kind of work. They even become good at that way to solve those problems.
It is also difficult to sell this solution to the manager. You need to be able to accurately describe in a plain language how this solution can make the game faster. The following is the reason for saying:
"When we add new features to the game, it will take a long time to complete, which will lead to very bugs. If we use this new component object, it will allow us to add new features faster and there will be fewer buckets"
I adopt a quiet method. I first discuss this idea with some programmers and finally persuade them that this is a good idea. I then implemented the basic framework of general components, and also implemented a small part of the game object function as a component.
I then presented these results to the remaining programmers. They have some doubts and conflicts, but since it has been implemented and it is not a big controversy to work there.
Slow progress
When the framework is linked, the convenience from the static hierarchy to the object combination is very slow. It is a thankless job. Even if you spend many hours and many days re-constructing code that looks decent, it is no different from the replaced code. We are still doing this, and we are still implementing new features for the next game.
Earlier on, we hit the challenge of restructuring our largest class-the ski class. Because it contains a large number of functions, it can hardly be reconstructed for a period of time. In fact, it cannot be restructured unless other object systems in the game are already in the form of components. In other words, other object systems are not easily componentized, unless the ski is already a component.
The solution here is to create a "fat ball component ". This is a separate giant component that encapsulates the functionality of a large number of ski classes. A small number of other fat ball components also need to be used elsewhere. We finally forced the object system into the component. When this is done, the fat ball component can be re-formed to form more atomic components.
Result
The initial reconstruction result is not so obvious. However, over time, the Code becomes clearer and easier to maintain, and functions are encapsulated into scattered components. Programmers start to create new types of objects with less time, simply combine some components and then add a new one.
We have created a data-driven object creation system, so the entire new type of object can be created by the designer. This proves to be very valuable for quickly creating and configuring new types of objects.
Finally, programmers began to accept componentized systems (at different speeds. And they become very skilled in adding new functions through components. Common interfaces and strict encapsulation make the bugs much easier to read and maintain and reuse.
Implementation Details
A common interface for each component means that it inherits from the same base class with virtual functions. This will incur additional costs. But do not contradict this method because of this. The cost savings are not important compared with the simplicity of objects.
Since each component has a public interface, it is very easy to add additional debugging member functions to each component. This makes it easier to add a diagnosis object that can export the readable information of component composite objects. Then, this can be evolved into a remote debugging tool with complex functions, which can always obtain the latest information about almost all types of Game objects. This may be annoying to implement and maintain in a traditional hierarchical system.
Normally, components do not know each other. In the real world, there is always a dependency between specific components. Performance issues also determine that the component should be able to quickly access other components. At the beginning, we made reference to all components through the component manager. However, at the beginning, only 5% of the CPU time was used. We allowed Component Storage to point pointers to other objects, and directly call member functions in other components.
In components, it is very important to combine the sequence of objects. In our initial system, we stored components in a container object as a linked list. Each component has an update function. Each object is called every time the component list is iterated.
Because object creation is data-driven, it will cause trouble if the components in the linked list are not in the expected sequence. If the physical content of an object is updated before the animation content, but the animation content of another object is updated before the physical content, they will lose synchronization. Mutual dependencies such as this must be found, and then define mandatory rules in the code.
End
One of the best decisions I have made is to use components to transform an object hierarchy from a fat ball style to a composite object structure. The initial result was disappointing. It took too much time to refactor the existing code. In any case, the final result is very worthwhile, lightweight, flexible, robust, and reusable code.