完善联机框架

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

View File

@ -15,4 +15,7 @@ public class PlayerInput implements Serializable {
this.key = key;
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;
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;
}
LEFT,
RIGHT,
SHIFT,
SPACE
}

View File

@ -1,20 +1,11 @@
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;
}
PRESS,
RELEASE
}

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 消息类型
*
* @auther spyn
* @author spyn
*/
public enum MessageType {
INPUT("input"),
ROOM("room"),
LETTER("letter"),
SCORE("score"),
HEALTH("health");
private final String type;
MessageType(String type) {
this.type = type;
}
INPUT,
ROOM,
LETTER,
SCORE,
HEALTH,
ERROR
}

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
*/
public enum RoomStatus {
WAITING("waiting"),
PLAYING("playing");
private final String status;
RoomStatus(String status) {
this.status = status;
}
WAITING,
PLAYING
}

View File

@ -1,10 +1,20 @@
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.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;
import java.io.IOException;
/**
* WebSocket 处理器
*
@ -13,8 +23,41 @@ import org.springframework.web.socket.handler.TextWebSocketHandler;
@Controller
public class WebSocketHandler extends TextWebSocketHandler {
private final ObjectMapper objectMapper = new ObjectMapper();
@Resource
private RoomService roomService;
@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);
/**
* 踢出玩家
* @param roomId 房间号
* @param session 房主的 WebSocket 会话
* @param playerID 要踢出的玩家 ID
*/
void kickPlayer(long roomId, WebSocketSession session, String playerID);
/**
* 开始游戏
* @param roomId 房间号
@ -64,4 +72,33 @@ public interface RoomService {
* @param 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.enums.MessageType;
import com.example.catchTheLetters.enums.RoomActionType;
import com.example.catchTheLetters.enums.RoomStatus;
import com.example.catchTheLetters.model.vo.Letter;
import com.example.catchTheLetters.service.AuthService;
@ -18,12 +19,20 @@ import java.util.Map;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;
/**
* 房间服务实现类
*
* @author spyn
*/
@Service
public class RoomServiceImpl implements RoomService {
// 房间列表
private final ConcurrentHashMap<Long, GameRoom> rooms = new ConcurrentHashMap<>();
// 玩家列表
private final ConcurrentHashMap<String, WebSocketSession> playerInGame = new ConcurrentHashMap<>();
private final ObjectMapper objectMapper = new ObjectMapper();
private final Random random = new Random();
@ -70,6 +79,24 @@ public class RoomServiceImpl implements RoomService {
// 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
public void startGame(long roomId, WebSocketSession session) {
var room = rooms.get(roomId);
@ -129,6 +156,53 @@ public class RoomServiceImpl implements RoomService {
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) {
var players = room.getPlayers();
var words = room.getWords();
@ -156,22 +230,6 @@ public class RoomServiceImpl implements RoomService {
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中并把单词数组推送给所有玩家
}

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;
import com.example.catchTheLetters.entity.GameMessage;
import com.example.catchTheLetters.entity.PlayerInput;
import com.example.catchTheLetters.enums.InputKeyType;
import com.example.catchTheLetters.enums.InputState;
import com.example.catchTheLetters.enums.MessageType;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
@ -13,8 +15,17 @@ class JSONTest {
private final ObjectMapper objectMapper = new ObjectMapper();
@Test
void contextLoads() throws JsonProcessingException {
void enumToJSON() throws JsonProcessingException {
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());
}
}