Skip to main content

第1回 コンソール版から画面付きゲームへ

第1回 コンソール版から画面付きゲームへ

この章では、コンソール版で作ったHIT&BLOWを、DxLibを使った画面付きゲームへ作り替えていきます。

今回は、コンソール版と画面付きゲームの違いを確認し、ゲームテンプレートのDraw()へタイトルを表示します。

この回は、付録2で作ったゲームテンプレートのmain.cppから作業を始めます。


このページの進め方

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

説明
    仕組みを確認する

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

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

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

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


1.画像素材について

数字ボタンなどの画像は第3回から使用します。
画像のダウンロード、展開、配置は第3回で行います。


説明1 コンソール版とゲームループの違い

コンソール版では何をしていたか

コンソール版では、次のような流れでゲームを進めていました。

問題を作る
    ↓
キーボードから4桁の数字を入力する
    ↓
入力内容を確認する
    ↓
HITとBLOWを数える
    ↓
結果を文字で表示する
    ↓
正解するまで繰り返す

入力にはscanf()、表示にはprintf()を使っていました。

printf("予想した4桁の数字を入力:");
scanf("%s", input);

printf("HIT :%d\n", hit);
printf("BLOW:%d\n", blow);

scanf()を実行すると、入力されるまでプログラムはその場所で止まります。

scanfを実行する
    ↓
プレイヤーが入力するまで待つ
    ↓
入力されたら次の処理へ進む

コンソールプログラムでは、この方法で問題ありません。


画面付きゲームでは止まらずに動き続ける

画面付きゲームでは、入力を待つためにプログラムを止めません。

ゲームは、次の処理を何度も繰り返します。

入力を確認する
    ↓
ゲームの状態を更新する
    ↓
画面を描画する
    ↓
最初へ戻る

この繰り返しをゲームループと呼びます。

今回使用するテンプレートでは、次の関数を使います。

Initialize();
Update();
Draw();
DeltaTime();

それぞれの役割は次のとおりです。

関数 役割
Initialize() ゲーム開始時の準備を行う
Update() 入力確認とゲームの更新を行う
Draw() 現在のゲーム画面を描画する
DeltaTime() Update()1回分の時間を秒で返す

ゲーム開始時にInitialize()が1回だけ実行され、その後はUpdate()Draw()が繰り返されます。

Initialize
    ↓
Update
    ↓
Draw
    ↓
Update
    ↓
Draw
    ↓
...

DeltaTime()は、1回の更新で進める時間を返す関数です。
このプログラムでは1秒間に60回更新するため、1.0 / 60.0秒を返します。

ゲームループでは、この値を使って「次の更新を行う時間になったか」を調べます。

while (accumulator >= DeltaTime())
{
    Update();
    accumulator -= DeltaTime();
}

前回の画面更新から経過した時間をaccumulatorへため、DeltaTime()が返す1/60秒以上になったらUpdate()を実行します。

・実際に経過した時間をためる

■ 1/60秒以上たまっている間
|・Updateを実行する
|・ためた時間から1/60秒を引く
■

・Drawで画面を描画する

image-1786253115166.png

図01-01のように、Initialize()は最初に1回だけ実行されます。
その後は、Update()でゲームの状態を更新し、Draw()で画面へ表示する処理を繰り返します。

このテンプレートで固定しているのは、Update()によるゲーム内部の更新間隔です。
Draw()の実行回数は、モニターのリフレッシュレートや処理速度によって変わることがあります。

コンソール版から変わる部分

コンソール版の入力と表示は、DxLib版では次のように変わります。

コンソール版 DxLib版
printf()でその場に表示する 結果を変数へ保存し、Draw()で表示する
scanf()で入力を待つ Update()でマウス入力を確認する
正解までwhileで繰り返す すでにあるゲームループの中で少しずつ処理する

一方、ゲームのルールを処理する次の関数は、そのまま再利用できます。

MakeProblem();
CheckAnswerError();
CountHit();
CountBlow();

入力方法と表示方法だけをDxLib用に作り直します。


複数のフレームにまたがって使う変数

コンソール版では、入力から判定までを一度に実行していました。

DxLib版では、入力、判定、描画が別々のタイミングで実行されます。

あるUpdateで数字を入力する
        ↓
後のUpdateで判定する
        ↓
Drawで結果を表示する

そのため、入力内容や判定結果は、次のフレームまで残しておく必要があります。

この教材では、まずグローバル変数を使って状態を保存します。

char answer[5];
char input[5];

int hit;
int blow;

関数の中で作ったローカル変数は、その関数が終わると使用できなくなります。

複数の関数や複数のフレームで使う値は、関数の外へ用意します。


この教材で扱う正解は、0~9から重複しない4個を並べた数字列です。
先頭が0の0123も正解として使用します。


STEP 1 Draw関数へタイトル表示を追加する

変更する場所:現在のDraw()
確認すること:黒いウィンドウへHIT & BLOWと表示される

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

// この関数を丸ごと置き換える
void Draw(void)
{
    DrawString(300, 100, "HIT & BLOW", GetColor(255, 255, 255));
}

2.動作確認

Ctrl + F5で実行します。

次の状態になれば成功です。

  • 800×600の黒いウィンドウが表示される
  • 画面にHIT & BLOWと表示される
  • ウィンドウはすぐに閉じない
  • ESCキーを押すと終了する

Draw()はゲームループから繰り返し呼び出されます。
そのため、ウィンドウを開いている間、タイトルが描画され続けます。


3.プログラム全体

第1回終了時点のmain.cppは、次のようになります。

#include "DxLib.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;

// ゲーム開始時に1回だけ実行する
void Initialize(void)
{
    // 初期化処理を書く
}

// 入力確認とゲームの更新を行う
void Update(void)
{
    // キーボードやマウスの入力処理を書く

    // ゲームの更新処理を書く
}

// ゲーム画面を描画する
void Draw(void)
{
    // ===== 【第1回で追加・変更】最初の文字を画面へ表示 ここから =====
    DrawString(300, 100, "HIT & BLOW", GetColor(255, 255, 255));
    // ===== 【第1回で追加・変更】最初の文字を画面へ表示 ここまで =====
}

// 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;
}

4.今回のまとめ

コンソール版とDxLib版では、入力と表示の方法が変わります。

printf
    ↓
Drawで文字や画像を描画する
scanf
    ↓
Updateでマウスやキーボードを確認する
入力待ちをするwhile
    ↓
止まらずに動き続けるゲームループ

DeltaTime()は、ゲームループの中でUpdate()を1/60秒間隔で進めるために使用されています。

一方で、問題作成やHIT・BLOWの判定処理は、そのまま再利用できます。

次回は、コンソール版で作ったゲーム処理をDxLibのプロジェクトへ移します。