以下は Unreal Engine 4.26.1 + Visual Studio 2017/2019 で確認。UnrealC++ で何かする前に読んでおくと、いろいろ思い出して幸せになれる、かなあ。
Unreal C++ 特有の制限とか罠とか
-
自作した Blueprint クラスを C++ クラスの親にできない(少なくともUEのエディタ上では)
-
C++クラスの親クラスを「なし」にすると、Blueprint 側からは参照できないクラスになる。Blueprint 側から参照したい場合でActorなどの子にしたくないときでも、親のクラスを Object クラス(C++側では UObject クラスに相当)にする必要がある。
-
.h ファイルで
#include "Classname.generated.h"
よりも後ろに他の#include
を書くとエラーになる。 -
public:
などの後に、メソッドの宣言を書いた後に Blueprint 側に公開しない変数の宣言を書くと、エディタが落ちることがある(ぽい)。UPROPERTY
を書いた後に変数を書くのは(つまり公開する設定にすれば)大丈夫らしい。また、変数を先に書いて、関数を後に書くのは大丈夫(多分)。 -
C++ のコンストラクタで下記のように、関数の本体の中でスーパークラスのコンストラクタを呼ぶコードを書くとエディタが落ちる(4.26.0で確認)。
ClassName::ClassName()
{
Super();
}
下記のようにすれば問題ない。
ClassName::ClassName() : Super()
{
}
コンストラクタではない、メンバ関数内でスーパークラスの関数を呼ぶのは問題ないぽい。
- C++ 側のコードを変更したことが原因で UE エディタが落ちてしまうと、そのままの状態では何度UEエディタを起動しなおしても落ちてしまう(というか起動しない)。その場合は、C++のコードを元に戻して Visual Studio 側でビルドしなおしてやると、エディタが起動してくれる(ことがある)。
関数(メソッド)関連
- UFUNCTION について:基本、下記のいずれかでいいはず:参考:UE4 UFUNCTIONの種類について
UFUNCTION(BlueprintCallable, Category="MyFunc")
int32 BPCallable();
// ブループリント側から呼び出せる関数
UFUNCTION(BlueprintCallableEvent, Category="MyFunc")
void BPCallableEvent();
// ブループリント側からイベントとして呼び出せる
UFUNCTION(BlueprintImplementableEvent, Category="MyFunc")
void BPImplementableEvent();
// C++クラス側の実装はなく、BP側で赤いイベントのノードとして表示される。
UFUNCTION(BlueprintPure, Category="MyFunc")
int32 BPPure(); // 実行ピンを持たないノードとして表示される。
C++ クラスのサブクラスを Blueprint で作って、親のC++クラスに定義されているメソッドを上書きできるようにしたい場合は、BlueprintNativeEvent を使う。このとき、C++ 側にも実装したい場合は _Implementation
という関数に実装する。
UFUNCTION(BlueprintNativeEvent, Category="MyFunc")
int32 BPNativeEvent();
virtual int32 BPNativeEvent_Implementation();
こうしといて、cpp 側のファイルに実装を書く。
int32 SomeClass::BPNativeEvent_Implementation()
{
return 0;
}
これで、子クラス側のブループリントのイベントグラフ内で、上記のようにオーバーライドできるようになる。
プロパティ(インスタンス変数)関連
UE4 よく使うUPROPERTYメモ から一部だけ抜粋。基本 EditAnywhere でよい。
UPROPERTY(EditAnywhere, Category="MyFunc")
int32 EAnywhere;
// 常にUEのエディタ側で値を編集できる
UFUNCTION(EditDefaultsOnly, Category="MyFunc")
int32 EDefaultsOnly;
// クラスのデフォルト値としてのみ編集できる
UFUNCTION(EditInstanceOnly, Category="MyFunc")
int32 EInstanceOnly;
// インスタンスでのみ値を編集できる
UPROPERTY(VisibleAnywhere, Category="MyFunc")
int32 EAnywhere;
// 常にUEのエディタ側で値を見ることだけができる。編集はできない。
UPROPERTY(BlueprintReadWrite, Category="MyFunc")
int32 BPintReadWrite;
// ブループリント側で読み書きできる
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="MyFunc")
int32 EAnywhereBPintReadWrite;
// 常にUEのエディタ側で値を編集でき、
// なおかつブループリント側で読み書きできる。
継承
-
多重継承は許されないぽい(そのほうが絶対いい)。コンポーネントかインターフェースを使う(後述)。
-
ActorClass → C++ClassA → C++ClassB → BlueprintClass のように継承関係を作ったとき、VisualStudio 側でビルドしたのでは何故かUEエディタ側には C++ClassB のコードが反映されないことがある。その場合は、UE4側でコンパイルとビルドの両方する必要があるようだ。
レベルブループリント
レベルブループリントをC++から触るときは、LevelScriptActor を親クラスにしたC++クラスを作成して、レベルブループリントの親クラスをそのC++クラスにする。参考:【UnrealC++】⑩レベルブループリント【UE4】
デフォルト値とコンストラクタ
デフォルト値:C++側でBlueprintにEditAnywhereやEditDefaultsOnlyで公開している変数の値をコンストラクタで設定すると、Blueprint側でデフォルト値として表示される。これ何げに結構すごい気はする。ただし、下記のような罠が少しある。
一度デフォルト値を設定してビルドした後に、C++側でデフォルト値を変更しても、Blueprint 側のクラスのデフォルト値は、前にビルドしたときの値のままになってしまう。ただし、デフォルト値をC++側で変更すると、UEエディタ側の「リセット」のアイコンが有効になる。ここで値のリセットをすると、C++側で更新した値に直る。つまり、Edit可能なデフォルト値を変更したら Blueprint 側でリセットしないと新しい値をデフォルト値が以前に設定したままになる。これ結構罠じゃない?
String と Text
- Blueprint の
String
クラスは C++ ではFString
クラスになる。 Blueprint のText
クラスは C++ ではFText
クラスになる。 String の値はTEXT()
をつけることで UTF8 扱いになる(多分)。下記のような感じ。
FString string = TEXT("日本語もこれでOK");
ブループリントのオブジェクトのC++からの参照
- Blueprint のクラスのインスタンスを引数で受け取るときはポインタで受け取る。たとえば下記のような感じ。
public:
UFUNCTION(BlueprintCallable, Category = "MyFunc")
void SetProperty(AMyActor *actor);
配列
配列は可変配列の TArray
クラスを使う。#include "Containers/Array.h"
が必要。テンプレートで型指定する。参考:TArray:Unreal Engine における配列
- 配列でBluepirntのクラスのインスタンスを格納する場合もポインタを使う。
MyClass::SomeFunction( AMyActor* actor ){
TArray<AMyActor*> ActorList;
ActorList.Add( actor );
}
列挙型
enum は適当にクラスを作って、そこのヘッダに下記のような感じで書いて、それぞれのクラスで include する、でよさそう:参考 UE4 C++とUnreal C++の列挙型の扱い - PaperSloth's diary
UENUM(BlueprintType)
enum class SomeState: uint8
{
NONE UMETA(DisplayName = "None"),
REST UMETA(DisplayName = "Rest"),
TO_OFFICE UMETA(DisplayName = "ToOffice"),
WORKING UMETA(DisplayName = "Working"),
TO_HOME UMETA(DisplayName = "ToHome"),
DEAD UMETA(DisplayName = "Dead"),
};
クラス側のコードは、ふつうに変数をBlueprint側に出すときと同じ。
UPROPERTY(EditAnywhere, Category = "TownEditor")
SomeState State;
これで、上記 enum 型の変数はエディタ側ではこんな風に見えるようになる。
enum は struct とは違って、親クラスがないクラスのヘッダファイルで定義しても、問題なくビルドを通る。
連想配列
連想配列(辞書)は TMap クラスを使う。#include "Containers/Map.h"
が必要。参考:TMap
TMap<int32, FString> map;
map.Add(5, TEXT("Banana"));
FString *str = map.Find(5); // 返り値はポインタになる
構造体
構造体は下記のようなコードを .h ファイに書く。ただし .h ファイルを自前で作ってもダメで、Object
クラス(C++ではUObjectクラス)の子クラスとして作成したクラスのヘッダファイルに書かないと GENERATED_BODY()
でエラーが出てしまう。これわからなかったわー。また、構造体の名前は先頭が F でなければならない。
#pragma once
#include "CoreMinimal.h"
#include "UObject/NoExportTypes.h"
#include "MyClass.generated.h"
USTRUCT(BlueprintType)
struct FHoge {
GENERATED_BODY()
FHoge() { hogehoge = 0; }
~FHoge() {}
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Hoge")
int32 hogehoge;
};
UCLASS()
class TOWNEDITORV4_API UMyClass : public UObject
{
GENERATED_BODY()
};
こんな感じで、UObject のサブクラスとして作成した MyClass のヘッダ内で定義すればビルドは通る。MyClass 自体は空で問題ない。何かこれ別の方法ありそうな気がするんだけど…。generated.h を見て少し触ったくらいでは分からなかい程度には複雑そうなので、暇なときにもう少し解析してみたい。
ちなみに上の構造体をレベルブループリントとかで変数化すると、イベントグラフ内ではこんな感じで見える。
インタフェース関連
インタフェースは、C++クラスの作成で親クラスをアンリアルインタフェースにすることで作成できる。
コードはヘッダに下記のような感じで書く。
#pragma once
#include "CoreMinimal.h"
#include "UObject/Interface.h"
#include "MyInterface.generated.h"
// This class does not need to be modified.
UINTERFACE(MinimalAPI)
class UMyInterface : public UInterface
{
GENERATED_BODY()
};
class TOWNEDITORV4_API IMyInterface
{
GENERATED_BODY()
// Add interface functions to this class. This is the class that will be inherited to implement this interface.
public:
UFUNCTION(BlueprintCallable, BlueprintImplementableEvent, Category = "Hoge")
void HogeImplemental();
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Hoge")
void HogeNative();
public:
virtual void HogeCppOnly() {}
};
UMyInterface 側には何も書いてはいけないらしい。IMyInterface に関数を書いていく。インタフェース(クラス)は必ず I が先頭につく。UFUNCTION は上記のいずれかでないとダメらしい。UFUNCTION をつけた関数は実装がなくてもエラーにならない。C++でしか使わない関数は virtual をつけてUFUNCTIONはつけない。デフォルトの実装を何か書いておかないとビルドエラーになる。実装を書く場所は .h でも .cpp でもいい。
ブループリントクラスの「クラス設定」でこのようにインターフェースに選べるようになる。参考:UFUNCTION
イベントグラフ上では、いずれも同じイベントとして見える。
イベントディスパッチャ関連
コンポーネントの作成
コンポーネントは、C++クラスとして作成する。コンポーネントを作成するときは、必ずいずれかの既存のコンポーネントクラスのサブクラスとして作成する必要がある(ぽい)。コンポーネントのクラスごとに、付けられる親のオブジェクトが限定されているようなので、どのクラスにつけるコンポーネントにするかによって、親クラスを選ぶ感じになる(ぽい)。
Actor クラス用のコンポーネントと、Scene クラス用のコンポーネントは、C++クラス作成時の「親クラスを選択」の選択肢にある。それ以外の親クラスを選びたいときは、右上の「すべてのクラスを表示」として Component とか入力すると検索できる。
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "MyActorComponent.generated.h"
UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class TOWNSIMV3MPP_API UMyActorComponent : public UActorComponent
{
GENERATED_BODY()
public:
// Sets default values for this component's properties
UMyActorComponent();
protected:
// Called when the game starts
virtual void BeginPlay() override;
public:
// Called every frame
virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
};
コンポーネントクラスのヘッダはこんな感じになる。Tick の引数が Actor クラスのサブクラスを作った時と微妙に違う感じになる。Tick に関しては右を参照のこと:アクタのティック
ガーベージコレクション
ガーベージコレクション:まあまあ複雑なので、詳細は右リンクを参照のこと:アンリアルでのオブジェクト処理
基本、C++のコード内で自前で(newで)作成したオブジェクトは、ガベージコレクションの対象にならない。後述する SpawnActor とか NewObject とかで生成したオブジェクトは、ガーベージコレクションの対象になる。
ただし、UPROPERTY を付けた変数で参照しているオブジェクトも、ガベージコレクションの対象にならない。なので、自分で責任をもって delete
するとか Destroy()
する必要がある(多分)。
ガベージコレクションの対象にしたければ、UPROPERTY を付けない変数で参照しておく。ただし、Actor オブジェクトは、UPROPERTY が付いていない変数で参照している場合でもガベージコレクションの対象にはならない。Actor オブジェクトを破壊するには、Destroy()
を明示的に呼び出す必要がある(ということらしい)。なかなかややこしい。
4.26 でもできるかは確認してないけど、UPROPERTY を付けなくてもガベージコレクションの対象から外すことはできるらしい:UPROPERTY を使わずにオブジェクトをガーベジ コレクションの対象から外す方法
アクターのスポーン
new
ではダメで、SpawnActor<>()
を使って下記のようにする。(参考:[UE4] C++で動的にアクターを生成(スポーン)する方法で一番実用的だった方法)
FString path = "/Game/SomeActor.SomeActor_C";
TSubclassOf<class AActor> ClassObject = TSoftClassPtr<AActor>(FSoftObjectPath(*PathName)).LoadSynchronous();
if (ClassObject != nullptr)
{
ASomeActor* actor = (ASomeActor*)GetWorld()->SpawnActor<AActor>(ClassObject);
}
アクターでない UObject の生成
new
ではなく NewObject()
を使う。outer
には、特に理由がない限りは GetWorld()
の返り値を与えておくと良いぽい。参考:Unreal Engine C++ 逆引きメモ
SomeClass *obj = (SomeClass*)NewObject<SomeClass>(GetWorld());
ただし、コンストラクタ内では NewObject は使えない(何でやねん)。代わりに FObjectInitializer::CreateDefaultSubobject
を使う。詳細は上記リンク先を参照のこと。
C++ と Blueprint の使い分け
- C++/Blueprint の選択について:参考 [UE4] C++ or Blueprint?
表示回りやインタフェース、モデル関連は Blueprint で書いて、C++ではコントローラやロジック部分を書くのが効率よい感じはしてます。