第二十二部分:天气系统基础:晴天、雨天、雪天、雾天、天气概率、天气视觉效果、天气存档

2026-05-19
342612 分钟
...

这一部分开始做天气系统。

前面我们已经有了:

时间系统
日期
季节
白天黑夜
每日刷新
睡觉到第二天
存档恢复时间

天气系统就可以接在时间系统上。

比如:

春天更容易下雨
夏天可能有暴雨
秋天可能有雾
冬天可能下雪
每天早上生成当天天气
天气影响画面色调粒子音效可见度

这样世界就会更有生命感。

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. 设置为 AutoloadWeatherManager
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 世界就会从“有时间”进一步变成“有气候”。

如果您觉得这篇文章有帮助,请点个赞吧~

分享文章

相关文章

更多文章 →
godot2026-07-27
Godot 4 常用 UI 节点详解
Godot 4 常用 UI 节点详解 在 Godot 4 中,UI 系统基于 Control(控件) 节点构建。所有 UI 节点都继承自 Control,形成一棵完整的 UI 树。与游戏引擎中常见的"Canvas + DOM"模式不同,Godot 的 UI 系统是声明式的——你在场景中搭好节点树,引擎自动完成布局计算。 一、布局系统:Container 家族 Container 是 Godot UI 的 骨架 。它决定了子节点的大小和位...
学习
godot2026-07-09
Godot 4 自动地形系统(AutoTileSet / Terrains)完全指南
前言 在 2D 游戏开发中,地形瓦片(tile)的拼接是一个绕不开的问题。想象一下:你有一片草地、一条河流、一段平台——如果每一块边缘、角落、过渡区域都要手动选择对应的瓦片图,工作量将是巨大的。 自动地形系统 就是为了解决这个问题而生的。 一、什么是自动地形(Autotiling)? 自动地形的核心思想很简单: 你只管画,引擎帮你选对瓦片 。 当你在 TileMap 上绘制地形时,引擎会自动检测每个瓦片的上下左右邻居,然后根据预设的规则...
学习
godot2026-06-25
Godot 中 zindex 和 ysort 的区别总结
在 Godot 2D 游戏开发中,角色、树木、怪物、地面、技能特效、UI 都需要正确的显示顺序。比如角色走到树前面时,角色应该挡住树;角色走到树后面时,树又应该挡住角色。 这种显示顺序主要和两个概念有关: 和 。 其中 用来手动控制图层顺序, 用来根据物体的 Y 坐标自动排序。 一句话理解 是手动分层。 是根据 Y 坐标自动排序。 简单来说: | 属性 | 作用 | 适合场景 | | | | | | | 数值越大,显示越靠前 | 地面、...
学习
godot2026-06-11
Tileset 资源图的标准和规范
一、什么是 Tileset 资源图 Tileset,中文通常叫“图块资源图”或“瓦片图”,是 2D 游戏中非常常见的一种地图资源组织方式。 简单来说,Tileset 就是把很多小图块按照固定尺寸排列在一张图片里。游戏引擎会按照固定的格子大小去切割这张图片,然后把每个小格子当成一个独立的地图块使用。 比如一个 32×32 像素的 Tileset 中,每一个 tile 都是 32×32 像素。地图编辑器或游戏引擎会按照 32×32 的网格,...
学习
godot2026-05-29
用户角色精灵图制作角色的完整流程
在 2D RPG 游戏里,角色通常不是用一张单独图片完成的,而是用一张“角色精灵图”来做。 所谓角色精灵图,通常是一张包含多个动作帧的大图。比如角色向下走有 4 帧,向左走有 4 帧,向右走有 4 帧,向上走有 4 帧。Godot 会根据这些帧不断切换图片,看起来角色就动起来了。 这篇文章主要介绍:拿到一张角色精灵图之后,如何在 Godot 中把它做成一个可以正常移动、播放动画、和地图产生遮挡关系的角色。 一、先理解角色精灵图是什么 角...
学习
godot2026-05-26
Godot 节点系统详细介绍
Godot 里最核心的东西不是“类”,也不是“组件”,而是 节点 Node 。 你可以把 Godot 的节点理解成: 在前端里,一个页面是由很多 DOM 元素组成的; 在 Godot 里,一个游戏场景是由很多 Node 节点组成的。 比如一个玩家角色,可能不是一个单独对象,而是这样的结构: 这里的 是根节点,下面挂着显示图片、播放动画、碰撞检测、摄像机、音效等子节点。 Godot 官方文档也把节点和场景放在一起讲:多个节点组成树状结构后...
学习

评论

请登录后发表评论

去登录
加载评论中...