version-2.2[前后端同步操作+多线程+读写锁+数据库:录像]

pull/1/head
dyh 3 years ago
parent 521ba31fa8
commit 28c2c2dc04

@ -4,6 +4,7 @@ package com.kob.backend.consumer;
import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson.JSONObject;
import com.kob.backend.consumer.utils.Game; import com.kob.backend.consumer.utils.Game;
import com.kob.backend.consumer.utils.JwtAuthentication; import com.kob.backend.consumer.utils.JwtAuthentication;
import com.kob.backend.mapper.RecordMapper;
import com.kob.backend.mapper.UserMapper; import com.kob.backend.mapper.UserMapper;
import com.kob.backend.pojo.User; import com.kob.backend.pojo.User;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@ -22,7 +23,7 @@ public class WebSocketServer {
//ConcurrentHashMap 的优势在于兼顾性能和线程安全,一个线程进行写操作时, //ConcurrentHashMap 的优势在于兼顾性能和线程安全,一个线程进行写操作时,
//它会锁住一小部分,其他部分的读写不受影响,其他线程访问没上锁的地方不会被阻塞。 //它会锁住一小部分,其他部分的读写不受影响,其他线程访问没上锁的地方不会被阻塞。
//(userId,WebSocketServer实例连接) //(userId,WebSocketServer实例连接)
final private static ConcurrentHashMap<Integer,WebSocketServer> users = new ConcurrentHashMap<>(); public final static ConcurrentHashMap<Integer,WebSocketServer> users = new ConcurrentHashMap<>();
//匹配池 //匹配池
final private static CopyOnWriteArraySet<User> matchpool = new CopyOnWriteArraySet<>(); final private static CopyOnWriteArraySet<User> matchpool = new CopyOnWriteArraySet<>();
//当前用户 //当前用户
@ -33,11 +34,13 @@ public class WebSocketServer {
private Game game ; private Game game ;
//加入数据库 //加入数据库
private static UserMapper userMapper; private static UserMapper userMapper;
public static RecordMapper recordMapper;
@Autowired @Autowired
public void setUserMapper(UserMapper userMapper){ public void setUserMapper(UserMapper userMapper){
WebSocketServer.userMapper = userMapper; WebSocketServer.userMapper = userMapper;
} }
@Autowired
public void setRecordMapper(RecordMapper recordMapper) { WebSocketServer.recordMapper = recordMapper;}
@OnOpen @OnOpen
@ -47,7 +50,7 @@ public class WebSocketServer {
this.session = session ;//将session存起来--》一个用户一个session this.session = session ;//将session存起来--》一个用户一个session
System.out.println("connected!"); System.out.println("connected!");
Integer userId = JwtAuthentication.getUserId(token);//获取id Integer userId = JwtAuthentication.getUserId(token);//获取id
this.user = userMapper.selectById(userId);//获取用户 this.user = userMapper.selectById(userId);//获取当前用户
if(this.user != null){//存在用户 加入连接 if(this.user != null){//存在用户 加入连接
users.put(userId,this);//用户链接--》加入到集合中 users.put(userId,this);//用户链接--》加入到集合中
@ -77,21 +80,36 @@ public class WebSocketServer {
matchpool.remove(a); matchpool.remove(a);
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();
//a,b共同的地图==>将地图赋给a,b对应的连接
users.get(a.getId()).game = game ;
users.get(b.getId()).game = game ;//b连接的地图
game.start();
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.getG());
JSONObject respA = new JSONObject();//返回给a JSONObject respA = new JSONObject();//返回给a
respA.put("event", "start-matching"); respA.put("event", "start-matching");
respA.put("opponent_username", b.getUsername()); respA.put("opponent_username", b.getUsername());
respA.put("opponent_photo", b.getPhoto()); respA.put("opponent_photo", b.getPhoto());
respA.put("gamemap", game.getG()); respA.put("game", respGame);
users.get(a.getId()).sendMessage(respA.toJSONString());//获取a对应的连接向前端传递信息String users.get(a.getId()).sendMessage(respA.toJSONString());//获取a对应的连接向前端传递信息String
JSONObject respB = new JSONObject();//返回给b JSONObject respB = new JSONObject();//返回给b
respB.put("event", "start-matching"); respB.put("event", "start-matching");
respB.put("opponent_username", a.getUsername()); respB.put("opponent_username", a.getUsername());
respB.put("opponent_photo", a.getPhoto()); respB.put("opponent_photo", a.getPhoto());
respB.put("gamemap", game.getG()); respB.put("game", respGame);
users.get(b.getId()).sendMessage(respB.toJSONString());//获取b对应的连接向前端传递信息String users.get(b.getId()).sendMessage(respB.toJSONString());//获取b对应的连接向前端传递信息String
} }
} }
@ -101,6 +119,14 @@ public class WebSocketServer {
matchpool.remove(this.user); matchpool.remove(this.user);
} }
public void move(int dirction){
if(game.getPlayerA().getId().equals(user.getId())){//当前链接是A用户
game.setNextStepA(dirction);
} else if(game.getPlayerB().getId().equals(user.getId())){//当前链接是B用户
game.setNextStepB(dirction);
}
}
@OnMessage @OnMessage
public void onMessage(String message, Session session) { public void onMessage(String message, Session session) {
// 接收前端信息 // 接收前端信息
@ -111,6 +137,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"));
} }
} }

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

