すぎしーのXRと3DCG

主にXR, Unity, 3DCG系の記事を投稿していきます。

Claude Code での Unity 開発の振り返り (2025年)

こんにちは、すぎしーです。クラスター株式会社にジョインして4年目になります! クラスター Advent Calendar 2025 の11日目の記事です!

qiita.com

前日は @Kazuya0123 さんの 「僕と地域とクラスター」でした。僕も cluster がオンラインで様々な市区町村がつながるプラットフォームとして認知されるよう、頑張っていきたいです。

note.com

さて、今回は2025年の Claude Code での Unity 開発の振り返りについてです。

世間の例に漏れず、クラスターのエンジニアたちにも AI コーディングを使った開発があっという間に浸透しました。

せっかく導入した年なので、そんな AI コーディングの1つである Claude Code を Unity で何に使ったかについて、3つほどピックアップして振り返ってみようと思います。

クラスターでの AI コーディングの開発事例については Cluster Tech Blog もどうぞ!

何に Claude Code を使った?

UnityEditor 向けツールの作成

UnityEditor 向けツールの作成がやりやすくなったのは、一番わかりやすい恩恵だったように思います。
特にいままで一番時間を要していた「Unity の API 仕様書を都度調べる時間」が大幅に削減できるという点は非常にありがたいと感じています。

以前までは EditorGUILayout, AssetDatabase, SerializedObject, EditorWindow といったお馴染みなものから Addressables などライブラリまで、様々な API の仕様書とにらめっこしながら開発していたことが AI にツールの仕様を伝えるだけであっという間に出来上がるようになりました。とんでもない時代が来ましたね。

以下のようなツールも作ったのですが、コード行のほとんどが Claude Code による生成です。

InputAction の編集

Unity の InputSystem で用いる InputAction ですが、項目が多いと UI でポチポチするのが大変ですし、何よりミスが起きやすいです。 特に VR のトラッカーは 11点 * 3要素(位置、回転、状態) の合計33点を指定する必要があるので、指定ミスしないように注意が必要でした。

そこで InputAction の中身を確認したところ JSON 形式でユニークなGUIDを各入力に紐づけるフォーマットだったので、 Claude Code に必要な項目を指定して規則性に従って編集してもらうことにしました。

少し調整はありましたが、あっという間に作ってくれました。

