はなちるのマイノート

Unityをメインとした技術ブログ。自分らしくまったりやっていきたいと思いますー!

【Unity】UI ToolkitのScrollViewを強制的に再描画させる方法

はじめに

UI Toolkitを使用していて、ScrollViewScrollbarを再描画したいと思い以下のコードを書きました。

var scrollView = uxml.Q("SampleScrollView");
scrollView.MarkDirtyRepaint();

割と名前から推測してしまっていますが、これだと特に動作しないようです。

なかなか情報が見つからず大変でしたが、解決方法を見つけたので書き残しておきたいと思います。

解決方法

以下のメソッドを定義して呼び出せばOKです。

private static void ForceUpdate(ScrollView view)
{
    view.schedule.Execute(() =>
    {
        var fakeOldRect = Rect.zero;
        var fakeNewRect = view.layout;

        using var evt = GeometryChangedEvent.GetPooled(fakeOldRect, fakeNewRect);
        evt.target = view.contentContainer;
        view.contentContainer.SendEvent(evt);
    });
}

https://discussions.unity.com/t/how-to-refresh-scrollview-scrollbars-to-reflect-changed-content-width-and-height/876749/16

【Unity2024 Advent Calendar】Unityのコードメトリクスを可視化するための分析基盤の作り方(CodeCoveragePackage + coverlet.collector + octocov + BigQuery + Looker Studio)

はじめに

こちらはUnity2024 Advent Calendarの12/20記事になります。是非他の方の記事もチェックしてみてください。
qiita.com

今回はUnityでのコードメトリクスを可視化するための分析基盤の作り方について紹介したいと思います。

PRのコメントでコードメトリクスを表示してくれる
コードメトリクスの遷移を可視化した様子

Code Coverageとは

Code Coverageとはテストで用いられるコードがどれだけ実行されたかを示す指標です。テストケースがあまり網羅されていないコードを検出することに役に立ちます。

コード網羅率(コードもうらりつ、英: Code coverage、コードカバレッジ)は、ソフトウェアテストで用いられる尺度の1つである。プログラムのソースコードがテストされた割合を意味する。この場合のテストはコードを見ながら行うもので、ホワイトボックステストに分類される。

コード網羅率は体系的なソフトウェアテストのための技法として最初に生み出されたものの1つである。1963年の Communications of the ACM にある Miller と Maloney の論文に言及されているのが最初である。

コード網羅率 - Wikipedia

構成概要

具体的には以下の構成で構築しました。

構成図

主に以下を利用しています。

  • CodeCoverage Package
  • coverlet.collector
  • octocov
  • BigQuery
  • Looker Studio

それぞれについては後述します。

Unity公式パッケージ「CodeCoverage」

Unityの公式パッケージであるCode Coverage Packageを利用することで、コードカバレッジを取得することができます。
docs.unity3d.com

またUnityに依存していないコードについてはdotnet testによる計測ができないかは一考の余地があると思います。Unityに依存するコードは面倒くさいことが多くイテレーションも遅いので、なるべくUnityに依存するコードとUnityに依存しないコードをアセンブリレベルで分離したほうが良いでしょう。

dotnet testを用いたコードカバレッジの収集方法は下記に記載しています。
www.hanachiru-blog.com

セットアップ方法

Package Managerからインストールします。Add Package from git URL...からcom.unity.testtools.codecoverageと入力すればOKです。

UPMでcom.unity.testtools.codecoverageをインストール

実行方法

コードカバレッジはCIで収集するケースがほとんどだと思うので、Unityのバッチモードでテストを実行 + カバレッジ収集を行います。
Using Code Coverage in batchmode | Code Coverage | 1.2.6

# MacでUnity 6000.0.23f1の場合
# generateAdditionalReportsを付与することで、SonarQube, Cobertura and LCOV形式で出力する
$ /Applications/Unity/Hub/Editor/6000.0.23f1/Unity.app/Contents/MacOS/Unity  \
            -projectPath <path-to-project-path> \
            -batchmode \
            -testPlatform editmode \
            -runTests \
            -debugCodeOptimization \
            -enableCodeCoverage \
            -burst-disable-compilation \
            -coverageResultsPath <path-to-codecoverage-result> \
            -coverageOptions "generateAdditionalReports;"

↓実際に実行した例

$ tree
<path-to-codecoverage-result>
├── <ProjectName>-opencov
│   └── EditMode
│       ├── TestCoverageResults_0000.xml
│       └── TestCoverageResults_0001.xml
└── Report
    ├── Cobertura.xml
    ├── SonarQube.xml
    ├── Summary.json
    ├── Summary.md
    ├── Summary.xml
    └── lcov.info
アセンブリをフィルタする

コードカバレッジを計測するアセンブリを絞りたいことはよくあると思います。assemblyFiltersを用いることで含めるアセンブリを絞ることができます。+は含めて、-は除外です。

# assemblyFiltersにより計測するアセンブリをフィルタする
# "Hoge."でアセンブリ名が始まるものを対象にし、"Hoge.Samples."でアセンブリ名が始まるものは計測対象外にする(Globにより指定)
$ /Applications/Unity/Hub/Editor/6000.0.23f1/Unity.app/Contents/MacOS/Unity  \
            -projectPath . \
            -batchmode \
            -testPlatform editmode \
            -runTests \
            -debugCodeOptimization \
            -enableCodeCoverage \
            -burst-disable-compilation \
            -coverageResultsPath <path-to-codecoverage-result> \
            -coverageOptions "generateAdditionalReports;assemblyFilters:+Hoge.*,-Hoge.Samples.*"

またpathFiltersなどもありますので、気になる方は公式ドキュメントを参照してください。

HTML Reportを生成する

CodeCoverage Packageでは下記のようなHTMLレポートを生成することができます。具体的にどのLineがテスト実行されていないか調べるケースでは有用なので、GitHubのArtifactやGCSなどにあげてPRを送ったタイミングでみれるようにするとかはアリかなと思います。

公式ドキュメントより引用
# generateHtmlReportを追加する
$ /Applications/Unity/Hub/Editor/6000.0.23f1/Unity.app/Contents/MacOS/Unity  \
            -projectPath . \
            -batchmode \
            -testPlatform editmode \
            -runTests \
            -debugCodeOptimization \
            -enableCodeCoverage \
            -burst-disable-compilation \
            -coverageResultsPath <path-to-codecoverage-result> \
            -coverageOptions "generateAdditionalReports;generateHtmlReport"

octocovを利用してGitHubに通知を行う

octocovコードメトリクスを収集するためのツールキットです。今回は GitHubにコードメトリクスをコメントで通知する + Big Queryにデータをアップロードするために利用します。

octocov is a toolkit for collecting code metrics (code coverage, code to test ratio, test execution time and your own custom metrics).

// DeepL翻訳
octocovは、コード・メトリクス(コード・カバレッジ、コード対テスト比、テスト実行時間、独自のカスタム・メトリクス)を収集するためのツールキットです。

github.com

カバレッジレポートのフォーマットは沢山あるのですが、octcovは以下に対応しています。

  • Go coverage
  • LCOV
  • SimpleCov
  • Clover
  • Cobertura
  • JaCoCo

UnityのCodeCoverageLCOVを吐けるので、LCOVを利用すると良いでしょう。またcoverlet.collectorを利用したdotnet testでもLCOVを吐けます。

設定ファイルの作成

octocovには設定ファイルを利用することで様々な設定ができます。場所はどこでも良いのですが.octocov.ymlファイルを作成して中身を記述してください。

詳細はreadmeを見てみて欲しいのですが、私がよく利用する設定を載せておきます。
github.com

# .octocov.yml

# lcov.infoを読み込む(UnityのCode Coverage Packageで出力先をCodeCoverageに設定する)
coverage:
  paths:
    - CodeCoverage/Report/lcov.info
      
# テストの実行時間として利用するsteps (Actions側でnameを'Unity Test'にしている場合)
testExecutionTime:
  steps:
    - 'Unity Test'

# codeとtestの割合を計算するのに利用するファイル
codeToTestRatio:
  code:
    - '**/*.cs'
    - '!**/Tests/**/*.cs'
  test:
    - '**/Tests/**/*.cs'
      
# プルリクエストにコメントをつける
comment:
  if: is_pull_request
  hideFooterLink: true

# JobのSummaryを表示する
summary:
  if: true
  
# 差分を表示するためにArtifactを利用(後ほどBig Queryに置き換える)
report:
  if: is_default_branch
  datastores:
    - artifact://${GITHUB_REPOSITORY}/dotnet-test-report
      
diff:
  datastores:
    - artifact://${GITHUB_REPOSITORY}/dotnet-test-report

GitHub Actionsのワークフロー作成

Unityでテストを実行して、octocovでPRにコメントをするワークフローを作成します。

# .github/workflows/test.yml
name: test
on:
  push:
    branches: main
  pull_request:
    branches: main
  workflow_dispatch:

jobs:
  test:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      pull-requests: write
      actions: read
      checks: write
    env:
      LCOV_DIR: ${{ github.workspace }}/CodeCoverage
    steps:
      - uses: actions/checkout@v4
      - name: Unity Test
        step:
          run: |
            # ここで下記のようにUnityをバッチモードで立ち上げてテストを実行しなければならない
            # 以下の書き方だと動作しないので注意してください
            /Applications/Unity/Hub/Editor/6000.0.23f1/Unity.app/Contents/MacOS/Unity  \
              -projectPath . \
              -batchmode \
              -testPlatform editmode \
              -runTests \
              -debugCodeOptimization \
              -enableCodeCoverage \
              -burst-disable-compilation \
              -coverageResultsPath "$LCOV_DIR" \
              -coverageOptions "generateAdditionalReports;"
      - uses: k1LoW/octocov-action@v1
        with:
          config: .octocov.yml
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          GITHUB_REPOSITORY: ${{ github.repository }}

