Godot4 你的第一个2d游戏

系列文章目录

Godot 4引擎调试以及游戏开发-前言


基本介绍

大致分为以下部分
宏观介绍,基础节点讲解,代码讲解,例程
本文可以看作是Godot Engine 4.2 简体中文文档文章的补充

一、介绍

你的第一个2d游戏大概运行后是这样。在这里插入图片描述
这个游戏叫做“Dodge the Creeps!”。你的角色必须尽可能长时间移动并避开敌人。
你将学到:

使用 Godot 编辑器创建完整的 2D 游戏。
搭建简单的游戏项目。
移动玩家角色并为其修改精灵。
生成随机敌人。
计分。

二、构建步骤

前期准备

官网下载完引擎,傻瓜式,打开就行。

为什么从 2D 开始?
官方是这样说的:若你是游戏开发新手或不熟悉 Godot,我们建议你从 2D 游戏开始。 这将使你在处理往往更复杂的 3D 游戏之前熟悉两者。但是真实原因是你必须先从2d开始,毕竟一个能运行的游戏是你开始的起点

2d游戏比3d游戏要简单,并且不推荐初学者用godot开发3d游戏

1.设置项目

在这个简短的第一部分中,我们将设置和组织项目。

启动 Godot 然后新建一个项目。
在这里插入图片描述
你的项目文件夹应如下所示。
在这里插入图片描述
直接从硬盘中粘贴或者导入都可以

这个游戏是针对竖屏模式设计的,所以我们需要调整游戏窗口的大小。点击项目 -> 项目设置打开项目设置窗口,然后在左栏中打开显示 -> 窗口选项卡,将“视口宽度”设置为 480,并将“视口高度”设置为 720。

在这里插入图片描述
另外,滚动到该小节的底部,在拉伸选项中,将模式设置为 canvas_items,将比例设置为 keep。这样就可以保证在不同大小的屏幕上,游戏都能够进行一致的比例缩放。

在Godot中,2D场景中的元素被称为Canvas Items。这可以包括精灵(Sprite)、标签(Label)、面板(Panel)、粒子效果等等。它们都是2D场景中的可渲染对象。

模式设置为 canvas_items:这表示将拉伸模式设置为"canvas_items",即当屏幕尺寸发生变化时,会根据Canvas Items的布局进行相应的调整,以保持画面的一致性。
比例设置为 keep:这表示保持画面的纵横比例不变。无论屏幕的宽高比如何变化,Canvas Items都将以一致的比例进行缩放,以确保在不同尺寸的屏幕上,画面看起来不会因拉伸而失真。
在这里插入图片描述
组织项目
在这个项目中,我们将制作 3 个独立的场景:Player、Mob 以及 HUD,我们将把这些场景合并成游戏的 Main 场景。

在更大的项目中,为各个场景及对应的脚本创建各自的文件夹会比较好。而这是一个相对小型的游戏,你可以把场景和脚本放在项目的根文件夹里,根文件夹用 res:// 表示。可以在左下角的“文件系统”面板中查看项目文件夹:

提示一下,可以直接右键复制资源的路径,这个后续会很有用

在这里插入图片描述
项目构建完成,接下来创建玩家场景。

2.创建玩家场景

项目设置到位后,我们可以开始处理玩家控制的角色。

第一个场景, 我们会定义 Player 对象. 单独创建Player场景的好处之一是, 在游戏的其他部分做出来之前, 我们就可以对其进行单独测试.

节点结构:首先,我们需要为玩家对象选择一个根节点。一般而言,场景的根节点应该反映对象所需的功能——对象是什么。单击“其他节点”按钮并将 Area2D 节点添加到场景中。
在这里插入图片描述
**节点是一切的核心!**所有的组件都会以节点树的形式表现,以后会持续使用。

Godot 将在场景树中的节点旁边显示警告图标。一个黄叹号,请暂时忽略。

使用 Area2D 可以检测到与玩家重叠或进入玩家内的物体. 通过双击节点名称将其名称更改为 Player. 我们已经设置好了场景的根节点, 现在可以向该角色中添加其他节点来增加功能.

