编写脚本

简介

在Godot 3.0版本之前,编写游戏脚本的唯一选择是使用 GDScript。如今,Godot已经有四种(没错,是四种!)官方语言,并具有动态添加额外的脚本语言的能力!

这很好,主要在于提供了大量的灵活性,但也使得我们对语言的支持工作变得更加困难。

不过,Godot中的“主要”语言是GDScript和VisualScript。选择它们的主要原因是它们与Godot的整合程度,这使得体验更佳顺畅;两者都有非常好的编辑器集成,而C#和C++需要在外部IDE中进行编辑。如果您是静态语言的忠实粉丝,也可以使用C#和C++。

GDScript

如上所述,GDScript 是Godot使用的主要语言。由于与Godot的高度集成,使用它与其他语言相比有一些积极意义:

  • 简单,优雅,设计上为Lua、Python、Squirrel等语言用户所熟悉。
  • 快速加载和编译。
  • 编辑器集成非常令人愉快,有属于被编辑场景的节点、信号、以及其它条目的代码补全功能。
  • 有内建的矢量类型(比如Vector、Transform等),大量使用线性代数时非常有效。
  • 像静态类型语言一样高效地支持多线程——这是我们避免使用诸如Lua、Squirrel等虚拟机的原因之一。
  • 不使用垃圾回收器,因此它通过确定性,交换自动处理的一小部分(大多数对象都是参考计数)。
  • 如果需要更高的性能,它的动态特性可以很容易地优化C ++中的代码段(通过GDNative),而无需重新编译引擎。

如果还没决定好,并且对编程有经验,特别是动态类型语言,那就选择GDScript吧!

可视化脚本

从3.0开始,Godot开始支持 可视化脚本。这是“节点和连线”语言的典型实现,但适用于Godot的开发方式。

对非程序员来说,可视化编程是一个很好的工具,甚至对于经验丰富的开发人员来说也是如此,他们想让部分代码易于被他人访问,比如游戏设计师和美术工作者。

程序员也可以用它创建状态机或自定义可视节点工作流程,例如对话系统。

.NET / C#

由于微软的C#是游戏开发者的最爱,我们增加了官方支持。C#是一种成熟的语言,人们为它编写了大量代码,而且支持增加得益于微软的慷慨捐赠。

它在性能和易用性之间有很好的折衷,尽管必须注意它的垃圾回收器。

由于Godot使用 Mono .NET运行时,因此理论上任何第三方.NET库或框架都可用于编写Godot脚本,以及任何符合通用语言标准架构的语言,如F#、Boo或ClojureCLR。但实际上,C#是唯一官方支持的.NET选项。

GDNative / C++

最后是3.0版本的一个亮点:GDNative允许使用C++编写脚本,无需重新编译(甚至都不用重启)Godot。

任意C++版本都可以使用,并且由于我们使用内部C API桥接,因此生成的共享库使用的混合编译器品牌和版本可以完美地工作。

这种语言是性能上的最佳选择,它不需要在整个游戏中都使用,因为其他部分可以用GDScript或Visual Script编写。它的API清晰且易于使用,类似于Godot的实际C++ API。

通过GDNative接口可以使用更多语言,但请记住我们没有官方支持。

编写场景脚本

在教程剩下的部分里,我们将建立一个GUI场景,包含一个按钮和一个标签,当按下按钮时会更新标签。这将会演示:

  • 编写脚本并附加到节点上。
  • 通过信号来连接到UI元素。
  • 编写脚本,使其能访问场景中的其它节点。

在继续之前,请务必阅读 GDScript 参考手册。它被设计为一种简单的语言,参考手册很简短,因此概览这些概念并不需要花费太长时间。

场景设置

使用从场景选项卡访问的“添加子节点”对话框(或按 Ctrl+A)创建具有以下节点的层次结构:

  • 面板
    • 标签
    • 按钮

场景树看起来应该是这个样子:

../../_images/scripting_scene_tree.png

使用2D编辑器定位并调整按钮和标签的大小,使其看起来像下面的样子。可以在 属性检查器(Inspector) 选项卡中设置文本。

../../_images/label_button_example.png

最后,保存场景,用例如 sayhello.tscn 这样的名字。

添加脚本

右击面板(Panel)节点, 在弹出菜单中选择 附加脚本(Attach Script)

../../_images/add_script.png

脚本创建对话框会弹出。这个对话框允许您设置脚本语言、类名和其它相关选项。

在GDScript里,文件本身代表了类,所以类名一栏无法编辑。

我们要附加脚本的节点是个Panel(面板),所以继承一栏会被自动填入``Panel``。这是我们想要的,因为脚本的目的就是扩展这个面板节点的功能。

最后,输入脚本的路径名然后选择新建(Create):

../../_images/script_create.png

