メインコンテンツまでスキップ

Unreal Engine の VCS プラグインインターフェースを理解する

Unreal Engine には Git や Perforce といったバージョン管理システム(VCS)との連携機能が標準搭載されています。Content Browser でアセットを右クリックすれば「Check Out」「Submit」といった操作ができますし、エディタ右下のステータスバーには接続状態が表示されます。

では、この機能はどのように実装されているのでしょうか。独自の VCS プラグインを作りたい場合、エンジンはどこまでサポートしてくれるのでしょうか。

本記事ではエンジンソースを読み解きながら、VCS プラグイン開発のためのインターフェース群を詳しく解説します。

対象読者

  • カスタム VCS プラグインを開発したい方
  • 既存 VCS プラグインの挙動を理解したい方
  • UE のモジュラー設計に興味がある方

C++ と UE のモジュールシステムについて基本的な理解があることを前提とします。

検証環境

  • Unreal Engine 5.7

本記事はエンジンソースの解説であり、ベストプラクティスの提示ではありません。実際のプラグイン開発では Epic の公式ドキュメントも併せて参照してください。

全体アーキテクチャ

レイヤー構造

UE の VCS 統合は、以下の 4 層で構成されています。

この設計のポイントは 抽象化による疎結合 です。

エディタは「どの VCS を使っているか」を知りません。ISourceControlModule::Get().GetProvider() で取得したプロバイダーに対して操作を依頼するだけです。Git なのか Perforce なのかは、そのプロバイダーの実装次第です。

ソースコードの配置

パス内容
Engine/Source/Developer/SourceControl/コアモジュール(インターフェース定義)
Engine/Plugins/Developer/GitSourceControl/Git プラグイン
Engine/Plugins/Developer/PerforceSourceControl/Perforce プラグイン
Engine/Plugins/Developer/SubversionSourceControl/SVN プラグイン
Engine/Plugins/Developer/PlasticSourceControl/Plastic SCM プラグイン

コアモジュールは 約 45 ファイル、Git プラグインは 約 20 ファイル で構成されています。Git プラグインが最もシンプルな実装なので、プラグイン開発の参考にするならこれがおすすめです。

コアインターフェース

VCS プラグインを構成するインターフェースは 6 つあります。これらは互いに連携しながら、エディタと VCS の橋渡しを行います。

まず全体像を把握しましょう。

インターフェース一言で言うとプラグイン開発で実装するか
ISourceControlModule司令塔しない(エンジン提供)
ISourceControlProviderVCS との窓口必須
ISourceControlStateファイルの状態必須
ISourceControlOperation操作の定義必要に応じて
ISourceControlChangelist変更のまとまりVCS が対応していれば
ISourceControlRevision履歴の 1 項目履歴機能を実装するなら

【司令塔】ISourceControlModule

ファイル: Public/ISourceControlModule.h

このインターフェースは、ソースコントロールシステム全体の「司令塔」です。プラグイン開発者が実装するものではなく、エンジンが提供するものです。しかし、その役割を理解しておくことで、自分のプラグインがどう呼び出されるのかが見えてきます。

何をしているのか

ISourceControlModule の主な仕事は 3 つあります。

1. プロバイダーの管理

エディタでは、Editor Preferences から使用する VCS を選択できます。標準では Git、Perforce、SVN、Plastic SCM、None(無効)が用意されていますが、この選択肢は固定ではありません。

IModularFeatures"SourceControl" として登録されたプロバイダーは、すべてこの選択肢に表示されます。つまり、本記事で解説するインターフェースを実装したカスタムプラグインを作れば、それも選択肢に加わるということです。

ユーザーがプロバイダーを選択すると、内部的には SetProvider("プロバイダー名") が呼ばれ、以降のすべての VCS 操作はそのプロバイダーに委譲されます。

2. 操作の中継

エディタの各所(Content Browser、レベルエディタ、マテリアルエディタなど)から VCS 操作のリクエストが来ます。これらはすべて ISourceControlModule::Get().GetProvider() を経由して、現在アクティブなプロバイダーに届けられます。

つまり、エディタ側のコードは「どの VCS を使っているか」を知る必要がありません。GetProvider() が返すものに対して操作を依頼するだけです。

3. バックグラウンド更新の管理

Content Browser を開いていると、ファイルのアイコンに VCS の状態(チェックアウト済み、変更あり等)が表示されます。しかし、毎回サーバーに問い合わせていてはパフォーマンスに影響します。

