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)) {
// 落地
if (data.isMolotov()) {
// 落地 —— 触发爆炸伤害
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,10 +51,26 @@ public class CollisionSystem implements System {
}
}
// 检测与坚果墙的碰撞
// 检测与机枪塔/坚果墙的碰撞
if (!hit) {
int gx = (int) Math.floor(bulletPos.getX());
int gy = (int) Math.floor(bulletPos.getY());
// 优先检测 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(hitTurretId);
}
}
} else {
// 坚果墙碰撞500HP不会被一枪打掉
Wall wall = world.getMap().getWall(gx, gy);
if (wall instanceof NutWall && !wall.isDestroyed()) {
wall.takeDamage(data.getDamage());
@@ -64,25 +80,6 @@ public class CollisionSystem implements System {
}
}
}
// 检测与机枪塔的碰撞
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());
hit = true;
if (!turretHealth.isAlive()) {
world.getMap().removeWall(we.getGridX(), we.getGridY());
world.destroyEntity(turretId);
}
break;
}
}
}
if (hit) {
@@ -123,10 +120,22 @@ public class CollisionSystem implements System {
}
}
// 检测与坚果墙的碰撞
// 检测与机枪塔/坚果墙的碰撞
if (!hit) {
int gx = (int) Math.floor(bulletPos.getX());
int gy = (int) Math.floor(bulletPos.getY());
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 (!turretHealth.isAlive()) {
world.getMap().removeWall(we.getGridX(), we.getGridY());
world.destroyEntity(hitTurretId);
}
} else {
Wall wall = world.getMap().getWall(gx, gy);
if (wall instanceof NutWall && !wall.isDestroyed()) {
wall.takeDamage(data.getDamage());
@@ -136,25 +145,6 @@ public class CollisionSystem implements System {
}
}
}
// 检测与机枪塔的碰撞
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());
hit = true;
if (!turretHealth.isAlive()) {
world.getMap().removeWall(we.getGridX(), we.getGridY());
world.destroyEntity(turretId);
}
break;
}
}
}
if (hit) {
@@ -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,23 +45,95 @@ public class ZombieMovementSystem implements System {
float wallCenterY = ai.getAttackingWallGridY() + 0.5f;
float distToWall = pos.distanceTo(wallCenterX, wallCenterY);
if (distToWall < 0.8f) {
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) {
wall.takeDamage(1.0f);
ai.setLastAttackTime(now);
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;
}
// 墙已被摧毁,清除攻击状态
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);
}
// 目标选择
@@ -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,

View File

