The Art of Spell Casting, Part 1

A Journey of Discovery

Hwei y’all! My name is Luke Li, aka Riot Quasmic, joined by Cliff Zhu, aka Riot 1kFeathers, and we are the game engineers on Hwei! We’ve been working with Myles Salholm, aka Riot Emizery, who is the mastermind behind Hwei’s design, to bring the Visionary to life in game. I’ll be the voice starting from the beginning, and will be handing it off to Cliff during part 2!

We wanted to share with y’all the tech we’ve been working on these past couple of months to make Hwei’s spell casting feel precise and fluid, so players can paint disaster and despair onto opponents with ease. It took many ideas and iterations before we landed on a solution that we would consider ideal, and we’ll be covering the problem space and how we progressed from a workaround to the extra responsive and robust implementation you see in-game today. 

Two Spells for the Price of One

Postponed Spell

If you’ve ever looked very closely at your cooldowns while casting multiple spells in quick succession, you might have noticed something interesting. 

Ezreal Utilizing the “Postponed Spell” system

Ezreal here can input E and then Q while E is still casting, but the Q input doesn’t just disappear; after casting “Arcane Shift (E)”, it casts “Mystic Shot (Q)” immediately after “Arcane Shift” finishes! This happens due to a system we call the “Postponed Spell”, which tries to cast a pending spell for a set duration if another spell is casting currently, until it is either successful in casting, times out, or something else clears the pending cast. 

An example of something that can clear the postponed spell is actually another postponed spell! If you make 3 inputs in quick succession for Ezreal, like E -> W -> Q, then the postponed W spell will actually be replaced by Q, and you will cast E -> Q!

Ezreal inputting E, W, and Q, but only casting E and Q

The Visionary’s Issue with Postponed Spells

This spell cast buffering is a standard quality of life feature that we want all champions to have, and all standard champions get this system for free. Hwei is an exception, and to explain why that is, we first have to look at how he casts his spells. When Hwei casts a spell, he needs to press 2 buttons. The first picks the subject and the second chooses which spell from that subject to cast. For example, to cast “Molten Fissure”, Hwei would need to first press Q to pick “Subject: Disaster”, then E to pick the spell itself. 

Normally, each input maps to a spell cast, so the existing spell casting system has a built-in assumption that every input of the QWER buttons will lead to a spell cast, and handles each input as such. Therefore, to implement Hwei’s abilities, we would need to have the base Q, W, and E spells cast a “subject” spell that replaces the available spells Hwei has at his disposal with their derivative spells (e.g. an initial Q press would replace Q, W, and E with disaster spells and R with “Wash Brush”). Then, we would want “Wash Brush” to cast a spell that changes everything back to his base spells. Finally, all other derivative spells and base R would just be implemented as regular spell casts with each having their own separate logic.

Hwei casting 7 spells to cast 3 “complete” spells (with one spell canceled)

This is the main problem. Casting any spell ends up requiring a subject spell cast to swap the spells and then the actual spell cast, so we functionally are casting 2 spells for the price of one. And since we are casting 2 spells, the “Postponed Spell” system kicks in. If we cast a subject spell, and then a derived spell shortly after, we would postpone the derived spell cast while casting the subject spell. Immediately following the subject spell cast we would cast the derived spell, which feels fine for one “complete” spell cast (equivalent to one spell cast for a standard champion).

But if Hwei wants to cast “Molten Fissure (QE)” then “Fleeting Current (WQ)”, he would require 4 spell casts for 2 “complete” spell casts. If done fast enough we could end up casting “Devastating Fire (QQ)” with the “Postponed Spell” system because the Q subject spell cast would fire off, then after the E input “Molten Fissure (QE)” would be postponed, then “Severing Bolt (QW) would replace “Molten Fissure” because W was pressed, and then Q would be pressed which yet again replaces the previous spell and leaves us with a QQ spell cast.

Hwei attempting to cast 2 “complete” spells but only casting 1

What we want is for Hwei to be able to cast spells like other champions. Back to back “complete” spell casts should have the second buffered “complete” spell cast immediately after the first one finishes. Hwei’s EE + QQ combo should not be dictated by having to wait for the EE spell cast to finish before being able to input the QQ spell cast, as that would feel terribly slow and prone to error. If a Hwei player wants to cast EE and then QQ, they should be able to speedily input EEQQ and have them cast in quick succession without waiting for spell cast completions.

Sketching Prototypes

The Groundwork

