今回は、崖から崖へといった高い所から高い所へとジャンプでは渡れない場所があった時、ロープに捕まって渡れるような機能を作成してみたいと思います。
ゴッドオブウォーやゴッドオブウォー2やゴッドオブウォー3にあるような鎖を掴んで飛ぶようなやつですね。
それ以外のゲームが思いつきませんでした・・・・(^_^;)
キャラクターにジャンプ機能を搭載していれば穴がある場所も飛ぶことは出来ますが、ジャンプでは到達出来ない部分に行く為に上からロープや鎖のようなものがぶら下がっており、それを使ってブランコの要領?で飛び越えられるような機能です。
今回の機能を作成すると、
↑のような感じでロープを掴んで移動するようなアクションゲームに使えると思います。
ロープを掴んで崖を渡る機能の作成
それではロープを掴んで崖を渡る機能を作成していきましょう。
崖とロープを含んだステージの作成
まずは、崖とロープのゲームオブジェクトを配置していきます。
サンプルなので崖はCubeで作成し、ロープはCylinderで作成します。
ヒエラルキーで右クリック→Create Emptyで空のゲームオブジェクトを作成し、名前をStageとします。
Positionは(0, 0, 0)、Rotationを(0, 0, 0)、Scaleを(1, 1, 1)とします。
機能が完成したら位置を変更しても問題はありません。
Stageの子要素にCubeで崖を作成します。
↑のようにStageの子要素にCubeで作成します。
↑が作成した崖のサンプルです。
崖はただの土台として作成しているだけなので、今回の機能とはあまり関係がありません。
崖以外の地面はPlaneで作成し、ワールド空間に作成します。
キャラクターはStandardAssetsのEthanを配置し、AnimatorControllerの設定をしておきます。
キャラクター操作スクリプトは、後で作りますのでおいておきます。
次にロープを作成します。
Stageの子要素に右クリック→3D Object→Cylinderを作成し名前をRopeとし、Scaleを(0.1, 1.5, 0.1)とします。
RopeのCapsule ColliderのIs Triggerにチェックを入れ、物理的に当たらないようにしておきます。
その後Ropeの子要素にCreate Emptyで空のゲームオブジェクトを作成し、名前をRopeBaseとします。
RopeBaseの位置を調整しRopeの上に来るようにします。
RopeBaseはRopeを回転させる時のBaseの位置になるもので、後でRopeをRopeBaseの子要素にします。
RopeBaseを選択した状態で位置をRopeの上になるように調整します。
調整が終了したらRopeBaseをドラッグしStageの上でドロップします。
するとRopeとの親子関係が解除されるので、次はRopeをドラッグしRopeBaseの上でドロップします。
これでRopeBaseが親、Ropeが子になりました。
なぜこんな面倒くさい事をしたかというと、RopeBaseの位置をRopeの上に合わせる為です。
RopeBaseとRopeの親子関係を逆にしたので、RopeBaseのScaleを(1, 1, 1)、Ropeを(0.1, 1.5, 0.1)に変更します。
ここまできたらRopeBaseの位置を調整し、崖と崖の間に移動させます。
崖やロープにマテリアルを設定し、見た目を変えてみてもいいですね。
これで崖とロープが完成しました。
ロープを動かす
ロープが完成したので、ロープを左右に動かすようにしましょう。
RopeBaseの前方は↑のように青軸の向きとなっています。
つまりロープを左右に振るにはこの青軸を中心に回転させればいい事になります。
新しくMoveRopeスクリプトを作成し、RopeBaseに取り付けます。
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 UnityEngine; using System.Collections; public class MoveRope : MonoBehaviour { // 進んでいる方向 private int direction = 1; // Z軸の角度 private float angle = 0f; // 動き始める時の時間 private float startTime; // 補間間隔 [SerializeField] private float duration = 5f; // Z軸で振り子をする角度 [SerializeField] private float limitAngle = 90f; void Start() { startTime = Time.time; } // Update is called once per frame void Update () { // 経過時間に合わせた割合を計算 float t = (Time.time - startTime) / duration; // スムーズに角度を計算 angle = Mathf.SmoothStep (angle, direction * limitAngle, t); // 角度を変更 transform.localEulerAngles = new Vector3(0f, 0f, angle); // 角度が指定した角度と1度の差になったら反転 if (Mathf.Abs (Mathf.DeltaAngle (angle, direction * limitAngle)) < 1f) { direction *= -1; startTime = Time.time; } } // 進んでいる向きを返す(実際にはint値) public int GetDirection() { return direction; } } |
↑のように経過時間を感覚時間で割った秒数に応じて現在の角度(angle)からdirection * limitAngleの角度の間でスムーズに補間する角度を計算しangleに代入します。
ロープの角度が限界角度の絶対値との差が1度より下になったら向きを反転させています。
ベースのキャラクター操作スクリプト
ロープに捕まり移動するキャラクターを作る前に、とりあえず移動とジャンプが出来ないといけません。
そこでベースとなるスクリプトを作成します。
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 | using UnityEngine; using System.Collections; public class JumpToRopeChara : MonoBehaviour { private Animator animator; private CharacterController cCon; private Vector3 velocity; [SerializeField] private float jumpPower = 5f; private Vector3 input; [SerializeField] private float walkSpeed = 2f; void Start () { animator = GetComponent<Animator>(); cCon = GetComponent<CharacterController>(); velocity = Vector3.zero; } void Update () { // キャラクターコライダが接地 if (cCon.isGrounded) { // 地面に接地してる時は初期化 velocity = Vector3.zero; 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 * walkSpeed; // キーの押しが小さすぎる場合は移動しない } else { animator.SetFloat ("Speed", 0); } // ジャンプ if (Input.GetButtonDown ("Jump") && !animator.GetCurrentAnimatorStateInfo (0).IsName ("Jump") && !animator.IsInTransition (0)) { velocity.y += jumpPower; } } velocity.y += Physics.gravity.y * Time.deltaTime; cCon.Move(velocity * Time.deltaTime); } } |
スクリプトの詳細については
や
を参照してください。
今回はこのスクリプトに処理を追加していきます。
さてどれから作成しましょうか・・・・(-_-)
キャラクターをロープに張りつかせる
キャラクターがロープにジャンプした時に、キャラクターがロープと一緒に動くようにしなければいけません。
そこでロープにキャラクターを検知するエリアを作成し、キャラクターが侵入した時にロープに張りつかせるようにしましょう。
ロープに張りつかせるというのは難しい感じがしますが、実は簡単です。
キャラクターをロープの子要素に配置してしまえば、ロープと一緒に動くキャラクターになります。
まずはRopeBaseにAdd ComponentからPhysics→Box Colliderを選択し取りつけます。
Is Triggerにチェックを入れ物理的に当たらないようにします。
↑のような感じのキャラクター検知エリアとなるように、Box ColliderのCenterとSizeを調整します。
RopeBaseには、CatchTheRopeというキャラクターが侵入した事を検知するスクリプトを作成し取りつけます。
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 CatchTheRope : MonoBehaviour { // キャラクターの到達点 [SerializeField] private Transform arrivalPoint; void OnTriggerEnter(Collider col) { if (col.tag == "Player" && col.GetComponent <JumpToRopeChara>().GetState () != JumpToRopeChara.State.catchRope ) { // キャラクターの親をロープにする col.transform.SetParent (transform); // キャラクターにCatchTheRopeスクリプトを渡し、状態を変更する var jumpToRope = col.GetComponent <JumpToRopeChara> (); jumpToRope.SetState (JumpToRopeChara.State.catchRope, this); } } // ロープに記憶しておくキャラクターの状態をセット public void SetState(State sta) { state = sta; } } |
OnTriggerEnterメソッドでキャラクターが侵入してきた事を検知し、自身が保持しているキャラクターの状態がState.catchRopeでなければ、
キャラクターの親要素をこのスクリプトを設置しているRopeBaseにします。
その後、キャラクター操作スクリプトのSetStateメソッドを呼び出し、キャラクターの状態を変更しています。
SetStateはまだ定義していないのでこれから定義します。
処理の順番の関係で、キャラクターをロープの子要素にしてから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 | public class JumpToRopeChara : MonoBehaviour { public enum State { normal, catchRope, } private State state; void Start () { state = State.normal; } void Update () { // 通常時だけ移動やジャンプが出来る if (state == State.normal) { // キャラクターコライダが接地、またはレイが地面に到達している場合 if (cCon.isGrounded) { //キャラクターの移動やジャンプ等の処理 } } } public void SetState(State sta) { state = sta; if (state == State.catchRope) { // 現在の角度を保持しておく preRotation = transform.rotation; animator.SetFloat ("Speed", 0f); velocity = Vector3.zero; // 移動値等の初期化 var rot = transform.localEulerAngles.y; // 角度を設定し直す transform.localRotation = Quaternion.Euler (0f, rot, 0f); // キャラクターを到達点に動かすフラグオン moveFlag = true; } } } |
キャラクターの状態を表すStateを宣言し、Startメソッド内でState.normalを設定します。
Updateメソッドでは、キャラクターの状態がState.normalの時だけ実行するように変更します。
SetStateメソッドでは、キャラクターの状態を変更し、AnimatorControllerのアニメーションパラメータの設定や移動値の初期化等を行っています。
これでキャラクターがロープの検知エリアに侵入したら、キャラクターがロープの子要素になりキーボードでの移動やジャンプ、重力が働かなくなります。
それでは実行してみましょう。
↑のように、キャラクターがロープと接触するとロープの子要素となり一緒に動いています。
ヒエラルキーを確認するとRopeBaseの子要素にキャラクターがいますね。
キャラクターの位置と角度を合わせる
先ほどキャラクターをロープの子要素にする事が出来ましたが、キャラクターの角度や位置がロープに合っていません。
キャラクターの到達点をRopeBaseの子要素に作成し、ロープを掴んだ状態の時は徐々に移動させるようにします。
また、キャラクターの角度はY座標だけを回転させロープの角度と合わせます。
まずはRopeBaseの子要素に到達点であるArrivalPointを作ります。
RopeBaseの子要素に空オブジェクトを作成し、名前をArrivalPointとします。
↑のように子要素にArrivalPointが作成されました。
ArrivalPointを移動させキャラクターをロープの子要素にした時に、キャラクターの足元が来る位置にArrivalPointを合わせます。
次にJumpToRopeCharaスクリプトに追加していきます。
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 | // ロープの所定の位置に動いているか? private bool moveFlag = false; // CatchTheRopeスクリプト private CatchTheRope rope; // ロープを動かすスクリプト private MoveRope moveRope; // ロープの所定の位置までのスピード [SerializeField] private float speedToRope = 5f; // ロープの所定の位置に動いているか? private bool moveFlag = false; void Update () { if (state == State.normal) { // ノーマル状態の時の処理 } else if (state == State.catchRope) { if (moveFlag) { if (transform.localPosition != rope.GetArrivalPoint ()) { // 滑らかに決められた位置に移動させる transform.localPosition = Vector3.Lerp (transform.localPosition, rope.GetArrivalPoint (), speedToRope * Time.deltaTime); } else { moveFlag = false; } } } } public void SetState(State sta, CatchTheRope catchTheRope = null) { state = sta; if (state == State.catchRope) { // 現在の角度を保持しておく preRotation = transform.rotation; animator.SetFloat ("Speed", 0f); velocity = Vector3.zero; // 移動値等の初期化 var rot = transform.localEulerAngles.y; // 角度を設定し直す transform.localRotation = Quaternion.Euler (0f, rot, 0f); // キャラクターを到達点に動かすフラグオン moveFlag = true; SetCatchTheRope (catchTheRope); } } public void SetCatchTheRope(CatchTheRope rope) { // CatchTheRopeとMoveRopeスクリプトの取得 this.rope = rope; moveRope = this.rope.GetComponent<MoveRope> (); } |
ロープを掴んでいる状態の時で、キャラクター位置が到達点に達していない時は、キャラクターの位置を少しづつ到達点に移動させます。
speedToRopeはインスペクタで設定出来るようにし、到達点までのスピードを設定します。
到達点に達したらmoveFlagをfalseにし、到達点移動処理の実行を止めます。
次にCatchTheRopeスクリプトを修正します。
1 2 3 4 5 6 7 8 9 10 | // キャラクターの到達点 [SerializeField] private Transform arrivalPoint; // 到達点を返す public Vector3 GetArrivalPoint() { return arrivalPoint.localPosition; } |
CatchTheRopeスクリプトでは、ArrivalPointをインスペクタで設定出来るようにし、そのローカル位置を返すメソッドを定義します。
これで処理の修正が終わりました。
CatchTheRopeスクリプトのarrivalPointにArrivalPointゲームオブジェクトを設定し、JumpToRopeCharaスクリプトのspeedToRopeに1を設定し確認してみます。
speedToRopeに1を設定した理由は到達点に至る過程を確認する為です。
本来であれば、もう少し数値を大きくし到達点に早く移動出来るようにします。
それでは実行してみましょう。
試しに横向きでジャンプしてみました。
ロープに接触すると、段々と到達点(ArrivalPoint)に移動していくのがわかると思います。
これで、キャラクターの位置と角度をロープと合わせる事が出来ました。
手をロープに合わせる
ここまでで、キャラクターをロープに合わせて動かす事が出来ました。
しかし、キャラクターは棒立ちのままなのでキャラクターがロープを掴むようにしてみます。
これにはIKを使って手を所定の位置に設定するようにします。
IKを使うと本来のアニメーションとは別に手の位置や角度、肘の方向を所定の位置に固定する事が出来ます。
IKに関しては
等を参考にしてください。
まずはRopeBaseの子要素にIKで使う右手、左手、右ひじ、左ひじ交互に見て(謎)
の位置や角度を表す空のゲームオブジェクトを作ります。
親はSphereで作成し、MeshRendererのチェックを外します。
親のゲームオブジェクトも空のゲームオブジェクトで作成してもいいんですが、位置等を確認する時にSphereの方が便利かと思ってそうしました。
親の名前をIKとし、その子要素に右手の位置や角度を設定するRightHand、左手のLeftHand、右ひじの方向を表すRightElbow、左ひじの方向を表すLeftElbowの空のゲームオブジェクトを作成します。
位置や角度は、スクリプトでIKを有効にしてから調整しますのでおいておいてください。
IKを使うには、AnimatorのベースとなるレイヤーのIK Passにチェックを入れます。
これでAnimatorを設定しているゲームオブジェクトのスクリプトで、OnAnimatorIKやOnAnimatorMove等のメソッドが呼ばれるようになります。
IKはこれらのメソッドの中で設定します。
まずは、CatchTheRopeスクリプトにIK関連の処理を追記します(追記処理だけ載せています)。
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 | // IK関連の大元 [SerializeField] private Transform ik; // IKゲームオブジェクトの初期の角度 private Quaternion initRotation; // 右手情報 [SerializeField] public Transform rightHand; // 左手情報 [SerializeField] private Transform leftHand; // 右ひじのHint [SerializeField] private Transform rightElbow; // 左ひじのHint [SerializeField] private Transform leftElbow; void Start() { // IKの初期の角度を保持 initRotation = ik.localRotation; } void OnTriggerEnter(Collider col) { if (col.tag == "Player" && col.GetComponent <JumpToRopeChara>().GetState () != JumpToRopeChara.State.catchRope ) { // IKゲームオブジェクトの角度を初期化 ik.localRotation = initRotation; // キャラクターに合わせてIKの親元を回転 ik.localRotation = ik.localRotation * Quaternion.LookRotation(transform.InverseTransformDirection (col.transform.right)); } } // 右手の情報を返す public Transform GetRightHand() { return rightHand; } // 左手の情報を返す public Transform GetLeftHand() { return leftHand; } // 右ひじの情報を返す public Transform GetRightElbow() { return rightElbow; } // 左ひじの情報を返す public Transform GetLeftElbow() { return leftElbow; } |
親のIKや右手、左手のゲームオブジェクトのTransformをインスペクタで設定出来るようにしておきます。
大元のIKゲームオブジェクトは、キャラクターがロープを掴んだ時の向きによって回転させるので、初期位置をStartメソッドで入れています。
キャラクターがロープを掴んだ時に、IKのローカル角度にキャラクターの右手方向のローカル方向の角度をかけた値を再度IKのローカル角度に代入しています。
ここら辺は難しいので、とりあえず元のIKの角度をキャラクターの向きに合わせて回転させる処理だと思ってください。
次にキャラクター操作スクリプトJumpToRopeCharaに処理を追記します。
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 | // IKのウエイト private float weight = 0f; void Update () { } else if (state == State.catchRope) { if (moveFlag) { if (transform.localPosition != rope.GetArrivalPoint ()) { // 滑らかに決められた位置に移動させる transform.localPosition = Vector3.Lerp (transform.localPosition, rope.GetArrivalPoint (), speedToRope * Time.deltaTime); weight = Mathf.Lerp (weight, 1f, speedToRope * Time.deltaTime); } else { moveFlag = false; } } } } void OnAnimatorIK() { // ロープを掴んだ状態 if (state == State.catchRope) { // 右手、左手、右ひじ、左ひじの位置ウエイトを設定 animator.SetIKPositionWeight (AvatarIKGoal.RightHand, weight); animator.SetIKPositionWeight (AvatarIKGoal.LeftHand, weight); animator.SetIKHintPositionWeight (AvatarIKHint.RightElbow, weight); animator.SetIKHintPositionWeight (AvatarIKHint.LeftElbow, weight); // 右手、左手の角度ウエイトを設定 animator.SetIKRotationWeight (AvatarIKGoal.RightHand, weight); animator.SetIKRotationWeight (AvatarIKGoal.LeftHand, weight); // 右手、左手、右ひじ、左ひじの位置を設定 animator.SetIKPosition (AvatarIKGoal.RightHand, rope.GetRightHand ().position); animator.SetIKPosition (AvatarIKGoal.LeftHand, rope.GetLeftHand ().position); animator.SetIKHintPosition (AvatarIKHint.RightElbow, rope.GetRightElbow ().position); animator.SetIKHintPosition (AvatarIKHint.LeftElbow, rope.GetLeftElbow ().position); // 右手、左手の角度を設定 animator.SetIKRotation (AvatarIKGoal.RightHand, rope.GetRightHand ().rotation); animator.SetIKRotation (AvatarIKGoal.LeftHand, rope.GetLeftHand ().rotation); } } |
IK用のウエイト変数を用意し、ロープを掴んでいる間は0→1へとウエイトを足していきます。
OnAnimatorIKメソッド内でウエイトを設定し、右手や左手の位置や角度をRightHandやLeftHandの位置や角度に設定しています。
ウエイトはどれだけその位置や角度にするかの値で、0だとIKが働かず、1だと完全にその位置や角度になります。
つまり、徐々にIKを働かせる事によって、手の位置を少しづつRightHandやLeftHandに近づけているわけです。
徐々に手の位置を動かさなくてもいい場合は、ウエイトを1にするだけです。
それでは実行してみましょう。
↑のようにキャラクターの方向に合わせてIKゲームオブジェクトが回転し、手の位置が合うようになっています。
途中ジャンプしてもロープに接触出来てないですが・・・まぁ気にしないでください・・・・。
Is Triggerにチェックを入れるのを忘れていただけです・・・・(;一_一)
先にすでにRightHandやLeftHandの位置や角度を調整した状態を紹介しましたが、通常であれば手の位置や角度がおかしなところを向いているはずです。
IK用のゲームオブジェクトの位置や角度を調整します。
右側の崖から左向きに飛んだ時に、右手の位置や角度、左手の位置や角度が合うように調整してください。
調整の仕方はUnityを実行し、キャラクターを右側のがけから左にむかせロープに接触させます。
実行中のままRightHandの位置や角度を調整すると、キャラクターの右手も動きますので、RightHandのインスペクタの歯車をクリックしCopy Componentをします。
Unityの実行を停止したあと、RightHandの歯車からPaste Component Valuesを選択し値をペーストします。
面倒ではありますがRightHand、LeftHand、RightElbow、LeftElbowを一つ一つ実行、停止してベストな位置と角度を探します。
これでロープを掴む処理が完成しました。
ロープを離した時の処理を追加する
ここまででロープを掴んで手の位置や角度を合わせる事まで出来ました。
しかし、キャラクターがロープを掴んだまま離す事が出来ません。
そこで、Jumpキーを押した時にロープを離すようにしたいと思います。
ただ、ロープを離した時にキャラクターの状態をState.normalにするだけでは、ロープの振りの力をキャラクターの移動値に反映出来ません。
そこで、ロープを離してから地面に触れるまではロープを離した状態(State.releaseRope)にし、ロープが動いている方向にキャラクターを動かすことにします。
例えば、左から右へロープが移動している場合は左から右へ移動値を加えます。
右から左へロープが移動している場合は左へ移動値を加えます。
JampToRopeCharaスクリプトに追記します。
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 | public enum State { normal, catchRope, releaseRope } // ロープを掴んだ時の主人公の角度 private Quaternion preRotation; // ロープが進んでいる向き [SerializeField] private float xDirection; void Update () { } else if (state == State.catchRope) { if (Input.GetButtonDown ("Jump")) { SetState (State.releaseRope); } // 到達点に移動させる処理やIKのウエイトの処理 } else if (state == State.releaseRope) { transform.localRotation = Quaternion.Lerp (transform.localRotation, preRotation, speedToRope * Time.deltaTime); // ロープの動いている速度を取得 Vector3 velocityXZ = (moveRope.transform.right * xDirection * releasePower); // Y軸方向は重力に任せる為0にする velocityXZ.y = 0f; // ロープを離した時のロープが動いている速度と重力を足して全体の速度を計算 velocity = velocityXZ + new Vector3 (0f, velocity.y, 0f); // 移動値を減少させる xDirection = Mathf.Lerp (xDirection, 0f, dampingTime * Time.deltaTime); // 重力を働かせる velocity.y += Physics.gravity.y * Time.deltaTime; cCon.Move (velocity * Time.deltaTime); // 着地したらノーマル状態にする if (cCon.isGrounded) { // 先にロープのキャラクター状態をノーマルに SetState (State.normal); } } } public void SetState(State sta, CatchTheRope catchTheRope = null) { state = sta; if (state == State.catchRope) { // 現在の角度を保持しておく preRotation = transform.rotation; animator.SetFloat ("Speed", 0f); velocity = Vector3.zero; // 移動値等の初期化 var rot = transform.localEulerAngles.y; // 角度を設定し直す transform.localRotation = Quaternion.Euler (0f, rot, 0f); // キャラクターを到達点に動かすフラグオン moveFlag = true; SetCatchTheRope (catchTheRope); } else if (state == State.releaseRope) { transform.SetParent (null); weight = 0f; // ロープを離した時の向きを保持 if (moveRope.GetDirection () == 1) { xDirection = 1; } else { xDirection = -1; } moveRope.SetMoveFlag (false); } else if (state == State.normal) { rope = null; moveFlag = true; transform.rotation = preRotation; } } |
ロープを掴んでいる状態の時にJumpボタンを押したら、キャラクターの状態をState.releaseRopeにします。
キャラクターの状態がState.releaseRopeの時は、キャラクターの角度をロープを掴む前の状態に徐々に変化させます。
MoveRopeのdirectionによって分岐させ、移動値を加える方向を算出してます。
キャラクターに移動値を加える方向はロープを離した時の方向なのでそれをxDirectionに保持しておきます。
また、移動値には重力値も加えて下方向に移動させています。
地面に接地したらキャラクターの状態をState.normalにし、通常の移動やジャンプが出来るようにしています。
SetStateメソッドでは、キャラクターがロープを離した時に親子関係を解除したり、ロープを離した時のアニメーションの遷移をさせたりしています。
キャラクターがノーマル状態になったら、強制でキャラクターの角度をロープを掴む前の角度に設定しています。
これで機能が出来ました。
JumpToRopeCharaのインスペクタのspeedToRopeの値を5にして実行してみます。
↑のようにキャラクターがロープを離した時に、ロープの向かっている方向に移動値を加える事が出来ました。
ロープが停止している状態から動かす
ロープが停止している時にキャラクターがロープに触れたら接触した方向へ動き出すようにしてみます。
またキャラクターがロープを離した後は、ロープが自然に停止するようにします。
ロープを動かすMoveRopeスクリプトの改造
それではまずMoveRopeを改造していきましょう。
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 | // Z軸で振り子をする初期の限界角度 [SerializeField] private float defaultLimitAngle = 90f; // ロープを元の位置に戻すスピード [SerializeField] private float undoSpeed = 2f; // ロープが動いているかどうか [SerializeField] private bool moveFlag = false; void Start () { startTime = Time.time; limitAngle = defaultLimitAngle; } void Update () { // ロープを掴んでいる時のロープの動き if (moveFlag) { // 経過時間に合わせた割合を計算 float t = (Time.time - startTime) / duration; // スムーズに角度を計算 angle = Mathf.SmoothStep (angle, direction * limitAngle, t); // 角度を変更 transform.localEulerAngles = new Vector3 (0f, 0f, angle); // 角度が指定した角度と1度の差になったら反転 if (Mathf.Abs (Mathf.DeltaAngle (angle, direction * limitAngle)) < 1f) { direction *= -1; startTime = Time.time; } // ロープを離している時 } else { // 初期の角度になるまで振り子を繰り返す if (transform.localEulerAngles.z != 0) { // 徐々に限界角度を小さくする if (limitAngle > 0f) { limitAngle -= undoSpeed * Time.deltaTime; } // 経過時間に合わせた割合を計算 float t = (Time.time - startTime) / duration; // スムーズに角度を計算 angle = Mathf.SmoothStep (angle, undoDirection * limitAngle, t); // 角度を変更 transform.localEulerAngles = new Vector3 (0f, 0f, angle); // 角度が指定した角度と1度の差になったら反転 if (Mathf.Abs (Mathf.DeltaAngle (angle, undoDirection * limitAngle)) < 1f) { undoDirection *= -1; startTime = Time.time; } } } } // 進んでいる向きを返す(実際にはint値) public int GetDirection() { return direction; } // ロープ機能の検知エリアによってロープを動かす向きを指定 public void SetDirection(int value) { direction = value; } // ロープの動きを切り替える時に呼び出す public void SetMoveFlag(bool flag) { moveFlag = flag; limitAngle = defaultLimitAngle; // ロープを離した時 if (!flag) { // ロープを離した時用の方向値にdirectionを入れる undoDirection = direction; // ロープを掴んだ時 } else { startTime = Time.time; } } public bool GetMoveFlag() { return moveFlag; } |
defaultLimitAngleはロープの限界の角度のデフォルト値で、limitAngleは現在のロープの限界角度です。
キャラクターがロープを離した時はこのlimitAngleの値を段々小さくすることでロープの動きを弱めていきます。
その弱めていくスピードがundoSpeedになります。
moveFlagがtrueの時はキャラクターがロープを掴んでいる時でロープを動かします。
ロープを離した時はlimitAngleの値を徐々に小さくし、ロープの動きを弱めます。
SetDirectionメソッドはキャラクターが飛び乗った方向を指定する時に使用します。
SetMoveFlagはmoveFlagのオンオフをするメソッドでそれ用の設定変更を行います。
directionをロープを離した時にも使用すると後で困りますので・・・・別のフィールドに保持しておきます。
ロープとキャラクターが接触した時のCatchTheRopeスクリプトの改造
次にCatchTheRopeに追記します。
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 | private MoveRope moveRope; void Start() { moveRope = GetComponent<MoveRope> (); } void OnTriggerEnter(Collider col) { if (col.tag == "Player" && col.GetComponent <JumpToRopeChara>().GetState () != JumpToRopeChara.State.catchRope && col.GetComponent<JumpToRopeChara>().GetState() != JumpToRopeChara.State.releaseRope ) { // キャラクターの親をロープにする col.transform.SetParent (transform); // キャラクターにCatchTheRopeスクリプトを渡し、状態を変更する var jumpToRope = col.GetComponent <JumpToRopeChara> (); jumpToRope.SetState (JumpToRopeChara.State.catchRope, this); // IKゲームオブジェクトの角度を初期化 ik.localRotation = initRotation; // キャラクターに合わせてIKの親元を回転 ik.localRotation = ik.localRotation * Quaternion.LookRotation(transform.InverseTransformDirection (col.transform.right)); // ロープを動かす moveRope.SetMoveFlag (true); } } |
キャラクターの状態がロープを掴んでいる時やロープを離した後地面に接地していないときは何もしないようにします。
それ以外でロープに接触したらMoveRopeスクリプトのSetMoveFlagの引数にtrueを渡してロープが動き出すようにします。
ロープの動かす方向を設定するRopeAreaスクリプトの作成
次にロープにキャラクターが接触した向きによってロープを動かす方法を決めるので、検知エリアを左右に作成しそこに侵入した時にMoveRopeスクリプトのSetDirectionメソッドを呼び出し方向を設定することにします。
↑のようにRightAreaとLeftAreaという名前の空のゲームオブジェクトを作成し、BoxColliderを取り付けIs Triggerにチェックを入れて検知範囲とします。
RightAreaとLeftAreaにはRopeAreaスクリプトを取り付けます。
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; public class RopeArea : MonoBehaviour { [SerializeField] private JumpToRopeChara jumpToRopeChara; [SerializeField] private MoveRope moveRope; void OnTriggerEnter(Collider col) { // ロープを掴んでいない時 if (col.tag == "Player" && jumpToRopeChara.GetState() != JumpToRopeChara.State.catchRope) { // 侵入したエリアに応じてロープを動かす向きを変更する(ここでは値を変更しているだけ) if (gameObject.name == "RightArea") { moveRope.SetDirection (1); } else { moveRope.SetDirection (-1); } } } } |
Playerタグを設定したキャラクターが検知範囲に入ったら、そのゲームオブジェクトの名前でロープを動かす方向の設定をしています。
実際の検知エリアの領域は
↑のような感じに作成しました。
検知エリアを作るのではなくキャラクターがロープのエリアのどこに接触したかで判断し動かす向きを設定する事も出来そうですが、難しいので今回は検知エリアで方向を決めてしまいます。
これで機能が完成しました。
実行して確認しましょう。
↑のようになりました。
ロープをHingeJointとRigidbodyを使って動かす
2019/07/17に追記した内容になります。
ここまででキャラクターがロープに飛んで掴み離すという機能は出来ましたが、ロープを動かすのにスクリプトで細かく制御していて分かり辛くなっています。
そこでロープにはHingeJointとRigidbodyを取り付け、キャラクターがロープと接触した時にロープに力を加えて動かすだけでロープ自体の動きをスクリプトで動かさないようにしてみます。
まずはロープに上る階段とロープを1セットにして名前をJointRopePrefabとし、Assetsエリア内にドラッグ&ドロップをします。
子要素にあるどちらの階段から侵入したか検知しているRightAreaとLeftAreaは使わないので削除します(上の画像では削除済みです)。
RopeBaseに設定していたMoveRopeとCatchTheRopeスクリプトを削除します。
RopeBaseのTagに新しくRopeを作成し設定しておきます。
RopeBaseにインスペクタのAdd ComponentからHinge Jointを取り付けると一緒にRigidbodyも取り付けられます。
RigidbodyのUse Gravityにチェックを入れて重力を働かせ、Dragを0.1にして空気抵抗を加えます。
Hinge JointのUse Limitにチェックを入れMinに―90、Maxに90を設定しロープが回転しすぎないようにします。
RopeBaseに新しくRopeScriptを作成し取り付けます。
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 | using System.Collections; using System.Collections.Generic; using UnityEngine; public class RopeScript : MonoBehaviour { // キャラクターの到達点 private Transform arrivalPoint; // IK関連の大元 private Transform ik; // IKゲームオブジェクトの初期の角度 private Quaternion initRotation; // 右手情報 private Transform rightHand; // 左手情報 private Transform leftHand; // 右ひじのHint private Transform rightElbow; // 左ひじのHint private Transform leftElbow; private Rigidbody rigid; private float oldAngle; // Start is called before the first frame update void Start() { arrivalPoint = transform.Find("ArrivalPoint"); ik = transform.Find("IK"); rightHand = ik.Find("RightHand"); leftHand = ik.Find("LeftHand"); rightElbow = ik.Find("RightElbow"); leftElbow = ik.Find("LeftElbow"); rigid = GetComponent<Rigidbody>(); } // Update is called once per frame void Update() { oldAngle = transform.eulerAngles.z; } // 到達点を返す public Vector3 GetArrivalPoint() { return arrivalPoint.localPosition; } // 右手の情報を返す public Transform GetRightHand() { return rightHand; } // 左手の情報を返す public Transform GetLeftHand() { return leftHand; } // 右ひじの情報を返す public Transform GetRightElbow() { return rightElbow; } // 左ひじの情報を返す public Transform GetLeftElbow() { return leftElbow; } public void AlignDirectionOfIK(Transform characterTransform) { // IKゲームオブジェクトの角度を初期化 ik.localRotation = initRotation; // キャラクターに合わせてIKの親元を回転 ik.localRotation = ik.localRotation * Quaternion.LookRotation(transform.InverseTransformDirection(characterTransform.transform.right)); } public void Swing(Vector3 direction, float swingPower) { var angle = transform.eulerAngles.z; // -90から90度の範囲に補正する if(270f <= angle && angle <= 360f) { angle -= 360f; } if (270f <= oldAngle && oldAngle <= 360f) { oldAngle -= 360f; } if (Vector3.Dot(direction, transform.right) >= 0f) { if (-90f <= angle && angle < 0f && angle > oldAngle ) { rigid.AddForce(direction * swingPower); } } else if(Vector3.Dot(direction, transform.right) < 0f) { if (0f <= angle && angle <= 90f && angle < oldAngle ) { rigid.AddForce(direction * swingPower); } } } } |
RopeScriptはCatchTheRopeスクリプトに記述していた内容を一部移しました。
またインスペクタで設定していた手の位置等はStartメソッドで階層下から設定することにします。
oldAngleはUpdateメソッドが呼ばれる度にロープのRotationのZの値を入れます。
AlignDirectionOfIKメソッドではキャラクターがロープに飛び乗った時にIKの角度をキャラクターの向きに合わせて変更する為の処理です。
Swingメソッドはキャラクターがロープに捕まっている時にブランコを漕ぐのと同じようにロープに力を加えた時に呼び出す処理です。
引数ではキャラクターの前方方向と漕ぐ力を受け取ります。
角度の計算ではロープの下向きが0でX軸の正の方向が0~90、負の方向が270~360になるので、-90から90の間に補正しています。
ロープに力を加える時はキャラクターの前方方向とロープの前方方向(この記事ではX軸の方に動く)からVector3.Dotで内積を求め0より大きければキャラクターの横より前方、0より小さければキャラクターの横より後方になります。
つまりキャラクターの前方方向とロープの前方からキャラクターがどっちからロープに捕まっているかを計算しています。
その向きに合わせてロープを漕げる角度の範囲を指定しロープに力を加えています。
文章では分かり辛いので画像で説明します。
内積が0以上の時は下のようにキャラクターが右を向いていて、赤い四角で囲まれている部分で上から下にロープが回転している時がロープを漕げる場所になり、
内積が0より小さい時は下のようにキャラクターが左を向いていて、赤い四角で囲まれている部分で上から下にロープが回転している時にロープを漕げます。
ロープが上から下へと回転している時に前回のロープのZの角度と今のロープの角度を調べています。
次に主人公の操作スクリプトとロープに接触した時のスクリプトを新しく作ります。
主人公操作スクリプトはJumpToRopeChara2でロープに接触した時のスクリプトはCatchTheRope2という名前にします(名前はちゃんとした名前を付けてください)。
主人公にこの二つのスクリプトを取り付けます。
JumpToRopeChara2スクリプトはJumpToRopeCharaスクリプトとほとんど同じです。
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 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 | using UnityEngine; using System.Collections; using System; public class JumpToRopeChara2 : MonoBehaviour { public enum State { normal, catchRope, releaseRope } private Animator animator; private CharacterController cCon; private Vector3 velocity; [SerializeField] private float jumpPower = 5f; [SerializeField] private float walkSpeed = 2f; private State state; // ロープの所定の位置に動いているか? private bool moveFlag = false; // RopeDataスクリプト private RopeScript ropeScript; // ロープを動かすスクリプト private MoveRope moveRope; // ロープの所定の位置までのスピード [SerializeField] private float speedToRope = 5f; // IKのウエイト private float weight = 0f; // ロープを掴んだ時の主人公の角度 private Quaternion preRotation; // ロープを離した時にその向きに加える力 [SerializeField] private float releasePower = 2f; // ロープを離した時の力を減衰させる時間 [SerializeField] private float dampingTime = 2f; // ブランコを漕ぐ力 [SerializeField] private float swingPower = 200f; void Start() { animator = GetComponent<Animator>(); cCon = GetComponent<CharacterController>(); velocity = Vector3.zero; state = State.normal; } void Update() { if (state == State.normal) { // キャラクターコライダが接地 if (cCon.isGrounded) { // 地面に接地してる時は初期化 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 * walkSpeed; // キーの押しが小さすぎる場合は移動しない } else { animator.SetFloat("Speed", 0); } // ジャンプ if (Input.GetButtonDown("Jump") && !animator.GetCurrentAnimatorStateInfo(0).IsName("Jump") // && !animator.IsInTransition (0) ) { velocity.y += jumpPower; } } velocity.y += Physics.gravity.y * Time.deltaTime; cCon.Move(velocity * Time.deltaTime); } else if (state == State.catchRope) { if (moveFlag) { if (transform.localPosition != ropeScript.GetArrivalPoint()) { // 滑らかに決められた位置に移動させる transform.localPosition = Vector3.Lerp(transform.localPosition, ropeScript.GetArrivalPoint(), speedToRope * Time.deltaTime); weight = Mathf.Lerp(weight, 1f, speedToRope * Time.deltaTime); } else { moveFlag = false; } } if (Input.GetButtonDown("Jump")) { SetState(State.releaseRope); } else if(Input.GetKeyDown("return")) { ropeScript.Swing(transform.forward, swingPower); } } else if (state == State.releaseRope) { // 元の角度に戻していく transform.localRotation = Quaternion.Lerp(transform.localRotation, preRotation, speedToRope * Time.deltaTime); // 重力を働かせる velocity.y += Physics.gravity.y * Time.deltaTime; cCon.Move(velocity * Time.deltaTime); // 着地したらノーマル状態にする if (cCon.isGrounded) { // 先にロープのキャラクター状態をノーマルに SetState(State.normal); } } } public void SetState(State sta, RopeScript tempRopeData = null) { state = sta; if (state == State.catchRope) { // 現在の角度を保持しておく preRotation = transform.rotation; animator.SetFloat("Speed", 0f); velocity = Vector3.zero; // 移動値等の初期化 var rot = transform.localEulerAngles.y; // 角度を設定し直す transform.localRotation = Quaternion.Euler(0f, rot, 0f); // キャラクターを到達点に動かすフラグオン moveFlag = true; SetRopeData(tempRopeData); cCon.enabled = false; } else if (state == State.releaseRope) { transform.SetParent(null); weight = 0f; cCon.enabled = true; } else if (state == State.normal) { ropeScript = null; moveFlag = true; transform.rotation = preRotation; } } public State GetState() { return state; } public void SetRopeData(RopeScript rope) { // RopeDataスクリプトの設定 this.ropeScript = rope; } void OnAnimatorIK() { // ロープを掴んだ状態 if (state == State.catchRope) { // 右手、左手、右ひじ、左ひじの位置ウエイトを設定 animator.SetIKPositionWeight(AvatarIKGoal.RightHand, weight); animator.SetIKPositionWeight(AvatarIKGoal.LeftHand, weight); animator.SetIKHintPositionWeight(AvatarIKHint.RightElbow, weight); animator.SetIKHintPositionWeight(AvatarIKHint.LeftElbow, weight); // 右手、左手の角度ウエイトを設定 animator.SetIKRotationWeight(AvatarIKGoal.RightHand, weight); animator.SetIKRotationWeight(AvatarIKGoal.LeftHand, weight); // 右手、左手、右ひじ、左ひじの位置を設定 animator.SetIKPosition(AvatarIKGoal.RightHand, ropeScript.GetRightHand().position); animator.SetIKPosition(AvatarIKGoal.LeftHand, ropeScript.GetLeftHand().position); animator.SetIKHintPosition(AvatarIKHint.RightElbow, ropeScript.GetRightElbow().position); animator.SetIKHintPosition(AvatarIKHint.LeftElbow, ropeScript.GetLeftElbow().position); // 右手、左手の角度を設定 animator.SetIKRotation(AvatarIKGoal.RightHand, ropeScript.GetRightHand().rotation); animator.SetIKRotation(AvatarIKGoal.LeftHand, ropeScript.GetLeftHand().rotation); } } public float GetSwingPower() { return swingPower; } } |
キャラクターの状態がロープを掴んでいる時にEnterキー(return)を押したらRopeScriptのSwingメソッドでロープを漕ぎます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | } else if (state == State.catchRope) { if (moveFlag) { if (transform.localPosition != ropeScript.GetArrivalPoint()) { // 滑らかに決められた位置に移動させる transform.localPosition = Vector3.Lerp(transform.localPosition, ropeScript.GetArrivalPoint(), speedToRope * Time.deltaTime); weight = Mathf.Lerp(weight, 1f, speedToRope * Time.deltaTime); } else { moveFlag = false; } } if (Input.GetButtonDown("Jump")) { SetState(State.releaseRope); } else if(Input.GetKeyDown("return")) { ropeScript.Swing(transform.forward, swingPower); } |
SetStateメソッドでキャラクターがロープを掴んだ状態に変更する時にCharacterControllerを無効化し、ロープを離した時に有効にする処理を記述しています。
CharacterControllerのコライダが邪魔してロープに影響を与えてしまう為です。
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 | public void SetState(State sta, RopeScript tempRopeData = null) { state = sta; if (state == State.catchRope) { // 現在の角度を保持しておく preRotation = transform.rotation; animator.SetFloat("Speed", 0f); velocity = Vector3.zero; // 移動値等の初期化 var rot = transform.localEulerAngles.y; // 角度を設定し直す transform.localRotation = Quaternion.Euler(0f, rot, 0f); // キャラクターを到達点に動かすフラグオン moveFlag = true; SetRopeData(tempRopeData); cCon.enabled = false; } else if (state == State.releaseRope) { transform.SetParent(null); weight = 0f; cCon.enabled = true; } else if (state == State.normal) { ropeScript = null; moveFlag = true; transform.rotation = preRotation; } } |
SetRopeDataでRopeScriptを保持します。
GetSwingPowerメソッドでロープを漕ぐ力を返します。
1 2 3 4 5 6 7 8 9 10 | public void SetRopeData(RopeScript rope) { // RopeDataスクリプトの設定 this.ropeScript = rope; } public float GetSwingPower() { return swingPower; } |
次にCatchTheRope2スクリプトを見ていきます。
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 | using UnityEngine; using System.Collections; public class CatchTheRope2 : MonoBehaviour { void Start() { } // Blockレイヤーを持つゲームオブジェクトと接触したら力を加える void OnControllerColliderHit(ControllerColliderHit col) { if (col.gameObject.tag == "Rope") { var jumpToRope = GetComponent<JumpToRopeChara2>(); if (jumpToRope.GetState() != JumpToRopeChara2.State.catchRope && jumpToRope.GetState() != JumpToRopeChara2.State.releaseRope ) { // キャラクターの親をロープにする transform.SetParent(col.transform); // キャラクターにCatchTheRopeスクリプトを渡し、状態を変更する jumpToRope.SetState(JumpToRopeChara2.State.catchRope, col.transform.GetComponent<RopeScript>()); var ropeScript = col.transform.GetComponent<RopeScript>(); // 手の位置と角度をキャラクターの向きに応じて合わせる為にSetIKを呼ぶ ropeScript.AlignDirectionOfIK(transform); // ロープに力を加える ropeScript.GetComponent<Rigidbody>().AddForce(transform.forward * jumpToRope.GetSwingPower()); } } } } |
OnControllerColliderHitはCharacterControllerで移動中に他のコライダと接触した時に呼ばれます(動いていない時は呼ばれないので別途それ用の処理が必要かもしれません)。
キャラクターがRopeという名前のタグを設定されたコライダと接触したらキャラクターの親をロープにします。
キャラクター操作スクリプトのSetStateで状態と接触したロープのRopeScriptを引数として渡します。
RopeScriptと同じゲームオブジェクトに設定されているRigidbodyにAddForceメソッドで力を加えます。
これで機能が完成しました。
上のようになります。
ロープにHinge JointとRigidbodyを付けるだけで振り子運動をさせることが出来ますし、
ロープ自体をスクリプトを使って動かすのではなくキャラクターが接触した時とロープを漕いだ時だけロープのRigidbodyに力を加えるだけなので結構楽ですね。
ロープを漕ぐ時にアニメーションも変化させてアニメーションに合わせてロープに力を加えるとより良くなりそうですね。
終わりに
今回の機能は、前々から作ろう作ろうと思っていた機能です。
もう少し簡単にこの機能を作れると思っていたんですが、結構大変でした・・・・。
かなり苦労したけど、この記事のアクセスも燦々たるものになるんでしょうね・・・・・・・・・・( ノД`)シクシク…
まぁ苦労していない記事など存在していませんが・・・・(+_+)