GDScript:动态语言简介

关于

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

动态性

动态类型的优缺点

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

  • 这种语言简单易学.
  • 大多数代码都可以快速地编写和更改, 而且没有任何麻烦.
  • 编写更少的代码意味着要修复的错误和失误更少.
  • 更容易阅读代码(减少混乱).
  • 测试不需要编译.
  • 运行时(Runtime)很小.
  • 本质上是鸭子类型和多态性. 主要缺点是:
  • 比静态类型语言的性能要低.
  • 重构更加困难(无法追踪符号)
  • 一些通常在静态类型语言编译时检测到的错误, 只会在运行代码时出现(因为表达式解析更严格).
  • 代码补全的灵活性较低(某些变量的类型只能在运行时确定). 这转化为现实, 意味着Godot+GDScript是一个旨在快速高效创建游戏的组合. 对于计算量很大并且无法从引擎内置工具(例如向量类型, 物理引擎, 数学库等)中受益的游戏, 也存在使用C++的可能性. 这允许您依旧使用GDScript创建游戏的绝大部分, 并在需要性能的地方添加少量C++代码.

    变量与赋值

    变体 的. 这意味着它们的类型不是固定的, 只能通过赋值修改. 示例: 静态的:
    1. int a; // Value uninitialized.a = 5; // This is valid.a = "Hi!"; // This is invalid.
    动态的:
    1. var a # 'null' by default.a = 5 # Valid, 'a' becomes an integer.a = "Hi!" # Valid, 'a' changed to a string.

    作为函数参数:

    函数也是动态的, 这意味着它们可以用不同的参数调用, 例如: 静态的:
    1. void print_value(int value) { printf("value is %i\n", value);}[..]print_value(55); // Valid.print_value("Hello"); // Invalid.
    动态的:
    1. func print_value(value): print(value)[..]print_value(55) # Valid.print_value("Hello") # Valid.

    指针和引用:

    在静态语言, 如C或C++(以及在一定程度上,Java和C#)中, 变量和变量的指针/引用之间存在区别. 后者通过传递对原始函数的引用, 允许其他函数修改该对象. 在C# 或Java中, 不是内置类型(int,float, 有时的String)的任何东西都是指针或引用. 引用也是自动垃圾回收的, 这意味着它们在不再使用时被删除. 动态类型的语言也倾向于使用这种内存模型. 一些示例: -
    1. void use_class(SomeClass *instance) { instance->use();}void do_something() { SomeClass *instance = new SomeClass; // Created as pointer. use_class(instance); // Passed as pointer. delete instance; // Otherwise it will leak memory.}
    -
    1. @Overridepublic final void use_class(SomeClass instance) { instance.use();}public final void do_something() { SomeClass instance = new SomeClass(); // Created as reference. use_class(instance); // Passed as reference. // Garbage collector will get rid of it when not in // use and freeze your game randomly for a second.}
    -
    1. func use_class(instance): # Does not care about class type instance.use() # Will work with any class that has a ".use()" method.func do_something(): var instance = SomeClass.new() # Created as reference. use_class(instance) # Passed as reference. # Will be unreferenced and deleted.
    Reference (如果没有指定任何内容, 则默认从这里继承)的类在不使用时将被释放, 但对手动继承自 Object 的类也允许手动管理内存.

    数组

    动态类型语言的数组内部可以包含许多不同的混合数据类型, 并且始终是动态的(可以随时调整大小). 比较在静态类型语言中的数组示例:
    1. int *array = new int[4]; // Create array.array[0] = 10; // Initialize manually.array[1] = 20; // Can't mix types.array[2] = 40;array[3] = 60;// Can't resize.use_array(array); // Passed as pointer.delete[] array; // Must be freed.// orstd::vector<int> array;array.resize(4);array[0] = 10; // Initialize manually.array[1] = 20; // Can't mix types.array[2] = 40;array[3] = 60;array.resize(3); // Can be resized.use_array(array); // Passed reference or value.// Freed when stack ends.
    以及在GDScript中:
    1. var array = [10, "hello", 40, 60] # Simple, and can mix types.array.resize(3) # Can be resized.use_array(array) # Passed as reference.# Freed when no longer in use.
    在动态类型语言中, 数组也可以作为其他数据类型有多种用法, 例如列表:
    1. var array = []array.append(4)array.append(5)array.pop_front()
    或无序集合:
    1. var a = 20if a in [10, 20, 30]: print("We have a winner!")

    字典

    字典是动态类型化语言中的一个强大工具. 来自静态类型语言(例如C++或C#)的大多数程序员都忽略了它们的存在, 并不必要地增加了他们的工作难度. 这种数据类型通常不存在于此类语言中(或仅以受限的形式). 字典可以完全忽略用作键或值的数据类型, 将任何值映射到任何其他值. 与流行的观点相反, 它们是有效的, 因为它们可以通过哈希表实现. 事实上, 它们非常高效, 一些语言甚至可以像实现字典一样实现数组. 字典的示例:
    1. var d = {"name": "John", "age": 22} # Simple syntax.print("Name: ", d["name"], " Age: ", d["age"])
    字典也是动态的, 键可以在任何一点添加或删除, 花费很少:
    1. d["mother"] = "Rebecca" # Addition.d["age"] = 11 # Modification.d.erase("name") # Removal.
    在大多数情况下, 使用字典可以更容易地实现二维数组. 这里有一个简单的战舰游戏的示例:
    1. # Battleship Gameconst SHIP = 0const SHIP_HIT = 1const WATER_HIT = 2var board = {}func initialize(): board[Vector2(1, 1)] = SHIP board[Vector2(1, 2)] = SHIP board[Vector2(1, 3)] = SHIPfunc missile(pos): if pos in board: # Something at that position. if board[pos] == SHIP: # There was a ship! hit it. board[pos] = SHIP_HIT else: print("Already hit here!") # Hey dude you already hit here. else: # Nothing, mark as water. board[pos] = WATER_HITfunc game(): initialize() missile(Vector2(1, 1)) missile(Vector2(5, 8)) missile(Vector2(2, 3))
    字典还可以用作数据标记或快速结构. 虽然GDScript字典类似于python字典, 但它也支持Lua风格的语法和索引, 这使得它对于编写初始状态和快速结构非常有用:
    1. # Same example, lua-style support.# This syntax is a lot more readable and usable.# Like any GDScript identifier, keys written in this form cannot start# with a digit.var d = { name = "John", age = 22}print("Name: ", d.name, " Age: ", d.age) # Used "." based indexing.# Indexingd["mother"] = "Rebecca"d.mother = "Caroline" # This would work too to create a new key.

    For & while

    在一些静态类型的语言中迭代可能非常复杂:
    1. const char* strings = new const char*[50];[..]for (int i = 0; i < 50; i++) { printf("Value: %s\n", i, strings[i]);}// Even in STL:for (std::list<std::string>::const_iterator it = strings.begin(); it != strings.end(); it++) { std::cout << *it << std::endl;}
    这通常在动态类型语言中得到极大简化:
    1. for s in strings: print(s)
    容器数据类型(数组和字典)是可迭代的. 字典允许迭代键:
    1. for key in dict: print(key, " -> ", dict[key])
    迭代索引也是可能的:
    1. for i in range(strings.size()): print(strings[i])
    range() 函数可以有3个参数:
    1. range(n) # Will go from 0 to n-1.range(b, n) # Will go from b to n-1.range(b, n, s) # Will go from b to n-1, in steps of s.
    一些静态类型的编程语言示例:
    1. for (int i = 0; i < 10; i++) {}for (int i = 5; i < 10; i++) {}for (int i = 5; i < 10; i += 2) {}
    转变成:
    1. for i in range(10): passfor i in range(5, 10): passfor i in range(5, 10, 2): pass
    反向循环是通过一个负计数器完成的:
    1. for (int i = 10; i > 0; i--) {}
    变成:
    1. for i in range(10, 0, -1): pass

    While

    while() 循环在任何地方都是相同的:
    1. var i = 0while i < strings.size(): print(strings[i]) i += 1

    自定义迭代器

    Variant 类的 _iter_init, _iter_next, 和 _iter_get 函数来创建自定义迭代器. 正向迭代器的一个示例实现如下:
    1. class ForwardIterator: var start var current var end var increment func _init(start, stop, increment): self.start = start self.current = start self.end = stop self.increment = increment func should_continue(): return (current < end) func _iter_init(arg): current = start return should_continue() func _iter_next(arg): current += increment return should_continue() func _iter_get(arg): return current
    它可以像任何其他迭代器一样使用:
    1. var itr = ForwardIterator.new(0, 6, 2)for i in itr: print(i) # Will print 0, 2, and 4.
    _iter_init 中重置迭代器的状态, 否则使用自定义迭代器的嵌套for循环将无法正常工作.

    鸭子类型

    当从静态类型语言迁移到动态类型语言时, 最难掌握的概念之一是鸭子类型. 鸭子类型使整个代码设计更加简单和直接, 但是它的工作方式并不明显. 举个示例, 想象一个大石头从隧道里掉下来, 在路上砸碎了一切. 在静态类型语言中石头的代码有点像:
    1. void BigRollingRock::on_object_hit(Smashable *entity) { entity->smash();}
    Smashable. 如果一个角色, 敌人, 家具, 小石块都易碎, 它们需要从 Smashable 类继承, 可能需要多次继承. 如果不希望进行多重继承, 那么它们必须继承像 Entity 这样的公共类. 然而, 如果只是其中几个能被粉碎的话, 在 Entity 中添加一个虚拟方法 smash() 并不十分优雅. smash() 函数, 就行了. 无需考虑继承, 基类等.
    1. func _on_object_hit(object): object.smash()
    smash() 方法, 它将被调用. 不需要考虑继承或多态性. 动态类型化语言只关心具有所需方法或成员的实例, 而不关心它继承什么或其类型. 鸭子类型的定义应该使这一点更清楚: “当我看到一只鸟像鸭子一样走路, 像鸭子一样游泳, 像鸭子一样呱呱叫时, 我就叫它鸭子” 在这种情况下, 它可转变成: “如果物体可以被砸碎, 不要在意它是什么, 只管砸碎它.” 是的,称它为绿巨人(Hulk)类型适乎更合适。 有可能被击中的对象并没有smash()函数。一些动态类型的语言在调用方法不存在时,会简单地忽略它,但GDScript更严格,所以有必要检查函数是否存在:
    1. func _on_object_hit(object): if object.has_method("smash"): object.smash()
    然后, 简单地定义这个方法, 岩石触碰的任何东西都可以被粉碎了.