When I was onboarded on the Hwei pod, Emizery told me that the biggest and most important thing we needed to get done was to make Hwei’s spell casting feel right. He had already tried various techniques and methods to extend the current system with scripts to cast spells more fluidly, but concluded that engineering intervention was required. 

One of the reasons why we couldn’t just script out a solution was simply because not enough of the spell casting system in code is exposed to designers. We could implement workaround after workaround but it would just be better to create an optimal implementation in code, so that there is less design overhead and a quicker, more refined system in gameplay code. 

But even if we were able to expose everything to design side scripting, we still wouldn’t be able to fully create ideal spell casting for Hwei because our scripts are purely server based. Normally, this restriction isn’t an issue due to the server authoritative nature of LOL; having scripts handling game logic on the server is almost always correct since it is the source of truth that ends up propagating to player clients. However, with Hwei’s spell casting, there are things we would like to tap into that are on the player client that server side scripts wouldn’t have immediate access to; things like mouse position, mouse inputs, button inputs, are all handled on the client side. While we could access this information by sending a round-trip packet from the server to the client, this would always induce a latency cost associated with a player’s ping, which would leave us with ping related sluggishness that would have us feeling despair while spell casting.

Server querying client mouse position

Rapid Prototyping

Since I was new to the spell casting system as a whole, I thought it would be best to quickly investigate how a button press became a spell. In short, a button press gets converted into spell data that gets passed along the client, checked for validity, sent to the server in the form of a spell cast packet, checked for validity again on the server, and then finally casts a spell after all of that.

How a button press becomes a spell cast (Simplified)

After this initial investigation, I immediately tried out the most obvious solution: a client side queue that could queue up inputs. The idea was to convert these inputs on the queue into spell cast packets when they were ready to cast, and then have the server handle the spell casts in order.

The key reason this implementation didn’t work was because client side validation succeeds a majority of the time even when a spell can’t be cast server side. This renders the queue useless because if we determine whether or not to send the next spell cast packet based on this validation, we will for the most part be sending a bunch of invalid spell cast packets to the server. This was further emphasized when I read this line of code (inside a function that checks if we can cast a spell client side):

Code snippet of a “Can Cast” check client side

Basically, if we don’t have enough mana, heck it let’s send the spell cast packet anyways. The server would look at that packet and smite it down because it is the source of truth and knows that you don’t have enough mana to cast the spell. This piece of the puzzle means that no client side queues relying on spell cast validation could ever work, because the final validation was done on the server. If we ever wanted to know if a spell cast succeeded or not, we would need to move to the server, which is where the queue implementation would go next.

I then went on to try creating a spell cast queue on the server. The first iteration of this tried to create a queue of spell casts upon initial handling of the spell cast packet server side. Funnily enough this first attempt didn’t work either for the exact same reason as the client side implementation, because initial server-side validation was not the full validation and was missing validation for when the spell cast actually happens. Postponed spells also heavily interfered with this iteration due to postponement happening after initial validation.

This interference leads us to my next idea, extending the “Postponed Spell” system! This was the first of the implementations that I thought could work, and so I went ahead and made it so postponed spells could optionally hold more than just 1 spell. If you remember from the Ezreal example earlier, if you would cast E -> W -> Q, you would only cast E -> Q because the W gets replaced. The idea was to instead make it so there is no stomping of the postponed spell, rather, all spells get queued up. E -> W -> Q in quick succession would cast just that!

Ezreal casting E+W+Q spells even with quick successive inputs

It wasn’t as simple as “increase queue max size from 1 to X” due to how the “Postponed Spell” system was architected, as it also stored special information on spell data, special handling for certain movement, and had legacy code considerations that had to be worked with. I knew if this was the final direction, there would be a lot of special case code wrangling necessary; but first we needed to check if it was a direction we could even take. I created a quick prototype to see if the concept worked, and upon loading up a game I found out that… it acted very strangely.

Postponed Spell Queue” with Hwei inputting QQWE (Recreation)

What the flub? I tested it with other champions, and it seemed like it actually did work. I was able to queue up a metric ton of spells and they would all cast in order properly, and then would test Hwei again and spell casts past the first 2 would cast improperly.

Postponed Spell Queue” with Brand inputting W 4 times, and Q 4 times after (Recreation)

It was only after debugging that I found out the reason a postponed spell queue would not work for Hwei specifically, and it’s actually pretty interesting. The crux of it is that Hwei has spell casts that modify what his current spells are, and that the data stored in a postponed spell is spell cast information. If Hwei tries to queue up spells based on spell cast information, we can end up queuing the wrong spell cast information based on the previous sequence of events.

