1. 2.5D粒子与视觉特效
2026/4/14大约 7 分钟
2.5D 粒子与视觉特效
粒子系统和后处理效果是让游戏画面从"能玩"变成"好看"的关键。本章深入讲解 Godot 4 中的粒子系统和视觉特效,帮助你为 2.5D 游戏打造精美的视觉表现。
1.1 GPUParticles3D 深度解析
GPUParticles3D 是 Godot 4 中性能最强的粒子节点,粒子的运算全部在 GPU 上完成,可以轻松支持数万个粒子同时运行。
核心参数详解
| 参数 | 说明 | 典型值 |
|---|---|---|
amount | 粒子总数量 | 100~10000 |
lifetime | 单个粒子存活时间(秒) | 0.5~5.0 |
emitting | 是否正在发射 | true/false |
one_shot | 是否只发射一次 | 爆炸效果用 true |
explosiveness | 爆发性(0=均匀,1=同时发射) | 0.0~1.0 |
randomness | 随机性 | 0.0~1.0 |
在 2.5D 中使用 GPUParticles3D
2.5D 游戏的粒子需要特别注意:粒子是 3D 的,但要看起来像 2D。关键是控制粒子在 Z 轴方向的扩散范围。
C#
using Godot;
public partial class ParticleEffect25D : Node3D
{
[Export] private GpuParticles3D _particles;
public override void _Ready()
{
// 获取粒子材质
var material = _particles.ProcessMaterial as ParticleProcessMaterial;
if (material == null) return;
// 限制 Z 轴扩散,保持 2.5D 风格
material.EmissionBoxExtents = new Vector3(1.0f, 1.0f, 0.05f);
// 设置重力方向(向下)
material.Gravity = new Vector3(0, -9.8f, 0);
// 初速度范围
material.InitialVelocityMin = 2.0f;
material.InitialVelocityMax = 5.0f;
// 粒子颜色渐变(从亮到透明)
var gradient = new Gradient();
gradient.SetColor(0, new Color(1, 0.8f, 0.2f, 1.0f)); // 金黄色
gradient.SetColor(1, new Color(1, 0.2f, 0.0f, 0.0f)); // 红色透明
var gradientTexture = new GradientTexture1D();
gradientTexture.Gradient = gradient;
material.ColorRamp = gradientTexture;
}
// 在指定位置播放一次性爆炸粒子
public void PlayExplosion(Vector3 position)
{
_particles.GlobalPosition = position;
_particles.Restart();
_particles.Emitting = true;
}
}GDScript
extends Node3D
@export var particles: GPUParticles3D
func _ready() -> void:
var material := particles.process_material as ParticleProcessMaterial
if not material:
return
# 限制 Z 轴扩散,保持 2.5D 风格
material.emission_box_extents = Vector3(1.0, 1.0, 0.05)
# 设置重力方向(向下)
material.gravity = Vector3(0, -9.8, 0)
# 初速度范围
material.initial_velocity_min = 2.0
material.initial_velocity_max = 5.0
# 粒子颜色渐变(从亮到透明)
var gradient := Gradient.new()
gradient.set_color(0, Color(1, 0.8, 0.2, 1.0)) # 金黄色
gradient.set_color(1, Color(1, 0.2, 0.0, 0.0)) # 红色透明
var gradient_texture := GradientTexture1D.new()
gradient_texture.gradient = gradient
material.color_ramp = gradient_texture
# 在指定位置播放一次性爆炸粒子
func play_explosion(position: Vector3) -> void:
particles.global_position = position
particles.restart()
particles.emitting = true1.2 CPUParticles3D 的适用场景
CPUParticles3D 在 CPU 上运算,性能不如 GPU 版本,但有一个重要优势:支持自定义粒子行为,可以用代码精确控制每个粒子。
适合使用 CPUParticles3D 的场景:
- 粒子数量少(< 500)但行为复杂
- 需要粒子与游戏逻辑交互(如粒子碰到墙壁反弹)
- 需要在低端设备上运行
C#
using Godot;
public partial class MagicTrail : Node3D
{
[Export] private CpuParticles3D _trail;
public override void _Ready()
{
// CPUParticles3D 配置
_trail.Amount = 50;
_trail.Lifetime = 0.8f;
_trail.SpreadAngle = 15.0f;
// 设置粒子形状为球形发射
_trail.EmissionShape = CpuParticles3D.EmissionShapeEnum.Sphere;
_trail.EmissionSphereRadius = 0.1f;
// 粒子缩放随时间变小
_trail.ScaleAmountMin = 0.5f;
_trail.ScaleAmountMax = 1.0f;
_trail.ScaleCurve = CreateFadeCurve();
}
private Curve CreateFadeCurve()
{
var curve = new Curve();
curve.AddPoint(new Vector2(0, 1)); // 开始时大
curve.AddPoint(new Vector2(1, 0)); // 结束时消失
return curve;
}
}GDScript
extends Node3D
@export var trail: CPUParticles3D
func _ready() -> void:
# CPUParticles3D 配置
trail.amount = 50
trail.lifetime = 0.8
trail.spread_angle = 15.0
# 设置粒子形状为球形发射
trail.emission_shape = CPUParticles3D.EMISSION_SHAPE_SPHERE
trail.emission_sphere_radius = 0.1
# 粒子缩放随时间变小
trail.scale_amount_min = 0.5
trail.scale_amount_max = 1.0
trail.scale_curve = _create_fade_curve()
func _create_fade_curve() -> Curve:
var curve := Curve.new()
curve.add_point(Vector2(0, 1)) # 开始时大
curve.add_point(Vector2(1, 0)) # 结束时消失
return curve1.3 粒子特效实战:硬币收集效果
下面实现一个经典的硬币收集粒子效果:硬币消失时,金色粒子向四周飞散。
C#
using Godot;
public partial class CoinCollectEffect : Node3D
{
private GpuParticles3D _burstParticles;
private GpuParticles3D _sparkleParticles;
public override void _Ready()
{
_burstParticles = GetNode<GpuParticles3D>("BurstParticles");
_sparkleParticles = GetNode<GpuParticles3D>("SparkleParticles");
// 配置爆发粒子(金色碎片)
SetupBurstParticles();
// 配置闪光粒子(星星)
SetupSparkleParticles();
}
private void SetupBurstParticles()
{
var mat = _burstParticles.ProcessMaterial as ParticleProcessMaterial;
if (mat == null) return;
_burstParticles.Amount = 20;
_burstParticles.Lifetime = 0.6f;
_burstParticles.OneShot = true;
_burstParticles.Explosiveness = 0.9f; // 几乎同时爆发
mat.Direction = new Vector3(0, 1, 0);
mat.Spread = 180.0f; // 360度扩散
mat.InitialVelocityMin = 3.0f;
mat.InitialVelocityMax = 6.0f;
mat.Gravity = new Vector3(0, -15.0f, 0);
mat.LinearAccelMin = -2.0f;
mat.LinearAccelMax = 2.0f;
}
private void SetupSparkleParticles()
{
var mat = _sparkleParticles.ProcessMaterial as ParticleProcessMaterial;
if (mat == null) return;
_sparkleParticles.Amount = 8;
_sparkleParticles.Lifetime = 0.4f;
_sparkleParticles.OneShot = true;
_sparkleParticles.Explosiveness = 1.0f;
mat.InitialVelocityMin = 1.0f;
mat.InitialVelocityMax = 2.0f;
mat.Spread = 180.0f;
}
public void Play()
{
_burstParticles.Restart();
_sparkleParticles.Restart();
_burstParticles.Emitting = true;
_sparkleParticles.Emitting = true;
// 特效播放完毕后自动销毁
float maxLifetime = Mathf.Max(_burstParticles.Lifetime, _sparkleParticles.Lifetime);
GetTree().CreateTimer(maxLifetime + 0.1f).Timeout += () => QueueFree();
}
}GDScript
extends Node3D
var _burst_particles: GPUParticles3D
var _sparkle_particles: GPUParticles3D
func _ready() -> void:
_burst_particles = $BurstParticles
_sparkle_particles = $SparkleParticles
_setup_burst_particles()
_setup_sparkle_particles()
func _setup_burst_particles() -> void:
var mat := _burst_particles.process_material as ParticleProcessMaterial
if not mat:
return
_burst_particles.amount = 20
_burst_particles.lifetime = 0.6
_burst_particles.one_shot = true
_burst_particles.explosiveness = 0.9
mat.direction = Vector3(0, 1, 0)
mat.spread = 180.0
mat.initial_velocity_min = 3.0
mat.initial_velocity_max = 6.0
mat.gravity = Vector3(0, -15.0, 0)
func _setup_sparkle_particles() -> void:
var mat := _sparkle_particles.process_material as ParticleProcessMaterial
if not mat:
return
_sparkle_particles.amount = 8
_sparkle_particles.lifetime = 0.4
_sparkle_particles.one_shot = true
_sparkle_particles.explosiveness = 1.0
mat.initial_velocity_min = 1.0
mat.initial_velocity_max = 2.0
mat.spread = 180.0
func play() -> void:
_burst_particles.restart()
_sparkle_particles.restart()
_burst_particles.emitting = true
_sparkle_particles.emitting = true
var max_lifetime := maxf(_burst_particles.lifetime, _sparkle_particles.lifetime)
get_tree().create_timer(max_lifetime + 0.1).timeout.connect(queue_free)1.4 后处理效果:WorldEnvironment
后处理效果(Post-Processing)是在整个画面渲染完成后,对最终图像进行的额外处理。Godot 通过 WorldEnvironment 节点和 Environment 资源来配置。
配置 Bloom(泛光)效果
Bloom 让明亮的区域产生发光晕染效果,是 2.5D 游戏中最常用的后处理之一。
C#
using Godot;
public partial class EnvironmentSetup : Node3D
{
[Export] private WorldEnvironment _worldEnv;
public override void _Ready()
{
var env = _worldEnv.Environment;
// 启用 Bloom
env.GlowEnabled = true;
env.GlowIntensity = 0.8f; // 泛光强度
env.GlowBloom = 0.3f; // 泛光扩散
env.GlowBlendMode = Environment.GlowBlendModeEnum.Additive;
env.GlowHdrThreshold = 1.0f; // 只有亮度超过此值才发光
env.GlowHdrScale = 2.0f;
// 启用色调映射(让画面颜色更自然)
env.TonemapMode = Environment.ToneMapper.Aces;
env.TonemapExposure = 1.0f;
env.TonemapWhite = 1.0f;
// 环境光遮蔽(增加立体感)
env.SsaoEnabled = true;
env.SsaoRadius = 1.0f;
env.SsaoIntensity = 2.0f;
}
// 动态调整 Bloom 强度(用于特效时刻)
public void SetBloomIntensity(float intensity)
{
_worldEnv.Environment.GlowIntensity = intensity;
}
}GDScript
extends Node3D
@export var world_env: WorldEnvironment
func _ready() -> void:
var env := world_env.environment
# 启用 Bloom
env.glow_enabled = true
env.glow_intensity = 0.8
env.glow_bloom = 0.3
env.glow_blend_mode = Environment.GLOW_BLEND_MODE_ADDITIVE
env.glow_hdr_threshold = 1.0
env.glow_hdr_scale = 2.0
# 启用色调映射
env.tonemap_mode = Environment.TONE_MAPPER_ACES
env.tonemap_exposure = 1.0
env.tonemap_white = 1.0
# 环境光遮蔽
env.ssao_enabled = true
env.ssao_radius = 1.0
env.ssao_intensity = 2.0
# 动态调整 Bloom 强度
func set_bloom_intensity(intensity: float) -> void:
world_env.environment.glow_intensity = intensity1.5 色彩校正与风格化
色彩校正可以让游戏呈现特定的视觉风格,比如复古风、赛博朋克风、温暖童话风等。
C#
using Godot;
public partial class ColorGrading : Node3D
{
[Export] private WorldEnvironment _worldEnv;
// 应用复古风格色彩校正
public void ApplyRetroStyle()
{
var env = _worldEnv.Environment;
// 降低饱和度
env.AdjustmentEnabled = true;
env.AdjustmentSaturation = 0.6f;
env.AdjustmentContrast = 1.2f;
env.AdjustmentBrightness = 0.9f;
// 使用 LUT(颜色查找表)进行精确色彩映射
// LUT 是一张特殊的图片,记录了颜色的映射关系
var lut = GD.Load<Texture3D>("res://assets/luts/retro_lut.png");
if (lut != null)
env.AdjustmentColorCorrection = lut;
}
// 应用赛博朋克风格
public void ApplyCyberpunkStyle()
{
var env = _worldEnv.Environment;
env.AdjustmentEnabled = true;
env.AdjustmentSaturation = 1.4f; // 高饱和度
env.AdjustmentContrast = 1.3f;
// 配合 Bloom 增强霓虹感
env.GlowEnabled = true;
env.GlowIntensity = 1.5f;
env.GlowBloom = 0.5f;
}
// 平滑过渡到新风格
public async void TransitionToStyle(float targetSaturation, float duration)
{
var env = _worldEnv.Environment;
float startSat = env.AdjustmentSaturation;
float elapsed = 0f;
while (elapsed < duration)
{
elapsed += (float)GetProcessDeltaTime();
float t = elapsed / duration;
env.AdjustmentSaturation = Mathf.Lerp(startSat, targetSaturation, t);
await ToSignal(GetTree(), SceneTree.SignalName.ProcessFrame);
}
env.AdjustmentSaturation = targetSaturation;
}
}GDScript
extends Node3D
@export var world_env: WorldEnvironment
# 应用复古风格色彩校正
func apply_retro_style() -> void:
var env := world_env.environment
env.adjustment_enabled = true
env.adjustment_saturation = 0.6
env.adjustment_contrast = 1.2
env.adjustment_brightness = 0.9
# 使用 LUT 进行精确色彩映射
var lut := load("res://assets/luts/retro_lut.png") as Texture3D
if lut:
env.adjustment_color_correction = lut
# 应用赛博朋克风格
func apply_cyberpunk_style() -> void:
var env := world_env.environment
env.adjustment_enabled = true
env.adjustment_saturation = 1.4
env.adjustment_contrast = 1.3
env.glow_enabled = true
env.glow_intensity = 1.5
env.glow_bloom = 0.5
# 平滑过渡到新风格
func transition_to_style(target_saturation: float, duration: float) -> void:
var env := world_env.environment
var start_sat := env.adjustment_saturation
var elapsed := 0.0
while elapsed < duration:
elapsed += get_process_delta_time()
var t := elapsed / duration
env.adjustment_saturation = lerpf(start_sat, target_saturation, t)
await get_tree().process_frame
env.adjustment_saturation = target_saturation1.6 屏幕空间特效:CanvasLayer 后处理
对于 2.5D 游戏,有时需要在 2D 层面做后处理(比如全屏闪白、屏幕震动)。
C#
using Godot;
public partial class ScreenEffects : CanvasLayer
{
private ColorRect _flashOverlay;
private Camera3D _camera;
public override void _Ready()
{
// 创建全屏覆盖层
_flashOverlay = new ColorRect();
_flashOverlay.Color = new Color(1, 1, 1, 0); // 初始透明
_flashOverlay.SetAnchorsPreset(Control.LayoutPreset.FullRect);
_flashOverlay.MouseFilter = Control.MouseFilterEnum.Ignore;
AddChild(_flashOverlay);
_camera = GetViewport().GetCamera3D();
}
// 屏幕闪白效果(受击反馈)
public async void FlashWhite(float duration = 0.15f)
{
_flashOverlay.Color = new Color(1, 1, 1, 0.8f);
float elapsed = 0f;
while (elapsed < duration)
{
elapsed += (float)GetProcessDeltaTime();
float alpha = Mathf.Lerp(0.8f, 0f, elapsed / duration);
_flashOverlay.Color = new Color(1, 1, 1, alpha);
await ToSignal(GetTree(), SceneTree.SignalName.ProcessFrame);
}
_flashOverlay.Color = new Color(1, 1, 1, 0);
}
// 相机震动效果
public async void ShakeCamera(float intensity = 0.3f, float duration = 0.3f)
{
if (_camera == null) return;
Vector3 originalPos = _camera.Position;
float elapsed = 0f;
while (elapsed < duration)
{
elapsed += (float)GetProcessDeltaTime();
float remaining = 1f - (elapsed / duration);
float offsetX = GD.Randf() * 2f - 1f;
float offsetY = GD.Randf() * 2f - 1f;
_camera.Position = originalPos + new Vector3(offsetX, offsetY, 0) * intensity * remaining;
await ToSignal(GetTree(), SceneTree.SignalName.ProcessFrame);
}
_camera.Position = originalPos;
}
}GDScript
extends CanvasLayer
var _flash_overlay: ColorRect
var _camera: Camera3D
func _ready() -> void:
_flash_overlay = ColorRect.new()
_flash_overlay.color = Color(1, 1, 1, 0)
_flash_overlay.set_anchors_preset(Control.PRESET_FULL_RECT)
_flash_overlay.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(_flash_overlay)
_camera = get_viewport().get_camera_3d()
# 屏幕闪白效果
func flash_white(duration: float = 0.15) -> void:
_flash_overlay.color = Color(1, 1, 1, 0.8)
var elapsed := 0.0
while elapsed < duration:
elapsed += get_process_delta_time()
var alpha := lerpf(0.8, 0.0, elapsed / duration)
_flash_overlay.color = Color(1, 1, 1, alpha)
await get_tree().process_frame
_flash_overlay.color = Color(1, 1, 1, 0)
# 相机震动效果
func shake_camera(intensity: float = 0.3, duration: float = 0.3) -> void:
if not _camera:
return
var original_pos := _camera.position
var elapsed := 0.0
while elapsed < duration:
elapsed += get_process_delta_time()
var remaining := 1.0 - (elapsed / duration)
var offset := Vector3(randf() * 2 - 1, randf() * 2 - 1, 0)
_camera.position = original_pos + offset * intensity * remaining
await get_tree().process_frame
_camera.position = original_pos1.7 性能注意事项
| 特效类型 | 性能消耗 | 建议上限 |
|---|---|---|
| GPUParticles3D | 低(GPU) | 10000+ 粒子 |
| CPUParticles3D | 中(CPU) | 500 粒子 |
| Bloom(泛光) | 中 | 全局开启 |
| SSAO | 高 | 按需开启 |
| 屏幕空间反射 | 很高 | 高端设备 |
移动端注意
在移动端,SSAO 和屏幕空间反射几乎不可用。Bloom 也需要降低质量。建议为不同平台准备不同的 Environment 配置。
粒子池化
频繁创建和销毁粒子节点会造成性能抖动。建议使用对象池(见第 3 章)管理粒子特效实例。
