## EnemyManager — 敌人管理器 Autoload(GDScript 接口层) ## 热路径由 C# EnemyManagerCs 子节点驱动。 ## 权威来源:implementation_plan.md §2.3.C ## ## SoA 布局(ENEMY_STRIDE=8): ## [ x, y, vx, vy, hp, hp_max, enemy_type, state ] ## 0 1 2 3 4 5 6 7 extends Node const ENEMY_STRIDE: int = 8 # 禁止裸整数 8(P6-N54) const MAX_ENEMIES: int = 1024 const OFFSCREEN_SEPARATION_INTERVAL: int = 10 # 屏外低频 Boid 分离力更新频率(P6-N1) # SoA 热数组 var _data: PackedFloat32Array = PackedFloat32Array() var _active_count: int = 0 # entity_id ↔ SoA 槽位 双向映射 var _entity_index_map: Dictionary = {} # { entity_id → slot_idx } var _slot_entity_map: Dictionary = {} # { slot_idx → entity_id } var _next_entity_id: int = 1 # LOD:屏内可见标志(PackedByteArray)0=屏外,1=屏内 var _visible_flags: PackedByteArray = PackedByteArray() var _offscreen_sep_counter: int = 0 var _external_movers: Dictionary = {} # entity_id → true:移动由外部系统(BossManager)接管,_gd_update_movement 跳过 var _cs_node: Node = null ## ADR-A4:精英寻路。W1–14 杂鱼永远 Boid;W15+ Elite/Boss 按需挂 NavigationAgent2D, ## 并发寻路上限 MAX_PATHFINDING_ENEMIES=20,超出降级 Boid(P-S5-AI-01)。 const MAX_PATHFINDING_ENEMIES: int = 20 const ELITE_SPEED: float = 90.0 ## 敌人原型(enemy_type 存于 SoA[+6])。S6 内容:基础/快速/护甲/精英/Boss enum Type { BASIC = 0, FAST = 1, ARMORED = 2, ELITE = 3, MINIBOSS = 4, BOSS = 5 } ## 各原型数值:纯数据驱动,唯一来源 data/enemies.json(游戏设计器「敌人」面板维护) ## get() 的单值默认仅为防崩溃,非内容副本 var _SPEED: Dictionary = {} # type → 移动速度 var _ARMOR: Dictionary = {} # type → 护甲 var _RENDER_SIZE: Dictionary = {} # type → 渲染直径 var _RENDER_COLOR: Dictionary = {} # type → 渲染颜色 var _RESIST: Dictionary = {} # type → PackedFloat32Array(6),index=damage_type,[0]物理恒0 const _RESIST_ELEMENTS := {"fire": 1, "ice": 2, "lightning": 3, "poison": 4, "arcane": 5} const ARCHETYPE_JSON: String = "res://data/enemies.json" ## 数据驱动:从 data/enemies.json 加载原型数值(速度/护甲/尺寸/颜色) func _load_json_archetypes() -> void: if not FileAccess.file_exists(ARCHETYPE_JSON): push_error("EnemyManager: 缺失 res://data/enemies.json(敌人原型数值)") return var data = JSON.parse_string(FileAccess.get_file_as_string(ARCHETYPE_JSON)) if not (data is Dictionary): push_error("EnemyManager: enemies.json 格式错误") return for k in data: var t: int = int(k) var d: Dictionary = data[k] _SPEED[t] = float(d.get("speed", 80.0)) _ARMOR[t] = float(d.get("armor", 0.0)) _RENDER_SIZE[t] = float(d.get("size", 14.0)) var c: Array = d.get("color", [0.9, 0.3, 0.3]) _RENDER_COLOR[t] = Color(float(c[0]), float(c[1]), float(c[2])) var rf := PackedFloat32Array([0, 0, 0, 0, 0, 0]) var res_d: Dictionary = d.get("resistances", {}) for ename in _RESIST_ELEMENTS: if res_d.has(ename): rf[_RESIST_ELEMENTS[ename]] = clampf(float(res_d[ename]), -0.9, 0.9) _RESIST[t] = rf var _nav_region: NavigationRegion2D = null # 程序化矩形导航区 var _pathfinders: Dictionary = {} # entity_id → {"agent":NavigationAgent2D, "host":Node2D} var _pathfinder_order: Array = [] # FIFO,用于超上限淘汰 func _ready() -> void: _load_json_archetypes() # 数据驱动:覆盖原型数值(游戏设计器面板维护) _data.resize(MAX_ENEMIES * ENEMY_STRIDE) _data.fill(0.0) _visible_flags.resize(MAX_ENEMIES) _visible_flags.fill(0) func _physics_process(delta: float) -> void: _offscreen_sep_counter += 1 var do_sep: bool = (_offscreen_sep_counter >= OFFSCREEN_SEPARATION_INTERVAL) if do_sep: _offscreen_sep_counter = 0 if not _cs_node: _gd_update_movement(delta) # ADR-A4:精英寻路移动(NavigationAgent2D 跟随),仅当存在 pathfinder 时执行 if not _pathfinders.is_empty(): _update_pathfinding_movement(delta) # S1: 移动后更新 SpatialGrid(碰撞检测依赖) _update_spatial_grid() func _update_spatial_grid() -> void: for i in _active_count: var base: int = i * ENEMY_STRIDE var eid: int = _slot_entity_map.get(i, -1) if eid >= 0: SpatialGrid.insert(eid, Vector2(_data[base + 0], _data[base + 1])) # GDScript 回退:直线追玩家(C# 没就绪时使用) func _gd_update_movement(delta: float) -> void: var player_pos: Vector2 = PlayerManager.get_position() var has_pf: bool = not _pathfinders.is_empty() # 无精英时零额外开销 var has_ext: bool = not _external_movers.is_empty() for i in _active_count: var eid_i: int = _slot_entity_map.get(i, -1) if has_pf and _pathfinders.has(eid_i): continue # 寻路精英由 _update_pathfinding_movement 处理 if has_ext and _external_movers.has(eid_i): continue # Boss 等外部驱动实体由 BossManager 移动 var base: int = i * ENEMY_STRIDE var ex: float = _data[base + 0] var ey: float = _data[base + 1] var spd: float = _SPEED.get(int(_data[base + 6]), 80.0) # 按原型查速度 var dx: float = player_pos.x - ex var dy: float = player_pos.y - ey var dist: float = sqrt(dx * dx + dy * dy) if dist > 1.0: _data[base + 2] = (dx / dist) * spd _data[base + 3] = (dy / dist) * spd _data[base + 0] += _data[base + 2] * delta _data[base + 1] += _data[base + 3] * delta # ── 对外接口 ─────────────────────────────────────────────────── func spawn_enemy(pos: Vector2, hp: float, enemy_type: int = 0, has_pathfinding: bool = false) -> int: if _active_count >= MAX_ENEMIES: return -1 var entity_id: int = _next_entity_id _next_entity_id += 1 var slot: int = _active_count var base: int = slot * ENEMY_STRIDE _data[base + 0] = pos.x _data[base + 1] = pos.y _data[base + 2] = 0.0 _data[base + 3] = 0.0 _data[base + 4] = hp # hp _data[base + 5] = hp # hp_max _data[base + 6] = float(enemy_type) _data[base + 7] = 0.0 # state: 0=alive _entity_index_map[entity_id] = slot _slot_entity_map[slot] = entity_id _active_count += 1 # ADR-A4:W15+ Elite 在并发上限内挂寻路(超限降级 Boid) if has_pathfinding and _pathfinders.size() < MAX_PATHFINDING_ENEMIES: _register_pathfinder(entity_id, pos) return entity_id # ── ADR-A4 精英寻路子系统 ────────────────────────────────────── func _ensure_nav_region() -> void: if _nav_region != null: return _nav_region = NavigationRegion2D.new() var poly := NavigationPolygon.new() # 覆盖竞技场的单个凸矩形导航多边形(无障碍占位;P-S5-AI-01 仅测寻路开销) var ext: float = 4000.0 poly.vertices = PackedVector2Array([ Vector2(-ext, -ext), Vector2(-ext, ext), Vector2(ext, ext), Vector2(ext, -ext), ]) poly.add_polygon(PackedInt32Array([0, 1, 2, 3])) _nav_region.navigation_polygon = poly add_child(_nav_region) func _register_pathfinder(entity_id: int, pos: Vector2) -> void: _ensure_nav_region() var host := Node2D.new() host.position = pos add_child(host) var agent := NavigationAgent2D.new() agent.path_desired_distance = 8.0 agent.target_desired_distance = 8.0 agent.avoidance_enabled = false # 与 SoA Boid 分离力共存,避免双重避障 host.add_child(agent) _pathfinders[entity_id] = {"agent": agent, "host": host} _pathfinder_order.append(entity_id) func _unregister_pathfinder(entity_id: int) -> void: var pf: Dictionary = _pathfinders.get(entity_id, {}) if pf.is_empty(): return (pf["host"] as Node).queue_free() # agent 是 host 子节点,随之释放 _pathfinders.erase(entity_id) _pathfinder_order.erase(entity_id) ## 寻路精英移动:host 同步到 SoA 坐标,agent 朝玩家求下一路径点并转向 func _update_pathfinding_movement(delta: float) -> void: var player_pos: Vector2 = PlayerManager.get_position() for eid in _pathfinders: var slot: int = _entity_index_map.get(eid, -1) if slot < 0: continue var base: int = slot * ENEMY_STRIDE var pos: Vector2 = Vector2(_data[base + 0], _data[base + 1]) var pf: Dictionary = _pathfinders[eid] var agent: NavigationAgent2D = pf["agent"] (pf["host"] as Node2D).global_position = pos agent.target_position = player_pos var nxt: Vector2 = agent.get_next_path_position() var dir: Vector2 = nxt - pos if dir.length() > 1.0: dir = dir.normalized() _data[base + 2] = dir.x * ELITE_SPEED _data[base + 3] = dir.y * ELITE_SPEED _data[base + 0] += _data[base + 2] * delta _data[base + 1] += _data[base + 3] * delta func get_pathfinder_count() -> int: return _pathfinders.size() func despawn_enemy(entity_id: int) -> void: var slot: int = _entity_index_map.get(entity_id, -1) if slot < 0: return StatusManager.remove_all_for_entity(entity_id) SpatialGrid.remove_entity(entity_id) if _pathfinders.has(entity_id): _unregister_pathfinder(entity_id) var last: int = _active_count - 1 if slot != last: var base_slot: int = slot * ENEMY_STRIDE var base_last: int = last * ENEMY_STRIDE for s in ENEMY_STRIDE: _data[base_slot + s] = _data[base_last + s] var moved_id: int = _slot_entity_map[last] _entity_index_map[moved_id] = slot _slot_entity_map[slot] = moved_id _entity_index_map.erase(entity_id) _slot_entity_map.erase(last) _active_count -= 1 ## 获取指定 entity_id 的世界坐标(找不到返回 Vector2(-9999,-9999)) func get_pos_by_id(entity_id: int) -> Vector2: var slot: int = _entity_index_map.get(entity_id, -1) if slot < 0: return Vector2(-9999.0, -9999.0) var base: int = slot * ENEMY_STRIDE return Vector2(_data[base + 0], _data[base + 1]) ## 外部系统(BossManager)接管移动:注册后 _gd_update_movement 跳过该实体 func register_external_mover(entity_id: int) -> void: _external_movers[entity_id] = true func unregister_external_mover(entity_id: int) -> void: _external_movers.erase(entity_id) ## 外部系统写回实体坐标(BossManager 驱动移动/冲刺用) func set_entity_pos(entity_id: int, pos: Vector2) -> void: var slot: int = _entity_index_map.get(entity_id, -1) if slot < 0: return var base: int = slot * ENEMY_STRIDE _data[base + 0] = pos.x _data[base + 1] = pos.y ## 元素抗性查表:越界/缺失回退 0(物理 index 0 恒 0) func _resist_for(etype: int, dtype: int) -> float: var rf: PackedFloat32Array = _RESIST.get(etype, PackedFloat32Array()) if dtype < 0 or dtype >= rf.size(): return 0.0 return rf[dtype] ## S1 完整伤害接口:从 DamageContextPool 取 context,计算公式后扣 HP ## 公式(S1 简化):final_dmg = base_damage * mult ## S4 起补充 resistance / armor(参见 combat_mechanics_depth.md §4) func apply_damage_from_context(entity_id: int, ctx_id: int) -> bool: var ctx: DamageContext = DamageContextPool.get_context(ctx_id) if ctx == null: return false var slot: int = _entity_index_map.get(entity_id, -1) var res: float = 0.0 if slot >= 0: var etype: int = int(_data[slot * ENEMY_STRIDE + 6]) res = _resist_for(etype, ctx.damage_type) var final_dmg: float = ctx.calc_damage(0.0, res) return apply_damage(entity_id, final_dmg, ctx.owner_id, true) ## 直接扣血接口;source_id 用于击杀归属,record_dps 控制是否计入 DPS func apply_damage(entity_id: int, damage: float, source_id: int = -1, record_dps: bool = true) -> bool: var slot: int = _entity_index_map.get(entity_id, -1) if slot < 0: return false var combo_stacks: int = StatusManager.get_stacks(entity_id, StatusID.COMBO_MARK) var combo_bonus: float = 1.0 + float(combo_stacks) * 0.02 # 每层 +2% 伤害 var base: int = slot * ENEMY_STRIDE var armor: float = _ARMOR.get(int(_data[base + 6]), 0.0) # 原型护甲,扣减后保底 1 var final_dmg: float = maxf(1.0, damage * combo_bonus - armor) _data[base + 4] -= final_dmg if record_dps: DpsTracker.record_damage(final_dmg) if _data[base + 4] <= 0.0: StatusManager.remove_all_for_entity(entity_id) EventBus.emit(EventID.ENEMY_KILLED, {"entity_id": entity_id, "killer_id": source_id}) despawn_enemy(entity_id) return true return false func has_entity(entity_id: int) -> bool: return _entity_index_map.has(entity_id) ## 返回敌人血量百分比(权威来源:arch §5.2 get_hp_percent) func get_hp_percent(entity_id: int) -> float: var slot: int = _entity_index_map.get(entity_id, -1) if slot < 0: return 0.0 var base: int = slot * ENEMY_STRIDE var hp: float = _data[base + 4] # slot +4: hp var hp_max: float = _data[base + 5] # slot +5: hp_max return hp / max(hp_max, 0.001) func get_active_count() -> int: return _active_count ## 渲染:活跃敌人 SoA 位置 → MultiMesh(按原型设尺寸/颜色,Boss 大且醒目) func sync_multimesh(mm: MultiMesh) -> void: if mm == null: return var n: int = min(_active_count, mm.instance_count) mm.visible_instance_count = n for i in n: var base: int = i * ENEMY_STRIDE var etype: int = int(_data[base + 6]) var sz: float = _RENDER_SIZE.get(etype, 14.0) mm.set_instance_transform_2d(i, Transform2D( 0.0, Vector2(sz, sz), 0.0, Vector2(_data[base + 0], _data[base + 1]))) mm.set_instance_color(i, _RENDER_COLOR.get(etype, Color(0.9, 0.3, 0.3))) ## 返回最近敌人位置 func get_nearest_pos(origin: Vector2, max_dist: float = 9999.0) -> Vector2: var best_sq: float = max_dist * max_dist var best: Vector2 = origin for i in _active_count: var base: int = i * ENEMY_STRIDE var dx: float = _data[base + 0] - origin.x var dy: float = _data[base + 1] - origin.y var dsq: float = dx * dx + dy * dy if dsq < best_sq: best_sq = dsq best = Vector2(_data[base + 0], _data[base + 1]) return best ## 同步 Node2D 显示位置(屏内 dirty sync) func sync_node_positions(enemy_nodes: Array) -> void: for i in min(_active_count, enemy_nodes.size()): if _visible_flags[i] == int(1): var base: int = i * ENEMY_STRIDE enemy_nodes[i].position = Vector2(_data[base + 0], _data[base + 1]) func reset() -> void: _active_count = 0 _data.fill(0.0) _visible_flags.fill(0) _entity_index_map.clear() _slot_entity_map.clear() _external_movers.clear() _next_entity_id = 1 # ADR-A4:释放所有寻路精英节点 for eid in _pathfinders.keys(): (_pathfinders[eid]["host"] as Node).queue_free() _pathfinders.clear() _pathfinder_order.clear()