第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()を呼ぶ |
| 履歴を空にする | historyCountとcurrentHistoryPageを0に戻す |
| ゲーム画面へ移動する | currentSceneをSCENE_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() → クリア画面
画面ごとの更新関数は、共通変数のmouseX、mouseY、leftClicked、rightClickedを使います。
STEP 5 現在のUpdate関数をUpdatePlay関数へ変更する
変更する場所:現在の
Update()
確認すること:このSTEPではまだビルドせず、そのままSTEP 6へ進むこと
現在のUpdate()を、下のUpdatePlay()へ丸ごと置き換えます。
マウス入力の取得とleftClickedの更新は、次のSTEPで作る共通のUpdate()へ移します。
UpdatePlay()は引数を受け取らず、共通変数のleftClickedとrightClickedを確認します。
// この関数を丸ごと置き換える
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() |
leftClickedとrightClickedは共通の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 タイトル画面
- 起動直後にタイトル画面が表示される
- ゲーム画面の数字ボタンや履歴が表示されていない
Push Startが表示される
確認2 ゲーム開始
- タイトル画面の
Push Startを1回クリックする - ゲーム画面へ移動する
- デバッグ用正解が表示される
- 履歴が0件になっている
確認3 クリア
- デバッグ用正解と同じ4桁を入力する
- 判定ボタンを押す
- クリア画面へ移動する
- 正解と挑戦回数が表示される
確認4 もう一度遊ぶ
- クリア画面の
Push Startをクリックする - ゲーム画面へ移動する
- 新しい正解が作られている
- 入力欄と履歴が空になっている
確認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回以内に正解できなかった場合のゲームオーバー画面を追加し、全体を完成させます。