PaperSloth’s diary

主にゲーム開発関連についての記事を書きます。

UE4 個人開発メモ

最終更新日:2020/09/17

変更履歴
2020/09/17 : Visual Studioのバージョン変更方法を追加

概要

 個人開発でのUE4のEditor Preference, Project Settings等のメモ
 随時更新していきます。
 基本はWindowsPC向けの内容。
 家庭用ゲーム機、アーケードゲーム、モバイル、XR(AR / VR)でも役立つ内容も含みます。
 各項目には確認時のバージョンを記載しています。
 バージョンが古かったり新しかったりすると
 項目がなくなったり、場所や名前が変わったりしている可能性があります。

環境

・Platform : Windows
・Unreal Engine4.17.2~
Visual Studio Community 2015


Project Settings

Windowタイトルの変更方法(UE4.17.2)

 WindowタイトルとはPC向けに作成した際のWidnowの左上に出るタイトルです。
f:id:PaperSloth:20171130003303p:plain

 何も指定していない場合はProject名が表示されます。

 設定は下図のProject - Description - Displayed
 「Project Displayed Title」から変更できます。
f:id:PaperSloth:20171130004532p:plain

 試しに"Example Window Name"と入れてみました。
f:id:PaperSloth:20171130004635p:plain

Application名の変更方法(UE4.17.2)

 上記のWindowsタイトルとよく間違えやすいのがこれです。
 設定は下図のProject - Description - About
 「Project Name」から変更できます。
 デフォルトではUE4Gameと入っています。
f:id:PaperSloth:20171130005438p:plain

 よくWindowタイトルを変更しようとしますが、ここを変えても上手くいきません。
 この名前はタスクマネージャー等で表示されるアプリの名前です。
f:id:PaperSloth:20171130005618p:plain

 ちょっと図がわかりにくいのですがProject Nameがあって、その下にWindowTitleがくるようになっています。
 編集をしていない場合だと、UE4Gameの下にProject名がくるようになっています。
f:id:PaperSloth:20171130010129p:plain

シェーダーモデルの制限によるメモリ容量削減(UE4.17.2)

 設定は下図のPlatforms - Windows
 「Targeted RHIs」から変更できます。

 UE4Windows向けに作成している場合
 デフォルトのRHI(Rendering Hardward Interface)はDX10, DX11が有効になっています。
f:id:PaperSloth:20171130220108p:plain
 これをDX11(ShaderModel5)のみにすることで不要なシェーダーが減り、パッケージ容量を削減できます。
 ThirdPersonTemplate等で試してみてもほぼ全く変わらないのですが
 規模が大きく、マテリアルが増えるほど効果が現れやすい部分です。

 試しに自分のゲームで比較してみた結果
 DX10, 11だと242KBのシェーダー
f:id:PaperSloth:20171130221048p:plain
 DX11のみだと124KBと約半分の容量になりました。
f:id:PaperSloth:20171130221057p:plain

 このように数百KB程度だと微々たる差ですが、これが数百、数千のマテリアルとなると話が変わってきます。
 また、シェーダーも1つで数百MBだと半分ほどの容量になるのは大きな変化ですね。

パッケージング関連

 項目が多いので、細かく分けます。
 設定箇所はProject - Packaging以下のものになります。

PakFileの使用(UE4.17.2)

 「Use Pak File」にチェックを入れると有効になります。
 説明の前に図で見てもらうと分かりやすいと思います。
 Pak Fileを使用した場合
f:id:PaperSloth:20171130222234p:plain
 中には.pakの形式でContent以下のAssetが1つのデータに圧縮されています。

 Pak Fileを使用しない場合
f:id:PaperSloth:20171130222333p:plain

 Pakファイルの使用は容量の削減になり
 転送ファイル数が減るので配布時のダウンロードも早くなり
 ロードも早くなるため使用することを推奨します。
 容量の参考までに自分のプロジェクトで使用した場合の容量を載せておきます。
 使用した場合  :532MB
 使用しない場合 :786MB

パッケージの圧縮(4.17.2)

 「Create compressed cooked packages」にチェックを入れると有効になります。
 f:id:PaperSloth:20171203220605p:plain
 なお、このオプションの効果を有効にするにはUse Pak Fileにチェックを入れておく必要があります。
 容量の参考までに自分のプロジェクトで使用した場合の容量を載せておきます。
 使用した場合  :547MB
 使用しない場合 :950MB

 さて、パッケージの圧縮はDLで配信するPC向け等にはオススメですが、
 一部プラットフォームは注意が必要です。
 PlayStation 4の場合
  PlayStation 4 の圧縮と重複し、ファイル サイズは減らずにロード時間が長くなってしまいます。
 Nintendo Switchの場合
  ロード時間が早くなる場合と遅くなる場合があります。
 詳しくは下部の参考資料のリンクを見てください。


Editor Preference

Blutilityの有効化(UE4.17.2)

 BlutilitはBlueprintをEditor上で呼び出すことでお手軽にEditor拡張が出来る機能です。
 UE4.17.2現在は実験的な機能として提供されています。
 この機能に関しては不安定というよりは、機能がまだ足りないため実験的な機能なのだと思います。

 設定は下図のEditor Preference - General - Experimental - Tools
 「Editor Utility Blueprints(Blutility)」から有効に出来ます。
 有効にした際のEditorの再起動は不要です。
