蛇的控制逻辑,增长逻辑,碰撞检测改为后端实现 + 结果计分板实现

This commit is contained in:
flykhan 2023-03-03 16:51:58 +08:00
parent 4b3ebf9d77
commit ae4d8ef42d
10 changed files with 475 additions and 23 deletions

View File

@ -27,7 +27,7 @@ public class WebSocketServer {
使用线程安全的 Hash ConcurrentHashMap<>(), 使用线程安全的 Hash ConcurrentHashMap<>(),
userId 映射到 webSocketServer userId 映射到 webSocketServer
*/ */
private static final ConcurrentHashMap<Integer, WebSocketServer> users = new ConcurrentHashMap<>(); public static final ConcurrentHashMap<Integer, WebSocketServer> users = new ConcurrentHashMap<>();
// 使用 CopyOnWriteArraySet 创建一个线程安全的匹配池 matchPool // 使用 CopyOnWriteArraySet 创建一个线程安全的匹配池 matchPool
private static final CopyOnWriteArraySet<User> matchPool = new CopyOnWriteArraySet<>(); private static final CopyOnWriteArraySet<User> matchPool = new CopyOnWriteArraySet<>();
// WebSocketServer 中注入数据库的方法演示-> 使用 static 定义为独一份的变量 // WebSocketServer 中注入数据库的方法演示-> 使用 static 定义为独一份的变量
@ -36,6 +36,7 @@ public class WebSocketServer {
private Session session = null; private Session session = null;
// 用户信息:定义一个成员变量 // 用户信息:定义一个成员变量
private User user; private User user;
private Game game = null;
// 注入方法 // 注入方法
@Autowired @Autowired
@ -99,14 +100,24 @@ public class WebSocketServer {
matchPool.remove(b); matchPool.remove(b);
// 匹配成功时,创建联机地图 // 匹配成功时,创建联机地图
Game game = new Game(13,14,20); Game game = new Game(13,14,20,a.getId(),b.getId());
game.createMap(); // 初始化地图 game.createMap(); // 初始化地图
users.get(a.getId()).game = game; // game 赋给 a 玩家
users.get(b.getId()).game = game;
game.start(); // 开启新线程,执行函数
JSONObject respGameData = new JSONObject(); JSONObject respGameData = new JSONObject();
respGameData.put("game_map",game.getG()); respGameData.put("game_map",game.getG());
respGameData.put("rows",game.getRows()); respGameData.put("rows",game.getRows());
respGameData.put("cols",game.getCols()); respGameData.put("cols",game.getCols());
respGameData.put("inner_walls_count",game.getInnerWallsCount()); respGameData.put("inner_walls_count",game.getInnerWallsCount());
respGameData.put("a_id",game.getPlayerA().getId());
respGameData.put("a_sx",game.getPlayerA().getSx());
respGameData.put("a_sy",game.getPlayerA().getSy());
respGameData.put("b_id",game.getPlayerB().getId());
respGameData.put("b_sx",game.getPlayerB().getSx());
respGameData.put("b_sy",game.getPlayerB().getSy());
// a 配对成功的消息传回客户端 // a 配对成功的消息传回客户端
JSONObject respA = new JSONObject(); JSONObject respA = new JSONObject();
@ -133,6 +144,18 @@ public class WebSocketServer {
matchPool.remove(this.user); matchPool.remove(this.user);
} }
// direction 传入 move(移动) 方向参数
private void move(int direction){
// 判断角色:如果是 A 角色,则将获取到的方向设置为 A nextStep 方向
// user.getId() 是获取当前链接的用户 id
if(game.getPlayerA().getId().equals(user.getId())){
game.setNextStepA(direction);
}
else if(game.getPlayerB().getId().equals(user.getId())){
game.setNextStepB(direction);
}
}
// @OnMessage 用于从前端接收请求: 一般 onMessage 当成路由使用,做为消息判断处理的中间部分 // @OnMessage 用于从前端接收请求: 一般 onMessage 当成路由使用,做为消息判断处理的中间部分
@OnMessage @OnMessage
public void onMessage(String message, Session session) { public void onMessage(String message, Session session) {
@ -148,6 +171,8 @@ public class WebSocketServer {
startMatching(); startMatching();
} else if ("stop-matching".equals(event)) { } else if ("stop-matching".equals(event)) {
stopMatching(); stopMatching();
} else if ("move".equals(event)) {
move(data.getInteger("direction"));
} }
} }

View File

@ -0,0 +1,12 @@
package com.kob.backend.consumer.utils;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Cell {
int x, y;
}

View File

@ -1,9 +1,17 @@
package com.kob.backend.consumer.utils; package com.kob.backend.consumer.utils;
import java.util.Random; import com.alibaba.fastjson2.JSONObject;
import com.kob.backend.consumer.WebSocketServer;
// 用来管理整个游戏流程 import javax.swing.event.InternalFrameEvent;
public class Game { import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Random;
import java.util.concurrent.locks.ReentrantLock;
// 用来管理整个游戏流程:这里需要多线程
public class Game extends Thread {
// 游戏地图:行数,列数,内部障碍物数量 // 游戏地图:行数,列数,内部障碍物数量
private final Integer rows; private final Integer rows;
private final Integer cols; private final Integer cols;
@ -12,25 +20,68 @@ public class Game {
private final int[][] g; private final int[][] g;
// 定义"上右下左"四个方向的 dx, dy偏移量 // 定义"上右下左"四个方向的 dx, dy偏移量
private final int[] dx = {-1, 0, 1, 0}, dy = {0, 1, 0, -1}; private final int[] dx = {-1, 0, 1, 0}, dy = {0, 1, 0, -1};
private final Player playerA, playerB;
// 玩家下一步操作状态定义
private Integer nextStepA = null;
private Integer nextStepB = null;
// 加锁:解决读写冲突用
private ReentrantLock lock = new ReentrantLock();
// 整个游戏的当前状态: playing(正在进行) --> finished(游戏结束)
private String status = "playing";
// 定义失败者: all(平局), A(A输), B(B输)
private String loser = "";
// 初始化构造函数
public Game(Integer rows, Integer cols, Integer inner_walls_count) { // 初始化(有参)构造函数
public Game(Integer rows, Integer cols, Integer inner_walls_count, Integer idA, Integer idB) {
this.rows = rows; this.rows = rows;
this.cols = cols; this.cols = cols;
this.inner_walls_count = inner_walls_count; this.inner_walls_count = inner_walls_count;
this.g = new int[rows][cols]; this.g = new int[rows][cols];
playerA = new Player(idA, rows - 2, 1, new ArrayList<>());
playerB = new Player(idB, 1, cols - 2, new ArrayList<>());
}
public Player getPlayerA() {
return playerA;
}
public Player getPlayerB() {
return playerB;
} }
public int getRows() { public int getRows() {
return rows; return rows;
} }
public int getCols() { public int getCols() {
return cols; return cols;
} }
public int getInnerWallsCount() { public int getInnerWallsCount() {
return inner_walls_count; return inner_walls_count;
} }
public void setNextStepA(Integer nextStepA) {
// 为了防止两方读写冲突,需要加线程锁之后操作 nextStep
lock.lock();
try {
this.nextStepA = nextStepA;
} finally {
// 操作完成后解锁
lock.unlock();
}
}
public void setNextStepB(Integer nextStepB) {
lock.lock();
try {
this.nextStepB = nextStepB;
} finally {
lock.unlock();
}
}
// 返回生成的地图 // 返回生成的地图
public int[][] getG() { public int[][] getG() {
return g; return g;
@ -117,4 +168,162 @@ public class Game {
break; break;
} }
} }
// 获取两名玩家的下一步操作
private boolean nextStep() {
try {
// 返回下一步操作之前,先睡眠 200 毫秒,用于等待前端渲染完成,避免渲染速度跟不上后端运算处理速度
// 前端定义了每 1 秒钟 5 个格子,则每 200 毫秒走一格(每一步至少需要 200 毫秒的时间)
Thread.sleep(200);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// 循环 50 秒钟,每次 100 毫秒,判断用户输入(50 * 100 = 5000 ms后如果没有接收到输入,则判断输赢)
for (int i = 0; i < 50; i++) {
try {
// 两次接收输入的等待间隔
Thread.sleep(100);
lock.lock();
try {
// 判断两名玩家是否都已经读取到输入:如果读入都非空,则表示读取结束,返回 true
if (nextStepA != null && nextStepB != null) {
// 将下一步操作存下来
playerA.getSteps().add(nextStepA);
playerB.getSteps().add(nextStepB);
// 成功获取到两名玩家的下一步操作,返回 true
return true;
}
} finally {
lock.unlock();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return false;
}
// 碰撞合法性检测辅助函数:需要判断 snakeA snakeB 是否重合
private boolean checkValid(List<Cell> cellsA, List<Cell> cellsB) {
int n = cellsA.size(); // cellsA 的当前长度
Cell cell = cellsA.get(n - 1); // 取出 cellsA 的蛇头位置(数组最后一位)(第0个元素是蛇尾巴)
if (g[cell.x][cell.y] == 1) return false; // 判断 A 的最后一位(蛇头)的坐标是否是障碍物(值为1),如果是障碍物则返回false(判断已碰撞)
// 判断 A蛇头位置是否和A除了蛇头以外的其他身体部分坐标有重合
for (int i = 0; i < n - 1; i++) {
if (cellsA.get(i).x == cell.x && cellsA.get(i).y == cell.y)
return false;
}
for (Cell cellB : cellsB) { // 判断 A蛇头位置是否和B蛇身体各部分坐标有重合:有重合返回不合法(表示已经碰撞)
if (cellB.x == cell.x && cellB.y == cell.y)
return false;
}
// 以上判断条件如果都合法,则返回 true合法(表示没有碰撞)
return true;
}
// 判断两名玩家下一步操作是否合法
private void judge() {
// 取出 A,B 两蛇
List<Cell> cellsA = playerA.getCells();
List<Cell> cellsB = playerB.getCells();
boolean validA = checkValid(cellsA, cellsB);
boolean validB = checkValid(cellsB, cellsA);
// 如果 A B 有人这一步不合法(已经产生碰撞),将游戏状态改为 "finished",表示游戏结束
if (!validA || !validB) {
status = "finished";
// 游戏结束时,判断输赢
if (!validA && !validB) {
loser = "all";
} else if (!validA && validB) {
loser = "A";
} else if (validA && !validB) {
loser = "B";
}
}
}
// 向两个 Client 广播信息
private void sendAllMessage(String message) {
WebSocketServer.users.get(playerA.getId()).sendMessage(message);
WebSocketServer.users.get(playerB.getId()).sendMessage(message);
}
// 向两个 Client 传递移动信息
private void sendMove() {
//由于这里需要读入 nextStepA, nextStepB, 这里需要加一下线程锁
lock.lock();
try {
JSONObject resp = new JSONObject();
resp.put("event", "move"); // event(事件类型): move(移动信息)
resp.put("a_direction", nextStepA); // a_direction( playerA 移动的方向)
resp.put("b_direction", nextStepB); // b_direction( playerB 移动的方向)
sendAllMessage(resp.toJSONString());
// 本次获取完下一步操作的同时需要进行再下一步操作输入,在这之前,需要将当前的 nextStep 清空
nextStepA = nextStepB = null;
} finally {
lock.unlock();
}
}
// 向两个 Client 公布结果
private void sendResult() {
lock.lock();
try {
JSONObject resp = new JSONObject();
resp.put("event", "result"); // event(事件类型): result(结果)
resp.put("loser", loser); // 失败者
// 将最后碰撞时的蛇的眼睛指向传给前端
resp.put("a_eyes_finally_direction", nextStepA);
resp.put("b_eyes_finally_direction", nextStepB);
sendAllMessage(resp.toJSONString());
} finally {
lock.unlock();
}
}
// 重写线程函数
@Override
public void run() {
// 循环 1000 : 13格x14格x(最大)3步/ < 1000保证 1000 次一定可以走完
for (int i = 0; i < 1000; i++) {
if (nextStep()) { // 是否获取到两条蛇的下一步操作
// 判断下一步操作是否合法
judge();
// 如果游戏状态还是 playing,则向两个玩家广播移动信息
if ("playing".equals(status)) {
sendMove();
} else if ("finished".equals(status)) {
// 如果游戏状态已经更改为了 finished,则将结果返回给 Client break 中断执行后续操作
sendResult();
break;
}
} else {
// 未获取到某蛇的下一步操作,则将游戏状态改为 finished
status = "finished";
// 判断失败者:这里需要加锁,因为涉及到对 nextStep 的读操作
lock.lock();
try {
if (nextStepA == null && nextStepB == null) {
loser = "all";
} else if (nextStepA == null) {
loser = "A";
} else {
loser = "B";
}
} finally {
lock.unlock();
}
}
}
}
} }

View File

@ -0,0 +1,51 @@
package com.kob.backend.consumer.utils;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.ArrayList;
import java.util.List;
/// 玩家信息
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Player {
private Integer id;
private Integer sx;
private Integer sy;
// 玩家走过的路径中每一步的方向
private List<Integer> steps;
// 检验当前回合蛇的长度(蛇尾)是否需要增加
public boolean checkTailIncreasing(int step) {
// 10 回合每次都增加一节蛇尾,后面每 3 回合增加一节蛇尾
if (step <= 10) return true;
if (step % 3 == 1) return true;
return false;
}
// 蛇身 Cell 列表:记录蛇身各部分坐标
public List<Cell> getCells() {
List<Cell> res = new ArrayList<>();
// 四方向偏移量
int[] dx = {-1, 0, 1, 0}, dy = {0, 1, 0, -1};
int x = sx, y = sy; // 起始坐标
int step = 0; // 定义当前回合数: 开始时为 0 回合
res.add(new Cell(x, y)); // 现将蛇头添加进 List
for (int d : steps) { // 使用下一步方向求出下一个结点的坐标
x += dx[d];
y += dy[d];
res.add(new Cell(x, y)); // 添加新的身体结点
step++; // 回合数 +1
// 判断蛇尾要不要增加:如果本回合蛇尾不增加,则将本回合新生成的蛇尾(列表的第0个元素)删掉
if (!checkTailIncreasing(step)) {
res.remove(0);
}
}
return res;
}
}

View File

@ -33,7 +33,7 @@ export class GameMap extends AcGameObject {
// 上面的 super() 会先将 AcGameObject 先绘制, walls 的绘制在后面执行,因此墙最后会覆盖原棋盘格进行绘制 // 上面的 super() 会先将 AcGameObject 先绘制, walls 的绘制在后面执行,因此墙最后会覆盖原棋盘格进行绘制
this.walls = []; this.walls = [];
// 创建蛇对象数组 // 创建蛇对象数组(可以通过 store.state.pk.game_object.snakes 全局变量调取)
this.snakes = [ this.snakes = [
// 注意这里的对象生成方式和传参方式 // 注意这里的对象生成方式和传参方式
new Snake({ id: 0, color: "#4876ec", r: this.rows - 2, c: 1 }, this), new Snake({ id: 0, color: "#4876ec", r: this.rows - 2, c: 1 }, this),
@ -167,20 +167,35 @@ export class GameMap extends AcGameObject {
this.ctx.canvas.focus(); this.ctx.canvas.focus();
// 先取出两条蛇对象 // 先取出两条蛇对象
const [snake0, snake1] = this.snakes; // const [snake0, snake1] = this.snakes;
// 获取用户信息:绑定 keydown 事件 // 获取用户信息:绑定 keydown 事件
this.ctx.canvas.addEventListener("keydown", (e) => { this.ctx.canvas.addEventListener("keydown", (e) => {
// 取出移动的方向,需要将方向传给后端
let d = -1; // -1 表示没有方向, 0-上, 1-右, 2-下, 3-左
// 定义 snake0 的键盘绑定事件 // 定义 snake0 的键盘绑定事件
if (e.key === "w") snake0.set_direction(0); if (e.key === "w") d = 0;
else if (e.key === "d") snake0.set_direction(1); else if (e.key === "d") d = 1;
else if (e.key === "s") snake0.set_direction(2); else if (e.key === "s") d = 2;
else if (e.key === "a") snake0.set_direction(3); else if (e.key === "a") d = 3;
// 定义 snake1 的键盘绑定事件
// 如果进行了合法的移动操作:向后端发送方向
if(d>=0){
// 使用 socket.send() 传递信息给后端,使用 JSON.stringify() 将一个 JSON 封装成一字符串
this.store.state.pk.socket.send(JSON.stringify({
event: "move", // 事件类型: move
direction : d, // 方向: d
}))
}
/*
// 定义 snake1 的键盘绑定事件(前端调试时启用)
else if (e.key === "ArrowUp") snake1.set_direction(0); else if (e.key === "ArrowUp") snake1.set_direction(0);
else if (e.key === "ArrowRight") snake1.set_direction(1); else if (e.key === "ArrowRight") snake1.set_direction(1);
else if (e.key === "ArrowDown") snake1.set_direction(2); else if (e.key === "ArrowDown") snake1.set_direction(2);
else if (e.key === "ArrowLeft") snake1.set_direction(3); else if (e.key === "ArrowLeft") snake1.set_direction(3);
*/
}); });
} }

View File

@ -104,11 +104,12 @@ export class Snake extends AcGameObject {
// 这里需要使用 JSON 方法进行深度复制,以产生新的对象避免数据出错 // 这里需要使用 JSON 方法进行深度复制,以产生新的对象避免数据出错
this.cells[i] = JSON.parse(JSON.stringify(this.cells[i - 1])); this.cells[i] = JSON.parse(JSON.stringify(this.cells[i - 1]));
} }
/* //(改为后端实现)
// 如果下一步操作的目标位置碰撞检测不合法,则蛇直接去世 // 如果下一步操作的目标位置碰撞检测不合法,则蛇直接去世
if (!this.gamemap.check_valid(this.next_cell)) { if (!this.gamemap.check_valid(this.next_cell)) {
this.status = "die"; this.status = "die";
} }
*/
} }
update_move() { update_move() {

View File

@ -24,8 +24,10 @@ export default {
// , // ,
onMounted(() => { onMounted(() => {
// new GameMap(canvas.value.getContext("2d"), parent.value); // new GameMap(canvas.value.getContext("2d"), parent.value);
// () // (): store
new GameMap(canvas.value.getContext("2d"), parent.value, store); store.commit("updateGameObject",
new GameMap(canvas.value.getContext("2d"), parent.value, store),
);
}); });
return { return {

View File

@ -0,0 +1,78 @@
//
<template>
<div class="result-borad">
<div class="result-board-text" v-if="$store.state.pk.loser === 'all'">
平局
</div>
<div class="result-board-text"
v-else-if="$store.state.pk.loser === 'A' && $store.state.pk.a_id === parseInt($store.state.user.id)">
你输了!
</div>
<div class="result-board-text"
v-else-if="$store.state.pk.loser === 'B' && $store.state.pk.b_id == $store.state.user.id">
你输了!
</div>
<div class="result-board-text"
v-else-if="$store.state.pk.loser === 'A' && $store.state.pk.b_id == $store.state.user.id">
你赢了!
</div>
<div class="result-board-text"
v-else-if="$store.state.pk.loser === 'B' && $store.state.pk.a_id == $store.state.user.id">
你赢了!
</div>
<div class="result-board-btn">
<button type="button" class="btn btn-dark" @click="restart">再来一次</button>
</div>
</div>
</template>
<script>
import { useStore } from "vuex";
export default {
setup() {
const store = useStore();
// ,
const restart = () => {
store.commit("updateStatus", "matching");
store.commit("updateLoser", "none");
}
return {
restart,
};
}
}
</script>
<style scoped>
div.result-borad {
height: 30vh;
width: 30vw;
background-color: rgba(194, 194, 195, 0.504);
/* 位置 */
position: absolute;
top: 30vh;
left: 35vw;
}
.result-board-text {
text-align: center;
color: rgb(234, 234, 234);
font-size: 50px;
font-weight: 600;
font-style: italic;
padding-top: 5vh;
}
.result-board-btn {
padding-top: 3vh;
text-align: center;
}
</style>

View File

@ -7,10 +7,20 @@ export default {
// 对手信息 // 对手信息
opponent_username: "", opponent_username: "",
opponent_photo: "", opponent_photo: "",
// 地图信息(后端传入)
game_map: null, game_map: null,
rows: 0, rows: 0,
cols: 0, cols: 0,
inner_walls_count: 0, inner_walls_count: 0,
a_id: 0,
a_sx: 0,
a_sy: 0,
b_id: 0,
b_sx: 0,
b_sy: 0,
// 游戏全局
game_object: null,
loser:"none", // all, A, B 三种结果
}, },
getters: {}, getters: {},
mutations: { mutations: {
@ -33,7 +43,20 @@ export default {
state.rows = game_data.rows; state.rows = game_data.rows;
state.cols = game_data.cols; state.cols = game_data.cols;
state.inner_walls_count = game_data.inner_walls_count; state.inner_walls_count = game_data.inner_walls_count;
state.a_id = game_data.a_id;
state.a_sx = game_data.a_sx;
state.a_sy = game_data.a_sy;
state.b_id = game_data.b_id;
state.b_sx = game_data.b_sx;
state.b_sy = game_data.b_sy;
}, },
updateGameObject(state, game_object){
// 在 GameMap.vue 中使用
state.game_object = game_object;
},
updateLoser(state,loser){
state.loser = loser;
}
}, },
actions: {}, actions: {},
module: {}, module: {},

View File

@ -1,11 +1,14 @@
<template> <template>
<PlayGround v-if="$store.state.pk.status === 'playing'" /> <PlayGround v-if="$store.state.pk.status === 'playing'" />
<MatchGround v-else-if="$store.state.pk.status === 'matching'" /> <MatchGround v-else-if="$store.state.pk.status === 'matching'" />
<ResultBoard v-if="$store.state.pk.loser !== 'none'" />
</template> </template>
<script> <script>
import PlayGround from "../../components/PlayGround.vue"; import PlayGround from "../../components/PlayGround.vue";
import MatchGround from "../../components/MatchGround.vue" import MatchGround from "../../components/MatchGround.vue";
import ResultBoard from "../../components/ResultBoard.vue";
// onMounted , onUnmounted // onMounted , onUnmounted
import { onMounted, onUnmounted } from "vue"; import { onMounted, onUnmounted } from "vue";
// //
@ -14,7 +17,8 @@ import { useStore } from "vuex";
export default { export default {
components: { components: {
PlayGround, PlayGround,
MatchGround MatchGround,
ResultBoard
}, },
// :,,使 onMounted // :,,使 onMounted
setup() { setup() {
@ -23,6 +27,7 @@ export default {
const socketUrl = `ws://127.0.0.1:3000/websocket/${store.state.user.token}/`; const socketUrl = `ws://127.0.0.1:3000/websocket/${store.state.user.token}/`;
let socket = null; let socket = null;
// ,, store // ,, store
onMounted(() => { onMounted(() => {
// (使) // (使)
@ -61,6 +66,37 @@ export default {
store.commit("updateGameMap", data.game_data); store.commit("updateGameMap", data.game_data);
console.log(data.game_data); console.log(data.game_data);
} }
else if (data.event === "move") {
// console.log(data);
// store
const game = store.state.pk.game_object;
// GameMap.js snakes
const [snake0, snake1] = game.snakes;
// : sendMove
snake0.set_direction(data.a_direction);
snake1.set_direction(data.b_direction);
}
else if (data.event === "result") {
console.log(data);
const game = store.state.pk.game_object;
const [snake0, snake1] = game.snakes;
// ()
snake0.eye_direction = data.a_eyes_finally_direction;
snake1.eye_direction = data.b_eyes_finally_direction;
// loser , store
store.commit("updateLoser",data.loser);
// loser snake status
if (data.loser === "all" || data.loser === "A") {
snake0.status = "die";
}
if (data.loser === "all" || data.loser === "B") {
snake1.status = "die";
}
}
} }
socket.onclose = () => { socket.onclose = () => {