Chapter 01

インターフェース
で「できること」を約束する

インターフェース(Interface)は、クラスが 何を継承しているか ではなく 何ができるか を宣言する仕組みです。ドアでも宝箱でも NPC でも、IInteractable を実装していれば「調べられる」とみなせます。呼び出す側は相手の正体を知る必要がありません。

01
多重継承の代わり
UE の Actor は 1 つの親しか持てません。インターフェースは何個でも足せます。
02
Blueprint とつながる
C++ で宣言した関数を BP 側で実装できます。デザイナーとの分担がしやすくなります。
03
モジュール間の依存を切る
呼び出す側は具体クラスの型を include しなくて済みます。
CHAPTER 02

継承でやろうとすると詰まる

「調べられる物」に共通の基底クラスを作る設計から始めてみます。切り替えて比べてください。

AActor
ABaseInteractable
ADoor
AChest
ANPC
!
ANPC は ACharacter を継承したい。しかし親は 1 つだけなので ABaseInteractable にはできません。「調べられる」を足すだけのために、継承ツリー全体を作り直すことになります。
// 呼び出し側は「基底クラスか?」を毎回聞く
void AMyCharacter::TryInteract(AActor* Target)
{
    if (ABaseInteractable* B = Cast<ABaseInteractable>(Target))
    {
        B->Interact(this);
    }
    else if (ANPC* N = Cast<ANPC>(Target))
    {
        N->TalkTo(this);  // 例外が増えていく
    }
}
AActor
ADoor
AChest
ACharacter
ANPC
IInteractable
3 つのクラスが、継承ツリーを変えずに同じ約束を実装しています。
呼び出し側の分岐が消えました。新しく「調べられる物」を追加しても、TryInteract は一行も変わりません。
// 相手の正体を知らないまま呼べる
void AMyCharacter::TryInteract(AActor* Target)
{
    if (Target->Implements<UInteractable>())
    {
        IInteractable::Execute_Interact(Target, this);
    }
}
CHAPTER 03

登場人物を確認する

インターフェースを 1 つ作ると、実はクラスが 2 つできます。図の要素をクリックすると、それぞれの役割が右に出ます。

Execute_Interact(Target, this)
implements
IInteractable — 関数の宣言
インターフェースを使う側です。相手が ADoor なのか BP なのかを知りません。Implements<UInteractable>() で確認し、Execute_ 付きの静的関数で呼び出します。ここに具体クラスの include が現れないことが利点です。
UHT(Unreal Header Tool)がリフレクションに登録するための、実装を持たない空のクラスです。GENERATED_BODY() だけを書きます。Implements<UInteractable>() や BP の Class Settings で指定するのは、こちらの U 側の型です。関数は書きません。
実際に使う関数を宣言する側です。実装クラスが多重継承するのはこちらで、Execute_Interact のような静的関数も UHT がこのクラスに生成します。C++ で呼ぶときの窓口は常に I 側です。
C++ での実装クラスです。public AActor, public IInteractable と多重継承し、Interact_Implementationoverride します。関数名の末尾に _Implementation が付く点に注意してください。
Blueprint での実装です。Class Settings → Implemented Interfaces に Interactable を追加すると、イベントグラフに Event Interact が現れます。C++ 側は BP か C++ かを区別せず同じ呼び方で動きます。
CHAPTER 04

ヘッダを 1 行ずつ読む

コードの行、または右のカードにマウスを乗せると対応する箇所が光ります。

Interactable.h
#pragma once
#include "CoreMinimal.h"
#include "UObject/Interface.h"
#include "Interactable.generated.h"
UINTERFACE(MinimalAPI, BlueprintType)
class UInteractable : public UInterface
{
GENERATED_BODY()
};
class MYGAME_API IInteractable
{
GENERATED_BODY()
public:
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "Interaction")
void Interact(AActor* Interactor);
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "Interaction")
FText GetInteractionPrompt() const;
};
UINTERFACE(...)
MinimalAPI は生成コードの露出を最小にする指定、BlueprintType は BP から変数・引数として扱えるようにする指定です。BP で実装させたいなら Blueprintable も付けます。
U + クラス名
リフレクション登録用の空クラス。ここに関数を書いてはいけません。型として参照するときだけ使います(Implements<UInteractable>())。
GENERATED_BODY()
UHT が生成したコードを差し込む場所です。両方のクラスに必要で、Execute_Interact はここから生えてきます。.generated.h の include は必ず最後に書きます。
I + クラス名
関数を宣言する側で、実装クラスが継承するのもこちらです。MYGAME_API(自分のモジュール名 + _API)を付けて他モジュールから使えるようにします。
BlueprintNativeEvent
C++ に既定の実装(Interact_Implementation)を置き、BP 側で上書きできる指定です。C++ 実装を持たせず BP 専用にするなら BlueprintImplementableEvent、逆に BP から実装させないなら通常の virtual 関数にします。
CHAPTER 05

