Skip to main content

第7回 画面遷移(状態遷移)

第7回 タイトル画面とクリア画面を作ろう

前回までのプログラムは、起動するとすぐにゲームが始まりました。

今回は、次の3つの画面を作ります。

タイトル画面
ゲーム画面
クリア画面

現在の画面に応じて、実行するUpdate()Draw()の処理を切り替えます。

この回は、第6回終了時点のmain.cppから作業を始めます。


このページの進め方

このページでは、読む部分とソースを書き換える部分を分けています。

説明
    仕組みを確認する

STEP ○ ここでソースを書き換える!
    main.cppを実際に変更する

動作確認
    ビルドして実行する

STEPを上から順番に進めてください。
詳しい仕組みは、各STEPの前後にある説明で確認できます。

ソースを書き換える場所は、コード内のここから追加ここを変更この部分を削除などのコメントで示します。


1.今回の完成状態

起動直後はタイトル画面を表示します。

Push Startをクリックすると、ゲーム画面へ移動します。

正解して4 HITになると、クリア画面へ移動します。

クリア画面のPush Startをクリックすると、問題と履歴を初期化して、新しいゲームを始めます。


2.現在の画面を変数で管理する

画面が増えると、どの画面を表示しているのかを保存する必要があります。

今回は、列挙型enumを使って3つの画面を区別します。

それぞれの値は、次の画面を表します。

まず、画面の状態を表すGameSceneを追加します。

// ここから追加
enum GameScene
{
    SCENE_TITLE,
    SCENE_PLAY,
    SCENE_CLEAR
};

GameScene currentScene;
// ここまで追加
画面
SCENE_TITLE タイトル画面
SCENE_PLAY ゲーム画面
SCENE_CLEAR クリア画面

現在の画面は、currentSceneへ保存します。

3つの画面は、次の条件で切り替わります。

現在の画面 切り替わる条件 次の画面
SCENE_TITLE Push Startをクリックする SCENE_PLAY
SCENE_PLAY 4 HITになる SCENE_CLEAR
SCENE_CLEAR Push Startをクリックする SCENE_PLAY

実際のenumと変数の宣言は、次のSTEPで追加します。


STEP 1 ここでソースを書き換える!

変更する場所:履歴欄の定数の下
確認すること:エラーなくビルドできること

Push Startボタンの定数を追加する

// ここから追加
// Push Startボタンの配置
const int PUSH_START_BUTTON_X = 300;
const int PUSH_START_BUTTON_Y = 390;
const int PUSH_START_BUTTON_WIDTH = 200;
const int PUSH_START_BUTTON_HEIGHT = 45;
// ここまで追加

画面を表す列挙型を追加する

// ここから追加
// 現在表示している画面
enum GameScene
{
    SCENE_TITLE,
    SCENE_PLAY,
    SCENE_CLEAR
};

// 現在の画面
GameScene currentScene;
// ここまで追加

STEP 2 ここでソースを書き換える!

変更する場所:画像ハンドルの宣言とLoadGameImages()
確認すること:エラーなくビルドできること

画像ハンドルを追加する

judgeImageの下へ追加します。

// ここから追加
// タイトル画面とクリア画面で使うボタン画像
int pushStartImage;
// ここまで追加

LoadGameImagesへ追加する

judgeImageの読み込みとエラー確認の後へ追加します。

// ここから追加
pushStartImage = LoadGraph("images/pushstart.png");

if (pushStartImage == -1)
{
    return 1;
}
// ここまで追加

1枚の画像を、タイトル画面とクリア画面の両方で使用します。


3.新しいゲームの初期化をまとめる

前回までは、起動時のInitialize()で問題と履歴を初期化していました。

今回は、タイトル画面やクリア画面から、何度でも新しいゲームを開始します。

そのため、新しいゲームに必要な初期化をStartGame()へまとめます。

StartGame()で行うこと 内容
新しい正解を作る MakeProblem(answer)を呼ぶ
入力欄と判定結果を空にする ClearInput()を呼ぶ
履歴を空にする historyCountcurrentHistoryPage0に戻す
ゲーム画面へ移動する currentSceneSCENE_PLAYにする

STEP 3 ここでソースを書き換える!

変更する場所ClearInput()AddHistory()の間
確認すること:エラーなくビルドできること

次の関数を追加します。

// 新しいゲームを始める
void StartGame(void)
{
    // 新しい正解を作る
    MakeProblem(answer);

    // 入力欄と判定結果を空にする
    ClearInput();

    // 解答履歴を空にする
    historyCount = 0;
    currentHistoryPage = 0;

    // ゲーム画面へ移動する
    currentScene = SCENE_PLAY;
}

StartGame()を呼び出すたびに、新しい正解が作られます。

履歴も0件へ戻るため、前のゲームの結果は残りません。


STEP 4 ここでソースを書き換える!

変更する場所:マウス入力の変数と、現在のInitialize()
確認すること:エラーなくビルドできること

クリックした瞬間を共有する変数を追加する

previousMouseInputの下へ、次の変数を追加します。

// ここに追加
// 左ボタンをクリックした瞬間か
bool leftClicked;

// 右ボタンをクリックした瞬間か
bool rightClicked;

この変数は、共通のUpdate()で更新し、UpdateTitle()UpdatePlay()UpdateClear()からそのまま使います。

Initializeを書き換える

現在のInitialize()を探し、下のコメントが付いた関数へ丸ごと置き換えます。

// この関数を丸ごと置き換える
// ゲーム開始時に1回だけ実行する
void Initialize(void)
{
    srand((unsigned int)time(NULL));

    mouseX = 0;
    mouseY = 0;
    previousMouseInput = 0;

    SetMouseDispFlag(TRUE);

    imageLoadError = LoadGameImages();

    // 起動直後はタイトル画面を表示する
    currentScene = SCENE_TITLE;
}

問題作成、入力初期化、履歴初期化は、Initialize()からStartGame()へ移りました。


4.画面ごとにUpdateを分ける

これまでのUpdate()をゲーム画面用のUpdatePlay()へ分け、タイトル画面とクリア画面にも専用の更新関数を用意します。

どの更新関数も引数を持たない同じ形にします。

Update()
・マウス入力を1回だけ取得する
・leftClickedを更新する
・現在の画面に対応するUpdateを呼ぶ

UpdateTitle() → タイトル画面
UpdatePlay()  → ゲーム画面
UpdateClear() → クリア画面

画面ごとの更新関数は、共通変数のmouseXmouseYleftClickedrightClickedを使います。


STEP 5 現在のUpdate関数をUpdatePlay関数へ変更する

変更する場所:現在のUpdate()
確認すること:このSTEPではまだビルドせず、そのままSTEP 6へ進むこと

現在のUpdate()を、下のUpdatePlay()へ丸ごと置き換えます。
マウス入力の取得とleftClickedの更新は、次のSTEPで作る共通のUpdate()へ移します。

UpdatePlay()は引数を受け取らず、共通変数のleftClickedrightClickedを確認します。

// この関数を丸ごと置き換える
void UpdatePlay(void)
{
    if (leftClicked)
    {
        int i;

        // 0~9の数字ボタンを順番に調べる
        for (i = 0; i < NUMBER_COUNT; i++)
        {
            int column;
            int row;
            int x;
            int y;

            column = i % NUMBER_BUTTON_COLUMNS;

            row = i / NUMBER_BUTTON_COLUMNS;

            x = NUMBER_BUTTON_START_X + column * (NUMBER_BUTTON_SIZE + NUMBER_BUTTON_GAP);

            y = NUMBER_BUTTON_START_Y + row * (NUMBER_BUTTON_SIZE + NUMBER_BUTTON_GAP);

            if (IsPointInRect(mouseX, mouseY, x, y, x + NUMBER_BUTTON_SIZE, y + NUMBER_BUTTON_SIZE))
            {
                // 4桁未満なら数字を追加する
                if (inputCount < INPUT_DIGIT_COUNT)
                {
                    input[inputCount] = (char)('0' + i);

                    inputCount++;

                    input[inputCount] = '\0';

                    // 入力を変更したので、
                    // 前回のエラーや判定結果を消す
                    inputError = 0;
                    hit = 0;
                    blow = 0;
                    hasResult = false;
                }

                break;
            }
        }

        // 判定ボタンをクリックしたか
        if (IsPointInRect(
            mouseX,
            mouseY,
            JUDGE_BUTTON_X,
            JUDGE_BUTTON_Y,
            JUDGE_BUTTON_X + JUDGE_BUTTON_WIDTH,
            JUDGE_BUTTON_Y + JUDGE_BUTTON_HEIGHT))
        {
            JudgeInput();
        }


        // 履歴ページのタブを順番に調べる
        for (i = 0; i < HISTORY_PAGE_COUNT; i++)
        {
            int tabX;

            tabX = HISTORY_START_X + i * HISTORY_TAB_WIDTH;

            if (IsPointInRect(mouseX, mouseY, tabX, HISTORY_TAB_Y, tabX + HISTORY_TAB_WIDTH, HISTORY_TAB_Y + HISTORY_TAB_HEIGHT))
            {
                currentHistoryPage = i;
                break;
            }
        }
    }
}

この時点では、共通のUpdate()がまだないため、ビルドするとエラーになります。
ここではビルドせず、そのままSTEP 6まで作業します。


STEP 6 ここでソースを書き換える!

変更する場所UpdatePlay()の前後
確認すること:エラーなくビルドできること

UpdateTitleを追加する

UpdatePlay()の前へ追加します。

// タイトル画面を更新する
void UpdateTitle(void)
{
    if (!leftClicked)
    {
        return;
    }

    // Push Startボタンをクリックしたか
    if (IsPointInRect(
        mouseX,
        mouseY,
        PUSH_START_BUTTON_X,
        PUSH_START_BUTTON_Y,
        PUSH_START_BUTTON_X + PUSH_START_BUTTON_WIDTH,
        PUSH_START_BUTTON_Y + PUSH_START_BUTTON_HEIGHT))
    {
        StartGame();
    }

    // 右クリックしたら入力中の数字をすべて消す
    if (rightClicked)
    {
        ClearInput();
    }
}

UpdateClearを追加する

UpdatePlay()の後へ追加します。

// クリア画面を更新する
void UpdateClear(void)
{
    if (!leftClicked)
    {
        return;
    }

    if (IsPointInRect(
        mouseX,
        mouseY,
        PUSH_START_BUTTON_X,
        PUSH_START_BUTTON_Y,
        PUSH_START_BUTTON_X + PUSH_START_BUTTON_WIDTH,
        PUSH_START_BUTTON_Y + PUSH_START_BUTTON_HEIGHT))
    {
        StartGame();
    }
}

新しいUpdateを追加する

UpdateClear()の後へ追加します。

// 現在の画面に対応した更新処理を呼び出す
void Update(void)
{
    int mouseInput;

    GetMousePoint(&mouseX, &mouseY);

    mouseInput = GetMouseInput();

    leftClicked = (mouseInput & MOUSE_INPUT_LEFT) != 0 && (previousMouseInput & MOUSE_INPUT_LEFT) == 0;

    rightClicked = (mouseInput & MOUSE_INPUT_RIGHT) != 0 && (previousMouseInput & MOUSE_INPUT_RIGHT) == 0;

    switch (currentScene)
    {
    case SCENE_TITLE:
        UpdateTitle();
        break;

    case SCENE_PLAY:
        // ゲーム画面を更新する
        UpdatePlay();
        break;

    case SCENE_CLEAR:
        UpdateClear();
        break;
    }

    previousMouseInput = mouseInput;
}

switch文は、currentSceneの値に対応するcaseだけを実行します。

currentSceneの値 呼び出す更新関数
SCENE_TITLE UpdateTitle()
SCENE_PLAY UpdatePlay()
SCENE_CLEAR UpdateClear()

leftClickedrightClickedは共通のUpdate()で1回だけ更新されるため、各画面の更新関数へ引数で渡す必要はありません。

caseの最後には、必ずbreakを書きます。


5.4 HITでクリア画面へ移動する

正解した結果も履歴へ残したいため、処理の流れを疑似言語で表すと次のようになります。

・HIT数とBLOW数を求める
・AddHistory()で履歴へ追加する

▲ hit = 4
|・クリア画面へ移動する
+---------
|・次の入力を始める準備
▼

STEP 7 ここでソースを書き換える!

変更する場所AddHistory()を呼び出した直後
確認すること:エラーなくビルドできること

現在のAddHistory()以降の処理を、次の形へ置き換えます。

// ここから変更
AddHistory();

// 4 HITならクリア画面へ移動する
if (hit == INPUT_DIGIT_COUNT)
{
    currentScene = SCENE_CLEAR;
}
else
{
    // 次の入力を始められるように、
    // 入力欄と表示状態を空にする
    inputCount = 0;
    input[0] = '\0';
    inputError = 0;
    hasResult = false;
}
// ここまで変更

INPUT_DIGIT_COUNTは4なので、hit == INPUT_DIGIT_COUNTが4 HITを表します。


6.画面ごとにDrawを分ける

現在のDraw()をゲーム画面用のDrawPlay()へ変更し、タイトル用とクリア用の描画関数を追加します。
最後に、Draw()から現在の画面に対応する描画関数を呼び分けます。


STEP 8 ここでソースを書き換える!

変更する場所:現在のDraw()と、その前後
確認すること:3つの画面が完成する

現在のDrawの関数名を変更する

// ゲーム画面を描画する
void DrawPlay(void)
{
    }

関数の中身は変更しません。

DrawTitleを追加する

pushstart.pngは表示する大きさが200×45なので、DrawGraph()でそのまま表示します。

// タイトル画面を描画する
void DrawTitle(void)
{
    unsigned int white = GetColor(255, 255, 255);

    unsigned int gray = GetColor(180, 190, 205);

    unsigned int red = GetColor(255, 80, 80);

    DrawString(315, 165, "HIT & BLOW", white);

    DrawString(225, 255, "4桁の数字を当てるゲームです。", gray);

    DrawString(208, 290, "数字と場所を推理して正解を探そう!", gray);

    if (imageLoadError != 0)
    {
        DrawString(235, 350, "画像を読み込めませんでした。", red);

        return;
    }

    DrawGraph(PUSH_START_BUTTON_X, PUSH_START_BUTTON_Y, pushStartImage, TRUE);
}

DrawClearを追加する

// クリア画面を描画する
void DrawClear(void)
{
    unsigned int white = GetColor(255, 255, 255);

    unsigned int yellow = GetColor(255, 225, 80);

    unsigned int gray = GetColor(180, 190, 205);

    DrawString(270, 150, "CONGRATULATIONS!", yellow);

    DrawFormatString(315, 230, white, "正解:%s", answer);

    DrawFormatString(285, 275, white, "%d回で正解しました。", historyCount);

    DrawString(270, 330, "Push Startでもう一度遊べます。", gray);

    if (imageLoadError == 0)
    {
        DrawGraph(PUSH_START_BUTTON_X, PUSH_START_BUTTON_Y, pushStartImage, TRUE);
    }
}

挑戦回数には、履歴件数のhistoryCountを使用します。

新しいDrawを追加する

// 現在の画面に対応した描画処理を呼び出す
void Draw(void)
{
    switch (currentScene)
    {
    case SCENE_TITLE:
        DrawTitle();
        break;

    case SCENE_PLAY:
        DrawPlay();
        break;

    case SCENE_CLEAR:
        DrawClear();
        break;
    }
}

7.ここまで正しく動くか確認しよう

Ctrl + F5で実行し、次の順番で確認します。

確認1 タイトル画面

  1. 起動直後にタイトル画面が表示される
  2. ゲーム画面の数字ボタンや履歴が表示されていない
  3. Push Startが表示される

確認2 ゲーム開始

  1. タイトル画面のPush Startを1回クリックする
  2. ゲーム画面へ移動する
  3. デバッグ用正解が表示される
  4. 履歴が0件になっている

確認3 クリア

  1. デバッグ用正解と同じ4桁を入力する
  2. 判定ボタンを押す
  3. クリア画面へ移動する
  4. 正解と挑戦回数が表示される

確認4 もう一度遊ぶ

  1. クリア画面のPush Startをクリックする
  2. ゲーム画面へ移動する
  3. 新しい正解が作られている
  4. 入力欄と履歴が空になっている

確認5 クリックが連続しない

Push Startを少し長く押しても、開始直後に数字ボタンが勝手に押されないことを確認します。


8.うまく動かない場合

起動するとゲーム画面が表示される

Initialize()の最後が次の処理になっているか確認します。

currentScene = SCENE_TITLE;

Push Startを押しても画面が変わらない

次を確認します。

  • Update()SCENE_TITLEからUpdateTitle()を呼んでいるか
  • UpdateTitle()からStartGame()を呼んでいるか
  • 描画とクリック判定で同じボタン座標を使用しているか

ゲーム画面で数字ボタンが動かない

以前のUpdate()にあったクリック処理が、UpdatePlay()へ残っているか確認します。

4 HITでもクリア画面へ移動しない

JudgeInput()で、AddHistory()の後に次の条件があるか確認します。

if (hit == INPUT_DIGIT_COUNT)
{
    currentScene = SCENE_CLEAR;
}

挑戦回数が1回少ない

正解した結果をAddHistory()へ追加してから、クリア画面へ移動しているか確認します。

switch文で複数の画面処理が呼ばれる

caseの最後にbreakがあるか確認します。


9.プログラム全体

ここまでの内容をすべて入れたmain.cppは、次のようになります。

#include "DxLib.h"

#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

// 画面の大きさ
const int SCREEN_WIDTH = 800;
const int SCREEN_HEIGHT = 600;

// ゲーム内部を1秒間に更新する回数
const int UPDATE_FPS = 60;

// Update1回分の時間
const double FIXED_DELTA_TIME = 1.0 / UPDATE_FPS;

// 長時間処理が止まった場合に、
// 一度に処理する経過時間の上限
const double MAX_FRAME_TIME = 0.25;

// HIT&BLOWで使用する数字の数
const int NUMBER_COUNT = 10;

// 入力する桁数
const int INPUT_DIGIT_COUNT = 4;

// 入力欄の配置
const int INPUT_START_X = 50;
const int INPUT_START_Y = 110;
const int INPUT_IMAGE_SIZE = 80;
const int INPUT_IMAGE_GAP = 10;

// 数字ボタンの配置
const int NUMBER_BUTTON_START_X = 30;
const int NUMBER_BUTTON_START_Y = 335;
const int NUMBER_BUTTON_SIZE = 70;
const int NUMBER_BUTTON_GAP = 8;
const int NUMBER_BUTTON_COLUMNS = 5;

// 判定ボタンの配置
const int JUDGE_BUTTON_X = 120;
const int JUDGE_BUTTON_Y = 510;
const int JUDGE_BUTTON_WIDTH = 200;
const int JUDGE_BUTTON_HEIGHT = 46;


// 解答履歴の最大件数
const int MAX_HISTORY = 60;

// 1ページに表示する履歴の件数
const int HISTORY_PER_PAGE = 15;

// 履歴ページの数
const int HISTORY_PAGE_COUNT = 4;

// 解答履歴欄の配置
const int HISTORY_START_X = 470;
const int HISTORY_TAB_Y = 60;
const int HISTORY_TAB_WIDTH = 78;
const int HISTORY_TAB_HEIGHT = 36;
const int HISTORY_TABLE_Y = 100;
const int HISTORY_TABLE_WIDTH = 312;
const int HISTORY_HEADER_HEIGHT = 28;
const int HISTORY_ROW_HEIGHT = 26;

// ===== 【第7回で追加・変更】Push Startの配置と画面状態を追加 ここから =====
// Push Startボタンの配置
const int PUSH_START_BUTTON_X = 300;
const int PUSH_START_BUTTON_Y = 390;
const int PUSH_START_BUTTON_WIDTH = 200;
const int PUSH_START_BUTTON_HEIGHT = 45;

// 現在表示している画面
enum GameScene
{
    SCENE_TITLE,
    SCENE_PLAY,
    SCENE_CLEAR
};

// 現在の画面
GameScene currentScene;

// ===== 【第7回で追加・変更】Push Startの配置と画面状態を追加 ここまで =====
// ゲームの正解
char answer[5];

// プレイヤーの入力
char input[5];

// 現在入力されている桁数
int inputCount;

// 入力内容のエラー番号
int inputError;

// 判定結果
int hit;
int blow;

// 判定結果を表示できる状態か
bool hasResult;

// 入力した4桁の履歴
char historyInput[MAX_HISTORY][INPUT_DIGIT_COUNT + 1];

// HIT数の履歴
int historyHit[MAX_HISTORY];

// BLOW数の履歴
int historyBlow[MAX_HISTORY];

// 現在保存されている履歴の件数
int historyCount;

// 現在表示している履歴ページ
int currentHistoryPage;

// マウスカーソルの座標
int mouseX;
int mouseY;

// 1回前のUpdateで取得したマウス状態
int previousMouseInput;

// ===== 【第7回で追加・変更】クリックした瞬間を画面間で共通利用 ここから =====
// 左ボタンをクリックした瞬間か
bool leftClicked;

// 右ボタンをクリックした瞬間か
bool rightClicked;

// ===== 【第7回で追加・変更】クリックした瞬間を画面間で共通利用 ここまで =====
// 0~9の数字ボタン画像
int numberImages[NUMBER_COUNT];

// 入力欄へ表示する0~9の画像
int displayNumberImages[NUMBER_COUNT];

// 未入力の桁に表示する画像
int blankImage;

// 判定ボタン画像
int judgeImage;
// ===== 【第7回で追加・変更】Push Start画像のハンドルを追加 ここから =====

// タイトル画面とクリア画面で使うボタン画像
int pushStartImage;
// ===== 【第7回で追加・変更】Push Start画像のハンドルを追加 ここまで =====

// 選択中の履歴タブ画像
int activeTabImages[HISTORY_PAGE_COUNT];

// 選択されていない履歴タブ画像
int inactiveTabImages[HISTORY_PAGE_COUNT];

// 画像の読み込み結果
int imageLoadError;


// 重複しない4桁の問題を作る
void MakeProblem(char outputAnswer[5])
{
    char temp[] = "0123456789";
    int i;

    for (i = 0; i < 100; i++)
    {
        int index1 = rand() % 10;
        int index2 = rand() % 10;
        char work;

        work = temp[index1];
        temp[index1] = temp[index2];
        temp[index2] = work;
    }

    for (i = 0; i < 4; i++)
    {
        outputAnswer[i] = temp[i];
    }

    outputAnswer[4] = '\0';
}

// 入力内容に問題がないか確認する
int CheckAnswerError(char inputData[])
{
    int i;
    int j;

    // 4桁か確認する
    if (strlen(inputData) != 4)
    {
        return 1;
    }

    // すべて数字か確認する
    for (i = 0; i < 4; i++)
    {
        if (!isdigit((unsigned char)inputData[i]))
        {
            return 2;
        }
    }

    // 同じ数字が含まれていないか確認する
    for (i = 0; i < 4; i++)
    {
        for (j = i + 1; j < 4; j++)
        {
            if (inputData[i] == inputData[j])
            {
                return 3;
            }
        }
    }

    return 0;
}

// HIT数を数える
int CountHit(char correctAnswer[], char inputData[])
{
    int count = 0;
    int i;

    for (i = 0; i < 4; i++)
    {
        if (correctAnswer[i] == inputData[i])
        {
            count++;
        }
    }

    return count;
}

// BLOW数を数える
int CountBlow(char correctAnswer[], char inputData[])
{
    int count = 0;
    int i;
    int j;

    for (i = 0; i < 4; i++)
    {
        for (j = 0; j < 4; j++)
        {
            if (inputData[i] == correctAnswer[j] && i != j)
            {
                count++;
            }
        }
    }

    return count;
}

// ゲームで使用する画像を読み込む
int LoadGameImages(void)
{
    char fileName[64];
    int i;

    // 0~9の画像を読み込む
    for (i = 0; i < NUMBER_COUNT; i++)
    {
        // 青い数字ボタンのファイル名を作る
        sprintf_s(fileName, sizeof(fileName), "images/%d.png", i);

        numberImages[i] = LoadGraph(fileName);

        if (numberImages[i] == -1)
        {
            return 1;
        }

        // 緑色の数字画像のファイル名を作る
        sprintf_s(fileName, sizeof(fileName), "images/%d%d.png", i, i);

        displayNumberImages[i] = LoadGraph(fileName);

        if (displayNumberImages[i] == -1)
        {
            return 1;
        }
    }

    // 未入力の桁に表示する画像を読み込む
    blankImage = LoadGraph("images/kome.png");

    if (blankImage == -1)
    {
        return 1;
    }

    // 判定ボタン画像を読み込む
    judgeImage = LoadGraph("images/judge.png");

    if (judgeImage == -1)
    {
        return 1;
    }

    // ===== 【第7回で追加・変更】Push Start画像を読み込む ここから =====
    pushStartImage = LoadGraph("images/pushstart.png");

    if (pushStartImage == -1)
    {
        return 1;
    }

    // ===== 【第7回で追加・変更】Push Start画像を読み込む ここまで =====
    // 履歴ページのタブ画像を読み込む
    for (i = 0; i < HISTORY_PAGE_COUNT; i++)
    {
        sprintf_s(fileName, sizeof(fileName), "images/tab%d.png", i + 1);

        activeTabImages[i] = LoadGraph(fileName);

        if (activeTabImages[i] == -1)
        {
            return 1;
        }

        sprintf_s(fileName, sizeof(fileName), "images/tab%da.png", i + 1);

        inactiveTabImages[i] = LoadGraph(fileName);

        if (inactiveTabImages[i] == -1)
        {
            return 1;
        }
    }

    return 0;
}

// 点が長方形の中にあるか調べる
bool IsPointInRect(int pointX, int pointY, int left, int top, int right, int bottom)
{
    return left <= pointX && pointX < right && top <= pointY && pointY < bottom;
}

// 入力中の4桁をすべて消す
void ClearInput(void)
{
    inputCount = 0;
    input[0] = '\0';

    inputError = 0;
    hit = 0;
    blow = 0;
    hasResult = false;
// ===== 【第7回で追加・変更】新しいゲームを始めるStartGame()を追加 ここから =====
}

// 新しいゲームを始める
void StartGame(void)
{
    // 新しい正解を作る
    MakeProblem(answer);

    // 入力欄と判定結果を空にする
    ClearInput();

    // 解答履歴を空にする
    historyCount = 0;
    currentHistoryPage = 0;

    // ゲーム画面へ移動する
    currentScene = SCENE_PLAY;
// ===== 【第7回で追加・変更】新しいゲームを始めるStartGame()を追加 ここまで =====
}

// 正常に判定できた1回分を履歴へ追加する
void AddHistory(void)
{
    // 配列の範囲を超える場合は追加しない
    if (historyCount >= MAX_HISTORY)
    {
        return;
    }

    // 入力した4桁を履歴へコピーする
    strcpy_s(historyInput[historyCount], sizeof(historyInput[historyCount]), input);

    // 同じ添え字へHIT数とBLOW数を保存する
    historyHit[historyCount] = hit;

    historyBlow[historyCount] = blow;

    // 履歴件数を1増やす
    historyCount++;

    // 今追加した履歴があるページを表示する
    currentHistoryPage = (historyCount - 1) / HISTORY_PER_PAGE;
}

// 入力内容を確認し、HIT数とBLOW数を求める
void JudgeInput(void)
{
    // 入力内容のエラー番号を受け取る
    inputError = CheckAnswerError(input);

    // 以前の判定結果をいったん消す
    hit = 0;
    blow = 0;
    hasResult = false;

    // 入力内容に問題がない場合だけ判定する
    if (inputError == 0)
    {
        hit = CountHit(answer, input);

        blow = CountBlow(answer, input);

        hasResult = true;

        // 履歴が満杯でなければ、
        // 判定結果を履歴へ保存する
        if (historyCount < MAX_HISTORY)
        {
            AddHistory();

            // ===== 【第7回で追加・変更】4 HITでクリア画面へ切り替える ここから =====
            // 4 HITならクリア画面へ移動する
            if (hit == INPUT_DIGIT_COUNT)
            {
                currentScene = SCENE_CLEAR;
            }
            else
            {
                // 次の入力を始められるように、
                // 入力欄と表示状態を空にする
                inputCount = 0;
                input[0] = '\0';
                inputError = 0;
                hasResult = false;
            }
            // ===== 【第7回で追加・変更】4 HITでクリア画面へ切り替える ここまで =====
        }
    }
}

// ゲーム開始時に1回だけ実行する
void Initialize(void)
{
    // 実行するたびに乱数の並びを変える
    srand((unsigned int)time(NULL));

    mouseX = 0;
    mouseY = 0;
    previousMouseInput = 0;

    SetMouseDispFlag(TRUE);

    // ゲームで使用する画像を読み込む
    imageLoadError = LoadGameImages();

// ===== 【第7回で追加・変更】起動直後をタイトル画面にする ここから =====

    // 起動直後はタイトル画面を表示する
    currentScene = SCENE_TITLE;
// ===== 【第7回で追加・変更】起動直後をタイトル画面にする ここまで =====
}

