使用 Matter.js 合成大西瓜

前言

關注 Matter.js 好久了,但一直沒有機會使用過,恰好最近《合成大西瓜》這個遊戲比較熱門,就用 Matter.js 來試着實現了一下。javascript

效果預覽

點擊試玩 或者掃描下方二維碼瀏覽:html

掃碼查看

爲何要使用 Matter.js

《合成大西瓜》這款小遊戲,核心的原理就是物理引擎的使用。而 Matter.js 的簡介是這樣的:java

Matter.js 是一個2D 剛體物理引擎。git

Matter.js 引擎內置了物體的運動規律和碰撞檢測,所以經過它來實現這個遊戲,也僅僅就是一個 API 使用的過程。github

核心功能

在實現的過程當中,我將功能分紅了五部分,分別爲:場景初始化、建立小球、給小球添加事件、碰撞檢測以及遊戲結束的檢測。canvas

核心功能僅展現核心代碼,完整代碼在文末附出。數組

場景初始化

場景初始化這部分,主要是學習 Matter.js 的大框架,按照官網的指引分別配置 EngineRenderWorldmarkdown

  • Engine 是 Matter.js 中物理引擎的配置部分,初始化它,也就給物體添加好了引擎;
  • Render 和其餘引擎相似,是畫布的渲染,畫布的尺寸、背景顏色等繪製相關的內容再次配置;
  • World 在 Matter.js 中是相似與舞臺,全部要展示出來的內容,都須要添加到 World 當中。
// Engine 初始化
this.engine = Matter.Engine.create({
    enableSleeping: true // 在遊戲結束檢測的時候,用到該 sleep 功能,enableSleeping 爲 true 能夠檢測到小球中止運動的狀態,從而方便進行遊戲結束的檢測。
});

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


// Render 初始化
this.render = Matter.Render.create({
    canvas: document.getElementById('canvas'),
    engine: this.engine,
    options: {
        width: window.innerWidth,
        height: window.innerHeight,
        wireframes: false, // 設爲 false 後,才能夠展示出添加到小球的紋理
        background :"#ffe89d",
        showSleeping: false, // 隱藏 sleep 半透明的狀態
    },
});


// 這裏使用內置的長方形物體建立了遊戲的牆壁和地面
// 建立地面
const ground = Matter.Bodies.rectangle(window.innerWidth / 2, window.innerHeight - 120 / 2, window.innerWidth, 120, { 
	isStatic: true, // true 可將物體做爲牆壁或者地面,將不會有重力等物理屬性
    render: {
        fillStyle: '#7b5438', // 地面背景顏色
    }
});

// 左牆
const leftWall = Matter.Bodies.rectangle(-10/2, canvasHeight/2, 10, canvasHeight, { isStatic: true });

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

// 將建立的物體添加到 World
Matter.World.add(this.world, [ground, leftWall, rightWall]);

// 運行引擎與渲染器
Matter.Engine.run(this.engine);
Matter.Render.run(this.render);
複製代碼

建立小球

在遊戲當中,小球默認懸浮在頁面最上面,點擊或者左右滑動到指定位置,小球脫落,脫落後延時一段時間再次建立一個小球。框架

在這裏,能夠將建立一個方法:建立一個球形物體,指定他出現的位置,並賦予貼圖。dom

// 球體半徑的合集
const radius = [52/2, 80/2, 108/2, 118/2, 152/2, 184/2, 194/2, 258/2, 308/2, 310/2, 408/2]; 

// 小球紋理數組
const assets = ['./assets/1.png',.....];

// 小球出現的次數,能夠根據次數的累計增長遊戲難度,或者用於計算分數。
let circleAmount = 0;

// 添加球體
addCircle(){
	// 隨機一個半徑
    const radiusTemp = radius.slice(0, 6);
    const index = circleAmount === 0 ? 0 : (Math.random() * radiusTemp.length | 0);
    const circleRadius = radiusTemp[index];
    
    // 建立一個球體
    this.circle = Matter.Bodies.circle(
    	window.innerWidth /2, // 小球的 x 座標,這裏是根據小球的圓心來定位的
        circleRadius + 30, // 小球的 y 座標,將初始化的小球安置在水平居中,距離頂部 30 像素的位置
        circleRadius, {
            isStatic: true, // 首先設置爲 true ,在觸發事件以後再改成 false ,給小球添加下落動做
            restitution: 0.2, // 設置小球彈性
            render: {
                sprite: {
                    texture: assets[index], // 給小球設置紋理
                }
            }
        }
    );
    
    // 將小球添加到 World
    Matter.World.add(this.world, this.circle);
    
    // 遊戲狀態檢測,後續會說明
    this.gameProgressChecking(this.circle);
    circleAmount++;
}
複製代碼

給小球添加事件

初始化後的小球,有兩種狀況下落,一種是點擊任意一處,根據 x 座標下落,二是手指觸摸,滑動小球到指定位置,手指擡起下落。

// 使用 Matter.js 內置的 MouseConstraint 和 Events 就能夠實現 touch 事件
const mouseconstraint = Matter.MouseConstraint.create(this.engine);

// touchmove 事件
Matter.Events.on(mouseconstraint, "mousemove", (e)=>{ 
    if(!this.circle || !this.canPlay) return; // this.canPlay 判斷遊戲是否結束
    this.updateCirclePosition(e); // 在 touchmove 中更新小球的 x 座標
})

// touchend 事件
Matter.Events.on(mouseconstraint, "mouseup", (e)=>{
    if(!this.circle || !this.canPlay) return;
    this.updateCirclePosition(e);
    Matter.Sleeping.set(this.circle, false); // 接觸小球的 sleep 模式,以便添加物理屬性
    Matter.Body.setStatic(this.circle, false ); // 給小球激活物理屬性,小球會由於重力自動落下
    this.circle = null;
    setTimeout(()=>{ // 延遲 1s 後再次建立小球
        this.addCircle();
    }, 1000);
});

// 更新小球的 x 座標
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});
}
複製代碼

碰撞檢測

遊戲中最吸引人的部分,就是兩個相同的水果接觸會變成一個更大的水果。在此功能部分,咱們以來 Matter.js 內置的碰撞檢測,只需判斷碰撞的兩個小球半徑是否一致便可,若是半徑一致,就變成一個更大半徑的小球。

Matter.Events.on(this.engine, "collisionStart", e => this.collisionEvent(e)); // 下落的小球剛碰撞在一塊兒的事件
Matter.Events.on(this.engine, "collisionActive", e => this.collisionEvent(e)); // 其餘被動的小球相互碰撞的事件

collisionEvent(e){
    if(!this.canPlay) return;
    const { pairs } = e; // pairs 爲全部小球碰撞的集合,經過遍歷該集合中參與碰撞的小球半徑,就完成了邏輯判斷
    Matter.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 = Matter.Bodies.circle(x, y, radius[index],{ // 建立大的球
                restitution: 0.2,
                render: {
                    sprite: {
                        texture: this.assets[index],
                    }
                }
            });

            Matter.World.remove(this.world, bodyA); // 移除兩個碰撞的小球
            Matter.World.remove(this.world, bodyB);
            Matter.World.add(this.world, circleNew); // 將生成的大球加入到 World
            this.gameProgressChecking(circleNew); // 判斷遊戲的狀態
        }
    }
}
複製代碼

遊戲結束的檢測

Matter.js 中提供了小球是否運動中止,也就是 Sleep 狀態,咱們只需判斷最近添加到 World 的小球位置,是否溢出了遊戲區域便可,若是 y 座標溢出了遊戲區域,則遊戲就結束了。

// gameProgressChecking 在上文小球開始掉落的時候開始觸發
gameProgressChecking(body){
    Matter.Events.on(body, 'sleepStart', (event)=> {
        if (!event.source.isStatic && event.source.position.y <= 300) { // 若是小球靜止時,y 座標移除遊戲區域,遊戲結束
            this.gameOver();
        }
    })
}
複製代碼

總結

以上,就完成了《合成大西瓜》的核心功能,藉助 Matter.js ,讓咱們節省了大量的時間去研究小球間的物理關係,讓咱們站在巨人的肩膀上快速的完成了遊戲的開發。

更多文章

本文完整代碼

TypeScript 源碼:github.com/ningbonb/de…

// JS 源碼
// 重命名
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;

// 基本數據
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();
    }
    
    // 場景初始化
    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);

    }
    
    // 添加球體
    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++;
    }
    
    // 添加事件
    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));
    }
    
    // 碰撞檢測
    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);
            }
        }
    }
    
    // 更新小球位置
    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});
    }
    
    // 遊戲狀態檢測
    gameProgressChecking(body){
        Events.on(body, 'sleepStart', (event)=> {
            if (!event.source.isStatic && event.source.position.y <= 300) {
                this.gameOver();
            }
        })
    }
    
    // 遊戲結束
    gameOver(){
        this.canPlay = false;
        this.gameOverCallback();
    }
}
複製代碼

使用方法:

import MatterClass from './matter.js';
const matterObj = new MatterClass({
    canvas: document.getElementById('canvas'), // canvas 元素
    assets: ['../assets/0.png',...], // 紋理合集
    gameOverCallback: ()=>{ // 失敗回調
        
    }
});
複製代碼
相關文章
相關標籤/搜索