• チュートリアル
  • 機能解説
  • リファレンス
  • 初代Altseedはコチラ

    • チュートリアル
    • 0章 : Altseedを初めよう
    • 1章 : ウィンドウを表示してみよう
    • 2章 : キャラクターを描画してみよう
    • 3章 : キャラクターに弾を撃たせてみよう
    • 4章 : クラスを自分で設計してみよう
    • 5章 : 敵・敵のショットを表示してみよう
    • 6章 : 当たり判定の機能を使ってみよう
    • 7章 : 音を鳴らしてみよう
    • 8章 : 得点を表示してみよう
    • 9章 : タイトル・ゲームオーバー画面を作ってみよう
    • 10章 : いざ、公開準備

    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)));
            }
        }
    }
    

    Meteor

    弾を打つ敵

    弾を打つ敵を用意しますが、その前に弾を共通化します。 味方の弾と敵の弾を全く異なるクラスにしてもいいですが、ほとんどの機能は共通なので同じようなコードが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)));
            }
        }
    }
    

    StraightShotEnemy

    他の敵

    他の敵もそれぞれ実装します。

    複数方向に打てる敵です。 経過時間を計測し、経過時間ごとに異なる方向に弾を打ちます。

    • 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));
            }
        }
    }
    

    Enemies

    続・敵の出現

    いままでのだと、敵が一気に出現するし、そのあとにも敵は出現しないので面白くありません。 そこで複数の敵が順番に出るようにします。

    ここでは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());
    +               }
    +           }
    +       }
        }
    }
    

    Finish

    まとめ

    ここでは敵の処理を実装しました。 ただ、この章で弾は打てるようになりましたが、一切攻撃は命中しません。 次章では、弾が命中するようにします。

    Copyright © 2020 Altseed .