GDScript:动态语言简介

关于

本教程旨在快速介绍如何更有效地使用GDScript. 它只关注特定于该语言的常见情况, 但是也涉及许多关于动态类型语言的信息.

对于没有或几乎没有动态类型语言经验的程序员来说, 它特别有用.

动态性

动态类型的优缺点

GDScript是一种动态类型语言. 因此, 它的主要优点是:

  • 这种语言简单易学.

  • 大多数代码都可以快速地编写和更改, 而且没有任何麻烦.

  • 编写更少的代码意味着要修复的错误和失误更少.

  • 更容易阅读代码(减少混乱).

  • 测试不需要编译.

  • 运行时(Runtime)很小.

  • 本质上是鸭子类型和多态性.

主要缺点是:

  • 比静态类型语言的性能要低.

  • 重构更加困难(无法追踪符号)

  • 一些通常在静态类型语言编译时检测到的错误, 只会在运行代码时出现(因为表达式解析更严格).

  • 代码补全的灵活性较低(某些变量的类型只能在运行时确定).

这转化为现实, 意味着Godot+GDScript是一个旨在快速高效创建游戏的组合. 对于计算量很大并且无法从引擎内置工具(例如向量类型, 物理引擎, 数学库等)中受益的游戏, 也存在使用C++的可能性. 这允许您依旧使用GDScript创建游戏的绝大部分, 并在需要性能的地方添加少量C++代码.

变量与赋值

动态类型语言中的所有变量都类似“变体”。这意味着它们的类型不是固定的,只能通过赋值修改。示例:

静态的:

  1. int a; // Value uninitialized.
  2. a = 5; // This is valid.
  3. a = "Hi!"; // This is invalid.

动态的:

  1. var a # 'null' by default.
  2. a = 5 # Valid, 'a' becomes an integer.
  3. a = "Hi!" # Valid, 'a' changed to a string.

作为函数参数:

函数也是动态的,这意味着它们可以用不同的参数调用,例如:

静态的:

  1. void print_value(int value) {
  2. printf("value is %i\n", value);
  3. }
  4. [..]
  5. print_value(55); // Valid.
  6. print_value("Hello"); // Invalid.

动态的:

  1. func print_value(value):
  2. print(value)
  3. [..]
  4. print_value(55) # Valid.
  5. print_value("Hello") # Valid.

指针和引用:

在 C、C++ 等静态语言中(Java 和 C# 某种程度上也是)存在变量和变量的指针/引用的区别。后者的作用是,如果传的是原始对象的引用,那么其他函数就可以修改这个对象。

在 C# 或 Java 中,非内置类型(int、float、某些时候的 String)的任何东西都是指针或引用。而且引用会被自动垃圾回收,这意味着它们在不再使用时被删除。动态类型的语言也倾向于使用这种内存模型。一些示例:

  • C++:
  1. void use_class(SomeClass *instance) {
  2. instance->use();
  3. }
  4. void do_something() {
  5. SomeClass *instance = new SomeClass; // Created as pointer.
  6. use_class(instance); // Passed as pointer.
  7. delete instance; // Otherwise it will leak memory.
  8. }
  • Java:
  1. @Override
  2. public final void use_class(SomeClass instance) {
  3. instance.use();
  4. }
  5. public final void do_something() {
  6. SomeClass instance = new SomeClass(); // Created as reference.
  7. use_class(instance); // Passed as reference.
  8. // Garbage collector will get rid of it when not in
  9. // use and freeze your game randomly for a second.
  10. }
  • GDScript:
  1. func use_class(instance): # Does not care about class type
  2. instance.use() # Will work with any class that has a ".use()" method.
  3. func do_something():
  4. var instance = SomeClass.new() # Created as reference.
  5. use_class(instance) # Passed as reference.
  6. # Will be unreferenced and deleted.

在 GDScript 中,只有基础类型(int、float、String、PoolArray 类型)会通过值传递给函数(值会被复制)。其他所有类型(实例、数组、字典等)都会作为引用传递。继承自 Reference 的类(未指定父类时会默认继承它)在不使用时将被释放,但对手动继承自 Object 的类也允许手动管理内存。

备注

按值传递指的是用作函数参数时,每次都会复制一份。因此,函数是无法对参数造成外部可见的修改的:

  1. func greet(text):
  2. text = "Hello " + text
  3. func _ready():
  4. # Create a String (passed by value and immutable).
  5. var example = "Godot"
  6. # Pass example as a parameter to `greet()`,
  7. # which modifies the parameter and does not return any value.
  8. greet(example)
  9. print(example) # Godot

按引用传递指的是用作函数参数时,不会每次都复制一份。这样函数体内就可以修改参数(函数外部也可以访问到这个修改后的值)。缺点是无法保证作为参数的数据不被修改,如果使用不当,可能会导致难以排查的问题:

  1. func greet(text):
  2. text.push_front("Hello")
  3. func _ready():
  4. # Create an Array (passed by reference and mutable) containing a String,
  5. # instead of a String (passed by value and immutable).
  6. var example = ["Godot"]
  7. # Pass example as a parameter to `greet()`,
  8. # which modifies the parameter and does not return any value.
  9. greet(example)
  10. print(example) # [Hello, Godot] (Array with 2 String elements)

与按值传递相比,按引用传递在使用大型对象时性能更好,因为在内存中复制大型对象比较慢。

另外,在 Godot 中,String 等基础类型都是不可修改的。这意味着修改他们的值总是会返回原始值的副本,而不是对原值进行修改。

数组

动态类型语言的数组内部可以包含许多不同的混合数据类型, 并且始终是动态的(可以随时调整大小). 比较在静态类型语言中的数组示例:

  1. int *array = new int[4]; // Create array.
  2. array[0] = 10; // Initialize manually.
  3. array[1] = 20; // Can't mix types.
  4. array[2] = 40;
  5. array[3] = 60;
  6. // Can't resize.
  7. use_array(array); // Passed as pointer.
  8. delete[] array; // Must be freed.
  9. // or
  10. std::vector<int> array;
  11. array.resize(4);
  12. array[0] = 10; // Initialize manually.
  13. array[1] = 20; // Can't mix types.
  14. array[2] = 40;
  15. array[3] = 60;
  16. array.resize(3); // Can be resized.
  17. use_array(array); // Passed reference or value.
  18. // Freed when stack ends.

以及在GDScript中:

  1. var array = [10, "hello", 40, 60] # Simple, and can mix types.
  2. array.resize(3) # Can be resized.
  3. use_array(array) # Passed as reference.
  4. # Freed when no longer in use.

在动态类型语言中, 数组也可以作为其他数据类型有多种用法, 例如列表:

  1. var array = []
  2. array.append(4)
  3. array.append(5)
  4. array.pop_front()

或无序集合:

  1. var a = 20
  2. if a in [10, 20, 30]:
  3. print("We have a winner!")

字典

字典是动态类型化语言中的一个强大工具. 来自静态类型语言(例如C++或C#)的大多数程序员都忽略了它们的存在, 并不必要地增加了他们的工作难度. 这种数据类型通常不存在于此类语言中(或仅以受限的形式).

字典可以完全忽略用作键或值的数据类型, 将任何值映射到任何其他值. 与流行的观点相反, 它们是有效的, 因为它们可以通过哈希表实现. 事实上, 它们非常高效, 一些语言甚至可以像实现字典一样实现数组.

字典的示例:

  1. var d = {"name": "John", "age": 22} # Simple syntax.
  2. print("Name: ", d["name"], " Age: ", d["age"])

字典也是动态的, 键可以在任何一点添加或删除, 花费很少:

  1. d["mother"] = "Rebecca" # Addition.
  2. d["age"] = 11 # Modification.
  3. d.erase("name") # Removal.

在大多数情况下, 使用字典可以更容易地实现二维数组. 这里有一个简单的战舰游戏的示例:

  1. # Battleship Game
  2. const SHIP = 0
  3. const SHIP_HIT = 1
  4. const WATER_HIT = 2
  5. var board = {}
  6. func initialize():
  7. board[Vector2(1, 1)] = SHIP
  8. board[Vector2(1, 2)] = SHIP
  9. board[Vector2(1, 3)] = SHIP
  10. func missile(pos):
  11. if pos in board: # Something at that position.
  12. if board[pos] == SHIP: # There was a ship! hit it.
  13. board[pos] = SHIP_HIT
  14. else:
  15. print("Already hit here!") # Hey dude you already hit here.
  16. else: # Nothing, mark as water.
  17. board[pos] = WATER_HIT
  18. func game():
  19. initialize()
  20. missile(Vector2(1, 1))
  21. missile(Vector2(5, 8))
  22. missile(Vector2(2, 3))

字典还可以用作数据标记或快速结构. 虽然GDScript字典类似于python字典, 但它也支持Lua风格的语法和索引, 这使得它对于编写初始状态和快速结构非常有用:

  1. # Same example, lua-style support.
  2. # This syntax is a lot more readable and usable.
  3. # Like any GDScript identifier, keys written in this form cannot start
  4. # with a digit.
  5. var d = {
  6. name = "John",
  7. age = 22
  8. }
  9. print("Name: ", d.name, " Age: ", d.age) # Used "." based indexing.
  10. # Indexing
  11. d["mother"] = "Rebecca"
  12. d.mother = "Caroline" # This would work too to create a new key.

For & while

在一些静态类型的语言中迭代可能非常复杂:

  1. const char* strings = new const char*[50];
  2. [..]
  3. for (int i = 0; i < 50; i++) {
  4. printf("Value: %s\n", i, strings[i]);
  5. }
  6. // Even in STL:
  7. for (std::list<std::string>::const_iterator it = strings.begin(); it != strings.end(); it++) {
  8. std::cout << *it << std::endl;
  9. }

这通常在动态类型语言中得到极大简化:

  1. for s in strings:
  2. print(s)

容器数据类型(数组和字典)是可迭代的. 字典允许迭代键:

  1. for key in dict:
  2. print(key, " -> ", dict[key])

迭代索引也是可能的:

  1. for i in range(strings.size()):
  2. print(strings[i])

range() 函数可以有 3 个参数:

  1. range(n) # Will go from 0 to n-1.
  2. range(b, n) # Will go from b to n-1.
  3. range(b, n, s) # Will go from b to n-1, in steps of s.

一些静态类型的编程语言示例:

  1. for (int i = 0; i < 10; i++) {}
  2. for (int i = 5; i < 10; i++) {}
  3. for (int i = 5; i < 10; i += 2) {}

转变成:

  1. for i in range(10):
  2. pass
  3. for i in range(5, 10):
  4. pass
  5. for i in range(5, 10, 2):
  6. pass

反向循环是通过一个负计数器完成的:

  1. for (int i = 10; i > 0; i--) {}

变成:

  1. for i in range(10, 0, -1):
  2. pass

While

while() 循环在任何地方都是相同的:

  1. var i = 0
  2. while i < strings.size():
  3. print(strings[i])
  4. i += 1

自定义迭代器

在默认迭代器无法完全满足你的需求的情况下, 你可以通过重写脚本中 Variant 类的 _iter_init, _iter_next, 和 _iter_get 函数来创建自定义迭代器. 正向迭代器的一个示例实现如下:

  1. class ForwardIterator:
  2. var start
  3. var current
  4. var end
  5. var increment
  6. func _init(start, stop, increment):
  7. self.start = start
  8. self.current = start
  9. self.end = stop
  10. self.increment = increment
  11. func should_continue():
  12. return (current < end)
  13. func _iter_init(arg):
  14. current = start
  15. return should_continue()
  16. func _iter_next(arg):
  17. current += increment
  18. return should_continue()
  19. func _iter_get(arg):
  20. return current

它可以像任何其他迭代器一样使用:

  1. var itr = ForwardIterator.new(0, 6, 2)
  2. for i in itr:
  3. print(i) # Will print 0, 2, and 4.

确保在 _iter_init 中重置迭代器的状态, 否则使用自定义迭代器的嵌套for循环将无法正常工作.

鸭子类型

当从静态类型语言迁移到动态类型语言时, 最难掌握的概念之一是鸭子类型. 鸭子类型使整个代码设计更加简单和直接, 但是它的工作方式并不明显.

举个示例, 想象一个大石头从隧道里掉下来, 在路上砸碎了一切. 在静态类型语言中石头的代码有点像:

  1. void BigRollingRock::on_object_hit(Smashable *entity) {
  2. entity->smash();
  3. }

这样,任何能被岩石砸碎的东西都必须继承 Smashable。如果角色、敌人、家具、小石块都易碎,它们需要从 Smashable 类继承,可能需要多次继承。如果不希望进行多重继承,那么它们必须继承像 Entity 这样的公共类。然而,如果只是其中几个能被粉碎的话,在 Entity 中添加一个虚方法 smash() 并不十分优雅。

使用动态类型的语言, 这将不是问题. 鸭子类型确保你只需在需要的地方定义一个 smash() 函数, 就行了. 无需考虑继承, 基类等.

  1. func _on_object_hit(object):
  2. object.smash()

就是这样. 如果击中大岩石的对象有一个 smash() 方法, 它将被调用. 不需要考虑继承或多态性. 动态类型化语言只关心具有所需方法或成员的实例, 而不关心它继承什么或其类型. 鸭子类型的定义应该使这一点更清楚:

“当我看到一只鸟像鸭子一样走路、像鸭子一样游泳、像鸭子一样呱呱叫时,我就管它叫鸭子”

在这种情况下,它可转变成:

“如果物体可以被砸碎,不要在意它是什么,只管砸碎它。”

是的,称它为绿巨人(Hulk)类型适乎更合适。

有可能被击中的对象并没有smash()函数。一些动态类型的语言在调用方法不存在时,会简单地忽略它,但GDScript更严格,所以有必要检查函数是否存在:

  1. func _on_object_hit(object):
  2. if object.has_method("smash"):
  3. object.smash()

然后, 简单地定义这个方法, 岩石触碰的任何东西都可以被粉碎了.