- 投稿日:2020-12-04T21:15:30+09:00
PHPer&HTML5が好きな自分がなぜC#に惚れたのか
ご無沙汰しております。
Hiro_Matsunoです。
ほぼ10ヶ月ぶりかなぁ。
qiitaで投稿するのはということはさておきなぜこのテーマになったかですが。
実は現在C#よりも今年11月からPHPerとして復帰してしまいましたのでこのテーマとさせていただきます。
今回はポエムになってしまいますが許してくださいm(_ _)m。
この投稿はC#アドベンドカレンダー4日目の投稿です。実は職場には内緒で書いてるのでw。
C#を最初はどう思っていたのか
実は好意的に見ていませんでした。
どちらかというと自分はOSS属性の人間でLinux大好き人間だったのでJavaのNull使える版じゃないのとしか見ていませんでした。
仕事で使う機会もなかったのもあってかなかなか触る機会がなかったので食わず嫌い王でしたね。
本当にPHPとjQueryとHTMLで飯を食っていましたので本当に使うということが少なかったのです。
使っては見たかったんですが余裕ゼロのカツカツの年末年始盆暮れ正月なしの夜11時までハンドスクラッチしていたほどだったので実は業界も一回離れて戻ってくるといった経歴の持ち主です。
とういうか過食と拒食を繰り返した上胃も腸もボロボロの上持病も悪化していた時期でもあったので致し方なしでしたね。なぜC#に出会ってしまったのか
実は久方ぶりの業界復帰はこともあろうか今主流のRPAでした。
けれどあまり良く使いこなせなかったというよりも実際いうと実業と合ってない部分が多い某RPAに出会ってしまっていたので大変でした。
正直そのソフトが悪さをしていることを認めてもらえたのは辞める直前でした。
なんとその後に以前であって意気投合していた社長さんの会社に務めることとなりC#に出会いました。
実際使ってみたら意外と思い通りにかけるし楽できる部分も多かったです。
特にデータセットやLinQやエンティティフレームワークなどデータベースとの接続を楽にしてくれるフレームワークが多く当時の自分にとってこれほど素晴らしいプログラム言語はないなと思ったほどです。
Windowsフォームの開発でしたがWebと同じ感覚で開発できたので良かったと思っています。
もっと早く出会っていればなぁと思っていた部分も結構多かったので反省させられた部分も多くあります。正直言っておく多くのプラットフォームで使いたいならC#を使え
なぜこれを言うのかというと実はC#は結構多くのプラットフォームで動く言語です。
Xamarin知ってますでしょうか。
オリジナルのC#を多くのプラットフォームに使ってもらうためのフレームワークです。
これを使うことによりMac・Linux・iOS・Androidへの移植が簡単になってきました。
実はこのことが一番の自分にとっての反省だったのです。
C#知っていたらもっと楽にクロスプラットフォームの開発できたのになと。
結構HTML5とjQueryMobileなどにはない特徴があるのでみんな良ければ使ってください。
Unity使えなんて言いません。
Xamarin使えと自分は言います。
実は巷に有名な接触確認アプリCOCOAも実はXamarinです。
C#覚えるといろんな開発に使えるということがわかります。いま初心者に勧めるとしたらPythonやJavaScriptよりC#だ
正直言っておきます。
JavascriptはECMAの登場とともに特殊になりすぎてVirtualDOMよりも大変な労力がいる様になってきました。
正直昔みたいにjQueryでちょいと書いてお金がもらえる時代とは違いクラス構造・アロー関数の登場で結構特殊になりかけてます。
自分はC#やPHPerだったのでかけましたが昔より残念ながら気楽さがなくなってきました。
Pythonは自由にかけるけど正直ソースが雑然になることが多いです。
だからこそ自分は今はXamarinやC#を覚えることを勧めたいと思っています。
意外と初心者でもクラスの概念さえわかれば簡単ですし画面をちょくに見ながらの開発ができるのが一番の特色です。
実は一番大嫌いな言語のVBも同じことできますが一番直感的に書くのに適していると自分は思っています。
でもPHPには負けるかな。
あれはバックエンド強強だしクラス書くのも簡単だしっておいおい。
だけど画面を直感的に作りながら開発できるからこそC#を強く推すのです。
ちなみにCode For AichiがCallForCodeでトップ50に入ったアプリも実はC#です。
名古屋にいるC#マイスターの三浦くん達が毎年CallForCodeにチャレンジしてくれてます。
今年はどうだったんだ三浦くん。皆さんソースが見たかった人多かったと思いますが申し訳ございません。
C#書く機会が減ってしまったのであえて今回はポエマーとさせていただきます。
長々と書きましたが本当にC#は良いプログラム言語です。
みんなもどんどんC#を書いて色んな所で役立てましょう。
皆さんお付き合いしてくださりありがとうございました。明日は@naminodarieさんです。
お楽しみに。
- 投稿日:2020-12-04T20:53:06+09:00
【Unity(C#)】Mirrorで同期通信① マッチング機能
Unity #2 Advent Calendar 2020
こちらは Unity #2 Advent Calendar 2020 の 6日目の記事です。
Mirror
オープンソースのネットワークライブラリ(アセット)です。
プレイヤーのマッチングに公式サーバーが必要ないので
同一LAN内が担保されていれば接続が可能です。当然、サーバーをゴリゴリ頑張れば自前運用も可能です。
【参考リンク】:無料で使えるネットライブラリMirrorのざっくり紹介
公式Discordに参加してみましたが、
アップデートが頻繁に行われているのもあってか、
実装上の質問も飛び交って賑やかでした。(全部英語です)今回やること
①同一LAN内のサーバー(ホスト)を検索
②サーバーが見つかればクライアントとして接続、なければ自身がサーバー(ホスト)になる
③サーバー(ホスト)がマッチングを確認し、ゲームを開始する
④サーバー(ホスト)、各クライアント、共にシーン遷移する一言でまとめるとオートマッチングシステムを作ります。
バージョン
Unity 2019.4.8f1
Mirror 26.2.2
UniTask.2.0.18デモ
左上が自動でホストになり、残りの3画面がクライアントとして接続を試みます。
同一LAN内が前提なのでIPアドレスの入力などは省略できます。
コード(CustomNetworkDiscovery )
まずはサーバーを検索し、接続するための処理を担うコードです。
using System; using System.Threading; using Cysharp.Threading.Tasks; using Mirror; using Mirror.Discovery; using UnityEngine; using UnityEngine.UI; /// <summary> /// サーバー検索、接続 /// </summary> public class CustomNetworkDiscovery : NetworkDiscovery { [SerializeField] private Button _multiPlayButton; [SerializeField] private Button _backButton; [SerializeField] private Button _playButton; [SerializeField] private Text _playerCountText; [SerializeField] private Text _connectionStateText; //SceneのアトリビュートはMirrorに用意されている便利機能 //Inspectorでシーンを参照してコード内で文字列として使用できる [SerializeField,Scene] private string _gameSceneName; private ServerResponse _discoveredServer; private CancellationTokenSource _cancellationTokenSource; private const int CONNECT_INTERVAL_TIME = 2; private const int WAIT_TIME = 2; private const int CONNECT_TRY_COUNT = 1; private const string CONNECTION_STATUS_CLIENT_WAITING = "Waiting start..."; private const string CONNECTION_STATUS_HOST_WAITING = "Waiting other player..."; private const string CONNECTION_STATUS_SUCCESS = "Success!"; private bool _isHostReady; private NetworkManager _networkManager; private void OnDestroy() { //シーン遷移などで破棄されたタイミングで検索をやめる StopDiscovery(); } private void Awake() { //データ受信の準備 NetworkClient.RegisterHandler<SendHostReadyData>(ReceivedReadyInfo); NetworkClient.RegisterHandler<SendPlayerCountData>(ReceivedPlayerCountInfo); //サーバー見つけたらこれが呼ばれる OnServerFound.AddListener(serverResponse => { //見つけたサーバーを辞書に登録 _discoveredServer = serverResponse; Debug.Log("ServerFound"); }); //サーバーの検索&接続開始 _multiPlayButton.onClick.AddListener(() => { Debug.Log("Search Connection"); _backButton.transform.gameObject.SetActive(true); _multiPlayButton.transform.gameObject.SetActive(false); //接続を試みる _cancellationTokenSource = new CancellationTokenSource(); CancellationToken token = _cancellationTokenSource.Token; TryConnectAsync(token).Forget(); }); //最初の画面に戻る _backButton.onClick.AddListener(() => { Debug.Log("Cancel"); //サーバーから抜ける //サーバーの検索停止 StopDiscovery(); NetworkManager.singleton.StopHost(); //非同期処理止める _cancellationTokenSource.Cancel(); _cancellationTokenSource.Dispose(); }); //ホスト側にのみ表示されるボタン プレイボタン押下で準備完了とする _playButton.onClick.AddListener(() => { Debug.Log("Ready Ok"); //各クライアントにフラグデータを送る SendHostReadyData sendData = new SendHostReadyData() {IsHostReady = true}; NetworkServer.SendToAll(sendData); _playButton.transform.gameObject.SetActive(false); }); } /// <summary> /// サーバーから受け取ったデータを各クライアントで使う /// </summary> /// <param name="conn">コネクション情報 関数内で使ってないけど必要みたい</param> /// <param name="receivedData">受け取ったデータ</param> private void ReceivedReadyInfo(NetworkConnection conn, SendHostReadyData receivedData) { //ローカルのフラグに反映 _isHostReady = receivedData.IsHostReady; } /// <summary> /// サーバーから受け取ったデータを各クライアントで使う /// </summary> /// <param name="conn">コネクション情報 関数内で使ってないけど必要みたい</param> /// <param name="receivedData">受け取ったデータ</param> private void ReceivedPlayerCountInfo(NetworkConnection conn, SendPlayerCountData receivedData) { if (_playButton == null) return; _playerCountText.text = receivedData.PlayerCount + "/" + _networkManager.maxConnections; } /// <summary> /// 接続を試みる /// 非同期 /// </summary> private async UniTaskVoid TryConnectAsync(CancellationToken token) { _networkManager = NetworkManager.singleton; int tryCount = 0; //サーバーの検索開始 StartDiscovery(); //サーバーに接続するまでループ while (!_networkManager.isNetworkActive) { //n秒間隔で実行 await UniTask.Delay(TimeSpan.FromSeconds(CONNECT_INTERVAL_TIME), cancellationToken: token); //サーバー発見した場合 if (_discoveredServer.uri != null) { Debug.Log("Start Client"); //クライアントとして接続開始 _networkManager.StartClient(_discoveredServer.uri); //接続ステータスの文言変更 _connectionStateText.text =CONNECTION_STATUS_CLIENT_WAITING; //サーバーの検索停止 StopDiscovery(); //ここでホストの開始フラグを待つ await UniTask.WaitUntil(() => _isHostReady, cancellationToken: token); //接続ステータスの文言変更 _connectionStateText.text = CONNECTION_STATUS_SUCCESS; } //サーバー見つからない場合 else { Debug.Log("Try Connect..."); //接続を試みた回数をカウントアップ tryCount++; //任意の回数以上接続に試みて失敗した場合は自身がホストになる if (tryCount > CONNECT_TRY_COUNT) { Debug.Log("Start Host"); //ホストになる(サーバー) _networkManager.StartHost(); //サーバーあるよーってお知らせする AdvertiseServer(); //接続ステータスの文言変更 _connectionStateText.text = CONNECTION_STATUS_HOST_WAITING; //プレイボタン表示 _playButton.gameObject.SetActive(true); //ここでホストの開始フラグを待つ await UniTask.WaitUntil(() => _isHostReady, cancellationToken: token); //接続ステータスの文言変更 _connectionStateText.text = CONNECTION_STATUS_SUCCESS; //n秒待つ await UniTask.Delay(TimeSpan.FromSeconds(WAIT_TIME), cancellationToken: token); //シーン遷移 _networkManager.ServerChangeScene(_gameSceneName); } } } } }
NetworkDiscovery
サーバーを検索、もしくはサーバーが自身の存在を通知する機能を持ちます。
NetworkDiscovery
はそのまま使用することもできますが、
UIをカスタマイズしたかったり、
シーン遷移時のフェードアニメーションなどを追加したかったりする場合には
カスタムしないと難しいです。そのために継承して利用しています。
StartDiscovery
,StopDiscovery
,AdvertiseServer
などは
NetworkDiscovery
の機能に当たります。これらの機能は名前のまんまです。
ただし、シーン遷移時にしっかりとサーバーの検索、通知を停止させないと
サーバーは停止しているのにレスポンスだけは返ってくるという謎の減少が起きるので
OnDestroy
で確実にStopDiscovery
するのが安全だと思います。
NetworkServer.SendToAll
サーバー内のすべてのクライアント(ホスト含む)に引数で指定したデータを送信します。
CustomNetworkDiscovery
内ではホストがプレイボタンを押したことを各クライアントに通知しています。//ホスト側にのみ表示されるボタン プレイボタン押下で準備完了とする _playButton.onClick.AddListener(() => { Debug.Log("Ready Ok"); //各クライアントにフラグデータを送る SendHostReadyData sendData = new SendHostReadyData() {IsHostReady = true}; NetworkServer.SendToAll(sendData); _playButton.transform.gameObject.SetActive(false); });
NetworkClient.RegisterHandler
先ほどの
SendToAll
でデータが送られてきたことを検知し、
各クライアントでデータの受信時に行いたい処理を登録できます。(引数の
NetworkConnection
は別になくても動きます。)private void Start() { //データ受信の準備 NetworkClient.RegisterHandler<SendHostReadyData>(ReceivedReadyInfo); } /// <summary> /// サーバーから受け取ったデータを各クライアントで使う /// </summary> /// <param name="conn">コネクション情報 関数内で使ってないけど必要みたい</param> /// <param name="receivedData">受け取ったデータ</param> private void ReceivedReadyInfo(NetworkConnection conn, SendHostReadyData receivedData) { //ローカルのフラグに反映 _isHostReady = receivedData.IsHostReady; }やり取りするデータも別途定義が必要となります。
NetworkMessage
というインターフェースを実装することで
やり取りが可能なデータとなります。using System; using Mirror; /// <summary> /// 送信するデータ /// </summary> [Serializable] public struct SendHostReadyData : NetworkMessage { /// <summary> /// ホストが準備できたかどうか /// </summary> public bool IsHostReady; }コード(CustomNetworkManager)
次に接続にまつわるコードです。
using Mirror; using UnityEngine; using UnityEngine.SceneManagement; /// <summary> /// 接続にまつわるいろいろ /// </summary> public class CustomNetworkManager : NetworkManager { [SerializeField,Scene] private string _titleScene; [SerializeField,Scene] private string _mainScene; private Transform _playerTransform; private Material _playerMaterial; /// <summary> /// プレイヤー入室時にサーバー側が実行 /// </summary> /// <param name="conn">接続されたプレイヤーのコネクション</param> public override void OnServerAddPlayer(NetworkConnection conn) { Debug.Log("Add Player"); //タイトルシーンでのみ実行 if (_titleScene.Contains(SceneManager.GetActiveScene().name)) { //接続中の人数表記を変える SendPlayerCountData sendData = new SendPlayerCountData() {PlayerCount = NetworkServer.connections.Count}; NetworkServer.SendToAll(sendData); } //メインシーンでのみ実行 if (_mainScene.Contains(SceneManager.GetActiveScene().name)) { Debug.Log("Spawn Player"); //プレイヤー生成 GameObject player = Instantiate(playerPrefab); //今立ち上げているサーバーにプレイヤーを追加登録 NetworkServer.AddPlayerForConnection(conn, player); } } /// <summary> /// 各プレイヤー退室時にサーバー側が実行 /// </summary> /// <param name="conn">切れたコネクション</param> public override void OnServerDisconnect(NetworkConnection conn) { //接続中の人数表記を変える SendPlayerCountData sendData = new SendPlayerCountData() {PlayerCount = NetworkServer.connections.Count}; NetworkServer.SendToAll(sendData); Debug.Log("Anyone Disconnect"); base.OnServerDisconnect(conn); } /// <summary> /// サーバーとの接続が切れた時にクライアント側で呼ばれる /// </summary> public override void OnStopClient() { SceneManager.LoadScene(_titleScene); Debug.Log("Disconnect"); base.OnStopClient(); } }
NetworkManager
文字通りネットワークにまつわるいろいろを担います。
The Network Manager is a component for managing the networking aspects of a multiplayer game.
これもあまりそのまま使う想定のものではないので、
継承してメソッドをオーバーライドしてカスタムします。コールバック含め、大量に機能があるので今回使ったものだけ解説します。
StartHost, StartClient, StopHost
接続にまつわる関数です。
StartHost
を実行した場合、サーバーとクライアントの両方の役割を持つことになります。
StartClient
は引数に指定したアドレスのサーバーにクライアントとして接続します。
StopHost
は自身がサーバーならサーバーの接続を中断し、
クライアントならサーバーから抜けます。
NetworkManager
はシングルトンとなっており、
インスタンスをどこからでも呼び出せます。
StartHost
,StartClient
,StopHost
は全てPublic
な関数なので、
これらもどこからでも呼び出せるってことです。今回はサーバーの検索を担う、
CustomNetworkDiscovery
で接続にまつわる関数を呼び出しています。そうすることで、
・LAN内にサーバーが見つかったら→StartClient
・LAN内にサーバーが見つからなかったら→StartHost
のように同一LAN内で自動でマッチングする仕組みを作れます。
OnStopClient
クライアントがサーバーから切断された場合に各クライアントで呼び出されます。
このコールバックの中でシーン遷移を呼び出すことで
切断→シーン遷移 という処理が可能となります。すなわち、接続状態にあるクライアントで
StopHost
を呼び出せば
下記処理が呼ばれるということです。/// <summary> /// サーバーとの接続が切れた時にクライアント側で呼ばれる /// </summary> public override void OnStopClient() { SceneManager.LoadScene(_titleScene); Debug.Log("Disconnect"); base.OnStopClient(); }
OnServerAddPlayer
Mirrorにはプレイヤーという概念があります。
誤解を恐れずに簡単にまとめると
サーバーに接続したクライアントのことをプレイヤーと呼び、接続時にサーバーに追加されます。この
OnServerAddPlayer
はプレイヤーが追加された際に呼び出される処理です。デモにおける接続された人数の表記の変更の通知(プレイヤー増加時)は
OnServerAddPlayer
で行っています。/// <summary> /// プレイヤー入室時にサーバー側が実行 /// </summary> /// <param name="conn">接続されたプレイヤーのコネクション</param> public override void OnServerAddPlayer(NetworkConnection conn) { Debug.Log("Add Player"); //タイトルシーンでのみ実行 if (_titleScene.Contains(SceneManager.GetActiveScene().name)) { //接続中の人数表記を変える SendPlayerCountData sendData = new SendPlayerCountData() {PlayerCount = NetworkServer.connections.Count}; NetworkServer.SendToAll(sendData); } //メインシーンでのみ実行 if (_mainScene.Contains(SceneManager.GetActiveScene().name)) { Debug.Log("Spawn Player"); //プレイヤー生成 GameObject player = Instantiate(playerPrefab); //今立ち上げているサーバーにプレイヤーを追加登録 NetworkServer.AddPlayerForConnection(conn, player); } }また、プレイヤーを概念ではなく、実体として生成する場合もあるかと思います。
その場合、
OnServerAddPlayer
でInstantiateしてあげれば
各クライアントにプレイヤーが生成されます。ただし、この機能を利用するには
InspectorのPlayerPrefab
にNetworkIdentity
が付与されたPrefabを
事前に登録しておく必要があります。最後に
詳しくは知りませんがUNETという機能?がひと昔前にあったそうで、
それを改良したのがMirrorのようです。結構なビッグタイトルに採用されているようですが、
ドキュメント以外の情報がなかなか無いので苦労しました。私の今の力では及びませんがサーバー側の実装とかもいずれできるようになりたいです。
(UniTaskの実装は見よう見真似でやったので間違ってたら教えてください。)
- 投稿日:2020-12-04T20:40:08+09:00
【Unity(C#),MagicLeap】MLTK使ってハンドトラッキングでオブジェクトを操作
Magic Leap Advent Calendar 2020
この記事は、Magic Leap Advent Calendar 2020 の5日目です。
Magic Leap Challenge
2020年9月に「Magic Leap Challenge」というハッカソンに参加させていただきました。
デバイスを1週間貸りて、家で開発を行うリモートハッカソンです。私は料理アプリのMRバージョンを検証として作成しました。
作品:MRクッキングその際にUIを掴んで動かす実装について方法がわからず苦戦しました。
最終的にオレオレ実装でゴリ押ししました。下記GIFが実際に実装したものです。
位置を変えたい場合は摘まんで動かす、
向きを変えたい場合は押す という実装で乗り切りました。なぜ苦戦したかというと理由はシンプルで
指のボーンの情報に回転座標が含まれていないからです。
1週間という短い実装期間で座標からよしなにするのは無理でした。しかし、ハッカソンの懇親会で「MLTK使えばできるらしいよ」という
神の助言を頂いたので、今回はそのMLTKを使ってみた という内容になっています。MLTK(Magic Leap Toolkit)
MLTKはGithubからパッケージでインポートしました。
【参考リンク】:magicleap/Magic-Leap-Toolkit-Unityバージョンの違い次第で動かないことがあるようです。
私の試した環境のバージョン情報を置いときます。
Unity 2019.3.10f1
MLTK ? 記載なし
MLSDK v0.24.1デモ
実際に試してみたものが下記です。特にコーディングは必要ありませんでした。神。
MagicLeapのアドカレに参加しているので
— KENTO⚽️XRエンジニア?Zenn100記事マラソン挑戦中19/100 (@okprogramming) December 2, 2020
MLTKを触ってました?https://t.co/X0OdExlzYJ
MLTK使えばこういうのが
ほとんど何もしなくても実装できます?
もっと早くMLTK使って
おけばよかったと思いました? pic.twitter.com/e6aFzrqm4jHandInput
HandInput
というコンポーネントが用意されており、シーン上に配置する必要があります。
Palm Collisions
というパラメータをオンにすると
手を開いた状態でも手に当たり判定を付けることができます。DirectManipulation
DirectManipulation
はインタラクションさせたいオブジェクトにアタッチする必要があります。
Rigidbody
とCollider
も必要です。パラメーターは大量にありますが、言葉の通りで特に理解が難しいものはありませんでした。
SoundOnCollision
SoundOnCollision
をインタラクションさせたいオブジェクトにアタッチすると
オブジェクトと手が衝突した際に音が鳴ります。おわりに
他にも超便利機能が山盛りなようなようです。
もっと早く使っとけばよかったです。
- 投稿日:2020-12-04T19:42:02+09:00
UniRxをミニゲームを作りながら学ぶ
はじめに
本記事はLife is Tech!Advent Calendar 6日目の記事です!
ぜひ最後まで目を通していってください〜よろしくお願いします!><さて、タイトルにもある通り今回はUnityの便利ライブラリ、UniRxについての入門解説記事を書いていこうと思います。
ですが、僕自身もUniRxを初めて知った頃は、
- 何から学べば良いのか分からない。
- ネットの解説記事やコードを見ていても何が書いてあるかが分からない。
などなど何度も挫折しました。
ただやはりそんな中でも、無理やりでも書いていくと何となく分かるようになったので、今回は
- Unityでゲームを少しでも作ったことがある人
- UniRx学んでみたい・書いてみたいけど何すれば良いか分からない人
などの人でも読み進めていけて、書き方が何となくでも身につくことを目指す記事にしたいなと思います。
(事前にLINQなどの知識があるとより学びやすいです。)ということで今回は簡単なミニゲームを作りながら、
UniRxを使用しない記法・使用した記法
を並べ、主にUniRxでの書き方を身につけていきましょう。
細かい部分まで詳しく説明していくとキリがないので、本記事で詳しく触れていないよく分からない部分に関しては、自分でも随時調べながら進めていくと尚良しです!UniRxは使えるようになるととても便利だと思うので、初心者や学びたての方でも、後々のために存在だけでも知っておいて欲しいなと思います。
それではいってみましょう。今回作るゲーム
アドベントカレンダーっぽくサンタを登場させたかったので、
サンタがプレゼントを集めたらクリア
のシンプルなゲームにしましょう。サンタがプレゼントを集める……非常に謎ですがそこは触れないでください。
実装
0. プロジェクトの準備
0-1. プロジェクトのダウンロード
もし今回の記事を自分でも書きながら進めたい人がいましたら、下のリンクからまずプロジェクトをダウンロードしてください。
サンタやステージのアセット(無料)は事前に入れてある & 今回触れないコードは少しですがすでに事前に書いてあります。
Unityのバージョンは 2019.1.14f を使用しています!プロジェクトURL : https://github.com/kaku710/unirx_learn_project
0-2. Mainシーンを開く
Scenes/Mainを開いて、画像のようになっていたら大丈夫です!
ここから実際に機能をつけていきます!
1. プレイヤーが動く処理
それではまずはサンタを動かしていきましょう。
Create→C# Scriptから、SantaController.cs
を作ります。UniRx無しでシンプルに実装すると以下の感じでしょうか。
実装例 (UniRx無し)
SantaController.csusing UnityEngine; public class SantaController : MonoBehaviour { float speed = 5; void Update() { Vector3 v = new Vector3(Input.GetAxis("Horizontal"), 0, Input.GetAxis("Vertical")); Move(v); } void Move(Vector3 v) { transform.position += new Vector3(v.x * speed * Time.deltaTime, 0, v.z * speed * Time.deltaTime); } }これをUniRxを用いて実装すると以下のように実装することができます。
実装例 (UniRx使用)
SantaController.csusing UnityEngine; using UniRx; // UniRx使用時は忘れずに using UniRx.Triggers; // UpdateAsObservable使用に必要 public class SantaController : MonoBehaviour { float speed = 5; void Start() { this.UpdateAsObservable() .Select(_ => new Vector3(Input.GetAxis("Horizontal"), 0, Input.GetAxis("Vertical"))) .Subscribe(v => Move(v)); } void Move(Vector3 v) { transform.position += new Vector3(v.x * speed * Time.deltaTime, 0, v.z * speed * Time.deltaTime); } }軽く解説
ここやっていることとしては
1. UnityのUpdate関数
をStart内でUpdateAsObservable()
を利用してストリーム (一連の処理の流れのようなもの) というものに変換
( ストリームについて詳しく知りたい人はこちら )
2.Select()
を利用して入力の値を取得
3.SubScribe()
で関数を登録するというような流れです。
この実装だけでは何でストリームに変換する必要があるの?など、メリットがパッとしない人も多いと思いますが、これをしておくことで例えば今後
bool型のisGameOverがfalseの時だけ動かしたい
などの時には、
this.UpdateAsObservable() .Where(_ => !isGameOver) // この1行でMove関数を呼ぶ条件をフィルタリングできる .Select(_ => new Vector3(Input.GetAxis("Horizontal"), 0, Input.GetAxis("Vertical"))) .Subscribe(v => Move(v));
Where()
を利用してこんな感じで簡単に記述できたり、更に条件としてよくあるような
入力がない時はMove関数を実行したくない
などの時には、
this.UpdateAsObservable() .Where(_ => !isGameOver) .Select(_ => new Vector3(Input.GetAxis("Horizontal"), 0, Input.GetAxis("Vertical"))) .Where(v => v.magnitude > 0f) // この1行でさらに入力があるかどうかをフィルタリングする .Subscribe(v => Move(v));もう1度
Where()
を利用してこのようにフィルタリングすることで実装することができます。
Where()
はif()
のような役割ですね。ちなみにこのようなフィルタリングはUniRxにはとても便利なものが多く用意されています。ちなみに後でもいくつか出てくる。先に色々見たい方はこちら↓
逆引き記事 : UniRx オペレータ逆引き メッセージのフィルタ
UpdateAsObservable()
を使用するメリット個人的に感じる、
UpdateAsObservable()
を使用するメリットとしては、
- Update内のネストが深くなるのを防げる
- 複雑になればなるほど可読性が上がる
こんなところです。
メリットなどについてもっと詳しく知りたい!という方は、こちらの記事にもより詳しく書いてあるので、興味のある方はぜひ読んでみてください。ここら辺はよく分からない方というも、今はとりあえずこんな感じで書き換える方法もあるんだ!くらいに思ってもらえればOKです。どんどん使ってけば慣れていくはず!
(コードが書けたらサンタにアタッチするのを忘れずに)
ゲームを再生するとサンタが無事動いてくれました!(進行方向に向くコードは事前にSantaRotator.cs
に書いてあります。)
それでは次いきましょう。2. プレゼントに当たった時の処理
サンタがプレゼントに当たったら、プレゼントを削除 → カウント用の変数を増やす、の流れでいきましょう。
Prefabsフォルダ配下に"Present"のプレハブがあるので、
3つ程ステージ上に適当に配置してください。
また、タグを事前に"Present"に設定しています。そしたら先ほど作成した
SantaController.cs
にコードを追加していきます。まずUniRx無しだと以下のような感じでしょうか。(追記分のみ記載)
実装例 (UniRx無し)
SantaController.cspublic class SantaController : MonoBehaviour { int presentCount = 0; // プレゼントを数える変数 void OnTriggerEnter(Collider other) { if (other.CompareTag("Present")) { Destroy(other.gameObject); presentCount++; } } }これをUniRxを用いて以下のように実装します。
実装例 (UniRx使用)
public class SantaController : MonoBehaviour { int presentCount = 0; // プレゼントを数える変数 void Start() { this.OnTriggerEnterAsObservable() .Where(o => o.CompareTag("Present")) .Subscribe(o => { Destroy(o.gameObject); presentCount++; }); } }軽く解説
こちらもStart内で
OnTriggerEnter関数
をOnTriggerEnterAsObservable()
を利用してストリームに変換しています。
このようにUniRx.Triggers
を使うとUnityが用意しているコールバックをストリームにし、全てをAwake/Start内にまとめて記述することが可能になります。ちなみにUnityで用意されているコールバックイベントはほぼ全て用意されているそう。すごい。
Wiki : https://github.com/neuecc/UniRx/wiki/UniRx.Triggers補足 : ストリームを終了したい時
例えばこの衝突関係のストリームをゲームの途中で停止したい時があったとします。
そんな時は以下のように実装することで終了することができます。IDisposable disposable; void Start() { // ストリームをIDisposableに格納 disposable = this.OnTriggerEnterAsObservable() .Where(o => o.CompareTag("Present")) .Subscribe(o => { Destroy(o.gameObject); presentCount++; }); } void StopSubscribeStream() { disposable.Dispose(); // 停止したいタイミングでこのようにDispose()を実行 }このような感じでいつでも停止することもできます。めっちゃ便利。補足でした。
それではゲームがきちんと動くか確かめましょう!
このようにサンタが当たったらプレゼントが消えていればOKです!続いては、ここで用意した変数
presentCount
を用いてゲームクリア条件をつけていきましょう。3. ゲームクリア条件をつける
サンタがプレゼントを一定数(今回は3つ)集めたらクリアにしたいのですが、
UniRx無しで愚直に書くとこんな感じでしょうか。Clearシーンは事前に用意しています。実装例 (UniRx無し)
SantaController.cspublic class SantaController : MonoBehaviour { const int CLEAR_PRESENT_COUNT = 3; void OnTriggerEnter(Collider other) { if (other.CompareTag("Present")) { Destroy(other.gameObject); presentCount++; // ここから追記 if(presentCount >= CLEAR_PRESENT_COUNT) { UnityEngine.SceneManagement.SceneManager.LoadScene("Clear"); } } } }
ReactiveProperty
上記のクリア条件をUniRxを用いて実装したいのですが、UniRxを用いると、
変数の監視→値の変更を検知してActionを実行
といったようなことが簡単に行えるようになります。例えば、HPを監視して変更を検知したらHPバーに反映させたりとか。
それを実現してくれるのがUniRxのReactiveProperty
です。
これは本当に便利なので、使えるようになるとよりレベルアップできると思います。とりあえず手を動かして動くことを確認してみましょう。
まず先ほど
int型
で宣言したpresentCount
をReactiveProperty<int>
型で宣言しなおします。ReactiveProperty<int> presentCount = new ReactiveProperty<int>(0);またそれに伴って、
presentCount++
の部分がエラーになると思うので、以下のように書き換えてあげます。presentCount++; → presentCount.Value++;これで変数が監視できるようになったので、Start内で値の変更を監視するようにします!以下が実装例です。
実装例 (UniRx使用)
SantaController.cspublic class SantaController : MonoBehaviour { const int CLEAR_PRESENT_COUNT = 3; ReactiveProperty<int> presentCount = new ReactiveProperty<int>(0); void Start() { presentCount .Where(x => x >= CLEAR_PRESENT_COUNT) .Subscribe(_ => { UnityEngine.SceneManagement.SceneManager.LoadScene("Clear"); }); } }こんな感じで実装することができます!とても便利ですね。
ReactiveProperty
のメリットこれも色々あるかとは思いますが、個人的には
- MVPパターンに非常に有用なこと
が1番のメリットかなと思います。(あくまで個人的意見です)
MVPパターンとはデザインパターンの1つで、この設計はゲームの設計 (だけじゃ無く他のプロダクトでも) をしていく上でとても便利なのでぜひ知っておくと良いと思います。
この記事内では長くなるので解説しませんが、もしUnityでのMVPパターンについて詳しく知りたい方は以下の神記事を読むと良いです。(マジで僕もめちゃくちゃ読んだ)MVPパターンに関して今はあまり分からなくても、
ReactiveProperty
に関しては非常に便利なので、少しずつでも慣れていくと良いと思います。それではゲームを確かめてみましょう!
このようにプレゼントを3つ集めたらClearシーンに遷移すればOKです!4. リトライ機能をつける
最後はボタンを押した時のイベントについて。ゲーム的にもせっかくなのでClearシーンからMainシーンに戻れるようにしましょう。
ボタンは事前に置いていますが、クリックした時のイベントの設定はしていませんのでそれをUniRxを用いて行ってみましょう。UniRx無しの実装は
-publicで関数を定義してUGUIで紐付け
-onClick.AddListner()
を使用
このあたりかなと。こちらの実装例は省略します。適当にc#ファイルを新規で作成して、
実装例 (UniRx使用)
ClearPresenter.csusing UniRx; using UnityEngine; public class ClearPresenter : MonoBehaviour { public UnityEngine.UI.Button retryButton; // Inspectorで設定 void Start() { retryButton.OnClickAsObservable() .Subscribe(_ => { UnityEngine.SceneManagement.SceneManager.LoadScene("Main"); }); } }こんな感じで書いてあげれば実装できます。
UniRxを用いてボタンのイベントを実装すると色々と便利で、例えば、
1回押したら1秒は入力を受け付けないボタン
、などもbutton.OnClickAsObservable() .ThrottleFirst(System.TimeSpan.FromSeconds(1)) // 1秒間入力を受け付けない .Subscribe(_ => { // 押した時の処理 });こんな感じで1行追加してあげるだけで実装することができます!UniRx使わないとなると結構めんどくさそうなので、とても便利ですね。他にも色々あるので気になる人は調べてみてください。
では最後にゲームが動くかの確認をしましょう!
このようにボタンを押してMainシーンに戻ることができていたらOKです!最後に念のため、
実装した最低限のSantaController.csの全文
も載せておきます。SantaController.csusing UnityEngine; using UniRx; using UniRx.Triggers; public class SantaController : MonoBehaviour { float speed = 5; const int CLEAR_PRESENT_COUNT = 3; ReactiveProperty<int> presentCount = new ReactiveProperty<int>(0); void Start() { this.UpdateAsObservable() .Select(_ => new Vector3(Input.GetAxis("Horizontal"), 0, Input.GetAxis("Vertical"))) .Subscribe(v => Move(v)); this.OnTriggerEnterAsObservable() .Where(o => o.CompareTag("Present")) .Subscribe(o => { Destroy(o.gameObject); presentCount.Value++; }); presentCount .Where(x => x >= CLEAR_PRESENT_COUNT) .Subscribe(_ => { UnityEngine.SceneManagement.SceneManager.LoadScene("Clear"); }); } void Move(Vector3 v) { transform.position += new Vector3(v.x * speed * Time.deltaTime, 0, v.z * speed * Time.deltaTime); } }さいごに
UniRxについて初めて記事を書いてみましたが、いかがだったでしょうか。
今回紹介したUniRxの機能は本当にほんの一部で、まだまだ便利な機能がいっぱいあります!自分もまだまだ勉強中です。もっとUniRxについて詳しく知りたい・学びたい方は、
などなど読んでみると良いと思います。ちなみに本記事の参考もここです。(toRisouP様様ですね。)
ぜひ今後のゲーム開発に少しでも取り入れるきっかけになれば幸いです。
ではでは明日以降のLife is Tech!Advent Calendarもお楽しみに!ありがとうございました〜!
- 投稿日:2020-12-04T18:55:30+09:00
C# .net、Unityアプリケーション間でMemoryMappedFileを用いてデータを共有する
やりたいこと
弊社の業務では、なんらかのセンサーを用いて情報を読み取り、その状況によって演出を行うといったことがよくあります。
演出をするアプリケーションはUnityで作成するとして、センサーを読み取る部分をどのように実装するかが問題になります。ネイティブプラグインもしくはライブラリとして実装する手もありますが、センサーの調整や制御が少し面倒になるので、
- アプリケーションをC#.NETのWindowsフォームアプリケーションで作成
- 演出アプリケーションをUnityで作成
というように、2つのアプリケーションで分けて行うことにしました。次のような恩恵がありました。
- センサー機器の担当者と演出の担当者の作業を分けることが出来る
- センサーの種類が変わっても対応できるやりかた
MemoryMappedFileを用いて、メモリ内でファイルのようなものを扱い、アプリ間でデータのやりとりを行います。
MSDNのMemoryMappedFileクラスドキュメント
構造体を直接バイナリファイルに書き込んでポインタで参照したり、BinaryFormatterで書き込む手もあると思います。それだとパフォーマンスは間違いなく出ると思いますが怖いので……
今回は安全にxmlにシリアライズしたデータをやりとりします。仕組みは単純で、センサーアプリ側ではデータを1つのクラスにまとめてxmlの形式にシリアライズし、メモリマップトファイルに書き込みます。演出アプリ側ではそれを読み取り、デシリアライズして元のクラスのインスタンスを復元することが出来ます。
ただし注意点として、2つのアプリでメモリを同時に読み書きしようとすると何らかの不具合が出るのは必至ですので、排他制御を確実に行います。共通(データ定義)
以下が受け渡しを行うデータの例です。ここではSensorDataクラスがシリアライズするクラスです。
SerializableAttribute属性を付与していますが、実はXmlSerializerではSerializableAttribute属性は不要でした。
念のため残しております。CensorData.cs[Serializable] struct SensorPoint { public float x; public float y; } [Serializable] class SensorData { public float time; public List<SensorPoint> points; }書き込み側
Censor.csclass hogeClass{ private Mutex mutex; // 排他制御に使用 MemoryMappedFile mmfile = null; // Formの作成時などに呼ばれる public void Initialize(){ string mutexName = "SensorAppMutex"; bool createdNew = false; mutex = new Mutex(false, mutexName, out createdNew); } // アプリ終了時に呼ばれる public void Finalize(){ mmfile?.Dispose(); mutex?.Dispose(); } // 共有するデータを保存したいタイミングで呼ぶ public void SaveSensorData(const SensorData data){ // シリアライザーの作成 System.Xml.Serialization.XmlSerializer serializer = new System.Xml.Serialization.XmlSerializer(typeof(SensorData)); bool getMutex = false; try { if (getMutex = mutex.WaitOne(5000)) { mmfile?.Dispose(); mmfile = MemoryMappedFile.CreateNew("SensorAppData", 1024 * 1024 * 1); using (MemoryMappedViewStream stream = mmfile.CreateViewStream()) { serializer.Serialize(stream, data); } } } finally{ if (getMutex) mutex.ReleaseMutex(); } } }読み込み側
Unityアプリ側の処理です。
センサーアプリから渡された座標に、オブジェクトを生成しています。Spawner.cs// センサーアプリから渡された座標に、オブジェクトを生成する public class Spawner : MonoBehaviour { Mutex mutex; // 排他制御 // 下記のSampleObjectコンポーネントを取り付けたゲームオブジェクトのプレハブ [SerializeField] GameObject sampleObject; void Start() { string mutexName = "SensorAppMutex"; bool createdNew = false; mutex = new Mutex(false, mutexName, out createdNew); } void OnDestroy() { mutex?.Dispose(); } void Update() { System.Xml.Serialization.XmlSerializer serializer = new System.Xml.Serialization.XmlSerializer(typeof(SensorData)); // センサーから取得した結果 SensorData data = null; bool getMutex = false; try { if (getMutex = mutex.WaitOne(3000)) { using (MemoryMappedFile mmfile = MemoryMappedFile.OpenExisting("SensorAppData")) using (MemoryMappedViewStream stream = mmfile.CreateViewStream()) { result = (SensorData)serializer.Deserialize(stream); } } catch (Exception ex) { // なにかまずいことが起こった場合、ログに残す Debug.LogError($"exception type: {ex.GetType()} msg: {ex.Message}"); throw ex; } finally { if (getMutex) mutex.ReleaseMutex(); } } // データが取得できたので、その位置にオブジェクトを生成する if (data != null) { foreach (var point in data.points) { Vector3 pos = new Vector3(point.x, point.y, 0); GameObject obj = Instantiate<GameObject>(samplePrefab, pos, Quaternion.identity, this.transform); } } } }SampleObject.cs// 生成されて一定時間で消えるゲームオブジェクトの例 public class SampleObject : MonoBehaviour { const float lifeTimeMax = 0.5f; float lifeTime = lifeTimeMax; void Update() { lifeTime -= Time.deltaTime; if (lifeTime < 0) { Destroy(this.gameObject); } } }
- 投稿日:2020-12-04T14:44:13+09:00
Unityでの経路探索とAIに何を使うか
はじめに
Unityで経路探索やAIを使うにあたって、
どんなものがあるのか?
どんな違いがあるのか?
どうやって使うのか?
を以下に絞って比較し、それぞれの簡単な使用方法を紹介します。
- 経路探索
- Unity標準のNavMesh
- A* Pathfinding Project
- AI
- StateMachine(ImtStateMachine)
- BehaviourTree(FluidBehaviorTree)
どんなものがあるのか
経路探索
主に見かけるのは以下の2つです。
- Unity標準のNavMesh
- Unityの標準機能なのでそのまま使えます。
- キャラが通れる経路をメッシュ状に表現したものです。
- A* Pathfinding Project
- NavMeshの他にも、GridGraph や PointGraph での経路探索もできるアセットです。
- Pro版とFree版があります。
AI
簡易なAIの手法として2つピックアップします。
(オープンワールドやMMOなどのスケールの大きいゲームでの複雑なAIでは、これらを組み合わせたり、別の手法にしたりといったアプローチが必要になると思われます。)
- StateMachine
- BehaviourTree
どんな違いがあるのか
経路探索の比較
項目 Unity標準のNavMesh A* Pathfinding Project 導入 マップとなるオブジェクトをStatic(もしくはNavigation-Static)に設定。 アセットのインポート。
Pathfinderコンポーネント追加。Editorでの経路生成 Window → AI → Navigation から設定してBake。 空オブジェクトにPathfinderコンポーネントを追加して、NavMesh/Grid/PointどれかのGraphを登録してScan。 動的な経路生成 NavMeshSurface.BuildNavMesh()
(オブジェクトをStaticにする必要はない)
別途GitHubからコードを拾ってくる必要がある。AstarPath.Scan()
事前にPathfinderコンポーネントに空Graphの追加が必要。移動経路のパス取得 NavMeshAgent に計算させるか、パスのみ計算する。 Seeker に計算させる。
Modifier コンポーネントを追加しておくとパスが自動補正される。移動 取得したパスを辿るか、NavMeshAgentで移動。
NavMeshAgentでspeed類を0にして自動移動させないようにした場合でも、updatePositionをtrueにしておくとNavMesh範囲から外れないように移動させられる。取得したパスを辿る。 障害物 NavMeshObstacleを配置するとNavMeshが更新される。 Collider.boundsを使用してGraphUpdateObjectを生成・反映すると更新される。
NavMesh用のNavmeshCut機能はPro版のみ。経路設定のセーブ・ロード BakeすることでNavMeshDataファイルを生成。 NavMesh.AddNavMeshData()
でロード。NavMesh.RemoveNavMeshData()
で破棄。Pathfinderコンポーネントからセーブしてファイル生成。 AstarPath.data.DeserializeGraphs()
でロード。AstarPath.data.RemoveGraph()
で破棄。AIの比較
項目 StateMachine BehaviourTree 全体把握 ステートが増えると全体像を把握しにくくなる。
特に遷移関係がややこしくなる。全体像を把握しやすい。 実行時の状態把握 現在の状態をContextに保持しておくなどして、CurrentStateTypeといった単一の情報で把握できる。 現在の状態を把握しづらい場合がある。
(各種状況が組み合ってTreeを走査するので、Stateなどの単一の情報で表現しづらい)
GUIでTree状態を見られれば全体像と同じく把握は容易。実装 ステートの追加にクラス自体の追加が必要となり、単純なステートだった場合は BehaviourTree と比較してコード記述が多くなる。 行動の追加に必要なコード量が、StateMachine と比較して少ない。
ただし、書き方によってはスパゲティになりやすい側面もある。追加・修正 行動の追加・修正時に全体像を把握せずに、関連するステートのみ把握していれば十分な場合がある。 行動の追加・修正時に全体像を把握しないといけない場合がある。
(内部まで詳細に把握する必要は無いが、関連しない行動であっても、Tree上の優先度やどの状況での行動なのかの把握が必要になる場合がある)アセットの比較
AIに後述するアセット(ImtStateMachine, FluidBehaviorTree)を使用した場合における比較です。
(AIの比較と重複する部分もあります。)
項目 ImtStateMachine FluidBehaviorTree 実装方法 ステートごとにクラスを実装。 単一コードでツリーを実装。
Treeを部分的にパーツとして作成し、別のTreeに追加することも可能。行動の切り替え 各ステートごとの遷移可否関係を設定しておく必要あり。
ステートを増やしたらその都度遷移関係の記述も必要。毎フレームTree全体を走査して、都度現状に対応した行動を処理させる。 実行 現在のステートのクラスのみが実行される。 毎フレームTree全体を走査するため、不要なチェックが発生する場合がある。
Wait や Continue で Tree全体の走査ではなく行動を継続させることも可能。全体把握 GUI表示機能なし。
ステート遷移を矢印で表現するとしたら、ステートが増えるのに応じて矢印も増えるため全体の把握が難しくなる。
遷移可否の関係性はImtStateMachine.AddTransition()
をまとめて記載することで把握しやすくはできる。
ただし、ステートクラス側で遷移ガードができるため把握しづらくなる場合がある。実行中にTreeをGUI表示可能。
(実行中のみ表示可能で、編集は不可。)
Treeを辿ることで、どの状況ならどの行動をするかを把握できる。
コード記述自体がTree状になるためGUIでなくても俯瞰での把握は難しくない。実行時の状態把握 CurrentStateType などを用意して現在のステートを把握できるようにすれば、エディタでも本番環境でもログ等ですぐ判別できる。
単一の状態として表現するため、今どの行動をしているのかは把握しやすい。エディタでの確認はTreeのGUI表示を見ることで可能。
実機環境用の確認機能はない。
実装するとしても、単一の状態としての表現ではなく全体の状況に応じて行動を選択するため、複数情報が必要。キャッチアップ 行動をクラスごとに切り替える作りであることが分かれば、理解は容易。
遷移関係の把握はステートが増えるごとにややこしくなるが、行動の実装・修正は単一ステートや関連ステートの一部のみの把握で済む場合もある。Sequence, Selector の組み合わせ方や優先順位の考慮が必要。
状況に応じて都度行動を変化させるため、全体像を把握しておかないと意図しない行動をさせてしまう場合がある。どうやって使うのか
Unity標準のNavMesh
Window → AI → Navigation を開きます。
Objectタブを選択した状態で、Hierarchyからマップとなる床や壁のオブジェクトを選択すると、対象オブジェクトの設定を変更できます。
Navigation Static にチェックして、Navigation Area を床であれば Walkable, 壁であれば Not Walkable にします。すべてのマップ用オブジェクトを設定したら、Bakeタブの中にあるBakeボタンを押してNavMeshを生成します。
SceneビューでBakeした結果を確認できます。実行中にNavMeshを生成・更新するには、NavMeshComponentsという追加機能群が必要です。
https://github.com/Unity-Technologies/NavMeshComponents
NavMeshSurfaceコンポーネントを使用することで、動的生成が可能になります。キャラクターなどの移動させたいオブジェクトに、NavMeshAgentコンポーネントを追加します。
SetDestination()
で移動先座標を設定すると、自動で移動し始めます。navMeshAgent.SetDestination(targetPosition);または、移動経路となるパスのみを取得することもできます。
var navMeshPath = new NavMeshPath(); NavMesh.CalculatePath(myPosition, targetPosition, -1, navMeshPath); // navMeshPath.corners に移動経路が入っていますA* Pathfinding Project
Free版があるので、ダウンロードしてみて採用するかどうか検討できます。
https://arongranberg.com/astar/download
(Pro版の機能が不要であればFree版を採用することもできます。)空オブジェクトを生成して、 Component → Pathfinding → Pathfinder でコンポーネントを追加します。
InspectorでAdd Mesh Graphから設定したいタイプを選択して追加し、Scanボタンで経路を生成します。
なお、NavMeshを使用する場合、Meshデータが必要となります。(アセット内にサンプルが入っています。)
GridGraphの場合は、NodeSize, Width, Depth等の数値設定のみで生成できます。
SceneビューでScanした結果を確認できます。実行中にNavMeshやGridGraphを生成・更新するには、Inspectorで設定済みのNavGraphを取得して、設定を更新するなどしてからScanします。
var graph = AstarPath.active.graphs[0] as NavMeshGraph; graph.sourceMesh = mesh; AstarPath.active.Scan();キャラクターなどの移動させたいオブジェクトに、Seekerコンポーネントを追加して、移動経路を取得できます。
astarSeeker.StartPath(myPosition, targetPosition, path => { if (!path.error) { // path.vectorPath に移動経路が入っています } });ImtStateMachine (StateMachine)
IceMilkTeaというフレームワークの一部として公開されている、ImtStateMachineを使用する例です。
https://github.com/Sinoa/IceMilkTea/blob/develop/Packages/IceMilkTea/Runtime/Core/UnitCode/PureCsharp/StateMachine.cs参照用のメンバを用意しておきます。
public class StateMachineAi : MonoBehaviour { // 各ステートクラスでの参照用 Character character; // ステート遷移用ID enum TransitionEventId { Move, Attack, } }ステートごとの挙動を記述したクラスを作成します。
// 移動ステート class MoveState : ImtStateMachine<StateMachineAi, TransitionEventId>.State { protected override void Update() { Context.character.Move(); // ターゲットに近づいたら攻撃ステートに遷移 if (Context.character.IsNearTarget()) { Context.stateMachine.SendEvent(TransitionEventId.Attack); } } }// 攻撃ステート class AttackState : ImtStateMachine<StateMachineAi, TransitionEventId>.State { protected override void Enter() { // ステート遷移時に攻撃開始 Context.character.Attack(); } protected override void Update() { // 攻撃が終わったら移動ステートに遷移 if (!Context.character.IsAttacking()) { Context.stateMachine.SendEvent(TransitionEventId.Move); } } }
Enter()
,Update()
以外にも、終了処理・特定時の遷移ガードといった機能もあります。初期化として、StateMachineを生成して、ステートの遷移条件を設定します。
void Start() { stateMachine = new ImtStateMachine<CasualEnemyStateMachineAi, TransitionEventId>(this); // 遷移条件の設定 stateMachine.AddTransition<MoveState, AttackState>(TransitionEventId.Attack); stateMachine.AddTransition<AttackState, MoveState>(TransitionEventId.Move); stateMachine.SetStartState<MoveState>(); }後は、ImtStateMachine.Update()を呼ぶことで、現在のステートのクラスのUpdateが動作します。
void Update() { stateMachine.Update(); }FluidBehaviorTree (BehaviourTree)
GitHubで公開されているFluidBehaviorTreeを使用する例です。
https://github.com/ashblue/fluid-behavior-tree
コードからTreeを作成し、Editorでの実行中にTreeの状態をGUIで確認することができます。
(蛇足ですが、Behavior / Bihaviour のu
が入るかどうかはアメリカ式かイギリス式かの違いで、意味は同じです。)Treeを生成します。
void Start() { tree = new BehaviorTreeBuilder(gameObject) .Selector() .Sequence("攻撃") .Condition(("攻撃していないか") => !character.IsAttaking()) .Condition(("ターゲットとの距離判定") => character.IsNearTarget()) .Do(() => { character.Attack(); return TaskStatus.Success; }) .End() .Sequence("移動") .Condition(("攻撃していないか") => !character.IsAttaking()) .Do(() => { character.Move(); return TaskStatus.Success; }) .End() .End() .Build(); }
Selector()
,Sequence()
は子をすべて処理するけれど途中の成否によって処理を終了する仕組みで、
Condition()
は条件判定、Do()
は内包する処理を実行する仕組みです。
他にも、処理の待機/継続・成否の反転・Treeのパーツ化といった機能もあります。後は、
BehaviorTree.Tick()
により、Treeを走査します。
MonoBehaviour.Update()
で呼べば、毎フレーム走査することになります。void Update() { tree.Tick(); }終わりに
結局の所どれを使えば良いのかは、ゲームの規模やジャンルによっても変わります。
いずれも一長一短あるため、GridGraphが使いたい、複雑なAIは不要、などの要望・状況に応じて選択できると良いかと思います。
Unityで経路探索・AIを実装したいけれどとりあえずどうしたらいいのか、といった方へのとっかかりとなることができれば幸いです。
- 投稿日:2020-12-04T11:40:50+09:00
.NET 5なWPFからWinRTのAPIを呼ぶ方法
はじめに
Microsoftは11月10日(現地時間) 「.NET Framework」と「.NET Core」フレームワークを統合した「.NET 5.0」のリリースが発表されました。
[速報]マイクロソフト「.NET 5」正式リリース。1つのフレームワークでWindows/Mac/Linuxのデスクトップ、サーバアプリ、Webアプリなどが開発可能に
https://www.publickey1.jp/blog/20/net_51windowsmaclinuxweb.html以前、Non UWPな環境から WinRTにアクセスするのが割と面倒だった記憶があるのですが、
.NET5からWindows10向けAPI(WinRT)にアクセスするのが簡単になったとの噂を聞いたので早速試してみました。テスト環境
Microsoft Visual Studio Professional 2019 Version 16.8.0
.NET 5.0 SDK(https://dotnet.microsoft.com/download/dotnet/5.0)サンプルコード
https://github.com/DandyMania/WinRT_Test共通の手順
- .NET 5.0 SDKをインストール
- VisualStudioを開いて、プロジェクトの新規作成から WPF App(.NET)を選択
- *.csprojをテキストエディタ等で開いて
<Project Sdk="Microsoft.NET.Sdk"> ... <TargetFramework>net5.0-windows10.0.19041.0</TargetFramework>に変更する。
※.NET5からはTargetFramework タグに net5.0-windows10.0.17763.0 のように Windows であることと
対象のバージョン番号を付けるだけで良くなったということらしい。
Nugetにて、Microsoft.Windows.CsWinRT と Microsoft.Windows.SDK.Contrac をインストール
あとは、using Windows.??? を定義してAPIを呼べばOK
トーストを表示する方法
トーストに関しては上記手順を踏めば特に苦労すること無く呼び出すことが可能です。
MainWindow.xaml.csusing System; using System.Windows; // toast using Windows.UI.Notifications; using System.IO; using System.Reflection; namespace WinRT_Test { /// <summary> /// Interaction logic for MainWindow.xaml /// </summary> public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); } /// <summary> /// トースト /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void Toast_Click(object sender, RoutedEventArgs e) { var template = ToastTemplateType.ToastImageAndText04; var content = ToastNotificationManager.GetTemplateContent(template); var images = content.GetElementsByTagName("image"); var src = images[0].Attributes.GetNamedItem("src"); // 画像ファイルはexeと同じフォルダにコピー src.InnerText = "file:///" + Path.GetFullPath("sample.jpg"); var texts = content.GetElementsByTagName("text"); texts[0].AppendChild(content.CreateTextNode("Title")); texts[1].AppendChild(content.CreateTextNode("ToastMessage")); // AppIDの代わりにアセンブリ名を突っ込んでおく Assembly assembly = Assembly.GetExecutingAssembly(); AssemblyName asmName = assembly.GetName(); var notifier = ToastNotificationManager.CreateToastNotifier(asmName.Name); notifier.Show(new ToastNotification(content)); } } }MainWindow.xaml<Window x:Class="WinRT_Test.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:WinRT_Test" mc:Ignorable="d" Title="MainWindow" Height="450" Width="800"> <Grid> <Button Content="Toast" HorizontalAlignment="Left" Margin="30,50,0,0" VerticalAlignment="Top" Click="Toast_Click"/> </Grid> </Window>MessageDialogを表示する方法
ウィンドウハンドルを渡す方法が特殊なことと、async/awaitが使われている事により少々苦労しました。
API呼び出し自体は以下で大丈夫なはずですが、
MainWindow.xaml.cs// MessageDialog using System.Threading.Tasks; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using Windows.Foundation; using Windows.UI.Popups; using System.Windows.Interop; using WinRT; ... /// <summary> /// WPFからMessageDialogを呼ぶ場合のおまじない /// https://qiita.com/okazuki/items/227f8d19e38a67099006 /// </summary> [ComImport] [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] [Guid("3E68D4BD-7135-4D10-8018-9FB6D9F33FA1")] public interface IInitializeWithWindow { void Initialize(IntPtr hwnd); } /// <summary> /// メッセージダイアログ /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private async void MessageDialog_Click(object sender, RoutedEventArgs e) { var dlg = new MessageDialog("メッセージ", "タイトル"); // It doesn't work on .NET 5 // ((IInitializeWithWindow)(object)dlg).Initialize(new WindowInteropHelper(this).Handle); var withWindow = dlg.As<IInitializeWithWindow>(); // Windowハンドルを渡して初期化 withWindow.Initialize(new WindowInteropHelper(Application.Current.MainWindow).Handle); await dlg.ShowAsync(); }エラー CS0012 型 'IAsyncAction' は、参照されていないアセンブリに定義されています。アセンブリ 'Windows, Version=255.255.255.255, Culture=neutral, PublicKeyToken=null, ContentType=WindowsRuntime' に参照を追加する必要があります。 WinRT_Test D:\oss\WinRT_Test\MainWindow.xaml.cs 59 該当なし エラー CS0012 型 'IAsyncActionWithProgress<>' は、参照されていないアセンブリに定義されています。アセンブリ 'Windows, Version=255.255.255.255, Culture=neutral, PublicKeyToken=null, ContentType=WindowsRuntime' に参照を追加する必要があります。 WinRT_Test D:\oss\WinRT_Test\MainWindow.xaml.cs 59 該当なし エラー CS0012 型 'IAsyncOperation<>' は、参照されていないアセンブリに定義されています。アセンブリ 'Windows, Version=255.255.255.255, Culture=neutral, PublicKeyToken=null, ContentType=WindowsRuntime' に参照を追加する必要があります。 WinRT_Test D:\oss\WinRT_Test\MainWindow.xaml.cs 59 該当なし エラー CS0012 型 'IAsyncOperationWithProgress<,>' は、参照されていないアセンブリに定義されています。アセンブリ 'Windows, Version=255.255.255.255, Culture=neutral, PublicKeyToken=null, ContentType=WindowsRuntime' に参照を追加する必要があります。 WinRT_Test D:\oss\WinRT_Test\MainWindow.xaml.cs 59 該当なしと何故か怒られるので、以下のクラスを追加します。
※WindowsRuntimeの拡張クラスに含まれてるはずなのですが…/// <summary> /// 'IAsyncAction' は、参照されていないアセンブリに定義されていますと言われるので自前で定義 /// https://www.moonmile.net/blog/archives/8584 /// </summary> public static class TaskEx { public static Task<T> AsTask<T>(this IAsyncOperation<T> operation) { var tcs = new TaskCompletionSource<T>(); operation.Completed = delegate //--- コールバックを設定 { switch (operation.Status) //--- 状態に合わせて完了通知 { case AsyncStatus.Completed: tcs.SetResult(operation.GetResults()); break; case AsyncStatus.Error: tcs.SetException(operation.ErrorCode); break; case AsyncStatus.Canceled: tcs.SetCanceled(); break; } }; return tcs.Task; //--- 完了が通知されるTaskを返す } public static TaskAwaiter<T> GetAwaiter<T>(this IAsyncOperation<T> operation) { return operation.AsTask().GetAwaiter(); } }以上。
参考ページ
.NET 5 から Windows Runtime API を呼ぶのが凄い楽になってる
https://qiita.com/okazuki/items/acf95b3ebb21d4d5083b[C#] デスクトップアプリ (WPF) から手軽にWinRT APIを活用しよう
https://qiita.com/everylittle/items/62ce313fe09883c6da5f.NET 5 で Microsoft Store のアプリ内課金の API の呼び方
https://qiita.com/okazuki/items/227f8d19e38a67099006Microsoft OCR をデスクトップのWFPアプリで動かす方法
https://www.moonmile.net/blog/archives/8584非同期メソッド入門 (10) - WinRTとの相互運用
https://blog.xin9le.net/entry/2012/11/12/123231
- 投稿日:2020-12-04T07:09:49+09:00
.NET 5で追加されたBlazorの新機能の説明(サンプル付き)
概要
.NET 5の正式版がリリースされ、Blazorも新機能が追加されました。
WebAssemblyがGAされてからしばらくBlazorから遠のいていましたが、リハビリを兼ねて新機能に関して簡単なサンプルを見ながら説明していこうと思います。MSの公式ドキュメントに書いてある事をなぞっている点が多いとは思いますが、合わせて読むことで理解の助けになればと思います。
大した内容ではありませんが、下記にデモとソースを公開しています。
今回、紹介する項目は下記になります。
- CSSの分離
- JavaScriptの分離とオブジェクト参照
- InputRadioおよびInputRadioGroupコンポーネント
- InputFileコンポーネント
- コンポーネントの仮想化
- UIフォーカスの設定
- アセンブリの遅延読み込み
※基本的にWebAssemblyの視点での話になります。
各項目説明
1.CSSの分離
VueやReact、AngularといったSPAフレームワークではコンポーネント単位でCSSを適応できる機能が提供されていますが、Blazorでも対応できるようになりました。
コンポーネントの範囲にスタイル適応を限定できるので、下記のようなメリットがあるかと思います。
- スタイル適応の影響範囲の極小化
- 管理しやすい(razorとCSSをペアで管理)
実現方法
razorコンポーネントと同名のcssファイルを作るだけで対応できます。
(Test.razorというコンポーネントであれば、Test.razor.cssというファイルを作成。)Visual Studioだと下記のようにネストして表示してくれるのでわかりやすいですね。
あとは、cssに通常のスタイルシート同様に記載するだけです。
下記ではh1タグのフォント色を赤色にしています。Parent.razor<h1>Parent H1</h1>Parent.razor.cssh1 { color: red; }このコンポーネントをページに配置してみます。
ページ内にはコンポーネントと同じく、h1タグがあります。Page.razor<h1>Page H1</h1> <Parent></Parent>表示させるとコンポーネントのh1のみにフォント色が適応されている事が確認できますね。
子コンポーネントへの適応
コンポーネント内でさらに別コンポーネントを使用している場合に、そのコンポーネントに対しても同一のスタイルを適応させることができます。
下記のようなChildといったコンポーネントと、それを使用するParentというコンポーネントがある場合、通常ではParentのスタイルはChildには適応されません。
Child.razor<h1>Child H1</h1> <h2>Child H2</h2>Parent.razor<!-- ルートにdivタグを定義している理由は後述 --> <div> <h1>Parent H1</h1> <h2>Parent H2</h2> <Child /> </div>この場合、CSSにdeepといった連結子を付与することで子コンポーネントに対しても有効なスタイルとなります。
Parent.razor.cssh1 { color: red; } ::deep h2 { color: blue; }h2タグだけ子コンポーネントにもスタイルが適応されていることがわかります。
注意点としてdeep連結子は、ルート要素に対しては効果が無いのでルート要素にならないようにする必要があります。上記の例ですと、Parent.razorに対してdivタグを定義して、h2タグおよびChildコンポーネントがルートにならないようにしています。
ちなみに、これらのコンポーネントに記載されたCSSはビルド時に1つのCSSファイルにまとめられて、index.htmlに記載された(Project名).style.cssといった形で読み込まれていますのでこの参照を外さないように注意が必要です。
index.html<head> ... <link href="<ProjectName>.styles.css" rel="stylesheet" /> </head>2.JavaScriptの分離とオブジェクト参照
これまでBlazorからJavascirptの処理を呼び出しを行うにはindex.htmlに使用するjsファイルの参照の追加やグローバルなwindowオブジェクトへの関数等の参照登録が必要でした。
ですが、動的にjsファイルのロード機能を使用することでグローバル名前空間の汚染やjsファイルのインポートが不要になります。実現方法
まずは、使用したい関数をexportしているjsファイルを作成します。
下記はalert関数で引数に渡されたメッセージを表示する処理となっています。displayAlert.jsexport function displayAlert(message) { alert(message); }jsファイルはwwwroot以下の任意の場所に配置します。
今回は wwwroot/js/displayAlert.js に配置するとします。あとはC#コード側で下記を実行します。
- IJSRuntime.InvokeAsync("import", "JSのパス");でモジュールを呼び出し
- モジュールのInvvokeAsync(InvokeVoidAsync)でメソッド名と引数を渡す
@inject IJSRuntime js; @code { string message = "hello"; async void ShowAlertAsync() { var module = await js.InvokeAsync<IJSObjectReference>("import", "./js/displayAlert.js"); await module.InvokeVoidAsync("displayAlert", message); } }JSのパスはBlazorアプリのPJの場合は上記の通り、wwwrootからのパスとなりますがライブラリの場合には、
_content/{ライブラリ名}/{wwwrootからのパス}
となるようなので注意が必要です。下図のようにバインドした値を渡して呼び出すことができました。
JSがファイルの配置だけで呼び出せるようになったのはかなり嬉しいですね!
3.InputRadioおよびInputRadioGroupコンポーネント
RadioButton及びRadioButtonグループ用のコンポーネントが追加されました。
Enumのデータをバインドして双方向バインディングがお手軽に実装できます。まず、下記のようなデータクラスを定義します。
public enum Country { Japan, China, America, Brazil } public enum FoodType { Rice, Bread, Meat, Vegetable } public enum DrinkType { Water, Tea, Coffee } public class InputData { [Required, EnumDataType(typeof(Country))] public Country? SelectedCountry { get; set; } = null; [Required, EnumDataType(typeof(DrinkType))] public DrinkType? SelectedDrinkType { get; set; } = null; [Required, EnumDataType(typeof(FoodType))] public FoodType? SelectedFoodType { get; set; } = null; }InputRadioGroup内にInputRadioを記載するだけで使用可能です。
RadioButtons.razor<EditForm Model="@InputData"> <DataAnnotationsValidator /> <ValidationSummary /> <p> <InputRadioGroup @bind-Value="InputData.SelectedCountry"> Country: <br> @foreach (var country in Enum.GetValues(typeof(Country))) { <InputRadio Value="country" /> @country } </InputRadioGroup> </p> @if (InputData.SelectedCountry != null) { <p>@InputData.SelectedCountry.ToString() is selected</p> } else { <p>Not Selected Country</p> } <button type="submit">Submit</button> </EditForm> @code { public InputData InputData = new InputData(); }InputRadioGroupのNameとInputRadioのNameで一致させることでネストしたグループ内でグルーピングすることができます。
RadioButtons.razor<EditForm Model="@InputData"> <DataAnnotationsValidator /> <ValidationSummary /> <p> <InputRadioGroup @bind-Value="InputData.SelectedFoodType" Name="food"> <InputRadioGroup @bind-Value="InputData.SelectedDrinkType" Name="drink"> <InputRadio Value="FoodType.Rice" Name="food" /> Rice <InputRadio Value="FoodType.Bread" Name="food" /> Bread <InputRadio Value="FoodType.Meat" Name="food" /> Mead <InputRadio Value="FoodType.Vegetable" Name="food" /> Vegetable <InputRadio Value="DrinkType.Tea" Name="drink" /> Tea <InputRadio Value="DrinkType.Coffee" Name="drink" /> Coffee <InputRadio Value="DrinkType.Water" Name="drink" /> Water </InputRadioGroup> </InputRadioGroup> </p> <button type="submit">Submit</button> </EditForm> @code { public InputData InputData = new InputData(); }4.InputFileコンポーネント
inputタグによるファイルのアップロード処理がJavascriptのFile APIを直接使用しなくても実現可能なInputFileコンポーネントが追加されました。
ファイルのアップロード
下記のようなファイルを選択すると画面にファイル情報を表示するような機能を作成してみます。
uploadFile.razor<InputFile OnChange="LoadFiles" multiple /> <br /> <span>@errorMessage</span> @if (isLoading) { <p>Loading...</p> <br /> } @foreach (var file in loadedFiles) { <p id="file-@(file.FileName)"> <strong>Name:</strong> <span id="file-name">@(file.FileName)</span><br /> <strong>Last modified:</strong> <span id="file-last-modified">@(file.LastModified.ToString())</span><br /> <strong>Size (bytes):</strong> <span id="file-size">@(file.Size)</span><br /> <strong>Content type:</strong> <span id="file-content-type">@(file.ContentType)</span><br /> </p> } @code { List<UploadFile> loadedFiles = new List<UploadFile>(); bool isLoading; string errorMessage; async Task LoadFiles(InputFileChangeEventArgs e) { isLoading = true; loadedFiles.Clear(); errorMessage = string.Empty; try { foreach (var file in e.GetMultipleFiles(3)) { StateHasChanged(); var buffers = new byte[file.Size]; await file.OpenReadStream().ReadAsync(buffers); var uploadFile = new UploadFile() { FileName = file.Name, ContentType = file.ContentType, Size = file.Size, LastModified = file.LastModified, Content = buffers }; loadedFiles.Add(uploadFile); } } catch (Exception ex) { errorMessage = ex.Message; } finally { isLoading = false; } } public class UploadFile { public string FileName { get; set; } public byte[] Content { get; set; } public DateTimeOffset LastModified { get; set; } public string ContentType { get; set; } public long Size { get; set; } } }まず、InputFileタグにはmultipleという属性を付与することで複数のファイルを選択可能としています。
(通常のinputタグにmultipleファイルを付与するのと同じ。)次に、OnChangeにInputFileChangeEventArgsを引数とするメソッドをバインドします。
InputFileChangeEventArgsには、GetMultipleFilesといったメソッドがあり、選択したファイルから何個まで情報を取得するかを指定して、予想外の大量のファイルの選択された場合に不用意に処理を実行しない対応が可能です。
GetMultipleFiles取り出した情報はIBrowserFileという型となっていて、ファイル名やファイルサイズなどの情報を持っています。サーバー側にファイルをアップロードしたい際には下記のような感じでJson化して送れば良いかと思います。
uploadFile.razorasync Task SubmitAsync() { var data = new UploadData() { UploadFiles = loadedFiles }; await Http.PostAsJsonAsync<UploadData>("サーバの宛先URL", data); } public class UploadData { public List<UploadFile> UploadFiles { get; set; } }こんな感じのJsonがPOSTされるので後はサーバ側で処理するだけですね。
画像のプレビュー表示
次に下図のようなアップロードした画像をサムネイルとしてプレビューできる例を見ていきます。
やっていることは先ほどのファイルアップロードとほぼ同じで、変更点としては下記になります。
1.RequestImageFileAsyncメソッドで画像をリサイズ
2.DataUrlに変換してブラウザ上に表示ImageUpload.razor<div class="card" style="width:30rem;"> <div class="card-body"> @foreach (var imageDataUrl in imageDataUrls) { <img class="rounded m-1" src="@imageDataUrl" /> } </div> </div> @code { private List<string> imageDataUrls = new List<string>(); private async Task OnInputFileChange(InputFileChangeEventArgs e) { var maxAllowedFiles = 3; var format = "image/png"; foreach (var imageFile in e.GetMultipleFiles(maxAllowedFiles)) { // 100*100の画像にリサイズ var resizedImageFile = await imageFile.RequestImageFileAsync(format, 100, 100); var buffer = new byte[resizedImageFile.Size]; await resizedImageFile.OpenReadStream().ReadAsync(buffer); // ブラウザ上に表示するためにDataUrlに変換 var imageDataUrl = $"data:{format};base64,{Convert.ToBase64String(buffer)}"; imageDataUrls.Add(imageDataUrl); } } }これまでJavascritp側でゴリゴリ実装が必要だった部分がC#で完結しているのと、画像のリサイズ機能まで提供されているのはうれしいですね。
5.コンポーネントの仮想化
現在表示されている部分のみを描画する機能が提供されました。
コレクションなどで大量の要素を描画する際に表示部分だけを描画するようになるのでUIのパフォーマンスの向上が期待できます。使い方は簡単で、foreachで列挙する代わりに、Virtualizeタグにコレクションをバインディングするだけです。
Contextにコレクションの要素のクラス名、Itemsにコレクションを指定します。<!-- 仮想化あり --> <p>Virtualization</p> <Virtualize Context="Person" Items="@People"> <p> name: @Person.Name , age: @Person.Age </p> </Virtualize> <!-- 仮想化なし --> <p>Non Virtualization</p> @foreach (var person in People) { <p> name: @person.Name , age: @person.Age </p> } @code { public List<Person> People = Enumerable.Range(1, 30000).Select(x => new Person(x.ToString(), x % 60)).ToList(); public class Person { public Person(string name, int age) { Name = name; Age = age; } public string Name { get; } public int Age { get; } } }下図の通り、仮想化している場合には大量のコレクションを表示させてもすぐ表示されますが、非仮想化の場合には表示・非表示の切り替えを行う際に画面が一定時間フリーズします。
項目プロバイダーによる非同期読み込み
一般的なユースケースであれば大量のコレクションを読み込む必要がある時には、REST APIなど外部のIFから適宜必要な分だけを部分読み込みをすると思います。
Virtualizeタグではこのような機能を実現するために項目プロバイダといった機能が提供されています。まず下記のような、ItemsProviderRequestを引数として、ValueTaskを戻り値とするメソッドを定義します。
// REST APIなどからデータを取得するクラス private DummyPersonService _service = new DummyPersonService(); // 取得するデータの総数 private int? totalCount; private async ValueTask<ItemsProviderResult<Person>> LoadPeople(ItemsProviderRequest request) { // ロードするデータの総数を取得 if (totalCount == null) { totalCount = await _service.GetTotalCountAsync(); } // 開始位置と(StartIndex)データ数(Count)を指定して部分的にデータを取得 var people = await _service.GetPeopleAsync(request.StartIndex, request.Count); // 取得したデータ数とデータ総数を渡す return new ItemsProviderResult<Person>(people, totalCount ??= 0); }引数のItemsProviderRequestにはStartIndexとCountというプロパティが定義されていて、Virtualizationタグが必要としているデータの開始位置とデータ数が渡されてきます。
開始位置とデータ数を外部のAPIに渡すことでデータを取得して、その戻り値を返します。
よって呼び出し側のAPIにはインデックスによるページネーション機能が無いと使用が厳しいかもしれません。
合わせて、データの総数を取得しておく必要もあり、戻り値としてItemsProviderRequestに部分取得したデータと総データ数を渡します。あとはVirtualizeタグのItemsProviderに上記のメソッドをバインドします。
ItemContent内に表示する内容、Placeholderにデータ読み込み中に表示する内容を記載します。
ItemSizeが一度のロードで読み込むデータ数、OverscanCountが前後の非表示の領域を事前読み込みする数として調整ができます。
指定しない場合にはデフォルト値(25と3)で実行されます。<Virtualize Context="Person" ItemsProvider="@LoadPeople" ItemSize="25" OverscanCount="4"> <ItemContent> <p> name: @Person.Name , age: @Person.Age </p> </ItemContent> <Placeholder> <p> Loading... </p> </Placeholder> </Virtualize>こうすると下図のようにスクロール位置に合わせていい感じにデータを読み込んでくれます。
初回の読み込みにはPlaceHolderが適応されないので自前でロード表示する実装と、戻った場合にも再読み込みされるのでAPI呼び出し前の層にキャッシュする層を入れるなどの工夫が必要かもしれませんね。6.UIフォーカスの設定
C#コードだけでフォーカスの設定が可能になりました。
あまり意識することが無かったのです、今まではフォーカスを特定の要素に移動させたい場合はJavascriptを呼び出すしかなかったようです。
使い方は簡単でrefで要素の参照を定義して、FocustAsyncメソッドを呼び出すだけです。<input @ref="textInput" /> <button @onclick="SetFocusToTextInput">Set focus</button> @code { private ElementReference textInput; private async Task SetFocusToTextInput() { await textInput.FocusAsync(); } }7.アセンブリの遅延読み込み
Blazorアプリ起動時に全てのアセンブリを読み込むのではなく必要時に後から読み込む、遅延読み込みが可能になりました。
遅延読み込みを行うことで必要最低限のアセンブリのみを読み込んで、アプリの起動の高速化が期待できます。
ログイン後の画面など、特定のユーザしか使用しない機能などで使うと良いかもしれませんね。まず、下記のようにBlazorアプリのcsprojに遅延読み込みしたいDLLをBlazorWebAssemblyLazyLoadという項目名で指定します。
Project参照や既存のDLL参照はそのまま残した上で、BlazorWebAssemblyLazyLoadを追加します。
(PackageReferenceやProjectReferenceと合わせて2個記載する。)csproj<!-- 自分のプロジェクト参照 --> <ItemGroup> <ProjectReference Include="..\LazyLoadModule\LazyLoadModule.csproj" /> </ItemGroup> <!-- 自分のプロジェクトのDLL --> <ItemGroup> <BlazorWebAssemblyLazyLoad Include="LazyLoadModule.dll" /> </ItemGroup>App.razorに記載されたRouterコンポーネントを使用します。
AdditionalAssembliesという属性をバインドしてこの値に遅延読み込みしたアセンブリを追加します。
後は、下記の流れで処理を行います。1.OnNavigateAsyncでページ遷移時にURLをチェックして動的にアセンブリを読み込むかどうか判断
2.LazyAssemblyLoader.LoadAssembliesAsyncでアセンブリを指定して読み込む
3.読み込んだアセンブリをAdditionalAssembliesに追加するなお、Navigatingタグを使用することで、アセンブリ読み込み中の待ち時間に表示する要素を定義できます。
App.razor@using System.Reflection @using Microsoft.AspNetCore.Components.Routing @using Microsoft.AspNetCore.Components.WebAssembly.Services @inject LazyAssemblyLoader LazyAssemblyLoader <Router AppAssembly="@typeof(Program).Assembly" AdditionalAssemblies="@lazyLoadedAssemblies" OnNavigateAsync="@OnNavigateAsync"> <Navigating> <div> <p>Loading modules...</p> </div> </Navigating> <Found Context="routeData"> <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" /> </Found> <NotFound> <LayoutView Layout="@typeof(MainLayout)"> <p>Sorry, there's nothing at this address.</p> </LayoutView> </NotFound> </Router> @code { const string ModuleName = "LazyLoadModule.dll"; List<Assembly> lazyLoadedAssemblies = new List<Assembly>(); async Task OnNavigateAsync(NavigationContext args) { if (args.Path == "lazy" && !lazyLoadedAssemblies.Any(x => x.GetName().Name + ".dll" == ModuleName)) { var assemblies = await LazyAssemblyLoader.LoadAssembliesAsync(new string[] { ModuleName }); lazyLoadedAssemblies.AddRange(assemblies); } } }サンプルだと毎回モジュールを読み込んでいたのですが、lazyLoadedAssembliesの中身をチェックして一度読み込んだモジュールは再度読み込まないといった事もできましたが、効果のほどは不明です。
その他の機能
これまで紹介した機能以外にも追加された機能が多々あります。
BlazorServer系の機能を中心に有用な機能がありますので下記を参照してみてください。https://docs.microsoft.com/ja-jp/aspnet/core/release-notes/aspnetcore-5.0?view=aspnetcore-5.0#blazor
まとめ
.NET 5で追加されたBlazorの新機能の一部をコード例と合わせて簡単に説明してみました。
説明だけではなく自分でコードを書くことでわかる事もあり、大変でしたがやってみて良かったです。新機能でJavascriptからの依存が低減し、C#コードだけで完結できるスコープが広がった点や、SPAフレームワークとして欲しい機能が拡張されるなど着実に進化しています。
今後の機能追加に関しても期待したいですね!
- 投稿日:2020-12-04T01:31:46+09:00
Unityで「シリアライズするフィールド」と「プロパティー」を簡潔に書きたくて、SourceGeneratorを作ってみた
※ この投稿で紹介するSourceGeneratorは、まだUnityじゃ使えません。将来的にこうできればいいなーって話
さてまずは、C#のプロパティーの話をします。
C#では、フィールドをpublicにするのは良くなくて、プロパティーを使ってフィールドにアクセスするのが良いとされています。↓のコードは良くなくて
public class Launcher { // publicなフィールドだからこれは良くない public float speed; }↓な感じでプロパティーにするのがGoodです。
public class Launcher { float speed; // ちょっと古い書き方のプロパティー public float Speed { get { return speed; } set { speed = value; } } }さて、↑みたいなコードはわざわざフィールドを作らなくても、↓のような自動実装プロパティーで実現ができます。
public class Launcher { public float Speed { get; set; } }自動実装プロパティーを使うことで、C#のコードからは見えない次のようなフィールドとアクセッサが、"自動"で生成されます。
<Speed>k__BackingField
というフィールドget_Speed
というメソッドset_Speed
というメソッド自動実装プロパティー、便利ですね!
さて次はUnityのシリアライズとフィールドとプロパティーの話。
UnityにおいてMonoBehaviourのサブクラスやScriptableObjectのサブクラスなどで、次のようなpublicなフィールドに設定した値や参照は、シーンやプレファブにシリアライズされることがあります。
using UnityEngine; public class Launcher : MonoBehaviour { // publicなフィールドだからこれは良くない public float speed; }ですが先にも説明したとおり、C#として↑のようなpublicなフィールドは良くなかったですね。
こういう場合は、SerializeField属性の出番です。SerializeField属性をつけたフィールドはpublicでなくても、シリアライズされます。using UnityEngine; public class Launcher : MonoBehaviour { [SerializeField] float speed; }さて、このフィールドに外部からアクセスするために、プロパティーを付けてあげましょう。(とりあえずゲッタープロパティーだけ)
using UnityEngine; public class Launcher : MonoBehaviour { [SerializeField] float speed; public float Speed => speed; }うん、よくあるパターンです。 このUnityで「シリアライズするフィールド」と「プロパティー」というよくあるパターンをもっと簡潔にしたいな!というのがこの投稿の趣旨です。
先ほど説明した自動実装プロパティ、あれは使えないのでしょうか?
自動で実装される内部的なフィールド(バッキングフィールド)に、SerializeField属性をつけることができたら、実現できそうです。なんと、これ実現できます。
C# 7.3から自動実装プロパティの自動で実装される内部的なフィールド(バッキングフィールド)に属性を付与できるようになりました。
using UnityEngine; public class Launcher : MonoBehaviour { [field:SerializeField] public float Speed => speed; }うん、簡潔!めでたしめでたし!
とは、ならないいんです・・・自動実装プロパティのフィールド名は、「
<Speed>k__BackingField
」という名前です。困ったことに、この名前でシリアライズされてしまいます。次の画像みたいな感じで。うーん、困った。
さて、ここでC# 9.0から加わったSourceGeneratorの出番です。(まだUnityじゃ使えません。将来的にこうできればいいなーって話)
C# Source Generatorは、ビルド時にC#のソースコードを生成する仕組みです。
- メインのプロジェクトがビルドされる前にコード生成
- コード生成するために必要な入力値はコンパイル時に必要
- 出力結果は、プロジェクトの一部となる
- IDEにおいて、生成したコードの宣言にジャンプもできる
- ILではなくC#を生成するので、デバックがすごい楽
- 既存のソースコードを上書きしたりけしたりすることはできない
このC# Source Generatorを使うことで、プログラマティカルにコード生成をすることができます。C# Source Generatorを使うことで、ボイラープレートのコードは非常に簡単になります。
さて、自分はC# Source Generatorをつかって、「フィールドに付与すると、そのフィールドのゲッタープロパティーを生成してくれるSource Generator」を作ってみました!
ソースコードはこちら! RyotaMurohoshi/PropertyGenerator
using System; using PropertyGenerator; public partial class Product { [GetterProperty(PropertyName = "Identifier")] private readonly int id; [GetterProperty] private readonly string name; public Product(string name, int id) { this.name = name; this.id = id; } }↑みたいな感じで、idやnameというフィールドにGetterPropertyという属性をつけると、↓のようなプロパティが生成されます。
public partial class Product { public int Identifier => this.id; public string Name => this.name; }このGetterPropertyを使って、次のようなボイラープレートなコードもC# Source Generator使ってすっきりさせてみましょう。
using UnityEngine; public class Launcher : MonoBehaviour { [SerializeField] float speed; public float Speed => speed; }↑が、↓こうなります!プロパティーがいらなくなりました!すっきり!
using UnityEngine; public class Launcher : MonoBehaviour { [SerializeField, GetterProperty] float speed; }あんまり嬉しくない?確かに1行だとそうですね。
これが↓みたいにたくさんあったらどうでしょう?
using UnityEngine; public class Monster : ScriptableObject { [SerializeField] int maxHp; public int MaxHp => maxHp; [SerializeField] int maxMp; public int MaxMp => maxHp; [SerializeField] int attack; public int Attack => attack; [SerializeField] int defense public int Defense => defense; [SerializeField] int speed public int Speed => speed; }これが↑、↓こうなる!すっきりしましたね!
ちなみに↑は、一部実装が間違っている箇所にきがつきましたか!↑はうっかりミスをしてます。↓ならこういううっかりミスも防げますね!using UnityEngine; public class Monster : ScriptableObject { [SerializeField, GetterProperty] int maxHp; [SerializeField, GetterProperty] int maxMp; [SerializeField, GetterProperty] int attack; [SerializeField, GetterProperty] int defense [SerializeField, GetterProperty] int speed }
Unityで「シリアライズするフィールド」と「プロパティー」ですが、残念ながら自動実装プロパティはつかえません。そこで、Unityで「シリアライズするフィールド」と「プロパティー」を簡潔に書きたくて、SourceGeneratorを作ってみました。フィールドにつけるとゲッタープロパティーを生成するという非常に簡潔なものです!
よかったらコードを見てみてください! : RyotaMurohoshi/PropertyGenerator
これがUnityで使えるようになるのが楽しみです!