您的第一个游戏

概览

本教程将指导您完成第一个Godot项目.您将学习Godot编辑器的工作原理、如何构建项目、以及如何构建2D游戏.

注解

该项目是Godot引擎的一个介绍.它假定您已经有一定的编程经验.如果您完全不熟悉编程,则应从这里开始: 编写脚本.

这个游戏叫做 Dodge the Creeps!.您的角色必须尽可能长时间移动并避开敌人.这是最终结果的预览:

../../_images/dodge_preview.gif

为什么是2D? 3D游戏比2D游戏复杂得多.在你充分掌握了游戏开发过程和Godot的使用方式之前,请坚持使用2D.

项目设置

启动Godot并创建一个新项目.然后,下载 dodge_assets.zip ——用于制作这个游戏的图像和声音.将这些文件解压缩到您的项目文件夹中.

注解

在本教程中,我们假设您已经熟悉了编辑器.如果您还没有阅读 场景与节点 ,请先阅读,学习如何设置项目与编辑器的使用方式.

这个游戏使用竖屏模式,所以我们需要调整游戏窗口的大小.点击项目->项目设置->显示->窗口,设置”宽度”为 480 ,”高度”为 720.

在本节中,在”拉伸”选项下,将”Mode”设置为”2d”,将”Aspect”设置为”keep”.这确保了游戏在不同大小的屏幕上的缩放一致.

组织项目

在这个项目中,我们将制作3个独立的场景:PlayerMob、和 HUD,之后将它们组合到游戏的 Main 场景中.在较大的项目中,创建文件夹来保存各种场景及其脚本可能会很有用,但是对于这个相对较小的游戏,你可以将场景和脚本保存在项目的根文件夹 res:// .您可以在左下角的文件系统停靠面板中看到您的项目文件夹:

../../_images/filesystem_dock.png

Player 场景

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

节点结构

首先,我们需要为player对象选择一个根节点.作为一般规则,场景的根节点应该反映对象所需的功能——对象*是什么*.单击”其他节点”按钮并将 Area2D 节点添加到场景中.

../../_images/add_node.png

Godot将在场景树中的节点旁边显示警告图标.你现在可以忽略它.我们稍后再谈.

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

在将任何子级添加到 Player 节点之前,我们要确保不会通过点击它们而意外地移动它们或调整其大小.选择节点,然后点击锁右侧的图标;它的工具提示显示 确保对象的子级不可选择.

../../_images/lock_children.png

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

注解

对于此项目,我们将遵循Godot的命名约定.

  • GDScript:类(节点)使用大驼峰命名法(PascalCase),变量和函数使用蛇形命名法(snake_case),常量使用全大写(ALL_CAPS)(请参阅 GDScript 风格指南).

  • C#:类、导出变量和方法使用PascalCase,私有字段使用_camelCase,局部变量和参数使用camelCase(参见 C# 风格指南).连接信号时,请务必准确键入方法名称.

精灵动画

点击 Player 节点并添加一个 AnimatedSprite 节点作为子节点.``AnimatedSprite`` 将为我们的 Player 处理外观和动画.请注意,节点旁边有一个警告符号.一个 AnimatedSprite 需要一个 SpriteFrames 资源,它是一个可显示的动画列表.要创建它,在属性检查器面板中找到 Frames 属性,然后点击”[空白]“ -> “新建SpriteFrames”.再次点击来打开 SpriteFrames 面板:

../../_images/spriteframes_panel.png

左边是一个动画列表.点击 “defalult”动画,并将其重命名为 “walk”.然后点击 “新动画”按钮,创建另一个名为 “up “的动画.在 “文件系统 “选项卡中找到player图像-——它们应该在你之前解压的 art 文件夹中.将每个动画的两张图像, playerGrey_up[1/2]playerGrey_walk[1/2] ,拖到对应动画的面板的 “动画帧 “处:

../../_images/spriteframes_panel2.png

Player 图像对于游戏窗口来说有点太大,所以我们需要缩小它们.点击 AnimatedSprite 节点并将 Scale 属性设置为 (0.5,0.5) .您可以在属性检查器面板中的 Node2D 标题下找到它.

../../_images/player_scale.png

