LongCut logo

Alexey Merzlikin - Architecture Behind Our Most Popular Unity Games

By Digital Dragons

Summary

Topics Covered

  • Architecture must match project goals
  • Single entry point enforces consistent data flow
  • Hierarchy enables modularity and reusability
  • Controllers implement RAII to prevent memory leaks
  • Refactor incrementally without stopping releases

Full Transcript

[Music] game development was always my passion I started creating games a long time ago as an India

developer and of course the code in my first games was really bad with no architecture whatsoever so the perfectionist in me quickly realized that I needed to learn programming and

Architectural patterns to write well organized structured code and so I started to consume a lot of materials books posts conference

talks uh and try to apply as much as I could in practice but still I always dreamed of seeing how the most popular games in the world are made what are

architecture they use and how their developers structure their project and write their code so many years later I'm ready to

tell you how we develop our games and not just one but multiple of the biggest mobile games in the world right now so good afternoon and welcome to my talk on

the architecture behind our most popular games actually I'm glad to see so many people here uh happy I'm happy that all of you are interested in the

architecture of big games as I do and who works with unity here wow other engines great uh even though the games

in Focus today are made with unity the framework itself is engine agnostic so you're in the right place to learn what challenges we face while working on our

games and how we solve them it's not working okay my name is Alex M I'm a game developer come

on yeah definitely I'm game developer with more than 10 years of experience right now I work at pla as a tech lead on one of the top

grossing games in the world and what is more I've been working on multiple mobile casual heits throughout my

career and I'm the author of Game def.

Center go check it out after the talk to learn more about performance optimization and good coding practices here it is

so why do we need a good architecture let's first talk about actually what is a good architecture raise your hands who thinks that a good architecture should be very flexible

with a lot of abstractions different layers allowing to add changes and replace implementations Almost in no time all right and who thinks that a

good architecture is a bunch of singl tons resulting in a very simple structure with no over engineering great actually all of you

are right because our project and goals yeah actually our project and goals Define what is a good architecture there

is no Silver Bullet that fits all the projects and solves all the problems and obviously a project for game Jam or hyper casual prototype would have absolutely different requirements to the

architecture compared to a realtime strategy game or a Content heavy mobile casual game that is scaling in production and is expected to be

supported and growing for many many years so why do we need a good architecture for our games pla long-standing top grossing games

highlight the need for continuous play players engagement through regular content updates and today's top gross in Market shows

the need for constant roll out of new features meta changes and more and more content every month and as game developers we all strive to create immersive and engaging

experiences for our players but as the complexity and dynamic nature of games continue to evolve we are all faced with challenges when it comes to managing large amount of modules in our games as

well as keeping the scalable maintainable allowing collaboration in big teams and managing the growing complexity as game

evolves so that's where architecture comes in by choosing the right architecture we can optimize our game development flow and create better experiences for our players by improving

and adding new functionality to our games easier and today I want to introduce you to the hierarchical model view controller architecture and how it helps us to

maintain and grow various games the controller tree is a key component of hmvc and it helps to manage data flow in the game you will be able to create more

complex games and facilitate collaboration of many developers using this approach and let's start with hmvc it is an extension of traditional model view

controller architecture and it offers more robust and flexible solution to the challenges faced by developers while working on a complex systems by delegating tasks to child

controllers Each of which is responsible for a specific part of the application and the main difference to MBC here is that Triads of model view

controllers are structured into a tree and controllers Drive the data flow by spawning instances of child controllers and delegating the behavior

and data processing to them instead of having many entry points where different systems depend on various separated M Triads we have a single entry point into the game and when a particular

controller is instantiated it communicates with other systems to get the needed data and the controlls tree refers to the hierarchical structure of

controllers inside hmvc and we use this term internally the controller tree as the name of our hmvc pattern implementation this structure helps to

enforce a single entry point into the game ensuring that all data flow is managed in a consistent and organized manner the controls Tre also provides

game developers to maintain separation of responsibilities between modules making code more maintainable and easy to support

and let's look at our hmvc Tri and isolation models and controllers are always plain csharp objects while views are mono behaviors that only provide

