執筆バージョン: Unreal Engine 4.22
|
皆さんこんにちは。今回は便利な機能の実装方法をご紹介したいと思います。ゲーム開発においてイテレーションの最適化やバグを未然に防ぐ実装は常に課題になっているかと思います。そこで今回ご紹介する機能は、プログラマが事前に用意しておく事でレベルデザイナーの作業が効率化し、バグが発生しにくくなるというものです。
はじめに
さて、みなさんはUE4のデータテーブルは使っていますか?事前に静的なデータを用意しておき、ゲーム内でそれを参照することはよくあることだと思います。そこで有効になってくるのがデータテーブルです。特にUE4ではcsvファイルから簡単に指定した構造体でのデータテーブルアセットを用意することが可能です。詳細についてはこちらの記事を参照下さい。
さて、今回はキャラクターの名前とステータスの一覧をこんな感じで用意しました。ちなみに構造体はブループリントで用意しましたが、後々の取り回しを考えるとC++側で定義した方がよいでしょう。

次にこのデータテーブルを利用して、キャラクター名からキャラクターのステータスを取得する関数を作成します。

当然呼び出し側はこんな感じになるはずです。引数のCharacterNameはName型になりますし、ノードを使う側はその文字列を手で入力することになります。

ですがこのCharacterNameはデータテーブルの中から探してくるものであり、本来なら以下のようになって欲しいはずです。

ノードを利用する側はデータテーブルの一覧から選択したいわけですし、レベルに配置したアクターのプロパティを編集する場合も同じように一覧から選択する方が効率よく、また手入力による間違いも発生しなくなるはずです。

つまり顧客が本当に欲しかったものはName型ではなく、データテーブルのRowデータが列挙されるようなEnum型に近い型になるはずです。

実はこのノードのピンをプルダウンメニューから選択する方法、過去に弊社ブログで紹介した機能になります。今回はこの機能を使いつつ、更に詳細パネルの方もついで実装してしまおうというわけです。
それでは、どうやればこれらが実装できるかを解説していきたいと思います。ちなみにこれらの実装には事前に以下の内容が必要になってくるため、それぞれ過去のブログ内容をご参照下さい。
configに項目を追加 → [UE4]「エディタの環境設定」や「プロジェクト設定」に項目を追加する
エディタ用にモジュールを追加 → [UE4] モジュールについて
実装手順
1.C++側でデータテーブルが参照できるようにiniファイルにアセット参照を追加
まず準備としてC++側でアセットデータを簡単に参照できるように、configファイルにデータテーブルのアセット参照を追加します。
新規作成するTestSettings.hの実装です。cppは特に何も書きません。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
|
// Fill out your copyright notice in the Description page of Project Settings. #pragma once #include "CoreMinimal.h" #include "TestSettings.generated.h" /** * */ UCLASS( config = Game, meta = (DisplayName = "Test") ) class TEST_API UTestSettings : public UObject { GENERATED_BODY() public: UTestSettings() {}; // キャラクターステータス用データテーブルの参照 UPROPERTY( BlueprintReadOnly, EditAnywhere, Config, Category = Character ) FSoftObjectPath CharacterStatusAsset; }; |
既存の<ProjectName>.ppの内容です。今回はプロジェクト名がTestなので、Test.cppになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
|
// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. #include "Test.h" #include "Modules/ModuleManager.h" #if WITH_EDITOR #include "ISettingsModule.h" #include "TestSettings.h" #endif #define LOCTEXT_NAMESPACE "TEST" class FTestModule : public FDefaultGameModuleImpl { public: virtual void StartupModule() override { RegisterSettings(); } virtual void ShutdownModule() override { UnregisterSettings(); } protected: /** Register settings objects. */ void RegisterSettings() { #if WITH_EDITOR ISettingsModule* SettingsModule = FModuleManager::GetModulePtr<ISettingsModule>( "Settings" ); if( SettingsModule != nullptr ) { SettingsModule->RegisterSettings( "Project", "Project", "Test", LOCTEXT( "TestSettingsName", "Test" ), LOCTEXT( "TestSettingsDescription", "Configure the project Test." ), GetMutableDefault<UTestSettings>() ); } #endif } /** Unregister settings objects. */ void UnregisterSettings() { #if WITH_EDITOR ISettingsModule* SettingsModule = FModuleManager::GetModulePtr<ISettingsModule>( "Settings" ); if( SettingsModule != nullptr ) { SettingsModule->UnregisterSettings( "Project", "Project", "Test" ); } #endif } }; IMPLEMENT_PRIMARY_GAME_MODULE( FTestModule, Test, "Test" ); #undef LOCTEXT_NAMESPACE |
プロジェクト設定に項目が追加されたら該当のデータテーブルアセットを設定します。

