やらなイカ?

たぶん、iOS/Androidアプリの開発・テスト関係。

#C103 ありがとうございました

コミックマーケット103、無事終わりました。 ご購入いただいた方々、また、スペースにお立ち寄りいただいた方々、準備会やインフラの方々、そしてお手伝いいただいたきみかさん、本当にありがとうございました。

毎回楽しいのですけれど、今回特に楽しくて(サークルとしても買う側としても)、個人的には色々あった一年でしたが、よい締めくくりになりました。

色々お話できたりしたのですけれど、今回さまざまな国のかた*1がスペースに立ち寄ってCopilotとかTDDの話をしていったのが印象的でした。 日本語は読めないけど電子版だと機械翻訳して読めるから嬉しいとか、現金よりカードで決済したいとかも直接聞けて、次回サークル参加するとき考慮したいなと。

おかげさまで多めに刷った新刊『GitHub CopilotとのペアプロTDDでつくるローグライクRPG』も、閉会15分前くらいに完売しました。 新刊は電子版のみ、既刊は電子・物理ともBOOTHで頒布しています。

ikagoya.booth.pm

頒布した書籍の詳細・フォローアップは次の記事を参照してください。

www.nowsprinting.com

www.nowsprinting.com

www.nowsprinting.com

来年は放置気味のOSSをメンテしたりVision Proで遊んだりもしたいので執筆はどうするか未定ですが、なにかネタが沸いたら書くのではないかと。

*1:確認したわけではないですが

GitHub CopilotとのペアプロTDDでつくるローグライクRPG

GitHub Copilotはそのまま使っても便利ですが、その特性を活かす共同作業の形として「ペアプロTDD」がお勧めです。 実際にCopilotとのペアプロTDDで「ローグライクRPG」を開発していく過程を紹介する同人誌を、 コミックマーケット103の2日目(日曜)東U44b「いか小屋」で頒布します。

[12/31追記]電子版(PDF)をBOOTHで頒布しています

ikagoya.booth.pm

想定読者

何らかの言語でのプログラミング経験のあるソフトウェアエンジニア(プログラマー)を想定しています。

サンプルコードにはゲームエンジンUnityおよびC#言語を使用していますが、ゲームエンジン固有の機能には縛られないロジック部分にフォーカスしていますので、Unityの知識は不要です。 Unity固有の機能については、本書を読み進めるのに問題がない程度の解説をつけています。

また、「人間がテストコードを書けばCopilotがプロダクトコード(ゲーム本体の実装)を書いてくれる」というコンセプトなので、ぜひテストエンジニアの方にもお試しいただきたいです。

目次の紹介

第1章 GitHub Copilot

Copilot*1の利用方法、IDEへのプラグインインストールと操作方法*2、Coilotの仕組みを紹介しています。

第2章 ペアプログラミングとTDD

人間が行なうペアプログラミングテスト駆動開発(TDD)それぞれの簡単な紹介と、本書で提唱する「CopilotとのペアプロTDD」について紹介しています。

CopilotとのペアプロTDDには、Copilotの提案を受け入れる判断基準になるテストコードが得られるだけでなく、Copilotから精度の高い提案を引き出す効果があります。 ただし(まだ)万能な方法ではないため、注意すべき点も掲載しています。

第3章 開発の準備

本書サンプルプロジェクトで使用しているゲームエンジンUnityのインストールとプロジェクトの設定方法について、簡単に紹介しています。

第4章 ダンジョンの自動生成

ローグライクRPGの特徴であるランダム生成のダンジョンマップを、CopilotとのペアプロTDDで作っていきます。

本章では、実現したい仕様をテストコードに書く、それをもとにCopilotがプロダクトコードを提案、テストを実行して成功させる、というサイクルをステップ・バイ・ステップで解説しています。

また途中に、Copilotからよりよい提案を受けるためのTipsを盛り込んでいます。

第5章 プレイヤーキャラクター

