凹みTips

C++、JavaScript、Unity、ガジェット等の Tips について雑多に書いています。

Unity で .unitypackage で配布していたアセットを Package Manager 対応してみた

はじめに

先日 Package Manager 対応のプルリクをいただいたきました。恥ずかしながら今まで Package Manger の対応およびその調査はサボってきており、あまり知識がない状態でしたので、これを機に色々と調べて配布する側としての自分なりの利用方針を決めていこうと思いました。以下、Unity Package Manager を省略して UPM と書きます(公式では一部を除き*1あまり UPM という表記は使われていないようですが、開発者の間では普通に使われている印象です)。

本エントリでは、既に色々配布しているライブラリがある上での立場から UPM についての調査を行った内容をまとめます。また、その上で自分は GitHub Actions を使ったパッケージのリリースを行う方針に決めたのですが、その道筋や具体的なコードを紹介していきます。

UPM の基本

UPM そのものについては既に世に色々と解説がありますので、本記事では基本のところ(使い方など)の解説は省きます。公式にとても分かりやすいドキュメントがありますので、そちらを参照いただくのをおすすめします。

docs.unity3d.com

簡単に述べると、これまで .unitypackage や直接インポートしたりしながら Assets 以下に整理して置いていた依存ライブラリが、Package Manager という仕組みを通じて、GUI を通じてライブラリを簡単に追加・削除でき、それらのバージョン管理が適切に行われ、依存関係が適切に解決され、またグローバルキャッシュを通じて異なるプロジェクト間でアセットが共有され、...といった具合に色々と便利になります。数々のアップデートを通じて、かなり使いやすいものになったようです。

f:id:hecomi:20211017224343p:plain

他の言語やプラットフォームではパッケージ管理の概念は以前から存在しています。例えば Node.js では npm(Node Package Manager)というパッケージ管理システムを通じて、ライブラリの管理を行うことが出来ます。ちなみに UPM のバックエンドでも、この npm を利用しているようです。npm はインストールや管理だけでなくレジストリnpmjs)も持っており、各開発者が作成したライブラリを npm publish してホストしてもらえるのですが、Unity でもこの npmjs にライブラリを置いても良い?みたいです。

Package Managerに自作パッケージを追加する2019年版|fuqunaga|note

また、非公式レジストリである OpenUPM などもあり Project Settings から Scoped Registry を登録すると Package Manager から見えるようになります。

openupm.com

zenn.dev

パッケージのソースとしては、このようなレジストリベースのものの他に以下のようなものにも対応しています。

  • ビルトイン
    • Unity 公式の機能
  • 埋め込み
  • ローカル
    • PC 上の任意のフォルダ
  • ローカル tarball
    • 特殊用途
  • Git

Unity が提供するものだけでなく、PC 上の任意のフォルダや Packages ディレクトリも読めるのでコードの依存関係を疎にしたりチーム開発する上でも便利そうですね。また、加えて Git が利用できるようになったのはライブラリ開発者・利用者双方から便利で素晴らしいです。また後で解説しますがリポジトリ上の任意のディレクトリを指定できる仕組みになっています。

docs.unity3d.com

どういったソースかはパッケージ名の右に表示されるのでわかりやすいです(ローカルだと Custom、Git だと git などのラベル)。

f:id:hecomi:20211023140911p:plain

f:id:hecomi:20211023140932p:plain

このようにとても便利になった UPM ですが、ライブラリ開発者側は気をつける点がたくさんあります。次にそれらを見ていきましょう。

UPM 対応の基本

パッケージ作成についての基本は以下のドキュメントにまとまっています。

docs.unity3d.com

パッケージ名

パッケージ名を決める必要があり、これには 2 つの表記があります。一つは表示用で、今までのように例えば拙作のものであれば uRaymarching、uREPL、uDesktopDuplication といったような名前がそれにあたります。もう一つは正式名で、逆ドメイン表記で表されるものです。com.hecomi.uraymarchingcom.hecomi.urepl といったようなものになります。日本語のドキュメントだと「com.<company-name>」から始まるものにすると書いてありますが、英語のドキュメントを見ると、別に com に限らずドメインなら何でも良いようです。

docs.unity3d.com

ディレクトリ構造

次にディレクトリ構造を大きく変える必要があります。

