1. 项目概述与核心价值
这个打地鼠游戏项目是一个典型的Web前端练手项目,非常适合刚掌握HTML+CSS+JavaScript基础的学习者进行实战演练。通过这个项目,开发者可以综合运用DOM操作、事件处理、定时器、动画效果等前端核心技术,同时培养游戏逻辑思维。
我在2015年第一次接触这个项目时,发现它看似简单实则暗藏玄机。一个完整的打地鼠游戏需要考虑:
- 游戏场景的视觉呈现(CSS布局与动画)
- 地鼠随机出现的逻辑控制(JavaScript算法)
- 用户交互的即时响应(事件监听)
- 游戏状态的动态管理(分数计算、难度调整)
2. 技术架构解析
2.1 HTML结构设计
游戏的主体结构采用语义化HTML5标签:
html复制<div class="game-container">
<header class="game-header">
<h1>打地鼠</h1>
<div class="score-board">
<span>分数:</span>
<span id="score">0</span>
</div>
</header>
<div class="game-field">
<div class="hole" id="hole1"></div>
<div class="hole" id="hole2"></div>
<!-- 更多洞位... -->
</div>
<div class="control-panel">
<button id="start-btn">开始游戏</button>
<select id="level-select">
<option value="easy">简单</option>
<option value="medium">中等</option>
<option value="hard">困难</option>
</select>
</div>
</div>
关键技巧:使用data-*属性存储洞位状态比直接操作className性能更好
2.2 CSS动画实现
地鼠的进出动画采用CSS3 transition实现流畅效果:
css复制.hole {
position: relative;
width: 100px;
height: 100px;
overflow: hidden;
background: url('hole.png') center no-repeat;
}
.mole {
position: absolute;
width: 80px;
height: 80px;
bottom: -80px;
left: 10px;
background: url('mole.png') center no-repeat;
transition: bottom 0.3s ease-out;
cursor: pointer;
}
.mole.up {
bottom: 10px;
}
.mole.hit {
background: url('mole-hit.png') center no-repeat;
}
实测发现:使用transform: translateY()比修改bottom值性能更优
2.3 JavaScript核心逻辑
游戏主控逻辑采用面向对象方式组织:
javascript复制class WhackAMole {
constructor() {
this.score = 0;
this.timeUp = false;
this.lastHole = null;
this.holes = document.querySelectorAll('.hole');
this.scoreBoard = document.querySelector('#score');
this.startButton = document.querySelector('#start-btn');
this.levelSelect = document.querySelector('#level-select');
this.difficulty = {
easy: { delay: 1500, duration: 1000 },
medium: { delay: 1000, duration: 800 },
hard: { delay: 600, duration: 500 }
};
}
startGame() {
this.score = 0;
this.timeUp = false;
this.scoreBoard.textContent = 0;
this.startButton.disabled = true;
const level = this.levelSelect.value;
this.peep(level);
setTimeout(() => {
this.timeUp = true;
this.startButton.disabled = false;
}, 10000); // 10秒游戏时长
}
peep(level) {
if (this.timeUp) return;
const time = this.difficulty[level];
const hole = this.randomHole();
hole.classList.add('up');
setTimeout(() => {
hole.classList.remove('up');
if (!this.timeUp) this.peep(level);
}, time.duration);
setTimeout(() => this.peep(level), time.delay);
}
randomHole() {
const idx = Math.floor(Math.random() * this.holes.length);
const hole = this.holes[idx];
// 避免连续在同一个洞出现
if (hole === this.lastHole) {
return this.randomHole();
}
this.lastHole = hole;
return hole;
}
bonk(e) {
if (!e.isTrusted) return; // 防止脚本作弊
this.score++;
this.scoreBoard.textContent = this.score;
e.target.classList.add('hit');
setTimeout(() => {
e.target.classList.remove('hit', 'up');
}, 300);
}
}
// 初始化游戏
const game = new WhackAMole();
game.holes.forEach(hole =>
hole.addEventListener('click', (e) => game.bonk(e))
);
game.startButton.addEventListener('click', () => game.startGame());
3. 性能优化实践
3.1 动画性能提升
使用will-change属性预声明动画元素:
css复制.mole {
will-change: transform;
transform: translateY(100%);
}
.mole.up {
transform: translateY(0);
}
3.2 事件委托优化
将每个洞的点击监听改为事件委托:
javascript复制document.querySelector('.game-field').addEventListener('click', function(e) {
if (e.target.classList.contains('mole')) {
game.bonk(e);
}
});
3.3 资源预加载
在游戏开始前预加载所有图片:
javascript复制function preloadImages() {
const images = ['hole.png', 'mole.png', 'mole-hit.png'];
images.forEach(src => {
new Image().src = src;
});
}
window.addEventListener('load', preloadImages);
4. 扩展功能实现
4.1 音效增强
添加击打音效和背景音乐:
javascript复制// 音频上下文初始化
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
function playSound(frequency, duration) {
const oscillator = audioContext.createOscillator();
const gainNode = audioContext.createGain();
oscillator.connect(gainNode);
gainNode.connect(audioContext.destination);
oscillator.type = 'square';
oscillator.frequency.value = frequency;
gainNode.gain.exponentialRampToValueAtTime(
0.00001, audioContext.currentTime + duration
);
oscillator.start();
oscillator.stop(audioContext.currentTime + duration);
}
// 修改bonk方法
bonk(e) {
if (!e.isTrusted) return;
playSound(800, 0.1); // 击打音效
// ...原有逻辑
}
4.2 游戏存档功能
使用localStorage保存最高分:
javascript复制class WhackAMole {
constructor() {
this.highScore = localStorage.getItem('moleHighScore') || 0;
// ...其他初始化
}
endGame() {
if (this.score > this.highScore) {
this.highScore = this.score;
localStorage.setItem('moleHighScore', this.highScore);
}
// ...其他结束逻辑
}
}
5. 常见问题排查
5.1 地鼠点击无效
可能原因及解决方案:
- z-index问题:确保地鼠元素的z-index高于洞
- 事件冒泡阻止:检查是否有父元素阻止了事件传播
- CSS指针事件:确认没有设置pointer-events: none
5.2 动画卡顿
优化建议:
- 减少同时显示的动画元素数量
- 使用requestAnimationFrame替代setTimeout
- 对静态元素使用will-change提示浏览器
5.3 移动端适配
关键调整:
css复制/* 触摸反馈优化 */
.mole {
-webkit-tap-highlight-color: transparent;
}
/* 响应式布局 */
.game-field {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
gap: 10px;
}
6. 完整项目代码
以下是整合所有优化后的完整代码:
html复制<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>打地鼠游戏</title>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Arial', sans-serif;
background: #f5f5f5;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
padding: 20px;
}
.game-container {
background: white;
border-radius: 10px;
box-shadow: 0 0 20px rgba(0,0,0,0.1);
overflow: hidden;
width: 100%;
max-width: 600px;
}
.game-header {
background: #8BC34A;
color: white;
padding: 15px;
text-align: center;
}
.score-board {
margin-top: 10px;
font-size: 1.2em;
}
.game-field {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 15px;
padding: 20px;
background: #4CAF50;
}
.hole {
position: relative;
width: 100%;
aspect-ratio: 1/1;
overflow: hidden;
background: #5D4037;
border-radius: 50%;
}
.mole {
position: absolute;
width: 80%;
height: 80%;
bottom: -80%;
left: 10%;
background: #795548;
border-radius: 50%;
transition: transform 0.3s ease-out;
will-change: transform;
cursor: pointer;
user-select: none;
-webkit-tap-highlight-color: transparent;
}
.mole.up {
transform: translateY(-100%);
}
.mole.hit {
background: #D32F2F;
}
.control-panel {
padding: 15px;
display: flex;
justify-content: space-between;
background: #f5f5f5;
}
button, select {
padding: 8px 15px;
border: none;
border-radius: 4px;
background: #2196F3;
color: white;
cursor: pointer;
}
button:disabled {
background: #BBDEFB;
cursor: not-allowed;
}
select {
background: white;
color: #333;
border: 1px solid #ddd;
}
</style>
</head>
<body>
<div class="game-container">
<header class="game-header">
<h1>打地鼠</h1>
<div class="score-board">
<span>分数:</span>
<span id="score">0</span>
<span> | 最高分:</span>
<span id="high-score">0</span>
</div>
</header>
<div class="game-field">
<div class="hole" id="hole1"><div class="mole"></div></div>
<div class="hole" id="hole2"><div class="mole"></div></div>
<div class="hole" id="hole3"><div class="mole"></div></div>
<div class="hole" id="hole4"><div class="mole"></div></div>
<div class="hole" id="hole5"><div class="mole"></div></div>
<div class="hole" id="hole6"><div class="mole"></div></div>
</div>
<div class="control-panel">
<button id="start-btn">开始游戏</button>
<select id="level-select">
<option value="easy">简单</option>
<option value="medium">中等</option>
<option value="hard">困难</option>
</select>
</div>
</div>
<script>
class WhackAMole {
constructor() {
this.score = 0;
this.timeUp = false;
this.lastHole = null;
this.holes = document.querySelectorAll('.hole');
this.moles = document.querySelectorAll('.mole');
this.scoreBoard = document.querySelector('#score');
this.highScoreBoard = document.querySelector('#high-score');
this.startButton = document.querySelector('#start-btn');
this.levelSelect = document.querySelector('#level-select');
this.highScore = localStorage.getItem('moleHighScore') || 0;
this.highScoreBoard.textContent = this.highScore;
this.difficulty = {
easy: { delay: 1500, duration: 1000 },
medium: { delay: 1000, duration: 800 },
hard: { delay: 600, duration: 500 }
};
this.setupEventListeners();
}
setupEventListeners() {
document.querySelector('.game-field').addEventListener('click', (e) => {
if (e.target.classList.contains('mole')) {
this.bonk(e);
}
});
this.startButton.addEventListener('click', () => this.startGame());
}
startGame() {
this.score = 0;
this.timeUp = false;
this.scoreBoard.textContent = 0;
this.startButton.disabled = true;
// 重置所有地鼠状态
this.moles.forEach(mole => {
mole.classList.remove('up', 'hit');
});
const level = this.levelSelect.value;
this.peep(level);
setTimeout(() => {
this.timeUp = true;
this.startButton.disabled = false;
// 更新最高分
if (this.score > this.highScore) {
this.highScore = this.score;
this.highScoreBoard.textContent = this.highScore;
localStorage.setItem('moleHighScore', this.highScore);
}
}, 10000); // 10秒游戏时长
}
peep(level) {
if (this.timeUp) return;
const time = this.difficulty[level];
const hole = this.randomHole();
const mole = hole.querySelector('.mole');
mole.classList.add('up');
setTimeout(() => {
mole.classList.remove('up');
if (!this.timeUp) this.peep(level);
}, time.duration);
setTimeout(() => this.peep(level), time.delay);
}
randomHole() {
const idx = Math.floor(Math.random() * this.holes.length);
const hole = this.holes[idx];
// 避免连续在同一个洞出现
if (hole === this.lastHole) {
return this.randomHole();
}
this.lastHole = hole;
return hole;
}
bonk(e) {
if (!e.isTrusted || !e.target.classList.contains('up')) return;
// 播放音效
this.playSound(800, 0.1);
this.score++;
this.scoreBoard.textContent = this.score;
e.target.classList.add('hit');
setTimeout(() => {
e.target.classList.remove('hit', 'up');
}, 300);
}
playSound(frequency, duration) {
try {
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
const oscillator = audioContext.createOscillator();
const gainNode = audioContext.createGain();
oscillator.connect(gainNode);
gainNode.connect(audioContext.destination);
oscillator.type = 'square';
oscillator.frequency.value = frequency;
gainNode.gain.exponentialRampToValueAtTime(
0.00001, audioContext.currentTime + duration
);
oscillator.start();
oscillator.stop(audioContext.currentTime + duration);
} catch (e) {
console.log('音频播放失败:', e);
}
}
}
// 预加载资源
function preloadResources() {
// 这里可以添加实际图片资源的预加载
console.log('资源预加载完成');
}
// 初始化游戏
window.addEventListener('load', () => {
preloadResources();
const game = new WhackAMole();
});
</script>
</body>
</html>
这个项目经过多次迭代优化,已经包含了响应式设计、性能优化、状态持久化等进阶特性。建议初学者可以先实现基础功能,再逐步添加高级特性。在实际开发中,还可以考虑添加游戏音效、更多动画效果、成就系统等扩展功能。