最后,添加一个 CollisionShape2D 作为 Player 的子节点.它用于决定 Player 的”碰撞盒”,亦或者说是它碰撞区域的边界.对于该角色,``CapsuleShape2D`` 节点最适合,因此,在属性检查器中的”形状”旁边,单击” [空]“”->”新建CapsuleShape2D”.使用两个尺寸手柄,调整形状,以覆盖住精灵:

../../_images/player_coll_shape.png

完成后,您的 Player 场景看起来应该像这样:

../../_images/player_scene_nodes.png

修改完成后请确保再次保存场景.

移动 Player

现在我们需要添加一些内置节点所不具备的功能,因此要添加一个脚本.点击 Player 节点然后点击 附加脚本 按钮:

../../_images/add_script_button.png

在脚本设置窗口中,您可以维持默认设置.点击 创建 即可:

注解

如果您要创建一个C#脚本或者其他语言的脚本,那就在创建之前在 语言 下拉菜单中选择语言.

../../_images/attach_node_window.png

注解

如果这是您第一次使用GDScript,请在继续之前阅读 编写脚本.

首先声明该对象将需要的成员变量:

GDScript

C#

  1. extends Area2D
  2. export var speed = 400 # How fast the player will move (pixels/sec).
  3. var screen_size # Size of the game window.
  1. public class Player : Area2D
  2. {
  3. [Export]
  4. public int Speed = 400; // How fast the player will move (pixels/sec).
  5. private Vector2 _screenSize; // Size of the game window.
  6. }

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

警告

如果使用的是C#,则每当要查看新的导出变量或信号时,都需要(重新)构建项目程序集.点击编辑器窗口底部的” Mono”一词以显示Mono面板,然后单击” 构建项目”按钮,即可手动触发此构建.

../../_images/export_variable.png

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

GDScript

C#

  1. func _ready():
  2. screen_size = get_viewport_rect().size
  1. public override void _Ready()
  2. {
  3. _screenSize = GetViewport().Size;
  4. }

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

  • 检查输入.

  • 沿给定方向移动.

  • 播放适当的动画.

首先,我们需要检查输入—— Player 是否按下了键?对于这个游戏,我们有4个方向的输入要检查.输入动作在项目设置中的”输入映射”下定义.在这里,您可以定义自定义事件,并为其分配不同的键、鼠标事件、或其他输入.对于此演示项目,我们将使用分配给键盘上箭头键的默认事件.

您可以使用 Input.is_action_pressed() 来检测是否按下了键,如果按下会返回 true,否则返回 false .

GDScript

C#

  1. func _process(delta):
  2. var velocity = Vector2() # The player's movement vector.
  3. if Input.is_action_pressed("ui_right"):
  4. velocity.x += 1
  5. if Input.is_action_pressed("ui_left"):
  6. velocity.x -= 1
  7. if Input.is_action_pressed("ui_down"):
  8. velocity.y += 1
  9. if Input.is_action_pressed("ui_up"):
  10. velocity.y -= 1
  11. if velocity.length() > 0:
  12. velocity = velocity.normalized() * speed
  13. $AnimatedSprite.play()
  14. else:
  15. $AnimatedSprite.stop()
  1. public override void _Process(float delta)
  2. {
  3. var velocity = new Vector2(); // The player's movement vector.
  4. if (Input.IsActionPressed("ui_right"))
  5. {
  6. velocity.x += 1;
  7. }
  8. if (Input.IsActionPressed("ui_left"))
  9. {
  10. velocity.x -= 1;
  11. }
  12. if (Input.IsActionPressed("ui_down"))
  13. {
  14. velocity.y += 1;
  15. }
  16. if (Input.IsActionPressed("ui_up"))
  17. {
  18. velocity.y -= 1;
  19. }
  20. var animatedSprite = GetNode<AnimatedSprite>("AnimatedSprite");
  21. if (velocity.Length() > 0)
  22. {
  23. velocity = velocity.Normalized() * Speed;
  24. animatedSprite.Play();
  25. }
  26. else
  27. {
  28. animatedSprite.Stop();
  29. }
  30. }

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

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

小技巧

如果您以前从未使用过向量数学,或者需要复习,可以在Godot中的 向量数学 上查看向量用法的解释.最好了解一下,但对于本教程的其余部分而言,这不是必需的.

