前情提要

在上一节中,我们实现了碰撞检测功能。现在是时候添加一些敌人,让游戏变得更有挑战性了。

设计敌人的数据结构

和角色一样,敌人也需要一个对象来存储它的属性:

const enemy = {
  image: new Image(),
  x: 500,
  y: floorY,
  moveSpeed: 50, // 敌人的移动速度
  direction: 0, // 移动方向:0 表示向左,1 表示向右
  isAlive: true, // 是否存活
};
enemy.image.src = "./images/bad_guy.png";

绘制敌人

在 drawImg 函数中添加敌人的绘制:

function drawImg() {
  // ... 其他绘制代码 ...

  // 绘制敌人
  if (enemy.isAlive) {
    ctx.drawImage(
      enemy.image,
      enemy.x - enemy.image.width / 2,
      enemy.y - enemy.image.height / 2
    );
  }

  // 绘制角色
  ctx.drawImage(
    player.image,
    player.x - player.image.width / 2,
    player.y - player.image.height / 2
  );
}

让敌人也受重力影响

敌人也应该受到重力的影响,我们可以把之前的 gravityController 函数改造成通用的:

function gravityController(entity, modifier) {
  entity.y -= entity.ySpeed * modifier;

  if (entity.y < floorY) {
    entity.ySpeed -= g * modifier;
  } else {
    entity.ySpeed = 0;
    entity.y = floorY;
    if (entity.airCount !== undefined) {
      entity.airCount = 0;
    }
  }
}

然后在 update 函数中同时应用到角色和敌人:

function update(modifier) {
  // ... 其他更新逻辑 ...

  // 对角色和敌人都应用重力
  gravityController(player, modifier);
  if (enemy.isAlive) {
    gravityController(enemy, modifier);
  }
}

不过,敌人需要有 ySpeed 属性:

const enemy = {
  image: new Image(),
  x: 500,
  y: floorY,
  moveSpeed: 50,
  direction: 0,
  isAlive: true,
  ySpeed: 0, // 新增:垂直速度
};
enemy.image.src = "./images/bad_guy.png";

检测角色和敌人的碰撞

现在我们需要检测角色和敌人是否碰撞:

function isCollision(entity1, entity2) {
  const xDistance = Math.abs(entity1.x - entity2.x);
  const yDistance = Math.abs(entity1.y - entity2.y);

  const width1 = entity1.image ? entity1.image.width : entity1.width;
  const height1 = entity1.image ? entity1.image.height : entity1.height;
  const width2 = entity2.image ? entity2.image.width : entity2.width;
  const height2 = entity2.image ? entity2.image.height : entity2.height;

  const xOverlap = xDistance <= (width1 + width2) / 2;
  const yOverlap = yDistance <= (height1 + height2) / 2;

  return xOverlap && yOverlap;
}

判断是否踩到敌人

在马里奥游戏中,如果角色从上方踩到敌人,敌人会被消灭。我们需要一个函数来判断是否是"踩"的动作:

function isStampOn(player, enemy) {
  // 如果角色的底部位置高于敌人的中心位置,说明是从上方踩下来的
  const playerBottom = player.y + (player.image.height / 2);
  const enemyCenter = enemy.y;

  return playerBottom < enemyCenter;
}

处理碰撞逻辑

在 update 函数中添加碰撞处理:

function update(modifier) {
  // ... 其他更新逻辑 ...

  gravityController(player, modifier);
  if (enemy.isAlive) {
    gravityController(enemy, modifier);
  }

  // 检测角色和敌人的碰撞
  if (enemy.isAlive && isCollision(player, enemy)) {
    if (isStampOn(player, enemy)) {
      // 踩到敌人,敌人死亡
      enemy.isAlive = false;
      player.ySpeed = 200; // 给角色一个向上的反弹
      console.log("踩死敌人!");
    } else {
      // 被敌人撞到,游戏结束
      console.log("被敌人撞到了!游戏结束!");
      // 这里可以添加游戏结束的逻辑
    }
  }
}

敌人重生

当敌人被踩死后,我们可以让它在一段时间后重生:

function update(modifier) {
  // ... 其他更新逻辑 ...

  // 检测角色和敌人的碰撞
  if (enemy.isAlive && isCollision(player, enemy)) {
    if (isStampOn(player, enemy)) {
      enemy.isAlive = false;
      player.ySpeed = 200;

      // 1 秒后重生
      setTimeout(() => {
        enemy.isAlive = true;
        // 随机重生位置
        enemy.x = Math.random() * cvWidth;
        enemy.y = floorY;
      }, 1000);
    } else {
      console.log("被敌人撞到了!游戏结束!");
    }
  }
}

添加得分系统

每次踩到敌人,增加分数:

const gameData = {
  currentTimeStamp: new Date().getTime(),
  score: 0, // 新增:分数
};

在踩到敌人时增加分数:

if (isStampOn(player, enemy)) {
  enemy.isAlive = false;
  player.ySpeed = 200;
  gameData.score++; // 增加分数

  setTimeout(() => {
    enemy.isAlive = true;
    enemy.x = Math.random() * cvWidth;
    enemy.y = floorY;
  }, 1000);
}

在 drawImg 函数中显示分数:

function drawImg() {
  // ... 其他绘制代码 ...

  ctx.font = "20px 微软雅黑";
  ctx.fillStyle = "#fff";
  ctx.fillText("得分: " + gameData.score, 10, 30);
  ctx.fillText("按空格跳跃,A/D 左右移动", 10, 60);
  ctx.fillText("踩敌人得分!", 10, 90);

  // ... 其他绘制代码 ...
}

小结

这节课我们学习了:

  1. 如何添加敌人角色
  2. 如何让敌人也受重力影响
  3. 如何判断是否踩到敌人
  4. 如何实现敌人重生
  5. 如何添加得分系统

在下一节课中,我们会给敌人添加 AI,让它能够自动移动。

下载完整代码

Leason7.zip