编写脚本

简介

在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桥接,因此生成的共享库使用的混合编译器品牌和版本可以完美地工作。

This language is the best choice for performance and does not need to be used throughout an entire game, as other parts can be written in GDScript or Visual Script. However, the API is clear and easy to use as it resembles, mostly, Godot's actual C++ API.

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

编写场景脚本

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

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

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

场景设置

Use the "Add Child Node" dialogue accessed from the Scene tab (or by pressing Ctrl + A) to create a hierarchy with the following nodes:

  • 面板
    • 标签
    • 按钮

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

../../_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!";
}

最后,使用 Object.connect() 将按钮的“按下”信号连接到 _ready()

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 方法以获取更多信息。