- 投稿日:2021-01-01T23:25:16+09:00
『UniTask v2』の色々な .ToUniTask
はじめに
UniTask にはあらゆるものを UniTask に変換する
.ToUniTask
というメソッドがあります
同じものを変換するときでもオーバーロードがあったりでややこしいので備忘録としてまとめてみました
GetAwaiter
やWithCancellation
なども書いておきます(実装を書くのは省きます)環境
以下の環境で動作しています
- Unity 2019.4.15f1
- UniTask 2.0.37
目次
準備
ダミー変数var token = this.GetCancellationTokenOnDestroy(); var uniTask = UniTask.Run(() => "hoge");こんな感じで変数
token
は何かしらのCancellationToken
が、
変数uniTask
には何かしらのUniTaskが入っていると考えてくださいCancellationToken
CancellationToken
を UniTask に変換できますToUniTask
// 実装 public static (UniTask, CancellationTokenRegistration) ToUniTask(this CancellationToken cancellationToken) // 使用例 var (task,registration) = token.ToUniTask(); registration.Dispose(); // CancellationTokenRegistrationから破棄もできる await task;CancellationToken がキャンセル状態になるまで await される UniTask が返ってきます
返り値は(UniTask, CancellationTokenRegistration)
というタプルですWaitUntilCanceled
UniTask型ではありませんが、awaitableなCancellationTokenの型が返ってきます
// 実装 public static CancellationTokenAwaitable WaitUntilCanceled(this CancellationToken cancellationToken) // 使用例 await token.WaitUntilCanceled(); // 直接awaitできる。ToUniTaskはできない逆変換
UniTask を CancellationToken に変換できます
// 実装 public static CancellationToken ToCancellationToken<T>(this UniTask<T> task) public static CancellationToken ToCancellationToken<T>(this UniTask<T> task, CancellationToken linkToken) // 使用例 var ct = uniTask.ToCancellationToken(); var ct2 = uniTask.ToCancellationToken(linkToken);コルーチン
ダミーのコルーチンprivate IEnumerator HogeCoroutine(){ yield break; }ToUniTask , GetAwaiter , WithCancellation
UniTask では
StartCoroutine
を使わずにコルーチンを起動することができます// 実装 public static UniTask ToUniTask(this IEnumerator enumerator, PlayerLoopTiming timing = PlayerLoopTiming.Update, CancellationToken cancellationToken = default(CancellationToken)) // 使用例 await HogeCoroutine(); // コルーチン起動 await HogeCoroutine().ToUniTask(PlayerLoopTiming.FixedUpdate, token); await HogeCoroutine().WithCancellation(token);下記は
MonoBehaviour
とコルーチンを結び付けるためのメソッドです
MonoBehaviourでStartCoroutine
をするように、ゲームオブジェクトの寿命とコルーチンが結びつきます// 実装 public static UniTask ToUniTask(this IEnumerator enumerator, MonoBehaviour coroutineRunner) // 使用例 await HogeCoroutine().ToUniTask(this); // ゲームオブジェクトと結び付けられる逆変換
UniTask を IEnumerator に変換できます
// 実装 public static IEnumerator ToCoroutine<T>(this UniTask<T> task, Action<T> resultHandler = null, Action<Exception> exceptionHandler = null) public static IEnumerator ToCoroutine(this UniTask task, Action<Exception> exceptionHandler = null) // 使用例 StartCoroutine(uniTask.ToCoroutine(Debug.Log,Debug.LogError)); StartCoroutine(uniTask.ToCoroutine(Debug.LogError));エラーハンドリングや結果を得るためのデリゲートを渡せるオーバーロードが用意されてます
AsyncOperation
AsyncOperation
を UniTask に変換できますUniTask には 様々な AsyncOperation が awaitable になっています。
- AsyncOperation
- ResourceRequest
- AssetBundleRequest
- AssetBundleCreateRequest
- UnityWebRequest
シグネチャは拡張メソッドのthisキーワードが付いた引数の型の違いしかなく、使い方は全部一緒なので省きます。
// 実装 public static UniTask ToUniTask(this AsyncOperation asyncOperation, IProgress<float> progress = null, PlayerLoopTiming timing = PlayerLoopTiming.Update, CancellationToken cancellationToken = default(CancellationToken)) // 使用例 await SceneManager.LoadSceneAsync("Game"); await SceneManager.LoadSceneAsync("Game").ToUniTask(cancellationToken : token); await SceneManager.LoadSceneAsync("Game").WithCancellation(token); await SceneManager.LoadSceneAsync("HogeScene") .ToUniTask(Progress.Create<float>(n => { Debug.Log($"現在{n * 100}%"); }), PlayerLoopTiming.Update, token);
SceneManager.LoadSceneAsync
の例です。
.ToUniTaskにProgress
を入れて進行状況を取ることができます。AsyncLazy
UniTask に用意されている
AsyncLazy型
です。ToUniTask ではなく.Taskプロパティ
から取得できます.Taskプロパティ
var lazy = UniTask.Lazy(() => uniTask); var task = lazy.Task; // UniTaskに変換逆変換
UniTask から AsyncLazy に変換できます
uniTask.ToAsyncLazy(); // AsyncLazyに変換IObservable
IObservable
を UniTask に変換できます。ToUniTask
// 実装 public static UniTask<T> ToUniTask<T>(this IObservable<T> source, bool useFirstValue = false, CancellationToken cancellationToken = default) // 使用例 await subject.ToUniTask(cancellationToken:token); // OnCompletedが発行されるまで待つ await subject.ToUniTask(true,token); //次に発行される最初のメッセージを待つ第一引数を
true
にすると、次に発行される最初のメッセージだけを待機できます。逆変換
UniTask を IObservable に変換できます。
uniTask.ToObservable();JobHandle
ジョブの実行が終了するまで待機できます
// 実装 public static UniTask ToUniTask(this JobHandle jobHandle, PlayerLoopTiming waitTiming) public static async UniTask WaitAsync(this JobHandle jobHandle, PlayerLoopTiming waitTiming, CancellationToken cancellationToken = default) // 使用例 await jobHandle; // CancellationTokenが指定できない await jobHandle.ToUniTask(PlayerLoopTiming.FixedUpdate); // 指定したタイミングに切り替えてjobHandle.Complete();される await jobHandle.WaitAsync(PlayerLoopTiming.FixedUpdate,token);実装を見ればどれも違うのがわかりますが、自分はJobHandleに詳しくないので有識者の方、コメントや編集リクエストをお願いします!
AsyncGPUReadbackRequest
AsyncGPUReadbackRequest
を UniTask に変換できます。ToUniTask
// 実装 public static UniTask<AsyncGPUReadbackRequest> ToUniTask(this AsyncGPUReadbackRequest asyncOperation, PlayerLoopTiming timing = PlayerLoopTiming.Update, CancellationToken cancellationToken = default(CancellationToken)) await AsyncGPUReadback.Request(src); await AsyncGPUReadback.Request(src).ToUniTask(cancellationToken : token); await AsyncGPUReadback.Request(src).WithCancellation(token);逆変換
UniTask を AsyncGPUReadbackRequest に変換できます
var asyncGpuReadbackRequest = AsyncGPUReadback.Request(src); asyncGpuReadbackRequest.ToUniTask(PlayerLoopTiming.FixedUpdate,token);おわりに
文章書くの下手すぎて読みにくいかもです。すいません。
間違いがあったら優しく教えてください。
- 投稿日:2021-01-01T17:00:58+09:00
Unity - boltでの敵AIセオリーまとめ 〜 bolt kit Third Personからわかること
あけおめです。
かなり久しぶりに記事を書きます、asiramでございます。
ちょっと思うこともあり、これからちょいちょい投稿できればなーと思っております。さて本題ですが、2020年に無償化・Unityファミリー入りしたビジュアルスクリプティングツール「bolt」について、敵AIセオリーがサンプルプロジェクトからある程度読み取れたので、まとめてみようかなと思います。
その前にちょっとだけboltについて
boltの何がいいの?
- 視覚的に挙動を組むことができる
- 実行中に処理の流れをGUI上で確認できて、エラー個所もわかりやすくデバッグしやすい
- プログラムを書かないので、プログラミング知識が乏しくても組みやすい(かもしれない
勉強するには?
unity公式のサンプルがとてもわかりやすいです。
定番のボールを転がすやつです。インストールから一連の機能を触れるのでオススメですよ。
https://learn.unity.com/project/bolt-roll-a-ball-tutorial本題。bolt kit Third Personから読み取れる敵AIセオリー
今回はフリーアセット「Bolt Kit: Third Person for Unity」から読み取れる敵AIのセオリーになりそうなフローグラフを見ていきましょう。
ちなみに、このアセットはUnity Technologies公式から提供されており、他にもFPS版や2Dゲーム版もあるので興味がある方は見てみてくださいねー(bolt kitでasset storeにて検索すれば出てきます)。今回の環境
Unity 2020.2.1f1
bolt 1.4.13bolt kit Third Personの敵AIの挙動
- パトロール - 指定された範囲をランダムで巡回
- 追跡 - プレイヤーを追跡
- 攻撃 - プレイヤーに向かって弾を撃って攻撃
この他にも、頭上にあるHP的なものの制御やダメージを受けた時の処理などもフローグラフで表現されていますが、今回は上記3つの挙動についてみていきます。
敵オブジェクトにはState Machineが設定されており、EnemyAiというマクロが設定されています。
上記3つの動作はこのマクロで定義されています。
当然(?)ですが、各種Variables(変数)も定義されてます。
状態遷移図(ステートグラフ)
こちらが状態遷移設定です。
「パトロール(Patrol)」「追跡(Chase)」「攻撃(Attack)」が状態遷移条件によってそれぞれ結ばれています。
シンプルなそれぞれの状態の中には、挙動が定義されています。状態遷移条件
よくある「敵の視界にプレイヤーが入ったら/出たら」というものと「プレイヤーが攻撃範囲に入ったら/出たら」が状態遷移の条件として定義されています。
これは、「敵の視界にプレイヤーが入ったら次の状態に遷移する」処理です。
「パトロール」→「追跡」に遷移する時の条件ですね。
Update
イベントで「IsWithinRange」のbool戻り値で判定しています。
ちなみに、Player
変数はプレイヤーGameObject、ChaseRange
はfloat値です。基本的に他の状態遷移条件の処理も同じように、「IsWithinRange」で距離を測って判定しています。
再利用可能なSuperUnit
状態遷移条件処理で出てきた「IsWithinRange」は、SuperUnitといって、関数のように扱えるフローグラフです。
使いまわせる関数として、ファイルそのものを別で用意しておいて、他のフローグラフ内でinputを受け取ってoutputを返す処理として使えます。
boltを使う場合、頻出する処理はもちろん出てくるでしょうから、このようにSuperUnitで切り出すのは必須でやったほうが効率が良さそうですね。グラフもスッキリします。今回の「IsWithinRange」はこんな定義になっています。
自分のposition
と入力のオブジェクトのposition
のDistanceが指定float値より小さければtrueが返る感じですね。パトロールステート
次はそれぞれの状態に書かれている処理を見ていきます。
まずは指定された範囲をランダム巡回するパトロールステートです。
二つに分けて見ていきます。巡回する目的地の設定
一言で言うとこんな感じでしょうか。
「指定された範囲内のランダム値秒おきに、ナビメッシュのdestination(宛先)を決定」
destinationに設定する値(緑色の矢印)は、この後触れます。ナビメッシュは、簡単に言うとあらかじめキャラクターが通れる場所を定義しておいて、宛先を指定すると最適な経路を通って移動してくれる便利な機能です。(詳しくは調べてくださいませ!
while loopで無限ループにしてるのはちょっと意外でしたが、よく考えるとこの場合はupdateイベントで呼ぶより確かに良さそうです。
- Waitで待ち時間を設定しているので、必要以上に処理が呼ばれない(updateにすると、都度呼ばれてしまう
目的地の算出
後半部分(ランダムで目的地を設定する挙動のうちの、目的地を算出する部分)です。こちらも一言で言うと
「指定されたパトロールエリア(BoxCollider)の端から端までのxyz座標をランダムで算出してVector3を返す」
と言う感じでしょうか。colliderからboundsにアクセスして算出しちゃうなんてダイナミックですね。
unityの機能をカバーできちゃうboltだからこその組み方ですねー。追跡ステート
追跡ステートはとてもシンプルです
updateで毎フレームプレイヤー位置を目的地として、ナビメッシュのdestinationに設定しているだけですね。攻撃ステート
攻撃ステートも二つに分けてみてみます。
プレイヤーの方向に体(頭)を向ける
やっていることはシンプルで、基本的にはプレイヤーの座標に向けてLookAt
を呼んでいるだけですね。
ちょっとした注意点としては、y座標だけは自分の座標を使って、体を傾かせないように工夫しているところですね。プレイヤーに向けて弾を撃つ
こちらの処理は、先ほどのLookAtの処理の次に実行されるように設定されています(白い矢印)。ここでもSuperUnitが使用されています。
実は、プレイヤーも弾を撃てるんですが、そこで再利用したいのもありSuperUnitになっているんだと思われます。必要な変数を持ってきて、Fireに渡していますね。
これがFireの中身です。一言だと
「指定オブジェクトの向きに回転させてから、Instantiateを行う(クールタイムあり)」
ですかね。このままだとInstantiateして終わりになってしまう...と思いきや、ちゃんとその先があります。
弾オブジェクトに設定されているフローグラフでまっすぐにvelocityを加えてやる記述があり、それで弾は飛んでいくといった具合です。まとめ
bolt kit Third Personから読み取れる敵AIセオリーを紹介しました。
パトロール - 追跡 - 攻撃の遷移の流れは、いろんなゲームにそのまま使えそうですね。
パトロール、追跡は特に一般的なゲームに見られるものにも近い気がします。(とはいえ改造は必要そう本当は、敵AIがもう少し複雑になっている例を見てみたかったんですが、シンプルな例でわかりやすく良いサンプルでした。
個人的には敵AIは複雑になりがちなので、boltだけでやろうとするとかなり複雑になりそうだなあと思っており、ビヘイビアツリーを使う方法(こっちだとboltでは未対応)と合わせて模索中です。
知見が出来てきたらそこらへんも投稿できればなあと思っています。OMAKE
おまけですよ。
若干愚痴入りですが...bolt kit Third Personを導入に少し手間取ったんです。
2020.2.1f1という最先端でやったせいかもしれませんが...
同じ境遇の人もいるかもなのでちょっとだけ書きますasset storeから落としてきて導入。
そのままだとboltが入っていないのでboltも一緒に導入。(Package Managerで管理されてないので一緒に入ってこない)
サンプルシーンを開くと...、bolt周りの設定が全て消えてる...(フローグラフの割り当てとか変数の設定が消えてる
なぜか元プレハブを開いて編集モードにすると設定が蘇るので、そこでComponentのコピーをして変数を復元したり、フローグラフの割り当てをしました。また、「Use Deterministic Compilation」をオフにしてねというエラーが出ているので、Player settingsからチェックを外すのも行いました。
あとは、パトロールステートの目的地を設定するフローグラフは、そもそもエラーが出ていて、つなぎ直してあげると直る箇所がありました。
boltのプロジェクトを外部から持ってくるときは要注意かもしれない...
- 投稿日:2021-01-01T07:22:36+09:00
Sceneの切り替え方法
Sceneの切り替えをざっくりですがまとめました。
自分用メモです。①まず、SampleSceneとNextSceneを作る。
②sampleSceneに移動し、UIからButtonを作る。
(positionX 0; Y 0; で真ん中にくる)③project部分で右クリックしcreate→C#ScriptでScriptの作成。
④下記のコードを追加する。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;↓ここから下の部分を追記
using UnityEngine.SceneManagement;public class Test : MonoBehaviour
{
public void MoveScene()
{
SceneManager.LoadScene("NextScene");
}
}⑤ヒエラルキーにCreateEmptyでからのオブジェクトを作成。
⑥作成済のC#Scriptをドラッグ。
⑦ButtonのインスペクタのOnClick部分の+を押し、
GameObjectをドラッグ。
右の選択部分からTest→Test.MoveSceneを選択する。⑧ビルドセッティングを開き、NewSceneを追加する。
動作確認する。