SpriteKit Box2D

This project integrates Box2D v3 in C with Apple SpriteKit in Swift.

SpriteKit-Box2D-Screenshots.png

Videos

Getting Started

Try different values in the preset functions, such as:

static func pyramid(_ scene: SpriteKit_Box2D.Scene) {
    scene.removeContent()
    scene.setupBox2D(gravityY: -10) /// Gravity strength
    scene.createGround(width: 10000, startY: -300)
    scene.createPyramid(baseCount: 50, startY: -250) /// Pyramid width and initial height
}

You can also navigate to the factory functions and change them directly.

Context

SpriteKit built-in physics engine is based on Box2D, likely from an older 2.x generation. This project uses Box2D 3.x directly.

Box2D 3.0 was first released in 2024 as a major rewrite with a new C API. It brings improved collision handling, better performance, better stability for demanding simulations, and features that are not exposed through SpriteKit’s SKPhysicsWorld.

Box2D 3 is also designed with determinism in mind. SpriteKit's physics implementation is nice to use, but it is not deterministic. See the Determinism section of my SKRenderer Demo for more information about determinism in SpriteKit.

Swift & C

Because Box2D 3 is written in C, this sample app also shows how to mix Swift and C. I wrote a tutorial on how to mix Swift with C in an Xcode project.

Minimal Scene

To setup Box2D in SpriteKit, a minimal scene can be structured as follows:

For a minimal setup, stepping Box2D directly from SpriteKit update(_:) is enough. For a production app, use a fixed-step accumulator so Box2D updates steadily even if the rendering engine draws at 60 fps, 120 fps, or with occasional frame drops.

Update and Fixed Update

A physics engine advances with a given increment of time called a timestep, typically 1/60 second. In Box2D, the method that tells the engine to simulate one additional increment of time is called b2World_Step, and it takes a timestep.

Usually, the goal of the simulation is to stay in sync with real time. 3 seconds of wall-clock time should simulate 3 seconds of physics time. But if we call b2World_Step directly from update(_:), we depend on how steady the render refresh cycle is: a frame may take too long before calling the next update, or the device might be running at 120fps, twice the physics rate.

If we pass the same timestep regardless of rendering speed, we may get slow/fast physics motion depending on update speed. If we pass a variable timestep to the physics engine, we won't get similar results, because a physics solver doesn't produce the same outcome from different increments of time.

We need a fixed update. A fixed update is a function that keeps simulation time aligned with real-time. A common way to implement it is with the accumulator pattern, documented in Glenn Fiedler's classic Fix Your Timestep! post. It works like this:

With this pattern, the rendering engine may provide variable time, but the accumulator converts it into stable ticks.

In SpriteKit, the implementation looks like this:

class MyScene: SKScene {

    private let fixedTimestep: TimeInterval = 1 / 60
    private var lastUpdateTime: TimeInterval?
    private var accumulatedTime: TimeInterval = 0 /// The accumulator

    override func update(_ currentTime: TimeInterval) {
        /// Calculate delta time
        guard let lastUpdateTime else {
            lastUpdateTime = currentTime
            return
        }
        let deltaTime = currentTime - lastUpdateTime
        self.lastUpdateTime = currentTime

        /// Accumulate time from display refresh cycle
        accumulatedTime += deltaTime

        /// Run code that updates once per rendered frame
        //..
        
        /// Check if enough real time has passed to run fixed update
        while accumulatedTime >= fixedTimestep {
            /// Run code on fixed ticks
            fixedUpdate(fixedTimestep)
            accumulatedTime -= fixedTimestep
        }
    }
    
    func fixedUpdate(_ fixedTimestep: TimeInterval) {
        /// Run the Box2D simulation with a fixed time increment
        b2World_Step(b2WorldId, Float(fixedTimestep), 4)
    }

}

Notice that:

In this project, fixed update is called in didSimulatePhysics, not in update(_:). didSimulatePhysics is executed after SpriteKit has evaluated actions and simulated its own physics. This lets the app pass SpriteKit action or physics results into Box2D before stepping Box2D, if needed later. You may choose a different structure.

Timestep

The physics engine doesn't have to run in sync with real-time. We could speed up or slow down the rate at which each step is called, using a time scale parameter:

class MyScene: SKScene {

    /// 1 = normal speed, 0.5 = slow motion, 2 = fast forward.
    private var timeScale: CGFloat = 1
    
    override func update(_ currentTime: TimeInterval) {
        ///...

        /// Use the time scale for accumulating time
        accumulatedTime += deltaTime * timeScale
        
        ///...
    }

}

If we use a time scale of 2, fixed update will be called twice more often, and the physics engine will simulate 2 seconds in 1 second of real-time. How fast can we advance in time? Ignoring rendering, as fast as the computer can process a step.

If we use a time scale of 0.5 or 0.1, physics will only be updated 30 or 6 times per second, respectively. The motion will appear jagged, unless a rendering-side interpolation is added.

Regardless of time scale, the physics engine should produce the same result after the same number of steps, because each step still uses the same timestep. Time scale changes how often we call fixed update relative to real time. It does not change the size of each physics step.

What happens if we change the timestep? A simulation stepped at 1/60 and a simulation stepped at 1/120 are not the same simulation. They produce different results. Many applications use 1/60 or 60Hz as default. But shorter timesteps can be very interesting. For example, in this project, switching the timestep to 1/120 on a ProMotion device makes dragging significantly more responsive.

That happens because dragging is controlled by a motor joint. Each fixed step updates the pointer body and gives the joint a new target. At 1/120, the target is updated twice as often as at 1/60, so the dragged body follows touch input with less delay. The tradeoff is cost: the physics engine now has to process twice as many steps for the same amount of simulated time on all devices it runs on, regardless of the rendering refresh rate.

In conclusion, time scale changes how fast fixed steps are consumed in real time. The timestep changes the simulation itself.

Determinism

Box2D determinism is truly remarkable. Given identical inputs, Box2D produces bit-for-bit identical results across multiple runs. This makes rollback, replay, and reproducible simulation possible.

SpriteKit-Box2D-Determinism-1.png

To explore determinism, I set up a test scene such as:

Findings

Box2D is deterministic, provided:

Creation order is important and part of the simulation input.

Creation Order

A transient body test explores the creation order question further: creating and destroying an extra body during the simulation to see if its presence would affect the outcome of the remaining bodies.

Test A:

Test B:

Test C:

It seems that if a body was created but did not collide with the rest of the simulation, its creation order did not affect the outcome of the other bodies.

References

License

This project is licensed under the Apache License 2.0.

If this project helps your work, attribution or a link back is appreciated: https://github.com/AchrafKassioui/SpriteKit-Box2D

The Box2D source code included in this project is licensed separately under the MIT License.