ゼロからのUnity(7)コルーチンとInstantiate()を利用して敵キャラを作ってみよう
skill

ゼロからのUnity(7)
コルーチンとInstantiate()を利用して敵キャラを作ってみよう

2016.02.25

2016年2月某日、Amazonからゲーム開発エンジン「Lumberyard」がローンチされました。
Unityのライバルになり得る存在です。

現時点では実績/対応プラットフォームなどでUnityの方が大幅に優れていますが、相手はあの巨大なAmazonです。
今後の動向が非常に気になるところです。

また、同じタイミングでマルチプレイを簡単に構築するためのAWSサービス「GameLift」も同時にローンチされました。
プレイヤーの人数に応じて自動でスケールアウトしてくれたり便利な機能があるようですので、こちらはUnityと組み合わせて使うのも面白そうですね。
ゲームを取り巻く環境も日々変化していて、将来何が主流になるかを予測するのはなかなか難しいです。
ただ確実に言えるのは、Unityでゲーム開発を学んだ経験があれば他のプラットフォームで開発を行う時にも必ず役に立ちます。
変化を恐れず突き進みましょう!

(賀好 昭仁)

さて、本題に移ります。

前回はゲームに基本的なルールを作りました。
今回はゲームをより面白くするために、敵キャラを作ってみましょう。

※なお、古いバージョンのUnityでは本記事に記載した一部の機能が使えない場合があります。Unity5.3以上でお試しください。

■敵キャラとは

多くのゲームには敵 キャラが存在します。
共通しているのは、「プレイヤーの邪魔をすること」です。
(敵なので当然ですよね)

時には弾を撃ち、時には体当たりで向かってくる。
そんな多彩な動きでプレイヤーを翻弄する敵キャラを作っちゃいましょう。

■作成のためのテクニックを学ぼう

まずは敵キャラ作成のために役立つテクニックを学んでいきましょう。

それぞれざっくりと概要を説明してから、学んだ内容を使って敵キャラを作ります。

■コルーチン

キャラなどの動きを作っていく上で欠かせないのが、コルーチンです。

関数やメソッドは、実行後ただちに処理が行われます。(1フレームの間=画面が1回描画される間に処理が実行されます)
そのため、時間のかかる処理や、一定時間待つような処理を書くのには適していません。

コルーチンを使うと、フレームをまたいで処理を行うことができます。
< a href="http://docs.unity3d.com/ja/current/Manual/Coroutines.html" target="_blank">http://docs.unity3d.com/ja/current/Manual/Coroutines.html

コルーチンはとても使い勝手が良く、
・ 敵キャラのAIに使う
・ アニメーションや演出に使う
・ ダウンロードが終わるのを待つ
・ 重い処理を複数フレームに渡って実行し、処理落ちしないようにする
など、様々な場面で活用できます。

■コルーチンの仕組み

コルーチンの仕組みは簡単です。

コルーチンは、戻り値がIEnume rator のメソッドとして記述します。
IEnumeratorは「列挙子」というもので、コルーチンは「列挙された処理を順に実行」していくイメージです。

コルーチンの中では必ずyield演算子が登場します。

yield return xxx;

の記述がある場合、処理(xxx)の終了を待ちます。
例えば、

yield return null;

ですと、1フ レーム後に処理を再開します。

yield return new WaitForSeconds(1f);

ですと、1秒後に処理を再開します。

処理を中断する場合は

yield break;

でOKです。

■コルーチンのサンプルコード

では、実際にサンプルコードを見てみましょう。

private void Start()
{
  // Tes t1Coroutine()を実行
  StartCoroutine( Test1Cor outine());
  // Test2Coroutine()を 実行
  StartCoroutine(Test2Coroutine()) ;
}

privat e IEnume rator T est1Coroutine()
{
  Debug.Log( "Test1コル ーチン開始");
  yield return new W aitForSeconds(3f);
  Debug.Log("3秒経過");
}

private IEnumerator Test2Coroutine()
{
  Debug.Log("Test2コルーチン開始");
  whil e (true) {
    Debug.Log("延 々と実行してやるわい");
    yield return Test1Co routine();
    if (U nityEngine.Ra ndom.Range(0, 100) == 0) {
      // 100分の1の確率で止め る
      Debug.Log("ああよかった!やっと止まったよ");
      yield break;
    }
    Debug.Log("ムヘヘヘ!");
  }
}

Test1Coroutine()はご覧のとおり極めてシンプルです。
「Test1コルーチン開始」のデバッグログを出力、3秒待ってから「3秒経過」のデバッグログを出力します。

Test2Coroutine()のように、コルーチンの中で無限ループさせたり、別のコルーチンを実行することも可能です。

■コルーチン実行方法は他にもある

IEnumeratorの代わりに 、コルーチンのメソッド名を文字列で渡してもコルーチンを実行することができます。

StartCoroutine("Test1Coroutine");

ただし、メソッド名が間違っていても実行されるまでエラーが発 生しないため、特別な理由がない限りはIEnumeratorを渡す形の方が良いでしょう。

(余談)Start()メソッドもコルーチンにできる
Start()メソッドはコルーチンにすることが可能です。

private IEnumerator Start() {}

ただし、Awake()やUpdate()はコルーチンにできません。

■コルーチンを止める

コルーチンを止める場合も、開始する場合と同じく
・IE numeratorを指定する
・メソッド名を文字列で指定する
の2つがありますが、こちらも同じ理由でIEnumeratorを指定する方をオススメします。

// IEnumeratorを保持しておく
var ienumerator = Test1Coroutine();
// コルーチン実行
Start Coroutine(ienumerator);
...略…
// StopCoroutine()に実行中のIEnumeratorを渡すと止まる
StopCoroutine(ienumerator);

■コルーチンで注意すべきこと

とても便利なコルーチンですが、コルーチンを実行中のゲームオブジェクトが非アクティブになるとコルーチンが勝手に止まってしまいます。
一度止まってしまうと、ゲームオブジェクトがアクティブになっても再開されません。

// コルーチンを開始
StartCoroutine(Test1());
...略…
// これをやるとコルーチンが止まる
gameObject.SetActive(false);
...略…
// その後、これをやっても再開はされない
gameObject.SetActive(true);

また、ゲームオブジェクトが破棄された場合も、当然ながらコルーチンは止まってしまいます。

回避策として、絶対に破棄されないゲームオブジェクトにコルーチンを実行させる方法があります。
http://docs.unity3d.com/ja/current/ScriptReference/Object.DontDestroyOnLoad.html

こちらの方法であれば、例えシーンが切り替わってもコルーチンは継続されます。サーバにセーブデータを保存する際など、止まると困る重要な処理を行う時にオススメです。

■Instantiate()

敵キャラや弾など、ゲーム中に多数登場するものを扱う場合、Instantiate()が活躍します。
http://docs.unity3d.com/j a/current/ScriptReference/Object.Instantiate.html

このメソッドにGameObjectやMonoBehaviourを継承したコンポーネントを渡すと、インスタンスを生成してくれます。
(簡単に言うと、設計図を元にゲームオブジェクト複製してくれるということです)

■Instantiate()のサンプルコード()

サンプルコードを見てみましょう。

public class InstantiateTest : MonoBehaviour {
  public GameObject test;

  private void Start()
  {
    // Instantiate()でtestのインスタンスを生成
    var test2 = Instantiate(test);
  }
}

GameObject「test」を設計図として、Instantiate()でゲームオブジェクトを複製しています。
「test」は、インスペクターから好きなGameObjectをひも付けておくことが可能です。

■Instantiate()の注意点

Instantiate()を使う場合、処理コストが高いことは意識してお くべきです 。

もしUpdate()などの中で何度もInstantiate()して1フレームの間に大量のゲームオブジェクトを生成しようとすると、すぐ処理落ち してしまいます。

シューティングゲームなど、フレームごとに沢山のゲームオブジェクト(弾)を生成/破棄せざるを得ない場合はオブジェクトプールパターンでの実装をオススメします。
(簡単に説明すると、ゲームオブジェクトを必要な数だけ先に作ってお き、使い終わったあとも破棄せずに使い回す、という実装パターンです)

詳しく説明すると長くなるので、本記事では割愛させていただきます。
検索するとたくさん情報が見つかりますので、興味のある方は調べてみてください。

■敵キャラの準備

コルーチンとInstantiate()、2つのテクニックを学びました。
次はこれらを使って敵キャラを作ってみましょう!

まずはキャラ画像を配置します。

Assets/Sprite Pack #1 - Tap and Fly/Sprites/Background/scene_02_midground をゲームシーンにドラッグ&ドロップします。

これは良いサボテンで すね。
愛らしくも触 ると痛そうな程よいツンデレっぷりで す。
名前はSabo sanにして みましょうか。

Hiyokoちゃんを作った時と同じように、Inspectorで
RigidBody 2D
Circle Collider 2D
をアタッチします。

転がらないよう、Rigidbody 2DのConstraints->Freeze Rotation Zにチェックしておきましょう。

■敵キャラのAIを作ってみる

続いて、Asse ts/ScriptsにSabosan.csを作成します。
この中にキャラの動きを書いていきましょう。

using UnityEngine;
using System;
using System.Collections;

public class Sabosan : MonoBehaviour
{
  public GameObject bulletPrefab;
   private GameObject hiyoko;
  priv ate bool isMovable = true;
  private bool isJumpable = true;
  private float velocityX = 0f;

  private void Start()
  {
    // ひよこちゃんを追っかけさせるため、ゲームオブジェクトを取得
    hiyoko = GameObject.Find("Hiyoko");

    StartCoroutine(ChaseCoroutine());
    StartCoroutine(ShootIfPossibleCoroutine());
  }
  private void Update()
  {
    // 移動させるための力を加える
    var rigidbody = GetComponent<Rigidbody2D>();
    rigidbody.velocity = new Vector2(isMovable ? velocityX : 0f, rigidbody.velocity.y);

    // ジャンプ処理
    if (hiyoko.t ransform.position.y > transform.position.y&& rigidb ody.velocity.y == 0f)
    {
      // ひよこちゃんが自分より高い位置に居るならジャンプする
      JumpIfPossible();
    }
  }

  /// <summary>
  /// ひよこちゃんを追っかけるためのコルーチンです。
  /// </summary>
  /// <returns>The coroutine.</returns>
  private IEnumerator ChaseCoroutine()
  {
    while (true)
    {
      // ひよこちゃんが左右どちらに居るかをチェック、進行方 向を決めて追いかける
      velocityX = hiyoko.transform.position.x > transf orm.position.x ? 0.5f : -0.5f;

      // 次の方向転換は3秒後
      yield return new WaitForSeconds(3f);
    }
  }

  /// <summary>
  /// 弾を発射するかどうか制御するコルーチンです。
  /// </summary>
  /// <returns>The coroutine.</returns>
  private IEnumerator ShootIfPossibleCoroutine()
  {
    while (true)
    {
      if (Math.Abs(hiyoko.transform.position.x - transform.position.x) > 1.5f)
      {
        // ひよこちゃんと一定以上の距離が離れているならば、進行方向に向かって弾を発射
        StartCoroutine(ShootBulletAndDestroyCoroutine());
        // クールダウンで2秒立ち止まる
        isMovable = false;
        yield return new WaitFo rSeconds(2f);
        isMovable = true ;
      }
      else
      {
        yield return new WaitForSeconds(0.1f);
      }
    }
  }

  /// <summary>
  /// 弾発射から消滅までを制御するコルーチンです。
  /// </ summary>
  /// <returns>The bullet and destroy coroutine.</returns>
  private IEnumerator ShootBulletAndDestroyCo routine()
  {
    var bullet = Instantiate(bulletPrefab);
    bullet.transform.position = transform.position + new Vector3(0f, 0.3 f);
    // 発射
    bullet.GetComponent<Rigidbody2D>().velocity = new Vector2(velocityX* 3f, 5f);
    // 発射してから5秒待つ
    yield return new WaitForSeconds(5f);
    // 発射した弾を削除
    Destroy(bullet);
  }

  /// <summary >
  /// ジャンプ可能であればジャンプします。
  /// </summary>
  private void JumpIfPossible()
  {
    if (!isJumpable)
    {
      return;
    }
    var rigidbody = GetComponent<Rigidbody2D>();
    rigidbody.AddForce( new Vector2(0f, 250f));
    isJumpable = false;
    StartCoroutine(JumpCooldownCoroutine());
  }

  /// <summary>
  ///ジャンプ後、次にジャンプ可能になるまでのクールダウンを制御します。
  /// </summary>
  /// <returns>The cooldown coroutine.</returns>
  private IEnumerator JumpCooldownCoroutine()
  {
    yield return new WaitForSeconds(3f);
    isJumpable = true;
  }
}

やや長くて複雑に見えますが、ひとつひとつのメソッドはシンプルです。
先ほど学んだコルーチンとInstantiate()もふんだんに盛り込んでみました。
ポイントは、Start()で定期的に実行したい処理のコルーチンを開始しているところです。

こちらのスクリプトをSabosanにアタッチしましょう。

■敵キャラの弾を作る

続いて、弾を作ります。

Assets/Sprite Pack #1 - Tap and Fly/Sprites/Background/scene_01_cloud をゲームシーンにドラッグ&ドロップします。
名前はBulletにします。

ちょっと大きいのでScaleのX/Yを0.3にしておきましょう。
雲がだんだんタマゴに見えてきましたか?
(もしかすると、お腹が空いているのかもしれません)

Rigidbody 2DとCircle Collider 2Dもアタッチしておきます。

■ゲームオブジェクトにタグを付与する

ひよこちゃんとの衝突判定を行うため、SabosanとBulletにタグを付けておきましょう。

インスペクターの一番上にあるTagプルダウンをクリックします。