ただしUnityを用意しないといけないのですが、SelfHosted Runnerを個人で用意するのはかなり大変です。そこで利用させていただくのがGame CIです。
www.hanachiru-blog.com

Game CIの利用

Game CI側でUnityを用意してくれているので、それをありがたく利用させていただきます。セットアップの仕方は先ほど貼った記事に書いてあるので省くのですが、game-ci/unity-test-runner@v4を使う際にinputscoverageOptionsを用意してくれています。

github.com
Code Coverage Options With Combined Coverage Results · Issue #181 · game-ci/unity-test-runner · GitHub
github action check status is "neutral" when tests pass · Issue #220 · game-ci/unity-test-runner · GitHub

# .github/workflows/test.yml
name: test
on:
  push:
    branches: main
  pull_request:
    branches: main
  workflow_dispatch:

jobs:
  test:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      pull-requests: write
      actions: read
      checks: write
    steps:
      - uses: actions/checkout@v4
      - name: Unity Test
        uses: game-ci/unity-test-runner@v4
        env:
          UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
          UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
          UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
        with:
          githubToken: ${{ secrets.GITHUB_TOKEN }}
          unityVersion: 6000.0.23f1
          # PlayModeとEditModeのTestを実行
          testMode: all
          # CodeCoverageの出力先を指定できないよう(-coverageResultsPathでも制御できなかった)なので、CodeCoverage/Report/lcov.infoに固定で出力されているので使う
          coverageOptions: generateAdditionalReports;dontClear
          customParameters: -debugCodeOptimization -enableCodeCoverage -burst-disable-compilation
      - uses: k1LoW/octocov-action@v1
        with:
          config: .octocov.yml
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          GITHUB_REPOSITORY: ${{ github.repository }}      

またaction.ymlのoutputにcoveragePathを用意してくれているので、それを使うこともできます。ただoctcov側に環境変数を渡せなかった(多分)ので、CodeCoverage/Report/lcov.infoを直書きしてます。また-coverageResultsPathを用いても出力先を制御できなさそうでした。
github.com

# 利用する例
- uses: actions/upload-artifact@v3
  if: always()
  with:
    name: Coverage results
    path: ${{ steps.myTestStep.outputs.coveragePath }}

Test runner | GameCI

動作している様子

正しく動作していれば、PRを作成すると以下のような表示がなされるはずです。

PRのコメントでコードメトリクスを表示してくれる

BigQueryの構築

BigQueryはGCPが提供するデータ分析プラットフォームです。今回はコードメトリクスデータを保存するために利用します。

GCPのサービスアカウントの作成

octocovからBig Queryへコードメトリクスデータを送るにあたって、まずはサービスアカウントを作成する必要があります。
cloud.google.com

権限をどうするかはちゃんとご自身で調べて欲しいですが、私の場合は以下を利用しました。

  • roles/bigquery.jobUser
  • roles/bigquery.dataEditor

https://cloud.google.com/bigquery/docs/access-control?hl=ja


またどんな権限が必要かはoctcovのreadmeに載っています。

  • bigquery.datasets.get
  • bigquery.tables.get
  • bigquery.tables.updateData
  • bigquery.jobs.create
  • bigquery.tables.getData

https://github.com/k1LoW/octocov?tab=readme-ov-file#bigquery
https://github.com/k1LoW/octocov?tab=readme-ov-file#use-bigquery-table-as-datastore

サービスアカウントが無事作成できたら、鍵(JSON)を作成しておきましょう。

鍵を作成している様子

作成できたら、GitHubのsecretsにGOOGLE_APPLICATION_CREDENTIALS_JSONなどとして入れておきましょう。

Big Queryであらかじめデータセットを用意しておく

GCPのBigQuery上にあらかじめデータセットを用意しておきます。コンソール上でも良いですし、コマンドでも好きなように作成してください。

データセットを用意している様子

テーブルは後ほど作成するので、自身で定義しなくてOKです。

作成したテーブルに情報を追加していくように、先ほどGitHub Artifactにあげるように設定した.octocov.ymlをBig Queryにあげるように修正します。bq://[project ID]/[dataset ID]/[table]のように指定してください。

# .octocov.yml

...
# 差分を表示するためにBig Queryを利用する
# bq://[project ID]/[dataset ID]/[table]
report:
  if: is_default_branch
  datastores:
    - bq://hogehogehoge/octocov_sandbox/reports

diff:
  datastores:
    - bq://hogehogehoge/octocov_sandbox/reports

tableの名前は好きに決めちゃってしまってOKです。

またGitHub上で実行するためにはGOOGLE_APPLICATION_CREDENTIALS_JSONを先ほど取得したサービスアカウントの鍵として設定する必要があります。

- uses: k1LoW/octocov-action@v1
  with:
    config: .octocov.yml
  env:
    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
    GITHUB_REPOSITORY: ${{ github.repository }}
    GOOGLE_APPLICATION_CREDENTIALS_JSON: ${{ secrets.GOOGLE_APPLICATION_CREDENTIALS_JSON }}

https://github.com/k1LoW/octocov?tab=readme-ov-file#bigquery

Big Queryにてテーブルのmigrateをする

migrateするにあたって、CLIツールをインストールしておきます。

$ brew install k1LoW/tap/octocov

BigQueryを活用するためにはテーブルのmigrateが必要になります。.octocov.ymlがカレントディレクトリに含まれる状態で以下のコマンドを叩きます。

$ octocov migrate-bq-table

また環境変数等の設定も必要なので、雑に以下のようなPowerShellを用意しておきました。(私は生粋のPowerShell教なので、bash/zshは書きません)

#!/usr/bin/env pwsh
#Requires -Version 7.4

$PSNativeCommandUseErrorActionPreference = $true
$ErrorActionPreference = "Stop"

$env:GOOGLE_APPLICATION_CREDENTIALS_JSON = @"
{
  "type": "service_account",
  ...
}
"@

octocov migrate-bq-table


具体的なSchemaは下記に載っています。
octocov/docs/bq/schema/reports.md at main · k1LoW/octocov · GitHub

Looker Studioにより可視化する

Looker Studioを用いることで、簡単にBig Queryに保存したデータの可視化をすることができます。詳細は書きませんが、直感的にわかるように作られているのでボタンをポチポチしていたらできると思います。

cloud.google.com

データのレポートへの追加からBigQueryのテーブルを選択する
コードメトリクスの遷移を可視化した様子

結果

PRを出す度にテスト + コードメトリクス計測を行い、その結果をGitHub上で表示してBigQueryにあげることができました。またその推移をLooker Studio上で確認することができます。さらにHTMLレポートもどこかにホスティングしていい感じにみれるようにすれば、どのLineが具体的にテストが通っていないかの確認もできます。

PRのコメントでコードメトリクスを表示してくれる
コードメトリクスの遷移を可視化した様子

考察

分析基盤を構築した後、これをどうやって活用していくのかもとても重要な要素かと思います。正直私自身もまだ模索している中です。

特にゲームクライアントだとテスト自体あまり活用されていないことも多々あるのかなと思うので、是非上手に活用してみてください。

【Unity】com.unity.search.extensionsに依存しているとEditMode TestがFailする話

はじめに

先日Unityが公開している(といってもOfficialではない)パッケージであるcom.unity.search.extensionsに依存しているプロジェクトのEditMode TestがFailしてしまうことに気が付きました。

EditMode Testが失敗している様子
ValidateCustomIndexation(t:shader sh_rendertype=opaque [Assets/Materials/SurfaceShader.shader] => 1 ) (0.013s)
---
  Query t:shader sh_rendertype=opaque yielded 0 expected was 1
  Expected: 1
  But was:  0

---
at CustomIndexationTests+<ValidateCustomIndexation>d__2.MoveNext () [0x00105] in Library/PackageCache/com.unity.search.extensions/Indexing/CustomIndexationTests.cs:77
at UnityEngine.TestTools.TestEnumerator+<Execute>d__7.MoveNext () [0x0004e] in Library/PackageCache/com.unity.test-framework/UnityEngine.TestRunner/NUnitExtensions/Attributes/TestEnumerator.cs:44

github.com

これが結構根深い問題でして、意外と解決が難しかったりします。ちなみに同様の問題提起をしているIssueもあります。
github.com

今回は原因と暫定的な対処法について紹介したいと思います。

問題

まずcom.unity.search.extensionsには[UnityTest]を利用したEditMode Testが実装されています。
github.com

そのテストがFailしてしまう状態で公開されている + 利用者側のEditMode Testに入り込んでくるという罠を抱えています。

バージョニングについて

com.unity.search.extensionsのバージョニングはかなり適当なので辛いです。

package.jsonv1.0.1を指していますが、同じバージョンでも頻繁に更新がされています。またgit URLで取得するスタイルなので、取得タイミングによっては中身が変わります。
github.com

packages-lock.jsonhashにより利用するパッケージのコミットハッシュが固定されるのでまだ良いですが、バージョンはあてにせず最悪コミットハッシュを明示的に指定しましょう。

testableについて

またmanifest.jsontestableで明示的にcom.unity.search.extensionsをしないようにしてみました。

{
  "dependencies": {
    "com.unity.search.extensions": "https://github.com/Unity-Technologies/com.unity.search.extensions.git?path=package",
    ...
  },
  "testables": [
      // ここにcom.unity.search.extensionsを指定しない
  ]
}

しかしそれでも何故かEditMode Testに含まれてしまいました。有識者に何故か教えていただきたいです...。

追記(2024/11/26)

testablesがうまく動作しないのは、com.unity.search.extensions.editor.asmdefdefineConstraintsUNITY_INCLUDE_TESTSがないことが原因でした。
github.com

おそらく多分UNITY_INCLUDE_TESTSの有無がUnityのtestable判定に関係してるんだと思います。editor用のasmdefにtest入れ込んでしまったことが大きな要因な気がしてます。

これもパッケージの中身を書き換えないといけないので、利用者としては辛いですね。

PR・Issueについて

PRやIssueは受け付けていませんスタイルです。

This repository is read-only. Owners won't accept pull requests, GitHub review requests, or any other GitHub-hosted issue management requests.

なので中身を変えたいとなったら、forkして自前で修正するしかありません。

対処法

testableで上手く解決できれば良かったのですが上手くできず、以下の3つが考えられました。

  • CIでFilterをかける
  • forkして改造
  • TestがPassするようにする

CIでFilterをかける

CIでEditMode Testを実行している場合は、com.unity.search.extensionsに依存していると失敗します。それを防ぐにはCommand Line引数によりTest対象から外すことができます。

docs.unity3d.com
docs.unity3d.com

しかしローカルでテストを実行する際は大体Unity Editor上でしょうし、やって損はないものの問題の解決には至りません。

forkして改造

なしではないとは思いますが、まあ辛いことが多いですね。

TestがPassするようにする

Assets/Materials/SurfaceShader.shaderというパスでopaqueなShaderを配置してあげるとテストが通るようになります。

var shaderInPackage = "Assets/Materials/SurfaceShader.shader";
yield return new CustomIndexationTestCase("t:shader sh_rendertype=opaque", shaderInPackage, true);

com.unity.search.extensions/package/Indexing/CustomIndexationTests.cs at 4996b11aa61dd5b0bdda66b897ac1a15b9d58c72 · Unity-Technologies/com.unity.search.extensions · GitHub

大分いかついパスですが、まあ一番コスパよく対応するならこれが良いのかなと思います。

Shaderを配置した様子

↓例

Shader "Unlit/SurfaceShader"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            // make fog work
            #pragma multi_compile_fog

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                UNITY_FOG_COORDS(1)
                float4 vertex : SV_POSITION;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                UNITY_TRANSFER_FOG(o,o.vertex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                // sample the texture
                fixed4 col = tex2D(_MainTex, i.uv);
                // apply fog
                UNITY_APPLY_FOG(i.fogCoord, col);
                return col;
            }
            ENDCG
        }
    }
}
結果

【Unity】生成したコードをPrefabに自動でアタッチする方法

はじめに

今回は生成したコードをPrefabに自動アタッチする方法を紹介したいと思います。

概要

PrefabへのアタッチにはPrefabUtilityを利用すると簡単に実現できます。
docs.unity3d.com

private static void AddComponent(string prefabPath, IReadOnlyList<string> scriptPaths)
{
    if (string.IsNullOrEmpty(prefabPath)) return;
    if (scriptPaths == null) return;
    if (scriptPaths.Count == 0) return;

    // Prefabを読み込む
    var prefab = PrefabUtility.LoadPrefabContents(prefabPath);

    foreach (var scriptPath in scriptPaths)
    {
        // スクリプトをMonoScriptとして読み込む
        var scriptAsset = AssetDatabase.LoadAssetAtPath<MonoScript>(scriptPath);

        if (scriptAsset != null)
        {
            var scriptType = scriptAsset.GetClass();
            if (scriptType != null)
            {
                // Prefabへアタッチする
                prefab.AddComponent(scriptType);
            }
        }
    }

    // Prefabを保存する
    PrefabUtility.SaveAsPrefabAsset(prefab, prefabPath);
    PrefabUtility.UnloadPrefabContents(prefab);
}

ただ生成したコードをそのままアタッチするのは一筋縄ではいきません。一度コンパイルを挟まないといけないのです。

コンパイル後にアタッチする

コンパイル後に続きの動作をするためには、シリアライズして保存しておかないといけません。そこで便利なのがScriptableSingletonです。
docs.unity3d.com

ScriptableSingletonはコンパイルが走っても値を保持してくれます。

public class State : ScriptableSingleton<State>
{
    public string[] ScriptPaths { get; set; }
}
// 利用する側
State.instance.ScriptPaths = new []
{
    "Assets/Scripts/Sample.cs",
};

コードからコンパイルを要求するには以下のメソッドを用います。

CompilationPipeline.RequestScriptCompilation();

Unity - Scripting API: Compilation.CompilationPipeline.RequestScriptCompilation


そして[DidReloadScripts]を用いることで、コンパイルした後のタイミングで属性を付与したメソッドを実行することができます。
docs.unity3d.com

public static class Hoge
{
    [DidReloadScripts]
    public static void OnDidReloadScripts()
    {
        Debug.Log("Compiled");
    }
}

実験

パーツは全て整いました。今まで紹介したものを全て組み合わせて、実験をしてみます。

using System.Collections.Generic;
using UnityEditor;
using UnityEditor.Callbacks;
using UnityEditor.Compilation;
using UnityEngine;

namespace Sample
{
    public class SampleEditor : EditorWindow
    {
        [MenuItem("Sample/Sample Editor")]
        public static void ShowWindow()
        {
            GetWindow<SampleEditor>("Sample Editor");
        }

        // コンパイル後に呼び出される
        [DidReloadScripts]
        public static void OnDidReloadScripts()
        {
            if (Application.isBatchMode) return;
            if (State.instance.ScriptPaths == null) return;
            if (State.instance.ScriptPaths.Length == 0) return;
            AddComponent("Assets/Prefabs/Sample.prefab", State.instance.ScriptPaths);
            State.instance.ScriptPaths = null;
        }

        public void OnGUI()
        {
            if (GUILayout.Button("Create And Attach"))
            {
                // アタッチするスクリプトを設定してコンパイルを走らせる
                // 今回は省くがSample.csは生成したものとする
                State.instance.ScriptPaths = new []
                {
                    "Assets/Scripts/Sample.cs",
                }; 
                AssetDatabase.Refresh();
                CompilationPipeline.RequestScriptCompilation();
            }
        }

        private static void AddComponent(string prefabPath, IReadOnlyList<string> scriptPaths)
        {
            if (string.IsNullOrEmpty(prefabPath)) return;
            if (scriptPaths == null) return;
            if (scriptPaths.Count == 0) return;
            
            // Prefabを読み込む
            var prefab = PrefabUtility.LoadPrefabContents(prefabPath);

            foreach (var scriptPath in scriptPaths)
            {
                // スクリプトをMonoScriptとして読み込む
                var scriptAsset = AssetDatabase.LoadAssetAtPath<MonoScript>(scriptPath);

                if (scriptAsset != null)
                {
                    var scriptType = scriptAsset.GetClass();
                    if (scriptType != null)
                    {
                        // Prefabへアタッチする
                        prefab.AddComponent(scriptType);
                    }
                }
            }

            // Prefabを保存する
            PrefabUtility.SaveAsPrefabAsset(prefab, prefabPath);
            PrefabUtility.UnloadPrefabContents(prefab);
        }
    }
    
    /// <summary>
    /// コンパイルが走ってもデータを保持する情報です。
    /// </summary>
    public class State : ScriptableSingleton<State>
    {
        public string[] ScriptPaths { get; set; }
    }
}

PrefabPathは適宜替えていただいて、メニューバーのSample/Sample Editorよりボタンを押すと自動でPrefabへコードがアタッチされます。

またAssets/Scripts/Sample.csを自動生成したあとにこの処理を走らせてあげれば、実現したいコードは達成できるでしょう。

【Unity】YetAnotherHttpHandlerを用いてHTTP/2を扱えるHttpClientを作成する

はじめに

今回はYetAnotherHttpHandlerを用いてUnityでHTTP/2を扱う方法を紹介したいと思います。

背景

まず.NETが提供するHttpClientですが、.NET Core3.0以降であればHTTP/2に対応しています。
HttpClient クラス (System.Net.Http) | Microsoft Learn

var client = new HttpClient
{
    BaseAddress = new Uri("https://localhost:5001"),
    DefaultRequestVersion = new Version(2, 0)
};

using var response = await client.GetAsync("/");
Console.WriteLine(response.Content);

.NET Core 3.0 の新機能 - .NET | Microsoft Learn


現在対応しているUnityのランタイムの最新は.NET Standard2.1ですが、まだHttpClientがHTTP/2に対応していません。(全てはMonoのせい)
learn.microsoft.com

UnityでHttp/2を扱いたい場合はどうすればよいのかという話になるわけですが、そこでYetAnotherHttpHandlerが登場します。

概要

YetAnotherHttpHandlerによりUnity(.NET Standard2.1)でHTTP/2を利用することができるようになります。ライブラリが提供するYetAnotherHttpHandlerHttpMessageHandlerを継承しており、HttpClientのコンストラクタに渡すだけでHTTP/2を利用するようになります。

// .NETの内部実装
public HttpClient(HttpMessageHandler handler)  : this(handler, true){ }
// YetAnotherHttpHandlerの内部実装
public class YetAnotherHttpHandler : HttpMessageHandler

https://github.com/dotnet/runtime/blob/0154a2f3403d94ea6d6f93f5a774b6e366969e0a/src/libraries/System.Net.Http/src/System/Net/Http/HttpClient.cs#L142-L151
github.com

つまりユーザーはいつも通りHttpClientを利用するので、使い心地をほとんど変える必要がないという素晴らしさです。

YetAnotherHttpHandler brings the power of HTTP/2 to Unity and .NET Standard.

This library enables the use of HTTP/2, which Unity does not support. It allows you to use grpc-dotnet instead of the deprecated C-core gRPC library. It can also be used for asset downloading via HTTP/2, providing more functionality to your projects.

The library is implemented as a HttpHandler for HttpClient, so you can use the same API by just replacing the handler. (drop-in replacement)

YetAnotherHttpHandlerはHTTP/2のパワーをUnityと.NET Standardにもたらします。 このライブラリはUnityがサポートしていないHTTP/2の使用を可能にします。 非推奨のCコアgRPCライブラリの代わりにgrpc-dotnetを使用することができます。 また、HTTP/2経由のアセットダウンロードにも使用でき、プロジェクトにさらなる機能を提供します。 このライブラリはHttpClientのHttpHandlerとして実装されているので、ハンドラを置き換えるだけで同じAPIを使用できます。 (ドロップイン置き換え)

https://github.com/Cysharp/YetAnotherHttpHandler

環境

  • Unity 2021.3 (LTS) or later

Architecture/Platformの対応について詳細はreadmeをご覧ください。
Builder in hyper_util::client::legacy - Rust

インストール方法

最近CySharpさんはNuGetForUnityやUnityNuGetを採用してきていますが、私は考え方が古いのかUPMの方が良いのかなとまだ思ってます... (何故かライブラリを信用できない)

というわけでUPMでインストールするには以下のURLをUnity Package ManagerのAdd package from git URL...に入力してください。

https://github.com/Cysharp/YetAnotherHttpHandler.git?path=src/YetAnotherHttpHandler
UPMによるインストール

追加で依存先のライブラリをインポートする必要があります。

Releasesに依存先のdllを含めた.unitypackageを上げておいてくれてるみたいです。Cysharp.Net.Http.YetAnotherHttpHandler.Dependencies.unitypackageをインストールしてUnityへインポートすればOKです。
github.com


それかnuget.orgから直接取ってくる手法も一応あります。Download packageを押して以下の手順で操作をします。

  • .nupkgをダウンロード
  • 拡張子を.zipに変更して展開
  • system.io.pipelines.8.0.0/lib/netstandard2.0/System.IO.Pipelines.dllをUnityのPluginsフォルダに配置
  • system.runtime.compilerservices.unsafe.6.0.0/lib/netstandard2.0/System.Runtime.CompilerServices.Unsafe.dllを同様にPluginsフォルダに配置

使い方

YetAnotherHttpHandlerを生成して、HttpClientのコンストラクタに渡してあげます。

using System.Net.Http;
using Cysharp.Net.Http;
using UnityEngine;

public class Sample : MonoBehaviour
{
    private async void Start()
    {
        using var handler = new YetAnotherHttpHandler();
        using var httpClient = new HttpClient(handler);

        // 適当にブログのページを取得します。HTTP/2に対応していることは確認済です。
        var result = await httpClient.GetAsync("https://www.hanachiru-blog.com/");

        // HTTP message version : 2.0
        var version = result.Version;
        Debug.Log(version);

        var content = await result.Content.ReadAsStringAsync();
        Debug.Log(content);
    }
}

github.com

HTTP/2で通信に成功している

正しく通信できることを確認できました。

さいごに

readmeに書かれている通り、Grpc.Netに移行でHTTP/2が必要になり利用されるケースが多いようです。

そこら辺も詳しくreadmeにかかれているので、気になる方はチェックしてみてください。
github.com

【Unity】UI ToolkitのTwoPaneSplitViewをコードから動的に生成せずに利用する方法

はじめに

UI ToolkitのTwoPaneSplitViewというVisualElementがあるのですが、それがめちゃくちゃ便利です。2つの要素をリサイズできるようにしつつ分割して表示してくれるものになります。
docs.unity3d.com

TwoPaneSplitView

よくコードからTwoPaneSplitViewを生成する方法が紹介されていますが、実用するうえではUXMLに直書きしたい場合の方が多いのではないのかなと思います。しかしUnityのデフォルトではLibrary/Standardにはありません。(Unity2022.3.22f1時点で確認)

Library/StandardにTwoPaneSplitViewはない

今回はコードから生成せずにTwoPaneSplitViewを利用する方法を紹介したいと思います。

概要

改めて紹介するとTwoPaneSplitViewとは水平または垂直に2つのペインに子要素を配置するコンテナです。

TwoPaneSplitView

The TwoPaneSplitView element is a container that arranges its children in two panes, either horizontally or vertically. The user can resize the panes by dragging the divider between them. A TwoPaneSplitView must have exactly two children.

// DeepL翻訳
TwoPaneSplitView要素は、水平または垂直に2つのペインに子要素を配置するコンテナです。 ユーザーは、ペイン間の仕切りをドラッグすることによって、ペインのサイズを変更することができます。 TwoPaneSplitViewは、正確に2つの子を持つ必要があります。

