すぎしーのXRと3DCG
主にXR, Unity, 3DCG系の記事を投稿していきます。
2024-12-17T19:52:22+09:00
tsgcpp
Hatena::Blog
hatenablog://blog/26006613577180627
Claude Code での Unity 開発の振り返り (2025年)
hatenablog://entry/17179246901330266238
2025-12-11T12:00:00+09:00
2025-12-25T11:35:43+09:00 クラスター Advent Calendar 2025 の11日目の記事です。
Claude Code を Unity で何に使ったかについて、3つほどピックアップして振り返ってみようと思います。
<p>こんにちは、すぎしーです。<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AF%A5%E9%A5%B9%A5%BF">クラスタ</a>ー株式会社にジョインして4年目になります!
<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AF%A5%E9%A5%B9%A5%BF">クラスタ</a>ー Advent Calendar 2025 の11日目の記事です!</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fqiita.com%2Fadvent-calendar%2F2025%2Fcluster" title="クラスター - Qiita Advent Calendar 2025 - Qiita" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://qiita.com/advent-calendar/2025/cluster">qiita.com</a></cite></p>
<p>前日は <a href="https://qiita.com/Kazuya0123">@Kazuya0123</a> さんの 「僕と地域と<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AF%A5%E9%A5%B9%A5%BF">クラスタ</a>ー」でした。僕も cluster がオンラインで様々な市区町村がつながるプラットフォームとして認知されるよう、頑張っていきたいです。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fnote.com%2Ftokaina%2Fn%2Fnb01c07910efd" title="僕と地域とクラスター|たーふじ(藤田和也)" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://note.com/tokaina/n/nb01c07910efd">note.com</a></cite></p>
<p>さて、今回は2025年の Claude Code での Unity 開発の振り返りについてです。</p>
<p>世間の例に漏れず、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AF%A5%E9%A5%B9%A5%BF">クラスタ</a>ーのエンジニアたちにも AI コーディングを使った開発があっという間に浸透しました。</p>
<p>せっかく導入した年なので、そんな AI コーディングの1つである Claude Code を Unity で何に使ったかについて、3つほどピックアップして振り返ってみようと思います。</p>
<p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AF%A5%E9%A5%B9%A5%BF">クラスタ</a>ーでの AI コーディングの開発事例については <a href="https://tech-blog.cluster.mu/">Cluster Tech Blog</a> もどうぞ!</p>
<ul class="table-of-contents">
<li><a href="#何に-Claude-Code-を使った">何に Claude Code を使った?</a><ul>
<li><a href="#UnityEditor-向けツールの作成">UnityEditor 向けツールの作成</a></li>
<li><a href="#InputAction-の編集">InputAction の編集</a></li>
<li><a href="#エラー通知から修正案を出させる">エラー通知から修正案を出させる</a></li>
<li><a href="#その他">その他</a></li>
</ul>
</li>
<li><a href="#雑感">雑感</a></li>
</ul>
<h1 id="何に-Claude-Code-を使った">何に Claude Code を使った?</h1>
<h2 id="UnityEditor-向けツールの作成">UnityEditor 向けツールの作成</h2>
<p>UnityEditor 向けツールの作成がやりやすくなったのは、一番わかりやすい恩恵だったように思います。<br/>
特にいままで一番時間を要していた「Unity の <a class="keyword" href="https://d.hatena.ne.jp/keyword/API">API</a> 仕様書を都度調べる時間」が大幅に削減できるという点は非常にありがたいと感じています。</p>
<p>以前までは <a href="https://docs.unity3d.com/ScriptReference/EditorGUILayout.html">EditorGUILayout</a>, <a href="https://docs.unity3d.com/ScriptReference/AssetDatabase.html">AssetDatabase</a>, <a href="https://docs.unity3d.com/ScriptReference/SerializedObject.html">SerializedObject</a>, <a href="https://docs.unity3d.com/ScriptReference/EditorWindow.html">EditorWindow</a> といったお馴染みなものから <a href="https://docs.unity3d.com/Packages/[email protected]/manual/index.html">Addressables</a> などライブラリまで、様々な <a class="keyword" href="https://d.hatena.ne.jp/keyword/API">API</a> の仕様書とにらめっこしながら開発していたことが AI にツールの仕様を伝えるだけであっという間に出来上がるようになりました。とんでもない時代が来ましたね。</p>
<p>以下のようなツールも作ったのですが、コード行のほとんどが Claude Code による生成です。
<span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20251209/20251209104311.png" width="603" height="639" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<h2 id="InputAction-の編集">InputAction の編集</h2>
<p>Unity の InputSystem で用いる InputAction ですが、項目が多いと UI でポチポチするのが大変ですし、何よりミスが起きやすいです。
特に <a class="keyword" href="https://d.hatena.ne.jp/keyword/VR">VR</a> のトラッカーは 11点 * 3要素(位置、回転、状態) の合計33点を指定する必要があるので、指定ミスしないように注意が必要でした。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20251211/20251211100953.png" width="942" height="227" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p>そこで InputAction の中身を確認したところ <a class="keyword" href="https://d.hatena.ne.jp/keyword/JSON">JSON</a> 形式でユニークなGUIDを各入力に紐づけるフォーマットだったので、
Claude Code に必要な項目を指定して規則性に従って編集してもらうことにしました。</p>
<p>少し調整はありましたが、あっという間に作ってくれました。</p>
<pre class="code lang-json" data-lang="json" data-unlink><span class="synSpecial">{</span>
...
<span class="synSpecial">{</span>
"<span class="synStatement">name</span>": "<span class="synConstant">Tracker</span>",
"<span class="synStatement">id</span>": "<span class="synConstant">b6b46f2b-fb24-4481-9840-28738adcb0d6</span>",
"<span class="synStatement">actions</span>": <span class="synSpecial">[</span>
<span class="synSpecial">{</span>
"<span class="synStatement">name</span>": "<span class="synConstant">LeftFootPosition</span>",
"<span class="synStatement">type</span>": "<span class="synConstant">Value</span>",
"<span class="synStatement">id</span>": "<span class="synConstant">969c5f83-4d27-4f58-8deb-2b6dd6365b8e</span>",
"<span class="synStatement">expectedControlType</span>": "<span class="synConstant">Vector3</span>",
"<span class="synStatement">processors</span>": "",
"<span class="synStatement">interactions</span>": "",
"<span class="synStatement">initialStateCheck</span>": <span class="synConstant">true</span>
<span class="synSpecial">}</span>,
<span class="synSpecial">{</span>
"<span class="synStatement">name</span>": "<span class="synConstant">LeftFootRotation</span>",
"<span class="synStatement">type</span>": "<span class="synConstant">Value</span>",
"<span class="synStatement">id</span>": "<span class="synConstant">65634ff7-fa1b-4a27-bb29-f74dc757d170</span>",
"<span class="synStatement">expectedControlType</span>": "<span class="synConstant">Quaternion</span>",
"<span class="synStatement">processors</span>": "",
"<span class="synStatement">interactions</span>": "",
"<span class="synStatement">initialStateCheck</span>": <span class="synConstant">true</span>
<span class="synSpecial">}</span>,
<span class="synSpecial">{</span>
"<span class="synStatement">name</span>": "<span class="synConstant">LeftFootState</span>",
"<span class="synStatement">type</span>": "<span class="synConstant">Value</span>",
"<span class="synStatement">id</span>": "<span class="synConstant">c9d667fd-f506-4a68-bee8-39225817751e</span>",
"<span class="synStatement">expectedControlType</span>": "<span class="synConstant">Integer</span>",
"<span class="synStatement">processors</span>": "",
"<span class="synStatement">interactions</span>": "",
"<span class="synStatement">initialStateCheck</span>": <span class="synConstant">true</span>
<span class="synSpecial">}</span>,
</pre>
<p>余談ですが、AI に GUID の生成を任せると「GUID っぽい文字列」を作ってしまいます。
例: <code>a1b2c3d4-e5f6-g7h8-a1b2-c3d4e5f6g7h8</code></p>
<p>ちゃんとした GUID を使わせたい場合は、GUID を生成するコマンドを渡しておくと良いです。
例: <code>./generate-guid.go</code> → <code>b6b46f2b-fb24-4481-9840-28738adcb0d6</code></p>
<p>GUID に限らず、AI がシンプルに使えるツールを用意することも大事ですね。</p>
<h2 id="エラー通知から修正案を出させる">エラー通知から修正案を出させる</h2>
<p>Claude Code に <a href="https://docs.sentry.io/product/sentry-mcp/">Sentry MCP Server</a> の使用を許可して Issue のリンクを渡し、必要に応じて修正案を出させています。<br/>
補足: Sentry とは、エラー監視プラットフォームのことです。</p>
<p>まだすべてを任せているわけではなく、以下のような AI エージェントでも比較的解決しやすい部類の Issue をお願いする形を取っています。</p>
<ul>
<li>HTTP レスポンスのハンドリング漏れ</li>
<li>例外 (IOException) などのハンドリング漏れ</li>
</ul>
<p>修正内容の精度がまだ安定していない部分があるのでエラー通知毎にプルリクエストを提出させる形にはしていませんが、いずれはそうしていきたいなと考えています。</p>
<h2 id="その他">その他</h2>
<p>以下のような活用をしています。</p>
<ul>
<li>client 視点での server 側の実装調査 (重要度の低いものに限定、重要度が高い場合は server エンジニアに直接確認します)</li>
<li>設計、実装の壁打ち</li>
<li>ライブラリの詳細調査</li>
<li>リリース後に不要となった Feature Flag 分岐コードの廃止、及び関連して不要になったコードの削除</li>
<li><a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Actions で使用しているバージョンの一括更新</li>
<li>Node.js の Node バージョンの更新</li>
</ul>
<h1 id="雑感">雑感</h1>
<p>というわけで Claude Code を Unity 開発でどのように活用したかについて書いてみましたがいかがだったでしょうか?</p>
<p>今年は AI コーディングを単に導入しただけでなく、AI がスムーズに開発できる環境づくりにも力をいれていた年だったかなと思います。</p>
<p>AI の進化の早さにも食らいついていって、どんどん開発を回していきたいですね。</p>
<p>さて、明日は <a href="https://qiita.com/FUKUDA_concrete">@FUKUDA_concrete</a> さんの 「はじめてのUnityワールド作成のススメ」です。お楽しみに!</p>
tsgcpp
Visual Studio CodeでVRMを改変する
hatenablog://entry/6802418398312082875
2024-12-17T19:52:22+09:00
2024-12-17T20:05:25+09:00 こちらは クラスター Advent Calendar 2024 の17日目の記事です! qiita.com こんにちは、すぎしーです。クラスター株式会社にジョインして2年になります。 本記事のテーマは「Visual Studio CodeでVRMを改変する」になります!今回はUnityは使用しません! 改変例として「VRMのテクスチャをVSCodeで差し替える」と「VRM1.0のExpressionのOverride設定をVSCodeで変更する」も紹介しているので、よかったら参考にしてみてください。 使用するツール 前準備 1. VSCodeをインストール 2. VSCodeにglTF Too…
<p>こちらは <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AF%A5%E9%A5%B9%A5%BF">クラスタ</a>ー Advent Calendar 2024 の17日目の記事です!</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fqiita.com%2Fadvent-calendar%2F2024%2Fcluster" title="クラスター - Qiita Advent Calendar 2024 - Qiita" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://qiita.com/advent-calendar/2024/cluster">qiita.com</a></cite></p>
<hr />
<p>こんにちは、すぎしーです。<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AF%A5%E9%A5%B9%A5%BF">クラスタ</a>ー株式会社にジョインして2年になります。</p>
<p>本記事のテーマは「<a class="keyword" href="https://d.hatena.ne.jp/keyword/Visual%20Studio%20Code">Visual Studio Code</a>で<a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a>を改変する」になります!今回はUnityは使用しません!</p>
<p>改変例として「<a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a>のテクスチャを<a class="keyword" href="https://d.hatena.ne.jp/keyword/VSCode">VSCode</a>で差し替える」と「VRM1.0のExpressionのOverride設定を<a class="keyword" href="https://d.hatena.ne.jp/keyword/VSCode">VSCode</a>で変更する」も紹介しているので、よかったら参考にしてみてください。</p>
<ul class="table-of-contents">
<li><a href="#使用するツール">使用するツール</a></li>
<li><a href="#前準備">前準備</a><ul>
<li><a href="#1-VSCodeをインストール">1. VSCodeをインストール</a></li>
<li><a href="#2-VSCodeにglTF-Toolsをインストール">2. VSCodeにglTF Toolsをインストール</a></li>
</ul>
</li>
<li><a href="#補足情報-VRMはglTF拡張である">補足情報: VRMはglTF拡張である</a></li>
<li><a href="#glTF-ToolsでVRMをインポート">glTF ToolsでVRMをインポート</a><ul>
<li><a href="#編集対象のVRMを作業用のフォルダにコピーする">編集対象のVRMを作業用のフォルダにコピーする</a></li>
<li><a href="#VSCodeで対象のVRMがある作業用フォルダを開く">VSCodeで対象のVRMがある作業用フォルダを開く</a></li>
<li><a href="#対象のVRMをインポートする">対象のVRMをインポートする</a></li>
</ul>
</li>
<li><a href="#Appendix-編集が許可されているVRMであることを確認する">Appendix: 編集が許可されているVRMであることを確認する</a></li>
<li><a href="#VRMを編集する">VRMを編集する</a></li>
<li><a href="#glTF-ToolsでVRMをエクスポート">glTF ToolsでVRMをエクスポート</a></li>
<li><a href="#補足-エクスポートで出力されるVRMバージョンはインポート時に従う">補足: エクスポートで出力されるVRMバージョンはインポート時に従う</a></li>
<li><a href="#改変例">改変例</a><ul>
<li><a href="#改変例1-VRMのテクスチャをVSCodeで差し替える">改変例1: VRMのテクスチャをVSCodeで差し替える</a></li>
<li><a href="#改変例2-VRM10のExpressionのOverride設定をVSCodeで変更する">改変例2: VRM1.0のExpressionのOverride設定をVSCodeで変更する</a></li>
<li><a href="#gltfファイルのJSONのから-expressions-項目を確認する">gltfファイルのJSONのから "expressions" 項目を確認する</a></li>
<li><a href="#対象のExpressionのoverrideBlinkoverrideLookAtoverrideMouthを変更する">対象のExpressionのoverrideBlink、overrideLookAt、overrideMouthを変更する</a></li>
<li><a href="#VRMに再エクスポートして確認する">VRMに再エクスポートして確認する</a></li>
</ul>
</li>
<li><a href="#おまけ-VSCodeでモデルをプレビューする">おまけ: VSCodeでモデルをプレビューする</a></li>
<li><a href="#雑感">雑感</a></li>
</ul>
<h1 id="使用するツール">使用するツール</h1>
<ul>
<li><a href="https://azure.microsoft.com/ja-jp/products/visual-studio-code">Visual Studio Code</a>
<ul>
<li>以下「<a class="keyword" href="https://d.hatena.ne.jp/keyword/VSCode">VSCode</a>」と呼称</li>
</ul>
</li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=cesium.gltf-vscode">glTF Tools</a> (<a class="keyword" href="https://d.hatena.ne.jp/keyword/Visual%20Studio%20Code">Visual Studio Code</a>の拡張)</li>
</ul>
<h1 id="前準備">前準備</h1>
<h2 id="1-VSCodeをインストール">1. <a class="keyword" href="https://d.hatena.ne.jp/keyword/VSCode">VSCode</a>をインストール</h2>
<p><a href="https://azure.microsoft.com/ja-jp/products/visual-studio-code">Visual Studio Code</a> から<a class="keyword" href="https://d.hatena.ne.jp/keyword/VSCode">VSCode</a>をインストールしてください。</p>
<h2 id="2-VSCodeにglTF-Toolsをインストール">2. <a class="keyword" href="https://d.hatena.ne.jp/keyword/VSCode">VSCode</a>にglTF Toolsをインストール</h2>
<p>以下を参考にglTF Toolsを<a class="keyword" href="https://d.hatena.ne.jp/keyword/VSCode">VSCode</a>にインストールしてください。</p>
<ol>
<li><a class="keyword" href="https://d.hatena.ne.jp/keyword/VSCode">VSCode</a>のExtensionsを開く</li>
<li>"glTF Tools"と検索する</li>
<li>検索で出たglTF Toolsをクリック</li>
<li>Install ボタンをクリック</li>
</ol>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20241216/20241216183041.png" width="1200" height="482" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p>以上で前準備は完了です!</p>
<h1 id="補足情報-VRMはglTF拡張である">補足情報: <a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a>はglTF拡張である</h1>
<p><a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a>はglTFをいうファイルフォーマットを拡張したものになっています。
そのためglTF向けのツールで改変が可能な場合が多いです。</p>
<p>もし深堀りしたい方は以下の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AF%A5%E9%A5%B9%A5%BF">クラスタ</a>ー Advent Calendar 2023の記事を御覧ください!</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftsgcpp.hateblo.jp%2Fentry%2Fvrm_file_format_beginning" title="【VRM, glTF】3Dアバターファイルフォーマット "VRM" の構造をのぞいてみよう - すぎしーのXRと3DCG" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://tsgcpp.hateblo.jp/entry/vrm_file_format_beginning">tsgcpp.hateblo.jp</a></cite></p>
<p>この性質を利用してglTF Toolsで<a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a>を編集しようと思います。</p>
<h1 id="glTF-ToolsでVRMをインポート">glTF Toolsで<a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a>をインポート</h1>
<p>まずはglTF Toolsで<a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a>を改変する時に毎回実施することになるインポート方法を紹介します!</p>
<h2 id="編集対象のVRMを作業用のフォルダにコピーする">編集対象の<a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a>を作業用のフォルダにコピーする</h2>
<p>glTF Toolsはインポート時に<a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a>内のテクスチャや頂点などのバイナリデータを一気にファイル化するため、作業用フォルダを用意してそのフォルダの中に<a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a>ファイルを置きましょう。</p>
<h2 id="VSCodeで対象のVRMがある作業用フォルダを開く"><a class="keyword" href="https://d.hatena.ne.jp/keyword/VSCode">VSCode</a>で対象の<a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a>がある作業用フォルダを開く</h2>
<p><a class="keyword" href="https://d.hatena.ne.jp/keyword/VSCode">VSCode</a>のOpen Folder...で作業用フォルダを開いてください。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20241216/20241216184525.png" width="625" height="308" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<h2 id="対象のVRMをインポートする">対象の<a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a>をインポートする</h2>
<p>以下の流れでインポートできます。</p>
<ol>
<li>ファイル一覧を開く</li>
<li>編集対象の<a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a>上で右クリックする</li>
<li>"glTF: Import from GLB" をクリックする</li>
<li>インポートされたファイルを保存先(拡張子 "gltf"、以後「gltfファイル」と呼称)を指定する</li>
</ol>
<p>保存先は<a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a>ファイルとは別のフォルダでも大丈夫ですが、今回は同じ作業用フォルダに保存して説明します。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20241217/20241217191638.png" width="684" height="855" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p>インポートが終わると<a class="keyword" href="https://d.hatena.ne.jp/keyword/JSON">JSON</a>形式のgltfファイルと関連するテクスチャなどのファイル群が生成されているはずです。</p>
<p><span style="color: #ff0000">一緒に出力されたファイルたちはエクスポート時に必要になるので消さずに残しておいてください!</span></p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20241216/20241216185241.png" width="917" height="934" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p>これでインポートは完了です。</p>
<h1 id="Appendix-編集が許可されているVRMであることを確認する">Appendix: 編集が許可されている<a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a>であることを確認する</h1>
<p>自身が所有者ではない<a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a>の場合は、glTF内の項目を確認して「このモデルを改変することを許可するか否か」を確認しておきましょう。</p>
<p>(<a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a>確認ツールでもいいですが、本記事ではトコトン<a class="keyword" href="https://d.hatena.ne.jp/keyword/VSCode">VSCode</a>で確認します!)</p>
<ul>
<li>VRM0.xの場合は <code>extensions.VRM.meta.licenseName</code> (参考: 公式仕様書 <a href="https://github.com/vrm-c/vrm-specification/blob/master/specification/0.0/README.ja.md">0.0/README.ja.md</a>)</li>
<li>VRM1.0の場合は <code>extensions.VRMC_vrm.meta.modification</code>: (参考: 公式仕様書 <a href="https://github.com/vrm-c/vrm-specification/blob/master/specification/VRMC_vrm-1.0/meta.ja.md#metamodification">VRMC_vrm-1.0/meta.ja.md#metamodification</a>)</li>
</ul>
<p>VRM1.0の場合であれば <code>allowModification</code>、もしくは<code>allowModificationRedistribution</code>であれば改変が許可されています。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20241217/20241217192102.png" width="1200" height="521" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p>ちなみに VRoid Studioで作成した<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%D0%A5%BF%A1%BC">アバター</a>の場合は、エクスポート時に「改変」の項目で設定した内容が反映されているはずです。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20241216/20241216190741.png" width="578" height="715" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<h1 id="VRMを編集する"><a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a>を編集する</h1>
<p>インポートが終わったら<a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a>の仕様書 (<a href="https://github.com/vrm-c/vrm-specification/tree/master">vrm-specification</a>) に従いつつ、テクスチャを変えたりgltfファイルを変更して<a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a>を編集することになります。</p>
<p><strong>改変したgltfファイルは必ず保存(Ctrl+S)してください!</strong></p>
<p>具体的な作業は「改変例」を後述しているので参考にしてみてください。</p>
<p>次項でひとまず改変が完了した後に実施する<a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a>へのエクスポート方法を紹介します。</p>
<h1 id="glTF-ToolsでVRMをエクスポート">glTF Toolsで<a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a>をエクスポート</h1>
<p>以下の流れで編集したgltfファイルから<a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a>をエクスポートします。</p>
<ol>
<li>エクスポート対象のgltfファイル上で右クリックする (開いているタブ上で右クリックでも可)</li>
<li>"glTF: Export to GLB (Binary file)" をクリックする</li>
<li>出力するファイルの拡張子を".<a class="keyword" href="https://d.hatena.ne.jp/keyword/vrm">vrm</a>"にして保存先を指定する (例: "vroid_sample_ex.<a class="keyword" href="https://d.hatena.ne.jp/keyword/vrm">vrm</a>")</li>
</ol>
<p>エクスポート時の拡張子はデフォルトで ".glb" になっているので、<br/>
<strong><span style="color: #ff0000">拡張子を必ず ".<a class="keyword" href="https://d.hatena.ne.jp/keyword/vrm">vrm</a>" に変更して保存</span></strong>してください!</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20241216/20241216192120.png" width="714" height="861" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20241216/20241216201735.png" width="1133" height="986" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p>エクスポートが完了すると<a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a>ファイルが出力されているはずです。</p>
<p>ここまでが <a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a>をインポート → <a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a>を改変 → <a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a>をエクスポート の流れになります。<a class="keyword" href="https://d.hatena.ne.jp/keyword/VSCode">VSCode</a>で<a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a>を編集する場合は毎回やることになります。</p>
<h1 id="補足-エクスポートで出力されるVRMバージョンはインポート時に従う">補足: エクスポートで出力される<a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a>バージョンはインポート時に従う</h1>
<p>今回のやり方でエクスポートした場合、出力される<a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a>バージョンはインポート時と同じになります。</p>
<p>というよりglTF Toolsは純粋なglTF向けツールでありVRM0.xからVRM1.0に変換するみたいな機能は特に入っていないため、gltfファイルの<a class="keyword" href="https://d.hatena.ne.jp/keyword/JSON">JSON</a>内容はそのままで出力されるので結果としてインポート時と同じ<a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a>バージョンが出力されます。</p>
<h1 id="改変例">改変例</h1>
<p>今回はVRoid Studioから作成した以下のVRM1.0<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%D0%A5%BF%A1%BC">アバター</a>の子を使います。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20241216/20241216195917.jpg" width="960" height="510" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<h2 id="改変例1-VRMのテクスチャをVSCodeで差し替える">改変例1: <a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a>のテクスチャを<a class="keyword" href="https://d.hatena.ne.jp/keyword/VSCode">VSCode</a>で差し替える</h2>
<p>glTF Toolsはテクスチャの入れ替えができます。やり方は簡単でインポート直後に出力されている<a class="keyword" href="https://d.hatena.ne.jp/keyword/png">png</a>画像のうち、テクスチャに使われる<a class="keyword" href="https://d.hatena.ne.jp/keyword/png">png</a>画像を差し替えるだけです。</p>
<p>今回は先程の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%D0%A5%BF%A1%BC">アバター</a>のシャツのテクスチャを変えて色を変えてみようと思います。</p>
<p><a class="keyword" href="https://d.hatena.ne.jp/keyword/VSCode">VSCode</a>で確認するとシャツのテクスチャの名前は <code>vroid_sample_img13.png</code> になっているようです。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20241217/20241217185956.png" width="1200" height="530" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p>そのテクスチャを色を変更した以下の<a class="keyword" href="https://d.hatena.ne.jp/keyword/png">png</a>に差し替えます。差し替えた後もファイル名は必ず一致させてください。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20241217/20241217190353.png" width="1200" height="537" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p>あとはgltfファイルからエクスポートするだけです。</p>
<p>clusterで確認してみると<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%D0%A5%BF%A1%BC">アバター</a>のシャツが差し替わっているはずです。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20241217/20241217191025.png" width="1200" height="638" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p>テクスチャを変えたいぐらいであれば<a class="keyword" href="https://d.hatena.ne.jp/keyword/VSCode">VSCode</a>だけでできちゃうのでぜひ試してみてください。</p>
<h2 id="改変例2-VRM10のExpressionのOverride設定をVSCodeで変更する">改変例2: VRM1.0のExpressionのOverride設定を<a class="keyword" href="https://d.hatena.ne.jp/keyword/VSCode">VSCode</a>で変更する</h2>
<p>お次はVRM1.0のExpression(表情)のOverride設定を<a class="keyword" href="https://d.hatena.ne.jp/keyword/VSCode">VSCode</a>で変更してみようと思います。</p>
<p>ちなみに今回の作業内容は以下の「エモート中に口が動くようにする」をUnityを使わず<a class="keyword" href="https://d.hatena.ne.jp/keyword/VSCode">VSCode</a>で実現するやり方になっています。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcreator.cluster.mu%2F2024%2F10%2F31%2Fvrm10-override%2F" title="エモート中に口が動くようにする【VRM1.0 ExpressionのOverride設定をする】 - Cluster Creators Guide|バーチャル空間での創作を学ぶなら" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://creator.cluster.mu/2024/10/31/vrm10-override/">creator.cluster.mu</a></cite></p>
<p>今回用意したVRoidStudioから出力された<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%D0%A5%BF%A1%BC">アバター</a>ですが、↑の記事と同様にOverride設定がないのでエモート「笑顔」のときにまばたきが発生すると表情が崩れてしまいます。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20241216/20241216195930.jpg" width="960" height="510" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p>今回はエモート「笑顔」のときにまばたきをブロックして、表情が崩れるのを回避するように設定したいと思います。</p>
<h2 id="gltfファイルのJSONのから-expressions-項目を確認する">gltfファイルの<a class="keyword" href="https://d.hatena.ne.jp/keyword/JSON">JSON</a>のから "expressions" 項目を確認する</h2>
<p>まずは表情設定項目である "expressions" をgltf内で確認しましょう。"expressions"はVRM1.0におけるExpressionの設定項目になっています。</p>
<p><a class="keyword" href="https://d.hatena.ne.jp/keyword/VSCode">VSCode</a>でgltfファイルを開いて検索(Ctrl+F)で <code>"expressions":</code> と検索すると飛べます。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20241216/20241216193244.png" width="1200" height="636" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p>ここを改変することでExpressionの設定変更が可能になります。</p>
<h2 id="対象のExpressionのoverrideBlinkoverrideLookAtoverrideMouthを変更する">対象のExpressionのoverrideBlink、overrideLookAt、overrideMouthを変更する</h2>
<p>VRM1.0の仕様書の<a href="https://github.com/vrm-c/vrm-specification/blob/master/specification/VRMC_vrm-1.0/expressions.ja.md">VRMC_vrm-1.0/expressions.ja.md</a> の「プロシージャルのオーバーライド」を参照すると以下の記載があります。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20241216/20241216200420.png" width="653" height="204" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20241216/20241216200819.png" width="1037" height="245" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p>つまり対象のExpressionの表情でOverride設定を指定する場合は、<code>overrideBlink</code>, <code>overrideLookAt</code>, <code>overrideMouth</code> それぞれに <code>none</code>, <code>block</code>, <code>blend</code> のいずれかを指定すれば良いことになります。</p>
<p>試しに改変前のgltfファイルのexpressions項目のエモート「笑顔」に該当する<code>happy</code>を見てみると以下のようになっています。</p>
<pre class="code lang-json" data-lang="json" data-unlink> "<span class="synStatement">expressions</span>": <span class="synSpecial">{</span>
"<span class="synStatement">preset</span>": <span class="synSpecial">{</span>
"<span class="synStatement">happy</span>": <span class="synSpecial">{</span>
"<span class="synStatement">morphTargetBinds</span>": <span class="synSpecial">[</span>
<span class="synSpecial">{</span>
"<span class="synStatement">node</span>": <span class="synConstant">126</span>,
"<span class="synStatement">index</span>": <span class="synConstant">3</span>,
"<span class="synStatement">weight</span>": <span class="synConstant">1</span>
<span class="synSpecial">}</span>
<span class="synSpecial">]</span>,
"<span class="synStatement">isBinary</span>": <span class="synConstant">false</span>,
"<span class="synStatement">overrideBlink</span>": "<span class="synConstant">none</span>",
"<span class="synStatement">overrideLookAt</span>": "<span class="synConstant">none</span>",
"<span class="synStatement">overrideMouth</span>": "<span class="synConstant">none</span>"
<span class="synSpecial">}</span>,
</pre>
<p><code>overrideBlink</code>, <code>overrideLookAt</code>, <code>overrideMouth</code> それぞれに <code>none</code> つまり「指定無し」になっているため、エモート「笑顔」中にまばたきがブロックされていなかったことがわかります。</p>
<p>今回はまばたきや<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%C3%A5%D7%A5%B7%A5%F3%A5%AF">リップシンク</a>などの表情切替をすべてブロックしたいので、<code>overrideBlink</code>, <code>overrideLookAt</code>, <code>overrideMouth</code> に <code>block</code> を指定します。</p>
<pre class="code lang-json" data-lang="json" data-unlink> "<span class="synStatement">expressions</span>": <span class="synSpecial">{</span>
"<span class="synStatement">preset</span>": <span class="synSpecial">{</span>
"<span class="synStatement">happy</span>": <span class="synSpecial">{</span>
"<span class="synStatement">morphTargetBinds</span>": <span class="synSpecial">[</span>
<span class="synSpecial">{</span>
"<span class="synStatement">node</span>": <span class="synConstant">126</span>,
"<span class="synStatement">index</span>": <span class="synConstant">3</span>,
"<span class="synStatement">weight</span>": <span class="synConstant">1</span>
<span class="synSpecial">}</span>
<span class="synSpecial">]</span>,
"<span class="synStatement">isBinary</span>": <span class="synConstant">false</span>,
"<span class="synStatement">overrideBlink</span>": "<span class="synConstant">block</span>",
"<span class="synStatement">overrideLookAt</span>": "<span class="synConstant">block</span>",
"<span class="synStatement">overrideMouth</span>": "<span class="synConstant">block</span>"
<span class="synSpecial">}</span>,
</pre>
<p>こうすることでエモート「笑顔」中は <code>"overrideBlink": "block",</code>によりまばたきが、<code>"overrideMouth": "block"</code>により<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%C3%A5%D7%A5%B7%A5%F3%A5%AF">リップシンク</a>がブロックされるので結果的に表情が崩れることを避けられます。</p>
<h2 id="VRMに再エクスポートして確認する"><a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a>に再エクスポートして確認する</h2>
<p>このgltfファイルからエクスポートしてclusterで確認してみると、エモート「笑顔」中はまばたきが発生せずマイクに声を入れても口が変形しなくなっていることが確認できます。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20241216/20241216203345.jpg" width="960" height="510" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p>他Expression (<code>angry</code>, <code>sad</code>, <code>aa</code>, <code>ih</code>, <code>ou</code>, etc..) も同様に<code>overrideBlink</code>, <code>overrideLookAt</code>, <code>overrideMouth</code> を編集することでOverride設定の変更が可能です。</p>
<p><a class="keyword" href="https://d.hatena.ne.jp/keyword/VSCode">VSCode</a>とUnityのどっちがやりやすいかは人それぞれかと思いますがよかったらご活用ください!</p>
<h1 id="おまけ-VSCodeでモデルをプレビューする">おまけ: <a class="keyword" href="https://d.hatena.ne.jp/keyword/VSCode">VSCode</a>でモデルをプレビューする</h1>
<p>glTF Toolsにはモデルをプレビューする機能がついているので紹介します。</p>
<p>ボタンはgltfファイルを開いた状態のときにタブの右側に出現しています。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20241217/20241217200316.png" width="728" height="246" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20241217/20241217200124.png" width="1200" height="634" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p><a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a>のポーズを変えたりはできませんが、簡易的なモデルの確認に使えます。</p>
<h1 id="雑感">雑感</h1>
<p>去年に引き続き<a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a>を取り上げてみましたがいかがでしたでしょうか?</p>
<p>今年から<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AF%A5%E9%A5%B9%A5%BF">クラスタ</a>ーでもVRM1.0<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%D0%A5%BF%A1%BC">アバター</a>が使用できるようになりましたし、みなさんの<a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a>ライフの一助になれば幸いです。</p>
<p>明日の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AF%A5%E9%A5%B9%A5%BF">クラスタ</a>ー Advent Calendar 2024の18日目は <a href="https://qiita.com/MSA-i">@MSA-i</a>さんの記事になります。お楽しみに!</p>
<p>記事をご覧いただきありがとうございました!それでは~</p>
tsgcpp
【VRM, glTF】3Dアバターファイルフォーマット "VRM" の構造をのぞいてみよう
hatenablog://entry/6801883189063657902
2023-12-06T00:00:00+09:00
2023-12-06T15:03:50+09:00 本記事のテーマは「"VRM" の中身を覗いてみよう」です。ガチガチな解説ではなくおおまかにイメージできるぐらいで紹介しようと思います。
<p>こちらは <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AF%A5%E9%A5%B9%A5%BF">クラスタ</a>ー Advent Calendar 2023 の1ページ目の6日目の記事です!</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fqiita.com%2Fadvent-calendar%2F2023%2Fcluster" title="クラスターのカレンダー | Advent Calendar 2023 - Qiita" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://qiita.com/advent-calendar/2023/cluster">qiita.com</a></cite></p>
<p>前日は <a href="https://qiita.com/neguse_k">@neguse_k</a>さんの「<a href="https://qiita.com/neguse_k/items/b0c9346acf2a248b4eb4">Blenderでポーズを作ってUnityに取り込む</a>」でした!<a class="keyword" href="https://d.hatena.ne.jp/keyword/Blender">Blender</a>でポーズを作成されている方はこちらの記事を参考にぜひclusterにも組み込んでみてください!</p>
<hr />
<p>こんにちは、すぎしーです。 <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AF%A5%E9%A5%B9%A5%BF">クラスタ</a>ー株式会社のUnityエンジニアになってちょうど1年が経ちました。エンジニアとしてできることも増え、やりがいのある日々を送っています!</p>
<p>さて、本記事のテーマは「"<a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a>" の構造をのぞいてみよう」です。ガチな解説ではなくおおまかに<a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a>の中身をイメージできるぐらいで紹介したいと思います。</p>
<h1 id="使用するツール">使用するツール</h1>
<ul>
<li><a href="https://code.visualstudio.com/">Visual Studio Code</a></li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=ms-vscode.hexeditor">Hex Editor</a>
<ul>
<li><a class="keyword" href="https://d.hatena.ne.jp/keyword/Visual%20Studio%20Code">Visual Studio Code</a>のエクステンションで、いわゆる<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D0%A5%A4%A5%CA%A5%EA%A5%A8%A5%C7%A5%A3%A5%BF">バイナリエディタ</a>です。</li>
</ul>
</li>
<li><a href="https://malaybaku.github.io/VMagicMirror/">VMagicMirror</a>
<ul>
<li><a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a>の確認用として使用します。</li>
<li>開発者の<a href="https://twitter.com/baku_dreameater">獏星(ばくすたー)</a>さんも<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%C9%A5%D9%A5%F3%A5%C8%A5%AB%A5%EC%A5%F3%A5%C0%A1%BC">アドベントカレンダー</a>で記事を投稿されています! → 「<a href="https://note.com/baku_dreameater/n/n26908f443e5b">VRM Animation (.vrma)をUnity上で簡単に生成できるようにした話</a>」</li>
</ul>
</li>
</ul>
<p>今回はUnityとUniVRMは使用しません!</p>
<h1 id="使用するVRM">使用する<a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a></h1>
<ul>
<li><a href="https://github.com/vrm-c/UniVRM/blob/v0.115.0/Tests/Models/Alicia_vrm-0.51/AliciaSolid_vrm-0.51.vrm">AliciaSolid_vrm-0.51.vrm</a>
<ul>
<li><a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a>コンソーシアムから提供されているサンプル<a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a></li>
<li><a href="https://3d.nicovideo.jp/alicia/">アリシア・ソリッド</a>ちゃんの<a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a>になっています</li>
</ul>
</li>
</ul>
<h1 id="VRMについて"><a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a>について</h1>
<p><a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a>とは、<a href="https://vrm.dev/vrm/vrm_about.html">VRMコンソーシアム</a>が提唱している人型<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%D0%A5%BF%A1%BC">アバター</a>を定義するファイルフォーマットとなっていて、<a href="https://cluster.mu/">cluster</a>でも<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%D0%A5%BF%A1%BC">アバター</a>の形式として使用されています。 <a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a>に対応した様々なアプリケーションで<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%D0%A5%BF%A1%BC">アバター</a>を表示することができます。</p>
<p><figure class="figure-image figure-image-fotolife" title="例: VMagicMirrorでアリシア・ソリッドちゃんのVRMを読み込んでみた"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20231205/20231205224540.png" width="485" height="600" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>例: VMagicMirrorで<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%EA%A5%B7%A5%A2">アリシア</a>・ソリッドちゃんの<a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a>を読み込んで表示</figcaption></figure></p>
<p><a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%D0%A5%BF%A1%BC">アバター</a>を作ったり使ったりするだけならいろいろなツールが提供されているので構造を知る必要はないんですが、知っておくとさらに<a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a>と仲良くなれるかも?</p>
<h1 id="VRMはglTF-20がベースになっている"><a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a>はglTF-2.0がベースになっている</h1>
<p><a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a>コンソーシアムから公開されている<a href="https://github.com/vrm-c/vrm-specification/blob/master/specification/0.0/README.ja.md">VRM仕様</a>を見ると以下のように記載されています。</p>
<blockquote><p>glTF-2.0のバイナリ形式glbをベースにした、<a class="keyword" href="https://d.hatena.ne.jp/keyword/VR">VR</a>向けモデルフォーマットです。</p></blockquote>
<p>つまり結論を言ってしまうと <strong>"<a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a>の構造" ≒ "glTF-2.0の構造"</strong> と言えます! ということでまずはglTF-2.0の構造について簡単に紹介します。</p>
<h2 id="glTF-20-はバイナリとJSONの2つで構成されている">glTF-2.0 はバイナリと<a class="keyword" href="https://d.hatena.ne.jp/keyword/JSON">JSON</a>の2つで構成されている</h2>
<p>glTFは3Dコンテンツ向けのファイルフォーマットでKhronos Groupから提供されています。仕様書は <a href="https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html">glTF™ 2.0 Specification</a>にあります。glTFファイルには主に以下の情報が1ファイル内に格納できるように設計されています。</p>
<ul>
<li>オブジェクトの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D2%A5%A8%A5%E9%A5%EB%A5%AD%A1%BC">ヒエラルキー</a></li>
<li>メッシュ (法線やウェイトなども含む)</li>
<li>テクスチャ</li>
<li>マテリアル</li>
<li>etc...</li>
</ul>
<p>glTFはバイナリファイルですが、中身は「ヘッダ領域」を除いて「<a class="keyword" href="https://d.hatena.ne.jp/keyword/JSON">JSON</a>領域」と「バイナリ領域」のチャンクで構成されています。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20231203/20231203153219.png" width="1137" height="129" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p>AliciaSolid_<a class="keyword" href="https://d.hatena.ne.jp/keyword/vrm">vrm</a>-0.51.<a class="keyword" href="https://d.hatena.ne.jp/keyword/vrm">vrm</a>をHex Editorで見ると、<a class="keyword" href="https://d.hatena.ne.jp/keyword/JSON">JSON</a>文字列が格納されていることが確認できます。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20231203/20231203155103.png" width="952" height="358" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p>そして、ある境界からテキスト部分ではなくバイナリ形式で格納された領域を確認することができます。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20231203/20231203155248.png" width="957" height="280" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p>ここで<a class="keyword" href="https://d.hatena.ne.jp/keyword/JSON">JSON</a>領域とバイナリ領域を少し深堀りします。</p>
<h3 id="JSON領域"><a class="keyword" href="https://d.hatena.ne.jp/keyword/JSON">JSON</a>領域</h3>
<p><a class="keyword" href="https://d.hatena.ne.jp/keyword/JSON">JSON</a>領域には以下のような情報が格納されています。</p>
<table>
<thead>
<tr>
<th>項目名</th>
<th>説明</th>
</tr>
</thead>
<tbody>
<tr>
<td>buffer</td>
<td>シンプルなバイナリ領域のアドレス情報。ほとんどの<a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a>は1つだけ持っている。</td>
</tr>
<tr>
<td>bufferView</td>
<td>bufferをさらに区分けしたもの。</td>
</tr>
<tr>
<td>image</td>
<td>あるbufferViewをどの画像形式でロードするかの情報を持つ。画像形式はmimeTypeで表現される。 </td>
</tr>
<tr>
<td>accessor</td>
<td>どのbufferViewをどうやって読み込むかの情報を持つ。</td>
</tr>
<tr>
<td>mesh</td>
<td>メッシュを構成するための情報を持つ。プリミティブ(マテリアルを割り当てる単位)毎に頂点位置(POSITION)、法線(NORMAL)、UV座標(TEXCOORD_0)などをaccessorを介して取得する。</td>
</tr>
<tr>
<td>etc...</td>
<td> - </td>
</tr>
</tbody>
</table>
<p>例として、以下はAliciaSolid_<a class="keyword" href="https://d.hatena.ne.jp/keyword/vrm">vrm</a>-0.51.<a class="keyword" href="https://d.hatena.ne.jp/keyword/vrm">vrm</a>のノード情報(nodes)の<a class="keyword" href="https://d.hatena.ne.jp/keyword/JSON">JSON</a>の一部を整形表示したものになります。</p>
<p>ノードは位置や回転のほかにメッシュの有無の情報を持ちます。</p>
<p><em>Unityを知っている方向けに説明すれば、ノードはGameObjectのようなものでボーンとしても活用されたり、meshがあればMeshRenderer、さらにskinがあればSkinnedMeshRendererを持つようなイメージとなります。</em></p>
<pre class="code lang-json" data-lang="json" data-unlink>"<span class="synStatement">nodes</span>": <span class="synSpecial">[</span>
<span class="synSpecial">{</span>
"<span class="synStatement">name</span>": "<span class="synConstant">mesh</span>",
"<span class="synStatement">children</span>": <span class="synSpecial">[</span> <span class="synConstant">1</span>, <span class="synConstant">2</span>, <span class="synConstant">3</span>, <span class="synConstant">4</span>, <span class="synConstant">5</span>, <span class="synConstant">6</span>, <span class="synConstant">7</span>, <span class="synConstant">8</span>, <span class="synConstant">9</span>, <span class="synConstant">10</span>, <span class="synConstant">11</span>, <span class="synConstant">12</span> <span class="synSpecial">]</span>,
"<span class="synStatement">translation</span>": <span class="synSpecial">[</span> <span class="synConstant">0</span>, <span class="synConstant">0</span>, <span class="synConstant">0</span> <span class="synSpecial">]</span>,
"<span class="synStatement">rotation</span>": <span class="synSpecial">[</span> <span class="synConstant">0</span>, <span class="synConstant">0</span>, <span class="synConstant">0</span>, <span class="synConstant">1</span> <span class="synSpecial">]</span>,
"<span class="synStatement">scale</span>": <span class="synSpecial">[</span> <span class="synConstant">1</span>, <span class="synConstant">1</span>, <span class="synConstant">1</span> <span class="synSpecial">]</span>,
"<span class="synStatement">extras</span>": <span class="synSpecial">{}</span>
<span class="synSpecial">}</span>,
<span class="synSpecial">{</span>
"<span class="synStatement">name</span>": "<span class="synConstant">body_top</span>",
"<span class="synStatement">translation</span>": <span class="synSpecial">[</span><span class="synConstant">0</span>, <span class="synConstant">0</span>, <span class="synConstant">0</span><span class="synSpecial">]</span>,
"<span class="synStatement">rotation</span>": <span class="synSpecial">[</span><span class="synConstant">0</span>, <span class="synConstant">0</span>, <span class="synConstant">0</span>, <span class="synConstant">1</span><span class="synSpecial">]</span>,
"<span class="synStatement">scale</span>": <span class="synSpecial">[</span><span class="synConstant">1</span>, <span class="synConstant">1</span>, <span class="synConstant">1</span><span class="synSpecial">]</span>,
"<span class="synStatement">mesh</span>": <span class="synConstant">0</span>,
"<span class="synStatement">skin</span>": <span class="synConstant">0</span>,
"<span class="synStatement">extras</span>": <span class="synSpecial">{}</span>
<span class="synSpecial">}</span>,
<span class="synError">// ...</span>
<span class="synSpecial">{</span>
"<span class="synStatement">name</span>": "<span class="synConstant">Head</span>",
"<span class="synStatement">children</span>": <span class="synSpecial">[</span> <span class="synConstant">77</span>, <span class="synConstant">78</span>, <span class="synConstant">79</span>, <span class="synConstant">80</span>, <span class="synConstant">81</span>, <span class="synConstant">89</span>, <span class="synConstant">97</span>, <span class="synConstant">99</span>, <span class="synConstant">101</span>, <span class="synConstant">103</span>, <span class="synConstant">104</span>, <span class="synConstant">108</span>, <span class="synConstant">109</span>, <span class="synConstant">113</span>, <span class="synConstant">114</span> <span class="synSpecial">]</span>,
"<span class="synStatement">translation</span>": <span class="synSpecial">[</span> <span class="synConstant">-9.235208e-9</span>, <span class="synConstant">0.0388788</span>, <span class="synConstant">-0.00014353916</span> <span class="synSpecial">]</span>,
"<span class="synStatement">rotation</span>": <span class="synSpecial">[</span> <span class="synConstant">0</span>, <span class="synConstant">0</span>, <span class="synConstant">0</span>, <span class="synConstant">1</span> <span class="synSpecial">]</span>,
"<span class="synStatement">scale</span>": <span class="synSpecial">[</span> <span class="synConstant">1</span>, <span class="synConstant">1</span>, <span class="synConstant">1</span> <span class="synSpecial">]</span>,
"<span class="synStatement">extras</span>": <span class="synSpecial">{}</span>
<span class="synSpecial">}</span>,
</pre>
<p><em>※↑の<a class="keyword" href="https://d.hatena.ne.jp/keyword/JSON">JSON</a>は成形したもので、<a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a>内の<a class="keyword" href="https://d.hatena.ne.jp/keyword/JSON">JSON</a>領域ではスペースや改行は基本的に除外されています</em></p>
<h3 id="バイナリ領域">バイナリ領域</h3>
<p>バイナリ領域(Binary Buffer)は何を表しているかというと、メッシュ頂点やテクスチャ、モーフがバイナリデータとして格納されています。</p>
<p>実は<strong>バイナリ領域単体では何のデータかはわかりません</strong>。バイナリ領域の読み込み方ですが、<a class="keyword" href="https://d.hatena.ne.jp/keyword/JSON">JSON</a>項目の<strong>buffer、bufferView + 各情報から確定</strong> させる流れになります。</p>
<h3 id="JSON項目からバイナリ領域を読み込む流れ"><a class="keyword" href="https://d.hatena.ne.jp/keyword/JSON">JSON</a>項目からバイナリ領域を読み込む流れ</h3>
<h4 id="buffer">buffer</h4>
<p>bufferはバイナリ領域の一番大きな単位です。以下のように、バイトの長さのみでとてもシンプルです。</p>
<p>前述したとおり、ほとんどの<a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a>は1つだけ持っています。</p>
<pre class="code lang-json" data-lang="json" data-unlink> "<span class="synStatement">buffers</span>": <span class="synSpecial">[</span>
<span class="synSpecial">{</span>
"<span class="synStatement">byteLength</span>": <span class="synConstant">7772784</span>
<span class="synSpecial">}</span>
<span class="synSpecial">]</span>,
</pre>
<h4 id="bufferView">bufferView</h4>
<p>bufferViewはbufferを区切ったもので、大雑把なイメージは以下です。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20231206/20231206142017.png" width="1061" height="88" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p>以下の<a class="keyword" href="https://d.hatena.ne.jp/keyword/JSON">JSON</a>を例にするとbufferViewの0番目は「0番目のbufferのオフセット0から344698バイト」、bufferViewの1番目は「0番目のbufferのオフセット344698から136063バイト」、...のように表現されます。</p>
<pre class="code lang-json" data-lang="json" data-unlink> "<span class="synStatement">bufferViews</span>": <span class="synSpecial">[</span>
<span class="synSpecial">{</span>
"<span class="synStatement">buffer</span>": <span class="synConstant">0</span>,
"<span class="synStatement">byteOffset</span>": <span class="synConstant">0</span>,
"<span class="synStatement">byteLength</span>": <span class="synConstant">344698</span>
<span class="synSpecial">}</span>,
<span class="synSpecial">{</span>
"<span class="synStatement">buffer</span>": <span class="synConstant">0</span>,
"<span class="synStatement">byteOffset</span>": <span class="synConstant">344698</span>,
"<span class="synStatement">byteLength</span>": <span class="synConstant">136063</span>
<span class="synSpecial">}</span>,
<span class="synSpecial">{</span>
"<span class="synStatement">buffer</span>": <span class="synConstant">0</span>,
"<span class="synStatement">byteOffset</span>": <span class="synConstant">480761</span>,
"<span class="synStatement">byteLength</span>": <span class="synConstant">1708146</span>
<span class="synSpecial">}</span>,
<span class="synError">// ...</span>
</pre>
<p>あくまでbufferを区切っただけなので、bufferView単体でも何のデータとして読めばいいかはわかりません。後述する<strong>imageやaccessorなどから活用方法を確定させる</strong>ことになります。</p>
<h4 id="image">image</h4>
<p>テクスチャに使用される画像ファイルはimage項目から参照できます。<br/>
例えば"<a class="keyword" href="https://d.hatena.ne.jp/keyword/Alicia">Alicia</a>_body"というテクスチャ画像は「bufferViewの0番目を<a class="keyword" href="https://d.hatena.ne.jp/keyword/png">png</a>形式でロードする」となります。</p>
<p><code>"bufferView": 0, "mimeType": "image\/png"</code>の場合は、「bufferViewの0番目を<a class="keyword" href="https://d.hatena.ne.jp/keyword/png">png</a>画像として読み込む」という解釈になります。</p>
<pre class="code lang-cs" data-lang="cs" data-unlink> <span class="synConstant">"images"</span><span class="synStatement">:</span> [
{
<span class="synConstant">"name"</span><span class="synStatement">:</span> <span class="synConstant">"Alicia_body"</span>,
<span class="synConstant">"bufferView"</span><span class="synStatement">:</span> <span class="synConstant">0</span>,
<span class="synConstant">"mimeType"</span><span class="synStatement">:</span> <span class="synConstant">"image</span><span class="synError">\/</span><span class="synConstant">png"</span>
},
{
<span class="synConstant">"name"</span><span class="synStatement">:</span> <span class="synConstant">"Sphere"</span>,
<span class="synConstant">"bufferView"</span><span class="synStatement">:</span> <span class="synConstant">1</span>,
<span class="synConstant">"mimeType"</span><span class="synStatement">:</span> <span class="synConstant">"image</span><span class="synError">\/</span><span class="synConstant">png"</span>
},
<span class="synComment">// ...</span>
</pre>
<h4 id="accessor">accessor</h4>
<p>accessorは以下のように情報を扱います。</p>
<ul>
<li>typeで整数(int) or <a class="keyword" href="https://d.hatena.ne.jp/keyword/%C9%E2%C6%B0%BE%AE%BF%F4">浮動小数</a>点(float)を決定</li>
<li>componentTypeでScalar(1個毎) or Vector3(3個毎) or Matrix4x4(16個毎) or etcを決定
<ul>
<li>定義は<a href="https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#accessor-data-types">Accessor Data Types</a>から確認できます。5126はfloatになります。</li>
</ul>
</li>
</ul>
<p>accessorのみでは明確な用途は不明なため、さらに別の項目から使用されます。</p>
<pre class="code lang-json" data-lang="json" data-unlink> "<span class="synStatement">accessors</span>": <span class="synSpecial">[</span>
<span class="synSpecial">{</span>
"<span class="synStatement">bufferView</span>": <span class="synConstant">7</span>,
"<span class="synStatement">byteOffset</span>": <span class="synConstant">0</span>,
"<span class="synStatement">type</span>": "<span class="synConstant">VEC3</span>",
"<span class="synStatement">componentType</span>": <span class="synConstant">5126</span>,
"<span class="synStatement">count</span>": <span class="synConstant">4804</span>,
"<span class="synStatement">max</span>": <span class="synSpecial">[</span>
<span class="synConstant">0.614650965</span>,
<span class="synConstant">1.31239367</span>,
<span class="synConstant">0.150282308</span>
<span class="synSpecial">]</span>,
"<span class="synStatement">min</span>": <span class="synSpecial">[</span>
<span class="synConstant">-0.614748538</span>,
<span class="synConstant">0.9991788</span>,
<span class="synConstant">-0.0840013</span>
<span class="synSpecial">]</span>,
"<span class="synStatement">normalized</span>": <span class="synConstant">false</span>
<span class="synSpecial">}</span>,
<span class="synSpecial">{</span>
"<span class="synStatement">bufferView</span>": <span class="synConstant">8</span>,
"<span class="synStatement">byteOffset</span>": <span class="synConstant">0</span>,
"<span class="synStatement">type</span>": "<span class="synConstant">VEC3</span>",
"<span class="synStatement">componentType</span>": <span class="synConstant">5126</span>,
"<span class="synStatement">count</span>": <span class="synConstant">4804</span>,
"<span class="synStatement">normalized</span>": <span class="synConstant">false</span>
<span class="synSpecial">}</span>,
<span class="synError">// ...</span>
</pre>
<h4 id="mesh">mesh</h4>
<p>primitivesのattributesとして頂点位置(POSITION)や法線(NORMAL)を持っており、それらの数値がaccessorのインデックス値を表しています。<br/>
<code>"POSITION": 0</code> の場合は、「accessorsの0番目を頂点位置として使用する」という解釈になります。頂点位置なのでそのaccessorのtypeはVector3(VEC3)になっているはずです。</p>
<pre class="code lang-json" data-lang="json" data-unlink> "<span class="synStatement">meshes</span>": <span class="synSpecial">[</span>
<span class="synSpecial">{</span>
"<span class="synStatement">name</span>": "<span class="synConstant">body_top.baked</span>",
"<span class="synStatement">primitives</span>": <span class="synSpecial">[</span>
<span class="synSpecial">{</span>
"<span class="synStatement">mode</span>": <span class="synConstant">4</span>,
"<span class="synStatement">indices</span>": <span class="synConstant">5</span>,
"<span class="synStatement">attributes</span>": <span class="synSpecial">{</span>
"<span class="synStatement">POSITION</span>": <span class="synConstant">0</span>,
"<span class="synStatement">NORMAL</span>": <span class="synConstant">1</span>,
"<span class="synStatement">TEXCOORD_0</span>": <span class="synConstant">2</span>,
"<span class="synStatement">JOINTS_0</span>": <span class="synConstant">4</span>,
"<span class="synStatement">WEIGHTS_0</span>": <span class="synConstant">3</span>
<span class="synSpecial">}</span>,
"<span class="synStatement">material</span>": <span class="synConstant">0</span>
<span class="synSpecial">}</span>,
<span class="synError">// ...</span>
</pre>
<h1 id="VRMはglTF拡張にVRM特有の情報を持つ"><a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a>はglTF拡張に<a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a>特有の情報を持つ</h1>
<p>glTFは前述のとおり<a class="keyword" href="https://d.hatena.ne.jp/keyword/JSON">JSON</a>領域を持っていますが、この<a class="keyword" href="https://d.hatena.ne.jp/keyword/JSON">JSON</a>に追加情報を格納しても良いことになっています。この<strong>追加情報のことをglTF拡張(glTF Extensions)</strong>と呼びます。</p>
<p><a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a>はこのglTF拡張部分に<a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a>特有の情報を持たせることで実現されていて、VRM0.xの場合は<code>extensions.VRM</code>項目に格納されています。</p>
<p>以下はAliciaSolid_<a class="keyword" href="https://d.hatena.ne.jp/keyword/vrm">vrm</a>-0.51.<a class="keyword" href="https://d.hatena.ne.jp/keyword/vrm">vrm</a>から抽出した情報ですが、一部紹介します。</p>
<table>
<thead>
<tr>
<th>拡張項目名</th>
<th>説明</th>
</tr>
</thead>
<tbody>
<tr>
<td>humanoid</td>
<td> <a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a>の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D2%A5%E5%A1%BC%A5%DE%A5%CE%A5%A4%A5%C9">ヒューマノイド</a>(人型)のボーン情報 </td>
</tr>
<tr>
<td>secondaryAnimation</td>
<td> <a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a>のSpringBoneの情報 </td>
</tr>
<tr>
<td>etc...</td>
<td> - </td>
</tr>
</tbody>
</table>
<pre class="code lang-json" data-lang="json" data-unlink>"<span class="synStatement">extensions</span>": <span class="synSpecial">{</span>
"<span class="synStatement">VRM</span>": <span class="synSpecial">{</span>
"<span class="synStatement">exporterVersion</span>": "<span class="synConstant">UniVRM-0.51.0</span>",
"<span class="synStatement">specVersion</span>": "<span class="synConstant">0.0</span>",
"<span class="synStatement">meta</span>": <span class="synSpecial">{</span>
"<span class="synStatement">title</span>": "<span class="synConstant">Alicia Solid</span>",
"<span class="synStatement">version</span>": "<span class="synConstant">1.10</span>",
"<span class="synStatement">author</span>": "<span class="synConstant">© DWANGO Co., Ltd.</span>",
<span class="synError">// ...</span>
<span class="synSpecial">}</span>,
"<span class="synStatement">humanoid</span>": <span class="synSpecial">{</span> <span class="synError">// ...</span>
<span class="synSpecial">}</span>,
"<span class="synStatement">firstPerson</span>": <span class="synSpecial">{</span> <span class="synError">// ...</span>
<span class="synSpecial">}</span>,
"<span class="synStatement">blendShapeMaster</span>": <span class="synSpecial">{</span> <span class="synError">// ...</span>
<span class="synSpecial">}</span>,
"<span class="synStatement">secondaryAnimation</span>": <span class="synSpecial">{</span> <span class="synError">// ...</span>
<span class="synSpecial">}</span>,
"<span class="synStatement">materialProperties</span>": <span class="synSpecial">[</span>
<span class="synSpecial">]</span>
<span class="synSpecial">}</span>
<span class="synError">// ...</span>
</pre>
<p>ということで<a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a>の構造を簡単に説明してみました。もう少しglTFに詳しくなりたいな~という方は <a href="https://www.khronos.org/files/gltf20-reference-guide.pdf">gltf20-reference-guide.pdf</a> が図もあったりして参考になるかと思います!</p>
<p><a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a>はglTFの仕組みをうまく利用したファイルフォーマットだったんですね~。</p>
<h1 id="おまけ">おまけ</h1>
<h2 id="バイナリエディタでVRMを編集してみよう"><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D0%A5%A4%A5%CA%A5%EA%A5%A8%A5%C7%A5%A3%A5%BF">バイナリエディタ</a>で<a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a>を編集してみよう</h2>
<p>さて、glTFの大まかな構造が分かったところで Hex Editor でAliciaSolid_<a class="keyword" href="https://d.hatena.ne.jp/keyword/vrm">vrm</a>-0.51.<a class="keyword" href="https://d.hatena.ne.jp/keyword/vrm">vrm</a>を編集してみましょう!</p>
<p>今回は編集結果がはっきりとわかるようにマテリアルの色の乗算値を<span style="color: #ff0000">赤色</span>に変更してみます。</p>
<p>VRM0.xのMToonの色の乗算値は <code>extensions.VRM.materialProperties[*].vectorProperties._Color</code> です。Hex Editorで文字列<code>"_Color"</code>を検索すると見つけられます。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20231203/20231203204440.png" width="948" height="133" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p>乗算値は白(<code>[1,1,1,1]</code>)になっていようなので、置換処理で <code>"_Color":[1,1,1,1],</code> → <code>"_Color":[1,0,0,1],</code>に変更保存して、VMagicMirrorで確認してみましょう。</p>
<ol>
<li>AliciaSolid_<a class="keyword" href="https://d.hatena.ne.jp/keyword/vrm">vrm</a>-0.51.<a class="keyword" href="https://d.hatena.ne.jp/keyword/vrm">vrm</a>からAliciaSolid_<a class="keyword" href="https://d.hatena.ne.jp/keyword/vrm">vrm</a>-0.51_edited.<a class="keyword" href="https://d.hatena.ne.jp/keyword/vrm">vrm</a>をコピーして作成</li>
<li>Hex Editorを開く</li>
<li>Ctrl+F → 置換モードに切替 → 置換を実施</li>
<li>Ctrl+Sで保存する</li>
<li>VMagicMirrorでAliciaSolid_<a class="keyword" href="https://d.hatena.ne.jp/keyword/vrm">vrm</a>-0.51_edited.<a class="keyword" href="https://d.hatena.ne.jp/keyword/vrm">vrm</a>をロード</li>
</ol>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20231203/20231203205438.png" width="1200" height="403" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p>以下が読み込んだ結果です。しっかり赤色になっていますね。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20231205/20231205224824.png" width="485" height="600" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p>実際に<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D0%A5%A4%A5%CA%A5%EA%A5%A8%A5%C7%A5%A3%A5%BF">バイナリエディタ</a>で<a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a>を編集することはほとんどないと思いますが、「<a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a>の構造を知ってるとこんなこともできるよ~」という紹介でした。</p>
<h2 id="VRM0x-と-VRM10-について">VRM0.x と VRM1.0 について</h2>
<p>まだclusterではVRM1.0に対応はしていませんが、せっかくなのでVRM1.0についても少し触れようと思います。</p>
<p>VRM1.0はVRM0.xよりさらにglTFに準拠したフォーマットになっています。具体的に言うと以下のような変更が入りました。</p>
<ul>
<li>UniVRM以外の一般的なglTFライブラリでもロードしやすいようにbufferViewの扱いが変更された</li>
<li>glTF拡張での名前は <code>extensions.VRMC_vrm</code>、<code>extensions.VRMC_springBone</code>、<code>VRMC_materials_mtoon</code>などに変更された</li>
<li>マテリアルのパラメータはglTF標準の<code>materials</code>項目を使用するようになった
<ul>
<li>色の乗算値を例にすると <code>extensions.VRM.materialProperties[*].vectorProperties._Color</code> → <code>materials[*].pbrMetallicRoughness.baseColorFactor</code> のようにglTF標準のパラメータが使用されるようになった</li>
<li>Unity特有のシェーダープロパティ名は使用しなくなった</li>
</ul>
</li>
</ul>
<p>変更点の詳細については <a href="https://vrm.dev/vrm1/changed.html">VRM-1.0の変更点</a> で確認できます。</p>
<p>VRM1.0は<a href="https://github.com/vrm-c/vrm-specification/blob/master/specification/VRMC_vrm-1.0/README.ja.md">vrm-specification</a>にて仕様がより明確に記載され、MToonについても項目が定義されたりとUnity製アプリ以外でも<a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a>を利用できるように見直されています。</p>
<p>ハロクラで発表されているようにclusterもVRM1.0対応を進めていますため、リリースされたらぜひVRM1.0<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%D0%A5%BF%A1%BC">アバター</a>でclusterを楽しんでください!</p>
<h2 id="参考">参考</h2>
<ul>
<li><a href="https://www.khronos.org/files/gltf20-reference-guide.pdf">gltf20-reference-guide.pdf</a></li>
<li><a href="https://github.com/KhronosGroup/glTF">github.com/KhronosGroup/glTF</a></li>
<li><a href="https://vrm.dev/index.html">VRMドキュメント</a></li>
</ul>
<h1 id="雑感">雑感</h1>
<p><a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a>の構造について僕なりに紹介してみましたがいかがでしたでしょうか?<br/>
clusterのエンジニアになってプロダクト開発でも<a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a>に触れることも増えたので、せっかくなので知見の共有も兼ねて記事にしてみました。</p>
<p><a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a>は<a class="keyword" href="https://d.hatena.ne.jp/keyword/VRM">VRM</a> Meetupも開催されたりと盛り上がりを見せているので、今後も注目していきたいです!</p>
<p>記事をご覧いただきありがとうございました!</p>
<p>明日のAdvent Calendar 2023 7日目は <a href="https://qiita.com/uzzu">@uzzu</a>さんの「UnityのPlay Asset DeliveryをtargetSdk34に対応させる」です。clusterの<a class="keyword" href="https://d.hatena.ne.jp/keyword/Android">Android</a><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%DA%A5%B7%A5%E3">スペシャ</a>リストの記事をどうぞお楽しみに!(Play Asset Deliveryについてはuzzuさんにとても助けられました)</p>
<p>それでは~</p>
tsgcpp
UnityでMoqを使う (Unity2021バージョン)
hatenablog://entry/4207112889944228308
2022-12-17T14:08:49+09:00
2022-12-17T14:08:49+09:00 こちらは クラスター Advent Calendar 2022(2ページ目)の17日目の記事です! 前日はスワンマンさん (@Swanman) の「Unityのエディタ拡張で動的にメニューを追加・削除する」でした! まさかエンジニアではなくカスタマーサポートの方からReflectionを使ったツールの作り方を教えてもらえるとは! Unity上でツールを作るときに知っておくと便利なテクニックになると思いますのでぜひ参考にしてください。 こんにちは、すぎしーです。 クラスター株式会社のUnityエンジニアをなりました! クラスター株式会社のエンジニアになりました!これからもバーチャルにのめり込む所…
<p>こちらは <a href="https://qiita.com/advent-calendar/2022/cluster">クラスター Advent Calendar 2022</a>(2ページ目)の17日目の記事です!</p>
<p>前日はスワンマンさん (<a href="https://qiita.com/Swanman">@Swanman</a>) の「<a href="https://qiita.com/Swanman/items/279b3b679f3f96a5f925">Unityのエディタ拡張で動的にメニューを追加・削除する</a>」でした!</p>
<p>まさかエンジニアではなくカスタマーサポートの方からReflectionを使ったツールの作り方を教えてもらえるとは!<br/>
Unity上でツールを作るときに知っておくと便利なテクニックになると思いますのでぜひ参考にしてください。</p>
<hr />
<p>こんにちは、すぎしーです。
<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%AF%A5%E9%A5%B9%A5%BF">クラスタ</a>ー株式会社のUnityエンジニアをなりました!</p>
<p><blockquote data-conversation="none" class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr"><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%AF%A5%E9%A5%B9%A5%BF">クラスタ</a>ー株式会社のエンジニアになりました!<br>これからもバーチャルにのめり込む所存🚀<a href="https://t.co/zkkbM0HEtE">https://t.co/zkkbM0HEtE</a></p>— すぎしー (@tsgcpp) <a href="https://twitter.com/tsgcpp/status/1598300959435763717?ref_src=twsrc%5Etfw">2022年12月1日</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> </p>
<p>改めてよろしくお願いします。</p>
<ul class="table-of-contents">
<li><a href="#概要">概要</a><ul>
<li><a href="#変更履歴">変更履歴</a></li>
</ul>
</li>
<li><a href="#Moqとは">Moqとは</a></li>
<li><a href="#UnityでMoqを導入">UnityでMoqを導入</a><ul>
<li><a href="#1-MoqとCastleCoreのnupkgをダウンロード">1. MoqとCastle.Coreのnupkgをダウンロード</a></li>
<li><a href="#2-nupkgを展開">2. nupkgを展開</a></li>
<li><a href="#3-dll-を-Unityプロジェクト内に配置">3. dll を Unityプロジェクト内に配置</a></li>
<li><a href="#4-Unity上でdllをTestRunner向けに調整">4. Unity上でdllをTestRunner向けに調整</a></li>
</ul>
</li>
<li><a href="#UnityでMoqを使用">UnityでMoqを使用</a><ul>
<li><a href="#テスト用Assembly-Definitionを作成">テスト用Assembly Definitionを作成</a></li>
<li><a href="#テスト用Assembly-DefinitionにMoqの参照を追加">テスト用Assembly DefinitionにMoqの参照を追加</a></li>
<li><a href="#テストを書いて実行">テストを書いて実行</a></li>
</ul>
</li>
<li><a href="#Moqの使用例">Moqの使用例</a><ul>
<li><a href="#SetupSequenceでコールごとの挙動を指定">SetupSequenceでコールごとの挙動を指定</a></li>
<li><a href="#コール時の引数と回数の検査">コール時の引数と回数の検査</a></li>
<li><a href="#コール時の処理を設定">コール時の処理を設定</a></li>
</ul>
</li>
<li><a href="#モックライブラリを使用するメリット">モックライブラリを使用するメリット</a><ul>
<li><a href="#モックオブジェクトを簡単に生成可能">モックオブジェクトを簡単に生成可能</a></li>
<li><a href="#IDEのリファレンス検索に余計な候補がでない">IDEのリファレンス検索に余計な候補がでない</a></li>
<li><a href="#高度な検証がより簡単に実現可能">高度な検証がより簡単に実現可能</a></li>
</ul>
</li>
<li><a href="#簡易導入方法">簡易導入方法</a></li>
<li><a href="#余談">余談</a><ul>
<li><a href="#Moq-4182以上にする理由">Moq 4.18.2以上にする理由</a></li>
</ul>
</li>
<li><a href="#雑感">雑感</a></li>
<li><a href="#クラスター-Advent-Calendar-2022-明日の記事の紹介">クラスター Advent Calendar 2022 明日の記事の紹介</a></li>
</ul>
<h1 id="概要">概要</h1>
<p>今回の内容は2年前に書いた 「<a href="https://tsgcpp.hateblo.jp/entry/2020/11/27/221411">UnityでのMoq導入方法</a>」のUnity2021版です。<br/>
この2年でUnityもMoqもアップデートされているので、導入も前回より内容を強化した方法で紹介します!</p>
<p>記事の最後の方に、導入までをある程度自動化した方法も載せておきます。</p>
<p><strong>ソフトウェアエンジニア向け</strong> の記事になります。</p>
<h2 id="変更履歴">変更履歴</h2>
<ul>
<li>2022/12/18<a class="keyword" href="http://d.hatena.ne.jp/keyword/%20.NET"> .NET</a> Framework向けの依存dllを追加 及び "Moq 4.18.2以上にする理由"の説明を一部修正</li>
</ul>
<h1 id="Moqとは">Moqとは</h1>
<p>Moqとは <a class="keyword" href="http://d.hatena.ne.jp/keyword/C%23">C#</a>(.Net) 向けのモックオブジェクト作成ライブラリです。</p>
<p>モッククラスはUnitTestなどで依存interfaceと同じふるまい(モック)になるクラスですが、自作で実装するのはなかなか骨が折れる作業になります。<br/>
そんなときにモックライブラリを用いることで簡単にモックオブジェクトを用意でき、より高度なUnitTest (クラスの<a class="keyword" href="http://d.hatena.ne.jp/keyword/%C3%B1%C2%CE%A5%C6%A5%B9%A5%C8">単体テスト</a>) が可能になります。</p>
<p><br></p>
<h1 id="UnityでMoqを導入">UnityでMoqを導入</h1>
<p><strong>※Unity2020でも可能と思いますが、Unity2021以上推奨です!</strong></p>
<p>最初に手作業でのやり方紹介します。</p>
<h2 id="1-MoqとCastleCoreのnupkgをダウンロード">1. MoqとCastle.Coreのnupkgをダウンロード</h2>
<p>NuGetからnupkgをダウンロードします。</p>
<ul>
<li>"<a class="keyword" href="http://d.hatena.ne.jp/keyword/Api">Api</a> Compatibility Level" が ".Net Standard 2.1"
<ul>
<li><a href="https://www.nuget.org/packages/Moq">Moq</a> <strong>4.18.2以上</strong></li>
<li><a href="https://www.nuget.org/packages/Castle.Core">Castle.Core</a> 5.1.0以上</li>
<li><a href="https://www.nuget.org/packages/System.Diagnostics.EventLog">System.Diagnostics.EventLog</a> 4.7.0以上</li>
</ul>
</li>
<li>"<a class="keyword" href="http://d.hatena.ne.jp/keyword/Api">Api</a> Compatibility Level" が "<a class="keyword" href="http://d.hatena.ne.jp/keyword/.Net%20Framework">.Net Framework</a>"
<ul>
<li><a href="https://www.nuget.org/packages/Moq">Moq</a> <strong>4.18.2以上</strong></li>
<li><a href="https://www.nuget.org/packages/Castle.Core">Castle.Core</a> 5.1.0以上</li>
<li><a href="https://www.nuget.org/packages/System.Threading.Tasks.Extensions">System.Threading.Tasks.Extensions</a> 4.5.4以上</li>
<li><a href="https://www.nuget.org/packages/System.Runtime.CompilerServices.Unsafe">System.Runtime.CompilerServices.Unsafe</a> 6.0.0以上</li>
</ul>
</li>
</ul>
<p>Moq 4.18.2以上にする理由は後述します。</p>
<p>ダウンロードはページ横の "Download package" から可能です。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20221211/20221211200306.png" width="1168" height="555" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<h2 id="2-nupkgを展開">2. nupkgを展開</h2>
<p>nupkgの実態はzipなので<a class="keyword" href="http://d.hatena.ne.jp/keyword/7-zip">7-zip</a>などで直接展開できます。<br/>
拡張子を <code>.zip</code> に変えてOS標準のzip展開でも可能です。</p>
<h2 id="3-dll-を-Unityプロジェクト内に配置">3. dll を Unityプロジェクト内に配置</h2>
<p>展開したファイルのうち、以下のファイルをUnity以下に配置しましょう。<br/>
個人的なオススメのフォルダは <code>Plugins/Moq</code> です。</p>
<ul>
<li>"<a class="keyword" href="http://d.hatena.ne.jp/keyword/Api">Api</a> Compatibility Level" が ".Net Standard 2.1"の場合
<ul>
<li><code>moq.4.18.3/lib/netstandard2.1/Moq.dll</code></li>
<li><code>castle.core.5.1.0/lib/netstandard2.1/Castle.Core.dll</code></li>
<li><code>system.diagnostics.eventlog.7.0.0/lib/netstandard2.0/System.Diagnostics.EventLog.dll</code></li>
</ul>
</li>
<li>"<a class="keyword" href="http://d.hatena.ne.jp/keyword/Api">Api</a> Compatibility Level" が "<a class="keyword" href="http://d.hatena.ne.jp/keyword/.Net%20Framework">.Net Framework</a>"の場合
<ul>
<li><code>moq.4.18.3/lib/net462/Moq.dll</code></li>
<li><code>castle.core.5.1.0/lib/net462/Castle.Core.dll</code></li>
<li><code>system.threading.tasks.extensions.4.5.4/lib/net461/System.Threading.Tasks.Extensions.dll</code></li>
<li><code>system.runtime.compilerservices.unsafe.6.0.0/lib/netstandard2.0/System.Runtime.CompilerServices.Unsafe.dll</code></li>
</ul>
</li>
</ul>
<p>以下のように配置してください。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20221212/20221212004251.png" width="315" height="208" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<h2 id="4-Unity上でdllをTestRunner向けに調整">4. Unity上でdllをTestRunner向けに調整</h2>
<p>Moqはあくまでテスト用なので、ビルドしたアプリには含まれないように設定しましょう。</p>
<p>※ビルドしたアプリに混ぜる場合は再頒布となるため各dllのライセンス表記が必要となります。</p>
<p>以下は <span style="color: #ff0000"><strong>Moq.dll, Castle.Core.dll, System.Diagnostics.EventLog.dll全てで実施</strong></span>してください。</p>
<ul>
<li>Inspectorを表示</li>
<li><code>Auto Reference</code> を無効化</li>
<li><code>Validate References</code> を有効化</li>
<li>"Define Constraints" に <code>UNITY_INCLUDE_TESTS</code> を指定</li>
<li>"Apply" ボタンをクリック</li>
</ul>
<p>特に <code>UNITY_INCLUDE_TESTS</code> を指定することでEditMode及びPlayMode Test Runnerで使用できる状態で、<br/>
ビルドしたアプリ本体にはMoqと依存dllが除外されます。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20221212/20221212004605.png" width="803" height="698" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p>以上で導入は完了です!</p>
<p><br></p>
<h1 id="UnityでMoqを使用">UnityでMoqを使用</h1>
<p>次は導入したMoqを使ってみましょう!</p>
<p>※<a href="https://docs.unity3d.com/ja/2021.3/Manual/testing-editortestsrunner.html">Unity Test Framework</a> の詳細は省略します</p>
<h2 id="テスト用Assembly-Definitionを作成">テスト用Assembly Definitionを作成</h2>
<ul>
<li>テスト用<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%EA%A5%D7%A5%C8">スクリプト</a>を配置したいフォルダで右クリック</li>
<li><code>Create -> Testing -> Tests Assembly Folder</code> をクリック</li>
<li>Assembly名を指定してasmdefを作成</li>
</ul>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20221211/20221211204946.png" width="968" height="272" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<h2 id="テスト用Assembly-DefinitionにMoqの参照を追加">テスト用Assembly DefinitionにMoqの参照を追加</h2>
<ul>
<li>テスト用asmdefのInspectorを開く</li>
<li>"Assembly References"に <code>Moq.dll</code> を追加
<ul>
<li><code>Castle.Core.dll</code> と <code>System.Diagnostics.EventLog</code> の指定は基本的に不要 (テスト用<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%EA%A5%D7%A5%C8">スクリプト</a>で直接参照することは稀なため)</li>
</ul>
</li>
</ul>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20221211/20221211205358.png" width="791" height="630" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<h2 id="テストを書いて実行">テストを書いて実行</h2>
<p>あとは普段どおりテスト<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%EA%A5%D7%A5%C8">スクリプト</a>を作成して、Test Runnerで実行するだけです!<br/>
Moqを使った簡易的なテストコードの例を載せておきます。</p>
<pre class="code lang-cs" data-lang="cs" data-unlink><span class="synStatement">using</span> System.Collections.Generic;
<span class="synStatement">using</span> NUnit.Framework;
<span class="synStatement">using</span> Moq;
<span class="synType">public</span> <span class="synType">class</span> <span class="synType">TestFuncProxy</span>
{
<span class="synType">public</span> <span class="synType">interface</span> IFunc
{
<span class="synType">bool</span> Invoke(<span class="synType">int</span> number);
}
[Test]
<span class="synType">public</span> <span class="synType">void</span> Invoke_ReturnsFalse_IfFuncReturnsFalse()
{
<span class="synComment">// Arrange</span>
<span class="synType">var</span> mock <span class="synStatement">=</span> <span class="synStatement">new</span> Mock<IFunc>();
<span class="synType">var</span> target <span class="synStatement">=</span> <span class="synStatement">new</span> FuncProxy(mock.Object);
<span class="synComment">// Note: Moqの仕様でSetupなしの場合はdefaultを返す (bool Invoke(...) の場合はfalse)</span>
<span class="synComment">// FYI: 実際のテストではテストパターンを明確にするために明示しましょう!</span>
<span class="synComment">// Act</span>
<span class="synType">bool</span> actual <span class="synStatement">=</span> target.Invoke(<span class="synConstant">3</span>);
<span class="synComment">// Assert</span>
Assert.That(actual, Is.False);
}
[Test]
<span class="synType">public</span> <span class="synType">void</span> Invoke_ReturnsTrue_IfFuncReturnsTrue()
{
<span class="synComment">// Arrange</span>
<span class="synType">var</span> mock <span class="synStatement">=</span> <span class="synStatement">new</span> Mock<IFunc>();
<span class="synType">var</span> target <span class="synStatement">=</span> <span class="synStatement">new</span> FuncProxy(mock.Object);
<span class="synComment">// Note: 引数3を渡されたらtrueを返す</span>
mock.Setup(m <span class="synStatement">=></span> m.Invoke(<span class="synConstant">3</span>)).Returns(<span class="synConstant">true</span>);
<span class="synComment">// Act</span>
<span class="synType">bool</span> actual <span class="synStatement">=</span> target.Invoke(<span class="synConstant">3</span>);
<span class="synComment">// Assert</span>
Assert.That(actual, Is.True);
}
}
</pre>
<p>以下は実行結果です。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20221211/20221211210931.png" width="629" height="205" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p>使用方法の紹介は以上です!</p>
<h1 id="Moqの使用例">Moqの使用例</h1>
<p>Moqでできることをちょっと紹介します!</p>
<h2 id="SetupSequenceでコールごとの挙動を指定">SetupSequenceでコールごとの挙動を指定</h2>
<p><code>SetupSequence</code> で指定するとコールごとの戻り値を指定できます。</p>
<pre class="code lang-cs" data-lang="cs" data-unlink> [Test]
<span class="synType">public</span> <span class="synType">void</span> Example_SetupSequence()
{
<span class="synType">var</span> mock <span class="synStatement">=</span> <span class="synStatement">new</span> Mock<IFunc>();
<span class="synComment">// 渡された引数に関係なくfalse -> true -> false -> throw Exception</span>
mock.SetupSequence(m <span class="synStatement">=></span> m.Invoke(It.IsAny<<span class="synType">int</span>>()))
.Returns(<span class="synConstant">false</span>)
.Returns(<span class="synConstant">true</span>)
.Returns(<span class="synConstant">false</span>)
.Throws(<span class="synStatement">new</span> System.Exception(<span class="synConstant">"Unexpected Call"</span>));
Assert.That(mock.Object.Invoke(<span class="synStatement">default</span>), Is.False);
Assert.That(mock.Object.Invoke(<span class="synStatement">default</span>), Is.True);
Assert.That(mock.Object.Invoke(<span class="synStatement">default</span>), Is.False);
Assert.Throws<System.Exception>(() <span class="synStatement">=></span> mock.Object.Invoke(<span class="synStatement">default</span>));
}
</pre>
<h2 id="コール時の引数と回数の検査">コール時の引数と回数の検査</h2>
<p><code>Verify</code> を使用するとコールされたときの引数やその引数でのコール回数を検査することができます。<br/>
Moqを使う場合は一番使う機能ではないかと!</p>
<pre class="code lang-cs" data-lang="cs" data-unlink> [Test]
<span class="synType">public</span> <span class="synType">void</span> Example_Verify()
{
<span class="synType">var</span> mock <span class="synStatement">=</span> <span class="synStatement">new</span> Mock<IFunc>();
mock.Object.Invoke(<span class="synConstant">2</span>);
mock.Object.Invoke(<span class="synConstant">5</span>);
mock.Object.Invoke(<span class="synConstant">2</span>);
<span class="synComment">// 引数2で2回コールされたことの検証</span>
mock.Verify(m <span class="synStatement">=></span> m.Invoke(<span class="synConstant">2</span>), Times.Exactly(<span class="synConstant">2</span>));
<span class="synComment">// 引数関係なく3回以上コールされたことの検査</span>
mock.Verify(m <span class="synStatement">=></span> m.Invoke(It.IsAny<<span class="synType">int</span>>()), Times.AtLeast(<span class="synConstant">3</span>));
}
</pre>
<p>不正の場合は例外(<code>MockException</code>)が出て、テストが失敗します。</p>
<h2 id="コール時の処理を設定">コール時の処理を設定</h2>
<p><code>Callback</code> を使用するとコールされたときの処理を設定できます。<br/>
複数オブジェクトのコールされた順番を検査するときなどに利用できます。</p>
<pre class="code lang-cs" data-lang="cs" data-unlink> [Test]
<span class="synType">public</span> <span class="synType">void</span> Example_Callback()
{
<span class="synType">var</span> messageList <span class="synStatement">=</span> <span class="synStatement">new</span> List<<span class="synType">string</span>>();
<span class="synType">var</span> mock1 <span class="synStatement">=</span> <span class="synStatement">new</span> Mock<IFunc>();
<span class="synType">var</span> mock2 <span class="synStatement">=</span> <span class="synStatement">new</span> Mock<IFunc>();
<span class="synType">var</span> mock3 <span class="synStatement">=</span> <span class="synStatement">new</span> Mock<IFunc>();
<span class="synComment">// コールされたら messageList に文字列を追加</span>
mock1.Setup(m <span class="synStatement">=></span> m.Invoke(It.IsAny<<span class="synType">int</span>>())).Callback(() <span class="synStatement">=></span> messageList.Add(<span class="synConstant">"From 1"</span>));
mock2.Setup(m <span class="synStatement">=></span> m.Invoke(It.IsAny<<span class="synType">int</span>>())).Callback(() <span class="synStatement">=></span> messageList.Add(<span class="synConstant">"From 2"</span>));
mock3.Setup(m <span class="synStatement">=></span> m.Invoke(It.IsAny<<span class="synType">int</span>>())).Callback(() <span class="synStatement">=></span> messageList.Add(<span class="synConstant">"From 3"</span>));
mock3.Object.Invoke(<span class="synConstant">0</span>);
mock1.Object.Invoke(<span class="synConstant">0</span>);
mock2.Object.Invoke(<span class="synConstant">0</span>);
mock1.Object.Invoke(<span class="synConstant">0</span>);
<span class="synComment">// 合計のコール回数 及び コールされた順番を検査</span>
Assert.That(messageList.Count, Is.EqualTo(<span class="synConstant">4</span>));
Assert.That(messageList[<span class="synConstant">0</span>], Is.EqualTo(<span class="synConstant">"From 3"</span>));
Assert.That(messageList[<span class="synConstant">1</span>], Is.EqualTo(<span class="synConstant">"From 1"</span>));
Assert.That(messageList[<span class="synConstant">2</span>], Is.EqualTo(<span class="synConstant">"From 2"</span>));
Assert.That(messageList[<span class="synConstant">3</span>], Is.EqualTo(<span class="synConstant">"From 1"</span>));
}
</pre>
<p><br></p>
<h1 id="モックライブラリを使用するメリット">モックライブラリを使用するメリット</h1>
<p>改めてモックライブラリを使うメリットを紹介します。</p>
<h2 id="モックオブジェクトを簡単に生成可能">モックオブジェクトを簡単に生成可能</h2>
<p>これまで説明した通りですが、モッククラスを独自に実装する必要がなくなります。</p>
<p>interface を使ったモッククラスを自作する場合は結構な行数を書くことになり、何より<u>保守コストが発生</u>して大変です。<br/>
テストの品質を考える場合はモッククラスのテストも必要になってきます。</p>
<p>ちなみにちょっと実装してみましたが、実際に自作する場合はもっと機能が必要になっていきます。</p>
<pre class="code lang-cs" data-lang="cs" data-unlink><span class="synStatement">using</span> System.Linq;
...
<span class="synType">public</span> <span class="synType">class</span> <span class="synType">MyMockFunc </span><span class="synStatement">:</span> IFunc
{
<span class="synComment">// コールされた戻り値の設定 (直近のコールのみ)</span>
<span class="synType">public</span> <span class="synType">bool</span> RetNumber { <span class="synStatement">get</span>; <span class="synStatement">set</span>; }
<span class="synComment">// コールされたときの引数の格納用リスト</span>
<span class="synType">public</span> List<<span class="synType">int</span>> CallHistory <span class="synStatement">=</span> <span class="synStatement">new</span>();
<span class="synComment">// 対象の引数でコールされた回数の検査</span>
<span class="synType">public</span> <span class="synType">bool</span> Verify(<span class="synType">int</span> targetNumber, <span class="synType">int</span> expected)
<span class="synStatement">=></span> CallHistory.Where(number <span class="synStatement">=></span> number <span class="synStatement">==</span> targetNumber).Count() <span class="synStatement">==</span> expected;
<span class="synComment">// メソッドをコールされたときの処理</span>
<span class="synType">public</span> <span class="synType">bool</span> Invoke(<span class="synType">int</span> number)
{
CallHistory.Add(number);
<span class="synStatement">return</span> RetNumber;
}
}
</pre>
<p>特別な事情がない限り、早めにモックライブラリを導入しておくとUnitTestが億劫にならなくて良いかと思います。</p>
<h2 id="IDEのリファレンス検索に余計な候補がでない"><a class="keyword" href="http://d.hatena.ne.jp/keyword/IDE">IDE</a>のリファレンス検索に余計な候補がでない</h2>
<p>モックライブラリ使う場合はモッククラスを実装するわけではないので、<a class="keyword" href="http://d.hatena.ne.jp/keyword/IDE">IDE</a>のinterface継承クラスの検索結果にモッククラスが並びません。</p>
<p>以下はモッククラスが定義されたプロジェクトでinterface継承クラスの検索結果イメージです。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20221217/20221217113953.png" width="1200" height="426" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p>(interfaceがGeneric型だった場合はさらに大変なことに)</p>
<h2 id="高度な検証がより簡単に実現可能">高度な検証がより簡単に実現可能</h2>
<p>「Moqの使用例」で紹介した通り、モックライブラリを使用すると複雑な検証もやりやすくなります。</p>
<ul>
<li>特定の引数で依存クラスのメソッドをN回コールすること</li>
<li>依存クラスが <code>OperationCanceledException</code> を返すときにはエラーにならないこと</li>
<li>etc...</li>
</ul>
<p><br>
モックライブラリの導入はUnitTest自体の敷居を下げることができるので、是非活用してみてください。</p>
<p><br></p>
<h1 id="簡易導入方法">簡易導入方法</h1>
<p>以下にある程度自動化した方法を紹介しています。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Ftsgcpp%2FUnityMoqSample%2Ftree%2Fmain%2FPluginGenerationTool" title="UnityMoqSample/PluginGenerationTool at main · tsgcpp/UnityMoqSample" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://github.com/tsgcpp/UnityMoqSample/tree/main/PluginGenerationTool">github.com</a></cite></p>
<p>オススメは実施環境に依存しない 「<a class="keyword" href="http://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Acrtionsを使用する場合」 です! (Actionsの<a class="keyword" href="http://d.hatena.ne.jp/keyword/yaml">yaml</a>設定ファイルも作成済みです)</p>
<p>生成されたフォルダをそのまま <code>Assets</code> 以下に配置すれば使用できる状態になっています。</p>
<p><br></p>
<h1 id="余談">余談</h1>
<h2 id="Moq-4182以上にする理由">Moq 4.18.2以上にする理由</h2>
<p>理由は".Net Standard 2.1"の依存するライブラリが削減されており、導入がより簡単になるためです。</p>
<p>実は4.18.1以前では <code>System.Threading.Tasks.Extensions</code> とその依存 <code>System.Runtime.CompilerServices.Unsafe</code> も一緒に入れる必要がありましたが、
4.18.2で依存が削除されました。</p>
<pre class="code" data-lang="" data-unlink>Removed dependency on System.Threading.Tasks.Extensions for netstandard2.1 and net6.0 (@tibel, #1274)</pre>
<p><a href="https://github.com/moq/moq4/blob/main/CHANGELOG.md#4182-2022-08-02">moq4/CHANGELOG.md at main · moq/moq4 · GitHub</a></p>
<p><strong>追記</strong></p>
<p>"<a class="keyword" href="http://d.hatena.ne.jp/keyword/.NET%20Framework">.NET Framework</a> 4.x"では <code>System.Threading.Tasks.Extensions</code> の依存は残っているため引き続き必要となるようです。<br/>
簡易導入方法で紹介しているツールも修正済みです。</p>
<p><br></p>
<h1 id="雑感">雑感</h1>
<p>実装したクラスすべてにUnitTestが必要になるわけでは有りませんが、<br/>
恒久的に機能を保証したい場合などは強力な武器になるので、Unity開発でもMoqを活用してみてください。</p>
<p><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%AF%A5%E9%A5%B9%A5%BF">クラスタ</a>ー株式会社にジョインして業務にも慣れてきましたが、<br/>
エンジニアに限らず様々な分野の<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B9%A5%DA%A5%B7%A5%E3">スペシャ</a>リストやジェネラリストの方がいて、刺激的な日々を送っています。</p>
<p>これからもバーチャルにのめり込んでいきます!</p>
<p><br></p>
<h1 id="クラスター-Advent-Calendar-2022-明日の記事の紹介"><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%AF%A5%E9%A5%B9%A5%BF">クラスタ</a>ー Advent Calendar 2022 明日の記事の紹介</h1>
<p>明日は Soraさん (<a href="https://qiita.com/BlueRose_Sora">@BlueRose_Sora</a>) の「Tips:clusterで大規模な展示会をする」です!</p>
<p>お楽しみに!</p>
tsgcpp
【GitHub Actions】Composite ActionのTipsと注意点
hatenablog://entry/4207112889920107718
2022-09-25T13:51:15+09:00
2022-09-25T13:51:15+09:00 概要 動作環境 用語 使用するプロジェクト Composite Actions の実装 Composite Actions を組み込むワークフロー Composite Actions 対応 Composite Actionのファイル構成 Composite Actionの組込方針 .Netのビルドの一連の流れを集約 (複数のステップを1つに集約) upload-artifactの有効日数3日をデフォルト化 (入力のデフォルト値を独自に定義) Composite Action を使用 Composite Actionの細かな仕様 Actionの補足 通常のActionとComposite Act…
<ul class="table-of-contents">
<li><a href="#概要">概要</a></li>
<li><a href="#動作環境">動作環境</a></li>
<li><a href="#用語">用語</a></li>
<li><a href="#使用するプロジェクト">使用するプロジェクト</a></li>
<li><a href="#Composite-Actions-の実装">Composite Actions の実装</a><ul>
<li><a href="#Composite-Actions-を組み込むワークフロー">Composite Actions を組み込むワークフロー</a></li>
<li><a href="#Composite-Actions-対応">Composite Actions 対応</a></li>
<li><a href="#Composite-Actionのファイル構成">Composite Actionのファイル構成</a></li>
<li><a href="#Composite-Actionの組込方針">Composite Actionの組込方針</a><ul>
<li><a href="#Netのビルドの一連の流れを集約-複数のステップを1つに集約">.Netのビルドの一連の流れを集約 (複数のステップを1つに集約)</a></li>
<li><a href="#upload-artifactの有効日数3日をデフォルト化-入力のデフォルト値を独自に定義">upload-artifactの有効日数3日をデフォルト化 (入力のデフォルト値を独自に定義)</a></li>
<li><a href="#Composite-Action-を使用">Composite Action を使用</a></li>
</ul>
</li>
</ul>
</li>
<li><a href="#Composite-Actionの細かな仕様">Composite Actionの細かな仕様</a></li>
<li><a href="#Actionの補足">Actionの補足</a><ul>
<li><a href="#通常のActionとComposite-Actionは構成自体は同じ">通常のActionとComposite Actionは構成自体は同じ</a></li>
<li><a href="#Actionはcheckoutしてからフォルダを指定しても実行可能">Actionはcheckoutしてからフォルダを指定しても実行可能</a></li>
<li><a href="#PrivateリポジトリのActionもcheckoutしてフォルダを指定すれば実行可能">PrivateリポジトリのActionもcheckoutしてフォルダを指定すれば実行可能</a><ul>
<li><a href="#Private-Actionを直接-uses-に指定できない理由">Private Actionを直接 uses に指定できない理由</a></li>
</ul>
</li>
<li><a href="#ダウンロード済みのActionは再利用される">ダウンロード済みのActionは再利用される</a></li>
</ul>
</li>
<li><a href="#Composite-Actionの注意点">Composite Actionの注意点</a><ul>
<li><a href="#Composite-Action自体のcheckoutが必要">Composite Action自体のcheckoutが必要</a><ul>
<li><a href="#Publicリポジトリの場合はリポジトリ指定で実行可能">Publicリポジトリの場合はリポジトリ指定で実行可能</a></li>
</ul>
</li>
<li><a href="#ワークフロー本体のyamlのusesで指定されたActionは事前ダウンロードされる">ワークフロー本体のyamlのusesで指定されたActionは事前ダウンロードされる</a></li>
<li><a href="#外部yamlに定義されたActionはステップ実行時に遅延ダウンロードされる">外部yamlに定義されたActionはステップ実行時に遅延ダウンロードされる</a></li>
<li><a href="#遅延ダウンロードの何が問題なのか">遅延ダウンロードの何が問題なのか?</a></li>
<li><a href="#対策1-あらかじめ使用するActionすべてのcheckoutを済ませる-オススメ">対策1 あらかじめ使用するActionすべてのcheckoutを済ませる (オススメ)</a></li>
<li><a href="#対策2-usesで使用するActionを宣言-非推奨">対策2 usesで使用するActionを宣言 (非推奨)</a></li>
</ul>
</li>
<li><a href="#Reusing-Workflows-との違い">Reusing Workflows との違い</a></li>
<li><a href="#余談筆者が遭遇した事象">余談、筆者が遭遇した事象</a></li>
<li><a href="#サンプルプロジェクト">サンプルプロジェクト</a></li>
<li><a href="#雑感">雑感</a></li>
</ul>
<h1 id="概要">概要</h1>
<p>今回は<a class="keyword" href="http://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Actionsの機能の一つである "Composite Action" について紹介します。</p>
<p>今回の記事は、<a class="keyword" href="http://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Actionsに多少知見がある人向けの記事になります。</p>
<p>Composite Actionはいわゆる再利用性のあるステップを<a class="keyword" href="http://d.hatena.ne.jp/keyword/yaml">yaml</a>ファイルに集約して再利用可能にする機能です。<br />
テンプレート的な機能、もしくはプログラミングにおける関数的なものと考えてもらっても良いと思います。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdocs.github.com%2Fen%2Factions%2Fcreating-actions%2Fcreating-a-composite-action" title="Creating a composite action - GitHub Docs" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://docs.github.com/en/actions/creating-actions/creating-a-composite-action">docs.github.com</a></cite></p>
<p>Composite Actionは便利ですが、注意点もあるため紹介しようと思います。</p>
<p>ついでにPrivate Action (Privateな<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>に作成したAction) の使用方法も合わせて紹介します。</p>
<p>記事の最後にサンプル<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>も記載しておきます。</p>
<h1 id="動作環境">動作環境</h1>
<ul>
<li><a class="keyword" href="http://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Actions + <a class="keyword" href="http://d.hatena.ne.jp/keyword/ubuntu">ubuntu</a>-latest
<ul>
<li><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C7%A5%D0%A5%C3%A5%B0">デバッグ</a>モード有効化 <a href="https://docs.github.com/en/actions/monitoring-and-troubleshooting-workflows/enabling-debug-logging">Enabling debug logging</a> を参照</li>
</ul>
</li>
</ul>
<h1 id="用語">用語</h1>
<ul>
<li>Public Action
<ul>
<li>Publicな<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>にあるAction (例、 actions/checkout, actions/upload-artifactなど)</li>
</ul>
</li>
<li>Private Action
<ul>
<li>Privateな<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>にあるAction</li>
<li>非公開の<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>にあるアクセスが制限されたAction</li>
</ul>
</li>
</ul>
<h1 id="使用するプロジェクト">使用するプロジェクト</h1>
<p>本題では有りませんが、ビルドのサンプル用に以下を入れています。</p>
<ul>
<li><a class="keyword" href="http://d.hatena.ne.jp/keyword/C%23">C#</a> プロジェクトのビルド用サンプルプロジェクト
<ul>
<li><code>Lottery</code> という実行するたびにtrue or falseを返すだけのプログラム</li>
<li><code>LotteryTests</code> はUnitTest</li>
</ul>
</li>
</ul>
<h1 id="Composite-Actions-の実装">Composite Actions の実装</h1>
<h2 id="Composite-Actions-を組み込むワークフロー">Composite Actions を組み込むワークフロー</h2>
<p>まずは Composite Actionなしのworkflowを例にしたいと思います。</p>
<pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synComment"># .github/workflows/build-dotnet-without-composite-actions.yml</span>
<span class="synIdentifier">name</span><span class="synSpecial">:</span> <span class="synConstant">"Build Dotnet without Composite Actions"</span>
<span class="synIdentifier">on</span><span class="synSpecial">:</span>
<span class="synIdentifier">workflow_dispatch</span><span class="synSpecial">:</span> <span class="synSpecial">{}</span>
<span class="synIdentifier">jobs</span><span class="synSpecial">:</span>
<span class="synIdentifier">build</span><span class="synSpecial">:</span>
<span class="synIdentifier">name</span><span class="synSpecial">:</span> Build
<span class="synIdentifier">runs-on</span><span class="synSpecial">:</span> ubuntu-latest
<span class="synIdentifier">steps</span><span class="synSpecial">:</span>
<span class="synStatement">- </span><span class="synIdentifier">uses</span><span class="synSpecial">:</span> actions/checkout@v3
<span class="synIdentifier">with</span><span class="synSpecial">:</span>
<span class="synIdentifier">lfs</span><span class="synSpecial">:</span> <span class="synConstant">true</span>
<span class="synStatement">- </span><span class="synIdentifier">uses</span><span class="synSpecial">:</span> actions/cache@v3
<span class="synIdentifier">with</span><span class="synSpecial">:</span>
<span class="synIdentifier">path</span><span class="synSpecial">:</span> ./Lottery/obj
<span class="synIdentifier">key</span><span class="synSpecial">:</span> dotnet-${{ runner.os }}-${{ github.ref_name }}
<span class="synStatement">- </span><span class="synIdentifier">uses</span><span class="synSpecial">:</span> actions/setup-dotnet@v2
<span class="synIdentifier">with</span><span class="synSpecial">:</span>
<span class="synIdentifier">dotnet-version</span><span class="synSpecial">:</span> <span class="synConstant">'6.0.x'</span>
<span class="synIdentifier">include-prerelease</span><span class="synSpecial">:</span> <span class="synConstant">false</span>
<span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Restore Packages
<span class="synIdentifier">shell</span><span class="synSpecial">:</span> bash
<span class="synIdentifier">run</span><span class="synSpecial">:</span> dotnet restore ./GitHubActionsTestbed.sln
<span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Build Projects
<span class="synIdentifier">shell</span><span class="synSpecial">:</span> bash
<span class="synIdentifier">run</span><span class="synSpecial">:</span> dotnet build ./GitHubActionsTestbed.sln --configuration Release
<span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Test Projects
<span class="synIdentifier">shell</span><span class="synSpecial">:</span> bash
<span class="synIdentifier">run</span><span class="synSpecial">:</span> dotnet test ./GitHubActionsTestbed.sln --blame
<span class="synStatement">- </span><span class="synIdentifier">uses</span><span class="synSpecial">:</span> actions/upload-artifact@v3
<span class="synIdentifier">with</span><span class="synSpecial">:</span>
<span class="synIdentifier">name</span><span class="synSpecial">:</span> Lottery
<span class="synIdentifier">path</span><span class="synSpecial">:</span> ./Lottery/bin/Release/net6.0
<span class="synIdentifier">retention-days</span><span class="synSpecial">:</span> <span class="synConstant">3</span>
</pre>
<p>ワークフローの詳細</p>
<ul>
<li><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>のcheckout (actions/checkout)</li>
<li>.Net 6.0のビルド環境の構築 (actions/setup-<a class="keyword" href="http://d.hatena.ne.jp/keyword/dotnet">dotnet</a>)</li>
<li><a class="keyword" href="http://d.hatena.ne.jp/keyword/dotnet">dotnet</a> コマンドを使ったビルド (パッケージの取得、テストを含む)</li>
<li>Artifactとしてアップロード</li>
</ul>
<p>実行結果は以下です。</p>
<p><a href="https://github.com/tsgcpp/GitHubActionsTestbed/actions/runs/3117273807">https://github.com/tsgcpp/GitHubActionsTestbed/actions/runs/3117273807</a></p>
<h2 id="Composite-Actions-対応">Composite Actions 対応</h2>
<h2 id="Composite-Actionのファイル構成">Composite Actionのファイル構成</h2>
<p>以下のようなファイル構成を取ります。</p>
<pre class="code" data-lang="" data-unlink><path to action>/<composite action name>/action.yml</pre>
<p><code><composite action name></code> はフォルダで、 ステップは <code>action.yml</code> に定義します。<br />
<span style="color: #ff0000">フォルダ名はステップの流れがわかる名前にすると良いです</span>。</p>
<p>例えば、「.Netのビルドの一連の流れを集約」するComposite Actionを作りたい場合は以下のようにします。</p>
<pre class="code" data-lang="" data-unlink>.github/composite/dotnet-build/action.yml</pre>
<p>自分は<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>専用のComposite Actionは <code>.github/composite</code> に置くようにしていますが、
別に<code>.github/composite</code> 以下でなくとも問題ありません。</p>
<p>そして呼び出すときは以下のように <code>uses</code> にフォルダを指定します</p>
<pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synStatement">- </span><span class="synIdentifier">uses</span><span class="synSpecial">:</span> ./.github/composite/dotnet-build
</pre>
<p>後述しますが、<code>with</code> により入力(<code>inputs</code>)を与えることも可能です。</p>
<pre class="code lang-yaml" data-lang="yaml" data-unlink> <span class="synStatement">- </span><span class="synIdentifier">uses</span><span class="synSpecial">:</span> ./.github/composite/upload-artifact
<span class="synIdentifier">with</span><span class="synSpecial">:</span>
<span class="synIdentifier">name</span><span class="synSpecial">:</span> Lottery
<span class="synIdentifier">path</span><span class="synSpecial">:</span> ./Lottery/bin/Release/net6.0
</pre>
<h2 id="Composite-Actionの組込方針">Composite Actionの組込方針</h2>
<p>Composite Action は個人的には以下の活用方法があると考えています。</p>
<ul>
<li>複数のステップを1つに集約</li>
<li>入力のデフォルト値を独自に定義</li>
</ul>
<h3 id="Netのビルドの一連の流れを集約-複数のステップを1つに集約">.Netのビルドの一連の流れを集約 (複数のステップを1つに集約)</h3>
<p>.NetのビルドをComposite Action対応します。<br />
フォルダ構成は固定として入力(<code>inputs</code>)はありません。</p>
<pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synComment"># .github/composite/dotnet-build/action.yml</span>
<span class="synIdentifier">name</span><span class="synSpecial">:</span> <span class="synConstant">'Dotnet Build'</span>
<span class="synIdentifier">description</span><span class="synSpecial">:</span> <span class="synConstant">'Restore packages, Build and Test'</span>
<span class="synIdentifier">runs</span><span class="synSpecial">:</span>
<span class="synIdentifier">using</span><span class="synSpecial">:</span> <span class="synConstant">"composite"</span>
<span class="synIdentifier">steps</span><span class="synSpecial">:</span>
<span class="synStatement">- </span><span class="synIdentifier">uses</span><span class="synSpecial">:</span> actions/setup-dotnet@v2
<span class="synIdentifier">with</span><span class="synSpecial">:</span>
<span class="synIdentifier">dotnet-version</span><span class="synSpecial">:</span> <span class="synConstant">'6.0.x'</span>
<span class="synIdentifier">include-prerelease</span><span class="synSpecial">:</span> <span class="synConstant">false</span>
<span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Restore Packages
<span class="synIdentifier">shell</span><span class="synSpecial">:</span> bash
<span class="synIdentifier">run</span><span class="synSpecial">:</span> dotnet restore ./GitHubActionsTestbed.sln
<span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Build Projects
<span class="synIdentifier">shell</span><span class="synSpecial">:</span> bash
<span class="synIdentifier">run</span><span class="synSpecial">:</span> dotnet build ./GitHubActionsTestbed.sln --configuration Release
<span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Test Projects
<span class="synIdentifier">shell</span><span class="synSpecial">:</span> bash
<span class="synIdentifier">run</span><span class="synSpecial">:</span> dotnet test ./GitHubActionsTestbed.sln --blame
</pre>
<h3 id="upload-artifactの有効日数3日をデフォルト化-入力のデフォルト値を独自に定義">upload-artifactの有効日数3日をデフォルト化 (入力のデフォルト値を独自に定義)</h3>
<p>公式の <code>actions/upload-artifact@v3</code> ですが、デフォルトが90日となかなか長いです。</p>
<p><span style="color: #ff0000">Composite Actionsは独自の入力 (<code>inputs</code>) を定義することが可能です。</span></p>
<p>ArtifactはPrivateな<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>の場合、使いすぎると従量課金の対象となるためデフォルトで3日ぐらいにしたい場合などは、<br />
Composite Actionの <code>inputs</code> を使用することで独自のデフォルト値を定義できます。</p>
<pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synComment"># .github/composite/upload-artifact/action.yml</span>
<span class="synIdentifier">name</span><span class="synSpecial">:</span> <span class="synConstant">'Upload Artifact'</span>
<span class="synIdentifier">description</span><span class="synSpecial">:</span> <span class="synConstant">'An action to create a artifact'</span>
<span class="synIdentifier">inputs</span><span class="synSpecial">:</span>
<span class="synIdentifier">name</span><span class="synSpecial">:</span>
<span class="synIdentifier">required</span><span class="synSpecial">:</span> <span class="synConstant">true</span>
<span class="synIdentifier">default</span><span class="synSpecial">:</span> <span class="synConstant">'Artifact'</span>
<span class="synIdentifier">path</span><span class="synSpecial">:</span>
<span class="synIdentifier">required</span><span class="synSpecial">:</span> <span class="synConstant">true</span>
<span class="synIdentifier">retention-days</span><span class="synSpecial">:</span>
<span class="synIdentifier">required</span><span class="synSpecial">:</span> <span class="synConstant">false</span>
<span class="synIdentifier">default</span><span class="synSpecial">:</span> <span class="synConstant">3</span>
<span class="synIdentifier">runs</span><span class="synSpecial">:</span>
<span class="synIdentifier">using</span><span class="synSpecial">:</span> <span class="synConstant">"composite"</span>
<span class="synIdentifier">steps</span><span class="synSpecial">:</span>
<span class="synStatement">- </span><span class="synIdentifier">uses</span><span class="synSpecial">:</span> actions/upload-artifact@v3
<span class="synIdentifier">with</span><span class="synSpecial">:</span>
<span class="synIdentifier">name</span><span class="synSpecial">:</span> ${{ inputs.name }}
<span class="synIdentifier">path</span><span class="synSpecial">:</span> ${{ inputs.path }}
<span class="synIdentifier">retention-days</span><span class="synSpecial">:</span> ${{ inputs.retention-days }}
</pre>
<ul>
<li><code>name</code> (Artifact名)のデフォルトを"Artifact"</li>
<li><code>retention-days</code> (有効期限)をデフォルトを3 (3日)</li>
<li><code>path</code> (対象のファイル群)はデフォルトなしで指定を必須化</li>
</ul>
<p><code>required</code> 一応指定しておきましょう。(ただ、個人的にはComposite Actionだと微妙にrequired機能していない印象です)</p>
<h3 id="Composite-Action-を使用">Composite Action を使用</h3>
<p>改めて「Composite Actions を使用していないワークフロー」を改修したいと思います。</p>
<pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synComment"># .github/workflows/build-dotnet.yml</span>
<span class="synIdentifier">name</span><span class="synSpecial">:</span> <span class="synConstant">"Build Dotnet"</span>
<span class="synIdentifier">on</span><span class="synSpecial">:</span>
<span class="synIdentifier">workflow_dispatch</span><span class="synSpecial">:</span> <span class="synSpecial">{}</span>
<span class="synIdentifier">jobs</span><span class="synSpecial">:</span>
<span class="synIdentifier">build</span><span class="synSpecial">:</span>
<span class="synIdentifier">name</span><span class="synSpecial">:</span> Build
<span class="synIdentifier">runs-on</span><span class="synSpecial">:</span> ubuntu-latest
<span class="synIdentifier">steps</span><span class="synSpecial">:</span>
<span class="synStatement">- </span><span class="synIdentifier">uses</span><span class="synSpecial">:</span> actions/checkout@v3
<span class="synIdentifier">with</span><span class="synSpecial">:</span>
<span class="synIdentifier">lfs</span><span class="synSpecial">:</span> <span class="synConstant">true</span>
<span class="synStatement">- </span><span class="synIdentifier">uses</span><span class="synSpecial">:</span> actions/cache@v3
<span class="synIdentifier">with</span><span class="synSpecial">:</span>
<span class="synIdentifier">path</span><span class="synSpecial">:</span> ./Lottery/obj
<span class="synIdentifier">key</span><span class="synSpecial">:</span> dotnet-${{ runner.os }}-${{ github.ref_name }}
<span class="synStatement">- </span><span class="synIdentifier">uses</span><span class="synSpecial">:</span> ./.github/composite/dotnet-build
<span class="synStatement">- </span><span class="synIdentifier">uses</span><span class="synSpecial">:</span> ./.github/composite/upload-artifact
<span class="synIdentifier">with</span><span class="synSpecial">:</span>
<span class="synIdentifier">name</span><span class="synSpecial">:</span> Lottery
<span class="synIdentifier">path</span><span class="synSpecial">:</span> ./Lottery/bin/Release/net6.0
</pre>
<p>上記のようにビルドの流れがスッキリした見た目になりました。<br />
また、<code>upload-artifact</code> は有効期限を指定していなくてもデフォルトの3日が設定されるようになっています。</p>
<p>実行結果は以下です。</p>
<p><a href="https://github.com/tsgcpp/GitHubActionsTestbed/actions/runs/3117435297">https://github.com/tsgcpp/GitHubActionsTestbed/actions/runs/3117435297</a></p>
<h1 id="Composite-Actionの細かな仕様">Composite Actionの細かな仕様</h1>
<p>以下に記載されています。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Factions%2Frunner%2Fblob%2Fmain%2Fdocs%2Fadrs%2F0549-composite-run-steps.md" title="runner/0549-composite-run-steps.md at main · actions/runner" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://github.com/actions/runner/blob/main/docs/adrs/0549-composite-run-steps.md">github.com</a></cite></p>
<p>デフォルトのshellは指定できないなど、細かい仕様が書いてあります。</p>
<h1 id="Actionの補足">Actionの補足</h1>
<h2 id="通常のActionとComposite-Actionは構成自体は同じ">通常のActionとComposite Actionは構成自体は同じ</h2>
<p>実はComposite Actionのファイル構成 (<code><path to action>/<composite action name>/action.yml</code>) ですが、<br />
特殊に見えて、実は通常のActionと同じ構成になっています。</p>
<p>例えば、公式の <code>actions/checkout</code> のルートのファイルを見ると <code>action.yml</code>が存在しています。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Factions%2Fcheckout%2Fblob%2Fmain%2Faction.yml" title="checkout/action.yml at main · actions/checkout" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://github.com/actions/checkout/blob/main/action.yml">github.com</a></cite></p>
<p>つまり<a class="keyword" href="http://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Actionsで使用されるActionは、必ずaction.ymlを持ったファイル群となっています。</p>
<h2 id="Actionはcheckoutしてからフォルダを指定しても実行可能">Actionはcheckoutしてからフォルダを指定しても実行可能</h2>
<p>実はActionは特定のフォルダにcheckoutして、<code>uses</code>に指定しても使用可能です。</p>
<p>例えば <code>actions/upload-artifact</code>は一旦 <code>./.github/repos/actions/upload-artifact</code>というフォルダにcheckoutして、<br />
usesでそのフォルダを指定する形をとっても、同様の機能を得ることができます。</p>
<pre class="code lang-yaml" data-lang="yaml" data-unlink> <span class="synStatement">- </span><span class="synIdentifier">uses</span><span class="synSpecial">:</span> actions/upload-artifact@v3
<span class="synIdentifier">with</span><span class="synSpecial">:</span>
<span class="synIdentifier">name</span><span class="synSpecial">:</span> Lottery
<span class="synIdentifier">path</span><span class="synSpecial">:</span> ./Lottery/bin/Release/net6.0
<span class="synIdentifier">retention-days</span><span class="synSpecial">:</span> <span class="synConstant">3</span>
</pre>
<p>↓</p>
<pre class="code lang-yaml" data-lang="yaml" data-unlink> <span class="synStatement">- </span><span class="synIdentifier">uses</span><span class="synSpecial">:</span> actions/checkout@v3
<span class="synIdentifier">with</span><span class="synSpecial">:</span>
<span class="synIdentifier">repository</span><span class="synSpecial">:</span> <span class="synConstant">'actions/upload-artifact'</span>
<span class="synIdentifier">ref</span><span class="synSpecial">:</span> v3.1.0
<span class="synIdentifier">path</span><span class="synSpecial">:</span> ./.github/repos/actions/upload-artifact
<span class="synStatement">- </span><span class="synIdentifier">uses</span><span class="synSpecial">:</span> ./.github/repos/actions/upload-artifact
<span class="synIdentifier">with</span><span class="synSpecial">:</span>
<span class="synIdentifier">name</span><span class="synSpecial">:</span> Lottery
<span class="synIdentifier">path</span><span class="synSpecial">:</span> ./Lottery/bin/Release/net6.0
<span class="synIdentifier">retention-days</span><span class="synSpecial">:</span> <span class="synConstant">3</span>
</pre>
<h2 id="PrivateリポジトリのActionもcheckoutしてフォルダを指定すれば実行可能">Private<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>のActionもcheckoutしてフォルダを指定すれば実行可能</h2>
<p>前項と同じ原理でPrivate<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>もcheckoutして実行が可能です。</p>
<p>社内専用のActionを作って使用したい場合などにご活用ください。</p>
<pre class="code lang-yaml" data-lang="yaml" data-unlink> <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Checkout tsgcpp/upload-artifact-private
<span class="synIdentifier">uses</span><span class="synSpecial">:</span> actions/checkout@v3
<span class="synIdentifier">with</span><span class="synSpecial">:</span>
<span class="synComment"> # actions/upload-artifact をコピーしてPrivate化したリポジトリ</span>
<span class="synIdentifier">repository</span><span class="synSpecial">:</span> <span class="synConstant">'tsgcpp/upload-artifact-private'</span>
<span class="synIdentifier">ref</span><span class="synSpecial">:</span> main
<span class="synIdentifier">path</span><span class="synSpecial">:</span> ./.github/repos/tsgcpp/upload-artifact-private
<span class="synIdentifier">token</span><span class="synSpecial">:</span> ${{ secrets.PAT_TOKEN }}
<span class="synStatement">- </span><span class="synIdentifier">uses</span><span class="synSpecial">:</span> ./.github/repos/tsgcpp/upload-artifact-private
<span class="synIdentifier">with</span><span class="synSpecial">:</span>
<span class="synIdentifier">name</span><span class="synSpecial">:</span> Lottery
<span class="synIdentifier">path</span><span class="synSpecial">:</span> ./Lottery/bin/Release/net6.0
<span class="synIdentifier">retention-days</span><span class="synSpecial">:</span> <span class="synConstant">3</span>
</pre>
<p>対象の<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>にアクセス可能なPersonal <a class="keyword" href="http://d.hatena.ne.jp/keyword/Access">Access</a> Tokenを作成してsecretsに登録して使用する必要があるなど、多少手間があります。</p>
<h3 id="Private-Actionを直接-uses-に指定できない理由">Private Actionを直接 <code>uses</code> に指定できない理由</h3>
<p><a class="keyword" href="http://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Actionsのワークフローでデフォルトで発行される <code>GITHUB_TOKEN</code> があるのですが、<br />
<span style="color: #ff0000"><strong><code>GITHUB_TOKEN</code> は ワークフローを実行した<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>のみアクセス可能な<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C8%A1%BC%A5%AF">トーク</a>ン</strong></span>なので他の<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>にはアクセスできません。</p>
<p>そのため、Private Actionの場合はアクセス可能な<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C8%A1%BC%A5%AF">トーク</a>ンを使ってcheckoutしてから、<code>uses</code>に指定する必要があります。</p>
<p><span style="color: #999999"><a class="keyword" href="http://d.hatena.ne.jp/keyword/GitHub">GitHub</a>様、Private Actionに特化した<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C8%A1%BC%A5%AF">トーク</a>ンの機能つくってほしいなー</span></p>
<h2 id="ダウンロード済みのActionは再利用される">ダウンロード済みのActionは再利用される</h2>
<p><u>全く同じバージョンやSHA</u>のActionがダウンロード済みの場合は、ダウンロード済みのものが再利用されます。</p>
<p><span style="color: #ff0000">ダウンロード済みのActionはComposite Actionなどの外部<a class="keyword" href="http://d.hatena.ne.jp/keyword/yaml">yaml</a>でも共有されます</span>。</p>
<pre class="code lang-yaml" data-lang="yaml" data-unlink> <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Cache actions/cache
<span class="synIdentifier">uses</span><span class="synSpecial">:</span> actions/checkout@v3
<span class="synIdentifier">with</span><span class="synSpecial">:</span>
<span class="synIdentifier">repository</span><span class="synSpecial">:</span> <span class="synConstant">'actions/cache'</span>
<span class="synIdentifier">ref</span><span class="synSpecial">:</span> v3.0.8
<span class="synIdentifier">path</span><span class="synSpecial">:</span> ${{ inputs.pathRoot }}/actions/cache
<span class="synStatement">- </span><span class="synIdentifier">uses</span><span class="synSpecial">:</span> ./.github/composite/checkout-actions
</pre>
<pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synComment"># .github/composite/checkout-actions</span>
<span class="synComment"> # Compsite Action側</span>
<span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Cache actions/upload-artifact
<span class="synIdentifier">uses</span><span class="synSpecial">:</span> actions/checkout@v3 <span class="synComment"> # ダウンロード済みの `actions/checkout` を使用</span>
<span class="synIdentifier">with</span><span class="synSpecial">:</span>
<span class="synIdentifier">repository</span><span class="synSpecial">:</span> <span class="synConstant">'actions/upload-artifact'</span>
<span class="synIdentifier">ref</span><span class="synSpecial">:</span> v3.1.0
<span class="synIdentifier">path</span><span class="synSpecial">:</span> ${{ inputs.pathRoot }}/actions/upload-artifact
</pre>
<p>ちなみに<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C7%A5%D0%A5%C3%A5%B0">デバッグ</a>モードを有効化すると、以下のログで再利用されていることが確認できます。</p>
<pre class="code" data-lang="" data-unlink>Getting action download info
##[debug]Action 'actions/upload-artifact@v3' already downloaded at '/home/runner/work/_actions/actions/upload-artifact/v3'.</pre>
<p><a href="https://github.com/tsgcpp/GitHubActionsTestbed/actions/runs/3117276928/jobs/5055819436#step:7:9">https://github.com/tsgcpp/GitHubActionsTestbed/actions/runs/3117276928/jobs/5055819436#step:7:9</a></p>
<h1 id="Composite-Actionの注意点">Composite Actionの注意点</h1>
<h2 id="Composite-Action自体のcheckoutが必要">Composite Action自体のcheckoutが必要</h2>
<p>ワークフロー実行時は<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>の内容はcheckoutされていません。<br />
Composite Actionは外部<a class="keyword" href="http://d.hatena.ne.jp/keyword/yaml">yaml</a>に定義する関係であらかじめcheckoutで他ソースと一緒に取得する必要があります。</p>
<pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synPreProc">...</span>
<span class="synIdentifier">steps</span><span class="synSpecial">:</span>
<span class="synStatement">- </span><span class="synIdentifier">uses</span><span class="synSpecial">:</span> actions/checkout@v3
<span class="synIdentifier">with</span><span class="synSpecial">:</span>
<span class="synIdentifier">lfs</span><span class="synSpecial">:</span> <span class="synConstant">true</span>
<span class="synPreProc">...</span>
<span class="synComment"> # actions/checkoutで取得したComposite Actionを使用</span>
<span class="synStatement">- </span><span class="synIdentifier">uses</span><span class="synSpecial">:</span> ./.github/composite/dotnet-build
</pre>
<h3 id="Publicリポジトリの場合はリポジトリ指定で実行可能">Public<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>の場合は<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>指定で実行可能</h3>
<p>Publicな<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>に配置されたComposite Actionであれば、以下の様に指定できます。</p>
<pre class="code lang-yaml" data-lang="yaml" data-unlink> <span class="synStatement">- </span><span class="synIdentifier">uses</span><span class="synSpecial">:</span> <org>/<repository>/<path to action directory>@<ref(tag or branch)>
</pre>
<p>以下は指定例です。</p>
<pre class="code lang-yaml" data-lang="yaml" data-unlink> <span class="synStatement">- </span><span class="synIdentifier">uses</span><span class="synSpecial">:</span> tsgcpp/GitHubActionsTestbed/.github/composite/dotnet-build@main
</pre>
<h2 id="ワークフロー本体のyamlのusesで指定されたActionは事前ダウンロードされる">ワークフロー本体の<a class="keyword" href="http://d.hatena.ne.jp/keyword/yaml">yaml</a>の<code>uses</code>で指定されたActionは事前ダウンロードされる</h2>
<p><strong>ワークフロー本体の<a class="keyword" href="http://d.hatena.ne.jp/keyword/yaml">yaml</a>内の <code>uses</code> に定義したActionですが、 ワークフローの最初 (<code>Set up job</code>) で事前ダウンロードされます。</strong></p>
<p>こちらも<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C7%A5%D0%A5%C3%A5%B0">デバッグ</a>モードを有効化すると確認できます。</p>
<pre class="code" data-lang="" data-unlink>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'</pre>
<p><a href="https://github.com/tsgcpp/GitHubActionsTestbed/actions/runs/3117276928/jobs/5055819436#step:1:45">https://github.com/tsgcpp/GitHubActionsTestbed/actions/runs/3117276928/jobs/5055819436#step:1:45</a></p>
<h2 id="外部yamlに定義されたActionはステップ実行時に遅延ダウンロードされる">外部<a class="keyword" href="http://d.hatena.ne.jp/keyword/yaml">yaml</a>に定義されたActionはステップ実行時に遅延ダウンロードされる</h2>
<p><span style="color: #ff0000"><strong>本記事の本題といっても過言ではありません!</strong></span></p>
<p><span style="color: #ff0000"><strong>Composite Actionを含む外部<a class="keyword" href="http://d.hatena.ne.jp/keyword/yaml">yaml</a>内のActionは実行されるタイミングでダウンロードされます!</strong></span></p>
<p>つまり、<strong>外部<a class="keyword" href="http://d.hatena.ne.jp/keyword/yaml">yaml</a>のActionは遅延処理的な性質があります</strong>。</p>
<p><code>.github/workflows/build-dotnet.yml</code> を例に取ると</p>
<ul>
<li><code>actions/cache</code>はワークフローのはじめにダウンロードされる
<ul>
<li>ワークフロー本体の<a class="keyword" href="http://d.hatena.ne.jp/keyword/yaml">yaml</a>内で定義されているため</li>
</ul>
</li>
<li><code>actions/setup-dotnet</code>と<code>actions/upload-artifact</code>は各ステップ実行時にダウンロードされる
<ul>
<li>Composite Actionの<a class="keyword" href="http://d.hatena.ne.jp/keyword/yaml">yaml</a>内に定義されているため</li>
</ul>
</li>
</ul>
<pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synComment"># withは省略</span>
<span class="synStatement">- </span><span class="synIdentifier">uses</span><span class="synSpecial">:</span> actions/cache@v3
<span class="synPreProc">...</span>
<span class="synComment"> # 内部で uses: actions/setup-dotnet@v2</span>
<span class="synStatement">- </span><span class="synIdentifier">uses</span><span class="synSpecial">:</span> ./.github/composite/dotnet-build
<span class="synComment"> # 内部で uses: actions/upload-artifact@v3</span>
<span class="synStatement">- </span><span class="synIdentifier">uses</span><span class="synSpecial">:</span> ./.github/composite/upload-artifact
<span class="synPreProc">...</span>
</pre>
<p>Actionのログを見てみると、<code>Set up job</code> で<code>actions/cache</code>はダウンロードされていますが、
<code>actions/setup-dotnet</code>と<code>actions/upload-artifact</code>はダウンロードされていないことがわかります。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20220924/20220924173022.png" width="1200" height="667" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p><code>actions/setup-dotnet</code>と<code>actions/upload-artifact</code>は各種ステップの実行時にダウンロードされています。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20220924/20220924173159.png" width="1200" height="441" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20220924/20220924173235.png" width="1200" height="476" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p>ログの全体は以下です。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Ftsgcpp%2FGitHubActionsTestbed%2Factions%2Fruns%2F3117276734%2Fjobs%2F5055819110%23step%3A1%3A44" title="Build Dotnet · tsgcpp/GitHubActionsTestbed@c7d27a8" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://github.com/tsgcpp/GitHubActionsTestbed/actions/runs/3117276734/jobs/5055819110#step:1:44">github.com</a></cite></p>
<h2 id="遅延ダウンロードの何が問題なのか">遅延ダウンロードの何が問題なのか?</h2>
<p>「大した問題じゃなくね?」って思った方もいると思いますし、実際大した問題にならないパターンも多いです。</p>
<p>問題になりやすい例として、<strong>完了に長時間を要するワークフローがあります</strong>。</p>
<p>例えば以下のようなワークフローです。</p>
<ul>
<li>5時間かかるアプリのビルド実行</li>
<li>ビルド完了後に Composite Actionを使ってアプリをストアへアップロード
<ul>
<li>Composite Action内でアプリのストアアップロード用Actionを取得して使用</li>
</ul>
</li>
</ul>
<p>ワークフロー開始時には<a class="keyword" href="http://d.hatena.ne.jp/keyword/GitHub">GitHub</a>は正常だったのに、<br />
5時間後のビルド時に<a class="keyword" href="http://d.hatena.ne.jp/keyword/GitHub">GitHub</a>の<a class="keyword" href="http://d.hatena.ne.jp/keyword/API">API</a>が一部死んでいてストア用のActionのダウンロード(checkout)が失敗してビルドがパーになっちゃうパターンです。</p>
<p>ストア側の<a class="keyword" href="http://d.hatena.ne.jp/keyword/API">API</a>は問題がなかった場合、予めストア用のActionをダウンロードできていれば回避できた問題ですね。。。</p>
<p>昨今<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%AF%A5%E9%A5%A6%A5%C9">クラウド</a>ベンダー(<a class="keyword" href="http://d.hatena.ne.jp/keyword/AWS">AWS</a>など)の一時<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>でビルドすることも多くなっていて、ビルド成果物をどこかに退避していないとサルベージも困難だったりします。</p>
<h2 id="対策1-あらかじめ使用するActionすべてのcheckoutを済ませる-オススメ">対策1 あらかじめ使用するActionすべてのcheckoutを済ませる (オススメ)</h2>
<p>事前ダウンロードされてないなら、明示的に事前ダウンロードしてしまおうという発想です。</p>
<p>1例として、以下のようなセットアップ用Composite Actionを用いる方法があります。</p>
<pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synIdentifier">name</span><span class="synSpecial">:</span> <span class="synConstant">'Set Up Actions'</span>
<span class="synIdentifier">inputs</span><span class="synSpecial">:</span>
<span class="synIdentifier">pathRoot</span><span class="synSpecial">:</span>
<span class="synIdentifier">required</span><span class="synSpecial">:</span> <span class="synConstant">true</span>
<span class="synIdentifier">description</span><span class="synSpecial">:</span> <span class="synConstant">'Relative path the actions will be into'</span>
<span class="synIdentifier">default</span><span class="synSpecial">:</span> ./.github/repos
<span class="synIdentifier">patToken</span><span class="synSpecial">:</span>
<span class="synIdentifier">required</span><span class="synSpecial">:</span> <span class="synConstant">true</span>
<span class="synIdentifier">description</span><span class="synSpecial">:</span> <span class="synConstant">'GitHub Personal Access Token to checkout private repositories.'</span>
<span class="synIdentifier">runs</span><span class="synSpecial">:</span>
<span class="synIdentifier">using</span><span class="synSpecial">:</span> <span class="synConstant">"composite"</span>
<span class="synIdentifier">steps</span><span class="synSpecial">:</span>
<span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Cache actions/checkout
<span class="synIdentifier">uses</span><span class="synSpecial">:</span> actions/checkout@v3
<span class="synIdentifier">with</span><span class="synSpecial">:</span>
<span class="synIdentifier">repository</span><span class="synSpecial">:</span> <span class="synConstant">'actions/checkout'</span>
<span class="synIdentifier">ref</span><span class="synSpecial">:</span> v3.0.2
<span class="synIdentifier">path</span><span class="synSpecial">:</span> actions/checkout@v3
<span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Cache actions/cache
<span class="synIdentifier">uses</span><span class="synSpecial">:</span> actions/checkout@v3
<span class="synIdentifier">with</span><span class="synSpecial">:</span>
<span class="synIdentifier">repository</span><span class="synSpecial">:</span> <span class="synConstant">'actions/cache'</span>
<span class="synIdentifier">ref</span><span class="synSpecial">:</span> v3.0.8
<span class="synIdentifier">path</span><span class="synSpecial">:</span> ${{ inputs.pathRoot }}/actions/cache
<span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Cache actions/upload-artifact
<span class="synIdentifier">uses</span><span class="synSpecial">:</span> actions/checkout@v3
<span class="synIdentifier">with</span><span class="synSpecial">:</span>
<span class="synIdentifier">repository</span><span class="synSpecial">:</span> <span class="synConstant">'actions/upload-artifact'</span>
<span class="synIdentifier">ref</span><span class="synSpecial">:</span> v3.1.0
<span class="synIdentifier">path</span><span class="synSpecial">:</span> ${{ inputs.pathRoot }}/actions/upload-artifact
<span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Cache actions/download-artifact
<span class="synIdentifier">uses</span><span class="synSpecial">:</span> actions/checkout@v3
<span class="synIdentifier">with</span><span class="synSpecial">:</span>
<span class="synIdentifier">repository</span><span class="synSpecial">:</span> <span class="synConstant">'actions/download-artifact'</span>
<span class="synIdentifier">ref</span><span class="synSpecial">:</span> v3.0.0
<span class="synIdentifier">path</span><span class="synSpecial">:</span> ${{ inputs.pathRoot }}/actions/download-artifact
<span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Cache actions/setup-dotnet
<span class="synIdentifier">uses</span><span class="synSpecial">:</span> actions/checkout@v3
<span class="synIdentifier">with</span><span class="synSpecial">:</span>
<span class="synIdentifier">repository</span><span class="synSpecial">:</span> <span class="synConstant">'actions/setup-dotnet'</span>
<span class="synIdentifier">ref</span><span class="synSpecial">:</span> v2.1.0
<span class="synIdentifier">path</span><span class="synSpecial">:</span> ${{ inputs.pathRoot }}/actions/setup-dotnet
<span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Checkout tsgcpp/upload-artifact-private
<span class="synIdentifier">uses</span><span class="synSpecial">:</span> actions/checkout@v3
<span class="synIdentifier">with</span><span class="synSpecial">:</span>
<span class="synComment"> # Same with actions/upload-artifact</span>
<span class="synIdentifier">repository</span><span class="synSpecial">:</span> <span class="synConstant">'tsgcpp/upload-artifact-private'</span>
<span class="synIdentifier">ref</span><span class="synSpecial">:</span> main
<span class="synIdentifier">path</span><span class="synSpecial">:</span> ${{ inputs.pathRoot }}/tsgcpp/upload-artifact-private
<span class="synIdentifier">token</span><span class="synSpecial">:</span> ${{ inputs.patToken }}
</pre>
<p>後は、<code>uses</code>にダウンロード済みのActionを指定するだけです。</p>
<pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synIdentifier">name</span><span class="synSpecial">:</span> <span class="synConstant">"Build Dotnet with Set Up Actions"</span>
<span class="synIdentifier">on</span><span class="synSpecial">:</span>
<span class="synIdentifier">workflow_dispatch</span><span class="synSpecial">:</span> <span class="synSpecial">{}</span>
<span class="synIdentifier">jobs</span><span class="synSpecial">:</span>
<span class="synIdentifier">build</span><span class="synSpecial">:</span>
<span class="synIdentifier">name</span><span class="synSpecial">:</span> Build
<span class="synIdentifier">runs-on</span><span class="synSpecial">:</span> ubuntu-latest
<span class="synIdentifier">steps</span><span class="synSpecial">:</span>
<span class="synStatement">- </span><span class="synIdentifier">uses</span><span class="synSpecial">:</span> actions/checkout@v3
<span class="synIdentifier">with</span><span class="synSpecial">:</span>
<span class="synIdentifier">lfs</span><span class="synSpecial">:</span> <span class="synConstant">true</span>
<span class="synStatement">- </span><span class="synIdentifier">uses</span><span class="synSpecial">:</span> ./.github/composite/setup-actions
<span class="synIdentifier">with</span><span class="synSpecial">:</span>
<span class="synIdentifier">patToken</span><span class="synSpecial">:</span> ${{ secrets.PAT_TOKEN }}
<span class="synStatement">- </span><span class="synIdentifier">uses</span><span class="synSpecial">:</span> actions/cache@v3
<span class="synIdentifier">with</span><span class="synSpecial">:</span>
<span class="synIdentifier">path</span><span class="synSpecial">:</span> ./Lottery/obj
<span class="synIdentifier">key</span><span class="synSpecial">:</span> dotnet-${{ runner.os }}-${{ github.ref_name }}
<span class="synStatement">- </span><span class="synIdentifier">uses</span><span class="synSpecial">:</span> ./.github/composite/dotnet-build-with-setup-actions
<span class="synStatement">- </span><span class="synIdentifier">uses</span><span class="synSpecial">:</span> ./.github/composite/upload-artifact-with-setup-actions
<span class="synIdentifier">with</span><span class="synSpecial">:</span>
<span class="synIdentifier">name</span><span class="synSpecial">:</span> Lottery
<span class="synIdentifier">path</span><span class="synSpecial">:</span> ./Lottery/bin/Release/net6.0
</pre>
<pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synComment"># .github/composite/dotnet-build-with-setup-actions/action.yml</span>
<span class="synIdentifier">name</span><span class="synSpecial">:</span> <span class="synConstant">'Dotnet Build with Set Up Actions'</span>
<span class="synIdentifier">description</span><span class="synSpecial">:</span> <span class="synConstant">'Restore packages, Build and Test'</span>
<span class="synIdentifier">runs</span><span class="synSpecial">:</span>
<span class="synIdentifier">using</span><span class="synSpecial">:</span> <span class="synConstant">"composite"</span>
<span class="synIdentifier">steps</span><span class="synSpecial">:</span>
<span class="synStatement">- </span><span class="synIdentifier">uses</span><span class="synSpecial">:</span> ./.github/repos/actions/setup-dotnet
<span class="synIdentifier">with</span><span class="synSpecial">:</span>
<span class="synIdentifier">dotnet-version</span><span class="synSpecial">:</span> <span class="synConstant">'6.0.x'</span>
<span class="synIdentifier">include-prerelease</span><span class="synSpecial">:</span> <span class="synConstant">false</span>
<span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Restore Packages
<span class="synIdentifier">shell</span><span class="synSpecial">:</span> bash
<span class="synIdentifier">run</span><span class="synSpecial">:</span> dotnet restore ./GitHubActionsTestbed.sln
<span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Build Projects
<span class="synIdentifier">shell</span><span class="synSpecial">:</span> bash
<span class="synIdentifier">run</span><span class="synSpecial">:</span> dotnet build ./GitHubActionsTestbed.sln --configuration Release
<span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Test Projects
<span class="synIdentifier">shell</span><span class="synSpecial">:</span> bash
<span class="synIdentifier">run</span><span class="synSpecial">:</span> dotnet test ./GitHubActionsTestbed.sln --blame
</pre>
<pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synComment"># .github/composite/upload-artifact-with-setup-actions/action.yml</span>
<span class="synIdentifier">name</span><span class="synSpecial">:</span> <span class="synConstant">'Upload Artifact with Set Up Actions'</span>
<span class="synIdentifier">description</span><span class="synSpecial">:</span> <span class="synConstant">'An action to create a artifact'</span>
<span class="synIdentifier">inputs</span><span class="synSpecial">:</span>
<span class="synIdentifier">name</span><span class="synSpecial">:</span>
<span class="synIdentifier">required</span><span class="synSpecial">:</span> <span class="synConstant">true</span>
<span class="synIdentifier">default</span><span class="synSpecial">:</span> <span class="synConstant">'Artifact'</span>
<span class="synIdentifier">path</span><span class="synSpecial">:</span>
<span class="synIdentifier">required</span><span class="synSpecial">:</span> <span class="synConstant">true</span>
<span class="synIdentifier">retention-days</span><span class="synSpecial">:</span>
<span class="synIdentifier">required</span><span class="synSpecial">:</span> <span class="synConstant">false</span>
<span class="synIdentifier">default</span><span class="synSpecial">:</span> <span class="synConstant">3</span>
<span class="synIdentifier">runs</span><span class="synSpecial">:</span>
<span class="synIdentifier">using</span><span class="synSpecial">:</span> <span class="synConstant">"composite"</span>
<span class="synIdentifier">steps</span><span class="synSpecial">:</span>
<span class="synStatement">- </span><span class="synIdentifier">uses</span><span class="synSpecial">:</span> ./.github/repos/actions/upload-artifact
<span class="synIdentifier">with</span><span class="synSpecial">:</span>
<span class="synIdentifier">name</span><span class="synSpecial">:</span> ${{ inputs.name }}
<span class="synIdentifier">path</span><span class="synSpecial">:</span> ${{ inputs.path }}
<span class="synIdentifier">retention-days</span><span class="synSpecial">:</span> ${{ inputs.retention-days }}
</pre>
<p>このやり方の利点は以下があると思っています。</p>
<ul>
<li>複数のWorkflow間で使用するActionのバージョンを統一できる
<ul>
<li>特に同じActionを使う場合でも、v2とv3で指定を間違えるなども回避しやすい</li>
</ul>
</li>
<li>Public Action, Private Actionどちらも使用時の <code>uses</code> への指定方法が統一される
<ul>
<li>どちらもダウンロード済みのActionになっているため</li>
</ul>
</li>
</ul>
<h2 id="対策2-usesで使用するActionを宣言-非推奨">対策2 usesで使用するActionを宣言 (非推奨)</h2>
<p>「ダウンロード済みのActionは再利用される」の性質を利用したやり方ですね。</p>
<p>ただ、このやり方は公式ドキュメントにはないやり方で、仕様の裏をついたやり方なので非推奨です。<br />
また、<code>uses</code>で宣言した限りステップ自体は実行されてしまうため、Actionによっては予期しない副作用が発生する可能性もあります。</p>
<p>事前ダウンロード時に失敗してもワークフローを継続させるために <code>continue-on-error: true</code> を宣言しています。</p>
<pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synIdentifier">jobs</span><span class="synSpecial">:</span>
<span class="synIdentifier">build</span><span class="synSpecial">:</span>
<span class="synIdentifier">name</span><span class="synSpecial">:</span> Build
<span class="synIdentifier">runs-on</span><span class="synSpecial">:</span> ubuntu-latest
<span class="synIdentifier">steps</span><span class="synSpecial">:</span>
<span class="synComment"> # actions/upload-artifactを事前ダウンロードさせる</span>
<span class="synStatement">- </span><span class="synIdentifier">uses</span><span class="synSpecial">:</span> actions/upload-artifact@v3
<span class="synIdentifier">continue-on-error</span><span class="synSpecial">:</span> <span class="synConstant">true</span>
<span class="synComment"> # actions/setup-dotnetを事前ダウンロードさせる</span>
<span class="synStatement">- </span><span class="synIdentifier">uses</span><span class="synSpecial">:</span> actions/setup-dotnet@v2
<span class="synIdentifier">continue-on-error</span><span class="synSpecial">:</span> <span class="synConstant">true</span>
<span class="synIdentifier">with</span><span class="synSpecial">:</span>
<span class="synIdentifier">dotnet-version</span><span class="synSpecial">:</span> <span class="synConstant">'6.0.x'</span>
<span class="synIdentifier">include-prerelease</span><span class="synSpecial">:</span> <span class="synConstant">false</span>
<span class="synStatement">- </span><span class="synIdentifier">uses</span><span class="synSpecial">:</span> actions/checkout@v3
<span class="synIdentifier">with</span><span class="synSpecial">:</span>
<span class="synIdentifier">lfs</span><span class="synSpecial">:</span> <span class="synConstant">true</span>
<span class="synStatement">- </span><span class="synIdentifier">uses</span><span class="synSpecial">:</span> actions/cache@v3
<span class="synIdentifier">with</span><span class="synSpecial">:</span>
<span class="synIdentifier">path</span><span class="synSpecial">:</span> ./Lottery/obj
<span class="synIdentifier">key</span><span class="synSpecial">:</span> dotnet-${{ runner.os }}-${{ github.ref_name }}
<span class="synComment"> # ダウンロード済みのactions/setup-dotnetが使用される</span>
<span class="synStatement">- </span><span class="synIdentifier">uses</span><span class="synSpecial">:</span> ./.github/composite/dotnet-build
<span class="synComment"> # ダウンロード済みのactions/upload-artifactが使用される</span>
<span class="synStatement">- </span><span class="synIdentifier">uses</span><span class="synSpecial">:</span> ./.github/composite/upload-artifact
<span class="synIdentifier">with</span><span class="synSpecial">:</span>
<span class="synIdentifier">name</span><span class="synSpecial">:</span> Lottery
<span class="synIdentifier">path</span><span class="synSpecial">:</span> ./Lottery/bin/Release/net6.0
</pre>
<p><a class="keyword" href="http://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Actions側に事前ダウンロード機能として、usesの <code>pre-download</code>的なオプションを要望として出しても良さそうな気はしてます。</p>
<h1 id="Reusing-Workflows-との違い">Reusing Workflows との違い</h1>
<p><a class="keyword" href="http://d.hatena.ne.jp/keyword/GitHub">GitHub</a> ActionsにはReusing Workflowsという機能があります。<br />
こちらは名前の通りワークフロー全体を再利用する形になります。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdocs.github.com%2Fen%2Factions%2Fusing-workflows%2Freusing-workflows" title="Reusing workflows - GitHub Docs" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://docs.github.com/en/actions/using-workflows/reusing-workflows">docs.github.com</a></cite></p>
<p>一方でComposite Actionは数ステップを集約して、ワークフロー(ジョブ)にステップとして組み込む機能になります。<br />
もし<strong>ワークフロー全体を再利用する場合は、Composite ActionではなくReusing Workflowsのほうが最適</strong>と言えます。</p>
<h1 id="余談筆者が遭遇した事象">余談、筆者が遭遇した事象</h1>
<p>「外部<a class="keyword" href="http://d.hatena.ne.jp/keyword/yaml">yaml</a>に定義されたActionはステップ実行時に遅延ダウンロードされる」の仕様を認識するきっかけになった事象がありました。</p>
<p>Composite Actionを使ったCIのワークフローを運用していて、半年以上問題が発生していなかったのですが、<br />
ある日大事な提出でビルドマシンがいつも以上にガンガン回っているときでした。</p>
<p>初回のcheckoutは問題なく実行されましたが、数時間のビルドを終わらせた後のComposite Action内でActionのダウンロード(checkout)が発生したとき、<br />
なぜか急にUnauthorizedになってcheckout不可になる現象が発生するようになりました。</p>
<p>checkout対象はPublic Actionの <code>uses: actions/upload-artifact</code> だったので、なぜUnauthorizedになったのかは本当に謎でした。。。<br />
ただ、今回の現象に関わらず外部<a class="keyword" href="http://d.hatena.ne.jp/keyword/API">API</a>にアクセスできなくなる可能性は十分に考えられます。</p>
<p>一番の問題は失敗時の時間的損失が大きかったことのため、<br />
<span style="color: #ff0000">外部<a class="keyword" href="http://d.hatena.ne.jp/keyword/API">API</a>などが関係するステップをワークフローの初回に集約し、失敗しても時間的損失を極力回避できるように組み直しました</span>。</p>
<p>今回紹介した <code>setup-actions</code> がその一例となります。</p>
<p>完全なネットワーク障害などに対応しきれるわけではありませんが、あらかじめActionをダウンロードしておくことは一部ワークフローでは有効かと思いますため、<br />
良かったら参考にしていただければと!</p>
<h1 id="サンプルプロジェクト">サンプルプロジェクト</h1>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Ftsgcpp%2FGitHubActionsTestbed" title="GitHub - tsgcpp/GitHubActionsTestbed" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://github.com/tsgcpp/GitHubActionsTestbed">github.com</a></cite></p>
<h1 id="雑感">雑感</h1>
<p>直近はかなりドタバタしていたので、これまた久々の記事です。。。</p>
<p>最近はXR、非XRに限らずインフラはやはり重要だなと痛烈に感じています。<br />
XRアプリの開発もどんどん規模が大きくなっているため、開発基盤の重要性もかなり上がっています。</p>
<p>Unityに限らず<a class="keyword" href="http://d.hatena.ne.jp/keyword/Android">Android</a>, <a class="keyword" href="http://d.hatena.ne.jp/keyword/iOS">iOS</a>, Dockerを含むサーバーサイドのCI/CDを経験してきた身としては、<br />
インフラをもっと強化していきたいと常に考えるようになりました。</p>
<p>そういえば「すぎしーのXRと3DCG」というブログ名ですが、そろそろ改名を考えています。<br />
XR開発はインタ<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%E9%A5%AF">ラク</a>ションやグラフィックスももちろん重要ですが、それに負けないくらいクリエイターが開発に注力できる環境を用意することも大事だと思います。</p>
<p>これからもよろしくです!</p>
<p>それでは~</p>
tsgcpp
【Unity Localization】 GCPのサービスアカウントでプライベートSheetsと連携 (Cronジョブ対応含む)
hatenablog://entry/4207112889910687288
2022-08-21T19:37:30+09:00
2022-08-21T19:37:30+09:00 概要 前回の記事 動作環境 備考 GCPのサービスアカウント対応手順 サービスアカウントの作成 サービスアカウントの認証用JSON形式の鍵をダウンロード 対象のSheetsにサービスアカウントのアクセス権限を付与 GCPのサービスアカウント向けSheetsServiceProviderを作成 GCPのサービスアカウント向けSheetsServiceProviderを使用してPull or Push CronによるGoogle SheetsからのStringTableCollectionの定期更新 そもそもなぜOAuthが使えないのか 鍵文字列をServiceAccountSheetsServi…
<ul class="table-of-contents">
<li><a href="#概要">概要</a><ul>
<li><a href="#前回の記事">前回の記事</a></li>
</ul>
</li>
<li><a href="#動作環境">動作環境</a></li>
<li><a href="#備考">備考</a></li>
<li><a href="#GCPのサービスアカウント対応手順">GCPのサービスアカウント対応手順</a><ul>
<li><a href="#サービスアカウントの作成">サービスアカウントの作成</a></li>
<li><a href="#サービスアカウントの認証用JSON形式の鍵をダウンロード">サービスアカウントの認証用JSON形式の鍵をダウンロード</a></li>
<li><a href="#対象のSheetsにサービスアカウントのアクセス権限を付与">対象のSheetsにサービスアカウントのアクセス権限を付与</a></li>
<li><a href="#GCPのサービスアカウント向けSheetsServiceProviderを作成">GCPのサービスアカウント向けSheetsServiceProviderを作成</a></li>
<li><a href="#GCPのサービスアカウント向けSheetsServiceProviderを使用してPull-or-Push">GCPのサービスアカウント向けSheetsServiceProviderを使用してPull or Push</a></li>
</ul>
</li>
<li><a href="#CronによるGoogle-SheetsからのStringTableCollectionの定期更新">CronによるGoogle SheetsからのStringTableCollectionの定期更新</a><ul>
<li><a href="#そもそもなぜOAuthが使えないのか">そもそもなぜOAuthが使えないのか</a></li>
<li><a href="#鍵文字列をServiceAccountSheetsServiceProviderに渡す方法">鍵文字列をServiceAccountSheetsServiceProviderに渡す方法</a><ul>
<li><a href="#1-環境変数で渡す">1. 環境変数で渡す</a></li>
<li><a href="#2-ジョブ中のみ鍵ファイルを生成">2. ジョブ中のみ鍵ファイルを生成</a></li>
<li><a href="#余談現時点の-game-ciunity-builder-では独自の環境変数をメソッドに渡せない">余談、現時点の game-ci/unity-builder では独自の環境変数をメソッドに渡せない</a></li>
</ul>
</li>
<li><a href="#Pull-or-Push">Pull or Push</a></li>
</ul>
</li>
<li><a href="#おまけGitHub-ActionsでSheets連携をCronジョブ対応">おまけ、GitHub ActionsでSheets連携をCronジョブ対応</a></li>
<li><a href="#拡張パッケージ-Example含む">拡張パッケージ (Example含む)</a></li>
<li><a href="#雑感">雑感</a></li>
</ul>
<h1 id="概要">概要</h1>
<p>今回も前回に引き続きUnity Localizationに関するTipsです。<br />
<a class="keyword" href="http://d.hatena.ne.jp/keyword/GCP">GCP</a>のサービスアカウントでプライベートSheetsと連携する方法を紹介します。</p>
<p>Unity Localization標準の<a class="keyword" href="http://d.hatena.ne.jp/keyword/OAuth%C7%A7%BE%DA">OAuth認証</a>を使用すれば一応プライベートSheetsにアクセスすることは可能ですが、Cronジョブなどで定期更新に対応したいときに不都合があります。</p>
<p>そんな問題を<a class="keyword" href="http://d.hatena.ne.jp/keyword/GCP">GCP</a>のサービスアカウントを用いて解決したいと思います!</p>
<p>おまけでCronジョブで<a class="keyword" href="http://d.hatena.ne.jp/keyword/Google">Google</a> SheetsからStringTableCollectionを定期更新するTipsも紹介します。</p>
<h2 id="前回の記事">前回の記事</h2>
<p>良かったら合わせてどうぞ!</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftsgcpp.hateblo.jp%2Fentry%2F2022%2F07%2F29%2F233111" title=" 【Unity Localization】 複数のStringTableCollectionを一括PullのTips (拡張ツール提供有り) - すぎしーのXRと3DCG" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://tsgcpp.hateblo.jp/entry/2022/07/29/233111">tsgcpp.hateblo.jp</a></cite></p>
<h1 id="動作環境">動作環境</h1>
<ul>
<li>Unity 2021.3.6f1
<ul>
<li>Unity 2020 でも可</li>
</ul>
</li>
<li>Unity Localization 1.3.2</li>
</ul>
<h1 id="備考">備考</h1>
<p><strong>今回紹介する機能は拡張パッケージとしても用意しています。 </strong></p>
<p>記事の最後に<a class="keyword" href="http://d.hatena.ne.jp/keyword/GitHub">GitHub</a>へのリンクを記載していますため、よろしければどうぞ。</p>
<h1 id="GCPのサービスアカウント対応手順"><a class="keyword" href="http://d.hatena.ne.jp/keyword/GCP">GCP</a>のサービスアカウント対応手順</h1>
<h2 id="サービスアカウントの作成">サービスアカウントの作成</h2>
<ul>
<li><a class="keyword" href="http://d.hatena.ne.jp/keyword/GCP">GCP</a>のCredentialsページにてサービスアカウントを作成
<ul>
<li><a class="keyword" href="http://d.hatena.ne.jp/keyword/GCP">GCP</a>自体のアカウント作成などは各自調べてください</li>
</ul>
</li>
</ul>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20220821/20220821165635.png" width="798" height="398" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fconsole.cloud.google.com%2Fapis%2Fcredentials" title="Google Cloud Platform" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://console.cloud.google.com/apis/credentials">console.cloud.google.com</a></cite></p>
<h2 id="サービスアカウントの認証用JSON形式の鍵をダウンロード">サービスアカウントの認証用<a class="keyword" href="http://d.hatena.ne.jp/keyword/JSON">JSON</a>形式の鍵をダウンロード</h2>
<p>以下のような<a class="keyword" href="http://d.hatena.ne.jp/keyword/JSON">JSON</a>形式の鍵がダウンロード出来ます。<br />
<a class="keyword" href="http://d.hatena.ne.jp/keyword/JSON">JSON</a>文字列は後述するクラスに渡すために使用します。</p>
<pre class="code lang-json" data-lang="json" data-unlink><span class="synSpecial">{</span>
"<span class="synStatement">type</span>": "<span class="synConstant">service_account</span>",
"<span class="synStatement">project_id</span>": "<span class="synConstant">xxx-workspace</span>",
"<span class="synStatement">private_key_id</span>": "<span class="synConstant">xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx</span>",
"<span class="synStatement">private_key</span>": "<span class="synConstant">-----BEGIN PRIVATE KEY-----</span><span class="synSpecial">\n</span><span class="synConstant">...</span>",
"<span class="synStatement">client_email</span>": "<span class="synConstant">[email protected]</span>",
"<span class="synStatement">client_id</span>": "<span class="synConstant">121212121212121212121</span>",
"<span class="synStatement">auth_uri</span>": "<span class="synConstant">https://accounts.google.com/o/oauth2/auth</span>",
"<span class="synStatement">token_uri</span>": "<span class="synConstant">https://oauth2.googleapis.com/token</span>",
"<span class="synStatement">auth_provider_x509_cert_url</span>": "<span class="synConstant">https://www.googleapis.com/oauth2/v1/certs</span>",
"<span class="synStatement">client_x509_cert_url</span>": "<span class="synConstant">...</span>"
<span class="synSpecial">}</span>
</pre>
<p><span style="color: #ff0000">※アクセス権限に使用する鍵のため、厳重に管理してください</span></p>
<h2 id="対象のSheetsにサービスアカウントのアクセス権限を付与">対象のSheetsにサービスアカウントのアクセス権限を付与</h2>
<p>Pullのみの場合は閲覧権限、Pushも行う場合は編集権限をサービスアカウントに与えてください。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20220821/20220821180628.png" width="599" height="238" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span>
<span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20220821/20220821180639.png" width="775" height="354" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<h2 id="GCPのサービスアカウント向けSheetsServiceProviderを作成"><a class="keyword" href="http://d.hatena.ne.jp/keyword/GCP">GCP</a>のサービスアカウント向けSheetsServiceProviderを作成</h2>
<p>1.3.2時点のUnity Localizationには<a class="keyword" href="http://d.hatena.ne.jp/keyword/GCP">GCP</a>のサービスアカウント連携用クラスが用意されていなかったため用意します。<br />
いずれ公式から提供される気がしますが、今回は実装します。</p>
<pre class="code lang-cs" data-lang="cs" data-unlink> <span class="synType">public</span> <span class="synType">class</span> <span class="synType">ServiceAccountSheetsServiceProvider </span><span class="synStatement">:</span> IGoogleSheetsService
{
<span class="synType">private</span> <span class="synType">readonly</span> <span class="synType">string</span> _serviceAccountKeyJson;
<span class="synType">private</span> <span class="synType">readonly</span> <span class="synType">string</span> _applicationName;
<span class="synType">public</span> ServiceAccountSheetsServiceProvider(
<span class="synType">string</span> serviceAccountKeyJson,
<span class="synType">string</span> applicationName)
{
_serviceAccountKeyJson <span class="synStatement">=</span> serviceAccountKeyJson;
_applicationName <span class="synStatement">=</span> applicationName;
}
<span class="synType">public</span> SheetsService Service <span class="synStatement">=></span> GetSheetsService();
<span class="synType">private</span> SheetsService GetSheetsService()
{
<span class="synType">var</span> credential <span class="synStatement">=</span> GoogleCredential.FromJson(_serviceAccountKeyJson);
<span class="synType">var</span> initializer <span class="synStatement">=</span> <span class="synStatement">new</span> BaseClientService.Initializer
{
HttpClientInitializer <span class="synStatement">=</span> credential,
ApplicationName <span class="synStatement">=</span> _applicationName,
};
<span class="synType">var</span> sheetsService <span class="synStatement">=</span> <span class="synStatement">new</span> SheetsService(initializer);
<span class="synStatement">return</span> sheetsService;
}
}
</pre>
<ul>
<li><code>serviceAccountKeyJson</code> には、先程作成したサービスアカウントの<a class="keyword" href="http://d.hatena.ne.jp/keyword/JSON">JSON</a>文字列を渡す</li>
<li><code>applicationName</code> には GoogleSheetsService (ScriptableObject) に設定した <code>ApplicationName</code> を指定</li>
</ul>
<h2 id="GCPのサービスアカウント向けSheetsServiceProviderを使用してPull-or-Push"><a class="keyword" href="http://d.hatena.ne.jp/keyword/GCP">GCP</a>のサービスアカウント向けSheetsServiceProviderを使用してPull or Push</h2>
<ul>
<li>先程作成した <code>ServiceAccountSheetsServiceProvider</code> を用いてPull or Pushを実施
<ul>
<li>※以下の実装は、<a href="https://tsgcpp.hateblo.jp/entry/2022/07/29/233111#%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%97%E3%83%88%E3%81%A7-StringTableCollection-%E3%82%92Pull%E3%81%99%E3%82%8B%E6%96%B9%E6%B3%95">前回の記事</a>で紹介したコードを<code>ServiceAccountSheetsServiceProvider</code>に入れ替えたもの</li>
</ul>
</li>
</ul>
<pre class="code lang-cs" data-lang="cs" data-unlink> <span class="synComment">// 対象のStringTableCollectionを取得</span>
<span class="synType">var</span> collection <span class="synStatement">=</span> AssetDatabase.LoadAssetAtPath<StringTableCollection>(<span class="synConstant">"Assets/<path to StringTableCollection>"</span>);
<span class="synComment">// GoogleSheetsExtensionをStringTableCollectionから取得</span>
<span class="synType">var</span> sheetsExtension <span class="synStatement">=</span> collection.Extensions.OfType<GoogleSheetsExtension>().FirstOrDefault();
<span class="synComment">// *前回の記事と異なり、ここでServiceAccountSheetsServiceProviderを使用</span>
<span class="synType">var</span> serviceProvider <span class="synStatement">=</span> <span class="synStatement">new</span> ServiceAccountSheetsServiceProvider(
<span class="synStatement"> serviceAccountKeyJson:</span> <span class="synConstant">"<GCPサービスアカウントのJSON形式の鍵の文字列>"</span>,
<span class="synStatement"> applicationName:</span> <span class="synConstant">"<GoogleSheetsService (ScriptableObject) に設定したApplicationName>"</span>);
<span class="synComment">// Google Sheetsアクセス用インスタンスを生成</span>
<span class="synType">var</span> sheets <span class="synStatement">=</span> <span class="synStatement">new</span> GoogleSheets(serviceProvider);
<span class="synComment">// ※必ずSpreadSheetIdをGoogleSheetsインスタンスに指定してください!</span>
sheets.SpreadSheetId <span class="synStatement">=</span> sheetsExtension.SpreadsheetId;
<span class="synComment">// 対象のStringTableCollection内の全言語(全Locale)のPullを実施</span>
sheets.PullIntoStringTableCollection(
<span class="synStatement"> sheetId:</span> sheetsExtension.SheetId,
<span class="synStatement"> collection:</span> collection,
<span class="synStatement"> columnMapping:</span> sheetsExtension.Columns);
</pre>
<p>上記の方法を使用することで、<a class="keyword" href="http://d.hatena.ne.jp/keyword/GCP">GCP</a>のサービスアカウントでPull及びPushが可能となります。</p>
<p>以上が<a class="keyword" href="http://d.hatena.ne.jp/keyword/GCP">GCP</a>のサービスアカウントを使用した連携方法となります。</p>
<hr />
<h1 id="CronによるGoogle-SheetsからのStringTableCollectionの定期更新">Cronによる<a class="keyword" href="http://d.hatena.ne.jp/keyword/Google">Google</a> SheetsからのStringTableCollectionの定期更新</h1>
<p>さて、今回の実質的な本題に移りたいと思います!<br />
今回、<a class="keyword" href="http://d.hatena.ne.jp/keyword/GCP">GCP</a>サービスアカウントを用いた本当の理由はCron(定期ジョブ)を使ったStringTableCollection更新に対応させるためです!</p>
<h2 id="そもそもなぜOAuthが使えないのか">そもそもなぜOAuthが使えないのか</h2>
<p>Cron対応なしで運用する場合はそもそも<a class="keyword" href="http://d.hatena.ne.jp/keyword/GCP">GCP</a>サービスアカウント対応は不要です。<br />
概要で少し触れましたがプライベートのSheetsには標準の<a class="keyword" href="http://d.hatena.ne.jp/keyword/OAuth%C7%A7%BE%DA">OAuth認証</a>でも連携可能です。</p>
<p>Unity Localization標準のOAuthの問題は、主に以下の2点です。</p>
<ul>
<li><strong><span style="color: #ff0000">認証にUnityエディタ上でのUI操作が必要</span></strong>
<ul>
<li>batchモード(<a class="keyword" href="http://d.hatena.ne.jp/keyword/CLI">CLI</a>)での認証が困難</li>
</ul>
</li>
<li>認証後に生成される認証ファイルが <code>Library/Google/GoogleSheetsService</code> 以下で管理
<ul>
<li><code>Library</code>フォルダはUnityにおけるキャッシュフォルダでもあるため、クリーンビルドを行うCIなどと相性が悪い</li>
</ul>
</li>
</ul>
<p>というわけで、<br />
<strong>batchモードで認証を完結させるために、<a class="keyword" href="http://d.hatena.ne.jp/keyword/JSON">JSON</a>形式の鍵文字列を渡すだけで認証が可能な<code>ServiceAccountSheetsServiceProvider</code> を用意</strong>した感じです。</p>
<h2 id="鍵文字列をServiceAccountSheetsServiceProviderに渡す方法">鍵文字列をServiceAccountSheetsServiceProviderに渡す方法</h2>
<p>サービスアカウントの鍵はアクセス権限を握るセンシティブなデータファイルなので、鍵の文字列をgit<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>内で管理することはタブーです!</p>
<p>セキュリティを考慮して使用後はキャッシュとして残らない方法と取りましょう。</p>
<h3 id="1-環境変数で渡す">1. <a class="keyword" href="http://d.hatena.ne.jp/keyword/%B4%C4%B6%AD%CA%D1%BF%F4">環境変数</a>で渡す</h3>
<p>CIとも相性がよくジョブプロセス単位で管理可能なため、筆者としても<a class="keyword" href="http://d.hatena.ne.jp/keyword/%B4%C4%B6%AD%CA%D1%BF%F4">環境変数</a>を利用する方法がオススメです!</p>
<pre class="code lang-cs" data-lang="cs" data-unlink> <span class="synType">const</span> <span class="synType">string</span> EnvironmentGoogleServiceAccountKey <span class="synStatement">=</span> <span class="synConstant">"UNITY_LOCALIZATION_GOOGLE_SERVICE_ACCOUNT_KEY"</span>;
<span class="synType">var</span> serviceAccountKeyJson <span class="synStatement">=</span> Environment.GetEnvironmentVariable(EnvironmentGoogleServiceAccountKey);
<span class="synType">var</span> serviceProvider <span class="synStatement">=</span> <span class="synStatement">new</span> ServiceAccountSheetsServiceProvider(
<span class="synStatement"> serviceAccountKeyJson:</span> serviceAccountKeyJson,
<span class="synStatement"> applicationName:</span> <span class="synConstant">"<GoogleSheetsService (ScriptableObject) に設定したApplicationName>"</span>);
</pre>
<h3 id="2-ジョブ中のみ鍵ファイルを生成">2. ジョブ中のみ鍵ファイルを生成</h3>
<p><a class="keyword" href="http://d.hatena.ne.jp/keyword/%B4%C4%B6%AD%CA%D1%BF%F4">環境変数</a>を使用できない場合は、ジョブ中のみ<a class="keyword" href="http://d.hatena.ne.jp/keyword/JSON">JSON</a>形式の鍵ファイルを生成する対応が一つの選択肢になります。</p>
<p>以下は<a class="keyword" href="http://d.hatena.ne.jp/keyword/bash">bash</a>を用いて<a class="keyword" href="http://d.hatena.ne.jp/keyword/%B4%C4%B6%AD%CA%D1%BF%F4">環境変数</a>から<a class="keyword" href="http://d.hatena.ne.jp/keyword/JSON">JSON</a>鍵ファイルを復元する方法です。</p>
<pre class="code bash" data-lang="bash" data-unlink>$ echo "${UNITY_LOCALIZATION_GOOGLE_SERVICE_ACCOUNT_KEY}" > "<path to key>/service-account-key.json"</pre>
<p>あとは、上記鍵ファイルから<a class="keyword" href="http://d.hatena.ne.jp/keyword/JSON">JSON</a>文字列を取り出して渡すだけです。</p>
<pre class="code lang-cs" data-lang="cs" data-unlink> <span class="synType">const</span> <span class="synType">string</span> JsonKeyPath <span class="synStatement">=</span> <span class="synConstant">"<path to key>/service-account-key.json"</span>;
<span class="synType">string</span> serviceAccountKeyJson <span class="synStatement">=</span> File.ReadAllText(keyJsonPath);
<span class="synType">var</span> serviceProvider <span class="synStatement">=</span> <span class="synStatement">new</span> ServiceAccountSheetsServiceProvider(
<span class="synStatement"> serviceAccountKeyJson:</span> serviceAccountKeyJson,
<span class="synStatement"> applicationName:</span> bundle.SheetsServiceProvider.ApplicationName);
</pre>
<p><span style="color: #ff0000"><strong>ジョブの成否に関わらず、不要になったら生成した鍵ファイル削除を忘れずに!</strong></span></p>
<pre class="code bash" data-lang="bash" data-unlink>$ rm "<path to key>/service-account-key.json"</pre>
<h3 id="余談現時点の-game-ciunity-builder-では独自の環境変数をメソッドに渡せない">余談、現時点の game-ci/unity-builder では独自の<a class="keyword" href="http://d.hatena.ne.jp/keyword/%B4%C4%B6%AD%CA%D1%BF%F4">環境変数</a>をメソッドに渡せない</h3>
<p><a class="keyword" href="http://d.hatena.ne.jp/keyword/GitHub">GitHub</a> ActionsでGameCIを使っている人は結構いるかと思いますが、<br />
残念ながら<u><code>game-ci/unity-builder</code>の<code>buildMethod</code>でUnity側のメソッドを叩くときに独自の<a class="keyword" href="http://d.hatena.ne.jp/keyword/%B4%C4%B6%AD%CA%D1%BF%F4">環境変数</a>を渡せない</u>ようでした。。。 (知っている方がいたらぜひ教えてください!)</p>
<p>そんな経緯もあって、「ジョブ中のみ鍵ファイルを生成」という方法を紹介しました。</p>
<h2 id="Pull-or-Push">Pull or Push</h2>
<p>あとは「<a class="keyword" href="http://d.hatena.ne.jp/keyword/GCP">GCP</a>のサービスアカウント向けSheetsServiceProviderを使用してPull or Push」と同様に生成した<code>serviceProvider</code>を使用するだけです。</p>
<pre class="code lang-cs" data-lang="cs" data-unlink> <span class="synComment">// Google Sheetsアクセス用インスタンスを生成</span>
<span class="synType">var</span> sheets <span class="synStatement">=</span> <span class="synStatement">new</span> GoogleSheets(serviceProvider);
<span class="synComment">// ※必ずSpreadSheetIdをGoogleSheetsインスタンスに指定してください!</span>
sheets.SpreadSheetId <span class="synStatement">=</span> sheetsExtension.SpreadsheetId;
<span class="synComment">// 対象のStringTableCollection内の全言語(全Locale)のPullを実施</span>
sheets.PullIntoStringTableCollection(
<span class="synStatement"> sheetId:</span> sheetsExtension.SheetId,
<span class="synStatement"> collection:</span> collection,
<span class="synStatement"> columnMapping:</span> sheetsExtension.Columns);
</pre>
<hr />
<h1 id="おまけGitHub-ActionsでSheets連携をCronジョブ対応">おまけ、<a class="keyword" href="http://d.hatena.ne.jp/keyword/GitHub">GitHub</a> ActionsでSheets連携をCronジョブ対応</h1>
<p>サンプルを用意したので、よければ参考にして下さい。</p>
<ul>
<li><a href="https://github.com/tsgcpp/UnityLocalizationExtension/blob/main/.github/workflows/pull-localization.yml">pull-localization.yml (GitHub Actionsのワークフロー)</a></li>
<li><a href="https://github.com/tsgcpp/UnityLocalizationExtension/commits/main/Assets/Example/Editor/ExampleLocalizationSynchronizationMenu.cs">ExampleLocalizationSynchronizationMenu.cs (Unity側のメソッド)</a>
<ul>
<li>コード内のregion "Service Account Key from Environment Variable" を参照</li>
</ul>
</li>
</ul>
<p>大まかな流れは以下です。</p>
<ul>
<li><a class="keyword" href="http://d.hatena.ne.jp/keyword/GitHub">GitHub</a> ActionsのSecrets (<code>GOOGLE_SERVICE_ACCOUNT_KEY_JSON_BASE64</code>) から鍵文字列をデコードして<a class="keyword" href="http://d.hatena.ne.jp/keyword/JSON">JSON</a>ファイルを生成
<ul>
<li>鍵文字列の<a class="keyword" href="http://d.hatena.ne.jp/keyword/Base64">Base64</a>対応は必須ではないですが、環境依存な文字(かっこやカンマなど)があっても対応しやすいので入れています</li>
</ul>
</li>
<li>Unity側のメソッド <code>PullAllLocalizationTablesFromTempKeyJson</code> を<code>game-ci/unity-builder</code>で<code>buildMethod</code>経由で実行
<ul>
<li><code>game-ci/unity-builder</code>の<code>buildMethod</code>の使用方法はGameCIのドキュメントを参照</li>
</ul>
</li>
<li>Pull完了後は生成した鍵ファイルを削除</li>
<li>Commit, Push して Pull Request</li>
</ul>
<p>ちなみに、サンプルでは以下のようなプルリク<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>トが発行されます。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Ftsgcpp%2FUnityLocalizationExtension%2Fpull%2F32" title="[CI] Update StringCollectionTable (2022/08/21 07:37) by github-actions[bot] · Pull Request #32 · tsgcpp/UnityLocalizationExtension" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://github.com/tsgcpp/UnityLocalizationExtension/pull/32">github.com</a></cite></p>
<h1 id="拡張パッケージ-Example含む">拡張パッケージ (Example含む)</h1>
<p>使用方法はREADME.mdの "Feature" を参照</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Ftsgcpp%2FUnityLocalizationExtension" title="GitHub - tsgcpp/UnityLocalizationExtension: Extension package for Unity Localization" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://github.com/tsgcpp/UnityLocalizationExtension">github.com</a></cite></p>
<h1 id="雑感">雑感</h1>
<p>Unity Localization第2弾でした。<br />
また時間が空いてしまいました。。。</p>
<p>Unityと<a class="keyword" href="http://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Actionsを活用したワークフローに関するノウハウも結構溜まってきたので、機会があれば紹介したいですね。<br />
次回の記事は決まってませんが、状況が落ち着いたらまた何か書きます。</p>
<p>それでは~</p>
tsgcpp
【Unity Localization】 複数のStringTableCollectionを一括PullのTips (拡張ツール提供有り)
hatenablog://entry/4207112889903946388
2022-07-29T23:31:11+09:00
2022-07-29T23:31:11+09:00 概要 環境 補足 StringTableCollection GoogleSheetsService Tips スクリプトで StringTableCollection をPullする方法 複数のStringTableCollectionを一括でPullする方法 StringTableCollectionを一括で取得するEditorコード 余談、公式サンプルコードについて 拡張ツールの紹介 サンプルコード 雑感 概要 今回は Unity Localization に関するTipsです。 ついでに拡張パッケージの作ってみたのでよかったら使ってみてください。 docs.unity3d.com 最近…
<ul class="table-of-contents">
<li><a href="#概要">概要</a></li>
<li><a href="#環境">環境</a></li>
<li><a href="#補足">補足</a><ul>
<li><a href="#StringTableCollection">StringTableCollection</a></li>
<li><a href="#GoogleSheetsService">GoogleSheetsService</a></li>
</ul>
</li>
<li><a href="#Tips">Tips</a><ul>
<li><a href="#スクリプトで-StringTableCollection-をPullする方法">スクリプトで StringTableCollection をPullする方法</a></li>
<li><a href="#複数のStringTableCollectionを一括でPullする方法">複数のStringTableCollectionを一括でPullする方法</a><ul>
<li><a href="#StringTableCollectionを一括で取得するEditorコード">StringTableCollectionを一括で取得するEditorコード</a></li>
</ul>
</li>
<li><a href="#余談公式サンプルコードについて">余談、公式サンプルコードについて</a></li>
</ul>
</li>
<li><a href="#拡張ツールの紹介">拡張ツールの紹介</a></li>
<li><a href="#サンプルコード">サンプルコード</a></li>
<li><a href="#雑感">雑感</a></li>
</ul>
<h1 id="概要">概要</h1>
<p>今回は Unity Localization に関するTipsです。<br />
ついでに拡張パッケージの作ってみたのでよかったら使ってみてください。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdocs.unity3d.com%2FPackages%2Fcom.unity.localization%401.3%2Fmanual%2Findex.html" title="About Localization | Localization | 1.3.2" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://docs.unity3d.com/Packages/[email protected]/manual/index.html">docs.unity3d.com</a></cite></p>
<p>最近 Unity Localization を触りだしたのですが、リリースされてまだ日も浅いパッケージのせいかまだまだ機能に物足りなさも感じられます。<br />
今回はUnity Localizationで<a class="keyword" href="http://d.hatena.ne.jp/keyword/Google">Google</a> Sheetsから 複数の<code>StringTableCollection</code> をまとめて更新するTipsを紹介します。</p>
<p>Unity Localization に関しては デニックさんの以下の記事をご覧ください!</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fxrdnk.hateblo.jp%2Fentry%2F2021%2F11%2F26%2F090000" title="Unity Localization を用いた多言語対応の実装方法 - デニッキ!" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://xrdnk.hateblo.jp/entry/2021/11/26/090000">xrdnk.hateblo.jp</a></cite></p>
<h1 id="環境">環境</h1>
<ul>
<li>Unity 2021.3.6f1</li>
<li>Unity Localization 1.3.2</li>
</ul>
<h1 id="補足">補足</h1>
<h2 id="StringTableCollection">StringTableCollection</h2>
<p><code>StringTableCollection</code>はKey, <a class="keyword" href="http://d.hatena.ne.jp/keyword/Value">Value</a>形式で全言語分の翻訳テーブルをまとめているScriptableObjectです。<br />
<code>StringTable</code>はRuntimeで使用可能に対して、<code>StringTableCollection</code>はEditor限定の機能となります。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20220729/20220729173827.png" width="780" height="292" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p><a class="keyword" href="http://d.hatena.ne.jp/keyword/Google">Google</a> Sheetsとの対応は <code>Extensions</code> 項目の <code>GoogleSheets</code> を追加及び設定することで実現できます。<br />
<code>SpreadSheetsId</code>, <code>SheetsId</code> (<a class="keyword" href="http://d.hatena.ne.jp/keyword/gid">gid</a>) を設定することで、<a class="keyword" href="http://d.hatena.ne.jp/keyword/Google">Google</a>ドライブ上の<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B9%A5%D7%A5%EC%A5%C3%A5%C9%A5%B7%A1%BC%A5%C8">スプレッドシート</a>と対応させます。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20220729/20220729174045.png" width="469" height="562" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<h2 id="GoogleSheetsService">GoogleSheetsService</h2>
<p><code>GoogleSheetsService</code> (<code>SheetsServiceProvider</code>) は <a class="keyword" href="http://d.hatena.ne.jp/keyword/Google">Google</a>のサービスとの認証関連を設定するScriptableObjectです。<br />
<u>今回はこちらを使用するため設定をお願いします。</u></p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20220729/20220729174418.png" width="804" height="445" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p>認証の設定については以下のデニックさんの記事をどうぞ!</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fxrdnk.hateblo.jp%2Fentry%2Funity_localization_google_spreadsheet_" title="Unity Localization の String Table と CSV ファイル・Google SpreadSheet を連携させる - デニッキ!" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://xrdnk.hateblo.jp/entry/unity_localization_google_spreadsheet_">xrdnk.hateblo.jp</a></cite></p>
<h1 id="Tips">Tips</h1>
<h2 id="スクリプトで-StringTableCollection-をPullする方法"><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%EA%A5%D7%A5%C8">スクリプト</a>で <code>StringTableCollection</code> をPullする方法</h2>
<p>簡単に説明すると <code>GoogleSheets.PullIntoStringTableCollection</code> に認証済みの<code>SheetsServiceProvider</code>, <code>SpreadSheetsId</code>, <code>SheetsId</code> を渡すことで可能となります。</p>
<p>結構説明が難しいのでサンプルコードを記載します。</p>
<pre class="code lang-cs" data-lang="cs" data-unlink> <span class="synComment">// 対象のStringTableCollectionを取得</span>
StringTableCollection collection = AssetDatabase.LoadAssetAtPath<StringTableCollection>(<span class="synConstant">"Assets/<path to StringTableCollection>"</span>);
<span class="synComment">// GoogleSheetsExtensionをStringTableCollectionから取得</span>
var sheetsExtension = collection.Extensions.OfType<GoogleSheetsExtension>().FirstOrDefault();
<span class="synComment">// Google認証設定を持つSheetsServiceProviderを取得</span>
SheetsServiceProvider serviceProvider = AssetDatabase.LoadAssetAtPath<SheetsServiceProvider>(<span class="synConstant">"Assets/<path to SheetsServiceProvider>"</span>);
<span class="synComment">// Google Sheetsアクセス用インスタンスを生成</span>
var sheets = <span class="synStatement">new</span> GoogleSheets(serviceProvider);
<span class="synComment">// ※必ずSpreadSheetIdをGoogleSheetsインスタンスに指定してください!</span>
sheets.SpreadSheetId = sheetsExtension.SpreadsheetId;
<span class="synComment">// 対象のStringTableCollection内の全言語(全Locale)のPullを実施</span>
sheets.PullIntoStringTableCollection(
<span class="synStatement"> sheetId:</span> sheetsExtension.SheetId,
<span class="synStatement"> collection:</span> collection,
<span class="synStatement"> columnMapping:</span> sheetsExtension.Columns);
</pre>
<p><strong><span style="color: #ff0000"><code>sheets.SpreadSheetId = sheetsExtension.SpreadsheetId;</code> は必ず実施してください。 </span></strong> <br />
どうやら1.3.2時点ではInspectorのPullを押したときしか実施されていないようです。</p>
<p>上記コードが <code>StringTableCollection</code> のInspector上のPullボタンと似た処理を実施しています。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20220729/20220729180140.png" width="483" height="521" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<h2 id="複数のStringTableCollectionを一括でPullする方法">複数のStringTableCollectionを一括でPullする方法</h2>
<p><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%EA%A5%D7%A5%C8">スクリプト</a>でPullする方法がわかったので、あとは対象のStringTableCollectionを取得して for ループで実行するのみです。</p>
<h3 id="StringTableCollectionを一括で取得するEditorコード">StringTableCollectionを一括で取得するEditorコード</h3>
<p>自分は以下のような指定のフォルダ以下のアセットをまとめて取得する<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%EA%A5%D7%A5%C8">スクリプト</a>を使用しています。</p>
<p><a href="https://github.com/tsgcpp/UnityLocalizationExtension/blob/main/Assets/Plugins/LocalizationExtension/Editor/AssetTool/AssetFinding.cs">AssetFinding.cs</a></p>
<h2 id="余談公式サンプルコードについて">余談、公式サンプルコードについて</h2>
<p>上記のコードのヒントですが、Unity Localization に含まれている <code>DocCodeSamples.Tests/GoogleSheetsSamples.cs</code> に記載されています。</p>
<p>また、その他のサンプルコードも <code>DocCodeSamples.Tests/</code> 以下にいくつかありますため参考にすると良いと思います!</p>
<h1 id="拡張ツールの紹介">拡張ツールの紹介</h1>
<p>今回、以下のようなUnity Localitionの拡張ツールを用意しました。<br />
いずれ提供される機能だとは思いますが、それまでの代用やコードの参考としてどうぞ。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Ftsgcpp%2FUnityLocalizationExtension" title="GitHub - tsgcpp/UnityLocalizationExtension: Extension package for Unity Localization" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://github.com/tsgcpp/UnityLocalizationExtension">github.com</a></cite></p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20220729/20220729181357.png" width="484" height="370" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p><code>StringTableCollectionBundle</code> というScriptableObjectを設定することでまとめてPull, Pushできるようにしています。<br />
Pullには閲覧権限、Pushには編集権限が必要となります。</p>
<p>詳細な使用方法は README.md を参照ください。</p>
<h1 id="サンプルコード">サンプルコード</h1>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Ftsgcpp%2FUnityLocalizationExtension" title="GitHub - tsgcpp/UnityLocalizationExtension: Extension package for Unity Localization" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://github.com/tsgcpp/UnityLocalizationExtension">github.com</a></cite></p>
<p><code>Assets/Example</code> 以下に実装例があります。</p>
<h1 id="雑感">雑感</h1>
<p>まさかの<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%ED%A1%BC%A5%AB%A5%E9%A5%A4%A5%BA">ローカライズ</a>系の記事になりました。</p>
<p>前回の記事で絶対にXR系の記事を書くと述べたのですが、<br />
どうもいい感じのネタになっておらず一旦別の記事に逃げることにしました。。。</p>
<p>さて、次回は今回の延長で「Unity Localizationで<a class="keyword" href="http://d.hatena.ne.jp/keyword/GCP">GCP</a>のサービスアカウントを使ってプライベートなSheetsと連携」みたいな記事を書く予定です。<br />
(ちなみにこの機能はすでに UnityLocalizationExtension に組み込んであったりします。)</p>
<p>それでは~</p>
tsgcpp
GitHub ActionsのWindows self-hostedでpwshとbashを使う方法
hatenablog://entry/13574176438093942378
2022-05-20T00:38:59+09:00
2022-05-20T00:38:59+09:00 今回は GitHub ActionsのWindows self-hostedでpwshとbashを使う方法を紹介します。
残念ながら素のWindowsマシンでshellとしてpwshやbashを指定しても使用することができません。
Windows上でpwshとbashが使用できるとCIで行えるフローの選択肢が大幅に増えると思いますので、ぜひ参考にしていただければと思います!
<ul class="table-of-contents">
<li><a href="#概要">概要</a></li>
<li><a href="#動作環境">動作環境</a></li>
<li><a href="#pwshとbashを使用する条件">pwshとbashを使用する条件</a></li>
<li><a href="#手順">手順</a><ul>
<li><a href="#1-Git-for-Windows-と-PowerShell7-をインストール">1. Git for Windows と PowerShell7 をインストール</a></li>
<li><a href="#2-システム環境変数のPATHに登録">2. システム環境変数のPATHに登録</a></li>
<li><a href="#3-self-hostedのrunnerプロセスを再起動">3. self-hostedのrunnerプロセスを再起動</a></li>
</ul>
</li>
<li><a href="#詳細">詳細</a><ul>
<li><a href="#確認用のworkflow">確認用のworkflow</a></li>
<li><a href="#インストール前">インストール前</a></li>
<li><a href="#インストール後">インストール後</a></li>
</ul>
</li>
<li><a href="#活用例">活用例</a><ul>
<li><a href="#日付を文字列として取得">日付を文字列として取得</a></li>
<li><a href="#レジストリから値を取得">レジストリから値を取得</a></li>
</ul>
</li>
<li><a href="#余談GitHub-Actionsで提供されているWindowsのRunnerでは標準でpwshとbashが使用可能">余談、GitHub Actionsで提供されているWindowsのRunnerでは標準でpwshとbashが使用可能</a><ul>
<li><a href="#windows-latestでpwshとbashが使用可能なことを確認">windows-latestでpwshとbashが使用可能なことを確認</a></li>
</ul>
</li>
<li><a href="#雑感">雑感</a></li>
</ul>
<h1 id="概要">概要</h1>
<p>今回は <a class="keyword" href="http://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Actionsの<a class="keyword" href="http://d.hatena.ne.jp/keyword/Windows">Windows</a> self-hostedでpwshと<a class="keyword" href="http://d.hatena.ne.jp/keyword/bash">bash</a>を使う方法を紹介します。</p>
<p>残念ながら素の<a class="keyword" href="http://d.hatena.ne.jp/keyword/Windows">Windows</a>マシンでshellとしてpwshや<a class="keyword" href="http://d.hatena.ne.jp/keyword/bash">bash</a>を指定しても使用することができません。</p>
<p><a class="keyword" href="http://d.hatena.ne.jp/keyword/Windows">Windows</a>上でpwshと<a class="keyword" href="http://d.hatena.ne.jp/keyword/bash">bash</a>が使用できるとCIで行えるフローの選択肢が大幅に増えると思いますので、ぜひ参考にしていただければと思います!</p>
<h1 id="動作環境">動作環境</h1>
<ul>
<li>Windows10 Pro (Homeでも可)</li>
</ul>
<h1 id="pwshとbashを使用する条件">pwshと<a class="keyword" href="http://d.hatena.ne.jp/keyword/bash">bash</a>を使用する条件</h1>
<p>簡単に言えば、以下を満たせば良いです</p>
<ul>
<li>Git for <a class="keyword" href="http://d.hatena.ne.jp/keyword/Windows">Windows</a> (Git <a class="keyword" href="http://d.hatena.ne.jp/keyword/Bash">Bash</a>) と PowerShell7 (pwsh付属) をインストール</li>
<li><a class="keyword" href="http://d.hatena.ne.jp/keyword/Windows">Windows</a>のシステム<a class="keyword" href="http://d.hatena.ne.jp/keyword/%B4%C4%B6%AD%CA%D1%BF%F4">環境変数</a>のPATHに <code>bash.exe</code> と <code>pwsh.exe</code> があるフォルダを追加</li>
</ul>
<h1 id="手順">手順</h1>
<p>詳細はあとで記載します。</p>
<h2 id="1-Git-for-Windows-と-PowerShell7-をインストール">1. Git for <a class="keyword" href="http://d.hatena.ne.jp/keyword/Windows">Windows</a> と PowerShell7 をインストール</h2>
<p>以前紹介した記事を参考にGit for <a class="keyword" href="http://d.hatena.ne.jp/keyword/Windows">Windows</a>とPowerShell7をインストールしてください</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftsgcpp.hateblo.jp%2Fentry%2Finstallation-git-for-windows" title="【Git, PowerShell】 Git for Windowsをコマンドラインからインストールする方法 - すぎしーのXRと3DCG" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://tsgcpp.hateblo.jp/entry/installation-git-for-windows">tsgcpp.hateblo.jp</a></cite></p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftsgcpp.hateblo.jp%2Fentry%2Finstallation-powershell7" title="pwshをコマンドラインでインストールしてPATHを通す方法 - すぎしーのXRと3DCG" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://tsgcpp.hateblo.jp/entry/installation-powershell7">tsgcpp.hateblo.jp</a></cite></p>
<h2 id="2-システム環境変数のPATHに登録">2. システム<a class="keyword" href="http://d.hatena.ne.jp/keyword/%B4%C4%B6%AD%CA%D1%BF%F4">環境変数</a>のPATHに登録</h2>
<p>Git for <a class="keyword" href="http://d.hatena.ne.jp/keyword/Windows">Windows</a>とPowerShell7をインストールしたフォルダをPATHに登録してください。<br />
PATHを通すps1ファイル (<code>set-git-path.ps1</code>, <code>set-powershell-path.ps1</code>)も上記の記事内にて記載しています。</p>
<p>システム<a class="keyword" href="http://d.hatena.ne.jp/keyword/%B4%C4%B6%AD%CA%D1%BF%F4">環境変数</a>に直接以下を追加しても大丈夫です(標準のインストールフォルダの場合)。</p>
<pre class="code" data-lang="" data-unlink>C:\Program Files\Git\cmd;C:\Program Files\Git\bin;C:\Program Files\PowerShell\7</pre>
<h2 id="3-self-hostedのrunnerプロセスを再起動">3. self-hostedのrunnerプロセスを再起動</h2>
<p><a class="keyword" href="http://d.hatena.ne.jp/keyword/%B4%C4%B6%AD%CA%D1%BF%F4">環境変数</a>をプロセスに反映させるために再起動してください。<br />
再起動方法は以下の公式ドキュメントを参考にしてください。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdocs.github.com%2Fen%2Factions%2Fhosting-your-own-runners%2Fconfiguring-the-self-hosted-runner-application-as-a-service" title="Configuring the self-hosted runner application as a service - GitHub Docs" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://docs.github.com/en/actions/hosting-your-own-runners/configuring-the-self-hosted-runner-application-as-a-service">docs.github.com</a></cite></p>
<p>もし、<a class="keyword" href="http://d.hatena.ne.jp/keyword/PowerShell">PowerShell</a>上で<a class="keyword" href="http://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Actionsのself-hostedの <code>.\run.cmd</code> で起動している場合は<a class="keyword" href="http://d.hatena.ne.jp/keyword/PowerShell">PowerShell</a>自体も再起動してください。</p>
<p>手順は以上となります!</p>
<hr />
<h1 id="詳細">詳細</h1>
<h2 id="確認用のworkflow">確認用のworkflow</h2>
<p>以下のworkflowを使って確認します。</p>
<pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synIdentifier">name</span><span class="synSpecial">:</span> Windows Shell Check
<span class="synIdentifier">on</span><span class="synSpecial">:</span> workflow_dispatch
<span class="synIdentifier">jobs</span><span class="synSpecial">:</span>
<span class="synIdentifier">shell-check-all</span><span class="synSpecial">:</span>
<span class="synIdentifier">runs-on</span><span class="synSpecial">:</span> self-hosted
<span class="synIdentifier">steps</span><span class="synSpecial">:</span>
<span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Check cmd
<span class="synIdentifier">shell</span><span class="synSpecial">:</span> cmd
<span class="synIdentifier">run</span><span class="synSpecial">:</span> |
echo cmd ok
<span class="synIdentifier">if</span><span class="synSpecial">:</span> ${{ always() }}
<span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Check pwsh
<span class="synIdentifier">shell</span><span class="synSpecial">:</span> pwsh
<span class="synIdentifier">run</span><span class="synSpecial">:</span> |
echo pwsh ok
<span class="synIdentifier">if</span><span class="synSpecial">:</span> ${{ always() }}
<span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Check bash
<span class="synIdentifier">shell</span><span class="synSpecial">:</span> bash
<span class="synIdentifier">run</span><span class="synSpecial">:</span> |
echo bash ok
<span class="synIdentifier">if</span><span class="synSpecial">:</span> ${{ always() }}
</pre>
<h2 id="インストール前">インストール前</h2>
<p>以下のように <code>cmd</code> のみ使用可能で、それ以外は使用できません。。。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20220519/20220519232451.png" width="983" height="585" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<h2 id="インストール後">インストール後</h2>
<p>問題なさそうですね。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20220519/20220519233812.png" width="700" height="524" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<h1 id="活用例">活用例</h1>
<p>せっかくなので、活用例を紹介します。</p>
<h2 id="日付を文字列として取得">日付を文字列として取得</h2>
<p><a class="keyword" href="http://d.hatena.ne.jp/keyword/Bash">Bash</a>の<code>date</code>コマンドを使って取得します。</p>
<pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synIdentifier">name</span><span class="synSpecial">:</span> Get Date String
<span class="synIdentifier">on</span><span class="synSpecial">:</span> workflow_dispatch
<span class="synIdentifier">jobs</span><span class="synSpecial">:</span>
<span class="synIdentifier">get-date-string</span><span class="synSpecial">:</span>
<span class="synIdentifier">runs-on</span><span class="synSpecial">:</span> self-hosted
<span class="synIdentifier">steps</span><span class="synSpecial">:</span>
<span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Get Date String
<span class="synIdentifier">run</span><span class="synSpecial">:</span> echo <span class="synConstant">"::set-output name=DATE::$(date +%Y%m%d%H%M)"</span>
<span class="synIdentifier">shell</span><span class="synSpecial">:</span> bash
<span class="synIdentifier">id</span><span class="synSpecial">:</span> get-date-string
<span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Use Date String
<span class="synIdentifier">run</span><span class="synSpecial">:</span> echo <span class="synConstant">"${{ steps.get-date-string.outputs.DATE }}"</span>
</pre>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20220520/20220520000840.png" width="924" height="381" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p><a class="keyword" href="http://d.hatena.ne.jp/keyword/Bash">Bash</a>は手軽に文字列を生成可能なコマンドがたくさんあります!</p>
<h2 id="レジストリから値を取得"><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EC%A5%B8%A5%B9%A5%C8%A5%EA">レジストリ</a>から値を取得</h2>
<p>以下は<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EC%A5%B8%A5%B9%A5%C8%A5%EA">レジストリ</a>に登録されたUnity 2021.3.2f1 のインストールフォルダを取得する方法です。</p>
<pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synIdentifier">name</span><span class="synSpecial">:</span> Get Unity Folder
<span class="synIdentifier">on</span><span class="synSpecial">:</span> workflow_dispatch
<span class="synIdentifier">jobs</span><span class="synSpecial">:</span>
<span class="synIdentifier">get-unity-folder</span><span class="synSpecial">:</span>
<span class="synIdentifier">runs-on</span><span class="synSpecial">:</span> self-hosted
<span class="synIdentifier">steps</span><span class="synSpecial">:</span>
<span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Get Unity Folder
<span class="synIdentifier">run</span><span class="synSpecial">:</span> Write-Host -NoNewline ('::set-output name=UNITY_FOLDER::' + (Get-ItemProperty <span class="synConstant">'HKCU:SOFTWARE\Unity Technologies\Installer\Unity 2021.3.2f1'</span>).'Location x64')
<span class="synIdentifier">shell</span><span class="synSpecial">:</span> pwsh
<span class="synIdentifier">id</span><span class="synSpecial">:</span> get-unity-folder
<span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Use Unity Folder
<span class="synIdentifier">run</span><span class="synSpecial">:</span> echo <span class="synConstant">"${{ steps.get-unity-folder.outputs.UNITY_FOLDER }}"</span>
</pre>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20220520/20220520000822.png" width="1200" height="302" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p><a class="keyword" href="http://d.hatena.ne.jp/keyword/Windows">Windows</a>のシステムに関わる情報を取得したい場合は<a class="keyword" href="http://d.hatena.ne.jp/keyword/PowerShell">PowerShell</a>のほうが適している気がします。</p>
<h1 id="余談GitHub-Actionsで提供されているWindowsのRunnerでは標準でpwshとbashが使用可能">余談、<a class="keyword" href="http://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Actionsで提供されている<a class="keyword" href="http://d.hatena.ne.jp/keyword/Windows">Windows</a>のRunnerでは標準でpwshと<a class="keyword" href="http://d.hatena.ne.jp/keyword/bash">bash</a>が使用可能</h1>
<p><a class="keyword" href="http://d.hatena.ne.jp/keyword/PowerShell">PowerShell</a>はともかく、<a class="keyword" href="http://d.hatena.ne.jp/keyword/Bash">Bash</a>も使用できるように用意してくれていたりします。</p>
<p>ドキュメントでも以下のように記載されています。<br />
<a class="keyword" href="http://d.hatena.ne.jp/keyword/GitHub">GitHub</a> ActionsもGit for <a class="keyword" href="http://d.hatena.ne.jp/keyword/Windows">Windows</a>を使用しているようですね。</p>
<pre class="code" data-lang="" data-unlink>When specifying a bash shell on Windows, the bash shell included with Git for Windows is used.</pre>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdocs.github.com%2Fen%2Factions%2Fusing-workflows%2Fworkflow-syntax-for-github-actions%23jobsjob_idstepsshell" title="Workflow syntax for GitHub Actions - GitHub Docs" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsshell">docs.github.com</a></cite></p>
<h2 id="windows-latestでpwshとbashが使用可能なことを確認"><a class="keyword" href="http://d.hatena.ne.jp/keyword/windows">windows</a>-latestでpwshと<a class="keyword" href="http://d.hatena.ne.jp/keyword/bash">bash</a>が使用可能なことを確認</h2>
<p>先程のworkflowのrunnerに<a class="keyword" href="http://d.hatena.ne.jp/keyword/windows">windows</a>-latestを指定して確認してみましょう。</p>
<pre class="code" data-lang="" data-unlink>jobs:
shell-check-all:
strategy:
matrix:
runner: [self-hosted, windows-latest]
runs-on: ${{ matrix.runner }}
steps:</pre>
<p>結果は以下のように問題なく使用可能です!</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20220519/20220519234406.png" width="777" height="538" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<h1 id="雑感">雑感</h1>
<p>最近Git for <a class="keyword" href="http://d.hatena.ne.jp/keyword/Windows">Windows</a>とPowerShell7のインストール方法を紹介した本当の目的はこちらだったりします。</p>
<p>私個人では開発基盤を用意する機会が増えているんですが、<br />
pwshと<a class="keyword" href="http://d.hatena.ne.jp/keyword/bash">bash</a> (特に<a class="keyword" href="http://d.hatena.ne.jp/keyword/bash">bash</a>!)を<a class="keyword" href="http://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Actionsで使えるようにしておくと何かと捗ったり、self-hostedと<a class="keyword" href="http://d.hatena.ne.jp/keyword/windows">windows</a>-latestでworkflowの互換性が確保できたりします。</p>
<p>わざわざ<a class="keyword" href="http://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Marketplaceや独自アクションを使用しなくてもできることが増えると思いますので、ぜひご活用ください!</p>
<p>それでは~</p>
tsgcpp
pwshをコマンドラインでインストールしてPATHを通す方法
hatenablog://entry/13574176438093330781
2022-05-18T00:50:18+09:00
2022-05-18T00:50:18+09:00 前回に続いて、今回はWindowsのコマンドラインで PowerShell7 をインストールする方法を紹介します。
PowerShell5はWindowsに標準で搭載されていますが、`pwsh` コマンドは搭載されていませんが、
PowerShell7をインストールすることで `pwsh`コマンドとして使用可能になります。
そんなPowerShell7をコマンドライン経由でインストールしPATHを登録する方法を紹介します。
<ul class="table-of-contents">
<li><a href="#概要">概要</a></li>
<li><a href="#動作環境">動作環境</a></li>
<li><a href="#変更履歴">変更履歴</a></li>
<li><a href="#注意事項">注意事項</a></li>
<li><a href="#インストール方法">インストール方法</a><ul>
<li><a href="#Gitからps1ファイルをクローン">Gitからps1ファイルをクローン</a></li>
<li><a href="#PowerShellを管理者権限で開く">PowerShellを管理者権限で開く</a></li>
<li><a href="#PowerShellでクローンしたWindowsInfrastructureExample上に移動">PowerShellでクローンしたWindowsInfrastructureExample上に移動</a></li>
<li><a href="#PowerShell-7xx-win-x64msi-を配置">PowerShell-7.x.x-win-x64.msi を配置</a><ul>
<li><a href="#コマンドでダウンロードする場合">コマンドでダウンロードする場合</a></li>
<li><a href="#ブラウザでダウンロードする場合">ブラウザでダウンロードする場合</a></li>
</ul>
</li>
<li><a href="#ps1の実行を許可">ps1の実行を許可</a></li>
<li><a href="#install-powershellps1-を実行">install-powershell.ps1 を実行</a></li>
<li><a href="#インストールの確認-コマンドラインの場合">インストールの確認 (コマンドラインの場合)</a></li>
<li><a href="#任意PATHの追加">(任意)PATHの追加</a></li>
</ul>
</li>
<li><a href="#スクリプト解説">スクリプト解説</a><ul>
<li><a href="#msiexecによるインストール">msiexecによるインストール</a></li>
<li><a href="#PowerShell-7xx-win-x64msi-のインストールオプション">PowerShell-7.x.x-win-x64.msi のインストールオプション</a><ul>
<li><a href="#INSTALLFOLDER">INSTALLFOLDER</a></li>
<li><a href="#ADD_PATH0">ADD_PATH=0</a></li>
<li><a href="#USE_MU0-ENABLE_MU0">USE_MU=0, ENABLE_MU=0</a></li>
</ul>
</li>
</ul>
</li>
<li><a href="#雑感">雑感</a></li>
</ul>
<h1 id="概要">概要</h1>
<p><a href="https://tsgcpp.hateblo.jp/entry/installation-git-for-windows">前回</a> に続いて、今回は<a class="keyword" href="http://d.hatena.ne.jp/keyword/Windows">Windows</a>の<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%DE%A5%F3%A5%C9%A5%E9%A5%A4%A5%F3">コマンドライン</a>で PowerShell7 をインストールする方法を紹介します。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2FPowerShell%2FPowerShell" title="GitHub - PowerShell/PowerShell: PowerShell for every system!" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://github.com/PowerShell/PowerShell">github.com</a></cite></p>
<p>PowerShell5は<a class="keyword" href="http://d.hatena.ne.jp/keyword/Windows">Windows</a>に標準で搭載されていますが、<code>pwsh</code> コマンドは実行できません。
そこでPowerShell7を別途インストールすることで <code>pwsh</code>コマンドとして使用可能になります。</p>
<p>そんなPowerShell7を<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%DE%A5%F3%A5%C9%A5%E9%A5%A4%A5%F3">コマンドライン</a>経由でインストールしPATHを登録する方法を紹介します。</p>
<h1 id="動作環境">動作環境</h1>
<ul>
<li><a class="keyword" href="http://d.hatena.ne.jp/keyword/Windows">Windows</a> 10
<ul>
<li>筆者はPro版ですが、Homeでも問題ないと思います</li>
</ul>
</li>
</ul>
<h1 id="変更履歴">変更履歴</h1>
<ul>
<li>2022/05/18
<ul>
<li>タイトルを「【<a class="keyword" href="http://d.hatena.ne.jp/keyword/PowerShell">PowerShell</a>】 PowerShell7を<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%DE%A5%F3%A5%C9%A5%E9%A5%A4%A5%F3">コマンドライン</a>からインストールして、pwshコマンドにPATHを通す」→「pwshを<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%DE%A5%F3%A5%C9%A5%E9%A5%A4%A5%F3">コマンドライン</a>でインストールしてPATHを通す方法」に変更</li>
</ul>
</li>
<li>2023/02/05
<ul>
<li>「install-<a class="keyword" href="http://d.hatena.ne.jp/keyword/powershell">powershell</a>.ps1 を実行」を <code>PowerShell -ExecutionPolicy Bypass</code> を使ったコマンドに修正</li>
</ul>
</li>
</ul>
<h1 id="注意事項">注意事項</h1>
<ul>
<li><span style="color: #ff0000"><strong>以下を試す場合は自己責任でお願いします!</strong></span></li>
<li><span style="color: #ff0000"><strong>以下の処理は管理者権限で実行しますが、当方は事故や故障などの一切の責任を負いません!</strong></span></li>
<li><span style="color: #ff0000"><strong><a class="keyword" href="http://d.hatena.ne.jp/keyword/%B4%C4%B6%AD%CA%D1%BF%F4">環境変数</a> PATH を変更するため、変更前のPATHを記録しておくことを推奨!</strong></span></li>
</ul>
<p>上記の件、ご留意ください</p>
<h1 id="インストール方法">インストール方法</h1>
<p>※<a href="https://tsgcpp.hateblo.jp/entry/installation-git-for-windows">前回の記事 (Git for Windowsをコマンドラインからインストールする方法)</a> と流れはほとんど同じです</p>
<h2 id="Gitからps1ファイルをクローン">Gitからps1ファイルをクローン</h2>
<ul>
<li>ps1ファイル(<a class="keyword" href="http://d.hatena.ne.jp/keyword/PowerShell">PowerShell</a><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%EA%A5%D7%A5%C8">スクリプト</a>)を以下の<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>からクローンする</li>
</ul>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Ftsgcpp%2FWindowsInfrastructureExample" title="GitHub - tsgcpp/WindowsInfrastructureExample: An infrastructure example in Windows for CI" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://github.com/tsgcpp/WindowsInfrastructureExample">github.com</a></cite></p>
<h2 id="PowerShellを管理者権限で開く"><a class="keyword" href="http://d.hatena.ne.jp/keyword/PowerShell">PowerShell</a>を管理者権限で開く</h2>
<p>※開き方は好きなやり方で問題ありません</p>
<p><a class="keyword" href="http://d.hatena.ne.jp/keyword/Windows">Windows</a> 10標準搭載のPowerShell5を使用</p>
<ul>
<li><code>Windows</code> キーを押して、「<a class="keyword" href="http://d.hatena.ne.jp/keyword/powershell">powershell</a>」を検索</li>
<li>「管理者として実行する」をクリック</li>
</ul>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20220514/20220514201533.jpg" width="777" height="636" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<h2 id="PowerShellでクローンしたWindowsInfrastructureExample上に移動"><a class="keyword" href="http://d.hatena.ne.jp/keyword/PowerShell">PowerShell</a>でクローンしたWindowsInfrastructureExample上に移動</h2>
<pre class="code pwsh" data-lang="pwsh" data-unlink>cd <path>\WindowsInfrastructureExample</pre>
<h2 id="PowerShell-7xx-win-x64msi-を配置"><a class="keyword" href="http://d.hatena.ne.jp/keyword/PowerShell">PowerShell</a>-7.x.x-win-x64.<a class="keyword" href="http://d.hatena.ne.jp/keyword/msi">msi</a> を配置</h2>
<h3 id="コマンドでダウンロードする場合">コマンドでダウンロードする場合</h3>
<pre class="code pwsh" data-lang="pwsh" data-unlink>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</pre>
<p>※インフラ構築の<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%EA%A5%D7%A5%C8">スクリプト</a>を書く場合は、このコマンド自体をps1<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%EA%A5%D7%A5%C8">スクリプト</a>に組み込んでも良い</p>
<h3 id="ブラウザでダウンロードする場合">ブラウザでダウンロードする場合</h3>
<p>以下より <code>PowerShell-7.x.x-win-x64.msi</code>をダウンロードし、<code>WindowsInfrastructureExample</code> フォルダ以下に配置</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2FPowerShell%2FPowerShell%2Freleases" title="Releases · PowerShell/PowerShell" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://github.com/PowerShell/PowerShell/releases">github.com</a></cite></p>
<h2 id="ps1の実行を許可">ps1の実行を許可</h2>
<pre class="code pwsh" data-lang="pwsh" data-unlink>Set-ExecutionPolicy RemoteSigned -Scope Process</pre>
<h2 id="install-powershellps1-を実行">install-<a class="keyword" href="http://d.hatena.ne.jp/keyword/powershell">powershell</a>.ps1 を実行</h2>
<pre class="code pwsh" data-lang="pwsh" data-unlink>> PowerShell -ExecutionPolicy Bypass .\install-powershell.ps1</pre>
<p>エラーログが出なければ問題なく PowerShell7 のインストール完了</p>
<h2 id="インストールの確認-コマンドラインの場合">インストールの確認 (<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%DE%A5%F3%A5%C9%A5%E9%A5%A4%A5%F3">コマンドライン</a>の場合)</h2>
<ul>
<li><code>Dir 'C:\Program Files\PowerShell\7\pwsh.exe'</code>でエラーが出なければインストール完了</li>
</ul>
<pre class="code" data-lang="" data-unlink>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</pre>
<h2 id="任意PATHの追加">(任意)PATHの追加</h2>
<p><span style="color: #ff0000"><strong>ローカルPCの<a class="keyword" href="http://d.hatena.ne.jp/keyword/%B4%C4%B6%AD%CA%D1%BF%F4">環境変数</a> PATH が変更されます!ご注意ください!</strong></span></p>
<pre class="code pwsh" data-lang="pwsh" data-unlink>PowerShell -ExecutionPolicy Bypass .\set-powershell-path.ps1</pre>
<p>以上でインストール及び設定は完了です。</p>
<hr />
<h1 id="スクリプト解説"><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%EA%A5%D7%A5%C8">スクリプト</a>解説</h1>
<p><a href="https://github.com/tsgcpp/WindowsInfrastructureExample/blob/main/install-powershell.ps1">install-powershell.ps1</a></p>
<h2 id="msiexecによるインストール">msiexecによるインストール</h2>
<ul>
<li><a class="keyword" href="http://d.hatena.ne.jp/keyword/msi">msi</a><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%C8%A1%BC%A5%E9">インストーラ</a>ーは <code>msiexec.exe</code>を使用することで、<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%DE%A5%F3%A5%C9%A5%E9%A5%A4%A5%F3">コマンドライン</a>からインストール可能です。</li>
</ul>
<h2 id="PowerShell-7xx-win-x64msi-のインストールオプション"><a class="keyword" href="http://d.hatena.ne.jp/keyword/PowerShell">PowerShell</a>-7.x.x-win-x64.<a class="keyword" href="http://d.hatena.ne.jp/keyword/msi">msi</a> のインストールオプション</h2>
<p>詳細は以下のページに記載されています</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdocs.microsoft.com%2Fen-us%2Fpowershell%2Fscripting%2Finstall%2Finstalling-powershell-on-windows%3Fview%3Dpowershell-7.2" title="Installing PowerShell on Windows - PowerShell" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://docs.microsoft.com/en-us/powershell/scripting/install/installing-powershell-on-windows?view=powershell-7.2">docs.microsoft.com</a></cite></p>
<h3 id="INSTALLFOLDER">INSTALLFOLDER</h3>
<p>名前の通りインストール先のフォルダを指定。<br/>
以下のように指定すると良いです。</p>
<pre class="code pwsh" data-lang="pwsh" data-unlink>$InstallFolder = 'C:\Program Files\PowerShell'
(略)
msiexec.exe `
...
INSTALLFOLDER=`"$InstallFolder`" `
...</pre>
<p>ちなみに、<code>C:\Program Files\PowerShell</code> を指定した場合、<code>pwsh</code>は<code>C:\Program Files\PowerShell\7\pwsh.exe</code>に配置されます。</p>
<p>また、<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%EA%A5%D7%A5%C8">スクリプト</a>内でフォルダ名を<code>`"XXX`"</code> のように囲むのは不思議な感じがしますが、これで問題ありません。</p>
<p><span style="color: #999999"><a class="keyword" href="http://d.hatena.ne.jp/keyword/Windows">Windows</a>の<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%DE%A5%F3%A5%C9%A5%E9%A5%A4%A5%F3">コマンドライン</a>はスペースの扱いがめんどくさいですね。。。それなのに "Program Files" はスペースが有るという。。。</span></p>
<h3 id="ADD_PATH0">ADD_PATH=0</h3>
<p>PATHに追加するかのオプションです。<code>ADD_PATH=1</code> にすればパスを通してくれます。<br/>
今回は<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%DE%A5%F3%A5%C9%A5%E9%A5%A4%A5%F3">コマンドライン</a>で諸々完結させたかったので0指定(登録しない)にしています。</p>
<h3 id="USE_MU0-ENABLE_MU0">USE_MU=0, ENABLE_MU=0</h3>
<p>いわゆる<a class="keyword" href="http://d.hatena.ne.jp/keyword/Microsoft%20Update">Microsoft Update</a>による自動更新の設定です。<br/>
毎回<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%DE%A5%F3%A5%C9%A5%E9%A5%A4%A5%F3">コマンドライン</a>で更新することを想定し、オフにしています。</p>
<p>解説は以上です!</p>
<hr />
<h1 id="雑感">雑感</h1>
<p>Git for <a class="keyword" href="http://d.hatena.ne.jp/keyword/Windows">Windows</a>, PowerShell7 のインストール方法を紹介したのは、<code>bash</code>, <code>pwsh</code> をインストールするためだったりします。</p>
<p>次回はこの2つを活用したCIについてお話します!<br/>
もしかしたらピンと来た方がいらっしゃるかもですね。</p>
<p>さらにその次は絶対にUnity + XR関連にします(そっちのほうが断然楽しいし、何よりブログのタイトル的に。。。)</p>
<p>PATHを変更するような手順の紹介は、試した人の環境を壊しかねないので結構神経を使いますね。。。</p>
<p>それでは~</p>
tsgcpp
【Windows, PowerShell】SETX vs. [Environment]::SetEnvironmentVariable
hatenablog://entry/13574176438093022687
2022-05-17T00:47:25+09:00
2022-05-17T00:47:25+09:00 `SETX` と `[Environment]::SetEnvironmentVariable` の違いに躓いたので調査したことを共有します!
どちらもWindowsで環境変数を設定する手段ですが、細かいところで違いがありました。
知らずに使ってしまうと最悪Windows上でアプリが起動しなくなるかもしれないので、
よかったら参考にしてください。
<ul class="table-of-contents">
<li><a href="#概要">概要</a></li>
<li><a href="#結論">結論</a></li>
<li><a href="#違い">違い</a><ul>
<li><a href="#設定時のレジストリキーの種類が異なる">設定時のレジストリキーの種類が異なる</a><ul>
<li><a href="#SETX-の場合は-REG_SZ-と-REG_EXPAND_SZ-から適した方">SETX の場合は REG_SZ と REG_EXPAND_SZ から適した方</a></li>
<li><a href="#EnvironmentSetEnvironmentVariable-は-REG_SZ-固定">[Environment]::SetEnvironmentVariable は REG_SZ 固定</a></li>
<li><a href="#変数が展開されていないことの確認">変数が展開されていないことの確認</a></li>
</ul>
</li>
<li><a href="#バグ-SETXの場合-末尾にダブルクォーテーションが挿入されるパターンがある">(バグ?) SETXの場合、 末尾にダブルクォーテーションが挿入されるパターンがある</a></li>
</ul>
</li>
<li><a href="#簡易解説">簡易解説</a><ul>
<li><a href="#環境変数の設定方法">環境変数の設定方法</a><ul>
<li><a href="#ユーザー環境変数の場合">ユーザー環境変数の場合</a></li>
<li><a href="#システム環境変数の場合">システム環境変数の場合</a></li>
</ul>
</li>
<li><a href="#環境変数の確認方法">環境変数の確認方法</a><ul>
<li><a href="#コマンドライン-混合">コマンドライン (混合)</a></li>
<li><a href="#コマンドライン-個別">コマンドライン (個別)</a></li>
<li><a href="#レジストリ-エディタ">レジストリ エディタ</a></li>
<li><a href="#システムのプロパティ---環境変数">システムのプロパティ -> 環境変数</a></li>
</ul>
</li>
</ul>
</li>
<li><a href="#余談PATHのREG_SZ登録をREG_EXPAND_SZに切り替える方法">余談、PATHのREG_SZ登録をREG_EXPAND_SZに切り替える方法</a></li>
<li><a href="#環境変数の補足">環境変数の補足</a></li>
<li><a href="#雑感">雑感</a></li>
</ul>
<h1 id="概要">概要</h1>
<p><code>SETX</code> と <code>[Environment]::SetEnvironmentVariable</code> の違いに躓いたので調査したことを共有します!</p>
<p>どちらも<a class="keyword" href="http://d.hatena.ne.jp/keyword/Windows">Windows</a>で<a class="keyword" href="http://d.hatena.ne.jp/keyword/%B4%C4%B6%AD%CA%D1%BF%F4">環境変数</a>を設定する手段ですが、細かいところで違いがありました。</p>
<p>知らずに使ってしまうと最悪<a class="keyword" href="http://d.hatena.ne.jp/keyword/Windows">Windows</a>上でアプリが起動しなくなるかもしれないので、
よかったら参考にしてください。</p>
<p><strong><span style="color: #ff0000">以下の説明はすべては<u><a class="keyword" href="http://d.hatena.ne.jp/keyword/PowerShell">PowerShell</a></u>での実行を想定</span></strong></p>
<h1 id="結論">結論</h1>
<p>説明が長くなったので、先に結論です。</p>
<ul>
<li><strong>展開(%...%)を使用するなら、<code>SETX</code> 使って、<a class="keyword" href="http://d.hatena.ne.jp/keyword/%B4%C4%B6%AD%CA%D1%BF%F4">環境変数</a>の末尾に <code>/</code> を入れない</strong>
<ul>
<li>x: <code>C:\Program Files\Git\bin;%SystemRoot%\System32\WindowsPowerShell\v1.0\</code></li>
<li>o: <code>C:\Program Files\Git\bin;%SystemRoot%\System32\WindowsPowerShell\v1.0</code></li>
</ul>
</li>
<li><strong>展開(%...%)を使用しないなら、<code>[Environment]::SetEnvironmentVariable</code>でも可</strong></li>
</ul>
<hr />
<p>詳しくは以下</p>
<h1 id="違い">違い</h1>
<h2 id="設定時のレジストリキーの種類が異なる">設定時の<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EC%A5%B8%A5%B9%A5%C8%A5%EA">レジストリ</a>キーの種類が異なる</h2>
<p>試しに<a class="keyword" href="http://d.hatena.ne.jp/keyword/%B4%C4%B6%AD%CA%D1%BF%F4">環境変数</a> <code>Foo</code> に <code>%SystemRoot%\System32</code> を登録してみます。</p>
<h3 id="SETX-の場合は-REG_SZ-と-REG_EXPAND_SZ-から適した方"><code>SETX</code> の場合は <code>REG_SZ</code> と <code>REG_EXPAND_SZ</code> から適した方</h3>
<ul>
<li><code>SETX Foo 'C:\Windows\System32'</code> -> <code>REG_SZ</code></li>
<li><code>SETX Foo '%SystemRoot%\System32'</code> -> <code>REG_EXPAND_SZ</code></li>
</ul>
<p>つまり、<code>SETX</code> は<code>%SystemRoot%</code> のような<a class="keyword" href="http://d.hatena.ne.jp/keyword/%B4%C4%B6%AD%CA%D1%BF%F4">環境変数</a>を展開するような書式を検知したら<code>REG_EXPAND_SZ</code>として登録してくれます。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20220516/20220516233337.png" width="360" height="19" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<h3 id="EnvironmentSetEnvironmentVariable-は-REG_SZ-固定"><code>[Environment]::SetEnvironmentVariable</code> は <code>REG_SZ</code> 固定</h3>
<p>つまり<code>%SystemRoot%</code>があろうがなかろうが <code>REG_SZ</code> 固定になります。</p>
<p>以下は <code>[Environment]::SetEnvironmentVariable('Foo', '%SystemRoot%', [System.EnvironmentVariableTarget]::User)</code> の場合です。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20220516/20220516233506.png" width="340" height="23" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p><span style="color: #ff0000"><strong>REG_SZの場合、変数が展開されません!これは PATH を設定するときは非常にまずいです!</strong></span></p>
<p>これに関しては <a class="keyword" href="http://d.hatena.ne.jp/keyword/dotnet">dotnet</a> の<a class="keyword" href="http://d.hatena.ne.jp/keyword/GitHub">GitHub</a>のIssueでずっと議論になっているようです。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fdotnet%2Fruntime%2Fissues%2F1442" title="Environment.SetEnvironmentVariable() messing up system variables and registry keys · Issue #1442 · dotnet/runtime" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://github.com/dotnet/runtime/issues/1442">github.com</a></cite></p>
<h3 id="変数が展開されていないことの確認">変数が展開されていないことの確認</h3>
<p><span style="color: #ff0000">以下のコマンドを実施する場合は <code>echo ([Environment]::GetEnvironmentVariable('PATH', [System.EnvironmentVariableTarget]::Machine))</code> などで既存の変数を控えて最後に戻すようにしてください!</span><br />
<strong><span style="color: #ff0000">特に PATH は元に戻さないとアプリがおかしくなる可能性があります!</span></strong></p>
<p> <code>[Environment]::SetEnvironmentVariable('PATH', '%SystemRoot%\System32', [System.EnvironmentVariableTarget]::Machine)</code> 実行して、<a class="keyword" href="http://d.hatena.ne.jp/keyword/PowerShell">PowerShell</a>を再起動してPATHを確認すると、以下のように展開されていないことがわかります。<br />
そして、<u>もちろんパスも通らないので、PATHを使用しているアプリがおかしくなります!</u></p>
<pre class="code pwsh" data-lang="pwsh" data-unlink>> echo $Env:PATH
%SystemRoot%\System32;...</pre>
<p><code>SETX /M PATH '%SystemRoot%\System32'</code> であれば <code>REG_EXPAND_SZ</code>で登録されるので展開されたものがPATHとして登録されます。</p>
<pre class="code pwsh" data-lang="pwsh" data-unlink>> echo $Env:PATH
C:\WINDOWS\System32;...</pre>
<h2 id="バグ-SETXの場合-末尾にダブルクォーテーションが挿入されるパターンがある">(バグ?) SETXの場合、 末尾にダブルクォーテーションが挿入されるパターンがある</h2>
<p>条件は以下で発生することを確認しています</p>
<ul>
<li>値の中にスペースが有る</li>
<li>値の最後が <code>/</code></li>
</ul>
<p>例: <code>C:\Program Files\Git\bin;C:\WINDOWS\System32\WindowsPowerShell\v1.0\</code></p>
<p><code>Program Files</code>にスペース、<code>v1.0\</code>で末尾がバックスラッシュみたいな場合におかしくなります。(実際に起こった事象を例にしています)。</p>
<pre class="code pwsh" data-lang="pwsh" data-unlink>> SETX /M PATH 'C:\Program Files\Git\bin;C:\WINDOWS\System32\WindowsPowerShell\v1.0\'
成功: 指定した値は保存されました。
> echo ([Environment]::GetEnvironmentVariable('PATH', [System.EnvironmentVariableTarget]::Machine))
C:\Program Files\Git\bin;C:\WINDOWS\System32\WindowsPowerShell\v1.0"</pre>
<p>最後に <code>"</code> がなぜが挿入されます。。。 (<code>SETX</code>のバグじゃないかと思っています)</p>
<p>ちなみに、<span style="color: #ff0000">PATHに登録する場合は <code>"</code> がついた方のパス(上記では <code>C:\WINDOWS\System32\WindowsPowerShell\v1.0</code> )がPATHから除外されてしまいます。</span></p>
<p><code>;</code>区切りのパスの末尾には <code>/</code> は不要なので、基本的には最後は <code>/</code> にしないほうが安全だと思います。<br />
結論で述べた「<a class="keyword" href="http://d.hatena.ne.jp/keyword/%B4%C4%B6%AD%CA%D1%BF%F4">環境変数</a>の末尾に <code>/</code> を入れない」はこれが理由になります。</p>
<p>ご注意ください!</p>
<hr />
<h1 id="簡易解説">簡易解説</h1>
<p>せっかくなので<code>SETX</code>と<code>[Environment]::SetEnvironmentVariable</code>を少し解説します。</p>
<h2 id="環境変数の設定方法"><a class="keyword" href="http://d.hatena.ne.jp/keyword/%B4%C4%B6%AD%CA%D1%BF%F4">環境変数</a>の設定方法</h2>
<p>例えば <code>Foo</code> という<a class="keyword" href="http://d.hatena.ne.jp/keyword/%B4%C4%B6%AD%CA%D1%BF%F4">環境変数</a>に値 <code>Bar;Baz</code> を入れる場合は以下のようにコールします。</p>
<h3 id="ユーザー環境変数の場合">ユーザー<a class="keyword" href="http://d.hatena.ne.jp/keyword/%B4%C4%B6%AD%CA%D1%BF%F4">環境変数</a>の場合</h3>
<pre class="code pwsh" data-lang="pwsh" data-unlink>SETX Foo 'Bar;Baz'</pre>
<pre class="code pwsh" data-lang="pwsh" data-unlink>[Environment]::SetEnvironmentVariable('Foo', 'Bar;Baz', [System.EnvironmentVariableTarget]::User)</pre>
<h3 id="システム環境変数の場合">システム<a class="keyword" href="http://d.hatena.ne.jp/keyword/%B4%C4%B6%AD%CA%D1%BF%F4">環境変数</a>の場合</h3>
<p>システム<a class="keyword" href="http://d.hatena.ne.jp/keyword/%B4%C4%B6%AD%CA%D1%BF%F4">環境変数</a>とは対象のマシンのユーザー全体で共通となる<a class="keyword" href="http://d.hatena.ne.jp/keyword/%B4%C4%B6%AD%CA%D1%BF%F4">環境変数</a>のことです。<br />
実行には管理者権限が必要となります。</p>
<pre class="code pwsh" data-lang="pwsh" data-unlink>SETX /M Foo 'Bar;Baz'</pre>
<pre class="code pwsh" data-lang="pwsh" data-unlink>[Environment]::SetEnvironmentVariable('Foo', 'Bar;Baz', [System.EnvironmentVariableTarget]::Machine)</pre>
<h2 id="環境変数の確認方法"><a class="keyword" href="http://d.hatena.ne.jp/keyword/%B4%C4%B6%AD%CA%D1%BF%F4">環境変数</a>の確認方法</h2>
<h3 id="コマンドライン-混合"><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%DE%A5%F3%A5%C9%A5%E9%A5%A4%A5%F3">コマンドライン</a> (混合)</h3>
<p>※<a class="keyword" href="http://d.hatena.ne.jp/keyword/%B4%C4%B6%AD%CA%D1%BF%F4">環境変数</a>設定後は<a class="keyword" href="http://d.hatena.ne.jp/keyword/PowerShell">PowerShell</a>の再起動が必要</p>
<pre class="code pwsh" data-lang="pwsh" data-unlink>echo $Env:Foo</pre>
<h3 id="コマンドライン-個別"><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%DE%A5%F3%A5%C9%A5%E9%A5%A4%A5%F3">コマンドライン</a> (個別)</h3>
<pre class="code pwsh" data-lang="pwsh" data-unlink># ユーザー環境変数の場合
echo ([Environment]::GetEnvironmentVariable('Foo', [System.EnvironmentVariableTarget]::User))
# システム環境変数の場合
> echo ([Environment]::GetEnvironmentVariable('Foo', [System.EnvironmentVariableTarget]::Machine))</pre>
<h3 id="レジストリ-エディタ"><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EC%A5%B8%A5%B9%A5%C8%A5%EA">レジストリ</a> エディタ</h3>
<ul>
<li><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EC%A5%B8%A5%B9%A5%C8%A5%EA">レジストリ</a>エディタを開く</li>
<li>ユーザー<a class="keyword" href="http://d.hatena.ne.jp/keyword/%B4%C4%B6%AD%CA%D1%BF%F4">環境変数</a>の場合は <code>コンピューター\HKEY_CURRENT_USER\Environment</code></li>
<li>システム<a class="keyword" href="http://d.hatena.ne.jp/keyword/%B4%C4%B6%AD%CA%D1%BF%F4">環境変数</a>の場合は <code>コンピューター\HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Environment</code></li>
</ul>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20220516/20220516232225.jpg" width="731" height="231" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<h3 id="システムのプロパティ---環境変数">システムのプロパティ -> <a class="keyword" href="http://d.hatena.ne.jp/keyword/%B4%C4%B6%AD%CA%D1%BF%F4">環境変数</a></h3>
<ul>
<li><a class="keyword" href="http://d.hatena.ne.jp/keyword/Windows%A5%AD%A1%BC">Windowsキー</a>を押す</li>
<li>"env" と検索</li>
<li><code>システム環境変数の編集</code> をクリック</li>
</ul>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20220516/20220516230832.jpg" width="775" height="581" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<ul>
<li><code>環境変数</code> をクリック</li>
</ul>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20220516/20220516230932.png" width="476" height="530" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20220516/20220516231145.jpg" width="616" height="582" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<h1 id="余談PATHのREG_SZ登録をREG_EXPAND_SZに切り替える方法">余談、PATHの<code>REG_SZ</code>登録を<code>REG_EXPAND_SZ</code>に切り替える方法</h1>
<p>途中までは「システムのプロパティ -> <a class="keyword" href="http://d.hatena.ne.jp/keyword/%B4%C4%B6%AD%CA%D1%BF%F4">環境変数</a>」と同様</p>
<ul>
<li><code>システム環境変数の編集</code> をクリック</li>
<li><code>環境変数</code> をクリック</li>
<li>変数 <code>Path</code> を選択して、編集をクリック
<ul>
<li>ユーザー変数 と システム変数 どちらも同様</li>
</ul>
</li>
<li>"<a class="keyword" href="http://d.hatena.ne.jp/keyword/%B4%C4%B6%AD%CA%D1%BF%F4">環境変数</a>名の編集" の OK をクリック</li>
<li>"<a class="keyword" href="http://d.hatena.ne.jp/keyword/%B4%C4%B6%AD%CA%D1%BF%F4">環境変数</a>" の OK をクリック</li>
</ul>
<p>以上</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20220517/20220517003114.jpg" width="610" height="581" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p>どうやら、"<a class="keyword" href="http://d.hatena.ne.jp/keyword/%B4%C4%B6%AD%CA%D1%BF%F4">環境変数</a>名の編集" の OK を押したときに <code>REG_EXPAND_SZ</code> で登録される処理が発生するようです。<br />
もし、PATHに <code>%SystemRoot%</code> を使用していて展開されていない場合は試してみてください。</p>
<h1 id="環境変数の補足"><a class="keyword" href="http://d.hatena.ne.jp/keyword/%B4%C4%B6%AD%CA%D1%BF%F4">環境変数</a>の補足</h1>
<p><a class="keyword" href="http://d.hatena.ne.jp/keyword/Windows">Windows</a>上では <code>%SystemRoot%</code> = <code>C:\Windows</code> です。</p>
<p>ちなみに <code>%USERPROFILE%</code>は現在のユーザーの<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%DB%A1%BC%A5%E0%A5%D5%A5%A9%A5%EB%A5%C0">ホームフォルダ</a>を表します。<br />
<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A8%A5%AF%A5%B9%A5%D7%A5%ED%A1%BC%A5%E9">エクスプローラ</a>ーで以下のように入力すると<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%DB%A1%BC%A5%E0%A5%D5%A5%A9%A5%EB%A5%C0">ホームフォルダ</a>に移動することができます。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20220516/20220516234004.png" width="456" height="138" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<h1 id="雑感">雑感</h1>
<p>前回の記事で、Git for <a class="keyword" href="http://d.hatena.ne.jp/keyword/Windows">Windows</a>の設定するときのコマンドで<code>SetEnvironmentVariable</code>使ってたのですが、<br />
<code>%SystemRoot%</code>が展開されないことに気づいて急遽記事を書きました。</p>
<p>もし、前回の記事を試してローカルがおかしくなった方がいたらごめんなさい。。。
「余談、PATHの<code>REG_SZ</code>登録を<code>REG_EXPAND_SZ</code>に切り替える方法」を実施すれば解消すると思います 🙇♂️</p>
<p>「<code>GetEnvironmentVariable</code>使うなら、対の<code>SetEnvironmentVariable</code>でしょ!」って感じで使ってたらこんな罠があったとは。。。</p>
<p>ご参考になれば幸いです。</p>
<p>それでは~</p>
tsgcpp
Git for Windowsをコマンドラインからインストールする方法
hatenablog://entry/13574176438092339410
2022-05-15T16:12:14+09:00
2022-05-15T16:12:14+09:00 今回はWindowsのコマンドラインでGit for Windows (Git Bash) をインストールする方法を紹介します。
Git for WindowsはWindows上でGitやBashを使用する場合にインストールしておくと何かと役に立つパッケージですが、
GUI経由でのインストールは必要オプションの取捨選択が必要であったり、なによりインストール状態の再現性に乏しいところがあります。
コマンドライン経由のインストール方法を確立することで、再現性があり開発環境インフラ構築などにも応用させることができますためよかったらご活用ください!
<ul class="table-of-contents">
<li><a href="#概要">概要</a></li>
<li><a href="#動作環境">動作環境</a></li>
<li><a href="#注意事項">注意事項</a></li>
<li><a href="#変更履歴">変更履歴</a></li>
<li><a href="#20220516-2300以前のExampleを試した方は以下を実施を推奨">2022/05/16 23:00以前のExampleを試した方は以下を実施を推奨</a></li>
<li><a href="#インストール方法">インストール方法</a><ul>
<li><a href="#インストールオプションについて">インストールオプションについて</a></li>
<li><a href="#Gitからps1ファイルをクローン">Gitからps1ファイルをクローン</a></li>
<li><a href="#PowerShellを管理者権限で開く">PowerShellを管理者権限で開く</a></li>
<li><a href="#PowerShellでクローンしたWindowsInfrastructureExample上に移動">PowerShellでクローンしたWindowsInfrastructureExample上に移動</a></li>
<li><a href="#Git-2xx-64-bitexe-を配置">Git-2.x.x-64-bit.exe を配置</a><ul>
<li><a href="#コマンドでダウンロードする場合">コマンドでダウンロードする場合</a></li>
<li><a href="#ブラウザでダウンロードする場合">ブラウザでダウンロードする場合</a></li>
</ul>
</li>
<li><a href="#install-gitps1-を実行">install-git.ps1 を実行</a></li>
<li><a href="#インストールの確認-コマンドラインの場合">インストールの確認 (コマンドラインの場合)</a></li>
<li><a href="#任意PATHの追加">(任意)PATHの追加</a></li>
</ul>
</li>
<li><a href="#スクリプト解説">スクリプト解説</a><ul>
<li><a href="#InnoSetup">InnoSetup</a></li>
<li><a href="#LOADINF">/LOADINF</a><ul>
<li><a href="#余談git-for-windowsinf-の方針">余談、git-for-windows.inf の方針</a></li>
</ul>
</li>
<li><a href="#SAVEINF">/SAVEINF</a></li>
<li><a href="#-Out-Null">| Out-Null</a></li>
</ul>
</li>
<li><a href="#余談今回の調査で躓いたところ">余談、今回の調査で躓いたところ</a></li>
<li><a href="#雑感">雑感</a></li>
</ul>
<h1 id="概要">概要</h1>
<p>今回は<a class="keyword" href="http://d.hatena.ne.jp/keyword/Windows">Windows</a>の<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%DE%A5%F3%A5%C9%A5%E9%A5%A4%A5%F3">コマンドライン</a>でGit for <a class="keyword" href="http://d.hatena.ne.jp/keyword/Windows">Windows</a> (Git <a class="keyword" href="http://d.hatena.ne.jp/keyword/Bash">Bash</a>) をインストールする方法を紹介します。</p>
<p>Git for <a class="keyword" href="http://d.hatena.ne.jp/keyword/Windows">Windows</a>は<a class="keyword" href="http://d.hatena.ne.jp/keyword/Windows">Windows</a>上でGitや<a class="keyword" href="http://d.hatena.ne.jp/keyword/Bash">Bash</a>を使用する場合にインストールしておくと何かと役に立つパッケージですが、<br/>
<a class="keyword" href="http://d.hatena.ne.jp/keyword/GUI">GUI</a>経由でのインストールは必要オプションの取捨選択が必要であったり、なによりインストール状態の再現性に乏しいところがあります。</p>
<p><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%DE%A5%F3%A5%C9%A5%E9%A5%A4%A5%F3">コマンドライン</a>経由のインストール方法を確立することで、再現性があり開発環境インフラ構築などにも応用させることができますためよかったらご活用ください!</p>
<h1 id="動作環境">動作環境</h1>
<ul>
<li><a class="keyword" href="http://d.hatena.ne.jp/keyword/Windows">Windows</a> 10
<ul>
<li>筆者はPro版ですが、Homeでも問題ないと思います</li>
</ul>
</li>
</ul>
<h1 id="注意事項">注意事項</h1>
<ul>
<li><span style="color: #ff0000"><strong>以下を試す場合は自己責任でお願いします!</strong></span></li>
<li><span style="color: #ff0000"><strong>以下の処理は管理者権限で実行しますが、当方は事故や故障などの一切の責任を負いません!</strong></span></li>
</ul>
<p>上記の件、ご留意ください</p>
<h1 id="変更履歴">変更履歴</h1>
<ul>
<li>2022/05/17
<ul>
<li>「2022/05/16 23:00以前のExampleを試した場合は以下を実施をお願いします」を追加</li>
<li>「(任意)PATHの追加」に注意文を追記</li>
</ul>
</li>
<li>2023/02/05
<ul>
<li>「install-git.ps1 を実行」を <code>PowerShell -ExecutionPolicy Bypass</code> を使ったコマンドに修正</li>
</ul>
</li>
</ul>
<hr />
<h1 id="20220516-2300以前のExampleを試した方は以下を実施を推奨">2022/05/16 23:00以前のExampleを試した方は以下を実施を推奨</h1>
<p><a class="keyword" href="http://d.hatena.ne.jp/keyword/%B4%C4%B6%AD%CA%D1%BF%F4">環境変数</a>の設定に使用していたコマンド (<code>[Environment]::SetEnvironmentVariable</code>) に少し難点がありました。<br/>
<code>WindowsInfrastructureExample</code> 上の<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%EA%A5%D7%A5%C8">スクリプト</a>は修正済みです。</p>
<p>もし、すでに試していてパスが通らなくなったり、コマンドが実行できなくなった方は以下の「余談、PATHのREG_SZ登録をREG_EXPAND_SZに切り替える方法」を試してみてください。</p>
<p><a href="https://tsgcpp.hateblo.jp/entry/2022/05/17/004725#%E4%BD%99%E8%AB%87PATH%E3%81%AEREG_SZ%E7%99%BB%E9%8C%B2%E3%82%92REG_EXPAND_SZ%E3%81%AB%E5%88%87%E3%82%8A%E6%9B%BF%E3%81%88%E3%82%8B%E6%96%B9%E6%B3%95">余談、PATHのREG_SZ登録をREG_EXPAND_SZに切り替える方法</a></p>
<p>詳細も上記記事に記載しています。</p>
<hr />
<h1 id="インストール方法">インストール方法</h1>
<h2 id="インストールオプションについて">インストールオプションについて</h2>
<ul>
<li>以下のコマンドでのGit for <a class="keyword" href="http://d.hatena.ne.jp/keyword/Windows">Windows</a>のインストールオプションは筆者が普段使用しているオプションを使用
<ul>
<li>詳細は後述しますが、各自で <code>git-for-windows.inf</code> 都合の良いように改変してください</li>
</ul>
</li>
</ul>
<h2 id="Gitからps1ファイルをクローン">Gitからps1ファイルをクローン</h2>
<ul>
<li>ps1ファイル(<a class="keyword" href="http://d.hatena.ne.jp/keyword/PowerShell">PowerShell</a><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%EA%A5%D7%A5%C8">スクリプト</a>)を以下の<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>からクローンする</li>
</ul>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Ftsgcpp%2FWindowsInfrastructureExample" title="GitHub - tsgcpp/WindowsInfrastructureExample: An infrastructure example in Windows for CI" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://github.com/tsgcpp/WindowsInfrastructureExample">github.com</a></cite></p>
<h2 id="PowerShellを管理者権限で開く"><a class="keyword" href="http://d.hatena.ne.jp/keyword/PowerShell">PowerShell</a>を管理者権限で開く</h2>
<p>※開き方は好きなやり方で問題ありません</p>
<p>今回は<a class="keyword" href="http://d.hatena.ne.jp/keyword/Windows">Windows</a> 10標準搭載の<a class="keyword" href="http://d.hatena.ne.jp/keyword/PowerShell">PowerShell</a>を使用</p>
<ul>
<li><code>Windows</code> キーを押して、「<a class="keyword" href="http://d.hatena.ne.jp/keyword/powershell">powershell</a>」を検索</li>
<li>「管理者として実行する」をクリック</li>
</ul>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20220514/20220514201533.jpg" width="777" height="636" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<h2 id="PowerShellでクローンしたWindowsInfrastructureExample上に移動"><a class="keyword" href="http://d.hatena.ne.jp/keyword/PowerShell">PowerShell</a>でクローンしたWindowsInfrastructureExample上に移動</h2>
<pre class="code pwsh" data-lang="pwsh" data-unlink>> cd <path>\WindowsInfrastructureExample</pre>
<h2 id="Git-2xx-64-bitexe-を配置">Git-2.x.x-64-bit.exe を配置</h2>
<h3 id="コマンドでダウンロードする場合">コマンドでダウンロードする場合</h3>
<pre class="code pwsh" data-lang="pwsh" data-unlink>> Invoke-WebRequest https://github.com/git-for-windows/git/releases/download/v2.36.1.windows.1/Git-2.36.1-64-bit.exe -OutFile Git-2.36.1-64-bit.exe</pre>
<p>※インフラ構築の<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%EA%A5%D7%A5%C8">スクリプト</a>を書く場合は、このコマンド自体をps1<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%EA%A5%D7%A5%C8">スクリプト</a>に組み込んでも良い</p>
<h3 id="ブラウザでダウンロードする場合">ブラウザでダウンロードする場合</h3>
<p>以下より <code>Git-2.x.x-64-bit.exe</code>をダウンロードし、<code>WindowsInfrastructureExample</code> フォルダ以下に配置</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fgit-for-windows%2Fgit%2Freleases" title="Releases · git-for-windows/git" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://github.com/git-for-windows/git/releases">github.com</a></cite></p>
<h2 id="install-gitps1-を実行">install-git.ps1 を実行</h2>
<pre class="code pwsh" data-lang="pwsh" data-unlink>> PowerShell -ExecutionPolicy Bypass .\install-git.ps1</pre>
<p>エラーログが出なければ問題なく Git for <a class="keyword" href="http://d.hatena.ne.jp/keyword/Windows">Windows</a>のインストール完了</p>
<h2 id="インストールの確認-コマンドラインの場合">インストールの確認 (<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%DE%A5%F3%A5%C9%A5%E9%A5%A4%A5%F3">コマンドライン</a>の場合)</h2>
<ul>
<li><code>Dir 'C:\Program Files\Git\cmd\git.exe'</code>でエラーが出なければインストール完了</li>
</ul>
<pre class="code" data-lang="" data-unlink>> Dir 'C:\Program Files\Git\bin\git.exe'
ディレクトリ: C:\Program Files\Git\bin
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a---- 2022/05/09 13:28 45584 git.exe</pre>
<h2 id="任意PATHの追加">(任意)PATHの追加</h2>
<p><span style="color: #ff0000"><strong>(追記)ローカルPCの<a class="keyword" href="http://d.hatena.ne.jp/keyword/%B4%C4%B6%AD%CA%D1%BF%F4">環境変数</a> PATH が変更されます!ご注意ください!</strong></span></p>
<pre class="code pwsh" data-lang="pwsh" data-unlink>> .\set-git-path.ps1</pre>
<p>以上でインストール及び設定は完了です。</p>
<hr />
<h1 id="スクリプト解説"><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%EA%A5%D7%A5%C8">スクリプト</a>解説</h1>
<p><a href="https://github.com/tsgcpp/WindowsInfrastructureExample/blob/main/install-git.ps1">install-git.ps1</a></p>
<h2 id="InnoSetup">InnoSetup</h2>
<p>Git for <a class="keyword" href="http://d.hatena.ne.jp/keyword/Windows">Windows</a>はInnoSetupというフリーの<a class="keyword" href="http://d.hatena.ne.jp/keyword/Windows">Windows</a>向け<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%C8%A1%BC%A5%E9">インストーラ</a>ーが使用されています</p>
<p>パラメータの詳細は <a href="https://jrsoftware.org/ishelp/index.php?topic=setupcmdline">Setup Command Line Parameters</a> で確認できます</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fjrsoftware.org%2Fisinfo.php" title="Inno Setup" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://jrsoftware.org/isinfo.php">jrsoftware.org</a></cite></p>
<ul>
<li><code>/ALLUSERS</code> はその通り<a class="keyword" href="http://d.hatena.ne.jp/keyword/Windows">Windows</a>上の全部のユーザーにインストール</li>
<li><code>/VERYSILENT</code> はダイアログなどを抑制</li>
<li><code>/Log xxx.log</code> でログ出力
<ul>
<li>標準出力に直接ログを流し込めなかったため、一度ログファイルに落として最後に出力に流しています</li>
</ul>
</li>
</ul>
<h2 id="LOADINF">/LOADINF</h2>
<p><code>/LOADINF=git-for-windows.inf</code></p>
<p>オプションファイルの指定で、実質的にGit for <a class="keyword" href="http://d.hatena.ne.jp/keyword/Windows">Windows</a>のオプション指定方法になります</p>
<ul>
<li>Gitのcommit時の改行に<code>LF</code>を指定 (<code>CRLFOption</code>)</li>
<li>Credential Managerの使用 (<code>UseCredentialManager</code>)</li>
<li><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%DE%A5%F3%A5%C9%A5%D7%A5%ED%A5%F3%A5%D7%A5%C8">コマンドプロンプト</a>でgitを使用可能にするかの指定 (<code>PathOption</code>)</li>
<li>etc...</li>
</ul>
<p>手順で使用したファイルは <a href="https://github.com/tsgcpp/WindowsInfrastructureExample/blob/main/git-for-windows.inf">git-for-windows.inf</a> となります</p>
<p>後述の <code>SAVEINF</code> からオプションファイルを作成できます。</p>
<p><span style="color: #999999">自分は<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%C8%A1%BC%A5%E9">インストーラ</a>ーの<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%EA%A5%D7%A5%C8">スクリプト</a> (<a href="https://github.com/git-for-windows/build-extra/blob/HEAD/installer/install.iss">install.iss</a>) から割り出してました。。。</span></p>
<h3 id="余談git-for-windowsinf-の方針">余談、git-for-<a class="keyword" href="http://d.hatena.ne.jp/keyword/windows">windows</a>.inf の方針</h3>
<ul>
<li><strong><a class="keyword" href="http://d.hatena.ne.jp/keyword/CLI">CLI</a>特化</strong></li>
<li><a class="keyword" href="http://d.hatena.ne.jp/keyword/UNIX">UNIX</a>系と互換性を意識
<ul>
<li>OpenSSHの使用</li>
<li>LFOnly指定など</li>
</ul>
</li>
<li>Credential Manager を有効化
<ul>
<li>これがあると<a class="keyword" href="http://d.hatena.ne.jp/keyword/GitHub">GitHub</a>との連携が楽になります! (別記事でいずれ紹介します!)</li>
</ul>
</li>
<li>デフォルトブランチ名は<code>main</code> (昨今の流れを汲み取って)</li>
<li>git <a class="keyword" href="http://d.hatena.ne.jp/keyword/LFS">LFS</a> を有効化</li>
</ul>
<h2 id="SAVEINF">/SAVEINF</h2>
<p><code>/LOADINF=git-for-windows_save.inf</code></p>
<p>※<code>install-git.ps1</code>では非使用</p>
<p>今回は直接は使用していませんが、指定するとインストール時のオプションを確認可能です!</p>
<h2 id="-Out-Null">| Out-Null</h2>
<p>コマンドの最後に <code>| Out-Null</code> を入れることで、インストール完了までコマンド終了待ちになります。<br/>
<a class="keyword" href="http://d.hatena.ne.jp/keyword/PowerShell">PowerShell</a>だとメジャーなテクニックのようです。</p>
<h1 id="余談今回の調査で躓いたところ">余談、今回の調査で躓いたところ</h1>
<ul>
<li><a class="keyword" href="http://d.hatena.ne.jp/keyword/PowerShell">PowerShell</a>で変数に実行ファイルを仕込む場合は <code>& ./$PackageFile</code>のように記載
<ul>
<li><code>&</code>なしで <code>./$PackageFile</code> とするとエラーになる</li>
</ul>
</li>
<li><code>Set-ExecutionPolicy</code> を実行しないと <a class="keyword" href="http://d.hatena.ne.jp/keyword/PowerShell">PowerShell</a>上でps1ファイルが実行できない</li>
<li>オプションの探し方
<ul>
<li><code>Foo.exe /?</code> or <code>Foo.exe /HELP</code> などで確認できる場合があるようです</li>
<li><a class="keyword" href="http://d.hatena.ne.jp/keyword/Windows">Windows</a>のダイアログ出でるのでコピーできないなど面倒。。。</li>
</ul>
</li>
<li>別ps1ファイルのfunctionをコールする方法</li>
</ul>
<p>自分は元々<a class="keyword" href="http://d.hatena.ne.jp/keyword/UNIX">UNIX</a>系のエンジニアだったので、<a class="keyword" href="http://d.hatena.ne.jp/keyword/Windows">Windows</a>と<a class="keyword" href="http://d.hatena.ne.jp/keyword/PowerShell">PowerShell</a>の<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%DE%A5%F3%A5%C9%A5%E9%A5%A4%A5%F3">コマンドライン</a>化には結構手こずりました。<br/>
再現性のあるインフラの構築にはIaC (Infrastructure as Code) は欠かせないのですが、<a class="keyword" href="http://d.hatena.ne.jp/keyword/Windows">Windows</a>に慣れていないせいかほとんど<a class="keyword" href="http://d.hatena.ne.jp/keyword/PowerShell">PowerShell</a>調査みたいになってしまいました。</p>
<h1 id="雑感">雑感</h1>
<p>前回の記事投稿から2ヶ月経ってしまいましたね。。。</p>
<p>しかも今回はXRと3DCGから程遠い<a class="keyword" href="http://d.hatena.ne.jp/keyword/CLI">CLI</a>に関する記事に。</p>
<p>「3DCGとかUnityやる気あるの?」と思われた方もいらっしゃるかもしれませんが、ちゃんと普段は触ってます!<br/>
地味なことですが、再現性のある開発環境の構築方法は何かと便利だったりします。</p>
<p>また、Git for <a class="keyword" href="http://d.hatena.ne.jp/keyword/Windows">Windows</a>ってインストールしておくと、UnityのPackage Managerで<a class="keyword" href="http://d.hatena.ne.jp/keyword/GitHub">GitHub</a>経由のパッケージが取得しやすくなったり、<br/>
<a class="keyword" href="http://d.hatena.ne.jp/keyword/GitHub">GitHub</a>の<a class="keyword" href="http://d.hatena.ne.jp/keyword/HTTPS">HTTPS</a>経由での連携が安定したりと、何気に非エンジニアの方にもメリットがあったりします!</p>
<p>さて、次回は「PowerShell7を<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%DE%A5%F3%A5%C9%A5%E9%A5%A4%A5%F3">コマンドライン</a>からインストールする方法」にする予定です!</p>
<p>それでは~</p>
tsgcpp
【Unity, XR】Single Pass Instanced向けURP Render FeatureのTips 【2020.3, 2021.2】
hatenablog://entry/13574176438075358896
2022-03-23T01:13:30+09:00
2022-03-23T01:13:30+09:00 概要 追記 略語 動作環境 Tips Unity 2020 ポストエフェクトでは cmd.Blit は使用不可で cmd.DrawMesh を使用 サンプルのColorBlitRenderFeature について (2021.2以降) SwapBufferはSPIでは実質的に使用不可 画面キャプチャは不透明オブジェクトのみ 完全な描画結果が取れない理由 Unity 2021.2以降でのポストエフェクトにはIntermediateTextureMode.Alwaysの指定が必要 IntermediateTextureMode.Alwaysの指定が必要な理由 Unity 2020.3 以前での中間…
<ul class="table-of-contents">
<li><a href="#概要">概要</a></li>
<li><a href="#追記">追記</a></li>
<li><a href="#略語">略語</a></li>
<li><a href="#動作環境">動作環境</a></li>
<li><a href="#Tips-Unity-2020">Tips Unity 2020</a><ul>
<li><a href="#ポストエフェクトでは-cmdBlit-は使用不可で-cmdDrawMesh-を使用">ポストエフェクトでは cmd.Blit は使用不可で cmd.DrawMesh を使用</a><ul>
<li><a href="#サンプルのColorBlitRenderFeature-について">サンプルのColorBlitRenderFeature について</a></li>
</ul>
</li>
<li><a href="#20212以降-SwapBufferはSPIでは実質的に使用不可">(2021.2以降) SwapBufferはSPIでは実質的に使用不可</a></li>
<li><a href="#画面キャプチャは不透明オブジェクトのみ">画面キャプチャは不透明オブジェクトのみ</a><ul>
<li><a href="#完全な描画結果が取れない理由">完全な描画結果が取れない理由</a></li>
</ul>
</li>
<li><a href="#Unity-20212以降でのポストエフェクトにはIntermediateTextureModeAlwaysの指定が必要">Unity 2021.2以降でのポストエフェクトにはIntermediateTextureMode.Alwaysの指定が必要</a><ul>
<li><a href="#IntermediateTextureModeAlwaysの指定が必要な理由">IntermediateTextureMode.Alwaysの指定が必要な理由</a></li>
<li><a href="#Unity-20203-以前での中間テクスチャ">Unity 2020.3 以前での中間テクスチャ</a></li>
</ul>
</li>
<li><a href="#おまけIntermediateTextureModeAlwaysの指定なしでも機能させる方法">おまけ、IntermediateTextureMode.Alwaysの指定なしでも機能させる方法</a><ul>
<li><a href="#IntermediateTextureModeAlwaysの指定なしでも機能する理由">IntermediateTextureMode.Alwaysの指定なしでも機能する理由</a></li>
</ul>
</li>
<li><a href="#URP--SPI-で可能な表現の例">URP + SPI で可能な表現の例</a><ul>
<li><a href="#不透明オブジェクトの描画結果を使用したポストエフェクト">不透明オブジェクトの描画結果を使用したポストエフェクト</a></li>
<li><a href="#アルファブレンドによるフェードインフェードアウト">アルファブレンドによるフェードイン・フェードアウト</a></li>
</ul>
</li>
</ul>
</li>
<li><a href="#雑感">雑感</a></li>
</ul>
<h1 id="概要">概要</h1>
<p>UnityのUniversal Render Pipelineには Render Feature という描画処理を差し込む機能があります。<br />
この Render Feature ですが、XRのSingle Pass Instanced 対応をする場合はクセがあります。</p>
<p>今回はそんな<strong>Single Pass Instanced向けのRender FeatureのTips</strong>を共有します。</p>
<h1 id="追記">追記</h1>
<ul>
<li>2022/03/27 「おまけ、IntermediateTextureMode.Alwaysの指定なしでも機能させる方法」項目を追加</li>
</ul>
<h1 id="略語">略語</h1>
<p>用語が長いので、以下では略語で紹介します。</p>
<table>
<thead>
<tr>
<th> 用語 </th>
<th> 略語 </th>
</tr>
</thead>
<tbody>
<tr>
<td> Universal Render Pipeline </td>
<td> URP </td>
</tr>
<tr>
<td> Render Feature </td>
<td> RF </td>
</tr>
<tr>
<td> Single Pass Instanced </td>
<td> SPI </td>
</tr>
<tr>
<td> Render Texture </td>
<td> RT </td>
</tr>
</tbody>
</table>
<h1 id="動作環境">動作環境</h1>
<ul>
<li>Unity 2020.3.30f1 + Universal RP 10.8.1</li>
<li>Unity 2021.2.16f1 + Universal RP 12.1.6</li>
</ul>
<h1 id="Tips-Unity-2020">Tips Unity 2020</h1>
<h2 id="ポストエフェクトでは-cmdBlit-は使用不可で-cmdDrawMesh-を使用">ポストエフェクトでは cmd.Blit は使用不可で cmd.DrawMesh を使用</h2>
<p>詳細は以下のドキュメントに記載されています。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdocs.unity3d.com%2FPackages%2Fcom.unity.render-pipelines.universal%4012.1%2Fmanual%2Frenderer-features%2Fhow-to-fullscreen-blit-in-xr-spi.html" title="How to perform a full screen blit in Single Pass Instanced rendering in XR | Universal RP | 12.1.4" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://docs.unity3d.com/Packages/[email protected]/manual/renderer-features/how-to-fullscreen-blit-in-xr-spi.html">docs.unity3d.com</a></cite></p>
<p>※以下、上記ドキュメントを 「SPI Blit Example」と呼びます</p>
<pre class="code" data-lang="" data-unlink>NOTE: Do not use the cmd.Blit method in URP XR projects because that method has compatibility issues with the URP XR integration.</pre>
<p>互換性の問題でSPIでは <code>cmd.Blit</code> は正しく機能しないようで、<br />
もし、ポストエフェクト的なことを実施したい場合は <code>cmd.DrawMesh</code> + <code>_CameraColorTarget</code> から画面キャプチャを取得して Blit を実現します。</p>
<h3 id="サンプルのColorBlitRenderFeature-について">サンプルのColorBlitRenderFeature について</h3>
<p>ちなみに SPI Blit Example の <code>ColorBlitRenderFeature</code> のサンプルは描画の緑成分を抽出するポストエフェクトになっています。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20220321/20220321235931.png" alt="f:id:tsgcpp:20220321235931p:plain" width="739" height="412" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span>
↓
<span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20220321/20220321235957.png" alt="f:id:tsgcpp:20220321235957p:plain" width="747" height="415" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<h2 id="20212以降-SwapBufferはSPIでは実質的に使用不可">(2021.2以降) SwapBufferはSPIでは実質的に使用不可</h2>
<p>Unity 2021.2 ではRFでポストエフェクトを実装しやすいようにSwapBufferという機能が追加されています。</p>
<p>残念ながらSwapBufferはSPIでは使用できません。。。(<span style="color: #ff0000"><strong>もし使用できる場合は教えていただけるとありがたいです!!</strong></span>)。<br />
原因はSwapBufferの処理が隠蔽されていること、SwapBufferを使用する<code>Blit</code>メソッドが <code>cmd.Blit</code> を使用するためです。</p>
<p>※SwapBufferについては以下の動画をご参照ください。</p>
<p><iframe width="560" height="315" src="https://www.youtube.com/embed/kchozuXTf4c?start=2122&feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe><cite class="hatena-citation"><a href="https://www.youtube.com/watch?v=kchozuXTf4c&t=2122s">www.youtube.com</a></cite></p>
<h2 id="画面キャプチャは不透明オブジェクトのみ">画面キャプチャは不透明オブジェクトのみ</h2>
<p>さて、ポストエフェクトと言いましたが、URP + SPIにおいては欠点があります。</p>
<p><span style="color: #ff0000"><strong>不透明オブジェクトの描画結果しか取得できません</strong></span></p>
<p>これは、シェーダーに渡されるテクスチャの名前 (<code>_CameraOpaqueTexture</code>) の通り、<br />
不透明オブジェクトのみ描画した画面キャプチャが渡されるためです。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20220323/20220323003420.png" alt="f:id:tsgcpp:20220323003420p:plain" width="742" height="416" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span>
↓
<figure class="figure-image figure-image-fotolife" title="renderPassEvent = RenderPassEvent.BeforeRenderingPostProcessing の場合"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20220322/20220322000310.png" alt="f:id:tsgcpp:20220322000310p:plain" width="745" height="417" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>renderPassEvent = RenderPassEvent.BeforeRenderingPostProcessing の場合</figcaption></figure></p>
<p><figure class="figure-image figure-image-fotolife" title="renderPassEvent = RenderPassEvent.BeforeRenderingTransparents の場合"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20220322/20220322012404.png" alt="f:id:tsgcpp:20220322012404p:plain" width="744" height="419" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>renderPassEvent = RenderPassEvent.BeforeRenderingTransparents の場合</figcaption></figure></p>
<p>SwapBufferも使用できないため、残念ながらSPIでのポストエフェクトは透過オブジェクトを対象にできないなど制限が強いです。。。</p>
<h3 id="完全な描画結果が取れない理由">完全な描画結果が取れない理由</h3>
<p>SPIにおいて、スクリーン向けRTにアクセスする術がないためです。</p>
<ul>
<li><code>ScriptableRenderer.GetCameraColorFrontBuffer</code> が internalメソッドのためアクセスできない</li>
<li>(2021.2以降)SwapBuffer用のRTにアクセスできない</li>
</ul>
<h2 id="Unity-20212以降でのポストエフェクトにはIntermediateTextureModeAlwaysの指定が必要">Unity 2021.2以降でのポストエフェクトにはIntermediateTextureMode.Alwaysの指定が必要</h2>
<p>Unity2021.2から <code>IntermediateTextureMode</code> という設定項目が追加されました。</p>
<p><span style="color: #ff0000"><code>IntermediateTextureMode.Always</code>を指定しないとSPI Blit Exampleの <code>ColorBlitRenderFeature</code> は機能しません!</span></p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdocs.unity3d.com%2FPackages%2Fcom.unity.render-pipelines.universal%4012.1%2Fapi%2FUnityEngine.Rendering.Universal.IntermediateTextureMode.html" title="Enum IntermediateTextureMode | Universal RP | 12.1.4" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://docs.unity3d.com/Packages/[email protected]/api/UnityEngine.Rendering.Universal.IntermediateTextureMode.html">docs.unity3d.com</a></cite></p>
<p>名前の通り、中間テクスチャ(IntermediateTexture)を使用するかの指定になります。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20220322/20220322004654.png" alt="f:id:tsgcpp:20220322004654p:plain" width="805" height="674" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<h3 id="IntermediateTextureModeAlwaysの指定が必要な理由">IntermediateTextureMode.Alwaysの指定が必要な理由</h3>
<p>指定していない場合は <code>cmd.DrawMesh</code> の描画先が中間テクスチャではなくスクリーンのRTに直接描画されるのですが、<br />
その後、<code>FinalBlitPass</code>によってスクリーンのRTが上書きされてしまうためです。</p>
<p>以下のコードに<code>IntermediateTextureMode</code>による条件分岐があります。
<iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2FUnity-Technologies%2FGraphics%2Fblob%2F2021.2.16f1.4502%2Fcom.unity.render-pipelines.universal%2FRuntime%2FUniversalRenderer.cs%23L406-L415" title="Graphics/UniversalRenderer.cs at 2021.2.16f1.4502 · Unity-Technologies/Graphics" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://github.com/Unity-Technologies/Graphics/blob/2021.2.16f1.4502/com.unity.render-pipelines.universal/Runtime/UniversalRenderer.cs#L406-L415">github.com</a></cite></p>
<p>SPI Blit Example には、注意書きがないので<code>IntermediateTextureMode.Always</code>の指定を忘れると Unity 2021.2 では実質的に描画結果に現れません。</p>
<p><span style="color: #999999">判明するまで数時間かかりました。。。一応Report投げときました。</span></p>
<p>ちなみに非SPI(Multi Pass) や NonXRではSwapBufferが使用可能なので問題になりません。</p>
<h3 id="Unity-20203-以前での中間テクスチャ">Unity 2020.3 以前での中間テクスチャ</h3>
<p>Unity 2020.3 + Universal RP 10 では <code>IntermediateTextureMode</code> は有りませんが、<br />
実は RF が Forward Rendererに1つでも追加されている場合に、中間テクスチャが自動で有効になっていたようです。</p>
<p>以下のコードの <code>rendererFeatures.Count != 0</code> という判定からわかります。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2FUnity-Technologies%2FGraphics%2Fblob%2Fv10.8.1%2Fcom.unity.render-pipelines.universal%2FRuntime%2FForwardRenderer.cs%23L280-L289" title="Graphics/ForwardRenderer.cs at v10.8.1 · Unity-Technologies/Graphics" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://github.com/Unity-Technologies/Graphics/blob/v10.8.1/com.unity.render-pipelines.universal/Runtime/ForwardRenderer.cs#L280-L289">github.com</a></cite></p>
<h2 id="おまけIntermediateTextureModeAlwaysの指定なしでも機能させる方法">おまけ、IntermediateTextureMode.Alwaysの指定なしでも機能させる方法</h2>
<p><span style="color: #ff0000">※<code>IntermediateTextureMode.Always</code>を指定する方が確実と思われますが、一応紹介します。</span></p>
<p>SPI Blit Exampleの<code>ColorBlitPass</code>を以下のように<code>renderingData.cameraData.renderer.cameraColorTarget</code>を指定することで、
<code>IntermediateTextureMode.Always</code>を指定していない場合でも機能させることができます。</p>
<pre class="code lang-diff" data-lang="diff" data-unlink><span class="synType">--- a/Assets/ColorBlitPass.cs</span>
<span class="synType">+++ b/Assets/ColorBlitPass.cs</span>
<span class="synStatement">@@ -34,7 +34,7 @@</span><span class="synPreProc"> internal class ColorBlitPass : ScriptableRenderPass</span>
using (new ProfilingScope(cmd, m_ProfilingSampler))
{
m_Material.SetFloat("_Intensity", m_Intensity);
<span class="synSpecial">- cmd.SetRenderTarget(new RenderTargetIdentifier(m_CameraColorTarget, 0, CubemapFace.Unknown, -1));</span>
<span class="synIdentifier">+ cmd.SetRenderTarget(new RenderTargetIdentifier(renderingData.cameraData.renderer.cameraColorTarget, 0, CubemapFace.Unknown, -1));</span>
//The RenderingUtils.fullscreenMesh argument specifies that the mesh to draw is a quad.
cmd.DrawMesh(RenderingUtils.fullscreenMesh, Matrix4x4.identity, m_Material);
}
</pre>
<h3 id="IntermediateTextureModeAlwaysの指定なしでも機能する理由">IntermediateTextureMode.Alwaysの指定なしでも機能する理由</h3>
<ul>
<li><code>renderingData.cameraData.renderer.cameraColorTarget</code>は基本的に中間テクスチャとなっているため
<ul>
<li>以下のコードを読む限り<code>createColorTexture</code>がtrueの場合に中間テクスチャが描画先にになるのですが、スクリーン(非SceneView)向け描画は基本的にtrueになるみたいです</li>
</ul>
</li>
</ul>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2FUnity-Technologies%2FGraphics%2Fblob%2F2021.2.16f1.4502%2Fcom.unity.render-pipelines.universal%2FRuntime%2FUniversalRenderer.cs%23L492-L494" title="Graphics/UniversalRenderer.cs at 2021.2.16f1.4502 · Unity-Technologies/Graphics" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://github.com/Unity-Technologies/Graphics/blob/2021.2.16f1.4502/com.unity.render-pipelines.universal/Runtime/UniversalRenderer.cs#L492-L494">github.com</a></cite></p>
<p><code>createColorTexture == false</code>になる(中間テクスチャを描画先にしない)パターンのほうが珍しいですが、<br />
SPIの場合で中間テクスチャを描画先にする場合は、<code>renderingData.cameraData.renderer.cameraColorTarget</code>を使用するなら<code>IntermediateTextureMode.Always</code>を指定する方が確実だと思います。</p>
<h2 id="URP--SPI-で可能な表現の例">URP + SPI で可能な表現の例</h2>
<h3 id="不透明オブジェクトの描画結果を使用したポストエフェクト">不透明オブジェクトの描画結果を使用したポストエフェクト</h3>
<p>透明オブジェクトは対象にできませんが、不透明オブジェクトならポストエフェクトをかけることができます。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20220321/20220321235957.png" alt="f:id:tsgcpp:20220321235957p:plain" width="747" height="415" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<h3 id="アルファブレンドによるフェードインフェードアウト">アルファ<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D6%A5%EC%A5%F3%A5%C9">ブレンド</a>によるフェードイン・フェードアウト</h3>
<p>シェーダーのアルファ<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D6%A5%EC%A5%F3%A5%C9">ブレンド</a>は問題なく使用できるため、フェードイン・フェードアウトは実現できます。</p>
<pre class="code hlsl" data-lang="hlsl" data-unlink> Blend SrcAlpha OneMinusSrcAlpha
...
half4 frag (Varyings input) : SV_Target
{
UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);
return half4(0, 0, 0, saturate(_Intensity));
}</pre>
<p><blockquote data-conversation="none" class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">Single Pass Instanced向けURP RenderFeatureでアルファ<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D6%A5%EC%A5%F3%A5%C9">ブレンド</a>が機能することの確認 <a href="https://t.co/RH3MDPJwq2">pic.twitter.com/RH3MDPJwq2</a></p>— すぎしー (@tsgcpp) <a href="https://twitter.com/tsgcpp/status/1506291777514643458?ref_src=twsrc%5Etfw">2022年3月22日</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> </p>
<h1 id="雑感">雑感</h1>
<p>なんとか3月中に1つ記事が書けました!
本当はAddressablesに関する記事を書いていたんですが、検証の自動テストがGameCIで実現できず。。。</p>
<p>Unity2020でRFを使用すると中間テクスチャが必ず使用されるようになるのも知るきっかけになってよかったです(RF使用する場合のパフォーマンスの懸念自体は増えましたが)。</p>
<p>世間は卒業シーズン、新卒入社で新生活を始める人も多そうですね。
これからUnity、XR始める人にも興味を持っていただければ幸いです!</p>
<p>それでは~</p>
tsgcpp
【Moq】Moqはメソッドチェーンでまるごとモック化できるよ
hatenablog://entry/13574176438070983898
2022-03-09T00:58:42+09:00
2022-03-09T00:58:42+09:00 メソッドチェーンのモック化 何が起こっているの? Setupで指定しない場合はdefault 検証コード 雑感 メソッドチェーンのモック化 IFoo -> IBar -> IBaz のようにinterfaceのプロパティが連結されている場合、 以下の2つのモック化は同等になります。 // setup var fooMock = new Mock<IFoo>(); var barMock = new Mock<IBar>(); var bazMock = new Mock<IBaz>(); // when fooMock.Setup(m => m.Bar).Returns(barMock.Obj…
<ul class="table-of-contents">
<li><a href="#メソッドチェーンのモック化">メソッドチェーンのモック化</a><ul>
<li><a href="#何が起こっているの">何が起こっているの?</a></li>
<li><a href="#Setupで指定しない場合はdefault">Setupで指定しない場合はdefault</a></li>
</ul>
</li>
<li><a href="#検証コード">検証コード</a></li>
<li><a href="#雑感">雑感</a></li>
</ul>
<h1 id="メソッドチェーンのモック化">メソッドチェーンのモック化</h1>
<p>IFoo -> IBar -> IBaz のようにinterfaceのプロパティが連結されている場合、
以下の2つのモック化は同等になります。</p>
<pre class="code lang-cs" data-lang="cs" data-unlink> <span class="synComment">// setup</span>
var fooMock = <span class="synStatement">new</span> Mock<IFoo>();
var barMock = <span class="synStatement">new</span> Mock<IBar>();
var bazMock = <span class="synStatement">new</span> Mock<IBaz>();
<span class="synComment">// when</span>
fooMock.Setup(m => m.Bar).Returns(barMock.Object);
barMock.Setup(m => m.Baz).Returns(bazMock.Object);
bazMock.Setup(m => m.Message).Returns(<span class="synConstant">"Hello Redundant"</span>);
<span class="synComment">// then</span>
IFoo target = fooMock.Object;
Assert.That(target.Bar.Baz.Message, Is.EqualTo(<span class="synConstant">"Hello Redundant"</span>));
Assert.That(target.Bar.Baz, Is.Not.Null);
</pre>
<p>↓</p>
<pre class="code lang-cs" data-lang="cs" data-unlink> <span class="synComment">// setup</span>
var fooMock = <span class="synStatement">new</span> Mock<IFoo>();
<span class="synComment">// when</span>
fooMock.Setup(m => m.Bar.Baz.Message).Returns(<span class="synConstant">"Hello Chain"</span>);
<span class="synComment">// then</span>
IFoo target = fooMock.Object;
Assert.That(target.Bar.Baz.Message, Is.EqualTo(<span class="synConstant">"Hello Chain"</span>));
</pre>
<p>上部(以後Aパターン)のようにわざわざ、<code>IFoo</code>, <code>IBar</code>, <code>IBaz</code>のMockを作る必要はなく、</p>
<p>下部(以後Bパターン)の <code>fooMock.Setup(m => m.Bar.Baz.Message).Returns("Hello Chain");</code> のように<code>IFoo</code>からメソッドチェーンでまとめてモック化してくれるという話でした。</p>
<h2 id="何が起こっているの">何が起こっているの?</h2>
<p>どうやらAパターンの宣言でBパターンと同等になるようです。</p>
<p>後で検証コードを記載しますが、<br />
<code>target.Bar</code> に <code>Mock<IBar>.Object</code>, <code>target.Bar.Baz</code>に<code>Mock<IBaz>.Object</code>の<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>が割り当てられていました。</p>
<h2 id="Setupで指定しない場合はdefault">Setupで指定しない場合はdefault</h2>
<p>以下の用に<code>Setup</code>を実施しない場合はdefault、つまりnullを返します。</p>
<pre class="code lang-cs" data-lang="cs" data-unlink> <span class="synComment">// setup</span>
var fooMock = <span class="synStatement">new</span> Mock<IFoo>();
<span class="synComment">// when</span>
<span class="synComment">// fooMock.Setup(m => m.Bar.Baz.Message).Returns("Hello Chain");</span>
<span class="synComment">// then</span>
IFoo target = fooMock.Object;
Assert.That(() => target.Bar.Baz.Message, Throws.TypeOf<NullReferenceException>());
</pre>
<h1 id="検証コード">検証コード</h1>
<ul>
<li><a class="keyword" href="http://d.hatena.ne.jp/keyword/NUnit">NUnit</a>で検証</li>
<li><code>IFoo</code>, <code>IBar</code>, <code>IBaz</code> のinterfaceも定義</li>
</ul>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Ftsgcpp%2FUnityMoqSample%2Fblob%2Fmain%2FAssets%2FTests%2FMoqExamples%2FMoqPropertyChainTest.cs" title="UnityMoqSample/MoqPropertyChainTest.cs at main · tsgcpp/UnityMoqSample" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://github.com/tsgcpp/UnityMoqSample/blob/main/Assets/Tests/MoqExamples/MoqPropertyChainTest.cs">github.com</a></cite></p>
<h1 id="雑感">雑感</h1>
<p>Moqを使ったことない同僚の方に、自分のコードを参考にMoqを使ってもらったら、<br />
上記のようにメソッドチェーンでまるごとモック化する使い方をされていて発覚しました。</p>
<p>Moq使いだして2年以上、全然気づきませんでした。。。
先入観って怖いですね(笑)。</p>
<p>それでは~</p>
tsgcpp
【Unity, VSCode】Visual Studio Code Editor 1.2.5 への更新のススメ
hatenablog://entry/13574176438062655653
2022-02-12T19:54:18+09:00
2022-02-12T19:54:18+09:00 概要 対象Unity 1.2.4の問題 1.2.5のススメ 雑感 概要 Visual Studio Code Editor 1.2.5 が 02/09 (日本だと2/10) に公開されたため、 1.2.4の問題と合わせて、1.2.5を紹介したいと思います。 今回はVSCode愛用者向けの記事となります。 対象Unity Unity 2020 or later Unity 2021 or later 1.2.4の問題 Windowsの場合に Package Manager経由で取り込んだ(Packages/ 以下の)ソースコードへの参照が機能しないバグがあったこと! github.com Fin…
<ul class="table-of-contents">
<li><a href="#概要">概要</a></li>
<li><a href="#対象Unity">対象Unity</a></li>
<li><a href="#124の問題">1.2.4の問題</a></li>
<li><a href="#125のススメ">1.2.5のススメ</a></li>
<li><a href="#雑感">雑感</a></li>
</ul>
<h1 id="概要">概要</h1>
<p><a class="keyword" href="http://d.hatena.ne.jp/keyword/Visual%20Studio%20Code">Visual Studio Code</a> Editor 1.2.5 が 02/09 (日本だと2/10) に公開されたため、
1.2.4の問題と合わせて、1.2.5を紹介したいと思います。</p>
<p>今回は<a class="keyword" href="http://d.hatena.ne.jp/keyword/VSCode">VSCode</a>愛用者向けの記事となります。</p>
<h1 id="対象Unity">対象Unity</h1>
<ul>
<li>Unity 2020 or later</li>
<li>Unity 2021 or later</li>
</ul>
<h1 id="124の問題">1.2.4の問題</h1>
<p><strong> <a class="keyword" href="http://d.hatena.ne.jp/keyword/Windows">Windows</a>の場合に Package Manager経由で取り込んだ(<code>Packages/</code> 以下の)<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%BD%A1%BC%A5%B9%A5%B3%A1%BC%A5%C9">ソースコード</a>への参照が機能しないバグがあったこと!</strong></p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2FUnity-Technologies%2Fcom.unity.ide.vscode%2Fissues%2F4" title="Invalid Compile Include path for built-in packages · Issue #4 · Unity-Technologies/com.unity.ide.vscode" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://github.com/Unity-Technologies/com.unity.ide.vscode/issues/4">github.com</a></cite></p>
<p><code>Find All References</code>などでinterfaceや実装の確認もできず、コードのSuggestionも機能しないため、
<a class="keyword" href="http://d.hatena.ne.jp/keyword/VSCode">VSCode</a>ユーザーにとって、コーディングの効率を損なう問題がありました。。。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20220212/20220212193601.png" alt="f:id:tsgcpp:20220212193601p:plain" width="884" height="165" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p>一応、<u>1.2.3 にダウングレードすれば上記問題は回避はできました</u>が、<br />
ただ、最新のUnity2020とUnity2021では標準で1.2.4がインストールされるため、煩わしさがありました。</p>
<p>ちなみにですが、1.2.4は 2021/09/01 にリリースされたので5ヶ月は放置されていた問題になります。。。</p>
<h1 id="125のススメ">1.2.5のススメ</h1>
<p>っということで<span style="color: #ff0000"><strong>結論として1.2.5へ更新しましょう!</strong></span></p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20220212/20220212193246.png" alt="f:id:tsgcpp:20220212193246p:plain" width="1101" height="293" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p>ちゃんと<a class="keyword" href="http://d.hatena.ne.jp/keyword/Windows">Windows</a>で参照の問題が解決されていました。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20220212/20220212192238.png" alt="f:id:tsgcpp:20220212192238p:plain" width="848" height="244" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p>パッケージ更新後は Preferences -> External Tools -> Regenerate project files を忘れずに!</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20220212/20220212193812.png" alt="f:id:tsgcpp:20220212193812p:plain" width="1080" height="427" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p>ちなみに以下のプルリクが問題の修正箇所です。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2FUnity-Technologies%2Fcom.unity.ide.vscode%2Fpull%2F6%2Ffiles" title="Fix paths for source files during project generation by goncalo · Pull Request #6 · Unity-Technologies/com.unity.ide.vscode" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://github.com/Unity-Technologies/com.unity.ide.vscode/pull/6/files">github.com</a></cite></p>
<p><span style="color: #cccccc">この1行の修正を5ヶ月待ったことになります。。。</span></p>
<h1 id="雑感">雑感</h1>
<p>今Addressablesに関する記事を書いているんですが、
資料をまとめるのに時間がかかりそうなので、1つ軽めの記事を書いてみました。</p>
<p>多分、この記事のタイトル見て「さっさとRiderにしたら?」って思った方もいらっしゃるかもですね。</p>
<p>正直<a class="keyword" href="http://d.hatena.ne.jp/keyword/VSCode">VSCode</a>に出会ってから、<a class="keyword" href="http://d.hatena.ne.jp/keyword/VSCode">VSCode</a>の魅力に取りつかれて手放せなくなっています。</p>
<p>ちなみに僕の<a class="keyword" href="http://d.hatena.ne.jp/keyword/VSCode">VSCode</a>を主な用途は以下です。</p>
<ul>
<li>Unity含む<a class="keyword" href="http://d.hatena.ne.jp/keyword/C%23">C#</a>のコーディングと<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C7%A5%D0%A5%C3%A5%B0">デバッグ</a></li>
<li>テキストファイル(mdなど)の編集</li>
<li><a class="keyword" href="http://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Actionsのymlファイル作成</li>
<li><strong><a class="keyword" href="http://d.hatena.ne.jp/keyword/Blender">Blender</a>のAddon(<a class="keyword" href="http://d.hatena.ne.jp/keyword/Python">Python</a>)作成</strong></li>
</ul>
<p>特に<a class="keyword" href="http://d.hatena.ne.jp/keyword/Blender">Blender</a>のアドオンの作りやすさは感動を覚えるレベルです!</p>
<p>もちろん、豊富なアドオンがあることも理由の一つですね。</p>
<p>そんな<a class="keyword" href="http://d.hatena.ne.jp/keyword/VSCode">VSCode</a>ですが、Unityでのバグが数ヶ月放置されていたのでやっと解消されてホッとしてます。</p>
<p>みなさんも、良い<a class="keyword" href="http://d.hatena.ne.jp/keyword/VSCode">VSCode</a>ライフを!</p>
<p>それでは~</p>
tsgcpp
【Unity】Transparentシェーダー レンダリングTips (RenderQueue編)
hatenablog://entry/13574176438046497747
2021-12-27T23:56:52+09:00
2021-12-27T23:56:52+09:00 概要 動作環境 検証対象のTransparentシェーダー Transparentシェーダーの特徴 アルファブレンドの場合、描画の順番で結果が異なる アルファブレンドは描画順で結果が変わる理由 加算ブレンドのみの場合、描画の順番に関わらず結果が同じ RenderQueueが2501以上の場合、ソートが発生 おまけ1: BlendingMode Additiveの場合 おまけ2: 赤がAlpha, 緑がAdditiveの場合 デプス (深度) を通常は書き込まない デプスを書き込まない場合と書き込む場合との違い Transparentシェーダーパフォーマンス サンプルプロジェクト 雑感 概要 U…
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20211227/20211227024159.jpg" alt="f:id:tsgcpp:20211227024159j:plain" width="480" height="270" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<ul class="table-of-contents">
<li><a href="#概要">概要</a></li>
<li><a href="#動作環境">動作環境</a></li>
<li><a href="#検証対象のTransparentシェーダー">検証対象のTransparentシェーダー</a></li>
<li><a href="#Transparentシェーダーの特徴">Transparentシェーダーの特徴</a><ul>
<li><a href="#アルファブレンドの場合描画の順番で結果が異なる">アルファブレンドの場合、描画の順番で結果が異なる</a><ul>
<li><a href="#アルファブレンドは描画順で結果が変わる理由">アルファブレンドは描画順で結果が変わる理由</a></li>
</ul>
</li>
<li><a href="#加算ブレンドのみの場合描画の順番に関わらず結果が同じ">加算ブレンドのみの場合、描画の順番に関わらず結果が同じ</a></li>
<li><a href="#RenderQueueが2501以上の場合ソートが発生">RenderQueueが2501以上の場合、ソートが発生</a><ul>
<li><a href="#おまけ1-BlendingMode-Additiveの場合">おまけ1: BlendingMode Additiveの場合</a></li>
<li><a href="#おまけ2-赤がAlpha-緑がAdditiveの場合">おまけ2: 赤がAlpha, 緑がAdditiveの場合</a></li>
</ul>
</li>
<li><a href="#デプス-深度-を通常は書き込まない">デプス (深度) を通常は書き込まない</a><ul>
<li><a href="#デプスを書き込まない場合と書き込む場合との違い">デプスを書き込まない場合と書き込む場合との違い</a></li>
</ul>
</li>
</ul>
</li>
<li><a href="#Transparentシェーダーパフォーマンス">Transparentシェーダーパフォーマンス</a></li>
<li><a href="#サンプルプロジェクト">サンプルプロジェクト</a></li>
<li><a href="#雑感">雑感</a></li>
</ul>
<h1 id="概要">概要</h1>
<p>UnityにおけるTransparentシェーダー(透過シェーダー)に関するTipsです。</p>
<p>Transparentシェーダーは3DCGにおいてよく使われる一方で昔から非常に悩ましい存在であったりします。<br />
特に描画順の関係で思ったような絵にならなかったというのはよくあるパターンだと思います。</p>
<p>Unityでの透過オブジェクトを利用する場合の参考にしていただければと思います。</p>
<h1 id="動作環境">動作環境</h1>
<ul>
<li><a class="keyword" href="http://d.hatena.ne.jp/keyword/Windows">Windows</a> 10</li>
<li>Unity 2021.2.6f1</li>
<li>Universal RP 12.1.2</li>
</ul>
<h1 id="検証対象のTransparentシェーダー">検証対象のTransparentシェーダー</h1>
<ul>
<li>Shader Graphを使用</li>
<li><code>_Color</code> プロパティのみを持ち、アルファも使用</li>
</ul>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20211226/20211226235733.png" alt="f:id:tsgcpp:20211226235733p:plain" width="1100" height="470" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<h1 id="Transparentシェーダーの特徴">Transparentシェーダーの特徴</h1>
<h2 id="アルファブレンドの場合描画の順番で結果が異なる">アルファ<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D6%A5%EC%A5%F3%A5%C9">ブレンド</a>の場合、描画の順番で結果が異なる</h2>
<p>デフォルトの<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D6%A5%EC%A5%F3%A5%C9">ブレンド</a>設定であるアルファ<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D6%A5%EC%A5%F3%A5%C9">ブレンド</a>(Blending Mode が Alpha)の場合、オブジェクトの描画順で結果が異なります。</p>
<p>アルファ0.4の赤と緑のシェーダーを使ったオブジェクトを描画順を変えて見てみましょう。</p>
<p><figure class="figure-image figure-image-fotolife" title="緑 -> 赤"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20211227/20211227011002.jpg" alt="f:id:tsgcpp:20211227011002j:plain" width="640" height="480" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>緑 -> 赤</figcaption></figure></p>
<p><figure class="figure-image figure-image-fotolife" title="赤 -> 緑"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20211227/20211227011020.jpg" alt="f:id:tsgcpp:20211227011020j:plain" width="640" height="480" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>赤 -> 緑</figcaption></figure></p>
<p>上記のように描画順で描画結果が異なります。</p>
<p><strong><span style="color: #ff0000">Transparentシェーダーはアルファ<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D6%A5%EC%A5%F3%A5%C9">ブレンド</a>の場合に描画順で結果が変わる</span></strong>ということを覚えておきましょう!<br />
基本的なことですが、とても重要な性質になります。</p>
<h3 id="アルファブレンドは描画順で結果が変わる理由">アルファ<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D6%A5%EC%A5%F3%A5%C9">ブレンド</a>は描画順で結果が変わる理由</h3>
<p>簡単に説明するとアルファ<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D6%A5%EC%A5%F3%A5%C9">ブレンド</a>の計算式のためです。</p>
<pre class="code" data-lang="" data-unlink>R * a + G * (1 - a) => 0.4R + 0.6G (緑 -> 赤の順番)</pre>
<pre class="code" data-lang="" data-unlink>G * a + R * (1 - a) => 0.6R + 0.4G (赤 -> 緑の順番)</pre>
<p>※上記はイメージのための計算式で実際は <code>R * Ra + (G * Ga + Dest * (1 - Ga)) * (1 - Ra)</code> のような計算</p>
<h2 id="加算ブレンドのみの場合描画の順番に関わらず結果が同じ">加算<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D6%A5%EC%A5%F3%A5%C9">ブレンド</a>のみの場合、描画の順番に関わらず結果が同じ</h2>
<p>加算<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D6%A5%EC%A5%F3%A5%C9">ブレンド</a>(Blending Mode が Additive)のマテリアル同士の場合は描画順に関わらず結果が同じになります。<br />
<strong>加算<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D6%A5%EC%A5%F3%A5%C9">ブレンド</a>のみの場合</strong>ということに注意してください。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20211227/20211227012441.jpg" alt="f:id:tsgcpp:20211227012441j:plain" width="640" height="480" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p>アルファの乗算色を単純に加算するため、計算の順番で結果が変わらないからです。</p>
<pre class="code" data-lang="" data-unlink>R * Ra + G * Ga(緑 -> 赤の順番) = G * Ga + R * Ra(赤 -> 緑の順番)
* Ra: 赤のアルファ値, Ga: 緑のアルファ値</pre>
<h2 id="RenderQueueが2501以上の場合ソートが発生">RenderQueueが2501以上の場合、ソートが発生</h2>
<p><span style="color: #ff0000"><strong>RenderQueue 2501以上かつ同一のRenderQueueの場合はカメラから遠いオブジェクトから近いオブジェクトの順番で描画</strong></span>する処理が発生します。</p>
<p>透明オブジェクトはカメラから遠いオブジェクトから近いオブジェクトの順番で描画したほうが結果が安定します。<br />
Unityはデフォルトでソートを実施して、遠->近の順番でオブジェクトを描画してくれています。</p>
<p><blockquote data-conversation="none" class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">UnityのTransparentシェーダー + BlendingMode AlphaでRenderQueueを変えたときの描画結果 <a href="https://t.co/PjsRwgjHBz">pic.twitter.com/PjsRwgjHBz</a></p>— すぎしー (@tsgcpp) <a href="https://twitter.com/tsgcpp/status/1475149171577724928?ref_src=twsrc%5Etfw">2021年12月26日</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> </p>
<p>※どちらもアルファは1.0</p>
<p>ポイントは以下です。</p>
<ul>
<li>RenderQueue 2501以上かつ同一のRenderQueueのマテリアルを持つRendererが複数ある場合はソートが発生
<ul>
<li>カメラから見て遠いオブジェクトから近いオブジェクトの順番で描画</li>
<li>ソートの基準位置はオブジェクトのTransformのposition (オブジェクトのピボット)</li>
</ul>
</li>
<li><strong>異なるRenderQueueのマテリアル間ではソートは発生しない</strong>
<ul>
<li>異なる場合はRenderQueueが小 -> 大の順番で描画</li>
</ul>
</li>
<li>RenderQueue 2500以下の場合はソートは発生しない
<ul>
<li>不透明オブジェクトはデプステストにより基本的に描画結果が安定するため</li>
</ul>
</li>
</ul>
<p><a href="https://docs.unity3d.com/ScriptReference/TransparencySortMode.html">TransparencySortMode</a> の説明にも以下のように記載されています。</p>
<pre class="code" data-lang="" data-unlink>By default, perspective cameras sort objects based on distance from camera position to the object center;</pre>
<h3 id="おまけ1-BlendingMode-Additiveの場合">おまけ1: BlendingMode Additiveの場合</h3>
<p>描画順に関わらず結果が一定になります。</p>
<p><blockquote data-conversation="none" class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">BlendingMode Additive の場合。ずっと同じ <a href="https://t.co/u7BRj6ph5I">pic.twitter.com/u7BRj6ph5I</a></p>— すぎしー (@tsgcpp) <a href="https://twitter.com/tsgcpp/status/1475149433960820736?ref_src=twsrc%5Etfw">2021年12月26日</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> </p>
<p>※どちらもアルファは1.0</p>
<h3 id="おまけ2-赤がAlpha-緑がAdditiveの場合">おまけ2: 赤がAlpha, 緑がAdditiveの場合</h3>
<p>全体がAdditiveではない場合は結果が不安定になります。</p>
<p><strong>マテリアルにBlendingMode Alphaが存在する場合はRenderQueueを意識</strong>する必要があります。</p>
<p><blockquote data-conversation="none" class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">おまけ、赤がAlphaで緑がAdditiveの場合 <a href="https://t.co/HQyGzn35lV">pic.twitter.com/HQyGzn35lV</a></p>— すぎしー (@tsgcpp) <a href="https://twitter.com/tsgcpp/status/1475149611899957249?ref_src=twsrc%5Etfw">2021年12月26日</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> </p>
<p>※どちらもアルファは1.0</p>
<h2 id="デプス-深度-を通常は書き込まない">デプス (深度) を通常は書き込まない</h2>
<p>Transparentシェーダーはデプスを書き込まないように設定することが一般的です。<br />
こちらも描画順で結果が不安定になるためです。</p>
<ul>
<li>Transparentシェーダーはデフォルト(<code>Depth Write: Auto</code>)の場合はデプスを書き込まない</li>
<li>意図的に書き込むことは可能
<ul>
<li><code>ZWrite On</code> (<code>Depth Write: ForceEnabled</code>) をシェーダー or マテリアルで指定</li>
</ul>
</li>
</ul>
<h3 id="デプスを書き込まない場合と書き込む場合との違い">デプスを書き込まない場合と書き込む場合との違い</h3>
<p>例えば以下のようにオブジェクトを配置した場合を見てみましょう。</p>
<ul>
<li>赤Cubeと緑<a class="keyword" href="http://d.hatena.ne.jp/keyword/Sphere">Sphere</a>を重なるように配置</li>
<li>両マテリアルのアルファは0.4</li>
<li>両マテリアルのRenderQueueは3000</li>
<li>緑<a class="keyword" href="http://d.hatena.ne.jp/keyword/Sphere">Sphere</a>のほうがカメラに近い
<ul>
<li>遠い赤Cube -> 近い緑<a class="keyword" href="http://d.hatena.ne.jp/keyword/Sphere">Sphere</a>の順番で描画</li>
</ul>
</li>
</ul>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20211227/20211227232124.png" alt="f:id:tsgcpp:20211227232124p:plain" width="506" height="296" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p><figure class="figure-image figure-image-fotolife" title="デプスを書き込まない場合"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20211227/20211227232327.jpg" alt="f:id:tsgcpp:20211227232327j:plain" width="480" height="270" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>デプスを書き込まない場合</figcaption></figure>
<figure class="figure-image figure-image-fotolife" title="デプスを書き込む場合"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20211227/20211227232356.jpg" alt="f:id:tsgcpp:20211227232356j:plain" width="480" height="270" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>デプスを書き込む場合</figcaption></figure></p>
<p>デプスを書き込んだ場合、重なった部分の緑側が描画されていません。<br />
これは赤 -> 緑と描画されて、赤側でデプスが書き込まれたため後続の緑側の深度テストにより<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D4%A5%AF%A5%BB%A5%EB">ピクセル</a>シェーダーがスキップされたためです。</p>
<p>意図的にこちらの現象を利用することもありますが、基本的にはTransparentシェーダーはデプスを書き込まないのが一般的です。</p>
<h1 id="Transparentシェーダーパフォーマンス">Transparentシェーダーパフォーマンス</h1>
<p>※本記事では詳しくは取り上げません。</p>
<p>ここまで読んだ方の中にはパフォーマンス面が気になった方もいらっしゃるかもしれません。<br />
Transparentシェーダーは<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D4%A5%AF%A5%BB%A5%EB">ピクセル</a>シェーダー処理が発生しやすい性質上、パフォーマンス面で注意が必要です。</p>
<p>簡単に言えばオーバードローという現象が確実に発生するようなものです。<br />
安易にUIやパーティクルなどで使用すると描画負荷が一気に上がりますためご注意ください!</p>
<p>また、ソート処理も発生するためCPU側にも影響があると考えられます。</p>
<p>パフォーマンスに関しては別の機会に取り上げようと思います。</p>
<h1 id="サンプルプロジェクト">サンプルプロジェクト</h1>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Ftsgcpp%2FTransparencySortTest" title="GitHub - tsgcpp/TransparencySortTest: Transparent tests about the sort order in Unity" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://github.com/tsgcpp/TransparencySortTest">github.com</a></cite></p>
<h1 id="雑感">雑感</h1>
<p>今年最後の技術ブログになります。<br />
今回は自分の復習も兼ねて3DCGっぽい記事にしてみました。</p>
<p>というかここ数ヶ月、3DCGっぽくない記事が多かったですね。。。</p>
<p>昔のUnityでは透明マテリアルはRenderQueue 2450だった気がしますが、いつの間にか3000になってましたね。<br />
パフォーマンス面については別の機会に紹介しようとは思いますが、いつになることやら。。。</p>
<p>Unity 2021.2ではShader Graphに <code>Allow Material Override</code> でBlending Modeを切り替えれたり、URPで<a class="keyword" href="http://d.hatena.ne.jp/keyword/VFX">VFX</a>が使えたりと2020.3と比べて一気に使いやすさが上がっている気がしています。</p>
<p>そういえば、サンプルプロジェクトにTimelineでマテリアルをいじる簡易的なカスタムTrackも入れているので良かったら参考にどうぞ(<a class="keyword" href="http://d.hatena.ne.jp/keyword/Preview">Preview</a>機能とか色々微妙ですが)。</p>
<p>来年もUnityを追求していきたいです!</p>
<p>それでは、良いお年を~</p>
tsgcpp
【Unity C#】 IReadOnlyListとアロケーション
hatenablog://entry/13574176438027827962
2021-11-21T00:19:27+09:00
2021-11-21T00:19:27+09:00 概要 環境 IReadOnlyList の アロケーション発生ポイント foreach使用時などのIEnumerable.GetEnumerator アロケーションの原因 アロケーションの回避方法 IEquatable<T>を実装しない値型(struct)でEquals アロケーションの原因 アロケーションの回避方法 IReadOnlyListでLinq.Enumerable.Contains アロケーションの回避方法 アロケーションの原因 共変性変換のIReadOnlyListに対してLinq.Enumerable.Contains IEquatable<T>非継承のstructを型とするL…
<ul class="table-of-contents">
<li><a href="#概要">概要</a></li>
<li><a href="#環境">環境</a></li>
<li><a href="#IReadOnlyList-の-アロケーション発生ポイント">IReadOnlyList の アロケーション発生ポイント</a><ul>
<li><a href="#foreach使用時などのIEnumerableGetEnumerator">foreach使用時などのIEnumerable.GetEnumerator</a><ul>
<li><a href="#アロケーションの原因">アロケーションの原因</a></li>
<li><a href="#アロケーションの回避方法">アロケーションの回避方法</a></li>
</ul>
</li>
<li><a href="#IEquatableTを実装しない値型structでEquals">IEquatable<T>を実装しない値型(struct)でEquals</a><ul>
<li><a href="#アロケーションの原因-1">アロケーションの原因</a></li>
<li><a href="#アロケーションの回避方法-1">アロケーションの回避方法</a></li>
</ul>
</li>
<li><a href="#IReadOnlyListでLinqEnumerableContains">IReadOnlyListでLinq.Enumerable.Contains</a><ul>
<li><a href="#アロケーションの回避方法-2">アロケーションの回避方法</a></li>
<li><a href="#アロケーションの原因-2">アロケーションの原因</a><ul>
<li><a href="#共変性変換のIReadOnlyListに対してLinqEnumerableContains">共変性変換のIReadOnlyListに対してLinq.Enumerable.Contains</a></li>
<li><a href="#IEquatableT非継承のstructを型とするListでLinqEnumerableContains">IEquatable<T>非継承のstructを型とするListでLinq.Enumerable.Contains</a></li>
<li><a href="#IEqualityComparerT指定有りでLinqEnumerableContains">IEqualityComparer<T>指定有りでLinq.Enumerable.Contains</a></li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li><a href="#Unity-Test-Runnerによるアロケーション確認">Unity Test Runnerによるアロケーション確認</a><ul>
<li><a href="#ListFoo">List<Foo></a></li>
<li><a href="#ListFoo---IReadOnlyListFoo">List<Foo> -> IReadOnlyList<Foo></a></li>
<li><a href="#ListFoo---IReadOnlyListIFoo">List<Foo> -> IReadOnlyList<IFoo></a></li>
<li><a href="#ListSimpleData---IReadOnlyListSimpleData">List<SimpleData> -> IReadOnlyList<SimpleData></a></li>
<li><a href="#ListEquatableData---IReadOnlyListEquatableData">List<EquatableData> -> IReadOnlyList<EquatableData></a></li>
</ul>
</li>
<li><a href="#サンプルコード">サンプルコード</a><ul>
<li><a href="#追記">追記</a></li>
</ul>
</li>
<li><a href="#参考">参考</a></li>
<li><a href="#雑感">雑感</a></li>
</ul>
<h1 id="概要">概要</h1>
<p>今回は<code>IReadOnlyList</code>と <a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A2%A5%ED%A5%B1%A1%BC%A5%B7%A5%E7%A5%F3">アロケーション</a>について深堀りしようと思います。</p>
<p>前回の記事でも述べましたが、<code>IReadOnlyList</code>は<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A2%A5%ED%A5%B1%A1%BC%A5%B7%A5%E7%A5%F3">アロケーション</a>を回避したい場合は注意が必要なinterfaceになっていたりします。<br />
そんな<code>IReadOnlyList</code>の<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A2%A5%ED%A5%B1%A1%BC%A5%B7%A5%E7%A5%F3">アロケーション</a>について調べたので共有します。</p>
<p><code>IReadOnlyList</code>については前回の記事で少し紹介していますので合わせてどうぞ!</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftsgcpp.hateblo.jp%2Fentry%2F2021%2F10%2F30%2F184507" title="【Unity C#】 IReadOnlyListの紹介 - すぎしーのXRと3DCG" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://tsgcpp.hateblo.jp/entry/2021/10/30/184507">tsgcpp.hateblo.jp</a></cite></p>
<h1 id="環境">環境</h1>
<ul>
<li>Unity 2021.2.2f1</li>
<li>Mono</li>
<li><a class="keyword" href="http://d.hatena.ne.jp/keyword/.Net%20Framework">.Net Framework</a> (<a class="keyword" href="http://d.hatena.ne.jp/keyword/API">API</a> Comptibility Level)</li>
</ul>
<h1 id="IReadOnlyList-の-アロケーション発生ポイント">IReadOnlyList の <a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A2%A5%ED%A5%B1%A1%BC%A5%B7%A5%E7%A5%F3">アロケーション</a>発生ポイント</h1>
<h2 id="foreach使用時などのIEnumerableGetEnumerator">foreach使用時などのIEnumerable.GetEnumerator</h2>
<p><code>List</code>を<code>IReadOnlyList</code> に変換して<code>foreach</code>に回すと使用するたびに<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A2%A5%ED%A5%B1%A1%BC%A5%B7%A5%E7%A5%F3">アロケーション</a>が発生します。</p>
<pre class="code lang-cs" data-lang="cs" data-unlink> List<Foo> list = <span class="synStatement">new</span> List<Foo>();
<span class="synStatement">foreach</span> (var item <span class="synStatement">in</span> list) { } <span class="synComment">// アロケーションは発生しない</span>
IReadOnlyList<Foo> readonlyList = list;
<span class="synStatement">foreach</span> (var item <span class="synStatement">in</span> readonlyList) { } <span class="synComment">// アロケーションが発生する</span>
</pre>
<h3 id="アロケーションの原因"><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A2%A5%ED%A5%B1%A1%BC%A5%B7%A5%E7%A5%F3">アロケーション</a>の原因</h3>
<p><code>List</code>の場合は<code>Enumerable</code>(値型)の<a class="keyword" href="http://d.hatena.ne.jp/keyword/boxing">boxing</a>が原因です。</p>
<pre class="code lang-cs" data-lang="cs" data-unlink> <span class="synType">public</span> Enumerator GetEnumerator() {
<span class="synStatement">return</span> <span class="synStatement">new</span> Enumerator(<span class="synStatement">this</span>);
}
</pre>
<p><a href="https://referencesource.microsoft.com/#mscorlib/system/collections/generic/list.cs,569">list.cs,569</a> より</p>
<p><code>List</code>の<code>GetEnumerator</code>が返す<code>Enumerable</code>は値型のため、<code>List</code>を直接使用する場合は<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A2%A5%ED%A5%B1%A1%BC%A5%B7%A5%E7%A5%F3">アロケーション</a>は発生しません。</p>
<p>一方で、<code>IReadOnlyList</code>に変換した場合の<code>GetEnumerator</code>は<code>IEnumerable</code>として<code>Enumerable</code>を返します。</p>
<pre class="code lang-cs" data-lang="cs" data-unlink> IEnumerator<T> IEnumerable<T>.GetEnumerator() {
<span class="synStatement">return</span> <span class="synStatement">new</span> Enumerator(<span class="synStatement">this</span>);
}
</pre>
<p><a href="https://referencesource.microsoft.com/#mscorlib/system/collections/generic/list.cs,574">list.cs,574</a> より</p>
<p>この戻り値の<code>Enumerable</code>から<code>IEnumerable</code>は値型から参照型への変換、つまり<a class="keyword" href="http://d.hatena.ne.jp/keyword/boxing">boxing</a>が発生するため<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A2%A5%ED%A5%B1%A1%BC%A5%B7%A5%E7%A5%F3">アロケーション</a>が発生します。</p>
<h3 id="アロケーションの回避方法"><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A2%A5%ED%A5%B1%A1%BC%A5%B7%A5%E7%A5%F3">アロケーション</a>の回避方法</h3>
<p><code>foreach</code> ではなく <code>for</code> を使用すれば回避可能です。</p>
<pre class="code lang-cs" data-lang="cs" data-unlink> <span class="synStatement">for</span> (<span class="synType">int</span> i = <span class="synConstant">0</span>; i < readonlyList.Count; ++i)
{
var item = readonlyList[i];
}
</pre>
<p><code>IEnumerable</code>と異なり<code>IReadOnlyList</code>は<code>list[i]</code>が配列の要素に直接アクセスする形となっています。</p>
<p><a href="https://referencesource.microsoft.com/#mscorlib/system/collections/generic/list.cs,1107">list.cs,1107</a></p>
<p>Listを<code>IEnumerable</code>(or <code>IReadOnlyCollection</code>)に変換してしまうと<code>for</code>が使用不可で<code>GetEnumerator</code>による<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A2%A5%ED%A5%B1%A1%BC%A5%B7%A5%E7%A5%F3">アロケーション</a>は避けられなくなりますが
<code>IReadOnlyList</code>への変換であれば読込専用を保ちつつ<code>for</code>が使用可能なため結果的に<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A2%A5%ED%A5%B1%A1%BC%A5%B7%A5%E7%A5%F3">アロケーション</a>の回避が可能です。</p>
<p>コーディングの煩わしさはありますが、メモリ的には優しくなります。</p>
<h2 id="IEquatableTを実装しない値型structでEquals">IEquatable<T>を実装しない値型(struct)でEquals</h2>
<p><code>IEquatable<T></code>を継承していない値型は<code>Equals</code>の挙動により<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A2%A5%ED%A5%B1%A1%BC%A5%B7%A5%E7%A5%F3">アロケーション</a>が発生するパターンが多いです。</p>
<p><code>IReadOnlyList</code>とは直接は関係ありませんが、後述する<code>Linq.Enumerable.Contains</code>に関わってくるため紹介します。</p>
<pre class="code lang-cs" data-lang="cs" data-unlink> <span class="synType">public</span> <span class="synType">struct</span> SimpleData
{
<span class="synType">public</span> <span class="synType">int</span> <span class="synStatement">value</span>;
}
</pre>
<h3 id="アロケーションの原因-1"><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A2%A5%ED%A5%B1%A1%BC%A5%B7%A5%E7%A5%F3">アロケーション</a>の原因</h3>
<p><a class="keyword" href="http://d.hatena.ne.jp/keyword/C%23">C#</a>の型に定義されている<code>ValueType.Equals(Object)</code>は引数にobject型を受け取ります。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdocs.microsoft.com%2Fen-us%2Fdotnet%2Fapi%2Fsystem.valuetype.equals%3Fview%3Dnet-5.0%23System_ValueType_Equals_System_Object_" title="ValueType.Equals(Object) Method (System)" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://docs.microsoft.com/en-us/dotnet/api/system.valuetype.equals?view=net-5.0#System_ValueType_Equals_System_Object_">docs.microsoft.com</a></cite></p>
<p>つまり、このEqualsに値型を渡すと値型から参照型への変換で<a class="keyword" href="http://d.hatena.ne.jp/keyword/boxing">boxing</a>が発生し<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A2%A5%ED%A5%B1%A1%BC%A5%B7%A5%E7%A5%F3">アロケーション</a>に繋がります。</p>
<h3 id="アロケーションの回避方法-1"><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A2%A5%ED%A5%B1%A1%BC%A5%B7%A5%E7%A5%F3">アロケーション</a>の回避方法</h3>
<p>主に2つあります。</p>
<ul>
<li>structに<code>IEquatable<T></code>を継承して実装
<ul>
<li><code>Equals(T value)</code>で型指定となるため<a class="keyword" href="http://d.hatena.ne.jp/keyword/boxing">boxing</a>が発生しない</li>
</ul>
</li>
<li><code>IEqualityComparer<T></code>を継承したオブジェクトを実装して使用
<ul>
<li><code>Equals(T x, T y)</code>で型指定となるため<a class="keyword" href="http://d.hatena.ne.jp/keyword/boxing">boxing</a>が発生しない</li>
</ul>
</li>
</ul>
<pre class="code lang-cs" data-lang="cs" data-unlink> <span class="synType">public</span> <span class="synType">struct</span> EquatableData : IEquatable<EquatableData>
{
<span class="synType">public</span> <span class="synType">int</span> <span class="synStatement">value</span>;
<span class="synType">public</span> <span class="synType">bool</span> Equals(EquatableData other)
{
<span class="synStatement">return</span> <span class="synStatement">this</span>.<span class="synStatement">value</span> == other.<span class="synStatement">value</span>;
}
...
}
</pre>
<h2 id="IReadOnlyListでLinqEnumerableContains">IReadOnlyListで<a class="keyword" href="http://d.hatena.ne.jp/keyword/Linq">Linq</a>.Enumerable.Contains</h2>
<pre class="code lang-cs" data-lang="cs" data-unlink><span class="synStatement">using</span> System.Linq;
...
readonlyList.Contains(item);
</pre>
<p><code>IReadOnlyList</code>は残念ながら<code>Contains</code>メソッドを持っていません(<code>List.Contains</code>は定義されている)。<br />
拡張メソッド<code>Linq.Enumerable.Contains</code>を使用することで同様の機能を使用できます。</p>
<p>しかし、<code>Linq.Enumerable.Contains</code>は<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A2%A5%ED%A5%B1%A1%BC%A5%B7%A5%E7%A5%F3">アロケーション</a>が発生するパターンが多いです。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdocs.microsoft.com%2Fen-us%2Fdotnet%2Fapi%2Fsystem.linq.enumerable.contains%3Fview%3Dnetframework-4.8" title="Enumerable.Contains Method (System.Linq)" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://docs.microsoft.com/en-us/dotnet/api/system.linq.enumerable.contains?view=netframework-4.8">docs.microsoft.com</a></cite></p>
<h3 id="アロケーションの回避方法-2"><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A2%A5%ED%A5%B1%A1%BC%A5%B7%A5%E7%A5%F3">アロケーション</a>の回避方法</h3>
<p><code>Linq.Enumerable.Contains</code> の<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A2%A5%ED%A5%B1%A1%BC%A5%B7%A5%E7%A5%F3">アロケーション</a>は細かい話が多いため先に回避方法を紹介します。<br />
これに関しては<code>IReadOnlyList</code>向けの<code>Contains</code>を実装する必要がありました。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Ftsgcpp%2FAllocationVerification-Unity%2Fblob%2Fmain%2FAssets%2FRuntime%2FSystem.Extension%2FList%2FReadOnlyListExtensions.cs" title="AllocationVerification-Unity/ReadOnlyListExtensions.cs at main · tsgcpp/AllocationVerification-Unity" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://github.com/tsgcpp/AllocationVerification-Unity/blob/main/Assets/Runtime/System.Extension/List/ReadOnlyListExtensions.cs">github.com</a></cite></p>
<p>後述する「<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A2%A5%ED%A5%B1%A1%BC%A5%B7%A5%E7%A5%F3">アロケーション</a>の原因」を
<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A2%A5%ED%A5%B1%A1%BC%A5%B7%A5%E7%A5%F3">アロケーション</a>回避の検証も含めUnitTestも作成しています。</p>
<p>余談ですが、汎用性を保つため<code>where T : struct</code>のような制約は入れていません。<br />
その代わり<code>typeof(T).IsValueType</code>による条件分岐が入っています。</p>
<h3 id="アロケーションの原因-2"><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A2%A5%ED%A5%B1%A1%BC%A5%B7%A5%E7%A5%F3">アロケーション</a>の原因</h3>
<h4 id="共変性変換のIReadOnlyListに対してLinqEnumerableContains">共変性変換のIReadOnlyListに対して<a class="keyword" href="http://d.hatena.ne.jp/keyword/Linq">Linq</a>.Enumerable.Contains</h4>
<p>「共変性を使用したIReadOnlyList」とは要するに<code>List<Foo></code> -> <code>IReadOnlyList<IFoo></code>みたいな変換のことです。
共変性については<a href="https://tsgcpp.hateblo.jp/entry/2021/10/30/184507#IReadOnlyListout-T%E3%81%AE%E3%81%9F%E3%82%81%E5%85%B1%E5%A4%89%E6%80%A7%E6%8C%81%E3%81%A4">前回の記事</a>を参照。</p>
<p>ポイントは <span style="color: #ff0000">共変性を利用して変換した<code>IReadOnlyList</code></span> にあります。</p>
<p>実は <code>List<Foo></code> -> <code>IReadOnlyList<Foo></code> のように不変(Tを<code>Foo</code>のまま変換)の場合は<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A2%A5%ED%A5%B1%A1%BC%A5%B7%A5%E7%A5%F3">アロケーション</a>は発生しません。<br />
共変性変換の場合に<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A2%A5%ED%A5%B1%A1%BC%A5%B7%A5%E7%A5%F3">アロケーション</a>が発生する要因は<code>Linq.Enumerable.Contains</code>の定義にあります。</p>
<pre class="code lang-cs" data-lang="cs" data-unlink> <span class="synType">public</span> <span class="synType">static</span> <span class="synType">bool</span> Contains<TSource>(<span class="synStatement">this</span> IEnumerable<TSource> source, TSource <span class="synStatement">value</span>) {
ICollection<TSource> collection = source <span class="synStatement">as</span> ICollection<TSource>; <span class="synComment">// ICollectionは不変性のため、IReadOnlyList<IFoo>へのキャストは不可のためnullを返す</span>
<span class="synStatement">if</span> (collection != <span class="synConstant">null</span>) <span class="synStatement">return</span> collection.Contains(<span class="synStatement">value</span>);
<span class="synStatement">return</span> Contains<TSource>(source, <span class="synStatement">value</span>, <span class="synConstant">null</span>);
}
<span class="synType">public</span> <span class="synType">static</span> <span class="synType">bool</span> Contains<TSource>(<span class="synStatement">this</span> IEnumerable<TSource> source, TSource <span class="synStatement">value</span>, IEqualityComparer<TSource> comparer)
{
<span class="synStatement">if</span> (comparer == <span class="synConstant">null</span>) comparer = EqualityComparer<TSource>.Default;
<span class="synStatement">if</span> (source == <span class="synConstant">null</span>) <span class="synStatement">throw</span> Error.ArgumentNull(<span class="synConstant">"source"</span>);
<span class="synStatement">foreach</span> (TSource element <span class="synStatement">in</span> source) <span class="synComment">// <- GetEnumerator使用によりboxingによるアロケーションが発生</span>
<span class="synStatement">if</span> (comparer.Equals(element, <span class="synStatement">value</span>)) <span class="synStatement">return</span> <span class="synConstant">true</span>;
<span class="synStatement">return</span> <span class="synConstant">false</span>;
}
</pre>
<p><a href="https://referencesource.microsoft.com/#system.core/system/linq/Enumerable.cs,1364">Enumerable.cs,1364</a> より</p>
<p><code>ICollection<T></code>は共変性(<code>out T</code>)を持たず不変性のため、<code>List<Foo></code> -> <code>ICollection<IFoo></code>は不可となります。<br />
よって <code>IReadOnlyList<IFoo></code> -> <code>ICollection<IFoo></code>の変換も不可のため、上記<code>Contains</code>のコードで<code>source as ICollection<TSource></code>はnullを返します。</p>
<p>その後<code>foreach</code>にたどり着き、「foreachなど<code>IEnumerable</code>を必要とする処理」で述べた<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A2%A5%ED%A5%B1%A1%BC%A5%B7%A5%E7%A5%F3">アロケーション</a>が発生します。</p>
<p>逆に<code>IReadOnlyList<Foo></code>は不変性を満たすため<code>ICollection<Foo></code>にキャストができ<code>foreach</code>に到達しないため<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A2%A5%ED%A5%B1%A1%BC%A5%B7%A5%E7%A5%F3">アロケーション</a>が発生しません。</p>
<p>なんともややこしい。。。</p>
<h4 id="IEquatableT非継承のstructを型とするListでLinqEnumerableContains">IEquatable<T>非継承のstructを型とするListで<a class="keyword" href="http://d.hatena.ne.jp/keyword/Linq">Linq</a>.Enumerable.Contains</h4>
<p><code>IEquatable<T></code>非継承のstructを<code>Linq.Enumerable.Contains</code>に使用した場合は<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A2%A5%ED%A5%B1%A1%BC%A5%B7%A5%E7%A5%F3">アロケーション</a>が発生します。</p>
<pre class="code lang-cs" data-lang="cs" data-unlink> <span class="synType">private</span> <span class="synType">static</span> EqualityComparer<T> CreateComparer() {
...
<span class="synStatement">if</span> (<span class="synStatement">typeof</span>(IEquatable<T>).IsAssignableFrom(t)) {
<span class="synStatement">return</span> (EqualityComparer<T>)RuntimeTypeHandle.CreateInstanceForAnotherGenericParameter((RuntimeType)<span class="synStatement">typeof</span>(GenericEqualityComparer<<span class="synType">int</span>>), t);
}
...
<span class="synStatement">return</span> <span class="synStatement">new</span> ObjectEqualityComparer<T>();
}
</pre>
<p><a href="https://referencesource.microsoft.com/#mscorlib/system/collections/generic/equalitycomparer.cs,40">equalitycomparer.cs,40</a> より</p>
<p><code>IEquatable<T></code>継承の場合は型指定の<code>EqualityComparer<T></code>が生成されますが、<br />
<code>IEquatable<T></code>非継承の場合は<code>ObjectEqualityComparer<T></code>が使用されて<a class="keyword" href="http://d.hatena.ne.jp/keyword/boxing">boxing</a>が発生するんですね。</p>
<h4 id="IEqualityComparerT指定有りでLinqEnumerableContains">IEqualityComparer<T>指定有りで<a class="keyword" href="http://d.hatena.ne.jp/keyword/Linq">Linq</a>.Enumerable.Contains</h4>
<p><code>Linq.Enumerable.Contains</code>は<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%AA%A1%BC%A5%D0%A1%BC%A5%ED%A1%BC%A5%C9">オーバーロード</a>で第2引数に<code>IEqualityComparer<T></code>指定可能なメソッドがあります。<br />
<code>Equals</code>の<a class="keyword" href="http://d.hatena.ne.jp/keyword/boxing">boxing</a>は回避できるんですが、残念ながらその後で使用される<code>foreach</code>の<a class="keyword" href="http://d.hatena.ne.jp/keyword/boxing">boxing</a>は回避できません。。。</p>
<p><a href="https://referencesource.microsoft.com/#system.core/system/linq/Enumerable.cs,1370">Enumerable.cs,1370</a></p>
<p>「<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A2%A5%ED%A5%B1%A1%BC%A5%B7%A5%E7%A5%F3">アロケーション</a>の回避方法」にて記載したコードには、<code>IEqualityComparer<T></code>指定可能な<code>Contains</code>も実装して記載しています。</p>
<h1 id="Unity-Test-Runnerによるアロケーション確認">Unity Test Runnerによる<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A2%A5%ED%A5%B1%A1%BC%A5%B7%A5%E7%A5%F3">アロケーション</a>確認</h1>
<p>今回の<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A2%A5%ED%A5%B1%A1%BC%A5%B7%A5%E7%A5%F3">アロケーション</a>有無の検証ですがUnity Test Runnerを利用しました。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Ftsgcpp%2FAllocationVerification-Unity%2Fblob%2Fmain%2FAssets%2FTests%2FAllocation%2FSystem%2FList%2FTestAllocationIReadOnlyList.TestBase.cs" title="AllocationVerification-Unity/TestAllocationIReadOnlyList.TestBase.cs at main · tsgcpp/AllocationVerification-Unity" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://github.com/tsgcpp/AllocationVerification-Unity/blob/main/Assets/Tests/Allocation/System/List/TestAllocationIReadOnlyList.TestBase.cs">github.com</a></cite></p>
<p>実装開始時は<code>Is.AllocatingGCMemory()</code>と<code>Is.Not.AllocatingGCMemory()</code>を使い分けていましたが、
再利用性(TestBaseクラスからの派生)と視認性のために<code>Is.Not.AllocatingGCMemory()</code>指定で統一しました。</p>
<p>以下の例だと✅で<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A2%A5%ED%A5%B1%A1%BC%A5%B7%A5%E7%A5%F3">アロケーション</a>発生無し、🚫で<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A2%A5%ED%A5%B1%A1%BC%A5%B7%A5%E7%A5%F3">アロケーション</a>発生有りという見方になります。<br />
テストコード的にはおかしいですが、一旦視認性を重視しました(別の視認性が確保しやすい方法が思いついたら修正しておきます)。</p>
<p>また、1st, 2ndは<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>ごとの初回と2回目で<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A2%A5%ED%A5%B1%A1%BC%A5%B7%A5%E7%A5%F3">アロケーション</a>の変化があるかを確認するために入れています。</p>
<h2 id="ListFoo">List<Foo></h2>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20211116/20211116022853.png" alt="f:id:tsgcpp:20211116022853p:plain" width="414" height="140" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<h2 id="ListFoo---IReadOnlyListFoo">List<Foo> -> IReadOnlyList<Foo></h2>
<p><code>foreach</code> のみで発生</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20211116/20211116022823.png" alt="f:id:tsgcpp:20211116022823p:plain" width="435" height="131" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<h2 id="ListFoo---IReadOnlyListIFoo">List<Foo> -> IReadOnlyList<IFoo></h2>
<p><code>foreach</code> および 共変性変換のため<code>Linq.Enumerable.Contains</code>で発生</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20211116/20211116022932.png" alt="f:id:tsgcpp:20211116022932p:plain" width="441" height="131" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<h2 id="ListSimpleData---IReadOnlyListSimpleData">List<SimpleData> -> IReadOnlyList<SimpleData></h2>
<p><code>IEquatable</code>非実装のstruct</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20211120/20211120235310.png" alt="f:id:tsgcpp:20211120235310p:plain" width="412" height="132" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<h2 id="ListEquatableData---IReadOnlyListEquatableData">List<EquatableData> -> IReadOnlyList<EquatableData></h2>
<p><code>IEquatable</code>実装のstruct</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20211120/20211120235152.png" alt="f:id:tsgcpp:20211120235152p:plain" width="425" height="133" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<h1 id="サンプルコード">サンプルコード</h1>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Ftsgcpp%2FAllocationVerification-Unity" title="GitHub - tsgcpp/AllocationVerification-Unity: Allocation tests for Unity" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://github.com/tsgcpp/AllocationVerification-Unity">github.com</a></cite></p>
<ul>
<li><code>IReadOnlyList</code>向け<code>Contains</code>なども含む</li>
</ul>
<h2 id="追記">追記</h2>
<p>Package Manager対応版を用意しました。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Ftsgcpp%2FTSCSharpSystemExtension" title="GitHub - tsgcpp/TSCSharpSystemExtension: C# extension classes for "System"" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://github.com/tsgcpp/TSCSharpSystemExtension">github.com</a></cite></p>
<h1 id="参考">参考</h1>
<ul>
<li><a href="https://docs.unity3d.com/2019.1/Documentation/ScriptReference/TestTools.Constraints.AllocatingGCMemoryConstraint.html">Unity - Scripting API: AllocatingGCMemoryConstraint</a></li>
<li><a href="https://tsubakit1.hateblo.jp/entry/2018/10/02/190528">【Unity】指定のコードがGCを発生させるかどうかをテストする(AllocatingGCMemory) - テラシュールブログ</a>
<ul>
<li>テストでの<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A2%A5%ED%A5%B1%A1%BC%A5%B7%A5%E7%A5%F3">アロケーション</a>の確認方法</li>
</ul>
</li>
</ul>
<h1 id="雑感">雑感</h1>
<p>世間では<a class="keyword" href="http://d.hatena.ne.jp/keyword/Facebook">Facebook</a>からMetaへと社名変更、新しいOculusが発表、<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%E1%A5%BF%A5%D0%A1%BC%A5%B9">メタバース</a>への注目など、<br />
XR界隈で目まぐるしい変化が来そうなときに、<br />
「なんでXRとは程遠い<a class="keyword" href="http://d.hatena.ne.jp/keyword/C%23">C#</a>の話してるの?」って言われそうですねw。</p>
<p>アプリケーションの楽しさに直接関連するものでは確かに有りませんが、<br />
一方で快適なXRアプリケーションの実現において、過剰な<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A2%A5%ED%A5%B1%A1%BC%A5%B7%A5%E7%A5%F3">アロケーション</a>の回避や削減は大事な要素であると考えています。</p>
<p>もちろんUnityにおいて完全に<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A2%A5%ED%A5%B1%A1%BC%A5%B7%A5%E7%A5%F3">アロケーション</a>を避けることは難しいため、<br />
こだわり過ぎず、避けれるなら避けるぐらいがちょうど良いのではと思います。</p>
<p>マニアックな話では有りましたが、少しでも面白いと思っていただけたなら幸いです。<br />
それでは~</p>
tsgcpp
【Unity C#】 IReadOnlyListの紹介
hatenablog://entry/13574176438027633831
2021-10-30T18:45:07+09:00
2021-10-30T18:45:07+09:00 概要 IReadOnlyList について 注意事項 追記 2022/02/25 2022/02/26 IReadOnlyListは読込専用のList C#標準の一次元配列はIReadOnlyListにキャスト可能 IReadOnlyCollectionとの違いは index指定で要素にアクセスが可能なこと IReadOnlyListのTに値型を宣言すれば要素含め読込専用となる IReadOnlyListは参照型の要素を読込専用にはしない IReadOnlyList<out T>のため共変性持つ Unityでの使用例 MonoBehaviourやScriptableObjectを依存逆転の法則…
<ul class="table-of-contents">
<li><a href="#概要">概要</a></li>
<li><a href="#IReadOnlyList-について">IReadOnlyList について</a><ul>
<li><a href="#注意事項">注意事項</a></li>
</ul>
</li>
<li><a href="#追記">追記</a><ul>
<li><a href="#20220225">2022/02/25</a></li>
<li><a href="#20220226">2022/02/26</a></li>
<li><a href="#IReadOnlyListは読込専用のList">IReadOnlyListは読込専用のList</a></li>
<li><a href="#C標準の一次元配列はIReadOnlyListにキャスト可能">C#標準の一次元配列はIReadOnlyListにキャスト可能</a></li>
<li><a href="#IReadOnlyCollectionとの違いは-index指定で要素にアクセスが可能なこと">IReadOnlyCollectionとの違いは index指定で要素にアクセスが可能なこと</a></li>
<li><a href="#IReadOnlyListのTに値型を宣言すれば要素含め読込専用となる">IReadOnlyListのTに値型を宣言すれば要素含め読込専用となる</a></li>
<li><a href="#IReadOnlyListは参照型の要素を読込専用にはしない">IReadOnlyListは参照型の要素を読込専用にはしない</a></li>
<li><a href="#IReadOnlyListout-Tのため共変性持つ">IReadOnlyList<out T>のため共変性持つ</a></li>
</ul>
</li>
<li><a href="#Unityでの使用例">Unityでの使用例</a><ul>
<li><a href="#MonoBehaviourやScriptableObjectを依存逆転の法則を当てはめて提供">MonoBehaviourやScriptableObjectを依存逆転の法則を当てはめて提供</a></li>
</ul>
</li>
<li><a href="#関連">関連</a></li>
<li><a href="#雑感">雑感</a></li>
</ul>
<h1 id="概要">概要</h1>
<p>今回の主役は <code>IReadOnlyList</code> です!</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdocs.microsoft.com%2Fen-us%2Fdotnet%2Fapi%2Fsystem.collections.generic.ireadonlylist-1%3Fview%3Dnetframework-4.8" title="IReadOnlyList<T> Interface (System.Collections.Generic)" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://docs.microsoft.com/en-us/dotnet/api/system.collections.generic.ireadonlylist-1?view=netframework-4.8">docs.microsoft.com</a></cite></p>
<p><code>List</code>でもなく、<code>IReadOnlyCollection</code>でもありません!</p>
<p>個人的には <strong>パフォーマンス</strong> と <strong><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%DD%A5%EA%A5%E2%A1%BC%A5%D5%A5%A3%A5%BA%A5%E0">ポリモーフィズム</a></strong>を併せ持った良いinterfaceだと思っています。</p>
<p>しかし、<strong><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A2%A5%ED%A5%B1%A1%BC%A5%B7%A5%E7%A5%F3">アロケーション</a>の発生を避けたいときにはなかなか注意が必要</strong> な存在だったりします。。。<br />
そんな<code>IReadOnlyList</code>を紹介していきたいと思います。</p>
<p>ちなみに<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A2%A5%ED%A5%B1%A1%BC%A5%B7%A5%E7%A5%F3">アロケーション</a>については別記事にする予定です。</p>
<h1 id="IReadOnlyList-について">IReadOnlyList について</h1>
<h2 id="注意事項">注意事項</h2>
<ul>
<li>ダウンキャスト (<code>readonlyList as List<></code>など)は禁止という前提で説明
<ul>
<li>ダウンキャストを許した場合は読込専用が簡単に崩壊するため</li>
</ul>
</li>
</ul>
<h1 id="追記">追記</h1>
<h2 id="20220225">2022/02/25</h2>
<ul>
<li>「Unityでの使用例」で共変性を利用したアップキャストになるようにコード例を修正</li>
</ul>
<h2 id="20220226">2022/02/26</h2>
<ul>
<li>「【Unity <a class="keyword" href="http://d.hatena.ne.jp/keyword/C%23">C#</a>】 IReadOnlyListと<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A2%A5%ED%A5%B1%A1%BC%A5%B7%A5%E7%A5%F3">アロケーション</a>」へのリンクを追加</li>
</ul>
<h2 id="IReadOnlyListは読込専用のList">IReadOnlyListは読込専用のList</h2>
<ul>
<li>名前の通り <strong>読込専用のList</strong> 向けinterface
<ul>
<li>標準配列(<code>Array</code>)や<code>List</code>などが継承している</li>
</ul>
</li>
<li>各要素の変更が不可になっており <code>list[0] = default</code> などは<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%D1%A5%A4%A5%EB">コンパイル</a>エラーとなる</li>
<li>主な利用用途は変更不可のListとして提供したいときなど</li>
</ul>
<pre class="code lang-cs" data-lang="cs" data-unlink> [Tooltip(<span class="synConstant">"ラベル一覧"</span>)]
[SerializeField] <span class="synType">private</span> List<<span class="synType">string</span>> _labelList;
<span class="synComment">// 読込専用としてラベル一覧をクラス外に提供</span>
<span class="synType">public</span> IReadOnlyList<<span class="synType">string</span>> LabelList => _labelList;
</pre>
<h2 id="C標準の一次元配列はIReadOnlyListにキャスト可能"><a class="keyword" href="http://d.hatena.ne.jp/keyword/C%23">C#</a>標準の一次元配列はIReadOnlyListにキャスト可能</h2>
<pre class="code lang-cs" data-lang="cs" data-unlink> <span class="synType">string</span>[] strList = <span class="synStatement">new</span> <span class="synType">string</span>[] { <span class="synConstant">"foo"</span>, <span class="synConstant">"bar"</span>, <span class="synConstant">"baz"</span> };
IReadOnlyList<<span class="synType">string</span>> readonlyStrList = strList;
</pre>
<ul>
<li>公式ドキュメントにも 「一次元配列は <code>IList<T></code> と <code>IEnumerable<T></code> を実装している」と明記されている
<ul>
<li><code>Single-dimensional arrays also implement IList<T> and IEnumerable<T>.</code></li>
</ul>
</li>
</ul>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdocs.microsoft.com%2Fen-us%2Fdotnet%2Fcsharp%2Fprogramming-guide%2Farrays%2F" title="Arrays - C# Programming Guide" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/arrays/">docs.microsoft.com</a></cite></p>
<h2 id="IReadOnlyCollectionとの違いは-index指定で要素にアクセスが可能なこと">IReadOnlyCollectionとの違いは index指定で要素にアクセスが可能なこと</h2>
<ul>
<li><code>list[i]</code>が<code>List</code>同様に使用可能</li>
<li>そもそも<code>T this[int index] { get; }</code>の定義は <code>IReadOnlyList</code>由来</li>
<li><code>IReadOnlyCollection</code>と異なりindexさえわかれば <code>O(1)</code>の計算量でアクセス可能</li>
</ul>
<p><span style="color: #ff0000">※個人的にパフォーマンス的観点で有効性を感じるところ</span></p>
<h2 id="IReadOnlyListのTに値型を宣言すれば要素含め読込専用となる">IReadOnlyListのTに値型を宣言すれば要素含め読込専用となる</h2>
<pre class="code lang-cs" data-lang="cs" data-unlink> [SerializeField] <span class="synType">private</span> List<Vector3> _vectorList;
<span class="synType">public</span> IReadOnlyList<Vector3> VectorList => _vectorList;
</pre>
<ul>
<li>上記のようにTが値型で<code>IReadOnlyList</code>に変換した場合は完全な読込専用として提供される</li>
</ul>
<h2 id="IReadOnlyListは参照型の要素を読込専用にはしない">IReadOnlyListは参照型の要素を読込専用にはしない</h2>
<ul>
<li><code>IReadOnlyList</code>はあくまでList自体の変更を不可にしたもので、要素本体はT型に従う
<ul>
<li>要素の方が<code>GameObject</code>の場合は引き続きnameやtransformを変更することは可能</li>
</ul>
</li>
</ul>
<pre class="code lang-cs" data-lang="cs" data-unlink> IReadOnlyList<GameObject> readonlyObjectList = <span class="synStatement">new</span> List<GameObject> { ... };
readonlyObjectList[<span class="synConstant">0</span>].name = <span class="synConstant">"Renamed_"</span> + readonlyObjectList[<span class="synConstant">0</span>].name;
</pre>
<p>但し、後述する<u><strong>共変性</strong>を利用することで参照型の要素本体も読込専用としての提供も可能</u></p>
<h2 id="IReadOnlyListout-Tのため共変性持つ">IReadOnlyList<out T>のため共変性持つ</h2>
<pre class="code lang-cs" data-lang="cs" data-unlink><span class="synType">public</span> <span class="synType">interface</span> IReadOnlyList<<span class="synStatement">out</span> T> : System.Collections.Generic.IEnumerable<<span class="synStatement">out</span> T>, System.Collections.Generic.IReadOnlyCollection<<span class="synStatement">out</span> T>
</pre>
<ul>
<li>上記のように <code>out T</code> で宣言されているため、<code>IReadOnlyList</code>は共変性がある</li>
</ul>
<p>例えば以下のような参照型クラス<code>Foo</code>、interface <code>INameHolder</code> があったとする。</p>
<pre class="code lang-cs" data-lang="cs" data-unlink><span class="synType">public</span> <span class="synType">interface</span> INameHolder
{
<span class="synType">string</span> Name { get; }
}
<span class="synType">public</span> <span class="synType">class</span> Foo : INameHolder
{
<span class="synType">public</span> <span class="synType">string</span> Name { get; set; } = <span class="synConstant">"Default Name"</span>;
}
</pre>
<p>上記<code>Foo</code>使った<code>List<Foo></code>は共変性を使って以下のような変換が可能。</p>
<pre class="code lang-cs" data-lang="cs" data-unlink> IReadOnlyList<INameHolder> readonlyNameHolderList = <span class="synStatement">new</span> List<Foo> { ... };
</pre>
<pre class="code lang-cs" data-lang="cs" data-unlink> <span class="synComment">// コンパイルエラー(INameHolderはget_Nameのみ提供のため)</span>
readonlyNameHolderList[<span class="synConstant">1</span>].Name = <span class="synConstant">"Melon"</span>;
</pre>
<p><strong>readonlyの継承型として提供することで参照型要素もreadonlyとして提供可能</strong>。</p>
<h1 id="Unityでの使用例">Unityでの使用例</h1>
<h2 id="MonoBehaviourやScriptableObjectを依存逆転の法則を当てはめて提供">MonoBehaviourやScriptableObjectを依存逆転の法則を当てはめて提供</h2>
<ul>
<li>Unityとは<a class="keyword" href="http://d.hatena.ne.jp/keyword/%C1%C2%B7%EB%B9%E7">疎結合</a>にしたまま、Unityの機能を利用した読込専用データ群を配布に利用するなど</li>
</ul>
<p>※以下はあくまで実装イメージ</p>
<pre class="code lang-cs" data-lang="cs" data-unlink>[CreateAssetMenu(fileName = nameof(ScriptableObjectNameHolder), menuName = <span class="synConstant">"ScriptableObjects/"</span> + nameof(ScriptableObjectNameHolder), order = <span class="synConstant">1</span>)]
<span class="synType">public</span> <span class="synType">sealed</span> <span class="synType">class</span> ScriptableObjectNameHolder : ScriptableObject, INameHolder
{
[SerializeField] <span class="synType">private</span> <span class="synType">string</span> _name;
<span class="synComment">// ScriptableObject側で定義された名前を読込専用として提供</span>
<span class="synType">public</span> <span class="synType">string</span> Name => _name;
}
</pre>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20220225/20220225231223.png" alt="f:id:tsgcpp:20220225231223p:plain" width="489" height="110" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p>以下の様に <code>List<ScriptableObjectNameHolder></code> から <code>IReadOnlyList<INameHolder></code> に変換し、Unityとは<a class="keyword" href="http://d.hatena.ne.jp/keyword/%C1%C2%B7%EB%B9%E7">疎結合</a>な<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>公開が可能。</p>
<pre class="code lang-cs" data-lang="cs" data-unlink> [SerializeField] <span class="synType">private</span> List<ScriptableObjectNameHolder> _nameHolders;
<span class="synComment">// 共変性を利用してUnityの存在を隠蔽し、読み込み専用として公開</span>
<span class="synType">public</span> IReadOnlyList<INameHolder> NameHolders => _nameHolders;
</pre>
<p><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>の公開はExtenjectやVContainerなどを利用することが多いですが詳細は割愛。</p>
<h1 id="関連">関連</h1>
<p>本題記事</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftsgcpp.hateblo.jp%2Fentry%2F2021%2F11%2F21%2F001927" title="【Unity C#】 IReadOnlyListとアロケーション - すぎしーのXRと3DCG" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://tsgcpp.hateblo.jp/entry/2021/11/21/001927">tsgcpp.hateblo.jp</a></cite></p>
<h1 id="雑感">雑感</h1>
<p>久々の投稿です。<br />
最近、環境が変わって色々ドタバタしていました。</p>
<p>世間では<a class="keyword" href="http://d.hatena.ne.jp/keyword/Facebook">Facebook</a>のMetaへ社名変更、新しいQuestが発表、<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%E1%A5%BF%A5%D0%A1%BC%A5%B9">メタバース</a>への注目など、<br />
XR界隈で目まぐるしい変化が来そうなときに、<br />
「なんで<a class="keyword" href="http://d.hatena.ne.jp/keyword/C%23">C#</a>の話してるの?」って言われそうですねw。</p>
<p><code>IReadOnlyList</code>はUnityでの開発でも結構役立つことが多いと感じています。<br />
DI, SOLID原則, テスタビリティ, etc...</p>
<p>ちなみに今回の記事は前座で本題は次回の「IReadOnlyListの<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A2%A5%ED%A5%B1%A1%BC%A5%B7%A5%E7%A5%F3">アロケーション</a>(関連にリンク)」だったりします!<br />
なんで<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A2%A5%ED%A5%B1%A1%BC%A5%B7%A5%E7%A5%F3">アロケーション</a>なんかを調べたのかは次回にお話できればと思います。</p>
<p>それでは~</p>
tsgcpp
【Extenject】Composite Installer を紹介!
hatenablog://entry/26006613797535830
2021-08-15T13:10:29+09:00
2021-08-15T13:10:29+09:00 概要 Compsote Installer について Compositeパターン Compsote Installer の活用術 1. 再利用可能なInstallerグループを作成可能 2. 疎結合Installer, 抽象Installerとして活用 3. 特定の機能提供向けInstallerとして配布 Compsote Installer の使い方 CompositeMonoInstaller CompositeScriptableObjectInstaller FYI: 循環参照の検知 Composite Installer 開発小話 標準搭載化 循環参照対策 循環参照が起こるケース 循…
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20210221/20210221104046.jpg" alt="f:id:tsgcpp:20210221104046j:plain" width="512" height="288" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<ul class="table-of-contents">
<li><a href="#概要">概要</a></li>
<li><a href="#Compsote-Installer-について">Compsote Installer について</a><ul>
<li><a href="#Compositeパターン">Compositeパターン</a></li>
<li><a href="#Compsote-Installer-の活用術">Compsote Installer の活用術</a><ul>
<li><a href="#1-再利用可能なInstallerグループを作成可能">1. 再利用可能なInstallerグループを作成可能</a></li>
<li><a href="#2-疎結合Installer-抽象Installerとして活用">2. 疎結合Installer, 抽象Installerとして活用</a></li>
<li><a href="#3-特定の機能提供向けInstallerとして配布">3. 特定の機能提供向けInstallerとして配布</a></li>
</ul>
</li>
</ul>
</li>
<li><a href="#Compsote-Installer-の使い方">Compsote Installer の使い方</a><ul>
<li><a href="#CompositeMonoInstaller">CompositeMonoInstaller</a></li>
<li><a href="#CompositeScriptableObjectInstaller">CompositeScriptableObjectInstaller</a></li>
<li><a href="#FYI-循環参照の検知">FYI: 循環参照の検知</a></li>
</ul>
</li>
<li><a href="#Composite-Installer-開発小話">Composite Installer 開発小話</a><ul>
<li><a href="#標準搭載化">標準搭載化</a></li>
<li><a href="#循環参照対策">循環参照対策</a><ul>
<li><a href="#循環参照が起こるケース">循環参照が起こるケース</a></li>
<li><a href="#循環参照の検証方法">循環参照の検証方法</a></li>
<li><a href="#検証処理のアロケーションの削減">検証処理のアロケーションの削減</a></li>
<li><a href="#余談-循環参照対策なしの場合">余談: 循環参照対策なしの場合</a></li>
</ul>
</li>
<li><a href="#そもそもなぜ作ろうとおもったのか">そもそもなぜ作ろうとおもったのか</a></li>
</ul>
</li>
<li><a href="#Composite-Installerを試したい場合">Composite Installerを試したい場合</a></li>
<li><a href="#クレジット">クレジット</a></li>
<li><a href="#雑感">雑感</a></li>
</ul>
<h1 id="概要">概要</h1>
<p>今回は Extenject に追加された Compsote Installer について紹介します!</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fsvermeulen%2FExtenject%2Fblob%2Fmaster%2FDocumentation%2FCompositeInstaller.md" title="Zenject/CompositeInstaller.md at master · modesttree/Zenject" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://github.com/svermeulen/Extenject/blob/master/Documentation/CompositeInstaller.md">github.com</a></cite></p>
<p>ちなみにPullRequestを出したのは私です(欲しかったので)。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fsvermeulen%2FExtenject%2Fpull%2F211" title="Feat: Composite Design Pattern for installers by tsgcpp · Pull Request #211 · modesttree/Zenject" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://github.com/svermeulen/Extenject/pull/211">github.com</a></cite></p>
<p>まだComposite Installerが同梱されているリリースバージョンはありませんが、
せっかくなので活用方法と、ついでにマージされるまでの小話について記事にしようと思います。</p>
<p>記事の最後にunitypackageとタグへのリンクを記載していますので、使ってみたいと思った方はご利用ください!</p>
<p><b>今回の記事は <span style="color: #ff0000">Extenject にある程度慣れている方向けの内容</span>です。</b></p>
<h1 id="Compsote-Installer-について">Compsote Installer について</h1>
<p>Composite Installer とは <b>Compositeパターンを使った Extenject のInstaller</b> です。</p>
<h2 id="Compositeパターン">Compositeパターン</h2>
<p>Extenject観点で説明しますと、
Composite Installerを<a class="keyword" href="http://d.hatena.ne.jp/keyword/DIContainer">DIContainer</a>に登録するだけで、そのComposite Installerに登録されているすべてのInstallerで <code>InstallBindings()</code>が実施されます。</p>
<p>また、Composite Installer に 別のComposite Installer を登録することも可能です。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20210221/20210221105446.jpg" alt="f:id:tsgcpp:20210221105446j:plain" width="971" height="233" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p>Compositeパターンはメジャーな<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C7%A5%B6%A5%A4%A5%F3%A5%D1%A5%BF%A1%BC%A5%F3">デザインパターン</a>のため、解説記事もたくさんあります。<br />
詳しくは検索してみてください。</p>
<h2 id="Compsote-Installer-の活用術">Compsote Installer の活用術</h2>
<h3 id="1-再利用可能なInstallerグループを作成可能">1. 再利用可能なInstallerグループを作成可能</h3>
<p>Compositeパターンの通り複数のInstallerを1つのComposite Installerにグループとして集約することで、再利用性のあるInstallerとしての管理が可能になります。</p>
<ul>
<li>機能ごとにInstallerのグループとして管理</li>
<li>機能ごとにComposite Installerを作成しシーンごとに取捨選択して利用</li>
</ul>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20210220/20210220201233.jpg" alt="f:id:tsgcpp:20210220201233j:plain" width="911" height="339" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p>Extenjectを利用してかつ Pure <a class="keyword" href="http://d.hatena.ne.jp/keyword/C%23">C#</a> が増えてくると比例してInstallerクラスも増えてくると思いますが、<br />
そんなときに目的や機能ごとにComposite InstallerにInstallerグループを分割登録して管理すると良いと思います!</p>
<h3 id="2-疎結合Installer-抽象Installerとして活用">2. <a class="keyword" href="http://d.hatena.ne.jp/keyword/%C1%C2%B7%EB%B9%E7">疎結合</a>Installer, 抽象Installerとして活用</h3>
<p>特定のInstallerの前にComposite Installerをレイヤーとして設けることで、ContextとInstaller間を<a class="keyword" href="http://d.hatena.ne.jp/keyword/%C1%C2%B7%EB%B9%E7">疎結合</a>化することができます。</p>
<ul>
<li>特定のInstallerに依存しない形で各Contextに登録可能</li>
<li>ContextのPrefabを編集することなく登録Installer群を変更可能</li>
</ul>
<p> <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20210220/20210220203403.jpg" alt="f:id:tsgcpp:20210220203403j:plain" width="711" height="191" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p><span style="color: #ff0000"><b>特にComposite Scriptable Object Installerはオススメです!</b></span></p>
<p>ContextはアセットをGUID経由で取り込む形になるため、<br />
特定の機能にInstallerを追加したい場合はContextを編集せずComposite Scriptable Object Installerアセットに新しいInstallerを登録するだけで済みます。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20210220/20210220204459.png" alt="f:id:tsgcpp:20210220204459p:plain" width="552" height="343" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<h3 id="3-特定の機能提供向けInstallerとして配布">3. 特定の機能提供向けInstallerとして配布</h3>
<p>複数のチームでExtenjectを利用している場合の連携に活用できると思います。</p>
<ul>
<li>特定の機能を集約したComposite Installerをパッケージに含めて共有可能</li>
<li>Composite Installerへの修正やInstaller追加もパッケージの更新のみで配布先に反映することが可能</li>
</ul>
<h1 id="Compsote-Installer-の使い方">Compsote Installer の使い方</h1>
<p>以下にドキュメントページがあります。<br />
こちらも私の方で作成して一緒にPullRequestに出しました(ガバガバな英語で恐縮ですが)。</p>
<p><a href="https://github.com/svermeulen/Extenject/blob/master/Documentation/CompositeInstaller.md">CompositeInstaller.md</a></p>
<p>せっかくなので、簡単にではありますが日本語でも使い方を紹介します。</p>
<h2 id="CompositeMonoInstaller">CompositeMonoInstaller</h2>
<p>名前の通り MonoInstaller の Compositeパターン版です。</p>
<p><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>はすでにExtenjectの中に含まれていますので、他のMonoBehaviourと同様にAdd Component可能です。</p>
<p>また、<b>Prefab化することで Prefab Installerとしても利用することができます。</b></p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20210220/20210220210533.png" alt="f:id:tsgcpp:20210220210533p:plain" width="810" height="350" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20210220/20210220210650.png" alt="f:id:tsgcpp:20210220210650p:plain" width="548" height="159" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<h2 id="CompositeScriptableObjectInstaller">CompositeScriptableObjectInstaller</h2>
<p>名前の通り ScriptableObjectInstaller の Compositeパターン版です。</p>
<p><code>Create -> Zenject -> Composite Scriptable Object Installer</code> をクリックすることで作成可能です。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20210220/20210220211141.png" alt="f:id:tsgcpp:20210220211141p:plain" width="861" height="189" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20210220/20210220211620.png" alt="f:id:tsgcpp:20210220211620p:plain" width="453" height="180" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p><span style="color: #cccccc">先程も述べましたが、個人的には ScriptableObjectInstaller をよく使うので特にオススメしたいです!</span></p>
<h2 id="FYI-循環参照の検知">FYI: 循環参照の検知</h2>
<p>Compositeパターンは性質上、循環参照ができてしまいます(詳細は後述)。<br />
指定ミスによるヒューマンエラーを回避するために、循環参照を検知したときはInspector上のプロパティが赤くなるEditorも用意しました。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20210220/20210220212259.png" alt="f:id:tsgcpp:20210220212259p:plain" width="449" height="210" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20210220/20210220212029.jpg" alt="f:id:tsgcpp:20210220212029j:plain" width="555" height="444" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p>ちなみにこの状態で起動すると例外(<code>ZenjectException</code>)が飛びます。</p>
<h1 id="Composite-Installer-開発小話">Composite Installer 開発小話</h1>
<h2 id="標準搭載化">標準搭載化</h2>
<p>ありがたいことにPullRequestを出したところ、CollaboratorのMathijs-Bakkerさんに「標準的な機能になるから標準フォルダに移動してもいいんじゃないか?」とコメントをいただけました。</p>
<p>実質的に標準搭載化となったため追加修正を加えた上でマージされることになりました。<br />
そのおかげで5日ぐらいは修正に費やすことになりましたけど、正直に嬉しかったのでモチベは高かったですw。</p>
<p>「プルリクって普通標準搭載になるもんじゃないの?」って思われた方もいるかもしれないので補足すると、 <br />
Extenject (Zenject) には <code>OptionalExtras</code> というオプション的機能を詰め込んだフォルダがあります。<br />
当初はこちらに <a class="keyword" href="http://d.hatena.ne.jp/keyword/%B3%C8%C4%A5%B5%A1%C7%BD">拡張機能</a>としてComposite InstallerのPullRequestを出していたのですが、上記のようなご提案をいただけたため標準搭載となりました。</p>
<p>みなさんの開発の手助けになれば嬉しいです。</p>
<p>そういえば、PullRequestですがmergeじゃなくてrebaseで取り込まれたのは若干気になりました。<br />
<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%BD%A1%BC%A5%B9%A5%B3%A1%BC%A5%C9">ソースコード</a>自体はそのまま取り込まれたので問題ないと思いますけど。</p>
<h2 id="循環参照対策">循環参照対策</h2>
<p>Compositeパターンは循環参照に注意する必要があることを知っていたので、循環参照対策も主に2つ入れています。</p>
<ul>
<li>循環参照があるComposite Installer があった場合は例外を出す</li>
<li>エディタ上で循環参照を検知したらInspectorに赤く表示する</li>
</ul>
<p>こちらを導入するに当たり循環参照の検証処理も少し工夫したので共有します。</p>
<h3 id="循環参照が起こるケース">循環参照が起こるケース</h3>
<p>Compositeパターンにおける循環参照は以下のケースがあります。</p>
<ul>
<li>自分自身への参照を持つ場合</li>
</ul>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20210221/20210221022218.jpg" alt="f:id:tsgcpp:20210221022218j:plain" width="311" height="139" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<ul>
<li>葉要素(子孫)が自身への参照を持つ場合
<ul>
<li>以下の例では Self と Leaf06 で循環参照</li>
</ul>
</li>
</ul>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20210220/20210220215252.jpg" alt="f:id:tsgcpp:20210220215252j:plain" width="711" height="219" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<ul>
<li>葉要素(子孫)同士が循環参照している場合
<ul>
<li>以下の例では Leaf06 と Leaf07 が互いに循環参照</li>
</ul>
</li>
</ul>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20210220/20210220215435.jpg" alt="f:id:tsgcpp:20210220215435j:plain" width="739" height="191" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<h3 id="循環参照の検証方法">循環参照の検証方法</h3>
<p>以下の手順で行います</p>
<ul>
<li>葉要素がCompositeではないこと
<ul>
<li>Compositeパターンの葉要素は元となったinterfaceなのでダウンキャスト(<code>as</code>)でCompositeかどうかの判定が必要</li>
</ul>
</li>
<li>葉要素がCompositeである場合は自身でないこと</li>
<li>自身ではない場合は先祖一覧の中に子要素がないこと</li>
<li>自身を先祖一覧に登録</li>
<li>先祖一覧を用いて子要素すべてで同様の検証を実施</li>
</ul>
<p>上記をComposite Installer の子要素が見つからなくなるまで行うことで循環参照を検知することができます。</p>
<p>先祖一覧を記録しながら探索することになるため、<span style="color: #ff0000">可変Listが必要</span>になります。</p>
<p>また、<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C7%A5%B6%A5%A4%A5%F3%A5%D1%A5%BF%A1%BC%A5%F3">デザインパターン</a>などを考慮するとダウンキャストは<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A2%A5%F3%A5%C1%A5%D1%A5%BF%A1%BC%A5%F3">アンチパターン</a>になり得ますが、<br />
今回は広く利用されることを想定し循環参照問題の回避を優先してダウンキャスト込みの検証方法にしました。</p>
<p>ちなみに検証コードは以下となります。</p>
<p><a href="https://github.com/svermeulen/Extenject/blob/master/UnityProject/Assets/Plugins/Zenject/Source/Install/CompositeInstallerExtensions.cs">CompositeInstallerExtensions.cs</a></p>
<h3 id="検証処理のアロケーションの削減">検証処理の<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A2%A5%ED%A5%B1%A1%BC%A5%B7%A5%E7%A5%F3">アロケーション</a>の削減</h3>
<p>私は余計な<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A2%A5%ED%A5%B1%A1%BC%A5%B7%A5%E7%A5%F3">アロケーション</a>は極力避けたい性質なので、<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A2%A5%ED%A5%B1%A1%BC%A5%B7%A5%E7%A5%F3">アロケーション</a>をなくしたい欲が出ました。<br />
一応、RuntimeだけでなくEditor上でもInspectorの更新毎に検証処理が動いてしまうため、ある程度<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A2%A5%ED%A5%B1%A1%BC%A5%B7%A5%E7%A5%F3">アロケーション</a>を回避したかったという理由もあります。</p>
<p>結論を述べますと <span style="color: #ff0000"><b>4連結までの検証は<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A2%A5%ED%A5%B1%A1%BC%A5%B7%A5%E7%A5%F3">アロケーション</a>が回避される</b></span> ようになっています。</p>
<p>やり方は単純で先祖1つ、先祖2つ、先祖3つ、先祖4つで検証するメソッドを用意して連結するテクニック(つまり面倒くさいやり方)で対応しました!<br />
見たほうが説明が早いと思いますので以下のコードを御覧ください。</p>
<p><a href="https://github.com/svermeulen/Extenject/blob/master/UnityProject/Assets/Plugins/Zenject/Source/Install/CompositeInstallerExtensions.cs#L122">CompositeInstallerExtensions.ValidateAsComposite</a></p>
<p>4連結まで対応したのは「多くても3連結ぐらいかな?」と思ったためです。</p>
<p>4連結以上の場合は<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A2%A5%ED%A5%B1%A1%BC%A5%B7%A5%E7%A5%F3">アロケーション</a>が発生しますので、「いやもうちょっと<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A2%A5%ED%A5%B1%A1%BC%A5%B7%A5%E7%A5%F3">アロケーション</a>発生しないようにしたい」って人は上記のコードを参考にPullRequestを出してください。</p>
<p>もちろん、テスト (<a href="https://github.com/svermeulen/Extenject/blob/master/UnityProject/Assets/Plugins/Zenject/OptionalExtras/IntegrationTests/Tests/TestCompositeInstallerExtensions/TestCompositeInstallerExtensions.cs#L135">TestCompositeInstallerExtensions</a>) も追加してあげてください!</p>
<p><span style="color: #cccccc">stackallocとかいうunsafeコードを使うことも一瞬考えたけど流石にすぐ却下しましたw</span></p>
<h3 id="余談-循環参照対策なしの場合">余談: 循環参照対策なしの場合</h3>
<p>循環参照のあるComposite Installerに対して<code>InstallBindings</code>をコールした場合、以下の現象が発生しました。</p>
<ul>
<li>Playでは<code>StackOverflowException</code> or エディタ自体が落ちる
<ul>
<li>どちらになるかはPlay毎に異なり、<code>StackOverflowException</code> 確定ではありませんでした</li>
</ul>
</li>
<li>Zenject Validate では落ちることなくフリーズし、エディタが入力を一切受け付けなくなる</li>
</ul>
<p>どちらにしても<code>InstallBindings</code> の初めに検証(<code>Assert</code>)を入れる必要がありました。</p>
<p><span style="color: #cccccc"><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A2%A5%ED%A5%B1%A1%BC%A5%B7%A5%E7%A5%F3">アロケーション</a>発生させたくないとか言ってられない!</span></p>
<h2 id="そもそもなぜ作ろうとおもったのか">そもそもなぜ作ろうとおもったのか</h2>
<ul>
<li>Installer群を一括でContextに登録する機能がほしかったから
<ul>
<li>Installerが増えて管理しづらかったので</li>
<li>1つのInstallerクラス内にすべてのBind処理を書くと保守性が悪化するため避けたかった</li>
</ul>
</li>
<li>開発におけるワークフローを考えてみたときにComposite Installerがあればいろいろと応用が効くと思ったから
<ul>
<li>具体的には「Compsote Installer の活用術」で述べた通りです</li>
</ul>
</li>
</ul>
<p>他に良さそうな活用方法があればぜひ教えて下さい!</p>
<h1 id="Composite-Installerを試したい場合">Composite Installerを試したい場合</h1>
<p>9.2.0 にrebaseでComposite Installerのcommitを取り込んだタグを用意しました。<br />
もともと拡張として作成していたこともあってExtenjectの基本機能に変更はありません(Inspector表示用のEditorに少し変更が入ったぐらいです)。</p>
<p>metaファイルも同じなのでリリースバージョンが出てもCompositeMonoInstaller, CompositeScriptableObjectInstallerはそのまま使えると思います。<br />
MITライセンスなので言う必要はないと思いますが、<span style="color: #ff5252">自己責任でお願いします!</span></p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Ftsgcpp%2FExtenject%2Freleases%2Ftag%2F9.2.0_with_composite_installers" title="Release 9.2.0 with Composite Installers · tsgcpp/Extenject" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://github.com/tsgcpp/Extenject/releases/tag/9.2.0_with_composite_installers">github.com</a></cite></p>
<p>フォークした<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>(tsgcpp上)にタグを登録していますが、本体にはすでに取り込まれている内容なためtsgcppの記名は不要です。</p>
<h1 id="クレジット">クレジット</h1>
<ul>
<li>draw.io @diagrams.net
<ul>
<li>図形の作成に利用させていただきました</li>
</ul>
</li>
</ul>
<h1 id="雑感">雑感</h1>
<p>まさかの3ヶ月ぶりの投稿です。。。<br />
個人開発を優先していたらあっという間に月日が流れていました。</p>
<p>記事を書いた理由はそろそろ情報発信しないとなと思ったことと、<br />
Extenjectを使ったワークフローの提案的なことをやってみたいと思ったからです。</p>
<p>Extenjectに限らずDIコンテナはゲームやアプリケーションを面白くするための仕組みではありませんが、<br />
工夫次第で幅広いシステムに対応させることが可能になると思います。</p>
<p>UnityでSOLID原則などを取り入れる上でも欠かせない存在かな思いますので、ぜひ活用してください!</p>
<p>それでは~</p>
tsgcpp
【GitHub Packages for Unity】限定配布も可能, GitHub Packages で Unity アセット配布
hatenablog://entry/26006613797154753
2021-08-14T12:20:48+09:00
2021-08-14T12:20:48+09:00 Unity向けにGitHub Pakcagesでアセットを配布する方法を紹介
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20210731/20210731231035.jpg" alt="f:id:tsgcpp:20210731231035j:plain" width="512" height="288" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<ul class="table-of-contents">
<li><a href="#概要">概要</a></li>
<li><a href="#記事内でのキーワード略称">記事内でのキーワード略称</a></li>
<li><a href="#GitHub-Packages-について">GitHub Packages について</a><ul>
<li><a href="#様々なパッケージ形式に対応">様々なパッケージ形式に対応</a></li>
<li><a href="#GitHubの仕組みでパッケージのアクセス権限を制御可能">GitHubの仕組みでパッケージのアクセス権限を制御可能</a></li>
<li><a href="#Publicは無料Privateは従量課金制">Publicは無料、Privateは従量課金制</a></li>
<li><a href="#GitHub-Actions-経由でリポジトリからアップロード">GitHub Actions 経由でリポジトリからアップロード</a></li>
</ul>
</li>
<li><a href="#アップロードまでの流れ">アップロードまでの流れ</a><ul>
<li><a href="#Package-Manager形式でアセットを用意">Package Manager形式でアセットを用意</a><ul>
<li><a href="#packagejson-の-name-について">package.json の name について</a></li>
<li><a href="#packagejson-について">package.json について</a></li>
</ul>
</li>
<li><a href="#アップロード用アクションを定義">アップロード用アクションを定義</a></li>
<li><a href="#パッケージのアップロード">パッケージのアップロード</a></li>
</ul>
</li>
<li><a href="#UPM経由でのパッケージのインストール">UPM経由でのパッケージのインストール</a><ul>
<li><a href="#アクセストークンの発行">アクセストークンの発行</a><ul>
<li><a href="#アクセストークンの注意点">アクセストークンの注意点</a></li>
</ul>
</li>
<li><a href="#GitHub上でアクセストークンを発行">GitHub上でアクセストークンを発行</a></li>
<li><a href="#upmconfigtoml-ファイルを作成">.upmconfig.toml ファイルを作成</a></li>
<li><a href="#Unity-で-Scoped-Registries-とインストールパッケージを設定">Unity で Scoped Registries とインストールパッケージを設定</a><ul>
<li><a href="#補足-comtsgcppunitygithubpackageexampleintegration-サンプルパッケージ-の依存グラフ-について">補足: com.tsgcpp.unitygithubpackageexample.integration (サンプルパッケージ) の依存グラフ について</a></li>
<li><a href="#インストールの確認">インストールの確認</a></li>
</ul>
</li>
</ul>
</li>
<li><a href="#GitHub-PackageでUnityパッケージを配布するメリット">GitHub PackageでUnityパッケージを配布するメリット</a><ul>
<li><a href="#packagejson-の-dependencies-に依存を定義可能">package.json の dependencies に依存を定義可能</a></li>
<li><a href="#Privateリポジトリでのパッケージ配布が容易">Privateリポジトリでのパッケージ配布が容易</a><ul>
<li><a href="#補足-Git-URLでもPrivateリポジトリから取り込みは一応可能">補足: Git URLでもPrivateリポジトリから取り込みは一応可能</a></li>
<li><a href="#GitHubアカウントの仕組みでアクセス権限を制御可能">GitHubアカウントの仕組みでアクセス権限を制御可能</a></li>
</ul>
</li>
<li><a href="#独自のnpmレジストリが不要">独自のnpmレジストリが不要</a></li>
</ul>
</li>
<li><a href="#GitHub-PackageでUnityパッケージを配布するデメリット">GitHub PackageでUnityパッケージを配布するデメリット</a><ul>
<li><a href="#Unity-Package-Manager-のパッケージ検索機能が使用不可">Unity Package Manager のパッケージ検索機能が使用不可</a></li>
<li><a href="#Packages以下で-displayName-で表示されない">Packages以下で displayName で表示されない</a></li>
</ul>
</li>
<li><a href="#GitHub-Package-を使用する場合の諸注意">GitHub Package を使用する場合の諸注意</a></li>
<li><a href="#サンプル">サンプル</a><ul>
<li><a href="#サンプルリポジトリ">サンプルリポジトリ</a></li>
<li><a href="#サンプルパッケージ">サンプルパッケージ</a></li>
<li><a href="#サンプルパッケージ発行時のアクション">サンプルパッケージ発行時のアクション</a></li>
</ul>
</li>
<li><a href="#参考">参考</a></li>
<li><a href="#雑感">雑感</a></li>
</ul>
<h1 id="概要">概要</h1>
<p>今回はUnity向けに<a class="keyword" href="http://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Pakcagesでアセットを配布する方法を紹介します。</p>
<p>社内やチーム内でUnityのパッケージを限定配布できたりもするので、<br />
良かったら活用してみてください!</p>
<p>記事の最後にサンプルを記載しています。</p>
<h1 id="記事内でのキーワード略称">記事内でのキーワード略称</h1>
<ul>
<li><a class="keyword" href="http://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Packages -> GHP</li>
<li><a class="keyword" href="http://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Actions -> GHA</li>
<li>Unity Package Manager -> UPM</li>
</ul>
<h1 id="GitHub-Packages-について"><a class="keyword" href="http://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Packages について</h1>
<p>簡単に言えば「パッケージ公開サービス」で<a class="keyword" href="http://d.hatena.ne.jp/keyword/GitHub">GitHub</a>から提供されています。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Ffeatures%2Fpackages" title="GitHub Packages: Your packages, at home with their code" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://github.com/features/packages">github.com</a></cite></p>
<h2 id="様々なパッケージ形式に対応">様々なパッケージ形式に対応</h2>
<ul>
<li><strong>npm</strong>, gradle, docker container などあらゆるパッケージ形式に対応</li>
</ul>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdocs.github.com%2Fen%2Fpackages%2Flearn-github-packages%2Fintroduction-to-github-packages" title="Introduction to GitHub Packages - GitHub Docs" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://docs.github.com/en/packages/learn-github-packages/introduction-to-github-packages">docs.github.com</a></cite></p>
<h2 id="GitHubの仕組みでパッケージのアクセス権限を制御可能"><a class="keyword" href="http://d.hatena.ne.jp/keyword/GitHub">GitHub</a>の仕組みでパッケージのアクセス権限を制御可能</h2>
<ul>
<li><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>の Manage <a class="keyword" href="http://d.hatena.ne.jp/keyword/access">access</a> がそのままパッケージにも適用</li>
<li>Private<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>にパッケージを登録することで限定配布も可能</li>
</ul>
<h2 id="Publicは無料Privateは従量課金制">Publicは無料、Privateは従量課金制</h2>
<ul>
<li>Publicパッケージは無料で配布可能</li>
<li>Privateパッケージでも Free, Team, Enterpriseそれぞれにも無料枠有り
<ul>
<li>ストレージ枠とデータ転送枠が使用されます</li>
</ul>
</li>
<li><a href="https://github.com/settings/billing">billing</a> ページにて使用状況を確認可能
<ul>
<li><code>Data transfer out</code> がデータ転送枠</li>
</ul>
</li>
</ul>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdocs.github.com%2Fen%2Fbilling%2Fmanaging-billing-for-github-packages%2Fabout-billing-for-github-packages" title="About billing for GitHub Packages - GitHub Docs" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://docs.github.com/en/billing/managing-billing-for-github-packages/about-billing-for-github-packages">docs.github.com</a></cite></p>
<h2 id="GitHub-Actions-経由でリポジトリからアップロード"><a class="keyword" href="http://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Actions 経由で<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>からアップロード</h2>
<ul>
<li>Repository -> Actions -> Packages の流れでアップロード</li>
</ul>
<p>今回は GHA での簡単なアップロード例を紹介します</p>
<p><br></p>
<h1 id="アップロードまでの流れ">アップロードまでの流れ</h1>
<p>まずはパッケージをアップロードするまでを紹介します!</p>
<h2 id="Package-Manager形式でアセットを用意">Package Manager形式でアセットを用意</h2>
<ul>
<li>UPM向けにアセットを用意
<ul>
<li>アセット + <code>package.json</code></li>
</ul>
</li>
<li><span style="color: #ff0000"><code>meta</code> ファイルも必ず生成</span>
<ul>
<li>UPMで必須、存在しないと取り込んだときエラー</li>
</ul>
</li>
</ul>
<p>サンプルコードにも簡単なパッケージ用アセットを4つ用意しています。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20210731/20210731173145.png" alt="f:id:tsgcpp:20210731173145p:plain" width="488" height="434" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<h3 id="packagejson-の-name-について">package.<a class="keyword" href="http://d.hatena.ne.jp/keyword/json">json</a> の name について</h3>
<ul>
<li><code>name</code>の推奨文字は <code>a-z0-9.-_</code> で<span style="color: #ff0000">大文字は非推奨</span></li>
<li><code>com.<org>.<name>(.<sub name>)</code> のように<span style="color: #ff0000"><b><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C9%A5%E1%A5%A4%A5%F3">ドメイン</a>を含めること推奨</b></span>
<ul>
<li>UPMで使用する際の <code>Scoped Registries</code> は<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C9%A5%E1%A5%A4%A5%F3">ドメイン</a>部分でアクセス先を制御するため</li>
<li><code>jp.co</code>, <code>jp.ne</code> などでも可</li>
</ul>
</li>
</ul>
<pre class="code" data-lang="" data-unlink>com.tsgcpp.unitygithubpackageexample.integration
com.tsgcpp.tscubemapgenerator
jp.co.tsgcpp.groupname.categoryname</pre>
<h3 id="packagejson-について">package.<a class="keyword" href="http://d.hatena.ne.jp/keyword/json">json</a> について</h3>
<ul>
<li><code>publishConfig</code> 項目が必須
<ul>
<li><span style="color: #ff0000">UserもしくはOrganizationの前には <code>@</code> が必須</span></li>
<li><span style="color: #ff0000">すべて小文字 (アカウント名がSampleAccの場合は<code>@sampleacc</code>)</span></li>
<li>パッケージをアップロード先として使用される</li>
<li>余談: <a class="keyword" href="http://d.hatena.ne.jp/keyword/GitHub">GitHub</a>上ではアカウント名は <code>tsgcpp</code> も <code>TsGCpP</code> も同じ扱い</li>
</ul>
</li>
<li><code>unity</code> 項目もUnity向けのため必須</li>
<li>それ以外は通常のUPMと同様
<ul>
<li><a href="https://docs.unity3d.com/Manual/CustomPackages.html">Creating custom packages</a></li>
</ul>
</li>
</ul>
<pre class="code lang-json" data-lang="json" data-unlink>"<span class="synStatement">publishConfig</span>": <span class="synSpecial">{</span>
"<span class="synStatement">registry</span>": "<span class="synConstant">https://npm.pkg.github.com/@<user or organization></span>"
<span class="synSpecial">}</span>
</pre>
<p>サンプルの全体は以下となります</p>
<pre class="code lang-json" data-lang="json" data-unlink><span class="synSpecial">{</span>
"<span class="synStatement">name</span>": "<span class="synConstant">com.tsgcpp.unitygithubpackageexample.integration</span>",
"<span class="synStatement">version</span>": "<span class="synConstant">1.2.3</span>",
"<span class="synStatement">displayName</span>": "<span class="synConstant">Integration Package Example</span>",
"<span class="synStatement">description</span>": "<span class="synConstant">Integration Package Example for UnityGithubPackageExample</span>",
"<span class="synStatement">unity</span>": "<span class="synConstant">2019.4</span>",
"<span class="synStatement">keywords</span>": <span class="synSpecial">[</span>
"<span class="synConstant">tsgcpp</span>",
"<span class="synConstant">main</span>"
<span class="synSpecial">]</span>,
"<span class="synStatement">license</span>": "<span class="synConstant">UNLICENSED</span>",
"<span class="synStatement">dependencies</span>": <span class="synSpecial">{</span>
"<span class="synStatement">com.tsgcpp.unitygithubpackageexample.script</span>": "<span class="synConstant">2.3.4</span>",
"<span class="synStatement">com.tsgcpp.unitygithubpackageexample.prefab</span>": "<span class="synConstant">3.4.5</span>"
<span class="synSpecial">}</span>,
"<span class="synStatement">author</span>": <span class="synSpecial">{</span>
"<span class="synStatement">name</span>": "<span class="synConstant">tsgcpp</span>",
"<span class="synStatement">url</span>": "<span class="synConstant">https://github.com/tsgcpp</span>"
<span class="synSpecial">}</span>,
"<span class="synStatement">scripts</span>": <span class="synSpecial">{</span>
"<span class="synStatement">test</span>": "<span class="synConstant">exit 0</span>"
<span class="synSpecial">}</span>,
"<span class="synStatement">repository</span>": <span class="synSpecial">{</span>
"<span class="synStatement">type</span>": "<span class="synConstant">git</span>",
"<span class="synStatement">url</span>": "<span class="synConstant">git+https://github.com/tsgcpp/UnityGithubPackageExample.git</span>"
<span class="synSpecial">}</span>,
"<span class="synStatement">bugs</span>": <span class="synSpecial">{</span>
"<span class="synStatement">url</span>": "<span class="synConstant">https://github.com/tsgcpp/UnityGithubPackageExample/issues</span>"
<span class="synSpecial">}</span>,
"<span class="synStatement">publishConfig</span>": <span class="synSpecial">{</span>
"<span class="synStatement">registry</span>": "<span class="synConstant">https://npm.pkg.github.com/@tsgcpp</span>"
<span class="synSpecial">}</span>
<span class="synSpecial">}</span>
</pre>
<h2 id="アップロード用アクションを定義">アップロード用アクションを定義</h2>
<ul>
<li>GHA の <a class="keyword" href="http://d.hatena.ne.jp/keyword/Yaml">Yaml</a> を定義
<ul>
<li>サンプルでは <code>.github/workflows/release-package.yml</code> に定義</li>
</ul>
</li>
<li><code>packagePath</code> にパッケージの<a class="keyword" href="http://d.hatena.ne.jp/keyword/%C1%EA%C2%D0%A5%D1%A5%B9">相対パス</a>を指定(複数可)</li>
<li><code>actions/setup-node</code> のstepは必須</li>
<li><code>npm publish</code> でパッケージをアップロード</li>
</ul>
<p>せっかくGHAを使うので、アップロード処理前にテストも組み込みましょう!</p>
<pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synIdentifier">name</span><span class="synSpecial">:</span> Unity Example Packages Publish
<span class="synComment"># release tagを発行時にアップロード</span>
<span class="synIdentifier">on</span><span class="synSpecial">:</span>
<span class="synIdentifier">release</span><span class="synSpecial">:</span>
<span class="synIdentifier">types</span><span class="synSpecial">:</span> <span class="synSpecial">[</span>created<span class="synSpecial">]</span>
<span class="synIdentifier">jobs</span><span class="synSpecial">:</span>
<span class="synComment"> # Unity Test Runner の実行 (GameCIのunity-test-runnerを利用)</span>
<span class="synIdentifier">test</span><span class="synSpecial">:</span>
<span class="synIdentifier">name</span><span class="synSpecial">:</span> Test in ${{ matrix.testMode }}
<span class="synIdentifier">runs-on</span><span class="synSpecial">:</span> ubuntu-latest
<span class="synIdentifier">strategy</span><span class="synSpecial">:</span>
<span class="synIdentifier">fail-fast</span><span class="synSpecial">:</span> <span class="synConstant">false</span>
<span class="synIdentifier">matrix</span><span class="synSpecial">:</span>
<span class="synIdentifier">projectPath</span><span class="synSpecial">:</span>
<span class="synStatement">- </span>.
<span class="synIdentifier">unityVersion</span><span class="synSpecial">:</span>
<span class="synStatement">- </span>2020.3.14f1
<span class="synIdentifier">testMode</span><span class="synSpecial">:</span>
<span class="synStatement">- </span>all
<span class="synIdentifier">steps</span><span class="synSpecial">:</span>
<span class="synStatement">- </span><span class="synIdentifier">uses</span><span class="synSpecial">:</span> actions/checkout@v2
<span class="synIdentifier">with</span><span class="synSpecial">:</span>
<span class="synIdentifier">lfs</span><span class="synSpecial">:</span> <span class="synConstant">true</span>
<span class="synStatement">- </span><span class="synIdentifier">uses</span><span class="synSpecial">:</span> game-ci/unity-test-runner@v2
<span class="synIdentifier">id</span><span class="synSpecial">:</span> tests
<span class="synIdentifier">env</span><span class="synSpecial">:</span>
<span class="synIdentifier">UNITY_LICENSE</span><span class="synSpecial">:</span> ${{ secrets.UNITY_LICENSE }}
<span class="synIdentifier">with</span><span class="synSpecial">:</span>
<span class="synIdentifier">unityVersion</span><span class="synSpecial">:</span> ${{ matrix.unityVersion }}
<span class="synIdentifier">projectPath</span><span class="synSpecial">:</span> ${{ matrix.projectPath }}
<span class="synIdentifier">testMode</span><span class="synSpecial">:</span> ${{ matrix.testMode }}
<span class="synIdentifier">artifactsPath</span><span class="synSpecial">:</span> ${{ matrix.testMode }}-artifacts
<span class="synIdentifier">githubToken</span><span class="synSpecial">:</span> ${{ secrets.GITHUB_TOKEN }}
<span class="synIdentifier">checkName</span><span class="synSpecial">:</span> ${{ matrix.testMode }} Test Results
<span class="synStatement">- </span><span class="synIdentifier">uses</span><span class="synSpecial">:</span> actions/upload-artifact@v2
<span class="synIdentifier">if</span><span class="synSpecial">:</span> always()
<span class="synIdentifier">with</span><span class="synSpecial">:</span>
<span class="synIdentifier">name</span><span class="synSpecial">:</span> Test results for ${{ matrix.testMode }}
<span class="synIdentifier">path</span><span class="synSpecial">:</span> ${{ steps.tests.outputs.artifactsPath }}
<span class="synComment"> # アップロード処理の実施</span>
<span class="synIdentifier">publish</span><span class="synSpecial">:</span>
<span class="synIdentifier">needs</span><span class="synSpecial">:</span> test
<span class="synIdentifier">runs-on</span><span class="synSpecial">:</span> ubuntu-latest
<span class="synIdentifier">strategy</span><span class="synSpecial">:</span>
<span class="synIdentifier">fail-fast</span><span class="synSpecial">:</span> <span class="synConstant">false</span>
<span class="synIdentifier">matrix</span><span class="synSpecial">:</span>
<span class="synIdentifier">packagePath</span><span class="synSpecial">:</span>
<span class="synStatement">- </span>./Assets/Plugins/Script
<span class="synStatement">- </span>./Assets/Plugins/Material
<span class="synStatement">- </span>./Assets/Plugins/Prefab
<span class="synStatement">- </span>./Assets/Plugins/Integration
<span class="synIdentifier">permissions</span><span class="synSpecial">:</span>
<span class="synIdentifier">packages</span><span class="synSpecial">:</span> write
<span class="synIdentifier">contents</span><span class="synSpecial">:</span> read
<span class="synIdentifier">steps</span><span class="synSpecial">:</span>
<span class="synStatement">- </span><span class="synIdentifier">uses</span><span class="synSpecial">:</span> actions/checkout@v2
<span class="synStatement">- </span><span class="synIdentifier">uses</span><span class="synSpecial">:</span> actions/setup-node@v2
<span class="synIdentifier">with</span><span class="synSpecial">:</span>
<span class="synIdentifier">node-version</span><span class="synSpecial">:</span> <span class="synConstant">12</span>
<span class="synIdentifier">registry-url</span><span class="synSpecial">:</span> https://npm.pkg.github.com/
<span class="synStatement">- </span><span class="synIdentifier">run</span><span class="synSpecial">:</span> npm publish ${{ matrix.packagePath }}
<span class="synIdentifier">env</span><span class="synSpecial">:</span>
<span class="synIdentifier">NODE_AUTH_TOKEN</span><span class="synSpecial">:</span> ${{secrets.GITHUB_TOKEN}}
</pre>
<ul>
<li>余談 <code>.npmrc</code> はUnityの場合は不要(node.jsプロジェクトではないため)</li>
</ul>
<h2 id="パッケージのアップロード">パッケージのアップロード</h2>
<ul>
<li>masterにアセット, package.<a class="keyword" href="http://d.hatena.ne.jp/keyword/json">json</a>, GHAの<a class="keyword" href="http://d.hatena.ne.jp/keyword/Yaml">Yaml</a>を追加</li>
<li><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>の Releases ページの <code>Draft a new release</code> より release tag を発行
<ul>
<li>release tagの発行がアクションのトリガーのため</li>
</ul>
</li>
<li>アクションが正常に完了すると Packages に登録される
<ul>
<li>初回のみ反映されるまで若干時間がかかる可能性有り</li>
</ul>
</li>
</ul>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20210731/20210731212117.png" alt="f:id:tsgcpp:20210731212117p:plain" width="380" height="219" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p>サンプル<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>は記事の最後に記載しています。</p>
<p><br></p>
<h1 id="UPM経由でのパッケージのインストール">UPM経由でのパッケージのインストール</h1>
<p>次はアップロードされたパッケージのUPM経由でのインストール方法を紹介します!</p>
<h2 id="アクセストークンの発行">アクセス<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C8%A1%BC%A5%AF">トーク</a>ンの発行</h2>
<p>アクセス<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C8%A1%BC%A5%AF">トーク</a>ンは<a class="keyword" href="http://d.hatena.ne.jp/keyword/API">API</a>経由などで<a class="keyword" href="http://d.hatena.ne.jp/keyword/GitHub">GitHub</a>のコンテンツにアクセスするための<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C8%A1%BC%A5%AF">トーク</a>ンになります。</p>
<ul>
<li>GHPのアクセスにはアクセス<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C8%A1%BC%A5%AF">トーク</a>ンが必要</li>
<li><span style="color: #ff0000">public, private にどちらでも必要</span></li>
</ul>
<h3 id="アクセストークンの注意点">アクセス<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C8%A1%BC%A5%AF">トーク</a>ンの注意点</h3>
<p>発行する前にアクセス<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C8%A1%BC%A5%AF">トーク</a>ンの注意点についてです。</p>
<ul>
<li><b><span style="color: #ff0000"><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C8%A1%BC%A5%AF">トーク</a>ンの発行はパッケージにアクセスする本人のアカウントで実施</span></b></li>
<li><b><span style="color: #ff0000">発行した<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C8%A1%BC%A5%AF">トーク</a>ンは第<a class="keyword" href="http://d.hatena.ne.jp/keyword/%BB%B0%BC%D4">三者</a>へは非公開にし、基本的に本人のみ使用する</span></b></li>
<li>(任意)期限 (Expiration) を指定して定期的に更新 (セキュリティ観点)</li>
</ul>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20210731/20210731213012.png" alt="f:id:tsgcpp:20210731213012p:plain" width="413" height="77" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<h2 id="GitHub上でアクセストークンを発行"><a class="keyword" href="http://d.hatena.ne.jp/keyword/GitHub">GitHub</a>上でアクセス<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C8%A1%BC%A5%AF">トーク</a>ンを発行</h2>
<ul>
<li>Settings -> Developer Settings -> Personal <a class="keyword" href="http://d.hatena.ne.jp/keyword/access">access</a> tokens のページに進む
<ul>
<li>URL: <a href="https://github.com/settings/tokens">https://github.com/settings/tokens</a></li>
</ul>
</li>
<li>"Generate new tokens" のボタンをクリック</li>
</ul>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20210731/20210731222321.jpg" alt="f:id:tsgcpp:20210731222321j:plain" width="1132" height="270" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<ul>
<li>Note 項目に任意の名前を指定 (ex: Package <a class="keyword" href="http://d.hatena.ne.jp/keyword/Access">Access</a>)</li>
<li>(任意) 期限(Expiration)を設定</li>
<li><code>read:packages</code> をチェック
<ul>
<li>パッケージアクセスのみであれば <code>read:packages</code> のみで問題有りません</li>
</ul>
</li>
</ul>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20210731/20210731213813.png" alt="f:id:tsgcpp:20210731213813p:plain" width="871" height="122" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<ul>
<li>ページ下部の "Generate token" ボタンで発行</li>
<li>発行された<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C8%A1%BC%A5%AF">トーク</a>ンをコピー
<ul>
<li><span style="color: #ff0000">ページを閉じると再確認できません!</span></li>
</ul>
</li>
</ul>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20210731/20210731214220.jpg" alt="f:id:tsgcpp:20210731214220j:plain" width="848" height="278" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<h2 id="upmconfigtoml-ファイルを作成">.upmconfig.toml ファイルを作成</h2>
<ul>
<li><code>.upmconfig.toml</code> は UPM におけるアクセス設定ファイル
<ul>
<li><a href="https://docs.unity3d.com/Manual/upm-config.html">https://docs.unity3d.com/Manual/upm-config.html</a></li>
<li>ファイルパスも上記ページの "User configuration file location" を参照</li>
</ul>
</li>
<li>アクセス先(<code>npmAuth</code>)とそのアクセス<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C8%A1%BC%A5%AF">トーク</a>ンを記載</li>
</ul>
<pre class="code" data-lang="" data-unlink>[npmAuth."https://npm.pkg.github.com/@<user or organization>"]
token = "<ACCESS TOKEN>"
alwaysAuth = true</pre>
<p>以下は記入イメージ</p>
<pre class="code" data-lang="" data-unlink>[npmAuth."https://npm.pkg.github.com/@tsgcpp"]
token = "ghp_hogehogehogehogehoge"
alwaysAuth = true</pre>
<p>これでUnityのパッケージインストール時にアクセス<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C8%A1%BC%A5%AF">トーク</a>ンが使用されます</p>
<h2 id="Unity-で-Scoped-Registries-とインストールパッケージを設定">Unity で Scoped Registries とインストールパッケージを設定</h2>
<ul>
<li><code>Packages/manifest.json</code> をエディタで開く</li>
<li><code>scopedRegistries</code> 項目に記載
<ul>
<li><code>url</code> には <code>https://npm.pkg.github.com/@<user or organization></code></li>
<li><code>scopes</code> に <a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C9%A5%E1%A5%A4%A5%F3">ドメイン</a>部分を記載</li>
</ul>
</li>
</ul>
<p>以下は記入例で、<code>com.tsgcpp</code>とついたパッケージは GHP の <code>@tsgcpp</code> 経由で取得されます。</p>
<pre class="code lang-json" data-lang="json" data-unlink><span class="synSpecial">{</span>
"<span class="synStatement">scopedRegistries</span>": <span class="synSpecial">[</span>
<span class="synSpecial">{</span>
"<span class="synStatement">name</span>": "<span class="synConstant">tsgcpp public</span>",
"<span class="synStatement">url</span>": "<span class="synConstant">https://npm.pkg.github.com/@tsgcpp</span>",
"<span class="synStatement">scopes</span>": <span class="synSpecial">[</span>
"<span class="synConstant">com.tsgcpp</span>"
<span class="synSpecial">]</span>
<span class="synSpecial">}</span>
<span class="synSpecial">]</span>,
"<span class="synStatement">dependencies</span>": <span class="synSpecial">{</span>
"<span class="synStatement">com.tsgcpp.unitygithubpackageexample.integration</span>": "<span class="synConstant">1.2.3</span>",
...
<span class="synSpecial">}</span>
<span class="synSpecial">}</span>
</pre>
<h3 id="補足-comtsgcppunitygithubpackageexampleintegration-サンプルパッケージ-の依存グラフ-について">補足: com.tsgcpp.unitygithubpackageexample.integration (サンプルパッケージ) の依存グラフ について</h3>
<ul>
<li>今回 integration, script, prefab, material のパッケージは以下の依存関係</li>
<li><code>com.tsgcpp.unitygithubpackageexample.integration</code> のみの取り込みで他3パッケージも取得</li>
</ul>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20210731/20210731220943.jpg" alt="f:id:tsgcpp:20210731220943j:plain" width="521" height="121" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<h3 id="インストールの確認">インストールの確認</h3>
<ul>
<li>Unityエディタ上で <code>Packages</code> 項目を確認しましょう</li>
</ul>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20210731/20210731221437.png" alt="f:id:tsgcpp:20210731221437p:plain" width="347" height="194" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p><br></p>
<p>以上がGHP経由での取込までの流れとなります!</p>
<p>ちなみに紹介したとおりに <code>.upmconfig.toml</code> と <code>manifest.json</code> を設定すれば、<br />
私が用意したサンプルパッケージ (public) が取り込まれます。</p>
<p>残りは GHP を使用する場合のメリット、デメリットを紹介します!</p>
<p><br></p>
<h1 id="GitHub-PackageでUnityパッケージを配布するメリット"><a class="keyword" href="http://d.hatena.ne.jp/keyword/GitHub">GitHub</a> PackageでUnityパッケージを配布するメリット</h1>
<h2 id="packagejson-の-dependencies-に依存を定義可能">package.<a class="keyword" href="http://d.hatena.ne.jp/keyword/json">json</a> の dependencies に依存を定義可能</h2>
<ul>
<li>GHPにアップロードされたパッケージは <code>dependencies</code> 項目で依存パッケージを指定可能</li>
<li>UPMで取り込んだ場合は依存パッケージも合わせて取り込まれる
<ul>
<li>Git URL 経由では難しかった依存パッケージの同時取込が可能</li>
</ul>
</li>
</ul>
<pre class="code lang-json" data-lang="json" data-unlink><span class="synSpecial">{</span>
"<span class="synStatement">name</span>": "<span class="synConstant">com.tsgcpp.unitygithubpackageexample.integration</span>",
"<span class="synStatement">version</span>": "<span class="synConstant">1.2.3</span>",
"<span class="synStatement">displayName</span>": "<span class="synConstant">Integration Package Example</span>",
"<span class="synStatement">description</span>": "<span class="synConstant">Integration Package Example for UnityGithubPackageExample</span>",
"<span class="synStatement">unity</span>": "<span class="synConstant">2019.4</span>",
...
"<span class="synStatement">dependencies</span>": <span class="synSpecial">{</span>
"<span class="synStatement">com.tsgcpp.unitygithubpackageexample.script</span>": "<span class="synConstant">2.3.4</span>",
"<span class="synStatement">com.tsgcpp.unitygithubpackageexample.prefab</span>": "<span class="synConstant">3.4.5</span>"
<span class="synSpecial">}</span>,
...
<span class="synSpecial">}</span>
</pre>
<h2 id="Privateリポジトリでのパッケージ配布が容易">Private<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>でのパッケージ配布が容易</h2>
<ul>
<li><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>にアクセス可能な人限定での配布も可能</li>
<li><a class="keyword" href="http://d.hatena.ne.jp/keyword/GitHub">GitHub</a>向けのアクセス<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C8%A1%BC%A5%AF">トーク</a>ンを利用することで実現</li>
</ul>
<h3 id="補足-Git-URLでもPrivateリポジトリから取り込みは一応可能">補足: Git URLでもPrivate<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>から取り込みは一応可能</h3>
<p>最近のUnityではGit URL経由の場合も、専用のアクセス<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C8%A1%BC%A5%AF">トーク</a>ンを勝手に作ってくれたりします。</p>
<p>ただ、アクセス<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C8%A1%BC%A5%AF">トーク</a>ンの削除が面倒だったり、<a class="keyword" href="http://d.hatena.ne.jp/keyword/Windows">Windows</a>と<a class="keyword" href="http://d.hatena.ne.jp/keyword/Mac">Mac</a>で動作が異なったり、<br />
チーム内での厳密な運用には若干向かない印象があります。</p>
<h3 id="GitHubアカウントの仕組みでアクセス権限を制御可能"><a class="keyword" href="http://d.hatena.ne.jp/keyword/GitHub">GitHub</a>アカウントの仕組みでアクセス権限を制御可能</h3>
<ul>
<li><a class="keyword" href="http://d.hatena.ne.jp/keyword/GitHub">GitHub</a>の <code>Manage Access</code> でパッケージのアクセス範囲を制御可能
<ul>
<li>限定配布が容易になる理由の1つ</li>
</ul>
</li>
</ul>
<h2 id="独自のnpmレジストリが不要">独自のnpm<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EC%A5%B8%A5%B9%A5%C8%A5%EA">レジストリ</a>が不要</h2>
<ul>
<li>privateな独自のregistryの作成、保守、運用が不要</li>
</ul>
<p><br></p>
<h1 id="GitHub-PackageでUnityパッケージを配布するデメリット"><a class="keyword" href="http://d.hatena.ne.jp/keyword/GitHub">GitHub</a> PackageでUnityパッケージを配布するデメリット</h1>
<h2 id="Unity-Package-Manager-のパッケージ検索機能が使用不可">Unity Package Manager のパッケージ検索機能が使用不可</h2>
<ul>
<li>GHP側に検索<a class="keyword" href="http://d.hatena.ne.jp/keyword/API">API</a> endpoint (<code>/-/all</code>, <code>/-/v1/search</code>)が提供されていない</li>
<li>UPMでパッケージ検索が発生すると毎回エラーログが出てしまう
<ul>
<li>UPMウィンドウを開いたときなどに <code>scopedRegistries</code> に登録された<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EC%A5%B8%A5%B9%A5%C8%A5%EA">レジストリ</a>全体で検索が実施される仕様のため</li>
</ul>
</li>
</ul>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20210731/20210731165326.png" alt="f:id:tsgcpp:20210731165326p:plain" width="899" height="199" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p><span style="color: #ff0000">ビルド自体には影響しません</span></p>
<h2 id="Packages以下で-displayName-で表示されない">Packages以下で displayName で表示されない</h2>
<p>パッケージの使用自体は問題ない (と思います)</p>
<ul>
<li>原因不明、検索機能が使えないことが原因?</li>
<li><code>displayName</code> 自体は認識されている</li>
<li><code>Add package from git URL...</code> の場合は問題なく <code>displayName</code> で表示される</li>
</ul>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20210731/20210731171050.png" alt="f:id:tsgcpp:20210731171050p:plain" width="383" height="110" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20210731/20210731171836.jpg" alt="f:id:tsgcpp:20210731171836j:plain" width="447" height="159" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p><br></p>
<h1 id="GitHub-Package-を使用する場合の諸注意"><a class="keyword" href="http://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Package を使用する場合の諸注意</h1>
<ul>
<li>privateの場合はストレージ枠に注意
<ul>
<li>アップロードされたパッケージ毎にストレージ枠を消費</li>
<li>巨大なファイルをアップロードする場合は特に注意 (3Dモデル, <a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B5%A5%A6%A5%F3%A5%C9">サウンド</a>ファイル、動画ファイルなど)</li>
</ul>
</li>
<li>パッケージ削除は一応可能ですが、基本的に削除しない方針を推奨
<ul>
<li><span style="color: #ff0000">作業コストとヒューマンエラーにつながる</span></li>
<li>特にパッケージが大量になったときの手作業は担当者が地獄を見ます</li>
</ul>
</li>
</ul>
<p>おサイフと相談しましょう!ご利用は計画的に!</p>
<h1 id="サンプル">サンプル</h1>
<h2 id="サンプルリポジトリ">サンプル<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a></h2>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Ftsgcpp%2FUnityGithubPackageExample" title="GitHub - tsgcpp/UnityGithubPackageExample: Unity package example with Github Packages" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://github.com/tsgcpp/UnityGithubPackageExample">github.com</a></cite></p>
<h2 id="サンプルパッケージ">サンプルパッケージ</h2>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Ftsgcpp%3Ftab%3Dpackages%26repo_name%3DUnityGithubPackageExample" title="tsgcpp - Packages" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://github.com/tsgcpp?tab=packages&repo_name=UnityGithubPackageExample">github.com</a></cite></p>
<h2 id="サンプルパッケージ発行時のアクション">サンプルパッケージ発行時のアクション</h2>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Ftsgcpp%2FUnityGithubPackageExample%2Factions%2Fruns%2F1064171293" title="Merge pull request #3 from tsgcpp/feature/init · tsgcpp/UnityGithubPackageExample@9f013b9 " class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://github.com/tsgcpp/UnityGithubPackageExample/actions/runs/1064171293">github.com</a></cite></p>
<h1 id="参考">参考</h1>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fforum.unity.com%2Fthreads%2Fusing-github-packages-registry-with-unity-package-manager.861076%2F" title="Unity - Using GitHub Packages Registry with Unity Package Manager" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://forum.unity.com/threads/using-github-packages-registry-with-unity-package-manager.861076/">forum.unity.com</a></cite></p>
<h1 id="雑感">雑感</h1>
<p><a class="keyword" href="http://d.hatena.ne.jp/keyword/GitHub">GitHub</a>にサンプルを作成してからちょっと遅れてしまいました。</p>
<p>今週はちょっとドタバタしていたので、今日は集中して記事にしてみました!</p>
<p>GHP経由の場合は検索機能がありませんが、依存パッケージを定義できたりアクセス制御も<a class="keyword" href="http://d.hatena.ne.jp/keyword/GitHub">GitHub</a>の仕組みを流用できたりで運用上もメリットがあると思います!</p>
<p>あと、displayNameが反映されない問題は気になるのであとでUnityに報告しておきます(公開パッケージがあったほうが報告しやすかったので)。</p>
<p>それでは~</p>
tsgcpp
(旧)【GitHub Packages for Unity】限定配布も可能, GitHub Packages で Unity アセット配布
hatenablog://entry/26006613790454066
2021-08-01T00:16:06+09:00
2021-08-01T00:16:06+09:00 以下にページに移行しました tsgcpp.hateblo.jp
<p>以下にページに移行しました</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftsgcpp.hateblo.jp%2Fentry%2Fgithub_packages_for_unity" title="【GitHub Packages for Unity】限定配布も可能, GitHub Packages で Unity アセット配布 - すぎしーのXRと3DCG" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://tsgcpp.hateblo.jp/entry/github_packages_for_unity">tsgcpp.hateblo.jp</a></cite></p>
tsgcpp
【感想】 Adaptive Code のススメ
hatenablog://entry/26006613713515710
2021-07-05T21:08:23+09:00
2021-07-05T21:08:23+09:00 概要 知見 リスコフの置換原則 (Liskov Substitution Principle) の保証 LSPの概要 LSPの保証方法 LSP保証のテストクラスのサンプル テスト対象のinterface ルール定義 ベースのテストクラスを定義 継承クラスのテストを作成 テストの実行 LSPのまとめ 投機的な一般化(Speculative Generality) 投機的な一般化の概要 予想されるバリエーション 投機的な一般化の問題 投機的な一般化の回避例 修飾子(sealed, internalなど)を指定 プロダクトの拡張ポイントを見極める 投機的な一般化のまとめ コナーセンス(Connasc…
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20210704/20210704153046.jpg" alt="f:id:tsgcpp:20210704153046j:plain" width="512" height="288" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<ul class="table-of-contents">
<li><a href="#概要">概要</a></li>
<li><a href="#知見">知見</a><ul>
<li><a href="#リスコフの置換原則-Liskov-Substitution-Principle-の保証">リスコフの置換原則 (Liskov Substitution Principle) の保証</a><ul>
<li><a href="#LSPの概要">LSPの概要</a></li>
<li><a href="#LSPの保証方法">LSPの保証方法</a></li>
<li><a href="#LSP保証のテストクラスのサンプル">LSP保証のテストクラスのサンプル</a><ul>
<li><a href="#テスト対象のinterface">テスト対象のinterface</a></li>
<li><a href="#ルール定義">ルール定義</a></li>
</ul>
</li>
<li><a href="#ベースのテストクラスを定義">ベースのテストクラスを定義</a></li>
<li><a href="#継承クラスのテストを作成">継承クラスのテストを作成</a></li>
<li><a href="#テストの実行">テストの実行</a></li>
<li><a href="#LSPのまとめ">LSPのまとめ</a></li>
</ul>
</li>
<li><a href="#投機的な一般化Speculative-Generality">投機的な一般化(Speculative Generality)</a><ul>
<li><a href="#投機的な一般化の概要">投機的な一般化の概要</a></li>
<li><a href="#予想されるバリエーション">予想されるバリエーション</a></li>
<li><a href="#投機的な一般化の問題">投機的な一般化の問題</a></li>
<li><a href="#投機的な一般化の回避例">投機的な一般化の回避例</a><ul>
<li><a href="#修飾子sealed-internalなどを指定">修飾子(sealed, internalなど)を指定</a></li>
<li><a href="#プロダクトの拡張ポイントを見極める">プロダクトの拡張ポイントを見極める</a></li>
</ul>
</li>
<li><a href="#投機的な一般化のまとめ">投機的な一般化のまとめ</a></li>
</ul>
</li>
<li><a href="#コナーセンスConnascence">コナーセンス(Connascence)</a><ul>
<li><a href="#コナーセンスの概要">コナーセンスの概要</a></li>
<li><a href="#コナーセンスの結合度">コナーセンスの結合度</a><ul>
<li><a href="#名前のコナーセンス">名前のコナーセンス</a></li>
<li><a href="#値のコナーセンス">値のコナーセンス</a></li>
</ul>
</li>
<li><a href="#コナーセンスのまとめ">コナーセンスのまとめ</a></li>
</ul>
</li>
</ul>
</li>
<li><a href="#サンプル">サンプル</a></li>
<li><a href="#雑感">雑感</a></li>
</ul>
<h1 id="概要">概要</h1>
<p>今回は 「Adaptive Code <a class="keyword" href="http://d.hatena.ne.jp/keyword/C%23">C#</a>実践開発手法 第2版」を読み終わったので、<br />
そこで得た知見から特に感銘を受けた部分を3つ共有しようと思います。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.nikkeibp.co.jp%2Fatclpubmkt%2Fbook%2F18%2FP53540%2F" title="Adaptive Code ~ C#実践開発手法 第2版" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://www.nikkeibp.co.jp/atclpubmkt/book/18/P53540/">www.nikkeibp.co.jp</a></cite></p>
<p>Unity開発者なので、Unityと<a class="keyword" href="http://d.hatena.ne.jp/keyword/C%23">C#</a>を題材にして紹介します。</p>
<p><span style="color: #cccccc">本当は数か月前には読み終わってたんですが、記事にできていませんでした。。。復習の意味も込めて記事にします。</span></p>
<h1 id="知見">知見</h1>
<h2 id="リスコフの置換原則-Liskov-Substitution-Principle-の保証">リスコフの置換原則 (Liskov Substitution Principle) の保証</h2>
<p>以下 LSP と呼称します。</p>
<h3 id="LSPの概要">LSPの概要</h3>
<p>平たく言うと<br />
<i>"サブクラス、継承クラスはベースクラスに置き換え可能でなければならない"</i><br />
というSOLID原則の1つですね。</p>
<p>本には「<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%C8%A5%E9">コントラ</a>クト(ルール定義)」として<b>事前条件、事後条件、データ不変条件</b>などといった重要な要素も紹介されています。</p>
<p>この本から得られたのは単なるリスコフの置換原則の知識ではなく、</p>
<p>「<b>如何にしてリスコフの置換原則を保証するか</b>」</p>
<p>という部分でした。</p>
<h3 id="LSPの保証方法">LSPの保証方法</h3>
<p>結論を述べるとテストクラスを活用します</p>
<ol>
<li>ベースのテストクラスを定義</li>
<li><b>継承クラスのテストはベースのテストを継承 </b></li>
</ol>
<p>つまり、単に「LSPを満たすように実装する」だけではなく、<br />
「LSPを<a class="keyword" href="http://d.hatena.ne.jp/keyword/%B5%A1%B3%A3%C5%AA">機械的</a>に保証する」をテストクラスで実現することになります。</p>
<h3 id="LSP保証のテストクラスのサンプル">LSP保証のテストクラスのサンプル</h3>
<h4 id="テスト対象のinterface">テスト対象のinterface</h4>
<p>今回の題材は「数値の丸め処理を行うメソッド」とします。<br />
以下の <code>IRounder.Round</code> の事前条件、事後条件をベースのテストクラスで定義しましょう。</p>
<pre class="code lang-cs" data-lang="cs" data-unlink><span class="synType">public</span> <span class="synType">interface</span> IRounder
{
<span class="synType">float</span> Round(<span class="synType">float</span> <span class="synStatement">value</span>, <span class="synType">float</span> interval);
}
</pre>
<h4 id="ルール定義">ルール定義</h4>
<ul>
<li>事前条件
<ul>
<li>intervalに0は指定不可</li>
<li>intervalに負の値は指定不可</li>
</ul>
</li>
<li>事後条件
<ul>
<li>引数の<a class="keyword" href="http://d.hatena.ne.jp/keyword/value">value</a>とintervalが同じ値の場合はintervalを返すこと</li>
</ul>
</li>
</ul>
<h3 id="ベースのテストクラスを定義">ベースのテストクラスを定義</h3>
<p>ベースのテストクラスのポイントとして</p>
<ul>
<li><b>abstractクラスとして定義</b></li>
<li><b>abstractメソッドとして生成メソッドを定義</b>
<ul>
<li>継承クラスの<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>生成を定義可能にするため</li>
</ul>
</li>
<li>ルールを満たすことを確認するテストを定義</li>
</ul>
<pre class="code lang-cs" data-lang="cs" data-unlink><span class="synStatement">using</span> NUnit.Framework;
<span class="synType">public</span> <span class="synType">abstract</span> <span class="synType">class</span> TestRounderBase
{
<span class="synComment">// abstract指定により対象オブジェクトのインスタンス生成を継承テストクラスで定義</span>
<span class="synType">public</span> <span class="synType">abstract</span> IRounder CreateTarget();
IRounder _target;
[SetUp]
<span class="synType">public</span> <span class="synType">void</span> SetUp() => _target = CreateTarget();
<span class="synComment">// 事前条件1: intervalに0は指定不可として、例外を出すこと</span>
[Test]
<span class="synType">public</span> <span class="synType">void</span> Round_ThrowsExceptionIfIntervalIsZero()
{
Assert.Throws<RounderException>(() =>
{
_target.Round(<span class="synConstant">1f</span>, interval: <span class="synConstant">0f</span>);
});
}
<span class="synComment">// 事前条件2: intervalに負の値は指定不可として、例外を出すこと</span>
[Test]
<span class="synType">public</span> <span class="synType">void</span> Round_ThrowsExceptionIfIntervalIsLessThanZero()
{
Assert.Throws<RounderException>(() =>
{
_target.Round(<span class="synConstant">1f</span>, interval: -<span class="synConstant">1f</span>);
});
}
<span class="synComment">// 事後条件1: 引数のvalueとintervalが同じ値の場合はintervalを返すこと</span>
[Test]
<span class="synType">public</span> <span class="synType">void</span> Round_ReturnsIntervalIfValueIsEqualToInterval()
{
Assert.AreEqual(<span class="synConstant">1f</span>, _target.Round(<span class="synConstant">1f</span>, interval: <span class="synConstant">1f</span>));
Assert.AreEqual(<span class="synConstant">2f</span>, _target.Round(<span class="synConstant">2f</span>, interval: <span class="synConstant">2f</span>));
Assert.AreEqual(<span class="synConstant">10f</span>, _target.Round(<span class="synConstant">10f</span>, interval: <span class="synConstant">10f</span>));
}
}
</pre>
<h3 id="継承クラスのテストを作成">継承クラスのテストを作成</h3>
<p>ベースクラスを継承したテストクラスを作成。<br />
ベースクラスを継承して、継承クラスの<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>生成メソッドを定義する。</p>
<pre class="code lang-cs" data-lang="cs" data-unlink><span class="synType">public</span> <span class="synType">class</span> TestNearestRounder_LSP : TestRounderBase
{
<span class="synType">public</span> <span class="synType">override</span> IRounder CreateTarget() => <span class="synStatement">new</span> NearestRounder();
}
</pre>
<p>ちなみに <code>NearestRounder</code> の定義は以下です。</p>
<pre class="code lang-cs" data-lang="cs" data-unlink><span class="synStatement">using</span> System;
<span class="synType">public</span> <span class="synType">sealed</span> <span class="synType">class</span> NearestRounder : IRounder
{
<span class="synType">public</span> <span class="synType">float</span> Round(<span class="synType">float</span> <span class="synStatement">value</span>, <span class="synType">float</span> interval)
{
<span class="synStatement">if</span> (interval <= <span class="synConstant">0f</span>)
{
<span class="synStatement">throw</span> <span class="synStatement">new</span> RounderException(<span class="synConstant">"</span><span class="synSpecial">\"</span><span class="synConstant">interval</span><span class="synSpecial">\"</span><span class="synConstant"> is 0 or less"</span>);
}
<span class="synStatement">return</span> (<span class="synType">float</span>)Math.Round(<span class="synStatement">value</span> / interval) * interval;
}
}
</pre>
<p>テスト全体は <a href="https://github.com/tsgcpp/BlogCodeExample-AdaptiveCode/blob/master/Assets/Tests/LiskovSubstitutionPrinciple/TestNearestRounder.cs">TestNearestRounder.cs</a> で確認できます。<br />
また、こちらのサンプルには <code>NearestRounder</code>特有のテストも別クラスとして記載しています。</p>
<h3 id="テストの実行">テストの実行</h3>
<p><code>IRounder</code>の継承クラスがLSPを満たしていることをUnitTestで確認。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20210704/20210704120333.png" alt="f:id:tsgcpp:20210704120333p:plain" width="615" height="306" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p>また、以後ルールを追加したい場合はベースクラスに追加することで、<br />
継承クラスも常にルールを満たしていることが検証されるようになります。</p>
<h3 id="LSPのまとめ">LSPのまとめ</h3>
<p>ベースとなるテストクラスでLSPのルールを定義することで、より強固なLSPを守るフローを構築することが可能になります。</p>
<p>interfaceのコメントに「intervalは0以上を必ず指定させること」などを記載してもプログラム上では何の影響もありませんが、
テストクラスを再利用することで保守する上でも保証することが可能になります。</p>
<p>abstractなテストクラスを利用する発想があまりなかったので、目からうろこでした。</p>
<h2 id="投機的な一般化Speculative-Generality">投機的な一般化(Speculative Generality)</h2>
<p>書籍には "投機的な一般化" と記載されていましたが文字だけだとなんのこっちゃ感がありますね(笑)。</p>
<p>原文は"Speculative Generality"です。</p>
<p>ちなみに書籍では「開放/ 閉鎖 の 原則」を意識するための要素として紹介されています。</p>
<h3 id="投機的な一般化の概要">投機的な一般化の概要</h3>
<p>ここは私自身の解釈も含まれますが、端的に言えば</p>
<p><i>"あると良さそうと思って拡張可能に実装した、でも結局使わなかった"</i></p>
<p>的なことです。</p>
<p>interfaceを使った抽象化を身に着けたばかりの<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D7%A5%ED%A5%B0%A5%E9%A5%DE%A1%BC">プログラマー</a>によくある行動として、<br />
「あらゆるものをinterface化して、意図しない拡張性を持たせてしまう」というものがり、これが投機的な一般化となる可能性があります。</p>
<p>「すべての設計は明確な意図をもって行うべき」との説明があり、<br />
投機的な一般化はそれに反する行いのことです。</p>
<h3 id="予想されるバリエーション">予想されるバリエーション</h3>
<p>「予想されるバリエーション」とは「何が拡張できて、何が拡張できないのかを明確にすべきである」と説明があります。<br />
投機的な一般化の対比となり、設計は「投機的な一般化」ではなく「予想されるバリエーション」となるようにするべきということになります。</p>
<h3 id="投機的な一般化の問題">投機的な一般化の問題</h3>
<ul>
<li>拡張ポイント特有の実装が無駄になってしまう
<ul>
<li>interfaceで抽象化されたメソッドを使用する場合、そのメソッドのあらゆる結果の対策が必要となる</li>
<li>例: そのメソッドが異常系を返すパターン、例外を出すパターンなどの考慮が必要となり、結果的に開発コストが高くなる。</li>
</ul>
</li>
<li>意図しない継承クラスが作成されてしまう可能性がある
<ul>
<li>継承クラスが作成された場合は継承クラス特有の考慮が必要になる</li>
<li><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A2%A5%BB%A5%F3%A5%D6%A5%EA">アセンブリ</a>(Unityにおけるasmdef)に継承クラスが分散し、むやみにinterfaceを変更できなくなる</li>
</ul>
</li>
</ul>
<h3 id="投機的な一般化の回避例">投機的な一般化の回避例</h3>
<h4 id="修飾子sealed-internalなどを指定">修飾子(sealed, internalなど)を指定</h4>
<p>修飾子は<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%D1%A5%A4%A5%E9">コンパイラ</a>が解釈する要素となります。<br />
つまりクラスのルールを<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%D1%A5%A4%A5%EB">コンパイル</a>レベルで定義することが可能になります。</p>
<ul>
<li>クラスに<code>sealed</code>を指定し、派生クラスの作成を制限
<ul>
<li>もし派生クラスを想定していないのであれば基本的に<code>sealed</code>を付けて保守範囲を限定する</li>
<li>余談: <a class="keyword" href="http://d.hatena.ne.jp/keyword/C%23">C#</a>はデフォルトで非<code>sealed</code> (派生クラスの作成可能)</li>
</ul>
</li>
<li>クラスに<code>internal</code>を指定し、異なる<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A2%A5%BB%A5%F3%A5%D6%A5%EA">アセンブリ</a>での派生クラス発生の回避
<ul>
<li><code>internal</code>クラスは<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A2%A5%BB%A5%F3%A5%D6%A5%EA">アセンブリ</a>内でのみアクセス可能なので、自分の管轄外での派生クラスの発生を回避できます</li>
<li>余談、もしinternalクラスのUnitTestをしたい場合は <a href="https://docs.microsoft.com/en-us/dotnet/api/system.runtime.compilerservices.internalsvisibletoattribute">InternalsVisibleToAttribute</a> を活用すると良いです</li>
</ul>
</li>
</ul>
<p><span style="color: #cccccc">ちなみにKotlinだとデフォルトでは派生不可(非open)で、僕がKotlinを好きな理由の1つだったりします。</span></p>
<h4 id="プロダクトの拡張ポイントを見極める">プロダクトの拡張ポイントを見極める</h4>
<p>ここはノウハウや経験に基づくものになります。</p>
<p>「投機的な一般化」は拡張部分に焦点を絞ったテーマのため少し話はずれてしまいますが、<br />
以下のことを意識してみるとよいかもしれません。</p>
<ul>
<li>もし自身が企画者である場合は「この拡張が必要な理由が明確になっている」かを考慮する</li>
<li>もし自身が実装する立場であった場合は、企画した人が「とりあえずあると良さそうな部分を提案する」といった性質がある場合は、そのア<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A4%A5%C7%A5%A2">イデア</a>を深堀して必要性を明確化する</li>
</ul>
<p>「15分程度で終わる議論をせず、5日かけて実装したけどやっぱりいらなかった」みたいなことは極力避けましょう。</p>
<p>拡張ポイントを作るということは、それだけ保守範囲も増えるということを意識すると良いと思います。</p>
<h3 id="投機的な一般化のまとめ">投機的な一般化のまとめ</h3>
<p>「すべての設計は明確な意図をもって行うべき」という言葉にはなかなか考えさせられるものがありました。</p>
<p>意図をしない部分があるということは、言ってしまえばあらゆることが起こってしまうと可能性を秘めているということです。<br />
だからこそノウハウを蓄積して、予測できなかった事象を少しでも減らしていきましょう。</p>
<p>私もクラスを作るとき、派生を想定していない場合は <code>sealed</code> を忘れないようになど小さなところから意識するように心掛けていきたいです。</p>
<h2 id="コナーセンスConnascence">コナーセンス(Connascence)</h2>
<h3 id="コナーセンスの概要">コナーセンスの概要</h3>
<p>コナーセンスとは「コードにおける依存度合(結合度)を図る指標」のことです。<br />
依存関係の尺度を見極めるうえで意識すると役立つ概念となります。</p>
<p>コナーセンスにはレベルがあり、高いものほど処理との結合度が強いということになります。</p>
<h3 id="コナーセンスの結合度">コナーセンスの結合度</h3>
<p>結合度を考える場合に簡単な判断として、<br />
「ある要素を変更した際の影響度合い」と考えても良いと思います。</p>
<p>具体例を2つほど出します</p>
<h4 id="名前のコナーセンス">名前のコナーセンス</h4>
<p>"名前" のコナーセンスは、結合度が高いです。</p>
<p>ここでいう名前とは、クラス名やメソッド名など"<b><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%D1%A5%A4%A5%E9">コンパイラ</a>が解釈する名前</b>"のことです。<br />
注意として、<span style="color: #ff0000">メソッド名に使用されている単語自体の意味は関係ありません</span>。</p>
<p>とあるメソッド名を変更したときは、そのクラスを使っていた部分も修正しないと<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%D1%A5%A4%A5%EB">コンパイル</a>エラーが発生しますよね?</p>
<pre class="code lang-cs" data-lang="cs" data-unlink> <span class="synType">public</span> <span class="synType">float</span> Calc() => ...
<span class="synType">public</span> <span class="synType">void</span> Main()
{
var result = Calc();
...
}
</pre>
<p>上記のコードで、もし <code>Calc()</code> -> <code>Calculate()</code>とリネームした場合は、<code>Main</code>関数内も修正が必要になります。<br />
クラス名やメソッド名は名前を変えると、それだけで使用者に影響を与えるため強い結合度を持っていると言えます。</p>
<h4 id="値のコナーセンス">値のコナーセンス</h4>
<p>"値" のコナーセンスは、結合度が低いです。</p>
<p>これは身近ないい例があります。<br />
よく「<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%DE%A5%B8%A5%C3%A5%AF%A5%CA%A5%F3%A5%D0%A1%BC">マジックナンバー</a>は定数化しましょう」みたいなことを聞きませんか?</p>
<p>なぜ定数化するかというと、定数名に意味を込めて意図が伝わりやすいようにするためという側面もあります。<br />
(もちろん、共通の定数を再利用するといった理由もあります)</p>
<pre class="code lang-cs" data-lang="cs" data-unlink> <span class="synType">public</span> <span class="synType">float</span> CalcArea(<span class="synType">float</span> radius) => radius * radius * <span class="synConstant">3.141592f</span>;
</pre>
<p>例えば上記のように3.14...とみると大体の人は「円周率だ!」となると思います。<br />
しかし、すべての値がそれだけで意図が伝わるとは限りません。</p>
<pre class="code lang-cs" data-lang="cs" data-unlink> <span class="synType">public</span> <span class="synType">int</span> CheckCode(<span class="synType">int</span> code) => code == <span class="synConstant">200</span> ? <span class="synConstant">0</span> : -<span class="synConstant">1</span>;
</pre>
<p>上記のコードは見慣れた人には「ああ、HTTPステータスかな?」となると思いますが、<br />
見慣れない人には人には暗号のように見えると思います。</p>
<p>コードを書いた人は「<a class="keyword" href="http://d.hatena.ne.jp/keyword/HTTP%A5%B9%A5%C6%A1%BC%A5%BF%A5%B9%A5%B3%A1%BC%A5%C9">HTTPステータスコード</a>によってエラーコードを返すようにした」つもりで書いても、<br />
第<a class="keyword" href="http://d.hatena.ne.jp/keyword/%BB%B0%BC%D4">三者</a>には伝わらりづらいコードになるかもしれませんので、以下のように定数名に意味を込めてあげましょう。</p>
<pre class="code lang-cs" data-lang="cs" data-unlink> <span class="synType">private</span> <span class="synType">const</span> <span class="synType">int</span> HttpOk = <span class="synConstant">200</span>;
<span class="synType">private</span> <span class="synType">const</span> <span class="synType">int</span> ValidValue = <span class="synConstant">0</span>;
<span class="synType">private</span> <span class="synType">const</span> <span class="synType">int</span> InvalidValue = -<span class="synConstant">1</span>;
<span class="synType">public</span> <span class="synType">int</span> CheckCode(<span class="synType">int</span> code) => code == HttpOk ? ValidValue : InvalidValue;
</pre>
<p>プログラム的にも 200 -> 100 に置き換えても別に<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%D1%A5%A4%A5%EB">コンパイル</a>は通ってしまいますが、<br />
それだと意図しない挙動になってしまいます。</p>
<p>値のコナーセンスのように結合度の低い場合は、定数化するなどの工夫が求められることが多いです。</p>
<h3 id="コナーセンスのまとめ">コナーセンスのまとめ</h3>
<p>コナーセンスはコード自体の品質を図るうえで非常に役立つ指標になります。</p>
<p>書籍には「<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%BF%A1%BC%A5%D5%A5%A7%A5%A4%A5%B9">インターフェイス</a>のコナーセンス」という非公式なコナーセンスが紹介されています。<br />
<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%BF%A1%BC%A5%D5%A5%A7%A5%A4%A5%B9">インターフェイス</a>は継承するオブジェクトの必須要素やクライアントの依存部分を定義することが可能で、<br />
意味を込めるうえでは非常に有用な要素といえます。</p>
<p>書籍を読むまではコナーセンスという単語自体は知りませんでしたが、<br />
「interfaceで要素を定義する」などは意識していたこともあって、より具体的な概念として知ることができました。</p>
<p>コナーセンスの要素として10個ほど紹介されており、それぞれに特有の観点がありますので気になったら確認してみてください!</p>
<h1 id="サンプル">サンプル</h1>
<p>LSPについてのサンプルコードは以下にあります。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Ftsgcpp%2FBlogCodeExample-AdaptiveCode" title="GitHub - tsgcpp/BlogCodeExample-AdaptiveCode" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://github.com/tsgcpp/BlogCodeExample-AdaptiveCode">github.com</a></cite></p>
<h1 id="雑感">雑感</h1>
<p>今回は技術書の感想という形で記事にしてみました。</p>
<p>こんな1ページの記事では紹介しきれないぐらいたくさんの知見について記述されています。</p>
<p>何気に<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A2%A5%B8%A5%E3%A5%A4%A5%EB">アジャイル</a>開発や<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%E9%A5%E0">スクラム</a>についても簡易的に紹介されていますので、<br />
<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A2%A1%BC%A5%AD%A5%C6%A5%AF%A5%C1%A5%E3">アーキテクチャ</a>やコーディングだけでなく、ワークフローについても勉強になる本だと思います。</p>
<p>SOLID原則をより深堀したい人にはとてもオススメなのでよかったら手に取ってみてください!<br />
それでは~</p>
tsgcpp
【Unity】OculusIntegrationがPackageManager対応しやすくなりました!
hatenablog://entry/26006613779587906
2021-06-25T00:50:29+09:00
2021-06-25T00:50:29+09:00 概要 環境 追記 Meta公式によるPackage Manager対応 (2022/10/12 追記) PackageManager対応方法 1. Oculus Integration パッケージ化専用のUnityプロジェクトを作成 2. Oculus Integrationをインストール 3. gitignoreにOculus向け設定ファイルを追加 4. package.json を作成 5. Assets/Oculusをcommit 6. Private用のGitリポジトリにpush (任意) フォルダ構成 パッケージの取り込み方 Add package from git URL... A…
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20210625/20210625010347.jpg" width="512" height="288" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<ul class="table-of-contents">
<li><a href="#概要">概要</a></li>
<li><a href="#環境">環境</a></li>
<li><a href="#追記">追記</a></li>
<li><a href="#Meta公式によるPackage-Manager対応-20221012-追記">Meta公式によるPackage Manager対応 (2022/10/12 追記)</a></li>
<li><a href="#PackageManager対応方法">PackageManager対応方法</a><ul>
<li><a href="#1-Oculus-Integration-パッケージ化専用のUnityプロジェクトを作成">1. Oculus Integration パッケージ化専用のUnityプロジェクトを作成</a></li>
<li><a href="#2-Oculus-Integrationをインストール">2. Oculus Integrationをインストール</a></li>
<li><a href="#3-gitignoreにOculus向け設定ファイルを追加">3. gitignoreにOculus向け設定ファイルを追加</a></li>
<li><a href="#4-packagejson-を作成">4. package.json を作成</a></li>
<li><a href="#5-AssetsOculusをcommit">5. Assets/Oculusをcommit</a></li>
<li><a href="#6-Private用のGitリポジトリにpush-任意">6. Private用のGitリポジトリにpush (任意)</a></li>
<li><a href="#フォルダ構成">フォルダ構成</a></li>
</ul>
</li>
<li><a href="#パッケージの取り込み方">パッケージの取り込み方</a><ul>
<li><a href="#Add-package-from-git-URL">Add package from git URL...</a></li>
<li><a href="#Add-package-from-disk">Add package from disk...</a></li>
<li><a href="#取り込み後に設定ファイルが生成される">取り込み後に設定ファイルが生成される</a></li>
</ul>
</li>
<li><a href="#対応しやすくなった理由">対応しやすくなった理由</a><ul>
<li><a href="#そもそもなぜ対応が難しかったのか">そもそもなぜ対応が難しかったのか</a><ul>
<li><a href="#暫定対応">暫定対応</a></li>
</ul>
</li>
<li><a href="#PackageManagerで取り込まれた場合の考慮が280以降で追加">PackageManagerで取り込まれた場合の考慮が28.0以降で追加</a></li>
</ul>
</li>
<li><a href="#PackageManager対応するメリット">PackageManager対応するメリット</a></li>
<li><a href="#雑感">雑感</a></li>
</ul>
<h1 id="概要">概要</h1>
<p><a href="https://developer.oculus.com/downloads/package/unity-integration/">Oculus Integration SDK</a> がいつの間にか<a href="https://docs.unity3d.com/Manual/upm-ui.html">Unity Package Manager</a>に対応させやすくなっていたのでちょっと紹介します。</p>
<p>巷では、Oculus Quest向けのSplash Screen が指定可能になったことで話題でしたが、何気にPackageManager対応しやすくなったのも見過ごせないと思っています!</p>
<p>※Splash Screenについての公式ツイート
<blockquote data-conversation="none" class="twitter-tweet" data-lang="ja"><p lang="en" dir="ltr">Introducing Instant Runtime-driven Splash Screens. Get people into your Quest apps faster than ever. Check out our blog post for how to implement: <a href="https://t.co/XLKZy3ukM6">https://t.co/XLKZy3ukM6</a> <a href="https://t.co/kkcSJrMvik">pic.twitter.com/kkcSJrMvik</a></p>— Oculus Developers (@Oculus_Dev) <a href="https://twitter.com/Oculus_Dev/status/1392902838146248704?ref_src=twsrc%5Etfw">2021年5月13日</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> </p>
<h1 id="環境">環境</h1>
<ul>
<li>Oculus Integration <a class="keyword" href="http://d.hatena.ne.jp/keyword/SDK">SDK</a> 28.0 (1.60) 以降</li>
</ul>
<h1 id="追記">追記</h1>
<ul>
<li>2022/10/12
<ul>
<li>Meta公式によるPackage Manager対応</li>
</ul>
</li>
</ul>
<h1 id="Meta公式によるPackage-Manager対応-20221012-追記">Meta公式によるPackage Manager対応 (2022/10/12 追記)</h1>
<p>Meta公式からMeta XR Integration (旧Oculus Integration) v44 から Pacakge Managerによるインストール方法がアナウンスされています。</p>
<p><a href="https://developer.oculus.com/documentation/unity/unity-xr-integration-installer?intern_source=devblog&intern_content=unity-package-manager-integration-sdks">Import Individual SDKs from Unity Package Manager | Oculus Developers</a></p>
<p><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EC%A5%B8%A5%B9%A5%C8%A5%EA">レジストリ</a>は <code>https://npm.developer.oculus.com/</code> とのこと。</p>
<pre class="code" data-lang="" data-unlink>packages are hosted on Meta’s scoped registry: https://npm.developer.oculus.com/. </pre>
<p>現時点では基本機能 (<code>Oculus/VR</code>) のみのようですが、順次対応されるようです。</p>
<p><a href="https://developer.oculus.com/blog/unity-package-manager-integration-sdks/">How Unity Package Manager Can Help You Manage Integration SDKs</a></p>
<p>Meta Quest Proの発表もあったので、対応が進められたのかもしれません。</p>
<p><span style="color: #999999">こちらの記事を公開してから1年4ヶ月は経ってるので、あまり「そのうち」ではありませんでしたね(笑)</span></p>
<h1 id="PackageManager対応方法">PackageManager対応方法</h1>
<h2 id="1-Oculus-Integration-パッケージ化専用のUnityプロジェクトを作成">1. Oculus Integration パッケージ化専用のUnityプロジェクトを作成</h2>
<ul>
<li>パッケージ用のUnityのプロジェクトを作成</li>
</ul>
<h2 id="2-Oculus-Integrationをインストール">2. Oculus Integrationをインストール</h2>
<ul>
<li>Asset Store or <a href="https://developer.oculus.com/downloads/package/unity-integration/">アーカイブページ</a> からOculusIntegrationをダウンロードおよびインストール
<ul>
<li>必要なアセットにチェックを入れて "Import"</li>
</ul>
</li>
</ul>
<p>※今回は基本機能のみをインストールするため <code>Oculus/VR</code>のみ指定</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20210624/20210624231349.jpg" width="353" height="579" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<h2 id="3-gitignoreにOculus向け設定ファイルを追加">3. gitignoreにOculus向け設定ファイルを追加</h2>
<ul>
<li>commitから除外するために.gitignoreにOculus向け設定ファイルを追加
<ul>
<li>パッケージ対応したOculusIntegrationを取り込むと自動的に生成されます</li>
<li>パッケージ側からは除外しておきましょう</li>
</ul>
</li>
</ul>
<pre class="code" data-lang="" data-unlink># Oculus Assets
/[Aa]ssets/[Oo]culus/OculusProjectConfig.asset
/[Aa]ssets/[Oo]culus/OculusProjectConfig.asset.meta
/[Aa]ssets/Resources/OVRPlatformToolSettings.asset
/[Aa]ssets/Resources/OVRPlatformToolSettings.asset.meta</pre>
<h2 id="4-packagejson-を作成">4. package.<a class="keyword" href="http://d.hatena.ne.jp/keyword/json">json</a> を作成</h2>
<ul>
<li>Package Managerに対応させるため <code>Assets/Oculus/package.json</code> を作成
<ul>
<li>nameなどはプロジェクトに合わせて適宜修正してください</li>
</ul>
</li>
</ul>
<pre class="code lang-json" data-lang="json" data-unlink><span class="synSpecial">{</span>
"<span class="synStatement">name</span>": "<span class="synConstant">com.tsgcpp.oculusintegration</span>",
"<span class="synStatement">version</span>": "<span class="synConstant">1.61.0</span>",
"<span class="synStatement">displayName</span>": "<span class="synConstant">OculusIntegration</span>",
"<span class="synStatement">description</span>": "<span class="synConstant">The Oculus Integration SDK (private).</span>",
"<span class="synStatement">unity</span>": "<span class="synConstant">2019.4</span>",
"<span class="synStatement">keywords</span>": <span class="synSpecial">[</span>
"<span class="synConstant">oculus</span>"
<span class="synSpecial">]</span>,
"<span class="synStatement">dependencies</span>": <span class="synSpecial">{}</span>
<span class="synSpecial">}</span>
</pre>
<h2 id="5-AssetsOculusをcommit">5. Assets/Oculusをcommit</h2>
<ul>
<li><code>.gitignore</code> を commit</li>
<li><code>Assets/Oculus</code> と <code>Assets/Oculus.meta</code> をcommit
<ul>
<li>package.<a class="keyword" href="http://d.hatena.ne.jp/keyword/json">json</a>も併せてcommit</li>
</ul>
</li>
</ul>
<h2 id="6-Private用のGitリポジトリにpush-任意">6. Private用のGit<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>にpush (任意)</h2>
<p><span style="color: #ff0000">※Publicにすると再配布になるため注意が必要</span></p>
<ul>
<li>Private用のGit<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>にpushしましょう
<ul>
<li>Git<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>にpushすることで、Git経由でPackage Managerで取り込みが可能となります</li>
</ul>
</li>
</ul>
<h2 id="フォルダ構成">フォルダ構成</h2>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20210624/20210624232305.jpg" width="425" height="229" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<h1 id="パッケージの取り込み方">パッケージの取り込み方</h1>
<ul>
<li>通常のPackageManagerでの取り込みと同様となります</li>
</ul>
<h2 id="Add-package-from-git-URL">Add package from git URL...</h2>
<ul>
<li><code>https://<git url>/<org>/<repository>.git?path=Assets/Oculus#<tag or branch></code> のように指定
<ul>
<li>i.e. <code>https://github.com/tsgcpp/OculusIntegration.git?path=Assets/Oculus#1.61.0</code> (<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>名: OculusIntegration, タグ名: 1.61.0 の場合)</li>
</ul>
</li>
</ul>
<h2 id="Add-package-from-disk">Add package from disk...</h2>
<ul>
<li><code>Assets/Oculus/package.json</code> を指定</li>
</ul>
<h2 id="取り込み後に設定ファイルが生成される">取り込み後に設定ファイルが生成される</h2>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20210625/20210625003014.jpg" width="284" height="322" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p><br><br></p>
<h1 id="対応しやすくなった理由">対応しやすくなった理由</h1>
<h2 id="そもそもなぜ対応が難しかったのか">そもそもなぜ対応が難しかったのか</h2>
<p><b>Oculus Integration <a class="keyword" href="http://d.hatena.ne.jp/keyword/SDK">SDK</a> 27.0 (1.59) 以前の話となります </b></p>
<ul>
<li>原因は <code>Assets/Oculus/VR/Editor/OVRProjectConfig.cs</code>
<ul>
<li><code>OculusProjectConfig.asset</code>(Oculus向けの設定アセット) を<b>自動生成</b>するEditor<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%EA%A5%D7%A5%C8">スクリプト</a></li>
</ul>
</li>
<li>自動生成するパスの指定方法に問題があり、PackageManager経由で取り込んだ場合にエラーが発生
<ul>
<li>端的に言うと<code>OVRProjectConfig.cs</code>の親の親フォルダが生成先</li>
</ul>
</li>
<li>PackageManagerで取り込んだ場合は<code>Assets/</code>以下ではなく、<code>Packages/</code>以下が生成対象となってしまっている
<ul>
<li><code>Packages/com.tsgcpp.oculusintegration/VR/Editor/OVRProjectConfig.cs</code> に配置されているため</li>
<li><code>AssetDatabase.CreateAsset</code>でScriptableObject(<code>OVRProjectConfig</code>)の生成先に<code>Packages/</code>以下を指定するとエラー (<code>Assets/</code>以下ではない場合にエラー)</li>
</ul>
</li>
</ul>
<h3 id="暫定対応">暫定対応</h3>
<p><b>Oculus Integration <a class="keyword" href="http://d.hatena.ne.jp/keyword/SDK">SDK</a> 27.0 (1.59) 以前の話となります </b></p>
<ul>
<li><code>Assets/Oculus</code>以下のEditorフォルダすべてをunitypackageにExport</li>
<li><code>Assets/Oculus</code>以下にある<code>Editor</code>フォルダ以下をすべて削除</li>
<li>OculusIntegrationを使用するメインプロジェクトに上記unitypackageでEditor群を取り込み
<ul>
<li><code>OculusProjectConfig.asset</code>を使用するにはEditor以下の<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%EA%A5%D7%A5%C8">スクリプト</a>が必要なため</li>
</ul>
</li>
</ul>
<p>上記の方法で暫定的に対応することは可能でした。<br />
ただ、Editor<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%EA%A5%D7%A5%C8">スクリプト</a>のみ直接取り込む形となるため、結構残念な感じになってました。</p>
<h2 id="PackageManagerで取り込まれた場合の考慮が280以降で追加">PackageManagerで取り込まれた場合の考慮が28.0以降で追加</h2>
<ul>
<li><a class="keyword" href="http://d.hatena.ne.jp/keyword/SDK">SDK</a> 28.0 (1.60) で以下の考慮が追加されました</li>
<li><code>OVRPluginUpdaterStub.IsInsidePackageDistribution</code> は <code>assetPath.StartsWith("Packages/"</code> の判定を実施</li>
<li><code>Packages/</code>以下だった場合は、<code>Assets/Oculus/OculusProjectConfig.asset</code> を生成先に変更</li>
</ul>
<pre class="code lang-cs" data-lang="cs" data-unlink> <span class="synType">private</span> <span class="synType">static</span> <span class="synType">string</span> GetOculusProjectConfigAssetPath()
{
...
<span class="synStatement">if</span> (OVRPluginUpdaterStub.IsInsidePackageDistribution())
{
</pre>
<p>つまり、<span style="color: #ff0000">PackageManagerで取り込まれた場合でも<code>Assets/Oculus</code>以下に設定ファイルが生成されるようになりました。</span><br />
これがOculus IntegrationがPackageManager対応しやすくなった理由です!</p>
<h1 id="PackageManager対応するメリット">PackageManager対応するメリット</h1>
<p>「別に直接プロジェクトに取り込んでもよくない???」って思った方もいらっしゃると思いますので、<br />
メリットについても補足しようと思います。</p>
<ul>
<li>OculusIntegrationを管理するGit<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>を分割可能
<ul>
<li><a class="keyword" href="http://d.hatena.ne.jp/keyword/GitHub">GitHub</a>などの容量制限にひっかかるのを回避可能</li>
<li>OculusIntegrationは単体でもかなり容量が大きく、追加するアセットによってはあっさり100MBを超えます</li>
<li><code>Assets/Oculus/VR</code>のみでも60MBあります (29.0の場合)</li>
</ul>
</li>
<li>アセットのバージョン管理と更新が容易
<ul>
<li>Package Managerでアセットを取り込むと更新が容易になります(バージョンを更新するのみ)
<ul>
<li>Asset Store(unitypackage)で取り込んだ場合、一度アセットをすべて削除する必要がある場合が多い</li>
</ul>
</li>
<li><span style="color: #ff0000">hookされるEditor<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%EA%A5%D7%A5%C8">スクリプト</a>があるため再起動はどちらにしても推奨</span></li>
</ul>
</li>
</ul>
<h1 id="雑感">雑感</h1>
<p>今回はOculus関連にテーマを絞ってみました。<br />
Oculus Quest2がかなり売れているので、OculusIntegrationの更新もかなり加速している感じがします。</p>
<p>もしかしたら、そのうちUnity Package ManagerでOculus Integrationが提供されるのかもしれませんね。</p>
<p>Unity向けだけじゃなくて、<a class="keyword" href="http://d.hatena.ne.jp/keyword/Unreal">Unreal</a>向けも加速しているような気がします。<br />
Splash Screenも追加できるようになったので、これを機に追加しても良さそうですね。</p>
<p>みなさんのOculus向けアプリケーション開発の助けになれば幸いです。<br />
それでは~。</p>
tsgcpp
【Unity】Physics.SyncTransforms の特性調査
hatenablog://entry/26006613721345196
2021-04-28T22:10:38+09:00
2021-04-28T22:10:38+09:00 経緯 検証環境 前知識 物理オブジェクトの物理エンジンへのFlush Physics.SyncTransforms Physics.autoSyncTransforms Flushの発生条件 Flushの発生タイミング 登場人物 PlayerColliders AnimationColliders PhysicsAnimationColliders DisabledAnimationColliders DisabledAnimationColliders + CapsuleCollider ColliderDisabledAnimationColliders Static Colliders …
<ul class="table-of-contents">
<li><a href="#経緯">経緯</a></li>
<li><a href="#検証環境">検証環境</a></li>
<li><a href="#前知識">前知識</a><ul>
<li><a href="#物理オブジェクトの物理エンジンへのFlush">物理オブジェクトの物理エンジンへのFlush</a></li>
<li><a href="#PhysicsSyncTransforms">Physics.SyncTransforms</a></li>
<li><a href="#PhysicsautoSyncTransforms">Physics.autoSyncTransforms</a></li>
<li><a href="#Flushの発生条件">Flushの発生条件</a></li>
<li><a href="#Flushの発生タイミング">Flushの発生タイミング</a></li>
</ul>
</li>
<li><a href="#登場人物">登場人物</a><ul>
<li><a href="#PlayerColliders">PlayerColliders</a></li>
<li><a href="#AnimationColliders">AnimationColliders</a></li>
<li><a href="#PhysicsAnimationColliders">PhysicsAnimationColliders</a></li>
<li><a href="#DisabledAnimationColliders">DisabledAnimationColliders</a></li>
<li><a href="#DisabledAnimationColliders--CapsuleCollider">DisabledAnimationColliders + CapsuleCollider</a></li>
<li><a href="#ColliderDisabledAnimationColliders">ColliderDisabledAnimationColliders</a></li>
<li><a href="#Static-Colliders">Static Colliders</a></li>
</ul>
</li>
<li><a href="#検証方法">検証方法</a></li>
<li><a href="#検証結果">検証結果</a><ul>
<li><a href="#PlayerColliders-x1">PlayerColliders x1</a></li>
<li><a href="#PlayerColliders-x2">PlayerColliders x2</a></li>
<li><a href="#PlayerColliders-x4">PlayerColliders x4</a></li>
<li><a href="#PlayerColliders-のみ-Animatorをオフ">PlayerColliders のみ (Animatorをオフ)</a></li>
<li><a href="#PlayerColliders--AnimationColliders">PlayerColliders + AnimationColliders</a></li>
<li><a href="#PlayerColliders--PhysicsAnimationColliders">PlayerColliders + PhysicsAnimationColliders</a></li>
<li><a href="#PlayerColliders--DisabledAnimationColliders">PlayerColliders + DisabledAnimationColliders</a></li>
<li><a href="#PlayerColliders--DisabledAnimationColliders--CapsuleCollider">PlayerColliders + DisabledAnimationColliders + CapsuleCollider</a></li>
<li><a href="#PlayerColliders--ColliderDisabledAnimationColliders">PlayerColliders + ColliderDisabledAnimationColliders</a></li>
<li><a href="#PlayerColliders--StaticColliders">PlayerColliders + StaticColliders</a></li>
<li><a href="#注意事項">注意事項</a></li>
</ul>
</li>
<li><a href="#判明した特性">判明した特性</a></li>
<li><a href="#考察">考察</a></li>
<li><a href="#検証プロジェクト">検証プロジェクト</a></li>
<li><a href="#雑感">雑感</a></li>
</ul>
<p>今回は <a href="https://docs.unity3d.com/ScriptReference/Physics.SyncTransforms.html"><code>Physics.SyncTransforms</code></a> の特性を調査したため共有します。</p>
<h1 id="経緯">経緯</h1>
<ul>
<li>Colliderの<a class="keyword" href="http://d.hatena.ne.jp/keyword/%CA%AA%CD%FD%A5%A8%A5%F3%A5%B8%A5%F3">物理エンジン</a>へのFlushを 通常のタイミングとは別で実施したい</li>
<li>Flushを任意のタイミングで実行するには<a href="https://docs.unity3d.com/ScriptReference/Physics.SyncTransforms.html"><code>Physics.SyncTransforms</code></a> をコールする必要があった</li>
<li><code>Physics.SyncTransforms</code>を通常のFlushとは別に実施する場合にパフォーマンスなどが気になったため調査</li>
</ul>
<p>※Flushとは実際にデータを反映する処理のこと(ここでは<a class="keyword" href="http://d.hatena.ne.jp/keyword/%CA%AA%CD%FD%A5%A8%A5%F3%A5%B8%A5%F3">物理エンジン</a>用メモリ領域への反映)</p>
<h1 id="検証環境">検証環境</h1>
<ul>
<li>Unity 2020.3.4f1
<ul>
<li>Mono</li>
<li>.Net Standard 2.0</li>
</ul>
</li>
<li><span style="color: #ff0000">Physics.autoSyncTransforms = false</span></li>
<li>Oculus Quest 2 (<a class="keyword" href="http://d.hatena.ne.jp/keyword/Android">Android</a>)</li>
<li>Development Build 有り(Profiler用)</li>
</ul>
<h1 id="前知識">前知識</h1>
<h2 id="物理オブジェクトの物理エンジンへのFlush">物理オブジェクトの<a class="keyword" href="http://d.hatena.ne.jp/keyword/%CA%AA%CD%FD%A5%A8%A5%F3%A5%B8%A5%F3">物理エンジン</a>へのFlush</h2>
<ul>
<li>ColliderのTransformが<a class="keyword" href="http://d.hatena.ne.jp/keyword/%CA%AA%CD%FD%A5%A8%A5%F3%A5%B8%A5%F3">物理エンジン</a>に反映されるタイミングはFixedUpdate後 (<a href="https://docs.unity3d.com/ScriptReference/Physics-autoSyncTransforms.html"><code>Physics.autoSyncTransforms</code></a> がfalseの場合)
<ul>
<li>厳密には<a href="https://docs.unity3d.com/Manual/ExecutionOrder.html">ExecutionOrder</a> でいう "Internal Physics Update" がFlushのタイミングと考えられる</li>
</ul>
</li>
<li>Flushタイミングは <a href="https://github.com/tsgcpp/SyncTransformPerformanceCheck/blob/master/Assets/Tests/PhysicsSyncTiming/TestPhysicsSyncTiming.cs">TestPhysicsSyncTiming.cs</a> で確認</li>
</ul>
<h2 id="PhysicsSyncTransforms">Physics.SyncTransforms</h2>
<ul>
<li>上記「物理オブジェクトの<a class="keyword" href="http://d.hatena.ne.jp/keyword/%CA%AA%CD%FD%A5%A8%A5%F3%A5%B8%A5%F3">物理エンジン</a>へのFlush」を任意のタイミングで発動させるメソッド</li>
<li><b>今回の主役</b></li>
</ul>
<h2 id="PhysicsautoSyncTransforms">Physics.autoSyncTransforms</h2>
<ul>
<li>ColliderのTransformが変更された際に即座に<a class="keyword" href="http://d.hatena.ne.jp/keyword/%CA%AA%CD%FD%A5%A8%A5%F3%A5%B8%A5%F3">物理エンジン</a>に反映するかの設定</li>
<li>Unity 2019以降はデフォルトでfalse
<ul>
<li>つまりデフォルトではFixedUpdate後にFlushが実施されると同義</li>
</ul>
</li>
<li>ProjectSettingsのPhysicsからも設定可能</li>
</ul>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20210428/20210428212234.jpg" width="636" height="375" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<ul>
<li><a href="https://docs.unity3d.com/ScriptReference/Physics-autoSyncTransforms.html"><code>Physics.autoSyncTransforms</code></a> によるとtrueにするとパフォーマンスの低下の可能性がある</li>
</ul>
<pre class="code" data-lang="" data-unlink>When autoSyncTransforms is set to true, repeatedly changing a Transform and then performing a physics query can cause performance loss.</pre>
<ul>
<li><code>Physics.autoSyncTransforms = true</code>にする場合はオーバーヘッドに注意
<ul>
<li>物理演算の正確性とパフォーマンスが<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C8%A5%EC%A1%BC%A5%C9%A5%AA%A5%D5">トレードオフ</a>となる</li>
</ul>
</li>
</ul>
<p><br>
<span style="color: #ff0000">今回は Physics.autoSyncTransforms = false の場合をメインに調査</span></p>
<h2 id="Flushの発生条件">Flushの発生条件</h2>
<ul>
<li>ColliderのTransformが変更された場合
<ul>
<li>Dirtyフラグが付き、後述の「Flushの発生タイミング」で<a class="keyword" href="http://d.hatena.ne.jp/keyword/%CA%AA%CD%FD%A5%A8%A5%F3%A5%B8%A5%F3">物理エンジン</a>に反映</li>
</ul>
</li>
<li><a href="https://forpro.unity3d.jp/unity_pro_tips/2019/12/03/326/">こちらのUnity Pro Tips</a> によれば移動する物理オブジェクトがない場合はオーバーヘッドはほとんどないとのこと
<ul>
<li>つまり移動しないオブジェクトに関しては static かどうかにかかわらず Flush が発生しない</li>
</ul>
</li>
</ul>
<h2 id="Flushの発生タイミング">Flushの発生タイミング</h2>
<ul>
<li><code>Physics.autoSyncTransforms = false</code> であれば FixedUpdate後 or <code>Physics.SyncTransforms</code> コール時点
<ul>
<li>Dirtyフラグが付いた物理オブジェクトのみ</li>
</ul>
</li>
<li><code>Physics.autoSyncTransforms = true</code> であればColliderのTransformが変更された時点</li>
</ul>
<p><br></p>
<p>-- ここまでが前知識 --</p>
<p><br></p>
<h1 id="登場人物">登場人物</h1>
<h2 id="PlayerColliders">PlayerColliders</h2>
<ul>
<li>CapsuleCollider + BoxCollider 64個 (4 x 4 x 4) をアニメーション</li>
<li>LateUpdate(Animation Update後として) にて <code>Physics.SyncTransforms</code> を実施し時間を計測</li>
<li><span style="color: #ff0000">すべての検証環境で使用</span></li>
</ul>
<p><iframe width="420" height="315" src="https://www.youtube.com/embed/PrEiUSoxb0Q?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen title="Physics SyncTransforms パフォーマンス調査 PlayerColliders"></iframe><cite class="hatena-citation"><a href="https://www.youtube.com/watch?v=PrEiUSoxb0Q">www.youtube.com</a></cite></p>
<h2 id="AnimationColliders">AnimationColliders</h2>
<ul>
<li>BoxColliderを大量(64000個)に配置してプレイヤー同様にアニメーション
<ul>
<li>AnimatorのUpdate Mode は <code>Normal</code></li>
</ul>
</li>
</ul>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20210427/20210427220742.png" width="543" height="383" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p><iframe width="420" height="315" src="https://www.youtube.com/embed/o6AUPWf7oIw?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen title="Physics SyncTransforms パフォーマンス調査 AnimationColliders"></iframe><cite class="hatena-citation"><a href="https://www.youtube.com/watch?v=o6AUPWf7oIw">www.youtube.com</a></cite></p>
<h2 id="PhysicsAnimationColliders">PhysicsAnimationColliders</h2>
<ul>
<li>AnimationColliders の Update Mode を <code>Animate Physics</code> に変更したもの</li>
</ul>
<h2 id="DisabledAnimationColliders">DisabledAnimationColliders</h2>
<ul>
<li>AnimationCollidersのコライダー群のルートのGameObject(ColliderRoot)をオフにしたもの</li>
</ul>
<h2 id="DisabledAnimationColliders--CapsuleCollider">DisabledAnimationColliders + CapsuleCollider</h2>
<ul>
<li>DisabledAnimationCollidersのColliderRootと同階層に<b>1つの</b>CapsuleColliderを配置したもの</li>
</ul>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20210427/20210427222947.png" width="1015" height="373" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<h2 id="ColliderDisabledAnimationColliders">ColliderDisabledAnimationColliders</h2>
<ul>
<li>ColliderRoot以下のすべてのColliderに対してCollider.enabled = false にしたもの
<ul>
<li>ColliderRootオブジェクト自体はアクティブのまま</li>
</ul>
</li>
</ul>
<h2 id="Static-Colliders">Static Colliders</h2>
<ul>
<li>10000個のstaticのBoxCollider群</li>
</ul>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20210427/20210427220518.jpg" width="764" height="406" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p><br></p>
<h1 id="検証方法">検証方法</h1>
<ul>
<li>後述する「登場人物」を組み合わせた状態で <code>Physics.SyncTransforms</code> をコールして実行時間の変化を計測
<ul>
<li>コールはLateUpdate時 (Animatorが終了した後として)</li>
</ul>
</li>
</ul>
<h1 id="検証結果">検証結果</h1>
<p>※おおよその平均</p>
<p>注目したい部分は色付き</p>
<table>
<thead>
<tr>
<th> 検証対象 </th>
<th> 時間 (ms) </th>
</tr>
</thead>
<tbody>
<tr>
<td> PlayerColliders x1 </td>
<td> 0.07 </td>
</tr>
<tr>
<td> PlayerColliders x2 </td>
<td> 0.10 </td>
</tr>
<tr>
<td> PlayerColliders x4 </td>
<td> 0.19 </td>
</tr>
<tr>
<td> PlayerColliders x1 (Animator disabled) </td>
<td> <span style="color: #ff0000">0.00</span> </td>
</tr>
<tr>
<td> PlayerColliders + AnimationColliders </td>
<td> <span style="color: #ff0000">21.01</span> </td>
</tr>
<tr>
<td> PlayerColliders + PhysicsAnimationColliders </td>
<td> <span style="color: #ff0000">0.07</span> </td>
</tr>
<tr>
<td> PlayerColliders + DisabledAnimationColliders </td>
<td> 0.07 </td>
</tr>
<tr>
<td> PlayerColliders + DisabledAnimationColliders + CapsuleCollider </td>
<td> <span style="color: #0000cc">0.41</span> </td>
</tr>
<tr>
<td> PlayerColliders + ColliderDisabledAnimationColliders </td>
<td> 0.08 </td>
</tr>
<tr>
<td> PlayerColliders + StaticColliders </td>
<td> 0.07 </td>
</tr>
</tbody>
</table>
<p><details>
<summary>クリックで展開:各調査ごとのProfiler</summary></p>
<h2 id="PlayerColliders-x1">PlayerColliders x1</h2>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20210427/20210427221347.jpg" width="899" height="489" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<h2 id="PlayerColliders-x2">PlayerColliders x2</h2>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20210427/20210427221635.jpg" width="901" height="492" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<h2 id="PlayerColliders-x4">PlayerColliders x4</h2>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20210427/20210427221705.jpg" width="907" height="491" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<h2 id="PlayerColliders-のみ-Animatorをオフ">PlayerColliders のみ (Animatorをオフ)</h2>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20210427/20210427221430.jpg" width="879" height="495" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<h2 id="PlayerColliders--AnimationColliders">PlayerColliders + AnimationColliders</h2>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20210427/20210427221559.jpg" width="881" height="493" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<h2 id="PlayerColliders--PhysicsAnimationColliders">PlayerColliders + PhysicsAnimationColliders</h2>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20210427/20210427221747.jpg" width="882" height="492" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<h2 id="PlayerColliders--DisabledAnimationColliders">PlayerColliders + DisabledAnimationColliders</h2>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20210427/20210427221933.jpg" width="878" height="492" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<h2 id="PlayerColliders--DisabledAnimationColliders--CapsuleCollider">PlayerColliders + DisabledAnimationColliders + CapsuleCollider</h2>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20210427/20210427223307.jpg" width="881" height="492" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<h2 id="PlayerColliders--ColliderDisabledAnimationColliders">PlayerColliders + ColliderDisabledAnimationColliders</h2>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20210428/20210428011114.jpg" width="876" height="512" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<h2 id="PlayerColliders--StaticColliders">PlayerColliders + StaticColliders</h2>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20210427/20210427222020.jpg" width="875" height="488" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<h2 id="注意事項">注意事項</h2>
<ul>
<li>今回は <code>Physics.SyncTransforms</code> に主眼を置くためCPU Usageはあまり考慮しない</li>
<li>XR環境のためCPU Usageに表示されている時間の大部分は <code>Gfx.WaitForRenderThread</code></li>
</ul>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20210427/20210427221052.png" width="578" height="185" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p></details></p>
<h1 id="判明した特性">判明した特性</h1>
<ul>
<li><code>Physics.SyncTransforms</code>の時間はコール時点のFlushされていないオブジェクト数に依存
<ul>
<li>FixedUpdate直後から<code>Physics.SyncTransforms</code>をコールするまでにTransformが変更されたオブジェクトが対象</li>
</ul>
</li>
<li>Flush済みのオブジェクトは<code>Physics.SyncTransforms</code>の時間には大して影響はしない
<ul>
<li>つまりTransformが変更されたオブジェクトがなければオーバーヘッドは小さい</li>
</ul>
</li>
<li>通常Flushとそれ以外でFlushする対象は分けることは一応可能
<ul>
<li>ただし「特定のオブジェクトのみFlushしたい」の様なコン<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C8%A5%ED%A1%BC%A5%EB">トロール</a>は不可
<ul>
<li>Unity側に機能が用意されていない(と思われる、ご存じの方がいたら教えてください!)</li>
</ul>
</li>
</ul>
</li>
<li>非アクティブなCollider(<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>のenabled=false含む)はFlushの対象外
<ul>
<li>アクティブ化した後のFixedUpdate後 or <code>Physics.SyncTransforms</code> で改めてFlush</li>
</ul>
</li>
<li><span style="color: #0000cc">同階層にアクティブと非アクティブ両方のColliderが存在する場合は特殊なFlushとなる可能性あり(詳細は不明)</span>
<ul>
<li>"DisabledAnimationColliders + CapsuleCollider" の時間を見るとCapsuleCollider1つだけFlushされるにしては時間がかかっている</li>
</ul>
</li>
</ul>
<h1 id="考察">考察</h1>
<ul>
<li><code>Physics.SyncTransforms</code> 時にFlushされるCollider数をうまくコン<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C8%A5%ED%A1%BC%A5%EB">トロール</a>すれば、パフォーマンスへの影響をコン<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C8%A5%ED%A1%BC%A5%EB">トロール</a>できそう</li>
<li>UpdateやFixedUpdateなで移動するColliderが多い場合は実行時間に注意が必要になると思われる
<ul>
<li>あまりに多いと Physics.SyncTransforms 時スパイクが発生</li>
</ul>
</li>
</ul>
<h1 id="検証プロジェクト">検証プロジェクト</h1>
<ul>
<li><strong>2022/06/19変更</strong>
<ul>
<li><a href="https://docs.unity3d.com/Packages/[email protected]/manual/index.html">Performance Testing Extension for Unity Test Framework</a>を使用した検証の自動化のためmainブランチには大幅に変更されています</li>
<li>このブログ記事当初の検証プロジェクトは tag <strong>1.0.0</strong> をご参照ください</li>
</ul>
</li>
</ul>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Ftsgcpp%2FSyncTransformPerformanceCheck%2Ftree%2F1.0.0" title="GitHub - tsgcpp/SyncTransformPerformanceCheck at 1.0.0" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://github.com/tsgcpp/SyncTransformPerformanceCheck/tree/1.0.0">github.com</a></cite></p>
<h1 id="雑感">雑感</h1>
<p>ちょっと個人的な事情があって検証、ついでにドキュメント化してみました。<br>
Flush対象を明示的に指定できないのはちょっと残念ですね。。。<br>
ただ、物理で動くものとそれ以外で分けるのは一応できそうってとこでしょうか。</p>
<p>また、別の機会に <code>Physics.SyncTransforms</code> を利用したものを作ってみようと思います。</p>
<p>それでは~</p>
tsgcpp
(旧)【Extenject】Composite Installer を紹介!
hatenablog://entry/26006613694116679
2021-02-21T19:12:26+09:00
2021-02-21T19:12:26+09:00 以下にページに移行しました tsgcpp.hateblo.jp
<p>以下にページに移行しました</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftsgcpp.hateblo.jp%2Fentry%2Fextenject_zenject_composite_installer" title="【Extenject】Composite Installer を紹介! - すぎしーのXRと3DCG" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://tsgcpp.hateblo.jp/entry/extenject_zenject_composite_installer">tsgcpp.hateblo.jp</a></cite></p>
tsgcpp
【Moq】UnityでのMoq導入方法 と MoqのTips集を紹介!
hatenablog://entry/26006613656990174
2020-11-27T22:14:11+09:00
2020-11-27T22:14:11+09:00 概要 Moqの導入 Github Moqの依存ライブラリとそのライセンスについて MoqをUnityに導入する方法 Moqおよび依存ライブラリのダウンロード nupkgを展開 dllファイルの取り出し .NET Standard 2.0 .NET 4.x or .NET Framework dllのプラットフォームの指定 System.Threading.Tasks.Extensions と System.Runtime.CompilerServices.Unsafe のプラットフォーム指定について テスト用Assembly Definitionにdllを取り込み Moqによるモック作成サンプ…
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20201127/20201127215203.jpg" width="894" height="446" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<ul class="table-of-contents">
<li><a href="#概要">概要</a></li>
<li><a href="#Moqの導入">Moqの導入</a><ul>
<li><a href="#Github">Github</a></li>
<li><a href="#Moqの依存ライブラリとそのライセンスについて">Moqの依存ライブラリとそのライセンスについて</a></li>
<li><a href="#MoqをUnityに導入する方法">MoqをUnityに導入する方法</a><ul>
<li><a href="#Moqおよび依存ライブラリのダウンロード">Moqおよび依存ライブラリのダウンロード</a></li>
<li><a href="#nupkgを展開">nupkgを展開</a></li>
<li><a href="#dllファイルの取り出し">dllファイルの取り出し</a><ul>
<li><a href="#NET-Standard-20">.NET Standard 2.0</a></li>
<li><a href="#NET-4x-or-NET-Framework">.NET 4.x or .NET Framework</a></li>
</ul>
</li>
<li><a href="#dllのプラットフォームの指定">dllのプラットフォームの指定</a><ul>
<li><a href="#SystemThreadingTasksExtensions-と-SystemRuntimeCompilerServicesUnsafe-のプラットフォーム指定について">System.Threading.Tasks.Extensions と System.Runtime.CompilerServices.Unsafe のプラットフォーム指定について</a></li>
</ul>
</li>
<li><a href="#テスト用Assembly-Definitionにdllを取り込み">テスト用Assembly Definitionにdllを取り込み</a></li>
</ul>
</li>
</ul>
</li>
<li><a href="#Moqによるモック作成サンプル">Moqによるモック作成サンプル</a><ul>
<li><a href="#モック化するinterface">モック化するinterface</a></li>
<li><a href="#モック化の例">モック化の例</a><ul>
<li><a href="#モックオブジェクトの作成">モックオブジェクトの作成</a></li>
<li><a href="#モックオブジェクトから元となる型を取り出し">モックオブジェクトから元となる型を取り出し</a></li>
<li><a href="#メソッドの戻り値を指定">メソッドの戻り値を指定</a></li>
<li><a href="#プロパティの戻り値を指定">プロパティの戻り値を指定</a></li>
<li><a href="#特定のメソッドがコールされたかの確認Spyとしての利用">特定のメソッドがコールされたかの確認(Spyとしての利用)</a></li>
<li><a href="#Moqを使用したサンプルの全体">Moqを使用したサンプルの全体</a></li>
</ul>
</li>
<li><a href="#モック化するメソッドプロパティの注意点">モック化するメソッド、プロパティの注意点</a></li>
</ul>
</li>
<li><a href="#Moqを使ったTips集">Moqを使ったTips集</a><ul>
<li><a href="#in-ref指定の構造体を引数として持つメソッドの任意の引数でのVerify">in, ref指定の構造体を引数として持つメソッドの任意の引数でのVerify</a><ul>
<li><a href="#in-refどちらも指定されていない場合">in, refどちらも指定されていない場合</a></li>
<li><a href="#指定ミスの対策">指定ミスの対策</a></li>
</ul>
</li>
<li><a href="#Callbackの活用例">Callbackの活用例</a><ul>
<li><a href="#Callbackの紹介">Callbackの紹介</a></li>
</ul>
</li>
<li><a href="#MoqのCallbackでUnityのExecutionOrderとUniTaskの実行順序の検証">MoqのCallbackでUnityのExecutionOrderとUniTaskの実行順序の検証</a><ul>
<li><a href="#確認用コンポーネントとテストコードについて">確認用コンポーネントとテストコードについて</a></li>
<li><a href="#DefaultExecutionOrderを指定したコンポーネント">DefaultExecutionOrderを指定したコンポーネント</a></li>
<li><a href="#UniTaskを使用したクラス">UniTaskを使用したクラス</a></li>
<li><a href="#テストコード">テストコード</a></li>
</ul>
</li>
</ul>
</li>
<li><a href="#サンプルプロジェクト">サンプルプロジェクト</a></li>
<li><a href="#雑感">雑感</a></li>
</ul>
<p><span style="color: #ff0000"><strong>Unity 2021以降の場合は以下をどうぞ!</strong></span>
<iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftsgcpp.hateblo.jp%2Fentry%2Fmoq_for_unity_2021" title="UnityでMoqを使う (Unity2021バージョン) - すぎしーのXRと3DCG" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://tsgcpp.hateblo.jp/entry/moq_for_unity_2021">tsgcpp.hateblo.jp</a></cite></p>
<h1 id="概要">概要</h1>
<p>今回は Moq という<a class="keyword" href="http://d.hatena.ne.jp/keyword/C%23">C#</a>向けモックライブラリをUnityに導入する方法と、Moqを使ったテクニックをいくつか紹介しようと思います。</p>
<p>UnitTestにおいてモック化はかなり重要な立ち位置になると思いますが、自作モックは何かと保守コストが高かったり機能面が微妙になったりと悩みどころがあります。<br/>
そこでオススメなのが今回紹介する Moq となります。</p>
<p>他の言語のモックライブラリ同様、モック化だけでなく<a class="keyword" href="http://d.hatena.ne.jp/keyword/Spy">Spy</a>機能(メソッドがコールされたかの確認機能)も結構充実していますので、<br/>
ぜひUnity もしくは <a class="keyword" href="http://d.hatena.ne.jp/keyword/C%23">C#</a> のUnitTestに活かしていきましょう!</p>
<p>今回の記事はある程度UnitTestを知っていることが前提となりますが、UnitTestのサンプルコードにもできる限り説明を入れています。<br/>
<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C1%A1%BC%A5%C8%A5%B7%A1%BC%A5%C8">チートシート</a>にもなっていると思いますのでよかったら参考にしてください。</p>
<h1 id="Moqの導入">Moqの導入</h1>
<h2 id="Github"><a class="keyword" href="http://d.hatena.ne.jp/keyword/Github">Github</a></h2>
<p>※今回は<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%BD%A1%BC%A5%B9%A5%B3%A1%BC%A5%C9">ソースコード</a>を直接取り込みません</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fmoq%2Fmoq4" title="GitHub - moq/moq4: Repo for managing Moq 4.x" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://github.com/moq/moq4">github.com</a></cite></p>
<p>Moqの<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%BD%A1%BC%A5%B9%A5%B3%A1%BC%A5%C9">ソースコード</a>は<span style="color: #ff0000"><a class="keyword" href="http://d.hatena.ne.jp/keyword/C%23">C#</a> 8.0</span>で書かれており、現在のLTSであるUnity 2019.4 では<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%D1%A5%A4%A5%EB">コンパイル</a>できません。</p>
<p>よって、<b>後述する<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%D1%A5%A4%A5%EB">コンパイル</a>済みのdllをダウンロードして使用することになります!</b></p>
<h2 id="Moqの依存ライブラリとそのライセンスについて">Moqの依存ライブラリとそのライセンスについて</h2>
<p><b>使用する前に必ずライセンスを確認してください(特に業務で使用する場合は要注意です!)</b></p>
<ul>
<li><a href="https://github.com/moq/moq4/blob/master/License.txt">Moq</a>: <a class="keyword" href="http://d.hatena.ne.jp/keyword/BSD">BSD</a> 3-Clause License</li>
<li><a href="https://github.com/castleproject/Core/blob/master/LICENSE">Castle.Core</a>: <a class="keyword" href="http://d.hatena.ne.jp/keyword/Apache">Apache</a> License Version 2.0
<ul>
<li>Moqが依存するライブラリ</li>
</ul>
</li>
<li><a href="https://www.nuget.org/packages/System.Threading.Tasks.Extensions/">System.Threading.Tasks.Extensions</a>: MIT
<ul>
<li>Moqが依存するライブラリ (<a class="keyword" href="http://d.hatena.ne.jp/keyword/Microsoft">Microsoft</a> 提供)</li>
</ul>
</li>
<li><a href="https://www.nuget.org/packages/System.Runtime.CompilerServices.Unsafe/">System.Runtime.CompilerServices.Unsafe</a>: MIT
<ul>
<li>System.Threading.Tasks.Extensionsが依存するライブラリ</li>
</ul>
</li>
</ul>
<p>上記4つを取り込む必要があります。</p>
<h2 id="MoqをUnityに導入する方法">MoqをUnityに導入する方法</h2>
<h3 id="Moqおよび依存ライブラリのダウンロード">Moqおよび依存ライブラリのダウンロード</h3>
<p>今回はMoqと依存ライブラリ3つをNugetからダウンロードしましょう。</p>
<p>以下の用に"Download package"からnupkgをダウンロード可能です。
<span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20201125/20201125233413.jpg" width="1200" height="738" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.nuget.org%2Fpackages%2Fmoq" title="Moq 4.18.4" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://www.nuget.org/packages/moq">www.nuget.org</a></cite></p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.nuget.org%2Fpackages%2FCastle.Core%2F" title="Castle.Core 5.1.1" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://www.nuget.org/packages/Castle.Core/">www.nuget.org</a></cite></p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.nuget.org%2Fpackages%2FSystem.Threading.Tasks.Extensions%2F" title="System.Threading.Tasks.Extensions 4.5.4" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://www.nuget.org/packages/System.Threading.Tasks.Extensions/">www.nuget.org</a></cite></p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.nuget.org%2Fpackages%2FSystem.Runtime.CompilerServices.Unsafe%2F" title="System.Runtime.CompilerServices.Unsafe 6.0.0" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://www.nuget.org/packages/System.Runtime.CompilerServices.Unsafe/">www.nuget.org</a></cite></p>
<h3 id="nupkgを展開">nupkgを展開</h3>
<p>nupkgの実体はzipファイルなので拡張子を ".zip" にするだけで大体のOSの標準で展開できると思います。</p>
<p><a class="keyword" href="http://d.hatena.ne.jp/keyword/7-zip">7-zip</a>などを使用すれば拡張子を変えなくても展開できると思います。</p>
<h3 id="dllファイルの取り出し">dllファイルの取り出し</h3>
<p>展開したフォルダのうち、以下のパスにあるdllをUnityのAssets以下のフォルダにコピーしましょう。</p>
<p><span style="color: #ff0000"><a class="keyword" href="http://d.hatena.ne.jp/keyword/Api">Api</a> Compatibility Level</span> に従ってdll を選択してください。</p>
<h4 id="NET-Standard-20">.NET Standard 2.0</h4>
<ul>
<li>Moq: <code>moq.4.X.X/lib/netstandard2.0/Moq.dll</code></li>
<li>Castle.Core: <code>castle.core.4.X.X/lib/netstandard1.5/Castle.Core.dll</code>
<ul>
<li>.Net Standard2.0向けがないためこちらで代用</li>
</ul>
</li>
<li>System.Threading.Tasks.Extensions: <code>system.threading.tasks.extensions.4.X.X/lib/netstandard2.0/System.Threading.Tasks.Extensions.dll</code></li>
<li>System.Runtime.CompilerServices.Unsafe: <code>system.runtime.compilerservices.unsafe.5.0.0/lib/netstandard2.0/System.Runtime.CompilerServices.Unsafe.dll</code></li>
</ul>
<h4 id="NET-4x-or-NET-Framework">.NET 4.x or<a class="keyword" href="http://d.hatena.ne.jp/keyword/%20.NET"> .NET</a> Framework</h4>
<ul>
<li>Moq: <code>moq.4.X.X/lib/net45/Moq.dll</code></li>
<li>Castle.Core: <code>castle.core.4.X.X/lib/net45/Castle.Core.dll</code></li>
<li>System.Threading.Tasks.Extensions: <code>system.threading.tasks.extensions.4.X.X/lib/net461/System.Threading.Tasks.Extensions.dll</code></li>
<li>System.Runtime.CompilerServices.Unsafe: <code>system.runtime.compilerservices.unsafe.5.0.0/lib/net45/System.Runtime.CompilerServices.Unsafe.dll</code></li>
</ul>
<p>Unityのプロジェクトへの配置は以下を参考にしてください</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20201125/20201125234440.png" width="356" height="292" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<h3 id="dllのプラットフォームの指定">dllのプラットフォームの指定</h3>
<p>Moqはあくまでテストのみに使用するため、ビルドする実行ファイルには含まれないように設定しておきましょう!
大抵の<a class="keyword" href="http://d.hatena.ne.jp/keyword/OSS">OSS</a>は再頒布するとラインセス表記が必要なります。</p>
<p>特に <code>Moq</code> と <code>Castle.Core</code>は実行ファイルには不要なので<code>Include Platforms</code>を<code>Editor</code>のみにしておきましょう!</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20201125/20201125235027.jpg" width="815" height="640" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<h4 id="SystemThreadingTasksExtensions-と-SystemRuntimeCompilerServicesUnsafe-のプラットフォーム指定について">System.Threading.Tasks.Extensions と System.Runtime.CompilerServices.Unsafe のプラットフォーム指定について</h4>
<p>System.Threading.Tasks.Extensions と System.Runtime.CompilerServices.Unsafe も不要であれば同様に設定してください。</p>
<p>ただ、<a href="https://github.com/Cysharp/ZLogger">ZLogger</a> など一部メジャーな<a class="keyword" href="http://d.hatena.ne.jp/keyword/OSS">OSS</a>も依存していたりするので、その場合は逆に設定しないように注意してください。</p>
<h3 id="テスト用Assembly-Definitionにdllを取り込み">テスト用Assembly Definitionにdllを取り込み</h3>
<p>以下を参考にテスト用Assembly Definitionの"Assembly References"に追加して"Apply"して下さい。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20201125/20201125235439.jpg" width="808" height="584" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p>以上でMoqおよび依存ライブラリの取り込みは完了です!</p>
<h1 id="Moqによるモック作成サンプル">Moqによるモック作成サンプル</h1>
<p>せっかく取り込んでみたので、少し使い方を解説しようと思います。</p>
<h2 id="モック化するinterface">モック化するinterface</h2>
<pre class="code lang-cs" data-lang="cs" data-unlink>
<span class="synComment">// 戻り値有りのメソッドを持つinterface</span>
<span class="synType">public</span> <span class="synType">interface</span> IGetter
{
<span class="synType">object</span> Get();
}
<span class="synComment">// プロパティを持つinterface</span>
<span class="synType">public</span> <span class="synType">interface</span> IHolder
{
<span class="synType">object</span> Value { <span class="synStatement">get</span>; }
}
<span class="synComment">// 引数を持つinterface</span>
<span class="synType">public</span> <span class="synType">interface</span> ILogger
{
<span class="synType">void</span> Log(<span class="synType">string</span> message);
}
</pre>
<h2 id="モック化の例">モック化の例</h2>
<h3 id="モックオブジェクトの作成">モックオブジェクトの作成</h3>
<pre class="code lang-cs" data-lang="cs" data-unlink>Mock<IGetter> mock <span class="synStatement">=</span> <span class="synStatement">new</span> Mock<IGetter>();
</pre>
<h3 id="モックオブジェクトから元となる型を取り出し">モックオブジェクトから元となる型を取り出し</h3>
<pre class="code lang-cs" data-lang="cs" data-unlink><span class="synType">var</span> getterMock <span class="synStatement">=</span> <span class="synStatement">new</span> Mock<IGetter>();
<span class="synComment">// モックオブジェクトからIGetterインスタンスとして取り出し</span>
IGetter mockAsGetter <span class="synStatement">=</span> getterMock.Object;
</pre>
<h3 id="メソッドの戻り値を指定">メソッドの戻り値を指定</h3>
<pre class="code lang-cs" data-lang="cs" data-unlink><span class="synType">var</span> getterMock <span class="synStatement">=</span> <span class="synStatement">new</span> Mock<IGetter>();
<span class="synComment">// Getメソッドのモック化("Hello Moq!"を返すように仕込み)</span>
getterMock.Setup(m <span class="synStatement">=></span> m.Get()).Returns(<span class="synConstant">"Hello Moq!"</span>);
</pre>
<h3 id="プロパティの戻り値を指定">プロパティの戻り値を指定</h3>
<pre class="code lang-cs" data-lang="cs" data-unlink><span class="synType">var</span> holderMock <span class="synStatement">=</span> <span class="synStatement">new</span> Mock<IHolder>();
<span class="synComment">// Valueプロパティのモック化("Hi Moq!"を返すように仕込み)</span>
holderMock.Setup(m <span class="synStatement">=></span> m.Value).Returns(<span class="synConstant">"Hi Moq!"</span>)
</pre>
<h3 id="特定のメソッドがコールされたかの確認Spyとしての利用">特定のメソッドがコールされたかの確認(<a class="keyword" href="http://d.hatena.ne.jp/keyword/Spy">Spy</a>としての利用)</h3>
<p>メソッドがコールされたかを検知するモックオブジェクトを<a class="keyword" href="http://d.hatena.ne.jp/keyword/Spy">Spy</a>と呼ぶことがあります。</p>
<p>MoqのMockクラスは生成するだけで<a class="keyword" href="http://d.hatena.ne.jp/keyword/Spy">Spy</a>としても使用することが可能です!</p>
<p>また、「特定の引数が渡されたこと」、「任意の引数でコールされたこと」なども確認することが可能です。</p>
<pre class="code lang-cs" data-lang="cs" data-unlink><span class="synComment">// ILoggerのモックオブジェクトの作成</span>
<span class="synType">var</span> loggerMock <span class="synStatement">=</span> <span class="synStatement">new</span> Mock<ILogger>();
<span class="synType">var</span> mockAsLogger <span class="synStatement">=</span> loggerMock.Object;
<span class="synComment">// 複数回(3回)コール</span>
mockAsLogger.Log(<span class="synConstant">"One"</span>);
mockAsLogger.Log(<span class="synConstant">"Two"</span>);
mockAsLogger.Log(<span class="synConstant">"Two"</span>);
<span class="synComment">// 3回コールされたことの確認</span>
loggerMock.Verify(m <span class="synStatement">=></span> m.Log(It.IsAny<<span class="synType">string</span>>()), Times.Exactly(<span class="synConstant">3</span>));
<span class="synComment">// 複数回(1回以上)コールされたことの確認</span>
loggerMock.Verify(m <span class="synStatement">=></span> m.Log(It.IsAny<<span class="synType">string</span>>()), Times.AtLeastOnce());
<span class="synComment">// "One"でコールは1回であることの確認</span>
loggerMock.Verify(m <span class="synStatement">=></span> m.Log(<span class="synConstant">"One"</span>), Times.Once());
<span class="synComment">// "Two"でコールは2回であることの確認</span>
loggerMock.Verify(m <span class="synStatement">=></span> m.Log(<span class="synConstant">"Two"</span>), Times.Exactly(<span class="synConstant">2</span>));
</pre>
<h3 id="Moqを使用したサンプルの全体">Moqを使用したサンプルの全体</h3>
<p>Moqの使用例の全体は以下のテストクラスで確認できます!</p>
<p>[<a href="https://github.com/tsgcpp/UnityMoqSample/blob/master/Assets/Tests/MoqExamples/TestMoqSample.cs">https://github.com/tsgcpp/UnityMoqSample/blob/master/Assets/Tests/MoqExamples/TestMoqSample.cs</a></p>
<h2 id="モック化するメソッドプロパティの注意点">モック化するメソッド、プロパティの注意点</h2>
<p><b>モック化するメソッドやプロパティはオーバーライド可能である必要があります</b>。<br/>
オーバーライド不可の場合はモック化することはできません。。。</p>
<p>以下の<code>MockTarget</code>クラス(非interface)の場合、<code>FuncVirtual</code>と<code>FuncAbstract</code>はモック化できますが<code>Func</code>はオーバーライド不可のためモック化できません(例外<code>System.NotSupportedException</code>が飛びます)。</p>
<pre class="code lang-cs" data-lang="cs" data-unlink>
<span class="synType">public</span> <span class="synType">abstract</span> <span class="synType">class</span> <span class="synType">MockTarget</span>
{
<span class="synType">public</span> <span class="synType">object</span> Func()
{
<span class="synStatement">return</span> <span class="synConstant">1</span>;
}
<span class="synType">public</span> <span class="synType">virtual</span> <span class="synType">object</span> FuncVirtual()
{
<span class="synStatement">return</span> <span class="synConstant">2</span>;
}
<span class="synType">public</span> <span class="synType">abstract</span> <span class="synType">object</span> FuncAbstract();
}
</pre>
<p>Moqに限らずモックライブラリ全般に言えますが、モック化の対象は基本的にinterfaceを使用することをオススメします!</p>
<p><a href="https://github.com/tsgcpp/UnityMoqSample/blob/master/Assets/Tests/MoqExamples/TestMoqException.cs">https://github.com/tsgcpp/UnityMoqSample/blob/master/Assets/Tests/MoqExamples/TestMoqException.cs</a></p>
<h1 id="Moqを使ったTips集">Moqを使ったTips集</h1>
<h2 id="in-ref指定の構造体を引数として持つメソッドの任意の引数でのVerify">in, ref指定の構造体を引数として持つメソッドの任意の引数でのVerify</h2>
<p>以下のように引数に<code>in</code>が指定されたメソッドのことです。</p>
<pre class="code lang-cs" data-lang="cs" data-unlink>
<span class="synType">public</span> <span class="synType">interface</span> IInCaller
{
<span class="synType">void</span> Call(<span class="synStatement">in</span> Parameter parameter);
}
</pre>
<p>ちなみにParameterは以下です。</p>
<pre class="code lang-cs" data-lang="cs" data-unlink><span class="synType">public</span> <span class="synType">struct</span> Parameter
{
<span class="synType">public</span> <span class="synType">int</span> x;
}
</pre>
<p>さて、この<code>in</code>が指定されたメソッドのVerifyですが、なんと<code>It.IsAny<T>()</code>だと<a class="keyword" href="http://d.hatena.ne.jp/keyword/Spy">Spy</a>が機能しなくなります。。。</p>
<pre class="code lang-cs" data-lang="cs" data-unlink>target.Verify(m <span class="synStatement">=></span> m.Call(It.IsAny<Parameter>()), Times.Once());
</pre>
<p><span style="color: #ff0000"><code>in</code>や<code>ref</code>が指定されている場合は以下の <code>It.Ref<T>.IsAny</code> を使用しましょう!</span></p>
<pre class="code lang-cs" data-lang="cs" data-unlink>target.Verify(m <span class="synStatement">=></span> m.Call(It.Ref<Parameter>.IsAny), Times.Once());
</pre>
<p>Moq自体の仕様なのでご注意ください。</p>
<p>細かい使用確認は以下のテストコードで実施しています。
よかったらこちらもどうぞ!</p>
<p><a href="https://github.com/tsgcpp/UnityMoqSample/blob/master/Assets/Tests/MoqExamples/TestMoqRefVerify.cs">https://github.com/tsgcpp/UnityMoqSample/blob/master/Assets/Tests/MoqExamples/TestMoqRefVerify.cs</a></p>
<h3 id="in-refどちらも指定されていない場合">in, refどちらも指定されていない場合</h3>
<p><code>It.IsAny<T>()</code>で確認しましょう。<br/>
<span style="color: #ff0000">こちらは逆に<code>It.Ref<T>.IsAny</code>が機能しなくなります。</span></p>
<h3 id="指定ミスの対策">指定ミスの対策</h3>
<p><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%D1%A5%A4%A5%EB">コンパイル</a>エラーにならない関係でヒューマンエラーが起きそうですよね?</p>
<p>特に<code>Times.Never()</code>を使用する場合、コールなし or 指定ミス どちらなのか分からなくなってしまいます。</p>
<pre class="code lang-cs" data-lang="cs" data-unlink>target.Verify(m <span class="synStatement">=></span> m.Call(It.Ref<Parameter>.IsAny), Times.Never());
</pre>
<p>オススメなのは<code>It.IsAny<T>()</code> もしくは <code>It.Ref<T>.IsAny</code>を用いるテストを実施する場合は、
正常系のテストに以下の<code>Times.Once()</code>など "1回以上コールされたことの検証を混ぜ込むことです。</p>
<pre class="code lang-cs" data-lang="cs" data-unlink>target.Verify(m <span class="synStatement">=></span> m.Call(It.Ref<Parameter>.IsAny), Times.Once());
</pre>
<p><code>Times.Once()</code>や<code>Times.Exactly(N)</code>でテストが通る = 機能している ということになりますので!</p>
<h2 id="Callbackの活用例">Callbackの活用例</h2>
<h3 id="Callbackの紹介">Callbackの紹介</h3>
<p>Moqにはモックオブジェクトのメソッドがコールされたときにコールバック(イベント)を仕込むことが可能です。</p>
<pre class="code lang-cs" data-lang="cs" data-unlink><span class="synComment">// コールバック時のパラメータ格納用List</span>
<span class="synType">var</span> messageList <span class="synStatement">=</span> <span class="synStatement">new</span> List<<span class="synType">string</span>>();
<span class="synComment">// ILoggerのモックオブジェクトの作成</span>
<span class="synType">var</span> loggerMock <span class="synStatement">=</span> <span class="synStatement">new</span> Mock<ILogger>();
<span class="synComment">// モックオブジェクトにコールバックを仕込む</span>
loggerMock
.Setup(m <span class="synStatement">=></span> m.Log(It.IsAny<<span class="synType">string</span>>()))
.Callback<<span class="synType">string</span>>(message <span class="synStatement">=></span> messageList.Add(message));
</pre>
<p>上記の<code>Callback</code>は<code>Log</code>メソッドに渡された引数をコールバックにより取り出すことが可能になります。</p>
<h2 id="MoqのCallbackでUnityのExecutionOrderとUniTaskの実行順序の検証">MoqのCallbackでUnityのExecutionOrderとUniTaskの実行順序の検証</h2>
<p>さてコードを書いていると時々「絶対この順番でメソッドがコールされていることを保証したい」ということ有りませんか?</p>
<p>そんなときにCallbackを活用するとテストにコール順の検証を組み込むことができます(Moqを使用するので各メソッドがモック化可能なことが前提となります)。</p>
<p>今回はCallbackを活用して、UnityのExecutionOrderとUniTaskを混合した実行順序のテスト化をやってみたいと思います。</p>
<p>具体的には<code>DefaultExecutionOrder</code>を指定した<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>, <code>UniTask.Yield</code>に指定したPlayerLoopTimingそれぞれが同時に存在する場合に実行順序を確認するテストを作成します。</p>
<h3 id="確認用コンポーネントとテストコードについて">確認用<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>とテストコードについて</h3>
<p><code>IRunner</code>型の<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>を渡し、<code>Run()</code>メソッドをコールさせる構成を取ります。</p>
<h3 id="DefaultExecutionOrderを指定したコンポーネント">DefaultExecutionOrderを指定した<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a></h3>
<p><code>DefaultExecutionOrder</code>を指定する以外は共通のためベースクラスを作成し、それを継承したクラスを用意しました。</p>
<p>以下は int.MaxValueを指定した場合の例です。</p>
<pre class="code lang-cs" data-lang="cs" data-unlink><span class="synStatement">using</span> UnityEngine;
<span class="synComment">/// </span><span class="synIdentifier"><</span><span class="synStatement">summary</span><span class="synIdentifier">></span>
<span class="synComment">/// MonoBehaviourによるRunner実行 (order: int.MaxValue)</span>
<span class="synComment">/// </span><span class="synIdentifier"></</span><span class="synStatement">summary</span><span class="synIdentifier">></span>
[DefaultExecutionOrder(<span class="synType">int</span>.MaxValue)]
<span class="synType">public</span> <span class="synType">class</span> <span class="synType">ProcessorOrderMaxValue </span><span class="synStatement">:</span> BaseProcessOrder
{
}
<span class="synComment">/// </span><span class="synIdentifier"><</span><span class="synStatement">summary</span><span class="synIdentifier">></span>
<span class="synComment">/// MonoBehaviourによるRunner実行のベース</span>
<span class="synComment">/// </span><span class="synIdentifier"></</span><span class="synStatement">summary</span><span class="synIdentifier">></span>
<span class="synType">public</span> <span class="synType">abstract</span> <span class="synType">class</span> <span class="synType">BaseProcessOrder </span><span class="synStatement">:</span> MonoBehaviour, IProcessor
{
<span class="synType">public</span> IRunner Runner { <span class="synStatement">get</span>; <span class="synStatement">set</span>; } <span class="synStatement">=</span> <span class="synConstant">null</span>;
<span class="synType">public</span> IRunner LateRunner { <span class="synStatement">get</span>; <span class="synStatement">set</span>; } <span class="synStatement">=</span> <span class="synConstant">null</span>;
<span class="synType">protected</span> <span class="synType">virtual</span> <span class="synType">void</span> Update()
{
Runner<span class="synStatement">?</span>.Run();
}
<span class="synType">protected</span> <span class="synType">virtual</span> <span class="synType">void</span> LateUpdate()
{
LateRunner<span class="synStatement">?</span>.Run();
}
}
</pre>
<h3 id="UniTaskを使用したクラス">UniTaskを使用したクラス</h3>
<pre class="code lang-cs" data-lang="cs" data-unlink><span class="synStatement">using</span> UnityEngine;
<span class="synStatement">using</span> Cysharp.Threading.Tasks;
<span class="synComment">/// </span><span class="synIdentifier"><</span><span class="synStatement">summary</span><span class="synIdentifier">></span>
<span class="synComment">/// UniTaskによるRunner実行</span>
<span class="synComment">/// </span><span class="synIdentifier"></</span><span class="synStatement">summary</span><span class="synIdentifier">></span>
<span class="synType">public</span> <span class="synType">class</span> <span class="synType">ProcessorUniTask </span><span class="synStatement">:</span> IProcessor
{
<span class="synType">public</span> IRunner Runner { <span class="synStatement">get</span>; <span class="synStatement">set</span>; } <span class="synStatement">=</span> <span class="synConstant">null</span>;
<span class="synComment">// 未使用</span>
<span class="synType">public</span> IRunner LateRunner { <span class="synStatement">get</span>; <span class="synStatement">set</span>; } <span class="synStatement">=</span> <span class="synConstant">null</span>;
<span class="synType">public</span> PlayerLoopTiming Timing { <span class="synStatement">get</span>; <span class="synStatement">set</span>; } <span class="synStatement">=</span> PlayerLoopTiming.Update;
<span class="synComment">/// </span><span class="synIdentifier"><</span><span class="synStatement">summary</span><span class="synIdentifier">></span>
<span class="synComment">/// Runnerの非同期実行</span>
<span class="synComment">/// </span><span class="synIdentifier"></</span><span class="synStatement">summary</span><span class="synIdentifier">></span>
<span class="synType">public</span> <span class="synType">void</span> Process()
{
ProcessAsync().Forget();
}
<span class="synType">private</span> <span class="synStatement">async</span> UniTask ProcessAsync()
{
<span class="synStatement">await</span> UniTask.Yield(Timing);
Runner<span class="synStatement">?</span>.Run();
}
}
</pre>
<h3 id="テストコード">テストコード</h3>
<p>結構コードが長くなってしまったので全体は以下の<a class="keyword" href="http://d.hatena.ne.jp/keyword/Github">Github</a>のコードをご確認ください。</p>
<p><a href="https://github.com/tsgcpp/UnityMoqSample/blob/main/Assets/Tests.UniTask/ExecutionOrder/TestExecutionOrder.cs">https://github.com/tsgcpp/UnityMoqSample/blob/main/Assets/Tests.UniTask/ExecutionOrder/TestExecutionOrder.cs</a></p>
<p>大まかに言うと<code>IRunner</code>のモックオブジェクトを各<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>ごとに作成し、対象の<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>ごとにコールバック時のメッセージを仕込みます。<br/>
各<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>内の<code>Update()</code>, <code>LateUpdate()</code> および UniTaskのawait後に<code>IRunner.Run()</code>がコールされると順番にリスト<code>_callbackMessageList</code>にメッセージが追加されていきます。</p>
<pre class="code lang-cs" data-lang="cs" data-unlink><span class="synType">var</span> runnerMock <span class="synStatement">=</span> <span class="synStatement">new</span> Mock<IRunner>();
<span class="synComment">// MockのIRunner.Run()がコールされた際にメッセージを登録するように仕込む(いわゆるSpy)</span>
runnerMock
.Setup(mock <span class="synStatement">=></span> mock.Run())
.Callback(() <span class="synStatement">=></span> _callbackMessageList.Add(message))
</pre>
<p>最後に1フレーム動かして、<code>_callbackMessageList</code>にAddされたメッセージの順番を確認することで実行順序を確認します。</p>
<pre class="code lang-cs" data-lang="cs" data-unlink><span class="synStatement">yield</span> <span class="synStatement">return</span> <span class="synStatement">new</span> WaitForEndOfFrame()
Assert.AreEqual(<span class="synConstant">18</span>, _callbackMessageList.Count);
Assert.AreEqual(<span class="synConstant">"UniTask PlayerLoopTiming.PreUpdate"</span>, _callbackMessageList[<span class="synConstant">0</span>]);
Assert.AreEqual(<span class="synConstant">"UniTask PlayerLoopTiming.LastPreUpdate"</span>, _callbackMessageList[<span class="synConstant">1</span>]);
Assert.AreEqual(<span class="synConstant">"UniTask PlayerLoopTiming.Update"</span>, _callbackMessageList[<span class="synConstant">2</span>]);
Assert.AreEqual(<span class="synConstant">"ProcessorOrderMinValue Update"</span>, _callbackMessageList[<span class="synConstant">3</span>]);
Assert.AreEqual(<span class="synConstant">"ProcessorOrderM1 Update"</span>, _callbackMessageList[<span class="synConstant">4</span>]);
<span class="synComment">// 以下略</span>
</pre>
<p>TestRunnerを実行してみました。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20201127/20201127213747.png" width="988" height="299" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p>問題なさそうですね。</p>
<p>このテストを組み込んでおくことでUnityやUniTaskの更新で実行順序が変わった場合に検知することができたりはするかなと思います。</p>
<p><span style="color: #999999">「Debug.Logで良くない?」と言われるとその通りなんですけどねw</span></p>
<p>個人的に詳細な実行順序を調べたかったというのもあったので、ついでにCallbackに使用する題材にしてみました。</p>
<h1 id="サンプルプロジェクト">サンプルプロジェクト</h1>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Ftsgcpp%2FUnityMoqSample" title="GitHub - tsgcpp/UnityMoqSample: MoqをUnityで使用するサンプル" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://github.com/tsgcpp/UnityMoqSample">github.com</a></cite></p>
<h1 id="雑感">雑感</h1>
<p>1ヶ月半ぶりの投稿です。</p>
<p>ネタはいっぱいあるんですが、今やってることに熱中しちゃってなかなかブログを書けませんでしたw。</p>
<p>次こそは3DCG系のネタをやりたいかなと思います。</p>
<p>なんかブログを出すこと自体が目的になってるようなとこがあるので、焦らずやっていこうかなと思います。</p>
<p>それでは~</p>
tsgcpp
【Extenject】AsTransient, AsCached, AsSingle を使い分けよう!
hatenablog://entry/26006613638775575
2020-10-10T16:27:16+09:00
2020-10-10T16:27:16+09:00 概要 動作環境 関連 前知識 ContractType と ResultType について Zenjectによるインスタンス生成 コンストラクタが複数ある場合 WithArguments の引数の数について Scope (AsTransient, AsCached, AsSingle) Scopeの違い AsTransient AsTransient の 利用例 AsCached AsCachedの使用例 AsSingle AsSingleの使用例 余談:Singletonパターンからの移行 Singletonパターンのデメリット Zenject + AsSingleでSingletonの問題…
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20201010/20201010091122.jpg" alt="f:id:tsgcpp:20201010091122j:plain" title="" class="hatena-fotolife" itemprop="image"></span></p>
<ul class="table-of-contents">
<li><a href="#概要">概要</a></li>
<li><a href="#動作環境">動作環境</a></li>
<li><a href="#関連">関連</a></li>
<li><a href="#前知識">前知識</a><ul>
<li><a href="#ContractType-と-ResultType-について">ContractType と ResultType について</a></li>
<li><a href="#Zenjectによるインスタンス生成">Zenjectによるインスタンス生成</a><ul>
<li><a href="#コンストラクタが複数ある場合">コンストラクタが複数ある場合</a></li>
<li><a href="#WithArguments-の引数の数について">WithArguments の引数の数について</a></li>
</ul>
</li>
</ul>
</li>
<li><a href="#Scope-AsTransient-AsCached-AsSingle">Scope (AsTransient, AsCached, AsSingle)</a><ul>
<li><a href="#Scopeの違い">Scopeの違い</a></li>
<li><a href="#AsTransient">AsTransient</a><ul>
<li><a href="#AsTransient-の-利用例">AsTransient の 利用例</a></li>
</ul>
</li>
<li><a href="#AsCached">AsCached</a><ul>
<li><a href="#AsCachedの使用例">AsCachedの使用例</a></li>
</ul>
</li>
<li><a href="#AsSingle">AsSingle</a><ul>
<li><a href="#AsSingleの使用例">AsSingleの使用例</a></li>
<li><a href="#余談Singletonパターンからの移行">余談:Singletonパターンからの移行</a><ul>
<li><a href="#Singletonパターンのデメリット">Singletonパターンのデメリット</a></li>
<li><a href="#Zenject--AsSingleでSingletonの問題を解決">Zenject + AsSingleでSingletonの問題を解決</a></li>
<li><a href="#AsSingleの注意点">AsSingleの注意点</a></li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li><a href="#簡単なまとめ">簡単なまとめ</a></li>
<li><a href="#サンプルプロジェクト">サンプルプロジェクト</a></li>
<li><a href="#雑感">雑感</a></li>
</ul>
<h1 id="概要">概要</h1>
<p>今回はZenjectの<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>生成の Scope である <code>AsTransient</code>, <code>AsCached</code>, <code>AsSingle</code> の使い分けについて解説します。</p>
<p>この3つは生成される<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>に明確なルール付けをすることができるので、上手に利用してあげてください!</p>
<p>例によって記事の最後にサンプルプロジェクトを置いておきます。</p>
<h1 id="動作環境">動作環境</h1>
<ul>
<li>Unity 2019.4.12f1</li>
<li><a href="https://github.com/svermeulen/Extenject">Extenject 9.2.0</a>
<ul>
<li>例によって現在保守されているExtenjectの方を使用</li>
</ul>
</li>
</ul>
<h1 id="関連">関連</h1>
<p>以前の記事です。 よかったらどうぞ!<br />
インストール方法やテストについて紹介しています。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftsgcpp.hateblo.jp%2Fentry%2F2020%2F06%2F17%2F142700" title=" 【Unity】Zenject (Extenject) を使った自動テストを紹介 - すぎしーのXRと3DCG" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://tsgcpp.hateblo.jp/entry/2020/06/17/142700">tsgcpp.hateblo.jp</a></cite></p>
<h1 id="前知識">前知識</h1>
<h2 id="ContractType-と-ResultType-について">ContractType と ResultType について</h2>
<p>Zenjectには ContractType と ResultType という概念があります。</p>
<ul>
<li>ContractType は <code>Bind<></code>に指定する型</li>
<li>ResultType は <code>To<></code> に指定する型</li>
</ul>
<p>以下を例にすると <code>IRandomizer</code> がContractType, <code>FixedRandomizer</code>がResultTypeです。</p>
<pre class="code lang-cs" data-lang="cs" data-unlink>Container
.Bind<IRandomizer>()
.To<FixedRandomizer>()
</pre>
<p>Scopeを考える上でこの ContractType と ResultType は重要なので意識しておいてください!</p>
<h2 id="Zenjectによるインスタンス生成">Zenjectによる<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>生成</h2>
<p><b>Containerにバインドすると基本的にZenjectはResultTypeの<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>を必要なときに生成してくれます。 </b></p>
<pre class="code lang-cs" data-lang="cs" data-unlink><span class="synType">public</span> <span class="synType">interface</span> IGreeter
{
<span class="synType">string</span> Greeting { get; }
}
</pre>
<pre class="code lang-cs" data-lang="cs" data-unlink><span class="synType">public</span> <span class="synType">class</span> ResultTypeGreeter : IGreeter
{
<span class="synType">private</span> <span class="synType">string</span> _greeting;
<span class="synType">public</span> ResultTypeGreeter(<span class="synType">string</span> message, <span class="synType">string</span> target)
{
_greeting = <span class="synType">string</span>.Format(<span class="synConstant">"{0} {1}!"</span>, message, target);
}
<span class="synType">public</span> <span class="synType">string</span> Greeting => _greeting;
}
</pre>
<pre class="code lang-cs" data-lang="cs" data-unlink>Container
.Bind<IGreeter>()
.To<ResultTypeGreeter>()
.AsTransient()
.WithArguments(<span class="synConstant">"Hey"</span>, <span class="synConstant">"Guys"</span>);
</pre>
<p>この例の場合、<code>ResultTypeGreeter</code> のコンスト<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%E9%A5%AF">ラク</a>タの第1引数に"Hey"、第2引数に"Guys"が渡されて<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>生成されます。<br />
結果、Zenjectによって生成された<code>ResultTypeGreeter</code>のプロパティ <code>Greeting</code> は "Hey Guys!" を返します。</p>
<p><code>FromInstance</code>で直接<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>を渡したりすることもできますが、Scopeのメリットが弱くなってしまいます(理由は後述)。</p>
<p><span style="font-size: 150%"><span style="color: #ff0000"><b>Scopeを使用する場合において、この<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>生成してくれる機能は非常に重要な意味を持ちます。</b></span></span></p>
<h3 id="コンストラクタが複数ある場合">コンスト<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%E9%A5%AF">ラク</a>タが複数ある場合</h3>
<p><code>[Inject]</code>で使用するコンスト<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%E9%A5%AF">ラク</a>タをZenjectに明示してください。</p>
<p>コンスト<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%E9%A5%AF">ラク</a>タが1つの場合は不要です。</p>
<p><a href="https://github.com/tsgcpp/ExtenjectScopeExample-Unity/blob/master/Assets/Scripts/ResultTypeGreeter.cs">ResultTypeGreeter.cs</a></p>
<p>以下のテストコードで動作確認してます。</p>
<p><a href="https://github.com/tsgcpp/ExtenjectScopeExample-Unity/blob/master/Assets/Tests/ResultTypeGreeterZenjectConstructionTest.cs">ResultTypeGreeterZenjectConstructionTest.cs</a></p>
<p>ResultType に複数のコンスト<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%E9%A5%AF">ラク</a>タがある場合はいずれかのコンスト<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%E9%A5%AF">ラク</a>タ(大抵は一番上に宣言したコンスト<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%E9%A5%AF">ラク</a>タ)が使用されます。<br />
予期せぬ動作になるかもしれないのでコンスト<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%E9%A5%AF">ラク</a>タが複数ある場合は<code>[Inject]</code>で明示したほうが良いと思います。</p>
<p>また、この<code>[Inject]</code>ですが複数の<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%B9%A5%C8%A5%E9%A5%AF%A5%BF%A1%BC">コンストラクター</a>に指定した場合はエラーになります。</p>
<p>つまり、<b>Zenjectが<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>生成に利用するコンスト<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%E9%A5%AF">ラク</a>タは1つ</b> ということになります。</p>
<h3 id="WithArguments-の引数の数について">WithArguments の引数の数について</h3>
<p>基本的に使用するコンスト<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%E9%A5%AF">ラク</a>タの引数と同じ数を指定する必要があります。</p>
<pre class="code lang-cs" data-lang="cs" data-unlink> <span class="synType">public</span> ResultTypeGreeter(<span class="synType">string</span> message, <span class="synType">string</span> target)
{
_greeting = <span class="synType">string</span>.Format(<span class="synConstant">"{0} {1}!"</span>, message, target);
}
</pre>
<p>以下のようにコンスト<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%E9%A5%AF">ラク</a>タの引数と<code>WithArguments</code>の数が一致していないとエラーになります。</p>
<pre class="code lang-cs" data-lang="cs" data-unlink> <span class="synComment">// エラー</span>
Container
.Bind<IGreeter>()
.To<ResultTypeGreeter>()
.AsTransient();
<span class="synComment">// WithArgumentsなし</span>
Container.Inject(<span class="synStatement">this</span>)
<span class="synComment">// エラー</span>
Container
.Bind<IGreeter>()
.To<ResultTypeGreeter>()
.AsTransient()
.WithArguments(<span class="synConstant">"Hi"</span>); <span class="synComment">// 引数1つ指定</span>
Container.Inject(<span class="synStatement">this</span>)
<span class="synComment">// 正常</span>
Container
.Bind<IGreeter>()
.To<ResultTypeGreeter>()
.AsTransient()
.WithArguments(<span class="synConstant">"Hey"</span>, <span class="synConstant">"Guys"</span>); <span class="synComment">// 2つ指定</span>
Container.Inject(<span class="synStatement">this</span>)
</pre>
<p>ただし、デフォルト引数が指定されている場合はエラーにならないようです。</p>
<pre class="code lang-cs" data-lang="cs" data-unlink> [Inject]
<span class="synType">public</span> ResultTypeGreeter(<span class="synType">string</span> message = <span class="synConstant">"Hello"</span>, <span class="synType">string</span> target = <span class="synConstant">"Everybody"</span>)
{
_greeting = <span class="synType">string</span>.Format(<span class="synConstant">"{0} {1}!"</span>, message, target);
}
</pre>
<p>ご参考までに。</p>
<h1 id="Scope-AsTransient-AsCached-AsSingle">Scope (AsTransient, AsCached, AsSingle)</h1>
<p>いよいよ本題です! ZenjectのScopeについて見ていきましょう。</p>
<p>Scope(スコープ)と聞くとやはりプログラミングにおけるスコープが思い当たりますが、Zenjectのスコープもほぼ同じ意味です。<br />
<b><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>のスコープ、つまり参照される範囲を指定</b> することになります。</p>
<h2 id="Scopeの違い">Scopeの違い</h2>
<table>
<thead>
<tr>
<th></th>
<th>AsTransient</th>
<th>AsCached</th>
<th>AsSingle</th>
<th>備考</th>
</tr>
</thead>
<tbody>
<tr>
<td>Context毎の<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>数</td>
<td>DI先のクラスごと</td>
<td>BindしたContractType(WithId指定有りの場合はさらにID毎)に1つ</td>
<td><span style="color: #ff0000">Context毎に1つ</span></td>
<td>Contextごとに管理</td>
</tr>
</tbody>
</table>
<p>それぞれ掘り下げて見ましょう。</p>
<h2 id="AsTransient">AsTransient</h2>
<p><code>AsTransient</code> は DI先のクラス毎に異なる<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>として渡されます。<br />
渡された<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>はすべて別物なので好き勝手に変更しても、他のDI先には影響しないことになります。</p>
<p><a href="https://github.com/tsgcpp/ExtenjectScopeExample-Unity/blob/master/Assets/Tests/AsTransientTest.cs">AsTransient確認用UnitTest</a></p>
<p>以下のような用途に使えます。</p>
<ul>
<li>メモリ使用量を気にしないからとりあえずDIで挙動を変更したい</li>
<li>DI先ごとに別の<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>として持たせたい(他に影響させたくない)</li>
</ul>
<h3 id="AsTransient-の-利用例">AsTransient の 利用例</h3>
<p>DI先には<code>ICollection</code>として使用してもらい、バインドで<code>List</code> や <code>LinkedList</code>などを切り替えるなどの使用方法が考えられます。</p>
<pre class="code lang-cs" data-lang="cs" data-unlink> <span class="synComment">// ListをDIしたい場合</span>
Container
.Bind<ICollection<<span class="synType">int</span>>>()
.To<List<<span class="synType">int</span>>>()
.AsTransient();
Container.Inject(<span class="synStatement">this</span>);
</pre>
<pre class="code lang-cs" data-lang="cs" data-unlink> <span class="synComment">// LinkedListをDIしたい場合</span>
Container
.Bind<ICollection<<span class="synType">int</span>>>()
.To<LinkedList<<span class="synType">int</span>>>()
.AsTransient();
Container.Inject(<span class="synStatement">this</span>);
</pre>
<pre class="code lang-cs" data-lang="cs" data-unlink> <span class="synComment">// SortedListをDIしたい場合</span>
Container
.Bind<ICollection<<span class="synType">int</span>>>()
.To<SortedList<<span class="synType">int</span>>>()
.AsTransient();
Container.Inject(<span class="synStatement">this</span>);
</pre>
<p>DI先のコードを変更することなく性能(LinkedList)や機能(SortedList)を変えることができます。</p>
<p><a href="https://github.com/tsgcpp/ExtenjectScopeExample-Unity/blob/master/Assets/Tests/CollectionInjectionExampleTest.cs">ICollectionを使用したAsTransientサンプル</a></p>
<h2 id="AsCached">AsCached</h2>
<p><code>AsTransient</code> は DI先のクラス毎に同一の<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>として渡されます。<br />
<code>WithId</code>を指定している場合はIDごとに異なる<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>となります。</p>
<p><a href="https://github.com/tsgcpp/ExtenjectScopeExample-Unity/blob/master/Assets/Tests/AsCachedTest.cs">AsCached確認用UnitTest</a></p>
<p><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>の単位は細かくは以下のようになります。</p>
<ul>
<li>ContractType毎に1つ</li>
<li>ID指定無しは「ID指定無しのグループ」として1つ生成</li>
<li>ID指定有りの場合は、IDごとに1つ生成</li>
</ul>
<pre class="code lang-cs" data-lang="cs" data-unlink> <span class="synComment">// ID指定無し向けのインスタンス</span>
Container
.Bind<IGreeter>()
.To<ResultTypeGreeter>()
.AsCached();
<span class="synComment">// ID_01向けのインスタンス</span>
Container
.Bind<IGreeter>()
.WithId(<span class="synConstant">"ID_01"</span>)
.To<ResultTypeGreeter>()
.AsCached();
<span class="synComment">// ID_02向けのインスタンス</span>
Container
.Bind<IGreeter>()
.WithId(<span class="synConstant">"ID_02"</span>)
.To<ResultTypeGreeter>()
.AsCached();
</pre>
<p>以下のような用途に使えます。</p>
<ul>
<li>ResultTypeは同じだけど、DI先のグループごとに異なる<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>にしたい
<ul>
<li>後述しますが、AsSingleの場合はResultTypeは1つしか作成されません</li>
</ul>
</li>
</ul>
<h3 id="AsCachedの使用例">AsCachedの使用例</h3>
<p>今回はとあるメッセージキューをグループに分けてインジェクションさせてみましょう。</p>
<pre class="code lang-cs" data-lang="cs" data-unlink> Container
.Bind<Queue<<span class="synType">string</span>>>() <span class="synComment">// Queueは残念ながら専用interfaceなし</span>
.WithId(Group01)
<span class="synComment">// .To<Queue<string>>() // ContractTypeとResultTypeが同じならToは不要</span>
.AsCached();
Container
.Bind<Queue<<span class="synType">string</span>>>()
.WithId(Group02)
.AsCached();
</pre>
<p><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>を受け取る先は引数にId を指定</p>
<pre class="code lang-cs" data-lang="cs" data-unlink> <span class="synType">public</span> <span class="synType">class</span> DITarget01
{
<span class="synType">private</span> Queue<<span class="synType">string</span>> _queue;
<span class="synComment">// MonoBehaviourでない場合はIdは引数で指定</span>
<span class="synType">public</span> DITarget01([Inject(Id = Group01)] Queue<<span class="synType">string</span>> queue)
{
_queue = queue;
}
<span class="synType">public</span> <span class="synType">void</span> Enqueue()
{
_queue.Enqueue(<span class="synConstant">"Hello"</span>);
}
}
</pre>
<p>メッセージキューをグループで別<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>として管理することが可能になります。</p>
<p><a href="https://github.com/tsgcpp/ExtenjectScopeExample-Unity/blob/master/Assets/Tests/QueueInjectionExampleTest.cs">Queueを使用したAsCachedサンプル</a></p>
<h2 id="AsSingle">AsSingle</h2>
<p><code>AsSingle</code> は <b>ResultTypeの<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>が単一であることを保証するScope</b>です。<br />
<span style="color: #999999">Singleという単語がついている通りですね。</span></p>
<p>どう保証するのかというと、同じResultTypeがバインドされたときに例外(<code>ZenjectException</code>)を出すようになっています。</p>
<pre class="code lang-cs" data-lang="cs" data-unlink> Container
.Bind<IGreeter>()
.To<ResultTypeGreeter>()
.AsSingle();
<span class="synComment">// IDを指定しようが、AsTransientを指定しようが例外を出す</span>
Container
.Bind<IGreeter>()
.WithId(Identifier01)
.To<ResultTypeGreeter>()
.AsTransient();
</pre>
<p>以下のような用途に使うと良いと思います。</p>
<ul>
<li>複数の<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>を作成したくないResultTypeのバインド
<ul>
<li>複数<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>が存在するとおかしくなる(通信に関わる部分など)</li>
<li>メモリを無駄に消費したくない</li>
</ul>
</li>
<li>複数の<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>を作る必要がないもの
<ul>
<li>引数に対して必ず同じ結果を返す(副作用のない)メソッド(関数)を持つResultType</li>
<li>Strategyパターン、Factoryパターンのクラスなど</li>
</ul>
</li>
<li>Singletonパターンからの移行(理由は後述)</li>
</ul>
<p>ちなみにですが、<code>FromInstance</code>を使用すると<code>AsSingle</code>の恩恵がなくなってしまうので、<br />
<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>の単一性を保証したいのであれば<code>To<></code>を使用して、Zenjectに<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>を管理させましょう。</p>
<p><span style="color: #999999">個人的には<a class="keyword" href="http://d.hatena.ne.jp/keyword/%C1%C2%B7%EB%B9%E7">疎結合</a>化がZenjectの主な使用目的のため、<code>AsSingle</code>を一番使用しています。</span></p>
<h3 id="AsSingleの使用例">AsSingleの使用例</h3>
<p>いろいろな使用例があるのですが、今回はシンプルに<a class="keyword" href="http://d.hatena.ne.jp/keyword/%C1%C2%B7%EB%B9%E7">疎結合</a>化の実現で使ってみましょう。</p>
<p><a href="https://tsgcpp.hateblo.jp/entry/2020/06/17/142700#%E3%83%86%E3%82%B9%E3%83%88%E5%AF%BE%E8%B1%A1%E3%82%B3%E3%83%BC%E3%83%89%E3%81%AE%E4%BD%9C%E6%88%90">前回の記事</a> の <code>IRandomizer</code>のように<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>は全体で1つのみでいいようなResultTypeに使用すると良いと思います。</p>
<h3 id="余談Singletonパターンからの移行">余談:Singletonパターンからの移行</h3>
<p><code>AsSingle</code>を見て「Singletonパターンで良くない?」って思った方もいるかもしれません。<br />
DIにすることで様々な恩恵があると考えていますので取り上げたいと思います。</p>
<h4 id="Singletonパターンのデメリット">Singletonパターンのデメリット</h4>
<ul>
<li>参照先のクラスと密結合になりテスタビリティが損なわれる
<ul>
<li>参照先の挙動をUnitTest時に変更できない</li>
<li>例: <code>StaticClass.Instance.Method()</code>の場合はStaticClassと密結合</li>
</ul>
</li>
<li>Unityで同一シーンの読み込み毎に前回の状態に引きずられてしまう
<ul>
<li>1回目と2回目以降で動作が変わるリスクが生まれる</li>
<li>実装が簡単だが、シーンの切り替え毎にリセット処理など注意する点が多い</li>
</ul>
</li>
<li>生成した<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>がいつまでも残り続ける(static変数として使用する場合)
<ul>
<li>staticオブジェクトが解放されない</li>
<li>ヒープ領域の整理の際に障害になり得る(極稀な話だと思いますが)</li>
</ul>
</li>
<li><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>生成のタイミングが<a class="keyword" href="http://d.hatena.ne.jp/keyword/%C9%D4%C4%EA">不定</a>(個人的に嫌いな性質)
<ul>
<li>大抵は初回アクセス時に<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>生成
<ul>
<li><code>if (_instance == null) _instance = new StaticClass();</code></li>
</ul>
</li>
</ul>
</li>
</ul>
<p>色々と書きましたが「実装は簡単だけど、注意がかなり必要」な<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C7%A5%B6%A5%A4%A5%F3%A5%D1%A5%BF%A1%BC%A5%F3">デザインパターン</a>だと考えています。</p>
<p><span style="color: #ff0000">特にUnityや<a class="keyword" href="http://d.hatena.ne.jp/keyword/iOS">iOS</a>, <a class="keyword" href="http://d.hatena.ne.jp/keyword/Android">Android</a>のネイティブアプリのように "ライフサイクル" を持つアプリケーション開発においては特に注意が必要です。</span></p>
<h4 id="Zenject--AsSingleでSingletonの問題を解決">Zenject + AsSingleでSingletonの問題を解決</h4>
<p>さて、このSingletonパターンですが<b>大体はDIによる移行が可能だと考えています</b>。</p>
<p>Singletonパターンを使う理由の大体は「<i>同一の<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>にアクセスしたい</i>」だと思います。</p>
<p>Zenjectを使っているならResultTypeを<code>AsSingle</code>でバインドしてDIすることで、上記デメリットを解消した上で同一<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>にアクセスするという要望を満たせると思います。</p>
<p>どうしてもSingletonでないといけないパターンがあるかもしれませんが、<br />
<span style="color: #ff0000">Singletonを使用する理由が「簡単だから」であれば、一度DIの使用を検討してみてください。</span></p>
<p><span style="color: #cccccc">プロジェクトが大きくなると密結合のSingletonパターンは。。。</span></p>
<h4 id="AsSingleの注意点">AsSingleの注意点</h4>
<p>ExtenjectのREADMEにも記載されていますが、<code>AsSingle</code>は同一のContainer(Context)毎に<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>が管理されます。</p>
<p>ProjectContext と SceneContext両方に同じResultTypeをバインドしても例外はでないのでご留意ください。</p>
<p>サンプルシーン <code>Assets/Scenes/GreeterDISample.unity</code> に ProjectContext と SceneContext両方でGreeterをAsSingleでバインドしています。<br />
よかったら動かしてみてください。</p>
<h1 id="簡単なまとめ">簡単なまとめ</h1>
<ul>
<li><code>AsTransient</code>は挙動は変えたいけど、<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>は共有したくないとき</li>
<li><code>AsCached</code>はIDグループ毎に<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>を共有したいとき</li>
<li><code>AsSingle</code>はResultTypeの<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>を必ず1つだけにしたいとき</li>
</ul>
<p>に使うといい気がします(超大雑把)</p>
<h1 id="サンプルプロジェクト">サンプルプロジェクト</h1>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Ftsgcpp%2FExtenjectScopeExample-Unity" title="tsgcpp/ExtenjectScopeExample-Unity" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://github.com/tsgcpp/ExtenjectScopeExample-Unity">github.com</a></cite></p>
<h1 id="雑感">雑感</h1>
<p>久々の記事投稿です。<br />
<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%DD%A1%BC%A5%C8%A5%D5%A5%A9%A5%EA%A5%AA">ポートフォリオ</a>に集中してたんですが、さすがにネタが溜まりすぎて来たので簡単なやつを1つでも解消したいと思い記事にしました。</p>
<p>Zenjectは使ってて本当に気持ちいいですね~。</p>
<p>Zenjectを使ってて思ったメリットとして 「UnityでDIが実現できる」だけじゃなくて「UnityでMonoBehaviour以外のクラスが使いやすくなる」っていうのもあると思います!
<a class="keyword" href="http://d.hatena.ne.jp/keyword/%C1%C2%B7%EB%B9%E7">疎結合</a>にできるライブラリですが、自分自身はZenjectに密結合になっちゃってますね~。</p>
<p>それでは良いUnityライフを!</p>
tsgcpp
【Unity】Shader Graphで走査線っぽいもの
hatenablog://entry/26006613625525927
2020-09-11T01:06:09+09:00
2020-09-11T01:06:09+09:00 概要 動作環境 今回作るもの 実装機能 シェーダーテクニック UVノード と 分離(Split) Fraction Time + UV + Split + Fraction 三角波 三角波 + Step 三角波 + Smoothstep 余談:Time + Fraction + Smoothstep 波の出現の周期を変える 完成形 サンプルプロジェクト 雑感 概要 今回は Shader Graph で走査線っぽいもの作り方を紹介します。 併せてシェーダーを作るときの考え方も解説したいと思います! 動作環境 Unity 2020.1.4f1 Universal RP 8.2.0 今回作るもの 以…
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20200908/20200908230132.jpg" alt="f:id:tsgcpp:20200908230132j:plain" title="f:id:tsgcpp:20200908230132j:plain" class="hatena-fotolife" itemprop="image"></span></p>
<ul class="table-of-contents">
<li><a href="#概要">概要</a></li>
<li><a href="#動作環境">動作環境</a></li>
<li><a href="#今回作るもの">今回作るもの</a><ul>
<li><a href="#実装機能">実装機能</a></li>
</ul>
</li>
<li><a href="#シェーダーテクニック">シェーダーテクニック</a><ul>
<li><a href="#UVノード-と-分離Split">UVノード と 分離(Split)</a></li>
<li><a href="#Fraction">Fraction</a></li>
<li><a href="#Time--UV--Split--Fraction">Time + UV + Split + Fraction</a></li>
<li><a href="#三角波">三角波</a></li>
<li><a href="#三角波--Step">三角波 + Step</a></li>
<li><a href="#三角波--Smoothstep">三角波 + Smoothstep</a></li>
<li><a href="#余談Time--Fraction--Smoothstep">余談:Time + Fraction + Smoothstep</a></li>
<li><a href="#波の出現の周期を変える">波の出現の周期を変える</a></li>
</ul>
</li>
<li><a href="#完成形">完成形</a></li>
<li><a href="#サンプルプロジェクト">サンプルプロジェクト</a></li>
<li><a href="#雑感">雑感</a></li>
</ul>
<h1 id="概要">概要</h1>
<p>今回は Shader Graph で走査線っぽいもの作り方を紹介します。
併せてシェーダーを作るときの考え方も解説したいと思います!</p>
<h1 id="動作環境">動作環境</h1>
<ul>
<li>Unity 2020.1.4f1
<ul>
<li>Universal RP 8.2.0</li>
</ul>
</li>
</ul>
<h1 id="今回作るもの">今回作るもの</h1>
<p>以下の動画の様にスクロールする走査線を作ります。</p>
<p><iframe width="480" height="270" src="https://www.youtube.com/embed/MPDoB0HWM8U?feature=oembed" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe><cite class="hatena-citation"><a href="https://www.youtube.com/watch?v=MPDoB0HWM8U&feature=youtu.be">www.youtube.com</a></cite></p>
<h2 id="実装機能">実装機能</h2>
<p>せっかくのシェーダなので、パラメータで以下のものを調整できるようにしましょう。</p>
<ul>
<li>走査線の色</li>
<li>走査線の幅</li>
<li>走査線の速度</li>
<li>走査線の出現頻度</li>
<li>UV座標のV方向に走査線が移動(理由は後述)</li>
</ul>
<h1 id="シェーダーテクニック">シェーダーテクニック</h1>
<p>シェーダーを実装する前にいくつかテクニックを紹介します。</p>
<h2 id="UVノード-と-分離Split">UVノード と 分離(Split)</h2>
<p><a href="https://docs.unity3d.com/Packages/[email protected]/manual/UV-Node.html">UVノード</a> は文字通りUV座標を取り出すことができます。<br />
R要素にはU座標、G要素にはV座標が含まれています。</p>
<p>UV座標は[0, 1]の範囲のため、Splitノードで要素をU, Vに分離することでグレースケールのグラデーションを作ることができます。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20200909/20200909001555.png" alt="f:id:tsgcpp:20200909001555p:plain" title="f:id:tsgcpp:20200909001555p:plain" class="hatena-fotolife" itemprop="image"></span></p>
<p>V要素に関して言えば下方(0の方)が黒、上方(1の方)が白というグレースケールを取り出すことができます。</p>
<h2 id="Fraction">Fraction</h2>
<p><a href="https://docs.unity3d.com/Packages/[email protected]/manual/Fraction-Node.html">Fractionノード</a> というものがあります。<br />
「入力数値の小数点を取り出す」ノードとなります。</p>
<p>グラフで表すと以下のような一次関数を並べた感じになります。</p>
<p><figure class="figure-image figure-image-fotolife" title="Fractionのグラフ(横軸: 入力、縦軸: 出力)"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20200909/20200909003158.jpg" alt="f:id:tsgcpp:20200909003158j:plain" title="f:id:tsgcpp:20200909003158j:plain" class="hatena-fotolife" itemprop="image"></span><figcaption>Fractionのグラフ(横軸: 入力、縦軸: 出力)</figcaption></figure></p>
<p>簡単に言うと 0 から 1 (厳密には0.999...) を繰り返すノードを作成することができます。</p>
<p><iframe width="480" height="270" src="https://www.youtube.com/embed/L88GDDE-6rY?feature=oembed" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe><cite class="hatena-citation"><a href="https://www.youtube.com/watch?v=L88GDDE-6rY">www.youtube.com</a></cite></p>
<h2 id="Time--UV--Split--Fraction">Time + UV + Split + Fraction</h2>
<p><iframe width="480" height="270" src="https://www.youtube.com/embed/oX1y-QAm_7k?feature=oembed" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe><cite class="hatena-citation"><a href="https://www.youtube.com/watch?v=oX1y-QAm_7k">www.youtube.com</a></cite></p>
<p><iframe width="459" height="344" src="https://www.youtube.com/embed/nIt0nW3lIVY?feature=oembed" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe><cite class="hatena-citation"><a href="https://www.youtube.com/watch?v=nIt0nW3lIVY">www.youtube.com</a></cite></p>
<p>所謂「UVスクロール」です。</p>
<p>UVをSplitノードで分離することでU or V方向にスクロールするノードとなります。<br />
また、AddノードとSubtractノードでスクロール方向が反転する性質があります。</p>
<h2 id="三角波"><a class="keyword" href="http://d.hatena.ne.jp/keyword/%BB%B0%B3%D1%C7%C8">三角波</a></h2>
<p><iframe width="459" height="344" src="https://www.youtube.com/embed/g-E_ZpJmRO0?feature=oembed" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe><cite class="hatena-citation"><a href="https://www.youtube.com/watch?v=g-E_ZpJmRO0">www.youtube.com</a></cite></p>
<p>所謂「<a class="keyword" href="http://d.hatena.ne.jp/keyword/%BB%B0%B3%D1%C7%C8">三角波</a>ノード」です。</p>
<p>このノード、見た目はシンプルですが <b>「走査線の幅」の調整とかなり相性が良かったりします</b>。</p>
<div onclick="obj=document.getElementById('expand_triangle_wave').style; obj.display=(obj.display=='none')?'block':'none';">
<a style="cursor:pointer;">...クリックで展開:三角波作成の流れ</a>
</div>
<div id="expand_triangle_wave" style="display:none;clear:both;">
1. Fractionの結果を2倍
<span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20200910/20200910222118.jpg" alt="f:id:tsgcpp:20200910222118j:plain" title="f:id:tsgcpp:20200910222118j:plain" class="hatena-fotolife" itemprop="image"></span>
<br>
<br>
<br>
2. -1 して[-1, 1]の範囲にする(半分をマイナスにする)
<span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20200910/20200910222234.jpg" alt="f:id:tsgcpp:20200910222234j:plain" title="f:id:tsgcpp:20200910222234j:plain" class="hatena-fotolife" itemprop="image"></span>
<br>
<br>
<br>
3. 絶対値をとりマイナス部分を0を境界に反転
<span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20200910/20200910222313.jpg" alt="f:id:tsgcpp:20200910222313j:plain" title="f:id:tsgcpp:20200910222313j:plain" class="hatena-fotolife" itemprop="image"></span>
</div>
<p><figure class="figure-image figure-image-fotolife" title="三角波のイメージ"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20200909/20200909014633.png" alt="f:id:tsgcpp:20200909014633p:plain" title="f:id:tsgcpp:20200909014633p:plain" class="hatena-fotolife" itemprop="image"></span><figcaption><a class="keyword" href="http://d.hatena.ne.jp/keyword/%BB%B0%B3%D1%C7%C8">三角波</a>のイメージ</figcaption></figure></p>
<p>Fractionを[0, 1] → [-1, 1]に変換して絶対値を取ると、0 → 1 → 0...を繰り返す<a class="keyword" href="http://d.hatena.ne.jp/keyword/%BB%B0%B3%D1%C7%C8">三角波</a>を実現できます。</p>
<h2 id="三角波--Step"><a class="keyword" href="http://d.hatena.ne.jp/keyword/%BB%B0%B3%D1%C7%C8">三角波</a> + Step</h2>
<p>先程作成した<a class="keyword" href="http://d.hatena.ne.jp/keyword/%BB%B0%B3%D1%C7%C8">三角波</a>ノードをStepに渡すと以下のようなノードになります。</p>
<p><iframe width="459" height="344" src="https://www.youtube.com/embed/KE85toKuvDo?feature=oembed" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe><cite class="hatena-citation"><a href="https://www.youtube.com/watch?v=KE85toKuvDo">www.youtube.com</a></cite></p>
<p>何が言いたいかというと「<b>波の幅をパラメータで設定しやすい</b>」ということです!</p>
<div onclick="obj=document.getElementById('expand_triangle_wave_step').style; obj.display=(obj.display=='none')?'block':'none';">
<a style="cursor:pointer;">...クリックで展開:三角波とStepのイメージ</a>
</div>
<div id="expand_triangle_wave_step" style="display:none;clear:both;">
<figure class="figure-image figure-image-fotolife" title="Edge = 0.5 の場合 50%が0、50%が1"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20200910/20200910232017.png" alt="f:id:tsgcpp:20200910232017p:plain" title="f:id:tsgcpp:20200910232017p:plain" class="hatena-fotolife" itemprop="image"></span><figcaption>Edge = 0.5 の場合 50%が0、50%が1</figcaption></figure>
<figure class="figure-image figure-image-fotolife" title="Edge = 0.75 の場合 75%が0、25%が1"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20200910/20200910232044.png" alt="f:id:tsgcpp:20200910232044p:plain" title="f:id:tsgcpp:20200910232044p:plain" class="hatena-fotolife" itemprop="image"></span><figcaption>Edge = 0.75 の場合 75%が0、25%が1</figcaption></figure>
</div>
<h2 id="三角波--Smoothstep"><a class="keyword" href="http://d.hatena.ne.jp/keyword/%BB%B0%B3%D1%C7%C8">三角波</a> + Smoothstep</h2>
<p><b><span style="color: #ff0000">今回の主役ノードです!</span></b></p>
<p><iframe width="480" height="270" src="https://www.youtube.com/embed/tbymATZz870?feature=oembed" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe><cite class="hatena-citation"><a href="https://www.youtube.com/watch?v=tbymATZz870">www.youtube.com</a></cite></p>
<p><a class="keyword" href="http://d.hatena.ne.jp/keyword/%BB%B0%B3%D1%C7%C8">三角波</a>はSmoothstepとも相性が良いです。<br />
グラデーション付きの波をシームレスに作ることができます。</p>
<h2 id="余談Time--Fraction--Smoothstep">余談:Time + Fraction + Smoothstep</h2>
<p>stepをであれば"Time + fraction"で十分ですが、 Smoothstepの場合は残念ながらシームレスになりません。<br />
<a class="keyword" href="http://d.hatena.ne.jp/keyword/%BB%B0%B3%D1%C7%C8">三角波</a>を作った理由は「シームレスな波が必要」だったからなんですね。</p>
<h2 id="波の出現の周期を変える">波の出現の周期を変える</h2>
<p><iframe width="459" height="344" src="https://www.youtube.com/embed/Xgl_Mz0yCzw?feature=oembed" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe><cite class="hatena-citation"><a href="https://www.youtube.com/watch?v=Xgl_Mz0yCzw">www.youtube.com</a></cite></p>
<p>これはUVスクロールの値を周期数で割り、StepのEdgeも同様に周期数で割れば実現できます。</p>
<p>※UVスクロールの値だけだと走査線になる部分の幅が広がってしまいますので、幅も割って調整します。</p>
<p>UVスクロールのスピードを遅らせるということですね。</p>
<h1 id="完成形">完成形</h1>
<p>上記のテクニックを組み合わせて、プロパティで調整可能にしてアルファ(不透明度)に使用したのが今回のシェーダになります!</p>
<p><iframe width="459" height="344" src="https://www.youtube.com/embed/t_lwlVAGoKM?feature=oembed" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe><cite class="hatena-citation"><a href="https://www.youtube.com/watch?v=t_lwlVAGoKM">www.youtube.com</a></cite></p>
<p><a class="keyword" href="http://d.hatena.ne.jp/keyword/Github">Github</a>のプロジェクトに完成形を置いておきますので、良かったらどうぞ。</p>
<h1 id="サンプルプロジェクト">サンプルプロジェクト</h1>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Ftsgcpp%2FShaderGraphSamples-tsgcpp" title="tsgcpp/ShaderGraphSamples-tsgcpp" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://github.com/tsgcpp/ShaderGraphSamples-tsgcpp">github.com</a></cite></p>
<h1 id="雑感">雑感</h1>
<p>今回はShader Graphを題材にしてみました。<br />
記事を書いていて思ったのは「これ、音声解説 + 動画のほうが絶対わかりやすいだろ」でした。。。</p>
<p>別にAviutl使えないわけじゃないんですがちょっと<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C8%A1%BC%A5%AF">トーク</a>に自信がないです。。。<br />
ただ動画のほうが情報をたくさん詰め込めますし、僕みたいに字だとわからなくても動画だと理解しやすい人のほうが多いと思いますのでいつか挑戦してみたいです。</p>
<p>次回ですが、そろそろ以前から考えていた<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%DD%A1%BC%A5%C8%A5%D5%A5%A9%A5%EA%A5%AA">ポートフォリオ</a>に本格的に集中しようと思いますので、 しばらくは投稿頻度は下がると思います。</p>
<p>それでは~。</p>
tsgcpp
【C#】え、Generic Interfaceでメソッド引数を設定すれば構造体のBoxingを回避できるの?
hatenablog://entry/26006613618073056
2020-08-22T18:29:43+09:00
2020-08-22T18:29:43+09:00 概要 Constraints on type parameters について 例:structで制約を付けた場合 interfaceを継承した構造体のboxing intefaceを引数としたメソッドに渡した場合のBoxing なぜBoxingが発生したのか Generic Interfaceによる構造体のBoxingの回避 Generic Interfaceとは なぜBoxingが回避されたのか Generic InterfaceでのBoxing回避はメソッド限定 余談: OpCodes.Constrained Field 余談:なぜBoxingが必要なのか 雑感 どうも、最近GC Allo…
<ul class="table-of-contents">
<li><a href="#概要">概要</a></li>
<li><a href="#Constraints-on-type-parameters-について">Constraints on type parameters について</a><ul>
<li><a href="#例structで制約を付けた場合">例:structで制約を付けた場合</a></li>
</ul>
</li>
<li><a href="#interfaceを継承した構造体のboxing">interfaceを継承した構造体のboxing</a><ul>
<li><a href="#intefaceを引数としたメソッドに渡した場合のBoxing">intefaceを引数としたメソッドに渡した場合のBoxing</a></li>
<li><a href="#なぜBoxingが発生したのか">なぜBoxingが発生したのか</a></li>
</ul>
</li>
<li><a href="#Generic-Interfaceによる構造体のBoxingの回避">Generic Interfaceによる構造体のBoxingの回避</a><ul>
<li><a href="#Generic-Interfaceとは">Generic Interfaceとは</a></li>
<li><a href="#なぜBoxingが回避されたのか">なぜBoxingが回避されたのか</a></li>
<li><a href="#Generic-InterfaceでのBoxing回避はメソッド限定">Generic InterfaceでのBoxing回避はメソッド限定 </a></li>
<li><a href="#余談-OpCodesConstrained-Field">余談: OpCodes.Constrained Field</a></li>
</ul>
</li>
<li><a href="#余談なぜBoxingが必要なのか">余談:なぜBoxingが必要なのか</a></li>
<li><a href="#雑感">雑感</a></li>
</ul>
<p>どうも、最近<a class="keyword" href="http://d.hatena.ne.jp/keyword/GC">GC</a> Allocにおびえているすぎしーです。<br />
今日はUnityじゃなくて<a class="keyword" href="http://d.hatena.ne.jp/keyword/C%23">C#</a>がメインの話題です。</p>
<h2 id="概要">概要</h2>
<p>今回はGeneric Interfaceと値型の<a class="keyword" href="http://d.hatena.ne.jp/keyword/Boxing">Boxing</a>回避についてお話します!<br />
<a class="keyword" href="http://d.hatena.ne.jp/keyword/Boxing">Boxing</a>は余計な<a class="keyword" href="http://d.hatena.ne.jp/keyword/GC">GC</a> Allocが発生してしまいますが、回避が難しいパターンも存在します。</p>
<p>ただ、一見変わった方法でその<a class="keyword" href="http://d.hatena.ne.jp/keyword/Boxing">Boxing</a>を回避する方法があったので紹介したいと思います!</p>
<h2 id="Constraints-on-type-parameters-について">Constraints on type parameters について</h2>
<p>要するにGenericで指定される型に制約を付けられる<a class="keyword" href="http://d.hatena.ne.jp/keyword/C%23">C#</a>の機能のことです。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdocs.microsoft.com%2Fen-us%2Fdotnet%2Fcsharp%2Fprogramming-guide%2Fgenerics%2Fconstraints-on-type-parameters" title="Constraints on type parameters - C# Programming Guide" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/generics/constraints-on-type-parameters">docs.microsoft.com</a></cite></p>
<p><a class="keyword" href="http://d.hatena.ne.jp/keyword/C%23">C#</a>では、<code>where</code>句を使うことでGenericの型に制約を付けることができます。</p>
<h3 id="例structで制約を付けた場合">例:structで制約を付けた場合</h3>
<p>例えば、以下のように<code>where T : struct</code>とつけると、Tの指定をstruct(構造体)のみに制限させることができます。</p>
<pre class="code lang-cs" data-lang="cs" data-unlink><span class="synType">public</span> <span class="synType">class</span> GenericData<T> <span class="synStatement">where</span> T : <span class="synType">struct</span>
{
T Value { get; }
}
</pre>
<p>以下のようなコードで確認することができます。</p>
<pre class="code lang-cs" data-lang="cs" data-unlink><span class="synComment">// エラーなし</span>
GenericData<StructSample> structData;
<span class="synType">public</span> <span class="synType">struct</span> StructSample
{
}
</pre>
<pre class="code lang-cs" data-lang="cs" data-unlink><span class="synComment">// エラー: The type 'ClassSample' must be a non-nullable value type in order to use it as parameter 'T' in the generic type or method 'GenericData<T>'</span>
GenericData<ClassSample> classData;
<span class="synType">public</span> <span class="synType">class</span> ClassSample
{
}
</pre>
<pre class="code lang-cs" data-lang="cs" data-unlink><span class="synComment">// エラー: The type 'IInterfaceSample' must be a non-nullable value type in order to use it as parameter 'T' in the generic type or method 'GenericData<T>'</span>
GenericData<IInterfaceSample> interfaceData
<span class="synType">public</span> <span class="synType">interface</span> IInterfaceSample
{
}
</pre>
<h2 id="interfaceを継承した構造体のboxing">interfaceを継承した構造体の<a class="keyword" href="http://d.hatena.ne.jp/keyword/boxing">boxing</a></h2>
<p><a class="keyword" href="http://d.hatena.ne.jp/keyword/C%23">C#</a>の構造体はinterfaceを継承することができます。</p>
<pre class="code lang-cs" data-lang="cs" data-unlink><span class="synComment">// ILoggerを継承した構造体</span>
<span class="synType">public</span> <span class="synType">struct</span> Data : ILogger
{
<span class="synType">public</span> <span class="synType">int</span> <span class="synStatement">value</span>;
<span class="synType">public</span> <span class="synType">void</span> Log()
{
Debug.Log(<span class="synConstant">"I am `Data`"</span>);
}
}
<span class="synType">public</span> <span class="synType">interface</span> ILogger
{
<span class="synType">void</span> Log();
}
</pre>
<p>さて、このinterfaceを継承した構造体ですが意図しない<a class="keyword" href="http://d.hatena.ne.jp/keyword/Boxing">Boxing</a>があっさり発生します。。。</p>
<pre class="code" data-lang="" data-unlink>var data = new Data { value = 0, };
// ILogger型の変数に構造体Dataを渡す
ILogger logger = data;</pre>
<div onclick="obj=document.getElementById('expand_boxing_check').style; obj.display=(obj.display=='none')?'block':'none';">
<a style="cursor:pointer;">...クリックで展開:Boxing発生の確認</a>
</div>
<div id="expand_boxing_check" style="display:none;clear:both;">
UnityのProfilerを使えば簡単に確認できます。
<pre class="code lang-cs" data-lang="cs" data-unlink>var data = <span class="synStatement">new</span> Data { <span class="synStatement">value</span> = <span class="synConstant">0</span> };
Profiler.BeginSample(<span class="synConstant">"Profile: Data to ILogger variable"</span>);
ILogger logger = data;
Profiler.EndSample()
</pre>
<span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20200822/20200822123938.jpg" alt="f:id:tsgcpp:20200822123938j:plain" title="f:id:tsgcpp:20200822123938j:plain" class="hatena-fotolife" itemprop="image"></span>
</div>
<h3 id="intefaceを引数としたメソッドに渡した場合のBoxing">intefaceを引数としたメソッドに渡した場合の<a class="keyword" href="http://d.hatena.ne.jp/keyword/Boxing">Boxing</a></h3>
<p>先述した<a class="keyword" href="http://d.hatena.ne.jp/keyword/Boxing">Boxing</a>ですが、引数の型がinterfaceのメソッドに引数として渡しても発生します。</p>
<pre class="code lang-cs" data-lang="cs" data-unlink>var data = <span class="synStatement">new</span> Data { <span class="synStatement">value</span> = <span class="synConstant">0</span> };
Func(data); <span class="synComment">// 引数として渡す</span>
<span class="synComment">// 引数の型がinterfaceのメソッド</span>
<span class="synType">public</span> <span class="synType">void</span> Func(ILogger logger)
{
<span class="synComment">// 何もしない</span>
}
</pre>
<div onclick="obj=document.getElementById('expand_boxing_check_as_a_arg').style; obj.display=(obj.display=='none')?'block':'none';">
<a style="cursor:pointer;">...クリックで展開:Boxing発生の確認</a>
</div>
<div id="expand_boxing_check_as_a_arg" style="display:none;clear:both;">
<pre class="code lang-cs" data-lang="cs" data-unlink>var data = <span class="synStatement">new</span> Data { <span class="synStatement">value</span> = <span class="synConstant">0</span> };
Profiler.BeginSample(<span class="synConstant">"Profile: Data as a ILogger variable"</span>);
Func(data);
Profiler.EndSample()
<span class="synType">public</span> <span class="synType">void</span> Func(ILogger logger)
{
<span class="synComment">// 何もしない</span>
}
</pre>
<span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20200822/20200822125610.jpg" alt="f:id:tsgcpp:20200822125610j:plain" title="f:id:tsgcpp:20200822125610j:plain" class="hatena-fotolife" itemprop="image"></span>
</div>
<h3 id="なぜBoxingが発生したのか">なぜ<a class="keyword" href="http://d.hatena.ne.jp/keyword/Boxing">Boxing</a>が発生したのか</h3>
<p>そもそも「<b><a class="keyword" href="http://d.hatena.ne.jp/keyword/Boxing">Boxing</a>は参照型の変数に値型のデータを渡すことで発生</b>」します。</p>
<pre class="code lang-cs" data-lang="cs" data-unlink>var data = <span class="synStatement">new</span> Data { <span class="synStatement">value</span> = <span class="synConstant">0</span> };
<span class="synComment">// 参照型変数のloggerに値型を渡すためBoxingが発生</span>
ILogger logger = data
</pre>
<p>interfaceの変数もclass, <a class="keyword" href="http://d.hatena.ne.jp/keyword/delegate">delegate</a>同様参照型として扱われます。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdocs.microsoft.com%2Fen-us%2Fdotnet%2Fcsharp%2Flanguage-reference%2Fkeywords%2Freference-types" title="Reference types - C# Reference" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/reference-types">docs.microsoft.com</a></cite></p>
<p>interfaceを継承している構造体であって、参照型にするには<a class="keyword" href="http://d.hatena.ne.jp/keyword/Boxing">Boxing</a>を行って参照型に変換してやる必要があります。</p>
<p>※「なぜ<a class="keyword" href="http://d.hatena.ne.jp/keyword/Boxing">Boxing</a>が必要なのか」を後述しています。</p>
<h2 id="Generic-Interfaceによる構造体のBoxingの回避">Generic Interfaceによる構造体の<a class="keyword" href="http://d.hatena.ne.jp/keyword/Boxing">Boxing</a>の回避</h2>
<p><span style="font-size: 150%"><span style="color: #ff0000"><b>さて、いよいよ本題です! </b></span></span></p>
<p>先述の「intefaceを引数としたメソッドに渡した場合の<a class="keyword" href="http://d.hatena.ne.jp/keyword/Boxing">Boxing</a>」に限っては回避方法があります!<br />
それは<code>Func(ILogger logger)</code>メソッドを以下のように書き換えるだけです。</p>
<pre class="code lang-cs" data-lang="cs" data-unlink><span class="synType">public</span> <span class="synType">void</span> Func<T>(T logger) <span class="synStatement">where</span> T : ILogger
{
<span class="synComment">// 何もしない</span>
}
</pre>
<p>ポイントは <code>where T : ILogger</code> の部分で"<b>Generic Interface</b>"と呼ばれるものです。</p>
<div onclick="obj=document.getElementById('expand_boxing_check_as_a_arg_of_generic_interface').style; obj.display=(obj.display=='none')?'block':'none';">
<a style="cursor:pointer;">...クリックで展開:Boxing非発生の確認</a>
</div>
<div id="expand_boxing_check_as_a_arg_of_generic_interface" style="display:none;clear:both;">
<pre class="code lang-cs" data-lang="cs" data-unlink>var data = <span class="synStatement">new</span> Data { <span class="synStatement">value</span> = <span class="synConstant">0</span> };
Profiler.BeginSample(<span class="synConstant">"Profile: Data as a arg of Generic Interface"</span>);
Func(data);
Profiler.EndSample();
<span class="synType">public</span> <span class="synType">void</span> Func<T>(T logger) <span class="synStatement">where</span> T : ILogger
{
<span class="synComment">// 何もしない</span>
}
</pre>
<span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20200822/20200822131036.jpg" alt="f:id:tsgcpp:20200822131036j:plain" title="f:id:tsgcpp:20200822131036j:plain" class="hatena-fotolife" itemprop="image"></span>
</div>
<h3 id="Generic-Interfaceとは">Generic Interfaceとは</h3>
<p>Generic InterfaceはGenericの制約にinterfaceを指定したもののことです。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdocs.microsoft.com%2Fen-us%2Fdotnet%2Fcsharp%2Fprogramming-guide%2Fgenerics%2Fgeneric-interfaces" title="Generic Interfaces - C# Programming Guide" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/generics/generic-interfaces">docs.microsoft.com</a></cite></p>
<p>上記参考ページでも以下のような記述があり、Generic Interfaceを使うことで<a class="keyword" href="http://d.hatena.ne.jp/keyword/Boxing">Boxing</a>の回避が可能なことがわかります。</p>
<pre class="code" data-lang="" data-unlink>The preference for generic classes is to use generic interfaces, such as IComparable<T> rather than IComparable, in order to avoid boxing and unboxing operations on value types.</pre>
<h3 id="なぜBoxingが回避されたのか">なぜ<a class="keyword" href="http://d.hatena.ne.jp/keyword/Boxing">Boxing</a>が回避されたのか</h3>
<p>Generic型の引数は渡されたデータの型になります。</p>
<p><code>where T : ILogger</code>とinterfaceの制約が付いたとしても<code>Func<T>(T logger)</code>の<code>T</code>はあくまで引数の型、つまり構造体<code>Data</code>になります。</p>
<p>つまり、「<b>引数<code>T</code>が値型となり、参照型への変換がそもそも不要のため、<a class="keyword" href="http://d.hatena.ne.jp/keyword/Boxing">Boxing</a>も発生しなかった</b>」ということになります。</p>
<p>逆に<code>Func(ILogger logger)</code>の場合は引数は参照型で固定のため、値型を渡してしまうと<a class="keyword" href="http://d.hatena.ne.jp/keyword/Boxing">Boxing</a>が発生してしまったということだったんですね。</p>
<h3 id="Generic-InterfaceでのBoxing回避はメソッド限定">Generic Interfaceでの<a class="keyword" href="http://d.hatena.ne.jp/keyword/Boxing">Boxing</a>回避はメソッド限定 </h3>
<p><b>この回避方法はメソッドのみ限定で可能</b>です。<br />
後述していますが「メソッドのコールスタック」と「メソッドの引数」は基本的にスタック領域に置かれます。</p>
<p>Generic型に値型を指定した場合、<code>T</code>は参照型に変換されることなくそのままスタック領域の格納され、 メソッドからはその領域のデータにアクセスされます。<br />
メソッドもその引数もスタック領域に置かれているため、実現できた回避方法と言えそうです。</p>
<h3 id="余談-OpCodesConstrained-Field">余談: OpCodes.Constrained Field</h3>
<p>さらに細かく話すとGeneric Interfaceの場合、以下の特殊なオペコードが使われるようになりスタック領域のデータが参照されるようです。<br />
気になる方はどうぞ~。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdocs.microsoft.com%2Fen-us%2Fdotnet%2Fapi%2Fsystem.reflection.emit.opcodes.constrained%3Fredirectedfrom%3DMSDN%26view%3Dnetcore-3.1" title="OpCodes.Constrained Field (System.Reflection.Emit)" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://docs.microsoft.com/en-us/dotnet/api/system.reflection.emit.opcodes.constrained?redirectedfrom=MSDN&view=netcore-3.1">docs.microsoft.com</a></cite>
<iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fstackoverflow.com%2Fa%2F3033357" title="Structs, Interfaces and Boxing" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://stackoverflow.com/a/3033357">stackoverflow.com</a></cite></p>
<h2 id="余談なぜBoxingが必要なのか">余談:なぜ<a class="keyword" href="http://d.hatena.ne.jp/keyword/Boxing">Boxing</a>が必要なのか</h2>
<p><a class="keyword" href="http://d.hatena.ne.jp/keyword/Boxing">Boxing</a>は値型のデータを参照型として扱うために必要な機能と言えます(意図しない<a class="keyword" href="http://d.hatena.ne.jp/keyword/Boxing">Boxing</a>の場合は別ですが)。</p>
<p>参照型(objectなど)と値型(struct)の大きな違いとして、その「データを格納する領域」に違いがあります。</p>
<table>
<thead>
<tr>
<th> </th>
<th> データの格納場所 </th>
</tr>
</thead>
<tbody>
<tr>
<td> 値型(<a class="keyword" href="http://d.hatena.ne.jp/keyword/Value">Value</a> Type) </td>
<td> スタック領域(一時領域)、ヒープ領域(永続領域)の両方 </td>
</tr>
<tr>
<td> 参照型(Reference Type) </td>
<td> 基本的にヒープ領域(永続領域)のみ </td>
</tr>
<tr>
<td> 関数のコールタック(余談) </td>
<td> スタック領域(一時領域) </td>
</tr>
</tbody>
</table>
<p>スタック領域は一時的、ヒープ領域は永続的にデータを保持します。<br />
そして、<a class="keyword" href="http://d.hatena.ne.jp/keyword/Boxing">Boxing</a>は「一時的なスタック領域にある値型」を「ヒープ領域に移して参照型にする」ために行います。</p>
<p>コードとイメージは以下のような感じです。</p>
<pre class="code lang-cs" data-lang="cs" data-unlink><span class="synComment">// ヒープ領域に確保</span>
var loggerHolder = <span class="synStatement">new</span> LoggerHolder();
<span class="synComment">// ローカル変数のためスタック領域</span>
var data = <span class="synStatement">new</span> Data() { a = <span class="synConstant">1f</span>, b = <span class="synConstant">2f</span>, c = <span class="synConstant">3f</span>, d = <span class="synConstant">4f</span>, e = <span class="synConstant">5f</span> };
<span class="synComment">// dataのBoxingを行い、ヒープ領域確保、データをコピーして永続化</span>
loggerHolder.logger = data
</pre>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20200822/20200822142943.jpg" alt="f:id:tsgcpp:20200822142943j:plain" title="f:id:tsgcpp:20200822142943j:plain" class="hatena-fotolife" itemprop="image"></span></p>
<p>参照型はいつ参照しても期待するデータにアクセスできないといけません。<br />
interfaceの変数は<b>参照型</b>のため、構造体が対象のinterfaceを継承していたとしても必ずヒープ領域に持っていって参照型にしておく必要があります。</p>
<p>これが<a class="keyword" href="http://d.hatena.ne.jp/keyword/Boxing">Boxing</a>が発生する主理由の1つと言えます。
(他にもスタック領域は急に領域を確保できないからヒープ領域を使うなど有りますが、細かくは割愛しますw)。</p>
<p><a href="https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/types/boxing-and-unboxing">参考: Boxing and Unboxing</a></p>
<h2 id="雑感">雑感</h2>
<p>1つの<a class="keyword" href="http://d.hatena.ne.jp/keyword/GC">GC</a> Allocの回避を紹介するのに、ずいぶんと長文になってしまいました。。。</p>
<p>記事を1週間以上投稿しなかったのは、夏バテが原因だった気がします(言い訳)。</p>
<p>まあ、使命感でやってるわけではないので気長にやっていきます。</p>
<p>皆さんの<a class="keyword" href="http://d.hatena.ne.jp/keyword/C%23">C#</a>の知識向上につながれば幸いですー。
それでは~。</p>
tsgcpp
【Unity】指のポーズをブレンドしてハンドジェスチャーを作ろう
hatenablog://entry/26006613612466696
2020-08-10T19:05:02+09:00
2020-08-10T19:05:02+09:00 概要 動作環境 使用アセット ポーズの用意 ポーズの条件 用意するポーズ HandOpened ThumbClosed, FingerClosed ポーズの簡単な作り方 パーからグーを作る方法 Blender グーから一部の指をデフォルトに戻す アニメーションをExport UnityでFBXをImport 非ループ化 Animation Clipから不要なキーフレームを削除 Avatar Mask の作成 AnimatorControllerを作成 ブレンドの方針 ベースポーズ用レイヤーを作成 加算用レイヤーを作成 レイヤーの作成 BlendTreeの設定 動作確認 Additive Ref…
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20200810/20200810193210.png" alt="f:id:tsgcpp:20200810193210p:plain" title="" class="hatena-fotolife" itemprop="image"></span></p>
<ul class="table-of-contents">
<li><a href="#概要">概要</a></li>
<li><a href="#動作環境">動作環境</a></li>
<li><a href="#使用アセット">使用アセット</a></li>
<li><a href="#ポーズの用意">ポーズの用意</a><ul>
<li><a href="#ポーズの条件">ポーズの条件</a></li>
<li><a href="#用意するポーズ">用意するポーズ</a><ul>
<li><a href="#HandOpened">HandOpened</a></li>
<li><a href="#ThumbClosed-FingerClosed">ThumbClosed, FingerClosed</a></li>
</ul>
</li>
</ul>
</li>
<li><a href="#ポーズの簡単な作り方">ポーズの簡単な作り方</a><ul>
<li><a href="#パーからグーを作る方法">パーからグーを作る方法</a></li>
<li><a href="#Blender-グーから一部の指をデフォルトに戻す">Blender グーから一部の指をデフォルトに戻す</a></li>
<li><a href="#アニメーションをExport">アニメーションをExport</a></li>
</ul>
</li>
<li><a href="#UnityでFBXをImport">UnityでFBXをImport</a><ul>
<li><a href="#非ループ化">非ループ化</a></li>
<li><a href="#Animation-Clipから不要なキーフレームを削除">Animation Clipから不要なキーフレームを削除</a></li>
</ul>
</li>
<li><a href="#Avatar-Mask-の作成">Avatar Mask の作成</a></li>
<li><a href="#AnimatorControllerを作成">AnimatorControllerを作成</a><ul>
<li><a href="#ブレンドの方針">ブレンドの方針</a></li>
<li><a href="#ベースポーズ用レイヤーを作成">ベースポーズ用レイヤーを作成</a></li>
<li><a href="#加算用レイヤーを作成">加算用レイヤーを作成</a><ul>
<li><a href="#レイヤーの作成">レイヤーの作成</a></li>
<li><a href="#BlendTreeの設定">BlendTreeの設定</a></li>
</ul>
</li>
</ul>
</li>
<li><a href="#動作確認">動作確認</a></li>
<li><a href="#Additive-Reference-Pose-について">Additive Reference Pose について</a><ul>
<li><a href="#Additive-Reference-Pose-とは">Additive Reference Pose とは</a></li>
<li><a href="#デフォルトのAdditive-Reference-Pose">デフォルトのAdditive Reference Pose</a></li>
<li><a href="#余談Additive-Reference-Pose-を明示的に指定する方法">余談:Additive Reference Pose を明示的に指定する方法</a><ul>
<li><a href="#FBXのImport時に指定">FBXのImport時に指定</a></li>
<li><a href="#Debug-Inspectorで指定">Debug Inspectorで指定</a></li>
<li><a href="#AnimationUtilitySetAdditiveReferencePoseを使って指定">AnimationUtility.SetAdditiveReferencePoseを使って指定</a></li>
<li><a href="#ツールの注意点">ツールの注意点</a></li>
</ul>
</li>
</ul>
</li>
<li><a href="#サンプルプロジェクト">サンプルプロジェクト</a></li>
<li><a href="#雑感">雑感</a></li>
</ul>
<h2 id="概要">概要</h2>
<p>今回は手の指を曲げるアニメーションを組み合わせてハンド<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B8%A5%A7%A5%B9%A5%C1%A5%E3%A1%BC">ジェスチャー</a>の作ってみたいと思います。</p>
<p>UnityにはBlendTreeというアニメーションを<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D6%A5%EC%A5%F3%A5%C9">ブレンド</a>する機能があります。<br />
今回はそのBlendTreeと指のアニメーションを組み合わせることでハンド<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B8%A5%A7%A5%B9%A5%C1%A5%E3%A1%BC">ジェスチャー</a>を実現したいと思います。</p>
<h2 id="動作環境">動作環境</h2>
<ul>
<li>Unity 2020.1.1f1</li>
<li><a class="keyword" href="http://d.hatena.ne.jp/keyword/Blender">Blender</a> 2.83</li>
</ul>
<p>動作確認可能なプロジェクトを記事の最後に記載します。</p>
<h2 id="使用アセット">使用アセット</h2>
<ul>
<li><a href="https://assetstore.unity.com/packages/3d/characters/unity-chan-model-18705">Unity-Chan! Model</a></li>
</ul>
<h2 id="ポーズの用意">ポーズの用意</h2>
<p><a class="keyword" href="http://d.hatena.ne.jp/keyword/Blender">Blender</a>でモデルを読み込んで、以下のポーズ(<a class="keyword" href="http://d.hatena.ne.jp/keyword/Blender">Blender</a>ではActions)を作成。</p>
<p><b><span style="color: #ff0000">1ポーズに左右の手両方のキーフレーム登録をオススメします。</span></b>。<br />
後述する<a class="keyword" href="http://d.hatena.ne.jp/keyword/Avatar">Avatar</a> Maskで左右の手のアニメーションを分離できます。</p>
<p>※ポーズの作り方については後述</p>
<h3 id="ポーズの条件">ポーズの条件</h3>
<ul>
<li><b>0フレーム目は デフォルトの状態</b>
<ul>
<li>0フレーム目は必ずデフォルト(曲げる前)の状態のキーフレームを登録すること</li>
</ul>
</li>
<li>1フレーム目は 対象の指を曲げた状態
<ul>
<li>例えば "IndexFingerClosed" であれば、人差し指のみ曲げた状態</li>
</ul>
</li>
</ul>
<h3 id="用意するポーズ">用意するポーズ</h3>
<ul>
<li>HandOpened (いわゆるパーのポーズ、デフォルトのポーズ)</li>
<li>ThumbClosed (親指のみ閉じたポーズ)</li>
<li>IndexFingerClosed (人差し指のみ閉じたポーズ)</li>
<li>MiddleFingerClosed (中指のみ閉じたポーズ)</li>
<li>RingFingerClosed (薬指のみ閉じたポーズ)</li>
<li>PinkyFingerClosed (子指のみ閉じたポーズ)</li>
</ul>
<h4 id="HandOpened">HandOpened</h4>
<p>手がパーのポーズ、いわゆるデフォルトポーズ</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20200810/20200810155530.png" alt="f:id:tsgcpp:20200810155530p:plain" title="" class="hatena-fotolife" itemprop="image"></span></p>
<h4 id="ThumbClosed-FingerClosed">ThumbClosed, FingerClosed</h4>
<p>5本の指それぞれを個別に閉じている状態のポーズを用意。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20200810/20200810155651.png" alt="f:id:tsgcpp:20200810155651p:plain" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p>※親指、中指、薬指、小指は省略</p>
<h2 id="ポーズの簡単な作り方">ポーズの簡単な作り方</h2>
<p><span style="color: #ff0000">※<a class="keyword" href="http://d.hatena.ne.jp/keyword/Blender">Blender</a>をある程度触ったことがある人向けです。</span></p>
<p>以下の流れで、それぞれの指のポーズを作成できます。</p>
<ul>
<li>初めにグーのポーズを作る</li>
<li>グーのポーズから対象の指以外をデフォルトに戻す</li>
</ul>
<p>グーから作ることによって、すべての指のポーズを<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D6%A5%EC%A5%F3%A5%C9">ブレンド</a>した時にきれいなグーになると思います。</p>
<h3 id="パーからグーを作る方法">パーからグーを作る方法</h3>
<div onclick="obj=document.getElementById('expand_blender01').style; obj.display=(obj.display=='none')?'block':'none';">
<a style="cursor:pointer;">...クリックで展開</a>
</div>
<div id="expand_blender01" style="display:none;clear:both;">
「orientationを"Local"」に、「Pivotを"Indivisual Origins」にすると簡単にグーにできると思います。<br>
あとは指すべてを選択してキーを登録し、さらに1秒後にも複製(Shift + D)すれば完了です。<br>
<iframe width="420" height="315" src="https://www.youtube.com/embed/pJ-aOlsD6xM?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe><cite class="hatena-citation"><a href="https://www.youtube.com/watch?v=pJ-aOlsD6xM">www.youtube.com</a></cite>
</div>
<h3 id="Blender-グーから一部の指をデフォルトに戻す"><a class="keyword" href="http://d.hatena.ne.jp/keyword/Blender">Blender</a> グーから一部の指をデフォルトに戻す</h3>
<div onclick="obj=document.getElementById('expand_blender02').style; obj.display=(obj.display=='none')?'block':'none';">
<a style="cursor:pointer;">...クリックで展開</a>
</div>
<div id="expand_blender02" style="display:none;clear:both;">
グーを先に作ったのは「5本の指それぞれに閉じている状態のポーズ」を用意するためです。<br>
対象の指を選択して Ctrl + I で選択を反転できます。 <br>
あとは Alt + R で選択されているポーズを解除できます(選択されていない指が閉じたままになる)。 <br>
<iframe width="420" height="315" src="https://www.youtube.com/embed/DBHFD23iRnY?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe><cite class="hatena-citation"><a href="https://www.youtube.com/watch?v=DBHFD23iRnY">www.youtube.com</a></cite>
</div>
<h3 id="アニメーションをExport">アニメーションをExport</h3>
<p>以下の手順で<a class="keyword" href="http://d.hatena.ne.jp/keyword/Blender">Blender</a>からFbxを算出</p>
<ul>
<li>Export対象のモデルを選択</li>
<li>File -> Export -> FBX(.fbx)</li>
<li>"Selected Objects"を有効にし、"Object Types"を"Armature"のみする
<ul>
<li>今回はス<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B1%A5%EB%A5%C8">ケルト</a>ンポーズ(アニメーション)のみを出力したいため、メッシュは除外</li>
</ul>
</li>
<li>Export FBX</li>
</ul>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20200810/20200810135627.jpg" alt="f:id:tsgcpp:20200810135627j:plain" title="" class="hatena-fotolife" itemprop="image"></span></p>
<h2 id="UnityでFBXをImport">UnityでFBXをImport</h2>
<p>HumanoidとしてImportして、AnimationをHumanoid向けに変換しましょう。</p>
<ul>
<li>"Animation Type" を "Humanoid"に変更</li>
<li>"Apply"</li>
</ul>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20200810/20200810143446.jpg" alt="f:id:tsgcpp:20200810143446j:plain" title="" class="hatena-fotolife" itemprop="image"></span></p>
<h3 id="非ループ化">非ループ化</h3>
<p>Loop Timeなどは無効のままにしてください。<br />
<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D6%A5%EC%A5%F3%A5%C9">ブレンド</a>中におかしくなってしまいます。</p>
<h3 id="Animation-Clipから不要なキーフレームを削除">Animation Clipから不要なキーフレームを削除</h3>
<p>必須ではありませんが、HumanoidでImportするとすべてのMuscleのキーフレームが登録されてしまいます。<br />
Animation Clipのサイズが削減できるので不要なキーフレームは削除しておきましょう。</p>
<ul>
<li>HandOpened は指以外のキーフレームを削除</li>
<li>各FingerClosedは対象の指以外のキーフレームを削除</li>
</ul>
<p><figure class="figure-image figure-image-fotolife" title="HandOpened のキーフレーム(修正後)"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20200810/20200810174348.jpg" alt="f:id:tsgcpp:20200810174348j:plain" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>HandOpened のキーフレーム(修正後)</figcaption></figure></p>
<p><figure class="figure-image figure-image-fotolife" title="IndexFingerClosed のキーフレーム(修正後)"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20200810/20200810174438.jpg" alt="f:id:tsgcpp:20200810174438j:plain" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>IndexFingerClosed のキーフレーム(修正後)</figcaption></figure></p>
<h2 id="Avatar-Mask-の作成"><a class="keyword" href="http://d.hatena.ne.jp/keyword/Avatar">Avatar</a> Mask の作成</h2>
<p>Unityでは<a class="keyword" href="http://d.hatena.ne.jp/keyword/Avatar">Avatar</a> Maskを使ってアニメーションの適用範囲をマスクで制限することができます。</p>
<ul>
<li>"Project"上で右クリック -> "Create" -> "<a class="keyword" href="http://d.hatena.ne.jp/keyword/Avatar">Avatar</a> Mask"</li>
<li><b>アニメーションはループさせない(Loop Time <a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C1%A5%A7%A5%C3%A5%AF%A5%DC%A5%C3%A5%AF%A5%B9">チェックボックス</a>を オフ)</b>
<ul>
<li>プレイ中に大変なことになります</li>
</ul>
</li>
<li>左右の手それぞれの <a class="keyword" href="http://d.hatena.ne.jp/keyword/Avatar">Avatar</a> Mask を作成</li>
</ul>
<p><figure class="figure-image figure-image-fotolife" title="左手用 Avatar Mask"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20200810/20200810144122.jpg" alt="f:id:tsgcpp:20200810144122j:plain" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>左手用 <a class="keyword" href="http://d.hatena.ne.jp/keyword/Avatar">Avatar</a> Mask</figcaption></figure>
<figure class="figure-image figure-image-fotolife" title="右手用 Avatar Mask"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20200810/20200810144148.jpg" alt="f:id:tsgcpp:20200810144148j:plain" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>右手用 <a class="keyword" href="http://d.hatena.ne.jp/keyword/Avatar">Avatar</a> Mask</figcaption></figure></p>
<h2 id="AnimatorControllerを作成">AnimatorControllerを作成</h2>
<p>前置きが長かったですがいよいよ本題です。<br />
これまで用意したAnimation(ポーズ)を<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D6%A5%EC%A5%F3%A5%C9">ブレンド</a>するAnimationControllerを用意して、ハンド<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B8%A5%A7%A5%B9%A5%C1%A5%E3%A1%BC">ジェスチャー</a>を作っていきます。</p>
<h3 id="ブレンドの方針"><a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D6%A5%EC%A5%F3%A5%C9">ブレンド</a>の方針</h3>
<ul>
<li>AnimatorControllerのレイヤーを使用</li>
<li>"Blending" を "Additive" にし、ベースポーズからの加算によってポーズの組み合わせを実現</li>
</ul>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20200810/20200810150053.jpg" alt="f:id:tsgcpp:20200810150053j:plain" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p>簡単に言えば"パー" から"指を曲げるポーズ"を加算することで、<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B8%A5%A7%A5%B9%A5%C1%A5%E3%A1%BC">ジェスチャー</a>を実現する方針になります。</p>
<h3 id="ベースポーズ用レイヤーを作成">ベースポーズ用レイヤーを作成</h3>
<ul>
<li>レイヤーを追加</li>
<li>Weightを1, Maskに左手用<a class="keyword" href="http://d.hatena.ne.jp/keyword/Avatar">Avatar</a> Mask, BlendingをOverrideに設定</li>
<li>DefaultStateのアニメーションに "HandOpened" を指定</li>
</ul>
<p>これでBase Layerの左手のアニメーションが常にパーに上書きされる形になります。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20200810/20200810150734.jpg" alt="f:id:tsgcpp:20200810150734j:plain" title="" class="hatena-fotolife" itemprop="image"></span></p>
<h3 id="加算用レイヤーを作成">加算用レイヤーを作成</h3>
<h4 id="レイヤーの作成">レイヤーの作成</h4>
<ul>
<li>レイヤーを追加</li>
<li>Weightを1, Maskに左手用<a class="keyword" href="http://d.hatena.ne.jp/keyword/Avatar">Avatar</a> Mask, Blendingを<span style="color: #ff0000"><b>Additive</b></span>に設定</li>
<li>DefaultStateにBlendTreeを指定</li>
</ul>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20200810/20200810151509.jpg" alt="f:id:tsgcpp:20200810151509j:plain" title="" class="hatena-fotolife" itemprop="image"></span></p>
<h4 id="BlendTreeの設定">BlendTreeの設定</h4>
<ul>
<li>Parametersに<code>CloseThumb_L</code>, <code>CloseIndex_L</code>... の様に左手の指ごとのパラメータをfloatで用意
<ul>
<li>右手の指の場合は<code>CloseThumb_R</code>, <code>CloseIndex_R</code>...</li>
</ul>
</li>
<li>"BlendType" に "Direct" を指定</li>
<li>Motionを5つ追加し、各指のアニメーションとパラメータを指定</li>
</ul>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20200810/20200810152457.jpg" alt="f:id:tsgcpp:20200810152457j:plain" title="" class="hatena-fotolife" itemprop="image"></span></p>
<h2 id="動作確認">動作確認</h2>
<p>それでは作ったAnimatorControllerをユニティちゃんのAnimatorに入れて動作確認してみましょう!</p>
<p><iframe width="420" height="315" src="https://www.youtube.com/embed/Wm2pFhy4ghQ?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe><cite class="hatena-citation"><a href="https://www.youtube.com/watch?v=Wm2pFhy4ghQ">www.youtube.com</a></cite></p>
<p>うまくいきました!</p>
<h2 id="Additive-Reference-Pose-について">Additive Reference Pose について</h2>
<p><a class="keyword" href="http://d.hatena.ne.jp/keyword/Blender">Blender</a>でアニメーションを作る際、「0フレーム目は デフォルトの状態」という条件を加えました。
実はこれ、"Additive Reference Pose"というものが関係しています。</p>
<h3 id="Additive-Reference-Pose-とは">Additive Reference Pose とは</h3>
<p>平たく言えば「<b>Additive(レイヤー)で使用する際にベースとなるポーズ</b>」のことです。</p>
<p>残念ながら"Additive Reference Pose"の明確なドキュメントは見つかりませんでした。。。(知っている方いらっしゃいましたら教えてください!!!)</p>
<h3 id="デフォルトのAdditive-Reference-Pose">デフォルトのAdditive Reference Pose</h3>
<p><a href="https://docs.unity3d.com/ScriptReference/AnimationUtility.SetAdditiveReferencePose.html">AnimationUtility.SetAdditiveReferencePose</a>のページにこんな説明があります。</p>
<pre class="code" data-lang="" data-unlink>By default any animation clip used in an additive layer use the pose at time 0 to define the reference pose</pre>
<p>つまり、"Additive Reference Pose"はデフォルトではそのAnimation Clipの0フレーム目のポーズになるということです。</p>
<p>これが 「0フレーム目は デフォルトの状態」という条件にした最大の理由になります。</p>
<h3 id="余談Additive-Reference-Pose-を明示的に指定する方法">余談:Additive Reference Pose を明示的に指定する方法</h3>
<p>一応、Additive Reference Pose は明示的に指定する方法があります。</p>
<p>ただ、どれも扱いやすいとは言えないと個人的には思います。。。<br />
<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%D6%A5%EC%A5%F3%A5%C9">ブレンド</a>前提のAnimation Clip は素直にベースのポーズを0フレーム目に指定したほうが良いと思います。</p>
<h4 id="FBXのImport時に指定">FBXのImport時に指定</h4>
<p>以下の方法を使用すると、同一Animation Clip内で0フレーム目以外を"Additive Reference Pose"として指定できます。</p>
<ul>
<li>FBXをInspectorで確認し、"Animation"タブを開く</li>
<li>下部に"Additive Reference Pose" の<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C1%A5%A7%A5%C3%A5%AF%A5%DC%A5%C3%A5%AF%A5%B9">チェックボックス</a>をオンにする</li>
<li>Pose Frameを指定</li>
</ul>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20200810/20200810165228.jpg" alt="f:id:tsgcpp:20200810165228j:plain" title="" class="hatena-fotolife" itemprop="image"></span></p>
<h4 id="Debug-Inspectorで指定">Debug Inspectorで指定</h4>
<p>実はDebug Inspectorにすると、Animation ClipのInspectorに"Additive Reference Pose"を指定する欄が表示されます。</p>
<ul>
<li>InspectorをDebug化</li>
<li>"Additive Reference Pose Clip" と "Additive Reference Pose Time" を指定</li>
<li><b>"Has Additive Reference Pose"の<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%C1%A5%A7%A5%C3%A5%AF%A5%DC%A5%C3%A5%AF%A5%B9">チェックボックス</a>をオン</b>
<ul>
<li>オンにしていないと"Additive Reference Pose"が有効になりません</li>
</ul>
</li>
</ul>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20200810/20200810170930.jpg" alt="f:id:tsgcpp:20200810170930j:plain" title="" class="hatena-fotolife" itemprop="image"></span></p>
<h4 id="AnimationUtilitySetAdditiveReferencePoseを使って指定">AnimationUtility.SetAdditiveReferencePoseを使って指定</h4>
<p>※個人的には非推奨</p>
<p><a href="https://docs.unity3d.com/ScriptReference/AnimationUtility.SetAdditiveReferencePose.html">AnimationUtility.SetAdditiveReferencePose</a> を使うことで指定できます。<br />
別のAnimation Clipも指定可能です。</p>
<pre class="code lang-cs" data-lang="cs" data-unlink>AnimationUtility.SetAdditiveReferencePose(targetClip, referenceClip, poseTime);
</pre>
<p>メソッド呼び出しの結果を確認したい場合は、前述したDebug Inspectorで確認して下さい。</p>
<p>また、以下のようなツールも作ってみました(プロジェクトにいれています)。<br />
複数のAnimationClipでまとめて"Additive Reference Pose"を指定できます。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20200810/20200810165859.jpg" alt="f:id:tsgcpp:20200810165859j:plain" title="" class="hatena-fotolife" itemprop="image"></span></p>
<h4 id="ツールの注意点">ツールの注意点</h4>
<p>Set/Reset を繰り返すと以下のようなエラーを吐くことがあります。<br />
こうなるとUnityを再起動しないとうまく動作しません。。。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20200810/20200810170254.jpg" alt="f:id:tsgcpp:20200810170254j:plain" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p>原因不明なので何か知っている人は良かったら教えてください。</p>
<h2 id="サンプルプロジェクト">サンプルプロジェクト</h2>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Ftsgcpp%2FBlendTreeAdditiveSample-Unity" title="tsgcpp/BlendTreeAdditiveSample-Unity" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://github.com/tsgcpp/BlendTreeAdditiveSample-Unity">github.com</a></cite></p>
<h2 id="雑感">雑感</h2>
<p>今回は<a class="keyword" href="http://d.hatena.ne.jp/keyword/Blender">Blender</a>, Animator, Animation Clip, Additive Reference Poseと詰め込みすぎ感がありますねw。<br />
有料アセット使えばUnity内で完結できたと思うんですが、無料でできるようにしたかったので<a class="keyword" href="http://d.hatena.ne.jp/keyword/Blender">Blender</a>も入れてみました。</p>
<p>今回の記事の経緯は手のアニメーション作ってるときに「あれ、これアニメーションの組み合わせでできんじゃね?」と思ったことですね。</p>
<p>Animationの用意やAnimatorの調整など、面倒な部分もありますがモデルに適したAnimation Clipを使えることがメリットでしょうか。<br />
AnimatorControllerOverrideを使えばモデルごとにAnimationClipを設定することも可能です。</p>
<p>"Additive Reference Pose"についてはいいドキュメントが見つからなかったので調べるのがちょっと大変でした。</p>
<p>今のところ手の<a class="keyword" href="http://d.hatena.ne.jp/keyword/%A5%B8%A5%A7%A5%B9%A5%C1%A5%E3%A1%BC">ジェスチャー</a>以外にいい組み合わせは思いつかないですが、
何かよさげな応用例があれば教えてください!</p>
<p>それでは~。</p>
tsgcpp
【UniRx】UniRxのDelayFrameが初回だけ1フレーム余計に遅延する問題と解決方法
hatenablog://entry/26006613610467090
2020-08-07T21:12:25+09:00
2020-08-07T21:12:25+09:00 概要 UniRxについて DelayFrameの問題について 現象の確認方法 問題の原因 簡単に言うと 詳細に言うと バグではないの? 制限事項とする理由 余談:修正できないの? 問題の回避 方法1. MainThreadDispatcher.Initialize()をコール 方法2. Scheduler.SetDefaultForUnity()をコール 方法3. Scheduler, Dispatcherが初期化されるPropertyを呼び出し 回避策の結果 余談2:起動したコルーチンはどうなるの? 雑感 概要 UniRxでストリームを遅延させるDelayFrameを使用した場合に初回だけO…
<ul class="table-of-contents">
<li><a href="#概要">概要</a></li>
<li><a href="#UniRxについて">UniRxについて</a></li>
<li><a href="#DelayFrameの問題について">DelayFrameの問題について</a><ul>
<li><a href="#現象の確認方法">現象の確認方法</a></li>
</ul>
</li>
<li><a href="#問題の原因">問題の原因</a><ul>
<li><a href="#簡単に言うと">簡単に言うと</a></li>
<li><a href="#詳細に言うと">詳細に言うと</a></li>
</ul>
</li>
<li><a href="#バグではないの">バグではないの?</a><ul>
<li><a href="#制限事項とする理由">制限事項とする理由</a></li>
<li><a href="#余談修正できないの">余談:修正できないの?</a></li>
</ul>
</li>
<li><a href="#問題の回避">問題の回避</a><ul>
<li><a href="#方法1-MainThreadDispatcherInitializeをコール">方法1. MainThreadDispatcher.Initialize()をコール</a></li>
<li><a href="#方法2-SchedulerSetDefaultForUnityをコール">方法2. Scheduler.SetDefaultForUnity()をコール</a></li>
<li><a href="#方法3-Scheduler-Dispatcherが初期化されるPropertyを呼び出し">方法3. Scheduler, Dispatcherが初期化されるPropertyを呼び出し</a></li>
<li><a href="#回避策の結果">回避策の結果</a></li>
</ul>
</li>
<li><a href="#余談2起動したコルーチンはどうなるの">余談2:起動したコルーチンはどうなるの?</a></li>
<li><a href="#雑感">雑感</a></li>
</ul>
<h2 id="概要">概要</h2>
<p>UniRxでストリームを遅延させる<code>DelayFrame</code>を使用した場合に初回だけ<code>OnNext</code>が1フレーム余計に遅延する現象がありました。<br />
今回はその現象と解決方法を紹介したいと思います。</p>
<h2 id="UniRxについて">UniRxについて</h2>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fneuecc%2FUniRx" title="neuecc/UniRx" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://github.com/neuecc/UniRx">github.com</a></cite></p>
<p>Unityで使用可能なReactive Extensionsライブラリ。</p>
<p>Reactive Extensionsについては今回は詳しく触れませんが、<br />
独特のクセがあり慣れない人には最初戸惑うと思いますが、使えるといろいろと捗ります!!!</p>
<h2 id="DelayFrameの問題について">DelayFrameの問題について</h2>
<p>現象を詳しく説明すると</p>
<ul>
<li><code>Subject.DelayFrame(N)</code>によりNフレーム遅延するSubject(Observable)を生成</li>
<li>上記SubjectをSubscribeする</li>
<li><code>Subject.OnNext(...)</code>により値を流す</li>
<li>初回の<code>OnNext</code>のみ「N+1」フレーム遅延して値が流れてくる
<ul>
<li>2回目以降の<code>OnNext</code>は指定通り「N」フレーム遅延</li>
</ul>
</li>
</ul>
<h3 id="現象の確認方法">現象の確認方法</h3>
<p>以下のコードを使用してSubscribe, OnNextを意図的に発生させて確認</p>
<div onclick="obj=document.getElementById('expand_unittest').style; obj.display=(obj.display=='none')?'block':'none';">
<a style="cursor:pointer;">...クリックでコードを展開</a>
</div>
<div id="expand_unittest" style="display:none;clear:both;">
Aで`Subscribe`, Z, Xで`OnNext`
<pre class="code lang-cs" data-lang="cs" data-unlink><span class="synStatement">using</span> System;
<span class="synStatement">using</span> UnityEngine;
<span class="synStatement">using</span> UniRx;
<span class="synType">public</span> <span class="synType">class</span> DelayFrameTest : MonoBehaviour
{
<span class="synType">private</span> Subject<<span class="synType">string</span>> subject = <span class="synStatement">new</span> Subject<<span class="synType">string</span>>();
<span class="synType">private</span> IObservable<<span class="synType">string</span>> observable;
<span class="synType">private</span> IDisposable disposable;
<span class="synType">void</span> Start()
{
observable = subject.DelayFrame(<span class="synConstant">1</span>);
}
<span class="synType">void</span> Update()
{
<span class="synStatement">if</span> (Input.GetKeyDown(KeyCode.A)) {
Debug.Log(Time.frameCount + <span class="synConstant">": Subscribe Start"</span>);
disposable = observable.Subscribe(s => {
Debug.Log(Time.frameCount + <span class="synConstant">" onNext: "</span> + s);
});
Debug.Log(Time.frameCount + <span class="synConstant">": Subscribe End"</span>);
}
<span class="synStatement">if</span> (Input.GetKeyDown(KeyCode.Z)) {
Debug.Log(Time.frameCount + <span class="synConstant">": Inputed Z Start"</span>);
subject.OnNext(<span class="synConstant">"Input Z"</span>);
Debug.Log(Time.frameCount + <span class="synConstant">": Inputed Z End"</span>);
}
}
}
</pre>
</div>
<p><code>DelayFrame(1)</code>で1フレーム遅延を指定しています。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20200806/20200806215443.jpg" alt="f:id:tsgcpp:20200806215443j:plain" title="f:id:tsgcpp:20200806215443j:plain" class="hatena-fotolife" itemprop="image"></span></p>
<p>上記コードをA -> Z -> Z と入力した時の結果です。<br />
Z入力が1021、OnNextがコールされたのが1023 と2フレームの差がついています。</p>
<h2 id="問題の原因">問題の原因</h2>
<h3 id="簡単に言うと">簡単に言うと</h3>
<ul>
<li>UniRxは裏側でUniRx専用のDispatcher(スレッドの処理を管理するオブジェクト)を使用</li>
<li>そのDispatcher生成は遅延処理などが必要になったときに実施</li>
<li>Dispatcherの仕事は生成したフレームの次フレームから行われる</li>
</ul>
<p>簡単に言えばこのような動きになるため、初回のみ1フレームずれます</p>
<h3 id="詳細に言うと">詳細に言うと</h3>
<ul>
<li>初めて<code>DelayFrame</code>などの遅延処理が発生した時に<code>MainThreadDispatcher</code>というオブジェクトが生成される
<ul>
<li>Hierarchyで確認可能(DontDestroyOnLoad以下に存在)</li>
<li>アプリケーション終了まで存続</li>
</ul>
</li>
<li><code>DelayFrame</code>などの遅延処理が初めて発生した時に<code>MainThreadDispatcher.StartCoroutine</code>がコールされる
<ul>
<li>UniRxのDispatcherは<code>MonoBehaviour</code>のコルーチンを使用して実現している</li>
</ul>
</li>
<li>コルーチンは<code>while(true)</code>と<code>yield return null</code>で<code>Run()</code>のループ処理を行う
<ul>
<li><code>Run()</code>内で遅延処理、および遅延後の<code>OnNext</code>をコールする</li>
<li><code>yield return null</code> -> <code>Run()</code> の順番でループする(コードは <a href="https://github.com/neuecc/UniRx/blob/89b406c6e88c964507b1159111aa6d357e2b7d0f/Assets/Plugins/UniRx/Scripts/UnityEngineBridge/MainThreadDispatcher.cs#L534">こちら</a>)</li>
</ul>
</li>
</ul>
<p><code>StartCoroutine</code>した直後に<code>yield return null</code>があり、<br />
次のフレームから遅延処理が始まるため、<br />
初回のみ1フレーム遅れる現象が起きます。</p>
<h2 id="バグではないの">バグではないの?</h2>
<p>バグではあると思います。</p>
<p>ですが、個人的には<br />
<b> 「制限事項 」としてとらえたほうが良い</b>と思います。</p>
<h3 id="制限事項とする理由">制限事項とする理由</h3>
<p>これはUniRxなりの気遣いの結果、生まれてしまった問題だと思ったからです。<br />
コードを読むと「遅延処理を必要としないならDispatcherは生成しない」という設計が見て取れます。</p>
<p>また、「初回だけなら気にしない」もしくは「1フレームぐらいずれてもいい」という場合にも問題にはなりません。</p>
<p>そして、後述しますが問題の回避が簡単です。</p>
<h3 id="余談修正できないの">余談:修正できないの?</h3>
<p>難しいと思います。</p>
<p>というのもUniRxという使われる側からすると初回の<code>DelayFrame</code>などが「どこからコールされるかわからない」からです。</p>
<p>コルーチンを使っている関係で、<code>FixedUpdate</code>, <code>Update</code>, <code>LateUPdate</code>など、
どこから呼ばれるかによって初回の処理のみ差異が生まれてしまいます。</p>
<p>だったら初回のみ特別扱いせず、1フレーム後から<code>Run()</code>を開始したほうがよほど合理的だと思います。</p>
<h2 id="問題の回避">問題の回避</h2>
<p>この問題、結構簡単に解決できます。</p>
<p>Dispatcherの生成を真っ先に行えば良いだけです。<br />
<b>以下に紹介する<code>MonoBehaviour</code>をオブジェクトにアタッチして初期シーンに配置すれば問題を回避できます。</b></p>
<h3 id="方法1-MainThreadDispatcherInitializeをコール">方法1. <code>MainThreadDispatcher.Initialize()</code>をコール</h3>
<p>見た目の通りMainThreadのDispatcherを初期化します。<br />
コール時に<code>StartCoroutine</code>も実施されるため、<code>DelayFrame</code>する前に初期化を完了できます。</p>
<pre class="code lang-cs" data-lang="cs" data-unlink><span class="synStatement">using</span> UnityEngine;
<span class="synStatement">using</span> UniRx;
<span class="synType">public</span> <span class="synType">class</span> UniRxThreadStarter : MonoBehaviour
{
<span class="synType">void</span> Start()
{
MainThreadDispatcher.Initialize();
}
}
</pre>
<h3 id="方法2-SchedulerSetDefaultForUnityをコール">方法2. <code>Scheduler.SetDefaultForUnity()</code>をコール</h3>
<p>※個人的には非推奨</p>
<p>これも結果としてMainThreadのDispatcherを初期化することになります。<br />
こちらはもっと広い範囲でスレッド周りの初期化を行います。<br />
主にUnitTestで使われているようです。</p>
<p>ただ、注意点として<a class="keyword" href="http://d.hatena.ne.jp/keyword/WebGL">WebGL</a>だとおかしくなるかもしれません。<br />
<a href="https://github.com/neuecc/UniRx/blob/aa3b6d3e30354c25f5ee276d19be4f5ff7d7a82c/Assets/Plugins/UniRx/Scripts/Schedulers/Scheduler.cs#L77">こちら</a>のような記述があるのですが、上記メソッドだとそれに反するスレッドが使用されてしまいます。</p>
<pre class="code lang-cs" data-lang="cs" data-unlink><span class="synStatement">using</span> UnityEngine;
<span class="synStatement">using</span> UniRx;
<span class="synType">public</span> <span class="synType">class</span> UniRxThreadStarter : MonoBehaviour
{
<span class="synType">void</span> Start()
{
Scheduler.SetDefaultForUnity();
}
}
</pre>
<h3 id="方法3-Scheduler-Dispatcherが初期化されるPropertyを呼び出し">方法3. Scheduler, Dispatcherが初期化されるPropertyを呼び出し</h3>
<p>以下のPropertyは初回呼び出し時にScheduler, Dispatcherが初期化されるようになっています。<br />
予めいろいろな初期化を済ませておきたい人にはオススメのやり方かなと思います。</p>
<pre class="code lang-cs" data-lang="cs" data-unlink><span class="synStatement">using</span> UnityEngine;
<span class="synStatement">using</span> UniRx;
<span class="synType">public</span> <span class="synType">class</span> UniRxThreadStarter : MonoBehaviour
{
<span class="synType">void</span> Start()
{
<span class="synComment">// Propertyの初期呼び出しで初期化が実施される</span>
var immediate = Scheduler.Immediate;
var currentThread = Scheduler.CurrentThread;
var mainThread = Scheduler.MainThread;
var threadPool = Scheduler.ThreadPool;
}
}
</pre>
<h3 id="回避策の結果">回避策の結果</h3>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsgcpp/20200807/20200807210550.jpg" alt="f:id:tsgcpp:20200807210550j:plain" title="f:id:tsgcpp:20200807210550j:plain" class="hatena-fotolife" itemprop="image"></span></p>
<p>初回の<code>OnNext</code>も2回目以降と同様に指定通りの遅延になりました。</p>
<h2 id="余談2起動したコルーチンはどうなるの">余談2:起動したコルーチンはどうなるの?</h2>
<p>少なくともUniRx v7.1.0 時点ではアプリケーションが終了するまで残り続けます。<br />
<code>StopCoroutine</code>などコルーチンを止める処理も見当たりませんでした。</p>
<p>再起動がないなら割り切ってさっさと起動させてしまうのも手かなと思います。</p>
<h2 id="雑感">雑感</h2>
<p>今回はUniRxを取り上げてみました。</p>
<p>個人的には厳密なフレーム単位の遅延を考えるのが好きだったりするので、今回の現象は興味深くもありすごく気になる部分でもありました。</p>
<p>初期化処理はなんだかんだ特殊な処理が生まれるので、<br />
UniRxにも諸々の初期化を実施してくれるメソッドがあってもいいかなと思ったりしました。</p>
<p>何にしろこれからもお世話になるライブラリかなと思います!(最近更新されてないのが気になりますが)</p>
<p>それでは~</p>
tsgcpp