【VRM, glTF】3Dアバターファイルフォーマット "VRM" の構造をのぞいてみよう
こちらは クラスター Advent Calendar 2023 の1ページ目の6日目の記事です!
前日は @neguse_kさんの「Blenderでポーズを作ってUnityに取り込む」でした!Blenderでポーズを作成されている方はこちらの記事を参考にぜひclusterにも組み込んでみてください!
こんにちは、すぎしーです。 クラスター株式会社のUnityエンジニアになってちょうど1年が経ちました。エンジニアとしてできることも増え、やりがいのある日々を送っています!
さて、本記事のテーマは「"VRM" の構造をのぞいてみよう」です。ガチな解説ではなくおおまかにVRMの中身をイメージできるぐらいで紹介したいと思います。
使用するツール
- Visual Studio Code
- Hex Editor
- Visual Studio Codeのエクステンションで、いわゆるバイナリエディタです。
- VMagicMirror
- VRMの確認用として使用します。
- 開発者の獏星(ばくすたー)さんもアドベントカレンダーで記事を投稿されています! → 「VRM Animation (.vrma)をUnity上で簡単に生成できるようにした話」
今回はUnityとUniVRMは使用しません!
使用するVRM
VRMについて
VRMとは、VRMコンソーシアムが提唱している人型アバターを定義するファイルフォーマットとなっていて、clusterでもアバターの形式として使用されています。 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 Data Typesから確認できます。5126はfloatになります。
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で確認してみましょう。
- AliciaSolid_vrm-0.51.vrmからAliciaSolid_vrm-0.51_edited.vrmをコピーして作成
- Hex Editorを開く
- Ctrl+F → 置換モードに切替 → 置換を実施
- Ctrl+Sで保存する
- 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_vrm
、extensions.VRMC_springBone
、VRMC_materials_mtoon
などに変更された - マテリアルのパラメータはglTF標準の
materials
項目を使用するようになった- 色の乗算値を例にすると
extensions.VRM.materialProperties[*].vectorProperties._Color
→materials[*].pbrMetallicRoughness.baseColorFactor
のようにglTF標準のパラメータが使用されるようになった - Unity特有のシェーダープロパティ名は使用しなくなった
- 色の乗算値を例にすると
変更点の詳細については VRM-1.0の変更点 で確認できます。
VRM1.0はvrm-specificationにて仕様がより明確に記載され、MToonについても項目が定義されたりとUnity製アプリ以外でもVRMを利用できるように見直されています。
ハロクラで発表されているようにclusterもVRM1.0対応を進めていますため、リリースされたらぜひVRM1.0アバターでclusterを楽しんでください!
参考
雑感
VRMの構造について僕なりに紹介してみましたがいかがでしたでしょうか?
clusterのエンジニアになってプロダクト開発でもVRMに触れることも増えたので、せっかくなので知見の共有も兼ねて記事にしてみました。
VRMはVRM 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エンジニアをなりました!
クラスター株式会社のエンジニアになりました!
— すぎしー (@tsgcpp) 2022年12月1日
これからもバーチャルにのめり込む所存🚀https://t.co/zkkbM0HEtE
改めてよろしくお願いします。
- 概要
- Moqとは
- UnityでMoqを導入
- UnityでMoqを使用
- Moqの使用例
- モックライブラリを使用するメリット
- 簡易導入方法
- 余談
- 雑感
- クラスター Advent Calendar 2022 明日の記事の紹介
概要
今回の内容は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をダウンロードします。
- "Api Compatibility Level" が ".Net Standard 2.1"
- Moq 4.18.2以上
- Castle.Core 5.1.0以上
- System.Diagnostics.EventLog 4.7.0以上
- "Api Compatibility Level" が ".Net Framework"
- Moq 4.18.2以上
- Castle.Core 5.1.0以上
- System.Threading.Tasks.Extensions 4.5.4以上
- System.Runtime.CompilerServices.Unsafe 6.0.0以上
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.dll
とSystem.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 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と注意点
- 概要
- 動作環境
- 用語
- 使用するプロジェクト
- Composite Actions の実装
- Composite Actionの細かな仕様
- Actionの補足
- Composite Actionの注意点
- Reusing Workflows との違い
- 余談、筆者が遭遇した事象
- サンプルプロジェクト
- 雑感
概要
今回はGitHub Actionsの機能の一つである "Composite Action" について紹介します。
今回の記事は、GitHub Actionsに多少知見がある人向けの記事になります。
Composite Actionはいわゆる再利用性のあるステップをyamlファイルに集約して再利用可能にする機能です。
テンプレート的な機能、もしくはプログラミングにおける関数的なものと考えてもらっても良いと思います。
Composite Actionは便利ですが、注意点もあるため紹介しようと思います。
ついでにPrivate Action (Privateなリポジトリに作成したAction) の使用方法も合わせて紹介します。
記事の最後にサンプルリポジトリも記載しておきます。
動作環境
- GitHub Actions + ubuntu-latest
- デバッグモード有効化 Enabling debug logging を参照
用語
- 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の細かな仕様
以下に記載されています。
デフォルトのshellは指定できないなど、細かい仕様が書いてあります。
Actionの補足
通常のActionとComposite Actionは構成自体は同じ
実はComposite Actionのファイル構成 (<path to action>/<composite action name>/action.yml
) ですが、
特殊に見えて、実は通常のActionと同じ構成になっています。
例えば、公式の actions/checkout
のルートのファイルを見ると action.yml
が存在しています。
つまり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
ワークフロー本体のyamlのuses
で指定された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-dotnet
とactions/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 job
でactions/cache
はダウンロードされていますが、
actions/setup-dotnet
とactions/upload-artifact
はダウンロードされていないことがわかります。
actions/setup-dotnet
とactions/upload-artifact
は各種ステップの実行時にダウンロードされています。
ログの全体は以下です。
遅延ダウンロードの何が問題なのか?
「大した問題じゃなくね?」って思った方もいると思いますし、実際大した問題にならないパターンも多いです。
問題になりやすい例として、完了に長時間を要するワークフローがあります。
例えば以下のようなワークフローです。
- 5時間かかるアプリのビルド実行
- ビルド完了後に Composite Actionを使ってアプリをストアへアップロード
- Composite Action内でアプリのストアアップロード用Actionを取得して使用
ワークフロー開始時にはGitHubは正常だったのに、
5時間後のビルド時にGitHubのAPIが一部死んでいてストア用の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という機能があります。
こちらは名前の通りワークフロー全体を再利用する形になります。
一方で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をダウンロードしておくことは一部ワークフローでは有効かと思いますため、
良かったら参考にしていただければと!
サンプルプロジェクト
雑感
直近はかなりドタバタしていたので、これまた久々の記事です。。。
最近はXR、非XRに限らずインフラはやはり重要だなと痛烈に感じています。
XRアプリの開発もどんどん規模が大きくなっているため、開発基盤の重要性もかなり上がっています。
Unityに限らずAndroid, iOS, Dockerを含むサーバーサイドのCI/CDを経験してきた身としては、
インフラをもっと強化していきたいと常に考えるようになりました。
そういえば「すぎしーのXRと3DCG」というブログ名ですが、そろそろ改名を考えています。
XR開発はインタラクションやグラフィックスももちろん重要ですが、それに負けないくらいクリエイターが開発に注力できる環境を用意することも大事だと思います。
これからもよろしくです!
それでは~
【Unity Localization】 GCPのサービスアカウントでプライベートSheetsと連携 (Cronジョブ対応含む)
- 概要
- 動作環境
- 備考
- GCPのサービスアカウント対応手順
- CronによるGoogle SheetsからのStringTableCollectionの定期更新
- おまけ、GitHub ActionsでSheets連携をCronジョブ対応
- 拡張パッケージ (Example含む)
- 雑感
概要
今回も前回に引き続きUnity Localizationに関するTipsです。
GCPのサービスアカウントでプライベートSheetsと連携する方法を紹介します。
Unity Localization標準のOAuth認証を使用すれば一応プライベートSheetsにアクセスすることは可能ですが、Cronジョブなどで定期更新に対応したいときに不都合があります。
そんな問題をGCPのサービスアカウントを用いて解決したいと思います!
おまけでCronジョブでGoogle SheetsからStringTableCollectionを定期更新するTipsも紹介します。
前回の記事
良かったら合わせてどうぞ!
動作環境
- Unity 2021.3.6f1
- Unity 2020 でも可
- Unity Localization 1.3.2
備考
今回紹介する機能は拡張パッケージとしても用意しています。
記事の最後にGitHubへのリンクを記載していますため、よろしければどうぞ。
GCPのサービスアカウント対応手順
サービスアカウントの作成
サービスアカウントの認証用JSON形式の鍵をダウンロード
以下のようなJSON形式の鍵がダウンロード出来ます。
JSON文字列は後述するクラスに渡すために使用します。
{ "type": "service_account", "project_id": "xxx-workspace", "private_key_id": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "private_key": "-----BEGIN PRIVATE KEY-----\n...", "client_email": "unity-localization-example@xxx.xxx.gserviceaccount.com", "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-builder
のbuildMethod
で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ジョブ対応
サンプルを用意したので、よければ参考にして下さい。
- pull-localization.yml (GitHub Actionsのワークフロー)
- ExampleLocalizationSynchronizationMenu.cs (Unity側のメソッド)
- コード内のregion "Service Account Key from Environment Variable" を参照
大まかな流れは以下です。
- GitHub ActionsのSecrets (
GOOGLE_SERVICE_ACCOUNT_KEY_JSON_BASE64
) から鍵文字列をデコードしてJSONファイルを生成- 鍵文字列のBase64対応は必須ではないですが、環境依存な文字(かっこやカンマなど)があっても対応しやすいので入れています
- Unity側のメソッド
PullAllLocalizationTablesFromTempKeyJson
をgame-ci/unity-builder
でbuildMethod
経由で実行game-ci/unity-builder
のbuildMethod
の使用方法はGameCIのドキュメントを参照
- Pull完了後は生成した鍵ファイルを削除
- Commit, Push して Pull Request
ちなみに、サンプルでは以下のようなプルリクエストが発行されます。
拡張パッケージ (Example含む)
使用方法はREADME.mdの "Feature" を参照
雑感
Unity Localization第2弾でした。
また時間が空いてしまいました。。。
UnityとGitHub Actionsを活用したワークフローに関するノウハウも結構溜まってきたので、機会があれば紹介したいですね。
次回の記事は決まってませんが、状況が落ち着いたらまた何か書きます。
それでは~
【Unity Localization】 複数のStringTableCollectionを一括PullのTips (拡張ツール提供有り)
概要
今回は Unity Localization に関するTipsです。
ついでに拡張パッケージの作ってみたのでよかったら使ってみてください。
最近 Unity Localization を触りだしたのですが、リリースされてまだ日も浅いパッケージのせいかまだまだ機能に物足りなさも感じられます。
今回はUnity LocalizationでGoogle Sheetsから 複数のStringTableCollection
をまとめて更新するTipsを紹介します。
Unity Localization に関しては デニックさんの以下の記事をご覧ください!
環境
- 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です。
今回はこちらを使用するため設定をお願いします。
認証の設定については以下のデニックさんの記事をどうぞ!
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コード
自分は以下のような指定のフォルダ以下のアセットをまとめて取得するスクリプトを使用しています。
余談、公式サンプルコードについて
上記のコードのヒントですが、Unity Localization に含まれている DocCodeSamples.Tests/GoogleSheetsSamples.cs
に記載されています。
また、その他のサンプルコードも DocCodeSamples.Tests/
以下にいくつかありますため参考にすると良いと思います!
拡張ツールの紹介
今回、以下のようなUnity Localitionの拡張ツールを用意しました。
いずれ提供される機能だとは思いますが、それまでの代用やコードの参考としてどうぞ。
StringTableCollectionBundle
というScriptableObjectを設定することでまとめてPull, Pushできるようにしています。
Pullには閲覧権限、Pushには編集権限が必要となります。
詳細な使用方法は README.md を参照ください。
サンプルコード
Assets/Example
以下に実装例があります。
雑感
まさかのローカライズ系の記事になりました。
前回の記事で絶対にXR系の記事を書くと述べたのですが、
どうもいい感じのネタになっておらず一旦別の記事に逃げることにしました。。。
さて、次回は今回の延長で「Unity LocalizationでGCPのサービスアカウントを使ってプライベートなSheetsと連携」みたいな記事を書く予定です。
(ちなみにこの機能はすでに UnityLocalizationExtension に組み込んであったりします。)
それでは~
GitHub ActionsのWindows self-hostedでpwshとbashを使う方法
概要
今回は GitHub ActionsのWindows self-hostedでpwshとbashを使う方法を紹介します。
残念ながら素のWindowsマシンでshellとしてpwshやbashを指定しても使用することができません。
Windows上でpwshとbashが使用できるとCIで行えるフローの選択肢が大幅に増えると思いますので、ぜひ参考にしていただければと思います!
動作環境
- Windows10 Pro (Homeでも可)
pwshとbashを使用する条件
簡単に言えば、以下を満たせば良いです
- Git for Windows (Git Bash) と PowerShell7 (pwsh付属) をインストール
- Windowsのシステム環境変数のPATHに
bash.exe
とpwsh.exe
があるフォルダを追加
手順
詳細はあとで記載します。
1. Git for Windows と PowerShell7 をインストール
以前紹介した記事を参考にGit for WindowsとPowerShell7をインストールしてください
2. システム環境変数のPATHに登録
Git for WindowsとPowerShell7をインストールしたフォルダをPATHに登録してください。
PATHを通すps1ファイル (set-git-path.ps1
, set-powershell-path.ps1
)も上記の記事内にて記載しています。
システム環境変数に直接以下を追加しても大丈夫です(標準のインストールフォルダの場合)。
C:\Program Files\Git\cmd;C:\Program Files\Git\bin;C:\Program Files\PowerShell\7
3. self-hostedのrunnerプロセスを再起動
環境変数をプロセスに反映させるために再起動してください。
再起動方法は以下の公式ドキュメントを参考にしてください。
もし、PowerShell上でGitHub Actionsのself-hostedの .\run.cmd
で起動している場合はPowerShell自体も再起動してください。
手順は以上となります!
詳細
確認用のworkflow
以下のworkflowを使って確認します。
name: Windows Shell Check on: workflow_dispatch jobs: shell-check-all: runs-on: self-hosted steps: - name: Check cmd shell: cmd run: | echo cmd ok if: ${{ always() }} - name: Check pwsh shell: pwsh run: | echo pwsh ok if: ${{ always() }} - name: Check bash shell: bash run: | echo bash ok if: ${{ always() }}
インストール前
以下のように cmd
のみ使用可能で、それ以外は使用できません。。。
インストール後
問題なさそうですね。
活用例
せっかくなので、活用例を紹介します。
日付を文字列として取得
Bashのdate
コマンドを使って取得します。
name: Get Date String on: workflow_dispatch jobs: get-date-string: runs-on: self-hosted steps: - name: Get Date String run: echo "::set-output name=DATE::$(date +%Y%m%d%H%M)" shell: bash id: get-date-string - name: Use Date String run: echo "${{ steps.get-date-string.outputs.DATE }}"
Bashは手軽に文字列を生成可能なコマンドがたくさんあります!
レジストリから値を取得
以下はレジストリに登録されたUnity 2021.3.2f1 のインストールフォルダを取得する方法です。
name: Get Unity Folder on: workflow_dispatch jobs: get-unity-folder: runs-on: self-hosted steps: - name: Get Unity Folder run: Write-Host -NoNewline ('::set-output name=UNITY_FOLDER::' + (Get-ItemProperty 'HKCU:SOFTWARE\Unity Technologies\Installer\Unity 2021.3.2f1').'Location x64') shell: pwsh id: get-unity-folder - name: Use Unity Folder run: echo "${{ steps.get-unity-folder.outputs.UNITY_FOLDER }}"
Windowsのシステムに関わる情報を取得したい場合はPowerShellのほうが適している気がします。
余談、GitHub Actionsで提供されているWindowsのRunnerでは標準でpwshとbashが使用可能
PowerShellはともかく、Bashも使用できるように用意してくれていたりします。
ドキュメントでも以下のように記載されています。
GitHub ActionsもGit for Windowsを使用しているようですね。
When specifying a bash shell on Windows, the bash shell included with Git for Windows is used.
windows-latestでpwshとbashが使用可能なことを確認
先程のworkflowのrunnerにwindows-latestを指定して確認してみましょう。
jobs: shell-check-all: strategy: matrix: runner: [self-hosted, windows-latest] runs-on: ${{ matrix.runner }} steps:
結果は以下のように問題なく使用可能です!
雑感
最近Git for WindowsとPowerShell7のインストール方法を紹介した本当の目的はこちらだったりします。
私個人では開発基盤を用意する機会が増えているんですが、
pwshとbash (特にbash!)をGitHub Actionsで使えるようにしておくと何かと捗ったり、self-hostedとwindows-latestでworkflowの互換性が確保できたりします。
わざわざGitHub Marketplaceや独自アクションを使用しなくてもできることが増えると思いますので、ぜひご活用ください!
それでは~
pwshをコマンドラインでインストールしてPATHを通す方法
概要
前回 に続いて、今回はWindowsのコマンドラインで PowerShell7 をインストールする方法を紹介します。
PowerShell5はWindowsに標準で搭載されていますが、pwsh
コマンドは実行できません。
そこでPowerShell7を別途インストールすることで pwsh
コマンドとして使用可能になります。
そんなPowerShell7をコマンドライン経由でインストールしPATHを登録する方法を紹介します。
動作環境
- Windows 10
- 筆者はPro版ですが、Homeでも問題ないと思います
変更履歴
- 2022/05/18
- タイトルを「【PowerShell】 PowerShell7をコマンドラインからインストールして、pwshコマンドにPATHを通す」→「pwshをコマンドラインでインストールしてPATHを通す方法」に変更
- 2023/02/05
- 「install-powershell.ps1 を実行」を
PowerShell -ExecutionPolicy Bypass
を使ったコマンドに修正
- 「install-powershell.ps1 を実行」を
注意事項
- 以下を試す場合は自己責任でお願いします!
- 以下の処理は管理者権限で実行しますが、当方は事故や故障などの一切の責任を負いません!
- 環境変数 PATH を変更するため、変更前のPATHを記録しておくことを推奨!
上記の件、ご留意ください
インストール方法
※前回の記事 (Git for Windowsをコマンドラインからインストールする方法) と流れはほとんど同じです
Gitからps1ファイルをクローン
- ps1ファイル(PowerShellスクリプト)を以下のリポジトリからクローンする
PowerShellを管理者権限で開く
※開き方は好きなやり方で問題ありません
Windows 10標準搭載のPowerShell5を使用
Windows
キーを押して、「powershell」を検索- 「管理者として実行する」をクリック
PowerShellでクローンしたWindowsInfrastructureExample上に移動
cd <path>\WindowsInfrastructureExample
PowerShell-7.x.x-win-x64.msi を配置
コマンドでダウンロードする場合
Invoke-WebRequest https://github.com/PowerShell/PowerShell/releases/download/v7.2.3/PowerShell-7.2.3-win-x64.msi -OutFile PowerShell-7.2.3-win-x64.msi
※インフラ構築のスクリプトを書く場合は、このコマンド自体をps1スクリプトに組み込んでも良い
ブラウザでダウンロードする場合
以下より PowerShell-7.x.x-win-x64.msi
をダウンロードし、WindowsInfrastructureExample
フォルダ以下に配置
ps1の実行を許可
Set-ExecutionPolicy RemoteSigned -Scope Process
install-powershell.ps1 を実行
> PowerShell -ExecutionPolicy Bypass .\install-powershell.ps1
エラーログが出なければ問題なく PowerShell7 のインストール完了
インストールの確認 (コマンドラインの場合)
Dir 'C:\Program Files\PowerShell\7\pwsh.exe'
でエラーが出なければインストール完了
Dir 'C:\Program Files\PowerShell\7\pwsh.exe' ディレクトリ: C:\Program Files\PowerShell\7 Mode LastWriteTime Length Name ---- ------------- ------ ---- -a---- 2022/04/15 20:07 287664 pwsh.exe
(任意)PATHの追加
ローカルPCの環境変数 PATH が変更されます!ご注意ください!
PowerShell -ExecutionPolicy Bypass .\set-powershell-path.ps1
以上でインストール及び設定は完了です。
スクリプト解説
msiexecによるインストール
PowerShell-7.x.x-win-x64.msi のインストールオプション
詳細は以下のページに記載されています
INSTALLFOLDER
名前の通りインストール先のフォルダを指定。
以下のように指定すると良いです。
$InstallFolder = 'C:\Program Files\PowerShell' (略) msiexec.exe ` ... INSTALLFOLDER=`"$InstallFolder`" ` ...
ちなみに、C:\Program Files\PowerShell
を指定した場合、pwsh
はC:\Program Files\PowerShell\7\pwsh.exe
に配置されます。
また、スクリプト内でフォルダ名を`"XXX`"
のように囲むのは不思議な感じがしますが、これで問題ありません。
Windowsのコマンドラインはスペースの扱いがめんどくさいですね。。。それなのに "Program Files" はスペースが有るという。。。
ADD_PATH=0
PATHに追加するかのオプションです。ADD_PATH=1
にすればパスを通してくれます。
今回はコマンドラインで諸々完結させたかったので0指定(登録しない)にしています。
USE_MU=0, ENABLE_MU=0
いわゆるMicrosoft Updateによる自動更新の設定です。
毎回コマンドラインで更新することを想定し、オフにしています。
解説は以上です!
雑感
Git for Windows, PowerShell7 のインストール方法を紹介したのは、bash
, pwsh
をインストールするためだったりします。
次回はこの2つを活用したCIについてお話します!
もしかしたらピンと来た方がいらっしゃるかもですね。
さらにその次は絶対にUnity + XR関連にします(そっちのほうが断然楽しいし、何よりブログのタイトル的に。。。)
PATHを変更するような手順の紹介は、試した人の環境を壊しかねないので結構神経を使いますね。。。
それでは~