Lance novas alturas, centenas de linhas de cheats para jogos Canvas

prefácio

Este artigo simplesmente implementará um jogo de tela, a lógica total é de apenas algumas centenas de linhas, mas você pode aprender muitos fundamentos gerais do jogo 2D.

Para jogos Canvas comuns, as coisas mais importantes são pensamento orientado a objetos, detecção de colisão, aplicação de mapa de sprite, processamento de fundo do jogo, interações de teclado e mouse. Pode ser facilmente controlado se você for proficiente nele.

introdução do jogo

Este jogo é relativamente simples, ou seja, o jogador fica clicando no corvo com o mouse, e cada clique irá eliminá-lo. Se ultrapassar o lado esquerdo da tela, falhará

1684671155234.gif

Comece a jornada do jogo!

em camadas

Sem mencionar html e css, em primeiro lugar, temos duas camadas de tela aqui, a camada inferior é para desenho básico e a camada superior é para a camada de interação

const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;

// 用于后面根据颜色块体删除被点击的敌人,注意是使用canvas的获取像素的api
const collisionCanvas = document.getElementById("collisionCanvas");
const collisionCtx = collisionCanvas.getContext("2d");
collisionCanvas.width = window.innerWidth;
collisionCanvas.height = window.innerHeight;

lógica de desenho

//通过帧的控制
function animate(timestamp) {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  // ...
    
  // 简单的canvas绘制分数
  drawScore();
  
  // 在这里做一系列绘制的工作,如乌鸦类、爆炸类、拖尾类
  // ...
  //游戏结束就取消
  if (!gameOver) requestAnimationFrame(animate);
  else drawGameOver();
}

animate(0); 

Depois de pensar em camadas, temos que começar a focar na entrada do desenho. requestAnimationFrame pode renderizar quadros sob demanda de acordo com o desempenho do computador e não será executado quando o computador estiver ocioso. Precisamos ser muito claros sobre todo o processo de desenho, desde a limpeza da tela -> aula de desenho -> desenho de outros -> condições finais do jogo

Controle de taxa de quadros inteligente

let ravenInterval = 500
//通过帧的控制
function animate(timestamp) {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  collisionCtx.clearRect(0, 0, canvas.width, canvas.height);
    
  // 绘制乌鸦的帧
  let deltatime = timestamp - lastTime;
  lastTime = timestamp;
  // 控制渲染下一个乌鸦的帧是否达到目标值
  timeToNextRaven += deltatime;
  //获取每次绘制乌鸦的需要的帧,超过500ms,开始绘制下一个乌鸦
  if (timeToNextRaven > ravenInterval) {
    ravens.push(new Raven());
    timeToNextRaven = 0;
    ravens.sort(function (x, y) {
      return x.width - y.width;
    });
  }
  
  // 绘制分数
  drawScore();
  //这里的扩展运算符可以更直观序列化的展示所有要绘制的内容
  [...trailings, ...ravens, ...explosions].forEach((object) =>
    object.update(deltatime)
  );
  [...trailings, ...ravens, ...explosions].forEach((object) => object.draw());

  // 把被消灭的从数组中移除
  ravens = ravens.filter((object) => !object.markedForDeletion);

  explosions = explosions.filter((object) => !object.markedForDeletion);
  trailings = trailings.filter((object) => !object.markedForDeletion);

  //游戏结束就取消
  if (!gameOver) requestAnimationFrame(animate);
  else drawGameOver();
}

animate(0); 

Em seguida, adicionamos a lógica básica do desenho. Aqui calculamos a taxa de quadros por meio de requestAnimationFrame. Uma vez que ultrapasse 500ms para se juntar ao inimigo, em certo sentido, essa é uma variável para controlar o número de inimigos e, ao mesmo tempo, pode ter melhores desempenho. Aqui mudamos ravenInterval para 10 para ver o que acontece

1684676709328.gif

Em seguida, podemos ver a seguinte matriz em cache, que armazena várias classes e pode executar claramente a lógica de desenho de atualização por meio da atribuição de desconstrução