然后将创建脚本并将其添加到节点上。您可以在“场景”选项卡中的节点旁边的“打开脚本”图标,以及“属性检查器”下的脚本属性中看到:

../../_images/script_added.png

要编辑脚本,请选择这两个按钮中的一个,在上图中两个按钮都被高亮突出显示。这将带您到脚本编辑器,其中将包含默认模板:

../../_images/script_template.png

这里没有多少内容。_ready() 函数在节点及其所有子节点进入活动场景时被调用。注意: _ready() 并不是构造器;构造器是 _init()

脚本的作用

脚本为节点添加行为。它用于控制节点的功能以及与其他节点(子节点,父节点,兄弟姐妹等)的交互方式。脚本的局部作用域是节点。换句话说,脚本继承了该节点提供的功能。

../../_images/brainslug.jpg

处理信号

当某种特定类型的动作发生时,信号被“发出”,并且它们可以连接到任意脚本实例的任意函数。信号主要用于GUI节点,尽管其他节点也有信号,您甚至可以在自己的脚本中定义自定义信号。

在此步骤中,我们将“按下”信号连接到自定义函数。形成连接是第一部分,定义自定义功能是第二部分。对于第一部分,Godot提供了两种创建连接的方式:通过编辑器提供的可视界面、或通过代码。

虽然在本系列教程的其余部分中,我们将使用代码方法,但现在还是让我们介绍一下编辑器界面的工作方式,以备将来参考。

在场景树中选择 Button 节点,然后选择 节点(Node) 选项卡。接下来,确保已选择 信号(Signals)

../../_images/signals.png

如果稍后在 BaseButton 下选择 pressed() 并单击右下角的 Connect ... 按钮,则将打开连接创建对话框。

../../_images/connect_dialogue.png

对话框的顶部显示了场景的节点列表,其中发射节点的名称会以蓝色突出显示。在这里选择“面板”节点。

对话框的底部显示了将要创建的方法的名称。方法名字默认会包含发出节点的名字(在当前例子中为“Button”),即:“_on_[EmitterNode]_[signal_name]”。

至此,已经介绍了有关如何使用可视界面的指南。但是,这是一个脚本教程,因此,为了学习起见,让我们深入研究手动过程!

为此,我们将介绍一个可能是Godot程序员最常使用的函数: Node.get_node()。此函数使用场景中相对于拥有脚本的节点的节点路径来获取节点。

为了方便起见,删除 extends Panel 下面的所有内容。您将手动填写脚本的其余部分。

由于 ButtonLabel 是有脚本附加的 Panel 下的兄弟节点,因此可以通过在 _ready() 函数下输入以下内容来获取 Button 引用:

func _ready():
    get_node("Button")
public override void _Ready()
{
    GetNode("Button");
}

接下来,编写一个在按下按钮时将被调用的函数:

func _on_Button_pressed():
    get_node("Label").text = "HELLO!"
public void _OnButtonPressed()
{
    GetNode<Label>("Label").Text = "HELLO!";
}

Finally, connect the button’s “pressed” signal to _on_Button_pressed() by using Object.connect().

func _ready():
    get_node("Button").connect("pressed", self, "_on_Button_pressed")
public override void _Ready()
{
    GetNode("Button").Connect("pressed", this, nameof(_OnButtonPressed));
}

脚本最终看起来是这个样子:

extends Panel

func _ready():
    get_node("Button").connect("pressed", self, "_on_Button_pressed")

func _on_Button_pressed():
    get_node("Label").text = "HELLO!"
using Godot;

// IMPORTANT: the name of the class MUST match the filename exactly.
// this is case sensitive!
public class sayhello : Panel
{
    public override void _Ready()
    {
        GetNode("Button").Connect("pressed", this, nameof(_OnButtonPressed));
    }

    public void _OnButtonPressed()
    {
        GetNode<Label>("Label").Text = "HELLO!";
    }
}

运行场景并按下按钮,您应该得到以下结果:

../../_images/scripting_hello.png

您好啊!恭喜您完成了您的第一个场景脚本。

注解

关于本教程的一个常见的误解是 get_node(path) 的工作原理。对于给定的节点,get_node(path) 搜索其直接子节点。在上面的代码中,这意味着 Button 必须是 Panel 的子项。如果 ButtonLabel 的子项,则获得它的代码是:

# Not for this case,
# but just in case.
get_node("Label/Button")
// Not for this case,
// but just in case.
GetNode("Label/Button")

另外,请记住,节点是通过名称而不是类型来引用的。

注解

连接对话框的右侧面板用于将特定值绑定到所连接函数的参数。您可以添加和删除不同类型的值。

代码方法还使用第四个 Array 参数启用此功能,该参数默认为空。随时阅读 Object.connect 方法以获取更多信息。