// ===== 【第7回で追加・変更】タイトル画面とゲーム画面の更新処理を分離 ここから =====
// タイトル画面を更新する
void UpdateTitle(void)
{
    if (!leftClicked)
    {
        return;
    }

    // Push Startボタンをクリックしたか
    if (IsPointInRect(
        mouseX,
        mouseY,
        PUSH_START_BUTTON_X,
        PUSH_START_BUTTON_Y,
        PUSH_START_BUTTON_X + PUSH_START_BUTTON_WIDTH,
        PUSH_START_BUTTON_Y + PUSH_START_BUTTON_HEIGHT))
    {
        StartGame();
    }
}

// ゲーム画面を更新する
void UpdatePlay(void)
{
    if (leftClicked)
    {
        int i;

        // 0~9の数字ボタンを順番に調べる
        for (i = 0; i < NUMBER_COUNT; i++)
        {
            int column;
            int row;
            int x;
            int y;

            column = i % NUMBER_BUTTON_COLUMNS;

            row = i / NUMBER_BUTTON_COLUMNS;

            x = NUMBER_BUTTON_START_X + column * (NUMBER_BUTTON_SIZE + NUMBER_BUTTON_GAP);

            y = NUMBER_BUTTON_START_Y + row * (NUMBER_BUTTON_SIZE + NUMBER_BUTTON_GAP);

            if (IsPointInRect(mouseX, mouseY, x, y, x + NUMBER_BUTTON_SIZE, y + NUMBER_BUTTON_SIZE))
            {
                // 4桁未満なら数字を追加する
                if (inputCount < INPUT_DIGIT_COUNT)
                {
                    input[inputCount] = (char)('0' + i);

                    inputCount++;

                    input[inputCount] = '\0';

                    // 入力を変更したので、
                    // 前回のエラーや判定結果を消す
                    inputError = 0;
                    hit = 0;
                    blow = 0;
                    hasResult = false;
                }

                break;
            }
        }

        // 判定ボタンをクリックしたか
        if (IsPointInRect(
            mouseX,
            mouseY,
            JUDGE_BUTTON_X,
            JUDGE_BUTTON_Y,
            JUDGE_BUTTON_X + JUDGE_BUTTON_WIDTH,
            JUDGE_BUTTON_Y + JUDGE_BUTTON_HEIGHT))
        {
            JudgeInput();
        }


        // 履歴ページのタブを順番に調べる
        for (i = 0; i < HISTORY_PAGE_COUNT; i++)
        {
            int tabX;

            tabX = HISTORY_START_X + i * HISTORY_TAB_WIDTH;

            if (IsPointInRect(mouseX, mouseY, tabX, HISTORY_TAB_Y, tabX + HISTORY_TAB_WIDTH, HISTORY_TAB_Y + HISTORY_TAB_HEIGHT))
            {
                currentHistoryPage = i;
                break;
            }
        }
    }

    // 右クリックしたら入力中の数字をすべて消す
    if (rightClicked)
    {
        ClearInput();
    }
}

// ===== 【第7回で追加・変更】タイトル画面とゲーム画面の更新処理を分離 ここまで =====
// ===== 【第7回で追加・変更】クリア画面と画面別Update()を追加 ここから =====
// クリア画面を更新する
void UpdateClear(void)
{
    if (!leftClicked)
    {
        return;
    }

    // Push Startボタンをクリックしたか
    if (IsPointInRect(
        mouseX,
        mouseY,
        PUSH_START_BUTTON_X,
        PUSH_START_BUTTON_Y,
        PUSH_START_BUTTON_X + PUSH_START_BUTTON_WIDTH,
        PUSH_START_BUTTON_Y + PUSH_START_BUTTON_HEIGHT))
    {
        StartGame();
    }
}

// 現在の画面に対応した更新処理を呼び出す
void Update(void)
{
    int mouseInput;

    // 現在のマウス座標を取得する
    GetMousePoint(&mouseX, &mouseY);

    // 現在のマウスボタン状態を取得する
    mouseInput = GetMouseInput();

    // 左クリックした瞬間を求める
    leftClicked = (mouseInput & MOUSE_INPUT_LEFT) != 0 && (previousMouseInput & MOUSE_INPUT_LEFT) == 0;

    // 右クリックした瞬間を求める
    rightClicked = (mouseInput & MOUSE_INPUT_RIGHT) != 0 && (previousMouseInput & MOUSE_INPUT_RIGHT) == 0;

    switch (currentScene)
    {
    case SCENE_TITLE:
        UpdateTitle();
        break;

    case SCENE_PLAY:
        UpdatePlay();
        break;

    case SCENE_CLEAR:
        UpdateClear();
        break;
// ===== 【第7回で追加・変更】クリア画面と画面別Update()を追加 ここまで =====
    }

    // 今回の状態を次回の比較用に保存する
    previousMouseInput = mouseInput;
}


