下落的矩形

一个简单的 WebGL 游戏,演示了使用纯色进行清除、剪刀式裁剪、动画和用户交互。

带剪刀式裁剪的动画和用户交互

这是一个简单的游戏。目标是:通过点击尽可能多的下落的矩形来捕捉它们。在此示例中,我们对显示的矩形使用了面向对象的方 法,这有助于将矩形的状态(其位置、颜色等)集中管理,并使整个代码更简洁、更易于复用。

此示例结合了使用纯色清除绘图缓冲区和剪刀式裁剪操作。它是处理 WebGL 图形管线和状态机的各个阶段的完整图形应用程序的预览。

此外,该示例还演示了如何在游戏循环中集成 WebGL 函数调用。游戏循环负责绘制动画帧,并使动画响应用户输入。在这里,游戏循环是使用 timeouts 实现的。

js
const canvas = document.querySelector("canvas");
const [scoreDisplay, missesDisplay] = document.querySelectorAll("strong");

function getRenderingContext() {
  canvas.width = canvas.clientWidth;
  canvas.height = canvas.clientHeight;
  const gl = canvas.getContext("webgl");
  gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
  gl.clearColor(0.0, 0.0, 0.0, 1.0);
  gl.clear(gl.COLOR_BUFFER_BIT);
  return gl;
}

const gl = getRenderingContext();
gl.enable(gl.SCISSOR_TEST);

function getRandomVector() {
  return [Math.random(), Math.random(), Math.random()];
}

class Rectangle {
  constructor() {
    // We get three random numbers and use them for new rectangle
    // size and position. For each we use a different number,
    // because we want horizontal size, vertical size and
    // position to be determined independently.
    const randVec = getRandomVector();
    this.size = [5 + 120 * randVec[0], 5 + 120 * randVec[1]];
    this.position = [
      randVec[2] * (gl.drawingBufferWidth - this.size[0]),
      gl.drawingBufferHeight,
    ];
    this.velocity = 1.0 + 6.0 * Math.random();
    this.color = getRandomVector();
    gl.clearColor(this.color[0], this.color[1], this.color[2], 1.0);
  }
}

let rainingRect = new Rectangle();

let score = 0;
let misses = 0;
let timer = null;
function drawAnimation() {
  gl.scissor(
    rainingRect.position[0],
    rainingRect.position[1],
    rainingRect.size[0],
    rainingRect.size[1],
  );
  gl.clear(gl.COLOR_BUFFER_BIT);
  rainingRect.position[1] -= rainingRect.velocity;
  if (rainingRect.position[1] < 0) {
    misses += 1;
    missesDisplay.textContent = misses;
    rainingRect = new Rectangle();
  }
  // We are using setTimeout for animation. So we reschedule
  // the timeout to call drawAnimation again in 17ms.
  // Otherwise we won't get any animation.
  timer = setTimeout(drawAnimation, 17);
}

function playerClick(evt) {
  // We need to transform the position of the click event from
  // window coordinates to relative position inside the canvas.
  // In addition we need to remember that vertical position in
  // WebGL increases from bottom to top, unlike in the browser
  // window.
  const position = [
    evt.pageX - evt.target.offsetLeft,
    gl.drawingBufferHeight - (evt.pageY - evt.target.offsetTop),
  ];
  // If the click falls inside the rectangle, we caught it.

  // Increment score and create a new rectangle.
  const diffPos = [
    position[0] - rainingRect.position[0],
    position[1] - rainingRect.position[1],
  ];
  if (
    diffPos[0] >= 0 &&
    diffPos[0] < rainingRect.size[0] &&
    diffPos[1] >= 0 &&
    diffPos[1] < rainingRect.size[1]
  ) {
    score += 1;
    scoreDisplay.textContent = score;
    rainingRect = new Rectangle();
  }
}

timer = setTimeout(drawAnimation, 17);
canvas.addEventListener("click", playerClick);

此示例的源代码也可在 GitHub 上找到。