This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.
Congratulations! While writing the cycle of developing our mobile game, we’ve finally arrived to a technical section. It is time to discuss the under-the-hood of Uprising game. While it is absolutely impossible to describe all technical decisions made in the project in the scope of a single article, we’ll try to outline a generic approach that you could possibly use to some extent in your projects based on the experience of our game dev studio Knocknock Games.
This project uses 3 scenes (well, technically, it could be done in two scenes, the third was added for convenience:
SplashScene - this scene obviously contains some splash screens and performs initial game initialization tasks, such as profile loading/creation/network updates and synchronization. It also contains all persistent objects (i.e. DoNotDestroyOnLoad instances). Once initialization is complete and players are bugged enough with splash screens, the menu scene is loaded.
MenuScene - as the title suggests, this scene contains all screens, that are related to the menu workflow. Its main content is a canvas with about 40 panels for different screens (though menu screens themselves are pretty unique, some panels are reused (those are counted as one). In addition to canvases, this scene contains a reduced copy of game scene 3D elements - this is a design decision - different objects are added to game location as players progress. These objects also appear in the menu scene as well.
LevelPlayScene - this is the actual scene in the game that does all that gameplay magic. Aside from obvious gameplay-related objects, it contains canvas with all UI, necessary for a game scene (HUD, pause screen, trimmed version of a shop, etc)
Of course, there could be different scene layout as well, such as using additive scenes for HUD, menu, 3D scene, etc., but here we used our best judgment to split project into scenes and our design assumed complete separation of pre-game and game experiences, so, we found it is OK to have back and forth loading (where, by the way, some behind-the-scenes maintenance is done (i.e. manual garbage collection)
Our game uses a lot of different data, so we have a lot of data files. There are about 55 different upgrade parameters with many of them having 150 levels. All values are pre-calculated and stored in JSON files, which might not be a good fit for every project, but for this one - it is fine. Gamedata is loaded only once during initialization, so the format doesn’t really matter here.
In addition to 55 upgrades data files, we also have the following entities:
The primary data file, which contains all the necessary parameters for enemies, weapons, in-app-purchases, etc. We call it God Data class.
Dialogues data file - a file, where all speech lines are stored.
Hints data file - a file that stores game hints, shown during the loading process.
The game tries to follow concept one class - one file, so 250 classes are what we have in this title. Additionally, try to keep code files as small as possible. In my opinion, a larger number of logical entities is more manageable than files, that turn into sheets of code. I’ve previously been on a AAA project, which had a class with 13k lines in it - it is really a so-so experience.
Some DTO objects aside, there are two types of source code files (no, these are not bad and ugly): Managers and Controllers. This is the code convention we chose to ease developers' lives.
Managers - are basically singleton objects, which we assign to separate game objects on the scene.
Controllers - are classes, that can’t be singletons (i.e. bullet, enemy controllers). Those only exist as components of other objects (i.e. EnemyController that is attached to an Enemy game object).
This game heavily utilizes singleton pattern and singletons are called Managers here, so we have such classes as UniverseManager, LevelPlayManager, ProfileManager, etc. While some might argue that Singleton is not the best way to do things, I see no issues with it and try to make as many objects singletons as possible. Our game developers try to follow the rule: if something will/should exist only in a single quantity (i.e. HUD) - make it a singleton as they are much more convenient to control.
ProfileManager - well, this one is simple, this is a persistent class that handles all transaction work, related to PlayerProfile (this is necessary, so all parts of the game don’t work directly with the profile, because it might change a lot over the product lifetime.
MainMenuManager - this class is responsible for displaying the Main Menu Panel. Actually, all menu screens are controlled by singleton objects (since they exist only in a single quantity, which falls right under a concept, mentioned above). Again, some might argue, that singleton objects for the menu are bad, but I see no reason creating new objects each time you open a certain screen or write additional code to maintain references to already created objects.
Universe Manager - this is a gameplay class and it can be called a Supervisor (or Overmind, if you like). It tracks the world state - what is the current stage, which objects are available, what player has earned, etc. This class starts and correctly finished the game by sending the “GameOver” event to all other entities that should receive it. This is arguably the largest class in the project, totaling 700 lines of code. Why arguably? Well, because analytic and statistic events are captured here, which just do 150 lines alone, so, if you remove them, it’ll compete with SpawnManager.
LevelPlay Manager. Normally, game levels have multiple conditions and parameters, used to switch these conditions:
Game Start - when starting animation is played.
Game Over - when the game is over and no further input (or other actions) should be captured.
Game Playing - when a player is currently playing the game.
Game Paused - when the game is paused and no updates to logic or animations should be set
There can be many more conditions, depending on the project, but you’ll eventually end up coding a simple Finite State Machine that does level switching.
Spawn Manager. This is the second-largest source code file in the project and one of the most important because it does make the game fun. This file contains all logic used to spawn enemies on the field, so their power, type, speed and quantity are defined here. This file is 600 lines of pure logic, which maintains enemy waves.
Sound Manager - well, this class obviously controls sound playback in the game. It is engineered so that it keeps references to all existing sound controllers (in order to maintain sound sanity, the sounds are divided into objects they belong to.), which are derived from base SoundController Class. It looks like this in Inspector:
Again, it is arguable, that the object’s sounds should be attached to the object itself and controlled by the object. I.e. if enemy shoots - he plays bullet shot sound or something like that. I’m against this approach, because:
Not all sounds should be played every time (i.e. if you have a lot going on in the game, you might want to skip some sounds and a game object is not aware of other sounds, so it requires additional logic.
If you have multiple objects of the same type (i.e. enemies), means that several controller instances will be created with references to the same sound sets, which results in higher memory consumption.
EnemyController - this class controls the behavior of every enemy on the field, This includes not only logic but also visuals (i.e. movement animations, pfx). Some might argue that visuals and logic should be separated and I generally agree with that. If it was 300-400 lines of code more, I’d requested a refactor, but the class is 300 lines of code long, so it wasn’t a big deal (again, best judgment gents)
RocketAmmoController - this is the only bullet controller we have in the game, because rockets are flying slowly and should hit on impact. Gun and rifle shots are instant in the game, because there’s really no point of calculating bullets that hit enemies quite quickly. This controller manages rocket bullet flight and plays explosion effects once a rocket has hit the ground or enemy.
The overall principle which I follow when structuring a project - is to keep it as simple and maintainable as possible and the following basic rules:
Create final implementations. If you are on the post-prototype stage and actually develop a commercial product, implement all features as they are final, don’t use temporary hard-code as it will haunt you down till the end of the project. I.e. if you need to implement player parameters to be used on the level, first implement storage of those parameters (i.e. database, config files, whichever is applicable)
Constantly Refactor code in the process. Game products are the once that tend to change a lot during development, so no architecture, defined at the beginning of the project, is final. The golden rule here is that if something doesn’t fit the approach - refactor it immediately. If you leave it as it is, soon this component will be so deeply integrated into the project, that you’ll be afraid to ever touch it.
Avoid large source code files. Large source code files should be refactored to be split into smaller components. The most obvious example is Game HUD. I.e. in our game HUD has the following components:
Weapon Selection Panel
Mesh Mode Panel
All the above contain buttons that can be pressed, interactive elements, that are animated, based on different conditions, status labels constantly updated. But this all is HUD. This is a lot of quite difficult logic here. If we’d keep everything in a single HUDManager, it’d easily surpass 5000 lines mark, which goes far beyond being manageable.
Keep the project’s sanity. One word to that: Final! All the project structure should be final, all file names should be final, all texture import settings should be final, etc. If something falls out of this concept - it should be made final immediately. Also, there should be no unused resources in the project - those should be physically deleted the moment they become unnecessary.
Well, I hope this article was useful to give you one of the possible options on how to organize your project. Don’t forget to download our mobile game Uprising and read the whole cycle of its development. Till next time!