第二章-Coin Dash

这第一个项目将指导你制作你的第一个Godot引擎项目。你将学习Godot编辑器如何工作,如何构建一个项目,以及如何构建一个小型2D游戏。

重要提示-即使您不是游戏开发的新手,也不要跳过本章。 尽管您可能已经了解了许多基本概念,但本项目将介绍您需要了解的一些基本Godot功能和设计范例。 在开发更复杂的项目时,将基于这些概念。

本章中的游戏称为Coin Dash。 您的角色必须在屏幕上四处移动,在争分夺秒的比赛中收集尽可能多的硬币。 完成后,游戏将如下所示:
在这里插入图片描述

Project setup

启动Godot并创建一个新项目,确保使用“创建文件夹”按钮以确保该项目的文件与其他项目分开保存。 您可以在这里https://github.com/PacktPublishing/Godot-Game-Engine-Projects/releases下载有关游戏的艺术和声音的Zip文件(统称为资产)。

将此文件解压到你的新项目文件夹中。

在这个项目中,您将制作三个独立的场景:Player,Coin和HUD,它们都将合并到游戏的Main场景中。 在较大的项目中,创建单独的文件夹来保存每个场景的资产和脚本可能很有用,但是对于这个相对较小的游戏,您可以将场景和脚本保存在根文件夹中,该根文件夹称为res://( res是资源的缩写)。 项目中的所有资源都将相对于res://文件夹放置。 您可以在左上角的FileSystem停靠栏中看到您的项目文件夹:
在这里插入图片描述

例如,硬币的图片将位于res://assets/coin/。

这个游戏将使用竖屏模式,所以你需要调整游戏窗口的大小。点击项目菜单,选择项目设置,如下图所示:
在这里插入图片描述

查找“显示/窗口”部分,并将“宽度”设置为480,将“高度”设置为720。同样在此部分,将“拉伸/模式”设置为2D,将“长宽比”保持不变。 这将确保,如果用户调整游戏窗口的大小,则所有内容都将适当缩放,并且不会拉伸或变形。 如果愿意,还可以取消选中“可调整大小”复选框,以防止完全调整窗口的大小。

向量和2D坐标系

