## VFXManager — 视觉特效管理(Autoload: VFXManager) ## S4:程序化色块占位特效 + 对象池(P6-N72 one_shot=true) extends Node2D const MAX_ACTIVE_VFX: int = 200 var _vfx_pool: Dictionary = {} # { effect_id: Array[Node2D] } var _active_count: int = 0 func _ready() -> void: EventBus.subscribe(EventID.STATUS_APPLIED, _on_status_applied) func play(effect_id: String, world_pos: Vector2, override_scale: float = 1.0) -> void: if _active_count >= MAX_ACTIVE_VFX: return var node: Node2D = _pool_pop(effect_id) node.global_position = world_pos node.scale = Vector2.ONE * override_scale node.visible = true _active_count += 1 # 触发一次性粒子爆发 var p: GPUParticles2D = node.get_node_or_null("Particles") if p: p.restart() p.emitting = true var timer: Timer = node.get_node_or_null("AutoReturn") if timer: timer.start() func _pool_pop(effect_id: String) -> Node2D: if _vfx_pool.has(effect_id) and not _vfx_pool[effect_id].is_empty(): return _vfx_pool[effect_id].pop_back() return _instantiate_vfx(effect_id) func _pool_return(effect_id: String, node: Node2D) -> void: node.visible = false _active_count = max(0, _active_count - 1) if not _vfx_pool.has(effect_id): _vfx_pool[effect_id] = [] _vfx_pool[effect_id].append(node) func _instantiate_vfx(effect_id: String) -> Node2D: # 程序化美术占位:GPUParticles2D 爆裂粒子(放 scenes/vfx/.tscn 可整体替换) var node := Node2D.new() node.name = "VFX_" + effect_id var col: Color = _color_for(effect_id) var p := GPUParticles2D.new() p.name = "Particles" p.amount = _amount_for(effect_id) p.lifetime = 0.45 p.one_shot = true # P6-N72 p.explosiveness = 0.9 # 一次性爆发 p.local_coords = false var mat := ParticleProcessMaterial.new() mat.emission_shape = ParticleProcessMaterial.EMISSION_SHAPE_SPHERE mat.emission_sphere_radius = 4.0 mat.direction = Vector3(0, 0, 0) mat.spread = 180.0 mat.gravity = Vector3(0, 0, 0) mat.initial_velocity_min = _speed_for(effect_id) * 0.5 mat.initial_velocity_max = _speed_for(effect_id) mat.scale_min = 1.5 mat.scale_max = 4.0 # 颜色随生命淡出 var grad := Gradient.new() grad.set_color(0, Color(col.r, col.g, col.b, col.a)) grad.set_color(1, Color(col.r, col.g, col.b, 0.0)) var gtex := GradientTexture1D.new() gtex.gradient = grad mat.color_ramp = gtex p.process_material = mat node.add_child(p) # 自动回收(粒子播完) var timer := Timer.new() timer.name = "AutoReturn" timer.one_shot = true timer.wait_time = 0.55 timer.timeout.connect(_pool_return.bind(effect_id, node)) node.add_child(timer) add_child(node) node.visible = false return node ## 不同特效的粒子数 / 速度 func _amount_for(effect_id: String) -> int: match effect_id: "death_burst": return 24 "hit_spark": return 10 "cast_flash": return 8 _: return 12 func _speed_for(effect_id: String) -> float: match effect_id: "death_burst": return 220.0 "hit_spark": return 160.0 _: return 110.0 func _color_for(effect_id: String) -> Color: match effect_id: "hit_spark": return Color(1.0, 0.9, 0.3, 0.9) "status_burn": return Color(1.0, 0.4, 0.1, 0.8) "status_poison": return Color(0.3, 0.9, 0.2, 0.8) "cast_flash": return Color(0.6, 0.8, 1.0, 0.7) "death_burst": return Color(1.0, 0.2, 0.2, 0.8) _: return Color(1, 1, 1, 0.6) func _on_status_applied(payload: Dictionary) -> void: var status_type: int = int(payload.get("status_type", 0)) var typedef: StatusTypeDef = StatusRegistry.get_type(status_type) if typedef and typedef.vfx_id != "": var eid: int = int(payload.get("target_id", -1)) var pos: Vector2 = EnemyManager.get_pos_by_id(eid) if pos != Vector2(-9999.0, -9999.0): play(typedef.vfx_id, pos) func get_active_count() -> int: return _active_count func reset() -> void: for key in _vfx_pool: for n in _vfx_pool[key]: if is_instance_valid(n): n.queue_free() _vfx_pool.clear() _active_count = 0