View All 7 Images

/tech is a new Nexus series exploring the tech side of League of Legends. If you like this story, consider checking out the Riot Games Engineering blog for even more complex deep-dives of the systems that power the game. Hey friends, RiotAaronMike and Lucida here! We’re software engineers on League’s Core Gameplay team, and we want to share a little bit about a system called the Contextual Action Component (CAC for short), which adds an extra layer of awesome to our champion’s personalities. Essentially, the CAC is a system that enables developers to build custom interactions into champions, allowing those champs to react more naturally to what’s happening on the Rift. Exploring the System Let’s start by looking at an example of what happens when Poppy uses her taunt emote while she’s alone compared to when she’s standing next to her golden-winged ally, Galio:

Poppy taunt without Galio | Poppy taunt in front of Galio

When Poppy taunts alone or near unassociated champions, her taunt VO will be one of a few generic lines. When Poppy stands near her allied Galio, she instead engages in a little playful banter, and what’s more, Galio will respond if he’s still nearby when Poppy finishes. It seems obvious now, but when League launched in 2010, this type of interaction wasn’t possible. The only way to create variation was to let the sound engine pick a random VO line from a list. This forced audio designers to use generic lines to avoid playing an improper line in certain situations.

For example, Lux’s “Sibling rivalry! This will be fun!” line makes sense when she’s standing next to Garen. Standing next to Katarina? Not so much. A line like this would not be allowed with League’s old audio system, since the system had no way to pick this line properly. We were losing valuable opportunities to show each champion’s personality and relationship to other champions! And that’s exactly what the CAC is for. It lies at the heart of these types of interactions; it’s what makes Poppy and Lux “contextually-aware” of real-time information like their nearest ally, which item they bought, or which champion they just killed. It also gives Pulsefire Caitlyn her unique Pentakill line and allows Xayah and Rakan to sweet talk each other on the Rift. Under the Hood The CAC was designed for a simple use: to execute different actions based on the context of any given situation in game. The structure of the system can be represented as: A situation

Rule 1

Conditions Action Rule 2

Conditions Action More rules

A situation is a predefined construct in our game, like KillChampion, AttackBuilding, or BuyItem. Over the past couple of years, we’ve built dozens of situations into League. Each situation can contain a list of rules , which itself contains a list of conditions and a singular action . When a situation occurs, a rule whose conditions are met is picked, then its action is executed. A tie amongst multiple valid rules can be resolved either on a first-come-first-serve or randomly if specified. Conditions are the predefined contexts, like “self HP range”, “target champion name”, “map region”, “spell level”, etc. Voice actions can choose lines for different listeners, including self, allies, enemies, and spectators. Below is an example of Camille with Program Camille skin taunting Ashe with Project Ashe skin:

As with the majority of the League codebase, this system is written in C++ and utilizes our Game Data Server (GDS) for its configuration. Take a look at an abridged snippet of the code that is called whenever a champion kills another champion: // To be called whenever a champion kills another champion

void HandleChampionKillSituation(Champion* killer, Champion* victim, uint8_t killerMultikill = 0)

{ ContextualActionComponent& cac = killer->GetContextualActionComponent(); // See if the killer has a KillChampion situation

const ContextualSituation* situation = cac.FindSituation(kKillChampion);

if (situation != nullptr) {

// Set the relevant facts

ContextualFacts& facts = cac.GetFacts();

facts.mKiller = killer;

facts.mVictim = victim;

facts.mKillerMultiKillSize = killerMultikill; // Attempt to find a rule that matches these kill facts

const ContextualRule* rule = situation->PickRule(facts);

if (rule) { // a qualified rule has been found

if (rule->ExecuteAudioAction(facts)) {

// Tell the other CACs that the killer just executed this event

cac.NotifyAllCacsOfPlayedAction(rule->GetAudioSituationTrigger());

} // Reset the momentary facts

facts.mKiller = nullptr;

facts.mVictim = nullptr;

facts.mKillerMultiKillSize = 0;

}

}

} This snippet illustrates how the system determines which action should be taken according to context. The PickRule function will iterate over each of the rules for the KillChampion situation until it finds the rule that passes all conditions and then execute the corresponding action (s). Authoring The following screenshot shows a rule we set up for any player skilled (or lucky) enough to get a pentakill with Pulsefire Caitlyn:

