📜 ⬆️ ⬇️

Architecture simple 2D games on unity3d. Plan, fact and work on the bugs

Recently, the Whistling Kite Framework team released another game, this time the Snake, written in Unity3D. As with most game projects, when deciding how much detail you need to design an application, time was a critical factor. In our case, the reason is simple: development was carried out in his spare time from the main work, then the ideal approach to design would postpone the release for another year. Therefore, having made the initial division into modules, we completed the design and began to develop. Under the cut description of what came out of it, as well as a couple of lessons that I learned for myself.


Careful, pictures!

I just want to insert a reservation: everything described below is only a retrospective of events and an attempt to analyze what went well and what went wrong. The article does not pretend that it contains an indisputable truth, rather even the opposite - “bad 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 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, knowingly excluding from it at this stage all the additional features.
')
One of the main advantages of our snake is a variety of management options that you can find in the settings. We tried to provide them all:



We plan to add two more:



Dialed in the party rating is recorded in the table of records. Record can be shared with friends via SMS, mail or any social network connected to the smartphone - Twitter, VKontakte, Facebook, etc.

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



Architecture


Stage 1. Concept

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




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

Good ideas:


Slips:


The next step was the choice of the platform, although, strictly speaking, the choice was made between unity and development on pure java. A cursory review of other existing engines did not arouse enthusiasm in studying them. We came to a rather expected conclusion: it is easier to write a snake at all without a platform, besides, it looked more interesting - who does not like to make their bikes? However, we chose unity, in order to familiarize ourselves with the platform, which is close to the status of the standard de facto in the field of game-deva. Yes, we received a solid overhead projector due to the fact that unity is a three-dimensional engine (at the time of its development, unity still did not have native 2D support), but 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 architecture project was born, sharpened for the chosen platform.

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

Entity-connection (it is clear that this is more related to the category of requirements for the system than to its architecture, but in the context of the article I cannot but mention it:



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

Component and collaboration diagrams. For their perception, I will first describe a number of agreements:

General architecture - is of a general conceptual nature, being, in essence, a continuation of the diagram of stage 1. It displays:

Detailing diagrams contain:




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 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, the control is transferred to the control subsystem.

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

Separately highlighted components for logging and sharing records.

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

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

Good ideas:


Slips:


Stage 3. Actual result

After the start of development, no one has corrected the architecture. I just left separate notes in the appropriate section of the design document on how certain bottlenecks are implemented, but the main goal was to release the release. Well, we have achieved this goal, and I sat down to refactor in order to prepare for the development of the second release. Already at that moment I understood that first of all I had to transfer the game to an honest 2d, whose support had just appeared in the unity. And there was also an understanding that the result was far from ideal, primarily in terms of the division of functions into 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 provide one or more diagrams illustrating the solution built, and then describe in detail what was done and how.

Overview of the created classes

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



All available classes are divided into four packages:

In the game logic package, the central place is occupied by the party class, which is a single game party. This class is responsible for the start and end of the game, for the calculation of the rating, for coordinating the events of eating an apple and the expiration of the apple's lifetime, 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 (actually, a snake and an apple). This is more in line with encapsulation requirements, but made this class even more like a controller in terms of MVC.

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

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

The package with interfaces contains separate classes that are responsible for the display and processing of the user interface, depending on the situation. In the same place pressing of iron buttons on the device is processed.

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 incoming commands in the same sequence, but separately along 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 - this 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 design stages, work with the stored data was assumed only once: load them into memory and no longer access slower media. This approach did not take into account the issue of independence of scenes in Unity: as a result, when moving between the scenes of the main menu and the playing field, all the necessary data had to be re-read.

Initial states

Below is a description of the initial state of the two scenes of which the game consists: the scene of the main menu (in the figure below left) and the scene of the playing field (in the figure below to 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 of the sequence diagrams start below.



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

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

Application launch

The diagram below represents the procedure for starting the application (in that part of it that is controlled by the developer). Particular attention can be paid unless the alternative exit from the script and the transition to loading the game, if there is a saved unfinished game.



An application load consists of handling two events: awake and start. The awake event processes the player object: at this point, it accesses the DataProvider to load player information, and then calls its own method that is responsible for applying the current settings, such as muting.

The Start event is handled a little more complicated:
  1. The GUInavigator in the initMain method initiates all the interfaces required in a given scene, each with a sign of inactivity.
  2. Next, he 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 script is interrupted.
  3. Conditionally at the same time, Start events for all initiated classes of interfaces are processed — they are additionally initialized inherent to each specific interface separately (including the interface of records loads data on the table of records using dataProvider).
  4. The main menu interface is assigned a sign of active, and it is displayed to the user.


Running game

Next, we consider a sequence diagram illustrating the actions when starting a game. The starting point is loading the scene with the game.
In this diagram, it is worth noting an alternative scenario of loading a saved game — this is necessary for the case of interrupting a game, for example, with an incoming call.



The first trigger events are Awake for Player objects (loading all settings, similar to starting an application) and Snake (Init () method - initializing the tail by default from two segments). Then the Start event is triggered to select a controller (for example, FourButtonController is used), load the GUI and initialize the game. A little more about handling this event:
  1. Controller selector in the Start event, checks the player settings and loads the desired control controller.
  2. The GUInavigator in the initGame method initiates all the interfaces required in a given scene, each with a sign of inactivity. The definition of which method to call is based on the parameter set through the editor. Initialization and activation of the active interface is similar to the launch of the application.
  3. The Start event then processes the party object. First of all, it checks if there is a saved game.
    1. If there is a game, then party calls dataProvider for recovery, which, after reading the data, calls the party's Restorebackup method.
    2. He, in turn, calls the analogous method of the Snake object, and that one along the chain for all links.
    3. After data recovery, control is returned to the party object, it includes a pause and waits for the player to take action.

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


Game Cycle: Snake Move

The game logic is centered around the processing of 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 handling of collisions of the head of a snake with fruit, walls and its own tail.



Let us examine the collision event in more detail:
  1. When an event occurs, it is first checked who we are faced with: if it is an apple, then the Eatfruit method is called, and if the tail or wall, then Killme. In both cases, the collision information is reset to the logs.
  2. Apple case:
    1. Snake sets a flag to add a link and calls the party's Eatfruit method.
    2. Party increases the party's rating and calls its own createfruit method, 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 the event handling to the party object in the Endgame method.
    2. Party sets the pause mode and, through the interface navigator, takes the player to 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 snake speed is checked: the moment of transition has come or not
  3. If yes, then check the flag of the need to add a link
    1. If necessary, a link is created that is analogous to the one following the head.
    2. Only the head and one newly created link are shifted.

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


Conclusion

A lot of logic remains outside the scope of this article: it is logging, both internally and using external analytics; this is the work of the interface navigator, which encapsulates all the features associated with the two scenes; This work with data, with the device, display of advertising and many other features. If anyone is interested, I can write about it separately.

The main problems identified are:



What conclusions for future projects, I did, based on the foregoing?
  1. You need to know the platform before you take on something serious. This is obvious in general, but just in case I decided to repeat it, maybe someone will save the top MMO from experience without launching another killer.
  2. It is necessary to design the separation of an application (even a simple one) into components. At this stage, the main thing is to minimize the number of interactions and data flows.
  3. Thinking through the structure of classes, you need to immediately think about how objects will relate to the 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 ready to change it when new conditions arise.

If you see some other not optimal solutions, then please speak in the comments, we will be happy to discuss.

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


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

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


All Articles