前言
在现代游戏开发中,玩家体验不仅依赖于玩法,更取决于数据的实时同步和游戏结果的透明展示。本文分享基于 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 {
();
resp.put(, );
resp.put(, loser);
sendAllMessage(resp.toJSONString());
}
{
( ; i < ; i++) {
(nextStep()) {
judge();
(.equals(status)) {
sendMove();
} {
sendResult();
;
}
} {
status = ;
lock.lock();
{
(nextStepA == && nextStepB == ) {
loser = ;
} (nextStepA == ) {
loser = ;
} {
loser = ;
}
} {
lock.unlock();
}
sendResult();
;
}
}
}
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 = ;
loser = ;
}
}
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 实现了低延迟的双向通信,并通过数据库持久化了对战数据,为构建高质量的在线游戏系统提供了基础架构支持。


