I really enjoyed Mikey Hooks game, now available only through GameClub, and I also really enjoyed to prototype the hook physics with a lot of languages in my Mikey Hooks tutorial series. After AS3 + Box2D, AS3 + Nape, Phaser 2 + Box2D, Unity + C# and Phaser 3 + Matter, now it’s time to build the hook with Phaser 3 and Box2D powered by Planck.js.
Like in previous parts of the series, it’s just a matter of working with distance hooks.
Look at the prototype:
Click on a box and hold to create a distance joint from the player to the box working like a hook and start swinging by clicking and holding on different boxes.
Let me spend a couple of words about the way it works:
1 – Some random boxes are created
2 – When the player clicks or taps, we check for bodies under the finger/pointer using an AABB (Axis Aligned Bounding Box) query.
3 – If there is a box under the pointer/finger, a distance hook is created between the player and the box. Player body also needs to be awakened.
4 – If the player holds the mouse/finger, we shorten a bit the distance joint and increase a bit the linear velocity of the player, giving the feeling of a swinging rope.
5 – When the player releases the mouse/finger, the joint is destroyed and we wait for another input to start from point 2.
Have a look at the source code, with comments on the lines of code which manage the hook:
let game; let gameOptions = { worldScale: 30 } window.onload = function() { let gameConfig = { type: Phaser.AUTO, scale: { mode: Phaser.Scale.FIT, autoCenter: Phaser.Scale.CENTER_BOTH, parent: "thegame", width: 600, height: 600 }, scene: playGame } game = new Phaser.Game(gameConfig); window.focus(); } class playGame extends Phaser.Scene { constructor() { super("PlayGame"); } create() { let gravity = planck.Vec2(0, 3); this.world = planck.World(gravity); for (let i = 0; i < 5; i ++) { this.createBox(Phaser.Math.Between(100, game.config.width - 100), Phaser.Math.Between(100, game.config.height - 200), Phaser.Math.Between(30, 50), Phaser.Math.Between(30, 50), false); } this.createBox(game.config.width / 2, game.config.height - 10, game.config.width, 20, false); this.hero = this.createBox(game.config.width / 2, game.config.height - 20, 20, 20, true); this.input.on("pointerdown", this.fireHook, this); this.input.on("pointerup", this.releaseHook, this); this.jointGraphics = this.add.graphics(); this.joint = null; } fireHook(e) { // queryAABB scans all world bodies to check if they overlap the AABB rectangle this.world.queryAABB(planck.AABB(planck.Vec2(this.toWorldScale(e.x), this.toWorldScale(e.y)), planck.Vec2(this.toWorldScale(e.x), this.toWorldScale(e.y))), function(fixture) { let body = fixture.getBody(); // this is how we create a distance joint this.joint = this.world.createJoint(planck.DistanceJoint({ bodyA: this.hero, bodyB: body, anchorA: this.hero.getWorldCenter(), anchorB: body.getWorldCenter(), collideConnected: true })); // we must awake the body this.hero.setAwake(true); // return false to exit the AABB query without looking for another fixture return false; }.bind(this)); } toWorldScale(n) { return n / gameOptions.worldScale; } toPixels(n) { return n * gameOptions.worldScale; } releaseHook() { if (this.joint) { // this is how we destroy a joint this.world.destroyJoint(this.joint); this.joint = null; } } createBox(posX, posY, width, height, isDynamic) { let box = this.world.createBody(); if (isDynamic) { box.setDynamic(); } box.createFixture(planck.Box(this.toWorldScale(width / 2 ), this.toWorldScale(height / 2))); box.setPosition(planck.Vec2(this.toWorldScale(posX), this.toWorldScale(posY))); box.setMassData({ mass: 1, center: planck.Vec2(), I: 1 }); var color = new Phaser.Display.Color(); color.random(); color.brighten(50).saturate(100); let userData = this.add.graphics(); userData.lineStyle(2, color.color, 1); userData.strokeRect(- width / 2, - height / 2, width, height); box.setUserData(userData); return box; } update(t, dt) { this.world.step(dt / 1000 * 2); this.world.clearForces(); for (let b = this.world.getBodyList(); b; b = b.getNext()) { let bodyPosition = b.getPosition(); let bodyAngle = b.getAngle(); let userData = b.getUserData(); userData.x = this.toPixels(bodyPosition.x); userData.y = this.toPixels(bodyPosition.y); userData.rotation = bodyAngle; } this.jointGraphics.clear(); if (this.joint) { // get joint length and shorten it a bit this.joint.setLength(this.joint.getLength() - 0.1); // get hero velocity let velocity = this.hero.getLinearVelocity(); // increase hero velocity this.hero.setLinearVelocity(planck.Vec2(velocity.x * 1.01, velocity.y * 1.01)); // draw the joint let anchorA = this.joint.getAnchorA(); let anchorB = this.joint.getAnchorB(); this.jointGraphics.lineStyle(2, 0xffffff, 1); this.jointGraphics.moveTo(this.toPixels(anchorA.x), this.toPixels(anchorA.y)); this.jointGraphics.lineTo(this.toPixels(anchorB.x), this.toPixels(anchorB.y)); this.jointGraphics.strokePath(); } } };
And this is another great and fun prototype built in a bunch of lines. Download the source code.