E24. 简单联网互动游戏示例
WARNING
本节内容尚未经过审核!不保证代码能够正常运行。
4.1. 🌟 系统架构设计
mermaid
graph LR
A[浏览器端] --> B[Vue.js]
B --> C[WebSocket(socket.io)]
C --> D[Node.js服务端]
D --> E[SQLite数据库]
E -->|存储玩家数据| D
D -->|实时通信| C
4.2. 🌟 核心流程详解
服务端流程(Express+Socket.IO)
javascript
// server.mjs
import express from 'express';
import { Server } from 'socket.io';
import sqlite3 from 'sqlite3';
// 数据库初始化
const db = new sqlite3.Database('players.db');
db.serialize(() => {
db.run('CREATE TABLE IF NOT EXISTS players (id TEXT PRIMARY KEY, score INTEGER, high_score INTEGER)');
});
const app = express();
const httpServer = app.listen(3000);
const io = new Server(httpServer);
// 玩家实时数据存储
const players = new Map();
// 每轮游戏状态
let gameInterval = null;
let currentRoundPlayers = new Map();
// 每5秒一轮
function startGameCycle() {
gameInterval = setInterval(() => {
// 1. 清空当前轮次数据
currentRoundPlayers.clear();
// 2. 广播游戏开始
io.emit('game-start', { timeLeft: 5 });
// 3. 5秒后结束本轮
setTimeout(() => {
// 计算得分
calculateScores();
// 重置数据
currentRoundPlayers.clear();
// 广播结果
io.emit('round-end', { scores: Array.from(players.values()) });
}, 5000);
}, 5000);
}
// 连接事件
io.on('connection', (socket) => {
socket.on('join', (playerId) => {
// 初始化玩家数据
db.get('SELECT * FROM players WHERE id = ?', [playerId], (err, row) => {
if (row) {
players.set(playerId, { id: playerId, score: row.score || 0, high_score: row.high_score || 0 });
} else {
players.set(playerId, { id: playerId, score: 0, high_score: 0 });
db.run('INSERT INTO players (id, score, high_score) VALUES (?, 0, 0)', [playerId]);
}
socket.emit('player-joined', players.get(playerId));
});
});
socket.on('submit-number', (data) => {
const { playerId, number } = data;
if (!currentRoundPlayers.has(playerId)) {
currentRoundPlayers.set(playerId, number);
}
});
socket.on('disconnect', () => {
// 断开时保存数据到数据库
players.forEach((player) => {
db.run('UPDATE players SET score = ?, high_score = ? WHERE id = ?',
[player.score, player.high_score, player.id]);
});
});
});
startGameCycle();
4.3. 🌟 前端实现(Vue.js)
vue
<!-- Game.vue -->
<template>
<div class="game-container">
<h1>实时互动游戏</h1>
<div v-if="gameState === 'waiting'">
<p>游戏即将开始,请准备...</p>
</div>
<div v-else-if="gameState === 'active'">
<p>剩余时间:{{ timeLeft }} 秒</p>
<div class="number-input">
<input v-model.number="selectedNumber" type="number" min="1" max="5">
<button @click="submitNumber">提交</button>
</div>
</div>
<div v-else-if="gameState === 'result'">
<p>本轮结果:</p>
<ul>
<li v-for="player in players" :key="player.id">
{{ player.id.substr(0,5) }}: {{ player.score }} 分
</li>
</ul>
</div>
<div>
<p>当前分数:{{ myScore }}</p>
<p>历史最高分:{{ myHighScore }}</p>
</div>
</div>
</template>
<script>
import { io } from 'socket.io-client';
export default {
data() {
return {
playerId: localStorage.getItem('playerId') || this.generateId(),
socket: null,
gameState: 'waiting',
timeLeft: 0,
selectedNumber: null,
players: [],
myScore: 0,
myHighScore: 0
};
},
mounted() {
this.socket = io('http://localhost:3000');
this.socket.emit('join', this.playerId);
this.socket.on('game-start', (data) => {
this.gameState = 'active';
this.timeLeft = data.timeLeft;
this.countdown = setInterval(() => {
this.timeLeft--;
if (this.timeLeft <= 0) clearInterval(this.countdown);
}, 1000);
});
this.socket.on('round-end', (data) => {
this.gameState = 'result';
this.players = data.scores;
this.myScore = this.players.find(p => p.id === this.playerId).score;
this.myHighScore = Math.max(this.myScore, this.players.find(p => p.id === this.playerId).high_score);
setTimeout(() => this.gameState = 'waiting', 3000);
});
},
methods: {
generateId() {
const nanoid = require('nanoid');
const id = nanoid(10);
localStorage.setItem('playerId', id);
return id;
},
submitNumber() {
if (this.selectedNumber >= 1 && this.selectedNumber <= 5) {
this.socket.emit('submit-number', { playerId: this.playerId, number: this.selectedNumber });
this.selectedNumber = null;
}
}
}
};
</script>
4.4. 🌟 关键算法实现
分数计算逻辑
javascript
function calculateScores() {
const roundPlayers = Array.from(currentRoundPlayers.entries());
// 遍历所有玩家对
roundPlayers.forEach(([playerAId, playerANum]) => {
roundPlayers.forEach(([playerBId, playerBNum]) => {
if (playerAId !== playerBId) {
// 循环比较:5比1小1,1比5小1?
const diff = (playerBNum - playerANum + 5) % 5;
if (diff === 4) { // 当差值为4时,说明B比A小1(5-1=4)
// B的数字比A小1
const playerA = players.get(playerAId);
const playerB = players.get(playerBId);
playerA.score += 1;
playerB.score -= 1;
updateHighScore(playerA);
updateHighScore(playerB);
}
}
});
});
}
function updateHighScore(player) {
if (player.score > player.high_score) {
player.high_score = player.score;
}
}
4.5. ⚠️ 关键点说明
4.5.1. 玩家ID生成与存储
javascript
// 使用 nanoid 生成唯一ID
const nanoid = require('nanoid');
const playerId = nanoid(10);
localStorage.setItem('playerId', playerId);
4.5.2. 循环比较逻辑
javascript
// 示例:5与1的比较
const a = 5, b = 1;
const diff = (b - a + 5) % 5; // (1-5+5)%5 = 1 → 不满足条件?
// 需要调整公式:
// 正确公式应为:(playerANum - playerBNum + 5) % 5 === 1
// 当 A选5,B选1 → (5-1) mod5=4 → 不满足
// 正确判断应为:当 (playerBNum === playerANum -1) || (playerANum ===5 && playerBNum ===1)
// 因此需要重新设计比较逻辑:
if ( (playerBNum === playerANum -1) ||
(playerANum ===5 && playerBNum ===1) ) {
// B比A小1
}
4.5.3. 数据库持久化
javascript
// 玩家连接时初始化数据
db.get('SELECT * FROM players WHERE id = ?', [playerId], (err, row) => {
if (!row) {
db.run('INSERT INTO players (id, score, high_score) VALUES (?,0,0)', [playerId]);
}
});
// 断开时保存数据
socket.on('disconnect', () => {
players.forEach((player) => {
db.run('UPDATE players SET score=?, high_score=? WHERE id=?',
[player.score, player.high_score, player.id]);
});
});
4.6. 🌟 完整开发步骤
服务端部署
bash
# 安装依赖
npm install express socket.io sqlite3 nanoid
# 初始化数据库
node -e "const sqlite3 = require('sqlite3');
new sqlite3.Database('players.db',
db => db.serialize(() =>
db.run('CREATE TABLE IF NOT EXISTS players (id TEXT PRIMARY KEY, score INTEGER, high_score INTEGER)')))"
# 启动服务
node server.mjs
前端配置
bash
# 创建Vue项目
vue create game-project
cd game-project
# 安装依赖
npm install socket.io-client nanoid
javascript
// vue.config.js
module.exports = {
devServer: {
proxy: {
'/': {
target: 'http://localhost:3000',
changeOrigin: true,
pathRewrite: { '^/': '' }
}
}
}
}
配置vue.config.js(允许跨域)
知识回顾
- 实时通信:通过 socket.io 实现玩家间的数据同步,每5秒一轮。
- 分数计算:采用循环比较逻辑,确保5和1的特殊关系。
- 数据持久化:SQLite 存储玩家ID、当前分数和历史最高分。
- 用户体验:通过 Vue 实现动态界面,显示倒计时、玩家分数等实时数据。
- 防作弊机制:玩家ID通过 localStorage 存储,但需注意安全性(可扩展加密存储)。
课后练习
- (编程)实现「历史最高分」功能,确保每次得分更新时自动记录最高分。
- (优化)添加「玩家排行榜」功能,实时显示前10名玩家。
- (扩展)实现「回合重置」功能,允许玩家手动重置当前分数。
参考答案(问题1)
javascript
// 在 calculateScores() 中添加:
function updateHighScore(player) {
if (player.score > player.high_score) {
player.high_score = player.score;
db.run('UPDATE players SET high_score=? WHERE id=?',
[player.high_score, player.id]);
}
}