Interaction Nodes

The default way of working with SpriteKit is to have the SKScene instance capture all the inputs and then have logic within that scene file to figure out the user’s intention.

In order for this to work, SKNode instances added to this scene have their isUserInteractionEnabled property set to false by default. This property prevents these nodes from capturing input and are effectively invisible to the event chain.

I enable this property on all of my interactive nodes in AdventureKit as it is at the core of how my instruction messaging system works.

This means that I now have to be aware of the hit test areas and the z-positioning of nodes in order to understand which node is receiving an instruction at any given moment.

If I’m not careful, I might find myself inadvertently increasing the hit test area of a given node and getting confused about why nodes under it aren’t receiving input. This is especially true when using things like particle emitters.

A Convoluted Example

Let’s say there is a car entity with an exhaust emitting smoke.

Example animation showing a blue rectangle that represents a car emitting smoke from the bottom right

The smoke emitter needs to be in the car’s hierarchy, underneath the sprite, so that it follows the sprite as it moves around the scene.

The car is moved by the player dragging it with their finger. This means that it needs to be an interactive node that is always receiving input. However, dragging the exhaust fumes should not move it because, well, it’s smoke and that would just be wrong.

I need the hit test area to look like this:

Screenshot of the blue 'car' emitting smoke. There is a white square around only the purple rectangle to indicate the hit test area

However, if I add the smoke emitter as a child of the interactive node, the actual hit test area looks like this:

Screenshot of the blue 'car' emitting smoke. There is a white square around both the purple rectangle and the smoke particles to indicate that the hit test area extends beyond the bounds of the blue 'car' itself.

This is even more pronounced if I put something under the smoke.

In this example, I have the car set so it will turn black if it receives a click or a tap, while the purple square will disappear it it receives this input:

Animation of the purple square not disappearing when smoke is above it and it is tapped, but disappearing when it is tapped in an area when there is no smoke above it

This just doesn’t feel right. Smoke is ephemeral and gaseous and it feels like I should be able to “reach” through to whatever is underneath.

Right now, the hierarchy looks like this:

Interaction Node (`isUserInteractionEnabled` set to `true)
---> Exhaust emitter

What I need is for the particle emitter’s parent to have its isUserInteractionEnabled set to false. As this is the default behaviour of standard SKNode instances, I can set up a node tree that looks like this:

Regular SKNode (`isUserInteractionEnabled` set to `false`)
---> Interaction Node (`isUserInteractionEnabled` set to `true`)

Then, when I add the emitter, I can add it to the hierarchy so it looks like this:

Regular SKNode
---> Exhaust emitter 
---> Interaction Node

The Bug

In theory, this should be everything I need to do for this to work. Unfortunately, there’s a bug in SpriteKit that prevents this all working correctly. The linked post details a workaround that I use to make sure that the events do get to their intended recipients.

With this fix in place, everything works as expected:

Animated gif showing the disappearing purple square when it is clicked or tapped through the exhaust smoke.

Managing the Node Hierarchy from a Component

Of course, there are situations when child nodes should be part of the hit test area. If I was constructing a character sprite out of individual parts (head, torso, arms, and legs), then each of those parts should increase the size of this frame.

In order to make managing all of this easier, my generic node component sets up this hierarchy when it’s first initialised. It then has a convenience method that takes a bool to let it know whether this new child node should be part of the interactive node (and therefore potentially increasing the size of the hit test area) or not:

func addChild(_ node : SKNode, contributesToInteraction contributes : Bool = false) {
		let container = ( contributes ) ? self.interactionNode : self.node
		container.addChild(child)
}

Z-Positions

The other thing worth mentioning in all of this is that z-positioning allows non-interactive nodes to be placed either above or below the interactive node:

Root SKNode (z-position = 10)
---> Exhaust emitter (z-position = 0)
---> Interaction Node (z-position = 1)
---------> Wheels (z-position = 0)
---------> Body  (z-position = 1)
---------> Water cannon (z-position = 2)
---> Water emitter (z-position = 2)

The z-position of the root node defines its placement in the main scene hierarchy whereas the z-positions of the children define their placement relative to their parents.

This allows complex node hierarchies to be constructed with a mix of nodes that do participate in the interaction system and nodes that don’t.

Animated gif showing the blue 'car' with a huge explosion halo thing coming out of the top and the exhaust coming out of the bottom. Tapping on the purple square even while its covered with all this explosion noise causes it to disappear. The car responds to being dragged around the scene as it should