Techniques for designing a game architecture

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

    For examples, the popular Unity3D engine will be used. The approaches applicable in large games are considered. Written from my personal experience and from how I understand it. Of course, somewhere I may be wrong, somewhere I can do better. I'm also still in the process of gaining experience and gaining new bumps.

    The following topics are covered in the publication:
    • Inheritance VS components
    • Complex hierarchies of unit classes, items, and more
    • State machines, behavior trees
    • Game Object Abstractions
    • Simplification of access to other components in the object, scene
    • Complex composite game objects
    • Characteristics of objects in the game
    • Modifiers (buffs / debuffs)
    • Data serialization


    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 alterations 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 where it is impossible to change the implementation of the ancestor type without violating the correct 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 the components in that container. ”

    Components are a bit like interfaces. But the interfaces only allow you to highlight the common signature of functions and properties among the classes, and the components allow you to make the general implementation of the classes separately.
    In the OOP approach, an object is defined by its class.
    In the CPC approach, an object is defined by the components of which it consists. It doesn’t matter what the object is. It is important what he has and what he knows how to do.

    CPC simplifies the reuse of written code - the use of one component in different objects. Also, from various combinations of existing components, you can assemble a new type of object.

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

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

    Complex hierarchies of unit classes, items, and more


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

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



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

    The image below shows how part of this design might look when using CPC.



    Because Since objects are assembled from components, there is no longer a problem that when you change one object, you need to change others. Also, there is no need to puzzle over creating a class hierarchy suitable for all objects.

    State machines, behavior trees


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

    1. State machine The
    logic of an object is divided into states, events, transitions, and can also be divided into actions. Variations in the implementation of these elements may vary markedly.
    • Object state - it can be like a class without game logic, just storing some data, for example, the name of the state of the object: attack, movement. Or the class “state” can describe the behavior of an object in a specific state.
    • An action is a function that can be executed in a given state.
    • Transition is a connection between 2 states. Indicates from which state to which transition is possible.
    • Event - a certain message / command transmitted to the state machine or called inside it. Serves to indicate that it is necessary to transition to another state, if possible from the current state.

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



    An interestingly implemented state machine 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 behavior tree.

    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 state machine can be used as a state. Thus, a tree-like hierarchy of states is obtained.

    3. Behavior Tree
    To simplify the writing of AI, games use Behavior Trees.

    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, sets up the tree nodes. This structure will be responsible for making decisions by the character and his interaction with the game world.

    Each node returns a result that determines how the remaining nodes of the tree will be processed. Options for the return result are usually the following: success, failure, performed.

    The main types of nodes in the behavior tree:
    • Action Node (Action)
      Just some function that should be executed when visiting this node.
    • Condition
      Usually serves to determine whether or not to execute nodes following it. If true, returns Success, and if false returns Fail.
    • Sequencer (sequence)
      Executes all nested nodes in order until either of them fails (in this case returns Fail), or until they all succeed (then returns Success).
    • Selector
      Unlike the Sequencer, it stops processing as soon as any nested node returns Success.
    • Iterator (iterator - acts as a for loop)
      Used to execute a series of actions in a loop a number of times.
    • Parallel Node
      Executes all of its child nodes "simultaneously." It does not mean that nodes are executed by several threads. It just creates the illusion of parallel execution, similar to coroutines in Unity3d.

    The screenshot shows the behavior tree made in the Behavior Machine plugin.



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

    I tried to use the first option - to introduce task nodes into the behavior tree. The work is very complicated, because it is necessary not only to implement the task change, but also to implement the “zeroing” of the variables of the completed / canceled task.

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

    Game Object Abstractions


    You should not consider that a player-driven character is a Player object. Also, do not assume that only a person or only a computer can control this character. Who knows how the gameplay will be redone in the future.
    Player (can be either a computer or a person; you should not mix them in one class) - this is a separate object. A character / unit is also a separate object that can be controlled by any player. In strategic games, you can also separately take out the “squad” object.

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

    Simplification of access to other components in the object, scene


    With a large number of components in the object, there is inconvenience when you need to access them. You constantly have to create fields in each component for storing links to other components or access them through GetComponent ().

    The mediator pattern prompted me to introduce a certain intermediary component through which the components could access each other. In addition, this will make it possible to check the existence of other components in this component and the code will need to be written only 1 time. Such a component for different types of objects should also be made different, because different sets of components are used. In this case, this is not an implementation of the mediator pattern, but simply caching links in one class for easy access to other components of the object.

    Example:

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


    The scenes have a similar situation. It is possible to make links to frequently used components in a singleton object, so that in the inspector of specific components you do not have to constantly indicate links to other objects.

    Example:

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

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

    Because components should 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.

    Complex composite game objects


    Characters, UI elements, and some other objects can consist of a larger number of script components and many nested objects. If the hierarchy of such objects is poorly thought out, then this can greatly complicate the development.
    Consider 2 cases when it is important to think over the hierarchy of an object:
    • individual parts of the object must be replaced by others during the game;
    • some of the object’s scripts should work only in one scene, and the other part in another.

    First, consider the first case.
    It is possible to completely replace an object, but if after replacement it should 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
    • Data
    • ControlLogic (scripts to control the character)
    • RootBone (character’s root bone; Animator components and scripts for working with IK should be here, otherwise they will not work)
    • Animation (other scripts for working with animation)
    • Model

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

    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 a lot of objects, you will have to configure 2 times more prefabs.

    You can go the other way and organize the structure as follows:
    ObjectView (picture of the object)
    • Data (object data used in both scenes)
    • UpgradeLogic (button and scripts for the improvement scene)
    • GameLogic (button and scripts for the game scene)

    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, for various types, the set of characteristics is different.
    From my own experience I can say that it will be more convenient to work with the characteristics if they are taken out in a separate component (s). In other words, separate data from functionality.

    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 create a special class for storing 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:
    • An event that is raised when a value changes;
    • A value that can be of several types, for example:
      1. present value;
      2. minimum and maximum values ​​(for example, random attack strength in the range of 10–20);
      3. current and maximum values ​​(e.g. health).

    Universality is quite difficult to achieve. Sets of stats for objects are one (strength), for characters are different (health, mana), for abilities are third (duration of cooldown). These sets should not overlap so that there is no confusion. It is better to store them in different enum-s, and not in one. In addition, there is a problem of specifying characteristics in the inspector, because dictionaries and the type “object” are not serialized in Unity3D.

    In my opinion, there is no need to pursue universality here. For example, often only one data type (int or float) is sufficient, which simplifies the work. You can also make characteristics with a different type from the rest separately from the dictionary.

    Modifiers (buffs / debuffs)


    Characteristics of a character / item / ability may change due to the effect of any superimposed effects or effects of dressed items. An entity that changes these characteristics, in this context, I will call a 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 that it affects and the magnitude of the effect. Perhaps it will 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. Next, 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 a modifier is deleted, the recalculation is performed in the same way. Most likely, you need to store 2 dictionaries of characteristics - relevant (calculated) and initial.

    Recalculation is needed so that you do not have to constantly calculate actual values ​​every time you access the character’s data. Those. Normal caching for increased performance.

    Data serialization


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

    When using XML or JSON, many use rather non-optimal ways to work with them. With a bunch of code for reading / writing / creating structures in these formats, and even with the need to indicate in a string form the names of the elements to be accessed.

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

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

    XML Serialization Example:
    Saving and Loading Data: XmlSerialize

    What you can read about architecture in games


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

    It is also useful to disassemble third-party visual scripting systems, for example: PlayMaker, Behavior Machine, Behavior Designer, Rain AI (useful for learning, but not convenient in real projects). Some ideas can be gleaned from them, look at which logical blocks game classes can be divided.

    Also popular now: