最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 使用 Matter.js 合成大西瓜

    正文概述 掘金(NingBo)   2021-02-20   1361

    前言

    关注 Matter.js 很久了,但一直没有机会使用过,刚好最近《合成大西瓜》这个游戏比较热门,就用 Matter.js 来试着实现了一下。

    效果预览

    点击试玩 或者扫描下方二维码浏览:

    使用 Matter.js 合成大西瓜

    为什么要使用 Matter.js

    《合成大西瓜》这款小游戏,核心的原理就是物理引擎的使用。而 Matter.js 的简介是这样的:

    Matter.js 引擎内置了物体的运动规律和碰撞检测,因此通过它来实现这个游戏,也仅仅就是一个 API 使用的过程。

    核心功能

    在实现的过程中,我将功能分成了五部分,分别为:场景初始化、创建小球、给小球添加事件、碰撞检测以及游戏结束的检测。

    核心功能仅展示核心代码,完整代码在文末附出。

    场景初始化

    场景初始化这部分,主要是学习 Matter.js 的大框架,按照官网的指引分别配置 EngineRenderWorld

    • 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);
    

    创建小球

    在游戏当中,小球默认悬浮在页面最上面,点击或者左右滑动到指定位置,小球脱落,脱落后延时一段时间再次创建一个小球。

    在这里,可以将创建一个方法:创建一个球形物体,指定他出现的位置,并赋予贴图。

    // 球体半径的合集
    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 ,让我们节省了大量的时间去研究小球间的物理关系,让我们站在巨人的肩膀上快速的完成了游戏的开发。

    更多文章

    • 查看我的更多文章:github.com/ningbonb/bl…

    本文完整代码

    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: ()=>{ // 失败回调
            
        }
    });
    

    起源地下载网 » 使用 Matter.js 合成大西瓜

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元