Every time Pulsefire Caitlyn kills an enemy champion, the CAC will run through rules in the KillChampion situation . This rule says: if this is the fifth kill in a killing spree, play the KillChampion3DPentakill voice line for self (the player) and the player’s enemies. Note that this rule has a limit of 3 “occurences” – observe the misspelling (brb, I’m gonna fix it) — so it will only play for the first three successful pentakills since, as we all know, it gets a little noisy on the fourth. Wins Historically, audio was triggered directly by events in various systems across the game. Events could be particle creation, animation events, spell casts, user input, etc. For example, when the player moves their champion, the game client builds an audio event named something like “Champion_VO_MoveCommand” and tries to play the corresponding audio clip. Since the historical triggers were not aware of in-game context, they were incapable of customized interactions. Direct events only scratch the surface of what can be done using the CAC. The combination of situations and rules allows for very specialized interactions to be authored. Before this system, we had some specialized interactions in the game, but we relied on randomness to apply them at an appropriate frequency. For example, Zac has two generic taunt lines, “Go big, or go home,” and “It’s not how much you can lift. It’s how good you look.” When he taunts, the game will randomly choose one line to play. Now we have the levers to fine-tune their appearance to only appropriate situations. This way, we can cause rare, detailed interactions on purpose rather than leaving them up to luck of the draw. Xayah and Rakan In early 2016 we set out to make our first couple champions in the League of Legends universe. Our goal was to make these two champions interact in game like the lovers they are instead of just using generic and interchangeable interactions. What if we wanted Rakan to be able to lift Xayah into the air during a dance piece? What if we wanted Xayah to prod Rakan with a little (loving) banter? What if Rakan needed to warn Xayah of impending danger? In order to make these “what ifs” a possibility, we needed to upgrade the CAC with a couple new actions and situations .

Xayah and Rakan

Animation The animation system wasn’t equipped with some of the context necessary for animators to create the desired synchronized dance or recall pieces. To achieve the end result, we added a new action type to the CAC to control champion animations. Whenever Xayah does anything – or nothing if idling – she makes a PlayAnimation request to the animation system with the desired animation name. We modified this flow so that the CAC intercepts these requests and checks if any contextual conditions are met. If there’s a match, the animation is swapped out for a more context-aware animation. The request is then sent on its merry way to be carried out by the animation system. Interactive CACs The next challenge was the dance. How would Xayah and Rakan let each other know when they wanted to start dancing with each other? This was achieved by adding a new situation that triggers whenever another champion performs a CAC action . All of the CACs in the game are notified of the completed action along with the current game context so that they can determine whether a reaction is necessary.

From left to right: Both are idling, Xayah starts her solo dance, Rakan opts into the dance, and they perform their synchronized dance.

Contextual Pings Another awesome win came from rerouting ping requests through the CAC. Now, in addition to the regular pings, the lovers are able to say things like “Babe, watch out!” for the Danger ping and “They ain’t here!” for the Enemy Missing ping. Technical Concerns Anti-Cheat Cheating and hacking are crucial concerns whenever we add new systems like the CAC to League of Legends. One form of hacking involves gleaning more information than the game itself offers to players to gain a competitive advantage. A cheater might exploit a contextual system to provide this information. Imagine if Elise triggered a line like “My arachnid senses are tingling…” whenever your team was hidden in brush nearby. To prevent such exploits, we’ve designed the CAC to act only on the information that the client already has and nothing more (in other words, it sees what you see). Performance We want to give devs the freedom and usability they need to bring League’s characters to life, but we also want to avoid a performance hit for any player, whether that player is running a liquid nitrogen-cooled monster machine or a lower-spec laptop. It has always been our goal to make the system as lightweight as possible and as performant as possible at every point in the process. A combination of coding choices and best practices allows us to achieve this: Situations are stored in a hashmap, with string hash as its key type. With this structure, we can quickly retrieve a situation from a CAC object. If a champion doesn’t have data for a given situation, the handle function will simply return. Since each champion has a small set of relevant situations, most situations cost very little. We prefer specific situations over general situations. Normally, we’d prefer to have general, reusable solutions that solve multiple issues at once, but this is a unique case. More general situations contain more rules, each of which includes other conditions that must be processed by the CPU. Splitting a general situation into a few specific situations reduces the number of rules and improves performance. Situations without rules can even directly return. For example, we have four specific kill situations: KillChampion, KillTurret, KillNeutralMinion, and KillWard. KillChampion often has the most variations, but only happens a few times in a game. KillNeutralMinion has the fewest variations, but happens more often. If we used a general situation like KillTarget for all of our kill situations instead, we’d have to parse through a huge list of rules every time one of these four types of target is killed. Prefer to check simple but important facts or conditions first. If any of these conditions fails, other complex checks in this process can be skipped. Skip audio situations if the owner is speaking a voice line. League doesn’t allow simultaneous VO lines playing from the same character. This gives us a great opportunity to optimize. If CAC finds out the owner is speaking, it can ignore new audio situations. In the case of spamming emotes, CAC is still very efficient because it will skip most of the process once the champion start speaking. Another important condition is situation cooldown. If the champion shouldn’t respond to a situation in a short period after last execution, then there is no reason to process the situation. Avoid high-frequency situations if possible. If a situation does happen frequently, there are a few ways to avoid a performance hit: Set a cooldown for the situation so the game client can skip checking every time the situation occurs. Make sure high-frequency situations have few rules so that they run faster. Conclusion With the CAC, systems like audio and animation are more aware of context in the game, opening up many new possibilities for our creative colleagues. This gives them the power to enrich champion personalities and continue expanding the world of League of Legends. Each and every voice over line we add to the game is an opportunity to make someone smile and bring players closer to their favorite champions.