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 video analysis...