126 lines
3.9 KiB
GDScript
126 lines
3.9 KiB
GDScript
## 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/<id>.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
|