【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段ジャンプを作るにしても1段目は地面からして欲しいものです。

このように地面にいる時だけやりたい事が色々出てくるので接地判定をとります。

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

さて、接地判定を取りたいのですが、これも移動と同じで様々な方法があります。しかし、移動と違って重さにそこまで差異は無いので一番簡単な方法を取ろうと思います。

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

add Box Collider 2D

Box Collider 2Dがつきました。この時Box Collider 2DにだけIs Triggerというところをチェックしてください。

box collider 2d is trigger

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

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

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

<足元にTrigger判定をおく>

接地判定を行うために、Box Collider 2Dを足元に持っていきたいです。

Box Collider 2Dの中のOffsetで位置を調整できます。Sizeで大きさを調整できます。これを利用してIs Triggerにチェックが入ったコライダーを足元に持って来ましょう。

Adjust the position of the collider

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

毎度同じく適当で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で判定外にでた事を検知する事ができます。

これは特別な関数ですので適当な名前にせず、↑の通りに記述してください。

↑の関数をplayerに書くと

log collision

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

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

さて、現状のままだと何かが入って来ていることはわかりますが、何が入って来ているのかはわかっていません

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

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

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

add tag

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

tags and layers

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

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

add ground tags

SaveでOKです。そうすると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」で侵入して来たやつのタグを見ます。そして、それの名前が「先ほど作ったタグだった場合」という判定をつける事によって、地面かどうか判定をします。

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

<FixedUpdate>

FixedUpdateを使ってみよう

さて、地面を取得するところまで完成しました。これから接地判定を作成します。が、その前に一つやる事があります。

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

void Update()
{
}

↓変更

void FixedUpdate()
{
}

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

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

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

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

FixedUpdateを使う意味

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

Point

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

Check

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

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

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

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

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

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

「物理演算を使用するから、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になってしまいます。○○した瞬間という命令を書く場合は考える必要がありますので注意してください。

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

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

FixedUpdate(移動)

FixedUpdate(移動)

FixedUpdate(移動)

Update(入力と描画)

以下ループ….

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

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

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

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

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

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

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

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

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

<フラグを用意しよう>

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

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

private string groundTag = "Ground";
private 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 isGroundEnter, isGroundStay, isGroundExit;
private bool isGround = false;

void FixedUpdate()
{
  if(isGroundEnter || isGroundStay)
  {
     isGround = true;
  }
  else if(isGroundExit)
  {
     isGround = false;
  }

  isGroundEnter = false;
  isGroundStay = false;
  isGroundExit = false;
}

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;
  }
}

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

private bool isGroundEnter, isGroundStay, isGroundExit;

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

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

FixedUpdateの最後に各種フラグを下ろすのを忘れずに。ずっとオンになってしまうので。

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

enter stay exit

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

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

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

<まとめ>

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

 using System.Collections;
 using System.Collections.Generic;
 using UnityEngine;
 
 public class player : MonoBehaviour
 {
     //インスペクターで設定する
     public float speed;        //移動速度
 
     //プライベート変数
     private Animator anim = null;
     private Rigidbody2D rb = null;
     private string groundTag = "Ground";
     private bool isGroundEnter, isGroundStay, isGroundExit;
     private bool isGround = false;
     
     void Start()
     {
         //コンポーネントのインスタンスを捕まえる
         anim = GetComponent<Animator>();
         rb = GetComponent<Rigidbody2D>();
     }
 
     void FixedUpdate()
     {
         //接地しているかどうかの判定をとる
         if(isGroundEnter || isGroundStay)
         {
             isGround = true;
         }
         else if(isGroundExit)
         {
             isGround = false;
         }
 
         //キー入力されたら行動する
         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);
 
 
         //接地しているか判断する為のフラグをリセットする
         isGroundEnter = false;
         isGroundStay = false;
         isGroundExit = false;
     }
 
     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をコピーしました