8. 音效与特效
2026/4/14大约 7 分钟
音效与特效
闭上眼睛想一想:一辆摩托车从远处飞驰而过,你会听到什么?引擎从低沉的"嗡嗡"变成尖锐的"嗷嗷",轮胎碾过碎石的沙沙声,风呼啸着从耳边掠过......这些声音和视觉效果才是让玩家"感觉自己在骑车"的关键。
本章你将学到
- 引擎声浪系统(随转速动态变化)
- 漂移碎石音效
- 天气环境音
- 速度线粒子特效(GPUParticles3D)
- 轮胎烟雾效果
引擎声浪系统
引擎声是竞速游戏的"灵魂"。如果引擎声不对,玩家立刻就会觉得假。
引擎声怎么变化?
真实摩托车的引擎声有两个主要变化:
- 音调(Pitch):转速越高,声音越尖。就像吉他弦拧得越紧,声音越高
- 音量(Volume):油门踩得越深,声音越大
引擎声浪频率映射
完整引擎声浪代码
C#
// 引擎声浪控制器
public partial class EngineAudio : Node
{
[Export] public NodePath MotorcyclePath;
[Export] public AudioStreamPlayer EnginePlayer;
[Export] public float MinPitch = 0.5f; // 怠速音调
[Export] public float MaxPitch = 2.0f; // 最高转速音调
[Export] public float MaxSpeed = 50f; // 最高速度 m/s
private RigidBody3D _motorcycle;
public override void _Ready()
{
_motorcycle = GetNode<RigidBody3D>(MotorcyclePath);
EnginePlayer.Play();
}
public override void _Process(double delta)
{
if (_motorcycle == null) return;
float speed = _motorcycle.LinearVelocity.Length();
float speedRatio = Mathf.Clamp(speed / MaxSpeed, 0f, 1f);
// 音调随速度线性变化
float pitch = Mathf.Lerp(MinPitch, MaxPitch, speedRatio);
EnginePlayer.PitchScale = pitch;
// 音量也略有变化(高速时稍大)
float volume = Mathf.Lerp(-10f, 0f, speedRatio); // dB
EnginePlayer.VolumeDb = volume;
}
}GDScript
# 引擎声浪控制器
extends Node
@export var motorcycle_path: NodePath
@export var engine_player: AudioStreamPlayer
@export var min_pitch: float = 0.5 # 怠速音调
@export var max_pitch: float = 2.0 # 最高转速音调
@export var max_speed: float = 50.0 # 最高速度 m/s
var _motorcycle: RigidBody3D
func _ready():
_motorcycle = get_node(motorcycle_path)
engine_player.play()
func _process(delta):
if _motorcycle == null:
return
var speed = _motorcycle.linear_velocity.length()
var speed_ratio = clampf(speed / max_speed, 0.0, 1.0)
# 音调随速度线性变化
var pitch = lerpf(min_pitch, max_pitch, speed_ratio)
engine_player.pitch_scale = pitch
# 音量也略有变化
var volume = lerpf(-10.0, 0.0, speed_ratio) # dB
engine_player.volume_db = volume引擎音效素材建议
你可以用多种方式获取引擎音效:
- 录制真实引擎:效果最好,但需要设备
- 免费音效库:Freesound.org 上搜索 "motorcycle engine"
- 合成器生成:用 Audacity 等工具合成一个低频嗡嗡声
建议使用一段中等转速的引擎录音,然后通过代码动态调整音调来覆盖整个转速范围。
漂移碎石音效
摩托车在沙地或碎石路上行驶时,轮胎会扬起碎石,发出沙沙声。
C#
// 轮胎摩擦音效
public partial class TireSoundFX : Node
{
[Export] public AudioStreamPlayer TireScreechPlayer;
[Export] public AudioStreamPlayer GravelPlayer;
[Export] public NodePath MotorcyclePath;
[Export] public float SlideThreshold = 3f; // 侧滑速度阈值
private RigidBody3D _motorcycle;
public override void _Ready()
{
_motorcycle = GetNode<RigidBody3D>(MotorcyclePath);
}
public override void _Process(double delta)
{
if (_motorcycle == null) return;
// 计算侧滑速度(摩托车前进方向以外的速度分量)
Vector3 forward = -_motorcycle.GlobalTransform.Basis.Z;
Vector3 velocity = _motorcycle.LinearVelocity;
float forwardSpeed = velocity.Dot(forward);
Vector3 lateralVelocity = velocity - forward * forwardSpeed;
float slideSpeed = lateralVelocity.Length();
// 侧滑时播放轮胎摩擦声
if (slideSpeed > SlideThreshold)
{
if (!TireScreechPlayer.Playing)
TireScreechPlayer.Play();
float intensity = Mathf.Clamp(slideSpeed / 10f, 0f, 1f);
TireScreechPlayer.VolumeDb = Mathf.Lerp(-20f, 0f, intensity);
TireScreechPlayer.PitchScale = Mathf.Lerp(0.8f, 1.2f, intensity);
}
else
{
TireScreechPlayer.Stop();
}
}
}GDScript
# 轮胎摩擦音效
extends Node
@export var tire_screech_player: AudioStreamPlayer
@export var gravel_player: AudioStreamPlayer
@export var motorcycle_path: NodePath
@export var slide_threshold: float = 3.0 # 侧滑速度阈值
var _motorcycle: RigidBody3D
func _ready():
_motorcycle = get_node(motorcycle_path)
func _process(delta):
if _motorcycle == null:
return
# 计算侧滑速度
var forward = -_motorcycle.global_transform.basis.z
var velocity = _motorcycle.linear_velocity
var forward_speed = velocity.dot(forward)
var lateral_velocity = velocity - forward * forward_speed
var slide_speed = lateral_velocity.length()
# 侧滑时播放轮胎摩擦声
if slide_speed > slide_threshold:
if not tire_screech_player.playing:
tire_screech_player.play()
var intensity = clampf(slide_speed / 10.0, 0.0, 1.0)
tire_screech_player.volume_db = lerpf(-20.0, 0.0, intensity)
tire_screech_player.pitch_scale = lerpf(0.8, 1.2, intensity)
else:
tire_screech_player.stop()天气环境音
不同天气有不同的背景音效:
| 天气 | 音效 | 实现方式 |
|---|---|---|
| 晴天 | 鸟叫+微风 | AudioStreamPlayer 循环播放 |
| 雨天 | 雨声 | AudioStreamPlayer 循环播放 |
| 雪天 | 风声 | AudioStreamPlayer 循环播放 |
| 雾天 | 寂静+微弱风声 | 低音量播放 |
环境音跟随天气系统联动:天气变化时,淡出当前环境音,淡入新环境音。
速度线特效
速度线是飞驰而过的光线条纹,出现在画面两侧。这是增强速度感最有效的视觉手段之一。
实现方式
使用 GPUParticles3D,从摩托车两侧发射细长的粒子,方向朝后方:
SpeedLines (GPUParticles3D)
├── amount: 100
├── process_material:
│ ├── emission_shape: BOX (2, 1, 0.5)
│ ├── direction: (0, 0, 1) ← 朝后方
│ ├── initial_velocity: 30~50
│ ├── gravity: (0, 0, 0)
│ └── 颜色:白色半透明
├── draw_pass_1: 细长条mesh
└── 跟随摄像机位置速度线控制代码
C#
// 速度线特效控制器
public partial class SpeedLineFX : GPUParticles3D
{
[Export] public NodePath MotorcyclePath;
[Export] public float MinSpeedForLines = 15f; // 最低速度才出现速度线
[Export] public float MaxSpeed = 50f;
private RigidBody3D _motorcycle;
public override void _Ready()
{
_motorcycle = GetNode<RigidBody3D>(MotorcyclePath);
}
public override void _Process(double delta)
{
if (_motorcycle == null) return;
float speed = _motorcycle.LinearVelocity.Length();
// 只在高速时显示速度线
if (speed > MinSpeedForLines)
{
Emitting = true;
float ratio = (speed - MinSpeedForLines) / (MaxSpeed - MinSpeedForLines);
Amount = (int)Mathf.Lerp(20, 150, ratio); // 速度越快粒子越多
}
else
{
Emitting = false;
}
// 跟随摩托车位置(在摄像机和摩托车之间)
GlobalPosition = _motorcycle.GlobalPosition + Vector3.Up * 1.5f;
}
}GDScript
# 速度线特效控制器
extends GPUParticles3D
@export var motorcycle_path: NodePath
@export var min_speed_for_lines: float = 15.0 # 最低速度才出现速度线
@export var max_speed: float = 50.0
var _motorcycle: RigidBody3D
func _ready():
_motorcycle = get_node(motorcycle_path)
func _process(delta):
if _motorcycle == null:
return
var speed = _motorcycle.linear_velocity.length()
# 只在高速时显示速度线
if speed > min_speed_for_lines:
emitting = true
var ratio = (speed - min_speed_for_lines) / (max_speed - min_speed_for_lines)
amount = int(lerpf(20.0, 150.0, ratio)) # 速度越快粒子越多
else:
emitting = false
# 跟随摩托车位置
global_position = _motorcycle.global_position + Vector3.UP * 1.5轮胎烟雾效果
摩托车在沙地或泥地上急加速、急刹车时,后轮会扬起尘土或烟雾。
烟雾粒子设置
TireSmoke (GPUParticles3D) ← 挂在摩托车后轮位置
├── amount: 50
├── process_material:
│ ├── emission_shape: POINT
│ ├── direction: (0, 1, 0) ← 向上飘
│ ├── spread: 30
│ ├── initial_velocity: 2~5
│ ├── gravity: (0, 0.5, 0) ← 微微上升
│ ├── scale: 0.5~1.5 ← 烟雾大小
│ └── color_ramp: 白色→透明 ← 逐渐消失
└── draw_pass_1: SphereMesh (烟雾球)烟雾触发条件
C#
// 轮胎烟雾控制器
public partial class TireSmokeFX : GPUParticles3D
{
[Export] public NodePath MotorcyclePath;
private RigidBody3D _motorcycle;
public override void _Ready()
{
_motorcycle = GetNode<RigidBody3D>(MotorcyclePath);
}
public override void _Process(double delta)
{
if (_motorcycle == null) return;
float speed = _motorcycle.LinearVelocity.Length();
float throttle = Input.GetStrength("accelerate");
float brake = Input.GetStrength("brake_rear");
// 急加速或急刹车时产生烟雾
bool shouldSmoke = (throttle > 0.7f && speed > 5f) ||
(brake > 0.5f && speed > 10f);
Emitting = shouldSmoke;
// 烟雾量与操作力度成正比
if (shouldSmoke)
{
float intensity = Mathf.Max(throttle, brake);
Amount = (int)Mathf.Lerp(10, 50, intensity);
}
}
}GDScript
# 轮胎烟雾控制器
extends GPUParticles3D
@export var motorcycle_path: NodePath
var _motorcycle: RigidBody3D
func _ready():
_motorcycle = get_node(motorcycle_path)
func _process(delta):
if _motorcycle == null:
return
var speed = _motorcycle.linear_velocity.length()
var throttle = Input.get_strength("accelerate")
var brake = Input.get_strength("brake_rear")
# 急加速或急刹车时产生烟雾
var should_smoke = (throttle > 0.7 and speed > 5.0) or \
(brake > 0.5 and speed > 10.0)
emitting = should_smoke
if should_smoke:
var intensity = maxf(throttle, brake)
amount = int(lerpf(10.0, 50.0, intensity))音效层级总览
常见问题
Q:引擎声循环播放有明显的"接头"怎么办?
选择一段本身就循环友好的音效(开头和结尾波形平滑过渡)。或者使用 AudioStreamSample 的 loop 属性,设置好循环起始点。
Q:粒子特效太多会卡吗?
会。建议总量控制在 2000 个活跃粒子以内。用 amount 属性控制同屏粒子数,而不是无限发射。在低端设备上可以关闭部分特效。
Q:怎么让声音有远近感?
使用 AudioStreamPlayer3D(3D音频播放器)代替普通的 AudioStreamPlayer。3D 音频会根据听者和声源的距离自动调整音量和衰减。
下一步
音频特效完成后,开始 打磨与发布。
