编写脚本(续)

处理

Godot中的多个动作是由回调或虚函数触发的, 因此无需编写始终运行的代码.

但是, 在每一帧上都需要处理脚本仍然很常见. 有两种处理类型: 空闲处理和物理处理.

当在脚本中找到 Node._process() 方法时, 将激活空闲处理. 可以通过 Node.set_process() 函数来打开或关闭.

这个方法将在每次绘制帧时被调用:

func _process(delta):
    # Do something...
    pass
public override void _Process(float delta)
{
    // Do something...
}

重要的是要记住, 调用 _process() 的频率取决于应用程序运行的每秒帧数(FPS). 该速率会随着时间和设备的不同而变化.

为了管理这种可变性有所帮助,``delta`` 参数包含自上次调用 _process() 以来经过的时间, 以秒为单位的浮点数表示.

此参数可用于确保事物始终花费相同的时间, 从而与游戏的FPS无关.

例如, 运动经常与时间增量相乘, 以使运动速度既恒定又独立于帧速率.

_physics_process() 进行物理处理是相似的, 但应将其用于必须在每个物理步骤之前进行的处理, 例如控制角色. 它始终在物理步骤之前运行, 并且以固定的时间间隔调用: 默认为每秒60次. 您可以从 "项目设置" 的 "物理" ->"通用" ->"物理Fps" 下更改时间间隔.

然而, 函数 _process() 不与物理同步. 它的帧率不是恒定的, 并且取决于硬件和游戏优化. 在单线程游戏中它的执行是在的物理步骤之后完成的.

在作品中查看 _process() 函数的一种简单方法是创建具有单个Label节点的且带有以下脚本的场景:

extends Label

var accum = 0

func _process(delta):
    accum += delta
    text = str(accum) # 'text' is a built-in label property.
public class CustomLabel : Label
{
    private float _accum;

    public override void _Process(float delta)
    {
        _accum += delta;
        Text = _accum.ToString(); // 'Text' is a built-in label property.
    }
}

这将显示一个每帧增加的计数器.

分组

Godot中的编组的工作方式类似于您可能在其他软件中遇到的标记. 一个节点可以根据需要添加到任意多个编组. 这是组织大型场景的一个有用特性. 有两种方法可以向编组中添加节点. 第一个是从UI, 使用 节点 面板下的 Groups 按钮:

../../_images/groups_in_nodes.png

第二种方法是从代码. 下面的脚本会在当前节点出现在场景树中后立即将其添加到 enemies 编组中.

func _ready():
    add_to_group("enemies")
public override void _Ready()
{
    base._Ready();

    AddToGroup("enemies");
}

这样, 如果发现玩家潜入秘密基地, 则可以使用 SceneTree.call_group() 发出警报声来通知所有敌人:

func _on_discovered(): # This is a purely illustrative function.
    get_tree().call_group("enemies", "player_was_discovered")
public void _OnDiscovered() // This is a purely illustrative function.
{
    GetTree().CallGroup("enemies", "player_was_discovered");
}

上面的代码在 enemies 编组的每个成员上调用函数 player_was_discovered.

也可以通过调用 SceneTree.get_nodes_in_group() 获得 enemies 节点的完整列表:

var enemies = get_tree().get_nodes_in_group("enemies")
var enemies = GetTree().GetNodesInGroup("enemies");

SceneTree 类提供了许多有用的方法, 例如与场景, 其节点层次结构, 及节点编组交互. 它使您可以轻松切换场景或重新加载场景, 退出游戏或暂停和取消暂停游戏. 它还有一些有趣的信号, 如果您有空可以详细去查看!

通知

Godot 有一个通知系统. 这个是非常底层的虚函数, 通常不需要放入脚本代码里. 只需要知道有这个系统的存在. 例如, 您可以在您的脚本里添加 Object._notification() 函数:

func _notification(what):
    match what:
        NOTIFICATION_READY:
            print("This is the same as overriding _ready()...")
        NOTIFICATION_PROCESS:
            print("This is the same as overriding _process()...")
public override void _Notification(int what)
{
    base._Notification(what);

    switch (what)
    {
        case NotificationReady:
            GD.Print("This is the same as overriding _Ready()...");
            break;
        case NotificationProcess:
            var delta = GetProcessDeltaTime();
            GD.Print("This is the same as overriding _Process()...");
            break;
    }
}

类参考手册 每个类的文档里都说明了它们能接收的通知. 不过,GDScript在大多数情况下都提供了更简单的可重写函数.

可重载函数

如下所述的这些可重载函数, 可以应用于节点:

func _enter_tree():
    # When the node enters the Scene Tree, it becomes active
    # and  this function is called. Children nodes have not entered
    # the active scene yet. In general, it's better to use _ready()
    # for most cases.
    pass

