The Architecture of the League Client Update
Greetings! My name is Andrew McVeigh, and I'm a software architect at Riot.
We're in the latter stages of re-engineering the League of Legends client. We're calling this the League client update. In this post, I’ll outline the software architecture of this update and provide some background to the choices we made by pointing out some of the limitations of the current (original) client. The journey to our final architecture has been technically fascinating, and I’m excited to be able to share it with you!
Why replace the client?
We built our client in 2008 on a front-end technology called Adobe AIR, which uses an RTMP session-based networking protocol to talk to our servers. This platform served us well: it offered a rich multi-media runtime with animation and effects that just weren’t possible with HTML at the time. Additionally, it was cross platform and made building screens easy for a team that was light on artists and designers.
Fast forward seven years to 2015 and three particular issues with the AIR client have become very stark. These are the issues that our new architecture solves for, amongst other things.
Issue #1 - HTML5 is a standardized, widely adopted platform
Issue #2 - Players want to stay connected in and out of game
You wouldn’t want to leave the AIR client running constantly due to its high resource usage even when it is in the background, yet expectations around online connectedness have radically changed in the last few years. Players want to be connected to their friends in and out of game—no one should miss a game invite just because they aren’t currently in front of their PC with the client open (something we're also working to solve with our mobile app).
Issue #3 - Riot dev teams want to work in harmony
League (and also Riot) has grown hugely since 2008 and we could never have guessed that we’d end up with so many teams wanting to add so many features to the client. The original codebase was structured around a small, highly cohesive team and didn’t give the required level of autonomy and independence as the number of features teams grew.
This problem of dev teams colliding with each other wasn’t going away—it was getting worse over time as we added new features. In addition, we aspire to be a multi-game studio, which brings many other challenges and opportunities.
Incrementing towards an awesome architecture
There are a number of ways to solve for the above issues—for instance, we spent some serious time reorganizing the existing AIR code with the intention of staying on the (outdated) AIR browser. However, while exploring this option in depth, we realized that we could solve the above issues in a more powerful and sustainable way by changing our platform. We decided to do this despite being completely aware of the dangers of rewrites, because we believe the benefits to the player will outweigh the risks in the long-run.
As such, we chose to start again and build around Chromium Embedded Framework (CEF) as our front-end. This gives us a super strong HTML5 browser component that we can easily customize.
Let me now explain how we found our way to our final architecture.
What could possibly go wrong with something so straightforward? Well, quite a bit as it turned out.
So, this architecture solved issue #1, but did nothing for issues #2 or #3. An internal developer satisfaction survey found that developing in this way was less productive than the AIR client. Ouch.
Step B: We rediscovered web apps (but on the desktop)!
Our next step was to provide microservices in C++, presenting the async game protocol as a set of REST resources.
This allowed us to start building towards the vision of an "always available" client, which addresses issue #2. The foundation can bring up a system tray notification, even in the background, to indicate a game invite or a friend request.
Step C: Still stepping on toes…
The next evolution was to add an extensibility architecture.
As teams started developing microservices and features, a new problem arose. These teams were often updating the same code, and colliding with each other.
Let me use an analogy to make the point about extensibility. Imagine if ten people were asked to write a single book. Suppose we asked them all to work on each chapter together (e.g. chapter two: “Playing League as a beginner”). It would be a nightmare because they would literally all be trying to change one another's sentences. Instead, it makes sense to give them each a single chapter to write (e.g. chapter five: “Masteries” and chapter six: “Runes”), allowing them to work independently on the same book. Of course, the challenge now is to make the book read cohesively.
We faced a similar challenge on the client update. Teams were adding their features, but too often they had to adjust shared code and frameworks. We needed an architectural pattern to keep them as independent as possible.
I’ve spent a lot of time working on extensibility architectures and I know how nicely they solve the developer collision problem. To keep our architecture simple we chose a variant on a tried and tested extensibility approach called plugins. A plugin is an independent unit of ownership, development, testing, and deployment. Each plugin respects semver and must indicate which other plugins and versions it depends on, which keeps the architecture explicit. We augmented this style with a fully explicit dependency graph, indicating how all the plugins were connected.
So, a UI screen or feature goes into what we call a front-end (FE) plugin, and C++ communications logic goes into a (somewhat confusingly named) back-end (BE) plugin. Note that both types of plugin live inside the new client - we used the terms front and back-end because the architecture mimics a typical client-server architecture, even if it is inside a single desktop process.
We then allow the set of plugins chosen for a player to be based on what entitlements that player has. Entitlements are used to control regional-specific features, and will also be used to give access to new games.
So, we retired our use of Orbit and used a simple resource cache owned by each front-end plugin. Doing all this retired a metric shedload of complexity.
We now had a much more productive architecture and teams were able to make screens and services quickly, with clear separation of responsibilities.
However, we had a developer problem. Some of the teams really didn’t like Ember.js and wanted to use web components directly. Other teams already had their own web apps written in a variety of frameworks that they wished to host inside our new client.
At this point we were still reluctant to relax the "Ember.js for all” rule just for developer preference. Removing the assumptions related to Ember as the single embedded framework was going to take some serious work.
Note that the key word above is potentially. We still use Ember for the majority of our features for consistency purposes, but this is no longer technically required.
The final architecture
We repeated the developer satisfaction survey mentioned above, and found that the situation had improved by around 15% and was rising quickly. Nice!
One of the developers commented that we’d made a "datacenter in a desktop." From another perspective, I realized we'd created the game client equivalent of Atom, Github’s CEF-based extensible text editor based on the Electron framework. Interesting stuff, and I would never have guessed that we'd end up here at the start.
There are a lot of questions that this post hasn’t explicitly addressed, such as how we can ensure that features interact seamlessly and don't look stitched together, how we get game quality effects using HTML5, and how we allow for the independent deployment of features. These are fascinating areas in their own right, and we're happy to talk more about some of these things if there's a lot of interest. Hit us up in the comments!