tipo

Antes de tudo, precisamos desmontar as categorias. As regras de divisão são divididas de acordo com os objetos que precisamos reutilizar. Existem três tipos no total: a categoria à direita, a categoria corvo e a categoria explosão. Claro, também pode ser extraído para uma classe geral e ser herdado por outros. Por exemplo, quando você tem muitos inimigos diferentes, por exemplo, todos os inimigos têm barras de saúde comuns, etc. Observe que também podemos usar fechamentos funcionais, mas, uma vez complicados, são difíceis de manter.

classe corvo corvo

class Raven {
  constructor() {
    //定义乌鸦的尺寸以及位置
    this.spriteWidth = 271;
    this.spriteHeight = 194;

    this.sizeModifier = Math.random() * 0.6 + 0.4;
    this.width = this.spriteWidth * this.sizeModifier;
    this.height = this.spriteHeight * this.sizeModifier;
    this.x = canvas.width;
    // 控制竖向位置
    this.y = Math.random() * (canvas.height - this.height);
    // 控制方向
    this.directionX = Math.random() * 5 + 3; // [3, 8)
    this.directionY = Math.random() * 5 - 2.5;
    // 加载图片
    this.image = new Image();
    this.image.src = "../static/collision/raven.png";
    
    // 默认未被消灭
    this.markedForDeletion = false;
    
    // 控制精灵图的帧率
    this.frame = 0;
    this.maxFrame = 4;
    this.timeSinceFlap = 0;
    this.flapInterval = Math.random() * 50 + 50;
    this.randomColors = [
      Math.floor(Math.random() * 255),
      Math.floor(Math.random() * 255),
      Math.floor(Math.random() * 255),
    ];
    this.color =
      "rgb(" +
      this.randomColors[0] +
      "," +
      this.randomColors[1] +
      "," +
      this.randomColors[2] +
      ")";

    this.hasTrail = Math.random() > 0.2;
  }
  update(deltatime) {
    // 控制在画面内
    if (this.y < 0 || this.y > canvas.height - this.height) {
      this.directionY = this.directionY * -1;
    }
    this.x -= this.directionX;
    this.y += this.directionY;
    if (this.x < 0 - this.width) this.markedForDeletion = true;
    
    this.timeSinceFlap += deltatime;

    if (this.timeSinceFlap > this.flapInterval) {
      if (this.frame > this.maxFrame) this.frame = 0;
      else this.frame++;
      this.timeSinceFlap = 0;

      //放入拖尾效果
      if (this.hasTrail) {
        for (let i = 0; i < 3; i++) {
          trailings.push(new Trailing(this.x, this.y, this.width, this.color));
        }
      }
    }

    if (this.x < 0 - this.width) gameOver = true;
  }

A coisa mais importante aqui é o controle de quadros, deltatime representa o número de quadros para desenhar um corvo, quadro e maxFrame são para controlar o número de imagens sprite que queremos renderizar, se maxFrame for 3, então o correspondente é

imagem.png

是不是很方便?我们可以控制动作了,那么我们怎么控制动作的速率呢,就是拍打翅膀的速度。我们从代码可以看到flapInterval,flapInterval其实就是控制我们帧的累加是否超过限制。

精灵图的应用

imagem.png

imagem.png

精灵图可以让我们把多个动作的图,只要一张图就可以加载,除了性能考虑也方便我们控制帧。

毕竟gif大体积的控制帧是很麻烦的,ae json动画虽然性能好控制帧稍微复杂点也是问题。

在这里我们只要用drawImage来裁剪图像就可以了,根据每帧的渲染,我们只要控制x的值不断循环变化。并且可以通过一个系数来处理速率。

绘制

