今回はUnityのIKを使って物を運びながら走るアニメーションを作成してみようと思います。
岩や箱を一時的にでもキャラクターが持つという場面を考えます。
それ用のアニメーションを作成するとなると箱の大きさを考えて手の位置を変更し細かく調整する必要があります。
が、箱の大きさや持つ位置が合わないのでまた最初からアニメーションの調整!をしているとたまったもんじゃありません。
IKを使って右手、左手の位置や角度を指定すると元々のアニメーションにかかわらず、右手、左手、右足、左足の位置を特定の位置と角度に合わせる事が出来ます。
今回の機能を作成すると、
のような感じで重い箱を持っているように見せる事が出来ます。
使っているアニメーション自体はただの走るアニメーションで、IKを使って重心や手の位置、膝の向きなどを変えているだけです。
IKを使用する為にアニメーターコントローラの設定を変更する
IKを使用する為にアニメーターコントローラーの設定を変更する必要があります。
元々の走るアニメーションは上のような感じです。
キャラクターに設定しているアニメーターコントローラのBase Layerの歯車をクリックし、出てきたウインドウのIK Passにチェックを入れます。
これでスクリプトでOnAnimatorIKメソッドが呼ばれるようになります。
IKの処理はOnAnimatorIKメソッド内に記述します。
キャラクターが持つ箱を作成する
次にキャラクターが持つ箱を用意します。
上のように箱(Box)と右手と左手を置く位置をCreate Emptyで二つ(LeftHand、RightHand)作っておきます。
箱はキャラクターと一緒に動かしたいので子要素にしておきます。
まったく別物として扱うと難しくなるのでやりません・・・・。
最初は別オブジェクトとしておき、それを運びたい時は持ちあげるまでは別物として、持ち上げてからはキャラクターの子要素とした方が、運ぶというアニメーションだけなら楽かもしれません。
手の位置を設定するスクリプトIKを作成する
次にスクリプトで右手、左手の位置と角度をさきほど作成したBoxの右手(RightHand)、左手(LeftHand)の位置と角度に合わせるスクリプトを作成します。
スクリプトの名前はBoxIKとしキャラクターに設定します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | using UnityEngine; using System.Collections; public class BoxIK : MonoBehaviour { // 左手位置用のTransform [SerializeField] private Transform leftHandTransform; // 右手位置用のTransform [SerializeField] private Transform rightHandTransform; private Animator animator; void Start () { animator = GetComponent<Animator>(); } void OnAnimatorIK() { // 左手、右手のIK設定 animator.SetIKPositionWeight(AvatarIKGoal.RightHand, 1); animator.SetIKRotationWeight(AvatarIKGoal.RightHand, 1); animator.SetIKPositionWeight(AvatarIKGoal.LeftHand, 1); animator.SetIKRotationWeight(AvatarIKGoal.LeftHand, 1); animator.SetIKPosition(AvatarIKGoal.RightHand, rightHandTransform.position); animator.SetIKRotation(AvatarIKGoal.RightHand, rightHandTransform.rotation); animator.SetIKPosition(AvatarIKGoal.LeftHand, leftHandTransform.position); animator.SetIKRotation(AvatarIKGoal.LeftHand, leftHandTransform.rotation); } } |
右手と左手のTransformをインスペクタで設定出来るようにします。
右手、左手、右足、左足のIKは
1 2 3 | AvatarIKGoal.名前 |
で取得出来、AnimatorのメソッドSetIKPositionWeight、SetIKRotationWeightでIKのウエイト、SetIKPositionで位置、SetIKRotationで角度の設定が出来ます。
OnAnimatorIKメソッド内でAnimatorの右手、左手の位置と角度のウエイトを1とします。
ウエイトは0~1の値の間の比率で与えます。
0にするとIKが働かず、1にすると完全に指定した位置や角度になります。
IKに関しては
を参考にしてください。
実はこっちのはしごを昇る記事の方が難しいかも・・・・。
これでスクリプトが完成したので、次はUnityの実行ボタンを押して、細かい位置と角度を調整します。
手の位置と角度を細かく調整する
Boxの子要素に作成したLeftHandとRightHandは適当に配置しただけなので、キャラクターがBoxを持つ時に手を置く位置と角度に変更します。
Unityの実行ボタンを押してGameウインドウタブからSceneタブに切り替えキャラクターをダブルクリックして近くによります。
Boxの子要素であるRightHandとLeftHandの位置と角度を調整します。RightHandの位置と角度を変更すると、リアルタイムで右手の位置と角度が変わります。
変更が終了したらインスペクタの歯車をクリックして、Copy Componentを選択します。
その後、実行を解除して、RightHandを選択しインスペクタの歯車をクリックしてPaste Component Valuesを選択します。
これで実行時のRightHandの位置が止まっている時のパラメータに設定されます。
右手と同じように左手のLeftHandの位置も変更します。
上のように右手と左手が固定され物を持って走るようになりました。
元のアニメーションで手が動いているので完全に合わせるのは難しいです。
これはアニメーターコントローラで物を持っている時は別のレイヤーで指先を動かさないものにすれば解決すると思います。
部分的にアニメーションを変更したい場合は、
を参考にしてください。
IKHintを使い関節の位置と角度を調整する
重い物を持っている時のように多少ガニ股にしてみます。
その為に膝の位置を変更します。
AvatarIKGoalは右手、左手、右足、左足の位置と角度ですが、
AvatarIKHintは右ひじ、左ひじ、右ひざ、左ひざの位置になります(位置というより向く方向を決めているのかも?)。
それではスクリプトに追加します(追記部分のみ)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | // 左ひざのHint用Transform [SerializeField] private Transform leftKneeTransform; // 右ひざのHint用Transform [SerializeField] private Transform rightKneeTransform; // 左ひじのHint用Transform [SerializeField] private Transform leftElbowTransform; // 右ひじのHint用Transform [SerializeField] private Transform rightElbowTransform; void OnAnimatorIK() { // 左ひざ、右ひざのIKHintの設定 animator.SetIKHintPositionWeight(AvatarIKHint.RightKnee, 1); animator.SetIKHintPositionWeight(AvatarIKHint.LeftKnee, 1); animator.SetIKHintPosition(AvatarIKHint.RightKnee, rightKneeTransform.position); animator.SetIKHintPosition(AvatarIKHint.LeftKnee, leftKneeTransform.position); // 左ひじ、右ひじのIKHint設定 animator.SetIKHintPositionWeight(AvatarIKHint.RightElbow, 1); animator.SetIKHintPositionWeight(AvatarIKHint.LeftElbow, 1); animator.SetIKHintPosition(AvatarIKHint.RightElbow, rightElbowTransform.position); animator.SetIKHintPosition(AvatarIKHint.LeftElbow, leftElbowTransform.position); } |
追加する部分はほとんど同じなので大丈夫だと思います。
右手、左手の時と同じように膝の位置を設定します。
膝の少し前で少し外側にRightKneeを置きます。RightKneeはBoxの子要素に作成してください。
同じように左ひざLeftKneeを作成しUnityの実行ボタンを押して位置を調整してください。
右手や左手と違って前方の方にしないと曲がる時に膝が変な感じになるかもしれません。
RightElbow、LeftElbowも同じように設定します。
これで膝の位置の調整も終わったので確認してみましょう。
見事にガニ股になりました。
これでIKを使って物を運ぶアニメーションが完成しました。
もう少し重量のある物を運ぶアニメーションにする場合は元のアニメーションで重心を下げておいた方が良さそうです。
また、運んでいる箱(シャレ?)がまったく動かないのでBoxも定期的に動かした方がリアルな感じが出ると思います。
箱を動かす機能を追加する
--以降追記--
物を持って動いている時は箱を少し動かしてみます。
膝のIKHintの位置をBoxの子要素にしていましたが、Boxを動かす時に膝の位置は変えたくないので、膝のIKHintの位置をキャラクターの子要素に
移動させます。
ヒエラルキーは上のようになります。手のIKの位置はBoxが動いた時に一緒に動かしたいのでそのままです。
次にBoxを動かすスクリプトShakeBoxスクリプトを作成します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 | using UnityEngine; using System.Collections; public class ShakeBox : MonoBehaviour { // 初期の箱のローカルポジション private Vector3 boxBasePosition; private Animator animator; // ポジションセットフラグ private bool flag = false; // アニメーションの再生位置 private float normalizedTime; // アニメーターの情報 private AnimatorStateInfo state; // Boxの移動位置 private Vector3 boxNowPosition; void Start () { // 初期化処理 boxBasePosition = transform.localPosition; boxNowPosition = boxBasePosition; animator = transform.root.GetComponent<Animator>(); } void Update () { // アニメーターの情報を取得 state = animator.GetCurrentAnimatorStateInfo(0); // アニメーションの再生位置を0から1の間に調整 normalizedTime = Mathf.Repeat(state.normalizedTime, 1); // 走っている時だけ箱を動かす if(state.IsName("Run")) { if(flag) { // アニメーションの特定位置以外はフラグをオフにする if((normalizedTime < 0.141 || (0.216f < normalizedTime && normalizedTime < 0.355f) || (0.468f < normalizedTime && normalizedTime < 0.645f) || (0.720f < normalizedTime && normalizedTime < 0.768f) || 0.898f < normalizedTime)){ flag = false; } } else { var randomValue1 = Random.Range(-0.2f, 0.2f); var randomValue2 = Random.Range(-0.2f, 0.2f); // 箱を上に移動 if((0.141f <= normalizedTime && normalizedTime <= 0.216f) || (0.645f <= normalizedTime && normalizedTime <= 0.720f) ) { flag = true; boxNowPosition = boxBasePosition + new Vector3(randomValue1, 0.3f, randomValue2); // 箱を下に移動 } else if((0.355f <= normalizedTime && normalizedTime <= 0.468f) || (0.768f <= normalizedTime && normalizedTime <= 0.898f) ) { flag = true; boxNowPosition = boxBasePosition + new Vector3(randomValue1, -0.3f, randomValue2); } } // 設定された位置に滑らかに箱を移動させる。 transform.localPosition = Vector3.Lerp(boxBasePosition, boxNowPosition, Time.deltaTime); } } } |
初期のBoxのローカル位置をboxBasePositionに入れ、そこにランダム値を入れて動かします。
なにやら難しそうな事をしていますが、やっている内容がわかればそんなに問題はないと思います。
アニメーターの現在の再生位置を取得するにはanimator.GetCurrentAnimatorStateInfo(0).normalizedTimeを使います。
例えば走るアニメーションの左足が着地した時の再生位置は上のようにアニメーションが46.4%再生された時です。
animator.GetCurrentAnimatorStateInfo(0).normalizedTimeを取得すると0.464の値の時が左足が着地した時の位置になります。
その値を使ってBoxが移動するアニメーションの範囲を指定し、位置を設定しています。
0.464のような定まった値を取得するのは無理なので、ある程度範囲を広げて、その範囲内になったらflagをOnにして、1回しか設定しないようにします。
flagを使わないと範囲内に入っている時は連続で位置が指定されてしまいます。
normalizedTimeはアニメーションの再生位置を取得出来ますが、1回目の再生の時は0~1の間の値で取得出来ますが、2回目以降は整数値が足されていきます。
例えば2回目の0.464の位置になるとnormalizedTimeの値は1.464になってしまいます。
これではうまく値を取得出来ないので値の制限を加えて0~1の間に設定し直します。
normalizedTime = Mathf.Repeat(state.normalizedTime, 1);
Mathf.Repeatを使うと第1引数の値が0~第2引数の値の間で設定されます。
transform.localPosition = Vector3.Lerp(basePosition, position, Time.deltaTime);
最後に現在の位置から指定した位置までなめらかに移動させます。
これでアニメーションと連動させて箱を動かす事が出来るようになります。
Unityの実行ボタンを押して試してみます。
少しだけリアルになったでしょうか?
Boxをただ動かしたいだけならば、走っているアニメーションの時だけBoxのローカルポジションをランダムに動かすだけでも、表現する事が出来ます。
あまり細かい所は気にしなくてもいい場合はそちらの方が簡単ですね。