Chrome Dino 恐龙游戏 — 指导技术手册
适合人群:刚接触前端、想学习「如何用代码做游戏」的初学者
前置知识:知道 HTML 是什么、JS 变量和函数、见过 CSS 怎么写
看完你能:理解这个恐龙游戏从零到一的完整制作思路,并能动手做一个属于自己的简单精灵图小游戏
前言:这个游戏到底是什么
Chrome 断网时你会看到的那个小恐龙跑酷,有人把它拆出来做成了独立网页游戏。它就是一个文件 —— 一个 HTML 文件,里面包含:
- 一张细长条 PNG 图片(画了所有角色和场景的「精灵图」)
- 一段 JS 代码(控制游戏逻辑)
- 一些 CSS 样式(管外观和动画)
没有任何框架,没有 React/Vue,没有游戏引擎——只有浏览器自带的三个东西:HTML、CSS、JavaScript + Canvas。
我们分析的就是这个游戏是怎么用这几个最基础的技术搭出来的。
建议:边看边对照reference/目录里的文件 —— 打开game.html在浏览器里实际玩玩,感受一下你要理解的东西长什么样。
第 1 课:Canvas —— 浏览器里的画板
🎯 什么是 Canvas
想象你小时候用的那种可以画画的磁性画板 — 你画一笔,板上就多一笔。Canvas 就是网页里的那块画板。
<canvas id="game" width="600" height="150"></canvas>
var canvas = document.getElementById('game');
var ctx = canvas.getContext('2d');
ctx.fillStyle = 'red';
ctx.fillRect(10, 20, 50, 50);
💡 为什么用 Canvas 做游戏
- 你能在任意位置画任意东西(不像普通 HTML 元素只能按文档流排列)
- 画完一帧可以立刻擦掉画下一帧 — 这就是动画的基础
- 浏览器对 Canvas 做了 GPU 硬件加速,60 帧/秒跑起来不卡
📝 这个游戏是怎么用的
关键细节:这个游戏的 HTML 里根本没有 <canvas> 标签!Canvas 是 JS 运行时动态创建的:
var canvas = document.createElement('canvas');
canvas.className = 'runner-canvas';
document.querySelector('.runner-container').appendChild(canvas);
这样做的好处:JS 完全掌控 Canvas 的尺寸和属性,不受 HTML 写死。Canvas 的属性通过 CSS 定义尺寸:
.runner-canvas {
height: 150px;
max-width: 1000px;
position: absolute;
top: 0;
z-index: 2;
}
Canvas = 画板,ctx = 画笔。游戏里所有你能看到的东西全都是用 ctx.xxx() 方法画上去的。Canvas 可以动态创建,不一定要写在 HTML 里。
第 2 课:精灵图 Sprite Sheet —— 把画都挤到一张纸上
🎯 什么是精灵图
假设你要做一本人物走路的翻页动画书,每页画一张图。精灵图的做法就是把所有页平铺在一张大纸上:
💡 为什么要挤在一起
| 好处 | 解释 |
|---|---|
| 一次下载 | 浏览器只要请求 1 个 PNG 文件,不是 50 个 |
| 加载快 | 1 个 HTTP 请求比 50 个快得多 |
| 管理简单 | 所有美术资源在一张图上,不会漏文件 |
| 性能好 | Canvas 切换帧时不用换图片源,只改坐标 |
📝 这个游戏的精灵图实际规格
| 版本 | 文件名 | 实际尺寸 | 为什么有两张 |
|---|---|---|---|
| 1x | 100-offline-sprite.png | 1233 × 68 px | 普通屏幕用 |
| 2x | 200-offline-sprite.png | 2441 × 130 px | Retina 高清屏用 |
2x 图是 1x 图的精确 2 倍 — 内容一样,像素翻倍。
💡 JS 怎么选该用哪张
var useHighRes = window.devicePixelRatio > 1;
var spriteImg = useHighRes
? document.getElementById('offline-resources-2x')
: document.getElementById('offline-resources-1x');
💡 精灵图怎么放到页面里预加载
<div id="offline-resources">
<img id="offline-resources-1x" src="100-offline-sprite.png">
<img id="offline-resources-2x" src="200-offline-sprite.png">
</div>
#offline-resources { display: none; }
关键理解:display: none 只是不显示,浏览器照样会下载 <img src>。等 JS 要用的时候,图片已经在内存里解码好了,传给 Canvas 直接用。
精灵图 = 把所有角色和场景的每一帧画在同一张 PNG 上。只做 1 次 HTTP 请求,Canvas 按坐标裁切显示不同部分。DPR 决定选 1x 还是 2x。自己玩只需要一张 1x 图即可。
第 3 课:drawImage —— 从纸上剪下来贴到画板上
🎯 把精灵图想象成一张贴纸
你有一张大贴纸(精灵图),上面印满了各种图案。现在你要做的是:
- 用剪刀从大贴纸上剪一个 44×47 的恐龙下来
- 把剪下来的小贴纸贴到画板的 (120, 200) 这个位置
Canvas 的 drawImage 就是在帮你做这件事 — 它有 9 个参数:
ctx.drawImage(
spriteImg, // ① 大贴纸本身
892, 5, // ②③ 从大贴纸的哪个像素点开始剪
44, 47, // ④⑤ 剪多宽、多高
120, 200, // ⑥⑦ 贴到画板的哪个位置
132, 141 // ⑧⑨ 贴多宽、多高(可缩放!)
);
💡 9 个参数分组记忆法
drawImage(
贴纸, ← 选图
剪的起点 x, 剪的起点 y, ← 在精灵图上切哪里
剪的宽, 剪的高, ← 切多大
贴的位置 x, 贴的位置 y, ← 画到 Canvas 的哪里
贴的宽, 贴的高 ← 画多大(可缩放)
);
共三组:选图 → 源(切)→ 目标(贴)
💡 像素放大的秘密
为什么精灵图只有 68 像素高,游戏里看起来却很正常?
// 精灵图上的恐龙只有 44×47 像素
// 但画到 Canvas 时放大 3 倍:132×141
ctx.drawImage(sprite, 892, 5, 44, 47,
120, 200,
44*3, 47*3);
配合 CSS:
canvas { image-rendering: pixelated; }
pixelated 告诉浏览器:用最近邻算法放大,不要用平滑插值。这就是像素游戏那种锐利边缘的来源。
💡 数字怎么办 —— 精灵图上的数字
精灵图里预先画好了数字 0-9。显示分数其实就是:把分数的每一位数字,从精灵图上裁出来贴到右上角。
// 分数是 42
// '4':精灵图上第 4 个数字在 x=654 + 4*9 = 690 处
ctx.drawImage(sprite, 690, 2, 9, 13, canvasW-30, 20, 9, 13);
// '2':精灵图上第 2 个数字在 x=654 + 2*9 = 672 处
ctx.drawImage(sprite, 672, 2, 9, 13, canvasW-20, 20, 9, 13);
drawImage 的本质是「剪贴」操作。精灵图上画多小都可以,通过目标宽高参数任意放大。image-rendering: pixelated 是保持锐利的关键。同一个精灵图可以给游戏里所有东西用。
第 4 课:游戏主循环 —— 翻页动画书每秒翻 60 页
🎯 翻页动画书的原理
游戏是这样运行的:
- 清空画板
- 画出当前帧的背景、人物、障碍物……
- 等 1/60 秒
- 回到第 1 步
每一「页」叫一帧(frame),每秒翻 60 页 = 60 FPS。
💡 浏览器提供了一个完美的定时器
function gameLoop() {
update(); // 计算
draw(); // 画画
requestAnimationFrame(gameLoop);
}
requestAnimationFrame(gameLoop);
| requestAnimationFrame | setInterval(16) | |
|---|---|---|
| 刷新率 | 和显示器同步(60/120fps自动适配) | 固定60fps |
| 切标签页 | 自动暂停 | 继续跑 |
| 流畅度 | 浏览器最优时机 | 可能卡顿 |
💡 处理时间差(delta time)
function gameLoop(timestamp) {
var dt = (timestamp - lastTime) / 16.667; // 归一化60fps=1
dt = Math.min(dt, 3); // 上限3:防切标签页瞬移
player.y += vy * dt;
obstacle.x -= speed * dt;
lastTime = timestamp;
requestAnimationFrame(gameLoop);
}
游戏 =「清空→计算→画→等待→重复」。requestAnimationFrame 是最好的循环工具。dt 确保不同性能电脑上速度一致。
第 5 课:人物动起来 —— 切换坐标就是动画
🎯 动画的本质
不是让恐龙真的在动,而是每隔一段时间换一张图。
var runFrames = [
{ x: 892, y: 5, w: 44, h: 47 }, // 左脚前
{ x: 936, y: 5, w: 44, h: 47 }, // 右脚前
];
var idx = Math.floor(time / 100) % runFrames.length;
var frame = runFrames[idx];
ctx.drawImage(sprite, frame.x, frame.y, frame.w, frame.h,
playerX, playerY, frame.w * scale, frame.h * scale);
Math.floor(time / 100) % 2 这条公式:time=0→帧0, time=100→帧1, time=200→帧0…循环往复。
💡 不同东西换帧速度不同
var runIdx = Math.floor(time / 100) % 2; // 跑步:100ms
var flapIdx = Math.floor(time / 80) % 2; // 翅膀:80ms 快
var moonIdx = Math.floor(score / 100) % 7; // 月亮:每100分
动画 = 循环切换源坐标。Math.floor(time/interval) % frameCount 是切换公式。不同对象用不同切换速度。
第 6 课:地面滚动 —— 传送带永远跑不完
🎯 传送带的原理
恐龙定在屏幕左边不动,地面(和障碍物)向左移动。
💡 怎么让地面无限循环
画两段,首尾相接:
var tileW = 1200;
var offset = -(scrollOffset % tileW);
// offset 始终在 [-tileW, 0]
ctx.drawImage(sprite, 2, 54, tileW, 12, offset, groundY, tileW, 12);
ctx.drawImage(sprite, 2, 54, tileW, 12, offset + tileW, groundY, tileW, 12);
当 offset 滚出屏幕,取模让它自动回到右边——无缝循环。
跑酷游戏里角色不动,世界向左滚。-(offset % tileW) 是核心公式。画两段首尾相接。
第 7 课:让角色跳起来 —— 重力和跳跃
🎯 跳跃就是「向上抛球」
var player = { y: GROUND_Y, vy: 0, grounded: true };
function jump() {
if (player.grounded) {
player.vy = -12;
player.grounded = false;
}
}
function updatePhysics() {
if (!player.grounded) {
player.vy += 0.65; // 重力拉回
player.y += player.vy; // 位置改变
if (player.y >= GROUND_Y) {
player.y = GROUND_Y; // 落地
player.vy = 0;
player.grounded = true;
}
}
}
| 参数 | 调大 | 调小 |
|---|---|---|
jumpForce (-12) | 跳更高 | 跳更矮 |
gravity (0.65) | 落更快 | 飘得久 |
跳跃 = 向上初速度 + 每帧重力下拉。两个数字控制手感:jumpForce 和 gravity。
第 8 课:碰撞检测 —— 两个盒子碰上了吗
🎯 两个矩形是否重叠
如果 ① 恐龙右边 > 仙人掌左边 并且 ② 恐龙左边 < 仙人掌右边 并且 ③ 恐龙底边 > 仙人掌顶边 并且 ④ 恐龙顶边 < 仙人掌底边 → 碰上了!
这就是 AABB(轴对齐包围盒)碰撞检测。
function checkCollision(player, obstacle) {
var pl = player.x + 6, pr = player.x + player.w - 6;
var pt = player.y - player.h + 6, pb = player.y - 4;
var ol = obstacle.x - obstacle.w/2 + 3;
var or = obstacle.x + obstacle.w/2 - 3;
var ot = obstacle.y;
var ob = obstacle.y + obstacle.h;
if (pr > ol && pl < or && pb > ot && pt < ob) {
gameOver();
}
}
💡 蹲下有什么用 —— 碰撞盒会变小
按下 ↓ 键时,恐龙蹲下的精灵图更高更宽(59×25),但碰撞盒高度从 47 变成 25。可以躲过高飞的翼龙。
碰撞检测 = 判断两个矩形是否重叠。碰撞盒应比图片略小。蹲下切换碰撞盒高度。
第 9 课:游戏状态 —— 红绿灯控制流程
🎯 游戏有三种「状态」
var gameState = 'waiting';
function update() {
if (gameState !== 'playing') return;
// 物理、碰撞、计分……
}
function onSpace() {
if (gameState === 'waiting') startGame();
else if (gameState === 'playing') jump();
}
💡 死亡动画怎么实现
撞到障碍物后不是立刻弹窗,而是播放 25 帧死亡动画:
var deathTimer = 25;
function draw() {
if (gameState === 'over' && deathTimer > 0) {
deathTimer--;
ctx.globalAlpha = deathTimer / 25;
ctx.scale(deathTimer / 25, deathTimer / 25);
drawDino();
ctx.globalAlpha = 1;
ctx.setTransform(1, 0, 0, 1, 0, 0);
}
}
游戏状态机 = 一个变量 + 三个值。每个函数先判断状态。死亡动画 25 帧后弹窗。
第 10 课:声音 —— 把音效藏在网页里
🎯 两种做法
这个游戏用 Base64 MP3 嵌入做音效:
<template id="audio-resources">
<audio src="data:audio/mpeg;base64,//uQxAAAAA..."></audio>
</template>
💡 为什么用 <template> 标签
用 <template> 包裹后,内容完全不被解析。要播放时 JS 拆开取出:
function playSound(soundId) {
var tmpl = document.getElementById('audio-resources');
var audio = tmpl.content.querySelector('#' + soundId).cloneNode(true);
audio.play();
}
💡 另一种做法(Web Audio API 合成,更省空间)
var osc = audioCtx.createOscillator();
osc.type = 'square';
osc.frequency.setValueAtTime(330, now);
osc.frequency.rampTo(660, now + 0.06);
osc.start(); osc.stop(now + 0.08);
| 方案 | 优点 | 缺点 |
|---|---|---|
| Base64 MP3 | 音质好 | 增大体积~20KB×3 |
| Web Audio 合成 | 零文件体积 | 音色单一 |
音效可内嵌(Base64)或 JS 合成。先做可玩的游戏,再加声音。
第 11 课:搭建脚手架 —— 从零做一个最小游戏
🎯 目标:一个能跑的极简精灵图游戏
📝 第一步:准备你的精灵图
用任何画图软件画一张这样的图:
📝 第二步:HTML 骨架
<!DOCTYPE html>
<html><head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>My Dino Game</title>
<style>
body { margin: 0; min-height: 100vh; display: flex; align-items: center;
justify-content: center; background: #f7f7f7; }
canvas { width: 100%; max-width: 600px; image-rendering: pixelated; cursor: pointer; }
body { touch-action: manipulation; }
</style>
</head>
<body>
<img id="sprite" src="sprite.png" style="display:none">
<canvas id="game"></canvas>
<script>/* ═══ 游戏代码写在这里 ═══ */</script>
</body></html>
📝 第三步:核心 JavaScript(完整 150 行)
// ── 1. 初始化 ──
var canvas = document.getElementById('game');
var ctx = canvas.getContext('2d');
var sprite = document.getElementById('sprite');
canvas.width = 600; canvas.height = 150;
var W = 600, H = 150, GROUND = H - 20;
// ── 2. 精灵坐标定义 ──
var SPRITES = {
idle: { x: 0, y: 0, w: 44, h: 47 },
run1: { x: 44, y: 0, w: 44, h: 47 },
run2: { x: 88, y: 0, w: 44, h: 47 },
cactus: { x: 132,y: 10,w: 20, h: 40 },
};
// ── 3. 游戏状态 ──
var state = 'waiting', score = 0, speed = 3, frameCount = 0;
// ── 4. 玩家 ──
var player = { x: 80, y: GROUND, w: 44, h: 47, vy: 0, grounded: true };
var obstacles = [];
// ── 5. 跳跃 ──
function jump() {
if (player.grounded) { player.vy = -9; player.grounded = false; }
}
// ── 6. 更新 ──
function update(dt) {
if (state !== 'playing') return;
frameCount++;
score += Math.floor(speed * dt * 0.1);
speed += 0.001;
if (!player.grounded) {
player.vy += 0.5; player.y += player.vy;
if (player.y >= GROUND) { player.y = GROUND; player.vy = 0; player.grounded = true; }
}
obstacles.forEach(function(o) { o.x -= speed * dt; });
obstacles = obstacles.filter(function(o) { return o.x > -50; });
var last = obstacles[obstacles.length - 1];
if (!last || last.x < W - (150 + Math.random() * 250)) {
obstacles.push({ x: W + 30, y: GROUND - 40, w: 20, h: 40 });
}
for (var i = 0; i < obstacles.length; i++) {
var o = obstacles[i];
var pl = player.x + 6, pr = player.x + player.w - 6;
var pt = player.y - player.h + 4, pb = player.y - 4;
var ol = o.x - o.w/2 + 3, or = o.x + o.w/2 - 3;
if (pr > ol && pl < or && pb > o.y && pt < o.y + o.h) state = 'over';
}
}
// ── 7. 绘制 ──
function draw() {
ctx.clearRect(0, 0, W, H);
ctx.fillStyle = '#f7f7f7'; ctx.fillRect(0, 0, W, H);
ctx.strokeStyle = '#555'; ctx.lineWidth = 2;
ctx.beginPath(); ctx.moveTo(0, GROUND); ctx.lineTo(W, GROUND); ctx.stroke();
obstacles.forEach(function(o) {
ctx.drawImage(sprite, SPRITES.cactus.x, SPRITES.cactus.y,
SPRITES.cactus.w, SPRITES.cactus.h,
o.x - o.w/2, o.y, o.w, o.h);
});
var frame;
if (!player.grounded) frame = SPRITES.idle;
else if (state === 'playing')
frame = (Math.floor(frameCount/6)%2===0) ? SPRITES.run1 : SPRITES.run2;
else frame = SPRITES.idle;
ctx.drawImage(sprite, frame.x, frame.y, frame.w, frame.h,
player.x, player.y - player.h, player.w, player.h);
ctx.fillStyle = '#333'; ctx.font = '14px sans-serif';
ctx.textAlign = 'right'; ctx.fillText(score + ' m', W - 20, 24);
if (state === 'waiting') {
ctx.textAlign = 'center'; ctx.fillText('Press Space to Start', W/2, H/2);
}
if (state === 'over') {
ctx.textAlign = 'center'; ctx.fillText('Game Over - Tap Retry', W/2, H/2);
}
}
// ── 8. 主循环 ──
var lastTime = 0;
function loop(ts) {
var dt = (ts - lastTime) / 16.667 || 1;
dt = Math.min(dt, 3); lastTime = ts;
update(dt); draw();
requestAnimationFrame(loop);
}
// ── 9. 输入 ──
document.addEventListener('keydown', function(e) {
if (e.code === 'Space' || e.code === 'ArrowUp') {
e.preventDefault();
if (state === 'waiting') state = 'playing';
else if (state === 'over') { state = 'playing'; score = 0; speed = 3; obstacles = []; player.y = GROUND; }
else jump();
}
});
canvas.addEventListener('click', function() {
if (state === 'waiting') state = 'playing';
else if (state === 'over') { state = 'playing'; score = 0; speed = 3; obstacles = []; player.y = GROUND; }
else jump();
});
canvas.addEventListener('touchstart', function(e) { e.preventDefault(); canvas.click(); });
requestAnimationFrame(loop);
💡 这个 150 行的代码就是一个完整游戏
复制到 HTML 文件,配上一张精灵图就能跑。从这里改:
- 改
SPRITES坐标 → 换角色 - 改
speed→ 调难度 - 加更多帧 → 动画更流畅
一个可玩的精灵图游戏最少只要 150 行代码。核心结构永远不变:Canvas → 精灵图 → drawImage → 主循环 → 碰撞 → 输入。最难的步骤在画图。
第 12 课:进阶概念速览
读完前 11 课已经能做一个基础游戏了。以下按推荐学习顺序排列:
12.1 iframe 嵌入
index.html 用 <iframe> 把 game.html 嵌入到更大页面。首页负责 SEO、说明,游戏页只运行游戏。自己玩不需要 iframe。
12.2 背景视差
云朵慢、远山中等、地面快——三层不同速度创造景深:每层 speed * layerFactor。
12.3 粒子效果
灰尘、火花——数组里维护小圆点,每帧移动+缩小+透明后删除。
12.4 音效(Base64 嵌入)
<template><audio src="data:audio/mpeg;base64,..."> 把 MP3 藏进 HTML。
12.5 localStorage 存最高分
localStorage.setItem('hi', score) ——关浏览器重开分数还在。一行代码。
12.6 移动端适配
viewport meta、touch-action、touchstart、matchMedia("orientation")。
12.7 CSS 动画做 UI
加载圈、弹窗淡入——不用 Canvas 画,纯 CSS 更简单(走 GPU 合成层)。
12.8 代码混淆
发布时 JS 压缩成乱码——减小文件大小,防抄袭。自己学不用混淆。
12.9 SEO 结构化数据
ld+json、OpenGraph 标签给搜索引擎看的。自己做来玩不用管。
附录 A:Sprite Sheet 完整坐标参考表
所有坐标基于 1x 版本 (100-offline-sprite.png, 1233×68px)。2x 版本所有坐标 ×2。
上层 (y ≈ 2-5)
| 精灵名称 | x | y | w | h | 说明 |
|---|---|---|---|---|---|
| 重启按钮 | 5 | 15 | 36 | 32 | 圆形箭头 |
| 恐龙-闲置 | 44 | 5 | 44 | 47 | 等待时静态帧 |
| 云朵 | 86 | 5 | 48 | 15 | 背景 |
| 翼龙-翅膀下 | 134 | 12 | 46 | 34 | 帧1 |
| 翼龙-翅膀上 | 180 | 2 | 46 | 30 | 帧2 |
| 大仙人掌-1 | 332 | 2 | 25 | 50 | 单柱 |
| 大仙人掌-2 | 357 | 2 | 50 | 50 | 双柱 |
| 大仙人掌-3 | 407 | 2 | 75 | 50 | 三柱 |
| 数字 0-9 + HI | 654 | 2 | 120 | 13 | 各宽约9-10px |
| 恐龙-跑步1 | 892 | 5 | 44 | 47 | 左脚前 |
| 恐龙-跑步2 | 936 | 5 | 44 | 47 | 右脚前 |
下层 (y ≈ 15-30)
| 精灵名称 | x | y | w | h | 说明 |
|---|---|---|---|---|---|
| 小仙人掌-1 | 228 | 15 | 17 | 35 | 单柱 |
| 小仙人掌-2 | 245 | 15 | 34 | 35 | 双柱 |
| 小仙人掌-3 | 279 | 15 | 51 | 35 | 三柱 |
| 小仙人掌-最矮 | 482 | 28 | 15 | 22 | 极短 |
| 星星 | 484 | 5 | 9 | 33 | 3颗竖排 |
| 月亮-1 | 494 | 15 | 20 | 40 | 新月 |
| 月亮-2 | 514 | 15 | 20 | 40 | 1/4 |
| 月亮-3 | 534 | 15 | 40 | 40 | 满月 |
| 月亮-4 | 574 | 15 | 20 | 40 | 3/4 |
| 月亮-5 | 594 | 15 | 20 | 40 | 残月 |
| 月亮-6 | 614 | 15 | 20 | 40 | 细残月 |
| 月亮-7 | 634 | 15 | 20 | 40 | 细残月 |
| 恐龙-死亡 | 848 | 5 | 44 | 47 | 眼睛叉叉 |
| "GAME OVER" | 654 | 18 | 191 | 13 | 像素字体 |
| 恐龙-蹲1 | 980 | 30 | 59 | 25 | 蹲下帧1 |
| 恐龙-蹲2 | 1039 | 30 | 59 | 25 | 蹲下帧2 |
| 恐龙-蹲死 | 1098 | 30 | 59 | 25 | 蹲下碰撞 |
底边 (y = 54)
| 精灵名称 | x | y | w | h | 说明 |
|---|---|---|---|---|---|
| 地面条 | 2 | 54 | 1200 | 12 | 地面纹理 |
角色核心尺寸速查
| 角色 | 宽×高 | 帧数 |
|---|---|---|
| 恐龙站立/跑步 | 44 × 47 | 3 |
| 恐龙蹲下 | 59 × 25 | 3 |
| 翼龙 | 46 × 34 | 2 |
附录 B:文件清单与各自职责
reference/
├── index.html 首页(SEO、FAQ、操作说明、iframe)
├── game.html 游戏页面(加载 dinorun.js)
├── dinorun.js ★核心游戏 JS(混淆压缩)
├── run.js 首页交互(键盘防滚、FAQ折叠)
├── rate.js 评分组件 AJAX 提交
├── dinosaurgame.css 游戏页面 CSS
├── styleNew.css 首页 CSS
├── 100-offline-sprite.png 1x 精灵图(1233×68px)
├── 200-offline-sprite.png 2x 精灵图(2441×130px)
├── 100-error-offline.png 静态图标 1x
├── 200-error-offline.png 静态图标 2x
├── favicon.png / ico.png 网站图标
├── turnScreen.png 横屏旋转提示
├── stars.svg 评分星星 SVG
├── DinoGamePlay.gif 首页演示 GIF
└── 技术手册.md / 指导技术手册.md 文档
附录 C:DOM 结构参考图
game.html <body>
├── #DINO_loading (z-index: 9999)
│ └── .DINO_spinner ← 纯 CSS 旋转加载动画
├── #DINO_orientationAlert (z-index: 99999)
│ └── <img src="turnScreen.png"> ← 竖屏提示
├── .DINO_game-container
│ └── #t.offline
│ ├── #messageBox
│ └── #main-frame-error
│ ├── #main-content
│ │ └── .icon-offline ← CSS 静态图标
│ └── #offline-resources (display: none)
│ ├── <img 1x sprite> ← 隐藏预加载
│ ├── <img 2x sprite>
│ └── <template audio> ← 按需克隆
├── #DINO_dialogs (z-index: 1000)
│ └── #DINO_gameOverDialog
│ ├── Game Over
│ ├── 得分
│ └── 再玩一次
└── <script src="dinorun.js"> ← 游戏全部逻辑
关键:Canvas 不在 HTML 里!JS 动态创建并插入 #t.offline 中。
附录 D:完整启动流程时间线
T=0s HTML 加载完成
├── <img> 开始后台下载精灵图 PNG
├── 显示加载转圈
└── dinorun.js 开始执行
T=0.1s dinorun.js 初始化
├── 判断 DPR → 选 1x 或 2x
├── createElement('canvas') → 插入 DOM
├── gameState = 'waiting'
└── requestAnimationFrame → 画第一帧
T=0.5s 精灵图 PNG 下载+解码完成
T=2.0s 加载转圈隐藏 + 竖屏检测
T=?s 用户按 Space / 点击
├── gameState = 'playing'
├── 地面开始滚动 + 障碍物生成
├── 恐龙开始跑步动画
└── 分数累加
运行时 每帧循环
├── 分数递增 + 速度递增(每300帧+0.25)
├── 障碍物左移 + 生成新障碍物
├── 碰撞检测
└── 逐层绘制
碰撞时
├── gameState = 'over'
├── 播放碰撞音效 + GameOver 音效
├── 死亡动画 ×25 帧(缩小+透明)
└── 25帧后弹出 GameOver 对话框
附录 E:性能优化清单
| 技术 | 原理 | 效果 |
|---|---|---|
| 精灵图合并 | 多张图合成一张 | 1次HTTP替代N次 |
display:none预加载 | 隐藏但浏览器仍下载 | 图片在JS执行前就绪 |
<template>延迟解析 | 不解析不执行 | 音频不解码直到需要 |
requestAnimationFrame | 浏览器VSync同步 | 60fps丝滑 |
image-rendering:pixelated | GPU最近邻采样 | 放大不模糊 |
| CSS transform/transition | GPU合成层 | 动画流畅 |
<script defer> | 延迟执行 | 不阻塞首屏 |
| 极小文件体积 | 精灵图15KB×2 | 慢网秒加载 |
附录 F:关键 API 速查卡
Canvas 绘制
| API | 作用 |
|---|---|
ctx.drawImage(img,sx,sy,sw,sh,dx,dy,dw,dh) | 精灵图裁切绘制 |
ctx.clearRect(0,0,w,h) | 清空画布 |
ctx.fillRect(x,y,w,h) | 画纯色矩形 |
ctx.fillText(str,x,y) | 写文字 |
ctx.globalAlpha = 0.5 | 全局透明度 |
CSS 关键属性
| 属性 | 值 | 作用 |
|---|---|---|
image-rendering | pixelated | 像素放大不模糊 |
touch-action | manipulation | 禁止双击缩放 |
-webkit-tap-highlight-color | transparent | 禁止点击高亮 |
JavaScript 关键 API
| API | 作用 |
|---|---|
requestAnimationFrame(fn) | 注册下一帧回调 |
document.createElement('canvas') | JS创建Canvas |
localStorage.getItem/setItem | 存取最高分 |
window.devicePixelRatio | 获取设备像素比 |
window.matchMedia('(orientation: portrait)') | 检测屏幕方向 |
template.content.cloneNode(true) | 克隆模板内容 |
audio.play() | 播放音频 |
Math.floor() / % | 取整/取模 |
附录 G:常见疑问解答 FAQ
Q1:精灵图一定要在一行吗?可以多行吗?
可以多行。但单行最简单,第一次做就单行。
Q2:精灵图用什么软件画?
Aseprite(付费,推荐)、Piskel(免费网页版)、Photoshop(铅笔工具)、GIMP、甚至 Windows 画图。
Q3:2x 精灵图是必须的吗?
不是。自己玩只要一张 1x 图就行。写 pixelated 后 1x 也够清晰。
Q4:为什么不用 CSS 动画而要用 Canvas?
CSS 动画适合 UI。Canvas 适合游戏渲染——每帧精确控制位置、碰撞检测、多层绘制。CSS 做不到。
Q5:这个游戏为什么把 JS 搞得那么乱?
代码混淆(packer),防抄袭+减小体积。自己学完全不需要混淆。
Q6:为什么要用 requestAnimationFrame 而不是 setInterval?
setInterval(16) 是盲目的。requestAnimationFrame 智能匹配刷新率,切标签页暂停。任何游戏都用它。
Q7:我可以直接用这个游戏的精灵图吗?
不可以。Chrome Dino 美术是 Google 版权。画一套自己的。
Q8:电脑和手机都能玩,需要两套代码吗?
不需要。Canvas + 同一份 JS + 响应式 CSS 即可。额外加 touch 事件、防缩放、竖屏检测,几行的事。
Q9:障碍物太密/太松怎么调?
// 太密→调大
var minGap = 200; var maxGap = 500;
// 太松→调小
var minGap = 80; var maxGap = 200;
这是整个游戏难度调节的核心参数。
Q10:看完这个手册我接下来该做什么?
- 打开第 11 课的骨架代码,复制一个 HTML 跑起来
- 画一张自己的简单精灵图(一个小人、一个障碍物,够了)
- 改
SPRITES坐标表匹配你的图 - 调代码玩:改跳跃高度、改速度、改障碍物密度
- 加新东西:第二个障碍物、背景云朵、音效
- 给别人玩,听听反馈
先跑起来再改进,不要试图第一次就做完美。
— END —
Design: Pixel Art / Gaming Dark Theme
Fonts: Press Start 2P + VT323 · Google Fonts