- 投稿日:2019-12-23T23:25:43+09:00
UniRx & UniTask とは何なのか
はじめに
何番煎じなのかわかりませんが、過去にいろいろ解説した記事へのリンクをまとめる意味も込めて解説します。
UniRxとは
おすすめ資料
- UniRx入門シリーズ 目次
- UniRx オペレータ一覧
- UniRx オペレータ逆引き
- ReactiveCommand/AsyncReactiveCommandについて
- ObserverパターンからはじめるUniRx
- Observable の非同期処理への活用
概要
UniRxとは、
Reactive Extensions
をUnity向けの実装したC#ライブラリです。
かなり昔のバージョンのUnityでも扱うことができます。
導入することで、Unityにおいて次のような処理の実装が簡単になります。
- 非同期処理
- 何らかの処理の完了を待ち受けて次の処理を行うような処理の管理
- エラーハンドリングやリトライ処理の簡略化
- 実行結果のハンドリングやキャッシュ
- 実行スレッドの柔軟な切り替え
// data.txtをスレッドプール上で読み込み、メインスレッド上で表示する Observable .Start(() => File.ReadAllText("data.txt")) .ObserveOnMainThread() .Subscribe(x => Debug.Log(x));
- イベント処理
- 処理のトリガーと実際にハンドリングする場所を分離して記述できる
- イベントメッセージの加工や合成
- 実行スレッドの柔軟な切り替え
void Start() { // Jumpボタンが押されたらログに出す // 1度発動したら1秒間は何もしない this.UpdateAsObservable() .Where(_ => Input.GetButtonDown("Jump")) .ThrottleFirst(TimeSpan.FromSeconds(1000)) .Subscribe(_ => Debug.Log("Jump!!")); }UniRxは「非同期処理」と「イベント処理」の2つを扱うことができるライブラリです。
特に「時間」の扱いに長けており、Unityにおける「フレームをまたいだ処理」などの実装が簡単になります。なお、
UniRx
が提供するこれら便利な機能は正式には「Observable
」と呼ばれています。Observable
Observable
とはUniRx
が提供する、「メッセージを通知のための機構およびそのオブジェクト」の指します。
型としてはIObservable<T>
で表現されます。// 実行結果は IObservable<T> でハンドリングする private IObservable<string> ReadFileObservable(string path) { return Observable .Start(() => File.ReadAllText(path)) .ObserveOnMainThread(); }
Observable
の仕組み、およびScheduler
やOperator
の概念について詳しく知りたい方は次の資料を御覧ください。Operator一覧
UniRx
でもっとも便利な機能がこのOperator
です。
Operator
はObservable
に対するさまざまな処理を提供してくれます。
そのためこのOperator
を組み合わせるだけで、だいたいの処理の実装が完了してしまいます。UniTaskとは
おすすめ資料
- Deep Dive async/await in Unity with UniTask(UniRx.Async)
- さては非同期だなオメー!async/await完全に理解しよう
- async/await のしくみ
- 実践! ライブコーディングで覚えるasync/await
- UniTask入門
- 【Unity開発者向け】「SynchronizationContext」と「Taskのawait」
- UniTask機能紹介
- UniRxとUniTask 相互変換の変わったパターン紹介
概要
UniTaskとは、C#の標準TaskおよびTaskSchedulerをUnity向けに最適化して実装したC#ライブラリです。
UniTaskは標準Taskと比べてパフォーマンスが出るため、非同期処理を扱う場合は是非導入してほしいライブラリです。ただし導入時はUnity 2018.3以降を推奨。UniTaskを導入することで、次のようなメリットがあります。
- Unity向けに最適化されたTaskおよびTaskSchedulerが利用できる
ValueTask
のUnity向け実装- UnityのPlayerLoopを用いたSchedule管理
- SynchronizationContextに依存しない
async void Start() { var path = "data.txt"; // UniTaskのawaitは基本的にメインスレッドに戻る var result = await LoadFileAsync(path); Debug.Log(result); } private UniTask<string> LoadFileAsync(string path) { return UniTask.Run(() => File.ReadAllText(path)); }
- さまざまなオブジェクトへの
Awaiter
実装の提供
- コルーチンを
async/await
に置き換える可能にyield return
の代わりにawait
で非同期処理の待機ができるようになるprivate void Start() { JumpAsync(this.GetCancellationTokenOnDestroy()).Forget(); } /// <summary> /// ボタンが押されたらジャンプする /// </summary> private async UniTaskVoid JumpAsync(CancellationToken token) { var rigidBody = GetComponent<Rigidbody>(); while (!token.IsCancellationRequested) { // ボタンが押されるのをUpdateタイミングで待つ await UniTask.WaitUntil(() => Input.GetButtonDown("Jump"), PlayerLoopTiming.Update, cancellationToken: token); // 押されたらFixedUpdateのタイミングでジャンプする await UniTask.Yield(PlayerLoopTiming.FixedUpdate); rigidBody.AddForce(Vector3.up, ForceMode.Impulse); // 1秒待って繰り返す await UniTask.Delay(1000, cancellationToken: token); } }UniTaskは「非同期処理」に特化したライブラリです。
導入することでほぼすべてのコルーチンをasync/await
に置換することができます。標準Taskとの比較
UniTask
はC# 7.0
で追加された、ValueTask
ライクに作られています。
そのためasync/await
時、処理が同期で完了する場合においてはヒープアロケーションが発生しないという特徴があります。また、UniTask
は実行コンテキストの管理にSynchronizationContext
ではなくUnity Player Loop
を用いるように作られています。そのため、標準の
Task
/ValueTask
と比較してUniTaskの方がよりUnityではパフォーマンスを出すことができるようになっています。
Task UniTask 機能 Unityでは不要な機能が多い Unityで活用できる機能のみ実装 オブジェクトサイズ 大きい 小さい 実行コンテキストの管理 TaskScheduler & SynchronizationContext UnityのPlayerLoop 必要なC# version C# 5.0以上 C# 7.0以上 TaskTracker 無 UnityEditor上で確認可能 メモリアロケーション 常にヒープを確保する 同期処理で完了する場合はヒープを確保しない UniRxとUniTask、それぞれの使い分け
「
UniTask
とUniRx
の非同期処理の使い分けの基準は何か」という疑問を持つ人もいるでしょう。
それぞれの使い分けについて説明します。UniTaskを使うべき場合
非同期処理の結果通知が「1回」で済む場合
結果通知が1回である、つまりは単発で完了する普通の非同期処理の場合です。
こちらはUniTask
(とasync/await
)を使うべきです。理由として次が挙げられます。
- 同期処理と見た目がほぼ変わらないコードを書ける
- 単発で済む非同期処理に対して、
Observable
自体がオーバースペック気味
async/await
を使うと非同期処理をほぼ同期処理と変わらない見た目で記述することができます。
そのため同期処理で書いていた部分をあとから非同期化する、またはその逆といった対応も比較的楽にすみます。
Observable
の場合、処理が1回で済む非同期処理に対してはかなりオーバースペックになってしまいます。まず「メッセージの発行元は何か」「メッセージは何回発行されるのか」を常に意識しなくてはいけません。
さらにはHotやColdといった性質も考えなくてはならず、UniTask
ほど気軽に扱えません。また、
Observable
を用いた非同期処理は同期処理とまったく異なる記述法になってしまいます。
そのため、あとから処理内容を同期処理へ直そうと思ったときに、Observable
を使っている場所ほぼすべてを新しく書き直す必要が出てきてしまいます。処理を手続き的に記述したい場合
Observable
では非同期処理をOperator
を用いて処理を宣言的に記述することができました。
これは処理の内容がOperator
の表現範囲で済む限りにおいては便利ではあります。
ですが実際の開発においてはそう簡単に行きません。Operator
ではどうしても表現しきれない処理が出てきた場合、これを手続き的に書き下す必要があります。また、
Observable
は「処理の条件分岐ができない」という欠点があります。
メッセージ内容に応じてその場で実行する処理をごっそり切り替えるといったことができません。
(処理内容ごとに新しいObservable
を定義しなくてはいけないため無駄が多い)
UniTask
(とasync/await
)であれば、処理をほぼ同期処理と変わらずに手続き的に記述することができます。
そのため、要求どおりの仕様を満たした処理をObservable
とOperator
で記述するよりも、よりわかりやすい簡単に実装することができます。UniRx(Observable)を使うべき場合
非同期処理の結果が複数個になる場合
ここでいう複数個の結果とは、
UniTask<IEnumerable<T>>
で済むような場合の話ではありません。非同期処理そのものが継続的に実行され、結果が断続的に発行されるようなシチュエーションを想定しています。
(たとえばディレクトリの中身をまとめて読み込んで、読み込みが先に終わったファイルから処理を次々に行うなど)このような非同期処理は
UniTask
では表現が不可能なため、IObservable<T>
を使うことになります。補足
IAsyncEnumerable<T>
とIObservable<T>
の違い
C# 8.0
でIAsyncEnumerable<T>
というものが追加されました。名前のとおり「非同期ストリーム」です。IObservable<T>
と役割が被ってそうですが、そこは明確に異なります。
IObservable<T>
: 非同期処理が常に動いておりその結果がPUSHで通知されるIAsyncEnumerable<T>
: 結果をPULLしたときに1つ非同期処理が走る非同期処理を裏で動かしてその結果がPUSH通知されるのを待つなら
IObservable<T>
を使う。
await foreach
と組み合わせて、逐次処理をしていくならIAsyncEnumerable<T>
を使う。といった使い分けが今後は必要になってくるでしょう。
(まだUnityはC# 8.0
に対応していないので、もう少しあとの話ですが)イベント処理を行う場合
C#
のevent
構文や、UnityEvent
の代替としてUniRx
は利用することができます。
またUpdate()
やFixedUpdate()
をObservable
に変換したり、uGUI
と組み合わせて使うと表現力が広がりかなり便利に使うことができます。(そもそも「イベント処理」=「値が複数回発行される非同期処理」とほぼ同義なので、同じことを2回説明しているだけですが)
Unityで動くC#バージョンが低い場合
古のUnityでは対応しているC#バージョンが低く、
async/await
すら使えない場合があります。
特に長期運用しているプロジェクトですと未だにUnity 5系を使っているなんてこともあるでしょう。この場合は
UniTask
を用いることはできないので、UniRxが非同期処理における唯一の選択肢となります。
コルーチンとObservable
を併用するなどするとよいでしょう。相互変換
なお、
Observable
とUniTask
はそれぞれ相互変換が可能です。
そのためどちらか片方にこだわる必要はなく、場面に応じて変換して使い分けるとよいでしょう。UniTask -> UniRx
キャンセルを考えない場合
UniTask.ToObservable()
で変換できます。
この場合、Scheduler
は自動的にMainThreadScheduler
指定となります。using UniRx; using UniRx.Async; using UnityEngine; using UnityEngine.Networking; public class UniTaskToUniRx : MonoBehaviour { void Start() { // UniTask<string> var uniTask = LoadTextAsync("https://github.com/"); // UniTask<string> -> IObservable<string> // UniTaskのキャンセルを考えないならこれだけでOK uniTask.ToObservable().Subscribe(Debug.Log); } /// <summary> /// Textを指定のパスから読み込む /// </summary> private async UniTask<string> LoadTextAsync(string path) { var uwr = UnityWebRequest.Get(path); await uwr.SendWebRequest(); return uwr.downloadHandler.text; } }キャンセルを考える場合
一発で変換するメソッドはありません。
そのため次のような方法でがんばるしかないです。UniRx -> UniTask
IObservable -> UniTask
ToUniTask()
を使うことでUniTask
に変換できます。
ただしデフォルトではOnCompleted
メッセージが発行されるまで待ち受けてしまいます。
次に発行される1メッセージだけを待ち受けたい場合は、useFirstValue
オプションをつけましょう。private async UniTaskVoid CheckEnemyAsync(CancellationToken cancellationToken) { // 新たに生成されたEnemyが通知されるObservableがあったとして IObservable<Enemy> enemyObservable = _enemySpawner.OnEnemySpawned; while (!cancellationToken.IsCancellationRequested) { // ToUniTask(useFirstValue: true) でUniTask化してawaitできる var enemy = await enemyObservable .ToUniTask(cancellationToken, useFirstValue: true); Debug.Log(enemy.Name); } }IReadonlyReactivePropertyのawait
正確にいえば
UniTask
変換ではないですが、どうせasync/await
とセットで使うので解説します。
IReadonlyReactiveProperty<T>
はawait
することで、次のメッセージ発行を待ち受けることができます。using System.Threading; using UniRx; using UniRx.Async; using UniRx.Async.Triggers; using UnityEngine; public class ReactivePropertyAwait : MonoBehaviour { // なにかのステート private enum GameState { Ready, Battle, Result } // ステート管理するReactiveProperty private ReactiveProperty<GameState> _currentGameState = new ReactiveProperty<GameState>(); private void Start() { StateChangedAsync(this.GetCancellationTokenOnDestroy()).Forget(); } /// <summary> /// ステート遷移するたびに処理を走らせる /// </summary> private async UniTaskVoid StateChangedAsync(CancellationToken cancellationToken) { while (!cancellationToken.IsCancellationRequested) { // ステート遷移を待つ var next = await _currentGameState; // 遷移先に合わせて処理をする switch (next) { case GameState.Ready: // Do something... break; case GameState.Battle: // Do something... break; case GameState.Result: // Do something... break; } } } }その他、変わった変換の例
その他の変わった変換パターンについては別記事にまとめました。
まとめ
UniRx
とUniTask
はUnityでC#
を触るならぜひとも導入してほしいライブラリです。
入れて絶対に損はしない、むしろ無いと困るくらいには便利なものなので使い方を覚えて活用できるようになるとよいでしょう。また、現在UniRxとUniTaskの書籍の執筆を行っています。全体の執筆が概ね終わり、現在はレビューフェーズに入っています。発売されたらぜひよろしくおねがいします。
- 投稿日:2019-12-23T23:13:49+09:00
【Unity】LateUpdate()やOnGUI()よりもタイミングが遅い、OnEndOfFrame()を作ってみた
問題
フレーム内のなるべく遅いタイミングで、何か処理をしたい。普通なら、
LateUpdate()
で十分だが、諸々の事情によりOnGUI()
よりも遅いタイミングが必要になった。そこで実行順に関する公式ドキュメント(Order of Execution for Event Functions)を確認したところ良さげなタイミングを見つけたので、
WaitForEndOfFrame()
を利用して強引に作ってみた。endofframetest.cspublic class endofframetest : MonoBehaviour { void Start() { StartCoroutine(StartEndOfFrame()); Debug.Log($"Start():{Time.frameCount}"); } IEnumerator StartEndOfFrame() { while (true) { // Wait until all rendering + UI is done. yield return new WaitForEndOfFrame(); OnEndOfFrame(); } } private void FixedUpdate() { Debug.Log($"FixedUpdate():{Time.frameCount}"); } void Update() { Debug.Log($"Update():{Time.frameCount}"); } private void LateUpdate() { Debug.Log($"LateUpdate():{Time.frameCount}"); } private void OnGUI() { Debug.Log($"OnGUI():{Time.frameCount}"); } private void OnEndOfFrame() { Debug.Log($"OnEndOfFrame():{Time.frameCount}"); } }Console.Start():1 FixedUpdate():1 Update():1 LateUpdate():1 FixedUpdate():2 Update():2 LateUpdate():2 OnGUI():2 OnGUI():2 OnEndOfFrame():2結果
Start()のタイミングを1フレーム目としたとして2フレーム目からしか呼ばれないので、その点は注意する必要があるが概ね問題なく利用できた。
ちなみに、、、OnGUI()も2フレーム目からしか呼ばれないようだ(確認したのはUnity2020.1α)。
- 投稿日:2019-12-23T22:43:02+09:00
2.5Dキャラクターアニメーション - Mirror Animation Playable
これはUnity Advent Calender 2019の24日目の記事です。
3Dキャラクターをミラー処理するPlayableを作りたい
2D格闘ゲームや横スクロールアクションゲームでキャラクターや背景を3Dで表現した、いわゆる2.5Dゲームにおいて、キャラクターをミラーリング(左右反転)処理したいケースがあります。
なぜミラーリングが必要なのか?それはキャラクターは常に画面側を向いててほしいからです。
普通の3Dキャラクターでは右向きで画面に向くよう調整しても、左向きだと背中向けることになってしまい、こちら側を見てくれません。
ミラーリング処理をすることで、左向きでも右向きでも常にこちら向いてくれるようになるわけです。UnityのMecanimには既にHumanoidに限りMirror機能がありますが、それを使わずにPlayables APIを使って自前で実装します。
結果
左右反転した動きをする3Dキャラクターを表示できました。
が、、よく見ると顔パーツの反転ができてません。
顔の中身をテクスチャで描いているためです。こちらの問題に対応するにXスケールを反転する(ポリゴンが反転するため、シェーダー側の対応も必要)するか、左右反転したテクスチャを用意しておいて、それに切り替えるかで対応する必要があります。
目をボーンで制御している昨今の3Dキャラクターであれば得に問題ないでしょう。そもそも完全な左右対称の結果を得るにはシンメトリー(左右対称)なキャラクターに限定している話でもあります。
アシンメトリー(左右非対称)なキャラクターの場合、ある程度なら反転結果の差異を許容してもよいですが、場合によっては難しいケース(腕の長さが違う等)もあります。
今記事ではそこまでは触れないでおきます。今回作成したサンプルプロジェクトをGithubで公開してます。合わせてどうぞ。
https://github.com/you-ri/MirrorAnimationPlayableなぜMecanim(AnimatorController)のMirrorを使わず自前で実装するのか?
Mecanimには既にMirror機能があります。なのになぜ自前で用意するのでしょうか?
自身のプロジェクトは当初普通にAnimatorControllerを使っていましたが、2018.2あたりでAnimatorControllerのPlayable版、AnimatorControllerPlayableに移行しました。
ところがUnity2019.2にバージョンアップした際、キャラクターのアニメーションが動かなくなるバグを踏んでしまいました。
AnimatorControllerOverrideを使ってランタイム中に書き換えるアプローチで大量のAnimationClipを処理していたのですが、これが機能しなくなったのです。当然バグ報告しましたが、音沙汰なし。。。これは、、、仕様ということか?
しびれを切らして、自前で大量のAnimationClipを捌き、ミラーを再現する機能をPlayables APIを使って開発する必要がでてきたというわけです。HumanoidのミラーをAnimationScriptPlayabeで再現する
まずググったところ以下のスレッドがヒットしました。
Playables API - Mirroring Clips and DirectorUpdateMode.Manual
残念なのことにUnityの中の人も途中で投げだしてしまって解決には至っていません。
Muscleの情報から顔や体のロール(RollLeftRight)とヨー(LeftRight)を反転、さらに左右の手、足、指、目などは交換するとで、ミラーした結果が得られるのは理解できるのですが。なぜうまく行かないのか。。。少し悩んだ末、ルートの回転ミラー計算を見直したところ、左右反転後の姿勢を作り出せることに成功しました。
ついでにIKのミラーにも対応しておきました。public static void MirrorPose (this AnimationHumanStream humanStream) { humanStream.bodyLocalPosition = Mirrored (humanStream.bodyLocalPosition); humanStream.bodyLocalRotation = Mirrored (humanStream.bodyLocalRotation); // mirror body for (int i = 0; i < (int)BodyDof.LastBodyDof; i++) { humanStream.MultMuscle (new MuscleHandle ((BodyDof)i), BodyDoFMirror[i]); } // mirror head for (int i = 0; i < (int)HeadDof.LastHeadDof; i++) { humanStream.MultMuscle (new MuscleHandle ((HeadDof)i), HeadDoFMirror[i]); } // swap arms for (int i = 0; i < (int)ArmDof.LastArmDof; i++) { humanStream.SwapMuscles ( new MuscleHandle (HumanPartDof.LeftArm, (ArmDof)i), new MuscleHandle (HumanPartDof.RightArm, (ArmDof)i)); } // swap legs for (int i = 0; i < (int)LegDof.LastLegDof; i++) { humanStream.SwapMuscles ( new MuscleHandle (HumanPartDof.LeftLeg, (LegDof)i), new MuscleHandle (HumanPartDof.RightLeg, (LegDof)i)); } // swap fingers for (int i = 0; i < (int)FingerDof.LastFingerDof; i++) { humanStream.SwapMuscles ( new MuscleHandle (HumanPartDof.LeftThumb, (FingerDof)i), new MuscleHandle (HumanPartDof.RightThumb, (FingerDof)i)); humanStream.SwapMuscles ( new MuscleHandle (HumanPartDof.LeftIndex, (FingerDof)i), new MuscleHandle (HumanPartDof.RightIndex, (FingerDof)i)); humanStream.SwapMuscles ( new MuscleHandle (HumanPartDof.LeftMiddle, (FingerDof)i), new MuscleHandle (HumanPartDof.RightMiddle, (FingerDof)i)); humanStream.SwapMuscles ( new MuscleHandle (HumanPartDof.LeftRing, (FingerDof)i), new MuscleHandle (HumanPartDof.RightRing, (FingerDof)i)); humanStream.SwapMuscles ( new MuscleHandle (HumanPartDof.LeftLittle, (FingerDof)i), new MuscleHandle (HumanPartDof.RightLittle, (FingerDof)i)); } // swap ik Vector3[] goalPositions = new Vector3[4]; Quaternion[] goalRotations = new Quaternion[4]; float[] goalWeightPositons = new float[4]; float[] goalWeightRotations = new float[4]; Vector3[] hintPositions = new Vector3[4]; float[] hintWeightPositions = new float[4]; for (int i = 0; i < 4; i++) { goalPositions[i] = humanStream.GetGoalLocalPosition (AvatarIKGoal.LeftFoot + i); goalRotations[i] = humanStream.GetGoalLocalRotation (AvatarIKGoal.LeftFoot + i); goalWeightPositons[i] = humanStream.GetGoalWeightPosition (AvatarIKGoal.LeftFoot + i); goalWeightRotations[i] = humanStream.GetGoalWeightRotation (AvatarIKGoal.LeftFoot + i); hintPositions[i] = humanStream.GetHintPosition (AvatarIKHint.LeftKnee + i); hintWeightPositions[i] = humanStream.GetHintWeightPosition (AvatarIKHint.LeftKnee + i); } for (int i = 0; i < 4; i++) { int j = (i + 1) % 2 + (i / 2) * 2; // make [1, 0, 3, 2] humanStream.SetGoalLocalPosition (AvatarIKGoal.LeftFoot + i, Mirrored(goalPositions[j])); humanStream.SetGoalLocalRotation (AvatarIKGoal.LeftFoot + i, Mirrored(goalRotations[j])); humanStream.SetGoalWeightPosition (AvatarIKGoal.LeftFoot + i, goalWeightPositons[j]); humanStream.SetGoalWeightRotation (AvatarIKGoal.LeftFoot + i, goalWeightRotations[j]); humanStream.SetHintPosition (AvatarIKHint.LeftKnee + i, hintPositions[j]); humanStream.SetHintWeightPosition (AvatarIKHint.LeftKnee + i, hintWeightPositions[j]); } } public static Vector3 Mirrored (Vector3 value) { return new Vector3 (-value.x, value.y, value.z); } public static Quaternion Mirrored (Quaternion value) { return Quaternion.Euler (value.eulerAngles.x, -value.eulerAngles.y, -value.eulerAngles.z); }しかし、これだけでは不完全です。武器もミラーリングしないと実際のゲームでは使えません。
武器のミラーに対応する
Humanoidのミラー対応できました。次は武器、追加ボーンのミラー対応をします。
武器のミラーリングは複製した武器をもう片方の手にもたせて、表示、非表示で表現する方法が最も簡単ですが、武器オブジェクトが複製されているので、更新も2重に適応したりする必要があったり扱いが煩雑になりそうです。
今回はミラーリング時には片方のボーンに固定することで擬似的に親子関係が変わったように表現します。
武器の親ボーンをミラーリングした後に、武器をそのボーン下のトランスフォームに固定します。ヒエラルキーのセットアップ
開発中のゲームのモデルを例に説明したいと思います。
まずミラーリング対応するためのヒエラルキーをセットアップします。アニメーション制作時に向く方向は統一しておきます。今回
weapon_R
にアニメーション情報が入っており、常にAnimationClipによって更新されます。
それの反転姿勢を保持するためのweapon_L
を作り、その子にミラー後の表示位置を確認するための武器としてjanis_weapon_mirror
をアタッチし、に左右対称になるように調整しておきます。
janis_weapon_mirror
はミラー後の武器のアタリとして用意するだけで実際は表示しません。無効化しておきます。追加トランスフォーム分をミラー処理する
武器の親トランスフォームをミラーリングしないといけません。
weapon_R
のミラーリング後した姿勢をweapon_L
へ反映します。Humanoidのミラーリング処理する前に武器ボーンをミラーリングします。
ミラーリングしたいボーンをキャラクターの座標系(ルート座標系)に変換した後、XY平面でミラー化してその結果を片方のボーンに設定します。public struct MirroringPlayableJob : IAnimationJob, IDisposable { public struct MirroringConstrant { public TransformStreamHandle driven; public TransformStreamHandle source; } public struct MirroringPosture { public TransformStreamHandle source; public TransformStreamHandle driven; } public bool debug; public bool isMirror; public TransformStreamHandle root; public NativeArray<MirroringPosture> mirroringTransforms; public NativeArray<MirroringConstrant> mirroringConstrants; public void ProcessRootMotion (AnimationStream stream) { } public void ProcessAnimation (AnimationStream stream) { Vector3 rootPosition; Quaternion rootRotation; root.GetGlobalTR (stream, out rootPosition, out rootRotation); var rootTx = new AffineTransform (rootPosition, rootRotation); var mirroredTransforms = new NativeArray<AffineTransform> (mirroringTransforms.Length, Allocator.Temp); // 追加トランスフォームのミラーリング計算 if (isMirror) { for (int i = 0; i < mirroringTransforms.Length; i++) { if (!mirroringTransforms[i].source.IsValid (stream)) continue; if (!mirroringTransforms[i].driven.IsValid (stream)) continue; Vector3 position; Quaternion rotation; mirroringTransforms[i].source.GetGlobalTR (stream, out position, out rotation); var drivenTx = new AffineTransform (position, rotation); drivenTx = rootTx.Inverse() * drivenTx; drivenTx = AnimationStreamMirrorExtensions.Mirrored (drivenTx); drivenTx = rootTx * drivenTx; mirroredTransforms[i] = drivenTx; } } // Humanoid ミラーリング ~~ 省略 ~~ // 追加トランスフォームのミラーリング適用 if (isMirror) { for (int i = 0; i < mirroringTransforms.Length; i++) { if (!mirroringTransforms[i].source.IsValid (stream)) continue; if (!mirroringTransforms[i].driven.IsValid (stream)) continue; mirroringTransforms[i].driven.SetGlobalTR (stream, mirroredTransforms[i].position, mirroredTransforms[i].rotation, false); } } // 追加トランスフォームのミラーリング拘束 ~~ 省略 ~~ } }ミラー時にペアレントを切り替える
ミラー後のトランスフォームに武器を拘束して親子関係を切り替えたように見せます。
やり方は単純です。
この段階ではjanis_weapon_mirror
の親トランスフォームのweapon_L
にはミラー後の姿勢になっているのでjanis_weapon_mirror
の姿勢をjanis_weapon
にコピーするだけで、あたかも親トランスフォームがweapon_L
に切り替わったかのように見せることができます。public void ProcessAnimation (AnimationStream stream) { // Humanoid のミラーリング ~~ 省略 ~~ // 追加トランスフォームのミラーリングの適用 ~~ 省略 ~~ // ミラーリングしたトランスフォームへのコンストレント if (isMirror) { for (int i = 0; i < mirroringConstrants.Length; i++) { if (!mirroringConstrants[i].source.IsValid (stream)) continue; if (!mirroringConstrants[i].driven.IsValid (stream)) continue; Vector3 position; Quaternion rotation; mirroringConstrants[i].source.GetGlobalTR (stream, out position, out rotation); mirroringConstrants[i].driven.SetGlobalTR (stream, position, rotation, false); } } }あとはこれらのコードをPlayableGraphに登録するためのコンポーネントを作成すればおkです。
それの詳細についてはここでは言及しません。サンプルプロジェクトの
CustomAnimation.csを参照ください。まとめ
これで3Dキャラクターを使って2Dゲーム的な表現ができるようになった!といいたいところですが、画角があるため立ち位置によって見え方が変化してしまう問題が残っています。
これを解決するにはキャラクターやそれに付随するエフェクトなどは画角を抑えて描画する必要があります。
それはまた別の機会があれば記事にしたいと思います。3Dキャラクターを使うことで開発中もデザインを後から修正することが楽になりますし、ランタイムで入れ替えることも可能になります。
3Dくささを打ち消すのが大変だったりしますが、魅力的なキャラクターが活躍する2.5Dゲームが増えればいいな~と思っています。コツコツとゲーム作ってます。
対戦格闘アクション http://extrival.com参考
- 投稿日:2019-12-23T21:55:34+09:00
Unity内で取得したデータをcsvに書き出す方法
1. はじめに
Unityのゲーム内で取得したデータをcsvに書き出す方法についての記事を書きます。
具体的には、2つのGameObjectの操作記録をcsvに時系列順で書き出すスクリプトを作成します。
2. 準備
2.1. 用意するGameObject
用意するGameObjectは3つです。
- F … 赤色のGameObject
- J … 水色のGameObject
- SaveCsv … EmptyGameObject
2.2. 用意するスクリプト
用意するはスクリプトも3つです。
- SampleFScript … F用のスクリプト
- SampleJScript … J用のスクリプト
- SampleSaveCsvScript … SaveCsv用のスクリプト
3. スクリプトに記述
3.1. SampleSaveCsvScripのコードの解説
まずは、csvに保存するためのコードをSampleSaveCsvScripに記述します。
3.1.1. void Start()
引用元サイト①で詳しく解説されています。
ざっくりとした説明をすると、新しくcsvファイルを作成して、{}の中の要素分csvに追記をするコードです。3.1.2. public void SaveData(string txt1, string txt2, string txt3)
「s1」で記述したヘッダーの数分「string txt」を用意してください。
「public」をつけることで他のスクリプトでも「SaveData(~)」が使用できるようになります。
その他については、上記と同じです。3.1.2. void Update()
Enterキーが押されたらcsvへの書き込みを終了するコードを記述しています。
SampleSaveCsvScripusing System.IO; using System.Text; public class SampleSaveCsvScript : MonoBehaviour { private StreamWriter sw; void Start() { sw = new StreamWriter(@"SaveData.csv", true, Encoding.GetEncoding("Shift_JIS")); string[] s1 = { "F", "J", "time" }; string s2 = string.Join(",", s1); sw.WriteLine(s2); } public void SaveData(string txt1, string txt2, string txt3) { string[] s1 = { txt1, txt2, txt3 }; string s2 = string.Join(",", s1); sw.WriteLine(s2); } void Update() { if (Input.GetKeyDown(KeyCode.Return)) { sw.Close(); } } }3.2. SampleFScriptのコードの解説
SampleFScriptには、Fキーを検出するコードを記述します。
3.2.1. void Start()
引用元サイト②で詳しく解説されています。
ざっくりとした説明をすると、他のスクリプトを参照するコードを記述しています。3.2.2. void Update()
Fキーが押されたら、csvに「F」と「いつ押されたか」という情報が書き加えられます。
SampleFScripusing UnityEngine; using System.IO; public class SampleFScript : MonoBehaviour { private float time; private StreamWriter sw; GameObject SaveCsv; SampleSaveCsvScript SampleSaveCsvScript; void Start() { SaveCsv = GameObject.Find("SaveCsv"); SampleSaveCsvScript = SaveCsv.GetComponent<SampleSaveCsvScript>(); } void Update() { time += Time.deltaTime; if (Input.GetKeyDown(KeyCode.F)) { SampleSaveCsvScript.SaveData("F", " ", time.ToString()); } } }3.3. SampleJScriptのコードの解説
SampleJScriptには、Jキーを検出するコードを記述します。
3.3.1. void Start()
上記と同じです。
3.2.2. void Update()
Jキーが押されたら、csvに「J」と「いつ押されたか」という情報が書き加えられます。
SampleJScripusing UnityEngine; using System.IO; public class SampleFScript : MonoBehaviour { private float time; private StreamWriter sw; GameObject SaveCsv; SampleSaveCsvScript SampleSaveCsvScript; void Start() { SaveCsv = GameObject.Find("SaveCsv"); SampleSaveCsvScript = SaveCsv.GetComponent<SampleSaveCsvScript>(); } void Update() { time += Time.deltaTime; if (Input.GetKeyDown(KeyCode.J)) { SampleSaveCsvScript.SaveData(" ", "J", time.ToString()); } } }4. 確認
実際にゲームを動かして確認をしてみます(ここは省略)。
すると、下記のようなcsvが出てきます。
「F」や「J」そして「いつ押されたか」という情報がきちんと書き出されていることが確認できると思います。5. 最後に
Unityのゲーム内で取得したデータをcsvに書き出す方法について書いてみましたが、いかかだったでしょうか。
分かりづらい点や間違っている点があれば、ご指摘いただけると幸いです。参考URL
① Unity 2D] データを保存する(外部ファイルCSV)
https://high-programmer.com/2017/12/10/unity-savedata-otherfile/② [Unity]他のオブジェクトについているスクリプトの変数を参照したり関数を実行したりする。
https://qiita.com/tsukasa_wear_parker/items/09d4bcc5af3556b9bb3a
- 投稿日:2019-12-23T19:12:46+09:00
GitHub ActionsでUnityのAndroid, iOSビルドをやってみる
この記事は Akatsuki Advent Calendar 2019 23日目の記事です。
前日は @ShaderError さんによる Unityでシェーダー描いてみたい でした。
アカツキ人事がハートドリブンに書く Advent Calendar 2019 もあるのでそちらもぜひ。はじめに
以前に仕事でCircleCIを使ってUnityのAndroidとiOSビルドを行なっていたのでその事について書こうかなと思っていたのですが、最近
GitHub Actions
という新機能がGitHubで公開されました。
これがいい感じにCircleCIでUnityビルドをしていた際に起きていた問題を解決していたので、試験的に試してみたまとめが本記事になります。環境
GitHub Actions 12/23時点のワークフロー構文
Unity 2019.3.0f1
MacBook Pro (self-hostedで利用するマシン)GitHub Actionsとは
GitHubによるCI/CDツールです。
詳しいことは GitHub Actions Documentation に書いてありますが、Jenkins
やCircleCI
などに替わる新しいツールとなるのか(個人的に)気になるサービスです。GitHub Actions を導入する
前提:UnityProjectのリポジトリがGitHubに存在する状態であること
利用想定:self-hostedを使ってローカルにあるマシンをGitHub Actions
で利用する
前提の状態にするまでの解説は行いませんので、各自作業を行なってください。GitHubのリポジトリの Setting -> Actions を選択して
Actions permissions
もEnable local and third party Actions for this repository
に変更してください
その後にSelf-hosted runners
のAdd runner
を選択
そうするとrunnerを追加するためのCLIのコマンドが表示されます。
各コマンドを順に実行してください。
実行した後にGitHubのページを再読み込みをするとSelf-hosted runners
にCLIコマンドを実行したPCが表示されていると思います。
これでGitHub Actions
でself-hosted
が使えるようになりました。
その後にGitHubのActionsタブにあるSimple workflow
を使って動作テストをしてみます。
runs-on
をself-hosted
にするのを忘れないでください。
変更が完了しコミットをすると、self-hosted
でbuild.yml
が実行されます。
なお、こちらに記載がありますが、ワークフローファイルは、リポジトリの.github/workflowsディレクトリに保存する必要があるのでご注意ください。
実行結果はActionタブから確認できます。
おそらく以下の画像のような結果になっていると思います。
これでローカルにあるself-hosted
に追加されたマシンから実行できるようになりました。
ひとまずこれで基本設定は完了になります。
Unityのビルドスクリプトを作成する
ここからは本格的にビルド処理を実装していきます。
まずはUnityでビルドスクリプトを書きます。
using System; using System.Linq; using UnityEngine; using UnityEditor; using UnityEditor.Build.Reporting; using System.Collections.Generic; using UnityEditor.iOS.Xcode; using UnityEditor.Callbacks; public class MobileBuild { static string[] GetEnabledScenes() { return ( from scene in EditorBuildSettings.scenes where scene.enabled where !string.IsNullOrEmpty(scene.path) select scene.path ).ToArray(); } private static void BuildAndroid() { // Setting for Android EditorPrefs.SetBool("NdkUseEmbedded", true); EditorPrefs.SetBool("SdkUseEmbedded", true); EditorPrefs.SetBool("JdkUseEmbedded", true); EditorUserBuildSettings.androidBuildSystem = AndroidBuildSystem.Gradle; PlayerSettings.SetScriptingBackend(BuildTargetGroup.Android, ScriptingImplementation.IL2CPP); // Build bool result = Build(BuildTarget.Android); // Exit Editor EditorApplication.Exit(result ? 0 : 1); } private static void BuildIOS() { // Setting for iOS PlayerSettings.SetScriptingBackend(BuildTargetGroup.iOS, ScriptingImplementation.IL2CPP); EditorUserBuildSettings.iOSBuildConfigType = iOSBuildType.Debug; // Build bool result = Build(BuildTarget.iOS); // Exit Editor EditorApplication.Exit(result ? 0 : 1); } private static bool Build(BuildTarget buildTarget) { // Get Env string outputPath = GetEnvVar("OUTPUT_PATH"); // Output path string bundleId = GetEnvVar("BUNDLE_ID"); // Bundle Identifier string productName = GetEnvVar("PRODUCT_NAME"); // Product Name string companyName = GetEnvVar("COMPANY_NAME"); // Company Name outputPath = AddExpand(buildTarget, outputPath); Debug.Log("[MobileBuild] Build OUTPUT_PATH :" + outputPath); Debug.Log("[MobileBuild] Build BUILD_SCENES :" + String.Join("", GetEnabledScenes())); // Player Settings BuildOptions buildOptions; buildOptions = BuildOptions.Development | BuildOptions.CompressWithLz4; if (!string.IsNullOrEmpty(companyName)) { PlayerSettings.companyName = companyName; } if (!string.IsNullOrEmpty(productName)) { PlayerSettings.productName = productName; } if (!string.IsNullOrEmpty(bundleId)) { PlayerSettings.applicationIdentifier = bundleId; } // Build var report = BuildPipeline.BuildPlayer(GetEnabledScenes(), outputPath, buildTarget, buildOptions); var summary = report.summary; // Build Report for (int i = 0; i < report.steps.Length; ++i) { var step = report.steps[i]; Debug.Log($"{step.name} Depth:{step.depth} Duration:{step.duration}"); for (int d = 0; d < step.messages.Length; ++d) { Debug.Log($"{step.messages[d].content}"); } } if (summary.result == BuildResult.Succeeded) { Debug.Log("<color=white>[MobileBuild] Build Success : " + outputPath + "</color>"); return true; } else { Debug.Assert(false, "[MobileBuild] Build Error : " + report.name); return false; } } private static string GetEnvVar(string pKey) { return Environment.GetEnvironmentVariable(pKey); } private static string AddExpand(BuildTarget buildTarget, string outputPath) { switch (buildTarget) { case BuildTarget.Android : outputPath += ".apk"; break; } return outputPath; } }大まかに書くとこのような感じになるかと思います。
ビルド後に実行する処理等は書いていませんので、必要に応じて追加してください。ローカルで上記のスクリプトが動作するか以下のコマンドをCLIで確認してください。
# 適宜書き換えてください export COMPANY_NAME="" export PRODUCT_NAME="" export BUNDLE_ID="" export OUTPUT_PATH ="" /Applications/Unity/Hub/Editor/2019.3.0f1/Unity.app/Contents/MacOS/Unity -quit -batchmode -nographics -silent-crashes -logFile -projectPath ./ -executeMethod MobileBuild.BuildIOS -buildTarget iOS /Applications/Unity/Hub/Editor/2019.3.0f1/Unity.app/Contents/MacOS/Unity -quit -batchmode -nographics -silent-crashes -logFile -projectPath ./ -executeMethod MobileBuild.BuildAndroid -buildTarget AndroidCLIが実行できたのであればいよいよ
GitHub Actions
を使った話へ進みますGitHub Actions で Unity のビルドを行う
Android Build
まずはAndroidのビルドを行います。
Unity2019からAndroidのNDKやJDKがインストール時に追加できるようになりました!
なのでNDKやJDKの設定は各自でお願いします(いい時代になりましたね)早速
build.yml
ファイルを書き換えましょうname: ApplicationBuild on: [push, pull_request] env: OUTPUT_PATH: "" BUNDLE_ID: "" PRODUCT_NAME: "" COMPANY_NAME: "" UNITY_VERSION: 2019.3.0f1 jobs: build: runs-on: self-hosted steps: - uses: actions/checkout@v2 - name: Android Build run: | /Applications/Unity/Hub/Editor/$UNITY_VERSION/Unity.app/Contents/MacOS/Unity -quit -batchmode -nographics -silent-crashes -logFile \ -projectPath ./ -executeMethod MobileBuild.BuildAndroid -buildTarget AndroidAndroidビルドをするだけのシンプルな処理です。
環境変数は各自で適切に入力してください。いくつかを説明を軽くしますと、
name
name:
はワークフローの名前になります。
この名前がリポジトリのアクションページにワークフローに表示されます。
on
on:
はワークフローをトリガーするGitHubイベントの名前です。
イベントの種類は多くありこちらにドキュメントとしてまとめてあります。
今回はpush
とpull_request
をトリガーにしています。env
env
はワークフローの全てのジョブから利用できる環境変数を定義します。jobs.<job_id>.env
は<job_id>
ジョブから利用できる環境変数を定義します。jobs.<job_id>.steps.env
はステップから利用できる環境変数を定義します。先ほどの
build.yml
は 2. のenv
を使っています。
この後のiOSビルドでも使いますからね。jobs.job_id.steps.uses
ジョブでステップの一部として実行されるアクションを選択します。
actions/checkout
はワークフローで使用できる標準アクションで、v2
を指定することでチェックアウトアクションのv2
を利用するという設定になります。(標準のアクションはこちらにまとまっています)jobs.job_id.steps.run
オペレーティングシステムのシェルを使用してコマンドラインプログラムを実行します。
さらにshell
キーワードを使用すると、環境のOSのデフォルトシェルを上書きできます。iOS Build
続いてiOSビルドです。
今回は時間がなくなったのと複雑になるので、ipaファイルをビルドするところまでは行わず、xcodeprojをビルドするところまで行います。
では、build.yml
を書き換えましょう。name: ApplicationBuild on: [push, pull_request] env: OUTPUT_PATH: "" BUNDLE_ID: "" PRODUCT_NAME: "" COMPANY_NAME: "" UNITY_VERSION: "2019.3.0f1" jobs: android-build: runs-on: self-hosted steps: - uses: actions/checkout@v2 with: path: android - name: Android Build run: | /Applications/Unity/Hub/Editor/$UNITY_VERSION/Unity.app/Contents/MacOS/Unity -quit -batchmode -nographics -silent-crashes -logFile \ -projectPath ./android -executeMethod MobileBuild.BuildAndroid -buildTarget Android ios-build: runs-on: self-hosted steps: - uses: actions/checkout@v2 with: path: ios - name: iOS Build run: | /Applications/Unity/Hub/Editor/$UNITY_VERSION/Unity.app/Contents/MacOS/Unity -quit -batchmode -nographics -silent-crashes -logFile \ -projectPath ./ios -executeMethod MobileBuild.BuildIOS -buildTarget iOS書き方が変わった箇所がありますね。
actions/checkout
にwith:
キーワードを使ってpath:
の設定を渡しています。
こうしている理由は、AndroidとiOSで利用するフォルダを分けてインポート時間やビルド時間の高速化するためです。この状態でpushしてみましょう。
self-hosted
に登録したサーバーが動作してビルドが実行されるはずです。
AndroidとiOSのビルドが成功すると以下のような画面になります。
暗号化されたシークレットを利用する
実際に運用しようとなると、
yml
に書きたくない情報を渡したいケースがあると思います。
その場合は GitHub のリポジトリの Setting -> Secret ページへいきAdd a new secret
をクリック。
シークレットとは、暗号化された環境変数のことです。
Name
とValue
をそれぞれ入力します。
ここで設定した値をワークフローで利用するにはenv: SOME_PARAM: ${{ secrets.SOME_PARAM }}のように記述することで可能です。
これでpassword
やTOKEN
の情報を渡すことができますね!
なお、シークレットの制限として
1. ワークフローで最大100のシークレットを持てる
2. シークレットの容量は最大64KB
の2点があります。
参考: 暗号化されたシークレットの作成と利用終わりに
個人的にとてもいいツールだと感じました。
具体的には以下の点に可能性があると思います
1. マシンスペックをこちらで自由にカスタマイズできる(self-hostedの場合)
2. CI/CDがGitHubで完結する
3. Jenkinsから解放される(アセットバンドルをどうするのかという問題はありますが・・・)他にもCircleCI(MacOS)と違ってUnityのインストールを毎回行う必要がなかったりと比較すると優れている点が多いなという印象です。
簡単な導入まででしたが、以上になります。
参考になれば幸いです。
- 投稿日:2019-12-23T18:49:39+09:00
UniRx オペレータ一覧
はじめに
以前、UniRx オペレータ逆引きという記事は書いたのですが、正引きがなかったので改めてまとめました。
フィルタ系
名前 効果・用途 備考 Where<T>
1:OnNextメッセージの内容を条件式でフィルタリングする 後ろが Select
の場合は最適化が自動的に走る2:OnNextメッセージの内容および発行回数でフィルタリングする OfType<T>
指定した型にキャスト可能なメッセージのみ通す Cast
との違いは、変換失敗時に”何もしない”ことIgnoreElements<T>
すべてのOnNextメッセージをフィルタリングする 何も通さない Distinct<T>
過去に入力された同値のメッセージをフィルタリングする Func<TSource, TKey>
で比較値の取り出し方法の指定、およびIEqualityComparer
の指定が可能DistinctUntilChanged<T>
直前に入力されたメッセージと同値の場合にフィルタリングする Func<TSource, TKey>
で比較値の取り出し方法の指定、およびIEqualityComparer
の指定が可能First<T>
条件を満たした最初の1つのOnNextメッセージのみを取り出したあと、OnCompletedメッセージをセットで発行する 条件式を指定可能。OnNextメッセージが1つも入力されなかった場合は OnError(InvalidOperationException)
が発行される。FirstOrDefault<T>
条件を満たした最初の1つのOnNextメッセージのみを取り出したあと、OnCompletedメッセージをセットで発行する 条件式を指定可能。OnNextメッセージが1つも入力されなかった場合は OnNext(default<T>)
が発行される。Last<T>
条件を満たした最後の1つのOnNextメッセージのみを取り出したあと、OnCompletedメッセージをセットで発行する 条件式を指定可能。OnNextメッセージが1つも入力されなかった場合は OnError(InvalidOperationException)
が発行される。LastOrDefault<T>
条件を満たした最後の1つのOnNextメッセージのみを取り出したあと、OnCompletedメッセージをセットで発行する 条件式を指定可能。OnNextメッセージが1つも入力されなかった場合は OnNext(default<T>)
が発行される。Single<T>
条件を満たすOnNextメッセージは必ず1回入力されるという制約をつける 入力が0回だった、または2回以上条件を満たすOnNextメッセージが入力された場合は OnError(InvalidOperationException)
が発行される。SingleOrDefault<T>
条件を満たすOnNextメッセージは必ず1回入力されるという制約をつける 入力が0回だった、または2回以上条件を満たすOnNextメッセージが入力された場合は OnNext(default<T>)
が発行される。Skip<T>
1:先頭から指定した個数のOnNextメッセージを無視する 引数は int
(無視する個数)2:購読開始から指定した期間メッセージを無視する 引数は TimeSpan
および使用するScheduler
SkipWhile<T>
条件を満たす間はメッセージを無視する。一度でも条件を満たさなくなるとそれ以降は常に通過させる。 Take<T>
1:先頭から指定した個数のOnNextメッセージのみ通過させ、その直後にOnCompletedメッセージを発行する 引数は int
(通過させる個数)2:購読開始から指定した期間のみメッセージを通過させる。その後はOnCompletedメッセージを発行する 引数は TimeSpan
および使用するScheduler
TakeWhile<T>
条件を満たす間のみメッセージを通過させる。条件を満たさなくなるとOnCompletedメッセージを発行する TakeUntil<T>
他の Observable
からのOnNextメッセージ入力を受けた瞬間に、OnCompletedメッセージを発行するObservable
を外から止めたいときに使えるTakeUntilDestroy<T>
指定した GameObject
およびComponent
が破棄されたタイミングでOnCompletedメッセージを発行するGameObject
などに連動してObservable
を止めたいときに使えるTakeUntilDisable<T>
指定した GameObject
およびComponent
がOnDisable()
されたタイミングでOnCompletedメッセージを発行するTakeLast<T>
1:OnCompletedメッセージが入力されたタイミングで、直前のOnNextメッセージを指定の個数分取り出す OnCompletedメッセージが入力されるまで動作しない 2:OnCompletedメッセージが入力されたタイミング、直前のOnNextメッセージを指定の期間分取り出す OnCompletedメッセージが入力されるまで動作しない Throttle<T>
短期間に大量にメッセージが入力された場合はそれを無視し、落ち着いたタイミングで最後の1つだけを取り出して流す 引数は Timespan
(落ち着いたと判断するまでの猶予時間)ThrottleFrame<T>
短期間に大量にメッセージが入力された場合はそれを無視し、落ち着いたタイミングで最後の1つだけを取り出して流す 引数はフレーム数 ThrottleFirst<T>
1度OnNextメッセージが入力されたらそれを流し、そのあとは一定時間メッセージを無視する 連打の防止などに使える。引数は Timespan
ThrottleFirstFrame<T>
1度OnNextメッセージが入力されたらそれを流し、そのあとは一定時間メッセージを無視する 連打の防止などに使える。引数はフレーム数 メッセージの変換系
名前 効果・用途 備考 Select<T>
1:OnNextメッセージの内容を別の値に変換する 後ろが Where
の場合は最適化が自動的に走る2:OnNextメッセージおよび発行回数を用いて別の値に変換する Cast<T>
OnNextメッセージを指定した型にキャストする キャスト失敗時にはOnErrorメッセージが発行される AsUnitObservable
OnNextメッセージの型を Unit
型に変換するSelect(_ => Unit.Default)
の省略記法AsSingleUnitObservable
Observableが終了するタイミングで Unit
型のOnNextメッセージを1回だけ発行するLastOrDefault().AsUnitObservable()
メッセージやストリームの合成系
名前 効果・用途 備考 Merge<T>
1.複数のストリームの内容を並列に結合する 引数は IObservable<T>[]
2. IObservable<IObservable<T>>
をIObservable<T>
にまとめるIObservable<IObservable<T>>
に対する拡張メソッド3.複数のストリームの内容を並列に結合する IEnumerable<IObservable<T>>
に対する拡張メソッドConcat<T>
1.複数のストリームの内容を直列に結合する 引数は IObservable<T>[]
2. IObservable<IObservable<T>>
を先頭から順番に購読するIObservable<IObservable<T>>
に対する拡張メソッド3.複数の IObservable<T>
を先頭から順番に購読するIEnumerable<IObservable<TSource>>
に対する拡張メソッドSelectMany<T>
1.OnNextメッセージを使って新しい Observable
を生成し、それを並列に合成するflatMap
に相当する2.OnNextメッセージを使って新しい Observable
を生成し、そのメッセージを入力値と合成してから、並列に合成する引数は「 Func<T, IObservable<TR>> collectionSelector
」「Func<T, TC, TR> resultSelector
」の2つContinueWith<T>
SelectMany<T>
の軽量版。1回しか実行されない代わりに軽量。1回しかメッセージが発行されない非同期処理に向く Switch<T>
購読する対象の Observable
を次々に切り替えるIObservable<IObservable<T>>
に対してのみ使えるAggregate<T>
OnNextメッセージに対して加工を行い、その結果を記録してさらに次のメッセージに引き継ぐ Scan<T>
との違いは、OnCompletedメッセージが発行されたタイミングで最終結果を出力するScan<T>
OnNextメッセージに対して加工を行い、その結果を記録してさらに次のメッセージに引き継ぐ Aggregate<T>
との違いは、こちらは1回処理するたびにメッセージを発行するBuffer<T>
1.複数のOnNextメッセージを指定した個数分まとめて、1つのOnNextメッセージに加工する count
==skip
2.複数のOnNextメッセージを指定した個数分まとめて、1つのOnNextメッセージに加工したあと、指定個数スキップする count
とskip
を別々に設定できる。Buffer(count:2, skip:1)
にすると直前のメッセージとセットにしてメッセージを発行できる。3.指定の時間間隔でメッセージをまとめる 4.指定の時間間隔でメッセージをまとめたあと、指定した時間計測を一時中断する 5.個数指定と時間指定を両方組み合わせてメッセージをまとめる 時間か個数どちらかの条件を満たしたタイミングで出力される 6.メッセージをまとめ、外部からメッセージ入力されたタイミングで出力する 引数は IObservable<TWindowBoundary>
。TWindowBoundary
の型はなんでもよいBatchFrame<T>
1.指定したフレーム数の間に発行されたOnNextメッセージを1つにまとめる フレーム数と、 FrameCountType
を指定できる2.メッセージのタイミングを指定の FrameCountType
のタイミング変換するIObservable<Unit>
の場合PairWise<T>
1. 1個前のメッセージと最新のメッセージをセットにして出力する Buffer(count:2, skip:1)
とだいたいおなじ挙動2. 1個前のメッセージと最新のメッセージをセットにして、それをさらに加工して出力する Buffer(count:2, skip:1).Select()
に似ているZip<TLeft,TRight,TResult>
1. 型が異なる2つのストリームを購読し、値がそれぞれセットで揃ったタイミングで加工して出力する IObservable<TLeft>
に対してIObservable<TRight>
を合成し、IObservable<TResult>
になるZip<T>
2. 型が同じストリームを購読し、値がそれぞれセットで揃ったタイミングで加工して出力する。最大7ストリームまで合成できる IObservable<T>
に対する拡張メソッドZip<T>
3. 型が同じ複数のObservableをまとめ、 IList<T>
として出力する「 IEnumerable<IObservable<T>>
に定義された拡張メソッド」または「Observable.Zip
」から利用できる。合成できるストリーム数に上限はないZipLatest<TLeft,TRight,TResult>
1. 型が異なる2つのストリームを購読し、値がそれぞれセットで揃ったタイミングで最新値のみを取り出して加工して出力する Zip
との違いは、こちらは余剰に入力されたメッセージは破棄されるZipLatest<T>
2. 型が同じストリームを購読し、値がそれぞれセットで揃ったタイミングで最新値のみを取り出して加工して出力する。最大7ストリームまで合成できる Zip
との違いは、こちらは余剰に入力されたメッセージは破棄されるCombineLatest<TLeft,TRight,TResult>
過去に入力されたメッセージ値を記憶し、それを流用して合成を強制的に行う Zip
との違いは、値がセットにならなくてもメッセージを無理やり合成するWithLatestFrom<TLeft,TRight,TResult>
1つのストリームを主軸とし、そこに別のストリームの最新値を合成する 別のストリームのメッセージを、もう片方のストリームのタイミングで読み取る、といったことができる。例: Update()
で発行されたメッセージをFixedUpdate()
で読み取るObservable.WhenAll<T>
複数のObservableがすべて終了状態になるのを待ち、完了時にその結果をまとめて通知する static
メソッドAmb<T>
複数のストリームのうち、もっとも早くメッセージが到達したストリームをひとつだけ採択する ストリームそのものを加工する
名前 効果・用途 備考 Delay<T>
指定した時間分、OnNextとOnCompletedメッセージを遅延させる OnErrorは素通しする DelayFrame<T>
指定したフレーム分、OnNextとOnCompletedメッセージを遅延させる OnErrorは素通しする DefaultIfEmpty<T>
1回もOnNextメッセージが入力されずにOnCompletedメッセージが入力された場合に、デフォルト値を発行する StartWith<T>
1. 購読された瞬間に指定の値のOnNextメッセージを一番最初に発行する StartWith<T>
2. 購読された瞬間に指定の関数を実行しその結果をOnNextメッセージとして一番最初に発行する 登録した関数は購読された瞬間に実行される StartWith<T>
3. 購読された瞬間に指定された複数の値を一番最初に発行する 指定された値はそれぞれ個別のOnNextメッセージとして発行される GroupBy<TSource, TKey>
1. OnNextメッセージを指定した条件でグループ分けを行い、それぞれに分割したストリームとして出力する 戻り値が IObservable<IGroupedObservable<TKey, TSource>>
型になるGroupBy<TSource, TKey, TElement>
2. OnNextメッセージを指定した条件でグループ分けを行い、それぞれに分割したストリームにした上でそれを変換してから出力する 戻り値が IObservable<IGroupedObservable<TKey, TElement>>
型になるRepeat<T>
OnCompletedメッセージが入力されると、自分より上流のストリームに対して再購読を実行する ストリームが完了したときに Subscribe()
を再実行してくれる。無限ループに注意RepeatSafe<T>
Repeat<T>
とほぼ同じ。ただし、OnCompletedメッセージが連続して入力されると処理を中断するRepeat<T>
を安全にしたものRepeatUntilDestroy<T>
指定した GameObject
かComponent
が破棄されるまでの間、Repeat<T>
として機能するRepeat<T>
を安全にしたものRepeatUntilDisable<T>
指定した GameObject
かComponent
がDisableされるまでの間、Repeat<T>
として機能するRepeat<T>
を安全にしたものSample<T>
1. OnNextメッセージを指定の時間間隔でサンプリングする 一定時間ごとに、そのときの最新値を出力する Sample<T>
2. OnNextメッセージを別のストリームの入力タイミングでサンプリングする 別ストリームから入力があるたびに、そのときの最新値を出力する SampleFrame<T>
OnNextメッセージを指定のフレーム間隔でサンプリングする Timeout<T>
1. OnNextメッセージの発行間隔が一定時間あいたらOnErrorメッセージを発行する Timeout<T>
2. 指定の時刻までにストリームが完了しなかったらOnErrorメッセージを発行する TimeoutFrame<T>
OnNextメッセージの発行間隔が一定フレーム数あいたらOnErrorメッセージを発行する Timestamp<T>
OnNextメッセージにそのメッセージが発行された時刻を付与する 戻り値は IObservable<Timestamped<T>>
TimeInterval<T>
OnNextメッセージが発行された時間間隔を計測し、それをメッセージに付与する 戻り値は IObservable<TimeInterval<T>>
FrameTimeInterval<T>
OnNextメッセージが発行された時間間隔を計測し、それをメッセージに付与する。ただし時間の計測にUnityの Time.time
またはTime.unscaledTime
を用いる時間の計測方法が違う以外は TimeInterval<T>
と同じFrameInterval<T>
直前のOnNextメッセージとの経過フレーム数をOnNextメッセージに付け加える Synchronize<T>
メッセージ処理に排他ロックを行う lock
に用いるオブジェクトを複数ストリームで共有することもできるエラーハンドリング
名前 効果・用途 備考 Catch<T, TException>
指定した型の例外を含むOnErrorメッセージがきた場合に登録した処理を実行し、任意のストリームに差し替える。型が一致しないOnErrorメッセージは無視する 引数の型は Func<TException, IObservable<T>> errorHandler
。TException
の型は必ず明示しないといけないObservable.Catch<T>
指定された複数のストリームを先頭から順番に購読し、OnNextメッセージをそのまま伝える。途中でOnErrorメッセージが発生すると次のストリームにスイッチする。OnCompletedメッセージが発行されたタイミングで終了する 失敗したら次へ、失敗したら次へ、成功するまで繰り返す CatchIgnore<T, TException>
指定した型の例外を含むOnErrorメッセージがきた場合に登録した処理を実行し、 Observable.Empty<T>
に差し替える。型が一致しないOnErrorメッセージは無視するエラーが起きたときにもみ消して終了する Retry<T>
OnErrorメッセージが発行されたときに、上流のストリームを再購読する 計何回まで試行するか指定できる OnErrorRetry<T>
1. OnErrorメッセージが発行されたときに、上流のストリームを再購読する 引数を何も指定しない場合。 Retry<T>
と同じ挙動をするOnErrorRetry<T, TException>
2. 指定した型の例外を含むOnErrorメッセージがきた場合に登録した処理を実行し、上流のストリームを再購読する Catch
+Retry
の複合OnErrorRetry<T, TException>
3. 指定した型の例外を含むOnErrorメッセージがきた場合に登録した処理を実行し、一定時間待ってから上流のストリームを再購読する Catch
+Retry
の複合だが、リトライするまでのディレイをつけられるFinally<T>
ストリームが解体されるタイミングで登録した関数を実行する ストリームの解体とは次に挙げるシチュエーションのこと。「OnCompletedメッセージが発行される」「OnErrorメッセージが発行される」「Disposeにより購読が中断される」「Subscribeで登録した関数の処理途中で例外が発生する」 Hot変換用
名前 効果・用途 備考 Multicast<T>
指定の ISubject
を用いてHot変換するPublish<T>
1. Subject<T>
を用いてHot変換するMulticast(new Subject<T>())
に相当Publish<T>
2. BehaviorSubject<T>
を用いてHot変換する初期値を設定した場合はこちらの挙動になる。 Multicast(new BehaviorSubject<T>())
に相当PublishLast<T>
AsyncSubject<T>
を用いてHot変換するMulticast(new AsyncSubject<T>())
に相当。OnErrorメッセージもキャッシュしてしまうのでエラーハンドリングに注意Replay<T>
ReplaySubject<T>
を用いてHot変換するMulticast(new ReplaySubject<T>())
に相当RefCount<T>
Hot変換時、自分を購読する Observer
がいる場合にストリームを稼働させる。Observer
がいなくなると自動で停止するIConnectableObservable<T>
に対してのみ利用可能Share<T>
Publish<T>().RefCount()
の省略記法Schedulerの切り替え
名前 効果・用途 備考 ObserveOn<T>
メッセージの実行コンテキストを指定のSchedulerに切り替える ObserveOnMainThread<T>
メッセージの実行コンテキストをUnityメインスレッドに切り替える MainThreadDispatchType
でメインスレッド上のどのタイミングにするかを指定できるSubscribeOn<T>
ストリームの初期構築を指定のSchedulerで行う あまり使うことはない SubscribeOnMainThread<T>
ストリームの初期構築をUnityメインスレッド上で行う DelaySubscription<T>
1. 指定された時間だけ待ってから Subscribe()
を実行する購読開始のタイミングをずらせる DelaySubscription<T>
2. 指定された時刻になったら Subscribe()
を実行する購読開始のタイミングを指定できる DelayFrameSubscription<T>
指定されたフレーム数だけ待ってから Subscribe()
を実行するUpdateやFixedUpdateのカウント数を指定できる その他
名前 効果・用途 備考 Do<T>
ストリームのメッセージを用いて副作用を起こす。メッセージそのものは加工しない ストリームの途中で、ストリーム外部の状態を変化させるときに使う DoOnError<T>
OnErrorメッセージを用いて副作用を起こす。メッセージそのものは加工しない OnErrorメッセージのみに反応 DoOnCompleted<T>
OnCompletedメッセージを用いて副作用を起こす。メッセージそのものは加工しない OnCompletedメッセージのみに反応 DoOnTerminate<T>
OnErrorまたはOnCompletedメッセージが発行されたとき副作用を起こす。メッセージそのものは加工しない OnErrorメッセージまたはOnCompletedメッセージに反応 DoOnSubscribe<T>
ストリームが Subscribe()
されたときに副作用を起こす購読された瞬間をログに出したりするとデバッグに便利 DoOnCancel<T>
ストリームが Dispose()
によってキャンセルされたときに副作用を起こすOnErrorメッセージやOnCompletedメッセージには反応しない ForEachAsync<T>
非同期処理の結果を伝搬しつつ、そのメッセージ消費を行う Do().Last().AsUnitObservable()
に挙動は似ている。AsyncReactiveCommandと組み合わせると非常に便利Materialize<T>
すべてのメッセージを OnNext(Notification<T>)
メッセージへと変換するOnErrorメッセージやOnCompletedメッセージをOnNextメッセージに格納してしまう。どんなメッセージが発行されたのかを列挙できるので、テストするときに便利 Dematerialize<T>
Materialize<T>
によって変換されたメッセージをもとに戻すAsObservable<T>
指定のオブジェクトのインタフェースを IObservable<T>
に制限するオブジェクトのダウンキャストやクロスキャストを防止できる ToArray<T>
発行されたOnNextメッセージをすべてキャッシュし、OnCompletedメッセージが入力されたタイミングで1つの OnNext(T[])
メッセージに変換して出力するToList<T>
発行されたOnNextメッセージをすべてキャッシュし、OnCompletedメッセージが入力されたタイミングで1つの OnNext(IList<T>)
メッセージに変換して出力するWait<T>
ストリームが完了するのをスレッドをブロックして待機する メインスレッドで使うとフリーズするので常用禁止
- 投稿日:2019-12-23T18:27:55+09:00
【用Unity开发Oculus Quest用的APP】 第②回:抓住物体吧
◆写在开头
这次是第②回、
手的表示,与可抓住物体的制作。第①回的内容请点这里
第①回:导入APP、进行运行状态确认(还有日文版哦)
PS:因为我的制作环境是日语环境,所以下面的软件截图,都是日文版的截图。
表示内容不同,但菜单的位置,作用等都是一样的,
根据说明和参考截图应该能找到相对应的功能的所在位置。◆ 开发环境
macOS Mojave 版本 10.14.6
Unity 2018.4.12f1
Android SDK◆ 制作顺序
- 1. 在画面中表示手
- 1_1. 导入LocalAvatarWithGrab
- 1_2. Ovr Avatar 的设定
- 1_3. 发行与设置OculusAPP用ID
- 2. 追加一个可以被抓住的物体
- 3. 运行确认
1.在画面中表示手
1_1. 导入LocalAvatarWithGrab
新建一个Scene
具体的制作方法请参考上回的内容第①回:导入APP、进行运行状态确认想要抓住物体,首先要有抓住物体用的手。
这次依旧使用上一回导入的Oculus Integration中的功能,
在里面找到LocalAvatarwithGrab,它是搭载了抓住物体功能的Prefab
「LocalAvatarWithGrab」在
「Assets」→「Oculus」→「SampleFramework」→「Core」→「AvatarGrab」→「Prefabs」里面可以找到。
文件夹的层次比较深、推荐使用Project上方的搜索栏,直接搜索「LocalAvatarWithGrab」。
找到后,将它拖入Hierarchy后自动追加完毕。
确认追加完成后、点开「Inspector」→「Transform」完成一些初始设定。
这里的设定基本是自由的,这次只把「Scale」调整成1.7便可。
设置调整完成后、将上一回制作的「OVRCameraRig」删除掉。
因为「LocalAvatarWithGrab」里面已经存在「OVRCameraRig」了,所以之前的就不需要了。1_2. Ovr Avatar 的设定
接着,点击刚才追加的「LocalAvatarWithGrab」。
点击「Inspector」→「Ovr Avatar」→「Shaders 」调整设定。
点击「Controller Shader」右边的小圆圈,
会自动弹出「Shader」的设置画面。
直接在搜索栏搜索「AvatarPBRV2Simple」
找到文件名对得上的「Shader」后点击,自动设置完成。
设置完成后、「Shader」会变成如图所示的状态。1_3. 发行与设置OculusAPP用ID
为了能够表示追加完成的手、需要OculusAPP用ID「Create android manifest」。
AppID需要访问Oculus主页,申请后获得。
这里就不一一详细说明了。
ID发行后、回到Unity。
选择菜单的「Oculus] →「Avatars」→「Edit Setting」
弹出「OvrAvatarSettings」窗口。将刚才发行的ID设置好。
最后,为了手能够正确显示,需要设置「Create android manifest」。
它的设置方法非常简单。
点击「Oculus」→「Tools」→「Create store-compatible AndroidManifest.xml」即可完成。2. 追加一个可以被抓住的物体
在「Hierarchy」中右键,选择「3D」→「Object」→「Cube」后,画面中会自动追加一个方块。
方块尺寸可自由调整。
选择刚刚追加的「Cube」
在「Inspector」中选择「Add Component」找到「Rigidbody」和「OVR Grabbable 」追加。
「Box Collider」在生成方块的时候应该自动生成了、但如果没有的话,需要用上面同样的方式追加进去。
这样方块的设置就完成了。3. 运行确认
导入APP之前、推荐先在Unity上运行一次、确认一下方块是否会自然落下。
如果没有问题,那么就把APP导入Oculus,实际确认以下是否运行正常吧。
- 投稿日:2019-12-23T17:57:06+09:00
【UnityでOculus Quest向けのアプリを作る】 第②回:物を掴む為には
◆はじめに
第②回ですね、
手の表示と物を掴む方法をやります。第①回は↓に参照してください。
第①回:APPをビルドして、動作確認する(日本語と中国版あります)
◆ 開発環境
macOS Mojave バージョン 10.14.6
Unity 2018.4.12f1
Android SDK◆ 手順
- 1. 手を表示する
- 1_1. LocalAvatarWithGrab導入
- 1_2. Ovr Avatar の設定
- 1_3. OculusのアプリのID発行と設置
- 2. 掴む物体の追加と設定
- 3. 動作確認
1.手を表示する
1_1. LocalAvatarWithGrab導入
新規シーンを作ります。
具体的な制作方法は前回の第①回:APPをビルドして、動作確認するを参照してください。物を掴む為には、まず手を表示する必要が有ります。
前回導入した、Oculus Integrationの中には、物を掴む機能を搭載しているLocalAvatarwithGrab というプレハブが存在しています。
今回は、それを使います。
「Assets」→「Oculus」→「SampleFramework」→「Core」→「AvatarGrab」→「Prefabs」
の中に「LocalAvatarWithGrab」あります。
階層は深い為、Projectの検索欄で「LocalAvatarWithGrab」と検索を掛けたら探しやすいです。
これをHierarchyにドラックし、追加します。
追加したら、「Inspector」→「Transform」でポジションなどを設定します。
今回は、 「Scale」は1.7、他は全部0にします。
設置完了後、前回作った「OVRCameraRig」を削除します。
「LocalAvatarWithGrab」の中にも「OVRCameraRig」が存在している為、不要です。1_2. Ovr Avatar の設定
続いて、追加した「LocalAvatarWithGrab」クリックし、
「Inspector」→「Ovr Avatar」→「Shaders 」の設定を調整します。
「Controller Shader」右側の丸をクリック。
「Shader」を設定する画面に遷移するはずです。
検索欄で「AvatarPBRV2Simple」検索し、
ファイル名が合っている「Shader」をダブルクリックし、設定する事ができます。
設定したら、「Shader」はこのような状態になります。1_3. OculusのアプリのID発行と設置
追加した手を表示するためには、OculusのアプリのIDと「Create android manifest 」が必要です。
アプリのIDは、
Oculus ホームページのダッシュボードにアクセスし、
「新しいアプリを作成」からアプリを登録して、発行出来ます。
Oculus Developer Dashboard
詳しいID発行方法は、ここでは割愛します。
ID発行後、Unityに戻ります。
メニューから「Oculus] →「Avatars」→「Edit Setting」をクリックすると、
「OvrAvatarSettings」が表示されます。先程発行したアプリIDを設定して、これでID設定は完了になります。
最後、手を正しく表示するために「Create android manifest」を設置する必要があります。
設置方法は非常に簡単、
「Oculus」→「Tools」→「Create store-compatible AndroidManifest.xml」をクリックするだけです。2. 掴む物体の追加と設定
掴む用のオブジェクトを追加します。
「Hierarchy」で右クリック「3D」→「Object」→「Cube」を追加します。
サイズは自由です。
追加した「Cube」を選択します。
「Inspector」→「Add Component」で「Rigidbody」と「OVR Grabbable 」を検索し、追加します。
「Box Collider」は最初から存在するはずですが、なければ「Rigidbody」と同じ方法で追加します。
これで、設置完了です。3. 動作確認
ビルドする前に、まずUnity上で一回実行して、「Cube」落下するかどうか確認します。
問題無ければ、ビルドして、実機で動作確認しましょう。
- 投稿日:2019-12-23T17:50:44+09:00
UniRxとUniTask 相互変換の変わったパターン紹介
はじめに
UniRxの
Observable
と、UniTask
はそれぞれ相互変換することができます。
今回はその中でも少し変わった変換パターンを紹介したいと思います。※
System.Linq
UniRx
UniRx.Async
のusingを忘れずに
IEnumerable<UniTask<T>>
->IObservable<T>
複数の
UniTask<T>
をまとめて、1つのIObservable<T>
にする方法です。
やり方が何パターンかあります。// 対象 IEnumerable<UniTask<string>> tasks = CreateSample(); // --- // 並列にまとめる(要素の順序を無視して、終わったものから結果を返す) IObservable<string> parallel = tasks .Select(x => x.ToObservable()) // IE<IO<T>> .Merge(); // IO<T> // 直列にまとめる(要素の先頭から順番に結果を返す) IObservable<string> sequential = tasks .Select(x => x.ToObservable()) // IE<IO<T>> .Concat(); // IO<T> // 全部終わってからまとめて結果をとるなら(IO<IE<T>>) IObservable<IList<string>> whenAll = tasks .Select(x => x.ToObservable()) // IE<IO<T>> .Zip(); // IO<IList<T>> // でも、全部まとめてとるならUniTask.WhenAllでいいのでは? UniTask<string[]> whenAll2 = UniTask.WhenAll(tasks);
UniTask<IEnumerable<T>>
->IObservable<T>
さっきとネストの仕方が逆のパターン。
UniTask<IE<T>>
を分解して1つのIObservable<T>
にする方法です。
2パターンあるけど結果は同じです。// 対象 UniTask<IEnumerable<string>> task = CreateSample(); // パターン1 IObservable<string> p1 = task .ToObservable() // IO<IE<T>> .Select(x => x.ToObservable()) // IO<IO<T>> .Merge(); // IO<T> // パターン2 IObservable<string> p2 = task .ToObservable() // IO<IE<T>> .SelectMany(x => x.ToObservable()); // IO<T>
IObservable<T>
->UniTask<IEnumerable<T>>
IObservable<T>
が発行するすべてのメッセージを「まとめて」待ち受けるUniTask<T>
を作りたい場合。// 対象 IObservable<string> observable = CreateSample(); // ToArray()してからToUniTask()でOK UniTask<string[]> task = observable.ToArray().ToUniTask();
IObservable<UniTask<T>>
->IObservable<T>
Observable
がUniTask
を扱う場合に、それを1つのObservable
にまとめる。// 対象 IObservable<UniTask<string>> observable = CreateSample(); // 並列(終わった順に結果を出すなら) IObservable<string> parallel = observable.SelectMany(x => x.ToObservable()); // 直列(もとのIO<T>から発行された順序を維持するなら) IObservable<string> sequential = observable.Select(x => x.ToObservable()).Concat();
IObservable<UniTask<T>>
->UniTask<IEnumerable<T>>
Observable
がUniTask
を扱う場合に、それをUniTask
側にまとめる。IObservable<UniTask<string>> observable = CreateSample(); // 結果は先に終わったUniTaskの順番になる UniTask<string[]> task = observable .SelectMany(x => x.ToObservable()) // IO<IO<T>> .Merge() // IO<T> .ToArray() // IO<T[]> .ToUniTask(); // UniTask<T[]>
UniTask<IObservable<T>>
->IObservable<T>
UniTask
の中にObservable
が入り込んじゃった場合。// 対象 UniTask<IObservable<string>> task = CreateSample(); // taskをIO<IO<T>>に変換してからMerge() IObservable<string> observable = task.ToObservable().Merge();
UniTask<IObservable<T>>
->UniTask<IEnumerable<T>>
UniTask
の中にObservable
が入り込んじゃったものを、今度はUniTask
側にまとめる場合。// async/await使っちゃうのが楽 private async UniTask<IEnumerable<string>> Unwrap(UniTask<IObservable<string>> task) { var observable = await task; return await observable.ToArray(); }
IObservable<IObservable<T>>
->UniTask<IEnumerable<T>>
こんなシチュエーションあるのかよくわからないけど。
IObservable<IObservable<string>> observable = CreateSample(); UniTask<string[]> task = observable .SelectMany(x => x) // IO<T> .ToArray() // IO<T[]> .ToUniTask(); // UniTask<T[]>まとめ
UniRx
とUniTask
はだいたいどんなパターンでもそれぞれに変換することができます。
両者を組み合わせて使い、必要に応じて変換をかけるとよいでしょう。
- 投稿日:2019-12-23T17:19:31+09:00
ゲーム製作の感想
通っている専門学校でチームでのunityを使ったゲーム制作があり、自分の至らぬ点も見つかったので一人反省会です
私の担当はゲームシーンが切り替わるときのエフェクト作成が主でしたフェードインとフェードアウトのプログラムはTAMA-LABさんのスクリプトを参考にしました
→Unityでフェードイン/フェードアウトを実現する方法(TAMA-LABさん)
Fade_controler.csusing System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; using UnityEngine.SceneManagement; public class Fade_controler : MonoBehaviour { float fadeSpeed = 0.01f; //透明度が変わるスピードを管理 float red, green, blue, alfa; //パネルの色、不透明度を管理 private bool isFadeOut = false; //フェードアウト処理の開始、完了を管理するフラグ private bool isFadeIn = false; //フェードイン処理の開始、完了を管理するフラグ Image fadeImage; //透明度を変更するパネルのイメージ void Start() { isFadeIn = true; fadeImage = GetComponent<Image>(); red = fadeImage.color.r; green = fadeImage.color.g; blue = fadeImage.color.b; alfa = fadeImage.color.a; } void Update() { if (Input.GetMouseButtonDown(0)&&isFadeIn==false) { isFadeOut = true; } if (isFadeIn==true) { StartFadeIn(); } if (isFadeOut==true) { StartFadeOut(); } } void StartFadeIn() { Debug.Log("1"); alfa -= fadeSpeed; //a)不透明度を徐々に下げる SetAlpha(); //b)変更した不透明度パネルに反映する if (alfa <= 0) { //c)完全に透明になったら処理を抜ける isFadeIn = false; fadeImage.enabled = false; //d)パネルの表示をオフにする } } void StartFadeOut() { Debug.Log("2"); fadeImage.enabled = true; // a)パネルの表示をオンにする alfa += fadeSpeed; // b)不透明度を徐々にあげる SetAlpha(); // c)変更した透明度をパネルに反映する if (alfa >= 1) { // d)完全に不透明になったら処理を抜ける isFadeOut = false; SceneManager.LoadScene("Traning"); } } void SetAlpha() { fadeImage.color = new Color(red, green, blue, alfa); } }今回の反省点は、この親切でわかりやすいプログラムを自分で理解して扱うまでに一週間かけてしまったことです
理由を考えてみるとフラグを使い慣れておらず使い方を思い出せなかったことや、何がわからないかを整頓できなかったことから、日ごろからプログラムを学ぼうとする意欲が足りなかったことが原因だと考えつきました
これをもとに、どんなゲームを作るか考えることだけでなく、今まで考えたゲームを片っ端から作ることでもっとプログラムに親しもうと思いました
あとゆにてぃがおもったとりにうごいてくれたときはすごくたのしかったしうれしかったです。
- 投稿日:2019-12-23T17:18:02+09:00
unityのParticle Systemを使って魔法っぽいエフェクトを作る
制作理由
学校でグループ制作中に戦闘時に攻撃する際にエフェクトがあったほうが戦闘時に単調にならず、またプレイヤーを楽しませる要素の一つになると考えたので制作しました。
Particle Systemを作成する
製作途中のゲームのものをそのまま使用しているため実際には必要のないものが混じっています
まずはHierarchy内で右クリックをしてEffects > Particle Systemで作成します。
今回は攻撃魔法用の爆発エフェクトを例に作成していきます。
パーティクルのカスタマイズ
作成した段階では白い球のようなものが上の方向に拡散しながら上がっていくだけなので、まずは爆発っぽくなるように中心から外側に広がっていくように設定します。
InspectorからShapeを選択し、ConeからSphereに変更します。
これだけだとまだ爆発っぽくないのでもっと爆発に近いエフェクトになるようにしていきましょう。
まずはEmissionを下のように設定します。
これで一度に出現するParticleの数が増加しました。
続けてLimit Velocity over LifetimeとSize over Lifetimeを次のように設定してください。
これで爆発のようなエフェクトにはなりますが少しParticleが残る時間が長いのでもう少し短くしましょう。
DurationとStart Lifetimeの値を次のように設定してください。
最後にRendererのMaterialからお好みのMaterialを選択すれば完成です。
↑こんな感じになります今回はStandard AssetsのParticleFireballを使用しましたが、ほかに自分が気に入ったものがあればそちらをお使いください。
最後に
今回が初めての投稿になります。こうしたほうがもっと見やすくなる、こっちのほうがもっと爆発っぽいエフェクトになるなどのアドバイスがありましたら是非教えていただきたいです。よろしくお願いいたします。
- 投稿日:2019-12-23T16:30:41+09:00
3Dマークアップ言語でWebサービス的なやつを作ってみる
目的
前回までの、3Dマークアップ言語を動的に出力するWebシステムを作る。
道具立て
比較的慣れてるので、PythonでFlaskを使ってサクッと作ってみる。
時計を作る
データベースとかアクセスしたりAPI使うのも大がかりなので、PoCとして最低限として、アクセス時刻の表示をする時計を作ってみる。
以下、時分秒に対応する球を表示するプログラム。時刻の経過に合わせて動くわけではない。app.pyfrom flask import Flask import datetime import math app = Flask(__name__) @app.route('/') def clock(): dt_now = datetime.datetime.now() hour = dt_now.hour % 12 minute = dt_now.minute sec = dt_now.second yh = 0.2 * math.cos(hour * 2 * 3.141592 / 12) xh = 0.2 * math.sin(hour * 2 * 3.141592 / 12) ym = 0.4 * math.cos(minute * 2 * 3.141592 / 60) xm = 0.4 * math.sin(minute * 2 * 3.141592 / 60) ys = 0.35 * math.cos(sec * 2 * 3.141592 / 60) xs = 0.35 * math.sin(sec * 2 * 3.141592 / 60) homl = '''<homl><head><title>CLOCK</title></head> <body><a-scene wx=0.2 wy=0.2 wz=0.2> <a-sphere r=0.05 x=0 y=0 z=0 color=white /> <a-sphere r=0.05 x={xh} y={yh} z=0 color=red /> <a-sphere r=0.03 x={xm} y={ym} z=0 color=green /> <a-sphere r=0.01 x={xs} y={ys} z=0 color=blue /> </a-scene></body></homl> '''.format(xh=xh,yh=yh,xm=xm,ym=ym,xs=xs,ys=ys) return homl if __name__ == '__main__': app.run()結果
それっぽくなった。
これで、インターネットの世界とXRの世界が簡単につながるようになった。
- 投稿日:2019-12-23T15:29:13+09:00
LuaエンジンをUnityに組み込んでみる
目的
HTMLに対応するJavaScriptのように、前回の3Dマークアップ言語にスクリプティングシステムとしてLuaを組み込んでみたい。
そのためのとっかかりとして、UnityにLuaエンジンを組み込み、GameObjectをLuaから操作してみる。道具立て
組み込むLuaエンジンとして、NLuaを使ってみた。
nugetなどで導入する。Unityプロジェクトとプログラム
Unityのヒエラルキーはこんな感じ。
プログラムは、まず、以下のコードを関係する全オブジェクト(Root,Child1,Cube)にアタッチしておく。これを介してLuaからGameObjectを操作する。
NLuaTest.csusing System.Collections; using System.Collections.Generic; using UnityEngine; namespace NLuaTestNS { public class NLuaTest : MonoBehaviour { public NLuaTest getChild(int i) { return gameObject.transform.GetChild(i).gameObject.GetComponent<NLuaTest>(); } public void setColor(int r, int g, int b, int a) { gameObject.GetComponent<Renderer>().material.color = new Color((float)r/255f, (float)g/255f, (float)b/255f, (float)a/255f); } public void setPosition(float x, float y, float z) { gameObject.transform.localPosition = new Vector3(x/10f,y/10f,z/10f); } public void setRotation(float x, float y, float z) { gameObject.transform.localRotation = Quaternion.Euler(x,y,z); } } }そして、以下のコードをRootにアタッチして実行。
NLuaDo.csusing System.Collections; using System.Collections.Generic; using UnityEngine; using NLuaTestNS; using NLua; public class NLuaDo : MonoBehaviour { private Lua state; private int color; // Start is called before the first frame update void Start() { state = new Lua(); state["root"] = GetComponent<NLuaTest>(); state.DoString("root:getChild(0):getChild(0):setColor(0,0,0,255)"); } void Update() { color += 1; color %= 256; state.DoString("root:getChild(0):getChild(0):setColor("+color+","+color+","+color+",255)"); state.DoString("root:getChild(0):getChild(0):setPosition(" + color + "," + color + "," + color + ")"); state.DoString("root:getChild(0):getChild(0):setRotation(" + color + "," + color + "," + color + ")"); } }色を変えるのと、適当なアニメーションをつけてみた。
結果
素直に動いてくれた。
- 投稿日:2019-12-23T14:33:18+09:00
unityちゃんを透明にする方法
製作理由
学校のグループ製作中に、敵であるunityちゃんの姿を消したいなと思い、このプログラムを書きました。
unityちゃんのメッシュ情報を取得する
製作中のゲームからそのまま持ってきたので余計な物も入ってます
まずunitychanの中にあるmesh_rootを取得します
画像参照
このmesh_rootをつけたり消したりすると以下のようになります
これらを用いて簡単に付いたり消えたりするものを作りました
sample.csGameObject mesh; private void start { float elapsedTime = 0; mesh = GameObject.Find("mesh_root"); } private void update { elapsedTime+=Time.deltaTime; if(elapsedTime > 2f) mesh.SetActive(true); else mesh.SetActive(false); if(elapsedTime > 4f) elapsedTime = 0f; }これでunityちゃんが見えたり消えたりするようにできました。
最後に
もっと簡単にできるよ!等のアドバイス等ありましたら
是非教えていただきたいです!よろしくお願いします!
- 投稿日:2019-12-23T14:32:23+09:00
HTML Agility Packを使ってUnityで使える3Dマークアップ言語を実装してみる
目的
3Dオブジェクトを記述する3Dマークアップ言語をUnityで実装してみる。
マークアップ言語?
ざっくりHTMLみたいな、A-Frameに近いものを作ってみます。
道具立て
HTMLライクなマークアップ言語のパースに、HTML Agility Packを使います。
NuGetで入れることもできますし、サイトからnupkgをダウンロードして7zipなどで展開して、DLLだけ使うということもできます。
.Net 4.5用だったり、.Net Standard 2.0用だったり、いくつか種類があるので、自分のプロジェクトに合ったものを使います。
UnityのDLLのImportセッティングのところで、適切なプラットフォームに対して、適切なDLLが使われるように設定しましょう。実際のプログラム
以下のプログラムがパーサーのエントリー部分になります。
HomlParser.csusing System.Collections; using System.Collections.Generic; using System.Runtime; using UnityEngine; using HtmlAgilityPack; using Homl.DOM; using System.Reflection; namespace Homl.Parser { public class HomlParser { public static Dictionary<string, ParseNode> nodeTemplates; public HomlParser() { if(nodeTemplates == null) { nodeTemplates = new Dictionary<string, ParseNode>(); nodeTemplates.Add("a-scene", new Body()); nodeTemplates.Add("a-box", new Cube()); nodeTemplates.Add("a-cylinder", new Cylinder()); nodeTemplates.Add("a-sphere", new Sphere()); nodeTemplates.Add("a-link", new ATag()); nodeTemplates.Add("a-text", new Text()); nodeTemplates.Add("homl", new Homl()); } } public Document Parse(string homl) { var htmlDoc = new HtmlAgilityPack.HtmlDocument(); htmlDoc.LoadHtml(homl); GameObject documentGO = new GameObject("Document"); Document document = documentGO.AddComponent<Document>(); BoxCollider bc = documentGO.AddComponent<BoxCollider>(); walkinChild(htmlDoc.DocumentNode.ChildNodes, documentGO); bc.size = new Vector3(0.3f, 0.3f, 0.3f); return document; } private void walkinChild(HtmlAgilityPack.HtmlNodeCollection nodes, GameObject parent) { foreach (HtmlAgilityPack.HtmlNode node in nodes) { GameObject nodeObject = null; Debug.Log(node.Name); if (nodeTemplates.ContainsKey(node.Name)) { nodeObject = nodeTemplates[node.Name].parse(node, parent); } else { nodeObject = parent; } if (node.HasChildNodes) { walkinChild(node.ChildNodes, nodeObject); } } } } }再帰的にマークアップの構造をたどり、タグ名の辞書から該当タグをパースするオブジェクトを探しながらUnityのGameObjectを作成していきます。
各タグのパーサーは、例えばCubeに対するパーサーが以下のようになっています。
Cube.csusing System.Collections; using System.Collections.Generic; using UnityEngine; using HtmlAgilityPack; namespace Homl.Parser { public class Cube : ParseNode { public override GameObject parse(HtmlNode homlNode, GameObject parent) { PrimitiveProducer pp = GameObject.Find("PrimitiveProducer").GetComponent<PrimitiveProducer>(); GameObject cube = GameObject.Instantiate(pp.getCube()); float x = float.Parse(homlNode.GetAttributeValue("x", "0.0f")); float y = float.Parse(homlNode.GetAttributeValue("y", "0.0f")); float z = float.Parse(homlNode.GetAttributeValue("z", "0.0f")); float s = float.Parse(homlNode.GetAttributeValue("size", "0.1f")); string color = homlNode.GetAttributeValue("color", "white"); cube.transform.parent = parent.transform; cube.transform.Translate(new Vector3(x, y, z)); cube.transform.localScale = new Vector3(1.0f, 1.0f, 1.0f); Material mat = cube.GetComponent<Renderer>().material; mat.color = ColorManager.Instance.getColor(color); mat.SetColor("_BaseColor", ColorManager.Instance.getColor(color)); Vector3 scale = new Vector3(s, s, s); Vector3[] verts = cube.GetComponent<MeshFilter>().mesh.vertices; Vector3[] newverts = new Vector3[verts.Length]; int i = 0; foreach (Vector3 v in verts) { v.Scale(scale); newverts[i] = v; i++; } cube.GetComponent<MeshFilter>().mesh.vertices = newverts; BoxCollider bc = cube.GetComponent<BoxCollider>(); bc.size = scale; DOM.Cube cb = cube.AddComponent<DOM.Cube>(); return cube; } } }PrimitiveProducerは、プリミティブの3DオブジェクトPrefabの参照を管理しているオブジェクトです。
Attributeの指定の仕方などは、本家のA-Frameと異なるところなので、コンポーネント志向の構造と合わせて抜本的な見直しは必要ですが、
これに加えて、DOMのクラス群を合わせて動かすことで、3DオブジェクトをマークアップからUnity内に生成して、リンクなどの仕組みを作ることもできるようになりました。表示結果
もとになったマークアップ
天気予報の雪だるまのつもり
<homl> <head> <title>Weather</title> </head> <body> <a-scene wx=0.2 wy=0.001 wz=0.2> <a-cylinder height=0.02 r=0.02 x=0 y=0.18 z=0 color=red /> <a-sphere r=0.1 x=0 y=0.05 z=0 color=white /> <a-sphere r=0.08 x=0 y=0.14 z=0 color=white /> </a-scene> </body> </homl>機能テスト用
<homl> <head> <title>TestHoloML</title> </head> <body> <a-scene wx=0.2 wy=0.2 wz=0.2> <a-box size=0.1 x=0 y=0 z=0 color=blue> <a-cylinder height=0.05 r=0.02 x=0.1 y=0.1 z=0.1 color=red /> <a-sphere r=0.05 x=-0.1 y=-0.1 z=-0.1 color=green /> <a-text size=0.1 x=0 y=0 z=0 color=blue>ABCDEF</text> </a-box> <a-link href=”test.homl”> <a-box size=0.1 x=0.1 y=-0.1 z=0.1 color=white /> </a-link> </a-scene> </body> </homl>3Dグラフ
<homl> <head> <title>3D Graph</title> </head> <body> <a-scene wx=0.5 wy=0.001 wz=0.2> <a-cylinder height=0.2 r=0.05 x=-0.2 y=0.2 z=0 color=red /> <a-cylinder height=0.12 r=0.05 x=-0.1 y=0.12 z=0 color=red /> <a-cylinder height=0.14 r=0.05 x=0 y=0.14 z=0 color=red /> <a-cylinder height=0.08 r=0.05 x=0.1 y=0.08 z=0 color=red /> <a-cylinder height=0.05 r=0.05 x=0.2 y=0.05 z=0 color=red /> </a-scene> </body> </homl>現状ではプリミティブオブジェクトの組み合わせだけなのですが、このように表示が可能になりました。
- 投稿日:2019-12-23T14:07:51+09:00
30分で作る初めてのUnity
はじめに
- ZEALS Advent Calender 23日目の記事です。
普段はWeb周りの技術を扱ってばかりなので、
今回は以前から興味があったUnityを触ってみました。この記事は、
なんでもいいから一度Unityを使ってプロジェクトを作ってみたい。
という方向けに、プロジェクトの作成からBuildまでの流れを掴んでもらう為に書いたものです。
ですので、数あるUnity機能の中からほんの数個ピックアップして追加したのみにすぎません。
ご了承ください。目次
- イントール
- プロジェクトの作成
- レイアウトの変更
- オブジェクトの作成
- オブジェクトに装飾
- オブジェクトに動きを与える
- プロジェクトをBuild
1. イントール
まずはUnityをダウンロードしましょう。
個人開発であれば無料です。
→ ダウンロード画面へ
(*途中のアカウント作成等は割愛させていただきます。)2. プロジェクトの作成
インストールしたApplicationを開くと以下の画面が現れるので、
New
を選択します。
Template
を3D
にしてプロジェクトを作成します。
3. レイアウトの変更
こちらがプロジェクトのデフォルト画面です。
自分の作業しやすいレイアウトに変更してください。
私は2by3
にしています。4. オブジェクトを作成
ゲームオブジェクト(以下
オブジェクト
)を作成します。
Hierarchy
→Create
→3Dオブジェクト
→Cube
の順で選択してください。すると画面左の
Scene
にCube
が現れます。オブジェクトの形を変えるために画面右上の
Inspector
を下記のように編集します。この調子で
Square
も追加してみます。5. オブジェクトに装飾
Cube
とSquare
に色を与えましょう
Project
→Materials
を選択画面を見やすくするために 右上設定(三本の棒線)から
One Column Layout
を選択します。
Material
内のスポイトのようなアイコンから色を選択します。この調子でもう1色作成します。
そして、今作成した
Material
は対象のオブジェクトにドラッグ&ドロップできます。
(Scene
,Hierarchy
どちらでも可)6. オブジェクトに動きを加える
次は
Square
に動きをつけられるようにします。まず、
Square
のInspector
からRigidBody
(オブジェクトに重力を付与)を追加します。次に、
Add Component
を選択します。
最下部にあるNew Scrip
を選択、名前を決めてScriptを作成します。作成されたScriptをダブルクリックするとEditorが開くはずです。
以下、カーソルを押した方向に
Square
を動かすようにするScriptです。using System.Collections; using System.Collections.Generic; using UnityEngine; public class moveScriot : MonoBehaviour { Rigidbody rigidbody; // Start is called before the first frame update void Start() { rigidbody = GetComponent<Rigidbody>(); } // Update is called once per frame void Update() { float moveH = Input.GetAxis("Horizontal"); float moveV = Input.GetAxis("Vertical"); Vector3 move = new Vector3(moveH, 0, moveV); rigidbody.AddForce(move); } }これで
Square
が動くようになったと思います。画面左下に見えている
Game
が実際にユーザーがゲームを遊ぶときの視点です。
わかりやすくするために、Main Camera
のTransForm
を調整してください。7. プロジェクトをBuildする
最後にこのプロジェクトを
Build
してみましょう。
File
→Build Settings
を選択こちらでプラットフォームを選択することができます。
今回はMacOS
のまま進むのでPlatform
はそのままで問題ありません。
右上のAdd Open Souce
から現在のScenes
を追加して、
最後にBuild
を選択してください。
デスクトップに作成したゲームが出現します。
これでいつでも先程のGameを開くことができるようになりました。
- 投稿日:2019-12-23T11:56:45+09:00
OculusQuest ハンドトラッキングSDKから、指Boneの情報を取得し分析する
最近のOculusQuestさんちょっと攻めすぎですよね。
OculusLinkのβが出たのもつい最近な気がしているのに、さらにハンドトラッキングまで!!!そして、2019/12/20にはUnityのAssetStoreにハンドトラッキング対応のSDKが配布されました。 高速感!
導入方法なんかについては他の方の記事にお任せして・・・。
肝はOVRSkelton
OVRHand には
GetFingerIsPinching()
とGetFingerPinchStrength()
があり、それぞれ、ピンチ(つまんでいる)状態かどうかと
つまんでいる力(距離?)が取得できるメソッドが入っています。
参考:OculusQuestのハンドトラッキングについて色々調べてみたしかし、ぱっと見たところそれ以外の情報取得がそんなにありません。
そこで上記記事でも言及していますが、もっと詳しい情報を知りたい場合は
OVRSkelton.Bones
にボーン情報が入っている のでそれを使うことにします。というのも
僕がやりたかったのは以前作った 「空中に図形を描いて物体を生成するアプリを作ってみた」 のハンドトラッキング版だったので、空中に絵を描くイメージです。
みなさんが空中に絵を描く(例えば、誰かに「**って漢字どう書くんだったっけ?」と聞かれた)時に指をどんな形にするかっていうと十中八九←これですよね
この「人差し指だけを伸ばして、他曲げている」を
GetFingerIsPinching()
やGetFingerPinchStrength()
でやるのはちょっと無理があります。(できなくはないと思いますが)そこで、
OVRSkelton.Bones
から情報を抜き出して簡単に分析することにしました。Bone情報取得
OVRSkelton.Bones
は IList(Readonly)で、何番目にどの情報が入っているかは以下のようにenumで宣言されています。public enum BoneId { Invalid = OVRPlugin.BoneId.Invalid, Hand_Start = OVRPlugin.BoneId.Hand_Start, Hand_WristRoot = OVRPlugin.BoneId.Hand_WristRoot, // root frame of the hand, where the wrist is located Hand_ForearmStub = OVRPlugin.BoneId.Hand_ForearmStub, // frame for user's forearm Hand_Thumb0 = OVRPlugin.BoneId.Hand_Thumb0, // thumb trapezium bone Hand_Thumb1 = OVRPlugin.BoneId.Hand_Thumb1, // thumb metacarpal bone Hand_Thumb2 = OVRPlugin.BoneId.Hand_Thumb2, // thumb proximal phalange bone Hand_Thumb3 = OVRPlugin.BoneId.Hand_Thumb3, // thumb distal phalange bone Hand_Index1 = OVRPlugin.BoneId.Hand_Index1, // index proximal phalange bone Hand_Index2 = OVRPlugin.BoneId.Hand_Index2, // index intermediate phalange bone Hand_Index3 = OVRPlugin.BoneId.Hand_Index3, // index distal phalange bone Hand_Middle1 = OVRPlugin.BoneId.Hand_Middle1, // middle proximal phalange bone Hand_Middle2 = OVRPlugin.BoneId.Hand_Middle2, // middle intermediate phalange bone Hand_Middle3 = OVRPlugin.BoneId.Hand_Middle3, // middle distal phalange bone Hand_Ring1 = OVRPlugin.BoneId.Hand_Ring1, // ring proximal phalange bone Hand_Ring2 = OVRPlugin.BoneId.Hand_Ring2, // ring intermediate phalange bone Hand_Ring3 = OVRPlugin.BoneId.Hand_Ring3, // ring distal phalange bone Hand_Pinky0 = OVRPlugin.BoneId.Hand_Pinky0, // pinky metacarpal bone Hand_Pinky1 = OVRPlugin.BoneId.Hand_Pinky1, // pinky proximal phalange bone Hand_Pinky2 = OVRPlugin.BoneId.Hand_Pinky2, // pinky intermediate phalange bone Hand_Pinky3 = OVRPlugin.BoneId.Hand_Pinky3, // pinky distal phalange bone Hand_MaxSkinnable = OVRPlugin.BoneId.Hand_MaxSkinnable, // Bone tips are position only. They are not used for skinning but are useful for hit-testing. // NOTE: Hand_ThumbTip == Hand_MaxSkinnable since the extended tips need to be contiguous Hand_ThumbTip = OVRPlugin.BoneId.Hand_ThumbTip, // tip of the thumb Hand_IndexTip = OVRPlugin.BoneId.Hand_IndexTip, // tip of the index finger Hand_MiddleTip = OVRPlugin.BoneId.Hand_MiddleTip, // tip of the middle finger Hand_RingTip = OVRPlugin.BoneId.Hand_RingTip, // tip of the ring finger Hand_PinkyTip = OVRPlugin.BoneId.Hand_PinkyTip, // tip of the pinky Hand_End = OVRPlugin.BoneId.Hand_End, // add new bones here Max = OVRPlugin.BoneId.Max }親指:Thumb
人差し指:Index
中指:Middle
薬指:Ring
小指:Pinkyで、1,2,3 は関節を表しており、数字が大きくなるほど指先に近づいていく ようです。
僕は英語をロクに読まず 「第一関節」= 1 だと思い込んで処理を書いていたらさっぱり正しい値が返ってこなくてハテ? となりました。ご注意を・・・。
そして、Tip
とついているのは指先です。なお、Pinky と Thumb だけ 0番があります。
そして、この Bone には Transform が入っており、位置や回転が取れそうです。
例えば「人差し指の先端の位置」を取るにはvar indexTipPos = skeleton.Bones[(int) OVRSkeleton.BoneId.Hand_IndexTip].Transform.position;こうなります(注:enumなのでintキャストが必要)
直線判定
以上を踏まえ、この「人差し指がまっすぐになっていて、中指、薬指、小指は曲がっている」を判定します。(親指は除きました。)
この「人差し指がまっすぐになっている」というのは人差し指の「第三関節→第二関節」の方向(ベクトル)と、「第二関節→第一関節」の方向(ベクトル)と「第一関節→指先」の方向(ベクトル)が大体同じ方向を向いている ということです。
この「二つのベクトルが大体同じ方向を向いている」=「どれぐらい似通っているか」を表すのは。そう内積(Vector3.Dot)です。
ベクトルの内積は直角(一番似通っていない)な場合は0
全く同じ場合は+1
全く逆方向の場合は-1ですこれを人差し指だけではなく他の指の分もベタ書きするとそこそこ長くなってしまうので、以下のようなメソッドを用意すると便利だと思います。
[SerializeField] private OVRSkeleton _skeleton; //右手、もしくは左手の Bone情報 /// <summary> /// 指定した全てのBoneIDが直線状にあるかどうか調べる /// </summary> /// <param name="threshold">閾値 1に近いほど厳しい</param> /// <param name="boneids"></param> /// <returns></returns> private bool IsStraight(float threshold, params OVRSkeleton.BoneId[] boneids) { if (boneids.Length < 3) return false; //調べようがない Vector3? oldVec = null; var dot = 1.0f; for (var index = 0; index < boneids.Length-1; index++) { var v = (_skeleton.Bones[(int)boneids[index+1]].Transform.position - _skeleton.Bones[(int)boneids[index]].Transform.position).normalized; if (oldVec.HasValue) { dot *= Vector3.Dot(v, oldVec.Value); //内積の値を総乗していく } oldVec = v;//ひとつ前の指ベクトル } return dot >= threshold; //指定したBoneIDの内積の総乗が閾値を超えていたら直線とみなす }可変長配列でBoneIDを複数(3個以上)受けとり、一つ前のBoneIDが示す関節から関節のベクトルと、今のBoneIDが示す関節と次の関節のベクトルの内積を計算して、
dot
にどんどん乗算していっています。(別に平均でも良い気はしますが)これを使うと、人差し指がまっすぐかどうかを判定してLogに表示する場合、このようになります。
var isIndexStraight = IsStraight(0.8f, OVRSkeleton.BoneId.Hand_Index1, OVRSkeleton.BoneId.Hand_Index2, OVRSkeleton.BoneId.Hand_Index3, OVRSkeleton.BoneId.Hand_IndexTip); Debug.Log($"人差し指は{isIndexStraight?"まっすぐ":"曲がってる"}");おなじく、他の指も調べていけば、「人差し指がまっすぐになっていて、中指、薬指、小指は曲がっている」は判定できそうです。
var isIndexStraight = IsStraight(0.8f, OVRSkeleton.BoneId.Hand_Index1, OVRSkeleton.BoneId.Hand_Index2, OVRSkeleton.BoneId.Hand_Index3, OVRSkeleton.BoneId.Hand_IndexTip); var isMiddleStraight = IsStraight(0.8f, OVRSkeleton.BoneId.Hand_Middle1, OVRSkeleton.BoneId.Hand_Middle2, OVRSkeleton.BoneId.Hand_Middle3, OVRSkeleton.BoneId.Hand_MiddleTip); var isRingStraight = IsStraight(0.8f, OVRSkeleton.BoneId.Hand_Ring1, OVRSkeleton.BoneId.Hand_Ring2, OVRSkeleton.BoneId.Hand_Ring3, OVRSkeleton.BoneId.Hand_RingTip); var isPinkyStraight = IsStraight(0.8f, OVRSkeleton.BoneId.Hand_Pinky0, OVRSkeleton.BoneId.Hand_Pinky1, OVRSkeleton.BoneId.Hand_Pinky2, OVRSkeleton.BoneId.Hand_Pinky3, OVRSkeleton.BoneId.Hand_PinkyTip); Debug.Log($"人差し指は{isIndexStraight?"まっすぐ":"曲がってる"}"); Debug.Log($"中指は{isMiddleStraight?"まっすぐ":"曲がってる"}"); Debug.Log($"薬指は{isRingStraight?"まっすぐ":"曲がってる"}"); Debug.Log($"小指は{isPinkyStraight?"まっすぐ":"曲がってる"}"); if(isIndexStraight && !isMiddleStraight && !isRingStraight && !isPinkyStraight ){ //人差し指だけまっすぐで、その他が曲がっている Debug.Log($"お前がナンバーワンだ!"); }そして、人差し指の先端の位置 (IndexTip) で線を描くとこうなりました
実家に帰っている間にOculusQuestのハンドトラッキングがUnity対応していたので、色々と試し中。
— すずきかつーき (@divideby_zero) December 22, 2019
左手がボーンだけ表示。
ボーンから関節情報が取れるので、各指が伸びているか曲がっているかを独自に計算してみた。
しかし・・・。座標が安定しないので全然図形は描けない。ぐぬぬ。#OculusQuest pic.twitter.com/6PaNVLBfmhうーん。 ノイズなのかなんなのか。 まったく直線が描けてないですね。
とりあえずの目標(で線を描く)はできているので、よしとします。
この問題はまた後日・・・。まとめ
OVRHandからとれるピンチ情報に加え、この「各指が曲がっているか(false)伸びているか(true)」が加わるだけでもいろんな事が出来るんじゃないかなと思います。
<例>
- 全部falseならグー、人差し指と中指だけtrueならチョキ、全部trueならパー、でじゃんけん
- 中指と小指がfalse、そのほかがtrueで「グワシ!」
- 中指だけtrue そのほかfalse で 「F******!」で、プログラム強制終了。
などなど。しかし・・。ちゃんとリファレンス見てないので、こんなことしなくても情報は取れるよ!もっといい方法あるよ! などあったらコメント教えてください。
ではでは、よきOculusQuestライフを。
- 投稿日:2019-12-23T10:04:14+09:00
Unity AR Plane Occlusionの実装について
ARで現実世界にオブジェクトを生成する時、開発者が指示しなければ、カメラは生成したバーチャルオブジェクトと現実世界のオブジェクト(机とか)との位置関係を正しく認識することができず、結果的に不自然な描画になってしまいます。
このオブジェクトの重なりをいかに描画するかと言う問題は何もARに限った話ではなく、Computer Graphicsにおける基本的な問題の1つです。
もちろんすでに先人達が解決してくれているので、その知恵を基にこの問題に対処したいと思います。今回、AR Plane Occlusionを実装するに当たって、レンダリングパイプラインやdepth bufferについての理解が必要不可欠だったので、そこら辺も含めて記事にしたいと思います。
今回実現したいこと
ARでより現実味のある自然な描画を実現したい!
現状
- ARで出力された描画が不自然
- レンダリングパイプラインのどの部分をいじれば、自然な描画に近づけるのかわからない。
準備
そもそもレンダリングパイプラインとは、パイプライン処理によって入力されたデータ(3次元モデルデータなど)を最終的に2次元の画像として出力するまでの過程全体のことです。
Unity道場 2019.2 シェーダを書けるプログラマになろう #1 シェーダを理解しよう
と言う動画で非常にわかりやすくUnityにおける描画プロセスが解説されています。各ステップについては先ほどの動画を見てもらうとして、今回注目したいのはステップ6と8です。
ステップ6
ステップ6はZTestと言われるものです。
描画するオブジェクトが重なっている場合、depth bufferの値を基に、奥にあるのか手前にあるのかを判定します。Depth Bufferとはざっくり言えば、カメラから対象とするオブジェクトまでの奥行き(z値)を保持しているバッファになります。なので今回のようなオブジェクト同士の前後関係(手前か奥か)を判定するとは、各オブジェクトのz値を比較すると言っても過言ではないでしょう。
ステップ8
ステップ8ではZWriteがOnならばZ値の更新を行います。基本的には不透明なオブジェクトの描画の際はOnで、部分的な透過などを実装したい際はOffにします。
後述しますが今回は不透明な場合なのでOnにします。
AR Plane Occlusionの実装
ZTestで手前にあるオブジェクトが描画されるようにすれば良いまでは分かりました。
しかし現実にある机や椅子はそのままではオブジェクトとして扱えません。そこで、机やら椅子やらに対してマッピングするように透明なplaneを生成します。ARFoundationではAR Plane Managerなるものがあり自動的に平面を検知して、指定したplaneオブジェクトを生成してくれます。
まずAR Session OriginにAR Plane Managerコンポーネントをアタッチする。
AR Default Planeを作成し、MeshRendererには自作のMaterialをセットします。
Line Rendererも必要ないのでremoveしても良いです。自作Materialには以下のシェーダーを指定するだけで良いです。
PlaneOcclusionShader "Custom/PlaneOcclusion" { SubShader { Tags { "RenderType"="Opaque" "Queue" = "Geometry-1" } ZWrite On ZTest LEqual ColorMask 0 Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; }; struct v2f { float4 vertex : SV_POSITION; }; v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); return o; } fixed4 frag (v2f i) : SV_Target { return fixed4(0, 0, 0, 0); } ENDCG } } }大したことはしていませんが、注目ポイントはPassの前の設定の部分です。
Tags { "RenderType"="Opaque" "Queue" = "Geometry-1" } ZWrite On ZTest LEqual ColorMask 0まずQueueタグに関して
"Queue" = "Geometry-1"Queueタグはレンダリングの順番を指定するためのタグです。Stack, Queueとかでよく出てくるQueueと同じで、値が小さいほど先に描画されます。半透明なオブジェクトを描くためには、不透明オブジェクトとの描画順を正しく指定してあげる必要があったりしますので、Queueはそういう時のために力を発揮します。Unityでは
Name Value Background 1000 Geometry 2000 AlphaTest 2450 Transparent 3000 Overlay 4000 の順番に従って描画されます。今回はGeometry-1とすることでこのshaderをアタッチしたオブジェクトが通常のGeometryタグが指定されているオブジェクトよりも先に描画されると言うことがわかります。
ZWrite On ZTest LEqualZWrite Onとはdepth bufferの更新を行うと言うことなので
また、Z値の比較判定はLess than equalの時(つまりよりカメラに近くある時)に成功とします。こうすることで、例えば現実世界にある机が生成したバーチャルオブジェクトよりもカメラ側により近くにあるならば、机が描かれて、バーチャルオブジェクトは描かれなくなります。
ColorMask 0これを指定することで色の出力を無効にすることができます。現実のオブジェクトに対して色付けなどしないので、より自然な描画となります。
結果
plane detectionの精度が高くなく、段ボールの面よりも大きくplaneをマッピングしているために、不自然な描画になってしまいました。
しかし、今回目標としていたplane occlusionはできていることが分かります。Next Step
- AR People Occlusionを実現したい!
参考文献
最後に
間違いがあれば指摘していただけると嬉しく思います!
- 投稿日:2019-12-23T08:35:20+09:00
Unity for Mac 不具合と対処法メモ
・「Save Scene as」がない
→ サンプルシーンを開き、現在のシーンを保存するか聞かれるので、そこで保存・上書き保存ができない
→ シーンをasset内に保存すると、保存できるようになる。・Unityからスクリプトを開くと白紙になる。
→ ビジュアルスタジオから開いて、スクリプトを一旦デスクトップに出してから、上書き保存。
- 投稿日:2019-12-23T02:48:15+09:00
macOS CatalinaでUnityでマイク入力が取れない場合の対処方法
Catalinaにしたら何故かマイク入力から情報が取れない。
特にエラーが出ているわけではなく見かけ上は正常に動いているがデータが空っぽ。全然理由が分からなくて諦めかけたが他のソフトでも似たような症状が出ていることを発見して対処方法を見つけた。
ターミナルからopenでUnityを開くとマイクアクセスのダイアログが出て許可することでマイク入力が取れるようになる。
open /Applications/Unity/Hub/Editor/2019.2.15f1/Unity.app
参考:Audacity under macOS 10.15 Catalina
https://forum.audacityteam.org/viewtopic.php?f=47&t=107162&p=378694
- 投稿日:2019-12-23T02:41:56+09:00
Unityでシューティングゲームを作る(3)
ここまでの進捗
- 背景がループするようにした。
- 普通の敵の動作を作成し、その敵が3秒ごとに生成される。
- 瞬間移動する敵の動作を作成し、5秒ごとに生成する。
- プレイヤーが画面の範囲外に行かないようにした。
- 敵とプレイヤーが衝突したらプレイヤーが消滅する。
- 分散攻撃の敵を実装する。
今後やること
- タイトルシーンとエンディングシーンを追加する。
- ボスキャラの動作を実装する。
- エフェクトとBGMを追加する。
- 様々な敵の出現方法を考える。
この記事で書くのは赤文字の部分
前回の最後に「次はボスかぁー」って書いたけどシーン遷移とかやりたかったから先にタイトルとエンディングをやるタイトルシーンの追加
最終的にどんな感じになったかというと
シーン作成
File→New Sceneから新しいシーンを作成して名前を「Title Scene」とした
以下のようにオブジェクトを追加する
背景
background1,2は背景のオブジェクトで、画面をスクロールするために2つ作った。
オブジェクト1と2で真ん中の赤い部分が中心。
背景オブジェクト1と背景オブジェクト2をY軸に対してマイナスの方向に移動させ、もし中心がある座標までいったらオブジェクト2の位置まで移動するようにした。
具体的なスクリプトとしては以下のようになる。transform.Translate(0, -0.05f, 0); if(transform.position.y <= -20.44f) { transform.position = new Vector3(0, 18.44f, 0); }これはゲーム内の背景にも適用した。
タイトル名のTextとButton
タイトル画面にタイトル名のTextとゲームを開始するためのボタンを設置した。
これらをそのまま画面に出すのは面白くないので、アニメーションをつけた。
ButtonはScaleを0.3秒毎に変更し続けるようにして、Textのアニメーションは以下のサイトを参考にした。参考:uGUIのTextで1文字単位のアニメーションを実装できる「Text Juice」紹介
参考:badawe/Text-Juicer今回はこの中のY Modifierというアニメーションを使用した。
- 投稿日:2019-12-23T01:10:03+09:00
SpresenseをROSでUnityにつなぐ
今日の目的
SpresenseのL1Sでの測位結果をUnityで取得する。
SpresenseでL1S測位
Spresenseは、Sonyが出しているマイコンボードです。Arduinoや、Sony独自組み込みOS環境でプログラムできます。
特徴的な機能として、GPSがマイコンボードに組み込まれていて、さらに準天頂衛星みちびきのL1Sという補正信号を受信して、より精度の高い測位をすることができます。
(他にも特徴的な機能はあり、全体として非常に面白いマイコンボードですが、ここではGNSSについてだけ書きます。)
まずは、Arduino開発環境で、L1Sを取得してシリアルに送出するプログラムを作成します。といっても、よくできたサンプルコードを公式が公開しているので、今回はこれを少しだけ変更して使います。
変更点は、以下の使う衛星の設定箇所です。
/* Set this parameter depending on your current region. */ static enum ParamSat satType = eSatGpsQz1cQz1S;これで、GPSとQZSSのL1C/AとL1Sを受信して測位するようになります。
Raspberry Pi Zero WH でシリアル受信して、ROSでメッセージ送出
上記のプログラムをArduino IDEからコンパイルして書き込むと、USBシリアルに測位関連のメッセージが流れてきます。
pi@raspberrypi:~/ros_catkin_ws $ cat /dev/ttyUSB0 SpGnss : begin in SpGnss : begin out SpGnss : start in mode = COLD_START SpGnss : start out Gnss setup OK 1980/01/06 00:00:01.000498, numSat: 0, No-Fix, No Position 1980/01/06 00:00:02.000519, numSat: 1, No-Fix, No Position 1980/01/06 00:00:03.000508, numSat: 1, No-Fix, No Position 1980/01/06 00:00:04.000498, numSat: 1, No-Fix, No Positionこんな感じです。
実際に使うときは、NMEAあたりのフォーマットで送るのがいいかなと思いますが、今回はこのままROS経由でUnityに持って行ってみます。先回作成したROSのPublisherのプログラムを以下のように変えました。
ros_gnss_test_node.cpp#include <stdio.h> #include <stdlib.h> #include "ros/ros.h" #include "ros_gnss_test/MsgGNSS.h" int main(int argc, char **argv) { FILE *fp; char buffer[1024]; fp = fopen("/dev/ttyUSB0", "r"); if(fp == NULL) { printf("ERROR fopen"); exit(0); } ros::init(argc, argv, "ros_gnss_test"); ros::NodeHandle nh; ros::Publisher ros_gnss_test_pub = nh.advertise<ros_gnss_test::MsgGNSS>("ros_gnss_msg", 100); ros::Rate loop_rate(10); ros_gnss_test::MsgGNSS msg; while(ros::ok()) { for(int i = 0; i< 1024; i++) buffer[i] = 0; if(fgets(buffer, 1024, fp) != NULL){ msg.data = buffer; msg.stamp = ros::Time::now(); ROS_INFO("send msg = %s", msg.data); ros_gnss_test_pub.publish(msg); loop_rate.sleep(); } } fclose(fp); return 0; }UnityでROSメッセージを受け取る
前回のプロジェクトをそのまま使って、メッセージを受け取ってみました。
雑ですが、GPSの情報をUnityに持ってくることができました。
ちゃんと作ればいろいろ便利そうです。
- 投稿日:2019-12-23T01:07:44+09:00
AAAクオリティのエフェクトアセット「Magic Effects Pack 1」の使い方
この記事について
この記事はUnityアセット冬のアドベントカレンダー 2019 Winter!23日目の記事です。
「Magic Effects Pack 1」とは?
アセットストアにてkripto289氏より販売されている、実写(AAA)クオリティ魔法のエフェクトがたくさん詰まったアセットです。
氏は他にもMesh EffectやRealistic Effects Pack 3、4等の魔法系エフェクト以外にも、様々な美麗エフェクト等のアセットを販売しているので、気になった方は是非チェックしてみてください。Magic Effects Pack 1
エフェクトのサンプル
火花を撒き散らす雷を飛ばしたり…
水の中に相手を閉じ込めたり…
炎の盾で敵の弾を防いだり…これらを含めた実に計33個ものエフェクトが詰まった大容量パックとなっております。
「Magic Effects Pack 1」の特徴
本アセットの大きな特徴として、飛び道具等のエフェクトにはヒットエフェクトもついていることです。
それらも含めると実際のエフェクトの数は50程はあると思います!
かつ用意されているPrefabにはヒット時にそれらのエフェクトを発動させるスクリプトが搭載されているので、Prefabを置くことですぐに使えるのも特徴の一つです。
ほかのエフェクトや動画で見たい方はこちらからご覧下さい。エフェクトの使い方
プロジェクトを作成し、本アセットをインポートした前提で進めます。
本アセットをインポートしたら、ProjectタブのAssetsフォルダ内に「KriptoFX」という名前のフォルダがあるはずなのでそれを開き、さらに「Realistic Effects Pack 1」というフォルダの中に、本アセットの内容物が入っています。
エフェクトのPrefab名がエフェクト名ではなく、番号が振られているだけなので少々分かりにくいかもしれませんので、そんな時は「PC_DEMO」という名前のサンプルシーンを開けば、全てのエフェクトを番号ごとに見ることができます。
使いたいエフェクトを見つけたら、「Prefab」フォルダ内の、「Character」フォルダを開けばキャラクターとセットのエフェクトが、
同じく「Prefab」フォルダ内の、「Effects」フォルダを開き「PC」フォルダを開けば単体エフェクトを取り出すことができます。
また、エフェクトによっては親オブジェクトに「RFX1_Target」というスクリプトがついている場合があり、そのスクリプトの「Target」という変数にオブジェクトを入れると、そのエフェクトが「Target」に入れたオブジェクトに向かって飛ぶようになります!
これらを上手く組み合わせて、貴方の作品に溶け込ませましょう!最後に
kripto289氏が作成しているアセットはどれもが高品質でリアリティのあるものばかりなので、貴方が作成している(する)ゲームによっては合わないこともあるかもしれませんが、もしリアル味のあるゲームを作成している(する)のであれば氏のアセットはどれもとても重宝することになると思います!
最後まで読んで頂き、ありがとうございました!
- 投稿日:2019-12-23T00:45:06+09:00
ひとつのスクリプトで多数のオブジェクト管理する
オブジェクトごとにスクリプトを生成する方法ではオブジェクト間(スクリプト間)での値のやり取りがとても面倒くさい。
そのため、ひとつのスクリプトで多数のオブジェクトを管理する方法を記載する。
まずいつも通り
- Crate Emptyで空のオブジェクト生成
- Input Field生成
- Button生成
を行う。Create EmptyしたGame Objectは名前をAdminにしておく(何でもよいが分かりやすくするため)。
AdminオブジェクトでAdd ComponentしてNew Scritpを生成する(スクリプト名は今回はAdminScriptとする)。
以下のようにアタッチする。using System.Collections; using System.Collections.Generic; using UnityEngine; // これ追加忘れずに using UnityEngine.UI; public class AdminScript : MonoBehaviour { // publicにしないとダメ public InputField input_field_id; // 入力した文字列保存用 private string input_txt; // input fieldでの入力判定 public void InputID() { input_txt = this.input_field_id.text; print(input_txt); } // ボタンが押された時の処理 public void LoginButtonPush() { if (input_txt == "abc") { print("正解です。"); } else { print("不正解です。"); } } }Adminオブジェクトのスクリプト内のメンバ変数input_filed_idにInputFIledオブジェクトを対応付けする。
これで実行してinputFieldにabcを入力してButtonを押すと"正解です。"と出力され、それ以外を入力した場合は"不正解です。"と出力される。
このように、Adminオブジェクトに対応させたAdminScriptひとつでインプットフィールドとボタンのオブジェクトが操作できるようになる。
参考
- 投稿日:2019-12-23T00:38:43+09:00
スクリプト内で別スクリプトの関数を実行する
オブジェクトAからオブジェクトBの値を持ってくるとか、実行する的なことができる。
Hierarchyで
"Create Empty"してオブジェクト名をGameObjectからAdminに変える(名前は何でもよい)
次に2D Object → "sprite"でNew Spriteを追加し、Assetsに画像を追加して
New Spriteに対応付けする。
AdminとNew Spriteにそれぞれ別のスクリプトをAdd Componentする。
New Spreteのスクリプトを以下のようにアタッチ
using System.Collections; using System.Collections.Generic; using UnityEngine; public class SpriteScript : MonoBehaviour { // Start is called before the first frame update void Start() { } // Update is called once per frame void Update() { } // public付けないとダメ public void PrintSpriteName() { print(this.gameObject.name); } }Adminのスクリプトを以下のようにアタッチ
using System.Collections; using System.Collections.Generic; using UnityEngine; public class AdminScript : MonoBehaviour { // public付けないとダメ public GameObject sprite_object; // Start is called before the first frame update void Start() { //sprinte_object<> } // Update is called once per frame void Update() { //test[0].GetComponent<CurrentSelected>().aaa(); sprite_object.GetComponent<SpriteScript>().PrintSpriteName(); } }すると、AdminのInspectorのAdminScriptコンポーネントにスクリプト内で宣言した
Sprite_object
が追加されるので、の⦿マークを押してNew Spriteを入れる(もしくはドラッグ&ドロップ)。
これで実行すれば、Adminスクリプト内でNewSpriteスクリプト内の関数を呼び出せる。
値渡したい場合はset、getのプロパティ関数作ってやればよい。
- 投稿日:2019-12-23T00:13:54+09:00
【Unity学习笔记】跟随物体移动的UI的三种实现方案
游戏中跟随物体移动的的UI,大致可以分为以下三种
- 显示在画面的最上层,不会被其他游戏物体遮挡。这样的UI一般是属于系统的,例如HUD(抬头显示)中的锁定标志等。
- 参与游戏物体的远近关系,会被其他游戏物体所遮挡。这样的UI一般是属于跟随主的,可以称为Billboard,例如血条,名字等。
- 同2一样会被其他物体遮挡,但不同是的UI尺寸不会因为远近而变化,始终保持一致。
接下来来介绍一下3中UI在UGUI下的实现方法
1.显示在画面的最上层,不会被其他游戏物体遮挡。这里就用锁定标志来举例
创建一个脚本AimMark.cs,装在实际AimMark的UI物体上
Unity提供了一个RectTransformUtility工具类,可以很方便的实现这个功能。
关键代码:private void LateUpdate() { // 将Target的世界坐标转先转换到屏幕坐标,再将其转换到父RectTransform内的局部坐标 var screenPoint = RectTransformUtility.WorldToScreenPoint(MainCamera, Target.position); if (RectTransformUtility.ScreenPointToLocalPointInRectangle(ParentRectTransform, screenPoint, CanvasCamera, out var vector2)) { transform.localPosition = vector2; } }
使用RectTransformUtility.WorldToScreenPoint把世界坐标转换到屏幕坐标,但这还不够,因为UI是隶属于Canvas下的RectTranform的,最终决定UI位置的其实是父RectTransform下的局部坐标。
使用RectTransformUtility.ScreenPointToLocalPointInRectangle,将屏幕坐标转换为指定的父RectTranform下的局部坐标。其中CanvasCamera是隶属于的Canvas的RanderCamera,如果Canvas的渲染模式是Overlay,则这个值应为null,否则位置计算会出错。
AimMark.cs的完整代码
AimMark.csusing UnityEngine; // 锁定标志的控制类 public class AimMark : MonoBehaviour { [SerializeField] private Animator animator; private Canvas ParentCanvas { get; set; } // 隶属于的Canvas private Camera CanvasCamera { get; set; } // Canvas的渲染相机 private RectTransform ParentRectTransform { get; set; } // 父节点的RectTransform private Transform Target { get; set; } // 锁定的目标 private Camera mainCamera; // 游戏画面的主相机 private Camera MainCamera { get { if (mainCamera == null) { mainCamera = Camera.main; } return mainCamera; } } // 指定一个显示锁定标志的屏幕范围 private Rect ViewProtRect { get; } = new Rect(0f, 0f, 1f, 1f); // 记录上一帧是否在范围内的状态,用于和当前状态对比得到转换的瞬间 private bool IsInScreen { get; set; } public void Setup(Canvas rootCanvas, RectTransform rootRectTransform, Transform target) { ParentCanvas = rootCanvas; ParentRectTransform = rootRectTransform; CanvasCamera = ParentCanvas.worldCamera; SetAimTarget(target); } public void SetAimTarget(Transform target) { Target = target; IsInScreen = false; } public void PlayLockAnimation(bool isLock) { if (isLock) animator.SetTrigger("Lock"); else animator.SetTrigger("UnLock"); } private void LateUpdate() { if(Target == null) { Destroy(gameObject); return; } if (IsTargetInScreen()) { if (!IsInScreen) { IsInScreen = true; PlayLockAnimation(true); } // 将Target的世界坐标转先转换到屏幕坐标,再将其转换到父RectTransform内的局部坐标 var screenPoint = RectTransformUtility.WorldToScreenPoint(MainCamera, Target.position); if (RectTransformUtility.ScreenPointToLocalPointInRectangle(ParentRectTransform, screenPoint, CanvasCamera, out var vector2)) { transform.localPosition = vector2; } } else { if (IsInScreen) { IsInScreen = false; PlayLockAnimation(false); } } } // 判断锁定对象是否处于可视范围内 private bool IsTargetInScreen() { // 转换目标的世界坐标至屏幕坐标 var point = MainCamera.WorldToViewportPoint(Target.position); // 屏幕坐标的z值处于相机的进截面和远截面之间,并且处于屏幕范围内,即表示锁定对象在可视范围内 var isInScreen = point.z > MainCamera.nearClipPlane && point.z < MainCamera.farClipPlane && ViewProtRect.Contains(point); return isInScreen; } }2.参与游戏物体的远近关系,会被其他游戏物体所遮挡。这里用血条和名字作为例子
- 在跟随主物体下建立一个Canvas,RanderMode设置为WolrdSpace
这里要注意的是,从Canvas的Z轴正方向看去,Canvas上显示的UI是反向的,也就是说需要让Canvas的Z轴正方向和相机的Z轴正方向一致,UI才是正的
- 接下来就是写一个脚本,控制Canvas的Z轴正向始终保持和相机的Z轴正向一致,即Canvas的旋转值和Camera的旋转值保持一致。
关键代码:
private void LateUpdate() { // 令自身旋转值和相机的旋转值保持一致,使UI始终面向相机 transform.rotation = MainCamera.transform.rotation; }3. 和2一样会被物体遮挡,但UI在屏幕上的尺寸保持一致,不随距离而变化。
实现方法前半和2相同,需要增加的是根据UI距离相机的垂直距离,来缩放UI,实现UI在屏幕上显示的尺寸保持不变。
原理图:
根据三角形相似的定理,其中需要求的缩放比例 = l/L = d/D。其中d为选定的参考距离,D为实际UI距离相机的垂直距离。具体方法如下
1. 先通过2中实现的UI,调整相机到UI的距离,取一个认为合适的UI大小,将此时相机和UI间的垂直距离作为一个参考距离,记为d。
2. 在游戏运行中中取得D。因为UICanvas的旋转值和相机是一致的,所以UI到相机的垂直距离,就等于UI在相机坐标系内的Z值。要做的就是将UICanvas的世界坐标转换到相机坐标系下的局部坐标,然后取z值即可。Unity中也有非常方便在各种坐标系下转换的方法。// 计算出自身在相机坐标系内的局部坐标,此时局部坐标的Z值即为自身到相机的垂直距离 var posInCamera = MainCamera.transform.InverseTransformPoint(transform.position);3。计算2中取得的z值和参考距离的比值,即为需要缩放的比例。
// 使用当前垂直距离比上参考距离,即可得出需要缩放的比例 var rate = posInCamera.z / baseDistance;4。最后将3中计算的出的缩放值乘以原本的缩放值,即可得到最终缩放值。
把1到4整合成一个计算最终缩放值的函数。// 根据Canvas相对于相机的垂直距离和参考距离的比,来计算出新的缩放比例 private Vector2 CalcScale() { // 计算出自身在相机坐标系内的局部坐标,此时局部坐标的Z值即为自身到相机的垂直距离 var posInCamera = MainCamera.transform.InverseTransformPoint(transform.position); // 使用当前垂直距离比上参考距离,即可得出需要缩放的比例 var rate = posInCamera.z / baseDistance; // 用原本的缩放比例乘以需要缩放的比例,得到最终缩放比例 return baseScale * rate; }Billboard.cs的完整代码
Billboard.csusing UnityEngine; public class Billboard : MonoBehaviour { [SerializeField] private bool isScaleSize; // 是否根据距离来缩放大小 [SerializeField] private float baseDistance = 10f; // 给定距离相机的参考距离。在该距离下的UI大小是我们想要的 private Camera mainCamera; // 游戏主相机 private Camera MainCamera { get { if (mainCamera == null) { mainCamera = Camera.main; } return mainCamera; } } private Vector2 baseScale; // 原本的缩放比例 private void Start() { baseScale = transform.localScale; } private void LateUpdate() { // 令自身旋转值和相机的旋转值保持一致,使UI始终面向相机 transform.rotation = MainCamera.transform.rotation; if (isScaleSize) { var scale = CalcScale(); transform.localScale = new Vector3(scale.x, scale.y, 1); } } // 根据Canvas相对于相机的垂直距离和参考距离的比,来计算出新的缩放比例 private Vector2 CalcScale() { // 计算出自身在相机坐标系内的局部坐标,此时局部坐标的Z值即为自身到相机的垂直距离 var posInCamera = MainCamera.transform.InverseTransformPoint(transform.position); // 使用当前垂直距离比上参考距离,即可得出需要缩放的比例 var rate = posInCamera.z / baseDistance; // 用原本的缩放比例乘以需要缩放的比例,得到最终缩放比例 return baseScale * rate; } }4.注意事项
可以看到,上述的位置和缩放的操作均在LateUpdate中进行,其目的是为了保证UI的位置和缩放的计算在相机和物体的位置计算之后进行。如果UI的位置和缩放计算先于相机和物体,就会发生UI渲染在上一帧时物体的位置上而导致UI错位和滞后感,在快速运动时极为明显。
但仅使用LateUpdate还不能保证一定脚本的执行在相机和物体后,因为相机运动脚本也可能会使用LateUpdate,例如本方案中相机使用了Cinemachine就是默认使用LateUpdate,并且还指定了其脚本执行顺序在默认之后。
因此我们需要把AimMark和Billboard的执行顺序添加到CinemachineBrain的后面
保证UI的位置计算在物体和相机之后,无论如何运动也不会出现UI错位和滞后感了。
总结
- 有一个公共的Canvas,将跟随主的世界坐标转换成需要该Canvas下的RectTransform坐标系下的局部坐标赋值给需要显示的UI局部坐标。
- 在跟随主物体下建立一个Canvas,把UI放入其中。在游戏执行中令Canvas的旋转值和Camera的保持一致。
- 在2的基础上,选定一个参考距离,计算出Canvas在相机坐标系下的局部坐标取得当前距离,利用相似三角形定理计算出缩放值。
- 需要保证UI的位置和缩放计算在物体和相机的位置计算之后。
- 投稿日:2019-12-23T00:07:43+09:00
UnityでROS#で独自のメッセージをSubscribeする
目的
前回導入したROS#で、前々々回作成した独自のメッセージをUnityで受け取ってみる。
結論から言うと、メッセージを受け取ることはできたが問題ありなので、ここではトラブルシュートの過程を記載する。SubScriberを作る
Unity側でのメッセージのクラスとSubscriberのクラスを作成する。
メッセージのクラスは、ROS#のEditor拡張から作ることもできる。便利。MsgGNSS.cs/* This message class is generated automatically with 'SimpleMessageGenerator' of ROS# */ using Newtonsoft.Json; using RosSharp.RosBridgeClient.MessageTypes.Std; namespace RosSharp.RosBridgeClient.MessageTypes { public class MsgGNSS : Message { [JsonIgnore] public const string RosMessageName = "/ros_gnss_test/MsgGNSS"; public Time stamp; public String data; public MsgGNSS() { stamp = new Time(); data = new String(); } public MsgGNSS(Time stamp, String data) { this.stamp = stamp; this.data = data; } } }GNSSSubscriber.csusing System.Collections; using System.Collections.Generic; using UnityEngine; using RosSharp.RosBridgeClient; using RosSharp.RosBridgeClient.MessageTypes; using Newtonsoft.Json.Linq; using Newtonsoft.Json; namespace RosSharp.RosBridgeClient { public class GNSSSubscriber : UnitySubscriber<MsgGNSS> { public string messageData; protected override void Start() { Debug.Log("SubscribeStart"); base.Start(); } protected override void ReceiveMessage(MsgGNSS message) { Debug.Log("ReceiveMsg "+ message); messageData = message.data.ToString(); } } }SubscriberをROSConnectorをアタッチしたGameObjectのアタッチして動かしてみたが、メッセージを受信している気配がない。
さて困った。うまくいかないからパケットキャプチャで確認する
ラズパイのROS側のコンソールログを見てみると、
2019-12-22 14:32:34+0000 [-] [INFO] [1577025154.734286]: [Client 20] Subscribed to /ros_gnss_msg 2019-12-22 14:32:39+0000 [-] [INFO] [1577025159.331651]: [Client 20] Unsubscribed from /ros_gnss_msgちゃんとSubscribeできているみたい。
しかたがないので、WireSharkでパケットキャプチャしてみたところ、それらしきWebSocketのパケットは送受信されている。
つまり、ROS#の側まではデータがきているが、自前のSubscriberまで来ていないということに。C#のソースコードの形でライブラリを導入してデバッグする
Asset Storeや、UnityPackageでの導入だとROS#はRosBridgeClientをdllとして持っていてそれを使う。そのため、そもそもデバッグがしづらい。
そのため、リポジトリをcloneして、DLLの代わりにRosBridgeClientのソースから導入してみる。
ほぼそのまま動くようになったので、Visual Studioでブレークポイントはったり、Debug.Logで値を出力しながらデバッグしていく。判明した原因
独自メッセージの中の、RosSharp.RosBridgeClient.MessageTypes.St.StringがうまくJSONから変換できていない。Execption出しているのだが、握りつぶされてすごくわかりにくい状態になっていた。
ライブラリ側に手を入れて、むりやりメッセージを受け取る
一旦正攻法での解決は後回しにして、ライブラリ側に手を入れて、メッセージをなんとか無理やり受け取れるようにしてみる。
RosSocket.cs内の、メッセージをディスパッチしているところに、どうせ他のメッセージこないからと、以下のようなコードを入れてみる。
case "publish": { string topic = jObject.GetValue("topic").ToString(); foreach (Subscriber subscriber in SubscribersOf(topic)) { //subscriber.Receive(jObject.GetValue("msg")); GNSSMsgBind.sec = uint.Parse((string)jObject["msg"]["stamp"]["secs"]); GNSSMsgBind.nsec = uint.Parse((string)jObject["msg"]["stamp"]["nsecs"]); GNSSMsgBind.data = (string)jObject["msg"]["data"]; } return; }我ながらひどいやりかただ。もちろん同時に以下のようなこれまたひどい設計も何もあったもんじゃないクラスを作っておく。
GNSSMsgBind.csusing System.Collections; using System.Collections.Generic; using UnityEngine; public class GNSSMsgBind { public static string data; public static uint sec; public static uint nsec; }さらに、表示用として、以下のコードを作成して、GameObjectにアタッチしておく。
GNSSSubscDisplay.csusing System.Collections; using System.Collections.Generic; using UnityEngine; public class GNSSSubscDisplay : MonoBehaviour { public string data; // Update is called once per frame void Update() { data = GNSSMsgBind.data; } }なかなかひどいやり方だが、メインスレッドとは別スレッドなので、直接呼出しがしにくく、一番手っ取り早い思いついた方法がこれだった。
ちゃんとラズパイのROSからのメッセージが受信できている。
何が問題か分かったが、納得いかないので今後の課題
この解決方法ではさすがに厳しいので、デシリアライズをちゃんとすることを考えなければいけないが、この確認のためにかなり時間を取ってしまったので、いったんここでおしまい。
- 投稿日:2019-12-23T00:06:08+09:00
Unityプログラマにオススメの新しいC#の機能
Unityでも新しいC#!
長い歴史を持つプログラミング言語、C#。C#は着実に進化し、便利な言語機能を追加してきました。ところがゲームエンジンUnityでは少し前まで、古いC#しか使うことができませんでした。
そんなUnityも、現在は特に工夫をせずに比較的新しいC#を使うことができます。(投稿執筆時の最新C#は8.0、最新Unity 2019.2ではC# 7.3を利用可能です。)
ところで、Unityプログラマの方の中には「こんなC#の機能があるのか!」と驚く人や、「新しいC#の機能、わからない」と困っている人もいるのではないでしょうか?
この投稿では、Unityでのロジック記述でオススメの新しいC#の言語機能を、筆者の独断と偏見で紹介します。
プロパティの書き方いろいろ
次のコードはUnityでよく使うプロパティの例です。
ゲッターオンリーのプロパティで、
SerializeField
がついたフィールドをバッキングフィールドとしてもっています。using System; using System.Collections.Generic; using UnityEngine; [Serializable] public class Monster { [SerializeField] private int hp; // 古いC#でのゲッターオンリーのプロパティ public int Hp { get { return hp; }} }新しいC#では次のように、
=>
を使って短く書けます。[Serializable] public class Monster { [SerializeField] private int hp; // 新しいC#では短く書けるゲッターオンリーのプロパティ public int Hp => hp; }冗長な部分のコードがなくなり、コードが短く簡潔になったことに注目してください。
次のコードは、古いC#におけるセッター・ゲッター両方をもつプロパティの例です。
[Serializable] public class Monster { [SerializeField] private int hp; // 古いC#でのセッター・ゲッタープロパティ public int Hp { get { return hp; } set { hp = value; } } }これらも
=>
を使って冗長な部分を取り除き、簡潔に記述することができます。[Serializable] public class Monster { [SerializeField] private int hp; // 新しいC#でのセッター・ゲッタープロパティ public int Hp { get => hp; set => hp = value; } }
C#にはもともと自動実装プロパティという機能がありました。
自動実装プロパティは、バッキングフィールドを自分で書かなくてよいプロパティです。using System; public class Player { // 自動実装プロパティ public int Name { get; private set; } public Player (string name) { this.Name = name; } }古いC#では自動実装プロパティが使えない場面がいくつかありました。新しいC#では、自動実装プロパティが使える場面が増えています。
次のコードでは
readonly
なフィールドをバッキングフィールドとしてもつNameプロパティです。
古いC#では「コンストラクタで値 or 参照を設定しそれを書き換えない」というプロパティを実現するためには、このように書く必要がありました。
自動実装プロパティは使えなかったのです。public class Player { // 古いC#では、readonlyのために自動実装プロパティでなく // バッキングフィールドを使う private readonly string name; public string Name { get { return name; } } public Player(string name) { this.name = name; } }新しいC#では、このようにreadonlyなプロパティを自動実装プロパティのみで簡潔に実現できます。
public class Player { // 新しいC#では、readonlyの自動実装プロパティが使える public string Name { get; } public Player(string name) { Name = name; } }
次のコードは、バッキングフィールドに初期値をフィールド初期化子で設定しているプロパティです。
古いC#ではプロパティの初期値を設定するためには、このように書く必要がありました。
自動実装プロパティは使えなかったのです。public class Player { // 古いC#では初期値を設定するために、バッキングフィールドを使う // 自動実装プロパティは使えない private string name = "No Name"; public string Name { get { return name; } set { name = value; } } }新しいC#では、初期値の設定とともに自動実装プロパティが使える。
public class Player { // 新しいC#では初期値の設定とともに // 自動実装プロパティを使える public string Name { get; set; } = "No Name"; }
新しく加わった機能は便利機能ばかりですが、注意しないといけない機能もあります。
新しいC#では、自動実装プロパティのバッキングフィールドに属性をつけられるようになりました。この機能を使い、
SerializeField
をプロパティのバッキングフィールドに付けたくなります。残念ながらこれは期待する挙動になりません。(フィールドの名前が変 or インスペクターに出てこない)
「自動実装プロパティのバッキングフィールドに属性付与」と「SerializeField」は合わせて使わないようにしてください。
[Serializable] public class Monster { // Unityでは使ってはいけない [field:SerializeField] public int Hp { get; } }
新しいプロパティは、コードの設計が劇的に変わるわけではありませんが、コードが簡潔になります。ぜひ試してみてください。
複数の値を返したい時・まとめたい時はValueTuple
メソッドで複数の値を返したい時、どうすればいいでしょうか?クラスか構造体を作ればいいでしょうか?
ValueTupleは、クラスや構造体などの型を定義しなくても、複数の値をまとめることができるデータ型です。これを使えば、メソッドで複数の値を簡単に返すことができます。
ToStringや、HashCode、Equals、
==
での比較も実装されており、データ処理時にとても活躍します。新しいC#では、ぜひValueTupleを使ってみてください。
ValueTupleは、非常に扱いやすい形で複数の値をまとめることができる構造体です。
ValueTupleは、つぎのように
()
や要素名を記述し、生成することができます。(これ以外の書き方も存在します)var person0 = (name: "Ryota", level: 31);上で作ったValueTupleには、
name
とlevel
というメンバがあります。Debug.Log($"{person0.name} {person0.level}");メソッドの返値型としてValueTupleを使う時は、このように書きます。
public static (string name, int level) LoadNameAndLevel() => (name: "Ryota", level: 31);ToStringやHashCode、Equalsや
==
も実装されています。var person0 = (name: "Ryota", level: 31); var person1 = (name: "Ryosuke", level: 30); Debug.Log(person0 == person1); Debug.Log(person0.name); Debug.Log(person0.level); Debug.Log(person0.ToString());ValueTupleを扱う際
分解
を使うと、非常に簡潔にかけます。// ValueTupleを返すLoadNameAndLevel public static (string name, int level) LoadNameAndLevel() => (name: "Ryota", level: 31); public static void Main(string[] args) { // 分解で返値を受け取る // stringのnameとintのlevel var (name, level) = LoadNameAndLevel(); }
今までの古いC#でも、匿名型という便利な言語機能がありました
匿名型もクラスや構造体を定義しなくても、名前のない型を作れる機能です。
詳しくはこちら「C#の匿名型について調べてみた」。
匿名型は、LINQやRxなどの処理の中間データとしては非常に便利だったのですが、メソッドの返り値型にできませんでした。
ValueTupleはメソッドの返値型にできます。また、ValueTuple構造体よりも前、クラス型のTupleがありました。
Tupleを使えば複数の値をまとめることはできました。
しかし、メンバの名前がItem1やItem2となっていること、構造体ではなくクラスであったことなど、あまり使い安くありませんでした。
ダメージ計算・特典計算などのロジックにおいて、
「privateメソッドで複数の値をまとめて返したい。しかし型を作るほどではない」
という場面があると思います。
そのような時は、ぜひValueTupleを活用してください。
※ ValueTupleは便利ですが、型を作るべき場面もあります。使いすぎに注意してください。
※ ValueTupleを活用したライブラリ、ImportedLinqもみてみてください。アセンブリを意識したい時のinternalとprivate protected
今までのC#のアクセスレベルは次のものがありました。
private
protected
internal
protected internal
public
それに加えて新しいC#では、
private protected
が加わりました。
Unityでは
Assembly Definition Files
が使えるようになり、アセンブリを意識して開発する機会が増えました。今までのUnityにおけるアクセスレベルでは、次の3個を使うことが多かったです。
private
protected
public
Assembly Definition Files
により、Unityでも簡単にアセンブリを分割できるようになりました。これにより、「アセンブリ内に閉じる」ということが大事になりました。
internal
アクセス修飾子を使えば、同一アセンブリ内のみにアクセスを制限できるようになりました。Assembly Definition Files
とともに活用してください。また、
protected internal
は「同一アセンブリ」もしくは「その型とその派生型」のどちらかであればアクセスできるアクセスレベルです。新しく加わった
private protected
は「同一アセンブリ」かつ「その型とその派生型」がアクセスできるアクセスレベルです。
新しいUnityでは
Assembly Definition Files
が使えるようになり、アセンブリを意識して開発する機会が増えました。そこで、
internal
アクセスレベルとprivate protected
アクセスレベルを活用してください。合わせて、「C#のアクセス修飾子 2019 〜protectedは 結構でかい〜」も参照してください。
nullの扱いもやりやすく
「null参照の発明は10億ドルにも相当する誤りだった」という言葉もありますが、C#にはnullがあります。nullと上手につきあっていかないといけません。
新しいC#では、そんなnullを上手に扱える記法が追加されています。
次のようなMonsterクラスとPlayerクラスがあります。
public class Monster { public string Name { get; set; } } public class Player { public Monster Target { get; set; } }MonsterのNameプロパティもPlayerのTargetプロパティもnullになりえます。
そこで次のように三項演算子とnull判定を使って、次のようなコードを書く必要があります。
本当にやりたいことは、メンバへのアクセスだけなのに、非常に冗長です。
// 古いC#では冗長 Player player = LoadPlayer(); var targetMonsterName = player != null && player.Target != null ? player?.Target?.Name : null;新しいC#ではこのように
?.
を使って非常に簡潔に記述できます。// 新しいC#ではこんな感じに簡潔に書ける var targetMonsterName = player?.Target?.Name;
「もし対象がnullだったら指定した既定の値を設定したい」という状況があると思います。
古いC#では次のような書き方をする必要がありました。
// 古いC#の書き方 Player player = LoadPlayer(); var targetMonsterName = player != null && player.Target != null ? player?.Target?.Name : "Default Target Name";新しいC#ではこのように
??
を使って非常に簡潔に記述できます。// 新しいC#ではこんな感じに簡潔に書ける var targetMonsterName = player?.Target?.Name ?? "Default Target Name";
内部的な話をすると、「player?.Target」と「player == null ? null : player.Target」は等価ではありません。
==
をその型が実装している時は注意してください。?.
や??
を使う場合、==
は呼ばれません。
?.
や??
は非常に便利ですが、UnityのGameObjectやMonoBehaviourの中で使うには注意が必要です。Unityにおいて、GameObjectやコンポーネントでは、
?.
や??
には注意が必要です。GameObjectやコンポーネントでは==
が実装されています。
?.
や??
を使った際に、何が起こるか考えてみてください。
- Reshaper/Riderの「Possible unintended bypass of lifetime check of underlying Unity engine object」って何ぞや?
- C#で「person?.Name」と「person == null ? null : person.Name」は等価じゃない。
- Possible unintended bypass of lifetime check of underlying Unity engine object (JetBrains/resharper-unityのwiki)
進化したSwitch
プログラミング言語C#を学び始めた時、ほとんど全ての人はswitchを勉強したと思います。
新しいC#では、switchはとても強化されています。
今までのC#でのswitch文では、列挙型の値、数値の値、文字列の値で分岐するだけでした。
例えば次のコードのようにです。
public enum Shape { Circle, Triangle, Polygon }public static void SwitchExample0(Shape shape) { switch (shape) { case Shape.Circle: Debug.Log("Circleだよ"); break; case Shape.Triangle: Debug.Log("Triangleだよ"); break; case Shape.Polygon: Debug.Log("Polygonだよ"); break; default: throw new ArgumentOutOfRangeException(nameof(shape), shape, "Un expected shape."); } }
新しいC#では型で分岐できるようになりました。次のようなことができるようになったのです。
// objはどんな型がくるかわからない public static void SwitchExample0(object obj) { switch (obj) { case int n when n < 0: Debug.Log("負の数だよ!"); break; case 7: Debug.Log("ラッキーセブンだよ!"); break; case int n: Debug.Log($"整数だよ! {n}"); break; case string s: Debug.Log($"文字列だよ : {s}"); break; case null: Debug.Log("nullだよ"); break; default: Debug.Log("それ意外だよ"); break; } }
より具体的で実用的なコードだとこのようなことができるようになりました。
public abstract class Shape { public abstract double Area { get; } } public class Rect : Shape { public int Height { get; set; } public int Width { get; set; } public override double Area => Width * Height; } public class Circle : Shape { public int Radius { get; set; } public override double Area => Radius * Radius * Math.PI; }Shape型を継承したRect型とCircle型があります。これとswitchを使って、次のようなコードを書くことができます。
// 抽象型のShape。列挙型じゃないよ! public static void SwitchExample0(Shape shape) { switch (shape) { case Rect r when r.Width == r.Height: Debug.Log($"正方形だよ! 面積: {r.Area}"); break; case Rect r: Debug.Log($"長方形だよ! 面積 : {r.Area}"); break; case Circle c: Debug.Log($"円だよ! {c.Area}"); break; } }ダメージ計算やポイント計算で活用できそうですね!
switchはC# 7.3のさらに先、C# 8.0でさらに進化しています。また今後のC#でさらに強くなっていくでしょう。
ダメージ計算、特定計算などで活躍すること間違いなしです。今後の強化にも期待しましょう。
構造体をより効率よく扱う
C#は、Unityそして.NET Coreの躍進により、よりいろいろな領域で活躍するようになりました。
領域が広がったことにより、パフォーマンスを求められることも増えてきました。
新しいC#では、パフォーマンス改善で活躍する多くの機能が追加されました。一例をあげると、
- 参照ローカル変数
- 参照戻り値
- 読み取り専用参照
- readonly 構造体
- ref 構造体
などです。
これらの機能に関して、neueccさんのUnite 2019の公演、「Understanding C# Struct All Things」というとても素晴らしい公演を参照してください。
まとめ
C#は着実に進化し便利な言語機能を追加してきました。
今までUnityでは古いC#しか使えませんでしたが、最近新しいC#が使えるようになりました。
Unityプログラマの方に使って欲しい新しいC#の機能がたくさんあります!この投稿では、Unityでのロジック記述でオススメの新しいC#の言語機能を、筆者の独断と偏見で紹介しました。
この投稿で紹介していない、便利な新しいC#の機能もたくさんあります。
次の公式ドキュメントや、ufcppさんのとてもわかりやすいブログでぜひ調べてみてください。MSDN