让我们通过制作 RPG 来学习 Godot 4 — 第 18 部分:场景转换和昼夜循环
在经历了漫长的一天,射击坏蛋并完成任务后,我们的玩家理应回家好好睡一觉。然而,在他们能够这样做之前,我们需要为他们提供一个带床的房子。在这一部分中,我们将实现这一点,同时为我们的玩家添加在场景之间切换的能力。我们还将添加一个“酒馆”,玩家可以进出,以及一个昼夜循环系统,根据时间改变游戏的颜色!
你将在这部分学到:
- 如何通过 Area2D 节点触发节点的可见性。
- 如何在代码中切换游戏场景。
- 如何使用 CanvasModulate 节点。
- 如何访问系统时间。
这一部分将向你展示两种让玩家探索更多世界的方式。一种是通过显示/隐藏节点来模拟进入现有建筑物,另一种是切换玩家所在的场景——类似于《星露谷物语》中的效果。
世界切换选项 1
由于我们的玩家需要睡觉,我们需要为玩家的房子创建一个新场景。创建一个以 Node2D 节点为根节点的新场景。我们在 EnemySpawner 和 Main 场景中使用过这个节点,所以你应该已经很熟悉了。它是 Godot 节点的基础节点,因此可以包含任何节点。将根节点重命名为 PlayerHouse,并将此场景保存到你的 Scenes 文件夹中。
这个场景将包含一个用于地板的 TileMap 节点,以及一个用于屋顶图像的 Sprite2D 节点。当玩家进入房子时,屋顶将被隐藏,房子内部的地板和家具将显示出来。当玩家离开房子时,屋顶将再次可见。
让我们添加一个 Tilemap 节点并将其重命名为 Interior,然后添加一个 Sprite2D 节点并将其重命名为 Exterior。
你可以为 Exterior 精灵节点分配一个房子外部的图像。这个图像可以在你的 Assets > Buildings > House.png 目录中找到。
暂时将 Exterior 节点的 Alpha (A) 调制值改为 20,这样我们可以透过屋顶看到内部,以便绘制地板。你可以在 Inspector 面板中的 Visibility > Modulate 下更改调制值。
现在我们有了房子的大小,可以为 TileMap 节点分配一个 Tilesheet 资源,以便绘制地板并添加家具。在 Tilemap 的 Inspector 面板中,添加一个新的 tileset 资源。在下面的 Tileset 面板中,添加两个新的 tilesheet:Interior 和 Atlas。这些 tilesheet 可以在 Assets 文件夹的根目录中找到。
现在使用 Atlas tilesheet 中的木地板绘制地板。如果你不想绘制地形,可以自由绘制。
然后,将 Exterior 的调制值重置为完全可见,屋顶应该会再次显示出来。它应该看起来像这样:
接下来,让我们添加一些家具。你可以像在地形部分中那样为这些瓷砖添加碰撞,但今天我将使用纯碰撞形状来加快速度!不过,你应该为瓷砖添加图层,以便我们可以在地板上绘制家具。
现在,使用 Furniture tilesheet 绘制一些家具。你可以在这里添加任何你想要的物品——只需确保玩家有足够的空间走到床上。
接下来,我们需要添加我之前提到的碰撞。我们希望玩家与每面墙以及家具发生碰撞。为此,我们将添加 StaticBody2D 节点来为墙和家具保存碰撞。StaticBody2D 是一个简单的物体,不会在物理模拟中移动,即它不能被外力或接触移动,但其变换仍然可以由用户手动更新。它非常适合用于实现环境中的物体,例如墙壁或平台。我们将使用此节点作为天空的容器。
添加五个 StaticBody2D 节点和两个 Area2D 节点。按照下图所示重命名它们。
你会看到这些新的 StaticBody2D 和 Area2D 节点旁边出现警告。这是因为它们需要一个碰撞区域。
让我们从 Wall_Top 节点开始。为其添加一个 CollisionShape2D 节点。将其设置为矩形形状,并绘制在 TileMap 节点的顶部上方。这将确保玩家不会越过这面“墙”。
对其他墙也执行相同的操作。
现在,为你的家具也添加 CollisionShapes2D 节点(除了床)。通过物理图层可能会更快,但我在这里给你提供了创建选项!我的碰撞在下面用粉色高亮显示。
为你的床添加碰撞。我的床碰撞在下面用红色高亮显示。
现在,让我们为 TriggerArea 添加碰撞。TriggerArea 将触发外部和内部的显示或隐藏。使此区域足够大以覆盖整个地板。我的触发区域在下面用绿色高亮显示。
让我们为场景添加一个脚本。将其保存到 Scripts 文件夹中。
在这个脚本中,我们希望显示/隐藏内部和外部。我们必须将 TriggerArea 节点的 body entered/exited 信号连接到我们的脚本。如果玩家进入触发区域,内部将显示,外部将隐藏——反之亦然。
### PlayerHouse.gd
extends Node2D
# 节点引用
@onready var exterior = $Exterior
@onready var interior = $Interior
func _on_trigger_area_body_entered(body):
if body.is_in_group("player"):
interior.show()
exterior.hide()
func _on_trigger_area_body_exited(body):
if body.is_in_group("player"):
interior.hide()
exterior.show()
默认情况下隐藏 Interior 节点,以便在游戏加载时家具不会突出显示。
在你的 Main 场景中实例化 PlayerHouse 节点,并确保将该节点放在 Player 节点之上,以便 Player 节点显示在其前面。请不要将房子放在任何碰撞体上——例如水。
现在,如果你运行场景并穿过前门,屋顶应该会消失,内部应该会显示出来。如果你从楼梯跑出去,屋顶应该会显示,内部应该被隐藏。你还应该无法穿过墙壁或家具。
我们现在有一个问题——任何人都可以进入玩家的房子,尤其是敌人!为了解决这个问题,我们需要更新代码以阻止敌人进入。如果他们碰巧进入房子,我们的代码需要将他们的移动重定向回外面。我们通过每 4 秒重定向他们的旋转来实现这一点,直到他们离开房子。
###PlayerHouse.gd
extends Node2D
# Node refs
@onready var exterior = $Exterior
@onready var interior = $Interior
func _on_trigger_area_body_entered(body):
if body.is_in_group("player"):
interior.show()
exterior.hide()
#prevent enemy from entering our direction
elif body.is_in_group("enemy"):
body.direction = -body.direction
body.timer = 16
设置好房子后,我们可以告诉玩家如果他们与床互动就去睡觉。我们可以使用 RayCast 节点来查看它是否击中了名为 Bed 的物体(这将是我们的 Bed StaticBody2D 节点)。如果击中了,我们将播放一个动画并恢复玩家的状态。我们已经在 ui_interact 下设置了此输入的设置——这意味着我们将在靠近床时按 TAB 键睡觉。
在更新 Player 代码之前,让我们创建睡眠动画。在 Player 场景的 AnimationPlayer 中,添加一个名为 sleeping 的新动画。
在添加动画轨道之前,我们需要为其创建 UI。我们希望在我们睡觉时屏幕显示“睡眠中”屏幕。在 Player 场景的 UI 层下,添加一个新的 ColorRect,并将其子节点设置为 Label。将 ColorRect 重命名为 SleepScreen。
将 SleepScreen 节点的锚点预设设置为 Full Rect,并将其颜色更改为 #d6c376。
将标签的文本更改为 Snoozing…。将其字体更改为 Schrödinger;字体大小(20),垂直和水平对齐(居中)。其锚点预设应为居中。
现在回到 AnimationPlayer 的 sleeping 动画,我们想要添加一个使睡眠屏幕可见的动画。我们之前在 Game Over 屏幕中做过类似的事情。添加一个属性轨道,将其连接到 SleepScreen 节点,并选择属性为 modulate 属性。
我们希望睡眠屏幕显示 3 秒钟,因此将时间更改为 3。还在以下时间戳添加三个键(0, 0.5, 2.5, 3)。
将关键帧 0 和 3 的 Alpha(A) 值更改为 0(不可见)。我们希望它流动为不可见 -> 可见 -> 不可见。如果你播放动画,它应该按预期流动。
将 SleepScreen 的可见性设置为隐藏。
现在在你的 Player 脚本中,在 ui_interact 输入下,让我们检查 Raycast 是否击中了我们的床。如果是,我们将播放睡眠屏幕动画,并恢复玩家的生命值和耐力值。
### Player.gd
func _input(event):
#older code
#interact with world
elif event.is_action_pressed("ui_interact"):
var target = ray_cast.get_collider()
if target != null:
if target.is_in_group("NPC"):
# Talk to NPC
target.dialog()
return
#go to sleep
if target.name == "Bed":
# play sleep screen
animation_player.play("sleeping")
health = max_health
stamina = max_stamina
health_updated.emit(health, max_health)
stamina_updated.emit(stamina, max_stamina)
return
为了使此功能正常工作,我们需要启用 RayCast2D 节点以能够与物体(例如我们的 Area2D 物体)碰撞。
如果你现在运行场景并在床附近按 TAB,你的动画将播放,状态将重新填充!
世界切换选项 2
现在,让我们开始第二个世界切换。在这个选项中,我们将为我们的酒馆创建一个新场景——如果你愿意,你可以将其设置为完全不同的地图。这个场景可以是任何东西。它可以是一个海滩区域、洞穴、豪宅——任何东西,但我们将创建一个简单的酒馆,展示如何通过场景切换来改变整个区域。
我们可以为此复制我们的 Main 场景。将此场景重命名为 Main_2 并分离信号和脚本。此外,删除 Tilemap 中所有绘制的瓷砖。我们希望这个场景是一张白纸。
现在,你可以在这里尽情发挥,创建你梦想中的酒馆——或者如果你不想创建酒馆,而是希望你的玩家前往一个新区域,例如森林,那就去创建吧。只需记住,如果你正在创建一个新的世界地图,请像我们在地图创建教程中所做的那样,为你的 Tilemap 添加碰撞(物理图层)。
我将继续创建一个酒馆。
现在我们有了新区域,我们需要为墙壁添加碰撞。
接下来,我们需要为家具添加碰撞。为了加快此过程,我将通过 TileMap 资源中的物理图层为我的家具添加碰撞。你也可以这样做。如果你忘记了如何操作,请参阅文档或我们之前的教程(第 4 部分)。
然后在出口或门旁边添加 TriggerArea 碰撞。如果我们的玩家穿过此区域,他们将被传送回 Main 场景的地图。
在你的 Main 场景中也这样做,以便我们可以前往 Main_2 场景。
在我们的 Main_2 场景中,我们还需要再次实例化 Player 场景,因为这是玩家进入场景时“生成”的地方。
接下来,让我们将一个新脚本附加到 Main_2 场景的根节点。
在这个脚本中,我们希望玩家在穿过我们的触发区域时能够返回 Main 场景。你可能已经猜到,我们必须将 TriggerArea 节点的 body_entered() 信号连接到我们的 Main_2 脚本。
还要将 Main 场景的 TriggerArea 的 body_entered 信号连接到你的 Main 脚本。
现在要切换场景,我们需要跟踪玩家当前所在的场景,以便我们可以动态更改场景引用。目前,我们到处都在引用 Main 场景,因此当我们转到 Main_2 场景时会出现错误。
例如,以前我们在场景中有这样的路径引用:
player = get_tree().root.get_node("/Main/Player")
如果我们想在 Main_2 场景中重用此节点怎么办?我们的游戏会崩溃,因为 Main/Player 在该树中不存在!但在我们的 Global 脚本的帮助下,我们可以根据玩家当前所在的场景动态更改场景名称。
我们最终会使用这样的东西:
player = get_tree().root.get_node("%s/Player" % Global.current_scene_name)
现在,在我们的 Global 脚本中,让我们在 ready 函数中设置当前加载场景的名称。这可能是 MainMenu、Menu 或 Main_2。我们稍后会在其自定义函数中更改它。
### Global.gd
extends Node
# 场景资源
@onready var pickups_scene = preload("res://Scenes/Pickup.tscn")
@onready var enemy_scene = preload("res://Scenes/Enemy.tscn")
@onready var bullet_scene = preload("res://Scenes/Bullet.tscn")
@onready var enemy_bullet_scene = preload("res://Scenes/EnemyBullet.tscn")
# 拾取物
enum Pickups { AMMO, STAMINA, HEALTH }
# TileMap 图层
const WATER_LAYER = 0
const GRASS_LAYER = 1
const SAND_LAYER = 2
const FOLIAGE_LAYER = 3
const EXTERIOR_1_LAYER = 4
const EXTERIOR_2_LAYER = 5
# 当前场景
var current_scene_name
# 加载时设置当前场景
func _ready():
current_scene_name = get_tree().get_current_scene().name
现在在我们的其他脚本中,我们可以将所有静态的 /Main/ 路径替换为引用 current_scene_name 变量。我们将使用 % 运算符格式化路径字符串,以插入 Global.current_scene_name 的值。%s 是字符串值的占位符,例如 Main。
### Bullet.gd
extends Area2D
# 节点引用
@onready var tilemap = get_tree().root.get_node("%s/Map" % Global.current_scene_name)
@onready var animated_sprite = $AnimatedSprite2D
### Enemy.gd
# 节点引用
@onready var player = get_tree().root.get_node("%s/Player" % Global.current_scene_name)
# 当敌人受到攻击时会受到伤害
func hit(damage):
health -= damage
if health > 0:
# 伤害
animation_player.play("damage")
else:
# 死亡
# 停止移动
timer_node.stop()
direction = Vector2.ZERO
# 停止生命值恢复
set_process(false)
# 触发动画完成信号
is_attacking = true
# 最后,我们播放死亡动画
animation_sprite.play("death")
# 添加经验值
player.update_xp(70)
death.emit()
# 以 90% 的几率随机掉落战利品
if rng.randf() < 0.9:
var pickup = Global.pickups_scene.instantiate()
pickup.item = rng.randi() % 3 # 我们的枚举中有三个拾取物
get_tree().root.get_node("%s/PickupSpawner/SpawnedPickups" % Global.current_scene_name).call_deferred("add_child", pickup)
pickup.position = position
# 子弹和移除
func _on_animated_sprite_2d_animation_finished():
if animation_sprite.animation == "death":
get_tree().queue_delete(self)
is_attacking = false
# 实例化子弹
if animation_sprite.animation.begins_with("attack_"):
var bullet = Global.enemy_bullet_scene.instantiate()
bullet.damage = bullet_damage
bullet.direction = new_direction.normalized()
# 将其放置在敌人前方 8 像素处
bullet.position = player.position + new_direction.normalized() * 8
get_tree().root.get_node("%s" % Global.current_scene_name).add_child(bullet)
### EnemyBullet.gd
extends Area2D
# 节点引用
@onready var tilemap = get_tree().root.get_node("%s/Map" % Global.current_scene_name)
@onready var animated_sprite = $AnimatedSprite2D
### EnemySpawner.gd
extends Node2D
# 节点引用
@onready var spawned_enemies = $SpawnedEnemies
@onready var tilemap = get_tree().root.get_node("%s/Map" % Global.current_scene_name)
### NPC
extends CharacterBody2D
# 节点引用
@onready var dialog_popup = get_tree().root.get_node("%s/Player/UI/DialogPopup" % Global.current_scene_name)
@onready var player = get_tree().root.get_node("%s/Player" % Global.current_scene_name)
@onready var animation_sprite = $AnimatedSprite2D
### PickupSpawner.gd
extends Node2D
# 节点引用
@onready var map = get_tree().root.get_node("%s/Map" % Global.current_scene_name)
@onready var spawned_pickups = $SpawnedPickups
### Player.gd
# 重置动画状态
func _on_animated_sprite_2d_animation_finished():
is_attacking = false
# 实例化子弹
if animation_sprite.animation.begins_with("attack_"):
var bullet = Global.bullet_scene.instantiate()
bullet.damage = bullet_damage
bullet.direction = new_direction.normalized()
# 将其放置在玩家前方 4-5 像素处
bullet.position = position + new_direction.normalized() * 4
get_tree().root.get_node("%s" % Global.current_scene_name).add_child(bullet)
### QuestItem.gd
extends Area2D
# NPC 节点引用
@onready var npc = get_tree().root.get_node("%s/SpawnedNPC/NPC" % Global.current_scene_name)
让我们在 Global 脚本中创建一个允许我们切换场景的函数。这将更新我们的场景名称为玩家当前所在的场景。在这个函数中,我们将获取当前场景,然后释放它。因此,如果我们的当前场景是 Main 场景,并且我们正在移动到 Main_2 场景,它将获取我们的 Main 并从场景树中释放它。然后它将加载并实例化我们的新场景,并将新场景添加为根节点的子节点。最后,它将新场景设置为当前场景。
这一系列操作实际上是用新场景替换当前场景。稍后,我们还会在玩家进入新场景后加载玩家的数据(如他们的健康值、硬币、拾取数量等),这是我们添加了保存和加载系统之后。这将解决玩家更换区域时数据不持久的问题。然后,当玩家离开一个区域时,它会保存他们的数据,以便在进入新区域时可以加载。
### Global.gd
extends Node
# Scene resources
@onready var pickups_scene = preload("res://Scenes/Pickup.tscn")
@onready var enemy_scene = preload("res://Scenes/Enemy.tscn")
@onready var bullet_scene = preload("res://Scenes/Bullet.tscn")
@onready var enemy_bullet_scene = preload("res://Scenes/EnemyBullet.tscn")
# Pickups
enum Pickups { AMMO, STAMINA, HEALTH }
# TileMap layers
const WATER_LAYER = 0
const GRASS_LAYER = 1
const SAND_LAYER = 2
const FOLIAGE_LAYER = 3
const EXTERIOR_1_LAYER = 4
const EXTERIOR_2_LAYER = 5
# current scene
var current_scene_name
# ----------------------- Scene handling ----------------------------
#set current scene on load
func _ready():
current_scene_name = get_tree().get_current_scene().name
# Change scene
func change_scene(scene_path):
# Get the current scene
current_scene_name = scene_path.get_file().get_basename()
var current_scene = get_tree().get_root().get_child(get_tree().get_root().get_child_count() - 1)
# Free it for the new scene
current_scene.queue_free()
# Change the scene
var new_scene = load(scene_path).instantiate()
get_tree().get_root().call_deferred("add_child", new_scene)
get_tree().call_deferred("set_current_scene", new_scene)
现在在我们的Main和Main_2脚本中,我们可以通过调用这个全局函数并传递我们想要切换到的场景路径作为参数来更改场景。然后,我们将场景排队释放(queue_free),这样Main或Main_2实际上就被从我们的场景树中移除了。
### Main.gd
extends Node2D
# Change scene
func _on_trigger_area_body_entered(body):
if body.is_in_group("player"):
Global.change_scene("res://Scenes/Main_2.tscn")
queue_free()
### Main_2.gd
extends Node2D
# Change scene
func _on_trigger_area_body_entered(body):
if body.is_in_group("player"):
Global.change_scene("res://Scenes/Main.tscn")
queue_free()
在我们的Main场景中,我们现在在更改场景后直接释放我们的场景资源。当你调用queue_free()时,它并不会立即删除节点。它会将节点安排在当前帧结束或稍后安全时进行删除。
因此,在我们新场景正确设置之前,旧场景可能仍然存在。queue_free()函数是异步的,这是有充分理由的,以防止节点在仍在使用时被删除。为了避免场景切换和自动保存之间的这种潜在竞争条件,我们可以在Global脚本中使用一个信号,该信号在场景完全过渡后发出。
### Global.gd
# older code
#notifies scene change
signal scene_changed()
然后,在我们加载新场景之后,在change_scene函数中发出这个信号。
### Global.gd
# older code
# Change scene
func change_scene(scene_path):
# Get the current scene
current_scene_name = scene_path.get_file().get_basename()
var current_scene = get_tree().get_root().get_child(get_tree().get_root().get_child_count() - 1)
# Free it for the new scene
current_scene.queue_free()
# Change the scene
var new_scene = load(scene_path).instantiate()
get_tree().get_root().call_deferred("add_child", new_scene)
get_tree().call_deferred("set_current_scene", new_scene)
call_deferred("post_scene_change_initialization")
func post_scene_change_initialization():
scene_changed.emit()
然后,在Main.gd和Main_2.gd中,你连接到这个信号,并在它被发出时调用queue_free:
### Main.gd
extends Node2D
# Change scene
func _on_trigger_area_body_entered(body):
if body.is_in_group("player"):
Global.change_scene("res://Scenes/Main_2.tscn")
Global.scene_changed.connect(_on_scene_changed)
#only after scene has been changed, do we free our resource
func _on_scene_changed():
queue_free()
### Main_2.gd
extends Node2D
# Change scene
func _on_trigger_area_body_entered(body):
if body.is_in_group("player"):
Global.change_scene("res://Scenes/Main.tscn")
Global.scene_changed.connect(_on_scene_changed)
#only after scene has been changed, do we free our resource
func _on_scene_changed():
queue_free()
现在如果你运行你的场景并且进入触发区域,你的场景应该会发生变化,你的玩家和其他实体应该会被生成!记得将你的玩家节点放置在两个场景中都可以生成的地方。
实时日夜循环 现
在我们的玩家可以旅行到其他区域,而且他们不再是无家可归了,让我们通过给游戏添加一些“时间”来增加这些新区域的现实感。我们将通过一个实时的日夜循环来实现这一点。这意味着我们场景的颜色将根据玩家当前游戏的时间发生变化。所以,如果你在下午玩这款游戏,游戏中的时间也将是下午,以此类推!
如果你不希望游戏中有这样的系统,可以跳过这一部分,但如果你想知道这是如何工作的,那就让我们开始吧。
在我们的项目中,让我们创建一个新场景,以Node2D节点作为根节点。这是我们2D游戏的基础节点,我们使用它是因为它可以包含和调用任何其他节点。将这个节点重命名为“Sky”,并将其保存到你的Scenes文件夹下。
对于我们的Sky,我们将根据从白天到夜晚过渡时天空的颜色来调整天空的色调。我将使用这个颜色调色板作为我的参考。我们想要调整整个屏幕的色调。现在你可以通过拖动一个正常的ColorRect节点到屏幕大小来实现这一点,或者你可以采取更简单的方法,添加一个CanvasModulate节点。这个节点使用其指定的颜色来调整整个画布元素的色调。
在你的Sky场景中添加一个CanvasModulate节点。
您会看到它只有一个颜色属性,您可以在检查器面板中更改。
现在,我们希望这个画布根据一天中的时间改变颜色。您可能考虑使用一个计时器节点和一些条件语句来实现这一点,但为了实现颜色之间的平滑过渡,我们需要使用一个AnimationPlayer节点。我们将在动画中为CanvasModule节点添加一系列颜色轨道,根据一天中的时间,颜色将在动画时间线上缓慢过渡。
这听起来可能很复杂,但别担心——您马上就会明白的!在您的天空场景中添加一个AnimationPlayer节点。
在您的AnimationPlayer节点中,添加一个名为“day_night_cycle”的新动画。
在现实生活中,我们的昼夜循环持续24小时,所以将动画长度也设置为24。我们稍后会添加代码,该代码将在时间线上寻找秒数,并根据我们现实生活中的时间分配颜色。所以如果现实生活中是凌晨5点,那么动画播放器将返回时间线上5秒标记处设置的颜色。
我们想要更改CanvasModulate节点的颜色属性,所以让我们添加一个连接到我们的画布节点的属性轨道,并选择“颜色”作为要更改的属性。
好的,对于我们的day_night_cycle,我们有六种颜色需要分配到六个时间段:
清晨 -> 凌晨12点 — 3点
早晨 -> 凌晨3点 — 5点
白天 -> 早上6点 — 下午3点
下午 -> 下午3点 — 5点
傍晚 -> 下午5点 — 7点
夜晚 -> 晚上7点 — 凌晨12点
根据您所在的地区更改这些时间,但在我的国家,这是天空颜色变化的大致时间。让我们将这些时间段分配到我们的动画时间线上。在您希望时间变化的地方分配六个关键帧。
现在在检查器面板中,将我们的调色板中的值分配给这些时间。记得从夜晚颜色开始并以夜晚颜色结束。
清晨 -> 凌晨12点 — 3点 -> #292965
早晨 -> 凌晨3点 — 5点 -> #6696ba
白天 -> 早上6点 — 下午3点 -> #e2e38b
下午 -> 下午3点 — 5点 -> #e7a553
傍晚 -> 下午5点 — 7点 -> #7e4b68
夜晚 -> 晚上7点 — 凌晨12点 -> #292965
现在,如果您在场景中添加一个Sprite2D节点并运行动画,颜色应该根据时间线上的关键帧当前所在的位置而变化。记得之后删除这个Sprite2D节点,因为这个天空场景将在我们的主场景中被实例化!
让我们为场景添加一个脚本。将其保存在您的脚本文件夹下。
我们希望游戏中的当前时间(小时、分钟和秒)在整个游戏中不断计算,以便我们可以将当前时间分配到day_night_cycle动画的时间线上。让我们定义几个变量来存储我们的当前时间、我们的当前时间(以秒为单位),然后将我们的秒数映射到我们的动画时间线上的值。
### Sky.gd
extends Node2D
#time variables
var current_time
var time_to_seconds
var seconds_to_timeline
这将在一分钟内更有意义。我们只是想做一个计算,得到我们的当前时间。然后用那个当前时间,我们将其转换为秒。然后用那些秒,我们在我们的动画时间线上的那个关键帧范围内播放动画(所以如果我们返回9.864秒,时间线上的9.864处的动画将播放)。我们希望这个动画被不断计算,所以我们将在我们的_process()函数中做这个。
为了获取当前时间,我们将使用我们的Time对象。我们之前在玩家场景中使用这个来获取我们子弹的重新加载时间。
### Sky.gd
#calculate the time
func _process(delta):
#gets the current time
current_time = Time.get_time_dict_from_system()
~~~
然后我们将调用current_time值的小时、分钟和秒,并将total current_time值转换为秒。
~~~
### Sky.gd
#older code
#calculate the time
func _process(delta):
#gets the current time
current_time = Time.get_time_dict_from_system()
#converts the current time into seconds
time_to_seconds = current_time.hour * 3600 + current_time.minute * 60 + current_time.second
使用我们的time_to_seconds值,我们需要使用我们的remap()方法来计算范围内的线性插值。在编程中,lerp(线性插值)是用于各种领域的常见函数,如游戏开发、图形和动画。它用于找到两个其他值之间的特定混合值。
我们将使用我们的remap()方法将我们的time_to_seconds转换为可以在我们的动画时间线中使用的值。我们将通过将其从[0, 86400]的范围缩放到[0, 24]来实现这一点。86400是一天中的秒数,24可以代表代表24小时的动画时间线的总帧数或单位(记得我们将其设置为24秒)。
### Sky.gd
#older code
#calculate the time
func _process(delta):
#gets the current time
current_time = Time.get_time_dict_from_system()
#converts the current time into seconds
time_to_seconds = current_time.hour * 3600 + current_time.minute * 60 + current_time.second
#converts the seconds into a remap value for our animation timeline
seconds_to_timeline = remap(time_to_seconds, 0, 86400, 0, 24)
现在我们可以使用我们的seconds_to_timeline值来播放时间线上的动画。
### Sky.gd
extends Node2D
# Node refs
@onready var animation_player = $AnimationPlayer
#time variables
var current_time
var time_to_seconds
var seconds_to_timeline
#calculate the time
func _process(delta):
#gets the current time
current_time = Time.get_time_dict_from_system()
#converts the current time into seconds
time_to_seconds = current_time.hour * 3600 + current_time.minute * 60 + current_time.second
#converts the seconds into a remap value for our animation timeline
seconds_to_timeline = remap(time_to_seconds, 0, 86400, 0, 24)
#plays the animation at that second value on the timeline
animation_player.seek(seconds_to_timeline)
animation_player.play("day_night_cycle")
就这样,我们的昼夜循环完成了。我们只需要回到我们的主场景并实例化我们的天空场景!
当我写这篇文章的时候,我的地区是下午——所以当我运行我的场景时,我的颜色应该在我的“下午”范围内:
您总是可以调整您的颜色,如果您愿意的话。让我们测试其他时间,看看它是否有效。确保您的游戏场景仍在运行以测试这一点。
在您的计算机设置中,在“日期和时间”下,禁用“自动设置时间”功能,并将时间更改为不同的时间,例如晚上11点。
如果您回到正在运行的您的游戏实例,您的场景现在应该是您的系统新时间的颜色!
- 晚上11点:
- 下午6点:
- 凌晨2点:
这些颜色对我来说有点暗,所以我建议您调整它们。就这样,您有了两种新的方式来过渡到游戏中的新区域,加上一个昼夜循环!我们非常接近本教程系列的结尾,剩下的唯一事情就是为我们的游戏添加暂停和主菜单、保存和加载以及音乐和音效!记得保存您的项目,我们下一部分见。