Skip to main content

付録2 DxLibテンプレート

付録2 今後のゲDxLibゲーム制作で使用するDxLibテンプレート

このページでは、これからのゲーム制作で共通して使用するDxLibのテンプレートを用意します。

このテンプレートは、HIT&BLOWだけために作るも4つではありません。
次にシューティングゲームやアクション関数を使ってゲームを作るときも、基本的には同じものを使用します。

最初から用意されている処理は、次のとおりです。

Initialize();
Update();
Draw();
DeltaTime();
  • 800×600の黒いウィンドウを表示Initialize():ゲーム開始時に1回だけ実行する
  • DxLibを初期化するUpdate():入力確認とゲームの更新を行う
  • Draw()画面描画する
  • DeltaTime()Update()1回分の経過時間を秒で返す

1.ゲーム内部を1秒間に60回ループ

ゲームは、入力・更新・描画を繰り返して動いています。

Initializeを1回実行する
        
  • 実際に経過した時間をする
  • GameUpdatedeltaTimeを渡す
  • ESCキーまたはウィンドウの閉じるボタンで終了する
  • ゲーム終了時の後始末を行う
  • 次のチャプターから、このテンプレートへゲームの処理を追加していきます。


    1.ゲームは処理を繰り返して動いている

    コンソールプログラムでは、上から下へ命令を実行し、最後まで進むとプログラムが終了するものが多くありました。

    ゲームは、すぐには終了しません。

    入力を調べる
            ↓
    ゲームの状態を更新1/60秒分たまっていればUpdateを実行する
            ↓
    Drawで画面を描画する
            ↓
    最初へ戻る
    

    この繰り返しをUpdate()、ゲームループといい内部の時間が1/60秒進むたびに実行されます。

    処理が遅れて1/60秒が複数のテンプレートで分たまっている場合は、Draw()を行う前次の関Update()を複へ処理を追加回実行します。

    void GameInitialize(void)
    {
        // ゲーム開始時に1回だけ行う処理
    }
    
    void InputUpdate(void)
    {
        // キーボードやマウスの入力を調べる処理
    }
    
    void GameUpdate(double deltaTime)
    {
        // ゲームの状態を更新する処理
    }
    
    void GameDraw(double interpolation)
    {
        // ゲーム画面を描画する処理
    }
    
    void GameFinalize(void)
    {
        // ゲーム終了時に1回だけ行う処理
    }
    

    2.フレーム内部は1秒間に60回更新するレート

    このテンプレートでは、ゲーム内部の更新回数を1秒隔を次のよう決めてい60回へ固定します。

    const int UPDATE_FPS = 60;
    const double FIXED_DELTA_TIME = 1.0 / UPDATE_FPS;
    

    FIXED_DELTA_TIMEDeltaTime()が返す値は、ゲーム内部を1回進める時間次のとおりです。

    1 ÷ 60 = 約0.016666秒
    

    ゲーム内部したがって、Update()の中は、約0.016666秒ずつ時間を進めます。

    1回目のUpdate    約0.016666秒進む
    2回目のUpdate    約0.016666秒進む
    3回目のUpdate    約0.016666秒進む
            :
    60回目のUpdate   合計で約1秒進む
    

    のように書くとゲーム内部を一定の時1秒ずつ進める方法を固定タイムステップといいます。


    3.このテンプレートで扱う2種類の時間

    このテンプレートでは、2種類の時間を扱います。

    名前意味
    frameDeltaTimespeed前回のループから実際に経過した時間
    deltaTimeゲーム内部を1回更新するときに進める固定時間

    frameDeltaTime

    frameDeltaTimeは、コンピュータ上で実際に経過した時間です。

    実行するたびに、少しずつ異なります。

    0.0162秒
    0.0170秒
    0.0158秒
    

    実際に経過した時間は、次の関数を使って計測だけ移動します。

    GetNowHiPerformanceCount()position 

    取得した値はマイクロ秒単位なので、1000000.0で割って秒へ変換します。

    frameDeltaTime += (currentTimespeed -* previousTime) / 1000000.0;
    

    deltaTime

    GameUpdateへ渡されるdeltaTimeは、固定で60分の1秒です。

    void GameUpdate(double deltaTime)
    {
    }DeltaTime();
    

    このテンプレートでは、次の値が渡されます。

    deltaTime = 1.0 ÷ 60.0
              = 約0.016666秒
    

    シューティングゲームで自機を移動させる場合は、次のように使用します。

    playerX += playerSpeed * deltaTime;
    

    例えば、移動速度が1秒間に240ピクセルなら、1回の更新で進む距離は次のようになります。

    240 × 1/60 = 4ピクセル
    

    これを1秒間に60回行うので、1秒間に240ピクセル移動します。

    ゲーム内の移動、弾、敵、アニメーションなどには、原則としてGameUpdateへ渡されるdeltaTimeを使用します。


    4.固定タイムステップの処理

    実際に経過した時間は、accumulatorという変数へためておきます。

    実際に経過した時間を測る
            ↓
    accumulatorへ加える
            ↓
    1/60秒以上たまっているか調べる
            ↓
    たまっていればGameUpdateを1回実行する
            ↓
    処理した1/60秒をaccumulatorから引く
    

    疑似言語で表すと、次のようになります。

    ○ 実数型:frameDeltaTime
    ○ 実数型:accumulator
    ○ 実数型:deltaTime
    
    ・deltaTime ← 1 ÷ 60
    ・accumulator ← 0
    
    ■ ゲームが終了していない間
    
    │ ・実際に経過した時間をframeDeltaTimeへ入れる
    │ ・accumulatorへframeDeltaTimeを加える
    
    │ ■ accumulatorがdeltaTime以上の間
    │ │
    │ │ ・入力状態を更新する
    │ │ ・ゲームをdeltaTime秒だけ進める
    │ │ ・accumulatorからdeltaTimeを引く
    │ │
    │ ■
    
    │ ・ゲーム画面を描画する
    
    ■
    

    例えば、前回から約0.033秒経過していた場合、約60分の2秒がたまっています。

    0.033秒 ÷ 0.016666秒 ≒ 2回
    

    この場合は、画面を描画する前にGameUpdateを2回実行します。


    5.描画回数と更新回数は同じとは限らない

    このテンプレートが固定するのは、Update()によるゲーム内部の更新間隔です。

    画面Draw()描画実行回数必ず毎秒60回に固定するわけではありません。

    ゲーム内部の更新
    1秒間に60回を基準として進める
    
    画面の描画
    モニターのリフレッシュレートや処理速度によって変せて行う
    

    例えば、144Hzのモニターでは、画面が1秒間に約144回描画されることがあります。

    それでも、ゲーム内部は1秒間に60回を基準として更新されます。


    反対に、処理が重くなり、画面を1秒間に30回しか描画できない場合は、1回の描画までにGameUpdateを複数回実行して時間を進めます。

    「描画を60FPSへ固定する」のではなく、「ゲーム内部を60分の1秒ずつ進める」

    3.テンプレートです。


    6.テンプレートのソースコード

    次のコードをmain.cppへ記述します。

    #include "DxLib.h"
    
    // 画面の大きさ
    const int SCREEN_WIDTH = 800;
    const int SCREEN_HEIGHT = 600;
    
    // ゲーム内部を1秒間に更新する回数
    const int UPDATE_FPS = 60;
    
    // ゲーム内部を1回更新するときに進めるUpdate1回分の時間
    const double FIXED_DELTA_TIME = 1.0 / UPDATE_FPS;
    
    // ウィンドウ移動やデバッグ停止などで長時間処理が止まった場合に、
    // 一度に処理する経過時間の上限
    const double MAX_FRAME_DELTA_TIMEMAX_FRAME_TIME = 0.25;
    
    // 1回の描画までに実行するGameUpdateの最大回数
    const int MAX_UPDATE_COUNT = 5;
    
    // 実際の1フレームで経過した時間
    double g_frameDeltaTime = 0.0;
    
    // ゲーム開始時に1回だけ実行する
    void GameInitialize(Initialize(void)
    {
        // ====================================
        // ゲームの初期化処理を書く場所
    }
    
    // ====================================入力確認とゲームの更新を行う
    }void Update(void)
    {
        // キーボードやマウスの入力状態を更新する
    void InputUpdate(void)
    {
        // ====================================
        // 入力の更新処理を書く場所
        // ====================================
    }
    
    // ゲーム内部を更新する
    void GameUpdate(double deltaTime)
    {
        // ====================================
    
        // ゲームの更新処理を書く場所
        // ====================================
    
        /*
        シューティングゲームでの使用例
    
        playerX += playerSpeed * deltaTime;
        */
    
        // 現在はdeltaTimeを使用しないため、
        // 未使用の警告を防いでいる
        (void)deltaTime;
    }
    
    // ゲーム画面を描画する
    void GameDraw(double interpolation)Draw(void)
    {
        // ====================================
        // ゲームの描画処理を書く場所
        // ====================================
    
        /*
        interpolationは、
        前回の更新と次の更新の間の位置を表す。
    
        0.0以上1.0未満の値になり、
        後で描画位置を滑らかに補間するときに使用できる。
        */
    
        // 現在はinterpolationを使用しないため、
        // 未使用の警告を防いでいる
        (void)interpolation;
    }
    
    // ゲーム終了時に1回だけ実行する
    void GameFinalize(void)
    {
        // ====================================
        // 画像や音などUpdate1回分後始末を書く場所
        // ====================================
    }
    
    // 実際の1フレームで経過した時間を取得秒で返
    double GetFrameDeltaTime(DeltaTime(void)
    {
        return g_frameDeltaTime;FIXED_DELTA_TIME;
    }
    
    int WINAPI WinMain(
        HINSTANCE hInstance,
        HINSTANCE hPrevInstance,
        LPSTR lpCmdLine,
        int nCmdShow)
    {
        LONGLONG previousTime;
        double accumulator;
    
        int exitRequested;
    
        // ウィンドウモードで起動する
        ChangeWindowMode(TRUE);
    
        // 画面の大きさと色数を設定する
        SetGraphMode(
            SCREEN_WIDTH,
            SCREEN_HEIGHT,
            32);
    
        /*/ * ScreenFlipを実行したときに、
         * モニターのScreenFlipで垂直同期を待つ。
         *
         * この設定はDxLib_Initより前に行う。
         */
        SetWaitVSyncFlag(TRUE);
    
        // DxLibを初期化する
        if (DxLib_Init() == -1)
        {
            return -1;
        }
    
        // 描画先を裏画面にする
        SetDrawScreen(DX_SCREEN_BACK);
    
        // ゲームを初期化する
        GameInitialize(Initialize();
    
        // 時間計測を開始する
        previousTime = GetNowHiPerformanceCount();
    
        // まだゲーム内部でUpdateで処理していない経過時間
        accumulator = 0.0;
    
        // 終了要求
        exitRequested = FALSE;
    
        // メインゲームループ
        while (ProcessMessage() == 0)
        {
            LONGLONG currentTime;
            int updateCount;
            double interpolation;frameTime;
    
            // ESCキーが押されたら終了する
            if (CheckHitKey(KEY_INPUT_ESCAPE) != 0)
            {
                break;
            }
    
            // 現在時刻を取得する
            currentTime = GetNowHiPerformanceCount();
    
            /*
             */ 前回から実際に経過した時間を秒で求める
            *
             * GetNowHiPerformanceCountの値は
             * マイクロ秒単位なので、1000000.0で割る。
             */
            g_frameDeltaTimeframeTime =
                (currentTime - previousTime) / 1000000.0;
    
            // 次の計測に備えて現在時刻を保存する
            previousTime = currentTime;
    
            /*
             * ウィンドウ移動やデバッグ停止などで
             */ 長時間止まった場合の対策。
             *
             * 0.25秒より長い経過時間は、
             * 0.25秒として処理する。
             */時間を制限する
            if (g_frameDeltaTimeframeTime > MAX_FRAME_DELTA_TIME)MAX_FRAME_TIME)
            {
                g_frameDeltaTimeframeTime = MAX_FRAME_DELTA_TIME;MAX_FRAME_TIME;
            }
    
            // 実際に経過した時間をためる
            accumulator += g_frameDeltaTime;frameTime;
    
            // この描画までに実行したUpdateの回数
            updateCount = 0;
    
            /*
             * 1/60秒以上たまっている間、
             * ゲーム内部を1/60秒ずつ進める。
             */
            while (
                accumulator >= FIXED_DELTA_TIME &&
                updateCount < MAX_UPDATE_COUNT)
            {
                // 入力状態を更新する
            InputUpdate(while (accumulator >= DeltaTime())
            {
                Update();
    
                // ゲーム内部を固定時間だけ進める
                GameUpdate(FIXED_DELTA_TIME);
    
                // Updateで処理した時間を引く
                accumulator -= FIXED_DELTA_TIME;
    
                updateCount++DeltaTime();
            }
    
            /*
             * 処理落ちが大きすぎる場合、
             * 遅れをすべて取り戻そうとすると、
             * GameUpdateを何度も実行し続ける可能性がある。
             *
             * 最大回数まで更新しても遅れている場合は、
             * 残った遅れを破棄する。
             */
            if (
                updateCount == MAX_UPDATE_COUNT &&
                accumulator >= FIXED_DELTA_TIME)
            {
                accumulator = 0.0;
            }
    
            /*
             * 前回の更新から次の更新までの間を、
             * どこまで進んでいるか求める。
             *
             * 結果は0.0以上1.0未満になる。
             */
            interpolation =
                accumulator / FIXED_DELTA_TIME;
    
            // 裏画面を黒で消す
            ClearDrawScreen();
    
            // ゲーム画面を描画する
            GameDraw(interpolation)Draw();
    
            // 裏画面を表画面へ表示する
            if (ScreenFlip() == -1)
            {
                exitRequested = TRUE;;
        }
    
            if (exitRequested == TRUE)
            {
                break;
            }
        }
    
        // ゲームで使用したデータを後始末する
        GameFinalize();
    
        // DxLibを終了する
        DxLib_End();
    
        return 0;
    }
    

    7.4.実行して確認する

    プログラムを実行し、次のことを確認してください。

    • 800×600の黒いウィンドウが表示される
    • ウィンドウがすぐに閉じない
    • ESCキーを押すと終了する
    • ウィンドウ右上の閉じるボタンでも終了できる
    • コンパイルエラーが発生していない

    黒いウィンドウが表示されれば、ゲーム制作の準備は完了です。


    8.最初は変更しない場所

    次の処理は、ゲームループと時間管理を行う部分です。

    while (ProcessMessage() == 0)
    {
        // 時間の計測
        // 固定タイムステップによる更新
        // 画面の描画
    }
    

    HIT&BLOWを作る段階では、このメインループの中身を直接変更しません。

    主に、次の5つの関数へ処理を追加します。

    void GameInitialize(void);
    void InputUpdate(void);
    void GameUpdate(double deltaTime);
    void GameDraw(double interpolation);
    void GameFinalize(void);
    

    ゲームが変わっても、メインループはできるだけ変更せずに使用します。


    9.deltaTimeの使用例

    シューティングゲームで、右キーを押している間、自機を右へ移動させる場合は次のように書きます。

    double playerX = 100.0;
    const double PLAYER_SPEED = 240.0;
    
    void GameUpdate(double deltaTime)
    {
        if (CheckHitKey(KEY_INPUT_RIGHT) != 0)
        {
            playerX += PLAYER_SPEED * deltaTime;
        }
    }
    

    PLAYER_SPEEDは、1秒間に移動するピクセル数です。

    PLAYER_SPEED = 240.0
    

    ならば、1秒間に約240ピクセル移動します。

    移動処理を、

    playerX += PLAYER_SPEED;
    

    とは書きません。

    これでは、1回の更新ごとに240ピクセル移動してしまいます。

    playerX += PLAYER_SPEED * deltaTime;
    

    と書くことで、経過時間に応じた移動量になります。


    10.実際の経過時間を確認する

    実際の1フレームで経過した時間は、次の関数で取得できます。

    double frameDeltaTime;
    
    frameDeltaTime = GetFrameDeltaTime();
    

    実測した描画間隔やFPSを確認するときに使用できます。

    例えば、次の処理をGameDrawへ一時的に書くと、実測したFPSを表示できます。

    void GameDraw(double interpolation)
    {
        double frameDeltaTime;
        double fps;
    
        frameDeltaTime = GetFrameDeltaTime();
    
        if (frameDeltaTime > 0.0)
        {
            fps = 1.0 / frameDeltaTime;
    
            DrawFormatString(
                10,
                10,
                GetColor(255, 255, 255),
                "描画FPS:%.1f",
                fps);
        }
    
        (void)interpolation;
    }
    

    この値は、モニターのリフレッシュレートやコンピュータの処理状況によって変化します。

    ゲーム内の移動処理には、実測したframeDeltaTimeではなく、GameUpdateへ渡される固定のdeltaTimeを使用します。


    11.次のチャプターから書き足す場所

    次のチャプターでは、このテンプレートを使ってHIT&BLOWゲームを作ります。

    最初は、GameDrawへ文字を1行追加します。

    void GameDraw(double interpolation)
    {
        DrawString(
            280,
            250,
            "HIT & BLOW",
            GetColor(255, 255, 255));
    
        (void)interpolation;
    }
    

    実行すると、黒いウィンドウへHIT & BLOWと表示されます。

    その後、次の機能を少しずつ追加します。

    タイトルを表示する
            ↓
    数字の入力枠を描く
            ↓
    数字ボタンを描く
            ↓
    マウスで数字を入力する
            ↓
    HITとBLOWを判定する
            ↓
    入力履歴を表示する
            ↓
    ゲームを完成させる
    

    テンプレートを実行して黒いウィンドウが表示されれば、次のチャプターへ進んでください。