@ -1,24 +1,52 @@
package com.kob.backend.consumer.utils; package com.kob.backend.consumer.utils;
import com.alibaba.fastjson.JSONObject;
import com.kob.backend.consumer.WebSocketServer;
import com.kob.backend.pojo.Record;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Random; import java.util.Random;
import java.util.concurrent.locks.ReentrantLock;
public class Game extends Thread {
private final Integer rows;
private final Integer cols;
private final Integer inner_walls_count;
private final int[][] g;
private final static int[] dx = {-1, 0, 1, 0}, dy = {0, 1, 0, -1};
private final Player playerA,playerB;
//用户a,b的下一步方向
private Integer nextStepA = null;
private Integer nextStepB = null;
private ReentrantLock lock = new ReentrantLock();
public class Game { private String status = "playing";//playing-->finished
private String loser = "" ;//all平局AA输B:B输
final private Integer rows;
final private Integer cols;
final private Integer inner_walls_count;
final private int[][] g;
final private static int[] dx = {-1, 0, 1, 0}, dy = {0, 1, 0, -1};
//构造map规模 public Game(Integer rows, Integer cols, Integer inner_walls_count,Integer idA, Integer idB) {
public Game(Integer rows, Integer cols, Integer inner_walls_count) {
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,this.rows - 2, 1, new ArrayList<>());
playerB = new Player(idB,1, this.cols - 2, new ArrayList<>());
}
public Player getPlayerA(){
return this.playerA;
}
public Player getPlayerB(){
return this.playerB;
} }
//货物gamemap //@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
//获取gamemap
public int[][] getG() public int[][] getG()
{ {
return g; return g;
@ -95,4 +123,190 @@ public class Game {
if (draw()) break; if (draw()) break;
} }
} }
//@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
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 <= 50;i ++)
{
try {
Thread.sleep(100);
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 ;
}
//检查这一步是否合法
//判断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" ;
}
}
}
private void sendResult(){//向两个client端公布结果
JSONObject resp = new JSONObject();
resp.put("event","result");
resp.put("loser",loser);
saveToDatabase();
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);
}
private void saveToDatabase()
{
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);
}
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();
}
//线程的入口
@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;
}
}
}
} }

@ -0,0 +1,58 @@
package com.kob.backend.consumer.utils;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.ArrayList;
import java.util.List;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Player {
private Integer id;
//起点
private Integer sx;
private Integer sy;
private List<Integer> steps;//存方向0123
//蛇是否增长
private boolean check_tail_increasing(int step){
if(step <= 10)return true;
if(step % 3 == 1)return true;
return false;
}
//获取蛇的路径
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;//第几步
res.add(new Cell(x,y));//存入起点
for(int d:steps){
x = x + dx[d];
y = y + dy[d];
res.add(new Cell(x,y));
if(!check_tail_increasing( ++step)){//不增长,去除尾巴
res.remove(0);
}
}
return res ;
}
public String getStepsString() {
StringBuilder res = new StringBuilder();
for(int x:steps){
res.append(x);
}
return res.toString();
}
}

@ -0,0 +1,9 @@
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> {
}

@ -0,0 +1,30 @@
package com.kob.backend.pojo;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
@Data
@NoArgsConstructor
@AllArgsConstructor
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 ;
}

