5. 地形与天气系统
2026/4/14大约 8 分钟
地形与天气系统
你有没有这样的体验:同一条路,晴天开车和雨天开车完全是两回事?拉力赛更是如此——路面从柏油变成泥地,抓地力直接打折扣;突然下起暴雨,能见度骤降,还要小心打滑。本章就来实现这些让游戏"活起来"的系统。
本章你将学到
- 地形材质切换系统(不同路面不同摩擦力)
- 天气系统(晴天/雨天/雪天/雾天)
- 天气对物理的影响
- 天气粒子特效
地形材质系统
为什么路面材质很重要?
在真实世界里,你骑自行车在水泥地上和沙地上感觉完全不同。水泥地上刹车一脚就停了,沙地上可能滑好几米。我们的游戏也要模拟这种差异。
路面类型和物理属性
| 路面类型 | 摩擦系数 | 颜色示意 | 驾驶感受 |
|---|---|---|---|
| 柏油路 | 1.0 | 深灰色 | 抓地力强,转弯稳 |
| 泥地 | 0.7 | 棕色 | 中等抓地力,有些滑 |
| 沙地 | 0.4 | 金黄色 | 很滑,容易侧滑 |
| 碎石路 | 0.6 | 灰色碎石 | 颠簸,抓地力一般 |
| 雪地 | 0.3 | 白色 | 非常滑,需要小心控制 |
| 结冰 | 0.15 | 透明偏蓝 | 极度光滑,几乎无摩擦 |
地面检测和材质切换
核心思路是:摩托车下方放一条射线,碰到什么材质的地面就切换到对应的物理参数。
C#
// 地形管理器 - 检测路面类型并调整物理
public partial class TerrainManager : Node
{
// 不同路面的物理参数
public class SurfaceConfig
{
public float Friction; // 摩擦系数
public float Drag; // 空气阻力
public float Bounce; // 弹性
public float MaxSpeedFactor; // 最高速度系数
}
private static readonly Dictionary<string, SurfaceConfig> SurfaceSettings = new()
{
["surface_asphalt"] = new() { Friction = 1.0f, Drag = 0.5f, Bounce = 0.1f, MaxSpeedFactor = 1.0f },
["surface_dirt"] = new() { Friction = 0.7f, Drag = 0.4f, Bounce = 0.15f, MaxSpeedFactor = 0.9f },
["surface_sand"] = new() { Friction = 0.4f, Drag = 0.3f, Bounce = 0.2f, MaxSpeedFactor = 0.75f },
["surface_gravel"] = new() { Friction = 0.6f, Drag = 0.35f, Bounce = 0.25f, MaxSpeedFactor = 0.85f },
["surface_snow"] = new() { Friction = 0.3f, Drag = 0.2f, Bounce = 0.1f, MaxSpeedFactor = 0.7f },
["surface_ice"] = new() { Friction = 0.15f, Drag = 0.1f, Bounce = 0.05f, MaxSpeedFactor = 0.6f }
};
private RigidBody3D _motorcycle;
private string _currentSurface = "surface_dirt";
public override void _Ready()
{
_motorcycle = GetParent<RigidBody3D>();
}
public override void _PhysicsProcess(double delta)
{
// 射线检测地面
var spaceState = _motorcycle.GetWorld3D().DirectSpaceState;
var query = PhysicsRayQueryParameters3D.Create(
_motorcycle.GlobalPosition + Vector3.Up * 0.5f,
_motorcycle.GlobalPosition + Vector3.Down * 1.5f
);
query.Exclude = new Godot.Collections.Array<Rid> { _motorcycle.GetRid() };
var result = spaceState.IntersectRay(query);
if (result.Count > 0)
{
var collider = result["collider"].AsGodotObject() as CollisionObject3D;
if (collider != null)
{
// 检测碰撞体所属的地面组
foreach (var surfaceName in SurfaceSettings.Keys)
{
if (collider.IsInGroup(surfaceName))
{
if (_currentSurface != surfaceName)
{
_currentSurface = surfaceName;
ApplySurfaceSettings();
GD.Print($"路面切换为:{surfaceName}");
}
break;
}
}
}
}
}
private void ApplySurfaceSettings()
{
var config = SurfaceSettings[_currentSurface];
// 调整摩托车的物理参数
_motorcycle.LinearDamp = config.Drag;
// 这里可以根据 friction 调整抓地力
// 具体实现取决于你的摩托车物理系统
}
public float GetCurrentFriction()
{
return SurfaceSettings.ContainsKey(_currentSurface)
? SurfaceSettings[_currentSurface].Friction
: 0.7f;
}
}GDScript
# 地形管理器 - 检测路面类型并调整物理
extends Node
# 路面物理参数
var _surface_settings: Dictionary = {
"surface_asphalt": { "friction": 1.0, "drag": 0.5, "bounce": 0.1, "max_speed_factor": 1.0 },
"surface_dirt": { "friction": 0.7, "drag": 0.4, "bounce": 0.15, "max_speed_factor": 0.9 },
"surface_sand": { "friction": 0.4, "drag": 0.3, "bounce": 0.2, "max_speed_factor": 0.75 },
"surface_gravel": { "friction": 0.6, "drag": 0.35, "bounce": 0.25, "max_speed_factor": 0.85 },
"surface_snow": { "friction": 0.3, "drag": 0.2, "bounce": 0.1, "max_speed_factor": 0.7 },
"surface_ice": { "friction": 0.15, "drag": 0.1, "bounce": 0.05, "max_speed_factor": 0.6 }
}
var _motorcycle: RigidBody3D
var _current_surface: String = "surface_dirt"
func _ready():
_motorcycle = get_parent()
func _physics_process(delta):
# 射线检测地面
var space_state = _motorcycle.get_world_3d().direct_space_state
var query = PhysicsRayQueryParameters3D.create(
_motorcycle.global_position + Vector3.UP * 0.5,
_motorcycle.global_position + Vector3.DOWN * 1.5
)
query.exclude = [_motorcycle.get_rid()]
var result = space_state.intersect_ray(query)
if result.size() > 0:
var collider = result["collider"] as CollisionObject3D
if collider:
# 检测碰撞体所属的地面组
for surface_name in _surface_settings.keys():
if collider.is_in_group(surface_name):
if _current_surface != surface_name:
_current_surface = surface_name
_apply_surface_settings()
print("路面切换为:%s" % surface_name)
break
func _apply_surface_settings():
var config = _surface_settings[_current_surface]
_motorcycle.linear_damp = config.drag
func get_current_friction() -> float:
if _surface_settings.has(_current_surface):
return _surface_settings[_current_surface].friction
return 0.7天气系统
天气系统让游戏不再是一成不变的晴天。突如其来的暴雨或大雾会给比赛增加变数。
天气状态机
天气类型和效果
| 天气 | 能见度 | 物理影响 | 粒子效果 | 环境音 |
|---|---|---|---|---|
| 晴天 | 100% | 无 | 无 | 鸟叫、风声 |
| 多云 | 95% | 微弱影响 | 无 | 风声 |
| 雨天 | 60% | 摩擦降低30% | 雨滴粒子 | 雨声 |
| 雪天 | 50% | 摩擦降低50% | 雪花粒子 | 风雪声 |
| 雾天 | 30% | 无直接影响 | 雾气效果 | 寂静+风声 |
天气管理器
C#
// 天气类型
public enum WeatherType { Clear, Cloudy, Rain, Snow, Fog }
// 天气管理器
public partial class WeatherManager : Node
{
[Signal] public delegate void WeatherChangedEventHandler(WeatherType newWeather);
[Export] public WeatherType StartWeather = WeatherType.Clear;
[Export] public float MinWeatherDuration = 30f; // 最短持续30秒
[Export] public float MaxWeatherDuration = 120f; // 最长持续2分钟
private WeatherType _currentWeather;
private float _weatherTimer;
private float _nextWeatherTime;
private WorldEnvironment _worldEnv;
private GPUParticles3D _rainParticles;
private GPUParticles3D _snowParticles;
private FogVolume _fogVolume;
public override void _Ready()
{
_worldEnv = GetNode<WorldEnvironment>("/root/Main/WorldEnvironment");
_rainParticles = GetNode<GPUParticles3D>("RainParticles");
_snowParticles = GetNode<GPUParticles3D>("SnowParticles");
_fogVolume = GetNodeOrNull<FogVolume>("FogVolume");
SetWeather(StartWeather);
_nextWeatherTime = (float)GD.RandRange(MinWeatherDuration, MaxWeatherDuration);
}
public override void _Process(double delta)
{
_weatherTimer += (float)delta;
if (_weatherTimer >= _nextWeatherTime)
{
_weatherTimer = 0f;
_nextWeatherTime = (float)GD.RandRange(MinWeatherDuration, MaxWeatherDuration);
RandomWeatherChange();
}
}
// 随机切换天气
private void RandomWeatherChange()
{
// 加权随机:晴天概率最高
float roll = (float)GD.RandRange(0.0, 1.0);
WeatherType newWeather;
if (roll < 0.3f)
newWeather = WeatherType.Clear;
else if (roll < 0.5f)
newWeather = WeatherType.Cloudy;
else if (roll < 0.7f)
newWeather = WeatherType.Rain;
else if (roll < 0.85f)
newWeather = WeatherType.Fog;
else
newWeather = WeatherType.Snow;
SetWeather(newWeather);
}
// 设置天气
public void SetWeather(WeatherType weather)
{
_currentWeather = weather;
GD.Print($"天气变为:{weather}");
// 关闭所有效果
_rainParticles.Emitting = false;
_snowParticles.Emitting = false;
if (_fogVolume != null) _fogVisible = false;
// 根据天气类型开启对应效果
switch (weather)
{
case WeatherType.Clear:
SetEnvironment(1.0f, 400f); // 完全能见度
break;
case WeatherType.Cloudy:
SetEnvironment(0.85f, 350f);
break;
case WeatherType.Rain:
_rainParticles.Emitting = true;
SetEnvironment(0.6f, 200f); // 降低能见度
break;
case WeatherType.Snow:
_snowParticles.Emitting = true;
SetEnvironment(0.5f, 150f);
break;
case WeatherType.Fog:
if (_fogVolume != null) _fogVisible = true;
SetEnvironment(0.3f, 80f); // 大幅降低能见度
break;
}
EmitSignal(SignalName.WeatherChanged, (int)weather);
}
private bool _fogVisible
{
set { if (_fogVolume != null) _fogVolume.Visible = value; }
}
private void SetEnvironment(float visibility, float fogDepth)
{
var env = _worldEnv.Environment;
if (env != null)
{
// 调整雾效(模拟能见度)
env.FogLightColor = new Color(0.8f, 0.8f, 0.85f);
env.FogDepthBegin = fogDepth * 0.5f;
env.FogDepthEnd = fogDepth;
env.VolumetricFogDensity = (1f - visibility) * 0.1f;
}
}
public WeatherType GetCurrentWeather() => _currentWeather;
// 获取天气对摩擦力的影响系数
public float GetFrictionModifier()
{
return _currentWeather switch
{
WeatherType.Rain => 0.7f,
WeatherType.Snow => 0.5f,
_ => 1.0f
};
}
}GDScript
# 天气类型枚举
enum WeatherType { CLEAR, CLOUDY, RAIN, SNOW, FOG }
# 天气管理器
extends Node
signal weather_changed(new_weather: int)
@export var start_weather: int = WeatherType.CLEAR
@export var min_weather_duration: float = 30.0
@export var max_weather_duration: float = 120.0
var _current_weather: int
var _weather_timer: float = 0.0
var _next_weather_time: float
var _world_env: WorldEnvironment
var _rain_particles: GPUParticles3D
var _snow_particles: GPUParticles3D
var _fog_volume: FogVolume
func _ready():
_world_env = get_node("/root/Main/WorldEnvironment")
_rain_particles = $RainParticles
_snow_particles = $SnowParticles
_fog_volume = get_node_or_null("FogVolume")
set_weather(start_weather)
_next_weather_time = randf_range(min_weather_duration, max_weather_duration)
func _process(delta):
_weather_timer += delta
if _weather_timer >= _next_weather_time:
_weather_timer = 0.0
_next_weather_time = randf_range(min_weather_duration, max_weather_duration)
_random_weather_change()
# 随机切换天气
func _random_weather_change():
var roll = randf()
var new_weather: int
if roll < 0.3:
new_weather = WeatherType.CLEAR
elif roll < 0.5:
new_weather = WeatherType.CLOUDY
elif roll < 0.7:
new_weather = WeatherType.RAIN
elif roll < 0.85:
new_weather = WeatherType.FOG
else:
new_weather = WeatherType.SNOW
set_weather(new_weather)
# 设置天气
func set_weather(weather: int):
_current_weather = weather
print("天气变为:%d" % weather)
# 关闭所有效果
_rain_particles.emitting = false
_snow_particles.emitting = false
if _fog_volume:
_fog_volume.visible = false
# 根据天气类型开启对应效果
match weather:
WeatherType.CLEAR:
_set_environment(1.0, 400.0)
WeatherType.CLOUDY:
_set_environment(0.85, 350.0)
WeatherType.RAIN:
_rain_particles.emitting = true
_set_environment(0.6, 200.0)
WeatherType.SNOW:
_snow_particles.emitting = true
_set_environment(0.5, 150.0)
WeatherType.FOG:
if _fog_volume:
_fog_volume.visible = true
_set_environment(0.3, 80.0)
weather_changed.emit(weather)
func _set_environment(visibility: float, fog_depth: float):
var env = _world_env.environment
if env:
env.fog_light_color = Color(0.8, 0.8, 0.85)
env.fog_depth_begin = fog_depth * 0.5
env.fog_depth_end = fog_depth
env.volumetric_fog_density = (1.0 - visibility) * 0.1
func get_current_weather() -> int:
return _current_weather
# 获取天气对摩擦力的影响系数
func get_friction_modifier() -> float:
match _current_weather:
WeatherType.RAIN:
return 0.7
WeatherType.SNOW:
return 0.5
_:
return 1.0天气粒子特效
天气不只是改变物理参数,还需要看得见的效果——雨滴从天而降、雪花飘落、雾气弥漫。
雨滴粒子
RainParticles (GPUParticles3D)
├── amount: 2000
├── process_material:
│ ├── emission_shape: BOX (30, 20, 30)
│ ├── direction: (0, -1, 0)
│ ├── spread: 5
│ ├── gravity: (0, -30, 0)
│ └── initial_velocity: 15~20
└── draw_pass_1: 雨滴mesh (细长条形)雪花粒子
SnowParticles (GPUParticles3D)
├── amount: 1500
├── process_material:
│ ├── emission_shape: BOX (40, 20, 40)
│ ├── direction: (0, -1, 0)
│ ├── spread: 30 (雪花会飘)
│ ├── gravity: (0, -3, 0) (比雨慢很多)
│ └── turbulence_enabled: true (雪花乱飘)
└── draw_pass_1: 雪花mesh (小圆点)雾效
使用 Godot 的 FogVolume 节点:
FogVolume
├── size: (100, 30, 100)
├── shape: ShapeType.Box
├── density: 0.3
└── 跟随摄像机位置移动天气对物理的综合影响
最终的抓地力是路面材质和天气的综合效果:
计算公式很简单:
最终抓地力 = 路面摩擦系数 x 天气影响系数比如你在沙地上(摩擦 0.4)又下着雨(影响 0.7),最终抓地力 = 0.4 x 0.7 = 0.28,几乎是在冰上滑了。
常见问题
Q:天气变化太频繁怎么办?
调大 MinWeatherDuration 和 MaxWeatherDuration。也可以让某些赛道固定天气(比如沙漠永远是晴天,雪地永远下雪)。
Q:雨天看不清路怎么办?
这正是我们想要的效果!但可以在 HUD 上加一个"挡风玻璃雨刷"动画,或者用后处理效果模拟雨水打在镜头上的感觉。
Q:怎么让赛道的一部分有雪、一部分没雪?
把赛道分成多个区域,每个区域设置不同的地面组(surface_snow vs surface_dirt)。摩托车经过时,物理参数会自动切换。
下一步
地形天气完成后,开始 AI骑手。
