preface

I have been paying attention to Matter. Js for a long time, but I haven’t had a chance to use it. Just now, the game Synthesis Watermelon is popular, so I tried to implement it with Matter.

Results the preview

Click to try it or scan the QR code below to browse:

Why use Matter. Js

The core principle of a small game called Synthesis Watermelon is the use of a physics engine. The introduction to matter.js looks like this:

Matter.js is a 2D rigid-body physics engine.

The matter.js engine has built-in object motion and collision detection, so implementing this game through it is just an API process.

The core function

In the implementation process, I divided the function into five parts, respectively: scene initialization, creating a ball, adding events to the ball, collision detection and game end detection.

Core functions only show the core code, the complete code is attached at the end of the article.

Scene initialization

This section is mainly to learn the big framework of Matter. Js and configure Engine, Render and World respectively according to the guidance of the official website:

  • EngineIs the configuration part of the physics engine in Matter. Js.
  • RenderSimilar to other engines, it is the rendering of canvas, canvas size, background color and other related content is configured again;
  • WorldIn Matter. Js it is similar to a stage, everything that needs to be displayed needs to be added toWorldIn the middle.
// Engine is initialized
this.engine = Matter.Engine.create({
    enableSleeping: true // enableSleeping is true and can be used to detect the rest of the ball at the end of the game.
});

// World is initialized
this.world = this.engine.world;
this.world.bounds = { min: { x: 0.y: 0}, max: { x: window.innerWidth, y: window.innerHeight } };


