Magical Spell Masters Devlog Week 3 – Questionable Payloads

Week two of Magical Spell Masters was all about getting more assets in place and, for me, building the event-based logic to get things talking to one another. This week: refinement, a cute companion book, and the general scramble to close the game loop prior to the final build.

The closer I got to implementing everything we needed in this prototype—bringing in models and scripting UI animations—the more I began to see the problems in my event system. This was inevitable, and I never expected it to stand up very long to the demands I was placing upon it. Unsurprisingly, I’ve learned a lot of not-to-dos in a short time:

Plan Things Out Ahead of Time

This is true of pretty much anything in game programming. But I’ll be sure to dedicate more time on this in preproduction if Magical Spell Masters goes on to full production. Writing out events on paper (OnSpellCast, OnPlayerTurnEnded, etc.), visualising the flow and sequence of events, noting their overlaps and dependencies—this is critical. Better still, documenting this properly in a flow chart of some kind (and keeping it always up to date) will be a handy map for anyone wanting to get their head around the system as a whole.

Beware of Payloads, Especially for Globally-Significant Data

I’ve been a little overenthusiastic about taking advantage of the ability to pass data to event listeners. It’s obviously a handy thing to be able to do. You can end up with really robust and self-contained interactions between game objects. For example, here’s an event that’s called when the player clicks on a letter in the grid of tiles:

//Called by LetterTile when an instance of LetterTile prefab 
//in the tile grid is clicked
public event Action<string, int, int> onGridTileClicked;

public void GridTileClicked(string letter, int letterScore, int gridIndex)
{

    if (onGridTileClicked != null)
    {
        onGridTileClicked(letter, letterScore, gridIndex);
    }
}

That third integer, the grid index, is a nifty little thing. In the game we have a tile grid, we have a drop zone, and we have the letter tiles that populate each one. When you click on a tile in the grid, the game object disables itself. But it otherwise stays where it is both in world space and in the grid’s list of tiles. An identical tile is then spawned on top and flings itself over to the drop zone. When it arrives there, it destroys itself, at which point the drop zone spawns its own version of that tile as a child (and adds one to its own list).

The nifty thing is that, when you click on a tile in the grid, it passes an integer representing its list index to the flying dummy tile. The dummy tile passes this same integer to the tile spawned in the drop zone. And what this means is that the process works in reverse when you click a drop zone tile, so the grid knows which tile to re-enable. I’m sure there are cleverer ways of doing this. But it works and, to me, is a nice example of using events to hand over neat little payloads of data that are pertinent to specific, self-contained interactions.

Here’s something not so clever:

//Called when the 'Cast' button is pressed for a magic word 
public event Action<string, string> onMagicWordCast;

public void MagicWordCast(string word, string spellType)
{
    if (onMagicWordCast != null)
    {
        onMagicWordCast(word, spellType);
    }
}

There’s no good reason to be handing over data for this event, and the fact that I’m doing this sort of thing is adding needless complexity to the game. Worse still, it’s creating dependencies between game objects that don’t need to be there and, when I get to my third point, you’ll see why this becomes an issue.

So, what am I actually doing with these ‘word’ and ‘type’ strings? Let’s say the user spells ‘fire’ in the drop zone. This is one of the elemental words in the player’s deck, and it will cast a damaging projectile. When ‘Cast’ is clicked, the drop zone object catches the event and, using its own ‘currentWord’ variable, spawns a sequence of tiles that, after having their sprite set according to ‘spellType’, fly over to the Companion Book and result in a spell being cast.

The drop zone doesn’t even use the ‘word’ string. I don’t remember which listener does. I’d have to check. Presumably I put it there in response to some need. But here’s the bottom line: when data is globally significant (like the currently spelled word, or its element type, or the word score) it should be stored in a blackboard-like object for easy, reliable public access. Certainly, if I find my listeners frequently not even using payloads, it’s probably a solid indication that I need to refactor.

Which I will get around to.

Eventually.

Events Are Sequenced, Even When You Think They’re Not

A letter tile is clicked. And then it lands in the drop zone. And then it is clicked again. And then it is deleted from the drop zone and re-enabled in the tile grid.

This is an explicit sequence. It could—and should—be plotted out in a flow diagram.

But there’s a hidden sequence in there too. It’s one that I often forget when I work in Unity, as opposed to building something in Visual Studio using C# or C++. But I do so at my peril. It’s that final ‘and’ in the sentence: the one implying the drop zone deleting a tile and the grid enabling a tile happen at the same time.

Of course, they don’t. Instructions are getting executed one at a time. Both game objects are listening for the event, and the event is getting invoked once. But one will run its logic before the other, and I don’t necessarily have control over the order.

This wasn’t an issue until it was. In line with my previous point about event payloads, I’ve been handing over letter scores—the Scrabble-value of each letter—between game objects. This, more than anything else, is the most foolish thing I’ve been doing in Magical Spell Masters.

So, let’s take the casting of a spell:

The letter tiles fly over to the Companion Book. Each one carries its letter score. The book sums up these scores as each letter ‘arrives’ (another event). When all the expected letters have arrived, the book casts a spell with a damage rating equal to the total sum of the letter scores.

But it doesn’t work that way. Somewhere, in spaghetti of all my events and payloads, the Companion Book is executing the logic associated with all the tiles ‘arriving’ and casting a spell before the final letter has had its score added. And this is why, in the GIF above, the word score is 16 but only 15 damage is being applied to the enemy. (Oh, that’s another thing: the spell projectile is carrying the bad data from the Companion Book over to the enemy. Gross.)

This isn’t good. As I said earlier, things like the currently spelled word and the damage score are variables that get used everywhere. Making them centralised and reliably accessible will solve a lot of problems. But I need to make sure I remember that, even if different objects are listening for the same events, they won’t necessarily have access to timely data.

Final Thoughts

This will be my last post about the Magical Spell Masters prototype. With luck, it’ll be selected to be put into major production. We might even get another programmer! Whatever happens, it’s been fun digging deep into things like event systems, layout groups, and tween-based animations. As is always the case with programming, I’ve learned that complex things are complex. If this game is going to work, it needs a solid amount of time in preproduction so we can plan properly and implement these systems on a larger scale.

Some pretty pictures to wrap things up! Spell book/tile grid swapping via tweens:

A fiery particle effect:

Concept art of the Scribe attacking:

‘The Scribe’ attacking, by Monika Haselhuhn