📜 ⬆️ ⬇️

Creating a shared library of code gameplay and more

The other day I talked to Artem Vorobyov, the technical player of the zGames game studio, a member of the Softeq group of companies, who, without any doubt, shared his team's experience (over 5 years of developing mobile games for mobile phones, consoles and other fashion devices). We present to your attention an elegant instruction with specific practical advice.

1. Motivation: why it is necessary


We love copying good decisions. Programmers call this "code reuse." This article will discuss how to reuse the library of code and effectively expand it.

The task of creating a library of code is usually complicated by the fact that:
a) The library is used and expanded by several people.
b) The library is involved simultaneously on several projects.
')
Our library of common code has existed for four years already. It all started with a couple of classes on Objective-C. Then we switched to C ++ and increased the library several times. Now we work in Unity3d, and the library of the general code totals already about 400 classes.

2. Why create a library if there is a game engine?


The game engine contains a lot of useful components that are reused on different projects. But the engine, as a rule, provides solutions only for the most common tasks: physics, rendering, sound, network, general architecture of objects, etc. However, it is often necessary to expand the circle of these decisions or create some kind of new subsystem.

Consider, for example, the implementation of logs (eng. Logs) in the Unity3d engine. There is a Debug.Log method that displays messages in the Unity console. But the engine cannot write these messages to the specified file. Also, the implementation of the log in the engine is not able to filter messages. These and many other log extensions can be transferred to the general library.

3. Name: KST - always zbs?


Start with the name. You can, of course, call the library something like “CommonCode”, but it would be better to come up with a more “own” name for the project. This will make it easier for your team to talk about the common code library.

Our library is called zLib - an abbreviated version of the zGames Library. It sounds nice, although it slightly coincides with the name of the well-known library for data compression. We at one time somehow ignored this fact, and, in an amicable way, it would be worthwhile to come up with something more unique. Therefore, choosing a name for your library, make sure that it will not overlap with the names of other libraries with which you happen to work.

4. The scheme of integration into the project. Repositories, externals, SVN vs Mercurial


The next question is how the library will get into the project. There is a simple way that often begins, copying code from a project to a project. Here there are several problems: fixes bugs and new components are quite difficult to exchange between projects.

The method is more advanced - integration of the library through a third-party repository. In SVN, this scheme is called “external inclusions” (English externals), in Mercurial - “sub repositories” (English subrepository, subrepo), in Git - “submodules” (English submodule). Using this technique will allow you to make the project repository composite, and as a result, it will be able to include not only its own code, but also the code of other projects (for example, a common library).

The library will be a separate folder somewhere inside your project, updated from a separate repository. You can specify not only the address of a third-party repository, but also a specific revision (not necessarily HEAD), with which your project should work. All our third-party repositories are located in the same “Externals” folder inside the project (Externals / zLib, Externals / NGUI, etc.)

image

5. Moderation code. What code to put in the library


One of the most important questions that arise when creating a common code library is which code to put there. I happened to see several unsuccessful attempts to organize a common library of code. Almost always, the reason for the failure is that they put what was needed only within the framework of a single project into the common code, and no one will need it anymore. To make the library work, you only need to add code that can be actually reused.

It should be noted that in games there is usually a lot of code specific to this particular game. So do not rush to bring to the library your first implementation of match-3 / runner / shooter, if you do not plan to develop clones of this particular gameplay. Approach the task more systematically: try to isolate common parts in solutions.

We follow a simple pattern:
a) Any problem solved for the first time is carried out within the framework of this project and is “honed” under its goals.
b) If we are confronted with this task again, it is, if possible, summarized and submitted to the library
c) As long as the approach cannot be generalized, the solution code is copied from project to project, continuing to adapt to specific needs.

In our practice, some architectural solutions went through up to 6 iterations of changes on different projects, until we could trace the patterns and decide the library.

6. Moderation of file structure: not a single code


An important task is also to determine the structure of files within the shared library. You can choose any approach, it is important that it be specific, and all team members understand it. Our library root folder looks like this:
zLib / _Externals
zLib / _Resources
zLib / ZGLib
zLib / ZGLib.Collections
etc.

The _Externals folder contains third-party libraries that we integrate into ours. The _Resources folder contains library files that are related to the library but are not code, for example, text files or images.
The ZGLib, ZGLib.Collections and other folders designate namespaces (English namespace) within the library and contain classes related to this namespace. Inside the namespace the folder structure can be arbitrary.

7. Limitations of architecture: when not in focus


One of the main objections that I have heard about the common code library concerned the limitations of the architecture. Indeed, over time, components that form the framework of the application appear in the shared library so that you have to stick to its framework when writing your project.
The trick is that with proper organization the frame will not limit your possibilities at all. The key to the correct implementation of the framework is modularity. Make it so that it rigidly defines the interfaces, but does not limit the functionality at all.

I will give an example. We have a global object framework - the singleton load subsystem (English). All singletones created by us must implement a certain interface (ZGlob) and be placed in a special place in the game scene (of course, this does not apply to singletons supplied with third-party frameworks). Thus, we create a single point of loading and initializing the application. A single interface allows us to flexibly manage the order of loading global objects, in no way limiting their functionality.

image

8. Single Style: Crucially Important


A prerequisite for creating a library of code is the formation of a common style of writing code for the team. The question of a single style, in principle, is relevant for the team and in the absence of a common code library: at least it will be easier for the guys to switch between projects. And when creating a common library, the issue of style is really vital.