2.エディタ用モジュール作成
自作用のピンや詳細パネル表示用の処理をランタイムと分けるためにエディタ用にモジュールを作成します。その際、エディタ側で使用する別モジュールを忘れずに追加しておきましょう。
新規作成したTestEd.Build.csは以下のようになります。
|
// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. using UnrealBuildTool; public class TestEd : ModuleRules { public TestEd(ReadOnlyTargetRules Target) : base(Target) { PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs; PublicDependencyModuleNames.AddRange(new string[] { "Test", "Core", "CoreUObject", "Engine", "InputCore" }); PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore", "GraphEditor", "UnrealEd", "BlueprintGraph" }); } } |
3.実際に使用する型の追加
以上で準備が完了したら、次は今まで使用していたName型に変わる新しい型を定義します。ここではキャラクターステータス用のデータテーブルなので、それが分かるような構造体名にします。ちなみに同じように別のデータテーブルでこの機能を作る場合、中身は全く同じで名前のみを変更した別の構造体を定義する事になります。この構造体はランタイム時も使用することになるので、当然ゲームモジュール側に追加して下さい。
新規作成したCustomPinStruct.hの中身です。cpp側は特に何も書きません。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
|
// Fill out your copyright notice in the Description page of Project Settings. #pragma once #include "CoreMinimal.h" #include "CustomPinStruct.generated.h" /** * キャラクターステータス用一覧 */ USTRUCT(BlueprintType) struct TEST_API FCharacterStatusList { GENERATED_USTRUCT_BODY() public: // データテーブルのRowNameになるのでFName型 UPROPERTY( EditAnywhere, BlueprintReadWrite ) FName Key; FCharacterStatusList( FName InKey ) :Key( InKey ) {}; FCharacterStatusList() : Key( TEXT( "None" ) ) {}; }; |
4.自作ピンの作成
まずはノードのピンの処理を実装します。この処理に関してはエディタのみでしか使用しないためエディタモジュール側に追加することにご注意下さい。また、後述する理由により、実際にデータテーブルを参照して文字列を列挙する処理は別クラスに分けているため、この時点ではまだビルドは通りません。ここでも別途データテーブルを増やしたい場合は、SCustomGraphPinBaseを基底クラスとした別クラスを追加し、SetDisplayStrings関数の中で呼び出す関数を変更することになります。また、FCustomGraphPinFactoryクラス内のCreatePin関数で、「3」で追加定義した構造体と先程追加したクラスを紐付ける処理を追加します。
新規作成したCustomGraphPin.hの中身です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79
|
// Fill out your copyright notice in the Description page of Project Settings. #pragma once #include "CoreMinimal.h" #include "SlateBasics.h" #include "SGraphPin.h" #include "EdGraph/EdGraphPin.h" #include "EdGraph/EdGraphSchema.h" #include "EdGraphSchema_K2.h" #include "EdGraphUtilities.h" #include "Blueprint/CustomPinStruct.h" #include "CustomDisplayList.h" /** * */ class TESTED_API SCustomGraphPinBase : public SGraphPin { protected: // ピンに表示される文字列の一覧 TArray<TSharedPtr<FString>> DisplayStrings; // 現在選択されている値 FName Key; public: SLATE_BEGIN_ARGS( SCustomGraphPinBase ) {} SLATE_END_ARGS() public: void Construct( const FArguments& InArgs, UEdGraphPin* InGraphPinObj ) { SGraphPin::Construct( SGraphPin::FArguments(), InGraphPinObj ); } // デフォルト値の取得 virtual TSharedRef<SWidget> GetDefaultValueWidget() override; // 値が変更された void OnValueChanged( TSharedPtr<FString> ItemSelected, ESelectInfo::Type SelectInfo ); protected: virtual void SetDisplayStrings() = 0; private: void SetValue( FName InKey ); }; // キャラクターステータス用 class TESTED_API SCharacerStatusGraphPin : public SCustomGraphPinBase { protected: virtual void SetDisplayStrings() override { // 詳細パネル用にも同じ処理を使いたいので文字列の一覧取得は処理を別にしていく CustomDisplayList::GetCharacterStatusDisplayStrings( DisplayStrings ); } }; class FCustomGraphPinFactory : public FGraphPanelPinFactory { // 登録したオブジェクトなら自作ピンを返す virtual TSharedPtr<class SGraphPin> CreatePin( class UEdGraphPin* InPin ) const override { const UEdGraphSchema_K2* K2Schema = GetDefault<UEdGraphSchema_K2>(); if( InPin->PinType.PinCategory == UEdGraphSchema_K2::PC_Struct ) { if( InPin->PinType.PinSubCategoryObject == FCharacterStatusList::StaticStruct() ) return SNew( SCharacerStatusGraphPin, InPin ); // 以下同じ様にここに処理を書く //else if( InPin->PinType.PinSubCategoryObject == XXXXX::StaticStruct() ) // return SNew( XXXXXX, InPin ); } return nullptr; } }; |
新規作成したCustomGraphPin.cppの中身です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90
|
// Fill out your copyright notice in the Description page of Project Settings. #include "CustomGraphPin.h" #include "KismetEditorUtilities.h" #include "STextComboBox.h" TSharedRef<SWidget> SCustomGraphPinBase::GetDefaultValueWidget() { DisplayStrings.Add( MakeShareable<FString>( new FString( TEXT( "None" ) ) ) ); // 派生先で文字列の一覧を取得してくる SetDisplayStrings(); int Index = 0; FString CurrentDefault = GraphPinObj->GetDefaultAsString(); if( CurrentDefault.Len() > 0 ) { int32 StartIndex = 5; int32 EndIndex; CurrentDefault.FindLastChar( ')', EndIndex ); FString DefaultValString = CurrentDefault.Mid( StartIndex, EndIndex - StartIndex ); // 設定されていた値からインデックスの検索 bool Found = false; for( int32 i = 0; i < DisplayStrings.Num(); ++i ) { if( DisplayStrings[i]->Equals( DefaultValString ) ) { Index = i; Key = FName( *DefaultValString ); Found = true; break; } } if( !Found ) { FString TempString = *(DisplayStrings[0]); Key = FName( *TempString ); UE_LOG( LogTemp, Error, TEXT( "SCustomGraphPinBase: %s 設定していた文字列が一覧から見つかりませんでした。再指定して下さい" ), *DefaultValString ); } } else { FString TempString = *(DisplayStrings[0]); Key = FName( *TempString ); } // リストになるようなウィジェットを生成する return SNew( SHorizontalBox ) .Visibility( this, &SGraphPin::GetDefaultValueVisibility ) + SHorizontalBox::Slot() .HAlign( HAlign_Left ) [ SNew( STextComboBox ) .OptionsSource( &DisplayStrings ) .OnSelectionChanged( this, &SCustomGraphPinBase::OnValueChanged ) .InitiallySelectedItem( DisplayStrings[Index] ) ]; } void SCustomGraphPinBase::OnValueChanged( TSharedPtr<FString> ItemSelected, ESelectInfo::Type SelectInfo ) { if( ItemSelected.IsValid() ) { if( DisplayStrings.Find( ItemSelected ) ) { Key = FName( *(*ItemSelected) ); SetValue( Key ); } else { UE_LOG( LogTemp, Error, TEXT( "CustomGraphPinBase:%s 値変更時に選択した名前が一覧から見つかりませんでした" ), *(*ItemSelected) ); } } } void SCustomGraphPinBase::SetValue( FName InKey ) { FString strKey = InKey.ToString(); FString KeyString = TEXT( "(" ); KeyString += TEXT( "Key=" ); KeyString += strKey; KeyString += TEXT( ")" ); GraphPinObj->GetSchema()->TrySetDefaultValue( *GraphPinObj, KeyString ); } |
5.データテーブルから文字列を列挙する処理の追加
今回の肝となる部分です。プロジェクト設定から指定したデータテーブルアセットを参照し、Rowデータを渡す処理を実装します。この処理は自作ピンの他、詳細パネルの表示部分でも使用するため、別クラスに分けるようにします。もちろん、別途データテーブルを増やしたい場合は、ここに関数を追加していくことになります。
新規作成したCustomDisplayList.hの中身です。
|
// Fill out your copyright notice in the Description page of Project Settings. #pragma once #include "CoreMinimal.h" /** * */ class TESTED_API CustomDisplayList { public: static void GetCharacterStatusDisplayStrings( TArray<TSharedPtr<FString>>& OutDisplayStrings ); }; |
新規作成したCustomDisplayList.cppの中身です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
|
// Fill out your copyright notice in the Description page of Project Settings. #include "CustomDisplayList.h" #include "TestSettings.h" #include "Engine/DataTable.h" void CustomDisplayList::GetCharacterStatusDisplayStrings( TArray<TSharedPtr<FString>>& OutDisplayStrings ) { // キャラクターステータスのデータテーブル読み込み FSoftObjectPath DataAssetRef = UTestSettings::StaticClass()->GetDefaultObject<UTestSettings>()->CharacterStatusAsset; UDataTable* CharacterStatusDT = Cast<UDataTable>( StaticLoadObject( UDataTable::StaticClass(), nullptr, *(DataAssetRef.ToString()) ) ); auto RowNames = CharacterStatusDT->GetRowNames(); for( auto& RowName : RowNames ) { OutDisplayStrings.Add( MakeShareable<FString>( new FString( RowName.ToString() ) ) ); } } |
6.詳細パネルの表示処理を追加
続いて詳細パネル側の表示処理を追加します。こちらも自作ピン同様エディタモジュール側の処理となります。そして自作ピンと同様、データテーブルを追加する場合はこちらもICustomDetailBaseを基底クラスとしてた別クラスを追加し、SetDisplayStrings関数の中身を変更しましょう。
新規作成したCustomDetail.hの中身です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
|
// Fill out your copyright notice in the Description page of Project Settings. #pragma once #include "CoreMinimal.h" #include "DetailCustomizations.h" #include "IPropertyTypeCustomization.h" #include "CustomDisplayList.h" /** * */ class TESTED_API ICustomDetailBase : public IPropertyTypeCustomization { public: /** IPropertyTypeCustomization interface */ virtual void CustomizeHeader( TSharedRef<class IPropertyHandle> inStructPropertyHandle, class FDetailWidgetRow& HeaderRow, IPropertyTypeCustomizationUtils& StructCustomizationUtils ) override; virtual void CustomizeChildren( TSharedRef<class IPropertyHandle> inStructPropertyHandle, class IDetailChildrenBuilder& StructBuilder, IPropertyTypeCustomizationUtils& StructCustomizationUtils ) override; private: TSharedPtr<IPropertyHandle> StructPropertyHandle; TSharedPtr<IPropertyHandle> KeyHandle; TSharedPtr<STextComboBox> KeyComboBox; void OnStateValueChanged( TSharedPtr<FString> ItemSelected, ESelectInfo::Type SelectInfo ); void OnStateListOpened(); void OnCheckStateChanged( ECheckBoxState CheckState ); TSharedPtr<class IPropertyUtilities> PropertyUtilities; protected: TArray<TSharedPtr<FString>> DisplayStrings; virtual void SetDisplayStrings() = 0; }; // キャラクターステータス用 class TESTED_API ICharacterStatusDetail : public ICustomDetailBase { public: static TSharedRef<IPropertyTypeCustomization> MakeInstance() { return MakeShareable( new ICharacterStatusDetail() ); } protected: virtual void SetDisplayStrings() override { // ピンと同じ処理を使う CustomDisplayList::GetCharacterStatusDisplayStrings( DisplayStrings ); } }; |
新規作成したCustomDetail.cppの中身です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91
|
// Fill out your copyright notice in the Description page of Project Settings. #include "CustomDetail.h" #include "STextComboBox.h" #include "SCheckBox.h" #include "PropertyHandle.h" #include "DetailWidgetRow.h" #include "IPropertyUtilities.h" void ICustomDetailBase::CustomizeHeader( TSharedRef<class IPropertyHandle> inStructPropertyHandle, class FDetailWidgetRow& HeaderRow, IPropertyTypeCustomizationUtils& StructCustomizationUtils ) { StructPropertyHandle = inStructPropertyHandle; DisplayStrings.Add( MakeShareable<FString>( new FString( "None" ) ) ); // 派生先で文字列の一覧を取得してくる SetDisplayStrings(); uint32 NumChildren; StructPropertyHandle->GetNumChildren( NumChildren ); FName Key; for( uint32 ChildIndex = 0; ChildIndex < NumChildren; ++ChildIndex ) { const TSharedPtr< IPropertyHandle > ChildHandle = StructPropertyHandle->GetChildHandle( ChildIndex ); if( ChildHandle->GetProperty()->GetName() == TEXT( "Key" ) ) { KeyHandle = ChildHandle; ChildHandle->GetValue( Key ); } } check( KeyHandle.IsValid() ); // 取得してきたKeyがリストの中にあるかチェック int Index = 0; bool Found = false; for( int32 i = 0; i < DisplayStrings.Num(); ++i ) { if( DisplayStrings[i]->Equals( Key.ToString() ) ) { Index = i; Found = true; break; } } if( !Found ) { Key = TEXT( "None" ); KeyHandle->SetValue( Key ); UE_LOG( LogTemp, Error, TEXT( "ICustomDetailBase: %s 設定していた文字列が一覧から見つかりませんでした。再指定して下さい" ), *Key.ToString() ); } HeaderRow .NameContent() [ StructPropertyHandle->CreatePropertyNameWidget() ] .ValueContent() .MinDesiredWidth( 500 ) [ SNew( SHorizontalBox ) + SHorizontalBox::Slot() .HAlign( HAlign_Left ) [ SAssignNew( KeyComboBox, STextComboBox ) .OptionsSource( &DisplayStrings ) .OnSelectionChanged( this, &ICustomDetailBase::OnStateValueChanged ) .InitiallySelectedItem( DisplayStrings[Index] ) ] ]; } void ICustomDetailBase::CustomizeChildren( TSharedRef<class IPropertyHandle> inStructPropertyHandle, class IDetailChildrenBuilder& StructBuilder, IPropertyTypeCustomizationUtils& StructCustomizationUtils ) { } void ICustomDetailBase::OnStateValueChanged( TSharedPtr<FString> ItemSelected, ESelectInfo::Type SelectInfo ) { if( ItemSelected.IsValid() ) { if( DisplayStrings.Find( ItemSelected ) ) { KeyHandle->SetValue( FName( **ItemSelected ) ); } } } void ICustomDetailBase::OnStateListOpened() { } |
7.エディタモジュールの編集
最後に、モジュール登録時にこれらのピンや詳細パネルが動くように登録します。自作ピンに関してはファクトリークラスを1つだけ登録すれば完了ですが、詳細パネルの方はデータテーブルを増やすたびにこちらに処理を追加して行くことにご注意下さい。
TestEd.cppの中身です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
|
// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. #include "TestEd.h" #include "Modules/ModuleManager.h" #include "Blueprint/CustomGraphPin.h" #include "Blueprint/CustomDetail.h" #include "PropertyEditorModule.h" #define LOCTEXT_NAMESPACE "TEST" class FTestEdModule : public FDefaultGameModuleImpl { public: virtual void StartupModule() override { RegisterSettings(); } virtual void ShutdownModule() override { UnregisterSettings(); } protected: /** Register settings objects. */ void RegisterSettings() { TSharedPtr<FCustomGraphPinFactory> CGraphPinFactory = MakeShareable( new FCustomGraphPinFactory() ); FEdGraphUtilities::RegisterVisualPinFactory( CGraphPinFactory ); FPropertyEditorModule& PropertyModule = FModuleManager::LoadModuleChecked<FPropertyEditorModule>( "PropertyEditor" ); PropertyModule.RegisterCustomPropertyTypeLayout( "CharacterStatusList", FOnGetPropertyTypeCustomizationInstance::CreateStatic( &ICharacterStatusDetail::MakeInstance ) ); // 以下同じ様にここに処理を書く //PropertyModule.RegisterCustomPropertyTypeLayout( "XXXXX", FOnGetPropertyTypeCustomizationInstance::CreateStatic( &XXXXXX::MakeInstance ) ); } /** Unregister settings objects. */ void UnregisterSettings() { FPropertyEditorModule& PropertyModule = FModuleManager::LoadModuleChecked<FPropertyEditorModule>( "PropertyEditor" ); PropertyModule.UnregisterCustomPropertyTypeLayout( "CharacterStatusList" ); // 以下同じ様にここに処理を書く //PropertyModule.UnregisterCustomPropertyTypeLayout( "XXXXX" ); } }; IMPLEMENT_GAME_MODULE( FTestEdModule, TestEd ); #undef LOCTEXT_NAMESPACE |
使い方
以上で、実装方法は完了しました。続いて実際の使用方法ですが、まず引数がName型だったものを今回新しく用意した構造体に変更します。

このままでは構造体なので中身をバラしてKeyを取り出します。この中身がデータテーブルから取得してきた文字列になります。

もちろんただの構造体なので右クリックの「Split Struct Pin」で簡潔にすることも可能です。

最後に
さて、これでわざわざ文字列を手入力することなく、データテーブルのデータを簡単に取得できるようになりました。ですがこの機能には少し問題があります。それはデータテーブルの種類が増えた時、これらの追加対応がちょっと面倒な所です。もう少し自動化できるとよいのですが、その方法は只今模索中です。なにか思いつけば後日ご紹介したいと思います。