0から2Dアクションバトルゲームを作ろう!⑤ボスの攻撃を作ろう

0からアクションゲームを作ろう講座その5です。
ボスの攻撃を実装し、ゲームとして遊べる状態に仕上げましょう!

この章でやること

前章までの開発で、プレイヤーの攻撃でボスを倒せる所までやってきました。
後はボスの攻撃を実装し、スリルある撃ち合いが出来るようにしていきます。

またプログラミングをスムーズにする為に、自分で新しくメソッドを作成するという経験もしてみましょう。
今まで程難しくはないですが、ここまでの基礎知識を理解できていないと大変ですので、不安な方はここまでを振り返っておくと良いでしょう。

  1. ゲーム画面の作成
  2. プレイヤーキャラを動かそう
  3. 弾を発射させてみよう
  4. 当たり判定を実装しよう

 

目標とする動作

前章の最後でプレイヤー側に当たり判定と体力の実装をしたので、ボスが弾を撃ってくれればそれの確認が出来ます。
まずは左方向に直進する弾を発射させてみましょう。

その後は難易度を上げるために、ボスがプレイヤーに向かって弾を発射する(通称:自機狙い弾)機能を実装してみます。
これによってプレイヤーには安全地帯が無くなり、ボスの攻撃を避け続ける必要が出てきます。

ボスに弾を撃たせよう

それではボスのスクリプトを編集し、弾を発射させましょう!

一定時間ごとに弾を発射

弾発射部分は今までの復習になります。わからない部分は丸写しでも構わないので、完成させることを優先させてみましょう。

BossController.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class BossController : MonoBehaviour {

	// メンバ変数宣言
	public GameObject bulletPrefab; // 弾のプレハブ
	public int health;  // 体力
	private float time; // 経過時間(秒)
	
	// 毎フレーム呼び出されるメソッド
	void Update ()
	{
		// 経過時間をカウント
		time += Time.deltaTime;

		// -----弾発射処理-----
		// 1秒ごとに弾を発射
		if (time > 1.0f)
		{ // 経過時間が1秒より大きければ
			// 経過時間をリセット
			time -= 1.0f;

			// GameObject型ローカル変数を宣言
			GameObject obj;
			// 弾プレハブのインスタンスを生成し、変数objに格納
			obj = Instantiate (bulletPrefab);
			// 弾インスタンスの座標にボスの座標をセット
			obj.transform.position = transform.position;
			// インスタンスのオブジェクト名を変更(自弾と区別するため)
			obj.name = "EnemyBullet";
		}
	}

	// 当たり判定内に他オブジェクトが侵入した際呼び出されるメソッド
	// 引数:接触オブジェクトしたオブジェクトのCollider情報
	void OnTriggerEnter2D (Collider2D collider) {...}
}

このサンプルコードではOnTriggerEnter2Dメソッドに変化がない為記載を省略しています。削除する訳ではないのでご注意ください。

スクリプトの変更が終わったら、ボスオブジェクト(内のBossControllerコンポーネント)と弾プレハブの紐づけを忘れずに行いましょう。

現在の動作を確認


ボスの弾がプレイヤーの弾と同じく右方向のみに飛んでいってしまいますね。
これではバトルにならないので、スクリプトを再び書き加える必要があります。

なお弾オブジェクトのスプライト(絵)がボスの後ろに隠れるのが嫌な場合、
BulletPrefab内のSprite Rendererコンポーネントの設定の内、「Order in Layer」を変えると表示順を変更させられます。
気になる方は変更しておきましょう。(1でとりあえず手前に表示されます。)

弾のスクリプトを改良しよう

弾がまっすぐ右にしか飛ばないのは弾のスクリプトに原因があります。
そこまで難しい事は行いませんが、広範にわたって変化があるので注意して見ていきましょう。

速度と角度を変更可能にする

今回、弾の速度や角度等によって使用するスクリプト(コンポーネント)を変える事はしません。
つまり1つのスクリプトを自弾・敵弾で共通して利用する為、弾側で速度や角度の変化に対応する必要があります。

具体的には、弾を発射するプレイヤー・ボスそれぞれが発射時に速度・角度を指定します。
弾のスクリプトは受け取ったそれらの情報を元に座標計算を行っていくという流れです。

publicに指定したメンバ変数は当然他のスクリプトからも可能になる為、今回はその機能を利用しましょう。

Bullet.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Bullet : MonoBehaviour {

	// メンバ変数宣言
	private float time = 0.0f;  // 経過時間
	// 生成時指定パラメータ
	public float speed;		// 速度
	public float angle;     // 角度(Degree)
	public float limitTime;	// 生存時間(秒)

	// 毎フレーム呼び出されるメソッド
	void Update ()
	{
		// -----移動処理-----
		// 変数宣言
		float rad;		// 角度(Radian)←計算に使用する
		Vector2 vec;    // 移動量ベクトル(x方向, y方向)
		float move_x, move_y;	// x方向、y方向それぞれの移動量格納用

		// 角度変数angleをラジアン値に変換して変数radに格納
		rad = angle * Mathf.Deg2Rad;

		// 速度と角度から移動量を算出
		move_x = Mathf.Cos (rad) * speed * Time.deltaTime; // x方向の移動量
		move_y = Mathf.Sin (rad) * speed * Time.deltaTime; // y方向の移動量
		vec = new Vector2 (move_x, move_y); // 変数vecを初期化しつつ、xとyの移動量をセット

		// 変数vec分だけ自オブジェクトを移動
		transform.Translate (vec);

		// -----寿命処理-----
		// 前回のUpdate実行から経過した時間をtimeに加算
		time += Time.deltaTime;
		// 消滅処理
		if (time > 5.0f)
		{ // 弾の経過時間が5秒より大きければ
			Destroy (gameObject);
		}
	}
}

このように弾のスクリプトを編集すれば、弾の生成時に一緒に速度・角度・生存時間の指定をしてあげる事でそれに沿った動作をしてくれるようになります。

途中で出てきた「Mathf」というのはクラスの1つなのですが、これはスクリプト中でのみ使用するクラスであり、SinやCosを始めとした様々な数学的計算をメソッドや定数でサポートしてくれます。
非常に沢山の機能がありますので、スクリプトリファレンスを読んでおくときっと役に立つはずです。(今すぐに理解する必要はありません)
https://docs.unity3d.com/ja/2017.4/ScriptReference/Mathf.html

弾発射時の処理を変更

弾がデフォルトで速度や角度を持たず、生成時(発射時)に指定するようになった為、もちろん今までのスクリプトにも変更が必要です。
まずはボスから修正しましょう。
BossController.cs内Update()メソッド

	// 毎フレーム呼び出されるメソッド
	void Update ()
	{
		// 経過時間をカウント
		time += Time.deltaTime;

		// -----弾発射処理-----
		// 1秒ごとに弾を発射
		if (time > 1.0f)
		{ // 経過時間が1秒より大きければ
			// 経過時間をリセット
			time -= 1.0f;

			// GameObject型ローカル変数を宣言
			GameObject obj;
			// 弾プレハブのインスタンスを生成し、変数objに格納
			obj = Instantiate (bulletPrefab);
			// 弾インスタンスの座標にボスの座標をセット
			obj.transform.position = transform.position;
			// インスタンスのオブジェクト名を変更(自弾と区別するため)
			obj.name = "EnemyBullet";
			// 弾のパラメータをセット
			obj.GetComponent<Bullet> ().speed = 4.0f;		// 速度
			obj.GetComponent<Bullet> ().angle = 180.0f;		// 角度
			obj.GetComponent<Bullet> ().limitTime = 5.0f;	// 生存時間
			// 弾の色を変更
			obj.GetComponent<SpriteRenderer> ().color = Color.magenta; // マゼンタ
		}
	}

GetComponent<>()メソッドでBulletクラスを参照し、値をそれぞれセットしています。
ついでなので弾の色も変えてみました。Colorクラスにはいくつかの色パターンが定数として用意されており、今回はマゼンタカラーを使用しています。

角度についてですが、セットする時は弧度法で指定するようにしています。
なので0から360までのfloat値を入れるのですが、右が0°、左が180°の反時計周りで値を入力します。

プレイヤーも同じように修正していきます。
PlayerController.cs内Update()メソッド

	// 毎フレーム呼び出されるメソッド
	void Update ()
	{
		// -----移動処理-----
		(省略)

		// -----弾発射処理-----
		if (Input.GetMouseButtonDown (0))
		{ // 左クリックを押された瞬間
			// GameObject型ローカル変数を宣言 (生成したインスタンスを格納する)
			GameObject obj;
			// 弾プレハブのインスタンスを生成し、変数objに格納
			obj = Instantiate (bulletPrefab);
			// 弾インスタンスの座標にプレイヤーの座標をセット
			obj.transform.position = transform.position;
			// インスタンスのオブジェクト名を変更(敵弾と区別するため)
			obj.name = "PlayerBullet";
			// 弾のパラメータをセット
			obj.GetComponent<Bullet> ().speed = 6.0f;       // 速度
			obj.GetComponent<Bullet> ().angle = 0.0f;		// 角度
			obj.GetComponent<Bullet> ().limitTime = 5.0f;   // 生存時間
			// 弾の色を変更
			obj.GetComponent<SpriteRenderer> ().color = Color.cyan; // シアン
		}
	}

動作確認


これでやっと撃ち合いができるようになりました…が、プレイヤー側に安全地帯があり一方的に攻撃できてしまいます。
それでは簡単すぎるためプレイヤーを狙って撃つ自機狙い弾がやはり必要です。

また、まだ見た目が地味すぎると感じるかもしれませんが、これは第6回以降で改善していきます。
もう我慢ならない!という方は第7回冒頭の「背景に画像を付ける」項を先に読むと良いでしょう。

自機狙い弾を発射させよう

続いては、ボスがプレイヤーに向けて弾を発射する、通称「自機狙い弾」を作成しましょう。
計算するのは角度のみなので、弾のスクリプトは変更不要です。

プレイヤーの情報を取得

自機狙い弾で必要になるのは、ボスから見たプレイヤーの方向です。
より突き詰めて言えば、「自身の座標」と「プレイヤーの座標」の2つが必要です。

自身の座標は勿論transform.positionで持ってこられるので、後はPlayerオブジェクトをスクリプトから参照できるようにしましょう。
流れは今までと同じで、public変数を使いインスペクターウィンドウから設定します。
BossControllerのメンバ変数に以下を加え、Playerオブジェクトをセットしましょう。

public GameObject playerObj;	// プレイヤーオブジェクト

上記の画像のようにセットしましょう。

これでスクリプト上でいつでもプレイヤーの座標を取得できます。

計算で角度を求める

それではこの2つの座標からプレイヤーへの角度を求めて、弾の発射時角度として設定しましょう。

少々強引ですが、弾発射処理の途中にこの計算を入れ込んでみます。
不便で見づらいソースコードとなるため後述の「メソッド化」を後で施します。

BossController.cs内のUpdate()メソッド内の弾発射処理

		// -----弾発射処理-----
		// 1秒ごとに弾を発射
		if (time > 1.0f)
		{ // 経過時間が1秒より大きければ
			// 経過時間をリセット
			time -= 1.0f;

			// GameObject型ローカル変数を宣言
			GameObject obj;
			// 弾プレハブのインスタンスを生成し、変数objに格納
			obj = Instantiate (bulletPrefab);
			// 弾インスタンスの座標にボスの座標をセット
			obj.transform.position = transform.position;
			// インスタンスのオブジェクト名を変更(自弾と区別するため)
			obj.name = "EnemyBullet";
			// プレイヤーへの角度を計算
			float playerAng;	// プレイヤーへの角度
			Vector2 target;		// プレイヤーの相対座標
			target = playerObj.transform.position - transform.position; // 相対座標を計算
			playerAng = Mathf.Atan2 (target.y, target.x); // 相対座標から角度(Radian)を計算
			playerAng *= Mathf.Rad2Deg; // Radian角度をDegree角度に変換
			// 弾のパラメータをセット
			obj.GetComponent<Bullet> ().speed = 4.0f;		// 速度
			obj.GetComponent<Bullet> ().angle = playerAng;		// 角度
			obj.GetComponent<Bullet> ().limitTime = 5.0f;	// 生存時間
			// 弾の色を変更
			obj.GetComponent<SpriteRenderer> ().color = Color.magenta; // マゼンタ
		}

この処理によって変数playerAng内にプレイヤーへの角度がDegreeで算出されます。
それを弾の角度にセットしてあげると自機狙い弾の出来上がりです。


これで安全地帯はなくなり、避けなければ負けるようになりました。

ボスの弾発射処理をメソッド化しよう

Update()メソッドなどの処理が複雑になったり、長大になるとソースコードが読みにくくなり、バグが発生する原因になりがちです。
また同じ処理を繰り返すような場合、長い処理をコピーペーストすると後から変更を加えるのも難しくなります。

プログラミングにおいて、処理は機能単位でメソッド化すると上記の問題を解決しやすいです。
メソッドはメンバ変数と同じくクラスの機能として扱われ、publicで作成すると他のクラスからそのメソッドを呼び出す事も可能になります。

メソッドの作り方

メソッドの作り方は今まで書いてきたものと同じです。Start()やUpdate()と同じであり、自動的に呼び出される機能が無いという違いだけです。
なので新しく覚える事はほとんどありませんが、今一度「引数」と「戻り値」についての理解が必要です。改めてこれら2つの機能について整理してみましょう。

メソッド名の左側に戻り値(型)、右側に引数(型と引数名)を指定します。
この2つについて簡単に説明します。

  • 戻り値
    メソッドの呼び出し元に返すデータです。ここでは型名のみを指定します。
    型は何でもよく、intやfloatなら数値情報を、GameObjectならオブジェクト情報を返せます。何も返さないならvoidを指定します。
    void以外の場合メソッド内の処理が終了する時、必ず「return 〇〇;」という文が必要です。〇〇には型に合ったデータを入れます。
  • 引数
    メソッド内に呼び出し元から渡されるデータです。引数名はそのままメソッド内で変数名として扱えます。
    引数は2つ以上指定可能であり、その場合引数間は,(カンマ)で区切ります。

例として、引数で渡されたint型数値のAとBの和を返すメソッドを作る場合は以下のようになります。

int Plus (int A, int B)
{
	return A + B;
}

これを他のメソッドから呼び出す場合、
result = Plus (X, 3);
のようになります。この例ですと変数resultには変数Xと3を足した数値が代入されます。

ちなみに引数で渡されたデータをメソッド内で変更した場合、呼び出し元にその変更が適用される場合と適用されない場合があります。
値渡しや参照渡しなどという仕組みがあるのですが、難しいので分からない内はなるべく引数の中身を変えないようにしましょう。

今回メソッド化するもの

メソッドの仕組みを学んだ所で、早速実践してみましょう。

現在、ボスのスクリプトは少し拡張性が悪くなっています。読みづらさという点ではまだ大きな問題はありませんが、
今後弾を何発も同時に撃たせたりする場合一気に冗長なスクリプトになります。

それをメソッドで解決しようと思います。
今回メソッド化の対象とするのは以下の機能です。

  • プレイヤーへの角度計算機能
  • 弾を発射し、速度や角度などをセットする機能

プレイヤーとの角度を計算するメソッド

まずは先ほど組んだばかりの角度計算処理をメソッド化します。

BossController.cs内新規メソッド

	// プレイヤーへの角度計算メソッド
	float GetAngleToPlayer ()
	{
		// 変数宣言
		float playerAng;    // プレイヤーへの角度
		Vector2 target;     // プレイヤーの相対座標

		// 角度計算処理
		target = playerObj.transform.position - transform.position; // 相対座標を計算
		playerAng = Mathf.Atan2 (target.y, target.x); // 相対座標から角度(Radian)を計算
		playerAng *= Mathf.Rad2Deg; // Radian角度をDegree角度に変換

		// 計算した角度情報を呼び出し元に返す
		return playerAng;
	}

プレイヤーの座標はメンバ変数から使用しているので引数で渡す必要はありません。
戻り値としてプレイヤーへの角度が返ってきます。

このメソッドを弾のパラメータセット時に呼び出せば、先ほどと同じ動作が実現します。
次項にスクリプト全文がありますので、使い方が不安な方はそちらを参考にしてください。

弾発射メソッド

前項でメソッド1つでプレイヤーへの角度を得られるようになりましたが、肝心の弾生成処理が長大である為これもメソッド化したいですね。
今度は引数で速度や角度を指定できるようにし、思い通りの弾を1行のスクリプトで発射できるようにしましょう!