// Render initializes
this.render = Matter.Render.create({
    canvas: document.getElementById('canvas'),
    engine: this.engine,
    options: {
        width: window.innerWidth,
        height: window.innerHeight,
        wireframes: false.// Set it to false to display the texture added to the ball
        background :"#ffe89d".showSleeping: false.// Hide the translucent state of sleep}});// Here we use the built-in rectangles to create the walls and floors of the game
// Create the ground
const ground = Matter.Bodies.rectangle(window.innerWidth / 2.window.innerHeight - 120 / 2.window.innerWidth, 120, { 
	isStatic: true.// true Objects can be used as walls or floors, no physical properties such as gravity
    render: {
        fillStyle: '#7b5438'.// Ground background color}});/ / the left wall
const leftWall = Matter.Bodies.rectangle(-10/2, canvasHeight/2.10, canvasHeight, { isStatic: true });

/ / right wall
const rightWall = Matter.Bodies.rectangle(10/2 + psdWidth, canvasHeight/2.10, canvasHeight, { isStatic: true });

// Add the created object to the World
Matter.World.add(this.world, [ground, leftWall, rightWall]);

// Run engine and renderer
Matter.Engine.run(this.engine);
Matter.Render.run(this.render);
Copy the code

Create a small ball

In the game, the ball floats on the top of the page by default. Click or slide left or right to the specified position, and the ball falls off. After falling off, a delay of a period of time is delayed to create another ball.

Here, you can create a method: create a spherical object, specify where it will appear, and give it a map.

// The radius of the sphere
const radius = [52/2.80/2.108/2.118/2.152/2.184/2.194/2.258/2.308/2.310/2.408/2]; 

// Ball texture array
const assets = ['./assets/1.png'. ] ;// The number of balls can be used to increase the difficulty of the game, or to calculate the score.
let circleAmount = 0;

// Add sphere
addCircle(){
	// A random radius
    const radiusTemp = radius.slice(0.6);
    const index = circleAmount === 0 ? 0 : (Math.random() * radiusTemp.length | 0);
    const circleRadius = radiusTemp[index];
    
    // Create a sphere
    this.circle = Matter.Bodies.circle(
    	window.innerWidth /2.// The x coordinate of the ball is located according to the center of the ball
        circleRadius + 30.// The ball's y coordinates, place the initialized ball horizontally centered, 30 pixels from the top
        circleRadius, {
            isStatic: true.// First set to true, then set to false after the event is triggered to add a drop action to the ball
            restitution: 0.2.// Set the ball elasticity
            render: {
                sprite: {
                    texture: assets[index], // Set the texture for the ball}}});// Add the ball to World
    Matter.World.add(this.world, this.circle);
    
    // Game state detection, explained later
    this.gameProgressChecking(this.circle);
    circleAmount++;
}
Copy the code

Add events to the ball

After initialization, the ball can fall in two ways: one is to click any place and fall according to the X coordinate; the other is to touch the ball and slide it to the specified position, and the finger will lift and fall.

// Touch Events can be implemented using MouseConstraint and Events built into mate.js
const mouseconstraint = Matter.MouseConstraint.create(this.engine);

/ / touchmove events
Matter.Events.on(mouseconstraint, "mousemove".(e) = >{ 
    if(!this.circle || !this.canPlay) return; // this.canPlay determines whether the game is over
    this.updateCirclePosition(e); // Update the ball's x coordinates in touchMove
})

/ / touchend events
Matter.Events.on(mouseconstraint, "mouseup".(e) = >{
    if(!this.circle || !this.canPlay) return;
    this.updateCirclePosition(e);
    Matter.Sleeping.set(this.circle, false); // Touch the ball's sleep mode to add physical properties
    Matter.Body.setStatic(this.circle, false ); // Activate the physical properties of the ball, the ball will fall automatically due to gravity
    this.circle = null;
    setTimeout(() = >{ // Delay creating the ball again for 1s
        this.addCircle();
    }, 1000);
});

// Update the ball's x coordinates
updateCirclePosition(e){
    const xTemp = e.mouse.absolute.x;
    const radius = this.circle.circleRadius;
    Matter.Body.setPosition(this.circle, {x: xTemp < radius ? radius : xTemp + radius > psdWidth ? psdWidth - radius : xTemp, y: radius + 30});
}
Copy the code

Collision detection

The most fascinating part of the game is that two identical fruits touch to make a larger fruit. In this function part, we use the built-in collision detection of matter.js, and only need to judge whether the radius of the two small balls in collision is the same. If the radius is the same, it will become a ball with a larger radius.

Matter.Events.on(this.engine, "collisionStart".e= > this.collisionEvent(e)); // The event when the falling balls just collide
Matter.Events.on(this.engine, "collisionActive".e= > this.collisionEvent(e)); // Other passive ball collision events

collisionEvent(e){
    if(!this.canPlay) return;
    const { pairs } = e; // Pairs is a set of all ball collisions. Logical judgment is completed by traversing the radius of the small ball involved in the collisions in the set
    Matter.Sleeping.afterCollisions(pairs); // Activate the ball participating in the collision from sleep
    
    for(let i = 0; i < pairs.length; i++ ){
        const {bodyA, bodyB} = pairs[i]; // Get the ball involved in the collision
        if(bodyA.circleRadius && bodyA.circleRadius == bodyB.circleRadius){ // Small balls have the same radius and become larger ones
            const { position: { x: bx, y: by }, circleRadius, } = bodyA; // Get two balls of the same radius
            const { position: { x: ax, y: ay } } = bodyB;

            const x = (ax + bx) / 2;
            const y = (ay + by) / 2;

            const index = radius.indexOf(circleRadius)+1;

            const circleNew = Matter.Bodies.circle(x, y, radius[index],{ // Create big balls
                restitution: 0.2.render: {
                    sprite: {
                        texture: this.assets[index],
                    }
                }
            });

            Matter.World.remove(this.world, bodyA); // Remove two colliding balls
            Matter.World.remove(this.world, bodyB);
            Matter.World.add(this.world, circleNew); // Add the generated ball to the World
            this.gameProgressChecking(circleNew); // Determine the state of the game}}}Copy the code

End of game detection

In matter.js, it is provided whether the ball has stopped moving, that is, the Sleep state. We only need to determine whether the position of the ball recently added to the World has overflowed the game area. If the y-coordinate has overflowed the game area, the game is over.

// gameProgressChecking starts when the ball starts to fall
gameProgressChecking(body){
    Matter.Events.on(body, 'sleepStart'.(event) = > {
        if(! event.source.isStatic && event.source.position.y <=300) { // If the ball is stationary, the y coordinates remove the game area, the game ends
            this.gameOver(); }})}Copy the code

conclusion

Above, the core function of Synthesis Watermelon has been completed. With the help of Matter. Js, we have saved a lot of time to study the physical relationship between the balls, allowing us to stand on the shoulders of giants and quickly complete the development of the game.

More articles

  • Check out more of my articles: github.com/ningbonb/bl…

Complete code for this article

TypeScript source: github.com/ningbonb/de…

/ / JS source code
/ / renamed
const Engine = window['Matter'].Engine,
    Render = window['Matter'].Render,
    World = window['Matter'].World,
    Bodies = window['Matter'].Bodies,
    Body = window['Matter'].Body,
    MouseConstraint = window['Matter'].MouseConstraint,
    Sleeping = window['Matter'].Sleeping,
    Events = window['Matter'].Events;

// Basic data
const psdWidth = 750,
    canvasHeight = window.innerHeight * psdWidth / window.innerWidth,
    radius = [52/2.80/2.108/2.118/2.152/2.184/2.194/2.258/2.308/2.310/2.408/2];

export default class MatterClass{
    constructor(prop) {
        this.canvas = prop.canvas;
        this.assets = prop.assets;
        this.gameOverCallback = prop.gameOverCallback;
        this.circle = null;
        this.circleAmount = 0;
        this.canPlay = true;

        this.init();
        this.addCircle();
        this.addEvents();
    }
    
    // The scene is initialized
    init(){
        this.engine = Engine.create({
            enableSleeping: true
        });
        this.world = this.engine.world;
        this.world.bounds = { min: { x: 0.y: 0}, max: { x: psdWidth, y: canvasHeight } };
        this.render = Render.create({
            canvas: this.canvas,
            engine: this.engine,
            options: {
                width: psdWidth,
                height: canvasHeight,
                wireframes: false.background :"#ffe89d".showSleeping: false,}});const ground = Bodies.rectangle(psdWidth / 2, canvasHeight - 120 / 2, psdWidth, 120, { isStatic: true.render: {
                fillStyle: '#7b5438',}});const leftWall = Bodies.rectangle(-10/2, canvasHeight/2.10, canvasHeight, { isStatic: true });
        const rightWall = Bodies.rectangle(10/2 + psdWidth, canvasHeight/2.10, canvasHeight, { isStatic: true });
        World.add(this.world, [ground, leftWall, rightWall]);

        Engine.run(this.engine);
        Render.run(this.render);

    }
    
    // Add sphere
    addCircle(){
        const radiusTemp = radius.slice(0.6);
        const index = this.circleAmount === 0 ? 0 : (Math.random() * radiusTemp.length | 0);
        const circleRadius = radiusTemp[index];
        this.circle = Bodies.circle(psdWidth /2, circleRadius + 30, circleRadius, {
                isStatic: true.restitution: 0.2.render: {
                    sprite: {
                        texture: this.assets[index],
                    }
                }
            }
        );
        World.add(this.world, this.circle);
        this.gameProgressChecking(this.circle);
        this.circleAmount++;
    }
    
    // Add events
    addEvents(){
        const mouseconstraint = MouseConstraint.create(this.engine);
        Events.on(mouseconstraint, "mousemove".(e) = >{
            if(!this.circle || !this.canPlay) return;
            this.updateCirclePosition(e);
        })
        Events.on(mouseconstraint, "mouseup".(e) = >{
            if(!this.circle || !this.canPlay) return;
            this.updateCirclePosition(e);
            Sleeping.set(this.circle, false);
            Body.setStatic(this.circle, false );
            this.circle = null;
            setTimeout(() = >{
                this.addCircle();
            }, 1000);
        });

        Events.on(this.engine, "collisionStart".e= > this.collisionEvent(e));
        Events.on(this.engine, "collisionActive".e= > this.collisionEvent(e));
    }
    
    // Collision detection
    collisionEvent(e){
        if(!this.canPlay) return;
        const { pairs } = e;
        Sleeping.afterCollisions(pairs);
        for(let i = 0; i < pairs.length; i++ ){
            const {bodyA, bodyB} = pairs[i];
            if(bodyA.circleRadius && bodyA.circleRadius == bodyB.circleRadius){
                const { position: { x: bx, y: by }, circleRadius, } = bodyA;
                const { position: { x: ax, y: ay } } = bodyB;

                const x = (ax + bx) / 2;
                const y = (ay + by) / 2;

                const index = radius.indexOf(circleRadius)+1;

                const circleNew = Bodies.circle(x, y, radius[index],{
                    restitution: 0.2.render: {
                        sprite: {
                            texture: this.assets[index],
                        }
                    }
                });

                World.remove(this.world, bodyA);
                World.remove(this.world, bodyB);
                World.add(this.world, circleNew);
                this.gameProgressChecking(circleNew); }}}// Update the ball position
    updateCirclePosition(e){
        const xTemp = e.mouse.absolute.x * psdWidth / window.innerWidth;
        const radius = this.circle.circleRadius;
        Body.setPosition(this.circle, {x: xTemp < radius ? radius : xTemp + radius > psdWidth ? psdWidth - radius : xTemp, y: radius + 30});
    }
    
    // Game state detection
    gameProgressChecking(body){
        Events.on(body, 'sleepStart'.(event) = > {
            if(! event.source.isStatic && event.source.position.y <=300) {
                this.gameOver(); }})}// Game over
    gameOver(){
        this.canPlay = false;
        this.gameOverCallback(); }}Copy the code

Usage:

import MatterClass from './matter.js';
const matterObj = new MatterClass({
    canvas: document.getElementById('canvas'), / / canvas element
    assets: ['.. /assets/0.png'. ] .// Texture collection
    gameOverCallback: () = >{ // Failed callback}});Copy the code