読者です 読者をやめる 読者になる 読者になる

tanaka's Programming Memo

プログラミングについてのメモ。

(5)ボールを動かす(2015.1改訂版)

←(4)プレイヤーバーの実装(2015.1改訂版)
(6)ブロックを消す(2015.1改訂版)→

ブロック崩しのボールの動きを、自分で実装しようとすると案外大変である。ブロックに横からぶつかったか、縦からぶつかったか、斜めからぶつかったかで跳ね返る方向を変えるには、それらを判定しないといけない。

ところが、物理エンジンを持っているUnityでは、それらを全て自動的に、しかもちゃんとぶつかり方に応じて正確に処理してくれる。リジッドボディ(Rigidbody)とコライダー(Collider)の設定を正しく行えば、あとは初速を与えるだけで跳ね返りも含めて自動的に動いてくれるのである。【Unity】の醍醐味の一つである。それではボールを動かしてみよう。

ボールにリジッドボディを設定

ボールのプレハブにリジッドボディ(Rigidbody)のコンポーネント(Component)を追加する。

  • 【プロジェクト(Project)】ビューから「Ball」プレハブを選択する
  • メニューから【Component】→【Physics】→【Rigidbody】を選択

ジッドボディを設定したら、インスペクター(Inspector)から以下のように設定しよう。

  • Mass
    • ボールの重量。とりあえず1のままにしておく
  • Drag
    • 空気抵抗。不要なので0のまま
  • Angular Drag
    • 回転時の空気抵抗。不要なので0にする
  • Use Gravity
    • 重力落下させないので、チェックを外す
  • Is Kinematic
    • 物理法則を適用するのでチェックを外したまま
  • Interpolate
    • Noneのまま
  • Collision Detection
    • Discreteのまま
  • Constraints
    • Z軸は使わないので、【Freeze Position】【Freeze Rotation】両方の【Z】にチェックを入れる


ボールを発射

まだ細かい設定が残っているが、一先ずボールを発射させてみよう。ボール用のスクリプトを作成して、shotBall()というボールを発射させる関数を作る。発射する方向と速度はインスペクターで設定できるようにする。

スクリプトを作成しよう。

  • 【プロジェクト(Project)】ビューの「Scripts」フォルダを右クリックして、【Create】→【C# Script】を選ぶ
  • 作成したスクリプトのファイル名を「CBall」にする
  • 作成した[CBall]スクリプトをドラッグして、【プロジェクト(Project)】ビューの[Ball]プレハブにドロップする
  • 「CBall」スクリプトを開いて、クラス名が「CBall」になっているかを確認する。「NewBehaviourScript」のままだったら「CBall」に修正する

グローバル変数を作成しよう。

  • 「public class CBall : MonoBehaviour {」という行を探して、その下の行に以下のプログラムを追加する。
	public float INIT_DEGREE = 75f;
	public float INIT_SPEED = 40f;

ボールを発射するshotBall()という関数を作成する。

  • 「CBall」スクリプトの最後の「}」を探して、その上の行に以下のプログラムを追加する。
	/** ボールを発射する*/
	void shotBall() {
		Vector3 vel = Vector3.zero;
		vel.x = INIT_SPEED*Mathf.Cos(INIT_DEGREE*Mathf.PI/180f);
		vel.y = INIT_SPEED*Mathf.Sin(INIT_DEGREE*Mathf.PI/180f);
		rigidbody.velocity = vel;
	}

とりあえずゲームの開始と同時にボールを発射させる。

  • 「void Start() {」という行を探し、その下の行に以下のプログラムを追加する。
		shotBall();

以上でスクリプトは出来上がり。[Ctrl]キー+[S]キーで保存、実行して動きをみてみよう。

ジッドボディとコライダーを調整する

ボールは打ち出せたが、ブロックにぶつかっても跳ね返ってくれない。これはボールやブロックの「弾性係数」が0で、物にぶつかっても、粘土のように衝撃を吸収して跳ね返らないオブジェクトになっているからである。ぶつかった時と同じ速度で跳ね返ることを「完全弾性衝突」といい、Unityでは「弾性係数(Bounceness)」を1にすることで再現できる。

このような衝突時の挙動を決めるパラメータは「Physic Material」というアセット(Asset)で設定する。

ボールの弾性係数を設定する
  • 【プロジェクト(Project)】ビューの【Create】→【Physic Material】を選択する
  • 作成した「New Physic Material」の名前を「phmatBall」などに変更する
  • インスペクターで弾性係数を表す【Bounciness】を1にする
  • [Bounce Combine]を[Maximum]に変更する
    • 跳ね返る際、衝突した両方のオブジェクトのPhysicMaterialのパラメータをどのように利用するかを設定できる。初期値は[Average]で、両者の平均を利用する。これが通常の動作である
    • 今回は、Ballの全ての衝突を完全弾性衝突にしたい。そういう時は[Maximum]を選択すると、BallのBouncenessの1が常に適用されるので、他の設定が不要になって楽である
  • 【プロジェクト(Project)】ビューから「phmatBall」をドラッグして、【プロジェクト(Project)】ビューの「Ball」プレハブにドロップする

設定が完了したら動きを確認してみよう。ブロックに跳ね返るのが確認できるだろう。

Ballプレハブの[Sphere Collider]の[Material]欄に[phmatBall]が設定されていることを確認しよう。

プレイヤーバーの設定を行う

プレイヤーバーでボールを跳ね返そうとしても跳ね返らない。ブロックはRigidbodyを持たないので、しっかりと固定されていることを表している。そのため、ボールの衝突エネルギーは全てボールに戻るので跳ね返る。

それに対して、プレイヤーバーはRigidbodyを持つので、ボールとの作用反作用のシミュレーションが行われる。現状ではボールもプレイヤーも同じ重さ(mass)1なので、ボールの運動エネルギーは全てプレイヤーバーに移動してしまい、ボールが止まってしまうのである。ビリヤードでボールをぶつけると、ぶつけたボールが停止して、もう一方が動くのと同じである。

Sphereを作成して、Rigidbodyを設定、Use Gravityをオフにして、打ち出すボールの軌道上に配置してみよう。ボールが正面からぶつかると、打ち出したボールが停止して、もう一方が動くのが確認できる。

これを修正するには、ボールの重さを小さくして、ボールの運動エネルギーを限りなく小さくすればよい。以下の通り設定してみよう。

  • 【プロジェクト(Project)】ビューの「Ball」を選択する
  • インスペクター(Inspector)から「Rigidbody」コンポーネントの「Mass」を見つけて「0」を設定する。
  • 以下のように「1e-07」となればよい

実行して、プレイヤーバーで跳ね返してみよう。今度はうまく跳ね返るだろう。

ボールの摩擦をなくす

phmatBallのDynamic FrictionやStatic Frictionはそれぞれ、動摩擦係数、静摩擦係数である。これが0になっていない場合は、プレイヤーバーを動かしながらボールを跳ね返すと、ボールの軌道が摩擦で変わる。動きは面白いが、運動エネルギーが回転のエネルギーに使われることでボールの速度が落ちてしまう。それを避けるため、摩擦はなくすことにする。

  • 【プロジェクト(Project)】ビューから[phmatBall]を選ぶ
  • [Dynamic Friction]と[Static Friction]のどちらも「0」にする
  • [Friction Combine]項目を[Minimum]に変更する

以上の変更で、ボールの動摩擦係数、静止摩擦係数とも「0」になる。これだけだと、接触相手の摩擦との平均になって、摩擦が0になるとは限らないので、[Friction Combine]を[Minimum]にした。この設定で、接触時の摩擦係数は、小さい方が適用されるので常に摩擦係数は0になる。

以上でボールの動きの実装完了である。プレイヤーバーの角にボールをぶつけたりしてみよう。物理演算が行われているので、それらしい挙動をするはずである。

スペースキーでボールを発射

ボールの発射と移動が組み込めたので、当初の予定通り、最初はボールはプレイヤーバーにくっついて動いて、スペースキーを押したら発射するようにしよう。

まずは、ゲームの開始と同時にボールを発射していた処理を消す。

  • 「CBall」スクリプトを開く
  • 「Start()」関数に書いた「shotBall();」という行を削除する
  • 上書き保存する。

プレイヤーがボールを発射するようにする。

  • 「CPlayer」スクリプトを開く。
  • 「void Update() {」という行を探して、その下の行に以下のプログラムを追加する。
		// ボールの発射
		if (insBall != null) {
			if (Input.GetButtonDown("Jump")) {
				insBall.SendMessage("shotBall");
				insBall = null;
			}
		}
  • 上書き保存する

プログラムの意味は以下の通り。

if (insBall != null) {
  • プレイヤーバーにボールがくっついている時は「insBall」にはボールのインスタンスが入っているので、null以外になる。このチェックをすることで、プレイヤーバーにボールがくっついているかを判定していることになる
if (Input.GetButtonDown("Jump")) {
  • 【Jump】ボタンが押されたかを確認するif文
  • 【Jump】はキーボードのスペースキーに割り当てられているので、スペースキーを押した時にif文が成立することになる
insBall.SendMessage("shotBall");
  • 「SendMessage()」文は【Unity】独特の命令である
  • 「insBall」には、ボールプレハブのインスタンス(実体)が入っている
  • ボールプレハブには「CBall」スクリプトが登録されている
  • 「SendMessage()」は指定のゲームオブジェクト(今回でいうとボール)にくっついているスクリプト内の関数を呼び出す命令である
  • よって、上記のプログラムで「Ball」が持っている「CBall」スクリプト内の「shotBall()」関数を呼び出すことが出来る
insBall = null;
  • ボールを発射した後に、スペースキーが押されても再度ボールを発射しないようにするための処理

動かして動作を確認してみよう。スペースキーを押すまでボールが留まっているようになった。しかし、ボールがくっついている状態でプレイヤーバーを左右に操作しても、ボールがくっついてこなくなった。

ボールはプレイヤーバーの子オブジェクトになっているのだが、リジッドボディが有効になっているため、プレイヤーバーが動いても外部から力が加えられなければ動かないのである。ボールにくっついている時は、物理シミュレーションを無効にするために【Is Kinematic】をtrueにしよう。

  • 【CPlayer】スクリプトを開く。
  • 「void createHoldBall() {」から始まる行を探して、「}」で閉じられている行の直前に以下のプログラムを追加する。
		// ボールの物理処理を無効にする
		insBall.rigidbody.isKinematic = true;

ボールを発射した後に「isKinematic」をfalseにして、物理シミュレーションを有効にする。

  • 「void Update() {」から始まる行を探して、その中の「if (Input.GetButtonDown("Jump")) {」という行を探す。
  • 見つけた次の行に、以下のプログラムを追加する。
				insBall.rigidbody.isKinematic = false;
  • 上書き保存して【Unity】に戻る。

動かして、ボールがプレイヤーバーにくっついて動くのを確認しよう。

以上で、ボールを跳ね返す処理の実装は完了である。以下にこの時点までの「CBall.cs」と「CPlayer.cs」を示す。非常に短い上に、ボールが動き回るプログラムが一切書かれていないのが衝撃的である。

CBall.cs

using UnityEngine;
using System.Collections;

public class CBall : MonoBehaviour {
	public float INIT_DEGREE = 75f;
	public float INIT_SPEED = 40f;

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

	void shotBall() {
		Vector3 vel = Vector3.zero;
		vel.x = INIT_SPEED * Mathf.Cos (INIT_DEGREE * Mathf.PI / 180f);
		vel.y = INIT_SPEED * Mathf.Sin (INIT_DEGREE * Mathf.PI / 180f);
		rigidbody.velocity = vel;
	}
}

CPlayer.cs

using UnityEngine;
using System.Collections;

public class CPlayer : MonoBehaviour {
	public float VEL = 40f;
	public GameObject prefBall = null;
	GameObject insBall = null;

	// Use this for initialization
	void Start () {
		createHoldBall ();
	}
	
	// Update is called once per frame
	void Update () {
		Vector3 vel = Vector3.zero;
		vel.x = VEL*Input.GetAxisRaw ("Horizontal");
		rigidbody.velocity = vel;

		// ボールの発射
		if (insBall != null) {
			if (Input.GetButtonDown("Jump")) {
				// ボールの物理シミュレーションを有効にする
				insBall.rigidbody.isKinematic = false;

				// ボールを発射する
				insBall.SendMessage ("shotBall");
				insBall = null;
			}
		}
	}

	// ボールを生成して、プレイヤーバーにくっつける
	void createHoldBall() {
		Vector3 bpos = transform.position;
		bpos.y += (collider.bounds.size.y + prefBall.transform.localScale.y) / 2f;
		insBall = (GameObject)Instantiate (prefBall, bpos, Quaternion.identity);
		insBall.transform.parent = transform;

		// ボールの物理シミュレーションを無効にする
		insBall.rigidbody.isKinematic = true;
	}
}

←(4)プレイヤーバーの実装(2015.1改訂版)
(6)ブロックを消す(2015.1改訂版)→