跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客GitHub 精选镜像工具UI配色美学隐私政策关于联系
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
Java大前端java算法

Spring Boot 游戏开发实战:实现游戏同步、结果页面与记录管理

综述由AI生成基于 Spring Boot 和 Vue 的多人在线游戏开发实践。主要内容包括:1. 玩家类与游戏类的数据结构设计,用于存储玩家信息及操作序列;2. 采用多线程处理游戏逻辑,利用 ReentrantLock 解决线程安全问题,确保服务端状态同步;3. 通过 WebSocket 实现前后端实时通信,处理移动指令与状态广播;4. 在后端进行碰撞检测与胜负判定,避免前端作弊风险;5. 设计数据库表结构存储对战录像,支持复盘功能。最终实现了游戏同步、结果反馈及记录管理的全流程功能。

SqlMaster发布于 2026/3/30更新于 2026/5/2429 浏览
Spring Boot 游戏开发实战:实现游戏同步、结果页面与记录管理

前言

在现代游戏开发中,玩家体验不仅依赖于玩法,更取决于数据的实时同步和游戏结果的透明展示。本文分享基于 Spring Boot 实现游戏同步机制、游戏结果页面设计以及游戏记录管理的实践经验。

1. 玩家类设计

为区分玩家,需在 Game.java 中添加 Player 类存储玩家信息,包括玩家 ID、起始位置 (sx, sy) 及历史操作序列 steps。

consumer/utils/Player.java

package org.example.backend.consumer.utils;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Player {
    private Integer id;
    private Integer sx;
    private Integer sy;
    private List<Integer> steps;
}

在 consumer/utils/Game.java 中添加 playerA(左下角)和 playerB(右上角),并提供获取函数。

private Player playerA, playerB;

public Game(Integer rows, Integer cols, Integer inner_walls_count, Integer idA, Integer idB) {
    this.rows = rows;
    this.cols = cols;
    this.inner_walls_count = inner_walls_count;
    this.mark = new boolean[rows][cols];
    playerA = new Player(idA, this.rows - 2, 1, new ArrayList<>());
    playerB = new Player(idB, 1, this.cols - 2, new ArrayList<>());
}

public Player getPlayerA() { return playerA; }
public Player getPlayerB() { return playerB; }

修改 consumer/WebSocketServer.java 中的传参逻辑。

Game game = new Game(13, 14, 36, a.getId(), b.getId());

将游戏相关信息封装成 JSONObject 返回给客户端。

JSONObject respGame = new JSONObject();
respGame.put("a_id", game.getPlayerA().getId());
respGame.put("a_sx", game.getPlayerA().getSx());
respGame.put("a_sy", game.getPlayerA().getSy());
respGame.put("b_id", game.getPlayerB().getId());
respGame.put("b_sx", game.getPlayerB().getSx());
respGame.put("b_sy", game.getPlayerB().getSy());
respGame.put("map", game.getMark());
// 直接传游戏信息给玩家 A 和玩家 B
respA.put("game", respGame);
respB.put("game", respGame);

2. 前端状态管理

在 store/pk.js 中更新 state 以接收游戏数据。

state: {
    socket: null,
    opponent_username: "",
    opponent_photo: "",
    status: "matching",
    game_map: null,
    a_id: 0, a_sx: 0, a_sy: 0,
    b_id: 0, b_sx: 0, b_sy: 0,
},
mutations: {
    updateGame(state, game) {
        state.game_map = game.map;
        state.a_id = game.a_id;
        state.a_sx = game.a_sx;
        state.a_sy = game.a_sy;
        state.b_id = game.b_id;
        state.b_sx = game.b_sx;
        state.b_sy = game.b_sy;
    },
    // ... other mutations
}

在 PkindexView.vue 中提交状态更新。

store.commit("updateGame", data.game);

游戏界面

3. 实现游戏同步

游戏对战涉及两个客户端棋盘和一个云端棋盘,需实现云端与客户端的同步。

3.1 多线程处理

为避免阻塞主线程,Game 类继承 Thread,每个对局单独开启新线程。

consumer/utils/Game.java

public class Game extends Thread {
    @Override
    public void run() {
        super.run();
    }
}

在 WebSocketServer.java 中启动线程。

users.get(a.getId()).game = game;
users.get(b.getId()).game = game;
game.start();
3.2 线程同步锁

使用 ReentrantLock 保护共享变量 nextStepA 和 nextStepB 的读写。

private Integer nextStepA = null;
private Integer nextStepB = null;
private ReentrantLock lock = new ReentrantLock();

