1
This commit is contained in:
@@ -1,30 +1,52 @@
|
||||
package com.zombie.game;
|
||||
|
||||
import com.zombie.game.server.GameWebSocketServer;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* 游戏服务器主入口类
|
||||
*
|
||||
* 职责:
|
||||
* 1. 解析命令行参数(端口号)
|
||||
* 2. 启动 WebSocket 游戏服务器
|
||||
* 3. 注册 JVM 关闭钩子,确保服务器优雅退出
|
||||
*/
|
||||
public class GameServerMain {
|
||||
private static final Logger logger = LoggerFactory.getLogger(GameServerMain.class);
|
||||
|
||||
/**
|
||||
* 程序入口方法
|
||||
*
|
||||
* @param args 命令行参数,第一个参数可选为端口号(默认 8080)
|
||||
*/
|
||||
public static void main(String[] args) {
|
||||
// 默认监听端口
|
||||
int port = 8080;
|
||||
|
||||
// 如果传入了端口号参数,尝试解析
|
||||
if (args.length > 0) {
|
||||
try {
|
||||
port = Integer.parseInt(args[0]);
|
||||
} catch (NumberFormatException e) {
|
||||
System.err.println("Invalid port number, using default: 8080");
|
||||
logger.warn("Invalid port number, using default: 8080");
|
||||
}
|
||||
}
|
||||
|
||||
// 创建并启动 WebSocket 服务器
|
||||
GameWebSocketServer server = new GameWebSocketServer(port);
|
||||
server.start();
|
||||
|
||||
System.out.println("Zombie Crisis 3 Server started on port " + port);
|
||||
System.out.println("Press Ctrl+C to stop the server");
|
||||
logger.info("Zombie Crisis 3 Server started on port {}", port);
|
||||
logger.info("Press Ctrl+C to stop the server");
|
||||
|
||||
// 注册关闭钩子:当 JVM 退出时(Ctrl+C 或系统信号),优雅地停止服务器
|
||||
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
|
||||
System.out.println("Shutting down server...");
|
||||
logger.info("Shutting down server...");
|
||||
try {
|
||||
server.stop();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
logger.error("Error during server shutdown", e);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
package com.zombie.game.model;
|
||||
|
||||
import lombok.Getter;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* 子弹/投掷物类
|
||||
*
|
||||
* 管理玩家和僵尸发射的子弹、手榴弹等投掷物。
|
||||
* 支持普通子弹的直线飞行和手榴弹的抛物线轨迹。
|
||||
*/
|
||||
@Getter
|
||||
public class Bullet {
|
||||
private int id;
|
||||
private float x, y;
|
||||
@@ -44,6 +52,19 @@ public class Bullet {
|
||||
this.targetY = y + (float) Math.cos(angle) * range;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造函数 - 手榴弹
|
||||
*
|
||||
* @param id 子弹ID
|
||||
* @param startX 起始X坐标
|
||||
* @param startY 起始Y坐标
|
||||
* @param targetX 目标X坐标
|
||||
* @param targetY 目标Y坐标
|
||||
* @param flightDuration 飞行时长
|
||||
* @param damage 伤害值
|
||||
* @param ownerId 发射者ID
|
||||
* @param explosionRadius 爆炸半径
|
||||
*/
|
||||
public Bullet(int id, float startX, float startY, float targetX, float targetY,
|
||||
float flightDuration, int damage, String ownerId, float explosionRadius) {
|
||||
this.id = id;
|
||||
@@ -71,21 +92,6 @@ public class Bullet {
|
||||
this.vz = 3.0f;
|
||||
}
|
||||
|
||||
public int getId() { return id; }
|
||||
public float getX() { return x; }
|
||||
public float getY() { return y; }
|
||||
public float getZ() { return z; }
|
||||
public float getVx() { return vx; }
|
||||
public float getVy() { return vy; }
|
||||
public int getDamage() { return damage; }
|
||||
public String getOwnerId() { return ownerId; }
|
||||
public String getWeapon() { return weapon; }
|
||||
public boolean isExplosive() { return explosive; }
|
||||
public float getExplosionRadius() { return explosionRadius; }
|
||||
public float getTargetX() { return targetX; }
|
||||
public float getTargetY() { return targetY; }
|
||||
public boolean isGrenade() { return isGrenade; }
|
||||
|
||||
public boolean update(float dt, GameMap map) {
|
||||
if (isGrenade) {
|
||||
flightTime += dt;
|
||||
@@ -123,6 +129,14 @@ public class Bullet {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测子弹是否命中实体
|
||||
*
|
||||
* @param ex 实体X坐标
|
||||
* @param ey 实体Y坐标
|
||||
* @param size 实体碰撞体大小
|
||||
* @return true 表示命中
|
||||
*/
|
||||
public boolean hitsEntity(float ex, float ey, float size) {
|
||||
float dx = x - ex;
|
||||
float dy = y - ey;
|
||||
@@ -130,6 +144,11 @@ public class Bullet {
|
||||
return dist < size / 2 + 0.1f;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将子弹状态转换为Map格式,用于网络传输
|
||||
*
|
||||
* @return 包含子弹状态的Map
|
||||
*/
|
||||
public Map<String, Object> toStateMap() {
|
||||
Map<String, Object> map = new LinkedHashMap<>();
|
||||
map.put("id", id);
|
||||
|
||||
@@ -1,56 +1,141 @@
|
||||
package com.zombie.game.model;
|
||||
|
||||
/**
|
||||
* 游戏常量定义类
|
||||
*
|
||||
* 集中管理所有游戏平衡性参数、消息类型常量。
|
||||
* 修改此文件中的数值可以调整游戏难度和行为。
|
||||
*/
|
||||
public class Constants {
|
||||
|
||||
// ==================== 地图与基础参数 ====================
|
||||
|
||||
/** 地图网格尺寸(32×32 格子) */
|
||||
public static final int GRID_SIZE = 32;
|
||||
/** 玩家碰撞体大小 */
|
||||
public static final float PLAYER_SIZE = 0.8f;
|
||||
/** 僵尸碰撞体大小 */
|
||||
public static final float ZOMBIE_SIZE = 0.8f;
|
||||
/** 服务器逻辑帧率(每秒 tick 数) */
|
||||
public static final int TICK_RATE = 30;
|
||||
/** 每次 tick 的时间间隔(秒) */
|
||||
public static final float TICK_INTERVAL = 1.0f / TICK_RATE;
|
||||
|
||||
public static final float PLAYER_SPEED = 5.0f;
|
||||
public static final float PLAYER_MAX_HEALTH = 100;
|
||||
public static final float PLAYER_INVULNERABLE_TIME = 0.5f;
|
||||
// ==================== 玩家参数 ====================
|
||||
|
||||
/** 玩家移动速度(格/秒) */
|
||||
public static final float PLAYER_SPEED = 5.0f;
|
||||
/** 玩家最大生命值 */
|
||||
public static final float PLAYER_MAX_HEALTH = 100;
|
||||
/** 玩家受伤后的无敌时间(秒),防止连续受伤 */
|
||||
public static final float PLAYER_INVULNERABLE_TIME = 0.5f;
|
||||
/** 玩家死亡后的重生等待时间(秒) */
|
||||
public static final float PLAYER_RESPAWN_TIME = 30.0f;
|
||||
|
||||
// ==================== 普通僵尸参数 ====================
|
||||
|
||||
/** 普通僵尸基础生命值 */
|
||||
public static final float ZOMBIE_BASE_HEALTH = 100;
|
||||
/** 普通僵尸基础移动速度(格/秒) */
|
||||
public static final float ZOMBIE_BASE_SPEED = 2.0f;
|
||||
/** 普通僵尸近战伤害 */
|
||||
public static final float ZOMBIE_DAMAGE = 10;
|
||||
/** 普通僵尸近战攻击间隔(秒) */
|
||||
public static final float ZOMBIE_ATTACK_RATE = 1.0f;
|
||||
/** 僵尸死亡后掉落物品的概率 */
|
||||
public static final float ZOMBIE_LOOT_DROP_CHANCE = 0.3f;
|
||||
/** 僵尸生成间隔基础值(秒),难度提升后会逐渐缩短 */
|
||||
public static final float ZOMBIE_SPAWN_INTERVAL_BASE = 3.0f;
|
||||
public static final float ZOMBIE_SPAWN_INTERVAL_MIN = 0.8f;
|
||||
/** 僵尸生成间隔最小值(秒),防止生成过快 */
|
||||
public static final float ZOMBIE_SPAWN_INTERVAL_MIN = 0.5f;
|
||||
/** 每次难度提升增加的僵尸生命值 */
|
||||
public static final float ZOMBIE_HEALTH_INCREASE = 20;
|
||||
/** 每次难度提升增加的僵尸速度 */
|
||||
public static final float ZOMBIE_SPEED_INCREASE = 0.1f;
|
||||
/** 难度提升的时间间隔(秒) */
|
||||
public static final float DIFFICULTY_INCREASE_INTERVAL = 30.0f;
|
||||
|
||||
// ==================== 精英僵尸参数 ====================
|
||||
// 精英僵尸:血量高、可远程射击的强力僵尸
|
||||
|
||||
/** 精英僵尸生命值 */
|
||||
public static final float ELITE_ZOMBIE_HEALTH = 800;
|
||||
/** 精英僵尸移动速度(比普通僵尸稍慢) */
|
||||
public static final float ELITE_ZOMBIE_SPEED = 1.5f;
|
||||
/** 精英僵尸近战伤害 */
|
||||
public static final float ELITE_ZOMBIE_DAMAGE = 20;
|
||||
/** 精英僵尸远程攻击范围(格) */
|
||||
public static final float ELITE_ZOMBIE_ATTACK_RANGE = 8.0f;
|
||||
/** 精英僵尸远程攻击间隔(秒) */
|
||||
public static final float ELITE_ZOMBIE_ATTACK_RATE = 2.0f;
|
||||
/** 精英僵尸远程子弹伤害 */
|
||||
public static final int ELITE_ZOMBIE_BULLET_DAMAGE = 30;
|
||||
/** 精英僵尸远程子弹飞行速度 */
|
||||
public static final float ELITE_ZOMBIE_BULLET_SPEED = 6.0f;
|
||||
/** 精英僵尸的生成概率(5%) */
|
||||
public static final float ELITE_ZOMBIE_SPAWN_CHANCE = 0.05f;
|
||||
|
||||
// ==================== 分裂僵尸参数 ====================
|
||||
// 分裂僵尸:被击杀后分裂成多个普通小僵尸
|
||||
|
||||
/** 分裂僵尸生命值(较低,但击杀后会分裂) */
|
||||
public static final float SPLITTER_ZOMBIE_HEALTH = 50;
|
||||
/** 分裂僵尸移动速度(比普通僵尸快) */
|
||||
public static final float SPLITTER_ZOMBIE_SPEED = 2.5f;
|
||||
/** 分裂僵尸死亡后最少分裂数量 */
|
||||
public static final int SPLITTER_ZOMBIE_MIN_SPLIT = 2;
|
||||
/** 分裂僵尸死亡后最多分裂数量 */
|
||||
public static final int SPLITTER_ZOMBIE_MAX_SPLIT = 6;
|
||||
/** 分裂僵尸的生成概率(5%) */
|
||||
public static final float SPLITTER_ZOMBIE_SPAWN_CHANCE = 0.05f;
|
||||
|
||||
// ==================== 武器类型 ====================
|
||||
|
||||
/** 手枪(初始武器,弹药无限) */
|
||||
public static final String WEAPON_PISTOL = "pistol";
|
||||
/** 机枪(高射速,弹药有限) */
|
||||
public static final String WEAPON_MACHINE_GUN = "machine_gun";
|
||||
/** 霰弹枪(散射多弹丸,近距离高伤害) */
|
||||
public static final String WEAPON_SHOTGUN = "shotgun";
|
||||
/** 手榴弹(可蓄力投掷,爆炸范围伤害) */
|
||||
public static final String WEAPON_GRENADE = "grenade";
|
||||
|
||||
// ==================== 掉落物类型 ====================
|
||||
|
||||
/** 弹药补给 */
|
||||
public static final String LOOT_TYPE_AMMO = "ammo";
|
||||
/** 医疗包 */
|
||||
public static final String LOOT_TYPE_HEALTH = "health";
|
||||
/** 医疗包恢复的生命值 */
|
||||
public static final float LOOT_HEALTH_AMOUNT = 30;
|
||||
|
||||
// ==================== 网络消息类型 ====================
|
||||
// 客户端与服务器之间通信的消息类型标识
|
||||
|
||||
/** 创建房间 */
|
||||
public static final String MSG_CREATE_ROOM = "create_room";
|
||||
/** 加入房间 */
|
||||
public static final String MSG_JOIN_ROOM = "join_room";
|
||||
/** 离开房间 */
|
||||
public static final String MSG_LEAVE_ROOM = "leave_room";
|
||||
/** 房间列表 */
|
||||
public static final String MSG_ROOM_LIST = "room_list";
|
||||
/** 房间状态更新 */
|
||||
public static final String MSG_ROOM_STATE = "room_state";
|
||||
/** 玩家准备/取消准备 */
|
||||
public static final String MSG_READY = "ready";
|
||||
/** 开始游戏 */
|
||||
public static final String MSG_START_GAME = "start_game";
|
||||
/** 游戏已开始(服务器→客户端) */
|
||||
public static final String MSG_GAME_STARTED = "game_started";
|
||||
/** 玩家输入(移动、射击等操作) */
|
||||
public static final String MSG_PLAYER_INPUT = "player_input";
|
||||
/** 游戏状态同步(服务器→客户端,每 tick 发送) */
|
||||
public static final String MSG_GAME_STATE = "game_state";
|
||||
/** 玩家加入通知 */
|
||||
public static final String MSG_PLAYER_JOIN = "player_join";
|
||||
/** 玩家离开通知 */
|
||||
public static final String MSG_PLAYER_LEAVE = "player_leave";
|
||||
/** 错误消息 */
|
||||
public static final String MSG_ERROR = "error";
|
||||
}
|
||||
|
||||
230
backend/src/main/java/com/zombie/game/model/FlowField.java
Normal file
230
backend/src/main/java/com/zombie/game/model/FlowField.java
Normal file
@@ -0,0 +1,230 @@
|
||||
package com.zombie.game.model;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* 流场导航类
|
||||
*
|
||||
* 基于 BFS 算法计算距离场和流场,用于僵尸寻路。
|
||||
* 支持加权障碍物(如坚果墙体),僵尸可自动权衡绕道 vs 摧毁。
|
||||
*/
|
||||
public class FlowField {
|
||||
/** 地图宽度 */
|
||||
private final int width;
|
||||
/** 地图高度 */
|
||||
private final int height;
|
||||
/** 距离场:记录每个格子到最近玩家的距离 */
|
||||
private float[][] distanceField;
|
||||
/** 流场X方向:僵尸移动的X方向分量 */
|
||||
private float[][] flowFieldX;
|
||||
/** 流场Y方向:僵尸移动的Y方向分量 */
|
||||
private float[][] flowFieldY;
|
||||
/** 流场是否有效 */
|
||||
private boolean valid;
|
||||
|
||||
public FlowField(int width, int height) {
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.distanceField = new float[height][width];
|
||||
this.flowFieldX = new float[height][width];
|
||||
this.flowFieldY = new float[height][width];
|
||||
this.valid = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新流场导航
|
||||
*
|
||||
* 基于玩家位置计算流场,使用 Dijkstra 算法(支持加权格子)。
|
||||
*
|
||||
* @param walls 墙体集合,用于查询格子通行代价
|
||||
* @param playerPositions 玩家位置列表
|
||||
*/
|
||||
public void update(Map<String, Wall> walls, List<float[]> playerPositions) {
|
||||
// 初始化距离场
|
||||
for (int y = 0; y < height; y++) {
|
||||
for (int x = 0; x < width; x++) {
|
||||
distanceField[y][x] = Float.MAX_VALUE;
|
||||
}
|
||||
}
|
||||
|
||||
// 优先队列实现 Dijkstra
|
||||
PriorityQueue<Node> queue = new PriorityQueue<>(Comparator.comparingDouble(n -> n.dist));
|
||||
|
||||
for (float[] pos : playerPositions) {
|
||||
int px = (int) Math.floor(pos[0]);
|
||||
int py = (int) Math.floor(pos[1]);
|
||||
if (px >= 0 && px < width && py >= 0 && py < height) {
|
||||
String key = key(px, py);
|
||||
Wall wall = walls.get(key);
|
||||
if (wall == null || !wall.blocksMovement()) {
|
||||
distanceField[py][px] = 0;
|
||||
queue.add(new Node(px, py, 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int[][] dirs = {{0, -1}, {0, 1}, {-1, 0}, {1, 0}, {-1, -1}, {1, -1}, {-1, 1}, {1, 1}};
|
||||
|
||||
while (!queue.isEmpty()) {
|
||||
Node current = queue.poll();
|
||||
int cx = current.x;
|
||||
int cy = current.y;
|
||||
float currentDist = current.dist;
|
||||
|
||||
if (currentDist > distanceField[cy][cx]) continue;
|
||||
|
||||
for (int[] dir : dirs) {
|
||||
int nx = cx + dir[0];
|
||||
int ny = cy + dir[1];
|
||||
|
||||
if (nx < 0 || nx >= width || ny < 0 || ny >= height) continue;
|
||||
|
||||
if (dir[0] != 0 && dir[1] != 0) {
|
||||
if (isBlocked(walls, cx + dir[0], cy) || isBlocked(walls, cx, cy + dir[1])) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
float moveCost = getMovementCost(walls, nx, ny, dir);
|
||||
if (moveCost == Float.MAX_VALUE) continue;
|
||||
|
||||
float newDist = currentDist + moveCost;
|
||||
|
||||
if (newDist < distanceField[ny][nx]) {
|
||||
distanceField[ny][nx] = newDist;
|
||||
queue.add(new Node(nx, ny, newDist));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 计算流场方向
|
||||
for (int y = 0; y < height; y++) {
|
||||
for (int x = 0; x < width; x++) {
|
||||
if (isBlocked(walls, x, y) || distanceField[y][x] == Float.MAX_VALUE) {
|
||||
flowFieldX[y][x] = 0;
|
||||
flowFieldY[y][x] = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
float bestDist = distanceField[y][x];
|
||||
float bestDirX = 0;
|
||||
float bestDirY = 0;
|
||||
|
||||
for (int[] dir : dirs) {
|
||||
int nx = x + dir[0];
|
||||
int ny = y + dir[1];
|
||||
|
||||
if (nx < 0 || nx >= width || ny < 0 || ny >= height) continue;
|
||||
if (isBlocked(walls, nx, ny)) continue;
|
||||
|
||||
if (dir[0] != 0 && dir[1] != 0) {
|
||||
if (isBlocked(walls, x + dir[0], y) || isBlocked(walls, x, y + dir[1])) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (distanceField[ny][nx] < bestDist) {
|
||||
bestDist = distanceField[ny][nx];
|
||||
float len = (float) Math.sqrt(dir[0] * dir[0] + dir[1] * dir[1]);
|
||||
bestDirX = dir[0] / len;
|
||||
bestDirY = dir[1] / len;
|
||||
}
|
||||
}
|
||||
|
||||
flowFieldX[y][x] = bestDirX;
|
||||
flowFieldY[y][x] = bestDirY;
|
||||
}
|
||||
}
|
||||
|
||||
valid = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定位置的流场方向
|
||||
*
|
||||
* @param wx 世界X坐标
|
||||
* @param wy 世界Y坐标
|
||||
* @return 流场方向向量 [dx, dy]
|
||||
*/
|
||||
public float[] getDirection(float wx, float wy) {
|
||||
int gx = (int) Math.floor(wx);
|
||||
int gy = (int) Math.floor(wy);
|
||||
|
||||
if (gx < 0 || gx >= width || gy < 0 || gy >= height) {
|
||||
return new float[]{0, 0};
|
||||
}
|
||||
|
||||
return new float[]{flowFieldX[gy][gx], flowFieldY[gy][gx]};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定位置到最近玩家的距离
|
||||
*
|
||||
* @param wx 世界X坐标
|
||||
* @param wy 世界Y坐标
|
||||
* @return 距离值
|
||||
*/
|
||||
public float getDistance(float wx, float wy) {
|
||||
int gx = (int) Math.floor(wx);
|
||||
int gy = (int) Math.floor(wy);
|
||||
|
||||
if (gx < 0 || gx >= width || gy < 0 || gy >= height) {
|
||||
return Float.MAX_VALUE;
|
||||
}
|
||||
|
||||
return distanceField[gy][gx];
|
||||
}
|
||||
|
||||
/** 检查流场是否有效 */
|
||||
public boolean isValid() {
|
||||
return valid;
|
||||
}
|
||||
|
||||
/** 标记流场为无效(当障碍物变化时调用) */
|
||||
public void invalidate() {
|
||||
valid = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查格子是否被阻挡
|
||||
*/
|
||||
private boolean isBlocked(Map<String, Wall> walls, int gx, int gy) {
|
||||
if (gx < 0 || gx >= width || gy < 0 || gy >= height) return true;
|
||||
Wall wall = walls.get(key(gx, gy));
|
||||
return wall != null && wall.blocksMovement();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取格子的移动代价
|
||||
*/
|
||||
private float getMovementCost(Map<String, Wall> walls, int gx, int gy, int[] dir) {
|
||||
Wall wall = walls.get(key(gx, gy));
|
||||
float baseCost = (dir[0] != 0 && dir[1] != 0) ? 1.414f : 1.0f;
|
||||
if (wall == null || wall.isDestroyed()) {
|
||||
return baseCost;
|
||||
}
|
||||
float wallCost = wall.getMovementCost();
|
||||
if (wallCost == Float.MAX_VALUE) {
|
||||
return Float.MAX_VALUE;
|
||||
}
|
||||
return baseCost + wallCost - 1.0f; // wallCost 已包含基础代价
|
||||
}
|
||||
|
||||
private static String key(int x, int y) {
|
||||
return x + "," + y;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dijkstra 节点
|
||||
*/
|
||||
private static class Node {
|
||||
int x, y;
|
||||
float dist;
|
||||
|
||||
Node(int x, int y, float dist) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.dist = dist;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,32 +1,52 @@
|
||||
package com.zombie.game.model;
|
||||
|
||||
import lombok.Getter;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* 游戏地图类
|
||||
*
|
||||
* 管理游戏地图数据,包括:
|
||||
* - 地图格子(玩家出生点、僵尸出生点)
|
||||
* - 墙体管理(StaticWall、NutWall)
|
||||
* - A* 寻路算法
|
||||
*/
|
||||
@Getter
|
||||
public class GameMap {
|
||||
/** 地图格子数据:0=空地, 2=玩家出生点, 3=僵尸出生点 */
|
||||
private final int[][] cells;
|
||||
/** 地图宽度 */
|
||||
private final int width;
|
||||
/** 地图高度 */
|
||||
private final int height;
|
||||
private float[][] distanceField;
|
||||
private float[][] flowFieldX;
|
||||
private float[][] flowFieldY;
|
||||
private boolean flowFieldValid;
|
||||
/** 墙体集合:key="x,y" */
|
||||
private final Map<String, Wall> walls;
|
||||
/** 流场导航 */
|
||||
private final FlowField flowField;
|
||||
|
||||
/**
|
||||
* 构造函数 - 初始化地图并生成默认布局
|
||||
*/
|
||||
public GameMap() {
|
||||
this.width = Constants.GRID_SIZE;
|
||||
this.height = Constants.GRID_SIZE;
|
||||
this.cells = new int[height][width];
|
||||
this.distanceField = new float[height][width];
|
||||
this.flowFieldX = new float[height][width];
|
||||
this.flowFieldY = new float[height][width];
|
||||
this.flowFieldValid = false;
|
||||
this.walls = new HashMap<>();
|
||||
this.flowField = new FlowField(width, height);
|
||||
generateDefaultMap();
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成默认地图布局
|
||||
*
|
||||
* 创建边界墙壁、内部障碍物、玩家出生点和僵尸出生点
|
||||
*/
|
||||
private void generateDefaultMap() {
|
||||
for (int y = 0; y < height; y++) {
|
||||
for (int x = 0; x < width; x++) {
|
||||
if (x == 0 || x == width - 1 || y == 0 || y == height - 1) {
|
||||
cells[y][x] = 1;
|
||||
cells[y][x] = 0;
|
||||
walls.put(key(x, y), new StaticWall(x, y));
|
||||
} else {
|
||||
cells[y][x] = 0;
|
||||
}
|
||||
@@ -45,13 +65,13 @@ public class GameMap {
|
||||
if (seg[0] == seg[2]) {
|
||||
for (int y = seg[1]; y <= seg[3]; y++) {
|
||||
if (seg[0] > 0 && seg[0] < width - 1 && y > 0 && y < height - 1) {
|
||||
cells[y][seg[0]] = 1;
|
||||
walls.put(key(seg[0], y), new StaticWall(seg[0], y));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (int x = seg[0]; x <= seg[2]; x++) {
|
||||
if (x > 0 && x < width - 1 && seg[1] > 0 && seg[1] < height - 1) {
|
||||
cells[seg[1]][x] = 1;
|
||||
walls.put(key(x, seg[1]), new StaticWall(x, seg[1]));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -64,7 +84,7 @@ public class GameMap {
|
||||
for (int dx = -1; dx <= 1; dx++) {
|
||||
int ny = sp[1] + dy, nx = sp[0] + dx;
|
||||
if (ny > 0 && ny < height - 1 && nx > 0 && nx < width - 1) {
|
||||
if (cells[ny][nx] == 1) cells[ny][nx] = 0;
|
||||
walls.remove(key(nx, ny));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -76,7 +96,7 @@ public class GameMap {
|
||||
for (int dx = -1; dx <= 1; dx++) {
|
||||
int ny = sp[1] + dy, nx = sp[0] + dx;
|
||||
if (ny > 0 && ny < height - 1 && nx > 0 && nx < width - 1) {
|
||||
if (cells[ny][nx] == 1) cells[ny][nx] = 0;
|
||||
walls.remove(key(nx, ny));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -89,18 +109,84 @@ public class GameMap {
|
||||
for (int dx = -1; dx <= 1; dx++) {
|
||||
int ny = sp[1] + dy, nx = sp[0] + dx;
|
||||
if (ny > 0 && ny < height - 1 && nx > 0 && nx < width - 1) {
|
||||
if (cells[ny][nx] == 1) cells[ny][nx] = 0;
|
||||
walls.remove(key(nx, ny));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isWall(int gx, int gy) {
|
||||
if (gx < 0 || gx >= width || gy < 0 || gy >= height) return true;
|
||||
return cells[gy][gx] == 1;
|
||||
/**
|
||||
* 添加坚果墙体
|
||||
*
|
||||
* @param gx 格子X坐标
|
||||
* @param gy 格子Y坐标
|
||||
* @return true 表示添加成功
|
||||
*/
|
||||
public boolean addNutWall(int gx, int gy) {
|
||||
if (gx < 0 || gx >= width || gy < 0 || gy >= height) return false;
|
||||
if (cells[gy][gx] != 0) return false;
|
||||
if (walls.containsKey(key(gx, gy))) return false;
|
||||
walls.put(key(gx, gy), new NutWall(gx, gy));
|
||||
flowField.invalidate();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除墙体(当坚果被破坏时调用)
|
||||
*
|
||||
* @param gx 格子X坐标
|
||||
* @param gy 格子Y坐标
|
||||
*/
|
||||
public void removeWall(int gx, int gy) {
|
||||
walls.remove(key(gx, gy));
|
||||
flowField.invalidate();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定格子的墙体
|
||||
*
|
||||
* @param gx 格子X坐标
|
||||
* @param gy 格子Y坐标
|
||||
* @return 墙体对象,如果没有则返回 null
|
||||
*/
|
||||
public Wall getWall(int gx, int gy) {
|
||||
return walls.get(key(gx, gy));
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查指定格子是否为墙壁(静态或坚果)
|
||||
*
|
||||
* @param gx 格子X坐标
|
||||
* @param gy 格子Y坐标
|
||||
* @return true 表示是墙壁或越界
|
||||
*/
|
||||
public boolean isWall(int gx, int gy) {
|
||||
if (gx < 0 || gx >= width || gy < 0 || gy >= height) return true;
|
||||
Wall wall = walls.get(key(gx, gy));
|
||||
return wall != null && wall.blocksMovement();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查指定格子是否有可破坏的坚果墙体
|
||||
*
|
||||
* @param gx 格子X坐标
|
||||
* @param gy 格子Y坐标
|
||||
* @return true 表示有未破坏的坚果墙体
|
||||
*/
|
||||
public boolean isNutWall(int gx, int gy) {
|
||||
Wall wall = getWall(gx, gy);
|
||||
return wall instanceof NutWall && !wall.isDestroyed();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查指定世界坐标是否可通行
|
||||
*
|
||||
* @param wx 世界X坐标
|
||||
* @param wy 世界Y坐标
|
||||
* @param size 实体碰撞体大小
|
||||
* @return true 表示可通行
|
||||
*/
|
||||
public boolean isWalkable(float wx, float wy, float size) {
|
||||
float half = size / 2;
|
||||
float[][] corners = {
|
||||
@@ -119,6 +205,11 @@ public class GameMap {
|
||||
return cells;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有玩家出生点
|
||||
*
|
||||
* @return 出生点坐标列表
|
||||
*/
|
||||
public List<int[]> getSpawnPoints() {
|
||||
List<int[]> points = new ArrayList<>();
|
||||
for (int y = 0; y < height; y++) {
|
||||
@@ -131,6 +222,11 @@ public class GameMap {
|
||||
return points;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有僵尸出生点
|
||||
*
|
||||
* @return 僵尸出生点坐标列表
|
||||
*/
|
||||
public List<int[]> getZombieSpawnPoints() {
|
||||
List<int[]> points = new ArrayList<>();
|
||||
int[][] zombieSpawns = {{8, 8}, {24, 24}};
|
||||
@@ -140,193 +236,47 @@ public class GameMap {
|
||||
return points;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新流场导航
|
||||
*
|
||||
* @param playerPositions 玩家位置列表
|
||||
*/
|
||||
public void updateFlowField(List<float[]> playerPositions) {
|
||||
for (int y = 0; y < height; y++) {
|
||||
for (int x = 0; x < width; x++) {
|
||||
distanceField[y][x] = Float.MAX_VALUE;
|
||||
}
|
||||
}
|
||||
|
||||
Queue<int[]> queue = new LinkedList<>();
|
||||
for (float[] pos : playerPositions) {
|
||||
int px = (int) Math.floor(pos[0]);
|
||||
int py = (int) Math.floor(pos[1]);
|
||||
if (px >= 0 && px < width && py >= 0 && py < height && !isWall(px, py)) {
|
||||
distanceField[py][px] = 0;
|
||||
queue.add(new int[]{px, py});
|
||||
}
|
||||
}
|
||||
|
||||
int[][] dirs = {{0, -1}, {0, 1}, {-1, 0}, {1, 0}, {-1, -1}, {1, -1}, {-1, 1}, {1, 1}};
|
||||
|
||||
while (!queue.isEmpty()) {
|
||||
int[] current = queue.poll();
|
||||
int cx = current[0];
|
||||
int cy = current[1];
|
||||
float currentDist = distanceField[cy][cx];
|
||||
|
||||
for (int[] dir : dirs) {
|
||||
int nx = cx + dir[0];
|
||||
int ny = cy + dir[1];
|
||||
|
||||
if (nx < 0 || nx >= width || ny < 0 || ny >= height) continue;
|
||||
if (isWall(nx, ny)) continue;
|
||||
|
||||
if (dir[0] != 0 && dir[1] != 0) {
|
||||
if (isWall(cx + dir[0], cy) || isWall(cx, cy + dir[1])) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
float moveCost = (dir[0] != 0 && dir[1] != 0) ? 1.414f : 1.0f;
|
||||
float newDist = currentDist + moveCost;
|
||||
|
||||
if (newDist < distanceField[ny][nx]) {
|
||||
distanceField[ny][nx] = newDist;
|
||||
queue.add(new int[]{nx, ny});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (int y = 0; y < height; y++) {
|
||||
for (int x = 0; x < width; x++) {
|
||||
if (isWall(x, y) || distanceField[y][x] == Float.MAX_VALUE) {
|
||||
flowFieldX[y][x] = 0;
|
||||
flowFieldY[y][x] = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
float bestDist = distanceField[y][x];
|
||||
float bestDirX = 0;
|
||||
float bestDirY = 0;
|
||||
|
||||
for (int[] dir : dirs) {
|
||||
int nx = x + dir[0];
|
||||
int ny = y + dir[1];
|
||||
|
||||
if (nx < 0 || nx >= width || ny < 0 || ny >= height) continue;
|
||||
if (isWall(nx, ny)) continue;
|
||||
|
||||
if (dir[0] != 0 && dir[1] != 0) {
|
||||
if (isWall(x + dir[0], y) || isWall(x, y + dir[1])) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (distanceField[ny][nx] < bestDist) {
|
||||
bestDist = distanceField[ny][nx];
|
||||
float len = (float) Math.sqrt(dir[0] * dir[0] + dir[1] * dir[1]);
|
||||
bestDirX = dir[0] / len;
|
||||
bestDirY = dir[1] / len;
|
||||
}
|
||||
}
|
||||
|
||||
flowFieldX[y][x] = bestDirX;
|
||||
flowFieldY[y][x] = bestDirY;
|
||||
}
|
||||
}
|
||||
|
||||
flowFieldValid = true;
|
||||
flowField.update(walls, playerPositions);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定位置的流场方向
|
||||
*
|
||||
* @param wx 世界X坐标
|
||||
* @param wy 世界Y坐标
|
||||
* @return 流场方向向量 [dx, dy]
|
||||
*/
|
||||
public float[] getFlowDirection(float wx, float wy) {
|
||||
int gx = (int) Math.floor(wx);
|
||||
int gy = (int) Math.floor(wy);
|
||||
|
||||
if (gx < 0 || gx >= width || gy < 0 || gy >= height) {
|
||||
return new float[]{0, 0};
|
||||
}
|
||||
|
||||
return new float[]{flowFieldX[gy][gx], flowFieldY[gy][gx]};
|
||||
return flowField.getDirection(wx, wy);
|
||||
}
|
||||
|
||||
/** 检查流场是否有效 */
|
||||
public boolean isFlowFieldValid() {
|
||||
return flowFieldValid;
|
||||
return flowField.isValid();
|
||||
}
|
||||
|
||||
public float getDistance(float wx, float wy) {
|
||||
int gx = (int) Math.floor(wx);
|
||||
int gy = (int) Math.floor(wy);
|
||||
|
||||
if (gx < 0 || gx >= width || gy < 0 || gy >= height) {
|
||||
return Float.MAX_VALUE;
|
||||
}
|
||||
|
||||
return distanceField[gy][gx];
|
||||
}
|
||||
|
||||
public List<float[]> findPath(float startX, float startY, float endX, float endY) {
|
||||
int sgx = (int) Math.floor(startX);
|
||||
int sgy = (int) Math.floor(startY);
|
||||
int egx = (int) Math.floor(endX);
|
||||
int egy = (int) Math.floor(endY);
|
||||
|
||||
if (isWall(egx, egy)) return null;
|
||||
|
||||
PriorityQueue<PathNode> openSet = new PriorityQueue<>(Comparator.comparingDouble(n -> n.f));
|
||||
Set<String> closedSet = new HashSet<>();
|
||||
Map<String, String> cameFrom = new HashMap<>();
|
||||
|
||||
String startKey = sgx + "," + sgy;
|
||||
double h = Math.abs(sgx - egx) + Math.abs(sgy - egy);
|
||||
openSet.add(new PathNode(sgx, sgy, 0, h));
|
||||
|
||||
int[][] dirs = {{0, -1}, {0, 1}, {-1, 0}, {1, 0}, {-1, -1}, {1, -1}, {-1, 1}, {1, 1}};
|
||||
int iterations = 0;
|
||||
|
||||
while (!openSet.isEmpty() && iterations < 2000) {
|
||||
iterations++;
|
||||
PathNode current = openSet.poll();
|
||||
String currentKey = current.x + "," + current.y;
|
||||
|
||||
if (current.x == egx && current.y == egy) {
|
||||
List<float[]> path = new ArrayList<>();
|
||||
String key = currentKey;
|
||||
while (cameFrom.containsKey(key)) {
|
||||
String[] parts = key.split(",");
|
||||
int cx = Integer.parseInt(parts[0]);
|
||||
int cy = Integer.parseInt(parts[1]);
|
||||
path.add(0, new float[]{cx + 0.5f, cy + 0.5f});
|
||||
key = cameFrom.get(key);
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
closedSet.add(currentKey);
|
||||
|
||||
for (int[] dir : dirs) {
|
||||
int nx = current.x + dir[0];
|
||||
int ny = current.y + dir[1];
|
||||
String nKey = nx + "," + ny;
|
||||
|
||||
if (closedSet.contains(nKey)) continue;
|
||||
if (isWall(nx, ny)) continue;
|
||||
if (nx < 0 || nx >= width || ny < 0 || ny >= height) continue;
|
||||
|
||||
if (dir[0] != 0 && dir[1] != 0) {
|
||||
if (isWall(current.x + dir[0], current.y) || isWall(current.x, current.y + dir[1])) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
boolean diagonal = dir[0] != 0 && dir[1] != 0;
|
||||
double moveCost = diagonal ? 1.414 : 1.0;
|
||||
double g = current.g + moveCost;
|
||||
|
||||
double nh = Math.abs(nx - egx) + Math.abs(ny - egy);
|
||||
openSet.add(new PathNode(nx, ny, g, nh));
|
||||
if (!cameFrom.containsKey(nKey)) {
|
||||
cameFrom.put(nKey, currentKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
private static String key(int x, int y) {
|
||||
return x + "," + y;
|
||||
}
|
||||
|
||||
/**
|
||||
* A* 寻路算法的节点类
|
||||
*/
|
||||
private static class PathNode {
|
||||
/** 节点格子坐标 */
|
||||
int x, y;
|
||||
double g, h, f;
|
||||
/** 从起点到当前节点的实际代价 */
|
||||
double g;
|
||||
/** 从当前节点到终点的启发式代价 */
|
||||
double h;
|
||||
/** 总代价 f = g + h */
|
||||
double f;
|
||||
|
||||
PathNode(int x, int y, double g, double h) {
|
||||
this.x = x;
|
||||
|
||||
@@ -1,8 +1,20 @@
|
||||
package com.zombie.game.model;
|
||||
|
||||
import lombok.Getter;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* 游戏世界类
|
||||
*
|
||||
* 管理整个游戏状态,包括:
|
||||
* - 玩家、僵尸、子弹、掉落物等实体
|
||||
* - 游戏时间、波数、分数
|
||||
* - 难度递增系统
|
||||
* - 碰撞检测和游戏逻辑更新
|
||||
*/
|
||||
@Getter
|
||||
public class GameWorld {
|
||||
private final Object lock = new Object();
|
||||
private GameMap map;
|
||||
private Map<String, Player> players;
|
||||
private Map<Integer, Zombie> zombies;
|
||||
@@ -51,8 +63,11 @@ public class GameWorld {
|
||||
this.removedZombieBullets = new ArrayList<>();
|
||||
}
|
||||
|
||||
public GameMap getMap() { return map; }
|
||||
|
||||
/**
|
||||
* 添加玩家到游戏世界
|
||||
*
|
||||
* @param player 玩家对象
|
||||
*/
|
||||
public void addPlayer(Player player) {
|
||||
List<int[]> spawnPoints = map.getSpawnPoints();
|
||||
int idx = players.size() % spawnPoints.size();
|
||||
@@ -63,16 +78,38 @@ public class GameWorld {
|
||||
players.put(player.getId(), player);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从游戏世界移除玩家
|
||||
*
|
||||
* @param playerId 玩家ID
|
||||
*/
|
||||
public void removePlayer(String playerId) {
|
||||
players.remove(playerId);
|
||||
}
|
||||
|
||||
/** 获取指定ID的玩家 */
|
||||
public Player getPlayer(String id) { return players.get(id); }
|
||||
/** 获取所有玩家 */
|
||||
public Collection<Player> getPlayers() { return players.values(); }
|
||||
/** 获取所有僵尸 */
|
||||
public Collection<Zombie> getZombies() { return zombies.values(); }
|
||||
/** 获取所有玩家子弹 */
|
||||
public Collection<Bullet> getBullets() { return bullets.values(); }
|
||||
/** 获取所有掉落物 */
|
||||
public Collection<Loot> getLoots() { return loots.values(); }
|
||||
|
||||
/**
|
||||
* 更新游戏世界状态
|
||||
*
|
||||
* 每帧调用,处理:
|
||||
* - 时间流逝和难度提升
|
||||
* - 僵尸生成
|
||||
* - 实体移动和碰撞
|
||||
* - 掉落物收集
|
||||
* - 玩家重生
|
||||
*
|
||||
* @param dt 时间增量(秒)
|
||||
*/
|
||||
public void update(float dt) {
|
||||
explosions.clear();
|
||||
removedBullets.clear();
|
||||
@@ -103,8 +140,14 @@ public class GameWorld {
|
||||
checkZombieBulletCollisions();
|
||||
checkZombieAttacks();
|
||||
checkLootCollection();
|
||||
checkPlayerRespawn();
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新流场导航
|
||||
*
|
||||
* 基于存活玩家的位置更新流场
|
||||
*/
|
||||
private void updateFlowField() {
|
||||
List<float[]> playerPositions = new ArrayList<>();
|
||||
for (Player p : players.values()) {
|
||||
@@ -117,22 +160,36 @@ public class GameWorld {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成僵尸
|
||||
*
|
||||
* 根据概率生成普通僵尸、精英僵尸或分裂僵尸
|
||||
*/
|
||||
private void spawnZombie() {
|
||||
List<int[]> spawnPoints = map.getZombieSpawnPoints();
|
||||
int[] sp = spawnPoints.get(random.nextInt(spawnPoints.size()));
|
||||
float wx = sp[0] + 0.5f;
|
||||
float wy = sp[1] + 0.5f;
|
||||
|
||||
boolean isElite = random.nextFloat() < Constants.ELITE_ZOMBIE_SPAWN_CHANCE;
|
||||
float roll = random.nextFloat();
|
||||
Zombie zombie;
|
||||
if (isElite) {
|
||||
zombie = new Zombie(nextZombieId++, wx, wy, Constants.ELITE_ZOMBIE_HEALTH, Constants.ELITE_ZOMBIE_SPEED, true);
|
||||
if (roll < Constants.ELITE_ZOMBIE_SPAWN_CHANCE) {
|
||||
zombie = new Zombie(nextZombieId++, wx, wy, Constants.ELITE_ZOMBIE_HEALTH, Constants.ELITE_ZOMBIE_SPEED, true, false);
|
||||
} else if (roll < Constants.ELITE_ZOMBIE_SPAWN_CHANCE + Constants.SPLITTER_ZOMBIE_SPAWN_CHANCE) {
|
||||
zombie = new Zombie(nextZombieId++, wx, wy, Constants.SPLITTER_ZOMBIE_HEALTH, Constants.SPLITTER_ZOMBIE_SPEED, false, true);
|
||||
} else {
|
||||
zombie = new Zombie(nextZombieId++, wx, wy, zombieHealth, zombieSpeed, false);
|
||||
zombie = new Zombie(nextZombieId++, wx, wy, zombieHealth, zombieSpeed, false, false);
|
||||
}
|
||||
zombies.put(zombie.getId(), zombie);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找最近的存活玩家
|
||||
*
|
||||
* @param x X坐标
|
||||
* @param y Y坐标
|
||||
* @return 最近的玩家,如果没有存活玩家则返回 null
|
||||
*/
|
||||
private Player findNearestPlayer(float x, float y) {
|
||||
Player nearest = null;
|
||||
float minDist = Float.MAX_VALUE;
|
||||
@@ -147,6 +204,13 @@ public class GameWorld {
|
||||
return nearest;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新所有僵尸
|
||||
*
|
||||
* 处理僵尸移动、攻击和死亡
|
||||
*
|
||||
* @param dt 时间增量(秒)
|
||||
*/
|
||||
private void updateZombies(float dt) {
|
||||
long now = System.currentTimeMillis();
|
||||
|
||||
@@ -175,6 +239,12 @@ public class GameWorld {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 精英僵尸发射子弹
|
||||
*
|
||||
* @param zombie 发射子弹的僵尸
|
||||
* @param target 目标玩家
|
||||
*/
|
||||
private void fireZombieBullet(Zombie zombie, Player target) {
|
||||
float dx = target.getX() - zombie.getX();
|
||||
float dy = target.getY() - zombie.getY();
|
||||
@@ -189,6 +259,11 @@ public class GameWorld {
|
||||
zombieBullets.put(bullet.getId(), bullet);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新所有僵尸子弹
|
||||
*
|
||||
* @param dt 时间增量(秒)
|
||||
*/
|
||||
private void updateZombieBullets(float dt) {
|
||||
List<Integer> toRemove = new ArrayList<>();
|
||||
for (Bullet b : zombieBullets.values()) {
|
||||
@@ -202,6 +277,9 @@ public class GameWorld {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测僵尸子弹与玩家的碰撞
|
||||
*/
|
||||
private void checkZombieBulletCollisions() {
|
||||
List<Integer> bulletsToRemove = new ArrayList<>();
|
||||
for (Bullet b : new ArrayList<>(zombieBullets.values())) {
|
||||
@@ -220,8 +298,31 @@ public class GameWorld {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理僵尸死亡
|
||||
*
|
||||
* - 分裂僵尸分裂成多个小僵尸
|
||||
* - 增加分数
|
||||
* - 可能掉落物品
|
||||
*
|
||||
* @param z 被击杀的僵尸
|
||||
*/
|
||||
private void onZombieKilled(Zombie z) {
|
||||
score += z.isElite() ? 50 : 10;
|
||||
if (z.isSplitter()) {
|
||||
int splitCount = Constants.SPLITTER_ZOMBIE_MIN_SPLIT +
|
||||
random.nextInt(Constants.SPLITTER_ZOMBIE_MAX_SPLIT - Constants.SPLITTER_ZOMBIE_MIN_SPLIT + 1);
|
||||
for (int i = 0; i < splitCount; i++) {
|
||||
float offsetX = (random.nextFloat() - 0.5f) * 1.0f;
|
||||
float offsetY = (random.nextFloat() - 0.5f) * 1.0f;
|
||||
Zombie splitZombie = new Zombie(nextZombieId++,
|
||||
z.getX() + offsetX, z.getY() + offsetY,
|
||||
zombieHealth, zombieSpeed, false, false);
|
||||
zombies.put(splitZombie.getId(), splitZombie);
|
||||
}
|
||||
score += 20;
|
||||
} else {
|
||||
score += z.isElite() ? 50 : 10;
|
||||
}
|
||||
if (random.nextFloat() < Constants.ZOMBIE_LOOT_DROP_CHANCE) {
|
||||
String lootType = random.nextFloat() < 0.5f ? Constants.LOOT_TYPE_AMMO : Constants.LOOT_TYPE_HEALTH;
|
||||
Loot loot = new Loot(nextLootId++, z.getX(), z.getY(), lootType);
|
||||
@@ -229,6 +330,11 @@ public class GameWorld {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新所有玩家子弹
|
||||
*
|
||||
* @param dt 时间增量(秒)
|
||||
*/
|
||||
private void updateBullets(float dt) {
|
||||
List<Integer> toRemove = new ArrayList<>();
|
||||
for (Bullet b : bullets.values()) {
|
||||
@@ -245,6 +351,9 @@ public class GameWorld {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测玩家子弹与僵尸的碰撞
|
||||
*/
|
||||
private void checkBulletCollisions() {
|
||||
List<Integer> bulletsToRemove = new ArrayList<>();
|
||||
for (Bullet b : new ArrayList<>(bullets.values())) {
|
||||
@@ -264,6 +373,16 @@ public class GameWorld {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建爆炸效果
|
||||
*
|
||||
* 对范围内的僵尸造成伤害
|
||||
*
|
||||
* @param x 爆炸中心X坐标
|
||||
* @param y 爆炸中心Y坐标
|
||||
* @param radius 爆炸半径
|
||||
* @param ownerId 爆炸发起者ID
|
||||
*/
|
||||
private void createExplosion(float x, float y, float radius, String ownerId) {
|
||||
Map<String, Object> exp = new LinkedHashMap<>();
|
||||
exp.put("x", x);
|
||||
@@ -279,6 +398,9 @@ public class GameWorld {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测僵尸近战攻击
|
||||
*/
|
||||
private void checkZombieAttacks() {
|
||||
long now = System.currentTimeMillis();
|
||||
for (Zombie z : zombies.values()) {
|
||||
@@ -294,6 +416,9 @@ public class GameWorld {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测掉落物收集
|
||||
*/
|
||||
private void checkLootCollection() {
|
||||
List<Integer> toRemove = new ArrayList<>();
|
||||
for (Loot loot : loots.values()) {
|
||||
@@ -315,10 +440,53 @@ public class GameWorld {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测玩家重生
|
||||
*/
|
||||
private void checkPlayerRespawn() {
|
||||
boolean hasAlivePlayer = false;
|
||||
for (Player p : players.values()) {
|
||||
if (p.isAlive()) {
|
||||
hasAlivePlayer = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (Player p : players.values()) {
|
||||
if (p.isWaitingForRespawn()) {
|
||||
p.updateRespawnTimer(Constants.TICK_INTERVAL);
|
||||
if (hasAlivePlayer && p.canRespawn()) {
|
||||
List<int[]> spawnPoints = map.getSpawnPoints();
|
||||
int[] sp = spawnPoints.get(random.nextInt(spawnPoints.size()));
|
||||
float wx = sp[0] + 0.5f;
|
||||
float wy = sp[1] + 0.5f;
|
||||
p.respawn(wx, wy);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 玩家开火(无蓄力)
|
||||
*
|
||||
* @param player 玩家
|
||||
* @param aimX 瞄准X坐标
|
||||
* @param aimY 瞄准Y坐标
|
||||
* @return 新创建的子弹ID列表
|
||||
*/
|
||||
public List<Integer> fireWeapon(Player player, float aimX, float aimY) {
|
||||
return fireWeapon(player, aimX, aimY, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 玩家开火(支持蓄力)
|
||||
*
|
||||
* @param player 玩家
|
||||
* @param aimX 瞄准X坐标
|
||||
* @param aimY 瞄准Y坐标
|
||||
* @param chargePercent 蓄力百分比(0-1),影响手榴弹投掷距离
|
||||
* @return 新创建的子弹ID列表
|
||||
*/
|
||||
public List<Integer> fireWeapon(Player player, float aimX, float aimY, float chargePercent) {
|
||||
List<Integer> newBulletIds = new ArrayList<>();
|
||||
long now = System.currentTimeMillis();
|
||||
@@ -399,10 +567,19 @@ public class GameWorld {
|
||||
return newBulletIds;
|
||||
}
|
||||
|
||||
/** 获取地图数据 */
|
||||
public int[][] getMapData() {
|
||||
return map.getCells();
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建游戏状态数据
|
||||
*
|
||||
* 将当前游戏世界的所有状态打包成Map格式,用于网络传输
|
||||
*
|
||||
* @param forPlayerId 目标玩家ID(用于发送该玩家的弹药信息)
|
||||
* @return 游戏状态Map
|
||||
*/
|
||||
public Map<String, Object> buildGameState(String forPlayerId) {
|
||||
Map<String, Object> state = new LinkedHashMap<>();
|
||||
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
package com.zombie.game.model;
|
||||
|
||||
import lombok.Getter;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* 掉落物类
|
||||
*
|
||||
* 管理僵尸死亡后掉落的物品,包括弹药和生命值补给。
|
||||
*/
|
||||
@Getter
|
||||
public class Loot {
|
||||
private int id;
|
||||
private float x, y;
|
||||
private String type;
|
||||
private long spawnTime;
|
||||
private final int id;
|
||||
private final float x, y;
|
||||
private final String type;
|
||||
private final long spawnTime;
|
||||
|
||||
public Loot(int id, float x, float y) {
|
||||
this(id, x, y, Constants.LOOT_TYPE_AMMO);
|
||||
@@ -20,11 +27,6 @@ public class Loot {
|
||||
this.spawnTime = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
public int getId() { return id; }
|
||||
public float getX() { return x; }
|
||||
public float getY() { return y; }
|
||||
public String getType() { return type; }
|
||||
|
||||
public boolean isCollectedBy(float px, float py) {
|
||||
float dx = px - x;
|
||||
float dy = py - y;
|
||||
|
||||
60
backend/src/main/java/com/zombie/game/model/NutWall.java
Normal file
60
backend/src/main/java/com/zombie/game/model/NutWall.java
Normal file
@@ -0,0 +1,60 @@
|
||||
package com.zombie.game.model;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 坚果墙体
|
||||
*
|
||||
* 可被僵尸攻击破坏的障碍物。
|
||||
* 设计血量:僵尸移动速度 2.0格/秒,攻击间隔 1.0秒,
|
||||
* 假设僵尸每攻击一次造成 1 点伤害(用于破坏墙体),
|
||||
* 需要 20 个时间单位(约 20 秒)才能破坏。
|
||||
*
|
||||
* 流场中,坚果的移动代价 = 基础代价 + 摧毁代价(20.0),
|
||||
* 这样 BFS 会自动权衡绕道 vs 摧毁。
|
||||
*/
|
||||
public class NutWall extends Wall {
|
||||
/** 坚果最大血量(20个时间单位) */
|
||||
public static final float MAX_HEALTH = 20.0f;
|
||||
/** 坚果在流场中的额外移动代价(摧毁时间折算) */
|
||||
public static final float MOVEMENT_COST_PENALTY = 20.0f;
|
||||
/** 当前血量 */
|
||||
@Getter
|
||||
private float health;
|
||||
/** 是否已被破坏 */
|
||||
private boolean destroyed;
|
||||
|
||||
public NutWall(int gridX, int gridY) {
|
||||
super(gridX, gridY);
|
||||
this.health = MAX_HEALTH;
|
||||
this.destroyed = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDestructible() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDestroyed() {
|
||||
return destroyed;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void takeDamage(float damage) {
|
||||
if (destroyed) return;
|
||||
this.health -= damage;
|
||||
if (this.health <= 0) {
|
||||
this.health = 0;
|
||||
this.destroyed = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getMovementCost() {
|
||||
if (destroyed) {
|
||||
return 1.0f; // 被破坏后视为空地
|
||||
}
|
||||
return 1.0f + MOVEMENT_COST_PENALTY;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,18 @@
|
||||
package com.zombie.game.model;
|
||||
|
||||
import lombok.Getter;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* 玩家类
|
||||
*
|
||||
* 管理玩家状态,包括:
|
||||
* - 位置、朝向、生命值
|
||||
* - 武器和弹药
|
||||
* - 移动和射击
|
||||
* - 重生机制
|
||||
*/
|
||||
@Getter
|
||||
public class Player {
|
||||
private String id;
|
||||
private String name;
|
||||
@@ -17,6 +28,8 @@ public class Player {
|
||||
private float grenadeChargeStart;
|
||||
private boolean chargingGrenade;
|
||||
private int lastProcessedSeq;
|
||||
private float respawnTimer;
|
||||
private boolean waitingForRespawn;
|
||||
|
||||
private static final String[] WEAPONS = {
|
||||
Constants.WEAPON_PISTOL, Constants.WEAPON_MACHINE_GUN,
|
||||
@@ -41,23 +54,31 @@ public class Player {
|
||||
this.grenadeChargeStart = 0;
|
||||
this.chargingGrenade = false;
|
||||
this.lastProcessedSeq = 0;
|
||||
this.respawnTimer = 0;
|
||||
this.waitingForRespawn = false;
|
||||
}
|
||||
|
||||
public String getId() { return id; }
|
||||
public String getName() { return name; }
|
||||
public float getX() { return x; }
|
||||
public float getY() { return y; }
|
||||
public float getAngle() { return angle; }
|
||||
public float getHealth() { return health; }
|
||||
public int getWeaponIndex() { return weaponIndex; }
|
||||
public boolean isReady() { return ready; }
|
||||
public float[] getAmmo() { return ammo; }
|
||||
public boolean isFiring() { return firing; }
|
||||
public int getLastProcessedSeq() { return lastProcessedSeq; }
|
||||
|
||||
public void setReady(boolean ready) { this.ready = ready; }
|
||||
public void setWeaponIndex(int idx) { this.weaponIndex = Math.max(0, Math.min(3, idx)); }
|
||||
|
||||
/**
|
||||
* 设置玩家位置
|
||||
*
|
||||
* @param x 新位置X坐标
|
||||
* @param y 新位置Y坐标
|
||||
*/
|
||||
public void setPosition(float x, float y) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用移动输入
|
||||
*
|
||||
* @param dx X方向移动量
|
||||
* @param dy Y方向移动量
|
||||
* @param map 游戏地图(用于碰撞检测)
|
||||
*/
|
||||
public void applyMovement(float dx, float dy, GameMap map) {
|
||||
float speed = Constants.PLAYER_SPEED * Constants.TICK_INTERVAL;
|
||||
float newX = x + dx * speed;
|
||||
@@ -71,34 +92,114 @@ public class Player {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置朝向角度
|
||||
*
|
||||
* @param aimX 瞄准点X坐标
|
||||
* @param aimY 瞄准点Y坐标
|
||||
*/
|
||||
public void setAngle(float aimX, float aimY) {
|
||||
this.angle = (float) Math.atan2(aimX - x, aimY - y);
|
||||
}
|
||||
|
||||
/**
|
||||
* 受到伤害
|
||||
*
|
||||
* @param damage 伤害值
|
||||
*/
|
||||
public void takeDamage(float damage) {
|
||||
long now = System.currentTimeMillis();
|
||||
if (now - lastDamageTime < Constants.PLAYER_INVULNERABLE_TIME * 1000) return;
|
||||
this.health -= damage;
|
||||
this.lastDamageTime = now;
|
||||
if (this.health < 0) this.health = 0;
|
||||
if (this.health <= 0) {
|
||||
startRespawnTimer();
|
||||
}
|
||||
}
|
||||
|
||||
/** 开始重生倒计时 */
|
||||
public void startRespawnTimer() {
|
||||
this.waitingForRespawn = true;
|
||||
this.respawnTimer = Constants.PLAYER_RESPAWN_TIME;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新重生倒计时
|
||||
*
|
||||
* @param dt 时间增量(秒)
|
||||
*/
|
||||
public void updateRespawnTimer(float dt) {
|
||||
if (waitingForRespawn && respawnTimer > 0) {
|
||||
respawnTimer -= dt;
|
||||
}
|
||||
}
|
||||
|
||||
/** 检查是否可以重生 */
|
||||
public boolean canRespawn() {
|
||||
return waitingForRespawn && respawnTimer <= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重生玩家
|
||||
*
|
||||
* @param newX 新位置X坐标
|
||||
* @param newY 新位置Y坐标
|
||||
*/
|
||||
public void respawn(float newX, float newY) {
|
||||
this.health = Constants.PLAYER_MAX_HEALTH;
|
||||
this.x = newX;
|
||||
this.y = newY;
|
||||
this.waitingForRespawn = false;
|
||||
this.respawnTimer = 0;
|
||||
this.lastDamageTime = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
/** 是否等待重生 */
|
||||
public boolean isWaitingForRespawn() {
|
||||
return waitingForRespawn;
|
||||
}
|
||||
|
||||
/** 获取重生倒计时 */
|
||||
public float getRespawnTimer() {
|
||||
return respawnTimer;
|
||||
}
|
||||
|
||||
/** 是否存活 */
|
||||
public boolean isAlive() {
|
||||
return health > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算到指定点的距离
|
||||
*
|
||||
* @param px 目标X坐标
|
||||
* @param py 目标Y坐标
|
||||
* @return 距离值
|
||||
*/
|
||||
public float distanceTo(float px, float py) {
|
||||
float dx = px - x;
|
||||
float dy = py - y;
|
||||
return (float) Math.sqrt(dx * dx + dy * dy);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否可以开火
|
||||
*
|
||||
* @param now 当前时间戳
|
||||
* @return true 表示可以开火
|
||||
*/
|
||||
public boolean canFire(long now) {
|
||||
String weapon = WEAPONS[weaponIndex];
|
||||
long fireRate = getFireRate(weapon);
|
||||
return now - lastAttackTime >= fireRate;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行开火动作
|
||||
*
|
||||
* @param now 当前时间戳
|
||||
*/
|
||||
public void fire(long now) {
|
||||
lastAttackTime = now;
|
||||
String weapon = WEAPONS[weaponIndex];
|
||||
@@ -108,24 +209,34 @@ public class Player {
|
||||
}
|
||||
}
|
||||
|
||||
/** 检查是否有弹药 */
|
||||
public boolean hasAmmo() {
|
||||
if (weaponIndex == 0) return true;
|
||||
return ammo[weaponIndex] > 0;
|
||||
}
|
||||
|
||||
/** 随机补充一个武器的弹药 */
|
||||
public void refillRandomWeapon() {
|
||||
Random rand = new Random();
|
||||
int idx = rand.nextInt(3) + 1;
|
||||
ammo[idx] = MAX_AMMO[idx];
|
||||
}
|
||||
|
||||
/**
|
||||
* 治疗
|
||||
*
|
||||
* @param amount 治疗量
|
||||
*/
|
||||
public void heal(float amount) {
|
||||
this.health = Math.min(Constants.PLAYER_MAX_HEALTH, this.health + amount);
|
||||
}
|
||||
|
||||
/** 设置开火状态 */
|
||||
public void setFiring(boolean firing) { this.firing = firing; }
|
||||
/** 设置最后处理的输入序列号 */
|
||||
public void setLastProcessedSeq(int seq) { this.lastProcessedSeq = seq; }
|
||||
|
||||
/** 开始手榴弹蓄力 */
|
||||
public void startGrenadeCharge() {
|
||||
if (!chargingGrenade) {
|
||||
chargingGrenade = true;
|
||||
@@ -133,16 +244,24 @@ public class Player {
|
||||
}
|
||||
}
|
||||
|
||||
/** 获取手榴弹蓄力百分比(0-1) */
|
||||
public float getGrenadeChargePercent() {
|
||||
if (!chargingGrenade) return 0;
|
||||
float elapsed = (System.currentTimeMillis() - grenadeChargeStart) / 2000f;
|
||||
return Math.min(1.0f, elapsed);
|
||||
}
|
||||
|
||||
/** 停止手榴弹蓄力 */
|
||||
public void stopGrenadeCharge() {
|
||||
chargingGrenade = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取武器射速
|
||||
*
|
||||
* @param weapon 武器类型
|
||||
* @return 射速(毫秒)
|
||||
*/
|
||||
private long getFireRate(String weapon) {
|
||||
switch (weapon) {
|
||||
case Constants.WEAPON_PISTOL: return 400;
|
||||
@@ -153,6 +272,7 @@ public class Player {
|
||||
}
|
||||
}
|
||||
|
||||
/** 获取当前武器伤害 */
|
||||
public int getDamage() {
|
||||
switch (WEAPONS[weaponIndex]) {
|
||||
case Constants.WEAPON_PISTOL: return 50;
|
||||
@@ -163,6 +283,7 @@ public class Player {
|
||||
}
|
||||
}
|
||||
|
||||
/** 获取当前武器子弹速度 */
|
||||
public float getBulletSpeed() {
|
||||
switch (WEAPONS[weaponIndex]) {
|
||||
case Constants.WEAPON_PISTOL: return 20;
|
||||
@@ -173,10 +294,12 @@ public class Player {
|
||||
}
|
||||
}
|
||||
|
||||
/** 获取当前武器弹丸数量(霰弹枪发射多个弹丸) */
|
||||
public int getPelletCount() {
|
||||
return WEAPONS[weaponIndex].equals(Constants.WEAPON_SHOTGUN) ? 10 : 1;
|
||||
}
|
||||
|
||||
/** 获取当前武器散射角度 */
|
||||
public float getSpread() {
|
||||
switch (WEAPONS[weaponIndex]) {
|
||||
case Constants.WEAPON_MACHINE_GUN: return 0.05f;
|
||||
@@ -185,10 +308,16 @@ public class Player {
|
||||
}
|
||||
}
|
||||
|
||||
/** 当前武器是否可蓄力(手榴弹) */
|
||||
public boolean isChargeable() {
|
||||
return WEAPONS[weaponIndex].equals(Constants.WEAPON_GRENADE);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将玩家状态转换为Map格式,用于网络传输
|
||||
*
|
||||
* @return 包含玩家状态的Map
|
||||
*/
|
||||
public Map<String, Object> toStateMap() {
|
||||
Map<String, Object> map = new LinkedHashMap<>();
|
||||
map.put("id", id);
|
||||
@@ -198,6 +327,8 @@ public class Player {
|
||||
map.put("health", health);
|
||||
map.put("weaponIndex", weaponIndex);
|
||||
map.put("lastProcessedSeq", lastProcessedSeq);
|
||||
map.put("waitingForRespawn", waitingForRespawn);
|
||||
map.put("respawnTimer", respawnTimer);
|
||||
return map;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,25 @@
|
||||
package com.zombie.game.model;
|
||||
|
||||
import lombok.Getter;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* 游戏房间类
|
||||
*
|
||||
* 管理游戏房间状态,包括:
|
||||
* - 房间ID和房主
|
||||
* - 房间内的玩家列表
|
||||
* - 游戏开始状态
|
||||
*/
|
||||
@Getter
|
||||
public class Room {
|
||||
private String id;
|
||||
private String hostId;
|
||||
private Map<String, Player> players;
|
||||
private boolean gameStarted;
|
||||
private int maxPlayers = 4;
|
||||
private final int maxPlayers = 4;
|
||||
|
||||
public void setGameStarted(boolean started) { this.gameStarted = started; }
|
||||
|
||||
public Room(String id, String hostId, String hostName) {
|
||||
this.id = id;
|
||||
@@ -17,11 +29,6 @@ public class Room {
|
||||
players.put(hostId, host);
|
||||
}
|
||||
|
||||
public String getId() { return id; }
|
||||
public String getHostId() { return hostId; }
|
||||
public boolean isGameStarted() { return gameStarted; }
|
||||
public void setGameStarted(boolean started) { this.gameStarted = started; }
|
||||
|
||||
public boolean addPlayer(String playerId, String playerName) {
|
||||
if (players.size() >= maxPlayers) return false;
|
||||
if (players.containsKey(playerId)) return false;
|
||||
@@ -57,6 +64,12 @@ public class Room {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将房间状态转换为Map格式,用于网络传输
|
||||
*
|
||||
* @param playerId 目标玩家ID
|
||||
* @return 包含房间状态的Map
|
||||
*/
|
||||
public Map<String, Object> toStateMap(String playerId) {
|
||||
Map<String, Object> map = new LinkedHashMap<>();
|
||||
map.put("roomId", id);
|
||||
@@ -78,6 +91,11 @@ public class Room {
|
||||
return map;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将房间信息转换为房间列表格式,用于大厅显示
|
||||
*
|
||||
* @return 包含房间信息的Map
|
||||
*/
|
||||
public Map<String, Object> toRoomListMap() {
|
||||
Map<String, Object> map = new LinkedHashMap<>();
|
||||
map.put("id", id);
|
||||
|
||||
38
backend/src/main/java/com/zombie/game/model/StaticWall.java
Normal file
38
backend/src/main/java/com/zombie/game/model/StaticWall.java
Normal file
@@ -0,0 +1,38 @@
|
||||
package com.zombie.game.model;
|
||||
|
||||
/**
|
||||
* 静态墙体
|
||||
*
|
||||
* 不可破坏的永久性障碍物,如边界墙、建筑等。
|
||||
*/
|
||||
public class StaticWall extends Wall {
|
||||
|
||||
public StaticWall(int gridX, int gridY) {
|
||||
super(gridX, gridY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDestructible() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDestroyed() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getHealth() {
|
||||
return Float.MAX_VALUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void takeDamage(float damage) {
|
||||
// 静态墙体不可破坏,忽略伤害
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getMovementCost() {
|
||||
return Float.MAX_VALUE;
|
||||
}
|
||||
}
|
||||
68
backend/src/main/java/com/zombie/game/model/Wall.java
Normal file
68
backend/src/main/java/com/zombie/game/model/Wall.java
Normal file
@@ -0,0 +1,68 @@
|
||||
package com.zombie.game.model;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 墙体抽象基类
|
||||
*
|
||||
* 地图中的障碍物,分为:
|
||||
* - StaticWall:静态墙体,不可破坏
|
||||
* - NutWall:坚果墙体,可被僵尸攻击破坏,有血量
|
||||
*/
|
||||
public abstract class Wall {
|
||||
/** 格子X坐标 */
|
||||
@Getter
|
||||
protected final int gridX;
|
||||
/** 格子Y坐标 */
|
||||
@Getter
|
||||
protected final int gridY;
|
||||
|
||||
public Wall(int gridX, int gridY) {
|
||||
this.gridX = gridX;
|
||||
this.gridY = gridY;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否可破坏
|
||||
*
|
||||
* @return true 表示可以被破坏
|
||||
*/
|
||||
public abstract boolean isDestructible();
|
||||
|
||||
/**
|
||||
* 是否已被破坏(仅对可破坏墙体有效)
|
||||
*
|
||||
* @return true 表示已破坏
|
||||
*/
|
||||
public abstract boolean isDestroyed();
|
||||
|
||||
/**
|
||||
* 获取当前血量(仅对可破坏墙体有效)
|
||||
*
|
||||
* @return 当前血量
|
||||
*/
|
||||
public abstract float getHealth();
|
||||
|
||||
/**
|
||||
* 受到伤害
|
||||
*
|
||||
* @param damage 伤害值
|
||||
*/
|
||||
public abstract void takeDamage(float damage);
|
||||
|
||||
/**
|
||||
* 获取移动代价(用于流场计算)
|
||||
*
|
||||
* @return 移动代价,不可通行返回 Float.MAX_VALUE
|
||||
*/
|
||||
public abstract float getMovementCost();
|
||||
|
||||
/**
|
||||
* 是否阻挡移动
|
||||
*
|
||||
* @return true 表示阻挡
|
||||
*/
|
||||
public boolean blocksMovement() {
|
||||
return !isDestroyed();
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,18 @@
|
||||
package com.zombie.game.model;
|
||||
|
||||
import lombok.Getter;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* 僵尸类
|
||||
*
|
||||
* 管理僵尸状态和行为,包括:
|
||||
* - 位置、朝向、生命值
|
||||
* - 移动和寻路(基于流场导航)
|
||||
* - 近战和远程攻击
|
||||
* - 三种类型:普通僵尸、精英僵尸、分裂僵尸
|
||||
*/
|
||||
@Getter
|
||||
public class Zombie {
|
||||
private int id;
|
||||
private float x, y;
|
||||
@@ -11,17 +22,25 @@ public class Zombie {
|
||||
private float speed;
|
||||
private long lastAttackTime;
|
||||
private boolean isElite;
|
||||
private boolean isSplitter;
|
||||
private long lastRangedAttackTime;
|
||||
private float targetX, targetY;
|
||||
private boolean hasTarget;
|
||||
private int reservedGridX, reservedGridY;
|
||||
private boolean hasReservation;
|
||||
private boolean reservation; // 是否有预留格子
|
||||
private int attackingWallGridX = -1; // 正在攻击的坚果墙体格子X
|
||||
private int attackingWallGridY = -1; // 正在攻击的坚果墙体格子Y
|
||||
private boolean attackingWall; // 是否正在攻击墙体
|
||||
|
||||
public Zombie(int id, float x, float y, float health, float speed) {
|
||||
this(id, x, y, health, speed, false);
|
||||
this(id, x, y, health, speed, false, false);
|
||||
}
|
||||
|
||||
public Zombie(int id, float x, float y, float health, float speed, boolean isElite) {
|
||||
this(id, x, y, health, speed, isElite, false);
|
||||
}
|
||||
|
||||
public Zombie(int id, float x, float y, float health, float speed, boolean isElite, boolean isSplitter) {
|
||||
this.id = id;
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
@@ -31,35 +50,46 @@ public class Zombie {
|
||||
this.speed = speed;
|
||||
this.lastAttackTime = 0;
|
||||
this.isElite = isElite;
|
||||
this.isSplitter = isSplitter;
|
||||
this.lastRangedAttackTime = 0;
|
||||
this.targetX = 0;
|
||||
this.targetY = 0;
|
||||
this.hasTarget = false;
|
||||
this.reservedGridX = -1;
|
||||
this.reservedGridY = -1;
|
||||
this.hasReservation = false;
|
||||
this.reservation = false;
|
||||
this.attackingWallGridX = -1;
|
||||
this.attackingWallGridY = -1;
|
||||
this.attackingWall = false;
|
||||
}
|
||||
|
||||
public int getId() { return id; }
|
||||
public float getX() { return x; }
|
||||
public float getY() { return y; }
|
||||
public float getAngle() { return angle; }
|
||||
public float getHealth() { return health; }
|
||||
public float getMaxHealth() { return maxHealth; }
|
||||
public boolean isElite() { return isElite; }
|
||||
public int getReservedGridX() { return reservedGridX; }
|
||||
public int getReservedGridY() { return reservedGridY; }
|
||||
public boolean hasReservation() { return hasReservation; }
|
||||
|
||||
/**
|
||||
* 受到伤害
|
||||
*
|
||||
* @param damage 伤害值
|
||||
*/
|
||||
public void takeDamage(float damage) {
|
||||
this.health -= damage;
|
||||
if (this.health < 0) this.health = 0;
|
||||
}
|
||||
|
||||
/** 是否存活 */
|
||||
public boolean isAlive() {
|
||||
return health > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 移动僵尸
|
||||
*
|
||||
* 基于流场导航移动,包含:
|
||||
* - 路径规划
|
||||
* - 避免与其他僵尸重叠
|
||||
* - 墙壁碰撞检测
|
||||
*
|
||||
* @param map 游戏地图
|
||||
* @param dt 时间增量(秒)
|
||||
* @param otherZombies 其他僵尸集合
|
||||
*/
|
||||
public void move(GameMap map, float dt, Collection<Zombie> otherZombies) {
|
||||
if (!map.isFlowFieldValid()) return;
|
||||
|
||||
@@ -101,7 +131,7 @@ public class Zombie {
|
||||
nextGridX = currentGridX;
|
||||
nextGridY = currentGridY + (int) Math.signum(dirY);
|
||||
} else {
|
||||
hasReservation = false;
|
||||
reservation = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -113,14 +143,14 @@ public class Zombie {
|
||||
nextGridX = altDirs[0];
|
||||
nextGridY = altDirs[1];
|
||||
} else {
|
||||
hasReservation = false;
|
||||
reservation = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
reservedGridX = nextGridX;
|
||||
reservedGridY = nextGridY;
|
||||
hasReservation = true;
|
||||
reservation = true;
|
||||
|
||||
targetX = nextGridX + 0.5f;
|
||||
targetY = nextGridY + 0.5f;
|
||||
@@ -223,6 +253,9 @@ public class Zombie {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查指定格子是否被其他僵尸占用或预留
|
||||
*/
|
||||
private boolean isGridOccupiedOrReserved(int gridX, int gridY, Collection<Zombie> otherZombies) {
|
||||
for (Zombie other : otherZombies) {
|
||||
if (other.getId() == this.id) continue;
|
||||
@@ -235,13 +268,18 @@ public class Zombie {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (other.hasReservation() && other.getReservedGridX() == gridX && other.getReservedGridY() == gridY) {
|
||||
if (other.isReservation() && other.getReservedGridX() == gridX && other.getReservedGridY() == gridY) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 寻找替代移动方向
|
||||
*
|
||||
* 当目标格子被占用时,寻找其他可行方向
|
||||
*/
|
||||
private int[] findAlternativeDirection(int currentGridX, int currentGridY, float dirX, float dirY,
|
||||
GameMap map, Collection<Zombie> otherZombies) {
|
||||
int[][] allDirs = {{0, -1}, {0, 1}, {-1, 0}, {1, 0}, {-1, -1}, {1, -1}, {-1, 1}, {1, 1}};
|
||||
@@ -274,29 +312,63 @@ public class Zombie {
|
||||
return new int[]{candidates.get(0)[0], candidates.get(0)[1]};
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否可以近战攻击
|
||||
*
|
||||
* @param now 当前时间戳
|
||||
* @return true 表示可以攻击
|
||||
*/
|
||||
public boolean canAttack(long now) {
|
||||
return now - lastAttackTime >= Constants.ZOMBIE_ATTACK_RATE * 1000;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行近战攻击
|
||||
*
|
||||
* @param now 当前时间戳
|
||||
*/
|
||||
public void attack(long now) {
|
||||
lastAttackTime = now;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否可以远程攻击(精英僵尸专用)
|
||||
*
|
||||
* @param now 当前时间戳
|
||||
* @return true 表示可以攻击
|
||||
*/
|
||||
public boolean canRangedAttack(long now) {
|
||||
if (!isElite) return false;
|
||||
return now - lastRangedAttackTime >= Constants.ELITE_ZOMBIE_ATTACK_RATE * 1000;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行远程攻击(精英僵尸专用)
|
||||
*
|
||||
* @param now 当前时间戳
|
||||
*/
|
||||
public void rangedAttack(long now) {
|
||||
lastRangedAttackTime = now;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算到指定点的距离
|
||||
*
|
||||
* @param px 目标X坐标
|
||||
* @param py 目标Y坐标
|
||||
* @return 距离值
|
||||
*/
|
||||
public float distanceTo(float px, float py) {
|
||||
float dx = px - x;
|
||||
float dy = py - y;
|
||||
return (float) Math.sqrt(dx * dx + dy * dy);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将僵尸状态转换为Map格式,用于网络传输
|
||||
*
|
||||
* @return 包含僵尸状态的Map
|
||||
*/
|
||||
public Map<String, Object> toStateMap() {
|
||||
Map<String, Object> map = new LinkedHashMap<>();
|
||||
map.put("id", id);
|
||||
@@ -305,6 +377,7 @@ public class Zombie {
|
||||
map.put("angle", angle);
|
||||
map.put("health", health);
|
||||
map.put("isElite", isElite);
|
||||
map.put("isSplitter", isSplitter);
|
||||
return map;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,45 +2,87 @@ package com.zombie.game.server;
|
||||
|
||||
import com.zombie.game.model.GameWorld;
|
||||
|
||||
public class GameLoop implements Runnable {
|
||||
private String roomId;
|
||||
private GameWorld world;
|
||||
private GameWebSocketServer server;
|
||||
private volatile boolean running;
|
||||
private static final int TICK_RATE = 30;
|
||||
private static final long TICK_INTERVAL = 1000 / TICK_RATE;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public GameLoop(String roomId, GameWorld world, GameWebSocketServer server) {
|
||||
/**
|
||||
* 游戏循环类
|
||||
*
|
||||
* 管理游戏主循环,以固定帧率更新游戏世界状态。
|
||||
* 使用 ScheduledExecutorService 实现精确的定时调度,避免忙等待。
|
||||
*/
|
||||
public class GameLoop {
|
||||
/** 房间ID */
|
||||
private String roomId;
|
||||
/** 游戏世界实例 */
|
||||
private GameWorld world;
|
||||
/** 游戏状态广播回调 */
|
||||
private GameService.GameStateBroadcast broadcaster;
|
||||
/** 循环运行标志 */
|
||||
private volatile boolean running;
|
||||
/** 逻辑帧率(每秒 tick 数) */
|
||||
private static final int TICK_RATE = 30;
|
||||
/** 每次 tick 的时间间隔(毫秒) */
|
||||
private static final long TICK_INTERVAL_MS = 1000 / TICK_RATE;
|
||||
/** 每次 tick 的时间间隔(秒,用于游戏逻辑计算) */
|
||||
private static final float TICK_INTERVAL_SEC = 1.0f / TICK_RATE;
|
||||
/** 定时任务执行器 */
|
||||
private ScheduledExecutorService scheduler;
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
* @param roomId 房间ID
|
||||
* @param world 游戏世界实例
|
||||
* @param broadcaster 游戏状态广播回调
|
||||
*/
|
||||
public GameLoop(String roomId, GameWorld world, GameService.GameStateBroadcast broadcaster) {
|
||||
this.roomId = roomId;
|
||||
this.world = world;
|
||||
this.server = server;
|
||||
this.broadcaster = broadcaster;
|
||||
this.running = true;
|
||||
this.scheduler = Executors.newSingleThreadScheduledExecutor(r -> {
|
||||
Thread t = new Thread(r, "GameLoop-" + roomId);
|
||||
t.setDaemon(true);
|
||||
return t;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
long lastTime = System.currentTimeMillis();
|
||||
/**
|
||||
* 启动游戏循环
|
||||
*/
|
||||
public void start() {
|
||||
scheduler.scheduleAtFixedRate(this::tick, 0, TICK_INTERVAL_MS, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
while (running) {
|
||||
long now = System.currentTimeMillis();
|
||||
long delta = now - lastTime;
|
||||
/**
|
||||
* 单次游戏逻辑更新
|
||||
*/
|
||||
private void tick() {
|
||||
if (!running) {
|
||||
return;
|
||||
}
|
||||
synchronized (world.getLock()) {
|
||||
world.update(TICK_INTERVAL_SEC);
|
||||
}
|
||||
broadcaster.broadcast(roomId, world);
|
||||
}
|
||||
|
||||
if (delta >= TICK_INTERVAL) {
|
||||
float dt = delta / 1000.0f;
|
||||
world.update(dt);
|
||||
server.broadcastGameState(roomId, world);
|
||||
lastTime = now;
|
||||
} else {
|
||||
try {
|
||||
Thread.sleep(1);
|
||||
} catch (InterruptedException e) {
|
||||
break;
|
||||
/**
|
||||
* 停止游戏循环
|
||||
*/
|
||||
public void stop() {
|
||||
running = false;
|
||||
if (scheduler != null && !scheduler.isShutdown()) {
|
||||
scheduler.shutdown();
|
||||
try {
|
||||
if (!scheduler.awaitTermination(1, TimeUnit.SECONDS)) {
|
||||
scheduler.shutdownNow();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
scheduler.shutdownNow();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
running = false;
|
||||
}
|
||||
}
|
||||
|
||||
179
backend/src/main/java/com/zombie/game/server/GameService.java
Normal file
179
backend/src/main/java/com/zombie/game/server/GameService.java
Normal file
@@ -0,0 +1,179 @@
|
||||
package com.zombie.game.server;
|
||||
|
||||
import com.zombie.game.model.*;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* 游戏服务类
|
||||
*
|
||||
* 负责游戏生命周期管理,包括:
|
||||
* - 游戏启动和停止
|
||||
* - 游戏世界状态管理
|
||||
* - 玩家输入处理
|
||||
* - 游戏状态构建
|
||||
*
|
||||
* 将游戏逻辑从 WebSocket 层解耦,便于独立测试和维护。
|
||||
*/
|
||||
public class GameService {
|
||||
private static final Logger logger = LoggerFactory.getLogger(GameService.class);
|
||||
|
||||
/** 活跃游戏世界集合:roomId -> GameWorld */
|
||||
private final Map<String, GameWorld> activeGames = new ConcurrentHashMap<>();
|
||||
/** 游戏循环集合:roomId -> GameLoop */
|
||||
private final Map<String, GameLoop> gameLoops = new ConcurrentHashMap<>();
|
||||
/** 游戏状态广播回调 */
|
||||
private final GameStateBroadcast broadcaster;
|
||||
|
||||
/**
|
||||
* 游戏状态广播接口
|
||||
*/
|
||||
public interface GameStateBroadcast {
|
||||
void broadcast(String roomId, GameWorld world);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
* @param broadcaster 状态广播回调
|
||||
*/
|
||||
public GameService(GameStateBroadcast broadcaster) {
|
||||
this.broadcaster = broadcaster;
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动游戏
|
||||
*
|
||||
* @param room 游戏房间
|
||||
* @return 游戏初始化数据,按玩家ID分组
|
||||
*/
|
||||
public Map<String, Map<String, Object>> startGame(Room room) {
|
||||
GameWorld world = new GameWorld();
|
||||
int index = 0;
|
||||
List<int[]> spawnPoints = world.getMap().getSpawnPoints();
|
||||
|
||||
for (Player player : room.getPlayers()) {
|
||||
int[] sp = spawnPoints.get(index % spawnPoints.size());
|
||||
float wx = sp[0] + 0.5f;
|
||||
float wy = sp[1] + 0.5f;
|
||||
|
||||
player.setPosition(wx, wy);
|
||||
world.addPlayer(player);
|
||||
index++;
|
||||
}
|
||||
|
||||
activeGames.put(room.getId(), world);
|
||||
|
||||
Map<String, Map<String, Object>> playerInitData = new LinkedHashMap<>();
|
||||
for (Player player : room.getPlayers()) {
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
data.put("playerId", player.getId());
|
||||
data.put("mapData", serializeMapData(world.getMapData()));
|
||||
|
||||
List<Map<String, Object>> playerList = new ArrayList<>();
|
||||
int idx = 0;
|
||||
for (Player p : room.getPlayers()) {
|
||||
Map<String, Object> pm = new LinkedHashMap<>();
|
||||
pm.put("id", p.getId());
|
||||
pm.put("name", p.getName());
|
||||
pm.put("x", p.getX());
|
||||
pm.put("y", p.getY());
|
||||
pm.put("index", idx++);
|
||||
playerList.add(pm);
|
||||
}
|
||||
data.put("players", playerList);
|
||||
playerInitData.put(player.getId(), data);
|
||||
}
|
||||
|
||||
GameLoop loop = new GameLoop(room.getId(), world, broadcaster);
|
||||
gameLoops.put(room.getId(), loop);
|
||||
loop.start();
|
||||
|
||||
logger.info("Game started for room: {}", room.getId());
|
||||
return playerInitData;
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止游戏
|
||||
*
|
||||
* @param roomId 房间ID
|
||||
*/
|
||||
public void stopGame(String roomId) {
|
||||
GameLoop loop = gameLoops.remove(roomId);
|
||||
if (loop != null) {
|
||||
loop.stop();
|
||||
}
|
||||
activeGames.remove(roomId);
|
||||
logger.info("Game stopped for room: {}", roomId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理玩家输入
|
||||
*
|
||||
* @param roomId 房间ID
|
||||
* @param playerId 玩家ID
|
||||
* @param dx X方向移动量
|
||||
* @param dy Y方向移动量
|
||||
* @param aimX 瞄准X坐标
|
||||
* @param aimY 瞄准Y坐标
|
||||
* @param firing 是否开火
|
||||
* @param weaponIndex 武器索引
|
||||
* @param seq 输入序列号
|
||||
* @param grenadeCharge 手榴弹蓄力值
|
||||
* @param grenadeReleased 手榴弹是否释放
|
||||
*/
|
||||
public void processPlayerInput(String roomId, String playerId,
|
||||
float dx, float dy, float aimX, float aimY,
|
||||
boolean firing, int weaponIndex, int seq,
|
||||
float grenadeCharge, boolean grenadeReleased) {
|
||||
GameWorld world = activeGames.get(roomId);
|
||||
if (world == null) return;
|
||||
|
||||
synchronized (world.getLock()) {
|
||||
Player player = world.getPlayer(playerId);
|
||||
if (player == null || !player.isAlive()) return;
|
||||
|
||||
if (weaponIndex >= 0 && weaponIndex <= 3) {
|
||||
player.setWeaponIndex(weaponIndex);
|
||||
}
|
||||
|
||||
player.applyMovement(dx, dy, world.getMap());
|
||||
player.setAngle(aimX, aimY);
|
||||
player.setLastProcessedSeq(seq);
|
||||
|
||||
if (grenadeReleased && player.hasAmmo() && player.getWeaponIndex() == 3) {
|
||||
world.fireWeapon(player, aimX, aimY, grenadeCharge);
|
||||
} else if (firing && player.hasAmmo() && player.getWeaponIndex() != 3) {
|
||||
world.fireWeapon(player, aimX, aimY);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查房间是否有活跃游戏
|
||||
*
|
||||
* @param roomId 房间ID
|
||||
* @return true 表示有活跃游戏
|
||||
*/
|
||||
public boolean hasActiveGame(String roomId) {
|
||||
return activeGames.containsKey(roomId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 序列化地图数据为二维列表
|
||||
*/
|
||||
private List<List<Integer>> serializeMapData(int[][] cells) {
|
||||
List<List<Integer>> result = new ArrayList<>();
|
||||
for (int[] row : cells) {
|
||||
List<Integer> rowList = new ArrayList<>();
|
||||
for (int cell : row) {
|
||||
rowList.add(cell);
|
||||
}
|
||||
result.add(rowList);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -6,38 +6,68 @@ import com.zombie.game.model.*;
|
||||
import org.java_websocket.WebSocket;
|
||||
import org.java_websocket.handshake.ClientHandshake;
|
||||
import org.java_websocket.server.WebSocketServer;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* 游戏 WebSocket 服务器
|
||||
*
|
||||
* 处理客户端连接和消息,协调房间管理和游戏实例。
|
||||
* 主要功能:
|
||||
* - 房间创建、加入、离开
|
||||
* - 玩家准备和游戏开始
|
||||
* - 玩家输入处理
|
||||
* - 游戏状态广播
|
||||
*/
|
||||
public class GameWebSocketServer extends WebSocketServer {
|
||||
private static final Logger logger = LoggerFactory.getLogger(GameWebSocketServer.class);
|
||||
/** JSON 序列化工具 */
|
||||
private Gson gson;
|
||||
private Map<String, Room> rooms;
|
||||
/** 房间管理器 */
|
||||
private RoomManager roomManager;
|
||||
/** 游戏服务 */
|
||||
private GameService gameService;
|
||||
/** WebSocket 连接到玩家ID的映射 */
|
||||
private Map<WebSocket, String> connectionToPlayer;
|
||||
/** 玩家ID到WebSocket连接的映射 */
|
||||
private Map<String, WebSocket> playerToConnection;
|
||||
private Map<String, GameWorld> activeGames;
|
||||
private Map<String, GameLoop> gameLoops;
|
||||
/** 房间列表广播定时器 */
|
||||
private Timer roomListTimer;
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
* @param port 监听端口号
|
||||
*/
|
||||
public GameWebSocketServer(int port) {
|
||||
super(new InetSocketAddress(port));
|
||||
this.gson = new Gson();
|
||||
this.rooms = new ConcurrentHashMap<>();
|
||||
this.roomManager = new RoomManager();
|
||||
this.connectionToPlayer = new ConcurrentHashMap<>();
|
||||
this.playerToConnection = new ConcurrentHashMap<>();
|
||||
this.activeGames = new ConcurrentHashMap<>();
|
||||
this.gameLoops = new ConcurrentHashMap<>();
|
||||
this.gameService = new GameService(this::broadcastGameState);
|
||||
}
|
||||
|
||||
/**
|
||||
* 新连接建立时的回调
|
||||
*/
|
||||
@Override
|
||||
public void onOpen(WebSocket conn, ClientHandshake handshake) {
|
||||
System.out.println("New connection: " + conn.getRemoteSocketAddress());
|
||||
logger.info("New connection: {}", conn.getRemoteSocketAddress());
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接关闭时的回调
|
||||
*
|
||||
* 清理玩家数据并处理离开房间
|
||||
*/
|
||||
@Override
|
||||
public void onClose(WebSocket conn, int code, String reason, boolean remote) {
|
||||
System.out.println("Connection closed: " + conn.getRemoteSocketAddress());
|
||||
logger.info("Connection closed: {}", conn.getRemoteSocketAddress());
|
||||
String playerId = connectionToPlayer.remove(conn);
|
||||
if (playerId != null) {
|
||||
playerToConnection.remove(playerId);
|
||||
@@ -45,6 +75,11 @@ public class GameWebSocketServer extends WebSocketServer {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 收到消息时的回调
|
||||
*
|
||||
* 根据消息类型分发到对应的处理方法
|
||||
*/
|
||||
@Override
|
||||
public void onMessage(WebSocket conn, String message) {
|
||||
try {
|
||||
@@ -76,19 +111,27 @@ public class GameWebSocketServer extends WebSocketServer {
|
||||
break;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
logger.error("Error processing message from {}", conn.getRemoteSocketAddress(), e);
|
||||
sendError(conn, "Invalid message format");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发生错误时的回调
|
||||
*/
|
||||
@Override
|
||||
public void onError(WebSocket conn, Exception ex) {
|
||||
ex.printStackTrace();
|
||||
logger.error("WebSocket error on connection {}", conn != null ? conn.getRemoteSocketAddress() : "null", ex);
|
||||
}
|
||||
|
||||
/**
|
||||
* 服务器启动时的回调
|
||||
*
|
||||
* 启动房间列表广播定时器
|
||||
*/
|
||||
@Override
|
||||
public void onStart() {
|
||||
System.out.println("Game WebSocket Server started on port " + getPort());
|
||||
logger.info("Game WebSocket Server started on port {}", getPort());
|
||||
roomListTimer = new Timer(true);
|
||||
roomListTimer.scheduleAtFixedRate(new TimerTask() {
|
||||
@Override
|
||||
@@ -98,8 +141,15 @@ public class GameWebSocketServer extends WebSocketServer {
|
||||
}, 0, 2000);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理创建房间请求
|
||||
*/
|
||||
private void handleCreateRoom(WebSocket conn, JsonObject data) {
|
||||
String playerName = data.get("playerName").getAsString();
|
||||
if (!MessageUtils.hasRequired(data, "playerName")) {
|
||||
sendError(conn, "Missing playerName");
|
||||
return;
|
||||
}
|
||||
String playerName = MessageUtils.getString(data, "playerName");
|
||||
String playerId = UUID.randomUUID().toString();
|
||||
String roomId = UUID.randomUUID().toString().substring(0, 8);
|
||||
|
||||
@@ -107,17 +157,24 @@ public class GameWebSocketServer extends WebSocketServer {
|
||||
playerToConnection.put(playerId, conn);
|
||||
|
||||
Room room = new Room(roomId, playerId, playerName);
|
||||
rooms.put(roomId, room);
|
||||
roomManager.addRoom(roomId, room);
|
||||
|
||||
sendToConnection(conn, Constants.MSG_ROOM_STATE, room.toStateMap(playerId));
|
||||
System.out.println("Room created: " + roomId + " by " + playerName);
|
||||
logger.info("Room created: {} by {}", roomId, playerName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理加入房间请求
|
||||
*/
|
||||
private void handleJoinRoom(WebSocket conn, JsonObject data) {
|
||||
String roomId = data.get("roomId").getAsString();
|
||||
String playerName = data.get("playerName").getAsString();
|
||||
if (!MessageUtils.hasRequired(data, "roomId", "playerName")) {
|
||||
sendError(conn, "Missing roomId or playerName");
|
||||
return;
|
||||
}
|
||||
String roomId = MessageUtils.getString(data, "roomId");
|
||||
String playerName = MessageUtils.getString(data, "playerName");
|
||||
|
||||
Room room = rooms.get(roomId);
|
||||
Room room = roomManager.getRoom(roomId);
|
||||
if (room == null) {
|
||||
sendError(conn, "Room not found");
|
||||
return;
|
||||
@@ -133,191 +190,151 @@ public class GameWebSocketServer extends WebSocketServer {
|
||||
playerToConnection.put(playerId, conn);
|
||||
|
||||
room.addPlayer(playerId, playerName);
|
||||
roomManager.joinRoom(roomId, playerId, playerName);
|
||||
|
||||
broadcastRoomState(room);
|
||||
System.out.println("Player " + playerName + " joined room " + roomId);
|
||||
logger.info("Player {} joined room {}", playerName, roomId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过连接处理离开房间
|
||||
*/
|
||||
private void handleLeaveRoomByConn(WebSocket conn) {
|
||||
String playerId = connectionToPlayer.get(conn);
|
||||
if (playerId == null) return;
|
||||
handleLeaveRoomByPlayerId(playerId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过玩家ID处理离开房间
|
||||
*/
|
||||
private void handleLeaveRoomByPlayerId(String playerId) {
|
||||
for (Room room : new ArrayList<>(rooms.values())) {
|
||||
if (room.getPlayer(playerId) != null) {
|
||||
room.removePlayer(playerId);
|
||||
if (room.getPlayerCount() == 0) {
|
||||
stopGame(room.getId());
|
||||
rooms.remove(room.getId());
|
||||
} else {
|
||||
broadcastRoomState(room);
|
||||
}
|
||||
break;
|
||||
}
|
||||
Room room = roomManager.leaveRoom(playerId);
|
||||
if (room == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Room updatedRoom = roomManager.getRoom(room.getId());
|
||||
if (updatedRoom == null) {
|
||||
gameService.stopGame(room.getId());
|
||||
} else {
|
||||
broadcastRoomState(updatedRoom);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理获取房间列表请求
|
||||
*/
|
||||
private void handleRoomList(WebSocket conn) {
|
||||
List<Map<String, Object>> roomList = new ArrayList<>();
|
||||
for (Room room : rooms.values()) {
|
||||
if (!room.isGameStarted()) {
|
||||
roomList.add(room.toRoomListMap());
|
||||
}
|
||||
for (Room room : roomManager.getAvailableRooms()) {
|
||||
roomList.add(room.toRoomListMap());
|
||||
}
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
data.put("rooms", roomList);
|
||||
sendToConnection(conn, Constants.MSG_ROOM_LIST, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理玩家准备请求
|
||||
*/
|
||||
private void handleReady(WebSocket conn) {
|
||||
String playerId = connectionToPlayer.get(conn);
|
||||
if (playerId == null) return;
|
||||
|
||||
for (Room room : rooms.values()) {
|
||||
Player player = room.getPlayer(playerId);
|
||||
if (player != null) {
|
||||
player.setReady(!player.isReady());
|
||||
broadcastRoomState(room);
|
||||
break;
|
||||
}
|
||||
Room room = roomManager.getRoomByPlayerId(playerId);
|
||||
if (room == null) return;
|
||||
|
||||
Player player = room.getPlayer(playerId);
|
||||
if (player != null) {
|
||||
player.setReady(!player.isReady());
|
||||
broadcastRoomState(room);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理开始游戏请求
|
||||
*/
|
||||
private void handleStartGame(WebSocket conn) {
|
||||
String playerId = connectionToPlayer.get(conn);
|
||||
if (playerId == null) return;
|
||||
|
||||
for (Room room : rooms.values()) {
|
||||
if (room.isHost(playerId) && !room.isGameStarted() && room.allReady()) {
|
||||
room.setGameStarted(true);
|
||||
startGame(room);
|
||||
break;
|
||||
}
|
||||
Room room = roomManager.getRoomByPlayerId(playerId);
|
||||
if (room == null) return;
|
||||
|
||||
if (room.isHost(playerId) && !room.isGameStarted() && room.allReady()) {
|
||||
room.setGameStarted(true);
|
||||
startGame(room);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动游戏
|
||||
*
|
||||
* @param room 游戏房间
|
||||
*/
|
||||
private void startGame(Room room) {
|
||||
GameWorld world = new GameWorld();
|
||||
int index = 0;
|
||||
List<int[]> spawnPoints = world.getMap().getSpawnPoints();
|
||||
Map<String, Map<String, Object>> playerInitData = gameService.startGame(room);
|
||||
|
||||
for (Player player : room.getPlayers()) {
|
||||
int[] sp = spawnPoints.get(index % spawnPoints.size());
|
||||
float wx = sp[0] + 0.5f;
|
||||
float wy = sp[1] + 0.5f;
|
||||
|
||||
try {
|
||||
java.lang.reflect.Field xField = Player.class.getDeclaredField("x");
|
||||
xField.setAccessible(true);
|
||||
xField.setFloat(player, wx);
|
||||
java.lang.reflect.Field yField = Player.class.getDeclaredField("y");
|
||||
yField.setAccessible(true);
|
||||
yField.setFloat(player, wy);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
world.addPlayer(player);
|
||||
index++;
|
||||
}
|
||||
|
||||
activeGames.put(room.getId(), world);
|
||||
|
||||
for (Player player : room.getPlayers()) {
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
data.put("playerId", player.getId());
|
||||
data.put("mapData", serializeMapData(world.getMapData()));
|
||||
|
||||
List<Map<String, Object>> playerList = new ArrayList<>();
|
||||
int idx = 0;
|
||||
for (Player p : room.getPlayers()) {
|
||||
Map<String, Object> pm = new LinkedHashMap<>();
|
||||
pm.put("id", p.getId());
|
||||
pm.put("name", p.getName());
|
||||
pm.put("x", p.getX());
|
||||
pm.put("y", p.getY());
|
||||
pm.put("index", idx++);
|
||||
playerList.add(pm);
|
||||
}
|
||||
data.put("players", playerList);
|
||||
|
||||
WebSocket pConn = playerToConnection.get(player.getId());
|
||||
for (Map.Entry<String, Map<String, Object>> entry : playerInitData.entrySet()) {
|
||||
WebSocket pConn = playerToConnection.get(entry.getKey());
|
||||
if (pConn != null) {
|
||||
sendToConnection(pConn, Constants.MSG_GAME_STARTED, data);
|
||||
sendToConnection(pConn, Constants.MSG_GAME_STARTED, entry.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
GameLoop loop = new GameLoop(room.getId(), world, this);
|
||||
gameLoops.put(room.getId(), loop);
|
||||
new Thread(loop).start();
|
||||
|
||||
System.out.println("Game started for room: " + room.getId());
|
||||
}
|
||||
|
||||
private void stopGame(String roomId) {
|
||||
GameLoop loop = gameLoops.remove(roomId);
|
||||
if (loop != null) {
|
||||
loop.stop();
|
||||
}
|
||||
activeGames.remove(roomId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理玩家输入
|
||||
*
|
||||
* 接收并处理玩家的移动、射击、武器切换等输入
|
||||
*/
|
||||
private void handlePlayerInput(WebSocket conn, JsonObject data) {
|
||||
String playerId = connectionToPlayer.get(conn);
|
||||
if (playerId == null) return;
|
||||
|
||||
for (Room room : rooms.values()) {
|
||||
if (room.isGameStarted()) {
|
||||
GameWorld world = activeGames.get(room.getId());
|
||||
if (world == null) continue;
|
||||
Room room = roomManager.getRoomByPlayerId(playerId);
|
||||
if (room == null || !room.isGameStarted()) return;
|
||||
|
||||
Player player = world.getPlayer(playerId);
|
||||
if (player == null) continue;
|
||||
float dx = MessageUtils.getFloat(data, "dx", 0);
|
||||
float dy = MessageUtils.getFloat(data, "dy", 0);
|
||||
float aimX = MessageUtils.getFloat(data, "aimX", 0);
|
||||
float aimY = MessageUtils.getFloat(data, "aimY", 0);
|
||||
boolean firing = MessageUtils.getBoolean(data, "firing", false);
|
||||
int weaponIndex = MessageUtils.getInt(data, "weaponIndex", -1);
|
||||
int seq = MessageUtils.getInt(data, "seq", 0);
|
||||
float grenadeCharge = MessageUtils.getFloat(data, "grenadeCharge", 0);
|
||||
boolean grenadeReleased = MessageUtils.getBoolean(data, "grenadeReleased", false);
|
||||
|
||||
float dx = data.has("dx") ? data.get("dx").getAsFloat() : 0;
|
||||
float dy = data.has("dy") ? data.get("dy").getAsFloat() : 0;
|
||||
float aimX = data.has("aimX") ? data.get("aimX").getAsFloat() : 0;
|
||||
float aimY = data.has("aimY") ? data.get("aimY").getAsFloat() : 0;
|
||||
boolean firing = data.has("firing") && data.get("firing").getAsBoolean();
|
||||
int weaponIndex = data.has("weaponIndex") ? data.get("weaponIndex").getAsInt() : -1;
|
||||
int seq = data.has("seq") ? data.get("seq").getAsInt() : 0;
|
||||
float grenadeCharge = data.has("grenadeCharge") ? data.get("grenadeCharge").getAsFloat() : 0;
|
||||
boolean grenadeReleased = data.has("grenadeReleased") && data.get("grenadeReleased").getAsBoolean();
|
||||
|
||||
if (weaponIndex >= 0 && weaponIndex <= 3) {
|
||||
player.setWeaponIndex(weaponIndex);
|
||||
}
|
||||
|
||||
player.applyMovement(dx, dy, world.getMap());
|
||||
player.setAngle(aimX, aimY);
|
||||
player.setLastProcessedSeq(seq);
|
||||
|
||||
if (grenadeReleased && player.hasAmmo() && player.getWeaponIndex() == 3) {
|
||||
world.fireWeapon(player, aimX, aimY, grenadeCharge);
|
||||
} else if (firing && player.hasAmmo() && player.getWeaponIndex() != 3) {
|
||||
world.fireWeapon(player, aimX, aimY);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
gameService.processPlayerInput(room.getId(), playerId, dx, dy, aimX, aimY,
|
||||
firing, weaponIndex, seq, grenadeCharge, grenadeReleased);
|
||||
}
|
||||
|
||||
public void broadcastGameState(String roomId, GameWorld world) {
|
||||
Room room = rooms.get(roomId);
|
||||
/**
|
||||
* 广播游戏状态给房间内所有玩家
|
||||
*
|
||||
* @param roomId 房间ID
|
||||
* @param world 游戏世界
|
||||
*/
|
||||
private void broadcastGameState(String roomId, GameWorld world) {
|
||||
Room room = roomManager.getRoom(roomId);
|
||||
if (room == null) return;
|
||||
|
||||
for (Player player : room.getPlayers()) {
|
||||
Map<String, Object> state = world.buildGameState(player.getId());
|
||||
WebSocket pConn = playerToConnection.get(player.getId());
|
||||
if (pConn != null && pConn.isOpen()) {
|
||||
sendToConnection(pConn, Constants.MSG_GAME_STATE, state);
|
||||
Map<String, Object> state = null;
|
||||
synchronized (world.getLock()) {
|
||||
for (Player player : room.getPlayers()) {
|
||||
state = world.buildGameState(player.getId());
|
||||
WebSocket pConn = playerToConnection.get(player.getId());
|
||||
if (pConn != null && pConn.isOpen()) {
|
||||
sendToConnection(pConn, Constants.MSG_GAME_STATE, state);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 广播房间状态给房间内所有玩家
|
||||
*/
|
||||
private void broadcastRoomState(Room room) {
|
||||
for (Player player : room.getPlayers()) {
|
||||
WebSocket pConn = playerToConnection.get(player.getId());
|
||||
@@ -327,12 +344,13 @@ public class GameWebSocketServer extends WebSocketServer {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 广播房间列表给所有未加入房间的连接
|
||||
*/
|
||||
private void broadcastRoomList() {
|
||||
List<Map<String, Object>> roomList = new ArrayList<>();
|
||||
for (Room room : rooms.values()) {
|
||||
if (!room.isGameStarted()) {
|
||||
roomList.add(room.toRoomListMap());
|
||||
}
|
||||
for (Room room : roomManager.getAvailableRooms()) {
|
||||
roomList.add(room.toRoomListMap());
|
||||
}
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
data.put("rooms", roomList);
|
||||
@@ -344,18 +362,13 @@ public class GameWebSocketServer extends WebSocketServer {
|
||||
}
|
||||
}
|
||||
|
||||
private List<List<Integer>> serializeMapData(int[][] cells) {
|
||||
List<List<Integer>> result = new ArrayList<>();
|
||||
for (int[] row : cells) {
|
||||
List<Integer> rowList = new ArrayList<>();
|
||||
for (int cell : row) {
|
||||
rowList.add(cell);
|
||||
}
|
||||
result.add(rowList);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送消息给指定连接
|
||||
*
|
||||
* @param conn WebSocket 连接
|
||||
* @param type 消息类型
|
||||
* @param data 消息数据
|
||||
*/
|
||||
private void sendToConnection(WebSocket conn, String type, Object data) {
|
||||
if (conn != null && conn.isOpen()) {
|
||||
Map<String, Object> msg = new LinkedHashMap<>();
|
||||
@@ -365,6 +378,12 @@ public class GameWebSocketServer extends WebSocketServer {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送错误消息
|
||||
*
|
||||
* @param conn WebSocket 连接
|
||||
* @param message 错误消息
|
||||
*/
|
||||
private void sendError(WebSocket conn, String message) {
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
data.put("message", message);
|
||||
|
||||
115
backend/src/main/java/com/zombie/game/server/MessageUtils.java
Normal file
115
backend/src/main/java/com/zombie/game/server/MessageUtils.java
Normal file
@@ -0,0 +1,115 @@
|
||||
package com.zombie.game.server;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
|
||||
/**
|
||||
* 消息工具类
|
||||
*
|
||||
* 提供安全的 JSON 字段读取方法,避免直接调用 getAsXxx() 时抛出异常。
|
||||
*/
|
||||
public class MessageUtils {
|
||||
|
||||
/**
|
||||
* 安全获取字符串字段
|
||||
*
|
||||
* @param data JSON 对象
|
||||
* @param key 字段名
|
||||
* @param defaultValue 默认值
|
||||
* @return 字段值或默认值
|
||||
*/
|
||||
public static String getString(JsonObject data, String key, String defaultValue) {
|
||||
if (data == null || !data.has(key) || data.get(key).isJsonNull()) {
|
||||
return defaultValue;
|
||||
}
|
||||
try {
|
||||
return data.get(key).getAsString();
|
||||
} catch (Exception e) {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全获取字符串字段(无默认值)
|
||||
*
|
||||
* @param data JSON 对象
|
||||
* @param key 字段名
|
||||
* @return 字段值,不存在或类型错误时返回 null
|
||||
*/
|
||||
public static String getString(JsonObject data, String key) {
|
||||
return getString(data, key, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全获取浮点数字段
|
||||
*
|
||||
* @param data JSON 对象
|
||||
* @param key 字段名
|
||||
* @param defaultValue 默认值
|
||||
* @return 字段值或默认值
|
||||
*/
|
||||
public static float getFloat(JsonObject data, String key, float defaultValue) {
|
||||
if (data == null || !data.has(key) || data.get(key).isJsonNull()) {
|
||||
return defaultValue;
|
||||
}
|
||||
try {
|
||||
return data.get(key).getAsFloat();
|
||||
} catch (Exception e) {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全获取整数字段
|
||||
*
|
||||
* @param data JSON 对象
|
||||
* @param key 字段名
|
||||
* @param defaultValue 默认值
|
||||
* @return 字段值或默认值
|
||||
*/
|
||||
public static int getInt(JsonObject data, String key, int defaultValue) {
|
||||
if (data == null || !data.has(key) || data.get(key).isJsonNull()) {
|
||||
return defaultValue;
|
||||
}
|
||||
try {
|
||||
return data.get(key).getAsInt();
|
||||
} catch (Exception e) {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全获取布尔字段
|
||||
*
|
||||
* @param data JSON 对象
|
||||
* @param key 字段名
|
||||
* @param defaultValue 默认值
|
||||
* @return 字段值或默认值
|
||||
*/
|
||||
public static boolean getBoolean(JsonObject data, String key, boolean defaultValue) {
|
||||
if (data == null || !data.has(key) || data.get(key).isJsonNull()) {
|
||||
return defaultValue;
|
||||
}
|
||||
try {
|
||||
return data.get(key).getAsBoolean();
|
||||
} catch (Exception e) {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查必填字段是否存在且有效
|
||||
*
|
||||
* @param data JSON 对象
|
||||
* @param keys 必填字段名列表
|
||||
* @return true 表示所有必填字段都存在
|
||||
*/
|
||||
public static boolean hasRequired(JsonObject data, String... keys) {
|
||||
if (data == null) return false;
|
||||
for (String key : keys) {
|
||||
if (!data.has(key) || data.get(key).isJsonNull()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
147
backend/src/main/java/com/zombie/game/server/RoomManager.java
Normal file
147
backend/src/main/java/com/zombie/game/server/RoomManager.java
Normal file
@@ -0,0 +1,147 @@
|
||||
package com.zombie.game.server;
|
||||
|
||||
import com.zombie.game.model.Player;
|
||||
import com.zombie.game.model.Room;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* 房间管理器
|
||||
*
|
||||
* 负责房间的创建、查询、玩家加入/离开等管理操作。
|
||||
* 维护 playerId -> roomId 的映射,实现 O(1) 的玩家房间查找。
|
||||
*/
|
||||
public class RoomManager {
|
||||
/** 房间集合:roomId -> Room */
|
||||
private final Map<String, Room> rooms = new ConcurrentHashMap<>();
|
||||
/** 玩家到房间的映射:playerId -> roomId */
|
||||
private final Map<String, String> playerToRoom = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 创建房间
|
||||
*
|
||||
* @param roomId 房间ID
|
||||
* @param room 房间实例
|
||||
*/
|
||||
public void addRoom(String roomId, Room room) {
|
||||
rooms.put(roomId, room);
|
||||
for (Player player : room.getPlayers()) {
|
||||
playerToRoom.put(player.getId(), roomId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 玩家加入房间
|
||||
*
|
||||
* @param roomId 房间ID
|
||||
* @param playerId 玩家ID
|
||||
* @param playerName 玩家名称
|
||||
* @return true 表示加入成功
|
||||
*/
|
||||
public boolean joinRoom(String roomId, String playerId, String playerName) {
|
||||
Room room = rooms.get(roomId);
|
||||
if (room == null) return false;
|
||||
if (room.getPlayerCount() >= 4) return false;
|
||||
|
||||
boolean added = room.addPlayer(playerId, playerName);
|
||||
if (added) {
|
||||
playerToRoom.put(playerId, roomId);
|
||||
}
|
||||
return added;
|
||||
}
|
||||
|
||||
/**
|
||||
* 玩家离开房间
|
||||
*
|
||||
* @param playerId 玩家ID
|
||||
* @return 离开后的房间,如果房间已空则返回 null
|
||||
*/
|
||||
public Room leaveRoom(String playerId) {
|
||||
String roomId = playerToRoom.remove(playerId);
|
||||
if (roomId == null) return null;
|
||||
|
||||
Room room = rooms.get(roomId);
|
||||
if (room == null) return null;
|
||||
|
||||
room.removePlayer(playerId);
|
||||
if (room.getPlayerCount() == 0) {
|
||||
rooms.remove(roomId);
|
||||
return null;
|
||||
}
|
||||
return room;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取玩家所在的房间
|
||||
*
|
||||
* @param playerId 玩家ID
|
||||
* @return 房间实例,未找到则返回 null
|
||||
*/
|
||||
public Room getRoomByPlayerId(String playerId) {
|
||||
String roomId = playerToRoom.get(playerId);
|
||||
if (roomId == null) return null;
|
||||
return rooms.get(roomId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定ID的房间
|
||||
*
|
||||
* @param roomId 房间ID
|
||||
* @return 房间实例,未找到则返回 null
|
||||
*/
|
||||
public Room getRoom(String roomId) {
|
||||
return rooms.get(roomId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有房间
|
||||
*
|
||||
* @return 房间集合
|
||||
*/
|
||||
public Collection<Room> getAllRooms() {
|
||||
return rooms.values();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取未开始游戏的房间列表
|
||||
*
|
||||
* @return 可加入的房间列表
|
||||
*/
|
||||
public List<Room> getAvailableRooms() {
|
||||
List<Room> available = new ArrayList<>();
|
||||
for (Room room : rooms.values()) {
|
||||
if (!room.isGameStarted()) {
|
||||
available.add(room);
|
||||
}
|
||||
}
|
||||
return available;
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除房间
|
||||
*
|
||||
* @param roomId 房间ID
|
||||
*/
|
||||
public void removeRoom(String roomId) {
|
||||
Room room = rooms.remove(roomId);
|
||||
if (room != null) {
|
||||
for (Player player : room.getPlayers()) {
|
||||
playerToRoom.remove(player.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查玩家是否在房间中
|
||||
*
|
||||
* @param playerId 玩家ID
|
||||
* @return true 表示在房间中
|
||||
*/
|
||||
public boolean isPlayerInRoom(String playerId) {
|
||||
return playerToRoom.containsKey(playerId);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user