UEC++ 介面

2022-09-23 12:00:44

詞義廣泛,用來陳述功能,選項,與其他程式結構進行溝通的方式。介面抽象出了互動結構,提供了兩個未知邏輯互動的便捷性。對於程式設計中,如何更好的設計低耦合程式起到了至關重要的作用。設計者可以在互不關心的情況下,進行友好的程式設計,並且通過介面來完成設計的整合互動。

虛幻引擎中,加入了介面設計,從一定程度上「去掉了」多繼承。介面可以幫助我們解決在不同型別的類之間卻有相同行為的特性。介面的設計增加了程式碼編寫的便捷性。

例如在設計射擊類遊戲時,我們需要子彈與場景中的物體進行互動,場景中的桌椅板凳,角色,怪物(都是獨立的物件)都希望受到子彈的攻擊傷害。那麼子彈在打到目標後要逐一排查,審查目標是否屬於上述的物件!這很麻煩!但是我們可以通過介面,增加上述目標具有受傷的能力。當子彈打到目標時,我只需要檢查目標是否繼承受傷的介面,如果有,則呼叫介面函數即可!

構建介面類:

我們可以直接在虛幻編輯器中繼承介面類,然後完成構建

編寫介面:

  • 如果在C++中希望獲得介面能力,則需要繼承介面。需要注意的是,必須繼承I開頭的介面名稱,並且繼承修飾為public。不要嘗試重寫介面中的函數!
  • 如果介面中的函數使用BlueprintNativeEvent說明,則在繼承類中可以編寫同名函數,並用字尾「_Implementation」進行標記。
  • 如果介面中的函數使用BlueprintImplementableEvent說明,則無法在C++的繼承類中實現介面函數

實現介面:

繼承I類介面完畢後,可以選擇的將介面中的函數進行定義。如果需要定義,則需要將介面中函數說明是BlueprintNativeEvent的函數進行定義。

注意,不要省略override,函數的返回值,參數列需要和介面的一致

呼叫操作:

呼叫函數,持有繼承介面物件指標,第一步先轉換到I類指標,呼叫Execute_介面函數名,引數第一位需要傳遞原物件指標,後面直接按照原函數引數填入即可

整體程式碼演示:

//  TestInterface.h
UINTERFACE(MinimalAPI)
class UTestInterface : public UInterface
{
    GENERATED_BODY()
};

/**
 * 1、U類中不能去寫介面函數,只能用來檢查是否繼承了介面類
 * 2、介面函數,必須寫在I類中,並且必須寫共有域中
 * 3、介面函數在介面類中不能進行定義
 * 
 */
class MX_API ITestInterface
{
    GENERATED_BODY()

    // Add interface functions to this class. This is the class that will be inherited to implement this interface.
public:
    // 定義介面函數
    UFUNCTION(BlueprintNativeEvent)
    void Notify_None();
    UFUNCTION(BlueprintNativeEvent)
    int32 Notify_RetVal();
    UFUNCTION(BlueprintNativeEvent)
    int32 Notify_RetVal_Params(int32 Num);
};

/////////////////////////////////////////////////////////////
// Actor2.h
public:    
    // Called every frame
    virtual void Tick(float DeltaTime) override;

    // 實現介面
    virtual void Notify_None_Implementation() override;
    virtual int32 Notify_RetVal_Implementation() override;
    virtual int32 Notify_RetVal_Params_Implementation(int32 Num) override;

// Actor2.cpp
void AActor2::Notify_None_Implementation()
{
    UKismetSystemLibrary::PrintString(this, TEXT("----無參無返回值----"));
}

int32 AActor2::Notify_RetVal_Implementation()
{
    UKismetSystemLibrary::PrintString(this, TEXT("----無參有返回值----"));
    return 0;
}

int32 AActor2::Notify_RetVal_Params_Implementation(int32 Num)
{
    UKismetSystemLibrary::PrintString(this, TEXT("----有參有返回值----"));
    return Num;
}void AActor2::Notify_None_Implementation()
{
    UKismetSystemLibrary::PrintString(this, TEXT("----無參無返回值----"));
}

int32 AActor2::Notify_RetVal_Implementation()
{
    UKismetSystemLibrary::PrintString(this, TEXT("----無參有返回值----"));
    return 0;
}

int32 AActor2::Notify_RetVal_Params_Implementation(int32 Num)
{
    UKismetSystemLibrary::PrintString(this, TEXT("----有參有返回值----"));
    return Num;
}
/////////////////////////////////////////////////////////
//Actor1.cpp
    ac2 = GetWorld()->SpawnActor<AActor2>(AActor2::StaticClass());
    // 檢查是否繼承了介面
    ITestInterface* testInterface = Cast<ITestInterface>(ac2);
    // 如果繼承了介面,就執行介面函數
    if (testInterface) {
        testInterface->Execute_Notify_None(ac2);
        testInterface->Execute_Notify_RetVal(ac2);
        testInterface->Execute_Notify_RetVal_Params(ac2,10);
    }

測試結果:

 

 

 包裹介面:

藉助模板類TScriptInterface可以將介面包裹,用於使用UPROPERTY描述,並且可以暴露到藍圖中。使用時同普通介面一樣使用。介面不支援智慧指標的管理,所以需要使用TS類進行管理

    UPROPERTY(EditAnywhere)
        TScriptInterface<ITestInterface> TestInterface;

藍圖繼承介面:

如果介面在藍圖中被繼承,則需要注意下面的問題

  • 如果函數沒有返回型別,則在藍圖中當作事件Event使用
  • 如果函數存在返回型別或是存在傳遞參照引數,則在藍圖中當作函數使用
  • 介面函數說明符使用BlueprintNativeEvent或是BlueprintImplementableEvent標記都可以在藍圖中找到

總結:

  • 介面函數需要定在I開頭的類中,不要修改存取域public關鍵字,宣告需要使用宏標記BlueprintNativeEvent或BlueprintImplementableEvent
  • 如需繼承介面,繼承I類,繼承關係public
  • 介面中的函數禁止重寫
  • 在繼承類中實現介面函數,並新增字尾_Implementation,需要注意,函數前加入虛擬函式關鍵字virtual,函數結尾加override關鍵字(可以不新增,但是建議加上,加強函數編寫正確性檢查),在CPP檔案中實現邏輯
  • 呼叫函數,持有繼承介面物件指標,第一步先轉換到I類指標,呼叫Execute_介面函數名,引數第一位需要傳遞原物件指標,後面直接按照原函數引數填入即可
  • 檢查某一個類是否實現了對應介面可以使用如下語法進行檢查
    • obj->GetClass()->ImplementsInterface(U型別::StaticClass());
    • act->GetClass()->ImplementsInterface(UMyInterface::StaticClass());
    • act是物件指指標

介面的優缺點:

優點:

  • 具備多型特性,介面衍生類支援里氏轉換原則
  • 介面可以使得整個繼承系統更加的乾淨單一
  • 介面可以規範類的具體行為
  • 介面可以隔離開發中的開發耦合,我們只需要針對介面去編碼,無需關心具體行為
  • 介面繼承可以使得繼承關係中出現真正的操作父類別

缺點:

  • 丟失了C++中的廣泛繼承特性
  • 介面拘束了型別的屬性拓展,無法進行更詳細的內容定義
  • 繼承關係中容易讓人混淆,介面本身不具備真正的繼承特性