docs.unity3d.com

<root>
  ├── package.json
  ├── README.md
  ├── CHANGELOG.md
  ├── LICENSE.md
  ├── Third Party Notices.md
  ├── Editor
  │   ├── [company-name].[package-name].Editor.asmdef
  │   └── EditorExample.cs
  ├── Runtime
  │   ├── [company-name].[package-name].asmdef
  │   └── RuntimeExample.cs
  ├── Tests
  │   ├── Editor
  │   │   ├── [company-name].[package-name].Editor.Tests.asmdef
  │   │   └── EditorExampleTest.cs
  │   └── Runtime
  │        ├── [company-name].[package-name].Tests.asmdef
  │        └── RuntimeExampleTest.cs
  ├── Samples~
  │        ├── SampleFolder1
  │        ├── SampleFolder2
  │        └── ...
  └── Documentation~
       └── [package-name].md

ライブラリとしての情報が記述されたパッケージマニフェストファイル package.json と、このディレクトリ構造のいくつかのファイルから情報をパースし、Package Manager がうまく管理してくれるようになる感じです。これまでは Scripts などのディレクトリの下に置かれることの多かったスクリプトRuntime 以下に置くようにし、asmdef を合わせて配置する形にします。Editor 以下にエディタ用のスクリプトと asmdef を置くようにすることでそれぞれの分離がきれいになります。サンプルは Samples~ 以下に配置し、どのようなサンプルがあるかを package.json に記述します。こうすると利用者側は必要なサンプルのみ Package Manager 上でポチポチしてインポートできるようになります。

package.json

パッケージの名前やバージョン、作者、依存関係などの情報をここに記述します。

docs.unity3d.com

内容としては次のようなものになります。

{
  "name": "com.unity.example",
  "version": "1.2.3",
  "displayName": "Package Example",
  "description": "This is an example package",
  "unity": "2019.1",
  "unityRelease": "0b5",
  "dependencies": {
    "com.unity.some-package": "1.0.0",
    "com.unity.other-package": "2.0.0"
 },
 "keywords": [
    "keyword1",
    "keyword2",
    "keyword3"
  ],
  "author": {
    "name": "Unity",
    "email": "unity@example.com",
    "url": "https://www.unity3d.com"
  }
}

これはローカルパッケージなら GUI 上で編集が可能です。

f:id:hecomi:20211023141840p:plain

librarymoduletool などのタイプを指定できる type だけは何をどういう基準で選択すればよいかの説明がまだないので自分は空にしています。。

配布

さて、こうしてディレクトリ構造を整理し、package.json や関連するファイルを追加した後、GitHub など何らかの Git のリポジトリに公開します。このリポジトリの URL を利用者側に Package Manager 上で入力してもらう形になります。

docs.unity3d.com

ただ、Unity のプロジェクトとして Git にあげているプロジェクトの場合は package.json がルートではなく Assets/ 以下に置かれていることもあると思います。以下は uOSC の例です:

github.com

このような場合は、次のように package.json の置かれた場所を示す URL にするとインポートできます:

f:id:hecomi:20211023143207p:plain

Git リポジトリに置く以外にも冒頭で述べたように npmjs や OpenUPM に登録すると、更新があった際にユーザが GUI 上で更新ボタンを押して更新できるようになります。これはまた別途検証します。

さて、これまでの情報でプロジェクト全体のディレクトリ構造を次に考えていきたいと思います。

開発リポジトリディレクトリ構造

Assets に含める方式

先述のプロジェクトの構造を見てみます。

<Repository>
  ├── Assets
  │   └── uOSC
  │         ├── Examples
  │         └── Scripts
  │              ├── package.json
  │              ├── uOSC.asmdef
  │              ├── uOscClient.cs
  │              ├── ...
  ├── ...

この構造の問題点は Examples がディレクトリの外側にありパッケージに含められていない点です。これをパッケージにサンプルとして含めようとすると次のようになります。

<Repository>
  ├── Assets
  │   └── uOSC
  │         ├── package.json
  │         ├── Runtime
  │         │    ├── uOSC.asmdef
  │         │    ├── uOscClient.cs
  │         │    └── ...
  │         └── Samples~
  │               ├── Sample 1
  │               ├── Sample 2
  │               ├── Sample 3
  │               └── ...
  ├── ...

これで package.json に Sample 1~3 のディレクトリを記述すればサンプルとしてポチポチとインポートできるようになります。しかしながらこの構造ではディレクトリ名にチルダがついていることから Unity のインポートから除外されてしまうため、Unity の Project ウィンドウからこれらサンプルが見えなくなってしまいます。これはライブラリ開発側として困りますし、.meta も生成されないので、一度チルダを外して調整、コミット前にチルダを追加...、などとやる必要が出てきてしまいます。面倒ですしリネームし忘れなどの事故も起きそうですね。

パッケージのみ管理方式

パッケージ部分のみ管理する方式を採用しているプロジェクトもあります。

github.com

これをローカルパッケージとして取り込み利用、外側のプロジェクトは自身の PC 上で開発、という方式ですね。自分はネイティブプラグインの開発プロジェクトも含むケースが多く、Packages の外側にそれを置きたいのでこの方式はしないかな、と判断しました。

Packages からシンボリックリンク方式

どうしようか困っていたら Twitter でご助言をいただきました。

この方式にすると以下のような構造になります。

<Repository>
  ├── Packages
  │   └── com.hecomi.uosc
  │         ├── package.json
  │         ├── Runtime
  │         │    ├── uOSC.asmdef
  │         │    ├── uOscClient.cs
  │         │    └── ...
  │         └── Samples~
  ├── Assets         │
  │   └── uOSC       ↓
  │         └── Samples [~Samples]
  │               ├── Sample 1
  │               ├── Sample 2
  │               ├── Sample 3
  │               └── ...
  ├── ...

チルダを含まない形で Assets にサンプルのディレクトリをインポートできるので、適切に .meta も作成されます。また、ローカル方式でパッケージを作成すると、Project ウィンドウから直接パッケージ内のファイルを修正したり、新たにファイルを追加したりも出来るので通常通り開発が可能です。ただシンボリックリンクを使うと Unity から警告メッセージが表示されます。

Assets/uOSC/Samples is a symbolic link. Using symlinks in Unity projects may cause your project to become corrupted if you create multiple references to the same asset, use recursive symlinks or use symlinks to share assets between projects used with different versions of Unity. Make sure you know what you are doing.

利用者側にはこれは表示されないですし、このケースでは衝突もないので無視してしまってもよいかと思います。

ただ、Package による配布と従来の .unitypackage の配布を両立したい、となってくると問題が出てきます。レガシーな環境や Package Manager を使わない開発をしていたり、インポートしたあとに色々とスクリプトを修正して使いたい人は、以前と同じく .unitypackage で直接プロジェクトに取り込みたいと思うかもしれません(実際はローカルパッケージ化すれば編集できます)。しかしながらこの構造にしてしまうと .unitypackage が作りづらくなってしまいます。.unitypackage は Packages 以下のものを含めてパッケージングすることは出来ないからです。.unitypackage を配布しない場合はお手軽でかなり良い選択肢だと思います。

GitHub Actions で配布用のブランチを作成する方式

こちらの記事ではかなり進んだ内容が紹介されてました。

qiita.com

GitHub Actions を使ってメインブランチにコミットがあった際、そのコミットメッセージに応じて自動的にバージョンを付与、upm ブランチへ git subtree split を使って必要なディレクトリのみ切り出し必要に応じて README.md の移動や Samples~ へのリネームも行い、更に npmjs のリリースまで行うというものです。

自分のパッケージ作成方針

これまでの調査を踏まえて自分なりの構造を考えていきます。自分のやりたいことは以下のような形です:

  1. UPM での動作を確認するため、コアはローカルパッケージの形式で Packages 以下から見える形にしたい
  2. Samples は UPM でインポートする形式にしたいが、開発時のプロジェクトでは開発しやすいように普通にプロジェクトに含まれるようにしたい
  3. UPM 使わない人のために .unitypackage を簡単に作れるようにしたい
  4. できれば 1 リポジトリ、1 プロジェクトで開発したい
  5. ネイティブプラグインの開発も同じリポジトリで管理したい
  6. README.md は 1 つにしたい(GitHub のプロジェクトのルートでの表示用と、パッケージのルートディレクトリの配置用を分けたくない)