保存场景。点击“场景 -> 保存”,或者在 Windows/Linux 平台上按下 Ctrl+S,在 macOS 上按下 Cmd+S

精灵动画(见后面代码分析)

点击 Player 节点并添加(在 Windows/Linux 中按Ctrl + A,或者在 macOS 中按 Cmd + A)一个 AnimatedSprite2D 节点作为子节点。AnimatedSprite2D 将处理我们玩家的外观和动画。请注意,节点旁边有一个警告符号。AnimatedSprite2D 需要一个 SpriteFrames 资源,该资源是一个可显示的动画列表。要创建它,在“检查器”的 Animation 选项卡中找到 Sprite Frames 属性,然后点击“[空]”->“新建 SpriteFrames”。再次点击来打开 SpriteFrames 面板:
在这里插入图片描述
左边是一个动画列表。点击“defalult”动画并将其重命名为“walk”。然后点击“Add Animation”按钮,创建另一个名为“up”的动画。在“FileSystem”选项卡中找到玩家图像——它们应该在你之前解压的 art 文件夹中。将每个动画的两张图像, playerGrey_up[1/2] 和 playerGrey_walk[1/2],拖到对应动画的面板的“Animation Frames”处:
在这里插入图片描述
玩家图像对于游戏窗口来说有点过大,需要缩小它们。点击 AnimatedSprite2D 节点,可以在检查器 Node2D 标签中,将 Scale 属性设置为 (0.5, 0.5) 。
在这里插入图片描述
最后,在 Player 下添加一个 CollisionShape2D 作为子节点,以确定玩家的“攻击框”,或者说碰撞范围。CapsuleShape2D 节点最适合这个角色,那么就在检查器中“Shape”的旁边点击“[空]”->“新建 CapsuleShape2D”添加形状,使用两个控制柄,调整形状大小以覆盖精灵:
在这里插入图片描述
完成后, 你的 Player 场景看起来应该像这样:
在这里插入图片描述

3.编写玩家代码

我们将添加玩家的动作、动画,并将其设置为检测碰撞。

现在我们需要添加一些内置节点所不具备的功能,因此要添加一个脚本。点击 Player 节点然后点击“附加脚本”按钮:
在这里插入图片描述
在脚本设置窗口中,你可以维持默认设置。点击“创建”即可:

在这里插入图片描述
首先声明该对象将需要的成员变量:

extends Area2D

@export var speed = 400 # How fast the player will move (pixels/sec).
var screen_size # Size of the game window.

在第一个变量 speed 上使用 export 关键字,这样我们就可以在“检查器”中设置其值。对于希望能够像节点的内置属性一样进行调整的值,这可能很方便。点击 Player 节点,你将看到该属性现在显示在“检查器”的“Script Variables”(脚本变量)部分。请记住,如果你在此处更改值,它将覆盖脚本中所写的值。

你的 player.gd 脚本应该已经包含一个 _ready() 和一个 _process() 函数。如果你没有选择上面展示的默认模板,请在学习本课程的同时创建这些函数。

当节点进入场景树时,_ready() 函数被调用,这是查看游戏窗口大小的好时机:

func _ready():
	screen_size = get_viewport_rect().size

使用代码的时候,要注意缩进

现在我们可以使用 _process() 函数定义玩家将执行的操作。_process() 在每一帧都被调用,因此我们将使用它来更新我们希望会经常变化的游戏元素。对于玩家而言,我们需要执行以下操作:

1 检查输入。

2 沿给定方向移动。

3 播放合适的动画。

首先,我们需要检查输入——玩家是否正在按键?对于这个游戏,我们有 4 个方向的输入要检查。输入动作在项目设置中的“输入映射”下定义。在这里,你可以定义自定义事件,并为其分配不同的按键、鼠标事件、或者其他输入。对于此游戏,我们将把方向键映射给四个方向。

点击项目 -> 项目设置打开项目设置窗口,然后单击顶部的输入映射选项卡。在顶部栏中键入“move_right”,然后单击“添加”按钮以添加该 move_right 动作。

