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_Implementation を override します。関数名の末尾に _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 が、どこを通って実装に届くかを追います。
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
鉄の剣
AArmorItem
革の胸当て
APotionItem
回復ポーション
AGemItem
魔石
AKeyItem
地下室の鍵
AEnchantedSword
呪われた剣
✓
// 性質ごとに 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++ の関数は不要なら
FGameplayTagContainer(Item.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 の使い分けから始めます。