Attention: Here be dragons

This is the latest (unstable) version of this documentation, which may document features not available in or compatible with released stable versions of Godot.

GDScript 静态类型编程

在本指南中,你将学会:

  • 如何在 GDScript 中使用静态类型编程;

  • 静态类型编程可以帮助你避免问题;

  • 静态类型编程可以提升编辑器的使用体验。

这项语言功能的使用场合、使用方式完全取决于你:你可以只在部分敏感的 GDScript 文件中使用,也可以在所有地方都使用,甚至可以完全不使用。

静态类型编程可用于变量、常量、函数、参数和返回类型。

静态类型编程简介

使用 GDScript 静态类型编程,Godot 在编写代码时甚至可以帮你检测到更多代码错误,在你工作时为你和你的团队提供更多信息,当你调用方法时,会显示出参数的类型。静态类型编程也能改善编辑器的自动补全体验,其中也包括脚本的文档

Imagine you're programming an inventory system. You code an Item class, then an Inventory. To add items to the inventory, the people who work with your code should always pass an Item to the Inventory.add() method. With types, you can enforce this:

class_name Inventory


func add(reference: Item, amount: int = 1):
    var item := find_item(reference)
    if not item:
        item = _instance_item_from_db(reference)
    item.amount += amount

静态类型编程还能为你提供更好的代码补全选项。下面,你可以看到一个名为 PlayerController 类的动态类型和静态类型补全选项之间的区别。

你之前可能已经将节点存储在了变量中,打了一个句点符号,却没有代码自动补全提示:

动态类型的代码补全选项。

由于动态代码是动态的,因此 Godot 无法得知你传递给函数的值的类型。可如果你明确地声明了类型,则将从该节点类型获取所有公共方法和变量:

静态类型的代码补全选项。

小技巧

若偏向静态类型编程,建议开启编辑器选项文本编辑器 > 补全 > 添加类型提示,顺便也可以考虑开启默认关闭的某些选项

同时,在操作数/参数类型在编译时已知时,静态类型编程编写的 GDScript 代码还能通过优化后的操作码提升代码运行性能。未来还计划进行更多 GDScript 方面的优化,如 JIT/AOT 编译。

总体而言,静态类型编程可为你提供更加结构化的体验,有助于避免代码错误,改善脚本的文档生成能力。当你在团队中或长期项目中工作时,静态类型编程将会特别有用。研究表明,开发人员将大部分时间要么都花在阅读别人的代码上,要么都花在阅读他们以前编写过但后来忘掉的脚本上。代码越清晰、越结构化,开发人员理解得也就越快,项目开发的速度也就越快。

如何使用静态类型编程

To define the type of a variable, parameter, or constant, write a colon after the name, followed by its type. E.g. var health: int. This forces the variable's type to always stay the same:

var damage: float = 10.5
const MOVE_SPEED: float = 50.0
func sum(a: float = 0.0, b: float = 0.0) -> float:
    return a + b

Godot will try to infer types if you write a colon, but you omit the type:

var damage := 10.5
const MOVE_SPEED := 50.0
func sum(a := 0.0, b := 0.0) -> float:
    return a + b

备注

  1. 对于常量而言,=:= 没有区别。

  2. 常量不需要写类型提示,Godot 会自动根据所赋的值设置该常量的类型,你仍然可以写上类型提示来让代码更整洁。同时,这样写对于类型化数组也还是很有用的(比如 const A: Array[int] = [1, 2, 3]),因为默认使用的是无类型的数组。

类型提示可以是什么

下面列出的是所有可以用作类型提示的东西:

  1. Variant,任何类型。大多数情况下与不写类型声明差不多,但能够增加可读性。作为返回类型时,能够强制函数显式返回值。

  2. (仅作返回类型使用) void。表示函数不返回任何值。

  3. 内置类型

  4. 原生类(ObjectNodeArea2DCamera2D 等)。

  5. 全局类

  6. 内部类

  7. 全局具名常量与内部具名常量。注意:枚举是 int 类型的数据,不能保证一个值属于该枚举。

  8. 包含预加载类和枚举的常量(或局部常量)。

