您的第一个游戏
概览
本教程将指导您完成第一个Godot项目.您将学习Godot编辑器的工作原理、如何构建项目、以及如何构建2D游戏.
注解
该项目是Godot引擎的一个介绍.它假定您已经有一定的编程经验.如果您完全不熟悉编程,则应从这里开始: 编写脚本.
这个游戏叫做 Dodge the Creeps!
.您的角色必须尽可能长时间移动并避开敌人.这是最终结果的预览:
为什么是2D? 3D游戏比2D游戏复杂得多.在你充分掌握了游戏开发过程和Godot的使用方式之前,请坚持使用2D.
项目设置
启动Godot并创建一个新项目.然后,下载 dodge_assets.zip
——用于制作这个游戏的图像和声音.将这些文件解压缩到您的项目文件夹中.
注解
在本教程中,我们假设您已经熟悉了编辑器.如果您还没有阅读 场景与节点 ,请先阅读,学习如何设置项目与编辑器的使用方式.
这个游戏使用竖屏模式,所以我们需要调整游戏窗口的大小.点击项目->项目设置->显示->窗口,设置”宽度”为 480
,”高度”为 720
.
在本节中,在”拉伸”选项下,将”Mode”设置为”2d”,将”Aspect”设置为”keep”.这确保了游戏在不同大小的屏幕上的缩放一致.
组织项目
在这个项目中,我们将制作3个独立的场景:Player
、 Mob
、和 HUD
,之后将它们组合到游戏的 Main
场景中.在较大的项目中,创建文件夹来保存各种场景及其脚本可能会很有用,但是对于这个相对较小的游戏,你可以将场景和脚本保存在项目的根文件夹 res://
.您可以在左下角的文件系统停靠面板中看到您的项目文件夹:
Player 场景
第一个场景,我们会定义 Player
对象.单独创建Player场景的好处之一是,在游戏的其他部分做出来之前,我们就可以对其进行单独测试.
节点结构
首先,我们需要为player对象选择一个根节点.作为一般规则,场景的根节点应该反映对象所需的功能——对象*是什么*.单击”其他节点”按钮并将 Area2D 节点添加到场景中.
Godot将在场景树中的节点旁边显示警告图标.你现在可以忽略它.我们稍后再谈.
使用 Area2D
可以检测到与玩家重叠或进入玩家内的物体.通过双击节点名称将其名称更改为 Player
.我们已经设置好了场景的根节点,现在可以向该角色中添加其他节点来增加功能.
在将任何子级添加到 Player
节点之前,我们要确保不会通过点击它们而意外地移动它们或调整其大小.选择节点,然后点击锁右侧的图标;它的工具提示显示 确保对象的子级不可选择.
保存场景.点击场景 -> 保存,或者在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
面板:
左边是一个动画列表.点击 “defalult”动画,并将其重命名为 “walk”.然后点击 “新动画”按钮,创建另一个名为 “up “的动画.在 “文件系统 “选项卡中找到player图像-——它们应该在你之前解压的 art
文件夹中.将每个动画的两张图像, playerGrey_up[1/2]
和 playerGrey_walk[1/2]
,拖到对应动画的面板的 “动画帧 “处:
Player
图像对于游戏窗口来说有点太大,所以我们需要缩小它们.点击 AnimatedSprite
节点并将 Scale
属性设置为 (0.5,0.5)
.您可以在属性检查器面板中的 Node2D
标题下找到它.
最后,添加一个 CollisionShape2D 作为 Player
的子节点.它用于决定 Player
的”碰撞盒”,亦或者说是它碰撞区域的边界.对于该角色,``CapsuleShape2D`` 节点最适合,因此,在属性检查器中的”形状”旁边,单击” [空]“”->”新建CapsuleShape2D”.使用两个尺寸手柄,调整形状,以覆盖住精灵:
完成后,您的 Player
场景看起来应该像这样:
修改完成后请确保再次保存场景.
移动 Player
现在我们需要添加一些内置节点所不具备的功能,因此要添加一个脚本.点击 Player
节点然后点击 附加脚本
按钮:
在脚本设置窗口中,您可以维持默认设置.点击 创建
即可:
注解
如果您要创建一个C#脚本或者其他语言的脚本,那就在创建之前在 语言 下拉菜单中选择语言.
注解
如果这是您第一次使用GDScript,请在继续之前阅读 编写脚本.
首先声明该对象将需要的成员变量:
GDScript
C#
extends Area2D
export var speed = 400 # How fast the player will move (pixels/sec).
var screen_size # Size of the game window.
public class Player : Area2D
{
[Export]
public int Speed = 400; // How fast the player will move (pixels/sec).
private Vector2 _screenSize; // Size of the game window.
}
在第一个变量 speed
上使用 export
关键字,这样允许在属性检查器中设置其值.对于希望能够像节点的内置属性一样进行调整的值,这可能很方便.点击 Player
节点,您将看到该属性现在显示在属性检查器的”脚本变量”部分中.请记住,如果您在此处更改值,它将覆盖脚本中已写入的值.
警告
如果使用的是C#,则每当要查看新的导出变量或信号时,都需要(重新)构建项目程序集.点击编辑器窗口底部的” Mono”一词以显示Mono面板,然后单击” 构建项目”按钮,即可手动触发此构建.
当节点进入场景树时,``_ready()`` 函数被调用,这是查找游戏窗口大小的好时机:
GDScript
C#
func _ready():
screen_size = get_viewport_rect().size
public override void _Ready()
{
_screenSize = GetViewport().Size;
}
现在我们可以使用 _process()
函数定义 Player
将执行的操作.``_process()`` 在每一帧都被调用,因此我们将使用它,来更新我们希望会经常变化的游戏元素.对于 Player
,我们需要执行以下操作:
检查输入.
沿给定方向移动.
播放适当的动画.
首先,我们需要检查输入—— Player
是否按下了键?对于这个游戏,我们有4个方向的输入要检查.输入动作在项目设置中的”输入映射”下定义.在这里,您可以定义自定义事件,并为其分配不同的键、鼠标事件、或其他输入.对于此演示项目,我们将使用分配给键盘上箭头键的默认事件.
您可以使用 Input.is_action_pressed()
来检测是否按下了键,如果按下会返回 true
,否则返回 false
.
GDScript
C#
func _process(delta):
var velocity = Vector2() # The player's movement vector.
if Input.is_action_pressed("ui_right"):
velocity.x += 1
if Input.is_action_pressed("ui_left"):
velocity.x -= 1
if Input.is_action_pressed("ui_down"):
velocity.y += 1
if Input.is_action_pressed("ui_up"):
velocity.y -= 1
if velocity.length() > 0:
velocity = velocity.normalized() * speed
$AnimatedSprite.play()
else:
$AnimatedSprite.stop()
public override void _Process(float delta)
{
var velocity = new Vector2(); // The player's movement vector.
if (Input.IsActionPressed("ui_right"))
{
velocity.x += 1;
}
if (Input.IsActionPressed("ui_left"))
{
velocity.x -= 1;
}
if (Input.IsActionPressed("ui_down"))
{
velocity.y += 1;
}
if (Input.IsActionPressed("ui_up"))
{
velocity.y -= 1;
}
var animatedSprite = GetNode<AnimatedSprite>("AnimatedSprite");
if (velocity.Length() > 0)
{
velocity = velocity.Normalized() * Speed;
animatedSprite.Play();
}
else
{
animatedSprite.Stop();
}
}
我们首先将 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#
position += velocity * delta
position.x = clamp(position.x, 0, screen_size.x)
position.y = clamp(position.y, 0, screen_size.y)
Position += velocity * delta;
Position = new Vector2(
x: Mathf.Clamp(Position.x, 0, _screenSize.x),
y: Mathf.Clamp(Position.y, 0, _screenSize.y)
);
小技巧
_process() 函数的 delta 参数是*帧长度*——完成上一帧所花费的时间.使用这个值的话,可以保证你的移动不会被帧率的变化所影响.
点击”运行场景”(F6
)并确认您能够在屏幕中沿任一方向移动玩家.
警告
如果在”调试器(Debugger)”面板中出现错误
Attempt to call function 'play' in base 'null instance' on a null instance [尝试在基类为'null实例'的null实例中调用'play'函数]
则可能意味着您拼错了 AnimatedSprite节点的名称.节点名称区分大小写,并且 $NodeName
或 get_node("NodeName")
必须与您在场景树中看到的名称匹配.
选择动画
现在 Player
可以移动了,我们需要根据方向更改AnimatedSprite正在播放哪个动画.我们有一个 right
动画,使用 flip_h
属性将其水平翻转以向左移动;以及一个 up
动画,用 flip_v
垂直翻转以向下移动.让我们将这些代码放在 _process()
函数的末尾:
GDScript
C#
if velocity.x != 0:
$AnimatedSprite.animation = "walk"
$AnimatedSprite.flip_v = false
# See the note below about boolean assignment
$AnimatedSprite.flip_h = velocity.x < 0
elif velocity.y != 0:
$AnimatedSprite.animation = "up"
$AnimatedSprite.flip_v = velocity.y > 0
if (velocity.x != 0)
{
animatedSprite.Animation = "walk";
animatedSprite.FlipV = false;
// See the note below about boolean assignment
animatedSprite.FlipH = velocity.x < 0;
}
else if (velocity.y != 0)
{
animatedSprite.Animation = "up";
animatedSprite.FlipV = velocity.y > 0;
}
注解
上面代码中的布尔赋值是程序员常用的缩写.在做布尔比较同时,同时可 赋 一个布尔值.参考这段代码与上面的单行布尔赋值:
GDScript
C#
if velocity.x < 0:
$AnimatedSprite.flip_h = true
else:
$AnimatedSprite.flip_h = false
if (velocity.x < 0)
{
animatedSprite.FlipH = true;
}
else
{
animatedSprite.FlipH = false;
}
再次播放场景并检查每个方向上的动画是否正确.
小技巧
这里一个常见的错误是把动画的名字打错了.”SpriteFrames”面板中的动画名称必须与在代码中键入的内容匹配.如果将动画命名为 "Walk
,则还必须在代码中使用大写字母”W”.
当您确定移动正常工作时,请将此行添加到 _ready()
,以便 Player
在游戏开始时会被隐藏:
GDScript
C#
hide()
Hide();
准备碰撞
我们希望 Player
能够检测到何时被敌人击中,但是我们还没有任何敌人!没关系,因为我们将使用Godot的 信号 功能来使其正常工作.
在脚本开头, extends Area2d
下添加:
GDScript
C#
signal hit
// Don't forget to rebuild the project so the editor knows about the new signal.
[Signal]
public delegate void Hit();
这定义了一个称为”hit”的自定义信号,当Player与敌人碰撞时,我们将使其Player发射(发出)信号.我们将使用 Area2D
来检测碰撞.选择 Player
节点,然后点击属性检查器选项卡旁边的”节点”选项卡,以查看Player可以发出的信号列表:
请注意自定义的 “hit “信号也在存在!由于敌人将是 RigidBody2D
节点,所以需要 body_entered(body: Node)
信号,当物体接触到玩家时,就会发出这个信号.点击 “连接…”,出现 “连接一个信号 “窗口,不需要改变这些设置,再次点击 “连接”,Godot会自动在你的玩家脚本中创建一个函数.
请注意函数名旁的绿色图标,这表示信号已经连接到这个函数.将以下代码添加到函数体中:
GDScript
C#
func _on_Player_body_entered(body):
hide() # Player disappears after being hit.
emit_signal("hit")
$CollisionShape2D.set_deferred("disabled", true)
public void OnPlayerBodyEntered(PhysicsBody2D body)
{
Hide(); // Player disappears after being hit.
EmitSignal("Hit");
GetNode<CollisionShape2D>("CollisionShape2D").SetDeferred("disabled", true);
}
敌人每次击中 Player
时,都会发出一个信号.我们需要禁用 Player
的碰撞检测,确保我们不会多次触发 hit
信号.
注解
如果在引擎的碰撞处理过程中发生,禁用区域的碰撞形状可能会导致错误.使用 set_delayed()
告诉Godot等待禁用该形状,直到可以安全地这样做为止.
最后再为 Player
添加一个函数,用于在开始新游戏时调用来重置 Player
.
GDScript
C#
func start(pos):
position = pos
show()
$CollisionShape2D.disabled = false
public void Start(Vector2 pos)
{
Position = pos;
Show();
GetNode<CollisionShape2D>("CollisionShape2D").Disabled = false;
}
Enemy
场景
是时候去做一些玩家必须躲避的敌人了.它们的行为很简单:怪物将随机生成在屏幕的边缘,沿着随机的方向直线移动.
我们将创建一个 Mob
的怪物场景,以便在游戏中独立 实例化 出任意数量的怪物.
注解
请参阅 实例化 以了解有关实例化的更多信息.
节点设置
点击场景 -> 新建场景然后添加以下节点:
别忘了设置子项,使其无法被选中,就像您对 Player
场景所做的那样.
在 RigidBody2D 属性中,将 Gravity Scale
设置为 0
,这样怪物就不会下坠.此外,在 PhysicsBody2D
部分下,点击 Mask
属性并去除第一个复选框的勾选.这会确保怪物不会彼此碰撞.
像设置玩家一样设置 AnimatedSprite.这一次, 我们有3个动画: fly
、 swim
和 walk
,每个动画在art文件夹中都有2张图片.
对于所有动画,将”速度(FPS)”调整为 3
.
将属性检查器中的 Playing
属性设置为”On”.
我们将随机选择其中一个动画,以便mobs有一些变化.
像 Player
图像一样,这些怪物的图像也要缩小.设置 AnimatedSprite
的 Scale
属性为 (0.75, 0.75)
.
像在 Player
场景中一样,为碰撞添加一个 CapsuleShape2D
.为了使形状与图像对齐,您需要将 Rotation Degrees
(在属性检查器的”Transorm”下)属性设置为 90
.
保存该场景.
敌人的脚本
将脚本添加到 Mob
并添加以下成员变量:
GDScript
C#
extends RigidBody2D
export var min_speed = 150 # Minimum speed range.
export var max_speed = 250 # Maximum speed range.
public class Mob : RigidBody2D
{
// Don't forget to rebuild the project so the editor knows about the new export variables.
[Export]
public int MinSpeed = 150; // Minimum speed range.
[Export]
public int MaxSpeed = 250; // Maximum speed range.
}
当我们生成怪物时,我们将在 min_speed
和 max_speed
之间选择一个随机值,以确定每个怪物的运动速度(如果它们都以相同的速度运动,那将很无聊).
现在让我们看一下脚本的其余部分.在 _ready()
中,我们从三个动画类型中随机选择一个:
GDScript
C#
func _ready():
var mob_types = $AnimatedSprite.frames.get_animation_names()
$AnimatedSprite.animation = mob_types[randi() % mob_types.size()]
// C# doesn't implement GDScript's random methods, so we use 'System.Random' as an alternative.
static private Random _random = new Random();
public override void _Ready()
{
var animSprite = GetNode<AnimatedSprite>("AnimatedSprite");
var mobTypes = animSprite.Frames.GetAnimationNames();
animSprite.Animation = mobTypes[_random.Next(0, mobTypes.Length)];
}
首先,我们从AnimatedSprite的 frames
读取所有动画的名称列表.这个属性会返回一个数组,该数组包含三个元素: ["walk", "swim", "fly"]
.
然后我们需要在 0
和 2
之间选取一个随机的数字,以在列表中选择一个名称(数组索引以 0
起始).``randi() % n`` 会在 0
and n-1
之中选择一个随机整数.
注解
如果希望每次运行场景时生成的”随机数”都不同,则必须使用 randomize()
.我们将在 Main
场景中使用 randomize()
,因此在这里不需要添加.
最后一步是让怪物在超出屏幕时删除自己.连接 VisibilityNotifier2D
节点的 screen_exited()
信号并添加以下代码:
GDScript
C#
func _on_VisibilityNotifier2D_screen_exited():
queue_free()
public void OnVisibilityNotifier2DScreenExited()
{
QueueFree();
}
这样就完成了 Mob
场景.
Main
场景
现在是时候将它们整合在一起了.创建新场景并添加一个 Node 节点,命名为 Main
.注意,确保你创建的是Node 而不是 Node2D.点击”实例化”按钮,然后选择保存的 Player.tscn
.
现在,将以下节点添加为 Main
的子节点,并按如下所示对其进行命名(值以秒为单位):
Timer (名为
MobTimer
)——控制怪物产生的频率Timer (名为
ScoreTimer
)——每秒增加分数Timer (名为
StartTimer
)——在开始之前给出延迟Position2D (名为
StartPosition
) - 表示玩家的起始位置
如下设置每个 Timer
节点的 Wait Time
属性:
MobTimer
:0.5
ScoreTimer
:1
StartTimer
:2
此外,将 StartTimer
的 One Shot
属性设置为 On
,并将 StartPosition
节点的 Position
设置为 (240, 450)
.
生成怪物
Main
节点将产生新的生物,我们希望它们出现在屏幕边缘的随机位置.添加一个名为 MobPath
的 Path2D 节点作为 Main
的子级.当您选择 Path2D
时,您将在编辑器顶部看到一些新按钮:
选择中间的按钮( 添加点
),然后通过点击给四角添加点来绘制路径.要使点吸附到网格,请确保同时选中”使用吸附”和”使用网格吸附”.该选项可以在”锁定”按钮左侧找到,图标为一个磁铁加三个点或一些网格线.
重要
以 顺时针 的顺序绘制路径,否则小怪会 向外 而非 向内 生成!
在图像上放置点 4
后,点击 闭合曲线
按钮,您的曲线将完成.
现在已经定义了路径,添加一个 PathFollow2D 节点作为 MobPath
的子节点,并将其命名为 MobSpawnLocation
.该节点在移动时,将自动旋转并沿着该路径,因此我们可以使用它沿路径来选择随机位置和方向.
您的场景应如下所示:
Main
脚本
将脚本添加到 Main
.在脚本的顶部,我们使用 export (PackedScene)
来允许我们选择要实例化的 Mob
场景.
GDScript
C#
extends Node
export (PackedScene) var Mob
var score
func _ready():
randomize()
public class Main : Node
{
// Don't forget to rebuild the project so the editor knows about the new export variable.
[Export]
public PackedScene Mob;
private int _score;
// We use 'System.Random' as an alternative to GDScript's random methods.
private Random _random = new Random();
public override void _Ready()
{
}
// We'll use this later because C# doesn't support GDScript's randi().
private float RandRange(float min, float max)
{
return (float)_random.NextDouble() * (max - min) + min;
}
}
单击 Main
节点,就可以在属性检查器(Inspector)的脚本变量区(Script Variables)看到 Mob
属性.
有两种方法来给这个属性赋值:
从”文件系统”面板中拖动
Mob.tscn
到Mob
属性中.单击”「空」”旁边的下拉箭头按钮,选择”载入”,接着选择
Mob.tscn
.
在场景树中选择 Player
节点,然后选择 节点(Node)
选项卡(位于右侧属性旁),确保已选择 信号(Signals)
.
你可以看到 Player
的信号列表.找到 hit
信号并双击(或右键选择”连接信号…”).我们将在打开的界面创建 game_over
函数,用来处理游戏结束时发生的事情.在 连接信号到方法
窗口底部的 接收方法
框中键入 game_over
.添加以下代码,以及 new_game
函数以设置新游戏的所需内容:
GDScript
C#
func game_over():
$ScoreTimer.stop()
$MobTimer.stop()
func new_game():
score = 0
$Player.start($StartPosition.position)
$StartTimer.start()
public void GameOver()
{
GetNode<Timer>("MobTimer").Stop();
GetNode<Timer>("ScoreTimer").Stop();
}
public void NewGame()
{
_score = 0;
var player = GetNode<Player>("Player");
var startPosition = GetNode<Position2D>("StartPosition");
player.Start(startPosition.Position);
GetNode<Timer>("StartTimer").Start();
}
现在将每个 Timer
节点( StartTimer
、 ScoreTimer
和 MobTimer
)的 timeout()
信号连接到 main
脚本.``StartTimer`` 将启动其他两个计时器.``ScoreTimer`` 将使得分以1的增速增加.
GDScript
C#
func _on_StartTimer_timeout():
$MobTimer.start()
$ScoreTimer.start()
func _on_ScoreTimer_timeout():
score += 1
public void OnStartTimerTimeout()
{
GetNode<Timer>("MobTimer").Start();
GetNode<Timer>("ScoreTimer").Start();
}
public void OnScoreTimerTimeout()
{
_score++;
}
在 _on_MobTimer_timeout()
中,我们将创建一个 mob
实例,沿着 Path2D
随机选择一个起始位置,然后让 mob
移动.``PathFollow2D`` 节点将沿路径移动,因此会自动旋转,所以我们将使用它来选择怪物的方向及其位置.
注意,必须使用 add_child()
将新实例添加到场景中.
GDScript
C#
func _on_MobTimer_timeout():
# Choose a random location on Path2D.
$MobPath/MobSpawnLocation.offset = randi()
# Create a Mob instance and add it to the scene.
var mob = Mob.instance()
add_child(mob)
# Set the mob's direction perpendicular to the path direction.
var direction = $MobPath/MobSpawnLocation.rotation + PI / 2
# Set the mob's position to a random location.
mob.position = $MobPath/MobSpawnLocation.position
# Add some randomness to the direction.
direction += rand_range(-PI / 4, PI / 4)
mob.rotation = direction
# Set the velocity (speed & direction).
mob.linear_velocity = Vector2(rand_range(mob.min_speed, mob.max_speed), 0)
mob.linear_velocity = mob.linear_velocity.rotated(direction)
public void OnMobTimerTimeout()
{
// Choose a random location on Path2D.
var mobSpawnLocation = GetNode<PathFollow2D>("MobPath/MobSpawnLocation");
mobSpawnLocation.Offset = _random.Next();
// Create a Mob instance and add it to the scene.
var mobInstance = (RigidBody2D)Mob.Instance();
AddChild(mobInstance);
// Set the mob's direction perpendicular to the path direction.
float direction = mobSpawnLocation.Rotation + Mathf.Pi / 2;
// Set the mob's position to a random location.
mobInstance.Position = mobSpawnLocation.Position;
// Add some randomness to the direction.
direction += RandRange(-Mathf.Pi / 4, Mathf.Pi / 4);
mobInstance.Rotation = direction;
// Choose the velocity.
mobInstance.LinearVelocity = new Vector2(RandRange(150f, 250f), 0).Rotated(direction);
}
重要
为什么使用 PI
?在需要角度的函数中,GDScript使用 弧度,而不是角度.如果您更喜欢使用角度,则需要使用 deg2rad()
和 rad2deg()
函数在角度和弧度之间进行转换.
测试场景
让我们测试这个场景,确保一切正常.将这段添加至 _ready()
:
GDScript
C#
func _ready():
randomize()
new_game()
public override void _Ready()
{
NewGame();
}
}
让我们同时指定 Main
作为我们的”主场景” —— 游戏启动时自动运行的场景. 按下”运行”按钮,当弹出提示时选择 Main.tscn
.
你应该可以四处移动游戏角色,看到可动对象生成,以及玩家被可动对象击中时会消失.
当你确定一切正常时,在``_ready()``中移除对``new_game()``的调用.
HUD
最后我们的游戏需要的是一个UI:一个显示诸如分数、 “游戏结束” 消息和重启按钮的界面.创建一个新的场景,并添加一个 CanvasLayer 节点,命名为 HUD
. “HUD “ 代表 “平视显示” ,是一种信息显示,以叠加的方式出现在游戏视图之上.
CanvasLayer 节点可以让我们在游戏的其他部分之上的一层绘制UI元素,这样它所显示的信息就不会被任何游戏元素(如玩家或暴徒)所覆盖.
HUD需要显示以下信息:
得分,由
ScoreTimer
更改.一条消息,例如
Game Over
或Get Ready!
一个
Start
按钮来开始游戏.
UI元素的基本节点是 Control.要创造UI,我们会使用 Control 的两种节点: Label 和 Button .
创建以下节点作为 HUD
的子节点:
点击 ScoreLabel
并在属性检查器的 Text
字段中键入一个数字.``Control`` 节点的默认字体很小,不能很好地缩放.游戏素材中包含一个字体文件( Xolonium-Regular.ttf
).要使用此字体,需要执行以下操作:
- 在 “Custom Fonts” 的下拉选项中,选择
新建DynamicFont
- 点击您添加的DynamicFont,然后在”Font/Font Data”的下拉选项中选择Load并选择Xolonium-Regular.ttf文件.您还必须设置字体的
Size
.设置为64
就可以了.
在” ScoreLabel”上完成此操作后,可以单击DynamicFont属性旁边的向下箭头,然后选择”复制”,然后将其”粘贴”到其他两个Control节点的相同位置.
注解
锚和边距: 控制
节点有一个位置和大小,但它们也有锚和边距.锚点定义了原点与节点边缘的参考点.当你移动或调整控制节点的大小时,边距会自动更新.它们表示从控制节点的边缘到其锚点的距离.更多细节请参见 使用 Control 节点设计界面.
按如下图所示排列节点.点击”布局”按钮以设置 一个Control 节点的布局:
您可以拖动节点以手动放置它们,或者要进行更精确的放置,请使用以下设置:
ScoreLabel
布局: “顶部宽度”
Text :
0
对齐: “居中”
Message
布局: “水平中心宽”
文本:
Dodge the Creeps!
对齐: “居中”
自动换行:”开”
StartButton
文本:
Start
布局: “中心底部”
边距:
顶部:
-200
底部:
-100
在 MessageTimer
中,将 Wait Time
设置为 2
并将 One Shot
属性设置为 “On”.
现将这个脚本添加到 HUD
:
GDScript
C#
extends CanvasLayer
signal start_game
public class HUD : CanvasLayer
{
// Don't forget to rebuild the project so the editor knows about the new signal.
[Signal]
public delegate void StartGame();
}
start_game
信号通知 Main
节点,按钮已经被按下.
GDScript
C#
func show_message(text):
$Message.text = text
$Message.show()
$MessageTimer.start()
public void ShowMessage(string text)
{
var message = GetNode<Label>("Message");
message.Text = text;
message.Show();
GetNode<Timer>("MessageTimer").Start();
}
当我们想要显示一条临时消息时,比如 Get Ready
,就会调用这个函数.
GDScript
C#
func show_game_over():
show_message("Game Over")
# Wait until the MessageTimer has counted down.
yield($MessageTimer, "timeout")
$Message.text = "Dodge the\nCreeps!"
$Message.show()
# Make a one-shot timer and wait for it to finish.
yield(get_tree().create_timer(1), "timeout")
$StartButton.show()
async public void ShowGameOver()
{
ShowMessage("Game Over");
var messageTimer = GetNode<Timer>("MessageTimer");
await ToSignal(messageTimer, "timeout");
var message = GetNode<Label>("Message");
message.Text = "Dodge the\nCreeps!";
message.Show();
await ToSignal(GetTree().CreateTimer(1), "timeout");
GetNode<Button>("StartButton").Show();
}
当 Player
输掉时调用这个函数.它将显示 Game Over
2秒,然后返回标题屏幕并显示 Start
按钮.
注解
当您需要暂停片刻时,可以使用场景树的 create_timer()
函数替代使用 Timer
节点.这对于延迟非常有用,例如在上述代码中,在这里我们需要在显示 开始
按钮前等待片刻.
GDScript
C#
func update_score(score):
$ScoreLabel.text = str(score)
public void UpdateScore(int score)
{
GetNode<Label>("ScoreLabel").Text = score.ToString();
}
每当分数改变,这个函数会被 Main
调用.
连接 MessageTimer
的 timeout()
信号和 StartButton
的 pressed()
信号并添加以下代码到新函数中:
GDScript
C#
func _on_StartButton_pressed():
$StartButton.hide()
emit_signal("start_game")
func _on_MessageTimer_timeout():
$Message.hide()
public void OnStartButtonPressed()
{
GetNode<Button>("StartButton").Hide();
EmitSignal("StartGame");
}
public void OnMessageTimerTimeout()
{
GetNode<Label>("Message").Hide();
}
将HUD场景连接到Main场景
现在我们完成了 HUD
场景,保存并返回 Main
场景.和 Player
场景的做法一样,在 Main
场景中实例化 HUD
场景.完整的场景树看起来应该像这样,确保您没有错过任何东西:
现在我们需要将 HUD
功能与我们的 Main
脚本连接起来.这需要在 Main
场景中添加一些内容:
在”节点”选项卡中,通过在”连接信号”窗口的”接收器方法”中键入 new_game
,将HUD的 `` start_game`` 信号连接到主节点的 `` new_game()`` 功能. 验证绿色的连接图标现在是否在脚本中的 func new_game()
旁边出现.
在 new_game()
函数中, 更新分数显示并显示 Get Ready
消息:
GDScript
C#
$HUD.update_score(score)
$HUD.show_message("Get Ready")
var hud = GetNode<HUD>("HUD");
hud.UpdateScore(_score);
hud.ShowMessage("Get Ready!");
在 game_over()
中我们需要调用相应的 HUD
函数:
GDScript
C#
$HUD.show_game_over()
GetNode<HUD>("HUD").ShowGameOver();
最后,将下面的代码添加到 _on_ScoreTimer_timeout()
以保持不断变化的分数的同步显示:
GDScript
C#
$HUD.update_score(score)
GetNode<HUD>("HUD").UpdateScore(_score);
现在您可以开始游戏了!点击 开始项目
按钮.将要求您选择一个主场景,因此选择 Main.tscn
.
删除旧的小怪
如果你一直玩到”游戏结束”,然后重新开始新游戏,上局游戏的小怪仍然显示在屏幕上.更好的做法是在新游戏开始时清除它们.我们需要一个同时让*所有*小怪删除它自己的方法,为此可以使用”分组”功能.
在 Mob
场景中,选择根节点,然后单击检查器旁边的” Node”选项卡(在该位置可以找到节点的信号). 在”信号”旁边,单击”分组”,然后可以输入新的组名称,然后单击”添加”.
现在,所有生物都将属于”生物(mobs)”组. 然后,我们可以将以下行添加到 Main
中的 game_over()
函数中:
GDScript
C#
get_tree().call_group("mobs", "queue_free")
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
. 这是与空格键关联的默认输入事件.
现在,当开始按钮出现时,您可以点击它或按 Space 来启动游戏.
项目文件
您可以在以下位置找到该项目的完整版本: