C++はOOP(英語でOop!は驚きや狼狽えた時に発する間投詞ですが、プログラミングではObject Oriented Programingの意味ですね)一つであり、クラスは属性のある主体をデータで定め、その動作もまとめて定義します。定義(クラス)に、現実に実体(インスタンス)を持たせるとオブジェクトになる、ということです。(例:(抽象)人間→男性→若い男性(具体)+(抽象)飲食、就寝、移動→男子トイレに行く→おしゃれする(具体) 👈 話は外れますが、私の子供時代にあったような社会的性差が無い今は「男性だけがする行動」って非常に少なくなってきていますね。カミさんにも訊いたのですが、精々「立ちシ〇〇」くらいじゃないかと。やー、時代は変わりました。)

 

これをLightCycleに当てはめると、まずはオブジェクトのイメージ(「画面の中を軌跡を表示しながら走ってゆく未来的な二輪車で、障害(軌跡)があると方向転換し、それができなくなると死ぬ。」)を「仕様」として具体化します。以下は初期のCLIGHTCYCLE.hファイルの定義部分です。

///////////////////////////////
//CLIGHTCYCLEクラス定義ファイル
///////////////////////////////
///////////////////////////////【仕様】///////////////////////////////
//【識別色】(m_Color)
//LightCycle車両は識別色を持つ。識別色はユーザーが決定する。
//【方向】(m_Dir)
//LightCycleでは方向を0-7で表す。
//    0                //y--
//  7 ↑ 1            //y--, x++(x--)
//6←    →2        //x++(x--)
// 5 ↓ 3            //y++, x++(x--)
//    4                //y++
//【位置関係】
//LightCycleの現在位置は2次元(平面)上のx(m_x)y(m_y)座標で特定する。
//走行範囲は競技場限界(m_MinX/-x座標最小値、m_MinY-y座標最小値、
//m_MaxX-x座標最大値、m_MaxY-y座標最大値)内とする。
//また、現在の方向と位置に基づく移動位置をm_nx、m_nyとする。
//競技の勝敗決定因である移動時間(プログラム的にはコールされた回数)
//はm_Timesとする。(要すれば一番長く生き残った者が勝つ。)
//【方向転換】
//方向転換のルールとして、進行方向の左右二つずつまでを許容
//    0                //m_Dir == 0であれば
//  7 ↑ 1            //左右斜め上と
//6←    →2        //左右の転回を許す。
//また、競技車に一定間隔で方向を自ら変えることを許す。プ
//ログラム上は一定の動作回数で方向を変換させる。(m_Caprice)
//if(!m_Caprice || (m_Times % m_Caprice)) return FALSE;
//【速度】(m_Delay)
//競技車の速度は可変とする。プログラム上はコールされる動作
//回数をm_Delayで除した余りが0の場合実行させる 。
//if(m_Delay && (m_Times % m_Delay)) return FALSE;
//////////////////////////////////////////////////////////////////////

この仕様定義は前にも書きましたが、実際のプログラミングをする前に、このようなコメントを記録しておき、その後の仕様変更があった際に「これどうしてあるんだっけ?」「何故こうしたんだっけ?」が分かるようにしておくとよいですね。(因みに速度調整と一定頻度での方向転換のコードは寝ていて思い着いたので記したものです。)

 

実際にコーディングをしてゆきます。

class CLIGHTCYCLE
{
public:        //メンバー変数
    //クラス共通静的変数(クラス外宣言が必要)
    static HDC m_hDC;        //表示デバイスのハンドル
    static int m_MinX;        //x座標最小値
    static int m_MinY;        //y座標最小値
    static int m_MaxX;        //x座標最大値
    static int m_MaxY;        //y座標最大値
    //競技車の属性変数
    int m_Color;            //競走車の色(CANVASクラスのカラー番号)
    int m_Dir;                //競走車の進行方向(0-7)
    int m_x;                //競走車の現在のx座標
    int m_y;                //競走車の現在のy座標
    int m_Delay;            //競走車のスピード(0-3-0はDelay無し)
    int m_Caprice;            //何遍に1回方向転換を行うか(0は方向転換しない)
    //内部管理変数
    UINT m_Times;            //競走車の移動回数
    bool m_Alive;            //現在走行できるか否か
    int m_nx;                //競走車の方向の次のx座標
    int m_ny;                //競走車の方向の次のy座標

public:        //メンバー関数
    CLIGHTCYCLE();            //コンストラクター
    void SetDC(HDC);        //デバイスコンテキストの初期化
    void SetLimit(int, int, int, int);    //座標限界の設定
    void Init();            //競技車データ初期化
    void SetData(int, int, int, int, int, int);    //競走車の初期化
    bool CheckNext(int);    //現在方向と位置から次に進めるかを確認
    bool CanGo();            //可能なら進みTRUE、不可なら進まずFALSE
    bool ChangeDir();        //現在の方向以外で進める方向があれ方向転換
};

まず、LightCycleはLIGHTCYCLEクラスの競技車が競技場で競争する想定ですので、競技場は共通(共有)する必要があります。それがPCの場合だとデバイスコンテキスト、x、y座標の上下限になります。この為、CLIGHTCYCLEのオブジェクト(インスタンス)で共有できる静的(static)変数にしています。

次に色、方向、位置、速度や方向転換頻度は個車の属性になるのでそれぞれローカル(動的)変数を用意します。

更に車両の移動量、走行可能か否かの状態(フラグで表します)、次に移動する予定の位置が内部的に必要になってきます。

これらを基に、インスタンスを生成した際の動作(コンストラクター)、デバイスコンテキストの割り当て、移動限界値の代入、次の移動先に移動可能か(移動限界を超える場合や既に走行された軌跡は移動できないルール=仕様)判断(CheckNext)し、行けなければ方向転換(ChangeDir)して、可能(TRUE)であれば走行(CanGo)を継続し、不可(FALSE)となった時に死亡することになります。
 

更にメンバー関数を実装してゆくことになりますが、その場合は実体(インスタンス)の現実の動きが必要になるので、CMyWndクラスでつくったスケルトンに表示させることになります。

 

【プログラミング独り言】

前にちょこっと書き、今回も感じたのですが、ウィンドウズプログラムでウィンドウや表示に関わるクラスとその動作と(今回で言えばCMyWnd)、主題となるクラスとその動作(今回で言えばCLIGHTCYCLE)の線引き(Who's doing what? - どっちが何をやるか)が結構面倒くさく、例えばCLIGHTCYCLEクラスでCanGo関数を作るので、画面に軌跡(CANVASクラスの仮想ウィンドウにドットを打つ)はCLIGHTCYCLEクラスでやるべきか、CanGo関数の結果からm_x、m_yメンバーを参照してCMyWndでやるべきか、という問題です。実際一旦一方でやることにしたものを仕様変更して他方に移したこともありました。こういうこともあるので、コメントは数多く残した方が良いと思います。

 

閑話休題、では実際に最終版のCLIGHTCYCLEクラスの定義はどうなったかというと次の通りです。変更点は赤字で解説します。

///////////////////////////////
//CLIGHTCYCLEクラス定義ファイル
///////////////////////////////
///////////////////////////////【仕様】///////////////////////////////
//【識別色】(m_Color)
//LightCycle車両は識別色を持つ。識別色はユーザーが決定する。
//【方向】(m_Dir)
//LightCycleでは方向を0-7で表す。
//    0                //y--
//  7 ↑ 1            //y--, x++(x--)
//6←    →2        //x++(x--)
// 5 ↓ 3            //y++, x++(x--)
//    4                //y++
//【位置関係】
//LightCycleの現在位置は2次元(平面)上のx(m_x)y(m_y)座標で特定する。
//走行範囲は競技場限界(m_MinX/-x座標最小値、m_MinY-y座標最小値、
//m_MaxX-x座標最大値、m_MaxY-y座標最大値)内とする。
//また、現在の方向と位置に基づく次の移動位置をm_nx、m_nyとする。
//競技の勝敗決定因である移動回数(プログラム的にはコールされた回数)
//はm_Timesとする。(要すれば一番長く生き残った者が勝つ。)
//【方向転換】
//方向転換のルールとして、進行方向の左右二つずつまでを許容
//    0                //m_Dir == 0であれば
//  7 ↑ 1            //左右斜め上(線をまたぐのは禁止)と
//6←    →2        //左右の転回を許す。
//また、競技車に一定間隔で方向を自ら変えることを許す。プ
//ログラム上は一定の動作回数で方向を変換させる。
//(m_Caprice:0-しない、1-1/100、2-1/200、3-1/400 )
//【速度】(m_Speed)
//速度はタイマー、可変とする。(0-1/1000、1-10/1000、2-100/1000ミリ秒)
//////////////////////////////////////////////////////////////////////
(解説:まず、寝ずに考えた速度調整や方向転換頻度の点ですが、最初はm_Delay(ウェイトとして使うつもりだったが、m_Speedに改名)に1、10、100のデータを入れるつもりでした。またm_Capriceにも0、100,200、400のデータを入れるつもりでした。しかし競技車両のデータを入れるダイアログのドロップダウンリストコントロールとの相性から共に「0, 1, 2, (3)」の値とし、それぞれの選択に関わる文字列や数値を別途外部配列変数で用意することが合理的との結論になりました。)

 

class CLIGHTCYCLE
{
public:        //メンバー変数
    //クラス共通静的変数(クラス外宣言が必要)
    static HDC m_hDC;        //表示デバイスのハンドル
    static int m_MinX;        //x座標最小値
    static int m_MinY;        //y座標最小値
    static int m_MaxX;        //x座標最大値
    static int m_MaxY;        //y座標最大値
    //競技車の属性変数
    int m_Color;            //競走車の色(CANVASクラスのカラー番号)
    int m_StartDir;            //競走車の設定進行方向(0-7)
    int m_StartX;            //競走車の最初のx座標
    int m_StartY;            //競走車の最初のy座標

    int m_Speed;            //競走車のスピード(0-1/1000、1-10/1000、2-100/1000ミリ秒)
    int m_Caprice;            //何遍に1回方向転換を行うか(0-しない、1-1/100、2-1/200、3-1/400)
    //内部管理変数
    UINT m_Times;            //競走車の移動回数
    bool m_Alive;            //現在走行できるか否か
    int m_Dir;                //競走車の進行方向(0-7)
    int m_x;                //競走車の現在のx座標
    int m_y;                //競走車の現在のy座標
    int m_nx;                //競走車の方向の次のx座標
    int m_ny;                //競走車の方向の次のy座標
    char m_msg[32];            //死亡時のメッセージ
(解説:まず、個車の属性データですが、当初の物が走行により変化してくるものとそうでないものがある(当たり前だが)ことに気が付きました。これは競技が終わったら「ハイ、おしまい。さようなら。」で済めばよいのですが、「もう一度」となるとデータロードからやり直さなければならないのでユーザー便益が大幅に下がります。ということで、「最初のデータ」と「現在のデータ」を方向性、位置については分けることにしました。また、最初はウェイトのつもりだったm_Delayを、仕様変更でm_Speedにしたこと上記の通りです。

加えて後でステータスバーに死亡した競技車とその移動距離(m_Times)を表示しようと思い、そのメッセージ用の文字列メンバー変数を設けました。
今後の可能性としてですが、とっても暇な方が特定競技車の競技結果を記録し、表示しようと思えば、「色属性でID」としている今のやり方では不味です。名前等のきちんとしたIDをメンバー変数として用意すること、並びに競技終了後、競技結果(例:サバイバル度<死亡順位> X 結果<移動距離>で評価等)を記録するならファイル書き出し関数が必要ですし、その累計結果を表示しようと思えば、ファイル読み込み関数、集計関数、結果表示関数が必要になりますね。これらは皆さんの宿題にしましょう。

 

public:        //メンバー関数
    CLIGHTCYCLE();            //コンストラクター
    void SetDC(HDC);        //デバイスコンテキストの初期化
    void SetLimit(int, int, int, int);    //座標限界の設定
    void Init();            //競技車データ初期化
    void GetReady();        //競技車スタート位置設定
    void SetData(int, int, int, int, int, int);    //競走車の初期化
    bool CheckNext(int);    //現在方向と位置から次に進めるかを確認
    bool CanGo();            //可能なら進みTRUE、不可なら進まずFALSE
    bool ChangeDir();        //現在の方向以外で進める方向があれ方向転換
    char* Dead();            //死亡処理と通知
};
(解説:上記したように「最初のデータ」と「現在のデータ」を方向性、位置については分けるならば、競技実行時に「最初のデータ」で「現在のデータ」を初期化することが必要ですし、終わったデータの入っているm_Timesを初期化する必要もあります。ということでそのような出走前の準備関数を用意しました。また、CanGoで行き場がなくなればm_Alive = FALSE;にしてCanGo関数を殺しますが、その際に死亡通知を作成するDead()関数を用意しました。(この関数で二度殺しをしていますが...汗;)

今後の可能性としては走行不能になったLightCycleを目で追っていないと1ドットが細かいので、位置が分かりづらいことから死んだ際に爆発とかのアニメーションを入れる(注)、とかするとよりゲームっぽくなるでしょうね。

注:死亡位置はCANVAS上にありますから、CANVASの機能を使えば①GetBMP関数で爆発を表示する一定サイズの画面をビットマップで切り取る、②予め用意した爆発を表現する同じサイズのビットマップをPutBMPで連続表示(アニメーション)する、③終わったら切り取ったビットマップをPutBMPで貼り付け、爆発アニメーション前に復元する、ことで実現できます。また、死亡位置の、主ウィンドウのクライアントエリア座標は(m_x, m_y)となりますから、そこに爆発を表現するアニメーション表示用ウィンドウを連続表示させることも一つの考えかと思います。これは「猫でもわかるプログラミング」のWindows SDK編第4部、「第346章透明ウィンドウを作る」「第348章ウィンドウアニメーション」辺りが参考になると思います。

 

さてCLIGHTC]YCLEクラス定義が出来上がったので、そのメンバー関数がどのように実装されているのかを次回でやりましょう。