完善联机框架

This commit is contained in:
石皮幼鸟 2024-06-14 23:58:52 +08:00
parent 6cd918d669
commit 87cd436846
19 changed files with 338 additions and 66 deletions

View File

@ -1,6 +1,8 @@
package com.example.catchTheLetters.entity; package com.example.catchTheLetters.entity;
import com.example.catchTheLetters.enums.MessageType; import com.example.catchTheLetters.enums.MessageType;
import com.example.catchTheLetters.utils.GameMessageDeserializer;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import lombok.Data; import lombok.Data;
import java.io.Serializable; import java.io.Serializable;
@ -8,11 +10,12 @@ import java.io.Serializable;
/** /**
* 游戏消息 * 游戏消息
* *
* @auther spyn * @author spyn
*/ */
@Data @Data
@JsonDeserialize(using = GameMessageDeserializer.class)
public class GameMessage<T> implements Serializable { public class GameMessage<T> implements Serializable {
private String roomId; private long roomId;
private MessageType type; private MessageType type;
private T data; private T data;
@ -21,9 +24,12 @@ public class GameMessage<T> implements Serializable {
this.data = data; this.data = data;
} }
public GameMessage(String roomId, MessageType type, T data) { public GameMessage(long roomId, MessageType type, T data) {
this.roomId = roomId; this.roomId = roomId;
this.type = type; this.type = type;
this.data = data; this.data = data;
} }
public GameMessage() {
}
} }

View File

@ -0,0 +1,18 @@
package com.example.catchTheLetters.entity;
import com.example.catchTheLetters.enums.HealthActionType;
import lombok.Data;
import java.io.Serializable;
/**
* 血量操作
*
* @auther spyn
*/
@Data
public class HealthAction implements Serializable {
private HealthActionType type;
private int health;
private String userId;
}

View File

@ -0,0 +1,19 @@
package com.example.catchTheLetters.entity;
import com.example.catchTheLetters.enums.LetterActionType;
import com.example.catchTheLetters.model.vo.Letter;
import lombok.Data;
import java.io.Serializable;
/**
* 字母操作
*
* @author spyn
*/
@Data
public class LetterAction implements Serializable {
private LetterActionType type;
private Letter letter;
private String userId;
}

View File

@ -7,7 +7,7 @@ import java.io.Serializable;
/** /**
* 游戏中的玩家 * 游戏中的玩家
* *
* @auther spyn * @author spyn
*/ */
@Data @Data
public class PlayerInGame implements Serializable { public class PlayerInGame implements Serializable {

View File

@ -15,4 +15,7 @@ public class PlayerInput implements Serializable {
this.key = key; this.key = key;
this.state = state; this.state = state;
} }
public PlayerInput() {
}
} }

View File

@ -0,0 +1,16 @@
package com.example.catchTheLetters.entity;
import com.example.catchTheLetters.enums.RoomActionType;
import lombok.Data;
import java.io.Serializable;
@Data
public class RoomAction implements Serializable {
private RoomActionType type;
// 创建或加入房间时的token
private String token;
// 房主踢出玩家时或被邀请的玩家ID
private String userID;
private long roomID;
}

View File

@ -0,0 +1,16 @@
package com.example.catchTheLetters.entity;
import lombok.Data;
import java.io.Serializable;
/**
* 分数操作
*
* @author spyn
*/
@Data
public class ScoreAction implements Serializable {
private String userId;
private int score;
}

View File

@ -0,0 +1,11 @@
package com.example.catchTheLetters.enums;
/**
* 血量操作类型
*
* @author spyn
*/
public enum HealthActionType {
CHANGE,
DEAD
}

View File

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

View File

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

View File

@ -0,0 +1,11 @@
package com.example.catchTheLetters.enums;
/**
* 字母操作类型
*
* @author spyn
*/
public enum LetterActionType {
CREATE,
GET
}

View File

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

View File

@ -0,0 +1,15 @@
package com.example.catchTheLetters.enums;
/**
* 房间操作
*
* @author spyn
*/
public enum RoomActionType {
CREATE,
JOIN,
KICK,
LEAVE,
START,
INVITE
}

View File

@ -6,12 +6,6 @@ package com.example.catchTheLetters.enums;
* @author spyn * @author spyn
*/ */
public enum RoomStatus { public enum RoomStatus {
WAITING("waiting"), WAITING,
PLAYING("playing"); PLAYING
private final String status;
RoomStatus(String status) {
this.status = status;
}
} }

View File

