1

我正在开发一个基于物理的自上而下的太空游戏。我希望视图的旋转始终显示玩家的船朝上,即使船可以旋转。我搜索了文档,但没有找到任何关于旋转世界或渲染器的信息,但我可能不知道要查找的正确术语。这甚至可以使用 matte.js 吗?

4

4 回答 4

2

我不确定如何为内置渲染器执行此操作。我使用了自定义渲染器,并使用画布转换来移动相机。

http://www.w3schools.com/tags/canvas_rotate.asp

ctx.save();
ctx.translate(transX, transY);
drawBody();
ctx.restore();

http://codepen.io/lilgreenland/pen/wzARJY

于 2016-10-09T19:44:54.113 回答
1

或者,将代码添加到 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 的矩形区域!

于 2020-02-17T17:25:28.323 回答
1

对于初学者,根据文档,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 无关,至少有两个警告超出了此概念验证线程的范围:

  1. 如果您的输入依赖于 x/y 坐标,则需要确保进入 MJS 的事件与其对世界的理解相匹配。
  2. 如果您的世界很大,您可能需要剔除物理和渲染更新以提高性能。

在这篇文章中,我将使用 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.rotateMatter.Body.setAngularVelocity

于 2020-12-27T01:38:50.913 回答
0

element.style.transform = 'rotate('+rotation+')'只要船停留在屏幕中央,您就可以使用 旋转 HTML 中的画布。我知道这类似于 lilgreenland 的答案,但是这样,您不必使用自定义渲染器,只需使用更新的函数requestAnimationFrame.

于 2019-04-06T22:03:31.277 回答