完善联机框架
This commit is contained in:
parent
6cd918d669
commit
87cd436846
|
@ -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() {
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -7,7 +7,7 @@ import java.io.Serializable;
|
|||
/**
|
||||
* 游戏中的玩家
|
||||
*
|
||||
* @auther spyn
|
||||
* @author spyn
|
||||
*/
|
||||
@Data
|
||||
public class PlayerInGame implements Serializable {
|
||||
|
|
|
@ -15,4 +15,7 @@ public class PlayerInput implements Serializable {
|
|||
this.key = key;
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
public PlayerInput() {
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
package com.example.catchTheLetters.enums;
|
||||
|
||||
/**
|
||||
* 血量操作类型
|
||||
*
|
||||
* @author spyn
|
||||
*/
|
||||
public enum HealthActionType {
|
||||
CHANGE,
|
||||
DEAD
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
package com.example.catchTheLetters.enums;
|
||||
|
||||
/**
|
||||
* 字母操作类型
|
||||
*
|
||||
* @author spyn
|
||||
*/
|
||||
public enum LetterActionType {
|
||||
CREATE,
|
||||
GET
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
package com.example.catchTheLetters.enums;
|
||||
|
||||
/**
|
||||
* 房间操作
|
||||
*
|
||||
* @author spyn
|
||||
*/
|
||||
public enum RoomActionType {
|
||||
CREATE,
|
||||
JOIN,
|
||||
KICK,
|
||||
LEAVE,
|
||||
START,
|
||||
INVITE
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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, "未知的消息类型"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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中,并把单词数组推送给所有玩家
|
||||
}
|
||||
|
|
|
@ -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("未知的消息类型");
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue