アマゾンバナーリンク

弾幕シューティングゲーム制作その3 スコアボードと敵キャラを動かす

2020年1月15日

前回は自キャラを動かすとこまでやりました。今回はスコアボードを表示、敵キャラを動かすところまでやりましょう。

スコアボードを制御するクラスを作る

背景画像の読み込み

最初に初期化処理で画像を読み込みます。
CShootingGame.csのInitalize関数に以下のコードを追加。

public int[] ImageEnemy = new int[9]; // 敵画像のイメージハンドルが入っている
public void Initalize() 
{
	// スコアボード読み込み
	int[] index = new int[] { 10, 11, 12, 20 };
	for (int i = 0; i < ImageBack.Length; ++i)
	{
	    ImageBack[i] = DX.LoadGraph($"dat/img/board/{index[i]}.png");
	    if (ImageBack[i] == -1)
	    {
	        MessageBox.Show($"BackGroundの{i}番目のImageが読めこめません");
	    }
	}
}

背景画像の描画

ファイルの新規追加で、CBackGround.csを追加。
DrawGraphFで以下の様に描画します。普通のDrawGraphとの違いは、Fが付いてる方はfloat型をついてない方は、int型を引数に受け取ります。画像のイメージハンドルはCShootingGame.ShootingGamePro.変数名でどこのファイルからでもアクセスできます。前回説明しましたが、using static宣言をしてるので、 CShootingGameは省略できます。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using DxLibDLL;
using System.Windows.Forms;
using static ShootingGame.CShootingGame;

namespace ShootingGame
{
    public class CBackGround : CMover
    {
        public CBackGround() : base(0,0,1)
        {

        }
        public void Move()
        {

        }
        public void Draw()
        {
            int[] x = new int[] { 0, 0,  0, 416 };
            int[] y = new int[] { 0, 16, 464, 0 };
            for(int i = 0; i < ShootingGamePro.ImageBack.Length; ++i)
            {
                DX.DrawGraphF(x[i], y[i], ShootingGamePro.ImageBack[i], 0);
            }
        }
    }
}

背景の描画と追加

CShootingGame.csにInitalize関数に以下の一行を追加。先ほど背景画像を読み込んだ処理の下が良いでしょう。

  • BackGround.Add(new CBackGround());
public int[] ImageEnemy = new int[9]; // 敵画像のイメージハンドルが入っている
public void Initalize() 
{
	// スコアボード読み込み
	int[] index = new int[] { 10, 11, 12, 20 };
	for (int i = 0; i < ImageBack.Length; ++i)
	{
	    ImageBack[i] = DX.LoadGraph($"dat/img/board/{index[i]}.png");
	    if (ImageBack[i] == -1)
	    {
	        MessageBox.Show($"BackGroundの{i}番目のImageが読めこめません");
	    }
	}
   // ここに追加
   BackGround.Add(new CBackGround()); 
}

次にMainLoop関数に以下の様に追加しましょう。

public void MainLoop() 
{
	// Lifeが0より多いなら処理する
	foreach (var player in Player.Where(x => 0 < x.Life)) player.Move();
	foreach (var back in BackGround.Where(x => 0 < x.Life)) back.Move();

	foreach (var player in Player.Where(x => 0 < x.Life)) player.Draw();
	foreach (var back in BackGround.Where(x => 0 < x.Life)) back.Draw();

	// Lifeが0以下の要素を削除する
	Player.RemoveAll(s => s.Life <= 0);
	BackGround.RemoveAll(s => s.Life <= 0);
}

ここまでの処理で背景が描画されます。次に敵キャラを描画して動かす準備をします。

敵の描画

敵の描画も背景の追加とほとんど同じです。
しかし、それだけではつまらないので、少し動かしてみます。
まずは先ほどと画像の読み込み処理です。
CShootingGame.csのInitalize関数に以下のコードを追加。

public int[] ImageBack = new int[4]; // 敵画像のイメージハンドル      
public void Initalize() 
{
	// 敵画像読み込み
	if (DX.LoadDivGraph("dat/img/enemy/0.png", 9, 3, 3, 32, 32, ImageEnemy) == -1)
	{
	    MessageBox.Show("プレイヤーの画像の読み込みに失敗しました");
	}
}

CEnemy.csファイルを新規作成して以下のコードを追加

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using DxLibDLL;
using System.Windows.Forms;
using static ShootingGame.CShootingGame;

namespace ShootingGame
{
    public class CEnemy : CMover
    {
        int Muki;
        int ImageCnt = 0;
        
        public CEnemy(double x , double y , int life) : base(x, y, life)
        {
            Muki = 1;               
        }
        public void Move()
        {
            if (Cnt < 60)
            {
                Y += 2.0;
            }
            if (Cnt > 60 + 240)
            {
                Y -= 2.0;
            }
            Cnt++;
            ImageCnt = Muki * 3 + (Cnt % 18) / 6;
            if (X < 10 + 20 || // 左座標の上限
                X > FIELD_MAX_X - 10 + 42 || // 右座標の上限
                Y < 0 || // 上座標の上限
                Y > FIELD_MAX_Y - 5 + 20) // 下座標の上限
            {
                Life = 0;
            }
        }
        public void Draw()
        {
            DX.DrawRotaGraphF((float)X, (float)Y, 1.0f, 0.0f, ShootingGamePro.ImageEnemy[ImageCnt], 1);
        }
    }
}

簡単に説明しておくとMove関数で、1フレームに1ずつ加算されるカウンターが、60になるになるまで、下に移動して、次にカウンターが300になるまで、上に移動します。そして、外にでたらLifeを0にして削除してます。

描画の際の注意点

DrawRotaGraphFを使って描画してます。 DrawGraphは座標の中心が左上になりますが、 DrawRotaGraphは座標の中心が真ん中なのに注意しましょう!

敵の描画と追加

これも背景と同じように追加します。
CShootingGame.csにInitalize関数に以下の一行を追加

  • Enemy.Add(new CEnemy(FIELD_MAX_X / 2, 0, 1000));

最終的にCShootingGame.csのInitialize関数は以下のようになります。

public List<CPlayer> Player = new List<CPlayer>();
public List<CEnemy> Enemy = new List<CEnemy>();
public List<CBackGround> BackGround = new List<CBackGround>();
public int[] ImagePlayer = new int[12]; // プレイヤー画像のイメージハンドル
public int[] ImageBack = new int[4]; // 敵画像のイメージハンドル
public int[] ImageEnemy = new int[9]; // 敵画像のイメージハンドル
// 初期化関数
public void Initalize() 
{
    // ここに初期化処理を追加する

    // 自キャラ画像の読み込み
    if (DX.LoadDivGraph("dat/img/char/0.png", 12, 4, 3, 73, 73, ImagePlayer) == -1)
    {
        MessageBox.Show("プレイヤーの画像の読み込みに失敗しました");
    }

    // 敵画像読み込み
    if (DX.LoadDivGraph("dat/img/enemy/0.png", 9, 3, 3, 32, 32, ImageEnemy) == -1)
    {
        MessageBox.Show("プレイヤーの画像の読み込みに失敗しました");
    }

    // スコアボード読み込み
    int[] index = new int[] { 10, 11, 12, 20 };
    for (int i = 0; i < ImageBack.Length; ++i)
    {
        ImageBack[i] = DX.LoadGraph($"dat/img/board/{index[i]}.png");
        if (ImageBack[i] == -1)
        {
            MessageBox.Show($"BackGroundの{i}番目のImageが読めこめません");
        }
    }

    Player.Add(new CPlayer(320, 240, 10));
    Enemy.Add(new CEnemy(FIELD_MAX_X / 2, 0, 1000));
    BackGround.Add(new CBackGround());
}

MainLoop関数も最終的には以下の様になります。

// 毎ループ処理する関数
public void MainLoop() 
{
    // ここに処理を追加する
    // Lifeが0より多いなら処理する
    foreach (var player in Player.Where(x => 0 < x.Life)) player.Move();
    foreach (var enemy in Enemy.Where(x => 0 < x.Life)) enemy.Move();
    foreach (var back in BackGround.Where(x => 0 < x.Life)) back.Move();

    foreach (var player in Player.Where(x => 0 < x.Life)) player.Draw();
    foreach (var enemy in Enemy.Where(x => 0 < x.Life)) enemy.Draw();
    foreach (var back in BackGround.Where(x => 0 < x.Life)) back.Draw();

    // Lifeが0以下の要素を削除する
    Player.RemoveAll(s => s.Life <= 0);
    Enemy.RemoveAll(s => s.Life <= 0);
    BackGround.RemoveAll(s => s.Life <= 0);

    CFpsControl.DrawFps(0, 465);
    CFpsControl.FpsWait();
    // 裏画面の内容を表画面に反映する
    DX.ScreenFlip();
}