前項のいずれの方法を使っても、全部同時に実現するのは難しそうです(今は思いつきませんでした)。色々と検討したのですが、最後の GitHub Actions で自動的に UPM 用のリリースブランチを作成し、そこに必要な構造を作成する方式がとても良さそうに思えました。

元記事ではセマンティックバージョニング含めかなりしっかり作られていますが、自分は結構ゆるく運用してるので次のような感じで行こうかと思います。

  • 1(ローカルパッケージとして開発)を諦める
  • Runtime / Editor / Samples といったフォルダ構成を利用する(Samples~ にはしない)
  • GitHub へバージョン付きの Tag を push した時に、自動的にパッケージ用の構造を作成する(git subtree で必要なファイルだけ切り出す & Samples を Samples~ へリネームするなど
  • OpenUPM や npmjs へのリリースは取り敢えず様子見

これにより、開発は次のような構造にします。

<Repository>
  ├── README.md
  ├── Assets
  │   └── uOSC
  │         ├── package.json
  │         ├── Runtime
  │         │    ├── uOSC.asmdef
  │         │    ├── uOscClient.cs
  │         │    └── ...
  │         └── Samples
  │               ├── Sample 1
  │               ├── Sample 2
  │               ├── Sample 3
  │               └── ...
  ├── ...

ディレクトリ構造だけ変えておきます。.unitypackage はいつもどおり Unity 上で適当なタイミングで手動で作成します。

タグを push したときはそれをイベントとして次のようなブランチを作成します。作成したブランチは upm ブランチには最新を、また upm/v1.2.3 のようにバージョン付きのものも同時にリリースして古いバージョンを使いたい人は使えるようにします。ブランチの構造は git subtree で切り出した上記 uOSC ディレクトリ内のファイル及び必要なファイル(README.mdpackage.json)を配置するようにします。

<Repository>
  ├── README.md
  ├── package.json
  ├── Runtime
  │         ├── uOSC.asmdef
  │         ├── uOscClient.cs
  │         └── ...
  ├─ Samples~
  │         ├── Sample 1
  │         ├── Sample 2
  │         ├── Sample 3
  │         └── ...
  ├── ...

GitHub Actions の作成

環境セットアップ

GitHub Actions の記事は、様々な業種で使われる関係上たくさんの解説がされていますし、公式のドキュメントも充実しています。

docs.github.com

GitHub 上で開発するのは大変なので、自分は act を使ってローカルでワークフローのテストを行いました。

github.com

動作には Docker が必要ですが、昨今は Windows でも Docker Desktop for Windows のセットアップがポチポチするだけで簡単に終わるので導入は難しくありませんでした。

docs.docker.jp

GitHub Actions の作成

次のようなアクションを作成します。

name: Update-UPM-Branch

# v1.2.3 のようなタグがプッシュされたら起動
on:
  push:
    tags:
      - v*

env:
  MAIN_BRANCH: main
  UPM_BRANCH: upm
  PKG_ROOT_DIR: Assets/uOSC
  SAMPLES_DIR: Samples
  DOC_FILES: README.md CHANGELOG.md LICENSE.md

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      # 最新を取得
      - name: Checkout
        uses: actions/checkout@v2
        with:
          fetch-depth: 0
      - run: git checkout main

      # イベントを起動したタグを steps.tag.outputs.name に格納
      - name: Tag name
        id: tag
        run: echo ::set-output name=name::${GITHUB_REF#refs/tags/v}

      # Git の設定
      - name: Git config
        run: |
          git config user.name "github-actions[bot]"
          git config user.email "github-actions[bot]@users.noreply.github.com"

      # UPM 用のブランチを作成
      - name: Create UPM branches
        run: |
          # 古いブランチを削除
          git branch -D $UPM_BRANCH &> /dev/null || echo $UPM_BRANCH branch is not found
          # Assets/uOSC 以下を upm ブランチに切り出す
          git subtree split -P "$PKG_ROOT_DIR" -b $UPM_BRANCH
          # 切り出したブランチに移動
          git checkout $UPM_BRANCH
          # メインブランチの方にあった README などをインポート
          for file in $DOC_FILES; do
            git checkout $MAIN_BRANCH $file &> /dev/null || echo $file is not found
          done
          # Samples ディレクトリを Samples~ に改名
          git mv $SAMPLES_DIR Samples~ &> /dev/null || echo $SAMPLES_DIR is not found
          # Samples.meta だけ別途インポートされるので不要(他の .meta は必要)
          rm Samples.meta
          # package.json のバージョンを置換
          sed -i -e "s/\"version\":.*$/\"version\": \"$TAG\",/" package.json || echo package.json is not foundあ
          # タグ名とともにコミット
          git commit -am "release $TAG."
          # GitHub へ push
          git push -f origin $UPM_BRANCH
          # タグ付きのブランチも作成して push
          git checkout -b $UPM_BRANCH@$TAG
          git push -f origin $UPM_BRANCH@$TAG
        env:
          TAG: ${{ steps.tag.outputs.name }}

これで例えば v1.2.3 を push すると、自動的に upm ブランチと upm@1.2.3 ブランチが作成され、これを https://github.com/hecomi/uOSC.git#upm といった URL で Package Manager 上でインポートできるようになります。

f:id:hecomi:20211026231355p:plain

f:id:hecomi:20211026231328p:plain

meta の追加

ただ、まだ 1 点だけ問題があります。それは README.md の meta ファイルが存在していないので、README.md がインポートされない点です…。

Asset Packages/com.hecomi.uosc/README.md has no meta file, but it's in an immutable folder. The asset will be ignored.

ちょっとお行儀が悪いですが、README.md.metapackage.json.meta の UUID だけ変えて生成する形にしてしまいましょう。

...
for file in $DOC_FILES; do
  git checkout $MAIN_BRANCH $file &> /dev/null || echo $file is not found
  # .meta ファイルを作成(package.json.meta を改変)
  if [ -f $file ]; then
    cp package.json.meta $file.meta
    UUID=$(cat /proc/sys/kernel/random/uuid | tr -d '-')
    sed -i -e "s/guid:.*$/guid: $UUID/" $file.meta
    git add $file.meta
  fi
done
...

これで警告もなくインポートができるようになりました。そのうち Samples~ も自動生成するとかもやりたいです。

Composite Actions 化

さて、1 つのプロジェクトならこれで良いですが複数のプロジェクトでこのコードを全部コピペするのはちょっとしんどいです。バグが見つかったときも直して回る必要があるのも微妙です。

そこで、Composite Action を使ってこの一連の流れを使い回せるようにしてみようと思います。

docs.github.com

先程のコードの envinputs にして外から与えられるようにし、bash スクリプト部分はシェルスクリプトのファイルとして分離しました。この Composite Action は以下のように別のリポジトリを用意しておきます。

github.com

この上でそれぞれのリポジトリ側のアクションを次のように修正します。

name: Update-UPM-Branch

on:
  push:
    tags:
      - v*

jobs:
  update:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v2
        with:
          fetch-depth: 0

      - name: Tag name
        id: tag
        run: echo ::set-output name=name::${GITHUB_REF#refs/tags/v}

      - name: Create UPM Branches
        uses: hecomi/create-upm-branch-action@main
        with:
          git-tag: ${{ steps.tag.outputs.name }}
          pkg-root-dir-path: Assets/uOSC

とても短くなりました!あとはこのアクションを各リポジトリに同じように設定していけば同じコードを使って tag を push するだけでデプロイ出来るようになります。同じ仕組みでやりたい方がいらっしゃいましたらご活用ください。

設定例

本記事のコードでもいくつか出てきた uOSC で使い始めてみました。

github.com

無事動き始めています。

github.com

その他

パッケージ管理用のスクリプトが書ける

  Unity.PackageManager.Client を使うと特定のパッケージをプロジェクトに追加したり、パッケージのソースごとに列挙したりといったことが可能です。

docs.unity3d.com

上記ページではより進んだサンプルとして、ローカルでないパッケージをプロジェクトの Packages フォルダに取り込み、編集可能にするためのスクリプトが紹介されています。

おわりに

Package Manager について調べてみましたが、想定していたよりも便利で、自分のプロジェクトではアセットはすべてこれで管理したほうが良さそうだなぁ、と思うようになりました。同じように思う人のためにも、ポチポチと時間を見つけて配布しているライブラリをメンテしていこうと思います。また、今回は UPM 用のリリースブランチを作成するところまででしたが、後々 OpenUPM への登録、unitypackage の作成も自動化していきたいなぁと思っています。