今回はUnityのゲーム内でスマートフォン風のデジタルデバイス機器を作成し、操作をする機能を作成してみます。
通常はTPS(三人称視点)操作のゲームでスマフォを見た時はFPS(一人称視点)にし、手に持ったスマフォをマウス操作で操作出来るようにします。
FPS視点の時はIKの機能を使って右手の人差し指をマウスポインタの位置に合わせ、スマフォの画面を押す位置と合わせるようにしていきます。
右手でスマフォを持って右手の親指で操作!とかなると難しいのでそれはみなさんにおまかせします。ε≡≡ヘ( ´Д`)ノ
スマフォじゃなくて操作パネルを持つ機械を操作する機能ってことにしておけばいいのだ。(=゚ω゚)ノ
この人差し指をマウスポインタの位置と合わせるのはちょっと苦労しました。(^_^;)
しばらくブログを休んでいたせいかも?
いや、関係ないかも・・・(-_-)
今回作成するスマフォもどきは数字を打てるだけですが、細かいUIのデザインや操作はみなさん試行錯誤して作成してみてください。
機能を作成すると、
上のような感じになります。
今回はマウス操作でスマフォを操作します。
今のところゲームパッドでの操作の対応はしていません(やらないと思います・・・・)。
なんだか前置きが長くなりましたが・・・・、それでは機能を作成していきましょう。
スマフォもどきの作成
まずはキャラクターに持たせるスマフォもどきのゲームオブジェクトを作成していきます。
大まかな形状の作成
スマフォをモデリングしてそれをUnityに使ってもいいのですが、今回はUnityのCubeのScaleを調整してスマフォを作成していきます。
ヒエラルキー上で右クリック→3D Object→Cubeを選択し、名前をSmartPhoneに変更します。
SmartPhoneを選択しインスペクタでScaleを調整し、形状を変形させます。
SmartPhoneを選択し右クリック→UI→Canvasを選択し、SmartPhoneの子要素にCanvasが作られるようにします。
CanvasのRender ModeでWorld Spaceを選択し、通常の3D空間上にUIが表示される設定にします。
RectTransformの設定値を変更し、SmartPhoneゲームオブジェクトのサイズに合うようにScaleの調整と幅と高さ、位置を変更します。
インスペクタのAdd ComponentからLayout→Canvas Groupを選択し取り付けます。
Canvas GroupはこのCanvasのグループ全体を操作する為のコンポーネントで、TPS視点でスマフォを持っている時はボタンを操作出来ないようにする為Canvas GroupのInteractableのチェックを外しておきます。
変更した箇所は上のようになっています。
PushSmartPhoneButtonスクリプトはスマフォのボタンを押した時に実行するスクリプトでボタンを作った後に作成するので置いておきます。
スマフォのディスプレイとボタンの作成
次にスマフォのディスプレイとボタンを作成していきます。
ディスプレイの作成
Canvasの子要素に右クリック→UI→Panelを作成し、名前をDisplayPanelとします。
DisplayPanelのAnchor Presetsで横がstretch、縦がcustomになるようにします。
DisplayPanelの子要素に右クリック→UI→Textを作成します。
TextのAnchor Presetsで横と縦をstretchにし、DisplayPanelの大きさに合わせてサイズが変わるようにします。
Display Panelのサイズを操作して上のような大きさになるようにします。
後でDisplay Panelにはボタンを押した時の数字が次々に表示されるようにします。
数字ボタンの作成
次に数字ボタンを表示するパネルを作成します。
Canvasの子要素にUI→Panelを作成し、名前をNumberButtonPanelとします。
NumberButtonPanelを選択し、インスペクタのAdd ComponentからLayout→Grid Layout Groupを選択し、グリッド上にボタンを自動で並べ替えるようにします。
次にNumberButtonPanelの子要素にUI→Buttonを選択し、名前を1にします。
上のようにButtonのHighlighted Colorを赤色にし、マウスポインタで選択されている時はボタンの色を赤色にします。
ボタン1が出来たら1を選択した状態でCtrl+Dキーでコピーし、0~9のボタンを作成します。
NumberButtonPanelのサイズをシーンビューで操作し上のようにDisplay Panelの下から始まりスマフォ画面の少し上までのサイズにします。
Display画面消去ボタンパネルの作成
最後にスマフォのディスプレイ上に表示された数字を一括で消すボタンを作成します。
Canvasの子要素にUI→Panelを作成し、名前をDeleteButtonPanelとします。
DeleteButtonPanelのサイズをシーンビューで変更し、NumberButtonPanelの下からスマフォの一番下までのサイズに調整します。
上が出来上がったスマフォ画面になります。
ボタン操作の作成
スマフォの画面が出来たので、次はボタンを押した時の操作を作成していきます。
ボタン操作時に実行するスクリプトの作成
スマフォのボタンを押した時に実行する処理はPushSmartPhoneButtonという名前のスクリプトを作成し、Canvasに取り付けます。
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 | using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; public class PushSmartPhoneButton : MonoBehaviour { [SerializeField] private Text smartPhoneDisplay; // Use this for initialization void Start () { smartPhoneDisplay.text = ""; } public void InputNumber(int buttonNum) { smartPhoneDisplay.text += buttonNum; } public void ClearDisplay() { smartPhoneDisplay.text = ""; } } |
処理はシンプルでsmartPhoneDisplayにインスペクタでえDisplay Panelの子要素のTextを設定し、そこに押されたボタンの数字を表示させるだけです。
番号ボタンの設定
ボタンを押した時の処理はCanvasに取り付けたPushSmartPhoneButtonスクリプトで行いますので、後はボタンから該当するメソッドを呼び出すだけです。
上は2のボタンの設定です。
On ClickでCanvasをドラッグ&ドロップし、実行するスクリプトはPushSmartPhoneButtonで実行するメソッドはInputNumberに設定します。
どのボタンを押したかをデータとして渡す必要があるのでそれぞれのボタンの番号に応じた数字を下に設定します。
Deleteボタンの設定
Deleteボタンは番号ボタンとほぼ同じでOn ClickにCanvasを設定し、PushSmartPhoneButtonスクリプトのClearDisplayメソッドを呼び出すだけです。
スマフォのCanvasGroupを操作するスクリプトの作成
SmartPhoneゲームオブジェクトにSmartPhoneUIスクリプトを作成し、取り付けます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | using System.Collections; using System.Collections.Generic; using UnityEngine; public class SmartPhoneUI : MonoBehaviour { private CanvasGroup canvasGroup; // Use this for initialization void Start () { canvasGroup = GetComponentInChildren<CanvasGroup>(); } public void SwitchUI(bool flag) { canvasGroup.interactable = flag; } } |
これはSmartPhoneゲームオブジェクト以下のCanvasGroupを保持しておき、キャラクターがスマフォを見るアニメーションをしたらSwitchUIメソッドを呼び出してスマフォのUIを操作出来るようにするか切り替え出来るようにするスクリプトです。
これでスマフォもどきが出来上がりました!
出来上がったスマフォはAssetsエリアにドラッグ&ドロップしてプレハブにしておきます。
なぜスマフォもどきと言ったかお判りでしょう?
全然スマフォには似ても似つかない代物だからです。(-ω-)/
キャラクターの作成
次はスマフォを持たせるキャラクターを作成していきます。
ヒエラルキー上にスタンダードアセットのEthanを配置します。
CharacterControllerコンポーネントを取り付け、コライダのサイズを調整してください。
AnimatorControllerの作成
次にキャラクター用のAnimatorControllerを作成します。
アニメーションパラメータにFloat型のSpeedとBool型のLookを作成します。
上のように状態と遷移を作成し、Idleは立っている状態、Walkは歩いている状態、Lookはスマフォを見ている状態のアニメーションを設定します。
スマフォを見るアニメーションは
上のようなものを作成しました。
間違えて右手にスマフォを持って左手で操作するようなアニメーションにしてしまったのでUnityのAnimatorControllerのMirrorを使って反転させました。(^_^;)
Idle→WalkはSpeedがGreaterで0.1
Walk→IdleはSpeedがLessで0.1
Idle→LookとWalk→LookはLookがtrue
Look→IdleはLookがfalse
を設定します。
作ったスマフォをキャラクターに持たせる
キャラクター操作スクリプトを作る前に先ほど作ったスマフォをキャラクターに持たせます。
スマフォのプレハブをキャラクターの左手の子要素に配置し位置と角度を調整します。
実際にスマフォの位置と角度を調整した画面は
上のようになります。
位置や角度を合わせるには
を参照してください。
キャラクター操作スクリプトの作成
キャラクターの操作スクリプトを作成していきます。
EthanにSmartPhoneCharaスクリプトを作成し取り付けます。
スクリプトが長いので少しづつ解説します。
宣言部とStartメソッド
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 | using System.Collections; using System.Collections.Generic; using UnityEngine; public class SmartPhoneChara : MonoBehaviour { public enum State { normal, smartPhone }; private CharacterController characterController; private Animator animator; private Vector3 velocity = Vector3.zero; [SerializeField] private float walkSpeed = 2f; // キャラクターの状態 private State state; // IK用フラグ private bool ikFlag; // スマフォのTransform [SerializeField] private Transform smartPhoneTra; // 右手のボーン [SerializeField] private Transform rightHandBone; // 人差し指のボーン [SerializeField] private Transform indexFingerBone; // 手首から人差し指の先のベクトル private Vector3 indexFingerBoneVector; // スマフォのSmartPhoneUIスクリプト [SerializeField] private SmartPhoneUI smartPhoneUI; // Use this for initialization void Start () { characterController = GetComponent<CharacterController>(); animator = GetComponent<Animator>(); state = State.normal; indexFingerBoneVector = indexFingerBone.position - rightHandBone.position; } |
キャラクターの状態を表す列挙型のStateを作成し、通常状態とスマフォを見ている状態の二つを作成します。
ikFlagはスマフォを見るまでのアニメーションが完了したらtrueにし右手をIKを使って動かせるようにします。
smartPhoneTraはスマートフォンのTransformを設定します。
rightHandBoneはEthanRightHandのボーンを設定します。
indexFingerBoneはEthanRightHandIndex4のボーンを設定します。
indexFingerBoneVectorは手首から人差し指の先までのベクトルを計算したものを入れます。
Startメソッドでは初期設定をしていますが、ここで一旦indexFingerBoneVectorに人差し指の先のボーンの位置から右手のボーンの位置を引いてベクトルを計算しています。
indexFingerBoneVectorについては後で解説します。
UpdateメソッドとLateUpdateメソッド
次はUpdateメソッドとLateUpdateメソッドを見ていきます。
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 | // Update is called once per frame void Update() { if (state == State.normal) { if (characterController.isGrounded && !animator.GetBool("Look") && !animator.GetCurrentAnimatorStateInfo(0).IsName("Look") ) { velocity = Vector3.zero; var input = new Vector3(Input.GetAxis("Horizontal"), 0f, Input.GetAxis("Vertical")); if (input.magnitude > 0f) { animator.SetFloat("Speed", input.magnitude); transform.LookAt(transform.position + input); velocity = input.normalized * walkSpeed; } else { animator.SetFloat("Speed", 0f); } } } else if(state == State.smartPhone) { // Look状態でアニメーションを最後まで再生したらカメラをFPSカメラに変換 if(animator.GetCurrentAnimatorStateInfo(0).IsName("Look") && animator.GetCurrentAnimatorStateInfo(0).normalizedTime >= 1f) { Camera.main.GetComponent<ChangeCamera>().SwitchCamera("fps"); ikFlag = true; smartPhoneUI.SwitchUI(true); } } // TPSとFPSの切り替え if (Input.GetButtonDown("Fire2")) { SetState(); } velocity.y += Physics.gravity.y * Time.deltaTime; characterController.Move(velocity * Time.deltaTime); } private void LateUpdate() { // 人差し指の方向を更新 indexFingerBoneVector = indexFingerBone.position - rightHandBone.position; } |
Updateメソッドでは通常のキャラクターの移動処理にスマフォを見ている時の処理を加えています。
移動処理はスマフォを見ている時はさせたくない為、if文の条件でスマフォを見ている状態の時を省いています。
マウスの右クリックをした時にSetStateメソッドを呼び出してTPS→FPSとFPS→TPSを切り替えています。
SetStateでキャラクターの状態をスマフォを見る状態にしてすぐにIKを有効にしたりスマフォを有効にすると動きがおかしくなるので、キャラクターがLook状態の時でアニメーションが最後まで再生された時に初めてikFlagをtrueにし、IKを有効にしています。
メインカメラのChangeCameraスクリプトのSwitchCameraメソッドを呼び出していますが、ChangeCameraスクリプトはまだ作成していないのでスルーしておきます。
LateUpdateメソッドでは人差し指のボーンの位置から右手のボーンの位置を引いてindexFingerBoneVectorの値を更新しています。
これはIKの処理で右手を動かした時の差分を更新する為です。
詳細は後述します・・・・((+_+))
SetStateメソッド
次にSetStateメソッドを見ていきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | private void SetState() { velocity = Vector3.zero; if(state == State.smartPhone) { state = State.normal; Camera.main.GetComponent<ChangeCamera>().SwitchCamera("tps"); ikFlag = false; smartPhoneUI.SwitchUI(false); } else { state = State.smartPhone; } animator.SetBool("Look", !animator.GetBool("Look")); } |
このメソッドでは通常状態とスマフォを見ている状態を切り替えています(他の状態があれば処理を変える必要あり)。
スマフォ状態であれば十条状態に戻し、カメラをTPSモードにしIKを無効、スマフォ操作の無効をしています。
通常状態からスマフォ状態へは状態を変更しているだけです。
今回の場合は2つの状態しかないのでアニメーションパラメータの状態を反転してLookに設定しています。
OnAnimatorIKメソッド
次にOnAnimatorIKメソッドを見ていきます。
OnAnimatorIKメソッドはAnimatorControllerのIK Passにチェックを入れると呼ばれるようになります。
上のようにIK Passにチェックを入れます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | public void OnAnimatorIK() { if (ikFlag) { // 右手のIKのウエイト設定 animator.SetIKPositionWeight(AvatarIKGoal.RightHand, 1f); animator.SetIKRotationWeight(AvatarIKGoal.RightHand, 1f); Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); // カメラからスマフォまでの距離を計算 float distance = Vector3.Distance(Camera.main.transform.position, smartPhoneTra.position); // 右手のIKの設定 animator.SetIKRotation(AvatarIKGoal.RightHand, smartPhoneTra.rotation); animator.SetIKPosition(AvatarIKGoal.RightHand, ray.GetPoint(distance - 0.02f) - indexFingerBoneVector); if(Input.GetButtonDown("Fire1")) { animator.SetIKPosition(AvatarIKGoal.RightHand, ray.GetPoint(distance - 0.02f) - indexFingerBoneVector + Camera.main.transform.forward * 0.005f); } } } } |
ikFlagがtrueの時にIKの処理を実行します。
SetIKPositionWeightとSetIKRotationWeightで右手の位置と角度のIKのウエイトを1に設定しています。
その後
1 2 3 | Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); |
でカメラからスクリーン上のマウスポインタの位置にレイを飛ばします。
1 2 3 | float distance = Vector3.Distance(Camera.main.transform.position, smartPhoneTra.position); |
でカメラの位置とスマフォの位置との距離を計算します。
1 2 3 4 | animator.SetIKRotation(AvatarIKGoal.RightHand, smartPhoneTra.rotation); animator.SetIKPosition(AvatarIKGoal.RightHand, ray.GetPoint(distance - 0.02f) - indexFingerBoneVector); |
で最初に右手のIKの角度をスマートフォンの角度に合わせてスマフォと右手の平が平行になるようにします(スマフォのローカルの角度等によっては面倒くさい計算をする必要があるかも)。
次の行で計算している部分が今回のIKの処理のちょっと苦労した部分です。
UnityでIKを使う時のIKの設定でAvatarIKGoalを使いますが、このAvatarIKGoalで指定出来るのは両手と両足のボーンだけです。
今回は右手の人差し指の先をマウスポインタの位置と合わせたいのですが、先ほど計算したマウスポインタの位置に合わせられるのは右手(の手首)になります。
そこで右手人差し指の先のボーンの位置と右手手首のボーンの位置の差を計算し、マウスポインタの位置に右手人差し指の先のボーンが来るように調整します。
その計算をしたのが先ほどから後で説明しますと言っていたindexFingerBoneVectorです。
このindexFingerBoneVectorの値を計算するのはLateUpdateメソッドで行う必要があります。
これはメソッドの呼び出し順序と関係していると思われますが、UpdateやOnAnimatorIKで計算すると位置がずれます。
そんなわけで計算したマウスポインタの3Dの方向(ray)を使って、その方向での長さをカメラとスマフォの距離で求め、オフセット値(0.02f)で手がスマフォに埋まらないよう(親指が少し埋まるので調整が必要かも)にして計算し、そこにindexFingerboneVectorのベクトルを引いて右手のIKの位置を調整します。
rayはカメラから見たマウスポインタの方向が求まっているのでray.GetPointでその長さを指定し3D座標を求めています。
1 2 3 4 5 | if(Input.GetButtonDown("Fire1")) { animator.SetIKPosition(AvatarIKGoal.RightHand, ray.GetPoint(distance - 0.02f) - indexFingerBoneVector + Camera.main.transform.forward * 0.005f); } |
上はマウスの左クリックをした時に指を押したように見せる為の処理で、右手のIKの位置をカメラが向いている前方に少し押し出すようにしています。
場合によっては処理がうまくいかない可能性もあります。
そんな時は指で押すアニメーションを別途作成し、Unityのアニメーターコントローラーのレイヤー機能とアバターマスクを使ってマウスの左クリックをした時に右手首から先のアニメーションだけを実行するのもいいかも?
カメラの設定
次にカメラの設定をしていきます。
カメラ切り替えスクリプトの作成
MainCameraにChangeCameraスクリプトを作成し取り付けます。
MainCameraにはスタンダードアセットのFollowTargetスクリプトを取り付けてTPSモードの時はカメラがキャラクターを追いかけるように設定してあるとします。
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 | using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityStandardAssets.Utility; public class ChangeCamera : MonoBehaviour { // FPSモード時のカメラを置くボーン [SerializeField] private Transform eyeTra; // TPSモード時のカメラの位置 private Vector3 defaultCameraPos; // TPSモード時のカメラの角度 private Quaternion defaultCameraRot; // FollowTargetスクリプト private FollowTarget followTarget; // Use this for initialization void Start () { defaultCameraPos = transform.position; defaultCameraRot = transform.rotation; followTarget = GetComponent<FollowTarget>(); } public void SwitchCamera(string cameraName) { if(cameraName == "fps") { followTarget.enabled = false; transform.SetParent(eyeTra); transform.position = eyeTra.position; transform.rotation = eyeTra.rotation; } else { transform.SetParent(null); transform.position = defaultCameraPos; transform.rotation = defaultCameraRot; followTarget.enabled = true; } } } |
eyeTraにはインスペクタでEthanの目の位置のゲームオブジェクトを設定します(この後作成します)。
Startメソッドでカメラの位置や角度を保持しておきます。
SwitchCameraメソッドでは受け取った文字列に応じてTPSとFPSを切り替えています。
fpsモード時はMainCameraの親をeyeTraにし、eyeTraの動きに応じてカメラも動くようにしておきます。
tpsモード時は親を解除し、FollowTargetスクリプトを有効にしています。
FPSモード時のカメラの位置を作成
次にFPSモード時のカメラの位置を作成します。
EthanのEthanHead1の子要素に右クリック→Create Emptyを作成し、名前をFPSCameraPosとします。
FPSCameraPosの位置を目の辺りに移動し、ローカル軸の青軸(前方)が目の先になるように角度を変更します。
上のような感じにしました。
後はMainCameraのインスペクタで設定をします。
上のようにFollowTargetの設定とChangeCameraに先ほど作ったFPSCameraPosを設定します。
これで機能が完成しました!
実際に試すとゲーム画面では手元しか映してないので影響はありませんが、もう少し全体像が見えるようだと右ひじ部分が体にめり込んでしまうのが見えてしまう事もあるかもしれません。
そんな時は右手のひじのIKHintを使って右手の肘の方向を調整するといいかもしれません。
上の記事の「IKHintを使い関節の位置と角度を調整する」の項目を参照して作成してみてください。
終わりに
今回の機能を作成していて一番大変だったのがマウスポインタの位置に右手の人差し指の先を持っていく事です。
なぜ大変だったかは記事内で触れていますが、メソッドの実行順序等も相まって手こずりました・・・・(^_^;)
途中でIKはやめて手の2Dアイコンをマウスポインタの部分に合わせる処理に変えちゃおうかとも思いましたが・・・出来てよかったです。(‘ω’)ノ