ダンジョンにプレイヤーキャラクターを表示し、キーボード操作で移動させるまでをCopilotとのペアプロTDDで作っていきます。

それだけ?

「キャラが動くだけでローグライクRPGと呼べるのか」それはごもっともですが、ここまでです。

締切的な問題もありましたが、すでに全体で100ページ、4・5章あわせて60ページあります。 ここまでにペアプロTDD、CopilotのTips、パラメタライズドテスト、ランダム性のテスト、非同期処理のテスト、テストダブル、と書くべきことは書き尽くしました。

書き続けても、淡々とローグライクRPGの実装を進めるだけになり、書くのも読むのもモチベーションが続かないと判断しました*3

ただ、サンプルプロジェクトは今後時間を見つけて実装を進めていくつもりです*4

表紙について

きみかさん(@kimika127)にユニティちゃんを描いていただきました。

なお、背後の黒いのはメンダコです。何かに似て見えるかもしれませんがメンダコです。

C103

2日目(日曜)東U44b「いか小屋」でお待ちしております。 東2ホールの真ん中あたりです。

既刊在庫も持っていきます。

www.nowsprinting.com

www.nowsprinting.com

信販売・電子版について

[12/31追記]電子版(PDF)をBOOTHで頒布しています

ikagoya.booth.pm

*1:個人向けのCopilot Individualのみ解説しています

*2:本書で使用するインライン候補とCopilot Chatのみ解説しています

*3:続刊することは無く、もし改版することがあればもう少し続きを書くかもしれません

*4:ただしCopilotでなくJetBrains AI Assistantに切り替えることになりそうです。比較もしたいので

Unity NuGetとNuGetForUnity

本記事はUnity Advent Calendar 2023 シリーズ2 21日目の記事です。

UnityでNuGet Galleryに公開されている.NET向けパッケージ(以下NuGetパッケージ)を使う場合、nugetコマンドでは管理できないためパッケージ(依存パッケージもすべて)を自分でダウンロードしてDLLを取り出し、Unityプロジェクト下に配置する必要があります。

その手間を省くためのソリューションとして代表的なものに、Unity NuGetとNuGetForUnityがあります。

Unity NuGetのほうが取り扱いは楽な反面、利用できるパッケージに制限があるなど、目的によってはNuGetForUnityのほうが便利なケースもあります。 双方の違いも含めて紹介します。

Unity NuGet

Unity NuGetは、Unityで利用可能なNuGetパッケージをUPMパッケージとしてラップした状態で提供してくれる、UPMパッケージレジストリです。

レジストリは公開しているものをそのまま利用することも、自分の環境やDockerコンテナ上に立ち上げることもできます(コードはオープンソース)。

github.com

Unity 2019.1以降で利用できます。

公開レジストリの利用方法

Package Managerウィンドウを使用する場合

Unityエディター上のPackage Managerウィンドウを使用する場合、次の手順でNuGetパッケージをインストールできます。

  1. UnityエディターのメニューからEdit > Project SettingsでProject Settingsウィンドウを開き、Package Managerタブを選択
  2. Scoped Registriesの下にある+ボタンをクリックし、NameURLScope(s)を入力してSave
  3. Window > Package ManagerでPackage Managerウィンドウを開き、左上にあるレジストリ選択ドロップダウンでMy Registriesを選択
  4. 目的のNuGetパッケージを探して、Installボタンをクリック

Scoped Registriesの設定内容は次のとおりです。

  • Name: Unity NuGet(任意)
  • URL: https://unitynuget-registry.azurewebsites.net
  • Scope(s): org.nuget(Package Manager上の表示を絞り込みたければ細かく指定)

これで、NuGetパッケージの依存関係解決も行われ、必要なDLLがPackages下にUPMパッケージとしてインストールされます。

なお、Unity 2022.2未満の場合、次の操作でAssembly Version Validationを無効化する必要があります(2022.2以降はデフォルトが無効)。

  1. Project Settings > Player > Other Settings > Configurationを開き、Assembly Version Validationチェックボックスをoff

openupmコマンドを使用する場合

openupmコマンドでも次のようにレジストリを指定してインストールできます。

openupm add --registry https://unitynuget-registry.azurewebsites.net パッケージのID

こちらもAssembly Version Validationの設定は必要です。

利用できるパッケージ

Unity NuGetで利用できるパッケージは登録制です。 registry.json ファイルに書かれているパッケージのみ利用できます。

.NET Standard 2.0と2.1のDLLが含まれるパッケージの場合、DLLのmetaファイルにConstraintsを設定してくれるので気にすることなく利用できるはずです。

Roslynアナライザも利用できますが、アナライザをビルドしたときのMicrosoft.CodeAnalysis.CSharpのバージョンに依存するため、インストールするバージョンを指定する必要がある場合もあります。 詳しくは次の記事を参照してください。

www.nowsprinting.com

パッケージの追加方法

利用したいパッケージがregistry.jsonにない場合、追加するPull Requestを送ります。 ただし追加できるパッケージは.NETStandard2.0をサポートしている必要があります。 また依存パッケージがある場合は依存関係すべてを追加する必要があります。

なお、Unityエディター上でしか動作しないパッケージにはdefineConstraints: ["UNITY_EDITOR"]を、Roslynアナライザの場合は"analyzer": trueを追加することを忘れないでください。

次のコマンドでテストを実行すると、依存関係も含めたバリデーションが行われます。

dotnet test src -c Release

ローカル環境でレジストリを起動する

Pull Requestを送る前に、手元でレジストリを起動して確認することもできます。 次のコマンドで起動できます。

cd examples/docker
docker-compose up

ポート5000でアクセスできます。つまりレジストリURLはhttp://localhost:5000です。 またパッケージIDのprefixがorg.nugetでなく、デフォルトではorg.customになります(docker-compose.ymlで変更できます)。

NuGetForUnity

NuGetForUnityは、Unityプロジェクトにインストールして使用するオープンソースのエディター拡張です。 Unityエディター上で直接NuGet GalleryにあるNuGetパッケージをインストールできます。

一時期メンテナンスが止まっていましたが、2023年のはじめごろ再開されました。

github.com

NuGetForUnityのインストールと設定

まずNuGetForUnity自体をUPMパッケージとしてインストールします。

Package Managerウィンドウを使用する場合

Scoped Registriesにhttps://package.openupm.comを追加してopenupmからインストールできます。 細かい手順は割愛します。

openupmコマンドを使用する場合

次のコマンドでインストールできます。

openupm add com.github-glitchenzo.nugetforunity

バージョン管理システムのトラッキング除外設定

NuGetForUnityは、ダウロードして展開したNuGetパッケージの中身を/Assets/Packages/下に置きます。 リポジトリにDLLを入れたくない場合、これを.gitignoreなどでバージョン管理システムのトラッキングから除外しておきます。

ただし、プロジェクトにインストールしているNuGetパッケージの情報は/Assets/packages.configに保存されます。 ケースインセンシティブなOSで/Assets/Packages*と書いてしまうとこちらも除外されますので注意してください。

推奨する.gitignoreの記述は次の記事を参考にしてください。

www.nowsprinting.com

NuGetパッケージのインストール

UnityエディターのメニューからNuGet > Manage NuGet PackagesでNuGet For Unityウィンドウが開きます。 インストールしたいパッケージを検索してInstallで依存関係含めインストールされます。

NuGetForUnityも、Roslynアナライザーに対応しています*1。 Unityプロジェクトで動作する条件についてはUnity NuGetと同様です。

パッケージのリストア

/Assets/Packages/下をバージョン管理していない場合でも、Unityエディターを起動したときに自動的にリストアしてくれます。 ただし、リストア前の時点でコンパイルエラーがあると実行されません。

継続的インテグレーション(CI)などではUnity起動前にリストアをCLIで実行するNuGetForUnity.Cliが利用できます。 dotnetサブコマンドとしてインストールして実行できます*2

www.nuget.org

まとめ

メジャーなパッケージを簡単に使うならUnity NuGetがおすすめです。

Unity NuGetで提供されていないもの、DLLの設定を変えたい、また自作のパッケージをテストするといった用途であれば、NuGetForUnityをおすすめします。

宣伝

年末のコミックマーケット103(C103)にサークル「いか小屋」で出ます。 2日目(日曜日)東U44b、東2の真ん中あたりです。

新刊は『GitHub CopilotとのペアプロTDDでつくるローグライクRPG』。 人間がテストコードを書き、GitHub Copilotがプロダクトコード生成という分担でのペアプログラミングテスト駆動開発(TDD)でローグライクRPGをつくろう!という本です。

既刊の在庫も持っていきます。

www.nowsprinting.com

www.nowsprinting.com

*1:私がやりました

*2:最近追加されたものでまだちゃんと検証していないのですが紹介だけしておきます

自動テストにおける「目視確認」をポジティブにこなす魔法

本記事はソフトウェアテスト Advent Calendar 2023 2日目の記事です。

自動テストを書いていて、テストコードで検証目的を達成できず「目視で確認」とするのは負けた気持ちになりますし、テストを実行するオペレーターやコントリビューターのモチベーションも下がります。 特に、技術的には可能であってもROIが悪くて予算・納期的に不可能なときはなおさらです。

そんなとき、目視確認を指示するコメントに次の言葉を添えてみましょう。 たちまち罪悪感は消え*1、またこれを見たオペレーターも高いモチベーションで取り組んでくれることでしょう*2

日本語の例

  • 君の目で確かめてくれ!
  • 目撃せよ!

実際のテストコードに適用するとこうなります*3

[Test]
public void TakeScreenshot_Gizmoあり_スクリーンショットにGizmoが写っていること()
{
  _sut.TakeScreenshot(gizmo: true);

  Assert.That(new FileInfo(path), Has.Length.GreaterThan(FileSizeThreshold));
  // Gizmoは写っているか? ぜひ君の目で確かめてくれ!
}

英語の例

  • See for yourself! (君自身で確かめてくれ!)
  • See it with your own eyes! (君の目で確かめてくれ!)
  • Be a witness! (目撃者になれ!)
  • Don’t just read a test report. Witness the drama unfold before you! (テストレポートを読むだけではだめだ。目の前でドラマが展開するのを目撃するのだ!)

1960年代の番宣とかで言ってそう…ということでChatGPT(GPT-4 with DALL・E)に生成したもらった画像がこちら。

受け取り方には個人差がありますし、コンテキストにもよるでしょう*4

実際のプロジェクトで使ってみたらどうなるか、それは君の目で確かめてくれ!

宣伝

年末のコミックマーケット103(C103)にサークル「いか小屋」で出ます。 2日目(日曜日)東U44b、東2の真ん中あたりです。

新刊は『GitHub CopilotとのペアプロTDDでつくるローグライクRPG』。 人間がテストコードを書き、GitHub Copilotがプロダクトコード生成という分担でのペアプログラミングテスト駆動開発(TDD)でローグライクRPGをつくろう!という本です。

既刊の在庫も持っていきます。

www.nowsprinting.com

また、新刊と同名のセッションを来週のソフトウェアテスト自動化カンファレンス2023でやります。 まだ参加できますので、ぜひお申し込みください!

testautomationresearch.connpass.com

*1:個人差があります

*2:個人差があります

*3:最低限、一定サイズ以上のファイルが生成されていることだけはアサーションしています

*4:私も複数回出てきたらイラッとすると思います…

Unity Test Frameworkで利用できるカスタム属性の実装方法

Unity Test Framework および NUnit では、テストコードに付与できるさまざまな属性(attribute)が提供されています。 また拡張ポイントであるインターフェイスを実装することで、プロジェクト独自のカスタム属性を定義してテストコードで使うこともできます。

本記事では、オープンソースの Test Helper パッケージにいくつかの汎用的なカスタム属性を実装するにあたって得た知見を紹介します。

github.com

本記事で紹介するインターフェイスは次の4つです。 先頭から3つが NUnit 由来のもの、最後のひとつが Unity Test Framework 独自のものです。

検証環境は次のとおりです。

  • Unity 2022.3.11f1 *1
  • Unity Test Framework v1.3.4 *2

カスタム属性実装の基本

カスタム属性は次のように定義します。 NUnitAttribute クラスを継承し、役割に応じたインターフェイスを実装(ここでは IApplyToContext)しています。

public class MyAttribute : NUnitAttribute, IApplyToContext
{
    private readonly string _message;

    public MyAttribute(string message) => this._message = message;

    public void ApplyToContext(ITestExecutionContext context)
    {
        Debug.Log(message);
    }
}

この属性を次のようにテストメソッドに付与すると*3、テストメソッドの実行直前に Test start! とログ出力されます。

[TestFixture]
public class MyTestClass
{
    [Test]
    [MyAttribute("Test start!")]
    public void MyTestMethod()
    {
        Assert.That(actual, Is.EqualTo(expected));
    }
}

インタフェースの解説

4つのインターフェイスの使用方法を順に解説します。

IApplyToTest

IApplyToTest インターフェイスを実装した属性は、テストランナーが実行するテストを決めるときに ApplyToTest メソッドが呼ばれます。 ここでテストコンテキストの RunState を変化させることで、テストの実行可否を制御できます。

次のコードは、付与されたテストを実行から除外するカスタム属性の例です。

public class MyIgnoreAttribute : NUnitAttribute, IApplyToTest
{
    void IApplyToTest.ApplyToTest(Test test)
    {
        test.RunState = RunState.Ignored; // RunStateを変更して実行されないようにする
    }
}

Unity Test Framework の Category 属性、Ignore 属性などはこのインターフェイスを実装しています。

Test Helperパッケージでは、次のカスタム属性で使用しています。

実装にあたっては、次の点に注意してください。

  • 属性の中で例外を発生させてはいけません。Test Runnerウィンドウで(スキップでなく)エラーと表示され、テスト実行結果の件数からも除外されます*4
  • 実行中のテストコンテキストを NUnit.Framework.TestContext から取得できません。引数で渡される test インスタンスを使います
  • テストメソッド実行直前だけでなく、Test Runnerウィンドウの表示更新の契機で呼ばれます。そのため、プロジェクトの設定を一時的に変更するといった用途では使用できません(変更を戻す契機がないため)

IApplyToContext

IApplyToContext インターフェイスを実装した属性は、付与されたテストの実行前に ApplyToContext メソッドが呼ばれます。 引数で渡されるテストコンテキストに変更を加えたり、何かしらの前処理を実行できます(ただし同期処理に限る)。

次のコードは、付与されたテスト実行直前にテスト名称をログ出力するカスタム属性の例です。

public class MyLoggingAttribute : NUnitAttribute, IApplyToContext
{
    public void ApplyToContext(ITestExecutionContext context)
    {
        Debug.Log($"{context.CurrentTest.Name} start!");
    }
}

Unity Test Framework の Timeout 属性はこのインターフェイスを実装しています。

Test Helperパッケージでは、次のカスタム属性で使用しています。

実装にあたっては、次の点に注意してください。

  • 属性の中で例外を発生させてはいけません。テストが無限ループします
  • 実行順は、OneTimeSetUpより後、UnitySetUp および SetUp よりも前です

ICommandWrapper

ICommandWrapper は直接実装するのではなく、サブインターフェイスである IWrapSetUpTearDown もしくは IWrapTestMethod を実装します。

テスト実行の前後に処理を行なうための属性ですが、Unityでは次の制限事項があります。 重めの問題があるため、Unityでは後述の IOuterUnityTestAction を使用することをおすすめします*5

  • テストメソッドに付与したときしか動作しません(テストクラス、テストアセンブリはNG)
  • 同期処理しか書くことができません
  • 属性の中で例外を発生させてはいけません。テストが無限ループします
  • 非同期テスト(asyncおよびUnityTest)に付与すると、実行時エラーや無限ループを引き起こします
  • IWrapSetUpTearDown を付与したテストに async SetUp メソッドがあると、無限ループを引き起こします
  • IWrapSetUpTearDown の実行順は、OneTimeSetUp および UnitySetUp より後、SetUp よりも前です *6
  • IWrapTestMethod の実行順は、SetUp の後、テストメソッドの前です
  • 引数で渡される TestCommand から取得できるテストコンテキストは、テストケースではなくテストフィクスチャ(テストクラス)のものです

Unity Test Framework の MaxTime 属性、Repeat 属性、Retry 属性は IWrapSetUpTearDown を実装しています。つまり上記の問題があるということです。 詳しくは次の記事を参照してください。

www.nowsprinting.com

IOuterUnityTestAction

IOuterUnityTestAction は、NUnit由来のものでなくUnity独自のインターフェイスです。 このインターフェイスを実装した属性は、付与されたテストの実行前に BeforeTest メソッド、実行後に AfterTest メソッドが呼ばれます。

BeforeTestUnitySetUp より前に呼ばれる、例外を投げても無限ループしないでテストが失敗してくれるなど、とても高品質なインターフェイスです*7

次のコードは、付与されたテスト前後にテスト名称をログ出力するカスタム属性の例です。

public class MyLoggingAttribute : NUnitAttribute, IOuterUnityTestAction
{
    public IEnumerator BeforeTest(ITest test)
    {
        Debug.Log($"{test.Name} start!");
        yield return null;
    }

    public IEnumerator AfterTest(ITest test)
    {
        if (TestContext.CurrentTestExecutionContext.CurrentResult.PassCount > 0)
        {
            Debug.Log($"{test.Name} success!");
        }
        else
        {
            Debug.Log($"{test.Name} failure!");
        }
        yield return null;
    }
}

Test Helperパッケージでは、次のカスタム属性で使用しています。

以下、補足です。

  • テストメソッドに付与したときしか動作しません(テストクラス、テストアセンブリはNG)
  • 実行順は、OneTimeSetUp -> BeforeTest -> UnitySetUp -> SetUp -> Test -> TearDown -> UnityTearDown -> AfterTest -> OneTimeTearDown
  • BeforeTest 内で何かしらの例外をスローすると、テスト本体は実行されず失敗扱いとなります
  • AfterTestは、テストの成否に関わらず必ず実行されます
  • AfterTest 内で例外をスローすると、テスト本体が成功していてもテストを失敗させることができます
  • AfterTest 内でテストの成否を判定するには、上例のようにテストコンテキストの PassCountFailCount を参照します

カスタム属性を実装するにあたってのTips

いくつか紹介します。

属性を付与できるシンボルを限定したい

属性の定義に AttributeUsage 属性を付与することで、属性を付与できるシンボルを限定できます。 例えばメソッドにしか付与できないようにするには、次のように書きます。

[AttributeUsage(AttributeTargets.Method)]
public class MyAttribute : NUnitAttribute, IOuterUnityTestAction {}

属性が付与されたシンボルを検索したい

テストに限らず、属性が付与されたシンボルを収集して処理したい場合、Unityエディター内ではリフレクションでなく TypeCache クラスのメソッドを使うと高速に処理できます。

次のコードは、LoadSceneAttribute が付与されたメソッドを返すメソッドの例です。

private static IEnumerable<LoadSceneAttribute> FindLoadSceneAttributesOnMethods()
{
    var symbols = TypeCache.GetMethodsWithAttribute<LoadSceneAttribute>();
    foreach (var attribute in symbols
                 .Select(symbol => symbol.GetCustomAttributes(typeof(LoadSceneAttribute), false))
                 .SelectMany(attributes => attributes))
    {
        yield return attribute as LoadSceneAttribute;
    }
}

