GDScript 中静态类型

在本指南中,您将学会:

  • 如何在GDScript中使用类型
  • 这种 静态类型可以帮助您避免bug

在何处以及如何使用此新语言功能完全取决于您:您可以只在某些敏感的GDScript文件中使用它,也可以在任何地方使用它,或者像往常一样编写代码!

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

注解

从Godot 3.1开始,可以使用类型化的GDScript。

静态类型简介

使用类型化的GDScript,在编写代码时Godot可以检测到更多错误!它会在您工作时为您和您的队友提供更多信息,因为当您调用方法时,参数的类型会显示出来。

想象一下你正在编写一个库存系统。您编写一个 Item 节点,然后再编写一个 Inventory 。要将项目添加到清单中,使用代码的人员应始终将 Item 传递给 Inventory.add 方法。使用类型,您可以强制执行以下操作:

# In Item.gd
class_name Item

# In Inventory.gd
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

类型化的GDScript的另一个显著优点是新的 警告系统 。从版本3.1开始,Godot会在您编写代码时向您发出有关代码的警告:引擎会识别代码中可能导致运行时出现问题的部分,但您可以决定是否要保留代码。稍后详细介绍。

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

您之前可能已经将节点存储在变量中,并输入了一个没有自动完成建议的点:

code completion options for dynamic

这是因为动态代码。Godot无法知道您将什么节点或值类型传递给了函数。但是,如果您显式写出了类型,则将从该节点获取所有公共方法和变量:

code completion options for typed

将来,类型化的GDScript也将提高代码性能:实时编译和其他编译器改进已经出现在路线图上!

总体而言,类型化编程可为您提供更加结构化的体验。它有助于防止错误并改善脚本的自我描述方面。当您在团队中或长期项目中工作时,这特别有用:研究表明,开发人员将大部分时间都花在阅读别人的代码或他们过去编写并忘记的脚本上。代码越清晰,越结构化,理解得就越快,前进的速度就越快。

如何使用静态类型

要定义变量或常量的类型,请在变量名称后加上一个冒号,后跟它的类型。例如 var health: int 。这将强制变量的类型始终保持不变:

var damage: float = 10.5
const MOVE_SPEED: float = 50.0

如果您写冒号但省略写类型时,Godot将尝试推测类型:

var life_points := 4
var damage := 10.5
var motion := Vector2()

当前,您可以使用三类…类型:

  1. 内置类型
  2. 核心类和节点( ObjectNodeArea2DCamera2D、等)
  3. 您自己定义的类。查看新的 class_name 特性,以便在编辑器中注册类型。

注解

您不需要为常量编写类型提示,因为Godot会根据分配的值自动设置。但是您仍然可以这样做,以使代码的意图更加清晰。

自定义变量类型

您可以将任何类(包括您的自定义类)用作类型。有两种在脚本中使用它们的方法。第一种方法是将要用作类型的脚本预加载为常量:

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

第二种方法是在创建时使用 class_name 关键字。对于上面的示例,您的 Rifle.gd 看起来就像这样:

extends Node2D
class_name Rifle

如果使用 class_name ,Godot会在编辑器中注册一个全局 Rifle 类型,您可以在任何地方使用它而无需将其预加载到常量中:

var my_rifle: Rifle

变量转换

类型转换是类型语言的关键概念。转换是将值从一种类型转换为另一种类型。

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

您可以使用 as 转换关键字检查这个 PhysicsBody2D 是否是您的游戏角色,并再次使用冒号 : 来强制变量使用这种类型。这会强制变量坚持使用 PlayerController 类型:

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

在处理自定义类型时,如果 body 没有扩展 PlayerController,则 player 变量将被设置为 null。我们可以用它来检查物体是否是游戏角色。幸亏类型转换,我们还将获得 player 变量的完整自动补全功能。

注解

如果您尝试使用内置类型进行转换并失败,则Godot将引发错误。

安全行

您也可以使用转换来确保安全行。安全行是Godot 3.1中的新工具,可以告诉您歧义的代码行何时是类型安全的。由于您有时会混合和匹配类型化的代码和动态代码,因此Godot没有足够的信息来知道指令在运行时是否会触发错误。

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

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

Safe vs Unsafe Line

安全与不安全行

注解

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

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

要定义函数的返回类型,请在声明后写一个短划线和一个右尖括号 ->,后跟返回类型:

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

类型 void 表示函数不返回任何内容。您可以使用任何类型,如变量:

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

您还可以使用自己的节点作为返回类型:

# Inventory.gd

# 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

类型或动态类型:坚持一种风格

类型化的GDScript和动态GDScript可以共存于同一项目中。但是我建议您使用任意一种风格,以确保您的代码库和同行的一致性。如果你们遵循相同的准则,那么每个人都可以更轻松地一起工作,并且可以更快地阅读和理解他人的代码。

类型化的代码需要编写更多,但是您将获得上面讨论的好处。这是一个动态风格的,相同的空脚本示例:

extends Node
    func _ready():
        pass
    func _process(delta):
        pass

和使用静态类型:

extends Node
    func _ready() -> void:
        pass
    func _process(delta: float) -> void:
        pass

如您所见,您还可以将类型与引擎的虚方法一起使用。与任何方法一样,信号回调也可以使用类型。这是一个动态风格的 body_entered 信号:

func _on_Area2D_body_entered(body):
    pass

以及具有类型提示的,相同的回调:

func _on_area_entered(area: CollisionObject2D) -> void:
    pass

您可以自由替代,例如使用您自己的类型的 CollisionObject2D,自动转换参数:

func _on_area_entered(bullet: Bullet) -> void:
    if not bullet:
        return
    take_damage(bullet.damage)

bullet 变量可以在这里保存任何 CollisionObject2D,但我们需要确保它是我们的 Bullet,即那个我们为项目创建的节点。如果它是其他任何东西,比如 Area2D ,或任何不扩展 Bullet 的节点, bullet 变量将是 null

警告系统

该警告系统是对类型化的GDScript的补充。它可以帮助您避免开发过程中的难以发现的错误,以及可能导致运行时错误的错误。

您可以在项目设置的新部分 GDScript 下配置警告:

warning system project settings

警告系统项目设置

您可以在脚本编辑器的状态栏中找到有关活动GDScript文件的警告列表。下面的示例有3条警告:

warning system example

警告系统示例

To ignore specific warnings in one file, insert a special comment of the form # warning-ignore:warning-id, or click on the ignore link to the right of the warning’s description. Godot will add a comment above the corresponding line and the code won’t trigger the corresponding warning anymore:

warning system ignore example

警告系统忽略示例

You can also choose to ignore not just one but all warnings of a certain type in this file with # warning-ignore-all:warning-id. To ignore all warnings of all types in a file add the comment # warnings-disable to it.

警告不会阻止游戏的运行,但是您可以根据需要将其转换为错误。这样,除非您修复所有警告,否则游戏无法编译。前往项目设置的 GDScript 部分打开此选项。这是与前一个示例相同的文件,并在启用了警告转成错误:

warnings as errors

警告当作错误

无法指定类型的情况

为了总结这个介绍,我们将介绍一些不能使用类型提示的情况。以下所有示例 都会触发错误

您不能将枚举作为类型:

enum MoveDirection {UP, DOWN, LEFT, RIGHT}
var current_direction: MoveDirection

您无法指定数组中单个成员的类型。这会给您一个错误:

var enemies: Array = [$Goblin: Enemy, $Zombie: Enemy]

您不能在 for 循环中强制分配类型,因为 for 关键字循环已经有不同的类型。所以您 不能 写:

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

两个脚本不能以循环的方式相互依赖:

# Player.gd
extends Area2D
class_name Player

var rifle: Rifle
# Rifle.gd
extends Area2D
class_name Rifle

var player: Player

总结

类型化的GDScript是一个强大的工具。从Godot 3.1版开始可用,它可以帮助您编写更多结构化的代码、避免常见错误、以及创建可伸缩的系统。将来,由于即将进行的编译器优化,静态类型也将为您带来不错的性能提升。