ISourceControlModule は「古くなった状態のみを更新する」仕組みを持っています。QueueStatusUpdate() が呼ばれると、そのファイルの状態が最後に更新されてから 5分以上経過しているか をチェックします。経過していれば更新キューに追加され、Tick() のタイミングでバッチ処理として非同期実行されます。

「定期的にポーリングする」のではなく、「リクエストされたファイルのうち、古いものだけを効率的に更新する」という設計です。

プラグイン開発者との関わり

プラグイン開発者が ISourceControlModule を直接実装することはありません。しかし、以下の点は覚えておく必要があります。

  • 自分のプロバイダーは GetProvider() 経由で呼び出される
  • Tick() が毎フレーム呼ばれ、非同期処理の完了チェックが行われる
  • プロバイダー切り替え時に Init()Close() が呼ばれる

【VCS との窓口】ISourceControlProvider

ファイル: Public/ISourceControlProvider.h

ここがプラグイン開発の本丸です。 このインターフェースを実装することで、任意の VCS を UE エディタに統合できます。

約 30 個のメソッドがありますが、大きく 5 つのカテゴリに分類できます。

ライフサイクル管理

プロバイダーには「生まれてから死ぬまで」のライフサイクルがあります。

Init() は、ユーザーがこのプロバイダーを選択したときに呼ばれます。ここで VCS サーバーへの接続を試みたり、ローカルリポジトリの存在確認を行ったりします。引数の bForceConnectiontrue なら即座に接続を試み、false ならユーザーがログインダイアログで明示的に接続するまで待ちます。

Close() は、別のプロバイダーに切り替えられたとき、またはエディタ終了時に呼ばれます。接続を閉じ、リソースを解放します。

Git プラグインの場合、Init() では git コマンドの存在確認と、プロジェクトディレクトリが Git リポジトリかどうかの確認を行っています。

状態の問い合わせ

「このファイルは VCS で管理されているか」「誰かがチェックアウトしているか」「最新版か」といった問い合わせに答えるのが GetState() です。

ここで重要なのが EStateCacheUsage パラメータです。

ISourceControlProvider.h
namespace EStateCacheUsage
{
enum Type
{
ForceUpdate, // 必ずサーバーに問い合わせる
Use, // キャッシュがあればそれを使う
};
}

VCS への問い合わせはネットワーク越しになることが多く、時間がかかります。Content Browser でフォルダを開くたびに数百ファイルの状態を取得していては、とても使い物になりません。

そこで、プロバイダーは内部に状態キャッシュを持ちます。EStateCacheUsage::Use が指定された場合はキャッシュを返し、ForceUpdate の場合のみ実際に VCS に問い合わせます。

操作の実行

チェックアウト、チェックイン、リバート、同期、これらの操作を実行するのが Execute() メソッドです。

Execute() の設計で特徴的なのは、操作の種類を ISourceControlOperation のサブクラスで表現している点です。FCheckOutFCheckInFSync といったクラスがあり、それぞれが操作固有のパラメータを持ちます。

プロバイダーは Execute() 内で操作の種類を判別し、適切な処理を行います。Git プラグインでは、ワーカーパターンを使って各操作を別々のクラスに委譲しています(後述)。

ワークフロー特性の宣言

VCS にはそれぞれ「流儀」があります。Perforce は編集前にチェックアウトが必要ですが、Git ではいつでも編集できます。Perforce にはチェンジリストがありますが、Git にはありません。

こうした違いを吸収するのが「ワークフロー特性メソッド」です。

メソッド問いかけ
UsesCheckout()編集前にチェックアウトが必要?
UsesChangelists()チェンジリストの概念がある?
UsesLocalReadOnlyState()未チェックアウトファイルを読み取り専用にする?
UsesFileRevisions()ファイル単位のリビジョンがある?
UsesSnapshots()スナップショット(タグ)の概念がある?

エディタ UI はこれらの戻り値を見て、表示する項目を調整します。たとえば、UsesCheckout()false を返すプロバイダーでは、「Check Out」メニュー項目の挙動が変わります。

Git プラグインの場合、ほとんどのメソッドが false を返します。Git は分散型 VCS であり、Perforce のようなサーバーベースのロック機構を持たないからです。

FGitSourceControlProvider.cpp
// FGitSourceControlProvider
bool UsesCheckout() const override { return false; }
bool UsesChangelists() const override { return false; }
bool UsesLocalReadOnlyState() const override { return false; }

UI の提供

MakeSettingsWidget() は、ログインダイアログに表示される設定 UI を返します。ここでユーザーはサーバーアドレス、ユーザー名、パスワードなどを入力します。

Git プラグインの場合は、Git バイナリのパスやリポジトリのルートディレクトリを設定する UI が表示されます。Perforce プラグインでは、サーバーアドレス、ユーザー名、ワークスペース名などの入力欄があります。

【ファイルの現状を伝達する】ISourceControlState

ファイル: Public/ISourceControlState.h

1 つのファイルの VCS 状態を表現するインターフェースです。Content Browser でファイルアイコンの右下に表示される小さなバッジの情報源がこれです。

状態の表現

ISourceControlState が答える質問は、大きく 3 種類あります。

「このファイルは今どんな状態?」

  • IsCheckedOut(): 自分がチェックアウトしている
  • IsCheckedOutOther(): 他の人がチェックアウトしている
  • IsAdded(): 新規追加としてマークされている
  • IsDeleted(): 削除としてマークされている
  • IsModified(): 変更がある
  • IsConflicted(): コンフリクトしている
  • IsCurrent(): 最新版と同期している

「このファイルに何ができる?」

  • CanCheckout(): チェックアウトできる
  • CanCheckIn(): チェックインできる
  • CanRevert(): リバートできる
  • CanAdd(): VCS に追加できる
  • CanDelete(): 削除できる
  • CanEdit(): 編集できる

「表示用の情報は?」

  • GetDisplayName(): 状態の表示名("Checked Out" など)
  • GetDisplayTooltip(): ツールチップのテキスト
  • GetIcon(): 状態を示すアイコン

VCS の違いを吸収する設計

ここで興味深いのは、Git と Perforce で「チェックアウト」の意味が全く異なることです。

Perforce の世界では、チェックアウトは「編集権の取得」を意味します。ファイルを編集する前にサーバーに宣言し、他の人が同時に編集しないようロックします。

Git の世界には、この概念がありません。ファイルはいつでも編集でき、コンフリクトはマージ時に解決します。

では、Git プラグインの IsCheckedOut() は何を返すべきでしょうか?

Git プラグインは、「ワーキングコピーに変更がある」ことをチェックアウト状態として扱っています。つまり、git status で変更ありと表示されるファイルは IsCheckedOut() == true になります。

FGitSourceControlState.cpp
bool FGitSourceControlState::IsCheckedOut() const
{
return WorkingCopyState == EWorkingCopyState::Added
|| WorkingCopyState == EWorkingCopyState::Deleted
|| WorkingCopyState == EWorkingCopyState::Modified
|| WorkingCopyState == EWorkingCopyState::Renamed
|| WorkingCopyState == EWorkingCopyState::Copied;
}

これは「意味の翻訳」です。Perforce の「チェックアウト済み」と Git の「変更あり」は、技術的には別物ですが、ユーザーにとっての意味(「このファイルは作業中」)は同じです。

スレッドセーフティ

ISourceControlStateTSharedFromThis<ISourceControlState, ESPMode::ThreadSafe> を継承しています。これは、状態オブジェクトが複数のスレッドから参照される可能性があるためです。

VCS への問い合わせはバックグラウンドスレッドで行われることがあり、その結果として状態オブジェクトが更新されます。一方、UI スレッドはその状態を読み取ってアイコンを描画します。この両者が安全に動作するよう、スレッドセーフな参照カウントが使われています。

【操作を抽象化する】ISourceControlOperation

ファイル: Public/ISourceControlOperation.h

「チェックアウトしたい」「チェックインしたい」「同期したい」、これらの操作を抽象化したのが ISourceControlOperation です。

なぜ操作をクラスにするのか

単純に考えれば、Provider->CheckOut(Files) のようなメソッドを用意すればよさそうです。しかし、それだと以下の問題があります。

  1. 操作ごとのパラメータが異なる: チェックインには説明文が必要、同期にはリビジョン指定が必要、など
  2. 操作結果の取得方法が統一できない: エラーメッセージ、警告、成功メッセージの扱いが操作ごとにバラバラになる
  3. 新しい操作を追加しにくい: プロバイダーインターフェースにメソッドを追加すると、すべての実装を修正する必要がある

操作をクラスとして表現することで、これらの問題を解決しています。

操作の実行パターン

操作を実行する典型的なコードは以下のようになります。

操作の実行例
// チェックイン操作を作成
TSharedRef<FCheckIn> CheckInOp = ISourceControlOperation::Create<FCheckIn>();
CheckInOp->SetDescription(FText::FromString(TEXT("バグ修正")));

// 実行
ECommandResult::Type Result = Provider.Execute(
CheckInOp,
nullptr, // Changelist
FilesToCheckIn,
EConcurrency::Synchronous
);

