第二十一部分:时间系统基础:游戏内时钟、白天黑夜、日期、季节、每日刷新、睡觉存档
这一部分开始给游戏世界加“时间感”。
前面我们的世界已经有:
地图
NPC
怪物
任务
商店
背包
装备
存档
地图状态
但它还是比较静态:
永远是同一个白天
NPC 永远站在那里
草药采了就永久没了
商店永远不刷新
玩家没有“睡觉到第二天”的感觉
时间系统要解决的就是:
让世界开始流动
比如:
早晨
白天
傍晚
夜晚
第 1 天
第 2 天
第 3 天
春天
夏天
秋天
冬天
每天刷新采集点
每天刷新商店库存
睡觉保存游戏
NPC 根据时间改变位置
夜晚出现特殊怪物
这套系统一出来,你的 RPG 就会开始有“生活模拟”的味道。
1. 游戏时间和真实时间不是一回事
先分清两个概念:
真实时间:
现实世界过去了多少秒
游戏时间:
游戏世界里过去了多少分钟、小时、天
比如:
现实 1 秒 = 游戏 1 分钟
现实 60 秒 = 游戏 1 小时
现实 24 分钟 = 游戏 1 天
这就是很多 RPG / 农场游戏 / 模拟经营游戏的时间压缩。
不要用电脑系统时间直接当游戏时间。
原因:
玩家可以暂停
游戏可以加速
读档要恢复
睡觉可以直接跳到第二天
剧情可以锁定时间
不同地图可以控制时间流动
Godot 的 Time 单例可以读取系统时间,也能提供单调递增的 ticks;官方文档特别提醒,系统时间可能被用户或操作系统调整,做精确计时应使用 get_ticks_usec() 或 get_ticks_msec() 这类单调时间。游戏内日夜循环这种系统,通常更适合自己维护一套“游戏时间”,而不是直接依赖系统时钟。(Godot Engine documentation)
2. 第一版时间系统要做什么
第一版先做:
游戏内分钟、小时、天数
时间自动流逝
白天 / 傍晚 / 夜晚阶段
UI 显示当前时间
睡觉跳到第二天早上
每日刷新事件
存档保存时间
读档恢复时间
先不要做:
复杂 NPC 日程
节日
天气概率
月相
农作物生长
商店营业时间
夜晚特殊事件
季节地图贴图切换
这些后面再慢慢加。
第一版目标是:
世界有时间
时间能显示
时间能推进
时间能跨天
每天能触发一次刷新
3. TimeManager 放在哪里
时间系统建议做成 Autoload。
因为时间属于全局游戏状态:
玩家在哪张地图,时间都应该存在
切地图不能重置时间
存档要保存时间
UI 要随时显示时间
每日刷新要全局通知
创建:
res://scripts/time/time_manager.gd
设置为 Autoload:
Project Settings
Globals
Autoload
添加 time_manager.gd
命名 TimeManager
之后任何地方都可以调用:
TimeManager.hour
TimeManager.day
TimeManager.sleep_to_next_day()
4. TimeManager 需要哪些数据
第一版需要:
var minute: int = 0
var hour: int = 6
var day: int = 1
var season: Season = Season.SPRING
var year: int = 1
意思是:
第 1 年
春季
第 1 天
早上 6:00
为什么从 6 点开始?
因为很多 RPG / 农场游戏习惯让一天从早晨开始。
比如:
06:00 起床
12:00 中午
18:00 傍晚
22:00 夜晚
第一版从 6:00 开始最自然。
5. Season 季节枚举
enum Season {
SPRING,
SUMMER,
AUTUMN,
WINTER
}
显示时可以转成中文:
func get_season_name() -> String:
match season:
Season.SPRING:
return "春"
Season.SUMMER:
return "夏"
Season.AUTUMN:
return "秋"
Season.WINTER:
return "冬"
return "未知"
第一版季节先只是显示。
后面可以让季节影响:
地图颜色
天气概率
可采集物
怪物种类
商店商品
农作物生长
节日
6. 时间流速
我们定义:
@export var real_seconds_per_game_minute: float = 1.0
意思是:
现实 1 秒 = 游戏 1 分钟
如果你想一天更快:
real_seconds_per_game_minute = 0.5
那就是:
现实 0.5 秒 = 游戏 1 分钟
现实 12 分钟 = 游戏 1 天
如果你想一天更慢:
real_seconds_per_game_minute = 2.0
那就是:
现实 2 秒 = 游戏 1 分钟
现实 48 分钟 = 游戏 1 天
第一版建议:
现实 1 秒 = 游戏 1 分钟
测试起来比较方便。
7. 用 _process(delta) 推进游戏时间
TimeManager.gd:
extends Node
signal time_changed(hour: int, minute: int)
signal day_changed(day: int)
signal season_changed(season: Season)
signal year_changed(year: int)
signal day_phase_changed(phase: DayPhase)
enum Season {
SPRING,
SUMMER,
AUTUMN,
WINTER
}
enum DayPhase {
MORNING,
DAY,
EVENING,
NIGHT
}
@export var real_seconds_per_game_minute: float = 1.0
var minute: int = 0
var hour: int = 6
var day: int = 1
var year: int = 1
var season: Season = Season.SPRING
var is_time_paused: bool = false
var _accumulator: float = 0.0
var current_phase: DayPhase = DayPhase.MORNING
func _process(delta):
if is_time_paused:
return
_accumulator += delta
while _accumulator >= real_seconds_per_game_minute:
_accumulator -= real_seconds_per_game_minute
add_game_minutes(1)
这里用 _accumulator 累积真实时间。
比如:
delta 每帧大约 0.016 秒
累积到 1.0 秒
游戏时间 +1 分钟
8. add_game_minutes()
func add_game_minutes(amount: int):
if amount <= 0:
return
for i in range(amount):
minute += 1
if minute >= 60:
minute = 0
hour += 1
if hour >= 24:
hour = 0
advance_day()
time_changed.emit(hour, minute)
update_day_phase()
这里是一分钟一分钟推进。
这样做的好处是:
如果一次加很多分钟
仍然能正确跨小时、跨天、触发阶段变化
当然,后面如果大量快进时间,可以优化。
第一版这样最清楚。
9. advance_day()
@export var days_per_season: int = 30
func advance_day():
day += 1
day_changed.emit(day)
if day > days_per_season:
day = 1
advance_season()
注意这里有个细节:
先 day += 1
如果超过本季天数
就进入下一个季节
如果 days_per_season = 30:
春 1
春 2
...
春 30
夏 1
10. advance_season()
func advance_season():
match season:
Season.SPRING:
season = Season.SUMMER
Season.SUMMER:
season = Season.AUTUMN
Season.AUTUMN:
season = Season.WINTER
Season.WINTER:
season = Season.SPRING
year += 1
year_changed.emit(year)
season_changed.emit(season)
这样季节顺序就是:
春 → 夏 → 秋 → 冬 → 下一年春
11. update_day_phase()
一天可以分成几个阶段:
早晨:06:00 - 11:59
白天:12:00 - 16:59
傍晚:17:00 - 19:59
夜晚:20:00 - 05:59
代码:
func update_day_phase():
var next_phase := get_day_phase()
if next_phase == current_phase:
return
current_phase = next_phase
day_phase_changed.emit(current_phase)
func get_day_phase() -> DayPhase:
if hour >= 6 and hour < 12:
return DayPhase.MORNING
if hour >= 12 and hour < 17:
return DayPhase.DAY
if hour >= 17 and hour < 20:
return DayPhase.EVENING
return DayPhase.NIGHT
这样其他系统可以监听:
TimeManager.day_phase_changed.connect(_on_day_phase_changed)
比如:
夜晚生成怪物
傍晚点亮路灯
早晨刷新 NPC
12. 完整 TimeManager 基础版
extends Node
signal time_changed(hour: int, minute: int)
signal day_changed(day: int)
signal season_changed(season: Season)
signal year_changed(year: int)
signal day_phase_changed(phase: DayPhase)
enum Season {
SPRING,
SUMMER,
AUTUMN,
WINTER
}
enum DayPhase {
MORNING,
DAY,
EVENING,
NIGHT
}
@export var real_seconds_per_game_minute: float = 1.0
@export var days_per_season: int = 30
var minute: int = 0
var hour: int = 6
var day: int = 1
var year: int = 1
var season: Season = Season.SPRING
var is_time_paused: bool = false
var _accumulator: float = 0.0
var current_phase: DayPhase = DayPhase.MORNING
func _ready():
current_phase = get_day_phase()
emit_time_signals()
func _process(delta):
if is_time_paused:
return
_accumulator += delta
while _accumulator >= real_seconds_per_game_minute:
_accumulator -= real_seconds_per_game_minute
add_game_minutes(1)
func add_game_minutes(amount: int):
if amount <= 0:
return
for i in range(amount):
minute += 1
if minute >= 60:
minute = 0
hour += 1
if hour >= 24:
hour = 0
advance_day()
time_changed.emit(hour, minute)
update_day_phase()
func advance_day():
day += 1
day_changed.emit(day)
if day > days_per_season:
day = 1
advance_season()
func advance_season():
match season:
Season.SPRING:
season = Season.SUMMER
Season.SUMMER:
season = Season.AUTUMN
Season.AUTUMN:
season = Season.WINTER
Season.WINTER:
season = Season.SPRING
year += 1
year_changed.emit(year)
season_changed.emit(season)
func get_day_phase() -> DayPhase:
if hour >= 6 and hour < 12:
return DayPhase.MORNING
if hour >= 12 and hour < 17:
return DayPhase.DAY
if hour >= 17 and hour < 20:
return DayPhase.EVENING
return DayPhase.NIGHT
func update_day_phase():
var next_phase := get_day_phase()
if next_phase == current_phase:
return
current_phase = next_phase
day_phase_changed.emit(current_phase)
func emit_time_signals():
time_changed.emit(hour, minute)
day_changed.emit(day)
season_changed.emit(season)
year_changed.emit(year)
day_phase_changed.emit(current_phase)
13. 时间 UI
在 UI 的 PlayerHUD 或单独 TimeHUD 里添加:
TimeHUD Control
├── DateLabel Label
├── TimeLabel Label
└── PhaseLabel Label
推荐单独做:
TimeHUD
结构:
UI CanvasLayer
├── PlayerHUD
├── TimeHUD
│ ├── DateLabel
│ ├── TimeLabel
│ └── PhaseLabel
├── InventoryPanel
├── QuestPanel
└── ...
14. TimeHUD.gd
extends Control
@onready var date_label: Label = $DateLabel
@onready var time_label: Label = $TimeLabel
@onready var phase_label: Label = $PhaseLabel
func _ready():
TimeManager.time_changed.connect(_on_time_changed)
TimeManager.day_changed.connect(_on_day_changed)
TimeManager.season_changed.connect(_on_season_changed)
TimeManager.year_changed.connect(_on_year_changed)
TimeManager.day_phase_changed.connect(_on_day_phase_changed)
refresh_all()
func refresh_all():
_on_time_changed(TimeManager.hour, TimeManager.minute)
_on_day_changed(TimeManager.day)
_on_season_changed(TimeManager.season)
_on_year_changed(TimeManager.year)
_on_day_phase_changed(TimeManager.current_phase)
func _on_time_changed(hour: int, minute: int):
time_label.text = "%02d:%02d" % [hour, minute]
func _on_day_changed(_day: int):
update_date_label()
func _on_season_changed(_season):
update_date_label()
func _on_year_changed(_year: int):
update_date_label()
func update_date_label():
date_label.text = "第 %d 年 %s 第 %d 天" % [
TimeManager.year,
TimeManager.get_season_name(),
TimeManager.day
]
func _on_day_phase_changed(phase):
phase_label.text = TimeManager.get_phase_name()
这里用了:
TimeManager.get_season_name()
TimeManager.get_phase_name()
所以 TimeManager 里补这两个方法。
15. TimeManager 补显示方法
func get_season_name() -> String:
match season:
Season.SPRING:
return "春"
Season.SUMMER:
return "夏"
Season.AUTUMN:
return "秋"
Season.WINTER:
return "冬"
return "未知"
func get_phase_name() -> String:
match current_phase:
DayPhase.MORNING:
return "早晨"
DayPhase.DAY:
return "白天"
DayPhase.EVENING:
return "傍晚"
DayPhase.NIGHT:
return "夜晚"
return "未知"
16. 暂停时间
有些情况下时间应该暂停:
打开菜单
打开商店
打开背包
对话中
剧情演出
暂停界面
可以提供:
func pause_time():
is_time_paused = true
func resume_time():
is_time_paused = false
然后 ShopPanel 打开时:
TimeManager.pause_time()
关闭时:
TimeManager.resume_time()
DialogueBox 开始时:
TimeManager.pause_time()
结束时:
TimeManager.resume_time()
不过第一版要小心一个问题:
多个 UI 同时打开时,一个关闭就 resume_time,可能不对
比如:
打开背包 → pause
又打开装备面板 → pause
关闭装备面板 → resume
但背包还开着
所以更稳的方式是用暂停计数。
17. 时间暂停计数
TimeManager:
var pause_count: int = 0
func pause_time():
pause_count += 1
is_time_paused = pause_count > 0
func resume_time():
pause_count = max(pause_count - 1, 0)
is_time_paused = pause_count > 0
func force_resume_time:
pause_count = 0
is_time_paused = false
注意 force_resume_time 要写成函数:
func force_resume_time():
pause_count = 0
is_time_paused = false
这样多个系统都可以安全暂停。
比如:
商店打开:pause_count = 1
背包打开:pause_count = 2
关闭背包:pause_count = 1,仍然暂停
关闭商店:pause_count = 0,恢复
这比一个简单 bool 更稳。
18. 睡觉系统
睡觉是时间系统的重要入口。
睡觉通常做几件事:
跳到第二天早晨
恢复玩家生命
触发每日刷新
保存游戏
关闭 UI
比如床 Bed 是一个可交互对象。
结构:
Bed Area2D
└── CollisionShape2D
Bed.gd:
extends Area2D
@export var prompt_text: String = "按 E 睡觉"
func interact(player: Node):
TimeManager.sleep_to_next_day()
if player.has_method("heal"):
player.heal(player.max_hp)
SaveManager.save_game(player, get_tree().current_scene.get_node("QuestManager"), SceneManager.current_map_id)
这能跑,但 Bed 直接找 QuestManager 不太优雅。
更好是:
Bed 只发出 sleep 请求
Main 或 GameManager 处理睡觉流程
第一版可以先简单做,后面再优化。
19. TimeManager.sleep_to_next_day()
signal before_sleep
signal after_sleep
signal daily_refresh(day: int)
func sleep_to_next_day():
before_sleep.emit()
hour = 6
minute = 0
advance_day()
current_phase = get_day_phase()
time_changed.emit(hour, minute)
day_phase_changed.emit(current_phase)
daily_refresh.emit(day)
after_sleep.emit()
这里做了:
发出 before_sleep
时间跳到 06:00
天数 +1
更新时间阶段
发出 daily_refresh
发出 after_sleep
注意:这里直接调用 advance_day()。
也就是说:
睡觉一定进入下一天
20. daily_refresh 是干什么的
每日刷新事件很关键。
很多系统都可以监听它:
采集点刷新
商店库存刷新
怪物刷新
NPC 日程重置
天气重新随机
任务每日进度刷新
比如:
TimeManager.daily_refresh.connect(_on_daily_refresh)
这比让每个系统自己判断日期变化更干净。
21. 采集点每日刷新
上一部分我们做的采集点是永久消失:
采集后永远不出现
现在如果想让草药第二天刷新,可以改成:
采集后当天消失
第二天刷新
这时不能只保存 true。
要保存采集日期。
SaveManager 的 collected_nodes 可以从:
"forest_herb_node_001": true
升级为:
"forest_herb_node_001": {
"day": 3,
"season": 0,
"year": 1
}
不过第一版可以不改存档结构,先做“不保存采集点,切地图刷新”。
如果你想做每日刷新,就要稍微复杂一点。
22. GatherNode 每日刷新版
先让 SaveManager 支持设置复杂状态:
func set_state_data(category: String, persistent_id: String, data: Dictionary):
if persistent_id == "":
return
if not world_state.has(category):
world_state[category] = {}
world_state[category][persistent_id] = data
func get_state_data(category: String, persistent_id: String) -> Dictionary:
if persistent_id == "":
return {}
if not world_state.has(category):
return {}
return world_state[category].get(persistent_id, {})
GatherNode:
extends Area2D
@export var persistent_id: String = ""
@export var prompt_text: String = "按 E 采集"
@export var item: ItemData
@export var quantity: int = 1
@export var respawn_after_days: int = 1
var collected: bool = false
func _ready():
apply_saved_state()
func apply_saved_state():
var data = SaveManager.get_state_data("collected_nodes", persistent_id)
if data.is_empty():
collected = false
visible = true
monitoring = true
return
var collected_day: int = data.get("day", 0)
var collected_year: int = data.get("year", 1)
var collected_season: int = data.get("season", TimeManager.Season.SPRING)
var days_passed = TimeManager.get_total_days() - TimeManager.get_total_days_from(collected_year, collected_season, collected_day)
if days_passed >= respawn_after_days:
collected = false
visible = true
monitoring = true
else:
collected = true
visible = false
monitoring = false
func interact(player: Node):
if collected:
return
collected = true
if player.has_method("pick_up_item"):
player.pick_up_item(item, quantity)
SaveManager.set_state_data("collected_nodes", persistent_id, {
"year": TimeManager.year,
"season": TimeManager.season,
"day": TimeManager.day
})
visible = false
monitoring = false
这个版本需要 TimeManager.get_total_days()。
23. TimeManager 总天数计算
func get_total_days() -> int:
return get_total_days_from(year, season, day)
func get_total_days_from(p_year: int, p_season: int, p_day: int) -> int:
var seasons_per_year := 4
return ((p_year - 1) * seasons_per_year * days_per_season) + (p_season * days_per_season) + p_day
比如:
第 1 年 春 第 1 天 = 1
第 1 年 春 第 2 天 = 2
第 1 年 夏 第 1 天 = 31
这样就能比较两个日期相差多少天。
24. 白天黑夜视觉效果
2D 游戏里做日夜效果,常见简单方案是:
CanvasModulate
在地图或 World 下面加:
CanvasModulate
它会给 2D 场景整体叠加颜色。
比如:
白天:Color(1, 1, 1, 1)
傍晚:Color(1.0, 0.75, 0.55, 1)
夜晚:Color(0.25, 0.3, 0.55, 1)
早晨:Color(0.9, 0.85, 0.75, 1)
第一版可以根据 DayPhase 直接切颜色。
更高级可以根据时间平滑渐变。
25. DayNightController
放在 Main 或 UI/World 下都可以。
结构:
Main
├── WorldContainer
├── Player
├── CanvasModulate
└── UI
脚本:
extends CanvasModulate
func _ready():
TimeManager.day_phase_changed.connect(_on_day_phase_changed)
apply_phase(TimeManager.current_phase)
func _on_day_phase_changed(phase):
apply_phase(phase)
func apply_phase(phase):
match phase:
TimeManager.DayPhase.MORNING:
color = Color(0.9, 0.85, 0.75, 1.0)
TimeManager.DayPhase.DAY:
color = Color(1.0, 1.0, 1.0, 1.0)
TimeManager.DayPhase.EVENING:
color = Color(1.0, 0.72, 0.48, 1.0)
TimeManager.DayPhase.NIGHT:
color = Color(0.32, 0.36, 0.58, 1.0)
这会让场景整体变色。
第一版很够用。
26. 平滑过渡
直接切颜色会有点硬。
可以用 Tween 平滑过渡。
Godot 4 里可以用 create_tween() 创建 Tween。
func apply_phase(phase):
var target_color := get_color_for_phase(phase)
var tween = create_tween()
tween.tween_property(self, "color", target_color, 2.0)
func get_color_for_phase(phase) -> Color:
match phase:
TimeManager.DayPhase.MORNING:
return Color(0.9, 0.85, 0.75, 1.0)
TimeManager.DayPhase.DAY:
return Color(1.0, 1.0, 1.0, 1.0)
TimeManager.DayPhase.EVENING:
return Color(1.0, 0.72, 0.48, 1.0)
TimeManager.DayPhase.NIGHT:
return Color(0.32, 0.36, 0.58, 1.0)
return Color(1, 1, 1, 1)
第一版直接切,第二版再 Tween。
27. Timer 和 SceneTreeTimer 在时间系统里的位置
注意,游戏内时间流逝不一定要用 Timer 节点。
我们现在用 _process(delta) 自己累积时间。
这很适合做:
游戏内时钟
可暂停
可加速
可存档
可跳转
Timer 更适合做:
每隔几秒执行一次
冷却
延迟
一次性等待
Godot 的 Timer 节点会倒计时并在结束时发出 timeout 信号,文档也提醒,想创建不需要节点的一次性计时器可以用 SceneTree.create_timer();Timer 还会受到 Engine.time_scale 影响。(Godot Engine documentation)
而 SceneTreeTimer 是由 SceneTree 管理的一次性计时器,不需要创建 Timer 节点,常见写法就是 await get_tree().create_timer(1.0).timeout。它很适合临时延迟,不太适合做整个世界的日期系统。(Godot Engine documentation)
所以你可以这样分工:
TimeManager:
管理游戏内时间、日期、季节
Timer / SceneTreeTimer:
做冷却、延迟、动画等待、提示消失
28. 时间加速和暂停
调试时可以加几个方法:
func set_time_scale(multiplier: float):
real_seconds_per_game_minute = 1.0 / max(multiplier, 0.01)
比如:
TimeManager.set_time_scale(10.0)
表示:
游戏时间速度 x10
但注意,这里的 time_scale 是你自己的游戏时间比例,不是 Engine.time_scale。
不要一开始就改:
Engine.time_scale
因为它会影响物理、动画、Timer 等很多东西。
我们现在只想让游戏内时钟变快,不想让玩家、怪物、攻击动画全都变快。
所以自己控制 TimeManager 的流速更安全。
29. 夜晚刷怪
以后可以做:
夜晚出现幽灵
白天消失
简单版:
func _ready():
TimeManager.day_phase_changed.connect(_on_day_phase_changed)
update_night_spawn()
func _on_day_phase_changed(_phase):
update_night_spawn()
func update_night_spawn():
if TimeManager.current_phase == TimeManager.DayPhase.NIGHT:
spawn_night_enemies()
else:
despawn_night_enemies()
第一版可以先不做。
但这个就是时间系统的价值:它能驱动世界变化。
30. 商店每日刷新
商店库存如果有限,可以监听:
TimeManager.daily_refresh.connect(_on_daily_refresh)
ShopData 或 ShopNPC 可以恢复库存。
例如:
func _on_daily_refresh(_day: int):
restock()
func restock():
for shop_item in shop_data.items:
if not shop_item.infinite_stock:
shop_item.stock = shop_item.default_stock
这需要 ShopItemData 增加:
@export var default_stock: int = 1
第一版商店无限库存,所以可以先不做。
31. 睡觉存档
睡觉通常是最佳存档点。
流程建议:
玩家与床交互
显示确认:要睡觉吗?
确认后:
关闭 UI
淡出
TimeManager.sleep_to_next_day()
恢复玩家生命
每日刷新
保存游戏
淡入
第一版可以简化:
按 E 睡觉
直接到第二天 6:00
恢复满血
保存
Bed.gd:
extends Area2D
@export var prompt_text: String = "按 E 睡觉"
func interact(player: Node):
if player == null:
return
TimeManager.sleep_to_next_day()
if player.has_method("heal"):
player.heal(player.max_hp)
var quest_manager = get_tree().current_scene.get_node_or_null("QuestManager")
if quest_manager != null:
SaveManager.save_game(player, quest_manager, SceneManager.current_map_id)
等你后面有 GameManager,再把这套流程集中管理。
32. 保存时间
SaveManager 里加时间数据。
保存:
func build_time_data() -> Dictionary:
return {
"minute": TimeManager.minute,
"hour": TimeManager.hour,
"day": TimeManager.day,
"season": TimeManager.season,
"year": TimeManager.year
}
save_data 里加:
"time": build_time_data()
33. 读取时间
TimeManager 加:
func apply_time_data(data: Dictionary):
minute = data.get("minute", minute)
hour = data.get("hour", hour)
day = data.get("day", day)
season = data.get("season", season)
year = data.get("year", year)
current_phase = get_day_phase()
emit_time_signals()
SaveManager 读档时:
TimeManager.apply_time_data(save_data.get("time", {}))
注意读档顺序:
读取 save_data
恢复 TimeManager
恢复 world_state
切地图
恢复玩家数据
为什么时间要在地图前恢复?
因为地图对象可能根据时间决定状态。
比如:
采集点是否刷新
夜晚怪物是否出现
NPC 是否在家
所以更稳的顺序是:
1. read_save_data
2. TimeManager.apply_time_data
3. SaveManager.apply_world_state_data
4. SceneManager.change_map
5. apply player/inventory/equipment/quests
34. 新游戏初始化时间
TimeManager 加:
func reset_to_new_game():
minute = 0
hour = 6
day = 1
year = 1
season = Season.SPRING
_accumulator = 0.0
current_phase = get_day_phase()
pause_count = 0
is_time_paused = false
emit_time_signals()
新游戏时调用:
TimeManager.reset_to_new_game()
SaveManager.clear_world_state()
35. TimeManager 存档相关完整片段
func build_save_data() -> Dictionary:
return {
"minute": minute,
"hour": hour,
"day": day,
"season": season,
"year": year
}
func apply_time_data(data: Dictionary):
minute = data.get("minute", minute)
hour = data.get("hour", hour)
day = data.get("day", day)
season = data.get("season", season)
year = data.get("year", year)
_accumulator = 0.0
current_phase = get_day_phase()
emit_time_signals()
func reset_to_new_game():
minute = 0
hour = 6
day = 1
year = 1
season = Season.SPRING
_accumulator = 0.0
current_phase = get_day_phase()
pause_count = 0
is_time_paused = false
emit_time_signals()
SaveManager 里:
func build_time_data() -> Dictionary:
return TimeManager.build_save_data()
save_data:
"time": build_time_data()
load:
TimeManager.apply_time_data(save_data.get("time", {}))
36. 时间和存档顺序总结
保存时:
保存 current_map_id
保存 player
保存 inventory
保存 equipment
保存 quests
保存 world_state
保存 time
读档时:
读取 save_data
恢复 TimeManager
恢复 SaveManager.world_state
切换到存档地图
恢复 player
恢复 inventory
恢复 equipment
恢复 quests
刷新 UI
这个顺序很重要。
尤其是:
时间和 world_state 要先恢复
地图再加载
否则地图对象在 _ready() 里读取到的是旧时间、旧世界状态。
37. 时间系统和暂停菜单
如果你有 PausePanel,打开时:
TimeManager.pause_time()
关闭时:
TimeManager.resume_time()
但是如果你还使用 Godot 的暂停树:
get_tree().paused = true
那要注意:
节点的 process 可能暂停
Timer 也可能受影响
UI 是否还能响应取决于 process_mode
第一版不建议你立刻用 get_tree().paused = true 做所有暂停。
先用:
TimeManager.pause_time()
Player.lock_control_for_menu()
就够了。
等项目复杂后,再统一 GameState 和 pause 模式。
38. TimeManager 和 GameState 的关系
未来你可能会有:
EXPLORING
DIALOGUE
SHOP
INVENTORY
PAUSED
CUTSCENE
BATTLE
不同状态下时间是否流动不同:
EXPLORING:时间流动
DIALOGUE:时间暂停
SHOP:时间暂停
INVENTORY:时间暂停
PAUSED:时间暂停
CUTSCENE:看情况
BATTLE:看游戏类型
后面可以做一个 GameStateManager,统一控制:
TimeManager.pause_time()
player.lock_control_for_menu()
ui.open_panel()
现在先不要急。
39. 常见错误 1:时间不走
排查:
TimeManager 是否 Autoload
TimeManager._process 是否运行
is_time_paused 是否为 true
pause_count 是否没有恢复
real_seconds_per_game_minute 是否太大
TimeHUD 是否连接 time_changed
临时打印:
func add_game_minutes(amount: int):
print("时间推进")
如果不打印,是 _process 或暂停问题。
40. 常见错误 2:UI 时间不刷新
排查:
TimeHUD 是否在场景里
TimeHUD._ready 是否连接 TimeManager.time_changed
TimeManager 是否 emit time_changed
Label 路径是否正确
临时打印:
func _on_time_changed(hour: int, minute: int):
print("UI 时间:", hour, ":", minute)
41. 常见错误 3:睡觉后日期不对
检查:
sleep_to_next_day 是否调用 advance_day
hour 是否设置为 6
minute 是否设置为 0
advance_day 是否正确处理 season
days_per_season 是否设置正确
如果你在 23:50 睡觉,第二天应该是:
06:00
day + 1
不是:
00:00
除非你设计成半夜醒来,听着就很惨。
42. 常见错误 4:读档后时间恢复了,但地图状态不对
排查读档顺序:
是否先 TimeManager.apply_time_data
是否先 SaveManager.apply_world_state_data
是否再 SceneManager.change_map
地图对象是否在 _ready 里读取状态
时间和世界状态必须早于地图加载。
43. 常见错误 5:对话时游戏时间还在走
解决:
DialogueBox 打开时 TimeManager.pause_time()
DialogueBox 结束时 TimeManager.resume_time()
如果 pause_count 越来越大,说明某些地方 pause 了但没 resume。
可以调试:
print("pause_count: ", pause_count)
44. 常见错误 6:夜晚颜色太黑
CanvasModulate 夜晚颜色不要一上来太暗。
可以先用:
Color(0.45, 0.48, 0.7, 1)
比:
Color(0.1, 0.1, 0.25, 1)
更适合调试。
不然你会陷入“我的游戏太真实了,真实到啥也看不见”的尴尬场面。
45. 常见错误 7:采集点每日刷新不稳定
如果你做了每日刷新版,排查:
采集时是否保存 year/season/day
get_total_days 是否计算正确
读档时 TimeManager 是否先恢复
GatherNode.apply_saved_state 是否根据 total days 判断
如果你暂时不想处理这些,就先保持永久采集状态。
别强行一步做完。
46. 当前推荐结构
Autoload:
SceneManager
SaveManager
ItemDatabase
QuestDatabase
TimeManager
Main:
Main
├── QuestManager
├── WorldContainer
├── Player
├── CanvasModulate 或 DayNightController
└── UI
├── PlayerHUD
├── TimeHUD
├── InventoryPanel
├── EquipmentPanel
├── QuestPanel
├── ShopPanel
├── DialogueBox
└── DeathPanel
TimeManager 负责:
分钟
小时
天数
季节
年份
时间推进
睡觉
每日刷新
存档恢复
DayNightController 负责:
根据时间阶段改变画面颜色
TimeHUD 负责:
显示当前日期和时间
47. 这一部分最重要的记忆点
游戏时间不要直接依赖系统时间
TimeManager 适合做 Autoload
用 _process(delta) 累积真实秒数,转换成游戏分钟
时间变化用 signal 通知 UI 和系统
白天黑夜可以先用 DayPhase
睡觉可以跳到第二天早上
daily_refresh 用来驱动每日刷新
存档要保存 minute/hour/day/season/year
读档时要先恢复时间,再加载地图
CanvasModulate 可以做简单 2D 日夜色调
暂停时间最好用 pause_count,而不是单个 bool
48. 你现在的小练习
按这个顺序做:
1. 创建 TimeManager.gd
2. 设置为 Autoload:TimeManager
3. 添加 minute / hour / day / season / year
4. 添加 real_seconds_per_game_minute
5. 用 _process(delta) 推进游戏时间
6. 实现 add_game_minutes()
7. 实现 advance_day()
8. 实现 advance_season()
9. 实现 DayPhase
10. 实现 day_phase_changed 信号
11. UI 添加 TimeHUD
12. TimeHUD 显示 06:00
13. TimeHUD 显示 第 1 年 春 第 1 天
14. 等待时间流逝,确认 UI 更新时间
15. 添加 CanvasModulate
16. 创建 DayNightController
17. 不同 DayPhase 改变画面颜色
18. 创建 Bed
19. Bed interact 后调用 sleep_to_next_day()
20. 睡觉后时间变成第二天 06:00
21. 睡觉后恢复玩家生命
22. 睡觉后触发 daily_refresh
23. SaveManager 保存 time
24. SaveManager 读档恢复 time
25. 测试在夜晚保存,重启读档后仍然是夜晚
完成这一部分后,你的 RPG 就开始拥有“世界在流动”的感觉了。
如果您觉得这篇文章有帮助,请点个赞吧~
评论
请登录后发表评论
去登录