- 投稿日:2020-02-09T23:38:47+09:00
ARKit 3のMotion CaptureでVRMを動かすために、関節の回転差分を計算する
前置き
ARKit3では、これまでの表情のキャプチャに加えて全身の姿勢や関節の回転を推定するモーションキャプチャが実装されました。
モーションキャプチャの簡単な解説は、WWDCの「Bringing People into AR」が分かりやすいです。Bringing People into AR
https://developer.apple.com/videos/play/wwdc2019/607/ARKit3 のモーションキャプチャでは、91点の関節を認識し3Dアバターの動きと同期することができます。
動きを同期する上で一番簡単な方法は、各関節ノードの名前とtransformが指定された値になったアバターを利用することです。
これは公式サンプルの「Capturing Body Motion in 3D」に含まれているrobot.usdzを見ると分かりやすいです。
https://developer.apple.com/documentation/arkit/capturing_body_motion_in_3dでは、自前のモデルなど関節名やtransformの異なるモデルを操るにはどうすれば良いでしょうか?
一つは @nkjzm さんの記事のように、対応する構造にエクスポートする方法がありますARKit 3のMotion CaptureでVRMを動かす【Unity】
https://qiita.com/nkjzm/items/d4379d5fd018de67a082そしてもう一つは、上記記事でも触れられていますが各関節の差分から回転を調整して適用する方法です。
今回はVR向け3DアバターファイルフォーマットであるVRMに対して、ARKitで取得した動きを同期する方法を解説します。
ARSkelton3DとVRMの違い
ARSkelton3DはARKitを用いて取得できる人体の構造です。VRMは同じく3Dアバターの構造ですが、両者はいくつかの異なる点が存在します。
骨格数
肩や首などを表す骨格数は
ARSkelton3D VRM 91 54 と異なります。背骨や首が細かく分けられているARSkelton3Dに対してキャラクターを扱うVRMはシンプルな構造になっています
各関節の名称
VRMでは各関節は、自由に名称を付けることができます。
モデルによって右肩一つ取ってもRightSholderやSholderRなど製作者によって名称が異なります。
これらの関節位置と名前はボーンマッピングによって決定されます。一方のARSkelton3Dは、全ての名称が決まっており次のような文字列で取得することができます。
T-Poseの角度
T-Poseは、一般的には両手を水平にあげた状態のことを指します。
VRMでは、それぞれの関節のtransformがidentity(つまり初期状態)の時に、このT-Poseとなります。
ARSkelton3Dの場合、全ての関節のtransformをidentityにすると次のような体制になります。
https://forum.unity.com/threads/example-rig-for-3d-human-skeleton.696512/#post-5158382そして、この体制に
ARSkeletonDefinition.defaultBody3D.neutralBodySkeleton3Dを適用すると、T-Poseになります。
これはつまり、ARSkelton3DがT-Poseの時に各関節がidentityではないということを表しています。pivotの位置
全体のノードの中心を表すpivotは、VRMの場合は足下が、ARSkelton3Dの場合はhipsが中心になります。
localTransform / modelTransformの違い
ARSkelton3Dからは、各関節のtransformが取得できるのですが、localTransform / modelTransformの2種類を得ることができます。
modelTransform
modelTransformは、pivotであるhipsからの移動と回転を持つtransformです。
ここで注意しなくてはならないのは、modelTransformは移動量を持つためそのままアバターに適用すると、腕や背格好が実際の人間と同じ割合になってしまいます。
VRMの場合はキャラクターを扱うことが多く、実際の体型と同じとは限らないのでこのtransformを扱うのはあまり適さないかと思います。localTransform
localTransformは、ある関節ノードの親ノードに対する回転情報のみを持ったtransformです。
基本的にはこのtransformを利用しますが、関節数が異なる箇所に関しては正しく差分を出さないとpivotから遠ざかるほどに誤差が生まれ最終的な姿勢が全く異なるものになってしまうので注意が必要です。関節の回転差分の出し方
ARSkelton3DのlocalTransformは、純粋な回転量(ここではar_quartanionとします。)+ニュートラル状態に持っていくための回転量の合成であると考えられます。
localTransform = ar_quartanion * neutral_quartanionでは、vrmの関節はneutral_quartanionを引いたar_quartanionだけを適用すれば良いでしょうか?
実はそうではありません。
ar_quartanionで与えられる回転は初期状態に依存するので、T-Pose時点で関節がidentityであるvrmと、neutralが与えられているARSkelton3Dでは合成結果が異なってしまうのです。そこで、一度各関節がARSkelton3Dのtransformになるようにquartanionを与え、その上でar_quartanionを与えて、最後にARSkelton3Dのquartanion分を戻すことでvrmでも同じ方向に回転を与えることができます。
回転を戻すにはinverseを合成してあげれば良いです。arOrientaion = neutralOrientation.inverse * transformOrientation target.simdOrientation = vrmNeutralOrienation * arOrientation * vrmNeutralOrienation.inverseでは、各関節をARSkelton3Dのtransformにするにはどのような計算をすれば良いでしょうか。
単純にlocalTransformを与えると、親やその親の回転を考慮出来ないため愚直に全てのノードの親を辿って計算する必要があります。
ただ、この計算は初回に1度だけしておけば良いのでレンダリングへの影響はほとんどありません。
親のtransformのindexはparentIndices
で取得できるので利用します。
parentIndexが存在しない場合は-1が返ってきます。
VRMはpivotの回転がARSkelton3Dと前後逆なので、この時点でy軸180度の回転をかけてあげます。lazy var vrmNeutralJointLocalTransforms: [simd_float4x4] = { let defaultBody3D = ARSkeletonDefinition.defaultBody3D let neutralJointLocalTransforms = defaultBody3D.neutralBodySkeleton3D!.jointLocalTransforms var vrmNeutralJointLocalTransforms: [simd_float4x4] = [] for (var index, localTransform) in neutralJointLocalTransforms.enumerated() { var transforms: [simd_float4x4] = [localTransform] while let parentIndex = defaultBody3D.parentIndices[safe: index], parentIndex > 0 { let parentLocalTransform = neutralJointLocalTransforms[parentIndex] transforms.insert(parentLocalTransform, at: 0) index = parentIndex } masterRotation: do { let rotate = simd_quaternion(.pi, simd_float3(0, 1, 0)) transforms[0] = simd_matrix4x4(rotate) * transforms[0] } vrmNeutralJointLocalTransforms.append(transforms.reduce(simd_float4x4(1), *)) } return vrmNeutralJointLocalTransforms }()完成!
以上の計算を行うことで、ARBodyTranckingでVRMを動作させることが出来るようになりました。
VRMを使った配信アプリvearでは、これらの処理を行ってモーションキャプチャを実装しています。(v1.2から実装)
興味のある方は是非使ってみてください!
https://apps.apple.com/us/app/vear/id1490697369
- 投稿日:2020-02-09T21:24:18+09:00
UnityでSRP(HDRP、URP)を使うメリット、デメリット
初めに
Unityでプロジェクトを新規作成する時に、レンダリングパイプライン(HDRP、URP)を決める必要があると思います。
この記事では、どのレンダリングパイプラインを使うべきなのかという事や、SRPのメリット、デメリットを探っていきたいと思います。レンダリングパイプラインとはメッシュ、シェーダーなど情報から、ゲーム画面に描画するプログラム集団の事を言います。
この記事は2020年2月9日に作られたものです。
メリット
Shader graphが使える
これはSRP(HDRP、URP)でしか使えません。
ノードを使って誰でも簡単にシェーダーが描けるので、表現の幅が広がります。
とっても便利です。従来のものより高機能
HDRPは、グラフィックがとても綺麗で、コンピューターゲーム向け。Shader graphやlightなどで、他にはない機能あります。
URPは、従来のパイプラインに比べ、軽いので、モバイル向け。軽いにもかかわらず、グラフィックの違いはよく見ないと分からないぐらいでしかないです。レンダリングパイプラインを改造出来る
HDRPやURPはSRP(スクリプタブルレンダーパイプライン)が使われているので、改造出来ます。
これによって、Shader graphなどで、描画したものを加工したり、独自のポストエフェクトを使えます。デメリット
情報量が少ない
最近出来たものだし、使い勝手が悪いので、情報が少ないです。
特にURPに関してはとても少ないと思います。シェーダーをプログラムで書くのが難しい
SRPでは色々な理由で、シェーダーをプログラムで書くのがとても難しいです。
私はずっとプログラムで書くために、たくさん調べてきましたが、SRPに伴い、シェーダーを書くコードがShader labでなくなったみたい?で、まだ書けていません。従来のシェーダーが使えない
従来のシェーダーを使おうとすると下のようにピンクになります。
これによって下のような問題が起きます。既存のアセットの一部が使えない、使いにくい
従来のシェーダーが違うために、GaiaやAura2などグラフィック系のアセットが使えないです。
モデルだけのアセットならマテリアルを新しく作って帰れば済みますが、Aura2などでは全く使えません。
良いアセットほど、独自のシェーダーが使われているので、良いアセットほど使いにくいです。HDRP、URPを使うべきか?
結論から言うと使うべきではないと思います。
グラフィックの良さに直結するアセットを使えないのは痛すぎるし、Shader graphは手書きシェーダーに比べ、機能が制限されるからです。
また、visual effect graph は従来のレンダリングパイプラインでも使えるからです。私はこれまでSRPについてたくさん調べて使ってきましたが、この記事を書いてより使うべきではないと感じました。
今からプロジェクトをデフォルトのレンダリングパイプラインに変えようと思います。
ありがとうございました!
- 投稿日:2020-02-09T16:36:59+09:00
Oculus Quest, ハンドトラッキングにおけるハンドサイン認識
目的
Questのハンドトラッキングでなんかしたい
手法
とりあえず指が曲がってるかどうかでハンドサインを認識してみる.
ボーンの内積掛けて指がどれだけ曲がってるかを閾値判定以下二つのコードはOVRSkeltonが付いてるオブジェクトに付けてください.
まず, 指の内積計算するコードHandBoneDot.csusing System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; using System; public class HandBoneDot : MonoBehaviour { public List<BoneDot> _boneDots; public List<FingerDot> _fingerDots; public enum FingerIndex { Thumb, Index, Middle, Ring, Pinky } private OVRSkeleton _ovrSkeleton; private bool _isInitialized; int[,] BoneDirectionIndex = { { (int)OVRSkeleton.BoneId.Hand_Thumb1,(int)OVRSkeleton.BoneId.Hand_Thumb2, (int)OVRSkeleton.BoneId.Hand_Thumb3 }, { (int)OVRSkeleton.BoneId.Hand_Thumb2,(int)OVRSkeleton.BoneId.Hand_Thumb3, (int)OVRSkeleton.BoneId.Hand_ThumbTip },//Thumb { (int)OVRPlugin.BoneId.Hand_WristRoot,(int)OVRSkeleton.BoneId.Hand_Index1, (int)OVRSkeleton.BoneId.Hand_Index2 }, { (int)OVRSkeleton.BoneId.Hand_Index1,(int)OVRSkeleton.BoneId.Hand_Index2, (int)OVRSkeleton.BoneId.Hand_Index3 }, { (int)OVRPlugin.BoneId.Hand_Index2,(int)OVRSkeleton.BoneId.Hand_Index3, (int)OVRSkeleton.BoneId.Hand_IndexTip },//Index { (int)OVRPlugin.BoneId.Hand_WristRoot,(int)OVRSkeleton.BoneId.Hand_Middle1, (int)OVRSkeleton.BoneId.Hand_Middle2 }, { (int)OVRSkeleton.BoneId.Hand_Middle1,(int)OVRSkeleton.BoneId.Hand_Middle2, (int)OVRSkeleton.BoneId.Hand_Middle3 }, { (int)OVRSkeleton.BoneId.Hand_Middle2,(int)OVRSkeleton.BoneId.Hand_Middle3, (int)OVRSkeleton.BoneId.Hand_MiddleTip },//Middle { (int)OVRPlugin.BoneId.Hand_WristRoot,(int)OVRSkeleton.BoneId.Hand_Ring1, (int)OVRSkeleton.BoneId.Hand_Ring2 }, { (int)OVRSkeleton.BoneId.Hand_Ring1,(int)OVRSkeleton.BoneId.Hand_Ring2, (int)OVRSkeleton.BoneId.Hand_Ring3 }, { (int)OVRSkeleton.BoneId.Hand_Ring2,(int)OVRSkeleton.BoneId.Hand_Ring3, (int)OVRSkeleton.BoneId.Hand_RingTip },//Ring { (int)OVRPlugin.BoneId.Hand_WristRoot,(int)OVRSkeleton.BoneId.Hand_Pinky1, (int)OVRSkeleton.BoneId.Hand_Pinky2 }, { (int)OVRSkeleton.BoneId.Hand_Pinky1,(int)OVRSkeleton.BoneId.Hand_Pinky2, (int)OVRSkeleton.BoneId.Hand_Pinky3 }, { (int)OVRSkeleton.BoneId.Hand_Pinky2,(int)OVRSkeleton.BoneId.Hand_Pinky3, (int)OVRSkeleton.BoneId.Hand_PinkyTip }//Pinky }; public class BoneDot { Transform BoneBegin, BoneMiddle, BoneEnd; public float dot; public BoneDot(Transform begin, Transform middle, Transform end) { BoneBegin = begin; BoneMiddle = middle; BoneEnd = end; this.Update(); } public void Update() { dot = Vector3.Dot((BoneMiddle.position - BoneBegin.position).normalized, (BoneEnd.position - BoneMiddle.position).normalized); } } public class FingerDot { BoneDot[] bones; public float dot; public FingerDot(params BoneDot[] bone) { bones = bone; } public void Update() { dot = 1; for (int i = 0; i < bones.Length; i++) dot *= bones[i].dot; } } private void Awake() { if (_ovrSkeleton == null) { _ovrSkeleton = GetComponent<OVRSkeleton>(); } } private void Start() { if (_ovrSkeleton == null) { this.enabled = false; return; } Initialize(); } private void Initialize() { _boneDots = new List<BoneDot>(); _fingerDots = new List<FingerDot>(); _ovrSkeleton = GetComponent<OVRSkeleton>(); for (int i = 0; i < BoneDirectionIndex.Length / 3; i++) { var boneVis = new BoneDot( _ovrSkeleton.Bones[BoneDirectionIndex[i, 0]].Transform, _ovrSkeleton.Bones[BoneDirectionIndex[i, 1]].Transform, _ovrSkeleton.Bones[BoneDirectionIndex[i, 2]].Transform); _boneDots.Add(boneVis); } for (int i = 0; i < Enum.GetNames(typeof(FingerIndex)).Length; i++) { FingerDot Fing; if (i == 0) Fing = new FingerDot(_boneDots[0], _boneDots[1]); else Fing = new FingerDot(_boneDots[2 + 3 * (i - 1)], _boneDots[3 + 3 * (i - 1)], _boneDots[4 + 3 * (i - 1)]); _fingerDots.Add(Fing); } _isInitialized = true; } public void Update() { if (_isInitialized) { for (int i = 0; i < _boneDots.Count; i++) _boneDots[i].Update(); for (int i = 0; i < _fingerDots.Count; i++) _fingerDots[i].Update(); } } }ハンドサイン認識のコード
閾値付近でハンドサインがバタつかないようにsignには多少補正をかけてある.
補正をかけてない値が欲しい場合はHandSignNumを参照する.
HandSignNumは親指を一桁目とした二進数になっている.
例えば親指だけ立っているとき00001(2)=1(10),ピースサインの場合は人差し指と中指が立ってるので00110(2)=6(10)といった具合.
一つ下のEnumも同様に0~31で表される.HandSignDetector.csusing UnityEngine; using System.Collections; public class HandSignDetector : MonoBehaviour { [SerializeField, Range(-1, 1)] float ThumbBendThrethold; [SerializeField, Range(-1, 1)] float IndexBendThrethold; [SerializeField, Range(-1, 1)] float MiddleBendThrethold; [SerializeField, Range(-1, 1)] float RingBendThrethold; [SerializeField, Range(-1, 1)] float PinkyBendThrethold; HandBoneDot handBone; public int HandSignNum; public HandSign_Bend sign; public float SignEndWaitTime = 0.2f; bool IsPosing = false, PoseChangeWaiting = false; void Start() { ThumbBendThrethold = 0.85f; IndexBendThrethold = MiddleBendThrethold = RingBendThrethold = PinkyBendThrethold = 0; handBone = GetComponent<HandBoneDot>(); if (handBone == null) return; HandSignNum = 0; for (int i = 0; i < handBone._fingerDots.Count; i++) if (handBone._fingerDots[i].dot > ThumbBendThrethold) HandSignNum += (int)Mathf.Pow(2, i); UpdateSignInit(); } void Update() { HandSignNum = 0; for (int i = 0; i < handBone._fingerDots.Count; i++) if (handBone._fingerDots[i].dot > ThumbBendThrethold) HandSignNum += (int)Mathf.Pow(2, i); if (!IsPosing) UpdateSignInit(); else if (HandSignNum != (int)sign && !PoseChangeWaiting && IsPosing) StartCoroutine(PoseEndWait(SignEndWaitTime)); } void UpdateSignInit() { sign = (HandSign_Bend)System.Enum.ToObject(typeof(HandSign_Bend), HandSignNum); IsPosing = true; } IEnumerator PoseEndWait(float waitime) { float t = Time.time; PoseChangeWaiting = true; while (Time.time < t + waitime) { if (HandSignNum == (int)sign) { PoseChangeWaiting = false; yield break; } yield return null; } IsPosing = false; PoseChangeWaiting = false; } }以下ハンドサイン一覧用のEnum
HandSign_Bend.csの方は思いつくハンドサインの名前を入れたEnum. 思いつかなかったところは立ってる指の名前を羅列してある.
HandSign_BendRaw.csの方は指の名前の羅列のみ.HandSign_Bend.cspublic enum HandSign_Bend { Fist, ThumbsUp, Point, Gun, Fuck, ThumbMiddle, Peace, ThumbIndexMiddle, Ring, ThumbRing, IndexRing, ThumbIndexRing, MiddleRing, ThumbMiddleRing, Three, ThumbIndexMiddleRing, Lover, Phone, DevilsHorn, Spidey, MiddlePinky, ThumbMiddlePinky, IndexMiddlePinky, ThumbIndexMiddlePinky, RingPinky, ThumbRingPinky, IndexRingPinky, ThumbIndexRingPinky, MiddleRingPinky, ThumbMiddleRingPinky, Four, OpenHand }HandSign_BendRaw.cspublic enum HandSign_BendRaw { AllBend, Thumbs, Index, ThumbIndex, Middle, ThumbMiddle, IndexMiddle, ThumbIndexMiddle, Ring, ThumbRing, IndexRing, ThumbIndexRing, MiddleRing, ThumbMiddleRing, IndexMiddleRing, ThumbIndexMiddleRing, Pinky, ThumbPinky, IndexPinky, ThumbIndexPinky, MiddlePinky, ThumbMiddlePinky, IndexMiddlePinky, ThumbIndexMiddlePinky, RingPinky, ThumbRingPinky, IndexRingPinky, ThumbIndexRingPinky, MiddleRingPinky, ThumbMiddleRingPinky, IndexMiddleRingPinky, ThumbIndexMiddleRingPinky }結果
うぃ~~
— KEEL_210/人間性2Cell (@jdatmtjp) January 29, 2020
ハンドトラッキング正直なところ結構トラッキング範囲広い癖に複雑な印はすぐ信頼度下がって消えるからちょっと使いづらい pic.twitter.com/wBe3TMd1eyまとめ
割とうまくいってる.
- 投稿日:2020-02-09T14:38:54+09:00
unityの勉強を兼ねて、STGを作ってみた
コロナです。
今回はUnityの新しくなったインターフェースやプログラムの勉強を兼ねてSTGの学習を始めました目的
ゲームのジャンルを簡単なものにして、エンジンやプログラムそのものの学習に集中するため一人開発の流れ
1.ネタを考える
実際の開発期間は年末年始だったのでテレビやニュースからネタを考えたり2.自分で動画撮影や操作をしながら志向作成
実際の動画について一つの例
URL https://www.youtube.com/watch?v=3uZDPFH-Cpgひな形まで作成
以下の3点について、作成できたので年を越したものの
・キャラクターを動かす
・武器を発射する
・敵が出ているhttps://unityroom.com/games/happy_gone
これからの課題
unityroom側に書いてあるissueの消化
PRをもらえたら適宜マージ
- 投稿日:2020-02-09T14:28:47+09:00
adventcalender ARCoreを触って、サンプルを作ってみる 12/11分として記載
2020/02/08 記事の構成を記載
こんにちは、コロナです。
今回は、ARの学習をしたくてARCoreのサンプルを作成しようのコーナーです。
ARCoreって何?
UnityがAndroid向けにゲームを拡張するために用意してくれたSDKになります。
Unity側の紹介ページ
https://unity3d.com/jp/partners/google/arcoreissueこれからの課題
google側に記載されているSDKの消化をしていく
https://developers.google.com/ar/develop/unity/quickstart-androidSDKの取り込んで成功した時のスクショ
サンプルシーンを取り込んでの動作確認とスクショ
プロジェクトの設定について、確認と必要であれば設定
Android環境として実際にビルドをする
ありがとうございました
- 投稿日:2020-02-09T14:16:27+09:00
adventcalender DOTS挑戦 2019/12/21分として投稿
2020/02/08 構成の記載
コロナです。
今日は、SNSなどで話題になっているUnityのdotsに挑戦しようということで進めていきます。
dotsって何?
話題になっているなーというだけで、dotsについて理解度が低いので調べてみるところから進めてみます。
正式名称は、EntityComponentSystemのことを表していました。
公式チュートリアルページを見てみる
https://learn.unity.com/tutorial/entity-component-system
更新して記入するときの課題
dotsのチュートリアルを消化する
dotsのチュートリアル後に簡易的なソフトを実装する(1ステージ)
- 投稿日:2020-02-09T07:53:09+09:00
Unity VR 「Oculus Integration」をインポートすると「Unable to start Oculus XR Plugin.」エラーが出る場合の対処法
はじめに
Unityに「Oculus Integration」をインポートし実行すると「Unable to start Oculus XR Plugin.」エラーが出る場合の対処方法を記述します。
原因
Unity内包のプラグインと「Oculus Integration」内包のプラグインのバージョンの差異によりエラーが発生しています。
開発環境
・Windows 10
・Unity 2019.3.0f6
・Oculus Integration 13.0
・Oculus Rift S手順
とりあえず Unity の VR 開発環境をセットアップしておいてください。
Unity 2019.3 VR設定[ spatializer plugin ]を更新するか聞いてくることがあります。
[ Upgrade ]ボタンを押します。
[ spatializer plugin ]が更新され、Unityエディタを再起動してよいか聞いてきます。
よろしければ[ Restart ]ボタンでUnityエディタを再起動します。
以上で完了です。プロジェクトを実行して「Unable to start Oculus XR Plugin.」エラーが出ないか確認してください。
- 投稿日:2020-02-09T06:36:01+09:00
Unity 2019.3 VR設定
はじめに
Unity 2019.3ではVR開発環境のセットアップの方法が新しくなりました。
この記事では Unity 2019.3でのVR開発環境のセットアップの手順を記述します。環境
・WIndows 10
・Unity 2019.3.0f6
・Oculus Rift S手順
メインメニューの[Edit] - [Project Settings...]を選択し「Project Settings」ウィンドウを開きます。
「Project Settings」ウィンドウの[XR Plugin Management]を選択し[Install XR Plugin Management]ボタンを押します。
対象の[Install]ボタンを押します。
ここでは Oculus Rift S用に[Oculus XR Plugin]の[Install]ボタンを押しています。
プラグインがインストールされます。
「Plugin Providers」リストの[+]ボタンを押し「Loader」を選択します。
ここでは Oculus Rift S用に[1. Oculus Loader]を選択しています。
これでセットアップ完了です。
- 投稿日:2020-02-09T05:44:08+09:00
[C#]非同期メソッドにいちいちCancellationTokenを渡すのがめんどくさい
この記事は非同期メソッドにCancellationTokenをいちいち渡すことがめんどくさいという話とそれを楽にするための考察について書きます。
実用的かどうかは微妙なので注意してください。はじめに
C#で非同期メソッドを使用するときキャンセルするためにはCancellationTokenを引数で渡す必要があります。
渡さなかった場合キャンセルできないため思いもよらぬバグに遭遇することがあります。
例えば以下のようなコードです。
(Unity用のコードですがだいたい察せると思います。)// 1秒間隔で表示を更新する class Hoge : MonoBehaviour { [SerializeField] private TextMeshProUGUI text = default; void Start() { _ = HogeAsync(); } async UniTask HogeAsync() { for (var i = 0; ; i++) { await UniTask.Delay(1000); text.text = $"count:{i}"; } } }このコードはHogeAsyncを止める手段がありません。
これによってシーン遷移などでtextが破棄された後にtextにアクセスしてしまうということが起こります。CancellationTokenを渡すように修正すると以下のようになります。
class Hoge : MonoBehaviour { [SerializeField] private TextMeshProUGUI text = default; void Start() { // 破棄されるときにキャンセル状態になるCancellationToken // thisとtextの寿命が違う場合はこれではまずいがとりあえず一緒とする var cancellationToken = this.GetCancellationTokenOnDestroy(); _ = HogeAsync(cancellationToken); } async UniTask HogeAsync(CancellationToken cancellationToken) { for (var i = 0; ; i++) { await UniTask.Delay(1000, cancellationToken: cancellationToken); text.text = $"count:{i}"; } } }従来のコルーチンを使用した方法では自動的に寿命がゲームオブジェクトと結びついていたので非同期にすると少し面倒になっているように感じます。
処理が長くなり複数の非同期メソッドを使用する場合はCancellationTokenを渡し忘れないようにする必要があります。
そもそもCancellationTokenを引数に取るオーバーロードがない場合はCancellationToken.ThrowIfCancellationRequested()
を使用してキャンセルされているかどうかチェックする必要があります。考察
確実にキャンセルされない/キャンセルできない処理、カジュアルな用途の場合はCancellationTokenを渡さないという選択肢もありだと思います。
そうはいっても渡さなければいけないことも多いと思うので以下のような書き方を考えました。static async YTask HogeAsync(CancellationToken token) { // CancellationTokenを挿入する // この後の処理でawaitを使用するとawait抜ける際にキャンセル状態がチェックされるようになる await YTask.Inject(token); // 無限ループなのでキャンセルしないと終わらない for (var i = 0; ; i++) { Console.WriteLine(i); // CancellationTokenを渡してなくても勝手にキャンセルされる await Task.Delay(1000); } }実際に動かしてみると以下のような結果になります。
static async Task Main(string[] args) { var cts = new CancellationTokenSource(); // 5秒後にキャンセルする _ = Task.Run(async () => { await Task.Delay(5000); cts.Cancel(); }); try { await HogeAsync(cts.Token); } catch (OperationCanceledException) { Console.WriteLine("catch OperationCanceledException"); } } /* 0 1 2 3 4 catch OperationCanceledException */動くコードはyaegaki/YTaskに置いています。
ポイントは戻り値のYTaskとYTask.Injectです。
これによって自動で後続のawaitの後にCancellationTokenの確認処理が差し込まれます。
最初にInjectしておけばawaitの度にいちいちCancellationTokenを渡さなくていいので多少楽になります。
しかし、この方法には以下のようなデメリットがあります。
- awaitした非同期メソッドをキャンセルできていない
- ネストした非同期メソッドに対応できない
- 同期的に完了したか最初から完了していたタスクをawaitした場合、キャンセルされない
- 結局最初にInjectを書かないといけなくて面倒
1について、この方法では必ずawaitの後でキャンセルの確認が行われるためawait対象の非同期メソッド自体はキャンセルされていません。
2について、Injectの引数としてCancellationTokenが必要なので結局CancellationTokenが必要になる点は変わっていません。
3について、実装上の制約です。(AsyncMethodBuilderのAwaitOnCompletedが呼ばれないため)
4について、これはそのままです。Unityのようにシングルスレッドが前提でコルーチン的に使用する場合はstatic変数を使用すれば楽になりますがマルチスレッドになった瞬間崩壊します。
static async YTask FugaAsync(string name, CancellationToken token) { // 最上位の非同期メソッドでstatic領域にInjectする await YTask.InjectToStatic(token); await Task.Delay(300); // await後にstatic領域のCancellationTokenがFugaAsyncの最初にInjectされたものに戻る // よって複数の非同期メソッドを別々のCancellationTokenで同時に動かしてもシングルスレッドの場合は正常に動作する await PiyoAsync(name); } static async YTask PiyoAsync(string name) { // 下位の非同期メソッドではstatic領域から拾ってきてInjectする await YTask.InjectFromStatic(); for (var i = 0; ; i++) { Debug.Log($"{name}:{i}"); await Task.Delay(1000); } }まとめ
ちょっと思いついたので書いてみましたがよく考えると微妙でしたという感じです。
結局のところ毎回引数にCancellationTokenを渡すのはそういうものだと思って書くのが楽かもしれません。
Unityについてのみ考えるのなら非同期メソッドの先頭で常にGetCancellationTokenOnDestroyで取得したものをInjectするというのもありかもしれません。(うーん...)
async YTask HogeAsync() { await YTask.Inject(this.GetCancellationTokenOnDestroy()); // FugaAsyncの中でも先頭でInjectしているはずなのでCancellationTokenを渡さない await FugaAsync(); } async YTask FugaAsync() { await YTask.Inject(this.GetCancellationTokenOnDestroy()); // 適当な後続処理... }