GDScript 中静态类型

在本指南中,您将学会:

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

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

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

注解

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

静态类型简介

有了GDScript中的类型,Godot在编写代码时甚至可以检测到更多错误!它会在你工作时为你和你的团队提供更多信息。因为当你调用方法时,会显示出参数的类型。

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

  1. # In 'Item.gd'.
  2. class_name Item
  3. # In 'Inventory.gd'.
  4. class_name Inventory
  5. func add(reference: Item, amount: int = 1):
  6. var item = find_item(reference)
  7. if not item:
  8. item = _instance_item_from_db(reference)
  9. item.amount += amount

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

Static types also give you better code completion options. Below, you can see the difference between a dynamic and a static typed completion options for a class called PlayerController.

You’ve probably stored a node in a variable before, and typed a dot to be left with no autocomplete suggestions:

code completion options for dynamic

This is due to dynamic code. Godot cannot know what node or value type you’re passing to the function. If you write the type explicitly however, you will get all public methods and variables from the node:

code completion options for typed

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

Overall, typed programming gives you a more structured experience. It helps prevent errors and improves the self-documenting aspect of your scripts. This is especially helpful when you’re working in a team or on a long-term project: studies have shown that developers spend most of their time reading other people’s code, or scripts they wrote in the past and forgot about. The clearer and the more structured the code, the faster it is to understand, the faster you can move forward.

如何使用静态类型

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

  1. var damage: float = 10.5
  2. const MOVE_SPEED: float = 50.0

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

  1. var life_points := 4
  2. var damage := 10.5
  3. var motion := Vector2()

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

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

注解

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

自定义变量类型

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

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

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

  1. extends Node2D
  2. class_name Rifle

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

  1. var my_rifle: Rifle

变量转换

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

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

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

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

As we’re dealing with a custom type, if the body doesn’t extend PlayerController, the playervariable will be set to null. We can use this to check if the body is the player or not. We will also get full autocompletion on the player variable thanks to that cast.

注解

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

安全行

You can also use casting to ensure safe lines. Safe lines are a new tool in Godot 3.1 to tell you when ambiguous lines of code are type-safe. As you can mix and match typed and dynamic code, at times, Godot doesn’t have enough information to know if an instruction will trigger an error or not at runtime.

This happens when you get a child node. Let’s take a timer for example: with dynamic code, you can get the node with $Timer. GDScript supports duck-typing, so even if your timer is of type Timer, it is also a Node and an Object, two classes it extends. With dynamic GDScript, you also don’t care about the node’s type as long as it has the methods you need to call.

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

Unsafe vs Safe Line

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

注解

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

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

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

  1. func _process(delta: float) -> void:
  2. pass

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

  1. func hit(damage: float) -> bool:
  2. health_points -= damage
  3. return health_points <= 0

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

  1. # Inventory.gd
  2. # Adds an item to the inventory and returns it.
  3. func add(reference: Item, amount: int) -> Item:
  4. var item: Item = find_item(reference)
  5. if not item:
  6. item = ItemDatabase.get_instance(reference)
  7. item.amount += amount
  8. return item

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

Typed GDScript and dynamic GDScript can coexist in the same project. But I recommend 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.

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:

  1. extends Node
  2. func _ready():
  3. pass
  4. func _process(delta):
  5. pass

和使用静态类型:

  1. extends Node
  2. func _ready() -> void:
  3. pass
  4. func _process(delta: float) -> void:
  5. 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:

  1. func _on_Area2D_body_entered(body):
  2. pass

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

  1. func _on_area_entered(area: CollisionObject2D) -> void:
  2. pass

You’re free to replace, e.g. the CollisionObject2D, with your own type, to cast parameters automatically:

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

The bullet variable could hold any CollisionObject2D here, but we make sure it is our Bullet, a node we created for our project. If it’s anything else, like an Area2D, or any node that doesn’t extend Bullet, the bullet variable will be null.

警告系统

注解

Documentation about the GDScript warning system has been moved to GDScript warning system.

Cases where you can’t specify types

To wrap up this introduction, let’s cover a few cases where you can’t use type hints. All the examples below will trigger errors.

You can’t use Enums as types:

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

You can’t specify the type of individual members in an array. This will give you an error:

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

You can’t force the assignment of types in a for loop, as each element the for keyword loops over already has a different type. So you cannot write:

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

Two scripts can’t depend on each other in a cyclic fashion:

  1. # Player.gd
  2. extends Area2D
  3. class_name Player
  4. var rifle: Rifle
  1. # Rifle.gd
  2. extends Area2D
  3. class_name Rifle
  4. var player: Player

总结

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