Unity API and rendering capabilities with no game logic at all what is more multiple controllers might work in reference the same model or view it means that we don't create a separate

type of model and view for each controller and we haveen a lot controllers that don't have any View at all for example the controller that

makes a server request and then updates the model without using any UI and whatsoever and the controllers tree was

used across various games at PLA and what is more across various Frameworks not all of these games were made with unity and to name the biggest ones board

Kings best fins solitary ground Harvest slom Mania Bingo blits and many more smaller

games and here are the metrics of our game in 2023 to show you their High pace of development more than 40 people pushing

changes to the client repo every day we are able to release we have more than 50 features added the last year and

we are able to release new update every two weeks for many years and what is more between every release cycle we have from one to three technical releases to

test new functionality on smaller audience and what is more 100% of features utilize Dynamic assets so they can be reskined for every event easily

and talking about events we have an incredible life Ops providing new events to our players daily and let's dive deeper into the

implementation details of controll his tree with the example of another game board Kings raise your hands who have seen this game

before all right happy to see your hands and for those who don't know BK is an online game uh where you build your own board

and also you can visit your friends to help them build their board or to destroy it there are a lot of stuff to collect like stickers art

buildings and many more we also have a rich meta game with life events being open to players consistently so the core mechanic is a board and its management as well as visiting

friends we also have a lot of mini games for example the steel mini game that is a classical example of pig three game in which you have to guess where the biggest price

is and it is launched in a sear scene we also have a daily bonus in form of a lucky wheel that is also launched in a separate

scene and an album with stickers that you can collect by playing the game or by exchanging with your friends so based on the example of these

features you can see why the architecture is called the controllers tree there is a root object that creates our branches for each feature for example the controller that manages the

board by spawning instances of controllers each doing its part of the job for example in instantiating buildings upgrading buildings showing

the UI and so on and the board right now also starts the steel mini game Branch because the only way to trigger that mini game is to

step on the particular tile on the board and only one controller is needed to be launched to show the whole miname flow and it does its job by spawning

instances of child controllers and delegating the whole flow to them and the beauty of this approach is that we can decide giving

out the steel mini game as a reward and just start the single controller in another Branch for example in the collect rewards popup and that's it what is more each controller

encapsulates game objects lifetime preventing memory leaks by instantiating using and then destroying acquired game objects right away of course for for

frequently used objects we use pooling to avoid an excessive garbage collection and raise your hands if one of the top crashes in your game is due

to out of memory exceptions all right actually I expected more hands because it's very familiar case uh for our games uh that have a lot

of assets and a lot of content and in many cases the reason for it are resources that are not properly unloaded after being used however

each controller encapsulates Resource Management as well and all resources built in and dynamic ones are following the same approach we load acid bundles

during crun time only when needed and unload them right away after being used preventing memory leaks reducing memory

consumption we also have a Tre viewer in the editor it provides means to inspect what is currently active in the game and also it provides easier debugging

and here's how our steel mini game looks next to its tree there are multiple controllers that are active all the time for example one

of them listens to the event of the player stepping on the particular tile on the board and once it's triggered multiple controllers start and the branch continues to grow as subsequent

game logic is executed so we roll the dice step on the tile and you can see that controllers

encapsulate async logic and when they finish their job they disappear from the tree one by one and the whole miname is

over and now all controllers are removed and all resources that were required and loaded for this mini game are unloaded too now let's get even more Technical

and dive into the implementation details controllers are the entities that drive the whole game Flow and we have two types of controllers the first one is a

controller's base its life cycle is managed by its parent it stops in two cases when its parent stops or due to an exception and

actually when any controller is stopped all of its children are being recursively stopped too and an example of a base controller can be the controller that listens to

the event of the player stepping on the special miname tile so this controller will be active all the time while it's parent board is active and the second type is a

controller with result it extends the base one and obviously have a result that returns back to the parent when it finishes its job and as an example of this controller it can be a login

controller that makes a server request and Returns the data after that and and when the data is returned the login controller stops

itself this type encapsulates an async operation and another example of such controller can be the whole steel miname

controller it loads resources and when the whole miname is over it stops itself and unloads all the acquired resources controllers are asynchronous so the

