Unreal Engine 5
講座など
C++
導入する
Epicアカウントを作るが、仕事用なのでskoniの方のメールアドレスで登録しよう。
Epic Games のLauncherからインストールができる。
ランチャーからアセットやサンプルプロジェクトも落とせる
今回は5.0.3を入れる(5.0.0で入れたら、5.0.3で入った)
.upproject
UEで作成したプロジェクト情報を管理するjsonのテキストファイル
ダブルクリックなどをすることで対応するUEを開くことができるが、開くファイルと開くURのverがかみ合わないと、Migrateやアセット読み込みがうまくいかず、透明なものとして扱われる
なので、5.5で開くファイルは5.5で開いたりと、工夫をしよう
Git管理手順
さっさとやるならVisual StudioでLocal Onlyで管理するのが手っ取り早い。
途中から導入すると差分が.gitignoreを追加してもかなり多い
code:.gitigonore
# Visual Studio関連の不要ファイル
.vs/
*.sln
*.suo
*.VC.db
*.opendb
*.sdf
# UE5の自動生成・一時ファイル
Binaries/
DerivedDataCache/
Intermediate/
Saved/
# ビルドログなど
*.log
https://scrapbox.io/files/69bd1e32b9e150750aec7fa6.png
GitHubで管理する場合、UEのプロジェクトファイルは非常に重いので、100MB制限に引っかかるケースがある。
※Git LFSを使えば解決する。
class関連のdocs
ビルドとエラーログ
C++構文エラーの場合は以下で検索かけたりして程よく読み飛ばすといい。
$ error C
$ error LNK
code:buildlog
3>C:\Users\watas\Ue5Project\Slash 5.5\Source\Slash\Private\Items\Weapons\Weapon.cpp(15): error C2143: syntax error: missing ')' before ';'
3>Trace file written to C:/Users/watas/AppData/Local/UnrealBuildTool/Log.uba with size 2.3kb
3>Total time in Unreal Build Accelerator local executor: 1.86 seconds
3>Total execution time: 2.37 seconds
3>C:\Program Files\Microsoft Visual Studio\2022\Community\MSBuild\Microsoft\VC\v170\Microsoft.MakeFile.Targets(44,5): error MSB3073: The command ""C:\Program Files\Epic Games\UE_5.5\Engine\Build\BatchFiles\Build.bat" SlashEditor Win64 Development -Project="C:\Users\watas\Ue5Project\Slash 5.5\Slash.uproject" -WaitMutex -FromMsBuild -architecture=x64" exited with code 6.
error C2143: syntax error: missing ')' before ';'で単純な)忘れであることが書かれている
別端末にプロジェクトを移行する
File > Zip Project でクラウドなどに保存。別端末で解凍して起動。
UPROPERTY
C++ クラスに付与することがあるマクロ。BPクラスと関係する。
code:cpp
private:
UPROPERTY(VisibleAnywhere)
UCapsuleComponent* Capsule;
このマクロを付与することで、UEのリフレクションシステムが認識する変数となる。
また、以下の機能が有効化する。
特定の指定子に応じた設定で、UEエディタ上で値を閲覧、編集することが出来る
C++で用意した変数をブループリントのノードとしてGet / Set で扱えるようになる
GC の管理下に置かれる。UEは使わなくなったオブジェクトのメモリを自動で解放するが、特にUCapsuleComponent*などのポインタ変数にUPROPERTY()を定義しておかなければ、UE側はこの変数を使っているかどうかを認識できない。仮に勝手にメモリから消されてしまった場合、ゲームクラッシュの要因となってしまう。
UEのオブジェクトをポインタとして管理するときは基本的にこのマクロをつけること。
includeについて
例えばUE5.5だと、Pawnクラスを継承すれば、USkeletalMeshComponentはincludeされなくても呼び出せる。
? include しなくても良いのであれば、#include は省略するべきか?
→これはNOである。UE5のバージョンが上がったりして、SkeletalMesh がincludeされていない前提で構成されたりするとバグの原因になる。自分が使いたいファイルは、自分が明示的にincludeで記載するというのが基本的な鉄則(IWYU原則と呼ばれる)
Enhanced Input
IA と IMC
IA(Input Action)
ジャンプ、移動、攻撃などアクションそのものを定義する。IA 自体はどのキーを押されたかという情報を持たなずに、ボタンかスティック(2D Axis)かといった、Value Type だけを持つ。
IMC(Imput Mapping Context)
物理キーと IA との紐づけ(マッピング)を担当する。スペースキーでジャンプ用の IA を発動するなど。入力の反転やデッドゾーンの調整、発動条件(長押しなど)もここで設定する。
IA と IMC が分割していることで起こるメリット
通常時と車に乗るシーンがあるとして、Wを押すと歩行 or アクセル という操作に分かれるゲームがあるとき、Subsystem (後述) で 通常の IMC を外して、 車用の IMC を適用するという作業だけで操作系を切り替えることができる
IMCに Priority という概念がある。通常 IMC で左クリックが攻撃、UI操作用の IMC で左クリックが決定というケースがあるとして、インベントリを開いた時に UI操作用 IMC の優先度を高めておいて重ね掛けするとメニュー画面でクリックしたとき、裏側でキャラクターが剣を振ってしまうというようなバグを防止することが出来る
こちらは IMC の切り替えでも実現できる。ただしメニューを開きながら歩いたりできるようにする際、以下のように重複して持たせなければならなくなる
通常 IMC に Move
UI用の IMC にも Move
通常 IMC にUI用の IMC を重ねて、Priority を高める。そうしたとき、左クリックは UI用 IMC のものが、WASD 操作は(UI用 IMC に用意が無ければ)通常 IMC のものが動作するという設計にすることが出来る。
IA を再利用できる。例えば基本移動操作はそのまま、別の操作を用意して切り替えられる。キーコンフィグを作成するときも IMC のキー情報を書き換えるだけでよいので、スマートな設計になる
UEnhancedInputLocalPlayerSubsystemについて
そもそも、Subsystemとは
UE においては特定のライフサイクルに合わせて生成・管理されるシステム。
UEnhancedInputLocalPlayerSubsystem は、現実のプレイヤー(LocalPlayer) の入力を管理する役割を持つシステム。具体的には現在のプレイヤーに対して、今はどの IMC を有効にするかを管理している。
このシステムはゲーム内キャラクター(Pawnなど)に紐づくわけではなく、あくまで現実プレイヤー(LocalPlayer)に紐づく。これはゲーム内キャラクターが死んでしまってもメニュー操作やリスポーンを選択する必要があるから。
Axis Mapping, Action Mapping と比べてどういいのか
メニューを開いているとき、キャラ操作時などでフラグ管理をする必要があったが、Input Mapping Contextで場面に応じた操作体系にすることができる
個別アセットによる管理が可能となり、競合が置きづらくなる
長押し、ダブルタップ、スティックの遊び設定などが楽になる
使う
IAとIMCを用意。それぞれボタン割当。
Projectname.Build.csで以下のように追加。
code:cpp
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "EnhancedInput" });
使用するc++クラスで、IAとIMCをBPで割り当てられる変数を用意すればよい。
実装の解説
IMCとIAをBlueprint側で割り当てるためにC++で定義。移動用の関数を用意(今回Move()とする)
code:.h
protected:
// ========= 入力関連 ===========
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Input")
UInputMappingContext* DefaultMappingContext;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Input")
UInputAction* MoveAction;
void Move(const FInputActionValue& Value);
Move(const FInputActionValue& Value)
参照として渡されている
参照の場合は前方宣言できないのでincludeで読み込んでおくこと。
ポインタでなく参照としている理由はUE側が設定した関数で参照を渡すようにしているから、というのもあるが、移動中の入力値が存在しないことはあり得ない(無入力なら無入力とか、そういった値が入る)。なので、ポインタは nullptr であることがあり得るがこの値に関しては、値が入っていることが確約されている参照で渡される。FInputActionValue Valueと値で渡すパターンに関しては、入力チェックをするたびに構造体データをまるごとコピーする処理が起きてパフォーマンスに影響が出るから、参照渡しでメモリ位置だけ渡して高速に動かすという作り。
protectedに配置する理由
このキャラをさらに継承するクラスがあったとする。protected なら、その子クラスからマッピングテキストなどを調整できるようになる。
privateとする場合は、BPエディタからも弄れなくなるので、AllowPrivateAccess = "true"等が必要
.cpp側で登録する:BeginPlay
code:cpp
# BeginPlay()
if (APlayerController* PlayerController = Cast<APlayerController>(GetController()))
{
if (UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PlayerController->GetLocalPlayer()))
{
if (DefaultMappingContext)
{
Subsystem->AddMappingContext(DefaultMappingContext, 0);
}
}
}
Pawnを操っているControllerをGetController()で取得。この値は敵が動かすパターンもある。今回はPlayerControllerという、自身が操るControllerとなるようキャストできたらifの入れ子に。
if (UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PlayerController->GetLocalPlayer()))
PlayerController->GetLocalPlayer()
ゲームでプレイヤーが複数いるときなど、自分自身が操作する対象を取得する
Subsystem
UE側のシステム。LocalPlayerの入力管理をするシステム、UEnhancedInputLocalPlayerSubsystemとして格納
Subsystem->AddMappingContext(DefaultMappingContext, 0);
先ほど作ったIMCを参照するようにしている。
0は優先度。メニュー時の操作などを優先度1で作った場合は、そちらの操作が優先されて、入力を入れてもプレイヤーが動かなくなったりするような処理を実現できる。
.cpp側で登録する: SetupPlayerInputComponent
code:cpp
if (UEnhancedInputComponent* EnhancedInputComponent = Cast<UEnhancedInputComponent>(PlayerInputComponent))
{
if (MoveAction)
{
EnhancedInputComponent->BindAction(MoveAction, ETriggerEvent::Triggered, this, &ABird::Move);
}
}
先ほど作ったIA(MoveAction)と。実行するC++関数のポインタ紐づけ。
UEnhancedInputComponent* EnhancedInputComponent = Cast<UEnhancedInputComponent>(PlayerInputComponent
いろんな親のInputクラスがあり、今回はEnhanced Input を使うので、それ専用の派生クラスとして扱うためにキャスト
BindAction
MoveActionというInputActionを対象として
ETriggerEvent::Triggeredで、入力を満たしている間、毎フレーム(Tick)イベントを発火させる
this: で、対象をこの Pawn 自身とする
&Bird::Move: 発火したときに何の関数を走らせるか。
IA で 作る Move の例について
https://scrapbox.io/files/6a05c2265c3c134ed437a9b1.png
WASD は、x軸に1.0という値を出力する。
Wの場合はYで受け取りたいので、Swizzle Input Axis Volumes を付与。
A は 逆に移動したいので、Swizzle と Negate を使って反転させる(Y = 1.0)
S は 左方向なので、Nagate を使って反転。
D は 右方向なので、そのままの値を使えばよい。
命名規則
たとえば、bSweepというような引数がSpringArm->AddLocalRotation()に渡されることがある。これは、bがboolであること、Sweepが機能名(移動中に他の物体にぶつかるのかという設定)であることを示すように、UEは命名規則が厳格に設定されているため、慣れればどんな値が格納されるべきなのかを判断できるようになっている。
Rotator の軸
code:cpp
FRotator(Pitch, Yaw, Roll)
Pitch
y軸。仰俯角(水平を基準とした上下方向)。機首を上下に振る、お辞儀のような動きがイメージ。
FPSで上下を向く動き。
Yaw
z軸。旋回。水平方向を左右に向く、首を振る動き。
FPSで左右を向く動きをする場合、この軸を使う。
Roll
x軸。横転。飛行機が翼を左右に傾けるイメージ。
地面を転がるなど。
UE5と他のツールとの上方向の差異
UE5, Blender: z軸が上。
Unity, Maya: y軸が上。
Collision 関連
https://scrapbox.io/files/69e4d9a73caed06dfe0049ad.png
例えば Query Only だと、目に見えないコリジョンに衝突して進めなくなる、というような挙動を防ぎつつ、検知自体は行うという処理が実現できる
Collsion の考え方
ue5_minigameでは、例えば Bird と Coin が反応しなかった。Coin は pawn の Overlap イベントを持っていたが、Bird の ObjectType設定が WorldDynamic だったので動作しなかった。Bird を Pawn にすると動作した。 それぞれに好ましい ObjectType 設定を考える。Character と Bird は Pawnでよく、コインなどのitemはWorldDynamicとすべき。さらにitemは、メッシュ側はNoCollisionとして、検知用のcomponent側だけで設定しておくと良い。
もっと知る
Brd だけでも、Root, Capsule, Skeletal, InteractArea と 4種類あってかなり大変なので紐解く
Root, Capsuleは今回Capsuleを親として考える設計としているので、まず同じ設定になる
Root, Capsule
壁や床と物理的にぶつかる、ぶつからないという設定をする。基本的にはCollision Enabled Query and Physics, 物理干渉したい相手にBlockの設定をする。
SkeletalMesh
見た目。特別な理由がない限りNoCollision とすればいい。ヘッドショットとかあったら調整する必要があるが。
InteractArea
感知判定。Query Only(No Physics Collision)として、感知したい相手を Overlap, それ以外は Ignore とすれば良い。自身のObjectType はWorldDynamicにしておくとよい。大規模開発では、専用のObjectTypeを用意して配置しておくことが多い。
カメラが気になるなら、Camera を ignore にしておこう
実際のゲーム側で反映されないときは、レベル上からその Actor に対して何かしら設定をダッシュボード側で弄ってしまったケースが多い。配置件数が少ないのであれば、再配置してみるとよい。
UMG(Unreal Motion Graphics)
UE5 における インゲーム HUD, UI 等を調整するときに使う。
private, protected の配置判断
UE における C++ では、Component などは private, 関数はprotected かつ virtualとするのが標準的。
Component 周りを public / protected としてしまうと、他のクラスがそのコンポーネントを取り除いたり、値を弄ることができるようになってしまう。これらのパーツは編集するのであれば、Blueprint 経由だけで行うのが良い。
関数周りを protected に配置する理由は、拡張性を持たせるため。例えばBirdクラスに、近づくとインタラクトキーが出る処理があったとする。この時virtual void OnInteractAreaBeginOverlap(...)としておけば、近づくときに鳴き声を上げる鳥を作りたいとなった場合も、子クラス側で呼べるようになる。
⭐絶対に拡張の予定が無い関数を定義するなら?
→厳格なセオリーに従って、privateで定義して良い。ただしゲーム開発では仕様変更が重なるケースが多いので、private で一定の柔軟さを持たせておくことも頻繁に起こる。
慣れてない場合はprivateで定義する癖をつけておくと、private->protectedにするという判断が養えるのでお勧め。
また上記の例なら、virtualは外しておく(子クラスで自由に上書きしていいことを示す表記のため、private に配置するなら必要ない)。ただ外さなくてもエラーにはならない。意図的にこう書くパターンもあるらしいが、一旦正しく定義することを心がける
UE におけるデリゲート
例えば C#(Unity) では、ボスやキャラにOnDied()というようなeventを用意して、それを状態管理マネージャー側が購読することでボス撃破通知を受け取る挙動を実現していた。UE ではどういった設計となるか。
まず、FOnBossDiedDelegate OnDied;のような形でイベントを定義。
定義したイベントにBoss->OnDied.AddDynamic(this, &AScoreManager::AddScore);というような形で購読。
イベント発火時は、OnDied.Broadcast();というような記載で行う(C# の OnDied?.Invoke())
UE の持つデリゲートの種類
シングルデリゲート
購読できる関数は1つだけ。別の関数を登録すると上書きされる
マルチキャストデリゲート
複数の関数を購読できる。Unity の標準的な event の挙動と同じ。
ダイナミックデリゲート
購読できる関数が1つだけ。Blueprintと連携ができる。
ダイナミックマルチキャストデリゲート
複数購読でき、Blueprint とも連携ができる。
OnComponentBeginOverlap()などの判定感知処理はこれが使われている(BP側でノードを繋いでも動作させられ、C++側でもAddDynamic()で購読ができている)
マクロとリフレクションシステム
デリゲートにはAddDynamic()やUFUNCTION()などのマクロが用いられるが、UE ではリフレクションシステムという仕組みが採用されている。
リフレクション
プログラム実行中に、自身のクラス名や所持している関数名を文字列として検索・実行できる機能。Blueprint を動かすために用いられるシステム
UFUCTION()
関数の上部に付与しておくことで、UE 側がBPから呼ばれたりイベントに登録される前提の関数という認識となる。コンパイラがエンジン側のリフレクションシステムに、関数名と引数情報を登録してくれる。
AddDynamic()
ダイナミックデリゲートに購読するためのマクロ。ポインタだけでなく関数名自体の文字列も使って登録が行われている。そのため UFUNCTION() がなければ関数名を検索できず、イベントが発火しなくなる挙動となる
例1: 実際のボス撃破イベントの作成から購読までのイメージ
code:Boss.cpp
# h
// ダイナミックマルチキャストデリゲートの型定義 引数無し
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnBossDiedSignature);
UCLASS()
class ABoss : public ACharacter
{
public:
// イベント定義
// Blueprintからもイベントを引けるように BlueprintAssignable をつけるのが一般的
UPROPERTY(BlueprintAssignable, Category = "Events")
FOnBossDiedSignature OnBossDied;
void Die();
# cpp
void ABoss::Die()
{
OnBossDied.Broadcast(); // 発火
Destroy();
}
# h (ボス発火イベントを受け取る側)
protected:
UFUNCTION()
void AddScore();
# cpp (ボス発火イベントを受け取る側)
void AScoreManager::BeginPlay()
{
Super::BeginPlay();
if (BossActor)
{
// イベント購読
BossActor->OnBossDied.AddDynamic(this, &AScoreManager::AddScore);
}
}
void AScoreManager::AddScore() {...
例2: OnComponentBeginOverlapの例
こちらに関しては、UE 側がすでにある程度の段階までソースコードで実装してくれている。
型宣言、イベント定義、Broadcastによる発火まで。
なので、こちらでやることは購読することだけ。
code:cpp
# h
private:
UFUNCTION()
void OnInteractAreaBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);
UFUNCTION()
void OnInteractAreaEndOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex);
# cpp
void ABird::BeginPlay()
{
Super::BeginPlay();
InteractArea->OnComponentBeginOverlap.AddDynamic(this, &ABird::OnInteractAreaBeginOverlap);
InteractArea->OnComponentEndOverlap.AddDynamic(this, &ABird::OnInteractAreaEndOverlap);
}
用意されているOnComponentBeginOverlapが発火したときに、こちらで用意したOnInteractAreaBeginOverlapの処理も行ってくれという書き方。イベントに購読させるときは引数が定義されたものと同じでなくてはならないので、OnComponentBeginOverlapと同じ引数を(使うにしろ使わないにしろ)OnInteractAreaBeginOverlap側で同じように用意している。
イベント購読時のAddDynamic()について
コンストラクタかBeginPlay()で紐づけることになるが、BeginPlay()側で紐づけてしまうのがデフォルト(UE のサンプルプロジェクトでもこうなっている)。リフレクションシステムの都合上、コンストラクタ側に書くと古い関数名がキャッシュとして残り、イベント名を編集したときなどに不具合が起こるケースがある。
デリゲートの宣言とマクロの仕組みについてもっと詳しく
デリゲートシステム
DECLARE_...マクロで、デリゲートの型を定義する
マクロで定義したデリゲート型の変数を宣言する。
Broadcast()で、バインドされた関数を実行。
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnCoinCountChanged, int32, NewCoinCount);を使って、FOnCoinCountChangedというデリゲート型を宣言。
FOnCoinCountChanged OnCoinCountChanged;として変数として定義。
AddCoin()という関数で、Broadcastを呼び出す。
エンジンのソースコード( PrimitiveComponent.h)で、既に DDECLARE_DYNAMIC_MULTICAST_SPARSE_DELEGATE_SixParams( FComponentBeginOverlapSignature, UPrimitiveComponent, OnComponentBeginOverlap, UPrimitiveComponent*, OverlappedComponent, AActor*, OtherActor, UPrimitiveComponent*, OtherComp, int32, OtherBodyIndex, bool, bFromSweep, const FHitResult &, SweepResult); を用いて型を宣言してくれている。
同クラス内部で、FComponentBeginOverlapSignature OnComponentBeginOverlap;というデリゲート型で宣言済み。
cpp側で書いた、InteractArea->OnComponentBeginOverlap.AddDynamic(this, &ABlankCharacter::OnInteractAreaBeginOverlap);で、自分で定義したOnInteractAreaBeginOverlap()を登録して相乗りさせてもらっている
C++ の設定値と BP 側の矢印
code:cpp
InteractArea->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
InteractArea->SetCollisionResponseToAllChannels(ECR_Ignore);
InteractArea->SetCollisionResponseToChannel(ECC_Pawn, ECR_Overlap);
https://scrapbox.io/files/69e827adfc4464f697bec193.png
上記が像は、C++側の記載が反映された状態で問題ない。ただし、BP 側で矢印が出ている。Scale や Rotation などはC++で決めた設定値をBP側で弄った場合に出る表示だが、今回はC++のみでしか弄っていないはず。なぜ矢印が出ているかというと、Collision などのプリセットファイルはC++ 側で弄った場合でも矢印が出るという設計になっているので挙動が異なる。
C++クラスのPrivate, Publicフォルダの配置分け
基本的に.hや.cppを分割して格納しているが、Private だけに入れるケースもある。
その時のモジュール(Blankというプロジェクトでしか使わないのなら、Blank)でしか使わないのであれば Private だけに入れて管理してもよい。ue5_minigameのケースなどは実際にどのファイルも Private に入れて管理してしまっても動作するが、UE のコードベースが分割して管理されているのでそれに習って分割管理するのがお作法。 UFUNCTIONなどが付与されていない関数について
code:ABlankPlayerController.h
public:
ABlankPlayerController();
void PossessToNewPawn(APawn* TargetPawn); // こういうの
UFUNCTION()は、C++の関数をUEのリフレクションに登録するためのもの。BPから呼び出す場合やイベントに動的バインドする場合に必須になる。今回、C++の別クラスからC++のpublicクラスを呼ぶだけであれば、このマクロを付与しなくてもよい(Characterから、ABlankPlayerController::PossessToNewPawn()とするケースなど)。
オーバーロード演算子
code:cpp
UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (AllowPrivateAccess = "true"))
FText MessageText;
void ASignboard::Interact(APawn* Interactor)
{
UE_LOG(LogTemp, Warning, TEXT("看板を読みました!テキスト: %s"), *MessageText.ToString());
*MessageText.ToString()
%sなど文字列フォーマット関数は、printfをベースとした仕組みで動いている。%sはC言語の生文字列であるTCHAR*(文字配列のポインタ)を受け取る想定で動作する。ただしMessageText.ToString()でFStringというUE独自クラスを渡すと不具合が生じるので、FStringの手前に単項演算子operator*を独自定義(オーバーロード)してくれている。*FStringとすることで、内部の文字列ポインタを参照できるような設計としてくれている
コンパイルエラー
code:cpp
if (ACharacter* Character = Cast<ACharacter>(GetPawn()))
{
Character->GetCharacterMovement()->StopMovementImmediately();
}
code:txt
5/8 Compile x64 BlankPlayerController.cpp C:\Users\watas\Ue5Project\ue5_minigame\Source\Blank\Private\Controller\BlankPlayerController.cpp(51): error C4458: declaration of 'Character' hides class member
C:\Program Files\Epic Games\UE_5.5\Engine\Source\Runtime\Engine\Classes\GameFramework\Controller.h(82): note: see declaration of 'AController::Character'
今回の場合、BlankPlayerControllerでCharacterという変数を使おうとしたが、親クラスのAControllerで既にCharacterという変数名が使われていた。なので、そちらを避けてくれという表記
関数、変数のマクロまとめ
そもそもUFUNCTION()やUPROPERTY()などのマクロは、Unreal Header Tool (UHT)に、このコードをUEのリフレクションシステムに登録してほしいというマーカーとして使われる。リフレクションシステムに登録することで以下の恩恵が得られる。
エディタのDetailsパネルで値取得
Blueprintから変数を取得、設定したり関数を呼び出すことが出来るようになる
UEのメモリ管理機能(GC)がポインタを追跡し、メモリリーク等を防ぐ
セーブデータとして保存し、ネットワーク経由で同期できるようになる
つけなくても良い場合もある
上記の恩恵がいらない場合。C++内部の計算だけで使うint32で定義したカウンターや、BPに公開する必要のない関数などは不要。
UPROPERTY()のオプション
表示系
VisibleAnywhere
Detailsパネルで値を見れるが、編集することが出来ない。
コンポーネントのポインタなど、ポインタそのものを別のものに差し替えることを防ぎつつ、コンポーネント内部のプロパティにはアクセスさせたい場合。
EditAnywhere
Detailsパネルで値を編集できる。
汎用的な設定値など、配置したActorごとに個別変更したいパラメータに使う。
EditDefaultOnly
BPのデフォルト設定画面(Class Defaults)でのみ編集できる。レベルに配置された個別Actorでは編集できない。
最大HPや基本移動速度など、マスタデータ的なものに付与する
BPアクセス系
BlueprintReadOnly
BPからGetできるが、Setはできない
BloeprintReadWrite
BPからGet / Set ができる
meta系
AllowPrivateAccess
C++のベストプラクティスとして、メンバ変数はprivate / protectedとして外部から書き換えられないようにカプセル化する。アクセス自体はpublicなゲッタ/セッタ関数を用意して経由指せるという仕組みが好ましい。しかしながら、こういったprivateとして持たせた関数でもUEのDetailsパネルから特別にアクセスを許可したいケースがある。そういったときにこちらでtrueとすることで、それを許す挙動となる。
サンプル
code:cpp
# 体力などをBP側で調整したい場合など
private:
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Character|Stats", meta = (AllowPrivateAccess = "true"))
float MaxHealth;
code:cpp
# コンポーネント
# ポインタの差替えを防ぎつつ, BP上で呼び出せるようにしている
private:
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components", meta = (AllowPrivateAccess = "true"))
TObjectPtr<class USpringArmComponent> CameraBoom;
実際に遭遇した例
Geometry Collection の 割当を変えようと思ったが、Details に何も出ない
code:BreakableActor.cpp
private:
UPROPERTY(BlueprintReadOnly, meta = (AllowPrivateAccess = "true"))
TObjectPtr<UGeometryCollectionComponent> GeometryCollection;
https://scrapbox.io/files/6a14dee265e24daf8cb7740b.png
この場合、UPROPERTY(VisibleAnywhere, BlueprintReadOnly, meta = (AllowPrivateAccess = "true"))とすることで閲覧ができる。
https://scrapbox.io/files/6a14df4265e24daf8cb77490.png
UFUNCTION()のオプション
BP系
BlueprintCallable
BPから実行ピンをつないで呼び出せる関数にする。
BlueprintPure
実行ピンがなく、値を返すだけのノード(緑)になる。メンバ変数の値を変更しない計算、ゲッタ関数はこれを使うとよい。
アイテムをふわふわ浮遊させるだけの関数などはこれでよい
BlueprintImplementableEvent
C++のヘッダで宣言だけ行い、実装を完全にBP側に任せるというマクロ。C++からこの関数を呼ぶと、BP上のEventGraph上にある同名イベントが発火する。UI表示演出などで使う。
code:BlankGameMode.h
# 実装イメージ
UFUNCTION(BlueprintImplementableEvent, Category = "Rules")
void OnGameCleared();
https://scrapbox.io/files/69f8541d8dc79f02a28af85b.png
BlueprintNativeEvent
C++でデフォルトの実装を持ちつつ、必要に応じてBP側でオーバーライドできるようになる。C++のデフォルト実装は関数名 + _Implementationという記述をつけたものとなる。
とりあえず、体系的に守っておいたほうがいいルール
Delegateにバインドする関数には、必ずUFUNCTION()を付与する
OnInteractAreaBeginOverlapなど。リフレクションシステムは関数の名前で結びつけを行うため、こちらが無いとコンパイルが通っても実行時にクラッシュする原因となりうる。
TObjectPtr
UE5から追加された。クラスのメンバ変数はゲーム実行中存在し続ける仕組みだが、UE5では、今は使ってないアセットをメモリからおろしておいて、必要になったときにロードするという遅延ロードが導入された。TObjectPtrを記載すると、遅延ロードやアクセス追跡を自動で行ってくれるようになる。
code:cpp
private:
UPROPERTY(VisibleAnywhere)
// USpringArmComponent* SpringArm;
TObjectPtr<USpringArmComponent> SpringArm; // こう書く
UPROPERTY()を付与する必要があるような要素にはこちらで定義しておくと良い。
逆に、.cpp側で一時的に使う変数等に対しては定義しないほうがよい(変数が作られるたびに、UEの遅延ロードに関する処理が走る)。素直に生のポインタを使う。
code:cpp
void ALinearPlayerCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
Super::SetupPlayerInputComponent(PlayerInputComponent);
// 使わない例
if (APlayerController* PC = Cast<APlayerController>(GetController()))
// ...
UE5 アクセス修飾子の例
ケース1: Overlap イベントの override
code:ItemBase.cpp
protected:
// UFINCTION() は UE のシステムがこの関数にアクセスしてチェックするので、
// protected 以上の関数で基本的に設定する
// またオーバーライド想定の関数は、同じように protected 以上で定義しておく
UFUNCTION()
virtual void OnItemBeginOverlap(
UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
UPrimitiveComponent* OtherComp, int32 OtherBodyIndex,
bool bFromSweep, const FHitResult& SweepResult
);
code:Weapon.cpp
protected:
// 親側で UFUNCTION() を付与していたら、書かなくてよい
// アクセス権は親が protected なので、そちらに合わせる
virtual void OnItemBeginOverlap(
UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
UPrimitiveComponent* OtherComp, int32 OtherBodyIndex,
bool bFromSweep, const FHitResult& SweepResult
) override;
ケース2: 親で定義した ItemMesh にアクセスするとき
BP から ItemMesh を Get して処理するケース
code:ItemBase.cpp
private:
// 派生クラスから nullptr 等に設定されてしまうことを防ぐために private とする
// BP で触れるようにしつつ, private に配置するために AllowPrivateAccess を設定
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, meta = (AllowPrivateAccess = "true"))
TObjectPtr<UStaticMeshComponent> ItemMesh;
BlueprintReadOnly, meta = (AllowPrivateAccess = "true")としておくと、上記コメントの懸念を解決できる。ただし、継承先で以下のようなコードは書けない
code:Weapon.cpp
void AWeapon::OnItemBeginOverlap(...)
{
ALinearPlayerCharacter* LinearPlayerCharacter = Cast<ALinearPlayerCharacter>(OtherActor);
if (LinearPlayerCharacter != nullptr)
{
// ItemMesh->... というような記載は、private なのでできない
}
C++ Class から調整するケースは、protected に配置して AllowPrivateAccess も除去しておく
code:ItemBase.cpp
protected:
UPROPERTY(VisibleAnywhere, BlueprintReadOnly)
TObjectPtr<UStaticMeshComponent> ItemMesh;
Enum
C++の場合
code:cpp
enum CharacterState
{
Unequipped,
EquippedOneHandedWeapon,
EquippedTwoHandedWeapon,
};
ECharacterState State = Unequipped;
基本はこんな感じ。ただし、ECharacterState::Unequippedというようにプレフィックスを必須としたい場合は、enum class ECharacterState というように、enum class として定義する。こう書くことで、Enum や変数が被るケースを防ぐ。
code:cpp
enum class CharacterState
{
Unequipped,...
};
ECharacterState State = CharacterState::Unequipped;
UE5 の場合
enum class は慣習的にEをつける。ECharacterStateという感じで。
enum 定数側にも、その enum class の頭の略字をつけることが多い。ECharacterStateなら、ECS
ECS_Unequippedという感じで。
BP 側で使えるようにするには、UENUM()マクロを付与する。
enum 定数に使われる数字を指定する。int32でもいいが、マイナスを無理に使わないならuint8として定義しておく。
enum 定数を BP で視認しやすくする。UMETA(DisplayName = "Unequipped")というように記載しておくと良い。
code:イメージ.cpp
UENUM(BlueprintType)
enum class ECharacterState : uint8
{
ECS_Unequipped UMETA(DisplayName = "Unequipped"),
ECS_EquippedOneHandedWeapon UMETA(DisplayName = "Equipped One-Handed Weapon"),
ECS_EquippedTwoHandedWeapon UMETA(DisplayName = "Equipped Two-Handed Weapon"),
};
注意点
別の.hで定義した Enum を他のクラスでも使いたい場合、#include無しでは使えない。前方宣言もダメ。
複数のクラスから呼び出す想定の Enum は特定の.hで定義するのではなく、Enum 単体のファイルを作ってしまうのが良い。
https://scrapbox.io/files/6a0ad75a5c3c134ed4421347.png
こんな感じで小分けにできる↓
Blueprint ノードの色
https://scrapbox.io/files/6a0c1dae34d306895b794c57.png
赤色: イベントノード
いつ実行するのかを決める起点となるノード
ゲーム中のキー入力や当たり判定、時間経過などで検知する。必ず一番左端で配置される。
画像の例だと、攻撃ボタンが押された時を起点としている。
青色: 関数(Function)ノード
何をするのかを決めるアクション。
データを加工したり、キャラクターにアクションを命じる。
画像の例だと、Montage の再生、特定の Montage Section に飛べ、というような命令を実行している。
緑色: 純粋関数(Pure Function) / 取得ノード
ターゲットの現在状態を引っ張ってくる。
実行ピンがない。何かが実行されるついでに、その時点での値を渡す役割なので実行順序が存在しない。
画像の例だと、Get Anim Instance を Montage Play の実行時に取り出しているという形。
その他(使うときに調べよう)
code:txt
# 色
グレー (灰色)フロー制御Branch (if文), Delay (待機), For Loop (ループ)
紫 (ムラサキ)コンストラクションConstruction Script 内でのコンポーネント操作など
オレンジ / 黄タイムラインTimeline (時間経過による数値変化を作る時)
水色 (シアン)キャストCast To Bp_Player (相手が誰かを確認・特定する時)
# 接続ピン
ピン(丸いポッチ)の色
白い矢印: 実行の流れ。これが繋がっていないとノードは無視。
青い丸: オブジェクト参照。MeshやActorなど「モノ」そのものを指す。
赤い丸: Boolean (真偽値)。Yes/No, True/False の情報。
黄色の丸: Vector (ベクトル)。X, Y, Z の3つの数字のセット(座標や方向)。
TArray と C++ の Array
C++
静的配列
Enemy* Enemies[5];というように、配列の大きさを明示する必要がある。これはコンパイル時に消費するメモリ量を記憶させ、事前に持たせる必要があるため。つまりこれ以上は持たせられないし、3つしか使わなかったとしても、5つ分保持することになる。また、Enemies[5]としたら、0から4の index として作られる。Enemies[5](6つめ)を参照しようとするとクラッシュする。
Dynamic Arrays (動的配列)
Array<Enemy*> Enemiesというように書く。Add()などで追加したとき、内部に保管する大きさが動的に変わる。
UE
TArray<Type> Enemies と書いて、動的配列の形で定義できる。
従来通り、Add(). AddUnique(), Remove(), Empty(), Enemies[5]->GetActorLocation()というように index などでも中身を呼べる。
DataTable / Data Table
Unity でいうところの ScriptableObject みたいな感じ。C++ で struct を作る。
※UE 側から Generate C++ Class で作るのはあんまり向いてないかも。
.cppを消したり、元のクラスを消したり自分で加工する必要がある
code:DropItemTable.h
USTRUCT(BlueprintType)
struct LINEARDUNGEON_API FDropItemData : public FTableRowBase
{
GENERATED_BODY()
// ドロップするアイテムのクラス
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Drop Data")
TSubclassOf<AItemBase> ItemClass;
// ドロップ確率 (0.0 = 0%, 1.0 = 100%)
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Drop Data", meta = (ClampMin = "0.0", ClampMax = "1.0"))
float DropRate = 0.2f;
// 最小ドロップ数
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Drop Data", meta = (ClampMin = "1"))
int32 MinQuantity = 1;
// 最大ドロップ数
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Drop Data", meta = (ClampMin = "1"))
int32 MaxQuantity = 1;
};
以下に気を付けないとビルドがこけることがある
#include "DropItemData.generated.h"を含める
.cpp側は削除する(or 作らない)
USTRUCT(BlueprintType)を付与する
GENERATED_BODY()は;は不要
使う
https://scrapbox.io/files/6a14387765e24daf8cb69ae6.png
Data Table 選択後、どの struct をベースに作るか選んで作成。開くと excel のようなページが出るので、自分で値を入れていく。
Actor Component
例えば、Geometry Collection に GetHit 時アイテムを落とす処理を作るとする。
ただし敵を倒したときなどにもアイテムを落としたい。そういったケースに、アイテムを落とすという Actor Component に分割しておくと、そうしたいクラスに対して付与していくだけでアイテムドロップ処理を共通化できる。
用意
⭐Loot: 戦利品の意味。
UE で C++ Class を作成時、Actor Components で作る。
code:LootDropComponent.cpp
# h
class UDataTable;
UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class LINEARDUNGEON_API ULootDropComponent : public UActorComponent
{
GENERATED_BODY()
public:
UFUNCTION(BlueprintCallable, Category = "Loot")
void ExecuteDrop(const FVector& SpawnLocation, const FRotator& SpawnRotation);
protected:
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Loot")
TObjectPtr<UDataTable> LootTable;
bool bIsExecDrop = false;
# cpp
void ULootDropComponent::ExecuteDrop(const FVector& SpawnLocation, const FRotator& SpawnRotation)
{
if (!LootTable || !GetWorld()) return;
if (bIsExecDrop == true) return; // 二重起動防止
bIsExecDrop = true;
TArray<FDropItemData*> AllRows;
LootTable->GetAllRows<FDropItemData>(TEXT("LootDropContext"), AllRows);
for (FDropItemData* Row : AllRows)
{
if (Row && Row->ItemClass)
{
const float RandomValue = FMath::FRand();
if (RandomValue <= Row->DropRate)
{
UE_LOGFMT(LogTemp, Warning, "Item spawned.");
const int32 SpawnCount = 1;
GetWorld()->SpawnActor<AItemBase>(Row->ItemClass, SpawnLocation, SpawnRotation);
public で、ExecuteDrop()というドロップを選択する処理を持たせる
code:BreakableActor.cpp
void ABreakableActor::GetHit_Implementation(const FVector& ImpactPoint)
{
if (LootDropComponent)
// 自身の位置を渡してドロップ処理をコンポーネントに委譲
LootDropComponent->ExecuteDrop(GetActorLocation(), GetActorRotation());
インターフェースで、こんな感じで自分が所持していたらそれを実行させられる
BP 側はこんな感じ
https://scrapbox.io/files/6a143daa65e24daf8cb6a331.png
プラグイン
Graph Printer を Fab からインストールしてみた。
アセットなどとは違い、UE に直接インストールする形になる。
プロジェクトを起動して、Edit > Plugins で探して、チェックを入れて restart する。
https://scrapbox.io/files/6a2f4a5974d67ad5aefec4fb.png
こんな感じで撮影できる
https://scrapbox.io/files/6a2f4ae274d67ad5aefec67b.png