在这里插入图片描述
我们需要为这个操作分配一个按键。单击右侧的“+”图标,打开事件管理器窗口。
在这里插入图片描述
会自动选中“监听输入…”区域。按下键盘上的“右方向”键,菜单应该像这样。
在这里插入图片描述
选择“确定”按钮。现在“右方向”键与 move_right 动作关联了。

重复这些步骤以再添加三个映射:

1 move_left 映射到左箭头键。
2 move_up 映射到向上箭头键。
3 move_down 映射到向下箭头键。

按键映射选项卡应该看起来类似这样:

你可以使用 Input.is_action_pressed() 来检测是否按下了键, 如果按下会返回 true, 否则返回 false .
在这里插入图片描述

func _process(delta):
	var velocity = Vector2.ZERO # The player's movement vector.
	if Input.is_action_pressed("move_right"):
		velocity.x += 1
	if Input.is_action_pressed("move_left"):
		velocity.x -= 1
	if Input.is_action_pressed("move_down"):
		velocity.y += 1
	if Input.is_action_pressed("move_up"):
		velocity.y -= 1

	if velocity.length() > 0:
		velocity = velocity.normalized() * speed
		$AnimatedSprite2D.play()
	else:
		$AnimatedSprite2D.stop()

我们首先将 velocity 设置为 (0, 0)——默认情况下玩家不应该移动。然后我们检查每个输入并从 velocity 中进行加/减以获得总方向。例如,如果你同时按住 右 和 下,则生成的 velocity 向量将为 (1, 1)。此时,由于我们同时向水平和垂直两个方向进行移动,玩家斜向移动的速度将会比水平移动要更快。

只要对速度进行归一化就可以防止这种情况,也就是将速度的长度设置为 1,然后乘以想要的速度。这样就不会有过快的斜向运动了。

我们还会检查玩家是否正在移动,以便在 AnimatedSprite2D 上调用 play() 或 stop() 。

在 GDScript 中, $ 返回从当前节点开始的相对路径上的节点,如果找不到该节点,则返回 null 。当前 AnimatedSprite2D 是该节点子节点,所以可以使用 $AnimatedSprite2D 以获取。

现在我们有了一个运动方向,我们可以更新玩家的位置了。我们也可以使用 clamp() 来防止它离开屏幕。 clamp 一个值意味着将其限制在给定范围内。将以下内容添加到 _process 函数的底部:

position += velocity * delta
position = position.clamp(Vector2.ZERO, screen_size)

选择动画
现在玩家可以移动了,我们需要根据方向更改 AnimatedSprite2D 所播放的动画。我们的“walk”动画显示的是玩家向右走。向左移动时就应该使用 flip_h 属性将这个动画进行水平翻转。我们还有向上的“up”动画,向下移动时就应该使用 flip_v 将其进行垂直翻转。让我们把这段代码放在 _process() 函数的末尾:

if velocity.x != 0:
	$AnimatedSprite2D.animation = "walk"
	$AnimatedSprite2D.flip_v = false
	# See the note below about boolean assignment.
	$AnimatedSprite2D.flip_h = velocity.x < 0
elif velocity.y != 0:
	$AnimatedSprite2D.animation = "up"
	$AnimatedSprite2D.flip_v = velocity.y > 0

当你确定移动正常工作时, 请将此行添加到 _ready() 中,在游戏开始时隐藏玩家:

hide()

准备碰撞
我们希望 Player 能够检测到何时被敌人击中, 但是我们还没有任何敌人!没关系, 因为我们将使用Godot的 信号 功能来使其正常工作.

在脚本顶部添加以下内容。如果你使用的是 GDScript,请将其添加到 extends Area2D 之后。如果你使用 C#,请将其添加到 public partial class Player : Area2D 之后:

signal hit

这定义了一个叫作“hit”的自定义信号,当玩家与敌人碰撞时,我们会让他发出这个信号。我们将使用 Area2D 来检测碰撞。选中 Player 节点,然后点击“检查器”选项卡旁边的“节点”选项卡,就可以查看玩家可以发出的信号列表:
在这里插入图片描述
请注意自定义的“hit”信号也在其中!由于敌人将是 RigidBody2D 节点,所以需要 body_entered(body: Node2D) 信号。当物体接触到玩家时就会发出这个信号。点击“连接…”就会出现“连接信号”窗口。

Godot 将直接在脚本中为你创建一个具有确切名称的函数。现在你不需要更改默认设置。
在这里插入图片描述
注意,绿色图标表示信号已连接到此函数;但这并不意味着该函数存在,只是信号将尝试连接到具有该名称的函数。因此请仔细检查该函数的拼写是否能完全匹配上!

接下来,将此代码添加到函数中:

func _on_body_entered(body):
	hide() # Player disappears after being hit.
	hit.emit()
	# Must be deferred as we can't change physics properties on a physics callback.
	$CollisionShape2D.set_deferred("disabled", true)

敌人每次击中 玩家时都会发出一个信号。我们需要禁用玩家的碰撞检测,确保我们不会多次触发 hit 信号。

最后再为玩家添加一个函数,用于在开始新游戏时调用来重置玩家。

func start(pos):
	position = pos
	show()
	$CollisionShape2D.disabled = false

在玩家部分的工作完成后,我们将在下一课中研究敌人。

4创建敌人

创建敌人
是时候去做一些玩家必须躲避的敌人了. 它们的行为很简单: 怪物将随机生成在屏幕的边缘, 沿着随机的方向直线移动.

我们将创建一个 Mob 的怪物场景,以便在游戏中独立实例化出任意数量的怪物。

节点设置
点击顶部菜单的“场景 -> 新建场景”,然后添加以下节点:

RigidBody2D(名为 Mob)

AnimatedSprite2D

CollisionShape2D

VisibleOnScreenNotifier2D

别忘了设置子项,使其无法被选中,就像你对 Player 场景所做的那样。

选择“Mob”节点,并在检视器(Inspector)中的“RigidBody2D”部分将其“Gravity Scale”属性设置为0。这将阻止“mob”节点向下掉落。

此外,在 RigidBody2D 部分下方的 CollisionObject2D 部分下,展开 Collision 分组并取消选中 Mask 属性内的 1。这将确保怪物不会相互碰撞。
在这里插入图片描述
像设置玩家一样设置 AnimatedSprite2D。这一次,我们有 3 个动画:fly、swim、walk,每个动画在 art 文件夹中都有两张图片。

必须为每个单独动画设置 动画速度 属性,将三个动画的对应动画速度值都调整为 3。

在这里插入图片描述
你可以使用 动画速度 输入区域右侧的“播放动画”按钮预览动画。

我们将随机选择其中一个动画,以便小怪有一些变化。

像玩家的图像一样,这些小怪的图像也要缩小。请将 AnimatedSprite2D 的 Scale 属性设为 (0.75, 0.75)。

像在 Player 场景中一样,为碰撞添加一个 CapsuleShape2D。为了使形状与图像对齐,你需要将 Rotation 属性设为 90(在“检查器”的“Transform”下)。

保存该场景。

敌人的脚本 通过之前的方法生成

extends RigidBody2D

现在让我们看一下脚本的其余部分。在 _ready() 中,我们从三个动画类型中随机选择一个播放

func _ready():
	var mob_types = $AnimatedSprite2D.sprite_frames.get_animation_names()
	$AnimatedSprite2D.play(mob_types[randi() % mob_types.size()])

首先,我们从 AnimatedSprite2D 的 sprite_frames 属性中获取动画名称的列表。返回的是一个数组,该数组包含三个动画名称:[“walk”, “swim”, “fly”]。

然后我们需要在 0 和 2 之间选取一个随机的数字, 以在列表中选择一个名称(数组索引以 0 起始). randi() % n 会在 0 and n-1 之中选择一个随机整数.

最后一步是让怪物在超出屏幕时删除自己。将 VisibleOnScreenNotifier2D 节点的 screen_exited() 信号连接到 Mob 上,然后添加如下代码:

func _on_visible_on_screen_notifier_2d_screen_exited():
	queue_free()

这样就完成了 Mob 场景。

玩家和敌人已经准备就绪,接下来,我们将在一个新的场景中把他们放到一起。我们将使敌人在游戏板上随机生成并前进,我们的项目将变成一个能玩的游戏。

5游戏主场景

现在是时候将我们所做的一切整合到一个可玩的游戏场景中了!

创建新场景并添加一个 Node 节点,命名为 Main。(我们之所以使用 Node 而不是 Node2D,是因为这个节点会作为处理游戏逻辑的容器使用。本身是不需要 2D 功能的。)

点击实例化按钮(由链条图标表示)并选择保存的 player.tscn。
在这里插入图片描述

现在, 将以下节点添加为 Main 的子节点, 并按如下所示对其进行命名(值以秒为单位):

Timer(名为 MobTimer)——控制怪物产生的频率

Timer(名为 ScoreTimer)——每秒增加分数

Timer(名为 StartTimer)——在开始之前给出延迟

Marker2D(名为 StartPosition)——表示玩家的起始位置

如下设置每个 Timer 节点的 Wait Time 属性:

MobTimer:0.5

ScoreTimer:1

StartTimer:2

此外,将 StartTimer 的 One Shot 属性设置为“启用”,并将 StartPosition 节点的 Position 设置为 (240, 450)。

生成怪物
Main 节点将产生新的生物, 我们希望它们出现在屏幕边缘的随机位置. 添加一个名为 MobPath 的 Path2D 节点作为 Main 的子级. 当你选择 Path2D 时, 你将在编辑器顶部看到一些新按钮:
选择添加点按钮,并单击以添加拐角点来绘制路径。可使用网格捕捉和用智能捕捉,使点对齐到网格。
在这里插入图片描述
在这里插入图片描述
以顺时针的顺序绘制路径,否则小怪会向外而非向内生成!

在这里插入图片描述
在图像上放置点 4 后, 点击 闭合曲线 按钮, 你的曲线将完成.

现在已经定义了路径, 添加一个 PathFollow2D 节点作为 MobPath 的子节点, 并将其命名为 MobSpawnLocation. 该节点在移动时, 将自动旋转并沿着该路径, 因此我们可以使用它沿路径来选择随机位置和方向.

你的场景应如下所示:
在这里插入图片描述
Main 脚本
将脚本添加到 Main。在脚本的顶部,我们使用 @export var mob_scene: PackedScene 来允许我们选择要实例化的 Mob 场景。

extends Node

@export var mob_scene: PackedScene
var score

单击 Main 节点,就可以在“检查器”的“Script Variables”(脚本变量)下看到 Mob Scene 属性。

有两种方法来给这个属性赋值:

将 mob.tscn 从“文件系统”面板拖放到 Mob Scene 属性里。

单击“[空]”旁边的下拉箭头按钮,选择“加载”。选择 mob.tscn。

然后选中“场景”面板中 Main 节点下的 Player 场景实例,切换到侧边栏的“节点”面板。请确保“节点”面板中的“信号”选项卡处于选中状态。

你可以看到 Player 的信号列表。找到 hit 信号并双击(或右键选择 “Connect…”)将会打开信号连接窗口。接下来创建用于在游戏结束时进行一些处理的 game_over 函数。在信号连接窗口底部的 “Receiver Method” 框中输入 “game_over”,并点击 “Connect”。 你的目标是从 Player 发出 hit 信号,并在 Main 脚本中进行处理。将以下代码添加到新函数中,以及一个 new_game 函数,该函数将为新游戏设置一切:

func game_over():
	$ScoreTimer.stop()
	$MobTimer.stop()

func new_game():
	score = 0
	$Player.start($StartPosition.position)
	$StartTimer.start()

现在将每个 Timer 节点(StartTimer,ScoreTimer 和 MobTimer)的 timeout() 信号连接到 main 脚本。 StartTimer 将启动其他两个计时器。 ScoreTimer 将使得分加1。

func _on_score_timer_timeout():
	score += 1

func _on_start_timer_timeout():
	$MobTimer.start()
	$ScoreTimer.start()

在 _on_mob_timer_timeout() 中, 我们先创建小怪实例,然后沿着 Path2D 路径随机选取起始位置,最后让小怪移动。PathFollow2D 节点将沿路径移动,并会自动旋转,所以我们将使用它来选择怪物的方位和朝向。生成小怪后,我们会在 150.0 和 250.0 之间选取随机值,表示每只小怪的移动速度(如果它们都以相同的速度移动,那么就太无聊了)。

注意,必须使用 add_child() 将新实例添加到场景中。

func _on_mob_timer_timeout():
	# Create a new instance of the Mob scene.
	var mob = mob_scene.instantiate()

	# Choose a random location on Path2D.
	var mob_spawn_location = $MobPath/MobSpawnLocation
	mob_spawn_location.progress_ratio = randf()

	# Set the mob's direction perpendicular to the path direction.
	var direction = mob_spawn_location.rotation + PI / 2

	# Set the mob's position to a random location.
	mob.position = mob_spawn_location.position

	# Add some randomness to the direction.
	direction += randf_range(-PI / 4, PI / 4)
	mob.rotation = direction

	# Choose the velocity for the mob.
	var velocity = Vector2(randf_range(150.0, 250.0), 0.0)
	mob.linear_velocity = velocity.rotated(direction)

	# Spawn the mob by adding it to the Main scene.
	add_child(mob)

测试场景
让我们测试这个场景,确保一切正常。请将对 new_game 的调用添加至 _ready():

func _ready():
	new_game()

让我们同时指定 Main 作为我们的“主场景”——游戏启动时自动运行的场景。按下“运行”按钮,当弹出提示时选择 main.tscn。

你应该可以四处移动游戏角色,观察敌人的生成,以及玩家被敌人击中时会消失。

当你确保一切正常运行时,从 _ready() 中移除对 new_game() 的调用,然后用 pass 进行替代。

我们的游戏还缺点啥?缺用户界面。在下一课中,我们将会添加标题界面并且显示玩家的分数。

6游戏信息显示

游戏信息显示
我们的游戏最后还需要用户界面(User Interface,UI),显示分数、“游戏结束”信息、重启按钮。

创建新场景,点击“其他节点”按钮,然后添加一个 CanvasLayer 节点并命名为 HUD。“HUD”是“heads-up display”(游戏信息显示)的缩写,是覆盖在游戏视图上显示的信息。

CanvasLayer 节点可以让我们在游戏的其他部分的上一层绘制 UI 元素,这样它所显示的信息就不会被任何游戏元素(如玩家或敌人)所覆盖。

HUD 中需要显示以下信息:

得分,由 ScoreTimer 更改。

消息,例如“Game Over”或“Get Ready!”

“Start”按钮来开始游戏。

UI 元素的基本节点是 Control 。要创建 UI,我们需使用 Control 下的两种节点:Label 和 Button。

创建以下节点作为 HUD 的子节点:

名为分数标签 ScoreLabel 的 Label。

名为消息 Message 的 Label。

名为开始按钮 StartButton 的 Button。

名为信息计数器 MessageTimer 的 Timer。

点击 ScoreLabel 并在“检查器”的 Text 字段中键入一个数字。 Control 节点的默认字体很小,不能很好地缩放。游戏资产包中有一个叫作“Xolonium-Regular.ttf”的字体文件。 使用此字体需要执行以下操作:

在“Theme Overrides > Fonts”(主题覆盖 > 字体)中选择“加载”,然后选中“Xolonium-Regular.ttf”文件。
在这里插入图片描述
字体尺寸仍然太小,请在“Theme Overrides > Font Sizes”(主题覆盖 > 字体大小)下将其增加到 64。当 ScoreLabel 完成此操作后,请重复对 Message 和 StartButton 节点做同样的修改。
在这里插入图片描述
请将节点如下图排列。拖动节点可以手动放置,也可以使用“锚点预设(Anchor Preset)”进行更精确的定位。
在这里插入图片描述
ScoreLabel
添加文本 0。

