血液喷溅特效模拟

复刻的血液喷溅效果图

这篇文章旨在我对于《武士零》血液特效的细节探索。项目从角色受击生成血液贴花继续推进,重点在受击方向、喷射节奏、血滴飞行、溶解、落点贴花和场景材质融合。

迭代到当前版本时,我觉得血滴飞出去以后要有重量感,消失时不能像突然删节点,落到场景里的痕迹要和背景有关系,多个血迹还要能各自保存喷射方向。

如何产生血液特效

不同角度的小型血液喷溅纹理

这个项目最核心的就是一次受击事件怎样一路传到最终画面。角色只负责产生受击方向和喷射节奏,管理器决定生成哪类血滴,血滴节点负责飞行和溶解,并在飞行过程中抛出快照,最后贴花节点把这个快照压到场景画面里。

这条链路可以概括成:

1
2
3
4
5
角色受击 -> 计算喷射方向
BloodFXManager -> 选择血滴类型
DissolvingSprite -> 飞行、溶解、生成快照
BloodDecal -> 独立材质、记录喷射角度
decal_composite.gdshader -> 根据背景亮度融合

大型血浆飞溅纹理

这样拆的好处是生命周期清楚。临时血滴可以自然消失,贴花可以长期留在 DecalContainer,角色脚本也不会被材质参数和贴花管理污染。

受击事件只输出方向和节奏

角色脚本是入口。它继承 Area2D,通过鼠标点击触发受击,状态从 IDLE 切到 HURT,播放受击动画,同时启动 BloodSpawnTimer。角色只保存受击方向、击退速度和喷血频率。

1
2
3
4
5
6
7
8
9
10
11
func take_damage(mouse_pos: Vector2) -> void:
_current_state = State.HURT
animated_sprite.play("hurt")
blood_spawn_timer.start()

var direction = (global_position - mouse_pos).normalized()
if direction.is_zero_approx():
direction = Vector2.UP

_last_hit_direction = direction
_velocity = direction * knockback_power

喷血时再在受击方向上加一个小随机角度。这里我控制在正负 15 度:范围太小会像一条直线,范围太大又会丢掉命中方向。

1
2
3
4
5
6
7
func _on_blood_spawn_timer_timeout() -> void:
var base_angle = _last_hit_direction.angle()
var random_offset = deg_to_rad(randf_range(-15, 15))
var final_direction = Vector2.from_angle(base_angle + random_offset)

var spawn_position = global_position + final_direction * 100
blood_fx_manager.spawn_effect(spawn_position, final_direction, blood_gravity_scale)

这里解决的是角色只告诉外部血液贴图从哪里喷、往哪里喷、重力比例是多少。

管理器负责生成策略

管理器所含场景

BloodFXManager 是调度层。它持有动画血滴、溶解血滴、固定角度血滴和贴花场景。所有持久贴花最后都进入 DecalContainer,这样后面做数量上限、清理、对象池都能从一个容器处理。

生成策略用 _spawn_counter 做简单分流:每三次出一个动画粒子,其余走静态或溶解 Sprite。动画粒子适合表现一团血的形状变化,静态高速血滴适合表现甩出去的长距离喷溅。

1
2
3
4
5
6
7
8
9
func spawn_effect(spawn_position: Vector2, target_direction: Vector2, gravity_scale: float):
_spawn_counter += 1

var particle = _pick_particle_scene(_spawn_counter).instantiate()
add_child(particle)

particle.global_position = spawn_position
particle.request_stamp.connect(_on_particle_request_stamp)
particle.launch_and_dissolve(target_direction, decal_speed, decal_dissolve_duration, gravity_scale)

方向动画选择也放在管理器里。角色只传 Vector2,管理器根据角度选择最接近的一组 SpriteFrames。这样美术资源和角色状态机就解耦了。

不同角度的小型血液喷溅纹理

1
2
3
4
5
6
7
8
9
func _get_animation_name_for_direction(direction: Vector2) -> StringName:
var lookup_angle = abs(rad_to_deg(direction.angle()))
var best_key = "0"

for key in ANIMATION_MAP.keys():
if key.is_valid_int() and abs(lookup_angle - key.to_int()) < abs(lookup_angle - best_key.to_int()):
best_key = key

