アマゾンバナーリンク

実践的に本格的なゲームを作るための技術を習得する

2019年12月16日

オブジェクト指向プログラミング

今まで計算機やもぐら叩きゲームなど小規模なプログラミングをしてきましたが、今回から本格的にDXライブラリを使う事になります。それにともなってゲームプログラミングに最適なオブジェクト指向のプログラミングを紹介します。
なにやら名前からして難しい雰囲気がありますが、慣れてしまえばそんなことはありません。
むしろ、すごく使いやすい機能なのでぜひ覚えましょう。

構造体

構造体とは変数と関数をまとめて1つにした設計図のようなものです。
定義するには以下の様に書きます。

[修飾子] struct 構造体名
{
  フィールド(メンバ変数);
 メソッド(メンバ関数);
}
// 例
public struct TagPoint
{
  public int X,Y;
}

クラス

[修飾子] class 構造体名
{
  フィールド(メンバ変数);
 メソッド(メンバ関数);
}

// 例
public class CPlayer
{
	public int X,Y;
}

構造体もクラスもデータや処理をまとめるという機能は一緒ですが決定的な違い構造体は値型、クラスは参照型ということです。この違いは非常に大事なので、今後に別の記事できちんと解説します。

定義した構造体とクラスの使い方

定義しただけでは何も起こらず。new演算子などで実体化すると使えるようになります。
この構造体やclassを実体化したものをインスタンスと呼びます。


CPlayer Player = new CPlayer(); // インスタンス

アクセス修飾子の働き

  • private その構造体やclassのみアクセスできる。
  • protected その構造体やclass以外にも継承した構造体やclassからもアクセスできる。
  • public その構造体やclassの外からアクセスできる。

コンストラクタ

インスタンスを生成時に呼び出されるもので、メンバ変数の初期化などに使います。

// クラスの定義
public class CPlayer
{
	public int X,Y;
	public CPlayer(int x, int y) // コンストラクタ
	{
		X = x; // CPlayerクラスのメンバXに引数xを代入
		Y = y; // CPlayerクラスのメンバYに引数yを代入
	}
}

// インスタンス生成時
CPlayer Player = new CPlayer(0, 0); // XとYを0で初期化してインスタンスを生成する

抑えておきたい重要な事

同じ構造体やclassのインスタンスでも生成したら、別々な物として扱われます。
例えばCEnemyクラスがあったとして

CEnemy Enemy1 = new CEnemy(); // CEnemyクラスからEnemy1を生成する
CEnemy Enemy2 = new CEnemy(); // CEnemyクラスからEnemy2を生成する

Enemy1とEnemy2はまったく別のインスタンスとして扱われます。
私が初心者の頃は、この理解が曖昧でなかなか苦労しました。
実例で例えると構造体とクラスはたこ焼の型で、インスタンスはたこ焼きといったとこでしょう。
しかし、この構造を理解すると、似たようなキャラがたくさん出てきて動くようなプログラムが、作りやすくなります。

前回の続きからプログラミングする

上の記事の続きで、全体を管理するCMainGameクラスを作ります。

  1. 全体を管理するCMainGameクラスを作ります。
  2. ソリューションエクスプローラーからプロジェクト名を右クリックして、CMainGame.csを追加
  3. 以下のコードを追加する
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace DxLibGame
{
    public static class CMainGame
    {
		// 初期化(一度だけ行う処理)
        public static void Initialize()
        {

        }
		// 毎ループ行う計算処理
        public static void Move()
        {

		}
		// 描画
       	public static void Draw()
        {

		}
	}
}

staticについて

メンバ変数やメンバ関数にstaticを付けると、インスタンス化できなくなるかわりに「クラス名.関数名」や「クラス名.変数名」でアクセスできるクラスになります。
classにstaticをつけると、そのクラス内はすべてstaticなメンバ変数とメンバ関数しか定義できなくなります。 staticを使うべき所はそのクラスのデータを複数使わない場合です。
キャラやエフェクトなどは、インスタンス化するので、データを複数使いますが、数学の計算やゲームを管理する機能などは、1つだけで十分なのでstaticを使います。

ゲームの構造について

ゲームを作る時は必ず初期化(一度だけ行う処理)、毎ループ行う計算処理、描画処理の3つにわかれます。
計算処理と描画処理をわける理由は、2Dの場合は、先に描画したオブジェクトが下になるので、計算と描画は必ずわけた方が都合がよいです。

継承

構造体やclassには、そのメンバ変数やメンバ関数を引き継ぐ継承という機能があります。
例えば、

public class CPlayer
{
	int X Y;
}

public class CEnemy
{
	int X,Y;
}
//というclassがあったら、XとYは同じ変数を定義しているので、1つにまとめる事ができます。
public class CMover
{
	int X,Y;
}

public class CPlayer : CMover
{

}

public class CEnemy : CMover
{

}

というように同じ定義をしなくてよくなります。
CMoverを基底クラスといい、CPlayerとCEnemyを派生クラスと呼びます。
この機能をうまく使うと重複を避ける事ができて、メンテナンスしやすいコードを書きやすくなります

基底クラスを定義する

プロジェクト名を右クリックして、CMover.csを追加して以下のコードを書きましょう。

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

namespace DxLibGame
{
    public class CMover
    {
        public int X { get; set; }
        public int Y { get; set; }
        public int HP { get; set; }
        public CMover(int x, int y, int hp)
        {
            X = x;
            Y = y;
            HP = hp;
        }
        public bool Hit(CMover mover)
        {
            if (X == mover.X && Y == mover.Y)
            {
                return true;
            }
            return false;
        }
    }
}

そして、同じようにCPlayer.csを追加して以下のコードを書きましょう。

