初次提交
This commit is contained in:
@@ -0,0 +1,329 @@
|
||||
## 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 _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 → 渲染颜色
|
||||
|
||||
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 _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() # 无精英时零额外开销
|
||||
for i in _active_count:
|
||||
if has_pf and _pathfinders.has(_slot_entity_map.get(i, -1)):
|
||||
continue # 寻路精英由 _update_pathfinding_movement 处理
|
||||
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])
|
||||
|
||||
## 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 final_dmg: float = ctx.calc_damage()
|
||||
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
|
||||
|
||||
## 建立敌人位置快照(BulletManager homing 共用)
|
||||
func fill_pos_snapshot(out_arr: PackedVector2Array) -> void:
|
||||
out_arr.resize(_active_count)
|
||||
for i in _active_count:
|
||||
var base: int = i * ENEMY_STRIDE
|
||||
out_arr[i] = Vector2(_data[base + 0], _data[base + 1])
|
||||
|
||||
## 同步 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()
|
||||
_next_entity_id = 1
|
||||
# ADR-A4:释放所有寻路精英节点
|
||||
for eid in _pathfinders.keys():
|
||||
(_pathfinders[eid]["host"] as Node).queue_free()
|
||||
_pathfinders.clear()
|
||||
_pathfinder_order.clear()
|
||||
Reference in New Issue
Block a user