ActionScript
TypeScript
JavaScript

matter-js物理引擎:创建Laya渲染器运行项目

发布时间:2017-01-09

物理引擎本身只负责模拟世界,和界面渲染分开,有些物理引擎会提供调试用的绘图界面。matter-js在源码中提供了两种渲染器:

• Render.js —— 原生画布渲染器

• RenderPixi.js —— pixi渲染器

 Render.js提供了完整地调试绘图界面代码。我们将要根据matter-js官方提供的渲染器写出LayaAir的渲染器,渲染器使用JavaScript语言。

 渲染器的工作流程如下:

调用create()创建渲染器

调用run()运行渲染器

run()函数中调用world()渲染世界

world()函数中渲染世界中的元素

 首先写出文件结构和类型别名:

(function()
{
 var LayaRender = {};

 var Common = Matter.Common;
 var Composite = Matter.Composite;
 var Bounds = Matter.Bounds;
 var Events = Matter.Events;
 var Grid = Matter.Grid;
 var Vector = Matter.Vector;
  
   window.LayaRender = LayaRender;
})();

这些类型别名都在Matter命名空间中,因此渲染器的引入顺序在Matter.js之后。我们将LayaRender引用传给了window

 接下来一步步创建上述的接口。create()函数:

/**
 * 创建新的渲染器。
 * @param  {object} options 所有属性都有默认值,options中的属性会覆盖默认属性。
 * @return {render}         返回创建的渲染器
 */
LayaRender.create = function(options)
{
 var defaults = {
 controller: LayaRender,
 engine: null,
 element: null,
 canvas: null,
 mouse: null,
 frameRequestId: null,
 options:
 {
 width: 800,
 height: 600,
 pixelRatio: 1,
 background: '#fafafa',
 wireframeBackground: '#222',
 hasBounds: !!options.bounds,
 enabled: true,
 wireframes: true,
 showSleeping: true,
 showDebug: false,
 showBroadphase: false,
 showBounds: false,
 showVelocity: false,
 showCollisions: false,
 showSeparations: false,
 showAxes: false,
 showPositions: false,
 showAngleIndicator: false,
 showIds: false,
 showShadows: false,
 showVertexNumbers: false,
 showConvexHulls: false,
 showInternalEdges: false,
 showMousePosition: false
 }
 };
 var render = Common.extend(defaults, options);
 render.mouse = options.mouse;
 render.engine = options.engine;
   // 如果用户没有指定contaienr,默认使用stage
 render.container = render.container || Laya.stage;
 render.bounds = render.bounds ||
 {
 min:
 {
 x: 0,
 y: 0
 },
 max:
 {
 x: render.width,
 y: render.height
 }
 };

 // caches
 render.sprites = {};
 render.primitives = {};

 return render;
}

render拥有很多默认参数,默认参数会被用户指定的options覆盖。从代码中可以得到一些信息:

• mouseengine是没有默认值的,其中engine是必须的,如果项目中没有创建mouse则没有鼠标交互。

• 如果用户没有指定contaienr,默认使用stage

• 如果用户在options字段中设置hasBoundstrue,则会开启boundsbounds在后面会讨论)。可以在options参数中设置了bounds可以设置视口,此时即使不设置options.hasBounds字段也会开启bounds

 run()stop()负责开启和停止渲染器:

/**
 * 运行渲染器。
 * @param  {render} render 渲染的目标是LayaRender.create()返回的对象
 * @return {void}
 */
LayaRender.run = function(render)
{
 Laya.timer.frameLoop(1, this, LayaRender.world, [render]);
};

/**
 * 停止渲染器。
 * @return {void}
 */
LayaRender.stop = function()
{
 Laya.timer.clear(this, LayaRender.world);
}

run()stop()分别调用和停止帧循环来渲染世界,渲染函数是world()

 world()函数会渲染世界中的子对象:

/**
 * 渲染给定的 engine 的 Matter.World 对象。
 * 这是渲染的入口,每次场景改变时都应该被调用。
 * @param  {render} render
 * @return {void}
 */
