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:
enum Shape { case circle, square, label, sprite, floor }
Then deal with this new case in the ShapeComponent
by replacing the current init(shape:)
initialiser with:
init( shape : Shape) { switch shape { case .circle: self.shape = SKShapeNode(circleOfRadius: 20) self.shape.fillColor = SKColor.purple case .square: self.shape = SKShapeNode(rectOf: CGSize(width: 50, height: 50)) self.shape.fillColor = SKColor.red case .floor: self.shape = SKShapeNode(rectOf: CGSize(width: 800, height: 20)) self.shape.fillColor = SKColor.orange } super.init() }
Next we’ll make some changes to the NodeType
enumeration so it looks like this:
enum NodeType : CaseIterable { case circle, square, label, sprite, floor }
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:
// 1. func addNode(_ type : NodeType, at point:CGPoint){ let entity = GKEntity() let nodeComponent = NodeComponent() let typeComponent : GKComponent let body: SKPhysicsBody? switch type { // 2. case .floor: typeComponent = ShapeComponent(shape: .floor) body = SKPhysicsBody(rectangleOf: CGSize(width: 800, height: 20)) body?.isDynamic = false case .circle: typeComponent = ShapeComponent(shape: .circle) body = SKPhysicsBody(circleOfRadius: 20) case .square: typeComponent = ShapeComponent(shape: .square) body = SKPhysicsBody(rectangleOf: CGSize(width: 50, height: 50)) case .label: typeComponent = LabelComponent(text: "Hello!") body = nil case .sprite: typeComponent = SpriteComponent() body = SKPhysicsBody(rectangleOf: CGSize(width: 100, height: 100)) } if let typeComponent = typeComponent as? ChildNode { nodeComponent.node.addChild(typeComponent.asNode()) } let positionComponent = PositionComponent(pos: point) let scaleComp = ScaleComponent() entity.addComponent(scaleComp) entity.addComponent(nodeComponent) entity.addComponent(positionComponent) // 3. if let hasBody = body { let physics = PhysicsComponent(body: hasBody) entity.addComponent(physics) } self.entities.append(entity) self.addChild(nodeComponent.node) for system in systems { system.addComponent(foundIn: entity) } }
Most of it is the same, things have just moved around a bit. The important parts:
- 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.
- We add a new case statement to the
switch
statement for ourfloor
case. We create a physics body that is the same size as the shape. More importantly, we setisDynamic
on the body tofalse
, as we want it to affect objects in the simulation but not be effected by objects. - 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:
if loc.y > 0 { self.addNode(.circle, at:loc) } else { self.addNode(.square, at:loc) }
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:
override func didMove(to view: SKView) { // 1. self.anchorPoint = CGPoint(x: 0.5, y: 0.5) // 2. self.addNode(.floor, at: CGPoint(x: 0, y: -(self.size.height / 2))) }
- This sets the anchor point to the middle of the scene.
- This adds the floor to the horizontal centre and vertical bottom of the scene.
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:
struct PhysicsCategory { static let none : UInt32 = 0 static let circle : UInt32 = 0b1 static let floor : UInt32 = 0b10 static let square : UInt32 = 0b100 }
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:
body.contactTestBitmask = PhysicsCategory.circle | PhysicsCategory.square
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:
Circle Floor Square 0001 0010 0100 0101 0101 0101 ------ ------ ------ 0001 0000 0100
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:
BadCategory 0110 (=6) 0100 ------ 0100 (=4)
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:
case .floor: typeComponent = ShapeComponent(shape: .floor) body = SKPhysicsBody(rectangleOf: CGSize(width: 800, height: 20)) body?.isDynamic = false // 1. body?.categoryBitMask = PhysicsCategory.floor // 2. body?.contactTestBitMask = PhysicsCategory.circle | PhysicsCategory.square case .circle: typeComponent = ShapeComponent(shape: .circle) body = SKPhysicsBody(circleOfRadius: 20) // 3. body?.categoryBitMask = PhysicsCategory.circle case .square: typeComponent = ShapeComponent(shape: .square) body = SKPhysicsBody(rectangleOf: CGSize(width: 50, height: 50)) // 4. body?.categoryBitMask = PhysicsCategory.square
- Here we set the category bit mask of the floor node to
floor
- 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
orsquare
will be reported to the delegate. - We set the category bit mask here to
circle
… - …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:
class ContactComponent : GKComponent { // 1. var targetEntities : [GKEntity] = [] // 2. func didMakeContact( with entity : GKEntity? ) { guard let entity = entity else { return } self.targetEntities.append(entity) } }
- 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.
- 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:
let contactComponent = ContactComponent() entity.addComponent(contactComponent)
Then we’ll make an extension of this component that will act like our system
override func update(deltaTime seconds: TimeInterval) { for entity in targetEntities { // 1. guard let body = entity.component(ofType: PhysicsComponent.self)?.body else { continue } // 2. if body.categoryBitMask == PhysicsCategory.circle { entity.component(ofType: NodeComponent.self)?.node.removeFromParent() } else { body.applyImpulse(CGVector(dx:10, dy: 25)) } } // 2. targetEntities.removeAll() }
- 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!) - 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!
- 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:
lazy var systems : [GKComponentSystem] = { let contact = GKComponentSystem(componentClass: ContactComponent.self) let render = GKComponentSystem(componentClass: NodeComponent.self) return [contact, render] }()
Still in GameScene
, we’ll add this to the didMove(to:)
method:
self.physicsWorld.contactDelegate = self
Finally, we’ll add the contact delegate method as an extension to our GameScene class:
extension GameScene : SKPhysicsContactDelegate { func didBegin(_ contact: SKPhysicsContact) { contact.bodyA.node?.entity?.component(ofType: ContactComponent.self)?.didMakeContact(with: contact.bodyB.node?.entity) contact.bodyB.node?.entity?.component(ofType: ContactComponent.self)?.didMakeContact(with: contact.bodyA.node?.entity) } }
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!
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!