This commit is contained in:
wfz
2026-05-02 21:30:28 +08:00
parent 9fd572c8c4
commit 18ac2f70f5
18 changed files with 772 additions and 133 deletions

View File

@@ -2,6 +2,7 @@ package com.zombie.game.ecs;
import com.zombie.game.ecs.components.*;
import com.zombie.game.model.GameMap;
import com.zombie.game.model.TurretWall;
import com.zombie.game.template.TemplateManager;
import com.zombie.game.template.ZombieTemplate;
import lombok.Getter;
@@ -78,6 +79,14 @@ public class ECSWorld {
/** 墙体实体组件 */
private final Map<Integer, WallEntity> wallEntityDatas = new HashMap<>();
// ==================== 对象池(子弹组件) ====================
private static final int BULLET_POOL_SIZE = 100;
private final ObjectPool<Position> positionPool = new ObjectPool<>(Position::new, BULLET_POOL_SIZE);
private final ObjectPool<Velocity> velocityPool = new ObjectPool<>(() -> new Velocity(0, 0, 0), BULLET_POOL_SIZE);
private final ObjectPool<BulletData> bulletDataPool = new ObjectPool<>(() -> new BulletData(0, "", 0, 0), BULLET_POOL_SIZE);
private final ObjectPool<Collision> collisionPool = new ObjectPool<>(() -> new Collision(0), BULLET_POOL_SIZE);
private final ObjectPool<RenderInfo> renderInfoPool = new ObjectPool<>(() -> new RenderInfo(RenderInfo.EntityType.BULLET), BULLET_POOL_SIZE);
// ==================== 系统列表 ====================
private final List<System> systems = new ArrayList<>();
@@ -136,8 +145,14 @@ public class ECSWorld {
return id;
}
/** 销毁实体,清除所有组件和集合引用 */
/** 销毁实体,清除所有组件和集合引用,子弹组件归还对象池 */
public void destroyEntity(int id) {
// 机枪塔销毁时同步移除流场障碍物
WallEntity we = wallEntityDatas.get(id);
if (we != null && turrets.contains(id)) {
map.removeWall(we.getGridX(), we.getGridY());
}
entities.remove(id);
players.remove(id);
zombies.remove(id);
@@ -149,21 +164,32 @@ public class ECSWorld {
wallEntities.remove(id);
entityNames.remove(id);
positions.remove(id);
healths.remove(id);
collisions.remove(id);
renderInfos.remove(id);
playerInputs.remove(id);
weaponStates.remove(id);
zombieAIs.remove(id);
velocities.remove(id);
bulletDatas.remove(id);
explosives.remove(id);
lootDatas.remove(id);
respawnStates.remove(id);
fireZonesData.remove(id);
turretStates.remove(id);
wallEntityDatas.remove(id);
// 子弹高频组件归还对象池
Position p = positions.remove(id);
if (p != null) positionPool.free(p);
Velocity v = velocities.remove(id);
if (v != null) velocityPool.free(v);
BulletData bd = bulletDatas.remove(id);
if (bd != null) bulletDataPool.free(bd);
Collision c = collisions.remove(id);
if (c != null) collisionPool.free(c);
RenderInfo ri = renderInfos.remove(id);
if (ri != null) renderInfoPool.free(ri);
}
// ==================== 玩家实体创建 ====================
@@ -230,21 +256,39 @@ public class ECSWorld {
// ==================== 子弹实体创建 ====================
/**
* 创建标准子弹实体(直线飞行)
* 创建标准子弹实体(直线飞行),使用对象池复用组件
*/
public int createBulletEntity(float x, float y, float angle, float speed, int damage,
String ownerId, int weaponIndex, float range, boolean isZombieBullet) {
int entityId = createEntity();
positions.put(entityId, new Position(x, y, angle));
velocities.put(entityId, new Velocity(
(float) Math.sin(angle) * speed,
(float) Math.cos(angle) * speed
));
bulletDatas.put(entityId, new BulletData(damage, ownerId, weaponIndex, range));
collisions.put(entityId, new Collision(0.2f));
renderInfos.put(entityId, new RenderInfo(
isZombieBullet ? RenderInfo.EntityType.ZOMBIE_BULLET : RenderInfo.EntityType.BULLET
));
Position pos = positionPool.obtain();
pos.setX(x); pos.setY(y); pos.setAngle(angle);
positions.put(entityId, pos);
Velocity vel = velocityPool.obtain();
vel.setVx((float) Math.sin(angle) * speed);
vel.setVy((float) Math.cos(angle) * speed);
vel.setVz(0);
velocities.put(entityId, vel);
BulletData bd = bulletDataPool.obtain();
bd.setDamage(damage); bd.setOwnerId(ownerId); bd.setWeaponIndex(weaponIndex);
bd.setRange(range); bd.setDistanceTraveled(0);
bd.setGrenade(false); bd.setMolotov(false);
bd.setFlightTime(0); bd.setMaxFlightTime(0);
bd.setStartX(0); bd.setStartY(0);
bd.setTargetX(0); bd.setTargetY(0); bd.setHasTarget(false);
bulletDatas.put(entityId, bd);
Collision col = collisionPool.obtain();
col.setSize(0.2f);
collisions.put(entityId, col);
RenderInfo ri = renderInfoPool.obtain();
ri.setType(isZombieBullet ? RenderInfo.EntityType.ZOMBIE_BULLET : RenderInfo.EntityType.BULLET);
ri.setSubType(""); ri.setWeaponIndex(-1);
renderInfos.put(entityId, ri);
if (isZombieBullet) {
zombieBullets.add(entityId);
@@ -255,55 +299,83 @@ public class ECSWorld {
}
/**
* 创建手榴弹实体(抛物线飞行,落地爆炸)
* 创建手榴弹实体(抛物线飞行,落地爆炸),使用对象池复用组件
*/
public int createGrenadeEntity(float startX, float startY, float targetX, float targetY,
float flightDuration, int damage, String ownerId, float explosionRadius) {
float flightDuration, int damage, String ownerId, int weaponIndex, float explosionRadius) {
int entityId = createEntity();
positions.put(entityId, new Position(startX, startY));
Position pos = positionPool.obtain();
pos.setX(startX); pos.setY(startY); pos.setAngle(0);
positions.put(entityId, pos);
float vx = (targetX - startX) / flightDuration;
float vy = (targetY - startY) / flightDuration;
velocities.put(entityId, new Velocity(vx, vy, 3.0f));
Velocity vel = velocityPool.obtain();
vel.setVx(vx); vel.setVy(vy); vel.setVz(3.0f);
velocities.put(entityId, vel);
BulletData data = BulletData.createGrenade(damage, ownerId, targetX, targetY, flightDuration);
data.setStartX(startX);
data.setStartY(startY);
bulletDatas.put(entityId, data);
BulletData bd = bulletDataPool.obtain();
bd.setDamage(damage); bd.setOwnerId(ownerId); bd.setWeaponIndex(weaponIndex); bd.setRange(0);
bd.setDistanceTraveled(0); bd.setGrenade(true); bd.setMolotov(false);
bd.setFlightTime(0); bd.setMaxFlightTime(flightDuration);
bd.setStartX(startX); bd.setStartY(startY);
bd.setTargetX(targetX); bd.setTargetY(targetY); bd.setHasTarget(true);
bulletDatas.put(entityId, bd);
explosives.put(entityId, new Explosive(explosionRadius, damage));
collisions.put(entityId, new Collision(0.2f));
renderInfos.put(entityId, new RenderInfo(RenderInfo.EntityType.BULLET));
Collision col = collisionPool.obtain();
col.setSize(0.2f);
collisions.put(entityId, col);
RenderInfo ri = renderInfoPool.obtain();
ri.setType(RenderInfo.EntityType.BULLET); ri.setSubType(""); ri.setWeaponIndex(-1);
renderInfos.put(entityId, ri);
playerBullets.add(entityId);
return entityId;
}
/**
* 创建燃烧瓶实体(抛物线飞行,落地后产生火焰区域)
* 创建燃烧瓶实体(抛物线飞行,落地后产生火焰区域),使用对象池复用组件
*/
public int createMolotovEntity(float startX, float startY, float targetX, float targetY,
float flightDuration, int damage, String ownerId,
float flightDuration, int damage, String ownerId, int weaponIndex,
float explosionRadius, float fireZoneRadius,
float fireZoneDamage, float fireZoneDuration) {
int entityId = createEntity();
positions.put(entityId, new Position(startX, startY));
Position pos = positionPool.obtain();
pos.setX(startX); pos.setY(startY); pos.setAngle(0);
positions.put(entityId, pos);
float vx = (targetX - startX) / flightDuration;
float vy = (targetY - startY) / flightDuration;
velocities.put(entityId, new Velocity(vx, vy, 3.0f));
Velocity vel = velocityPool.obtain();
vel.setVx(vx); vel.setVy(vy); vel.setVz(3.0f);
velocities.put(entityId, vel);
BulletData data = BulletData.createMolotov(damage, ownerId, targetX, targetY, flightDuration);
data.setStartX(startX);
data.setStartY(startY);
bulletDatas.put(entityId, data);
BulletData bd = bulletDataPool.obtain();
bd.setDamage(damage); bd.setOwnerId(ownerId); bd.setWeaponIndex(weaponIndex); bd.setRange(0);
bd.setDistanceTraveled(0); bd.setGrenade(false); bd.setMolotov(true);
bd.setFlightTime(0); bd.setMaxFlightTime(flightDuration);
bd.setStartX(startX); bd.setStartY(startY);
bd.setTargetX(targetX); bd.setTargetY(targetY); bd.setHasTarget(true);
bulletDatas.put(entityId, bd);
// 存储爆炸参数(落地初始爆炸使用)
explosives.put(entityId, new Explosive(explosionRadius, damage));
// 存储火焰区域参数落地时创建FireZone使用
fireZonesData.put(entityId, new FireZone(fireZoneRadius, fireZoneDamage, fireZoneDuration, ownerId));
collisions.put(entityId, new Collision(0.2f));
renderInfos.put(entityId, new RenderInfo(RenderInfo.EntityType.BULLET));
Collision col = collisionPool.obtain();
col.setSize(0.2f);
collisions.put(entityId, col);
RenderInfo ri = renderInfoPool.obtain();
ri.setType(RenderInfo.EntityType.BULLET); ri.setSubType(""); ri.setWeaponIndex(-1);
renderInfos.put(entityId, ri);
playerBullets.add(entityId);
return entityId;
@@ -356,6 +428,9 @@ public class ECSWorld {
wallEntityDatas.put(entityId, new WallEntity(gridX, gridY));
renderInfos.put(entityId, new RenderInfo(RenderInfo.EntityType.TURRET));
// 注册为流场障碍物,僵尸会将其视为可攻击目标
map.addWall(gridX, gridY, new TurretWall(gridX, gridY, entityId));
turrets.add(entityId);
wallEntities.add(entityId);
return entityId;
@@ -375,6 +450,19 @@ public class ECSWorld {
/** 获取当前僵尸数量 */
public int getZombieCount() { return zombies.size(); }
/** 根据网格坐标查找机枪塔实体ID未找到返回 -1 */
public int getTurretAtGrid(int gridX, int gridY) {
for (int turretId : turrets) {
WallEntity we = wallEntityDatas.get(turretId);
Health h = healths.get(turretId);
if (we != null && h != null && h.isAlive()
&& we.getGridX() == gridX && we.getGridY() == gridY) {
return turretId;
}
}
return -1;
}
/** 获取地图网格数据 */
public int[][] getMapData() { return map.getCells(); }

View File

@@ -0,0 +1,41 @@
package com.zombie.game.ecs;
import java.util.ArrayDeque;
import java.util.function.Supplier;
/**
* 通用对象池
* 避免频繁创建/销毁短生命周期对象(如子弹组件),减少 GC 压力。
*
* @param <T> 池化对象类型
*/
public class ObjectPool<T> {
private final ArrayDeque<T> pool;
private final Supplier<T> factory;
public ObjectPool(Supplier<T> factory, int initialCapacity) {
this.factory = factory;
this.pool = new ArrayDeque<>(initialCapacity);
for (int i = 0; i < initialCapacity; i++) {
pool.push(factory.get());
}
}
/** 从池中获取一个对象,池空时自动创建新实例 */
public T obtain() {
return pool.isEmpty() ? factory.get() : pool.pop();
}
/** 归还对象到池中(调用方需确保不再引用该对象) */
public void free(T obj) {
if (obj != null) {
pool.push(obj);
}
}
/** 当前池中可用对象数 */
public int getFreeCount() {
return pool.size();
}
}

View File

@@ -15,6 +15,13 @@ public class Position {
/** 朝向角度(弧度) */
private float angle;
/** 无参构造(对象池复用) */
public Position() {
this.x = 0;
this.y = 0;
this.angle = 0;
}
public Position(float x, float y) {
this.x = x;
this.y = y;

View File

@@ -28,6 +28,12 @@ public class ZombieAI {
private long lastAttackTime;
/** 上次远程攻击时间戳(毫秒,精英僵尸使用) */
private long lastRangedAttackTime;
/** 正在攻击的机枪塔实体ID-1表示未攻击机枪塔 */
private int attackingTurretId = -1;
/** 是否正在攻击(用于前端播放攻击动画) */
private boolean isAttacking;
/** 攻击动画计时器(秒),>0 时持续播放攻击动画 */
private float attackAnimTimer;
public ZombieAI(String templateId) {
this.templateId = templateId;

View File

@@ -186,12 +186,13 @@ public class FlowField {
}
/**
* 检查格子是否被阻挡
* 检查格子是否被完全阻挡(只有静态墙不可通行)
* 坚果墙视为高代价可通行,僵尸会选择攻击而非绕路
*/
private boolean isBlocked(Map<String, Wall> walls, int gx, int gy) {
if (gx < 0 || gx >= width || gy < 0 || gy >= height) return true;
Wall wall = walls.get(key(gx, gy));
return wall != null && wall.blocksMovement();
return wall instanceof StaticWall;
}
/**

View File

@@ -133,6 +133,22 @@ public class GameMap {
return true;
}
/**
* 添加任意类型的墙体(用于机枪塔等非坚果墙体)
*
* @param gx 格子X坐标
* @param gy 格子Y坐标
* @param wall 墙体对象
* @return true 表示添加成功
*/
public boolean addWall(int gx, int gy, Wall wall) {
if (gx < 0 || gx >= width || gy < 0 || gy >= height) return false;
if (walls.containsKey(key(gx, gy))) return false;
walls.put(key(gx, gy), wall);
flowField.invalidate();
return true;
}
/**
* 移除墙体(当坚果被破坏时调用)
*

View File

@@ -14,8 +14,8 @@ import lombok.Getter;
* 这样 BFS 会自动权衡绕道 vs 摧毁。
*/
public class NutWall extends Wall {
/** 坚果最大血量20个时间单位 */
public static final float MAX_HEALTH = 20.0f;
/** 坚果最大血量 */
public static final float MAX_HEALTH = 500.0f;
/** 坚果在流场中的额外移动代价(摧毁时间折算) */
public static final float MOVEMENT_COST_PENALTY = 20.0f;
/** 当前血量 */

View File

@@ -0,0 +1,48 @@
package com.zombie.game.model;
import lombok.Getter;
/**
* 机枪塔墙体
*
* 用于流场寻路中将机枪塔视为障碍物。
* 实际生命值由 ECS Health 组件管理,此类的 takeDamage 为空操作。
* 僵尸路径经过时会攻击机枪塔实体。
*/
public class TurretWall extends Wall {
/** 关联的机枪塔实体ID */
@Getter
private final int turretEntityId;
/** 流场中的额外移动代价(比坚果低,僵尸更倾向攻击塔而非绕路) */
public static final float MOVEMENT_COST_PENALTY = 5.0f;
public TurretWall(int gridX, int gridY, int turretEntityId) {
super(gridX, gridY);
this.turretEntityId = turretEntityId;
}
@Override
public boolean isDestructible() {
return true;
}
@Override
public boolean isDestroyed() {
return false;
}
@Override
public float getHealth() {
return Float.MAX_VALUE;
}
@Override
public void takeDamage(float damage) {
// 空操作实际HP由 ECS Health 组件管理
}
@Override
public float getMovementCost() {
return 1.0f + MOVEMENT_COST_PENALTY;
}
}

View File

@@ -52,16 +52,40 @@ public class BulletMovementSystem implements System {
float z = 0.5f + 4.0f * (float) Math.sin(progress * Math.PI);
if (data.getFlightTime() >= data.getMaxFlightTime() || (z <= 0.5f && progress > 0.5f)) {
// 落地
// 落地 —— 触发爆炸伤害
Explosive explosive = world.getExplosives().get(entityId);
if (explosive != null) {
float radius = explosive.getExplosionRadius();
int damage = explosive.getExplosionDamage();
// 对范围内僵尸造成爆炸伤害
for (int zombieId : world.getZombies()) {
Health zombieHealth = world.getHealths().get(zombieId);
Position zombiePos = world.getPositions().get(zombieId);
if (zombieHealth == null || !zombieHealth.isAlive() || zombiePos == null) continue;
float dist = pos.distanceTo(zombiePos.getX(), zombiePos.getY());
if (dist < radius) {
zombieHealth.takeDamage(damage);
}
}
// 记录爆炸效果(同步给客户端渲染)
Map<String, Object> explosion = new HashMap<>();
explosion.put("x", pos.getX());
explosion.put("y", pos.getY());
explosion.put("radius", radius);
explosion.put("type", data.isMolotov() ? "molotov" : "grenade");
world.getExplosions().add(explosion);
}
// 燃烧瓶落地后创建火焰区域
if (data.isMolotov()) {
// 燃烧瓶落地后创建火焰区域
FireZone fz = world.getFireZonesData().get(entityId);
if (fz != null) {
world.createFireZoneEntity(pos.getX(), pos.getY(),
fz.getRadius(), fz.getDamagePerTick(), fz.getDuration(), fz.getOwnerId());
}
}
// 触发爆炸(由 CollisionSystem 中的伤害处理)
toRemove.add(entityId);
continue;
}

View File

@@ -51,36 +51,33 @@ public class CollisionSystem implements System {
}
}
// 检测与坚果墙的碰撞
// 检测与机枪塔/坚果墙的碰撞
if (!hit) {
int gx = (int) Math.floor(bulletPos.getX());
int gy = (int) Math.floor(bulletPos.getY());
Wall wall = world.getMap().getWall(gx, gy);
if (wall instanceof NutWall && !wall.isDestroyed()) {
wall.takeDamage(data.getDamage());
hit = true;
if (wall.isDestroyed()) {
world.getMap().removeWall(gx, gy);
}
}
}
// 检测与机枪塔的碰撞
if (!hit) {
for (int turretId : new ArrayList<>(world.getTurrets())) {
WallEntity we = world.getWallEntityDatas().get(turretId);
Health turretHealth = world.getHealths().get(turretId);
Position turretPos = world.getPositions().get(turretId);
if (we == null || turretHealth == null || !turretHealth.isAlive() || turretPos == null) continue;
if (hitsEntity(bulletPos, turretPos, ZOMBIE_SIZE)) {
// 优先检测 turret 实体auto_turret 走实体血量)
int hitTurretId = findTurretAtGrid(world, gx, gy, bulletPos);
if (hitTurretId >= 0) {
if (!isOwnTurretBullet(data.getOwnerId(), hitTurretId)) {
Health turretHealth = world.getHealths().get(hitTurretId);
WallEntity we = world.getWallEntityDatas().get(hitTurretId);
turretHealth.takeDamage(data.getDamage());
hit = true;
if (!turretHealth.isAlive()) {
world.getMap().removeWall(we.getGridX(), we.getGridY());
world.destroyEntity(turretId);
world.destroyEntity(hitTurretId);
}
}
} else {
// 坚果墙碰撞500HP不会被一枪打掉
Wall wall = world.getMap().getWall(gx, gy);
if (wall instanceof NutWall && !wall.isDestroyed()) {
wall.takeDamage(data.getDamage());
hit = true;
if (wall.isDestroyed()) {
world.getMap().removeWall(gx, gy);
}
break;
}
}
}
@@ -123,36 +120,29 @@ public class CollisionSystem implements System {
}
}
// 检测与坚果墙的碰撞
// 检测与机枪塔/坚果墙的碰撞
if (!hit) {
int gx = (int) Math.floor(bulletPos.getX());
int gy = (int) Math.floor(bulletPos.getY());
Wall wall = world.getMap().getWall(gx, gy);
if (wall instanceof NutWall && !wall.isDestroyed()) {
wall.takeDamage(data.getDamage());
int hitTurretId = findTurretAtGrid(world, gx, gy, bulletPos);
if (hitTurretId >= 0) {
Health turretHealth = world.getHealths().get(hitTurretId);
WallEntity we = world.getWallEntityDatas().get(hitTurretId);
turretHealth.takeDamage(data.getDamage());
hit = true;
if (wall.isDestroyed()) {
world.getMap().removeWall(gx, gy);
if (!turretHealth.isAlive()) {
world.getMap().removeWall(we.getGridX(), we.getGridY());
world.destroyEntity(hitTurretId);
}
}
}
// 检测与机枪塔的碰撞
if (!hit) {
for (int turretId : new ArrayList<>(world.getTurrets())) {
WallEntity we = world.getWallEntityDatas().get(turretId);
Health turretHealth = world.getHealths().get(turretId);
Position turretPos = world.getPositions().get(turretId);
if (we == null || turretHealth == null || !turretHealth.isAlive() || turretPos == null) continue;
if (hitsEntity(bulletPos, turretPos, ZOMBIE_SIZE)) {
turretHealth.takeDamage(data.getDamage());
} else {
Wall wall = world.getMap().getWall(gx, gy);
if (wall instanceof NutWall && !wall.isDestroyed()) {
wall.takeDamage(data.getDamage());
hit = true;
if (!turretHealth.isAlive()) {
world.getMap().removeWall(we.getGridX(), we.getGridY());
world.destroyEntity(turretId);
if (wall.isDestroyed()) {
world.getMap().removeWall(gx, gy);
}
break;
}
}
}
@@ -169,6 +159,36 @@ public class CollisionSystem implements System {
}
}
/**
* 在指定网格查找活着的 turret 实体
*/
private int findTurretAtGrid(ECSWorld world, int gx, int gy, Position bulletPos) {
for (int turretId : world.getTurrets()) {
WallEntity we = world.getWallEntityDatas().get(turretId);
Health health = world.getHealths().get(turretId);
Position pos = world.getPositions().get(turretId);
if (we == null || health == null || !health.isAlive() || pos == null) continue;
if (we.getGridX() == gx && we.getGridY() == gy && hitsEntity(bulletPos, pos, ZOMBIE_SIZE)) {
return turretId;
}
}
return -1;
}
/**
* 判断子弹是否由该 turret 自己发射(避免自伤)
* ownerId 格式: "turret_{entityId}"
*/
private boolean isOwnTurretBullet(String ownerId, int turretId) {
if (ownerId == null) return false;
try {
if (ownerId.startsWith("turret_")) {
return Integer.parseInt(ownerId.substring(7)) == turretId;
}
} catch (NumberFormatException ignored) {}
return false;
}
/**
* 判断子弹是否命中实体(基于距离检测)
*/

View File

@@ -33,6 +33,8 @@ public class DamageSystem implements System {
if (dist < 1.0f && now - ai.getLastAttackTime() >= template.getAttackRate() * 1000) {
playerHealth.takeDamage(template.getDamage());
ai.setLastAttackTime(now);
ai.setAttacking(true);
ai.setAttackAnimTimer(0.5f);
// 玩家死亡时启动重生计时器
if (!playerHealth.isAlive()) {
@@ -87,6 +89,8 @@ public class DamageSystem implements System {
template.getRangedDamage(), "zombie_" + zombieId, -1, 15, true);
ai.setLastRangedAttackTime(now);
ai.setAttacking(true);
ai.setAttackAnimTimer(0.5f);
}
}
}

View File

@@ -58,6 +58,20 @@ public class StateSyncSystem implements System {
}
state.put("loots", lootStates);
// 燃烧区域状态
List<Map<String, Object>> fireZoneStates = new ArrayList<>();
for (int entityId : world.getFireZones()) {
fireZoneStates.add(buildFireZoneState(world, entityId));
}
state.put("fireZones", fireZoneStates);
// 机枪塔状态
List<Map<String, Object>> turretStates = new ArrayList<>();
for (int entityId : world.getTurrets()) {
turretStates.add(buildTurretState(world, entityId));
}
state.put("turrets", turretStates);
// 爆炸效果、移除子弹、坚果墙状态、游戏信息
state.put("explosions", new ArrayList<>(world.getExplosions()));
state.put("removedBullets", new ArrayList<>(world.getRemovedBullets()));
@@ -142,6 +156,7 @@ public class StateSyncSystem implements System {
var template = world.getZombieTemplate(ai.getTemplateId());
map.put("isElite", template.isCanRangedAttack());
map.put("isSplitter", template.isCanSplit());
map.put("isAttacking", ai.isAttacking());
}
return map;
}
@@ -197,6 +212,52 @@ public class StateSyncSystem implements System {
return map;
}
/** 构建单个燃烧区域的状态数据 */
private Map<String, Object> buildFireZoneState(ECSWorld world, int entityId) {
Map<String, Object> map = new LinkedHashMap<>();
Position pos = world.getPositions().get(entityId);
FireZone fz = world.getFireZonesData().get(entityId);
map.put("id", entityId);
if (pos != null) {
map.put("x", pos.getX());
map.put("y", pos.getY());
}
if (fz != null) {
map.put("radius", fz.getRadius());
map.put("duration", fz.getDuration());
map.put("elapsed", fz.getElapsed());
}
return map;
}
/** 构建单个机枪塔的状态数据 */
private Map<String, Object> buildTurretState(ECSWorld world, int entityId) {
Map<String, Object> map = new LinkedHashMap<>();
Position pos = world.getPositions().get(entityId);
Health health = world.getHealths().get(entityId);
WallEntity we = world.getWallEntityDatas().get(entityId);
TurretState ts = world.getTurretStates().get(entityId);
map.put("id", entityId);
if (pos != null) {
map.put("x", pos.getX());
map.put("y", pos.getY());
}
if (health != null) {
map.put("health", health.getHealth());
map.put("maxHealth", health.getMaxHealth());
}
if (we != null) {
map.put("gridX", we.getGridX());
map.put("gridY", we.getGridY());
}
if (ts != null) {
map.put("isAutoTurret", ts.getFireRange() > 0);
}
return map;
}
@Override
public void update(float dt, ECSWorld world) {
// 空实现:本系统通过 buildGameState() 显式调用

View File

@@ -118,12 +118,14 @@ public class WeaponFiringSystem implements System {
if ("molotov".equals(currentWeapon.getId())) {
world.createMolotovEntity(startX, startY, targetX, targetY, flightDuration,
currentWeapon.getDamage(), playerId, currentWeapon.getExplosionRadius(),
currentWeapon.getDamage(), playerId, weapon.getWeaponIndex(),
currentWeapon.getExplosionRadius(),
currentWeapon.getFireZoneRadius(), currentWeapon.getFireZoneDamage(),
currentWeapon.getFireZoneDuration());
} else {
world.createGrenadeEntity(startX, startY, targetX, targetY, flightDuration,
currentWeapon.getDamage(), playerId, currentWeapon.getExplosionRadius());
currentWeapon.getDamage(), playerId, weapon.getWeaponIndex(),
currentWeapon.getExplosionRadius());
}
} else if (input.isFiring() && !weapon.isChargingGrenade()) {
weapon.startGrenadeCharge();
@@ -151,11 +153,8 @@ public class WeaponFiringSystem implements System {
if ("nut_wall".equals(currentWeapon.getId())) {
world.getMap().addNutWall(gridX, gridY);
// 创建纯墙体实体fireRange=0无机枪塔行为
world.createTurretEntity(gridX + 0.5f, gridY + 0.5f, gridX, gridY,
0, 0, 0, 0, 20);
} else if ("auto_turret".equals(currentWeapon.getId())) {
world.getMap().addNutWall(gridX, gridY);
// createTurretEntity 内部会注册 TurretWall 到流场
world.createTurretEntity(gridX + 0.5f, gridY + 0.5f, gridX, gridY,
currentWeapon.getTurretRange(), currentWeapon.getTurretFireRate(),
currentWeapon.getTurretDamage(), currentWeapon.getTurretBulletSpeed(),

View File

@@ -45,25 +45,97 @@ public class ZombieMovementSystem implements System {
float wallCenterY = ai.getAttackingWallGridY() + 0.5f;
float distToWall = pos.distanceTo(wallCenterX, wallCenterY);
if (distToWall < 0.8f) {
Wall wall = map.getWall(ai.getAttackingWallGridX(), ai.getAttackingWallGridY());
if (wall != null && !wall.isDestroyed()) {
// 攻击墙体
if (now - ai.getLastAttackTime() >= template.getAttackRate() * 1000) {
wall.takeDamage(1.0f);
ai.setLastAttackTime(now);
if (wall.isDestroyed()) {
map.removeWall(wall.getGridX(), wall.getGridY());
if (distToWall < 1.5f) {
// 每次攻击前检查流场是否有更快的路
float[] flowDir = map.getFlowDirection(pos.getX(), pos.getY());
int cx = (int) Math.floor(pos.getX());
int cy = (int) Math.floor(pos.getY());
int flowNextX = cx + Math.round(flowDir[0]);
int flowNextY = cy + Math.round(flowDir[1]);
boolean flowPointsToWall = (flowNextX == ai.getAttackingWallGridX()
&& flowNextY == ai.getAttackingWallGridY());
if (flowPointsToWall) {
// 流场仍指向这面墙,继续攻击
Wall wall = map.getWall(ai.getAttackingWallGridX(), ai.getAttackingWallGridY());
if (wall != null && !wall.isDestroyed()) {
if (now - ai.getLastAttackTime() >= template.getAttackRate() * 1000) {
int turretId = world.getTurretAtGrid(ai.getAttackingWallGridX(), ai.getAttackingWallGridY());
if (turretId >= 0) {
Health turretHealth = world.getHealths().get(turretId);
if (turretHealth != null) {
turretHealth.takeDamage(template.getDamage());
if (!turretHealth.isAlive()) {
world.destroyEntity(turretId);
}
}
} else {
wall.takeDamage(template.getDamage());
if (wall.isDestroyed()) {
map.removeWall(wall.getGridX(), wall.getGridY());
}
}
ai.setLastAttackTime(now);
ai.setAttackAnimTimer(0.5f);
}
ai.setAttacking(true);
continue;
}
continue;
// 墙已被摧毁,清除攻击状态
ai.setAttackingWall(false);
ai.setAttackingWallGridX(-1);
ai.setAttackingWallGridY(-1);
} else {
// 流场不再指向这面墙,放弃攻击走更快的路
ai.setAttackingWall(false);
ai.setAttackingWallGridX(-1);
ai.setAttackingWallGridY(-1);
}
} else {
// 距离太远,清除攻击状态
ai.setAttackingWall(false);
ai.setAttackingWallGridX(-1);
ai.setAttackingWallGridY(-1);
}
}
// 处理机枪塔攻击状态
if (ai.getAttackingTurretId() >= 0) {
Health turretHealth = world.getHealths().get(ai.getAttackingTurretId());
Position turretPos = world.getPositions().get(ai.getAttackingTurretId());
if (turretHealth != null && turretHealth.isAlive() && turretPos != null) {
float distToTurret = pos.distanceTo(turretPos.getX(), turretPos.getY());
if (distToTurret < 1.5f) {
// 检查流场是否有更快的路
float[] flowDir = map.getFlowDirection(pos.getX(), pos.getY());
int cx = (int) Math.floor(pos.getX());
int cy = (int) Math.floor(pos.getY());
int flowNextX = cx + Math.round(flowDir[0]);
int flowNextY = cy + Math.round(flowDir[1]);
int turretGridX = (int) Math.floor(turretPos.getX());
int turretGridY = (int) Math.floor(turretPos.getY());
boolean flowPointsToTurret = (flowNextX == turretGridX && flowNextY == turretGridY);
if (flowPointsToTurret) {
if (now - ai.getLastAttackTime() >= template.getAttackRate() * 1000) {
turretHealth.takeDamage(template.getDamage());
ai.setLastAttackTime(now);
ai.setAttackAnimTimer(0.5f);
if (!turretHealth.isAlive()) {
world.destroyEntity(ai.getAttackingTurretId());
ai.setAttackingTurretId(-1);
}
}
ai.setAttacking(true);
continue;
}
// 流场不再指向这个机枪塔,放弃攻击
ai.setAttackingTurretId(-1);
}
}
ai.setAttackingTurretId(-1);
}
// 目标选择
float centerDist = Float.MAX_VALUE;
if (ai.isHasTarget()) {
@@ -94,12 +166,21 @@ public class ZombieMovementSystem implements System {
ai.setAttackingWall(true);
ai.setAttackingWallGridX(nextGridX);
ai.setAttackingWallGridY(nextGridY);
ai.setReservedGridX(nextGridX);
ai.setReservedGridY(nextGridY);
ai.setReservation(true);
ai.setTargetX(nextGridX + 0.5f);
ai.setTargetY(nextGridY + 0.5f);
ai.setHasTarget(true);
// 攻击时不占位,让其他僵尸可以穿过当前位置
ai.setReservation(false);
ai.setHasTarget(false);
ai.setAttacking(true);
ai.setAttackAnimTimer(0.5f);
continue;
} else if (world.getTurretAtGrid(nextGridX, nextGridY) >= 0) {
// 下一格有机枪塔,进入攻击模式
int turretId = world.getTurretAtGrid(nextGridX, nextGridY);
ai.setAttackingTurretId(turretId);
ai.setReservation(false);
ai.setHasTarget(false);
ai.setAttacking(true);
ai.setAttackAnimTimer(0.5f);
continue;
} else if (map.isWall(nextGridX, nextGridY)) {
nextGridX = currentGridX + (int) Math.signum(dirX);
nextGridY = currentGridY + (int) Math.signum(dirY);
@@ -213,6 +294,15 @@ public class ZombieMovementSystem implements System {
if (canMoveY) pos.setY(newY);
}
// 攻击动画计时器递减,保持动画持续播放
if (ai.getAttackAnimTimer() > 0) {
ai.setAttackAnimTimer(ai.getAttackAnimTimer() - dt);
ai.setAttacking(true);
} else {
ai.setAttacking(false);
ai.setAttackAnimTimer(0);
}
// 僵尸间分离(防止重叠)
for (int otherId : world.getZombies()) {
if (otherId == entityId) continue;

View File

@@ -102,7 +102,7 @@
"spread": 0,
"bulletSpeed": 12,
"range": 12,
"maxAmmo": 5,
"maxAmmo": 10,
"chargeable": true,
"explosive": true,
"explosionRadius": 2.0,
@@ -125,14 +125,14 @@
"spread": 0,
"bulletSpeed": 0,
"range": 0,
"maxAmmo": 3,
"maxAmmo": 10,
"chargeable": false,
"explosive": false,
"explosionRadius": 0,
"fireZoneRadius": 0,
"fireZoneDamage": 0,
"fireZoneDuration": 0,
"turretHealth": 20,
"turretHealth": 500,
"turretRange": 0,
"turretFireRate": 0,
"turretDamage": 0,
@@ -148,7 +148,7 @@
"spread": 0,
"bulletSpeed": 0,
"range": 0,
"maxAmmo": 2,
"maxAmmo": 5,
"chargeable": false,
"explosive": false,
"explosionRadius": 0,