今回は主人公キャラクターがロープ(滑車付きロープでもいいかも)を掴んで下りていく機能を作成します。
ダークソウル2?にあった板が重なり合ったステージで初心者には道案内の白が必要なエリアにあった機能を作ってみました。
わたくしもよく白になって道案内をしていました。
・・・やったことない人にはまったくわからないですね、やった事ある人もわからないかもしれませんが・・・。
とりあえずそのステージにあったロープを掴んで下りる機能を作成していきます。
PS3が壊れたのでどんなんだったか忘れていますが・・・・(^_^;)
まずはロープ等のゲームオブジェクトを作成します。
ロープや坂、ロープを掴むエリアを作成
ロープはCubeのサイズを変更して作成しました。
ロープなのでCylinderで作る事をお勧めします・・・・・(T_T)作った後に気がつくおバカさん
上のように階層を作ります。
RopeSystemは空のゲームオブジェクトで作成し、その子要素にロープ系のゲームオブジェクトを詰め込みます。
RopeSystemの子要素に空のゲームオブジェクトを作成し、名前をMoveHandPosとしてます。MoveHandPosはキャラクターの手の位置を合わせる場所でロープを掴んで降りる時に動かします。
MoveHandPosの子要素にはIKで指定する右手(RightHand)、左手(LeftHand)の位置と角度を合わせる空のゲームオブジェクトを作成します。
最初に設定する位置や角度は後で調整するので大体で設定しておきます。
RopeObj(CubeもしくはCylinderで作成)はロープ自体の見た目のゲームオブジェクト、SearchArea(Cubeで作成)はキャラクターがロープを下る場所に来たかどうかを判断するエリアとして使います。
StartBasePosition(空のゲームオブジェクトで作成)はロープを降りる時の最初のキャラクターの位置で、EndHandPos(空のゲームオブジェクトで作成)はロープを降り切ったとする手の位置になります。
真正面から見ると、
上のような感じになります。
主人公がロープを掴む動作をするエリアSearchAreaにスクリプトを設定
SearchAreaのゲームオブジェクトは上の画像のbaePositionの辺りにCubeを使って作成します。
SearchAreaのMesh FilterとMesh Rendererは使わないので削除します(元々空のゲームオブジェクトを作成しBox Colliderを取り付けるだけでもOK)。
このエリアと接触したらロープを掴み降りていく動作をさせます。
なので、SearchAreaオブジェクトにSearchCharaというスクリプトを作成し設定します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | using UnityEngine; using System.Collections; public class RopeSearchChara : MonoBehaviour { // ロープを掴むエリアに侵入した void OnTriggerEnter(Collider col) { if(col.tag == "Player") { col.gameObject.GetComponent<RopeChara>().SetState(RopeChara.State.Rope); } } } |
今回はOnTriggerEnterでSearchAreaに進入したキャラ(厳密にはコライダ)にPlayerタグが設定されたものだったら、そのゲームオブジェクトに設定されているRopeCharaスクリプトのSetStateメソッドを呼び出しキャラクターの状態を変更します。
キャラクター操作スクリプトにロープを掴んでいる時の処理を追加する
主人公操作スクリプトにロープを掴んで下りていく間は移動機能を無効化する必要があります。
ロープを下りていく間にキャラクターを操作出来ると空中を移動してしまいますからね・・・・(=_=)
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 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 | using UnityEngine; using System.Collections; public class RopeChara : MonoBehaviour { public enum State { Normal, Rope }; private Animator animator; private CharacterController characterController; private Vector3 velocity; [SerializeField] private float jumpPower; private State state; // 右手の位置 [SerializeField] private Transform rightHand; // 左手の位置 [SerializeField] private Transform leftHand; // キャラクターが立つ位置 [SerializeField] private Transform startBasePosition; // キャラクターが最初に立った位置 private Vector3 defaultStartPosition; // キャラクターがロープを離すMoveRopeの位置 [SerializeField] private Transform endHandPos; // MoveHandPosのTransform [SerializeField] private Transform moveHandPos; // MoveHandPosとStartPositionの距離 private float distance; void Start () { animator = GetComponent<Animator>(); characterController = GetComponent<CharacterController>(); velocity = Vector3.zero; state = State.Normal; // 初期状態はState.normalに設定 // ロープを掴んだ時のMoveRopePosの位置を保持しておく defaultStartPosition = moveHandPos.position; // ロープと立っている場所の位置の差を計算 distance = moveHandPos.position.y - startBasePosition.position.y; } void Update () { // 状態がState.normalの時だけ実行 if (state == State.Normal) { if (characterController.isGrounded) { velocity = Vector3.zero; var input = new Vector3 (Input.GetAxis ("Horizontal"), 0f, Input.GetAxis ("Vertical")); // 方向キーが多少押されている if (input.magnitude > 0f && !animator.GetCurrentAnimatorStateInfo (0).IsName ("Jump") && !animator.GetCurrentAnimatorStateInfo (0).IsName ("Attack1") && !animator.GetCurrentAnimatorStateInfo (0).IsName ("Attack2") && !animator.GetCurrentAnimatorStateInfo (0).IsName ("Attack3")) { animator.SetFloat ("Speed", input.magnitude); transform.LookAt (transform.position + input); velocity += input.normalized * 2; // キーの押しが小さすぎる場合は移動しない } else { animator.SetFloat ("Speed", 0); } if (Input.GetButtonDown ("Jump") && !animator.GetCurrentAnimatorStateInfo (0).IsName ("Jump")) { animator.SetBool ("Jump", true); velocity.y += jumpPower; } if (Input.GetButtonDown ("Fire1")) { animator.SetBool ("Attack", true); } } velocity.y += Physics.gravity.y * Time.deltaTime; characterController.Move (velocity * Time.deltaTime); // ロープを掴んでいる状態 } else if (state == State.Rope) { // MoveRopePosを移動させる moveHandPos.position = Vector3.Lerp(moveHandPos.position, endHandPos.position, Time.deltaTime); // キャラの位置をMoveRopePosと足元の差を考慮し移動させる transform.position = moveHandPos.position - new Vector3(0f, distance, 0f); // MoveHandPosが目的地に近付いたらロープを掴む動作をやめる if(Vector3.Distance(moveHandPos.position, endHandPos.position) < 0.1f) { SetState(RopeChara.State.Normal); } } } // 状態設定関数 public void SetState(State state) { this.state = state; if (state == State.Normal) { ResetHandPos (); } else if (state == State.Rope) { animator.SetFloat("Speed", 0); SetRopePosition (); } } void OnAnimatorIK() { // ロープを掴む動作をしている時はIKを使い両手の位置と角度をRightHand、LeftHandに合わせる if(state == State.Rope) { animator.SetIKPositionWeight(AvatarIKGoal.RightHand, 1); animator.SetIKPositionWeight(AvatarIKGoal.LeftHand, 1); animator.SetIKRotationWeight(AvatarIKGoal.RightHand, 1); animator.SetIKRotationWeight(AvatarIKGoal.LeftHand, 1); animator.SetIKPosition(AvatarIKGoal.RightHand, rightHand.position); animator.SetIKPosition(AvatarIKGoal.LeftHand, leftHand.position); animator.SetIKRotation(AvatarIKGoal.RightHand, rightHand.rotation); animator.SetIKRotation(AvatarIKGoal.LeftHand, leftHand.rotation); } } // キャラクターの位置を最初の位置に設定 public void SetRopePosition() { transform.position = startBasePosition.position; transform.rotation = startBasePosition.rotation; } // 移動してしまったMoveRopeの位置を元の位置に戻す void ResetHandPos() { moveHandPos.position = defaultStartPosition; } } |
ロープを掴んでいる状態を追加しています。
キャラクター移動処理はState.Normalの時だけ実行し、ロープを掴んで下りている時は動作しないようにします。
ロープを掴んでいる間は重力も働かせないようにします。
StartメソッドではMoveHandPosの位置とStartBasePositionの位置の差を取得しておきます。
これはロープを掴んでいる時にMoveHandPosを動かしますが、それに合わせてキャラクターの位置も動かしたい為、最初にその差を取得しておき、
キャラクターの位置はMoveHandPosにその差を加味した位置に移動させる為です。
またMoveHandPosは移動させるので、キャラクターがロープを離した時に元の位置に戻す必要があります。
その為、MoveHandPosの最初の位置はdefalutStartPositionにあらかじめ保持しておきます。
キャラクターがState.Ropeの状態の時はVector3.Leapで徐々にEndHandPosに移動させます。
ある程度EndHandPosに近付いたらキャラクターの状態をState.Normalに戻します。
RightHand、LeftHandの位置や角度もUnityの実行時に位置と角度を調整します。
RightHand、LeftHandは実行時にインスペクタのTransformの歯車をクリックしCopyComponentを選択した後実行を解除し、Transformの歯車からPasteComponentValuesを選択し実行時の値をペーストします。
SetStateメソッドではノーマル状態とロープを掴んだ状態に応じて設定を変更しています。
SetRopePositionメソッドはロープのサーチエリア内に入った時、キャラクターの位置と角度をStartBasePositionと合わせます。
その為、StartBasePositionのローカルのZ方向(青い矢印)はロープを下っていく方向に回転させておきます。
こうすることでキャラクターがロープのサーチエリアに入ったらロープを降りる方向に向くようになります。
ResetHandPosメソッドは動かしたMoveHandPosを元の位置に戻す処理です。
機能の確認をしてみる
これでロープを掴んで下りる機能が完成しました。
HandIKは
↑のように設定します。
それでは実行して確認してみましょう。
上のように手はロープを掴みキャラクターも移動します。
上半身のアップだけだと分かりづらいので全体を確認します。
上のようにロープを掴んで下りてくる動作が確認出来ました。
ロープを下りる時は専用のカメラに切り替えて主人公の頭の後ろから覗くような感じにするのもいいかもしれません。
ロープの移動で距離を計ったり、検知エリアを使ってロープを早く離す
キャラクターが目的地付近に来るとものすごくスピードが遅くなりロープを離すまでがもどかしい!!という方は
1 2 3 | if(Vector3.Distance(moveHandPos.position, endHandPos.position) < 0.1f) |
としている0.1fのところの数字を大きくすると目的地より前のスピードがある時にロープを離します。
これでは目的地の所で離さず手前で離してしまって本来の目的地とは違う場所になります。
それが嫌だという方は目的地との距離ではなく検知エリアを作りそこにキャラクターが入ったらロープを離すようにするといいかもしれません。
MoveTowardsを使って同じ速度でロープの移動を行う
もっと簡単に等速度でロープを降りたい時はVector3.MoveTowardsを使います。
1 2 3 | moveHandPos.position = Vector3.Lerp(moveHandPos.position, endHandPos.position, Time.deltaTime); |
とVector3.Lerpでなめらかに移動させている箇所を
1 2 3 | moveHandPos.position = Vector3.MoveTowards(moveHandPos.position, endHandPos.position, Time.deltaTime * 5); |
とします。
Time.deltaTimeのままだと遅いので5をかけました。
MoveTowardsを使うと、
↑のように同じ速度で最初の位置から目的地の位置まで移動するようになります。
ちょっとだけダークソウル風のロープを降りる機能が出来ました!
アクションゲームでロープをつたって降りる機能を作りたい時は参考にしてみてください。