GDScript:動態語言簡介¶
關於¶
本教學旨在提供一個更有效率使用 GDScript 的參考文件,並著重於 GDScript 中常見情形,同時也包含許多動態型別語言的資訊。
也希望本教學能為僅有少數動態型別語言經驗或完全沒經驗的程式設計師提供幫助。
動態性¶
動態型別的優缺點¶
GDScript 是動態型別語言。因此,GDScript 有下列優點:
GDScript 簡單易學。
可以方便地撰寫或修改大部分程式碼。
寫更少的程式碼,代表需要修正的錯誤與失誤更少。
更容易閱讀程式碼(混亂的部分更少)。
要測試時不需要編譯。
執行環境很小。
具有鴨子型別與多型。
而主要缺點如下:
與靜態型別相比效能較差。
比較難進行重構(無法追蹤符號)
在靜態型別語言中通常在編譯時偵測的錯誤只有在執行程式碼時才會出現(因為運算式解析比較嚴格)。
程式碼自動補全的靈活讀比較低(某些變數型別只有在執行時才可知)。
也就是說,實際上,這代表 Godot + GDScript 這樣的組合時設計來快速並有效率地製作遊戲。對於需要大量計算且無法通過 Godot 內建工具 (如 Vector 型別、物理引擎、數學函式…等) 來獲益的遊戲,也可以使用 C++。這樣一來便能以 GDScript 製作大部分的遊戲,並在需要改善效能的部分使用一點 C++。
變數與賦值¶
在動態型別語言中所有的變數都類似「Variant 型別」。這表示變數的型別並非固定,且只會在賦值時更改。如:
靜態語言:
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 PoolArray types) are passed by value to functions (value is copied). Everything else (instances, Arrays, Dictionaries, etc) is passed as reference. Classes that inherit Reference (the default if nothing is specified) will be freed when not used, but manual memory management is allowed too if inheriting manually from Object.
備註
A value is passed by value when it is copied every time it's specified as a function parameter. One consequence of this is that the function cannot modify the parameter in a way that is visible from outside the function:
func greet(text):
text = "Hello " + text
func _ready():
# Create a String (passed by value and immutable).
var example = "Godot"
# Pass example as a parameter to `greet()`,
# which modifies the parameter and does not return any value.
greet(example)
print(example) # Godot
A value is passed by reference when it is not copied every time it's specified as a function parameter. This allows modifying a function parameter within a function body (and having the modified value accessible outside the function). The downside is that the data passed as a function parameter is no longer guaranteed to be immutable, which can cause difficult-to-track bugs if not done carefully:
func greet(text):
text.push_front("Hello")
func _ready():
# Create an Array (passed by reference and mutable) containing a String,
# instead of a String (passed by value and immutable).
var example = ["Godot"]
# Pass example as a parameter to `greet()`,
# which modifies the parameter and does not return any value.
greet(example)
print(example) # [Hello, Godot] (Array with 2 String elements)
Compared to passing by value, passing by reference can perform better when using large objects since copying large objects in memory can be slow.
Additionally, in Godot, base types such as String are immutable. This means that modifying them will always return a copy of the original value, rather than modifying the value in-place.
陣列¶
動態型別中的陣列可以包含各種混合的不同資料型別,且永遠為動態 (可隨時調整大小)。與靜態型別語言的陣列比較範例:
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 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¶
在某些靜態型別語言中迭代可能很困難:
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() 函式有三個引數:
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
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() 方法,則會呼叫 smash()。不需要使用繼承或多型。動態型別語言只關心實體是否有需要的方法或成員,而不管實體繼承哪個類別。瞭解鴨子型別的定義應該可以更清楚:
「如果看到一隻鳥,像鴨子一樣走路、像鴨子一樣游泳、有像鴨子一樣的叫聲,那我們就叫這隻鳥鴨子」
這時,我們可以把這句話翻成:
「若物件可以被砸碎,不管是什麼物件,把它砸碎就對了。」
當然,我們也可以稱這個方法為「綠巨人浩克型別」。
It's possible that the object being hit doesn't have a smash() function. Some dynamically typed languages simply ignore a method call when it doesn't exist, but GDScript is stricter, so checking if the function exists is desirable:
func _on_object_hit(object):
if object.has_method("smash"):
object.smash()
接著,只需要在被石頭碰到可以砸碎的物件上定義 smash 方法就好了。