我正在开发一个基于物理的自上而下的太空游戏。我希望视图的旋转始终显示玩家的船朝上,即使船可以旋转。我搜索了文档,但没有找到任何关于旋转世界或渲染器的信息,但我可能不知道要查找的正确术语。这甚至可以使用 matte.js 吗?
4 回答
我不确定如何为内置渲染器执行此操作。我使用了自定义渲染器,并使用画布转换来移动相机。
http://www.w3schools.com/tags/canvas_rotate.asp
ctx.save();
ctx.translate(transX, transY);
drawBody();
ctx.restore();
或者,将代码添加到 Render.startViewTransform 方法:
// OVERLOAD THIS METHOD
Matter.Render.startViewTransform = function(render) {
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;
// add lines:
var w2 = render.canvas.width / 2;
var h2 = render.canvas.height / 2;
render.context.translate(w2, h2);
render.context.rotate(angle_target);
render.context.translate(-w2, -h2);
// /add lines.
render.context.scale(1 / boundsScaleX, 1 / boundsScaleY);
render.context.translate(-render.bounds.min.x, -render.bounds.min.y);
};
但是您仍然需要覆盖 render.bounds 的计算,它现在总是考虑角度 = 0 的矩形区域!
对于初学者,根据文档,MJS 渲染器“主要用于开发和调试目的”。因此,对于像这样涉及画布转换的复杂渲染,我会使用一些适合您项目的专用渲染器,例如 DOM、HTML5 画布或 p5.js。不管你选择哪一个,过程基本相同:无头运行 MJS 作为物理引擎,提取每帧的身体位置,然后按照你喜欢的方式渲染它们。
架构是这样的:
[asynchronous DOM events] [library calls to MJS]
| |
| |
| +-----------------------------+
| |
v v
.-----------. .-----------.
| matter.js |---[body positions]-->| rendering |
| engine | [ per frame ] | engine |
`-----------` `-----------`
由于 MJS 处理物理,但不知道或关心您在无头运行时如何选择渲染其主体,因此视口概念基本上是一个不相关的、大部分解耦的模块——无论您是在屏幕上显示整个地图还是在屏幕上显示一个微小的、旋转的部分它与 MJS 无关,至少有两个警告超出了此概念验证线程的范围:
- 如果您的输入依赖于 x/y 坐标,则需要确保进入 MJS 的事件与其对世界的理解相匹配。
- 如果您的世界很大,您可能需要剔除物理和渲染更新以提高性能。
在这篇文章中,我将使用 HTML5 画布,并展示如何将 MJS 集成到来自规范线程HTML5 Canvas 相机/视口的以播放器为中心的旋转视口中——如何实际做到这一点?. 无论您是否使用 HTML5 画布,我都建议您在继续之前阅读这篇文章 - 底层视口数学是相同的。
接下来,让我们看看使用画布作为渲染前端无头运行 Matter.js。一个最小的例子是:
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
canvas.width = canvas.height = 180;
const engine = Matter.Engine.create();
const size = 50;
const bodies = [
Matter.Bodies.rectangle(
canvas.width / 2, 0, size, size
),
Matter.Bodies.rectangle(
canvas.width / 2, 120,
size, size, {isStatic: true}
),
];
const mouseConstraint = Matter.MouseConstraint.create(
engine, {element: canvas}
);
Matter.World.add(engine.world, [...bodies, mouseConstraint]);
(function render() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
bodies.forEach((e, i) => {
const {x, y} = e.position;
ctx.save();
ctx.translate(x, y);
ctx.rotate(e.angle);
ctx.fillStyle = `rgb(${i * 200}, 100, 100)`;
ctx.fillRect(size / -2, size / -2, size, size);
ctx.restore();
});
Matter.Engine.update(engine);
requestAnimationFrame(render);
})();
canvas {
border: 4px solid black;
background: #eee;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.17.1/matter.min.js"></script>
<canvas></canvas>
请注意,MJS 默认将 x/y 坐标作为矩形的中心,而 canvas 使用左上角。ctx.fillRect(size / -2, size / -2, size, size);
是确保画布和 MJS 同步所需的典型规范化步骤。Matter.Engine.update(engine);
用于使引擎前进一个刻度。
有了这些例子,我们(只是)需要把它们联系在一起。在以下示例中,所有 MJS 代码几乎都是标准问题。其余代码用于设置状态、运行更新循环并将 MJS 主体绘制到正确位置的画布上。
const rnd = Math.random;
const canvas = document.createElement("canvas");
document.body.appendChild(canvas);
const ctx = canvas.getContext("2d");
canvas.height = canvas.width = 180;
const map = {height: 1000, width: 1000};
const engine = Matter.Engine.create();
engine.world.gravity.y = 0; // enable top-down
const ship = {
body: Matter.Bodies.rectangle(
canvas.width / 2, canvas.height / 2,
20, 20, {frictionAir: 0.02, density: 0.3}
),
size: 20,
color: "#eee",
accelForce: 0.03,
rotationAmt: 0.03,
rotationAngVel: 0.01,
accelerate() {
Matter.Body.applyForce(
this.body,
this.body.position,
{
x: Math.cos(this.body.angle) * this.accelForce,
y: Math.sin(this.body.angle) * this.accelForce
}
);
},
decelerate() {
Matter.Body.applyForce(
this.body,
this.body.position,
{
x: Math.cos(this.body.angle) * -this.accelForce,
y: Math.sin(this.body.angle) * -this.accelForce
}
);
},
rotateLeft() {
Matter.Body.rotate(this.body, -this.rotationAmt);
Matter.Body.setAngularVelocity(
this.body, -this.rotationAngVel
);
},
rotateRight() {
Matter.Body.rotate(this.body, this.rotationAmt);
Matter.Body.setAngularVelocity(
this.body, this.rotationAngVel
);
},
draw(ctx) {
ctx.save();
ctx.translate(
this.body.position.x,
this.body.position.y
);
ctx.rotate(this.body.angle);
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(this.size / 1.2, 0);
ctx.stroke();
ctx.fillStyle = this.color;
ctx.fillRect(
this.size / -2,
this.size / -2,
this.size,
this.size
);
ctx.strokeRect(
this.size / -2,
this.size / -2,
this.size,
this.size
);
ctx.restore();
},
};
const obsSize = 50;
const makeObstacle = () => ({
body: (() => {
const body = Matter.Bodies.fromVertices(
obsSize + rnd() * (map.width - obsSize * 2),
obsSize + rnd() * (map.height - obsSize * 2),
[...Array(3)].map(() => ({
x: rnd() * obsSize,
y: rnd() * obsSize
})),
{frictionAir: 0.02}
);
Matter.Body.rotate(body, rnd() * Math.PI * 2);
return body;
})(),
color: `hsl(${Math.random() * 30 + 200}, 80%, 70%)`,
});
const obstacles = [
...[...Array(100)].map(makeObstacle),
{
body: Matter.Bodies.rectangle(
-10, map.height / 2,
20, map.height, {isStatic: true}
),
color: "#333",
},
{
body: Matter.Bodies.rectangle(
map.width / 2, -10,
map.width, 20, {isStatic: true}
),
color: "#333",
},
{
body: Matter.Bodies.rectangle(
map.width / 2, map.height + 10,
map.width, 20, {isStatic: true}
),
color: "#333",
},
{
body: Matter.Bodies.rectangle(
map.width + 10, map.height / 2,
20, map.width, {isStatic: true}
),
color: "#333",
},
];
Matter.World.add(engine.world, [
ship.body, ...obstacles.map(e => e.body),
]);
const keyCodesToActions = {
38: () => ship.accelerate(),
37: () => ship.rotateLeft(),
39: () => ship.rotateRight(),
40: () => ship.decelerate(),
};
const validKeys = new Set(
Object.keys(keyCodesToActions).map(e => +e)
);
const keysPressed = new Set();
document.addEventListener("keydown", e => {
if (validKeys.has(e.keyCode)) {
e.preventDefault();
keysPressed.add(e.keyCode);
}
});
document.addEventListener("keyup", e => {
if (validKeys.has(e.keyCode)) {
e.preventDefault();
keysPressed.delete(e.keyCode);
}
});
(function update() {
requestAnimationFrame(update);
keysPressed.forEach(k => {
if (k in keyCodesToActions) {
keyCodesToActions[k]();
}
});
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.save();
ctx.translate(canvas.width / 2, canvas.height / 1.4);
// ^^^ optionally offset y a bit
// so the player can see better
ctx.rotate(-90 * Math.PI / 180 - ship.body.angle);
ctx.translate(-ship.body.position.x, -ship.body.position.y);
/* draw everything as normal */
const tileSize = 50;
for (let x = 0; x < map.width; x += tileSize) {
for (let y = 0; y < map.height; y += tileSize) {
// simple culling
if (x > ship.x + canvas.width || y > ship.y + canvas.height ||
x < ship.x - canvas.width || y < ship.y - canvas.height) {
continue;
}
const light = ((x / tileSize + y / tileSize) & 1) * 5 + 70;
ctx.fillStyle = `hsl(${360 - (x + y) / 10}, 50%, ${light}%)`;
ctx.fillRect(x, y, tileSize + 1, tileSize + 1);
}
}
obstacles.forEach(({body: {vertices}, color}) => {
ctx.beginPath();
ctx.fillStyle = color;
ctx.strokeStyle = "#000";
vertices.forEach(({x, y}) => ctx.lineTo(x, y));
ctx.lineWidth = 5;
ctx.closePath();
ctx.stroke();
ctx.fill();
});
ship.draw(ctx);
ctx.restore();
Matter.Engine.update(engine);
})();
body {
margin: 0;
font-family: monospace;
display: flex;
align-items: center;
}
html, body {
height: 100%;
}
canvas {
background: #eee;
margin: 1em;
border: 4px solid #222;
}
div {
transform: rotate(-90deg);
background: #222;
color: #fff;
padding: 2px;
}
<script src="https://cdn.jsdelivr.net/npm/poly-decomp@0.2.1/build/decomp.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.18.0/matter.min.js"></script>
<div>arrow keys to move</div>
从上面的代码中可以看出,以下模式是渲染 n 面 MJS 主体的典型模式:
ctx.beginPath();
vertices.forEach(({x, y}) => ctx.lineTo(x, y));
ctx.closePath();
ctx.fill();
此外,这篇文章还展示了创建自上而下游戏的各种技术。engine.world.gravity.y = 0;
在这里也被用于全局禁用重力。链接的帖子讨论(在此线程Matter.Body.applyForce
中深入介绍)和旋转;我在这里使用和组合的方式略有不同,但这是特定于用例的,对视口几乎无关紧要。Matter.Body.rotate
Matter.Body.setAngularVelocity
element.style.transform = 'rotate('+rotation+')'
只要船停留在屏幕中央,您就可以使用 旋转 HTML 中的画布。我知道这类似于 lilgreenland 的答案,但是这样,您不必使用自定义渲染器,只需使用更新的函数requestAnimationFrame
.