WorldPartition UE5.0からUE5.1に移行する前にすること

やあ

WorldPartitionを使っているレベルがあるプロジェクトをUE5.0からUE5.1に移行したら、DataLayerがアセットになってて、レベルがぐちゃぐちゃになっちゃった そんなときに見るメモ

やること

  1. UE5.1をインストールする
  2. プロジェクトをリビルドする
  3. コマンドプロンプトを開く (Ctrl+R → cmd → Enter)
  4. コマンドプロンプトに下記の内容を入力し実行
# UnrealEditor.exeのあるディレクトリに移動
cd C:\Program Files\Epic Games\UE_5.1\Engine\Binaries\Win64

# コマンド
UnrealEditor.exe "プロジェクトのパス" -run=DataLayerToAssetCommandlet WorldPartitionを使っているレベルのパス -DestinationFolder=/Game/DataLayers -IgnoreActorLoadingErrors

プロジェクトのパス例
C:\study\HogeProject\HogeProject.uproject

WorldPartitionを使っているレベルのパス
C:\study\HogeProject\Content\Map\HogeMap_WP.umap

おわり

UE5 Fractureがうまく動かない!?

やあ

Unreal Engine 5に移行してから、PhysXが非推奨になったことで、Apex Destructionが使えなくなちゃった!
ってことで、いろんなアセットを FractureGeometry Collectionに置き換えてるときに、一部のメッシュだけ、空のGeometry Collectionができることがあった。
そんな時に見るメモ。

問題のないメッシュ

問題のメッシュ

解決策

Fractureを使いたいメッシュのNaniteを無効にする!


おしまい!

Chaos Destructionを使ったあとに、ソースメッシュのNaniteを有効にしても大丈夫だよ。

ちなみにGeometry Collectionクラスのアセット内にはEnable Naniteというパラメーターがある。

これを有効にすると、メッシュが消えて空のGeometry Collectionが残る。
???
使い方が悪いんだろうけど ?????

UE4 Blenderから持ってきたアニメーションの骨の位置がおかしくなる問題

やあ

Blenderで作った特定のアニメーションを、UE4に持って行ったら、ルートの位置に腰が来てしまう問題が起きたんだ。
その原因と解決策がわかったっぽいのでメモとして残しておくよ。

環境

UE4.26.2
・Blender2.91.2

実際に起こっている問題

f:id:pto8913:20211209114755p:plain 左がアニメーションを設定した状態
右がデフォルト

原因1?

Blender上でアーマチュアのスケールを変更していたこと。
(1, 1, 1)じゃないとだめなところを、(0.5, 0.5, 0.5)みたいにしていた。

実際に、スケーリングしたキャラクターとそうでないキャラクターで比較してみたところこうなった。
f:id:pto8913:20211209120541p:plain

スケールが原因だー!と思い、
1. Ctrl+Aでスケールを適用
2. アニメーションカーブのY座標もすべて0.5倍
アーマチュアの大きさをY座標に掛けるとアニメーションが壊れない blender.stackexchange.com
にして、再びUE4にインポートすると・・・

直りませんでした\(^o^)/
f:id:pto8913:20211209120603p:plain

原因2?

UE4のスケルトンのTranslation Retargetingに原因がありそうかも~

自分のプロジェクトでは、ほぼすべてのメッシュが、一つのスケルトンを共有しているので、Translation Retargetingをここに載っているように設定していた。

docs.unrealengine.com

f:id:pto8913:20211209120712p:plain

解決策?(これで解決になるプロジェクトもあると思います)
ここを変えれば直るんじゃねと思って、Pelvisの位置にあたる骨をAnimation ScaledからAnimationにすると。

f:id:pto8913:20211209120747p:plain
直った!

けど、これって他のアセットで問題が起こりそうだよな~ってことで、この解決策?はいったん保留。

解決策(自分の場合)

問題が起こっているアニメーションをエクスポートするのを、スケーリングしていないアーマチュアからやればいいじゃん!

手順
1. アーマチュアを複製し、複製したほうのアーマチュアのスケールを(1, 1, 1)に戻す※スケールの適用ではないです
2. 複製してスケールを(1, 1, 1)にしたほうのアーマチュアからすべてのアニメーションをエクスポート
3. スケーリングしているほう((0.5, 0.5, 0.5)など )のアーマチュアで、メッシュのみをエクスポート
4. UE4でそれぞれをインポートして使うだけ!

