我的自习室—让生活从这里沉淀 - 2025年1月
type: channel
link: http://www.agoodlife.cn/index.php/2025/01/
description: 这里您可以静下心来学习，有自己的爱好，我喜欢编程，例如Godot游戏开发，HTML、CSS和Javascript前台开发，c#，sqlserver后台数据开发，还喜欢记录网海发现的各类好软件。
items: 让我们通过制作 RPG 来学习 Godot 4 — 第 16 部分：等级和 XP
items: 让我们通过制作 RPG 来学习 Godot 4 — 第 15 部分：玩家死亡和游戏结束
items: 让我们通过制作 RPG 来学习 Godot 4 — 第 14 部分：敌人射击和造成伤害
items: 让我们通过制作 RPG 来学习 Godot 4 — 第 13 部分：敌人死亡时掉落战利品
items: 让我们通过制作 RPG 来学习 Godot 4 — 第 12 部分：玩家射击和造成伤害
items: 让我们通过制作 RPG 来学习 Godot 4 — 第 11 部分：生成敌人 AI
items: 让我们通过制作 RPG 来学习 Godot 4 — 第 10 部分：动画 AI 运动
items: 让我们通过制作 RPG 来学习 Godot 4 — 第 9 部分：敌人 AI 设置
items: 让我们通过制作 RPG 来学习 Godot 4 — 第 8 部分：添加弹药拾取和消耗品
items: 让我们通过制作 RPG 来学习 Godot 4 — 第 7 部分：设置游戏 GUI #3

让我们通过制作 RPG 来学习 Godot 4 — 第 16 部分：等级和 XP
type: item
date: 2025-01-22T12:30:32+00:00
description: 现在我们有了基本的敌人和玩家，我们需要给自己一个杀死敌人的动机。我们可以通过升级系统来实现这一点，该系统在获得一定数量的 XP 后提高玩家的等级。如果玩家升级，他们会得到奖励，包括拾取物品和属性（健康、耐力）补充。我们还会在升级时增加他们的最大健康和最大耐力值。您将在本部分中学习到的内容：· 如何使用 Popup 节点。· 如何暂停场景树。· 如何允许/禁止输入处理。· 如何改变节点的处理模式。· 如何（可选）更改鼠标光标的图像和可见性。升级概述升级弹出窗口准备就绪后，打开你的游戏项目，在你的播放器脚本中通过你的信号定义三个新信号，它们将更新我们的 xp、xp 要求和级别值。### Player.gd # Custom signals signal health_updated signal stamina_updated signal ammo_pickups_updated signal health_pickups_updated signal stamina_pickups_updated signal xp_updated signal level_updated signal xp_requirements_updated接下来，我们需要创建这些信号在发出时将更新的变量。您可以将玩家的初始经验值、等级和所需经验值更改为您想要的任何值。### Player.gd # XP and levelling var xp = 0 var level = 1 var xp_requirements = 100如果您还记得我们在前面的部分如何更新健康和耐力 GUI 元素，您就会知道我们必须在玩家场景中的 XP 和 Level 元素中创建函数，然后将它们连接到玩家场景中的信号。在您的 Player Scene 中，在您的 UI CanvasLayer 下方，将新脚本附加到您的 XP 和 Level ColorRect。确保将其保存在您的 GUI 文件夹下方。我们的 XP ColorRect 也应该有两个值，一个用于我们的 XP，一个用于我们的 XP 要求，因此请继续复制您的 Value 节点。其变换值如下图所示。我们想要从这些 ColorRects（而不是 Label）更新 Value 子节点，因此让我们继续为每个新脚本创建一个函数来更新我们的 XP 和 Level 值。### XPAmount.gd extends ColorRect # Node refs @onready var value = $Value @onready var value2 = $Value2 #return xp func update_xp_ui(xp): #return something like 0 value.text = str(xp) #return xp_requirements func update_xp_requirements_ui(xp_requirements): #return something like / 100### LevelAmount.gd extends ColorRect # Node refs @onready var value = $Value # Return level func update_level_ui(level): #return something like 0 value.text = str(level)现在我们只需将这些 UI 元素函数连接到我们的 Player 脚本中每个新创建的信号。### Player.gd extends CharacterBody2D # Node references @onready var animation_sprite = $AnimatedSprite2D @onready var health_bar = $UI/HealthBar @onready var stamina_bar = $UI/StaminaBar @onready var ammo_amount = $UI/AmmoAmount @onready var stamina_amount = $UI/StaminaAmount @onready var health_amount = $UI/HealthAmount @onready var xp_amount = $UI/XP @onready var level_amount = $UI/Level @onready var animation_player = $AnimationPlayer func _ready(): # Connect the signals to the UI components' …

让我们通过制作 RPG 来学习 Godot 4 — 第 15 部分：玩家死亡和游戏结束
type: item
date: 2025-01-20T12:41:03+00:00
description: 现在我们的玩家可以被敌人伤害，我们可以继续实现该功能，以便他们可以死亡，如果他们已经死亡，游戏就结束了，并显示游戏结束屏幕。稍后，我们将在此游戏结束系统的基础上进行构建，以便我们重定向到主菜单屏幕。您将在本部分中学习到的内容：· 使用 AnimationPlayer 节点进一步练习。· 进一步练习使用 CanvasLayer 节点。在 Player 场景中，向 UI 节点添加新的 Label 和 ColorRect。将 ColorRect 重命名为“GameOver”。它应该如下所示：将 GameOver 节点的大小更改为游戏窗口的大小 (x: 320, y: 180)。此外，将其 Color 属性更改为 #d6c376。选择你的 Label 节点，将其文本更改为“GAME OVER!!!”。确保它是大写的，并将锚点预设更改为居中。让我们为这个标签分配一个新字体。在“控制”>“主题覆盖”>“字体”下，快速加载新字体并选择字体为“Schrödinger.tff”。将字体大小设置为 20。现在通过单击旁边的眼睛图标来隐藏 GameOver 节点。在窗口底部的动画面板中，单击动画标签创建一个新动画。将此动画命名为“game_over”。对于此动画，我们希望当玩家死亡时，游戏结束屏幕会慢慢显示出来。因此，我们将更改 ColorRect 节点 (GameOver) 的调制属性。添加新的 Property Track 并将其连接到 GameOver 节点。选择调制值属性并添加两个键 - 一个在关键帧 0，另一个在关键帧 1。选择第一个关键帧 (0)，然后将 Value 属性中的 Alpha 值更改为 0。这是因为我们希望窗口从不可见变为可见。您可以播放动画来查看我们想要的效果。现在，在我们的死亡条件之下的玩家伤害代码中，让我们播放这个动画。### Player.gd # older code # ------- Damage & Death ------------ #does damage to our player func hit(damage): health -= damage health_updated.emit(health, max_health) if health > 0: #damage animation_player.play("damage") health_updated.emit(health, max_health) else: #death set_process(false) animation_player.play("game_over")如果您运行场景，并且玩家被击中次数足够多，以至于其生命值低于 0，则应该显示游戏结束屏幕。稍后我们将使此屏幕在返回主屏幕时可暂停。现在我们的玩家可以“死亡”，游戏会通知我们游戏结束了。我们将在接下来的几部分中继续改进这个系统，但在下一部分中，我们将添加等级和 XP 功能，这将在完成任务或杀死敌人后增加我们的 XP 和等级。记得保存你的项目，下一部分见。

