注: 這是在Godot4.0中總結出的內容,並且語言是C#。
特別的,下面有的特性和C#關係比較大。
在Godot中,為某個節點編寫特別的程式碼時,需要為節點新建指令碼,或參照已有指令碼。
參照指令碼時,填入指令碼路徑即可,相當於是複用程式碼了。
新建指令碼時,一般做法是新建一個自定義型別,並且這個型別繼承自原有節點的型別。
其實,你也可以不繼承自原有節點的那個型別。下面的各小節文章均是針對這個情況的。
在選擇繼承的目標時,你可以選擇的範圍有:(最常規的)繼承自原有型別、繼承自自定義型別、繼承自"中間"型別。
繼承自自定義型別,一般來說是為了滿足這個需求的:
使用者為某節點編寫了一個自定義了型別後,在新的情境下,需要擴充這個型別,讓它有新的功能,並且舊的不能變。
此時,程式設計師一般第一個想到的就是繼承。Godot順理成章地允許使用者這樣進行繼承。
想要進行這樣的繼承時,為節點新建指令碼時必須先選擇已有型別,若直接填入想繼承的型別,Godot不允許這樣做。
建立完成後,使用者可以開啟指令碼檔案,手動修改繼承型別。
雖然看上去就像作弊,但是Godot方面認可這種做法,也有人建議在建立指令碼時,"繼承"內容框可以選擇自定義型別進行繼承,但是Godot的開發者們暫時並沒有這麼做。
需要注意的是,這種做法對此指令碼附加到的節點有要求。
讀了上面的內容時,你也許會意識到,其實任何一個自定義型別底層的"原生型別",可以是它附加的節點的繼承樹中大於等於Node
的型別之中的任何一個型別。
我稱這種做法為"繼承自中間型別"。
事實上,也許需要調整一下觀念。大家一開始似乎會認為,節點附加了一個自定義型別後,節點就是自定義型別的範例了。
實際情況似乎要稍微割裂一點。因為繼承自中間型別後,你會發現,這個自定義型別無法完全描述這個節點。請看接下來的例子。
這裡,我給一個精靈Sprite2d
掛上這樣一個自定義型別,它繼承自Node。
public partial class TNode : Node
{
}
我想知道,這個節點從C#繼承體系的角度看,還是不是"精靈(Sprite2D
)"了
讓我們在根節點中寫一點程式碼,試圖瞭解該節點的型別。
public partial class AskForClass : Node2D
{
public override void _Ready()
{
var t1node = GetNode("TNode");
//方法1 列印繼承樹
Type tobj;
tobj = t1node.GetType();
while (tobj != null)
{
GD.Print(tobj.Name);
tobj = tobj.BaseType;
}
GD.Print("以上是繼承樹。");
//方法2 型別轉換。轉換失敗的話就是null。
var t2node = t1node as Sprite2D;
GD.Print(t2node);
//方法3 我發現Godot有一個IsClass()方法
GD.Print("is sprite2D class?" + t1node.IsClass("Sprite2D"));
}
}
(小提示:Godot的_Ready()
函數被執行順序是先子節點,再父節點,這樣巢狀的,所以父節點存取子節點總是萬無一失的,當然,這個範例就算順序不是這樣也不存在問題)
上面用了3種方式試圖確認我們的t1node
有沒有Sprite2D
的成分,以及Sprite2D
的成分通過哪種方式能查到,猜猜看?
結論是:
TNode
Node
GodotObject
Object
以上是繼承樹。
null
is sprite2D class?True
除非使用Godot提供的函數IsClass()
, 光靠C#的繼承體系,查不到Sprite2D
的成分, 而且範例無法轉換成Sprite2D
型別,這樣就不能以C#通常的方式操作Sprite2D
特有的函數和變數了。
在C#內,雖然它"放棄了作為Sprite2D
的身份",但我們的節點在執行時仍然做著一個"精靈"會做的事情,比如我在編輯器裡對它的位置和旋轉進行了變更,這些變更都沒有丟失。
個人推測,此時需要使用GetIndexed()
和SetIndexed()
等方法來操作那些無法直接存取的東西。
這個範例和實際存在於Godot執行時的節點竟然有這樣的不同。
以後也許時不時需要想起來這樣一件事——C#範例和Godot內的節點只是連在一起,有一個對映的關係罷了,並不100%是那個節點本身。
節點除了有C#能提供給它的函數和欄位外,還可以擁有相當突出的Godot賦予它的不同型別的功能,即各種型別的"原生節點"的各種各樣的功能。
閱讀檔案後,大概可以這樣理解,Godot執行時維護的節點身上的函數和變數的表現更符合動態語言的特徵,而不是靜態型別語言。
不論是C#指令碼、Godot指令碼、還是"原生型別"的節點,行為都是將自己的各種功能附加或覆蓋到了節點身上。
Godot is very dynamic. An object's script, and therefore its properties, methods and signals, can be changed at run-time. Because of this, there can be occasions where, for example, a property required by a method may not exist. To prevent run-time errors, see methods such as set, get, call, has_method, has_signal, etc. Note that these methods are much slower than direct references.
Godot很是動態。物件的指令碼及其屬性、方法和訊號可以在執行時更改。因此,在某些情況下,例如,方法所需的屬性可能不存在。若要防止執行時錯誤,請參閱設定、獲取、呼叫、has_method、has_signal等方法。請注意,這些方法比直接參照慢得多。
上述特性可能會引發一個問題,當你需要用C#找到場景中的所有某一原生型別的節點時,從"中間"繼承的節點被獲取後,由於一些身份被放棄了,有可能被漏掉!
也就是說,在上面的案例中,想找Sprite2D
時,用下面的方法,掛載了TNode
指令碼的精靈將被跳過,儘管它這麼大一個放在螢幕上。
List<Sprite2D> lst = new List<Sprite2D>();
var children = FindChildren("*");
foreach (var chi in children)
{
if (chi is Sprite2D sp)
{
lst.Add(sp);
GD.Print("這個是精靈" + chi.Name);
}
else
{
GD.Print("這個不是精靈" + chi.Name);
}
}
我沒有測試GDScript,不知道是否情況會不同。也許它支援多重繼承?
綜上所述,個人建議儘量避免附加指令碼時從中間繼承。
實在有這樣的需求,要麼避免一個型別的每一個身份都需要被C#直接操作,要麼用IsClass()
配合GetIndexed()
和SetIndexed()
等方法處理該物件。
參考:
https://godotengine.org/qa/141137/best-way-to-add-a-node-that-extends-a-custom-class
https://docs.godotengine.org/en/latest/classes/class_object.html