はじめての C# 大統一理論
はじめに
この記事は Unity Advent Calendar 2023 シリーズ1 の 11日目の記事になります。
前日は @ruccho_vector さんの「Source Generatorの使いみち」でした。
Unity でも近代的な C# の機能が使えるようになってきており、Source Generator はパフォーマンスや開発効率にダイレクトに関わってきたりするので使いこなせると Good ですね!
Unity を使う上で C# という言語は切っても切り離せないものですが、Unity だけを触っている場合は Unity がヨシナにやってくれるアレコレは学習の機会が少なかったりします。
そこで本稿では、「今まで Unity しか触ってこなかった」という方向けに C# 大統一理論を実践するための第一歩として、C# 大統一理論の概要を説明しつつ、「.csproj
とか .sln
を用いたプロジェクト・ソリューションの管理」や「MSBuild を用いたオートメーション」について解説したいと思います。
C# 大統一理論について
is 何?
Unity を使ってゲーム開発を行っている人の大半(クソデカ主語)がお世話になっているであろう Cysharp の代表取締役 河合宜文さん (以下 @neuecc さん)が提唱されている理論で、ざっくり言えば「クライアントもサーバサイドも C# で書けば幸せになれるんじゃね?」と言う考え方です。
こちらのポスト(ツイート)からリンクされている gamebiz さんのインタビュー記事でも次のように言及されています。
開発する領域は、クライアント側、サーバー側問わず、ゲームに関してC#でやれるあらゆることを突き詰めていく
2021年2月に開催された CA.unity #1 でも、大統一理論の根幹を為すとも言える MagicOnion に関する講演をされているので、気になる方は Unity Learning Materials や logmi Tech さんの文字起こし記事をご覧になってみてください。
(ちなみに、私も CA.unity #1 で LT 登壇しており、UPM とか Addressables とかについて語っています (logmi Tech) ので興味ある方はそちらもどうぞ 😁)
また、2020年1月に開催された Unity 道場 京都スペシャル4でも MagicOnion に関連する講演をされており、この時のスライドの42ページ目に感銘を受けたこともあり、「オレもいつか大統一するぞ…!」と心に秘めた状態でお仕事しておりました。
何が嬉しいの?
@neuecc さんのスライドに全て書かれているんですが、掻い摘んで説明すると、以下のような嬉しさがあります。
- IDL (Interface Definition Language) を必要としないので、クライアント↔︎サーバ間の通信やモデルの型定義がそのまま利用できる
- プロジェクト構成にも依るが IDL のバージョン管理がツラくない
- やりようによってはロジックの共有も容易
- 言語知識をクライアント・サーバ間で共有できる
- (是非はあるが)エンジニアの行き来がしやすい
Why C# ?
ここまでの内容であれば「別に C# じゃなくても良いんじゃないの?」と思われるかもしれませんが、ことゲーム開発に於いてはクライアント側で選択できる言語にある程度の制約があり、Unity を使う場合には C# 一択です。(JavaScript ライクな UnityScript は2017年に、Boo は2014年に非推奨になってます。)
故に、Unity を用いるゲーム開発でクライアント・サーバの言語を大統一しようと思ったら C# 一択となるわけです。
はじめての C# 大統一理論
前提
今回、とある REST 風 API なバックエンドを必要とするプロジェクトで「C# 大統一理論を実践してみよう!」となり、以下のような構成で進めることにしました。
- クライアント: Unity 2022
- サーバ: ASP.NET Core
- Azure Functions にデプロイ
- EntityFramework Core を用いて RDBMS や Document DB と対話
MagicOnion にはいわゆる Web API を構築する機能が搭載されているので、そのまま MagicOnion に乗っかる選択肢もあったのですが、プロジェクト開始時点では PlayFab CloudScripts を通してエンドポイントと対話しようと思っていたので、素の ASP.NET Core な関数アプリを Azure Functions にデプロイする方針で進めていました。
(今となっては MagicOnion を使った方が楽だったろうなぁとは思っていますが、.NET なサーバサイドに関する勉強になったのでヨシとします 😅)
今回、基本的にクライアントとサーバのエンジニアはオーバーラップしないような配置でプロジェクトが進行していました。
(稀にサーバサイドのエンジニアがクライアント側に手を入れることはありましたが、殆ど独立して稼働していました。)
このことから、大統一するにあたって「DB スキーマや API インタフェースの共有コストを限りなくゼロに近づける」と言う点に主眼を置いており、本稿でもその辺りを掘り下げて紹介します。
方針
DB スキーマや API インタフェースのモデルをゼロコストで共有するための方法は幾つかのパターンが考えられると思いますが、今回のプロジェクトでは「クライアントとサーバとで DLL を共有する」方法を採択しました。
通常の .NET プロジェクトであれば DB スキーマや API インタフェースのモデルを定義した型をサーバサイドのプロジェクト(.csproj
)から切り離したプロジェクトに配置して、プロジェクトの参照をするだけで OK なのですが、Unity プロジェクトの場合ソリューション(.sln
)が別になってしまうため、Unity 側からモデル型を含むプロジェクトを参照することが非常に難しくなります。
(試してはいませんが、サーバ側のプロジェクトから Unity 側の .csproj
を無理やり書き換えるとかすればワンチャンありそうですが、Unity との編集合戦になりそうな気もするので、あまり良い手立てとは言えなさそうです。)
幸い、Unity は .dll
を素直(?)に読んでくれるので、サーバサイドのプロジェクトによってビルドされた DLL を Unity のアセットとして配置するだけで事足ります。
手順
以下の画像のようなコピーを行うと仮定します。
-
Monry.Sample.Client/
: クライアント側の Unity プロジェクト- ディレクトリ直下に Unity によって生成される
.sln
や.csproj
が生成される
- ディレクトリ直下に Unity によって生成される
-
Monry.Sample.Server/
: サーバ側の .NET プロジェクト -
Monry.Sample.Model/
: モデル型を定義する .NET プロジェクト- 今回 Server と Client とで共有したいヤツ
.csproj
編集
共有する型が定義されているプロジェクトの .csproj
ファイルを開きます。
JetBrains Rider であれば、Explorer ビューの該当プロジェクトを右クリックして Edit > Edit 'ProjectName.csproj' や F4
キー押下で開くことが出来ます。
<TargetFramework>
設定
必須ではないですが、TargetFramework を設定しておくと 👍 です。
Unity の場合、利用できる .NET のバージョンは .NET Standard 2.1 が最新ということになっているので、そのように設定しておきます。
言語バージョンはまぁなんでも良いんですが、record
使いたいこともあるかと思うので 9.0 にしておきましょう。
Null 許容参照型についてもお好みで。
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<LangVersion>9.0</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup>
<PropertyGroup>
定義
モデル型を定義したプロジェクトから見た時の Unity プロジェクトのパスや、Unity プロジェクト内の DLL 配置ディレクトリなんかを変数として定義しておきます。
<PropertyGroup>
<UnityProjectPath>$(MSBuildThisFileDirectory)..\Monry.Sample.Client\</UnityProjectPath>
<UnityPluginsPath>$(UnityProjectPath)Assets\Runtime\Plugins\</UnityPluginsPath>
</PropertyGroup>
次のような点がポイントでしょうか。
- MSBuild によって予約されたプロパティを上手いこと使ってあげる
- 定義済みのプロパティを再利用する
<Target>
定義
本稿の肝です。
MSBuild に於ける Target 要素の機能を利用することで、DLL や PDB (Programm DataBase) ファイルを容易にコピー可能です。
<Target Name="CopyDllToUnity" AfterTargets="Build" Condition="'$(Configuration)' == 'Release'">
<Copy SourceFiles="$(TargetDir)$(AssemblyName).dll" DestinationFolder="$(UnityPluginsPath)" />
<Copy SourceFiles="$(TargetDir)$(AssemblyName).pdb" DestinationFolder="$(UnityPluginsPath)" />
</Target>
一つ前の節で定義したプロパティを利用して、宛先ディレクトリを指定しています。
Condition="'$(Configuration)' == 'Release'"
として、Release 設定でビルドした時にだけコピー処理が走るようにしていますが、この辺はお好みで。
完成!
ここまでをまとめると以下のような .csproj
ファイルが出来上がります。
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<LangVersion>9.0</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup>
<PropertyGroup>
<UnityProjectPath>$(MSBuildThisFileDirectory)..\Monry.Sample.Client\</UnityProjectPath>
<UnityPluginsPath>$(UnityProjectPath)Assets\Runtime\Plugins\</UnityPluginsPath>
</PropertyGroup>
<Target Name="CopyDllToUnity" AfterTargets="Build" Condition="'$(Configuration)' == 'Release'">
<Copy SourceFiles="$(TargetDir)$(AssemblyName).dll" DestinationFolder="$(UnityPluginsPath)" />
<Copy SourceFiles="$(TargetDir)$(AssemblyName).pdb" DestinationFolder="$(UnityPluginsPath)" />
</Target>
</Project>
この設定を保存した状態で、Release なビルドを作成すると、Unity プロジェクト側に DLL と PDB がコピーされます。
更新毎にビルドするような設定を書いてあげれば、ビルド忘れとかも防げて安全かもしれません。
おわりに
昨日の ruccho さんのアドカレで紹介されている Source Generator を用いれば、モデル型の一覧を定義した型を宣言するコトとかも簡単にできるので、クライアント側の開発効率アップが期待できます。
ファクトリな Generics 型を作っておいて、それを DI コンテナに登録しておく、みたいなコードを自動生成しても良いかもしれません。
なお、本稿の執筆に差し当たって、動作検証用に簡単なプロジェクトを作成してありますので、興味のある方はご覧くださいませ。
明日の Unity Advent Calendar 2023 シリーズ1 は @Trapezoid さんの「Unity Searchを拡張する話(QueryEngine編)」です。
「Unity UI 完全に理解した勉強会」にご登壇いただいた際に発表いただいた Unity Search に関する記事ということで、とても楽しみです!
Discussion