让我们通过制作 RPG 来学习 Godot 4 — 第 14 部分：敌人射击和造成伤害
type: item
date: 2025-01-18T12:28:20+00:00
description: 如果玩家是游戏中唯一能够造成伤害的实体，那就不公平了。这会让他们成为毫无威胁的强大恶霸。这就是为什么在这一部分我们要赋予敌人反击的能力，他们将能够对我们的玩家造成真正的伤害！这个过程与我们赋予玩家射击和造成伤害的能力时所做的类似。这次只是反过来。这部分可能需要一段时间，所以请放松，让我们的敌人值得成为我们的敌人！您将在本部分中学习到的内容：· 如何使用 AnimationPlayer 节点。· 如何使用 RayCast2D 节点。· 如何使用调制值。· 如何复制/粘贴节点以及复制对象。敌人射击之前，我们在敌人脚本中添加了一些尚未使用的子弹和攻击变量。我们无法控制敌人，因此我们需要某种方式来确定他们是否面对我们。我们可以使用RayCast2D节点，它将创建一条射线或线，该射线或线将击中敌人周围有碰撞的节点。然后，将使用此射线投射来查看它们是否击中了玩家的碰撞（称为“玩家”）。如果是，我们将触发敌人向我们射击，因为这意味着他们面对着我们。这将产生一颗子弹，如果击中我们的玩家，将对我们造成伤害。让我们将此节点添加到我们的敌人场景树中。您将看到一条射线或箭头现在从您的敌人那里射出。您可以在 Inspector 面板中更改此射线的长度。我现在将我的设置为 50。我们想将这条射线移向敌人所面对的方向。由于我们在_physics_process()函数中执行了所有移动代码，因此我们可以在那里执行此操作。我们将射线投射方向转向敌人，乘以他们的射线投射箭头长度的值（对我来说是 50）。这是他们能够撞击其他碰撞的程度。### Enemy.gd extends CharacterBody2D # Node refs @onready var player = get_tree().root.get_node("Main/Player") @onready var animation_sprite = $AnimatedSprite2D @onready var animation_player = $AnimationPlayer @onready var timer_node = $Timer @onready var ray_cast = $RayCast2D # older code # ------------------------- Movement & Direction --------------------- # Apply movement to the enemy func _physics_process(delta): var movement = speed * direction * delta var collision = move_and_collide(movement) #if the enemy collides with other objects, turn them around and re-randomize the timer countdown if collision != null and collision.get_collider().name != "Player": #direction rotation direction = direction.rotated(rng.randf_range(PI/4, PI/2)) #timer countdown random range timer = rng.randf_range(2, 5) #if they collide with the player #trigger the timer's timeout() so that they can chase/move towards our player else: timer = 0 #plays animations only if the enemy is not attacking if !is_attacking: enemy_animations(direction) # Turn RayCast2D toward movement direction if direction != Vector2.ZERO: ray_cast.target_position = direction.normalized() * 50如果您在“调试”菜单中启用碰撞可见性，并运行游戏，您将看到敌人四处奔跑，射线投射会击中他们所面对方向的任何碰撞。让我们将我们的播放器组织到一个名为“播放器”的新组下。现在，我们可以更改process()函数，使其在与“player”组中的节点发生碰撞时生成子弹并播放敌人的射击动画。整个过程类似于ui_attack下的 Player 代码— 无需计算时间…

让我们通过制作 RPG 来学习 Godot 4 — 第 13 部分：敌人死亡时掉落战利品
type: item
date: 2025-01-15T12:30:00+00:00
description: 我们终于可以射击并杀死敌人了，但我们可以做得更好。我希望我们升级敌人的死亡功能，以便我们的敌人掉落我们可以捡起的战利品。这种战利品掉落的几率为 90%，并且战利品将从我们的 Pickups 枚举中随机抽取。这意味着我们的敌人可能会掉落弹药、健康饮料或耐力饮料。您将在本部分中学习到的内容：· 如何随机化百分比和枚举返回值。在 damage() 函数的死亡条件中，我们需要使用随机方法来随机化战利品变化百分比。如果值小于 0.9（90%），我们将随机化一个拾取物。这个拾取物也将根据我们拥有的枚举值数量（即 3）进行随机化。然后，这个拾取物将被添加到我们的场景中，并在我们被杀死的敌人的位置生成。### Enemy.gd extends Area2D # older code #will damage the enemy when they get hit func hit(damage): health -= damage if health > 0: #damage animation_player.play("damage") else: #death #stop movement timer_node.stop() direction = Vector2.ZERO #stop health regeneration set_process(false) #trigger animation finished signal is_attacking = true #Finally, we play the death animation and emit the signal for the spawner. animation_sprite.play("death") death.emit() #drop loot randomly at a 90% chance if rng.randf() < 0.9: var pickup = Global.pickups_scene.instantiate() pickup.item = rng.randi() % 3 #we have three pickups in our enum get_tree().root.get_node("Main/PickupSpawner/SpawnedPickups").call_deferred("add_child", pickup) pickup.position = position什么是 call_deffered？call_deferred是一种允许您将另一个方法的调用推迟到当前帧或周期结束时的方法。这在您想要更改场景树或其他对象但又不希望这些更改在当前函数或信号期间立即生效的情况下特别有用。如果你现在运行场景并杀死敌人，他们掉落战利品的几率应该为 90%。你可以根据需要将此值降低到 50% 或 60% — 根据你对敌人死亡时掉落战利品的概率的偏好进行设置。现在我们的敌人会掉落战利品，我们不必不断寻找拾取物。在下一部分中，我们将为我们的敌人添加功能，使其能够向我们的玩家射击并伤害我们。记得保存您的项目，下一部分见。本节代码下载。

