网页小游戏开发

01做个打地鼠:状态机、定时器和一点点手感

约 20 分钟

先玩两把:打地鼠 · 完整源码

整个游戏是一个 HTML 文件:9 个洞、1 个计时器、不到 300 行代码。这一课按“把它做出来需要想清楚哪几件事”来讲。

一、游戏拆解

打地鼠看着热闹,逻辑其实只有四个问题:

  1. 什么时候出洞、从哪个洞出 —— 随机性
  2. 怎么判断打中了 —— 状态机
  3. 分数和命中率怎么算 —— 计数
  4. 什么时候结束 —— 倒计时

四件事想清楚了,游戏就写完了。

二、核心:每个洞是一个小状态机

这是整个游戏最重要的设计。每只鼹鼠只有三种状态:

idle(在地里) → up(冒头) → idle(缩回去)
                 ↑
              hit(被敲中) ← 从 up 可以走到这里
const holes = [];  // 9 个洞,每个洞一个状态
holes.push({ el, mole, state: 'idle', timer: 0, type: 'mole' });

为什么需要状态机而不是“显示 / 隐藏”两个布尔值?因为只有 up 状态才能被打中。如果鼹鼠已经在下落的动画里还被算一击,玩家会觉得判定不对。状态机让每行逻辑都很直白:

function whack(h) {
  if (!running || h.state !== 'up') return;  // 只有冒头的才能打
  h.state = 'hit';
  // 计分、音效、飘字……
}

每次写游戏逻辑卡壳的时候,先问一句:是不是状态没收干净?

三、随机性和难度曲线

出洞是一个自我循环的定时器,不是 setInterval:

function spawn() {
  if (!running) return;
  // 从 idle 的洞里随机挑一个,让它冒头
  const p = (DURATION - timeLeft) / DURATION;   // 0 → 1,时间进度
  spawnTimer = setTimeout(spawn, 900 - p * 500 + Math.random() * 250);
}

用 setTimeout 链式调用而不是 setInterval,好处是间隔可以每一轮都变:

  • 开场 900ms 左右出一只,结尾接近 400ms 一只
  • 鼹鼠停留时间也从 1400ms 缩到 700ms

这就是一条最简陋但有效的难度曲线:不是换关卡,是让同一个机制越跑越快。玩家感受到的“越来越紧张”,背后只是一个线性插值。

鼹鼠类型也是在这里决定的:10% 炸弹、8% 金鼠,其余普通。

四、敲击判定与飘字

鼹鼠本身设了 pointer-events: none,所以指针事件永远落在“洞”上,再由洞查自己状态:

board.addEventListener('pointerdown', (e) => {
  const h = holes.find(x => x.el === e.target.closest('.hole'));
  if (h.state === 'up') { whack(h); }       // 命中
  else if (running) { attempts++; }         // 敲空,也算一次尝试
});

用 pointerdown 而不是 click,因为 click 要等抬起,手感慢半拍。一个小细节:敲空不算失败、只算进命中率,玩家不至于觉得惩罚太重,但想刷 100% 命中的人又有了追求。

分数飘字是一个临时 <div>,加进去 650ms 后删掉,不用管它的生命周期。

五、没素材怎么做出样子

  • 鼹鼠、炸弹、锤子都是内联 SVG。金鼠不换图,直接 filter: sepia() 调色
  • 音效用 WebAudio 合成:OscillatorNode 发个 120ms 的方波就是敲击声,锯齿波低音就是炸弹声。零音频文件
  • 鼹鼠冒头是 translateY 过渡动画,缩回去就是取消类名。没用 requestAnimationFrame,CSS 就够

一个值得记住的判断:这类小游戏的“手感”八成来自动画时长和音效时机,而不是贴图质量。把 transition 从 0.12s 改成 0.4s,游戏立刻变得肉。

六、结课的坑

  • 计时器嵌套了 setTimeout,结束时要把每只鼹鼠的 h.timer 都清掉,否则重开一局会有“鬼魂鼹鼠”冒出来
  • 最高分用 localStorage 存,读写都要容错(隐私模式可能抛异常)
  • cursor: none + 自定义锤子光标只在 pointer: fine(有鼠标)时启用,手机上保留默认触控

练习

  1. 加一只“忍者鼹鼠”:停留时间只有普通鼹鼠的一半,+5 分
  2. 敲空三次扣 1 分,观察这对命中率指标的影响
  3. 把 9 个洞改成难度可选:3×3、4×4 两档