テラシュールブログ

旧テラシュールウェアブログUnity記事。主にUnityのTipsやAR・VR、ニコニコ動画についてのメモを残します。

【Unity】3Dアクションの連続攻撃(コンボ)で学ぶAnimatorControllerのtips

今回はボタンをタイミング良く押すと連続攻撃になる動作を作成します。
格ゲーのようなコマンド入力ではなく、3Dアクションによくあるような非常に単純なもの です。

 

 

ボタン連打でコンボ攻撃

3Dゲームなどで「ボタンを連打したら連続攻撃」という機能をよく見ます。
今回はコンボ表現の単純な紹介をしようと思いましたが、コレが思ったより重要な要素の塊だったので、解説しながら内容を紹介していきます。

なお格ゲーが作りたいならPlayableAPIで再生するアニメーションを選択する部分を自分で拡張するがお勧めです。

f:id:tsubaki_t1:20181102003515g:plain

今回の動作を作るにあたり、使用したコードは「Animatorのトリガーを呼ぶ」くらいで、殆どスクリプトを記述せず実現出来ました。

 

1.  ボタンを押したら殴る

最初のステップでは、ボタンを押したら(Attackトリガーが有効になったら)殴るモーションに入らせます。

  1. [アイドル] と [パンチ] のモーションのトランジション(遷移)を設定
  2. [アイドル] → [パンチ]のコンディションにAttackトリガーを設定
  3. [パンチ] → [アイドル] のコンディションはHasExit(終了時間あり)を設定

これで実行時にAttackトリガーを押せば[パンチ]と[アイドル]の間を行ったり来たりします。

f:id:tsubaki_t1:20181101214104j:plain

ポイントは[アイドル]→[パンチ]のHasExit(終了時間あり)を外す事です。
アイドルや走行といった他のアニメーションに即時切り替わることが期待されているモーションは、HasExitを外さないとモッサリします

 

2.  ボタンを押したら次の攻撃への実現

単発ではなく、順番に攻撃アニメーションを行うように修正を行います。

上ではアニメーション終了時に即座に戻っていましたが、今回はトリガーが押されたら次のアニメーションへとなるようにAnimatorControllerを修正します。

f:id:tsubaki_t1:20181101212637g:plain

考え方は簡単で、攻撃モーションをトリガーで繋いでいくだけです。全ての遷移にはAttackのトリガーによる切り替えを設定しておきます。

f:id:tsubaki_t1:20181101213358j:plain

ここでのポイントはトリガーはトランジションを通ると即座にOFFになる事です。複数のステートの巡回を行いつつちゃんとアニメーション終了待ちさせるといった点では、トリガーは非常に有用です。
ただし「トランジションを通らなければOFFにならない(スクリプトでOFFにする必要がある)」為、使いすぎるとトリガーのリークが起こります。

 

3.  攻撃の入力待ち時間の調整

Attackトリガーの入力待ち受け時間とモーションの繋ぎを調整します。

f:id:tsubaki_t1:20181101220236j:plain

まず注目すべきはオレンジで囲った「終了時間」の部分です。この部分がAttackトリガーの入力を受け付ける時間です。初期値では0.5となっているので、これを出来る限り後ろの方(せめて大体の攻撃モーションが終了して収束する0.8割くらいの位置)に移動します。

f:id:tsubaki_t1:20181101221519g:plain

ここで重要な考えはトリガーの判定は終了待ちで指定したタイミングを「通過した瞬間」に行われるという点です。トリガーが通過すると次に判定するのは次の周の終了待ちのタイミングなので、終了待ちを早めに設定すると簡単にタイミングを逃します。

終了待ちを行わないという選択肢もありますが、その場合「攻撃モーションをキャンセルして次の攻撃モーションが始まる」可能性があります。

 

4.  モーションの繋ぎの調整

入力待ち時間の設定が終わったらモーションの切り替えにかかる時間を調整します。
ここでのポイントは遷移間隔を短くすることです。ここが長いとモーションのブレンド時間が伸びてモーションがボンヤリします。
遷移時間を短くすることで発生する急激な軸足の変化は、オフセットをズラして調整します。

f:id:tsubaki_t1:20181101222358g:plain

なお遷移間隔とオフセットでズレた位置が次のステートの開始点です。
つまり遷移時間が0.25(初期値)で次のステートの終了待ちが0.5(初期値)の場合、0.25~0.5までにトリガーを押さないとタイミングを逃します。

5.  ボタンが押されなかったらキャンセルする

タイミングよくボタンを押したら攻撃は出来ました。次はタイミングを逃してボタンを押さなかった場合にモーションをキャンセルしてアイドル状態に戻す挙動を作ります。

f:id:tsubaki_t1:20181101230043g:plain

やり方は[パンチ]や[フック]といったモーションからアイドルへ「終了時間あり」つきでトランジションを作ります。
この時、必ず終了時間を[パンチ]→[フック]への終了待ち時間より後に設定します。
これでモーション完了時にボタンが押されていなければ[アイドル]へ戻るようになります。

f:id:tsubaki_t1:20181101223854j:plain

なお、どうしても[パンチ]→[フック]の終了時間よりも[パンチ]→[アイドル]の終了時間が前に来る(必ず先に[パンチ]→[アイドル]の遷移が始まってしまう)という場合は、中断要因を使用して強引にフックモーションを実行するという力技もあります。
(これは理解できない挙動の要因になりえるので、余り使わない方が良いと思います)

f:id:tsubaki_t1:20181101225821j:plain

 

6.  ボタンで攻撃を分岐させる

コンボ中に押すボタンによって攻撃を切り替えます。

f:id:tsubaki_t1:20181101232745g:plain

ステートマシンの途中で分岐が発生しますが、この分岐はInt型のパラメーターをもたせる事で解決します。このInt型のパラメーター(AttackType)をボタンと一致させれば、ボタンで挙動が変わるコンボが的なのを実現出来ます。

f:id:tsubaki_t1:20181101232331j:plain

ボタンに応じたトリガーという手もありますが、トリガーのリーク(トリガーが開放されず押した状態が保持されてしまう状態的な)が発生するので、パラメーターで分岐させるほうが好みです。

今回はAttackTypeのパラメーターで分岐しましたが、実際には「移動中か?」や「HPの割合」等をコンディションに渡して再生するアニメーションを切り替えるのも良いかもしれません。

 

7.   ステートマシンを整理する

さて分岐を追加したことでステートマシンが割と大変な感じになってきました。最後にサブステートを使用してステートマシンを整理しておきます。

最初のポイントは、ステートマシンはサブステートに入るとEntryから始まり、Exitで出るとサブステートから遷移するという点です。

  1. サブステートマシン[アタック]を作成
  2. [アタック]と[アイドル]のトランジションを作成。[アイドル]→[アタック]のコンディションはAttack
  3. [アタック]へ攻撃系のモーションを全て移動
  4. 攻撃系モーションの遷移先をExitへ変更
    (終了時間は1に設定)

ダイジェストすると、下のような感じです。

https://user-images.githubusercontent.com/1644563/47859170-0c8b0000-de31-11e8-9fb6-ef052c2a174c.gif

f:id:tsubaki_t1:20181101234413j:plain

まだ攻撃処理が分岐する部分(ハイキックとアッパー)がゴチャゴチャしそうなので、少し過剰ですがもう少し整理しておきます。

分岐する攻撃をサブステート[アタック(分岐)]へ移動します。
[フック]→[アタック(分岐)]へのトランジションは「Attackトリガー」だけで良いです。判断はサブステートマシン側で行います。

f:id:tsubaki_t1:20181102000524j:plain

サブステート側では、Entryから[アッパー]と[キック]へのトランジションを設定します。その際、分岐はAttackTypeのみです。

あとはアニメーション終了時にExitに持っていけば、親サブステート[アタック]のExitへ到達し、最終的に[アイドル]へ戻ります。

f:id:tsubaki_t1:20181102000822j:plain

f:id:tsubaki_t1:20181102001207g:plain

関連

blogs.unity3d.com

www.slideshare.net

AnimatorControllerの整理系

tsubakit1.hateblo.jp終了待ちキャンセル

tsubakit1.hateblo.jp

攻撃モーションが「他の全ての攻撃モーションに遷移する」場合にステートがスパゲティ化するのを何とかするアプローチ

tsubakit1.hateblo.jp

【Unity】C# Job Systemからノイズを使う

前回に引き続き、今回はノイズを使用してみます。

ノイズを使用する

下のような感じの動きを作ってみます。

f:id:tsubaki_t1:20181030205634g:plain

Mathfは多分メインスレッドでしか動作しませんが、Mathematicsだと別スレッドでも問題なく動作します。
またRandomと異なりStatic関数(状態を持たない)なので、そんなに面倒なことをしなくてもノイズが実現出来ました。

コード

gist.github.com

【Unity】C# Job Systemからでもランダムを使いたい

今回はC# Job Systemでランダムを使うアプローチについてです。

UnityEngine.Randomがメインスレッドでしか動作しない

シューティングの弾や移動の判断、様々な要因でランダムを使いたくなります。特に C# Job Systemでデモ的な何かを作ろうと思った時など、要素が同じ動きにならないように動きをバラけさせたくなります。
ここで UnityEngine.Randomがメインスレッドでしか動作しない という問題にぶち当たります。

UnityException : RandomRangeInt can only be called from the main thread

強引なアプローチではランダムテーブルを作ってズラすという物もありますが、今回は異なるアプローチを試します。

Unity.Mathematics.Random

Mathematicsに追加された新しいAPI Unity.Mathematics.Random を試します。
このAPIUnityEngine.Randomと異なり、以下のような特徴があります。

  • 複数のインスタンスを持てる
    (他の箇所でRandomを実行しても再現性がある)
  • 別スレッドからも呼べる
  • Burstで最適化される

特に有り難いのがインスタンスを持てる事による再現性ですが、これは逆を言えば インスタンスを用意し初期化しないと使えません 。static関数ではないので、インスタンスの管理や生成といった手間が入ります。

RandomをC# Job Systemで使ってみる

Randomを使用してみます。
作ったのは下のような物です。

  • ボールが跳ねる
  • 着地時に別の座標(ランダム)へ移動

f:id:tsubaki_t1:20181029230516g:plain

とりあえずジョブにランダムを突っ込む。しかし

ランダムをとりあえず突っ込んでみます。普通に C# Job Systemのフィールドとして登録 し、中で使用するという感じです。
この時、UnityEngine.RandomUnity.Mathematics.Randomの宣言が重複するので、using Random = Unity.Mathematics.Random;とか宣言しておきます。

f:id:tsubaki_t1:20181029231444j:plain f:id:tsubaki_t1:20181029231127j:plain

これで実行してみますが、Random自体は使えてるのですが 値が非常に偏ります 。 対策にと毎フレームSeedを設定しても下のような感じ。対策しないと本当に偏る…というか最早固定値?

f:id:tsubaki_t1:20181029231851g:plain

同じ結果が出ないようにする

というのもジョブは実行時にコピーを作るので、Nextで次の値を取得しても大本に反映されない ため、全てのジョブで 最初のRandom を取得してしまってるからだと思われます。実際、上のはジョブ生成時に新しいSeedを与えた場合で、Seedを与えなければ全てのジョブが毎回同じ結果を返します。

毎回同じ結果を返さない為にジョブ毎にSeedを変えるかインスタンスを共有するかする必要があり、スレッドセーフにしてアクセスとか色々考えましたが、最終的に NativeArrayでインスタンスを共有してシングルスレッドでBurstかけてぶん回したほうが早いなー という結論に至りました。Burst強い。並列処理だと複数のスレッドからRandomにアクセスしちゃうのでヤバイですが、まぁIJobProcessComponentData.ScheduleSingleでシングルスレッドで動かせばまあ良いかなという発想です。

あ、別にStateを共有しても問題ないです。進行を保存出来れば良いので。

f:id:tsubaki_t1:20181029232825j:plain

コード

gist.github.com

感想

Randomを毎回維持してくれるのは有り難い事なのですが、インスタンスが独立してるせいで若干面倒くさい目に合いました。

なお .NET 4系を要求 します。.NET 3系の場合は諦めてランダムテーブルなり自前実装なりをするのが良いです。

関連

Randomのソースコード。xorshiftベース github.com

”ランダム”についての詳しい解説

www.slideshare.net

乱数の独自実装が紹介されています。 kan-kikuchi.hatenablog.com

【Unity】Animation C# Jobsで遊んでみる

Unite LAで盛り上がってますが、今回はソレは置いといてAnimation C# Jobsを試してみます。

Animation C# Jobs

Animation C# Jobsは言うならば 「アニメーションを独自実装するモノ」 です。
Playable APIや派生物(Animation Controller、Timeline、Simple Animator)が 「アニメーションの制御を定義するモノ」 なのに対して、アニメーションを実現するためのストリームに直接操作を行う機能です。

blogs.unity3d.com

そのため一工夫が必要ですがGameObjectを経由せず直接Skin Meshの行列への書込等も期待出来ます。

その他の用途としては、2ボーンIKやフルボディIKのようなコントロール、LookAtや揺れモノのようなエフェクト、マスク等があるみたいです。

www.youtube.com

www.youtube.com

作ってみたもの

f:id:tsubaki_t1:20181025233734j:plain

f:id:tsubaki_t1:20181025233531j:plain

ボーン構造を持つオブジェクトに対してEffectorの回転を流し込むだけのサンプルです。

実行前は単純に複数のボーン構造を持つようなオブジェクトですが、実行時に階層を平坦化して、かつ他のGameObjectから操作します。
コレはEffectorを使用していますが、実際にはNative ArrayでQuaternionを用意しておいてC# Job Systemでガガっと計算するなり、ファイルからストリームで書き込むなりするのが良いんじゃないかなと思います。

解説

f:id:tsubaki_t1:20181025234705j:plain

まずAnimation C# Jobsを実行するにはアバターが必ず必要です。基本アバターからストリームにアクセスしているので、コレが無い場合は InvalidOperationException: The TransformStreamHandle cannot be resolved. のエラーが出力されます。 もしモデルからインポートしていない場合、 Bind Generic Avatarを使用します 。実行後GameObjectの階層構造を変更した場合、アバターの再構築が必ず必要です。
なおバグか分かりませんがPrefabにBind Generic Avatarで作成したアバターは格納されてません。

f:id:tsubaki_t1:20181025234610j:plain

あとはAnimation C# Jobsで作成したジョブをPlayableに格納して実行します。

Animation C# Jobsで行うべき内容は3つ

  1. ジョブを定義し、ストリームにアクセスして色々とするコードを記述する
  2. アニメーションのStreamにアクセスする用のハンドルの取得
  3. Playable Graphにジョブを登録

まずはAnimation C# Jobsを定義します。

見るべき所は2点で、まずIAnimationJobを継承している点です。このインターフェースでAnimation Streamにアクセスします。

あとはハンドルを元にAnimaiton Streamからpositionやrotationを取得し、書き込みます。
もしAnimationClip等と連動したい場合、Playableの子としてアニメーションを再生しておきます。

f:id:tsubaki_t1:20181026000324j:plain

次は使用する側です。 Transformのハンドルを入手します。

animator.BindStreamTransformではAnimatorの子オブジェクトからハンドルを取得します。
正直これAvatarから取得すべきだと思うのですが、なぜか現状はTransformに必ずアクセスが必要です。多分Humanoidは回避出来ます。
このコードではtransform.Findで子オブジェクトを探索して取得しました。

なおTransformStreamHandleを取得したら用済みなので、AnimatorUtility.OptimizeTransformHierarchy(gameObject, null);でHierarchyをスッキリさせます。

あとはBindSceneTransformAvatar外のオブジェクトに対してのストリームを入手します。こちらは特にAvatarを用意したりする必要もなく、シーン内のオブジェクトにアクセス出来ます。
主にIKのエフェクター用です。

f:id:tsubaki_t1:20181025235253j:plain

あとはAnimation C# Jobsを登録します。
AnimationScriptPlayable.CreateでPlayableを作成し、作ったジョブを登録するだけです。
outputを云々するのが面倒くさいので AnimationPlayableUtilities.Play(animator, playable, graph);でサクッと動かしました。PlayableGraphを複数キャラクターで共有する良い子は真似しないでね。

f:id:tsubaki_t1:20181026000117j:plain

そこそこ配置して実行すると、こんな感じで実行されました。
それなりに並列化されています。

f:id:tsubaki_t1:20181026001205j:plain

コード

gist.github.com

感想

Playableを使用しないと使えない(=Animation Controllerをオーバーライドする)ので、ぶっちゃけ普通にGameObjectとLateUpdateでアクセスするのが楽そうな気がしますが、ストリームに直接云々出来るのは楽しいです。特にNativeArray経由でアニメーションデータを流し込んでやるとAnimationClip無しに色々できr…え?GameObjectベースでも出来るって? HAHAHA

関連

サンプルコード一覧です。

github.com

Playable APIとAnimation C# Jobsの比較があります

www.slideshare.net

聞いた話によるとKinematicaもAnimation C# Jobsで動いてるらしいです。

blogs.unity3d.com