{
        ...
        {
            "name": "Tracker",
            "id": "b6b46f2b-fb24-4481-9840-28738adcb0d6",
            "actions": [
                {
                    "name": "LeftFootPosition",
                    "type": "Value",
                    "id": "969c5f83-4d27-4f58-8deb-2b6dd6365b8e",
                    "expectedControlType": "Vector3",
                    "processors": "",
                    "interactions": "",
                    "initialStateCheck": true
                },
                {
                    "name": "LeftFootRotation",
                    "type": "Value",
                    "id": "65634ff7-fa1b-4a27-bb29-f74dc757d170",
                    "expectedControlType": "Quaternion",
                    "processors": "",
                    "interactions": "",
                    "initialStateCheck": true
                },
                {
                    "name": "LeftFootState",
                    "type": "Value",
                    "id": "c9d667fd-f506-4a68-bee8-39225817751e",
                    "expectedControlType": "Integer",
                    "processors": "",
                    "interactions": "",
                    "initialStateCheck": true
                },

余談ですが、AI に GUID の生成を任せると「GUID っぽい文字列」を作ってしまいます。 例: a1b2c3d4-e5f6-g7h8-a1b2-c3d4e5f6g7h8

ちゃんとした GUID を使わせたい場合は、GUID を生成するコマンドを渡しておくと良いです。 例: ./generate-guid.gob6b46f2b-fb24-4481-9840-28738adcb0d6

GUID に限らず、AI がシンプルに使えるツールを用意することも大事ですね。

エラー通知から修正案を出させる

Claude Code に Sentry MCP Server の使用を許可して Issue のリンクを渡し、必要に応じて修正案を出させています。
補足: Sentry とは、エラー監視プラットフォームのことです。

まだすべてを任せているわけではなく、以下のような AI エージェントでも比較的解決しやすい部類の Issue をお願いする形を取っています。

  • HTTP レスポンスのハンドリング漏れ
  • 例外 (IOException) などのハンドリング漏れ

修正内容の精度がまだ安定していない部分があるのでエラー通知毎にプルリクエストを提出させる形にはしていませんが、いずれはそうしていきたいなと考えています。

その他

以下のような活用をしています。

  • client 視点での server 側の実装調査 (重要度の低いものに限定、重要度が高い場合は server エンジニアに直接確認します)
  • 設計、実装の壁打ち
  • ライブラリの詳細調査
  • リリース後に不要となった Feature Flag 分岐コードの廃止、及び関連して不要になったコードの削除
  • GitHub Actions で使用しているバージョンの一括更新
  • Node.js の Node バージョンの更新

雑感

というわけで Claude Code を Unity 開発でどのように活用したかについて書いてみましたがいかがだったでしょうか?

今年は AI コーディングを単に導入しただけでなく、AI がスムーズに開発できる環境づくりにも力をいれていた年だったかなと思います。

AI の進化の早さにも食らいついていって、どんどん開発を回していきたいですね。

さて、明日は @FUKUDA_concrete さんの 「はじめてのUnityワールド作成のススメ」です。お楽しみに!

Visual Studio CodeでVRMを改変する

こちらは クラスター Advent Calendar 2024 の17日目の記事です!

qiita.com


こんにちは、すぎしーです。クラスター株式会社にジョインして2年になります。

本記事のテーマは「Visual Studio CodeVRMを改変する」になります!今回はUnityは使用しません!

改変例として「VRMのテクスチャをVSCodeで差し替える」と「VRM1.0のExpressionのOverride設定をVSCodeで変更する」も紹介しているので、よかったら参考にしてみてください。

使用するツール

前準備

1. VSCodeをインストール

Visual Studio Code からVSCodeをインストールしてください。

2. VSCodeにglTF Toolsをインストール

以下を参考にglTF ToolsをVSCodeにインストールしてください。

  1. VSCodeのExtensionsを開く
  2. "glTF Tools"と検索する
  3. 検索で出たglTF Toolsをクリック
  4. Install ボタンをクリック

以上で前準備は完了です!

補足情報: VRMはglTF拡張である

VRMはglTFをいうファイルフォーマットを拡張したものになっています。 そのためglTF向けのツールで改変が可能な場合が多いです。

もし深堀りしたい方は以下のクラスター Advent Calendar 2023の記事を御覧ください!

tsgcpp.hateblo.jp

この性質を利用してglTF ToolsでVRMを編集しようと思います。

glTF ToolsでVRMをインポート

まずはglTF ToolsでVRMを改変する時に毎回実施することになるインポート方法を紹介します!

編集対象のVRMを作業用のフォルダにコピーする

glTF Toolsはインポート時にVRM内のテクスチャや頂点などのバイナリデータを一気にファイル化するため、作業用フォルダを用意してそのフォルダの中にVRMファイルを置きましょう。

VSCodeで対象のVRMがある作業用フォルダを開く

VSCodeのOpen Folder...で作業用フォルダを開いてください。

対象のVRMをインポートする

以下の流れでインポートできます。

  1. ファイル一覧を開く
  2. 編集対象のVRM上で右クリックする
  3. "glTF: Import from GLB" をクリックする
  4. インポートされたファイルを保存先(拡張子 "gltf"、以後「gltfファイル」と呼称)を指定する

保存先はVRMファイルとは別のフォルダでも大丈夫ですが、今回は同じ作業用フォルダに保存して説明します。

インポートが終わるとJSON形式のgltfファイルと関連するテクスチャなどのファイル群が生成されているはずです。

一緒に出力されたファイルたちはエクスポート時に必要になるので消さずに残しておいてください!

これでインポートは完了です。

Appendix: 編集が許可されているVRMであることを確認する

自身が所有者ではないVRMの場合は、glTF内の項目を確認して「このモデルを改変することを許可するか否か」を確認しておきましょう。

(VRM確認ツールでもいいですが、本記事ではトコトンVSCodeで確認します!)

VRM1.0の場合であれば allowModification、もしくはallowModificationRedistributionであれば改変が許可されています。

ちなみに VRoid Studioで作成したアバターの場合は、エクスポート時に「改変」の項目で設定した内容が反映されているはずです。

VRMを編集する

インポートが終わったらVRMの仕様書 (vrm-specification) に従いつつ、テクスチャを変えたりgltfファイルを変更してVRMを編集することになります。

改変したgltfファイルは必ず保存(Ctrl+S)してください!

具体的な作業は「改変例」を後述しているので参考にしてみてください。

次項でひとまず改変が完了した後に実施するVRMへのエクスポート方法を紹介します。

glTF ToolsでVRMをエクスポート

以下の流れで編集したgltfファイルからVRMをエクスポートします。

  1. エクスポート対象のgltfファイル上で右クリックする (開いているタブ上で右クリックでも可)
  2. "glTF: Export to GLB (Binary file)" をクリックする
  3. 出力するファイルの拡張子を".vrm"にして保存先を指定する (例: "vroid_sample_ex.vrm")

エクスポート時の拡張子はデフォルトで ".glb" になっているので、
拡張子を必ず ".vrm" に変更して保存してください!

エクスポートが完了するとVRMファイルが出力されているはずです。

ここまでが VRMをインポート → VRMを改変 → VRMをエクスポート の流れになります。VSCodeVRMを編集する場合は毎回やることになります。

補足: エクスポートで出力されるVRMバージョンはインポート時に従う

今回のやり方でエクスポートした場合、出力されるVRMバージョンはインポート時と同じになります。

というよりglTF Toolsは純粋なglTF向けツールでありVRM0.xからVRM1.0に変換するみたいな機能は特に入っていないため、gltfファイルのJSON内容はそのままで出力されるので結果としてインポート時と同じVRMバージョンが出力されます。

改変例

今回はVRoid Studioから作成した以下のVRM1.0アバターの子を使います。

改変例1: VRMのテクスチャをVSCodeで差し替える

glTF Toolsはテクスチャの入れ替えができます。やり方は簡単でインポート直後に出力されているpng画像のうち、テクスチャに使われるpng画像を差し替えるだけです。

今回は先程のアバターのシャツのテクスチャを変えて色を変えてみようと思います。

VSCodeで確認するとシャツのテクスチャの名前は vroid_sample_img13.png になっているようです。

そのテクスチャを色を変更した以下のpngに差し替えます。差し替えた後もファイル名は必ず一致させてください。

あとはgltfファイルからエクスポートするだけです。

clusterで確認してみるとアバターのシャツが差し替わっているはずです。

テクスチャを変えたいぐらいであればVSCodeだけでできちゃうのでぜひ試してみてください。

改変例2: VRM1.0のExpressionのOverride設定をVSCodeで変更する

お次はVRM1.0のExpression(表情)のOverride設定をVSCodeで変更してみようと思います。

ちなみに今回の作業内容は以下の「エモート中に口が動くようにする」をUnityを使わずVSCodeで実現するやり方になっています。

creator.cluster.mu

今回用意したVRoidStudioから出力されたアバターですが、↑の記事と同様にOverride設定がないのでエモート「笑顔」のときにまばたきが発生すると表情が崩れてしまいます。

今回はエモート「笑顔」のときにまばたきをブロックして、表情が崩れるのを回避するように設定したいと思います。

gltfファイルのJSONのから "expressions" 項目を確認する

まずは表情設定項目である "expressions" をgltf内で確認しましょう。"expressions"はVRM1.0におけるExpressionの設定項目になっています。

VSCodeでgltfファイルを開いて検索(Ctrl+F)で "expressions": と検索すると飛べます。

ここを改変することでExpressionの設定変更が可能になります。

対象のExpressionのoverrideBlink、overrideLookAt、overrideMouthを変更する

VRM1.0の仕様書のVRMC_vrm-1.0/expressions.ja.md の「プロシージャルのオーバーライド」を参照すると以下の記載があります。

つまり対象のExpressionの表情でOverride設定を指定する場合は、overrideBlink, overrideLookAt, overrideMouth それぞれに none, block, blend のいずれかを指定すれば良いことになります。

試しに改変前のgltfファイルのexpressions項目のエモート「笑顔」に該当するhappyを見てみると以下のようになっています。

      "expressions": {
        "preset": {
          "happy": {
            "morphTargetBinds": [
              {
                "node": 126,
                "index": 3,
                "weight": 1
              }
            ],
            "isBinary": false,
            "overrideBlink": "none",
            "overrideLookAt": "none",
            "overrideMouth": "none"
          },

overrideBlink, overrideLookAt, overrideMouth それぞれに none つまり「指定無し」になっているため、エモート「笑顔」中にまばたきがブロックされていなかったことがわかります。

今回はまばたきやリップシンクなどの表情切替をすべてブロックしたいので、overrideBlink, overrideLookAt, overrideMouthblock を指定します。

      "expressions": {
        "preset": {
          "happy": {
            "morphTargetBinds": [
              {
                "node": 126,
                "index": 3,
                "weight": 1
              }
            ],
            "isBinary": false,
            "overrideBlink": "block",
            "overrideLookAt": "block",
            "overrideMouth": "block"
          },

こうすることでエモート「笑顔」中は "overrideBlink": "block",によりまばたきが、"overrideMouth": "block"によりリップシンクがブロックされるので結果的に表情が崩れることを避けられます。

VRMに再エクスポートして確認する

このgltfファイルからエクスポートしてclusterで確認してみると、エモート「笑顔」中はまばたきが発生せずマイクに声を入れても口が変形しなくなっていることが確認できます。

他Expression (angry, sad, aa, ih, ou, etc..) も同様にoverrideBlink, overrideLookAt, overrideMouth を編集することでOverride設定の変更が可能です。

VSCodeとUnityのどっちがやりやすいかは人それぞれかと思いますがよかったらご活用ください!

おまけ: VSCodeでモデルをプレビューする

glTF Toolsにはモデルをプレビューする機能がついているので紹介します。

ボタンはgltfファイルを開いた状態のときにタブの右側に出現しています。

VRMのポーズを変えたりはできませんが、簡易的なモデルの確認に使えます。

雑感

去年に引き続きVRMを取り上げてみましたがいかがでしたでしょうか?

今年からクラスターでもVRM1.0アバターが使用できるようになりましたし、みなさんのVRMライフの一助になれば幸いです。

明日のクラスター Advent Calendar 2024の18日目は @MSA-iさんの記事になります。お楽しみに!

記事をご覧いただきありがとうございました!それでは~

【VRM, glTF】3Dアバターファイルフォーマット "VRM" の構造をのぞいてみよう

こちらは クラスター Advent Calendar 2023 の1ページ目の6日目の記事です!

qiita.com

前日は @neguse_kさんの「Blenderでポーズを作ってUnityに取り込む」でした!Blenderでポーズを作成されている方はこちらの記事を参考にぜひclusterにも組み込んでみてください!


こんにちは、すぎしーです。 クラスター株式会社のUnityエンジニアになってちょうど1年が経ちました。エンジニアとしてできることも増え、やりがいのある日々を送っています!

さて、本記事のテーマは「"VRM" の構造をのぞいてみよう」です。ガチな解説ではなくおおまかにVRMの中身をイメージできるぐらいで紹介したいと思います。

使用するツール

今回はUnityとUniVRMは使用しません!

使用するVRM

VRMについて

VRMとは、VRMコンソーシアムが提唱している人型アバターを定義するファイルフォーマットとなっていて、clusterでもアバターの形式として使用されています。 VRMに対応した様々なアプリケーションでアバターを表示することができます。

例: VMagicMirrorでアリシア・ソリッドちゃんのVRMを読み込んで表示

VRMアバターを作ったり使ったりするだけならいろいろなツールが提供されているので構造を知る必要はないんですが、知っておくとさらにVRMと仲良くなれるかも?

VRMはglTF-2.0がベースになっている

VRMコンソーシアムから公開されているVRM仕様を見ると以下のように記載されています。

glTF-2.0のバイナリ形式glbをベースにした、VR向けモデルフォーマットです。

つまり結論を言ってしまうと "VRMの構造" ≒ "glTF-2.0の構造" と言えます! ということでまずはglTF-2.0の構造について簡単に紹介します。

glTF-2.0 はバイナリとJSONの2つで構成されている

glTFは3Dコンテンツ向けのファイルフォーマットでKhronos Groupから提供されています。仕様書は glTF™ 2.0 Specificationにあります。glTFファイルには主に以下の情報が1ファイル内に格納できるように設計されています。

  • オブジェクトのヒエラルキー
  • メッシュ (法線やウェイトなども含む)
  • テクスチャ
  • マテリアル
  • etc...

glTFはバイナリファイルですが、中身は「ヘッダ領域」を除いて「JSON領域」と「バイナリ領域」のチャンクで構成されています。

AliciaSolid_vrm-0.51.vrmをHex Editorで見ると、JSON文字列が格納されていることが確認できます。

そして、ある境界からテキスト部分ではなくバイナリ形式で格納された領域を確認することができます。

ここでJSON領域とバイナリ領域を少し深堀りします。

JSON領域

JSON領域には以下のような情報が格納されています。

項目名 説明
buffer シンプルなバイナリ領域のアドレス情報。ほとんどのVRMは1つだけ持っている。
bufferView bufferをさらに区分けしたもの。
image あるbufferViewをどの画像形式でロードするかの情報を持つ。画像形式はmimeTypeで表現される。
accessor どのbufferViewをどうやって読み込むかの情報を持つ。
mesh メッシュを構成するための情報を持つ。プリミティブ(マテリアルを割り当てる単位)毎に頂点位置(POSITION)、法線(NORMAL)、UV座標(TEXCOORD_0)などをaccessorを介して取得する。
etc... -

例として、以下はAliciaSolid_vrm-0.51.vrmのノード情報(nodes)のJSONの一部を整形表示したものになります。

ノードは位置や回転のほかにメッシュの有無の情報を持ちます。

Unityを知っている方向けに説明すれば、ノードはGameObjectのようなものでボーンとしても活用されたり、meshがあればMeshRenderer、さらにskinがあればSkinnedMeshRendererを持つようなイメージとなります。

"nodes": [
  {
    "name": "mesh",
    "children": [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 ],
    "translation": [ 0, 0, 0 ],
    "rotation": [ 0, 0, 0, 1 ],
    "scale": [ 1, 1, 1 ],
    "extras": {}
  },
  {
    "name": "body_top",
    "translation": [0, 0, 0],
    "rotation": [0, 0, 0, 1],
    "scale": [1, 1, 1],
    "mesh": 0,
    "skin": 0,
    "extras": {}
  },
  // ...
  {
    "name": "Head",
    "children": [ 77, 78, 79, 80, 81, 89, 97, 99, 101, 103, 104, 108, 109, 113, 114 ],
    "translation": [ -9.235208e-9, 0.0388788, -0.00014353916 ],
    "rotation": [ 0, 0, 0, 1 ],
    "scale": [ 1, 1, 1 ],
    "extras": {}
  },

※↑のJSONは成形したもので、VRM内のJSON領域ではスペースや改行は基本的に除外されています

バイナリ領域

バイナリ領域(Binary Buffer)は何を表しているかというと、メッシュ頂点やテクスチャ、モーフがバイナリデータとして格納されています。

実はバイナリ領域単体では何のデータかはわかりません。バイナリ領域の読み込み方ですが、JSON項目のbuffer、bufferView + 各情報から確定 させる流れになります。

JSON項目からバイナリ領域を読み込む流れ

buffer

bufferはバイナリ領域の一番大きな単位です。以下のように、バイトの長さのみでとてもシンプルです。

前述したとおり、ほとんどのVRMは1つだけ持っています。

  "buffers": [
    {
      "byteLength": 7772784
    }
  ],

bufferView

bufferViewはbufferを区切ったもので、大雑把なイメージは以下です。

以下のJSONを例にするとbufferViewの0番目は「0番目のbufferのオフセット0から344698バイト」、bufferViewの1番目は「0番目のbufferのオフセット344698から136063バイト」、...のように表現されます。

  "bufferViews": [
    {
      "buffer": 0,
      "byteOffset": 0,
      "byteLength": 344698
    },
    {
      "buffer": 0,
      "byteOffset": 344698,
      "byteLength": 136063
    },
    {
      "buffer": 0,
      "byteOffset": 480761,
      "byteLength": 1708146
    },
    // ...

あくまでbufferを区切っただけなので、bufferView単体でも何のデータとして読めばいいかはわかりません。後述するimageやaccessorなどから活用方法を確定させることになります。

image

テクスチャに使用される画像ファイルはimage項目から参照できます。
例えば"Alicia_body"というテクスチャ画像は「bufferViewの0番目をpng形式でロードする」となります。

"bufferView": 0, "mimeType": "image\/png"の場合は、「bufferViewの0番目をpng画像として読み込む」という解釈になります。

  "images": [
    {
      "name": "Alicia_body",
      "bufferView": 0,
      "mimeType": "image\/png"
    },
    {
      "name": "Sphere",
      "bufferView": 1,
      "mimeType": "image\/png"
    },
    // ...

accessor

accessorは以下のように情報を扱います。

  • typeで整数(int) or 浮動小数点(float)を決定
  • componentTypeでScalar(1個毎) or Vector3(3個毎) or Matrix4x4(16個毎) or etcを決定

accessorのみでは明確な用途は不明なため、さらに別の項目から使用されます。

  "accessors": [
    {
      "bufferView": 7,
      "byteOffset": 0,
      "type": "VEC3",
      "componentType": 5126,
      "count": 4804,
      "max": [
        0.614650965,
        1.31239367,
        0.150282308
      ],
      "min": [
        -0.614748538,
        0.9991788,
        -0.0840013
      ],
      "normalized": false
    },
    {
      "bufferView": 8,
      "byteOffset": 0,
      "type": "VEC3",
      "componentType": 5126,
      "count": 4804,
      "normalized": false
    },
    // ...

mesh

primitivesのattributesとして頂点位置(POSITION)や法線(NORMAL)を持っており、それらの数値がaccessorのインデックス値を表しています。
"POSITION": 0 の場合は、「accessorsの0番目を頂点位置として使用する」という解釈になります。頂点位置なのでそのaccessorのtypeはVector3(VEC3)になっているはずです。

  "meshes": [
    {
      "name": "body_top.baked",
      "primitives": [
        {
          "mode": 4,
          "indices": 5,
          "attributes": {
            "POSITION": 0,
            "NORMAL": 1,
            "TEXCOORD_0": 2,
            "JOINTS_0": 4,
            "WEIGHTS_0": 3
          },
          "material": 0
        },
    // ...

VRMはglTF拡張にVRM特有の情報を持つ

glTFは前述のとおりJSON領域を持っていますが、このJSONに追加情報を格納しても良いことになっています。この追加情報のことをglTF拡張(glTF Extensions)と呼びます。

VRMはこのglTF拡張部分にVRM特有の情報を持たせることで実現されていて、VRM0.xの場合はextensions.VRM項目に格納されています。

以下はAliciaSolid_vrm-0.51.vrmから抽出した情報ですが、一部紹介します。

拡張項目名 説明
humanoid VRMヒューマノイド(人型)のボーン情報
secondaryAnimation VRMのSpringBoneの情報
etc... -
"extensions": {
  "VRM": {
    "exporterVersion": "UniVRM-0.51.0",
    "specVersion": "0.0",
    "meta": {
      "title": "Alicia Solid",
      "version": "1.10",
      "author": "© DWANGO Co., Ltd.",
      // ...
    },
    "humanoid": { // ...
    },
    "firstPerson": { // ...
    },
    "blendShapeMaster": { // ...
    },
    "secondaryAnimation": { // ...
    },
    "materialProperties": [
    ]
  }
  // ...

ということでVRMの構造を簡単に説明してみました。もう少しglTFに詳しくなりたいな~という方は gltf20-reference-guide.pdf が図もあったりして参考になるかと思います!

VRMはglTFの仕組みをうまく利用したファイルフォーマットだったんですね~。

おまけ

バイナリエディタVRMを編集してみよう

さて、glTFの大まかな構造が分かったところで Hex Editor でAliciaSolid_vrm-0.51.vrmを編集してみましょう!

今回は編集結果がはっきりとわかるようにマテリアルの色の乗算値を赤色に変更してみます。

VRM0.xのMToonの色の乗算値は extensions.VRM.materialProperties[*].vectorProperties._Color です。Hex Editorで文字列"_Color"を検索すると見つけられます。

乗算値は白([1,1,1,1])になっていようなので、置換処理で "_Color":[1,1,1,1],"_Color":[1,0,0,1],に変更保存して、VMagicMirrorで確認してみましょう。

  1. AliciaSolid_vrm-0.51.vrmからAliciaSolid_vrm-0.51_edited.vrmをコピーして作成
  2. Hex Editorを開く
  3. Ctrl+F → 置換モードに切替 → 置換を実施
  4. Ctrl+Sで保存する
  5. VMagicMirrorでAliciaSolid_vrm-0.51_edited.vrmをロード

以下が読み込んだ結果です。しっかり赤色になっていますね。

実際にバイナリエディタVRMを編集することはほとんどないと思いますが、「VRMの構造を知ってるとこんなこともできるよ~」という紹介でした。

VRM0.x と VRM1.0 について

まだclusterではVRM1.0に対応はしていませんが、せっかくなのでVRM1.0についても少し触れようと思います。

VRM1.0はVRM0.xよりさらにglTFに準拠したフォーマットになっています。具体的に言うと以下のような変更が入りました。

  • UniVRM以外の一般的なglTFライブラリでもロードしやすいようにbufferViewの扱いが変更された
  • glTF拡張での名前は extensions.VRMC_vrmextensions.VRMC_springBoneVRMC_materials_mtoonなどに変更された
  • マテリアルのパラメータはglTF標準のmaterials項目を使用するようになった
    • 色の乗算値を例にすると extensions.VRM.materialProperties[*].vectorProperties._Colormaterials[*].pbrMetallicRoughness.baseColorFactor のようにglTF標準のパラメータが使用されるようになった
    • Unity特有のシェーダープロパティ名は使用しなくなった

変更点の詳細については VRM-1.0の変更点 で確認できます。

VRM1.0はvrm-specificationにて仕様がより明確に記載され、MToonについても項目が定義されたりとUnity製アプリ以外でもVRMを利用できるように見直されています。

ハロクラで発表されているようにclusterもVRM1.0対応を進めていますため、リリースされたらぜひVRM1.0アバターでclusterを楽しんでください!

参考

雑感

VRMの構造について僕なりに紹介してみましたがいかがでしたでしょうか?
clusterのエンジニアになってプロダクト開発でもVRMに触れることも増えたので、せっかくなので知見の共有も兼ねて記事にしてみました。

VRMVRM Meetupも開催されたりと盛り上がりを見せているので、今後も注目していきたいです!

記事をご覧いただきありがとうございました!

明日のAdvent Calendar 2023 7日目は @uzzuさんの「UnityのPlay Asset DeliveryをtargetSdk34に対応させる」です。clusterのAndroidスペシャリストの記事をどうぞお楽しみに!(Play Asset Deliveryについてはuzzuさんにとても助けられました)

それでは~

UnityでMoqを使う (Unity2021バージョン)

こちらは クラスター Advent Calendar 2022(2ページ目)の17日目の記事です!

前日はスワンマンさん (@Swanman) の「Unityのエディタ拡張で動的にメニューを追加・削除する」でした!

まさかエンジニアではなくカスタマーサポートの方からReflectionを使ったツールの作り方を教えてもらえるとは!
Unity上でツールを作るときに知っておくと便利なテクニックになると思いますのでぜひ参考にしてください。


こんにちは、すぎしーです。 クラスター株式会社のUnityエンジニアをなりました!

改めてよろしくお願いします。

概要

今回の内容は2年前に書いた 「UnityでのMoq導入方法」のUnity2021版です。
この2年でUnityもMoqもアップデートされているので、導入も前回より内容を強化した方法で紹介します!

記事の最後の方に、導入までをある程度自動化した方法も載せておきます。

ソフトウェアエンジニア向け の記事になります。

変更履歴

  • 2022/12/18 .NET Framework向けの依存dllを追加 及び "Moq 4.18.2以上にする理由"の説明を一部修正

Moqとは

Moqとは C#(.Net) 向けのモックオブジェクト作成ライブラリです。

モッククラスはUnitTestなどで依存interfaceと同じふるまい(モック)になるクラスですが、自作で実装するのはなかなか骨が折れる作業になります。
そんなときにモックライブラリを用いることで簡単にモックオブジェクトを用意でき、より高度なUnitTest (クラスの単体テスト) が可能になります。


UnityでMoqを導入

※Unity2020でも可能と思いますが、Unity2021以上推奨です!

最初に手作業でのやり方紹介します。

1. MoqとCastle.Coreのnupkgをダウンロード

NuGetからnupkgをダウンロードします。

Moq 4.18.2以上にする理由は後述します。

ダウンロードはページ横の "Download package" から可能です。

2. nupkgを展開

nupkgの実態はzipなので7-zipなどで直接展開できます。
拡張子を .zip に変えてOS標準のzip展開でも可能です。

3. dll を Unityプロジェクト内に配置

展開したファイルのうち、以下のファイルをUnity以下に配置しましょう。
個人的なオススメのフォルダは Plugins/Moq です。

  • "Api Compatibility Level" が ".Net Standard 2.1"の場合
    • moq.4.18.3/lib/netstandard2.1/Moq.dll
    • castle.core.5.1.0/lib/netstandard2.1/Castle.Core.dll
    • system.diagnostics.eventlog.7.0.0/lib/netstandard2.0/System.Diagnostics.EventLog.dll
  • "Api Compatibility Level" が ".Net Framework"の場合
    • moq.4.18.3/lib/net462/Moq.dll
    • castle.core.5.1.0/lib/net462/Castle.Core.dll
    • system.threading.tasks.extensions.4.5.4/lib/net461/System.Threading.Tasks.Extensions.dll
    • system.runtime.compilerservices.unsafe.6.0.0/lib/netstandard2.0/System.Runtime.CompilerServices.Unsafe.dll

以下のように配置してください。

4. Unity上でdllをTestRunner向けに調整

Moqはあくまでテスト用なので、ビルドしたアプリには含まれないように設定しましょう。

※ビルドしたアプリに混ぜる場合は再頒布となるため各dllのライセンス表記が必要となります。

以下は Moq.dll, Castle.Core.dll, System.Diagnostics.EventLog.dll全てで実施してください。

  • Inspectorを表示
  • Auto Reference を無効化
  • Validate References を有効化
  • "Define Constraints" に UNITY_INCLUDE_TESTS を指定
  • "Apply" ボタンをクリック

特に UNITY_INCLUDE_TESTS を指定することでEditMode及びPlayMode Test Runnerで使用できる状態で、
ビルドしたアプリ本体にはMoqと依存dllが除外されます。

以上で導入は完了です!


UnityでMoqを使用

次は導入したMoqを使ってみましょう!

Unity Test Framework の詳細は省略します

テスト用Assembly Definitionを作成

  • テスト用スクリプトを配置したいフォルダで右クリック
  • Create -> Testing -> Tests Assembly Folder をクリック
  • Assembly名を指定してasmdefを作成

テスト用Assembly DefinitionにMoqの参照を追加

  • テスト用asmdefのInspectorを開く
  • "Assembly References"に Moq.dll を追加
    • Castle.Core.dllSystem.Diagnostics.EventLog の指定は基本的に不要 (テスト用スクリプトで直接参照することは稀なため)

テストを書いて実行

あとは普段どおりテストスクリプトを作成して、Test Runnerで実行するだけです!
Moqを使った簡易的なテストコードの例を載せておきます。

using System.Collections.Generic;
using NUnit.Framework;
using Moq;

public class TestFuncProxy
{
    public interface IFunc
    {
        bool Invoke(int number);
    }

    [Test]
    public void Invoke_ReturnsFalse_IfFuncReturnsFalse()
    {
        // Arrange
        var mock = new Mock<IFunc>();
        var target = new FuncProxy(mock.Object);

        // Note: Moqの仕様でSetupなしの場合はdefaultを返す (bool Invoke(...) の場合はfalse)
        // FYI: 実際のテストではテストパターンを明確にするために明示しましょう!

        // Act
        bool actual = target.Invoke(3);

        // Assert
        Assert.That(actual, Is.False);
    }

    [Test]
    public void Invoke_ReturnsTrue_IfFuncReturnsTrue()
    {
        // Arrange
        var mock = new Mock<IFunc>();
        var target = new FuncProxy(mock.Object);

        // Note: 引数3を渡されたらtrueを返す
        mock.Setup(m => m.Invoke(3)).Returns(true);

        // Act
        bool actual = target.Invoke(3);

        // Assert
        Assert.That(actual, Is.True);
    }
}

以下は実行結果です。

使用方法の紹介は以上です!

Moqの使用例

Moqでできることをちょっと紹介します!

SetupSequenceでコールごとの挙動を指定

SetupSequence で指定するとコールごとの戻り値を指定できます。

    [Test]
    public void Example_SetupSequence()
    {
        var mock = new Mock<IFunc>();

        // 渡された引数に関係なくfalse -> true -> false -> throw Exception
        mock.SetupSequence(m => m.Invoke(It.IsAny<int>()))
            .Returns(false)
            .Returns(true)
            .Returns(false)
            .Throws(new System.Exception("Unexpected Call"));

        Assert.That(mock.Object.Invoke(default), Is.False);
        Assert.That(mock.Object.Invoke(default), Is.True);
        Assert.That(mock.Object.Invoke(default), Is.False);
        Assert.Throws<System.Exception>(() => mock.Object.Invoke(default));
    }

コール時の引数と回数の検査

Verify を使用するとコールされたときの引数やその引数でのコール回数を検査することができます。
Moqを使う場合は一番使う機能ではないかと!

    [Test]
    public void Example_Verify()
    {
        var mock = new Mock<IFunc>();

        mock.Object.Invoke(2);
        mock.Object.Invoke(5);
        mock.Object.Invoke(2);

        // 引数2で2回コールされたことの検証
        mock.Verify(m => m.Invoke(2), Times.Exactly(2));

        // 引数関係なく3回以上コールされたことの検査
        mock.Verify(m => m.Invoke(It.IsAny<int>()), Times.AtLeast(3));
    }

不正の場合は例外(MockException)が出て、テストが失敗します。

コール時の処理を設定

Callback を使用するとコールされたときの処理を設定できます。
複数オブジェクトのコールされた順番を検査するときなどに利用できます。

    [Test]
    public void Example_Callback()
    {
        var messageList = new List<string>();

        var mock1 = new Mock<IFunc>();
        var mock2 = new Mock<IFunc>();
        var mock3 = new Mock<IFunc>();

        // コールされたら messageList に文字列を追加
        mock1.Setup(m => m.Invoke(It.IsAny<int>())).Callback(() => messageList.Add("From 1"));
        mock2.Setup(m => m.Invoke(It.IsAny<int>())).Callback(() => messageList.Add("From 2"));
        mock3.Setup(m => m.Invoke(It.IsAny<int>())).Callback(() => messageList.Add("From 3"));

        mock3.Object.Invoke(0);
        mock1.Object.Invoke(0);
        mock2.Object.Invoke(0);
        mock1.Object.Invoke(0);

        // 合計のコール回数 及び コールされた順番を検査
        Assert.That(messageList.Count, Is.EqualTo(4));
        Assert.That(messageList[0], Is.EqualTo("From 3"));
        Assert.That(messageList[1], Is.EqualTo("From 1"));
        Assert.That(messageList[2], Is.EqualTo("From 2"));
        Assert.That(messageList[3], Is.EqualTo("From 1"));
    }


モックライブラリを使用するメリット

改めてモックライブラリを使うメリットを紹介します。

モックオブジェクトを簡単に生成可能

これまで説明した通りですが、モッククラスを独自に実装する必要がなくなります。

interface を使ったモッククラスを自作する場合は結構な行数を書くことになり、何より保守コストが発生して大変です。
テストの品質を考える場合はモッククラスのテストも必要になってきます。

ちなみにちょっと実装してみましたが、実際に自作する場合はもっと機能が必要になっていきます。

using System.Linq;
...
    public class MyMockFunc : IFunc
    {
        // コールされた戻り値の設定 (直近のコールのみ)
        public bool RetNumber { get; set; }

        // コールされたときの引数の格納用リスト
        public List<int> CallHistory = new();

        // 対象の引数でコールされた回数の検査
        public bool Verify(int targetNumber, int expected)
            => CallHistory.Where(number => number == targetNumber).Count() == expected;

        // メソッドをコールされたときの処理
        public bool Invoke(int number)
        {
            CallHistory.Add(number);
            return RetNumber;
        }
    }

特別な事情がない限り、早めにモックライブラリを導入しておくとUnitTestが億劫にならなくて良いかと思います。

IDEのリファレンス検索に余計な候補がでない

モックライブラリ使う場合はモッククラスを実装するわけではないので、IDEのinterface継承クラスの検索結果にモッククラスが並びません。

以下はモッククラスが定義されたプロジェクトでinterface継承クラスの検索結果イメージです。

(interfaceがGeneric型だった場合はさらに大変なことに)

高度な検証がより簡単に実現可能

「Moqの使用例」で紹介した通り、モックライブラリを使用すると複雑な検証もやりやすくなります。

  • 特定の引数で依存クラスのメソッドをN回コールすること
  • 依存クラスが OperationCanceledException を返すときにはエラーにならないこと
  • etc...


モックライブラリの導入はUnitTest自体の敷居を下げることができるので、是非活用してみてください。


簡易導入方法

以下にある程度自動化した方法を紹介しています。

github.com

オススメは実施環境に依存しない 「GitHub Acrtionsを使用する場合」 です! (Actionsのyaml設定ファイルも作成済みです)

生成されたフォルダをそのまま Assets 以下に配置すれば使用できる状態になっています。


余談

Moq 4.18.2以上にする理由

理由は".Net Standard 2.1"の依存するライブラリが削減されており、導入がより簡単になるためです。

実は4.18.1以前では System.Threading.Tasks.Extensions とその依存 System.Runtime.CompilerServices.Unsafe も一緒に入れる必要がありましたが、 4.18.2で依存が削除されました。

Removed dependency on System.Threading.Tasks.Extensions for netstandard2.1 and net6.0 (@tibel, #1274)

moq4/CHANGELOG.md at main · moq/moq4 · GitHub

追記

".NET Framework 4.x"では System.Threading.Tasks.Extensions の依存は残っているため引き続き必要となるようです。
簡易導入方法で紹介しているツールも修正済みです。


雑感

実装したクラスすべてにUnitTestが必要になるわけでは有りませんが、
恒久的に機能を保証したい場合などは強力な武器になるので、Unity開発でもMoqを活用してみてください。

クラスター株式会社にジョインして業務にも慣れてきましたが、
エンジニアに限らず様々な分野のスペシャリストやジェネラリストの方がいて、刺激的な日々を送っています。

これからもバーチャルにのめり込んでいきます!


クラスター Advent Calendar 2022 明日の記事の紹介

明日は Soraさん (@BlueRose_Sora) の「Tips:clusterで大規模な展示会をする」です!

お楽しみに!

【GitHub Actions】Composite ActionのTipsと注意点

概要

今回はGitHub Actionsの機能の一つである "Composite Action" について紹介します。

今回の記事は、GitHub Actionsに多少知見がある人向けの記事になります。

Composite Actionはいわゆる再利用性のあるステップをyamlファイルに集約して再利用可能にする機能です。
テンプレート的な機能、もしくはプログラミングにおける関数的なものと考えてもらっても良いと思います。

docs.github.com

Composite Actionは便利ですが、注意点もあるため紹介しようと思います。

ついでにPrivate Action (Privateなリポジトリに作成したAction) の使用方法も合わせて紹介します。

記事の最後にサンプルリポジトリも記載しておきます。

動作環境

用語

  • Public Action
    • PublicなリポジトリにあるAction (例、 actions/checkout, actions/upload-artifactなど)
  • Private Action

使用するプロジェクト

本題では有りませんが、ビルドのサンプル用に以下を入れています。

  • C# プロジェクトのビルド用サンプルプロジェクト
    • Lottery という実行するたびにtrue or falseを返すだけのプログラム
    • LotteryTests はUnitTest

Composite Actions の実装

Composite Actions を組み込むワークフロー

まずは Composite Actionなしのworkflowを例にしたいと思います。

# .github/workflows/build-dotnet-without-composite-actions.yml
name: "Build Dotnet without Composite Actions"

on:
  workflow_dispatch: {}

jobs:
  build:
    name: Build
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
        with:
          lfs: true

      - uses: actions/cache@v3
        with:
          path: ./Lottery/obj
          key: dotnet-${{ runner.os }}-${{ github.ref_name }}

      - uses: actions/setup-dotnet@v2
        with:
          dotnet-version: '6.0.x'
          include-prerelease: false

      - name: Restore Packages
        shell: bash
        run: dotnet restore ./GitHubActionsTestbed.sln

      - name: Build Projects
        shell: bash
        run: dotnet build ./GitHubActionsTestbed.sln --configuration Release

      - name: Test Projects
        shell: bash
        run: dotnet test ./GitHubActionsTestbed.sln --blame

      - uses: actions/upload-artifact@v3
        with:
          name: Lottery
          path: ./Lottery/bin/Release/net6.0
          retention-days: 3

ワークフローの詳細

  • リポジトリのcheckout (actions/checkout)
  • .Net 6.0のビルド環境の構築 (actions/setup-dotnet)
  • dotnet コマンドを使ったビルド (パッケージの取得、テストを含む)
  • Artifactとしてアップロード

実行結果は以下です。

https://github.com/tsgcpp/GitHubActionsTestbed/actions/runs/3117273807

Composite Actions 対応

Composite Actionのファイル構成

以下のようなファイル構成を取ります。

<path to action>/<composite action name>/action.yml

<composite action name> はフォルダで、 ステップは action.yml に定義します。
フォルダ名はステップの流れがわかる名前にすると良いです

例えば、「.Netのビルドの一連の流れを集約」するComposite Actionを作りたい場合は以下のようにします。

.github/composite/dotnet-build/action.yml

自分はリポジトリ専用のComposite Actionは .github/composite に置くようにしていますが、 別に.github/composite 以下でなくとも問題ありません。

そして呼び出すときは以下のように uses にフォルダを指定します

- uses: ./.github/composite/dotnet-build

後述しますが、with により入力(inputs)を与えることも可能です。

      - uses: ./.github/composite/upload-artifact
        with:
          name: Lottery
          path: ./Lottery/bin/Release/net6.0

Composite Actionの組込方針

Composite Action は個人的には以下の活用方法があると考えています。

  • 複数のステップを1つに集約
  • 入力のデフォルト値を独自に定義

.Netのビルドの一連の流れを集約 (複数のステップを1つに集約)

.NetのビルドをComposite Action対応します。
フォルダ構成は固定として入力(inputs)はありません。

# .github/composite/dotnet-build/action.yml
name: 'Dotnet Build'
description: 'Restore packages, Build and Test'
runs:
  using: "composite"
  steps:
    - uses: actions/setup-dotnet@v2
      with:
        dotnet-version: '6.0.x'
        include-prerelease: false

    - name: Restore Packages
      shell: bash
      run: dotnet restore ./GitHubActionsTestbed.sln

    - name: Build Projects
      shell: bash
      run: dotnet build ./GitHubActionsTestbed.sln --configuration Release

    - name: Test Projects
      shell: bash
      run: dotnet test ./GitHubActionsTestbed.sln --blame

upload-artifactの有効日数3日をデフォルト化 (入力のデフォルト値を独自に定義)

公式の actions/upload-artifact@v3 ですが、デフォルトが90日となかなか長いです。

Composite Actionsは独自の入力 (inputs) を定義することが可能です。

ArtifactはPrivateなリポジトリの場合、使いすぎると従量課金の対象となるためデフォルトで3日ぐらいにしたい場合などは、
Composite Actionの inputs を使用することで独自のデフォルト値を定義できます。

# .github/composite/upload-artifact/action.yml
name: 'Upload Artifact'
description: 'An action to create a artifact'
inputs:
  name:
    required: true
    default: 'Artifact'
  path:
    required: true
  retention-days:
    required: false
    default: 3
runs:
  using: "composite"
  steps:
    - uses: actions/upload-artifact@v3
      with:
        name: ${{ inputs.name }}
        path: ${{ inputs.path }}
        retention-days: ${{ inputs.retention-days }}
  • name (Artifact名)のデフォルトを"Artifact"
  • retention-days (有効期限)をデフォルトを3 (3日)
  • path (対象のファイル群)はデフォルトなしで指定を必須化

required 一応指定しておきましょう。(ただ、個人的にはComposite Actionだと微妙にrequired機能していない印象です)

Composite Action を使用

改めて「Composite Actions を使用していないワークフロー」を改修したいと思います。

# .github/workflows/build-dotnet.yml
name: "Build Dotnet"

on:
  workflow_dispatch: {}

jobs:
  build:
    name: Build
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
        with:
          lfs: true

      - uses: actions/cache@v3
        with:
          path: ./Lottery/obj
          key: dotnet-${{ runner.os }}-${{ github.ref_name }}

      - uses: ./.github/composite/dotnet-build

      - uses: ./.github/composite/upload-artifact
        with:
          name: Lottery
          path: ./Lottery/bin/Release/net6.0

上記のようにビルドの流れがスッキリした見た目になりました。
また、upload-artifact は有効期限を指定していなくてもデフォルトの3日が設定されるようになっています。

実行結果は以下です。

https://github.com/tsgcpp/GitHubActionsTestbed/actions/runs/3117435297

Composite Actionの細かな仕様

以下に記載されています。

github.com

デフォルトのshellは指定できないなど、細かい仕様が書いてあります。

Actionの補足

通常のActionとComposite Actionは構成自体は同じ

実はComposite Actionのファイル構成 (<path to action>/<composite action name>/action.yml) ですが、
特殊に見えて、実は通常のActionと同じ構成になっています。

例えば、公式の actions/checkout のルートのファイルを見ると action.ymlが存在しています。

github.com

つまりGitHub Actionsで使用されるActionは、必ずaction.ymlを持ったファイル群となっています。

Actionはcheckoutしてからフォルダを指定しても実行可能

実はActionは特定のフォルダにcheckoutして、usesに指定しても使用可能です。

例えば actions/upload-artifactは一旦 ./.github/repos/actions/upload-artifactというフォルダにcheckoutして、
usesでそのフォルダを指定する形をとっても、同様の機能を得ることができます。

      - uses: actions/upload-artifact@v3
        with:
          name: Lottery
          path: ./Lottery/bin/Release/net6.0
          retention-days: 3

      - uses: actions/checkout@v3
        with:
          repository: 'actions/upload-artifact'
          ref: v3.1.0
          path: ./.github/repos/actions/upload-artifact

      - uses: ./.github/repos/actions/upload-artifact
        with:
          name: Lottery
          path: ./Lottery/bin/Release/net6.0
          retention-days: 3

PrivateリポジトリのActionもcheckoutしてフォルダを指定すれば実行可能

前項と同じ原理でPrivateリポジトリもcheckoutして実行が可能です。

社内専用のActionを作って使用したい場合などにご活用ください。

      - name: Checkout tsgcpp/upload-artifact-private
        uses: actions/checkout@v3
        with:
          # actions/upload-artifact をコピーしてPrivate化したリポジトリ
          repository: 'tsgcpp/upload-artifact-private'
          ref: main
          path: ./.github/repos/tsgcpp/upload-artifact-private
          token: ${{ secrets.PAT_TOKEN }}

      - uses: ./.github/repos/tsgcpp/upload-artifact-private
        with:
          name: Lottery
          path: ./Lottery/bin/Release/net6.0
          retention-days: 3

対象のリポジトリにアクセス可能なPersonal Access Tokenを作成してsecretsに登録して使用する必要があるなど、多少手間があります。

Private Actionを直接 uses に指定できない理由

GitHub Actionsのワークフローでデフォルトで発行される GITHUB_TOKEN があるのですが、
GITHUB_TOKEN は ワークフローを実行したリポジトリのみアクセス可能なトークなので他のリポジトリにはアクセスできません。

そのため、Private Actionの場合はアクセス可能なトークンを使ってcheckoutしてから、usesに指定する必要があります。

GitHub様、Private Actionに特化したトークンの機能つくってほしいなー

ダウンロード済みのActionは再利用される

全く同じバージョンやSHAのActionがダウンロード済みの場合は、ダウンロード済みのものが再利用されます。

ダウンロード済みのActionはComposite Actionなどの外部yamlでも共有されます

    - name: Cache actions/cache
      uses: actions/checkout@v3
      with:
        repository: 'actions/cache'
        ref: v3.0.8
        path: ${{ inputs.pathRoot }}/actions/cache

    - uses: ./.github/composite/checkout-actions
# .github/composite/checkout-actions
    # Compsite Action側
    - name: Cache actions/upload-artifact
      uses: actions/checkout@v3  # ダウンロード済みの `actions/checkout` を使用
      with:
        repository: 'actions/upload-artifact'
        ref: v3.1.0
        path: ${{ inputs.pathRoot }}/actions/upload-artifact

ちなみにデバッグモードを有効化すると、以下のログで再利用されていることが確認できます。

Getting action download info
##[debug]Action 'actions/upload-artifact@v3' already downloaded at '/home/runner/work/_actions/actions/upload-artifact/v3'.

https://github.com/tsgcpp/GitHubActionsTestbed/actions/runs/3117276928/jobs/5055819436#step:7:9

Composite Actionの注意点

Composite Action自体のcheckoutが必要

ワークフロー実行時はリポジトリの内容はcheckoutされていません。
Composite Actionは外部yamlに定義する関係であらかじめcheckoutで他ソースと一緒に取得する必要があります。

...
    steps:
      - uses: actions/checkout@v3
        with:
          lfs: true
...
      # actions/checkoutで取得したComposite Actionを使用
      - uses: ./.github/composite/dotnet-build

Publicリポジトリの場合はリポジトリ指定で実行可能

Publicなリポジトリに配置されたComposite Actionであれば、以下の様に指定できます。

      - uses: <org>/<repository>/<path to action directory>@<ref(tag or branch)>

以下は指定例です。

      - uses: tsgcpp/GitHubActionsTestbed/.github/composite/dotnet-build@main

ワークフロー本体のyamlusesで指定されたActionは事前ダウンロードされる

ワークフロー本体のyaml内の uses に定義したActionですが、 ワークフローの最初 (Set up job) で事前ダウンロードされます。

こちらもデバッグモードを有効化すると確認できます。

Getting action download info
Download action repository 'actions/upload-artifact@v3' (SHA:3cea5372237819ed00197afe530f5a7ea3e805c8)
##[debug]Download 'https://api.github.com/repos/actions/upload-artifact/tarball/3cea5372237819ed00197afe530f5a7ea3e805c8' to '/home/runner/work/_actions/_temp_e6a3dde4-ca19-4d00-af3a-9a6c772ea0ec/241095c2-ea32-481b-83fe-d1b6af6915ac.tar.gz'

https://github.com/tsgcpp/GitHubActionsTestbed/actions/runs/3117276928/jobs/5055819436#step:1:45

外部yamlに定義されたActionはステップ実行時に遅延ダウンロードされる

本記事の本題といっても過言ではありません!

Composite Actionを含む外部yaml内のActionは実行されるタイミングでダウンロードされます!

つまり、外部yamlのActionは遅延処理的な性質があります

.github/workflows/build-dotnet.yml を例に取ると

  • actions/cacheはワークフローのはじめにダウンロードされる
    • ワークフロー本体のyaml内で定義されているため
  • actions/setup-dotnetactions/upload-artifactは各ステップ実行時にダウンロードされる
    • Composite Actionのyaml内に定義されているため
# withは省略
      - uses: actions/cache@v3
...
      # 内部で uses: actions/setup-dotnet@v2
      - uses: ./.github/composite/dotnet-build

      # 内部で uses: actions/upload-artifact@v3
      - uses: ./.github/composite/upload-artifact
...

Actionのログを見てみると、Set up jobactions/cacheはダウンロードされていますが、 actions/setup-dotnetactions/upload-artifactはダウンロードされていないことがわかります。

actions/setup-dotnetactions/upload-artifactは各種ステップの実行時にダウンロードされています。

ログの全体は以下です。

github.com

遅延ダウンロードの何が問題なのか?

「大した問題じゃなくね?」って思った方もいると思いますし、実際大した問題にならないパターンも多いです。

問題になりやすい例として、完了に長時間を要するワークフローがあります

例えば以下のようなワークフローです。

  • 5時間かかるアプリのビルド実行
  • ビルド完了後に Composite Actionを使ってアプリをストアへアップロード
    • Composite Action内でアプリのストアアップロード用Actionを取得して使用

ワークフロー開始時にはGitHubは正常だったのに、
5時間後のビルド時にGitHubAPIが一部死んでいてストア用のActionのダウンロード(checkout)が失敗してビルドがパーになっちゃうパターンです。

ストア側のAPIは問題がなかった場合、予めストア用のActionをダウンロードできていれば回避できた問題ですね。。。

昨今クラウドベンダー(AWSなど)の一時インスタンスでビルドすることも多くなっていて、ビルド成果物をどこかに退避していないとサルベージも困難だったりします。

対策1 あらかじめ使用するActionすべてのcheckoutを済ませる (オススメ)

事前ダウンロードされてないなら、明示的に事前ダウンロードしてしまおうという発想です。

1例として、以下のようなセットアップ用Composite Actionを用いる方法があります。

name: 'Set Up Actions'
inputs:
  pathRoot:
    required: true
    description: 'Relative path the actions will be into'
    default: ./.github/repos
  patToken:
    required: true
    description: 'GitHub Personal Access Token to checkout private repositories.'
runs:
  using: "composite"
  steps:
    - name: Cache actions/checkout
      uses: actions/checkout@v3
      with:
        repository: 'actions/checkout'
        ref: v3.0.2
        path: actions/checkout@v3

    - name: Cache actions/cache
      uses: actions/checkout@v3
      with:
        repository: 'actions/cache'
        ref: v3.0.8
        path: ${{ inputs.pathRoot }}/actions/cache

    - name: Cache actions/upload-artifact
      uses: actions/checkout@v3
      with:
        repository: 'actions/upload-artifact'
        ref: v3.1.0
        path: ${{ inputs.pathRoot }}/actions/upload-artifact

    - name: Cache actions/download-artifact
      uses: actions/checkout@v3
      with:
        repository: 'actions/download-artifact'
        ref: v3.0.0
        path: ${{ inputs.pathRoot }}/actions/download-artifact

    - name: Cache actions/setup-dotnet
      uses: actions/checkout@v3
      with:
        repository: 'actions/setup-dotnet'
        ref: v2.1.0
        path: ${{ inputs.pathRoot }}/actions/setup-dotnet

    - name: Checkout tsgcpp/upload-artifact-private
      uses: actions/checkout@v3
      with:
        # Same with actions/upload-artifact
        repository: 'tsgcpp/upload-artifact-private'
        ref: main
        path: ${{ inputs.pathRoot }}/tsgcpp/upload-artifact-private
        token: ${{ inputs.patToken }}

後は、usesにダウンロード済みのActionを指定するだけです。

name: "Build Dotnet with Set Up Actions"

on:
  workflow_dispatch: {}

jobs:
  build:
    name: Build
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
        with:
          lfs: true

      - uses: ./.github/composite/setup-actions
        with:
          patToken: ${{ secrets.PAT_TOKEN }}

      - uses: actions/cache@v3
        with:
          path: ./Lottery/obj
          key: dotnet-${{ runner.os }}-${{ github.ref_name }}

      - uses: ./.github/composite/dotnet-build-with-setup-actions

      - uses: ./.github/composite/upload-artifact-with-setup-actions
        with:
          name: Lottery
          path: ./Lottery/bin/Release/net6.0
# .github/composite/dotnet-build-with-setup-actions/action.yml
name: 'Dotnet Build with Set Up Actions'
description: 'Restore packages, Build and Test'
runs:
  using: "composite"
  steps:
    - uses: ./.github/repos/actions/setup-dotnet
      with:
        dotnet-version: '6.0.x'
        include-prerelease: false

    - name: Restore Packages
      shell: bash
      run: dotnet restore ./GitHubActionsTestbed.sln

    - name: Build Projects
      shell: bash
      run: dotnet build ./GitHubActionsTestbed.sln --configuration Release

    - name: Test Projects
      shell: bash
      run: dotnet test ./GitHubActionsTestbed.sln --blame
# .github/composite/upload-artifact-with-setup-actions/action.yml
name: 'Upload Artifact with Set Up Actions'
description: 'An action to create a artifact'
inputs:
  name:
    required: true
    default: 'Artifact'
  path:
    required: true
  retention-days:
    required: false
    default: 3
runs:
  using: "composite"
  steps:
    - uses: ./.github/repos/actions/upload-artifact
      with:
        name: ${{ inputs.name }}
        path: ${{ inputs.path }}
        retention-days: ${{ inputs.retention-days }}

このやり方の利点は以下があると思っています。

  • 複数のWorkflow間で使用するActionのバージョンを統一できる
    • 特に同じActionを使う場合でも、v2とv3で指定を間違えるなども回避しやすい
  • Public Action, Private Actionどちらも使用時の uses への指定方法が統一される
    • どちらもダウンロード済みのActionになっているため

対策2 usesで使用するActionを宣言 (非推奨)

「ダウンロード済みのActionは再利用される」の性質を利用したやり方ですね。

ただ、このやり方は公式ドキュメントにはないやり方で、仕様の裏をついたやり方なので非推奨です。
また、usesで宣言した限りステップ自体は実行されてしまうため、Actionによっては予期しない副作用が発生する可能性もあります。

事前ダウンロード時に失敗してもワークフローを継続させるために continue-on-error: true を宣言しています。

jobs:
  build:
    name: Build
    runs-on: ubuntu-latest
    steps:
      # actions/upload-artifactを事前ダウンロードさせる
      - uses: actions/upload-artifact@v3
        continue-on-error: true

      # actions/setup-dotnetを事前ダウンロードさせる
      - uses: actions/setup-dotnet@v2
        continue-on-error: true
        with:
          dotnet-version: '6.0.x'
          include-prerelease: false

      - uses: actions/checkout@v3
        with:
          lfs: true

      - uses: actions/cache@v3
        with:
          path: ./Lottery/obj
          key: dotnet-${{ runner.os }}-${{ github.ref_name }}

      # ダウンロード済みのactions/setup-dotnetが使用される
      - uses: ./.github/composite/dotnet-build

      # ダウンロード済みのactions/upload-artifactが使用される
      - uses: ./.github/composite/upload-artifact
        with:
          name: Lottery
          path: ./Lottery/bin/Release/net6.0

GitHub Actions側に事前ダウンロード機能として、usesの pre-download的なオプションを要望として出しても良さそうな気はしてます。

Reusing Workflows との違い

GitHub ActionsにはReusing Workflowsという機能があります。
こちらは名前の通りワークフロー全体を再利用する形になります。

docs.github.com

一方でComposite Actionは数ステップを集約して、ワークフロー(ジョブ)にステップとして組み込む機能になります。
もしワークフロー全体を再利用する場合は、Composite ActionではなくReusing Workflowsのほうが最適と言えます。

余談、筆者が遭遇した事象

「外部yamlに定義されたActionはステップ実行時に遅延ダウンロードされる」の仕様を認識するきっかけになった事象がありました。

Composite Actionを使ったCIのワークフローを運用していて、半年以上問題が発生していなかったのですが、
ある日大事な提出でビルドマシンがいつも以上にガンガン回っているときでした。

初回のcheckoutは問題なく実行されましたが、数時間のビルドを終わらせた後のComposite Action内でActionのダウンロード(checkout)が発生したとき、
なぜか急にUnauthorizedになってcheckout不可になる現象が発生するようになりました。

checkout対象はPublic Actionの uses: actions/upload-artifact だったので、なぜUnauthorizedになったのかは本当に謎でした。。。
ただ、今回の現象に関わらず外部APIにアクセスできなくなる可能性は十分に考えられます。

一番の問題は失敗時の時間的損失が大きかったことのため、
外部APIなどが関係するステップをワークフローの初回に集約し、失敗しても時間的損失を極力回避できるように組み直しました

今回紹介した setup-actions がその一例となります。

完全なネットワーク障害などに対応しきれるわけではありませんが、あらかじめActionをダウンロードしておくことは一部ワークフローでは有効かと思いますため、
良かったら参考にしていただければと!

サンプルプロジェクト

github.com

雑感

直近はかなりドタバタしていたので、これまた久々の記事です。。。

最近はXR、非XRに限らずインフラはやはり重要だなと痛烈に感じています。
XRアプリの開発もどんどん規模が大きくなっているため、開発基盤の重要性もかなり上がっています。

Unityに限らずAndroid, iOS, Dockerを含むサーバーサイドのCI/CDを経験してきた身としては、
インフラをもっと強化していきたいと常に考えるようになりました。

そういえば「すぎしーのXRと3DCG」というブログ名ですが、そろそろ改名を考えています。
XR開発はインタラクションやグラフィックスももちろん重要ですが、それに負けないくらいクリエイターが開発に注力できる環境を用意することも大事だと思います。

これからもよろしくです!

それでは~

【Unity Localization】 GCPのサービスアカウントでプライベートSheetsと連携 (Cronジョブ対応含む)

概要

今回も前回に引き続きUnity Localizationに関するTipsです。
GCPのサービスアカウントでプライベートSheetsと連携する方法を紹介します。

Unity Localization標準のOAuth認証を使用すれば一応プライベートSheetsにアクセスすることは可能ですが、Cronジョブなどで定期更新に対応したいときに不都合があります。

そんな問題をGCPのサービスアカウントを用いて解決したいと思います!

おまけでCronジョブでGoogle SheetsからStringTableCollectionを定期更新するTipsも紹介します。

前回の記事

良かったら合わせてどうぞ!

tsgcpp.hateblo.jp

動作環境

  • Unity 2021.3.6f1
    • Unity 2020 でも可
  • Unity Localization 1.3.2

備考

今回紹介する機能は拡張パッケージとしても用意しています。

記事の最後にGitHubへのリンクを記載していますため、よろしければどうぞ。

GCPのサービスアカウント対応手順

サービスアカウントの作成

  • GCPのCredentialsページにてサービスアカウントを作成
    • GCP自体のアカウント作成などは各自調べてください

console.cloud.google.com

サービスアカウントの認証用JSON形式の鍵をダウンロード

以下のようなJSON形式の鍵がダウンロード出来ます。
JSON文字列は後述するクラスに渡すために使用します。

{
  "type": "service_account",
  "project_id": "xxx-workspace",
  "private_key_id": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "private_key": "-----BEGIN PRIVATE KEY-----\n...",
  "client_email": "[email protected]",
  "client_id": "121212121212121212121",
  "auth_uri": "https://accounts.google.com/o/oauth2/auth",
  "token_uri": "https://oauth2.googleapis.com/token",
  "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
  "client_x509_cert_url": "..."
}

※アクセス権限に使用する鍵のため、厳重に管理してください

対象のSheetsにサービスアカウントのアクセス権限を付与

Pullのみの場合は閲覧権限、Pushも行う場合は編集権限をサービスアカウントに与えてください。

GCPのサービスアカウント向けSheetsServiceProviderを作成

1.3.2時点のUnity LocalizationにはGCPのサービスアカウント連携用クラスが用意されていなかったため用意します。
いずれ公式から提供される気がしますが、今回は実装します。

    public class ServiceAccountSheetsServiceProvider : IGoogleSheetsService
    {
        private readonly string _serviceAccountKeyJson;
        private readonly string _applicationName;

        public ServiceAccountSheetsServiceProvider(
            string serviceAccountKeyJson,
            string applicationName)
        {
            _serviceAccountKeyJson = serviceAccountKeyJson;
            _applicationName = applicationName;
        }

        public SheetsService Service => GetSheetsService();

        private SheetsService GetSheetsService()
        {
            var credential = GoogleCredential.FromJson(_serviceAccountKeyJson);
            var initializer = new BaseClientService.Initializer
            {
                HttpClientInitializer = credential,
                ApplicationName = _applicationName,
            };
            var sheetsService = new SheetsService(initializer);
            return sheetsService;
        }
    }
  • serviceAccountKeyJson には、先程作成したサービスアカウントのJSON文字列を渡す
  • applicationName には GoogleSheetsService (ScriptableObject) に設定した ApplicationName を指定

GCPのサービスアカウント向けSheetsServiceProviderを使用してPull or Push

  • 先程作成した ServiceAccountSheetsServiceProvider を用いてPull or Pushを実施
    • ※以下の実装は、前回の記事で紹介したコードをServiceAccountSheetsServiceProviderに入れ替えたもの
            // 対象のStringTableCollectionを取得
            var collection = AssetDatabase.LoadAssetAtPath<StringTableCollection>("Assets/<path to StringTableCollection>");

            // GoogleSheetsExtensionをStringTableCollectionから取得
            var sheetsExtension = collection.Extensions.OfType<GoogleSheetsExtension>().FirstOrDefault();

            // *前回の記事と異なり、ここでServiceAccountSheetsServiceProviderを使用
            var serviceProvider = new ServiceAccountSheetsServiceProvider(
                serviceAccountKeyJson: "<GCPサービスアカウントのJSON形式の鍵の文字列>",
                applicationName: "<GoogleSheetsService (ScriptableObject) に設定したApplicationName>");

            // Google Sheetsアクセス用インスタンスを生成
            var sheets = new GoogleSheets(serviceProvider);

            // ※必ずSpreadSheetIdをGoogleSheetsインスタンスに指定してください!
            sheets.SpreadSheetId = sheetsExtension.SpreadsheetId;

            // 対象のStringTableCollection内の全言語(全Locale)のPullを実施
            sheets.PullIntoStringTableCollection(
                sheetId: sheetsExtension.SheetId,
                collection: collection,
                columnMapping: sheetsExtension.Columns);

上記の方法を使用することで、GCPのサービスアカウントでPull及びPushが可能となります。

以上がGCPのサービスアカウントを使用した連携方法となります。


CronによるGoogle SheetsからのStringTableCollectionの定期更新

さて、今回の実質的な本題に移りたいと思います!
今回、GCPサービスアカウントを用いた本当の理由はCron(定期ジョブ)を使ったStringTableCollection更新に対応させるためです!

そもそもなぜOAuthが使えないのか

Cron対応なしで運用する場合はそもそもGCPサービスアカウント対応は不要です。
概要で少し触れましたがプライベートのSheetsには標準のOAuth認証でも連携可能です。

Unity Localization標準のOAuthの問題は、主に以下の2点です。

  • 認証にUnityエディタ上でのUI操作が必要
    • batchモード(CLI)での認証が困難
  • 認証後に生成される認証ファイルが Library/Google/GoogleSheetsService 以下で管理
    • LibraryフォルダはUnityにおけるキャッシュフォルダでもあるため、クリーンビルドを行うCIなどと相性が悪い

というわけで、
batchモードで認証を完結させるために、JSON形式の鍵文字列を渡すだけで認証が可能なServiceAccountSheetsServiceProvider を用意した感じです。

鍵文字列をServiceAccountSheetsServiceProviderに渡す方法

サービスアカウントの鍵はアクセス権限を握るセンシティブなデータファイルなので、鍵の文字列をgitリポジトリ内で管理することはタブーです!

セキュリティを考慮して使用後はキャッシュとして残らない方法と取りましょう。

1. 環境変数で渡す

CIとも相性がよくジョブプロセス単位で管理可能なため、筆者としても環境変数を利用する方法がオススメです!

            const string EnvironmentGoogleServiceAccountKey = "UNITY_LOCALIZATION_GOOGLE_SERVICE_ACCOUNT_KEY";
            var serviceAccountKeyJson = Environment.GetEnvironmentVariable(EnvironmentGoogleServiceAccountKey);

            var serviceProvider = new ServiceAccountSheetsServiceProvider(
                serviceAccountKeyJson: serviceAccountKeyJson,
                applicationName: "<GoogleSheetsService (ScriptableObject) に設定したApplicationName>");

2. ジョブ中のみ鍵ファイルを生成

環境変数を使用できない場合は、ジョブ中のみJSON形式の鍵ファイルを生成する対応が一つの選択肢になります。

以下はbashを用いて環境変数からJSON鍵ファイルを復元する方法です。

$ echo "${UNITY_LOCALIZATION_GOOGLE_SERVICE_ACCOUNT_KEY}" > "<path to key>/service-account-key.json"

あとは、上記鍵ファイルからJSON文字列を取り出して渡すだけです。

            const string JsonKeyPath = "<path to key>/service-account-key.json";
            string serviceAccountKeyJson = File.ReadAllText(keyJsonPath);

            var serviceProvider = new ServiceAccountSheetsServiceProvider(
                serviceAccountKeyJson: serviceAccountKeyJson,
                applicationName: bundle.SheetsServiceProvider.ApplicationName);

ジョブの成否に関わらず、不要になったら生成した鍵ファイル削除を忘れずに!

$ rm "<path to key>/service-account-key.json"

余談、現時点の game-ci/unity-builder では独自の環境変数をメソッドに渡せない

GitHub ActionsでGameCIを使っている人は結構いるかと思いますが、
残念ながらgame-ci/unity-builderbuildMethodでUnity側のメソッドを叩くときに独自の環境変数を渡せないようでした。。。 (知っている方がいたらぜひ教えてください!)

そんな経緯もあって、「ジョブ中のみ鍵ファイルを生成」という方法を紹介しました。

Pull or Push

あとは「GCPのサービスアカウント向けSheetsServiceProviderを使用してPull or Push」と同様に生成したserviceProviderを使用するだけです。

            // Google Sheetsアクセス用インスタンスを生成
            var sheets = new GoogleSheets(serviceProvider);

            // ※必ずSpreadSheetIdをGoogleSheetsインスタンスに指定してください!
            sheets.SpreadSheetId = sheetsExtension.SpreadsheetId;

            // 対象のStringTableCollection内の全言語(全Locale)のPullを実施
            sheets.PullIntoStringTableCollection(
                sheetId: sheetsExtension.SheetId,
                collection: collection,
                columnMapping: sheetsExtension.Columns);

おまけ、GitHub ActionsでSheets連携をCronジョブ対応

サンプルを用意したので、よければ参考にして下さい。

大まかな流れは以下です。

  • GitHub ActionsのSecrets (GOOGLE_SERVICE_ACCOUNT_KEY_JSON_BASE64) から鍵文字列をデコードしてJSONファイルを生成
    • 鍵文字列のBase64対応は必須ではないですが、環境依存な文字(かっこやカンマなど)があっても対応しやすいので入れています
  • Unity側のメソッド PullAllLocalizationTablesFromTempKeyJsongame-ci/unity-builderbuildMethod経由で実行
    • game-ci/unity-builderbuildMethodの使用方法はGameCIのドキュメントを参照
  • Pull完了後は生成した鍵ファイルを削除
  • Commit, Push して Pull Request

ちなみに、サンプルでは以下のようなプルリクエストが発行されます。

github.com

拡張パッケージ (Example含む)

使用方法はREADME.mdの "Feature" を参照

github.com

雑感

Unity Localization第2弾でした。
また時間が空いてしまいました。。。

UnityとGitHub Actionsを活用したワークフローに関するノウハウも結構溜まってきたので、機会があれば紹介したいですね。
次回の記事は決まってませんが、状況が落ち着いたらまた何か書きます。

それでは~

【Unity Localization】 複数のStringTableCollectionを一括PullのTips (拡張ツール提供有り)

概要

今回は Unity Localization に関するTipsです。
ついでに拡張パッケージの作ってみたのでよかったら使ってみてください。

docs.unity3d.com

最近 Unity Localization を触りだしたのですが、リリースされてまだ日も浅いパッケージのせいかまだまだ機能に物足りなさも感じられます。
今回はUnity LocalizationでGoogle Sheetsから 複数のStringTableCollection をまとめて更新するTipsを紹介します。

Unity Localization に関しては デニックさんの以下の記事をご覧ください!

xrdnk.hateblo.jp

環境

  • Unity 2021.3.6f1
  • Unity Localization 1.3.2

補足

StringTableCollection

StringTableCollectionはKey, Value形式で全言語分の翻訳テーブルをまとめているScriptableObjectです。
StringTableはRuntimeで使用可能に対して、StringTableCollectionはEditor限定の機能となります。

Google Sheetsとの対応は Extensions 項目の GoogleSheets を追加及び設定することで実現できます。
SpreadSheetsId, SheetsId (gid) を設定することで、Googleドライブ上のスプレッドシートと対応させます。

GoogleSheetsService

GoogleSheetsService (SheetsServiceProvider) は Googleのサービスとの認証関連を設定するScriptableObjectです。
今回はこちらを使用するため設定をお願いします。

認証の設定については以下のデニックさんの記事をどうぞ!

xrdnk.hateblo.jp

Tips

スクリプトStringTableCollection をPullする方法

簡単に説明すると GoogleSheets.PullIntoStringTableCollection に認証済みのSheetsServiceProvider, SpreadSheetsId, SheetsId を渡すことで可能となります。

結構説明が難しいのでサンプルコードを記載します。

    // 対象のStringTableCollectionを取得
    StringTableCollection collection = AssetDatabase.LoadAssetAtPath<StringTableCollection>("Assets/<path to StringTableCollection>");

    // GoogleSheetsExtensionをStringTableCollectionから取得
    var sheetsExtension = collection.Extensions.OfType<GoogleSheetsExtension>().FirstOrDefault();

    // Google認証設定を持つSheetsServiceProviderを取得
    SheetsServiceProvider serviceProvider = AssetDatabase.LoadAssetAtPath<SheetsServiceProvider>("Assets/<path to SheetsServiceProvider>");

    // Google Sheetsアクセス用インスタンスを生成
    var sheets = new GoogleSheets(serviceProvider);

    // ※必ずSpreadSheetIdをGoogleSheetsインスタンスに指定してください!
    sheets.SpreadSheetId = sheetsExtension.SpreadsheetId;

    // 対象のStringTableCollection内の全言語(全Locale)のPullを実施
    sheets.PullIntoStringTableCollection(
        sheetId: sheetsExtension.SheetId,
        collection: collection,
        columnMapping: sheetsExtension.Columns);

sheets.SpreadSheetId = sheetsExtension.SpreadsheetId; は必ず実施してください。
どうやら1.3.2時点ではInspectorのPullを押したときしか実施されていないようです。

上記コードが StringTableCollection のInspector上のPullボタンと似た処理を実施しています。

複数のStringTableCollectionを一括でPullする方法

スクリプトでPullする方法がわかったので、あとは対象のStringTableCollectionを取得して for ループで実行するのみです。

StringTableCollectionを一括で取得するEditorコード

自分は以下のような指定のフォルダ以下のアセットをまとめて取得するスクリプトを使用しています。

AssetFinding.cs

余談、公式サンプルコードについて

上記のコードのヒントですが、Unity Localization に含まれている DocCodeSamples.Tests/GoogleSheetsSamples.cs に記載されています。

また、その他のサンプルコードも DocCodeSamples.Tests/ 以下にいくつかありますため参考にすると良いと思います!

拡張ツールの紹介

今回、以下のようなUnity Localitionの拡張ツールを用意しました。
いずれ提供される機能だとは思いますが、それまでの代用やコードの参考としてどうぞ。

github.com

StringTableCollectionBundle というScriptableObjectを設定することでまとめてPull, Pushできるようにしています。
Pullには閲覧権限、Pushには編集権限が必要となります。

詳細な使用方法は README.md を参照ください。

サンプルコード

github.com

Assets/Example 以下に実装例があります。

雑感

まさかのローカライズ系の記事になりました。

前回の記事で絶対にXR系の記事を書くと述べたのですが、
どうもいい感じのネタになっておらず一旦別の記事に逃げることにしました。。。

さて、次回は今回の延長で「Unity LocalizationでGCPのサービスアカウントを使ってプライベートなSheetsと連携」みたいな記事を書く予定です。
(ちなみにこの機能はすでに UnityLocalizationExtension に組み込んであったりします。)

それでは~