@ -1,10 +1,20 @@
package com.example.catchTheLetters.handler; package com.example.catchTheLetters.handler;
import com.example.catchTheLetters.entity.GameMessage;
import com.example.catchTheLetters.entity.PlayerInput;
import com.example.catchTheLetters.enums.MessageType;
import com.example.catchTheLetters.service.RoomService;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.annotation.Resource;
import org.jetbrains.annotations.NotNull;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.web.socket.TextMessage; import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession; import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler; import org.springframework.web.socket.handler.TextWebSocketHandler;
import java.io.IOException;
/** /**
* WebSocket 处理器 * WebSocket 处理器
* *
@ -13,8 +23,41 @@ import org.springframework.web.socket.handler.TextWebSocketHandler;
@Controller @Controller
public class WebSocketHandler extends TextWebSocketHandler { public class WebSocketHandler extends TextWebSocketHandler {
private final ObjectMapper objectMapper = new ObjectMapper();
@Resource
private RoomService roomService;
@Override @Override
public void handleTextMessage(WebSocketSession session, TextMessage message) { public void handleTextMessage(@NotNull WebSocketSession session, TextMessage message) {
// 处理接收到的消息 // 处理接收到的消息
GameMessage<?> gameMessage;
try {
gameMessage = objectMapper.readValue(message.getPayload(), GameMessage.class);
} catch (JsonProcessingException e) {
roomService.sendMessage(session, new GameMessage<>(MessageType.ERROR, "消息解析失败"));
return;
}
switch (gameMessage.getType()) {
case INPUT:
// 处理输入消息
roomService.handleInput(gameMessage.getRoomId(), session, (PlayerInput) gameMessage.getData());
break;
case ROOM:
// 处理房间消息
break;
case LETTER:
// 处理字母消息
break;
case SCORE:
// 处理分数消息
break;
case HEALTH:
// 处理生命值消息
break;
default:
roomService.sendMessage(session, new GameMessage<>(MessageType.ERROR, "未知的消息类型"));
}
} }
} }

View File

@ -24,6 +24,14 @@ public interface RoomService {
*/ */
void removePlayer(long roomId, WebSocketSession session); void removePlayer(long roomId, WebSocketSession session);
/**
* 踢出玩家
* @param roomId 房间号
* @param session 房主的 WebSocket 会话
* @param playerID 要踢出的玩家 ID
*/
void kickPlayer(long roomId, WebSocketSession session, String playerID);
/** /**
* 开始游戏 * 开始游戏
* @param roomId 房间号 * @param roomId 房间号
@ -64,4 +72,33 @@ public interface RoomService {
* @param roomId 房间号 * @param roomId 房间号
*/ */
void removeRoom(long roomId); void removeRoom(long roomId);
/**
* 邀请玩家
* @param roomId 房间号
* @param session WebSocket 会话
* @param playerID 被邀请的玩家ID
*/
void invitePlayer(long roomId, WebSocketSession session, String playerID);
/**
* 连接ws
* @param session WebSocket 会话
* @param token 玩家 token
*/
void connect(WebSocketSession session, String token);
/**
* 断开ws
* @param token 玩家 token
*/
void disconnect(String token);
/**
* 发送消息
* @param session WebSocket 会话
* @param message 消息
* @param <T> 消息类型
*/
<T> void sendMessage(WebSocketSession session, T message);
} }

View File