General code should be written in the same style, period.
If you do not have a common style yet - start to form it. Create a document in Google Docs or any other online document service and put in paragraphs the general rules that you are already following when writing code. Then gradually agree on the creation of new rules and expand the document. For the successful formation of a common style, you need to appoint a person in the team who will make decisions if the team cannot reach a common opinion.

9. Possession of subsystems: “Very nice, king”


If you do everything correctly, the library of the common code will be divided into subsystems. Each subsystem must have an owner - the person responsible for everything that happens to it. Changes in the subsystem should be discussed with it, although other people can make them. The owner of the subsystem carries its philosophy, knows the history of its creation and best understands its goals.

10. Dependence on third-party frameworks: Facebook


At some point you will encounter the need to make decisions in the common code tied to other third-party libraries. This is typical, for example, for a common code for working with Facebook that works with the classes of the Facebook SDK directly.

For ourselves, we solved this problem by dividing the common library into parts, each of which lies in a separate repository.

We have the zLib kernel, which contains the main classes that are not associated with any specific third-party service. There is zLibFacebook, where the Facebook SDK and our general classes for working with it are. There are zLibTwitter, zLibChartboost, etc. - already about 10 different modules zLib'a. The zLib kernel is always added to our projects, and, if necessary, additional modules are added to individual sub-repositories.

image

11. Registration systems add. of behaviors


In addition to the modularity of the most common library (zLib, zLibFacebook, etc.), it is also sometimes necessary to create a modular architecture of a separate subsystem within the common code library. An example of a modular architecture of one of the subsystems given above is the framework of global objects.

Another, even more powerful example, is the gaming analytics implementation system. Imagine that you want to create a common analytics system so that the recorders of some events can be shared and reused on different projects (in our case, placed in zLib). Some events may also be common, but tied to a third-party service (we are in zLibFacebook). The rest of the events should be specific to this application (placed in its code).

To solve this problem, we create the skeleton of the subsystem in a shared library with a controlling singleton (in our case, this is ZAnalyticsManager). Next we create a base class for the event recorder (ZAnalyticsTracker). Loggers can be dynamically added to the ZAnalyticsManager. In Unity3d, this is easy to do through the composition of game objects (GameObject). For other technologies, you can choose the approach generally accepted for these technologies.

To support various third-party analytics services (Flurry, Google Analytics, Localytics, etc.) we create an event sender interface (ZAnalyticsSender). This interface is implemented for each of the analytics services, and we add to the ZAnalyticsManager those sender objects that are required in this project.

Thus, in order to raise the analytics system, we need to create a singleton control object and add registrars and senders to it - they can be implemented both within the common code library and within the specific code of your project. The main thing is to support a common interface.

image

12. Branching and project stability: ghosts of the bug


When sharing the same code between projects, there is a huge plus in the fact that the bug fix in the common code simultaneously fixes this bug in all projects. But there is a negative side: when you add a bug to the common code, it appears in all projects. To prevent the uncontrolled introduction of bugs, you can make branches for each project in the common code repository. Then the team working with this project will decide for itself when to merge changes from the main branch.

image

13. Deprecate changes: smoothly and painlessly


Sometimes, some parts of the common code have to be changed - a better name for the interface is invented or the general structure of the subsystem refactorizes. In this case, it is best to mark the old interfaces as forbidden (deprecate), but leave them in the common code for a while to allow the developers to transfer to the new rails without any serious consequences. In C #, this is done using the Obsolete attribute, other technologies have similar mechanisms. When making major changes, it is worth preparing a written instruction on switching to a new implementation.

There are times when a very cardinal refactoring is required, and the old interfaces cannot be saved. Here you have to manually control the stabilization of the new approach in each of the current projects. It is better not to implement the integration of serious changes at the final stages of development (before releases, Milestones, deadlines), but to postpone for calmer periods. Here will help the system of branshe and versioning of the general library.

14. Coverage test: "give two"


We all know about the benefits of writing automated tests. And we also know how rarely they are written. And, if you think about it, it is not always necessary. For small projects, the cost of manual testing and stabilization may be lower than the cost of writing automated tests. However, for a shared code library, automated tests will almost certainly pay for themselves.

Therefore, if you plan to introduce automated testing, but it still does not work out, start by creating a common library and write tests for it. Good managers will understand the benefit and give you time for it.

For games, automated tests are a separate sore point. Compared to business applications, game objects have more interconnections, and it is more difficult to test them separately. We are currently covering Unit-tests with utility classes and planning to integrate with the integration tests those objects of general game logic that can work separately.

15. Common code library: no time to smoke bamboo


An interesting bonus for creating a shared library for us was getting rid of downtime. Of course, there is always a category of developers who are in the joy of "doing nothing" at work - they, of course, will have difficulty. But for people who are out of work sour - this is a universal solution. If a person is not engaged in a project, he may be taken for the development of a shared library.

To organize the process effectively, the general library should have a manager, best of all, a technical director. Like any project, the shared library has bugs, there are requests for changes and features. Now our pool of tasks for the common library consists of 60 items and is gradually increasing. There are tasks both for a couple of hours and for several weeks, which makes it possible to utilize even short free intervals in the work on major projects.

16. zLib in a section: what subsystems we have


In conclusion, we give a list of some subsystems that we have brought to the library of common code:
a) Logs and asserts
b) Wrapping over asynchronous tasks
c) Working with JSON
d) Global Object Loader
e) In-game developer interface system (cheat UI)
f) Gesture recognition system
g) Analytics System
h) Window system
i) Application Configuration System
j) Event system
k) Wrappers over third-party frameworks

We would be very interested to hear feedback from readers and colleagues. Is our experience useful, what recommendations can you give, how is your shared code library organized? We welcome comments!

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


All Articles