📜 ⬆️ ⬇️

Techniques for designing game architecture

Unfortunately, nowhere is there more or less complete publication on the subject of designing architecture in games. There are separate articles on specific topics, but nowhere is all of this put together. Each developer has to independently collect such information bit by bit, stuff cones. Therefore, I decided to try to collect some of this together in this article.

For examples, the popular Unity3D engine will be used. Considers approaches applicable in big games. Written from my personal experience and from how I understand it. Of course, somewhere I may be wrong, somewhere you can do better. I, too, are still in the process of gaining experience and getting new buds.

The publication covers the following topics:


Inheritance VS Components


In big games, the architecture is rather complicated. Complex entities and complex interactions between classes. If you try to develop games using the standard OOP approach, then constant reworking of a bunch of code and a strong increase in the development time are guaranteed.
')
The problem lies in inheritance (the problem of fragile base classes is a situation when it is impossible to change the implementation of the ancestor type without violating the correctness of the functioning of the descendant types). When searching for solutions to combat this problem, a Component-Oriented Approach (CPC) was formed. In short, the essence of the CPC is as follows:
“There is a certain container class as well as a component class that can be added to the container class. An object consists of a container and components in this container. ”

Components are a bit like interfaces. But interfaces only allow to allocate a common signature of functions and properties from classes, and components allow us to take out the general implementation of classes separately.
In the OOP approach, an object is defined by its class.
In the KOP approach, an object is defined by the components of which it is composed. It does not matter what kind of object. It is important that he has and what he can do.

KOP simplifies reuse of written code — using the same component in different objects. Also, from various combinations of already existing components, it is possible to assemble a new type of object.

For example, take the object "character". From the point of view of the PLO - it would be one big class, perhaps inherited from something. From the point of view of the CPC is a set of components that make up the object "character". For example: character characteristics / stats - component “Stats”, character management “CharacterController”, character animation - “CharacterAnimationController”, collision handler - “CharacterCollisionHandler”.

Do not give up inheritance in games. Inheritance of components is quite normal practice. And in some situations, it will be more correct. But, if you can see that there will be several levels of class inheritance to describe objects, then it is better to use components.

Complex hierarchies of classes of units, objects and other things


Many developers, unfamiliar or new to CPC, make the same mistake when designing complex class systems for units, objects. Or even correctly select the components, but for some reason, make inheritance of AI components.

Let us consider in more detail the hierarchy of classes of units of different types. To shorten the text, units in this context can be characters and buildings.



Red lines indicate problem areas - the inherited type must have properties, behavior of different types. In this example, whatever you think of - nothing will solve the problem of the constant changes of all classes in this hierarchy.

The picture below shows how a part of this scheme might look when using CPC.



Since Since objects are assembled from components, then there is no problem anymore, when changing one object, you will need to change others. Also, no more puzzling over the creation of a hierarchy of classes suitable for all objects.

State machines, behavior trees


The scheme in the previous part was very simplified. In fact, the components will need much more. You also need to organize their interaction with each other. To simplify work with objects with complex behavior (units, characters) state machines, behavior trees are used.

1. State machine
The object's logic is divided into states, events, transitions, and can also be broken down into actions. Variations in the implementation of these elements may differ markedly.

The screenshot shows a diagram (graph) of the state machine, made in PlayMaker.



An interesting state machine is implemented in the Behavior Machine plugin. There, the state is the MonoBehaviour component, which is responsible for the logic of work in this state. Moreover, the state can also be a tree of behavior.

2. Hierarchical state machine
When there are many states, the number of connections between them increases, which complicates the work with the state machine graph. To simplify working with it, you can use a hierarchical state machine. It differs in that a nested state machine can be used as a state. Thus a tree hierarchy of states is obtained.

3. Behavior tree
To simplify the writing of AI, games use behavior trees (Behavior Tree).

The behavior tree is a tree structure, the nodes of which are small blocks of game logic. From various blocks of logic, the developer constructs a tree structure in the visual editor, adjusts tree nodes. This structure will be responsible for the decision-making character and its interaction with the game world.

Each node returns a result that determines how the remaining nodes of the tree will be processed. Return results are usually the following: success, failure, and execution.

The main types of nodes in the behavior tree:

The screenshot shows the behavior tree made in the Behaviour plugin.



When is it better to use a state machine, and when is a behavior tree?
Artificial Intelligence for Games II, on page 370, says that behavior trees are harder to implement if they need to respond to events from outside. It also proposes a possible solution - to introduce the concept of "task" (for example: patrolling, stalking the enemy, attacking the enemy) into the tree of behavior. Those. it is assumed that the controller of the behavior tree will move to another node-task in order to change the behavior. Also in the book offers an alternative option - to combine the tree of behavior with the state machine. By the way, this is already implemented in the Behavior Machine plugin.

I tried to use the first option - enter the task nodes in the behavior tree. The work is greatly complicated, since It is necessary not only to implement a task change, but also to “zero out” the variables of the completed / canceled task.

I'd add from myself - if AI itself receives data from the world and does not receive any commands, then Behavior Tree is suitable for it. If AI is controlled by something - by a person or other AI (for example, a unit in strategies is controlled by a computer player or a squad), then it is better to use a state machine.

Abstractions of game objects


It is not necessary to consider that a character controlled by a player is a Player object. Also, do not assume that this character can be controlled only by a person or only a computer. Who knows how the gameplay will be redone in the future.
Player (can be both a computer and a person; you should not mix them in one class) is a separate object. A character / unit is also a separate object that can be controlled by any player. In strategic games, in addition, you can make a separate "detachment" object.

It is not difficult to divide the game logic into such objects. In addition, it will be applicable to many different games.

Simplify access to other components in an object, scene