LayaRender.world = function(render)
{
 var engine = render.engine,
 world = engine.world,
 renderer = render.renderer,
 container = render.container,
 options = render.options,
 bodies = Composite.allBodies(world),
 allConstraints = Composite.allConstraints(world),
 constraints = [],
 i;

 if (options.wireframes)
 {
 LayaRender.setBackground(render, options.wireframeBackground);
 }
 else
 {
 LayaRender.setBackground(render, options.background);
 }

 // 处理 bounds
 var boundsWidth = render.bounds.max.x - render.bounds.min.x,
 boundsHeight = render.bounds.max.y - render.bounds.min.y,
 boundsScaleX = boundsWidth / render.options.width,
 boundsScaleY = boundsHeight / render.options.height;

 if (options.hasBounds)
 {
 // 隐藏不在视口内的bodies
 for (i = 0; i < bodies.length; i++)
 {
 var body = bodies[i];
 body.render.sprite.visible = Bounds.overlaps(body.bounds, render.bounds);
 }

 // 过滤掉不在视口内的 constraints
 for (i = 0; i < allConstraints.length; i++)
 {
 var constraint = allConstraints[i],
 bodyA = constraint.bodyA,
 bodyB = constraint.bodyB,
 pointAWorld = constraint.pointA,
 pointBWorld = constraint.pointB;

 if (bodyA) pointAWorld = Vector.add(bodyA.position, constraint.pointA);
 if (bodyB) pointBWorld = Vector.add(bodyB.position, constraint.pointB);

 if (!pointAWorld || !pointBWorld)
 continue;

 if (Bounds.contains(render.bounds, pointAWorld) || Bounds.contains(render.bounds, pointBWorld))
 constraints.push(constraint);
 }

 // 改变视口
 container.scale(1 / boundsScaleX, 1 / boundsScaleY);
 container.pos(-render.bounds.min.x * (1 / boundsScaleX), -render.bounds.min.y * (1 / boundsScaleY));
 }
 else
 {
 constraints = allConstraints;
 }

 for (i = 0; i < bodies.length; i++)
 LayaRender.body(render, bodies[i]);

 for (i = 0; i < constraints.length; i++)
 LayaRender.constraint(render, constraints[i]);
};

从上述代码可以得到以下信息:

• world先是设置背景(纯色或图片)。

• 在有bounds的情况下过滤掉看不见的bodiesconstraints

• 在有bounds的情况下改变视口缩放和位置。

• 调用body()渲染bodies,调用constraint()渲染constraints

 背景渲染函数setBackground()

/**
 * 设置背景色或者背景图片。
 * @param {render} render
 * @param {string} background 16进制颜色字符串或者图片路径
 */
LayaRender.setBackground = function(render, background)
{
 if (render.currentBackground !== background)
 {
 var isColor = background.indexOf && background.indexOf('#') !== -1;
 
 render.container.graphics.clear();

 if (isColor)
 {
 // 使用纯色背景
 render.container.bgColor = background;
 }
 else
 {
 render.container.loadImage(background);
 // 使用背景图片时把背景色设置为白色
 render.container.bgColor = "#FFFFFF";
 }

 render.currentBackground = background;
 }
}

 setBackground()设置了容器的背景色或者背景图片。

 body()渲染函数:

/**
 * 渲染 body
 * @param  {render} render
 * @param  {body} body
 * @return {void}
 */
LayaRender.body = function(render, body)
{
 var engine = render.engine,
 bodyRender = body.render;

 if (!bodyRender.visible)
 return;

 // 有纹理的body
 if (bodyRender.sprite && bodyRender.sprite.texture)
 {
 var spriteId = 'b-' + body.id,
 sprite = render.sprites[spriteId],
 container = render.container;

 // 如果sprite不存在,则初始化一个
 if (!sprite)
 sprite = render.sprites[spriteId] = _createBodySprite(render, body);

 // 如果sprite未在显示列表,则添加至显示列表
 if (!container.contains(sprite))
 container.addChild(sprite);

 // 更新sprite位置
 sprite.x = body.position.x;
 sprite.y = body.position.y;
 sprite.rotation = body.angle * 180 / Math.PI;
 sprite.scaleX = bodyRender.sprite.xScale || 1;
 sprite.scaleY = bodyRender.sprite.yScale || 1;
 }
 else // 没有纹理的body
 {
 var primitiveId = 'b-' + body.id,
 sprite = render.primitives[primitiveId],
 container = render.container;

 // 如果sprite不存在,则初始化一个
 if (!sprite)
 {
 sprite = render.primitives[primitiveId] = _createBodyPrimitive(render, body);
 }

 // 如果sprite未在显示列表,则添加至显示列表
 if (!container.contains(sprite))
 container.addChild(sprite);

 // 更新sprite位置
 sprite.x = body.position.x;
 sprite.y = body.position.y;
 }
};

body的渲染分为设置了纹理的body和没有设置纹理的body,没有设置纹理的body会使用指定的填充颜色和笔触颜色进行绘制。

 从上述代码可以看出:

• 忽略不可见body

• 创建Laya.Sprite,添加至显示列表。

 body()中使用了_createBodySprite函数来创建使用纹理的Laya.Sprite对象;使用_createBodyPrimitive()函数来创建使用矢量绘图的Laya.Sprite对象。

 _createBodySprite函数:

/**
 * 创建使用纹理的Sprite对象。
 * @param  {render} render
 * @param  {body} body
 * @return {void}
 */
var _createBodySprite = function(render, body)
{
 var bodyRender = body.render,
 texturePath = bodyRender.sprite.texture,
 sprite = new Laya.Sprite();

 sprite.loadImage(texturePath);
 sprite.pivotX = body.render.sprite.xOffset;
 sprite.pivotY = body.render.sprite.yOffset;

 return sprite;
};

 _createBodySprite()根据render属性保存的信息来创建body外观。

 _createBodyPrimitive()函数:

/**
 * 创建使用矢量绘图的Sprite对象。
 * @param  {render} render
 * @param  {body} body
 * @return {void}
 */
var _createBodyPrimitive = function(render, body)
{
 var bodyRender = body.render,
 options = render.options,
 sprite = new Laya.Sprite(),
 fillStyle, strokeStyle, lineWidth,
 part, points = [];

 var primitive = sprite.graphics;
 primitive.clear();

 // 处理 compound parts
 for (var k = body.parts.length > 1 ? 1 : 0; k < body.parts.length; k++)
 {
 part = body.parts[k];

 if (!options.wireframes)
 {
 fillStyle = bodyRender.fillStyle;
 strokeStyle = bodyRender.strokeStyle;
 lineWidth = bodyRender.lineWidth;
 }
 else
 {
 fillStyle = null;
 lineStyle = '#bbb';
 lineWidth = 1;
 }

 points.push(part.vertices[0].x - body.position.x, part.vertices[0].y - body.position.y);

 for (var j = 1; j < part.vertices.length; j++)
 {
 points.push(part.vertices[j].x - body.position.x, part.vertices[j].y - body.position.y);
 }

 points.push(part.vertices[0].x - body.position.x, part.vertices[0].y - body.position.y);

 primitive.drawPoly(0, 0, points, fillStyle, strokeStyle, lineWidth);

 // 角度指示器
 if (options.showAngleIndicator || options.showAxes)
 {
 primitive.beginFill(0, 0);

 lineWidth = 1;
 if (options.wireframes)
 {
 strokeStyle = '#CD5C5C';
 }
 else
 {
 strokeStyle = bodyRender.strokeStyle;
 }

 primitive.drawLine(part.position.x - body.position.x, part.position.y - body.position.y,
 ((part.vertices[0].x + part.vertices[part.vertices.length - 1].x) / 2 - body.position.x),
 ((part.vertices[0].y + part.vertices[part.vertices.length - 1].y) / 2 - body.position.y));
 }
 }

 return sprite;
};

 _createBodyPrimitive()的代码稍长,看似复杂,实际上只做了两件事:

• 处理 compound parts,它将compound部分一个个画出来。将compound的所有顶点取出并绘制它。

• 绘制角度指示器。在设置了showAngleIndicator或者showAxes的情况下,会在body上显示一条线段指示当前角度。

 constraint()函数:

/**
 * 绘制 constraint。
 * @param  {render} render
 * @param  {constraint} constraint
 * @return {void}
 */
LayaRender.constraint = function(render, constraint)
{
 var engine = render.engine,
 bodyA = constraint.bodyA,
 bodyB = constraint.bodyB,
 pointA = constraint.pointA,
 pointB = constraint.pointB,
 container = render.container,
 constraintRender = constraint.render,
 primitiveId = 'c-' + constraint.id,
 sprite = render.primitives[primitiveId];

 // 如果sprite不存在,则初始化一个
 if (!sprite)
 sprite = render.primitives[primitiveId] = new Laya.Sprite();

 var primitive = sprite.graphics;

 // constraint 没有两个终点时不渲染
 if (!constraintRender.visible || !constraint.pointA || !constraint.pointB)
 {
 primitive.clear();
 return;
 }

 // 如果sprite未在显示列表,则添加至显示列表
 if (!container.contains(sprite))
 container.addChild(sprite);

 // 渲染 constraint
 primitive.clear();

 var fromX, fromY, toX, toY;
 if (bodyA)
 {
 fromX = bodyA.position.x + pointA.x;
 fromY = bodyA.position.y + pointA.y;
 }
 else
 {
 fromX = pointA.x;
 fromY = pointA.y;
 }

 if (bodyB)
 {
 toX = bodyB.position.x + pointB.x;
 toY = bodyB.position.y + pointB.y;
 }
 else
 {
 toX = pointB.x;
 toY = pointB.y;
 }

 primitive.drawLine(fromX, fromY, toX, toY, constraintRender.strokeStyle, constraintRender.lineWidth);
};

 constraint()所做的事情也很简单,获取constraint两个端点的坐标,连接它们。

 以上就是LayaRender.js的全部代码。完整文件请在源码中查看。这个文件提供调试功能,并非实际项目中所用。实际项目的渲染可能比这简单得多,高效的多,因为不必从Composite.allBodies()中获取所有body。



使用渲染器运行项目

 在有了LayaRender之后,就可以使用项目测试了,下面我们使用Matter.js官方的Slingshot演示。它看起来是这样的:

blob.png 

 

 它的素材可以在Matter.js源码中或者本书源码中找到。

 先列出它使用的变量:

var stageWidth = 800;
var stageHeight = 600;

var Matter = Browser.window.Matter;
var LayaRender = Browser.window.LayaRender;

var mouseConstraint;
var engine;

 我们使用的游戏尺寸是800 * 600。

 构造函数如下:

function Slingshot()
{
Laya.init(stageWidth, stageHeight);

Laya.stage.alignV = Stage.ALIGN_MIDDLE;
Laya.stage.alignH = Stage.ALIGN_CENTER;

Laya.stage.scaleMode = "showall";

setup();
}

 在构造函数中指定了舞台适配模式,我们调用setup()来初始化:

function setup()
{
initMatter();
initWorld();
  
Laya.stage.on("resize", this, onResize);
}

 构建使用initMatter()初始化物理引擎,使用initWorld()构建世界。并且处理窗口resize。

 initMatter()函数:

function initMatter()
{
var gameWorld = new Sprite();
Laya.stage.addChild(gameWorld);

// 初始化物理引擎
engine = Matter.Engine.create(
{
enableSleeping: true
});
Matter.Engine.run(engine);

var render = LayaRender.create(
{
engine: engine,
width: 800,
height: 600,
options:
{
background: 'img/background.png',
wireframes: false
}
});
LayaRender.run(render);

mouseConstraint = Matter.MouseConstraint.create(engine,
{
constraint:
{
angularStiffness: 0.1,
stiffness: 2
},
element: Render.canvas
});
Matter.World.add(engine.world, mouseConstraint);
render.mouse = mouseConstraint.mouse;
}

 在上面的代码中,我们创建了engine并且调用其run()方法;创建了render并且调用其run()方法。之后,创建mouseConstraint并将其添加到world中,并需要为render指定mouse对象。

function initWorld()
{
var ground = Matter.Bodies.rectangle(395, 600, 815, 50,
{
isStatic: true,
render:
{
visible: false
}
}),
rockOptions = {
density: 0.004,
render:
{
sprite:
{
texture: 'img/rock.png',
xOffset: 23.5,
yOffset: 23.5
}
}
},
rock = Matter.Bodies.polygon(170, 450, 8, 20, rockOptions),
anchor = {
x: 170,
y: 450
},
elastic = Matter.Constraint.create(
{
pointA: anchor,
bodyB: rock,
stiffness: 0.05,
render:
{
lineWidth: 5,
strokeStyle: '#dfa417'
}
});

var pyramid = Matter.Composites.pyramid(500, 300, 9, 10, 0, 0, function(x, y, column): *
{
var texture = column % 2 === 0 ? 'img/block.png' : 'img/block-2.png';
return Matter.Bodies.rectangle(x, y, 25, 40,
{
render:
{
sprite:
{
texture: texture,
xOffset: 20.5,
yOffset: 28
}
}
});
});

var ground2 = Matter.Bodies.rectangle(610, 250, 200, 20,
{
isStatic: true,
render:
{
fillStyle: '#edc51e',
strokeStyle: '#b5a91c'
}
});

var pyramid2 = Matter.Composites.pyramid(550, 0, 5, 10, 0, 0, function(x, y, column): *
{
var texture = column % 2 === 0 ? 'img/block.png' : 'img/block-2.png';
return Matter.Bodies.rectangle(x, y, 25, 40,
{
render:
{
sprite:
{
texture: texture,
xOffset: 20.5,
yOffset: 28
}
}
});
});

Matter.World.add(engine.world, [mouseConstraint, ground, pyramid, ground2, pyramid2, rock, elastic]);

Matter.Events.on(engine, 'afterUpdate', function(): *
{
if (mouseConstraint.mouse.button === -1 && (rock.position.x > 190 || rock.position.y < 430))
{
rock = Matter.Bodies.polygon(170, 450, 7, 20, rockOptions);
Matter.World.add(engine.world, rock);
elastic.bodyB = rock;
}
});
}

 上述代码的前面部分都是创建世界中的物体,包含ground和负面半空中的ground2;放置在ground上的pyramid和放置在ground2上的pyramid2;发射物rock,并且使用elasticrock控制在一个锚点上。

 代码的后一部分,即侦听afterUpdate事件。当鼠标没有按下并且rock离开了限定区域后,就会生成一个新的rock

 最后需要处理窗口的resize事件,这部分代码虽然简短,却很重要。

function onResize()
{
// 设置鼠标的坐标缩放
Matter.Mouse.setScale(mouseConstraint.mouse,
{
// Laya.stage.clientScaleX代表舞台缩放
// Laya.stage._canvasTransform代表画布缩放
x: 1 / (Laya.stage.clientScaleX * Laya.stage._canvasTransform.a),
y: 1 / (Laya.stage.clientScaleY * Laya.stage._canvasTransform.d)
});
}

 先前我们创建mouseConstraint,给它的options传入了element字段,element是所使用的<canvas>元素。Matter.js需要它的缩放信息来正确地处理鼠标交互。由于我们设置了舞台的缩放模式,因此需要把舞台的所有缩放因子合在一起,并且调用Matter.Mouse.setScale()来告诉Matter.js正确的缩放值。

 以上所有代码的完整文件,请在源码中获取。运行程序,即可看到游戏界面。