让我们通过制作 RPG 来学习 Godot 4 — 第 12 部分：玩家射击和造成伤害
type: item
date: 2025-01-13T13:19:00+00:00
description: 如果不能射击敌人，敌人还有什么用呢？在这一部分中，我们将创建子弹，让玩家可以朝他们面对的方向射击。然后，我们将添加一个动画，显示如果我们的子弹击中敌人，敌人会受到伤害。如果子弹击中敌人的程度足以使其生命值耗尽，他们也会死亡。稍后，我们将提高玩家的 XP，并在敌人死亡时为其添加战利品。但现在，让我们系好安全带，因为这部分会很长！您将在本部分中学习到的内容：· 如何使用 AnimationPlayer 节点。· 如何使用 RayCast2D 节点。· 如何使用调制值。· 如何使用 Time 类。生成的子弹让我们首先创建玩家射击或攻击时产生的子弹。此场景将类似于我们的 Pickups 场景的结构。创建一个以 Area2D 节点为根的新场景。将此节点重命名为 Bullet 并将其保存在您的 Scenes 文件夹下。由于没有形状，因此会出现警告消息。我们可以通过向其添加 CollisionShape2D 节点并以 RectangleShape2D 作为形状来修复此问题。我们还需要查看我们的节点/子弹。我们的子弹将有一个撞击动画，因此我们需要添加一个 AnimatedSprite2D 节点。在 Inspector 面板中添加新的 SpriteFrames 资源，然后在下面的 SpriteFrames 窗格中添加一个名为“impact”的新动画。我们将用于子弹的 spritesheet 可以在 Assets > FX > Death Explosion.png 下找到。水平方向我们算 8 帧，垂直方向我们算 1 帧，因此让我们相应地更改值以裁剪动画帧。此外，只为动画选择前三帧。将 FPS 值更改为 10，并将其循环值关闭。我们还需要将一个启用了自动播放的 Timer 节点添加到场景中。当子弹在未击中或撞击任何东西后需要“自毁”时，此计时器将发出timeout()信号。最后，向场景添加脚本并将其保存在脚本文件夹下。好的，现在我们可以开始讨论我们的 Bullet 场景需要做什么。我们首先需要定义一些变量来设置子弹的速度、方向和伤害。我们还需要再次引用我们的 Tilemap 节点，以便我们可以忽略与某些层的碰撞，例如我们的水，它添加了碰撞，但它不应该阻止子弹。### Bullet.gd extends Area2D # Bullet variables @onready var tilemap = get_tree().root.get_node("Main/Map") var speed = 80 var direction : Vector2 var damage然后我们需要计算子弹射出后的位置。如果玩家移动，则应为每一帧重新计算此位置。因此，我们可以在内置的process()函数中计算它，以便它在每一帧移动期间更新其方向。我们可以通过将当前位置乘以方向乘以当前帧捕获的速度（作为增量）来计算此位置。### Bullet.gd extends Area2D # Bullet variables @onready var tilemap = get_tree().root.get_node("Main/Map") var speed = 80 var direction : Vector2 var damage # ---------------- Bullet ------------------------- # Position func _process(delta): position = position + speed * delta * direction当我们进行拾取时，我们将 Area2D 的 body_entered() 信号连接到我们的场景，以将拾取物添加到我们玩家的库存中。我们想对我们的 Bullet 场景做同样的事情，但这次我们将检查进入的身体是否是我们的敌人，以便我们可以伤害他们。我们还想检查进入 Bullets 碰撞的身体是否是来自 TileMap 的 Player 或“水”层，因为我们想忽略这些碰撞。将 body_entered() 信号连接到 Bullet 脚本。您将看到它在脚本末尾创建了一个新的“func _on_body_entered(body):”函数。在这个新函数中，让我们实现上面设定的目标。#Collision detection for the bullet func _on_body_entered(body): # Ignore collision with Player if body.name == "Player": return # Ignore collision with Water if body.name == "Map": #water == Layer 0 if tilem…

