Skip to content

Spell Casting, Continued

Spell List

I worked on getting the Spell Caster component able to actually cast spells. The first thing I needed was a way to define what spells the spell caster has available. I decided to go straight to using a list, instead of just a singular spell, because I knew that I was going to need to do that anyway, so why wait?

I exposed a list of SpellBases to the inspector, where a developer could slot in one or more of the ScriptableObject-based Spells. I also added a int field to keep track of the index of the currently selected/active Spell. Again, this is more of a future thing but I wanted to get the structure laid out ahead of time.

Placeholder

Decisions for Handling Input

Now that I had a spell to work with, I needed to actually have something to trigger the Spell. I added in a CastSpell action to the Player Input Actions and tied it to the E key (for now). This allowed me to add a check into my Spell Caster class to see if it was pressed. However, I ran into a dilemma. Do I make the triggers require discrete button presses or do I allow them to hold down the button and let the spell cooldown limit when it triggers. Since we wanted some spells to be "continuous" I decided to start with checking if the button is being held down and attempting to cast the spell every frame. This worked really well for both holding the button down or just pressing it once, since as soon as the spell triggered it would go on cooldown, preventing additional triggers. This took care of the Warmth spell (and the future Light spell) but what to do about the other types of spells?

This problem is more common in shooter games where you have semi-auto (discrete triggers) and full-auto (continuous hold). They typically have different logic based on the type of gun being wielded. If the gun is flagged as semi-auto, they check for discrete actions and if you press and hold the button it will still only trigger once, even if the "cooldown" has elapsed. Then if a gun is flagged as full-auto, they just check for if the is currently being pressed with it immediately firing as soon as the cooldown has elapsed.

I flip flopped on whether I wanted to implement the same type of system, where the spell could define what type it is and adjust the checks accordingly. However, I felt like I was getting too into the weeds with the system and consulted my oracle (Terra) to guide me down the right path. Ultimately, we decided it would be best to just stick with using the "full-auto" input system, even if it wasn't the most "proper". This is a game jam after all and there isn't anything that says we can't go back and refactor it if we have time. Really, the only difference between the two styles is really player expectation. Luckily, because there are no direct real-world examples to casting magic, the player can just assume that is how magic works in this game. It doesn't have the in-built expectation for a semi-auto gun to behave like a semi-auto gun.

404 - Component Not Found

Anyway, I got all that implemented and tested it out. It didn't work, yay! It would pick up the test cubes I put around that had the Warmth Mechanic component on the but it wouldn't pick up the Player. I spent a decent amount of time troubleshooting this. I hadn't used the Physics.OverlapSphere call in quite a long time, so I tried to make sure that I knew what it was doing.. That all looked good and I couldn't find anything in the code that seemed off.

After putting in some debug statements, I found that the Player was being hit by the sphere but it did not find the Warmth Mechanic on it. Lo and behold, the Player prefab lacked a Warmth component. I put that on, admonished myself for the oversight, and retested.

It still didn't work, yay! My debug statement still showed that the Player was being hit but it couldn't find the component. I had just ensured that it existed, so why wasn't the code finding it? Well, it turns out that the "graphics" for the Player had their own colliders and those were getting picked up by the sphere instead(?) of the collider on the Character Controller component. Since the Warmth Mechanic component was on the root Player GameObject and not on the bland capsule that is the Player's body, the check couldn't find it.

I removed the offending colliders, since they weren't needed anyway and everything seemed to work as expected. Finally! In the screenshot below, you can see that we attempt to cast the Warmth spell several times but it only actually triggers once due to the cooldown. You can also see that it found both the test cube and the Player.

Placeholder

Placeholder

Selectable Spells

Now that spell casting was working with one spell, I needed to expand it to support multiple spells. We already had the spell list and the field for tracking the active spell, now we just needed a way to actually change which spell is the active spell. As with most things, this came with a decision to be made. Should there be discrete buttons for selecting specific spells (i.e. press 1 for the first spell, 2 for the second, etc) or should the input "cycle" through the spells (i.e. scroll wheel up/E increments the index and scroll wheel down/Q decrements it), or some combination of the two. A lot of games I've seen tend to do a combination where you can hit the number to immediately swap to the slot or you can use the scroll wheel to cycle through them. Ideally, we would also support both but again, this is a game jam so I went with one and have the intent to add in the other if there is time.

I decided to go with the explicit numbers instead of the cycle, just because in the moment when playing, I think being able to explicitly pick which spell you need will be more important. This proved to be an interesting feature to add in. I started with adding the Input Actions, bound to the respective number keys. I added four of them for now, since that is the current number of spells we intend to support. More can be added later if need be.

Now that I had the Input Actions, I needed to update our GameInput class to expose that in a better package. My first attempt was to just put a if/else block where I checked each action individually, something like this:

public void HandleSpellSelection()
{
    if (GameInput.Instance.DidSelectSpell1())
    {
        _activeSpellIndex = 0;
    }
    else if (GameInput.Instance.DidSelectSpell2())
    {
        _activeSpellIndex = 1;
    }
    // Etc
}

This seemed like it was going to be a pretty big hassle, and it also opened it up to checking for spells that we may not have yet. For instance, we currently only have the Warmth spell but we support up to four spells. With the above solution, we would be checking to see if the Player tried selecting spells 2-4 and we'd have to handle that gracefully, since they don't actually exist in the list.

I knew there had to be a better way and after some sleuthing, I found that you can find actions dynamically by name on the Input Actions. Granted, this would make it a bit more fragile (since it is using strings instead of compiled property names) but I felt the risk was worth the reward. Running with this, I was able to make a more generic method for checking the inputs. Here is what I came up with:

In the SpellCaster class, now we only check for however many spells we actually have.

