Canvas实现植物大战僵尸

2,971 阅读5分钟

之前用canvas实现了一个canvas实现太空侵略者游戏,感觉不够过瘾,那么,我就利用空闲时间,又写了一个 Canvas实现植物大战僵尸 也就是本篇文章要讲的。这一次我加入了ui,也加入了更好玩的功能,来吧,各位小伙伴,一起看看如何用canvas写这个游戏。

  • 注意1:游戏运行效果试玩链接都在文章底部哦!不要心急嘛,心急吃不了热豆腐。
  • 注意2:由于篇幅限制,我主要讲下主要的逻辑,具体的代码实现可以点击我的 Github 仓库上找到。
  • 注意3:我的问题,心一大😫,就把代码部署到github Page上去了,即使把图片资源压缩到几十kb,仍然逃脱不了加载慢的问题,各位掘友也可以直接把代码clone下来直接跑来看效果
  • 已更新:增加种植物时的辅助框、更新的代码可以到我的GitHub上去看哦!

1. 玩法说明

这个游戏的玩法相对简单

1.1 玩法指南

  • 鼠标:用于种植植物和收集太阳。
  • 您可以使用太阳能够收集太阳来购买植物。
  • 每颗植物只有80发子弹哦,打完后自动死亡
  • 左上角显示能量集齐100后可在页面上任意位置鼠标点击,种下一颗植物

1.2 小提示

  • 尽可能多地种植植物来保护您的房子。
  • 小心您的太阳点数,只有在必要时才使用它们。
  • 审慎选择攻击僵尸的时机,以避免浪费时间。
  • 注意时间限制,确保您有足够的时间来保护您的房子。

2. 实现页面布局

这个游戏的HTML代码比较简单,只包含一个Canvas画布和四个按钮,分别是“开始游戏”、“暂停游戏”、“继续游戏”和“结束游戏”和一些提示语

<!DOCTYPE html>
<html>
  <head>
    <title>植物大战僵尸</title>
    <style type="text/css">
      canvas {
        border: 2px solid green;
        cursor: grab;
      }
      .tipBox {
        display: flex;
        justify-content: space-around;
        align-items: center;
        background-color: green;
        width: 800px;
        height: 40px;
        color: aliceblue;
        border: 2px solid green;
      }
      #sunPoints,
      #countBitZombie,
      #timeLeft {
        font-size: 20px;
        margin: 0 5px;
      }
      #countBitZombie {
        font-weight: bolder;
        color: red;
        transform: scale(2);
      }
      button {
        width: 100px;
        background-color: green;
        color: white;
        font-size: 16px;
        padding: 5px 10px;
      }
      button:disabled {
        background-color: gray;
      }
    </style>
  </head>

  <body>
    <div class="tipBox">
      <span>收集的阳光: <span id="sunPoints">25</span></span>
      <span>打爆僵尸: <span id="countBitZombie">0</span></span>
      <span>剩余时间: <span id="timeLeft">180</span></span>
    </div>
    <canvas id="gameCanvas" width="800" height="600"></canvas>
    <div>
      <button id="startButton" onclick="startGame()">开始游戏</button>
      <button id="pauseButton" onclick="pauseGame()">暂停</button>
      <button id="resumeButton" onclick="resumeGame()">继续</button>
      <button id="endButton" onclick="endGame()">结束游戏</button>
    </div>
    <script src="./index.js"></script>
  </body>
</html>

3. 加载图片资源

游戏里的元素、背景都是通过图片资源进行呈现,所以需要预加载若干张图片资源。通过定义了资源对象resources,然后使用Image对象来加载图片资源,等所有资源加载完成后,将startButton按钮设为可用状态。

let resources = {
  zombie: './img/zombie.png',
  pea: './img/bullet.png',
  sun: './img/sun.png',
  background: './img/bg.jpg',
  peaShooter: './img/shooter.png',
};

let images = {};
let totalResources = Object.keys(resources).length;
let numResourcesLoaded = 0;
for (let key in resources) {
  images[key] = new Image();
  images[key].onload = () => numResourcesLoaded++;
  images[key].src = resources[key];
}