让我们通过制作 RPG 来学习 Godot 4 — 第 11 部分：生成敌人 AI
type: item
date: 2025-01-11T15:07:00+00:00
description: 想象一下，手动将 50 多个敌人添加到我们的场景中需要做多少工作。这不仅看起来很混乱，而且还意味着我们的场景中只有 50 个敌人，如果我们杀死他们所有人，他们就会消失，直到我们重新加载游戏。为了解决这个小难题，我们可以创建一个敌人生成器，它将在整个地图的随机位置以恒定值生成我们的敌人，这意味着我们的敌人永远不会多于或少于我们定义的敌人。每次我们杀死一个敌人并将其移除时，生成器都会生成一个新的敌人，依此类推。我们还将使我们的生成器足够灵活，以便我们可以在 Inspector 面板中更改生成区域和最大敌人数。这意味着我们可以在主场景中实例化多个生成器，以便在不同区域生成 x 个敌人。这听起来有趣吗？好吧，你还在等什么？让我们生成一些敌人吧！您将在本部分中学习到的内容：· 如何使用计时器节点。· 如何创建场景参考。· 如何引用其他场景中的节点。· 如何引用 TileMap 属性。· 如何使用矩形属性。首先，让我们从主场景中删除敌人场景的所有实例。我们不会再像这样实例化我们的敌人，因为生成器会处理好这件事。让我们创建一个以Node2D为根的新场景。这将允许我们将敌人节点绘制到场景中。将此 Node2D 重命名为“EnemySpawner”并将其保存在您的 Scenes 文件夹下。我们需要向此场景添加一个 Timer 节点，以便我们每秒可以生成一个敌人 — 除非我们已经达到最大敌人数量。启用 AutoStart，因为我们希望此计时器在游戏开始时立即启动，以便我们开始生成敌人。我们还需要返回到敌人场景并禁用那里的计时器，因为我们不希望敌人在完成生成之前开始移动。稍后我们将在代码中启动此计时器。将脚本附加到您的根节点并将其保存在您的脚本文件夹下。最后，让我们添加一个新的 Node2D 节点，我们将在下面组织生成的敌人。在此脚本中，我们需要定义一些变量。这些变量将：· 随机化敌人的生成位置。· 设置并导出敌人最大值。· 设置并导出敌人当前的生成数量。· 设置对我们地图的 tilemap 属性的引用，以便我们的敌人不会出现在某些图层上（例如，我们的水和树叶）。###EnemySpawner.gd extends Node2D # Node refs @onready var spawned_enemies = $SpawnedEnemies @onready var tilemap = get_tree().root.get_node("Main/Map") # Enemy stats @export var max_enemies = 20 var enemy_count = 0 var rng = RandomNumberGenerator.new()在我们的全局脚本中，我们还将加载对敌人场景的引用。### Global.gd extends Node # Scene resources @onready var pickups_scene = preload("res://Scenes/Pickup.tscn") @onready var enemy_scene = preload("res://Scenes/Enemy.tscn")为了生成敌人，我们需要创建一个函数，该函数将在游戏开始时生成现有敌人，​​并在计时器超时时生成其余敌人。我们可以将函数称为spawn_enemy()。在此函数中，我们将创建对敌人场景的引用实例。当我们创建场景实例时，它会将其保存为资源，而无需每次调用​​时都从磁盘加载它。这节省了空间和时间。###EnemySpawner.gd # older code # ----------------------- Spawning ------------------ func spawn_enemy(): var enemy = Global.enemy_scene.instantiate()现在我们有了敌人场景的实例，我们可以通过add_child()方法将敌人添加为节点层次结构的子节点。简而言之，我们将敌人从敌人场景添加到生成器的场景树中。###EnemySpawner.gd # older code # --------------------------------- Spawning ------------------------------------ func spawn_enemy(): var enemy = Global.enemy_scene.instantiate() spawned_enemies.add_child(enemy)如果你从逻辑上思考，你就会知道你不能随便在任意位置生成敌人。如果我们的游戏在建筑物下生成敌人怎么办？或者在水中生成敌人怎么办？我们将无法接近敌人，因此为了防止发生…