プルダウンから、Add Tag…を選択します。

インスペクターにタグの一覧が表示されますので、List is Emptyとなっている右下の+ボタンを押します。

入力欄が出てきますので、Enemyと入力します。

再度ヒエラルキーからSabosanおよびBulletを選択します。
Tagプルダウンをみてみると、今追加したEnemyタグが表示されていますので、選択します。

これでタグ付け完了です。
付けたタグは、スクリプトから gameObject.tag で取得できるようになります。

■弾のPrefab化とSabosanへのひも付け

BulletオブジェクトをAssets/Prefabsにドラッグし、Prefab化したら
ゲームシーン上からはBulletを消してしまいましょう。

最後にSabosanオブジェクトを選択し、SabosanスクリプトのBullet Prefabに上記で作成したBulletのPrefabをドラッグしてひも付けておきます。

■被弾時の処理を書く

今の状態ですと、ひよこちゃんは敵にぶつかってもノーリアクションです。

ひよこちゃんはスーパーサ○ヤ人ではありませんし、世の中に興味が無いわけでもありません。(そういう設定も面白いですが)
ただちょっとカワイイだけの、普通のひよこです。

ということで、ひよこちゃんが敵や弾にぶつかった時に弾き飛ばされるよう、Assets/Scripts/Hiyoko.csを以下の内容に書き換えます。

using UnityEngine;
using System.Collections;

public classHiyoko : MonoBehaviour
{
  // 操作可能フラグ
  private bool isControllable = true;

  private void Update()
  {
    if (!isControllable)
    {
      return;
    }

    var rigidbody = GetComponent<Rigidbody2D>();
    if (Input.GetAxis("Horizontal") < 0)
    {
      rigidbody.velocity = new Vector2(-1f, rigidbody.velocity.y);
    }
    else if (Input.GetAxis("Horizontal") > 0)
    {
      rigidbody.velocity = new Vector2(1f, rigidbody.velocity.y);
    }
    else
    {
      rigidbody.velocity = new Vector2(0f, rigidbody.velocity.y);
    }
    if (Input.GetButtonDown("Jump"))
    {
      rigidbody.AddForce(new Vector2(0f, 250f));
    }
  }

  /// <summary>
  /// 他のオブジェクトと衝突した際の処理
  /// </summary>
  /// <param name= "coll">Coll.</param>
  private voidOnCollisionEnter2D(Collision2D coll)
  {
    if ("Enemy" == coll.gameObject.tag)
    {
      Debug.Log("ここきてる??");
      StartCoroutine(CollisionToEnemyCoroutine(coll));
    }
  }

  /// <summary>
  /// Enemyと当たった時のリアクション
  /// </summary>
  /// <returns>The to enemy coroutine.</returns>
  /// <param name="coll">Coll.</param>
  private IEnumerator CollisionToEnemyCoroutine(Collision2D coll)
  {
    // 操作不能になる
    isControllable = false;
    Debug.Log("ここきてる??");
    var rigidbody = GetComponent<Rigidbody2D>();
    // 吹き飛ばされる
    rigidbody.AddForce(new Vector2(transform.position.x < coll.transform.position.x? -100f: 100f, 100f));
    yield return new WaitForSeconds(0.5f);
    // 0.5秒後、操作可能に
    isControllable = true;
  }
}

これで準備は完了です。
動かしてみましょう!

[動画]
https://yo utu.be/SsIH1gCLyXM

ゲームが始まると、ジリジリとサボテンが迫ってきます。
少し離れると弾を撃ってきて、ひよこちゃんが高いところに行くとジャンプしてついてこようとします。

簡単なスクリプトでもそれっぽい動きが作れました。

また、敵や弾にぶつかるとひよこちゃんが弾き飛ばされます。
これもいい感じに仕上がりましたね。

■まとめ

今回はスクリプトがメインでしたので、少し難しかったかもしれません。

コードはゲームシーンのように見た目で直感的にわかるものではないので、自在に扱えるよ うになるに は慣れが必要です。
ただ、コードが書けるとゲーム開発の幅が格段に広がります。

慣れてくるとコードを書くこと自体が楽しくなっていきますので、ぜひ身に付けていきましょう。

次回はエフェクトを使ってゲームを演出する方法を学びながら、アイテムの実装を進めていきます。

原稿:賀好 昭仁 qnoteスマホアプリ開発チーム技術主任。PHP・Android・iOS・Unityなど複数のプラットフォームでの開発を行う。
しばしば7匹の先輩猫社員たちにイスを占領される。

この記事はどうでしたか?

おすすめの記事

キャリアを考える

BACK TO TOP ∧

FOLLOW

  • facebook
  • twitter
  • B!
  • RSS