f:id:PaperSloth:20171130011354p:plain

EQS(Environment Querying System)の有効化(UE4.17.2)

 EQSはAIが目的に応じた位置を探す時に使われる機能です。
 この位置は動的に生成され、その中から最もスコアの高い位置を選択します。

 こちらも実験的な機能として提供されていますが
 EpicGamesの開発タイトルである「Robo Recall」でも採用されており、信頼性は高いです。

 設定は下図のEditor Preference - General - Experimental - AI
 「Environment Querying System」から有効に出来ます。
 有効にした際のEditorの再起動は不要です。
f:id:PaperSloth:20171130012353p:plain

Visual Studioのバージョン指定方法(UE4.25.3)

 つい最近変更したのがUE4.25.3でしたが、結構古いバージョンでも同様のはずです。
 設定はEditor Preference -> Source Code
 「Source Code Editor」から変更できます。
f:id:PaperSloth:20200917005815p:plain
 変更後は「File -> Reflesh Visual Studio XX Project」から更新することで適用されます。
 以下のドキュメントにUE4のエンジンバージョンごとのVSのバージョンが載っています。
 2020/09/17時点では下記のバージョン対応になっています。
 VS2019 がないのはドキュメントの更新忘れのような気もしますが、一先ずこのバージョンのようですね。
  4.22 - ...  :VS 2017
  4.15 - 4.21 :VS 2017
  4.10 - 4.14 :VS 2015
  4.2 - 4.9 :VS 2013
Unreal Engine 用に Visual Studio をセットアップする | Unreal Engine ドキュメント


参考資料

シェーダーモデルの制限によるメモリ容量削減
バイキング様のスライド63ページ付近に該当箇所の説明があります。
マジシャンズデッド ポストモーテム ~マテリアル編~ (株式会社Byking: 鈴木孝司様、成相真治様) #UE4DD

パッケージング関連
パッケージの圧縮によるプラットフォームごとの注意点等詳しくはドキュメントに載っています。
プロジェクトをパッケージ化する | Unreal Engine ドキュメント

UE5 UE_LOGをもうちょっと使いやすくする話

環境

概要

実際に記述方法として
先日登壇したUnreal Engine Meetup Connect - Vol.1 - ゲーム開発編にて紹介させていただきました。
Unreal Engine Meetup Connect - Vol.1 - ゲーム開発編 - connpass

本スライドのp12 - 15の内容をもう少し分かりやすくまとめた記事になります。
イスを支える技術 | ドクセル

ソースコードの全文は最後のまとめに貼ってあります。
コピペしていい感じに使ってください。

通常のUE_LOGのおさらい

以下のように記述するケースが多いかと思います。

UE_LOG(LogTemp, Log, TEXT("Log Message."));
UE_LOG(LogTemp, Warning, TEXT("Log Warning."));
UE_LOG(LogTemp, Error, TEXT("Log Error."));

これでもよいですが、LogTempはEngineやPlugin内でも100箇所近くで使用されており、何かと混在する可能性があります。
そのため、LogCateogoryを追加するのが望ましいです。

UE_LOGをもうちょっと使いやすくするには

今回のProject構成
プロジェクト/モジュール名 : Odin
プロジェクト構成

+- Source
  +- Odin (Module名)
    +- Odin.h
    +- Odin.cpp
    +- Odin.Build.cs
+- Odin.uproject

ここにOdinLoggerクラスを追加しました。

プロジェクト構成

+- Source
  +- Odin (Module名)
    +- Core
        +- OdinLogger.h
        +- OdinLogger.cpp
    +- Odin.h
    +- Odin.cpp
    +- Odin.Build.cs
+- Odin.uproject

LogCategoryの追加

LogCategoryを追加するには
以下の2つのMacroを追加してやる必要があります。

DECLARE_LOG_CATEGORY_EXTERN({"LogCategoryName"}, Log, All);
DEFINE_LOG_CATEGORY({"LogCategoryName"});

それぞれ、headerとcppに定義を追加します。

あとはUE_LOGを使用したいクラス箇所でOdinLogger.hをincludeして
以下のように記述することで独自のLogCategoryの使用が可能になります。

#include "Core/OdinLogger.h"

... (省略)
UE_LOG(LogOdin, Log, TEXT("Odin Log Message."));

LogCategory を追加することの利点として
Output Log内のFilterが使えるようになることも利点の1つです。

UE_LOGを独自Categoryで再定義する

さて、先程の実装で新規にCategory分けができて便利になりました。
もう少し利便性を上げたいと思います。

都度UE_LOGのMacroを記述するのは結構手間ですので、以下の書き方だけで実行できるようにします。

ODIN_LOG(TEXT("Odin Log Message"));

先程追加したlog用のheaderファイルを以下のように改善します。

これでUE_LOGの呼び出しと検索がかなり楽になりました。
特にエラーだけやWarningだけを検索したいといったケースで役立つかと思います。
UE_LOGの場合だとEngine側のコードも検索にヒットしてしまうため、別途独自のLoggerを再定義するのはほぼほぼ必須だと思います。

ODIN_LOG(TEXT("Odin Log Message"));
ODIN_WARNING(TEXT("Odin Log Warning"));
ODIN_ERROR(TEXT("Odin Log Error."));

別Moduleでも使用したい場合

ここまでの実装では別Moduleからの呼び出しができません。
DECLARE_LOG_CATEGORY_EXTERNに少し手を加えるだけで対応ができます。
PrefixにModule名_APIを追加してやるだけです。

ODIN_API DECLARE_LOG_CATEGORY_EXTERN(LogOdin, Log, All);

まとめ

・独自のLogCateogoryを追加しよう
・UE_LOGの呼び出しを簡略化しよう
といった内容になります。
単純にUE_LOGの長い記述が短くなるだけでも楽になるので、こういった実装をするのも良いかなと思います。

最後にLoggerのheaderとcppの全文を載せておきます。

以上です!

UE5 大雑把にフリーカメラを使う方法

環境

  • Windows 10 Pro
  • Unreal Engine 5.3.2
  • VRM4U : ver:20240212 (VRM4U_5_3_20240212)
  • VRoid SDK : ver:0.3.2 (VRoidSDK-ue-0.3.2)

概要

昨今のゲームではオープンワールドやアクションゲームやレースゲーム等
様々なジャンルのゲームでフリーカメラ / 撮影モード機能が実装されています。

実際にゲームで実装しようとすると結構な工数がかかって手間ですね。
今回は正確には何も実装していませんが、UEの機能のご紹介を。

フリーカメラを使うには

本題ですが、フリーカメラ機能を使うには
UE5のコンソールコマンドで以下を入力します。

ToggleDebugCamera


