gdscript:动态语言简介

关于

本教程旨在为如何更有效地使用gdscript提供快速参考。它关注特定于语言的常见情况,但也涵盖了许多关于动态类型语言的信息。

这对于以前很少或根本没有使用动态类型语言经验的程序员特别有用。

动态性

动态打字的优缺点

gdscript是一种动态类型的语言。因此,其主要优势在于:

  • 语言简单易学。

  • 大多数代码都可以快速地编写和更改,而且不需要麻烦。

  • 写的代码越少,修复的错误就越少。

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

  • 不需要编译来测试。

  • 运行时间很短。

  • 鸭子的分型和多态性。

主要缺点是:

  • 性能不如静态类型语言。

  • 更难重构(无法跟踪符号)

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

  • 代码完成的灵活性较低(某些变量类型仅在运行时已知)。

这转化为现实,意味着godot+gdscript是一种组合,旨在快速高效地创建游戏。对于计算非常密集且不能从引擎内置工具(如向量类型、物理引擎、数学库等)获益的游戏,也存在使用C++的可能性。这使您仍然可以在GDScript创建大部分游戏,并在需要提升性能的区域中添加少量的C++。

变量与赋值

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

静态:

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,浮点,有时字符串)的任何东西都是指针或引用。引用也会被自动垃圾收集,这意味着不再使用时它们会被清除。动态类型语言也倾向于使用这个内存模型。一些例子:

  • 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
}
  • 爪哇:

@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

在gdscript中,只有基类型(int、float、string和vector类型)通过值传递给函数(复制值)。其他所有内容(实例、数组、字典等)都作为引用传递。继承的类 参考文献 (如果未指定,则为默认值)将在不使用时释放,但如果从手动继承,则也允许手动内存管理。 对象 .

数组

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

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] # Simple, and 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} # Simple syntax
print("Name: ", d["name"], " Age: ", d["age"])

字典也是动态的,可以在任何地方添加或删除键,成本很低:

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

在大多数情况下,二维数组通常可以用字典更容易地实现。以下是一个简单的战舰游戏示例:

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

暂时

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

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;
}

这通常在动态类型语言中被大大简化:

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

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

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

反向循环通过负计数器完成:

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

变成:

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

同时

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

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循环将无法按预期工作。

鸭子打字

当从静态类型语言转移到动态类型语言时,最难理解的概念之一是duck-typing。duck打字使得整个代码设计变得简单和简单,但是它的工作方式并不明显。

举个例子,想象一下这样一种情况:一块大石头从隧道里掉了下来,砸碎了路上所有的东西。用静态类型语言编写的rock代码如下:

void BigRollingRock::on_object_hit(Smashable *entity) {

    entity->smash();
}

这样,任何能被石头砸碎的东西都必须继承可砸碎的东西。如果一个人物、敌人、一件家具、一块小石头都是可以粉碎的,那么他们就需要从这个类中继承可粉碎的,可能需要多重继承。如果不希望有多个继承,那么它们必须继承一个类似于类的公共实体。然而,添加虚拟方法并不是很优雅 smash() 只有在其中一些可以被粉碎的情况下,才能成为实体。

对于动态类型语言,这不是问题。duck输入确保您只需要定义 smash() 在需要的地方运行,就是这样。不需要考虑继承、基类等。

func _on_object_hit(object):
    object.smash()

就这样。如果击中大石头的对象有一个smash()方法,它将被调用。不需要继承或多态性。动态类型语言只关心具有所需方法或成员的实例,而不关心它继承的内容或类类型。duck类型的定义应该更清楚:

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

在这种情况下,它转换为:

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

是的,我们应该称之为“笨重的打字”。

可能被击中的对象没有smash()函数。有些动态类型语言在方法调用不存在时(如目标C)会忽略它,但gdscript更严格,因此需要检查函数是否存在:

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

然后,简单地定义这个方法以及任何岩石接触到的东西都可以被粉碎。