I realised recently that I haven't ever really discussed the technicals behind Adventures of Kidd (Adventures, shorthand (preorders avail!)), mostly because I thought that, that would involve copy/pasting sections of code which wouldn't really be that interesting.. but there are actually some concepts that are worth exploring. This first post serves as a 101 of sorts.
Adventures is built using the SpriteKit Framework. Everything that the player would perceive as part of the interactive game world, like the enemies and the power ups maintain a position, a bounding box and a state.
Each state in a sprites state machine is a single enum that describes the condition of the sprite. This can be anything from idle, running, spawning or even bouncing. A sprite can only be exactly one state at any time.
Our main character, Kidd, has a unique state machine in that it mostly relies on player input to determine the next state. Enemy sprites are autonomous state machines, which is a slightly different (and probably more interesting) feature to look at.
1a.Choose Action is blue since it's a function & not a state
The current state of a character influences (but doesnt necessarily determine) the next state it's allowed to transition to. You can see a rudimentary example of this in 1a. In code it doesnt look nearly as simple and there are closer to 80 states to manage. In fact, tracking down loose ends when a state machine breaks is probably one of the most common bugs I come across when implementing new behaviour or a new character. If all the relevant states aren't implemented, things break like in 1b. where the sprite becomes frozen.
1b.Incomplete state machines can break sprites behaviour.
The state machine transitions to the next state either when the state is considered completed (like jump, hurt, block) but others expire. Using 1a, as long as there's no external interference, 'jump' will complete when the sprite reaches it's destination and the animation finishes. Conversely, when the state machine reaches 'bounce', the animation has completed and the sprite has reached it's destination. So, states like idle and bounce 'expire' after a random time within a certain range that varies from character to character. Run also expires, but exclusively under the conditions of positioning (because it's a looped animation).
Most characters from level to level use a similar state machine structure, with differences only in unique behaviour. The way I approach modifying difficulty or aggression for characters is by changing the weighting of choices inside the "choose action" function. At the minute the weightings are set statically, though I'd be curious to see the effects of dynamic weighting. Something like reducing the weight of an action if it's been chosen too (or more) frequently.
Each sprite always has at least 1 bounding box. The bounding boxes are supposed to be a way for the game to represent how much 'space' a sprite is taking up, which is necessary for collision detection. Depending on what state the sprite is in will determine which animation it's running and also the size of the bounding box.
2a.Bounding boxes and collision detection
When a sprite is in a attack state there might be more than one bounding box. In code it's called anattackRect. You can see an example in 2a. When the game performs collision detection checks, at the most basic level all it's really doing is checking whether the bounding boxes overlap.
After a collision happens, some events like reducing the sprites health and changing it's state happen. The boxes don't disappear after this though and continue to overlap for a few more 'frames'. To stop these events firing multiple times the sprites that collide share a collision tag, which is a random number between 0 and 9,999 that's generated when the attackRect is created. If these numbers match then the collision is ignored.
To understand how positioning works it's helpful to understand where Adventures lives within the game engine. The entire Adventures universe lives inside an event loop, which comes in the form of a update method provided by the SKScene object type. The functions only parameter is the current system time. So, if we define the current time as x then we can measure the amount of time that's passed between loops as t = x' - x. The event loop will try to run 60 times per second (for 60fps). You'd want t to be no more than 1/60th of a second or it will mean the game starts to become choppy. So essentially, you have to fit all the game mechanics into ~0.0167 seconds. Smartphones are pretty powerful these days though, so that makes life a touch easier.
If you think back to one of your maths classes you might remember seeing something like 3b. That's the formula that's used to position characters appropriately (turns out I did learn stuff in school).
During each loop, we take t and pass it down the object hierarchy to all the active sprites. Then, to determine the position of any sprite in this iteration of the loop it's d = v * t. Where v varies depending on the state.
You can also do some neat stuff by manipulating t like placing a single character (or more) into slow motion just by reducing t as it's processed through the sprite. Out of curiosity I tried t as a negative value and it's not pretty1.
These 3 properties are used in various places for different things, but they all combine in the GameLayer collision detection methods. The GameLayer which manages all the sprites. It knows what each sprite is doing (state), where it's doing it (position) and how much 'space' it's occupying (bbox). The collision detection is probably the longest running section of the event loop. It runs at about O(2n+n2) where n is the number of sprites. This is due to the different types of objects that can collide. n is rarely over about 6 but even when that number creeps up it's not so bad because the first conditional is how far apart the sprites we're checking are. If it's not a qualifying distance no further checks take place. So it actually stays pretty performant.
That's Adventures 101. I hope it was insightful. Tweet me if you have any questions.
1.Sprites move in the negative x,y directions but also to seemingly artibtrary positions that I didnt bother to investigate.