↑の動画でも解説しています。動画と併用してもらうとわかりやすいかなと思います。わからない、うまくいかない事があったら質問される前に、一回、動画の方で手順を確認してください
前回、敵に当たり判定をつけて、敵を踏んづけてプレイヤーが跳ねるところまで行きました。今回は敵を移動させていこうと思います。最後までついて来て頂ければ↑のようなゲームを作る事ができます。
この記事は本のように順を追って解説しています。この記事は途中のページになります。
この記事を見ていて、現在の状況がわからない場合や忘れてしまった事などが出てきたら↓のリンクから目次ページへ飛べますので立ち戻って見てください。
<マリオのクリボーみたいな敵を作ってみよう・その3>
さて、「挙動がクリボーの様な」敵を作成していきます。
まず、クリボーの動きをあげると
- 衝突するとプレイヤーはやられる
- ふんづけるとプレイヤーがちょっと跳ねて敵はやられる
- 画面内に入ると動き出す
- 動いている間はひたすら一定方向に動く
- 壁に当たると反対側へ動く
こんな感じでした。
今回はこの移動の部分
3. 画面内に入ると動き出す
4. 動いている間はひたすら一定方向に動く
5. 壁に当たると反対側へ動く
この部分を実装していこうと思います。
<画面内に入った場合の判定処理を追加しよう>
とりあえず敵にくっつけるスクリプトを作りましょう。
敵は何種類か作ると思うので、クリボーっぽい動きをする敵専用のスクリプトを組みます。とりあえず筆者はEnemy_Zako1と言う名前にしました。
現在敵にくっついているコンポーネントは↑の通りです。
ところで、○○ Rendererと言う名前のものは「画面に物体が見えるようにする」コンポーネントという事をチラッと解説の最初の方の記事で言った事があったのですが、このRenderer系のコンポーネントは「画面が見えているか」を判定をするのに便利です。
このSprite Rendererを活用すると↓のようになります。
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Enemy_Zako1 : MonoBehaviour { private SpriteRenderer sr = null; void Start() { sr = GetComponent<SpriteRenderer>(); } void Update() { if (sr.isVisible) { Debug.Log("画面に見えている"); } } }
Sprite Rendererのインスタンスを捕まえた後、
sr.isVisible
で画面に映っているかどうかを判定できます。
こんな感じで画面に映ったら行動を始めるという処理をしたい時に便利です。
特にマリオチックにするのであれば、画面内に入ったらクリボーが動き出して、崖から落ちていくみたいな場面が結構あるので、プレイヤーに合わせた敵の動きができるようになります。
逆に、画面外でも行動していて欲しい場合は個別にフラグを用意するのもアリだと思います。
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Enemy_Zako1 : MonoBehaviour { [Header("画面外でも行動する")] public bool nonVisibleAct; private SpriteRenderer sr = null; void Start() { sr = GetComponent<SpriteRenderer>(); } void Update() { if (sr.isVisible || nonVisibleAct) { //行動する } } }
↑こんな感じでフラグを用意してあげれば、インスペクターから画面内に入った場合行動する敵と、画面外でも行動する敵の2種類作る事ができるようになります。
<移動をしよう>
プレイヤーキャラクターを作った時と同じようにRigidbody2Dを捕まえてvelocityで移動するようにしましょう。
プレイヤーと違って敵キャラの物理法則を敢えて無視する意味はあんまりないのですが、あんまりいろんなやり方を紹介しても混乱すると思うのでvelocityで行きます。
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Enemy_Zako1 : MonoBehaviour { #region//インスペクターで設定する [Header("移動速度")]public float speed; [Header("重力")]public float gravity; [Header("画面外でも行動する")] public bool nonVisibleAct; #endregion #region//プライベート変数 private Rigidbody2D rb = null; private SpriteRenderer sr = null; private bool rightTleftF = false; #endregion // Start is called before the first frame update void Start() { rb = GetComponent<Rigidbody2D>(); sr = GetComponent<SpriteRenderer>(); } void FixedUpdate() { if (sr.isVisible || nonVisibleAct) { int xVector = -1; if (rightTleftF) { xVector = 1; transform.localScale = new Vector3(-1, 1, 1); } else { transform.localScale = new Vector3(1, 1, 1); } rb.velocity = new Vector2(xVector * speed, -gravity); } else { rb.Sleep(); } } }
インスペクターで敵の移動速度を設定できるようにして、右か左かのフラグを持つようにしています。
rightTleftFのフラグでtrueの時右に進み、falseの時左に進むといった感じです。
Rigidbody2D.Sleep()というのは、物理演算を一時的に働かせなくする命令です。画面に映っていない場合は物理演算を止めることで、処理負荷軽減をする事ができます。
また、Sleepという単語から、起こさないといけないのでは?と思うかもしれませんが、自分から動かしたり、何かにぶつかったりすると勝手に起きるので気にしなくて大丈夫です。
gravityに関してはRigidbody2Dとどっちにするのかお好みで。
ふわふわ浮く敵とか物理法則を無視する敵を作りたいならスクリプトで制御する方が楽なのでスクリプトで制御します。
スクリプトで重力制御するならGravity Scaleを0にするのを忘れずに
とりあえず、敵が移動するようになりました。
<敵にアニメーションをつけよう>
さて、このままでは味気ないので、プレイヤーでやった時と同じように敵にアニメーションをつけていきます。
やり方がわからない、忘れてしまったという人は↓の記事を参考にしてみてください。
とりあえず、アニメーターを作って敵にアタッチし、
アニメーションを作って歩きモーションとやられモーションを追加しました。
デフォルトのアニメーションは歩きモーションになるようにしてください。
ちょっとそれっぽくなりました。
<踏まれたらやられる処理>
では、踏まれたらやられてみましょう。
踏まれた判定は以前作成した、Object Collisionから取得できるので
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Enemy_Zako1 : MonoBehaviour { #region//インスペクターで設定する [Header("移動速度")]public float speed; [Header("重力")]public float gravity; [Header("画面外でも行動する")] public bool nonVisibleAct; #endregion #region//プライベート変数 private Rigidbody2D rb = null; private SpriteRenderer sr = null; private Animator anim = null; private ObjectCollision oc = null; //New ! private BoxCollider2D col = null; //New ! private bool rightTleftF = false; private bool isDead = false; //New ! #endregion // Start is called before the first frame update void Start() { rb = GetComponent<Rigidbody2D>(); sr = GetComponent<SpriteRenderer>(); anim = GetComponent<Animator>(); oc = GetComponent<ObjectCollision>(); //New ! col = GetComponent<BoxCollider2D>(); //New ! } void FixedUpdate() { if (!oc.playerStepOn) //New ! { if (sr.isVisible || nonVisibleAct) { int xVector = -1; if (rightTleftF) { xVector = 1; transform.localScale = new Vector3(-1, 1, 1); } else { transform.localScale = new Vector3(1, 1, 1); } rb.velocity = new Vector2(xVector * speed, -gravity); } else { rb.Sleep(); } } else //New ! { if (!isDead) { anim.Play("dead"); rb.velocity = new Vector2(0, -gravity); isDead = true; col.enabled = false; } } } }
もし、子オブジェクトにコライダーを複数持たせている場合は↓の処理を追加してもらえればと思います。
↓のようにコライダーを持たせている場合
↓のように空のゲームオブジェクトを用意して、コライダーを全てそのオブジェクトの子オブジェクトにします。(ColliderObjは空のゲームオブジェクト)
そして、col.enabled = false;の下に
Transform t = transform.Find("ColliderObj");
if(t != null)
{
t.gameObject.SetActive(false);
}
を追加してもらえればと思います。
やられる処理は1回しかしたくないので、フラグを用意して1回処理したらもう通らないようにしています。
やられアニメーションをして、コライダーが邪魔になってしまうので、やられたらコライダーを切るようにしています。
さて、これだけではちょっと味気ないので、やられたら回転しながら落下するようにしてみましょうか。
if (!isDead) { anim.Play("dead"); rb.velocity = new Vector2(0, -gravity); isDead = true; col.enabled = false; Destroy(gameObject,3f); } else { transform.Rotate(new Vector3(0,0,5)); }
やられたら回転するようにしています。
transform.Rotate(new Vector3(0,0,5));
これが物体を回転させるスクリプトです。
何度も言うように移動にはtransform系と物理演算系があって、物理演算で動いているものをtransfrom系で動かすと重くなります。
しかしながら
transform.Rotateはtransform系ですね。
何故このような事をしているかと言うと、やられた時の処理でコライダーを切っているからです。
5という数字は適当です。z軸周りに回転します。各自調整してください。
col.enabled = false;
これですね。
そして、回転しながら落ちていって、ずーっと残っていられるとメモリ上無駄になってしまうので3秒たったらゲームオブジェクトごとシーンから破棄します。
Destroy(gameObject,3f);
このDestroyというのはインスタンスを破棄する命令です。MonoBehaviourを継承しているので使えます。
gameObjectというのはEnemy_Zako1がくっついているゲームオブジェクトを指すので、雑魚敵のインスタンスを破棄しますという意味になります。
3fというのは3秒後に破棄するという意味になります。3秒間くるくる回って破棄されるという形になります。このDestroyは秒数を指定しないこともできます。Destroy(gameObject);とした場合、即座に破棄されます。
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Enemy_Zako1 : MonoBehaviour { #region//インスペクターで設定する [Header("移動速度")] public float speed; [Header("重力")] public float gravity; [Header("画面外でも行動する")] public bool nonVisibleAct; #endregion#region//プライベート変数
private Rigidbody2D rb = null;
private SpriteRenderer sr = null;
private Animator anim = null;
private ObjectCollision oc = null;
private BoxCollider2D col = null;
private bool rightTleftF = false;
private bool isDead = false;
#endregion
// Start is called before the first frame update
void Start()
{
rb = GetComponent<Rigidbody2D>();
sr = GetComponent<SpriteRenderer>();
anim = GetComponent<Animator>();
oc = GetComponent<ObjectCollision>();
col = GetComponent<BoxCollider2D>();
}
void FixedUpdate()
{
if (!oc.playerStepOn)
{
if (sr.isVisible || nonVisibleAct)
{
int xVector = -1;
if (rightTleftF)
{
xVector = 1;
transform.localScale = new Vector3(-1, 1, 1);
}
else
{
transform.localScale = new Vector3(1, 1, 1);
}
rb.velocity = new Vector2(xVector * speed, -gravity);
}else
{
rb.Sleep();
}
}
else
{
if (!isDead)
{
anim.Play("dead");
rb.velocity = new Vector2(0, -gravity);
isDead = true;
col.enabled = false;
Destroy(gameObject,3f);}
else
{
transform.Rotate(new Vector3(0, 0, 5));
}
}
}
}
<画像のレイヤー分けをしよう>
画像が重なって見えなくなってしまったら
さて、この状態で実行して敵を踏むと
下に落ちていった時になんか敵がタイルマップに隠れてしまいます。
ちょっとUnity2019.1現在のバグなのか何なのかZ座標をいじっても変な挙動で表示されてしまうので、画像のレイヤー分けを行います。
プレイヤーとか雑魚敵とかいちいち全部レイヤーを変えていたら大変なのでタイルマップのレイヤーを変更します。
TilemapにくっついているTilemap RendererのSorting Layerというところを押してください。
Add Sorting Layer…を押してください。
下の+ボタンを押して、レイヤーを追加し、TileMapとつけましょう。
そして、TileMapとつけたレイヤーをDefaultと入れ替えましょう。
上にあるレイヤーほど奥に描画されるようになります。
タイルマップレンダラーのソーティングレイヤーを作成したレイヤーに変更すればOKです。
Sorting Layerとは
Sorting Layerというのは2Dのオブジェクト同士がカメラから同じ距離になった時、どの順番で描画するかを取り決めるものです。
今現在、カメラは水平方向に向いているので、本来であればZ座標が手前の方が前に表示されるはずなのですが、なんかTilemapは2019年5月現在不安定(何故か5奥に持っていくと手前に表示されたりする)なので、Sorting Layerで描画順を整頓します。
UGUIはヒエラルキーの順番でしたが、その他の2Dオブジェクトはソーティングレイヤーで整頓するのもいいと思います。
UGUIはまた別の描画順制御があるのでこれは別格と考えてください。
はい。これでだいぶそれっぽくなりましたね。
<壁や敵に当たったら折り返そう>
壁と敵の判定用のBox Collider 2Dをつけよう
さて、次は壁と敵に当たったら折り返す処理を作ります。まずは「壁と敵に当たった」という判定を作りましょう。
敵の左側に緑色の四角が見えるでしょうか?
トリガー用の子オブジェクトを作って、敵の進行方向側に置きます。
子オブジェクトにするのはすでにBox Collider2Dが敵に付いているので、インスタンスを捕まえるのが困難になるため分けました。
Is Triggerにチェックを入れます。
接地判定を作った時のように、この左の緑の四角に敵か地面が入ったら反転するという処理を追加します。
何故左右両方につけないかというと、今の状態だと左右反転時の処理をscaleのX座標をマイナスにすることで実現しているので、この子オブジェクトの判定も反転するからです。
また、このことから、コライダーも反転します。複雑な形のコライダーを使用している場合は、一番親になっているオブジェクトがX軸で真ん中になるように、子オブジェクトを駆使して調整してください。
判定用のスクリプトを書こう
この各種トリガーを取得するために、新しくスクリプトを作成します。自分は EnemyCollisionCheckという名前にしました。
接地判定の時のようにOnTriggerEnter2Dを使います。
using System.Collections; using System.Collections.Generic; using UnityEngine; public class EnemyCollisionCheck : MonoBehaviour { /// <summary> /// 判定内に敵か壁がある /// </summary> [HideInInspector] public bool isOn = false; private string groundTag = "Ground"; private string enemyTag = "Enemy"; #region//接触判定 private void OnTriggerEnter2D(Collider2D collision) { if (collision.tag == groundTag || collision.tag == enemyTag) { isOn = true; } } private void OnTriggerExit2D(Collider2D collision) { if (collision.tag == groundTag || collision.tag == enemyTag) { isOn = false; } } #endregion }
プレイヤーにくっついている接地判定と似た感じです。判定に地面だけではなく、敵も含めています。
これを作成した子オブジェクトにくっつけましょう。
今度はこの判定を敵本体にくっつけてあるスクリプトで受け取ります。
インスペクターからインスタンスを指定しよう
さて、今まではGetComponentで同じゲームオブジェクトに付いているコンポーネントを取得していましたが、今回は違うゲームオブジェクトになります。
そのため、EnemyCollisionCheckが付いたゲームオブジェクトのインスタンスを探すところからしなければいけないのですが、めんどくさいのでインスペクターから指定しましょう。
enemy_zako1.csに↓のようなパラメータを追加します。
[Header("接触判定")]public EnemyCollisionCheck checkCollision;
EnemyCollisionCheckというのは先ほど書いたスクリプトです。
今は敵の子オブジェクトにくっついているこのスクリプトのインスタンスをインスペクターで指定します。
簡単です。そのスクリプトがくっついているゲームオブジェクトをインスペクターにドラッグ&ドロップするだけです。
これでインスタンスをスクリプトに渡すことができました。
受け取った結果から、もし「判定内に壁か敵がいた」場合反対方向を向くようにします。
if (sr.isVisible || nonVisibleAct) { if (checkCollision.isOn) { rightTleftF = !rightTleftF; } int xVector = -1; if (rightTleftF) { xVector = 1; transform.localScale = new Vector3(-1, 1, 1); } else { transform.localScale = new Vector3(1, 1, 1); } rb.velocity = new Vector2(xVector * speed, -gravity); }
「画面内に入ったら」の処理の中に反対を向く処理を入れます。
if (checkCollision.isOn) { rightTleftF = !rightTleftF; }
↑ですね。
checkCollision.isOnでインスペクターから受け取ったインスタンスから情報を見ます。
「!」は真偽を反対にする意味があるので
rightTleftF = !rightTleftF;
は左右逆にするよという意味になります。
こんな感じで、だいぶクリボーっぽい動きになりました。
ひとまず敵その1は一旦完成でいいかなと思います。
<まとめ>
今回作ったスクリプトは↓の通りです。
using System.Collections; using System.Collections.Generic; using UnityEngine; public class EnemyCollisionCheck : MonoBehaviour { /// <summary> /// 判定内に敵か壁がある /// </summary> [HideInInspector] public bool isOn = false; private string groundTag = "Ground"; private string enemyTag = "Enemy"; #region//接触判定 private void OnTriggerEnter2D(Collider2D collision) { if (collision.tag == groundTag || collision.tag == enemyTag) { isOn = true; } } private void OnTriggerExit2D(Collider2D collision) { if (collision.tag == groundTag || collision.tag == enemyTag) { isOn = false; } } #endregion }
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Enemy_Zako1 : MonoBehaviour { #region//インスペクターで設定する [Header("移動速度")] public float speed; [Header("重力")] public float gravity; [Header("画面外でも行動する")] public bool nonVisibleAct; [Header("接触判定")] public EnemyCollisionCheck checkCollision; #endregion #region//プライベート変数 private Rigidbody2D rb = null; private SpriteRenderer sr = null; private Animator anim = null; private ObjectCollision oc = null; private BoxCollider2D col = null; private bool rightTleftF = false; private bool isDead = false; #endregion // Start is called before the first frame update void Start() { rb = GetComponent<Rigidbody2D>(); sr = GetComponent<SpriteRenderer>(); anim = GetComponent<Animator>(); oc = GetComponent<ObjectCollision>(); col = GetComponent<BoxCollider2D>(); } void FixedUpdate() { if (!oc.playerStepOn) { if (sr.isVisible || nonVisibleAct) { if (checkCollision.isOn) { rightTleftF = !rightTleftF; } int xVector = -1; if (rightTleftF) { xVector = 1; transform.localScale = new Vector3(-1, 1, 1); } else { transform.localScale = new Vector3(1, 1, 1); } rb.velocity = new Vector2(xVector * speed, -gravity); } else { rb.Sleep(); } } else { if (!isDead) { anim.Play("dead"); rb.velocity = new Vector2(0, -gravity); isDead = true; col.enabled = false; Destroy(gameObject,3f); } else { transform.Rotate(new Vector3(0, 0, 5)); } } } }
何かうまくいかない事があった場合は↓の記事を参考にしてみてください
最低限↓の動画の要件を満たしていない質問は受けかねるので、ご理解ください。
また、筆者も間違えることはありますので、何か間違っている点などありましたら、動画コメント欄にでも書いていただけるとありがたいです。