英語環境なら「`キー」
日本語環境なら「@キー」
もしくはOutputLogやCmdの欄から入力が出来ます。

サンプルとしてThirdPersonMapでVRMを扱うLevelを用意しました。

こちらのモデルをサンプルにお借りさせていただきました。
黒棘 - VRoid Hub


この状態でToggleDebugCameraを実行すると以下のような画面に切り替わります。

カメラの移動は基本のEditor操作と同じです。
Backspaceキーを押下で画面左に表示されているUIの表示 / 非表示切り替えが可能です。

特筆すべきは、Fキーを押下でFreeze Renderingが使用可能なこと
Bキーを押下で各種Bufferを確認することも可能です。

他にもVキーを押下で各種View Modeの切り替えが可能です。(Reflection Overrideを表示中)

モードは以下の順で切り替わります。
Lit -> Unlit -> Wireframe -> Detail Lighting -> Reflection -> Collison Pawn -> Collision Vis -> Lit ...

操作の一部まとめ
・UI非表示 : Backspace
・Freeze Rendering : F
・Buffer 表示 : B
・View Mode 切り替え : V

まとめ

以上で大雑把にフリーカメラっぽい機能(DebugCamera)を使用できます。
実際にゲームに実装としてのせることは出来ませんが
(ConsoleコマンドはShipping Buildでは動作しないこと、プレイヤーに見せたくないゲーム要素も見えてしまうこと等)
開発中の疑似的なフリーカメラとしては代用が可能かと思います。

また、開発用途として何かしらのキーで切り替えれるようにしておくと楽かもですね。
以下はThirdPersonMapのLevelBlueprintでBackspaceキー押下で切り替えれるようにしたもの。


VRMが気軽に使えるの楽しい!ダイレクトマーケティング

VRoid SDK for Unreal Engineをみんな使ってみてね!
VRoid SDK - 好きな3Dモデルで遊べる世界を作ろう
VRM4U
セットアップ - VRM4U

UE5 KawaiiPhysicsをBlueprintプロジェクトでもpkg化する方法

環境

  • UE5.3.2
  • KawaiiPhysics v1.14.0
  • Windows 10 Pro

概要

KawaiiPhysicsとは
github.com

おかずさん(https://twitter.com/pafuhana1213)が開発されているPluginで
揺れものを「かんたんに」「かわいく」揺らすことができる優れものです!

本Pluginの使用にあたってpkgを作成する場合、通常はC++プロジェクトへの変換が必要となります。

今回はこのPluginをBlueprintのみのプロジェクトでも(ちょっと強引に)pkg作成できるようにしていきます。
ざっくりと概要を書くと、Blueprintプロジェクトでも強制的に再ビルドさせればOKということです。
※動作保証は出来かねますので詰まりそうだと感じた場合は素直にC++プロジェクトへの変換がオススメです。

前準備

まずは新規にBlueprintプロジェクトを作成します。

続いてDLしてきたPluginをプロジェクトに入れて、プロジェクトを起動
KawaiiPhysicsが有効になっていることを確認します。

これで準備が整いました。

この段階でpkg作成,実行 / Quick Launchを行うと以下のエラーが出力されます。

Plugin 'KawaiiPhysics' failed to load because module 'KawaiiPhysics' could not be found.  Please ensure the plugin is properly installed, otherwise consider disabling the plugin for this project.

実行しようとしたけど、KawaiiPhysicsのmoduleが見つからないよ!とのこと。
どうして・・・

エラー解決、pkg作成までとQuick Launch手順

Web Browser Pluginを有効にします。
※分かりやすいので、Web Browser Pluginを使用していますがデフォルトで無効なPluginであればなんでも良さそうです(要検証)。

あるいは、直接.uproject に以下を追記

{
  ...
  "Plugins": [
    {
      "Name": "ModelingToolsEditorMode",
      "Enabled": true,
      "TargetAllowList": [
        "Editor"
      ]
    },
    // 以下を追記
    {
      "Name": "WebBrowserWidget",
      "Enabled": true
    }
  ]
}

この状態でプロジェクトを再起動して再度Quick Launchを実行すると以下のLogが出力されます。

UAT: Building {Project Name}...

長いLogが流れて、DLL等必要なものが再ビルドされて更新されていることが分かります。

懸念点

UAT: Using Visual Studio 2022 14.38.33133 toolchain ...(省略)

上記のLogが含まれており、ビルドするにあたってC++プロジェクト化は不要ですが
Visual Studio自体のインストールは必要かもしれません。

ここまでの手順でQuick Launch に成功しました。

pkgの作成

Sampleプロジェクトから動作チェックに必要なデータをMigrateしてきました。

Game Default MapをKawaiiPhysicsSample に変更

Development にてpkgを作成

無事にBlueprintのみのプロジェクトでもpkg作成が完了し、pkgを実行することができました。

まとめ

外部Pluginを導入した際にBlueprintのみのプロジェクトではQuick Launchやpkgの作成/実行に失敗することがあります。
今回のように、別途何かしらのPluginを有効化することで強制的に再ビルドさせることができる場合があります。

さて、1つ困ったもので…
このあとにWebWidget Pluginを無効化した場合には再度pkg実行が出来なくなってしまいます。
そのため、出来るだけ影響の少ない小規模なPluginを選定して有効化できると良さそうですね。

ちょっとしたハック的な手法になりますので
少しでも動作がおかしいな?と感じたらC++プロジェクトに変換するのが現状最善の策だとは思います。

他にも外部Pluginの影響で上手く動かないなって時は試してみてください。
因みに、今回はKawaiiPhysicsだったため素直に動作しました。

別のPluginでDependenciesPluginsに色々なPluginが含まれている場合にはもう少し複雑な回避方法になります。
その手順についても、どこかでいずれ公開できればと思います。

以上です!

UE5 ちょっと便利かもしれないプログラミングの書き方

環境

・UE5.3.2
・Rider 2023.3.2

概要

対象読者の想定としてはプロの現役プログラマーというよりは、そこを目指して勉学に励んでいる方のレベルを想定しています。
プログラマーのアーティストの方にも役立ち部分はあるかもしれません。
現在プログラマーの方には真新しい情報はないと思われます。

UE4/5向けにBlueprintやC++で記述していますが、C#やその他の言語でも活用できるちょっとしたTips程度のものをいくつ書いてみました。
BlueprintとUnreal C++のどちらでも利用可能なものを紹介していきます。

if文 / branchノードの削減または単純化 (Min / Max / Clampの利用)

ここではBlueprintやC++向けに紹介していますが、Min / Max / ClampはMaterialでも使用可能です。
また、0.0 ~ 1.0のClampに関してはMaterialではsaturateノードがあり
個人的にはそちらの方が0 - 1のClampであることが伝わりやすくて好みです。

アイテムや魔法での回復処理を組むケース

まずは以下のようにゲームで回復を行った時に
現在HP + 回復量の値が最大HPを超えたら最大HPとして
そうでない場合は回復後のHPを割り当てるような処理があった場合
通常は以下のように組むことがあると思います。

こういったケースではif文は不要でMinノードを使用することで簡潔に記述できます。

さらに、Blueprintの場合はMathExpressionでより一層簡潔に記述が可能です。

ダメージを受けてHPが減るケース

実際にはAny | Point | Radial Damage Event等を使用しますが、ここでは簡潔に説明するためCustomEventで定義しています。

先程同様にダメージ処理を組んでいきます。
HP - ダメージが0以下ならHPを0とし、そうでない場合はHP - ダメージが現在のHPとなります。

今度はMaxノードを使用します。

Minのケースと同様にMath Expression化可能です。

Clampについて

Clampは上記のMinとMaxを組み合わせたものです。
Value, Min, Maxの3つのPinがあり、ValueがMin以上でMax以下となるように制限をかけてくれます。

最後にC++での記述を載せておきます。

// 実際にはMaxHealth等のパラメーターはDataTableやDataAsset等にして管理するのが望ましいです。
void OnHeal(const float Heal)
{
    Health = FMath::Min(MaxHealth, Health + Heal);
}
void OnDamage(const float Damage)
{
    Health = FMath::Max(0.0f, Health - Damage);
}

配列のwrap around(上限に達したら最初に戻る)

先ずは、以下のように手持ちの武器を次の武器に切り替えるような処理を組んだとします。

このままでも現在選択している武器のindex情報は正しく
剣 -> 銃 -> 拡張武器 -> 剣... といった形で次の武器に切り替えることが可能です。

こういった処理をより簡潔に組むには、index % Max といった形で配列の最大数で剰余してやればよいです。

wrap aroundをよく使う場面としては
ゲームの設定画面やアイテム選択画面なんかが特に多いと思います。
設定内の画面品質のUIで
クオリティー低 -> 中 -> 高 -> 最高 -> 低... といった場面ですね。

C++で組むと以下のようになります。

TArray<FString> WeaponArray{"Sword", "Gun", "ExtraWeapon"};
void ChangeNextWeapon()
{
    CurrentWeanponIndex = (WeaponArray.Num() % ++CurrentWeanponIndex);
}

マイナス方向の配列のwrap around(下限に達したら最大数に戻る)

残念ながらマイナス方向のwrap aroundは剰余で求めることができません。

Xにて親切なお犬様がマイナス方向のwrap aroundについて教えてくださいました。

まずは愚直に組むと以下のような処理になるかと思います。

先程教えていただいた内容を元にノードを組むと以下のようになります。
Blueprintの配列操作にはLastIndexというノードがあり、配列の末尾のindexを取得可能です。
この場合は要素数が3つなので、Lengthは3となり
LastIndexは0, 1, 2と0始まりでカウントされるため末尾の2が取得できます。
内容としてはLength - 1と同義です。

最後にC++で組むと以下のようになります。

void ChangePrevWeapon()
{
    const int WeaponNum = WeaponArray.Num();
    CurrentWeanponIndex = (CurrentWeanponIndex + (WeaponNum - 1)) % WeaponNum;
}


先程のプラス方向のwrap aroundと組み合わせて
プラスマイナスの双方向を考慮したWrap関数を作っておくと便利かもしれません。

小数点の比較 (floatの比較)

例えば以下のように何かしらのfloat値に計算をして、==ノードで0.0 かどうかを比較したいといったケースがあると思います。

この場合に、良くも悪くもBlueprintではわりと動いてtrue になることがあるのですが
基本的に小数点の比較というのはプログラミングでは御八度です。
小数点の誤差でfalse になるケースがほとんどだったりします。

こういった誤差を考慮してだいたい同じ値という比較用のノードが用意されています。
それがNearly Equalノードです。
原則として、このノードを使うようにするとよいでしょう。

C++で記述すると以下です。

bool IsZeroFloat(const float Value)
{
    // C++の場合は0との比較用にIsNearlyZero()があります
    return FMath::IsNearlyZero(Value);
    // return FMath::IsNearlyEqual(Value, 0.0f);
}

余談ですが、UnityではMathf.Approximatelyを使用することで比較が可能です。

早期returnの活用

正直なところ、Blueprintでは特にシンプルになった感じがなくあまり恩恵を感じられないかもですが・・・
攻撃用の関数を以下のように組んだとします。
HPが0以上で攻撃のクールタイムが0になっていたら攻撃をするといった関数です。

条件式として
HPが0以上 かつ
クールタイムが0
であれば攻撃となります。

こういったケースでは早期returnを使用すると良いとされています。
HPが0であれば攻撃しない
クールタイムが残っていれば攻撃しない
といった条件式に置き換えることができます。

Blueprintでは恩恵を受けれている感じがないですね。
C++で記述してみます。
先ずは早期return なしver

void Attack()
{
    if (Health > 0.0f)
    {
        if (FMath::IsNearlyZero(CoolTime))
        {
            // 何かしらの攻撃処理
        }
    }
}

このようにネストが深くなって処理が読みにくいです。
早期returnで書き換えると以下のようにスッキリします。

void Attack()
{
    if (FMath::IsNearlyZero(Health))
        return;
    if (0.0f < CoolTime)
        return;
    // 何かしらの攻撃処理
}

まとめ

C++に絞ればもっとたくさんのネタがありますが、BlueprintとC++で考えてみるとあまり浮かびませんでした。
いずれも学生時代の頃に知って便利だと感じた小ネタになります。

早期return や説明用のlocal 変数定義等については、C++やその他の言語では読みやすくなって便利なのですが
いかんせんノードエディタではその恩恵があまり受けられず
むしろlocal変数を増やしたりすることで余計にノード郡が大きくなってしまったりと逆効果かな?と感じることがあります。

こういった浅いネタでなく、もう少しちゃんとしたネタが浮かべばまた書いていこうと思います。

UE5 1フレーム後に処理を実行する方法(Delay Until Next Tick)について

目次

環境

Unreal Engine 5.1.1

Delay Until Next Tickについて

Delay Until Next Tickを語る前に…UE4ではどうやってたの?っていうところ
papersloth.hatenablog.com

UE4のときはDelay Nodeを配置して
Durationに0を設定することで1フレーム処理を遅らせることが出来ていました。

もちろん、この手法はUE5でも同様に動作します。


続いて、UE5から実装されたDelay Until Next Tickノードについて。
こちらのノードは1 Frame動作を遅らせるという Delay Nodeと異なり
1 Tick 遅らせるというTooltipが記載されています。

処理的には同じ結果になるかとは思います(要検証ですが)

個人開発の場合や小規模開発の場合にはDelay (Duration 0.0)でも
Delay Until Next Tickでも良いかと思います。

ですが、ある程度の規模の開発においてはDelay Until Next Tickを使った方が良いのではないかと思います。
理由としては最適化を行いたい際に、チームメンバーが組んだBlueprintを見て
Delayで数秒遅らせたい処理なのか、次のフレームに処理を逃したいのかを検索したい時なんかに役立つケースがあると思われるからです。


この次のTickに処理を逃したい場合ってどういう時があるの?

というのは、以下の例ではあまり正しくないのですが
何かしらのActorを初期化したい場合に
既存のActor(ここではとりあえずPlayerControllerとしています)が存在すれば何かしらの処理を行うという場合等に使えるかと思います。
Actorの初期化 / 生成順が確実に保証されていないというケースで役立つと思います。

例えば、Characterの生成ロジックと武器の生成ロジックを別Actorで管理している場合に
Characterを初期化 -> 武器のInstanceがあれば初期化といったケース等がありそうですね。

UE4|IUE5 Delegate, EventDispatcherをC++で書く方法

目次

環境

・JetBraings Rider 2022.1.1
・Unreal Engine5.1.1

Unreal Engineで使用可能なDelegateについて

Delegateの種類について
シングルキャスト、マルチキャスト、ダイナミック(動的)、イベント等の種類があります。
公式docも併せて参照されるとより理解が深まるかと思います。
デリゲート | Unreal Engine ドキュメント
Delegates and Lamba Functions in Unreal Engine | Unreal Engine 5.1 Documentation

・シングルキャスト(Singlecast)
 関数を1つだけデリゲートにバインドできます。
マルチキャスト(Multicast)
 複数の関数をバインドしてすべての関数を一度に実行できます。
・ダイナミック(Dynamic)
 シリアライズ可能でリフレクションを使って関数を呼び出すことができます。
通常のデリゲートよりも動作が遅くなります。
・イベント(Event)
 複数の関数をバインドしてすべての関数を一度に実行できます。
マルチキャストに近いですが、任意のクラス内で定義して使います。
イベントをデリゲートするクラスのみがそのイベントの Broadcast、IsBound、Clear 関数を呼び出すことが可能で
外部クラスが Broadcast、IsBound、Clear 関数を呼び出さないようにしています。

デリゲートの宣言について
引数なし~最大引数9個までの登録が可能です
Engineコードでは下記に宣言されています。
(EngineDir)\Engine\Source\Runtime\Core\Public\Delegates\DelegateCombinations.h

// 基本の宣言
DECLARE_DELEGATE(DelegateName);
DECLARE_MULTICAST_DELEGATE(DelegateName);
DECLARE_DYNAMIC_DELEGATE(Delegate);
DECLARE_EVENT(OwningType, EventName);
DECLARE_DYNAMIC_MULTICAST_DELEGATE(DelegateName);

// 戻り値有
DECLARE_DELEGATE_RetVal(ReturnValueType, DelegateName);

// 引数有
DECLARE_DELEGATE_OneParam(DelegateName, ValueType);
DECLARE_DELEGATE_TwoParams(DelegateName, ValueType1, ValueType2)
... Nineまであります。

// 戻り値、引数有
DECLARE_DELEGATE_RetVal_OneParam(ReturnValueType, DelegateName, ValueType)
DECLARE_DELEGATE_RetVal_TwoParam(ReturnValueType, DelegateName, ValueType1, ValueType2)
// 実際に宣言を行う際の使用例
DECLARE_DELEGATE(FOnPostEditChangeOwner);
FOnPostEditChangeOwner OnPostEditChangeOwner;

DECLARE_DELEGATE_RetVal(FString, FOnGetSelectedCurve);
FOnGetSelectedCurve OnGetSelectedCurve;

DECLARE_DELEGATE_OneParam(FOnIsTransitioningChanged, bool);
FOnIsTransitioningChanged OnIsTransitioningChanged;

DECLARE_DELEGATE_RetVal_OneParam(bool, FOnIsAssetAutoReimportAvailable, UObject*);
FOnIsAssetAutoReimportAvailable IsAssetAutoReimportAvailableHandler;

// DECLARE_DYNAMIC_MULTICAST_DELEGATEに関してはBlueprintと連携可能
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FConnectedToSignallingServer);
// Blueprintからアサイン可能にするためにBlueprintAssignableを付与する
UPROPERTY(BlueprintAssignable, Category = "Pixel Streaming Delegates")
FConnectedToSignallingServer OnConnectedToSignallingServer;

// Blueprintからのアサイン、呼び出しを可能にする場合には
// BlueprintAssignable, BlueprintCallableを付与する
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FDynamicOnFinalPassRendered, class ACompositingElement*, CompElement, UTexture*, Texture);
UPROPERTY(BlueprintAssignable, BlueprintCallable, DisplayName=OnFinalPassRendered, Category="Composure")
FDynamicOnFinalPassRendered OnFinalPassRendered_BP;

C++Delegateを記述する方法

まずは新規にActorを追加した場合のheaderの記述

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "(ActorName).generated.h"

// 1. delegateの宣言を追加する場合にはGlobalスコープに追加することが多いです。
// ここに追加

UCLASS()
class (ModuleName)_API A(ActorName) : public AActor
{
    GENERATED_BODY()
	
public:
    A(ActorName)();
protected:
    virtual void BeginPlay() override;
public:	
    virtual void Tick(float DeltaTime) override;

    // 2. class内でdelegateの宣言を書くことも可能で
    // Blueprint連携する場合にはこちらに書くことが多いです
    // ここに追加
};

次にdelegateに処理を結合するための関数を見ていきます。
基本的には以下があります。
今回はこの中から一部の使い方を解説します。

BindStatic(); // staticなグローバル関数を結合
BindRaw(); // 生のC++ポインタdelegateを結合
BindLambda(); // ラムダ関数を結合
BindWeakLambda();
BindSP(); // shared pointerベースのメンバ関数を結合
BindThreadSafeSP();
BindUFunction(); // UFUNCTION()のメンバ関数を結合
BindUObject(); // UObject継承のクラスのメンバ関数を結合

Singlecast delegateの場合(DECLARE_DELEGATE)

まずこういった使い方はしないのですが、ctorでbindしてBeginPlayで呼び出しを行っています
BindLambdaを使用したシンプルな実装例です

...

DECLARE_DELEGATE(FExampleDelegate);

UCLASS()
class SANDBOX510_API AExampleDelegateActor : public AActor
{
    GENERATED_BODY()

    FExampleDelegate ExampleDelegate;

public:
    AExampleDelegateActor()
    {
        PrimaryActorTick.bCanEverTick = true;
        ExampleDelegate.BindLambda([]
        {
            UE_LOG(LogTemp, Error, TEXT("Call singlecast delegate."));
        });
    }

protected:
    virtual void BeginPlay() override
    {
        if (ExampleDelegate.IsBound())
        {
            ExampleDelegate.Execute();
        }
    }
    ...
};

実行するとBindした処理がExecuteで呼ばれていることが確認できます

余談ですがOutput LogのFiltersからErrorsのみやWarningsのみの表示が便利です

Multicast delegateの場合(DECLARE_MULTICAST_DELEGATE)

続いて、同様にMulticast delegateのケースも書いていきます。
以下の例では1つの処理を登録していますが
実際にはMulticast delegateなので複数の処理を登録することができます。
Singlecast delegateとの違いは処理の登録がBindではなく、Addになっている点と
実行時の処理がExecuteではなく、Broadcastになっている点です。

...
DECLARE_MULTICAST_DELEGATE(FExampleDelegate);

UCLASS()
class SANDBOX510_API AExampleDelegateActor : public AActor
{
    GENERATED_BODY()

    FExampleDelegate ExampleDelegate;

public:
    AExampleDelegateActor()
    {
        PrimaryActorTick.bCanEverTick = true;
        ExampleDelegate.AddLambda([]
        {
            UE_LOG(LogTemp, Error, TEXT("Call multicast delegate."));
        });
    }

protected:
    virtual void BeginPlay() override
    {
        if (ExampleDelegate.IsBound())
        {
            ExampleDelegate.Broadcast();
        }
    }
    ...
};

以下が実行結果になります。
Singlecastのケースとほとんど同じように使用することが出来ます。

Dynamic singlecast delegateの場合(DECLARE_DYNAMIC_DELEGATE)

Dynamic delegateシリアライズ可能でリフレクションを使って関数を呼び出すことができます。
要約するとBindした処理を関数名で検索することが出来ます。
ただし、Dynamic delegateは通常のSinglecast delegateと比較して動作が遅くなるという特徴があります。
また、BindDynamicで登録する関数には必ずUFUNCTIONを付与する必要があります。

...
DECLARE_DYNAMIC_DELEGATE(FExampleDelegate);

UCLASS()
class SANDBOX510_API AExampleDelegateActor : public AActor
{
    GENERATED_BODY()

    FExampleDelegate ExampleDelegate;

public:
    // Dynamic delegateにBindする関数にはUFUNCTION()が必須となる
    UFUNCTION()
    void OnCallDynamic()
    {
        UE_LOG(LogTemp, Error, TEXT("Call dynamic singlecast delegate."));
    }

    AExampleDelegateActor()
    {
        PrimaryActorTick.bCanEverTick = true;
        ExampleDelegate.BindDynamic(this, &AExampleDelegateActor::OnCallDynamic);
    }

protected:
    virtual void BeginPlay() override
    {
        // IsBound() でチェックしてExecute() をする場合と同様に、関数がBindされていれば実行する
        ExampleDelegate.ExecuteIfBound();
    }
    ...
};

以下が実行結果になります。

Eventの場合(DECLARE_EVENT)

Eventを使うユースケースがイマイチ掴めておらずなのですが…
公式docにも「イベントは マルチキャスト デリゲート と非常によく似ています」と記載があるように似た機能ではあります。
内部的にはMulticast delegateなのかなと思います。
相違点として、戻り値を指定できない点と実行できるのは宣言したクラスだけであるということなのですが
実際にはどこからでも呼び出しが出来てしまい、イマイチ有用性が理解できておらずです。

...
DECLARE_EVENT(AExampleDelegateActor, FExampleEvent);

UCLASS()
class SANDBOX510_API AExampleDelegateActor : public AActor
{
	GENERATED_BODY()

	FExampleEvent ExampleEvent;
public:
    // 別クラスから OnUpdate().AddSP(this, &AnotherClass::Function)のような使い方をするのが良さそう?
    FExampleEvent& OnUpdate()
    {
        return ExampleEvent;
    }

    AExampleDelegateActor()
    {
        PrimaryActorTick.bCanEverTick = true;
        ExampleEvent.AddLambda([]
        {
            UE_LOG(LogTemp, Error, TEXT("Call event."));
        });
    }
protected:
    virtual void BeginPlay() override
    {
        if (ExampleEvent.IsBound())
        {
            ExampleEvent.Broadcast();
        }
    }
};

実行結果(Multicast delegateと同様)

Dynamic Multicast delegateの場合(DECLARE_DYNAMIC_MULTICAST_DELEGATE)

これは先程紹介したDynamic delegateとMulticast delegateを合わせたものです。
シリアライズ可能で複数Bindが可能なdelegateとなっています。
この後に紹介するBlueprint連携でも使用されるため、使う機会は他より多いと思います。
ただし、負荷的には一番大きなものになりますね。

...
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FExampleDynamicMulticast);

UCLASS()
class SANDBOX510_API AExampleDelegateActor : public AActor
{
    GENERATED_BODY()

    FExampleDynamicMulticast ExampleDelegate;
public:
    UFUNCTION()
    void OnDynamiMulticast()
    {
        UE_LOG(LogTemp, Error, TEXT("Call dynamic multicast."));
    }
	
    AExampleDelegateActor()
    {
        PrimaryActorTick.bCanEverTick = true;
        ExampleDelegate.AddDynamic(this, &AExampleDelegateActor::OnDynamiMulticast);
    }
protected:
    virtual void BeginPlay() override
    {
        if (ExampleDelegate.IsBound())
        {
            ExampleDelegate.Broadcast();
        }
    }
};

実行結果は以下です。

Blueprintで呼び出し可能なDelegateC++で記述する方法

ここから本題というか、特に使うケースが多いものになると思います。
基本的には先程紹介したDynamic multicast delegateを使うことで
Blueprint上でEvent Dispatcherを追加したのと同様の処理を組むことができます。

Dynamic Multicast delegateを使用する(Event Dispacher相当)

BlueprintからアクセスするためにはDelegateの宣言をした変数に

UPROPERTY(BlueprintAssignable)

を付与するだけです。

まずはC++側のコードから書いていきます。

...
UCLASS()
class SANDBOX510_API AExampleDelegateActor : public AActor
{
    GENERATED_BODY()

    DECLARE_DYNAMIC_MULTICAST_DELEGATE(FExampleDynamicMulticast);
    UPROPERTY(BlueprintAssignable)
    FExampleDynamicMulticast ExampleDelegate;

public:
    AExampleDelegateActor();
protected:
    virtual void BeginPlay() override;

    // BlueprintからDelegateを実行するための関数を用意
    UFUNCTION(BlueprintCallable)
    void Invoke()
    {
        if (ExampleDelegate.IsBound())
        {
            ExampleDelegate.Broadcast();
        }
    }
};

上記のC++クラスを継承したBlueprintを用意します。
ここで先程のDelegate変数のExampleDelegateを打つと、Event Dispatcherと同じように扱うことができます。

Delegateに対してCustomEventをBindしてログを出力するようにしました。
Bindしただけでは実行されないため、先ほど作成したInvoke() 関数を使用して実行しています。

実行結果は以下になります。

Singlecast delegateを使用する(実はできる)

DECLARE_DYNAMIC_DELEGATEで宣言したDelegateでBlueprintと連携することも可能です。
この場合は、BlueprintAssignableではなくBlueprintReadWriteの属性を付与します。

...
UCLASS()
class SANDBOX510_API AExampleDelegateActor : public AActor
{
	GENERATED_BODY()

public:
	AExampleDelegateActor();

protected:
    // BlueprintReadWriteな変数なのでprivateではエラーになる
    DECLARE_DYNAMIC_DELEGATE(FExampleDynamicDelegate);
    UPROPERTY(BlueprintReadWrite)
    FExampleDynamicDelegate ExampleDelegate;

    virtual void BeginPlay() override;

    // BlueprintからDelegateを実行するための関数を用意
    UFUNCTION(BlueprintCallable)
    void Invoke()
    {
        ExampleDelegate.ExecuteIfBound();
    }
};

この場合のBlueprint連携はBindではなく、Setterを使用します。
そのあとにEventを繋ぐだけで処理を追加することが出来ます。
さきほど紹介したEventdispatchersの方式に比べて処理が軽いという利点もあります。
ただし、Singlecastなので複数の処理を登録することはできない点に注意です。

実行結果

まとめ

Delegate自体は複数用意されていて、記述方法も独特なため最初は困惑するかと思いますが
慣れてしまえばそんなに苦戦することもないと思います。
また、よく使われるケースとしてBlueprint連携があるかと思いますが
こちらについては主に使用されるのは上記の2つだと思いますので、覚えることも少なくて済むと思います。
他にも引数がある場合にはC++であれば型名のみでよくて、Blueprint連携の場合には型名と引数名が必要だったりと微妙に違う点もあります。


おまけ
SetTimer系の処理をC++で書く方法については以前に記事にしましたので、こちらを参照ください。
papersloth.hatenablog.com
BlueprintでDelegateを引数で使用する方法(ハック的な手法)も以前に記事にしていますので、こちらも必要に応じて活用ください。
papersloth.hatenablog.com

参考資料
Unreal Engine でのデリゲートとラムダ関数 | Unreal Engine 5.1 ドキュメント
Delegates in UE4, Raw C++, and BP Exposed - UE4: Guidebook