You can use any class, including your custom classes, as types. There are two ways to use them in scripts. The first method is to preload the script you want to use as a type in a constant:

const Rifle = preload("res://player/weapons/rifle.gd")
var my_rifle: Rifle

The second method is to use the class_name keyword when you create. For the example above, your rifle.gd would look like this:

class_name Rifle
extends Node2D

If you use class_name, Godot registers the Rifle type globally in the editor, and you can use it anywhere, without having to preload it into a constant:

var my_rifle: Rifle

使用箭头 -> 来定义函数的返回类型

To define the return type of a function, write a dash and a right angle bracket -> after its declaration, followed by the return type:

func _process(delta: float) -> void:
    pass

The type void means the function does not return anything. You can use any type, as with variables:

func hit(damage: float) -> bool:
    health_points -= damage
    return health_points <= 0

You can also use your own classes as return types:

# Adds an item to the inventory and returns it.
func add(reference: Item, amount: int) -> Item:
    var item: Item = find_item(reference)
    if not item:
        item = ItemDatabase.get_instance(reference)

    item.amount += amount
    return item

协变与逆变

继承基类方法时,应遵循里氏代换原则

协变:继承方法时,你可以为子类方法指定一个比该子类方法的父类方法更为具体的返回值类型(子类型)。

逆变:继承方法时,你可以为子类方法指定一个比该子类方法的父类方法更不具体的参数类型(超类型)。

示例:

class_name Parent


func get_property(param: Label) -> Node:
    # ...
class_name Child extends Parent


# `Control` is a supertype of `Label`.
# `Node2D` is a subtype of `Node`.
func get_property(param: Control) -> Node2D:
    # ...

指定 Array 的元素类型

要定义 Array 的类型,请将类型名称填写在 [] 内。

An array's type applies to for loop variables, as well as some operators like [], []=, and +. Array methods (such as push_back) and other operators (such as ==) are still untyped. Built-in types, native and custom classes, and enums may be used as element types. Nested array types (like Array[Array[int]]) are not supported.

var scores: Array[int] = [10, 20, 30]
var vehicles: Array[Node] = [$Car, $Plane]
var items: Array[Item] = [Item.new()]
var array_of_arrays: Array[Array] = [[], []]
# var arrays: Array[Array[int]] -- disallowed

for score in scores:
    # score has type `int`

# The following would be errors:
scores += vehicles
var s: String = scores[0]
scores[0] = "lots"

Since Godot 4.2, you can also specify a type for the loop variable in a for loop. For instance, you can write:

var names = ["John", "Marta", "Samantha", "Jimmy"]
for name: String in names:
    pass

数组仍旧不会限定类型,但 for 循环的 name 循环变量则始终为 String 类型。

Specify the element type of a Dictionary

To define the type of a Dictionary's keys and values, enclose the type name in [] and separate the key and value type with a comma.

A dictionary's value type applies to for loop variables, as well as some operators like [] and []=. Dictionary methods that return values and other operators (such as ==) are still untyped. Built-in types, native and custom classes, and enums may be used as element types. Nested typed collections (like Dictionary[String, Dictionary[String, int]]) are not supported.

var fruit_costs: Dictionary[String, int] = { "apple": 5, "orange": 10 }
var vehicles: Dictionary[String, Node] = { "car": $Car, "plane": $Plane }
var item_tiles: Dictionary[Vector2i, Item] = { Vector2i(0, 0): Item.new(), Vector2i(0, 1): Item.new() }
var dictionary_of_dictionaries: Dictionary[String, Dictionary] = { { } }
# var dicts: Dictionary[String, Dictionary[String, int]] -- disallowed

for cost in fruit_costs:
    # cost has type `int`

# The following would be errors:
fruit_costs["pear"] += vehicles
var s: String = fruit_costs["apple"]
fruit_costs["orange"] = "lots"

类型转换

类型转换是类型语言的关键概念,转型是指将值从一种类型转换为另一种类型的操作或过程。

想象你的游戏中的一个敌人,extends Area2D。你希望它与游戏角色,即一个附带有一个名为 PlayerController 的脚本的 CharacterBody2D, 碰撞,那么你可以使用 body_entered 信号来检测碰撞。使用类型化代码,其检测到的物体(body)将是通用的 PhysicsBody2D 而非 _on_body_entered 回调上使用的 PlayerController

You can check if this PhysicsBody2D is your Player with the as keyword, and using the colon : again to force the variable to use this type. This forces the variable to stick to the PlayerController type:

func _on_body_entered(body: PhysicsBody2D) -> void:
    var player := body as PlayerController
    if not player:
        return

    player.damage()

在处理自定义类型时,如果 body 没有继承 PlayerController 类,则 player 变量将被赋值为 null。我们可以用这种操作来检查物体是否为游戏玩家角色。多亏了类型转换,我们还能获得 player 变量的代码自动补全功能。

备注

The as keyword silently casts the variable to null in case of a type mismatch at runtime, without an error/warning. While this may be convenient in some cases, it can also lead to bugs. Use the as keyword only if this behavior is intended. A safer alternative is to use the is keyword:

if not (body is PlayerController):
    push_error("Bug: body is not PlayerController.")

var player: PlayerController = body
if not player:
    return

player.damage()

You can also simplify the code by using the is not operator:

if body is not PlayerController:
    push_error("Bug: body is not PlayerController")

Alternatively, you can use the assert() statement:

assert(body is PlayerController, "Bug: body is not PlayerController.")

var player: PlayerController = body
if not player:
    return

player.damage()

备注

如果你尝试使用内置类型进行转型且转型失败,则将触发 Godot 脚本编辑器底部报错。

安全行

你也可以使用转型语法来确保存在安全行,安全行是 Godot 3.1 中加入的新工具,可以告诉你一行歧义代码在什么情况下类型安全。由于你有时会混合使用静态类型代码和动态类型代码,有时如果指令在运行时触发错误,Godot 可能没有足够的信息进行判断。

当你需要获得子节点时就会发生这种情况。以计时器为例:使用动态代码,你可以使用 $Timer 获取节点。GDScript 支持鸭子类型,即使你的计时器是 Timer 类型,计时器也继承了 NodeObject 这两个类。使用动态类型的 GDScript,只要节点具有你需要调用的方法,你也不必关心节点的类型。

当你得到一个节点时,可以使用强制转型来告诉 Godot 你所期望的类型: ($Timer as Timer)($Player as KinematicBody2D) 等,Godot 将确认该类型是否有效,如果有效,在脚本编辑器的左侧的行号将会变为绿色。

不安全行 vs 安全行

不安全代码行(第 7 行)vs 安全代码行(第 6 行和第 8 行)

备注

Safe lines do not always mean better or more reliable code. See the note above about the as keyword. For example:

@onready var node_1 := $Node1 as Type1 # Safe line.
@onready var node_2: Type2 = $Node2 # Unsafe line.

虽然 node_2 的声明被标记为了不安全行,但比起 node_1 的声明来看却更加可靠,这是因为如果你不小心在场景中更改了节点的类型,又忘记把它改回去的话,就会在场景加载时触发报错。node_1 的情况则是会被静默转型成 null ,触发报错。

备注

可以在编辑器设置中关闭安全行或更改其颜色。

静态编程还是动态编程:坚持一种风格

静态类型的 GDScript 和动态类型的 GDScript 可共存于同一项目,但还是建议二选其一,以确保每个人代码风格的一致性。如果你们遵循相同的规范,那么每个人就都可以更轻松地协作,阅读、理解他人的代码也就会更加迅速。

Typed code takes a little more writing, but you get the benefits we discussed above. Here's an example of the same, empty script, in a dynamic style:

extends Node


func _ready():
    pass


func _process(delta):
    pass

And with static typing:

extends Node


func _ready() -> void:
    pass


func _process(delta: float) -> void:
    pass

As you can see, you can also use types with the engine's virtual methods. Signal callbacks, like any methods, can also use types. Here's a body_entered signal in a dynamic style:

func _on_area_2d_body_entered(body):
    pass

And the same callback, with type hints:

func _on_area_2d_body_entered(body: PhysicsBody2D) -> void:

pass

警告系统

备注

关于 GDScript 警告系统的文档已移至 GDScript 警告系统

Godot gives you warnings about your code as you write it. The engine identifies sections of your code that may lead to issues at runtime, but lets you decide whether or not you want to leave the code as it is.

对于静态类型 GDScript 编程的用户,我们提供了大量警告,这些警告默认关闭,但你可以将这些警告在项目设置(调试 > GDScript)中开启,在此之前,请确保你已启用了项目设置的**高级设置**模式。

若始终进行静态类型编程,你可以启用 UNTYPED_DECLARATION 警告。此外,你还可以启用 INFERRED_DECLARATION 警告来让你的代码可读性更强、更有可靠性,但同时也会让你的代码更加冗长。

UNSAFE_* 警告会让不安全操作比不安全行更容易引人注意。目前, UNSAFE_* 警告并不能涵盖不安行所涵盖的所有情况。

常见的不安全操作及其安全操作

UNSAFE_PROPERTY_ACCESSUNSAFE_METHOD_ACCESS 警告

In this example, we aim to set a property and call a method on an object that has a script attached with class_name MyScript and that extends Node2D. If we have a reference to the object as a Node2D (for instance, as it was passed to us by the physics system), we can first check if the property and method exist and then set and call them if they do:

if "some_property" in node_2d:
    node_2d.some_property = 20  # Produces UNSAFE_PROPERTY_ACCESS warning.

if node_2d.has_method("some_function"):
    node_2d.some_function()  # Produces UNSAFE_METHOD_ACCESS warning.

However, this code will produce UNSAFE_PROPERTY_ACCESS and UNSAFE_METHOD_ACCESS warnings as the property and method are not present in the referenced type - in this case a Node2D. To make these operations safe, you can first check if the object is of type MyScript using the is keyword and then declare a variable with the type MyScript on which you can set its properties and call its methods:

if node_2d is MyScript:
    var my_script: MyScript = node_2d
    my_script.some_property = 20
    my_script.some_function()

Alternatively, you can declare a variable and use the as operator to try to cast the object. You'll then want to check whether the cast was successful by confirming that the variable was assigned:

var my_script := node_2d as MyScript
if my_script != null:
    my_script.some_property = 20
    my_script.some_function()

UNSAFE_CAST 警告

In this example, we would like the label connected to an object entering our collision area to show the area's name. Once the object enters the collision area, the physics system sends a signal with a Node2D object, and the most straightforward (but not statically typed) solution to do what we want could be achieved like this:

func _on_body_entered(body: Node2D) -> void:
    body.label.text = name  # Produces UNSAFE_PROPERTY_ACCESS warning.

This piece of code produces an UNSAFE_PROPERTY_ACCESS warning because label is not defined in Node2D. To solve this, we could first check if the label property exist and cast it to type Label before settings its text property like so:

func _on_body_entered(body: Node2D) -> void:
    if "label" in body:
        (body.label as Label).text = name  # Produces UNSAFE_CAST warning.

However, this produces an UNSAFE_CAST warning because body.label is of a Variant type. To safely get the property in the type you want, you can use the Object.get() method which returns the object as a Variant value or returns null if the property doesn't exist. You can then determine whether the property contains an object of the right type using the is keyword, and finally declare a statically typed variable with the object:

func _on_body_entered(body: Node2D) -> void:
    var label_variant: Variant = body.get("label")
    if label_variant is Label:
        var label: Label = label_variant
        label.text = name

不能指定类型的情况

最后,我们将介绍一些不能使用类型提示的情况。以下所有示例 都会触发报错

  1. You can't specify the type of individual elements in an array or a dictionary:

var enemies: Array = [$Goblin: Enemy, $Zombie: Enemy]
var character: Dictionary = {
    name: String = "Richard",
    money: int = 1000,
    inventory: Inventory = $Inventory,
}
  1. Nested types are not currently supported:

var teams: Array[Array[Character]] = []

总结

静态类型的 GDScript 是一个十分强大的工具,可以帮助编写更多结构化的代码,避免常见错误,创建灵活的代码系统。将来,由于即将进行的编译器优化,静态类型也将会带来不错的性能提升。