結果
f:id:pto8913:20211209120853p:plain

ちなみに、Translation RetargetingAnimationにした状態だと
f:id:pto8913:20211209120931p:plain
体が浮いてしまう

最後に

こんなことにならないようにアーマチュアを拡大縮小するのはやめよう!

他にも全部作り直すって手もあるよ()

・スケールを適用して、アニメーションカーブのY座標もスケーリングしたのに、UE4では直らないところ。
Translation Retargetingを変更すれば直ったけど、別の問題が起きそうだし・・・

UE4 PhysicsConstraintを使用したStaticMeshのSimulatePhysicsを切り替えると位置が戻る

やあ

プレイヤーが押すと特定方向に動くアクターを作ってたら、 SimulatePhysicsを切り替えるとStaticMeshの位置が戻ってしまう現象が起きたんだ。

環境

UE4.26.2

問題が起こってた部分

f:id:pto8913:20210913001249p:plain
SimulatePhysicsを有効にした後に、プレイヤーがアクターを押す方向によって、PhysicsConstraintの位置制限を変えようとしてる。

解決

f:id:pto8913:20210913001550p:plain
SimulatePhysicsを有効にする前に位置制限を変えるようにした。

位置が戻ってしまうのは、実行時に物理シミュレーションによって、SimulatePhysicsが有効になっているオブジェクトが所有されるかららしいです。

answers.unrealengine.com

UE4 WorldCompositionのサブレベルでMoveToを使ったら動かなかった話

やあ

World Compositionのサブレベルで、敵キャラクターを巡回させるためにMoveToを使ったら、全然移動しなくて1時間くらい悩んでたから、考えられる原因とその解決策を残しておくよ。

環境

UE4.26.2

考えられる原因

Navimesh Bounds Volumeを配置してない
・キャラクターをNavimesh Bounds Volumeの外側に配置している
・移動先が設定できてない
Behavior Treeが動いてない
Move Toまで処理が届いてない
・キャラクターの移動速度が0になっている

全部確認したのに動かない;;
なんでなんだー

解決策

Navimesh Bounds Volumeをサブレベルから、パーシスタントレベルに移動しただけ

動いたよ。やったー!

スプライン(移動先のアクタや位置)や敵キャラクターはサブレベルに置いたままで動きました。

World Compositionはこういった、サブレベルに置くことで動かなかったりするものがあるので、そのうちまとめようと思います。

おまけ

・アクターがどのレベルに配置されてるかは、ビューポートの右下に表示されてるよ。
f:id:pto8913:20210823175737p:plain

Navimesh Bounds Volumeが正しく配置されているかは、Pキーを押したら床が緑に表示されるかどうかでわかるよ。
f:id:pto8913:20210823175325p:plain

UE4 C++ GameplayAbilitiesのAttributeSetをJsonに書き込んだり読み込んだり

やあ

ゲームのセーブ、ロードの実装中にAttributeSetをJsonに保存したいなーって思って調べたよ。

前置き

Json関連のものを使えるようにするためにモジュールを追加する。

PublicDependencyModuleNames.AddRange(new string[] { 
    "Json", "JsonUtilities"
});

Jsonの読み書きに使うヘッダー。

/* 必須 */
#include "Json.h"
/* ファイルの読み込みや書き込みに使う */
#include "Misc/FileHelper.h"
/* UStructからFJsonObjectに変えたりするときに使う */
#include "JsonUtilities/Public/JsonObjectConverter.h"

FJsonObject
名前と値を保持してくれる。

TSharedPtr<FJsonObject> JsonObj = MakeShareable(new FJsonObject);

例えば、キャラクターの体力を保持したい場合。

JsonObj->SetNumberField(”Health”, 60);

この時、中身は

{
    "Health" : 60
}

になる。

ネストしたい場合。
例、キャラクターのIDの中に、体力の情報を入れる。

TSharedPtr<FJsonObject> RootJsonObj = MakeShareable(new FJsonObject);
TSharedPtr<FJsonObject> ElementJsonObj = MakeShareable(new FJsonObject);
ElementJsonObj->SetNumberField("Health", 60);

RootJsonObj->SetObjectField("CharacterID", ElementJsonObj);

この時、中身は

{
    "CharacterID" : {
        "Health" : 60
    }
}

