I am making a gesture controlled wand, and there's lots to talk about!
Each spell requires a series of gestures to perform. For example, the "lift" spell - which allows you to pick up enemies and objects with your wand - requires several steps:
So how do you represent this in code? How do you detect multiple paths of gestures?
I have written a library called the Sequence Tracker. It allows you to register a branching 'tree' of gestures, and add event handlers to each step.
At its most basic usage, the sequence tracker is made of conditions and actions.
When condition
is true, it will run action
(where action
is a C# Action
type).
Now, conditionOne
must be triggered first, then when conditionTwo
is triggered it will run action
.
You can even put in extra action
s in the middle!
For example, this is the full sequence I use for the aforementioned Lift spell:
Pretty fancy! This links in with the 'spell module' system that is described down below, such that adding a new spell to the wand can be as simple as this:
You can also 'branch out' multiple conditions from a single one. For example, the Petrify and Polymorph spell gestures:
As a side note, the "Petrify"
and "Polymorph"
strings are names given to states to help with debugging.
The 'sequence tree' now looks a bit like this:
You can see and feel this in-game. Every time the tracker transitions from one state to the next, the wand will vibrate in your hand and a little dot will appear on the trail (like the one shown below).
So, we can track sequences of conditions, but the next hard bit is what those conditions should be.
The first big concept is the three spaces the wand operates in. When I say that, to trigger a spell, you have to "push the wand forwards", what does "forwards" mean?
From the wand's perspective - the wand's "space" - forwards is stabbing the wand along its axis. From the player's point of view, it's pushing the wand forwards in the direction they are looking. And in Blade and Sorcery there is the concept of a 'global forwards', which is the same no matter which way you are facing.
This is used in gestures such as the lightning zap. You stab the wand forwards, which means that the wand checks to see if its velocity along the 'forward axis' in Wand Space is greater than a certain value.
The best example of player space is in the fireball spell. It's intended to be a 'slash' gesture with the wand.
The condition for a fireball is both:
Global space is mostly used for gestures that require up or down. Gemini, for example, requires that you target an item, face the tip of the wand downwards and swirl. Grapple requires that you point the wand upwards, then bring it downwards and point.
These two gestures are far more complex than simple checking a directional swish or flick.
For twisting, such as in the Lumos spell, I track the rotation of the wand around its forward axis (in wand-space). The conditions are based on angle - for example, trigger the lumos spell when the wand has been rotated at least 50 degrees.
Swirling is hard.
To track a swirl, I need to be able to track the angle that the tip of the wand has travelled around an arbitrary circle in space.
Circles are defined by two things - center, and radius. To test whether a circle is being drawn, I have to have both of these things. Additionally I need to know how 'circular' the circle is - it should be as even as possible, not squshed like an ellipse.
First, I track the position of the wand over time - in an array of vectors. Each frame I add another position into the list. These are represented by the blue lines in the picture above.
Finding the center is easy - just take the average position of the stored points. The radius can be found by getting the average distance each point is from the center. The center and radii are represented by the red dot and yellow lines in the picture above.
For testing circularity, I calculate the average radial deviation - this is a value that represents how much, on average, each points' radius deviates from the above average radius. In a perfect circle each radius would be equal, so the average radial deviation would be equal to zero.
The more circular a shape, the closer that value will be to zero. So I pick a value - 0.02, from memory - and test to see that the deviation is less than that.
Calculating the angle travelled around the circle is done by guessing the circle normal based on the wand's position, and finding the angle between the current radial vector and the one from the last frame.
Lots of my mods are one item or spell with multiple modes, abilities or uses - Shatterblade and the wand are both examples. Making these modes modular really helps with my own development, but it also allows other modders to make addons for these mods.
So: how can we expose a mod such that it can itself be modded? A great implementation to emulate is the one that B&S itself uses - Item Modules!
This is part of the JSON catalog definition for the U11 Zipline Wheel. The "modules"
key contains a list of item modules. Item modules are classes you can inherit from. In this case, ThunderRoad.ItemModuleReturnInInventory
is an item module that ensures an item returns to the inventory if you leave it alone somewhere.
So, I do something similar for my mods - in this case, the wand:
And in the wand's item JSON:
That's right - modules within modules! When the wand is loaded, it looks through its spell
list and loads all of the spells inside it. That way, modders can use B&S's JSON Array Update system to add their own spells, by adding a new entry to the spells
list that references their own code.
This is also helpful here because I can allow modders to very easily add tutorials for their spells to the list, and have them integrated in with all the other spells in the tutorial system!
The list of spells inside an item module will be a single static instance - so make sure you don't simply say wand.spells = module.spells
, otherwise each wand you spawn will share the same spell list.
The clone method is also simple:
Although I would recommend making this virtual and overridable, so that inheriting modules can add custom behaviour on cloning (like making sure that list properties get cloned and recreated).
The mod is available on my discord, at discord.gg/lyneca. Grab your PC/Nomad role from #roles, read the #rules and download the mod in #open-beta-downloads.
Have fun, and see you next time!