SpriteKit Tutorial: Adding Contact Tests

Now that the physics is working for this basic node component, let’s make it a full contact sport. Contact detection is a huge benefit of using a physics engine, so in this SpriteKit tutorial I want to make sure that we can still enjoy those benefits while adhering closely to my weird interpretation of Entity Component Systems.

We’ll start by adding a new floor shape at the bottom of the scene, then we’ll test for contact from the falling objects.

Adding the Floor

First I need to do a little refactoring to our rapidly expanding Playground.

We’ll add the floor case to our Shape enumeration so it looks like this:

Then deal with this new case in the ShapeComponent by replacing the current init(shape:) initialiser with:

Next we’ll make some changes to the NodeType enumeration so it looks like this:

CaseIterable is a nice convenience protocol introduced in Swift 4.2 that automatically creates an allCases property that is an array of all the cases in the enum. Neat (although we don’t need it any more).

In the GameScene class, we’ll change the addNode(at:) method to this:

Most of it is the same, things have just moved around a bit. The important parts:

  1. The new method signature includes a parameter to specify the type of node we want to create, instead of cycling through the available options like we did previously.
  2. We add a new case statement to the switch statement for our floor case. We create a physics body that is the same size as the shape. More importantly, we set isDynamic on the body to false, as we want it to affect objects in the simulation but not be effected by objects.
  3. We’ll use the existence of a value in the local body variable to determine whether or not the entity should even get a physics component.

(One day I will have to come back and deal with the fact that this method is seriously bloating my GameScene class and doesn’t feel super ECS-y to me, but that day is not today.)

Still in the GameScene class, in touchesEnded(:with:), we’ll replace self.addNode(at:loc) with:

A tap above the centre line will add a circle, a tap below it will add a square.

Finally, still in the GameScene class, we’ll add the following method:

  1. This sets the anchor point to the middle of the scene.
  2. This adds the floor to the horizontal centre and vertical bottom of the scene.
An animated gif from this SpriteKit tutorial showing a circle being added to a scene, falling to a floor and bouncing realistically.

We have a working floor!

Physics Categories

To get contact testing working, we need to add some categories. Categories work with bitmasks, which I understood in principle after reading this Wikipedia article but in order to really get it I had to spend a bunch of time playing with the programmer’s calculator in macOS.

Having done that, I will now attempt to explain it. Gods help us.

We need the categories to look like this:

What we’re doing here is assigning an integer to a variable, except that by using the binary representation we’re ensuring that only a single bit is set to one. The none category is binary 0000 (=0), the circle is 0001 (=1), the floor is 0010 (=2), and the square is 0100 (=4).

Now, as an example, let’s say our body’s contact test bitmask is:

When we use the OR operator (|) to assign the bitmask, we’re effectively giving this mask a binary value of 0101 (=5). If we look at this binary value as a filter, where the values above it only pass through if the equivalent position in the filter below it is set to 1, then we get this:

The circle and square pass through the filter and the floor does not.

We can also see why it’s important that the categories only have a single “1” value in their binary representation. This ensures that the entire number will “fall through” the filter.

Consider:

So the number that comes out of this will end up representing a different category. The physics engine will be confused and I will be confused and everyone will be sad.

(Note that you can still set multiple categories to a physics body which would give you the same 0110 number above, except in that case the 0110 would represent both the circle category and the floor category. The contact test would then only report that there was a collision with the circle, as it’s the only category that made it through the filter.)

Right.

I hope that’s correct.

Unfortunately, my understanding of this tends to have a half life of about 6 hours, so before I lose it let’s move quickly onwards!

Applying the Categories

In the GameScene class, in addNode(_:at:) method, we’ll update the floor case in the switch statement to look like this:

  1. Here we set the category bit mask of the floor node to floor
  2. And we set the contact test bit mask to circle OR square. This means that anything that touches this floor with the category of circle or square will be reported to the delegate.
  3. We set the category bit mask here to circle
  4. …and here to square.

What usually happens next is that we make our SKScene subclass a delegate of the SKhysicsWorld’s contactDelegate property and implement the didBegin(_:) method of SKPhysicsContactDelegate protocol.

