5章 : 敵・敵のショットを表示してみよう
今まではプレイヤーの挙動のみでした。 次は敵の実装を行います。
本章では継承を使用します。 確かに、今までも既存のクラスを継承することはありました。 この継承元のクラスは自分でも作成できます。
敵は様々な挙動をしますが、それぞれ個別に実装するのも管理するのも大変です。 それらを共通化するために継承があります。
敵クラスの実装
まずは敵を実装します。 基本的にはプレイヤーと変わりません。 プレイヤーとの違いとして、プレイヤーを攻撃するためにプレイヤーへの参照を持っていること、画面外にでたら自動的に消えることが違います。 画面外にでても常に敵が残り続けると、敵が多すぎて重くなってしまいます。 そのため、画面外に出たら消えるようになっています。
Enemy.csを追加し、下記のコードを記述します。
- Enemy.cs
using Altseed2;
namespace Tutorial
{
// 敵の基礎となるクラス
public class Enemy : SpriteNode
{
// 倒された時に加算されるスコアの値
protected int score;
// プレイヤーへの参照
protected Player player;
// コンストラクタ
public Enemy(Player player, Vector2F position)
{
// 座標を設定
Position = position;
// プレイヤーへの参照を設定
this.player = player;
}
// フレーム毎に実行
protected override void OnUpdate()
{
// 画面外に出たら自身を削除
RemoveMyselfIfOutOfWindow();
}
// 画面外に出た時自身を消去
protected void RemoveMyselfIfOutOfWindow()
{
var halfSize = Texture.Size / 2;
if (Position.X < -halfSize.X
|| Position.X > Engine.WindowSize.X + halfSize.X
|| Position.Y < -halfSize.Y
|| Position.Y > Engine.WindowSize.Y + halfSize.Y)
{
// 自身を削除
Parent?.RemoveChildNode(this);
}
}
}
}
前章では、protected
やpublic
の説明をしていませんでした。
これらはアクセス指定子といいます。
そのメンバー変数やメソッドにクラスの外部から使用できるかを指定します。
public
は外部から使用できる、protected
は継承先を含めたクラス内、private
、もしくは記述なしはクラス内のみ使用可能です。
今回の場合、Enemyクラスは継承して使用するので、多くのメンバー変数がprotected
になっています。
また、Parent?.RemoveChildNode(this);
という記述があります。
これは、
if(Parent != null)
Parent.RemoveChildNode(this);
と同じ意味です。nullでなかったら、何らかの処理を記述する、ということが多々あるため、簡単に記述できるようになっています。
ただ、見ての通り、この敵は動きもしないし攻撃もしません。 それでは、このクラスを継承して敵を実装しましょう。
隕石
先ほどのEnemyクラスを継承して隕石クラスを記述します。 Meteor.csを追加し、下記のコードを記述します。
- Meteor.cs
using Altseed2;
namespace Tutorial
{
// 隕石
public class Meteor : Enemy
{
// フレーム毎の移動速度
private Vector2F velocity;
// コンストラクタ
public Meteor(Player player, Vector2F position, Vector2F velocity) : base(player, position)
{
// 速度の設定
this.velocity = velocity;
// テクスチャの設定
Texture = Texture2D.LoadStrict("Resources/Meteor.png");
// 中心座標の設定
CenterPosition = ContentSize / 2;
// スコアの設定
score = 1;
}
// 毎フレーム実行
protected override void OnUpdate()
{
// 座標を速度分加算
Position += velocity;
// EnemyクラスのOnUpdate呼び出し
base.OnUpdate();
}
}
}
隕石は移動するだけの敵です。 更新するごとに速度の分、位置を動かしていきます。
敵の出現
敵のクラスを用意しただけでは、敵は出現してくれません。 そこでMainNodeを編集して、敵が出現するようにします。
- MainNode.cs
using Altseed2;
+using System.Collections.Generic;
namespace Tutorial
{
// メインステージのクラス
public class MainNode : Node
{
// キャラクターを表示するノード
private Node characterNode = new Node();
// プレイヤーの参照
private Player player;
// エンジンに追加された時に実行
protected override void OnAdded()
{
// キャラクターノードを追加
AddChildNode(characterNode);
// UIを表示するノード
var uiNode = new Node();
// UIノードを追加
AddChildNode(uiNode);
// 背景に使用するテクスチャ
var backTexture = new SpriteNode();
// 背景のテクスチャを読み込む
backTexture.Texture = Texture2D.LoadStrict("Resources/Background.png");
// 表示位置を奥に設定
backTexture.ZOrder = -100;
// 背景テクスチャを追加
AddChildNode(backTexture);
// プレイヤーを設定
player = new Player(new Vector2F(100, 360));
// キャラクターノードにプレイヤーを追加
characterNode.AddChildNode(player);
+ // 敵を追加する。
+ characterNode.AddChildNode(new Meteor(player, new Vector2F(910, 400), new Vector2F(-4.0f, 0.0f)));
}
}
}
弾を打つ敵
弾を打つ敵を用意しますが、その前に弾を共通化します。 味方の弾と敵の弾を全く異なるクラスにしてもいいですが、ほとんどの機能は共通なので同じようなコードが2箇所に書かれてしまいます。 そのため、弾クラスを用意して、それを継承するようにします。
前章で作成した弾クラスのコンストラクタを一部修正します。 弾の画像に関する部分を消しています。
- Bullet.cs
using Altseed2;
namespace Tutorial
{
// 弾のクラス
public class Bullet : SpriteNode
{
// フレーム毎に進む距離
private Vector2F velocity;
// コンストラクタ
public Bullet(Vector2F position, Vector2F velocity)
{
// 座標を設定
Position = position;
- // テクスチャを読み込む
- Texture = Texture2D.LoadStrict("Resources/Bullet_Blue.png");
-
- // 中心座標を設定
- CenterPosition = ContentSize / 2;
// 弾速を設定
this.velocity = velocity;
// 表示位置をプレイヤーや敵より奥に設定
ZOrder--;
}
// ================================================================
// 省略
// ================================================================
}
}
それに合わせて、プレイヤーのコードも変更します。 プレイヤーの弾クラスは弾クラスを継承するようにします。 それに合わせて、プレイヤーはプレイヤーの弾クラスを発射するようにします。
新たにBulletクラスを継承してPlayerBulletクラスを追加します。
- PlayerBullet.cs
using Altseed2;
namespace Tutorial
{
// 自機弾
public class PlayerBullet : Bullet
{
// コンストラクタ
public PlayerBullet(Vector2F position) : base(position, new Vector2F(10f, 0.0f))
{
// テクスチャを読み込む
Texture = Texture2D.LoadStrict("Resources/Bullet_Blue.png");
// 中心座標を設定
CenterPosition = ContentSize / 2;
}
}
}
- Player.cs
PlayerBulletを撃つように変更します。
using Altseed2;
namespace Tutorial
{
// プレイヤーのクラス
public class Player : SpriteNode
{
// ================================================================
// 省略
// ================================================================
// ショット
private void Shot()
{
// Zキーでショットを放つ
if (Engine.Keyboard.GetKeyState(Key.Z) == ButtonState.Push)
{
+ Parent.AddChildNode(new PlayerBullet(Position));
- Parent.AddChildNode(new Bullet(Position, new Vector2F(10f, 0f)));
}
}
}
}
次に敵の弾と弾を打つ敵クラスを実装します。 基本的には味方が弾を打つ処理と、敵の移動を組み合わせたものになります。 それぞれ、敵の弾クラスは弾クラスを継承し、弾を打つ敵クラスは敵クラスを継承します。
- StraightShotEnemy.cs
using Altseed2;
using System;
namespace Tutorial
{
// まっすぐな弾を発射する敵
public class StraightShotEnemy : Enemy
{
// カウンタ
private int count = 0;
// コンストラクタ
public StraightShotEnemy(Player player, Vector2F position) : base(player, position)
{
// テクスチャを読み込む
Texture = Texture2D.LoadStrict("Resources/UFO.png");
// 中心座標を設定
CenterPosition = ContentSize / 2;
// 倒された時に加算されるスコアを設定
score = 20;
}
// フレーム毎に実行
protected override void OnUpdate()
{
// カウントが150の倍数で実行
if (count % 150 == 0)
{
// プレイヤーに対するベクトルの単位ベクトルを取得
var velocity = (player.Position - Position).Normal;
// ベクトルの長さを調整(弾速になる)
velocity *= 5;
// 弾を追加
Shot(velocity);
}
// 座標を設定
Position -= new Vector2F(MathF.Sin(MathHelper.DegreeToRadian(count)) * 3.0f, 0);
// EnemyのOnUpdateを実行
base.OnUpdate();
// カウントを進める
count++;
}
// 弾を撃つ
private void Shot(Vector2F velocity)
{
// 敵弾を画面に追加
Parent.AddChildNode(new EnemyBullet(Position, velocity));
}
}
}
- EnemyBullet.cs
using Altseed2;
namespace Tutorial
{
// 敵の弾のクラス
public class EnemyBullet : Bullet
{
// コンストラクタ
public EnemyBullet(Vector2F position, Vector2F velocity) : base(position, velocity)
{
// テクスチャを読み込む
Texture = Texture2D.LoadStrict("Resources/Bullet_Red.png");
// 中心座標を設定
CenterPosition = ContentSize / 2;
}
}
}
この敵も出現するようにしましょう。 MainNodeに敵を追加します。
- MainNode.cs
using Altseed2;
using System.Collections.Generic;
namespace Tutorial
{
// メインステージのクラス
public class MainNode : Node
{
// キャラクターを表示するノード
private Node characterNode = new Node();
// プレイヤーの参照
private Player player;
// エンジンに追加された時に実行
protected override void OnAdded()
{
// ================================================================
// 省略
// ================================================================
// 敵を追加する。
+ characterNode.AddChildNode(new StraightShotEnemy(player, new Vector2F(600, 620)));
characterNode.AddChildNode(new Meteor(player, new Vector2F(910, 400), new Vector2F(-4.0f, 0.0f)));
}
}
}
他の敵
他の敵もそれぞれ実装します。
複数方向に打てる敵です。 経過時間を計測し、経過時間ごとに異なる方向に弾を打ちます。
- RadialShotEnemy.cs
using Altseed2;
namespace Tutorial
{
// 放射ショットの敵
public class RadialShotEnemy : Enemy
{
// カウンタ変数
private int count = 0;
// 撃ち出すショットの個数
private int shotAmount;
// フレーム毎の速度
private Vector2F velocity;
// コンストラクタ
public RadialShotEnemy(Player player, Vector2F position, int shotAmount) : base(player, position)
{
// 撃ち出すショットの個数を設定
this.shotAmount = shotAmount;
// テクスチャを読み込む
Texture = Texture2D.LoadStrict("Resources/UFO.png");
// 中心座標を設定
CenterPosition = ContentSize / 2;
// スコアを設定
score = 30;
}
// フレーム毎に実行
protected override void OnUpdate()
{
// カウントが250の倍数だったら
if (count % 250 == 0)
{
// 計算用のローカル変数
var half = shotAmount / 2;
for (int i = 0; i < shotAmount; i++)
{
// 現時点の座標からプレイヤーに向かうベクトルの単位ベクトルを取得する
var vector = (player.Position - Position).Normal;
// ベクトルを速度分掛ける
vector *= 7.0f;
// ベクトルを傾ける
vector.Degree += 30 * (i - half);
// ショットを放つ
Shot(vector);
}
}
// カウント÷100の余りが0~49だったら
if (count % 100 < 50)
{
// カウント÷100の余りが0だったら
if (count % 100 == 0)
{
// 進むベクトルを設定
velocity = (player.Position - Position).Normal * 3.0f;
}
// 速度分ベクトルを設定
Position += velocity;
}
// EnemyクラスのOnUpdateを呼び出す
base.OnUpdate();
// カウントを進める
count++;
}
// 弾を撃つ
private void Shot(Vector2F velocity)
{
// 敵弾を画面に追加
Parent.AddChildNode(new EnemyBullet(Position, velocity));
}
}
}
プレイヤーを追いかける敵です。 プレイヤーへの参照を使用し、プレイヤーのほうに近づきます。
- ChaseEnemy.cs
using Altseed2;
namespace Tutorial
{
// 追跡型敵
public class ChaseEnemy : Enemy
{
// 移動速度
private float speed;
// コンストラクタ
public ChaseEnemy(Player player, Vector2F position, float speed) : base(player, position)
{
// テクスチャを読み込む
Texture = Texture2D.LoadStrict("Resources/UFO.png");
// 中心座標を設定
CenterPosition = ContentSize / 2;
// 移動速度を設定
this.speed = speed;
// 自身が倒された時に加算されるスコアを設定
score = 10;
}
// フレーム毎に実行
protected override void OnUpdate()
{
// プレイヤーへのベクトルの単位ベクトルを取得
var vector = (player.Position - Position).Normal;
// ベクトルの長さを調整
vector *= speed;
// ベクトル分座標を動かす
Position += vector;
// EnemyのOnUpdateを実行
base.OnUpdate();
}
}
}
それぞれの敵を追加します。
using Altseed2;
using System.Collections.Generic;
namespace Tutorial
{
// メインステージのクラス
public class MainNode : Node
{
// キャラクターを表示するノード
private Node characterNode = new Node();
// プレイヤーの参照
private Player player;
// エンジンに追加された時に実行
protected override void OnAdded()
{
// ================================================================
// 省略
// ================================================================
// 敵を追加する。
+ characterNode.AddChildNode(new ChaseEnemy(player, new Vector2F(700, 160), 2.0f));
characterNode.AddChildNode(new StraightShotEnemy(player, new Vector2F(600, 620)));
characterNode.AddChildNode(new Meteor(player, new Vector2F(910, 400), new Vector2F(-4.0f, 0.0f)));
+ characterNode.AddChildNode(new RadialShotEnemy(player, new Vector2F(400, 160), 3));
}
}
}
続・敵の出現
いままでのだと、敵が一気に出現するし、そのあとにも敵は出現しないので面白くありません。 そこで複数の敵が順番に出るようにします。
ここではQueue
というクラスを使用しています。
これはListと同じようなものですが、挙動が異なります。
Listは、常に内部のコレクションの最後に値を追加するのみで、追加した後は任意の値にアクセスできました。
一方、Queueは、内部のコレクションの最後に値を追加し、取得するときは一番最初に追加した値を取得して、その値をコレクションから取り除きます。
追加には、Enqueue
、取り出しには、Dequeue
を使用します。
例えば、下記のような挙動になります。
Queue<int> queue = new Queue<int>();
queue.Enqueue(1);
queue.Enqueue(2);
// この時点ではqueueの中身には1,2がある
int value = queue.Dequeue();
// 1が表示される。
// この時点ではqueueの中身には2がある
Console.WriteLine(value);
これを使用して敵を管理します。
親ノードには敵を追加せず、Queueに敵ノードを追加します。 そして、一定時間ごとにQueueから敵ノードを取り出し、追加することで敵が徐々に出現するようにします。
using Altseed2;
+ using System.Collections.Generic;
namespace Tutorial
{
// メインステージのクラス
public class MainNode : Node
{
+ // カウンタ
+ private int count = 0;
+ // 敵を格納するキュー
+ private Queue<Enemy> enemies = new Queue<Enemy>();
// キャラクターを表示するノード
private Node characterNode = new Node();
// プレイヤーの参照
private Player player;
// エンジンに追加された時に実行
protected override void OnAdded()
{
// キャラクターノードを追加
AddChildNode(characterNode);
// UIを表示するノード
var uiNode = new Node();
// UIノードを追加
AddChildNode(uiNode);
// 背景に使用するテクスチャ
var backTexture = new SpriteNode();
// 背景のテクスチャを読み込む
backTexture.Texture = Texture2D.LoadStrict("Resources/Background.png");
// 表示位置を奥に設定
backTexture.ZOrder = -100;
// 背景テクスチャを追加
AddChildNode(backTexture);
// プレイヤーを設定
player = new Player(new Vector2F(100, 360));
// キャラクターノードにプレイヤーを追加
characterNode.AddChildNode(player);
- characterNode.AddChildNode(new StraightShotEnemy(player, new Vector2F(600, 620)));
-
- characterNode.AddChildNode(new RadialShotEnemy(player, new Vector2F(400, 160), 3));
-
- characterNode.AddChildNode(new StraightShotEnemy(player, new Vector2F(600, 620));
-
- characterNode.AddChildNode(new ChaseEnemy(player, new Vector2F(700, 160), 2.0f));
+ // ウェーブを初期化する
+ InitWave();
}
+ // ウェーブの初期化
+ private void InitWave()
+ {
+ // enemies.Enqueue~でウェーブに敵を追加
+ // 追加した順番に敵が出現する
+
+
+ enemies.Enqueue(new ChaseEnemy(player, new Vector2F(700, 160), 2.0f));
+
+ enemies.Enqueue(new StraightShotEnemy(player, new Vector2F(600, 620)));
+
+ enemies.Enqueue(new Meteor(player, new Vector2F(910, 400), new Vector2F(-4.0f, 0.0f)));
+
+ enemies.Enqueue(new RadialShotEnemy(player, new Vector2F(400, 160), 3));
+ }
+
+ // フレーム毎に実行
+ protected override void OnUpdate()
+ {
+ // ステージの更新
+ UpdateStage();
+
+ // カウントを進める
+ count++;
+ }
+
+ // 敵召還関連
+ private void UpdateStage()
+ {
+ // カウントが100の倍数だったら
+ if (count % 100 == 0)
+ {
+ // 敵が残っていたら画面に追加
+ if (enemies.Count > 0)
+ {
+ characterNode.AddChildNode(enemies.Dequeue());
+ }
+ }
+ }
}
}
まとめ
ここでは敵の処理を実装しました。 ただ、この章で弾は打てるようになりましたが、一切攻撃は命中しません。 次章では、弾が命中するようにします。