Modular architecture in Unity

In the process of unit development, developers go from “god objects” to designing complex, flexible, abstract systems. Over time, these systems acquire their own unique features, standards and tools. Forming a framework or even an engine.

Such frameworks often have excessive functionality for specific tasks. There is more and more documentation, just tons of documentation! Despite this, the entry threshold for a newly arrived developer on the project is steadily growing. And also there are those who immediately refuse to work on such frameworks, with the words “Why should I climb into this abstract swamp!?”. And I understand them perfectly.

And what happens, trying to avoid problems with hard code, we found other problems, without really increasing the speed of development!? Problems that take a lot of time to fix. Those. we have gone from extreme to extreme from “hard code” to a framework.

Here we are faced with the same problem. But we got the idea to go the other way. This is how the concept of a new modular architecture was born. Well, maybe not so new, but it has its own unique features. And this is not exactly what you can find on the Internet on the topic of modular architecture, we will talk about something else.

Tasks of the modular concept

So to the point! The following tasks were set for the new concept:

  1. The developer of secondary modules does not need to delve into all the intricacies of the framework.

  2. The division of responsibility between developers should be clearer.

  3. The module developer is given the opportunity to choose the tools himself.

  4. The project should be divided into loosely coupled modules, ideally there should be one single point of connection.

  5. Secondary modules should be developed in parallel independently from each other.

These were the tasks we set ourselves from the very beginning. But in the end it turned out even better!

Description of the modular system

What happened as a result:

1) Let’s first look at the structure of the code. The modular system consists of the main module and the secondary ones, in this case the game ones. The main module contains everything basic: initialization of all services, plugins, start scene, menu scene, etc. It also describes the abstract classes that secondary modules should inherit from.

Consider a simple example that fits perfectly with the modular concept. Imagine a project with a set of mini-games (just select a mini-game from the list in the main menu). The games are independent of each other. And all games have different gameplay. These mini-games are game (secondary) modules.

Each game module has a class that inherits an abstract class, which is described in the main module. Due to it, the main module perceives them as game modules. The minimum set of functions of an abstract class from which the game module is inherited. This is the entry point to the module, the method that is executed when the module starts. And the exit point, upon completion of the mini-game. This is the minimum set of functions for the interaction of the game module with the main module.

Also in the main module there is a MiniGamesManager that launches mini-games and does something else with them. For convenience, configs are used everywhere. To award points or in-game currency, modules can send data to GameData, where this data can then be saved, for example.

2) Now let’s see how it looks in the Project window. All modules are located in the _Modules folder. Here we also see the division of modules into folders. At the very top is the Main module. The lead developer of the project is responsible for it. It contains all the essentials. The Arts folder contains arts that belong to the main module, as well as to other modules of the project. In the Scene folder, the start scene and the menu scene.

In the Scripts folder, everything is basic for scripts. Modules are also divided into assemblies. There are already several of them in the main module _Main. In a modular system, it is easier to divide a project into assemblies. Third-party plugins and assets are installed for the entire project, by default in the Assets folder.

The game modules contain only what is needed for these modules. No need to delve into all the folders, look for the necessary file among hundreds of others. The module can be implemented as a scene or as a prefab if the mini-games are too small. You can run them right in the menu scene. This is more beneficial in terms of optimization.

However, there is still a problem that has not been completely solved. This is a duplication of resources. Secondary modules use files that are only needed for those modules. These files are located in the folder of the module itself. And they can also use shared files that are in the main module. But with this approach, you need to be more careful so that there are no duplicate files. Most often this happens with files for the UI. I solve it through Addressable/Analyze and AssetStudio. However, duplication is not so bad.

If the project uses more than one submodule (Git submodule), then following the concept, it is logical to move all submodules to the Assets/_Modules/_Submodules folder. In our project, we were sure that we would use one single submodule, so we left it in the main module.

3) Resources in Addressable can also be grouped modularly. But it’s not all that simple. Depends on the project.

4) Let’s go further, the git is also easier to work with a modular approach. Each module has its own branch. The coolest thing happens when we index files for a commit. We just commit those files that correspond to the module directory. It’s hard to go wrong here. Thus, conflicts never arise.

For better control of parallel development, it is required to periodically merge branches and divide them into stages.

And now we get such a fairly clear line of dividing the project into modules: in code, division into folders and in the git.

Standardization

One of the advantages of this modular concept is the freedom of choice of tools for developers of secondary modules. But still, some standards are needed on the project. Standardization is divided into 3 types: mandatory use, prohibited use, recommended use. All these standards are negotiated before the development of secondary modules.

Here are some standards that were on our project: everything should make its way through the links! It is forbidden to use Unity Events and the Button component, instead we must use our Pointer system. Inheritance should not exceed 5 generations (one way to get away from abstract swamps). For global events, you must use the (our choice of) Messenger. Loading resources only through Addressables. Recommended to use: Zenject, Unitask, for UniRx reactive properties or our NextVar, DoTween, Unity Input System, analytics events can be sent through the main module or by yourself.

Development speed

For clarity, graphs, development speed curves were built. Of course, this is all conditional. But notice how steeply the modular architecture graph rises after the design phase. And how much less time it takes to polish. This is achieved mainly due to the parallel development, a clear delineation of responsibility. And thanks to the isolation of modules, developers get to work faster without spending a lot of time reading code and finding resources. And also the modular architecture is the least prone to bugs.

Advantages and disadvantages

So let’s start with the positives.

1) The developer of secondary modules does not need to delve into all the intricacies of the framework / engine. This reduces the burden on all developers incl. on the leader and generally speeds up development. The low threshold of entry is great!

1.1) Developers of different levels can work on secondary modules.

1.2) No need to spend a lot of time writing a lot of documentation.

2) In theory, it is possible to parallelize the development of modules up to 100%. You can put a developer on each module. Since the modules do not depend on each other in any way.

3) Thanks to modularity (atomization), the project is easier to manage. Evaluation of tasks is greatly simplified, and this is very important. Managers and game designers can take a breath.

4) Modularity allows you to easily: disable any module, change their order or run in any order, at least from the end of the list, at least through one. It also helps with hotfixes.

5) It’s easier to work with resources, configs and Remote configs.

6) It is very convenient to use the git.

7) Easier to test, but not in all cases. But it is certainly easy to identify a bug, localize it and appoint a person responsible for fixing it. And in general, this architecture is less prone to bugs.

8) It is possible to reuse secondary modules in the following clone projects. Because they are isolated. In the case of monolithic projects, you would have to deal with a bunch of dependencies.

8.1) It is easier to divide the project into Local and Remote bundles in Addressables.

Cons of the modular approach

1) It is difficult to divide the project into modules. The example discussed in the article was the most suitable for the modular concept. Many projects (genres) will not be rationally designed according to such a modular architecture. And this is the main disappointment (you: – “I should have started the article with this !!!”).

2) When designing a modular system, you need to think through everything in advance. Drastic changes in the architecture, during the development of secondary modules, can lead to a lengthy reworking of modules.

3) Decreased exchange of experience between developers. Developers of secondary modules mainly develop only on specialized (gameplay) tasks, not particularly understanding the architecture.

4) You need to keep track of resources. There may be duplicates.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *