【UE4】【C++】独自の簡単なスクリプトシステムを作る-前編 【★★★☆】

※この記事で使用しているUnrealのVersionは04.23.0 Preview7です。

※記事が長くなってしまったので、2ページに分けて投稿します。@ページ1

後編はこちら

※この記事のサンプルプロジェクトは以下URLにアップされています。
サンプルプロジェクト

独自で定義するスクリプトシステムを作る レベル【★★★☆】

[UE4] 独自のアセットを実装する方法(1) アセットクラスの実装
[UE4] 独自のアセットを実装する方法(2) インポートの実装

今回は、上記記事で紹介している独自アセットの実装を使って、簡単なスクリプトシステムを作成してみたいと思います。

まずは、C++Codeを実装できるプロジェクトを作成しましょう。

EpicGamesLauncherから、Unrealを起動させ、プロジェクトブラウザの「新規プロジェクト」から「C++」タブを選択して、「基本コード」を選択し、プロジェクトを作成しましょう。

サンプルプロジェクトからダウンロードしたプロジェクトを確認する際は、「.uproject」を右クリック「Generate Visual Studio Project File」を選択すると、.slnのソリューションが作成されます。

作成し、エディターが開いたら、さっそく新規のC++クラスを作成します。

最初は、スクリプトの関数の元となるクラスを作成します。
エディター左上の「ファイル」から、「新規C++クラス」を選択します。

今回は、簡易的なスクリプトということで、エディター上で使える機能をスクリプトとして使ってみたいので、親クラスは「EditorUtilityObject」を選択してください。

中身は以下のように書きます。

ScriptTaskBase.hの中身

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "UObject/NoExportTypes.h"
#include "Blutility/Classes/EditorUtilityObject.h"
#include "ScriptTaskBase.generated.h"

/**
 * 
 */
UCLASS(Blueprintable)
class KA_SCRIPT_API UScriptTaskBase : public UEditorUtilityObject
{
	GENERATED_BODY()

	public:
		UPROPERTY(EditAnywhere, BlueprintReadWrite)
			TArray Arg;

		UPROPERTY(EditAnywhere, BlueprintReadWrite)
			int32 MinArgCnt;
		
		UPROPERTY(EditAnywhere, BlueprintReadWrite)
			int32 MaxArgCnt;

		UFUNCTION(BlueprintNativeEvent,BlueprintCallable, Category = "KA_Script")
			bool Check();
			virtual bool Check_Implementation();

		UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "KA_Script")
			bool ExecFunc();
			virtual bool ExecFunc_Implementation();
	
};

ScriptTaskBase.cppの中身

// Fill out your copyright notice in the Description page of Project Settings.


#include "ScriptTaskBase.h"

bool UScriptTaskBase::Check_Implementation()
{
	int32 ArgCnt = Arg.Num();

	return (MinArgCnt <= ArgCnt || MinArgCnt = ArgCnt || MaxArgCnt <= -1);
}

bool UScriptTaskBase::ExecFunc_Implementation()
{
	return false;
}

Check() と ExecFunc()は「BlueprintNativeEvent」と「BlueprintCallable」をつけ、仮想関数として、_Implementationをつけた関数を記載することで、Blueprint側でこの関数をオーバーライドできるのと同時に、C++側でも実装することが可能になります。共通処理をC++でやりたいときなんかは便利ですね。

次に、.Build.csを編集していきます。

KA_Script.Build.csの中身

// Fill out your copyright notice in the Description page of Project Settings.

using UnrealBuildTool;

public class KA_Script : ModuleRules
{
	public KA_Script(ReadOnlyTargetRules Target) : base(Target)
	{
		PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
	
		PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "UnrealEd" });

		PrivateDependencyModuleNames.AddRange(new string[] {  });

	}
}

この辺はヒストリア様のブログに書いてある通りの設定にしています。

続いて、新しいC++クラスを作成します。
継承元は、「UObject」を選択します。
このクラスは、アセットとして、先ほど作成した関数を保持しておくためのものになります。

処理は以下のようになります。

ScriptTaskAsset.hの中身

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "UObject/ObjectMacros.h"
#include "UObject/Object.h"
#include "UObject/NoExportTypes.h"
#include "ScriptTaskBase.h"
#include "ScriptTaskAsset.generated.h"

/**
 * 
 */
UCLASS(BlueprintType)
class KA_SCRIPT_API UScriptTaskAsset : public UObject
{
	GENERATED_UCLASS_BODY()

public:

	UPROPERTY(VisibleAnywhere)
		TArray TaskObjects;

		int32 TaskNum;

	UFUNCTION(BlueprintPure)
		TArray GetScriptTasks();

	UFUNCTION(BlueprintPure)
		int32 GetTaskNum();

	UFUNCTION(CallInEditor)
		void InstantExecuteScript();
#if WITH_EDITORONLY_DATA

	UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Instanced, Category = ImportSettings)
		class UAssetImportData* AssetImportData;

	virtual void PostInitProperties() override;
	virtual void GetAssetRegistryTags(TArray& OutTags) const override;
	virtual void Serialize(FArchive& Ar) override;
#endif
};

ScriptTaskAsset.cppの中身

// Fill out your copyright notice in the Description page of Project Settings.


#include "ScriptTaskAsset.h"
#include "EditorFramework/AssetImportData.h"
UScriptTaskAsset::UScriptTaskAsset(const FObjectInitializer& ObjectInitializer)
	: Super(ObjectInitializer)
{
}

TArray UScriptTaskAsset::GetScriptTasks()
{
	return TaskObjects;
}

int32 UScriptTaskAsset::GetTaskNum()
{
	return TaskNum;
}

void UScriptTaskAsset::InstantExecuteScript()
{
	TaskNum = 0;
	for (UScriptTaskBase* TaskBuffer : TaskObjects)
	{
		bool ResultFlag = TaskBuffer->ExecFunc();
		
		if (!ResultFlag)
		{
			break;
		}
		TaskNum++;
	}
}
#if WITH_EDITORONLY_DATA
void UScriptTaskAsset::PostInitProperties()
{

	if (!HasAnyFlags(RF_ClassDefaultObject))
	{
		AssetImportData = NewObject(this, TEXT("AssetImportData"));
	}

	Super::PostInitProperties();
}


void UScriptTaskAsset::GetAssetRegistryTags(TArray& OutTags) const
{
	if (AssetImportData)
	{
		OutTags.Add(FAssetRegistryTag(SourceFileTagName(), AssetImportData->GetSourceData().ToJson(), FAssetRegistryTag::TT_Hidden));
	}

	Super::GetAssetRegistryTags(OutTags);
}

void UScriptTaskAsset::Serialize(FArchive& Ar)
{
	Super::Serialize(Ar);

	if (Ar.IsLoading() && Ar.UE4Ver() < VER_UE4_ASSET_IMPORT_DATA_AS_JSON && !AssetImportData)
	{
		AssetImportData = NewObject(this, TEXT("AssetImportData"));
	}
}
#endif

ScriptTaskBaseインスタンスを保持しておくための配列や、現在実行しているスクリプトの配列番号を返す変数・関数があります。

「InstantExecuteScript」関数は、UFUNCTIONの「CallInEditor」をつけることで、アセットを開いたときに、スクリプトを実行するためのボタンを表示することができます。

また、「if WITH_EDITORONLY_DATA」で囲っている部分は、このアセットのReimport時に使うデータを保持しておくためのものです。

次は、このアセットのImport,Reimport用のFactoryクラスを作成します。

エディター左上のファイルから新しいC++を作成し、親クラスに「Factory」を選択してください。
このあたりは、[UE4] 独自のアセットを実装する方法(2) インポートの実装と似た実装になります。

ScriptFactory.hの中身

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "Factories/Factory.h"
#include "UObject/ObjectMacros.h"
#include "Script/ScriptTaskAsset.h"
#include "ScriptFactory.generated.h"

/**
 * 
 */
UCLASS(Blueprintable)
class KA_SCRIPT_API UScriptFactory : public UFactory
{
	GENERATED_UCLASS_BODY()

	virtual bool DoesSupportClass(UClass* Class) override;
	virtual UClass* ResolveSupportedClass() override;
	virtual UObject* FactoryCreateNew(
		UClass* InClass,
		UObject* InParent,
		FName InName,
		EObjectFlags Flags,
		UObject* Context,
		FFeedbackContext* Warn
	) override;
	virtual UObject* FactoryCreateText(
		UClass* InClass,
		UObject* InParent,
		FName InName,
		EObjectFlags Flags,
		UObject* Context,
		const TCHAR* Type,
		const TCHAR*& Buffer,
		const TCHAR* BuferEnd,
		FFeedbackContext* Warn
	) override;

};

ScriptFactory.cppの中身

// Fill out your copyright notice in the Description page of Project Settings.


#include "ScriptFactory.h"

UScriptFactory::UScriptFactory(const FObjectInitializer& ObjectInitializer)
	: Super(ObjectInitializer)
{
	SupportedClass = UScriptTaskAsset::StaticClass();
	bCreateNew = false;
	bEditorImport = true;
	bText = true;
	Formats.Add(TEXT("stf;Script Task File"));
}
bool UScriptFactory::DoesSupportClass(UClass* Class)
{
	return (Class == UScriptTaskAsset::StaticClass());
}
UClass* UScriptFactory::ResolveSupportedClass()
{
	return UScriptTaskAsset::StaticClass();
}
UObject* UScriptFactory::FactoryCreateNew(
	UClass* InClass,
	UObject* InParent,
	FName InName,
	EObjectFlags Flags,
	UObject* Context,
	FFeedbackContext* Warn
)
{
	UScriptTaskAsset* NewMyAsset =
		CastChecked(StaticConstructObject_Internal(InClass, InParent, InName, Flags));
	return NewMyAsset;
}
UObject* UScriptFactory::FactoryCreateText(
	UClass* InClass,
	UObject* InParent,
	FName InName,
	EObjectFlags Flags,
	UObject* Context,
	const TCHAR* Type,
	const TCHAR*& Buffer,
	const TCHAR* BuferEnd, FFeedbackContext* Warn
)
{
	TArray Columns;
	TArray Values;
	TArray ArgBuf;
	FString StrBuf;
	FString(Buffer).ParseIntoArray(Columns, TEXT("\r\n"), true);
	
	UScriptTaskAsset* NewMyAsset =
		CastChecked(StaticConstructObject_Internal(InClass, InParent, InName, Flags));
	if (!NewMyAsset)
	{
		return NULL;
	}
	for (FString BufString : Columns) 
	{
		BufString.ParseIntoArray(Values, TEXT(","), true);
		ArgBuf = Values;
		ArgBuf.RemoveAt(0);
		if (Values[0].Contains(TEXT("#")))
		{
			continue;
		}

		//暫定 パスと命名規則決め打ち
		StrBuf = FString(TEXT("/Game/Script/Task/ST_")) + Values[0] + FString(TEXT(".ST_")) + Values[0] + FString(TEXT("_C"));
		UClass* BPClass = TSoftClassPtr(FSoftClassPath(StrBuf)).LoadSynchronous();

		if (!NewMyAsset || (0 >= Values.Num()) || !BPClass)
		{
			return NULL;
		}
		
		UScriptTaskBase* TaskObject = Cast(NewObject(NewMyAsset, BPClass));
		TaskObject->Arg = ArgBuf;

		if (!TaskObject->Check())
		{
			FPlatformMisc::MessageBoxExt(EAppMsgType::Ok, TEXT("ArgNum is Invalid"), TEXT("ScriptImportError"));
			return NULL;
		}

		NewMyAsset->TaskObjects.Add(TaskObject);
	}

	return NewMyAsset;
}

インポートするファイルの拡張子を「stf」と決め、Import時にファイルの記述から、指定したパス内にある「ScriptTaskBase」を継承した、「ST_」という命名基規則のブループリントクラスのインスタンスを作成し、変数などを入れ込んでいます。
(パス指定は暫定実装です)

作成ができたら、今度は今作成したFactoryクラスを継承してもう一つC++クラスを作ります。

ScriptReimpotFactory.hの中身

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "Factory/ScriptFactory.h"
#include "Runtime/CoreUObject/Public/UObject/ObjectMacros.h"
#include "Editor/UnrealEd/Public/EditorReimportHandler.h"
#include "ScriptReimpotFactory.generated.h"

/**
 * 
 */
UCLASS()
class KA_SCRIPT_API UScriptReimpotFactory : public UScriptFactory, public FReimportHandler
{
	GENERATED_UCLASS_BODY()

	virtual bool CanReimport(UObject* Obj, TArray& OutFilenames) override;
	virtual void SetReimportPaths(UObject* Obj, const TArray& NewReimportPaths) override;
	virtual EReimportResult::Type Reimport(UObject* Obj) override;
	virtual int32 GetPriority() const override;	
};

ScriptReimpotFactory.cppの中身

// Fill out your copyright notice in the Description page of Project Settings.

#include "ScriptReimpotFactory.h"
#include "Script/ScriptTaskAsset.h"
#include "EditorFramework/AssetImportData.h"

UScriptReimpotFactory::UScriptReimpotFactory(const FObjectInitializer& ObjectInitializer)
	: Super(ObjectInitializer)
{
	SupportedClass = UScriptTaskAsset::StaticClass();

	bCreateNew = false;
}

bool UScriptReimpotFactory::CanReimport(UObject* Obj, TArray& OutFilenames)
{
	UScriptTaskAsset* TaskAsset = Cast(Obj);
	if (TaskAsset && TaskAsset->AssetImportData)
	{
		TaskAsset->AssetImportData->ExtractFilenames(OutFilenames);
		return true;
	}
	return false;
}

void UScriptReimpotFactory::SetReimportPaths(UObject* Obj, const TArray& NewReimportPaths)
{
	UScriptTaskAsset* TaskAsset = Cast(Obj);
	if (TaskAsset && ensure(NewReimportPaths.Num() == 1))
	{
		TaskAsset->AssetImportData->UpdateFilenameOnly(NewReimportPaths[0]);
	}
}

EReimportResult::Type UScriptReimpotFactory::Reimport(UObject* Obj)
{
	UScriptTaskAsset* TaskAsset = Cast(Obj);
	if (!TaskAsset)
	{
		return EReimportResult::Failed;
	}

	const FString Filename = TaskAsset->AssetImportData->GetFirstFilename();
	if (!Filename.Len() || IFileManager::Get().FileSize(*Filename) == INDEX_NONE)
	{
		return EReimportResult::Failed;
	}

	EReimportResult::Type Result = EReimportResult::Failed;
	bool OutCanceled = false;

	if (ImportObject(TaskAsset->GetClass(), TaskAsset->GetOuter(), *TaskAsset->GetName(), RF_Public | RF_Standalone, Filename, nullptr, OutCanceled) != nullptr)
	{

		TaskAsset->AssetImportData->Update(Filename);

		if (TaskAsset->GetOuter())
		{
			TaskAsset->GetOuter()->MarkPackageDirty();
		}
		else
		{
			TaskAsset->MarkPackageDirty();
		}
		Result = EReimportResult::Succeeded;
	}
	else
	{
		Result = EReimportResult::Failed;
	}

	return Result;
}

int32 UScriptReimpotFactory::GetPriority() const
{
	return ImportPriority;
}
#undef LOCTEXT_NAMESPACE

このクラスは、アセットにあるImport用のデータから、アセットにReimportを行うためのものです。(この部分はとりあえずファイルをコンテンツブラウザへドラッグアンドドロップする方式でのReimportをうまくいかせるためのものです。AssetTypeActionsからのReimportは今回は省きます)

先ほどImport用に作成したFactoryクラスと、FReimportHandlerからの多重継承を行っています。
基本的に中身は仮想関数をオーバーライドし、独自のアセットに対応したものに置き換えているだけです。

ここまで準備が出来ましたら、HotReloadかエディタービルドしなおして、今まで作成した機能をエディターで使えるようにしてください。

後編ではEditor側の実装を行います。

後編はこちら

※この記事のサンプルプロジェクトは以下URLにアップされています。
サンプルプロジェクト

ゲーミングPCなら【FRONTIER】