テストから属性に情報を渡したい

属性のコンストラクタに引数を定義できます。 本記事の最初のコード例を参照してください。

なお、上例では属性のフィールドに値を保持していますが、テストコンテキストの Properties に保持することもできます(後述)。

また NUnitAttribute の代わりに PropertyAttribute も利用できます。

属性からテストに情報を渡したい

属性の処理内で得た何かしらの情報をテストメソッドに渡したい場合、テストコンテキストの Properties を使用できます。 次のように使用します。

情報のセット

public class PassesObjectAttribute : NUnitAttribute, IApplyToContext
{
    public void ApplyToContext(ITestExecutionContext context)
    {
        MyClass obj = // snip

        if (obj != null)
        {
            test.Properties.Add("PassesMyClass", obj); // nullをセットしないよう注意
        }
    }
}

情報の取り出し

[TestFixture]
public class MyTestClass
{
    [Test]
    [PassesObject]
    public void MyTestMethod()
    {
        var obj = TestContext.CurrentTestExecutionContext.CurrentTest
            .Properties.Get("PassesMyClass") as MyClass;

        Assert.That(obj, Is.Not.Null);
    }
}

なお、プロパティに null をセットしてしまうと、バッチモードでテスト実行したときにテスト全体がエラーとなります。 テストレポート出力時に次の例外を処理されていないためです。

Uploading Crash Report
NullReferenceException: Object reference not set to an instance of an object
  at NUnit.Framework.Internal.PropertyBag.AddToXml (NUnit.Framework.Interfaces.TNode parentNode, System.Boolean recursive) [0x00054] in <956b82cfdef641c6bc6a0e5b19798f05>:0 
(snip)

その他の Unity Test Framework 拡張ポイント

本記事で紹介したカスタム属性以外にも、カスタム Comparer やカスタム制約といった拡張が可能です。 詳しくは『Unity Test Framework完全攻略ガイド』を参照してください。

www.nowsprinting.com

*1:テストは 2019 LTS, 2020 LTS, 2021 LTS, および 2023.1.16f1 でも実行しています

*2:正常系は最新のv1.3.9でも動作しますが、異常系の振る舞いが異なります

*3:テストクラス、テストアセンブリにも付与できます

*4:Test Frameworkのバグですが修正されないでしょう

*5:ただし IOuterUnityTestAction は IWrapSetUpTearDown の代替になりますが、IWrapTestMethod に代わるインターフェイスはありません

*6:UnitySetUp はほかにもドメインリロードで再実行されないなどの癖があります。どうして…

*7:冷静に考えると嫌味っぽいですが、検証したとき正直感動したのです…

Unity製ゲームをWebGLビルドしてGitHub Pagesに公開するワークフロー

Unityプロジェクトのバージョン管理にGitHubを使用している場合、GitHub ActionsワークフローでWebGLビルドを行ない、そのままGitHub Pagesに公開する方法を紹介します。

GitHub Pagesは、GitHubリポジトリに紐づく静的コンテンツをホスティングできる機能です。 公開できるサイズが10GB以下ということと、たとえprivateリポジトリであってもGitHub Pagesにはアクセス制限はかけらない*1という制限がありますので注意してください。

Unity - Builder アクション

ビルドには、GameCIの提供するUnity - Builder アクションを使用しています。 利用方法は公式サイトか、検索すれば日本語情報もありますので割愛します。

github.com

なお、CIをキックするユーザーはUnityのライセンスを持っている必要があります*2。publicなリポジトリで使用する場合は特に注意してください*3

また、現在パーソナルライセンスのCLIによる新規アクティベーションは塞がれています。次のIssueを参照してください。

alf->ulf license activation no longer possible for personal licenses · Issue #408 · game-ci/documentation · GitHub

GitHub Pages

GitHub Pagesで公開するコンテンツは、リポジトリの特定ブランチにあらかじめチェックインするか、GitHub Actionsによってアーティファクトにアップロードしたものを使用できます。

後者であれば、リポジトリにビルド成果物をチェックインする必要がなく便利です。 本例ではこの方法を紹介します。

まず、GitHubリポジトリSettings > Pagesを開き、SourceはGitHub Actionsを選択します。

すると、リポジトリのEnvironmentsにgithub-pagesが追加されます。 あとはワークフローで、WebGLビルド出力をtarで固めてgzipしたものをgithub-pagesという名前でアーティファクトにアップロードすることで、動作するゲームが https://OWNER.github.io/REPO/ で公開されます。

なお、GitHub Pagesで公開するWebGLは、Compression FormatをDisabledに設定しなければなりません。公開は行われますが実行時に次のエラーが出ます。

Unable to parse Build/WebGL.framework.js.gz! This can happen if build compression was enabled but web server hosting the content was misconfigured to not serve the file with HTTP Response Header "Content-Encoding: gzip" present. Check browser Console and Devtools Network tab to debug.

しかし、ゲームをunityroomで公開する場合、Compression FormatはGzipに設定する必要があります。 これを手動で切り替えるのは事故の元ですので、ワークフロー内でビルド前に書き換えています。

詳しくは、下記実装例を参照してください。

ワークフロー実装例

次のワークフローをリポジトリの.github/workflows下に置くことで、masterブランチにpush(もしくはPRをマージ)されるたびにWebGLビルドが実行され、GitHub Pagesで公開されます。

name: Deploy to GitHub Pages

on:
  push:
    branches:
      - master
  # masterブランチにpushされるたびにWebGLビルドして公開。条件はプロジェクトのニーズによって調整してください

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true
  # 先行ジョブがあればキャンセル

jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      contents: read

    steps:
      - uses: actions/checkout@v4

      - name: Set CompressionFormat to Disabled
        run: |
          sed -i -e 's/webGLCompressionFormat: ./webGLCompressionFormat: 2/' ProjectSettings/ProjectSettings.asset
        # Compression Formatを`Disabled`に書き換え

      - uses: game-ci/unity-builder@v3
        with:
          targetPlatform: WebGL
          allowDirtyBuild: true # 上でProjectSettings.assetを変更しているため指定
        env:
          UNITY_LICENSE: ${{ secrets[UNITY_LICENSE_2022] }}

      - uses: actions/upload-pages-artifact@v2
        with:
          path: build/WebGL/WebGL
        # WebGLビルド出力をアーティファクトにアップロード

  deploy:
    runs-on: ubuntu-latest
    needs: build
    permissions:
      id-token: write
      pages: write
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}

    steps:
      - name: Deploy to GitHub Pages
        id: deployment
        uses: actions/deploy-pages@v2
        # GitHub Pagesに公開

*1:GitHub Enterprise契約であればできます

*2:Proライセンスが必要なプロジェクトであれば全員Proライセンスが必要です

*3:public forkからのPull Requestの実行を抑止することで不特定ユーザーによるキックを防ぐことはできます。ただしこれもUnity社のお墨付きというわけではないので保証の限りではありません

#C102 ありがとうございました

コミックマーケット102、無事終わりました。 ご購入いただいた方々、また、スペースにお立ち寄りいただいた方々、ありがとうございました。

頒布した書籍の詳細・フォローアップは次の記事を参照してください。

www.nowsprinting.com

www.nowsprinting.com

いずれも、BOOTHで電子版および物理本を頒布しています。買いそびれた方はご利用ください。

ikagoya.booth.pm

またTAROMAN本はメロンブックスさんにも委託あります。 https://www.melonbooks.co.jp/detail/detail.php?product_id=2037143

今回、強気で多めに刷ったので、だいぶ在庫あります。 強気の分だけ丸々残っている感。 Unity本などと比べて情報が風化しにくいはずなので、しばらく既刊として持っていくつもりです。

ところで、今回はポスターも用意しましたが、やっぱり縦に使うべきだったかな、という反省。