ローカライゼーションのCommandlet紹介
使用バージョン:UE4.24.3
はじめに
UE4のローカライゼーションダッシュボードを利用してローカライズ対応を行う場合があるかと思います。
通常はテキストの追加や編集を行った際にローカライゼーションダッシュボードからテキスト収集やpoファイルのインポート、コンパイルを行うかと思いますが、これらの操作についてはCommandlet
が用意されているため、まとめて実行することで操作を簡略化することができます。
ローカライゼーションについては下記の資料が大変参考になります。
下記のコード中に出てくるExecuteCommandlet
の実装については以下を参照ください。
準備
以下のモジュールを依存関係として追加する必要があります。
Localization
UnrealEd
GatherTextCommandlet
GatherText
というCommandlet
があり、このCommandlet
から各操作を行います。
実行する際に引数として、{ProjectDir}/config/Localization
以下に生成されるゲームターゲットのiniファイルを指定します。
この時に指定するiniファイル内の設定を元に内部で適したCommandlet
が実行されます。
iniファイル | 動作 |
---|---|
[ゲームターゲット]_Gather.ini | テキスト収集 |
[ゲームターゲット]_Import.ini | poファイルインポート |
[ゲームターゲット]_Export.ini | poファイルエクスポート |
[ゲームターゲット]_GenerateReports.ini | WordCountやStatus等のレポート生成 |
[ゲームターゲット]_Compile.ini | コンパイル |
iniファイルパスの取得
LocalizationConfigurationScript
に各iniファイルを相対パスで取得する関数があります。
しかし、取得したパスをそのままCommandlet
の引数へ渡すと、相対パスだった場合はプロジェクトもしくはエンジンのパスが付与されてしまうため、絶対パスに変換してから渡します。
テキスト収集
#include "Misc/Paths.h" #include "LocalizationModule.h" #include "LocalizationTargetTypes.h" #include "LocalizationConfigurationScript.h" #include "Commandlets/GatherTextCommandletBase.h" void GatherText(const FString& GameTargetName) { // ゲームターゲット名からLocalizationTargetを取得. ILocalizationModule& LocalizationModule = ILocalizationModule::Get(); ULocalizationTarget* LocalizationTarget = LocalizationModule.GetLocalizationTargetByName(GameTargetName, false); check(::IsValid(LocalizationTarget)); // [ゲームターゲット]_GatherText.iniの相対パスを取得. FString ConfigPath = LocalizationConfigurationScript::GetGatherTextConfigPath(LocalizationTarget); // 相対パス->絶対パス. ConfigPath = FPaths::ConvertRelativePathToFull(ConfigPath); // Commandlet実行. ExecuteCommandlet(TEXT("GatherTextCommandlet"), FString::Format(TEXT("-config=\"{0}\""), { ConfigPath })); }
poファイルインポート
#include "Misc/Paths.h" #include "LocalizationModule.h" #include "LocalizationTargetTypes.h" #include "LocalizationConfigurationScript.h" #include "Commandlets/GatherTextCommandletBase.h" void ImportPO(const FString& GameTargetName, const TOptional<FString> CultureName) { // ゲームターゲット名からLocalizationTargetを取得. ILocalizationModule& LocalizationModule = ILocalizationModule::Get(); ULocalizationTarget* LocalizationTarget = LocalizationModule.GetLocalizationTargetByName(GameTargetName, false); check(::IsValid(LocalizationTarget)); // [ゲームターゲット]_Import.iniの相対パスを取得. FString ConfigPath = LocalizationConfigurationScript::GetImportTextConfigPath(LocalizationTarget, CultureName); // 相対パス->絶対パス. ConfigPath = FPaths::ConvertRelativePathToFull(ConfigPath); // Commandlet実行. ExecuteCommandlet(TEXT("GatherTextCommandlet"), FString::Format(TEXT("-config=\"{0}\""), { ConfigPath })); }
poファイルエクスポート
#include "Misc/Paths.h" #include "LocalizationModule.h" #include "LocalizationTargetTypes.h" #include "LocalizationConfigurationScript.h" #include "Commandlets/GatherTextCommandletBase.h" void ExportPO(const FString& GameTargetName, const TOptional<FString> CultureName) { // ゲームターゲット名からLocalizationTargetを取得. ILocalizationModule& LocalizationModule = ILocalizationModule::Get(); ULocalizationTarget* LocalizationTarget = LocalizationModule.GetLocalizationTargetByName(GameTargetName, false); check(::IsValid(LocalizationTarget)); // [ゲームターゲット]_Export.iniの相対パスを取得. FString ConfigPath = LocalizationConfigurationScript::GetExportTextConfigPath(LocalizationTarget, CultureName); // 相対パス->絶対パス. ConfigPath = FPaths::ConvertRelativePathToFull(ConfigPath); // Commandlet実行. ExecuteCommandlet(TEXT("GatherTextCommandlet"), FString::Format(TEXT("-config=\"{0}\""), { ConfigPath })); }
コンパイル
#include "Misc/Paths.h" #include "LocalizationModule.h" #include "LocalizationTargetTypes.h" #include "LocalizationConfigurationScript.h" #include "Commandlets/GatherTextCommandletBase.h" void Compile(const FString& GameTargetName, const TOptional<FString> CultureName) { // ゲームターゲット名からLocalizationTargetを取得. ILocalizationModule& LocalizationModule = ILocalizationModule::Get(); ULocalizationTarget* LocalizationTarget = LocalizationModule.GetLocalizationTargetByName(GameTargetName, false); check(::IsValid(LocalizationTarget)); // [ゲームターゲット]_Compile.iniの相対パスを取得. FString ConfigPath = LocalizationConfigurationScript::GetCompileTextConfigPath(LocalizationTarget, CultureName); // 相対パス->絶対パス. ConfigPath = FPaths::ConvertRelativePathToFull(ConfigPath); // Commandlet実行. ExecuteCommandlet(TEXT("GatherTextCommandlet"), FString::Format(TEXT("-config=\"{0}\""), { ConfigPath })); }
翻訳済みテキスト数を更新してWord Count
に反映
#include "Misc/Paths.h" #include "LocalizationModule.h" #include "LocalizationTargetTypes.h" #include "LocalizationConfigurationScript.h" #include "Commandlets/GatherTextCommandletBase.h" void UpdateWordCount(const FString& GameTargetName) { // ゲームターゲット名からLocalizationTargetを取得. ILocalizationModule& LocalizationModule = ILocalizationModule::Get(); ULocalizationTarget* LocalizationTarget = LocalizationModule.GetLocalizationTargetByName(GameTargetName, false); check(::IsValid(LocalizationTarget)); // [ゲームターゲット]_GenerateReports.iniの相対パスを取得. FString ConfigPath = LocalizationConfigurationScript::GetWordCountReportConfigPath(LocalizationTarget); // 相対パス->絶対パス. ConfigPath = FPaths::ConvertRelativePathToFull(ConfigPath); // Commandlet実行. ExecuteCommandlet(TEXT("GatherTextCommandlet"), FString::Format(TEXT("-config=\"{0}\""), { ConfigPath })); // 生成したレポートを元に更新. LocalizationTarget->UpdateStatusFromConflictReport(); LocalizationTarget->UpdateWordCountsFromCSV(); }
実行例
上記の処理をまとめて呼び出す例です。
※テキスト収集の前に収集対象のStringTable
等を更新すると自動化が捗ります。
void Example(const FString& GameTargetName) { // テキスト収集. GatherText(GameTargetName); // POファイルをインポート. ImportPO(GameTargetName); // コンパイル. Compile(GameTargetName); // WordCount更新. UpdateWordCount(GameTargetName); }
おまけ
GatherText
の引数は;
区切りで複数のiniファイルを指定することができるようになっているようです。
個別に実行した場合はソースコントロールに関する処理が都度呼ばれるだけのようです。
特に問題がないようなら都合のよいやり方を選択して構わないと思います。
最後に
最初にゲームターゲットの設定をしてしまえば、それ以降の追加や更新の作業については簡略化することが可能となり確認作業がしやすくなります。
また、会社によっては自社でテキスト管理ツールを作成されているところもあるかと思います。
そういった場合にも少しの作業で連携することが可能になるのではないかと思います。
C++からCommandletを実行する方法
使用バージョン:UE4.24.3
目的
何かしらのバッチ処理や自作ツールからCommandlet
を実行したい場合があります。
コマンドライン引数-run=[Commandlet名]
で実行することができますが、複数のCommandlet
を実行したい場合は実行する度にエディタの起動と終了が実行されてしまうため、時間がかかってしまうことがあります。
そういった場合は複数のCommandlet
を実行する処理を用意することになるかと思います。
コード
#include "Commandlets/Commandlet.h" void ExecuteCommandlet(const FString& CommaneletName, const FString& Params) { UClass* CommandletClass = FindObject<UClass>(ANY_PACKAGE, *CommaneletName, false); check(::IsValid(CommandletClass)); UCommandlet* Commandlet = NewObject<UCommandlet>(GetTransientPackage(), CommandletClass); check(::IsValid(Commandlet)); Commandlet->Main(Params); }
プロパティ(構造体)やクラスのレイアウトカスタマイズの登録/解除
使用バージョン:UE4.24.3
目的
エディタ上でのプロパティ(構造体)やクラスのレイアウトカスタマイズを使用する際の登録/解除の処理をFDetailCustomizationsModule
より抜粋したメモです。
準備
以下のモジュールを依存関係として追加する必要があります。
PropertyEditor
コード
モジュールh
#pragma once #include "CoreMinimal.h" #include "Modules/ModuleManager.h" #include "PropertyEditorDelegates.h" class FExampleModule : public IModuleInterface { private: /** 登録クラスリスト */ TSet<FName> RegisteredClassNames; /** 登録プロパティリスト */ TSet<FName> RegisteredPropertyTypes; public: /** IModuleInterface implementation */ virtual void StartupModule() override; virtual void ShutdownModule() override; private: /** クラスレイアウトカスタマイズ登録をまとめた関数 */ void RegisterCustomClassLayouts(); /** プロパティレイアウトカスタマイズ登録をまとめた関数 */ void RegisterCustomProperties(); /** クラスレイアウトカスタマイズ登録 */ void RegisterCustomClassLayout(FName ClassName, FOnGetDetailCustomizationInstance DetailLayoutDelegate); /** プロパティレイアウトカスタマイズ登録 */ void RegisterCustomPropertyTypeLayout(FName PropertyTypeName, FOnGetPropertyTypeCustomizationInstance PropertyTypeLayoutDelegate); /** 全てのレイアウトカスタマイズ登録解除 */ void UnregisterAllCustomLayouts(); };
モジュールcpp
#include "ExampleModule.h" #include "PropertyEditorModule.h" #define LOCTEXT_NAMESPACE "FExampleModule" void FExampleModule::StartupModule() { if (!IsRunningGame() && !IsRunningDedicatedServer()) { this->RegisterCustomClassLayouts(); this->RegisterCustomProperties(); // カスタマイズモジュールへ変更を通知. FPropertyEditorModule& PropertyModule = FModuleManager::GetModuleChecked<FPropertyEditorModule>("PropertyEditor"); PropertyModule.NotifyCustomizationModuleChanged(); } } //--- void FExampleModule::ShutdownModule() { this->UnregisterAllCustomLayouts(); } //--- void FExampleModule::RegisterCustomClassLayouts() { // クラスレイアウトカスタマイズ登録. } //--- void FExampleModule::RegisterCustomProperties() { // プロパティレイアウトカスタマイズ登録. } //--- void FExampleModule::RegisterCustomClassLayout(FName ClassName, FOnGetDetailCustomizationInstance DetailLayoutDelegate) { check(ClassName != NAME_None); this->RegisteredClassNames.Add(ClassName); static FName PropertyEditor("PropertyEditor"); FPropertyEditorModule& PropertyModule = FModuleManager::GetModuleChecked<FPropertyEditorModule>(PropertyEditor); PropertyModule.RegisterCustomClassLayout(ClassName, DetailLayoutDelegate); } //--- void FExampleModule::RegisterCustomPropertyTypeLayout(FName PropertyTypeName, FOnGetPropertyTypeCustomizationInstance PropertyTypeLayoutDelegate) { check(PropertyTypeName != NAME_None); this->RegisteredPropertyTypes.Add(PropertyTypeName); static FName PropertyEditor("PropertyEditor"); FPropertyEditorModule& PropertyModule = FModuleManager::GetModuleChecked<FPropertyEditorModule>(PropertyEditor); PropertyModule.RegisterCustomPropertyTypeLayout(PropertyTypeName, PropertyTypeLayoutDelegate); } //--- void FExampleModule::UnregisterAllCustomLayouts() { if (FModuleManager::Get().IsModuleLoaded("PropertyEditor")) { FPropertyEditorModule& PropertyModule = FModuleManager::GetModuleChecked<FPropertyEditorModule>("PropertyEditor"); // Unregister all classes customized by name for (auto It = this->RegisteredClassNames.CreateConstIterator(); It; ++It) { if (It->IsValid()) { PropertyModule.UnregisterCustomClassLayout(*It); } } // Unregister all structures for (auto It = this->RegisteredPropertyTypes.CreateConstIterator(); It; ++It) { if (It->IsValid()) { PropertyModule.UnregisterCustomPropertyTypeLayout(*It); } } PropertyModule.NotifyCustomizationModuleChanged(); } } //--- #undef LOCTEXT_NAMESPACE IMPLEMENT_MODULE(FExampleModule, Example)
IInputProcessorによるキー入力処理について
使用バージョン:UE4.24.3
はじめに
キー入力処理は通常ではPlayerController
、Level
、Actor
、Widget
等で行っているかと思います。
個々のキー入力処理を行う場合であればこれで問題ないと思いますが、例えば「最後に入力されたデバイス(キーボードorゲームパッドorマウス)によって操作アイコンを変更したい」といった場合だと状況によってはキー入力処理が呼ばれない場合や複数箇所に処理が必要な可能性があり色々と不都合があります。
そういった場合はIInputProcessor
を用いてキー入力処理を行う方法がよさそうです。
キー入力処理が呼ばれない状況とは
PlayerController
のSetInputMode()
を用いることでキー入力処理を制限することができます。
例えば、InputModeUIOnly
に設定している場合はPlayerController
のキー入力処理が行われなくなります。
これ以外にも、以降のキー入力処理が行われないように入力の消費設定を行っている場合もあります。
準備
以下のモジュールを依存関係として追加する必要があります。
Slate
使い方
IInputProcessor
を継承したクラスを用意し、FSlateApplication
へ登録することで呼び出されるようになります。
登録するIInputProcessor
継承クラスはGameInstance
等で保持させておき、必要なタイミングで登録/解除を行います。
IInputProcessor
継承クラス
#pragma once #include "CoreMinimal.h" #include "Framework/Application/IInputProcessor.h" #include "InputProcessor.generated.h" class FInputProcessor : public IInputProcessor { using Super = IInputProcessor; public: virtual void Tick(const float DeltaTime, FSlateApplication& SlateApp, TSharedRef<ICursor> Cursor) override { } virtual bool HandleKeyDownEvent(FSlateApplication& SlateApp, const FKeyEvent& InKeyEvent) override { // 任意のキー押下イベント. return Super::HandleKeyDownEvent(SlateApp, InKeyEvent); } virtual bool HandleKeyUpEvent(FSlateApplication& SlateApp, const FKeyEvent& InKeyEvent) override { // 任意のキー離上イベント. return Super::HandleKeyUpEvent(SlateApp, InKeyEvent); } virtual bool HandleAnalogInputEvent(FSlateApplication& SlateApp, const FAnalogInputEvent& InAnalogInputEvent) override { // 任意のアナログ入力イベント. return Super::HandleAnalogInputEvent(SlateApp, InAnalogInputEvent); } };
FSlateApplication
の登録/解除
#pragma once #include "Framework/Application/SlateApplication.h" class FExample { private: TSharedPtr<FInputProcessor> InputProcessor = MakeShareable(new FInputProcessor()); public: /** InputProcessorを登録して有効にします */ void EnableInputProcessor() { FSlateApplication& SlateApplication = FSlateApplication::Get(); SlateApplication.RegisterInputPreProcessor(this->InputProcessor); } /** InputProcessorを登録解除して無効にします */ void DisableInputProcessor() { FSlateApplication& SlateApplication = FSlateApplication::Get(); SlateApplication.UnregisterInputPreProcessor(this->InputProcessor); } };
最後に
今回紹介したIInputProcessor
を用いることで独自にキー入力処理することができます。
必ずしも利用される機能ではないかもしれませんが、状況を選ばずキー入力処理を行う必要がある場合は利用してみてもいいかと思います。
C++から変更アセットを保存する方法
使用バージョン:UE4.24.2
はじめに
エディタ上でアセットに対して何かしらの編集処理を行った際に自動で保存を実行したい場合があります。
そういった場合に対象アセットを保存するためのラッパー関数を用意する機会がありましたので、その備忘録となります。
準備
エディタ用のコードとなるため、エディタ用モジュールを用意しそちらへ実装します。
実装方法
依存モジュール
以下のモジュールを依存関係として追加する必要があります。
UnrealEd
ヘッダー
もし、BPから呼び出したい場合はUFUNCTION
でBPへ公開するよう設定すれば動作するかと思います。
#pragma once #include "CoreMinimal.h" #include "Kismet/BlueprintFunctionLibrary.h" #include "AssetUtilityFunctionLibrary.generated.h" UCLASS() class XXX_API UAssetUtilityFunctionLibrary: public UBlueprintFunctionLibrary { GENERATED_BODY() public: /** * アセットの保存を実行します * * @param [in] InTargets 対象アセット * @param [in] bCheckDirty trueの場合は変更(Dirty)アセットのみが保存されます * @param [in] bPromptToSave trueの場合はアセットの保存確認を求められます。falseの場合は全て保存されます。 */ static void ExecuteSaveAssets(const TArray<UObject*>& InTargets, bool bCheckDirty = true, bool bPromptToSave = false); };
実装
#include "AssetUtilityFunctionLibrary.h" #if WITH_EDITOR #include "FileHelpers.h" #endif // WITH_EDITOR void UAssetUtilityFunctionLibrary::ExecuteSaveAssets(const TArray<UObject*>& InTargets, const bool bCheckDirty, const bool bPromptToSave) { TArray<UPackage*> PackagesToSave; for (UObject* Obj : InTargets) { if (IsValid(Obj)) { if (Obj->HasAnyFlags(RF_Transient)) { continue; } UPackage* Package = Obj->GetOutermost(); if (IsValid(Package)) { PackagesToSave.Add(Package); } } } if (0 < PackagesToSave.Num()) { FEditorFileUtils::PromptForCheckoutAndSave( PackagesToSave, bCheckDirty, bPromptToSave); } } //---
最後に
直接FEditorFileUtils::PromptForCheckoutAndSave()
を呼び出しても構わないのですが、個人的にUObject
で渡す方が使い勝手がいいと思ったので、簡単なラッパー関数を用意して使用しています。
MarkPackageDirty()
の呼び出しだけだと保存まではされないため、保存したい場合はこのような処理を行う必要があります。
専用アセットを用意したり、アセットの変更を検知して別のアセットの変更を行ったりする場合に利用する機会があるかもしれません。
構造体のMake/BreakノードのNative実装
使用バージョン:UE4.24.2
はじめに
USTRUCT
のメタデータを使用することで構造体のMake/Breakノード
で指定の関数を呼び出すことができるようになります。
このときに指定する関数は、UBlueprintFunctionLibrary
等でBlueprintCallable
としてUFUNCTION
定義したものになります。
Makeノード
メタデータにHasNativeMake=[関数パス([モジュール名].[クラス名].[関数名])]
を設定します。
#pragma once #include "CoreMinimal.h" #include "Kismet/BlueprintFunctionLibrary.h" #include "Example.generated.h" /** * Makeノード実装 */ USTRUCT(BlueprintType , meta = (HasNativeMake="Example.ExampleFunctionLibrary.MakeExample") struct FExample { GENERATED_BODY() UPROPERTY() int32 Value = 0; }; UCLASS() class UExampleFunctionLibrary : public UBlueprintFunctionLibrary { GENERATED_BODY() public: /** FExampleのMakeノード用関数 */ UFUNCTION(BlueprintPure, meta = (NativeMakeFunc)) static FExample MakeExample() { FExample Example = { 1 }; return Example; } };
Breakノード
メタデータにHasNativeBreak=[関数パス([モジュール名].[クラス名].[関数名])]
を設定します。
#pragma once #include "CoreMinimal.h" #include "Kismet/BlueprintFunctionLibrary.h" #include "Example.generated.h" /** * Breakノード実装 */ USTRUCT(BlueprintType , meta = (HasNativeBreak= "Example.ExampleFunctionLibrary.BreakExample")) struct FExample { GENERATED_BODY() UPROPERTY() int32 Value = 0; }; UCLASS() class UExampleFunctionLibrary : public UBlueprintFunctionLibrary { GENERATED_BODY() public: /** FExampleのBreakノード用関数 */ UFUNCTION(BlueprintPure, meta = (NativeBreakFunc)) static void BreakExample(const FExample& InTarget, int32& Value) { Value = InTarget.Value; } };
最後に
通常は、プロパティをブループリント非公開の場合はMake/Breakノード
自体がノードリストに表示されませんが、この方法を利用した場合はノードリストに表示され使用することができます。
個人的な利用方法として、構造体をハンドルとして利用する際にプロパティをブループリント非公開にし、デフォルトのMake/Breakノード
を使用せず、必要であればMakeノード
の実装や関数を用意し必要最低限の機能だけを利用できるようにしています。
修正
関数パスのクラス名にプレフィックスついた状態だと、
PythonScriptPlugin
を利用した際にエディタ起動時にREPORT_PYTHON_GENERATION_ISSUE()
が発生してしまうのもあり、エンジン内を確認し該当箇所を修正いたしました。UExampleFunctionLibrary -> ExampleFunctionLibrary
デフォルトのLevelScriptActorを変更する方法
使用バージョン:UE4.24.2
はじめに
レベルを作成した際の親クラスとして設定されるLevelScriptActor
をプロジェクト設定より変更することができます。
例えばサブレベルの管理など各レベルに共通処理を実装したい場合に、独自のLevelScriptActor
を設定することで対応することも可能です。
準備
LevelScriptActor
を継承したクラスを作成します。
BPの場合
プロジェクト設定 > エンジン - 基本設定
を選択します。デフォルトクラス
のLevelScriptActorClass
の+
アイコンを選択します。
- 任意の名前をつけて保存します。
- 作成したブループリントが自動で開きますのでコンパイルし保存します。
C++の場合
#pragma once #include "CoreMinimal.h" #include "Engine/LevelScriptActor.h" #include "CustomLevelScriptActor.generated.h" /** * カスタムLevelScriptActor */ UCLASS() class ACustomLevelScriptActor : public ALevelScriptActor { GENERATED_BODY() };
デフォルトのLevelScriptActor
を変更
プロジェクト設定 > エンジン - 基本設定
を選択します。デフォルトクラス
のLevelScriptActorClass
に作成したLevelScriptActor
を設定します。
※BPを作成した場合は自動で設定されます。
- エディタを再起動します。
検証時はエディタ再起動することで新規レベル作成時に親クラスとして設定されるようになりました。
新規レベル作成し、親クラスがLevelScriptActor
のままだった場合はエディタを再起動してみてください。
注意
ULevel
からLevelScriptActor
を取得した際に、null
だった場合は該当レベルをコンパイルしてみてください。
※内部的にLevelScriptActor
が生成されるのは、該当レベルのBPをコンパイルしたときのようです。
最後に
各レベルの共通処理の対応方法は他にもありますが、サブレベルの管理や何かしらの管理機能の初期化を待つ必要がある場合など状況によってはこの方法を検討していいかなと思います。