我们也要检查玩家是否在移动,以便在AnimatedSprite上调用 play()stop() .

$get_node() 的简写.因此在上面的代码中, $AnimatedSprite.play()get_node("AnimatedSprite").play() 相同.

小技巧

在GDScript中,``$`` 返回在当前节点的相对路径处的节点,如果找不到该节点,则返回 null.由于AnimatedSprite是当前节点的子项,因此我们可以使用 $AnimatedSprite.

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

GDScript

C#

  1. position += velocity * delta
  2. position.x = clamp(position.x, 0, screen_size.x)
  3. position.y = clamp(position.y, 0, screen_size.y)
  1. Position += velocity * delta;
  2. Position = new Vector2(
  3. x: Mathf.Clamp(Position.x, 0, _screenSize.x),
  4. y: Mathf.Clamp(Position.y, 0, _screenSize.y)
  5. );

小技巧

_process() 函数的 delta 参数是*帧长度*——完成上一帧所花费的时间.使用这个值的话,可以保证你的移动不会被帧率的变化所影响.

点击”运行场景”(F6)并确认您能够在屏幕中沿任一方向移动玩家.

警告

如果在”调试器(Debugger)”面板中出现错误

Attempt to call function 'play' in base 'null instance' on a null instance [尝试在基类为'null实例'的null实例中调用'play'函数]

则可能意味着您拼错了 AnimatedSprite节点的名称.节点名称区分大小写,并且 $NodeNameget_node("NodeName") 必须与您在场景树中看到的名称匹配.

选择动画

现在 Player 可以移动了,我们需要根据方向更改AnimatedSprite正在播放哪个动画.我们有一个 right 动画,使用 flip_h 属性将其水平翻转以向左移动;以及一个 up 动画,用 flip_v 垂直翻转以向下移动.让我们将这些代码放在 _process() 函数的末尾:

GDScript

C#

  1. if velocity.x != 0:
  2. $AnimatedSprite.animation = "walk"
  3. $AnimatedSprite.flip_v = false
  4. # See the note below about boolean assignment
  5. $AnimatedSprite.flip_h = velocity.x < 0
  6. elif velocity.y != 0:
  7. $AnimatedSprite.animation = "up"
  8. $AnimatedSprite.flip_v = velocity.y > 0
  1. if (velocity.x != 0)
  2. {
  3. animatedSprite.Animation = "walk";
  4. animatedSprite.FlipV = false;
  5. // See the note below about boolean assignment
  6. animatedSprite.FlipH = velocity.x < 0;
  7. }
  8. else if (velocity.y != 0)
  9. {
  10. animatedSprite.Animation = "up";
  11. animatedSprite.FlipV = velocity.y > 0;
  12. }

注解

上面代码中的布尔赋值是程序员常用的缩写.在做布尔比较同时,同时可 一个布尔值.参考这段代码与上面的单行布尔赋值:

GDScript

C#

  1. if velocity.x < 0:
  2. $AnimatedSprite.flip_h = true
  3. else:
  4. $AnimatedSprite.flip_h = false
  1. if (velocity.x < 0)
  2. {
  3. animatedSprite.FlipH = true;
  4. }
  5. else
  6. {
  7. animatedSprite.FlipH = false;
  8. }

再次播放场景并检查每个方向上的动画是否正确.

小技巧

这里一个常见的错误是把动画的名字打错了.”SpriteFrames”面板中的动画名称必须与在代码中键入的内容匹配.如果将动画命名为 "Walk ,则还必须在代码中使用大写字母”W”.

当您确定移动正常工作时,请将此行添加到 _ready(),以便 Player 在游戏开始时会被隐藏:

GDScript

C#

  1. hide()
  1. Hide();

准备碰撞

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

在脚本开头, extends Area2d 下添加:

GDScript

C#

  1. signal hit
  1. // Don't forget to rebuild the project so the editor knows about the new signal.
  2. [Signal]
  3. public delegate void Hit();

这定义了一个称为”hit”的自定义信号,当Player与敌人碰撞时,我们将使其Player发射(发出)信号.我们将使用 Area2D 来检测碰撞.选择 Player 节点,然后点击属性检查器选项卡旁边的”节点”选项卡,以查看Player可以发出的信号列表:

../../_images/player_signals.png

请注意自定义的 “hit “信号也在存在!由于敌人将是 RigidBody2D 节点,所以需要 body_entered(body: Node) 信号,当物体接触到玩家时,就会发出这个信号.点击 “连接…”,出现 “连接一个信号 “窗口,不需要改变这些设置,再次点击 “连接”,Godot会自动在你的玩家脚本中创建一个函数.

../../_images/player_signal_connection.png

请注意函数名旁的绿色图标,这表示信号已经连接到这个函数.将以下代码添加到函数体中:

GDScript

C#

  1. func _on_Player_body_entered(body):
  2. hide() # Player disappears after being hit.
  3. emit_signal("hit")
  4. $CollisionShape2D.set_deferred("disabled", true)
  1. public void OnPlayerBodyEntered(PhysicsBody2D body)
  2. {
  3. Hide(); // Player disappears after being hit.
  4. EmitSignal("Hit");
  5. GetNode<CollisionShape2D>("CollisionShape2D").SetDeferred("disabled", true);
  6. }

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

注解

如果在引擎的碰撞处理过程中发生,禁用区域的碰撞形状可能会导致错误.使用 set_delayed() 告诉Godot等待禁用该形状,直到可以安全地这样做为止.

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

GDScript

C#

  1. func start(pos):
  2. position = pos
  3. show()
  4. $CollisionShape2D.disabled = false
  1. public void Start(Vector2 pos)
  2. {
  3. Position = pos;
  4. Show();
  5. GetNode<CollisionShape2D>("CollisionShape2D").Disabled = false;
  6. }

Enemy 场景

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

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

注解

请参阅 实例化 以了解有关实例化的更多信息.

节点设置

点击场景 -> 新建场景然后添加以下节点:

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

RigidBody2D 属性中,将 Gravity Scale 设置为 0,这样怪物就不会下坠.此外,在 PhysicsBody2D 部分下,点击 Mask 属性并去除第一个复选框的勾选.这会确保怪物不会彼此碰撞.

../../_images/set_collision_mask.png

像设置玩家一样设置 AnimatedSprite.这一次, 我们有3个动画: flyswimwalk ,每个动画在art文件夹中都有2张图片.

对于所有动画,将”速度(FPS)”调整为 3 .

../../_images/mob_animations.gif

将属性检查器中的 Playing 属性设置为”On”.

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

Player 图像一样,这些怪物的图像也要缩小.设置 AnimatedSpriteScale 属性为 (0.75, 0.75).

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

保存该场景.

敌人的脚本

将脚本添加到 Mob 并添加以下成员变量:

GDScript

C#

  1. extends RigidBody2D
  2. export var min_speed = 150 # Minimum speed range.
  3. export var max_speed = 250 # Maximum speed range.
  1. public class Mob : RigidBody2D
  2. {
  3. // Don't forget to rebuild the project so the editor knows about the new export variables.
  4. [Export]
  5. public int MinSpeed = 150; // Minimum speed range.
  6. [Export]
  7. public int MaxSpeed = 250; // Maximum speed range.
  8. }

当我们生成怪物时,我们将在 min_speedmax_speed 之间选择一个随机值,以确定每个怪物的运动速度(如果它们都以相同的速度运动,那将很无聊).

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

GDScript

C#

  1. func _ready():
  2. var mob_types = $AnimatedSprite.frames.get_animation_names()
  3. $AnimatedSprite.animation = mob_types[randi() % mob_types.size()]
  1. // C# doesn't implement GDScript's random methods, so we use 'System.Random' as an alternative.
  2. static private Random _random = new Random();
  3. public override void _Ready()
  4. {
  5. var animSprite = GetNode<AnimatedSprite>("AnimatedSprite");
  6. var mobTypes = animSprite.Frames.GetAnimationNames();
  7. animSprite.Animation = mobTypes[_random.Next(0, mobTypes.Length)];
  8. }

首先,我们从AnimatedSprite的 frames 读取所有动画的名称列表.这个属性会返回一个数组,该数组包含三个元素: ["walk", "swim", "fly"] .

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

注解

如果希望每次运行场景时生成的”随机数”都不同,则必须使用 randomize() .我们将在 Main 场景中使用 randomize() ,因此在这里不需要添加.

