我的自习室—让生活从这里沉淀 - 相信神话
通过制作2D平台游戏学习Godot 4 — 第10部分：游戏相机
到目前为止，和《大金刚》一样，我们的游戏使用默认相机，它可以捕捉整个游戏地图。这意味着我们可以一次性看到所有东西。每款2D平台游戏都有不同类型的相机。有些相机会跟随玩家，有些则会在屏幕上滚动。你最终为游戏选择的相机类型取决于你试图为玩家群体打造的体验。快速移动并捕捉小区域的相机适用于你试图营造紧张感的游戏或关卡，而移动较慢、捕捉较大区域的相机则能营造更轻松的游戏体验。相机的大小、焦点和速度取决于你的最终目标。由于我们的游戏灵感源自《大金刚》，所以无需改变相机设置，但为了让游戏独具特色，我们将通过三种不同方式在Godot中创建相机。在本部分你将学到什么：如何使用Camera2D节点。如何为相机的偏移值设置动画。如何估算和定义相机的限制。固定摄像机使用固定摄像头时，摄像头会捕捉整个屏幕或游戏地图的大部分区域。然后可以看到玩家在这个区域内移动。在玩家到达屏幕边缘或该区域尽头之前，摄像头会线性跟随玩家。在我们的游戏中，我们捕捉的是整个游戏地图（即区域）。一个例子是《大金刚》：这款游戏已经捕捉到了我们主场景的整个窗口，因此我们无需在场景中添加摄像机来作为固定摄像机。玩家固定视角相机使用固定于玩家的相机时，相机会被设置为跟随玩家的中心位置。这意味着相机的相对位置取决于玩家的位置。一个例子是《洛克人》：要为我们的角色添加一个固定视角相机，我们需要在“角色”场景中添加一个Camera2D节点。该节点会强制屏幕（当前层）跟随“角色”场景滚动。与手动更改基于CanvasItem的节点位置相比，这样编写可滚动场景的程序会更轻松（也更快）。如果你现在运行场景，并且你的玩家在地图上四处跑动，相机应该聚焦在他们身上。我们可以通过在检查器面板中更改相机的缩放值，将相机“移近”玩家。如果你现在运行场景，摄像机应该会拉近到玩家身上！如果你在地图上四处跑动，你可能已经注意到相机捕捉到了游戏边界之外的灰色背景。我们不希望相机捕捉这些区域，相反，我们希望相机在到达地图的外部边界时停止。要做到这一点，我们需要更改Camera2D节点的限制。这将以像素为单位限制左右上下的滚动范围。你可以在检查器面板中“限制”下方更改限制。要获取这些限制的值，你可以在主场景中从左右标尺上拖动，以获取游戏边界边缘的坐标值。左极限右极限上限下限这就是我最终得到的结果：现在，如果你运行场景，相机应该跟随玩家，并在到达游戏边界的外缘时停止！偏移固定相机有时我们希望相机仅在超出特定范围后才跟随玩家，而不是始终锁定玩家。这样在相机重新聚焦滚动之前，能给玩家一些空间，使我们能够看到前方的事物。我们可以通过更改相机的偏移量值来实现这一点。这是相机的相对偏移量，能让我们环顾四周。一个例子是《超级马里奥兄弟》：如果你已经有了一个Camera2D节点，在检查器面板中取消选择“启用”属性来禁用它。添加一个新的Camera2D节点。我们还希望根据玩家的方向，让相机缓慢地向左或向右移动，所以还要添加一个AnimationPlayer节点。在“动画”面板中创建两个新动画，分别命名为“move_left”和“move_right”。对于这两者，添加一个新的属性轨迹动画。将其连接到你的Camera2D（即你的新相机，而非已禁用的相机）。选择偏移属性，因为我们希望相机所处位置不以玩家为中心。将动画时长调整为你希望相机移动（转场）的速度。我这里采用 0.5 的测试值。然后在动画的起始和结束位置插入关键帧。现在，在向左移动关键帧的第一个值（0）处，我们需要更改偏移值。我希望相机向右偏移100像素，向上偏移 -50像素。然后在向左移动关键帧的第二个值（0.5）处，我们需要将偏移值更改为向右移动动画的起始值。我希望相机向左偏移100像素，向上偏移50像素。在你的向右移动关键帧的第一个值（0）处，我们需要更改偏移值。我希望相机向右偏移100像素，向上偏移50像素。然后在向右移动关键帧的第二个值（0.5）处，我们需要将偏移值更改为向左移动动画的起始值。我希望相机向左偏移100像素，向上偏移 -50像素。我还希望相机稍微拉近一些，所以将“缩放”值更改为更大的值，比如 “2”（这是可选的）。由于相机的限制取决于相机的偏移量，所以我们必须在代码中更改相机的限制范围，因为相机在各个方向上的位置会有所不同。我们还希望相机能够平滑过渡，因此请启用相机的“位置平滑”属性，该属性将以 position_smoothing_speed 的速度将相机平滑地移动到目标位置。你可以根据希望相机过渡或跟随玩家的速度来更改此速度。我们还希望动画中的过渡捕捉更加平滑。我们可以通过更改动画中的插值模式来实现这一点。插值告诉Godot如何计算关键帧之间的帧值。支持以下插值模式：最近值：设置最接近的关键帧值。线性：根据两个关键帧之间的线性函数计算来设置值三次方：根据两个关键帧之间的三次函数计算来设置值我们希望将向左移动和向右移动动画的插值模式从线性改为三次样条。在我们的玩家脚本中，我们需要创建新的变量来记录玩家当前和上一个方向。我们将使用这些方向来决定是否播放向左移动或向右移动动画。### Player.gd extends CharacterBody2D #player movement variables @export var speed = 100 @export var gravity = 200 @export var jump_height = -110 # Keep track of the last direction (1 for right, -1 for left, 0 for none) var last_direction = 0 # Check the direction of the player's movement var current_direction = 0如果我们玩家的当前方向为1，那么他们将面向右侧；如果为 -1，则面向左侧。游戏开始时，我们的玩家将面向右侧，因此在游戏开始时，我们必须将当前方向设置为面向右侧。### Player.gd #older code func _ready(): current_direction = -1现在，我们需要在游戏循环中根据玩家的x轴速度值不断更新这个current_direction变量。在2D游戏中，当速度的x轴分量为正时，通常表示向右移动，反之则表示向左移动。我们将在process()函数中更新这个变量。### Player.gd #older code func _process(delta): if velocity.x > 0: # Moving right current_direction = 1 elif velocity.x < 0: # Moving left current_direction = -1如果我们当前的方向current_direction与上一个方向不同，我们将根据方向值（1或 -1）播放动画。我们还需要将上一个方向更新为与当前方向相同。这样可以避免重复过渡，因此如果我们面向右侧并且按下向右输入，move_right动画在我们改变方向之前不会再次播放。### Player.gd #older code func _process(delta): if velocity.x > 0: # Moving right current_direction = 1 elif velocity.x < 0: # Moving left current_direction = -1 # If the direction has changed, play the appropriate animation if current_direction != last_direction: if current_direction == 1: # Play the right animation $AnimationPlayer.play("move_right") elif current_direction == -1: # Play the left animation $AnimationPlayer.play("move_left") # Update the last_direction variable last_direction = current_direction我们还需要根据方向设置边界。当你向左和向右时，需要针对每个边界（上、下、左、右）进行测试。### Player.gd #older code func _process(delta): if velocity.x > 0: # Moving right current_direction = 1 elif velocity.x < 0: # Moving left current_direction = -1 # If the direction has changed, play the appropriate animation if current_direction != last_direction: if current_direction == 1: # Play the right animation $AnimationPlayer.play("move_right") #limits $Camera2D.limit_left = -110 $Camera2D.limit_bottom = 705 $Camera2D.limit_top = 40 $Camera2D.limit_right = 1068 elif current_direction == -1: # Play the left animation $AnimationPlayer.play("move_left") #limits $Camera2D.limit_left = 90 $Camera2D.limit_bottom = 705 $Camera2D.limit_top = 40 $Camera2D.limit_right = 1268 # Update the last_direction variable last_direction = current_direction你的代码应该像这样。现在，如果你运行场景，相机应该跟随玩家，改变方向，并且在到达游戏边界的外边缘时停止！如果过渡仍然过于生硬，那么将动画时长设置得更长一点，以实现更平滑的过渡。恭喜你创建了游戏相机（如果你选择了不固定在主场景的相机）！在下一部分，我们将设置次要敌人，它们会在平台关卡中向我们投掷和生成箱子。现在是保存项目并为其制作备份的好时机，这样如果出现任何破坏游戏的错误，你就可以恢复到这一步。在继续学习本系列之前，回顾一下你所学的内容。准备好后，我们下一部分见！
通过制作2D平台游戏来学习Godot 4-第9部分: 加农炮处理程序
在前面的部分中，我们创建了大炮 (炸弹产生者) 和炸弹。现在是时候创建我们的大炮处理程序了，这是一只傲慢的绿猪，他会向我们大喊随机的语音气泡并 “点燃” 大炮。这个大炮处理程序在我们的游戏中没有特殊的目的，除了给我们的玩家一个不喜欢的人 (比如来自donkey Kong的驴)。我们的游戏可以在没有这个处理器的情况下完美地工作，但是如果大炮自己发射了，那会有什么乐趣呢？你将在这部分学到什么:如何使用RandomNumberGenerator方法。如何更改计时器wait_time。让我们创建一个Node2D节点作为其根节点的新场景。将此节点重命名为 “CannonHandler”，并将场景保存在Scenes文件夹下。现在在这个场景中，我们希望CannonHandler有两个AnimatedSprite2D节点。第一个AnimatedSprite2D将作为车身对于我们的猪，第二个AnimatedSprite2D将作为语音气泡为了我们的猪。将它们重命名为 “Body” 和 “SpeechBubble”。我们还需要在我们的场景中添加一个计时器节点，这将在计时器超时后随机化我们返回的语音气泡动画。我们将通过随机化计时器的wait_time,这是从x倒计时到0所需的时间 (以秒为单位)。简单地说，每x秒我们的计时器将超时 ()，并返回一个新的语音气泡!我们希望这个计时器在游戏开始后立即开始随机化我们的语音气泡，因此请在检查器面板中启用Autostart属性。选择你的身体节点，并添加两个新的动画: “空闲” 和 “匹配”。为你的慢车动画，导航到 “res:// 资产/国王和猪/精灵/07-猪与匹配/匹配 (26x18 ).png”，并裁剪出所有三个动画帧。将循环值保留为启用，并将其FPS值更改为3。为你的匹配动画，导航到 “res:// Assets/Kings and Pigs/Sprites/07-Pig与匹配/照明匹配 (26x18 ).png”，并裁剪出所有三个动画帧。关闭循环值并将其FPS值更改为3。在您的检查面板中，还可以在偏移选项下方启用 “翻转H” 属性-因为我们希望我们的猪也向左转向我们的大炮。选择您的SpeechBubble节点并向其中添加三个新动画: “boom”，“loser” 和 “sawaring”。为你的动臂动画，导航到 “res:// Assets/Kings and Pigs/Sprites/13-对话框/Boom In (24x8 ).png” 并裁剪所有三个动画帧。关闭循环值并将其FPS值更改为3。为你的失败者动画，导航到 “res:// 资产/国王和猪/精灵/13-对话框/失败者在 (24x8 ).png” 和裁剪出所有三个动画帧。关闭循环值并将其FPS值更改为3。为你的宣誓动画，导航到 “res:// Assets/Kings and Pigs/Sprites/13-对话框/WTF In (24x8 ).png” 并裁剪出所有三个动画帧。关闭循环值并将其FPS值更改为3。现在，将新脚本附加到CannonHandler场景根目录，并将其保存在脚本文件夹下。此外，连接您的计时器节点的超时 ()信号到你的脚本。由于我们的大炮会在游戏一开始就发射炸弹，我们需要我们的大炮处理程序的第一个动画是我们的匹配动画-因为一旦游戏开始产生炸弹，猪就会点燃大炮。我们将在我们的设置这个动画就绪 ()功能。### CannonHandler.gd extends Node2D #when it's loaded into the scene func _ready(): #play cannon lighting animation on start $Body.play("matching")接下来，我们希望我们的慢车动画播放，如果我们的炸弹是移动的-我们也希望我们的猪向我们喊随机的东西。我们可以通过运行一个条件来实现这一点，看看我们的全局is_bomb_moving变量设置为true，如果它返回为true，我们将使我们的SpeechBubble节点可见 (以显示我们的语音气泡)。我们将在内置的正在处理 ()功能，它将检查我们的炸弹在每一帧的运动状态。这将允许我们运行每帧更新一个节点的代码，从而尽可能频繁地更新我们的大炮处理程序的操作。### CannonHandler.gd #older code func _process(delta): #idle animation if Global.is_bomb_moving == true: $Body.play("idle") #show speech bubble $SpeechBubble.visible = true我们希望我们的匹配动画播放，如果我们的炸弹不动，因为这意味着我们的炸弹正在产生。我们希望我们的猪停止对我们大喊大叫，而是匹配大炮。我们可以通过运行一个条件来实现这一点，看看我们的全局is_bomb_moving变量设置为false，如果为true，我们将使SpeechBubble节点不可见。### CannonHandler.gd #older code func _process(delta): #idle animation if Global.is_bomb_moving == true: $Body.play("idle") #show speech bubble $SpeechBubble.visible = true #matching animation if Global.is_bomb_moving == false: $Body.play("matching") #hide speech bubble $SpeechBubble.visible = false我们需要播放我们为SpeechBubble节点设置的动画，当它变得可见时。我们将通过随机化我们的语音值来实现这一点兰迪 ()方法，该方法将生成一个随机整数。然后，这个随机整数将除以我们的SpeechBubble节点的动画数量 (在我们的例子中是3)。这意味着结果只能是0、1或2-我们可以使用这些值根据此返回值随机播放动画。### CannonHandler.gd #older code func _on_timer_timeout(): #randomizes speech var random_speech = randi() % 3 #will return 0, 2, or 2 match random_speech: 0: $SpeechBubble.play("boom") 1: $SpeechBubble.play("loser") 2: $SpeechBubble.play("swearing")最后，我们将需要随机化wait_time为我们的定时器的超时 ()。这将确保我们的语音气泡在随机的时间量后再生，而不是每x设置秒。我们将通过randi_range()方法，这将允许我们生成一个介于从和到 (包括) 之间的伪随机32位有符号整数。我们将在1到10的任何一秒钟后随机化我们的语音气泡。### CannonHandler.gd #older code func _process(delta): #randomizes speech bubble randomize time $Timer.wait_time = randi_range(1, 10) #idle animation if Global.is_bomb_moving == true: $Body.play("idle") #show speech bubble $SpeechBubble.visible = true #matching animation if Global.is_bomb_moving == false: $Body.play("matching") #hide speech bubble $SpeechBubble.visible = false您的代码应该看起来像这个。现在，让我们把身体节点调整得更大一点。将其Transform选项下的Scale属性更改为诸如 “1.5” 之类的值。你可以改变这个值，让你的猪像你想要的那样大或小。然后改变你的SpeechBubble节点也有点大。将其Transform选项下的Scale属性更改为诸如 “2.5” 之类的值。我们还想把这个演讲泡泡移到猪头上。保存场景，并在BombSpawner场景中实例化它。我们想把猪移到大炮前面。现在，如果你运行你的主场景，你的大炮处理程序应该 “匹配” 大炮产生炸弹，以及改变它的语音每x随机秒。祝贺你创建你的大炮处理程序!我们游戏中的这种变化可能纯粹是视觉上的，但它确实为我们的加农炮产卵器增加了一些生命。在下一部分中，我们将看看在游戏中添加相机。现在是保存项目并备份项目的好时机，以便在发生任何破坏游戏的错误时可以恢复到此部分。在你继续这个系列之前，回去复习一下你学过的东西，一旦你准备好了，下一部分见!
通过制作 2D 平台游戏来学习 Godot 4 — 第 8 部分：炸弹生成器
现在我们的游戏中有一个炸弹，但我们仍然需要一个刷怪箱。刷怪箱将在特定路径上生成炸弹 — 每次炸弹与玩家或墙壁/瓦片地图碰撞时，它都会重新生成一个新的炸弹。不是我们自己在地图上放置炸弹，而是刷怪箱会为我们处理它。您将在这部分中学到什么：如何使用 Path2D 节点。如何使用 PathFollow2D 节点。如何使用 AnimationPlayer 节点。我们将使用 Path2D 节点为炸弹创建预定义路径。这个 Path2D 节点允许我们在地图上绘制点，这些点将用作我们的炸弹路径。创建路径点后，我们将使用 PathFollow2D 节点沿此路径移动生成的 Bomb 场景。我们需要使用 AnimationPlayer 节点来为炸弹沿着此路径移动的动画。让我们为 Bomb Spawner 创建一个新场景！我们的根节点应该是 Node2D 节点 — 您可以将其重命名为 “BombSpawner”。将此场景保存在 Scenes 文件夹下。这个 BombSpawner 场景需要一个 Timer 节点，每次炸弹数量低于零时，该节点都会生成一个炸弹（如果没有炸弹，计时器将生成一个新的炸弹）。在 Timer 节点的 Inspector 面板中，我们需要启用 “Autostart” 选项。当 BombSpawner 进入 Main scene 树时，这将自动启动计时器。另一个选项“one-shot”触发计时器一次，计时器将在达到 0 时停止。最后，此场景将需要一个 AnimatedSprite2D 节点。此 AnimatedSprite2D 节点将包含将发射炸弹的大炮的动画。我们将在下一部分添加 cannon handler！让我们为大炮创建两个新的动画，分别名为 “cannon_idle” 和 “cannon_fired”。对于我们的cannon_idle动画，请导航到“res://Assets/Kings and Pigs/Sprites/10-Cannon/Idle.png”，然后将唯一的帧添加到您的动画中。暂时将 looping 和 FPS 值保留为默认值。对于我们的cannon_fired动画，请导航到“res://Assets/Kings and Pigs/Sprites/10-Cannon/Shoot （44x28）.png”，并将所有 4 帧添加到动画中。将第一帧添加两次，以便 cannon 动画以相同的帧开始和结束。关闭循环，并将其 FPS 值更改为 4。我们还希望大炮面向相反的方向（右），因此在 AnimatedSprite2D 节点的 Inspector 面板中，在 Offset 下启用“Flip H”属性。我们稍后会回到 BombSpawner 场景，但现在，让我们回到主场景，并向场景添加新的 Node2D 节点。将其重命名为 “BombPath”。此外，删除您之前从场景中实例化的任何 Bomb 场景。添加 Path2D 节点作为 BombPath 节点的子节点。此节点将允许我们为炸弹的移动绘制路径点。如果选择 Path2D 节点，您将在 Inspector 面板中看到有一个添加 Points 的选项。如果向 Points 属性添加元素，您将看到它在游戏窗口的 （x： 0， y： 0） 处创建了一个菱形。您可以将此点拖动到地图上您希望炸弹生成的任意位置。这是炸弹路径的起点。您可以稍后移动它。如果添加另一个元素，它将创建另一个点。您现在也可以单击线条来添加点，如果您想移动点，您只需选择它们并将它们拖动到您想要放置它们的位置即可。要删除某个点，您可以用鼠标右键单击它或在 Inspector 面板中将其删除。让我们为炸弹创建一条从顶部到底部的路径。炸弹每次生成时都会沿着这条路径移动，如果它到达这条路径的尽头或与我们的玩家发生碰撞，它将从顶部重生并再次下降到终点。确保为玩家留出足够的空间以便能够跳过炸弹，如果炸弹与 Tilemap 碰撞（选项 #1）- 确保有足够的空间让炸弹流动而不会爆炸。为了让我们的炸弹在生成后沿着这条路径移动，我们需要添加一个 PathFollow2D 节点作为 Path2D 节点的子节点。我们还需要添加一个 AnimationPlayer 节点作为 Path2D 节点的子节点。动画播放器用于 Animation 资源的通用播放。它包含 AnimationLibrary 资源的字典和动画过渡之间的自定义混合时间。每当需要对非 sprite 项（如标签或颜色）进行动画处理时，都可以使用此 AnimationPlayer 节点。我们在下面的 Animation 面板中向 AnimationPlayer 节点添加动画。要添加新的 Animation，请单击 Animation 属性并选择 “New” 选项。将此动画称为 “bomb_movement”。这将创建一个由四个部分组成的动画：控件（即添加、加载、保存和删除动画）曲目列表。带有关键帧的时间轴 — 定义属性在某个时间点的值。时间轴和轨道控件，例如，您可以在其中缩放时间轴和编辑轨道。您还可以通过按住 CRTL 并使用鼠标滚轮滚动来放大和缩小时间轴。图 9：动画面板概述让我们将动画长度更改为 5。这将是我们的炸弹从 Path2D 节点上的第一个点移动到最后一个点所需的时间。我们可以稍后更改此设置，使其变慢或更快。让我们创建一个新的 Track。每个轨迹都存储指向节点及其 affected 属性的路径。点击 “Add Track” 并选择 “Property Track”。我们选择 Property Track 是因为我们想更改某个节点的属性值（我们可以在 Inspector 面板中找到的属性）。我们希望对 PathFollow2D 节点的进度比率属性进行动画处理，以便我们可以根据该进度比率在路径上移动 Bomb。由于我们想要为 PathFollow2D 节点制作动画，因此我们需要在出现的弹出窗口中选择它。现在，我们可以选择要进行动画处理的 PathFollow2D 节点的属性。选择 “progress ratio”。此进度比率将沿路径的距离称为 0.0（对于第一个顶点）到 1.0（对于最后一个顶点）范围内的数字。这只是表示 path 中进度的另一种方式，因为提供的 offset 在内部乘以 path 的长度。让我们添加两个关键帧：一个位于动画时间轴的开头，另一个位于动画时间轴的结尾。要添加新的关键帧，请右键单击鼠标并选择 “Insert Key”。在时间轴上的点 0 和点 5 处添加键。0 是我们的开始位置，炸弹将在这里生成并开始移动，5 是结束位置，炸弹将被摧毁并停止移动。现在，动画不会移动，因为进度比率正在从值 0 移动到值 0。如果选择关键帧，则可以在 Inspector 面板中看到此值。我们需要将关键帧 “5” 的 Value 属性更改为 1。这意味着我们的动画将从 0 到 1 播放，因此我们的炸弹将沿着路径移动，而不仅仅是卡在原地。选择放置在时间 5 上的关键帧，并将其值更改为 1。我们还不能测试这个动画，但我们需要它，否则我们的炸弹不会在我们的路径上移动！在 Main_2 场景中的第二个关卡中重做与上述相同的步骤（针对我们的 BombPath）。现在，回到我们的 BombSpawner 场景，我们需要向场景添加新脚本。将其保存在 Scripts 文件夹中。在我们新创建的脚本中，我们需要定义一些变量。首先，我们需要创建对 Bomb 场景的引用。我们可以通过全局 preload 方法来实现这一点，该方法尽早加载我们的 Scene 资源，以提前加载 “loading”作，并避免在性能敏感代码中加载资源。预加载可确保在游戏开始之前将场景加载到内存中，这有助于避免以后的延迟。### BombSpawner.gd extends Node2D #bomb scene reference var bomb = preload("res://Scenes/Bomb.tscn")由于我们有多个关卡，因此需要找到一种方法来动态更改当前场景的名称，以便我们可以相应地获取其路径。换句话说，如果我们在主场景中，我们希望炸弹在 “/root/Main/BombPath/Path2D/PathFollow2D” 路径上生成，如果我们在 Main_2 场景中，我们希望炸弹在 “/root/Main_2/BombPath/Path2D/PathFollow2D” 路径上生成。我们可以使用 Global 脚本来跟踪当前处于活动状态的主场景。我们将使用 ready（） 函数来获取 Player 所在的当前场景的名称（Main 或 Main_2）。我们将通过 current_scene（） 对象来做到这一点。### Global.gd extends Node #movement states var is_attacking = false var is_climbing = false var is_jumping = false #current scene var current_scene_name func _ready(): # Sets the current scene's name current_scene_name = get_tree().get_current_scene().name现在，回到我们的 BombSpawner 脚本中，让我们定义一些变量，这些变量将存储我们的current_scene_path值 （Main 或 Main_2）、我们的炸弹路径 （current_scene_path + /BombPath/Path2D/PathFollow2D） 和我们的炸弹动画路径 （current_scene_path + /BombPath/Path2D/AnimationPlayer）。### Bombspawner.gd extends Node2D #Bomb scene reference var bomb = preload("res://Scenes/Bomb.tscn") #references to our scene, PathFollow2D path, and AnimationPlayer path var current_scene_path var bomb_path var bomb_animation我们将在 ready（） 函数中启动这些变量的值。我们还需要将大炮的默认加载动画设置为 “cannon_idle”。### Bombspawner.gd #older code #when it's loaded into the scene func _ready(): #default animation on load $AnimatedSprite2D.play("cannon_idle") #initiates paths current_scene_path = "/root/" + Global.current_scene_name + "/" #current scene bomb_path = get_node(current_scene_path + "/BombPath/Path2D/PathFollow2D") #PathFollow2D bomb_animation = get_node(current_scene_path + "/BombPath/Path2D/AnimationPlayer") #AnimationPlayer要生成我们的炸弹，我们需要创建一个新函数，该函数将创建炸弹场景的实例并返回它。要创建场景引用的实例，我们使用 instantiate（） 方法。我们还需要播放 cannon_fired 动画，每次调用我们的函数以生成新炸弹时，该动画都会播放。### Bombspawner.gd #older code #spawns bomb instance func shoot(): #play cannon shoot animation each time the function is fired off $AnimatedSprite2D.play("cannon_fired") #returns spawned bomb var spawned_bomb = bomb.instantiate() return spawned_bomb要通过 shoot 函数生成炸弹，我们需要将 Timer 节点的 timeout（） 信号连接到脚本。在 _on_timer_timeout（） 函数中，我们需要检查场景中当前是否没有生成炸弹，如果为 true，我们将调用 shoot（） 函数并在我们的 bomb_path 上生成炸弹。我们可以通过 get_child 或 get_child_count 方法检查 “/BombPath/Path2D/PathFollow2D/” 节点的子节点。您可以使用 add_child 方法将节点添加到定义的路径（例如我们的 bomb_path）。这将在我们的路径下创建一个 Bomb，因此我们的路径最终将看起来像 “/BombPath/Path2D/PathFollow2D/Bomb”。### Bombspawner.gd #older code #shoot and spawn bomb onto path func _on_timer_timeout(): #reset animation before shooting $AnimatedSprite2D.play("cannon_idle") #spawns a bomb onto our path if there are no bombs available if bomb_path.get_child_count() <= 0: bomb_path.add_child(shoot())最后，我们需要检查路径是否有任何更改。如果炸弹已到达路径的末尾（当动画达到 1 值时），我们需要将路径值重置回 0，以便动画可以重新启动，并生成新的炸弹。为了使其正常工作，我们需要创建一个 Global 变量，该变量将改变炸弹移动的状态。如果生成了炸弹，它应该在移动，如果它与玩家碰撞，则它不应该移动。### Global.gd extends Node #movement states var is_attacking = false var is_climbing = false var is_jumping = false #current scene var current_scene_name #bomb movement state var is_bomb_moving = false func _ready(): # Sets the current scene's name current_scene_name = get_tree().get_current_scene().name ### Bomb.gd #older code func _on_body_entered(body): #if the bomb collides with the player, play the explosion animation and start the timer if body.name == "Player": $AnimatedSprite2D.play("explode") $Timer.start() Global.is_bomb_moving = false #OPTION 1 #if the bomb collides with our Level Tilemap (floor and walls). if body.name == "Level" and !body.name.begins_with("Platform"): $AnimatedSprite2D.play("explode") $Timer.start() Global.is_bomb_moving = false #OPTION 2 #if the bomb collides with our Wall scene, explode and remove if body.name.begins_with("Wall"): $AnimatedSprite2D.play("explode") $Timer.start() Global.is_bomb_moving = false ### BombSpawner.gd #older code #spawns bomb instance func shoot(): #play cannon shoot animation each time the function is fired off $AnimatedSprite2D.play("cannon_fired") #sets the bomb to moving and plays bomb animation Global.is_bomb_moving = true bomb_animation.play("bomb_movement") #returns spawned bomb var spawned_bomb = bomb.instantiate() return spawned_bomb现在，在我们的 _on_timer_timeout（） 函数中，我们可以检查我们的炸弹是否没有移动，如果为 true，我们需要移除所有炸弹并停止播放动画，以便我们的进度比率可以重置回 0。我们可以通过运行 For 循环来做到这一点，该循环将计算我们 bomb_path 中的子项，如果存在，则将它们全部删除。### BombSpawner.gd #older code #shoot and spawn bomb onto path func _on_timer_timeout(): #reset animation before shooting $AnimatedSprite2D.play("cannon_idle") #spawns a bomb onto our path if there are no bombs available if bomb_path.get_child_count() <= 0: bomb_path.add_child(shoot()) # Clear all existing bombs if Global.is_bomb_moving == false: for bombs in bomb_path.get_children(): bomb_path.remove_child(bombs) bombs.queue_free() bomb_animation.stop()最后，我们需要关卡场景中的 AnimationPlayer 在开始时播放我们的 bomb_movement 动画。这将允许炸弹在 BombSpawner 进入场景时移动。### Bombspawner.gd #older code #when it's loaded into the scene func _ready(): #default animation on load $AnimatedSprite2D.play("cannon_idle") #initiates paths current_scene_path = "/root/" + Global.current_scene_name + "/" #current scene bomb_path = get_node(current_scene_path + "/BombPath/Path2D/PathFollow2D") #PathFollow2D bomb_animation = get_node(current_scene_path + "/BombPath/Path2D/AnimationPlayer") #AnimationPlayer #starts bomb movement bomb_animation.play("bomb_movement")现在，在您的 Main 场景中，实例化您的 BombSpawner 场景。你想把你的大炮放在你的炸弹路径前面。Main.tscn 中：Main_2.tscn 中：现在，如果你运行你的场景，你的炸弹应该会生成，如果它沿着路径爆炸，它应该会重新生成。炸弹移动得有点太快了，所以让我们将主场景长度中的 bomb_movement 动画改高一点。您必须根据路径的长度测试此值。Main.tscn 中：Main_2.tscn 中：如果您注意到玩家不断与炸弹碰撞（即，他们无法跳过炸弹），请考虑将玩家的 jump_height 变量更改为稍低的值，例如 -110。您还可以将 Bomb （炸弹） 节点的碰撞形状更改为略小一些。您也可以对 Player 的碰撞形状执行相同的作。让我们为这部分做最后一件事。如果 Bomb 正在移动，让我们让 Bomb 滚动。我们可以通过在 physics_process（） 函数中为其 rotate 值添加来做到这一点。在 Bomb 脚本中，创建一个新变量，并将其 speed 设置为您希望 Bomb 旋转的速度。### Bomb.gd extends Area2D var rotation_speed = 10然后，在 physics_process（） 函数中，如果炸弹正在移动，则让我们旋转炸弹。### Bomb.gd #older code #rolls the bomb func _physics_process(delta): # Rotate the bomb if it hasn't exploded if Global.is_bomb_moving == true: $AnimatedSprite2D.rotation += rotation_speed * delta您的代码应如下所示。现在，如果你运行你的场景，你的炸弹应该滚到端点。如果它与您在 Bomb 脚本中定义的碰撞体发生碰撞，它应该将自身从场景中移除，并且生成器应该重新生成一个新的炸弹。您的玩家也应该能够跳过炸弹。游戏开始时大炮第一次开火时会有一点生成延迟，但这并没有破坏交易。恭喜您创建了炸弹生成器！在下一部分中，我们将创建一个 Cannon Handler，它将有一个对话气泡，它会在其中嘲讽我们 — 除了它在游戏中的目的纯粹是为了视觉效果之外！现在是保存工程并备份工程的好时机，以便在发生任何破坏游戏的错误时可以恢复到此部分。在继续这个系列之前，请回去修改你学到的知识，一旦你准备好了，我们下一节见！
通过制作2D平台学习Godot 4-第7部分：炸弹设置
之前我们停在了创建游戏的前两个关卡的地方。在这一部分中，我们将创建炸弹，当炸弹生成器生成炸弹时，炸弹会在场景中移动。我们将在下一部分中创建炸弹生成器。您可以将炸弹想象成《大金刚》中的桶，将炸弹生成器想象成愤怒的大猩猩！您将在本部分中学习到的内容：如何使用 StaticBody2D 节点。如何使用queue_free()从场景中删除节点。让我们创建一个以 Area2D 节点为根的新场景。它应该有以下子节点：AnimatedSprite2D、CollisionShape2D（已为其分配 CircleShape2D）和 Timer 节点。这些节点将允许我们检查碰撞是否与我们的玩家发生碰撞，如果是，它应该播放动画并从我们的场景中移除炸弹。如果炸弹没有与我们的玩家发生碰撞，它应该在几秒钟后“自毁”让我们为场景分配一个新脚本并将其保存在脚本文件夹下。然后，将您的 Timer 节点timeout()信号以及您的 Area2D 节点body_entered()信号连接到您的脚本。在开始编码之前，让我们设置一些动画。在我们的 AnimatedSprite2D 节点中，让我们创建一个新的 SpriteFrames 资源，其中包含两个动画：“移动”和“爆炸”。对于移动动画，请导航至“res://Assets/Kings and Pigs/Sprites/09-Bomb/Bomb On (52x56).png”，并将所有四帧分配给动画。您现在可以将循环和 FPS 保留为默认值。对于爆炸动画，请导航至“res://Assets/Kings and Pigs/Sprites/09-Bomb/Boooooom (52x56).png”，并将所有六帧分配给动画。您现在可以将循环和 FPS 保留为默认值。在您的脚本中，当炸弹进入游戏场景时，让我们将移动动画设为默认动画。### Bomb.gd extends Area2D #default animation on spawn func _ready(): $AnimatedSprite2D.play("moving")在我们的func _on_body_entered(body):中，我们需要检查玩家是否正在进入炸弹场景的碰撞形状，如果是，我们应该播放爆炸动画并启动计时器。计时器将允许我们在从场景中移除炸弹之前延迟 1 秒的动画。### Bomb.gd extends Area2D #default animation on spawn func _ready(): $AnimatedSprite2D.play("moving") func _on_body_entered(body): #if the bomb collides with the player, play the explosion animation and start the timer if body.name == "Player": $AnimatedSprite2D.play("explode") $Timer.start()然后，在timeout()函数中，我们需要从游戏场景树中删除场景。要从场景中删除节点，我们使用对象queue_free()。这是从场景树中删除或“销毁对象”的最标准化方式。还有free()，它们之间的主要区别在于queue_free() 会在下一帧被调用。如果 Bomb 场景有效（即存在），我们才会删除 Bomb 场景。### Bomb.gd extends Area2D #older code #remove the bomb from the scene only if the Bomb exists func _on_timer_timeout(): if is_instance_valid(self): self.queue_free()现在，如果你运行主场景并实例化炸弹，当玩家撞到它时，它就会爆炸并从场景中移除。我们还希望炸弹在到达路径末端时爆炸。我们将在下一部分中设置此路径，但简单地说，此路径将从炸弹生成器的位置延伸到玩家的起始位置。一旦它到达末端且未与玩家发生碰撞，它就会爆炸。我们可以通过一个新场景来实现这一点，这个场景将充当我们的墙壁，或者我们可以引用我们的“关卡”Tilemap 节点作为主体。这意味着如果炸弹与关卡图块地图中的图块发生碰撞，它就会爆炸。这样做的缺点是，你在创建路径时必须非常小心——因为与你的 Tilemap 的任何碰撞都会导致炸弹爆炸。我将向你展示这两种方法。选项 1：使用 Tilemap 引爆炸弹如果炸弹与主场景（和 Main_2）中的“Level”Tilemap 节点发生碰撞，则为了引爆炸弹，我们可以简单地扩展现有的炸弹功能，如下所示：### Bomb.gd #older code func _on_body_entered(body): #if the bomb collides with the player, play the explosion animation and start the timer if body.name == "Player": $AnimatedSprite2D.play("explode") $Timer.start() #if the bomb collides with our Level Tilemap (floor and walls). if body.name == "Level": $AnimatedSprite2D.play("explode") $Timer.start()现在，只要炸弹与 Level 节点上的图块发生碰撞，它就会爆炸。请记住，炸弹只有在与墙壁或地板（任何指定了碰撞的图块）发生碰撞时才会爆炸。您的背景和装饰应该是安全的。将炸弹移到图块上，看看这是否有效。选项 2：使用碰撞场景引爆炸弹如果您希望炸弹仅在到达特定区域时爆炸，则必须创建一个新场景。创建一个以StaticBody2D节点为根的新场景，并向其添加 CollisionShape2D 节点。您可以将碰撞的形状设为 RectangeShape2D，然后将其向下拖动成一个长形。StaticBody2D 节点是静态的 2D 物理的物理体。它适用于地板、墙壁、拾取物，以及我们例子中的碰撞。炸弹场景中的 Area2D 节点将能够与 StaticBody2D 节点发生碰撞。将根重命名为“Wall”，并将场景保存在 Scenes 文件夹下。现在，只需在主场景中实例化您的墙场景。您可以通过缩放模式工具更改墙的 x 和 y 比例。将此墙放置在您想要爆炸区域的位置。这样您就可以将墙做成任何您想要的尺寸。对我来说，这将是我们场景的起点。在我们的脚本中，我们现在可以检查炸弹是否与墙壁相撞。我们将使用字符串的begins_with方法来检查名称是否以“Wall”开头。因此，如果我们在主场景中多次实例化我们的墙壁场景，即使名称是“Wall”、“Wall2”、“Wall_x”等，它也会爆炸。### Bomb.gd #older code func _on_body_entered(body): #if the bomb collides with the player, play the explosion animation and start the timer if body.name == "Player": $AnimatedSprite2D.play("explode") $Timer.start() #if the bomb collides with our Wall scene, explode and remove if body.name.begins_with("Wall"): $AnimatedSprite2D.play("explode") $Timer.start()现在如果你把炸弹放在墙壁区域并运行场景，它就会爆炸！如果您有多面墙，您可以在 Node2D 节点中组织它们。您的最终代码应如下所示。恭喜您创建了炸弹和炸弹触发器！在下一部分中，我们将创建一个炸弹生成器，它将生成炸弹并沿着特定路径移动。每次炸弹爆炸并从场景中移除时，它都会不断生成。现在是保存项目并备份的好时机，这样如果出现任何破坏游戏的错误，您就可以恢复到此部分。在继续本系列之前，请回顾并复习您学到的知识，一旦您准备好，我们将在下一部分与您见面！
在上一部分中，我们为游戏创建了第一个关卡。在这一部分中，我们将通过创建第二个关卡来完成本教程的关卡创建部分，并在现有关卡中添加最后的润色。您将在本部分学习：如何使用定时器节点。如何创建移动平台。如何使用Tilemap层。第1关：最后润色目前，当我们运行关卡时，地图后方仍有默认的灰色背景。我们将借助Tilemap图层把这个背景改为平铺背景。图层能帮助我们区分前景图块和背景图块，让项目结构更清晰。默认情况下，TileMap节点会自带一个预制图层。在主场景中，选中 “Level” 这个Tilemap节点，然后在检查器面板中找到 “Layers”（图层）属性。你会看到已有一个图层，但它没有名字。目前，我们绘制在Tilemap上的所有图块都在这个图层上。我们要把这个图层重命名为 “Foreground”（前景）图层。这时你会发现，Tilemap面板中的 “Layers” 属性已经显示了这一更改。我们希望背景显示在前景图层的后方，所以在 “Layers” 属性中添加一个新元素，并将其命名为 “Background”（背景）。现在你就可以在不同图层间切换了。因为我们希望背景图层在前景图层之后，所以需要通过将 “Background” 图层拖到 “Foreground” 图层上方来重新排列图层顺序。你可以按住图层元素旁边的堆叠线图标进行拖动。现在，在Tilemap面板的 “Layers” 选项中，确保选中了 “Background” 图层。当你选中一个图层时，其他图层上的所有图块都应该会变暗。绘制你的背景。我用的是纯黑色图块，但你可以用任何你喜欢的图块。只是要确保所选图块没有添加任何碰撞体——因为这会阻挡玩家移动！另外，注意不要在梯子上绘制背景。我们会添加另一个Tilemap节点来填充梯子后面的空间。还有一种解决方法，就是在主场景树中将 “Ladders”（梯子）节点拖到 “Tilemap” 节点上方——但我不喜欢梯子突出在地图上方的效果！让我们来填补这个空白。在主场景中复制Tilemap节点，然后把它拖到 “Ladders” 节点后面。我们可以把这个复制的节点重命名为 “Background”。删除这个Tilemap的 “Foreground” 图层，然后简单地绘制剩余的背景部分，去除那些难看的灰色空白区域！（或者直接从 “Level” 节点中删除现有的背景，然后在这里重新绘制）。最终效果应该如下：现在，我们给场景添加一些窗户作为装饰！在 “Level” Tilemap节点中，添加一个名为 “Decorations”（装饰）的新图层。在下方的 “TileSet” 面板中，我们需要为 “Decorations” 创建一个新的图块集。找到 “res://Assets/Kings and Pigs/Sprites/14-TileSets/Decorations (32x32).png”，将这个图片拖到 “Tiles” 属性中。在Tilemap面板中，在 “Decorations” 图层上绘制窗户。你也可以添加其他装饰，比如架子或蜡烛。你可以在 “res://Assets/Kings and Pigs/Sprites/7-Objects/” 目录下的资源中找到更多装饰物品。我最终的效果如下：[此处可能有图片，但文档未提供，可根据描述想象或参考原文]第2关 + 移动平台现在我们已经设置好了第一关，接下来可以创建第二关了。如果你愿意且有时间，也可以创建第3关和第4关。目前，我们只创建第2关。为此，我们可以直接复制主场景，将其命名为 “Main_2”。在新关卡中，玩家实例已经存在。当我们切换场景进入下一关时，玩家会在这里生成，所以要确保玩家位于关卡起点。以下是第二关的布局规划：我们会在顶部设置桶生成器。不过，这次生成器生成的不是桶，而是会沿特定路径滚动的炸弹。我们还会在两侧设置较小的障碍物生成器，它们会向玩家投掷箱子，增加游戏难度。这些箱子也会沿特定路径移动。关卡中还会随机分布生命值、攻击力和得分加成道具。会有上下移动的墙壁阻挡玩家前进。最后，会有玩家出生点和终点，到达终点即可通关。以下是这一关可能的样子：在这一关中，我们会在两侧增加更多炸弹生成器，让游戏更具挑战性，同时还会创建一个移动墙壁，每隔3秒左右阻挡玩家通过。在开始创建关卡之前，我们先创建可移动的墙壁或平台。为此，我们需要新建一个场景，以Area2D节点作为根节点。如果你愿意，也可以使用CharacterBody2D节点——我们只需要一个能处理碰撞和移动的根节点。将这个节点重命名为 “Platform”（平台），并将场景保存在 “Scenes” 文件夹下。给这个新场景添加一个CollisionShape2D节点，形状设为RectangleShape2D。之后我们会调整这个形状的宽度和高度。现在，把主场景中的 “Level TileMap” 复制粘贴到 “Platform” 场景中。我们复制这个节点是为了使用之前创建的自动图块（地形）。删除我们添加的所有图层（“Background”、“Foreground” 和 “Decorations”），这样我们就可以重新绘制了。绘制一个宽2格、高5格的墙壁，并调整碰撞形状，使其与墙壁轮廓匹配。这里给大家一个小提示，确保你的Tilemap不要命名为 “Wall”（墙壁）或 “Level”（关卡），否则之后炸弹与这个平台交互时会爆炸。现在，在 “Platform” 场景中连接一个新脚本，并将这个脚本保存在 “Scripts” 文件夹下。我们还需要给场景添加最后一个节点——Timer（定时器）节点。定时器节点会创建一个倒计时器，倒计时结束时会发出信号。我们希望这个定时器在 “x” 秒后改变墙壁的移动状态。我们还要将定时器节点的 “timeout()” 信号连接到脚本。每次定时器倒计时结束，这个信号就会发出。这样墙壁就会移动，倒计时结束后再次移动。定时器会触发这些移动状态的改变。在定时器节点的 “Signals”（信号）面板中，将 “timeout()” 信号连接到脚本。这会在脚本中创建一个 “func _on_timer_timeout():” 函数。在这个函数中，我们将改变平台的移动状态。在此之前，我们首先需要定义移动状态。我们的平台会垂直移动，在底部等待，然后向上移动，在顶部等待，再向下移动，如此循环。为了改变平台的移动状态，我们需要创建一个枚举来存储这些状态。枚举（也称为enumerations）是一种数据类型，包含一组固定的常量。# Platform.gd extends Area2D # 平台移动状态 enum State {WAIT_AT_BOTTOM, MOVING_UP, WAIT_AT_TOP, MOVING_DOWN}接下来，在 “_on_timer_timeout():” 函数中，我们可以改变状态。如果平台处于 “WAIT_AT_TOP” 状态，就应该将状态改为 “MOVING_DOWN”；如果处于 “WAIT_AT_BOTTOM” 状态，就应该将状态改为 “MOVING_UP”。我们还需要创建一个变量，用于记录游戏开始时以及状态改变时平台的状态。初始时，我们将其设为 “WAIT_AT_BOTTOM”。# Platform.gd extends Area2D # 平台移动状态 enum State {WAIT_AT_BOTTOM, MOVING_UP, WAIT_AT_TOP, MOVING_DOWN} # 记录平台当前状态 var current_state = State.WAIT_AT_BOTTOM # 定时器超时时平台方向改变 func _on_timer_timeout(): if current_state == State.WAIT_AT_TOP: switch_state(State.MOVING_DOWN) if current_state == State.WAIT_AT_BOTTOM: switch_state(State.MOVING_UP)我们还需要记录游戏开始时平台的y坐标，因为当平台移动状态改变时，我们改变的就是y值。让我们创建一个新变量，用于在游戏加载时设置平台的y坐标。# Platform.gd extends Area2D # 平台移动状态 enum State {WAIT_AT_BOTTOM, MOVING_UP, WAIT_AT_TOP, MOVING_DOWN} # 记录平台当前状态 var current_state = State.WAIT_AT_BOTTOM # 移动位置 var initial_position我们将在内置的 “ready()” 函数中设置这个变量的值，因为当节点添加到场景中时，这个函数就会被调用。在这个函数中，我们将 “initial_position” 变量设为平台当前的y坐标，并将其状态设为 “MOVING_UP”，因为 “current_state” 是 “WAIT_AT_BOTTOM”——我们希望游戏一加载，状态就发生改变。我们可以通过访问节点的 “position” 属性来获取节点的位置。# Platform.gd # 旧代码 # 在游戏开始时设置平台的y坐标并切换状态 func _ready(): initial_position = position.y switch_state(State.MOVING_UP)现在，我们还需要给平台设置一些其他变量，用于定义其移动速度（移动的快慢）、移动范围（上下移动的距离）、进度（是否完成移动）以及顶部和底部的等待时间（上下移动前等待的时长）。我们将导出这些变量，这样就可以在检查器面板中进行修改。这使得我们能够单独改变每个实例化平台的移动值——也就是说，有些平台移动得更快，有些等待时间更长，还有些会移动得更高。# Platform.gd extends Area2D # 平台移动状态 enum State {WAIT_AT_BOTTOM, MOVING_UP, WAIT_AT_TOP, MOVING_DOWN} # 记录平台当前状态 var current_state = State.WAIT_AT_BOTTOM # 移动位置和移动进度值 var initial_position var progress = 0.0 # 平台移动速度和范围 @export var movement_speed = 50.0 @export var movement_range = 50 # 等待时间 @export var wait_time_at_top = 3.0 # 在顶部等待的秒数 @export var wait_time_at_bottom = 3.0 # 在底部等待的秒数我们需要创建一个函数，持续切换平台的状态。根据新状态，它应该重置平台的移动进度，设置定时器及其等待时间（倒计时所需的秒数），并更改 “current_state” 变量的值。我们将使用 “match” 语句来检查和更改平台的状态。“match” 语句用于分支程序的执行，它类似于许多其他语言中的 “switch” 语句，但提供了一些额外功能。# Platform.gd # 旧代码 # 更改平台的移动状态 func switch_state(new_state): current_state = new_state match new_state: # 如果状态为向上移动，重置进度 State.MOVING_UP: progress = 0.0 # 如果状态为在顶部等待，启动定时器以更改状态 State.WAIT_AT_TOP: $Timer.wait_time = wait_time_at_top # 将等待x秒后再移动 $Timer.start() # 如果状态为在底部等待，启动定时器以更改状态 State.WAIT_AT_BOTTOM: $Timer.wait_time = wait_time_at_bottom # 将等待x秒后再移动 $Timer.start() # 如果状态为向下移动，根据定义的速度和范围移动平台 State.MOVING_DOWN: progress = movement_range / movement_speed最后，我们可以在 “physics_process()” 函数中调用新创建的 “switch_state()” 函数。这个函数每一个物理帧都会被调用。我们将用它来检查平台的当前状态，并根据状态移动或等待。“Lerp” 函数用于在底部和顶部位置之间进行平滑插值。# Platform.gd # 旧代码 # 移动我们的平台 func _physics_process(delta): match current_state: # 如果正在向上移动 State.MOVING_UP: progress += delta # 更改位置 position.y = lerp(initial_position, initial_position - movement_range, progress / (movement_range / movement_speed)) if progress >= (movement_range / movement_speed): switch_state(State.WAIT_AT_TOP) # 如果正在向下移动 State.MOVING_DOWN: progress -= delta # 更改位置 position.y = lerp(initial_position, initial_position - movement_range, progress / (movement_range / movement_speed)) if progress <= 0: switch_state(State.WAIT_AT_BOTTOM)在这个脚本中，我们让平台在两个点之间垂直移动，在顶部和底部有等待时间。由于使用了线性插值（lerp），移动过程很平滑，并且状态机制确保了移动的不同阶段都能得到正确处理。如果你在 “Main_2” 场景中实例化 “Platform”，你会发现可以在检查器面板中更改其值。确保将其放置在地面上，并设置移动范围，使其到达上方地面时停止——你需要让平台的边缘与上下方的地面连接。你可以按F6测试当前场景，来测试这些值。它的放置位置应该如下：并且它的移动范围应该调整为如下停止状态：我将实例化的平台组织在一个Node2D节点中，并为每个平台设置了独特的速度、范围和等待时间。现在你可以继续为第二关创建地图了。你可以通过在 “Level” 节点中绘制已有图块来完成。记住，要为墙壁上下移动留出足够空间，因为它会阻挡两层楼的区域。这是我为第二关创建的地图：如果你运行场景，玩家应该能够顺利到达顶部，并且平台也会移动！故障排除：修复玩家跳跃的小故障如果你测试过游戏，可能会注意到玩家跳跃并改变方向时会出现一个小故障。玩家跳跃时，如果按下向左或向右的输入，会在继续跳跃前稍微改变方向。我们希望玩家先完成跳跃动画，然后再向新方向奔跑，而不是在跳跃过程中改变方向。为此，我们需要修改 “player_animations()” 函数，使其仅在玩家不跳跃时运行。在此之前，我们先在 “Global” 脚本中创建一个新变量来跟踪跳跃状态。# Global.gd extends Node # 移动状态 var is_attacking = false var is_climbing = false var is_jumping = false然后，在 “player_animations()” 函数中，我们只希望在玩家不跳跃时触发奔跑动画。# Player.gd # 旧代码 # 动画 func player_animations(): # 向左（添加is_action_just_released，这样跳跃后能继续奔跑） if Input.is_action_pressed("ui_left") && Global.is_jumping == false: $AnimatedSprite2D.flip_h = true $AnimatedSprite2D.play("run") $CollisionShape2D.position.x = 7 # 向右（添加is_action_just_released，这样跳跃后能继续奔跑） if Input.is_action_pressed("ui_right") && Global.is_jumping == false: $AnimatedSprite2D.flip_h = false $AnimatedSprite2D.play("run") $CollisionShape2D.position.x = -7 # 空闲状态，当没有按键按下时 if !Input.is_anything_pressed(): $AnimatedSprite2D.play("idle")接下来，当玩家跳跃时，我们需要将 “is_jumping” 变量的状态改为 “true”。当重力值重置时，我们还需要将这个值重置为 “false”。# Player.gd # 旧代码 # 单个输入捕获 func _input(event): # 攻击 if event.is_action_pressed("ui_attack"): Global.is_attacking = true $AnimatedSprite2D.play("attack") # 跳跃 if event.is_action_pressed("ui_jump") and is_on_floor(): velocity.y = jump_height $AnimatedSprite2D.play("jump") # 爬梯子 if Global.is_climbing == true: if Input.is_action_pressed("ui_up"): $AnimatedSprite2D.play("climb") gravity = 100 velocity.y = -160 Global.is_jumping = true # 重置重力 else: gravity = 200 Global.is_climbing = false Global.is_jumping = false现在如果你运行场景，玩家应该能正常跳跃并正确切换奔跑动画！恭喜你创建了带有移动平台的第二关！你还学习了图层的使用，希望你最终做出了一个很棒的地图！如果你愿意，还可以创建第3关和第4关，添加更多平台和敌人区域。说到敌人，我们将在下一部分创建炸弹和炸弹生成器。这个炸弹生成器会生成炸弹，炸弹会沿着特定路径移动，到达终点或与玩家碰撞时会爆炸。现在是保存项目并备份的好时机，这样如果出现任何导致游戏崩溃的错误，你可以恢复到这一阶段。在继续学习本系列之前，回顾一下所学内容。准备好后，我们下一部分再见！
在上一部分中，我们为游戏的边界创建了基本碰撞和地形。我决定将 Tilemaps 部分分成几个部分，以防止信息过载。如果这是您第一次使用 Godot 中的 TileMap 节点，请不要担心，我保证您练习得越多就会变得越容易。这就是为什么我鼓励您在每部分结束时花时间复习您所做的事情。在这一部分中，我们将创建我们的梯子以及其余的平台。您将在本部分中学习到的内容：如何创建 Area2D 场景。如何连接 Area2D 信号。如何创建全局自动加载单例。阶梯创造在绘制玩家可以跳上的其他平台之前，我们首先需要创建梯子。我们将在新场景中执行此操作。让我们创建一个以Area2D节点为根节点的新场景。此节点创建一个 2D 区域，用于检测与其形状重叠、进入或退出的CollisionObject2D节点。我们将使用此 Area2D 节点检测玩家场景是否正在进入其碰撞形状，如果是，我们将信号指示is_climbing变量为真，这反过来又允许玩家攀爬。此节点上出现警告，因为它没有形状，因此为其分配一个以 RectangleShape2D 为形状的 CollisionShape2D 节点。我们将根节点重命名为“Ladder”。将此场景保存在您的“Scenes”文件夹下。让我们通过向 Ladder 场景添加 Sprite2D 节点来可视化我们的 Ladder。将图像“res://Assets/wood_set/ladder/28x128/2.png”分配给其纹理。现在，修复碰撞形状以勾勒出梯子的轮廓。将新脚本附加到您的 Ladder 场景并将该脚本保存在您的 Scripts 文件夹中。在这个脚本中，我们将获得对 Player 的 is_climbing 变量的引用。如果我们的 Player 进入梯子的碰撞，我们将触发他们的is_climbing变量为真。如果他们离开梯子的碰撞，我们将触发他们的is_climbing变量为假。我们可以使用 Area2D 节点的body_entered()和 body_exited() 信号触发这些变化。当接收的物体（我们的玩家）进入此区域时，会发出body_entered()信号。当接收的物体离开此区域时，会发出body_exited()信号。让我们将这两个信号连接到我们的 Ladder 脚本。这将创建两个新函数，_on_body_exited()和_on_body_entered()。现在，我们可以通过我们的Loading资源获取对我们的玩家节点的引用来实现这一点，但让我们用更好的方法来实现。我们将创建一个 Global 脚本，该脚本将添加到我们的AutoLoad Singleton中。Singleton模式是解决需要在场景之间存储持久信息的常见用例的有用工具。这将使我们能够从游戏中的任何其他场景或脚本访问 Global 脚本中的变量 - 而无需先将它们作为资源加载！让我们在脚本文件夹中创建一个新脚本，并将其命名为“Global”。现在，让我们将其添加到我们的自动加载资源中，以便我们可以全局访问此脚本的值。要将其添加到您的自动加载资源中，请进入您的项目设置 > 自动加载，然后将 Global.gd 文件添加到列表中。接下来，我们需要对 Player 脚本进行一些重构。我希望将is_climbing和is_attacking变量从 Player 脚本移至 Global 脚本。### Global.gd extends Node #移动状态 var is_attacking = false var is_climbing = false现在，在我们的玩家脚本中，让我们用来自 Global 单例的引用替换我们的变量（例如，用 Global.is_climbing 替换 is_climbing，用Global.is_attacking替换 is_attacking）。### Player.gd extends CharacterBody2D #player movement variables @export var speed = 100 @export var gravity = 200 @export var jump_height = -100 #movement and physics func _physics_process(delta): # vertical movement velocity (down) velocity.y += gravity * delta # horizontal movement processing (left, right) horizontal_movement() #applies movement move_and_slide() #applies animations if !Global.is_attacking: player_animations() #horizontal movement calculation func horizontal_movement(): # if keys are pressed it will return 1 for ui_right, -1 for ui_left, and 0 for neither var horizontal_input = Input.get_action_strength("ui_right") - Input.get_action_strength("ui_left") # horizontal velocity which moves player left or right based on input velocity.x = horizontal_input * speed #animations func player_animations(): #on left (add is_action_just_released so you continue running after jumping) if Input.is_action_pressed("ui_left") || Input.is_action_just_released("ui_jump"): $AnimatedSprite2D.flip_h = true $AnimatedSprite2D.play("run") #on right (add is_action_just_released so you continue running after jumping) if Input.is_action_pressed("ui_right") || Input.is_action_just_released("ui_jump"): $AnimatedSprite2D.flip_h = false $AnimatedSprite2D.play("run") #on idle if nothing is being pressed if !Input.is_anything_pressed(): $AnimatedSprite2D.play("idle") #singular input captures func _input(event): #on attack if event.is_action_pressed("ui_attack"): Global.is_attacking = true $AnimatedSprite2D.play("attack") #on jump if event.is_action_pressed("ui_jump") and is_on_floor(): velocity.y = jump_height $AnimatedSprite2D.play("jump") #on climbing ladders if Global.is_climbing == true: if Input.is_action_pressed("ui_up"): $AnimatedSprite2D.play("climb") gravity = 100 velocity.y = -200 #reset gravity else: gravity = 200 Global.is_climbing = false #reset our animation variables func _on_animated_sprite_2d_animation_finished(): Global.is_attacking = false Global.is_climbing = false创建全局变量后，我们可以返回 Ladder 脚本，并在Player 场景进入/退出 Area2D 主体时将is_climbing值设置为 true 或 false。我们可以通过检查进入的主体是否具有“Player”名称（这是我们 Player 场景的根节点的名称）来执行此操作，如果为 true，则我们相应地设置布尔值。### Ladder.gd extends Area2D #sets is_climbing to true to simulate climbing func _on_body_entered(body): if body.name == "Player": Global.is_climbing = true #sets is_climbing to false to simulate climbing func _on_body_exited(body): if body.name == "Player": Global.is_climbing = false现在，我们可以在主场景中实例化梯子场景。将其拖到靠近玩家的位置，以便测试is_climbing值是否正在变化。10.jpg现在，如果您运行场景，并且您的玩家跑进梯子区域，则只要您按下键盘上的 W 或 UP，它就会允许您攀爬。您可以根据需要将此“攀爬”输入的 velocity.y 值更改为更高或更低。您会注意到，如果我们在攀爬时按下其他输入来奔跑，我们的玩家将过渡到这些动画，而不是停留在攀爬动画中。让我们通过在玩家攀爬时禁用这些动画来解决这个问题。### Player.gd #older code #movement and physics func _physics_process(delta): # vertical movement velocity (down) velocity.y += gravity * delta # horizontal movement processing (left, right) horizontal_movement() #applies movement move_and_slide() #applies animations if !Global.is_attacking || !Global.is_climbing: player_animations()您的代码现在看起来应该是这样的。由于我们将在场景中添加多个梯子，因此让我们将梯子组织为 Node2D 节点的子节点。添加一个新的 Node2D 节点并将其重命名为“梯子”。我们可以将现有的梯子场景拖入其中作为梯子节点的子节点。平台创建创建梯子后，我们终于可以发挥创造力了，因为现在是时候创建关卡了。我们已经有了 Platform_Floor 地形，现在我们将使用它来构建通往出口的其余楼层。这是我们游戏的布局计划：我们将桶生成器放在顶部。我们的生成器将生成沿特定路径滚动的炸弹，而不是桶。我们还会将较小的障碍物生成器放在两侧，它们会向玩家投掷箱子，增加游戏难度。这些箱子也会遵循特定的路径。我们还将在我们的关卡中随机分布实时、攻击和得分提升拾取物。最后，我们将有一个玩家生成的起点和一个完成关卡的终点。以下是该级别的示例：创建关卡时请记住这些对象。让我们继续绘制平台地板。确保有足够的空间放置梯子，并尝试将地板绘制在玩家头顶上方 3 块瓷砖处。玩家应该有足够的空间跳跃，以及跨平台，但不要太容易或间隙太大。要添加更多梯子，只需实例化新场景。您可以通过更改梯子的 y 比例来更改梯子的高度，您可以在节点的 Inspector 面板的 Transform > Scale 下找到它。请记住取消链接，以便它只更改 y 值。还应该有足够的空间让玩家爬上梯子而不会被平台的碰撞阻挡。另外，请记住将您的 Ladder 节点移到 TileMap 节点后面。这就是我最终为我的第一个关卡所创建的内容：运行场景来测试玩家是否可以顺利地从起点到达终点，并在需要时进行修复。故障排除：我的播放器在梯子上停下来时继续奔跑！我们需要检查 ui_climbing 输入中是否再次按下了任何按钮。如果没有按下任何按钮，我们的动画应该重置为空闲动画，但如果我们正在攀爬，我们的攀爬动画应该播放！### Player.gd #older code #singular input captures func _input(event): #older code #on climbing ladders if Global.is_climbing == true: if !Input.is_anything_pressed(): $AnimatedSprite2D.play("idle") if Input.is_action_pressed("ui_up"): $AnimatedSprite2D.play("climb") gravity = 100 velocity.y = -160恭喜您创建了第一个关卡的平台！在下一部分中，我们将为关卡添加一些装饰，使其感觉更完整一些，例如带有几个窗口的背景。我们还将创建第二个关卡。稍后我们将用炸弹生成器、盒子生成器和拾取物品填充这些关卡 — 但现在，让我们专注于创建关卡的基础。现在是保存项目并备份的好时机，这样如果出现任何破坏游戏的错误，您就可以恢复到此部分。在继续本系列之前，请回顾并复习您学到的知识，一旦您准备好，我们将在下一部分与您见面！
在本教程系列的开头，我提到过这款游戏的灵感来源于1981年版本的《大金刚》。我再次提起这一点，是因为当我研究原版游戏以分析其机制和玩法时，发现原版《大金刚》只有四个关卡。四个关卡与当今大多数游戏相比似乎微不足道——我的意思是，我们可能都认识某个在《糖果粉碎传奇》中玩到506关的人。图7：经典《大金刚》的四个关卡 在当时，程序化生成关卡并不常见。程序化生成是一种通过算法生成关卡或内容的技术，而不是手动设计，这种技术在许多视频游戏中都有应用，尤其是在类Rogue游戏和开放世界游戏中。然而，原版《大金刚》及其早期续作主要依赖于手工设计的关卡。这意味着开发者自己创建了关卡，这也解释了为什么《大金刚》只有四个关卡。在Godot 4中创建一个程序化生成的《大金刚》风格关卡，意味着要通过算法生成平台、梯子和其他元素，而不是手动放置它们。由于这是一个初学者教程，我们今天不会深入探讨程序化生成的复杂性，而是保持传统，手动创建四个关卡。除了关于高级库存、任务和对话系统的完整教程系列外，我还在制作一个专注于程序化生成世界和地图的独立系列。这些教程将在今年晚些时候免费提供。在本部分中，你将学习：如何创建TileMap资源。如何添加物理和地形层。如何更改TileSet属性。让我们分解一个《大金刚》关卡的基础，以确定我们的关卡需要哪些对象：目前，我们只专注于添加平台和梯子。我们需要在创建每个关卡时考虑起点和终点。我建议你阅读MakeCode的这篇文章，它为你提供了平台游戏关卡设计基础的概述。让我们从基础开始，即在游戏屏幕上绘制游戏边框（城堡的外墙）。我们将使用TileMap节点来实现这一点。TileMap是用于创建游戏布局的网格。要使用TileMap，首先需要创建一个TileSet。TileSet是可以放置在TileMap节点中的图块集合。简单来说，TileMap是我们用来创建地图或环境的完整图块集合。例如：TileSet是将TileMap分解为小立方体的资源，然后我们将这些小立方体视为独立对象。例如：让我们在主场景中添加一个新的TileMap节点，并将其重命名为“Level”。要使用这个TileMap，我们需要为其分配一个TileSet资源。我们可以在选择TileMap节点后，在检查器面板中分配一个新的TileSet资源。我们需要在Tile Set属性旁边的<空>块中分配一个“新建TileSet”。这将在编辑器下方打开两个新的资源块：Tilemap和Tileset。我们将使用Tilemap面板将TileSet绘制到屏幕上，并使用Tileset面板为Tilemap面板分配TileSheet以进行绘制。我们还可以在Tileset面板中更改单个图块的属性。让我们为Tilemap分配一个新的TileSheet，以便绘制城堡边框。为此，点击Tileset面板，然后在文件系统管理器面板中将“res://Assets/Kings and Pigs/Sprites/14-TileSets/Terrain (32x32).png” TileSheet拖到Tiles块中。你可以根据需要拖入任意数量的TileSheet，因为我们将使用许多TileSheet来创建我们的世界。会弹出一个窗口，询问你是否要创建网格。选择“是”，因为这将把我们的TileSheet分解为可管理的小块。现在我们可以回到Tilemap面板，你会看到TileSheet已经可用。你可以使用上面的工具选择、绘制和擦除图块。如果你选择一个图块并将其绘制到屏幕上，它应该会被绘制为一个新对象！以下是Tilemap面板中可用的绘制工具概述：Tilemap碰撞 在开始绘制边框之前，我们首先需要为地板和墙壁图块分配物理层。这将为每个图块提供碰撞形状。为此，选择TileMap节点，点击检查器中的TileSet属性值进行编辑，然后展开物理层并选择添加元素。现在我们可以回到Tileset面板为图块配置碰撞。以下是Tileset面板中可用的工具概述：我们的TileSet只包含立方体形状的物品——因为它只有地板和墙壁。因此，我们可以使用绘制模式面板批量添加碰撞。如果我们有不同形状的图块，比如一棵树，我们将在选择模式面板中为其添加碰撞，因为我们需要手动绘制其形状。我们可以在选择和绘制模式面板中的物理层属性下绘制单个碰撞形状。如果你选择一个图块并导航到其物理层属性，你可以在红色轮廓的块中绘制形状。在这里，你将绘制出物品的形状。正如我之前提到的，我们的形状是简单的正方形，因此我们可以在绘制模式面板中加快这一过程。选择“绘制”面板并导航到物理层属性。你仍然可以像在设置模式面板中一样绘制形状，但在这里我们可以批量添加这个形状到多个图块。让我们不要改变这个形状，因为我们只需要一个立方体。如果你点击一个图块，蓝色框会勾勒出它，这意味着碰撞形状已经从框中添加到图块中！*记住不要为你希望玩家穿过的图块或用作背景的图块添加碰撞——因为它会阻挡玩家。按如下方式绘制碰撞：Tilemap地形 现在我们已经为墙壁边框添加了碰撞，我们需要继续绘制它们。我们可以手动绘制每个图块，或者通过使用自动图块来加快这一过程。Godot提供了地形功能，可以自动为墙壁、地板或地形等图块创建连接，这样我们就不必手动绘制图块的角落和边缘。这允许你自动使用“正确”的图块变体。图7：地形与无地形 要添加地形，选择TileMap节点，进入检查器并选择你的TileSet资源。然后添加一个新的地形层，这将创建一个需要地形元素的地形集。地形集可以是类似“草地”的东西，而地形元素可以是类似“草地_森林”的东西。该集合将包含同一类别的所有元素。让我们将地形元素重命名为“平台”，并将其颜色更改为易于识别的颜色。现在如果我们进入Tileset面板，在设置或绘制模式下，我们可以看到地形属性现在可用。在选择模式面板中，你会看到我们的地形集和地形都有ID值。-1表示“无地形集”或“无地形”，这意味着你必须将地形集设置为0或更大，然后才能将地形设置为0或更大。在我们的情况下，地形集“0”将包含我们所有的平台，地形“0”是我们的“平台”地形元素。然后你可以为其分配位对等值。对等位决定了将放置哪个图块，具体取决于相邻图块。例如，如果一个图块的所有位都设置为0或更大，则只有当所有8个相邻图块都使用具有相同地形ID的图块时，它才会出现。如果一个图块的位设置为0或更大，但左上、上和右上位设置为-1，则只有当其上方（包括对角线）有空间时，它才会出现。我们不会在选择模式面板中分配位对等值，而是会在绘制模式面板中将其绘制到地图上。你可以在绘制模式面板中的地形属性中直接选择地形元素并进行设置，而无需提供ID。让我们选择我们的平台元素。现在你可以通过点击图块来绘制位对等值。记住不要在明显的角落或尖锐边缘上绘制位对等值，因为这在绘制地形时会创建奇怪的形状。按如下方式绘制平台地形：如果你现在回到Tilemap面板，在地形下，你可以看到平台元素的“自动图块”已经创建。如果你选择平台元素并在地图上绘制，形状应该会自动创建并匹配！让我们在地形集中创建两个新的地形元素，分别称为“Platform_Wall”和“Platform_Floor”。按如下方式绘制Platform_Wall地形：按如下方式绘制Platform_Floor地形：如果你现在回到Tilemap面板，在地形下，你的新地形应该已经创建。让我们使用这些地形在游戏屏幕边缘绘制一个边框——即蓝色边框。确保你的玩家保持在这个边框内。在测试之前，我们首先需要修复玩家的碰撞形状。如果你在调试>可见碰撞形状选项中启用碰撞，并运行场景，你会注意到如果玩家向左或向右跑（由于动画帧中的空间），碰撞形状会远离玩家。这是一个问题，因为如果障碍物击中我们的碰撞形状，即使技术上没有击中我们，玩家也会在之后失去生命！我们可以通过将碰撞形状居中到AnimatedSprite2D节点来解决这个问题。让我们将CollisionShape2D节点的x位置偏移量更改为-7。这将在游戏开始时使形状居中。在检查器面板中，在变换>位置下，将x值更改为-7，这将使形状居中。还要确保矩形的形状覆盖玩家的整个身体。现在，在我们的代码中，我们需要在玩家改变方向时保持这个形状居中——因为动画有空间偏移。Player.gd# 旧代码 # 动画 func player_animations(): # 向左（添加 is_action_just_released，以便在跳跃后继续奔跑） if Input.is_action_pressed("ui_left") || Input.is_action_just_released("ui_jump"): $AnimatedSprite2D.flip_h = true $AnimatedSprite2D.play("run") $CollisionShape2D.position.x = 7 # 向右（添加 is_action_just_released，以便在跳跃后继续奔跑） if Input.is_action_pressed("ui_right") || Input.is_action_just_released("ui_jump"): $AnimatedSprite2D.flip_h = false $AnimatedSprite2D.play("run") $CollisionShape2D.position.x = -7 # 如果没有按下任何键，则处于空闲状态 if !Input.is_anything_pressed(): $AnimatedSprite2D.play("idle")你的代码应该看起来像这样。如果我们现在运行场景，边框应该会阻止玩家掉落，因为我们为其添加了碰撞，我们终于可以四处跳跃了！我们的碰撞形状现在也应该保持与玩家位置居中。这就是我们在本部分中要做的所有内容。我希望你休息一下，回顾一下你刚刚学到的概念——并阅读提供的链接中的文档，以深入了解地形、Tilemap和TileSet。在下一部分中，我们将添加梯子以及其余的平台。现在是保存项目并备份项目的好时机，以便在出现任何破坏游戏的错误时可以恢复到这一部分。在继续本系列之前，回去复习你所学的内容，一旦你准备好了，我们下一部分见！
创建完 Player 场景后，我们现在可以继续为 Player 添加动画，然后将其连接到脚本中的输入。我们希望 Player 在按下ui_left和ui_right时有奔跑动画，站立时有静止动画，按下ui_up时有攀爬动画，按下ui_jump时有跳跃动画，按下 ui_attack 时有攻击动画（我们仍需创建）。我们还希望 Player 受到伤害和死亡时有动画。您将在本部分中学习到的内容：如何创建多个 AnimatedSprite2D 动画。如何将节点信号连接到脚本。首先，让我们添加ui_attack输入操作。我们将为攻击分配 SHIFT 键。我们的玩家在拾取攻击增强拾取物时将需要此攻击动画和输入，这将使其能够在短时间内摧毁箱子和炸弹。现在，让我们创建动画，从奔跑动画开始。我们将使用此奔跑动画进行左跑和右跑，因此我们不需要创建 run_left 和 run_right 动画。让我们创建一个新动画（单击动画面板中的纸张图标）并将其命名为“run”。导航到 Assets 文件夹中的“res://Assets/Kings and Pigs/Sprites/01-King Human/Run (78x58).png”，然后裁剪出动画帧，如下图所示。将其 FPS 更改为 10 并保持循环开启。创建新动画并将其命名为“jump”。导航至 Assets 文件夹中的“res://Assets/Kings and Pigs/Sprites/01-King Human/Jump (78x58).png”，然后裁剪出动画帧，如下图所示。将其 FPS 更改为 5 并保持其循环开启。创建新动画并将其命名为“climb”。导航至 Assets 文件夹中的“res://Assets/Kings and Pigs/Sprites/01-King Human/Door In (78x58).png”，然后裁剪出动画帧，如下图所示。将其 FPS 更改为 10 并保持循环开启。创建新动画并将其命名为“attack”。导航至 Assets 文件夹中的“res://Assets/Kings and Pigs/Sprites/01-King Human/Attack (78x58).png”，然后裁剪出动画帧，如下图所示。将其 FPS 更改为 7 并关闭其循环。创建新动画并将其命名为“damage”。导航至 Assets 文件夹中的“res://Assets/Kings and Pigs/Sprites/01-King Human/Hit (78x58).png”，然后裁剪出动画帧，如下图所示。将其 FPS 更改为 2 并关闭其循环。最后，创建一个新的动画并将其命名为“死亡”。导航到 Assets 文件夹中的“res://Assets/Kings and Pigs/Sprites/01-King Human/Dead (78x58).png”，然后裁剪出动画帧，如下图所示。将其 FPS 更改为 5 并关闭其循环。创建动画后，我们现在可以继续向我们的 Player 脚本添加一个函数，该函数将根据捕获的输入操作更改动画。让我们从奔跑动画开始。如果我们的玩家按下ui_left或ui_right，我们的奔跑动画就会播放。我们将水平翻转精灵，以便我们可以重复使用“奔跑”动画进行左右移动。我们还需要确保我们没有按下ui_jump输入，否则，我们将在跳跃时奔跑！我们将使用我们的 Input 单例来检查这些输入，因为我们想在我们的physics_process函数中调用这个player_animations()函数，以便我们的动画可以在每一秒重新捕获。### Player.gd #旧代码 #动画 func player_animations(): #在左边 (添加 is_action_just_released 以便你在跳跃后继续奔跑) if Input.is_action_pressed( "ui_left" ) || Input.is_action_just_released( "ui_jump" ): $AnimatedSprite2D.flip_h = true $AnimatedSprite2D.play( "run" ) #在右边 (添加 is_action_just_released 以便你在跳跃后继续奔跑) if Input.is_action_pressed( "ui_right" ) || Input.is_action_just_released( "ui_jump" ): $AnimatedSprite2D.flip_h = false $AnimatedSprite2D.play( "run" )现在，如果你注释掉 move_and_slide 以暂时阻止玩家掉落，并且我们在physics_process() 函数中调用player_animations()函数，我们就可以运行我们的场景，当我们按下键盘上的 A 或 D 时，我们的左右奔跑动画应该会播放。我们将看到动画的急剧翻转非常明显，但这是由于我们的动画帧之间的间隙很大（我在上一部分中解释过）。我为此道歉，因为它看起来很丑——但现在只能这样了。# 旧代码 # 运动和物理 func _physics_process(delta): # 垂直运动速度（向下） velocity.y +=gravity * delta # 水平运动处理（左、右） Horizo​​ntal_movement() # 应用运动 #move_and_slide() # 应用动画 player_animations()接下来，如果玩家没有按下任何输入操作，让我们播放空闲动画。如果您运行场景，则如果玩家没有运行，则应该处于空闲状态。### Player.gd #旧代码 #动画 func player_animations(): #在左边 (添加 is_action_just_released 以便你在跳跃后继续奔跑) if Input.is_action_pressed( "ui_left" ) || Input.is_action_just_released( "ui_jump" ): $AnimatedSprite2D.flip_h = true $AnimatedSprite2D.play( "run" ) #在右边 (添加 is_action_just_released 以便你在跳跃后继续奔跑) if Input.is_action_pressed( "ui_right" ) || Input.is_action_just_released( "ui_jump" ): $AnimatedSprite2D.flip_h = false $AnimatedSprite2D.play( "run" ) #如果没有按下任何按钮，则在空闲状态 if !Input.is_anything_pressed(): $AnimatedSprite2D.play( "idle" )我们还为攻击动画添加攻击输入。当我们添加攻击增强时，我们将禁用此动画的播放。我们将在 input() 函数中捕获此输入，因为我们不希望它每秒都处理一次。这将导致玩家每次想要攻击时都必须按下它。我们还将创建一个变量来检查我们是否正在攻击，这样如果我们的玩家正在攻击，我们就可以停止处理其他动画。这将使我们能够奔跑，然后停止攻击并继续奔跑。在下面的代码中，我们看到，如果我们没有按下攻击输入，它将我们的is_attacking变量设置为 true，它只会播放我们的奔跑和空闲动画。### Player.gd 扩展了 CharacterBody2D #player 运动变量 @export var speed = 100 @export var grave = 200 @export var jump_height = - 100 var is_attacking = false #运动和物理 func _physics_process(delta): #垂直运动速度（向下） velocity.y += grave * delta #水平运动处理（左、右） Horizo​​ntal_movement() #应用运动 #move_and_slide() #应用动画 if !is_attacking: player_animations() #旧代码 #单数输入捕获 func _ input (event): #攻击 if event.is_action_pressed( "ui_attack" ): is_attacking = true $AnimatedSprite2D.play( "attack" )我们还需要将此变量重置回 false，否则，我们的玩家可能会卡在这个攻击动画中。我们可以通过 AnimatedSprite2D 节点的animation_finished() 信号来做到这一点。信号是 Godot 内置的一种委托机制，允许一个游戏对象对另一个游戏对象的变化做出反应，而无需它们相互引用。每当动画帧播放完毕时，此信号就会触发或发出。当我们的攻击动画播放完毕后，我们将is_attacking值重置回 false，以便我们的其他动画可以播放。要附加信号，请单击 AnimatedSprite2D 节点，然后在“信号”下的“节点”面板中双击animation_finished选项，并将其连接到要发出此信号的脚本。我们希望它在我们的 Player 脚本中触发，因为我们将更改 Player 脚本中的变量。您将在脚本中看到它创建了一个函数 on_animated_sprite_2d_animation_finished()。我们将在这里重置is_attacking变量。### Player.gd #旧代码 #重置我们的动画变量 func _on_animated_sprite_2d_animation_finished(): is_attacking = false现在，我们可以继续添加跳跃动画。如果他们在跳跃，我们希望根据一定的高度在 y 轴上向上移动精灵。让我们创建一个变量，我们将其称为jump _ height，它将包含我们的玩家在跳跃时向上移动的最大高度。### Player.gd #player 运动变量 @export var speed = 100 @export var Gravity = 200 @export var jump_height = - 100现在，我们可以继续告诉代码让玩家跳跃。为此，我们将检查玩家是否按下了 ui_jump 输入以及他们是否在地板上。我们不希望他们无休止地跳跃，因此我们将使用内置方法is_on_floor来检查我们的玩家当前是否接地，如果接地，则允许他们跳跃。如果身体在move_and_slide的最后一次调用中与地板发生碰撞，则此方法返回 true 。我们还没有地板，所以我们无法测试这一点。在下一部分中，我们将添加地板（或碰撞），然后我们将看到玩家的跳跃动画。然后，当我们跳跃时，我们希望根据jump_height变量移动玩家。我们可以通过将玩家的y速度更改为等于jump_height值来实现这一点。这会将玩家向上推 100 像素，模拟跳跃效果。### Player.gd #older code #singular input captures func _ input (event): #on attack if event.is_action_pressed( "ui_attack" ): is_attacking = true $AnimatedSprite2D.play( "attack" ) #on jump if event.is_action_pressed( "ui_jump" ) and is_on_floor(): velocity.y = jump_height $AnimatedSprite2D.play( "jump" )稍后我们将为他们的动画添加死亡和伤害功能。现在，我们将添加最后一个动画，即攀爬动画。为此，我们还需要创建一个变量来检查我们的玩家是否正在攀爬——因为如果他们没有爬梯子，我们不想播放这个动画。### Player.gd #玩家运动变量 @export var speed = 100 @export var grave = 200 @export var jump_height = - 100 #运动状态 var is_attacking = false var is_climbing = false在我们的 input() 函数中，我们将检查玩家是否正在攀爬。当我们与梯子交互时，此攀爬变量将在另一个场景中被触发并返回 true。如果他们正在攀爬，我们需要检查他们是否按下了ui_up输入，以及检查他们是否在地板上。如果所有这些都返回 true，我们将播放攀爬动画。我们还将重力的强度降低，因为我们想减少重力的影响，使角色在爬梯子时移动得更慢。改变重力值后，我们需要将角色沿 x 轴向上移动，以模拟它们“攀爬”。我们通过调整角色的垂直速度来实现这一点，使其在“爬”梯子时向上移动（-200 像素/秒）。我们还需要将它们的攀爬值重置回 false，并重置它们的垂直速度，因为当玩家不再站在梯子上时，它们就不应该再攀爬了。### Player.gd #旧代码 #单数输入捕获 func _ input (event): #攻击 时 if event.is_action_pressed( "ui_attack" ): is_attacking = true $AnimatedSprite2D.play( "attack" ) #跳跃时 if event.is_action_pressed( "ui_jump" ) and is_on_floor(): $AnimatedSprite2D.play( "jump" ) #爬梯子时 if is_climbing == true: if Input.is_action_pressed( "ui_up" ): $AnimatedSprite2D.play( "climb" ) grave = 100 velocity.y = - 200 #重置重力 else : grave = 200 is_climbing = false您的代码现在看起来应该是这样的。如果你现在运行你的场景，你的玩家应该能够左右奔跑（在一个地方，因为你只有在注释掉move_and_slide()方法后才能测试这一点），以及攻击。我们还看不到他们的攀爬或跳跃，但我们将在下一部分中解决这个问题。恭喜您为玩家的移动动画创建了基础！在下一部分中，我们将创建第一个关卡，我们还将能够在其中测试我们的攀爬和跳跃动画。现在是保存项目并备份项目的好时机，这样如果发生任何破坏游戏的错误，您就可以恢复到此部分。在继续本系列之前，请回过头来复习您学到的知识，一旦您准备好了，我们将在下一部分中与您见面！
通过制作2D平台游戏学习Godot 4 —— 第二部分：玩家设置与移动输入
您将在本部分中学习到的内容：如何创建、运行和实例化新场景。如何向节点添加输入动作、碰撞和物理。如何通过脚本操纵场景。如何添加计算速度和运动。现在我们已经设置好了项目，现在可以开始创建块，最终将我们的骨架项目变成一个完整的游戏。在这一部分中，我们将创建一个新的场景，其中包含组成玩家的节点。我们的玩家是我们在玩游戏时将控制的实际角色——它的场景将包含所有节点和脚本，这些节点和脚本将使我们能够看到这个角色并控制它们，以便我们可以四处奔跑并与世界互动。在主场景中，我们可以继续删除上一节中添加的 Sprite2D 节点 — 因为我们只添加了该节点来演示纹理过滤更改。我们将为 Player 创建一个全新的场景，然后在主场景中实例化（或实例化）它。实例化允许我们从模板复制对象，以便我们可以单独修改它们。换句话说，我们将 Player 与主场景分开，因此我们可以将其视为单独的对象。这样，我们的 Player 就是它自己的实体 — 一个不依赖于我们的关卡（或主）场景的实体。继续从主场景中删除 Sprite2D 节点，以便我们剩下这些：然后，让我们创建一个新场景。让我们创建一个新场景。您可以通过单击 Main(*) 旁边的加号图标来执行此操作，这将创建一个新的空场景。在场景码头中，添加一个新的根节点。我们需要将此根节点设为 CharacterBody2D 节点，这是一个专门用于角色的 2D 物理身体节点，可通过脚本移动。我们需要此节点为玩家角色提供物理特性，使其可以四处移动。双击新添加的 CharacterBody2D 节点将其重命名为“Player”，然后将场景保存在 Scenes 文件夹下。您将看到 Player 根节点旁边弹出一个黄色三角形。如果将鼠标悬停在它上面，它会告诉您该节点没有形状。它所指的形状是碰撞形状，该形状将围绕我们的角色，以便它可以与关卡中的其他项目发生碰撞和互动。让我们通过向我们的玩家场景添加 CollisionShape2D 节点来清除此警告。这将返回另一个警告，因为即使我们添加了碰撞节点，我们仍然没有为其分配碰撞形状。我们需要设置shape属性来配置形状。我们可以通过单击我们的CollisionShape2D节点并在Inspector面板中的Shape属性中为其分配一个形状来做到这一点。让我们添加一个新的RectangleShape2D形状。我选择 RectangleShape2D 形状是因为这是我的角色的形状 — 我们以后也可以随时更改此形状以满足我们的需求。您为对象选择的形状类型取决于您希望它们完成的任务（它们需要从哪里碰撞）以及它们的身体形状。因此，圆形角色可能具有 CircleShape2D，依此类推。按 F 键将焦点放在您的节点上。您将看到您的形状已添加。接下来，我们需要在 Player 场景中添加一个精灵，以便我们能够直观地看到我们的节点。如果您想要一个不移动或没有动画的项目，您可以添加一个 Sprite2D 节点来充当这个精灵。在我们的例子中，我们希望 Player 四处移动，并使用动画来实现这一点。因此，我们将添加一个 AnimatedSprite2D 节点。AnimatedSprite2D 与 Sprite2D 节点类似，只是它带有多个纹理作为动画帧。确保在添加此节点时选择了根节点（Player），而不是选择了 CollisionShape2D 节点。我们希望 AnimatedSprite2D 节点成为根节点的子节点，而不是碰撞形状！此节点还会返回警告，因为它需要 SpriteFrames 资源。此资源允许您导入图像文件（或包含所述文件的文件夹）以提供精灵的动画帧。我们可以通过 Sprite Frames 属性在 Inspector 面板中添加新的 SpriteFrames 资源。单击 Sprite Frames 属性旁边的 选项，然后选择“New SpriteFrames”。如果您单击此新添加的资源，它将在下面的编辑器中打开动画面板。在这里我们可以配置我们的 SpriteFrames 资源。配置包括添加新动画、删除动画、播放动画或编辑动画的功能。默认动画可以删除或重命名。我们将在下一部分中添加所有玩家的动画，以便我们可以更详细地探索此节点。现在，让我们通过单击“从 Sprite Sheet 添加帧”选项为默认动画分配一个精灵。会弹出一个窗口，让我们选择 Sprite Sheet。此 Spritesheet 将包含组成完整动画的动画帧。我们想添加一个基本的空闲动画，因此请进入“res://Assets/Kings and Pigs/Sprites/01-King Human/”并选择“Idle (78x58).png”表。从这里开始，我们需要裁剪动画。我们需要将水平和垂直值更改为我们可以计算的动画帧数。水平方向（列方向）我们计算出 11 个国王，垂直方向（行方向）我们计算出 1 个国王。让我们为空闲动画选择每个帧（按顺序选择它们）。稍后，您可能会注意到，当我们的玩家改变方向（左转或右转）时，会出现一个奇怪的间隙，因为我们的 Sprite 旁边有一个很大的间隙 — 即 Sprite Frame 不在中心。由于这不是我们要发布的游戏，而且我们获得的资产是免费的，所以我们不会太在意这一点，但如果您正在制作想要发布的游戏，请确保您制作或获得的 spritesheet 中的精灵在裁剪时处于中心位置。如果这对您来说是一个大问题，那么我建议您去 Itch.io 上寻找其他资产！图 5：因精灵空间而导致的尴尬翻转示例好的，让我们将“默认”动画重命名为空闲。我们还想关闭循环，因为我们不希望空闲动画不断循环。5 FPS 对我来说也太慢了，所以我要将 FPS 提高到 10。您可以尝试使用 FPS 值来查看您喜欢的值 — 只需考虑您希望动画播放的速度即可。现在我们已经设置好了播放器，让我们在场景中添加一个新脚本，这样我们就可以添加代码来移动播放器。选择您的根节点，然后单击旁边的滚动图标以添加新脚本。将此脚本重命名为“播放器”，并将其保存在您的脚本文件夹下。您可以将脚本添加到任何节点，并且您将能够通过脚本顶部的“extends Node”代码识别脚本所附加的节点。我们有一行写着“extends CharacterBody2D:”。此行表示此脚本继承自名为 CharacterBody2D 的类。我们希望玩家可以向四个方向移动：左、右、上、下。当我们按下左和右输入时，玩家应该向左或向右移动。当我们按下上输入爬梯子时，玩家应该向上移动。我们只会在玩家坠落时向下移动，因此我们将为玩家添加速度，而不是为此添加输入。我们还需要为玩家添加一个输入，以便在玩家按下跳跃键时跳跃。让我们从左、右和上移动的输入开始。Godot 已经内置了输入操作，但我们将为这些操作添加更多输入。输入操作映射到物理控件，例如键盘按键、操纵杆、按钮等。然后，这些操作被分配给将在游戏中移动我们的角色的功能。要添加输入操作，我们转到“项目设置”>“输入映射”。通过启用“显示内置操作”切换按钮来显示内置操作。我们想为 ui_left、ui_right 和 ui_up 输入添加额外的输入。ui_ 代表“用户输入”，您在未来的输入操作中不必遵循此命名约定，但对于我们的项目，我们会这样做。您可以通过按下旁边的铅笔图标来编辑这些输入。您还可以通过单击旁边的加号图标将其他键分配给此输入。让我们将键盘上的“W”键分配给 ui_up 输入。现在我们可以同时使用“W”和“⇧”键向上移动。让我们将键盘上的“A”键分配给我们的 ui_left 输入。现在我们可以同时使用“A”和“⇦”键向左移动。让我们将键盘上的“D”键分配给我们的 ui_right 输入。现在我们可以同时使用“D”和“⇨”键向右移动。最后，让我们添加一个名为 ui_jump 的新输入，并将“Space”键分配给它。现在，当我们按下 Space 键时，我们就可以跳跃。在我们的代码中，我们现在可以继续添加移动函数。目前，我们只会左右移动玩家。在下一部分添加移动动画时，我们将添加跳跃和攀爬功能。要移动玩家，我们需要使用 _physics_process 函数。此函数在每一帧处理 CharacterBody2D 节点的物理和移动，因此我们的玩家可以在每一帧移动。我们首先必须定义玩家的速度和重力变量，因为我们希望玩家以“x”速度水平移动，以“x”重力垂直下落。我们将导出这些变量，以便可以从 Godot 编辑器中编辑它们。### Player.gd 扩展了 CharacterBody2D #player 运动变量 @export var speed = 100 @export var Gravity = 200您现在可以在 Inspector 面板中更改播放器的这些变量值。在 Godot 中，我们可以通过 Input 单例（可以在任何函数和任何类中调用）或 input() 函数（每当发生输入事件时都会调用）捕获输入。如果您想捕获一次输入（例如跳跃时），您将在 input() 函数中捕获输入，但如果您想在输入发生时捕获输入（例如冲刺时），您将使用 Input 单例。我制作了一个 Mural 板，其中给出了可由 Input 单例和函数调用的方法的示例，您可以在此处查看。图 5：输入单例与 input() 函数现在，我们需要创建一个函数来处理玩家的水平移动（向左或向右）。在这个函数中，我们将计算“ui_left”和“ui_right”输入的水平向量 (1, -1) 值。如果通过“Input.get_action_strength(“ui_right”)”方法按下右键，我们的函数将返回 1。如果通过“Input.get_action_strength(“ui_left”)”按下左键，该函数将返回 1。通过从右值中减去左值，如果向左移动，则得到 -1，如果向右移动，则得到 1，如果未按下或同时按下，则得到 0。图 6：玩家向左或向右移动时的矢量位置然后我们将这些返回值（0、1 或 -1）乘以角色的速度来设置水平速度。这会让角色根据玩家的输入向左或向右移动。### Player.gd extends CharacterBody2D #player movement variables @export var speed = 100 @export var gravity = 200 func horizontal_movement(): # if keys are pressed it will return 1 for ui_right, -1 for ui_left, and 0 for neither var horizontal_input = Input.get_action_strength("ui_right") - Input.get_action_strength("ui_left") # horizontal velocity which moves player left or right based on input velocity.x = horizontal_input * speed现在，我们可以在 _physics_process 函数中调用此函数，该函数将每秒计算这些移动值（delta）。delta 参数表示自上一个物理帧以来经过的时间（以秒为单位），用于与帧速率无关的移动。在计算玩家的水平移动之前，我们需要计算玩家的垂直移动。我们希望 y 速度在重力的作用下将玩家拉下。然后，我们将调用我们的 Horizo​​ntal_movement() 函数来计算我们的水平移动。我们将使用 move_and_slide 方法应用这些移动。此方法根据 x 和 y 速度移动物体。如果物体与另一个物体发生碰撞，它将沿着另一个物体滑动（默认情况下仅在地板上），而不是立即停止。如果我们使用 move_and_collide 方法，我们的玩家将不会启用重力，因为该方法不需要速度。### Player.gd extends CharacterBody2D #player movement variables @export var speed = 100 @export var gravity = 200 #movement and physics func _physics_process(delta): # vertical movement velocity (down) velocity.y += gravity * delta # horizontal movement processing (left, right) horizontal_movement() #applies movement move_and_slide() func horizontal_movement(): # if keys are pressed it will return 1 for ui_right, -1 for ui_left, and 0 for neither var horizontal_input = Input.get_action_strength("ui_right") - Input.get_action_strength("ui_left") # horizontal velocity which moves player left or right based on input velocity.x = horizontal_input * speed您的代码现在应该看起来像这样。让我们在主场景中实例化玩家场景，以便测试我们的移动。在主场景中，单击链条图标以实例化一个新场景，即我们的玩家场景。这会将我们的玩家场景作为子场景添加到我们的主场景中 — 同时仍将我们的玩家场景视为独立对象。缩小并将 Player 节点移动到主场景的蓝色框架内。此蓝色边框代表您的游戏屏幕。您可以使用所选的移动工具来移动您的播放器。如果您现在使用 F5 键或按下检查器面板上方的播放按钮来运行场景，您将看到您可以移动您的角色，但它会一直下落，直到它从我们的屏幕上消失。它下落是因为它还没有地板来阻止它——我们将在接下来的几个部分中添加这一点。恭喜您设置了播放器并为其添加了一些动作！在下一部分中，我们将为我们的玩家角色添加动画，以便它开始变得生动。现在是保存项目并备份项目的好时机，这样如果发生任何破坏游戏的错误，您就可以恢复到此部分。在继续本系列之前，请回过头来复习您学到的知识，一旦您准备好了，我们将在下一部分与您见面！
通过制作2D平台游戏学习Godot 4——第一部分：项目编辑器与概览
如果你对复古游戏历史感兴趣，你会知道任天堂在1981年发布《大金刚》后取得的成功简直是一个奇迹。《大金刚》不仅让我们认识了深受喜爱的马里奥（当时被称为Jumpman），还挽救了任天堂免于彻底的经济崩溃。在本系列教程中，我们将追随伟大作品的脚步，尝试制作一款类似《大金刚》的游戏，名为《城堡攀登者》。图1：经典游戏《大金刚》这款游戏与《大金刚》街机游戏类似，玩家将扮演一位国王，需要躲避来自猪敌人的炸弹和箱子，最终爬到城堡顶部以逃脱并进入下一关。与Jumpman不同的是，我们没有公主需要拯救，但这仅仅是因为我们的“国王与猪”资源包中没有包含一个需要救援的公主角色！图2：《城堡攀登者》最终效果本系列教程将涵盖以下内容：玩家动画与移动通过全局脚本管理玩家与障碍物状态攻击激活器、生命值和分数提升的拾取物敌人动画玩家死亡、伤害与攻击带有碰撞与摄像头的游戏地图移动平台与排名系统障碍物的生成关卡进度暂停、保存与加载音乐与音效粒子效果完整的图形用户界面本教程使用的是Godot 4.0.3版本——如果你使用的是更新版本，可能会出现错误，因为引擎在不断变化！请随时向我反馈本系列教程的质量，或如果你遇到任何bug，请通知我，以便我们及时修正。现在让我们开始吧！在本部分你将学习到：如何在Godot编辑器中导航。如何更改项目设置。如何添加场景与节点。编辑器概览在我们开始编写代码之前，首先需要了解Godot编辑器的布局。如果你已经熟悉编辑器，可以跳过本部分的项目设置部分，但如果不熟悉，我建议你快速跟随我一起浏览一下编辑器。如果你还没有安装Godot，请前往他们的网站下载最新版本。下载完成后，解压文件，由于Godot不需要安装，你可以直接将其拖到你希望存储的位置，或者直接从下载文件夹中打开。启动可执行文件后，你将看到项目管理器窗口。在这里你可以创建新项目、加载现有项目，或扫描整个目录以导入多个项目。在顶部，你会看到一个名为“资源库项目”的菜单。在这里你可以下载和找到由社区成员为Godot制作的项目、模板或演示。它类似于其他在线市场，如Itch.io、Unreal Marketplace或Unity Asset Store。唯一的区别是这里的一切都是免费的。要创建我们的项目，点击“新建项目”，然后会出现一个窗口，我们可以选择项目的保存位置并为其命名。我将我的项目命名为“城堡攀登者”，但你可以随意命名。可用的三种渲染模式是：Forward+模式，适用于更高级的游戏项目，对图形和渲染要求较高。移动模式，通常用于移动游戏。兼容模式，使用与Godot 3相同的渲染器，图形和渲染质量较低，更适合较旧或低配设备。我的集成显卡与Godot 4支持的Vulkan版本存在兼容性问题，所以我需要选择兼容模式。但这并不影响使用，因为它仍然运行得很好。你可以在这里阅读更多关于新渲染更新的信息。选择“创建并编辑”，你将神奇地进入项目窗口。在顶部，我们有我们的工作区，这是我们完成任务的空间。3D模式将始终默认激活。如果你点击其他选项，可以在不同的工作区之间切换。图3：工作区导航的鼠标输入现在，让我们逐一了解这些工作区：2D工作区用于编辑和查看2D游戏的2D场景。3D工作区用于编辑和查看3D游戏的3D场景。脚本工作区是代码编辑器，我们在这里编写、编辑和查看脚本。AssetLib工作区是资源库项目，你可以免费在你的项目中使用。窗口左侧的面板是场景停靠栏。这里列出了你当前打开的场景内容，你也可以在这个停靠栏中向场景添加新节点。在Godot中，场景是节点的主要容器，节点是游戏的主要构建块。不同的节点有不同的功能，例如碰撞形状和动画精灵各有其用途，因此能够将它们组合在一个场景中，使我们能够为游戏创建具有更复杂功能的组件。场景还可以实例化其他场景，例如在你的世界场景中包含敌人场景。我建议你去看看Godot关于节点和场景的文档。我画了一个Mural板来给你展示节点和场景的示例，你可以在这里查看。另一个面板称为导入停靠栏，在这里你可以设置或编辑添加到项目文件夹中的资源（如图片、字体和音频文件）的属性，然后你可以重新导入这些资源并保存这些属性。在场景和导入停靠栏下方，你会找到文件系统面板，这是你管理项目文件、文件夹和资源的地方。你可以将新资源拖入此面板以将其导入项目，还可以添加新文件夹、脚本、场景等。窗口右侧的面板称为检查器面板，在这里你可以查看和编辑当前所选节点的属性。旁边的节点面板是你可以连接信号并向节点添加组的地方。组可以用于将场景或节点类型分组，例如我们的NPC，以便玩家知道如何与这些节点交互。信号可以在许多情况下使用，例如通知游戏按钮已被按下或动画已播放完毕。这使我们能够告诉游戏在某些事件发生时该做什么。大多数节点都有内置信号，例如按钮和动画播放器，但我们也可以创建自己的自定义信号。我画了一个Mural板来给你展示自定义和内置信号的示例，你可以在这里查看。在检查器和节点停靠栏上方，你会看到我们将用于播放、暂停、重新启动或停止场景的选项。这是我们启动游戏以测试它的地方。在编辑器窗口的底部，有输出面板、调试器控制台、音频混音器、搜索结果面板以及动画和着色器编辑器。它们默认折叠以节省屏幕空间，但如果你点击它们，它们会最大化。某些可自定义的节点（如瓦片地图或动画播放器节点）将为此窗口添加新选项。让我们逐一了解这些预设选项：输出控制台将返回我们添加到代码中以供测试的打印消息或通知。调试器控制台用于——你猜对了——调试和错误处理。音频混音器用于编辑音频文件。动画编辑器用于为动画播放器节点创建动画。在上方的功能区或菜单栏中，你有几个选项。在这里，你可以在“场景”选项卡下管理你的场景。可以添加、保存、打开或关闭场景。你可以在“项目”选项卡下管理你的项目设置和属性（如输入操作、窗口大小、自动加载单例等）。你也可以从这里退出项目管理器窗口。这也是我们将导出项目的地方。你可以在“调试”选项卡中启用或禁用调试工具，例如你想让碰撞可见，以便查看碰撞事件是否在游戏过程中执行。“编辑器”选项卡允许你更改与编辑器相关的设置，例如你可以更改其当前布局或主题。你甚至可以截取当前窗口的截图。最后，“帮助”选项卡为你提供了快速访问Godot 4文档和社区论坛的途径。如果你对使用编辑器感到更舒适，我们可以继续设置我们的项目。项目设置你可以从我的网盘下载本教程所需的资源。所有资源均来自Itch.io，你可以在下面的GitHub仓库中找到每个资源的单独致谢。我还为这款游戏使用了这个调色板。本项目的完整源代码可以在其GitHub仓库中找到。我建议你在完成项目之前不要阅读源代码，因为你可能会对尚未编写的代码感到困惑！我将在每部分的末尾链接单独的源代码版本。你还将找到带有“故障排除”部分的部分，这些部分将教你如何查找和修复项目中的bug。调试是一个持续的过程，我试图教你以一种相对现实的方式构建游戏。如果你还没有打开Godot，让我们打开它并创建一个新项目。我将我的项目命名为“城堡攀登者”，并将其保存在一个专门的文件夹中，以便我记住它的位置。确保你也这样做，但你可以随意命名。使用上面提供的链接下载资源，解压文件并将其拖入你的文件系统面板。我们还可以继续删除项目中存在的默认icon.svg和README.MD文件。右键单击面板并创建三个新文件夹：场景、GUI和脚本。场景文件夹将包含我们所有的.tscn文件（玩家场景、NPC场景、地图场景等），脚本文件夹将包含我们所有的.gd文件，GUI文件夹将包含我们所有的UI元素场景（分数、健康和时间UI）。在场景文件夹中，让我们创建我们的第一个场景，即主场景。主场景将作为我们游戏元素的核心容器。这通常是我们启动游戏时加载的第一个关卡。在这个主场景上，我们将添加我们玩游戏所需的地图、角色和物品。主场景也是游戏启动时自动运行的场景——当我们有主菜单场景时，我们稍后会更改这一点。图4：《城堡攀登者》场景流程概览要添加场景，你可以右键单击文件系统面板并添加新场景，就像我们添加文件夹时一样。你也可以在场景停靠栏菜单中选择“2D场景”选项，这将自动创建一个以Node2D节点为根的新场景。我更喜欢通过工作区面板中的加号图标添加新场景，因为这是创建新场景的最快方式。你现在不必这样做，但为了将来参考，我认为让你知道如何快速添加新场景是有益的。点击场景停靠栏中的2D场景选项，你会看到它将自动添加一个Node2D作为根节点。Node2D节点是2D游戏的基础节点。我们通常在主场景或任何包含不同类型节点和场景的场景中使用此节点。在这个Node2D节点上，我们可以添加2D类别中的任何节点——因为它能够实例化和引用任何2D子节点。现在你会看到Node2D节点已被添加为我们的根节点，但我们的场景尚未保存。在顶部的场景菜单中保存它或使用快捷键CTRL + S。将场景保存为“Main”在你的场景文件夹中。双击Node2D根节点以重命名它。让我们将其重命名为与我们的场景名称相匹配。将我们的根节点重命名为与我们的场景名称相同，这将帮助我们在以后开始在其他场景中实例化它们时识别我们的场景。因此，我们将知道“玩家”节点来自“玩家”场景，依此类推。现在我们可以向根节点添加子节点。你可以再次通过加号图标执行此操作。记住，我们正在制作2D游戏，因此以“3D”结尾的节点将不适用于我们的节点！让我们暂时添加一个Sprite2D节点，因为我们需要更改纹理过滤。Sprite2D节点将包含一个精灵，这是一个可以在屏幕上移动并作为单个实体操作的计算机图形。请注意，你添加的任何节点都将添加到节点菜单中的最近面板中，因此你下次可以轻松地从那里选择它，而无需搜索它！在2D工作区中（因为我们正在制作2D游戏），选择你的Sprite2D节点并按键盘上的F键以聚焦于你选择的节点。选择节点后，在检查器面板中，你会看到纹理选项。这将包含我们精灵的图像。如果你进入你的资源文件夹，你可以将任何你想要的图像拖到该纹理属性中。如果你放大你的精灵，你可能会注意到它有点模糊。这是因为纹理没有将我们的像素读取到其最近的像素。让我们修复纹理过滤，使其平滑我们的像素。在项目设置选项下的项目 > 项目设置中，将纹理渲染从线性更改为最近。这将放大我们的纹理渲染，使图像资源始终保持高质量。现在你会注意到它非常清晰。我们还需要将拉伸模式更改为“视口”，并将大小模式更改为最大化。拉伸模式设置定义了如何拉伸基础大小以适应窗口或屏幕的分辨率。视口模式将我们的游戏拉伸以适应屏幕。我们最后需要做的是将我们的主场景设置为主场景。在你的项目设置中，在应用程序 > 运行下，你可以在这里将主场景设置为我们新创建的场景。这将在我们运行游戏时运行此场景。稍后，我们将将其更改为我们的菜单场景，因为你希望玩家在开始菜单中迎接他们，他们可以在那里开始/加载关卡，而不是直接将他们扔到游戏的第一关。恭喜，你已经学会了如何在Godot编辑器中导航，并为你的项目添加了第一个节点和场景。为你自己的GitHub仓库或游戏备份文件夹创建备份是一个很好的做法，以便你可以定期备份你的项目。这就是我们的项目概览和设置部分！在下一部分中，我们将设置我们的游戏角色，即我们的玩家，并添加移动他们的功能。这意味着更多的节点，你将第一次使用脚本！记得保存你的游戏项目，我们下一部分见。