多人对战ws框架

This commit is contained in:
石皮幼鸟 2024-06-14 18:51:41 +08:00
parent b9126ef3ab
commit 673a277ea9
15 changed files with 465 additions and 44 deletions

View File

@ -7,6 +7,11 @@ import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
/**
* WebSocket 配置
*
* @author spyn
*/
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

View File

@ -0,0 +1,29 @@
package com.example.catchTheLetters.entity;
import com.example.catchTheLetters.enums.MessageType;
import lombok.Data;
import java.io.Serializable;
/**
* 游戏消息
*
* @auther spyn
*/
@Data
public class GameMessage<T> implements Serializable {
private String roomId;
private MessageType type;
private T data;
public GameMessage(MessageType type, T data) {
this.type = type;
this.data = data;
}
public GameMessage(String roomId, MessageType type, T data) {
this.roomId = roomId;
this.type = type;
this.data = data;
}
}

View File

@ -1,38 +1,32 @@
package com.example.catchTheLetters.entity;
import com.example.catchTheLetters.enums.Status;
import com.example.catchTheLetters.model.vo.Letter;
import cn.hutool.core.collection.ConcurrentHashSet;
import com.example.catchTheLetters.enums.RoomStatus;
import lombok.Data;
import org.springframework.web.socket.WebSocketSession;
import java.util.ArrayList;
import java.util.List;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentSkipListMap;
/**
* 游戏房间
*
* @author spyn
*/
@Data
public class GameRoom {
private List<WebSocketSession> players = new ArrayList<>();
private Status status = Status.WAITING;
private final Map<WebSocketSession, PlayerInGame> players = new ConcurrentHashMap<>();
private final Set<WebSocketSession> readyPlayers = new ConcurrentHashSet<>();
private RoomStatus status = RoomStatus.WAITING;
private long roomId;
private WebSocketSession host;
// 单词 : 目前在拼这个单词的玩家数
private final Map<String, Integer> words = new ConcurrentSkipListMap<>();
public void addPlayer(WebSocketSession session) {
players.add(session);
}
public void removePlayer(WebSocketSession session) {
players.remove(session);
}
public void startGame() {
status = Status.PLAYING;
}
public void endGame() {
status = Status.END;
}
public void handleInput(WebSocketSession session, String input) {
// 处理玩家输入
}
public Letter generateLetter() {
// 生成字母
return null;
public GameRoom(WebSocketSession host, PlayerInGame player) {
this.roomId = UUID.randomUUID().getLeastSignificantBits();
this.host = host;
players.put(host, player);
}
}

View File

@ -0,0 +1,29 @@
package com.example.catchTheLetters.entity;
import lombok.Data;
import java.io.Serializable;
/**
* 游戏中的玩家
*
* @auther spyn
*/
@Data
public class PlayerInGame implements Serializable {
private String userId;
private User user;
private int score;
private int health;
private String currentWord;
private String currentAnswer;
public PlayerInGame(String userId, User user) {
this.userId = userId;
this.user = user;
this.score = 0;
this.health = 100;
this.currentWord = "";
this.currentAnswer = "";
}
}

View File

@ -0,0 +1,18 @@
package com.example.catchTheLetters.entity;
import com.example.catchTheLetters.enums.InputKeyType;
import com.example.catchTheLetters.enums.InputState;
import lombok.Data;
import java.io.Serializable;
@Data
public class PlayerInput implements Serializable {
public InputKeyType key;
public InputState state;
public PlayerInput(InputKeyType key, InputState state) {
this.key = key;
this.state = state;
}
}

View File

@ -0,0 +1,21 @@
package com.example.catchTheLetters.enums;
import lombok.Getter;
/**
* 输入类型
*
* @author spyn
*/
@Getter
public enum InputKeyType {
LEFT("left"),
RIGHT("right"),
SHIFT("shift");
private final String key;
InputKeyType(String key) {
this.key = key;
}
}

View File

@ -0,0 +1,20 @@
package com.example.catchTheLetters.enums;
import lombok.Getter;
/**
* 输入状态
*
* @author spyn
*/
@Getter
public enum InputState {
PRESS("press"),
RELEASE("release");
private final String state;
InputState(String state) {
this.state = state;
}
}

View File

@ -0,0 +1,20 @@
package com.example.catchTheLetters.enums;
/**
* WebSocket 消息类型
*
* @auther spyn
*/
public enum MessageType {
INPUT("input"),
ROOM("room"),
LETTER("letter"),
SCORE("score"),
HEALTH("health");
private final String type;
MessageType(String type) {
this.type = type;
}
}

View File

@ -0,0 +1,17 @@
package com.example.catchTheLetters.enums;
/**
* 房间状态
*
* @author spyn
*/
public enum RoomStatus {
WAITING("waiting"),
PLAYING("playing");
private final String status;
RoomStatus(String status) {
this.status = status;
}
}

View File

@ -1,5 +0,0 @@
package com.example.catchTheLetters.enums;
public enum Status {
WAITING, PLAYING, END
}

View File

@ -5,6 +5,11 @@ import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;
/**
* WebSocket 处理器
*
* @author spyn
*/
@Controller
public class WebSocketHandler extends TextWebSocketHandler {

View File

@ -1,5 +1,13 @@
package com.example.catchTheLetters.model.vo;
import lombok.Data;
/**
* 字母
*
* @author spyn
*/
@Data
public class Letter {
// 字母值如果是10则是加血
private String letterVal;
@ -7,9 +15,17 @@ public class Letter {
// 下落速度
private float speed = 3f;
// 字母的x坐标
// 字母的x坐标0-1之间
private float x;
// 字母的y坐标
private float y;
public Letter(String letterVal, float x) {
this.letterVal = letterVal;
this.x = x;
}
public Letter(String letterVal, float x, float speed) {
this.letterVal = letterVal;
this.x = x;
this.speed = speed;
}
}

View File

@ -1,18 +1,67 @@
package com.example.catchTheLetters.service;
import com.example.catchTheLetters.model.vo.Letter;
import com.example.catchTheLetters.entity.PlayerInput;
import org.springframework.web.socket.WebSocketSession;
/**
* 房间服务
*
* @author spyn
*/
public interface RoomService {
void addPlayer(WebSocketSession session);
/**
* 添加玩家
* @param roomId 房间号
* @param session WebSocket 会话
* @param token 玩家 token
*/
void addPlayer(long roomId, WebSocketSession session, String token);
void removePlayer(WebSocketSession session);
/**
* 移除玩家
* @param roomId 房间号
* @param session WebSocket 会话
*/
void removePlayer(long roomId, WebSocketSession session);
void startGame();
/**
* 开始游戏
* @param roomId 房间号
* @param session WebSocket 会话
*/
void startGame(long roomId, WebSocketSession session);
void endGame();
/**
* 取消开始游戏
* @param roomId 房间号
* @param session WebSocket 会话
*/
void cancelStartGame(long roomId, WebSocketSession session);
void handleInput(WebSocketSession session, String input);
/**
* 结束游戏
* @param roomId 房间号
*/
void endGame(long roomId);
Letter generateLetter();
/**
* 处理玩家输入
* @param roomId 房间号
* @param session WebSocket 会话
* @param input 玩家输入
*/
void handleInput(long roomId, WebSocketSession session, PlayerInput input);
/**
* 创建房间
* @param session WebSocket 会话
* @param token 玩家 token
*/
void createRoom(WebSocketSession session, String token);
/**
* 移除房间
* @param roomId 房间号
*/
void removeRoom(long roomId);
}

View File

@ -0,0 +1,183 @@
package com.example.catchTheLetters.service.impl;
import com.example.catchTheLetters.entity.*;
import com.example.catchTheLetters.enums.MessageType;
import com.example.catchTheLetters.enums.RoomStatus;
import com.example.catchTheLetters.model.vo.Letter;
import com.example.catchTheLetters.service.AuthService;
import com.example.catchTheLetters.service.RoomService;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.annotation.Resource;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.stereotype.Service;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import java.io.IOException;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;
@Service
public class RoomServiceImpl implements RoomService {
// 房间列表
private final ConcurrentHashMap<Long, GameRoom> rooms = new ConcurrentHashMap<>();
private final ObjectMapper objectMapper = new ObjectMapper();
private final Random random = new Random();
@Resource
private MongoTemplate mongoTemplate;
@Resource
private AuthService authService;
@Override
public void addPlayer(long roomId, WebSocketSession session, String token) {
var room = rooms.get(roomId);
var players = room.getPlayers();
var user = authService.verify(token);
var player = new PlayerInGame(user.getId(), user);
// 如果玩家已经在房间中更新他们的WebSocketSession
for (var entry : players.entrySet()) {
if (entry.getValue().getUserId().equals(user.getId())) {
player = entry.getValue();
players.remove(entry.getKey());
break;
}
}
players.put(session, player);
// TODO 发送消息通知其他玩家有人加入了
}
@Override
public void removePlayer(long roomId, WebSocketSession session) {
var room = rooms.get(roomId);
var players = room.getPlayers();
players.remove(session);
// 如果是房主退出更换房主否则关闭房间
if (session == room.getHost()) {
if (!players.isEmpty()) {
room.setHost(players.keySet().iterator().next());
} else {
rooms.remove(roomId);
}
}
// TODO 发送消息通知其他玩家有人退出了
}
@Override
public void startGame(long roomId, WebSocketSession session) {
var room = rooms.get(roomId);
var players = room.getPlayers();
var readyPlayers = room.getReadyPlayers();
readyPlayers.add(session);
// TODO 发送消息通知其他玩家有人准备好了
// 如果所有玩家都准备好了开始游戏
if (readyPlayers.size() == room.getPlayers().size()) {
// TODO 发送消息通知所有玩家游戏开始
readyPlayers.clear();
room.setStatus(RoomStatus.PLAYING);
gameLogic(room);
}
}
@Override
public void cancelStartGame(long roomId, WebSocketSession session) {
var room = rooms.get(roomId);
var readyPlayers = room.getReadyPlayers();
readyPlayers.remove(session);
// TODO 发送消息通知其他玩家有人取消准备
}
@Override
public void endGame(long roomId) {
var room = rooms.get(roomId);
room.setStatus(RoomStatus.WAITING);
// TODO 发送消息通知所有玩家游戏结束
}
@Override
public void handleInput(long roomId, WebSocketSession session, PlayerInput input) {
var message = new GameMessage<>(MessageType.INPUT, input);
var room = rooms.get(roomId);
var players = room.getPlayers();
for (var player : players.keySet())
if (player != session) sendMessage(player, message);
}
@Override
public void createRoom(WebSocketSession session, String token) {
var user = authService.verify(token);
var player = new PlayerInGame(user.getId(), user);
var room = new GameRoom(session, player);
rooms.put(room.getRoomId(), room);
}
@Override
public void removeRoom(long roomId) {
// 如果房间内还有玩家全部移除
var room = rooms.get(roomId);
var players = room.getPlayers();
for (var player : players.keySet()) {
// TODO 发送消息通知所有玩家房间已解散
}
rooms.remove(roomId);
}
private void generateLetter(GameRoom room) {
var players = room.getPlayers();
var words = room.getWords();
// 如果words长度<=5从数据库中再获取一批随机单词
if (words.size() <= 5) getWords(words);
Letter letter;
var val = random.nextInt(100);
// 在80%概率当前单词的字母中随机选择一个19%随机生成一个字母1%是回血爱心选定后随机赋值2f到6f的下落速度
if (val == 0)
// 回血爱心
letter = new Letter("10", random.nextFloat(1), random.nextFloat(2, 6));
if (val >= 1 && val <= 81)
// 从当前单词中随机选择一个字母
letter = new Letter(words.keySet().toArray()[random.nextInt(words.size())].toString(), random.nextFloat(1), random.nextFloat(2, 6));
else
// 随机生成一个字母
letter = new Letter(String.valueOf((char) (random.nextInt(26) + 'a')), random.nextFloat(1), random.nextFloat(2, 6));
// 给所有玩家发送字母
for (var player : players.keySet())
sendMessage(player, new GameMessage<>(MessageType.LETTER, letter));
}
private <T> void sendMessage(WebSocketSession session, T message) {
String json;
if (!(message instanceof String)) {
try {
json = objectMapper.writeValueAsString(message);
} catch (Exception e) {
throw new RuntimeException("消息转为JSON字符串操作失败", e);
}
} else json = (String) message;
try {
session.sendMessage(new TextMessage(json));
} catch (IOException e) {
throw new RuntimeException("发送消息失败", e);
}
}
private void getWords(Map<String, Integer> words) {
// TODO 从数据库中获取一批随机单词然后放入words中并把单词数组推送给所有玩家
}
private void gameLogic(GameRoom room) {
getWords(room.getWords());
// TODO 从300秒开始倒计时每秒调用一次generateLetter方法如果时间到了调用endGame方法如果当前单词被某玩家拼完Map对应单词的value++如果value>=玩家数有可能中途有人退出目前单词出队列继续下一个单词
}
}

View File

@ -0,0 +1,20 @@
package com.example.catchTheLetters;
import com.example.catchTheLetters.entity.PlayerInput;
import com.example.catchTheLetters.enums.InputKeyType;
import com.example.catchTheLetters.enums.InputState;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class JSONTest {
private final ObjectMapper objectMapper = new ObjectMapper();
@Test
void contextLoads() throws JsonProcessingException {
var input = new PlayerInput(InputKeyType.LEFT, InputState.PRESS);
System.out.println(objectMapper.writeValueAsString(input));
}
}