多人对战ws框架
This commit is contained in:
parent
b9126ef3ab
commit
673a277ea9
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 = "";
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
package com.example.catchTheLetters.enums;
|
||||
|
||||
public enum Status {
|
||||
WAITING, PLAYING, END
|
||||
}
|
|
@ -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 {
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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>=玩家数(有可能中途有人退出),目前单词出队列,继续下一个单词
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue