一、概述
参考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 精灵和视窗功能。
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值进行搭配,做出很高级的碰撞关系