@ -2,6 +2,7 @@ package com.example.catchTheLetters.service.impl;
import com.example.catchTheLetters.entity.*; import com.example.catchTheLetters.entity.*;
import com.example.catchTheLetters.enums.MessageType; import com.example.catchTheLetters.enums.MessageType;
import com.example.catchTheLetters.enums.RoomActionType;
import com.example.catchTheLetters.enums.RoomStatus; import com.example.catchTheLetters.enums.RoomStatus;
import com.example.catchTheLetters.model.vo.Letter; import com.example.catchTheLetters.model.vo.Letter;
import com.example.catchTheLetters.service.AuthService; import com.example.catchTheLetters.service.AuthService;
@ -18,12 +19,20 @@ import java.util.Map;
import java.util.Random; import java.util.Random;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
/**
* 房间服务实现类
*
* @author spyn
*/
@Service @Service
public class RoomServiceImpl implements RoomService { public class RoomServiceImpl implements RoomService {
// 房间列表 // 房间列表
private final ConcurrentHashMap<Long, GameRoom> rooms = new ConcurrentHashMap<>(); private final ConcurrentHashMap<Long, GameRoom> rooms = new ConcurrentHashMap<>();
// 玩家列表
private final ConcurrentHashMap<String, WebSocketSession> playerInGame = new ConcurrentHashMap<>();
private final ObjectMapper objectMapper = new ObjectMapper(); private final ObjectMapper objectMapper = new ObjectMapper();
private final Random random = new Random(); private final Random random = new Random();
@ -70,6 +79,24 @@ public class RoomServiceImpl implements RoomService {
// TODO 发送消息通知其他玩家有人退出了 // TODO 发送消息通知其他玩家有人退出了
} }
@Override
public void kickPlayer(long roomId, WebSocketSession session, String playerID) {
var room = rooms.get(roomId);
// 如果不是房主返回错误信息
if (session != room.getHost()) {
sendMessage(session, new GameMessage<>(MessageType.ERROR, "你不是房主"));
return;
}
var players = room.getPlayers();
for (var player : players.entrySet()) {
if (player.getValue().getUserId().equals(playerID)) {
players.remove(player.getKey());
// TODO 发送消息通知有玩家被踢出了
break;
}
}
}
@Override @Override
public void startGame(long roomId, WebSocketSession session) { public void startGame(long roomId, WebSocketSession session) {
var room = rooms.get(roomId); var room = rooms.get(roomId);
@ -129,6 +156,53 @@ public class RoomServiceImpl implements RoomService {
rooms.remove(roomId); rooms.remove(roomId);
} }
@Override
public void invitePlayer(long roomId, WebSocketSession session, String playerID) {
var roomAction = new RoomAction();
roomAction.setType(RoomActionType.INVITE);
roomAction.setRoomID(roomId);
roomAction.setUserID(playerID);
var aimSession = playerInGame.get(playerID);
// 如果玩家在线发送邀请消息
if (aimSession != null) sendMessage(session, new GameMessage<>(MessageType.ROOM, roomAction));
else sendMessage(session, new GameMessage<>(MessageType.ERROR, "玩家不在线"));
}
@Override
public void connect(WebSocketSession session, String token) {
var player = authService.verify(token);
playerInGame.put(player.getId(), session);
}
@Override
public void disconnect(String token) {
var player = authService.verify(token);
var session = playerInGame.remove(player.getId());
try {
session.close();
} catch (IOException e) {
throw new RuntimeException("关闭WebSocket会话失败", e);
}
}
@Override
public <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 generateLetter(GameRoom room) { private void generateLetter(GameRoom room) {
var players = room.getPlayers(); var players = room.getPlayers();
var words = room.getWords(); var words = room.getWords();
@ -156,22 +230,6 @@ public class RoomServiceImpl implements RoomService {
sendMessage(player, new GameMessage<>(MessageType.LETTER, letter)); 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) { private void getWords(Map<String, Integer> words) {
// TODO 从数据库中获取一批随机单词然后放入words中并把单词数组推送给所有玩家 // TODO 从数据库中获取一批随机单词然后放入words中并把单词数组推送给所有玩家
} }

View File

@ -0,0 +1,36 @@
package com.example.catchTheLetters.utils;
import com.example.catchTheLetters.entity.*;
import com.example.catchTheLetters.enums.MessageType;
import com.fasterxml.jackson.core.JacksonException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
/**
* 游戏消息反序列化
*
* @author spyn
*/
public class GameMessageDeserializer extends JsonDeserializer<GameMessage<?>> {
private final ObjectMapper objectMapper = new ObjectMapper();
@Override
public GameMessage<?> deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JacksonException {
var node = jsonParser.getCodec().readTree(jsonParser);
var type = objectMapper.convertValue(node.get("type"), JsonNode.class);
var data = objectMapper.convertValue(node.get("data"), JsonNode.class);
return switch (type.asText()) {
case "INPUT" -> new GameMessage<>(MessageType.INPUT, objectMapper.treeToValue(data, PlayerInput.class));
case "ROOM" -> new GameMessage<>(MessageType.ROOM, objectMapper.treeToValue(data, RoomAction.class));
case "LETTER" -> new GameMessage<>(MessageType.LETTER, objectMapper.treeToValue(data, LetterAction.class));
case "SCORE" -> new GameMessage<>(MessageType.SCORE, objectMapper.treeToValue(data, ScoreAction.class));
case "HEALTH" -> new GameMessage<>(MessageType.HEALTH, objectMapper.treeToValue(data, HealthAction.class));
default -> throw new IllegalArgumentException("未知的消息类型");
};
}
}

View File

@ -1,8 +1,10 @@
package com.example.catchTheLetters; package com.example.catchTheLetters;
import com.example.catchTheLetters.entity.GameMessage;
import com.example.catchTheLetters.entity.PlayerInput; import com.example.catchTheLetters.entity.PlayerInput;
import com.example.catchTheLetters.enums.InputKeyType; import com.example.catchTheLetters.enums.InputKeyType;
import com.example.catchTheLetters.enums.InputState; import com.example.catchTheLetters.enums.InputState;
import com.example.catchTheLetters.enums.MessageType;
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@ -13,8 +15,17 @@ class JSONTest {
private final ObjectMapper objectMapper = new ObjectMapper(); private final ObjectMapper objectMapper = new ObjectMapper();
@Test @Test
void contextLoads() throws JsonProcessingException { void enumToJSON() throws JsonProcessingException {
var input = new PlayerInput(InputKeyType.LEFT, InputState.PRESS); var input = new PlayerInput(InputKeyType.LEFT, InputState.PRESS);
System.out.println(objectMapper.writeValueAsString(input)); System.out.println(objectMapper.writeValueAsString(new GameMessage<>(MessageType.INPUT, input)));
}
@Test
void jsonToEnum() throws JsonProcessingException {
var json = "{\"type\":\"INPUT\",\"data\":{\"key\":\"LEFT\",\"state\":\"PRESS\"}}";
var message = objectMapper.readValue(json, GameMessage.class);
System.out.println(message);
System.out.println(message.getType() == MessageType.INPUT);
System.out.println(message.getData());
} }
} }