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を継承したクラスを使ってノードを追加するよ。)