CheckHitKeyでキーの入力があったかどうかチェックしています
ただし、この関数は押したかどうか判定するだけで、押した瞬間はわかりません。
なので、InputCnt 配列を使って押されたらカウントして、話したらゼロにします。

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

namespace DxLibGame
{
    public class CPlayer : CMover
    {
        int[] InputCnt = new int[] { 0, 0, 0, 0 };
        public CPlayer(int x, int y, int hp) : base(x, y, hp)
        {
        
        }
        enum EInputState
        {
            KEY_INPUT_UP,
            KEY_INPUT_DOWN,
            KEY_INPUT_LEFT,
            KEY_INPUT_RIGHT
        }
;
        public void Move()
        {
       // 上キーを押したら
            if(DX.CheckHitKey(DX.KEY_INPUT_UP) == 1)
            {
                InputCnt[(int)EInputState.KEY_INPUT_UP]++;
            }
            else // 上キーを離したら
            {
                InputCnt[(int)EInputState.KEY_INPUT_UP] = 0;
            }
       // 下キーを押したら
            if (DX.CheckHitKey(DX.KEY_INPUT_DOWN) == 1)
            {
                InputCnt[(int)EInputState.KEY_INPUT_DOWN]++;
            }
            else // 下キーを離したら
            {
                InputCnt[(int)EInputState.KEY_INPUT_DOWN] = 0;
            }
       // 左キーを押したら
            if (DX.CheckHitKey(DX.KEY_INPUT_LEFT) == 1)
            {
                InputCnt[(int)EInputState.KEY_INPUT_LEFT]++;
            }
            else // 左キーを離したら
            {
                InputCnt[(int)EInputState.KEY_INPUT_LEFT] = 0;
            }
            // 右キーを押したら
            if (DX.CheckHitKey(DX.KEY_INPUT_RIGHT) == 1)
            {
                InputCnt[(int)EInputState.KEY_INPUT_RIGHT]++;
            }
            else // 右キーを離したら
            {
                InputCnt[(int)EInputState.KEY_INPUT_RIGHT] = 0;
            }
            // 上が押された瞬間なら上へ移動
            if(InputCnt[(int)EInputState.KEY_INPUT_UP] == 1)
            {
                --Y;
            }
       // 下が押された瞬間なら上へ移動
            if (InputCnt[(int)EInputState.KEY_INPUT_DOWN] == 1)
            {
                ++Y;
            }
       // 左が押された瞬間なら上へ移動
            if (InputCnt[(int)EInputState.KEY_INPUT_LEFT] == 1)
            {
                --X;
            }
       // 右が押された瞬間なら上へ移動
            if (InputCnt[(int)EInputState.KEY_INPUT_RIGHT] == 1)
            {
                ++X;
            }
            // 左にはみ出ないように調整
            if(X < 0)
            {
                X = 0;
            }
       // 右にはみ出ないように調整
            if(9 < X)
            {
                X = 9;
            }
       // 上にはみ出ないように調整
            if (Y < 0)
            {
                Y = 0;
            }
       // 下にはみ出ないように調整
            if (7 < Y)
            {
                Y = 7;
            }
        }

        public void Draw()
        {
            DX.DrawBox(X*32, Y*32 ,
                X*32 + 32, Y*32 + 32, DX.GetColor(255, 255, 255), 1);
        }
    }
}

更に同じようにCEnemy.csを追加して以下のコードを書きましょう。

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

namespace DxLibGame
{
    public class CEnemy : CMover
    {
        public bool Show { get; set; }
        public CEnemy(int x, int y, int hp) : base(x, y, hp)
        {
            X = x;
            Y = y;
            HP = hp;
            Show = true;
            _Stopwatch.Start(); // ストップウォッチをスタート
        }
        Stopwatch _Stopwatch = new Stopwatch();
        public bool Move()
        {
            if (!Show) return false;
            // 500ミリ秒以上経過したら移動する
            if (500 < _Stopwatch.ElapsedMilliseconds)
            {
                X += DX.GetRand(2)-1;
                Y += DX.GetRand(2)-1;
         // 左にはみ出ないように調整
                if (X < 0)
                {
                    X = 0;
                }
         // 右にはみ出ないように調整
                if (9 < X)
                {
                    X = 9;
                }
         // 上にはみ出ないように調整
                if (Y < 0)
                {
                    Y = 0;
                }
         // 下にはみ出ないように調整
                if (7 < Y)
                {
                    Y = 7;
                }
                _Stopwatch.Restart();
            }
            return true;
        }
        public void Draw()
        {
            if (!Show) return;
            DX.DrawBox(X * 32, Y * 32,
                X * 32 + 32, Y * 32 + 32, DX.GetColor(255, 0, 0), 1);
        }
    }
}

ここまで準備ができたら、一番最初に作ったCMainGame.csの関数の中身を書いていきます。

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

namespace DxLibGame
{
    public static class CMainGame
    {
        public static CPlayer Player = new CPlayer(x: 0, y: 0, hp: 10);
        public static CEnemy[] Enemy = new CEnemy[] { new CEnemy(x: 5, y: 5, hp: 1), new CEnemy(x: 8, y: 8, hp: 1) };
        public static void Initialize()
        {

        }
        public static void Move()
        {
            Player.Move();          
            for (int i = 0; i < Enemy.Length; ++i)
            {
                if(!Enemy[i].Move())
                {
                    Enemy[i].Show = false;
                }
            }
        }
        public static void Draw()
        {
            Player.Draw();
            for (int i = 0; i < Enemy.Length; ++i)
            {
                Enemy[i].Draw();
            }
        }
    }
}

ここまでできたらプレイヤーと敵キャラが動くようになります。
実行結果

https://gyazo.com/189ce6e683e63ea4c67ae4ff9d60e997

次回以降当たり判定を付けます。