// 結果確認
if (Result == ECommandResult::Succeeded)
{
FText SuccessMsg = CheckInOp->GetSuccessMessage();
}
else
{
const FSourceControlResultInfo& ResultInfo = CheckInOp->GetResultInfo();
for (const FText& Error : ResultInfo.ErrorMessages)
{
// エラー処理
}
}

操作オブジェクトは、入力パラメータ(SetDescription)と出力結果(GetSuccessMessageGetResultInfo)の両方を運ぶコンテナとして機能します。

【変更をまとめる】ISourceControlChangelist

ファイル: Public/ISourceControlChangelist.h

チェンジリストは、複数のファイル変更をひとまとまりにする概念です。Perforce では標準的な機能ですが、Git や SVN にはありません。

このインターフェースは非常にシンプルで、識別子の取得と削除可否の確認程度のメソッドしかありません。

ISourceControlChangelist.h
class ISourceControlChangelist
{
public:
virtual bool CanDelete() const { return true; }
virtual FString GetIdentifier() const { return TEXT(""); }
virtual bool IsDefault() const { return false; }
};

Git プラグインでは、このインターフェースはほとんど使われません。UsesChangelists()false を返すため、エディタ UI もチェンジリスト関連の機能を表示しません。

【履歴の 1 ページ】ISourceControlRevision

ファイル: Public/ISourceControlRevision.h

ファイルの変更履歴における 1 つのエントリを表現します。「誰が」「いつ」「何を」変更したかの情報を持ちます。

履歴機能を実装する場合、ISourceControlState::GetHistoryItem() から返されるのがこのインターフェースです。

主なメソッドは以下の通りです。

  • GetRevision(): リビジョン識別子(コミットハッシュや CL 番号)
  • GetUserName(): 変更者
  • GetDate(): 変更日時
  • GetDescription(): コミットメッセージ
  • Get(): このリビジョンのファイル内容を取得

Get() メソッドは、指定リビジョンのファイルを一時ファイルとして取得します。差分表示やマージツールで使われます。

操作(Operation)クラス

ファイル: Public/SourceControlOperations.h

操作をクラスで表現する理由

VCS プラグインシステムでは、「チェックアウト」「チェックイン」といった個々の操作が、それぞれ独立したクラスとして定義されています。一見すると、Provider->CheckOut(Files) のようなメソッドを用意すれば済む話に思えますが、実際にはそうはいきません。

パラメータの多様性

VCS の操作は、それぞれ必要とする情報が異なります。

チェックインには「コミットメッセージ」が必要です。同期(Sync)には「どのリビジョンに同期するか」という指定が必要です。リバートには「ソフトリバート(変更は残す)かハードリバート(完全に元に戻す)か」というオプションがあります。

これらをすべてメソッドの引数として渡そうとすると、引数の数が膨大になります。また、VCS によってはオプションの有無が異なるため、NULL チェックだらけのコードになってしまいます。

操作をクラスにすることで、各操作は自分に必要なパラメータだけをメンバー変数として持てます。FCheckIn クラスには SetDescription() があり、FSync クラスには SetRevision() があります。呼び出し側は、必要なパラメータだけをセットすればよいのです。

結果の取得を統一する

VCS 操作の結果は「成功/失敗」だけでは不十分です。チェックインが成功したら「どのリビジョンが作られたか」を知りたいですし、失敗したら「なぜ失敗したか」のエラーメッセージが必要です。

操作クラスは、入力パラメータと出力結果の両方を運ぶコンテナとして機能します。FCheckIn クラスの GetSuccessMessage() は、チェックイン成功後に「Changelist 12345 submitted」のようなメッセージを返します。GetResultInfo() からはエラーメッセージや警告を取得できます。

拡張性の確保

将来、新しい操作が必要になったとき、プロバイダーインターフェースにメソッドを追加すると、すべての既存実装を修正しなければなりません。しかし、操作クラスを追加する方式なら、既存のプロバイダーは「未知の操作は未サポート」として扱えばよく、既存コードへの影響がありません。

操作クラスの分類

約 30 種類ある操作クラスは、大きく 4 つのカテゴリに分けられます。

基本ファイル操作

日常的なバージョン管理作業で使う操作です。

操作クラス用途
FConnectVCS サーバーへの接続を確立する
FUpdateStatusファイルの VCS 状態を取得・更新する
FCheckOutファイルを編集可能な状態にする(Perforce 流のロック取得)
FCheckIn変更をリポジトリにコミットする
FMarkForAdd新規ファイルを VCS 管理下に追加する
FDeleteファイルを削除対象としてマークする
FRevertローカルの変更を破棄し、リポジトリの状態に戻す
FSyncリポジトリから最新版(または指定リビジョン)を取得する
FCopyファイルをコピーする(履歴を維持するかどうかを選択可能)
FResolveコンフリクト状態を解決済みとしてマークする

これらは、どの VCS プラグインでも実装が期待される基本操作です。Git プラグインでは 9 種類の操作(Connect, UpdateStatus, MarkForAdd, Delete, Revert, Sync, CheckIn, Copy, Resolve)に対応するワーカーが登録されています。

チェンジリスト操作

Perforce のような「チェンジリスト」概念を持つ VCS 向けの操作です。

操作クラス用途
FGetPendingChangelists未送信のチェンジリスト一覧を取得
FGetSubmittedChangelists送信済みチェンジリスト一覧を取得
FNewChangelist新しいチェンジリストを作成
FDeleteChangelist空のチェンジリストを削除
FEditChangelistチェンジリストの説明を編集
FMoveToChangelistファイルを別のチェンジリストに移動
FUpdatePendingChangelistsStatusチェンジリストの状態を更新

Git プラグインはこれらを実装していません。UsesChangelists()false を返すため、エディタ UI もこれらの操作を呼び出しません。

シェルブ操作

「作業中の変更を一時的にサーバーに保存し、後で取り出す」機能のための操作です。

操作クラス用途
FShelve変更をシェルブ(棚上げ)する
FUnshelveシェルブした変更を取り出す
FDeleteShelvedシェルブした変更を削除する

この機能は Perforce で特に重要です。Git の stash に相当しますが、サーバー側に保存される点が異なります。

ワークスペース・ユーティリティ操作

環境設定や情報取得のための操作です。

操作クラス用途
FCreateWorkspaceワークスペース(クライアント)を作成
FDeleteWorkspaceワークスペースを削除
FGetWorkspaces利用可能なワークスペース一覧を取得
FDownloadFileファイルをサーバーからダウンロード(同期はしない)
FGetFile特定リビジョンのファイル内容を取得
FWhereファイルのローカル/リモートパス対応を取得
FGetFileList指定パターンに一致するファイル一覧を取得

操作固有のパラメータ

いくつかの操作クラスは、固有のパラメータを持っています。プラグイン開発者は、これらのパラメータを適切に処理する必要があります。

FCheckIn のパラメータ

チェックイン操作は、コミットメッセージを必ず受け取ります。また、「チェックイン後もファイルをチェックアウト状態のままにする」オプションがあります。連続して作業を続けたい場合に便利です。

  • SetDescription() / GetDescription(): コミットメッセージ
  • SetKeepCheckedOut() / GetKeepCheckedOut(): チェックイン後もチェックアウト状態を維持するか

FSync のパラメータ

同期操作は、どのリビジョンに同期するかを指定できます。指定がなければ最新版(HEAD)に同期します。

  • SetRevision() / GetRevision(): 同期先のリビジョン
  • SetHeadRevisionFlag(): 最新版フラグ
  • SetForce(): 強制同期(すでに同じリビジョンでも再取得)

FRevert のパラメータ

リバート操作には「ソフトリバート」という概念があります。これは、VCS 上のマーク(チェックアウト状態など)だけを解除し、ファイルの内容変更は残すモードです。

  • SetSoftRevert() / IsSoftRevert(): ソフトリバートか
  • SetRevertAll() / IsRevertAll(): 全ファイルを対象とするか

FUpdateStatus のパラメータ

状態更新操作は、何を更新するかを細かく指定できます。すべてを更新すると時間がかかるため、必要な情報だけを取得するのが一般的です。

  • SetUpdateHistory(): 履歴情報も取得するか
  • SetGetOpenedOnly(): 編集中のファイルだけを対象とするか
  • SetUpdateModifiedState(): 変更状態の詳細確認を行うか(コストが高い)
  • SetCheckingAllFiles(): 全ファイルチェックのヒント(プロバイダーが最適化に使える)

同期実行と非同期実行

VCS 操作は、同期(ブロッキング)と非同期の 2 つのモードで実行できます。

なぜ非同期実行が必要か

VCS 操作は、ネットワーク通信を伴うことが多く、完了まで数秒から数十秒かかることがあります。この間、UI がフリーズするのは避けたいところです。

エディタでファイルを保存したとき、自動的にチェックアウトが行われることがあります。このとき同期実行だと、保存操作全体がブロックされます。非同期実行なら、バックグラウンドでチェックアウトが行われ、完了時に通知されます。

実行モードの選択

Execute() の引数で EConcurrency::SynchronousEConcurrency::Asynchronous を指定します。

同期実行は、結果をすぐに使いたい場合に適しています。たとえば、ユーザーが明示的に「同期」ボタンを押して、完了を待っている場合です。

非同期実行は、バックグラウンドで処理を行い、完了時にコールバックを受け取りたい場合に適しています。コールバックには操作オブジェクトと結果コードが渡されるので、成功・失敗に応じた処理ができます。

プロバイダー実装者の責務

非同期実行をサポートするには、プロバイダー側でスレッド管理を行う必要があります。多くのプラグインは「コマンドキュー」パターンを採用しています。

  1. 非同期リクエストを受けたら、コマンドオブジェクトを作成してキューに追加
  2. ワーカースレッドがキューからコマンドを取り出して実行
  3. 実行完了後、メインスレッドの Tick() でコールバックを呼び出す

この仕組みは後述の「ワーカースレッドアーキテクチャ」で詳しく説明します。

プラグイン登録の仕組み

VCS プラグインが「エディタの選択肢に表示される」までには、いくつかのステップがあります。このセクションでは、プラグインがどのように登録され、エンジンに認識されるのかを説明します。

IModularFeatures とは何か

UE には「Modular Features」という仕組みがあります。これは、あるモジュールが「こういう機能を持っていますよ」と宣言し、別のモジュールがそれを発見・利用するためのレジストリです。

VCS プラグインは、この仕組みを使って自分の存在をエンジンに知らせます。具体的には、"SourceControl" という名前でプロバイダーオブジェクトを登録します。

なぜ IModularFeatures を使うのか

VCS プラグインとエンジンコアは、互いを直接知りません。エンジンのソースコントロールモジュールは Git プラグインの存在を知らないし、Git プラグインもエンジンの内部実装を知りません。

この「お互いを知らない」状態で連携するには、間に立つ仲介者が必要です。IModularFeatures がその役割を担います。

  • Git プラグインは「自分は SourceControl 機能を提供できます」と登録する
  • エンジンは「SourceControl 機能を提供するプラグインをください」と問い合わせる
  • IModularFeatures が両者を橋渡しする

この設計により、新しい VCS プラグインを追加するときにエンジン側のコードを一切変更する必要がありません。

プラグインモジュールの責務

VCS プラグインは、UE の標準的なモジュールとして実装します。IModuleInterface を実装したモジュールクラスを作り、StartupModule()ShutdownModule() で初期化・終了処理を行います。

StartupModule での処理

プラグインがロードされると、StartupModule() が呼ばれます。ここで行うべき処理は 2 つあります。

1. ワーカーの登録

後述する「ワーカー」は、各操作の実際の処理を担当するクラスです。プロバイダーは、操作名("Connect"、"CheckIn" など)とワーカーのファクトリ関数を紐付けて登録します。

たとえば Git プラグインは、"Connect" 操作に FGitConnectWorker を、"UpdateStatus" 操作に FGitUpdateStatusWorker を登録しています。この登録により、エンジンから「Connect 操作を実行して」とリクエストが来たとき、適切なワーカーが呼び出されます。

2. Modular Feature への登録

プロバイダーオブジェクトを IModularFeatures に登録します。登録名は "SourceControl" です。この登録を行うことで、エンジンのソースコントロールモジュールがこのプロバイダーを発見できるようになります。

ShutdownModule での処理

モジュールがアンロードされるとき、ShutdownModule() が呼ばれます。ここでは逆の処理を行います。

まず、プロバイダーの Close() を呼んで、開いている接続やリソースを解放します。次に、IModularFeatures から登録を解除します。

登録解除を忘れると、存在しないプロバイダーへの参照が残り、クラッシュの原因になります。

エンジン側の発見メカニズム

エンジンのソースコントロールモジュール(FSourceControlModule)は、VCS プラグインを自動的に発見します。

起動時の処理

FSourceControlModule::StartupModule() では、IModularFeatures の登録イベントを監視するように設定します。これにより、後からロードされたプラグインも検出できます。

また、デフォルトプロバイダー("None"、つまり VCS を使わない選択肢)を最初に登録します。これがないと、エディタ起動時に利用可能なプロバイダーが存在せず、エラーになってしまいます。

プロバイダー登録時の処理

VCS プラグインが IModularFeatures に登録を行うと、FSourceControlModule に通知が届きます。通知を受けると、登録されたプロバイダーの一覧を更新し、必要に応じて初期化処理を行います。

この仕組みにより、プラグインの追加・削除に対してエンジンが動的に対応できます。

操作とワーカーの紐付け