BossController.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class BossController : MonoBehaviour {

	// メンバ変数宣言
	public GameObject bulletPrefab; // 弾のプレハブ
	public GameObject playerObj;	// プレイヤーオブジェクト
	public int health;  // 体力
	private float time; // 経過時間(秒)
	
	// 毎フレーム呼び出されるメソッド
	void Update ()
	{
		// 経過時間をカウント
		time += Time.deltaTime;

		// -----弾発射処理-----
		// 1秒ごとに弾を発射
		if (time > 1.0f)
		{ // 経過時間が1秒より大きければ
			// 経過時間をリセット
			time -= 1.0f;
			
			// 弾を発射
			Shot (14.0f, GetAngleToPlayer (), 5.0f);
		}
	}

	// プレイヤーへの角度計算メソッド
	float GetAngleToPlayer ()
	{
		(省略)
	}

	// 弾を発射するメソッド
	// 第1引数 speed : 速度
	// 第2引数 angle : 角度
	// 第3引数 limitTime : 生存時間(秒)
	void Shot (float speed, float angle, float limitTime)
	{
		// GameObject型ローカル変数を宣言
		GameObject obj;
		// 弾プレハブのインスタンスを生成し、変数objに格納
		obj = Instantiate (bulletPrefab);
		// 弾インスタンスの座標にボスの座標をセット
		obj.transform.position = transform.position;
		// インスタンスのオブジェクト名を変更(自弾と区別するため)
		obj.name = "EnemyBullet";
		// 弾のパラメータをセット
		obj.GetComponent<Bullet> ().speed = speed;			// 速度
		obj.GetComponent<Bullet> ().angle = angle;			// 角度
		obj.GetComponent<Bullet> ().limitTime = limitTime;  // 生存時間
		// 弾の色を変更
		obj.GetComponent<SpriteRenderer> ().color = Color.magenta; // マゼンタ
	}

	// 当たり判定内に他オブジェクトが侵入した際呼び出されるメソッド
	// 引数:接触オブジェクトしたオブジェクトのCollider情報
	void OnTriggerEnter2D (Collider2D collider)
	{
		(省略)
	}
}

メソッドの作り方は理解できましたか?
少しずつ使いこなして可読性や拡張性の高いスクリプトを書けるように目指しましょう。

沢山の弾を発射させる

せっかく1行で弾を発射できるようになったので、沢山の弾を同時発射させてみましょう。
どのくらい難易度を上げるかは皆さんの感覚にお任せします。速度や角度も自由に指定できる事を利用しましょう。

例えばこのような書き方をします。
BossController.cs内のUpdate()メソッド内の弾発射処理

		// -----弾発射処理-----
		// 0.4秒ごとに弾を発射
		if (time > 0.4f)
		{ // 経過時間が0.4秒より大きければ
			// 経過時間をリセット
			time -= 0.4f;

			// 高速弾を発射
			Shot (8.0f, GetAngleToPlayer (), 5.0f);
			// 3way弾を発射
			Shot (5.0f, GetAngleToPlayer () - 20.0f, 5.0f);
			Shot (5.0f, GetAngleToPlayer ()        , 5.0f);
			Shot (5.0f, GetAngleToPlayer () + 20.0f, 5.0f);
		}

少しずつ書き換えてはテストプレイを繰り返し、好きな弾幕を作ってください!

これまでの機能を全て実装すると上のようになります。(実際のプレイ時はもう少しゆっくりです)

現在はプレイヤーがやられた場合の処理を実装していないので、キャラクターがやられた場合はエラーが発生してゲームが止まってしまいます。今後その不具合も解消していきましょう。

*BossController.csについてはこちらからダウンロードできるようにしました。ここまでのスクリプトでわからない箇所がある場合にはこちらからDLして確認してみてください。

ここまでのおさらい

ボスの攻撃を実装し、ゲームとしてはだいぶ体裁が整ってきました。

今回学んだ知識で大事なのはメソッドの作成と利用です。
1つのメソッド内に様々な処理を長く詰め込んでしまうと、読むのも書き加えるのも大変になってしまい、バグの発生を引き起こしやすくなります。
出来るだけ機能単位でメソッドを分割し、戻り値や引数を利用した適切なデータのやり取りによってスマートで拡張しやすいスクリプトを書きましょう。

次章で学ぶこと

次章ではUI(ユーザーインターフェース)の実装に入ります。
UIも画面に表示する物なのでゲームオブジェクトなのですが、CanvasというUI用の機能を用いていきます。

現在、画面にはプレイヤーとボスと弾が映っているのみでまだ寂しいですね。
プレイヤーとボスの現在体力をリアルタイムで表示するUIを作成してみましょう。

 

<前回>  <=  「④当たり判定を実装しよう

<次回>  =>  「⑥UIを表示してみよう