 draw() {
    collisionCtx.fillStyle = this.color;
    collisionCtx.fillRect(this.x, this.y, this.width, this.height);

    ctx.drawImage(
      this.image, //图片
      // 控制在图片的位置
      this.frame * this.spriteWidth,
      0,  
      // 图的尺寸
      this.spriteWidth,
      this.spriteHeight,
      // 控制在canvas画布的位置
      this.x,  
      this.y,
      // 截取的尺寸
      this.width,
      this.height
    );
  }

就此逻辑都差不多了,爆炸类,只要我们在点击屏幕位置的时候,检查坐标是否在乌鸦内就push到爆炸的队列中。

巧妙的处理爆炸交互

window.addEventListener("click", function (e) {
  const pixelColor = collisionCtx.getImageData(e.x, e.y, 1, 1);

  let pc = pixelColor.data;

  ravens.forEach((object) => {
    if (
      object.randomColors[0] === pc[0] &&
      object.randomColors[1] === pc[1] &&
      object.randomColors[2] === pc[2]
    ) {
      object.markedForDeletion = true;
      score++;
      explosions.push(new Explosion(object.x, object.y, object.width));
    }
  });
});

其实这里我并没有计算坐标,而是巧妙的通过getImageData得到像素的颜色,来判断颜色值是否相同,颜色值相同证明被点击,如果你说重复的概率也是有的,微乎其微,但确实方便。所以之前collisionCtx的上下文派上用场了。

#collisionCanvas {
  opacity: 0;
}

这里我们把透明度改改,看看就明白了

imagem.png

Gravação de tela QQ 20230521221943.gif

神奇的拖尾效果

其实我们简单分析就是大量的圆的有规律的绘制。

class Trailing {
  constructor(x, y, size, color) {
    this.color = color;
    this.size = size;
    this.x = x + this.size / 2 + Math.random() * 10 - 5;
    this.y = y + this.size / 3 + Math.random() * 10 - 5;
    this.radius = (Math.random() * this.size) / 10 + 2;
    this.maxRadius = Math.random() * 20 + 35;
    this.markedForDeletion = false;
    this.speedX = Math.random() * 1 + 0.5;
    this.color = color;
  }
  //执行动作
  update() {
    // 控制不断变大并移动
    this.x += this.speedX;
    this.radius += 0.8;
    
    // 控制最大变化
    if (this.radius > this.maxRadius - 5) this.markedForDeletion = true;
  }
  draw() {
    ctx.save();
    ctx.globalAlpha = 1 - this.radius / this.maxRadius;
    ctx.beginPath();
    ctx.fillStyle = this.color;
    ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
    ctx.fill();
    ctx.restore();
  }
}

这里有个很细节的地方,比如save和restore,我们都知道这是一个canvas状态的栈的结构。如果这里我们不加save和restore很明显会抢占乌鸦的帧率,因为是在同一个canvas上下文中, 画面会一卡一卡,而一旦我们保存之前的状态,那么会在原来的基础上叠加我们的拖尾效果,画面就流畅很多了。

imagem.png

es6的语法结构巧妙处理图层绘制

我们都知道canvas的绘制是根据darw的先后顺序的,那么我们回顾下我们之前代码

 [...ravens, ...trailings, ...explosions].forEach((object) =>
    object.update(deltatime)
  );
  [...ravens, ...trailings, ...explosions].forEach((object) => object.draw());

绘制效果:

imagem.png

那么我们修改下顺序

  [...trailings, ...ravens, ...explosions].forEach((object) =>
    object.update(deltatime)
  );
  [...trailings, ...ravens, ...explosions].forEach((object) => object.draw());

修改后的效果

imagem.png

是不是感觉结构语法的先后顺序其实就是方便我们调整图层的绘制

再来看看效果吧!

1684671155234.gif

总结

Depois que algumas centenas de linhas são concluídas, existem muitos lugares que podem ser otimizados. Por exemplo, a trajetória de movimento do corvo é relativamente simples. Também podemos usar funções trigonométricas para suavizar sua trajetória. Além disso, nosso plano de fundo é não é um pouco simples.Nós também podemos encontrar uma imagem de fundo de ponta a ponta na Internet, para que o corvo tenha um melhor efeito de avanço. Se nosso jogo for mobile, devemos considerar também a adaptação das telas horizontais e verticais ?

おすすめ

転載: juejin.im/post/7235534634776248381