return ANIMATION_MAP[best_key]

血滴节点负责飞行、溶解和快照

DissolvingSprite.gd 是临时血滴的基类。它有速度、重力、溶解时间、贴花检测频率和贴花概率。飞行阶段每帧叠加重力,位置按速度推进,静态血滴根据速度方向旋转。

1
2
3
4
5
6
func _process(delta: float):
velocity += Vector2.DOWN * gravity_scale * delta
global_position += velocity * delta

if not use_animation:
rotation = velocity.angle() + fixed_angle_offset

溶解使用独立 ShaderMaterial。每个血滴在 _ready() 时创建自己的材质实例,然后用 Tween 推 progress。这样多个血滴同时存在时,溶解进度不会互相覆盖。

1
2
3
4
5
6
7
8
9
10
func launch_and_dissolve(direction: Vector2, speed: float, duration: float, gravity: float):
velocity = direction * speed
gravity_scale = gravity

var mat := _get_material()
_tween = create_tween().set_trans(Tween.TRANS_SINE)
_tween.tween_property(mat, "shader_parameter/progress", 1.0, duration)
_tween.tween_callback(queue_free)

_stamp_timer.start()

这里最关键的是“快照”。血滴本身会销毁,但它可以把当前 transform、贴图、材质进度、翻转状态和运动方向打包发出去。管理器收到以后生成独立贴花。

1
2
3
4
5
6
7
8
9
10
11
12
13
func _create_stamp_snapshot():
var live_material := _get_material()
var stamp_material := live_material.duplicate() as ShaderMaterial

var snapshot_data = {
"transform": global_transform,
"texture": _get_current_texture(),
"material": stamp_material,
"flip_v": _get_current_flip_v(),
"direction": velocity.normalized()
}

request_stamp.emit(snapshot_data)

这个设计解决了生命周期冲突:临时血滴负责动态效果,持久贴花负责环境痕迹。

贴花需要独立材质保存喷射方向

管理器收到快照后,实例化一个 Sprite2D 贴花,把 transform、texture、flip 状态恢复出来。然后给这个贴花复制一份材质,并写入自己的 spray_angle

1
2
3
4
5
6
7
8
9
10
11
12
func _on_particle_request_stamp(snapshot_data: Dictionary):
var decal = blood_decal_scene.instantiate() as Sprite2D

decal.global_transform = snapshot_data["transform"]
decal.texture = snapshot_data["texture"]
decal.flip_v = snapshot_data.get("flip_v", false)

var mat = decal.material.duplicate(true) as ShaderMaterial
mat.set_shader_parameter("spray_angle", rad_to_deg(snapshot_data["direction"].angle()))
decal.material = mat

decal_container.add_child(decal)

我一开始没有足够重视材质实例化。多个 Sprite 共用同一份 ShaderMaterial 时,后写入的 spray_angle 会影响前面的血迹,画面上会出现方向统一的问题。每个贴花独立材质后,喷射方向就能被保存下来。

溶解和屏幕空间融合

血液溶解Shader

飞行血滴的消失使用一个很小的 canvas_item Shader。它读取噪声纹理,根据 progresssoftnesssmoothstep 计算 alpha。

贴花如果只做普通 alpha 混合,会像浮在屏幕上的红色贴纸。decal_composite.gdshader 里我读取 SCREEN_TEXTURE,用背景亮度参与遮蔽、扭曲、暗部裁剪等风格混合,详细带注释的shader参考如下:

混合Shader

这套材质里贴花知道自己从哪里来。方向从角色受击一路传到血滴速度,再进入快照,最后写进贴花 Shader。一个小参数贯穿了事件层、运动层和材质层。

总结

这个专题展示的是血液喷溅特效系统。角色状态、特效生成、血滴生命周期、持久贴花和屏幕空间材质各自有职责。受击有方向,喷射有节奏,飞行有重力,消失有材质过程,落点能留下独立痕迹,痕迹还能根据背景产生融合感,我觉得是一个不错的模拟示例。

作者

VoidGameSpace

发布于

2025-09-27

更新于

2025-09-27

许可协议

评论