func _ready():
    # This function is called after _enter_tree, but it ensures
    # that all children nodes have also entered the Scene Tree,
    # and became active.
    pass

func _exit_tree():
    # When the node exits the Scene Tree, this function is called.
    # Children nodes have all exited the Scene Tree at this point
    # and all became inactive.
    pass

func _process(delta):
    # This function is called every frame.
    pass

func _physics_process(delta):
    # This is called every physics frame.
    pass
public override void _EnterTree()
{
    // When the node enters the Scene Tree, it becomes active
    // and  this function is called. Children nodes have not entered
    // the active scene yet. In general, it's better to use _ready()
    // for most cases.
    base._EnterTree();
}

public override void _Ready()
{
    // This function is called after _enter_tree, but it ensures
    // that all children nodes have also entered the Scene Tree,
    // and became active.
    base._Ready();
}

public override void _ExitTree()
{
    // When the node exits the Scene Tree, this function is called.
    // Children nodes have all exited the Scene Tree at this point
    // and all became inactive.
    base._ExitTree();
}

public override void _Process(float delta)
{
    // This function is called every frame.
    base._Process(delta);
}

public override void _PhysicsProcess(float delta)
{
    // This is called every physics frame.
    base._PhysicsProcess(delta);
}

如前所述, 最好使用这些函数代替通知系统.

创建节点

要通过代码创建节点, 请像其他任何基于类的数据类型一样, 调用 .new() 方法. 例如:

var s
func _ready():
    s = Sprite.new() # Create a new sprite!
    add_child(s) # Add it as a child of this node.
private Sprite _sprite;

public override void _Ready()
{
    base._Ready();

    _sprite = new Sprite(); // Create a new sprite!
    AddChild(_sprite); // Add it as a child of this node.
}

要删除节点, 无论是在场景内还是场景外, 都必须使用 free():

func _someaction():
    s.free() # Immediately removes the node from the scene and frees it.
public void _SomeAction()
{
    _sprite.Free(); // Immediately removes the node from the scene and frees it.
}

当一个节点被释放时, 它也会释放其所有子节点. 因此, 手动删除节点比看起来简单得多. 释放基节点, 那么子树中的其他所有东西都会随之消失.

当我们要删除当前 "阻塞" 的节点时, 可能会发生这种情况, 因为该节点正在发出信号或正在调用函数. 这会导致游戏崩溃. 使用调试器运行Godot通常能捕获这种情况并向您发出警告.

删除节点的最安全方法是使用 Node.queue_free(). 这将在空闲期间安全地删除节点.

func _someaction():
    s.queue_free() # Removes the node from the scene and frees it when it becomes safe to do so.
public void _SomeAction()
{
    _sprite.QueueFree(); // Removes the node from the scene and frees it when it becomes safe to do so.
}

实例化场景

从代码实例化场景分两个步骤完成. 第一步是从硬盘驱动器加载场景:

var scene = load("res://myscene.tscn") # Will load when the script is instanced.
var scene = GD.Load<PackedScene>("res://myscene.tscn"); // Will load when the script is instanced.

预加载可以更方便, 因为它是在解析时发生的(仅适用于GDScript):

var scene = preload("res://myscene.tscn") # Will load when parsing the script.

但是 scene 还不是一个节点. 它被打包在一个称为 PackedScene 的特殊资源中. 要想创建实际的节点, 就必须调用函数 PackedScene.instance(). 这将返回可以添加到活动场景的节点树:

var node = scene.instance()
add_child(node)
var node = scene.Instance();
AddChild(node);

此两步过程的优点在于, 打包的场景可以保持加载状态并可以随时使用, 以便您可以根据需要创建尽可能多的实例. 这对于在活动场景中快速实例化多个敌人, 子弹, 和其他实体特别有用.

将脚本注册为类

Godot有一个 "脚本类" 的功能, 可以使用编辑器注册单个脚本. 默认情况下, 您只能通过直接加载文件访问未命名的脚本.

You can name a script and register it as a type in the editor with the class_name keyword followed by the class's name. You may add a comma and an optional path to a PNG or SVG image to use as an icon (16×16 minimum, 32×32 recommended). You will then find your new type in the Node or Resource creation dialog. Note that the icon will only appear after restarting the editor.

extends Node

# Declare the class name here
class_name ScriptName, "res://path/to/optional/icon.svg"

func _ready():
    var this = ScriptName           # reference to the script
    var cppNode = MyCppNode.new()   # new instance of a class named MyCppNode

    cppNode.queue_free()
../../_images/script_class_nativescript_example.png

警告

在Godot 3.1中:

  • 只有GDScript和NativeScript, 即C ++和其他GDNative支持的语言, 可以注册脚本.

  • 只有GDScript为每个命名脚本创建全局变量.