実行結果

念のためCShooting.csの中身もすべて載せておきます。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.IO;
using DxLibDLL;
using System.Windows.Forms;

namespace ShootingGame
{
    public class CShootingGame
    {
        public const int FIELD_X = 32;
        public const int FIELD_Y = 16;
        public const int FIELD_MAX_X = 384;
        public const int FIELD_MAX_Y = 448;
        // 実装は外部から隠蔽(privateにしておく)
        private static CShootingGame ShootingGame; // staticで変数を宣言
        // 変数の取得・変更用のプロパティ
        public static CShootingGame ShootingGamePro
        {
            set { ShootingGame = value; }
            get { return ShootingGame; }
        }
        public List<CPlayer> Player = new List<CPlayer>();
        public List<CEnemy> Enemy = new List<CEnemy>();
        public List<CBackGround> BackGround = new List<CBackGround>();
        public int[] ImagePlayer = new int[12]; // プレイヤー画像のイメージハンドル
        public int[] ImageBack = new int[4]; // 敵画像のイメージハンドル
        public int[] ImageEnemy = new int[9]; // 敵画像のイメージハンドル
        // 初期化関数
        public void Initalize() 
        {
            // ここに初期化処理を追加する

            // 自キャラ画像の読み込み
            if (DX.LoadDivGraph("dat/img/char/0.png", 12, 4, 3, 73, 73, ImagePlayer) == -1)
            {
                MessageBox.Show("プレイヤーの画像の読み込みに失敗しました");
            }

            // 敵画像読み込み
            if (DX.LoadDivGraph("dat/img/enemy/0.png", 9, 3, 3, 32, 32, ImageEnemy) == -1)
            {
                MessageBox.Show("プレイヤーの画像の読み込みに失敗しました");
            }

            // スコアボード読み込み
            int[] index = new int[] { 10, 11, 12, 20 };
            for (int i = 0; i < ImageBack.Length; ++i)
            {
                ImageBack[i] = DX.LoadGraph($"dat/img/board/{index[i]}.png");
                if (ImageBack[i] == -1)
                {
                    MessageBox.Show($"BackGroundの{i}番目のImageが読めこめません");
                }
            }

            Player.Add(new CPlayer(320, 240, 10));
            //Enemy.Add(new CEnemy(FIELD_MAX_X / 2, 0, 1000));
            BackGround.Add(new CBackGround());
        }
        // 毎ループ処理する関数
        public void MainLoop() 
        {
            // ここに処理を追加する
            // Lifeが0より多いなら処理する
            foreach (var player in Player.Where(x => 0 < x.Life)) player.Move();
            foreach (var enemy in Enemy.Where(x => 0 < x.Life)) enemy.Move();
            foreach (var back in BackGround.Where(x => 0 < x.Life)) back.Move();

            foreach (var player in Player.Where(x => 0 < x.Life)) player.Draw();
            foreach (var enemy in Enemy.Where(x => 0 < x.Life)) enemy.Draw();
            foreach (var back in BackGround.Where(x => 0 < x.Life)) back.Draw();

            // Lifeが0以下の要素を削除する
            Player.RemoveAll(s => s.Life <= 0);
            Enemy.RemoveAll(s => s.Life <= 0);
            BackGround.RemoveAll(s => s.Life <= 0);

            CFpsControl.DrawFps(0, 465);
            CFpsControl.FpsWait();
            // 裏画面の内容を表画面に反映する
            DX.ScreenFlip();
        }
        // 終了処理関数
        public void Destory()
        {
            // ここに終了処理を追加する

            DX.DxLib_End();
        }
    }
}

今回のプロジェクトをダウンロードする。プロジェクトに画像は同梱してませんのでこちらからダウンロードしてください。画像を置く階層はデバッグモードで実行するなら、bin/Debugでリリースモードで実行するならbin/Releaseです。 それとダウンロードできるプロジェクトは少しいじっていて、Zキーを押すと敵が出現します。

今回は背景の描画と敵の移動と描画をしました。次回以降は弾を打たせて当たり判定を付けます。