E キーを押してから、ドアが開くまで

1 回の Execute_Interact が、どこを通って実装に届くかを追います。

STEP 01 / 6
E キーが押される
プレイヤーが E キーを押し、Enhanced Input から AMyCharacter::TryInteract() が呼ばれます。この時点では対象が何かは分かっていません。
カメラの前方 300cm に線を飛ばし、当たったアクタを Hit.GetActor() で受け取ります。返ってくるのは AActor* だけです。
Target->Implements<UInteractable>() でリフレクション情報を照会します。BP で実装されたクラスでも true になります。Cast<IInteractable> は C++ 実装しか拾えないため、ここでは使いません。
IInteractable::Execute_Interact(Target, this) を呼びます。UHT が生成した静的関数で、第 1 引数は オブジェクト側(UObject*)です。この形が BP 実装まで届く唯一の呼び方です。
Execute_ の内部で FindFunctionChecked により実際の関数が引かれ、BP のイベントグラフか C++ の Interact_Implementation のどちらかへ振り分けられます。呼び出し側のコードは変わりません。
ADoor::Interact_Implementation が走り、bIsOpen を反転させてドアを回転させます。呼び出し側は結果を知る必要すらありません。
MyCharacter.cpp
void AMyCharacter::TryInteract()
{
FHitResult Hit;
const FVector Start = Camera->GetComponentLocation();
const FVector End = Start + Camera->GetForwardVector() * 300.f;
if (!GetWorld()->LineTraceSingleByChannel(
Hit, Start, End, ECC_Visibility))
{
return;
}
AActor* Target = Hit.GetActor();
if (Target && Target->Implements<UInteractable>())
{
IInteractable::Execute_Interact(Target, this);
}
}
CHAPTER 06 — 実例 1

Interactable:E キーで調べる

ドアを実装します。タブを切り替えて、ヘッダ・実装・Blueprint 側の手順を確認してください。

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Interactable.h"
#include "Door.generated.h"

UCLASS()
class MYGAME_API ADoor : public AActor, public IInteractable
{
    GENERATED_BODY()

public:
    // _Implementation を付けて override する
    virtual void Interact_Implementation(AActor* Interactor) override;
    virtual FText GetInteractionPrompt_Implementation() const override;

protected:
    UPROPERTY(VisibleAnywhere, Category = "Door")
    bool bIsOpen = false;

    UPROPERTY(EditAnywhere, Category = "Door")
    float OpenYaw = 90.f;
};
#include "Door.h"

void ADoor::Interact_Implementation(AActor* Interactor)
{
    bIsOpen = !bIsOpen;
    SetActorRotation(FRotator(0.f, bIsOpen ? OpenYaw : 0.f, 0.f));

    UE_LOG(LogTemp, Log, TEXT("%s opened by %s"),
        *GetName(), *GetNameSafe(Interactor));
}

FText ADoor::GetInteractionPrompt_Implementation() const
{
    return bIsOpen
        ? NSLOCTEXT("Door", "Close", "閉める")
        : NSLOCTEXT("Door", "Open",  "開ける");
}
Blueprint 側の手順
STEP 1
BP を開き Class Settings → Interfaces → Implemented Interfaces に Interactable を追加します。
STEP 2
My Blueprint パネルの Interfaces に Interact が現れます。右クリック → Implement event でイベントノードを置きます。
STEP 3
戻り値のある関数(GetInteractionPrompt)は Functions 側に生成されるので、Override で実装します。
STEP 4
C++ から呼ぶときは Execute_Interact のまま。BP から呼ぶ場合は対象ピンに Actor を挿して Interact ノードを繋ぎます。
C++ 側で Interact_Implementation を定義していれば、それが既定の動きになり、BP でイベントを置いた場合は BP の実装が優先されます(親を呼びたいときは Parent: Interact ノードを使います)。
CHAPTER 07 — 実例 2

Damageable:ダメージを受け取る

戻り値のある関数と、インターフェースを変数として保持する TScriptInterface を扱います。

UINTERFACE(MinimalAPI, BlueprintType)
class UDamageable : public UInterface
{
    GENERATED_BODY()
};