let intervalId = setInterval(function () {
  if (numResourcesLoaded === totalResources) {
    clearInterval(intervalId);
    startButton.disabled = false;
  }
}, 100);

4. 植物、僵尸和子弹的构造函数

定义植物、僵尸和子弹的类和构造函数,来包含每个元素的属性和功能,通过ES6的继承来做到属性和方法的继承,避免代码的冗余,并且更加便于调试和查找问题,比传统的function来实现,代码会更清晰明了,让代码可维护性更高。

// 游戏状态
let gameState = "loading";
let sunPoints = 25;
let plants = [];
let zombies = [];
let projectiles = [];
let lastSunSpawnTime = new Date().getTime();

// 植物类
class Plant {
  lastShotTime = new Date().getTime();
  health = 80;
  width = 50;
  height = 50;
  constructor(x, y, image) {
    this.x = x;
    this.y = y;
    this.image = image;
  }
  drawLifeCount() {
    ctx.fillStyle = 'white';
    ctx.fillRect(this.x, this.y + this.height, this.width, 16);
    ctx.beginPath();
    ctx.fillStyle = 'red';
    ctx.font = "11px serif";
    ctx.fillText(`子弹:${this.health}发`, this.x, this.y + this.height + 10);
  }
  draw(w, h) {
    ctx.drawImage(this.image, this.x, this.y, w || this.width, h || this.height);
  }
  update() {
    // 每1.5秒生成一个子弹
    if (new Date().getTime() - this.lastShotTime > 1500) {
      let projectile = new Projectile(this.x + this.width, this.y + this.height / 2.5, images.pea);
      projectiles.push(projectile);
      this.lastShotTime = new Date().getTime();
      this.health -= 1;//每颗植物有50发弹量;
    }
  }
  //判断是否碰撞
  collidesWith(obj) {
    let boolX = this.x + this.width > obj.x;
    let boolY = this.y + this.height > obj.y;
    return boolX && boolY && this.y < obj.y + obj.height;
  }
}

// 豌豆射手类
class PeaShooter extends Plant {
  constructor(x, y, image) {
    super(x, y, image);
  }
  draw() {
    super.draw()
    super.drawLifeCount()
  }
}

// 子弹类
class Projectile extends Plant {
  speed = 5;
  constructor(x, y, image) {
    super(x, y, image);
    this.width = 60;
    this.height = 20;
  }
  update() {
    this.x += this.speed;
  }

  handleCollision(obj) {
    obj.health -= 25;
    let index = projectiles.indexOf(this);
    if (index !== -1) {
      projectiles.splice(index, 1);
    }
  }
}

// 僵尸类
class Zombie extends Plant {
  width = 80;
  height = 80;
  health = 100;
  speed = 1;
  constructor(x, y, image) {
    super(x, y, image);
  }
  update() {
    this.x -= this.speed;
  }
}

// 太阳类
class Sun extends Plant {
  width = 50;
  height = 50;
  pointValue = 25;
  constructor(x, y, image) {
    super(x, y, image);
  }
  update() {
    this.y += 2;
  }
  //检测点击太阳边界
  containsPoint(x, y) {
    return x >= this.x && x < this.x + this.width && y >= this.y && y < this.y + this.height;
  }
}

5. 游戏循环

游戏循环是游戏的核心部分,不断地更新游戏状态,并将游戏画面绘制在Canvas画布上.使用requestAnimationFrame函数来实现循环调用gameLoop函数。在游戏状态为playing时,先绘制背景,然后分别绘制植物、僵尸和子弹。
阳光是用于收集积累后可以种植物,这个在每2秒生成一个可收集的太阳。在updateGame函数中会更新植物、僵尸和子弹的状态,并且处理碰撞等逻辑。在gameLoop函数中重复调用updateGame函数。

