JavaScript Canvas 游戏:使用类管理多个独立移动的敌人
在构建基于 Canvas 的网页游戏时,管理多个独立移动的敌人是一个常见且核心的需求。如果为每个敌人编写重复的移动逻辑,代码会变得冗长且难以维护。通过 JavaScript 的类(Class)机制,我们可以将敌人的属性(如位置、速度、大小)和行为(如移动、绘制)封装在一起,从而优雅地管理成百上千个独立实体。本文将详细讲解如何实现这一过程。
一、面向对象与Canvas基础
在开始编码之前,我们先明确两个核心概念。首先,Canvas 提供了一个 <canvas> 元素,我们可以通过 JavaScript 在其上绘制图形。其次,类是 ES6 引入的语法糖,本质上是一个构造函数,用于创建具有相同属性和方法的对象。使用类来管理敌人,可以让我们轻松创建多个互不干扰的敌人实例。
基本的 HTML 结构如下:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Canvas 敌人管理示例</title>
<style>
body { margin: 0; display: flex; justify-content: center; align-items: center; height: 100vh; background: #222; }
canvas { border: 2px solid #fff; background: #111; }
</style>
</head>
<body>
<canvas id="gameCanvas" width="800" height="600"></canvas>
<script src="game.js"></script>
</body>
</html>注意,上述代码中的 <canvas> 标签在代码块内部已经进行了正确的转义处理。
二、定义 Enemy 类
我们定义一个 Enemy 类,包含以下属性和方法:
- 属性:
x、y(位置)、speedX、speedY(速度)、radius(大小)、color(颜色) - 方法:
update()(更新位置)、draw(ctx)(绘制自身)
类的定义如下:
class Enemy {
// 构造函数:初始化敌人的属性
constructor(x, y, speedX, speedY, radius, color) {
this.x = x;
this.y = y;
this.speedX = speedX; // 水平速度
this.speedY = speedY; // 垂直速度
this.radius = radius; // 半径
this.color = color; // 颜色
}
// 更新敌人位置,并处理边界反弹逻辑
update(canvasWidth, canvasHeight) {
this.x += this.speedX;
this.y += this.speedY;
// 左右边界碰撞检测与反弹
if (this.x - this.radius < 0) {
this.x = this.radius;
this.speedX = -this.speedX;
} else if (this.x + this.radius > canvasWidth) {
this.x = canvasWidth - this.radius;
this.speedX = -this.speedX;
}
// 上下边界碰撞检测与反弹
if (this.y - this.radius < 0) {
this.y = this.radius;
this.speedY = -this.speedY;
} else if (this.y + this.radius > canvasHeight) {
this.y = canvasHeight - this.radius;
this.speedY = -this.speedY;
}
}
// 在 Canvas 上绘制敌人
draw(ctx) {
ctx.beginPath();
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
ctx.fillStyle = this.color;
ctx.fill();
ctx.closePath();
}
}在上述代码中,update() 方法不仅负责移动敌人,还检测敌人是否碰到了画布的边界。当碰到边界时,敌人的速度方向会反转,从而实现反弹效果。每个敌人实例都拥有独立的速度和位置,因此它们会沿着各自的轨迹移动。
三、创建多个敌人实例并管理
有了 Enemy 类之后,创建多个敌人就变得非常简单。我们可以使用一个数组来存储所有的敌人实例,并在循环中初始化它们。每个敌人可以拥有不同的颜色、大小和移动速度,从而实现多样化的行为。
// 获取 Canvas 元素和绘图上下文
const canvas = document.getElementById("gameCanvas");
const ctx = canvas.getContext("2d");
// 存储所有敌人的数组
const enemies = [];
// 生成随机颜色辅助函数
function randomColor() {
const r = Math.floor(Math.random() * 256);
const g = Math.floor(Math.random() * 256);
const b = Math.floor(Math.random() * 256);
return `rgb(${r},${g},${b})`;
}
// 初始化 20 个敌人,随机分布位置、速度和颜色
const enemyCount = 20;
for (let i = 0; i < enemyCount; i++) {
const radius = 10 + Math.random() * 20; // 10 到 30 之间
const x = radius + Math.random() * (canvas.width - 2 * radius);
const y = radius + Math.random() * (canvas.height - 2 * radius);
const speedX = (Math.random() - 0.5) * 4; // -2 到 2 之间
const speedY = (Math.random() - 0.5) * 4;
const color = randomColor();
// 创建敌人实例并加入数组
const enemy = new Enemy(x, y, speedX, speedY, radius, color);
enemies.push(enemy);
}这段代码创建了 20 个敌人,每个敌人都有随机的起始位置、速度、大小和颜色。所有敌人实例都存储在 enemies 数组中,方便后续统一更新和绘制。
四、动画循环:驱动所有敌人
游戏的灵魂是动画循环。我们使用 requestAnimationFrame 来创建一个持续运行的循环。在每一帧中,我们执行以下三步:
- 清空画布,为绘制新一帧做准备。
- 遍历
enemies数组,调用每个敌人的update()方法更新位置。 - 再次遍历数组,调用每个敌人的
draw()方法绘制到画布上。
动画循环的代码如下:
// 动画循环函数
function gameLoop() {
// 1. 清空画布
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 2. 更新所有敌人的位置
for (let enemy of enemies) {
enemy.update(canvas.width, canvas.height);
}
// 3. 绘制所有敌人
for (let enemy of enemies) {
enemy.draw(ctx);
}
// 请求下一帧
requestAnimationFrame(gameLoop);
}
// 启动游戏循环
gameLoop();在这个循环中,每个敌人都是独立更新的。因为 update() 方法操作的是实例自身的 x 和 y 属性,所以彼此之间不会相互干扰。这就是使用类管理多个独立移动实体的核心优势。
五、敌人之间的碰撞检测(进阶)
在很多游戏中,敌人之间也需要发生交互,例如互相排斥或合并。我们可以在 Enemy 类中添加一个静态方法来判断两个敌人是否碰撞,并让它们做出反应。
下面是一个简单的弹性碰撞示例,当两个敌人发生碰撞时,它们会交换速度:
class Enemy {
// ... 之前的属性和方法保持不变 ...
// 静态方法:判断两个敌人是否碰撞
static checkCollision(enemyA, enemyB) {
const dx = enemyA.x - enemyB.x;
const dy = enemyA.y - enemyB.y;
const distance = Math.sqrt(dx * dx + dy * dy);
return distance < enemyA.radius + enemyB.radius;
}
// 实例方法:与另一个敌人发生弹性碰撞
bounceWith(other) {
// 交换速度向量
const tempSpeedX = this.speedX;
const tempSpeedY = this.speedY;
this.speedX = other.speedX;
this.speedY = other.speedY;
other.speedX = tempSpeedX;
other.speedY = tempSpeedY;
// 将两个敌人分开,避免粘在一起
const dx = this.x - other.x;
const dy = this.y - other.y;
const distance = Math.sqrt(dx * dx + dy * dy);
const overlap = this.radius + other.radius - distance;
if (overlap > 0) {
const angle = Math.atan2(dy, dx);
const moveX = overlap * 0.5 * Math.cos(angle);
const moveY = overlap * 0.5 * Math.sin(angle);
this.x += moveX;
this.y += moveY;
other.x -= moveX;
other.y -= moveY;
}
}
}然后,在游戏循环的更新阶段,添加碰撞检测逻辑:
function gameLoop() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 更新位置
for (let enemy of enemies) {
enemy.update(canvas.width, canvas.height);
}
// 检测敌人之间的碰撞并处理
for (let i = 0; i < enemies.length; i++) {
for (let j = i + 1; j < enemies.length; j++) {
if (Enemy.checkCollision(enemies[i], enemies[j])) {
enemies[i].bounceWith(enemies[j]);
}
}
}
// 绘制所有敌人
for (let enemy of enemies) {
enemy.draw(ctx);
}
requestAnimationFrame(gameLoop);
}通过这种方式,每个敌人不仅独立移动,还会在相遇时发生互动,大大提升了游戏的趣味性和真实感。
六、完整示例汇总
将上述所有代码整合到 game.js 文件中,完整的代码如下所示。请注意,这里省略了 HTML 部分,因为前面已经给出。
// ========== Enemy 类定义 ==========
class Enemy {
constructor(x, y, speedX, speedY, radius, color) {
this.x = x;
this.y = y;
this.speedX = speedX;
this.speedY = speedY;
this.radius = radius;
this.color = color;
}
update(canvasWidth, canvasHeight) {
this.x += this.speedX;
this.y += this.speedY;
// 边界反弹
if (this.x - this.radius < 0) {
this.x = this.radius;
this.speedX = -this.speedX;
} else if (this.x + this.radius > canvasWidth) {
this.x = canvasWidth - this.radius;
this.speedX = -this.speedX;
}
if (this.y - this.radius < 0) {
this.y = this.radius;
this.speedY = -this.speedY;
} else if (this.y + this.radius > canvasHeight) {
this.y = canvasHeight - this.radius;
this.speedY = -this.speedY;
}
}
draw(ctx) {
ctx.beginPath();
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
ctx.fillStyle = this.color;
ctx.fill();
ctx.closePath();
}
static checkCollision(enemyA, enemyB) {
const dx = enemyA.x - enemyB.x;
const dy = enemyA.y - enemyB.y;
const distance = Math.sqrt(dx * dx + dy * dy);
return distance < enemyA.radius + enemyB.radius;
}
bounceWith(other) {
const tempSpeedX = this.speedX;
const tempSpeedY = this.speedY;
this.speedX = other.speedX;
this.speedY = other.speedY;
other.speedX = tempSpeedX;
other.speedY = tempSpeedY;
const dx = this.x - other.x;
const dy = this.y - other.y;
const distance = Math.sqrt(dx * dx + dy * dy);
const overlap = this.radius + other.radius - distance;
if (overlap > 0) {
const angle = Math.atan2(dy, dx);
const moveX = overlap * 0.5 * Math.cos(angle);
const moveY = overlap * 0.5 * Math.sin(angle);
this.x += moveX;
this.y += moveY;
other.x -= moveX;
other.y -= moveY;
}
}
}
// ========== 初始化 ==========
const canvas = document.getElementById("gameCanvas");
const ctx = canvas.getContext("2d");
const enemies = [];
function randomColor() {
const r = Math.floor(Math.random() * 256);
const g = Math.floor(Math.random() * 256);
const b = Math.floor(Math.random() * 256);
return `rgb(${r},${g},${b})`;
}
const enemyCount = 20;
for (let i = 0; i < enemyCount; i++) {
const radius = 10 + Math.random() * 20;
const x = radius + Math.random() * (canvas.width - 2 * radius);
const y = radius + Math.random() * (canvas.height - 2 * radius);
const speedX = (Math.random() - 0.5) * 4;
const speedY = (Math.random() - 0.5) * 4;
const color = randomColor();
enemies.push(new Enemy(x, y, speedX, speedY, radius, color));
}
// ========== 游戏循环 ==========
function gameLoop() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (let enemy of enemies) {
enemy.update(canvas.width, canvas.height);
}
// 碰撞检测与响应
for (let i = 0; i < enemies.length; i++) {
for (let j = i + 1; j < enemies.length; j++) {
if (Enemy.checkCollision(enemies[i], enemies[j])) {
enemies[i].bounceWith(enemies[j]);
}
}
}
for (let enemy of enemies) {
enemy.draw(ctx);
}
requestAnimationFrame(gameLoop);
}
gameLoop();将这段代码保存为 game.js,并与之前的 HTML 文件放在同一目录下,在浏览器中打开即可看到 20 个色彩缤纷的圆形敌人在画布中独立移动并相互反弹。
七、总结与扩展建议
通过本文的讲解,我们掌握了如何使用 JavaScript 的类来管理 Canvas 游戏中多个独立移动的敌人。这种方法的核心优势在于:
- 封装性:每个敌人的属性和行为都封装在类内部,代码结构清晰。
- 可扩展性:可以轻松为敌人添加新的行为,如追踪玩家、发射子弹等。
- 可维护性:修改敌人的逻辑只需要改动类定义,而不需要改动管理代码。
- 性能友好:通过数组统一管理,可以使用
for循环高效处理大量敌人。
如果需要进一步扩展,可以考虑以下方向:
- 为敌人添加不同形态的子类,如
FastEnemy、TankEnemy,通过继承实现多态。 - 引入玩家控制的对象,并实现敌人与玩家之间的碰撞检测。
- 使用对象池技术优化大量敌人的创建和销毁性能。
- 结合粒子系统,在敌人被消灭时产生爆炸效果。
使用类来管理游戏实体是现代 JavaScript 游戏开发的基础范式。掌握了这种模式,你就拥有了构建复杂游戏世界的核心能力。现在,打开你的编辑器,开始创造属于你自己的游戏吧。