class MYGAME_API IDamageable
{
    GENERATED_BODY()

public:
    UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "Combat")
    void ApplyDamage(float Amount, AActor* Causer);

    // 戻り値があっても同じ書き方で使える
    UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "Combat")
    bool IsAlive() const;

    // BP に実装を強制したい(C++ 既定を持たない)場合
    UFUNCTION(BlueprintImplementableEvent, Category = "Combat")
    void OnDamageFeedback(float Amount);
};
void AEnemyCharacter::ApplyDamage_Implementation(float Amount, AActor* Causer)
{
    if (!IsAlive_Implementation())
    {
        return;
    }

    Health = FMath::Max(0.f, Health - Amount);
    OnDamageFeedback(Amount);   // BP で実装された演出を呼ぶ

    if (Health <= 0.f)
    {
        Die();
    }
}

bool AEnemyCharacter::IsAlive_Implementation() const
{
    return Health > 0.f;
}

// 呼び出し側(弾丸など)
void AProjectile::OnHit(AActor* Other)
{
    if (Other && Other->Implements<UDamageable>())
    {
        const bool bAlive = IDamageable::Execute_IsAlive(Other);
        if (bAlive)
        {
            IDamageable::Execute_ApplyDamage(Other, Damage, GetOwner());
        }
    }
}
// エディタで「ダメージを受けられるもの」だけ挿せるピンになる
UPROPERTY(EditInstanceOnly, Category = "Combat")
TScriptInterface<IDamageable> DefaultTarget;

void ATurret::Fire()
{
    if (!DefaultTarget)   // UObject 側が有効かを見てくれる
    {
        return;
    }

    // C++ 実装だけを呼ぶ(BP 実装は走らない)
    DefaultTarget->ApplyDamage_Implementation(10.f, this);

    // BP 実装も含めて正しく呼ぶならこちら
    IDamageable::Execute_ApplyDamage(DefaultTarget.GetObject(), 10.f, this);
}
!
TScriptInterface は UObject ポインタとインターフェースポインタを 1 組で持つ型で、UPROPERTY にできる(GC される・エディタで指定できる)のが利点です。ただし -> で直接呼ぶと C++ 実装しか走りません。BP 実装まで届かせたい関数は必ず Execute_ 経由で呼んでください。
CHAPTER 08 — 実例 3

アイテムを「カテゴリ」で分類する

武器・防具・ポーション・鍵。それぞれ別のクラスで、共通の親を作るとすぐ破綻します。剣は「装備できる」「売れる」、ポーションは「使える」「売れる」「重ねられる」、鍵は「クエスト用で売れない」。こうした性質の組み合わせは、インターフェースを性質ごとに 1 枚ずつ用意して重ねるのが素直な設計です。列挙型(EItemCategory)で分類する方法と違い、増えたときに switch が育ちません。

インベントリを絞り込む
カテゴリを選ぶと、そのインターフェースを実装しているアイテムだけが残ります。実行時にやっているのはこれと同じ照会です。
AWeaponItem
IEquippable ISellable
AArmorItem
IEquippable ISellable
APotionItem
IConsumable ISellable IStackable
AGemItem
ISellable IStackable
AKeyItem
IQuestItem
AEnchantedSword
IEquippable IConsumable ISellable
// 性質ごとに 1 枚。名前は形容詞にすると迷いません

UINTERFACE(MinimalAPI, BlueprintType)
class UEquippable : public UInterface { GENERATED_BODY() };

class MYGAME_API IEquippable
{
    GENERATED_BODY()
public:
    UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "Item")
    EEquipSlot GetEquipSlot() const;

    UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "Item")
    void OnEquipped(APawn* Owner);
};

// --- 使える ---
UINTERFACE(MinimalAPI, BlueprintType)
class UConsumable : public UInterface { GENERATED_BODY() };

class MYGAME_API IConsumable
{
    GENERATED_BODY()
public:
    UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "Item")
    void Consume(APawn* User);
};

// --- 売れる ---
UINTERFACE(MinimalAPI, BlueprintType)
class USellable : public UInterface { GENERATED_BODY() };

class MYGAME_API ISellable
{
    GENERATED_BODY()
public:
    UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "Item")
    int32 GetSellPrice() const;
};

// IStackable, IQuestItem も同じ形で並べる
// インベントリは AActor* / UObject* のまま持てばよい
UPROPERTY()
TArray<TObjectPtr<AItemBase>> Items;

// カテゴリ = インターフェースで絞り込む(テンプレートで共通化)
template<typename TInterface>
TArray<AItemBase*> UInventoryComponent::FilterByCategory() const
{
    TArray<AItemBase*> Result;
    for (AItemBase* Item : Items)
    {
        if (Item && Item->Implements<TInterface>())
        {
            Result.Add(Item);
        }
    }
    return Result;
}

