今回はUnityのゲームで湖や海といった水面をキャラクターが泳げるような機能を作成してみたいと思います。
湖や海を作成するやり方は

の記事を参考にしてください。
水中にカメラが入った時の表現は

の記事で作成しました。
今回作成する機能は水面をキャラクターが泳ぐ機能を作成しますが、水中では泳げない仕様にします。
その為、水中にカメラが入った時の表現は使わないかもしれませんね。
水中でも泳げるような機能はまた別の機会に作るかもしれません(作らないかもしれません)。
今回の機能で使うキャラクターとカメラについて
今回作成する機能で使うキャラクターとカメラは

で作成したものを改造して使います。
特別↑の記事のキャラクター移動スクリプト等を使う必要はありませんが、一応サンプルとして使っているので紹介しておきます。
泳ぐ機能のスクリプトはRadioControlスクリプトに追記する形で作成しますが、普通のキャラクター移動スクリプトを変更するだけで大丈夫です。
AnimatorControllerの作成
通常のキャラクター移動のアニメーションの状態と遷移の他に、泳いでいる状態と遷移を追加します。
移動と泳ぎが一緒のレイヤーに表示されていると解り辛いので、サブステートマシンでNormalとSwimmingを作成し、それぞれの状態と遷移をその中に移動させます。
サブステートマシンについては

を参照してください。
AnimatorControllerを見ると
↑のようになります。
アニメーションパラメータにはbool型のSwimを作成しておきます。
Normalサブステートマシン
まずはNormalのサブステートマシンを見てみます。
↑のような状態と遷移を作成しておき、それぞれの状態からSwimがtrueになったら、SwimmingサブステートマシンのStandSwimmingに遷移するように作成します。
StandSwimmingへの遷移のSettingsでInterruption SourceにNext Stateを設定しておきます。
Interruption Sourceに関しては

を参照してください。
現時点では、まだSwimmingサブステートマシンの中身を作成していないので、作成してから遷移を繋げます。
その他の状態と遷移に関しては、先ほど紹介したラジコン操作キャラクターの記事で作ったものです。
キャラクターの移動を簡単にする為に、IdleとWalk状態だけを作るだけでも構いません。
NormalのIdle状態を選択して右クリック→Set as Layer Default Stateを選択しておきます。
Swimmingサブステートマシン
次にSwimmingサブステートマシンの中身を作成します。
↑のような状態と遷移を作成します。
StandingSwimming(立ち泳ぎ状態)
StandingSwimmingに設定するアニメーションは、立ち泳ぎをしているアニメーションを設定します。
自作したアニメーションですが出来が悪いですね・・・・(^_^;)
なんだか変なダンスを踊っているようです。(;一_一)
このアニメーションのY座標は手の位置を基点としてBlenderで作りました。
↑のような感じですね。
しかし、今回の機能を作る時に足元に基点があった方が作りやすかった為、アニメーションのY座標の基点を足元に変更します(アニメーションの作り方によって変わります)。
↑のようにRoot Transform Position(Y)のBake Into Poseにチェックを入れ、Based Upon(at Start)をFeetに変更してApplyボタンを押します。
StandingSwimming→ForwardSwimmingへの遷移条件は、SpeedがGreaterで0.1とし、Has Exit Timeのチェックを外しておきます。
StandingSwimming→Idleへの遷移は、Swimがfalseの時でHas Exit Timeのチェックを外します。
StandingSwimming→Idleの遷移を作成するには、StandingSwimmingを選択した状態でMake Transitionをして(Up) Base Layerを選択すると、StateかStateMachineを選択出来るのでStates→Normal→Idleを選択します。
また、SettingsのInterruption SourceにNext Stateを選択します。
ForwardSwimming(前方に泳ぎ出す状態)
次はFowardSwimmingの状態に設定するアニメーションです。
↑のように、立ち泳ぎから平泳ぎへと移行するアニメーションになります。
これもいまいちな出来ですね・・・・(;一_一)
ForwardSwimmingに設定するアニメーションは、再生が終わるとBreaststroke状態へと遷移させています。
つまり泳ぎ始めのアニメーションをさせた後に、平泳ぎを繰り返すBreaststrokeアニメーションを再生させています。
こちらのアニメーションも、Root Transform Position(Y)の位置を足元に変更しておきます。
ForwardSwimming→Breaststrokeへの遷移条件は、Has Exit Timeにチェックを入れアニメーションの再生が終わったら遷移するようにしておきます。
ForwardSwimming→StandingSwimmingへの条件はSpeedがLessで0.1でHas Exit Timeのチェックを外しておきます。
またSettingsのInterruption SourceにNext Stateを設定します。
ForwardSwimming→IdleはSwimがfalseで、Has Exit Timeのチェックを外しておきます。Idleの選択方法はStandingSwimmingの時と同じです。
また、SettingsのInterruption SourceにNext Stateを選択します。
Breaststroke(平泳ぎ状態)
次は平泳ぎを繰り返すアニメーションです。
これまた動きがおかしいですが・・・泳ぎが苦手な人の平泳ぎということにしておきましょう・・・・(-.-)
このアニメーションもRoot Transform Position(Y)を足元に設定します。
また、平泳ぎで息継ぎをする時に水面から出るようにする為、下にあるOffset値を調整する可能性があります。
↑のように変更する可能性もありますが、これもアニメーションの作りによりますので変更する必要がないかもしれません。
Breaststroke→FowardSwimmingの遷移条件は、SpeedがLessで0.1でHas Exit Timeのチェックを外します。
また、SettingsのInterruption SourceにNext Stateを設定します。
Breaststroke→IdleはSwimがfalseで、Has Exit Timeのチェックを外します。Idleの選択方法はStandingSwimmingの時と同じです。
またSettingsのInterruption SourceにNext Stateを選択します。
これでSwimmingサブステートマシンの作成も終了しました。
アニメーターコントローラーの作成が面倒くさいですね・・・・・(ーー;)
キャラクター操作スクリプトに水面との接触処理を追加する
キャラクター操作スクリプトには通常の移動状態と、泳いでいる状態とで処理を分けます。
また、キャラクターが水と接触したかどうかをキャラクター操作スクリプトに追加します。
キャラクターが水と接触したかどうかを判定する為に、水にはコライダを設定し、TagにWaterを設定しておく必要があります。

それではキャラクター操作スクリプトに処理を加えます。
今回の機能で使う処理を追記していってください(一部の移動スクリプトで使うフィールド等は記載していません)。
フィールド宣言部と初期化部
まずは宣言等の追加です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | public class RadioControl : MonoBehaviour { public enum State { Normal, Swim } // キャラクターの状態 private State state; // 水 public Transform water; // キャラクターを水面からどれだけ浮かせるかというオフセット値 [SerializeField] private float offset = 1.15f; void Start () { animator = GetComponent<Animator>(); cCon = GetComponent<CharacterController>(); state = State.Normal; velocity = Vector3.zero; } } |
キャラクターの状態を2つ作成し、StartメソッドでState.Normalを設定しています。
offsetはキャラクターの基点から、その距離だけ上の位置にキャラクターが来たら重力の操作を切り替えます。
キャラクターの移動と泳ぎの処理
次はキャラクターの状態によって動きを変えている処理です。
重力の計算以外は動かす処理を同じにするので動かす処理をMoveというメソッドに切り分けました。
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 | void Update () { // ノーマル状態 if (state == State.Normal) { if (cCon.isGrounded) { velocity = Vector3.zero; animator.SetBool ("Swim", false); Move (); } // 水に入っている状態 } else if (state == State.Swim) { // 泳いでいる時はvelocityの重力値もリセット if (animator.GetBool ("Swim")) { velocity = Vector3.zero; animator.SetBool ("Swim", false); // 水に入っているけど泳いでいない時は重力を足していく } else { velocity = new Vector3 (0f, velocity.y, 0f); } // キャラクターの決められた位置が水面より下になったら泳がせる if (transform.position.y + offset <= water.position.y) { animator.SetBool ("Swim", true); // キャラクター位置を水面からのオフセット位置に調整 transform.position = new Vector3 (transform.position.x, water.position.y - offset, transform.position.z); } // キャラクター操作値の計算 Move (); } // 泳いでいない時だけ重力を働かせる if (!animator.GetBool ("Swim")) { Debug.Log ("重力"); velocity.y += Physics.gravity.y * Time.deltaTime; } // キーでの移動と重力を加味して移動 cCon.Move (velocity * Time.deltaTime); } // キャラクター操作処理 void Move() { hInput = Input.GetAxis ("Horizontal"); // キャラクターの向きを変える if (hInput != 0f) { // 回転値を設定 animator.SetFloat ("Rotate", hInput); transform.Rotate (new Vector3 (0, hInput * rotateSpeed * Time.deltaTime, 0)); } else { animator.SetFloat ("Rotate", 0f); } vInput = Input.GetAxis ("Vertical"); // 上の方向キーが多少押されている if (vInput != 0f) { animator.SetFloat ("Speed", vInput); velocity += transform.forward * vInput * moveSpeed; // キーの押しが小さすぎる場合は移動しない } else { animator.SetFloat ("Speed", 0f); } } |
ノーマル状態の時で地面に接地していれば、アニメーションパラメータのSwimをfalseにし、Moveメソッドを呼び出し動かします。
泳いでいる状態の時は重力を働かせたくないので、velocityのyの値も0にしていますが、泳いでいない時は重力をそのまま計算します。
その後、キャラクターの足元+オフセット値が水面の高さ以下になったら泳ぐようにしています。
キャラクター位置は水面の高さとオフセット値を使って再設定をします。
その後Moveメソッドを呼び出してキー押しでの移動処理の計算をします。
泳いでいる時は重力値を計算しないようにし、最後にCharacterControllerのMoveメソッドで移動させています。
このUpdateメソッド内の処理はもっと簡単な記述が出来るかもしれませんが、わたくしはこの処理だけでだいぶ時間を使ってしまったので多少変でもこのままいきます・・・。
水に入っていてもある程度の深さまでは泳ぐアニメーションに遷移させず、ある程度陸に上がったら通常のアニメーションにするという単純な事がうまく出来ず大変でした・・・(^_^;)
レイを複雑に使ったり、コライダで検知してどうにかしようとしたり、あとはパソコンの処理が遅いせいで処理が間違っているように見えて修正した後に気付いたり。
とにかくずいぶん時間を使ってしまいました・・・・(+_+)
ここら辺の処理はうまい具合にやってください・・・・(T_T)/~~~
自前のMoveメソッド内の処理はキャラクターのラジコン操作の処理なので今回は割愛します。
キャラクターの状態変更部分
次にキャラクターの状態を変更したり水を検知する処理の追加です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | public void SetState(State state) { this.state = state; } void OnTriggerEnter(Collider col) { if (col.tag == "Water") { SetState (State.Swim); } } void OnTriggerExit(Collider col) { if (col.tag == "Water") { SetState (State.Normal); } } |
SetStateはキャラクターの状態を設定するメソッドで、OnTriggerEnterとOnTriggerExitは水に入ったか、出たかを判定する為に使っています。
キャラクターが接触したコライダのタグがWaterだったら、水と接触したか外れたのでキャラクターの状態をそれぞれ変更しています。
今気づいたんですが、キャラクターはCharacterControllerのコライダを使っていますがOnTriggerEnterとOnTriggerExitが使えてますね・・・・。
OnControllerColliderHitは相手方に力を加えたい時に使って、相手を検知する時はOnTriggerEnterとかが使えたんですね。
RigidbodyがついていたりIsKinematicの有無、普通のCollider、CharacterControllerのお互いの接触で発生するメッセージは未だに混乱しますね・・・・(^_^;)
それに関しては以前記事を書いたんですが、Rigidbodyの場合動いていない時は無効化されたりなんだりもあってよくわからなくなってしまいました。
そこら辺はUnityのマニュアルを見た方が正確かもしれません。

キャラクターが水面を泳ぐかどうか確認
機能が完成したのでキャラクターを水に接触させて確認してみましょう。
↑のようになりました。
アニメーションはどうあれ水面を泳いでいるように見えなくもないですね・・・・(^_^;)
水面を後ろ向きに動かした時は立ち泳ぎのままで、平泳ぎで泳いでいる時と同じスピードで後ろに進みます。
水中にいる時は後ろ向きに動けないようにした方がいいかもしれません。
とりあえず水面を泳がせる事が出来ました!
あとは水中を泳ぐキャラクター機能か・・・・(-.-)
放置・・・・(゚Д゚=)ノ⌒゚