Here’s an example of what could happen. If I was on Hwei and input Q -> Q -> W -> E in quick succession, intending to quickly cast “Devastating Fire” (QQ) and “Stirring Lights” (WE), I would first cast “Subject: Disaster”, which replaces all of our spells, and then cast “Devastating Fire” with the next Q input. If I input W next to try and cast “Subject: Serenity” during the cast time of “Devastating Fire”, we would postpone that spell because we are currently casting a spell. All expected behavior so far, but here’s where the issue comes in. If I input E next with the intention of casting “Stirring Lights”, but during the spell cast of “Devastating Fire” and before “Subject: Serenity” spell cast, the next spell we put on queue would actually be “Subject: Torment” spell because the spell data hasn’t changed yet.

What ends up happening is the postponed “Subject: Serenity” would cast, replacing all of Hwei’s spells with their respective subject abilities, but the postponed “Subject: Torment” from the final E input would fail server side validation, making our input sequence produce a sad spell casting sequence where only 3 out of our 4 inputs were processed correctly, leading to only 1.5 “complete” spell casts.

“Sad Spell Casting Sequence”, example of why a spell data queue doesn’t work for Hwei

An idea for getting around this is to store the data of derived spells differently, and to change how spells change internally. We could map spell casts to their new data locations, and then queue spell data since we can safely assume their immutability. Unfortunately, due to legacy code considerations, this has many risks and would require a large system overhaul due to prior assumptions about spell data storage and spell changing. This means that it was time to sunset this idea and consider other implementations.

A Red Herring

The Spell Slot Queue

With all the discoveries made from previous implementations, we now had a better scope of implementation restrictions. If it’s a queue it can’t be on the client, it has to be able to translate spell cast packets from the client into spell casts on the server without being affected by spell swaps in the cast sequence, and doesn’t challenge existing spell data locations and spell changing assumptions.

To implement a spell casting queue, these restrictions meant that there was only one optimal solution, which was to use spell slots as the queued data. In short, spell slots are just numbers that map to a spell that a player currently has, e.g. if we are playing Ezreal: 0 maps to mystic shot (Q), 1 maps to essence flux (W), so on and so forth.

The idea behind the spell slot queue was simple: since spell slots map to spells, we could just queue up these spell slots and then try and cast whatever was first on the queue until the queue is empty. While the concept of a spell slot lives both on the client and the server, we would only be queuing up the slots on the server due to the lenient client side spell cast validation. On the server, there was some nuance to when a spell slot could be queued. Specifically, after the server receives the packet the spell cast has to still pass initial validation, and we would only queue up a spell slot if the spell cast fails (which was another pass of validation different from initial) or if there are slots on the queue already. The short reason why we don’t queue up spell slots based on the results of initial validation is due to it not being concerned about whether or not a spell is currently casting or not, which we want to fail if we are currently casting a spell so we can queue up spells while casting a spell. The final thing of note is that the “Postponed spell” system very much interferes with the idea of a spell slot queue, so in order to implement this I would have to stop spells from getting routed to the “Postponed spell” system. 

With all this in mind, I went ahead and added spell slot queuing functionality to Hwei and… it just worked! 

Hwei spamming spell casts using the spell slot queue

Would you look at that! With this, you could actually just adjust the queue size from 1 to X and then have a spell casting queue that could hold that many spell casts. After a bit of cleaning up and having Emizery play around with it we decided it was good to send it off to playtest!

Server Side Serenity

The initial feedback was extremely positive! Players who were used to Hwei got their hands on the new tech first, and in general it seemed much more reliable than what we had set up for him initially; spellcasting felt significantly better since you could chain more inputs. 

The implementation was also really quick to get up and running. We didn’t require much modification to the existing design workflow; besides removing the existing implementation to assist with spellcasting, scripts basically stayed the same. Another pro of the server side queue was that it had access to the source of truth, so we didn’t really have to worry about interference from hackers or whether or not the spell could actually be cast, we would just ask and immediately receive an answer. This also meant no extra latency, sending the spell cast packet from client to server would get processed like any other spell and then continually validated on the server. Hwei’s spell casts would now cast with similar responsiveness as any other champion!

This seemed like the direction we would be taking Hwei’s spell casting tech. However, while it was fast to implement, there was a whole slough of issues hiding underneath that made this implementation suboptimal. We’ll explore these problems in Part 2 and talk about the final implementation we landed on to make Hwei’s spell casting feel buttery smooth.

Posted by Luke Li and Cliff Zhu