Game1硬币收集 - 第1部分 玩家场景 - Godot引擎游戏开发项目实践

第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属性,然后单击 |New SpriteFrames

点击 |New SpriteFrames

接下来,在同一位置,点击以打开SpriteFrames面板:

SpriteFrames面板

左侧是动画列表。单击默认值并将其重命名为run。然后,单击Add按钮,创建第二个名为idle的动画,第三个名为hurt

在左侧的FileSystem停靠栏中,找到run(跑),idle(待机)和hurt(受伤)的玩家图像并将其拖动到相应的动画中:

Animation Frames

每个动画的默认速度设置为每秒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脚本

现在,您已准备好添加脚本。脚本允许您添加内置节点未提供的其他功能。单击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,如以下屏幕截图所示:

设置Speed速度

velocity将包含角色当前的移动速度和方向,screenize将用于设置玩家移动的极限(屏幕大小)。之后,游戏的主场景将设置此变量,但是现在您将手动设置它以便进行测试。


Player移动

接下来,您将使用_process()函数来定义player将执行的操作。每个帧都会调用_process()函数,因此您将使用它来更新您希望经常更改的游戏元素。你需要玩家做三件事:

  • 监测键盘输入
  • 沿指定方向移动
  • 播放相应的动画

首先,您需要监测输入。对于这个游戏,你有四个方向输入来监测(四个箭头键)。输入操作在Project Settings | Input Map选项卡下的项目设置中定义。在此选项卡中,您可以定义自定义事件并为其分配不同的键,鼠标操作或其他输入。默认情况下,Godot将事件分配给键盘箭头,因此您可以将它们用于此项目。

您可以使用Input.is_action_pressed()检测是否按下了输入,如果按住键则返回true,否则返回false。结合所有四个按钮的状态将为您提供合成的移动方向。例如,如果同时rightdown按住,则生成的速度矢量将为(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是一个布尔属性,这意味着它可以是truefalse。布尔值也是像<*的比较结果。因此,我们可以将属性设置为等于比较结果。这一行相当于写出来像这样:

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()

你可能感兴趣的:(Game1硬币收集 - 第1部分 玩家场景 - Godot引擎游戏开发项目实践)