In that method we’ll grab the nodes from the bodies and then figure out what we want to do with them.

Hmm.

We’ll be testing two entities that have come in contact with each other.

Then changing their properties based on that contact.

Sounds suspiciously like we need…

A Contact System

More components! ADD ALL THE COMPONENTS!

We’ll create one that looks like this:

  1. This is an array of entities that have collided with the entity holding this component. The reason that this is an array is that multiple collisions might have taken place in the 16ms between frames.
  2. This method accepts an optional entity and, if it is actually an entity, appends it to the array. It’s optional because we want the contact delegate to be as dumb as possible.

Next, we’ll give this component to the floor entity. By only giving it to the floor entity, we’re effectively allowing it and only it to respond to contact events.

In the addNode(_:at:) method in GameScene, we’ll add this to the floor case in the switch statement:

Then we’ll make an extension of this component that will act like our system

  1. Go through each entity captured in the last frame and ensure it has a PhysicsComponent (it should do, but a lot of things can happen between frames!)
  2. Check to see what category we have. If it’s a circle, remove it from the scene. If it’s a square, make it dance!
  3. Empty the array so that we don’t keep trying to remove the same nodes over and over.

Next, in’ the GameScene class, we’ll add this system to our array of systems. Replace the lazy var systems [...] property with:

Still in GameScene, we’ll add this to the didMove(to:) method:

Finally, we’ll add the contact delegate method as an extension to our GameScene class:

That’s a lot of optionals!

The order of bodies is not guaranteed by SpriteKit, so either bodyA or bodyB could be the floor. As only one of the bodies will have the ContactComponent, we can use optional chaining to call the didMakeContact(:) method on both bodies passing in their opposite.

Only the entities with the ContactComponent will respond (which is just the floor, in this case).

We have a delegate method that is as dumb as a bag of rocks, we have circles that disappear, and we have dancing squares!

An animated gif from this SpriteKit tutorial showing a circle and a square being added to a scene and falling to the floor. The circle disappears, and the square bounces its way over to the right.

A good day’s work.

The Playground is available on GitHub.

This SpriteKit Tutorial Seems…Overly Complicated

A common theme with my exploration of ECS is that I end up doing a lot more work to achieve something incredibly basic.

To be honest, I’m very much still testing ECS as a methodology within SpriteKit. I want to see what benefits (if any) can be had from doing things this way as an indie developer.

I can imagine in larger games, where behaviour is encapsulated into individual systems, it can make a lot of sense. Someone can be working on the contact system and someone else can be working on the render system and a third person can be working on the animation system. As they’re all in separate files and don’t really rely on knowledge of one another, there are less conflicts and iteration becomes that much faster—suddenly in the next build characters are animating but nothing got broken in between.

I will say that, in the docs for the SKPhysicsContactDelegate didUpdate(_:) method, we find this important notice:

The physics contact delegate methods are called during the physics simulation step. During that time, the physics world can’t be modified and the behavior of any changes to the physics bodies in the simulation is undefined. If you need to make such changes, set a flag inside didBegin(_:) or didEnd(_:) and make changes in response to that flag in the update(_:for:) method in a SKSceneDelegate.

Storing the entities we’re interested in and then only dealing with them in the update(deltaTime:) method of the ContactComponent means that we can modify anything about our entity. Even if we start futzing with the physicsBody, we can be sure we’re not going to interfere with the physics simulation—the physics are only simulated after the systems have completed processing.

If we set the contact system to take place first, we also now know that any contacts that took place in the last frame will be dealt with this frame before anything is rendered to screen.

Another benefit is that components and systems can also be isolated to become that much more testable.

Finally, if you’re trying to maximise performance, then it can be really helpful to track down which particular system is taking the longest to process and figure out how it can be optimised, and you can do this without breaking the build (in theory, at least).

These are all certainly benefits, but most of these are at such a low level that for small games they may not be all that relevant.

Don’t worry, I’m not giving up on ECS in SpriteKit yet—I want to see what else it can do for me first!

  • Share:

Get all of my latest posts direct to your inbox! Pop in your email address here: