当たり判定とアニメーションイベントとレイヤー

Geprüft mit Version: 5.5

-

Schwierigkeitsgrad: Anfänger

シューティングでは「弾に当たったらアウト」というように、当たり判定が存在します。 Unityでは当たり判定にCollider(コライダー)という判定機能を持つコンポーネントを使用します。

Collider

Collider(コライダー)はRigidbodyの物理挙動と強く関係しています。Rigidbodyは物理法則に従って動かすものですがColliderは衝突(当たり)判定を可能にするものになります。 詳しくは、Physics Componentsを御覧ください。

5.1 プレイヤーに当たり判定を付ける

PlayerにBox Collider 2Dをアタッチします。 SizeX 0.06 Y 0.06で、(おおよそ)4ドットの大きさの当たり判定にします。


図5.1:

トリガーにする

Playerをトリガーにします。Box Collider 2DIs Triggerにチェックを付けてください。


図5.2:

Trigger (トリガー)

通常、コライダー同士はぶつかり反発します。ですが反発の必要としないものに関してはトリガーにします。トリガーにするにはコライダーのIs Triggerにチェックを入れるだけです。そうするとコライダー同士はすり抜けます。トリガーは名前の通り何らかの別のイベントを起こすトリガーのために使用してください。

5.2 エネミーに当たり判定を付ける

EnemyにPolygon Collider 2Dをアタッチします。ですが、コライダーの大きさがエネミーの大きさに会わず不自然なものとなってしまいます。


図5.3:

Polygon Collider 2Dはメッシュを基準としてコライダーの形状を生成する

デフォルトではメッシュを基準としてコライダーが生成されます。メッシュの形状はTexture Import Settingsである程度編集可能です。


図5.4: Texture TypeをAdvancedにしてMeshTypeやExtrude Edgesを編集する

そこでコライダーの形状を編集してみましょう。編集はshiftを押しながら編集することが可能です。

Polygon Collider 2Dを編集する

コライダーの編集はPolygon Collider 2Dコンポーネントの「Edit Collider」ボタンを押すことで有効になります。

Polygon Collider 2Dコンポーネントの「Edit Collider」ボタン
図5.5: Polygon Collider 2Dコンポーネントの「Edit Collider」ボタン

編集の仕方

マウスをコライダーの緑色の線に近づけると緑色の■が表示されるので、これをクリックするとその部分を頂点として動かすことが可能です。新しい頂点を作りたい場合は、ハイライトされている緑の辺にカーソルを近づけると新しい■が表示されるので、これをドラッグすればOKです。■を削除したい場合は、Ctrl(Cmd)を押しながらカーソルを■に近づけると赤く表示されるので、その状態でクリックすれば削除出来ます。


図5.6:


図5.7:

トリガーにする

Enemyをトリガーにします。Polygon Collider 2DIs Triggerにチェックを付けてください。


図5.8:

5.3 弾に当たり判定を付ける

コライダーをアタッチ

PlayerBulletの2つのBulletにはPolygon Collider 2Dを、EnemyBulletにはCircle Collider 2Dをアタッチします。 Circle Collider 2DのRudiusを0.14にしてスプライトと同じ大きさにしましょう。


図5.9:

同じ形状のコライダーを作成するのは手間がかかるので片方ができたらCopy ComponentでコピーしてPaste Component Valuesを使うようにしましょう。

コンポーネントをコピーする


図5.11のようにコンポーネントの文字上で右クリック、または歯車をクリックするとメニューが表示されます。 このメニューはコンポーネントを操作するためのものです。


図5.10:

同じ内容のコンポーネントを追加する
図5.11: 同じ内容のコンポーネントを追加する

コンポーネントの値を同じにする
図5.12: コンポーネントの値を同じにする

トリガーにする

コライダーをトリガーにします。PlayerBulletの2つのBulletとEnemyBulletのコライダーのIs Triggerにチェックを入れてください。


図5.13:


図5.14:

ここまで出来たらPrefabを更新してそれぞれの弾をシーン上から削除しましょう。

5.4 スクリプトから当たり判定を検出する

スクリプトからトリガーの当たり判定を取得するには「OnTriggerEnter2D」「OnTriggerStay2D」「OnTriggerExit2D」の3つを使用します。

「OnTriggerEnter2D」「OnTriggerStay2D」「OnTriggerExit2D」

トリガーの当たり判定を取得するメソッドは「OnTriggerEnter2D」「OnTriggerStay2D」「OnTriggerExit2D」の3種類あります。これらは条件に合わせて使い分けるようにしてください。 詳しくはMonoBehaviourを御覧ください。

今回は「プレイヤーは何かに当たったら爆発する」という仕様のもと実装していきます。 まず、Spaceship.csにExplosionゲームオブジェクトを作成するためのコードを記述します。

Spaceship.cs


using UnityEngine;

[RequireComponent(typeof(Rigidbody2D))]
public class Spaceship : MonoBehaviour
{
  // 移動スピード
  public float speed;
  
  // 弾を撃つ間隔
  public float shotDelay;
  
  // 弾のPrefab
  public GameObject bullet;
  
  // 弾を撃つかどうか
  public bool canShot;
  
  // 爆発のPrefab
  public GameObject explosion;
  
  // 爆発の作成
  public void Explosion ()
  {
    Instantiate (explosion, transform.position, transform.rotation);
  }
  
  // 弾の作成
  public void Shot (Transform origin)
  {
    Instantiate (bullet, origin.position, origin.rotation);
  }
  
  // 機体の移動
  public void Move (Vector2 direction)
  {
    GetComponent<Rigidbody2D>().velocity = direction * speed;
  }
}


PlayerとEnemyのそれぞれ、インスペクター上でSpaceshipにExplosionのPrefabを格納してください。


図5.15:

Playerの当たり判定を検出するために、コードを追加します。

Player.cs


using UnityEngine;
using System.Collections;

public class Player : MonoBehaviour
{
  // Spaceshipコンポーネント
  Spaceship spaceship;

  IEnumerator Start ()
  {
    // Spaceshipコンポーネントを取得
    spaceship = GetComponent<Spaceship> ();

    while (true) {

      // 弾をプレイヤーと同じ位置/角度で作成
      spaceship.Shot (transform);

      // shotDelay秒待つ
      yield return new WaitForSeconds (spaceship.shotDelay);
    }
  }

  void Update ()
  {
    // 右・左
    float x = Input.GetAxisRaw ("Horizontal");

    // 上・下
    float y = Input.GetAxisRaw ("Vertical");

    // 移動する向きを求める
    Vector2 direction = new Vector2 (x, y).normalized;

    // 移動
    spaceship.Move (direction);
  }

  // ぶつかった瞬間に呼び出される
  void OnTriggerEnter2D (Collider2D c)
  {
    // 弾の削除
    Destroy(c.gameObject);

    // 爆発する
    spaceship.Explosion();

    // プレイヤーを削除
    Destroy (gameObject);
  }
}


この状態でゲームを再生してみましょう。 エネミーの弾に当たると爆発はしますが爆発が延々と続くはずです。

5.5 爆発の制御

アニメーションのループ設定

Explodeのアニメーションファイルを選択し、インスペクター上に表示される「Loop Time」のチェックを外してください。 こうすることで爆発のアニメーションは1回のみ再生されます。

5.6 爆発した後のゲームオブジェクト削除

爆発のアニメーションがループしなくなったとしても、爆発のゲームオブジェクトは残り続けます。 そこでアニメーションが終わった後、スクリプトのDestroy関数を使用してゲームオブジェクトを削除します。 Explosion.csファイルを作成し、ExplosionのPrefabにアタッチしてください。

Explosion.cs


using UnityEngine;

public class Explosion : MonoBehaviour
{
  void OnAnimationFinish ()
  {
    Destroy (gameObject);
  }
}


アニメーションイベント

アニメーションが終わった後、Explosion.csOnAnimationFinishメソッドを呼び出すようにします。 Window -&gt; Animationを選択して、Animationビューを表示させます。

表示されたAnimationビューのタブ部分をドラッグすると他のタブ部分へ移動することが出来ます。

次に、Explosionプレハブをシーンへドラッグして、ゲームオブジェクトを作成します。

Explosionのゲームオブジェクトを選択したままでAnimationビューにExplodeの情報が表示されていることを確認して下さい。

AnimationEventを追加します。図5.16を見ながらイベントの追加を行ってください。


図5.16:

イベントの追加が終わったらExplosionのゲームオブジェクトを削除します。 ゲームを再生して、爆発後、Explosionが削除されていることを確認して下さい。

5.7 弾とエネミーが削除されるエリアを作る

今のままだと発射した弾やエネミーは延々と画面の外へ移動してしまいます。 そこで、弾やエネミーが削除される範囲を作成しましょう。 空のGameObjectを作成し、名前をDestroyAreaとしました。さらにBox Collider 2Dをアタッチします。 Is Triggerにチェックを入れ、Sizeは X 9 Y 7 とします。


図5.17:

このDestroyAreaに弾が当たり、DestroyArea外に出てしまったら削除されるようにします。 DestroyArea.csを作成し、DestroyAreaにアタッチします。

DestroyArea.cs


using UnityEngine;

public class DestroyArea : MonoBehaviour
{
  void OnTriggerExit2D (Collider2D c)
  {
    Destroy (c.gameObject);
  }
}


再生して確認してみましょう。


図5.18:

再生と同時にプレイヤーが爆発しているはずです。それと同時にDestroyAreaが削除されています。

プレイヤーが爆発してしまっている理由

原因は単純で、Player.csに記述しているOnTriggerEnter2Dメソッドが呼ばれてしまっているためです。このためDestroyAreaも削除されてしまっています。 少し難しい話になりますが、最初からゲームオブジェクトがコライダー内にあるとしてもOnTriggerEnter2Dは必ず呼ばれます。

この問題を解決する方法の1つとして、レイヤーを使います。

レイヤーで当たり判定の制御

当たり判定を行うためにコライダーをゲームオブジェクトにアタッチしますが、必ずしも全てのコライダー同士で当たり判定が発生するとは限りません。少なくとも今回のシューティングゲームでは以下のように当たり判定に制限を設けなければいけません。1: プレイヤーの弾とプレイヤーは当たらない 2: エネミーの弾とエネミーは当たらない 3: プレイヤーの弾とエネミーの弾は当たらない 4: プレイヤーの弾同士は当たらない 5: エネミーの弾同士は当たらない 6: エネミー同士は当たらない 7: プレイヤー同士は当たらない

これらの制限を行う上で最も簡単なのがレイヤーで当たり判定を制御することです。

レイヤーの設定

レイヤーを使うために、レイヤーの登録を行います。Edit → Project Settings → Tags and Layersを選択してください。


図5.19:

Layer

レイヤーは一部の制限を設けるために使用することが多い機能です。今回のような当たり判定やカメラの描画、ライトの光など様々な用途があります。 詳しくはLayersを御覧ください。

今回はレイヤーを6つ用意します。1: Player 2: Enemy 3: Bullet (Player) 4: Bullet (Enemy) 5: DestroyArea


図5.20:

レイヤーで当たり判定の制御

先程登録したレイヤーで当たり判定の制御を行います。Edit → Project Settings → Physics 2Dを選択してください。


図5.21:

Layer Collision Matrixを使い、レイヤー同士の当たり判定を行うかどうかの設定をします。デフォルトでは全てにチェックが付いています。これを図5.22のようにチェックを外してください。

チェックを外すとそのレイヤー同士は当たり判定がなくなる
図5.22: チェックを外すとそのレイヤー同士は当たり判定がなくなる

以下の画像のようにそれぞれのGameObject、Prefabにレイヤーを設定しましょう。

子要素も全て適用する

親の設定を変更した時、子要素もまとめて適用できることがあります。その時は図5.23のようなダイアログが表示されます。

子要素のレイヤーをまとめて変更するかのダイアログ
図5.23: 子要素のレイヤーをまとめて変更するかのダイアログ

スクリプトでレイヤー制御

Layer Collision Matrixによって、ある程度の当たり判定の制御は行うことが可能になりました。 ですが今回は、特定の場合だけ当たり判定を無視したいという状況が発生します。プレイヤーが爆発してしまっている理由で話したような時です。

DestroyAreaに当たった時、OnTriggerExit2Dは呼び出したいけどOnTriggerEnter2Dは呼び出したくない!

今回はその方法の1つとして、スクリプトで対処する方法をご紹介します。 レイヤー情報をgameObjectから取得することが可能です。取得できるのは数値なので、数値からProject SettingsのTags and Layersで設定したレイヤー名を取得します。 ここで気をつけて欲しいのは、プレイヤーはエネミーとエネミーの弾2つに当たり判定があるということです。

Player.cs


using UnityEngine;
using System.Collections;

public class Player : MonoBehaviour
{
  // Spaceshipコンポーネント
  Spaceship spaceship;

  IEnumerator Start ()
  {
    // Spaceshipコンポーネントを取得
    spaceship = GetComponent<Spaceship> ();

    while (true) {

      // 弾をプレイヤーと同じ位置/角度で作成
      spaceship.Shot (transform);

      // shotDelay秒待つ
      yield return new WaitForSeconds (spaceship.shotDelay);
    }
  }

  void Update ()
  {
    // 右・左
    float x = Input.GetAxisRaw ("Horizontal");

    // 上・下
    float y = Input.GetAxisRaw ("Vertical");

    // 移動する向きを求める
    Vector2 direction = new Vector2 (x, y).normalized;

    // 移動
    spaceship.Move (direction);
  }

  // ぶつかった瞬間に呼び出される
  void OnTriggerEnter2D (Collider2D c)
  {
    // レイヤー名を取得
    string layerName = LayerMask.LayerToName(c.gameObject.layer);

    // レイヤー名がBullet (Enemy)の時は弾を削除
    if( layerName == "Bullet (Enemy)")
    {
      // 弾の削除
      Destroy(c.gameObject);
    }

    // レイヤー名がBullet (Enemy)またはEnemyの場合は爆発
    if( layerName == "Bullet (Enemy)" || layerName == "Enemy")
    {
      // 爆発する
      spaceship.Explosion();

      // プレイヤーを削除
      Destroy (gameObject);
    }
  }
}


設定を終えたらゲームを再生します。プレイヤーが削除されなくなりました。


図5.24:

エネミーの当たり判定

エネミーをプレイヤーの弾に当たったら爆発させ、同時にエネミーを削除します。 コードの追加はプレイヤーと同じくDestroyAreaを考慮し、spaceship.Explosion()を呼び出してExplosionのプレハブから爆発アニメーションを作成します。 そして最後にDestroyでエネミーを削除します。

Enemy.cs


using UnityEngine;
using System.Collections;

public class Enemy : MonoBehaviour
{
  // Spaceshipコンポーネント
  Spaceship spaceship;

  IEnumerator Start ()
  {

    // Spaceshipコンポーネントを取得
    spaceship = GetComponent<Spaceship> ();

    // ローカル座標のY軸のマイナス方向に移動する
    spaceship.Move (transform.up * -1);

    // canShotがfalseの場合、ここでコルーチンを終了させる
    if (spaceship.canShot == false) {
      yield break;
    }

    while (true) {

      // 子要素を全て取得する
      for (int i = 0; i < transform.childCount; i++) {

        Transform shotPosition = transform.GetChild(i);

        // ShotPositionの位置/角度で弾を撃つ
        spaceship.Shot (shotPosition);
      }

      // shotDelay秒待つ
      yield return new WaitForSeconds (spaceship.shotDelay);
    }
  }

  void OnTriggerEnter2D (Collider2D c)
  {
    // レイヤー名を取得
    string layerName = LayerMask.LayerToName(c.gameObject.layer);

    // レイヤー名がBullet (Player)以外の時は何も行わない
    if( layerName != "Bullet (Player)") return;

    // 弾の削除
    Destroy(c.gameObject);

    // 爆発
    spaceship.Explosion();

    // エネミーの削除
    Destroy(gameObject);
  }
}


ゲームを再生して確認してみましょう。 うまくいきましたか?

プレイヤーがエネミーの弾に当たった状態
図5.25: プレイヤーがエネミーの弾に当たった状態

エネミーがプレイヤーの弾に当たった状態
図5.26: エネミーがプレイヤーの弾に当たった状態

5.8 PlayerBulletゲームオブジェクトの削除

Bulletゲームオブジェクトの削除が行われますが親となるPlayerBulletが削除されていません。このまま弾を撃ち続けるとPlayerBulletがゴミとして残り続けてしまうため削除しなければいけません。 PlayerBulletの削除する方法はいくつか考えられますが、PlayerBulletは特に他に影響を与えるゲームオブジェクトではありません。 従って、最も簡単なn秒後に削除という方法で対処します。

Bullet.cs


using UnityEngine;

public class Bullet : MonoBehaviour
{
  // 弾の移動スピード
  public int speed = 10;
  
  // ゲームオブジェクト生成から削除するまでの時間
  public float lifeTime = 5;
  
  void Start ()
  {
    // ローカル座標のY軸方向に移動する
    GetComponent<Rigidbody2D>().velocity = transform.up.normalized * speed;
    
    // lifeTime秒後に削除
    Destroy (gameObject, lifeTime);
  }
}


PlayerBulletは1秒後に、EnemyBulletは5秒後に削除するようにしましょう。