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

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

    6章 : 当たり判定の機能を使ってみよう

    5章では敵機と敵弾の作成までを行いました。しかしこのままでは当たり判定がないただのオブジェクトになってしまいます。
    本章では敵機と敵弾に当たり判定をつけていきます。

    当たり判定仕組み

    「当たり判定」とは読んで字の如く、オブジェクト同士が当たっているかどうか判定するものです。当たり判定の実装方法には色々な手法が考えられますが、今回は簡単のために「円同士の当たり判定をピクセル単位で取る」という方法を扱います。

    例えば下のような2つの円のオブジェクトを考え、2つの円の中心座標を考えてみましょう。2つの円の半径をa, bとおき、2つの円のx座標の差をd、y座標の差をeとおくと、中学校で習うような「三平方の定理」よりd²+e²<(a+b)²ならば2つの円は「ぶつかっている」ということになりますね。

    当たり判定

    当たり判定実装

    当たり判定の仕組みは上で説明したとおりですが、自分で一から実装するのは面倒なのでAltseed2ではこの当たり判定をまとめたクラスであるCircleColliderが用意されています。

    早速このCircleColliderを使って当たり判定を実装していきましょう。

    今回は自機、自弾、敵機、敵弾の4つに当たり判定をつけるので基底クラスとしてCollidableObjectクラスを用意してそれを継承していくような実装にします。
    継承については以下を参照してください。

    C# によるプログラミング入門 : 継承

    CollidableObjectのコードは以下のようになります。

    using System.Collections.Generic;
    using Altseed2;
    
    namespace Tutorial
    {
        // 衝突可能なオブジェクト(円形)
        public class CollidableObject : SpriteNode
        {
            // コライダのコレクション
            public static HashSet<CollidableObject> objects = new HashSet<CollidableObject>();
    
            // コライダ
            protected CircleCollider collider = new CircleCollider();
    
            // OnUpdate内で衝突判定を調査するかどうか
            protected bool doSurvey;
    
            // 所属するメインノードへの参照
            public MainNode mainNode;
    
            // コンストラクタ
            public CollidableObject(MainNode mainNode, Vector2F position)
            {
                // メインノードへの参照を設定
                this.mainNode = mainNode;
    
                // コライダの座標を設定
                collider.Position = position;
    
                // 座標を設定
                Position = position;
            }
    
            // エンジンに追加された時に実行
            protected override void OnAdded()
            {
                // コライダのコレクションに自身を追加
                objects.Add(this);
            }
    
            // エンジンから削除された時に実行
            protected override void OnRemoved()
            {
                // コライダのコレクションから自身を削除
                objects.Remove(this);
            }
    
            // フレーム毎に実行
            protected override void OnUpdate()
            {
                // フラグが成立時に衝突判定を実行
                if (doSurvey)
                {
                    Survey();
                }
    
                // コライダの座標を更新
                collider.Position = Position;
            }
    
            // 衝突時に実行
            private void CollideWith(CollidableObject obj)
            {
                // nullだったら終了
                if (obj == null)
                {
                    return;
                }
    
                // 衝突対象がSurveyを実行しないオブジェクトだった場合,相手のOnCollisionも実行
                if (!obj.doSurvey)
                {
                    obj.OnCollision(this);
                }
    
                // 自身のOnCollisiionを実行
                OnCollision(obj);
            }
    
            // 衝突時に実行される内容をオーバーライドして設定できる
            protected virtual void OnCollision(CollidableObject obj)
            {
    
            }
    
            // 画面外に出た時自身を消去
            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);
                }
            }
    
            // 衝突判定を調査する
            private void Survey()
            {
                // objects内の全オブジェクトを検索し,衝突が確認されたオブジェクト間でCollideWithを実行
                foreach (var obj in objects)
                    if (collider.GetIsCollidedWith(obj.collider))
                        CollideWith(obj);
            }
        }
    }
    

    これまでSpriteNodeを継承していたため、CollidableObjectでもSpriteNodeを継承しています。
    また、EnemyクラスとBulletクラスに定義していたRemoveMyselfIfOutOfWindow関数ですが同じ処理が二か所にあって冗長です。基本的に同じ処理は一か所にまとめた方が良いのでそれぞれの親クラスになるCollidableObjectでこの関数を定義することにします。

    変数とコンストラクタを解説していきます。

    // コライダのコレクション
    public static HashSet<CollidableObject> objects = new HashSet<CollidableObject>();
    
    // コライダ
    protected CircleCollider collider = new CircleCollider();
    
    // OnUpdate内で衝突判定を調査するかどうか
    protected bool doSurvey;
    
    // 所属するメインノードへの参照
    public MainNode mainNode;
    
    // コンストラクタ
    public CollidableObject(MainNode mainNode, Vector2F position)
    {
        // メインノードへの参照を設定
        this.mainNode = mainNode;
    
        // コライダの座標を設定
        collider.Position = position;
    
        // 座標を設定
        Position = position;
    }
    

    それぞれのコライダとの当たり判定をとるためにコライダのコレクションを保存しておく必要があります。コライダオブジェクトは変数objectsに保存します。
    この変数objectsの宣言にはstaticというキーワードが使われています。これは静的メンバーと呼ばれるもので、すべてのインスタンスから共有されるような変数を宣言できます。詳しい説明は以下のリンク先を参照してください。

    C# によるプログラミング入門 : 静的メンバー

    変数colliderはコライダの本体で、先ほど言ったようにAltseed2で用意されたCircleColliderを使用します。 doSurveyとmainNodeはコメントにある通りです。
    コンストラクタのcollider.Position = positionはコライダの位置設定でPosition = positionは本体の描画されているオブジェクトの位置設定であることに注意してください。

    次にOnUpdateです。

    // フレーム毎に実行
    protected override void OnUpdate()
    {
        // フラグが成立時に衝突判定を実行
        if (doSurvey)
        {
            Survey();
        }
    
        // コライダの座標を更新
        collider.Position = Position;
    }
    

    先ほど定義したdoSurveyのフラグがtrueの場合後述するSurvey関数が呼ばれて当たり判定が開始します。
    オブジェクトが動いてもコライダの位置は変わらないのでコライダとオブジェクトの位置と同期させるためにcollider.Position = Position;としています。

    さらにOnAdded関数とOnRemoved関数があると思います。こちらはエンジンにオブジェクトが追加されたタイミングと消去されたタイミングで呼ばれます。この時変数objectsに追加と削除をしてエンジンに追加されているオブジェクトのみをコレクションに残しておきます。

    Survey関数です。

    // 衝突判定を調査する
    private void Survey()
    {
        // objects内の全オブジェクトを検索し,衝突が確認されたオブジェクト間でCollideWithを実行
        foreach (var obj in objects)
            if (collider.GetIsCollidedWith(obj.collider))
                CollideWith(obj);
    }
    

    コライダ間の衝突はGetIsCollidedWith関数で取ることができます。この関数は衝突している場合trueを返すのでif文で衝突した場合にCollideWithが呼ばれます。CollideWithには衝突した場合の処理を書いていきます。

    ここで、foreachとはfor文の拡張で、

    foreach (var obj in objects)
    

    というのは、 「 objects の要素をとりだして、 obj と名前を付ける」 ことをobjects の全要素について行ってくれます。
    今回はforeach文を使ってobjectsから取り出したCollidableObjectであるobjのコライダobj.colliderと自身のコライダcolliderの間での当たり判定を取っています。
    また、varというキーワードがあります。これは型推論と呼ばれるもので、名前の通り変数の宣言の際に型を推論してくれるというものです。なので上のforeach文は

    foreach (CollidableObject obj in objects)
        if (collider.GetIsCollidedWith(obj.collider))
            CollideWith(obj);
    

    このようにしても大丈夫です。ただ、varを使ったほうが記述が短くて楽です。

    foreachとvarについての詳しい解説を以下に載せておきます。

    C# によるプログラミング入門 : foreach
    C# によるプログラミング入門 : 型推論

    CollideWith関数では衝突時の処理を書いていきます。

    // 衝突時に実行
    private void CollideWith(CollidableObject obj)
    {
        // nullだったら終了
        if (obj == null)
        {
            return;
        }
    
        // 衝突対象がSurveyを実行しないオブジェクトだった場合,相手のOnCollisionも実行
        if (!obj.doSurvey)
        {
            obj.OnCollision(this);
        }
    
        // 自身のOnCollisiionを実行
        OnCollision(obj);
    }
    

    ここで,Surveyを実行しないオブジェクト(=doSurveyがfalse)に対してOnCollisionを呼び出しています。 何故,全てのCollidableObjectに対してSurveyを実行させず,doSurveyのような面倒な処理を挟むのかというのが気になるかと思います。 後々説明しますが,doSurveyフラグは自機や敵ではtrue,自機弾や敵弾ではfalseにします。 もし仮に全てのCollidableObjectにてSurveyを走らせるとなると,衝突判定が計算される回数はobjectsに登録されているCollidableObjectの2乗に相当します。 弾というオブジェクトは,自機や敵の個数に比べて大量に画面上に出現する機会が多いです。その為,弾幕シューティングを作ったときなどは処理が重くなることがあります。 それを避けるためにdoSurveyというフラグを用いてSurveyを実行する回数を最小限に留める事が出来ます。 プログラミングを行う際はこのようにパフォーマンスを意識するという事も大事です(最初のうちは動くこと重視,慣れてきたら意識すると良いです)。

    オブジェクトにより当たった時の処理は異なるので継承先でOnCollision関数をオーバーライドさせて処理を継承先に委託するようにしています。
    OnCollision関数にあるキーワードvirtualは仮想メソッドと呼ばれるものでこれをつけることで継承先で関数のオーバーライドができます。

    C# によるプログラミング入門 : 多態性

    // 衝突時に実行される内容をオーバーライドして設定できる
    protected virtual void OnCollision(CollidableObject obj)
    {
    
    }
    

    当たり判定を持つクラスへの切り替え

    ざっとですが基本的なコライダの使い方を解説しました。
    次は継承先での処理を作っていきましょう。

    まずは各クラスをCollidableObjectから継承させるようにします。
    Player クラス

    -   public class Player : SpriteNode
    +   public class Player : CollidableObject
    

    Enemy クラス

    -    public class Enemy : SpriteNode
    +    public class Enemy : CollidableObject
    

    Bullet クラス

    -    class Bullet : SpriteNode
    +    class Bullet : CollidableObject
    

    さらにコンストラクタも書き換えていきます。 冒頭でも少し触れましたがEnemyクラスとBulletクラスのRemoveMyselfIfOutOfWindow関数は親クラスであるCollidableObjectに移したのでついでに削除しましょう。 また,CollidableObjectのOnUpdateに処理を書いたので,base.OnUpdateを呼び出しましょう。

    Player クラス

    using Altseed2;
    
    namespace Tutorial
    {
        // プレイヤーのクラス
        public class Player : CollidableObject
        {
            // コンストラクタ
    -       public Player(Vector2F position)
    +       public Player(MainNode mainNode, Vector2F position) : base(mainNode, position)
            {
    -           // 座標を設定
    -           Position = position;
    
    +           // 衝突判定を行うように設定
    +           doSurvey = true;
    
                // テクスチャを読み込む
                Texture = Texture2D.LoadStrict("Resources/Player.png");
    
                // 中心座標を設定
                CenterPosition = ContentSize / 2;
    
    +           // コライダの半径を設定
    +           collider.Radius = Texture.Size.Y / 2;
            }
    
            // フレーム毎に実行
            protected override void OnUpdate()
            {
                // 移動を実行
                Move();
    
                // ショットを実行
                Shot();
    
    +           // CollidableObjectのOnUpdate呼び出し
    +           base.OnUpdate();
            }
    
            ...略...
    
        }
    }
    

    Enemy クラス

    using Altseed2;
    
    namespace Tutorial
    {
        // 敵の基礎となるクラス
        public class Enemy : CollidableObject
        {
            // 倒された時に加算されるスコアの値
            protected int score;
            
            // プレイヤーへの参照
            protected Player player;
            
            // コンストラクタ
    -       public Enemy(Player player, Vector2F position)
    +       public Enemy(Player player, Vector2F position) : base(player.mainNode, position)
            {
    +           // 衝突判定を行うように設定
    +           doSurvey = true;
    
    -           // 座標を設定
    -           Position = position;
    
                // プレイヤーへの参照を設定
                this.player = player;
            }
    
            // フレーム毎に実行
            protected override void OnUpdate()
            {
                // CollidableObjectのOnUpdateを実行
                base.OnUpdate();
    
    +           // 画面外に出たら自身を削除
    +           RemoveMyselfIfOutOfWindow();
            }
    
    -       private 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);
    -           }
    -       }
        }
    }
    

    Bullet クラス

    using Altseed2;
    
    namespace Tutorial
    {
        // 弾のクラス
        public class Bullet : CollidableObject
        {
            // フレーム毎に進む距離
            private Vector2F velocity;
    
            // コンストラクタ
    -       public Bullet(Vector2F position, Vector2F velocity)
    +       public Bullet(MainNode mainNode, Vector2F position, Vector2F velocity) : base(mainNode, position)
            {
    +           // 衝突判定を行わないように設定
    +           doSurvey = false;
    
    -           // 座標を設定
    -           Position = position;
    
                // 弾速を設定
                this.velocity = velocity;
    
                // 表示位置をプレイヤーや敵より奥に設定
                ZOrder--;
            }
    
            // フレーム毎に実行
            protected override void OnUpdate()
            {
                // 座標を速度分進める
                Position += velocity;
    
    +           // CollidableObjectのOnUpdateを呼び出す
    +           base.OnUpdate();
    
                // 画面外に出たら自身を削除
                RemoveMyselfIfOutOfWindow();
            }
    
    -       private 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);
    -           }
    -       }
        }
    }
    

    ここでコンストラクタの後ろにbaseというキーワードが出てきました。これは親クラスのコンストラクタ呼び出しという意味です。今回だとColliderObjectのコンストラクタを呼び出します。ColliderObjectのコンストラクタではMainNodeとpositionが必要なためbaseの後の引数で受け渡します。baseについての詳しい解説はこちらを参照してください。

    C# によるプログラミング入門 : 継承

    また、ColliderObjectのコンストラクタで座標を設定する処理があるので子クラスでは座標を設定するコードは消しています。

    Bulletの修正に併せてEnemyBulletとPlayerBulletのコードも修正していきましょう。

    これらはコンストラクタの引数変更と半径を設定させるだけで大丈夫です

    EnemyBullet クラス

    +       public EnemyBullet(MainNode mainNode, Vector2F position, Vector2F velocity) : base(mainNode, position, velocity)
    -       public EnemyBullet(Vector2F position, Vector2F velocity) : base(position, velocity)
            {
                // テクスチャを読み込む
                Texture = Texture2D.LoadStrict("Resources/Bullet_Red.png");
    
                // 中心座標を設定
                CenterPosition = ContentSize / 2;
    
    +           // 半径を設定
    +           collider.Radius = Texture.Size.X / 2;
            }
    

    PlayerBullet クラス

    +       public PlayerBullet(MainNode mainNode, Vector2F position) : base(mainNode, position, new Vector2F(10f, 0.0f))
    -       public PlayerBullet(Vector2F position) : base(position, new Vector2F(10f, 0.0f))
            {
                // テクスチャを読み込む
                Texture = Texture2D.LoadStrict("Resources/Bullet_Blue.png");
    
                // 中心座標を設定
                CenterPosition = ContentSize / 2;
    
    +           // 半径を設定
    +           collider.Radius = Texture.Size.X / 2;
            }
    

    次にEnemyクラスの変更に併せてその派生クラスであるChaseEnemyクラスとRadialShotEnemyクラスとStraightShotEnemyクラスとMeteorクラスを書き換えていきます。先ほどEnemyBulletクラスの引数を変更したのでShot関数のEnemyBulletを生成するコードもついでに書き換えましょう。

    ChaseEnemy クラス

            public ChaseEnemy(Player player, Vector2F position, float speed) : base(player, position)
            {
                // テクスチャを読み込む
                Texture = Texture2D.LoadStrict("Resources/UFO.png");
    
                // 中心座標を設定
                CenterPosition = ConentSize / 2;
    
    +           // 半径を設定
    +           collider.Radius = Texture.Size.X / 2;
    
                // 移動速度を設定
                this.speed = speed;
    
                // 自身が倒された時に加算されるスコアを設定
                score = 10;
            }
    

    RadialShotEnemy クラス

            public RadialShotEnemy(Player player, Vector2F position, int shotAmount) : base(player, position)
            {
                // 撃ち出すショットの個数を設定
                this.shotAmount = shotAmount;
    
                // テクスチャを読み込む
                Texture = Texture2D.LoadStrict("Resources/UFO.png");
    
                // 中心座標を設定
                CenterPosition = ContentSize / 2;
    
    +           // 半径を設定
    +           collider.Radius = Texture.Size.X / 2;
    
                // スコアを設定
                score = 30;
            }
    
            // 弾を撃つ
            private void Shot(Vector2F velocity)
            {
                // 敵弾を画面に追加
    +            Parent.AddChildNode(new EnemyBullet(mainNode, Position, velocity));
    -            Parent.AddChildNode(new EnemyBullet(Position, velocity));
            }
    

    StraightShotEnemy クラス

            public StraightShotEnemy(Player player, Vector2F position) : base(player, position)
            {
                // テクスチャを読み込む
                Texture = Texture2D.LoadStrict("Resources/UFO.png");
    
                // 中心座標を設定
                CenterPosition = ContentSize / 2;
    
    +           // 半径を設定
    +           collider.Radius = Texture.Size.X / 2;
    
                // 倒された時に加算されるスコアを設定
                score = 20;
            }
    
                    // 弾を撃つ
            private void Shot(Vector2F velocity)
            {
                // 敵弾を画面に追加
    +            Parent.AddChildNode(new EnemyBullet(mainNode, Position, velocity));
    -            Parent.AddChildNode(new EnemyBullet(Position, velocity));
            }
    

    Meteor クラス

            public Meteor(Player player, Vector2F position, Vector2F velocity) : base(player, position)
            {
                // 速度の設定
                this.velocity = velocity;
    
                // テクスチャの設定
                Texture = Texture2D.LoadStrict("Resources/Meteor.png");
    
                // 中心座標の設定
                CenterPosition = ContentSize / 2;
    
    +           // 半径の設定
    +           collider.Radius = Texture.Size.X / 2;
    
                // スコアの設定
                score = 1;
            }
    

    さらにPlayerクラスでPlayerBulletを使用していたのでこちらも修正が必要になります。

    Player クラスの Shot 関数

            // ショット
            private void Shot()
            {
                // Zキーでショットを放つ
                if (Engine.Keyboard.GetKeyState(Key.Z) == ButtonState.Push)
                {
    +               Parent.AddChildNode(new PlayerBullet(mainNode, Position));
    -               Parent.AddChildNode(new PlayerBullet(Position));
                }
            }
    

    最後にMainNodeクラスを修正します。まず、Playerクラスの呼び出しの変更が必要です。さらにもう一つ変更が必要です。もしMainNodeが消去されてもコライダがコレクションに残っている場合、使われていないコライダがコレクションに保存され続けるとになります。この場合、ゲームのリトライなどを行うとリトライ前のコライダが残ってしまい、バグなどを引き起こす恐れがあるのでコライダの消去を行います。HashSetクラスの中身消去はClear関数でできます。

    MainNode クラス

            protected override void OnAdded()
            {
                // ================================================================
                // 省略
                // ================================================================    
                    
                // プレイヤーを設定
    +            player = new Player(this, new Vector2F(100, 360));
    -            player = new Player(new Vector2F(100, 360));
                    
                // ================================================================
                // 省略
                // ================================================================
            }
    
    +       // エンジンから削除されたときに実行
    +       protected override void OnRemoved()
    +       {
    +           // 衝突判定を全てリセット
    +           CollidableObject.objects.Clear();
    +       }
    

    これでひと段落と思いきや、衝突時の処理をまだ書いていないので衝突してもまだ何も起こりません。次にそれぞれのクラスで衝突した時の処理を書いていきたいところですが、先に衝突したときのエフェクトを作りましょう。

    Effectを作成するクラス

    衝突したときに出すエフェクトであるDeathEffectクラスを作ります。
    DeathEffectのコードは以下のようになります。

    using Altseed2;
    
    namespace Tutorial
    {
        // 死亡時エフェクト
        public class DeathEffect : SpriteNode
        {
            // カウンタ
            private int count = 0;
    
            // コンストラクタ
            public DeathEffect(Vector2F position)
            {
                // 座標を設定
                Position = position;
    
                // テクスチャを読み込む
                Texture = Texture2D.LoadStrict("Resources/Explosion.png");
    
                // 中心座標を設定
                CenterPosition = new Vector2F(32f, 32f);
    
                // 表示位置をプレイヤーや敵よりも手前に設定
                ZOrder++;
    
                // テクスチャの描画範囲を設定
                Src = new RectF(new Vector2F(), new Vector2F(Texture.Size.X / 9, Texture.Size.Y));
            }
    
            // フレーム毎に実行
            protected override void OnUpdate()
            {
                // 表示されるテクスチャのサイズを取得
                var size = new Vector2F(Texture.Size.X / 9, Texture.Size.Y);
    
                // 表示されるテクスチャの左上の座標を計算する
                var pos = new Vector2F(size.X * (count / 2 % 9), size.Y);
    
                // 描画範囲を設定
                Src = new RectF(pos, size);
    
                // カウントを進める
                count++;
    
                // カウントが18以上で自身を削除
                if (count >= 18)
                {
                    Parent.RemoveChildNode(this);
                }
            }
        }
    }
    

    どのように実装されているかというと以下のような画像の一部を表示して表示位置をずらしてあげることでアニメーションのような効果を出しています。

    DeathEffect

    コード中で使われているSrcについて説明します。今まではTextureに設定された画像全てを描画していましたが、今回は画像の一部だけを切り取って描画する必要があります。 そこで、Src を使用します。SrcはSpriteNodeクラスの持つフィールドで、Srcに値を設定すると画像の中で指定された範囲のみが描画されるようになります。Srcに値を設定する方法ですが、第一引数は表示したい範囲の左上の座標を、第二引数はその座標から表示する範囲を指定します

    // 描画範囲を設定
    Src = new RectF(pos, size);
    

    設定する変数sizeとposはcountという毎フレーム1ずつ増える整数の変数を作って計算します。今回は2フレームごとに画像をずらすような式にしてあります。上に示した画像は爆破の画像が横に9枚並んでいるもので、2フレーム×9=18なのでcountが18になった場合Parent.RemoveChildNode(this);により自身を削除してエフェクトの再生を終了します。

    // 表示されるテクスチャのサイズを取得
    var size = new Vector2F(Texture.Size.X / 9, Texture.Size.Y);
    
    // 表示されるテクスチャの左上の座標を計算する
    var pos = new Vector2F(size.X * (count / 2 % 9), size.Y);
    
    // 描画範囲を設定
    Src = new RectF(pos, size);
    
    // カウントを進める
    count++;
    
    // カウントが18以上で自身を削除
    if (count >= 18)
    {
        Parent.RemoveChildNode(this);
    }
    

    爆破のエフェクトが完成したので衝突時の処理を書いていきましょう。

    OnCollideの実装

    CollidableObjectでは衝突したときにOnCollide関数を呼び出すように実装しましたね。なので子クラスでOnCollide関数の中身を記述してあげればよいです。
    衝突時に処理するクラスはPlayerとEnemyとPlayerBulletとEnemyBulletの4クラスになります。
    これら4つのOnCollide関数を以下に載せます。

    Player クラス

    // 衝突時に実行
    protected override void OnCollision(CollidableObject obj)
    {
        // 衝突対象が敵か敵の弾だったら
        if (obj is Enemy || obj is EnemyBullet)
        {
            // 自身を親から削除
            Parent.RemoveChildNode(this);
        }
    }
    

    PlayerBullet クラス

    // 衝突時に実行
    protected override void OnCollision(CollidableObject obj)
    {
        // 衝突対象が敵だったら自身を削除
        if (obj is Enemy)
        {
            Parent?.RemoveChildNode(this);
        }
    }
    

    Enemy クラス

    // 衝突時に実行
    protected override void OnCollision(CollidableObject obj)
    {
        // 衝突対象が自機弾だったら
        if (obj is PlayerBullet)
        {
            // 死亡時エフェクトを再生
            Parent.AddChildNode(new DeathEffect(Position));
    
            // 自身を削除
            Parent.RemoveChildNode(this);
        }
    }
    

    EnemyBullet クラス

    // 衝突時に実行
    protected override void OnCollision(CollidableObject obj)
    {
        // 衝突対象がプレイヤーだったらBulletのOnCollisionを実行して削除
        if (obj is Player)
        {
            Parent?.RemoveChildNode(this);
        }
    }
    

    ここでisというキーワードがありますね。これはis演算子と呼ばれるものです。このisというのは変数objがどの型を継承しているのか判断するために使えます。一例ですが、if (obj is Enemy)と書けば、objがEnemyクラスか、その派生クラスの時に処理をすることができます。
    余談ですが似たような機能にas演算子というものがあります。こちらは戻り値がboolではなく型変換したものになります。 is演算子とas演算子について詳しく知りたい方は以下を参照してください。

    C# によるプログラミング入門 : 多態性

    Meteorクラス改変

    今、Meteorクラスの衝突処理はEnemyクラスのOnCollideが呼ばれるので、Playerの弾に当たると消滅します。これで完成してもよいのですが、Meteorというからには岩石で硬いはずなのでPlayerの弾ごとき3回くらいまでなら耐えると思います。 そのように改変しましょう。

    Meteor クラス

    using Altseed2;
    
    namespace Tutorial
    {
        // 隕石
        public class Meteor : Enemy
        {
            // フレーム毎の移動速度
            private Vector2F velocity;
    
    +       // HP
    +       private int HP = 3;
    
            // コンストラクタ、OnUpdate略
    
    +        protected override void OnCollision(CollidableObject obj)
    +        {
    +           // 衝突したのが自機弾だったら
    +           if (obj is PlayerBullet)
    +           {
    +               // HPを1減らす
    +               HP--;
    +               // HPが0になったらEnemyクラスのOnCollisionを呼び出して削除
    +               if (HP == 0)
    +               {
    +                  base.OnCollision(obj);
    +              }
    +           }
    +       }
    +   }
    }
    

    HPというフィールドを追加して、プレイヤーの弾に当たる度にHPを1減らしていき、HPが0になったら消滅するというシンプルな処理です。
    このようにEnemyクラスでオーバーライドしたOnCollisionをさらにオーバーライドすると親クラスのOnCollisionは呼ばれなくなり、子クラスの処理に切り替わります。

    長い工程を経て衝突判定と衝突時の処理が完成しました。実行してみると敵に衝突したり、自弾が敵に衝突したときにエフェクトが出て画面から消えることが確認できると思います。

    まとめ

    今回は衝突判定をつけてみました。ようやくゲームらしさが増してきましたね。
    Altseed2ではCircleCollider以外にも多角形のコライダであるPolygonCollierクラスや四角形のコライダであるRectangleColliderクラスがあります。もし、厳密さが必要な形のオブジェクトに衝突判定をつけたい場合はそちらを使ってみてください。

    次章では音を鳴らしてみます。

    Copyright © 2020 Altseed .