编写脚本

简介

在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 参考. 这是一种设计得很简单的语言, 并且该参考文献各部分, 以使您可以更容易地获得概念的概述.

场景设置

如果上一教程的 "实例化" 项目仍然开着, 请将它关闭("项目" ->"退出到项目列表")并创建一个新项目.

使用从"添加子场景/Add Child Node"选项访问的" 添加子节点"对话框(或通过按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 下面的所有内容. 您将手动填写脚本的其余部分.

由于Button和Label是有脚本附加的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() 将按钮的 "pressed" 信号连接到 _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的子项. 如果Button是Label的子项, 则获得它的代码是:

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