この記事はUnity Advent Calendar 2019の第1日目の記事です。
ハンズオンの目標
- Mono.Cecilの使い方に慣れる
- LINQの内部実装についての初歩の理解を得る
- マネージドプラグイン開発に慣れる
- GitHub Actionsに慣れる
- UniNativeLinqのファンになる
LINQについては結構資料がありますので、主にMono.CecilとGitHub Actionsについてこの記事で学んでいただければと思います。
筆者の開発環境
- Windows10 Home
- Intel Core i7-8750H
- RAM 16GB
- Unity
- 2018.4.9f1
- .NET Core 3.0
- 3.0.101
- Visual Studio 2019
- 16.4.0 Preview2.0
- Git
- 2.24.0.windows.2
- Rider 2019.3 EAP
前提知識
- C#
- 値型、参照型の違いとパフォーマンス特性への理解 参考文献:C# によるプログラミング入門 [メモリとリソース管理] 値型と参照型
- foreachとそのコンパイラによる展開、特にパターンベースであることの理解 参考文献:C# によるプログラミング入門 [データ列処理] foreach
- LINQのAPIに対する理解 参考文献:C# によるプログラミング入門 [データ列処理] LINQ
- IL
- C#のコードがIL(Intermediate Language)の集合にコンパイルされるということへの理解 参考文献:ILに関するWikipedia記事
- 各命令についてわからないことがあればMSDocsのOpCodesクラスの説明を読むのが良いでしょう
- Unity
- Unity2018でC#7.3の機能が使えるということの理解 参考文献:Unity公式ブログより「Unity 2018.3 リリース」
- Unity2018からUnity.Collections.NativeArray<T>というアンマネージドなヒープやスタック上の連続したメモリ領域を表す配列的構造体についての理解
- 参考文献1:UnityのScriptingリファレンスのNativeArrayに関するページ
- 参考文献2:【Unity】アセット読書会に行ってきたよ。NativeArrayってなんだろう?
- Unity.Collections.UnsafeUtilityというstatic classがNativeArrayの基礎であることへの理解 参考文献:【Unity】UnsafeUtilityについて纏めてみる
- UnsafeUtilityの詳細なAPIとその機能についての理解
- 参考文献1:【Unity】UnsafeUtility基礎論【入門者向け】
- 参考文献2:UnityのScriptingリファレンスのUnsafeUtilityに関するページ
- GitHub Actions
- GitHubに統合されたCI/CDサービスであることの理解 参考文献:GitHub Actionsについて
- UniNativeLinq
- NativeArray<T>向けのLINQライブラリであることの理解 参考文献:UniNativeLinqに関して
事前にハンズオンを行う人がインストールしておくべきもの
- Unity2018.4
- Unityのバージョンについては2018.4系列である限りなんでもよいです。適宜読み替えを行ってください。
- .NET Core 3.0
- Visual Studio2019またはRider2019.2以上
- Git
第0章 準備
パス通し
以後ターミナル操作はPowerShell上で行います。
まずUnity2018.4の実体のあるパスを追加します。
私は"コントロール パネル\システムとセキュリティ\システム\システムの詳細設定\環境変数\Path"に"C:\Users\conve\Documents\Unity\Editor"と追加していますが、環境変数を汚したくない方は都度下のように書いてパスを通すのが良いのではないでしょうか。
$Env:Path+=";C:\Program Files\Unity\Hub\Editor\2018.4.13f1\Editor"
作業ディレクトリ
適当なディレクトリの下に新規に作業ディレクトリを作成します。
今回はUniNativeLinqHandsOnという名前にしましょう。
mkdirUniNativeLinqHandsOn
前節で正常にパスが通っているならば次のシェルコマンドを実行してUnityエディタが起動するはずです。
unity-createProject./UniNativeLinqHandsOn/
では、一旦エディタを閉じましょう。
Git初期化
GitHub Actionsを使う兼ね合いもあり、Gitのリポジトリを用意しましょう。
cdUniNativeLinqHandsOngitinitecho[Ll]ibrary/[Ll]ogs/[Oo]bj/.idea/.vs/.vscode//*.csproj/*.sln/*.sln.user/TestResults-*.xml>.gitignore
マネージドプラグインの下拵え
これからUniNativeLinqの基礎となるNativeEnumerable<T> where T : unmanagedを実装します。
マネージドプラグインとしてUnity外でDLLをビルドしますので、フォルダを作りましょう。フォルダ名は"core~"とします。
mkdircore~cdcore~
DLLを作るためにdotnet newコマンドでclasslib(ライブラリ作成)オプションを指定して初期化します。
Class1.csは特に要らないので削除します。
追加で.gitignoreをこのフォルダにも定義します。
dotnetnewclasslibdelClass1.csechobin/obj/>.gitignore
次にcore~.csprojを編集します。
初期状態では以下のように記述されているはずです。
<ProjectSdk="Microsoft.NET.Sdk"><PropertyGroup><TargetFramework>netstandard2.0</TargetFramework><RootNamespace>core_</RootNamespace></PropertyGroup></Project>
PropertyGroup要素以下に基本設定を記述します。 RootNamespace要素はVisual Studioで新規にcsファイルを作成する時に使用される名前空間を指定します。csprojの各要素の軽い解説
Visual Studio 2017の頃からcsprojの新しい形式としてSdk形式というものが登場しました。 参考文献:ufcppのブログ記事
全てを設定していた従来のものよりも、デフォルト値と異なる点のみ設定するSdk形式の方が非常に記述量が少なく可読性が高いですね。<Project Sdk="Microsoft.NET.Sdk">
というトップレベルのProject要素にSdk属性が定義されている場合Sdk形式となります。
TargetFarmework要素にビルド対象のプラットフォーム/フレームワークを指定します。
Unity2018以上で使うことを考え、.NET Standard 2.0を意味するnetstandard2.0を指定しておきます。
上記csprojを編集して以下の通りにします。
<ProjectSdk="Microsoft.NET.Sdk"><PropertyGroup><TargetFramework>netstandard2.0</TargetFramework><RootNamespace>UniNativeLinq</RootNamespace><AllowUnsafeBlocks>True</AllowUnsafeBlocks><AssemblyName>UniNativeLinq</AssemblyName><LangVersion>8</LangVersion></PropertyGroup><ItemGroup><ReferenceInclude="UnityEngine.CoreModule"><HintPath>○Unityのインストールしてあるフォルダ○\Editor\Data\Managed\UnityEngine\UnityEngine.CoreModule.dll</HintPath></Reference></ItemGroup></Project>
AllowUnsafeBlocks要素をTrueにしてポインタを使用可能にします。
AssemblyName要素によりアセンブリ名と出力されたDllファイルの名前を指定します。
そして、LangVersion要素を8に指定してunmanaged型制約の判定を緩めます。 参考文献:アンマネージな総称型に関するunmanaged型制約
最後に、ItemGroup/Reference要素でUnityEngine.CoreModule.dllを参照に追加しましょう。
Unityのランタイムで使用される基本的な機能はUnityEngine.CoreModule.dllを通じて提供されています。
以上で最初の下拵えを終わります。
第1章 UniNativeLinq-Coreの最低限の実装(1)
現在の作業ディレクトリが"UniNativeLinqHandsOn/core~"であることを確認してください。
これから私達は以下のファイル群を作成し、UniNativeLinqのコア機能を最低限の形で実装していきます。
- NativeEnumerable.cs
- NativeEnumerable<T>構造体を定義します。
- NativeArray<T>に対してSpan<T>的な役割を果たす基本的な構造体です。
- IRefEnumerable<T>を実装します。
- IRefEnumerable.cs
- System.Collections.IEnumerable<T>を継承したIRefEnumerable<TEnumerator, T>インターフェイスを定義します。
- 通常のIEnumerable<T>の型引数が1つであるのに対して、IRefEnumerable<TEnumerator, T>の型引数が2つであるのは、構造体イテレータのボクシングを避ける目的があります。
- IRefEnumerator.cs
- System.Collections.IEnumerator<T>を継承したIRefEnumerator<T>インターフェイスを定義します。
- foreach(ref var item in collection)のような参照をイテレーションするための種々の操作を定義します。
- AsRefEnumerable.cs
- NativeEnumerable静的クラスを定義します。
- NativeEnumerable<T>構造体と名前がほぼ同じですが別物です。
- NativeArray<T>とT[]に対して拡張メソッドを定義します。
mkdirCollectionmkdirInterfacemkdirUtilitymkdirAPINew-ItemCollection/NativeEnumerable.csNew-ItemInterface/IRefEnumerator.csNew-ItemInterface/IRefEnumerable.csNew-ItemAPI/AsRefEnumerable.cs
NativeEnumerable<T>の最初の定義
最初のNativeEnumerable.cs
namespaceUniNativeLinq{publicreadonlyunsafestructNativeEnumerable<T>whereT:unmanaged{publicreadonlyT*Ptr;publicreadonlylongLength;publicNativeEnumerable(T*ptr,longlength){if(ptr==default||length<=0){Ptr=default;Length=default;return;}Ptr=ptr;Length=length;}publicrefTthis[longindex]=>refPtr[index];}}
unsafeでreadonlyな構造体UniNativeLinq.NativeEnumerable<T>を定義します。
これはジェネリックなTのポインタであるPtrと要素数であるLengthフィールドを露出させています。
nullポインタやダングリングポインタに対する安全性保証は一切ないので、その辺りはエンドユーザーに一切合切投げっぱなしになるC++スタイルです。
これをビルドし、テストコードをUnityの方で実行してみましょう。
最低限のテスト
現在のワーキングディレクトリは"UniNativeLinqHandsOn/core~"のはずです。
以下のようにAssets以下Plugins/UNLフォルダを作成し、core~のビルド成果物であるUniNativeLinq.dllをコピーして配置します。
mkdir-p../Assets/Plugins/UNLdotnetbuild-cReleasecp-Force./bin/Release/netstandard2.0/UniNativeLinq.dll../Assets/Plugins/UNL/UniNativeLinq.dll
ビルドした後毎回"cp -Force ほげほげ"と入力するのも面倒ですので、core~.csprojにビルド後イベントを定義して自動化します。
ビルド後イベントでコピーを自動化したcsproj
<ProjectSdk="Microsoft.NET.Sdk"><PropertyGroup><TargetFramework>netstandard2.0</TargetFramework><RootNamespace>UniNativeLinq</RootNamespace><AllowUnsafeBlocks>True</AllowUnsafeBlocks><LangVersion>8</LangVersion><AssemblyName>UniNativeLinq</AssemblyName></PropertyGroup><ItemGroup><ReferenceInclude="UnityEngine.CoreModule"><HintPath>○Unityのインストールしてあるフォルダ○\Editor\Data\Managed\UnityEngine\UnityEngine.CoreModule.dll</HintPath></Reference></ItemGroup><TargetName="PostBuild"AfterTargets="PostBuildEvent"><ExecCommand="copy $(TargetPath) $(ProjectDir)..\Assets\Plugins\UNL\UniNativeLinq.dll"/></Target></Project>
ビルド後イベントでローカルデプロイの自動化は結構重宝しますのでオススメです。
さて、Assets/Plugins/UNL以下にdllを配置しましたので、それを対象としたテストコードを書きましょう。
cd..unity-projectPath.
エディタが起動しましたね?
ProjectタブのAssetsを選択してコンテキストメニューから"Create/Testing/Tests Assembly Folder"を選択してTestsフォルダーを作成してください。
無事にTestsフォルダが作成されたならばそのフォルダ以下にTests.asmdefファイルがあるはずです。
それを選択し、Inspectorタブから設定を変更します。
"Allow 'unsafe' Code"と"Override References"にチェックを入れ、"Assembly References"に"UniNativeLinq.dll"を加えてください。
そしてPlatformsをEditorだけにしてください。
次の画像のようなInspectorになるはずです。正しく設定できたならば一番下のApplyボタンを押して設定を保存してください。
次にProjectタブでAssets/Testsフォルダを右クリックしてコンテキストメニューを呼び出し、"Create/Testing/C# Test Script"を押して新規にテスト用スクリプトを作成します。
ファイル名は"NativeEnumerableTestScript"としましょう。
NativeEnumerableTestScriptをダブルクリックして編集を行います。
NativeEnumerableTestScript.csの中身
usingNUnit.Framework;usingUniNativeLinq;usingUnity.Collections;usingUnity.Collections.LowLevel.Unsafe;namespaceTests{publicsealedunsafeclassNativeEnumerableTestScript{[Test]publicvoidDefaultValuePass(){NativeEnumerable<int>nativeEnumerable=default;Assert.AreEqual(0L,nativeEnumerable.Length);Assert.IsTrue(nativeEnumerable.Ptr==null);}[TestCase(0L)][TestCase(-10L)][TestCase(-12241L)][TestCase(long.MinValue)]publicvoidZeroOrNegativeCountTest(longcount){using(vararray=newNativeArray<int>(1,Allocator.Persistent)){Assert.IsFalse(array.GetUnsafePtr()==null);varnativeEnumerable=newNativeEnumerable<int>((int*)array.GetUnsafePtr(),count);Assert.AreEqual(0L,nativeEnumerable.Length);Assert.IsTrue(nativeEnumerable.Ptr==null);}}[TestCase(0,Allocator.Temp)][TestCase(1,Allocator.Temp)][TestCase(10,Allocator.Temp)][TestCase(114,Allocator.Temp)][TestCase(0,Allocator.TempJob)][TestCase(1,Allocator.TempJob)][TestCase(10,Allocator.TempJob)][TestCase(114,Allocator.TempJob)][TestCase(0,Allocator.Persistent)][TestCase(1,Allocator.Persistent)][TestCase(10,Allocator.Persistent)][TestCase(114,Allocator.Persistent)]publicvoidFromNativeArrayPass(intcount,Allocatorallocator){using(vararray=newNativeArray<int>(count,allocator)){varnativeEnumerable=newNativeEnumerable<int>((int*)array.GetUnsafePtr(),array.Length);Assert.AreEqual((long)count,nativeEnumerable.Length);for(vari=0;i<nativeEnumerable.Length;i++){Assert.AreEqual(0,nativeEnumerable[i]);nativeEnumerable[i]=i;}for(vari=0;i<count;i++)Assert.AreEqual(i,array[i]);}}}}
上記コードに従ってUnity Test Runnerの為のEditor Mode Testを複数個用意します。
NUnit.Framework.TestCase属性はバリエーションを作り出すのにかなり便利な属性です。
テストコードの記述後はエディタに戻り、Unity Test Runnerのウィンドウを呼び出しましょう。メニューの"Window/General/Test Runner"をクリックすると開きます。
出てきたウィンドウのRun Allを押すと全ての項目が緑になり、テスト全てをPassしたことがわかります。
GitHubにリポジトリを作って成果物を公開する
GitHubに適当なリポジトリ名で新規リポジトリを作成してください。そこにこのプロジェクトを公開します。
私は"HandsOn_CSharpAdventCalendar20191201"と命名しました。
現在のワーキングディレクトリはUniNativeLinqHandsOnのはずです。
gitswitch-cdevelopgitadd.gitcommit-m"[init]"gitremoteaddoriginhttps://github.com/pCYSl5EDgo/HandsOn_CSharpAdventCalendar20191201.gitgitpush-uorigindevelop
git remote add origin https://github.com/pCYSl5EDgo/HandsOn_CSharpAdventCalendar20191201.git
については適切な読み替えを行ってください。
適切な.gitignore設定を行っているならば上記の操作で最初のコミットを過不足なくできます。
基本的にローカルのワーキングブランチはdevelopとし、リモートリポジトリのdevelopブランチにpushすることとします。
この措置はリモートのmasterブランチをUPM用にする為のものです。Assetsを含む通常のUnityプロジェクトはUPMの構成と相性が悪いのです。
GitHub Actions対応 CI/CDを行う
これからGitHub ReleasesでUniNativeLinq.dllをpush時に自動的に公開する仕組みを作ります。その際にテストも走らせ、テスト失敗時はリリースしないようにします。
Unityを利用するためには必ずメールアドレスとパスワードで認証する必要があります。 詳細な手順は公式の参考文献を読んで理解していただくとして、次のような手順でulfファイルを作成してください。 matrix.unity-versionにあなたの使うUnityのバージョンを指定してください。 入手したulfファイルをリポジトリ"HandsOn_CSharpAdventCalendar20191201"で利用しますが、秘密にすべき情報であるため、GitHub Secretsという機能を使って暗号化しましょう。GitHub ActionsでUnityを使うための下拵え 参考文献:GitHub ActionsでUnity開発
CI/CDサービスからUnityを利用する場合にはLinux環境を利用する形になります。なぜWindowsやMacではなくLinuxなのかについての補足
WinやMacはVMインスタンスとして立ち上がるので、ジョブ毎にMachine IDが変化します。
困ったことに後述するalfファイルの項目にMachine IDがありまして、ここがulfにも受け継がれてしまい、不一致だと認証にコケるのです。
故にulfファイルを使用してオフライン認証を行う手法をWindowsとMacOS環境では取り得ません。
CUIで認証する場合にはオフライン/ 手動アクティベーションを行う方がパスワード漏洩対策として安全です。
これは事前にUnityを動かすPCの情報と、Unityのバージョン、ユーザーのパスワードとメールアドレス等全ての情報を含んだulfファイルを生成しておき、GitHub Actionsでの実行時にulfファイルを使用して認証を行うという手法です。もしあなたがUnity2018.4.12f1以外でこのハンズオンを行う場合
CreateALFというGitHubのリポジトリをForkし、".github/workflows/CreateLicenseALF.yml"を編集してください。
name:Create ALF Fileon:[push]jobs:build:runs-on:ubuntu-lateststrategy:matrix:unity-version:-2018.4.9f1-2018.4.10f1-2018.4.11f1-2018.4.12f1-2018.4.13f1-2019.3.0f1-2020.1.0a14steps:-uses:pCYSl5EDgo/setup-unity@masterwith:unity-version:${{ matrix.unity-version }}-name:Create Manual Activation Filerun:/opt/Unity/Editor/Unity -quit -batchmode -nographics -logfile -createManualActivationFile || exit 0-name:Create Releaseid:create_releaseuses:actions/create-release@v1.0.0env:GITHUB_TOKEN:${{ secrets.GITHUB_TOKEN }}with:tag_name:setup-${{ matrix.unity-version }}release_name:Release setup-Unity ${{ matrix.unity-version }}draft:falseprerelease:false-name:Upload Release Assetid:upload-release-assetuses:actions/upload-release-asset@v1.0.1env:GITHUB_TOKEN:${{ secrets.GITHUB_TOKEN }}with:upload_url:${{ steps.create_release.outputs.upload_url }}# This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps asset_path:Unity_v${{ matrix.unity-version }}.alfasset_name:Unity_v${{ matrix.unity-version }}.alfasset_content_type:application/xml
masterブランチにpushするとGitHub Releaseにそのバージョンのalfファイルが登録されます。
GitHub SecretsはSettings/Secretsを選択し、そこにキーと値のペアを登録します。
今回はulfというキーでulfファイルの中身を登録しましょう。
以上でGitHub ActionsでUnityを扱う下拵えは完了です。
現在のワーキングディレクトリはUniNativeLinqHandsOnのはずです。
mkdir-p.github/workflowsNew-Item.github/workflows/CI.yaml
".github/workflows"フォルダ以下にyamlファイルを作成し、そこに自動化する仕事を記述します。
更にcore~.csproje.txtを新規に作成します。core~.csprojはWindows向けの記述をしていて、そのままではLinuxのDockerコンテナ上では動作しません。core~.csprojはこのように記述しなおしてください。
<Project Sdk="Microsoft.NET.Sdk"><PropertyGroup><TargetFramework>netstandard2.0</TargetFramework><RootNamespace>UniNativeLinq</RootNamespace><AllowUnsafeBlocks>True</AllowUnsafeBlocks><LangVersion>8</LangVersion><AssemblyName>UniNativeLinq</AssemblyName></PropertyGroup><ItemGroup><Reference Include="UnityEngine.CoreModule"><HintPath Condition="Exists('C:\Users\conve')">C:\Users\conve\Documents\Unity\Editor\Data\Managed\UnityEngine\UnityEngine.CoreModule.dll</HintPath><HintPath Condition="Exists('/opt/Unity/Editor/Unity')">/opt/Unity/Editor/Data/Managed/UnityEngine/UnityEngine.CoreModule.dll</HintPath></Reference></ItemGroup></Project>
CI.yamlの内容
name:CreateReleaseon:push:branches:-developjobs:buildReleaseJob:runs-on:ubuntu-lateststrategy:matrix:unity-version:[2018.4.9f1]user-name:[pCYSl5EDgo]repository-name:[HandsOn_CSharpAdventCalendar20191201]exe:['/opt/Unity/Editor/Unity']steps:-uses:pCYSl5EDgo/setup-unity@masterwith:unity-version:${{ matrix.unity-version }}-name:License Activationrun:|echo -n "$ULF" > unity.ulf${{ matrix.exe }} -nographics -batchmode -quit -logFile -manualLicenseFile ./unity.ulf || exit 0env:ULF:${{ secrets.ulf }}-run:git clone https://github.com/${{ github.repository }}-uses:actions/setup-dotnet@v1.0.2with:dotnet-version:'3.0.101'-name:Builds DLLrun:|cd ${{ matrix.repository-name }}/core~dotnet build -c Release-name:Post Process DLLrun:|cd ${{ matrix.repository-name }}mv -f ./core~/bin/Release/netstandard2.0/UniNativeLinq.dll ./Assets/Plugins/UNL/UniNativeLinq.dll-name:Run Testrun:${{ matrix.exe }} -batchmode -nographics -projectPath ${{ matrix.repository-name }} -logFile ./log.log -runEditorTests -editorTestsResultFile ../result.xml || exit 0-run:ls -l-run:cat log.log-run:cat result.xml-uses:pCYSl5EDgo/Unity-Test-Runner-Result-XML-interpreter@masterid:interpretwith:path:result.xml-if:steps.interpret.outputs.success != 'true'run:exit 1-name:Get Versionrun:|cd ${{ matrix.repository-name }}git describe --tags 1> ../version 2> ../error || exit 0-name:Cat Erroruses:pCYSl5EDgo/cat@masterid:errorwith:path:error-if:startsWith(steps.error.outputs.text, 'fatal') != 'true'run:|cat versioncat version | awk '{ split($0, versions, "-"); split(versions[1], numbers, "."); numbers[3]=numbers[3]+1; variable=numbers[1]"."numbers[2]"."numbers[3]; print variable; }' > version_increment-if:startsWith(steps.error.outputs.text, 'fatal')run:echo -n "0.0.1" > version_increment-name:Catuses:pCYSl5EDgo/cat@masterid:versionwith:path:version_increment-name:Create Releaseid:create_releaseuses:actions/create-release@v1.0.0env:GITHUB_TOKEN:${{ secrets.GITHUB_TOKEN }}with:tag_name:${{ steps.version.outputs.text }}release_name:Release Unity${{ matrix.unity-version }} - v${{ steps.version.outputs.text }}draft:falseprerelease:false-name:Upload DLLuses:actions/upload-release-asset@v1.0.1env:GITHUB_TOKEN:${{ secrets.GITHUB_TOKEN }}with:upload_url:${{ steps.create_release.outputs.upload_url }}asset_path:${{ matrix.repository-name }}/Assets/Plugins/UNL/UniNativeLinq.dllasset_name:UniNativeLinq.dllasset_content_type:application/vnd.microsoft.portable-executable
jobs.buildReleaseJob.startegy.matrix以下の3項目は適切に書き換えてください。
unity-tag: [2018.4.12f1]
user-name: [pCYSl5EDgo]
repository-name: [HandsOn_CSharpAdventCalendar20191201]
- unity-tag
- Unityのバージョン
- user-name
- あなたのGitHubアカウント名
- repository-name
- あなたのリポジトリ名
全体の流れとしては以下の通りになります。
- 作業リポジトリをクローン
- actions/checkoutだとcore~などの隠しフォルダを無視してしまうのでgit cloneするのが安牌
- GitHub Secretsに登録したulfファイルをトップレベルに
echo -n "${ULF}" > Unity_v2018.x.ulf
で出力- 環境変数に仕込んでいるので外部にバレずに利用可能
- ビルドに必要なdotnet core 3.0環境のセットアップ
- DLLをビルドしてそれをAssets/Plugins/UNL以下に配置
- Unityのライセンス認証
- Unity Test Runnerをコマンドラインから走らせる
- テストに失敗したなら全体を失敗させて終了
- 前回のビルド時のバージョンを取得
- 取得に失敗したならば今回のバージョンを0.0.1とする
- 取得成功時はawkでゴニョゴニョしてマイナーバージョンをインクリメントする
- GitHub Releasesに新規リリースを作成する
- リリースにファイルを追加する
gitadd.gitcommit-m"[update]Publish Release"gitpush
現在のワーキングディレクトリはUniNativeLinqHandsOnのはずです。
全ての作業が終わったらGitHubにpushして最初のGitHub Releasesを公開しましょう。
IEnumerable<T>の実装
NativeEnumerable<T>の中身として全てのフィールドとインデクサを定義しました。
これからIEnumerable<T>を実装します。記述が増えるのでpartial structにします。IEnumerable<T>を実装したNativeEnumerable<T>
usingSystem.Collections;usingSystem.Collections.Generic;namespaceUniNativeLinq{publicreadonlyunsafepartialstructNativeEnumerable<T>:IEnumerable<T>whereT:unmanaged{publicreadonlyT*Ptr;publicreadonlylongLength;publicNativeEnumerable(T*ptr,longlength){if(ptr==default||length<=0){Ptr=default;Length=default;return;}Ptr=ptr;Length=length;}publicrefTthis[longindex]=>refPtr[index];publicEnumeratorGetEnumerator()=>newEnumerator(this);IEnumerator<T>IEnumerable<T>.GetEnumerator()=>GetEnumerator();IEnumeratorIEnumerable.GetEnumerator()=>GetEnumerator();}}
IEnumerator<T>を実装したNativeEnumerable<T>.Enumerator
usingSystem.Collections;usingSystem.Collections.Generic;namespaceUniNativeLinq{publicreadonlypartialstructNativeEnumerable<T>{publicunsafestructEnumerator:IEnumerator<T>{privatereadonlyT*ptr;privatereadonlylonglength;privatelongindex;publicEnumerator(NativeEnumerable<T>parent){ptr=parent.Ptr;length=parent.Length;index=-1;}publicboolMoveNext()=>++index<length;publicvoidReset()=>index=-1;publicrefTCurrent=>refptr[index];TIEnumerator<T>.Current=>Current;objectIEnumerator.Current=>Current;publicvoidDispose()=>this=default;}}}
イテレータ構造体を内部型として定義するのはforeachの性能向上の常套手段です。
cdcore~dotnetbuild-cReleasecd..unity-projectPath.
ビルドをした後エディタを起動し、テストコードを書きましょう。
NativeEnumerableTestScriptクラスに追記する形で単一のクラスを肥大させましょう。
Unityのよくわからない仕様なのですが、1つのプロジェクトに2ファイル以上のテストスクリプトが存在するとコマンドラインからrunEditorTestsするとエラー吐きます。
このような事情もあり、簡易的な処置ですが神テストクラスを肥えさせます。本格的な処置についてはいずれまた別の記事で書くこともあるかも知れません。NativeEnumerableTestScript.cs
[TestCase(0,Allocator.Temp)][TestCase(114,Allocator.Temp)][TestCase(114514,Allocator.Temp)][TestCase(0,Allocator.TempJob)][TestCase(114,Allocator.TempJob)][TestCase(114514,Allocator.TempJob)][TestCase(0,Allocator.Persistent)][TestCase(114,Allocator.Persistent)][TestCase(114514,Allocator.Persistent)]publicvoidIEnumerableTest(intcount,Allocatorallocator){using(vararray=newNativeArray<long>(count,allocator)){varnativeEnumerable=newNativeEnumerable<long>((long*)array.GetUnsafePtr(),array.Length);Assert.AreEqual(count,nativeEnumerable.Length);for(vari=0L;i<count;i++)nativeEnumerable[i]=i;varindex=0L;foreach(refvariinnativeEnumerable){Assert.AreEqual(index++,i);i=index;}index=1L;foreach(variinnativeEnumerable)Assert.AreEqual(index++,i);}}
foreach文が正しく動いていることがこれで確認できます。
Unityエディターを閉じた後、GitHubにpushしてCI/CDを体感しましょう。
gitadd.gitcommit-m"[update]Implement IEnumerable<T> & IEnumerator<T>"gitpush
AsEnumerable()に相当するAsRefEnumerable()の実装
NativeArray<T>からNativeEnumerable<T>を生成するのに一々 var nativeEnumerable = new NativeEnumerable<T>((T*) array.GetUnsafePtr(), array.Length);
と記述するのも手間です。var nativeEnumerable = array.AsRefEnumerable();
だったら非常に楽ですので、拡張メソッドを定義します。
namespaceUniNativeLinq{publicstaticunsafeclassNativeEnumerable{publicstaticNativeEnumerable<T>AsRefEnumerable<T>(thisUnity.Collections.NativeArray<T>array)whereT:unmanaged=>newNativeEnumerable<T>(ptr:(T*)Unity.Collections.LowLevel.Unsafe.NativeArrayUnsafeUtility.GetUnsafeBufferPointerWithoutChecks(array),length:array.Length);}}
IRefEnumerable/torの定義と実装
NativeEnumerable<T>とその内部型Enumeratorは public Enumerator GetEnumetor();
と public ref T Current{get;}
が特徴的な要素です。
これをインターフェイスに抽出します。
IRefEnumerable.csとIRefEnumerator.csの定義
namespaceUniNativeLinq{publicinterfaceIRefEnumerable<TEnumerator,T>:System.Collections.Generic.IEnumerable<T>whereTEnumerator:IRefEnumerator<T>{newTEnumeratorGetEnumerator();}}
namespaceUniNativeLinq{publicinterfaceIRefEnumerator<T>:System.Collections.Generic.IEnumerator<T>{newrefTCurrent{get;}}}
上記インターフェイスをNativeEnumerableに実装します。
publicreadonlyunsafepartialstructNativeEnumerable<T>:IRefEnumerable<NativeEnumerable<T>.Enumerator,T>
publicunsafestructEnumerator:IRefEnumerator<T>
テストコードには何も差は生じません。(既存の実装を元にインターフェイスを抽出しただけですので)
第2章 初めてのAPI - Select
LINQで一番使うAPIはSelectまたはWhereのはずです。
今回はUniNativeLinqの特異性を学ぶのに好適であるため、Selectを実装してみます。
通常LINQのSelectについて
通常のSystem.Linq.Enumerableの提供するSelectメソッドのシグネチャを見てみましょう。
publicstaticIEnumerable<TTo>Select<TFrom,TTo>(thisIEunmerable<TFrom>collection,Func<TFrom,TTo>func);
引数にIEnumerable<TFrom>なコレクションと、Func<TFrom, TTo>な写像を取ってマッピングを行います。
LINQの優れている点は拡張メソッドの型引数を(C#の貧弱な型推論でも)型推論完了できるという点にあります。
標準にLINQに習ってAPIを定義してみましょう。
publicstaticIRefEnumerable<TToEnumerator,TTo>Select<TFromEnumerator,TFrom,TToEnumerator,TTo>(thisIRefEunmerable<TFromEnumerator,TFrom>collection,Func<TFrom,TTo>func);
このような感じでしょうか? 細々と必要な型があるので他にもいくつか新規にファイルを作成します。 実際の所、NotImplementExceptionとして中身は空っぽなモックAPIです。 Unsafe.AsRefはin引数をref戻り値に変換します。
TToEnumeratorを引数から導出できず、センスが悪いですね。実際のUniNativeLinqでは新たにSelectEnumerable<TPrevEnumerable, TPrevEnumerator, TPrev, T, TAction>型を定義します。
New-ItemAPI/RefAction.csNew-ItemInterface/IRefAction.csNew-ItemUtility/DelegateRefActionToStructOperatorAction.csNew-ItemUtility/Unsafe.csNew-ItemCollection/SelectEnumerable.csNew-ItemCollection/SelectEnumerable.Enumerator.cs
RefAction.csとIRefAction.cs
namespaceUniNativeLinq{publicdelegatevoidRefAction<T0,T1>(refT0arg0,refT1arg1);publicinterfaceIRefAction<T0,T1>{voidExecute(refT0arg0,refT1arg1);}}
namespaceUniNativeLinq{publicreadonlystructDelegateRefActionToStructOperatorAction<T0,T1>:IRefAction<T0,T1>{privatereadonlyRefAction<T0,T1>action;publicDelegateRefActionToStructOperatorAction(RefAction<T0,T1>action)=>this.action=action;publicvoidExecute(refT0arg0,refT1arg1)=>action(refarg0,refarg1);}}
Unsafe.csは、System.Runtime.CompilerServices.Unsafeの一部抜粋です。
namespaceUniNativeLinq{publicstaticclassUnsafe{// ref T AsRef<T>(in T value) => ref value;publicstaticrefTAsRef<T>(inTvalue)=>thrownewSystem.NotImplementedException();}}
後にいい感じにこのモックAPIを処理します。
引数にreadonlyフィールドの参照を与えたら、その戻り値が変更可能な参照になります。SelectEnumerable.cs
namespaceUniNativeLinq{publicreadonlypartialstructSelectEnumerable<TPrevEnumerable,TPrevEnumerator,TPrev,T,TAction>:IRefEnumerable<SelectEnumerable<TPrevEnumerable,TPrevEnumerator,TPrev,T,TAction>.Enumerator,T>whereTPrevEnumerable:IRefEnumerable<TPrevEnumerator,TPrev>whereTPrevEnumerator:IRefEnumerator<TPrev>whereTAction:IRefAction<TPrev,T>{privatereadonlyTPrevEnumerableenumerable;privatereadonlyTActionaction;publicSelectEnumerable(inTPrevEnumerableenumerable){this.enumerable=enumerable;action=default;}publicSelectEnumerable(inTPrevEnumerableenumerable,inTActionaction){this.enumerable=enumerable;this.action=action;}publicEnumeratorGetEnumerator()=>newEnumerator(refUnsafe.AsRef(inenumerable),action);System.Collections.Generic.IEnumerator<T>System.Collections.Generic.IEnumerable<T>.GetEnumerator()=>GetEnumerator();System.Collections.IEnumeratorSystem.Collections.IEnumerable.GetEnumerator()=>GetEnumerator();}}
GetEnumerator()においてUnsafe.AsRef(in enumerable)と記述されています。
Unsafe.AsRefはreadonly制約を無視する危険なメソッドですが、この場合において問題はありません。
UniNativeLinqの提供する範囲において、全てのGetEnumeratorメソッドがreadonlyなメソッドであるからです。
この辺りをC#の型制約で保証できればよいのですが、出来ないため今回のようにUnsafe.AsRefを利用する必要があるのです。SelectEnumerable.Enumerator.cs
namespaceUniNativeLinq{publicreadonlypartialstructSelectEnumerable<TPrevEnumerable,TPrevEnumerator,TPrev,T,TAction>{publicstructEnumerator:IRefEnumerator<T>{privateTPrevEnumeratorenumerator;privateTActionaction;privateTelement;publicEnumerator(refTPrevEnumerableenumerable,inTActionaction){enumerator=enumerable.GetEnumerator();this.action=action;element=default;}publicboolMoveNext(){if(!enumerator.MoveNext())returnfalse;action.Execute(refenumerator.Current,refelement);returntrue;}publicvoidReset()=>thrownewSystem.InvalidOperationException();publicrefTCurrent=>thrownewSystem.NotImplementedException();TSystem.Collections.Generic.IEnumerator<T>.Current=>Current;objectSystem.Collections.IEnumerator.Current=>Current;publicvoidDispose(){}}}}
SelectEnumerableの実装はそこまで変なものではありません。
コンストラクタで必要な情報をフィールドに初期化し、GetEnumerator()でEnumeratorを返すだけのシンプルな作りです。
EnumeratorではIRefAction<T0, T1>を実装した型引数TActionのインスタンスactionを使用してMoveNext()する度にT型フィールドであるelementを更新しています。
このEnumeratorの最大の特徴は、 public ref T Current => throw new System.NotImplementedException();
です。
そう、未実装のままなのです。これはバグではなく極めて意図的な仕様です。
これをこのままビルドしてテストコードを追加してもエラーを吐くだけです。
本当は public ref T Current => ref element;
と記述したいのですが、C#の文法の制限として無理です。
UniNativeLinq.dllのポストプロセス用dotnet core 3.0プロジェクト
現在のワーキングディレクトリはUniNativeLinqHandsOnのはずです。
Mono.Cecilを利用してUniNativeLinq.dllを編集してSelectEnumerable.Enumerator.CurrentからNotImplementedExceptionを消し飛ばしましょう。
mkdirpost~cdpost~dotnetnewconsoleechobin/obj/post~.sln>.gitignoredotnetaddpackageMono.CecilNew-ItemDllProcessor.csNew-ItemInstructionUtility.csNew-ItemToDefinitionUtility.csNew-ItemGenericInstanceUtility.cs
PackageReferenceタグでMono.Cecilをインストール可能です。参考までにpost~.csprojの中身
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.0</TargetFramework>
<RootNamespace>_post</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Mono.Cecil" Version="0.11.1" />
</ItemGroup>
</Project>
Main関数はコマンドライン引数を2つ要求します。 ValidateArgumentsで引数の妥当性を検証します。 RewriteThrowNotImplementedException内部で使用されます。 特に気にする必要はない拡張メソッドです。Program.cs
usingSystem;usingSystem.IO;publicsealedclassProgram{staticintMain(string[]args){if(!ValidateArguments(args,outFileInfoinputUniNativeLinqDll,outFileInfooutputUniNativeLinqDllPath,outDirectoryInfounityEngineFolder)){return1;}using(DllProcessorprocessor=newDllProcessor(inputUniNativeLinqDll,outputUniNativeLinqDllPath,unityEngineFolder)){processor.Process();}return0;}privatestaticboolValidateArguments(string[]args,outFileInfoinputUniNativeLinqDll,outFileInfooutputNativeLinqDllPath,outDirectoryInfounityEngineFolder){if(args.Length!=3){Console.Error.WriteLine("Invalid argument count.");inputUniNativeLinqDll=default;outputNativeLinqDllPath=default;unityEngineFolder=default;returnfalse;}inputUniNativeLinqDll=newFileInfo(args[0]);if(!inputUniNativeLinqDll.Exists){Console.Error.WriteLine("Empty Input UniNativeLinq.dll path");outputNativeLinqDllPath=default;unityEngineFolder=default;returnfalse;}stringoutputNativeLinqDllPathString=args[1];if(string.IsNullOrWhiteSpace(outputNativeLinqDllPathString)){Console.Error.WriteLine("Empty Output UniNativeLinq.dll path");unityEngineFolder=default;outputNativeLinqDllPath=default;returnfalse;}outputNativeLinqDllPath=newFileInfo(outputNativeLinqDllPathString);unityEngineFolder=newDirectoryInfo(args[2]);if(!unityEngineFolder.Exists){Console.Error.WriteLine("Unity Engine Dll Folder does not exist");returnfalse;}returntrue;}}
IDisposableを実装したDllProcessorのインスタンスを生成し、Processメソッドを実行することで適切な処理を加えます。DllProcessor.cs
usingSystem;usingSystem.IO;usingSystem.Linq;usingMono.Cecil;usingMono.Cecil.Cil;internalstructDllProcessor:IDisposable{privatereadonlyModuleDefinitionmainModule;privatereadonlyFileInfooutputDll;publicDllProcessor(FileInfoinput,FileInfooutput){mainModule=ModuleDefinition.ReadModule(input.FullName);outputDll=output;}publicvoidProcess(){ProcessEachMethod(RewriteUnsafeAsRef);mainModule.Types.Remove(mainModule.GetType("UniNativeLinq","Unsafe"));ProcessEachMethod(RewriteThrowNotImplementedException,PredicateThrowNotImplementedException);}privatevoidProcessEachMethod(Action<MethodDefinition>action,Func<TypeDefinition,bool>predicate=default){foreach(TypeDefinitiontypeDefinitioninmainModule.Types)ProcessEachMethod(action,predicate,typeDefinition);}privatevoidProcessEachMethod(Action<MethodDefinition>action,Func<TypeDefinition,bool>predicate,TypeDefinitiontypeDefinition){foreach(TypeDefinitionnestedTypeDefinitionintypeDefinition.NestedTypes)ProcessEachMethod(action,predicate,nestedTypeDefinition);if(predicateisnull||predicate(typeDefinition))foreach(MethodDefinitionmethodDefinitionintypeDefinition.Methods)action(methodDefinition);}privatevoidRewriteUnsafeAsRef(MethodDefinitionmethodDefinition){Mono.Collections.Generic.Collection<Instruction>instructions;try{instructions=methodDefinition.Body.Instructions;}catch(NullReferenceException){return;}catch{Console.WriteLine(methodDefinition.FullName);throw;}for(inti=instructions.Count-1;i>=0;i--){Instructioninstruction=instructions[i];if(instruction.OpCode.Code!=Code.Call)continue;MethodDefinitioncallMethodDefinition;try{callMethodDefinition=((MethodReference)instruction.Operand).ToDefinition();}catch{continue;}if(callMethodDefinition.Name!="AsRef"||callMethodDefinition.DeclaringType.Name!="Unsafe")continue;instructions.RemoveAt(i);}}privateboolPredicateThrowNotImplementedException(TypeDefinitiontypeDefinition){if(!typeDefinition.HasFields)returnfalse;returntypeDefinition.Fields.Any(field=>!field.IsStatic&&field.Name=="element");}privatevoidRewriteThrowNotImplementedException(MethodDefinitionmethodDefinition){if(methodDefinition.IsStatic)return;FieldReferenceelementFieldReference=methodDefinition.DeclaringType.FindField("element").MakeHostInstanceGeneric(methodDefinition.DeclaringType.GenericParameters);ILProcessorprocessor=methodDefinition.Body.GetILProcessor();Mono.Collections.Generic.Collection<Instruction>instructions=methodDefinition.Body.Instructions;for(inti=instructions.Count-1;i>=0;i--){InstructionthrowInstruction=instructions[i];if(throwInstruction.OpCode.Code!=Code.Throw)continue;InstructionnewObjInstruction=instructions[i-1];if(newObjInstruction.OpCode.Code!=Code.Newobj)continue;MethodDefinitionnewObjMethodDefinition;try{newObjMethodDefinition=((MethodReference)newObjInstruction.Operand).ToDefinition();}catch{continue;}if(newObjMethodDefinition.Name!=".ctor"||newObjMethodDefinition.DeclaringType.FullName!="System.NotImplementedException")continue;newObjInstruction.Replace(Instruction.Create(OpCodes.Ldarg_0));throwInstruction.Replace(Instruction.Create(OpCodes.Ldflda,elementFieldReference));processor.InsertAfter(throwInstruction,Instruction.Create(OpCodes.Ret));}}publicvoidDispose(){using(Streamwriter=newFileStream(outputDll.FullName,FileMode.Create,FileAccess.Write)){mainModule.Assembly.Write(writer);}mainModule.Dispose();}}
throw new NotImplementedException();
を return ref this.element;
に置換します。
InstructionUtility.cs
usingMono.Cecil.Cil;internalstaticclassInstructionUtility{publicstaticvoidReplace(thisInstructioninstruction,Instructionreplace)=>(instruction.OpCode,instruction.Operand)=(replace.OpCode,replace.Operand);}
ILの命令を置換するための拡張メソッドです。
ILProcessorのReplaceメソッドはバグを誘発するわりと使い物にならないメソッドです。
gotoやif, switchなどのジャンプ系の命令の行き先にまつわる致命的なバグを生じます。
こうしてわざわざ拡張メソッドを用意する必要があるのです。ToDefinitionUtility.cs
usingMono.Cecil;internalstaticclassToDefinitionUtility{publicstaticTypeDefinitionToDefinition(thisTypeReferencereference)=>referenceswitch{TypeDefinitiondefinition=>definition,GenericInstanceTypegeneric=>generic.ElementType.ToDefinition(),_=>reference.Resolve(),};publicstaticMethodDefinitionToDefinition(thisMethodReferencereference)=>referenceswitch{MethodDefinitiondefinition=>definition,GenericInstanceMethodgeneric=>generic.ElementMethod.ToDefinition(),_=>reference.Resolve(),};}
Resolve()が例外を投げる可能性が結構あります。
GenericInstanceUtility.cs
usingMono.Cecil;usingSystem.Linq;usingSystem.Collections.Generic;internalstaticclassGenericInstanceUtility{publicstaticFieldReferenceFindField(thisTypeReferencetype,stringname){if(typeisTypeDefinitiondefinition)returndefinition.FindField(name);if(typeisGenericInstanceTypegenericInstanceType)returngenericInstanceType.FindField(name);vartypeDefinition=type.ToDefinition();varfieldDefinition=typeDefinition.Fields.Single(x=>x.Name==name);if(fieldDefinition.Module==type.Module)returnfieldDefinition;returntype.Module.ImportReference(fieldDefinition);}publicstaticFieldReferenceFindField(thisTypeDefinitiontype,stringname)=>type.Fields.Single(x=>x.Name==name);publicstaticFieldReferenceFindField(thisGenericInstanceTypetype,stringname){vartypeDefinition=type.ToDefinition();vardefinition=typeDefinition.Fields.Single(x=>x.Name==name);returndefinition.MakeHostInstanceGeneric(type.GenericArguments);}publicstaticFieldReferenceMakeHostInstanceGeneric(thisFieldReferenceself,IEnumerable<TypeReference>arguments)=>newFieldReference(self.Name,self.FieldType,self.DeclaringType.MakeGenericInstanceType(arguments));publicstaticGenericInstanceTypeMakeGenericInstanceType(thisTypeReferenceself,IEnumerable<TypeReference>arguments){varinstance=newGenericInstanceType(self);foreach(varargumentinarguments)instance.GenericArguments.Add(argument);returninstance;}}
CI.yamlをアップデート
post~によりUniNativeLinq.dllにポストプロセスをする必要があり、CI.yamlを書き換えます。
CI.yaml全文
name:CreateReleaseon:push:branches:-developjobs:buildReleaseJob:runs-on:ubuntu-lateststrategy:matrix:unity-version:[2018.4.9f1]user-name:[pCYSl5EDgo]repository-name:[HandsOn_CSharpAdventCalendar20191201]exe:['/opt/Unity/Editor/Unity']steps:-uses:pCYSl5EDgo/setup-unity@masterwith:unity-version:${{ matrix.unity-version }}-name:License Activationrun:|echo -n "$ULF" > unity.ulf${{ matrix.exe }} -nographics -batchmode -quit -logFile -manualLicenseFile ./unity.ulf || exit 0env:ULF:${{ secrets.ulf }}-run:git clone https://github.com/${{ github.repository }}-uses:actions/setup-dotnet@v1.0.2with:dotnet-version:'3.0.101'-name:Builds DLLrun:|cd ${{ matrix.repository-name }}/core~dotnet build -c Release-name:Post Process DLLrun:|cd ${{ matrix.repository-name }}/post~ls -l ../Assets/Plugins/UNL/dotnet run ../core~/bin/Release/netstandard2.0/UniNativeLinq.dll ../Assets/Plugins/UNL/UniNativeLinq.dllls -l ../Assets/Plugins/UNL/-name:Run Testrun:${{ matrix.exe }} -batchmode -nographics -projectPath ${{ matrix.repository-name }} -logFile ./log.log -runEditorTests -editorTestsResultFile ../result.xml || exit 0-run:ls -l-run:cat log.log-run:cat result.xml-uses:pCYSl5EDgo/Unity-Test-Runner-Result-XML-interpreter@masterid:interpretwith:path:result.xml-if:steps.interpret.outputs.success != 'true'run:exit 1-name:Get Versionrun:|cd ${{ matrix.repository-name }}git describe --tags 1> ../version 2> ../error || exit 0-name:Cat Erroruses:pCYSl5EDgo/cat@masterid:errorwith:path:error-if:startsWith(steps.error.outputs.text, 'fatal') != 'true'run:|cat versioncat version | awk '{ split($0, versions, "-"); split(versions[1], numbers, "."); numbers[3]=numbers[3]+1; variable=numbers[1]"."numbers[2]"."numbers[3]; print variable; }' > version_increment-if:startsWith(steps.error.outputs.text, 'fatal')run:echo -n "0.0.1" > version_increment-name:Catuses:pCYSl5EDgo/cat@masterid:versionwith:path:version_increment-name:Create Releaseid:create_releaseuses:actions/create-release@v1.0.0env:GITHUB_TOKEN:${{ secrets.GITHUB_TOKEN }}with:tag_name:${{ steps.version.outputs.text }}release_name:Release Unity${{ matrix.unity-tag }} - v${{ steps.version.outputs.text }}draft:falseprerelease:false-name:Upload DLLuses:actions/upload-release-asset@v1.0.1env:GITHUB_TOKEN:${{ secrets.GITHUB_TOKEN }}with:upload_url:${{ steps.create_release.outputs.upload_url }}asset_path:${{ matrix.repository-name }}/core~/bin/Release/netstandard2.0/UniNativeLinq.dllasset_name:UniNativeLinq.dllasset_content_type:application/vnd.microsoft.portable-executable
変更箇所のみ抜粋
-name:Builds DLLrun:|mkdir artifactcd ${{ matrix.repository-name }}/core~dotnet build -c Release-name:Post Process DLLrun:|cd ${{ matrix.repository-name }}/post~dotnet run ../core~/bin/Release/netstandard2.0/UniNativeLinq.dll ../Assets/Plugins/UNL/UniNativeLinq.dll-name:License Activation
gitadd.gitcommit-m"[add]post~ prot-process project"gitpush
第3章 補足
UnityPackage化
ライブラリをGitHubから提供するならば素のDLLを提供するだけというのも不親切です。
やはりunitypackageファイルをGitHub Releasesから提供したいものです。
この節ではコマンドラインからUnityを操作してunitypackageを作成します。
Unityのコマンドラインの機能自体ではunitypackageを作成することは不可能ですが、プロジェクトのEditorフォルダ直下に存在するクラスのstaticメソッドを呼び出すことが可能です。
UnityEditor.AssetDatabase.ExportPackageメソッドをstaticメソッド内で呼び出してunitypackageを作成します。
現在のワーキングディレクトリはUniNativeLinqHandsOnのはずです。
mkdir-pAssets/EditorNew-ItemAssets/Editor/UnityPackageBuilder.cs
コマンドラインから呼び出すメソッドのシグネチャは必ずSystem.Actionである必要があります。 これは今回限りの約束事ですが、最後のコマンドライン引数がunitypackageの出力先のパスを示すようにします。 AssetDatabase.ExportPackageの第一引数にstring[]を渡してunitypackageを構築します。UnityPackageBuilder.cs
usingSystem;usingUnityEditor;namespaceHandsOn{publicstaticclassUnityPackageBuilder{publicstaticvoidBuild(){string[]args=Environment.GetCommandLineArgs();stringexportPath=args[args.Length-1];AssetDatabase.ExportPackage(new[]{"Assets/Plugins/UNL/UniNativeLinq.dll"},exportPath,ExportPackageOptions.Default);}}}
コマンドライン引数を扱いたい場合にはSystem.Environment.GetCommandLineArgsメソッドから適切に文節処理された文字列の配列を受け取りましょう。
ここで渡すファイルのパスはプロジェクトのルートに対する相対パスですね。
HandsOn.UnityPackageBuilder.BuildをGitHub Actionsから呼び出し、リリースに同梱します。yamlファイルの最後に追記する部分(インデントには気を付けてください)
-name:Create UnityPackagerun:${{ matrix.exe }} -batchmode -nographics -quit -projectPath ${{ matrix.repository-name }} -logFile ./log.log -executeMethod HandsOn.UnityPackageBuilder.Build "../UniNativeLinq.unitypackage"-run:cat log.log-name:Upload Unity Packageuses:actions/upload-release-asset@v1.0.1env:GITHUB_TOKEN:${{ secrets.GITHUB_TOKEN }}with:upload_url:${{ steps.create_release.outputs.upload_url }}asset_path:UniNativeLinq.unitypackageasset_name:UniNativeLinq.unitypackageasset_content_type:application/x-gzip
終わりに
UniNativeLinq本家ではエディタ拡張に関連して更にえげつない最適化やMono.Cecilテクニックが使用されています。
既存のLINQに比べて非常に高速に動作しますので是非使ってください。
このハンズオンよりも更に深くMono.CecilやUniNativeLinqを学びたいという方は私のTwitterのDMなどでご相談いただければ嬉しいです。
画像はUnityのサイトより引用 ↩