アマゾンバナーリンク

弾幕シューティングゲーム制作その2 プレイヤーを動かす

2020年1月15日

前回までで、プログラミングを始める骨格はできたので今回は自キャラであるプレイヤーを動かす処理を追加していきます。今回から画像を使うのでこちらからダウンロードしてください。画像を置く階層はデバッグモードで実行するなら、bin/Debugでリリースモードで実行するならbin/Releaseです。

CMoverクラス

まずは自キャラの基底クラスのCMoverクラスを定義します。
Xは横軸の座標、Yは縦軸の座標で座標の0,0は左上です。
Form1.csでウィンドウのサイズを640,480に定義しているのでその値が画面内の最大値です。
Lifeはダメージを受けるごとに減っていき、0になったキャラは削除します。
Cntはカウンタで画僧のアニメーションを行う時に使います。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ShootingGame
{
    public class CMover
    {
        protected double X { set; get; }
        protected double Y { set; get; }
        public int Life { set; get; }
        protected int Cnt { set; get; }
        public CMover(double x, double y, int life)
        {
            X = x;
            Y = y;
            Life = life;
            Cnt = 0;
        }
    }
}

CPlayerクラス

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

namespace ShootingGame
{
    public class CPlayer : CMover
    {
        double VX { set; get; }
        double VY { set; get; }
        double MaxX, MaxY; // X,Y座標の最大値
        double Sqrt2;
        int[] Image = new int[12]; // 画像のイメージハンドルが入っている
        public CPlayer(double x, double y, int life,int image) : base(x, y, life)
        {
            Sqrt2 = 1 / Math.Sqrt(2.0);
            MaxX = 608;
            MaxY = 448;
            string file_name = Directory.GetCurrentDirectory()+ "/dat/img/char/0.png";
            if(DX.LoadDivGraph(file_name, 12, 4, 3, 73, 73, Image) == -1)
            {
                MessageBox.Show("プレイヤーの画像の読み込みに失敗しました");
            }
        }
        public void Move()
        {
            // 速度
            VX = 4.0;
            VY = 4.0;
            // 低速キーを押してたら低速に(初期はVキー)
            if (0 < GetPadKey(EINPUT_TYPE.Slow))
            {
                VX = 1.8;
                VY = 1.8;
            }

            Cnt = ((Cnt+1) % 24) / 6;//現在の画像決定
            // 左右移動(初期は十字キー左右)
            if (0 < GetPadKey(EINPUT_TYPE.Left))
            {
                VX = -VX;
                Cnt += 4 * 2;//画像を左向きに
            }
            else if (0 < GetPadKey(EINPUT_TYPE.Right))
            {
                Cnt += 4 * 1;//画像を右向きに
            }
            else
            {
                VX = 0;
            }
            
            // Zキーを押すと自キャラが消える
            if (0 < GetPadKey(EINPUT_TYPE.Shot))
            {
                Life = 0;
            }

            // 上下移動(初期は十字キー上下)
            if (GetPadKey(EINPUT_TYPE.Up) > 0) VY = -VY; else if (GetPadKey(EINPUT_TYPE.Down) == 0) VY = 0;
            
            // 斜め方向の速度調節
            if (VX != 0 && VY != 0)
            {
                VX = Math.Floor(VX * Sqrt2 + 0.5);
                VY = Math.Floor(VY * Sqrt2 + 0.5);
            }

            // 移動
            X += VX;
            Y += VY;

            // 移動範囲の制限
            if (X < 32) X = 32; else if (MaxX < X) X = MaxX;
            if (Y < 32) Y = 32; else if (MaxY < Y) Y = MaxY;
        }
        public void Draw()
        {   
            // 自キャラの描画
            DX.DrawRotaGraphF((float)X, (float)Y, 1.0f, 0.0f, Image[Cnt], 1);
            // X,Y座標の描画
            DX.DrawString(10, 10, $"X{X},Y{Y},Cnt{Cnt}",DX.GetColor(255,255,255));
        }
    }
}
  • VX,VYは座標を加算する値
  • MaxX, MaxYは自キャラの座標が画面外にはみ出ない様にする値
  • 1/Sqrt2は斜め移動の時にかけると移動量が大きくなりすぎるのを調整する値
  • Image配列は画像を分割して代入する配列

初期化時に一度だけ行う処理

  • System.IOのDirectory.GetCurrentDirectory()でカレントディレクトリを取得
  • 取得したパスから画僧ファイルをImage配列に読み込む

画像を分割して読み込む時にはLoadDivGraphを使います。
-1が返ってきたら失敗なのでその時はメッセージボックスで警告します。

毎ループ行う計算処理

キー入力の取得方法

GetPadKey(EINPUT_TYPE.Left)とすると左キーもしくはパッドの左ボタンを押した時間が返ってきます。 なので、if (0 < GetPadKey(EINPUT_TYPE.Left))の様に書けば、左キーもしくは左ボタンを押してる状態であればという条件が書けます。
正確にはCInput.GetPadKey(CInput.EINPUT_TYPE.Left)と書かなければ、いけないのですが、using static ShootingGame.CInput;と一番上に宣言しているので、省略することができます。
GetPadKeyに渡せる引数の種類を知りたい場合は、Program.csのCInputのEINPUT_TYPEに定義されてい中身を確認しましょう!

現在の画像決定

Cnt = ((Cnt+1) % 24) / 6;
と書かれてますが、ゲームプログラミングでは、余剰演算子がよく使われます。
余剰演算子とは、ある数を割った余りを求める演算子のことです。
例えば、(Cnt+1)%3でCntを足していくと0→1→2→0→1→2と同じ範囲の数を順番に列挙できます。
更に割り算を使えば以下の様な数を作ることができます。
割と重要なテクニックなので、覚えておいておくと特です。

using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Test
{
    class Program
    {
        static void Main(string[] args)
        {
            for (int i = 0; i < 18; ++i)
            {
                Console.Write(((i % 9) / 3).ToString());
            }
            Console.WriteLine("");
        }
    }
}

実行結果

斜めの移動範囲の制限

XとYをVXとVYを、それぞれ足して、移動後の座標を求めますが、斜めに同時押しした場合は、移動量が多くなりすぎてしまうので、平方根の逆数をかけて調整します。

プレイヤーを画面外に出さないようにする

最後に画面の外に出ないように、外に出た場合は、制限値をXとYそれぞれ代入します。
// 移動範囲の制限
if (X < 32) X = 32; else if (MaxX < X) X = MaxX;
if (Y < 32) Y = 32; else if (MaxY < Y) Y = MaxY;

自キャラの描画処理

public void Draw()
{   
    // 自キャラの描画
    DX.DrawRotaGraphF((float)X, (float)Y, 1.0f, 0.0f, Image[Cnt], 1);
    // X,Y座標の描画
    DX.DrawString(10, 10, $"X{X},Y{Y},Cnt{Cnt}",DX.GetColor(255,255,255));
}

こちらでDrawRotaGraphDrawStringの説明があります。
DrawRotaGraphとDrawRotaGraphFとの違いは引数がintかfloatかの違いだけです。

文字列補間

なお、DrawStringの第三引数で$"X{X},Y{Y},Cnt{Cnt}"の様に書いてますが、これは文字列補間といいC#6.0から追加された機能で、文字列の中に{変数名}として定義することでその変数の中身を表示することができます。非常に便利な機能なので、積極的に使いましょう。

ゲームを制御するメインクラスの処理

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using DxLibDLL;

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; }
        }
        List<CPlayer> Player = new List<CPlayer>();
        // 初期化関数
        public void Initalize() 
        {
            // ここに初期化処理を追加する
            Player.Add(new CPlayer(320, 240, 10, 0));
        }
        // 毎ループ処理する関数
        public void MainLoop() 
        {
            // ここに処理を追加する
            // Lifeが0より多いなら処理する
            foreach (var player in Player.Where(x => 0 < x.Life)) player.Move();
            foreach (var player in Player.Where(x => 0 < x.Life)) player.Draw();

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

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

            DX.DxLib_End();
        }
    }
}

続いて、今まで定義したプレイヤークラスの機能を実行できるようCShootingGameクラスに追加します。
プレイヤーは1人なのでList型を使う必要はないですが、後から派生クラスを作ってオプションや分身などを作ったりもできるので、List型を使ってます。

初期化処理

まずは一度だけ行われるInitalize関数でプレイヤーを生成
Player.Add(new CPlayer(320, 240, 10, 0));

毎ループ実行する処理

MainLoop関数でWhere文を使ってライフが0より多い時はplayerのMove関数とDraw関数を実行しています。 別々にしてる理由は、これから敵キャラなどの処理が入って、計算部と描画部を完全に切り離すためです。

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

そして以下の部分でプレイヤーのLifeが0以下になったらプレイヤーを消すという処理をしてます。
// Lifeが0以下の要素を削除する
Player.RemoveAll(s => s.Life <= 0);

実行結果

今回のプロジェクトをダウンロードする。 プロジェクトに画像は同梱してませんのでこちらからダウンロードしてください。画像を置く階層はデバッグモードで実行するなら、bin/Debugでリリースモードで実行するならbin/Releaseです。
次回はスコアボードを表示する予定です。