💪 The Powerful Duo
p5.js and Matter.js are a powerful duo. Together, they allow you to create amazing physics-based games with minimal effort. Before reading this blog post, I recommend you check out both https://p5js.org/ and https://brm.io/matter-js/ just to get an idea of what both libraries are capable of doing.
with p5.js, creating games becomes easier without the hassle of dealing with HTML5 canvases and the way they work. The library allows you to focus mainly on coding what you want specifically and not waste time trying to figure out how to code a specific feature.
The task of Matter.js in this project is simple yet crucial. Matter.js will allow us to integrate a physics engine into our game to detect collisions and to apply forces to the bird to keep it floating in the air.
👨💻👩💻 Let's get right into it
In this project, I decided to take an OOP approach whereby each object in the scene corresponds to a class that has its own file. All together we have 4 classes (Bird, Box, Column, Ground). The bird is the player that tries to dodge all the obstacles. The box is a general class that represents a physical box that can be used as the ground or the obstacles. The column represents a single column with 2 boxes with a gap in the middle. The ground extends the box class and just represents the ground which acts as a trigger to determine if the player has lost or not.
The bird class is pretty simple, it is essentially an image with a circle created using Matter.js to determine its boundaries.
constructor(x, y, r) {
const options = {
restitution: 0.5,
}
this.body = Matter.Bodies.circle(x, y, r, options);
Matter.Body.setMass(this.body, this.body.mass * 2);
Matter.World.add(world, this.body);
this.r = r;
}
In the constructor of the bird class we can see that we instantiate the body, its mass and add it to the world (i.e. the scene). We then have a show function which displays the bird onto the scene using p5.js (you can see it in the full code).
Initializing the box class is similar to the bird class, we instead use a rectangle as the collider and ensure that it is static so that it isn't affected by gravity.
constructor(x, y, w, h, gap=false) {
var options = {
restitution: 0.5,
}
this.body = Matter.Bodies.rectangle(x, y, w, h, options);
this.body.inertia = Infinity
this.body.isStatic = true
Matter.World.add(world, this.body);
this.w = w;
this.h = h;
this.gap = gap
if (this.gap)
this.body.isSensor = true
}
The gap in between the 2 boxes is also a box in order to keep track of how many columns a user has successfully passed (could be done in many other ways). However, the gap has the isSensor attribute set to true in order to avoid any physical collisions (this is similar to Unity's isTrigger). The class also has a show function similar to the bird class and a move function which moves the box by a certain force:
move() {
let pushVec = Matter.Vector.create(-2, 0)
Matter.Body.translate(this.body, pushVec)
}
In the column class, we basically create 3 box objects, one for the top part, 1 for the gap and 1 for the bottom part like so:
constructor(box1Height, gapHeight, box2Height) {
this.box1 = new Box(width + 100, box1Height / 2, 100, box1Height)
this.box2 = new Box(width + 100, height - (box2Height / 2), 100, box2Height)
this.gap = new Box(width + 100, box1Height + (gapHeight / 2), 100, gapHeight, true)
}
The column class also has a show and move function which basically call the show and move functions on all 3 boxes.
The ground class is very simple and just extends the box class. It could have been done without creating its own class, I just did it for the sake of keeping everything organized:
constructor(x, y, w, h) {
super(x, y, w, h);
this.body.isStatic = true;
}
As mentioned above, this also uses the isStatic attribute to ensure that this entity isn't affected by gravity. The ground class also has a show function like the others by using the power of p5.js to display the object onto the screen.
That's it for the classes. All of these classes are then combined together in the sketch.js
file in order to complete the game using p5.js.
In every p5.js-powered game/app, there are 2 main functions: setup
and draw
. setup
is called once when the game is being loaded/starts and draw
is called many times in a second depending on the frame rate. In the setup, we call createCanvas
and give it the size of the canvas and we create the Matter.js physics engine. We also create the ground and the bird. And lastly we call the generateAllColumns
function which generates a column every 3 seconds:
function setup() {
const canvas = createCanvas(displayWidth, displayHeight - 110)
engine = Engine.create()
world = engine.world
ground = new Ground(width / 2, height - 10, width, 20)
bird = new Bird(150, 300, 20)
generateAllColumns()
}
p5.js makes it very simple to detect input from the user, so we can use the built-in mousePressed
function in order to detect if the user has clicked their mouse and add a force to the bird to make it fly upwards:
function mousePressed() {
if (canFly) {
let pushVec = Matter.Vector.create(0, -0.1)
let posVec = Matter.Vector.create(bird.body.position.x, bird.body.position.y)
Body.applyForce(bird.body, posVec, pushVec)
}
}
The last function there is to the game is the draw
function which has all the logic. In here, we update the Matter.js physics engine, we show the bird and the ground, and we check for collisions. Matter.js makes collision detection easier than doing it from scratch. Basically, we check if the bird has collided with the top or bottom part then we end the game by disabling the user's ability to click to fly. If the bird did not collide with anything, then they passed the gap and we can add one to their points (another approach is to check if the bird has collided with the game and hasn't collided with the other parts then add one to their points).
columns.forEach(function (column, i) {
if (column !== undefined) {
let box1Collide = Matter.SAT.collides(bird.body, column.box1.body)
let box2Collide = Matter.SAT.collides(bird.body, column.box2.body)
let gapCollide = Matter.SAT.collides(bird.body, column.gap.body)
if (box1Collide.collided || box2Collide.collided)
canFly = false
if ((column.box1.body.position.x + column.box1.w / 2) < 0 &&
(column.box2.body.position.x + column.box2.w / 2) < 0 &&
(column.gap.body.position.x + column.gap.w / 2) < 0) {
console.log('removed column ' + i)
Matter.World.remove(world, column.box1)
Matter.World.remove(world, column.gap)
Matter.World.remove(world, column.box2)
columns[i] = undefined
points++;
console.log(columns)
} else {
if (canFly) {
column.move()
}
column.show()
}
}
})
We can see here that Matter.js handles the collisions and if box1Collide.collided
or box2Collide.collided
is true then we set canFly
to false. The rest of the code just checks if the column has moved off-screen and remove it. Or if the column is still on-screen then we call the move function and show it to the user.
✨ Try it out!
You can try the game out at:
https://gifted-babbage-7b9dab.netlify.com/
💭 Final Thoughts
The entire code can be found in this GitHub repo:
https://github.com/omarsinan/FlappyBirdClone
If you would like to add some additional features and improve it, please do and share it with me :) I'd recommend you increase the speed, play around with the values and make the columns appear faster instead of having the user wait in the beginning for a long time.
One last thing. If you like what you read and you'd be interested in similar content, I'd suggest you follow my developer Twitter account @oohsinan 😁
Top comments (2)
Hey it was nice! Check mine too: github.com/imskr/Flappy-Bird-AI
imskr.github.io/Flappy-Bird-AI
Looks great! 🙌