Godot的幾個附加指令碼和進行繼承時比較特別的特性

2023-06-04 06:00:17

注: 這是在Godot4.0中總結出的內容,並且語言是C#。
特別的,下面有的特性和C#關係比較大。

基本特性

在Godot中,為某個節點編寫特別的程式碼時,需要為節點新建指令碼,或參照已有指令碼。

參照指令碼時,填入指令碼路徑即可,相當於是複用程式碼了。

新建指令碼時,一般做法是新建一個自定義型別,並且這個型別繼承自原有節點的型別。

其實,你也可以不繼承自原有節點的那個型別。下面的各小節文章均是針對這個情況的。

在選擇繼承的目標時,你可以選擇的範圍有:(最常規的)繼承自原有型別、繼承自自定義型別、繼承自"中間"型別。

繼承自自定義型別

繼承自自定義型別,一般來說是為了滿足這個需求的:

使用者為某節點編寫了一個自定義了型別後,在新的情境下,需要擴充這個型別,讓它有新的功能,並且舊的不能變。
此時,程式設計師一般第一個想到的就是繼承。Godot順理成章地允許使用者這樣進行繼承。

想要進行這樣的繼承時,為節點新建指令碼時必須先選擇已有型別,若直接填入想繼承的型別,Godot不允許這樣做。

建立完成後,使用者可以開啟指令碼檔案,手動修改繼承型別。

雖然看上去就像作弊,但是Godot方面認可這種做法,也有人建議在建立指令碼時,"繼承"內容框可以選擇自定義型別進行繼承,但是Godot的開發者們暫時並沒有這麼做。

需要注意的是,這種做法對此指令碼附加到的節點有要求。

  • 首先,你在編寫一系列一層一層繼承起來的類時,你建立起來的最底層的那個自定義類肯定是繼承自"原生型別"的
    • "原生型別"就是新建節點時,你能在面板裡選到的型別(沒錯,這裡找不到你自定義的型別,它們100%是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