1.玩家位置同步
1.1后端修改
玩家的位置也要在服务端确定,确定完之后将每个玩家的位置传到前端。
添加一个玩家类
consumer.utils.Game.java
import java.util.List; @Data @AllArgsConstructor @NoArgsConstructor public class Player {
private Integer id; private Integer sx;//起始x坐标 private Integer sy;//起始y坐标 private List<Integer> steps;//保存每一步操作---决定了蛇当前的形状 }
讯享网
在初始化Game的时候,实例化两个Player对象

讯享网

在WebSocketServer.java中,为了方便管理,将与Game相关的信息,封装成一个JSON

这样后端就可以将两名玩家的信息(包括生成的地图)传送给前端
1.2前端修改
在src\store\pk.js中添加玩家信息的变量和更新函数
讯享网export default ({
state: {
status:"matching",//matching表示匹配界面 playing表示对战界面 socket:null,//存储前后端建立的connection opponent_username:"",//对手名 opponent_photo:"",//对手头像 gamemap:null, a_id:0, a_sx:0, a_sy:0, b_id:0, b_sx:0, b_sy:0, }, 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; }, updateGame(state, game){
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.map; }, }, actions: {
}, modules: {
} })
在src\views\pk\PkIndexView.vue中,在onmessage中,调用updateGame函数
<script> import PlayGround from '../../components/PlayGround.vue' import MatchGround from '../../components/MatchGround.vue' import {
onMounted } from 'vue' import {
onUnmounted } from 'vue' import {
useStore } from 'vuex' export default {
components: {
PlayGround, MatchGround }, setup() {
const store = useStore(); const socketUrl = `ws://127.0.0.1:3000/websocket/${
store.state.user.token}`; let socket = null; onMounted(() => {
....//省略 socket.onmessage = msg => {
const data = JSON.parse(msg.data); console.log(data); if (data.event === "start-matching") {
store.commit("updateOpponent", {
username: data.opponent_username, photo: data.opponent_photo }); //匹配成功后,延时2秒,进入对战页面 setTimeout(() => {
store.commit("updateStatus", "playing") }, 2000); store.commit("updateGame",data.game)//更新Game:包括玩家信息和地图 } } socket.onclose = () => {
console.log("disconnected!"); } }); onUnmounted(() => {
socket.close(); store.commit("updateStatus", "matching"); }) } } </script>
运行项目,使用用户名sun和用户名hong的登录,两个浏览器控制台console.log(data.game)的输出内容一致,均为,同步成功

2.游戏同步:多线程
2.1分析过程
之前只是两个棋盘,在浏览器本地通过wsad和上下左右来控制移动。
现在三个棋盘,两个client和一个server,需要实现三个棋盘的同步

再来梳理一下之前的游戏流程

对于从等待用户orBot输入到判别系统这一过程是独立的,

但是一般代码的执行是单线程,也就是按照顺序执行,例如如果在当前线程执行操作,当等待用户输入的时候,线程就会卡死,需要我们这样一个线程中有多个游戏在运行,只有Game1结束之后才能跑Game2,这样在第二个对局中,玩家就会漫长的等待。

因此,Game不能作为一个单线程来处理,因此,需要另起一个新的线程来做。
也就是将Game变成一个支持多线程的类

2.2多线程
首先为WebSocketServer增加一个成员变量,用于记录链接中的Game实例

在确定两名匹配的玩家之后,更新两名玩家的WebSocketServer连接上的Game实例值。

然后回到Game.java,将Game变成一个支持多线程的类,只需将Game继承Thread类,就可以支持多线程
讯享网public class Game extends Thread
然后重写多线程的入口函数run()
在开启一个新线程执行game.start()的时候,新线程中的入口函数,就是run()
初始化两个成员变量,用于表示两名玩家的下一步操作
private Integer nextStepA; private Integer nextStepB; public void setNextStepA(Integer nextStepA) {
this.nextStepA = nextStepA; } public void setNextStepB(Integer nextStepB) {
this.nextStepB = nextStepB; }
未来会在WebSocketServer.java中,接收到输入的时候,调用这两个函数
也就是在蓝色的线程里面修改nextStepA和nextStepB的值,而在红色的线程里面,会读取这两个线程的值

