Attention: Here be dragons

This is the latest (unstable) version of this documentation, which may document features not available in or compatible with released stable versions of Godot.

GDScript:动态语言简介

关于

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

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

动态性

动态类型的优缺点

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

  • The language is easy to get started with.

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

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

  • The code is easy to read (little clutter).

  • 测试不需要编译.

  • 运行时(Runtime)很小.

  • It has duck-typing and polymorphism by nature.

主要缺点是:

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

  • More difficult to refactor (symbols can't be traced).

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

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

This, translated to reality, means that Godot used with GDScript is a combination designed to create games quickly and efficiently. For games that are very computationally intensive and can't benefit from the engine built-in tools (such as the Vector types, Physics Engine, Math library, etc), the possibility of using C++ is present too. This allows you to still create most of the game in GDScript and add small bits of C++ in the areas that need a performance boost.

变量与赋值

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

静态的:

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

动态的:

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

作为函数参数:

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

静态的:

void print_value(int value) {

    printf("value is %i\n", value);
}

[..]

print_value(55); // Valid.
print_value("Hello"); // Invalid.

动态的:

func print_value(value):
    print(value)

[..]

print_value(55) # Valid.
print_value("Hello") # Valid.

指针和引用:

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

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

  • C++:

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.
}
  • Java:

@Override
public 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.
}
  • GDScript:

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.

In GDScript, only base types (int, float, string and the vector types) are passed by value to functions (value is copied). Everything else (instances, arrays, dictionaries, etc) is passed as reference. Classes that inherit RefCounted (the default if nothing is specified) will be freed when not used, but manual memory management is allowed too if inheriting manually from Object.

数组

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

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.

// or

std::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中:

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

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

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

或无序集合:

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

字典

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

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

字典的示例:

var d = {"name": "John", "age": 22}
print("Name: ", d["name"], " Age: ", d["age"])

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

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

In most cases, two-dimensional arrays can often be implemented more easily with dictionaries. Here's a battleship game example:

# Battleship Game

const SHIP = 0
const SHIP_HIT = 1
const WATER_HIT = 2

var board = {}

func initialize():
    board[Vector2(1, 1)] = SHIP
    board[Vector2(1, 2)] = SHIP
    board[Vector2(1, 3)] = SHIP

func 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_HIT

func game():
    initialize()
    missile(Vector2(1, 1))
    missile(Vector2(5, 8))
    missile(Vector2(2, 3))

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

# 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.

# Indexing

d["mother"] = "Rebecca"
d.mother = "Caroline" # This would work too to create a new key.

For & while

Iterating using the C-style for loop in C-derived languages can be quite complex:

const char** strings = new const char*[50];

[..]

for (int i = 0; i < 50; i++) {
            printf("Value: %c Index: %d\n", strings[i], i);
    }

// Even in STL:
std::list<std::string> strings;

[..]

    for (std::string::const_iterator it = strings.begin(); it != strings.end(); it++) {
            std::cout << *it << std::endl;
    }

Because of this, GDScript makes the opinionated decision to have a for-in loop over iterables instead:

for s in strings:
    print(s)

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

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

迭代索引也是可能的:

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

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

range(n) # Will count from 0 to n in steps of 1. The parameter n is exclusive.
range(b, n) # Will count from b to n in steps of 1. The parameters b is inclusive. The parameter n is exclusive.
range(b, n, s) # Will count from b to n, in steps of s. The parameters b is inclusive. The parameter n is exclusive.

Some examples involving C-style for loops:

for (int i = 0; i < 10; i++) {}

for (int i = 5; i < 10; i++) {}

for (int i = 5; i < 10; i += 2) {}

转变成:

for i in range(10):
    pass

for i in range(5, 10):
    pass

for i in range(5, 10, 2):
    pass

And backwards looping done through a negative counter:

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

变成:

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

While

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

var i = 0

while i < strings.size():
    print(strings[i])
    i += 1

自定义迭代器

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

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

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

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

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

鸭子类型

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

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

void BigRollingRock::on_object_hit(Smashable *entity) {

    entity->smash();
}

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

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

func _on_object_hit(object):
    object.smash()

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

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

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

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

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

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

func _on_object_hit(object):
    if object.has_method("smash"):
        object.smash()

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