With a large number of components in the object, there is an inconvenience when you need to access them. Constantly it is necessary to create fields in each component for storing references to other components or to access them via GetComponent ().

The mediator pattern prompted me to introduce some kind of intermediary component through which the components could communicate with each other. In addition, this will allow to check the existence of other components in this component and the code will need to write only 1 time. Such a component for different types of objects is also worth doing different, because different sets of components are used. In this case, this is not the implementation of the mediator pattern, but simply caching of links in one class for the convenience of accessing other components of the object.

Example:

public class CharacterLinks : MonoBehaviour { public Stats stats; public CharacterAnimationController animationController; public CharacterController characterController; void Awake() { stats = GetComponent<Stats>(); animationController = GetComponent<CharacterAnimationController>(); characterController = GetComponent<CharacterController>(); } } public class CharacterAnimationController : MonoBehaviour { CharacterLinks _links; void Start() { _links = GetComponent<CharacterLinks>(); } void Update() { if (_links.characterController.isGrounded) ... } } 


In scenes, a similar situation. It is possible to make references to frequently used components in a certain singleton object so that in the inspector of specific components one does not have to constantly specify references to other objects.

Example:

 public class GameSceneUILinks : MonoSingleton<GameSceneUILinks> { public MainMenu MainMenu; public SettingsMenu SettingsMenu; public Tooltip Tooltip; } 

Using:
 GameSceneUILinks.Instance.MainMenu.Show(); 

Since components need to be specified only in one object, and not in several, the amount of work in the editor is slightly reduced, and the code as a whole will be less.

Difficult composite game objects


Characters, UI elements, and some other objects may consist of a larger number of script components and multiple nested objects. If the hierarchy of such objects is poorly thought out, then this can greatly complicate the development.
Consider 2 cases where it is important to think through the hierarchy of the object:

To begin, consider the first case.
You can completely replace an object, but if after the replacement it must be in the same state and have the same data as before the replacement, then the task becomes more complicated.
To simplify the replacement of any part of the object, its structure can be organized, for example, as follows:
Character

Such a partition will allow you to change the appearance of the object or the animation controller, without much affecting the other components.

Now consider the second case.
For example, there is a certain object with sprite and data. When you click on it in the scene of improvements, you need to improve this object. And when you click on it in the game scene, some kind of game action should be performed.

You can make 2 prefabs, but then, if there are many objects, you will have to configure 2 times more prefabs.

You can go the other way and organize the structure as follows:
ObjectView (object image)

The targetGraphic field in the buttons should refer to the image in the ObjectView.
This approach was tested in uGUI.

Characteristics of objects in the game


In many games, characters, items, abilities, etc. There are some characteristics (health, mana, strength, duration). Moreover, the different types of characteristics differ.
From my experience I can say that it will be more convenient to work with the characteristics if they are put into a separate component (s). In other words - to separate the data from the functional.

In order not to create a bunch of classes for storing various characteristics in a complex system, it is better to store them in a dictionary in a separate class that controls the work with this dictionary.

Also, most likely, you need to have a special class to store the values ​​themselves in the dictionary. Thus, the dictionary will not store the values ​​themselves, but instances of the wrapper class for these values.
What could be in this class:

Universality is quite difficult to achieve. There are only one set of stats (strength), other characters (health, mana), and third abilities (reload duration). These sets should not intersect so that there is no confusion. It is better to store them in different enum-ah, and not in one. In addition, there is the problem of specifying characteristics in the inspector, since dictionaries and type “object” are not serialized in Unity3D.

In my opinion, there is no need to pursue versatility here. For example, it is often enough just one data type (int or float), which simplifies the work. You can also render characteristics with a type other than the rest separate from the dictionary.

Modifiers (buffs / debuffs)


Characteristics of the character / item / ability may vary due to the effects of any superimposed effects or effects of dressed items. The entity that changes these characteristics, in this context, I will call the modifier. I myself did not work with modifiers, but the topic is important. Therefore, I will describe my thoughts on how this can be organized in code.

A modifier is a component that lists the characteristics it affects and the magnitude of the effect. It may be even better if the modifier affects only one specified characteristic. When an effect is applied to a character, a modifier component is added to it. Further, the modifier calls the function “apply itself to such an object”, and the characteristics of the object are recalculated. And only those characteristics that it affects. When removing a modifier - recalculation is performed in the same way. Most likely, you need to store 2 dictionaries of characteristics - actual (calculated) and initial.

Recalculation is needed so that it is not necessary to constantly calculate current values ​​during each access to the character data. Those. normal caching for increased performance.

Data serialization


When developing games, XML is still very often used, although there are alternatives that are often more convenient - JSON, SQLite, and data storage in prefabs. Of course, the choice depends on the tasks.

When using XML or JSON, many use fairly sub-optimal ways to work with them. With a bunch of code to read / write / create structures in these formats, and with the need to specify the names of the names of the elements to be addressed in a string form.

Instead, you can use serialization. The structure of XML and JSON in this case will be generated from the code (Code First approach).

You can use the built-in .NET tools to serialize XML in Unity3D, and you can use the JsonFx plugin for JSON. I tested the efficiency of both solutions on Android. It seems like it should work on other platforms, too. used API is used in third-party cross-platform plugins.

An example of using XML serialization:
Saving and Loading Data: XmlSerialize

What can be read on the architecture in games


In electronic form there is a translation of the following books into Russian:

It is also useful to sort out third-party visual scripting systems, for example: PlayMaker, Behavior Machine, Behavior Designer, Rain AI (useful for studying, but not convenient in real projects). Some of them can draw some ideas, look at what logical blocks can be divided game classes.

Source: https://habr.com/ru/post/255561/


All Articles