fixup
This commit is contained in:
@@ -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(); }
|
||||
|
||||
|
||||
41
backend/src/main/java/com/zombie/game/ecs/ObjectPool.java
Normal file
41
backend/src/main/java/com/zombie/game/ecs/ObjectPool.java
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除墙体(当坚果被破坏时调用)
|
||||
*
|
||||
|
||||
@@ -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;
|
||||
/** 当前血量 */
|
||||
|
||||
48
backend/src/main/java/com/zombie/game/model/TurretWall.java
Normal file
48
backend/src/main/java/com/zombie/game/model/TurretWall.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断子弹是否命中实体(基于距离检测)
|
||||
*/
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() 显式调用
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user