让我们通过制作 RPG 来学习 Godot 4 — 第 10 部分：动画 AI 运动
type: item
date: 2025-01-10T12:13:00+00:00
description: 如果敌人只是以静态图像的形式漂浮在我们的地图上，那它有什么用呢？这不行，所以让我们开始着手实现动画，让我们的敌人活过来。我还给你准备了一个惊喜：我们已经编写了大部分敌人的动画代码。嗯，不是真的，我的意思是它仍然在 Player 代码中，但这意味着我们可以继续复制和粘贴一些代码，这极大地加快了我们的开发时间。您将在本部分中学习到的内容：· 如何向不可控节点添加动画。· 进一步练习向量。在您的 Player 脚本中，我们想要将两个完整的函数复制到我们的 Enemy 脚本中。您应该复制的第一个函数是您的 func player_animations(direction: Vector2)函数，第二个函数是您的func returned_direction(direction: Vector2)函数。将Enemy 脚本中的player_animations()函数重命名为enemy_animations()。### Enemy.gd extends CharacterBody2D # Node refs @onready var player = get_tree().root.get_node("Main/Player") @onready var animation_sprite = $AnimatedSprite2D # Enemy stats @export var speed = 50 var direction : Vector2 # current direction var new_direction = Vector2(0,1) # next direction var animation # older code # Animation Direction func returned_direction(direction : Vector2): #it normalizes the direction vector to make sure it has length 1 (1, or -1 up, down, left, and right) 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 # Animations func enemy_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()函数中播放它们。我们再次为玩家角色做了同样的事情，所以这对您来说应该不会太难理解。从玩家代码中复制is_attacking变量。### Enemy.gd extends CharacterBody2D # Node refs @onready v…

让我们通过制作 RPG 来学习 Godot 4 — 第 9 部分：敌人 AI 设置
type: item
date: 2025-01-08T12:30:00+00:00
description: 如果没有某种威胁或敌人需要击败，游戏就不算完整。在我们的游戏中，我们将有一个仙人掌敌人，它会以恒定值在地图上生成，这意味着在游戏循环期间，地图上的敌人数量永远不会多于或少于 x。如果玩家触碰这个敌人，它会伤害我们的玩家，它也会向我们的玩家射击。让我们开始介绍我们的敌人 AI。您将在本部分中学习到的内容：· 如何向不可控节点添加运动。· 如何使用计时器节点。· 如何使用 RandomNumberGenerator 类。· 如何使节点随机移动，以及如何向其他节点移动。图 13：敌人概况敌人场景设置我们的敌人场景将具有与玩家场景相同的结构，以 CharacterBody2D 作为其根节点，后跟 AnimatedSprite2D 节点和 CollisionShape2D 节点。因此，我们可以继续复制我们的玩家场景并将其重命名为“敌人”或“仙人掌”。我只打算用仙人掌作为敌人，所以我将我的仙人掌称为通用术语“敌人”，但如果您要有多个不同的敌人，您可以为“仙人掌”、“匪徒”、“风滚草”等创建一个场景。将场景根重命名为您所需的任何名称（例如敌人），然后将玩家脚本从场景中分离出来。您还应该删除 Camera2D 节点，因为我们不会跟踪此节点，并且断开on_animation_finished()信号与 AnimatedSprite2D 节点的连接。您的最终场景应如下图所示。必须不断监视此敌人场景以更新其行为。例如，假设我们希望它漫游 1 分钟，1 分钟后，我们希望它停止 30 秒，然后重新定向并再次漫游。我们可以使用Timer节点，该节点将在倒计时指定间隔直至达到 0 后发出其内置的timeout() 信号。因此，每次计时器超时，敌人都应该重新定向并再次漫游。您的计时器有两个选项：一次性（如果为 true，计时器将在到达 0 时停止。如果为 false，它将重新启动）和自动启动（如果为 true，计时器将在进入场景树时自动启动）。我们希望此计时器在游戏开始时立即启动，因为我们想使用它在一定时间后更新敌人的动作。因此，您需要在 Timer 节点的 Inspector 面板中启用 Autostart 属性。稍后，当我们为敌人的漫游添加功能时，我们将回到此计时器节点。现在，让我们像为玩家设置动画一样设置敌人的动画。我们已经为敌人设置了所有动画名称，因为他们将能够完全按照玩家所做的那样做，我们只需切换动画帧即可。为此，您需要删除动画中现有的精灵帧。对所有动画执行此操作，但不要删除任何动画。让我们从 attack_down 动画开始。选择“从精灵表添加新精灵帧”选项，然后在 Assets > Mobs > Cactus 目录中，您将找到所有敌人的精灵表。对于我们的 attack_down 动画，我们将使用“Cactus Front Sheet.png”表来创建动画。我们水平计算 11 帧，垂直计算 4 帧，因此请相应地更改您的数字以正确裁剪帧。对于向下攻击，我们将使用第三行的帧。我挑战你现在使用下表作为指南，自行添加其余动画。调动敌人现在我们希望能够自主移动敌人。为此，我们需要创建一些变量来存储敌人的方向，以及经过一段随机时间后的新方向。我们不会将其设置为等待 1 分钟后再重定向的固定时间，而是将该值随机化。我们还希望我们的敌人在与物体碰撞后重定向。让我们将一个新脚本附加到敌人场景并将其保存在脚本文件夹下。我们的敌人的移动方式与玩家的移动方式类似，因此让我们从 Player.gd 脚本中添加一些熟悉的变量来捕捉其移动速度和新方向。我们还需要创建一个变量来存储其当前方向。### Enemy.gd extends CharacterBody2D # Enemy movement speed @export var speed = 50 #it’s the current movement direction of the cactus enemy. var direction : Vector2 #direction and animation to be updated throughout game state var new_direction = Vector2(0,1) #only move one spaces当计时器在随机倒计时结束后用完时，我们的方向就会改变。我们将使用RandomNumberGenerator类生成此随机倒计时值。顾名思义，这是一个用于生成伪随机数的类。new() 方法用于从类创建对象。### Enemy.gd # older code # RandomNumberGenerator to generate timer countdown value var rng = RandomNumberGenerator.new()…