将“Horizontal Alignment”和“Vertical Alignment”设置为 Center。

为“Anchor Preset”选择 Center Top。

Messagel
添加文本 Dodge the Creeps!。

将“Horizontal Alignment”和“Vertical Alignment”设置为 Center。

将“Autowrap Mode”设置为 Word,否则标签只会有一行。

在“Control - Layout/Transform”中将“Size X”设置为 480,使用屏幕的完整宽度。

为“Anchor Preset”选择 Center。

StartButton
添加文本 Start。

在“Control - Layout/Transform”中将“Size X”设置为 200、“Size Y”设置为 100,在边框和文本之间添加间距。

为“Anchor Preset”选择 Center Bottom。

在“Control - Layout/Transform”中将“Position Y”设置为 580。

在 MessageTimer 中,将 Wait Time 设置为 2 并将 One Shot 属性设置为“启用”。

现将这个脚本添加到 HUD:

extends CanvasLayer

signal start_game

当想显示一条临时消息时,比如“Get Ready”,就会调用这个函数

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

我们还需要处理玩家死亡的情况。以下代码会显示 2 秒“Game Over”,然后返回标题屏幕,暂停一会儿之后再显示“Start”按钮。

func show_game_over():
	show_message("Game Over")
	# Wait until the MessageTimer has counted down.
	await $MessageTimer.timeout

	$Message.text = "Dodge the Creeps!"
	$Message.show()
	# Make a one-shot timer and wait for it to finish.
	await get_tree().create_timer(1.0).timeout
	$StartButton.show()

当玩家死亡时调用这个函数。将显示“Game Over”2 秒,然后返回标题屏幕并显示“Start”按钮。
将以下更新分数代码添加到 HUD 中

func update_score(score):
	$ScoreLabel.text = str(score)

连接StartButton的pressed()信号和MessageTimer的timeout()信号,并在新的函数中添加以下代码:

func _on_start_button_pressed():
	$StartButton.hide()
	start_game.emit()

func _on_message_timer_timeout():
	$Message.hide()

将 HUD 场景连接到 Main 场景
现在我们完成了 HUD 场景,保存并返回 Main 场景。和 Player 场景的做法一样,在 Main 场景中实例化 HUD 场景。如果你没有错过任何东西,完整的场景树应该像这样:

在这里插入图片描述
现在我们需要将 HUD 功能与我们的 Main 脚本连接起来。这需要在 Main 场景中添加一些内容:

在“节点”选项卡中,点击“连接信号”窗口中的“选取”按钮,选择 new_game() 方法或在窗口的“接收方法”下面输入“new_game”,将 HUD 的 start_game 信号连接到 Main 节点的 new_game() 函数。请确认脚本中 func new_game() 的旁边出现了一个绿色的连接图标。

在 new_game() 函数中,更新分数显示并显示“Get Ready”消息:

$HUD.update_score(score)
$HUD.show_message("Get Ready")

在 game_over() 中我们需要调用相应的 HUD 函数:

$HUD.show_game_over()

最后,将下面的代码添加到 _on_score_timer_timeout() 中,保持不断变化的分数的同步显示

$HUD.update_score(score)

现在你就可以开始游戏了!点击“运行项目”按钮。此时会要求你选择一个主场景,选择 main.tscn 即可。

删除旧的小怪
如果你一直玩到“游戏结束”,然后重新开始新游戏,上局游戏的小怪仍然显示在屏幕上。更好的做法是在新游戏开始时清除它们。我们需要一个同时让所有小怪删除它自己的方法,为此可以使用“分组”功能。

在 Mob 场景中,选择根节点,然后单击检查器旁边的“节点”选项卡(在该位置可以找到节点的信号)。 点击“信号”旁边的“分组”,然后可以输入新的组名称,点击“添加”。
在这里插入图片描述
现在,所有小怪都将属于“mobs”(小怪)分组。我们可以将以下行添加到 Main 中的 new_game() 函数中:

get_tree().call_group("mobs", "queue_free")

call_group() 函数调用组中每个节点上的删除函数——让每个怪物删除其自身。

游戏在这一点上大部分已经完成。在下一部分和最后一部分中,我们将通过添加背景,循环音乐和一些键盘快捷键来对其进行一些润色。

7完成

现在,我们已经完成了游戏的所有功能。以下是一些剩余的步骤,为游戏加点“料”,改善游戏体验。

随意用你自己的想法扩展游戏玩法。

背景
默认的灰色背景不是很吸引人,那么我们就来改一下颜色。一种方法是使用 ColorRect 节点。将其设为 Main 下的第一个节点,这样这个节点就会绘制在其他节点之后。ColorRect 只有一个属性:Color(颜色)。选择一个你喜欢的颜色,然后在视口顶部的工具栏或者检查器中选择“布局”->“锚点预设”->“整个矩形”(Layout -> Anchors Preset -> Full Rect),使其覆盖屏幕。

如果你有背景图片, 你也可以通过使用 TextureRect 节点来添加背景图片.

音效
声音和音乐可能是增强游戏吸引力的最有效方法。在游戏 art 文件夹中,有两个声音文件:“House in a Forest Loop.ogg”用于背景音乐,而“gameover.wav”用于当玩家失败时。

添加两个 AudioStreamPlayer 节点作为 Main 的子节点。将其中一个命名为 Music,将另一个命名为 DeathSound。 在每个节点选项上,点击 Stream 属性,选择 加载,然后选择相应的音频文件。

所有音频都会在禁用 循环 设置的情况下自动导入。如果希望音乐无缝循环,请单击流文件下拉箭头,选择 唯一化,然后再单击流文件并选中 循环 框。

要播放音乐, 在 new_game() 函数中添加 $Music.play(), 在 game_over() 函数中添加 $Music.stop() .

最后, 在 game_over() 函数中添加 $DeathSound.play() .

func game_over():
	...
	$Music.stop()
	$DeathSound.play()

func new_game():
	...
	$Music.play()

键盘快捷键
当游戏使用键盘控制,可以方便地按键盘上的键来启动游戏。一种方法是使用 Button 节点的 “Shortcut”(快捷键)属性。

在上一课中,我们创建了四个输入动作来移动角色。我们将创建一个类似的输入动作来映射到开始按钮。

选择“项目 -> 项目设置”,然后单击“输入映射”选项卡。与创建移动输入动作的方式相同,创建一个名为 start_game 的新输入操作,并为 Enter 添加按键映射。
非常简单的完成了!
在这里插入图片描述
如果你有一个手柄,现在可以添加一个手柄支持。连接上你的手柄,然后在每一个你想添加手柄支持的输入动作下,点击 “+” 按钮然后按下该输入动作对应的按钮,方向键或者摇杆。

在 HUD 场景中,选择 StartButton 并在检查器中找到它的 Shortcut(快捷方式)属性。通过在框中单击来创建一个新的 快捷键 资源,打开 Events(事件) 数组并通过单击 Array[InputEvent] (size 0) 向其添加一个新的数组元素。
在这里插入图片描述
创建一个新的 InputEventAction并将其命名为 start_game。

在这里插入图片描述

这样,开始按钮出现后,你就可以点击它或按 Enter 来启动游戏。

就这样,你在 Godot 中完成了你的第一个 2D 游戏。

三、各部分代码和分析

精灵动画

:AnimatedSprite2D 与 Sprite2D 节点类似,但是包含多张纹理,可用作动画帧。动画使用 SpriteFrames 资源创建,可以导入图像文件(或包含此类文件的文件夹)为该精灵提供动画帧。可以在编辑器的“动画帧”底部面板中配置 SpriteFrames 资源。

例程下载

链接:https://pan.baidu.com/s/1O6ffIOD3tXIrMFfzkI2cMw
提取码:jg1u

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

超级馒头神

看看谁给我第一个打赏。太感谢您

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值