https://docs.unity.cn/Manual/UIE-uxml-element-TwoPaneSplitView.html

コードからの生成方法

これはよく紹介されているので書かなくてもいいかもしれませんが、例えば以下のようなコードで生成できます。

public void CreateGUI()
{
    // 最初の子要素を固定ペインとする場合は 0、2 番目の子要素を固定ペインとする場合は 1。
    var fixedPaneIndex = 0;

    // 固定ペインの初期の幅または初期の高さ。
    var fixedPaneInitialWidth = 200f;

    // 分割ビューの方向。(Horizontal or Vertical)
    var orientation = TwoPaneSplitViewOrientation.Horizontal;

    var splitView = new TwoPaneSplitView(fixedPaneIndex, fixedPaneInitialWidth, orientation);
    rootVisualElement.Add(splitView);

    // 左ペイン
    var leftPane = new VisualElement();
    splitView.Add(leftPane);

    // 右ペイン
    var rightPane = new VisualElement();
    splitView.Add(rightPane);
}

uxmlに書き込む方法

TwoPaneSplitViewを配置したい場所に以下の要素を入れ込んでください。

<ui:TwoPaneSplitView class="unity-two-pane-split-view">
    <ui:VisualElement />
    <ui:VisualElement />
</ui:TwoPaneSplitView>

一度これを入れ込むことにより、あとはUI Builder上からでもイジれるようになります。

UI Builder上でTwoPaneSplitViewをいじっている様子

【Unity】.asmrefはCore CLRがくると動作しなくなるよという話

はじめに

UnityはMonoからCore CLRへの移行を進めていますが、Core CLRがくると.asmrefが使えなくなるみたいです。そのあたりを軽く調べてみたので、そのまとめを書き残しておきたいと思います。

また現時点ではまだ開発中なので、今後変わるかも知れないのでご注意ください。

話題の議論

Unity DiscussionsでUnity Future .NET Development StatusというタイトルでユーザーとUnity中の人が議論している様子を見ることができます。
https://discussions.unity.com/t/unity-future-net-development-status/

そこでとあるユーザーがCoreCLRがくると.asmrefがなくなる話はどうなったの?と質問していました。

As a long time has passed since this was last talked about. How’s the latest status for .asmref and usage of [InternalsVisibleTo] attribute?
There were rumors that .asmref will be gone with CoreCLR and I’m not clear if that’s true or if it has changed by now.

Any kind of update would be appreciated. Thanks!

// DeepL翻訳
この話題からずいぶん時間が経ってしまったが、.asmrefと[InternalsVisibleTo]属性の最新状況はどうなっているのだろうか。 .asmrefと[InternalsVisibleTo]属性の使い方の最新状況はどうなっているのでしょうか? CoreCLRで.asmrefがなくなるという噂がありましたが、それが本当なのか、それとも今頃になって変わったのかよくわかりません。 何か最新情報があれば教えてください。 ありがとうございます!

Unityの中の人の回答としては、.asmrefは廃止するということみたいです。

Yes, asmref won’t be supported anymore. For now, as we can’t undisclosed more about some of the changes that we are making, It will be difficult to give more details about this, hope you understand.

// DeepL翻訳
はい、asmrefはもうサポートされません。 今のところ、私たちが行っているいくつかの変更についてこれ以上公表することができないため、これ以上の詳細をお伝えすることは難しいのですが、ご理解いただければ幸いです。

https://discussions.unity.com/t/unity-future-net-development-status/836646/2491

代替案

また中の人が代替案を提案してくれていました。

  • System.Reflection
  • IL Post Processing
  • UnsafeAccessorAttribute

We won’t have a strict substitute, but in this kind of situations (and that applies to any .NET code, not only for Unity), folks have been always able to workaround with e.g System.Reflection, IL post processing assemblies and there are also new unsafe workaround toolbox coming like the more recent [UnsafeAccessorAttribute]. But the best substitute imo would be to have closer collaboration with users, good feedback loop, and ways to add missing plugin entry points whenever possible. It might sound impossible but it is still a north star for us.

厳密な代用品はありませんが、このような状況では(Unityだけでなく、すべての.NETコードに当てはまります)、System.Reflection、ILポスト処理アセンブリなどで回避することができますし、最近の[UnsafeAccessorAttribute]のような新しい安全でない回避ツールボックスもあります。 しかし、最良の代替案は、ユーザーとの緊密なコラボレーション、良いフィードバックループ、そして可能な限り不足しているプラグインのエントリーポイントを追加する方法を持つことだと思います。 不可能に聞こえるかもしれませんが、それでも私たちにとっては北極星です。

.NET8が利用できるようになればわりとUnsafeAccessorAttributeは候補に上がってくるのかなと思ってます。

まあ.asmref程はユーザーにとって使いやすく安全なものではないですが、早くMono脱却してほしい気持ちの方がはるかに大きいです。