プラグイン登録で重要なのが「ワーカー」の概念です。

ワーカーとは

操作クラス(FCheckInFSync など)は「何をしたいか」を表現します。一方、ワーカーは「その操作を実際にどう実行するか」を実装します。

たとえば、FSync 操作クラスは「ファイルを同期したい」という意図を表します。Git プラグインの FGitSyncWorker は「git pull を実行してファイルを更新する」という具体的な処理を行います。

なぜ分離するのか

操作クラスはエンジンが定義する共通インターフェースです。VCS の種類に関係なく、同じ FSync クラスが使われます。

一方、ワーカーは各 VCS プラグインが独自に実装します。Git では git pull、Perforce では p4 sync、SVN では svn update と、まったく異なるコマンドを実行する必要があるからです。

この分離により、エディタ側のコードは操作クラスだけを知っていればよく、VCS ごとの実装の違いはワーカーに隠蔽されます。

登録の仕組み

プロバイダーは、操作名とワーカーファクトリの対応表を持っています。RegisterWorker() メソッドで、操作名(FName)とワーカーを生成するデリゲートを登録します。

プロバイダーの Execute() メソッドが呼ばれると、まず操作オブジェクトの GetName() で操作名を取得します。次に、その名前に対応するワーカーファクトリを対応表から探し、ワーカーインスタンスを生成します。そして、そのワーカーに実際の処理を委譲します。

対応表に操作名が見つからない場合は、その操作は「未サポート」として扱われます。これにより、すべての操作を実装しなくてもプラグインとして動作できます。

プラグイン認識までの流れ

全体の流れをまとめると、以下のようになります。

  1. エディタ起動: FSourceControlModule がロードされ、Modular Feature の監視を開始
  2. デフォルト登録: "None" プロバイダーが登録される
  3. プラグインロード: Git プラグイン等がロードされ、StartupModule() が呼ばれる
  4. ワーカー登録: 各操作に対応するワーカーがプロバイダーに登録される
  5. Modular Feature 登録: プロバイダーが "SourceControl" として登録される
  6. エンジン通知: FSourceControlModule が登録を検知し、プロバイダー一覧を更新
  7. UI 反映: エディタ設定画面のプロバイダー選択肢に表示される

ユーザーがプロバイダーを選択すると、FSourceControlModule::SetProvider() が呼ばれ、選択されたプロバイダーの Init() が実行されます。以降、すべての VCS 操作はそのプロバイダーに委譲されます。

ワーカーインターフェース

操作の実際の処理は「ワーカー」に委譲されます。ワーカーは各操作(Connect、CheckIn、Sync など)に対応する実装クラスで、プロバイダーの Execute() から呼び出されます。

IGitSourceControlWorker

Git プラグインのワーカーインターフェースは以下のように定義されています。

IGitSourceControlWorker.h
class IGitSourceControlWorker
{
public:
// ワーカーの識別名("Connect", "CheckIn" など)
virtual FName GetName() const = 0;

// VCS コマンドの実行。別スレッドで呼ばれる可能性がある
virtual bool Execute( class FGitSourceControlCommand& InCommand ) = 0;

// 状態キャッシュの更新。必ずメインスレッドで呼ばれる
virtual bool UpdateStates() const = 0;
};

重要なのは Execute()UpdateStates() の分離です。Execute() はバックグラウンドスレッドで実行される可能性があるため、プロバイダーの状態キャッシュに直接アクセスしてはいけません。代わりに、結果をワーカー自身のメンバー変数に一時保存します。

UpdateStates() は必ずメインスレッドから呼ばれるため、ここで初めてキャッシュへの書き込みを行います。

ワーカーの実装例

Git プラグインの FGitResolveWorker を例に見てみましょう。

GitSourceControlOperations.cpp
class FGitResolveWorker : public IGitSourceControlWorker
{
public:
virtual FName GetName() const override { return "Resolve"; }
virtual bool Execute(FGitSourceControlCommand& InCommand) override;
virtual bool UpdateStates() const override;

// 結果の一時保存領域
TArray<FGitSourceControlState> States;
};

bool FGitResolveWorker::Execute(FGitSourceControlCommand& InCommand)
{
// git add でコンフリクトを解決済みとしてマーク
TArray<FString> Results;
InCommand.bCommandSuccessful = GitSourceControlUtils::RunCommand(
TEXT("add"), InCommand.PathToGitBinary,
InCommand.PathToRepositoryRoot, TArray<FString>(),
InCommand.Files, Results, InCommand.ErrorMessages);

// 状態を取得して States に保存(キャッシュには触れない)
GitSourceControlUtils::RunUpdateStatus(..., States);
return InCommand.bCommandSuccessful;
}

bool FGitResolveWorker::UpdateStates() const
{
// メインスレッドでキャッシュに反映
return GitSourceControlUtils::UpdateCachedStates(States);
}

この設計により、複雑なロック機構なしでスレッドセーフな非同期処理が実現できています。

プラグインから制御できる UI 要素

VCS プラグインは、エディタの UI にどこまで関与できるのでしょうか。このセクションでは、プラグインから制御可能な UI 要素と、エンジンが共通で提供している UI 要素を整理します。

プラグインが制御する領域

プラグイン開発者が自由にカスタマイズできる UI 要素は、主に 3 つあります。

ファイル状態の表示

ISourceControlStateGetDisplayName()GetDisplayTooltip() で、Content Browser でファイルにカーソルを合わせたときに表示される状態名とツールチップを定義できます。

Git プラグインでは「Modified」「Added」「Deleted」といった Git らしい用語を、Perforce プラグインなら「Checked Out」「Marked for Add」といった用語を表示できます。ファイル単位の情報なので、VCS の特性に合わせた表現が可能です。

設定画面

ISourceControlProvider::MakeSettingsWidget() は、プロバイダー固有の設定 UI を返します。この UI は完全にプラグインの制御下にあり、Slate ウィジェットを自由に構築できます。

Git プラグインでは「Git バイナリのパス」「リポジトリルート」、Perforce プラグインでは「サーバーアドレス」「ユーザー名」「ワークスペース」など、VCS ごとに必要な設定項目を配置できます。ここに操作の説明やヘルプ情報を含めることも可能です。

状態アイコン

ISourceControlState::GetIcon() で、ファイル状態を示すアイコンを指定できます。UE が用意しているアイコンセットから選択するか、プラグイン側でカスタムアイコンリソースを追加することもできます。

エンジンが共通で提供する UI

エディタのメニュー、ダイアログ、進行状況表示などは、エンジンが共通の UI として提供しています。

メニューとツールバー

Content Browser の右クリックメニュー(「Check Out」「Check In」「Sync」など)やツールバーのボタンは、エンジンが一貫した操作体験を提供するために共通化しています。どの VCS プラグインを使っていても、同じ場所に同じメニューが表示されるため、ユーザーは VCS を切り替えても操作方法を覚え直す必要がありません。

ダイアログ

チェックインダイアログ、同期確認ダイアログ、リバート確認ダイアログなどは、エンジンが共通のワークフローとして提供しています。プラグイン開発者はダイアログの実装を気にすることなく、操作のロジックに集中できます。

進行状況メッセージ

操作実行中に表示される「Checking file(s) into Revision Control...」のようなメッセージは、操作クラス(FCheckIn など)で定義されています。これもエンジンが共通で管理しているため、プラグイン側で個別に実装する必要はありません。

用語体系について

エンジンの UI は、VCS 間で共通の用語体系を採用しています。

UE の表示Git での意味Perforce での意味
Submitcommit + pushsubmit
Check Out(自動処理)edit / lock
Syncpullsync
Revertcheckout / resetrevert

用語は Perforce の体系がベースになっていますが、これは Epic Games 社内での標準 VCS が Perforce であることに由来します。

Git ユーザーにとって「Submit」や「Check Out」は馴染みのない用語かもしれません。しかし、プラグイン開発者としては、これらの操作リクエストを受けたときに VCS 固有のコマンド(git commitgit push など)に変換すればよいだけです。UI の用語と内部実装の対応は、プラグインの責務として明確に分離されています。

ユーザー向け情報の提供

VCS 固有の情報をユーザーに伝えたい場合は、以下の方法が効果的です。

ツールチップの活用

ISourceControlState::GetDisplayTooltip() で、ファイル状態の詳細説明に VCS 固有の用語や状態を含めることができます。ユーザーがカーソルを合わせたときに、より詳しい情報を提供できます。

設定画面での説明

MakeSettingsWidget() で作成する設定 UI に、操作の対応表(「Submit = git commit + git push」など)や VCS 固有の注意事項を表示できます。

ドキュメントの整備

プラグインのドキュメントや README で、UE の操作と VCS コマンドの対応関係を説明しておくと、ユーザーの理解を助けられます。

まとめ

本記事では、UE の VCS プラグインインターフェースを解説しました。

おそらく多くの人は VCS プラグインを作ることはないとは思うのですが、普段から使っている VCS と UE の連携について、これを機に少しでも知っていただければ幸いです。

... みんなも VCS プラグイン作ってみる?😄👌