Unity 2Dアクションの作り方【接地判定】

さて、前回は移動を実装しました。次はジャンプを実装したいところなんですが、その前に接地判定についてやっていこうと思います。↑の動画でも解説しています。わからない、うまくいかない事があったら質問される前に、一回、動画の方で手順を確認してください

この記事は本のように順を追って解説しています。この記事は途中のページになります。
この記事を見ていて、現在の状況がわからない場合や忘れてしまった事などが出てきたら↓のリンクから目次ページへ飛べますので立ち戻って見てください。

<現状について>

クリックで展開します

前回からの状況はプレイヤー操作の移動が完了しました。まだまだ下書きの状態です。

dash

↑のようなに左右の移動が完了して

playerには作成したスクリプトとSprite RendererとAnimatorとRigidbody 2DとCapsule Collider 2Dがくっついている状態です。

now player Current status

この現状がよくわからないよという人はこのページトップに貼ってある記事一覧から立ち返ってみてください。

現在のスクリプトは↓のような感じです。

 using System.Collections;
 using System.Collections.Generic;
 using UnityEngine;
 
 public class Player : MonoBehaviour
 {
     //インスペクターで設定する
     public float speed;
 
     //プライベート変数
     private Animator anim = null;
     private Rigidbody2D rb = null;
     
     void Start()
     {
         //コンポーネントのインスタンスを捕まえる
         anim = GetComponent<Animator>();
         rb = GetComponent<Rigidbody2D>();
     }
 
     void Update()
     {
         //キー入力されたら行動する
         float horizontalKey = Input.GetAxis("Horizontal");
         float xSpeed = 0.0f; 
         if (horizontalKey > 0)
         {
             transform.localScale = new Vector3(1, 1, 1);
             anim.SetBool("run", true);
             xSpeed = speed;
         }
         else if (horizontalKey < 0)
         {
             transform.localScale = new Vector3(-1, 1, 1);
             anim.SetBool("run", true);
             xSpeed = -speed;
         }
         else
         {
             anim.SetBool("run", false);
             xSpeed = 0.0f;
         }
         rb.velocity = new Vector2(xSpeed, rb.velocity.y);
     } 
}

<接地判定とは>

接地判定というのは、文字通り地面についているかどうかの判定です。

何故この判定が必要かというと「地面についている時のみ」やりたい事があるからです。

例えば、ジャンプは地面についている時だけしてほしいですよね。2段ジャンプを作るにしても空中でジャンプしているのか地面でジャンプしているのかを判別しなければいけません。

このように地面にいるかどうか判別したい時が、色々出てくるので接地判定をとります。

<当たり判定の無い判定 IsTrigger>

さて、接地判定を取りたいのですが、これも移動と同じで様々な方法があります。

しかし、移動と違って重さにそこまで差異は無いので一番簡単な方法を取ろうと思います。

適当にカラのゲームオブジェクトを作成してください。

create empty game object

で、適当に名前をGroundCheckとかわかりやすいものにして、プレイヤーの子オブジェクトにしてください

そしてこれにAdd Componentしていきます。今度はBox Collider 2Dを追加してください。

box collider 2d is trigger

Box Collider 2DがついたらIs Triggerというところをチェックしてください。

Is Triggerにチェックを入れる事によって、このコライダーの当たり判定がなくなりました

当たり判定のないコライダーってなんの意味があるんだよって話ですが、このIs Triggerにチェックを入れる事によって当たり判定の侵入を検知できるようになります。

ちょっと今のままではわかりづらいので、新しいBox Collider 2Dの位置を変えましょうか。

<足元にTrigger判定をおく>

接地判定を行うために、先ほど作ったゲームオブジェクトをCapsule Collider 2Dの下に持ってきてください。

ground check collider

↑のような形になりました。足元にもう一個緑の四角があるのがわかるかと思います。こういう感じに配置してください。Capsule Collider 2Dと重ならないように注意してください

コライダーの大きさはBox Collider 2DのSizeのパラメーターをいじれば変えることができます。

毎度同じく適当でOKです。どーせ下書きなので。

このIs Triggerが入ったコライダーは当たり判定がない代わりに、この緑の四角の中に何か当たり判定があるものが入って来たら検知できます

緑の四角の中に地面が入っていたら「接地している」という判定にすれば良さそうです。

では、どうやって当たり判定の侵入を検知するのかというと、Box Collider 2Dがついているゲームオブジェクトにある特別なメソッドが記述してあるスクリプトをアタッチする事で検出する事ができます。

特別な意味を持つメソッド
private void OnTriggerEnter2D(Collider2D collision)
{
   Debug.Log("何かが判定に入りました");
}

private void OnTriggerStay2D(Collider2D collision)
{
   Debug.Log("何かが判定に入り続けています");
}
    
private void OnTriggerExit2D(Collider2D collision)
{
   Debug.Log("何かが判定をでました");
}

似たヤツで後ろに2Dとつけないものもあるので注意してください。

これがスクリプトの中に書いてあると特別な機能を持ちます。

書いてある通り、Enterで侵入を検知、Stayで侵入し続けていて、Exitで判定外にでた事を検知する事ができます。

新しいスクリプトを作成して↑を書きます。

これは特別なメソッドですので適当な名前にせず、↑の通りに記述してください。自分はGroundCheckと言う名前のスクリプトを作成しました。

クリックすると展開します
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GroundCheck : MonoBehaviour
{
    private void OnTriggerEnter2D(Collider2D collision)
    {
        Debug.Log("何かが判定に入りました");
    }

    private void OnTriggerStay2D(Collider2D collision)
    {
        Debug.Log("何かが判定に入り続けています");
    }

    private void OnTriggerExit2D(Collider2D collision)
    {
        Debug.Log("何かが判定をでました");
    }
}
ground check

↑のようにPlayerの子オブジェクトに接地判定をするオブジェクトを作り、そこに特別な意味を持つメソッドを書いたスクリプトをくっつけます。

すると↓のようになると思います。

log collision

↑のように何かが足元に入って来ている事がわかります。

<地面を地面と認識できるようにしよう>

さて、現状のままだと何かの当たり判定がGroundCheckの範囲内に入って来ていることはわかりますが、何が入って来ているのかはわかっていません

今は地面しかありませんが、今後敵などを作った時に、敵を踏んだ瞬間地面と錯覚してしまう可能性があります。

ではどうすればいいかと言うと、Tagを使用します。

ヒエラルキーでTilemapを選択した後、インスペクターのTagのところをクリックしてAdd Tag…をクリックします。

add tag

インスペクターが↓のようになったかと思います。

tags and layers

このタグと言うのはゲームオブジェクトを分類分けできるものです。自分で好きなタグを作って、ゲームオブジェクトを分類わけする事ができます。

Tagsの+を押して、タグを追加しましょう。名前は別になんでも構いませんが、わかりやすいものにしておくといいと思います。筆者は「Ground」という名前にしました。

add ground tags

SaveでOKです。

そうするとTagに作成したタグが追加されます。もう一度TilemapのTagをクリックすれば出てきます。

select ground tags

Tilemapのタグを先ほど作ったタグにしましょう。

それではこれを取得したいと思います。先ほどのメソッドを書き換えます。

侵入検知のスクリプトにタグで見分ける機能を追加したもの
private string groundTag = "Ground"; 

private void OnTriggerEnter2D(Collider2D collision)
{
   if (collision.tag == groundTag)
   {
       Debug.Log("何かが判定に入りました");
   }
}

private void OnTriggerStay2D(Collider2D collision)
{
    if (collision.tag == groundTag)
    {
        Debug.Log("何かが判定に入り続けています");
    }
}
    
private void OnTriggerExit2D(Collider2D collision)
{
    if (collision.tag == groundTag)
    {
        Debug.Log("何かが判定をでました");
    }
}

これらの特別なメソッドには引数にCollider2D型でcollisionというものが入って来ています。

このcollisionというやつはTrigger判定の中に入ってきたコライダーになります。要は侵入して来たやつです。

「.tag」で侵入して来たやつのタグを見ます。そして、それの名前が「先ほど作ったタグだった場合」という判定をつける事によって、地面かどうか判定をします。

これは別にどちらでもいいのですが、自分はこういう文字列の指定は間違いを誘発しやすいのと、使い回しがしやすいように変数にしています。

スポンサーリンク

<接地判定のフラグを用意しよう>

さて、では接地判定に使うフラグを作りましょうか。

おそらく大半の人が地面に入ったらフラグをオンにして、地面から出ていったらフラグをオフにすると思っているかなと思います。

ちょっと足りないスクリプト
private string groundTag = "Ground"; 
public bool isGround = false; 

private void OnTriggerEnter2D(Collider2D collision) 
{
     if (collision.tag == groundTag)
     {
         isGround = true;
     } 
}
      
private void OnTriggerExit2D(Collider2D collision) 
{
      if (collision.tag == groundTag)
      {
         isGround = false;
      } 
}

isGroundが地面にいるという判定にしたとすると、↑を想像された方は多いかなと思いますが、残念ながらこれでは足りません。

接地判定を作ると↓のようになります。

接地判定を行うスクリプト
private string groundTag = "Ground";
private bool isGround = false; 
private bool isGroundEnter, isGroundStay, isGroundExit;

//接地判定を返すメソッド
//物理判定の更新毎に呼ぶ必要がある
public bool IsGround()
{
   if(isGroundEnter || isGroundStay)
   {
      isGround = true;
   }
   else if(isGroundExit)
   {
      isGround = false;
   } 

   isGroundEnter = false;
   isGroundStay = false;
   isGroundExit = false;
   return isGround; 
}
 
private void OnTriggerEnter2D(Collider2D collision)
{
   if (collision.tag == groundTag)
   {
      isGroundEnter = true;
   }
}
 
private void OnTriggerStay2D(Collider2D collision)
{
   if (collision.tag == groundTag)
   {
      isGroundStay = true;
   }
}
     
private void OnTriggerExit2D(Collider2D collision)
{
   if (collision.tag == groundTag)
   {
      isGroundExit = true;
   }
}

GroundCheckについているスクリプトを↑のように書き換えてください。

コードが長くなってきて見るのが面倒かもしれませんが、書いてあることは簡単です。↓のように各種フラグを用意します。

private bool isGroundEnter, isGroundStay, isGroundExit;

地面に入るか居続けた場合、接地判定をオンにして、地面から出た場合オフにするという処理です。

Exitはいらない気がしますが、OnTriggerStayは止まっていると呼ばれないという特性があるのでExitでフラグをオフにしてあげる必要があります。

さて、何故このような書き方をするかというと、それはEnterとStayとExitのうち2つもしくは3つが同時に呼ばれるタイミングが存在するからです。

enter stay exit

↑のように、左の地面からExitして、右の動く床にEnterを同時に行う場合などがあります。

この時、フラグを各種類で持って置かないと、地面に接地しているのにExitしているのでisGroundがfalseになってしまう恐れがあります。

その為、あんなにフラグを用意したんですね。

そしてそれらのフラグから判断して、publicなメソッドから結果を返すことができるようにしています。これで他のスクリプトから接地判定を読むことができます。

最後に各種フラグを下ろすのを忘れずに。ずっとオンになってしまうので。しかし、isGroundのフラグだけは保持します。これはStayが止まっていると呼ばれないからです。

また、このフラグを下ろす必要があるため、このメソッドは物理判定のたびに毎回呼ぶ必要があります。(詳しくは後述)

本来ならもっといい書き方があるのですが、ちょっと現段階では難易度が高いため、妥協案としてこのような形をとっています。コメントに書いてわかるようにしておきましょう。

<PlayerでGroundCheckを読もう>

さて、接地判定ができてもプレイヤー側で読めなければ意味がありません。

プレイヤーでも読めるようにしましょう。プレイヤーのスクリプトを↓のようにします。

プレイヤーのスクリプトに接地判定を読み込む機能を追加したもの
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Player : MonoBehaviour
{
     //インスペクターで設定する
     public float speed;
     public GroundCheck ground; //new

     //プライベート変数
     private Animator anim = null;
     private Rigidbody2D rb = null;
     private bool isGround = false; //new

     void Start()
     {
          //コンポーネントのインスタンスを捕まえる
          anim = GetComponent<Animator>();
          rb = GetComponent<Rigidbody2D>();
     }

     void Update()
     {
          //接地判定を得る
          isGround = ground.IsGround(); //new

          //キー入力されたら行動する
          float horizontalKey = Input.GetAxis("Horizontal");
          float xSpeed = 0.0f;
          if (horizontalKey > 0)
          {
              transform.localScale = new Vector3(1, 1, 1);
              anim.SetBool("run", true);
              xSpeed = speed;
          }
          else if (horizontalKey < 0)
          {
              transform.localScale = new Vector3(-1, 1, 1);
              anim.SetBool("run", true);
              xSpeed = -speed;
          }
          else
          {
              anim.SetBool("run", false);
              xSpeed = 0.0f;
          }
          rb.velocity = new Vector2(xSpeed, rb.velocity.y);
      }
 }

publicな変数でgroundを定義しています。これはシリアライズ化されているのでインスペクターでアタッチすることができます。

ground check attach

これで返ってきた接地判定をisGroundという変数に入れて扱えるようにします。

<FixedUpdate>

FixedUpdateを使ってみよう

さて、地面を取得するところまで完成しました。

が、一つやる事があります。

今まで、プレイヤーのスクリプトの毎フレームの処理をUpdateに書いて来ました。それをFixedUpdateに変更します。

void Update()
{
}

↓変更

void FixedUpdate()
{
}

実はUnityには特別なメソッドがたくさんありまして、これもその一つです。

大きな特徴の一つとして物理エンジンの計算の前にこの処理を行います。アップデートとついていますが、毎フレーム処理されるとは限りません。

とりあえず、難しいことは考えず、物理エンジンの計算の前に毎回、処理されるものと覚えておけば大丈夫です。

これによって、接地判定のフラグを物理演算の前に毎回下ろすことができるようになります。

もし、詳しく知りたい方がいらっしゃったら↓の記事を参考してください。

FixedUpdateを使う意味

FixedUpdateは正直、文章で解説することがかなり困難な為、↑の動画を見ていただいた方がわかりやすいと思います。是非ご活用ください

さて、2種類のアップデートを使う上でどういった場合に使うかというと、

Point

・Updateはフレームに合わせたい時に使うよ!
・FixedUpdateは物理演算に合わせたい時に使うよ!

Check

・フレームはキー入力と画面に関係するよ!
・物理演算は位置や当たり判定に関係するよ!

と、このように使い時がUpdateとFixedUpdateで違います。

さて、今回の場合Rigidbody 2Dを操作しています。これは物理エンジンに影響があるのでFixed Updateにしたわけなんです。

FixedUpdateに合わせるかUpdateに合わせるか

さて、物理エンジンを使用するからFixedUpdateにすると言いましたが、思考停止でFixedUpdateを選択するのはちょっと待って欲しいです。

たまに物理演算を行なっていたら問答無用でFixed Updateにしてしまう人も見ます。

んー。ちょっと難しいかもしれませんが、思考停止は良くないので、Rigidbodyが使われたら無意識のウチにFixedUpdateにするのではなく、どっちがいいのかちょっと考えてみてください。

「物理演算を使用するから、Fixed Updateにする。」ではなく「物理演算に合わせたいからFixed Updateにする」を選択してください。

↑で接地判定のメソッドの為にFixed Updateに変更したと言っていますが、ただそれだけならプレイヤーのスクリプトを変更せず、接地判定の方で完結させればいい話なのです。そのため、何故プレイヤーの方のスクリプトを変更するのかを考えていきます。

何故今回はFixedUpdateを使うのか

今回の場合、Rigidbody2Dのvelocityの値を直接変更するという手法を取っているので、実はUpdateにしようがFixedUpdateにしようがあんまり変わりません。

何せ物理法則を無視する為に直接velocityに値をブチ込んでいるので物理演算も何もないのです。

もちろん当たり判定の物理演算は行なっていますが

では何故FixedUpdateにしたのかと言われると、こちらの方が制御しやすいという理由です。

ぶっちゃけ今回の場合は大きくは変わらないですが、いい機会なので覚えましょう。

どちらかに偏った時の事を考えよう

物理演算というのは時間の概念が大事(移動距離の計算などがあるので)なので、フレームという概念の中にいません。フレームはフレームレートによって時間が変動してしまうので当てにならないのです。

その為、UpdateとFixedUpdateは呼ばれ方が違います。ここにちょっと問題があります。

Updateは入力と画面の更新に関係し、FixedUpdateは物理的な移動に関係がある事に注目してください。

FixedUpdate(移動)

Update(入力と描画)

FixedUpdate(移動)

Update(入力と描画)

以下ループ……

こういう風な呼ばれ方をしている間は大丈夫です。移動した後描画されているので、ちゃんとした位置に描画されます。しかし、これはフレームレートによって

FixedUpdate(移動)

FixedUpdate(移動)

FixedUpdate(移動)

Update(入力と描画)

以下ループ…..

となったり、

Update(入力と描画)

Update(入力と描画)

Update(入力と描画)

FixedUpdate(移動)

Update(入力と描画)

以下ループ……

となったりします。順々に呼ばれるわけじゃぁないんです。どのような呼ばれ方するのかはフレームレートによります。

移動しまくった後に画面が更新されると瞬間移動したように見えます

ゲームでよく見る、処理落ちしたあとにガクッと移動するやつはコレです。

逆に移動の処理が入らずに画面が更新されると止まっているのですが、この場合、見え方はそんなに気にする必要はありません。Updateが連続で入っているときはフレームレートが高い時ですので人間の目には視認しづらいです。その為、今回の場合は特に考えなくて大丈夫です。

入力に合わせるべきか

本来なら入力の方を気にする必要があるのですが、

 float horizontalKey = Input.GetAxis("Horizontal");

これは押しっぱなしを検知できるので、問題ありません。

押した瞬間だけを検知したい場合、Updateが連続で呼ばれると次のフレームではfalseになってしまいます。「○○を入力した瞬間」という命令を書く場合はUpdateに書いた方がいいわけです。

