通过制作 2D 平台游戏学习 Godot 4 — 第 5 部分:关卡创建 #2
在上一部分中,我们为游戏的边界创建了基本碰撞和地形。我决定将 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
恭喜您创建了第一个关卡的平台!在下一部分中,我们将为关卡添加一些装饰,使其感觉更完整一些,例如带有几个窗口的背景。我们还将创建第二个关卡。稍后我们将用炸弹生成器、盒子生成器和拾取物品填充这些关卡 — 但现在,让我们专注于创建关卡的基础。
现在是保存项目并备份的好时机,这样如果出现任何破坏游戏的错误,您就可以恢复到此部分。在继续本系列之前,请回顾并复习您学到的知识,一旦您准备好,我们将在下一部分与您见面!