featured image
JingleSmash

Jingle Smash: Choosing a Physics Engine

 - 

This is part 2 of my series on how I built Jingle Smash, a block smashing WebVR game .

The key to a physics based game like Jingle Smash is of course the physics engine. In the Javascript world there are many to choose from. My requirements were for fully 3D collision simulation, working with ThreeJS, and being fairly easy to use. This narrowed it down to CannonJS, AmmoJS, and Oimo.js: I chose to use the CannonJS engine because AmmoJS was a compiled port of a C lib and I worried would be harder to debug, and Oimo appeared to be abandoned (though there was a recent commit so maybe not?).

CannonJS

CannonJS is not well documented in terms of tutorials, but it does have quite a bit of demo code and I was able to figure it out. The basic usage is quite simple. You create a Body object for everything in your scene that you want to simulate. Add these to a World object. On each frame you call world.step() then read back position and orientations of the calculated bodies and apply them to the ThreeJS objects on screen.

While working on the game I started building an editor for positioning blocks, changing their physical properties, testing the level, and resetting them. Combined with physics this means a whole lot of syncing data back and forth between the Cannon and ThreeJS sides. In the end I created a Block abstraction which holds the single source of truth and keeps the other objects updated. The blocks are managed entirely from within the BlockService.js class so that all of this stuff would be completely isolated from the game graphics and UI.

Physics Bodies

When a Block is created or modified it regenerates both the ThreeJS objects and the Cannon objects. Since ThreeJS is documented everywhere I'll only show the Cannon side.

let type = CANNON.Body.DYNAMIC
if(this.physicsType === BLOCK_TYPES.WALL) {
    type = CANNON.Body.KINEMATIC
}

this.body = new CANNON.Body({
    mass: 1,//kg
    type: type,
    position: new CANNON.Vec3(this.position.x,this.position.y,this.position.z),
    shape: new CANNON.Box(new CANNON.Vec3(this.width/2,this.height/2,this.depth/2)),
    material: wallMaterial,
})
this.body.quaternion.setFromEuler(this.rotation.x,this.rotation.y,this.rotation.z,'XYZ')
this.body.jtype = this.physicsType
this.body.userData = {}
this.body.userData.block = this
world.addBody(this.body)

Each body has a mass, type, position, quaternion, and shape.

For mass I’ve always used 1kg. This works well enough but if I ever update the game in the future I’ll make the mass configurable for each block. This would enable more variety in the levels.

The type is either dynamic or kinematic. Dynamic means the body can move and tumble in all directions. A kinematic body is one that does not move but other blocks can hit and bounce against it.

The shape is the actual shape of the body. For blocks this is a box. For the ball that you throw I used a sphere. It is also possible to create interactive meshes but I didn’t use them for this game.

An important note about Boxes. In ThreeJS the BoxGeometry takes the the full width, height, and depth in the constructor. In CannonJS you use the extent from the center, which is half of the full width, height, and depth. I didn’t realize this when I started, only to discover my cubes wouldn’t fall all the way to the ground. :)

The position and quaternion (orientation) properties use the same values in the same order as ThreeJS. The material refers to how that block will bounce against others. In my game I use only two materials: wall and ball. For each pair of materials you will create a contact material which defines the friction and restitution (bounciness) to use when that particular pair collides.

const wallMaterial = new CANNON.Material()
// …
const ballMaterial = new CANNON.Material()
// …
world.addContactMaterial(new CANNON.ContactMaterial( 
	wallMaterial,ballMaterial,
    {
        friction:this.wallFriction,
        restitution: this.wallRestitution
    }
))

Gravity

All of these bodies are added to a World object with a hard coded gravity property set to match Earth gravity (9.8m/s^2), though individual levels may override this. The last three levels of the current game have gravity set to 0 for a different play experience.

const world = new CANNON.World();
world.gravity.set(0, -9.82, 0);

Once the physics engine is set up and simulating the objects we need to update the on screen graphics after every world step. This is done by just copying the properties out of the body and back to the ThreeJS object.

this.obj.position.copy(this.body.position)
this.obj.quaternion.copy(this.body.quaternion)

Collision Detection

There is one more thing we need: collisions. The engine handles colliding all of the boxes and making them fall over, but the goal of the game is that the player must knock over all of the crystal boxes to complete the level. This means I have to define what knock over means. At first I just checked if a block had moved from its original orientation, but this proved tricky. Sometimes a box would be very gently knocked and tip slightly, triggering a ‘knock over’ event. Other times you could smash into a block at high speed but it wouldn’t tip over because there was a wall behind it.

Instead I added a collision handler so that my code would be called whenever two objects collide. The collision event includes a method to get the velocity at the impact. This allows me to ignore any collisions that aren’t strong enough.

You can see this in player.html

function handleCollision(e) {
    if(game.blockService.ignore_collisions) return

    //ignore tiny collisions
    if(Math.abs(e.contact.getImpactVelocityAlongNormal() < 1.0)) return

    //when ball hits moving block,
    if(e.body.jtype === BLOCK_TYPES.BALL) {
        if( e.target.jtype === BLOCK_TYPES.WALL) {
            game.audioService.play('click')
        }

        if (e.target.jtype === BLOCK_TYPES.BLOCK) {
            //hit a block, just make the thunk sound
            game.audioService.play('click')
        }
    }

    //if crystal hits anything and the impact was strong enought
    if(e.body.jtype === BLOCK_TYPES.CRYSTAL || e.target.jtype === BLOCK_TYPES.CRYSTAL) {
        if(Math.abs(e.contact.getImpactVelocityAlongNormal() >= 2.0)) {
            return destroyCrystal(e.target)
        }
    }
    // console.log(`collision: body ${e.body.jtype} target ${e.target.jtype}`)
}

The collision event handler was also the perfect place to add sound effects for when objects hit each other. Since the event includes which objects were involved I can use different sounds for different objects, like the crashing glass sound for the crystal blocks.

Firing the ball is similar to creating the block bodies except that it needs an initial velocity based on how much force the player slingshotted the ball with. If you don’t specify a velocity to the Body constructor then it will use a default of 0.

fireBall(pos, dir, strength) {
    this.group.worldToLocal(pos)
    dir.normalize()
    dir.multiplyScalar(strength*30)
    const ball = this.generateBallMesh(this.ballRadius,this.ballType)
    ball.castShadow = true
    ball.position.copy(pos)
    const sphereBody = new CANNON.Body({
        mass: this.ballMass,
        shape: new CANNON.Sphere(this.ballRadius),
        position: new CANNON.Vec3(pos.x, pos.y, pos.z),
        velocity: new CANNON.Vec3(dir.x,dir.y,dir.z),
        material: ballMaterial,
    })
    sphereBody.jtype = BLOCK_TYPES.BALL
    ball.userData.body = sphereBody
    this.addBall(ball)
    return ball
}

Next Steps

Overall CannonJS worked pretty well. I would like it to be faster as it costs me about 10fps to run, but other things in the game had a bigger impact on performance. If I ever revisit this game I will try to move the physics calculations to a worker thread, as well as redo the syncing code. I’m sure there is a better way to sync objects quickly. Perhaps JS Proxies would help. I would also move the graphics & styling code outside, so that the BlockService can really focus just on physics.

While there are some more powerful solutions coming with WASM, today I definitely recommend using CannonJS for the physics in your WebVR games. The ease of working with the API (despite being under documented) meant I could spend more time on the game and less time worrying about math.