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. 核心类和节点( Object, Node, Area2D, Camera2D, 等)

  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支持 鸭子类型 <https://stackoverflow.com/a/4205163/8125343> __, 所以即使您的计时器是 Timer 类型, 它也扩展了 NodeObject , 这两个类. 使用动态GDScript, 只要节点具有您需要调用的方法, 您也不必关心节点的类型.

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

Unsafe vs Safe Line

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

注解

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

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

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

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

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

Typed GDScript and dynamic GDScript can coexist in the same project. But it's recommended to stick to either style for consistency in your codebase, and for your peers. It's easier for everyone to work together if you follow the same guidelines, and faster to read and understand other people's code.

类型化的代码需要编写更多, 但是您将获得上面讨论的好处. 下面是内容一样的空脚本示例, 首先是使用动态风格:

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 警告系统.

无法指定类型的情况

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

您不能将枚举作为类型:

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版开始可用, 它可以帮助您编写更多结构化的代码, 避免常见错误, 以及创建可伸缩的系统. 将来, 由于即将进行的编译器优化, 静态类型也将为您带来不错的性能提升.