Architecture of a simple 2D game on Unity3D. Plan, fact and error handling

Published on August 27, 2014

Architecture of a simple 2D game on Unity3D. Plan, fact and error handling

    Recently, the Whistling Kite Framework team released another game, this time a Snake written in Unity3D. As in most game projects, when deciding how detailed the application needs to be designed, time was a critical factor. In our case, the reason is simple: development was carried out in free time from the main work, then an ideal approach to design would postpone the release for another year. Therefore, having made the initial division into modules, we finished the design and started development. Under the cutter is a description of what came of it, as well as a couple of lessons that I learned for myself.


    Caution Pictures!

    Immediately I want to insert a disclaimer: everything described below is only a retrospective of events and an attempt to analyze what turned out well and what was bad. The article does not pretend that it sets forth an indisputable truth, rather, on the contrary, “harmful advice”, which, perhaps, will save someone from the same mistakes.

    Functionality


    First, a couple of words, in fact, about the application, so that it is clear what exactly we designed and developed. Creating a snake, we tried to recreate exactly the good old classic snake, in which there was nothing but a snake, apples, walls and an infinite desire to move on. That is why we focused on the classic gameplay, deliberately excluding from it at this stage all the additional features.

    One of the main advantages of our snake is the variety of control options that you can find in the settings. We tried to provide for all of them:

    • 4 buttons - set a new direction of movement;
    • 2 buttons - turn left / right from the current direction;
    • svaypy - move your finger in the right direction.


    We plan to add two more:

    • gyroscope - the tilt of the device sets a new direction;
    • joystick - “bubble” - the shift of the finger from the first point of contact sets a new direction.


    The rating typed in the game is recorded in the high score table. The record can be shared with friends via SMS, mail or any social network connected to your smartphone - twitter, vkontakte, facebook, etc.

    The decision to start developing the snake we made, pursuing two goals:

    • Having tried most of the "snakes" existing under Android, we did not find the one that several team members wanted to play, we decided to fix this situation;
    • We first decided to enter the mobile market and use the Unity3D engine, so we chose a deliberately uncomplicated game to get comfortable with it.


    Architecture


    Stage 1. Concept

    The first, conceptual, version of the architecture was created by us even before the choice of unity. It is shown in the picture below. In this option, we highlighted the layers of the future application:

    • Initialization layer;
    • Interface layer;
    • Layer of logic;
    • Controller layer;
    • Control layer.



    Historically, the initialization layer was the last to be selected, although in the application it should have been launched first. The fact is that we first laid the initialization of objects in the objects themselves, but then decided to allocate it centrally.
    Layers of the interface, logic and controllers are almost a classic MVC pattern. At the same time, there was a desire to separate the processing of these layers even into different streams in order to ensure maximum smoothness of the interface.
    The control layer was taken out separately, because We decided that a variety of control options would be one of our main features, so we had to make sure that the rest of the application did not depend on the chosen option.
    All layers had to communicate with each other through dedicated interfaces, each layer contained its own objects and performed its functions, often contained its own processing flow.

    Good ideas:
    • Highlighting the initialization layer: in subsequent versions, this was lost, which sometimes led to rather confusing sequences of actions.
    • The correct general structure of dividing the application into modules: in the future, the boundaries were different and the concept of encapsulation was almost forgotten (you can give the picture "there is a bug" with cats, just replace it with "it’s impossible! How can encapsulation"


    Misses:
    • Separate layers of controllers and control: subsequently, controllers in terms of MVC were abandoned due to redundancy.
    • Too high-level description: it is not clear which code to write . Although this can not be called a pure blunder, because at the time of the creation of this architecture, an engine was not yet selected.


    The next step was the choice of platform, although, strictly speaking, the choice was made between unity and development in pure java. A cursory review of other existing engines did not arouse enthusiasm in their study. We came to a rather expected conclusion: it is easier to write a snake without a platform at all, besides it looked more interesting - who doesn’t like to make their own bikes? However, we chose unity in order to familiarize ourselves with the platform, which is close to the status of the de facto standard in the field of game maidens. Yes, we got a solid overhead due to the fact that unity is a three-dimensional engine (at the time the development began, unity did not have native 2D support yet), and we did a two-dimensional game, but the experience gained was worth it.

    Stage 2. Project

    While choosing a platform, we jointly wrote a design document, and already on its basis a second architectural project was created, sharpened for the chosen platform.

    This architecture consisted of several related diagrams, which I call uml terms below, although, of course, they follow the standard, to put it mildly, not completely.

    Essence-connection (it is clear that this is more likely to be a system’s requirements than its architecture, but in the context of the article I can’t help but mention it:



    ERD notation, I think, is familiar to everyone. It contains all the objects of game logic and logical connections between them.Each such object generates one class that ensures the operation of this

    object.Diagrams of components and cooperation.For their perception, I will first describe a number of agreements:

    General architecture - is of a general conceptual nature, being, in fact, a continuation of the diagram of stage 1. It displays:
    • System components (rectangles);
    • Storage subsystem (database symbol, magnetic disk);
    • Notes (yellow “sheets”);
    • Control flows (filled arrows);
    • Data streams (dashed arrows).

    Detailed charts contain:
    • Components, objects (rectangles);
    • Events handled by objects (hexagons);
    • Actions performed by events (ellipses).




    In general, the concept has not changed much: there is also a separate component for initialization, then two main subsystems are highlighted.
    After starting the application, the initialization component starts working first: it requests all the necessary data from the storage and initiates the GUI subsystem.

    The GUI subsystem should provide the user with the main menu, settings and records. At the start of the game, control is transferred to the control subsystem.

    The control subsystem should provide user interaction with the game world, first of all it is control directly by the snake.

    Separately selected components for logging and sharing records.

    The game world consists of objects taken from ERD. The key role is played by the party and the snake.

    The party stores links to the snake and the current fruit instance, provides initialization of game objects and their interaction.
    The snake processes the main game events: its own movement, eating fruit and collisions with walls or the tail.
    The fruit instance is responsible for controlling its own lifetime.

    Good ideas:
    • Isolation of two main independent subsystems: this was not taken into account during development, which initially led to a large number of extra scenes.


    Misses:
    • Does not take into account the separation of scenes and work with the storage subsystem from the game scene
    • Does not answer the question of how to implement the GUI subsystem and how the subsystems interact with each other.
    • The Party object took over the functions of the controller in terms of MVC.


    Stage 3. Actual result

    After the start of development, no one has corrected the architecture. I only left separate notes in the corresponding section of the design document on how these or other bottlenecks were implemented, but the main goal was to release the release. Well, we achieved this goal, and I sat down for refactoring to prepare for the development of the second release. Already at that moment I understood that first of all it was necessary to transfer the game to honest 2d, the support of which had just appeared in unity. And there was also an understanding that the result obtained was far from ideal, primarily in terms of dividing functions by objects and their interaction, therefore, I set myself two tasks:

    1. Create a general class diagram
    2. Create sequence diagrams for the main application scenarios.


    Based on this information, I planned to get a list of what needs to be changed. The results of such reverse engineering are presented below, in each section I first briefly describe what this section is about, then I give one or more diagrams illustrating the constructed solution, and then describe in detail what was done and how.

    Overview of Classes Created

    The diagram below shows all the classes created during game development. A part of the methods is hidden on the diagram that do not carry a semantic load or are so trivial that they are not worth mentioning. Also often under the name of a variable (for example, textures) is a whole block of variables (in this case, a variety of textures).



    All available classes are divided into four packages:
    • Gamelogic - classes for game objects existing on the irg field are presented here. In fact, this is soldered by the core of the game.
    • Gui - here are the classes responsible for building a static user interface: the main menu, settings, records and buttons on top of the playing field, except for the buttons for directly controlling the snake.
    • Controllers are the highlight of our application: a package with a variety of ways to control a snake. Each class fully contains everything necessary for the chosen control style: from interface buttons to rotation calculation logic.
    • Providers - this package contains auxiliary classes that provide functions such as reading and saving data, the ability to share a record, logging, analytics, etc.

    In the game logic package, the central place is occupied by the party class, which is one game party. This class is responsible for the beginning and end of the game, for the rating, for coordinating the events of eating an apple and the expiration of the life of an apple, and many other auxiliary functions. While this article was being written, I practically rewrote this class, leaving in it only calls to methods from other classes (in fact, snakes and apples). This is more suitable for encapsulation, but has made this class even more like a controller in terms of MVC.

    The second most important Snake class contains snake movement logic, including the management of dependent Snakechain objects. It is to this class that control controllers transmit commands.

    A factory is used to create fruit instances (FruitInstance), as in the future it is planned to increase the number of different types of fruits.

    The interface package contains separate classes that are responsible for displaying and processing the user interface, depending on the situation. It also handles pressing iron buttons on the device.

    The controller package contains control classes. They are added to the snake and interact directly with the Snake object using the command pattern, and the snake executes the received commands in the same sequence, but separately in its steps. This was done to correctly process fast sequential commands, for example, to rotate 180 degrees along its tail.

    The package with providers contains only one class that is interesting for this article - it is dataProvider. This class contains a set of static functions that are a wrapper over calls to standard methods for working with stored properties. At both previous stages of the design, working with stored data was only supposed to be done once: load them into memory and no longer access slower media. This approach did not take into account the issue of scene independence in Unity: as a result, when switching between the scenes of the main menu and the playing field, all the necessary data had to be re-read.

    Initial conditions

    The following is a description of the initial state of the two scenes that make up the game: the main menu scene (in the figure below on the left) and the scene of the playing field (in the figure below on the right). The initial state is determined by what was entered in the editor before running any executable code. It is from these states that most sequence diagrams begin below.



    The main menu is created by two classes, Player and GUInavigator, attached to a single object on the scene, to the camera. Player is responsible for downloading all the basic information about the player, and GUInavigator initiates the desired behavior from the interface package and provides further transitions between the interfaces.

    The game scene contains many more objects. Most of them are static, representing the world of a snake: background, playing field, walls. Additional behaviors are attached to only two objects: to the camera (GUInavigator, Player, party, fruitfabric) and the head of the snake (Snake, Controllerselector). Controllerselector is responsible for choosing a control controller according to the player’s settings.

    Application launch

    The diagram below reflects the application startup procedure (in that part, which is controlled by the developer). Particular attention can be paid only to an alternative way out of the script and the transition to loading the game, if there is a saved unfinished game.



    Application loading consists of handling two events: awake and start. The awake event is handled by the player object: at this moment it calls on the DataProvider to load information about the player, and then calls its own method, which is responsible for applying the current settings, for example, muting the sound.

    The Start event is handled a little more complicated:
    1. The GUInavigator in the initMain method initiates all the interfaces required in this scene, each with an inactive sign.
    2. He then checks to see if the unfinished game has been saved. This is possible, for example, when the game is interrupted by an incoming call.
      1. If there is a game, the game scene is loaded, and this scenario is interrupted.
    3. Conditionally, at the same time, Start events are processed for all initiated classes of interfaces - they are additionally initialized, which is specific to each particular interface individually (including the record interface that loads data from the record table using dataProvider).
    4. The main menu interface is assigned the active flag, and it is displayed to the user.


    Game launch

    Next, consider a sequence diagram illustrating the steps when starting a game. The starting point is loading the scene with the game.
    In this diagram, it is worth noting separately the alternative scenario for loading a saved game - this is necessary for the case of interruption of the game, for example, by an incoming call.



    The first events to trigger are Awake for Player objects (loading all settings, similar to starting the application) and Snake (Init () method - initializing the tail “by default” from two segments). Then the Start event is triggered to select the controller (for example, FourButtonController is used), load the GUI, and initialize the game. A bit more about handling this event:
    1. Controller selector in the Start event, checks the player’s settings and loads the necessary control controller.
    2. The GUInavigator in the initGame method initiates all the interfaces required in this scene, each with an inactive sign. The determination of which method to call occurs on the basis of the parameter specified through the editor. Initialization and activation of the active interface are similar to launching an application.
    3. Next, the Start event processes the party object. First of all, he checks if there is a saved game.
      1. If there is a game, then party calls dataProvider to restore, which, after reading the data, calls the Restorebackup method of the party object.
      2. He, in turn, calls a similar method of the Snake object, and that one is chained for all links.
      3. After data recovery, control is returned to the party object, it pauses and waits for player actions.

    4. If there was no game, then party calls the fruit factory to create an instance and the game began. By the way, the diagram shows that party is also responsible for setting a number of parameters for the fruit - now this is no longer the case: all necessary actions are encapsulated either in the factory or in the fruit instance.


    Game cycle: snake movement

    Game logic is concentrated around processing two events: update is processed in the snake by the movehead method, here the snake moves and controls the movement of its segments; and ontriggerenter, where the processing of snake head collisions with fruits, walls and its own tail takes place.



    We analyze the collision event in more detail:
    1. When an event occurs, it is first checked with whom we encountered: if it is an apple, then the Eatfruit method is called, and if it is a tail or a wall, then Killme. In both cases, collision information is dumped into the logs.
    2. Apple case:
      1. The snake sets the flag to add a link and calls the Eatfruit method of the party object.
      2. Party increases the party rating and calls its own createfruit method, which was also used when starting the game.
      3. In this method, the current fruit instance is first deleted, and then a new one is created through a call to the factory.

    3. Wall or tail case:
      1. The snake passes event processing to the party object in the Endgame method.
      2. Party sets the pause mode and, through the interface navigator, puts the player on the screen with the results of the party.


    The movement of the snake is implemented as follows:
    1. In the update event, the movehead method is called
    2. Here the speed of the snake is checked: the moment of transition has arrived or not
    3. If yes, then the flag of the need to add a link is checked
      1. If necessary, a link similar to the one following the head is created.
      2. Only the head and one newly created link are shifted.

    4. If there is no flag, then the head shifts, and after it all the links along the chain, with the adjust method.


    Conclusion

    A lot of logic has remained beyond the scope of this article: this is logging, both internal and using external analytics; this is the work of an interface navigator that encapsulates all the features associated with two scenes; this is working with data, with the device, displaying ads and many more other features. If anyone is interested, I can write about this separately.

    Key issues identified:

    • Replacing 3d with 2d. This does not follow from diagrams, but it is obvious from the point of view of preference of using tools for their intended purpose.
    • The party object is too overloaded with functions: it controls both apples, and the snake, and the game completion processing.
    • The game interface is overloaded - it must be divided into two states: in the game and after a collision.
    • There is no facade for working with device functions not provided by the engine: in our case, for android it is an overridden vibration method, the implementation of the share button (calling the system window for selecting an application) and the implementation of Google analytics calls.


    What conclusions have I drawn for future projects based on the foregoing?
    1. You need to know the platform before taking on something serious. This is generally obvious, but just in case I decided to repeat it, maybe someone will save someone from launching the next killer top MMOs without experience.
    2. It is necessary to design the separation of the application (even simple) into components. At this stage, the main thing is to minimize the number of interactions and data flows.
    3. Thinking over the structure of classes, you need to immediately think about how objects will be related to components and what functions (groups of functions) they will perform.
    4. On the one hand, you need to keep in touch with the planned architecture, but on the other hand, you need to be prepared to change it if new conditions arise.

    If you see some other not optimal solutions, then please speak in the comments and discuss with pleasure.

    Just in case, at the end I give all the links relevant to the article:


    PS During the preparation of this article, some of the errors found have already been fixed, and, probably, new ones were introduced, because development does not stand still.