しかしながら今回は押しっぱを検知しているのでUpdateが連続で呼ばれたとしてもプレイヤーはボタンを押し続けているのでFixedUpdateに入ってきても入力を受け取ることができます。

FixedUpdateが連続で呼ばれる場合を考える

では、↓の例のようにFixedUpdateが複数回呼ばれるケースに絞って考えましょう。

FixedUpdate(移動)

FixedUpdate(移動)

FixedUpdate(移動)

Update(入力と描画)

以下ループ….

移動が何回も呼ばれるので瞬間移動して見えます。これは、よく処理落ちした時やフレームレートが低いと見られる現象です。

今回の場合、velocity(速さ)に直接値を入れています。ここで、注目してほしいのが、「速さ」に値を入れているのであって、transformの時のように「位置」に値を入れているわけではありません

力を加えなければ「速さ」は減衰しますよね

もし、Updateに移動の処理を書いた場合、速さに値が代入されて、連続2回目以降のFixedUpdateでは何も代入されないので、減衰します。

一方、FixedUpdateに移動処理を書いた場合、移動の前に逐一速さに値が代入されるので、減衰せずにずっと一定です

↑の状態はフレームレートが30FPS以下だったり、処理落ちした時に見られます。通常時は逐一速さに値が代入される為、減衰せずにずっと一定です。

その為、処理落ちした時と通常時の操作感が少し違う為、これを合わせる為にFixedUpdateにしています。

まぁ、ほんのわずかな違いなのですが・・・今回は学習目的なので新しい事を覚えていきましょう。特にコレを知らないと、ある瞬間にしか起きない、非常に見つけにくいバグを誘発する可能性があるのでよーく考えてください。

さて、物理演算とフレームレートをなんとなーく理解できたかなと思います。完全に理解するのには時間がかかるでしょうから、ゆっくり覚えていけばOKです。

<まとめ>

今回の記事でコードは以下のようになっているかと思います。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Player : MonoBehaviour
{
     //インスペクターで設定する
     public float speed;
     public GroundCheck ground;

     //プライベート変数
     private Animator anim = null;
     private Rigidbody2D rb = null;
     private bool isGround = false;

     void Start()
     {
          //コンポーネントのインスタンスを捕まえる
          anim = GetComponent<Animator>();
          rb = GetComponent<Rigidbody2D>();
     }

     void FixedUpdate()
     {
          //接地判定を得る
          isGround = ground.IsGround();

          //キー入力されたら行動する
          float horizontalKey = Input.GetAxis("Horizontal");
          float xSpeed = 0.0f;
          if (horizontalKey > 0)
          {
              transform.localScale = new Vector3(1, 1, 1);
              anim.SetBool("run", true);
              xSpeed = speed;
          }
          else if (horizontalKey < 0)
          {
              transform.localScale = new Vector3(-1, 1, 1);
              anim.SetBool("run", true);
              xSpeed = -speed;
          }
          else
          {
              anim.SetBool("run", false);
              xSpeed = 0.0f;
          }
          rb.velocity = new Vector2(xSpeed, rb.velocity.y);
      }
 }
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GroundCheck : MonoBehaviour
{
     private string groundTag = "Ground";
    private bool isGround = false;
     private bool isGroundEnter, isGroundStay, isGroundExit;

     //接地判定を返すメソッド
   //物理判定の更新毎に呼ぶ必要がある
     public bool IsGround()
     {    
          if (isGroundEnter || isGroundStay)
          {
              isGround = true;
          }
          else if (isGroundExit)
          {
              isGround = false;
          }

          isGroundEnter = false;
          isGroundStay = false;
          isGroundExit = false;
          return isGround;
     }

     private void OnTriggerEnter2D(Collider2D collision)
     {
          if (collision.tag == groundTag)
          {
              isGroundEnter = true;
          }
     }

     private void OnTriggerStay2D(Collider2D collision)
     {
          if (collision.tag == groundTag)
          {
              isGroundStay = true;
          }
     }

     private void OnTriggerExit2D(Collider2D collision)
     {
          if (collision.tag == groundTag)
          {
              isGroundExit = true;
          }
     }
 }

だいぶプログラムっぽくなりましたね。

何かうまくいかない事があった場合は↓の記事を参考にしてみてください

最低限↓の動画の要件を満たしていない質問は受けかねるので、ご理解ください。

また、筆者も間違えることはありますので、何か間違っている点などありましたら、動画コメント欄にでも書いていただけるとありがたいです。

さて、次回はジャンプを実装していこうと思います。


タイトルとURLをコピーしました