我的自习室—让生活从这里沉淀 - 2024年12月
这里您可以静下心来学习，有自己的爱好，我喜欢编程，例如Godot游戏开发，HTML、CSS和Javascript前台开发，c#，sqlserver后台数据开发，还喜欢记录网海发现的各类好软件。
让我们通过制作 RPG 来学习 Godot 4 — 第 5 部分：设置游戏 GUI #1
在开始设置敌人和任务等有趣的部分之前，我们要完成游戏GUI的创建。GUI可以让我们直观地显示玩家统计数据的变化，例如他们的弹药和拾取量、健康和耐力值以及当前经验和等级。图 10：GUI 布局计划。我将GUI创建分为三个部分：健康和耐力；拾取物；以及经验和升级。您将在本部分中学习到的内容：· 如何向场景添加 UI 元素。· 如何复制节点。· 如何通过自定义信号更新 UI 元素。· 如何改变节点的锚定。· 如何创建、初始化和连接自定义信号。在您的 Player Scene 中，添加一个新的CanvasLayer节点。此节点是屏幕上绘制图形的容器，例如进度条或标签。将UI元素添加到主场景，然后通过信号将这些元素与实例化的Player脚本连接起来以更新它们的值。将其重命名为UI。我们希望 UI 元素包含在屏幕上的蓝色边框内。我隐藏了现有节点，以便我们现在可以只关注 UI，所以如果您发现我不再有地图或玩家，请不要惊慌！健康与耐力棒让我们将ColorRect节点作为子节点添加到 UI 节点。这将绘制一个我们可以用颜色填充的矩形。此节点将作为我们健康值的进度条。向新添加的节点添加另一个 ColorRect 节点。将第一个 ColorRect 重命名为 HealthBar，将第二个 ColorRect 重命名为 Value。外部矩形（HealthBar）将用作进度条的框或边框，内部矩形（Value）将是显示进度值的实际颜色。现在，在 Inspector 面板的 Layout > Transform 下，将 HealthBar 节点大小更改为 x：100 和 y：10。对 Value 节点执行相同操作，但将其 x 更改为：98 和 y 更改为：8。我们还希望将 Value 矩形置于 HealthBar 矩形的中心。为此，请将其锚点预设更改为中心。将 HealthBar 的颜色更改为 #3a39356a，将 Value 的颜色更改为 #a4de79。在我的游戏中，HealthBar 为绿色，但您可以随意更改颜色。让我们将生命值条锚定在屏幕的左下方。在 Inspector 面板的 Layout 下，将锚点预设从中心更改为左下方，并将其位置值更改为 x: 5, y: 155。复制整个 HealthBar 节点（及其子节点值）并将其重命名为 StaminaBar。将 StaminaBar 中的 Value 节点的颜色更改为 #377c9e。这会使其变成蓝色，但再次强调，这是您的游戏，因此您可以将其设置为任何您喜欢的颜色。我们还将耐力条锚定在屏幕的左下角。在 Inspector 面板的 Layout 下，将锚点预设从中心更改为左下角，并将其位置值更改为 x: 5, y: 165。现在我们已经设置了健康和耐力进度条！我们现在可以继续在代码中实现这些元素的功能。在我们的 Player 脚本中，我们需要添加变量来捕获我们的健康值、最大健康值和健康恢复值，以及我们的耐力值。在 Player.gd 脚本的顶部，在is_attacking代码下方，让我们添加这些变量。### Player.gd # 旧代码 # UI 变量 var health = 100 var max_health = 100 var regen_health = 1 var stamina = 100 var max_stamina = 100 var regen_stamina = 5创建变量后，我们需要为健康和体力创建自定义信号，以便游戏中的其他对象可以监听变化事件并做出反应，而无需直接相互引用。换句话说，这些信号将通知我们的游戏我们的健康或体力值发生了变化，然后触发其他事件。如果我们的健康和耐力变量发生变化，我们希望向其发出信号，以便这些变化在我们的进度条中直观显示，并且我们的健康值得到更新。我们之前使用过信号，当时我们将内置的on_animation_finished()信号连接到我们的播放器脚本，但这次，我们将创建自己的自定义信号。为什么要使用自定义信号？虽然内置信号涵盖了许多常见用例（例如按钮点击或鼠标事件），但它们可能无法处理游戏或应用程序特有的所有特定交互或事件。自定义信号提供了一种定义特定于游戏逻辑的事件集的方法，使您的代码库更有条理、可重复使用且更易于维护。现在，在创建自定义信号之前，让我们尝试了解何时需要发出这些信号并通知游戏我们的健康和体力值已发生变化。我们希望在按下冲刺输入时、以及在受到子弹伤害或稍后喝下健康药水时发出信号。图 12：自定义信号概览我们将把自定义信号添加到代码顶部，因为我们稍后会添加更多信号。这将使我们的所有信号保持整洁有序。要定义自定义信号，我们使用关键字signal后跟信号名称。我们的信号将更新我们的健康和耐力值，因此我们将它们称为 health_updated 和 stamina_updated。### Player.gd # 旧代码 # 自定义信号 signal health_updated signal stamina_updated现在我们已经创建了信号和变量，我们还需要不断检查它们，看看我们的健康或耐力值是否需要更新或重新生成。我们可以在_process()函数中进行此检查，该函数将在每次绘制帧时调用（每秒 60 次）。该函数有一个名为delta的参数，它表示自上一帧以来经过的时间。对于每个健康和耐力，我们需要计算每个的更新值。我们将使用min()方法执行此操作，这将确保它永远不会超过我们的 max_health 和 max_stamina 变量的最大值。在这个 min() 函数中，我们将计算健康和耐力变量的更新值，并将其添加到当前帧中捕获的这些变量的值中。### Player.gd # older code # ------------------------- UI -------------------------------------------------- func _process(delta): #calculate health var updated_health = min(health + regen_health * delta, max_health) #calculate stamina var updated_stamina = min(stamina + regen_stamina * delta, max_stamina)如果这些值与我们的原始值（var 健康和耐力）不同，我们将把我们的健康更新为新值并发出信号通知游戏这一变化。### Player.gd # older code # --------------- UI --------------------------------- func _process(delta): #regenerates health var updated_health = min(health + regen_health * delta, max_health) if updated_health != health: health = updated_health health_updated.emit(health, max_health) #regenerates stamina var updated_stamina = min(stamina + regen_stamina * delta, max_stamina) if updated_stamina != stamina: stamina = updated_stamina stamina_updated.emit(stamina, max_stamina)既然我们已经完成了，让我们更新一下冲刺输入动作，以便在按下时消耗一些体力。您会注意到，我们通过 . emit()方法发出信号。这会发出此信号，并且与此信号相关的所有可调用函数都将被触发。我们将在 UI 脚本中创建函数作为可调用函数，因此如果发出信号，此可调用函数将收到通知以更新我们的 UI 组件。### Player.gd # older code # -------------------- Movement & Animations ------------- func _physics_process(delta): # Get player input (left, right, up/down) var direction: Vector2 direction.x = Input.get_action_strength("ui_right") - Input.get_action_strength("ui_left") direction.y = Input.get_action_strength("ui_down") - Input.get_action_strength("ui_up") # Normalize movement if abs(direction.x) == 1 and abs(direction.y) == 1: direction = direction.normalized() # Sprinting if Input.is_action_pressed("ui_sprint"): if stamina >= 25: speed = 100 stamina = stamina - 5 stamina_updated.emit(stamina, max_stamina) elif Input.is_action_just_released("ui_sprint"): speed = 50 # older code您会注意到，我们将耐力变量作为参数传递到了信号中。由于我们将使用这些信号来更新 UI 中的进度条，因此在信号中传递 health/stamina 和 max_health/max_stamina 可以为 UI 组件提供必要的信息，以便准确灵活地显示玩家的耐力状态。它还可以确保游戏代码的一致性和效率。现在我们需要将信号连接到 UI 组件（可调用函数），因此在您的 Player 场景中为 HealthBar 和 StaminaBar 添加一个新脚本，并将这些脚本保存在您的 GUI 文件夹下。将其中一个命名为 Health，将另一个命名为 Stamina。在您新创建的 Health.gd 脚本中，让我们为 HealthBar/Value 节点的值创建一个@onready变量。### Health.gd extends ColorRect # Node refs @onready var value = $Value然后，我们需要创建一个函数来更新 Value 节点的颜色值。我们可以将其宽度 (98) 乘以玩家健康值除以 max_health 来实现这一点。这将返回一个百分比值，该值将反映我们的 Value 节点的新宽度。### Health.gd extends ColorRect # Node refs @onready var value = $Value # Updates UI func update_health_ui(health, max_health): value.size.x = 98 * health / max_health对 Stamina.gd 中的耐力值执行相同操作。### Stamina.gd extends ColorRect # Node refs @onready var value = $Value # Updates UI func update_stamina_ui(stamina, max_stamina): value.size.x = 98 * stamina / max_stamina现在，我们可以将 UI 组件中的函数连接到内置_ready()函数中的信号。当 Player 节点进入主场景时，此函数会将我们的可调用函数连接到我们的信号 — 这样 UI 将能够在游戏加载时更新进度条。何时使用 _ready()？每当我们需要设置或初始化在节点及其子节点完全添加到场景后立即运行的代码时，我们都会使用 _ready() 函数。此函数在任何 _process() 或 _physics_process() 函数之前只会执行一次。我们将通过connect关键字连接 Player 的 health_updated 信号，它将连接到的可调用函数是 HealthBar 的update_health_ui函数。这意味着每次我们的健康值发生变化时，玩家脚本都会发出信号，我们的健康栏将更新其值。我们将创建一个指向 HealthBar 和 StaminaBar 节点的节点引用，以便我们可以从其附加脚本访问这些函数。### Player.gd extends CharacterBody2D # Node references @onready var animation_sprite = $AnimatedSprite2D @onready var health_bar = $UI/HealthBar @onready var stamina_bar = $UI/StaminaBar # older code func _ready(): # Connect the signals to the UI components' functions health_updated.connect(health_bar.update_health_ui) stamina_updated.connect(stamina_bar.update_stamina_ui)如果您现在运行您的场景并且冲刺，您会看到耐力条减少，同时又会再生！让我们继续讨论 Pickups UI 的下一个 GUI 部分。记得保存并备份您的项目，下一部分见。本节代码下载。
让我们通过制作 RPG 来学习 Godot 4 — 第 4 部分：游戏图块地图和相机设置
欢迎回到我们的Dusty Trails教程系列！如果您错过了上一部分，您可以在本教程末尾找到它的链接。我们现在有了一个正常运行的玩家，好吧，我们还有很多东西可以添加到我们的小人物身上，使他们成为真正的游戏英雄！但现在，我们的玩家必须等待，因为在这一部分中，我们将添加带有碰撞的游戏TileMap，并添加一个Camera2D到我们的游戏中，以便我们可以跟随玩家。我们没有在本教程的播放器设置部分添加相机节点，因为相机节点在没有某种背景（即我们的图块地图）的情况下无法工作。您将在本部分中学到什么：· 如何使用 TileMap 节点的精灵表。· 如何使用 TileMap 和 TileSheet 面板。· 如何使用 TileMap 工具将图块绘制到地图上。· 如何创建图块地图图层以及物理和地形图层。· 如何向您的播放器添加摄像头。首先，什么是瓦片地图或瓦片集？图块地图是用称为图块的小型规则形状图像构建游戏世界或关卡地图的基础。这些图块存储在我们称为图集、图块集或精灵表的东西中，我们之前在添加动画时已经见过它们。换句话说，图块集是图块地图的实际图形。TileMap 和 TileSet 有什么区别？TileMap 是一个网格，您可以在其中放置 Tileset 中的图块以实际构建关卡。图块集是可用于构建关卡的图块集合。图 1：TileSet（左）和 TileMap（右）在我们的主场景中，我们可以添加一个 TileMap 类型的新节点。单击并拖动该节点，使其位于我们的玩家场景后面，否则，玩家将被我们的地图隐藏！我们将此节点重命名为 Map，然后在 Inspector 面板中单击 TileSet 旁边的 以创建新的图块集。您将看到下面弹出两个新选项：TileMap和TileSet。要创建 TileMap（这是我们将在屏幕上绘制的地图），我们首先需要为其分配一个 TileSet（这是我们将从中绘制的图像）。为此，请单击 TileSet 选项，然后在文件系统面板中打开资源文件夹。您会看到我给了您三个图块集：Interior、Water 和 Atlas。我们将使用这些图块来创建我们的世界。您可以将这些图块表拖到 TileSet 面板中的“图块”选项中。会弹出一个窗口询问您是否希望引擎自动创建图块。选择“是”，因为这会将我们的图像分成漂亮的方块以形成网格。您现在可以更改图块表名称、ID 和网格分隔值。更改您的姓名和 ID，如下所示：现在，如果我们返回到 TileMap 面板，我们可以看到我们的图块已添加。现在，您可以单击图块并开始在屏幕上绘制图像。以下是 TileMaps 面板中提供的工具的简要概述：一部分可能有点令人困惑，所以我将尝试将其分解成几个部分，以便您更好地理解这些概念。如果事情对您来说有点复杂，请下载我创建的地图场景，并将地图节点从场景拖入主场景。地图创建1. 地形图层介绍（自动平铺）我们可以使用提供的绘画工具绘制或创建地图，但我们的环境看起来会很单调，尤其是对于我们的地形而言。为了让事情变得更容易，我们可以使用一种称为autotiles的东西。地形（也称为自动图块）可创建有机地形形状，并自动绘制我们指定区域的角落和边缘。这消除了手动放置草地或水体边缘和侧面的繁琐工作，同时让我们尝试自己创建有趣的地形。要创建自动图块，只需转到“检查器”面板，单击“TileSet”资源（带有图像的资源），然后在“地形集”下添加一个地形集元素。请记住还要向地形集元素添加一个元素。您可以为不同的地形元素创建不同的地形集。例如，您可以将草地自动图块放在一组中，将泥土自动图块放在另一组中，依此类推。让我们为草地、沙子和水创建地形元素。您将从下图中看到，我有一个 Sand_Dessert 和一个 Sand_Forest。这只是为了向您展示您可以在一个地形元素集中拥有多个沙子。在我的例子中，草地、沙子和水都在不同的集合中，但沙子元素在同一个集合中。2. TileMap 图层趁此机会，我们还要创建图层，以便我们可以在不同的图层上绘制这些元素，这样它们就可以重叠。如果我们在同一层上绘制，这些图块就会相互擦除。要添加新图层，只需进入“检查器”面板，然后在“图层”属性下方添加一个新图层元素即可。让我们为每个地形集添加一个：水、草和沙。3. 地形图层创建（自动平铺）现在我们可以分别在图层上绘画。但首先，让我们创建这些自动图块。在您的 Tileset 面板顶部，您将看到三个选项：设置、选择和绘画。在设置中，我们已经看到，我们可以在这里更改图块集的名称和 ID。在选择面板中，从 TileMap 属性中添加图层（例如地形或物理图层）后，我们可以更改图块的属性，即通过选择然后更改图块组来添加碰撞或自动平铺。在绘制面板中，我们可以执行与选择面板中相同的操作，但这次我们可以将这些属性绘制到图块上，即一次将碰撞绘制到多个图块上。图 8：TileSet 模式概览a. 在选择面板中自动平铺请记住，选择面板仅允许我们选择单个或多个图块，然后通过属性面板更改它们的属性。您将看到我们的 Terrains 属性有两个数值：-1 表示 Terrain 集，-1 表示 Terrain。这指的是我们的地形集和地形元素的 ID。-1 表示“无地形集”或“无地形”（它将擦除地形），这意味着您必须将 Terrain 集设置为 0 或更大，然后才能将 Terrain 设置为 0 或更大。对于地形，我们有三组地形，一组用于沙地、水和草地。在每个地形组中，我们都有地形元素。对于沙地地形组，我们有 Sand_Dessert 地形元素和 Sand_Forest 地形元素。对于水地形组，我们只有水地形元素。对于草地地形组，我们只有草地地形元素。我希望这是合理的，因为它可能有点令人困惑或不知所措。对于我们的地形层，我们的元素将按如下方式关联：让我们将自动图块添加到地形集 0（沙地）、地形元素 0（Sand_Dessert）中。我们将为此使用 Atlas.png 图块集。现在我们可以继续选择沙地图块。我将选择左上角的图块。现在，我们必须为这些图块分配位对等值。对等位根据相邻图块确定将放置哪个图块。-1 是一个特殊值，表示空白空间。例如，如果某个图块的所有位都设置为 0 或更大，则只有当其所有相邻图块都使用具有相同地形 ID 的图块时，它才会出现。如果某个图块的位都设置为 0 或更大，但左上角、顶部和右上角的位都设置为 -1，则只有当其上方有空白空间（包括对角线）时，它才会出现。当我们分配这个值时，我们要避免将值分配给明显的角落或尖锐的边缘。如果我们回到 TileMaps 面板，在 Sand_Dessert 地形元素下方，我们现在可以将新创建​​的自动图块绘制到地图上。现在这不会有任何作用，因为我们需要添加其他沙地图块。为此，我们将通过绘制面板加快速度，在那里我们可以将位图块直接绘制到地形上。b. 在 Paint Panel 中自动平铺在您的 TileSet 面板中，在绘画下方，您可以看到您可以直接选择地形集和元素，而无需提供其 ID。让我们确保已选择 Sand_Dessert 的地形集元素，然后在图块上直接开始绘制位对等点。请记住避免出现角落、锐边或线条。要添加位值，请在图块上单击鼠标左键。如果您需要删除位值，只需右键单击您添加的位值即可将其删除。现在，如果您返回 TileMap 面板，您将看到其他图块已添加到地形设置自动图块中。如果您选择 Sand_Dessert 地形并开始在地图上绘图，您将看到它会自动绘制出美观、有机的地形。现在我们可以继续为其余的地形集元素创建自动图块。如果您看不到位块，您可以随时在“地形集”>“元素”>“颜色”下的“检查器”面板中更改地形元素颜色。这是我们的 Sand_Forest 自动耕作的地形位（使用 Water.png 图块集）：这是我们的草地自动耕作的地形位（使用 Atlas.png 图块集）：这是我们的 Water 自动耕作机的地形位（使用 Water.png 图块集）：4. 绘制地形对于我们的游戏，我将使用矩形工具，使用水自动图块绘制一个半大面积的水。绘制这个矩形的大小并不重要，重要的是在其中添加了多少个对象。单个场景中的项目太多会占用大量内存。所以请记住这一点。我不想让玩家跑过这片水域，所以这将是我地图的边界，也就是说，玩家跑进水里就会停下来。我们稍后会为这片水域添加碰撞。接下来，在草地层上，我要为我的地图绘制土地。尽情享受这个设计吧。只需记住地图的导航路径。如果您想更好地了解设计地图背后的工作流程，请查看这篇文章。我只是为了教程而创建这张地图，所以无论我最终得到什么都无关紧要，因为我不会出售或发布这款游戏！图 2：地图布局示例我们稍后还将绘制草地砖块边缘的碰撞。最后让我们将沙子添加到沙层中。我只会添加几块 Sand_Forest 砖块，因为草地边缘与我们的主草地不匹配。我只是为了向您展示这些设置是如何工作的而创建的。如果您想为地图创建不同的环境/部分，您可以从 Water.png 图块表中添加草。5. 添加世界物品现在我们已经设置好了地形（记住，以后你随时可以对其进行更改），我们可以继续添加花草树木甚至建筑物等东西。不过，在执行此操作之前，让我们为图块添加碰撞，以便我们的玩家可以撞到物品。为此，我们需要向 TileMap 添加物理层。在 Inspector Panel 中，确保已选择图块集资源，然后在 Physics Layers 下添加一个新元素。创建物理层后，您现在可以访问 TileSet 面板中的物理层选项。现在，我们可以通过在选择面板或绘制面板中添加碰撞来添加它们。让我们再用这两种方式重复一遍。a. 选择面板中的碰撞在我们的选择面板中，让我们在 Atlas.png 图块表中为树添加一些碰撞。我们根本不想让玩家穿过这棵树，所以选择树的左下根，然后在物理层 > 多边形下，我们可以开始在那个小框中绘制碰撞。让我们为整个图像轮廓添加边框。完成边框后，您可以使用框上方的点工具编辑和删除点。绘制完碰撞边界或轮廓并连接完最后的点后，您将看到绘制的形状变为红色。这意味着它现在已添加碰撞多边形。b. 绘画面板中的碰撞您还可以通过绘画面板将碰撞绘制到形状上。在绘画面板中选择物理层，然后单击另一个树形图块。如果单击它，将出现一个框。蓝色表示已将碰撞形状添加到整个图块，但您可以拖入新点来勾勒出所需的形状。现在，单击要分配碰撞形状的图块 - 以分配它。如果愿意，您还可以将此碰撞形状绘制到多个图块上。我不会这么做，所以让我们继续绘制树其余部分的碰撞形状。让我们创建一个新图层并将其命名为“树叶”。现在在此图层上，选择组成树的图块，并将其绘制到地图上（靠近玩家）。您还可以继续使用选择工具选择整棵树来创建树的图案，这样我们就不必为了创建新树而不断重新选择所有图块。您可以将此选定项目复制粘贴或拖放到 TileMap 选项中的 Patterns 面板中。现在您可以轻松地将您的树绘制为单一对象。如果你运行你的场景，你现在会看到你的玩家撞上了树，而不是穿过它们。让我们将碰撞添加到其他物体，例如岩石和地形边缘。您可以继续向水中添加碰撞以及您想要添加到世界中的任何其他内容！另外，不要忘记为您的瓷砖创建图案。6. 创建地图您还可以使用散布工具（骰子图标）将这些图块随机放置在地图上。散布值越高，放置在彼此近半径范围内的图块就越少。我将继续创建一张包含占位建筑等的基本地图。在这里尽情发挥您的创造力吧。*请注意，如果您发现您的玩家没有移动，这是因为水碰撞。您只需从您的土地/地面下方移除水砖即可解决此问题。玩家相机好的，现在我们有了地图，我们会注意到，如果我们运行场景，我们的玩家就会跑出地图，我们无法跟随他们！为了解决这个问题，我们需要为玩家添加一个摄像头。如果你想捕捉整个世界，你需要将摄像头连接到主场景，但如果你想跟随特定角色，你需要将摄像头连接到他们的场景。让我们打开我们的玩家场景，并在场景码头添加一个Camera2D节点。您会注意到，它会在玩家周围创建一个紫色边框。这是相机框架，我们可以在这些边缘添加限制，这样我们的玩家就无法看到地图中某个点以外的区域。由于我们的地图周围只有水，所以我们暂时不需要担心这个限制。这就是本部分的全部内容。我知道这是很长的部分，甚至可能让您有点不知所措，但不用担心，因为在本教程系列的下一部分中，我们将在开始创建游戏的 GUI 时做一些新的简单的事情。请记住保存您的游戏项目，我们将在下一部分中与您见面。本节代码下载。
我们的玩家已经设置好了，当我们运行游戏时，它会在地图上移动。但是有一个问题，那就是它只是静态移动。它没有配置动画！在本节中，我们将重点介绍如何为玩家添加动画，以便它可以四处走动并开始活跃起来。您将在本部分中学习到的内容：· 如何使用精灵表进行动画制作。· 如何使用 AnimatedSprite2D 节点添加动画。· 如何将动画与输入动作连接起来。· 如何添加自定义输入操作。· 如何使用 input() 函数和 Input 单例。· 如何使用内置信号。对于我们的玩家，我们需要动画来上下左右移动。对于每个方向，我们将设置空闲、行走、攻击、伤害和死亡的动画。由于我们要处理的动画太多，我将带您了解第一个方向，然后给您一个表格来完成其余部分。在 Player 场景中，选中 AnimatedSprite2D，我们可以看到已经添加了向下行走的动画。让我们将这个默认动画重命名为walk_down。单击文件图标添加新动画并将其命名为idle_down。与之前一样，让我们​​选择“从 Sprite Sheet 添加帧”选项，然后导航到播放器的正面动画。将水平值更改为 14，将垂直值更改为 5，然后选择我们的空闲动画的第一行帧。将其 FPS 值更改为 6。添加新动画，并将其命名为attack_down。对于这个，您需要选择第三行动画帧。将其 FPS 值更改为 6 并关闭循环，因为我们只希望此动画触发一次而不是连续触发。接下来，让我们添加一个新动画，并将其命名为damage。为此动画选择第 4 行中的单帧。将其 FPS 值更改为 1 并关闭循环。最后，创建一个新的动画并将其命名为“死亡”。将最后一行帧添加到此动画中。将其 FPS 值更改为 14 并关闭循环。现在我们已经创建了向下动画以及死亡和伤害动画。您认为自己可以自己添加其余部分吗？如果不行，请联系我，我会修改本节并提供有关这些内容的说明。对于更大胆的人，这里有一个你需要添加的动画表：最后，你的完整动画列表应如下所示：现在我们已经添加了所有玩家的动画，我们需要更新脚本，以便将这些动画链接到我们的输入操作。我们希望在按 W 或 UP 时播放“向上”动画，在按 S 或 DOWN 时播放“向下”动画，在按 A 和 LEFT 或 D 和 RIGHT 时播放“向左”和“向右”动画。让我们通过单击 Player 节点旁边的滚动图标来打开脚本。由于我们已经在_physics_process()函数中添加了移动功能，因此我们可以继续创建一个自定义函数，该函数将根据玩家的方向播放动画。此函数需要将 Vector2 作为参数，它是表示玩家在特定空间和时间中的位置或方向的浮点坐标（请参阅前面部分中的矢量图像作为对此的复习）。在现有代码下，让我们创建player _ animations()函数。 ### Player.gd extends CharacterBody2D # Player movement speed @export var speed = 50 func _physics_process(delta): # older code # Animations func player_animations(direction : Vector2): pass为了确定玩家所面对的方向，我们需要创建两个新变量。第一个变量是我们将与零向量进行比较的变量。如果方向不等于 Vector(0,0)，则表示玩家正在移动，这意味着玩家的方向。第二个变量将存储此方向的值，以便我们可以播放其动画。让我们在脚本之上、速度变量之下创建这些新变量。### Player.gd extends CharacterBody2D # Player movement speed @export var speed = 50 func _physics_process(delta): # older code # Animations func player_animations(direction : Vector2): pass为了使我们的代码更有条理，我们将使用@onready注释来创建对 AnimatedSprite2D 节点的引用实例。这样，我们可以重复使用变量名，而不必每次想要更改动画时都说 $AnimatedSprite2D.play()。请注意，在播放 side_ 动画时，我们还会水平翻转精灵。这样，我们就可以对左右方向重复使用相同的动画。### Player.gd extends CharacterBody2D # Node references @onready var animation_sprite = $AnimatedSprite2D现在，在我们新创建的函数中，我们需要将 new_direction 变量与零进行比较，然后将动画赋给它。如果方向不等于零，则我们正在移动，因此应该播放行走动画。如果它等于零，则我们静止不动，因此应该播放空闲动画。### Player.gd extends CharacterBody2D # Node references @onready var animation_sprite = $AnimatedSprite2D # older code # Animations func player_animations(direction : Vector2): #Vector2.ZERO is the shorthand for writing Vector2(0, 0). if direction != Vector2.ZERO: #update our direction with the new_direction new_direction = direction #play walk animation because we are moving animation = #todo animation_sprite.play(animation) else: #play idle animation because we are still animation = #todo animation_sprite.play(animation)但是我们如何确定动画呢？我们可以进行长条件检查，或者创建一个新函数，在玩家按下输入操作后，根据平面方向 (x, y) 确定方向（左、右、上、下）。如果玩家按下向上，那么我们应该返回“向上”，对于向下、侧面和左侧也是如此。如果玩家同时按下向上和向下，我们将返回侧面。还记得我们必须在每个动画后放置 _up、_down 和 _side 吗？好吧，这也是有原因的。让我们在player_animations()函数下面创建一个新函数来了解我的意思。### Player.gd # older code # Animation Direction func returned_direction(direction : Vector2): #it normalizes the direction vector var normalized_direction = direction.normalized() var default_return = "side" if normalized_direction.y > 0: return "down" elif normalized_direction.y < 0: return "up" elif normalized_direction.x > 0: #(right) $AnimatedSprite2D.flip_h = false return "side" elif normalized_direction.x < 0: #flip the animation for reusability (left) $AnimatedSprite2D.flip_h = true return "side" #default value is empty return default_return在这个函数中，我们检查玩家的 x 和 y 坐标的值，并在此基础上返回动画后缀，即单词 (_up、_down、_side) 的结尾。然后，我们将这个后缀附加到要播放的动画，即行走或空闲。让我们回到我们的player_animations()函数并用此功能替换#todo代码。### Player.gd # older code # Animations func player_animations(direction : Vector2): #Vector2.ZERO is the shorthand for writing Vector2(0, 0). if direction != Vector2.ZERO: #update our direction with the new_direction new_direction = direction #play walk animation, because we are moving animation = "walk_" + returned_direction(new_direction) animation_sprite.play(animation) else: #play idle animation, because we are still animation = "idle_" + returned_direction(new_direction) animation_sprite.play(animation)现在我们要做的就是在添加运动的函数中实际调用这个函数，该函数位于_physics_process()函数的末尾。### Player.gd # older code func _physics_process(delta): # Get player input (left, right, up/down) var direction: Vector2 direction.x = Input.get_action_strength("ui_right") - Input.get_action_strength("ui_left") direction.y = Input.get_action_strength("ui_down") - Input.get_action_strength("ui_up") # If input is digital, normalize it for diagonal movement if abs(direction.x) == 1 and abs(direction.y) == 1: direction = direction.normalized() # Apply movement var movement = speed * direction * delta # moves our player around, whilst enforcing collisions so that they come to a stop when colliding with another object. move_and_collide(movement) #plays animations player_animations(direction) 如果你现在运行你的场景，你会看到，如果玩家静止，则播放空闲动画；如果玩家移动，则播放行走动画，并且玩家还会改变方向！在此过程中，我们还要为玩家的进攻和冲刺添加动画功能。对于这些动画，我们需要新的输入操作，因此在项目设置的“输入映射”下，让我们添加一个新输入。将第一个命名为ui_sprint，将第二个命名为ui_attack。将 Shift 键分配给 ui_sprint，将 CTRL 键分配给 ui_attack。如果需要，您还可以为此分配操纵杆键。我们将在_physics_process()函数中添加冲刺输入，因为我们稍后会使用它以恒定速率跟踪我们的耐力值，但我们将在_input(event)函数中添加攻击输入，因为我们希望捕获输入，但不要在游戏循环期间跟踪。您可以在 Godot 的内置_input函数中添加输入，也可以通过Input Singleton 类添加。如果您希望在整个游戏循环中存储输入操作的状态，则可以使用 Input 单例，因为它独立于任何节点或场景，例如，您可以在希望能够同时按下两个按钮的场景中使用它。_input 函数可以捕获输入is_action_pressed() ' 和is_action_released() ' 函数，而 Input 单例通过`is_action_just_pressed()' / 'is_action_just_released() ' 函数捕获输入。我制作了一些图像来简单解释不同类型的输入函数。何时使用 Input Singleton 与 input() 函数？. 在 Godot 中，你可以通过两种方式触发输入事件： input() 函数和Input Singleton。两者之间的主要区别在于，在 input() 函数中捕获的输入只能触发一次，而 Input Singleton 可以在任何其他方法中调用。. 例如，我们在 physics_process() 函数中使用 Input 单例，因为我们希望连续捕获输入以触发事件，而如果我们要暂停游戏或射击，我们会在 input() 函数中捕获这些输入，因为我们只希望捕获一次输入 - 即当我们按下按钮时。图 6：输入法。在线版本请见此处。让我们从攻击输入的代码开始。我们只想在玩家不攻击时播放其他动画，反之亦然。为此，我们需要创建一个新变量，我们将使用它来检查玩家当前是否正在攻击。### Player.gd extends CharacterBody2D # Node references @onready var animation_sprite = $AnimatedSprite2D # Player states @export var speed = 50 var is_attacking = false现在我们可以修改我们的_physics_process()函数，使其仅播放返回的动画，并且仅在玩家未攻击时处理我们的运动。### Player.gd extends CharacterBody2D # older code func _physics_process(delta): # Get player input (left, right, up/down) var direction: Vector2 direction.x = Input.get_action_strength("ui_right") - Input.get_action_strength("ui_left") direction.y = Input.get_action_strength("ui_down") - Input.get_action_strength("ui_up") # If input is digital, normalize it for diagonal movement if abs(direction.x) == 1 and abs(direction.y) == 1: direction = direction.normalized() # Apply movement if the player is not attacking var movement = speed * direction * delta if is_attacking == false: move_and_collide(movement) player_animations(direction)现在我们需要调用内置的func input(event)函数，以便我们可以播放 attack 前缀的动画。让我们将此函数添加到_physics_process()函数下方。### Player.gd extends CharacterBody2D # older code func _input(event): #input event for our attacking, i.e. our shooting if event.is_action_pressed("ui_attack"): #attacking/shooting anim is_attacking = true var animation = "attack_" + returned_direction(new_direction) animation_sprite.play(animation)现在您会注意到，如果您运行场景并按下 CTRL 来触发攻击动画，动画会播放，但我们的角色不会返回到之前的空闲或行走动画。这是因为我们的角色卡在了攻击函数的最后一帧动画上，为了解决这个问题，我们需要使用信号通知游戏动画已播放完毕，以便它可以将is_attacking变量重新设置为 false。为此，我们需要单击 AnimatedSprite2D 节点，然后在“节点”面板中将animation_finished信号连接到我们的播放器脚本。当我们的动画到达其帧的末尾时，此信号将触发，因此如果我们的 attack_ 动画已播放完毕，此信号将触发函数将 is_attacking 变量重置回 false。双击信号并选择脚本来执行此操作。什么是信号？信号是一种发射器，它为节点提供了一种无需直接引用彼此即可进行通信的方式。这使我们的代码更加灵活且易于维护。信号在某些事件发生后发出，例如，如果我们的敌人数量在敌人被杀死后发生变化，我们可以使用信号通知 UI 它需要将敌人数量的值从 10 更改为 9。现在您将看到在玩家脚本的末尾创建了一个新方法。我们现在可以简单地将is_attacking变量重置回 false。### Player.gd extends CharacterBody2D # older code # Reset Animation states func _on_animated_sprite_2d_animation_finished(): is_attacking = false接下来，我们可以继续添加玩家按住 Shift 键时的冲刺功能。由于冲刺只是移动，我们可以在_physics_process()函数中添加它的代码，该函数处理玩家的移动和物理（而不是动画）。### Player.gd extends CharacterBody2D # --------------------------------- Movement & Animations ----------------------- func _physics_process(delta): # Get player input (left, right, up/down) var direction: Vector2 direction.x = Input.get_action_strength("ui_right") - Input.get_action_strength("ui_left") direction.y = Input.get_action_strength("ui_down") - Input.get_action_strength("ui_up") # Normalize movement if abs(direction.x) == 1 and abs(direction.y) == 1: direction = direction.normalized() # Sprinting if Input.is_action_pressed("ui_sprint"): speed = 100 elif Input.is_action_just_released("ui_sprint"): speed = 50 # Apply movement if the player is not attacking var movement = speed * direction * delta if is_attacking == false: move_and_collide(movement) player_animations(direction) # If no input is pressed, idle if !Input.is_anything_pressed(): if is_attacking == false: animation = "idle_" + returned_direction(new_direction)最后，如果玩家没有按下任何输入，则应播放空闲动画。这将防止我们的玩家即使输入被释放也停留在奔跑状态。### Player.gd extends CharacterBody2D # Movement & Animations func _physics_process(delta): # Get player input (left, right, up/down) var direction: Vector2 direction.x = Input.get_action_strength("ui_right") - Input.get_action_strength("ui_left") direction.y = Input.get_action_strength("ui_down") - Input.get_action_strength("ui_up") # Normalize movement if abs(direction.x) == 1 and abs(direction.y) == 1: direction = direction.normalized() # Sprinting if Input.is_action_pressed("ui_sprint"): speed = 100 elif Input.is_action_just_released("ui_sprint"): speed = 50 # Apply movement if the player is not attacking var movement = speed * direction * delta if is_attacking == false: move_and_collide(movement) player_animations(direction) # If no input is pressed, idle if !Input.is_anything_pressed(): if is_attacking == false: animation = "idle_" + returned_direction(new_direction)如果我们运行场景，我们可以看到玩家正在通过动画在地图上移动，并且他们还可以冲刺和攻击！设置好玩家角色后，我们就可以开始制作动画了。在下一部分中，我们将创建游戏地图，即世界，并设置玩家的相机。记得保存你的游戏项目，下一部分见。本节代码下载。
SQL（Structured Query Language，结构化查询语言）是一种用于管理关系数据库的编程语言。它用于处理数据、更新数据库中的数据、从数据库中检索数据以及在数据库中执行各种其他操作。以下是SQL的一些主要用途：数据查询：SQL最常用的功能是从数据库中检索数据。使用SELECT语句，可以指定要检索的数据。数据操作：可以使用SQL来插入（INSERT）、更新（UPDATE）和删除（DELETE）数据库中的数据。数据定义：SQL可以用来创建或删除数据库中的表格，以及其他对象，比如视图和索引。这通常使用CREATE、ALTER和DROP语句来完成。数据控制：SQL提供了一系列命令来帮助控制不同数据的访问权限，例如GRANT和REVOKE语句。事务控制：SQL支持事务处理，允许执行一系列操作，要么全部成功，要么全部失败。这通常涉及COMMIT和ROLLBACK语句。SQL遵循一套标准，这些标准由国际标准化组织（ISO）和国际电工委员会（IEC）的联合技术委员会JTC 1/SC32制定。不过，不同的数据库管理系统（DBMS），如MySQL、PostgreSQL、SQLServer、Oracle等，可能会有特定的扩展和差异。下面是一个简单的SQL查询示例，用于从名为employees的表中检索特定字段的数据：SELECT name, age FROM employees WHERE age > 30; 这条语句的意思是：从employees表中选择名字（name）和年龄（age），但只选择那些年龄大于30的记录。
PV（Page View）：页面浏览量，指的是网站页面被查看的次数。每当用户访问或刷新一个页面时，都会被计为一次页面浏览。UV（Unique Visitor）：独立访客数，指的是在特定时间内访问网站的独立个体数量。通常通过IP地址和Cookie来区分不同的用户，一个独立的IP地址在一天内多次访问网站只被计算一次。IP（Internet Protocol）：互联网协议地址，指的是访问网站的独立IP地址数量。与UV类似，它用于统计访问网站的不同计算机数量，但并不区分同一台计算机上的不同用户。这三个指标都是分析网站流量和用户行为的重要参数，帮助网站运营者了解网站的受欢迎程度和用户互动情况。
推荐一款好用的PDF操作软件PDF24，功能强大优秀且完全免费
PDF24 - 优秀实用且完全免费的PDF工具箱软件。是一个提供多种PDF处理功能的平台，包括在线工具和可下载的软件。以下是PDF24的一些主要特点和服务：PDF24 Tools：这是一套免费且易于使用的在线PDF工具，可以进行PDF合并、分割、压缩、编辑、转换、加密、解密等多种操作。这些工具直接在浏览器中运行，不需要安装任何软件，支持所有操作系统和浏览器。PDF24 Creator：这是一个免费的、功能丰富的可下载PDF软件。它允许用户在没有网络连接的情况下创建、编辑、转换、压缩、识别和保护PDF文件。PDF24Creator提供了包括PDF打印机、PDF阅读器、PDF转换器、PDF压缩工具、PDF文本识别等功能。安全性：PDF24非常重视用户数据的安全。传输到PDF24服务器的文件会被完全加密，并在处理后短时间内从服务器上删除。对于更高级的安全性需求，PDF24 Creator可以将所有文件保存在用户的个人电脑上。多语言支持：PDF24支持多种语言，使其适用于全球用户。用户评价：PDF24 Creator在用户中的评价很高，最近三年的用户评分为4.89星（满分5星）。太多的使用功能了，自己研究吧，全部免费，总的来说，PDF24是一个强大且用户友好的PDF处理工具，适用于各种PDF相关的需求。官网: https://www.pdf24.org/zh/
让我们通过制作 RPG 来学习 Godot 4 — 第 2 部分：玩家设置和移动
现在我们已经完成了项目设置，现在可以继续创建玩家场景了。我们的玩家场景将包含节点和脚本，使我们能够看到角色并控制他们，以便我们可以四处奔跑并与世界互动。您将在本部分中学到什么：如何创建、运行和实例化新场景。如何向节点添加输入动作、碰撞和动画。如何向场景添加脚本和代码。如何向节点添加运动。玩家设置在我们的主场景中，我们可以继续删除之前添加的 Sprite2D 节点。我们将为玩家创建一个全新的场景，然后在主场景中实例化它。实例化允许我们从模板复制对象，以便我们可以单独修改它们。换句话说，我们将把 Player 与主场景分开，这样我们就可以将它视为一个单独的对象。一分钟后您就会更清楚地看到它是如何工作的。在工作区中，单击 Main(*) 旁边的加号图标以创建新场景。在场景 Dock 中，添加一个新的根节点。我们需要这个根节点是一个CharacterBody2D节点，它是一个专门的2D物理身体节点，用于可以通过脚本移动的角色。我们将此节点用于需要物理处理进行运动的实体。双击CharacterBody2D节点将其重命名为“Player”。我们还可以继续将新场景保存为场景文件夹中的“Player”。您会注意到根节点旁边有一个警告图标。如果将鼠标悬停在其上，您将看到警告消息。它告诉我们，我们的角色没有形状，因此无法与其他角色发生碰撞或互动。为了解决这个问题，我们需要添加一个CollisionShape2D节点。该节点将允许我们的玩家与其他碰撞体发生碰撞，这将阻止、损坏或触发我们玩家中的事件。该节点还将返回一条警告消息，因为我们尚未在检查器面板中为其分配形状。让我们通过单击“形状”旁边的<空>框并选择一个形状来解决此问题。您为对象选择的形状类型取决于您希望它们完成的任务（从它们需要碰撞的位置）以及它们的身体形状。因此，圆形角色可能会发生圆形碰撞，等等。对于我们的精灵，我们选择 CapsuleShape2D 形状。您现在将看到我们有一个碰撞形状，因此我们可以与游戏中的其他对象碰撞，但我们甚至还没有身体！要查看我们的角色，我们需要将其分配给 Sprite。现在，我们可以通过两种方式实现这一点：通过Sprite2D节点和AnimatedSprite2D节点。如果我们想要一个静态对象，即不移动或没有动画的对象，Sprite2D 节点是完美的。然而，AnimatedSprite2D 对象则恰恰相反。当我们有一个需要精灵表动画的对象时，它是完美的。选择 Player 的根节点（确保选择它而不是 CollisionShape2D 节点，否则它会将我们的节点添加到其中作为其子节点），添加一个新的 AnimatedSprite2D 节点。如果您选择了错误的节点，请右键单击要替换的节点并说出更改类型。该节点将再次返回警告，这是因为它没有分配给它的精灵表，因此它无法返回动画帧。让我们通过进入 Inspector 面板并在 Sprite Frames 旁边的 下面选择 New SpriteFrames 来消除此警告。这个 SpriteFrames 资源允许我们导入图像文件（或包含所述文件的文件夹）来为精灵提供动画帧。要配置我们的 SpriteFrames 资源，请单击它，您将在下面的面板中看到一个新选项弹出。在此面板中我们可以添加新动画、删除动画、播放动画或编辑动画。可以删除或重命名默认动画。我们将在下一部分中添加所有玩家的动画，以便我们可以更详细地探索该节点。现在，我们只需单击“从 Sprite Sheet 添加帧”选项即可为默认动画分配一个 Sprite。将弹出一个窗口，以便我们可以选择要添加的 Sprite Sheet，因此请进入“Assets”>“Player”，然后选择“Front Sheet”。将会弹出另一个窗口，在这里我们需要裁剪单独的精灵来制作动画。默认的水平和垂直值为 4。在水平方向上，我们可以算出我们的玩家帧数最多为 14，因此将水平值更改为 14。在垂直方向上，我们可以算出我们的玩家帧数达到了 5，因此将垂直值更改为 5。现在我们可以（按顺序）选择我们想要的帧来创建一个动画。该表有五种可能的动画：闲置、行走、攻击、伤害和死亡。现在，我们只选择第二行来创建基本的行走动画。确保按顺序选择它，否则，您将不得不在 SpriteFrames 面板中重新排列它。我们的默认动画现在添加了 8 帧。如果您按播放，我们可以在工作区中看到动画播放。我们的玩家走得有点慢，所以让我们将 FPS 值从 5 提高到 10。保留循环值处于启用状态。运动输入添加步行动画后，我们现在可以添加输入和功能来移动它们。在我们开始编码之前，我们需要添加一个输入动作。这是物理控件，例如我们的键盘按键、操纵杆、按钮等，我们将它们映射到我们的功能以在游戏中移动我们的角色。要添加输入操作，我们转到“项目设置”>“输入映射”。Godot 4 带有预先存在的输入操作。要查看它们，请启用“显示内置操作”开关。 ui_代表“用户输入”，您不必为将来的输入操作遵守此命名约定，但对于这个项目，我们只是保持代码一致。您会看到 Godot 已经预先配置了输入以将角色向左、向右、向上和向下移动。他们将键盘按键“向上”、“向下”、“向左”和“向下”分配给这些操作。您可以通过按它们旁边的铅笔图标来编辑它们，甚至可以删除它们。您还可以通过单击旁边的加号图标为此输入分配其他键。让我们将 WASD 键分配给 ui_up（W 键）、ui_down（S 键）、ui_left（A 键）和 ui_right（D 键）。稍后我们将回来添加我们自己的用于攻击、消耗、交互和冲刺的输入动作。目前，我们无需在此处执行任何操作，因为大多数操作已为我们添加，因此您可以完全关闭此菜单。现在让我们向播放器添加一个脚本，以便我们可以使用这些输入移动它们。您可以将脚本分配给任何节点，但请记住 - 该脚本只会影响该节点，并且只有该节点的类对象是可调用的。但是，根节点除外。如果将脚本添加到根节点，它将能够影响并调用其所有子节点的类。要将脚本分配给节点，请选择要添加功能的节点（在本例中是我们的根节点），然后在场景停靠栏中单击它旁边的小滚动图标。将此脚本另存为 Player.gd 在 Scripts 文件夹下。一旦节点附加了脚本，您将看到滚动图像出现在它旁边。如果右键单击节点，则可以分离此脚本并附加一个新脚本。单击滚动条打开脚本，这将打开脚本工作区。在顶部，它说“扩展CharacterBody2D”，因为它是从基类CharacterBody2D扩展的。这意味着 CharacterBody2D 的所有属性和子函数都可以从此脚本中调用。现在为了让我们的角色移动，我们需要首先定义一个移动速度变量。### Player.gd extends CharacterBody2D # 玩家移动 速度 @export var speed = 50@export意味着我们可以导出变量以在检查器面板中进行编辑。因此，选择节点后，您现在可以更改属性中的速度值，而无需为其分配常量值。当我们创建敌人之类的东西并希望在场景中多次实例化它们并希望它们具有不同的移动速度时，这会很有帮助。接下来，我们将使用_physical_process(delta)函数，该函数不断处理角色的运动物理。在这个函数中，我们需要获取玩家输入的方向（左、右、上、下）。该输入需要标准化以获得更平滑的对角线移动，因为我们的玩家在笛卡尔平面水平上移动。这意味着它们需要向上、向下、向左或向右移动 1 个空间，并且归一化会将向量在任何方向上设置为相同的长度。何时使用 _processing() 和 _physical_processing()？对于图形化或需要快速响应的事物，请使用 _process() 函数。对于基于物理或需要以一致、可预测的速率发生的事情，请使用_physical_process() 函数。什么是向量？矢量是表示力、速度和位置等量的对象。在 2D 游戏中，我们使用向量来确定和计算实体在 X 轴和 Y 轴上的位置。图 5：矢量运动在平面尺度上的样子。然后，我们将使用 Godot 中的内置move_and_collide() 方法来移动并启用物理系统来移动我们的玩家，同时还强制碰撞，以便它们在弹到其他对象时停止。何时使用 move_and_slide 和 move_and_collide()？当您希望角色沿一般方向移动时（例如在平台游戏中），请使用 move_and_slide()。当您需要有关角色碰撞的详细信息以执行自定义操作时（例如在益智游戏中角色需要移动或躲避障碍物），请使用move_and_collide()。### Player.gd extends CharacterBody2D # Player movement speed @export var speed = 50 func _physics_process(delta): # Get player input (left, right, up/down) var direction: Vector2 direction.x = Input.get_action_strength("ui_right") -Input.get_action_strength("ui_left") direction.y = Input.get_action_strength("ui_down") - Input.get_action_strength("ui_up") # If input is digital, normalize it for diagonal movement if abs(direction.x) == 1 and abs(direction.y) == 1: direction = direction.normalized() # Apply movement var movement = speed * direction * delta # Moves our player around, whilst enforcing collisions so that they come to a stop when colliding with another object. move_and_collide(movement)保存您的脚本，让我们回到主场景。单击加号图标旁边的链图标，这将帮助我们将玩家场景实例（添加）到模式场景中。选择播放器场景。按 F 键将焦点放在主场景中新实例化的 Player 场景上，您将看到它已被添加，但我们看不到它的子节点（碰撞和精灵节点）。这是因为它现在被视为一个完整的对象，而不是单个节点。现在缩小，直到您在主场景中看到蓝色边框。这个蓝色窗口是可见游戏屏幕或视口的边框，简单来说，就是运行游戏时看到的屏幕尺寸。我们的玩家将在左上角生成，因此我们无法正确看到他们。为了防止这种情况，我们选择它们并将它们拖动到靠近中间或内边缘的空间。确保为此启用了选择工具，即光标。如果我们现在运行游戏，玩家将会非常小。这是因为我们的游戏窗口被缩小到离玩家所在的位置很远。这是因为我们的视口尺寸太大，我们的拉伸模式需要改变。进入项目设置 > 显示 > 窗口，然后进行以下更改：我们的角色现在将被放大，我们可以通过单击播放按钮或按 F5 来运行我们的游戏。当我们输入 WASD 或方向键时，玩家正在地图上移动，但没有任何动画！这是因为我们还没有为我们的输入分配任何动画，但是当我们将动画作为一个独立的主题来关注时，我们将在下一部分中这样做。设置好玩家角色后，我们就可以继续制作动画了。您可能想知道世界地图以及我们角色的相机设置，但不用担心，我们会在适当的时候完成所有内容。请记住保存您的游戏项目并进行备份，我们将在下一部分中见到您。本节代码下载。
让我们通过制作 RPG 来学习 Godot 4 — 第 1 部分：项目概述和设置
本系列2D游戏自学开发，使用的是Godot4和GDScript开发，实现了如何从头到尾制作游戏，并最终向示作为Godot开发人员初学者需要了解的所有内容！我们将要创建的游戏是什么？该游戏是RPG（角色扮演游戏），玩家可以在整个地图上奔跑，射击和掠夺敌人，并完成任务。敌人会随机出现，如果靠近玩家，他们会四处游荡并追逐玩家。我们还将有NPC和店主，他们会为玩家提供任务并向玩家出售拾取物品。图 1：我们的游戏和实体的概览。什么是RPG？RPG（角色扮演游戏）是一种电子游戏类型，玩家在虚构的场景中扮演角色。玩家负责在叙事中扮演这些角色，无论是通过文字表演还是通过有关角色发展的结构化决策过程。RPG通常涉及故事叙述、世界构建和角色发展。本系列教程将涵盖以下方面：玩家动画、动作和统计数据完整的游戏UI拾取器和耗材敌人运动、动画和统计数据敌人随机游走和生成随机掉落战利品玩家和敌人的死亡、伤害和攻击经验和升级带有任务的基本可定制NPC场景转换（位置改变）游戏音乐和音效存档、暂停和加载系统带碰撞和摄像头的游戏地图商店项目导出请注意，本教程系列适用于初级GDScript和Godot开发人员。此外，本教程使用Godot版本4.1.1。如果您在使用较新版本时遇到错误，请不要惊慌，因为这可能会发生。我会尝试尽可能多地更新此项目以使用较新版本的Godot。我还会在各个章节的末尾将分段源代码链接到每个部分，以便您可以轻松地将项目的代码与您的代码进行比较。您将在本部分中学习到的内容：如何浏览Godot编辑器。如何更改项目设置以及使用节点属性。如何创建场景。如何向场景添加节点。编辑概述在我们开始编写代码之前，首先需要了解Godot编辑器的布局。如果您已经熟悉编辑器，则可以跳至本部分的项目设置部分。如果不熟悉，我建议您和我一起浏览编辑器。如果你还没有安装 Godot，请前往goddot网站下载可用的最新版本。下载完成后，解压文件，由于 Godot 不需要任何安装，你可以直接将其拖到你希望存储在计算机上的位置 — 或者直接从下载文件夹中打开它。启动可执行文件，您将看到项目管理器窗口。在这里您可以创建新项目、加载现有项目或扫描整个目录以导入多个项目。在顶部，您将看到一个名为“资产库项目”的菜单。在这里，您可以下载和查找社区成员为 Godot 制作的项目、模板或演示。它类似于其他在线市场，例如 Itch.io、Unreal Marketplace 或 Unity Asset Store。唯一的区别是这里的一切都是免费的。要创建项目，我们将点击“新项目”，然后会出现一个窗口，我们将在其中选择项目的保存位置并为其命名。我将我的项目命名为 Dusty Trails，但您可以随意命名。渲染模式有哪些？Forward+ 模式，适用于图形和渲染更密集的更高级的游戏项目。移动模式，一般用于手机游戏。兼容模式使用与 Godot 3 相同的渲染器，这意味着图形和渲染质量较低，因此更适合较旧或规格较低的设备。您可以在兼容模式或 Forward+ 模式下启动游戏 - 这取决于您的图形驱动程序是否支持 Godot 提供的 Vulkan 版本。您可以在此处阅读有关新渲染更新的更多信息。选择“创建和编辑”，您将神奇地进入项目窗口。顶部是工作区，这是我们完成任务的空间。默认情况下，3D 将始终处于活动状态。如果单击其他选项，则可以在不同的工作区之间切换。7_mYbocY2AnVpCLU1koe1aWw.jpg图 2：工作场所导航的鼠标输入工作区有何用途？2D 工作区用于编辑和查看 2D 游戏的 2D 场景。3D 工作区用于编辑和查看 3D 游戏的 3D 场景。脚本 工作区是我们编写、编辑和查看脚本的代码编辑器。Asset工作区是您可以在 项目中免费使用的资产库项目。窗口左侧的面板是场景停靠栏。它列出了您当前打开的场景的内容，您还可以在此停靠栏中向场景添加新节点。Godot 中的场景是节点的主要持有者或容器，而节点是游戏的主要构建块。不同的节点有不同的功能，例如，碰撞形状和动画精灵各有其用途，因此能够将它们组合在一个单一的场景中使我们能够为游戏创建具有更复杂功能的组件。场景还可以实例化其他场景，例如，在您的世界场景中拥有敌人场景。我建议您去看看 Godot 文档中的节点和场景。什么是场景和节点？ 场景是一个容器，用于容纳按层次结构排列的节点集合（例如我们的玩家或敌人）。节点是场景中构成场景的基本单元（例如场景中的Sprite 或相机）。图 3：节点和场景的示例。点击此处查看在线版本。另一个面板称为“导入”，您可以在此设置或编辑已添加到项目文件夹的资产（例如图像、字体和音频文件）的属性，然后可以使用保存的这些属性重新导入它们。在场景和导入面板下方，您将看到文件系统面板，您可以在此管理项目文件、文件夹和资产。您可以将新资产拖到此面板以将其导入项目，以及添加新文件夹、脚本、场景等。窗口右侧的面板称为检查器面板，您可以在其中查看和编辑当前选定节点的属性。旁边的节点面板是您可以连接信号并向节点添加组的地方。组可用于将场景或节点类型分组在一起，例如我们的 NPC，以便我们的玩家知道如何与这些节点交互。信号可用于许多情况，例如，通知游戏已按下按钮或动画已播放完毕。这使我们能够告诉游戏在发生某些事件时该做什么。大多数节点都有内置信号，例如按钮和动画播放器，但我们也可以创建自己的自定义信号。我们稍后会更深入地解释这些概念。图 4：自定义信号（左）和内置信号（右）。点击此处查看在线版本。在 Inspector 和 Node Dock 上方，您将看到用于播放、暂停、重新启动或停止场景的选项。我们将在这里启动游戏进行测试。在编辑器窗口的底部，有输出面板、调试器控制台、音频混音器、搜索结果面板以及动画和着色器编辑器。默认情况下，它们处于折叠状态以节省屏幕空间，但如果您单击它们，它们将最大化。某些可自定义的节点（例如图块地图或动画播放器节点）将向此窗口添加新选项。让我们回顾一下每个现有的选项：输出 控制台将返回我们添加到代码中用于测试的打印消息或通知。调试器 控制台用于（您猜对了）调试和错误处理。音频 混音器用于编辑音频文件。动画 编辑器用于为动画播放器节点创建动画。在上方的功能区或菜单栏中，您有几个选项。您可以在“场景”选项卡下管理场景。可以添加、保存、打开或关闭场景。您可以在“项目”选项卡下管理项目设置和属性（例如输入操作、窗口大小、自动加载单例等）。您也可以从这里退出“项目管理器”窗口。这也是我们将从这里导出项目的地方。您可以在“调试”选项卡中启用或禁用调试工具，例如，使碰撞可见，以便您可以看到碰撞事件是否在游戏过程中执行。编辑器选项卡允许您更改与编辑器相关的设置，例如，您可以更改其当前布局或主题。您甚至可以截取当前窗口的屏幕截图。最后，帮助选项卡可让您快速访问 Godot 4 文档和社区论坛。如果您对使用编辑器感到更自在，我们可以继续设置我们的项目。项目设置在开始一个项目之前，我通常会列出制作游戏需要采取的步骤（或检查表）。这通常只是一个大纲，在实际制作游戏的过程中，我会对这些步骤进行不同的修改。关于游戏开发，有一件事要记住，那就是这不应该是一段线性的旅程。每个人的做法都不一样，但如果你对我的步骤通常是什么样子感兴趣，你可以在这里查看 Dusty Trails 项目步骤的示例，并将其用作你自己游戏的样板（或者不这样做，这 100% 取决于你）。我还为这款游戏使用了我在网上找到的这个调色板，以防你想知道我的配色方案值从哪里来！现在开始设置。本节所需的资产可在此处下载。这些免费资产的来源可在我的GitHub 存储库中找到。下载资产后，提取文件并将其拖到文件系统面板中。我们还可以继续删除项目中存在的默认 icon.svg 和 README.MD 文件。右键单击面板，然后创建三个新文件夹：Scenes、GUI 和 Scripts。Scenes 文件夹将包含我们所有的 .tscn 文件（玩家场景、NPC 场景、地图场景等），Scripts 文件夹将包含我们所有的 .gd 文件，GUI 文件夹将包含我们所有的 UI 元素场景（菜单、弹出窗口、HUD 等）。在 Scenes 文件夹中，让我们创建第一个场景，即主场景。主场景将作为游戏元素的核心容器。在这里，我们将添加地图、角色和物品，以便玩游戏。主场景也是游戏启动时自动运行的场景。要添加场景，您可以右键单击 FileSystem 面板并像我们添加文件夹一样添加它，或者您可以单击 Scene Dock 中的加号图标。这是更快捷的方法，因此从现在开始我将以这种方式进行操作。您将看到它会提示您选择一个节点。您在此处选择的节点将是根节点或父节点。根节点是树结构中的最高节点，没有父节点。我们以后可以随时更改此节点，所以如果您选错了也不必担心！我们需要选择Node2D作为根节点。Node2D 节点是 2D 游戏的基础节点。我们通常在主场景中使用此节点。您还可以在场景面板中的“2D 场景”选项中自动选择此节点作为您的根节点。现在您将看到 Node2D 节点已添加为我们的根节点，但我们的场景尚未保存。在上面的“场景”菜单中保存它或使用快捷键 CTRL + S。将场景保存为“场景”文件夹中的“主”场景。双击 Node2D 根节点以重命名它。为了简单起见，我们将其重命名为与场景名称匹配的名称。现在我们可以将子节点添加到根节点。您可以再次通过加号图标执行此操作。请记住，我们正在制作 2D 游戏，因此以“3D”结尾的节点将无法与我们的节点一起使用！现在让我们添加一个 Sprite2D 节点，因为我想向您展示一些东西。请注意，您添加的任何节点都将添加到节点菜单中的最近面板中，因此您下次可以轻松地从那里选择它，而无需搜索它！在 2D 工作区中（因为我们在制作 2D 游戏），选择您的 Sprite2D 节点，然后按键盘上的 F 键将焦点放在所选节点上。选择节点后，在检查器面板中，您将看到纹理选项。这是我们精灵的图像。如果您进入资产文件夹，您可以将任何您想要的图像拖到该纹理属性中。如果你放大你的精灵，你可能会注意到它有点模糊。这可不行，但幸运的是，我有一个解决办法。在项目设置选项中的“项目”>“项目设置”下，将纹理渲染从“线性”更改为“最近”。这将扩大我们的纹理渲染，以便图像资产始终保持高质量。什么是纹理过滤？纹理过滤是计算机图形学中使用的一种技术，用于在纹理应用于 3D 模型或 2D图像时改善纹理的外观。当纹理被缩放、旋转或以其他方式变换时，它变得尤为重要。选择正确的纹理过滤模式对于提高 2D 和 3D 图形的图像质量、性能和整体视觉体验至关重要。你的精灵现在应该很清晰了。这部分我们需要做的最后一件事是设置我们的主场景。在项目设置中的“应用程序”>“运行”下，您可以将主场景设置为我们新创建的场景。这样，每当我们运行游戏时，这个场景就会运行。稍后，我们将把它改为菜单场景，因为您希望玩家看到开始菜单，而不是直接进入游戏。恭喜，您已经学会了如何浏览 Godot 编辑器，以及如何使用节点和场景 - 这是您在整个游戏过程中将使用的核心基础。为您的项目创建自己的GitHub 存储库将是一个很好的做法，这样您就可以定期备份您的项目。在下一部分中，我们将设置我们的游戏角色，即我们的玩家，并添加在游戏中移动它们的功能。这意味着更多的节点，你将第一次使用脚本！记得保存你的游戏项目，下一部分见。本节代码下载。