result can be awaited in the parent using task or unit task depending on the implementation and our experience as well as controllers to Evolution inside

the company shows that the simpler the controller's API is the better the architecture and it's easier to use overall these are the main methods of

the controller's API and they all have the internal access modifier and internal means that it is impossible to invoke initialize start stop or dispose from the outside of the controller tree

package uh without Hax of course and there are no public Methods at all we also restrict creating controllers via Constructors and saving

its instances so the only way to start a new controller is to use the protected method execute and protected means that you can invoke it only from another

controller so that all new instances are inside the Single Tree structure and will be correctly processed when one of its branches or the whole tree is being

stopped the only exception to this is the root controller that is created in the apps entry point and it must have some public method so we can create it outside of the tree and actually start

the tree itself and execute encapsulates the controller's life cycle and uses all internal methods under the hood like initialize start stop and dispose and

this approach allows to eliminate cases when Developers misuse the API and create controllers that are not stopped or disposed properly so the single method leaves no space for a

mistake we also have the protected property with the canellation token so any point at any point in the controller's life cycle you can check if its asnc execution is cancelled for

example we don't need to play a sequence of idle animations when a player clicks to close the feature immediately so by checking the token we can interrupt our asnc animation flow in

ly we also have other logic in controllers and they are all located in private methods that are started in and invoked in initialize and here's the example how

it's used in the code we create a controller with result and to wait until it finishes the login sequence after that we can launch a base

controller that in turn initializes groups of features and that's how the tree grows by delegating the flow to child controllers the root controller itself

is created in the apps entry point which can be a monob behavior or a static class with initialized onload attribute we manually create the root controller

and invoke initializing start because those methods are exposed as public API in the root controller and its responsibility to start the tree and delegate the flow to child controllers

in that case it's high level controllers that initialize groups of other features and let's talk about advantages this approach brings

us the controller tree enforces a single entry point into the game ensuring that all data flow is managed in a consistent

and organized manner a new controller can be created only inside single existing tree and of course you can create multiple root controllers and

create therefore multiple trees but we found that the single tree is the most convenient approach and the controllers tree allows for the logic in the game to be

decoupled making the code less Tangled and easier to understand what is more multiple controllers can be developed and tested independently

without having to worry about all the intricacies of other controllers and it might lead to increased development process efficiency as you might work on

multiple parts of the game at the same time without any conflicts without any issues and the structure of control tree

promotes reusability as logic can be easily reused and shared across multiple features and I personally hated in the

past adding new items as a rewards in my games because it always required changing UI in every feature and a bunch of classes had to be modified to support

new items but now we have the common rewards controller that allows to give players items that they have purchased or earned by playing the game and it

really makes life a lot easier as all you have to do in your feuture is to start the common reward controller and be sure that it already supports all existing items in your game and what is

more you don't have to worry about any future items at all because they will be implemented in one place in a reward controller and your feature will support it automatically without any code

changes in your features and it's amazing because adding new items in big games like ours is really timec consuming process and also

it's very error prone so the set of common controllers can cover a lot of General use cases in your app therefore you might save a lot of

time what is more the clear separation of responsibilities in the controller tree makes it easier to support and

update individual parts and of course a rewards controller would be a perfect example of maintainability you don't have to change your feature code at all

to support all the new items that we might add the control tree helps ensure consistent implementation across modules as features have a s SAR structure and

what is more developers get used to easily navigate navigate inside any feature by inspecting the code and the flow is clearly seen in the code so all

features have a similar high level structure of course on lower levels features differ a lot that's normal but for this another Advantage comes into

play that greatly synergizes with the current one the hierarchical structure of controller tree provides a clear and organized flow for

managing the data in the game making it easier to understand and maintain over time you can even create a static or runtime analysis tool that would capture

all controllers and build the entire game tree graph in the editor just like the one from the video of board Kings and such tool helps you understand and

investigate the game Flow even easier and how many of you had to dive into memory profiler to find sneaky memory

leaks all right what a great audience uh because I'm personally keen on performance and optimizations and I'm really happy that you are not only interested in the architecture but you

also have experience with memory profiler so it means that you care about performance as I do and the controller tree greatly aligns with the resource