这就涉及到两个线程会同时读写一个变量,可能会产生读写冲突,需要枷锁
定义一个锁
讯享网private ReentrantLock lock = new ReentrantLock();
之后在setNextStepA和setNextStepB中
对两个变量进行更新之前,先锁上,操作完之后,解锁(不管有没有报异常)
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(); } }
在nextStep()函数中,负责等待两名玩家的输入,如果都在指定时间内输入了,就返回true
讯享网private boolean nextStep(){
//等待两名玩家的下一步操作 //由于前端动画200ms才能画一个格子 //如果在此期间接收到的输入多于一步 只会留最后一步 多余的会被覆盖 //因此在每一个下一步都要先休息200ms try {
Thread.sleep(200); } catch (InterruptedException e) {
e.printStackTrace(); } //如果5秒内有玩家没有输入 就返回false 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; }
如果其中一个超时没有输入,游戏就终止,并且分出胜负。
因此还需要定义一个游戏状态status和谁输了loser
private String status = "playing";//游戏状态 playing-->finished private String loser = "";//all:平; A:A输; B:B输了
最后,在线程的入口run()中初始逻辑如下
讯享网@Override public void run() {
for (int i = 0; i < 1000; i++) {
//1000步之内游戏肯定结束 if(nextStep()){
//如果获取两个玩家的下一步操作 }else {
status = "finished"; if(nextStepA == null && nextStepB == null){
loser = "all"; } else if (nextStepA == null) {
loser = "A"; } else{
loser = "B"; } } } }
但是上面这段逻辑有个问题,如果两名玩家在五秒内没有给出操作,就会进入else判断,此时本应该是平均,也就是loser = "all",但如果下面这段代码执行时,用户给出了输入,结果就会不符合预期。
if(nextStepA == null && nextStepB == null){
loser = "all"; } else if (nextStepA == null) {
loser = "A"; } else{
loser = "B"; }
所以,由于这里涉及到变量的读操作,为了在读的过程中被修改,因此也需要加锁。读完之后再解锁。
讯享网if(nextStep()){
//如果获取两个玩家的下一步操作 System.out.println(); }else {
status = "finished"; lock.lock(); try {
if(nextStepA == null && nextStepB == null){
loser = "all"; } else if (nextStepA == null) {
loser = "A"; } else{
loser = "B"; } }finally {
lock.lock(); } }
然后来看if (nextStep())判断,如果获取两个玩家的下一步操作
需要先进行judge(),来判断输入是否合法
并且,虽然A和B都知道自己的操作,但是看不到对方的操作,因此需要中心服务器以广播的形式来告知。

@Override public void run() {
for (int i = 0; i < 1000; i++) {
//1000步之内游戏肯定结束 if (nextStep()) {
//如果获取两个玩家的下一步操作 judge(); if(status.equals("playing")){
sentMove(); }else {
sentResult(); break; } } else {
status = "finished"; lock.lock(); try {
if (nextStepA == null && nextStepB == null) {
loser = "all"; } else if (nextStepA == null) {
loser = "A"; } else {
loser = "B"; } } finally {
lock.lock(); } sentResult(); break; } } }
而其中暂时不实现judge的逻辑,其他辅助函数的逻辑如下
讯享网private void sentAllmessage(String message){
//工具函数:向两名玩家广播信息 WebSocketServer.userConnectionInfo.get(playerA.getId()).sendMessage(message); WebSocketServer.userConnectionInfo.get(playerB.getId()).sendMessage(message); } private void sentMove() {
//向两个Client广播玩家操作信息 lock.lock();//凡是对操作进行读写的操作 都要加锁 try{
JSONObject resp = new JSONObject(); resp.put("event","move"); resp.put("a_direction",nextStepA); resp.put("b_direction",nextStepB); nextStepA = nextStepB = null;//清空操作 sentAllmessage(resp.toJSONString()); }finally {
lock.unlock(); } } private void sentResult() {
//向两个client公布结果信息 JSONObject resp = new JSONObject(); resp.put("event","result");//定义事件 resp.put("loser",loser); sentAllmessage(resp.toJSONString()); }
这样后端基本逻辑完成,接下来是前端与后端的通信,前端要将用户的操作发送过来,以及接收并处理中心服务器的广播
2.3前后端通信
此前判断蛇的移动,在scripts\GameMap.js
add_listening_events(){
this.ctx.canvas.focus();//聚焦 const [snake0, snake1] = this.snakes; this.ctx.canvas.addEventListener("keydown",e=>{
console.log(e.key); //wasd控制左下角球 上下左右控制右上角球 if(e.key === 'w') snake0.set_direction(0); else if (e.key === 'd') snake0.set_direction(1); else if (e.key === 's') snake0.set_direction(2); else if (e.key === 'a') snake0.set_direction(3); else if (e.key === 'ArrowUp') snake1.set_direction(0); else if (e.key === 'ArrowRight') snake1.set_direction(1); else if (e.key === 'ArrowDown') snake1.set_direction(2); else if (e.key === 'ArrowLeft') snake1.set_direction(3); }); }
这里,由于一个client负责一个玩家,只处理wsad即可。
修改如下,将玩家的操作操作传送到后端
讯享网 add_listening_events(){
this.ctx.canvas.focus();//聚焦 const [snake0, snake1] = this.snakes; this.ctx.canvas.addEventListener("keydown",e=>{
console.log(e.key); //wasd控制移动 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.sent(JSON.stringify({
//将JSON转换为字符串 event:"move", direction:d, })) } }); }
后端接收并分配给专门的路由来进行处理
private void move(Integer direction) {
//判断是A玩家还是B玩家在操作 if(game.getPlayerA().getId().equals(user.getId())){
game.setNextStepA(direction); }else if (game.getPlayerB().getId().equals(user.getId())) {
game.setNextStepB(direction); } else {
Exception e = new Exception("Error"); e.printStackTrace(); } } @OnMessage public void onMessage(String message, Session session) {
//当做路由 分配任务 // Server从Client接收消息时触发 System.out.println("Receive message!"); JSONObject data = JSONObject.parseObject(message);//将字符串解析成JSON String event = data.getString("event"); if("start-matching".equals(event)){
//防止event为空的异常 startMatching(); } else if ("stop-matching".equals(event)) {
stopMatching(); } else if ("move".equals(event)) {
Integer direction = data.getInteger("direction"); System.out.println(direction); move(direction); } }
此时,client端用户输入WSAD的时候,后端就能准确接收到信息。


同时,前端也要接收后端的广播来的信息,具体有两种event,分别是move和result
2.4(1) event == move

对操作进行更新需要用到Snack.js中的set_direction方法

两个玩家控制的snack对象在保存在GameMap对象中。

为了取到,需要将GameMap对象,作为游戏对象,保存为全局变量

先在src\store\pk.js中将gameObject存入全局变量,并写好更新函数

这样就能获取到游戏对象,并且更新两个玩家控制的snack的方向

此时,两个玩家都能够控制蛇正常移动

但是每次输入之后都会感觉到一些延迟,是因为输入之后可能线程还处于睡眠状态

调整为:

2.5(2) event == result
之前判断玩家输赢(蛇的状态)的逻辑在前端
讯享网//如果下一步操作撞了 蛇瞬间去世 if(!this.gamemap.check_valid(this.next_cell)){
this.status = "die"; }

将这段代码去掉。现在要交由后端来播报结果。
判断输赢有两部分逻辑:撞墙和超时,超时的逻辑已经写好,现在写判断撞墙的逻辑
参考前端GameMap.js中的check_valid(cell)函数

后端逻辑如下:
1)首先需要将两名玩家所控制的蛇取到:
新建Cell类代表蛇的单元
@Data @AllArgsConstructor @NoArgsConstructor public class Cell {
private Integer x; private Integer y; }
在Player.java中,将蛇的身体返回
0、1、2、3位置表示表示上右下左