// 游戏循环
function gameLoop() {
  if (gameState === "playing") {
    ctx.drawImage(images.background, 0, 0);// 绘制背景
    plants.forEach((item) => item.draw());// 绘制植物
    zombies.forEach((item) => item.draw());// 绘制僵尸
    projectiles.forEach((item) => item.draw());// 绘制子弹

    // 2秒生成一个可收集的太阳
    if (new Date().getTime() - lastSunSpawnTime > 2000) {
      let x = Math.floor(Math.random() * (canvas.width - 100));
      let y = -100;
      let sun = new Sun(x, y, images.sun);
      plants.push(sun);
      lastSunSpawnTime = new Date().getTime();
    }

    updateGame(); // 更新游戏状态
  }

  // 循环调用gameLoop函数
  requestAnimationFrame(gameLoop);
}

6. 开始、暂停、继续和结束游戏

定义开始、暂停、继续和结束游戏的函数。这些函数将根据当前的游戏状态,改变按钮的状态和游戏状态。在游戏结束时,清除游戏的计时器,并弹出游戏结束的提示框。

//设置按钮是否可以按
function setButtonDisable(boolStart, boolPause, boolResume, boolEnd) {
  startButton.disabled = boolStart;
  pauseButton.disabled = boolPause;
  resumeButton.disabled = boolResume;
  endButton.disabled = boolEnd;
}

// 游戏开始
function startGame() {
  gameState = "playing";
  // 每秒调用updateTimeLeft函数
  timeLeftInterval = setInterval(updateTimeLeft, 1000);
  // 创建植物
  let peaShooter = new PeaShooter(100, 100, images.peaShooter);
  plants.push(peaShooter);
  setButtonDisable(true, false, true, false)
}

// 游戏暂停
function pauseGame() {
  gameState = "paused";
  setButtonDisable(true, true, false, false)
}

// 游戏继续
function resumeGame() {
  gameState = "playing";
  setButtonDisable(true, false, true, false)
}

// 游戏结束
function endGame() {
  gameState = "gameover";
  setButtonDisable(true, true, true, true);
  clearInterval(timeLeftInterval);
  clearInterval(genZombieInterval);
  alert("游戏结束");
}

7. 鼠标点击事件处理函数

定义鼠标点击事件处理函数。在游戏状态为“playing”时,检测鼠标点击的位置并且创建豌豆射手,收集太阳等操作。

// 鼠标点击事件处理函数
canvas.addEventListener("click", function (event) {
  let x = event.offsetX;
  let y = event.offsetY;
  if (gameState === "playing") {
    // 创建豌豆射手
    if (sunPoints >= 100 && x >= 50 && x <= 600 && y >= 50 && y <= 800) {
      let peaShooter = new PeaShooter(x, y, images.peaShooter);
      plants.push(peaShooter);
      sunPoints -= 100;
      sunPointsSpan.innerText = sunPoints;
    }

    // 点击收集太阳
    for (let i = plants.length - 1; i >= 0; i--) {
      if (plants[i] instanceof Sun && plants[i].containsPoint(x, y)) {
        sunPoints += plants[i].pointValue;
        sunPointsSpan.innerText = sunPoints;
        plants.splice(i, 1);
        break;
      }
    }
  }
});

8. 僵尸的自动生成

通过setInterval来自动生成源源不断的僵尸

// 生成僵尸
genZombieInterval = setInterval(function () {
  if (gameState === "playing") {
    let x = canvas.width;
    let y = Math.floor(Math.random() * (canvas.height - 100));
    let zombie = new Zombie(x, y, images.zombie);
    zombies.push(zombie);
  }
}, 3000);

9.运行效果

下图是运行的GIF动图.
在线体验链接为 植物大战僵尸 (forrestyuan.github.io)

植物大战僵尸4.gif

10. 写在最后

使用 Canvas 实现植物大战僵尸可以让我们更好地理解游戏开发的基本流程和技术。同时也可以让我们更好地了解游戏的设计思路和实现方法。在此基础上,我们可以进一步扩展游戏的功能和玩法,例如增加新的植物种类和僵尸种类、添加地图和难度等,来使游戏更加有趣和挑战。
我等你来拓展并完善游戏的功能,代码github上自行fork,改好了在评论区发出你的github仓库链接哈!