acquisition is initialization pattern and the basic idea behind this pattern is to tie the lifetime of a resource to the lifetime of an object and this is done by acquiring

resources in the initialized method and releasing them in dispose so when any controller is stopped all the resources are automatically disposed and this approach helps prevent

resource leaks which might occur when developers forget to release a resource or an exception happens before it is released

by using this approach developers can ensure that all resources are properly managed even in the presence of exceptions and talking about

exceptions triage statements can be used on any layer of the tree giving developers full control of where to stop the particular branch of the tree when

exception happens and paired with the previous point we can be sure that when we stop any controller due to an exception all of of the resources loaded

by it or its child controllers will be correctly processed and disposed so in the end the cognitive load on developers is lower now please raise your hands who

have used dependency injection Frameworks in your games oh amazing and how many of you had uh issues with long startup

times all right actually it's a very common case for big games uh it's usually not a problem in the beginning but as a game continue to grow uh at one

point we always hit this issue and uh due to the isolated nature of modules in the controllers tree it is possible to have multiple subcontainers

it means that you can bind and resolve features as needed in a lazy manner leading to better resolution times

better memory management and therefore better performance what is more it adds one more of encapsulation as you cannot inject types

that are bound to different subcontext so once again all the communication between multiple features must be thoroughly thought through and well defined via

interfaces leading to better structured code and there will be less chances of circular dependencies that might lead to Spaghetti

code and what is more using this approach approach doesn't mean that you must use it all the way for each feature for each piece of

code uh for example you can combine it easily with other architectures for some of its parts like ECS that can be implemented for just particular mechanic

or a particular feature and you can launch the ECS world just in its own feature controller so that ECS part will be completely encapsulated inside own

sub tree and launch only when needed saving resources we talked about advantages now let's add some disadvantages because obviously we have

some while the controller tree doesn't require an excessive amount of boiler plate there are still some components that are necessary for facilitating the

infrastructure of the tree for example an arguments class that is implemented for each type of controller and passed as a payload

however to simplify the process of writing bowler plate code we can utilize different techniques like life templates in Rider code generation attributes and

even AI tools and by the way who is using any sort of AI assistance in your daily work wow great we definitely should catch up after the talk and share our

experience I would love to hear how you use it and how it helps you if you need any interaction between multiple features then it cannot be done

directly between two controllers because controllers have no public Methods at all and the absence of direct communication may be considered as a

controversial drawback at first glance on one hand to provide communication between multiple controllers we need some sort of a mediators we have chosen

creating models like in this slide it contains only events and methods to invoke these events so there is no other

logic and One controller listens to the event another just invokes it using the method and

um on the other hand creating such uh mediators allows for a thoughtful consideration of dependencies and what will be their

directions between our modules in addition allowing direct communication might cause other architectural issues such as

injecting controller instances into each other it means that in that case we would need to bind some controller types as single tons and it might lead to other architectural issues making

controllers less flexible and blocking the ability to start multiple controllers of the same type in parallel so in our implementation all controllers

are bound as transient so for some including myself the lack of direct communication can be considered even as an advantage because once

again all communication between features must be well defined via interfaces and it will be strictly limited via code review and assembly definitions to

prevent circular dependencies that might lead to Spaghetti code nevertheless it's worth noting that it is a significant restriction of this archit tecture and its controversial

nature should not be ignored because there are no public methods in controllers it means that you cannot test any method in isolation you can run only the whole

controller obviously we can move all our business logic into models and make controllers just invoke model methods and models are plain CP objects and they

are absolutely unit testable they're can be tested a lot easier than controllers but in practice you would always still have some parts of business logic

leaking into controllers anyway and it also leads us to an argument where business logic should be located when using MVC in controllers or in

models of course we have some workarounds how to test controllers for example we can introduce an interface just for the purpose of unit testing or

we can use the reflection we can use the attribute internal visible to but I find all these approaches is not really great practice for example in one of our

project projects we have public API initialize start stop and disos exposed as public API

therefore we might create controllers in tests as we wish and run them then check the result and assert that it is as expected but if you can use life cycle