最后一步是让怪物在超出屏幕时删除自己.连接 VisibilityNotifier2D 节点的 screen_exited() 信号并添加以下代码:

GDScript

C#

  1. func _on_VisibilityNotifier2D_screen_exited():
  2. queue_free()
  1. public void OnVisibilityNotifier2DScreenExited()
  2. {
  3. QueueFree();
  4. }

这样就完成了 Mob 场景.

Main 场景

现在是时候将它们整合在一起了.创建新场景并添加一个 Node 节点,命名为 Main .注意,确保你创建的是Node 而不是 Node2D.点击”实例化”按钮,然后选择保存的 Player.tscn.

../../_images/instance_scene.png

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

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

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

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

  • Position2D (名为 StartPosition) - 表示玩家的起始位置

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

  • MobTimer: 0.5

  • ScoreTimer: 1

  • StartTimer: 2

此外,将 StartTimerOne Shot 属性设置为 On,并将 StartPosition 节点的 Position 设置为 (240, 450).

生成怪物

Main 节点将产生新的生物,我们希望它们出现在屏幕边缘的随机位置.添加一个名为 MobPathPath2D 节点作为 Main 的子级.当您选择 Path2D 时,您将在编辑器顶部看到一些新按钮:

../../_images/path2d_buttons.png

选择中间的按钮( 添加点 ),然后通过点击给四角添加点来绘制路径.要使点吸附到网格,请确保同时选中”使用吸附”和”使用网格吸附”.该选项可以在”锁定”按钮左侧找到,图标为一个磁铁加三个点或一些网格线.

../../_images/grid_snap_button.png

重要

顺时针 的顺序绘制路径,否则小怪会 向外 而非 向内 生成!

../../_images/draw_path2d.gif

在图像上放置点 4 后,点击 闭合曲线 按钮,您的曲线将完成.

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

您的场景应如下所示:

../../_images/main_scene_nodes.png

Main 脚本

将脚本添加到 Main.在脚本的顶部,我们使用 export (PackedScene) 来允许我们选择要实例化的 Mob 场景.

GDScript

C#

  1. extends Node
  2. export (PackedScene) var Mob
  3. var score
  4. func _ready():
  5. randomize()
  1. public class Main : Node
  2. {
  3. // Don't forget to rebuild the project so the editor knows about the new export variable.
  4. [Export]
  5. public PackedScene Mob;
  6. private int _score;
  7. // We use 'System.Random' as an alternative to GDScript's random methods.
  8. private Random _random = new Random();
  9. public override void _Ready()
  10. {
  11. }
  12. // We'll use this later because C# doesn't support GDScript's randi().
  13. private float RandRange(float min, float max)
  14. {
  15. return (float)_random.NextDouble() * (max - min) + min;
  16. }
  17. }

单击 Main 节点,就可以在属性检查器(Inspector)的脚本变量区(Script Variables)看到 Mob 属性.

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

  • 从”文件系统”面板中拖动 Mob.tscnMob 属性中.

  • 单击”「空」”旁边的下拉箭头按钮,选择”载入”,接着选择 Mob.tscn .

在场景树中选择 Player 节点,然后选择 节点(Node) 选项卡(位于右侧属性旁),确保已选择 信号(Signals) .

你可以看到 Player 的信号列表.找到 hit 信号并双击(或右键选择”连接信号…”).我们将在打开的界面创建 game_over 函数,用来处理游戏结束时发生的事情.在 连接信号到方法 窗口底部的 接收方法 框中键入 game_over .添加以下代码,以及 new_game 函数以设置新游戏的所需内容:

GDScript

C#

  1. func game_over():
  2. $ScoreTimer.stop()
  3. $MobTimer.stop()
  4. func new_game():
  5. score = 0
  6. $Player.start($StartPosition.position)
  7. $StartTimer.start()
  1. public void GameOver()
  2. {
  3. GetNode<Timer>("MobTimer").Stop();
  4. GetNode<Timer>("ScoreTimer").Stop();
  5. }
  6. public void NewGame()
  7. {
  8. _score = 0;
  9. var player = GetNode<Player>("Player");
  10. var startPosition = GetNode<Position2D>("StartPosition");
  11. player.Start(startPosition.Position);
  12. GetNode<Timer>("StartTimer").Start();
  13. }

