rarilog

主にゲーム制作に関することを書いていきます

2015/12/03 : UnrealEngine

【2015/12/03 - 2015/12/03】 ボタンを激しく連打するとD言語くんが大暴れする UE4 エディタ拡張

はい。

この記事は、D言語くん Advent Calendar 2015 の 3 日目の記事です。

本当は今年作ったD言語くんの動画やイラストを並べるだけにしようと思っていたのですが、D言語くん Advent Calendar が Qiita に存在する以上はプログラムチックなことを書かねばならん気がした (というか中の人に若干釘刺された) ので、C++ で UE4 のエディタ拡張を書いてみました。

UE 4.9.2 を使用しています。

UE4 のエディタ拡張について

ゲームエンジンのエディタ拡張というと Unity のイメージが強いかと思いますが、UE4 でもできます。

Unreal Fest 2015 にてエディタ拡張に関する講演1があり、YouTube に動画が公開されているので、エディタ拡張でどのようなことができるのか興味がある人は、見ることをオススメします。

上記の動画では、詳細パネル上で Spline を編集できるエディタ拡張について解説されています。グリッドマップエディタも紹介されていますね。ウィジェットの描画部分を細かくいじれるようなので、ほぼ何でもできる気がします。

動画で紹介されたものも、今回自分が作ったものも、すべて「特定の Actor の詳細パネルに何かを追加する」タイプのエディタ拡張ですが、それだけではなく、独自のウィンドウを出すようなこともできます。

コード

ここからは、今回作成した『ボタンを激しく連打するとD言語くんが大暴れするエディタ拡張』のコードについて、簡単に解説していきます。

先ほど紹介した講演の資料 の p.102 以降でエディタコードモジュールの作成方法について解説されているので、それと合わせて読めば、何となくやっていることがわかるはずです。

UE4 プロジェクト DManProject のディレクトリ構造は以下のようになっています。

  • DManProject
    • Content
      • Editor
        • DManImageA.png
        • DManImageB.png
    • Source
      • DManProject
        • DMan.cpp
        • DMan.h
      • DManProjectEditor
        • DManDetailsCustomization.cpp
        • DManDetailsCustomization.h
        • DManDetailsStyle.cpp
        • DManDetailsStyle.h
        • DManProjectEditor.Build.cs
        • DManProjectEditor.cpp
        • DManProjectEditor.h
      • DManProject.Target.cs
      • DManProjectEditor.Target.cs

Source / DManProject.Target.cs, DManProjectEditor.Target.cs

講演資料 p.104~106 のとおり、まずは Unreal Build System の Target File を修正します。


DManProject.Target.cs

using UnrealBuildTool;
using System.Collections.Generic;

public class DManProjectTarget : TargetRules
{
    public DManProjectTarget(TargetInfo Target)
    {
        Type = TargetType.Game;
    }

    public override void SetupBinaries(
        TargetInfo Target,
        ref List<UEBuildBinaryConfiguration> OutBuildBinaryConfigurations,
        ref List<string> OutExtraModuleNames
    )
    {
        OutExtraModuleNames.Add("DManProject");
        if(UEBuildConfiguration.bBuildEditor)             // エディタをビルドするときだけ
        {
            OutExtraModuleNames.Add("DManProjectEditor"); // 追加
        }
    }
}


DManProjectEditor.Target.cs

using UnrealBuildTool;
using System.Collections.Generic;

public class DManProjectEditorTarget : TargetRules
{
    public DManProjectEditorTarget(TargetInfo Target)
    {
        Type = TargetType.Editor;
    }

    public override void SetupBinaries(
        TargetInfo Target,
        ref List<UEBuildBinaryConfiguration> OutBuildBinaryConfigurations,
        ref List<string> OutExtraModuleNames
    )
    {
        OutExtraModuleNames.Add("DManProject");
        OutExtraModuleNames.Add("DManProjectEditor"); // 追加
    }
}

Source / DManProjectEditor / DManProjectEditor.Build.cs

モジュールの依存関係の設定を行います。


DManProjectEditor.Build.cs

using UnrealBuildTool;

public class DManProjectEditor : ModuleRules
{
    public DManProjectEditor(TargetInfo Target)
    {
        PublicDependencyModuleNames.AddRange(new string[] {"Core", "CoreUObject", "Engine", "InputCore", "DManProject"});
        PrivateDependencyModuleNames.AddRange(new string[] {"Slate", "SlateCore", "PropertyEditor"});
    }
}

DManProject.uproject

講演資料 p.110 のとおりに Modules を修正します。


DManProject.uproject

{
    "FileVersion": 3,
    "EngineAssociation": "4.9",
    "Category": "",
    "Description": "",
    "Modules": [
        {
            "Name": "DManProject",
            "Type": "Runtime",
            "LoadingPhase": "Default",
            "AdditionalDependencies": [
                "Engine"
            ]
        },
        {
            "Name": "DManProjectEditor", // 追加
            "Type": "Editor",            // エディタをビルドするときだけ
            "LoadingPhase": "Default"
        }
    ],
    "Plugins": [
        {
            "Name": "FMODStudio",
            "Enabled": true
        }
    ]
}

Source / DManProject / DMan.h, DMan.cpp

これは単に Actor を継承しただけのクラスで、中身はテンプレそのまんまです。 エディタモジュールに含まれるものではないので、DManproject ディレクトリの方に入っています。このクラスのインスタンスの詳細パネルにD言語くんを表示することにします。


DMan.h

#pragma once

#include "GameFramework/Actor.h"
#include "DMan.generated.h"

UCLASS()
class DMANPROJECT_API ADMan : public AActor
{
    GENERATED_BODY()

public:
    ADMan();

    virtual void BeginPlay() override;
    virtual void Tick(float DeltaSeconds) override;
};


DMan.cpp

#include "DManProject.h"
#include "DMan.h"

ADMan::ADMan()
{
    PrimaryActorTick.bCanEverTick = true;
}

void ADMan::BeginPlay()
{
    Super::BeginPlay();
}

void ADMan::Tick(float DeltaTime)
{
    Super::Tick( DeltaTime );
}

Source / DManProjectEditor / DManDetailsStyle.h, DManDetailsStyle.cpp

これは Slate Style と呼ばれるもので、外観 (+ そのためのリソース) を管理します。詳細パネルに画像を出すためには、ここでブラシを作成してセットするという手続きが必要です。

画像ファイルは Content/Editor ディレクトリ以下に格納しています。余談ですが、画像は #D言語くん版深夜の真剣お絵描き60秒一本勝負 で描いたものを加工して作りました。


DManDetailsStyle.h

#pragma once

#include "UnrealEd.h"
#include "SlateStyle.h"

class FDManDetailsStyle : public FSlateStyleSet
{
public:
    FDManDetailsStyle();
    ~FDManDetailsStyle();
};


DManDetailsStyle.cpp

#include "DManProjectEditor.h"
#include "DManDetailsStyle.h"

FDManDetailsStyle::FDManDetailsStyle() : FSlateStyleSet("DManDetailsStyle")
{
    // ブラシを作成してセット (第一引数の文字列で呼び出して使えるようになる)
    Set("DManImageA", new FSlateImageBrush(FPaths::GameContentDir() / "Editor” / "DManImageA.png", FVector2D(270.0f, 270.0f)));
    Set("DManImageB", new FSlateImageBrush(FPaths::GameContentDir() / "Editor" / "DManImageB.png", FVector2D(270.0f, 270.0f)));

    FSlateStyleRegistry::RegisterSlateStyle(*this);
}

FDManDetailsStyle::~FDManDetailsStyle()
{
    FSlateStyleRegistry::UnRegisterSlateStyle(*this);
}

Source / DManProject / DManDetailsCustomization.h, DManDetailsCustomization.cpp

詳細パネルのカスタムレイアウトを記述します。入力に対する応答もここで設定します。


DManDetailsCustomization.h

#pragma once

#include "DManDetailsStyle.h"
#include "Editor/PropertyEditor/Public/PropertyEditorModule.h"

class FDManDetailsCustomization : public IDetailCustomization
{
private:
    TSharedPtr<FDManDetailsStyle> Style;
    TSharedPtr<SImage> Image;

    bool DancePattern;
public:
    FDManDetailsCustomization();

    static TSharedRef<IDetailCustomization> MakeInstance();
    virtual void CustomizeDetails(IDetailLayoutBuilder& DetailLayout) override;

    FReply OnButtonClicked();
};


DManDetailsCustomization.cpp

#include "DManProjectEditor.h"
#include "DManDetailsCustomization.h"
#include "Editor/PropertyEditor/Public/DetailLayoutBuilder.h"
#include "Editor/PropertyEditor/Public/DetailCategoryBuilder.h"
#include "Editor/PropertyEditor/Public/DetailWidgetRow.h"

FDManDetailsCustomization::FDManDetailsCustomization() : Style(MakeShareable(new FDManDetailsStyle())) {}

TSharedRef<IDetailCustomization> FDManDetailsCustomization::MakeInstance()
{
    return MakeShareable(new FDManDetailsCustomization);
}

void FDManDetailsCustomization::CustomizeDetails(IDetailLayoutBuilder& DetailLayout)
{
    // "D" という名前のカテゴリを編集
    IDetailCategoryBuilder& DManCategory = DetailLayout.EditCategory("D", FText::GetEmpty(), ECategoryPriority::Important);

    // "D" カテゴリにD言語くん表示用の行を作成
    FDetailWidgetRow& DManRow = DManCategory.AddCustomRow(FText::GetEmpty());

    // 中身の設定 (配置や大きさを設定した Box の中に画像を入れて表示)
    DManRow.WholeRowContent()
    [
        SNew(SBox)
        .HAlign(HAlign_Center)
        .VAlign(VAlign_Center)
        .WidthOverride(270.0f)
        .HeightOverride(270.0f)
        [
            // 画像はボタンを押した時に変更したいので、
            // SImage は SNew ではなく SAssignNew で生成して、後から参照できるようにする
            SAssignNew(Image, SImage)
            .Image(Style->GetBrush("DManImageA"))   // FDManDetailsStyle でセットしたブラシを取得して利用
        ]
    ];

    // "D" カテゴリにボタン設置用の行を作成
    FDetailWidgetRow& ButtonRow = DManCategory.AddCustomRow(FText::GetEmpty());

    // 中身の設定 (ボタンのラベルとクリック時の動作を設定)
    ButtonRow.WholeRowContent()
    [
        SNew(SButton)
        .Text(FString("Dance! Dance! Dance! Dance! Dance! Dance! Dance!"))
        .OnClicked(this, &FDManDetailsCustomization::OnButtonClicked)
    ];

}

// ボタンクリック時に呼び出されるメソッド
FReply FDManDetailsCustomization::OnButtonClicked()
{
    Image->SetImage(Style->GetBrush(DancePattern ? "DManImageA" : "DManImageB"));   // FDManDetailsStyle でセットしたブラシを取得して利用
    DancePattern = !DancePattern;
    return FReply::Handled();
}

Source / DManProjectEditor / DManProjectEditor.h, DManProjectEditor.cpp

エディタコードモジュールの本体です。ここで、FDManDetailsCustomization のインスタンスを、ADMan の詳細パネルのカスタムレイアウトとして割り当てる処理を行います。


DManProjectEditor.h

#pragma once

#include "EngineMinimal.h"
#include "UnrealEd.h"

class FDManProjectEditor : public IModuleInterface
{
public:
    virtual void StartupModule() override;
    virtual void ShutdownModule() override;
};


DManProjectEditor.cpp

#include "DManProjectEditor.h"
#include "DManDetailsCustomization.h"
#include "DMan.h"
#include "Editor/PropertyEditor/Public/PropertyEditorModule.h"

void FDManProjectEditor::StartupModule()
{
    FPropertyEditorModule& PropertyModule = FModuleManager::LoadModuleChecked<FPropertyEditorModule>("PropertyEditor");
    // FDManDetailsCustomization のインスタンスを ADMan の詳細パネルのカスタムレイアウトとして割り当てる
    PropertyModule.RegisterCustomClassLayout(ADMan::StaticClass()->GetFName(), FOnGetDetailCustomizationInstance::CreateStatic(&FDManDetailsCustomization::MakeInstance));

    PropertyModule.NotifyCustomizationModuleChanged();
}

void FDManProjectEditor::ShutdownModule()
{
    FPropertyEditorModule& PropertyModule = FModuleManager::LoadModuleChecked<FPropertyEditorModule>("PropertyEditor");
    PropertyModule.UnregisterCustomClassLayout(ADMan::StaticClass()->GetFName());
}

IMPLEMENT_MODULE(FDManProjectEditor, "DManProjectEditor");

以上のコードをビルドし、UE4 エディタ上で C++ クラス/DManProject/DMan をレベルに配置すれば、詳細パネルにD言語くんが表示されているはずです。ボタンを連打して大暴れさせてください。

最後に

今年はD言語くんの年だったな!

来年もD言語くんの年にしような!!!

  1. この講演の情報は Unreal Engine Forum にまとめられており、プロジェクトファイルも公開されています。プロジェクトファイルはエディタ拡張作成の参考になるのはもちろんですが、ピンボール台を簡単に作成するためのツールとしても使えるので、C++ が書けない人でも楽しめるものになっています。