// 解答履歴とページタブを描画する
void DrawHistory(void)
{
    unsigned int white = GetColor(255, 255, 255);

    unsigned int gray = GetColor(150, 160, 175);

    unsigned int lineColor = GetColor(80, 95, 115);

    unsigned int backgroundColor = GetColor(25, 30, 40);

    int tableBottom;
    int startIndex;
    int endIndex;
    int i;

    DrawString(HISTORY_START_X, 30, "解答履歴", white);

    // 4ページ分のタブを描画する
    for (i = 0; i < HISTORY_PAGE_COUNT; i++)
    {
        int tabX;
        int imageHandle;

        tabX = HISTORY_START_X + i * HISTORY_TAB_WIDTH;

        imageHandle = inactiveTabImages[i];

        if (i == currentHistoryPage)
        {
            imageHandle = activeTabImages[i];
        }

        DrawGraph(tabX, HISTORY_TAB_Y, imageHandle, TRUE);
    }

    tableBottom = HISTORY_TABLE_Y + HISTORY_HEADER_HEIGHT + HISTORY_PER_PAGE * HISTORY_ROW_HEIGHT;

    // 履歴表の背景と外枠を描画する
    DrawBox(HISTORY_START_X, HISTORY_TABLE_Y, HISTORY_START_X + HISTORY_TABLE_WIDTH, tableBottom, backgroundColor, TRUE);

    DrawBox(HISTORY_START_X, HISTORY_TABLE_Y, HISTORY_START_X + HISTORY_TABLE_WIDTH, tableBottom, lineColor, FALSE);

    // 表の見出しを描画する
    DrawString(HISTORY_START_X + 7, HISTORY_TABLE_Y + 5, "No", gray);

    DrawString(HISTORY_START_X + 50, HISTORY_TABLE_Y + 5, "INPUT", gray);

    DrawString(HISTORY_START_X + 151, HISTORY_TABLE_Y + 5, "HIT", gray);

    DrawString(HISTORY_START_X + 221, HISTORY_TABLE_Y + 5, "BLOW", gray);

    // 縦線を描画する
    DrawLine(HISTORY_START_X + 40, HISTORY_TABLE_Y, HISTORY_START_X + 40, tableBottom, lineColor);

    DrawLine(HISTORY_START_X + 140, HISTORY_TABLE_Y, HISTORY_START_X + 140, tableBottom, lineColor);

    DrawLine(HISTORY_START_X + 210, HISTORY_TABLE_Y, HISTORY_START_X + 210, tableBottom, lineColor);

    // 見出しと履歴行を分ける線を描画する
    DrawLine(
        HISTORY_START_X,
        HISTORY_TABLE_Y + HISTORY_HEADER_HEIGHT,
        HISTORY_START_X + HISTORY_TABLE_WIDTH,
        HISTORY_TABLE_Y + HISTORY_HEADER_HEIGHT,
        lineColor);

    // 15行分の横線を描画する
    for (i = 1; i <= HISTORY_PER_PAGE; i++)
    {
        int lineY;

        lineY = HISTORY_TABLE_Y + HISTORY_HEADER_HEIGHT + i * HISTORY_ROW_HEIGHT;

        DrawLine(HISTORY_START_X, lineY, HISTORY_START_X + HISTORY_TABLE_WIDTH, lineY, lineColor);
    }

    // 現在のページで表示する履歴の範囲を求める
    startIndex = currentHistoryPage * HISTORY_PER_PAGE;

    endIndex = startIndex + HISTORY_PER_PAGE;

    if (endIndex > historyCount)
    {
        endIndex = historyCount;
    }

    // 現在のページにある履歴を表示する
    for (i = startIndex; i < endIndex; i++)
    {
        int row;
        int y;

        row = i - startIndex;

        y = HISTORY_TABLE_Y + HISTORY_HEADER_HEIGHT + 4 + row * HISTORY_ROW_HEIGHT;

        DrawFormatString(HISTORY_START_X + 7, y, white, "%02d", i + 1);

        DrawString(HISTORY_START_X + 50, y, historyInput[i], white);

        DrawFormatString(HISTORY_START_X + 165, y, white, "%d", historyHit[i]);

        DrawFormatString(HISTORY_START_X + 242, y, white, "%d", historyBlow[i]);
    }
}

// ===== 【第7回で追加・変更】従来のゲーム描画をDrawPlay()へ変更 ここから =====
// ゲーム画面を描画する
void DrawPlay(void)
// ===== 【第7回で追加・変更】従来のゲーム描画をDrawPlay()へ変更 ここまで =====
{
    unsigned int white = GetColor(255, 255, 255);

    unsigned int yellow = GetColor(255, 255, 0);

    unsigned int red = GetColor(255, 80, 80);
    
    unsigned int gray = GetColor(160, 160, 160);

    // ゲームタイトルを表示する
    DrawString(40, 25, "HIT & BLOW", white);

    // 完成時には削除する正解表示
    DrawFormatString(40, 55, yellow, "デバッグ用正解:%s", answer);

    // 画像の読み込みに失敗していた場合
    if (imageLoadError != 0)
    {
        DrawString(40, 110, "画像を読み込めませんでした。", red);

        DrawString(40, 140, "imagesフォルダーの場所を確認してください。", red);

        return;
    }

    DrawString(40, 85, "数字を4桁入力してください", white);

    // 4桁の入力欄を描画する
    for (int i = 0; i < INPUT_DIGIT_COUNT; i++)
    {
        int x;
        int imageHandle;

        // i番目の桁を表示するX座標
        x = INPUT_START_X + i * (INPUT_IMAGE_SIZE + INPUT_IMAGE_GAP);

        // 未入力の場合に表示する画像
        imageHandle = blankImage;

        // 入力済みで、数字が入っている場合
        if (i < inputCount)
        {
            int number;

            // 文字の数字を整数へ変換する
            number = input[i] - '0';

            // 対応する緑色の数字画像を選ぶ
            imageHandle = displayNumberImages[number];
        }

        // 入力欄の画像を80×80で表示する
        DrawGraph(x, INPUT_START_Y, imageHandle, TRUE);
    }

    DrawFormatString(40, 215, white, "入力:%s", input);

    // 正しく判定できた場合はHIT数とBLOW数を表示する
    if (hasResult)
    {
        DrawFormatString(40, 245, white, "HIT :%d", hit);

        DrawFormatString(40, 275, white, "BLOW:%d", blow);
    }
    // 入力に問題がある場合は、
    // エラー番号に対応したメッセージを表示する
    else if (inputError == 1)
    {
        DrawString(40, 245, "4桁入力してください。", red);
    }
    else if (inputError == 2)
    {
        DrawString(40, 245, "0~9の数字だけを入力してください。", red);
    }
    else if (inputError == 3)
    {
        DrawString(40, 245, "同じ数字を2回使わないでください。", red);
    }

    DrawString(40, 300, "数字ボタン", white);

    // 0~9の数字ボタンを描画する
    for (int i = 0; i < NUMBER_COUNT; i++)
    {
        int column;
        int row;
        int x;
        int y;

        // 5個ずつ並べるための列と行を求める
        column = i % NUMBER_BUTTON_COLUMNS;

        row = i / NUMBER_BUTTON_COLUMNS;

        // 列と行から画面座標を求める
        x = NUMBER_BUTTON_START_X + column * (NUMBER_BUTTON_SIZE + NUMBER_BUTTON_GAP);

        y = NUMBER_BUTTON_START_Y + row * (NUMBER_BUTTON_SIZE + NUMBER_BUTTON_GAP);

        // 数字ボタンを70×70で表示する
        DrawGraph(x, y, numberImages[i], TRUE);
    }

    // 判定ボタンを表示する
    DrawGraph(JUDGE_BUTTON_X, JUDGE_BUTTON_Y, judgeImage, TRUE);

    // 右クリックで入力を消せることを表示する
    DrawString(330, 525, "右クリック:入力を消す", gray);

    // 右側へ解答履歴を表示する
    DrawHistory();
}

