Reducing Rendering Complexity with Gun Goddess Miss Fortune
Hi, my name is Eric Friedman, and I’m a Software Engineer who has been working on League of Legends for 8 years, focusing mainly on gameplay systems. For the last several years, I’ve been working in the Player Immersion and Expression (PIE) initiative, bringing you skins like Elementalist Lux and personalization options like Emotes. If you’re interested in how Elementalist Lux was implemented, check out our previous article. I’m excited to bring you another article about an ultimate skin, Gun Goddess Miss Fortune, and the technology it took to bring her to players.
The Content Team in the PIE initiative is the team responsible for designing and developing new skins. When they decided to develop Gun Goddess Miss Fortune (GGMF), they approached our team - the New Content Team - to pair on development, because we had the engineers needed to complete the project. The design required Miss Fortune to swap guns, resulting in different animations, textures, SFX, and VFX. Our main challenge was deciding on a strategy to handle the gun swapping, and successfully implementing it.
Strategies for similar skins
We’ve previously created form-changing skins that set the stage for how we considered implementing GGMF. Pulsefire Ezreal and Elementalist Lux, for example, both required specific techniques to efficiently change up their outfits mid-game. For Pulsefire Ezreal, we focused on changing submesh visibility, and for Elementalist Lux, we utilized full character swaps.
At first glance, either one of these options could potentially have worked for GGMF. However, other priorities and opportunities resulted in us creating a brand new solution that would enable us to open more doors for cool tech in the future. Before we can describe our final strategy - creating a component called SkinnedRenderable - we’ll need to explain the options we’ve used in the past. Let’s take a look at both changing submesh visibility and full character swaps, and examine how we determined viability, what elements we took from each strategy, and how they affected our final GGMF design.
Pulsefire Ezreal’s shiny accessory changes required changing submesh visibility. This means that different parts of the model are all included in one character, and designers hide and show different submeshes.
Ezreal shows off his fancy layers
Our early design for GGMF required building in enough flexibility that we could later decide to add changes to the skin after launch. This made submeshes less than ideal, as adding new elements would require adding geometry to the base model and re-exporting all of the animations. It also would have had a negative impact on performance because it would increase the memory footprint.
Later in the development of the skin, we decided not to add new content post-release. But in the interest of continuing to holistically level up skins tech, we kept this part of the design so we could consider this feature for possible future skins and products.
To allow Lux’s elemental-themed transformations, we used full character swaps. These require the artists to model and animate a whole separate character, and the designers must enable the character to change at the appropriate time in a game.
Lux transforms into Storm mode
If we were only considering how to ship GGMF, then using full character swaps would have been sufficient. However, full character swaps were overly complicated for what we wanted to accomplish, which was just to change one part of the model, and related texture, VFX, and SFX. Full character swaps require a lot of overhead to set up characters, and a product requirement of GGMF was that we’d simultaneously simplify the pipeline for creating new content. Since this was the beginning of the project, there was an opportunity to schedule cleaning up of tech debt and consider all engineering solutions to accomplish the goals for the new skin. We worked together to prioritize some cleanup that would improve the codebase for the future.
Given some of the potential upcoming content types and the rewards that were in development at the time, we decided to step back and take stock of the way we render 3D objects in League to see if a little extra time investment now would pay off later. We found a solution that worked elegantly for GGMF and set the development teams up for future success by cleaning up a lot of rendering tech debt. Let’s take a look at the problems we found, and how resolving these issues led to a better, more extensible solution.
The rendering tech debt problem space
After taking a look through the League of Legends game code, we came across a lot of tech debt and code duplication. Anything that wanted to render an animated 3D model needed to push its mesh and animation information to the engine level, and this was done in a number of ways. Characters (minions, champions, etc.), animated buildings, level props, and grass had to add themselves to the renderer in their own unique ways, gathering the relevant animation and mesh information from their internal structures, and adding it to the renderer each frame they should be seen on screen.
Anyone who cares about efficiency and code generalization may look at this diagram and get heart palpitations. Debugging rendering issues requires an engineer to first know which type of object is being dealt with. For example, towers are considered characters… unless the tower is dead, in which case it becomes an animated building.
New rendering features also required implementation in the 4 different types of objects illustrated above. For example, when an engineer added stencil buffer functionality to the game objects, this had to be implemented in all 4 places if each type of object wanted to take advantage of it. This led to quite a bit of code duplication that we knew we could cut down on.
With the goals of both an ultimate skin and in improved content pipeline, we set out to accomplish the following:
- Reduce code duplication by unifying the rendering paths of game objects into one component
- Set up future products that require rendering objects in game for easier success by simplifying and unifying the rendering paths
- Ship GGMF, meeting the following design requirements for each of the four forms:
- Unique gun models
- Unique visual effects
- Unique voiceover and SFX
- Unique textures
- Ability to change animations
Our solution: SkinnedRenderable
To improve our content pipeline, we decided to create a new component - a class in the code that encapsulates all of the functionality for one specific responsibility. This component would be called SkinnedRenderable and it would include everything needed to render and animate a 3D model. It could then be attached to all the different types of game objects one at a time, and carefully rolled out to players. This diagram demonstrates the final vision:
SkinnedRenderable addressed all of our goals:
- Code duplication removed - all objects that render and animate a 3D model have a component to do so, which handles pushing relevant information to the engine.
- Future products are easier to create - simple new objects became basic Game Objects with a SkinnedRenderable component, as opposed to the bloated “minion”.
- Ship GGMF - GGMF holds multiple meshes and animation data which represent the base model and each gun, instead of storing everything in separate characters. This allows us to show the gun the player chooses while hiding the other three.
While the solution for unifying code and removing duplication may be obvious to veterans and n00bs alike, we found ourselves in a particularly interesting problem space when it came to implementation. We had to refactor the way every animated object renders in-game while shipping GGMF on time and limiting any negative player impact.
Implementation and rollout
We started work on Gun Goddess Miss Fortune in November 2017, and the skin was released in March 2018. At the time of release, she was the only skin that used the SkinnedRenderable component. Rollout of the new component to all other objects was only completed in May 2018. This was intentionally a slow and deliberate process to minimize any potential impact on players if bugs snuck through our testing process.
The rollout plan was as follows:
Creating and using AnimationComponentProxy to enable moving characters’ AnimationComponent to SkinnedRenderable
Creating GDS toggles to give us control of the rollout timeline on characters
Replacing animated buildings’ rendering and RenderableMeshProxy with SkinnedRenderable
Creating and using AnimationComponentProxy
AnimationComponent, a complicated object with an extensive API to control functionality like which animations are playing, animation state, playback speed, and animation events that trigger things like sounds and particle effects. Taking it out of the individual character objects and putting it into SkinnedRenderable was inherently risky due to how much the character object is responsible for controlling its animation state through its AnimationComponent. That being said, it was necessary to move the AnimationComponent to SkinnedRenderable to create a self-contained and well-designed component for rendering animated objects.
The solution we came up with to mitigate the risk was called the AnimationComponentProxy:
The idea was simple: Find every call to the character’s AnimationComponent, and create an identical function in the AnimationComponentProxy. From there, the AnimationComponentProxy can check if its owner character has a SkinnedRenderable, and if it does, forward the function call to the SkinnedRenderable’s AnimationComponent. If the character doesn’t have a SkinnedRenderable, then it uses the character’s internal AnimationComponent.
Using a proxy to route the animation calls gave us lots of benefits. Any toggle system that would dynamically add SkinnedRenderable components to characters wouldn’t require any additional toggle system for the AnimationComponent. This allows for some characters to use the new SkinnedRenderable code while others use the old method. This meant we could disable SkinnedRenderable on a per-skin basis if individual skins had bugs. And by routing every call to the character’s AnimationComponent through a proxy, we generated a self-documenting list of functions that needed to be tested with the new code paths.
Creating GDS Toggles
After the AnimationComponentProxy was attached to the characters, it became easy and safe to create two micropatchable toggles, one for SkinnedRenderable components on a per-skin basis, and one for all SkinnedRenderable components for every skin. The combination of these two toggles gave us enough control to either disable a specific skin if there was an interaction between that particular skin and the SkinnedRenderable component, or disable the entire system if a severe bug was found in the SkinnedRenderable component that affected all skins.
In the end, the first toggle for individual skins ended up only being toggled on for GGMF, which was released in 8.6. By the time we released her, we were already in testing for the second toggle - the global toggle for all characters. This was completed by patch 8.8, and eventually rolled out to players in patch 8.11 after bug-fixing and extensive testing. Our QA teams did a full sweep of every skin and animation to verify everything looked the same.
Replacing animated building’s rendering and RenderableMeshProxy
With the above GDS toggles completed, we could turn on the SkinnedRenderable Component for characters. Next we needed to convert the remaining animated objects to the new component, which consisted of animated buildings and objects that used the RenderableMeshProxy (grass and props).
This step was relatively easy, because we had already created SkinnedRenderable for characters, which were much more complex. We just had to plug SkinnedRenderable into animated buildings, props, and grass, where all the functionality they needed was already present. Also, there are far fewer animated buildings and props than there are characters, so a limited amount of content needed to be tested.
While testing and bug-fixing was in progress for the characters transitions to SkinnedRenderable, grass, props, and animated buildings were all moved over to the new code in parallel. We shipped the SkinnedRenderable change to grass in 8.7, which unfortunately included a performance issue, which was quickly identified using Riot’s Performance Measurement Tools. A hotfix was released to resolve the performance issue, and props and animated buildings were completed and shipped in patch 8.11.
Finally, after everything was shipped and live for a couple patches, the AnimationComponentProxy - which had helped us switch between AnimationComponents - and the scaffolding and toggle system were removed from the codebase.
With the completion of this project, we had successfully achieved our goals of an improved content development pipeline. These changes meant that players were able to pull out the big guns with GGMF, while we were able to pull out a ton of duplication and tech debt that had been holding us back.
This giant leap into the depths of rendering tech debt taught me a lot about successfully refactoring content, managing project length, and clearly communicating with production teams. GGMF shipped in 8.6, and the refactoring that this skin kicked off wasn’t finished until 5 patches later. Taking the time to do this right to invest in a healthier code base was definitely the correct call.
Also, shipping a performance bug to the live game further illustrated the need to use available performance analysis tools diligently, especially when dealing with a section of game development that is often a performance concern, such as rendering. Also, putting these changes behind a micro-patchable toggle whenever possible to be able to quickly revert to previous behavior was invaluable.
I’m really proud of how Gun Goddess Miss Fortune turned out, and how many players have loved using the skin. As an engineer, I’m thankful that we were given the opportunity to clean up some really old tech debt. We’ve managed to replace it with something easy to iterate on, and we’re excited about all the new products it’ll enable.
Thanks for reading! If you have any questions, feel free to reach out in the comment section below.