Unity 2Dアクションの作り方【飛び道具を持つ敵】

make 2d action thumbnail

前回はグラフィックの賑やかしと背景を作成しました。今回は敵のバリエーションを増やしていきたいと思います。

<飛び道具を飛ばす敵を作ろう>

さて、敵のバリエーションを増やすにあたって、ただ動くだけの敵ばかりだと面白くないので、飛び道具を飛ばしてくる敵を作ってみましょう。

とりあえず、ベースとなる敵の画像を描くか、アセットストアで素材を探してきましょう。

自分は↓のアセットを使わせていただきました。

3Dを2Dにする方法は↓を参考にしてください。

アニメーションは通常と攻撃の2種類用意してください。(デザインによっては歩きモーションも必要な人はそれも用意してください)

アニメーションの作り方を忘れてしまった方は↓の記事を参考にしてください。

↑のアセットを2Dに変更してアニメーションを設定すると↓のようになりました。

enemy2 animation

何か発射できそうです。アニメーターは↓のようになっています。

enemy2 animator

通常の状態→攻撃はattackのトリガーがオンになったら遷移します。Fixed Durationのチェックを外し、Transition Durationを0にします。

攻撃→通常の状態はHas Exit Timeにチェックをし、Exit Timeを1に、Fixed Durationのチェックを外し、Transition Durationを0にしています。

皆さんも自分で好みの形にしてみてください。自分で描いてみてもいいですし、気に入ったアセットを探してみるのもいいかもしれません。

アセットストアを利用する場合は気をつける事があるので、アセットストアの利用に慣れていない方は↓の記事を参考にしてください。

<攻撃を飛ばそう>

次は飛ばす攻撃を作っていこうと思います。

自分は炎を飛ばしたかったので↓のアセットを使用させていただきました。

各々、飛ばす攻撃を自分で描くなり、ダウンロードしてくるなりして好みのものを作っていきましょう。

↓のような炎のエフェクトを使わせてもらおうと思います。

fire effect

アセットストアではエフェクトがプレハブになっている事が多いのでそれをシーンに持って来れば使えます。プレハブの使い方を忘れてしまった人は↓の記事を参考にしてください。

では、このオブジェクトに攻撃用の設定を加えていきます。

と、言っても今までやってきた事を思い出して頂ければ簡単です。

fire effect setting

まず、攻撃用のタグを用意して変更します。自分はEnemyAttackというタグを新しく作成して設定しました。

次に攻撃の判定にあったコライダーを設定しましょう。コライダーについて忘れてしまった方は↓の記事を参考にしてください。

自分は円形の攻撃判定を設定したかったのでCircle Collider 2Dを設定しました。

そして、Is Triggerにチェックを入れ、Radiusの数値を入れて大きさをちょうどよくします。

次にRigidbody 2Dを追加してBody TypeをKinematicに、Freeze RotationのZにチェックを入れます。

Rigidbodyの設定を詳しく知りたい方は↓の記事をご覧ください。

設定ができたら、敵の子オブジェクトに持ってきて非アクティブにします

fire child object

はい、これでオブジェクトの準備は完了です。

<敵のスクリプトを書こう>

敵と飛び道具の準備ができたら新しい敵用のスクリプトを書いていきましょう。

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

 public class Enemy_Zako2 : MonoBehaviour
 {
     [Header("攻撃オブジェクト")] public GameObject attackObj;
     [Header("攻撃間隔")] public float interval;

     private Animator anim;
     private float timer;

     // Start is called before the first frame update
     void Start()
     {
          anim = GetComponent<Animator>();
          if (anim == null || attackObj == null)
          {
              Debug.Log("設定が足りません");
              Destroy(this.gameObject);
          }
          else
          {
              attackObj.SetActive(false);
          }
     }

     // Update is called once per frame
     void Update()
     {
          AnimatorStateInfo currentState = anim.GetCurrentAnimatorStateInfo(0);

          //通常の状態
          if (currentState.IsName("idle"))
          {
              if(timer > interval)
              {
                  anim.SetTrigger("attack");
                  timer = 0.0f;
              }
              else
              {
                  timer += Time.deltaTime;
              }
          }
     }
 }
enemy2 inspector

↑敵にこのようなコンポーネントをセットします。

Object Collisionは以前に作成したスクリプトです。

新たに作ったスクリプトは一定間隔でアニメーションを行うスクリプトになっています。