private void HandleSpellSelection()
{
    for (int i = 0; i < _spells.Count; i++)
    {
        var slotNumber = i + 1; // Slots use 1-based indexing
        if (GameInput.Instance.WasSpellSelected(i + 1))
        {
            _activeSpellIndex = i;
        }
    }
}

In the GameInput class, we dynamically build the Action name, based on the slot number passed in.

public bool WasSpellSelected(int slotNumber)
{
    var actionName = $"{nameof(_playerInputActions.Player)}/SelectSpell{slotNumber}";
    var spellAction = _playerInputActions.FindAction(actionName);

    if (spellAction == null) return false;

    return spellAction.WasPressedThisFrame();
}

This avoids the whole problem with accidentally selecting a spell index that does not exist and it also makes it more extensible in the future if we want to add more spells. This code wouldn't need to change, only the actual actions defined in the Input Actions would need to be added. As mentioned before though, if the action names ever changed, this code would break. This shouldn't be an issue for this particular project though, since we shouldn't need to change those names.

Visualizing Spell Selection

We have the various spells slots being selected now but the Player has no way of knowing what spell slot they have active. It was finally time to add in some UI to give the player some information.

I built out a small UI chunk with a HorizontalLayoutGroup on it and created a SpellSlotUI prefab for it to lay out. There were a couple of things I knew we would need for each spell slot:

  • A numeric display for what slot it is, so the Player knows which number to press
  • A display for a spell icon, for easy identification
  • A way to show which spell is active
  • A visual way to see the remaining cooldown time

The first two were pretty easy, I just set up an Image for the icon and a TextMeshPro Text field for the number. I also added a script to the top level of the prefab that took those as dependencies.

For showing which spell was active, I wanted a two prong approach. I wanted to use color to draw the eye but I didn't want to rely solely on color (in case it blended in on a particular background or if the Player is color-blind). In addition to the color outline, I also increased the size of the slot as well. It took a few tries to remember how to make the Unity UI scalable how I wanted but I got it working fairly quickly.

Placeholder

On the script, I added a couple of methods: one to initialize the slot (taking in the index and the SpellBase data) and another for setting it Active/Inactive. All together it ends up looking like this.

Placeholder

I'm pretty happy with how it is turning out but the big blank squares are kind of grating. The spells needed some icons. I updated the SpellBase ScriptableObject to include a Sprite to use for the icon so each spell could define their own icons. Andrew is still workshopping what the icons should look like in general, so I sidestepped making my own and reused one of the placeholder ones he had added previously. Once I configured the image to be a Sprite, via the Inspector, I was able to drag it into the new Warmth spell definition and away it went!

Placeholder

In keeping with the whole "dynamic number of spells" theme being carried over from the spell selection, I made sure that it dynamically displays slots for the number of spells the Player has. It is set up to allow runtime changes to the available spells but we currently do not support that in the rest of the game. For now it just initializes with however many spells the Player has configured

Note: We only have the one spell implemented currently, so I had to duplicate the spell in order to test this. Hence the same icon for each.

Placeholder

The last thing that needed to be added was some type of visual to show that the spell is in cooldown and when it is expected to be off cooldown. I decided to go with a simple "wipe" overlay, since it is easy to implement using the Slider component provided by Unity and it is visually apparent. As we get final icons and such put in, a different style might work better (if the contrast is a lot lower) but for now it works great.

Placeholder

After that was wrapped up, I stuffed it into a PR and sent it off to the team for review.

Adding Energy Management

The next board item I picked up was adding energy management to the spells. This meant adding an energy pool to the Spell Caster component and the means of depleting and restoring the pool.

Depleting the pool was fairly straight forward. We were already casting spells, so it was easy to add in a check to see if the spell cast was successful and, if it was, remove the used energy from the pool.

private void HandleCastingSpell()
{
    if (!GameInput.Instance.CastSpell()) return;
    var spellToCast = _spells[_activeSpellIndex];

    if (!spellToCast.CanPerform(this)) return;

    if (spellToCast.Cast(this))
    {
        RemoveEnergyFromPool(spellToCast.EnergyCost);
    }
}

We also needed to update the CanPerform check to include energy as well. Now we are checking both the cooldown timer and the energy.

public virtual bool CanPerform(SpellCaster caster)
{
    if (_cooldownTimer.IsRunning && !_cooldownTimer.HasElapsed)
    {
        return false;
    }

    if (caster.CurrentEnergyPool < EnergyCost)
    {
        return false;
    }

    return true;
}

This is it in action, after adding a piece to the UI to display the current energy pool level. That was done in almost the exact same way as the cooldown display, using a Slider and exposing methods to manipulate it.

Placeholder

Now that energy depletion was taken care of, I needed to work on restoring energy. The high level concept in the game (in regards to restoring energy) is that there are piles of energy-infused snow that the Player can approach to recharge their energy. The piles should also have a limited amount of energy and should disappear when it is depleted.

To accommodate this, I added an IEnergyRecharger interface and built a basic interaction system. It works similarly to to the spell casting, where it looks for an input, does a raycast to find any IEnergyRechargers, then extracts the energy from it. There is a cooldown on the extraction, so we can control the recharge rate. This would allow us to have piles of snow that are "supercharged" if we wanted that have more energy and recharge faster. Once the snow pile is out of energy, it is destroyed.

As I was testing it, one thing was missing. How could I tell if I was looking at something (and within range of) that I could recharge my energy from? I decided to add a targeting indicator that changes color when looking at an IEnergyRecharger within range. I did have to re-order the logic in my Spell Caster class so I could tell if one was found without needing to hold the Recharge Energy button. For now it is just a (too small) dot but I hope that it gets updated to be something better later.

This is what that ended up looking like.

Placeholder

Here is the full system in action:

Placeholder