现在将每个 Timer 节点( StartTimerScoreTimerMobTimer )的 timeout() 信号连接到 main 脚本.``StartTimer`` 将启动其他两个计时器.``ScoreTimer`` 将使得分以1的增速增加.

GDScript

C#

  1. func _on_StartTimer_timeout():
  2. $MobTimer.start()
  3. $ScoreTimer.start()
  4. func _on_ScoreTimer_timeout():
  5. score += 1
  1. public void OnStartTimerTimeout()
  2. {
  3. GetNode<Timer>("MobTimer").Start();
  4. GetNode<Timer>("ScoreTimer").Start();
  5. }
  6. public void OnScoreTimerTimeout()
  7. {
  8. _score++;
  9. }

_on_MobTimer_timeout() 中,我们将创建一个 mob 实例,沿着 Path2D 随机选择一个起始位置,然后让 mob 移动.``PathFollow2D`` 节点将沿路径移动,因此会自动旋转,所以我们将使用它来选择怪物的方向及其位置.

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

GDScript

C#

  1. func _on_MobTimer_timeout():
  2. # Choose a random location on Path2D.
  3. $MobPath/MobSpawnLocation.offset = randi()
  4. # Create a Mob instance and add it to the scene.
  5. var mob = Mob.instance()
  6. add_child(mob)
  7. # Set the mob's direction perpendicular to the path direction.
  8. var direction = $MobPath/MobSpawnLocation.rotation + PI / 2
  9. # Set the mob's position to a random location.
  10. mob.position = $MobPath/MobSpawnLocation.position
  11. # Add some randomness to the direction.
  12. direction += rand_range(-PI / 4, PI / 4)
  13. mob.rotation = direction
  14. # Set the velocity (speed & direction).
  15. mob.linear_velocity = Vector2(rand_range(mob.min_speed, mob.max_speed), 0)
  16. mob.linear_velocity = mob.linear_velocity.rotated(direction)
  1. public void OnMobTimerTimeout()
  2. {
  3. // Choose a random location on Path2D.
  4. var mobSpawnLocation = GetNode<PathFollow2D>("MobPath/MobSpawnLocation");
  5. mobSpawnLocation.Offset = _random.Next();
  6. // Create a Mob instance and add it to the scene.
  7. var mobInstance = (RigidBody2D)Mob.Instance();
  8. AddChild(mobInstance);
  9. // Set the mob's direction perpendicular to the path direction.
  10. float direction = mobSpawnLocation.Rotation + Mathf.Pi / 2;
  11. // Set the mob's position to a random location.
  12. mobInstance.Position = mobSpawnLocation.Position;
  13. // Add some randomness to the direction.
  14. direction += RandRange(-Mathf.Pi / 4, Mathf.Pi / 4);
  15. mobInstance.Rotation = direction;
  16. // Choose the velocity.
  17. mobInstance.LinearVelocity = new Vector2(RandRange(150f, 250f), 0).Rotated(direction);
  18. }

重要

为什么使用 PI ?在需要角度的函数中,GDScript使用 弧度,而不是角度.如果您更喜欢使用角度,则需要使用 deg2rad()rad2deg() 函数在角度和弧度之间进行转换.

测试场景

让我们测试这个场景,确保一切正常.将这段添加至 _ready():

GDScript

C#

  1. func _ready():
  2. randomize()
  3. new_game()
  1. public override void _Ready()
  2. {
  3. NewGame();
  4. }
  5. }

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

你应该可以四处移动游戏角色,看到可动对象生成,以及玩家被可动对象击中时会消失.

当你确定一切正常时,在``_ready()``中移除对``new_game()``的调用.

HUD

最后我们的游戏需要的是一个UI:一个显示诸如分数、 “游戏结束” 消息和重启按钮的界面.创建一个新的场景,并添加一个 CanvasLayer 节点,命名为 HUD . “HUD “ 代表 “平视显示” ,是一种信息显示,以叠加的方式出现在游戏视图之上.

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

HUD需要显示以下信息:

  • 得分,由 ScoreTimer 更改.

  • 一条消息,例如 Game OverGet Ready!

  • 一个 Start 按钮来开始游戏.