AnimatorStateInfo currentState = anim.GetCurrentAnimatorStateInfo(0);

//通常の状態
if (currentState.IsName("idle"))
{
     if(timer > interval)
     {
          anim.SetTrigger("attack");
          timer = 0.0f;
     }
     else
     {
          timer += Time.deltaTime;
     }
}

↑通常の状態になった時に時間をカウントして一定間隔たったらまた攻撃アニメーションに入るようにします。

さて、このままではアニメーションをループさせるだけなので、攻撃を発生させる処理を追加していきます。

スポンサーリンク

<アニメーションからスクリプトを呼ぼう>

さて、攻撃を発生させる処理を追加したいのですが、単にスクリプトから呼ぶとアニメーションと合わない事が多いです。

そのため、今回はアニメーションに合わせて攻撃判定を発生させてみましょう。

まず、敵のスクリプトに↓のようなメソッドを追加します。

public void Attack()
{
   Debug.Log("攻撃");
}

次にヒエラルキーで敵を選択した状態でアニメーションウィンドウを開きます。

そして、攻撃をするタイミングのフレームを選択します。

animation event plus

そして、アニメーション名の右側にあるボタンの一番右のボタンを押します。

そうすると↓のようなものが追加されます。

add animation event tag

もし、これを消したかったら選択した状態で右クリックでDeleteする事ができます。

この、追加されたものを選択している状態でインスペクターを見て下さい。

add animation event method

するとFunctionという項目があるので、ここをクリックすると先ほど敵に追加したメソッドが出てきます。

ここに出てくるのは、選択しているアニメーターと同じゲームオブジェクトについているスクリプトのメソッドになります。

これで、アニメーションからメソッドを呼ぶ事ができるので、アニメーションに合わせて攻撃判定を発生させる事ができそうです。

animation call method

アニメーションに合わせてメソッドの呼び出すところまでできました。

<攻撃判定を発生させよう>

さて、いよいよ攻撃判定を発生させましょう。

Attackを↓のように書き換えます。

public void Attack()
{
     GameObject g = Instantiate(attackObj);
     g.transform.SetParent(transform);
     g.transform.position = attackObj.transform.position;
     g.SetActive(true);
}

Instantiateというのはインスタンスを作成するメソッドです。これはMonoBehaviourを継承しているから使用できる命令です。

最初の方で作成した飛び道具のゲームオブジェクトのインスタンス(実体)を作成するので、作成した飛び道具を元にして新たにゲームオブジェクトを作成します。

SetParentというのはスクリプトから親オブジェクトを変更する命令です。実体化したばかりのゲームオブジェクトは親を持たないので、自分自身を指定します。transformというのは自分自身のTransformです。

何故親子関係にするのかというと、この敵がどの方向を向いているのかわからないからです。

上向きかもしれないですし、横向きかもしれません。

しかし、子オブジェクトにすることによって、親の方向に合わせる事が可能になります。

また、Instantiateは元のゲームオブジェクトの情報をそのまま持ってくるので非アクティブになっています。そのため、元のオブジェクトは非アクティブなまま、新しく作成されたオブジェクトはアクティブにします。

次に発生した攻撃を移動させます。

クリックすると展開します
using System.Collections;
 using System.Collections.Generic;
 using UnityEngine;
 public class EnemyAttack : MonoBehaviour
 {
     [Header("スピード")] public float speed = 3.0f;
     [Header("最大移動距離")] public float maxDistance = 100.0f;

     private Rigidbody2D rb;
     private Vector3 defaultPos;

     // Start is called before the first frame update
     void Start()
     {
          rb = GetComponent<Rigidbody2D>();
          if(rb == null)
          {
              Debug.Log("設定が足りません");
              Destroy(this.gameObject);
          }
          defaultPos = transform.position;
     }

     // Update is called once per frame
     void FixedUpdate()
     {
          float d = Vector3.Distance(transform.position, defaultPos);

          //最大移動距離を超えている
          if (d > maxDistance)
          {
              Destroy(this.gameObject);
          }
          else
          {
              rb.MovePosition(transform.position += Vector3.up * Time.deltaTime * speed);
          }
     }

     private void OnTriggerEnter2D(Collider2D collision)
     {
          Destroy(this.gameObject);
     }
 }

ただ一定方向に進んでいくスクリプトで、何かにぶつかったら破棄されるようになっています。

