跳到主要内容
Spring Boot 游戏开发实战:实现游戏同步、结果页面与记录管理 | 极客日志
Java 大前端 java 算法
Spring Boot 游戏开发实战:实现游戏同步、结果页面与记录管理 基于 Spring Boot 和 Vue 的多人在线游戏开发实践。主要内容包括:1. 玩家类与游戏类的数据结构设计,用于存储玩家信息及操作序列;2. 采用多线程处理游戏逻辑,利用 ReentrantLock 解决线程安全问题,确保服务端状态同步;3. 通过 WebSocket 实现前后端实时通信,处理移动指令与状态广播;4. 在后端进行碰撞检测与胜负判定,避免前端作弊风险;5. 设计数据库表结构存储对战录像,支持复盘功能。最终实现了游戏同步、结果反馈及记录管理的全流程功能。
前言
在现代游戏开发中,玩家体验不仅依赖于玩法,更取决于数据的实时同步和游戏结果的透明展示。本文分享基于 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 , .cols - , <>());
}
Player { playerA; }
Player { playerB; }
this
2
new
ArrayList
public
getPlayerA
()
return
public
getPlayerB
()
return
修改 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());
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 ;
},
}
在 PkindexView.vue 中提交状态更新。
store.commit ("updateGame" , data.game );
3. 实现游戏同步 游戏对战涉及两个客户端棋盘和一个云端棋盘,需实现云端与客户端的同步。
3.1 多线程处理 为避免阻塞主线程,Game 类继承 Thread,每个对局单独开启新线程。
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;
}
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. 前端交互与结果展示 前端监听按键发送移动指令,并处理后端返回的移动和结果事件。
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);
}
}
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、位置、步骤、地图状态及结果。
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 实现了低延迟的双向通信,并通过数据库持久化了对战数据,为构建高质量的在线游戏系统提供了基础架构支持。
相关免费在线工具 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