// ===== 【第7回で追加・変更】タイトル・クリア画面と画面別Draw()を追加 ここから =====
// タイトル画面を描画する
void DrawTitle(void)
{
    unsigned int white = GetColor(255, 255, 255);

    unsigned int gray = GetColor(180, 190, 205);

    unsigned int red = GetColor(255, 80, 80);

    DrawString(315, 165, "HIT & BLOW", white);

    DrawString(225, 255, "4桁の数字を当てるゲームです。", gray);

    DrawString(208, 290, "数字と場所を推理して正解を探そう!", gray);

    if (imageLoadError != 0)
    {
        DrawString(235, 350, "画像を読み込めませんでした。", red);

        return;
    }

    DrawGraph(PUSH_START_BUTTON_X, PUSH_START_BUTTON_Y, pushStartImage, TRUE);
}

// クリア画面を描画する
void DrawClear(void)
{
    unsigned int white = GetColor(255, 255, 255);

    unsigned int yellow = GetColor(255, 225, 80);

    unsigned int gray = GetColor(180, 190, 205);

    DrawString(270, 150, "CONGRATULATIONS!", yellow);

    DrawFormatString(315, 230, white, "正解:%s", answer);

    DrawFormatString(285, 275, white, "%d回で正解しました。", historyCount);

    DrawString(270, 330, "Push Startでもう一度遊べます。", gray);

    if (imageLoadError == 0)
    {
        DrawGraph(PUSH_START_BUTTON_X, PUSH_START_BUTTON_Y, pushStartImage, TRUE);
    }
}

// 現在の画面に対応した描画処理を呼び出す
void Draw(void)
{
    switch (currentScene)
    {
    case SCENE_TITLE:
        DrawTitle();
        break;

    case SCENE_PLAY:
        DrawPlay();
        break;

    case SCENE_CLEAR:
        DrawClear();
        break;
    }
}


// ===== 【第7回で追加・変更】タイトル・クリア画面と画面別Draw()を追加 ここまで =====
// Update1回分の時間を秒で返す
double DeltaTime(void)
{
    return FIXED_DELTA_TIME;
}

int WINAPI WinMain(
    HINSTANCE hInstance,
    HINSTANCE hPrevInstance,
    LPSTR lpCmdLine,
    int nCmdShow)
{
    LONGLONG previousTime;
    double accumulator;

    // ウィンドウモードで起動する
    ChangeWindowMode(TRUE);

    // 画面の大きさを設定する
    SetGraphMode(SCREEN_WIDTH, SCREEN_HEIGHT, 32);

    // ScreenFlipで垂直同期を待つ
    SetWaitVSyncFlag(TRUE);

    // DxLibを初期化する
    if (DxLib_Init() == -1)
    {
        return -1;
    }

    // 描画先を裏画面にする
    SetDrawScreen(DX_SCREEN_BACK);

    // ゲームを初期化する
    Initialize();

    // 時間計測を開始する
    previousTime = GetNowHiPerformanceCount();

    // まだUpdateで処理していない時間
    accumulator = 0.0;

    // ゲームループ
    while (ProcessMessage() == 0)
    {
        LONGLONG currentTime;
        double frameTime;

        // ESCキーが押されたら終了する
        if (CheckHitKey(KEY_INPUT_ESCAPE) != 0)
        {
            break;
        }

        // 現在時刻を取得する
        currentTime = GetNowHiPerformanceCount();

        // 前回から実際に経過した時間を秒で求める
        frameTime = (currentTime - previousTime) / 1000000.0;

        // 次の計測に備えて現在時刻を保存する
        previousTime = currentTime;

        // 長時間止まった場合は、処理する時間を制限する
        if (frameTime > MAX_FRAME_TIME)
        {
            frameTime = MAX_FRAME_TIME;
        }

        // 実際に経過した時間をためる
        accumulator += frameTime;

        // 1/60秒以上たまっている間、ゲームを更新する
        while (accumulator >= DeltaTime())
        {
            Update();

            // Updateで処理した時間を引く
            accumulator -= DeltaTime();
        }

        // 裏画面を黒で消す
        ClearDrawScreen();

        // ゲーム画面を描画する
        Draw();

        // 裏画面を表画面へ表示する
        ScreenFlip();
    }

    // DxLibを終了する
    DxLib_End();

    return 0;
}

10.今回のまとめ

今回は、現在の画面をGameScene型のcurrentSceneへ保存しました。

SCENE_TITLE
SCENE_PLAY
SCENE_CLEAR

Update()Draw()では、switch文を使って現在の画面に対応する関数だけを呼び出します。

タイトル画面
    ↓ Push Start
ゲーム画面
    ↓ 4 HIT
クリア画面
    ↓ Push Start
ゲーム画面

新しいゲームに必要な初期化はStartGame()へまとめました。

次回は、60回以内に正解できなかった場合のゲームオーバー画面を追加し、全体を完成させます。