ポイントとしては最大移動距離を設定する事です。

float d = Vector3.Distance(transform.position, defaultPos);

//最大移動距離を超えている
if (d > maxDistance)
{
     Destroy(this.gameObject);
}

Vector3.Distanceというのは2つの位置の距離を算出してくれる便利なメソッドです。

Vector3の使い方について詳しく知りたい方は↓の記事で解説しています。

敵から攻撃が一定間隔で出てくるので、何かにぶつからなければ無限に存在する事ができます。そうするとメモリを圧迫してしまうので、最大移動距離を設定して、それ以上移動したら消えるようにします。

Instantiateは新たに生成するものなので、作り続けてしまうと処理落ち、アプリ落ちの原因になるので使わないものはキッチリ破棄しましょう。

そして、何かにぶつかってしまうと破棄してしまうので、最初に生成される位置は敵自体のコライダーとぶつからないようにしましょう。

collider not collision

↑発射する元々のゲームオブジェクトの位置を離してあげると大丈夫です。

できたら↓のようになります。

fire shot

あとはプレイヤー側が攻撃を受けるようにすればOKです。

プレイヤー側が新しく追加したタグのオブジェクトにぶつかったら残機を減らすようにしましょう。

クリックすると展開します
using System.Collections;
 using System.Collections.Generic;
 using UnityEngine;
 public class Player : MonoBehaviour
 {
     #region//インスペクターで設定する
     [Header("移動速度")] public float speed;
     [Header("重力")] public float gravity;
     [Header("ジャンプ速度")] public float jumpSpeed;
     [Header("ジャンプする高さ")] public float jumpHeight;
     [Header("ジャンプする長さ")] public float jumpLimitTime;
     [Header("接地判定")] public GroundCheck ground;
     [Header("天井判定")] public GroundCheck head;
     [Header("ダッシュの速さ表現")] public AnimationCurve dashCurve;
     [Header("ジャンプの速さ表現")] public AnimationCurve jumpCurve;
     [Header("踏みつけ判定の高さの割合(%)")] public float stepOnRate;
     [Header("ジャンプする時に鳴らすSE")] public AudioClip jumpSE;
     [Header("やられた鳴らすSE")] public AudioClip downSE;
     [Header("コンティニュー時に鳴らすSE")] public AudioClip continueSE;
     #endregion

     #region//プライベート変数
     private Animator anim = null;
     private Rigidbody2D rb = null;
     private CapsuleCollider2D capcol = null;
     private bool isGround = false;
     private bool isHead = false; 
     private bool isJump = false;
     private bool isRun = false;
     private bool isDown = false;
     private bool isOtherJump = false;
     private bool isClearMotion = false;
     private float jumpPos = 0.0f;
     private float otherJumpHeight = 0.0f;
     private float otherJumpSpeed = 0.0f;
     private float dashTime, jumpTime;
     private float beforeKey;
     private string enemyTag = "Enemy";
     private string moveFloorTag = "MoveFloor";
     private string jumpStepTag = "JumpStep";
     private string enemyAttackTag = "EnemyAttack";
     private bool isContinue = false;
     private float continueTime, blinkTime;
     private SpriteRenderer sr = null;
     private MoveObject moveObj;
     #endregion

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

     private void Update()
     {
          if (isContinue)
          {
              //明滅 ついている時に戻る
              if (blinkTime > 0.2f)
              {
                  sr.enabled = true;
                  blinkTime = 0.0f;
              }
              //明滅 消えているとき
              else if (blinkTime > 0.1f)
              {
                  sr.enabled = false;
              }
              //明滅 ついているとき
              else
              {
                  sr.enabled = true;
              }
              //1秒たったら明滅終わり
              if (continueTime > 1.0f)
              {
                  isContinue = false;
                  blinkTime = 0f;
                  continueTime = 0f;
                  sr.enabled = true;
              }
              else
              {
                  blinkTime += Time.deltaTime;
                  continueTime += Time.deltaTime;
              }
          }
     }

     void FixedUpdate()
     {
          if (!isDown && !GManager.instance.isGameOver && !GManager.instance.isStageClear)
          {
              //接地判定を得る
              isGround = ground.IsGround();
              isHead = head.IsGround(); 

              //各種座標軸の速度を求める
              float xSpeed = GetXSpeed();
              float ySpeed = GetYSpeed();

              //アニメーションを適用
              SetAnimation();

              //移動速度を設定
              Vector2 addVelocity = Vector2.zero;
              if (moveObj != null)
              {
                  addVelocity = moveObj.GetVelocity();
              }
              rb.velocity = new Vector2(xSpeed, ySpeed) + addVelocity;
          }
          else
          {
              if (!isClearMotion && GManager.instance.isStageClear)
              {
                  anim.Play("player_clear");
                  isClearMotion = true;
              }
              rb.velocity = new Vector2(0, -gravity);
          }
     }

     /// <summary>  
     /// Y成分で必要な計算をし、速度を返す。  
     /// </summary>  
     /// <returns>Y軸の速さ</returns>  
     private float GetYSpeed()
     {
          float verticalKey = Input.GetAxis("Vertical");
          float ySpeed = -gravity;

          //地面にいるとき
          if (isGround)
          {
              if (verticalKey > 0 && jumpTime < jumpLimitTime)
              {
                  ySpeed = jumpSpeed;
                  jumpPos = transform.position.y; //ジャンプした位置を記録する
                  isJump = true;
                  jumpTime = 0.0f;
                  GManager.instance.PlaySE(jumpSE);
              }
              else
              {
                  isJump = false;
                  isOtherJump = false;
              }
          }
          //何かを踏んだ際のジャンプ
          else if (isOtherJump)
          {
              if (jumpPos + otherJumpHeight > transform.position.y && jumpTime < jumpLimitTime && !isHead)
              {
                  ySpeed = otherJumpSpeed;
                 jumpTime += Time.deltaTime;
              }
              else
              {
                  isOtherJump = false;
                  jumpTime = 0.0f;
              }
          }
          //ジャンプ中
          else if (isJump)
          {
              //上ボタンを押されている。かつ、現在の高さがジャンプした位置から自分の決めた位置より下ならジャンプを継続する
              if (verticalKey > 0 && jumpPos + jumpHeight > transform.position.y && jumpTime < jumpLimitTime && !isHead)
              {
                  ySpeed = jumpSpeed;
                  jumpTime += Time.deltaTime;
              }
              else
              {
                  isJump = false;
                  jumpTime = 0.0f;
              }
          }
          if (isJump)
          {
              ySpeed *= jumpCurve.Evaluate(jumpTime);
          }
          return ySpeed;
     }

     /// <summary>  
     /// X成分で必要な計算をし、速度を返す。  
     /// </summary>  
     /// <returns>X軸の速さ</returns>  
     private float GetXSpeed()
     {
          float horizontalKey = Input.GetAxis("Horizontal");
          float xSpeed = 0.0f;
          if (horizontalKey > 0)
          {
              transform.localScale = new Vector3(1, 1, 1);
              isRun = true;
              dashTime += Time.deltaTime;
              xSpeed = speed;
          }
          else if (horizontalKey < 0)
          {
              transform.localScale = new Vector3(-1, 1, 1);
              isRun = true;
              dashTime += Time.deltaTime;
              xSpeed = -speed;
          }
          else
          {
              isRun = false;
              xSpeed = 0.0f;
              dashTime = 0.0f;
          }
          //前回の入力からダッシュの反転を判断して速度を変える
          if (horizontalKey > 0 && beforeKey < 0)
          {
              dashTime = 0.0f;
          }
          else if (horizontalKey < 0 && beforeKey > 0)
          {
              dashTime = 0.0f;
          }
          beforeKey = horizontalKey;
          xSpeed *= dashCurve.Evaluate(dashTime);
          beforeKey = horizontalKey;
          return xSpeed;
     }

     /// <summary>  
     /// アニメーションを設定する  
     /// </summary>  
     private void SetAnimation()
     {
          anim.SetBool("jump", isJump || isOtherJump);
          anim.SetBool("ground", isGround);
          anim.SetBool("run", isRun);
     }

     /// <summary>  
     /// ダウンアニメーションが終わっているかどうか  
     /// </summary>  
     /// <returns>終了しているかどうか</returns>   
     public bool IsDownAnimEnd()
     {
          if (isDown && anim != null)
          {
              AnimatorStateInfo currentState = anim.GetCurrentAnimatorStateInfo(0);
              if (currentState.IsName("player_down"))
              {
                  if (currentState.normalizedTime >= 1)
                  {
                      return true;
                  }
              }
          }
          return false;
     }

     /// <summary>  
     /// コンティニューする  
     /// </summary>  
     public void ContinuePlayer()
     {
          isDown = false;
          anim.Play("player_stand");
          isJump = false;
          isOtherJump = false;
          isRun = false;
          isContinue = true;
          GManager.instance.PlaySE(continueSE);
     }

     #region//接触判定  
     private void OnCollisionEnter2D(Collision2D collision)
     {
          if (!GManager.instance.isStageClear && !GManager.instance.isGameOver)
          {
              //敵
              if (collision.collider.tag == enemyTag)
              {
                  //踏みつけ判定になる高さ
                  float stepOnHeight = (capcol.size.y * (stepOnRate / 100f));
                  //踏みつけ判定のワールド座標
                  float judgePos = transform.position.y - (capcol.size.y / 2f) + stepOnHeight;

                  foreach (ContactPoint2D p in collision.contacts)
                  {
                      if (p.point.y < judgePos)
                      {
                          ObjectCollision o = collision.gameObject.GetComponent<ObjectCollision>();
                          if (o != null)
                          {
                              jumpPos = transform.position.y; //ジャンプした位置を記録する
                              otherJumpHeight = o.boundHeight;    //踏んづけたものから跳ねる高さを取得する
                              otherJumpSpeed = o.jumpSpeed;       //踏んづけたものから跳ねる速さを取得する
                              o.playerStepOn = true;        //踏んづけたものに対して踏んづけた事を通知する
                              isOtherJump = true;
                              isJump = false;
                              jumpTime = 0.0f;
                          }
                          else
                          {
                              Debug.Log("ObjectCollisionが付いてないよ!");
                          }
                      }
                      else
                      {
                          anim.Play("player_down");
                          isDown = true;
                          GManager.instance.SubHeartNum();
                          GManager.instance.PlaySE(downSE);
                          break;
                      }
                  }
              }
              else if (collision.collider.tag == jumpStepTag)
              {
                  //踏みつけ判定になる高さ
                  float stepOnHeight = (capcol.size.y * (stepOnRate / 100f));
                  //踏みつけ判定のワールド座標
                  float judgePos = transform.position.y - (capcol.size.y / 2f) + stepOnHeight;
                  foreach (ContactPoint2D p in collision.contacts)
                  {
                      if (p.point.y < judgePos)
                      {
                          ObjectCollision o = collision.gameObject.GetComponent<ObjectCollision>();
                          if (o != null)
                          {
                              jumpPos = transform.position.y; //ジャンプした位置を記録する
                              otherJumpHeight = o.boundHeight;    //踏んづけたものから跳ねる高さを取得する
                              otherJumpSpeed = o.jumpSpeed;       //踏んづけたものから跳ねる速さを取得する
                              o.playerStepOn = true;        //踏んづけたものに対して踏んづけた事を通知する
                              isOtherJump = true;
                              isJump = false;
                              jumpTime = 0.0f;
                          }
                          else
                          {
                              Debug.Log("ObjectCollisionが付いてないよ!");
                          }
                      }
                  }
              }
              //動く床
              else if (collision.collider.tag == moveFloorTag)
              {
                  //踏みつけ判定になる高さ
                  float stepOnHeight = (capcol.size.y * (stepOnRate / 100f));
                  //踏みつけ判定のワールド座標
                  float judgePos = transform.position.y - (capcol.size.y / 2f) + stepOnHeight;

                  foreach (ContactPoint2D p in collision.contacts)
                  {
                      //動く床に乗っている
                      if (p.point.y < judgePos)
                      {
                         moveObj = collision.gameObject.GetComponent<MoveObject>();
                      }
                  }
              }
          }
      }

     private void OnCollisionExit2D(Collision2D collision)
     {
          if (collision.collider.tag == moveFloorTag)
          {
              //動く床から離れた
              moveObj = null;
          }
     }

     private void OnTriggerEnter2D(Collider2D collision)
     {
          if (collision.tag == enemyAttackTag)
          {
              anim.Play("player_down");
              isDown = true;
              GManager.instance.SubHeartNum();
              GManager.instance.PlaySE(downSE);
          }
     }
     #endregion
 }

これでプレイヤーにダメージを与えられるようになりました。

もうそろそろステージをちゃんとした形で完成させる事ができそうです。というわけで次回はいよいよゲームを形作っていきましょう。

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



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