本系列带你使用 Deno 构建一个简单的浏览器恐龙跑酷游戏,依次介绍:基本项目设置;游戏循环、画布与控制;障碍物和碰撞检测;数据库与全球排行榜;玩家资料、自定义和实时调节;可观测性、指标和告警。
障碍物和碰撞检测
第2阶段已经设置基本游戏循环和渲染,但还算不上真正的游戏,因为没有可“玩”的内容。第3阶段将加入恐龙需要跳过的障碍物,实现碰撞检测,判断恐龙是否撞上障碍物并结束游戏。
完成本阶段后,你将学会:
- 创建和管理游戏中的障碍物。
- 实现恐龙与障碍物的碰撞检测。
- 根据碰撞更新游戏状态。
设置游戏状态
先向游戏状态加入管理障碍物和计分的新属性。打开 game.js,找到 DinoGame 类的构造函数,加入初始游戏速度、用于恐龙动画的帧计数和最高分追踪:
this.initialGameSpeed = 3;
this.frameCount = 0;
this.highScore = this.loadHighScore();
搭建游戏方法
游戏已有 startGame() 和 resetGame()。接下来加入处理障碍物生成、更新、碰撞检测和计分的方法。
先在现有方法下面添加空方法,稍后填充:
gameOver() {}
loadHighScore() {}
saveHighScore() {}
updateHighScore() {}
drawGameOver() {}
障碍物系统
从游戏设计角度看,障碍物通常是包含位置、大小和类型等属性的对象,其包围盒可用于碰撞检测。
我们实现一个简单系统:生成障碍物,让它们朝恐龙移动。玩家必须跳过它们以避免碰撞。成功避开障碍物时,得分增加,游戏速度也逐步提高。
在 game.js 构造函数中加入以下属性,可以放在 this.init(); 之前:
// Obstacle properties
this.obstacles = [];
this.obstacleSpawnTimer = 0;
this.obstacleSpawnRate = 120;
this.minObstacleSpawnRate = 60;
这些属性分别建立存放活动障碍物的数组、跟踪下次生成时机的计时器,以及控制出现频率的生成间隔。minObstacleSpawnRate 保证提速后障碍物不会出现过于频繁。
在 startGame() 中将障碍物计时器置零,每轮重新开始:
this.obstacleSpawnTimer = 0;
生成和更新障碍物
障碍物是从右边缘进入、按当前游戏速度向左移动的矩形。在类中加入以下方法,可以放在 jump() 之后:
spawnObstacle() {
const obstacleTypes = [
{ width: 20, height: 40, type: "cactus-small" },
{ width: 25, height: 50, type: "cactus-medium" },
{ width: 30, height: 35, type: "cactus-wide" },
];
const obstacle =
obstacleTypes[Math.floor(Math.random() * obstacleTypes.length)];
this.obstacles.push({
x: this.canvas.width,
y: this.groundY - obstacle.height,
width: obstacle.width,
height: obstacle.height,
type: obstacle.type,
});
}
updateObstacles() {
if (this.gameState !== "playing") return;
this.obstacleSpawnTimer++;
if (this.obstacleSpawnTimer >= this.obstacleSpawnRate) {
this.spawnObstacle();
this.obstacleSpawnTimer = 0;
}
for (let i = this.obstacles.length - 1; i >= 0; i--) {
this.obstacles[i].x -= this.gameSpeed;
if (this.obstacles[i].x + this.obstacles[i].width < 0) {
this.obstacles.splice(i, 1);
this.score += 10;
}
}
}
这里设置了几种不同尺寸的障碍物类型。spawnObstacle() 随机选择一种,在画布右边缘加入数组。
updateObstacles() 递增计时器,到达生成间隔时创建障碍物;同时让每个障碍物按当前速度向左移动,并移除离开屏幕的障碍物,为每个成功避开的障碍物加分。
碰撞检测和难度提升
碰撞检测用来判断两个对象是否接触。比较恐龙和每个障碍物的矩形包围盒,发生重叠时结束本轮。在 updateObstacles 之后加入:
checkCollisions() {
if (this.gameState !== "playing") return;
for (let obstacle of this.obstacles) {
const isOverlapping =
this.dino.x < obstacle.x + obstacle.width &&
this.dino.x + this.dino.width > obstacle.x &&
this.dino.y < obstacle.y + obstacle.height &&
this.dino.y + this.dino.height > obstacle.y;
if (isOverlapping) {
this.gameOver();
return;
}
}
}
updateGameDifficulty() {
if (this.gameState !== "playing") return;
const difficultyLevel = Math.floor(this.score / 200);
this.gameSpeed = this.initialGameSpeed + difficultyLevel * 0.5;
this.obstacleSpawnRate = Math.max(
this.minObstacleSpawnRate,
120 - difficultyLevel * 10,
);
}
difficultyLevel 大约每200分提高一级,同时增加速度和生成频率,让玩家存活越久,难度越高。
绘制障碍物
为了避免仙人掌过于单调,每种类型具有略有区别的轮廓。在类中加入以下方法,可以放在 drawDino() 之后:
drawObstacles() {
this.ctx.fillStyle = "olive";
for (let obstacle of this.obstacles) {
this.ctx.fillRect(
obstacle.x,
obstacle.y,
obstacle.width,
obstacle.height,
);
this.ctx.fillStyle = "darkolivegreen";
if (obstacle.type === "cactus-small") {
this.ctx.fillRect(obstacle.x - 3, obstacle.y + 10, 6, 4);
this.ctx.fillRect(obstacle.x + obstacle.width - 3, obstacle.y + 20, 6, 4);
} else if (obstacle.type === "cactus-medium") {
this.ctx.fillRect(obstacle.x - 4, obstacle.y + 8, 8, 6);
this.ctx.fillRect(obstacle.x + obstacle.width - 4, obstacle.y + 15, 8, 6);
this.ctx.fillRect(obstacle.x + obstacle.width / 2 - 2, obstacle.y + 25, 4, 8);
} else if (obstacle.type === "cactus-wide") {
this.ctx.fillRect(obstacle.x - 5, obstacle.y + 5, 10, 8);
this.ctx.fillRect(obstacle.x + obstacle.width - 5, obstacle.y + 10, 10, 8);
this.ctx.fillRect(obstacle.x + obstacle.width / 2 - 3, obstacle.y + 20, 6, 6);
}
this.ctx.fillStyle = "olive";
}
}
为恐龙添加动画
通过上下移动两条小腿,可以得到简单的奔跑动画。将 drawDino() 更新为:
drawDino() {
const legOffset = this.gameState === "playing" && !this.dino.isJumping
? (Math.floor(this.frameCount / 10) % 2) * 2
: 0;
this.ctx.fillStyle = "green";
this.ctx.fillRect(
this.dino.x,
this.dino.y,
this.dino.width,
this.dino.height,
);
this.ctx.fillStyle = "darkgreen";
this.ctx.fillRect(this.dino.x + 25, this.dino.y + 8, 4, 4);
this.ctx.fillRect(this.dino.x + 30, this.dino.y + 20, 8, 2);
if (!this.dino.isJumping) {
this.ctx.fillStyle = "green";
this.ctx.fillRect(
this.dino.x + 10,
this.dino.y + 40 + legOffset,
6,
8 - legOffset,
);
this.ctx.fillRect(
this.dino.x + 24,
this.dino.y + 40 - legOffset,
6,
8 + legOffset,
);
}
}
渲染游戏组件
更新 render():先清空画布,再调用 drawObstacles() 和 drawDino(),并在适当状态叠加说明界面或游戏结束界面。
render() {
// Clear canvas
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
// Draw obstacles
this.drawObstacles();
// Draw dino
this.drawDino();
// Draw instructions if waiting
if (this.gameState === "waiting") {
this.drawInstructions();
}
// Draw game over screen
if (this.gameState === "gameOver") {
this.drawGameOver();
}
}
接入物理更新、开始和重置
更新 startGame() 和 resetGame(),确保开始或重置时,障碍物系统正确初始化并清理。
游戏开始时重置障碍物数组,将速度和帧计数恢复为初始值:
startGame() {
this.gameState = "playing";
this.score = 0;
this.gameSpeed = this.initialGameSpeed;
this.obstacles = [];
this.obstacleSpawnTimer = 0;
this.frameCount = 0;
this.updateScore();
this.updateStatus("");
}
然后更新 resetGame(),清理残留障碍物,让重新开始即时生效:
resetGame() {
this.gameState = "waiting";
this.score = 0;
this.gameSpeed = this.initialGameSpeed;
this.obstacles = [];
this.obstacleSpawnTimer = 0;
this.frameCount = 0;
this.dino.y = this.dino.groundY;
this.dino.velocityY = 0;
this.dino.isJumping = false;
this.updateScore();
this.updateStatus("Click to Start!");
console.log("Game reset!");
}
最后在 updatePhysics() 中加入障碍物更新和碰撞检查:
updatePhysics() {
if (this.gameState !== "playing") return;
this.frameCount++;
// Apply gravity
this.dino.velocityY += this.gravity;
this.dino.y += this.dino.velocityY;
// Ground collision
if (this.dino.y >= this.dino.groundY) {
this.dino.y = this.dino.groundY;
this.dino.velocityY = 0;
this.dino.isJumping = false;
}
// Update score (continuous scoring)
this.score += 0.1;
this.updateScore();
// Update obstacles
this.updateObstacles();
// Check collisions
this.checkCollisions();
// Update difficulty
this.updateGameDifficulty();
}
游戏结束
目前已有跳跃的恐龙和移动的障碍物,但碰撞后尚无处理。现在补齐。
检测到碰撞时,需要结束游戏并显示结束界面。更新空的 gameOver(),设置状态、保存最高分并记录消息:
gameOver() {
this.gameState = "gameOver";
this.saveHighScore();
this.updateHighScore();
this.updateStatus("Game Over! Click to restart");
console.log(`Game Over! Final Score: ${Math.floor(this.score)}`);
}
接着更新 drawGameOver(),在画布上叠加简单的结束提示:
drawGameOver() {
// Semi-transparent overlay
this.ctx.fillStyle = "rgba(0, 0, 0, 0.8)";
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
// Game Over text
this.ctx.fillStyle = "white";
this.ctx.font = "36px Arial";
this.ctx.textAlign = "center";
this.ctx.fillText(
"GAME OVER",
this.canvas.width / 2,
this.canvas.height / 2 - 40,
);
// Final score
this.ctx.font = "20px Arial";
this.ctx.fillText(
`Final Score: ${Math.floor(this.score)}`,
this.canvas.width / 2,
this.canvas.height / 2 - 5,
);
// High score
if (Math.floor(this.score) === this.highScore && this.highScore > 0) {
this.ctx.fillStyle = "gold";
this.ctx.fillText(
"🏆 NEW HIGH SCORE! 🏆",
this.canvas.width / 2,
this.canvas.height / 2 + 25,
);
} else if (this.highScore > 0) {
this.ctx.fillStyle = "#CCCCCC";
this.ctx.fillText(
`High Score: ${this.highScore}`,
this.canvas.width / 2,
this.canvas.height / 2 + 25,
);
}
// Restart instruction
this.ctx.fillStyle = "#FFFFFF";
this.ctx.font = "16px Arial";
this.ctx.fillText(
"Click or press SPACE to restart",
this.canvas.width / 2,
this.canvas.height / 2 + 55,
);
}
最高分
为了让玩家感受到最佳表现的奖励,用 localStorage 实现简单的最高分系统。创建加载、保存和更新最高分的方法,并将最高分追踪接入结束和重置逻辑。更新 DinoGame 中的空方法:
loadHighScore() {
return parseInt(localStorage.getItem("dinoHighScore")) || 0;
}
saveHighScore() {
if (Math.floor(this.score) > this.highScore) {
this.highScore = Math.floor(this.score);
localStorage.setItem("dinoHighScore", this.highScore);
console.log(`New High Score: ${this.highScore}!`);
}
}
updateHighScore() {
if (this.highScoreElement) {
this.highScoreElement.textContent = this.highScore;
}
}
在 init() 中加入以下调用,在游戏初始化时更新最高分显示:
this.updateHighScore();
完成第3阶段
在类外保留第2阶段的健康检查,页面加载时确认 API 路由可用:
async function checkHealth() {
try {
const response = await fetch("/api/health");
const data = await response.json();
console.log("Server health check:", data);
} catch (error) {
console.error("Health check failed:", error);
}
}
window.addEventListener("load", () => {
checkHealth();
new DinoGame();
console.log(
"Stage 3 complete: Full game with obstacles and collision detection!",
);
});
运行后,游戏便有了障碍物和碰撞检测。原文此处展示运行效果;本译稿尚未复现该演示。
部署更新后的游戏
已有基本循环、恐龙跳跃和分数追踪,现在可以将更新部署到网页。在终端运行以下命令,更新第2阶段已部署的项目:
deno deploy
部署后,会得到一个恐龙可以跳过障碍物、碰撞时结束的完整游戏,也会获得可分享给其他玩家的 URL。
第4阶段将连接后端,在服务器保存分数,并为多人排行榜铺路。
原文:Build a dinosaur runner game with Deno, pt. 3
作者:Jo Franchetti,2025年12月22日。本文为中文译稿,依赖第2阶段已有代码;代码、命令及字符串保留原文。没有实际执行 deno deploy。Copyright © Deno Land Inc. 原文及示例权利归原权利人所有,转载依据用户明确授权;示例仓库许可证待发布前核验。











暂无评论内容