Welcome to the Death Dimension
Hi, my name is Joshua Parker, and I’m an engineer on our Champions team. I help create the systems that unlock new capabilities for champions in League. Although my work is typically focused on how we build out a new champion, it also means revamping older systems and smashing tech debt along the way to help our engine evolve and allow us to keep creating new exciting experiences for players.
In this article, I’ll describe the tech that went into reworking the League champion Mordekaiser. We’ll take a look at some of the problems with Mordekaiser’s old ultimate ability and how we dove into dimensions to create Mordekaiser’s new ultimate - the Realm of Death.
First, A Little Context
Mordekaiser’s original ultimate involved marking an enemy and granting a controllable clone if the enemy died while marked. One particularly popular Mordekaiser trick was casting his ultimate on the dragon and rampaging down the middle lane with a dragon for a teammate. This ability caused strange bugs and all kinds of problems across the board, because the game isn’t really set up to properly capture and replicate other characters.
Do you think apartments charge pet rents for dragons?
While planning out the Mordekaiser rework, we knew we’d have to redesign him at his core. Just bug-fixing wouldn’t accomplish our main goal - to remove Mordekaiser-related production costs for all champions, past and future.
Because we wanted to address how Mordekaiser interacts with other champions in the game, we had to examine why his abilities were causing those infamous bugs. Let’s take a look at two examples of in-game content that Mordekaiser’s old abilities struggled to handle.
A major set of issues came from champion passives. These abilities often happen in the background of the game - they usually don’t need to be activated, but they shape the way the champion interacts with other players or the environment.
A good example is Jhin. He’s designed with a four-shot auto attack, and his passive guarantees that his fourth shot critically strikes.
The problem here was that Mordekaiser would capture the enemy at a distinct moment in time. So when Mordekaiser would cast his ultimate on Jhin, he'd capture Jhin’s current state, which meant the Jhin clone would get stuck casting his empowered fourth shot for every auto attack.
The Problem with Clones
There were also some strange loading issues causing Mordekaiser bugs. The server would load extra content when a player started a game with Mordekaiser so it would be able to make a copy of other champions. But what if Mordekaiser used his ultimate on a champion that wasn’t supposed to have a clone?
Shaco, for example, is designed to have a clone - so his entire kit understands what happens when there are multiple versions of him. But what about other champions who aren’t built to interact with multiple versions of themselves? This may seem small, but multiply that across every champion in the game and you end up with widespread issues that quickly get out of hand.
Every champion after Mordekaiser would have to be adjusted so they could functionally turn into a clone. This would be a huge burden on all future champion development.
A Peek at Design
There was a time during Mordekaiser’s update where we were considering bringing the ghost clone from his ultimate ability back. But we decided not to, in part because it just didn’t feel true to who Mordekaiser was supposed to be.
His earlier gameplay was heavily rooted in spell casting where the clone does most of the work. This doesn’t really match the fantasy of an undead armor knight king who’s trying to populate his undead kingdom. And when you see a dude with a huge mace, you expect him to hit people with that mace. We did end up paying homage to his old ultimate by giving the new Mordekaiser a ghost crown when he steals the stats of another player during the 1v1 battle that happens during his new ultimate - in the death dimension.
Fun Fact: If you play our All For One mode and pick Mordekaiser, when you ult each other, you’ll all end up in the same realm.
Hey, how did you get here?
At the beginning of development, we came up with a prototype of Mordekaiser’s death realm. And although it worked at first, we knew the technology it was based on wasn’t going to scale due to server bandwidth. This is because it relied on our relationship tech, which describes gameplay relationships between two units.
This was unscalable because:
A relationship is a contract set between two units, but when a new unit is created, it needs a new contract. This meant constantly checking the server for new things being created and setting up those relationships, which created unwieldy code to maintain.
Setting up each relationship for every unit in the game to and from both champions transitioning into the death dimension caused an initial server hitch. This downgraded gameplay performance for all players, which we weren’t willing to accept.
We had to start from scratch and build out something foundational to League that would encapsulate all champions and systems, past and future.
This meant we’d have to introduce a new concept to the game - dimensionality. By giving the game the language to understand dimensions, we were able to establish the difference between the existing base dimension (Summoner’s Rift) and other dimensions… like Mordekaiser’s spooky scary death realm.
Now that’s an aesthetic.
Our vision system is highly complex because vision is such a foundational part of the game’s basic strategic experience. League is a game of vision - knowing the location of enemy units enables players to make decisions that are key to the lifecycle of a match. The vision system was built very early in League’s development life cycle, and with so many gameplay systems built on top of it, it’s not easy to start twisting knobs and pushing buttons.
We knew we’d have to leverage the vision system to deliver on an experience that would make victims of Mordekaiser’s ultimate feel completely isolated from their team. This meant we’d have to make changes at the very lowest level of the game, where vision is defined. It was time to start tinkering with GameObjects.
A Hierarchy of Objects
Games have hierarchies at their core, where the lowest-level objects determine every basic functionality the game needs, and the lineage path adds details and capabilities. In League, we use GameObjects as our most basic building block, and these branch out to attackable units (champions, minions, towers) and unattackable units (effect emitter, grass).
The first change I made was simple - adding a value on all GameObjects that establishes the dimension. I set this at “default” at first, and checked it in some time around week 2 of pre-production. This didn’t actually affect anything from the player side because they had only ever interacted with one dimension, but it gave us the language to start defining what it meant when a dimension wasn’t default.
Once this was done, I had to start digging into the questions that make up interactions. When two objects interact with each other, they go down a list of questions: Do I collide with you? Do I see you? Can I hurt you? In these places, I added another question - what dimension are you in? The answer to this question would determine whether the objects would continue down the list of questions or end the interaction immediately. To accurately answer these questions - especially the dimension one - objects need to consider if they can “net see” each other.
A ward gives ally players visibility in one area of the map.
League’s concept of “net visibility” determines whether network packets are being shared between players. If player A moves on their machine, those packets are sent to the server. If player B is able to see them on their map, the server sends them the packets of player A moving. In other words, the server is the only location that knows where everyone is and who’s moving.
On the backend, we consider vision as regions - imagine them like bubbles we place on GameObjects. These bubbles tend to be circles, but can actually be any shape. If we put a vision region bubble over an object, it begins to receive packets and send them back to their viewee team (all ally units). For example, minions all have vision regions attached to them, which is why players are able to see the parts of the map where their minions are.
Took a little creative liberty here for the sake of clarity, these are not the actual vision bubbles.
The vision system has a set of rules that determine which actions in the game impact which characters. The net-visibility system leverages the vision system’s rules to decide whether a packet is sent or not sent. For example, if a player is in the death dimension and minions die in any other dimension nearby them, that player still gets experience (because experience ignores vision rules).
The missile system decides whether to follow these rules or not, depending on the situation. So if a missile is created in one dimension, we have to let it decide whether it should interact with players in another dimension.
Missiles like to hit things. Determining those hits gets complicated when there are multiple dimensions. The source of the missile needs to be clear, and we need to know which dimension it exists in - and where it should go if the caster switches dimensions. Establishing accurate sources of missiles - and therefore spells - helps us avoid situations that are confusing to players by making it clear to the server which dimensions they should exist in.
This is difficult because it requires understanding when a spell really ends, which can be fuzzy.
For example, consider the following situation. A player casts a spell, which creates a buff, which summons a minion, which casts a missile spell, which hits someone and summons other minions that cast spells. When did the original spell actually end?
The tagging system that tracks the original source of a spell is called “Dimension Origin Context.” This is a piece of data collected when a spell is cast that captures where the missile was created and which dimension it was created in. When the spell goes through its lifecycle, it can always reference this breadcrumb to know which dimension it began in. This allows the spell to make decisions around when it should follow a player when they switch dimensions, and when it shouldn’t.
Example 1: Follow
For this example, we’ll use the champion Lucian and his ultimate ability.
Lucian’s ultimate ability is a series of shots fired in the direction he’s facing. These are each individual missiles, and because they look like they’re coming from his gun, they need to follow where he goes. Even though the ability lasts several seconds, if Lucian were to switch dimensions, his ultimate should follow him because that’s what players expect.
Example 2: Don’t Follow
Now let’s take a look at an ability that shouldn’t follow dimension changes - the Ziggs bomb.
Each bomb bounce is an individual missile. For each bounce, the game determines whether the missile hit anyone, and if it didn’t, it casts another missile for the next bounce. If the dimension source was tied directly to the caster and Ziggs switched dimensions in the middle of a bomb bounce, we want the bomb to stay behind. Otherwise a bomb would start bouncing in the new dimension with no warning, which could ruin the integrity of the game.
Once we had the Dimension Origin Context system in place, we had to decide how to implement it across every champion in the game.
Similar spells can be scripted differently depending on the designer that wrote them. This means there can’t be one catch-all solution for all spells/missiles/champions, because things need to be examined individually to determine the best dimension-switching behavior.
We did a granular sweep of the entire game by casting all the spells, missiles, abilities, and item effects during the following situations:
When a player transitions into the death dimension
When a player is in the death dimension
When a player transitions out of the death dimension
This quickly made it clear that we would have to make buckets with rules on what would come with and what wouldn’t.
For example, if a player leaves some sort of trap on the ground, that shouldn’t suddenly switch dimensions. Look closely at the gif at the beginning of this article - the traps on the ground don’t transition with Mordekaiser when he switches realms.
And what if they have some sort of pet?
This metal ball is not as cute as my friend’s puppy.
Our strategy involved creating buckets that were based on categorization of 80% vs 20%. We create rules that solve 80% of the problems, and the last 20% are special cases. Doing this allowed us to be efficient and consistent with solutions, instead of treating each item as an individual case that needed to be addressed individually.
This meant determining the real source of spells. The Dimension Origin Context system described above helped us solve especially complex situations, and it was particularly helpful in the case of trigger zones.
Example: Trigger Zones
Trigger zones are similar to vision, but notably separate - collision of objects or spells are determined by radiuses of impact. But how do these trigger zones interact with the death dimension? How do we know what their true source is, and where they should impact gameplay?
An example of a trigger zone is Yasuo’s Wind Wall.
This wall blocks missiles from continuing on their path, but while the Wind Wall understood trigger zones, it didn’t understand dimensions. If Wind Wall was cast before a player transitioned into the new dimension, it would still stop missiles in the death dimension, even though it really only existed in the default dimension. This meant trigger zones needed to understand dimension as well.
To do this, we gave trigger zones their own Dimension Origin Context. This allows them to track which dimension they should be interacting with over the course of their lifetime, and therefore decide when to switch dimensions and when to stay behind.
Scalable Systems + What's Next
While developing the death dimension, we thought a lot about how it would affect future champions and gameplay, as well as potentially new game modes. We wanted to create a system that was scalable and usable for future developers, and we wanted to avoid anything that would add considerable production cost to new content.
Champion spells and items are scripted in a language called BlockBuilder, which creates complex behaviors by stringing together blocks of functionality. Solving for future champions was relatively straightforward, we just needed to create tools within BlockBuilder to support changes to dimensions. One of these tools is an event called OnDimensionChange, which allows the script to react to a dimension change. Then the script can decide whether it would like to do something in response to the dimension change.
As described in the previous section, this can be:
Transition with the caster
Building Scalable Systems
Right now, we have three dimensions in League. Default, which is the base map, Death Realm, which is Mordekaiser’s ultimate ability, and All Realms. All Realms is a specialized system specifically used in cases where we want objects to ignore dimension rules. Buildings like the Nexus (a team’s base) need to exist in all dimensions.
Here’s an example of a strategic situation that helped us decide on All Realms for objects like buildings. If a Mordekaiser player is trying to destroy the enemy’s Nexus but the entire enemy team is there, he’s able to switch to the death realm by casting his ultimate on, say, a weak player. Now he’s able to hit the enemy’s base with only the weak character in his dimension and therefore able to (try to) stop him.
Now that we’ve already done the legwork around determining the dimension system for all characters, spells, missiles, and objects, it’s possible for future engineers to make the system extensible. In this new world, we could do game modes which spawned multiple dimensions at once to enable more complex player and NPC encounters.
Adding a new dimension would mean adding more content directly to our data so it’s visible inside Riot Editor. If we did this, it would make it easy for designers to quickly add multiple dimensions by using string hashing. Maturation of this dimension system would mean creating a pack of data that lives in the editor and applies the right data for dimensions when called.
By starting this work with Mordekaiser, we’ve made these possibilities significantly more accessible. Creating new dimensions was not only exciting for Mordekaiser’s abilities, but for what it means for the future of League.
Thanks for reading! If you have any questions or want to tell me about wild Mordekaiser bugs you’ve seen, please comment below.