になる。

作ったFJsonObjectを保存する。

FString OutPutString;
/* OutPutStringに書き込むように指定する */
const TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&OutPutString);
/* JsonObjに保持している名前と値をWriterに書き込む */
FJsonSerializer::Serialize(JsonObj.ToSharedRef(), Writer);
/* Project/Saved/ に保存する */
FFileHelper::SaveStringToFile(OutPutString, *(FPaths::ProjectSavedDir() + "Filename.json"));

Jsonファイルを読み込む。

FString LoadJsonStringData;
/* LoadJsonStringDataにJsonの中身を読み込む */
if (FFileHelper::LoadFileToString(LoadJsonStringData, "FileFullPath")
{
    TSharedPtr<FJsonObject> LoadedJsonObj = MakeShareable(new FJsonObject);
    /* Jsonの読み込み先をLoadJsonStringDataにする */
    const TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(LoadJsonStringData);
    /* ReaderからLoadJsonObjに */
    if (FJsonSerializer::Deserialize(Reader, LoadJsonObj))
    {
        TSharedPtr<FJsonObject> CharacterData = LoadJsonObj->GetObjectField("CharacterID");
        float Health = CharacterData->GetNumberField("Health");
    }
}

ここまで理解できればすぐにできるよ。

やる

こんな感じの構造を想定してるよ。

{
    "CharacterA": [
        {
            "Health":
            {
                "baseValue": 100,
                "currentValue": 100
            }
        },
        {
            "MaxHealth":
            {
                "baseValue": 100,
                "currentValue": 100
            }
        }
    ],
    "CharacterB": [
        {
            "Health":
            {
    // 以下省略
}

Jsonに書き込む

#include "Json.h"
#include "Misc/FileHelper.h"
#include "JsonUtilities/Public/JsonObjectConverter.h"
#include "Kismet/GameplayStatics.h"

void UHogeSaveGame::BeginSave(const UObject* _WorldContextObject)
{
    TArray<AActor*> OutActors;
    UGameplayStatics::GetAllActorsOfClass(_WorldContextObject, AHogeCharacter::StaticClass(), OutActors);

    TSharedPtr<FJsonObject> SaveJsonObj = MakeShareable(new FJsonObject());
    for (AActor* Actor : OutActors)
    {
        AHogeCharacter* Character = Cast<AHogeCharacter>(Actor);
        if (Character)
        {
            SaveAttributeSet(Character, SaveJsonObj);
        }
    }
    FString OutPutString;
    const TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&OutPutString);
    FJsonSerializer::Serialize(SaveJsonObj.ToSharedRef(), Writer);
    FFileHelper::SaveStringToFile(OutPutString, *FileFullPath);
}

void UHogeSaveGame::SaveAttributeSet(AHogeCharacter* InTarget, TSharedPtr<FJsonObject>& SaveJsonObj)
{
    if (!InTarget) return;
    /* 念のため、AbilitySystemComponentを持っているかを調べる */
    if (!InTarget->GetAbilitySystemComponent()) return;

    if (!InTarget->HogeAttributeSet) return;

    UHogeAttributeSet* HogeAttributeSet = InTarget->HogeAttributeSet;
    if (!HogeAttributeSet) return;

    /* AttributeSetの中身をひとまとめにするための配列 */
    TArray<TSharedPtr<FJsonValue>> Values;
    for (TFieldIterator<FProperty> It(HogeAttributeSet->GetClass(), EFieldIteratorFlags::IncludeSuper); It; ++It)
    {
        FProperty* Property = *It;
        FStructProperty* StructProperty = CastField<FStructProperty>(Property);
        if (!StructProperty) continue;
        /* StructPropertyをHogeAttributeSetからFGameplayAttributeDataにする */
        FGameplayAttributeData* DataPtr = StructProperty->ContainerPtrToValuePtr<FGameplayAttributeData>(HogeAttributeSet);
        if (!DataPtr) continue;
        /* FGameplayAttributeDataからFJsonObject型に変換する */
        TSharedPtr<FJsonObject> Obj = FJsonObjectConverter::UStructToJsonObject<FGameplayAttributeData>(*DataPtr);
        if (!Obj) continue;

        /* プロパティに値に名前を付けるためのJsonObj */
        TSharedPtr<FJsonObject> JsonObj_ForPropertyName = MakeShareable(new FJsonObject());
        /* JsonObj_ForPropertyNameの中身は
        {
           "Health" : {
               "baseValue" : 100,
               "currentValue" : 100
           }
        }
        */
        JsonObj_ForPropertyName->SetObjectField(Property->GetName(), Obj);
        /* JsonObjectをJsonValueに変換する */
        TSharedRef<FJsonValueObject> JsonValue = MakeShareable(new FJsonValueObject(JsonObj_ForPropertyName));
        /* プロパティを配列に追加 */
        Values.Add(JsonValue);
    }
    /* CharacterIDにAttributeSetの中身を保持する */
    SaveJsonObj->SetArrayField(”CharacterID”, Values);
    }
}

これでAttributeSetの中身をJsonに保存することができたよ。

Jsonから読み込み

void UCharacterSaveGame::LoadAttributeSet(AHogeCharacter* InTarget)
{
    FString LoadJsonStringData;
    if (FFileHelper::LoadFileToString(LoadJsonStringData, *FileFullPath))
    {
        TSharedPtr<FJsonObject> LoadJsonObj = MakeShareable(new FJsonObject());
        const TSharedRef<TJsonReader<>> JsonReader = TJsonReaderFactory<>::Create(LoadJsonStringData);
        if (FJsonSerializer::Deserialize(JsonReader, LoadJsonObj))
        {
            UHogeAttributeSet* HogeAttributeSet = InTarget->HogeAttributeSet;
            
            TArray<TSharedPtr<FJsonValue>> Values = LoadJsonObj->GetArrayField(CharacterID);
            TMap<FString, TSharedPtr<FJsonValue>> ValueWithPropertyName;
            /* CharacterIDごとにAttributeSetのプロパティをLoadJsonObjから読み込んでおく */
            for (TSharedPtr<FJsonValue>& Value : Values)
            {
                ValueWithPropertyName.Append(Value->AsObject()->Values);
            }
            for (TFieldIterator<FProperty> It(HogeAttributeSet->GetClass(), EFieldIteratorFlags::IncludeSuper); It; ++It)
            {
                FProperty* Property = *It;
                FStructProperty* StructProperty = CastField<FStructProperty>(Property);
                if (!StructProperty) continue;

                FGameplayAttributeData* DataPtr = StructProperty->ContainerPtrToValuePtr<FGameplayAttributeData>(MainStatusAttributeSet);
                if (!DataPtr) continue;

                TSharedPtr<FJsonValue>* PropertyValue = ValueWithPropertyName.Find(Property->GetName());
                if (!PropertyValue) continue;

                FGameplayAttributeData OutData;
                /* こんなデータをFGameplayAttributeDataに読み込む
               {
                   "Health" : {
                       "baseValue" : 100,
                       "currentValue" : 100
                   }
               }
                */
                TSharedPtr<FJsonObject> AttributeDataObj = (*PropertyValue)->AsObject();
                FJsonObjectConverter::JsonObjectToUStruct<FGameplayAttributeData>(AttributeDataObj.ToSharedRef(), &OutData, false, false);
                 /* HogeAttributeSetに値を設定する。 */
                DataPtr->SetBaseValue(OutData.GetBaseValue());
                DataPtr->SetCurrentValue(OutData.GetCurrentValue());
                 /* 注意 : 更新イベントは呼ばれないので手動で呼び出す必要があるよ */
            }
        }
    }
}

これでJsonからAttributeSetに値を読み込むことができるよ。

今回は終わり。
書いていて思ったんだけど、スタンドアローンならJsonに書き込む必要あんまりない?

UE4 C++からアセットを作る

やあ

データテーブルから大量のデータアセットを作ろうと思ったときに、面倒だったから自動化したいなーって思って調べてみたよ。

環境

UE4.26.0

準備

C++の処理をエディタから呼び出すためにEditorUtilityWidgetを使うよ。

docs.unrealengine.com

まず、Hoge.Build.csにモジュールを追加。

// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved.

using UnrealBuildTool;

public class Hoge : ModuleRules
{
    public Hoge(ReadOnlyTargetRules Target) : base(Target)
    {
        PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;

        PublicDependencyModuleNames.AddRange(new string[] { 
            "Core", "CoreUObject", "Engine", "InputCore",
            "AssetTools",
        });

        PrivateDependencyModuleNames.AddRange(new string[] { 
            "Slate", "SlateCore", "GraphEditor", "UnrealEd", "BlueprintGraph", 
            "EditorScriptingUtilities", "UMG",
        });
    }
}

追加したら、Hoge.uprojectを右クリックして、Generate Visual Studio project filesする。
f:id:pto8913:20210512232715p:plain

やる

今回はボタンを押すと、DataAssetクラスが作られるようにするよ。

新しいDataAssetFactoryクラス

まず最初に、DataAssetクラスをコンテンツブラウザから作ろうとすると、 f:id:pto8913:20210513113748p:plain
こんなのがでていちいち選択するのはめんどうなので、新しくDataAssetFactoryクラスを継承したC++クラスMyDataAssetFactoryを作ります。
f:id:pto8913:20210513114045p:plain

MyDataAssetFactory.hに追加

// Copyright(C)write by pto8913. 2020. All Rights Reserved.

#pragma once

#include "CoreMinimal.h"
#include "Factories/DataAssetFactory.h"
#include "MyDataAssetFactory.generated.h"

/* エディタに表示したくなかったのでabstractを使ったんだけど、正しいのかな? */
UCLASS(abstract)
class HOGE_API UMyAssetFactory : public UDataAssetFactory
{
    GENERATED_BODY()

    // UFactory interface
    virtual bool ConfigureProperties() override;
    virtual UObject* FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override;
    // End of UFactory interface
};

・UCLASS(abstract)を外すと、コンテンツブラウザを右クリックしてデータアセットを作ろうとしたときに、データアセットが二つ表示されるます。 f:id:pto8913:20210513115134p:plain

MyDataAssetFactory.cppに追加

// Copyright(C)write by pto8913. 2020. All Rights Reserved.

#include "MyDataAssetFactory.h"

#if WITH_EDITOR
#include "Misc/MessageDialog.h"
#endif

#define LOCTEXT_NAMESPACE "My EditorFactories"

bool UMyDataAssetFactory::ConfigureProperties()
{
    if (DataAssetClass == nullptr)
    {
        const FText TitleText = LOCTEXT("Title", "WarningMessage");
        FMessageDialog::Open(EAppMsgType::Ok, LOCTEXT("Message", "DataAssetClass is null"), &TitleText);
        return false;
    }
    return true;
}

UObject* UMyDataAssetFactory::FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn)
{
    if (DataAssetClass != nullptr)
    {
        return NewObject<UDataAsset>(InParent, DataAssetClass, Name, Flags | RF_Transactional);
    }
    else
    {
        // if we have no data asset class, use the passed-in class instead
        check(Class->IsChildOf(UDataAsset::StaticClass()));
        return NewObject<UDataAsset>(InParent, Class, Name, Flags);
    }
}

#undef LOCTEXT_NAMESPACE

親クラスのDataAssetFactoryでは、DataAssetClassをnullptrにして、人に選択させようとするので、それを全部消して、外部からDataAssetClassを設定するようにするよ。

EditorUtilityWidgetクラスを作る

エディタを開いたら、EditorUtilityWidgetを継承したC++クラスTestEditorUtilityWidgetを追加。
f:id:pto8913:20210512233033p:plain

TestEditorUtilityWidget.hに追加

// Copyright(C)write by pto8913. 2020. All Rights Reserved.

#pragma once

#include "CoreMinimal.h"
#include "EditorUtilityWidget.h"
#include "TestEditorUtilityWidget.generated.h"

class UButton;
class UDataAsset;

UCLASS(BlueprintType)
class HOGE_API TestEditorUtilityWidget : public UEditorUtilityWidget
{
    GENERATED_BODY()
public:
    virtual void NativeConstruct() override;
    virtual void NativeDestruct() override;

    UPROPERTY(BlueprintReadOnly, meta = (BindWidget))
        UButton* StartButton;
    UFUNCTION()
        void ClickedStart();

    UPROPERTY(EditAnywhere, BlueprintReadOnly)
        TSubclassOf<UDataAsset> AssetClass;

    UPROPERTY(EditAnywhere, BlueprintReadOnly)
        FName AssetName;
};

TSubclassOf<UDataAsset>には作りたいアセットのクラスを設定。
ここで設定したものをさっき作った、MyDataAssetFactoryDataAssetClassに設定してやることで、そのクラスのアセットを作る。
StartButtonmeta = (BindWidget)することで、ウィジェットブループリントに配置した、StartButtonという名前のButtonウィジェットにアクセスできるようになるよ。
便利だからぜひ覚えてね。

TestEditorUtilityWidget.cppに追加

// Copyright(C)write by pto8913. 2020. All Rights Reserved.

#include "TestEditorUtilityWidget.h"

#include "Components/Button.h"

#include "MyDataAssetFactory.h"

#include "EditorAssetLibrary.h"
#include "IAssetTools.h"
#include "AssetToolsModule.h"
#include "Modules/ModuleManager.h"

void UTestEditorUtilityWidget::NativeConstruct()
{
    if (StartButton != nullptr)
    {
        if (!StartButton->OnClicked.IsBound())
        {
            StartButton->OnClicked.AddDynamic(this, &UTestEditorUtilityWidget::ClickedStart);
        }
    }
    
    Super::NativeConstruct();
}

void UTestEditorUtilityWidget::NativeDestruct()
{
    if (StartButton != nullptr)
    {
        if (!StartButton->OnClicked.IsBound())
        {
            StartButton->OnClicked.RemoveDynamic(this, &UTestEditorUtilityWidget::ClickedStart);
        }
    }

    Super::NativeDestruct();
}

#define LOCTEXT_NAMESPACE "pto test"

void UTestEditorUtilityWidget::ClickedStart()
{
    /* アセットのクラスが設定されてなかったらダイアログを表示する */
    if (AssetClass == nullptr)
    {
        const FText TitleText = LOCTEXT("Title", "WarningMessage");
        FMessageDialog::Open(EAppMsgType::Ok, LOCTEXT("Message", "AssetClass is null"), &TitleText);
        return;
    }

    IAssetTools& AssetTool = FModuleManager::LoadModuleChecked<FAssetToolsModule>("AssetTools").Get();
    /*
       さっき作ったMyDataAssetFactoryを作る。
   */
    UMyDataAssetFactory* Factory = NewObject<MyDataAssetFactory>();
    Factory->DataAssetClass = DataAssetClass;

    /* 作ったアセットを設置したいフォルダのパス */
    FString DirPath = "/Game/test/";
    /* フォルダが存在しなかったらフォルダを作る */
    if (!UEditorAssetLibrary::DoesDirectoryExist(DirPath))
    {
        UEditorAssetLibrary::MakeDirectory(DirPath);
    }

    UObject* NewAsset = AssetTool.CreateAssetWithDialog(
        AssetName.ToString(), DirPath, AssetClass, Factory
    );

}
#undef LOCTEXT_NAMESPACE

・作りたいアセットによって、使うFactoryクラスが違うので注意。
Engine\Source\Editor\UnrealEd\Private\Factories\EditorFactories.cppで色々見れるよ。

これで、おーけー。

コンパイルしてね。

コンパイルが終わったら、EditorUtilityWidgetBlueprintを作る。
f:id:pto8913:20210513001800p:plain
EBP_CreateAssetTestと呼ぶよ。

EBP_CreateAssetTestを開いて、クラス設定を編集を押して、親クラスをさっき作った、TestEditorUtilityWidgetにする。
f:id:pto8913:20210513002124p:plain

すると、StartButtonがないぞって怒られるので、StartButtonを追加。
f:id:pto8913:20210513002253p:plain

ウィジェットからアセットのクラス、アセットの名前を設定するためにちょっと改造。
SinglePropertyViewを二つ追加して。
それぞれにプロパティの名前、AssetClass, AssetNameを設定。
f:id:pto8913:20210513003752p:plain
コンストラクタでSinglePropertyViewが参照するオブジェクトを設定する。
f:id:pto8913:20210513003904p:plain

これでおーけー。
EBP_CreateAssetTestを右クリックして、実行。
f:id:pto8913:20210513003938p:plain
するとこんな画面が出るので、
f:id:pto8913:20210513004030p:plain
クラスと名前を設定してボタンをクリック。
f:id:pto8913:20210513004144p:plain

できました。やったー!・。・!

これで終わりです。

おまけ

BP版

かきかけ BPから作る場合も後で追記するよ。(完全にBPだけではなくて、ちょっとだけBlueprintFunctionLibraryを継承したクラスを使ってノードを追加するよ。)