第1部分 - 玩家场景
您将要制作的第一个场景定义了Player对象。创建单独的玩家场景的好处之一是,您可以独立测试它,甚至在您创建游戏的其他部分之前。随着项目规模和复杂性的增加,游戏对象的这种分离将变得越来越有用。保持单个游戏对象彼此分离使得它们更容易进行故障排除,修改甚至完全替换,而不会影响游戏的其他部分。它还可以使您的Player重复使用 - 您可以将Player场景放入完全不同的游戏中,它也可以正常工作。
Player场景将显示您的角色及其动画,通过相应地移动角色来响应用户输入,并检测与游戏中其他对象的碰撞。
创建场景
首先单击Add/Create创建新节点按钮,然后选择Area2D。然后,单击其名称并将其更改为Player。单击Sence|Save Sence以保存场景。这是场景的root根节点或top-level顶级节点。您可以通过向此节点添加子项来向Player添加更多功能:
在添加任何子项之前,最好确保不要通过单击它们来移动或调整它们的大小。选择Player节点,然后单击旁边的锁图标:
工具提示将说明确保对象的子项不可选,如上面的屏幕截图所示。
提示
在创建新场景时始终执行此操作是个好主意。如果主体的碰撞形状或子画面变得偏移或缩放,则可能导致意外错误并且难以修复。使用此选项,节点及其所有子节点将始终一起移动。
精灵动画
使用Area2D,您可以检测其他对象何时重叠或运行到player中,但Area2D本身没有外观,因此单击Player节点并将AnimatedSprite节点添加为子节点。 AnimatedSprite将处理player的外观和动画。请注意,节点旁边有一个警告符号。 AnimatedSprite 需要一个SpriteFrames资源,该资源包含它可以显示的动画。要创建一个,请在Inspector中找到Frames属性,然后单击
接下来,在同一位置,点击
左侧是动画列表。单击默认值并将其重命名为run。然后,单击Add按钮,创建第二个名为idle的动画,第三个名为hurt。
在左侧的FileSystem停靠栏中,找到run(跑),idle(待机)和hurt(受伤)的玩家图像并将其拖动到相应的动画中:
每个动画的默认速度设置为每秒5帧。这有点太慢,因此请单击每个动画并将Speed(FPS)设置为8。在Inspector中,选中Playing属性旁边的On,然后选择Animation以查看动画中的动画:
稍后,您将编写代码以在这些动画之间进行选择,具体取决于玩家正在做什么。但首先,您需要完成设置player的节点。
碰撞形状
当使用Area2D或Godot中的其他碰撞对象之一时,它需要定义一个形状。碰撞形状定义对象占据的区域,并用于检测重叠或碰撞。形状由Shape2D定义,包括矩形,圆形,多边形和其他类型的形状。
为方便起见,当您需要向区域或物理主体添加形状时,可以将CollisionShape2D添加为子项。然后,您可以选择所需的形状类型,并可以在编辑器中编辑其大小。
添加CollisionShape2D作为Player的子节点(确保不将其添加为AnimatedSprite的子节点)。这将允许您确定玩家的hitbox或碰撞区域的边界。在Inspector中,单击Shape旁边的< null >并选择New RectangleShape2D。调整形状的大小以覆盖精灵:
提示
小心不要缩放形状的轮廓!只能使用尺寸手柄(红圈)来调整形状!对于缩放的碰撞形状,碰撞将无法正常工作。
您可能已经注意到碰撞形状不是以精灵为中心。那是因为精灵本身不是垂直居中的。我们可以通过向AnimatedSprite添加一个小偏移来解决这个问题。单击节点并在Inspector中查找Offset属性。将其设置为( 0,-5 )。
完成后,您的Player场景应如下所示:
编写Player脚本
现在,您已准备好添加脚本。脚本允许您添加内置节点未提供的其他功能。单击Player节点,然后单击Add Script按钮:
在Script Settings窗口中,您可以保留默认设置。如果您记得保存场景(请参见上面的屏幕截图),脚本将自动命名以匹配场景的名称。单击Create,您将进入脚本窗口。您的脚本将包含一些默认注释和提示。您可以删除注释(以#开头的行)。请参阅以下代码段:
extends Area2D
# class member variables go here, for example:
# var a = 2
# var b = "textvar"
func _ready():
# Called every time the node is added to the scene.
# Initialization here
pass
#func _process(delta):
# # Called every frame. Delta is time since last frame.
# # Update game logic here.
# pass
每个脚本的第一行将描述它所附加的节点类型。接下来,您将定义类变量:
extends Area2D
export (int) var speed
var velocity = Vector2()
var screensize = Vector2(480, 720)
文档
Vector2
在speed变量上使用export关键字允许您在Inspector中设置其值,并让Inspector知道变量应包含哪种类型的数据。这对于您希望能够调整的值非常方便,就像调整节点的内置属性一样。单击Player节点并将Speed属性设置为350,如以下屏幕截图所示:
velocity将包含角色当前的移动速度和方向,screenize将用于设置玩家移动的极限(屏幕大小)。之后,游戏的主场景将设置此变量,但是现在您将手动设置它以便进行测试。
Player移动
接下来,您将使用_process()
函数来定义player将执行的操作。每个帧都会调用_process()
函数,因此您将使用它来更新您希望经常更改的游戏元素。你需要玩家做三件事:
- 监测键盘输入
- 沿指定方向移动
- 播放相应的动画
首先,您需要监测输入。对于这个游戏,你有四个方向输入来监测(四个箭头键)。输入操作在Project Settings | Input Map选项卡下的项目设置中定义。在此选项卡中,您可以定义自定义事件并为其分配不同的键,鼠标操作或其他输入。默认情况下,Godot将事件分配给键盘箭头,因此您可以将它们用于此项目。
您可以使用Input.is_action_pressed()
检测是否按下了输入,如果按住键则返回true,否则返回false。结合所有四个按钮的状态将为您提供合成的移动方向。例如,如果同时right和down按住,则生成的速度矢量将为(1,1)。在这种情况下,由于我们一起添加水平和垂直移动,因此玩家移动的速度比他们刚刚水平移动的速度快。
您可以通过标准化速度来防止这种情况,这意味着将其长度设置为1,然后将其乘以所需的速度:
func get_input():
velocity = Vector2()
if Input.is_action_pressed("ui_left"):
velocity.x -= 1
if Input.is_action_pressed("ui_right"):
velocity.x += 1
if Input.is_action_pressed("ui_up"):
velocity.y -= 1
if Input.is_action_pressed("ui_down"):
velocity.y += 1
if velocity.length() > 0:
velocity = velocity.normalized() * speed
文档
Input
Input is_action_pressed
Vector2 normalized()
通过在get_input()
函数中将所有这些代码组合在一起,您可以更轻松地在以后更改内容。例如,您可以决定更改为模拟操纵杆或其他类型的控制器。从_process()
调用此函数,然后通过生成的velocity更改player的position。为防止player离开屏幕,您可以使用clamp()
函数将位置限制为最小值和最大值:
func _process(delta):
get_input()
position += velocity * delta
position.x = clamp(position.x, 0, screensize.x)
position.y = clamp(position.y, 0, screensize.y)
文档
clamp
Node._process()
单击运行Edited Scene(F6)并确认您可以在所有方向上围绕屏幕移动player。
关于delta
_process()
函数包含一个名为delta的参数,然后乘以速度。什么是delta?
游戏引擎尝试以每秒60帧的速度运行。但是,由于计算机速度降低,无论是在godot还是从计算机本身,这都会发生变化。如果帧速率不一致,则会影响游戏对象的移动。例如,考虑将对象设置为每帧移动10个像素。如果一切顺利进行,这将转化为在一秒钟内移动600像素。但是,如果这些帧中的某些帧需要更长时间,那么在那一秒中可能只有50帧,因此对象仅移动了500个像素。
和大多数游戏引擎和框架一样,Godot通过传递delta来解决这个问题,delta是自上一帧以来经过的时间。大多数情况下,这将是大约0.016秒(或大约16毫秒)。如果你然后采取你想要的速度(600 px / s)并乘以delta,你将获得正好10的运动。但是,如果增量增加到0.3,则对象将移动18个像素。总的来说,移动速度保持一致并且与帧速率无关。
作为附带好处,您可以以px/s为单位表示移动,而不是px/frame,这更容易可视化。
动画选择
现在玩家可以移动,你需要根据它是移动还是静止来改变AnimatedSprite正在播放的动画。run动画面向右侧,这意味着它向左移动(使用Flip H属性)应该水平翻转。将其添加到_process()
函数的末尾:
if velocity.length() > 0:
$AnimatedSprite.animation = "run"
$AnimatedSprite.flip_h = velocity.x < 0
else:
$AnimatedSprite.animation = "idle"
请注意,此代码有一点缩减。* flip_h是一个布尔属性,这意味着它可以是true或false。布尔值也是像<*的比较结果。因此,我们可以将属性设置为等于比较结果。这一行相当于写出来像这样:
if velocity.x < 0:
$AnimatedSprite.flip_h = true
else:
$AnimatedSprite.flip_h = false
再次启动场景并检查动画在每种情况下是否正确。确保在AnimatedSprite中将Playing设置为On,以便播放动画。
文档
AnimatedSprite.flip_h
开始和结束玩家的运动
当游戏开始时,主场景将需要通知玩家游戏已经开始。添加start()
函数如下,主场景将用于设置玩家的起始动画和位置:
func start(pos):
set_process(true)
position = pos
$AnimatedSprite.animation = "idle"
当玩家遇到障碍物或时间不足时,将调用die()
函数:
func die():
$AnimatedSprite.animation = "hurt"
set_process(false)
设置set_process(false)
会导致不再为此节点调用_process()
函数。这样,当玩家死亡时,他们仍然无法通过键输入来移动。
准备碰撞
玩家应该检测到它何时触碰硬币或障碍物,但是你还没有让它们这样做。没关系,因为你可以使用Godot的signal(信号)功能来使它工作。Signal是节点发送其他节点可以检测和响应的消息的一种方式。例如,当按下按钮时,许多节点都有内置信号来提醒您身体发生碰撞时。您还可以为自己的目的定义自定义信号。
通过将信号连接到您想要监听和响应的节点来使用信号。可以在Inspector或代码中进行此连接。在项目的后期,您将学习如何以两种方式连接信号。
将以下内容添加到脚本的顶部(在extends Area2D之后):
signal pickup
signal hurt
这些定义了玩家在触摸硬币或障碍物时会触发(发出)自定义信号。触摸将由Area2D本身检测到。选择Player节点,然后单击Inspector旁边的Node选项卡以查看player可以触发的信号列表:
请注意您的自定义信号也在那里。由于其他对象也将是Area2D节点,因此您需要area_entered()
信号。选择它并单击Connect。单击Connecting Signal窗口上的Connect - 您无需更改任何这些设置。 Godot会在你的脚本中自动创建一个名为_on_Player_area_entered()
的新函数。
提示
连接信号时,您也可以给出要将信号链接到现有函数的名称,而不是让Godot为您创建默认函数。如果您不希望Godot为您创建功能,请将Make Function开关切换为Off。
将以下代码添加到此新函数:
func _on_Player_area_entered( area ):
if area.is_in_group("coins"):
area.pickup()
emit_signal("pickup")
if area.is_in_group("obstacles"):
emit_signal("hurt")
die()
文档
Object.emit_signal()
当检测到另一个Area2D时,它将被传递给该函数(使用area变量)。硬币对象将具有pick()
功能,该功能定义拾取时硬币的行为(例如,播放动画或声音)。当您创建硬币和障碍物时,您将它们分配给相应的组,以便可以检测到它们。
总而言之,到目前为止,这是完整的player脚本
extends Area2D
signal pickup
signal hurt
export (int) var speed
var velocity = Vector2()
var screensize = Vector2(480, 720)
func get_input():
velocity = Vector2()
if Input.is_action_pressed("ui_left"):
velocity.x -= 1
if Input.is_action_pressed("ui_right"):
velocity.x += 1
if Input.is_action_pressed("ui_up"):
velocity.y -= 1
if Input.is_action_pressed("ui_down"):
velocity.y += 1
if velocity.length() > 0:
velocity = velocity.normalized() * speed
func _process(delta):
get_input()
position += velocity * delta
position.x = clamp(position.x, 0, screensize.x)
position.y = clamp(position.y, 0, screensize.y)
if velocity.length() > 0:
$AnimatedSprite.animation = "run"
$AnimatedSprite.flip_h = velocity.x < 0
else:
$AnimatedSprite.animation = "idle"
func start(pos):
set_process(true)
position = pos
$AnimatedSprite.animation = "idle"
func die():
$AnimatedSprite.animation = "hurt"
set_process(false)
func _on_Player_area_entered( area ):
if area.is_in_group("coins"):
area.pickup()
emit_signal("pickup")
if area.is_in_group("obstacles"):
emit_signal("hurt")
die()