Craft ropes that dance, limbs that sway, and fish that splash with the magic of physics and images in Phaser. Learn how to effortlessly create a rope-like mesh from an image, connecting it with physics boxes to infuse your game graphics with captivating animation and character.
As I was playing a handful of entries for yet another game jam, I found myself impressed with this game about dead fish mixed with pinball. Despite being dead, the fish looked so lifelike, but I had no idea how it was possible. I asked the developer how he did it.
Intriguingly, he explained,
"The fishes are 5 different sized circles connected by revolution joints with a set limit. I used the PixiJS Rope Mesh to draw the fish graphic along the bones (Ref gamedevjs-2022 game jam)."
This sounded too difficult, so I didn't even try to create it. Then, 2 years later, the itch to understand it lingered, and I decided to give it a try myself.
In this blog post, I'll take you on the same journey that started with a simple question during a game jam and led to the exploration of animated mesh distortion with Phaser.
We'll learn a simple yet powerful technique to add animation to our game graphics. We'll be using Phaser and MatterJs (the physics engine bundled with Phaser) to achieve this.
Before we dive into the implementation, it's a good idea to understand 2 main topics:
Mesh (image) distortion
Physics and constraints
It will be easier to go through the implementation when we understand the concept of these two topics.
I won't cover all the nitty gritty details and low-level implementation. Rather, I’ll give an easy-to-understand explanation and more info on how we can effectively use them.
A mesh is essentially an image, but we separate the image into sections, with each segment controlling a portion of the image. The mesh is connected with triangles (or quads), each with its set of coordinates. This may sound more confusing than it is, and the following image can help clarify what I mean.
I've separated the image into 4 different regions (with 10 control points.) This is our mesh. When we move the control points, the image will be distorted, and change its shape. This is mesh distortion in action.
It may be easier to understand if you've worked with photo editing tools, such as Photo Shop or Affinity Photo. The tools have filters to distort the image, e.g. "Perspective" or "Mesh Warp". The principle is the same. We adjust the mesh to shape the image. I used Affinity Photo for the above image.
Mesh distortion is useful because you can have 1 image and use it for animations. By manipulating the mesh, you can simulate natural movements, deformations, and reactions to different forces, providing a more lifelike appearance to objects or characters.
In Phaser, we have a physics engine. The default is called "Arcade," and the second is called "matter," (or "MatterJS".) We'll be using "matter" in this blog post. With this engine, we can create several boxes and connect them using constraints. A constraint is a rule that tell how objects are connected.
We don't have to care about the implementation details of this, we just need to know how to use them. That's the beauty of a physics engine. All the difficult calculations and collisions are handled for us.
Here's an example of how we can connect two boxes
The pink line between the boxes is a constraint. The constraint can have spring-like qualities, or be stiff as a bone. The physics engine will make sure to follow the rules, keeping the boxes connected while respecting the other rules of physics, such as gravity and collisions.
Once we understand how we can manipulate an image with a mesh, and connect boxes, we'll be able to create something with life. Phaser has some convenient functions to make this easier for us, but before we dive into the implementation, let me quickly go through what we will do.
All we've done is connect the mesh to the boxes. Phaser handles everything else for us. If we drag the boxes around, the image changes too.
The complete code example is available on CodePen, and can also be found at the end of this blog post. I'll go through each part in this blog post.
Phaser has a built in Rope function we can use to create the mesh for us. The rope will help us distort the image correctly.
createRope.js
// Use Phaser's built in Rope to create a mesh from the image.
createRope() {
this.rope = this.add.rope(0, 0, "example");
this.rope.setPoints(40); // a number that indicates how many segments to split the texture frame into.
}
We'll create boxes, and connect them together. For this example, I like to think of each box as a bone. Each bone is connected together which will create a skeleton, similar to a spine.
Create two functions:
createSkeleton, which creates a list of bones
createSpine, which connects the bones together using constraints
createSkeleton.js
// Create the physics boxes
createSkeleton() {
this.skeleton = []; // global variable. We'll use it in another function
// Create the bones, our physics boxes.
for (let i = 0; i < 18; i++) {
const bone = this.matter.add.rectangle(180, 150, 20, 55, {
friction: 1,
restitution: 0.06
});
this.skeleton.push(bone);
}
}
// Connect the bones to form our skeleton (spine)
createSpine() {
const spinePoints = [];
// Connect each bone with a constraint to create a spine
// Think of the spine as the curve that goes through the center of each box
for (let i = 0; i < this.skeleton.length - 1; i++) {
const boneA = this.skeleton[i];
const boneB = this.skeleton[i + 1];
// the points are used to offset the connection betweem the boxes
const pointA = { x: 10, y: 0 };
const pointB = { x: -10, y: 0 };
this.matter.add.constraint(boneA, boneB, 0.1, 1, {
pointA,
pointB
});
spinePoints.push(...[0, 0]);
}
// Creates a smooth line that goes through the center of our bones.
// A spline will create a smooth curve between each point.
this.spineCurve = new Phaser.Curves.Spline([0, 0, ...spinePoints]);
}
The final piece of the puzzle is to update the position of the points in our rope mesh according to the position of our skeleton.
updateMesh() {
// Used for Phaser to know that we must re-render the mesh
this.rope.setDirty();
let ropePoints = this.rope.points;
this.spinePoints = this.spineCurve.points;
// Update the position of the points in our spline curve to the center position of each bone in our skeleton.
// We do this to make sure we get a smooth curvature and not straight lines between each bone.
for (let i = 0; i < this.spinePoints.length; i++) {
this.spinePoints[i].x = this.skeleton[i].position.x;
this.spinePoints[i].y = this.skeleton[i].position.y;
}
// Get a distribution of points along the curve (with the same number of points as our rope mesh.)
const spinePoints = this.spineCurve.getPoints(ropePoints.length);
// Update the points in our rope mesh according to the curve.
for (let i = 0; i < ropePoints.length; i++) {
ropePoints[i].x = spinePoints[i].x;
ropePoints[i].y = spinePoints[i].y;
}
}
With this, we've connected our boxes to an image. This technique can be used in different creative ways.
I've used the same technique in my (work in progress) game, Fins of Fury.
The fish connected to a skeleton of physics boxes with different heights. The skeleton is shaped like the fish itself. This will make collisions look real, and the fish will look and feel like an actual fish.
The tentacle will be used as a slingshot in the game. Each bone can be connected with a spring constraint, which helps us stretch the tentacle like a rubber band. The more we stretch the "slingshot" the more tension will be added.
Here's the complete code example, which you can find on CodePen as well.
completeExample.js
class Level extends Phaser.Scene {
rope; // the rope that help us with drawing the mesh
graphics;
spineCurve; // the curve that goes from start to end, in the center of each bone
graphics; // used to draw the spline (curve) that goes through the spine
spinePoints;
skeleton;
preload() {
this.load.image("example", "assets/sprites/phaser3-logo.png");
}
create() {
this.createRope();
this.createSkeleton();
this.createSpine();
this.createFloor();
this.graphics = this.add.graphics();
this.matter.world.setBounds();
this.matter.add.mouseSpring();
}
update() {
this.updateMesh();
// this.renderRopeCenterLine();
}
// Use Phaser's built in Rope to create a mesh from the image.
createRope() {
this.rope = this.add.rope(0, 0, "example");
this.rope.setPoints(40);
}
// Create the physics boxes
createSkeleton() {
this.skeleton = [];
for (let i = 0; i < 18; i++) {
const bone = this.matter.add.rectangle(180, 150, 20, 55, {
friction: 1,
restitution: 0.06
});
this.skeleton.push(bone);
}
}
// Connect the bones to form our skeleton (spine)
createSpine() {
const spinePoints = [];
// connect each bone with a constraint to create a spine
for (let i = 0; i < this.skeleton.length - 1; i++) {
const boneA = this.skeleton[i];
const boneB = this.skeleton[i + 1];
const pointA = { x: 10, y: 0 };
const pointB = { x: -10, y: 0 };
this.matter.add.constraint(boneA, boneB, 0.1, 1, {
pointA,
pointB
});
spinePoints.push(...[0, 0]);
}
// Creates a smooth line that goes through the center of our bones (physics rectangles)
this.spineCurve = new Phaser.Curves.Spline([0, 0, ...spinePoints]);
}
updateMesh() {
this.rope.setDirty();
let ropePoints = this.rope.points;
this.spinePoints = this.spineCurve.points;
for (let i = 0; i < this.spinePoints.length; i++) {
this.spinePoints[i].x = this.skeleton[i].position.x;
this.spinePoints[i].y = this.skeleton[i].position.y;
}
const spinePoints = this.spineCurve.getPoints(ropePoints.length);
for (let i = 0; i < ropePoints.length; i++) {
ropePoints[i].x = spinePoints[i].x;
ropePoints[i].y = spinePoints[i].y;
}
}
createFloor() {
const floor = this.matter.add.rectangle(0, 350, 900 * 2, 100, {
isStatic: true
});
}
renderRopeCenterLine() {
this.graphics.clear();
this.graphics.lineStyle(8, 0xffffff, 1);
this.graphics.fillStyle(0xffff00, 1);
this.spineCurve.draw(this.graphics, 64);
}
}
const game = new Phaser.Game({
type: Phaser.WEBGL,
width: 900,
height: 400,
backgroundColor: "#ffffff",
scale: {
mode: Phaser.Scale.FIT
},
physics: {
default: "matter",
matter: {
debug: false,
gravity: { x: 0, y: 2 }
}
},
loader: {
baseURL: "https://labs.phaser.io",
crossOrigin: "anonymous"
}
});
game.scene.add("Level", Level, true);
While I initially struggled with programming, I've come to appreciate the value of learning through trial and error. It's important to maintain a determined mindset when tackling new challenges.
If connecting the mesh to the boxes seems challenging, try focusing on manipulating the mesh or connecting the physics boxes separately at first. The learning process is a journey, and growth accompanies each step.
If you enjoyed this blog post, you may find my other posts about game development in Phaser interesting:
Also published here.