Spring Boot 6.2
第一步:传递Player的位置
在Game.java
前创建Player
类
consumer/utils/Player.java
package com.kob.backend.consumer.utils;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Player {
private Integer id;
//起点
private Integer sx;
private Integer sy;
private List<Integer> steps;//存方向0123
}
在consumer/utils/Game.java
里添加Player
类,playerA
表示左下角的玩家,playerB
表示右上角的玩家,同时添加获取A,B player的函数,方便外部调用。
private final 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
consumer/WebSocketServer.java
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);
修改前端:store/pk.js
export default {
state: {
status:"matching",//matching匹配页面 playing对战页面
socket:null,
opponent_username:"",
opponent_photo:"",
gamemap:null,
//add
a_id:0,
a_sx:0,
a_sy:0,
b_id:0,
b_sx:0,
b_sy:0,
},
getters: {
},
mutations: {
updateSocket(state,socket){
state.socket = socket;
},
updateOpponent(state,opponent){
state.opponent_username = opponent.username;
state.opponent_photo = opponent.photo;
},
updateStatus(state,status){
state.status = status;
},
updateGamemap(state,game){
//add
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 ;
state.gamemap = game.gamemap ;
}
},
actions: {
},
modules: {
}
}
在PKindex.vue
里面直接把整个数据传进去就好了
store.commit("updateGame",data.game);
第二步:实现游戏同步(实现云端与两个客户端之间的同步)
实际上我们在游戏对战的时候存在三个棋盘,两个是对战双方客户端里存在的棋盘,一个是云端存在的棋盘,我们要求实现云端与两个客户端之间的同步。
实现方法
玩家每一次操作都会上传至云端服务器,当服务器接收到两个玩家的操作后,就会将两个玩家的蛇的移动信息同步给两个玩家。
流程
引入线程
为了优化游戏体验度,我们的Game
不能作为单线程去处理,每一个Game
要另起一个新线程来做。
从Next Step
开始的操作可以当成一个线程,获取用户操作可以当成另一个线程。
这里我们涉及到两个线程之间进行通信的问题,以及线程开锁解锁的问题。
每一局单独的游戏都会new 一个新的Game
类,都是一个单独的线程。
将类改成多线程
继承一个 Thread
类,并且ALT + INS
重写run()
方法
我们开始进行线程的执行的时候,线程的入口函数就是这个run()
函数
consumer/utils/Game.java
public class Game extends Thread{
...
@Override
public void run() {
super.run();
}
}
在consumer/WebSocketServer.java
里面通过start()
开始执行(是 Thread类的一个API)
game.createMap();
//a,b共同的地图==>将地图赋给a,b对应的连接
users.get(a.getId()).game = game;
users.get(b.getId()).game = game;
game.start();
读写锁的问题
consumer/utils/Game.java
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();
}
}
实现接受Client端输入的操作
consumer/WebSocketServer.java
public final static ConcurrentHashMap<Integer,WebSocketServer> users = new ConcurrentHashMap<>();
consumer/utils/Game.java
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();
}
}
private boolean nextStep(){//两名玩家的下一步
try {
Thread.sleep(200);//因为前端走一格200ms
} catch (InterruptedException e) {
e.printstacktrace();
}
//超时5s判断
for(int i = 1; i <= 5;i ++)
{
try {
Thread.sleep(1000);
lock.lock();
try{
if(this.nextStepA != null && this.nextStepB != null) {
//记录方向
playerA.getSteps().add(nextStepA);
playerB.getSteps().add(nextStepB);
return true;
}
} finally {
lock.unlock();
}
} catch (InterruptedException e) {
e.printstacktrace();
}
}
return false ;
}
编写后端逻辑
consumer/utils/Game.java
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();
}
}
private boolean nextStep(){//两名玩家的下一步
try {
Thread.sleep(200);//因为前端走一格200ms
} catch (InterruptedException e) {
e.printstacktrace();
}
//超时5s判断
for(int i = 1; i <= 5;i ++)
{
try {
Thread.sleep(1000);
lock.lock();
try{
if(this.nextStepA != null && this.nextStepB != null) {
//记录方向
playerA.getSteps().add(nextStepA);
playerB.getSteps().add(nextStepB);
return true;
}
} finally {
lock.unlock();
}
} catch (InterruptedException e) {
e.printstacktrace();
}
}
return false ;
}
private void judge(){//判断两名玩家下一步是否合法
}
private void sendResult(){//向两个client端公布结果
JSONObject resp = new JSONObject();
resp.put("event","result");
resp.put("loser",loser);
sendAllMessage(resp.toJSONString());
}
private void sendMove() {//向两个client传递移动信息
lock.lock();
try{
JSONObject resp = new JSONObject();
resp.put("event","move");
resp.put("a_direction",nextStepA);
resp.put("b_direction",nextStepB);
sendAllMessage(resp.toJSONString());
this.nextStepA = this.nextStepB = null;//清空下一步
} finally {
lock.unlock();
}
}
public void sendAllMessage(String message){
WebSocketServer.users.get(playerA.getId()).sendMessage(message);
WebSocketServer.users.get(playerB.getId()).sendMessage(message);
}
//线程的入口
@Override
public void run() {
for(int i = 0;i <1000;i++){
if(nextStep()){//获取了两条蛇的下一步
judge();//是否合法
if(this.status.equals("playing")){
sendMove();
} else if(this.status.equals("finished")){
sendResult();
break;//结束
}
} else {
this.status = "finished" ;//完成:结束
lock.lock();
try {
if (nextStepA == null && nextStepB == null) {//平局
this.loser = "all";
} else if (nextStepA == null) {
this.loser = "A";
} else {
this.loser = "B";
}
} finally {
lock.unlock();
}
sendResult();//向前端发送对战结果
break;
}
}
}
consumer/WebSocketServer.java
:接受前端信息:方向键等
@OnMessage
public void onMessage(String message, Session session) {
// 接收前端信息
System.out.println("receive message!");
JSONObject data = JSONObject.parSEObject(message);//解析message
String event = data.getString("event");//类似map
if("start-matching".equals(event)){
startMatching();
} else if("stop-matching".equals(event)){
stopMatching();
} else if("move".equals(event)){
move(data.getInteger("direction"));
}
}
修改前端
scripts/GameMap.js
//获取键盘输入
add_listening_events(){
this.ctx.canvas.focus();//cts[DOM] canvas[画板]
//为画板绑定keydown事件
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,
}))
}
})
}
在前端编写move
和result
的逻辑函数,让蛇动起来
同时,为了分别取出两条蛇可以将GameObject
在store/pk.js
里先存下来,记得写对应的update
函数哦!
然后我们再在components/GameMap.vue
里修改components/GameMap.vue
onMounted(() => {
store.commit("updateGameObject",new GameMap(canvas.value.getContext('2d'),parent.value,store));
});
蛇的去世判断要从前端搬到后端判断
先在前端写好情况分支选择views/pk/PKindex.vue
onMounted(() => { //当当前页面打开时调用
...
socket.onmessage = msg => { //前端接收到信息时调用的函数
...
} else 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 = "die";
}
if (data.loser === "all" || data.loser === "B") {
snake1.status = "die";
}
}
}
...
});
在后端写judge
逻辑
注意:要先添加一个Cell类
存储蛇的全部身体部分,在Player类
里面把蛇的身体都存储下来,
然后在Game类里判断的时候再循环一遍两个Player,各自取出自己的每一节cell逐个判断。
判断逻辑包括:撞墙、撞到自己、撞到他人,这些都会导致自己lose掉比赛.
consumer/utils/Player.java
//检查这一步是否合法
//判断cellsA是否合法的,判断cellsB的合法直接调用处反着写即可
private boolean check_valid(List<Cell> cellsA,List<Cell> cellsB)
{
int n = cellsA.size();
Cell cell = cellsA.get(n-1);//取出最后一步
if(g[cell.x][cell.y] == 1 ) return false ;
for(int i = 0; i < n-1 ;i++ ){
if(cell.x == cellsA.get(i).x && cell.y == cellsA.get(i).y){//撞自己
return false ;
}
}
for(int i = 0; i < n-1; i++) {
if (cell.x == cellsB.get(i).x && cell.y == cellsB.get(i).y) {//撞另一条蛇
return false;
}
}
return true;
}
//判断loser
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)//结束游戏
{
this.status = "finished" ;
if(!validA&&!validB){
this.loser = "all" ;
} else if(!validA){
this.loser = "A" ;
} else if(!validB){
this.loser = "B" ;
}
}
}
至此游戏的大部分逻辑已经写完了
写个游戏结果画面
首先在views/pk/PKindex.vue
里面添加游戏胜负显示逻辑
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);
}
在前端写一个组件components/ResultBoard.vue
这就是游戏结束后显示的结果版面,把谁是loser
存在store
里面就可以全局调用来判断了
实现再来
逻辑
接下来我们把再来按钮实现一下,玩家可以在游戏结束后点击这个按钮再来一局游戏。
实现逻辑也比较简单,每次点击按钮,把游戏页面展示状态status从playing 改成 matching即可,这样整个游戏页面就返回到匹配页面了。
不要忘记了要updateLoser改成none,即重新开始游戏前还没有loser
还有把对手头像updateOpponent成默认的灰头像。
<template>
<div class="result-board">
<!-- 为什么是==,而不是=== -->
<!-- $store.state.pk.a_id(数字1) $store.state.user.id(字符串1) -->
<div class="result-board-text" v-if="$store.state.pk.loser === 'all'">
Draw
</div>
<div class="result-board-text" v-else-if="$store.state.pk.loser === 'A' && $store.state.pk.a_id === parseInt($store.state.user.id)">
Lose
</div>
<div class="result-board-text" v-else-if="$store.state.pk.loser === 'B' && $store.state.pk.b_id === parseInt($store.state.user.id)">
Lose
</div>
<div class="result-board-text" v-else>
Win
</div>
<div class="result-board-btn">
<button @click="restart" type="button" class="btn btn-warning btn-lg">
再来 !
</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");
store.commit("updateOpponent",{
username:"我的对手",
photo:"https://cdn.acwing.com/media/article/image/2022/08/09/1_1db2488f17-anonymous.png",
});
}
return {
restart,
}
},
}
</script>
<style scoped>
div.result-board {
height: 30vh;
width: 30vw;
background-color: rgba(50, 50, 100, 0.5);
position:absolute;
top:30vh;
left:35vw;
}
div.result-board-text {
text-align: center;
color: white;
font-size: 50px;
font-weight: 600;
font-style: italic;
padding-top: 5vh;
}
div.result-board-btn {
text-align: center;
padding-top: 7vh;
}
</style>
第三步:设计录像数据库(后期存储对战录像)
为了后期存储对战录像,我们需要先设计一个存储对象的数据库。
数据库内容包括
id 自动递增、主键、唯一
a_id
a_sx
a_sy
b_id
b_sx
b_sy
a_steps
b_steps
map
loser
create_time
consumer/utils/Player.java
public String getStepsstring() {
StringBuilder res = new StringBuilder();
for(int x:steps){
res.append(x);
}
return res.toString();
}
consumer/utils/Game.java
private void sendResult(){//向两个client端公布结果
JSONObject resp = new JSONObject();
resp.put("event","result");
resp.put("loser",loser);
savetoDatabase();//调用处
sendAllMessage(resp.toJSONString());
}
private String getMapString() {
StringBuilder res = new StringBuilder();
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
res.append(g[i][j]);
}
}
return res.toString();
}
private void saveRecord() {
Record record = new Record(
null, //因为之前创建数据库时是把id定义为自动递增,所以这里不用手动传id
playerA.getId(),
playerA.getSx(),
playerA.getSy(),
playerB.getId(),
playerB.getSx(),
playerB.getSy(),
playerA.getStepsstring(),
playerB.getStepsstring(),
getMapString(),
loser,
new Date()
);
WebSocketServer.recordMapper.insert(record); //ws里数据库的注入
}