parent
42074fb132
commit
1b4d8d808a
13 changed files with 507 additions and 71 deletions
@ -0,0 +1,18 @@ |
|||||||
|
package com.kob.backend.config; |
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean; |
||||||
|
import org.springframework.context.annotation.Configuration; |
||||||
|
import org.springframework.web.socket.server.standard.ServerEndpointExporter; |
||||||
|
|
||||||
|
/** |
||||||
|
* @author zfp |
||||||
|
*/ |
||||||
|
@Configuration |
||||||
|
public class WebSocketConfig { |
||||||
|
|
||||||
|
@Bean |
||||||
|
public ServerEndpointExporter serverEndpointExporter() { |
||||||
|
|
||||||
|
return new ServerEndpointExporter(); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,146 @@ |
|||||||
|
package com.kob.backend.consumer; |
||||||
|
|
||||||
|
import com.alibaba.fastjson.JSONObject; |
||||||
|
import com.kob.backend.consumer.utils.Game; |
||||||
|
import com.kob.backend.consumer.utils.JwtAuthentication; |
||||||
|
import com.kob.backend.controller.pojo.User; |
||||||
|
import com.kob.backend.mapper.UserMapper; |
||||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||||
|
import org.springframework.stereotype.Component; |
||||||
|
|
||||||
|
import javax.websocket.*; |
||||||
|
import javax.websocket.server.PathParam; |
||||||
|
import javax.websocket.server.ServerEndpoint; |
||||||
|
import java.io.IOException; |
||||||
|
import java.util.Iterator; |
||||||
|
import java.util.concurrent.ConcurrentHashMap; |
||||||
|
import java.util.concurrent.CopyOnWriteArraySet; |
||||||
|
|
||||||
|
/** |
||||||
|
* @author zfp |
||||||
|
*/ |
||||||
|
@Component |
||||||
|
@ServerEndpoint("/websocket/{token}") // 注意不要以'/'结尾
|
||||||
|
public class WebSocketServer { |
||||||
|
|
||||||
|
// 保存所有用户对应的请求链接
|
||||||
|
// 线程安全的一个HashMap
|
||||||
|
final private static ConcurrentHashMap<Integer,WebSocketServer> users = new ConcurrentHashMap<>(); |
||||||
|
|
||||||
|
// 保存当前的匹配池
|
||||||
|
|
||||||
|
final private static CopyOnWriteArraySet<User> matchpool = new CopyOnWriteArraySet<>(); |
||||||
|
// 发起请求链接对应的用户
|
||||||
|
private User user; |
||||||
|
private Session session = null; |
||||||
|
|
||||||
|
// 注入userMapper(和之前的有点不一样)
|
||||||
|
private static UserMapper userMapper; |
||||||
|
|
||||||
|
// // 保存地图
|
||||||
|
// private Game game = null;
|
||||||
|
|
||||||
|
@Autowired |
||||||
|
public void setUserMapper(UserMapper userMapper) { |
||||||
|
WebSocketServer.userMapper = userMapper; |
||||||
|
} |
||||||
|
|
||||||
|
@OnOpen |
||||||
|
public void onOpen(Session session, @PathParam("token") String token) throws IOException { |
||||||
|
// 建立连接
|
||||||
|
this.session = session; |
||||||
|
|
||||||
|
// 根据token查询用户id(JWT验证)
|
||||||
|
Integer userId = JwtAuthentication.getUserId(token); |
||||||
|
// 根据userId查找对应的用户
|
||||||
|
this.user = userMapper.selectById(userId); |
||||||
|
// 连接成功
|
||||||
|
if (this.user != null) { |
||||||
|
users.put(userId,this); |
||||||
|
System.out.println("Connected Success!"); |
||||||
|
}else { |
||||||
|
this.session.close(); |
||||||
|
} |
||||||
|
|
||||||
|
System.out.println(users); |
||||||
|
} |
||||||
|
|
||||||
|
@OnClose |
||||||
|
public void onClose() { |
||||||
|
// 关闭链接
|
||||||
|
System.out.println("Disconnected!"); |
||||||
|
if (this.user != null) { |
||||||
|
users.remove(this.user.getId()); |
||||||
|
matchpool.remove(this.user); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private void startMatching() { |
||||||
|
System.out.println("start-----matching----"); |
||||||
|
matchpool.add(this.user); |
||||||
|
System.out.println(matchpool.size()); |
||||||
|
|
||||||
|
while (matchpool.size() >= 2) { |
||||||
|
Iterator<User> it = matchpool.iterator(); |
||||||
|
User a = it.next(), b = it.next(); |
||||||
|
matchpool.remove(a); |
||||||
|
matchpool.remove(b); |
||||||
|
|
||||||
|
Game game = new Game(13,14,20); |
||||||
|
game.createMap(); |
||||||
|
|
||||||
|
JSONObject respA = new JSONObject(); |
||||||
|
respA.put("event","start-matching"); |
||||||
|
respA.put("opponent_username",b.getUsername()); |
||||||
|
respA.put("opponent_photo",b.getPhoto()); |
||||||
|
respA.put("gamemap",game.getG()); |
||||||
|
|
||||||
|
users.get(a.getId()).sendMessage(respA.toJSONString()); |
||||||
|
|
||||||
|
JSONObject respB = new JSONObject(); |
||||||
|
respB.put("event","start-matching"); |
||||||
|
respB.put("opponent_username",a.getUsername()); |
||||||
|
respB.put("opponent_photo",a.getPhoto()); |
||||||
|
respB.put("gamemap",game.getG()); |
||||||
|
|
||||||
|
users.get(b.getId()).sendMessage(respB.toJSONString()); |
||||||
|
|
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private void stopMatching() { |
||||||
|
System.out.println("stop-----matching----"); |
||||||
|
matchpool.remove(this.user); |
||||||
|
} |
||||||
|
|
||||||
|
@OnMessage |
||||||
|
public void onMessage(String message, Session session) { |
||||||
|
// 从Client接收消息(读取前端发来的信息)
|
||||||
|
System.out.println("Received the message!"); |
||||||
|
JSONObject data = JSONObject.parseObject(message); |
||||||
|
String event = data.getString("event"); |
||||||
|
|
||||||
|
if ("start-matching".equals(event)) { |
||||||
|
startMatching(); |
||||||
|
} |
||||||
|
else if ("stop-matching".equals(event)){ |
||||||
|
stopMatching(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@OnError |
||||||
|
public void onError(Session session, Throwable error) { |
||||||
|
error.printStackTrace(); |
||||||
|
} |
||||||
|
|
||||||
|
public void sendMessage(String msg) { |
||||||
|
// 给前端发送信息
|
||||||
|
synchronized (this.session) { |
||||||
|
try { |
||||||
|
this.session.getBasicRemote().sendText(msg); |
||||||
|
} catch (IOException e) { |
||||||
|
e.printStackTrace(); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,97 @@ |
|||||||
|
package com.kob.backend.consumer.utils; |
||||||
|
|
||||||
|
import java.util.Random; |
||||||
|
|
||||||
|
/** |
||||||
|
* @author zfp |
||||||
|
*/ |
||||||
|
public class Game { |
||||||
|
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}; |
||||||
|
final private static int[] dy = {0,1,0,-1}; |
||||||
|
|
||||||
|
public Game(Integer rows,Integer cols,Integer inner_walls_count) { |
||||||
|
this.rows = rows; |
||||||
|
this.cols = cols; |
||||||
|
this.inner_walls_count = inner_walls_count; |
||||||
|
this.g = new int[rows][cols]; |
||||||
|
} |
||||||
|
|
||||||
|
public int[][] getG() { |
||||||
|
return g; |
||||||
|
} |
||||||
|
|
||||||
|
private boolean check_connectivity(int sx,int sy,int tx,int ty) { |
||||||
|
if (sx == tx && sy == ty) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
g[sx][sy] = 1; |
||||||
|
for (int i = 0; i < 4; i++) { |
||||||
|
int x = sx + dx[i] , y = sy + dy[i]; |
||||||
|
if (x >= 0 && x < this.rows && y >= 0 && y < this.cols && g[x][y] == 0 ) { |
||||||
|
if (check_connectivity(x,y,tx,ty)) { |
||||||
|
g[sx][sy] = 0; |
||||||
|
return true; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
g[sx][sy] = 0; |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
// 绘制地图是否成功
|
||||||
|
private boolean draw() { |
||||||
|
for (int i = 0; i < this.rows; i++) { |
||||||
|
for (int j = 0; j < this.cols; j++) { |
||||||
|
// 0表示空地,1表示障碍物
|
||||||
|
g[i][j] = 0; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// 四周加上障碍物
|
||||||
|
|
||||||
|
for (int r = 0; r < this.rows; r++) { |
||||||
|
g[r][0] = g[r][this.cols - 1] = 1; |
||||||
|
} |
||||||
|
|
||||||
|
for (int c = 0; c < this.cols; c++) { |
||||||
|
g[0][c] = g[this.rows - 1][c] = 1; |
||||||
|
} |
||||||
|
|
||||||
|
// 创建随机障碍物
|
||||||
|
|
||||||
|
Random random = new Random(); |
||||||
|
|
||||||
|
for (int i = 0; i < this.inner_walls_count / 2; i++) { |
||||||
|
for (int j = 0; j < 1000; j++) { |
||||||
|
int r = random.nextInt(this.rows); |
||||||
|
int c = random.nextInt(this.cols); |
||||||
|
// 判断对称的位置是否有障碍物
|
||||||
|
if (g[r][c] == 1 || g[this.rows - 1 - r][this.cols - 1 - c] == 1){ |
||||||
|
continue; |
||||||
|
} |
||||||
|
// 不要生成到蛇初始的位置上
|
||||||
|
if (r == this.rows - 2 && c == 1 || r == 1 && c == this.cols - 2) { |
||||||
|
continue; |
||||||
|
} |
||||||
|
// 生成的位置没有问题
|
||||||
|
g[r][c] = g[this.rows - 1 - r][this.cols - 1 - c] = 1; |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
return this.check_connectivity(this.rows - 2, 1,1,this.cols - 2); |
||||||
|
} |
||||||
|
|
||||||
|
public void createMap() { |
||||||
|
// 最多随机1000次,判断能够生成有效的地图
|
||||||
|
for (int i = 0; i < 1000; i++) { |
||||||
|
if (draw()) { |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,21 @@ |
|||||||
|
package com.kob.backend.consumer.utils; |
||||||
|
|
||||||
|
import com.kob.backend.utils.JwtUtil; |
||||||
|
import io.jsonwebtoken.Claims; |
||||||
|
|
||||||
|
/** |
||||||
|
* @author zfp |
||||||
|
*/ |
||||||
|
public class JwtAuthentication { |
||||||
|
public static Integer getUserId(String token) { |
||||||
|
// 返回值为-1时表示不存在
|
||||||
|
int userId = -1; |
||||||
|
try { |
||||||
|
Claims claims = JwtUtil.parseJWT(token); |
||||||
|
userId = Integer.parseInt(claims.getSubject()); |
||||||
|
} catch (Exception e) { |
||||||
|
throw new RuntimeException(e); |
||||||
|
} |
||||||
|
return userId; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,100 @@ |
|||||||
|
<template> |
||||||
|
<div class="matchground"> |
||||||
|
<div class="row"> |
||||||
|
<div class="col-6"> |
||||||
|
<div class="user_photo"> |
||||||
|
<img :src="$store.state.user.photo" alt=""> |
||||||
|
</div> |
||||||
|
<div class="username"> |
||||||
|
<span class="info">用户名: </span>{{ $store.state.user.username }} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="col-6"> |
||||||
|
<div class="user_photo"> |
||||||
|
<img :src="$store.state.pk.opponent_photo" alt=""> |
||||||
|
</div> |
||||||
|
<div class="username"> |
||||||
|
<span class="info">用户名: </span>{{ $store.state.pk.opponent_username }} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="col-12" style="text-align: center; padding-top: 10vh;"> |
||||||
|
<button type="button" v-if="is_matched" class="btn btn-success btn-lg" @click="click_match_btn">{{match_btn_info}}</button> |
||||||
|
<button type="button" v-else class="btn btn-danger btn-lg" @click="click_match_btn">{{match_btn_info}}</button> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
import { ref } from 'vue' |
||||||
|
import {useStore} from 'vuex' |
||||||
|
|
||||||
|
export default { |
||||||
|
setup() { |
||||||
|
const store = useStore(); |
||||||
|
|
||||||
|
let match_btn_info = ref("开始匹配"); |
||||||
|
let is_matched = ref(true); |
||||||
|
|
||||||
|
const click_match_btn = () => { |
||||||
|
if (match_btn_info.value === "开始匹配") { |
||||||
|
match_btn_info.value = "取消匹配"; |
||||||
|
is_matched.value = false; |
||||||
|
// 给后端发送一个json信息表示当前属于哪个事件 |
||||||
|
store.state.pk.socket.send(JSON.stringify({ |
||||||
|
event: "start-matching", |
||||||
|
})); |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}else { |
||||||
|
|
||||||
|
match_btn_info.value = "开始匹配"; |
||||||
|
is_matched.value = true; |
||||||
|
store.state.pk.socket.send(JSON.stringify({ |
||||||
|
event: "stop-matching", |
||||||
|
})); |
||||||
|
} |
||||||
|
} |
||||||
|
return { |
||||||
|
match_btn_info, |
||||||
|
is_matched, |
||||||
|
click_match_btn, |
||||||
|
} |
||||||
|
}, |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
|
||||||
|
<style scoped> |
||||||
|
.matchground { |
||||||
|
width: 60vw; |
||||||
|
height: 70vh; |
||||||
|
margin: 40px auto; |
||||||
|
background-color: rgba(123, 197, 212, 0.5); |
||||||
|
|
||||||
|
} |
||||||
|
.info { |
||||||
|
font-size: 24px; |
||||||
|
color: black; |
||||||
|
} |
||||||
|
|
||||||
|
img { |
||||||
|
margin-top: 10vh; |
||||||
|
border-radius: 50%; |
||||||
|
width: 20vh; |
||||||
|
} |
||||||
|
|
||||||
|
.user_photo { |
||||||
|
text-align: center; |
||||||
|
} |
||||||
|
|
||||||
|
.username { |
||||||
|
margin-top: 40px; |
||||||
|
font-size: 24px; |
||||||
|
font-weight: bold; |
||||||
|
color:crimson; |
||||||
|
text-align: center; |
||||||
|
} |
||||||
|
|
||||||
|
</style> |
@ -0,0 +1,32 @@ |
|||||||
|
|
||||||
|
export default { |
||||||
|
state: { // 全局变量
|
||||||
|
status: "matching", // matching表示匹配界面,playing表示对战界面
|
||||||
|
socket: null, |
||||||
|
opponent_username: "", //对手的用户名
|
||||||
|
opponent_photo: "", // 对手的头像
|
||||||
|
gamemap: null, // 对战的地图
|
||||||
|
}, |
||||||
|
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,gamemap) { |
||||||
|
state.gamemap = gamemap; |
||||||
|
}, |
||||||
|
}, |
||||||
|
actions: { // 在actions中调用修改全局变量的函数
|
||||||
|
|
||||||
|
}, |
||||||
|
modules: { |
||||||
|
} |
||||||
|
} |
@ -1,16 +1,70 @@ |
|||||||
<template> |
<template> |
||||||
<PlayGround/> |
<!-- status等于playing才进入对战界面 --> |
||||||
|
<PlayGround v-if="$store.state.pk.status === 'playing'" /> |
||||||
|
<MatchGround v-if="$store.state.pk.status === 'matching'" /> |
||||||
</template> |
</template> |
||||||
|
|
||||||
<script> |
<script> |
||||||
import PlayGround from '../../components/PlayGround.vue' |
import PlayGround from "../../components/PlayGround.vue"; |
||||||
|
import MatchGround from "../../components/MatchGround.vue"; |
||||||
|
import { onMounted, onUnmounted } from "vue"; // 加载组件和卸载组件时 |
||||||
|
|
||||||
|
import { useStore } from "vuex"; |
||||||
|
|
||||||
export default { |
export default { |
||||||
components: { |
components: { |
||||||
PlayGround |
PlayGround, |
||||||
} |
MatchGround, |
||||||
|
}, |
||||||
|
setup() { |
||||||
|
const store = useStore(); |
||||||
|
const socket_url = `ws://localhost:3000/websocket/${store.state.user.token}/`; |
||||||
|
let socket = null; |
||||||
|
// 当前组件被加载(点击pk页面)时执行该函数 |
||||||
|
// onMounted和setup作用相似 |
||||||
|
onMounted(() => { |
||||||
|
store.commit("updateOpponent", { |
||||||
|
username: "my opponent", |
||||||
|
photo: |
||||||
|
"https://cdn.acwing.com/media/article/image/2022/08/09/1_1db2488f17-anonymous.png", |
||||||
|
}); |
||||||
|
// 建立一个socket链接 |
||||||
|
socket = new WebSocket(socket_url); |
||||||
|
|
||||||
|
socket.onopen = () => { |
||||||
|
store.commit("updateSocket", socket); |
||||||
|
console.log("connnected!"); |
||||||
|
}; |
||||||
|
// 接收到后端发来的信息 |
||||||
|
socket.onmessage = (msg) => { |
||||||
|
const data = JSON.parse(msg.data); |
||||||
|
// 匹配成功 |
||||||
|
if (data.event === "start-matching") { |
||||||
|
console.log(data) |
||||||
|
store.commit("updateOpponent", { |
||||||
|
username: data.opponent_username, |
||||||
|
photo: data.opponent_photo, |
||||||
|
}); |
||||||
|
setTimeout(() => { |
||||||
|
store.commit("updateStatus", "playing"); |
||||||
|
}, 2000); |
||||||
|
|
||||||
|
// 更新地图 |
||||||
|
store.commit("updateGameMap", data.gamemap); |
||||||
} |
} |
||||||
|
}; |
||||||
|
|
||||||
|
socket.onclose = () => { |
||||||
|
console.log("disconnected!"); |
||||||
|
store.commit("updateStatus", "matching"); |
||||||
|
}; |
||||||
|
}); |
||||||
|
// 切换其他页面或者关闭pk页面时调用 |
||||||
|
onUnmounted(() => { |
||||||
|
socket.close(); |
||||||
|
}); |
||||||
|
}, |
||||||
|
}; |
||||||
</script> |
</script> |
||||||
|
|
||||||
<style scoped> |
<style scoped></style> |
||||||
</style> |
|
||||||
|
Loading…
Reference in new issue