第二十二部分:天气系统基础:晴天、雨天、雪天、雾天、天气概率、天气视觉效果、天气存档
这一部分开始做天气系统。
前面我们已经有了:
时间系统
日期
季节
白天黑夜
每日刷新
睡觉到第二天
存档恢复时间
天气系统就可以接在时间系统上。
比如:
春天更容易下雨
夏天可能有暴雨
秋天可能有雾
冬天可能下雪
每天早上生成当天天气
天气影响画面色调、粒子、音效、可见度
这样世界就会更有生命感。
1. 天气系统要解决什么
天气系统主要负责:
当前是什么天气
天气什么时候变化
不同季节有什么天气概率
天气视觉效果怎么显示
天气音效怎么播放
天气是否保存到存档
室内是否隐藏天气效果
第一版不要一上来做复杂气象模拟。
先做:
晴天
阴天
雨天
暴雨
雪天
雾天
就够了。
2. 天气系统第一版目标
第一版建议做到:
1. WeatherManager 管理当前天气
2. 每天早上随机一次天气
3. 天气受季节影响
4. 天气变化时发 signal
5. WeatherController 根据天气显示视觉效果
6. 雨天显示雨粒子
7. 雪天显示雪粒子
8. 雾天显示雾层
9. 室内地图隐藏天气效果
10. 存档保存当前天气
11. 读档恢复天气
先不要做:
真实湿度
温度系统
风向系统
雷电伤害
地面积水
脚印
动态云层
作物受天气影响
NPC 根据天气换路线
这些都可以后面慢慢加。
第一版核心就是:
天气会变化
画面看得出来
存档能恢复
3. WeatherManager 放在哪里
天气属于全局世界状态,所以推荐做成 Autoload。
创建:
res://scripts/weather/weather_manager.gd
设置为 Autoload:
Project Settings
Globals
Autoload
添加 weather_manager.gd
命名 WeatherManager
这样任何地方都可以用:
WeatherManager.current_weather
WeatherManager.set_weather(WeatherManager.Weather.RAIN)
4. 天气枚举
WeatherManager.gd:
extends Node
signal weather_changed(weather: Weather)
signal weather_intensity_changed(intensity: float)
enum Weather {
CLEAR,
CLOUDY,
RAIN,
STORM,
SNOW,
FOG
}
var current_weather: Weather = Weather.CLEAR
var intensity: float = 1.0
含义:
CLEAR:晴天
CLOUDY:阴天
RAIN:雨天
STORM:暴雨 / 雷雨
SNOW:雪天
FOG:雾天
intensity 表示强度:
0.3:小雨 / 薄雾 / 小雪
0.7:中等
1.0:正常强度
1.5:强雨 / 大雪 / 浓雾
第一版可以先固定为 1.0。
5. 设置天气
func set_weather(weather: Weather, new_intensity: float = 1.0):
current_weather = weather
intensity = new_intensity
weather_changed.emit(current_weather)
weather_intensity_changed.emit(intensity)
print("天气变化:", get_weather_name())
显示名称:
func get_weather_name() -> String:
match current_weather:
Weather.CLEAR:
return "晴天"
Weather.CLOUDY:
return "阴天"
Weather.RAIN:
return "雨天"
Weather.STORM:
return "暴雨"
Weather.SNOW:
return "雪天"
Weather.FOG:
return "雾天"
return "未知"
这样 UI 可以显示:
天气:雨天
6. 天气什么时候变化
第一版最简单:
每天早上 6:00 随机一次天气
因为 TimeManager 已经有:
signal daily_refresh(day: int)
WeatherManager 可以监听它。
func _ready():
TimeManager.daily_refresh.connect(_on_daily_refresh)
func _on_daily_refresh(_day: int):
randomize_daily_weather()
这样睡觉到第二天后,会重新生成天气。
7. 按季节随机天气
不同季节天气概率不同。
比如:
春天:
晴天 40%
阴天 20%
雨天 30%
雾天 10%
夏天:
晴天 50%
阴天 15%
雨天 20%
暴雨 15%
秋天:
晴天 35%
阴天 25%
雨天 20%
雾天 20%
冬天:
晴天 30%
阴天 20%
雪天 40%
雾天 10%
这不用特别真实,先让不同季节有明显差异。
8. randomize_daily_weather()
func randomize_daily_weather():
match TimeManager.season:
TimeManager.Season.SPRING:
set_weather_by_weight({
Weather.CLEAR: 40,
Weather.CLOUDY: 20,
Weather.RAIN: 30,
Weather.FOG: 10,
})
TimeManager.Season.SUMMER:
set_weather_by_weight({
Weather.CLEAR: 50,
Weather.CLOUDY: 15,
Weather.RAIN: 20,
Weather.STORM: 15,
})
TimeManager.Season.AUTUMN:
set_weather_by_weight({
Weather.CLEAR: 35,
Weather.CLOUDY: 25,
Weather.RAIN: 20,
Weather.FOG: 20,
})
TimeManager.Season.WINTER:
set_weather_by_weight({
Weather.CLEAR: 30,
Weather.CLOUDY: 20,
Weather.SNOW: 40,
Weather.FOG: 10,
})
这里用了权重。
权重不是百分比也可以,只要比例对就行。
9. 按权重选择天气
func set_weather_by_weight(weights: Dictionary):
var total_weight := 0
for weight in weights.values():
total_weight += int(weight)
if total_weight <= 0:
set_weather(Weather.CLEAR)
return
var roll := randi_range(1, total_weight)
var current := 0
for weather in weights.keys():
current += int(weights[weather])
if roll <= current:
set_weather(weather, get_random_intensity(weather))
return
set_weather(Weather.CLEAR)
随机强度:
func get_random_intensity(weather: Weather) -> float:
match weather:
Weather.RAIN:
return randf_range(0.7, 1.2)
Weather.STORM:
return randf_range(1.2, 1.8)
Weather.SNOW:
return randf_range(0.6, 1.3)
Weather.FOG:
return randf_range(0.5, 1.4)
return 1.0
这样雨天有时小一点,有时大一点。
10. WeatherManager 完整基础版
extends Node
signal weather_changed(weather: Weather)
signal weather_intensity_changed(intensity: float)
enum Weather {
CLEAR,
CLOUDY,
RAIN,
STORM,
SNOW,
FOG
}
var current_weather: Weather = Weather.CLEAR
var intensity: float = 1.0
func _ready():
TimeManager.daily_refresh.connect(_on_daily_refresh)
func _on_daily_refresh(_day: int):
randomize_daily_weather()
func set_weather(weather: Weather, new_intensity: float = 1.0):
current_weather = weather
intensity = new_intensity
weather_changed.emit(current_weather)
weather_intensity_changed.emit(intensity)
print("天气变化:", get_weather_name())
func randomize_daily_weather():
match TimeManager.season:
TimeManager.Season.SPRING:
set_weather_by_weight({
Weather.CLEAR: 40,
Weather.CLOUDY: 20,
Weather.RAIN: 30,
Weather.FOG: 10,
})
TimeManager.Season.SUMMER:
set_weather_by_weight({
Weather.CLEAR: 50,
Weather.CLOUDY: 15,
Weather.RAIN: 20,
Weather.STORM: 15,
})
TimeManager.Season.AUTUMN:
set_weather_by_weight({
Weather.CLEAR: 35,
Weather.CLOUDY: 25,
Weather.RAIN: 20,
Weather.FOG: 20,
})
TimeManager.Season.WINTER:
set_weather_by_weight({
Weather.CLEAR: 30,
Weather.CLOUDY: 20,
Weather.SNOW: 40,
Weather.FOG: 10,
})
func set_weather_by_weight(weights: Dictionary):
var total_weight := 0
for weight in weights.values():
total_weight += int(weight)
if total_weight <= 0:
set_weather(Weather.CLEAR)
return
var roll := randi_range(1, total_weight)
var current := 0
for weather in weights.keys():
current += int(weights[weather])
if roll <= current:
set_weather(weather, get_random_intensity(weather))
return
set_weather(Weather.CLEAR)
func get_random_intensity(weather: Weather) -> float:
match weather:
Weather.RAIN:
return randf_range(0.7, 1.2)
Weather.STORM:
return randf_range(1.2, 1.8)
Weather.SNOW:
return randf_range(0.6, 1.3)
Weather.FOG:
return randf_range(0.5, 1.4)
return 1.0
func get_weather_name() -> String:
match current_weather:
Weather.CLEAR:
return "晴天"
Weather.CLOUDY:
return "阴天"
Weather.RAIN:
return "雨天"
Weather.STORM:
return "暴雨"
Weather.SNOW:
return "雪天"
Weather.FOG:
return "雾天"
return "未知"
11. 天气视觉效果放在哪里
推荐在 Main 下放一个 WeatherController。
结构:
Main
├── QuestManager
├── WorldContainer
├── Player
├── DayNightController 或 CanvasModulate
├── WeatherController CanvasLayer
└── UI CanvasLayer
为什么 WeatherController 用 CanvasLayer?
因为雨、雪、雾这类效果可以作为屏幕覆盖层:
雨点一直在屏幕前方落下
雪花覆盖整个画面
雾层覆盖整个视口
这样不用关心地图多大。
不过有些天气效果也可以放在世界里,比如地面积水、落叶、闪电照亮地图。
第一版先用屏幕覆盖层。
12. WeatherController 节点结构
WeatherController CanvasLayer
├── RainParticles GPUParticles2D
├── SnowParticles GPUParticles2D
├── FogLayer ColorRect
├── CloudLayer ColorRect
├── LightningLayer ColorRect
├── RainSound AudioStreamPlayer
├── StormSound AudioStreamPlayer
└── SnowSound AudioStreamPlayer
第一版可以先只做:
RainParticles
SnowParticles
FogLayer
CloudLayer
音效后面再加。
13. 为什么用 GPUParticles2D
雨和雪都是大量重复小粒子。
用粒子系统比你手动创建很多 Sprite 更合适。
Godot 里 2D 粒子常用:
GPUParticles2D
CPUParticles2D
第一版用 GPUParticles2D 就行。
雨天粒子方向:
从上往下
稍微倾斜
速度较快
数量较多
雪天粒子方向:
从上往下
速度较慢
轻微左右摆动
数量中等
14. WeatherController.gd
extends CanvasLayer
@onready var rain_particles: GPUParticles2D = $RainParticles
@onready var snow_particles: GPUParticles2D = $SnowParticles
@onready var fog_layer: ColorRect = $FogLayer
@onready var cloud_layer: ColorRect = $CloudLayer
@onready var lightning_layer: ColorRect = $LightningLayer
var weather_enabled: bool = true
func _ready():
WeatherManager.weather_changed.connect(_on_weather_changed)
WeatherManager.weather_intensity_changed.connect(_on_weather_intensity_changed)
apply_weather(WeatherManager.current_weather)
apply_intensity(WeatherManager.intensity)
func _on_weather_changed(weather):
apply_weather(weather)
func _on_weather_intensity_changed(intensity: float):
apply_intensity(intensity)
15. apply_weather()
func apply_weather(weather):
hide_all_weather_effects()
if not weather_enabled:
return
match weather:
WeatherManager.Weather.CLEAR:
pass
WeatherManager.Weather.CLOUDY:
cloud_layer.visible = true
WeatherManager.Weather.RAIN:
rain_particles.visible = true
rain_particles.emitting = true
cloud_layer.visible = true
WeatherManager.Weather.STORM:
rain_particles.visible = true
rain_particles.emitting = true
cloud_layer.visible = true
lightning_layer.visible = true
start_lightning_loop()
WeatherManager.Weather.SNOW:
snow_particles.visible = true
snow_particles.emitting = true
cloud_layer.visible = true
WeatherManager.Weather.FOG:
fog_layer.visible = true
cloud_layer.visible = true
隐藏所有效果:
func hide_all_weather_effects():
rain_particles.visible = false
rain_particles.emitting = false
snow_particles.visible = false
snow_particles.emitting = false
fog_layer.visible = false
cloud_layer.visible = false
lightning_layer.visible = false
16. apply_intensity()
天气强度可以影响:
雨粒子数量
雪粒子数量
雾透明度
阴天遮罩透明度
func apply_intensity(value: float):
var safe_intensity = max(value, 0.1)
rain_particles.amount = int(300 * safe_intensity)
snow_particles.amount = int(180 * safe_intensity)
fog_layer.modulate.a = clamp(0.25 * safe_intensity, 0.1, 0.65)
cloud_layer.modulate.a = clamp(0.18 * safe_intensity, 0.05, 0.45)
注意:
amount 太高会影响性能
先从小数值开始测试。
17. RainParticles 怎么设置
选中 RainParticles GPUParticles2D。
常见设置:
Emitting = false
Amount = 300
Lifetime = 1.0
One Shot = false
Preprocess = 1.0
Process Material 新建 ParticleProcessMaterial。
大致设置:
Direction = Vector3(0.25, 1, 0)
Spread = 8
Gravity = Vector3(0, 980, 0)
Initial Velocity Min = 500
Initial Velocity Max = 800
Scale Min = 0.5
Scale Max = 1.0
为了让雨覆盖屏幕,需要设置发射区域。
可以用:
Emission Shape = Box
Emission Box Extents = Vector3(800, 20, 0)
Position = 屏幕上方
比如 WeatherController 的 RainParticles 放到:
Position = (640, -50)
如果你的窗口是 1280x720,发射宽度至少要覆盖屏幕。
18. 雨滴贴图
雨粒子需要一个小贴图。
可以先用简单白色短线:
1x8 或 2x12 的浅蓝/白色竖线
挂到:
GPUParticles2D.texture
如果你没有贴图,雨可能看不明显。
第一版临时可以用一张很小的白色线条图。
19. SnowParticles 怎么设置
雪和雨类似,但速度慢,方向更柔。
Amount = 180
Lifetime = 4.0
Direction = Vector3(0.1, 1, 0)
Spread = 25
Gravity = Vector3(0, 80, 0)
Initial Velocity Min = 30
Initial Velocity Max = 90
Scale Min = 0.4
Scale Max = 1.2
雪花贴图可以是:
小白点
小圆形
简易雪花
第一版用小白点就行。
20. FogLayer 怎么做
FogLayer 可以先用 ColorRect。
设置:
Anchor = Full Rect
Color = 浅灰白
Alpha = 0.25 左右
visible = false
脚本里通过:
fog_layer.modulate.a = 0.35
控制浓度。
雾天最简单就是一层半透明灰白遮罩。
当然,这种雾比较平。
后面可以用噪声贴图或 Shader 做流动雾。
21. CloudLayer 怎么做
阴天也可以用 ColorRect。
设置:
Anchor = Full Rect
Color = 灰蓝色
Alpha = 0.15
visible = false
它的作用是:
晴天:不显示
阴天:画面稍微变灰
雨天:画面稍微变暗
暴雨:画面更暗
雪天:画面偏冷
注意它会和 DayNightController 的 CanvasModulate 叠加。
所以不要太重。
画面颜色叠太多,最后会像在泥水里玩游戏。
22. LightningLayer 闪电效果
暴雨可以加闪电。
最简单:
LightningLayer 是一个全屏白色 ColorRect
平时透明
随机时快速闪一下
LightningLayer 设置:
Color = 白色
Alpha = 0
visible = false
脚本:
var lightning_running: bool = false
func start_lightning_loop():
if lightning_running:
return
lightning_running = true
lightning_loop()
func stop_lightning_loop():
lightning_running = false
lightning_layer.modulate.a = 0.0
lightning_layer.visible = false
func lightning_loop():
while lightning_running and WeatherManager.current_weather == WeatherManager.Weather.STORM:
await get_tree().create_timer(randf_range(3.0, 8.0)).timeout
if not lightning_running:
return
lightning_layer.visible = true
lightning_layer.modulate.a = 0.7
await get_tree().create_timer(0.08).timeout
lightning_layer.modulate.a = 0.0
await get_tree().create_timer(0.08).timeout
lightning_layer.modulate.a = 0.45
await get_tree().create_timer(0.05).timeout
lightning_layer.modulate.a = 0.0
lightning_layer.visible = false
在 hide_all_weather_effects() 里记得:
stop_lightning_loop()
23. hide_all_weather_effects 加上闪电停止
func hide_all_weather_effects():
stop_lightning_loop()
rain_particles.visible = false
rain_particles.emitting = false
snow_particles.visible = false
snow_particles.emitting = false
fog_layer.visible = false
cloud_layer.visible = false
lightning_layer.visible = false
否则切出暴雨后,闪电循环可能还在跑。
异步循环这种东西很像“忘关的水龙头”,不马上炸,但迟早把厨房泡了。
24. 室内地图是否显示天气
一般来说:
室外:显示天气
室内:不显示雨雪粒子
地牢:通常不显示自然天气
洞穴:不显示天气,但可以有雾、灰尘
所以地图需要告诉系统:
这张地图是否启用天气视觉效果
MapBase.gd 里加:
@export var weather_enabled: bool = true
室外地图:
weather_enabled = true
室内地图:
weather_enabled = false
SceneManager 切地图后通知 WeatherController。
25. SceneManager 切地图后应用天气开关
SceneManager.setup_current_map() 或 change_map 后:
func apply_map_weather_settings():
var ui = main.get_node_or_null("UI")
if ui == null:
return
var weather_controller = main.get_node_or_null("WeatherController")
if weather_controller == null:
return
var enabled = true
if current_map != null and "weather_enabled" in current_map:
enabled = current_map.weather_enabled
if weather_controller.has_method("set_weather_enabled"):
weather_controller.set_weather_enabled(enabled)
如果 WeatherController 是 Main 的子节点:
Main
├── WeatherController
└── UI
就用:
main.get_node_or_null("WeatherController")
WeatherController 里:
func set_weather_enabled(enabled: bool):
weather_enabled = enabled
apply_weather(WeatherManager.current_weather)
apply_intensity(WeatherManager.intensity)
这样进入房子后雨雪隐藏,出来后恢复。
26. 室内是否能听到雨声
第一版可以直接隐藏天气和声音。
后面可以做:
室外:雨声正常
室内:雨声变小、变闷
洞穴:没有雨声
这需要音频总线或不同音效层。
第一版别急。
先让视觉效果对就够。
27. 天气 UI
可以在 TimeHUD 里加一个天气显示。
结构:
TimeHUD
├── DateLabel
├── TimeLabel
├── PhaseLabel
└── WeatherLabel
TimeHUD.gd:
@onready var weather_label: Label = $WeatherLabel
_ready 里:
WeatherManager.weather_changed.connect(_on_weather_changed)
_on_weather_changed(WeatherManager.current_weather)
方法:
func _on_weather_changed(_weather):
weather_label.text = "天气:" + WeatherManager.get_weather_name()
这样 UI 显示:
天气:雨天
28. 手动测试天气
开发期间可以先绑定几个按键。
InputMap:
debug_weather_clear
debug_weather_rain
debug_weather_snow
debug_weather_fog
debug_weather_storm
或者先在 Main.gd 里临时写:
func _unhandled_input(event):
if event.is_action_pressed("debug_weather_rain"):
WeatherManager.set_weather(WeatherManager.Weather.RAIN)
if event.is_action_pressed("debug_weather_snow"):
WeatherManager.set_weather(WeatherManager.Weather.SNOW)
if event.is_action_pressed("debug_weather_fog"):
WeatherManager.set_weather(WeatherManager.Weather.FOG)
if event.is_action_pressed("debug_weather_storm"):
WeatherManager.set_weather(WeatherManager.Weather.STORM)
if event.is_action_pressed("debug_weather_clear"):
WeatherManager.set_weather(WeatherManager.Weather.CLEAR)
调试天气特别需要快捷键。
不然你为了等一场雨,现实中也快下雨了。
29. 天气存档
天气需要保存:
current_weather
intensity
WeatherManager.gd:
func build_save_data() -> Dictionary:
return {
"current_weather": current_weather,
"intensity": intensity
}
恢复:
func apply_weather_data(data: Dictionary):
current_weather = data.get("current_weather", current_weather)
intensity = data.get("intensity", intensity)
weather_changed.emit(current_weather)
weather_intensity_changed.emit(intensity)
SaveManager 保存时:
"weather": WeatherManager.build_save_data()
读档时:
WeatherManager.apply_weather_data(save_data.get("weather", {}))
30. 读档顺序里天气放在哪里
上一节读档顺序是:
读取 save_data
恢复 TimeManager
恢复 SaveManager.world_state
切换到存档地图
恢复玩家数据
现在加上天气:
读取 save_data
恢复 TimeManager
恢复 WeatherManager
恢复 SaveManager.world_state
切换到存档地图
恢复玩家数据、背包、装备、任务
刷新 UI
为什么 WeatherManager 要在切地图前恢复?
因为切地图后 WeatherController 会根据当前地图的 weather_enabled 和当前天气应用效果。
所以最好先恢复天气,再加载地图。
31. SaveManager 里添加天气
保存:
var save_data := {
"version": 1,
"current_map_id": current_map_id,
"time": TimeManager.build_save_data(),
"weather": WeatherManager.build_save_data(),
"player": build_player_data(player),
"inventory": build_inventory_data(inventory),
"equipment": build_equipment_data(player),
"quests": build_quests_data(quest_manager),
"world_state": build_world_state_data()
}
读档:
TimeManager.apply_time_data(save_data.get("time", {}))
WeatherManager.apply_weather_data(save_data.get("weather", {}))
SaveManager.apply_world_state_data(save_data.get("world_state", {}))
SceneManager.change_map(map_id, "DefaultSpawn")
SaveManager.apply_save_data_after_map_loaded(save_data, player, quest_manager)
32. 天气和季节的关系
现在天气随机已经受季节影响。
以后可以继续扩展:
春天:雨多、雾多
夏天:晴天多、暴雨多
秋天:雾多、阴天多
冬天:雪多、晴天少
还可以让季节影响地图:
春天:草地鲜绿
夏天:颜色更浓
秋天:落叶、橙黄色
冬天:雪地贴图
但第一版先只影响天气概率。
季节贴图切换是一个独立章节,别现在一起搅成一锅八宝粥。
33. 天气和时间的关系
天气也可以一天中变化。
比如:
早上晴天
下午下雨
晚上雨停
但第一版建议:
每天一个天气
睡觉后刷新
这样最简单、最清楚。
后面可以加:
天气持续时间
天气预报
天气转变概率
雨下几个小时后停止
那时 WeatherManager 就需要:
var weather_remaining_minutes: int
每过一分钟减少。
现在不需要。
34. 天气对玩法的影响
第一版天气先只影响视觉。
后面可以慢慢加玩法影响:
雨天:
火焰伤害降低
雷电怪物出现
某些 NPC 不出门
泥地减速
雪天:
移动略慢
冰属性怪出现
脚印可见
雾天:
视野降低
怪物发现范围降低
幽灵类怪物出现
晴天:
普通状态
商人出摊
村民活动
暴雨:
可见度低
雷声
野外更危险
现在先不要让天气影响数值。
先让视觉跑通。
35. 雾天降低视野的简单做法
以后可以这样做:
雾天:
缩小怪物 DetectionArea
降低玩家视野
远处遮罩更重
第一版先只用 FogLayer。
如果要影响怪物发现距离,Enemy 可以监听天气:
func _ready():
WeatherManager.weather_changed.connect(_on_weather_changed)
func _on_weather_changed(weather):
if weather == WeatherManager.Weather.FOG:
detection_shape.shape.radius = normal_detection_radius * 0.6
else:
detection_shape.shape.radius = normal_detection_radius
这属于进阶玩法效果。
36. 雨天和室内地图
如果玩家在雨天进入房子:
室外天气还是雨天
室内不显示雨
出来后继续下雨
所以 WeatherManager 的天气不应该因为进屋改变。
只应该 WeatherController 根据地图决定是否显示。
也就是:
天气状态:全局存在
天气表现:根据当前地图显示或隐藏
这个区别很重要。
不要进入室内就:
WeatherManager.set_weather(Weather.CLEAR)
那玩家出门雨就没了。
应该:
weather_controller.set_weather_enabled(false)
37. 天气和 CanvasModulate 的叠加问题
你现在可能已经有:
DayNightController 控制 CanvasModulate
WeatherController 控制 CloudLayer / FogLayer
这两个会叠加。
建议分工:
DayNightController:
负责时间色调,比如早晨、白天、傍晚、夜晚
WeatherController:
负责天气覆盖,比如雨、雪、雾、阴天遮罩、闪电
不要两个系统都疯狂改同一个 CanvasModulate。
否则你会遇到:
夜晚想变暗
雨天也想变暗
雾天想变白
最后谁覆盖谁都不知道
第一版保持:
DayNightController 一个 CanvasModulate
WeatherController 多个覆盖层
结构清楚。
38. WeatherController 完整基础版
extends CanvasLayer
@onready var rain_particles: GPUParticles2D = $RainParticles
@onready var snow_particles: GPUParticles2D = $SnowParticles
@onready var fog_layer: ColorRect = $FogLayer
@onready var cloud_layer: ColorRect = $CloudLayer
@onready var lightning_layer: ColorRect = $LightningLayer
var weather_enabled: bool = true
var lightning_running: bool = false
func _ready():
WeatherManager.weather_changed.connect(_on_weather_changed)
WeatherManager.weather_intensity_changed.connect(_on_weather_intensity_changed)
apply_weather(WeatherManager.current_weather)
apply_intensity(WeatherManager.intensity)
func _on_weather_changed(weather):
apply_weather(weather)
func _on_weather_intensity_changed(value: float):
apply_intensity(value)
func set_weather_enabled(enabled: bool):
weather_enabled = enabled
apply_weather(WeatherManager.current_weather)
apply_intensity(WeatherManager.intensity)
func apply_weather(weather):
hide_all_weather_effects()
if not weather_enabled:
return
match weather:
WeatherManager.Weather.CLEAR:
pass
WeatherManager.Weather.CLOUDY:
cloud_layer.visible = true
WeatherManager.Weather.RAIN:
rain_particles.visible = true
rain_particles.emitting = true
cloud_layer.visible = true
WeatherManager.Weather.STORM:
rain_particles.visible = true
rain_particles.emitting = true
cloud_layer.visible = true
lightning_layer.visible = true
start_lightning_loop()
WeatherManager.Weather.SNOW:
snow_particles.visible = true
snow_particles.emitting = true
cloud_layer.visible = true
WeatherManager.Weather.FOG:
fog_layer.visible = true
cloud_layer.visible = true
func apply_intensity(value: float):
var safe_intensity = max(value, 0.1)
rain_particles.amount = int(300 * safe_intensity)
snow_particles.amount = int(180 * safe_intensity)
fog_layer.modulate.a = clamp(0.25 * safe_intensity, 0.1, 0.65)
cloud_layer.modulate.a = clamp(0.18 * safe_intensity, 0.05, 0.45)
func hide_all_weather_effects():
stop_lightning_loop()
rain_particles.visible = false
rain_particles.emitting = false
snow_particles.visible = false
snow_particles.emitting = false
fog_layer.visible = false
cloud_layer.visible = false
lightning_layer.visible = false
func start_lightning_loop():
if lightning_running:
return
lightning_running = true
lightning_loop()
func stop_lightning_loop():
lightning_running = false
if lightning_layer != null:
lightning_layer.modulate.a = 0.0
lightning_layer.visible = false
func lightning_loop():
while lightning_running and WeatherManager.current_weather == WeatherManager.Weather.STORM:
await get_tree().create_timer(randf_range(3.0, 8.0)).timeout
if not lightning_running:
return
lightning_layer.visible = true
lightning_layer.modulate.a = 0.7
await get_tree().create_timer(0.08).timeout
lightning_layer.modulate.a = 0.0
await get_tree().create_timer(0.08).timeout
lightning_layer.modulate.a = 0.45
await get_tree().create_timer(0.05).timeout
lightning_layer.modulate.a = 0.0
lightning_layer.visible = false
39. 常见错误 1:天气不变化
排查:
WeatherManager 是否是 Autoload
TimeManager.daily_refresh 是否发出
WeatherManager 是否连接 daily_refresh
sleep_to_next_day 是否触发 daily_refresh
randomize_daily_weather 是否执行
临时打印:
func randomize_daily_weather():
print("生成今日天气")
40. 常见错误 2:天气状态变了,但画面没变化
排查:
WeatherController 是否在 Main 下
WeatherController._ready 是否连接 weather_changed
RainParticles / SnowParticles / FogLayer 路径是否正确
weather_enabled 是否为 false
当前地图是否是室内
临时打印:
func apply_weather(weather):
print("应用天气:", weather)
41. 常见错误 3:进入室内后天气还在显示
排查:
MapBase.weather_enabled 是否设置为 false
SceneManager 切地图后是否调用 apply_map_weather_settings
WeatherController.set_weather_enabled(false) 是否执行
WeatherController 是否正确隐藏粒子
室内隐藏的是天气表现,不是 WeatherManager 的天气状态。
42. 常见错误 4:出门后雨没有恢复
排查:
室外 MapBase.weather_enabled 是否为 true
SceneManager 是否切地图后重新 set_weather_enabled(true)
WeatherController.apply_weather 是否根据当前 WeatherManager.current_weather 重建效果
不要在进屋时把天气设置成晴天。
43. 常见错误 5:暴雨切晴天后闪电还在闪
原因:
异步 lightning_loop 没停
检查:
hide_all_weather_effects 是否调用 stop_lightning_loop
stop_lightning_loop 是否设置 lightning_running = false
异步循环一定要有停止条件。
44. 常见错误 6:雨粒子只在屏幕一小块出现
排查:
RainParticles 的位置是否在屏幕上方
Emission Shape 是否是 Box
Emission Box Extents 是否足够宽
粒子 CanvasLayer 是否跟随屏幕
窗口分辨率是否比发射区域大
发射范围要覆盖整个视口。
45. 常见错误 7:雪花像子弹一样飞下来
检查:
Initial Velocity 是否太高
Gravity 是否太大
Lifetime 是否太短
Direction 是否太直
雪要慢一点,飘一点。
雨可以快,雪不要急着上班。
46. 常见错误 8:读档后天气不对
排查:
SaveManager 是否保存 weather
WeatherManager.build_save_data 是否正确
读档时是否 WeatherManager.apply_weather_data
apply_weather_data 是否 emit weather_changed
读档顺序是否在 SceneManager.change_map 前恢复天气
读档顺序推荐:
TimeManager
WeatherManager
world_state
change_map
player/inventory/equipment/quests
47. 当前推荐结构
Autoload:
SceneManager
SaveManager
ItemDatabase
QuestDatabase
TimeManager
WeatherManager
Main:
Main
├── QuestManager
├── WorldContainer
├── Player
├── DayNightController / CanvasModulate
├── WeatherController CanvasLayer
│ ├── RainParticles
│ ├── SnowParticles
│ ├── FogLayer
│ ├── CloudLayer
│ └── LightningLayer
└── UI CanvasLayer
├── PlayerHUD
├── TimeHUD
├── InventoryPanel
├── EquipmentPanel
├── QuestPanel
├── ShopPanel
├── DialogueBox
└── DeathPanel
MapBase:
@export var weather_enabled: bool = true
室内地图:
weather_enabled = false
室外地图:
weather_enabled = true
48. 这一部分最重要的记忆点
WeatherManager 管理天气状态
WeatherController 管理天气视觉效果
天气状态是全局的,天气表现要看当前地图是否启用
每天随机一次天气最适合第一版
天气概率可以根据季节变化
雨和雪适合用 GPUParticles2D
雾和阴天可以先用 ColorRect 覆盖层
暴雨可以加 LightningLayer 闪电
读档要恢复 weather
室内不要改 WeatherManager,只隐藏 WeatherController
DayNightController 负责时间色调,WeatherController 负责天气覆盖
49. 你现在的小练习
按这个顺序做:
1. 创建 WeatherManager.gd
2. 设置为 Autoload:WeatherManager
3. 添加 Weather 枚举
4. 添加 current_weather 和 intensity
5. 添加 set_weather()
6. 添加 get_weather_name()
7. 连接 TimeManager.daily_refresh
8. 实现 randomize_daily_weather()
9. 按季节设置不同天气权重
10. 创建 WeatherController CanvasLayer
11. 添加 RainParticles
12. 添加 SnowParticles
13. 添加 FogLayer ColorRect
14. 添加 CloudLayer ColorRect
15. 添加 LightningLayer ColorRect
16. WeatherController 监听 WeatherManager.weather_changed
17. 晴天隐藏所有效果
18. 雨天显示雨粒子和阴天遮罩
19. 雪天显示雪粒子
20. 雾天显示 FogLayer
21. 暴雨显示雨粒子和闪电
22. TimeHUD 添加 WeatherLabel
23. WeatherLabel 显示当前天气
24. MapBase 添加 weather_enabled
25. 室内地图设置 weather_enabled = false
26. SceneManager 切地图后调用 WeatherController.set_weather_enabled()
27. SaveManager 保存 weather
28. SaveManager 读档恢复 weather
29. 测试雨天进入室内,雨隐藏
30. 从室内出来,雨恢复
31. 测试夜晚雨天保存,重启读档后仍是夜晚雨天
完成这一部分后,你的 RPG 世界就会从“有时间”进一步变成“有气候”。
如果您觉得这篇文章有帮助,请点个赞吧~
评论
请登录后发表评论
去登录