@ -23,8 +23,8 @@ export class GameMap extends AcGameObject
this.inner_walls_count = 20; this.inner_walls_count = 20;
this.snakes = [ this.snakes = [
new Snake({id:0,color:"#4876ED",r:this.rows-2,c:1},this), new Snake({id:0,color:"#4876ED",r:this.rows-2,c:1},this),//左下
new Snake({id:1,color:"#F94848",r:1,c:this.cols-2},this) new Snake({id:1,color:"#F94848",r:1,c:this.cols-2},this) //右上
] ]
@ -33,7 +33,6 @@ export class GameMap extends AcGameObject
//创建墙 //创建墙
create_wall(){ create_wall(){
const g = this.store.state.pk.gamemap ; const g = this.store.state.pk.gamemap ;
console.log(g)
for(let r=0;r<this.rows;r++) for(let r=0;r<this.rows;r++)
{ {
for(let c=0;c<this.cols;c++) for(let c=0;c<this.cols;c++)
@ -98,21 +97,28 @@ export class GameMap extends AcGameObject
//获取键盘输入 //获取键盘输入
add_listening_events(){ add_listening_events(){
this.ctx.canvas.focus();//cts[DOM] canvas[画板] this.ctx.canvas.focus();//cts[DOM] canvas[画板]
const [snake0,snake1] = this.snakes;
//为画板绑定keydown事件 //为画板绑定keydown事件
this.ctx.canvas.addEventListener("keydown",e => { this.ctx.canvas.addEventListener("keydown",e => {
if(e.key === "w") snake0.set_direction(0); let d = -1;
else if(e.key === "d") snake0.set_direction(1); if(e.key === "w") d = 0;
else if(e.key === 's') snake0.set_direction(2); else if(e.key === "d") d = 1;
else if(e.key === 'a') snake0.set_direction(3); else if(e.key === 's') d = 2;
else if(e.key === 'ArrowUp' ) snake1.set_direction(0); else if(e.key === 'a') d = 3;
else if(e.key === 'ArrowRight') snake1.set_direction(1);
else if(e.key === 'ArrowDown') snake1.set_direction(2); if(d >= 0){
else if(e.key === 'ArrowLeft') snake1.set_direction(3); this.store.state.pk.socket.send(JSON.stringify({
// console.log("keydown") event:"move",
direction:d,
}))
}
}) })
} }
// 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);
// console.log("keydown")
update(){ update(){
if(this.check_ready())this.next_step();//2个蛇准备好了下一步-->走下一步 if(this.check_ready())this.next_step();//2个蛇准备好了下一步-->走下一步

@ -76,7 +76,7 @@ export class Snake extends AcGameObject{
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)) this.status = "die"; //死亡 // if (!this.gamemap.check_valid(this.next_cell)) this.status = "die"; //死亡
} }
//蛇是否增长 //蛇是否增长

@ -19,7 +19,11 @@ export default {
let canvas = ref(null); let canvas = ref(null);
onMounted(()=>{ onMounted(()=>{
new GameMap(canvas.value.getContext('2d'),parent.value,store); store.commit(
"updateGameObject",
new GameMap(canvas.value.getContext('2d'),parent.value,store)
);
// console.log(parent.value);//DOM // console.log(parent.value);//DOM
// console.log(canvas.value);//DOM // console.log(canvas.value);//DOM
}) })

@ -0,0 +1,71 @@
<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>

@ -7,6 +7,14 @@ export default {
opponent_username:"", opponent_username:"",
opponent_photo:"", opponent_photo:"",
gamemap:null, gamemap:null,
a_id:0,
a_sx:0,
a_sy:0,
b_id:0,
b_sx:0,
b_sy:0,
gameObject:null,
loser:"none",//none all A B
}, },
getters: { getters: {
}, },
@ -21,8 +29,20 @@ export default {
updateStatus(state,status){ updateStatus(state,status){
state.status = status; state.status = status;
}, },
updateGamemap(state,gamemap){ updateGame(state,game){
state.gamemap = gamemap ; 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 ;
},
updateGameObject(state,gameObject){
state.gameObject = gameObject ;
},
updateLoser(state,loser){
state.loser = loser ;
} }
}, },
actions: { actions: {

@ -1,20 +1,26 @@
<template> <template>
<PlayGround v-if="$store.state.pk.status === 'playing'"></PlayGround> <PlayGround v-if="$store.state.pk.status === 'playing'"></PlayGround>
<MatchGround v-if="$store.state.pk.status === 'matching'"></MatchGround> <MatchGround v-if="$store.state.pk.status === 'matching'"></MatchGround>
<ResultBoard v-if="$store.state.pk.loser !== 'none'"/>
</template> </template>
<script> <script>
import PlayGround from "@/components/PlayGround" import PlayGround from "@/components/PlayGround"
import MatchGround from "@/components/MatchGround" import MatchGround from "@/components/MatchGround"
import ResultBoard from "@/components/ResultBoard"
import { onMounted, onUnmounted } from "vue" import { onMounted, onUnmounted } from "vue"
import { useStore } from 'vuex' import { useStore } from 'vuex'
export default { export default {
components:{ components:{
PlayGround, PlayGround,
MatchGround, MatchGround,
ResultBoard,
}, },
setup(){ setup(){
const store = useStore(); const store = useStore();
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}/`;
@ -22,7 +28,7 @@ export default {
onMounted(()=>{ onMounted(()=>{
socket = new WebSocket(socketUrl);//Js socket = new WebSocket(socketUrl);//Js
// //
store.commit("updateOpponent",{ store.commit("updateOpponent",{
username:"我的对手", username:"我的对手",
@ -47,9 +53,32 @@ export default {
setTimeout(() => { setTimeout(() => {
store.commit("updateStatus","playing"); store.commit("updateStatus","playing");
}, 2000); }, 200);
store.commit("updateGame",data.game);
} else if (data.event === "move"){
const gameObject = store.state.pk.gameObject ;//snakes
const [snake0,snake1] = gameObject.snakes ;
snake0.set_direction(data.a_direction);
snake1.set_direction(data.b_direction);
} else if (data.event === "result"){
console.log(data)
const gameObject = store.state.pk.gameObject ;//snakes
const [snake0,snake1] = gameObject.snakes ;
if(data.loser === "all" || data.loser === "A"){
snake0.status = "die";
}
if(data.loser === "all" || data.loser === "B"){
snake1.status = "die";
}
store.commit("updateGamemap",data.gamemap); store.commit("updateLoser",data.loser);
} }
} }

Loading…
Cancel
Save