Implement a GKComponent for Contact Tests with SpriteKit

Implementing an automatic system for contact test notifications on collisions with entities and components on top of GameplayKit and SpriteKit.

Johan Steen
by Johan Steen

This week I've been coding an implementation with Swift on top of SpriteKit and GameplayKit to handle contact test notifications between entities in my game.

My goal was to come up with a solution where I can avoid to have large chunks of repetitive boilerplate code when configuring contact tests for each entity that will trigger notifications on contact with other entities.

SpriteKit's physics system will be used for detecting contact between entities and as I'm using the entity component system design pattern I'll let a GameplayKit component handle the contact notification configuration for an entity.

When I add the component to an entity I want it to more or less do all of these steps as close to automatic as possible.

  • Create a categoryBitMask.
  • Create a contactTestBitMask.
  • (We won't need a collisionBitMask.)
  • Define a SKPhysicsBody.
  • Assign the SKPhysicsBody to the SKSpriteNode in the entity's RenderComponent.

Ideally I'd love an API as simple as this...

class Player: GKEntity {
    override init() {
        // ...

        // Add physics component to handle contact tests.
        addComponent(PhysicsComponent())
    }
}

Where I can just add a PhysicsComponent, and voila, it determines what entity it belongs to and configures itself based on that.

Defining the Physics Body

While I at one point had a solution just as simple as the one above, I wasn't happy with how that limited the definition possibilities of what kind of SKPhysicsBody to use for an entity. I could make it fully automatic by basing it on the entity's sprite size or even texture. But many times a simple geometric shape will be enough to the define the physic's shape, which is much better to use for performance reasons. I wanted to keep the flexibility to customize that on a per entity basis.

That led to the decision that I'll keep the definition of the SKPhysicsBody outside of the PhysicsComponent. Architecture wise I was choosing between either passing the physics body via the component constructor or defining it as a property of the entity that the component can read.

For now I've decided to go with passing the physics body via the constructor. I might revise that and move it to a property of the entity as I move forward. I'll see what feels right when I start to add more entities to the game and what will work out best at that time.

Anyway, we are now at a point where the PhysicsComponent takes one argument when adding it to an entity, which gives us code looking something like this.

class PhysicsComponent: GKComponent {
    let physicsBody: SKPhysicsBody

    init(physicsBody: SKPhysicsBody) {
        self.physicsBody = physicsBody
    }
}

class Bullet: GKEntity {
    override init() {
        addComponent(PhysicsComponent(physicsBody: SKPhysicsBody(rectangleOf: CGSize(width: 10, height: 30))))
    }
}

Doing it this way also allows us to set other physics properties on a per entity basis. One candidate for that is usesPreciseCollisionDetection. In most cases that can be left at the default, being turned off. But for the player's bullets I want it to be on, to ensure that the engine will never miss the contact of a player bullet and an enemy entity.

Let's avoid rage quits.

The other way around, if the contact between an enemy fired bullet and the player would occasionally not be triggered, will not be a problem. In that case I won't need to use precise collisions, which is good as they do come with a performance hit.

I consider it almost sort of a built-in shmup version of coyote time that an enemy bullet occasionally can miss the player to add some delight and make the player feel like an ace that escapes a narrow scenario once in a while.

For a player bullet entity, where we want to ensure every hit counts, we would add the physics component by doing something like this.

class PlayerBullet: GKEntity {
    override init() {
        let physicsComponent = PhysicsComponent(physicsBody: SKPhysicsBody(rectangleOf: CGSize(width: 10, height: 30)))
        physicsComponent.physicsBody.usesPreciseCollisionDetection = true

        addComponent(physicsComponent)
    }
}

Physics Body Types

Defining and managing physics body types with their bitmask based contact and collision relationships in SpriteKit can quickly get a bit messy, hard to manage and difficult to overview.

To avoid that I wanted a central place to define physics categories and contact tests that the PhysicsComponent can read from.

I've found the best approach to accomplish that is to use an OptionSet struct for categorizing and defining relationships between physics bodies in a game.

An initial setup would look like this, where each option defines the bitmask for the type's category.

struct PhysicsBodyType: OptionSet {
    let rawValue: UInt32

    static let player       = PhysicsBodyType(rawValue: 1 << 0)
    static let enemy        = PhysicsBodyType(rawValue: 1 << 1)
    static let playerBullet = PhysicsBodyType(rawValue: 1 << 2)
    static let enemyBullet  = PhysicsBodyType(rawValue: 1 << 3)
}

With that in place we can go ahead and add a computed property to the struct to retrieve the category bitmask for a physics body type.

var categoryBitMask: UInt32 {
    return rawValue
}

We can now set a category for any physics body using the struct properties and have the values managed in the struct.

physicsBody.categoryBitMask = PhysicsBodyType.player.categoryBitMask

As we are going to want this to happen automatically, I'm adding a static factory method to the struct that will take an entity and return the correct instance of the struct to be used with that entity.

/// Convenience method to get the collision options for an entity type.
static func forEntity(_ entity: GKEntity) -> PhysicsBodyType? {
    switch entity {
    case is Enemy:        return self.enemy
    case is Player:       return self.player
    case is PlayerBullet: return self.playerBullet
    case is EnemyBullet:  return self.enemyBullet
    default:              return nil
    }
}

Now we can get the values we need even easier.

let physicsBodyType = PhysicsBodyType.forEntity(player)
physicsBody.categoryBitMask = physicsBodyType.categoryBitMask

Next up is that we also need to manage the contact relationships within the struct so we can generate bitmasks for contact testing. We are going to use a dictionary for those definitions. To be able to use the struct options as keys in a dictionary we also need to make it hashable.

With that in mind, we're ending up with these additions to the struct.

struct PhysicsBodyType: OptionSet, Hashable {
    func hash(into hasher: inout Hasher) {
        hasher.combine(rawValue)
    }

    static var contactTestNotifications: [PhysicsBodyType: [PhysicsBodyType]] = [
        .player: [.enemy, .enemyBullet],
        .playerBullet: [.enemy]
    ]
}

The contactTestNotifications dictionary is where we will be defining what will trigger a notification. In the code example above a .player entity will trigger a notification if it hits an .enemy or an .enemyBullet entity. Also a .playerBullet hitting an .enemy will trigger a notification.

As we move forward we can just keep adding entities and relationships to this dictionary to have it managed in one place.

Just as we did with the categoryBitMask we will use a computed property, this time with a bitwise or operation, to compute the bitmask for contact tests.

var contactTestBitMask: UInt32 {
    let bitMask = PhysicsBodyType
        .contactTestNotifications[self]?
        .reduce(PhysicsBodyType(), { result, physicsBodyType in
            return result.union(physicsBodyType)
        })

    return bitMask?.rawValue ?? 0
}

Now we can assign a contactTestBitMask just as easily as we did previously with the categoryBitMask.

let physicsBodyType = PhysicsBodyType.forEntity(player)

physicsBody.categoryBitMask = physicsBodyType.categoryBitMask
physicsBody.contactTestBitMask = physicsBodyType.contactTestBitMask

This pretty much completes our OptionSet struct for PhysicsBodyType. Now we have one place where we can add new body categories, manage the relationships in a dictionary and use the factory to quickly get the bitmasks we need.

Completing the Physics Component

With all the above done, completing the physics component is now quite simple. We will override the didAddToEntity method of GKComponent and let all the automagic configuration happen there.

That will give us an addition to the PhysicsComponent that will look something like this.

class PhysicsComponent: GKComponent {
    override func didAddToEntity() {
        guard let entity = entity,
        let physicsBodyType = PhysicsBodyType.forEntity(entity),
        let renderComponent = entity.component(ofType: RenderComponent.self) else {
                fatalError("The entity needs both a physics body type defined and a render component.")
        }

        physicsBody.categoryBitMask = physicsBodyType.categoryBitMask
        physicsBody.contactTestBitMask = physicsBodyType.contactTestBitMask
        physicsBody.collisionBitMask = 0

        renderComponent.node.physicsBody = physicsBody
    }
}

We start with a guard to make sure that the current entity has both a PhysicsBodyType defined and a RenderComponent added. Then we simply assign the bitmasks for categoryBitMask and contactTestBitMask while setting collisionBitMask to 0 as we won't use collision physics in a game like this.

And finally we assign the physics body to the sprite node in the RenderComponent.

And there we have it. A system coded in Swift for contact notifications which is easy to use, maintain and that won't add any unnecessary performance hits.

Conclusion

This will be a robust setup as far as I can tell at this time. If I prove myself wrong I will revisit the subject in a future post.

I'll see how the implementation will hold up as I keep developing the game. I bet there will be some minor tweaks but I hope there will be no massive changes. Where and how I define the SKPhysicsBody is something I can see that I might tinker with a bit more.

Discuss this article

The conversation has just started. Comments? Thoughts?

I'm on Twitter and Mastodon, or come to the Discord server and hang out.

Sign up to the newsletter to get occasional emails about my game development.