UI元素的基本节点是 Control.要创造UI,我们会使用 Control 的两种节点: LabelButton .

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

  • 名为 ScoreLabelLabel .

  • 名为 MessageLabel .

  • 名为 StartButtonButton .

  • 名为 MessageTimerTimer .

点击 ScoreLabel 并在属性检查器的 Text 字段中键入一个数字.``Control`` 节点的默认字体很小,不能很好地缩放.游戏素材中包含一个字体文件( Xolonium-Regular.ttf ).要使用此字体,需要执行以下操作:

  1. 在 “Custom Fonts” 的下拉选项中,选择 新建DynamicFont

../../_images/custom_font1.png

  1. 点击您添加的DynamicFont,然后在”Font/Font Data”的下拉选项中选择Load并选择Xolonium-Regular.ttf文件.您还必须设置字体的 Size .设置为 64 就可以了.

../../_images/custom_font2.png

在” ScoreLabel”上完成此操作后,可以单击DynamicFont属性旁边的向下箭头,然后选择”复制”,然后将其”粘贴”到其他两个Control节点的相同位置.

注解

锚和边距: 控制 节点有一个位置和大小,但它们也有锚和边距.锚点定义了原点与节点边缘的参考点.当你移动或调整控制节点的大小时,边距会自动更新.它们表示从控制节点的边缘到其锚点的距离.更多细节请参见 使用 Control 节点设计界面.

按如下图所示排列节点.点击”布局”按钮以设置 一个Control 节点的布局:

../../_images/ui_anchor.png

您可以拖动节点以手动放置它们,或者要进行更精确的放置,请使用以下设置:

ScoreLabel

  • 布局: “顶部宽度”

  • Text : 0

  • 对齐: “居中”

Message

  • 布局: “水平中心宽”

  • 文本: Dodge the Creeps!

  • 对齐: “居中”

  • 自动换行:”开”

StartButton

  • 文本: Start

  • 布局: “中心底部”

  • 边距:

    • 顶部: -200

    • 底部: -100

MessageTimer 中,将 Wait Time 设置为 2 并将 One Shot 属性设置为 “On”.

现将这个脚本添加到 HUD:

GDScript

C#

  1. extends CanvasLayer
  2. signal start_game
  1. public class HUD : CanvasLayer
  2. {
  3. // Don't forget to rebuild the project so the editor knows about the new signal.
  4. [Signal]
  5. public delegate void StartGame();
  6. }

start_game 信号通知 Main 节点,按钮已经被按下.

GDScript

C#

  1. func show_message(text):
  2. $Message.text = text
  3. $Message.show()
  4. $MessageTimer.start()
  1. public void ShowMessage(string text)
  2. {
  3. var message = GetNode<Label>("Message");
  4. message.Text = text;
  5. message.Show();
  6. GetNode<Timer>("MessageTimer").Start();
  7. }

当我们想要显示一条临时消息时,比如 Get Ready ,就会调用这个函数.

GDScript

C#

  1. func show_game_over():
  2. show_message("Game Over")
  3. # Wait until the MessageTimer has counted down.
  4. yield($MessageTimer, "timeout")
  5. $Message.text = "Dodge the\nCreeps!"
  6. $Message.show()
  7. # Make a one-shot timer and wait for it to finish.
  8. yield(get_tree().create_timer(1), "timeout")
  9. $StartButton.show()
  1. async public void ShowGameOver()
  2. {
  3. ShowMessage("Game Over");
  4. var messageTimer = GetNode<Timer>("MessageTimer");
  5. await ToSignal(messageTimer, "timeout");
  6. var message = GetNode<Label>("Message");
  7. message.Text = "Dodge the\nCreeps!";
  8. message.Show();
  9. await ToSignal(GetTree().CreateTimer(1), "timeout");
  10. GetNode<Button>("StartButton").Show();
  11. }

Player 输掉时调用这个函数.它将显示 Game Over 2秒,然后返回标题屏幕并显示 Start 按钮.

注解

当您需要暂停片刻时,可以使用场景树的 create_timer() 函数替代使用 Timer 节点.这对于延迟非常有用,例如在上述代码中,在这里我们需要在显示 开始 按钮前等待片刻.

GDScript

