A Physics Component for an Entity Component System in SpriteKit

Now that I have node components working in SpriteKit, it’s time to add something more dynamic than tapping to add a node that then doesn’t do anything.

Let’s get…physical.

(sorry.)

Creating a Physics Component

I’ll pick up where we left off with the generic node component and use the same Playground that’s available on Github.

In keeping with the idea that components should describe attributes and abilities of entities, the presence of a physics component will define whether or not an entity is even part of the physics engine.

We’ll start with this PhysicsComponent:

  1. The component will have no idea what kind of node it will be attached to, so the size and shape of the body will need to be decided by whatever creates the entity.
  2. When this component gets added to an entity, it checks to see if there’s a node component (which should be the root node of the entity), and applies the physics body to it.
  3. If the component is ever removed, this should be taken as a sign that we don’t want the entity to participate in the physics system any more (whether or not the node itself was removed from the scene is irrelevant).
  4. Our PositionComponent is out of sync with the node’s actual position because it’s been the physics system moving it. We should update our position component to be the same as the node’s current position so that it stays wherever it is and doesn’t snap back to where it started.

Next, we’ll add it to some entities.

In the addNode(to:) function of the GameScene class, before the line entities.append(entity), we’ll add:

This will get the physics working.

Kind of:

An animated gif showing the broken physics engine. A pink square gets added then slowly moves down the screen.

Hmm. Gravity is usually a lot stronger than that.

And why is the square rotating randomly when I add the circle?

Don’t Cross The Streams

Let’s go back to the famous SpriteKit update loop:

The SpriteKit update loop. Each frame, update is called, then the SKScene evaluates actions, then didEvaluateActions is called, then the physics is simulated, then didSimulatePhysics is called, then constraints are applied, then didApplyConstraints is called, finally didFinishUpdates is called and the scene is rendered.

I have a position component that is being run in that first update(_:) method every frame. The position component is applying its current position (which doesn’t change) to the node at the start of every frame.

The SKScene simulates the physics, and updates the position of the node based on the results of that simulation.

Finally, SKView renders the scene using the node’s position, last updated by the physics engine.

At the start of the next frame, the position component puts the node right back where it started, and we go through the whole process again.

Shouldn’t The Node Stay In The Same Place, Then?

Let’s have a look. At the bottom of the GameScene class, we’ll add:

If I override the didSimulatePhysics() method and then print out some information about the body, we can see what’s happening:

The physics engine is not just updating the position of the node, it’s updating its velocity as well and using that in its calculation for where it wants to position it.

As gravity is working its magic on the body, the y-velocity of our node is increasing every frame.

However, at the start of every frame, the position component puts the box back at its initial point. Then the physics component updates the node’s position again based on how far it would travel in the fraction of a second between frames given its (rapidly increasing) velocity. The frame renderer paints the box on screen at the location which is the result of this calculation.

If I update the NodeComponent’s update(deltaTime:) method (which, remember, was acting as my render system) to look like this:

And I make a quick update to the PositionComponent to apply its initial position to the node when it’s added to an entity:

Everything works like it should:

An animated gif showing the physics engine working correctly. Nodes get added then fall off the bottom of the screen as if under the influence of gravity.

When we are working with SpriteKit, the golden rule is that we can either update the position of our nodes manually and not use physics or we relinquish control and let the physics engine do it. We can’t do both.

But What If I Want Control?

Then we need to work with the physics system. First, in the PhysicsComponent, we’ll add a new property called position:

It’s an optional CGPoint because we want to use nil as an indicator that the manual interaction is over.

Then, underneath the closing brace of the the GameScene class, we’ll add this new class:

  1. Because we’re dealing with a single small node, I’m only going to be interested in the first touch. Also, note that this first node in the tree has a reference to the scene its placed in. Useful.
  2. Convert the location of the touch (which will be in the node’s co-ordinate system) into the scene’s co-ordinate system and then apply it to the new Physics position property
  3. This method is the same as the touchesBegan(_:with:) method except for this bit: There may be a time where we want to remove control from the player (for example having a projectile knock the object we’re moving out of our “hand”). Another system could do this by setting the physics position property to nil. However, if the player doesn’t end the touch themselves, touchesEnded(_:with:) doesn’t get called. We therefore need to manually check here to see if the position property is nil. If it is, the touch has been cancelled by another system and we can bail out early.
  4. If the player does end the touch, then we set the position property to nil ourselves which indicates that full control should return to the physics engine.

Next, we want to actually use our new TouchyNode. Replace the current NodeComponent with:

  1. We’ll want a reference to the entity in the node, as it is the node that will be responding to touches (not the entity).
  2. We also have to manually enable isUserInteractionEnabled on the node as it’s off by default (meaning the touches within the node will be ignored).

Then replace the NodeComponent extension (for the second time this post) with:

  1. If we have a physics component, we’re positioning using physics properties and not manually so we need to check for this first.
  2. If the physics position is nil, the engine is back in full control so we do nothing and let the engine work it out.
  3. Do some math: speed = distance / time. The distance is the difference between the touch position and the current node position, and the time is the time between frames (these lines are adapted from this Stack Overflow answer).
  4. Apply this newly calculated velocity to the physics body.

Now, when the physics engine gets hold of the node (remember the physics are simulated after we’ve done all this), the velocity will be set perfectly to move the node from where it currently is to where our finger is each and every frame.

It also means our node works perfectly with any existing physics bodies, and it will maintain momentum if we let go:

An animated gif showing that nodes added take part in the physics simulation, but can also be moved and thrown around manually.

Note that I turned off gravity with self.physicsWorld.gravity = .zero so we can see it easier.

Now we have a working physics system using ECS!

But, really, labels shouldn’t participate in the physics simulation! They’re labels!

Let’s replace:

With:

An animated gif showing that nodes added take part in the physics simulation, but can also be moved and thrown around manually. As labels do not have a PhysicsComponent, they are ignored.

Better! The physics component’s presence defines whether or not the entity it belongs to should take part in a physics simulation. Better yet, they can also be added or removed dynamically to switch the physics for an entity on or off.

These components are getting interesting! Let’s see what else they can do…

  • Share:

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