// 装備タブを開いたとき
void UInventoryComponent::ShowEquipTab()
{
    for (AItemBase* Item : FilterByCategory<UEquippable>())
    {
        const EEquipSlot Slot = IEquippable::Execute_GetEquipSlot(Item);
        AddRow(Item, Slot);
    }
}

// 売却総額。ISellable を実装したものだけが加算される
int32 UInventoryComponent::GetTotalSellValue() const
{
    int32 Total = 0;
    for (AItemBase* Item : Items)
    {
        if (Item && Item->Implements<USellable>())
        {
            Total += ISellable::Execute_GetSellPrice(Item);
        }
    }
    return Total;   // 鍵は売れないので自然に除外される
}

// BP から呼びたいときは UClass* を受ける版を用意する
UFUNCTION(BlueprintCallable, Category = "Inventory",
    meta = (DeterminesOutputType = "Category"))
TArray<AItemBase*> FilterByInterface(TSubclassOf<UInterface> Category) const;
// 親は AItemBase(表示名やアイコンなど、全アイテム共通のものだけ)
// カテゴリは継承ではなくインターフェースで足す

UCLASS()
class AWeaponItem : public AItemBase,
                    public IEquippable, public ISellable
{
    GENERATED_BODY()
public:
    virtual EEquipSlot GetEquipSlot_Implementation() const override
        { return EEquipSlot::MainHand; }

    virtual void OnEquipped_Implementation(APawn* Owner) override;

    virtual int32 GetSellPrice_Implementation() const override
        { return BasePrice * (1.f - Wear); }
};

UCLASS()
class APotionItem : public AItemBase,
                    public IConsumable, public ISellable, public IStackable
{
    GENERATED_BODY()
public:
    virtual void Consume_Implementation(APawn* User) override;
    virtual int32 GetSellPrice_Implementation() const override { return 25; }
    virtual int32 GetMaxStack_Implementation() const override { return 99; }
};

UCLASS()
class AKeyItem : public AItemBase, public IQuestItem
{
    GENERATED_BODY()
    // ISellable を実装しない = 売れない。判定コードは要らない
};

// 魔法の剣を足したいとき(装備できて、使えて、売れる)
UCLASS()
class AEnchantedSword : public AItemBase,
                        public IEquippable, public IConsumable, public ISellable
{
    GENERATED_BODY()
    // 既存クラスの継承ツリーには一切触らない
};
// enum で分類した場合
UENUM(BlueprintType)
enum class EItemCategory : uint8 { Weapon, Armor, Potion, Quest };

void UInventoryComponent::Use(AItemBase* Item)
{
    switch (Item->Category)
    {
    case EItemCategory::Potion: Item->Drink(); break;
    case EItemCategory::Weapon: Item->Equip(); break;
    // 「装備できて飲める剣」を追加した瞬間に破綻する
    // カテゴリを足すたび、全ての switch を探して直す
    }
}
ENUM が向くとき
分類が排他的で、増えないと分かっている場合。UI のソート順やアイコン選択など、単なるラベルとして使うとき。セーブデータに保存するのも簡単です。
インターフェースが向くとき
分類が重なるとき、そして分類ごとにできる操作が違うとき。「売れる物は価格を返せる」のように、カテゴリと操作をセットで持てます。
Gameplay Tag という第三の手
階層のあるラベルだけが欲しく、C++ の関数は不要なら FGameplayTagContainerItem.Weapon.Sword)が適します。操作が伴うならインターフェース、印だけならタグ、と分けて考えます。
CHAPTER 09

理解度チェック

4 問。選ぶと解説が出ます。正解 0 / 4

Q1 BP で実装された Interact を C++ から呼ぶ、正しい書き方はどれですか。
正解は 3 番目です。Cast<IInteractable> は C++ で継承したクラスしか拾えず、BP 実装では nullptr が返ります。
Q2 関数の宣言を書くのは、U 側と I 側のどちらのクラスですか。
I 側です。U 側は GENERATED_BODY() だけの空クラスで、型として参照するためだけに存在します。
Q3 BlueprintNativeEvent の Interact を C++ で実装するとき、定義する関数名は。
Interact_Implementation です。Execute_Interact は UHT が生成する呼び出し用の静的関数で、自分で定義するものではありません。
Q4 TScriptInterface<IDamageable> を -> で直接呼ぶと何が起きますか。
C++ の実装しか走らず、BP 側のイベントは無視されます。BP 実装まで届かせるには Execute_ApplyDamage(Obj.GetObject(), ...) を使います。
次はデリゲート編。Dynamic / Multicast の使い分けから始めます。