Skip to content

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(允许跨域)

知识回顾

  1. 实时通信:通过 socket.io 实现玩家间的数据同步,每5秒一轮。
  2. 分数计算:采用循环比较逻辑,确保5和1的特殊关系。
  3. 数据持久化:SQLite 存储玩家ID、当前分数和历史最高分。
  4. 用户体验:通过 Vue 实现动态界面,显示倒计时、玩家分数等实时数据。
  5. 防作弊机制:玩家ID通过 localStorage 存储,但需注意安全性(可扩展加密存储)。

课后练习

  1. (编程)实现「历史最高分」功能,确保每次得分更新时自动记录最高分。
  2. (优化)添加「玩家排行榜」功能,实时显示前10名玩家。
  3. (扩展)实现「回合重置」功能,允许玩家手动重置当前分数。
参考答案(问题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]);
  }
}

Built by Vitepress | Apache 2.0 Licensed