そもそもの目的
ゲーム制作だと、ゲームの設定データをExcelやSpreadSheetなどで用意してそれをテーブル化してUnityなりUE4なりで扱ったりすることがよくあるのですが、そのデータを入れるのは企画屋さんです。
企画屋さんも人間なので完璧なマスタを一発で用意はできず、ゲームバランスを調整するためにマスタを書き替えてはテストプレイを繰り返します。
その時に間違ったデータや実行不可能なデータを入れてしまうことがあります。
これを回避するために、マスタの有効性を検証するツール(Validator、バリデータ)を用意して企画やさんをサポートします。
このあたりのツールをどう用意するかは人それぞれなのですが、自分は.NET畑の人間なので一気にC#でコンソールアプリを書いてしまうのが一番実装が速いです。
そこで問題となるのが、コンソールアプリをUnityやUE4からどう扱うかという話ですが、Unityの場合はEditor拡張であれば普通に.NETで呼びだせばいいだけなので割愛します。
今回はUE4かつWindowsのお話です。
フォルダの構成について
UE4でゲームを作る場合、ソースコードをDLする場合と、EpicGamesLauncherから起動する方式があるのですが、今回はソースコードです。
- MyProject
- UE4 // UE4のソースコード
- UnrealProject // 実際のゲームプロジェクト
- Tools // ツール類まとめ親フォルダ
- MasterValidator // 今回つくるやつ
- bin // UE4から呼び出すバイナリ置き場
- src // C#プロジェクト
- MasterValidator // 今回つくるやつ
今回はこのようにしています。
C#プロジェクトについて
今回はCysharpさんが出してるConsoleAppFrameworkを使っています。
これを使った理由は、
バリデーションチェック
MasterValidator.exe check -n MasterName
DataTable用CSV吐き出し
MasterValidator.exe export -n MasterName
でいい感じにコマンドを分けておきたかったからです。
また、バリデータはゲーム固有機能の塊であるマスタのチェックすることですので、機能実装エンジニアが「マスタの有効性をチェックするロジック」をValidatorに追加しなければいけません。
そのため、追加しやすいように以下のようなインターフェイスで実装できるようにしました。
// マスタの定義
[SheetName("some_master")] // SpreadSheetのシート名
public class SomeRow
{
// 開発版かリリース版かなどの情報はcheckでは使うけどexportには不要
// そういうものにはExcludeFromCsv属性を付けるとCSVに出力されない
[ExcludeFromCsv] public string tag;
public int id;
public string name;
}
// ↑の定義用のチェッカ
public class SomeMasterValidator : MasterValidatorBase<SomeRow>
{
protected override Task CheckValidationAsync(IReadOnlyList<SomeRow> rows)
{
// nameが空の行があったら失敗
var row = rows.FirstOrDefault(x => string.IsNullOrEmpty(x.name));
if (row != null)
Fail(row, "name is empty."); // 失敗時はFailを呼ぶだけ
// Fialが呼ばれなければ成功
return Task.CompletedTask;
}
}
実際にはSomeに関連するマスタ(例えばクエスト報酬マスタとアイテムマスタは関連するなど)も持ってこれるようにしているのですが、こちらUnity用に良い感じにして公開する予定なので、それと一緒に書きます。いつになるかわかりませんが。
あとはBuildEventsなどで上記のbinフォルダなとにcopyさせればOKです。
外部プロセスを呼ぶ
モジュール
UE4のエディタ拡張はモジュールとUEditorUtilityWidgetを使用すると楽です。
詳しい話はUEditorUtilityWidgetなどで検索してみてください。
外部プロセスの実行
今回はUE4のFPlatformProcess::ExecProcessを使うことにしました。
ExecProcessはプロセスが実行している間UE4が固まるのですが、今回はそれが良いかなと思ってそうしています。
さぼったとも言います。
FPlatformProcess::GetCurrentWorkingDirectory()を使うと今の実行フォルダが得られます。
.NETよろしくFPaths::Combineでそこから相対パスを錬成しようとすると\\と/が混在するパスが得られるので、おとなしくFStringの.Appendでやります。
今回は以下のようになりました。
FPlatformProcess::GetCurrentWorkingDirectory().Append(FString(TEXT("\\..\\..\\..\\..\\Tools\\MasterValidator\\bin\\MasterValidator.exe")));
FPlatformProcess::ExecProcessの使い方は以下です。
int32 Code;
FString Output;
// Outputに標準出力が入る
auto Result = FPlatformProcess::ExecProcess(*ExePath, *(FString(TEXT("check -n ")).Append(SheetName)), &Code, &Output, nullptr);
if (!Result)
{
UE_LOG(LogTemp, Error, TEXT("MasterChecker launch failed."));
return;
}
if (!Output.IsEmpty())
{
// 結果のあれこれ
}
あとはUEditorUtilityWidgetのボタンからこのUFUNCTIONを呼ぶようにして、標準出力をUE4のログに出したりFMessageDialog::Openで表示したりして返せば、無事に企画屋さんが使いやすいように連携ができます。
ただ、ここで一つ注意なのですが、Outputに入る標準出力は普通に日本のWindowsとか使っているのならShift-JISです。
つまりそのままUE4に表示すると文字化けします。
文字化けを解消するにはMultiByteToWideCharを使うのですが、UE4でそのままwindows.hをincludeして使うとTEXTマクロがダブってるだの定義が足りないだの、文句を言われます。
ので、以下のようにincludeするとよいそうです。
#if PLATFORM_WINDOWS
#define WIN32_LEAN_AND_MEAN
#include "Windows/AllowWindowsPlatformTypes.h"
#include "Windows/AllowWindowsPlatformAtomics.h"
#include "Windows.h"
#include "Windows/HideWindowsPlatformAtomics.h"
#include "Windows/HideWindowsPlatformTypes.h"
#endif
あとは頑張ってなんとかします。
自分はいいところまで行ったのですが、沼にハマりそうだったのでギリギリ文字化けを起こした状態で断念しました。
社内ツールやしええやろ精神は大事。
まとめ
自分はC++がよくわからないので、C++で下手に検証ロジック頑張るよりは個人的には外部ツールに逃がすほうがいいパターンもあるかな、という感じです。
特にC#だとリフレクションが使えるので、こういうツールはいい感じに作りやすいなという印象があります。