こんにちは。サーバーサイドエンジニアの佐藤です。
前回はDataMapperにおけるメソッドの再実装に関してお話させていただきました。
今回は大きく話題を変えまして、ゲームエンジンにより生成されるアセットのメタファイルのコミット漏れをGitフックで対策した話をさせていただきます。
対象となるタイトルではゲームエンジンとしてUnityを使い、Gitを利用してバージョン管理を行なっています。
Unityを用いるプロジェクトのバージョン管理ということで、クライアントに関連する話となります。
「なぜサーバーエンジニアがクライアントの話を?」と疑問に思うかもしれません。
コロプラでは組織的にクライアントエンジニアとサーバーエンジニアに分かれていますが、職種の垣根を超えて業務を行うことが可能となっています。
私自身もサーバーエンジニアの組織に属してはいますが、クライアントの実装やサーバーとクライアントの協調が必要となる機能における実装方針策定などの開発に携わっています。
そういった経験から、プロジェクトが抱える課題に対して組織の垣根を超えて対応を行いました。
結論として、pre-commitの開発によって、アセットとメタファイルの存在状態を一致させることができるようになり、Gitのトラブルの削減に成功しました。
背景
アセットとはテクスチャ(画像)、3Dモデル、スクリプト、サウンドなどのゲームエンジンにより使われるゲームのリソースです。
Unityの場合には、これらのアセットはAssetsディレクトリ以下で管理されています。少し特殊なこととしてUnityではAssets以下のディレクトリもアセットと見なされます。
エンジニアやデザイナー、プランナー、サウンドなど多くのセクションの人がこれらのアセットを作り、ディレクトリに配置することになります。
また、メタファイルはこのアセットに紐付けられたファイルであり、アセットには必ず1つのメタファイルが生成されます。
例えば”a.png”といったテクスチャのアセットが存在する場合には”a.png.meta”というメタファイルがゲームエンジンにより生成されます。
このメタファイルにはゲームエンジンにより利用されるための情報が含まれており、内容はアセットのファイルタイプによって様々です。
例えばテクスチャであれば、テクスチャの最大ピクセル数や圧縮形式などの情報が含まれます。
ファイルタイプに依存せず共通して含まれるデータとしてGUID(Globally Unique Identifier)があります。GUIDはグローバル一意識別子とも呼ばれ、アセットを特定するため重複しない値が割り当てられます。
メタファイルはゲームエンジンにより自動的に生成、編集、削除されるため、人が直接編集する必要はありません。
プロジェクトをGitで管理すると、追加/削除したアセットとゲームエンジンにより追加/削除されるメタファイルの両方を同時にコミットする必要が出てきます。
対になるメタファイルのコミットを忘れてしまうと、多くの作業者のワークスペースでゲームエンジンによりメタファイルの編集が行われてしまいます。
これは単純に編集に時間が取られるだけではありません。編集されたメタファイルがGit差分として表示されることにより、大きな問題を引き起こすことになります。
Gitの差分として表示されると自身の編集したアセットが差分に埋もれてしまい、探してコミットするのに手間がかかりますし、間違って自身の編集していないメタファイルをコミットしてしまう確率も高まります。
コミットから漏れたメタファイルをコミットに含めプッシュした場合には、そのメタファイルの差分がワークスペース上で発生している状態でプルした作業者はコンフリクトしてしまいます。
また、別々の人が同時にコミットした場合にも主にGUIDが原因となりコンフリクトが発生します。
コンフリクトの解決は非エンジニアには難しい作業となりますし、メタファイルがゲームエンジンによって編集されるため内容が理解しづらくより一層困難になります。
このようなコンフリクトが発生した際にはエンジニアが駆けつけて対応する必要が出てきます。
コンフリクトにより作業者とエンジニアの両方の手が止まってしまう状態はチームにとって大きな損害です。
対象のプロジェクトは100人以上が参加している非常に大規模なものです。人数が多ければ多いほど、これらの問題が発生する確率は高くなります。
問題の原因はアセットとメタファイルを同時コミットせずに差分が発生してしまうことです。
これをコミット時に実行されるGitフックであるpre-commitにより防げないか考えました。
Gitフックとは、コマンドを実行する直前もしくは直後にスクリプトを実行する仕組みであり、pre-commitはその中でもコミットの直前にスクリプトを実行するものになります。
このpre-commitで、アセットとメタファイルとの関係性を確認することにより、メタファイルのコミット漏れによる問題の発生を防ぎます。
チェックロジックの構築
対象の問題を解決するロジックを作るために、アセットとメタファイルの関係性と問題が発生する状態に関して考察していきます。
許される存在状態と許されない存在状態
アセットに対するメタファイルはゲームエンジンによって、作成、編集、削除されます。
アセットが追加された場合にはメタファイルは生成され、アセットが削除された場合にはメタファイルも削除されます。
つまり、アセットとメタファイルの両者は、常に同時に存在するもしくは同時に存在していない状態でいなければなりません。
言い換えれば、どちらか片方のみ存在している状態はイレギュラーであり、許されないことになります。
コミット後の存在状態
許されるべき存在状態と許されない存在状態が分かりました。今回目標することはこの許されない存在状態の発生を防ぐことです。
発生を防ぐためには、コミット後の存在状態が許されないものではないことを確認する必要があります。
コミット後の存在状態について掘り下げるため、コミット後に存在状態が一致するケースと一致しないケースに分けて考えていきましょう。
コミット後に存在状態が一致するケースには、ファイルが追加された場合と削除された場合があります。
また、コミット後に一致しないケースには、片方のファイルだけが追加された場合と削除された場合があります。
これらのコミット後に存在状態が一致しないケースが防がなくてはならないものとなります。
実際にチェックを行うには、コミット後の各ファイルの存在状態を知る必要があります。
これは「git ls-files」を使うことによって確認できます。
「git ls-files ファイルパス」の実行でパスが返ってきたらコミット後にファイルが存在し、空の場合にはファイルは存在しません。
実際にチェックを行う際には、全アセットに対してチェックを行うのは現実的ではないため、ステージされたファイルに対してのみチェックを行うこととします。
ステージされているファイルのリストは「git status」コマンドにより取得可能です。
アセットとメタファイルの双対性とステージされているファイルを基準とすることから、チェックを2方向に分けて考えます。
具体的には、「ステージされているファイルがアセットの場合のアセットを基準としたメタファイルの存在状態チェック」、「ステージされているファイルがメタファイルの場合のメタファイルを基準としたアセットの存在状態チェック」を行います。
2方向のチェック
1. アセット→メタファイルでのチェック
許可されるのはコミット後の存在状態が一致することです。
つまり、一致しない場合を検知してコミットを中止すれば良いということになります。
このことから以下のロジックを考えることができます。
if アセットがコミット後存在している then if メタファイルがコミット後存在していない then エラー処理 fi else // アセットがコミット後存在していない if メタファイルがコミット後存在している then エラー処理 fi fi
ここで考慮したいことがあります。
それは、メタファイルがゲームエンジンの手によって生成、編集、削除される都合上、作業者の意識から外れやすいということです。
実際に、作業者がコミットにアセットは含めたもののメタファイルを入れ忘れてしまうことが多くありました。
その都度エラーを発生させてしまうと修正する手間がかかってしまいます。
よって、このケースではエラーにすることなくpre-commit側で修正をかけることを考えます。
具体的にはメタファイルをステージすることによりコミット後の存在状態を一致させることができるケースが修正可能なものとなります。
メタファイルのステージにより存在状態を一致させられるかは、ワークスペース上での存在状態を確認することで判断可能です。
つまり、コミット後にアセットは存在しているもののメタファイルが存在していない場合には、ワークスペース上にメタファイルが存在している際にメタファイルをステージすることが可能であり、これにより解決できます。
また、コミット後にアセットが存在しておらずメタファイルが存在している場合も同様に考えることができ、ワークスペース上にメタファイルが存在していない際にメタファイルをステージすることが可能であり、これにより解決できます。
これらの修正を行いながら、コミット後のアセットとメタファイルの状態不一致を検出するロジックは以下のようになります。
if アセットがコミット後存在している then if メタファイルがコミット後存在していない then if メタファイルがワークスペースに存在している then メタファイルが未ステージファイルとして存在するのでステージする else メタファイルがゲームエンジンにより生成されていないためエラー fi fi else // アセットがコミット後存在していない if メタファイルがコミット後存在している then if メタファイルがワークスペースに存在していない then メタファイルが未ステージファイルとして含まれるのでステージする else メタファイルがゲームエンジンにより生成されていないためエラー fi fi fi
また、アセットにはディレクトリが含まれており、ディレクトリに対してもメタファイルが生成されます。
よって、ディレクトリに対してもメタファイルのチェックが必要となります。
ディレクトリ名をステージされているファイルパスのパースにより取得し、同様のチェックを行います。
2. メタファイル→アセットでのチェック
先程説明したように、作業者が注意を払うのはアセットであり、メタファイルは意識外にあることが多いです。
このため、意識的にメタファイルのみをステージし、アセットをステージし忘れる可能性は少ないと考えます。
よって、前述のアセット→メタファイルでのチェックのような漏れの修正は行わずエラーとします。
メタファイルからアセット方向へのチェックは単純なものになります。
if メタファイルがコミット後に存在する then if アセットがコミット後に存在しない then メタファイルだけが追加されるためエラー fi else // メタファイルがコミット後に存在しない then if アセットがコミット後に存在する then メタファイルだけが削除されるためエラー fi fi
Gitでは空のディレクトリは管理されません。これは非エンジニアが意識しづらいことです。
しかしUnityでは空のディレクトリにもメタファイルを生成します。
このためディレクトリとメタファイルでの存在不一致が多く見られました。
このロジックでは、このようなケースも防ぐことが可能です。
ロジックの組み込み
これらのロジックをベースとしてシェルスクリプトで実装し、pre-commitとして利用するようにしました。
エラー時の文言は作業者が次にどう対応すればよいのかわかるようになっています。
また、pre-commitによりメタファイルがステージされた場合にはその旨と対象のファイル名を表示し、Git操作をできるだけ隠さないようにしています。
まとめ
コミット後にアセットとメタファイルの存在状態が不一致となることをpre-commitにより防げるようになりました。
以前はローカルに大量のメタファイルのGit差分が発生しておりましたが、pre-commitの導入後は差分の発生が大幅に抑えられるようになっています。
また、Git差分によって発生していた編集していないファイルのコミットやコンフリクトの発生も抑えられています。
以上でアセットのメタファイルのコミット漏れを防ぐGitフック開発の話とさせていただきます。最後までお読みいただきありがとうございました。