对于四种操作0(w), 1(d), 2(s), 3(a)分别在行和列方向上的偏移量
讯享网int[] dx = {
-1, 0, 1, 0};//行方向的偏移量 int[] dy = {
0, 1, 0, -1}; //列方向的偏移量
所以Player.java的逻辑更新为
@Data @AllArgsConstructor @NoArgsConstructor public class Player {
private Integer id; private Integer sx;//起始x坐标 private Integer sy;//起始y坐标 private List<Integer> steps;//保存每一步操作---决定了蛇当前的形状 //检验当前回合 蛇的长度是否增加 private boolean check_tail_increasing(int step){
if(step <= 10) return true; else return step % 3 == 1; } //返回蛇的身体 public List<Cell> getCells(){
List<Cell> res = new ArrayList<>(); //对于四种操作0(w), 1(d), 2(s), 3(a) // 在行和列方向上的计算偏移量 int[] dx = {
-1, 0, 1, 0}; int[] dy = {
0, 1, 0, -1}; int x = sx; int y = sy; int step = 0;//回合数 res.add(new Cell(x,y));//添加起点 //不断根据steps计算出整个蛇身体 for (Integer d : steps) {
x += dx[d]; y += dy[d]; res.add(new Cell(x,y)); if(!check_tail_increasing(++step)){
//如果蛇尾不增加 就删掉蛇尾 res.remove(0);//O(N) } } return res; } }
2)判断两名玩家最后一步操作是否合法
- 没有撞到障碍物
- 没有撞到两条蛇的身体
- 没有撞到自己:最后一步与之前n-1个Cell是否重合
- 没有撞到别人:最后一步与之前n-1个Cell是否重合
- 由于A和B不可能走到同一个格子 因此不用判断最后一个格子是否重合
只需要判断最后一步,也就是蛇的最后一个Cell是否符合上面三种原则即可。
讯享网private boolean check_valid(List<Cell> cellsA, List<Cell> cellsB) {
int n = cellsA.size(); Cell cell = cellsA.get(n - 1);//取到A的最后一步 //三种不合法操作: A撞墙、A撞A、A撞B //A撞墙 if(g[cell.getX()][cell.getY()] == 1) return false; //A撞A for (int i = 0; i < n - 1; i++) {
if(cellsA.get(i).getX().equals(cell.getX()) && cellsA.get(i).getY().equals(cell.getY())){
return false; } } //A撞B for (int i = 0; i < n - 1; i++) {
if(cellsB.get(i).getX().equals(cell.getX()) && cellsB.get(i).getY().equals((cell.getY()))){
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 = "finished"; if(validA){
loser = "B"; } else if (validB) {
loser = "A"; } else {
loser = "all"; } } }
此时就能正常的进行合法性判断。

2.6游戏结果展示
最后,还需要将游戏的结果在前端展示,并且,设置一个重启按钮,点击重启之后,重新开始一局。
在pk.js中新增变量,方便用于展示谁赢谁输
新增一个组件ResultBoard.vue用于展示结果

核心代码如下:

然后在对战页面PkIndexView.vue导入组件,使其在loser!=none时展示出来

并且在收到后端播报结果时,更新全局变量中的loser

最终的结果如下,成功的实现了结果展示和重来一局。

点击重启

此时,再匹配的用户,又可以开始新的一轮对战。

这样,游戏同步功能就全部完成。
3.对局记录
接下来来实现另外一功能,就是将对局记录保存在数据库中。
3.1创建record
1)创建record表用来记录每局对战的信息
表中的列:
id: int- 非空 自增 唯一 主键
a_id: inta_sx: inta_sy: intb_id: intb_sx: intb_sy: inta_steps: varchar(1000)b_steps: varchar(1000)map: varchar(1000)loser: varchar(10)createtime: datetime
2)创建Pojo
注意,数据库中如果用下划线,则在pojo中要使用驼峰命名法
@Data @AllArgsConstructor @NoArgsConstructor public class Record {
@TableId(type = IdType.AUTO) private Integer id; private Integer aId; private Integer aSx; private Integer aSy; private Integer bId; private Integer bSx; private Integer bSy; private String aSteps; private String bSteps; private String map; private String loser; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai") private Date createtime; }
3)创建Mapper
讯享网@Mapper public interface RecordMapper extends BaseMapper<Record> {
}
写入数据库
首先将RecordMapper实例注入到WebSocketServer中

在Game.java中,在每次向client播报结果之前,将记录保存到数据库

这样在每局游戏结束时,记录就被保存下来

后续就可以根据记录来复原游戏画面
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请联系我们,一经查实,本站将立刻删除。
如需转载请保留出处:https://51itzy.com/kjqy/25879.html