注意:本节是2D坐标系的非常简短的概述,并且不会非常深入地研究矢量数学。 本文旨在对这些主题如何应用于Godot中的游戏开发做一个高层次的概述。向量数学是游戏开发中必不可少的工具,因此,如果您需要对该主题有更广泛的了解,请参阅可汗学院的线性代数系列(https://www.khanacademy.org/math/linear-algebra)。

在2D模式下工作时,您将使用直角坐标来标识空间中的位置。 2D空间中的特定位置被写为一对值,例如(4,3),分别代表沿x和y轴的位置。 以此方式可以描述2D平面中的任何位置。

在2D空间中,Godot遵循将x轴指向右侧,将y轴指向下方的通用计算机图形惯例:
在这里插入图片描述

如果你是计算机图形学或游戏开发的新手,正y轴指向向下而不是向上,这可能看起来很奇怪,因为你可能在数学课上学到的不一样。然而,这种方向在计算机图形应用中非常常见。

Vectors

您也可以将位置(4,3)视为与(0,0)点或原点的偏移量。 想象一下从原点指向该点的箭头:
在这里插入图片描述

此箭头是向量。 它代表了大量有用的信息,包括该点的位置(4,3),其长度m和其与x轴的夹角θ。 总之,这是一个位置向量,换句话说,它描述了空间中的位置。 向量还可以表示运动,加速度或具有x和y分量的任何其他量。

在Godot中,向量(用于2D的Vector2或用于3D的Vector3)被广泛使用,并且在本书的项目构建过程中将使用它们。

Pixel rendering像素渲染

Godot中的向量坐标是浮点数,而不是整数。这意味着一个Vector2可以有一个小数值,比如(1.5,1.5)。由于对象不能以半数像素绘制,这对于像素艺术游戏来说,可能会造成视觉上的问题,因为你要确保纹理的所有像素都被绘制出来。

为了解决这个问题,请打开项目|项目设置,并在下面找到渲染/质量部分。
侧边栏并启用 “使用像素捕捉”,如下截图所示。
在这里插入图片描述

如果你在游戏中使用2D像素艺术,最好在开始项目时就启用这个设置。这个设置在3D游戏中没有效果。

第一部分–玩家场景

你要做的第一个场景定义了玩家对象。创建一个单独的玩家场景的好处之一是,你可以独立地测试它,甚至在你创建游戏的其他部分之前。随着你的项目规模和复杂度的增长,这种游戏对象的分离将变得越来越有用。保持单个游戏对象相互分离,使它们更容易排除故障,修改,甚至完全替换而不影响游戏的其他部分。这也使得你的玩家可以重复使用–你可以把玩家场景放到一个完全不同的游戏中去,而且它的工作原理是一样的。

玩家场景将显示你的角色及其动画,响应用户的输入,相应地移动角色,并检测与游戏中其他物体的碰撞。

创建场景

首先点击 "添加/创建新节点 "按钮,选择一个Area2D。然后,点击它的名字,并将其改为Player。点击Scene | Save Scene来保存场景。这是场景的根节点或顶层节点。您将通过向这个节点添加子节点来为Player添加更多的功能:
在这里插入图片描述

在添加任何子代之前,最好确保你不会因为点击它们而意外地移动或调整它们的大小。选择Player节点,点击锁旁边的图标:
在这里插入图片描述

工具提示会说,确保对象的子对象是不可选择的,如上面的屏幕截图所示。

在创建一个新场景时,总是这样做是个好主意。如果一个物体的碰撞形状或精灵被偏移或缩放,它会导致意想不到的错误并且难以修复。使用此选项,节点及其所有子节点将始终一起移动。

精灵动画

使用Area2D,你可以检测到其他物体重叠或撞到玩家,但是Area2D自己没有一个外观,所以点击玩家节点并添加一个AnimatedSprite(动画精灵)节点作为子节点。动画精灵将为你的player处理外观和动画。注意,节点旁边有一个警告符号。一个动画精灵需要一个SpriteFrames资源,它包含了它可以显示的动画。要创建一个,在检查器中找到frame属性,然后点击 | New SpriteFrames:
在这里插入图片描述

接下来,在同样的位置,点击,打开SpriteFrames面板:
在这里插入图片描述

左边是动画的列表。点击默认的一个,并将其重命名为run。然后,单击 "添加 "按钮,创建第二个名为idle的动画和第三个名为hurt的动画。

在左侧的FileSystem停靠栏,找到run, idle,和hurt的玩家图像,并将其拖入相应的动画中:
在这里插入图片描述

每个动画的默认速度设置为每秒5帧。这有点太慢了,所以点击每个动画,将速度(FPS)设置为8。在 "检查器 "中,勾选 "播放 "属性旁边的 “开启”,然后选择一个动画来查看动画的运行情况:
在这里插入图片描述

稍后,你将根据玩家正在做的事情,编写代码在这些动画之间进行选择。但首先,你需要完成对玩家节点的设置。

碰撞形状

当使用Area2D或Godot中的其他碰撞对象之一时,它需要具有定义的形状,否则就无法检测到碰撞。 碰撞形状定义了对象占据的区域,并用于检测重叠和/或碰撞。 形状由Shape2D定义,包括矩形,圆形,多边形和其他类型的形状。
为了方便起见,当您需要向区域或物理物体添加形状时,可以将CollisionShape2D作为子级添加。 然后,选择所需的形状类型,然后可以在编辑器中编辑其大小。

添加一个CollisionShape2D作为Player的子级(确保你没有把它添加为AnimatedSprite的子级)。这将允许你确定玩家的hitbox,或其碰撞区域的边界。在 "检查器 "中,在 "形状 "旁边,点击,然后选择 “New RectangleShape2D”。调整形状的大小以覆盖精灵:
在这里插入图片描述

注意不要缩放形状的轮廓! 仅使用尺寸手柄(红色)来调整形状! 碰撞在缩放的碰撞形状下无法正常工作。

你可能已经注意到,碰撞形状并没有在精灵上居中。这是因为精灵本身没有垂直居中。我们可以通过给AnimatedSprite添加一个小的偏移来解决这个问题。点击节点,在检查器中寻找Offset属性。将其设置为(0,-5)。

当你完成后,你的Player场景应该是这样的:
在这里插入图片描述

编写Player脚本

现在,你已经准备好添加一个脚本了。脚本允许你添加内置节点没有提供的额外功能。点击Player节点,然后点击添加脚本按钮:

在这里插入图片描述

在 "脚本设置 "窗口中,你可以保持默认设置不变。如果你还记得保存场景(见前面的截图),脚本将自动被命名为与场景的名称相匹配。点击创建,你会被带到脚本窗口。你的脚本将包含一些默认的注释和提示。你可以删除注释(以#开头的行)。请参考以下代码片段。

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)

在speed变量上使用export关键字可以使您在Inspector中设置其值,并让Inspector知道该变量应包含的数据类型。 这对于您想要能够调整的值非常方便,就像调整节点的内置属性一样。 单击Player节点,并将Speed属性设置为350,如以下屏幕截图所示:

在这里插入图片描述

velocity将包含角色当前的移动速度和方向,screensize将用于设置玩家的移动限制。稍后,游戏的主场景将设置这个变量,但现在你将手动设置它,以便你可以测试。

移动Player

接下来,你将使用_process()函数来定义player将做什么。_process()函数在每一帧都会被调用,所以你会用它来更新你的游戏中那些你期望经常变化的元素。你需要玩家做三件事。

  • 检查键盘输入
  • 向指定方向移动
  • 播放相应的动画

首先,您需要检查输入。 对于此游戏,您需要检查四个方向输入(四个箭头键)。 输入动作在“输入映射”选项卡下的项目设置中定义。 在此选项卡中,您可以定义自定义事件并为它们分配不同的键,鼠标操作或其他输入。 默认情况下,Godot将事件分配给键盘箭头,因此您可以将其用于此项目。
您可以使用Input.is_action_pressed()检测是否按下了输入,如果按住该键,则返回true;否则,返回false。 组合所有四个按钮的状态将为您提供最终的移动方向。 例如,如果您同时按住向右和向下,则所得的速度矢量将为(1,1)。 在这种情况下,由于我们同时添加了水平和垂直移动,因此player的移动速度要比水平移动的速度快。

你可以通过将速度归一化来防止这种情况发生,也就是将它的长度设为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

通过在get_input()函数中将所有这些代码分组在一起,可以使以后更轻松地进行更改。 例如,您可以决定更改为模拟操纵杆或其他类型的控制器。 从_process()调用此函数,然后通过生成的速度更改玩家的位置。 为了防止player离开屏幕,可以使用clip()函数将位置限制为最小值和最大值:

func _process(delta): 
	get_input()
	position += velocity * delta
	position.x = clamp(position.x, 0, screensize.x) 
	position.y = clamp(position.y, 0, screensize.y)

单击“播放编辑的场景”(F6),并确认您可以沿屏幕的所有方向移动player。

关于delta

_process()函数包含一个名为delta的参数,然后乘以速度。什么是delta?

游戏引擎尝试以一致的每秒60帧的速度运行。然而,这可能会由于计算机速度变慢而改变,无论是在Godot还是从计算机本身。如果帧速率不一致,那么它将影响游戏对象的移动。例如,假设一个对象每帧移动10个像素。如果一切运行顺利,这将转化为在一秒内移动600像素。但是,如果其中一些帧需要更长的时间,那么在那一秒中可能只有50帧,所以对象只移动了500像素。

Godot,像大多数游戏引擎和框架一样,通过传递给你delta来解决这个问题,delta是指自上一帧以来的经过时间。大多数情况下,这将是0.016秒左右(或16毫秒左右)。如果你把你想要的速度(600 px/s)乘以delta,你将得到一个正好是10的运动。然而,如果将delta增加到0.3,那么对象将移动18像素。总的来说,移动速度保持一致,并且不受帧率的影响。

还有一个好处是,你可以用px/s而不是px/frame的单位来表达你的运动,这样更容易可视化。

选择动画

现在播放器可以移动了,您需要根据AnimatedSprite是移动还是静止来更改AnimatedSprite正在播放哪个动画。 运行动画的图面朝右,这意味着应将其水平翻转(使用Flip H属性)以向左移动。 将此添加到_process()函数的末尾:

if velocity.length() > 0:
	$AnimatedSprite.animation = "run"
	$AnimatedSprite.flip_h = velocity.x < 0 
else:
	$AnimatedSprite.animation = "idle"

请注意,这段代码走了一个小捷径,flip_h是一个布尔属性,这意味着它可以是真或假。布尔值也是像<这样比较的结果,正因为如此,我们可以将属性设置为等于比较的结果。这一行就相当于这样写出来:

if velocity.x < 0:
	$AnimatedSprite.flip_h = true else:
	$AnimatedSprite.flip_h = false

再次播放场景,并检查每种情况下的动画是否正确。确保AnimatedSprite中的Playing设置为On,这样动画就会播放。

开始和结束玩家的移动

当游戏开始时,主场景需要通知玩家游戏已经开始。添加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的信号功能来实现它。信号是节点发出信息的一种方式,其他节点可以检测到并做出反应。许多节点都有内置的信号,例如,当一个物体碰撞时,或者当一个按钮被按下时,会向你发出警报。你也可以为自己的目的定义自定义信号。

通过将信号连接到您要侦听和响应的节点来使用信号。 可以在检查器或代码中建立此连接。 在项目的后面,您将学习如何以两种方式连接信号。

在脚本的顶部添加以下内容(在扩展Area2D后):

signal pickup 
signal hurt

这些定义了自定义信号,当你的玩家触摸硬币或障碍物时,他们会发出(发送)信号。触摸会被Area2D本身检测到。选择 "Player "节点,点击 "检查器 "旁边的 "节点 "标签,可以看到player可以发出的信号列表:
在这里插入图片描述

注意你的自定义信号也在那里。由于其他对象也将是Area2D节点,所以你需要area_entered()信号。选择它,然后单击 “连接”。在连接信号窗口中点击连接–你不需要改变任何这些设置。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()

当检测到另一个Area2D时,它将被传递到函数中(使用area变量)。 硬币对象将具有Pickup()函数,该函数定义拾取后硬币的行为(例如播放动画或声音)。 创建硬币和障碍物时,将它们分配给适当的组,以便可以检测到它们。

总结一下,这是目前完整的玩家脚本:

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

第2部分-硬币场景

在这部分,你将制作硬币供玩家收集。这将是一个单独的场景,描述单个硬币的所有属性和行为。一旦保存,主场景将加载硬币场景并创建多个实例(即副本)。

节点设置

点击场景|新建场景,然后添加以下节点。不要忘记设置子节点不被选中,就像你在Player场景中做的那样。

  • Area2D (named Coin)
  • AnimatedSprite
  • CollisionShape2D

添加节点后一定要保存场景。

像在player场景中那样设置AnimatedSprite。这一次,你只有一个动画:一个闪亮/闪光效果,使硬币看起来不那么平坦和无聊。添加所有的帧,并将速度(FPS)设置为12。图片有点大,所以将AnimatedSprite的Scale设置为(0.5,0.5)。在CollisionShape2D中,使用CircleShape2D,并将其大小覆盖硬币图像。不要忘记:在确定碰撞形状的大小时,千万不要使用比例手柄。圆圈形状有一个手柄,可以调整圆圈的半径。

使用组

组为节点提供了标记系统,使您可以识别相似的节点。 一个节点可以属于任意数量的组。 您需要确保所有硬币都在一个名为“硬币”的组中,以便player脚本对触摸硬币做出正确反应。 选择“硬币”节点,然后单击“节点”选项卡(找到信号的相同选项卡),然后选择“组”。 在框中输入硬币,然后单击添加,如以下屏幕截图所示:
在这里插入图片描述

脚本

接下来,在Coin节点上添加一个脚本。如果你在模板设置中选择 “空”,Godot会创建一个没有任何注释或建议的空脚本。币的脚本的代码比玩家的代码短得多:

extends Area2D

func pickup(): 
	queue_free()

Pickup()函数由Player脚本调用,并告诉硬币被收集后该怎么做。 queue_free()是Godot的节点删除方法。 它安全地从树中删除该节点,并将其及其所有子节点从内存中删除。 稍后,您将在此处添加视觉效果,但是到目前为止,消失的硬币已经足够了。

queue_free()不会立即删除该对象,而是将其添加到要在当前帧末尾删除的队列中。 这比立即删除该节点更安全,因为游戏中运行的其他代码可能仍需要该节点存在。 通过等待直到帧结束,Godot可以确保可以访问该节点的所有代码均已完成,并且可以安全地删除该节点。

第三部分–主场景

主场景是将游戏的所有部分联系在一起的。它将管理玩家、硬币、计时器和游戏的其他部分。

节点设置

创建一个新场景,并添加一个名为Main的节点。要将Player添加到场景中,点击实例按钮并选择您保存的Player.tscn:
在这里插入图片描述

现在,添加以下节点作为Main的子节点,命名如下:

  • TextureRect (命名为Background)–用于背景图片。
  • Node(命名为CoinContainer)–用于存放所有硬币。
  • Position2D(命名为PlayerStart)–标记Player的起始位置。
  • Timer(命名为GameTimer)–用于跟踪时间限制。

确保Background是第一个子节点。 节点按所示顺序绘制,因此在这种情况下背景将位于Player后面。 通过将grass.png图像从资产文件夹拖动到Texture属性中,将图像添加到Background节点。

将拉伸模式更改为平铺,然后单击布局| Full Rect将帧调整为屏幕大小,如以下屏幕截图所示:
在这里插入图片描述

将PlayerStart节点的Position设置为(240,350)。

您的场景布局应如下所示:

在这里插入图片描述

主脚本

在Main节点中添加一个脚本(使用Empty模板)并添加以下变量:

extends Node

export (PackedScene) var Coin 
export (int) var playtime

var level 
var score
var time_left 
var screensize
var playing = false

现在,当您单击Main时,Coin和Playtime属性将显示在检查器中。 从“文件系统”面板中拖动Coin.tscn并将其放在Coin属性中。

将“playtime”设置为30(这是游戏将持续的时间)。 其余的变量将在以后的代码中使用。

初始化

接下来,添加_ready()函数:

func _ready(): 
	randomize()
	screensize = get_viewport().get_visible_rect().size
	$Player.screensize = screensize
	$Player.hide()

在GDScript中,可以使用$通过名称引用特定的节点。 这使您可以找到屏幕的大小并将其分配给Player的screensize变量。 hide()使玩家开始时不可见(您将使它们在游戏实际开始时显示)。

符 号 中 , 节 点 名 称 是 相 对 于 运 行 脚 本 的 节 点 而 言 的 。 例 如 , 符号中,节点名称是相对于运行脚本的节点而言的。例如, Node1/Node2指的是Node1的子节点(Node2),而Node1本身就是当前运行脚本的子节点。Godot的自动完成功能会在您输入时从树中推荐节点名称。请注意,如果节点的名称包含空格,您必须在它周围加上引号,例如,$“My Node”。

如果希望每次运行场景时“随机”数字的序列都不同,则必须使用randomize()。 从技术上讲,这为随机数生成器选择了一个随机种子。

开始新游戏

接下来,new_game()函数将初始化一个新游戏的一切。

func new_game(): 
	playing = true 
	level = 1
	score = 0
	time_left = playtime
	$Player.start($PlayerStart.position)
	$Player.show()
	$GameTimer.start() 
	spawn_coins()

除了将变量设置为其初始值之外,此函数还调用Player的start()函数以确保其移至正确的起始位置。 启动游戏计时器,它将倒计时游戏中的剩余时间。

您还需要一个函数,该函数将根据当前级别创建若干硬币:

func spawn_coins():
	for i in range(4 + level): 
		var c = Coin.instance()
		$CoinContainer.add_child(c) 
		c.screensize = screensize
		c.position = Vector2(rand_range(0, screensize.x), rand_range(0, screensize.y))

在这个函数中,你创建了许多Coin对象的实例(这次是在代码中,而不是通过点击Instance a Scene按钮),并将其添加为CoinContainer的子节点。每当你实例一个新的节点时,必须使用add_child()将其添加到树中。最后,你为硬币选择一个随机的位置。你会在每个关卡开始时调用这个函数,每次产生更多的硬币。

最终,你会希望new_game()在玩家点击开始按钮时被调用。现在,为了测试一切是否正常,将new_game()添加到你的_ready()函数的结尾,然后点击播放项目(F5)。当你被提示选择一个主场景时,选择Main.tscn。现在,每当你播放项目时,主场景就会被启动。

这时,你应该看到你的玩家和五个硬币出现在屏幕上。当玩家触摸到一枚硬币时,它就会消失。

检查剩余硬币

主脚本需要检测玩家是否捡到了所有的硬币。由于硬币都是CoinCointainer的子节点,你可以在这个节点上使用get_child_count()来找出还剩下多少硬币。把这个放在_process()函数中,这样每一帧都会被检查。

func _process(delta):
	if playing and $CoinContainer.get_child_count() == 0: 
		level += 1
		time_left += 5 
		spawn_coins()

如果没有更多的硬币剩余,那么玩家就会进入下一关。

第4部分–用户界面

你的游戏需要的最后一块是用户界面(UI)。这是一个用于显示玩家在游戏过程中需要看到的信息的界面,在游戏中,这也被称为抬头显示器(HUD),因为信息是以覆盖在游戏视图之上的方式出现的。在游戏中,这也被称为抬头显示器(HUD),因为这些信息是以叠加的形式出现在游戏视图之上的。你也会用这个场景来显示一个开始按钮。

HUD将显示以下信息:

  • 分数
  • 剩余时间
  • 一个信息,如游戏结束
  • 一个启动按钮

节点设置

创建一个新场景,并添加一个名为HUD的CanvasLayer节点。CanvasLayer节点允许你在游戏的其他部分之上的一层上绘制你的UI元素,这样它所显示的信息就不会被任何游戏元素(如玩家或硬币)所覆盖。

Godot提供了各种各样的UI元素,可以用来创建任何东西,从生命条等指标到库存等复杂界面。事实上,你用来制作这个游戏的Godot编辑器就是使用这些元素在Godot中构建的。UI元素的基本节点是从Control扩展而来的,并在节点列表中以绿色图标出现。为了创建你的UI,你将使用各种Control节点来定位、格式化和显示信息。下面是HUD完成后的样子。

在这里插入图片描述

锚和边距

控制节点有一个位置和大小,但它们也有称为锚和边距的属性。锚定义了节点的边缘相对于父容器的原点或参考点。边距表示从控制节点的边缘到其相应锚点的距离。当您移动或调整控制节点的大小时,边距会自动更新。

信息标签

在场景中添加一个Label节点,并将其名称改为MessageLabel。这个标签将显示游戏的标题,以及游戏结束时的Game Over。这个标签应该在游戏屏幕上居中。你可以用鼠标拖动它,但为了精确地放置UI元素,你应该使用Anchor属性。

选择 “视图”|"显示助手 "来显示有助于查看锚点位置的图钉,然后单击 "布局 "菜单并选择 “HCenter Wide”。

在这里插入图片描述

现在,MessageLabel跨过屏幕的宽度并垂直居中。 检查器中的“文本”属性设置标签显示的文本。将其设置为 Coin Dash!,并将 Align 和 Valign 设置为 Center。

Label节点的默认字体非常小,所以下一步就是分配一个自定义字体。向下滚动到 "检查器 "中的 "自定义字体 "部分,选择 “新建动态字体”(New DynamicFont),如下截图所示:
在这里插入图片描述

现在,点击DynamicFont,您可以调整字体设置。从FileSystem dock中,拖动Kenney Bold.ttf字体,并将其放入字体数据属性中。设置大小为48,如下截图所示:
在这里插入图片描述

分数和时间显示

HUD的顶部将显示玩家的分数和剩余时间。这两个都将是Label节点,位于游戏屏幕的相对两侧。你将使用容器节点来管理它们的位置,而不是将它们分开放置。

容器

UI容器会自动安排其子控件节点(包括其他容器)的位置。您可以使用它们在元素周围添加padding,将它们居中,或将元素按行或列排列。每种类型的容器都有特殊的属性来控制它们如何安排其子节点。您可以在 "检查器 "的 "自定义常量 "部分看到这些属性。

UI容器会自动安排其子控件节点(包括其他容器)的位置。您可以使用它们在元素周围添加padding,将它们居中,或将元素按行或列排列。每种类型的容器都有特殊的属性来控制它们如何安排其子节点。您可以在 "检查器 "的 "自定义常量 "部分看到这些属性。

要管理分数和时间标签,在HUD中添加一个MarginContainer节点。使用 "布局 "菜单将锚点设置为顶部宽。在 "自定义常量 "部分,将 “右边距”、"顶部边距 "和 "左边距 "设置为10。这将增加一些padding,这样文字就不会贴着屏幕的边缘了。

由于分数和时间标签将使用与MessageLabel相同的字体设置,所以如果你复制它将节省时间。点击MessageLabel并按Ctrl + D(macOS上为Cmd + D)两次来创建两个重复的标签。将它们都拖拽到MarginContainer上,使其成为它的子节点。命名一个 ScoreLabel 和另一个 TimeLabel,并将两个标签的 Text 属性设置为 0。将 ScoreLabel 的 Align 设置为 Left,TimeLabel 的 Align 设置为 Right。

通过GDScript更新UI

将脚本添加到HUD节点。 例如,此脚本将在需要更改UI元素的属性时更新UI元素,例如,每当收集硬币时都会更新分数文本。 请参考以下代码:

extends CanvasLayer 
signal start_game

func update_score(value):
	$MarginContainer/ScoreLabel.text = str(value)

func update_timer(value):
	$MarginContainer/TimeLabel.txt = str(value)

主场景的脚本将调用这些函数来更新显示,每当值有变化时,就会更新。对于MessageLabel,你还需要一个定时器来使它在短暂的时间后消失。添加一个定时器节点,并将其名称改为MessageTimer。在 "检查器 "中,将其 "等待时间 "设置为2秒,并勾选 "One Shot "设置为 “On”。这样可以确保在启动时,定时器只运行一次,而不是重复运行。添加以下代码。

func show_message(text):
	$MessageLabel.text = text
	$MessageLabel.show()
	$MessageTimer.start()

在这个函数中,你可以显示消息并启动定时器。要隐藏消息,连接MessageTimer的timeout()信号,并添加这个:

func _on_MessageTimer_timeout():
	$MessageLabel.hide()

使用按钮

添加一个Button节点,并将其名称改为StartButton。这个按钮将在游戏开始前显示,当点击时,它将隐藏自己,并向主场景发送一个信号来启动游戏。将Text属性设置为Start,并像对MessageLabel那样改变自定义字体。在 "布局 "菜单中,选择 “中心底部”。这将使按钮位于屏幕的最底部,因此可以通过按向上箭头键或编辑页边距并将Top设置为-150,Bottom设置为-50来将其向上移动一点。

当一个按钮被点击时,会发出一个信号。在StartButton的节点标签中,连接pressed()信号:

func _on_StartButton_pressed():
	$StartButton.hide()
	$MessageLabel.hide() 
	emit_signal("start_game")

HUD发出start_game信号,通知Main该开始新游戏了。

游戏结束

你的UI的最后任务是对游戏结局做出反应:

func show_game_over(): 
	show_message("Game Over") 
	yield($MessageTimer, "timeout")
	$StartButton.show()
	$MessageLabel.text = "Coin Dash!"
	$MessageLabel.show()

在这个函数中,你需要Game Over消息显示两秒钟,然后消失,这就是show_message()的作用。然而,你也希望在消息消失后显示开始按钮。yield()函数暂停执行函数,直到给定节点(MessageTimer)发出给定信号(超时)。一旦接收到信号,函数就会继续,将你返回到初始状态,这样你就可以再次播放。

将HUD添加到Main

现在,你需要设置主场景和HUD之间的通信。在Main场景中添加一个HUD场景的实例。在Main场景中,连接GameTimer的timeout()信号并添加以下内容:

func _on_GameTimer_timeout(): 
	time_left -= 1
	$HUD.update_timer(time_left) 
	if time_left <= 0:
		game_over()

每当GameTimer超时(每秒钟),剩余时间就会减少。接下来,连接Player的pickup()和hurt()信号:

func _on_Player_pickup(): 
	score += 1
	$HUD.update_score(score)

func _on_Player_hurt(): 
	game_over()

当游戏结束时,需要发生几件事,所以添加以下功能:

func game_over(): 
	playing = false
	$GameTimer.stop()
	for coin in $CoinContainer.get_children(): 
		coin.queue_free()
	$HUD.show_game_over()
	$Player.die()

此函数暂停游戏,还循环遍历硬币并删除剩余的硬币,以及调用HUD的show_game_over()函数。

最后,StartButton需要激活new_game()函数。点击HUD实例,选择其new_game()信号。在信号连接对话框中,单击Make Function to Off,并在Method In Node字段中,键入new_game。这将把信号连接到现有的函数,而不是创建一个新的函数。请看下面的截图:
在这里插入图片描述

从_ready()函数中删除new_game()并将这两行添加到new_game()函数中:

$HUD.update_score(score)
$HUD.update_timer(time_left)

现在,你可以玩这个游戏了 确认所有的部分都能按预期工作:分数、倒计时、游戏结束和重新开始等。如果你发现有一个部分不工作,回去检查你创建它的步骤,以及它与游戏其他部分连接的步骤。

第5部分-完成

您已经创建了一个可运行的游戏,但是仍然可以使它变得更加令人兴奋。 游戏开发人员使用“juice”一词来描述使游戏感觉良好的事物。 果汁可以包括声音,视觉效果或其他任何可以增加玩家娱乐性的内容,而不必改变游戏性。

在本节中,您将添加一些小juice来完成游戏。

视觉效果

当您拾起硬币时,它们就会消失,这并不是很吸引人。 添加视觉效果会使收集大量硬币变得更加令人满意。

首先在Coin场景中添加一个Tween节点。

什么是补间?

tween是一种使用特定函数对一些值进行插值(逐渐改变)的方法(从一个起始值到一个结束值)。例如,你可以选择一个稳定地改变数值的函数,或者一个开始时很慢但速度逐渐加快的函数。Tweening有时也被称为缓动。

当在Godot中使用Tween节点时,你可以指定它来改变一个节点的一个或多个属性。在本例中,你将增加硬币的Scale,同时使用Modulate属性使其淡出。

在Coin的_ready()函数中添加这一行:

$Tween.interpolate_property($AnimatedSprite, 'scale',
							$AnimatedSprite.scale,
							$AnimatedSprite.scale * 3, 0.3,
							Tween.TRANS_QUAD,
							Tween.EASE_IN_OUT)

interpolate_property()函数使Tween改变一个节点的属性。有七个参数:

  • 要影响的节点
  • 需要更改的属性
  • 属性的初始值
  • 属性的最终值
  • 持续时间(以秒为单位)
  • 使用的功能
  • 方向

当玩家拿起硬币时,tween应该开始播放。替换pickup()函数中的queue_free():

func pickup(): 
	monitoring = false
	$Tween.start()

将监控设置为false,可以确保在tween动画中,如果玩家触摸到硬币,则不会发出area_enter()信号。

最后,当动画结束时,硬币应该被删除,所以连接Tween节点的tween_completed()信号:

func _on_Tween_tween_completed(object, key): 
	queue_free()

现在,当你运行游戏时,你应该看到硬币在捡到时越来越大。这是很好的,但是当同时应用于多个属性时,tweens会更加有效。你可以添加另一个interpolate_property(),这次是为了改变精灵的不透明度。这是通过改变调制属性来实现的,调制属性是一个Color对象,并将其alpha通道从1(不透明)改为0(透明)。请参考以下代码:

$Tween.interpolate_property($AnimatedSprite, 'modulate',Color(1, 1, 1, 1),Color(1, 1, 1, 0), 0.3,ween.TRANS_QUAD,Tween.EASE_IN_OUT)

声音

声音是游戏设计中最重要但经常被忽视的部分之一。好的声音设计可以为你的游戏增加大量的juice,而你只需付出很小的努力.声音可以给玩家反馈,将他们与角色的情感联系起来,甚至成为游戏玩法的一部分。

在这个游戏中,你要添加三种音效。在主场景中,添加三个AudioStreamPlayer节点,并将它们命名为CoinSound、LevelSound和EndSound。将音频文件夹中的每一个声音(你可以在FileSystem dock中的assets下找到它)拖到每个节点的相应Stream属性中。

要播放声音,请在其上调用play()函数。 将$ CoinSound.play()添加到_on_Player_pickup()函数,将$ EndSound.play()添加到game_over()函数,将$ LevelSound.play()添加到spawn_coins()函数。

提升能力

物品有很多可能会给玩家带来小小的优势或力量。 在本部分中,您将添加一个能量道具,使玩家在收集时获得少量时间加成。 偶尔会出现一小段时间,然后消失。

新的场景将与你已经创建的硬币场景非常相似,所以点击你的硬币场景,选择场景|另存为,并将其保存为Powerup.tscn。将根节点的名称改为Powerup,并通过点击清除脚本按钮删除脚本: 。你还应该断开 area_entered 信号(你以后会重新连接)。在 "组 "选项卡中,通过点击删除按钮(它看起来像一个垃圾桶)删除硬币组,并将其添加到一个名为Powerups的新组中代替。

在AnimatedSprite中,将硬币的图片改为powerup,你可以在res://assets/pow/文件夹中找到。

点击添加一个新的脚本,并复制Coin.gd脚本中的代码。将_on_Coin_area_entered的名称改为_on_Powerup_area_entered,再将area_entered信号连接到它。记住,这个函数名将由信号连接窗口自动选择。

接下来,添加一个名为Lifetime的Timer节点。这将限制对象在屏幕上停留的时间。将其等待时间设置为2,将One Shot和Autostart都设置为On。连接它的超时信号,这样就可以在时间段结束时将其删除:

func _on_Lifetime_timeout(): 
	queue_free()

现在,转到您的Main场景并添加另一个名为PowerupTimer的Timer节点。 将其“One Shot”属性设置为“开”。 您可以使用另一个AudioStreamPlayer添加音频文件夹中的Powerup.wav声音。

连接超时信号,并添加以下代码来生成Powerup:

func _on_PowerupTimer_timeout(): 
	var p = Powerup.instance() 
	add_child(p)
	p.screensize = screensize
	p.position = Vector2(rand_range(0, screensize.x),rand_range(0, screensize.y))

Powerup场景需要通过添加一个变量来链接,然后在Inspector中拖动场景到属性中,就像你之前对Coin场景所做的那样:

export (PackedScene) var Powerup

能量的出现应该是不可预知的,所以每当你开始一个新的关卡时,需要设置PowerupTimer的等待时间。在用 spawn_coins()产生新的硬币后,将其添加到 _process()函数中:

$PowerupTimer.wait_time = rand_range(5, 10)
$PowerupTimer.start()

现在,你将会有能量出现,最后一步就是当收集到一个加电时,给玩家一些奖励时间。目前,玩家脚本假设它碰到的任何东西不是硬币就是障碍物。修改Player.gd中的代码,检查被撞到的是哪种物体:

func _on_Player_area_entered( area ): 
	if area.is_in_group("coins"):
		area.pickup() 
		emit_signal("pickup", "coin")
	if area.is_in_group("powerups"): 
		area.pickup() 
		emit_signal("pickup", "powerup")
	if area.is_in_group("obstacles"): 
		emit_signal("hurt")
		die()

请注意,现在你发出的拾取信号带有一个额外的参数,命名对象的类型。现在,Main.gd中的相应函数可以被修改为接受该参数,并使用匹配语句来决定采取什么行动:

func _on_Player_pickup(type): 
	match type:
		"coin":
			score += 1
			$CoinSound.play()
			$HUD.update_score(score) 
		"powerup":
			time_left += 5
			$PowerupSound.play()
			$HUD.update_timer(time_left)

match语句是if语句的有用替代方法,尤其是当您要测试大量可能的值时。

尝试运行游戏并收集能量。 确保声音播放并且计时器增加五秒钟。

硬币动画

创建硬币场景时,添加了AnimatedSprite,但尚未播放。 硬币动画显示在硬币表面传播的闪光效果。 如果所有硬币同时显示,则看起来太规则了,因此每个硬币在动画中需要一个小的随机延迟。

首先,点击AnimatedSprite,然后点击Frames资源。确保Loop被设置为Off,Speed被设置为12。

在Coin场景中添加一个Timer节点,并在_ready()中添加这段代码:

$Timer.wait_time = rand_range(3, 8)
$Timer.start()

现在,连接定时器的timeout()信号,并添加这个:

func _on_Timer_timeout():
	$AnimatedSprite.frame = 0
	$AnimatedSprite.play()

试着运行游戏,观察硬币的动画。这是一个很好的视觉效果,只需要付出很小的努力。在专业游戏中,你会注意到很多这样的效果。虽然非常微妙,但视觉上的吸引力让人有更愉悦的体验。

前面的Powerup对象有一个类似的动画,你可以用同样的方式添加。

障碍物

最后,可以通过引入一个玩家必须避开的障碍物,让游戏更具挑战性。碰到障碍物就会结束游戏。

为仙人掌创建一个新场景,并添加以下节点。

  • Area2D (named Cactus)
  • Sprite
  • CollisionShape2D

将仙人掌纹理从FileSystem停靠点拖到Sprite的Texture属性。 将RectangleShape2D添加到碰撞形状并调整其大小,以使其覆盖图像。 还记得您是否在播放器脚本中添加了area.is_in_group(“ obstacless”)吗? 使用“节点”选项卡(“检查器”旁边)将仙人掌主体添加到障碍物组。

现在,将仙人掌实例添加到主场景中,并将其移动到屏幕上半部的某个位置(远离Player生成的位置)。 玩游戏,看看碰到仙人掌会发生什么。

您可能已经发现了一个问题:硬币会在仙人掌后面产生,因此无法捡起。 放置硬币后,如果发现硬币与障碍物重叠,则需要移动硬币。 连接硬币的area_entered()信号并添加以下内容:

func _on_Coin_area_entered( area ): 
	if area.is_in_group("obstacles"):
		position = Vector2(rand_range(0, screensize.x), rand_range(0, screensize.y))

如果添加了前面的Powerup对象,则需要对其area_entered信号执行相同的操作。

总结

在本章中,您通过创建一个基本的2D游戏来学习Godot Engine的基础知识。您设置了项目并创建了多个场景,使用精灵和动画,捕获用户输入,使用信号与事件通信,并使用控制节点创建了UI。你在这里学到的东西是重要的技能,你将在任何Godot项目中使用。

在进入下一章之前,先看一下这个项目。你明白每个节点在做什么吗?是否有任何你不理解的代码部分?如果有,请回过头来复习本章的那部分内容。

另外,可以自由地对游戏进行实验,改变一些东西。最好的方法之一是改变它们,看看会发生什么,从而很好地感受到游戏的不同部分正在做什么。

在下一章中,你将探索更多Godot的功能,并学习如何通过构建一个更复杂的游戏来使用更多的节点类型。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值