最初に
Qiita初投稿&UEC++勉強中の身なので記事が見づらい&コードの書き方が変な点があるかもしれないのでご容赦ください
経緯
現在個人で開発しているゲームに「プレイヤーと接触しているアクタを特定のボタンを押すことで掴める」という仕様が追加されたので、Grabbableというインタフェースを作成して掴まれる側のアクタにそれ実装してプロトタイプを組んでみることにしました。
実装
まずは掴まれる側の処理から
Grabbableインタフェース(C++)
#pragma once
#include "CoreMinimal.h"
#include "UObject/Interface.h"
#include "Grabbable.generated.h"
UINTERFACE(MinimalAPI)
class UGrabbable : public UInterface
{
GENERATED_BODY()
};
class PROTOTYPE_API IGrabbable
{
GENERATED_BODY()
public:
//掴まれたとき
UFUNCTION(BlueprintNativeEvent, Category = "Grabbable")
void OnGrabbed();
//掴み状態が解除されたとき
UFUNCTION(BlueprintNativeEvent, Category = "Grabbable")
void OnGrabReleased();
};
BP側の実装
そして掴む側(今回はプレイヤー側)の処理はこんな感じ
MyPlayerクラス(C++)
//※他のパラメータは割愛
//現在ヒットしているIGrabbable
TScriptInterface<class IGrabbable> HitGrabbable;
//実際に掴んでいるかのフラグ
bool IsGrabbed = false;
//アクタがヒットしたときの処理
void AMyPlayer::OnHitActor(AActor* HitActor)
{
//既に掴み状態の場合は無視
if (IsGrabbed)
{
return;
}
//ヒットしたアクタがIGrabbableを実装していたらHitGrabbableにセット
if (HitActor->Implements<UGrabbable>())
{
HitGrabbable.SetObject(HitActor);
HitGrabbable.SetInterface(Cast<IGrabbable>(HitActor));
}
}
//掴む処理
void AMyPlayer::Grab()
{
//ヒットしてるIGrabbableがなければreturn
TObjectPtr<UObject> GrabbableObject = HitGrabbable.GetObjectRef();
if (!IsValid(GrabbableObject))
{
return;
}
//HitGrabbableに掴んだことを通知
HitGrabbable->OnGrabbed();
//プロトタイプなのでとりあえず掴んだアクタを親子関係に
if (auto Actor = Cast<AActor>(GrabbableObject))
{
Actor->AttachToActor(this, FAttachmentTransformRules::KeepWorldTransform);
}
IsGrabbed = true;
}
//掴んだアクタを離す処理
void AMyPlayer::GrabRelease()
{
//ヒットしてるIGrabbableがなければreturn
TObjectPtr<UObject> GrabbableObject = HitGrabbable.GetObjectRef();
if (!IsValid(GrabbableObject))
{
return;
}
//HitGrabbableに離したことを通知
HitGrabbable->OnGrabReleased();
//親子関係を元に戻す
if (auto Actor = Cast<AActor>(GrabbableObject))
{
Actor->DetachFromActor(FDetachmentTransformRules::KeepWorldTransform);
}
HitGrabbable = nullptr;
IsGrabbed = false;
}
BP側(MyPlayerを継承したもの)の実装
※OnHitActorはC++でコールバックをセットしようと最初考えましたが、今後アクタの階層やコライダーの構成とかが変わるかもしれなかったのでBP側で好きに設定できるようにしました
実際に実行してみる
コンパイルエラーもないしコード的にはこれで問題なさそうなのでいざ実行してみたのですが、実行してみるとDo not directly call Event functions in Interfaces. Call Execute_OnGrabbed instead.
というエラーが出てクラッシュするという結果に...
解決方法
とりあえずChatGPT先生に聞いてみるとこのような解答が
UEのインターフェース (UInterface) の関数は通常、以下の2種類に分かれます:
・純粋C++関数(BlueprintImplementableではない)
→ Interface->FuncName() で呼べる・BlueprintImplementableEventまたはBlueprintNativeEventの関数(BPで実装される)
→ 直接呼び出してはダメ。代わりに Execute_関数名() を使う必要がある
代わりにExecute_関数名()
を使えとのお達しなのでインタフェースの関数を呼び出してる箇所を修正することに
//掴む処理
void AMyPlayer::Grab()
{
//ヒットしてるIGrabbableがなければreturn
TObjectPtr<UObject> GrabbableObject = HitGrabbable.GetObjectRef();
if (!IsValid(GrabbableObject))
{
return;
}
//HitGrabbableに掴んだことを通知
- HitGrabbable->OnGrabbed();
+ IGrabbable::Execute_OnGrabbed(GrabbableObject);
//プロトタイプなのでとりあえず掴んだアクタを親子関係に
if (auto Actor = Cast<AActor>(GrabbableObject))
{
Actor->AttachToActor(this, FAttachmentTransformRules::KeepWorldTransform);
}
IsGrabbed = true;
}
//掴んだアクタを離す処理
void AMyPlayer::GrabRelease()
{
//ヒットしてるIGrabbableがなければreturn
TObjectPtr<UObject> GrabbableObject = HitGrabbable.GetObjectRef();
if (!IsValid(GrabbableObject))
{
return;
}
//HitGrabbableに離したことを通知
- HitGrabbable->OnGrabReleased();
+ IGrabbable::Execute_OnGrabReleased(GrabbableObject);
//親子関係を元に戻す
if (auto Actor = Cast<AActor>(GrabbableObject))
{
Actor->DetachFromActor(FDetachmentTransformRules::KeepWorldTransform);
}
HitGrabbable = nullptr;
IsGrabbed = false;
}
↑のように修正して実行してみたとことろ、クラッシュせず処理も正しく実行されました。
めでたしめでたし
なぜクラッシュするのか?
ChatGPT先生曰く
UEのBlueprintNativeEventは、実行時にBPでのオーバーライドを探す仕組みになっており、C++側のインターフェースポインタからは関数実体が存在しない可能性があるため、直接呼び出すとnullptrアクセスでクラッシュします。
とのこと。
ここからは完全に自分の予想なのですが、C++にはC#のようなインタフェース機能がないため、内部的には多重継承機能を使って擬似的にインタフェースを実装しているものだと思われます。
なのでC#と違いUnrealC++のインタフェースは通常のクラス継承と同じで、オーバーライドした関数であっても基底側に何かしらの実装処理があるという前提で作られているでは?というのが自分の予想です。
(まぁChatGPTなのでそもそもこの解説が間違っている可能性はありますが...)