Initial commit
This commit is contained in:
62
backend/dependency-reduced-pom.xml
Normal file
62
backend/dependency-reduced-pom.xml
Normal file
@@ -0,0 +1,62 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>com.zombie</groupId>
|
||||
<artifactId>zombie-crisis-server</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.11.0</version>
|
||||
<configuration>
|
||||
<source>17</source>
|
||||
<target>17</target>
|
||||
<annotationProcessorPaths>
|
||||
<path>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<version>1.18.30</version>
|
||||
</path>
|
||||
</annotationProcessorPaths>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-jar-plugin</artifactId>
|
||||
<version>3.3.0</version>
|
||||
<configuration>
|
||||
<archive>
|
||||
<manifest>
|
||||
<mainClass>com.zombie.game.GameServerMain</mainClass>
|
||||
</manifest>
|
||||
</archive>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<version>3.5.1</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>shade</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<version>1.18.30</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<properties>
|
||||
<maven.compiler.target>17</maven.compiler.target>
|
||||
<maven.compiler.source>17</maven.compiler.source>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
</project>
|
||||
92
backend/pom.xml
Normal file
92
backend/pom.xml
Normal file
@@ -0,0 +1,92 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>com.zombie</groupId>
|
||||
<artifactId>zombie-crisis-server</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>17</maven.compiler.source>
|
||||
<maven.compiler.target>17</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<version>1.18.30</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.java-websocket</groupId>
|
||||
<artifactId>Java-WebSocket</artifactId>
|
||||
<version>1.5.4</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.code.gson</groupId>
|
||||
<artifactId>gson</artifactId>
|
||||
<version>2.10.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
<version>2.15.2</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-simple</artifactId>
|
||||
<version>2.0.9</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.11.0</version>
|
||||
<configuration>
|
||||
<source>17</source>
|
||||
<target>17</target>
|
||||
<annotationProcessorPaths>
|
||||
<path>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<version>1.18.30</version>
|
||||
</path>
|
||||
</annotationProcessorPaths>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-jar-plugin</artifactId>
|
||||
<version>3.3.0</version>
|
||||
<configuration>
|
||||
<archive>
|
||||
<manifest>
|
||||
<mainClass>com.zombie.game.GameServerMain</mainClass>
|
||||
</manifest>
|
||||
</archive>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<version>3.5.1</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>shade</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
34
backend/src/main/java/com/zombie/game/GameServerMain.java
Normal file
34
backend/src/main/java/com/zombie/game/GameServerMain.java
Normal file
@@ -0,0 +1,34 @@
|
||||
package com.zombie.game;
|
||||
|
||||
import com.zombie.game.server.GameWebSocketServer;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class GameServerMain {
|
||||
private static final Logger logger = LoggerFactory.getLogger(GameServerMain.class);
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
int port = 8080;
|
||||
if (args.length > 0) {
|
||||
try {
|
||||
port = Integer.parseInt(args[0]);
|
||||
} catch (NumberFormatException e) {
|
||||
logger.warn("Invalid port number, using default: 8080");
|
||||
}
|
||||
}
|
||||
|
||||
GameWebSocketServer server = new GameWebSocketServer(port);
|
||||
server.start();
|
||||
|
||||
logger.info("ZP2 Server started on port {}", port);
|
||||
|
||||
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
|
||||
logger.info("Shutting down server...");
|
||||
try {
|
||||
server.stop();
|
||||
} catch (Exception e) {
|
||||
logger.error("Error during shutdown", e);
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
133
backend/src/main/java/com/zombie/game/ecs/ECSWorld.java
Normal file
133
backend/src/main/java/com/zombie/game/ecs/ECSWorld.java
Normal file
@@ -0,0 +1,133 @@
|
||||
package com.zombie.game.ecs;
|
||||
|
||||
import com.zombie.game.ecs.components.*;
|
||||
import com.zombie.game.model.Constants;
|
||||
import com.zombie.game.model.GameMap;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/** ECS world that manages all entities, components, and systems / ECS世界,管理所有实体、组件和系统 */
|
||||
@Getter
|
||||
public class ECSWorld {
|
||||
private final Object lock = new Object();
|
||||
private final GameMap map;
|
||||
private int nextEntityId;
|
||||
private final Set<Integer> entities = new LinkedHashSet<>();
|
||||
private final Set<Integer> players = new LinkedHashSet<>();
|
||||
private final Set<Integer> zombies = new LinkedHashSet<>();
|
||||
private final Map<Integer, Position> positions = new HashMap<>();
|
||||
private final Map<Integer, Health> healths = new HashMap<>();
|
||||
private final Map<Integer, Collision> collisions = new HashMap<>();
|
||||
private final Map<Integer, RenderInfo> renderInfos = new HashMap<>();
|
||||
private final Map<Integer, PlayerInput> playerInputs = new HashMap<>();
|
||||
private final Map<Integer, BlocksMovement> blocksMovements = new HashMap<>();
|
||||
private final Map<Integer, MovementCost> movementCosts = new HashMap<>();
|
||||
private final Map<Integer, ZombieAI> zombieAIs = new HashMap<>();
|
||||
private final List<System> systems = new ArrayList<>();
|
||||
private float gameTime;
|
||||
private final Map<String, Integer> playerIdToEntity = new HashMap<>();
|
||||
|
||||
public ECSWorld(String mapFilePath) {
|
||||
this.map = new GameMap(mapFilePath);
|
||||
this.nextEntityId = 0;
|
||||
this.gameTime = 0;
|
||||
|
||||
// Wire GameMap to ECS component maps
|
||||
map.setComponentMaps(blocksMovements, movementCosts);
|
||||
|
||||
// Create wall entities from map data
|
||||
createWallsFromMapData();
|
||||
}
|
||||
|
||||
/** Create static wall and nut wall entities from map JSON data */
|
||||
private void createWallsFromMapData() {
|
||||
for (int[] pos : map.getStaticWallData()) {
|
||||
createStaticWallEntity(pos[0], pos[1]);
|
||||
}
|
||||
for (int[] pos : map.getNutWallData()) {
|
||||
createNutWallEntity(pos[0], pos[1]);
|
||||
}
|
||||
}
|
||||
|
||||
/** Create a static wall entity (indestructible, blocks movement) */
|
||||
public int createStaticWallEntity(int gx, int gy) {
|
||||
int entityId = createEntity();
|
||||
positions.put(entityId, new Position(gx + 0.5f, gy + 0.5f));
|
||||
blocksMovements.put(entityId, new BlocksMovement());
|
||||
map.registerWall(gx, gy, entityId);
|
||||
return entityId;
|
||||
}
|
||||
|
||||
/** Create a nut wall entity (destructible, blocks movement with cost) */
|
||||
public int createNutWallEntity(int gx, int gy) {
|
||||
int entityId = createEntity();
|
||||
positions.put(entityId, new Position(gx + 0.5f, gy + 0.5f));
|
||||
healths.put(entityId, new Health(Constants.NUT_WALL_MAX_HEALTH));
|
||||
blocksMovements.put(entityId, new BlocksMovement());
|
||||
movementCosts.put(entityId, new MovementCost(Constants.NUT_WALL_MOVEMENT_COST));
|
||||
map.registerWall(gx, gy, entityId);
|
||||
return entityId;
|
||||
}
|
||||
|
||||
/** Create a zombie entity for testing pathfinding */
|
||||
public int createZombieEntity(float x, float y) {
|
||||
int entityId = createEntity();
|
||||
positions.put(entityId, new Position(x, y));
|
||||
healths.put(entityId, new Health(100));
|
||||
collisions.put(entityId, new Collision(Constants.ZOMBIE_SIZE));
|
||||
renderInfos.put(entityId, new RenderInfo(RenderInfo.EntityType.ZOMBIE));
|
||||
zombieAIs.put(entityId, new ZombieAI());
|
||||
zombies.add(entityId);
|
||||
return entityId;
|
||||
}
|
||||
|
||||
public int createEntity() {
|
||||
int id = nextEntityId++;
|
||||
entities.add(id);
|
||||
return id;
|
||||
}
|
||||
|
||||
public void destroyEntity(int id) {
|
||||
entities.remove(id);
|
||||
players.remove(id);
|
||||
zombies.remove(id);
|
||||
positions.remove(id);
|
||||
healths.remove(id);
|
||||
collisions.remove(id);
|
||||
renderInfos.remove(id);
|
||||
playerInputs.remove(id);
|
||||
blocksMovements.remove(id);
|
||||
movementCosts.remove(id);
|
||||
zombieAIs.remove(id);
|
||||
}
|
||||
|
||||
public int createPlayerEntity(String playerId, String name, float x, float y) {
|
||||
int entityId = createEntity();
|
||||
positions.put(entityId, new Position(x, y));
|
||||
healths.put(entityId, new Health(100));
|
||||
collisions.put(entityId, new Collision(Constants.PLAYER_SIZE));
|
||||
renderInfos.put(entityId, new RenderInfo(RenderInfo.EntityType.PLAYER));
|
||||
playerInputs.put(entityId, new PlayerInput());
|
||||
players.add(entityId);
|
||||
playerIdToEntity.put(playerId, entityId);
|
||||
return entityId;
|
||||
}
|
||||
|
||||
public Integer getPlayerEntity(String playerId) {
|
||||
return playerIdToEntity.get(playerId);
|
||||
}
|
||||
|
||||
public int[][] getMapData() { return map.getCells(); }
|
||||
|
||||
public void update(float dt) {
|
||||
gameTime += dt;
|
||||
for (System system : systems) {
|
||||
system.update(dt, this);
|
||||
}
|
||||
}
|
||||
|
||||
public void addSystem(System system) {
|
||||
systems.add(system);
|
||||
}
|
||||
}
|
||||
7
backend/src/main/java/com/zombie/game/ecs/System.java
Normal file
7
backend/src/main/java/com/zombie/game/ecs/System.java
Normal file
@@ -0,0 +1,7 @@
|
||||
package com.zombie.game.ecs;
|
||||
|
||||
/** Interface for ECS systems that process entities each frame / ECS系统接口,每帧处理实体 */
|
||||
public interface System {
|
||||
// Updates the system with delta time and world reference / 使用增量时间和世界引用更新系统
|
||||
void update(float dt, ECSWorld world);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.zombie.game.ecs.components;
|
||||
|
||||
/** Tag component: entity blocks movement on its grid cell / 标记组件:实体在其网格格子上阻挡移动 */
|
||||
public class BlocksMovement {
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.zombie.game.ecs.components;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/** Component storing collision size for entity hitboxes / 存储实体碰撞体大小的组件 */
|
||||
@Data
|
||||
public class Collision {
|
||||
// Collision radius/size for hitbox detection / 用于碰撞检测的碰撞体半径/大小
|
||||
private float size;
|
||||
|
||||
// Constructor setting the collision size / 构造函数,设置碰撞体大小
|
||||
public Collision(float size) {
|
||||
this.size = size;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.zombie.game.ecs.components;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/** Component storing entity health points / 存储实体生命值的组件 */
|
||||
@Data
|
||||
public class Health {
|
||||
// Current health value / 当前生命值
|
||||
private float health;
|
||||
// Maximum health value / 最大生命值
|
||||
private float maxHealth;
|
||||
|
||||
// Constructor setting initial and max health / 构造函数,设置初始和最大生命值
|
||||
public Health(float maxHealth) {
|
||||
this.health = maxHealth;
|
||||
this.maxHealth = maxHealth;
|
||||
}
|
||||
|
||||
// Checks if the entity is still alive / 检查实体是否仍然存活
|
||||
public boolean isAlive() {
|
||||
return health > 0;
|
||||
}
|
||||
|
||||
// Resets health to maximum value / 将生命值重置为最大值
|
||||
public void reset() {
|
||||
this.health = maxHealth;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.zombie.game.ecs.components;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/** Component storing extra movement cost for flow field pathfinding / 存储流场寻路额外移动代价的组件 */
|
||||
@Data
|
||||
public class MovementCost {
|
||||
/** Extra cost added on top of base (1.0 for straight, 1.414 for diagonal) / 基础代价之外的额外代价 */
|
||||
private float cost;
|
||||
|
||||
public MovementCost(float cost) {
|
||||
this.cost = cost;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.zombie.game.ecs.components;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/** Component storing player input state for movement and aiming / 存储玩家移动和瞄准输入状态的组件 */
|
||||
@Data
|
||||
public class PlayerInput {
|
||||
// Movement direction X component / 移动方向X分量
|
||||
private float dx;
|
||||
// Movement direction Y component / 移动方向Y分量
|
||||
private float dy;
|
||||
// Aim target X coordinate / 瞄准目标X坐标
|
||||
private float aimX;
|
||||
// Aim target Y coordinate / 瞄准目标Y坐标
|
||||
private float aimY;
|
||||
// Input sequence number for ordering / 用于排序的输入序列号
|
||||
private int seq;
|
||||
|
||||
// Default constructor initializing all values to zero / 默认构造函数,将所有值初始化为零
|
||||
public PlayerInput() {
|
||||
this.dx = 0;
|
||||
this.dy = 0;
|
||||
this.aimX = 0;
|
||||
this.aimY = 0;
|
||||
this.seq = 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.zombie.game.ecs.components;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/** Component storing entity position and facing angle / 存储实体位置和朝向角度的组件 */
|
||||
@Data
|
||||
public class Position {
|
||||
// X coordinate / X坐标
|
||||
private float x;
|
||||
// Y coordinate / Y坐标
|
||||
private float y;
|
||||
// Facing angle in radians / 朝向角度(弧度)
|
||||
private float angle;
|
||||
|
||||
// Default constructor initializing to origin / 默认构造函数,初始化为原点
|
||||
public Position() {
|
||||
this.x = 0;
|
||||
this.y = 0;
|
||||
this.angle = 0;
|
||||
}
|
||||
|
||||
// Constructor with position only / 仅带位置的构造函数
|
||||
public Position(float x, float y) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.angle = 0;
|
||||
}
|
||||
|
||||
// Constructor with position and angle / 带位置和角度的构造函数
|
||||
public Position(float x, float y, float angle) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.angle = angle;
|
||||
}
|
||||
|
||||
// Calculates Euclidean distance to a point / 计算到某点的欧几里得距离
|
||||
public float distanceTo(float px, float py) {
|
||||
float dx = px - x;
|
||||
float dy = py - y;
|
||||
return (float) Math.sqrt(dx * dx + dy * dy);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.zombie.game.ecs.components;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/** Component storing rendering information for an entity / 存储实体渲染信息的组件 */
|
||||
@Data
|
||||
public class RenderInfo {
|
||||
// Enum defining possible entity types / 定义可能实体类型的枚举
|
||||
public enum EntityType {
|
||||
PLAYER,
|
||||
ZOMBIE
|
||||
}
|
||||
|
||||
// The type of this entity / 此实体的类型
|
||||
private EntityType type;
|
||||
|
||||
// Constructor setting the entity type / 构造函数,设置实体类型
|
||||
public RenderInfo(EntityType type) {
|
||||
this.type = type;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.zombie.game.ecs.components;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/** Zombie AI component storing pathfinding target and state / 僵尸AI组件,存储寻路目标和状态 */
|
||||
@Data
|
||||
public class ZombieAI {
|
||||
/** Current movement target X / 当前移动目标X */
|
||||
private float targetX;
|
||||
/** Current movement target Y / 当前移动目标Y */
|
||||
private float targetY;
|
||||
/** Whether zombie has an active target / 是否有活跃目标 */
|
||||
private boolean hasTarget;
|
||||
/** Reserved grid X to prevent overlap / 预留网格X防止重叠 */
|
||||
private int reservedGridX = -1;
|
||||
/** Reserved grid Y to prevent overlap / 预留网格Y防止重叠 */
|
||||
private int reservedGridY = -1;
|
||||
/** Whether zombie holds a grid reservation / 是否持有网格预留 */
|
||||
private boolean reservation;
|
||||
|
||||
public ZombieAI() {
|
||||
this.targetX = 0;
|
||||
this.targetY = 0;
|
||||
this.hasTarget = false;
|
||||
}
|
||||
}
|
||||
56
backend/src/main/java/com/zombie/game/model/Constants.java
Normal file
56
backend/src/main/java/com/zombie/game/model/Constants.java
Normal file
@@ -0,0 +1,56 @@
|
||||
package com.zombie.game.model;
|
||||
|
||||
/** Game constants / 游戏常量 */
|
||||
public class Constants {
|
||||
// Grid cell size in pixels / 网格单元格大小(像素)
|
||||
public static final int GRID_SIZE = 32;
|
||||
// Tick rate per second / 每秒逻辑帧率
|
||||
public static final int TICK_RATE = 30;
|
||||
// Duration of one tick in seconds / 每帧持续时间(秒)
|
||||
public static final float TICK_INTERVAL = 1.0f / TICK_RATE;
|
||||
// Player collision size / 玩家碰撞体积
|
||||
public static final float PLAYER_SIZE = 0.8f;
|
||||
// Player movement speed / 玩家移动速度
|
||||
public static final float PLAYER_SPEED = 5.0f;
|
||||
// Zombie collision size / 僵尸碰撞体积
|
||||
public static final float ZOMBIE_SIZE = 0.8f;
|
||||
// Zombie movement speed / 僵尸移动速度
|
||||
public static final float ZOMBIE_SPEED = 2.0f;
|
||||
// Nut wall max health / 坚果墙最大生命值
|
||||
public static final float NUT_WALL_MAX_HEALTH = 500.0f;
|
||||
// Nut wall extra movement cost for flow field / 坚果墙流场额外移动代价
|
||||
public static final float NUT_WALL_MOVEMENT_COST = 20.0f;
|
||||
// Grid center threshold for retarget / 到达网格中心后重新选方向的阈值
|
||||
public static final float GRID_CENTER_THRESHOLD = 0.15f;
|
||||
// Zombie spawn interval in seconds / 僵尸刷新间隔(秒)
|
||||
public static final float ZOMBIE_SPAWN_INTERVAL = 3.0f;
|
||||
// Max zombie count / 最大僵尸数量
|
||||
public static final int ZOMBIE_MAX_COUNT = 20;
|
||||
|
||||
// Create room message type / 创建房间消息类型
|
||||
public static final String MSG_CREATE_ROOM = "create_room";
|
||||
// Join room message type / 加入房间消息类型
|
||||
public static final String MSG_JOIN_ROOM = "join_room";
|
||||
// Leave room message type / 离开房间消息类型
|
||||
public static final String MSG_LEAVE_ROOM = "leave_room";
|
||||
// Room list message type / 房间列表消息类型
|
||||
public static final String MSG_ROOM_LIST = "room_list";
|
||||
// Room state message type / 房间状态消息类型
|
||||
public static final String MSG_ROOM_STATE = "room_state";
|
||||
// Ready state message type / 准备状态消息类型
|
||||
public static final String MSG_READY = "ready";
|
||||
// Start game message type / 开始游戏消息类型
|
||||
public static final String MSG_START_GAME = "start_game";
|
||||
// Game started notification / 游戏已开始通知
|
||||
public static final String MSG_GAME_STARTED = "game_started";
|
||||
// Player input message type / 玩家输入消息类型
|
||||
public static final String MSG_PLAYER_INPUT = "player_input";
|
||||
// Game state broadcast / 游戏状态广播
|
||||
public static final String MSG_GAME_STATE = "game_state";
|
||||
// Player joined notification / 玩家加入通知
|
||||
public static final String MSG_PLAYER_JOIN = "player_join";
|
||||
// Player left notification / 玩家离开通知
|
||||
public static final String MSG_PLAYER_LEAVE = "player_leave";
|
||||
// Error message type / 错误消息类型
|
||||
public static final String MSG_ERROR = "error";
|
||||
}
|
||||
194
backend/src/main/java/com/zombie/game/model/FlowField.java
Normal file
194
backend/src/main/java/com/zombie/game/model/FlowField.java
Normal file
@@ -0,0 +1,194 @@
|
||||
package com.zombie.game.model;
|
||||
|
||||
import com.zombie.game.ecs.components.BlocksMovement;
|
||||
import com.zombie.game.ecs.components.MovementCost;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/** Flow field navigation using Dijkstra with ECS component-based wall queries / 基于ECS组件查询的Dijkstra流场导航 */
|
||||
public class FlowField {
|
||||
private final int width;
|
||||
private final int height;
|
||||
private float[][] distanceField;
|
||||
private float[][] flowFieldX;
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update flow field based on player positions and ECS wall components.
|
||||
* @param wallGrid spatial index "x,y" -> entityId
|
||||
* @param blocksMovements blocks-movement component map
|
||||
* @param movementCosts movement-cost component map
|
||||
* @param playerPositions list of [x, y] player positions
|
||||
*/
|
||||
public void update(Map<String, Integer> wallGrid,
|
||||
Map<Integer, BlocksMovement> blocksMovements,
|
||||
Map<Integer, MovementCost> movementCosts,
|
||||
List<float[]> playerPositions) {
|
||||
for (int y = 0; y < height; y++) {
|
||||
for (int x = 0; x < width; x++) {
|
||||
distanceField[y][x] = Float.MAX_VALUE;
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
if (!isBlocked(wallGrid, blocksMovements, px, py)) {
|
||||
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;
|
||||
|
||||
// Diagonal: check corner blocking
|
||||
if (dir[0] != 0 && dir[1] != 0) {
|
||||
if (isBlocked(wallGrid, blocksMovements, cx + dir[0], cy) ||
|
||||
isBlocked(wallGrid, blocksMovements, cx, cy + dir[1])) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
float moveCost = getMovementCost(wallGrid, blocksMovements, movementCosts, 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Compute flow directions
|
||||
for (int y = 0; y < height; y++) {
|
||||
for (int x = 0; x < width; x++) {
|
||||
if (isBlocked(wallGrid, blocksMovements, 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(wallGrid, blocksMovements, nx, ny)) continue;
|
||||
|
||||
if (dir[0] != 0 && dir[1] != 0) {
|
||||
if (isBlocked(wallGrid, blocksMovements, x + dir[0], y) ||
|
||||
isBlocked(wallGrid, blocksMovements, 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;
|
||||
}
|
||||
|
||||
/** Get flow direction at world position / 获取世界坐标处的流场方向 */
|
||||
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]};
|
||||
}
|
||||
|
||||
/** Get distance to nearest player at world position / 获取世界坐标处到最近玩家的距离 */
|
||||
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, Integer> wallGrid,
|
||||
Map<Integer, BlocksMovement> blocksMovements,
|
||||
int gx, int gy) {
|
||||
if (gx < 0 || gx >= width || gy < 0 || gy >= height) return true;
|
||||
Integer entityId = wallGrid.get(gx + "," + gy);
|
||||
if (entityId == null) return false;
|
||||
return blocksMovements.containsKey(entityId);
|
||||
}
|
||||
|
||||
private float getMovementCost(Map<String, Integer> wallGrid,
|
||||
Map<Integer, BlocksMovement> blocksMovements,
|
||||
Map<Integer, MovementCost> movementCosts,
|
||||
int gx, int gy, int[] dir) {
|
||||
float baseCost = (dir[0] != 0 && dir[1] != 0) ? 1.414f : 1.0f;
|
||||
Integer entityId = wallGrid.get(gx + "," + gy);
|
||||
if (entityId == null) return baseCost;
|
||||
// Completely blocked (no MovementCost but has BlocksMovement)
|
||||
if (blocksMovements.containsKey(entityId) && !movementCosts.containsKey(entityId)) {
|
||||
return Float.MAX_VALUE;
|
||||
}
|
||||
MovementCost mc = movementCosts.get(entityId);
|
||||
if (mc != null) {
|
||||
return baseCost + mc.getCost();
|
||||
}
|
||||
return baseCost;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
197
backend/src/main/java/com/zombie/game/model/GameMap.java
Normal file
197
backend/src/main/java/com/zombie/game/model/GameMap.java
Normal file
@@ -0,0 +1,197 @@
|
||||
package com.zombie.game.model;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.zombie.game.ecs.components.BlocksMovement;
|
||||
import com.zombie.game.ecs.components.MovementCost;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.*;
|
||||
|
||||
/** Game map: spatial index for wall entities, flow field, and spawn points / 游戏地图:墙体实体空间索引、流场和出生点 */
|
||||
@Getter
|
||||
public class GameMap {
|
||||
private final int[][] cells;
|
||||
private final int width;
|
||||
private final int height;
|
||||
/** Spatial index: "x,y" -> wall entity ID / 空间索引:"x,y" -> 墙体实体ID */
|
||||
private final Map<String, Integer> wallGrid = new HashMap<>();
|
||||
private final List<int[]> spawnPoints;
|
||||
/** Raw static wall positions loaded from JSON / 从JSON加载的静态墙位置 */
|
||||
private final List<int[]> staticWallData = new ArrayList<>();
|
||||
/** Raw nut wall positions loaded from JSON / 从JSON加载的坚果墙位置 */
|
||||
private final List<int[]> nutWallData = new ArrayList<>();
|
||||
/** Flow field for zombie pathfinding / 僵尸寻路流场 */
|
||||
private final FlowField flowField;
|
||||
|
||||
// References to ECS component maps, set by ECSWorld after construction
|
||||
private Map<Integer, BlocksMovement> blocksMovements;
|
||||
private Map<Integer, MovementCost> movementCosts;
|
||||
|
||||
public GameMap(String mapFilePath) {
|
||||
JsonNode root = loadMapFile(mapFilePath);
|
||||
this.width = root.get("width").asInt();
|
||||
this.height = root.get("height").asInt();
|
||||
this.cells = new int[height][width];
|
||||
this.spawnPoints = new ArrayList<>();
|
||||
this.flowField = new FlowField(width, height);
|
||||
loadFromJson(root);
|
||||
}
|
||||
|
||||
/** Set ECS component map references (called by ECSWorld) */
|
||||
public void setComponentMaps(Map<Integer, BlocksMovement> bm, Map<Integer, MovementCost> mc) {
|
||||
this.blocksMovements = bm;
|
||||
this.movementCosts = mc;
|
||||
}
|
||||
|
||||
/** Register a wall entity in the spatial index */
|
||||
public void registerWall(int gx, int gy, int entityId) {
|
||||
wallGrid.put(key(gx, gy), entityId);
|
||||
}
|
||||
|
||||
/** Unregister a wall entity and invalidate flow field */
|
||||
public void unregisterWall(int gx, int gy) {
|
||||
wallGrid.remove(key(gx, gy));
|
||||
flowField.invalidate();
|
||||
}
|
||||
|
||||
/** Check if grid cell blocks movement / 检查网格格子是否阻挡移动 */
|
||||
public boolean isWall(int gx, int gy) {
|
||||
if (gx < 0 || gx >= width || gy < 0 || gy >= height) return true;
|
||||
Integer entityId = wallGrid.get(key(gx, gy));
|
||||
if (entityId == null) return false;
|
||||
return blocksMovements != null && blocksMovements.containsKey(entityId);
|
||||
}
|
||||
|
||||
/** Check if world-space area is walkable / 检查世界坐标区域是否可行走 */
|
||||
public boolean isWalkable(float wx, float wy, float size) {
|
||||
float half = size / 2;
|
||||
float[][] corners = {
|
||||
{wx - half, wy - half}, {wx + half, wy - half},
|
||||
{wx - half, wy + half}, {wx + half, wy + half}
|
||||
};
|
||||
for (float[] c : corners) {
|
||||
int gx = (int) Math.floor(c[0]);
|
||||
int gy = (int) Math.floor(c[1]);
|
||||
if (isWall(gx, gy)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Update flow field with current player positions */
|
||||
public void updateFlowField(List<float[]> playerPositions) {
|
||||
flowField.update(wallGrid, blocksMovements, movementCosts, playerPositions);
|
||||
}
|
||||
|
||||
/** Get flow direction at world position */
|
||||
public float[] getFlowDirection(float wx, float wy) {
|
||||
return flowField.getDirection(wx, wy);
|
||||
}
|
||||
|
||||
/** Check if flow field is valid */
|
||||
public boolean isFlowFieldValid() {
|
||||
return flowField.isValid();
|
||||
}
|
||||
|
||||
/** Get cell grid with wall info (for sync) / 获取包含墙壁信息的格子网格 */
|
||||
public int[][] getCells() {
|
||||
int[][] result = new int[height][width];
|
||||
for (int y = 0; y < height; y++) {
|
||||
for (int x = 0; x < width; x++) {
|
||||
result[y][x] = cells[y][x];
|
||||
}
|
||||
}
|
||||
// Mark wall cells from spatial index
|
||||
for (Map.Entry<String, Integer> entry : wallGrid.entrySet()) {
|
||||
String[] parts = entry.getKey().split(",");
|
||||
int x = Integer.parseInt(parts[0]);
|
||||
int y = Integer.parseInt(parts[1]);
|
||||
if (x >= 0 && x < width && y >= 0 && y < height) {
|
||||
Integer entityId = entry.getValue();
|
||||
if (blocksMovements != null && blocksMovements.containsKey(entityId)) {
|
||||
// Check if it's a nut wall (has MovementCost) or static wall
|
||||
if (movementCosts != null && movementCosts.containsKey(entityId)) {
|
||||
result[y][x] = 4; // nut wall
|
||||
} else {
|
||||
result[y][x] = 1; // static wall
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Get player spawn points */
|
||||
public List<int[]> getSpawnPoints() {
|
||||
return spawnPoints;
|
||||
}
|
||||
|
||||
// --- Private ---
|
||||
|
||||
private JsonNode loadMapFile(String mapFilePath) {
|
||||
try {
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
return mapper.readTree(new File(mapFilePath));
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to load map file: " + mapFilePath, e);
|
||||
}
|
||||
}
|
||||
|
||||
private void loadFromJson(JsonNode root) {
|
||||
JsonNode wallsNode = root.get("walls");
|
||||
if (wallsNode != null && wallsNode.isArray()) {
|
||||
for (JsonNode wallNode : wallsNode) {
|
||||
int x = (int) wallNode.get("x").asDouble();
|
||||
int y = (int) wallNode.get("y").asDouble();
|
||||
String type = wallNode.get("type").asText();
|
||||
if (x < 0 || x >= width || y < 0 || y >= height) continue;
|
||||
if ("static".equals(type)) {
|
||||
staticWallData.add(new int[]{x, y});
|
||||
} else if ("nut".equals(type)) {
|
||||
nutWallData.add(new int[]{x, y});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
JsonNode playerSpawns = root.get("playerSpawns");
|
||||
if (playerSpawns != null && playerSpawns.isArray()) {
|
||||
for (JsonNode spawn : playerSpawns) {
|
||||
int x = spawn.get("x").asInt();
|
||||
int y = spawn.get("y").asInt();
|
||||
if (x >= 0 && x < width && y >= 0 && y < height) {
|
||||
cells[y][x] = 2;
|
||||
spawnPoints.add(new int[]{x, y});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
JsonNode zombieSpawns = root.get("zombieSpawns");
|
||||
if (zombieSpawns != null && zombieSpawns.isArray()) {
|
||||
for (JsonNode spawn : zombieSpawns) {
|
||||
int x = spawn.get("x").asInt();
|
||||
int y = spawn.get("y").asInt();
|
||||
if (x >= 0 && x < width && y >= 0 && y < height) {
|
||||
cells[y][x] = 3;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Get zombie spawn points from cells / 从格子中获取僵尸出生点 */
|
||||
public List<int[]> getZombieSpawnPoints() {
|
||||
List<int[]> points = new ArrayList<>();
|
||||
for (int y = 0; y < height; y++) {
|
||||
for (int x = 0; x < width; x++) {
|
||||
if (cells[y][x] == 3) {
|
||||
points.add(new int[]{x, y});
|
||||
}
|
||||
}
|
||||
}
|
||||
return points;
|
||||
}
|
||||
|
||||
private static String key(int x, int y) {
|
||||
return x + "," + y;
|
||||
}
|
||||
}
|
||||
144
backend/src/main/java/com/zombie/game/model/MapData.java
Normal file
144
backend/src/main/java/com/zombie/game/model/MapData.java
Normal file
@@ -0,0 +1,144 @@
|
||||
package com.zombie.game.model;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.HashMap;
|
||||
import java.util.ArrayList;
|
||||
|
||||
/** Serializable map data for storage or network transfer / 可序列化的地图数据,用于存储或网络传输 */
|
||||
public class MapData {
|
||||
// Map unique identifier / 地图唯一标识
|
||||
private String id;
|
||||
// Map display name / 地图显示名称
|
||||
private String name;
|
||||
// Map width in grid units / 地图宽度(网格单位)
|
||||
private int width;
|
||||
// Map height in grid units / 地图高度(网格单位)
|
||||
private int height;
|
||||
// List of wall data maps / 墙壁数据映射列表
|
||||
private List<Map<String, Object>> walls;
|
||||
// List of player spawn coordinates / 玩家出生点坐标列表
|
||||
private List<Map<String, Integer>> playerSpawns;
|
||||
// List of zombie spawn coordinates / 僵尸出生点坐标列表
|
||||
private List<Map<String, Integer>> zombieSpawns;
|
||||
|
||||
// Default constructor / 默认构造函数
|
||||
public MapData() {
|
||||
this.walls = new ArrayList<>();
|
||||
this.playerSpawns = new ArrayList<>();
|
||||
this.zombieSpawns = new ArrayList<>();
|
||||
}
|
||||
|
||||
// Full constructor with all fields / 全字段构造函数
|
||||
public MapData(String id, String name, int width, int height,
|
||||
List<Map<String, Object>> walls,
|
||||
List<Map<String, Integer>> playerSpawns,
|
||||
List<Map<String, Integer>> zombieSpawns) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.walls = walls;
|
||||
this.playerSpawns = playerSpawns;
|
||||
this.zombieSpawns = zombieSpawns;
|
||||
}
|
||||
|
||||
// Get map ID / 获取地图ID
|
||||
public String getId() { return id; }
|
||||
// Set map ID / 设置地图ID
|
||||
public void setId(String id) { this.id = id; }
|
||||
// Get map name / 获取地图名称
|
||||
public String getName() { return name; }
|
||||
// Set map name / 设置地图名称
|
||||
public void setName(String name) { this.name = name; }
|
||||
// Get map width / 获取地图宽度
|
||||
public int getWidth() { return width; }
|
||||
// Set map width / 设置地图宽度
|
||||
public void setWidth(int width) { this.width = width; }
|
||||
// Get map height / 获取地图高度
|
||||
public int getHeight() { return height; }
|
||||
// Set map height / 设置地图高度
|
||||
public void setHeight(int height) { this.height = height; }
|
||||
// Get walls list / 获取墙壁列表
|
||||
public List<Map<String, Object>> getWalls() { return walls; }
|
||||
// Set walls list / 设置墙壁列表
|
||||
public void setWalls(List<Map<String, Object>> walls) { this.walls = walls; }
|
||||
// Get player spawns / 获取玩家出生点
|
||||
public List<Map<String, Integer>> getPlayerSpawns() { return playerSpawns; }
|
||||
// Set player spawns / 设置玩家出生点
|
||||
public void setPlayerSpawns(List<Map<String, Integer>> playerSpawns) { this.playerSpawns = playerSpawns; }
|
||||
// Get zombie spawns / 获取僵尸出生点
|
||||
public List<Map<String, Integer>> getZombieSpawns() { return zombieSpawns; }
|
||||
// Set zombie spawns / 设置僵尸出生点
|
||||
public void setZombieSpawns(List<Map<String, Integer>> zombieSpawns) { this.zombieSpawns = zombieSpawns; }
|
||||
|
||||
// Convert this map data to a 2D cell array / 将此地图数据转换为二维格子数组
|
||||
public int[][] toCells() {
|
||||
int[][] cells = new int[height][width];
|
||||
for (int y = 0; y < height; y++) {
|
||||
for (int x = 0; x < width; x++) {
|
||||
cells[y][x] = 0;
|
||||
}
|
||||
}
|
||||
// Mark wall cells / 标记墙壁格子
|
||||
for (Map<String, Object> wall : walls) {
|
||||
int x = (Integer) wall.get("x");
|
||||
int y = (Integer) wall.get("y");
|
||||
if (x >= 0 && x < width && y >= 0 && y < height) {
|
||||
cells[y][x] = 1;
|
||||
}
|
||||
}
|
||||
// Mark player spawn cells / 标记玩家出生点格子
|
||||
for (Map<String, Integer> spawn : playerSpawns) {
|
||||
int x = spawn.get("x");
|
||||
int y = spawn.get("y");
|
||||
if (x >= 0 && x < width && y >= 0 && y < height) {
|
||||
cells[y][x] = 2;
|
||||
}
|
||||
}
|
||||
// Mark zombie spawn cells / 标记僵尸出生点格子
|
||||
for (Map<String, Integer> spawn : zombieSpawns) {
|
||||
int x = spawn.get("x");
|
||||
int y = spawn.get("y");
|
||||
if (x >= 0 && x < width && y >= 0 && y < height) {
|
||||
cells[y][x] = 3;
|
||||
}
|
||||
}
|
||||
return cells;
|
||||
}
|
||||
|
||||
// Create MapData from a GameMap instance / 从GameMap实例创建MapData
|
||||
public static MapData fromGameMap(String id, String name, GameMap map) {
|
||||
List<Map<String, Object>> walls = new ArrayList<>();
|
||||
List<Map<String, Integer>> playerSpawns = new ArrayList<>();
|
||||
List<Map<String, Integer>> zombieSpawns = new ArrayList<>();
|
||||
|
||||
// Convert static walls from raw data
|
||||
for (int[] pos : map.getStaticWallData()) {
|
||||
Map<String, Object> wallData = new HashMap<>();
|
||||
wallData.put("x", pos[0]);
|
||||
wallData.put("y", pos[1]);
|
||||
wallData.put("type", "static");
|
||||
walls.add(wallData);
|
||||
}
|
||||
|
||||
// Convert nut walls from raw data
|
||||
for (int[] pos : map.getNutWallData()) {
|
||||
Map<String, Object> wallData = new HashMap<>();
|
||||
wallData.put("x", pos[0]);
|
||||
wallData.put("y", pos[1]);
|
||||
wallData.put("type", "nut");
|
||||
walls.add(wallData);
|
||||
}
|
||||
|
||||
// Convert player spawn points
|
||||
for (int[] spawn : map.getSpawnPoints()) {
|
||||
Map<String, Integer> sp = new HashMap<>();
|
||||
sp.put("x", spawn[0]);
|
||||
sp.put("y", spawn[1]);
|
||||
playerSpawns.add(sp);
|
||||
}
|
||||
|
||||
return new MapData(id, name, map.getWidth(), map.getHeight(), walls, playerSpawns, zombieSpawns);
|
||||
}
|
||||
}
|
||||
26
backend/src/main/java/com/zombie/game/model/PlayerInfo.java
Normal file
26
backend/src/main/java/com/zombie/game/model/PlayerInfo.java
Normal file
@@ -0,0 +1,26 @@
|
||||
package com.zombie.game.model;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
/** Player information within a room / 房间内的玩家信息 */
|
||||
@Getter
|
||||
public class PlayerInfo {
|
||||
// Player unique identifier / 玩家唯一标识
|
||||
private final String id;
|
||||
// Player display name / 玩家显示名称
|
||||
private final String name;
|
||||
// Whether the player is ready / 玩家是否已准备
|
||||
private boolean ready;
|
||||
|
||||
// Create a new player info / 创建新的玩家信息
|
||||
public PlayerInfo(String id, String name) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.ready = false;
|
||||
}
|
||||
|
||||
// Set the ready state / 设置准备状态
|
||||
public void setReady(boolean ready) {
|
||||
this.ready = ready;
|
||||
}
|
||||
}
|
||||
106
backend/src/main/java/com/zombie/game/model/Room.java
Normal file
106
backend/src/main/java/com/zombie/game/model/Room.java
Normal file
@@ -0,0 +1,106 @@
|
||||
package com.zombie.game.model;
|
||||
|
||||
import lombok.Getter;
|
||||
import java.util.*;
|
||||
|
||||
import static com.zombie.game.model.Constants.*;
|
||||
|
||||
/** Game room holding players and game state / 游戏房间,管理玩家和游戏状态 */
|
||||
@Getter
|
||||
public class Room {
|
||||
// Room unique identifier / 房间唯一标识
|
||||
private String id;
|
||||
// Host player ID / 房主玩家ID
|
||||
private String hostId;
|
||||
// Map of playerId to PlayerInfo / 玩家ID到玩家信息的映射
|
||||
private Map<String, PlayerInfo> players;
|
||||
// Whether the game has started / 游戏是否已开始
|
||||
private boolean gameStarted;
|
||||
// Maximum number of players allowed / 最大玩家数量
|
||||
private final int maxPlayers = 4;
|
||||
|
||||
// Set game started flag / 设置游戏开始标志
|
||||
public void setGameStarted(boolean started) { this.gameStarted = started; }
|
||||
|
||||
// Create a new room with the host player / 创建新房间并添加房主
|
||||
public Room(String id, String hostId, String hostName) {
|
||||
this.id = id;
|
||||
this.hostId = hostId;
|
||||
this.players = new LinkedHashMap<>();
|
||||
players.put(hostId, new PlayerInfo(hostId, hostName));
|
||||
}
|
||||
|
||||
// Add a player to the room / 添加玩家到房间
|
||||
public boolean addPlayer(String playerId, String playerName) {
|
||||
if (players.size() >= maxPlayers) return false;
|
||||
if (players.containsKey(playerId)) return false;
|
||||
players.put(playerId, new PlayerInfo(playerId, playerName));
|
||||
return true;
|
||||
}
|
||||
|
||||
// Remove a player from the room / 从房间移除玩家
|
||||
public void removePlayer(String playerId) {
|
||||
players.remove(playerId);
|
||||
}
|
||||
|
||||
// Get player by ID / 根据ID获取玩家
|
||||
public PlayerInfo getPlayer(String playerId) {
|
||||
return players.get(playerId);
|
||||
}
|
||||
|
||||
// Get all players / 获取所有玩家
|
||||
public Collection<PlayerInfo> getPlayers() {
|
||||
return players.values();
|
||||
}
|
||||
|
||||
// Get current player count / 获取当前玩家数量
|
||||
public int getPlayerCount() {
|
||||
return players.size();
|
||||
}
|
||||
|
||||
// Check if a player is the host / 检查玩家是否为房主
|
||||
public boolean isHost(String playerId) {
|
||||
return hostId.equals(playerId);
|
||||
}
|
||||
|
||||
// Check if all non-host players are ready / 检查所有非房主玩家是否已准备
|
||||
public boolean allReady() {
|
||||
for (PlayerInfo p : players.values()) {
|
||||
if (!p.getId().equals(hostId) && !p.isReady()) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Convert room state to a map for a specific player / 将房间状态转换为指定玩家的Map
|
||||
public Map<String, Object> toStateMap(String playerId) {
|
||||
Map<String, Object> map = new LinkedHashMap<>();
|
||||
map.put("roomId", id);
|
||||
map.put("hostId", hostId);
|
||||
map.put("isHost", hostId.equals(playerId));
|
||||
map.put("playerId", playerId);
|
||||
|
||||
// Build player list with index / 构建带索引的玩家列表
|
||||
List<Map<String, Object>> playerList = new ArrayList<>();
|
||||
int index = 0;
|
||||
for (PlayerInfo p : players.values()) {
|
||||
Map<String, Object> pm = new LinkedHashMap<>();
|
||||
pm.put("id", p.getId());
|
||||
pm.put("name", p.getName());
|
||||
pm.put("ready", p.isReady());
|
||||
pm.put("index", index++);
|
||||
playerList.add(pm);
|
||||
}
|
||||
map.put("players", playerList);
|
||||
return map;
|
||||
}
|
||||
|
||||
// Convert to a lightweight room list entry / 转换为轻量级房间列表条目
|
||||
public Map<String, Object> toRoomListMap() {
|
||||
Map<String, Object> map = new LinkedHashMap<>();
|
||||
map.put("id", id);
|
||||
PlayerInfo host = players.get(hostId);
|
||||
map.put("hostName", host != null ? host.getName() : "Unknown");
|
||||
map.put("playerCount", players.size());
|
||||
return map;
|
||||
}
|
||||
}
|
||||
73
backend/src/main/java/com/zombie/game/server/GameLoop.java
Normal file
73
backend/src/main/java/com/zombie/game/server/GameLoop.java
Normal file
@@ -0,0 +1,73 @@
|
||||
package com.zombie.game.server;
|
||||
|
||||
import com.zombie.game.ecs.ECSWorld;
|
||||
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Game loop running at fixed tick rate for a single room / 以固定帧率运行的游戏循环,为单个房间服务
|
||||
*/
|
||||
public class GameLoop {
|
||||
// Room ID this loop serves / 该循环服务的房间ID
|
||||
private String roomId;
|
||||
// The ECS world being updated / 正在更新的ECS世界
|
||||
private ECSWorld world;
|
||||
// Callback to broadcast state each tick / 每帧广播状态的回调
|
||||
private GameService.GameStateBroadcast broadcaster;
|
||||
// Whether the loop is currently running / 循环当前是否正在运行
|
||||
private volatile boolean running;
|
||||
// Target tick rate (30 ticks per second) / 目标帧率(每秒30帧)
|
||||
private static final int TICK_RATE = 30;
|
||||
// Interval between ticks in milliseconds / 帧间隔(毫秒)
|
||||
private static final long TICK_INTERVAL_MS = 1000 / TICK_RATE;
|
||||
// Interval between ticks in seconds / 帧间隔(秒)
|
||||
private static final float TICK_INTERVAL_SEC = 1.0f / TICK_RATE;
|
||||
// Scheduler for running the loop / 运行循环的调度器
|
||||
private ScheduledExecutorService scheduler;
|
||||
|
||||
// Constructor: sets up the game loop for a room / 构造函数:为房间设置游戏循环
|
||||
public GameLoop(String roomId, ECSWorld world, GameService.GameStateBroadcast broadcaster) {
|
||||
this.roomId = roomId;
|
||||
this.world = world;
|
||||
this.broadcaster = broadcaster;
|
||||
this.running = true;
|
||||
// Create single-threaded daemon scheduler / 创建单线程守护调度器
|
||||
this.scheduler = Executors.newSingleThreadScheduledExecutor(r -> {
|
||||
Thread t = new Thread(r, "GameLoop-" + roomId);
|
||||
t.setDaemon(true);
|
||||
return t;
|
||||
});
|
||||
}
|
||||
|
||||
// Start the game loop / 启动游戏循环
|
||||
public void start() {
|
||||
scheduler.scheduleAtFixedRate(this::tick, 0, TICK_INTERVAL_MS, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
// Single tick: update world and broadcast state / 单帧:更新世界并广播状态
|
||||
private void tick() {
|
||||
if (!running) return;
|
||||
synchronized (world.getLock()) {
|
||||
world.update(TICK_INTERVAL_SEC);
|
||||
}
|
||||
broadcaster.broadcast(roomId, world);
|
||||
}
|
||||
|
||||
// Stop the game loop / 停止游戏循环
|
||||
public void stop() {
|
||||
running = false;
|
||||
if (scheduler != null && !scheduler.isShutdown()) {
|
||||
scheduler.shutdown();
|
||||
try {
|
||||
// Force shutdown if graceful shutdown fails / 优雅关闭失败则强制关闭
|
||||
if (!scheduler.awaitTermination(1, TimeUnit.SECONDS)) {
|
||||
scheduler.shutdownNow();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
scheduler.shutdownNow();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
163
backend/src/main/java/com/zombie/game/server/GameService.java
Normal file
163
backend/src/main/java/com/zombie/game/server/GameService.java
Normal file
@@ -0,0 +1,163 @@
|
||||
package com.zombie.game.server;
|
||||
|
||||
import com.zombie.game.ecs.ECSWorld;
|
||||
import com.zombie.game.ecs.components.*;
|
||||
import com.zombie.game.model.*;
|
||||
import com.zombie.game.systems.*;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* Core game service managing game lifecycle and state / 核心游戏服务,管理游戏生命周期和状态
|
||||
*/
|
||||
public class GameService {
|
||||
// Logger for game events / 游戏事件日志记录器
|
||||
private static final Logger logger = LoggerFactory.getLogger(GameService.class);
|
||||
|
||||
// Active game worlds mapped by room ID / 活跃的游戏世界,以房间ID映射
|
||||
private final Map<String, ECSWorld> activeGames = new ConcurrentHashMap<>();
|
||||
// Game loops mapped by room ID / 游戏循环,以房间ID映射
|
||||
private final Map<String, GameLoop> gameLoops = new ConcurrentHashMap<>();
|
||||
// Callback for broadcasting game state / 广播游戏状态的回调
|
||||
private final GameStateBroadcast broadcaster;
|
||||
|
||||
// Interface for broadcasting game state to clients / 向客户端广播游戏状态的接口
|
||||
public interface GameStateBroadcast {
|
||||
void broadcast(String roomId, ECSWorld world);
|
||||
}
|
||||
|
||||
// Constructor with broadcast callback / 带广播回调的构造函数
|
||||
public GameService(GameStateBroadcast broadcaster) {
|
||||
this.broadcaster = broadcaster;
|
||||
}
|
||||
|
||||
// Start a new game for the given room / 为指定房间启动新游戏
|
||||
public Map<String, Map<String, Object>> startGame(Room room) {
|
||||
// Load map from system property or default / 从系统属性或默认路径加载地图
|
||||
String mapPath = System.getProperty("map.path", "maps/d540209a.json");
|
||||
ECSWorld world = new ECSWorld(mapPath);
|
||||
|
||||
// Add systems in order: input -> flow field -> zombie spawn -> zombie movement
|
||||
world.addSystem(new PlayerInputSystem());
|
||||
world.addSystem(new FlowFieldUpdateSystem());
|
||||
world.addSystem(new ZombieSpawnSystem());
|
||||
world.addSystem(new ZombieMovementSystem());
|
||||
|
||||
// Spawn players at map spawn points / 在地图出生点生成玩家
|
||||
int index = 0;
|
||||
List<int[]> spawnPoints = world.getMap().getSpawnPoints();
|
||||
|
||||
for (PlayerInfo playerInfo : room.getPlayers()) {
|
||||
int[] sp = spawnPoints.get(index % spawnPoints.size());
|
||||
float wx = sp[0] + 0.5f;
|
||||
float wy = sp[1] + 0.5f;
|
||||
world.createPlayerEntity(playerInfo.getId(), playerInfo.getName(), wx, wy);
|
||||
index++;
|
||||
}
|
||||
|
||||
// Spawn test zombies at zombie spawn points / 在僵尸出生点生成测试僵尸
|
||||
List<int[]> zombieSpawns = world.getMap().getZombieSpawnPoints();
|
||||
for (int[] zsp : zombieSpawns) {
|
||||
int zombieId = world.createZombieEntity(zsp[0] + 0.5f, zsp[1] + 0.5f);
|
||||
logger.info("Initial zombie created: id={} at ({}, {})", zombieId, zsp[0], zsp[1]);
|
||||
}
|
||||
|
||||
activeGames.put(room.getId(), world);
|
||||
|
||||
// Build initial data for each player / 为每位玩家构建初始数据
|
||||
Map<String, Map<String, Object>> playerInitData = new LinkedHashMap<>();
|
||||
for (PlayerInfo playerInfo : room.getPlayers()) {
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
data.put("playerId", playerInfo.getId());
|
||||
data.put("mapData", serializeMapData(world.getMapData()));
|
||||
|
||||
// Build player list with positions / 构建包含位置的玩家列表
|
||||
List<Map<String, Object>> playerList = new ArrayList<>();
|
||||
int idx = 0;
|
||||
for (PlayerInfo p : room.getPlayers()) {
|
||||
Integer entityId = world.getPlayerEntity(p.getId());
|
||||
Map<String, Object> pm = new LinkedHashMap<>();
|
||||
pm.put("id", p.getId());
|
||||
pm.put("name", p.getName());
|
||||
if (entityId != null) {
|
||||
Position pos = world.getPositions().get(entityId);
|
||||
if (pos != null) {
|
||||
pm.put("x", pos.getX());
|
||||
pm.put("y", pos.getY());
|
||||
}
|
||||
}
|
||||
pm.put("index", idx++);
|
||||
playerList.add(pm);
|
||||
}
|
||||
data.put("players", playerList);
|
||||
playerInitData.put(playerInfo.getId(), data);
|
||||
}
|
||||
|
||||
// Create game loop (not started yet) / 创建游戏循环(尚未启动)
|
||||
GameLoop loop = new GameLoop(room.getId(), world, broadcaster);
|
||||
gameLoops.put(room.getId(), loop);
|
||||
|
||||
logger.info("Game initialized for room: {}", room.getId());
|
||||
return playerInitData;
|
||||
}
|
||||
|
||||
// Start the game loop for a room (call after sending game_started) / 启动房间的游戏循环(在发送 game_started 之后调用)
|
||||
public void startGameLoop(String roomId) {
|
||||
GameLoop loop = gameLoops.get(roomId);
|
||||
if (loop != null) {
|
||||
loop.start();
|
||||
}
|
||||
}
|
||||
|
||||
// Stop the game for a room / 停止指定房间的游戏
|
||||
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);
|
||||
}
|
||||
|
||||
// Process player input for movement and aiming / 处理玩家输入(移动和瞄准)
|
||||
public void processPlayerInput(String roomId, String playerId,
|
||||
float dx, float dy, float aimX, float aimY, int seq) {
|
||||
ECSWorld world = activeGames.get(roomId);
|
||||
if (world == null) return;
|
||||
|
||||
synchronized (world.getLock()) {
|
||||
Integer entityId = world.getPlayerEntity(playerId);
|
||||
if (entityId == null) return;
|
||||
|
||||
// Skip if player is dead / 如果玩家已死亡则跳过
|
||||
Health health = world.getHealths().get(entityId);
|
||||
if (health == null || !health.isAlive()) return;
|
||||
|
||||
// Apply input to player entity / 将输入应用到玩家实体
|
||||
PlayerInput input = world.getPlayerInputs().get(entityId);
|
||||
if (input == null) return;
|
||||
|
||||
input.setDx(dx);
|
||||
input.setDy(dy);
|
||||
input.setAimX(aimX);
|
||||
input.setAimY(aimY);
|
||||
input.setSeq(seq);
|
||||
}
|
||||
}
|
||||
|
||||
// Convert 2D int array to nested list for JSON serialization / 将二维整数数组转换为嵌套列表用于JSON序列化
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,370 @@
|
||||
package com.zombie.game.server;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.zombie.game.ecs.ECSWorld;
|
||||
import com.zombie.game.model.*;
|
||||
import com.zombie.game.model.Constants;
|
||||
import com.zombie.game.systems.StateSyncSystem;
|
||||
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;
|
||||
|
||||
/**
|
||||
* Main WebSocket server handling all client connections / 主WebSocket服务器,处理所有客户端连接
|
||||
* Manages game rooms, player connections, and message routing / 管理游戏房间、玩家连接和消息路由
|
||||
*/
|
||||
public class GameWebSocketServer extends WebSocketServer {
|
||||
// SLF4J logger for server events / SLF4J日志记录器,用于服务器事件
|
||||
private static final Logger logger = LoggerFactory.getLogger(GameWebSocketServer.class);
|
||||
// JSON serializer/deserializer / JSON序列化/反序列化工具
|
||||
private Gson gson;
|
||||
// Room manager for creating and tracking game rooms / 房间管理器,用于创建和追踪游戏房间
|
||||
private RoomManager roomManager;
|
||||
// Game service for game logic / 游戏服务,用于游戏逻辑处理
|
||||
private GameService gameService;
|
||||
// Map connection to player ID / 连接映射到玩家ID
|
||||
private Map<WebSocket, String> connectionToPlayer;
|
||||
// Map player ID to connection / 玩家ID映射到连接
|
||||
private Map<String, WebSocket> playerToConnection;
|
||||
// Timer for broadcasting room list periodically / 定时器,用于定期广播房间列表
|
||||
private Timer roomListTimer;
|
||||
|
||||
// Constructor: initializes server with given port / 构造函数:用指定端口初始化服务器
|
||||
public GameWebSocketServer(int port) {
|
||||
super(new InetSocketAddress(port));
|
||||
this.gson = new Gson();
|
||||
this.roomManager = new RoomManager();
|
||||
this.connectionToPlayer = new ConcurrentHashMap<>();
|
||||
this.playerToConnection = new ConcurrentHashMap<>();
|
||||
this.gameService = new GameService(this::broadcastGameState);
|
||||
}
|
||||
|
||||
// Called when a new client connects / 新客户端连接时调用
|
||||
@Override
|
||||
public void onOpen(WebSocket conn, ClientHandshake handshake) {
|
||||
logger.info("New connection: {}", conn.getRemoteSocketAddress());
|
||||
}
|
||||
|
||||
// Called when a client disconnects / 客户端断开连接时调用
|
||||
@Override
|
||||
public void onClose(WebSocket conn, int code, String reason, boolean remote) {
|
||||
logger.info("Connection closed: {}", conn.getRemoteSocketAddress());
|
||||
// Remove player mapping and handle room leave / 移除玩家映射并处理离开房间
|
||||
String playerId = connectionToPlayer.remove(conn);
|
||||
if (playerId != null) {
|
||||
playerToConnection.remove(playerId);
|
||||
handleLeaveRoomByPlayerId(playerId);
|
||||
}
|
||||
}
|
||||
|
||||
// Called when a message is received from a client / 收到客户端消息时调用
|
||||
@Override
|
||||
public void onMessage(WebSocket conn, String message) {
|
||||
try {
|
||||
// Parse message type and data / 解析消息类型和数据
|
||||
JsonObject msg = gson.fromJson(message, JsonObject.class);
|
||||
String type = msg.get("type").getAsString();
|
||||
JsonObject data = msg.has("data") ? msg.getAsJsonObject("data") : new JsonObject();
|
||||
|
||||
// Route message to appropriate handler / 将消息路由到对应的处理函数
|
||||
switch (type) {
|
||||
case Constants.MSG_CREATE_ROOM:
|
||||
handleCreateRoom(conn, data);
|
||||
break;
|
||||
case Constants.MSG_JOIN_ROOM:
|
||||
handleJoinRoom(conn, data);
|
||||
break;
|
||||
case Constants.MSG_LEAVE_ROOM:
|
||||
handleLeaveRoomByConn(conn);
|
||||
break;
|
||||
case Constants.MSG_ROOM_LIST:
|
||||
handleRoomList(conn);
|
||||
break;
|
||||
case Constants.MSG_READY:
|
||||
handleReady(conn);
|
||||
break;
|
||||
case Constants.MSG_START_GAME:
|
||||
handleStartGame(conn);
|
||||
break;
|
||||
case Constants.MSG_PLAYER_INPUT:
|
||||
handlePlayerInput(conn, data);
|
||||
break;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("Error processing message from {}", conn.getRemoteSocketAddress(), e);
|
||||
sendError(conn, "Invalid message format");
|
||||
}
|
||||
}
|
||||
|
||||
// Called when a WebSocket error occurs / WebSocket错误发生时调用
|
||||
@Override
|
||||
public void onError(WebSocket conn, Exception ex) {
|
||||
logger.error("WebSocket error on {}", conn != null ? conn.getRemoteSocketAddress() : "null", ex);
|
||||
}
|
||||
|
||||
// Called when the server starts successfully / 服务器成功启动时调用
|
||||
@Override
|
||||
public void onStart() {
|
||||
logger.info("Game WebSocket Server started on port {}", getPort());
|
||||
// Start periodic room list broadcast (every 2 seconds) / 启动定期房间列表广播(每2秒)
|
||||
roomListTimer = new Timer(true);
|
||||
roomListTimer.scheduleAtFixedRate(new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
broadcastRoomList();
|
||||
}
|
||||
}, 0, 2000);
|
||||
}
|
||||
|
||||
// Handle create room request / 处理创建房间请求
|
||||
private void handleCreateRoom(WebSocket conn, JsonObject data) {
|
||||
// Validate required fields / 验证必需字段
|
||||
if (!MessageUtils.hasRequired(data, "playerName")) {
|
||||
sendError(conn, "Missing playerName");
|
||||
return;
|
||||
}
|
||||
String playerName = MessageUtils.getString(data, "playerName");
|
||||
// Generate unique IDs for player and room / 为玩家和房间生成唯一ID
|
||||
String playerId = UUID.randomUUID().toString();
|
||||
String roomId = UUID.randomUUID().toString().substring(0, 8);
|
||||
|
||||
// Track connection mappings / 记录连接映射
|
||||
connectionToPlayer.put(conn, playerId);
|
||||
playerToConnection.put(playerId, conn);
|
||||
|
||||
// Create new room with player as host / 创建新房间,玩家作为房主
|
||||
Room room = new Room(roomId, playerId, playerName);
|
||||
roomManager.addRoom(roomId, room);
|
||||
|
||||
// Send room state back to creator / 向创建者发送房间状态
|
||||
sendToConnection(conn, Constants.MSG_ROOM_STATE, room.toStateMap(playerId));
|
||||
logger.info("Room created: {} by {}", roomId, playerName);
|
||||
}
|
||||
|
||||
// Handle join room request / 处理加入房间请求
|
||||
private void handleJoinRoom(WebSocket conn, JsonObject data) {
|
||||
// Validate required fields / 验证必需字段
|
||||
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 = roomManager.getRoom(roomId);
|
||||
if (room == null) {
|
||||
sendError(conn, "Room not found");
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if room is full (max 4 players) / 检查房间是否已满(最多4人)
|
||||
if (room.getPlayerCount() >= 4) {
|
||||
sendError(conn, "Room is full");
|
||||
return;
|
||||
}
|
||||
|
||||
// Generate player ID and track connection / 生成玩家ID并追踪连接
|
||||
String playerId = UUID.randomUUID().toString();
|
||||
connectionToPlayer.put(conn, playerId);
|
||||
playerToConnection.put(playerId, conn);
|
||||
|
||||
// Add player to room / 将玩家添加到房间
|
||||
room.addPlayer(playerId, playerName);
|
||||
roomManager.joinRoom(roomId, playerId, playerName);
|
||||
|
||||
// Notify all players of updated room state / 通知所有玩家房间状态已更新
|
||||
broadcastRoomState(room);
|
||||
logger.info("Player {} joined room {}", playerName, roomId);
|
||||
}
|
||||
|
||||
// Handle leave room by connection / 通过连接处理离开房间
|
||||
private void handleLeaveRoomByConn(WebSocket conn) {
|
||||
String playerId = connectionToPlayer.get(conn);
|
||||
if (playerId == null) return;
|
||||
handleLeaveRoomByPlayerId(playerId);
|
||||
}
|
||||
|
||||
// Handle leave room by player ID / 通过玩家ID处理离开房间
|
||||
private void handleLeaveRoomByPlayerId(String playerId) {
|
||||
Room room = roomManager.leaveRoom(playerId);
|
||||
if (room == null) return;
|
||||
|
||||
Room updatedRoom = roomManager.getRoom(room.getId());
|
||||
if (updatedRoom == null) {
|
||||
// Room was deleted, stop the game / 房间被删除,停止游戏
|
||||
gameService.stopGame(room.getId());
|
||||
} else {
|
||||
// Notify remaining players / 通知剩余玩家
|
||||
broadcastRoomState(updatedRoom);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle room list request / 处理房间列表请求
|
||||
private void handleRoomList(WebSocket conn) {
|
||||
List<Map<String, Object>> roomList = new ArrayList<>();
|
||||
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);
|
||||
}
|
||||
|
||||
// Handle player ready toggle / 处理玩家准备状态切换
|
||||
private void handleReady(WebSocket conn) {
|
||||
String playerId = connectionToPlayer.get(conn);
|
||||
if (playerId == null) return;
|
||||
|
||||
Room room = roomManager.getRoomByPlayerId(playerId);
|
||||
if (room == null) return;
|
||||
|
||||
PlayerInfo player = room.getPlayer(playerId);
|
||||
if (player != null) {
|
||||
// Toggle ready state / 切换准备状态
|
||||
player.setReady(!player.isReady());
|
||||
broadcastRoomState(room);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle start game request (host only) / 处理开始游戏请求(仅房主)
|
||||
private void handleStartGame(WebSocket conn) {
|
||||
String playerId = connectionToPlayer.get(conn);
|
||||
if (playerId == null) {
|
||||
sendError(conn, "Not in a room / 未在房间中");
|
||||
return;
|
||||
}
|
||||
|
||||
Room room = roomManager.getRoomByPlayerId(playerId);
|
||||
if (room == null) {
|
||||
sendError(conn, "Room not found / 房间不存在");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!room.isHost(playerId)) {
|
||||
sendError(conn, "Only host can start game / 只有房主可以开始游戏");
|
||||
return;
|
||||
}
|
||||
if (room.isGameStarted()) {
|
||||
sendError(conn, "Game already started / 游戏已经开始");
|
||||
return;
|
||||
}
|
||||
if (!room.allReady()) {
|
||||
sendError(conn, "Not all players are ready / 不是所有玩家都已准备");
|
||||
return;
|
||||
}
|
||||
|
||||
// Set game started flag after startGame succeeds / startGame 成功后再设置标志
|
||||
try {
|
||||
startGame(room);
|
||||
room.setGameStarted(true);
|
||||
} catch (Exception e) {
|
||||
logger.error("Failed to start game for room {}", room.getId(), e);
|
||||
sendError(conn, "Failed to start game: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// Start the game for a room / 为房间启动游戏
|
||||
private void startGame(Room room) {
|
||||
// Initialize game world and create player entities / 初始化游戏世界并创建玩家实体
|
||||
Map<String, Map<String, Object>> playerInitData = gameService.startGame(room);
|
||||
|
||||
// Send initial game data to each player / 向每位玩家发送初始游戏数据
|
||||
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, entry.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
// Start game loop after game_started is sent / game_started 发送完毕后再启动游戏循环
|
||||
gameService.startGameLoop(room.getId());
|
||||
}
|
||||
|
||||
// Handle player input (movement, aim) / 处理玩家输入(移动、瞄准)
|
||||
private void handlePlayerInput(WebSocket conn, JsonObject data) {
|
||||
String playerId = connectionToPlayer.get(conn);
|
||||
if (playerId == null) return;
|
||||
|
||||
Room room = roomManager.getRoomByPlayerId(playerId);
|
||||
if (room == null || !room.isGameStarted()) return;
|
||||
|
||||
// Extract input data / 提取输入数据
|
||||
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);
|
||||
int seq = MessageUtils.getInt(data, "seq", 0);
|
||||
|
||||
gameService.processPlayerInput(room.getId(), playerId, dx, dy, aimX, aimY, seq);
|
||||
}
|
||||
|
||||
// Broadcast game state to all players in a room / 向房间内所有玩家广播游戏状态
|
||||
private void broadcastGameState(String roomId, ECSWorld world) {
|
||||
Room room = roomManager.getRoom(roomId);
|
||||
if (room == null) return;
|
||||
|
||||
StateSyncSystem syncSystem = new StateSyncSystem();
|
||||
|
||||
// Sync state under world lock / 在世界锁下同步状态
|
||||
synchronized (world.getLock()) {
|
||||
Map<String, Object> state = syncSystem.buildGameState(world);
|
||||
for (PlayerInfo playerInfo : room.getPlayers()) {
|
||||
WebSocket pConn = playerToConnection.get(playerInfo.getId());
|
||||
if (pConn != null && pConn.isOpen()) {
|
||||
sendToConnection(pConn, Constants.MSG_GAME_STATE, state);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Broadcast room state to all players in a room / 向房间内所有玩家广播房间状态
|
||||
private void broadcastRoomState(Room room) {
|
||||
for (PlayerInfo player : room.getPlayers()) {
|
||||
WebSocket pConn = playerToConnection.get(player.getId());
|
||||
if (pConn != null && pConn.isOpen()) {
|
||||
sendToConnection(pConn, Constants.MSG_ROOM_STATE, room.toStateMap(player.getId()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Broadcast room list to all unconnected clients / 向所有未连接的玩家广播房间列表
|
||||
private void broadcastRoomList() {
|
||||
List<Map<String, Object>> roomList = new ArrayList<>();
|
||||
for (Room room : roomManager.getAvailableRooms()) {
|
||||
roomList.add(room.toRoomListMap());
|
||||
}
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
data.put("rooms", roomList);
|
||||
|
||||
// Only send to clients not in a room / 只发送给不在房间中的客户端
|
||||
for (WebSocket conn : getConnections()) {
|
||||
if (connectionToPlayer.get(conn) == null) {
|
||||
sendToConnection(conn, Constants.MSG_ROOM_LIST, data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Send a message to a specific connection / 向指定连接发送消息
|
||||
private void sendToConnection(WebSocket conn, String type, Object data) {
|
||||
if (conn != null && conn.isOpen()) {
|
||||
Map<String, Object> msg = new LinkedHashMap<>();
|
||||
msg.put("type", type);
|
||||
msg.put("data", data);
|
||||
conn.send(gson.toJson(msg));
|
||||
}
|
||||
}
|
||||
|
||||
// Send error message to a connection / 向连接发送错误消息
|
||||
private void sendError(WebSocket conn, String message) {
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
data.put("message", message);
|
||||
sendToConnection(conn, Constants.MSG_ERROR, data);
|
||||
}
|
||||
}
|
||||
136
backend/src/main/java/com/zombie/game/server/MapStorage.java
Normal file
136
backend/src/main/java/com/zombie/game/server/MapStorage.java
Normal file
@@ -0,0 +1,136 @@
|
||||
package com.zombie.game.server;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.zombie.game.model.MapData;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.file.*;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Storage service for game maps (save, load, list, delete) / 地图存储服务(保存、加载、列出、删除)
|
||||
*/
|
||||
public class MapStorage {
|
||||
// Logger for map operations / 地图操作日志记录器
|
||||
private static final Logger logger = LoggerFactory.getLogger(MapStorage.class);
|
||||
// Directory where map files are stored / 地图文件存储目录
|
||||
private static final String MAPS_DIR = "maps";
|
||||
// JSON serializer/deserializer / JSON序列化/反序列化工具
|
||||
private final Gson gson;
|
||||
// In-memory cache of loaded maps / 已加载地图的内存缓存
|
||||
private final Map<String, MapData> cache;
|
||||
|
||||
// Constructor: initializes storage and ensures maps directory exists / 构造函数:初始化存储并确保地图目录存在
|
||||
public MapStorage() {
|
||||
this.gson = new Gson();
|
||||
this.cache = new HashMap<>();
|
||||
ensureMapsDir();
|
||||
}
|
||||
|
||||
// Create maps directory if it doesn't exist / 如果地图目录不存在则创建
|
||||
private void ensureMapsDir() {
|
||||
Path mapsPath = Paths.get(MAPS_DIR);
|
||||
if (!Files.exists(mapsPath)) {
|
||||
try {
|
||||
Files.createDirectories(mapsPath);
|
||||
} catch (IOException e) {
|
||||
logger.error("Failed to create maps directory", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save a map to disk and cache / 将地图保存到磁盘并缓存
|
||||
public MapData save(MapData mapData) {
|
||||
// Assign ID if missing / 如果缺少ID则生成
|
||||
if (mapData.getId() == null || mapData.getId().isEmpty()) {
|
||||
mapData.setId(UUID.randomUUID().toString().substring(0, 8));
|
||||
}
|
||||
// Set default name if missing / 如果缺少名称则使用默认值
|
||||
if (mapData.getName() == null || mapData.getName().isEmpty()) {
|
||||
mapData.setName("untitled");
|
||||
}
|
||||
|
||||
String filename = mapData.getId() + ".json";
|
||||
Path filePath = Paths.get(MAPS_DIR, filename);
|
||||
|
||||
try (Writer writer = Files.newBufferedWriter(filePath)) {
|
||||
gson.toJson(mapData, writer);
|
||||
cache.put(mapData.getId(), mapData);
|
||||
logger.info("Map saved: {} ({})", mapData.getName(), mapData.getId());
|
||||
return mapData;
|
||||
} catch (IOException e) {
|
||||
logger.error("Failed to save map: {}", mapData.getId(), e);
|
||||
throw new RuntimeException("Failed to save map", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Load a map from cache or disk / 从缓存或磁盘加载地图
|
||||
public MapData load(String id) {
|
||||
// Check cache first / 先检查缓存
|
||||
MapData cached = cache.get(id);
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
String filename = id + ".json";
|
||||
Path filePath = Paths.get(MAPS_DIR, filename);
|
||||
|
||||
if (!Files.exists(filePath)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try (Reader reader = Files.newBufferedReader(filePath)) {
|
||||
MapData mapData = gson.fromJson(reader, MapData.class);
|
||||
cache.put(id, mapData);
|
||||
return mapData;
|
||||
} catch (IOException e) {
|
||||
logger.error("Failed to load map: {}", id, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// List all maps from disk / 列出磁盘上的所有地图
|
||||
public List<MapData> listMaps() {
|
||||
List<MapData> maps = new ArrayList<>();
|
||||
Path mapsPath = Paths.get(MAPS_DIR);
|
||||
|
||||
if (!Files.exists(mapsPath)) {
|
||||
return maps;
|
||||
}
|
||||
|
||||
try (DirectoryStream<Path> stream = Files.newDirectoryStream(mapsPath, "*.json")) {
|
||||
for (Path file : stream) {
|
||||
try (Reader reader = Files.newBufferedReader(file)) {
|
||||
MapData mapData = gson.fromJson(reader, MapData.class);
|
||||
maps.add(mapData);
|
||||
} catch (IOException e) {
|
||||
logger.warn("Failed to read map file: {}", file);
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.error("Failed to list maps", e);
|
||||
}
|
||||
|
||||
return maps;
|
||||
}
|
||||
|
||||
// Delete a map from disk and cache / 从磁盘和缓存中删除地图
|
||||
public boolean delete(String id) {
|
||||
String filename = id + ".json";
|
||||
Path filePath = Paths.get(MAPS_DIR, filename);
|
||||
|
||||
try {
|
||||
boolean deleted = Files.deleteIfExists(filePath);
|
||||
if (deleted) {
|
||||
cache.remove(id);
|
||||
logger.info("Map deleted: {}", id);
|
||||
}
|
||||
return deleted;
|
||||
} catch (IOException e) {
|
||||
logger.error("Failed to delete map: {}", id, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package com.zombie.game.server;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
|
||||
/**
|
||||
* Utility class for extracting values from JSON messages / 工具类,用于从JSON消息中提取值
|
||||
*/
|
||||
public class MessageUtils {
|
||||
|
||||
// Get string value from JSON with default / 从JSON获取字符串值,带默认值
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// Get string value from JSON (null default) / 从JSON获取字符串值(默认null)
|
||||
public static String getString(JsonObject data, String key) {
|
||||
return getString(data, key, null);
|
||||
}
|
||||
|
||||
// Get float value from JSON with default / 从JSON获取浮点数值,带默认值
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// Get int value from JSON with default / 从JSON获取整数值,带默认值
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// Get boolean value from JSON with default / 从JSON获取布尔值,带默认值
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if all required keys are present and non-null / 检查所有必需键是否存在且非空
|
||||
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;
|
||||
}
|
||||
}
|
||||
103
backend/src/main/java/com/zombie/game/server/RoomManager.java
Normal file
103
backend/src/main/java/com/zombie/game/server/RoomManager.java
Normal file
@@ -0,0 +1,103 @@
|
||||
package com.zombie.game.server;
|
||||
|
||||
import com.zombie.game.model.PlayerInfo;
|
||||
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;
|
||||
|
||||
/**
|
||||
* Room manager for tracking game rooms and player-room assignments / 房间管理器,用于追踪游戏房间和玩家房间分配
|
||||
*/
|
||||
public class RoomManager {
|
||||
// All active rooms mapped by room ID / 所有活跃房间,以房间ID映射
|
||||
private final Map<String, Room> rooms = new ConcurrentHashMap<>();
|
||||
// Player ID to room ID mapping / 玩家ID到房间ID的映射
|
||||
private final Map<String, String> playerToRoom = new ConcurrentHashMap<>();
|
||||
|
||||
// Add a new room to the manager / 向管理器添加新房间
|
||||
public void addRoom(String roomId, Room room) {
|
||||
rooms.put(roomId, room);
|
||||
// Map existing players in room to room ID / 将房间中现有玩家映射到房间ID
|
||||
for (PlayerInfo player : room.getPlayers()) {
|
||||
playerToRoom.put(player.getId(), roomId);
|
||||
}
|
||||
}
|
||||
|
||||
// Add a player to a room / 将玩家添加到房间
|
||||
public boolean joinRoom(String roomId, String playerId, String playerName) {
|
||||
Room room = rooms.get(roomId);
|
||||
if (room == null) return false;
|
||||
// Reject if room is full / 房间已满则拒绝
|
||||
if (room.getPlayerCount() >= 4) return false;
|
||||
|
||||
boolean added = room.addPlayer(playerId, playerName);
|
||||
if (added) {
|
||||
playerToRoom.put(playerId, roomId);
|
||||
}
|
||||
return added;
|
||||
}
|
||||
|
||||
// Remove a player from their room / 从房间中移除玩家
|
||||
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);
|
||||
// Delete room if empty / 如果房间为空则删除
|
||||
if (room.getPlayerCount() == 0) {
|
||||
rooms.remove(roomId);
|
||||
return null;
|
||||
}
|
||||
return room;
|
||||
}
|
||||
|
||||
// Get room by player ID / 通过玩家ID获取房间
|
||||
public Room getRoomByPlayerId(String playerId) {
|
||||
String roomId = playerToRoom.get(playerId);
|
||||
if (roomId == null) return null;
|
||||
return rooms.get(roomId);
|
||||
}
|
||||
|
||||
// Get room by room ID / 通过房间ID获取房间
|
||||
public Room getRoom(String roomId) {
|
||||
return rooms.get(roomId);
|
||||
}
|
||||
|
||||
// Get all rooms / 获取所有房间
|
||||
public Collection<Room> getAllRooms() {
|
||||
return rooms.values();
|
||||
}
|
||||
|
||||
// Get rooms that haven't started a game yet / 获取尚未开始游戏的房间
|
||||
public List<Room> getAvailableRooms() {
|
||||
List<Room> available = new ArrayList<>();
|
||||
for (Room room : rooms.values()) {
|
||||
if (!room.isGameStarted()) {
|
||||
available.add(room);
|
||||
}
|
||||
}
|
||||
return available;
|
||||
}
|
||||
|
||||
// Remove a room and all player mappings / 移除房间及其所有玩家映射
|
||||
public void removeRoom(String roomId) {
|
||||
Room room = rooms.remove(roomId);
|
||||
if (room != null) {
|
||||
for (PlayerInfo player : room.getPlayers()) {
|
||||
playerToRoom.remove(player.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if a player is in any room / 检查玩家是否在任意房间中
|
||||
public boolean isPlayerInRoom(String playerId) {
|
||||
return playerToRoom.containsKey(playerId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.zombie.game.systems;
|
||||
|
||||
import com.zombie.game.ecs.ECSWorld;
|
||||
import com.zombie.game.ecs.System;
|
||||
import com.zombie.game.ecs.components.Position;
|
||||
import com.zombie.game.model.GameMap;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/** Updates flow field each frame based on player positions / 每帧根据玩家位置更新流场 */
|
||||
public class FlowFieldUpdateSystem implements System {
|
||||
|
||||
@Override
|
||||
public void update(float dt, ECSWorld world) {
|
||||
GameMap map = world.getMap();
|
||||
|
||||
List<float[]> playerPositions = new ArrayList<>();
|
||||
for (int entityId : world.getPlayers()) {
|
||||
Position pos = world.getPositions().get(entityId);
|
||||
if (pos != null) {
|
||||
playerPositions.add(new float[]{pos.getX(), pos.getY()});
|
||||
}
|
||||
}
|
||||
|
||||
if (!playerPositions.isEmpty()) {
|
||||
map.updateFlowField(playerPositions);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.zombie.game.systems;
|
||||
|
||||
import com.zombie.game.ecs.ECSWorld;
|
||||
import com.zombie.game.ecs.System;
|
||||
import com.zombie.game.ecs.components.*;
|
||||
|
||||
import static com.zombie.game.model.Constants.*;
|
||||
|
||||
/**
|
||||
* Processes player input to update positions and aim angles.
|
||||
* 处理玩家输入以更新位置和瞄准角度。
|
||||
*/
|
||||
public class PlayerInputSystem implements System {
|
||||
|
||||
@Override
|
||||
public void update(float dt, ECSWorld world) {
|
||||
// Iterate over all player entities / 遍历所有玩家实体
|
||||
for (int entityId : world.getPlayers()) {
|
||||
// Retrieve components for this entity / 获取该实体的组件
|
||||
PlayerInput input = world.getPlayerInputs().get(entityId);
|
||||
Position pos = world.getPositions().get(entityId);
|
||||
Health health = world.getHealths().get(entityId);
|
||||
|
||||
// Skip if any required component is missing or player is dead / 若缺少必要组件或玩家已死亡则跳过
|
||||
if (input == null || pos == null || health == null || !health.isAlive()) continue;
|
||||
|
||||
// Calculate movement speed based on tick interval / 根据 tick 间隔计算移动速度
|
||||
float speed = PLAYER_SPEED * TICK_INTERVAL;
|
||||
// Compute new position from input direction / 根据输入方向计算新位置
|
||||
float newX = pos.getX() + input.getDx() * speed;
|
||||
float newY = pos.getY() + input.getDy() * speed;
|
||||
|
||||
// Update X position if walkable, clamped to grid bounds / 若可行走则更新 X 位置,限制在网格边界内
|
||||
if (world.getMap().isWalkable(newX, pos.getY(), PLAYER_SIZE)) {
|
||||
pos.setX(Math.max(0.5f, Math.min(GRID_SIZE - 0.5f, newX)));
|
||||
}
|
||||
// Update Y position if walkable, clamped to grid bounds / 若可行走则更新 Y 位置,限制在网格边界内
|
||||
if (world.getMap().isWalkable(pos.getX(), newY, PLAYER_SIZE)) {
|
||||
pos.setY(Math.max(0.5f, Math.min(GRID_SIZE - 0.5f, newY)));
|
||||
}
|
||||
|
||||
// Update facing angle based on aim direction / 根据瞄准方向更新朝向角度
|
||||
pos.setAngle((float) Math.atan2(input.getAimX() - pos.getX(), input.getAimY() - pos.getY()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package com.zombie.game.systems;
|
||||
|
||||
import com.zombie.game.ecs.ECSWorld;
|
||||
import com.zombie.game.ecs.System;
|
||||
import com.zombie.game.ecs.components.*;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Collects and serializes game state for network synchronization.
|
||||
* 收集并序列化游戏状态以进行网络同步。
|
||||
*/
|
||||
public class StateSyncSystem implements System {
|
||||
|
||||
/**
|
||||
* Builds a map representing the current game state including all players and zombies.
|
||||
*/
|
||||
public Map<String, Object> buildGameState(ECSWorld world) {
|
||||
Map<String, Object> state = new LinkedHashMap<>();
|
||||
|
||||
// Players
|
||||
List<Map<String, Object>> playerStates = new ArrayList<>();
|
||||
for (int entityId : world.getPlayers()) {
|
||||
Map<String, Object> map = new LinkedHashMap<>();
|
||||
Position pos = world.getPositions().get(entityId);
|
||||
Health health = world.getHealths().get(entityId);
|
||||
|
||||
String playerId = null;
|
||||
for (var entry : world.getPlayerIdToEntity().entrySet()) {
|
||||
if (entry.getValue() == entityId) {
|
||||
playerId = entry.getKey();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
map.put("id", playerId);
|
||||
if (pos != null) {
|
||||
map.put("x", pos.getX());
|
||||
map.put("y", pos.getY());
|
||||
map.put("angle", pos.getAngle());
|
||||
}
|
||||
if (health != null) {
|
||||
map.put("health", health.getHealth());
|
||||
}
|
||||
|
||||
PlayerInput input = world.getPlayerInputs().get(entityId);
|
||||
if (input != null) {
|
||||
map.put("lastProcessedSeq", input.getSeq());
|
||||
}
|
||||
|
||||
playerStates.add(map);
|
||||
}
|
||||
|
||||
// Zombies (only alive)
|
||||
List<Map<String, Object>> zombieStates = new ArrayList<>();
|
||||
for (int entityId : world.getZombies()) {
|
||||
Health health = world.getHealths().get(entityId);
|
||||
if (health == null || !health.isAlive()) continue;
|
||||
|
||||
Map<String, Object> map = new LinkedHashMap<>();
|
||||
Position pos = world.getPositions().get(entityId);
|
||||
|
||||
map.put("id", entityId);
|
||||
if (pos != null) {
|
||||
map.put("x", pos.getX());
|
||||
map.put("y", pos.getY());
|
||||
map.put("angle", pos.getAngle());
|
||||
}
|
||||
map.put("health", health.getHealth());
|
||||
map.put("maxHealth", health.getMaxHealth());
|
||||
|
||||
zombieStates.add(map);
|
||||
}
|
||||
|
||||
// Nut walls
|
||||
List<Map<String, Object>> nutWallStates = new ArrayList<>();
|
||||
for (Map.Entry<Integer, Health> entry : world.getHealths().entrySet()) {
|
||||
int entityId = entry.getKey();
|
||||
// Skip non-wall entities (players and zombies have health too)
|
||||
if (world.getPlayers().contains(entityId) || world.getZombies().contains(entityId)) continue;
|
||||
if (!world.getBlocksMovements().containsKey(entityId)) continue;
|
||||
|
||||
Health health = entry.getValue();
|
||||
Position pos = world.getPositions().get(entityId);
|
||||
if (pos == null) continue;
|
||||
|
||||
Map<String, Object> wallState = new LinkedHashMap<>();
|
||||
wallState.put("id", entityId);
|
||||
wallState.put("x", (int) Math.floor(pos.getX()));
|
||||
wallState.put("y", (int) Math.floor(pos.getY()));
|
||||
wallState.put("health", health.getHealth());
|
||||
wallState.put("maxHealth", health.getMaxHealth());
|
||||
nutWallStates.add(wallState);
|
||||
}
|
||||
|
||||
state.put("players", playerStates);
|
||||
state.put("zombies", zombieStates);
|
||||
state.put("nutWalls", nutWallStates);
|
||||
state.put("gameTime", world.getGameTime());
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(float dt, ECSWorld world) {
|
||||
// No per-frame logic; state is built on demand
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
package com.zombie.game.systems;
|
||||
|
||||
import com.zombie.game.ecs.ECSWorld;
|
||||
import com.zombie.game.ecs.System;
|
||||
import com.zombie.game.ecs.components.*;
|
||||
import com.zombie.game.model.GameMap;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import static com.zombie.game.model.Constants.*;
|
||||
|
||||
/** Zombie movement system using flow field pathfinding / 僵尸移动系统,使用流场寻路 */
|
||||
public class ZombieMovementSystem implements System {
|
||||
|
||||
@Override
|
||||
public void update(float dt, ECSWorld world) {
|
||||
GameMap map = world.getMap();
|
||||
if (!map.isFlowFieldValid()) return;
|
||||
|
||||
List<Integer> sortedZombies = new ArrayList<>(world.getZombies());
|
||||
Collections.sort(sortedZombies);
|
||||
|
||||
for (int entityId : sortedZombies) {
|
||||
Position pos = world.getPositions().get(entityId);
|
||||
Health health = world.getHealths().get(entityId);
|
||||
ZombieAI ai = world.getZombieAIs().get(entityId);
|
||||
|
||||
if (pos == null || health == null || !health.isAlive()) continue;
|
||||
if (ai == null) continue;
|
||||
|
||||
// Target selection: query flow field for next grid
|
||||
float centerDist = Float.MAX_VALUE;
|
||||
if (ai.isHasTarget()) {
|
||||
float dx = ai.getTargetX() - pos.getX();
|
||||
float dy = ai.getTargetY() - pos.getY();
|
||||
centerDist = (float) Math.sqrt(dx * dx + dy * dy);
|
||||
}
|
||||
|
||||
if (!ai.isHasTarget() || centerDist < GRID_CENTER_THRESHOLD) {
|
||||
float[] flowDir = map.getFlowDirection(pos.getX(), pos.getY());
|
||||
float dirX = flowDir[0];
|
||||
float dirY = flowDir[1];
|
||||
|
||||
if (dirX == 0 && dirY == 0) continue;
|
||||
|
||||
int currentGridX = (int) Math.floor(pos.getX());
|
||||
int currentGridY = (int) Math.floor(pos.getY());
|
||||
int nextGridX = currentGridX + (int) Math.round(dirX);
|
||||
int nextGridY = currentGridY + (int) Math.round(dirY);
|
||||
|
||||
// If next grid is wall, try to slide along single axis
|
||||
if (map.isWall(nextGridX, nextGridY)) {
|
||||
nextGridX = currentGridX + (int) Math.signum(dirX);
|
||||
nextGridY = currentGridY + (int) Math.signum(dirY);
|
||||
if (map.isWall(nextGridX, nextGridY)) {
|
||||
if (!map.isWall(currentGridX + (int) Math.signum(dirX), currentGridY)) {
|
||||
nextGridX = currentGridX + (int) Math.signum(dirX);
|
||||
nextGridY = currentGridY;
|
||||
} else if (!map.isWall(currentGridX, currentGridY + (int) Math.signum(dirY))) {
|
||||
nextGridX = currentGridX;
|
||||
nextGridY = currentGridY + (int) Math.signum(dirY);
|
||||
} else {
|
||||
ai.setReservation(false);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check grid occupation by other zombies
|
||||
if (isGridOccupiedOrReserved(nextGridX, nextGridY, world, entityId)) {
|
||||
int[] alt = findAlternativeDirection(currentGridX, currentGridY, dirX, dirY, map, world, entityId);
|
||||
if (alt != null) {
|
||||
nextGridX = alt[0];
|
||||
nextGridY = alt[1];
|
||||
} else {
|
||||
ai.setReservation(false);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
ai.setReservedGridX(nextGridX);
|
||||
ai.setReservedGridY(nextGridY);
|
||||
ai.setReservation(true);
|
||||
ai.setTargetX(nextGridX + 0.5f);
|
||||
ai.setTargetY(nextGridY + 0.5f);
|
||||
ai.setHasTarget(true);
|
||||
}
|
||||
|
||||
// Move toward target
|
||||
float dx = ai.getTargetX() - pos.getX();
|
||||
float dy = ai.getTargetY() - pos.getY();
|
||||
float dist = (float) Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (dist < 0.01f) {
|
||||
ai.setHasTarget(false);
|
||||
continue;
|
||||
}
|
||||
|
||||
float moveDirX = dx / dist;
|
||||
float moveDirY = dy / dist;
|
||||
float speed = ZOMBIE_SPEED;
|
||||
|
||||
float moveX = moveDirX * speed * dt;
|
||||
float moveY = moveDirY * speed * dt;
|
||||
float newX = pos.getX() + moveX;
|
||||
float newY = pos.getY() + moveY;
|
||||
|
||||
boolean canMoveX = map.isWalkable(newX, pos.getY(), ZOMBIE_SIZE);
|
||||
boolean canMoveY = map.isWalkable(pos.getX(), newY, ZOMBIE_SIZE);
|
||||
|
||||
if (moveX != 0 && moveY != 0) {
|
||||
// Diagonal: check corner blocking
|
||||
int checkX = (int) Math.floor(newX);
|
||||
int checkY = (int) Math.floor(newY);
|
||||
int checkCurrentX = (int) Math.floor(pos.getX());
|
||||
int checkCurrentY = (int) Math.floor(pos.getY());
|
||||
|
||||
boolean blockedByCorner = false;
|
||||
if (checkX != checkCurrentX && checkY != checkCurrentY) {
|
||||
boolean wallInX = map.isWall(checkX, checkCurrentY);
|
||||
boolean wallInY = map.isWall(checkCurrentX, checkY);
|
||||
if (wallInX || wallInY) {
|
||||
blockedByCorner = true;
|
||||
if (!wallInX && canMoveX) {
|
||||
pos.setX(newX);
|
||||
} else if (!wallInY && canMoveY) {
|
||||
pos.setY(newY);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!blockedByCorner) {
|
||||
boolean canMoveDiag = map.isWalkable(newX, newY, ZOMBIE_SIZE);
|
||||
if (canMoveDiag) {
|
||||
pos.setX(newX);
|
||||
pos.setY(newY);
|
||||
} else {
|
||||
if (canMoveX) pos.setX(newX);
|
||||
if (canMoveY) pos.setY(newY);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (canMoveX) pos.setX(newX);
|
||||
if (canMoveY) pos.setY(newY);
|
||||
}
|
||||
|
||||
// Zombie separation
|
||||
for (int otherId : world.getZombies()) {
|
||||
if (otherId == entityId) continue;
|
||||
Health otherHealth = world.getHealths().get(otherId);
|
||||
if (otherHealth == null || !otherHealth.isAlive()) continue;
|
||||
Position otherPos = world.getPositions().get(otherId);
|
||||
if (otherPos == null) continue;
|
||||
|
||||
float sepDx = pos.getX() - otherPos.getX();
|
||||
float sepDy = pos.getY() - otherPos.getY();
|
||||
float sepDist = (float) Math.sqrt(sepDx * sepDx + sepDy * sepDy);
|
||||
|
||||
if (sepDist < ZOMBIE_SIZE && sepDist > 0.01f) {
|
||||
float overlap = ZOMBIE_SIZE - sepDist;
|
||||
float pushX = (sepDx / sepDist) * overlap * 0.5f;
|
||||
float pushY = (sepDy / sepDist) * overlap * 0.5f;
|
||||
|
||||
if (map.isWalkable(pos.getX() + pushX, pos.getY() + pushY, ZOMBIE_SIZE)) {
|
||||
pos.setX(pos.getX() + pushX);
|
||||
pos.setY(pos.getY() + pushY);
|
||||
} else {
|
||||
if (map.isWalkable(pos.getX() + pushX, pos.getY(), ZOMBIE_SIZE)) {
|
||||
pos.setX(pos.getX() + pushX);
|
||||
}
|
||||
if (map.isWalkable(pos.getX(), pos.getY() + pushY, ZOMBIE_SIZE)) {
|
||||
pos.setY(pos.getY() + pushY);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update facing angle
|
||||
if (moveDirX != 0 || moveDirY != 0) {
|
||||
pos.setAngle((float) Math.atan2(moveDirX, moveDirY));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isGridOccupiedOrReserved(int gridX, int gridY, ECSWorld world, int selfId) {
|
||||
for (int otherId : world.getZombies()) {
|
||||
if (otherId == selfId) continue;
|
||||
Health otherHealth = world.getHealths().get(otherId);
|
||||
if (otherHealth == null || !otherHealth.isAlive()) continue;
|
||||
|
||||
Position otherPos = world.getPositions().get(otherId);
|
||||
if (otherPos == null) continue;
|
||||
|
||||
int otherGridX = (int) Math.floor(otherPos.getX());
|
||||
int otherGridY = (int) Math.floor(otherPos.getY());
|
||||
if (otherGridX == gridX && otherGridY == gridY) return true;
|
||||
|
||||
ZombieAI otherAi = world.getZombieAIs().get(otherId);
|
||||
if (otherAi != null && otherAi.isReservation() &&
|
||||
otherAi.getReservedGridX() == gridX && otherAi.getReservedGridY() == gridY) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private int[] findAlternativeDirection(int currentGridX, int currentGridY, float dirX, float dirY,
|
||||
GameMap map, ECSWorld world, int selfId) {
|
||||
int[][] allDirs = {{0, -1}, {0, 1}, {-1, 0}, {1, 0}, {-1, -1}, {1, -1}, {-1, 1}, {1, 1}};
|
||||
List<int[]> candidates = new ArrayList<>();
|
||||
|
||||
for (int[] dir : allDirs) {
|
||||
int nx = currentGridX + dir[0];
|
||||
int ny = currentGridY + dir[1];
|
||||
|
||||
if (map.isWall(nx, ny)) continue;
|
||||
|
||||
if (dir[0] != 0 && dir[1] != 0) {
|
||||
if (map.isWall(currentGridX + dir[0], currentGridY) ||
|
||||
map.isWall(currentGridX, currentGridY + dir[1])) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (isGridOccupiedOrReserved(nx, ny, world, selfId)) continue;
|
||||
|
||||
float dotProduct = dir[0] * dirX + dir[1] * dirY;
|
||||
candidates.add(new int[]{nx, ny, (int) (dotProduct * 1000)});
|
||||
}
|
||||
|
||||
if (candidates.isEmpty()) return null;
|
||||
candidates.sort((a, b) -> b[2] - a[2]);
|
||||
return new int[]{candidates.get(0)[0], candidates.get(0)[1]};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.zombie.game.systems;
|
||||
|
||||
import com.zombie.game.ecs.ECSWorld;
|
||||
import com.zombie.game.ecs.System;
|
||||
import com.zombie.game.ecs.components.Health;
|
||||
import com.zombie.game.model.GameMap;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
|
||||
import static com.zombie.game.model.Constants.*;
|
||||
|
||||
/** Periodically spawns zombies at map spawn points / 定期在地图出生点生成僵尸 */
|
||||
public class ZombieSpawnSystem implements System {
|
||||
private static final Logger logger = LoggerFactory.getLogger(ZombieSpawnSystem.class);
|
||||
private final Random random = new Random();
|
||||
private float spawnTimer = 0;
|
||||
|
||||
@Override
|
||||
public void update(float dt, ECSWorld world) {
|
||||
spawnTimer += dt;
|
||||
if (spawnTimer < ZOMBIE_SPAWN_INTERVAL) return;
|
||||
spawnTimer -= ZOMBIE_SPAWN_INTERVAL;
|
||||
|
||||
// Count alive zombies
|
||||
int aliveCount = 0;
|
||||
for (int entityId : world.getZombies()) {
|
||||
Health health = world.getHealths().get(entityId);
|
||||
if (health != null && health.isAlive()) {
|
||||
aliveCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (aliveCount >= ZOMBIE_MAX_COUNT) return;
|
||||
|
||||
// Pick a random spawn point
|
||||
GameMap map = world.getMap();
|
||||
List<int[]> spawns = map.getZombieSpawnPoints();
|
||||
if (spawns.isEmpty()) return;
|
||||
|
||||
int[] sp = spawns.get(random.nextInt(spawns.size()));
|
||||
int newZombie = world.createZombieEntity(sp[0] + 0.5f, sp[1] + 0.5f);
|
||||
logger.info("Zombie spawned: id={} at ({}, {}), alive={}", newZombie, sp[0], sp[1], aliveCount + 1);
|
||||
}
|
||||
}
|
||||
BIN
backend/target/classes/com/zombie/game/GameServerMain.class
Normal file
BIN
backend/target/classes/com/zombie/game/GameServerMain.class
Normal file
Binary file not shown.
BIN
backend/target/classes/com/zombie/game/ecs/ECSWorld.class
Normal file
BIN
backend/target/classes/com/zombie/game/ecs/ECSWorld.class
Normal file
Binary file not shown.
BIN
backend/target/classes/com/zombie/game/ecs/System.class
Normal file
BIN
backend/target/classes/com/zombie/game/ecs/System.class
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
backend/target/classes/com/zombie/game/model/Constants.class
Normal file
BIN
backend/target/classes/com/zombie/game/model/Constants.class
Normal file
Binary file not shown.
Binary file not shown.
BIN
backend/target/classes/com/zombie/game/model/FlowField.class
Normal file
BIN
backend/target/classes/com/zombie/game/model/FlowField.class
Normal file
Binary file not shown.
BIN
backend/target/classes/com/zombie/game/model/GameMap.class
Normal file
BIN
backend/target/classes/com/zombie/game/model/GameMap.class
Normal file
Binary file not shown.
BIN
backend/target/classes/com/zombie/game/model/MapData.class
Normal file
BIN
backend/target/classes/com/zombie/game/model/MapData.class
Normal file
Binary file not shown.
BIN
backend/target/classes/com/zombie/game/model/PlayerInfo.class
Normal file
BIN
backend/target/classes/com/zombie/game/model/PlayerInfo.class
Normal file
Binary file not shown.
BIN
backend/target/classes/com/zombie/game/model/Room.class
Normal file
BIN
backend/target/classes/com/zombie/game/model/Room.class
Normal file
Binary file not shown.
BIN
backend/target/classes/com/zombie/game/server/GameLoop.class
Normal file
BIN
backend/target/classes/com/zombie/game/server/GameLoop.class
Normal file
Binary file not shown.
Binary file not shown.
BIN
backend/target/classes/com/zombie/game/server/GameService.class
Normal file
BIN
backend/target/classes/com/zombie/game/server/GameService.class
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
backend/target/classes/com/zombie/game/server/MapStorage.class
Normal file
BIN
backend/target/classes/com/zombie/game/server/MapStorage.class
Normal file
Binary file not shown.
BIN
backend/target/classes/com/zombie/game/server/MessageUtils.class
Normal file
BIN
backend/target/classes/com/zombie/game/server/MessageUtils.class
Normal file
Binary file not shown.
BIN
backend/target/classes/com/zombie/game/server/RoomManager.class
Normal file
BIN
backend/target/classes/com/zombie/game/server/RoomManager.class
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
3
backend/target/maven-archiver/pom.properties
Normal file
3
backend/target/maven-archiver/pom.properties
Normal file
@@ -0,0 +1,3 @@
|
||||
artifactId=zombie-crisis-server
|
||||
groupId=com.zombie
|
||||
version=1.0.0
|
||||
@@ -0,0 +1,24 @@
|
||||
com/zombie/game/ecs/components/PlayerInput.class
|
||||
com/zombie/game/model/MapData.class
|
||||
com/zombie/game/model/GameMap.class
|
||||
com/zombie/game/ecs/components/Collision.class
|
||||
com/zombie/game/ecs/ECSWorld.class
|
||||
com/zombie/game/server/GameWebSocketServer$1.class
|
||||
com/zombie/game/ecs/components/Position.class
|
||||
com/zombie/game/GameServerMain.class
|
||||
com/zombie/game/server/MessageUtils.class
|
||||
com/zombie/game/model/PlayerInfo.class
|
||||
com/zombie/game/server/GameService$GameStateBroadcast.class
|
||||
com/zombie/game/server/GameService.class
|
||||
com/zombie/game/server/GameWebSocketServer.class
|
||||
com/zombie/game/ecs/components/RenderInfo$EntityType.class
|
||||
com/zombie/game/ecs/System.class
|
||||
com/zombie/game/model/Room.class
|
||||
com/zombie/game/server/MapStorage.class
|
||||
com/zombie/game/systems/PlayerInputSystem.class
|
||||
com/zombie/game/systems/StateSyncSystem.class
|
||||
com/zombie/game/ecs/components/Health.class
|
||||
com/zombie/game/model/Constants.class
|
||||
com/zombie/game/ecs/components/RenderInfo.class
|
||||
com/zombie/game/server/GameLoop.class
|
||||
com/zombie/game/server/RoomManager.class
|
||||
@@ -0,0 +1,28 @@
|
||||
/Users/wfz/workspace/zp2/backend/src/main/java/com/zombie/game/server/GameWebSocketServer.java
|
||||
/Users/wfz/workspace/zp2/backend/src/main/java/com/zombie/game/systems/ZombieSpawnSystem.java
|
||||
/Users/wfz/workspace/zp2/backend/src/main/java/com/zombie/game/ecs/ECSWorld.java
|
||||
/Users/wfz/workspace/zp2/backend/src/main/java/com/zombie/game/model/GameMap.java
|
||||
/Users/wfz/workspace/zp2/backend/src/main/java/com/zombie/game/systems/StateSyncSystem.java
|
||||
/Users/wfz/workspace/zp2/backend/src/main/java/com/zombie/game/server/GameLoop.java
|
||||
/Users/wfz/workspace/zp2/backend/src/main/java/com/zombie/game/ecs/components/ZombieAI.java
|
||||
/Users/wfz/workspace/zp2/backend/src/main/java/com/zombie/game/systems/PlayerInputSystem.java
|
||||
/Users/wfz/workspace/zp2/backend/src/main/java/com/zombie/game/ecs/components/Health.java
|
||||
/Users/wfz/workspace/zp2/backend/src/main/java/com/zombie/game/ecs/components/Collision.java
|
||||
/Users/wfz/workspace/zp2/backend/src/main/java/com/zombie/game/GameServerMain.java
|
||||
/Users/wfz/workspace/zp2/backend/src/main/java/com/zombie/game/model/Constants.java
|
||||
/Users/wfz/workspace/zp2/backend/src/main/java/com/zombie/game/server/RoomManager.java
|
||||
/Users/wfz/workspace/zp2/backend/src/main/java/com/zombie/game/server/GameService.java
|
||||
/Users/wfz/workspace/zp2/backend/src/main/java/com/zombie/game/model/MapData.java
|
||||
/Users/wfz/workspace/zp2/backend/src/main/java/com/zombie/game/systems/ZombieMovementSystem.java
|
||||
/Users/wfz/workspace/zp2/backend/src/main/java/com/zombie/game/ecs/components/BlocksMovement.java
|
||||
/Users/wfz/workspace/zp2/backend/src/main/java/com/zombie/game/server/MessageUtils.java
|
||||
/Users/wfz/workspace/zp2/backend/src/main/java/com/zombie/game/ecs/System.java
|
||||
/Users/wfz/workspace/zp2/backend/src/main/java/com/zombie/game/model/Room.java
|
||||
/Users/wfz/workspace/zp2/backend/src/main/java/com/zombie/game/server/MapStorage.java
|
||||
/Users/wfz/workspace/zp2/backend/src/main/java/com/zombie/game/ecs/components/PlayerInput.java
|
||||
/Users/wfz/workspace/zp2/backend/src/main/java/com/zombie/game/ecs/components/RenderInfo.java
|
||||
/Users/wfz/workspace/zp2/backend/src/main/java/com/zombie/game/model/PlayerInfo.java
|
||||
/Users/wfz/workspace/zp2/backend/src/main/java/com/zombie/game/ecs/components/MovementCost.java
|
||||
/Users/wfz/workspace/zp2/backend/src/main/java/com/zombie/game/ecs/components/Position.java
|
||||
/Users/wfz/workspace/zp2/backend/src/main/java/com/zombie/game/systems/FlowFieldUpdateSystem.java
|
||||
/Users/wfz/workspace/zp2/backend/src/main/java/com/zombie/game/model/FlowField.java
|
||||
BIN
backend/target/original-zombie-crisis-server-1.0.0.jar
Normal file
BIN
backend/target/original-zombie-crisis-server-1.0.0.jar
Normal file
Binary file not shown.
BIN
backend/target/zombie-crisis-server-1.0.0.jar
Normal file
BIN
backend/target/zombie-crisis-server-1.0.0.jar
Normal file
Binary file not shown.
Reference in New Issue
Block a user