Hi,我是前端人类学(之前叫布兰妮甜)!
贪吃蛇游戏自1976年诞生以来,已经从简单的像素游戏发展成为具有丰富功能的现代游戏体验。本文将通过一个功能增强版的贪吃蛇游戏
,探讨如何将经典游戏概念与现代Web技术相结合,创造出既保留经典玩法又具备现代特性的游戏体验。
一、技术架构与设计理念
这个增强版贪吃蛇游戏采用HTML5 Canvas作为渲染核心,结合现代CSS布局和JavaScript模块化设计,实现了以下核心功能:
- 基础游戏机制:蛇的移动、食物生成、碰撞检测
- 多人游戏模式:实时多玩家支持与竞争机制
- 成就系统:进度追踪与玩家激励
- 关卡编辑器:用户生成内容支持
- 资源管理系统:自定义皮肤与背景
二、多人在线模式的实现
多人在线功能是本项目的核心创新之一。通过模拟的网络通信机制,游戏支持2-4名玩家同时参与,并提供三种不同的游戏模式:
// 多人在线模式状态
let multiplayerState = {
players: [
{ id: 1, name: '玩家1', score: 0, color: '#4CAF50', alive: true, snake: [], dx: 1, dy: 0 },
{ id: 2, name: '玩家2', score: 0, color: '#FF5252', alive: true, snake: [], dx: 1, dy: 0 },
{ id: 3, name: '玩家3', score: 0, color: '#FFC107', alive: true, snake: [], dx: 1, dy: 0 },
{ id: 4, name: '玩家4', score: 0, color: '#9C27B0', alive: true, snake: [], dx: 1, dy: 0 }
],
mode: 'competition',
status: 'lobby',
food: []
}
这种设计允许玩家根据偏好选择不同的游戏体验,从合作共嬴到激烈竞争,大大扩展了游戏的可玩性。
三、成就系统的心理激励
成就系统通过提供明确的目标和奖励,有效增强了玩家的参与度和长期投入:
// 成就数据结构
const achievements = [
{
id: "first_blood",
name: "初出茅庐",
description: "获得100分",
icon: "fas fa-star",
progress: 0,
target: 100,
unlocked: false
},
{
id: "speed_demon",
name: "速度之王",
description: "以最高速度游戏1分钟",
icon: "fas fa-fire",
progress: 0,
target: 60,
unlocked: false
}
];
// 成就解锁检查
function checkAchievements() {
achievements.forEach(achievement => {
if (!achievement.unlocked && achievement.progress >= achievement.target) {
unlockAchievement(achievement.id);
}
});
}
这种成就系统不仅提供了短期目标,还通过进度可视化给予玩家持续的正向反馈,符合游戏化设计的基本原则。
四、关卡编辑器的创意表达
关卡编辑器功能将玩家从被动的消费者转变为主动的创作者,极大地扩展了游戏的内容生命周期:
canvas.addEventListener('click', e => {
if (!isEditing) return
const rect = canvas.getBoundingClientRect()
const x = e.clientX - rect.left
const y = e.clientY - rect.top
const gridX = Math.floor(x / gridSize)
const gridY = Math.floor(y / gridSize)
if (currentTool === 'wall') {
// 添加墙壁
obstacles.push({ x: gridX, y: gridY })
} else if (currentTool === 'obstacle') {
// 添加障碍物
obstacles.push({ x: gridX, y: gridY })
} else if (currentTool === 'erase') {
// 删除墙壁或障碍物
obstacles = obstacles.filter(obs => !(obs.x === gridX && obs.y === gridY))
}
draw()
})
通过简单的点击交互,玩家可以创建复杂多样的游戏关卡,分享给其他玩家,形成活跃的创作者社区。
五、响应式设计与跨平台体验
游戏采用完全响应式设计,确保在不同设备上都能提供一致的用户体验:
/* 响应式布局系统 */
.container {
width: 100%;
max-width: 1400px;
display: grid;
grid-template-columns: 1fr 2fr 1fr;
gap: 20px;
margin-top: 20px;
}
@media (max-width: 900px) {
.container {
grid-template-columns: 1fr;
}
.left-panel,
.right-panel {
display: none;
}
.mobile-controls {
display: grid;
}
}
/* 移动端控制优化 */
.mobile-controls {
grid-template-columns: repeat(3, 1fr);
grid-template-rows: repeat(2, 1fr);
}
.mobile-controls button {
height: 70px;
font-size: 1.5rem;
}
这种设计确保从桌面电脑到移动手机,玩家都能享受完整的游戏功能,包括多人游戏和关卡编辑等高级特性。
六、完整代码实现
**页面结构(snake-game.html) **
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<link rel="stylesheet" href="snake-game.css">
<title>高级贪吃蛇游戏</title>
</head>
<body>
<header>
<h1><i class="fas fa-snake"></i> 贪吃蛇游戏 - 多人在线版</h1>
<div class="tabs">
<div class="tab active" data-tab="single">单人游戏</div>
<div class="tab" data-tab="multiplayer">多人游戏</div>
<div class="tab" data-tab="achievements">成就系统</div>
<div class="tab" data-tab="level-editor">关卡编辑器</div>
</div>
<div class="game-info">
<div class="info-box">
<i class="fas fa-star"></i>
<span>得分: <span id="score">0</span></span>
</div>
<div class="info-box">
<i class="fas fa-tachometer-alt"></i>
<span>速度: <span id="speed">5</span></span>
</div>
<div class="info-box">
<i class="fas fa-clock"></i>
<span>时间: <span id="time">00:00</span></span>
</div>
</div>
<div class="multiplayer-status" id="multiplayerStatus">
<div class="player-indicator">
<div class="player-color player-1"></div>
<span>玩家1: <span id="player1Score">0</span></span>
</div>
<div class="player-indicator">
<div class="player-color player-2"></div>
<span>玩家2: <span id="player2Score">0</span></span>
</div>
<div class="player-indicator">
<div class="player-color player-3"></div>
<span>玩家3: <span id="player3Score">0</span></span>
</div>
<div class="player-indicator">
<div class="player-color player-4"></div>
<span>玩家4: <span id="player4Score">0</span></span>
</div>
</div>
</header>
<div class="container">
<div class="left-panel panel">
<h3 class="section-title"><i class="fas fa-cog"></i> 游戏设置</h3>
<div class="settings-group">
<label for="gameSpeed">游戏速度</label>
<input type="range" id="gameSpeed" min="1" max="10" value="5">
<div class="range-value"><span id="speedValue">5</span>/10</div>
</div>
<div class="settings-group">
<label for="gridSize">网格大小</label>
<select id="gridSize">
<option value="15">小 (15x15)</option>
<option value="20" selected>中 (20x20)</option>
<option value="25">大 (25x25)</option>
</select>
</div>
<div class="settings-group">
<label for="wallMode">墙壁模式</label>
<select id="wallMode">
<option value="solid">实心墙 (游戏结束)</option>
<option value="pass-through">穿透 (循环边界)</option>
</select>
</div>
<h3 class="section-title"><i class="fas fa-upload"></i> 上传资源</h3>
<div class="upload-area" id="uploadBg">
<i class="fas fa-image"></i>
<p>上传背景</p>
<input type="file" id="bgUpload" accept="image/*" style="display: none;">
</div>
<div class="upload-area" id="uploadSkin">
<i class="fas fa-palette"></i>
<p>上传蛇皮肤</p>
<input type="file" id="skinUpload" accept="image/*" style="display: none;">
</div>
<button class="secondary" id="resetSettings">
<i class="fas fa-undo"></i> 重置设置
</button>
</div>
<div class="game-area panel">
<canvas id="gameCanvas" width="600" height="600"></canvas>
<div class="level-editor" id="levelEditor">
<h3><i class="fas fa-edit"></i> 关卡编辑器</h3>
<div class="editor-tools">
<button id="wallTool"><i class="fas fa-wall"></i> 墙壁</button>
<button id="obstacleTool"><i class="fas fa-mountain"></i> 障碍物</button>
<button id="eraseTool"><i class="fas fa-eraser"></i> 擦除</button>
</div>
<div>
<input type="text" id="levelName" placeholder="关卡名称">
<button id="saveLevel"><i class="fas fa-save"></i> 保存关卡</button>
<button id="loadLevel"><i class="fas fa-folder-open"></i> 加载关卡</button>
</div>
</div>
<div class="game-over" id="gameOver">
<h2>游戏结束!</h2>
<p>得分: <span id="finalScore">0</span></p>
<div class="controls">
<button id="restartBtn">
<i class="fas fa-redo"></i> 重新开始
</button>
<button class="secondary" id="menuBtn">
<i class="fas fa-home"></i> 返回菜单
</button>
</div>
</div>
</div>
<div class="right-panel panel">
<div class="tab-content active" id="singleTab">
<h3 class="section-title"><i class="fas fa-trophy"></i> 排行榜</h3>
<ul class="leaderboard" id="leaderboard">
<li>
<span><span class="rank">1</span> 玩家1</span>
<span class="score">450</span>
</li>
<li>
<span><span class="rank">2</span> 玩家2</span>
<span class="score">320</span>
</li>
<li>
<span><span class="rank">3</span> 玩家3</span>
<span class="score">280</span>
</li>
</ul>
<h3 class="section-title"><i class="fas fa-gamepad"></i> 游戏控制</h3>
<div class="controls">
<button id="startBtn">
<i class="fas fa-play"></i> 开始游戏
</button>
<button class="secondary" id="pauseBtn">
<i class="fas fa-pause"></i> 暂停
</button>
<button class="accent" id="soundBtn">
<i class="fas fa-volume-up"></i> 音效: 开
</button>
<button id="saveBtn">
<i class="fas fa-save"></i> 保存设置
</button>
</div>
<p style="margin-top: 15px; text-align: center;">
使用 <i class="fas fa-arrow-up"></i> <i class="fas fa-arrow-down"></i>
<i class="fas fa-arrow-left"></i> <i class="fas fa-arrow-right"></i> 方向键控制
</p>
</div>
<div class="tab-content" id="multiplayerTab">
<h3 class="section-title"><i class="fas fa-users"></i> 多人游戏</h3>
<div class="settings-group">
<label for="playerCount">玩家数量</label>
<select id="playerCount">
<option value="2">2 玩家</option>
<option value="3">3 玩家</option>
<option value="4">4 玩家</option>
</select>
</div>
<div class="settings-group">
<label for="gameMode">游戏模式</label>
<select id="gameMode">
<option value="coop">合作模式</option>
<option value="competition">竞争模式</option>
<option value="last-standing">生存模式</option>
</select>
</div>
<div class="player-controls">
<div class="player-control">
<div class="online-indicator"></div>
<span>玩家1</span>
<select>
<option value="human">人类玩家</option>
<option value="easy">简单AI</option>
<option value="medium">中等AI</option>
<option value="hard">困难AI</option>
</select>
</div>
<div class="player-control">
<div class="online-indicator"></div>
<span>玩家2</span>
<select>
<option value="human">人类玩家</option>
<option value="easy">简单AI</option>
<option value="medium">中等AI</option>
<option value="hard">困难AI</option>
</select>
</div>
<div class="player-control">
<div class="online-indicator offline"></div>
<span>玩家3</span>
<select>
<option value="none">无玩家</option>
<option value="human">人类玩家</option>
<option value="easy">简单AI</option>
<option value="medium">中等AI</option>
<option value="hard">困难AI</option>
</select>
</div>
<div class="player-control">
<div class="online-indicator offline"></div>
<span>玩家4</span>
<select>
<option value="none">无玩家</option>
<option value="human">人类玩家</option>
<option value="easy">简单AI</option>
<option value="medium">中等AI</option>
<option value="hard">困难AI</option>
</select>
</div>
</div>
<button id="createLobby">
<i class="fas fa-plus"></i> 创建房间
</button>
<button id="joinLobby">
<i class="fas fa-sign-in-alt"></i> 加入房间
</button>
<div id="lobbyList" style="margin-top: 15px;">
<h4>可用房间</h4>
<ul style="list-style: none;">
<li>房间1 (2/4玩家)</li>
<li>房间2 (1/2玩家)</li>
</ul>
</div>
</div>
<div class="tab-content" id="achievementsTab">
<h3 class="section-title"><i class="fas fa-trophy"></i> 成就系统</h3>
<div class="achievements">
<div class="achievement unlocked">
<i class="fas fa-star"></i>
<h4>初出茅庐</h4>
<p>获得100分</p>
<div class="achievement-progress">
<div class="achievement-progress-bar" style="width: 100%"></div>
</div>
</div>
<div class="achievement">
<i class="fas fa-fire"></i>
<h4>速度之王</h4>
<p>以最高速度游戏1分钟</p>
<div class="achievement-progress">
<div class="achievement-progress-bar" style="width: 30%"></div>
</div>
</div>
<div class="achievement unlocked">
<i class="fas fa-users"></i>
<h4>团队合作</h4>
<p>完成一局合作模式</p>
<div class="achievement-progress">
<div class="achievement-progress-bar" style="width: 100%"></div>
</div>
</div>
<div class="achievement">
<i class="fas fa-ghost"></i>
<h4>幽灵模式</h4>
<p>在穿透模式下获得500分</p>
<div class="achievement-progress">
<div class="achievement-progress-bar" style="width: 65%"></div>
</div>
</div>
<div class="achievement">
<i class="fas fa-infinity"></i>
<h4>无限挑战</h4>
<p>蛇身长度达到50节</p>
<div class="achievement-progress">
<div class="achievement-progress-bar" style="width: 40%"></div>
</div>
</div>
<div class="achievement">
<i class="fas fa-crown"></i>
<h4>贪吃蛇大师</h4>
<p>解锁所有成就</p>
<div class="achievement-progress">
<div class="achievement-progress-bar" style="width: 25%"></div>
</div>
</div>
</div>
</div>
<div class="tab-content" id="levelEditorTab">
<h3 class="section-title"><i class="fas fa-edit"></i> 我的关卡</h3>
<div style="margin-bottom: 15px;">
<input type="text" placeholder="搜索关卡..." style="width: 100%;">
</div>
<ul class="leaderboard">
<li>
<span>迷宫挑战</span>
<span><i class="fas fa-play"></i> 游玩</span>
</li>
<li>
<span>极限模式</span>
<span><i class="fas fa-play"></i> 游玩</span>
</li>
<li>
<span>合作关卡</span>
<span><i class="fas fa-play"></i> 游玩</span>
</li>
</ul>
<button style="margin-top: 15px;">
<i class="fas fa-share"></i> 分享关卡
</button>
</div>
</div>
</div>
<div class="mobile-controls">
<button id="upBtn"><i class="fas fa-arrow-up"></i></button>
<button id="leftBtn"><i class="fas fa-arrow-left"></i></button>
<button id="downBtn"><i class="fas fa-arrow-down"></i></button>
<button id="rightBtn"><i class="fas fa-arrow-right"></i></button>
</div>
<script src="snake-game.js"></script>
</body>
</html>
页面样式(snake-game.css)
:root {
--primary-color: #4CAF50;
--secondary-color: #2196F3;
--accent-color: #FF5722;
--dark-color: #2c3e50;
--light-color: #ecf0f1;
--success-color: #2ecc71;
--danger-color: #e74c3c;
--warning-color: #f39c12;
--multiplayer-color-1: #FF5252;
--multiplayer-color-2: #FFC107;
--multiplayer-color-3: #9C27B0;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
body {
background: linear-gradient(135deg, var(--dark-color), #34495e);
color: var(--light-color);
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
padding: 20px;
}
.container {
width: 100%;
max-width: 1400px;
display: grid;
grid-template-columns: 1fr 2fr 1fr;
gap: 20px;
margin-top: 20px;
}
header {
text-align: center;
margin-bottom: 20px;
width: 100%;
}
h1 {
font-size: 2.5rem;
margin-bottom: 10px;
color: var(--primary-color);
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
}
.tabs {
display: flex;
justify-content: center;
gap: 10px;
margin-bottom: 20px;
}
.tab {
padding: 10px 20px;
background: rgba(0, 0, 0, 0.3);
border-radius: 8px;
cursor: pointer;
transition: all 0.3s;
}
.tab.active {
background: var(--primary-color);
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
.game-info {
display: flex;
justify-content: center;
gap: 20px;
margin-bottom: 15px;
}
.info-box {
background: rgba(0, 0, 0, 0.3);
padding: 10px 20px;
border-radius: 8px;
display: flex;
align-items: center;
gap: 10px;
}
.info-box i {
font-size: 1.5rem;
color: var(--primary-color);
}
.panel {
background: rgba(0, 0, 0, 0.5);
border-radius: 12px;
padding: 20px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
backdrop-filter: blur(10px);
max-height: 80vh;
overflow-y: auto;
}
.left-panel,
.right-panel {
display: flex;
flex-direction: column;
gap: 20px;
}
.game-area {
position: relative;
}
canvas {
background-color: #1a1a2e;
border-radius: 12px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
display: block;
width: 100%;
}
.controls {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
}
button {
background: var(--primary-color);
color: white;
border: none;
padding: 12px;
border-radius: 8px;
cursor: pointer;
font-weight: bold;
transition: all 0.3s;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
button:hover {
background: #3e8e41;
transform: translateY(-2px);
}
button.secondary {
background: var(--secondary-color);
}
button.secondary:hover {
background: #0b7dda;
}
button.accent {
background: var(--accent-color);
}
button.accent:hover {
background: #d84315;
}
.section-title {
font-size: 1.2rem;
margin-bottom: 15px;
padding-bottom: 8px;
border-bottom: 2px solid var(--primary-color);
color: var(--primary-color);
}
.settings-group {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: 500;
}
input[type="range"] {
width: 100%;
margin: 10px 0;
}
.range-value {
text-align: center;
font-weight: bold;
color: var(--primary-color);
}
select,
input[type="text"] {
width: 100%;
padding: 10px;
border-radius: 6px;
background: rgba(255, 255, 255, 0.1);
color: white;
border: 1px solid rgba(255, 255, 255, 0.2);
margin-bottom: 10px;
}
.upload-area {
border: 2px dashed rgba(255, 255, 255, 0.3);
border-radius: 8px;
padding: 20px;
text-align: center;
cursor: pointer;
transition: all 0.3s;
margin-top: 10px;
}
.upload-area:hover {
border-color: var(--primary-color);
background: rgba(76, 175, 80, 0.1);
}
.upload-area i {
font-size: 2rem;
margin-bottom: 10px;
color: var(--primary-color);
}
.achievements {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
}
.achievement {
background: rgba(255, 255, 255, 0.1);
border-radius: 8px;
padding: 10px;
text-align: center;
position: relative;
}
.achievement.locked {
opacity: 0.6;
}
.achievement i {
font-size: 2rem;
margin-bottom: 5px;
color: var(--warning-color);
}
.achievement.unlocked i {
color: gold;
}
.achievement-progress {
height: 5px;
background: rgba(255, 255, 255, 0.2);
border-radius: 3px;
margin-top: 5px;
overflow: hidden;
}
.achievement-progress-bar {
height: 100%;
background: var(--primary-color);
border-radius: 3px;
}
.leaderboard {
list-style: none;
}
.leaderboard li {
padding: 12px;
background: rgba(255, 255, 255, 0.1);
margin-bottom: 8px;
border-radius: 6px;
display: flex;
justify-content: space-between;
}
.leaderboard .rank {
font-weight: bold;
color: var(--primary-color);
margin-right: 10px;
}
.leaderboard .score {
font-weight: bold;
color: var(--accent-color);
}
.game-over {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
border-radius: 12px;
opacity: 0;
pointer-events: none;
transition: opacity 0.5s;
}
.game-over.show {
opacity: 1;
pointer-events: all;
}
.game-over h2 {
font-size: 3rem;
color: var(--danger-color);
margin-bottom: 20px;
}
.game-over p {
font-size: 1.5rem;
margin-bottom: 30px;
}
.multiplayer-status {
display: flex;
gap: 10px;
margin-bottom: 15px;
}
.player-indicator {
display: flex;
align-items: center;
gap: 5px;
}
.player-color {
width: 15px;
height: 15px;
border-radius: 50%;
}
.player-1 {
background: var(--primary-color);
}
.player-2 {
background: var(--multiplayer-color-1);
}
.player-3 {
background: var(--multiplayer-color-2);
}
.player-4 {
background: var(--multiplayer-color-3);
}
.level-editor {
display: none;
position: absolute;
top: 10px;
right: 10px;
background: rgba(0, 0, 0, 0.7);
padding: 10px;
border-radius: 8px;
z-index: 10;
}
.editor-tools {
display: flex;
gap: 5px;
margin-bottom: 10px;
}
.editor-tools button {
padding: 8px;
font-size: 0.9rem;
}
.mobile-controls {
display: none;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: repeat(2, 1fr);
gap: 10px;
margin-top: 20px;
}
.mobile-controls button {
height: 70px;
font-size: 1.5rem;
}
.mobile-controls button:nth-child(1) {
grid-column: 2;
grid-row: 1;
}
.mobile-controls button:nth-child(2) {
grid-column: 1;
grid-row: 2;
}
.mobile-controls button:nth-child(3) {
grid-column: 2;
grid-row: 2;
}
.mobile-controls button:nth-child(4) {
grid-column: 3;
grid-row: 2;
}
/* 多人在线模式特定样式 */
.player-controls {
display: flex;
flex-direction: column;
gap: 10px;
margin-top: 15px;
}
.player-control {
display: flex;
align-items: center;
gap: 10px;
padding: 8px;
background: rgba(255, 255, 255, 0.1);
border-radius: 6px;
}
.player-control select {
flex: 1;
margin: 0;
}
.online-indicator {
width: 10px;
height: 10px;
border-radius: 50%;
background: var(--success-color);
}
.online-indicator.offline {
background: var(--danger-color);
}
@media (max-width: 900px) {
.container {
grid-template-columns: 1fr;
}
.left-panel,
.right-panel {
display: none;
}
.mobile-controls {
display: grid;
}
}
业务逻辑(snake-game.js)
// 游戏主要变量
const canvas = document.getElementById('gameCanvas')
const ctx = canvas.getContext('2d')
const scoreElement = document.getElementById('score')
const speedElement = document.getElementById('speed')
const timeElement = document.getElementById('time')
const gameOverElement = document.getElementById('gameOver')
const finalScoreElement = document.getElementById('finalScore')
let gridSize = 20
let tileCount = canvas.width / gridSize
let snake = []
let food = {}
let dx = 0
let dy = 0
let score = 0
let gameSpeed = 150
let gameInterval
let isPaused = false
let isGameOver = false
let gameTime = 0
let timeInterval
let isSoundOn = true
let isMultiplayer = false
let obstacles = []
let currentTool = 'wall'
let isEditing = false
// 多人在线模式状态
let multiplayerState = {
players: [
{ id: 1, name: '玩家1', score: 0, color: '#4CAF50', alive: true, snake: [], dx: 1, dy: 0 },
{ id: 2, name: '玩家2', score: 0, color: '#FF5252', alive: true, snake: [], dx: 1, dy: 0 },
{ id: 3, name: '玩家3', score: 0, color: '#FFC107', alive: true, snake: [], dx: 1, dy: 0 },
{ id: 4, name: '玩家4', score: 0, color: '#9C27B0', alive: true, snake: [], dx: 1, dy: 0 }
],
mode: 'competition',
status: 'lobby',
food: []
}
// 初始化游戏
function initGame() {
// 初始化蛇
snake = [
{ x: 10, y: 10 },
{ x: 9, y: 10 },
{ x: 8, y: 10 }
]
// 生成食物
generateFood()
// 生成障碍物
generateObstacles()
// 重置游戏状态
score = 0
dx = 1
dy = 0
gameTime = 0
isGameOver = false
// 更新UI
scoreElement.textContent = score
gameOverElement.classList.remove('show')
// 绘制初始状态
draw()
// 启动计时器
startTimer()
}
// 初始化多人游戏
function initMultiplayerGame() {
const playerCount = parseInt(document.getElementById('playerCount').value)
const gameMode = document.getElementById('gameMode').value
// 设置玩家数量
multiplayerState.players = multiplayerState.players.slice(0, playerCount)
// 初始化每个玩家的蛇
multiplayerState.players.forEach((player, index) => {
const startX = 5 + index * 5
player.snake = [
{ x: startX, y: 10 },
{ x: startX - 1, y: 10 },
{ x: startX - 2, y: 10 }
]
player.score = 0
player.alive = true
// 设置初始方向
player.dx = 1
player.dy = 0
})
// 生成多个食物
generateMultiplayerFood()
// 更新玩家状态显示
updateMultiplayerStatus()
// 设置游戏模式
multiplayerState.mode = gameMode
multiplayerState.status = 'playing'
// 开始游戏循环
if (gameInterval) clearInterval(gameInterval)
gameInterval = setInterval(multiplayerGameLoop, gameSpeed)
}
// 多人游戏循环
function multiplayerGameLoop() {
if (!isPaused && multiplayerState.status === 'playing') {
moveMultiplayerSnakes()
drawMultiplayerGame()
checkMultiplayerGameOver()
}
}
// 移动所有玩家的蛇
function moveMultiplayerSnakes() {
multiplayerState.players.forEach(player => {
if (player.alive) {
const head = {
x: player.snake[0].x + player.dx,
y: player.snake[0].y + player.dy
}
// 墙壁碰撞检测
const wallMode = document.getElementById('wallMode').value
if (wallMode === 'solid') {
if (head.x < 0 || head.y < 0 || head.x >= tileCount || head.y >= tileCount) {
player.alive = false
return
}
} else {
if (head.x < 0) head.x = tileCount - 1
if (head.y < 0) head.y = tileCount - 1
if (head.x >= tileCount) head.x = 0
if (head.y >= tileCount) head.y = 0
}
// 添加到蛇头
player.snake.unshift(head)
// 检查是否吃到食物
let ateFood = false
for (let i = 0; i < multiplayerState.food.length; i++) {
const food = multiplayerState.food[i]
if (head.x === food.x && head.y === food.y) {
player.score += 10
multiplayerState.food.splice(i, 1)
generateMultiplayerFood() // 生成新食物
ateFood = true
break
}
}
// 如果没吃到食物,移除蛇尾
if (!ateFood) {
player.snake.pop()
}
// 检查碰撞
checkMultiplayerCollisions(player)
}
})
}
// 检查多人游戏碰撞
function checkMultiplayerCollisions(player) {
const head = player.snake[0]
// 检查是否撞到自己
for (let i = 1; i < player.snake.length; i++) {
if (head.x === player.snake[i].x && head.y === player.snake[i].y) {
player.alive = false
return
}
}
// 检查是否撞到障碍物
for (let obs of obstacles) {
if (head.x === obs.x && head.y === obs.y) {
player.alive = false
return
}
}
// 检查是否撞到其他玩家
multiplayerState.players.forEach(otherPlayer => {
if (otherPlayer.id !== player.id && otherPlayer.alive) {
for (let part of otherPlayer.snake) {
if (head.x === part.x && head.y === part.y) {
player.alive = false
return
}
}
}
})
}
// 绘制多人游戏
function drawMultiplayerGame() {
// 清空画布
ctx.fillStyle = '#1a1a2e'
ctx.fillRect(0, 0, canvas.width, canvas.height)
// 绘制网格
drawGrid()
// 绘制障碍物
drawObstacles()
// 绘制所有玩家的蛇
multiplayerState.players.forEach(player => {
if (player.alive) {
for (let i = 0; i < player.snake.length; i++) {
const part = player.snake[i]
if (i === 0) {
// 蛇头
ctx.fillStyle = player.color
} else {
// 蛇身
ctx.fillStyle = shadeColor(player.color, -20)
}
ctx.fillRect(part.x * gridSize, part.y * gridSize, gridSize - 1, gridSize - 1)
// 为蛇身添加圆角效果
ctx.beginPath()
ctx.arc(part.x * gridSize + gridSize / 2, part.y * gridSize + gridSize / 2, gridSize / 2 - 1, 0, Math.PI * 2)
ctx.fill()
}
}
})
// 绘制食物
multiplayerState.food.forEach(food => {
ctx.fillStyle = '#FF5252'
ctx.beginPath()
ctx.arc(food.x * gridSize + gridSize / 2, food.y * gridSize + gridSize / 2, gridSize / 2 - 1, 0, Math.PI * 2)
ctx.fill()
})
// 更新玩家分数显示
updateMultiplayerStatus()
}
// 生成多人游戏食物
function generateMultiplayerFood() {
multiplayerState.food = []
const foodCount = multiplayerState.players.length * 2
for (let i = 0; i < foodCount; i++) {
const food = {
x: Math.floor(Math.random() * tileCount),
y: Math.floor(Math.random() * tileCount)
}
// 确保食物不会出现在蛇身上或障碍物上
let validPosition = true
multiplayerState.players.forEach(player => {
for (let part of player.snake) {
if (part.x === food.x && part.y === food.y) {
validPosition = false
}
}
})
for (let obs of obstacles) {
if (obs.x === food.x && obs.y === food.y) {
validPosition = false
}
}
if (validPosition) {
multiplayerState.food.push(food)
} else {
i-- // 重试
}
}
}
// 检查多人游戏是否结束
function checkMultiplayerGameOver() {
let alivePlayers = 0
multiplayerState.players.forEach(player => {
if (player.alive) alivePlayers++
})
if (alivePlayers <= 1) {
multiplayerState.status = 'finished'
// 显示游戏结束画面
finalScoreElement.textContent = `玩家${multiplayerState.players.findIndex(p => p.alive) + 1}获胜!`
gameOverElement.classList.add('show')
// 播放游戏结束音效
if (isSoundOn) {
playSound('gameover')
}
}
}
// 更新多人游戏状态显示
function updateMultiplayerStatus() {
multiplayerState.players.forEach((player, index) => {
document.getElementById(`player${index + 1}Score`).textContent = player.score
})
}
// 工具函数:调整颜色亮度
function shadeColor(color, percent) {
let R = parseInt(color.substring(1, 3), 16)
let G = parseInt(color.substring(3, 5), 16)
let B = parseInt(color.substring(5, 7), 16)
R = parseInt((R * (100 + percent)) / 100)
G = parseInt((G * (100 + percent)) / 100)
B = parseInt((B * (100 + percent)) / 100)
R = R < 255 ? R : 255
G = G < 255 ? G : 255
B = B < 255 ? B : 255
R = R < 0 ? 0 : R
G = G < 0 ? 0 : G
B = B < 0 ? 0 : B
const RR = R.toString(16).length === 1 ? '0' + R.toString(16) : R.toString(16)
const GG = G.toString(16).length === 1 ? '0' + G.toString(16) : G.toString(16)
const BB = B.toString(16).length === 1 ? '0' + B.toString(16) : B.toString(16)
return '#' + RR + GG + BB
}
// 生成食物
function generateFood() {
food = {
x: Math.floor(Math.random() * tileCount),
y: Math.floor(Math.random() * tileCount)
}
// 确保食物不会出现在蛇身上或障碍物上
for (let part of snake) {
if (part.x === food.x && part.y === food.y) {
generateFood()
return
}
}
for (let obs of obstacles) {
if (obs.x === food.x && obs.y === food.y) {
generateFood()
return
}
}
}
// 生成障碍物
function generateObstacles() {
obstacles = []
const obstacleCount = Math.floor(tileCount * 0.1) // 10%的格子作为障碍物
for (let i = 0; i < obstacleCount; i++) {
let obstacle = {
x: Math.floor(Math.random() * tileCount),
y: Math.floor(Math.random() * tileCount)
}
// 确保障碍物不会出现在蛇的初始位置或食物上
let validPosition = true
for (let part of snake) {
if (part.x === obstacle.x && part.y === obstacle.y) {
validPosition = false
break
}
}
if (obstacle.x === food.x && obstacle.y === food.y) {
validPosition = false
}
if (validPosition) {
obstacles.push(obstacle)
} else {
i-- // 重试
}
}
}
// 绘制游戏
function draw() {
// 清空画布
ctx.fillStyle = '#1a1a2e'
ctx.fillRect(0, 0, canvas.width, canvas.height)
// 绘制网格
drawGrid()
// 绘制障碍物
drawObstacles()
// 绘制蛇
for (let i = 0; i < snake.length; i++) {
const part = snake[i]
// 蛇头用不同颜色
if (i === 0) {
ctx.fillStyle = '#4CAF50'
} else {
ctx.fillStyle = '#8BC34A'
}
ctx.fillRect(part.x * gridSize, part.y * gridSize, gridSize - 1, gridSize - 1)
// 为蛇身添加圆角效果
ctx.beginPath()
ctx.arc(part.x * gridSize + gridSize / 2, part.y * gridSize + gridSize / 2, gridSize / 2 - 1, 0, Math.PI * 2)
ctx.fill()
}
// 绘制食物
ctx.fillStyle = '#FF5252'
ctx.beginPath()
ctx.arc(food.x * gridSize + gridSize / 2, food.y * gridSize + gridSize / 2, gridSize / 2 - 1, 0, Math.PI * 2)
ctx.fill()
}
// 绘制网格
function drawGrid() {
ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)'
ctx.lineWidth = 0.5
for (let i = 0; i < tileCount; i++) {
// 垂直线
ctx.beginPath()
ctx.moveTo(i * gridSize, 0)
ctx.lineTo(i * gridSize, canvas.height)
ctx.stroke()
// 水平线
ctx.beginPath()
ctx.moveTo(0, i * gridSize)
ctx.lineTo(canvas.width, i * gridSize)
ctx.stroke()
}
}
// 绘制障碍物
function drawObstacles() {
ctx.fillStyle = '#607D8B'
for (let obs of obstacles) {
ctx.fillRect(obs.x * gridSize, obs.y * gridSize, gridSize - 1, gridSize - 1)
// 添加纹理效果
ctx.fillStyle = '#455A64'
ctx.fillRect(obs.x * gridSize + 2, obs.y * gridSize + 2, gridSize - 5, gridSize - 5)
ctx.fillStyle = '#607D8B'
}
}
// 移动蛇
function moveSnake() {
// 计算新的头部位置
const head = { x: snake[0].x + dx, y: snake[0].y + dy }
// 检查墙壁模式
const wallMode = document.getElementById('wallMode').value
if (wallMode === 'solid') {
// 实心墙模式 - 检查是否撞墙
if (head.x < 0 || head.y < 0 || head.x >= tileCount || head.y >= tileCount) {
gameOver()
return
}
} else {
// 穿透模式 - 从对面出现
if (head.x < 0) head.x = tileCount - 1
if (head.y < 0) head.y = tileCount - 1
if (head.x >= tileCount) head.x = 0
if (head.y >= tileCount) head.y = 0
}
// 检查是否撞到自己
if (isSnakeCollision(head)) {
gameOver()
return
}
// 检查是否撞到障碍物
for (let obs of obstacles) {
if (head.x === obs.x && head.y === obs.y) {
gameOver()
return
}
}
// 将新头部添加到蛇的起始位置
snake.unshift(head)
// 检查是否吃到食物
if (head.x === food.x && head.y === food.y) {
// 增加分数
score += 10
scoreElement.textContent = score
// 检查成就
checkAchievements()
// 生成新食物
generateFood()
// 播放吃食物音效
if (isSoundOn) {
playSound('eat')
}
} else {
// 如果没吃到食物,移除尾部
snake.pop()
}
}
// 检查蛇是否撞到自己
function isSnakeCollision(head) {
for (let i = 1; i < snake.length; i++) {
if (head.x === snake[i].x && head.y === snake[i].y) {
return true
}
}
return false
}
// 检查成就
function checkAchievements() {
// 这里简化处理,实际应用中会有更复杂的成就系统
if (score >= 100) {
unlockAchievement('初出茅庐')
}
if (snake.length >= 50) {
unlockAchievement('无限挑战')
}
}
// 解锁成就
function unlockAchievement(name) {
console.log(`成就已解锁: ${name}`)
// 在实际应用中,这里会更新UI并播放成就解锁动画
}
// 游戏循环
function gameLoop() {
if (!isPaused && !isGameOver) {
moveSnake()
draw()
}
}
// 开始游戏
function startGame() {
if (isGameOver) {
initGame()
}
if (!gameInterval) {
isPaused = false
isGameOver = false
gameInterval = setInterval(gameLoop, gameSpeed)
document.getElementById('startBtn').innerHTML = '<i class="fas fa-play"></i> 重新开始'
} else if (isPaused) {
isPaused = false
document.getElementById('pauseBtn').innerHTML = '<i class="fas fa-pause"></i> 暂停'
}
}
// 暂停游戏
function pauseGame() {
if (!isGameOver && gameInterval) {
isPaused = !isPaused
document.getElementById('pauseBtn').innerHTML = isPaused ? '<i class="fas fa-play"></i> 继续' : '<i class="fas fa-pause"></i> 暂停'
}
}
// 游戏结束
function gameOver() {
isGameOver = true
clearInterval(gameInterval)
clearInterval(timeInterval)
gameInterval = null
finalScoreElement.textContent = score
gameOverElement.classList.add('show')
// 播放游戏结束音效
if (isSoundOn) {
playSound('gameover')
}
// 更新排行榜
updateLeaderboard('玩家', score)
}
// 播放音效
function playSound(type) {
// 在实际应用中,这里会播放音频文件
console.log(`Playing sound: ${type}`)
}
// 更新排行榜
function updateLeaderboard(name, score) {
// 这里简化处理,实际应用中会保存到本地存储或服务器
const leaderboard = document.getElementById('leaderboard')
console.log(`Updating leaderboard with ${name}: ${score}`)
}
// 开始计时器
function startTimer() {
clearInterval(timeInterval)
gameTime = 0
updateTimer()
timeInterval = setInterval(() => {
if (!isPaused && !isGameOver) {
gameTime++
updateTimer()
}
}, 1000)
}
// 更新计时器显示
function updateTimer() {
const minutes = Math.floor(gameTime / 60)
const seconds = gameTime % 60
timeElement.textContent = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`
}
// 切换音效
function toggleSound() {
isSoundOn = !isSoundOn
document.getElementById('soundBtn').innerHTML = isSoundOn ? '<i class="fas fa-volume-up"></i> 音效: 开' : '<i class="fas fa-volume-mute"></i> 音效: 关'
}
// 保存设置
function saveSettings() {
const settings = {
speed: document.getElementById('gameSpeed').value,
gridSize: document.getElementById('gridSize').value,
wallMode: document.getElementById('wallMode').value,
sound: isSoundOn
}
localStorage.setItem('snakeGameSettings', JSON.stringify(settings))
alert('设置已保存!')
}
// 加载设置
function loadSettings() {
const savedSettings = localStorage.getItem('snakeGameSettings')
if (savedSettings) {
const settings = JSON.parse(savedSettings)
document.getElementById('gameSpeed').value = settings.speed
document.getElementById('speedValue').textContent = settings.speed
speedElement.textContent = settings.speed
document.getElementById('gridSize').value = settings.gridSize
document.getElementById('wallMode').value = settings.wallMode
isSoundOn = settings.sound
// 更新音效按钮状态
document.getElementById('soundBtn').innerHTML = isSoundOn ? '<i class="fas fa-volume-up"></i> 音效: 开' : '<i class="fas fa-volume-mute"></i> 音效: 关'
// 更新游戏速度
gameSpeed = 210 - settings.speed * 10
}
}
// 重置设置
function resetSettings() {
localStorage.removeItem('snakeGameSettings')
document.getElementById('gameSpeed').value = 5
document.getElementById('speedValue').textContent = 5
speedElement.textContent = 5
document.getElementById('gridSize').value = 20
document.getElementById('wallMode').value = 'solid'
isSoundOn = true
document.getElementById('soundBtn').innerHTML = '<i class="fas fa-volume-up"></i> 音效: 开'
gameSpeed = 160
alert('设置已重置为默认值!')
}
// 切换标签页
function switchTab(tabName) {
// 隐藏所有标签页
document.querySelectorAll('.tab-content').forEach(tab => {
tab.classList.remove('active')
})
// 取消所有标签的激活状态
document.querySelectorAll('.tab').forEach(tab => {
tab.classList.remove('active')
})
// 激活选中的标签页
document.getElementById(`${tabName}Tab`).classList.add('active')
// 激活选中的标签
document.querySelector(`.tab[data-tab="${tabName}"]`).classList.add('active')
// 根据标签页显示/隐藏编辑器
if (tabName === 'level-editor') {
document.getElementById('levelEditor').style.display = 'block'
isEditing = true
} else {
document.getElementById('levelEditor').style.display = 'none'
isEditing = false
}
// 根据标签页显示/隐藏多人游戏状态
if (tabName === 'multiplayer') {
document.getElementById('multiplayerStatus').style.display = 'flex'
isMultiplayer = true
} else {
document.getElementById('multiplayerStatus').style.display = 'none'
isMultiplayer = false
}
}
// 初始化事件监听
function initEvents() {
// 键盘控制
document.addEventListener('keydown', e => {
// 防止按键滚动页面
if (e.key.startsWith('Arrow')) {
e.preventDefault()
}
// 根据按键改变方向(防止180度转向)
switch (e.key) {
case 'ArrowUp':
if (dy === 0) {
dx = 0
dy = -1
}
break
case 'ArrowDown':
if (dy === 0) {
dx = 0
dy = 1
}
break
case 'ArrowLeft':
if (dx === 0) {
dx = -1
dy = 0
}
break
case 'ArrowRight':
if (dx === 0) {
dx = 1
dy = 0
}
break
case ' ':
pauseGame()
break
}
})
// 标签切换事件
document.querySelectorAll('.tab').forEach(tab => {
tab.addEventListener('click', () => {
switchTab(tab.dataset.tab)
})
})
// 按钮事件
document.getElementById('startBtn').addEventListener('click', startGame)
document.getElementById('pauseBtn').addEventListener('click', pauseGame)
document.getElementById('restartBtn').addEventListener('click', startGame)
document.getElementById('soundBtn').addEventListener('click', toggleSound)
document.getElementById('saveBtn').addEventListener('click', saveSettings)
document.getElementById('resetSettings').addEventListener('click', resetSettings)
document.getElementById('createLobby').addEventListener('click', initMultiplayerGame)
// 移动端控制按钮
document.getElementById('upBtn').addEventListener('click', () => {
if (dy === 0) {
dx = 0
dy = -1
}
})
document.getElementById('downBtn').addEventListener('click', () => {
if (dy === 0) {
dx = 0
dy = 1
}
})
document.getElementById('leftBtn').addEventListener('click', () => {
if (dx === 0) {
dx = -1
dy = 0
}
})
document.getElementById('rightBtn').addEventListener('click', () => {
if (dx === 0) {
dx = 1
dy = 0
}
})
// 速度滑块事件
document.getElementById('gameSpeed').addEventListener('input', e => {
const speedValue = e.target.value
document.getElementById('speedValue').textContent = speedValue
speedElement.textContent = speedValue
gameSpeed = 210 - speedValue * 10
if (gameInterval) {
clearInterval(gameInterval)
gameInterval = setInterval(gameLoop, gameSpeed)
}
})
// 网格大小改变事件
document.getElementById('gridSize').addEventListener('change', e => {
gridSize = parseInt(e.target.value)
tileCount = canvas.width / gridSize
initGame()
})
// 上传功能
document.getElementById('uploadBg').addEventListener('click', () => {
document.getElementById('bgUpload').click()
})
document.getElementById('uploadSkin').addEventListener('click', () => {
document.getElementById('skinUpload').click()
})
document.getElementById('bgUpload').addEventListener('change', handleBackgroundUpload)
document.getElementById('skinUpload').addEventListener('change', handleSkinUpload)
// 编辑器工具选择
document.getElementById('wallTool').addEventListener('click', () => {
currentTool = 'wall'
})
document.getElementById('obstacleTool').addEventListener('click', () => {
currentTool = 'obstacle'
})
document.getElementById('eraseTool').addEventListener('click', () => {
currentTool = 'erase'
})
// 画布点击事件(用于关卡编辑器)
canvas.addEventListener('click', e => {
if (!isEditing) return
const rect = canvas.getBoundingClientRect()
const x = e.clientX - rect.left
const y = e.clientY - rect.top
const gridX = Math.floor(x / gridSize)
const gridY = Math.floor(y / gridSize)
if (currentTool === 'wall') {
// 添加墙壁
obstacles.push({ x: gridX, y: gridY })
} else if (currentTool === 'obstacle') {
// 添加障碍物
obstacles.push({ x: gridX, y: gridY })
} else if (currentTool === 'erase') {
// 删除墙壁或障碍物
obstacles = obstacles.filter(obs => !(obs.x === gridX && obs.y === gridY))
}
draw()
})
}
// 处理背景上传
function handleBackgroundUpload(e) {
const file = e.target.files[0]
if (file) {
const reader = new FileReader()
reader.onload = function(event) {
// 创建背景图像
const bgImage = new Image()
bgImage.onload = function() {
// 应用背景
ctx.drawImage(bgImage, 0, 0, canvas.width, canvas.height)
}
bgImage.src = event.target.result
}
reader.readAsDataURL(file)
}
}
// 处理皮肤上传
function handleSkinUpload(e) {
const file = e.target.files[0]
if (file) {
const reader = new FileReader()
reader.onload = function(event) {
// 在实际应用中,这里会设置蛇的皮肤
console.log('Skin uploaded:', file.name)
alert(`蛇皮肤 "${file.name}" 上传成功!`)
}
reader.readAsDataURL(file)
}
}
// 初始化游戏
function init() {
loadSettings()
initEvents()
initGame()
}
// 启动游戏
window.onload = init
七、性能优化策略
- Canvas渲染优化:使用离屏渲染和局部重绘技术
- 游戏循环控制:基于requestAnimationFrame的自适应帧率
- 内存管理:及时释放不再使用的资源
- 数据序列化:高效的游戏状态保存与加载
这个增强版贪吃蛇游戏展示了如何通过现代Web技术赋予经典游戏新的生命力。它不仅保留了原始游戏的简单乐趣,还通过多人模式、成就系统和关卡编辑器等现代游戏元素,创造了深度和广度都大大扩展的游戏体验。
这种开发模式证明了即使是最简单的游戏概念,也可以通过技术创新和用户体验优化,发展成为吸引现代玩家的复杂产品。对于开发者而言,这个项目展示了如何平衡传统与创新,在保留经典魅力的同时引入现代功能。
最终,这个贪吃蛇游戏的进化之旅提醒我们,优秀的游戏设计不仅仅是关于尖端技术或复杂机制,而是关于创造引人入胜的体验,让玩家愿意一次又一次地回来享受游戏的乐趣。