【LightCycleProc.h】

//////////////////////////////////////////
// LightCycleProc.h
// Copyright (c) 10/30/2021 by BCCSkelton
//////////////////////////////////////////

/////////////////////////////////
//主ウィンドウCMyWndの関数の定義
//ウィンドウメッセージ関数
/////////////////////////////////
bool CMyWnd::OnCreate(WPARAM wParam, LPARAM lParam) {

    //コモンコントロールの初期化
    InitCommonControls();

    //ツールバー登録-Init(hWnd, hIinstance, ID, (以下省略可)Style)
    TBar.Init(m_hWnd, m_hInstance, TOOLBAR);
    //ツールバーボタン用カスタムビットマップ追加
    TBar.AddBmp(m_hInstance, MAKEINTRESOURCE(IDI_BITMAP), 7);
    //ツールバーボタン追加
    TBBUTTON tbb[10];
    ZeroMemory(tbb, sizeof(tbb));
    tbb[0].iBitmap = TBar.m_id + 0;
    tbb[0].fsState = TBSTATE_ENABLED;
    tbb[0].fsStyle = TBSTYLE_BUTTON;
    tbb[0].idCommand = IDM_ADD;
    tbb[1].iBitmap = TBar.m_id + 1;
    tbb[1].fsState = TBSTATE_ENABLED;
    tbb[1].fsStyle = TBSTYLE_BUTTON;
    tbb[1].idCommand = IDM_OPEN;
    tbb[2].iBitmap = TBar.m_id + 2;
    tbb[2].fsState = TBSTATE_ENABLED;
    tbb[2].fsStyle = TBSTYLE_BUTTON;
    tbb[2].idCommand = IDM_SAVE;
    tbb[3].fsStyle = TBSTYLE_SEP;    //セパレーター
    tbb[4].iBitmap = TBar.m_id + 3;
    tbb[4].fsState = TBSTATE_ENABLED;
    tbb[4].fsStyle = TBSTYLE_BUTTON;
    tbb[4].idCommand = IDM_EXIT;
    tbb[5].fsStyle = TBSTYLE_SEP;    //セパレーター
    tbb[6].iBitmap = TBar.m_id + 4;
    tbb[6].fsStyle = TBSTYLE_BUTTON;
    tbb[6].idCommand = IDM_GO;
    tbb[7].iBitmap = TBar.m_id + 5;
    tbb[7].fsStyle = TBSTYLE_BUTTON;
    tbb[7].idCommand = IDM_STOP;
    tbb[8].fsStyle = TBSTYLE_SEP;    //セパレーター
    tbb[9].iBitmap = TBar.m_id + 6;
    tbb[9].fsState = TBSTATE_ENABLED;
    tbb[9].fsStyle = TBSTYLE_BUTTON;
    tbb[9].idCommand = IDM_VERSION;
    TBar.AddButtons(10, tbb);

    //ステータスバー登録-Init(hWnd, hIinstance, ID, (以下省略可)Style)
    SBar.Init(m_hWnd, m_hInstance, STATUSBAR);
    //ステータスバー区画設定
    int sec[3] = {100, 300, -1};
    SBar.SetSection(3, sec);
    //ステータスバー文字列設定
    SBar.SetText(0, "LightCycle Ver 1.0");
    SBar.SetText(2, "マウス左クリックでケーム実行、右クリックでゲーム中止");

    //仮想ウィンドウの初期化
    m_cvs.SetCanvas(m_hWnd);
    //背景を黒の塗り潰しブラシで描画
    m_cvs.Color(0);
    m_cvs.BrSelection(1);
    m_cvs.Clear();

    //LightCycleのデバイスコンテキストの設定
    //m_LC[0].SetDC(m_cvs.hDC());でもよい
    CLIGHTCYCLE::m_hDC = m_cvs.hDC();

    //「ゲームメニュー」とステータスバーを不可にする
    ChangeMenuStatus(FALSE, FALSE);  

    //LightCycle登録数の初期化
    m_NoLC = 0;

    return TRUE;
}

bool CMyWnd::OnTimer(WPARAM wParam, LPARAM lParam) {

    int no;
    switch(wParam) {
    case IDT_TIMER0:
        no = 0;
        break;
    case IDT_TIMER1:
        no = 1;
        break;
    case IDT_TIMER2:
        no = 2;
        break;
    case IDT_TIMER3:
        no = 3;
        break;
    default:
        return FALSE;
    }
    if(m_LC[no].CanGo()) {
        m_cvs.Color(m_LC[no].m_Color);            //競技車両色で
        m_cvs.Dot(m_LC[no].m_x, m_LC[no].m_y);    //走行痕ドットを打つ
    }
    else {
        //タイマー割り込みを停止
        m_tm[no].Stop();
        //ステータスバー第3区分の文字列取得
        char str[MAX_PATH];
        SBar.GetText(2, str);
        CSTR Msg1 = str;
         CSTR Msg2 = g_Col[m_LC[no].m_Color - 8];
        Msg2 = Msg2 + "号";
        Msg2 = Msg2 + m_LC[no].Dead();
        Msg1 = Msg1 + Msg2;
        SBar.SetText(2, Msg1.ToChar());
    }
    //ゲーム終了処理
    if(!m_LC[0].m_Alive && !m_LC[1].m_Alive &&
        !m_LC[2].m_Alive && !m_LC[3].m_Alive) {
        MessageBox(m_hWnd, "全車両停止しました", "競技終了",
                    MB_OK | MB_ICONEXCLAMATION);
        g_On = FALSE;
        ChangeMenuStatus(TRUE, FALSE);
    }
    return TRUE;
}

bool CMyWnd::OnLButtonDown(WPARAM wParam, LPARAM lParam) {

    //メニューアイテムインフォ構造体
    MENUITEMINFO mii;
    //メニューハンドルの取得
    HMENU hMenu = GetMenu(m_hWnd);
    HMENU hGameMenu = GetSubMenu(hMenu, 1);
    mii.cbSize = sizeof(mii);
    mii.fMask = MIIM_STATE;
    GetMenuItemInfo(hGameMenu, IDM_GO, FALSE, &mii);
    if(mii.fState & MFS_DISABLED) {
        MessageBox(m_hWnd, "現在左クリックは使用できません", "注意",
                    MB_OK | MB_ICONWARNING);
        return FALSE;
    }
    else {
        OnGo();
        return TRUE;
    }
}

bool CMyWnd::OnRButtonDown(WPARAM wParam, LPARAM lParam) {

    //メニューアイテムインフォ構造体
    MENUITEMINFO mii;
    //メニューハンドルの取得
    HMENU hMenu = GetMenu(m_hWnd);
    HMENU hGameMenu = GetSubMenu(hMenu, 1);
    mii.cbSize = sizeof(mii);
    mii.fMask = MIIM_STATE;
    GetMenuItemInfo(hGameMenu, IDM_STOP, FALSE, &mii);
    if(mii.fState & MFS_DISABLED) {
        MessageBox(m_hWnd, "現在右クリックは使用できません", "注意",
                    MB_OK | MB_ICONWARNING);
        return FALSE;
    }
    else {
        OnStop();
        return TRUE;
    }
}

bool CMyWnd::OnNotify(WPARAM wParam, LPARAM lParam) {

    //ツールバーツールチップ-因みにこの時のidCtrl(wParam)はメニューアイテムになる(例:IDM_OPEN)
    if(((LPNMHDR)lParam)->code == TTN_NEEDTEXT) {    //TTN_GETDISPINFOと同じ
        ((LPTOOLTIPTEXT)lParam)->hinst = m_hInstance;
        char* tag;
        switch(((LPTOOLTIPTEXT)lParam)->hdr.idFrom) {
        case IDM_ADD:
            tag = "追加・編集";
            break;
        case IDM_OPEN:
            tag = "データを開く";
            break;
        case IDM_SAVE:
            tag = "データの保存";
            break;
        case IDM_EXIT:
            tag = "終了";
            break;
        case IDM_GO:
            tag = "実行";
            break;
        case IDM_STOP:
            tag = "中止";
            break;
            break;
        case IDM_VERSION:
            tag = "バージョン情報";
            break;
        }
        ((LPTOOLTIPTEXT)lParam)->lpszText = tag;
        return TRUE;
    }
    return FALSE;
}

bool CMyWnd::OnSize(WPARAM wParam, LPARAM lParam) {

    TBar.AutoSize();
    SBar.AutoSize();
    //ゲーム開始迄にゲーム領域を確定する
    if(!g_On) {
        CLIGHTCYCLE::m_MinX = 0;
        CLIGHTCYCLE::m_MaxX = LOWORD(lParam);
        RECT rec;
        GetWindowRect(TBar.m_hWnd, &rec);
        CLIGHTCYCLE::m_MinY = rec.bottom - rec.top;
        CLIGHTCYCLE::m_MaxY = HIWORD(lParam);    //仮置き
        GetWindowRect(SBar.m_hWnd, &rec);
        CLIGHTCYCLE::m_MaxY -= (rec.bottom - rec.top);
        //競技場枠の表示
        m_cvs.Clear();
        m_cvs.Color(7);
        m_cvs.Box(CLIGHTCYCLE::m_MinX, CLIGHTCYCLE::m_MinY, CLIGHTCYCLE::m_MaxX, CLIGHTCYCLE::m_MaxY, 0);
        //競技場座標の表示
        char str[64];
        wsprintf(str, "競技場 (%d,%d)-(%d,%d)", CLIGHTCYCLE::m_MinX, CLIGHTCYCLE::m_MinY, CLIGHTCYCLE::m_MaxX, CLIGHTCYCLE::m_MaxY);
        SBar.SetText(1, str);
    }
    return TRUE;
}

bool CMyWnd::OnPaint(WPARAM wParam, LPARAM lParam) {

    PAINTSTRUCT paint;
    m_cvs.OnPaint(BeginPaint(m_hWnd, &paint));
    EndPaint(m_hWnd, &paint);
    return TRUE;
}

bool CMyWnd::OnClose(WPARAM wParam, LPARAM lParam) {

    if(MessageBox(m_hWnd, "終了しますか", "終了確認",
                    MB_YESNO | MB_ICONINFORMATION) == IDYES) {
        //タイマーを廃棄
        for(int i = 0; i < m_NoLC; i++)
            m_tm[i].Stop();
        //処理をするとDestroyWindow、PostQuitMessageが呼ばれる
        return TRUE;
    }
    else
        //そうでなければウィンドウではDefWindowProc関数をreturn、ダイアログではreturn FALSEとなる。
        return FALSE;
}

bool CMyWnd::OnMinMax(WPARAM wParam, LPARAM lParam) {

    //典型的なウィンドウのサイズ制限処理
    MINMAXINFO *pmmi;
    pmmi = (MINMAXINFO*)lParam;
    pmmi->ptMinTrackSize.x = 656;    // 最小幅(競技エリア640)
    pmmi->ptMinTrackSize.y = 509;    // 最小高(競技エリア400)

    return FALSE;                    //処理はDefWndProcに任す
}

/////////////////////////////////
//主ウィンドウCMyWndの関数の定義
//メニュー項目、コントロール関数
/////////////////////////////////
bool CMyWnd::OnAdd() {

    if(inputdlg.DoModal(m_hWnd, "IDD_INPUT", inputdlgProc)) {
        if(m_NoLC)    //m_NoLCが0でなくなったらゲーム実行可能
            ChangeMenuStatus(TRUE, FALSE);
        return TRUE;
    }
    else {
        MessageBox(m_hWnd, "処理がキャンセルされました。", "エラー", MB_OK | MB_ICONERROR);
        return FALSE;
    }
}

bool CMyWnd::OnOpen() {

    char* fn = cmndlg.GetFileName(m_hWnd, "LightCycleデータファイル(*.lcd)\0*.lcd\0\0", TRUE, "lcd", ".", "LightCycleデータを開く");
    if(fn) {
        g_File.SetName(fn);    //ファイルパス、名の登録
        m_NoLC = g_File.ReadInt("車両", "台数");
        char str[16];        //セクション名用バッファ
        int Color, StartDir, StartX, StartY, Speed, Caprice;    //各属性変数
        for(int i = 0; i < m_NoLC; i++) {
            wsprintf(str, "車両No. %d", i);
            Color = g_File.ReadInt(str, "車両色");
            StartDir = g_File.ReadInt(str, "方向");
            StartX = g_File.ReadInt(str, "X座標");
            StartY = g_File.ReadInt(str, "Y座標");
            Speed = g_File.ReadInt(str, "速度");
            Caprice = g_File.ReadInt(str, "転回頻度");
            //データをセットするのみならず、走行可能状態にする
            m_LC[i].SetData(Color, StartDir, StartX, StartY, Speed, Caprice);
        }
        //m_NoLCが0でなくなったら以下を実行する。
        if(m_NoLC)
            ChangeMenuStatus(TRUE, FALSE);
        return TRUE;
    }
    else {
        MessageBox(m_hWnd, "処理がキャンセルされました", "エラー", MB_OK | MB_ICONERROR);
        return FALSE;
    }
}

bool CMyWnd::OnSave() {

    char* fn = cmndlg.GetFileName(m_hWnd, "LightCycleデータファイル(*.lcd)\0*.lcd\0\0", FALSE, "lcd", ".", "LightCycleデータを保存");
    if(fn) {
        g_File.SetName(fn);    //ファイルパス、名の登録
        g_File.WriteInt("車両", "台数", m_NoLC);
        char str[16];        //セクション名用バッファ
        for(int i = 0; i < m_NoLC; i++) {
            wsprintf(str, "車両No. %d", i);
            g_File.WriteInt(str, "車両色", m_LC[i].m_Color);
            g_File.WriteInt(str, "方向", m_LC[i].m_StartDir);
            g_File.WriteInt(str, "X座標", m_LC[i].m_StartX);
            g_File.WriteInt(str, "Y座標", m_LC[i].m_StartY);
            g_File.WriteInt(str, "速度", m_LC[i].m_Speed);
            g_File.WriteInt(str, "転回頻度", m_LC[i].m_Caprice);
        }
        return TRUE;
    }
    else {
        MessageBox(m_hWnd, "処理がキャンセルされました", "エラー", MB_OK | MB_ICONERROR);
        return FALSE;
    }
}

bool CMyWnd::OnExit() {

    SendMsg(WM_CLOSE, 0, 0);
    return TRUE;
}

bool CMyWnd::OnGo() {

    //ゲーム終了後は画面をクリア
    if(!g_On) {
        m_cvs.Clear();
        //競技場枠の表示
        m_cvs.Color(7);
        m_cvs.Box(CLIGHTCYCLE::m_MinX, CLIGHTCYCLE::m_MinY, CLIGHTCYCLE::m_MaxX, CLIGHTCYCLE::m_MaxY, 0);
    }
     //競技車のデータが登録されてい ない場合の処置
    if(!m_NoLC) {
        MessageBox(m_hWnd, "競技車のデータが登録されていません\n「ファイル"\
                        "「新規」または「データを開く」で設定してください。",
                        "エラー", MB_OK | MB_ICONERROR);
        return FALSE;
    }
    else {
        //タイマーの設定(ミリ秒設定なので0.001秒タイマーとなっている)
        for(int i = 0; i < m_NoLC; i++) {
            if(!g_On)                    //ゲーム開始前なら
                m_LC[i].GetReady();        //出走用意
            if(!m_tm[i].Begin(m_hWnd, g_Timer[i], g_Velo[m_LC[i].m_Speed])) {
                MessageBox(m_hWnd, "タイマーを設定できませんでした", "エラー",
                    MB_OK | MB_ICONERROR);
                return FALSE;
            }
        }
        //ゲーム開始後は「実行」メニューアイテムとボタンを凍結する。
        g_On = TRUE;
        //「ファイル」メニューを不可にする
        EnableMenuItem(GetMenu(m_hWnd), 0, MF_BYPOSITION | MF_GRAYED);
        //「ゲーム」メニューのアイテムの状態を変更する
        ChangeMenuStatus(FALSE, TRUE);
        //ステータスバーによる結果表示の為
        SBar.SetText(2, "");
        return TRUE;
    }
}

bool CMyWnd::OnStop() {

    int ret = MessageBox(m_hWnd, "完全に中止しますか(No-一時停止)", "確認",
                        MB_YESNOCANCEL | MB_ICONQUESTION);
    switch(ret) {
    case IDYES:
        //全車死亡、タイマーを廃棄
        for(int i = 0; i < m_NoLC; i++)
            m_LC[i].Dead();
        g_On = FALSE;
    case IDNO:
        //タイマーを廃棄
        for(int i = 0; i < m_NoLC; i++) {
            m_tm[i].Stop();
        }
        //「ファイル」メニューを可にする
        EnableMenuItem(GetMenu(m_hWnd), 0, MF_BYPOSITION | MF_ENABLED);
        //「ゲーム」メニューのアイテムの状態を変更する
        ChangeMenuStatus(TRUE, FALSE);
        return TRUE;
    case IDCANCEL:
        return FALSE;
    }
}

bool CMyWnd::OnVersion() {

    versiondlg.DoModal(m_hWnd, "IDD_VERSION", versiondlgProc);
    return TRUE;
}

//////////////////
//ユーザー定義関数
//////////////////
///////////////////////////////////////////
//「ゲーム」メニュー、ツールバー状態の変更
///////////////////////////////////////////
void CMyWnd::ChangeMenuStatus(bool Flag1, bool Flag2) {

    //メニューハンドルの取得
    HMENU hMenu = GetMenu(m_hWnd);
    //メニューアイテムを不可にする
    HMENU hGameMenu = GetSubMenu(hMenu, 1);
    EnableMenuItem(hGameMenu, IDM_GO, MF_BYCOMMAND | (Flag1 ? MF_ENABLED : MF_GRAYED));
    EnableMenuItem(hGameMenu, IDM_STOP, MF_BYCOMMAND | (Flag2 ? MF_ENABLED : MF_GRAYED));
    DrawMenuBar(LightCycle.GetHandle());    //Re-draw "&Game" menu

    //ツールバー(MAKELONGマクロは16bit整数2つをunsigned 32bit整数にする)
    SendMessage(TBar.GetHandle(), TB_ENABLEBUTTON, IDM_GO, MAKELONG(Flag1, 0));
    SendMessage(TBar.GetHandle(), TB_ENABLEBUTTON, IDM_STOP, MAKELONG(Flag2, 0));
}

///////////////////////////////
//ユーザーダイアログの関数定義
//Inputダイアログ
///////////////////////////////
bool INPUTDLG::OnInit(WPARAM, LPARAM) {

    char str[16];    //コンボボックス登録用文字列変数
    //競技車番号
    for(int i = 0; i < LightCycle.m_NoLC; i++) {
        wsprintf(str, "%d", i + 1);
        SendItemMsg(IDC_COMBOBOX0, CB_ADDSTRING, 0, (LPARAM)str);
    }
    //競技車色
    for(int i = 0; i < 8; i++) {
        SendItemMsg(IDC_COMBOBOX1, CB_ADDSTRING, 0, (LPARAM)g_Col[i]);
    }
    //進行方向
    for(int i = 0; i < 8; i++) {
        SendItemMsg(IDC_COMBOBOX2, CB_ADDSTRING, 0, (LPARAM)g_Dir[i]);
    }
    //速度
    for(int i = 0; i < 3; i++) {
        SendItemMsg(IDC_COMBOBOX3, CB_ADDSTRING, 0, (LPARAM)g_Speed[i]);
    }
    //転回頻度
    for(int i = 0; i < 4; i++) {
        SendItemMsg(IDC_COMBOBOX4, CB_ADDSTRING, 0, (LPARAM)g_Caprice[i]);
    }
    //登録車両数が0 - 3迄は次の未入力LC[i]を、4の場合先頭を指すは
    if(LightCycle.m_NoLC < 4)
        SendItemMsg(IDC_COMBOBOX0, CB_SETCURSEL, LightCycle.m_NoLC, 0);
    else {
        SendItemMsg(IDC_COMBOBOX0, CB_SETCURSEL, 0, 0);
        SetData(0);
    }

    //対象コントロールのハンドルを取得する
    HWND hCtrl = GetDlgItem(m_hWnd, IDC_IMAGE);
    //文字列リソースのある親ウィンドウ(ダイアログ)のインスタンス
    m_hInstance;
    //ツールチップの作成
    m_hTip = CreateWindowEx(NULL, TOOLTIPS_CLASS, NULL,
                WS_POPUP | TTS_ALWAYSTIP | TTS_BALLOON,
                CW_USEDEFAULT, CW_USEDEFAULT,
                CW_USEDEFAULT, CW_USEDEFAULT,
                m_hWnd, NULL, 
                m_hInstance, NULL);
    //IDC_IMAGEにツールヒントコントロールを付ける
    TOOLINFO toolInfo = { 0 };    //ゼロクリアー
    toolInfo.cbSize = sizeof(toolInfo);
    toolInfo.hwnd = m_hWnd;
    toolInfo.uFlags = TTF_IDISHWND | TTF_SUBCLASS;
    toolInfo.uId = (UINT_PTR)GetDlgItem(m_hWnd, IDC_IMAGE);
    toolInfo.hinst = m_hInstance;
    toolInfo.lpszText = "編集状態でも、登録が4台未満ならクリックすると「追加」になります";
    SendMessage(m_hTip, TTM_ADDTOOL, 0, (LPARAM)&toolInfo);
    SetWindowPos(m_hTip, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
    return TRUE;
}

bool INPUTDLG::OnSelChange(WPARAM wParam) {

    //コンボボックスの選択変更以外は何もしない
    if(HIWORD(wParam) != CBN_SELCHANGE)
        return FALSE;
    //新しい選択アイテムインデックスを取得する
    int i = SendItemMsg(IDC_COMBOBOX0, CB_GETCURSEL, 0, 0);
    SetData(i);
    return TRUE;
}

bool INPUTDLG::OnImage() {

    if(LightCycle.m_NoLC < 4) {
        SendMsg(WM_SETTEXT, 0, (LPARAM)"LightCycleデータの追加");
        SendItemMsg(IDC_COMBOBOX0, CB_SETCURSEL, (WPARAM)-1, 0);
        SendItemMsg(IDC_COMBOBOX1, CB_SETCURSEL, (WPARAM)-1, 0);
        SendItemMsg(IDC_COMBOBOX2, CB_SETCURSEL, (WPARAM)-1, 0);
        SendItemMsg(IDC_COMBOBOX3, CB_SETCURSEL, (WPARAM)-1, 0);
        SendItemMsg(IDC_COMBOBOX4, CB_SETCURSEL, (WPARAM)-1, 0);
        SendItemMsg(IDC_EDIT2, WM_SETTEXT, 0, (LPARAM)"");
        SendItemMsg(IDC_EDIT3, WM_SETTEXT, 0, (LPARAM)"");
    }
}

bool INPUTDLG::OnOk() {

    //選択車両番号インデックス(0-3)を取得する
    int i = SendItemMsg(IDC_COMBOBOX0, CB_GETCURSEL, 0, 0);
    if(i == CB_ERR)    //選択が無ければ追加登録
        i = LightCycle.m_NoLC;
    //車両色(0-7)の取得
    int j = SendItemMsg(IDC_COMBOBOX1, CB_GETCURSEL, 0, 0);
    if(j == CB_ERR) {
        MessageBox(m_hWnd, "色が選択されていません。", "エラー", MB_OK | MB_ICONERROR);
        return FALSE;    //ダイアログを開いたまま処理を無効とする
    }
    LightCycle.m_LC[i].m_Color = j + 8;    //CANVASクラスの色コード(8-15)
    //車両の方向(0-7)
    j = SendItemMsg(IDC_COMBOBOX2, CB_GETCURSEL, 0, 0);
    if(j == CB_ERR) {
        MessageBox(m_hWnd, "方向が選択されていません。", "エラー", MB_OK | MB_ICONERROR);
        return FALSE;    //ダイアログを開いたまま処理を無効とする
    }
    LightCycle.m_LC[i].m_StartDir = j;
    //車両のX座標
    char str[16];        //エディットボックス用文字列変数
    SendItemMsg(IDC_EDIT2, WM_GETTEXT, 16, (LPARAM)str);
    j = atoi(str);        //エラー処理をどうするか?
    if(j <= 0 || j > CLIGHTCYCLE::m_MaxX) {
        MessageBox(m_hWnd, "Xの値が不正です。", "エラー", MB_OK | MB_ICONERROR);
        return FALSE;    //ダイアログを開いたまま処理を無効とする
    }
    LightCycle.m_LC[i].m_StartX = j;
    //車両のY座標
    SendItemMsg(IDC_EDIT3, WM_GETTEXT, 16, (LPARAM)str);
    j = atoi(str);        //エラー処理をどうするか?
    if(j <= 0 || j > CLIGHTCYCLE::m_MaxY) {
        MessageBox(m_hWnd, "Xの値が不正です。", "エラー", MB_OK | MB_ICONERROR);
        return FALSE;    //ダイアログを開いたまま処理を無効とする
    }
    LightCycle.m_LC[i].m_StartY = j;
    //車両の速度(0-2)
    j = SendItemMsg(IDC_COMBOBOX3, CB_GETCURSEL, 0, 0);
    if(j == CB_ERR) {
        MessageBox(m_hWnd, "速度が選択されていません。", "エラー", MB_OK | MB_ICONERROR);
        return FALSE;    //ダイアログを開いたまま処理を無効とする
    }
    LightCycle.m_LC[i].m_Speed = j;
    //車両の転向頻度(0-3)
    j = SendItemMsg(IDC_COMBOBOX4, CB_GETCURSEL, 0, 0);
    if(j == CB_ERR) {
        MessageBox(m_hWnd, "転向頻度が選択されていません。", "エラー", MB_OK | MB_ICONERROR);
        return FALSE;    //ダイアログを開いたまま処理を無効とする
    }
    LightCycle.m_LC[i].m_Caprice = j;
    //走行可能状態にする
    LightCycle.m_LC[i].GetReady();
    //追加登録の場合、車両台数を増やす
    if(i == LightCycle.m_NoLC)
        LightCycle.m_NoLC++;
    EndModal(TRUE);
    return TRUE;
}

bool INPUTDLG::OnCancel() {

    EndModal(FALSE);
    return TRUE;
}

void INPUTDLG::SetData(int i) {

    char str[16];    //エディットボックス用文字列変数
    //既存データが選択されたので編集であることを示す
    SendMsg(WM_SETTEXT, 0, (LPARAM)"LightCycleデータの編集");
    //CANVASクラスの色コード8-15を使用している為
    SendItemMsg(IDC_COMBOBOX1, CB_SETCURSEL, (LightCycle.m_LC[i].m_Color - 8), 0);
    SendItemMsg(IDC_COMBOBOX2, CB_SETCURSEL, LightCycle.m_LC[i].m_StartDir, 0);
    wsprintf(str, "%d", LightCycle.m_LC[i].m_StartX);
    SendItemMsg(IDC_EDIT2, WM_SETTEXT, 0, (LPARAM)str);
    wsprintf(str, "%d", LightCycle.m_LC[i].m_StartY);
    SendItemMsg(IDC_EDIT3, WM_SETTEXT, 0, (LPARAM)str);
    SendItemMsg(IDC_COMBOBOX3, CB_SETCURSEL, (WPARAM)LightCycle.m_LC[i].m_Speed, 0);
    SendItemMsg(IDC_COMBOBOX4, CB_SETCURSEL, LightCycle.m_LC[i].m_Caprice, 0);
}

///////////////////////////////
//ユーザーダイアログの関数定義
//Versionダイアログ
///////////////////////////////
bool VERSIONDLG::OnOk() {

    EndModal(TRUE);
    return TRUE;
}

bool VERSIONDLG::OnCancel() {

    EndModal(FALSE);
    return TRUE;
}
 

 

【LightCycle.cpp】

//////////////////////////////////////////
// LightCycle.cpp
//Copyright (c) 10/30/2021 by BCCSkelton
//////////////////////////////////////////
#include    "LightCycle.h"
#include    "LightCycleProc.h"

////////////////
// WinMain関数
////////////////
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
                    LPSTR lpCmdLine, int nCmdShow) {

    //2重起動防止
    if(!LightCycle.IsOnlyOne()) {
        //ここに2重起動時の処理を書く(下記は1例)
        HWND hWnd = FindWindow("MainWnd", "LightCycle");
        if(IsIconic(hWnd))
            ShowWindow(hWnd, SW_RESTORE);
        SetForegroundWindow(hWnd);
        return 0L;
    }

    //ウィンドウ登録 - Init(ClassName, hInstance, WndProc, "IDM_MAIN",
    //                        (以下省略可)MAKEINTRESOURCE(IDI_ICON), IDC_ARROW, Brush)
    LightCycle.Init("MainWnd", hInstance, SDIPROC, "IDM_MAIN", MAKEINTRESOURCE(IDI_ICON));

    //ウィンドウ作成と表示-Create(WindowTitle, (以下省略可)Style,
    //                        ExStyle, hParent, hMenu, x, y, w, h)
    if(!LightCycle.Create("LightCycle"))
        return 0L;

    //メッセージループに入る
    return LightCycle.Loop();
}
 

【LightCycle.h】

//////////////////////////////////////////
// LightCycle.h
// Copyright (c) 10/30/2021 by BCCSkelton
//////////////////////////////////////////
//BCCSkeltonのヘッダー-これに必要なヘッダーが入っている
#include    "BCCSkelton.h"
//リソースIDのヘッダー
#include    "ResLightCycle.h"
//CLightCycleクラスのヘッダー
#include    "CLightCycle.h"
//ユーザー定義のヘッダー
#include    "User.h"

/////////////////////////////////////////////////////////////////////
//CMyWndクラスをCSDIクラスから派生させ、メッセージ用の関数を宣言する
/////////////////////////////////////////////////////////////////////
class CMyWnd : public CSDI
{
public:    //メンバー変数
    CANVAS m_cvs;            //仮想ウィンドウ
    int m_NoLC;                //LightCycle台数
    CTIMER m_tm[4];            //タイマー
    CLIGHTCYCLE m_LC[4];    //LightCycle競技車
public:    //以下はコールバック関数マクロと関連している
    //2重起動防止用のMutex用ID名称
    CMyWnd(char* UName) : CSDI(UName) {}
    //メニュー項目、ダイアログコントロール関連
    bool OnAdd();
    bool OnOpen();
    bool OnSave();
    bool OnExit();
    bool OnGo();
    bool OnStop();
    bool OnVersion();
    bool OnSLine();
    bool OnQCurve();
    bool OnCCurve();
    bool OnCircle();
    bool OnSine();
    //ウィンドウメッセージ関連
    bool OnCreate(WPARAM, LPARAM);
    bool OnTimer(WPARAM, LPARAM);
    bool OnLButtonDown(WPARAM, LPARAM);
    bool OnRButtonDown(WPARAM, LPARAM);
    bool OnNotify(WPARAM, LPARAM);
    bool OnSize(WPARAM, LPARAM);
    bool OnPaint(WPARAM, LPARAM);
    bool OnClose(WPARAM, LPARAM);
    bool OnMinMax(WPARAM, LPARAM);
    //ユーザー定義関数
    void ChangeMenuStatus(bool, bool);    //「ゲーム」メニューとツールバーボタン状態の変更
};

////////////////////////////////////////////////////////////////////////
//派生させたCMyWndクラスのインスタンスとコールバック関数(マクロ)の作成
//主ウィンドウはダイアログと違い、コールバック関数は一つしか作れない
////////////////////////////////////////////////////////////////////////
CMyWnd LightCycle("LightCycle");    //ウィンドウクラスインスタンスの生成

BEGIN_SDIMSG(LightCycle)    //ダイアログと違い、コールバック関数名を特定しない
    //メニュー項目、ダイアログコントロール関連
    ON_COMMAND(LightCycle, IDM_ADD, OnAdd())
    ON_COMMAND(LightCycle, IDM_OPEN, OnOpen())
    ON_COMMAND(LightCycle, IDM_SAVE, OnSave())
    ON_COMMAND(LightCycle, IDM_EXIT, OnExit())
    ON_COMMAND(LightCycle, IDM_GO, OnGo())
    ON_COMMAND(LightCycle, IDM_STOP, OnStop())
    ON_COMMAND(LightCycle, IDM_VERSION, OnVersion())
    //ウィンドウメッセージ関連
    ON_CREATE(LightCycle)
    ON_TIMER(LightCycle)
    ON_LBUTTONDOWN(LightCycle)
    ON_RBUTTONDOWN(LightCycle)
    ON_NOTIFY(LightCycle)
    ON_SIZE(LightCycle)
    ON_PAINT(LightCycle)
    ON_CLOSE(LightCycle)
    ON_(LightCycle, WM_GETMINMAXINFO, OnMinMax(wParam, lParam))
END_WNDMSG

///////////////////////////////////////////
// CDLGクラスからINPUTDLGクラスを派生
// 複数の同一ダイアログ変数とダイアログも作
// れるが、一つのダイアログに一つの派生ダイ
// アログクラスを作成するのが基本
///////////////////////////////////////////
class INPUTDLG : public CDLG {
public:
    //メンバー変数
    HWND m_hTip;                //IDC_IMAGE用ツールチップ
    //ウィンドウメッセージ関連
    bool OnInit(WPARAM, LPARAM);
    //コントロール関連
    bool OnSelChange(WPARAM);
    bool OnImage();
    bool OnOk();
    bool OnCancel();
    //ユーザー定義関数
    void SetData(int);
};

////////////////////////////////////////////////////////////////////////////
// INPUTDLGクラスダイアログ変数の生成とそのコールバック関数(マクロ)を定義
// 複数同一クラスのダイアログを作成することを予期してコールバック関数を明記
////////////////////////////////////////////////////////////////////////////
INPUTDLG inputdlg;

BEGIN_MODALDLGMSG(inputdlgProc, inputdlg)    //第1引数がコールバック関数の名前
    ON_COMMAND(inputdlg, IDC_COMBOBOX0, OnSelChange(wParam))
    ON_COMMAND(inputdlg, IDC_IMAGE, OnImage())
    ON_COMMAND(inputdlg, IDOK, OnOk())
    ON_COMMAND(inputdlg, IDCANCEL, OnCancel())
END_DLGMSG

///////////////////////////////////////////
// CDLGクラスからVERSIONDLGクラスを派生
// 複数の同一ダイアログ変数とダイアログも作
// れるが、一つのダイアログに一つの派生ダイ
// アログクラスを作成するのが基本
///////////////////////////////////////////
class VERSIONDLG : public CDLG {
public:
    //コントロール関連
    bool OnOk();
    bool OnCancel();
};

////////////////////////////////////////////////////////////////////////////
// VERSIONDLGクラスダイアログ変数の生成とそのコールバック関数(マクロ)を定義
// 複数同一クラスのダイアログを作成することを予期してコールバック関数を明記
////////////////////////////////////////////////////////////////////////////
VERSIONDLG versiondlg;

BEGIN_MODALDLGMSG(versiondlgProc, versiondlg)    //第1引数がコールバック関数の名前
    ON_COMMAND(versiondlg, IDOK, OnOk())
    ON_COMMAND(versiondlg, IDCANCEL, OnCancel())
END_DLGMSG

///////////////////
//ツールバーの作成
///////////////////
CTBAR TBar;

///////////////////////
//ステータスバーの作成
///////////////////////
CSBAR SBar;

////////////////////////
//コモンダイアログの作成
////////////////////////
CMNDLG cmndlg;

 

【User.h】

///////////////////
//ユーザー定義関係
///////////////////

///////////////////////
//LightCycleの識別色名
///////////////////////
char* g_Col[8] = {"グレイ",
                "ブルー",
                "レッド",
                "マゼンダ",
                "グリーン",
                "シアン",
                "イエロー",
                "ホワイト"};

///////////////////////
//LightCycleの進行方向
///////////////////////
char* g_Dir[8] = {"上",
                "右上",
                "右",
                "右下",
                "下",
                "左下",
                "左",
                "左上"};

///////////////////////
//LightCycleのスピード
///////////////////////
char* g_Speed[3] = {"1/1000",
                "10/1000",
                "100/1000"};

int g_Velo[3] = {1, 10, 100};

///////////////////////
//LightCycleの進行方向
///////////////////////
char* g_Caprice[4] = {"転回しない",
                "100回に1回",
                "200回に1回",
                "400回に1回"};

///////////////////////////////////////
//CLIGHTCYCLEクラス静的変数インスタンス
///////////////////////////////////////
HDC CLIGHTCYCLE::m_hDC;        //表示デバイスのハンドル
int CLIGHTCYCLE::m_MinX;    //x座標最小値
int CLIGHTCYCLE::m_MinY;    //y座標最小値
int CLIGHTCYCLE::m_MaxX;    //x座標最大値
int CLIGHTCYCLE::m_MaxY;    //y座標最大値

//////////////////////
//Timer定数の外部変数
//////////////////////
UINT g_Timer[4] = {IDT_TIMER0, IDT_TIMER1, IDT_TIMER2, IDT_TIMER3};

/////////////////////
//ゲーム進行中フラグ
/////////////////////
bool g_On = FALSE;

//////////////////////////////////
//データファイル用CINIインスタンス
//////////////////////////////////
CINI g_File;
 

 

【LightCycle.rc】

//-----------------------------------------
//             BCCForm Ver 2.41
//    An Easy Resource Editor for BCC
//  Copyright (c) February 2002 by ysama
//-----------------------------------------
#include    "ResLightCycle.h"

//----------------------------------
// ダイアログ (IDD_INPUT)
//----------------------------------
IDD_INPUT DIALOG DISCARDABLE 0, 0, 158, 140
EXSTYLE WS_EX_DLGMODALFRAME
STYLE WS_POPUP | WS_VISIBLE | WS_CAPTION | DS_MODALFRAME | DS_3DLOOK | DS_CENTER
CAPTION "LightCycleデータの追加"
FONT 9, "Times New Roman"
{
 CONTROL "", IDC_COMBOBOX0, "COMBOBOX", WS_CHILD | WS_VISIBLE | WS_TABSTOP | CBS_DROPDOWNLIST | WS_VSCROLL, 70, 8, 52, 48
 CONTROL "", IDC_COMBOBOX1, "COMBOBOX", WS_CHILD | WS_VISIBLE | WS_TABSTOP | CBS_DROPDOWNLIST | WS_VSCROLL, 70, 26, 84, 82
 CONTROL "", IDC_COMBOBOX2, "COMBOBOX", WS_CHILD | WS_VISIBLE | WS_TABSTOP | CBS_DROPDOWNLIST | WS_VSCROLL, 70, 42, 84, 82
 CONTROL "", IDC_EDIT2, "EDIT", WS_CHILD | WS_BORDER | WS_VISIBLE | WS_TABSTOP | ES_AUTOHSCROLL | ES_LEFT, 70, 58, 84, 12, WS_EX_CLIENTEDGE
 CONTROL "", IDC_EDIT3, "EDIT", WS_CHILD | WS_BORDER | WS_VISIBLE | WS_TABSTOP | ES_AUTOHSCROLL | ES_LEFT, 70, 74, 84, 12, WS_EX_CLIENTEDGE
 CONTROL "", IDC_COMBOBOX3, "COMBOBOX", WS_CHILD | WS_VISIBLE | WS_TABSTOP | CBS_DROPDOWNLIST | WS_VSCROLL, 70, 90, 84, 42
 CONTROL "", IDC_COMBOBOX4, "COMBOBOX", WS_CHILD | WS_VISIBLE | WS_TABSTOP | CBS_DROPDOWNLIST | WS_VSCROLL, 70, 106, 84, 48
 CONTROL "OK", IDOK, "BUTTON", BS_DEFPUSHBUTTON | BS_CENTER | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 112, 124, 40, 12
 CONTROL "Cancel", IDCANCEL, "BUTTON", BS_PUSHBUTTON | BS_CENTER | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 6, 124, 40, 12
 CONTROL "車両番号", IDC_LABEL1, "STATIC", WS_CHILD | WS_VISIBLE | SS_NOTIFY, 6, 10, 64, 10
 CONTROL "車両識別色", IDC_LABEL2, "STATIC", WS_CHILD | WS_VISIBLE | SS_NOTIFY, 6, 28, 64, 10
 CONTROL "進行方向", IDC_LABEL3, "STATIC", WS_CHILD | WS_VISIBLE | SS_NOTIFY, 6, 44, 64, 10
 CONTROL "X座標", IDC_LABEL4, "STATIC", WS_CHILD | WS_VISIBLE | SS_NOTIFY, 6, 60, 64, 10
 CONTROL "Y座標", IDC_LABEL5, "STATIC", WS_CHILD | WS_VISIBLE | SS_NOTIFY, 6, 76, 64, 10
 CONTROL "速度(ミリ秒)", IDC_LABEL6, "STATIC", WS_CHILD | WS_VISIBLE | SS_NOTIFY, 6, 92, 64, 10
 CONTROL "転回頻度", IDC_LABEL7, "STATIC", WS_CHILD | WS_VISIBLE | SS_NOTIFY, 6, 108, 64, 10
 CONTROL IDI_ICON, IDC_IMAGE, "STATIC", WS_CHILD | WS_VISIBLE | SS_NOTIFY | SS_SUNKEN | SS_ICON | SS_CENTERIMAGE, 132, 2, 22, 22
}

//----------------------------------
// ダイアログ (IDD_VERSION)
//----------------------------------
IDD_VERSION DIALOG DISCARDABLE 0, 0, 160, 40
EXSTYLE WS_EX_DLGMODALFRAME
STYLE WS_POPUP | WS_VISIBLE | WS_CAPTION | DS_MODALFRAME | DS_3DLOOK | DS_CENTER
CAPTION "バージョン情報"
FONT 9, "Times New Roman"
{
 CONTROL IDI_ICON, 0, "STATIC", SS_SUNKEN | SS_ICON | WS_CHILD | WS_VISIBLE, 12, 10, 32, 32
 CONTROL "LightCycle Version 1.0\nCopyright 2021b by Ysama", 0, "STATIC", SS_CENTER | SS_SUNKEN | WS_CHILD | WS_VISIBLE, 42, 8, 80, 24
 CONTROL "OK", IDOK, "BUTTON", BS_PUSHBUTTON | BS_CENTER | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 130, 14, 20, 12
}

//-------------------------
// メニュー(IDM_MAIN)
//-------------------------
IDM_MAIN MENU DISCARDABLE
{
    POPUP "ファイル(&F)"
    {
        MENUITEM "追加(&A)・編集", IDM_ADD
        MENUITEM "データを開く(&O)", IDM_OPEN
        MENUITEM "データの保存(&S)", IDM_SAVE

 


        MENUITEM SEPARATOR
        MENUITEM "終了(&X)", IDM_EXIT
    }
    POPUP "ゲーム(&G)"
    {
        MENUITEM "実行(&G)", IDM_GO
        MENUITEM "中止(&S)", IDM_STOP
    }
    POPUP "ヘルプ(&H)"
    {
        MENUITEM "バージョン情報(&V)", IDM_VERSION
    }
}

//--------------------------
// イメージ(IDI_ICON)
//--------------------------
IDI_ICON    ICON    DISCARDABLE    "C:\Users\(パス)LightCycle\LightCycle.ico"

//--------------------------
// イメージ(IDI_BITMAP)
//--------------------------
IDI_BITMAP    BITMAP    DISCARDABLE    "C:\Users\(パス)LightCycle\ToolBarBmp.bmp"

 

【ResLightCycle.h】

//-----------------------------------------
//             BCCForm Ver 2.41
//   Header File for Resource Script File
//   Copyright (c) February 2002 by ysama
//-----------------------------------------
//---------------------
//  ダイアログリソース
//---------------------
// ダイアログ IDD_INPUT
#define    IDC_COMBOBOX0    100
#define    IDC_COMBOBOX1    101
#define    IDC_COMBOBOX2    102
#define    IDC_EDIT2        103
#define    IDC_EDIT3        104
#define    IDC_COMBOBOX3    105
#define    IDC_COMBOBOX4    106
#define    IDC_LABEL1        107
#define    IDC_LABEL2        108
#define    IDC_LABEL3        109
#define    IDC_LABEL4        110
#define    IDC_LABEL5        111
#define    IDC_LABEL6        112
#define    IDC_LABEL7        113
#define    IDC_IMAGE        114
// ダイアログ IDD_VERSION

//---------------------
//  メニューリソース
//---------------------
// メニュー IDM_MAIN
#define    IDM_ADD            300
#define    IDM_OPEN        301
#define    IDM_SAVE        302
#define    IDM_EXIT        303
#define    IDM_GO            304
#define    IDM_STOP        305
#define    IDM_VERSION        306

//---------------------
//  イメージリソース
//---------------------
#define    IDI_ICON        400
#define    IDI_BITMAP        500

//---------------------
//  ストリングテーブル
//---------------------

//--------------------
//  アクセラレーター
//--------------------

//------------------
//  ヴァージョン情報
//------------------

//-------------
//  タイマー
//-------------
#define    IDT_TIMER0    600
#define    IDT_TIMER1    700
#define    IDT_TIMER2    800
#define    IDT_TIMER3    900
 

 

 

 

ではLightCycleを動かしてみましょう。簡単に終わるように最小サイズの競技場(640 x 400)で4台の競技車を走らせます。以下はその各車データと終了画像です。

 

【Test01.lcd】

[車両]
台数=4
[車両No. 0]
車両色=9
方向=2
X座標=40
Y座標=68
速度=0
転回頻度=0
[車両No. 1]
車両色=10
方向=4
X座標=600
Y座標=68
速度=0
転回頻度=0
[車両No. 2]
車両色=12
方向=6
X座標=600
Y座標=388
速度=0
転回頻度=0
[車両No. 3]
車両色=14
方向=0
X座標=40
Y座標=388
速度=0
転回頻度=0

 

【Test02.lcd】

[車両]
台数=4
[車両No. 0]
車両色=9
方向=2
X座標=40
Y座標=68
速度=0
転回頻度=0
[車両No. 1]
車両色=10
方向=4
X座標=600
Y座標=68
速度=1
転回頻度=1
[車両No. 2]
車両色=12
方向=6
X座標=600
Y座標=388
速度=2
転回頻度=2
[車両No. 3]
車両色=14
方向=0
X座標=40
Y座標=388
速度=0
転回頻度=3

 

【Test03.lcd】

[車両]
台数=4
[車両No. 0]
車両色=9
方向=2
X座標=40
Y座標=68
速度=0
転回頻度=0
[車両No. 1]
車両色=10
方向=2
X座標=40
Y座標=108
速度=1
転回頻度=1
[車両No. 2]
車両色=12
方向=2
X座標=40
Y座標=148
速度=2
転回頻度=2
[車両No. 3]
車両色=14
方向=2
X座標=40
Y座標=188
速度=0
転回頻度=3

 

【Test04.lcd】

[車両]
台数=4
[車両No. 0]
車両色=9
方向=3
X座標=320
Y座標=68
速度=0
転回頻度=0
[車両No. 1]
車両色=10
方向=5
X座標=600
Y座標=228
速度=1
転回頻度=3
[車両No. 2]
車両色=12
方向=7
X座標=320
Y座標=388
速度=2
転回頻度=2
[車両No. 3]
車両色=14
方向=1
X座標=40
Y座標=228
速度=0
転回頻度=1

 

如何でしょうか?ストレートで方向転換しない方が生き延びる率が高そうです。(よけいなことに首を突っ込むとろくなことはない。まさにcuriosity killed the catです。)次に速度は必ずしも早い方が良いとは限りません。「太く短く」対「細く長く」の感があります。後は始点の置き方も重要です。最初の二つが四隅から右下左上ですすみますが、三番目はよーいどんと右へ、最後はひし形に配置してみました。

 

槽の熱帯魚を眺めるような感覚、自分の来し方行く末を観るような気持ちで楽しんでください。(自虐的ですが、少なくともいTuneで音楽を演奏していてPCがスリープに陥ることは避けられそうです。)

 

最後はLightCycleProc.hのウィンドウメッセージ以外のメンバー関数等です。

 

/////////////////////////////////
//主ウィンドウCMyWndの関数の定義
//メニュー項目、コントロール関数
/////////////////////////////////
bool CMyWnd::OnAdd() {

    if(inputdlg.DoModal(m_hWnd, "IDD_INPUT", inputdlgProc)) {
        if(m_NoLC)    //m_NoLCが0でなくなったらゲーム実行可能
            ChangeMenuStatus(TRUE, FALSE);
        return TRUE;
    }
    else {
        MessageBox(m_hWnd, "処理がキャンセルされました。", "エラー", MB_OK | MB_ICONERROR);
        return FALSE;
    }
}
//(解説:IDD_INPUTのダイアログを開き、「追加モード」で入力します。戻り値がTRUE且つLightCycleの台数が0でなくなればメニューとツールバーボタンの状態を無効→有効へ変更します。)


bool CMyWnd::OnOpen() {

    char* fn = cmndlg.GetFileName(m_hWnd, "LightCycleデータファイル(*.lcd)\0*.lcd\0\0", TRUE, "lcd", ".", "LightCycleデータを開く");
    if(fn) {
        g_File.SetName(fn);    //ファイルパス、名の登録
        m_NoLC = g_File.ReadInt("車両", "台数");
        char str[16];        //セクション名用バッファ
        int Color, StartDir, StartX, StartY, Speed, Caprice;    //各属性変数
        for(int i = 0; i < m_NoLC; i++) {
            wsprintf(str, "車両No. %d", i);
            Color = g_File.ReadInt(str, "車両色");
            StartDir = g_File.ReadInt(str, "方向");
            StartX = g_File.ReadInt(str, "X座標");
            StartY = g_File.ReadInt(str, "Y座標");
            Speed = g_File.ReadInt(str, "速度");
            Caprice = g_File.ReadInt(str, "転回頻度");
            //データをセットするのみならず、走行可能状態にする
            m_LC[i].SetData(Color, StartDir, StartX, StartY, Speed, Caprice);
        }
        //m_NoLCが0でなくなったら以下を実行する。
        if(m_NoLC)
            ChangeMenuStatus(TRUE, FALSE);
        return TRUE;
    }
    else {
        MessageBox(m_hWnd, "処理がキャンセルされました", "エラー", MB_OK | MB_ICONERROR);
        return FALSE;
    }
}
//(解説:今まで説明したことが無かったので、ここでファイル入出力で使われているCINIクラスを説明します。CINIクラスは所謂"+.ini"ファイルを扱うGetPrivateProfileString、WritePrivateProfileString、GetPrivateProfileInt、WritePrivateProfileInt関数を簡便に扱うクラスです。まずm_NoLCに入れる車両台数を読み、それを基に台数分のLightCycleの各データを文字列、整数で読み込みます。最後にメニューとツールバーボタンの状態変更を行い完了です。)

bool CMyWnd::OnSave() {

    char* fn = cmndlg.GetFileName(m_hWnd, "LightCycleデータファイル(*.lcd)\0*.lcd\0\0", FALSE, "lcd", ".", "LightCycleデータを保存");
    if(fn) {
        g_File.SetName(fn);    //ファイルパス、名の登録
        g_File.WriteInt("車両", "台数", m_NoLC);
        char str[16];        //セクション名用バッファ
        for(int i = 0; i < m_NoLC; i++) {
            wsprintf(str, "車両No. %d", i);
            g_File.WriteInt(str, "車両色", m_LC[i].m_Color);
            g_File.WriteInt(str, "方向", m_LC[i].m_StartDir);
            g_File.WriteInt(str, "X座標", m_LC[i].m_StartX);
            g_File.WriteInt(str, "Y座標", m_LC[i].m_StartY);
            g_File.WriteInt(str, "速度", m_LC[i].m_Speed);
            g_File.WriteInt(str, "転回頻度", m_LC[i].m_Caprice);
        }
        return TRUE;
    }
    else {
        MessageBox(m_hWnd, "処理がキャンセルされました", "エラー", MB_OK | MB_ICONERROR);
        return FALSE;
    }
}
//(解説:今度はその書き込み版です。これも極めて簡単に記述されます。)


bool CMyWnd::OnExit() {

    SendMsg(WM_CLOSE, 0, 0);
    return TRUE;
}
//(解説:定番の終了処理です。)


bool CMyWnd::OnGo() {

    //ゲーム終了後は画面をクリア
    if(!g_On) {
        m_cvs.Clear();
        //競技場枠の表示
        m_cvs.Color(7);
        m_cvs.Box(CLIGHTCYCLE::m_MinX, CLIGHTCYCLE::m_MinY, CLIGHTCYCLE::m_MaxX, CLIGHTCYCLE::m_MaxY, 0);
    }

//(解説:ゲームが進行中でなければ、初期表示を行います。)
     //競技車のデータが登録されてい ない場合の処置
    if(!m_NoLC) {
        MessageBox(m_hWnd, "競技車のデータが登録されていません\n「ファイル"\
                        "「新規」または「データを開く」で設定してください。",
                        "エラー", MB_OK | MB_ICONERROR);
        return FALSE;
    }
    else {
        //タイマーの設定(ミリ秒設定なので0.001秒タイマーとなっている)
        for(int i = 0; i < m_NoLC; i++) {
            if(!g_On)                    //ゲーム開始前なら
                m_LC[i].GetReady();        //出走用意
            if(!m_tm[i].Begin(m_hWnd, g_Timer[i], g_Velo[m_LC[i].m_Speed])) {
                MessageBox(m_hWnd, "タイマーを設定できませんでした", "エラー",
                    MB_OK | MB_ICONERROR);
                return FALSE;
            }
        }
        //ゲーム開始後は「実行」メニューアイテムとボタンを凍結する。
        g_On = TRUE;
        //「ファイル」メニューを不可にする
        EnableMenuItem(GetMenu(m_hWnd), 0, MF_BYPOSITION | MF_GRAYED);
        //「ゲーム」メニューのアイテムの状態を変更する
        ChangeMenuStatus(FALSE, TRUE);
        //ステータスバーによる結果表示の為
        SBar.SetText(2, "");
        return TRUE;
    }
}
//(解説:LightCycleの登録が無ければエラーで戻ります。登録されている場合、まず出走可能状態にし(GetReady関数)、それぞれの速度とタイマーIDで、タイマー割り込み処理(即ち走行処理)を開始します。ゲームが始まったならばその状態に合わせてメニューとツールバーボタンの状態を変更し、ステータスバーの初期表示を消します。)


bool CMyWnd::OnStop() {

    int ret = MessageBox(m_hWnd, "完全に中止しますか(No-一時停止)", "確認",
                        MB_YESNOCANCEL | MB_ICONQUESTION);
    switch(ret) {
    case IDYES:
        //全車死亡、タイマーを廃棄
        for(int i = 0; i < m_NoLC; i++)
            m_LC[i].Dead();
        g_On = FALSE;
    case IDNO:
        //タイマーを廃棄
        for(int i = 0; i < m_NoLC; i++) {
            m_tm[i].Stop();
        }
        //「ファイル」メニューを可にする
        EnableMenuItem(GetMenu(m_hWnd), 0, MF_BYPOSITION | MF_ENABLED);
        //「ゲーム」メニューのアイテムの状態を変更する
        ChangeMenuStatus(TRUE, FALSE);
        return TRUE;
    case IDCANCEL:
        return FALSE;
    }
}
//(解説:ゲーム中断処理です。これは最初「一時中止」か否かの二択でしたが、「完全中止(Yes)」「一時中止(No)」「続行(Cancel)」に変更しました。なお、完全中止の場合、全車Dead関数で死亡させます。)

bool CMyWnd::OnVersion() {

    versiondlg.DoModal(m_hWnd, "IDD_VERSION", versiondlgProc);
    return TRUE;
}
//(解説:定番のバージョンダイアログ表示です。)

//////////////////
//ユーザー定義関数
//////////////////
///////////////////////////////////////////
//「ゲーム」メニュー、ツールバー状態の変更
///////////////////////////////////////////
void CMyWnd::ChangeMenuStatus(bool Flag1, bool Flag2) {

    //メニューハンドルの取得
    HMENU hMenu = GetMenu(m_hWnd);
    //メニューアイテムを不可にする
    HMENU hGameMenu = GetSubMenu(hMenu, 1);
    EnableMenuItem(hGameMenu, IDM_GO, MF_BYCOMMAND | (Flag1 ? MF_ENABLED : MF_GRAYED));
    EnableMenuItem(hGameMenu, IDM_STOP, MF_BYCOMMAND | (Flag2 ? MF_ENABLED : MF_GRAYED));
    DrawMenuBar(LightCycle.GetHandle());    //Re-draw "&Game" menu

    //ツールバー(MAKELONGマクロは16bit整数2つをunsigned 32bit整数にする)
    SendMessage(TBar.GetHandle(), TB_ENABLEBUTTON, IDM_GO, MAKELONG(Flag1, 0));
    SendMessage(TBar.GetHandle(), TB_ENABLEBUTTON, IDM_STOP, MAKELONG(Flag2, 0));
}
//(解説:これはメニュー、ツールバーボタンの「ゲーム」「実行」と「中断」を自由に変更できるように、まとめてユーザー関数にしました。)

///////////////////////////////
//ユーザーダイアログの関数定義
//Inputダイアログ
///////////////////////////////
bool INPUTDLG::OnInit(WPARAM, LPARAM) {

    char str[16];    //コンボボックス登録用文字列変数
    //競技車番号
    for(int i = 0; i < LightCycle.m_NoLC; i++) {
        wsprintf(str, "%d", i + 1);
        SendItemMsg(IDC_COMBOBOX0, CB_ADDSTRING, 0, (LPARAM)str);
    }
    //競技車色
    for(int i = 0; i < 8; i++) {
        SendItemMsg(IDC_COMBOBOX1, CB_ADDSTRING, 0, (LPARAM)g_Col[i]);
    }
    //進行方向
    for(int i = 0; i < 8; i++) {
        SendItemMsg(IDC_COMBOBOX2, CB_ADDSTRING, 0, (LPARAM)g_Dir[i]);
    }
    //速度
    for(int i = 0; i < 3; i++) {
        SendItemMsg(IDC_COMBOBOX3, CB_ADDSTRING, 0, (LPARAM)g_Speed[i]);
    }
    //転回頻度
    for(int i = 0; i < 4; i++) {
        SendItemMsg(IDC_COMBOBOX4, CB_ADDSTRING, 0, (LPARAM)g_Caprice[i]);
    }

//(解説:これはIDD_INPUTダイアログのドロップダウンコントロールの初期化です。よく使う処理なので覚えておきましょう。)
    //登録車両数が0 - 3迄は次の未入力LC[i]を、4の場合先頭を指すは
    if(LightCycle.m_NoLC < 4)
        SendItemMsg(IDC_COMBOBOX0, CB_SETCURSEL, LightCycle.m_NoLC, 0);
    else {
        SendItemMsg(IDC_COMBOBOX0, CB_SETCURSEL, 0, 0);
        SetData(0);
    }
//(解説:競技車数が4台でいっぱいでなければ、車両番号はブランク状態で「追加モード」となります。(ウィンドウタイトルが「LightCycleデータの追加」になります。)追加の余地がなければ「1号車」を選択し、そのデータをこの大ログのユーザー定義関数SetDataで表示し、「編集モード」となります。(ウィンドウタイトルが「LightCycleデータの編集」になります。))

    //対象コントロールのハンドルを取得する
    HWND hCtrl = GetDlgItem(m_hWnd, IDC_IMAGE);
    //文字列リソースのある親ウィンドウ(ダイアログ)のインスタンス
    m_hInstance;
    //ツールチップの作成
    m_hTip = CreateWindowEx(NULL, TOOLTIPS_CLASS, NULL,
                WS_POPUP | TTS_ALWAYSTIP | TTS_BALLOON,
                CW_USEDEFAULT, CW_USEDEFAULT,
                CW_USEDEFAULT, CW_USEDEFAULT,
                m_hWnd, NULL, 
                m_hInstance, NULL);
    //IDC_IMAGEにツールヒントコントロールを付ける
    TOOLINFO toolInfo = { 0 };    //ゼロクリアー
    toolInfo.cbSize = sizeof(toolInfo);
    toolInfo.hwnd = m_hWnd;
    toolInfo.uFlags = TTF_IDISHWND | TTF_SUBCLASS;
    toolInfo.uId = (UINT_PTR)GetDlgItem(m_hWnd, IDC_IMAGE);
    toolInfo.hinst = m_hInstance;
    toolInfo.lpszText = "編集状態でも、登録が4台未満ならクリックすると「追加」になります";
    SendMessage(m_hTip, TTM_ADDTOOL, 0, (LPARAM)&toolInfo);
    SetWindowPos(m_hTip, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
//(解説:これは後から追加したのですが、追加できる状態でダイアログを「追加モード」からドロップダウンコントロールを使って「編集モード」にすると、「やっぱり追加しよう」と思っても「追加モード」に戻れません。この為、ダイアログのLightCycleのスタティックコントロールをクリックすると戻れるようにし、ツールチップヘルプで説明を出すようにしています。)

    return TRUE;
}

bool INPUTDLG::OnSelChange(WPARAM wParam) {

    //コンボボックスの選択変更以外は何もしない
    if(HIWORD(wParam) != CBN_SELCHANGE)
        return FALSE;
    //新しい選択アイテムインデックスを取得する
    int i = SendItemMsg(IDC_COMBOBOX0, CB_GETCURSEL, 0, 0);
    SetData(i);
    return TRUE;
}
//(編集:車両番号のドロップダウンリストから選択変更の通知が来たならば、SetData関数を使ってその車両のデータを各コントロールにセットします。)
 

bool INPUTDLG::OnImage() {

    if(LightCycle.m_NoLC < 4) {
        SendMsg(WM_SETTEXT, 0, (LPARAM)"LightCycleデータの追加");
        SendItemMsg(IDC_COMBOBOX0, CB_SETCURSEL, (WPARAM)-1, 0);
        SendItemMsg(IDC_COMBOBOX1, CB_SETCURSEL, (WPARAM)-1, 0);
        SendItemMsg(IDC_COMBOBOX2, CB_SETCURSEL, (WPARAM)-1, 0);
        SendItemMsg(IDC_COMBOBOX3, CB_SETCURSEL, (WPARAM)-1, 0);
        SendItemMsg(IDC_COMBOBOX4, CB_SETCURSEL, (WPARAM)-1, 0);
        SendItemMsg(IDC_EDIT2, WM_SETTEXT, 0, (LPARAM)"");
        SendItemMsg(IDC_EDIT3, WM_SETTEXT, 0, (LPARAM)"");
    }
}
//(編集:LightCycleイメージを貼ったスタティックコントロールがクリックされると、登録車両が一杯(4台)でない限り、追加モードに復帰します。)
 

bool INPUTDLG::OnOk() {

    //選択車両番号インデックス(0-3)を取得する
    int i = SendItemMsg(IDC_COMBOBOX0, CB_GETCURSEL, 0, 0);
    if(i == CB_ERR)    //選択が無ければ追加登録
        i = LightCycle.m_NoLC;
    //車両色(0-7)の取得
    int j = SendItemMsg(IDC_COMBOBOX1, CB_GETCURSEL, 0, 0);
    if(j == CB_ERR) {
        MessageBox(m_hWnd, "色が選択されていません。", "エラー", MB_OK | MB_ICONERROR);
        return FALSE;    //ダイアログを開いたまま処理を無効とする
    }
    LightCycle.m_LC[i].m_Color = j + 8;    //CANVASクラスの色コード(8-15)
    //車両の方向(0-7)
    j = SendItemMsg(IDC_COMBOBOX2, CB_GETCURSEL, 0, 0);
    if(j == CB_ERR) {
        MessageBox(m_hWnd, "方向が選択されていません。", "エラー", MB_OK | MB_ICONERROR);
        return FALSE;    //ダイアログを開いたまま処理を無効とする
    }
    LightCycle.m_LC[i].m_StartDir = j;
    //車両のX座標
    char str[16];        //エディットボックス用文字列変数
    SendItemMsg(IDC_EDIT2, WM_GETTEXT, 16, (LPARAM)str);
    j = atoi(str);        //エラー処理をどうするか?
    if(j <= 0 || j > CLIGHTCYCLE::m_MaxX) {
        MessageBox(m_hWnd, "Xの値が不正です。", "エラー", MB_OK | MB_ICONERROR);
        return FALSE;    //ダイアログを開いたまま処理を無効とする
    }
    LightCycle.m_LC[i].m_StartX = j;
    //車両のY座標
    SendItemMsg(IDC_EDIT3, WM_GETTEXT, 16, (LPARAM)str);
    j = atoi(str);        //エラー処理をどうするか?
    if(j <= 0 || j > CLIGHTCYCLE::m_MaxY) {
        MessageBox(m_hWnd, "Xの値が不正です。", "エラー", MB_OK | MB_ICONERROR);
        return FALSE;    //ダイアログを開いたまま処理を無効とする
    }
    LightCycle.m_LC[i].m_StartY = j;
    //車両の速度(0-2)
    j = SendItemMsg(IDC_COMBOBOX3, CB_GETCURSEL, 0, 0);
    if(j == CB_ERR) {
        MessageBox(m_hWnd, "速度が選択されていません。", "エラー", MB_OK | MB_ICONERROR);
        return FALSE;    //ダイアログを開いたまま処理を無効とする
    }
    LightCycle.m_LC[i].m_Speed = j;
    //車両の転向頻度(0-3)
    j = SendItemMsg(IDC_COMBOBOX4, CB_GETCURSEL, 0, 0);
    if(j == CB_ERR) {
        MessageBox(m_hWnd, "転向頻度が選択されていません。", "エラー", MB_OK | MB_ICONERROR);
        return FALSE;    //ダイアログを開いたまま処理を無効とする
    }
    LightCycle.m_LC[i].m_Caprice = j;
    //走行可能状態にする
    LightCycle.m_LC[i].GetReady();
    //追加登録の場合、車両台数を増やす
    if(i == LightCycle.m_NoLC)
        LightCycle.m_NoLC++;
    EndModal(TRUE);
    return TRUE;
}
//(解説:OKボタンを押すと、車両番号が選択されていればそのデータに、なければ登録済車両の次にダイアログのデータを代入します。内容はコメント通りです。この処理で検討されているのがx、y座標のエラー処理です。まずatoi()の関数でエラーが出る場合が考えられます。この場合、atoiは0を返すので"0"が入力された場合は問題となりますが、座標はマイナスの可能性が無いので「0以下はエラー」にしています。次にx、y共に競技場以外の座標を入れられると問題です。この為、一応それもチェックしています。なお、エラー時に競技場座標を標示してエラー表示を分けることも考えましたが、ステータスバーに表示されているのでやりすぎと考えこのようにしています。)


bool INPUTDLG::OnCancel() {

    EndModal(FALSE);
    return TRUE;
}
//(解説:キャンセルボタンが押された場合の処理です。)
 

void INPUTDLG::SetData(int i) {

    char str[16];    //エディットボックス用文字列変数
    //既存データが選択されたので編集であることを示す
    SendMsg(WM_SETTEXT, 0, (LPARAM)"LightCycleデータの編集");
    //CANVASクラスの色コード8-15を使用している為
    SendItemMsg(IDC_COMBOBOX1, CB_SETCURSEL, (LightCycle.m_LC[i].m_Color - 8), 0);
    SendItemMsg(IDC_COMBOBOX2, CB_SETCURSEL, LightCycle.m_LC[i].m_StartDir, 0);
    wsprintf(str, "%d", LightCycle.m_LC[i].m_StartX);
    SendItemMsg(IDC_EDIT2, WM_SETTEXT, 0, (LPARAM)str);
    wsprintf(str, "%d", LightCycle.m_LC[i].m_StartY);
    SendItemMsg(IDC_EDIT3, WM_SETTEXT, 0, (LPARAM)str);
    SendItemMsg(IDC_COMBOBOX3, CB_SETCURSEL, (WPARAM)LightCycle.m_LC[i].m_Speed, 0);
    SendItemMsg(IDC_COMBOBOX4, CB_SETCURSEL, LightCycle.m_LC[i].m_Caprice, 0);
}
//(解説:うぃんづおタイトルを「編集モード」にして、ダイアログのコントロールにデータをセットします。)

///////////////////////////////
//ユーザーダイアログの関数定義
//Versionダイアログ
///////////////////////////////
bool VERSIONDLG::OnOk() {

    EndModal(TRUE);
    return TRUE;
}

bool VERSIONDLG::OnCancel() {

    EndModal(FALSE);
    return TRUE;
}

 

これでコーディングは終わりです。後は実際に色々な属性の競技車を走らせてみましょう、と書いたところで思い出しました。本プログラムはCLIGHTCYCLEの

CandGo関数と

    if(m_Caprice && !(rand() % ((1 << m_Caprice) * 50)))
ChangeDir関数

        m_Dir = result[rand() % count];    //進行可能方向候補からランダムに選ぶ
で、LightCycleが横行転換する際に乱数を使っています。ということは、乱数の初期化(srand(time(NULL));文)を入れなければならないのですが、それをどこに置くべき(CLIGHTCYCLEの初期化か、CMyWndのOnInit()か)か悩んでいたのでした。一回呼べばよいのでOnInit()かなとも思いましたが、結論としてインスタンスの数だけsrand関数を呼ぶことになりますが、矢張りカプシュールの観点から各LightCycleのコンストラクターの中に置くことにしました。

///////////////////
//コンストラクター
///////////////////
CLIGHTCYCLE::CLIGHTCYCLE() {

    srand(time(NULL));        //乱数の初期化
    Init();
}
 

ということで、次回に走行結果を報告して全ソースを掲載します。

終盤にかかってきました。最後にLightCycleProc.hの解説です。いつもながらフィルが長めなので、まずウィンドウメッセージ処理に関わるメンバー関数について解説します。

 

//////////////////////////////////////////
// LightCycleProc.h
// Copyright (c) 10/30/2021 by BCCSkelton
//////////////////////////////////////////

/////////////////////////////////
//主ウィンドウCMyWndの関数の定義
//ウィンドウメッセージ関数
/////////////////////////////////
bool CMyWnd::OnCreate(WPARAM wParam, LPARAM lParam) {

    //コモンコントロールの初期化
    InitCommonControls();

    //ツールバー登録-Init(hWnd, hIinstance, ID, (以下省略可)Style)
    TBar.Init(m_hWnd, m_hInstance, TOOLBAR);
    //ツールバーボタン用カスタムビットマップ追加
    TBar.AddBmp(m_hInstance, MAKEINTRESOURCE(IDI_BITMAP), 7);
    //ツールバーボタン追加
    TBBUTTON tbb[10];
    ZeroMemory(tbb, sizeof(tbb));
    tbb[0].iBitmap = TBar.m_id + 0;
    tbb[0].fsState = TBSTATE_ENABLED;
    tbb[0].fsStyle = TBSTYLE_BUTTON;
    tbb[0].idCommand = IDM_ADD;
    tbb[1].iBitmap = TBar.m_id + 1;
    tbb[1].fsState = TBSTATE_ENABLED;
    tbb[1].fsStyle = TBSTYLE_BUTTON;
    tbb[1].idCommand = IDM_OPEN;
    tbb[2].iBitmap = TBar.m_id + 2;
    tbb[2].fsState = TBSTATE_ENABLED;
    tbb[2].fsStyle = TBSTYLE_BUTTON;
    tbb[2].idCommand = IDM_SAVE;
    tbb[3].fsStyle = TBSTYLE_SEP;    //セパレーター
    tbb[4].iBitmap = TBar.m_id + 3;
    tbb[4].fsState = TBSTATE_ENABLED;
    tbb[4].fsStyle = TBSTYLE_BUTTON;
    tbb[4].idCommand = IDM_EXIT;
    tbb[5].fsStyle = TBSTYLE_SEP;    //セパレーター
    tbb[6].iBitmap = TBar.m_id + 4;
    tbb[6].fsStyle = TBSTYLE_BUTTON;
    tbb[6].idCommand = IDM_GO;
    tbb[7].iBitmap = TBar.m_id + 5;
    tbb[7].fsStyle = TBSTYLE_BUTTON;
    tbb[7].idCommand = IDM_STOP;
    tbb[8].fsStyle = TBSTYLE_SEP;    //セパレーター
    tbb[9].iBitmap = TBar.m_id + 6;
    tbb[9].fsState = TBSTATE_ENABLED;
    tbb[9].fsStyle = TBSTYLE_BUTTON;
    tbb[9].idCommand = IDM_VERSION;
    TBar.AddButtons(10, tbb);

    //ステータスバー登録-Init(hWnd, hIinstance, ID, (以下省略可)Style)
    SBar.Init(m_hWnd, m_hInstance, STATUSBAR);
    //ステータスバー区画設定
    int sec[3] = {100, 300, -1};
    SBar.SetSection(3, sec);
    //ステータスバー文字列設定
    SBar.SetText(0, "LightCycle Ver 1.0");
    SBar.SetText(2, "マウス左クリックでケーム実行、右クリックでゲーム中止");
//(解説:ステータスバーは(1) プログラム名、(2) 競技場座標、(3) 初期値がマウスの使用方法で、ゲームが始まった後は結果報告、にしています。)


    //仮想ウィンドウの初期化
    m_cvs.SetCanvas(m_hWnd);
    //背景を黒の塗り潰しブラシで描画
    m_cvs.Color(0);
    m_cvs.BrSelection(1);
    m_cvs.Clear();
//(解説:今回はCANVASクラスの仮想ウィンドウを使っています。コメントに動作が記述されています。)


    //LightCycleのデバイスコンテキストの設定
    //m_LC[0].SetDC(m_cvs.hDC());でもよい
    CLIGHTCYCLE::m_hDC = m_cvs.hDC();
//(解説:CLIGHTCYCLEクラスの判定に使うGetPixel関数に必要なので、CMyWndのメンバーであるm_cvs(キャンバスクラスインスタンスメンバー変数)のHDCを渡しています。これですべてのLightCycleが表示されるHDCが決定されました。)


    //「ゲームメニュー」とステータスバーを不可にする
    ChangeMenuStatus(FALSE, FALSE);  
//(解説:これは「ゲーム」メニューの「実行」と「中止」をトグル切り替えするユーザー関数です。)


    //LightCycle登録数の初期化
    m_NoLC = 0;
//(解説:LightCycleの登録台数の初期化です。)
 

    return TRUE;
}

bool CMyWnd::OnTimer(WPARAM wParam, LPARAM lParam) {
//(解説:今回はLightCycleの走行にタイマーを使いましたが、Windowsのタイマーメッセージ(WM_TIMER)が呼び出された場合の処理関数です。)


    int no;
    switch(wParam) {
    case IDT_TIMER0:
        no = 0;
        break;
    case IDT_TIMER1:
        no = 1;
        break;
    case IDT_TIMER2:
        no = 2;
        break;
    case IDT_TIMER3:
        no = 3;
        break;
    default:
        return FALSE;
    }
//(解説:4台までのLightCycleはそれぞれのタイマーIDで呼び出されます。ここでは呼び出されたタイマーIDで、CMyWndのメンバーであるLightCycle(m_LC[no])が特定されるようにnoを設定します。)

    if(m_LC[no].CanGo()) {
        m_cvs.Color(m_LC[no].m_Color);            //競技車両色で
        m_cvs.Dot(m_LC[no].m_x, m_LC[no].m_y);    //走行痕ドットを打つ
    }
    else {
        //タイマー割り込みを停止
        m_tm[no].Stop();
        //ステータスバー第3区分の文字列取得
        char str[MAX_PATH];
        SBar.GetText(2, str);
        CSTR Msg1 = str;
         CSTR Msg2 = g_Col[m_LC[no].m_Color - 8];
        Msg2 = Msg2 + "号";
        Msg2 = Msg2 + m_LC[no].Dead();
        Msg1 = Msg1 + Msg2;
        SBar.SetText(2, Msg1.ToChar());
    }

//(解説:ここがCanGo関数を使った走行プロセスです。(方向転換しても)走れる場合は軌跡を残し、走行不能となった場合にはステータスバーメッセージに次々に死亡通知を追加してゆきます。)
    //ゲーム終了処理
    if(!m_LC[0].m_Alive && !m_LC[1].m_Alive &&
        !m_LC[2].m_Alive && !m_LC[3].m_Alive) {
        MessageBox(m_hWnd, "全車両停止しました", "競技終了",
                    MB_OK | MB_ICONEXCLAMATION);
        g_On = FALSE;
        ChangeMenuStatus(TRUE, FALSE);
    }

//(解説:4台とも死亡した場合、ゲーム終了(g_On = FALSE)となり、メニュー八―ルバーボタンを元に戻します。)
    return TRUE;
}

bool CMyWnd::OnLButtonDown(WPARAM wParam, LPARAM lParam) {

    //メニューアイテムインフォ構造体
    MENUITEMINFO mii;
    //メニューハンドルの取得
    HMENU hMenu = GetMenu(m_hWnd);
    HMENU hGameMenu = GetSubMenu(hMenu, 1);
    mii.cbSize = sizeof(mii);
    mii.fMask = MIIM_STATE;
    GetMenuItemInfo(hGameMenu, IDM_GO, FALSE, &mii);
    if(mii.fState & MFS_DISABLED) {
        MessageBox(m_hWnd, "現在左クリックは使用できません", "注意",
                    MB_OK | MB_ICONWARNING);
        return FALSE;
    }
    else {
        OnGo();
        return TRUE;
    }
}
//(解説:ウィンドウ上でのマウス左クリックは「ゲーム」「実行」と連結しますが、メニュー、ツールバーボタンの状態と連動します。)
 

bool CMyWnd::OnRButtonDown(WPARAM wParam, LPARAM lParam) {

    //メニューアイテムインフォ構造体
    MENUITEMINFO mii;
    //メニューハンドルの取得
    HMENU hMenu = GetMenu(m_hWnd);
    HMENU hGameMenu = GetSubMenu(hMenu, 1);
    mii.cbSize = sizeof(mii);
    mii.fMask = MIIM_STATE;
    GetMenuItemInfo(hGameMenu, IDM_STOP, FALSE, &mii);
    if(mii.fState & MFS_DISABLED) {
        MessageBox(m_hWnd, "現在右クリックは使用できません", "注意",
                    MB_OK | MB_ICONWARNING);
        return FALSE;
    }
    else {
        OnStop();
        return TRUE;
    }
}
//(解説:これはマウス右クリックの処理です。)

bool CMyWnd::OnNotify(WPARAM wParam, LPARAM lParam) {

    //ツールバーツールチップ-因みにこの時のidCtrl(wParam)はメニューアイテムになる(例:IDM_OPEN)
    if(((LPNMHDR)lParam)->code == TTN_NEEDTEXT) {    //TTN_GETDISPINFOと同じ
        ((LPTOOLTIPTEXT)lParam)->hinst = m_hInstance;
        char* tag;
        switch(((LPTOOLTIPTEXT)lParam)->hdr.idFrom) {
        case IDM_ADD:
            tag = "追加・編集";
            break;
        case IDM_OPEN:
            tag = "データを開く";
            break;
        case IDM_SAVE:
            tag = "データの保存";
            break;
        case IDM_EXIT:
            tag = "終了";
            break;
        case IDM_GO:
            tag = "実行";
            break;
        case IDM_STOP:
            tag = "中止";
            break;
            break;
        case IDM_VERSION:
            tag = "バージョン情報";
            break;
        }
        ((LPTOOLTIPTEXT)lParam)->lpszText = tag;
        return TRUE;
    }
    return FALSE;
}
//(解説:今回のWM_NOTIFYは定番のツールバーボタンツールチップ処理だけです。)

bool CMyWnd::OnSize(WPARAM wParam, LPARAM lParam) {

    TBar.AutoSize();
    SBar.AutoSize();
//(解説:SkeltonWizard通りのツールバー、ステータスバーの調整です。)

    //ゲーム開始迄にゲーム領域を確定する
    if(!g_On) {
        CLIGHTCYCLE::m_MinX = 0;
        CLIGHTCYCLE::m_MaxX = LOWORD(lParam);
        RECT rec;
        GetWindowRect(TBar.m_hWnd, &rec);
        CLIGHTCYCLE::m_MinY = rec.bottom - rec.top;
        CLIGHTCYCLE::m_MaxY = HIWORD(lParam);    //仮置き
        GetWindowRect(SBar.m_hWnd, &rec);
        CLIGHTCYCLE::m_MaxY -= (rec.bottom - rec.top);
        //競技場枠の表示
        m_cvs.Clear();
        m_cvs.Color(7);
        m_cvs.Box(CLIGHTCYCLE::m_MinX, CLIGHTCYCLE::m_MinY, CLIGHTCYCLE::m_MaxX, CLIGHTCYCLE::m_MaxY, 0);
        //競技場座標の表示
        char str[64];
        wsprintf(str, "競技場 (%d,%d)-(%d,%d)", CLIGHTCYCLE::m_MinX, CLIGHTCYCLE::m_MinY, CLIGHTCYCLE::m_MaxX, CLIGHTCYCLE::m_MaxY);
        SBar.SetText(1, str);
    }

//(解説:ゲームが開始される前であれば、ウィンドウサイズが変更される度に競技場の大きさが変更されます。競技場はライトグレイの枠線が描れ、ステータスバーに座標が表示されます。)
    return TRUE;
}

bool CMyWnd::OnPaint(WPARAM wParam, LPARAM lParam) {

    PAINTSTRUCT paint;
    m_cvs.OnPaint(BeginPaint(m_hWnd, &paint));
    EndPaint(m_hWnd, &paint);
    return TRUE;
}
//(解説:お決まりのCANVASクラスインスタンスがつた場合のWM_PAINT処理です。)


bool CMyWnd::OnClose(WPARAM wParam, LPARAM lParam) {

    if(MessageBox(m_hWnd, "終了しますか", "終了確認",
                    MB_YESNO | MB_ICONINFORMATION) == IDYES) {
        //タイマーを廃棄
        for(int i = 0; i < m_NoLC; i++)
            m_tm[i].Stop();
//(解説:終了時には全タイマーを廃棄します。)

        //処理をするとDestroyWindow、PostQuitMessageが呼ばれる
        return TRUE;
    }
    else
        //そうでなければウィンドウではDefWindowProc関数をreturn、ダイアログではreturn FALSEとなる。
        return FALSE;
}

bool CMyWnd::OnMinMax(WPARAM wParam, LPARAM lParam) {

    //典型的なウィンドウのサイズ制限処理
    MINMAXINFO *pmmi;
    pmmi = (MINMAXINFO*)lParam;
    pmmi->ptMinTrackSize.x = 656;    // 最小幅(競技エリア640)
    pmmi->ptMinTrackSize.y = 509;    // 最小高(競技エリア400)

    return FALSE;                    //処理はDefWndProcに任す
}

//(解説:これは以前EZImageの解説でも紹介しましたが、ウィンドウをある大きさに制限する場合、

//LightCycle.hファイルのマクロテーブルに、

//    ON_(LightCycle, WM_GETMINMAXINFO, OnMinMax(wParam, lParam))
//を追加し、メンバー関数定義に、

//    bool OnMinMax(WPARAM, LPARAM);

//を追加します。

//LightCycleではMZ-2500のプログラムのオマージュから、競技場最小値を640x400に設定しています。

 

次回はメニュー、ユーザー定義関数、ダイアログメンバー関数について説明します。

 

前回は定義部分のみを説明しましたので、今回はCCLITHGTCYCLEクラスのメンバー関数を解説します。内容的に難しいところはありませんので、またコメント+(解説:)で説明します。

 

///////////////////
//コンストラクター
///////////////////
CLIGHTCYCLE::CLIGHTCYCLE() {

    Init();
}
//(解説:これがコンストラクターで、クラスのインスタンスが作られる際に実行されます。この例ではInit()という関数を実行します。)


///////////////////////////////
//デバイスコンテキストの初期化
//全車共通で一回だけ実行する
///////////////////////////////
void CLIGHTCYCLE::SetDC(HDC hDC) {    

    m_hDC = hDC;
}
//(解説:これはLightCycle走らせる場(表示デバイス)へのアクセス(コンテキスト)を設定します。本プログラムの場合にはCANVASクラスのm_cvsという仮想ウィンドウになります。)

/////////////////
//座標限界の設定
/////////////////
void CLIGHTCYCLE::SetLimit(int minx, int miny, int maxx, int maxy) {

    m_MinX = minx;        //x座標最小値
    m_MinY = miny;        //y座標最小値
    m_MaxX = maxx;        //x座標最大値
    m_MaxY = maxy;        //y座標最大値
}
//(解説:これは表示デバイスの座標上の表示限界を設定する関数です。CLIGHTCYCLEのメンバー変数に値を入れるだけです。)

/////////////////////
//競技車データ初期化
/////////////////////
void CLIGHTCYCLE::Init() {

    //個車属性情報
    m_Color = 0;
    m_StartDir = 0;
    m_StartX = 0;
    m_StartY = 0;
    m_Speed = 0;
    m_Caprice = 0;
    m_Alive = FALSE;
}
//(解説:これはCLIGHTCYCLEクラスに基づいて作られる競技車(インスタンス)に関わる所別の競技車両データです。順位色、開始時進行方向、開始時位置(x, y)、速度(0-2)、方向転換頻度(0-3)、生死フラグ(bool)になります。)


/////////////////////////
//競技車スタート位置設定
/////////////////////////
void CLIGHTCYCLE::GetReady() {

    //内部管理変数の設定
    m_Times = 0;
    m_Alive = TRUE;
    m_Dir = m_StartDir;
    m_x = m_StartX;
    m_y = m_StartY;
}
//(解説:これは競技が始まる際にワーク変数を初期化する関数です。協議が終了し、再度実行する際にも呼ぶ必要があります。)

///////////////////////
//競走車の初期化
//各車の属性を設定する
///////////////////////
void CLIGHTCYCLE::SetData(int col, int dir, int x, int y, int sp = 0, int cap = 0) {

    m_Color = col;
    m_StartDir = dir;
    m_StartX = x;
    m_StartY = y;
    m_Speed = sp;
    m_Caprice = cap;
}
//(解説:これは主にファイルや入力ダイアログからデータを読む場合を想定して作った関数ですが、実際には直接メンバー変数に代入することが多かったです。)

/////////////////////////////////////////
//現在位置(m_x、m_y)と引数の方向(dir)
//から次の位置(m_nx、m_nyに代入される)
//を求め、競技場内か否か、未走破(黒)か
//否か、斜線跨ぎが無いかで進める(TRUE)
//か否(FALSE)かを返す
/////////////////////////////////////////
bool CLIGHTCYCLE::CheckNext(int dir) {

    bool OK = TRUE;        //戻り値用ブール変数
    //現在方向から次の位置をm_nx、m_nyに代入
    switch(dir) {
    case 0:
        m_nx = m_x;
        m_ny = m_y - 1;
        if(m_ny <= m_MinY)    OK = FALSE;
        break;
    case 1:
        m_nx = m_x + 1;
        if(m_nx >= m_MaxX)    OK = FALSE;
        m_ny = m_y - 1;
        if(m_ny <= m_MinY)    OK = FALSE;
        break;
    case 2:
        m_nx = m_x + 1;
        if(m_nx >= m_MaxX)    OK = FALSE;
        m_ny = m_y;
        break;
    case 3:
        m_nx = m_x + 1;
        if(m_nx >= m_MaxX)    OK = FALSE;
        m_ny = m_y + 1;
        if(m_ny >= m_MaxY)    OK = FALSE;
        break;
    case 4:
        m_nx = m_x;
        m_ny = m_y + 1;
        if(m_ny >= m_MaxY)    OK = FALSE;
        break;
    case 5:
        m_nx = m_x - 1;
        if(m_nx <= m_MinX)    OK = FALSE;
        m_ny = m_y + 1;
        if(m_ny >= m_MaxY)    OK = FALSE;
        break;
    case 6:
        m_nx = m_x - 1;
        if(m_nx <= m_MinX)    OK = FALSE;
        m_ny = m_y;
        break;
    case 7:
        m_nx = m_x - 1;
        if(m_nx <= m_MinX)    OK = FALSE;
        m_ny = m_y - 1;
        if(m_ny <= m_MinY)    OK = FALSE;
        break;
    }    
    //次の位置が黒(COLORREF 0)以外であれば走行済
    if(GetPixel(m_hDC, m_nx, m_ny))
        OK = FALSE;
    else {        //黒であっても
        if(dir % 2)    {    //進行方向斜めで
            COLORREF c = GetPixel(m_hDC, m_x, m_ny);
            //m_(x, y)とm_n(x, y)の線が他の線と交差ていなら不可る
            if(c && c == GetPixel(m_hDC, m_nx, m_y))
                OK = FALSE;
        }
    }
    return OK;
}
//(解説:この関数がCCLIGHTCYCLEクラスの重要な関数となります。引数の方向(dir)と位置(m_x, m_y)からswitch文で、次に進む予定の位置(m_nx, m_ny)を求め、次に次の位置が移動限界内か否かを判断します。switch文を抜けると、まず次の位置のピクセルの色を調べ、既に走行されているか否かを判別し、仮に走行軌跡がなくとも走行軌跡を跨いでいるか否かも判別します。結果は進める場合gaTRUE、不可の場合がFALSEになります。)

////////////////////////////////////////
//可能なら進みTRUE、不可なら進まずFALSE
////////////////////////////////////////
bool CLIGHTCYCLE::CanGo() {

    //走行不能の場合は何もしない
    if(!m_Alive)
        return FALSE;
    //方向転換(不能の場合、m_Dirは変更されない)
    if(m_Caprice && !(rand() % ((1 << m_Caprice) * 50)))
        ChangeDir();
    //現在位置に基づいて次の位置を取得
    if(CheckNext(m_Dir)) {            //走行継続可能
        //現在位置に次の位置を代入
        m_x = m_nx;
        m_y = m_ny;
        //競走車の動作回数を増やす
        m_Times++;
        return TRUE;
    }
    else {
        if(ChangeDir()) {            //方向転換で走行継続可能
            //現在位置に次の位置を代入
            m_x = m_nx;
            m_y = m_ny;
            //競走車の動作回数を増やす
            m_Times++;
            return TRUE;
        }
        else {                        //走行継続不能
            m_Alive = FALSE;
            return FALSE;
        }
    }
}
//(解説:この関数はCheckNext関数の親分のようなもので、まず①自分が生きているか否か調べ(死んでいるなら何もしないで帰る)、②次にCheckNextで進めるか否かを判断し、不可の場合↓のChangeDir関数で方向転換可能な方向を確認、可能ならそちらへ進みます。進む場合には「次の位置(m_nx, m_ny)」を「現在位置(m_x, m_y)」に代入し、移動距離(m_Times)を増やします。逆に進行方向へ進めず、方向転換も不可になった(身動きが取れなくなった)場合には生死フラグをFALSE(死)にして「もう無理(FALSE)」を返します。)

///////////////////////////////////////////
//現在の方向以外で進める方向があれ方向転換
// 7 0 1    現在の方向から時計回りに2つ、
// 6 * 2    反時計回りに2つ方向を設定し、
// 5 4 3    CanGo()を実行する
//進めればTRUE、不可であればFALSEを返す
//FALSEの場合、進行ができないので終了する
///////////////////////////////////////////
bool CLIGHTCYCLE::ChangeDir() {

    //方向転換候補の特定
    int newdir[4];                        //転換方向候補
    newdir[0] = (m_Dir + 6) % 8;        //反時計回り2つ目
    newdir[1] = (m_Dir + 7) % 8;        //反時計回り1つ目
    newdir[2] = (m_Dir + 1) % 8;        //時計回り1つ目
    newdir[3] = (m_Dir + 2) % 8;        //時計回り2つ目
    //4候補について進行可能か否か確認(不可は-1を代入)
    int result[4];                        //進行可能方向とその数
    int count = 0;
    for(int i = 0; i < 4; i++) {
        if(CheckNext(newdir[i])) {
            result[count] = newdir[i];
            count++;
        }
    }
    //結果処理-進行可能方向(複数)があれば(乱数で選択し)m_Dirを変更する
    if(count) {
        m_Dir = result[rand() % count];    //進行可能方向候補からランダムに選ぶ
        CheckNext(m_Dir);                //再度m_nxとm_nyを設定する為
        return TRUE;
    }
    else
        return FALSE;
}
(解説:ここでやっているのは↑のコメントのように4つの転舵方向を剰余演算子%を活用して求め、それぞれの方向を使ってCheckNext関数で調べます。方向転換可能な方向があれば結果配列にその数と共に記録し、進行可能方向が複数あれば乱数で選択するようにしています。)

/////////////////
//死亡処理と通知
/////////////////
char* CLIGHTCYCLE::Dead() {

    m_Alive = FALSE;
    wsprintf(m_msg, "は走行距離%dで死亡。", m_Times);
    return m_msg;
}

(解説:一応生死フラグはCanGo関数で立てますが、ここでも立ててますね。一番大事なのは、移動距離がいくつで死亡したかのメッセージへオンポインターを返すことです。(メッセージの完成はCMyWndでやっていますが、ClightCycleインスタンスも色情報を持っているので全文書けますね。またこの関数をCanGoのm_Alive = FALSE;文の代わりに呼んだ方がスマートですが、ポインターをm_msgにアクセスさせることで渡すよりもDead()関数の戻り値で渡す方がキレイに思えたので...)

 

今回はここまでです。次回はLightCycleProc.hをやりましょうか。

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クラス定義が出来上がったので、そのメンバー関数がどのように実装されているのかを次回でやりましょう。

 

前回のSkeltonWizardでつくったドンガラに、今回使おうと思っているクラスの試験を兼ねて少しいじってみます。

 

先ずはCMyWnd(メインSDIウィンドウ)クラス。いつもは外部変数にしているCANVASクラスのcvs変数などともに、タイマーの設定、破棄を行うCTIMERクラスの変数を入れて動作試験を行います。メンバー変数にする際には"m_"を付けるようにしています。(外部変数の際には"g_"にするとよい。)また、タイマー割り込みを行うにはタイマーIDが必要なのでResLightCycle。hに追加し(まぁ、LightCycle.hでもいいんですけど)、割り込みはWM_TIMERメッセージで王られてくるので、マニュアルでマクロテーブルとメンバー関数を追加します。

【ResLightCycle.h】

//タイマーのID(WM_TIMERにwParamで渡される)
#define IDT_TIMER        400

 

【LightCycle.h】

/////////////////////////////////////////////////////////////////////
//CMyWndクラスをCSDIクラスから派生させ、メッセージ用の関数を宣言する
/////////////////////////////////////////////////////////////////////
class CMyWnd : public CSDI
{
public:    //メンバー変数
    CANVAS m_cvs;            //仮想ウィンドウ
    CTIMER m_tm;            //タイマー
    int m_MinX;                //X座標最小値
    int m_MaxX;                //X座標最大値
    int m_MinY;                //Y座標最小値
    int m_MaxY;                //Y座標最大値

public:    //以下はコールバック関数マクロと関連している
    //2重起動防止用のMutex用ID名称
    CMyWnd(char* UName) : CSDI(UName) {}
    //メニュー項目、ダイアログコントロール関連
    bool OnNew();
    bool OnOpen();
    bool OnSave();
    bool OnExit();
    bool OnGo();
    bool OnStop();
    bool OnVersion();
    //ウィンドウメッセージ関連
    bool OnCreate(WPARAM, LPARAM);
    bool OnTimer(WPARAM, LPARAM);
    bool OnLButtonDown(WPARAM, LPARAM);
    bool OnRButtonDown(WPARAM, LPARAM);
    bool OnNotify(WPARAM, LPARAM);
    bool OnSize(WPARAM, LPARAM);
    bool OnPaint(WPARAM, LPARAM);
    bool OnClose(WPARAM, LPARAM);
};

 

BEGIN_SDIMSG(LightCycle)    //ダイアログと違い、コールバック関数名を特定しない
    //メニュー項目、ダイアログコントロール関連

  ・

  ・

    //ウィンドウメッセージ関連
  ・

  ・

    ON_TIMER(LightCycle)
  ・

  ・

  ・

 

これに合わせて関数のテストをします。

bool CMyWnd::OnTimer(WPARAM wParam, LPARAM lParam) {

    if(wParam != IDT_TIMER)
        return FALSE;
    else {
        //ダミーサンプル-Begin
        char str[MAX_PATH];
        SYSTEMTIME st;
        GetSystemTime(&st);
        wsprintf(str, "世界協定時(UTC)%2d年%d月%2d日の%2d時%2d分%2d秒です",
            st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond);
        m_cvs.PrintText(10, 50, str);
        GetLocalTime(&st);
        wsprintf(str, "日本時間%2d年%d月%2d日の%2d時%2d分%2d秒です",
            st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond);
        m_cvs.PrintText(10, 80, str);
        //ダミーサンプル-End
        return TRUE;
    }

}
 

加えてCANVASクラスの動作(主にLightCycleの選択色)確認で次のようなダミーを入れてみます。(どこでもよいのですが、「ゲーム」「実行」に置いてみました。)

    //ゲーム開始時にゲーム領域を確定する
    RECT rec;
    GetClientRect(m_hWnd, &rec);
    m_MinX = rec.left;
    m_MaxX = rec.right;
    m_MaxY = rec.bottom - rec.top;    //仮置き
    GetWindowRect(TBar.m_hWnd, &rec);
    m_MinY = rec.bottom - rec.top;
    GetWindowRect(SBar.m_hWnd, &rec);
    m_MaxY -= (rec.bottom - rec.top);

     //ダミーサンプル-Begin
    char str[MAX_PATH];
    wsprintf(str, "X軸下限 %2d X軸上限 %2d:Y軸下限 %2d Y軸上限 %2d",
            m_MinX, m_MaxX, m_MinY, m_MaxY);
    m_cvs.PrintText(10, 150, str);
    for(int i = 0; i < 8; i++) {
        m_cvs.Color(8 + i);
        m_cvs.Box(m_MinX + i, m_MinY + i, m_MaxX - i, m_MaxY - i, 0);
        m_cvs.Circle(400, 300, 40 + (8 - i) * 20, 0);
        m_cvs.Color(0);
    }
    //ダミーサンプル-End

 

単なる真っ黒なウィンドウがちょっと華やかになりますね。

これからはLightCycleのクラス定義になります。

 

実は現実世界(リアル、っていうのかな、最近は)では、本日LightCycle.exeがまぁまぁ思っていた感じの動作を示してくれましたが、ブログの世界ではまだ始まったばかりですので、回顧しながら書いてみます。

 

「大体でいいから」という大雑把な仕様を決めると、まずは試行錯誤の仕事場(Work space)を作るのがBCCForm and BCCSkelton流です。(なお、完成版まではハイライトで一部のコードを載せるだけなので、ざっと眺めていただければよいかな、と思います。)

 

【LightCycle.rc】

(1)アイコンをEZImageで、ツールバービットマップをTBEditorで作成し、

(2)メニュー、バージョンダイアログ(IDListの奴をパクった)、作ったアイコンでrcファイルを作り、

(3)SkeltonWizardでSDIベースのドンガラを作りました。

 

//-----------------------------------------
//             BCCForm Ver 2.41
//    An Easy Resource Editor for BCC
//  Copyright (c) February 2002 by ysama
//-----------------------------------------
#include    "ResLightCycle.h"

//----------------------------------
// ダイアログ (IDD_VERSION)
//----------------------------------
IDD_VERSION DIALOG DISCARDABLE 0, 0, 160, 40
EXSTYLE WS_EX_DLGMODALFRAME
STYLE WS_POPUP | WS_VISIBLE | WS_CAPTION | DS_MODALFRAME | DS_3DLOOK | DS_CENTER
CAPTION "バージョン情報"
FONT 9, "Times New Roman"
{
 CONTROL IDI_ICON, 0, "STATIC", SS_SUNKEN | SS_ICON | WS_CHILD | WS_VISIBLE, 12, 10, 32, 32
 CONTROL "LightCycle Version 1.0\nCopyright 2021b by Ysama", 0, "STATIC", SS_CENTER | SS_SUNKEN | WS_CHILD | WS_VISIBLE, 42, 8, 80, 24
 CONTROL "OK", IDOK, "BUTTON", BS_PUSHBUTTON | BS_CENTER | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 130, 14, 20, 12
}

//-------------------------
// メニュー(IDM_MAIN)
//-------------------------
IDM_MAIN MENU DISCARDABLE
{
    POPUP "ファイル(&F)"
    {
        MENUITEM "新規(&N)", IDM_NEW
        MENUITEM "データを開く(&O)", IDM_OPEN
        MENUITEM "データの保存(&S)", IDM_SAVE
        MENUITEM SEPARATOR
        MENUITEM "終了(&X)", IDM_EXIT
    }
    POPUP "ゲーム(&G)"
    {
        MENUITEM "実行(&G)", IDM_GO
        MENUITEM "中止(&S)", IDM_STOP
    }
    POPUP "ヘルプ(&H)"
    {
        MENUITEM "バージョン情報(&V)", IDM_VERSION
    }
}

//--------------------------
// イメージ(IDI_ICON)
//--------------------------
IDI_ICON    ICON    DISCARDABLE    "C:\Users\
(ファイルパス)\LightCycle\LightCycle.ico"

//--------------------------
// イメージ(IDI_BITMAP)
//--------------------------
IDI_BITMAP    BITMAP    DISCARDABLE    "C:\Users\
(ファイルパス)\LightCycle\ToolBarBmp.bmp"

 

【LightCycle.cpp】

LightCycle.cppはいつもの通り、「2重起動防止のコメント外し」しかしておらず、省略。

 

【LightCycle.h】

これもCMyWndクラスでメンバー関数として残すのは次の通り。

    //メニュー項目、ダイアログコントロール関連
    bool OnNew();
    bool OnOpen();
    bool OnSave();
    bool OnExit();
    bool OnGo();
    bool OnStop();
    bool OnVersion();
    //ウィンドウメッセージ関連
    bool OnCreate(WPARAM, LPARAM);
    bool OnLButtonDown(WPARAM, LPARAM);
    bool OnRButtonDown(WPARAM, LPARAM);
    bool OnNotify(WPARAM, LPARAM);
    bool OnSize(WPARAM, LPARAM);
    bool OnPaint(WPARAM, LPARAM);
    bool OnClose(WPARAM, LPARAM);

 

バージョンダイアログと外部変数もSkeltonWizard通りです。

///////////////////////////////////////////
// CDLGクラスからVERSIONDLGクラスを派生
// 複数の同一ダイアログ変数とダイアログも作
// れるが、一つのダイアログに一つの派生ダイ
// アログクラスを作成するのが基本
///////////////////////////////////////////
class VERSIONDLG : public CDLG {
public:
    //コントロール関連
    bool OnIdok();
};


////////////////////////////////////////////////////////////////////////////
// VERSIONDLGクラスダイアログ変数の生成とそのコールバック関数(マクロ)を定義
// 複数同一クラスのダイアログを作成することを予期してコールバック関数を明記
////////////////////////////////////////////////////////////////////////////
VERSIONDLG versiondlg;

BEGIN_MODALDLGMSG(versiondlgProc, versiondlg)    //第1引数がコールバック関数の名前
    ON_COMMAND(versiondlg, IDOK, OnIdok())
END_DLGMSG

///////////////////
//ツールバーの作成
///////////////////
CTBAR TBar;

///////////////////////
//ステータスバーの作成
///////////////////////
CSBAR SBar;

////////////////////////
//コモンダイアログの作成
////////////////////////
CMNDLG cmndlg;

///////////////////////
//仮想ウィンドウの作成
///////////////////////
CANVAS cvs;

 

今回はこのCANVASクラスが描画で活躍します。

 

【LightCycleProc.h】

手を入れたのは(というほど入れていないが)Proc.hで、まずステータスバーは、

    //ステータスバー文字列設定
    SBar.SetText(0, "LightCycle Ver 1.0");
    SBar.SetText(1, "マウス左クリックでケーム実行、右クリックでゲーム中止");

としました。

 

次に(ブログでは初めてと思いますが)、仮想ウィンドウについて初期化と初期画面の設定をしました。

    //仮想ウィンドウの初期化
    cvs.SetCanvas(m_hWnd);
    //背景を黒の塗り潰しブラシで描画
    cvs.Color(0);
    cvs.BrSelection(1);
    cvs.Clear();

 

また、今回はSDIウィンドウ上でマウスを左クリック、右クリックした場合のメンバー関数をいれて、左は「ゲームの実行」を、右は「ゲームの中止」を呼ぶようにします。

bool CMyWnd::OnLButtonDown(WPARAM wParam, LPARAM lParam) {

    OnGo();    
    return TRUE;
}

bool CMyWnd::OnRButtonDown(WPARAM wParam, LPARAM lParam) {

    OnStop();    
    return TRUE;
}

 

OnNotify(WM_NOTIFY)では定番のツールバーツールチップ処理(省略-例えばこれの"bool CMyWnd::OnNotify(WPARAM wParam, LPARAM lParam) "を見てください)を入れ、メニューアイテムのメンバー関数に動作確認のためにメッセージボックスを付けてみます。(以下は「ファイル」「新規」の例)

    MessageBox(m_hWnd, "新規", "ファイル", MB_YESNO | MB_ICONINFORMATION);
 

しっかり描いたのは”終了”と”バージョン”ですね。

 

bool CMyWnd::OnExit() {

    SendMsg(WM_CLOSE, 0, 0);
    return TRUE;
}

 

bool CMyWnd::OnVersion() {

    versiondlg.DoModal(m_hWnd, "IDD_VERSION", versiondlgProc);
    return TRUE;
}


バージョンダイアログの方にも終了処理を入れています。
///////////////////////////////
//ユーザーダイアログの関数定義
//コントロール関数
///////////////////////////////
bool VERSIONDLG::OnIdok() {

    EndModal(TRUE);
    return TRUE;
}


これで「ドンガラ」が完成します。

 

力仕事はここからです。