methods in your test code it also means that you can use it in production too leading to various mistakes where controllers were not stopped or disposed correctly so that's why the single

method execute helps to solve this issue and very very useful to prevent any resource leaks what is more controller is a reference type and while it's not really

that heavy it still adds some overhead and adds garbage collection pressure it's not a problem in our case as new instance are instantiated pretty rarely

but if used incorrectly to spawn many instances every frame it might become an issue and it highlights once again that this architecture should not be used

blindly now let's talk about some advice that we have collected while working with the controller tree the editor tool is essential if you use hmvc like the same editors that was

used in the the video of board Kings it provides means to inspect what is currently active in the game by showing each controller additionally it allows to see each controller's state by

reflecting its properties and Fields what is more we are able to invoke any life cycle method or debug methods marked with a special attribute it makes

the debugging and testing a smoother process furthermore the clear flow through the application makes it possible to create a static

analysis tool and this tool can build you the entire game tree in the editor time without running the game and it can

be especially useful because some features are really hard to reproduce some features have complex flows and also they might require complex

configuration so raise your hands if you have uh if you ever had to ask other developers or QA Engineers to help you

set up any feature all right I have done it many many times and um of course in our

company we always strive to create configurations that are as simple as possible but we still have some features like the farm or the album that are

incredibly big with hundreds of items with a lot of complex flow resulting in a very complicated configuration so only our amazing Q

Engineers know how to set it up so with this tool you might save a lot of time and what is more you will save time of your

peers our development methodology emphasizes writing clear and organized code following code Centric approach and adopting a passive view variation of MBC

it means that the controller is responsible for communication between model and View and we strive to keep our views as

thin as possible as demonstrated in this slide you can see that our monares provide only serialized Fields so you can set them up in the editor and

methods that only invoke Unity API without any business logic and by separating business logic from views we

can test unit tests are code easier as models are plainy sharp objects and they are a lot easier to unit tests compared to

views and given the main principle of hmvc to split and delegate tasks to child controllers we can see how it greatly aligns with the single

responsibility principle and a lot of controllers each with only one responsibility are a lot easier to support and maintain compared to one

module doing all the work without splitting and of course using this approach doesn't block you from writing code that follows all other solid

principles some controllers listening to particular events to show its full screen animation just like this steel mini game controller that Waits until player steps on the particular tile on

the board but what can go wrong in that case we might have multiple features listening to the same event to show its

full screen animation and we definitely don't want them overlapping just like in the slide so we need a mechanism that would control what is currently being shown on the

screen and of course we have multiple solutions to this problem and all of them are highly dependent on the specifics of your project for example some projects implement a state machine

on top of the controller tree so that new controllers are instantiated one by one from the stack when needed other projects Implement an asyn queue that

works like a semaphore allowing only one controller to manage what is currently being shown on the screen and other controllers await its

turn so each approach has its pros and cons so should be carefully considered based on your project and your specifics and to wrap up here are the key

takeaways from today's talk don't trust anyone always do your own research when applying any approach especially when we talk about

architectural patterns because wrong decisions might bite you when it's way too late to fix architectural problems and replace your implementation and

architecture the controller tree is not an ideal architecture that fits all the projects and solves all the problems

however it helps facilitate the development in really big teams on content and meta heavy games that are growing in production for many many

years due to constant roll out of new updates and new features being added at a really high pace and the wide variety of games in the top charts of Mobile

stores prove this benefit in your next project I highly encourage you to evaluate the controllers tree approach and analyze if

it fits your project or you can even design only one feature in your existing project as this approach is not versatile enough to support other

architectures as part of it like the ECS example before but it also versatile enough to be started into it its own

entry point as one feature in existing game and actually we have multiple examples where big and successful games were refactored into this approach one

small step at a time refactoring features into its own sub branches one by one and adding new functionality completely following the controllers

tree approach and while still being able to release new updates every two weeks without any delays so I would be happy to hear about your experience with the controller tree

approach and it would be great to see other types of projects that benefit from controller tree so let's connect and also let's meet up today I

would love to hear about your games your architecture and your workflows so thank you very much and have a very very great rest of the day

[Applause]

Loading...

Loading video analysis...