C#

  1. func update_score(score):
  2. $ScoreLabel.text = str(score)
  1. public void UpdateScore(int score)
  2. {
  3. GetNode<Label>("ScoreLabel").Text = score.ToString();
  4. }

每当分数改变,这个函数会被 Main 调用.

连接 MessageTimertimeout() 信号和 StartButtonpressed() 信号并添加以下代码到新函数中:

GDScript

C#

  1. func _on_StartButton_pressed():
  2. $StartButton.hide()
  3. emit_signal("start_game")
  4. func _on_MessageTimer_timeout():
  5. $Message.hide()
  1. public void OnStartButtonPressed()
  2. {
  3. GetNode<Button>("StartButton").Hide();
  4. EmitSignal("StartGame");
  5. }
  6. public void OnMessageTimerTimeout()
  7. {
  8. GetNode<Label>("Message").Hide();
  9. }

将HUD场景连接到Main场景

现在我们完成了 HUD 场景,保存并返回 Main 场景.和 Player 场景的做法一样,在 Main 场景中实例化 HUD 场景.完整的场景树看起来应该像这样,确保您没有错过任何东西:

../../_images/completed_main_scene.png

现在我们需要将 HUD 功能与我们的 Main 脚本连接起来.这需要在 Main 场景中添加一些内容:

在”节点”选项卡中,通过在”连接信号”窗口的”接收器方法”中键入 new_game ,将HUD的 `` start_game`` 信号连接到主节点的 `` new_game()`` 功能. 验证绿色的连接图标现在是否在脚本中的 func new_game() 旁边出现.

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

GDScript

C#

  1. $HUD.update_score(score)
  2. $HUD.show_message("Get Ready")
  1. var hud = GetNode<HUD>("HUD");
  2. hud.UpdateScore(_score);
  3. hud.ShowMessage("Get Ready!");

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

GDScript

C#

  1. $HUD.show_game_over()
  1. GetNode<HUD>("HUD").ShowGameOver();

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

GDScript

C#

  1. $HUD.update_score(score)
  1. GetNode<HUD>("HUD").UpdateScore(_score);

现在您可以开始游戏了!点击 开始项目 按钮.将要求您选择一个主场景,因此选择 Main.tscn.

删除旧的小怪

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

Mob 场景中,选择根节点,然后单击检查器旁边的” Node”选项卡(在该位置可以找到节点的信号). 在”信号”旁边,单击”分组”,然后可以输入新的组名称,然后单击”添加”.

../../_images/group_tab.png

现在,所有生物都将属于”生物(mobs)”组. 然后,我们可以将以下行添加到 Main 中的 game_over() 函数中:

GDScript

C#

  1. get_tree().call_group("mobs", "queue_free")
  1. GetTree().CallGroup("mobs", "queue_free");

call_group() 函数在组中的每个节点上调用命名函数-在这种情况下,我们告诉每个生物都将其删除.

完成了

现在,我们已经完成了游戏的所有功能.以下是一些剩余的步骤,可以添加更多”果汁”以改善游戏体验.随心所欲地扩展游戏玩法.

背景

默认的灰色背景不是很吸引人,因此让我们更改其颜色.一种方法是使用 ColorRect 节点.将其设为 Main 下的第一个节点,以便将其绘制在其他节点之后. ColorRect 只有一个属性: Color .选择您喜欢的颜色,然后选择”布局”->”整个矩形”(位于主窗口上方工具条),使其覆盖屏幕.

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

音效

声音和音乐可能是增加游戏体验吸引力的最有效方法.在游戏素材文件夹中,您有两个声音文件: 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() .

键盘快捷键

由于游戏是使用键盘控制运行的,因此如果我们也可以通过按键盘上的键来启动游戏,将非常方便.一种方法是使用 Button 节点的 Shortcut 属性.

HUD 场景中,选择 StartButton ,然后在属性检查器中找到其 Shortcut 属性.选择”New Shortcut”,然后单击Shortcut项.将出现第二个 Shortcut 属性.选择 新建InputEventAction,然后点击刚创建的InputEventAction.最后,在 Action 属性中,键入名称 ui_select. 这是与空格键关联的默认输入事件.

../../_images/start_button_shortcut.png

现在,当开始按钮出现时,您可以点击它或按 Space 来启动游戏.

项目文件

您可以在以下位置找到该项目的完整版本: