Laya 物理引擎系列一 matter.js基础

一、概述

参考Matter.js 2D 物理引擎试玩报告
物理引擎是一个计算机程序,使用质量、速度、摩擦力和空气阻力等变量,模拟了一个近似真实的物理系统,为刚性物体赋予真实的物理效果,比如重力、旋转和碰撞等效果,让物体的行为表现的更加趋向真实,例如,守望先锋的英雄在跳起时,系统所设置的重力参数就决定了他能跳多高,下落时的速度有多快,子弹的飞行轨迹等等。

matter.js是一个用于 Web 的 JavaScript 2D 物理引擎库,该项目诞生于 2014 年 2 月 28 号(0.5.0-alpha 版本),目前已更新迭代了 11 个版本(最新为 0.12.0 版本),它相较于老牌的 Box2D 引擎库,Matter.js 更为轻量级(压缩版仅有 87 KB),并且在性能和功能方面也不逊色。

参考box2dweb和p2js性能差别有多大?
这段时间我试用了box2dweb,p2js,matterjs。
从功能来说:box2dweb > p2js > matterjs
从API友好性和社区活跃度来说 box2dweb < p2js < matterjs
性能上:box2dweb < p2js ≈ matterjs
p2js其实挺好的,就是文档太少,很多情况需要看源码
参考Egret Matter.js库集成教程
egret支持的p2虽然方便,但是坑太多,刚体碰撞出错,刚体穿透时常发生,社区也不活跃,资料少,还因为物理效果欠佳,搞得本人的单子凉了,煮熟的鸭子飞了。实在是气不过,不过不去寻求新的物理引擎,那么matter.js成了最好的选择。可惜没有现成的egret支付库使用。论坛里早有大神转投matter.js了,但都没有放出代码,只是偶有提及matter.js真NM好用。没办法,只好亲自动手了。

在没有 Matter.js 前,你想去制作一个物理游戏不仅需要扎实数学知识和物理知识,并且需要通过编程语言表示出来让机器读懂。而有 Matter.js 就不一样了,它为开发者提供了许多的功能模块,通过简单易用的 API 就可以实现例如弹跳、碰撞、重力、滚动等物理效果。

在2018.09.15发布的Laya2.0中,官方内置了Box2D

LayaAir1.0采用了轻量级的Matter物理引擎,虽然体积较小,但随着小游戏的开放,越来越多的游戏需要物理引擎的支持,产生出大量的新需求,例如Matter精度不是很高、没有防穿透支持等等,对于物理引擎需求较高的一些小游戏上,Matter物理引擎的压力越来越大。
因此LayaAir引擎组基于市场需求出发重新评估了主流的物理引擎。最终选定并内置了相对最为完善的Box2D物理引擎,封装了物理组件,提供了IDE物理属性的可视化编辑,还让制作物理游戏更加方便快捷。
当然,采用Box2D物理引擎后,引擎体积也会增加一些。如果对于物理引擎功能需求较为简单,并且希望能减小引擎体积的情况下,仍然可以使用Matter物理引擎。

**

二、使用实例

参考matter.js学习笔记(一)



1.Engine(引擎)和 World(世界)
Matter.Engine 模块包含了创建和处理引擎的方法,引擎是负责管理和更新模拟世界的控制器,引擎可以控制时间的缩放,可以检测所有的碰撞事件,并且拿到所有碰撞的物体对(pairs)。

在 Matter.js 中任何的物体都需要一个容身处,而存放这些物体的地方,我们称之为世界,物体必须添加到世界里,然后由引擎运行这个世界。而创建世界需要使用到 Matter.World 模块,该模块包含了用于创建和操作世界的方法,一个 Matter.World 相当于一个复合物体,物体、约束、复合物体的聚合体,其次世界还有额外的一些属性,比如重力、边界。

Matter.Render 是将实例渲染到 Canvas 中的渲染器,控制视图层的样式,它的主要作用是用于开发和调试,默认情况下 Matter.Render 将只显示物体的线框(轮廓),这对于开发和调试很有帮助,但如果需要使用到全局实体渲染则需要将线框模式关闭 render.options.wireframes = false,另外它同样也适合制作一些简单的游戏,因为它包括了一些绘图选项、线框、向量、Sprite 精灵和视窗功能。

image.png

2.Body(刚体)

物体或者叫刚体,在物理引擎里特指坚硬的物体,具有固定的形状,不能形变。刚体可以用于表示一个箱子、一个球或是一块木头,每个物体都有自己的物理属性,质量、速度、摩擦力、角度等,还可以设置刚体的标记。Matter.Bodies 模块中内置了几种刚体,矩形 Matter.rectangle、多边形 Matter.polygon、圆形 Matter.circle 、梯形 Matter.trapezoid 等等。

// 创建刚体
var rect = Bodies.rectangle(200, 100, 50, 50), // 矩形
    circle = Bodies.circle(300, 100, 25), // 圆
    polygon = Bodies.polygon(450, 100, 5, 25), // 多边形
    trapezoid = Bodies.trapezoid(590, 100, 50, 50, 3); // 梯形
// 将刚体添加到世界中
World.add(engine.world, [rect, circle, polygon, trapezoid]);

参考matter.js学习笔记(九)--Body的parts属性创造十字形物品

    //添加鼠标控制
    var mouseConstraint=MouseConstraint.create(engine,{});
    var boxA=Bodies.rectangle(300,300,500,100),
        boxB=Bodies.rectangle(300,300,100,500);
    //Body.create() 会生成物体,如果不传入参数,
    //会在(0,0)处生成一个物体。传入参数parts,
    //将两个部分结合生成新的物体
    var cup=Body.create({
        parts:[boxA,boxB]
    });

    World.add(world,[cup,mouseConstraint]);

3.Composite(复合体)

由刚体和复合材料通过约束组合在一起的就叫做复合体。复合体对外当作一个刚体,复合体的物理属性是通过所包含的刚体的属性综合计算出来的。Matter.Composite 模块包含用于创建和处理复合体的方法,另外还有一个 Matter.Composites 模块,提供了几种特别的复合材料,例如 链 Composites.chain、牛顿摆球 Composites.newtonsCradle、软体 Composites.softBody、汽车 Composites.car 、堆叠 Composites.stack 等等。
参考matter.js学习笔记(二)--Composites.stack()分析



参考matter.js学习笔记(五)--Composites.chain()制造铁索桥
Composites.chain()可以将已有的stack物体堆链接在一起,构成一个整体,里面的参数为已有的物体堆stack,链接点距离物体中心的x和y方向的偏移量。
Composites.chain = function(composite, xOffsetA, yOffsetA, xOffsetB, yOffsetB, options)
参数composite即为传入的物体堆,从第一个物体开始,到倒数第二个物体截止,每个物体和下一个物体链接。
xOffsetA, yOffsetA为每对链接的第一个物体的链接点距离中心位置的偏移百分比,之所以是百分比,是因为pointA: { x: bodyAWidth * xOffsetA, y: bodyAHeight * yOffsetA },pointA的x和y属性为实际的偏移量,是由物体的实际宽度乘以偏移百分比而得到。

    var Engine=Matter.Engine,
        Render=Matter.Render,
        World=Matter.World,
        Constraint=Matter.Constraint,
        MouseConstraint=Matter.MouseConstraint,
        Composites=Matter.Composites,
        Bodies=Matter.Bodies;

    var engine=Engine.create(),
        world=engine.world;

    var render=Render.create({
       engine:engine,
       element:document.body,
       options:{
           width:$(window).width(),
           height:$(window).width(),
           wireframes:false
       }
    });

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

    var rectA=Bodies.rectangle(333,$(window).height()-100,40,200,{
        isStatic:true,
        render:{
            fillStyle:"#f00"
        },
        collisionFilter:{
            group:-1
        }
    });
    var rectB=Bodies.rectangle(333,$(window).height()-180,400,40,{
        render:{
            fillStyle:"#00f"
        },
        collisionFilter:{
            group:-1
        }
    });
    var rotate=Constraint.create({
        bodyA:rectA,
        pointA:{x:0,y:-80},
        bodyB:rectB,
        length:0,
        stiffness:0.9
    });
    console.log(rotate.bodyA);

    var ground=Bodies.rectangle($(window).width()/2,
     $(window).height()-10,$(window).width(),20,{
        isStatic:true,
        render:{
            fillStyle:"#9fa"
        }
    });

    var stack_rect=Composites.stack(300,100,4,3,0,0,function(x,y){
        return Bodies.rectangle(x,y,150,40);
    });
    var stack_circle=Composites.stack(1200,10,1,5,2,8,function (x,y) {
        return Bodies.circle(x,y,30);
    });

    //创造两个桥墩
    var wallA=Bodies.rectangle(630,730,50,400,{isStatic:true});
    var wallB=Bodies.rectangle(1580,730,50,400,{isStatic:true});

    //桥上的木板 参数分析。这里的666和600分别是第一块木板的x和y坐标,
    //10和1分别为10列1行,9和0分别为每块木板之间的列间隙和行间隙,
    //chamfer:15 将矩形添加圆角。
    var chains=Composites.stack(666,600,10,1,9,0,function (x, y) {
        return Bodies.rectangle(x,y,80,30,{
            chamfer:15
        })
    });

    //三条链接铁索,将第一条铁索复制,并分别改变纵向的偏移百分比即可
    //chains为木板堆,
    //第一个链接点在横向上距离木板中心有向右的宽度的0.4倍的偏移量,
    //纵向上与木板中心平行。
    //第二个链接点在横向上距离木板中心有向左的宽度的0.4倍的偏移量,
    //纵向上与木板中心平行。
    Composites.chain(chains,0.4,0,-0.4,0,{});
    Composites.chain(chains,0.4,0.3,-0.4,0.3,{});
    Composites.chain(chains,0.4,-0.3,-0.4,-0.3,{});

    //左侧固定在桥墩上
    var fixLeft=Constraint.create({
       bodyA:wallA,
       pointA:{x:0,y:-90},
       bodyB:chains.bodies[0],
       pointB:{x:-25,y:0}
    });
    //右侧固定在桥墩上
    var fixRight=Constraint.create({
        bodyA:chains.bodies[chains.bodies.length-1],
        pointA:{x:25,y:0},
        bodyB:wallB,
        pointB:{x:0,y:-90}
    });

    var mouseConstraint=MouseConstraint.create(engine,{
        element:document.body
    });

    World.add(world,[rectA,rectB,ground,stack_rect,stack_circle,mouseConstraint,rotate]);
    //桥墩,铁索固定的木板加入到世界中
    World.add(world,[wallA,wallB,chains,fixLeft,fixRight]);

参考matter.js学习笔记(六)--Composites.softBody()制造布料或球网
softBody,顾名思义,创造出软体。Creates a simple soft body like object。

var Engine=Matter.Engine,
        Render=Matter.Render,
        World=Matter.World,
        Bodies=Matter.Bodies,
        Composites=Matter.Composites,
        Constraint=Matter.Constraint,
        MouseConstraint=Matter.MouseConstraint;

    var engine=Engine.create(),
        world=engine.world;

    var render=Render.create({
        engine:engine,
        element:document.body,
        options:{
            width:$(window).width(),
            height:$(window).height(),
            wireframes:false
        }

    });

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

    //添加鼠标控制
    var mouseConstraint=MouseConstraint.create(engine,{});
    
    //第一个物体中心在(100,100)坐标处,创造出的球形共6列10行,
    //行间隙和列间隙都为2,球半径为22,为了隐藏这些球,
    //将particleOptions参数的render属性的visible属性设为false。
    //将collisionFilter属性的group属性设为-1,
    //可以避免这些球之间相互碰撞。
    //constraintOptions属性传入空对象,设为默认。
    var cloth=Composites.softBody(100,100,6,10,2,2,false,22,{
        render:{
            visible:false
        },
        collisionFilter:{
            group:-1
        }
    },{});
    //将最上方的6个球固定。
    for(var i=0;i<6;i++){
        cloth.bodies[i].isStatic=true;
    }
    var ground=Bodies.rectangle(600,700,1800,100,{
        isStatic:true
    });

    World.add(world,[ground,cloth,mouseConstraint]);

参考matter.js学习笔记(七)--Composites.car()制造汽车
内置Composites.car()函数,可以生成简易小车。由一个矩形和两个圆形构成。两个圆形的圆心在矩形的两条宽的中心。

    //前两个参数为小车中心的x坐标和y坐标。
    //第三个和第四个参数为矩形车身的长和宽。
    //最后的参数为轮子的半径。
    var audi=Composites.car(300,100,200,50,50);
    //audi.bodies[0].render.showAngleIndicator=true;

    //生成两个斜面与地面
    var ground=Bodies.rectangle(800,900,1800,100,{
        isStatic:true
    });
    var stickA=Bodies.rectangle(400,400,50,700,{
        isStatic:true,
        angle:-Math.PI*0.4
    });
    var stickB=Bodies.rectangle(700,700,50,700,{
        isStatic:true,
        angle:Math.PI*0.4
    });

参考matter.js学习笔记(八)--Composites.newtonsCradle()制造牛顿摆
Composites.newtonsCradle()为内置函数,可以生产牛顿摆。

    //前两个参数为第一个摆的悬挂点的横纵坐标值,
    //7为摆球数量,50为摆球半径,500为悬挂绳的长度
    var newtonC=Composites.newtonsCradle(600,100,7,50,500);

    World.add(world,[newtonC,mouseConstraint]);

参考matter.js学习笔记(九)--Body的parts属性创造十字形物品

4.Constraint(约束)
约束可理解为通过一条线,将刚体 A 和刚体 B 两个刚体连接起来,被约束的两个刚体由于被连接在了一起,移动就相互受到了限制。Matter.Constraint 模块包含了用于创建和处理约束的方法,这个约束可以很宽松,也可以很紧绷,还可以定义约束的距离,约束具有弹性,可以用来当作橡皮筋。
参考matter.js学习笔记(四)--Constraint.create()制造跷跷板
制造跷跷板有两个关键点,一是设置两块板子不能互相碰撞,二是一个板子要固定,另外一个要可以自由活动,并且中心点固定。

    var Engine=Matter.Engine,
        Render=Matter.Render,
        World=Matter.World,
        Vector=Matter.Vector,
        Constraint=Matter.Constraint,
        MouseConstraint=Matter.MouseConstraint,
        Composites=Matter.Composites,
        Bodies=Matter.Bodies;

    var engine=Engine.create(),
        world=engine.world;

    var render=Render.create({
       engine:engine,
       element:document.body,
       options:{
           width:$(window).width(),
           height:$(window).width(),
           wireframes:false
       }
    });

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

    //为了设置两块板子呈分离状态,即不能互相碰撞,需要设置collisionFilter:{ group:-1 } 
    //字面上理解为碰撞过滤器,它可以设置哪些物体可以互相碰撞,哪些不能互相碰撞,
    //两个物体的collisionFilter属性的group属性都设为-1,则这两个物体不会发生碰撞。
    var rectA=Bodies.rectangle(666,$(window).height()-100,40,200,{
        isStatic:true,//静止
        render:{
            fillStyle:"#f00"//设为红色
        },
        collisionFilter:{
            group:-1
        }
    });
    var rectB=Bodies.rectangle(666,$(window).height()-180,400,40,{
        render:{
            fillStyle:"#00f"//设为蓝色
        },
        collisionFilter:{
            group:-1
        }
    });
    //Constraint.create()需要的参数主要有:bodyA,pointA,bodyB,pointB,length,stiffness. 
    //在跷跷板案例中,bodyA和bodyB分别为两个板子,pointA和pointB为两个向量,如果不赋值
        //,则默认为0向量,物体的约束点默认在中心位置,否则,物体的对应的约束点为中心位置加上所赋的向量值。 
    //物体rectA 的约束点设置在其中心位置向上平移80的位置,此位置也是物体rectB的中心位置,pointB未设置,默认为{x:0,y:0}
    var rotate=Constraint.create({
        bodyA:rectA,
        pointA:{x:0,y:-80},
        bodyB:rectB,
        length:0,
        stiffness:0.9
    });

    var ground=Bodies.rectangle($(window).width()/2,$(window).height()-10,$(window).width(),20,{
        isStatic:true,
        render:{
            fillStyle:"#9fa"
        }
    });

    var stack_rect=Composites.stack(300,100,4,3,0,0,function(x,y){
        return Bodies.rectangle(x,y,150,40);
    });
    var stack_circle=Composites.stack(1200,100,1,5,2,3,function (x,y) {
        return Bodies.circle(x,y,30);
    });

    var mouseConstraint=MouseConstraint.create(engine,{
        element:document.body
    });

    World.add(world,[rectA,rectB,ground,stack_rect,stack_circle,mouseConstraint,rotate]);

5.MouseConstraint(鼠标约束)
如果你想让刚体与用户之间有交互,那就要在鼠标和刚体之间建立连接,也就是鼠标和刚体间的约束,Matter.MouseConstraint 模块包含用于创建鼠标约束的方法,提供通过鼠标或触摸(移动端时)移动刚体的能力,可以设置什么标记的物体才能被鼠标操纵,创建鼠标约束后,可以捕获到鼠标的各类事件。

// 全局鼠标约束
var mouseConstraint = MouseConstraint.create({
    element: render.canvas
});
World.add(engine.world, mouseConstraint);

// 设置某个标记的物体才能被鼠标操纵
var categoryBall = 0x0001; // 分类
var ball = Matter.Bodies.circle(300, 350, 32, {
    density: 0.68, // 密度
    restitution: 1, // 弹性
    collisionFilter: {
        category: categoryBall
    }
});
var mouseConstraint = MouseConstraint.create({
    element: render.canvas,
    collisionFilter: {
        mask: categoryBall
    }
});
World.add(engine.world, mouseConstraint);

这里有个更详细的例子,参考matter.js学习笔记(三)--mouseConstraint鼠标控制。在这个例子中,也可以学到控制颜色和全屏:
为render的options添加wireframes:false可以为世界里的物品添加颜色,如果不对物品设置渲染颜色的话,引擎会默认物体颜色随机。如果要指定物体的颜色,可以在生成物体时设置render:render:{fillStyle:"#9fa"}

在生成render时,可以设置options选项,其中,有width和height属性,分别设置为$(window)的width()和height()即可。同时,要对body设置style:

body{
   margin:0;
   overflow: hidden;
}

代码全文如下:



    

6.Vector(向量)

Matter.Vector 模块包含用于创建和操纵向量的方法,向量是引擎有关几何操作行为的基础,修改物体的运动状态基本都是使用向量来控制,例如赋予物体一个力,或者设置物体的速度、旋转角度,并且内置了多个向量的求解函数:向量积、标量积、格式化、垂直向量等等。

7.Events(事件)
Matter.Events 模块包含了绑定、移除和触发对象的方法。

  • 绑定事件 Matter.Events.on(object, eventNames, callback)
  • 移除事件 Matter.Events.off(object, eventNames, callback)
  • 触发事件 Matter.Events.trigger(object, eventNames, event)

8.施加力

Matter.Body.applyForce(body, position, force) 方法可以给刚体施加一个力,传入 X 和 Y 轴需要的力度值,通过这个方法你可以去模拟踢一个足球、投一个篮球的效果。

var ball = Bodies.circle(300, 100, 25, {
    density: 0.68, // 密度
    restitution: 0.8 // 弹性
});
World.add(engine.world, ball);
function addForce() {
    var forceMagnitude = 0.02 * ball.mass;
    Body.applyForce(ball, ball.position, {
        x : (forceMagnitude + Common.random() * forceMagnitude) * Common.choose([1, -1]),
        y : -forceMagnitude + Common.random() * -forceMagnitude
    });
}
addForce();

9.重力
可以设置 X、Y 轴的重力值,默认都为 1,参数在 0、1、-1 中选择使用。

// 实现反重力效果
engine.world.gravity.y = -1;
// 无重力效果
engine.world.gravity.y = 0;

10.睡眠状态

通过 enableSleeping: true 开启睡眠模式后,当刚体处于不受作用状态时,会进入睡眠状态,这样可以有效的提高引擎的性能,当物体被其他物体碰撞或者对刚体施加力时,刚体会被叫醒,引擎会继续对其进行计算模拟。

// 开启睡眠状态
var engine = Engine.create({
    enableSleeping: true
});
// 还可以针对进入睡眠状态的刚体进行监听,比如将刚体移出世界
Event.on(ball, "sleepStart", function() {
    World.remove(engine.world, ball);
});

11.摩擦力
摩擦力在 Matter.js 中分别提供了三种:摩擦力 friction、空气摩擦力 frictionAir 以及静止摩擦力 frictionStatic。friction 默认值是 0.1,取值范围在 0 - 1,当值为 0 意味着刚体可以摩擦力的无限滑动,1 意味着对刚体施加力后会立刻停止,frictionAir 默认值是 0.01,取值范围 0 - 1,当值为 0 意味着刚体在空间中移动时速度永远不会减慢,值越高时刚体在空间的移动速度越慢,frictionStatic 默认值 0.5,当值为 0 时意味着刚体几乎是静止的,值越高时意味着需要移动刚体所需的力就越大。

// 摩擦力
Bodies.rectangle(300, 70, 40, 40, {
    friction: 0.01
})
// 空气摩擦力
Bodies.rectangle(300, 70, 40, 40, {
    frictionAir: 0.05
})
// 静止摩擦力
Bodies.rectangle(300, 70, 40, 40, {
    frictionStatic: 1
})

12.时间缩放
可以控制全局的时间,当值为 0 时为冻结模拟,值为 0.1 给出慢动作效果,值为 1.2 时给出加速效果。

engine.timing.timeScale = 0.1;

13.Matter.js 调试
除了前面讲 Matter.Render 模块的时候提到的线框模式 wireframes 便于调试外,Matter.Render 模块其实还为我们提供了以下几种方法,便于我们自定义调试选项:

var render = Render.create({
    element: document.body,
    engine: engine,
    options: {
        width: 800,
        height: 600,
        pixelRatio: 1, // 设置像素比
        background: '#fafafa', // 全局渲染模式时背景色
        wireframeBackground: '#222', // 线框模式时背景色
        hasBounds: false,
        enabled: true,
        wireframes: true, // 线框模式
        showSleeping: true, // 刚体睡眠状态
        showDebug: false, // Debug 信息
        showBroadphase: false, // 粗测阶段
        showBounds: false, // 刚体的界限
        showVelocity: false, // 移动刚体时速度
        showCollisions: false, // 刚体碰撞点
        showSeparations: false, // 刚体分离
        showAxes: false, // 刚体轴线
        showPositions: false, // 刚体位置
        showAngleIndicator: false, // 刚体转角指示
        showIds: false, // 显示每个刚体的 ID
        showVertexNumbers: false, // 刚体顶点数
        showConvexHulls: false, // 刚体凸包点
        showInternalEdges: false, // 刚体内部边界
        showMousePosition: false // 鼠标约束线
    }
});

14.参考Matter.js设置碰撞规则
Matter相互碰撞提供了collisionFilter属性,支持三种属性,分别是

  • group
  • category
  • mask

使用三种属性,就能设计出很复杂的碰撞关系。其中规则如下:

第一种情况,在两个group相等的前提下,如果任意group大于零,则两者始终碰撞,比如大家都是1,这大家相互直接始终碰撞;如果任意group小于0,比如大家都是-1,则大家永远也不碰撞。除上述两种情况,则根据category和mask进行判定

第二张情况,在两个group不相等的前提下,根据category和mask进行判定,category,mask判定规则:
category代表一个碰撞分类,其值可为1,2,4,8...直到 2^31,每个刚体设置一个值
mask为碰撞集合(category集合),是category相与的结果值,比如接受2,4类型,其值为6

a和b碰撞情况是:a的mask必须包含b的category,同时b的mask也必须包含a的category,即
(a.category & b.mask) !== 0 && (b.category & a.mask) !== 0

总结如下:
简单的碰撞关系,直接设置group即可。复杂的碰撞关系,可以通过设置category和和mask值进行搭配,做出很高级的碰撞关系

你可能感兴趣的:(Laya 物理引擎系列一 matter.js基础)