让我们通过制作 RPG 来学习 Godot 4 — 第 8 部分：添加弹药拾取和消耗品
type: item
date: 2025-01-06T14:42:00+00:00
description: 如果您已成功完成本系列教程的这一部分，那么您应该已经设置好了基本地图、可移动角色和游戏 GUI。在本节中，我们将重点介绍如何将拾取物添加到我们的游戏中。这些拾取物将包括健康和耐力消耗品以及弹药。当玩家拾取这些物品时，我们还将更新我们的 GUI 值。您将在本部分中学习到的内容：· 如何使用枚举。· 如何使用@tool对象。· 如何在编辑器中执行代码。· 如何通过内存队列从场景中删除节点。· 如何使用 Area2D 检测体。· 如何引用 TileMap 属性。拾取物品设置我们不会为每种消耗品创建单独的拾取物品（即弹药拾取、体力拾取和健康拾取的场景），而是为我们的拾取物品创建一个单一的基础场景，以便在整个游戏中重复使用。我们将通过枚举和导出资源来实现这一点。在您的项目中，创建一个以Area2D节点为根的新场景。Area2D 节点用作检测碰撞的区域，换句话说，我们将使用此区域来检测我们的 Player 场景是否通过碰撞进入其主体。如果玩家已经撞到了 Pickup 场景的碰撞主体，则应移除 Pickup 并更新 UI 值。将此 Area2D 节点重命名为“Pickup”，并将场景保存在 Scenes 文件夹下。您会注意到该节点旁边有一个警告，因为就像我们的 CharacterBody2D 节点一样，它需要形状或碰撞。让我们继续向其添加 CollisionShape2D 节点来解决此警告。在选择碰撞形状之前，我们先给它一个 Sprite，这样我们就可以在给它一个碰撞边界之前看到它是什么图像。添加一个 Sprite2D 节点并从 Assets/Icons 文件夹中为其分配任何图标。现在，让我们为 CollisionShape2D 节点分配一个形状。将其设为新的 RectangleShape2D，然后拖动碰撞形状的边界以框住您的图标。此 Pickup 场景将作为我们所有拾取物的基础场景。从这里我们希望能够动态选择拾取物类型，并在玩家跑过拾取物时将其添加到玩家的库存中。为此，我们需要将脚本附加到 Pickup 场景的根节点。将其保存在您的 Scripts 文件夹下。在脚本顶部，让我们创建一个枚举，包含所有拾取物品（弹药、生命值和耐力饮料）。我们将导出此枚举，以便我们可以从检查器面板/编辑器中动态选择要放置的拾取物品类型。枚举是常量的简写，如果您想将连续的整数分配给某个常量，枚举非常有用。要创建枚举，我们使用关键字 enum，后跟括号中的常量。### Pickup.gd extends Area2D # Pickups to choose from enum Pickups { AMMO, STAMINA, HEALTH } @export var item : Pickups现在，如果我们查看检查器面板，我们可以从下拉菜单中选择 Pickup 项。接下来，我们还要选择它的图标或纹理，因为并非所有拾取物品看起来都像钻石！为此，我们将使用一些新东西，以便我们可以在编辑器中看到这些纹理的实时变化。我们将使用@tool，这是一行功能强大的代码，当将其添加到脚本顶部时，它会在编辑器中执行。您还可以决定脚本的哪些部分在编辑器中执行，哪些部分在游戏中执行，哪些部分在两者中执行。您需要在节点类扩展之前将@tool添加到脚本的顶部。### Pickup.gd @tool extends Area2D # Pickups to choose from enum Pickups { AMMO, STAMINA, HEALTH } @export var item : Pickups然后我们需要分配纹理，因此对于每个 Pickups，我们将预加载纹理。预加载是指在场景播放/运行之前加载资源。我们预加载资源以加快加载速度并减少冻结，因为资源已经加载。让我们将用于 GUI 图标的纹理分配给每个 Pickups 项目。### Pickup.gd @tool extends Area2D # Pickups to choose from enum Pickups { AMMO, STAMINA, HEALTH } @export var item : Pickups # Texture assets/resources var ammo_texture = preload("res://Assets/Icons/shard_01i.png") var stamina_texture = preload("res://Assets/Icons/potion_02b.png") var health_texture = preload("res://Assets/Icons/potion_02c.png")由于我们希望在分配新的 Pickups 时不断检查这些精灵帧是否发生变化，…