@@ -34,6 +34,8 @@ export class GameEngine {
this.zombieBullets = new Map()
// 掉落物列表
this.loots = new Map()
// 机枪塔列表
this.turrets = new Map()
// 游戏运行状态
this.running = false
@@ -55,9 +57,9 @@ export class GameEngine {
[WEAPONS.MACHINE_GUN]: 100,
[WEAPONS.SHOTGUN]: 20,
[WEAPONS.GRENADE]: 10,
[WEAPONS.MOLOTOV]: 5,
[WEAPONS.NUT_WALL]: 3,
[WEAPONS.AUTO_TURRET]: 2
[WEAPONS.MOLOTOV]: 10,
[WEAPONS.NUT_WALL]: 10,
[WEAPONS.AUTO_TURRET]: 5
}
// 手雷蓄力相关
@@ -396,7 +398,7 @@ export class GameEngine {
zombie.y = zs.y
zombie.health = zs.health
const angle = zs.angle || 0
this.scene.updateZombie(zs.id, zs.x, zs.y, angle, zs.health)
this.scene.updateZombie(zs.id, zs.x, zs.y, angle, zs.health, zs.isAttacking || false)
// 受伤特效
if (prevHealth > zs.health && zs.health > 0) {
this.scene.addHitEffect(zs.x, zs.y)
@@ -465,13 +467,46 @@ export class GameEngine {
// 爆炸效果
if (state.explosions) {
console.log('Explosions received:', state.explosions)
for (const exp of state.explosions) {
console.log('Creating explosion at:', exp.x, exp.y, 'radius:', exp.radius)
this.scene.addExplosion(exp.x, exp.y, exp.radius || 3)
}
}
// 燃烧区域
if (state.fireZones) {
const serverIds = new Set(state.fireZones.map(f => f.id))
// 新增或更新
for (const fz of state.fireZones) {
this.scene.addFireZone(fz.id, fz.x, fz.y, fz.radius || 2, fz.duration || 5, fz.elapsed || 0)
}
// 移除已消失的
for (const [id] of this.scene.fireZones) {
if (!serverIds.has(id)) {
this.scene.removeFireZone(id)
}
}
}
// 机枪塔
if (state.turrets) {
const serverIds = new Set(state.turrets.map(t => t.id))
for (const ts of state.turrets) {
if (!this.turrets.has(ts.id)) {
this.turrets.set(ts.id, ts)
this.scene.addTurret(ts.id, ts.x, ts.y, ts.health, ts.maxHealth, ts.isAutoTurret)
} else {
this.turrets.set(ts.id, ts)
this.scene.updateTurret(ts.id, ts.health)
}
}
for (const [id] of this.turrets) {
if (!serverIds.has(id)) {
this.turrets.delete(id)
this.scene.removeTurret(id)
}
}
}
// 命中效果
if (state.hits) {
for (const hit of state.hits) {

View File

@@ -31,6 +31,8 @@ export class GameScene {
this.bullets = [] // 玩家子弹数组
this.zombieBullets = [] // 僵尸子弹数组
this.loots = new Map() // Map<lootId, {mesh, type}>
this.fireZones = new Map() // Map<fireZoneId, {mesh, light, startTime, duration}
this.turrets = new Map() // Map<turretId, {group, healthBar, healthBarBg, isAutoTurret}
this.effects = [] // 特效数组
this.wallMeshes = [] // 墙壁网格
this.nutWalls = new Map() // Map<key, {mesh, healthBar}>
@@ -413,18 +415,24 @@ export class GameScene {
head.castShadow = true
group.add(head)
// 手臂
const armGeo = new THREE.BoxGeometry(0.12, 0.6, 0.12)
// 手臂(比身体宽,确保从各个角度都可见)
const armGeo = new THREE.BoxGeometry(0.18, 0.7, 0.18)
const armMat = new THREE.MeshLambertMaterial({ color: armColor })
const leftArm = new THREE.Mesh(armGeo, armMat)
leftArm.position.set(-0.35, 0.5, 0.2)
leftArm.position.set(-0.5, 0.5, 0.0)
leftArm.rotation.x = -0.5
leftArm.castShadow = true
group.add(leftArm)
const rightArm = new THREE.Mesh(armGeo, armMat)
rightArm.position.set(0.35, 0.5, 0.2)
rightArm.position.set(0.5, 0.5, 0.0)
rightArm.rotation.x = -0.5
rightArm.castShadow = true
group.add(rightArm)
// 存储手臂引用供攻击动画使用
group.userData.leftArm = leftArm
group.userData.rightArm = rightArm
// 精英僵尸发光效果
if (isElite) {
const glowGeo = new THREE.SphereGeometry(0.6, 8, 8)
@@ -514,7 +522,12 @@ export class GameScene {
const model = this.createZombieModel(isElite, isSplitter)
model.position.set(x, 0, y)
this.scene.add(model)
this.zombies.set(id, { model, isElite, isSplitter })
this.zombies.set(id, {
model, isElite, isSplitter,
leftArm: model.userData.leftArm,
rightArm: model.userData.rightArm,
isAttacking: false
})
}
/**
@@ -534,19 +547,34 @@ export class GameScene {
}
/**
* 更新僵尸位置角度
* 更新僵尸位置角度和攻击动画
* @param {string} id 僵尸ID
* @param {number} x X坐标
* @param {number} y Y坐标
* @param {number} angle 旋转角度
* @param {number} health 生命值
* @param {boolean} isAttacking 是否正在攻击
*/
updateZombie(id, x, y, angle, health) {
updateZombie(id, x, y, angle, health, isAttacking = false) {
const zombie = this.zombies.get(id)
if (zombie) {
zombie.model.position.x = x
zombie.model.position.z = y
zombie.model.rotation.y = angle
// 攻击动画:手臂前后挥动
if (zombie.leftArm && zombie.rightArm) {
if (isAttacking) {
const t = Date.now() * 0.012
const swing = Math.sin(t) * 1.0
zombie.leftArm.rotation.x = -1.5 + swing
zombie.rightArm.rotation.x = -1.5 - swing
} else {
zombie.leftArm.rotation.x = -0.5
zombie.rightArm.rotation.x = -0.5
}
}
zombie.isAttacking = isAttacking
}
}
@@ -864,6 +892,164 @@ export class GameScene {
}
}
/**
* 添加火焰区域效果
* @param {number} id 火焰区域ID
* @param {number} x X坐标
* @param {number} y Y坐标
* @param {number} radius 火焰半径
* @param {number} duration 持续时间(秒)
* @param {number} elapsed 已经过时间(秒)
*/
addFireZone(id, x, y, radius, duration, elapsed) {
if (this.fireZones.has(id)) return
// 火焰地面圆
const geo = new THREE.CircleGeometry(radius, 24)
const mat = new THREE.MeshBasicMaterial({
color: 0xff4400,
transparent: true,
opacity: 0.5,
side: THREE.DoubleSide
})
const mesh = new THREE.Mesh(geo, mat)
mesh.rotation.x = -Math.PI / 2
mesh.position.set(x, 0.05, y)
this.scene.add(mesh)
// 火焰光源
const light = new THREE.PointLight(0xff6600, 2, radius * 3)
light.position.set(x, 1, y)
this.scene.add(light)
this.fireZones.set(id, { mesh, light, startTime: Date.now() - elapsed * 1000, duration: duration * 1000 })
}
/**
* 移除火焰区域效果
* @param {number} id 火焰区域ID
*/
removeFireZone(id) {
const fz = this.fireZones.get(id)
if (!fz) return
this.scene.remove(fz.mesh)
this.scene.remove(fz.light)
fz.mesh.geometry.dispose()
fz.mesh.material.dispose()
this.fireZones.delete(id)
}
/**
* 添加机枪塔渲染
* @param {number} id 实体ID
* @param {number} x X坐标
* @param {number} y Y坐标
* @param {number} health 当前生命值
* @param {number} maxHealth 最大生命值
* @param {boolean} isAutoTurret 是否为自动机枪塔false则为坚果墙塔基
*/
addTurret(id, x, y, health, maxHealth, isAutoTurret) {
if (this.turrets.has(id)) return
const group = new THREE.Group()
// 塔基(底座)
const baseGeo = new THREE.CylinderGeometry(0.35, 0.4, 0.3, 8)
const baseMat = new THREE.MeshLambertMaterial({ color: 0x555555 })
const base = new THREE.Mesh(baseGeo, baseMat)
base.position.y = 0.15
base.castShadow = true
group.add(base)
if (isAutoTurret) {
// 机身
const bodyGeo = new THREE.CylinderGeometry(0.2, 0.25, 0.4, 8)
const bodyMat = new THREE.MeshLambertMaterial({ color: 0x336633 })
const body = new THREE.Mesh(bodyGeo, bodyMat)
body.position.y = 0.5
body.castShadow = true
group.add(body)
// 枪管
const barrelGeo = new THREE.CylinderGeometry(0.05, 0.05, 0.5, 6)
const barrelMat = new THREE.MeshLambertMaterial({ color: 0x222222 })
const barrel = new THREE.Mesh(barrelGeo, barrelMat)
barrel.rotation.x = Math.PI / 2
barrel.position.set(0, 0.55, 0.25)
barrel.castShadow = true
group.add(barrel)
// 炮塔顶部
const topGeo = new THREE.SphereGeometry(0.15, 8, 6)
const topMat = new THREE.MeshLambertMaterial({ color: 0x446644 })
const top = new THREE.Mesh(topGeo, topMat)
top.position.y = 0.75
group.add(top)
} else {
// 坚果墙塔基 - 木箱外观
const boxGeo = new THREE.BoxGeometry(0.7, 0.8, 0.7)
const boxMat = new THREE.MeshLambertMaterial({ color: 0x8B4513 })
const box = new THREE.Mesh(boxGeo, boxMat)
box.position.y = 0.7
box.castShadow = true
group.add(box)
}
// 血条背景
const barBgGeo = new THREE.PlaneGeometry(0.8, 0.08)
const barBgMat = new THREE.MeshBasicMaterial({ color: 0x333333, side: THREE.DoubleSide })
const barBg = new THREE.Mesh(barBgGeo, barBgMat)
barBg.position.set(0, 1.3, 0)
barBg.rotation.x = -0.3
group.add(barBg)
// 血条前景
const barGeo = new THREE.PlaneGeometry(0.78, 0.06)
const barMat = new THREE.MeshBasicMaterial({ color: 0x00ff00, side: THREE.DoubleSide })
const healthBar = new THREE.Mesh(barGeo, barMat)
healthBar.position.set(0, 1.3, 0.01)
healthBar.rotation.x = -0.3
group.add(healthBar)
group.position.set(x, 0, y)
this.scene.add(group)
this.turrets.set(id, { group, healthBar, healthBarBg: barBg, isAutoTurret, maxHealth })
}
/**
* 更新机枪塔血量显示
*/
updateTurret(id, health) {
const turret = this.turrets.get(id)
if (!turret) return
const pct = Math.max(0, health / turret.maxHealth)
turret.healthBar.scale.x = pct
turret.healthBar.position.x = -(1 - pct) * 0.39
if (pct > 0.5) {
turret.healthBar.material.color.setHex(0x00ff00)
} else if (pct > 0.25) {
turret.healthBar.material.color.setHex(0xffff00)
} else {
turret.healthBar.material.color.setHex(0xff0000)
}
}
/**
* 移除机枪塔
*/
removeTurret(id) {
const turret = this.turrets.get(id)
if (!turret) return
this.scene.remove(turret.group)
turret.group.traverse(child => {
if (child.geometry) child.geometry.dispose()
if (child.material) child.material.dispose()
})
this.turrets.delete(id)
}
/**
* 添加枪口火焰效果
* @param {number} x X坐标
@@ -1084,6 +1270,19 @@ export class GameScene {
}
}
// 更新火焰区域闪烁效果
for (const [id, fz] of this.fireZones) {
const elapsed = (now - fz.startTime) / 1000
if (elapsed * 1000 >= fz.duration) {
this.removeFireZone(id)
} else {
// 火焰闪烁
const flicker = 0.3 + Math.sin(now * 0.01 + id) * 0.15 + Math.sin(now * 0.023) * 0.1
fz.mesh.material.opacity = flicker
fz.light.intensity = 1.5 + Math.sin(now * 0.015) * 0.5
}
}
// 更新子弹拖尾
for (const bullet of this.bullets) {
if (!bullet.isGrenade && !bullet.isMolotov && bullet.trail) {

View File

@@ -94,8 +94,8 @@ export const WEAPON_CONFIG = {
name: 'Molotov',
damage: 80,
fireRate: 2000,
ammo: 5,
maxAmmo: 5,
ammo: 10,
maxAmmo: 10,
speed: 12,
spread: 0,
pellets: 1,
@@ -110,8 +110,8 @@ export const WEAPON_CONFIG = {
name: 'Wall',
damage: 0,
fireRate: 1000,
ammo: 3,
maxAmmo: 3,
ammo: 10,
maxAmmo: 10,
speed: 0,
spread: 0,
pellets: 0,
@@ -124,8 +124,8 @@ export const WEAPON_CONFIG = {
name: 'Turret',
damage: 0,
fireRate: 2000,
ammo: 2,
maxAmmo: 2,
ammo: 5,
maxAmmo: 5,
speed: 0,
spread: 0,
pellets: 0,