[Unity] Cubeを転がすスクリプト

Colorful Diceを作った時の話ですが、思ったようにCubeを転がすのって意外と難しいですね。

物理エンジンに任せればなんとかなると思って始めましたが、そんなに簡単じゃなかったというお話。

20160426_001

上記のように適当にPlaneとCubeを配置して、CubeにRigidbodyと下記のスクリプトをCube付与します。

using UnityEngine;
using System.Collections;

public class move1 : MonoBehaviour {

	public float speed = 1;

	// Use this for initialization
	void Start () {
	
	}
	
	// Update is called once per frame
	void Update () {

		if (Input.GetAxisRaw ("Horizontal") != 0) {
			GetComponent<Rigidbody> ().angularVelocity = Vector3.right * Input.GetAxisRaw ("Horizontal") * speed;

		} else if (Input.GetAxisRaw ("Vertical") != 0) {
			GetComponent<Rigidbody> ().angularVelocity = Vector3.forward * -Input.GetAxisRaw ("Vertical") * speed;

		}
	}

}

これだけで、下記のようにヌルっとCubeが横に回転して移動してくれます。

ただ、移動した後の座標がちょっとキリのいい値からずれます。

今回だと、

Position: (0, 0.5, 1)

Rotation: (90, 0, 0)

となって欲しかったわけです。これは、回転時の床との摩擦だったり、回転が終わるときにはねたりする挙動が影響してるんだと思います。さらにスクリプトリファレンスによるとそもそもangularVelocityを直接セットするのは良くないみたいですね。非現実的な挙動につながるから。たしかに、止まってる車が次の瞬間に時速100kmになってるなんてこと現実にはないですからね。addTorqueという関数もあるようですが、多分結果は同じでしょう。わずかなズレですが、何度も移動しているとこれが馬鹿にならなくなってきます。さらに回転速度を早くすると更にズレが大きくなりますし、早過ぎると飛び上がります(これはこれで結構面白い: 下記動画)。

というわけで、結局物理エンジン任せじゃなくて、Script書いてpositionとrotationを制御することにしました。位置と回転は簡単な数学ですね。

20160426_005

上図を元にしたスクリプト例が下記。

using UnityEngine;
using System.Collections;

public class move2 : MonoBehaviour {

	public float rotationPeriod = 0.3f;		// 隣に移動するのにかかる時間
	public float sideLength = 1f;			// Cubeの辺の長さ

	bool isRotate = false;					// Cubeが回転中かどうかを検出するフラグ
	float directionX = 0;					// 回転方向フラグ
	float directionZ = 0;					// 回転方向フラグ

	Vector3 startPos;						// 回転前のCubeの位置
	float rotationTime = 0;					// 回転中の時間経過
	float radius;							// 重心の軌道半径
	Quaternion fromRotation;				// 回転前のCubeのクォータニオン
	Quaternion toRotation;					// 回転後のCubeのクォータニオン

	// Use this for initialization
	void Start () {

		// 重心の回転軌道半径を計算
		radius = sideLength * Mathf.Sqrt (2f) / 2f;

	}

	// Update is called once per frame
	void Update () {

		float x = 0;
		float y = 0;
		
		// キー入力を拾う。
		x = Input.GetAxisRaw ("Horizontal");
		if (x == 0) {
			y = Input.GetAxisRaw ("Vertical");
		}


		// キー入力がある かつ Cubeが回転中でない場合、Cubeを回転する。
		if ((x != 0 || y != 0) && !isRotate) {
			directionX = y;																// 回転方向セット (x,yどちらかは必ず0)
			directionZ = x;																// 回転方向セット (x,yどちらかは必ず0)
			startPos = transform.position;												// 回転前の座標を保持
			fromRotation = transform.rotation;											// 回転前のクォータニオンを保持
			transform.Rotate (directionZ * 90, 0, directionX * 90, Space.World);		// 回転方向に90度回転させる
			toRotation = transform.rotation;											// 回転後のクォータニオンを保持
			transform.rotation = fromRotation;											// CubeのRotationを回転前に戻す。(transformのシャローコピーとかできないんだろうか…。)
			rotationTime = 0;															// 回転中の経過時間を0に。
			isRotate = true;															// 回転中フラグをたてる。
		}
	}

	void FixedUpdate() {

		if (isRotate) {

			rotationTime += Time.fixedDeltaTime;									// 経過時間を増やす
			float ratio = Mathf.Lerp(0, 1, rotationTime / rotationPeriod);			// 回転の時間に対する今の経過時間の割合

			// 移動
			float thetaRad = Mathf.Lerp(0, Mathf.PI / 2f, ratio);					// 回転角をラジアンで。
			float distanceX = -directionX * radius * (Mathf.Cos (45f * Mathf.Deg2Rad) - Mathf.Cos (45f * Mathf.Deg2Rad + thetaRad));		// X軸の移動距離。 -の符号はキーと移動の向きを合わせるため。
			float distanceY = radius * (Mathf.Sin(45f * Mathf.Deg2Rad + thetaRad) - Mathf.Sin (45f * Mathf.Deg2Rad));						// Y軸の移動距離
			float distanceZ = directionZ * radius * (Mathf.Cos (45f * Mathf.Deg2Rad) - Mathf.Cos (45f * Mathf.Deg2Rad + thetaRad));			// Z軸の移動距離
			transform.position = new Vector3(startPos.x + distanceX, startPos.y + distanceY, startPos.z + distanceZ);						// 現在の位置をセット

			// 回転
			transform.rotation = Quaternion.Lerp(fromRotation, toRotation, ratio);		// Quaternion.Lerpで現在の回転角をセット(なんて便利な関数)

			// 移動・回転終了時に各パラメータを初期化。isRotateフラグを下ろす。
			if (ratio == 1) {
				isRotate = false;
				directionX = 0;
				directionZ = 0;
				rotationTime = 0;
			}
		}
	}
}

CubeのBoxColliderのIsTriggerにチェックを入れて、RigidbodyのUseGravityのチェックを外しておきます。これで下記のようにコロコロと転がります。

思うようにcubeを転がすのも案外むずかしいものですね。

(2016.9.13追記)

立方体だけじゃなく直方体でも転がるようにして欲しいというご要望があったので、改良してみました。改良したスクリプトは以下の記事に掲載しています。

[Unity] 直方体を転がすスクリプト

10 COMMENTS

西野

ブログ記事ありがとうございます!
非常に参考になるスクリプトの掲載ありがとうございました。

現在、Unity5.3.2で箱状のオブジェクトを、キーボード操作で転がすスクリプトを書いております。
こちら立方体ですと問題ないのですが、直方体ですと計算式を理解できていないため修正できません。

こちら1:1:2(高さ)のような直方体の場合、どうすればよいかアドバイスをいただけないでしょうか。
よろしければで構いませんので、ご確認頂けますと助かります。

お忙しい中、ご連絡してしまい申し訳ございません。

piromayo

素人のスクリプトですが、参考にしていただいているということでありがとうございます。

とりあえず直方体対応のスクリプトを
http://piromayo.xyz/2016/09/13/unity-%E7%9B%B4%E6%96%B9%E4%BD%93%E3%82%92%E8%BB%A2%E3%81%8C%E3%81%99%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%97%E3%83%88/
に公開しました。
あまり深く考えずにパッと書いただけなので、他によい方法があるかとおもいます。

計算式としては、上の四角が転がる図で回転半径とスタートする角度を直方体に対して計算して求めるという事になります。新しいスクリプトではキー入力されるごとに直方体の各辺の長さと転がる向きの位置関係を求めています。

参考にしてみてください。

西野

ご連絡が大変遅くなってしまい申し訳ございません。
参考になるものまで助かりました!こういったものを作れるのがすごいですね。
こちら本当にありがとうございます。ご確認させてくださいませ。

バニラ

とても参考になりました!ありがとうございます!
質問なのですが、こちらのスクリプトを付与したcubeをプレイヤーとしたとき壁とのあたり判定がうまくいきません。
どうすればよいかアドバイスをいただけないでしょうか。
お暇な時で構いませんので確認していただけると助かります。

piromayo

うーん。このスクリプト使って私もゲーム作ってますが、普通にあたり判定ができています。
ちょっとバニラさんの状況がわからないのでなんとも言えませんが、ぱっと思いつくものとしては、
当たり判定部分(Collider)の大きさをCubeよりもちょっとだけ大きくしてみるとか、Layer Collision Matrixを確認してみるとかですかねぇ。

バニラ

返信ありがとうございます。
参考にさせていただきます!

まめひこ

piromayoさん、はじめまして。二年も前の記事にすみません。
自分のやりたい表現そのものだったので興奮してしまいました、、
私も、ツールは違うんですが(Cinema4dとか3dsmaxです)こういった挙動を物理エンジンではなく表現したいです。
ただ、スクリプトに落とし込む以前にそもそも数学的知識が無さすぎて、
なにをどう計算したらキューブの位置や回転をコントロールできるのかわかりません。
piromayoさんが”位置と回転は簡単な数学ですね。”と仰ってる辺りですね、、
この数学の部分を学ぶのにおすすめの本など教えていただけませんか?
ググるにも数学のどの部分が、こういう物体の挙動に関わってくるのかわからず、
なにを勉強すればいいのかすらわかっていません。

本当にアホな質問で申し訳ありませんが、なにか教えていただけると嬉しいです。
長文すみません。

piromayo

まめひこさん、お問い合わせありがとうございます。

ここで使っているのは単純な三角関数なので、(昔のことで忘れましたが)高校あたりの数学の教科書を学びなおすのが良いのかなと思います。
クォータニオンっていうのは私も実はよくわかっていません。回転に関するデータを持っているんだろうな程度です。
私は読んだことはありませんが、「数学ガールの秘密ノート/丸い三角関数」とか「坂田アキラの 三角関数が面白いほどわかる本」など、Amazonの評価が高くて良さそうです。(読んだことないので保証しません。)

sin, cos, tanさえわかっていれば、あとは応用だと思います。

まめひこ

お忙しいところ、返信ありがとうございます。
このような質問にもお答えいただいて本当に感謝です。。。
紹介していただいた本も含めて、三角関数付近を勉強しようと思います。
ありがとうございました。

現在コメントは受け付けておりません。