让我们通过制作 RPG 来学习 Godot 4 — 第 7 部分：设置游戏 GUI #3
type: item
date: 2025-01-04T11:47:00+00:00
description: 在前两部分中，我们为健康和体力值以及拾取物创建了 UI 元素。在 GUI 部分的最后一部分中，我们将创建显示玩家等级和 XP 值所需的元素。您将在本部分中学习到的内容：· 如何更轻松地向场景中添加 UI 元素。· 如何定位标签。· 如何改变节点的字体和锚定。等级和 XP UI我们暂时不会添加任何代码来更新它，因为我们首先需要为其创建一个敌人。现在让我们继续创建这些元素。在您的 Player 场景中，让我们将一个新的 ColorRect 添加到我们的 UI 节点。将此节点重命名为 XP，并向其添加两个 Label 节点。将第二个 Label 节点重命名为“Value”。选择 XP 节点后，将其 Color 属性更改为 #3a39356a，并将其变换属性更改为以下内容：将两个标签节点的字体更改为 ARCADECLASSIC，并将字体大小更改为 15。将标签的文本更改为“XP：”，将值的文本更改为“0/100”。然后，将 Label 节点的锚点预设更改为居中偏左。将其属性更改为以下内容：对 Value 节点执行相同操作，但将其更改为居中偏右。将其属性更改为以下内容：复制您的 XP 节点及其所有子节点并将其重命名为“Level”。将 Level 节点的变换属性更改为以下内容：最后，将标签的文本更改为“Level:”，将值的文本更改为“1”。你的结果应该如下所示：这就是这个游戏 GUI 的全部内容。我知道这是一个很长的三部分，但在下一部分中，我们至少可以在创建弹药和消耗品拾取物时做一些更有趣的事情！请记住保存并备份您的游戏项目，我们将在下一部分中与您见面。本节代码下载。