【Extenject】Composite Installer を紹介!
- 概要
- Compsote Installer について
- Compsote Installer の使い方
- Composite Installer 開発小話
- Composite Installerを試したい場合
- クレジット
- 雑感
概要
今回は Extenject に追加された Compsote Installer について紹介します!
ちなみにPullRequestを出したのは私です(欲しかったので)。
まだComposite Installerが同梱されているリリースバージョンはありませんが、 せっかくなので活用方法と、ついでにマージされるまでの小話について記事にしようと思います。
記事の最後にunitypackageとタグへのリンクを記載していますので、使ってみたいと思った方はご利用ください!
今回の記事は Extenject にある程度慣れている方向けの内容です。
Compsote Installer について
Composite Installer とは Compositeパターンを使った Extenject のInstaller です。
Compositeパターン
Extenject観点で説明しますと、
Composite InstallerをDIContainerに登録するだけで、そのComposite Installerに登録されているすべてのInstallerで InstallBindings()
が実施されます。
また、Composite Installer に 別のComposite Installer を登録することも可能です。
Compositeパターンはメジャーなデザインパターンのため、解説記事もたくさんあります。
詳しくは検索してみてください。
Compsote Installer の活用術
1. 再利用可能なInstallerグループを作成可能
Compositeパターンの通り複数のInstallerを1つのComposite Installerにグループとして集約することで、再利用性のあるInstallerとしての管理が可能になります。
- 機能ごとにInstallerのグループとして管理
- 機能ごとにComposite Installerを作成しシーンごとに取捨選択して利用
Extenjectを利用してかつ Pure C# が増えてくると比例してInstallerクラスも増えてくると思いますが、
そんなときに目的や機能ごとにComposite InstallerにInstallerグループを分割登録して管理すると良いと思います!
2. 疎結合Installer, 抽象Installerとして活用
特定のInstallerの前にComposite Installerをレイヤーとして設けることで、ContextとInstaller間を疎結合化することができます。
- 特定のInstallerに依存しない形で各Contextに登録可能
- ContextのPrefabを編集することなく登録Installer群を変更可能
特にComposite Scriptable Object Installerはオススメです!
ContextはアセットをGUID経由で取り込む形になるため、
特定の機能にInstallerを追加したい場合はContextを編集せずComposite Scriptable Object Installerアセットに新しいInstallerを登録するだけで済みます。
3. 特定の機能提供向けInstallerとして配布
複数のチームでExtenjectを利用している場合の連携に活用できると思います。
- 特定の機能を集約したComposite Installerをパッケージに含めて共有可能
- Composite Installerへの修正やInstaller追加もパッケージの更新のみで配布先に反映することが可能
Compsote Installer の使い方
以下にドキュメントページがあります。
こちらも私の方で作成して一緒にPullRequestに出しました(ガバガバな英語で恐縮ですが)。
せっかくなので、簡単にではありますが日本語でも使い方を紹介します。
CompositeMonoInstaller
名前の通り MonoInstaller の Compositeパターン版です。
コンポーネントはすでにExtenjectの中に含まれていますので、他のMonoBehaviourと同様にAdd Component可能です。
また、Prefab化することで Prefab Installerとしても利用することができます。
CompositeScriptableObjectInstaller
名前の通り ScriptableObjectInstaller の Compositeパターン版です。
Create -> Zenject -> Composite Scriptable Object Installer
をクリックすることで作成可能です。
先程も述べましたが、個人的には ScriptableObjectInstaller をよく使うので特にオススメしたいです!
FYI: 循環参照の検知
Compositeパターンは性質上、循環参照ができてしまいます(詳細は後述)。
指定ミスによるヒューマンエラーを回避するために、循環参照を検知したときはInspector上のプロパティが赤くなるEditorも用意しました。
ちなみにこの状態で起動すると例外(ZenjectException
)が飛びます。
Composite Installer 開発小話
標準搭載化
ありがたいことにPullRequestを出したところ、CollaboratorのMathijs-Bakkerさんに「標準的な機能になるから標準フォルダに移動してもいいんじゃないか?」とコメントをいただけました。
実質的に標準搭載化となったため追加修正を加えた上でマージされることになりました。
そのおかげで5日ぐらいは修正に費やすことになりましたけど、正直に嬉しかったのでモチベは高かったですw。
「プルリクって普通標準搭載になるもんじゃないの?」って思われた方もいるかもしれないので補足すると、
Extenject (Zenject) には OptionalExtras
というオプション的機能を詰め込んだフォルダがあります。
当初はこちらに 拡張機能としてComposite InstallerのPullRequestを出していたのですが、上記のようなご提案をいただけたため標準搭載となりました。
みなさんの開発の手助けになれば嬉しいです。
そういえば、PullRequestですがmergeじゃなくてrebaseで取り込まれたのは若干気になりました。
ソースコード自体はそのまま取り込まれたので問題ないと思いますけど。
循環参照対策
Compositeパターンは循環参照に注意する必要があることを知っていたので、循環参照対策も主に2つ入れています。
- 循環参照があるComposite Installer があった場合は例外を出す
- エディタ上で循環参照を検知したらInspectorに赤く表示する
こちらを導入するに当たり循環参照の検証処理も少し工夫したので共有します。
循環参照が起こるケース
Compositeパターンにおける循環参照は以下のケースがあります。
- 自分自身への参照を持つ場合
- 葉要素(子孫)が自身への参照を持つ場合
- 以下の例では Self と Leaf06 で循環参照
- 葉要素(子孫)同士が循環参照している場合
- 以下の例では Leaf06 と Leaf07 が互いに循環参照
循環参照の検証方法
以下の手順で行います
- 葉要素がCompositeではないこと
- Compositeパターンの葉要素は元となったinterfaceなのでダウンキャスト(
as
)でCompositeかどうかの判定が必要
- Compositeパターンの葉要素は元となったinterfaceなのでダウンキャスト(
- 葉要素がCompositeである場合は自身でないこと
- 自身ではない場合は先祖一覧の中に子要素がないこと
- 自身を先祖一覧に登録
- 先祖一覧を用いて子要素すべてで同様の検証を実施
上記をComposite Installer の子要素が見つからなくなるまで行うことで循環参照を検知することができます。
先祖一覧を記録しながら探索することになるため、可変Listが必要になります。
また、デザインパターンなどを考慮するとダウンキャストはアンチパターンになり得ますが、
今回は広く利用されることを想定し循環参照問題の回避を優先してダウンキャスト込みの検証方法にしました。
ちなみに検証コードは以下となります。
CompositeInstallerExtensions.cs
検証処理のアロケーションの削減
私は余計なアロケーションは極力避けたい性質なので、アロケーションをなくしたい欲が出ました。
一応、RuntimeだけでなくEditor上でもInspectorの更新毎に検証処理が動いてしまうため、ある程度アロケーションを回避したかったという理由もあります。
結論を述べますと 4連結までの検証はアロケーションが回避される ようになっています。
やり方は単純で先祖1つ、先祖2つ、先祖3つ、先祖4つで検証するメソッドを用意して連結するテクニック(つまり面倒くさいやり方)で対応しました!
見たほうが説明が早いと思いますので以下のコードを御覧ください。
CompositeInstallerExtensions.ValidateAsComposite
4連結まで対応したのは「多くても3連結ぐらいかな?」と思ったためです。
4連結以上の場合はアロケーションが発生しますので、「いやもうちょっとアロケーション発生しないようにしたい」って人は上記のコードを参考にPullRequestを出してください。
もちろん、テスト (TestCompositeInstallerExtensions) も追加してあげてください!
stackallocとかいうunsafeコードを使うことも一瞬考えたけど流石にすぐ却下しましたw
余談: 循環参照対策なしの場合
循環参照のあるComposite Installerに対してInstallBindings
をコールした場合、以下の現象が発生しました。
- Playでは
StackOverflowException
or エディタ自体が落ちる- どちらになるかはPlay毎に異なり、
StackOverflowException
確定ではありませんでした
- どちらになるかはPlay毎に異なり、
- Zenject Validate では落ちることなくフリーズし、エディタが入力を一切受け付けなくなる
どちらにしてもInstallBindings
の初めに検証(Assert
)を入れる必要がありました。
アロケーション発生させたくないとか言ってられない!
そもそもなぜ作ろうとおもったのか
- Installer群を一括でContextに登録する機能がほしかったから
- Installerが増えて管理しづらかったので
- 1つのInstallerクラス内にすべてのBind処理を書くと保守性が悪化するため避けたかった
- 開発におけるワークフローを考えてみたときにComposite Installerがあればいろいろと応用が効くと思ったから
- 具体的には「Compsote Installer の活用術」で述べた通りです
他に良さそうな活用方法があればぜひ教えて下さい!
Composite Installerを試したい場合
9.2.0 にrebaseでComposite Installerのcommitを取り込んだタグを用意しました。
もともと拡張として作成していたこともあってExtenjectの基本機能に変更はありません(Inspector表示用のEditorに少し変更が入ったぐらいです)。
metaファイルも同じなのでリリースバージョンが出てもCompositeMonoInstaller, CompositeScriptableObjectInstallerはそのまま使えると思います。
MITライセンスなので言う必要はないと思いますが、自己責任でお願いします!
フォークしたリポジトリ(tsgcpp上)にタグを登録していますが、本体にはすでに取り込まれている内容なためtsgcppの記名は不要です。
クレジット
- draw.io @diagrams.net
- 図形の作成に利用させていただきました
雑感
まさかの3ヶ月ぶりの投稿です。。。
個人開発を優先していたらあっという間に月日が流れていました。
記事を書いた理由はそろそろ情報発信しないとなと思ったことと、
Extenjectを使ったワークフローの提案的なことをやってみたいと思ったからです。
Extenjectに限らずDIコンテナはゲームやアプリケーションを面白くするための仕組みではありませんが、
工夫次第で幅広いシステムに対応させることが可能になると思います。
UnityでSOLID原則などを取り入れる上でも欠かせない存在かな思いますので、ぜひ活用してください!
それでは~
【GitHub Packages for Unity】限定配布も可能, GitHub Packages で Unity アセット配布
- 概要
- 記事内でのキーワード略称
- GitHub Packages について
- アップロードまでの流れ
- UPM経由でのパッケージのインストール
- GitHub PackageでUnityパッケージを配布するメリット
- GitHub PackageでUnityパッケージを配布するデメリット
- GitHub Package を使用する場合の諸注意
- サンプル
- 参考
- 雑感
概要
今回はUnity向けにGitHub Pakcagesでアセットを配布する方法を紹介します。
社内やチーム内でUnityのパッケージを限定配布できたりもするので、
良かったら活用してみてください!
記事の最後にサンプルを記載しています。
記事内でのキーワード略称
GitHub Packages について
簡単に言えば「パッケージ公開サービス」でGitHubから提供されています。
様々なパッケージ形式に対応
- npm, gradle, docker container などあらゆるパッケージ形式に対応
GitHubの仕組みでパッケージのアクセス権限を制御可能
Publicは無料、Privateは従量課金制
- Publicパッケージは無料で配布可能
- Privateパッケージでも Free, Team, Enterpriseそれぞれにも無料枠有り
- ストレージ枠とデータ転送枠が使用されます
- billing ページにて使用状況を確認可能
Data transfer out
がデータ転送枠
GitHub Actions 経由でリポジトリからアップロード
- Repository -> Actions -> Packages の流れでアップロード
今回は GHA での簡単なアップロード例を紹介します
アップロードまでの流れ
まずはパッケージをアップロードするまでを紹介します!
Package Manager形式でアセットを用意
- UPM向けにアセットを用意
- アセット +
package.json
- アセット +
meta
ファイルも必ず生成- UPMで必須、存在しないと取り込んだときエラー
サンプルコードにも簡単なパッケージ用アセットを4つ用意しています。
package.json の name について
name
の推奨文字はa-z0-9.-_
で大文字は非推奨com.<org>.<name>(.<sub name>)
のようにドメインを含めること推奨- UPMで使用する際の
Scoped Registries
はドメイン部分でアクセス先を制御するため jp.co
,jp.ne
などでも可
- UPMで使用する際の
com.tsgcpp.unitygithubpackageexample.integration com.tsgcpp.tscubemapgenerator jp.co.tsgcpp.groupname.categoryname
package.json について
publishConfig
項目が必須- UserもしくはOrganizationの前には
@
が必須 - すべて小文字 (アカウント名がSampleAccの場合は
@sampleacc
) - パッケージをアップロード先として使用される
- 余談: GitHub上ではアカウント名は
tsgcpp
もTsGCpP
も同じ扱い
- UserもしくはOrganizationの前には
unity
項目もUnity向けのため必須- それ以外は通常のUPMと同様
"publishConfig": { "registry": "https://npm.pkg.github.com/@<user or organization>" }
サンプルの全体は以下となります
{ "name": "com.tsgcpp.unitygithubpackageexample.integration", "version": "1.2.3", "displayName": "Integration Package Example", "description": "Integration Package Example for UnityGithubPackageExample", "unity": "2019.4", "keywords": [ "tsgcpp", "main" ], "license": "UNLICENSED", "dependencies": { "com.tsgcpp.unitygithubpackageexample.script": "2.3.4", "com.tsgcpp.unitygithubpackageexample.prefab": "3.4.5" }, "author": { "name": "tsgcpp", "url": "https://github.com/tsgcpp" }, "scripts": { "test": "exit 0" }, "repository": { "type": "git", "url": "git+https://github.com/tsgcpp/UnityGithubPackageExample.git" }, "bugs": { "url": "https://github.com/tsgcpp/UnityGithubPackageExample/issues" }, "publishConfig": { "registry": "https://npm.pkg.github.com/@tsgcpp" } }
アップロード用アクションを定義
- GHA の Yaml を定義
- サンプルでは
.github/workflows/release-package.yml
に定義
- サンプルでは
packagePath
にパッケージの相対パスを指定(複数可)actions/setup-node
のstepは必須npm publish
でパッケージをアップロード
せっかくGHAを使うので、アップロード処理前にテストも組み込みましょう!
name: Unity Example Packages Publish # release tagを発行時にアップロード on: release: types: [created] jobs: # Unity Test Runner の実行 (GameCIのunity-test-runnerを利用) test: name: Test in ${{ matrix.testMode }} runs-on: ubuntu-latest strategy: fail-fast: false matrix: projectPath: - . unityVersion: - 2020.3.14f1 testMode: - all steps: - uses: actions/checkout@v2 with: lfs: true - uses: game-ci/unity-test-runner@v2 id: tests env: UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }} with: unityVersion: ${{ matrix.unityVersion }} projectPath: ${{ matrix.projectPath }} testMode: ${{ matrix.testMode }} artifactsPath: ${{ matrix.testMode }}-artifacts githubToken: ${{ secrets.GITHUB_TOKEN }} checkName: ${{ matrix.testMode }} Test Results - uses: actions/upload-artifact@v2 if: always() with: name: Test results for ${{ matrix.testMode }} path: ${{ steps.tests.outputs.artifactsPath }} # アップロード処理の実施 publish: needs: test runs-on: ubuntu-latest strategy: fail-fast: false matrix: packagePath: - ./Assets/Plugins/Script - ./Assets/Plugins/Material - ./Assets/Plugins/Prefab - ./Assets/Plugins/Integration permissions: packages: write contents: read steps: - uses: actions/checkout@v2 - uses: actions/setup-node@v2 with: node-version: 12 registry-url: https://npm.pkg.github.com/ - run: npm publish ${{ matrix.packagePath }} env: NODE_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}}
- 余談
.npmrc
はUnityの場合は不要(node.jsプロジェクトではないため)
パッケージのアップロード
- masterにアセット, package.json, GHAのYamlを追加
- リポジトリの Releases ページの
Draft a new release
より release tag を発行- release tagの発行がアクションのトリガーのため
- アクションが正常に完了すると Packages に登録される
- 初回のみ反映されるまで若干時間がかかる可能性有り
サンプルリポジトリは記事の最後に記載しています。
UPM経由でのパッケージのインストール
次はアップロードされたパッケージのUPM経由でのインストール方法を紹介します!
アクセストークンの発行
アクセストークンはAPI経由などでGitHubのコンテンツにアクセスするためのトークンになります。
- GHPのアクセスにはアクセストークンが必要
- public, private にどちらでも必要
アクセストークンの注意点
発行する前にアクセストークンの注意点についてです。
- トークンの発行はパッケージにアクセスする本人のアカウントで実施
- 発行したトークンは第三者へは非公開にし、基本的に本人のみ使用する
- (任意)期限 (Expiration) を指定して定期的に更新 (セキュリティ観点)
GitHub上でアクセストークンを発行
- Settings -> Developer Settings -> Personal access tokens のページに進む
- "Generate new tokens" のボタンをクリック
- Note 項目に任意の名前を指定 (ex: Package Access)
- (任意) 期限(Expiration)を設定
read:packages
をチェック- パッケージアクセスのみであれば
read:packages
のみで問題有りません
- パッケージアクセスのみであれば
- ページ下部の "Generate token" ボタンで発行
- 発行されたトークンをコピー
- ページを閉じると再確認できません!
.upmconfig.toml ファイルを作成
.upmconfig.toml
は UPM におけるアクセス設定ファイル- https://docs.unity3d.com/Manual/upm-config.html
- ファイルパスも上記ページの "User configuration file location" を参照
- アクセス先(
npmAuth
)とそのアクセストークンを記載
[npmAuth."https://npm.pkg.github.com/@<user or organization>"] token = "<ACCESS TOKEN>" alwaysAuth = true
以下は記入イメージ
[npmAuth."https://npm.pkg.github.com/@tsgcpp"] token = "ghp_hogehogehogehogehoge" alwaysAuth = true
これでUnityのパッケージインストール時にアクセストークンが使用されます
Unity で Scoped Registries とインストールパッケージを設定
Packages/manifest.json
をエディタで開くscopedRegistries
項目に記載url
にはhttps://npm.pkg.github.com/@<user or organization>
scopes
に ドメイン部分を記載
以下は記入例で、com.tsgcpp
とついたパッケージは GHP の @tsgcpp
経由で取得されます。
{ "scopedRegistries": [ { "name": "tsgcpp public", "url": "https://npm.pkg.github.com/@tsgcpp", "scopes": [ "com.tsgcpp" ] } ], "dependencies": { "com.tsgcpp.unitygithubpackageexample.integration": "1.2.3", ... } }
補足: com.tsgcpp.unitygithubpackageexample.integration (サンプルパッケージ) の依存グラフ について
- 今回 integration, script, prefab, material のパッケージは以下の依存関係
com.tsgcpp.unitygithubpackageexample.integration
のみの取り込みで他3パッケージも取得
インストールの確認
- Unityエディタ上で
Packages
項目を確認しましょう
以上がGHP経由での取込までの流れとなります!
ちなみに紹介したとおりに .upmconfig.toml
と manifest.json
を設定すれば、
私が用意したサンプルパッケージ (public) が取り込まれます。
残りは GHP を使用する場合のメリット、デメリットを紹介します!
GitHub PackageでUnityパッケージを配布するメリット
package.json の dependencies に依存を定義可能
- GHPにアップロードされたパッケージは
dependencies
項目で依存パッケージを指定可能 - UPMで取り込んだ場合は依存パッケージも合わせて取り込まれる
- Git URL 経由では難しかった依存パッケージの同時取込が可能
{ "name": "com.tsgcpp.unitygithubpackageexample.integration", "version": "1.2.3", "displayName": "Integration Package Example", "description": "Integration Package Example for UnityGithubPackageExample", "unity": "2019.4", ... "dependencies": { "com.tsgcpp.unitygithubpackageexample.script": "2.3.4", "com.tsgcpp.unitygithubpackageexample.prefab": "3.4.5" }, ... }
Privateリポジトリでのパッケージ配布が容易
補足: Git URLでもPrivateリポジトリから取り込みは一応可能
最近のUnityではGit URL経由の場合も、専用のアクセストークンを勝手に作ってくれたりします。
ただ、アクセストークンの削除が面倒だったり、WindowsとMacで動作が異なったり、
チーム内での厳密な運用には若干向かない印象があります。
GitHubアカウントの仕組みでアクセス権限を制御可能
- GitHubの
Manage Access
でパッケージのアクセス範囲を制御可能- 限定配布が容易になる理由の1つ
独自のnpmレジストリが不要
- privateな独自のregistryの作成、保守、運用が不要
GitHub PackageでUnityパッケージを配布するデメリット
Unity Package Manager のパッケージ検索機能が使用不可
- GHP側に検索API endpoint (
/-/all
,/-/v1/search
)が提供されていない - UPMでパッケージ検索が発生すると毎回エラーログが出てしまう
- UPMウィンドウを開いたときなどに
scopedRegistries
に登録されたレジストリ全体で検索が実施される仕様のため
- UPMウィンドウを開いたときなどに
ビルド自体には影響しません
Packages以下で displayName で表示されない
パッケージの使用自体は問題ない (と思います)
- 原因不明、検索機能が使えないことが原因?
displayName
自体は認識されているAdd package from git URL...
の場合は問題なくdisplayName
で表示される
GitHub Package を使用する場合の諸注意
- privateの場合はストレージ枠に注意
- アップロードされたパッケージ毎にストレージ枠を消費
- 巨大なファイルをアップロードする場合は特に注意 (3Dモデル, サウンドファイル、動画ファイルなど)
- パッケージ削除は一応可能ですが、基本的に削除しない方針を推奨
- 作業コストとヒューマンエラーにつながる
- 特にパッケージが大量になったときの手作業は担当者が地獄を見ます
おサイフと相談しましょう!ご利用は計画的に!
サンプル
サンプルリポジトリ
サンプルパッケージ
サンプルパッケージ発行時のアクション
参考
雑感
GitHubにサンプルを作成してからちょっと遅れてしまいました。
今週はちょっとドタバタしていたので、今日は集中して記事にしてみました!
GHP経由の場合は検索機能がありませんが、依存パッケージを定義できたりアクセス制御もGitHubの仕組みを流用できたりで運用上もメリットがあると思います!
あと、displayNameが反映されない問題は気になるのであとでUnityに報告しておきます(公開パッケージがあったほうが報告しやすかったので)。
それでは~
【感想】 Adaptive Code のススメ
- 概要
- 知見
- サンプル
- 雑感
概要
今回は 「Adaptive Code C#実践開発手法 第2版」を読み終わったので、
そこで得た知見から特に感銘を受けた部分を3つ共有しようと思います。
Unity開発者なので、UnityとC#を題材にして紹介します。
本当は数か月前には読み終わってたんですが、記事にできていませんでした。。。復習の意味も込めて記事にします。
知見
リスコフの置換原則 (Liskov Substitution Principle) の保証
以下 LSP と呼称します。
LSPの概要
平たく言うと
"サブクラス、継承クラスはベースクラスに置き換え可能でなければならない"
というSOLID原則の1つですね。
本には「コントラクト(ルール定義)」として事前条件、事後条件、データ不変条件などといった重要な要素も紹介されています。
この本から得られたのは単なるリスコフの置換原則の知識ではなく、
「如何にしてリスコフの置換原則を保証するか」
という部分でした。
LSPの保証方法
結論を述べるとテストクラスを活用します
- ベースのテストクラスを定義
- 継承クラスのテストはベースのテストを継承
つまり、単に「LSPを満たすように実装する」だけではなく、
「LSPを機械的に保証する」をテストクラスで実現することになります。
LSP保証のテストクラスのサンプル
テスト対象のinterface
今回の題材は「数値の丸め処理を行うメソッド」とします。
以下の IRounder.Round
の事前条件、事後条件をベースのテストクラスで定義しましょう。
public interface IRounder { float Round(float value, float interval); }
ルール定義
- 事前条件
- intervalに0は指定不可
- intervalに負の値は指定不可
- 事後条件
- 引数のvalueとintervalが同じ値の場合はintervalを返すこと
ベースのテストクラスを定義
ベースのテストクラスのポイントとして
- abstractクラスとして定義
- abstractメソッドとして生成メソッドを定義
- 継承クラスのインスタンス生成を定義可能にするため
- ルールを満たすことを確認するテストを定義
using NUnit.Framework; public abstract class TestRounderBase { // abstract指定により対象オブジェクトのインスタンス生成を継承テストクラスで定義 public abstract IRounder CreateTarget(); IRounder _target; [SetUp] public void SetUp() => _target = CreateTarget(); // 事前条件1: intervalに0は指定不可として、例外を出すこと [Test] public void Round_ThrowsExceptionIfIntervalIsZero() { Assert.Throws<RounderException>(() => { _target.Round(1f, interval: 0f); }); } // 事前条件2: intervalに負の値は指定不可として、例外を出すこと [Test] public void Round_ThrowsExceptionIfIntervalIsLessThanZero() { Assert.Throws<RounderException>(() => { _target.Round(1f, interval: -1f); }); } // 事後条件1: 引数のvalueとintervalが同じ値の場合はintervalを返すこと [Test] public void Round_ReturnsIntervalIfValueIsEqualToInterval() { Assert.AreEqual(1f, _target.Round(1f, interval: 1f)); Assert.AreEqual(2f, _target.Round(2f, interval: 2f)); Assert.AreEqual(10f, _target.Round(10f, interval: 10f)); } }
継承クラスのテストを作成
ベースクラスを継承したテストクラスを作成。
ベースクラスを継承して、継承クラスのインスタンス生成メソッドを定義する。
public class TestNearestRounder_LSP : TestRounderBase { public override IRounder CreateTarget() => new NearestRounder(); }
ちなみに NearestRounder
の定義は以下です。
using System; public sealed class NearestRounder : IRounder { public float Round(float value, float interval) { if (interval <= 0f) { throw new RounderException("\"interval\" is 0 or less"); } return (float)Math.Round(value / interval) * interval; } }
テスト全体は TestNearestRounder.cs で確認できます。
また、こちらのサンプルには NearestRounder
特有のテストも別クラスとして記載しています。
テストの実行
IRounder
の継承クラスがLSPを満たしていることをUnitTestで確認。
また、以後ルールを追加したい場合はベースクラスに追加することで、
継承クラスも常にルールを満たしていることが検証されるようになります。
LSPのまとめ
ベースとなるテストクラスでLSPのルールを定義することで、より強固なLSPを守るフローを構築することが可能になります。
interfaceのコメントに「intervalは0以上を必ず指定させること」などを記載してもプログラム上では何の影響もありませんが、 テストクラスを再利用することで保守する上でも保証することが可能になります。
abstractなテストクラスを利用する発想があまりなかったので、目からうろこでした。
投機的な一般化(Speculative Generality)
書籍には "投機的な一般化" と記載されていましたが文字だけだとなんのこっちゃ感がありますね(笑)。
原文は"Speculative Generality"です。
ちなみに書籍では「開放/ 閉鎖 の 原則」を意識するための要素として紹介されています。
投機的な一般化の概要
ここは私自身の解釈も含まれますが、端的に言えば
"あると良さそうと思って拡張可能に実装した、でも結局使わなかった"
的なことです。
interfaceを使った抽象化を身に着けたばかりのプログラマーによくある行動として、
「あらゆるものをinterface化して、意図しない拡張性を持たせてしまう」というものがり、これが投機的な一般化となる可能性があります。
「すべての設計は明確な意図をもって行うべき」との説明があり、
投機的な一般化はそれに反する行いのことです。
予想されるバリエーション
「予想されるバリエーション」とは「何が拡張できて、何が拡張できないのかを明確にすべきである」と説明があります。
投機的な一般化の対比となり、設計は「投機的な一般化」ではなく「予想されるバリエーション」となるようにするべきということになります。
投機的な一般化の問題
- 拡張ポイント特有の実装が無駄になってしまう
- interfaceで抽象化されたメソッドを使用する場合、そのメソッドのあらゆる結果の対策が必要となる
- 例: そのメソッドが異常系を返すパターン、例外を出すパターンなどの考慮が必要となり、結果的に開発コストが高くなる。
- 意図しない継承クラスが作成されてしまう可能性がある
- 継承クラスが作成された場合は継承クラス特有の考慮が必要になる
- アセンブリ(Unityにおけるasmdef)に継承クラスが分散し、むやみにinterfaceを変更できなくなる
投機的な一般化の回避例
修飾子(sealed, internalなど)を指定
修飾子はコンパイラが解釈する要素となります。
つまりクラスのルールをコンパイルレベルで定義することが可能になります。
- クラスに
sealed
を指定し、派生クラスの作成を制限- もし派生クラスを想定していないのであれば基本的に
sealed
を付けて保守範囲を限定する - 余談: C#はデフォルトで非
sealed
(派生クラスの作成可能)
- もし派生クラスを想定していないのであれば基本的に
- クラスに
internal
を指定し、異なるアセンブリでの派生クラス発生の回避internal
クラスはアセンブリ内でのみアクセス可能なので、自分の管轄外での派生クラスの発生を回避できます- 余談、もしinternalクラスのUnitTestをしたい場合は InternalsVisibleToAttribute を活用すると良いです
ちなみにKotlinだとデフォルトでは派生不可(非open)で、僕がKotlinを好きな理由の1つだったりします。
プロダクトの拡張ポイントを見極める
ここはノウハウや経験に基づくものになります。
「投機的な一般化」は拡張部分に焦点を絞ったテーマのため少し話はずれてしまいますが、
以下のことを意識してみるとよいかもしれません。
- もし自身が企画者である場合は「この拡張が必要な理由が明確になっている」かを考慮する
- もし自身が実装する立場であった場合は、企画した人が「とりあえずあると良さそうな部分を提案する」といった性質がある場合は、そのアイデアを深堀して必要性を明確化する
「15分程度で終わる議論をせず、5日かけて実装したけどやっぱりいらなかった」みたいなことは極力避けましょう。
拡張ポイントを作るということは、それだけ保守範囲も増えるということを意識すると良いと思います。
投機的な一般化のまとめ
「すべての設計は明確な意図をもって行うべき」という言葉にはなかなか考えさせられるものがありました。
意図をしない部分があるということは、言ってしまえばあらゆることが起こってしまうと可能性を秘めているということです。
だからこそノウハウを蓄積して、予測できなかった事象を少しでも減らしていきましょう。
私もクラスを作るとき、派生を想定していない場合は sealed
を忘れないようになど小さなところから意識するように心掛けていきたいです。
コナーセンス(Connascence)
コナーセンスの概要
コナーセンスとは「コードにおける依存度合(結合度)を図る指標」のことです。
依存関係の尺度を見極めるうえで意識すると役立つ概念となります。
コナーセンスにはレベルがあり、高いものほど処理との結合度が強いということになります。
コナーセンスの結合度
結合度を考える場合に簡単な判断として、
「ある要素を変更した際の影響度合い」と考えても良いと思います。
具体例を2つほど出します
名前のコナーセンス
"名前" のコナーセンスは、結合度が高いです。
ここでいう名前とは、クラス名やメソッド名など"コンパイラが解釈する名前"のことです。
注意として、メソッド名に使用されている単語自体の意味は関係ありません。
とあるメソッド名を変更したときは、そのクラスを使っていた部分も修正しないとコンパイルエラーが発生しますよね?
public float Calc() => ... public void Main() { var result = Calc(); ... }
上記のコードで、もし Calc()
-> Calculate()
とリネームした場合は、Main
関数内も修正が必要になります。
クラス名やメソッド名は名前を変えると、それだけで使用者に影響を与えるため強い結合度を持っていると言えます。
値のコナーセンス
"値" のコナーセンスは、結合度が低いです。
これは身近ないい例があります。
よく「マジックナンバーは定数化しましょう」みたいなことを聞きませんか?
なぜ定数化するかというと、定数名に意味を込めて意図が伝わりやすいようにするためという側面もあります。
(もちろん、共通の定数を再利用するといった理由もあります)
public float CalcArea(float radius) => radius * radius * 3.141592f;
例えば上記のように3.14...とみると大体の人は「円周率だ!」となると思います。
しかし、すべての値がそれだけで意図が伝わるとは限りません。
public int CheckCode(int code) => code == 200 ? 0 : -1;
上記のコードは見慣れた人には「ああ、HTTPステータスかな?」となると思いますが、
見慣れない人には人には暗号のように見えると思います。
コードを書いた人は「HTTPステータスコードによってエラーコードを返すようにした」つもりで書いても、
第三者には伝わらりづらいコードになるかもしれませんので、以下のように定数名に意味を込めてあげましょう。
private const int HttpOk = 200; private const int ValidValue = 0; private const int InvalidValue = -1; public int CheckCode(int code) => code == HttpOk ? ValidValue : InvalidValue;
プログラム的にも 200 -> 100 に置き換えても別にコンパイルは通ってしまいますが、
それだと意図しない挙動になってしまいます。
値のコナーセンスのように結合度の低い場合は、定数化するなどの工夫が求められることが多いです。
コナーセンスのまとめ
コナーセンスはコード自体の品質を図るうえで非常に役立つ指標になります。
書籍には「インターフェイスのコナーセンス」という非公式なコナーセンスが紹介されています。
インターフェイスは継承するオブジェクトの必須要素やクライアントの依存部分を定義することが可能で、
意味を込めるうえでは非常に有用な要素といえます。
書籍を読むまではコナーセンスという単語自体は知りませんでしたが、
「interfaceで要素を定義する」などは意識していたこともあって、より具体的な概念として知ることができました。
コナーセンスの要素として10個ほど紹介されており、それぞれに特有の観点がありますので気になったら確認してみてください!
サンプル
LSPについてのサンプルコードは以下にあります。
雑感
今回は技術書の感想という形で記事にしてみました。
こんな1ページの記事では紹介しきれないぐらいたくさんの知見について記述されています。
何気にアジャイル開発やスクラムについても簡易的に紹介されていますので、
アーキテクチャやコーディングだけでなく、ワークフローについても勉強になる本だと思います。
SOLID原則をより深堀したい人にはとてもオススメなのでよかったら手に取ってみてください!
それでは~
【Unity】OculusIntegrationがPackageManager対応しやすくなりました!
- 概要
- 環境
- 追記
- Meta公式によるPackage Manager対応 (2022/10/12 追記)
- PackageManager対応方法
- パッケージの取り込み方
- 対応しやすくなった理由
- PackageManager対応するメリット
- 雑感
概要
Oculus Integration SDK がいつの間にかUnity Package Managerに対応させやすくなっていたのでちょっと紹介します。
巷では、Oculus Quest向けのSplash Screen が指定可能になったことで話題でしたが、何気にPackageManager対応しやすくなったのも見過ごせないと思っています!
※Splash Screenについての公式ツイート
Introducing Instant Runtime-driven Splash Screens. Get people into your Quest apps faster than ever. Check out our blog post for how to implement: https://t.co/XLKZy3ukM6 pic.twitter.com/kkcSJrMvik
— Oculus Developers (@Oculus_Dev) 2021年5月13日
環境
- Oculus Integration SDK 28.0 (1.60) 以降
追記
- 2022/10/12
- Meta公式によるPackage Manager対応
Meta公式によるPackage Manager対応 (2022/10/12 追記)
Meta公式からMeta XR Integration (旧Oculus Integration) v44 から Pacakge Managerによるインストール方法がアナウンスされています。
Import Individual SDKs from Unity Package Manager | Oculus Developers
レジストリは https://npm.developer.oculus.com/
とのこと。
packages are hosted on Meta’s scoped registry: https://npm.developer.oculus.com/.
現時点では基本機能 (Oculus/VR
) のみのようですが、順次対応されるようです。
How Unity Package Manager Can Help You Manage Integration SDKs
Meta Quest Proの発表もあったので、対応が進められたのかもしれません。
こちらの記事を公開してから1年4ヶ月は経ってるので、あまり「そのうち」ではありませんでしたね(笑)
PackageManager対応方法
1. Oculus Integration パッケージ化専用のUnityプロジェクトを作成
- パッケージ用のUnityのプロジェクトを作成
2. Oculus Integrationをインストール
- Asset Store or アーカイブページ からOculusIntegrationをダウンロードおよびインストール
- 必要なアセットにチェックを入れて "Import"
※今回は基本機能のみをインストールするため Oculus/VR
のみ指定
3. gitignoreにOculus向け設定ファイルを追加
- commitから除外するために.gitignoreにOculus向け設定ファイルを追加
- パッケージ対応したOculusIntegrationを取り込むと自動的に生成されます
- パッケージ側からは除外しておきましょう
# 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
4. package.json を作成
- Package Managerに対応させるため
Assets/Oculus/package.json
を作成- nameなどはプロジェクトに合わせて適宜修正してください
{ "name": "com.tsgcpp.oculusintegration", "version": "1.61.0", "displayName": "OculusIntegration", "description": "The Oculus Integration SDK (private).", "unity": "2019.4", "keywords": [ "oculus" ], "dependencies": {} }
5. Assets/Oculusをcommit
.gitignore
を commitAssets/Oculus
とAssets/Oculus.meta
をcommit- package.jsonも併せてcommit
6. Private用のGitリポジトリにpush (任意)
※Publicにすると再配布になるため注意が必要
フォルダ構成
パッケージの取り込み方
- 通常のPackageManagerでの取り込みと同様となります
Add package from git URL...
https://<git url>/<org>/<repository>.git?path=Assets/Oculus#<tag or branch>
のように指定- i.e.
https://github.com/tsgcpp/OculusIntegration.git?path=Assets/Oculus#1.61.0
(リポジトリ名: OculusIntegration, タグ名: 1.61.0 の場合)
- i.e.
Add package from disk...
Assets/Oculus/package.json
を指定
取り込み後に設定ファイルが生成される
対応しやすくなった理由
そもそもなぜ対応が難しかったのか
Oculus Integration SDK 27.0 (1.59) 以前の話となります
- 原因は
Assets/Oculus/VR/Editor/OVRProjectConfig.cs
OculusProjectConfig.asset
(Oculus向けの設定アセット) を自動生成するEditorスクリプト
- 自動生成するパスの指定方法に問題があり、PackageManager経由で取り込んだ場合にエラーが発生
- 端的に言うと
OVRProjectConfig.cs
の親の親フォルダが生成先
- 端的に言うと
- PackageManagerで取り込んだ場合は
Assets/
以下ではなく、Packages/
以下が生成対象となってしまっているPackages/com.tsgcpp.oculusintegration/VR/Editor/OVRProjectConfig.cs
に配置されているためAssetDatabase.CreateAsset
でScriptableObject(OVRProjectConfig
)の生成先にPackages/
以下を指定するとエラー (Assets/
以下ではない場合にエラー)
暫定対応
Oculus Integration SDK 27.0 (1.59) 以前の話となります
Assets/Oculus
以下のEditorフォルダすべてをunitypackageにExportAssets/Oculus
以下にあるEditor
フォルダ以下をすべて削除- OculusIntegrationを使用するメインプロジェクトに上記unitypackageでEditor群を取り込み
OculusProjectConfig.asset
を使用するにはEditor以下のスクリプトが必要なため
上記の方法で暫定的に対応することは可能でした。
ただ、Editorスクリプトのみ直接取り込む形となるため、結構残念な感じになってました。
PackageManagerで取り込まれた場合の考慮が28.0以降で追加
- SDK 28.0 (1.60) で以下の考慮が追加されました
OVRPluginUpdaterStub.IsInsidePackageDistribution
はassetPath.StartsWith("Packages/"
の判定を実施Packages/
以下だった場合は、Assets/Oculus/OculusProjectConfig.asset
を生成先に変更
private static string GetOculusProjectConfigAssetPath() { ... if (OVRPluginUpdaterStub.IsInsidePackageDistribution()) {
つまり、PackageManagerで取り込まれた場合でもAssets/Oculus
以下に設定ファイルが生成されるようになりました。
これがOculus IntegrationがPackageManager対応しやすくなった理由です!
PackageManager対応するメリット
「別に直接プロジェクトに取り込んでもよくない???」って思った方もいらっしゃると思いますので、
メリットについても補足しようと思います。
- OculusIntegrationを管理するGitリポジトリを分割可能
- GitHubなどの容量制限にひっかかるのを回避可能
- OculusIntegrationは単体でもかなり容量が大きく、追加するアセットによってはあっさり100MBを超えます
Assets/Oculus/VR
のみでも60MBあります (29.0の場合)
- アセットのバージョン管理と更新が容易
- Package Managerでアセットを取り込むと更新が容易になります(バージョンを更新するのみ)
- Asset Store(unitypackage)で取り込んだ場合、一度アセットをすべて削除する必要がある場合が多い
- hookされるEditorスクリプトがあるため再起動はどちらにしても推奨
- Package Managerでアセットを取り込むと更新が容易になります(バージョンを更新するのみ)
雑感
今回はOculus関連にテーマを絞ってみました。
Oculus Quest2がかなり売れているので、OculusIntegrationの更新もかなり加速している感じがします。
もしかしたら、そのうちUnity Package ManagerでOculus Integrationが提供されるのかもしれませんね。
Unity向けだけじゃなくて、Unreal向けも加速しているような気がします。
Splash Screenも追加できるようになったので、これを機に追加しても良さそうですね。
みなさんのOculus向けアプリケーション開発の助けになれば幸いです。
それでは~。
【Unity】Physics.SyncTransforms の特性調査
- 経緯
- 検証環境
- 前知識
- 登場人物
- 検証方法
- 検証結果
- PlayerColliders x1
- PlayerColliders x2
- PlayerColliders x4
- PlayerColliders のみ (Animatorをオフ)
- PlayerColliders + AnimationColliders
- PlayerColliders + PhysicsAnimationColliders
- PlayerColliders + DisabledAnimationColliders
- PlayerColliders + DisabledAnimationColliders + CapsuleCollider
- PlayerColliders + ColliderDisabledAnimationColliders
- PlayerColliders + StaticColliders
- 注意事項
- 判明した特性
- 考察
- 検証プロジェクト
- 雑感
今回は Physics.SyncTransforms
の特性を調査したため共有します。
経緯
- Colliderの物理エンジンへのFlushを 通常のタイミングとは別で実施したい
- Flushを任意のタイミングで実行するには
Physics.SyncTransforms
をコールする必要があった Physics.SyncTransforms
を通常のFlushとは別に実施する場合にパフォーマンスなどが気になったため調査
※Flushとは実際にデータを反映する処理のこと(ここでは物理エンジン用メモリ領域への反映)
検証環境
- Unity 2020.3.4f1
- Mono
- .Net Standard 2.0
- Physics.autoSyncTransforms = false
- Oculus Quest 2 (Android)
- Development Build 有り(Profiler用)
前知識
物理オブジェクトの物理エンジンへのFlush
- ColliderのTransformが物理エンジンに反映されるタイミングはFixedUpdate後 (
Physics.autoSyncTransforms
がfalseの場合)- 厳密にはExecutionOrder でいう "Internal Physics Update" がFlushのタイミングと考えられる
- Flushタイミングは TestPhysicsSyncTiming.cs で確認
Physics.SyncTransforms
- 上記「物理オブジェクトの物理エンジンへのFlush」を任意のタイミングで発動させるメソッド
- 今回の主役
Physics.autoSyncTransforms
- ColliderのTransformが変更された際に即座に物理エンジンに反映するかの設定
- Unity 2019以降はデフォルトでfalse
- つまりデフォルトではFixedUpdate後にFlushが実施されると同義
- ProjectSettingsのPhysicsからも設定可能
Physics.autoSyncTransforms
によるとtrueにするとパフォーマンスの低下の可能性がある
When autoSyncTransforms is set to true, repeatedly changing a Transform and then performing a physics query can cause performance loss.
Physics.autoSyncTransforms = true
にする場合はオーバーヘッドに注意- 物理演算の正確性とパフォーマンスがトレードオフとなる
今回は Physics.autoSyncTransforms = false の場合をメインに調査
Flushの発生条件
- ColliderのTransformが変更された場合
- Dirtyフラグが付き、後述の「Flushの発生タイミング」で物理エンジンに反映
- こちらのUnity Pro Tips によれば移動する物理オブジェクトがない場合はオーバーヘッドはほとんどないとのこと
- つまり移動しないオブジェクトに関しては static かどうかにかかわらず Flush が発生しない
Flushの発生タイミング
Physics.autoSyncTransforms = false
であれば FixedUpdate後 orPhysics.SyncTransforms
コール時点- Dirtyフラグが付いた物理オブジェクトのみ
Physics.autoSyncTransforms = true
であればColliderのTransformが変更された時点
-- ここまでが前知識 --
登場人物
PlayerColliders
- CapsuleCollider + BoxCollider 64個 (4 x 4 x 4) をアニメーション
- LateUpdate(Animation Update後として) にて
Physics.SyncTransforms
を実施し時間を計測 - すべての検証環境で使用
AnimationColliders
- BoxColliderを大量(64000個)に配置してプレイヤー同様にアニメーション
- AnimatorのUpdate Mode は
Normal
- AnimatorのUpdate Mode は
PhysicsAnimationColliders
- AnimationColliders の Update Mode を
Animate Physics
に変更したもの
DisabledAnimationColliders
- AnimationCollidersのコライダー群のルートのGameObject(ColliderRoot)をオフにしたもの
DisabledAnimationColliders + CapsuleCollider
- DisabledAnimationCollidersのColliderRootと同階層に1つのCapsuleColliderを配置したもの
ColliderDisabledAnimationColliders
- ColliderRoot以下のすべてのColliderに対してCollider.enabled = false にしたもの
- ColliderRootオブジェクト自体はアクティブのまま
Static Colliders
- 10000個のstaticのBoxCollider群
検証方法
- 後述する「登場人物」を組み合わせた状態で
Physics.SyncTransforms
をコールして実行時間の変化を計測- コールはLateUpdate時 (Animatorが終了した後として)
検証結果
※おおよその平均
注目したい部分は色付き
検証対象 | 時間 (ms) |
---|---|
PlayerColliders x1 | 0.07 |
PlayerColliders x2 | 0.10 |
PlayerColliders x4 | 0.19 |
PlayerColliders x1 (Animator disabled) | 0.00 |
PlayerColliders + AnimationColliders | 21.01 |
PlayerColliders + PhysicsAnimationColliders | 0.07 |
PlayerColliders + DisabledAnimationColliders | 0.07 |
PlayerColliders + DisabledAnimationColliders + CapsuleCollider | 0.41 |
PlayerColliders + ColliderDisabledAnimationColliders | 0.08 |
PlayerColliders + StaticColliders | 0.07 |
クリックで展開:各調査ごとのProfiler
PlayerColliders x1
PlayerColliders x2
PlayerColliders x4
PlayerColliders のみ (Animatorをオフ)
PlayerColliders + AnimationColliders
PlayerColliders + PhysicsAnimationColliders
PlayerColliders + DisabledAnimationColliders
PlayerColliders + DisabledAnimationColliders + CapsuleCollider
PlayerColliders + ColliderDisabledAnimationColliders
PlayerColliders + StaticColliders
注意事項
- 今回は
Physics.SyncTransforms
に主眼を置くためCPU Usageはあまり考慮しない - XR環境のためCPU Usageに表示されている時間の大部分は
Gfx.WaitForRenderThread
判明した特性
Physics.SyncTransforms
の時間はコール時点のFlushされていないオブジェクト数に依存- FixedUpdate直後から
Physics.SyncTransforms
をコールするまでにTransformが変更されたオブジェクトが対象
- FixedUpdate直後から
- Flush済みのオブジェクトは
Physics.SyncTransforms
の時間には大して影響はしない- つまりTransformが変更されたオブジェクトがなければオーバーヘッドは小さい
- 通常Flushとそれ以外でFlushする対象は分けることは一応可能
- ただし「特定のオブジェクトのみFlushしたい」の様なコントロールは不可
- Unity側に機能が用意されていない(と思われる、ご存じの方がいたら教えてください!)
- ただし「特定のオブジェクトのみFlushしたい」の様なコントロールは不可
- 非アクティブなCollider(コンポーネントのenabled=false含む)はFlushの対象外
- アクティブ化した後のFixedUpdate後 or
Physics.SyncTransforms
で改めてFlush
- アクティブ化した後のFixedUpdate後 or
- 同階層にアクティブと非アクティブ両方のColliderが存在する場合は特殊なFlushとなる可能性あり(詳細は不明)
- "DisabledAnimationColliders + CapsuleCollider" の時間を見るとCapsuleCollider1つだけFlushされるにしては時間がかかっている
考察
Physics.SyncTransforms
時にFlushされるCollider数をうまくコントロールすれば、パフォーマンスへの影響をコントロールできそう- UpdateやFixedUpdateなで移動するColliderが多い場合は実行時間に注意が必要になると思われる
- あまりに多いと Physics.SyncTransforms 時スパイクが発生
検証プロジェクト
- 2022/06/19変更
- Performance Testing Extension for Unity Test Frameworkを使用した検証の自動化のためmainブランチには大幅に変更されています
- このブログ記事当初の検証プロジェクトは tag 1.0.0 をご参照ください
雑感
ちょっと個人的な事情があって検証、ついでにドキュメント化してみました。
Flush対象を明示的に指定できないのはちょっと残念ですね。。。
ただ、物理で動くものとそれ以外で分けるのは一応できそうってとこでしょうか。
また、別の機会に Physics.SyncTransforms
を利用したものを作ってみようと思います。
それでは~
(旧)【Extenject】Composite Installer を紹介!
以下にページに移行しました