public void setNextStepA(Integer nextStepA) {
    lock.lock();
    try {
        this.nextStepA = nextStepA;
    } finally {
        lock.unlock();
    }
}

public void setNextStepB(Integer nextStepB) {
    lock.lock();
    try {
        this.nextStepB = nextStepB;
    } finally {
        lock.unlock();
    }
}
3.3 后端逻辑与等待机制

设置最长等待时间为 5s,若超时未收到操作则判定失败。读取前需 sleep 200ms 以匹配前端操作频率。

private boolean nextStep() {
    try {
        Thread.sleep(200);
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }
    for (int i = 0; i < 5; i++) {
        try {
            Thread.sleep(1000);
            lock.lock();
            try {
                if (nextStepA != null && nextStepB != null) {
                    playerA.getSteps().add(nextStepA);
                    playerB.getSteps().add(nextStepB);
                    return true;
                }
            } finally {
                lock.unlock();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    return false;
}

private void sendMove() {
    lock.lock();
    try {
        JSONObject resp = new JSONObject();
        resp.put("event", "move");
        resp.put("a_direction", nextStepA);
        resp.put("b_direction", nextStepB);
        nextStepA = nextStepB = null;
    } finally {
        lock.unlock();
    }
}

private void sendResult() {
    JSONObject resp = new JSONObject();
    resp.put("event", "result");
    resp.put("loser", loser);
    sendAllMessage(resp.toJSONString());
}

@Override
public void run() {
    for (int i = 0; i < 1000; i++) {
        if (nextStep()) {
            judge();
            if ("playing".equals(status)) {
                sendMove();
            } else {
                sendResult();
                break;
            }
        } else {
            status = "over";
            lock.lock();
            try {
                if (nextStepA == null && nextStepB == null) {
                    loser = "all";
                } else if (nextStepA == null) {
                    loser = "A";
                } else {
                    loser = "B";
                }
            } finally {
                lock.unlock();
            }
            sendResult();
            break;
        }
    }
}
3.4 碰撞检测

蛇的死亡判断移至后端。定义 Cell 类存储身体部分,在 Player 类中生成身体列表,并在 Game 类中判断撞墙、撞自己或撞对手。

consumer/utils/Player.java

public List<Cell> getCells() {
    List<Cell> res = new ArrayList<>();
    int[][] fx = {{-1, 0}, {0, 1}, {1, 0}, {0, -1}};
    int x = sx, y = sy;
    res.add(new Cell(x, y));
    int step = 0;
    for (int d : steps) {
        x += fx[d][0];
        y += fx[d][1];
        res.add(new Cell(x, y));
        if (!check_tail_increasing(++step)) {
            res.remove(0);
        }
    }
    return res;
}

consumer/utils/Game.java

private boolean check_valid(List<Cell> cellsA, List<Cell> cellsB) {
    int n = cellsA.size();
    Cell cell = cellsA.get(n - 1);
    if (mark[cell.x][cell.y]) return false;
    for (int i = 0; i < n - 1; i++) {
        if (cellsA.get(i).x == cell.x && cellsA.get(i).y == cell.y) return false;
    }
    for (int i = 0; i < n - 1; i++) {
        if (cellsB.get(i).x == cell.x && cellsB.get(i).y == cell.y) return false;
    }
    return true;
}

private void judge() {
    List<Cell> cellsA = playerA.getCells();
    List<Cell> cellsB = playerB.getCells();
    boolean validA = check_valid(cellsA, cellsB);
    boolean validB = check_valid(cellsB, cellsA);
    if (!validA || !validB) {
        status = "over";
        if (!validA && !validB) loser = "all";
        else if (!validA) loser = "A";
        else loser = "B";
    }
}

4. 前端交互与结果展示

前端监听按键发送移动指令,并处理后端返回的移动和结果事件。

scripts/GameMap.js

this.ctx.canvas.addEventListener("keydown", e => {
    let d = -1;
    if (e.key === 'w') d = 0;
    else if (e.key === 'd') d = 1;
    else if (e.key === 's') d = 2;
    else if (e.key === 'a') d = 3;
    if (d >= 0) {
        this.store.state.pk.socket.send(JSON.stringify({ event: "move", direction: d }));
    }
});

consumer/WebSocketServer.java

@OnMessage
public void onMessage(String message, Session session) {
    JSONObject data = JSONObject.parseObject(message);
    String event = data.getString("event");
    if ("move".equals(event)) {
        int d = data.getInteger("direction");
        move(d);
    }
}

public void move(int direction) {
    if (game.getPlayerA().getId().equals(user.getId())) {
        game.setNextStepA(direction);
    } else if (game.getPlayerB().getId().equals(user.getId())) {
        game.setNextStepB(direction);
    }
}

views/pk/PkindexView.vue

socket.onmessage = msg => {
    const data = JSON.parse(msg.data);
    if (data.event === "move") {
        const game = store.state.pk.gameObject;
        const [snake0, snake1] = game.snakes;
        snake0.set_direction(data.a_direction);
        snake1.set_direction(data.b_direction);
    } else if (data.event === "result") {
        const game = store.state.pk.gameObject;
        const [snake0, snake1] = game.snakes;
        if (data.loser === "all" || data.loser === "A") snake0.status = "dead";
        if (data.loser === "all" || data.loser === "B") snake1.status = "dead";
        store.commit("updateLoser", data.loser);
    }
};
4.1 结果面板

创建 components/ResultBoard.vue 显示胜负信息。

<template>
<div class="result-board">
    <div class="result-board-text draw" v-if="$store.state.pk.loser == 'all'">Draw</div>
    <div class="result-board-text lose" v-else-if="$store.state.pk.loser =='A' && $store.state.pk.a_id == $store.state.user.id">Lose</div>
    <div class="result-board-text lose" v-else-if="$store.state.pk.loser =='B' && $store.state.pk.b_id == $store.state.user.id">Lose</div>
    <div class="result-board-text win" v-else>WIN</div>
    <div class="result-board-btn"><button type="button" class="btn">Try again</button></div>
</div>
</template>

点击重试按钮将状态重置为 matching,清空 loser 信息。

5. 游戏记录存储

设计数据库表存储对战录像,包含双方 ID、位置、步骤、地图状态及结果。

RecordMapper.java

package com.kob.backend.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.kob.backend.pojo.Record;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface RecordMapper extends BaseMapper<Record> {}

保存记录逻辑:

private void saveRecord() {
    Record record = new Record(
        null, playerA.getId(), playerA.getSx(), playerA.getSy(),
        playerB.getId(), playerB.getSx(), playerB.getSy(),
        playerA.getStepsString(), playerB.getStepsString(),
        getMapString(), loser, new Date()
    );
    WebSocketServer.recordMapper.insert(record);
}

总结

本文实现了完整的在线游戏同步逻辑、结果展示页面与记录存储机制。通过多线程与锁机制保证了服务端状态的并发安全,结合 WebSocket 实现了低延迟的双向通信,并通过数据库持久化了对战数据,为构建高质量的在线游戏系统提供了基础架构支持。

目录

  1. 前言
  2. 1. 玩家类设计
  3. 2. 前端状态管理
  4. 3. 实现游戏同步
  5. 3.1 多线程处理
  6. 3.2 线程同步锁
  7. 3.3 后端逻辑与等待机制
  8. 3.4 碰撞检测
  9. 4. 前端交互与结果展示
  10. 4.1 结果面板
  11. 5. 游戏记录存储
  12. 总结
  • 💰 8折买阿里云服务器限时8折了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 🤖 一键搭建Deepseek满血版了解详情
  • 一键打造专属AI 智能体了解详情
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • OpenClaw 龙虾机器人部署与配置指南
  • 分治算法:快速排序及经典题目解析
  • C++ 滑动窗口算法详解:基础题解与思维分析
  • 普元 Mobile SlidePage 子页面数据刷新方案
  • VSCode 新版禁用 Ctrl+I 快捷键唤起 Copilot AI 对话框
  • Windows 本地编译 llama.cpp 完整流程
  • 智慧企业为何选择大模型重塑客服体系
  • MySQL 基础入门指南
  • 插入排序原理及 Java 实现详解
  • Python 编程基础:从零开始掌握核心概念
  • 什么是人工智能?AI、机器学习与深度学习的关系
  • 用 Anthropic 官方 Skill 提升大模型生成前端的审美能力
  • OpenClaw QQ 机器人接入指南
  • AI 进化论:从 Function Calling 到 MCP
  • OpenClaw 多飞书机器人配置指南
  • 使用 Java 计算 1 到 20 的阶乘之和
  • Flutter与Web混合开发实践
  • 龙虾机器人(OpenClaw)本地部署技术指南
  • Meson:现代 C/C++ 构建系统详解
  • 大模型架构要点总结:LLM 基础与核心组件解析

相关免费在线工具

  • Keycode 信息

    查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online

  • Escape 与 Native 编解码

    JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online

  • JavaScript / HTML 格式化

    使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online

  • JavaScript 压缩与混淆

    Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online

  • 加密/解密文本

    使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online

  • Gemini 图片去水印

    基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online