ICON公式ブログ

ICON公式ブログ

静岡大学コンテンツ制作サークルICONの公式ブログです

C言語とDxLibで音ゲーのような何かをつくってみるvol.2

はじめに

お久しぶりです、プロ班の 杞憂 です。お久しぶりではない方は初めまして。

さて、タイトルからもわかる通り、この記事は、締め切りに間に合わせるために途中で終わらせてしまったアドカレ 0日目の記事の続きです。

前回の記事はこちら

年明けに公開します!と言っておきながら2月になってしまいました。
続編の公開が遅くなってしまい申し訳ありません...

では参りましょう。

※ほとんどコードではありますが、この記事も結構長いので注意してください。約19000文字あります。

音ゲーのような何かをつくってみるvol.2

キーでノーツを叩けるようにする

今までのプログラムでは、ノーツなどが表示されたウインドウをただ見守るだけでした。次は、キー入力をしてみたいと思います。

その前に、キー入力の取得について簡単なプログラムで実験してみます。次のコードを見てください。

コードを見る

#include "DxLib.h"

#define WIN_W 800 //ウインドウの横幅
#define WIN_H 600 //ウインドウの縦幅
#define LANE_SIZE 4 //レーンの数

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

    char buf[256]; //キー押下状態格納用配列
    int key[LANE_SIZE] = {
        KEY_INPUT_Z,
        KEY_INPUT_X,
        KEY_INPUT_C,
        KEY_INPUT_V
    };

    SetMainWindowText("OtogeNoYounaNanika"); //ウインドウのタイトルを設定
    ChangeWindowMode(TRUE); //ウィンドウモードで起動
    SetGraphMode(WIN_W, WIN_H, 32); //画面の解像度指定
    SetWindowSizeChangeEnableFlag(FALSE); //画面サイズ変更不可
    if (DxLib_Init() == -1) { return -1; } //DxLib初期化処理
    SetDrawScreen(DX_SCREEN_BACK); //描画先を裏画面に設定

    //while(裏画面を表画面に反映, メッセージ処理, 画面クリア)
    while (ScreenFlip() == 0 && ProcessMessage() == 0 && ClearDrawScreen() == 0) {
        GetHitKeyStateAll(buf); //キー押下状態取得
        if (buf[KEY_INPUT_ESCAPE] == 1) {
            break; //ループを抜ける
        }
        //描画
        for (int i = 0; i < LANE_SIZE; i++) {
            if (buf[key[i]] == 1) { //該当キーが押されていたら
                DrawBoxAA(100 + i * 100, 100, 100 + 50 + i * 100, 150, GetColor(0, 255, 0), TRUE);
            }
        }
    }
    DxLib_End(); //DxLibの終了処理
    return 0; //正常終了
}

これを実行すると、Z、X、C、Vを押したときに緑色の正方形が表示されると思います。キーによって正方形が表示される位置が違うことに注目してください。この正方形の描画は//描画とコメントした部分でやっています。

f:id:iconcreator:20190118230205p:plain (キーボードの Z, C, V キーを押したときの様子)

では、キー入力の部分についての説明です。キー入力に関しては基本的にGetHitKeyStateAll()関数を利用します。他にもキー入力を取得する関数はありますが、これが一番使い勝手が良いので私はこれを使っています。この関数のかっこの中、つまり引数にchar型の長さ256個の配列を与える(入れる)と、その配列に、関数が呼ばれた時点での全てのキーの押下状態が格納されます。
また、欲しいキーに該当する定数(KEY_INPUT_ESCAPEやKEY_INPUT_Zなど)をbuf[KEY_INPUT_ESCAPE]のように配列の添え字に当てはめれば、そのキーの押下状態が確認できます。

GetHitKeyStateAll()関数の引数は、正確にはchar型配列のポインタです。ポインタの説明が面倒なのであんな表現でお茶を濁しています。

key[LANE_SIZE]という配列変数にZ, X, C, Vキーに対応する定数を順番に入れることで、ループで利用しやすくしています。
また、今まではウインドウの右上の「✕」を押して終了していましたが、今回はif (buf[KEY_INPUT_ESCAPE] == 1) { break; //ループを抜ける }の部分のおかげでESCAPEキーで終了できるようになっています。

では、これを本筋のコードに応用してみます。次のコードを見てください。

コードを見る

#include "DxLib.h"

#define WIN_W 800 //ウインドウの横幅
#define WIN_H 600 //ウインドウの縦幅
#define LANE_SIZE 4 //レーンの数
#define BAR_SIZE 40 //1レーンあたりの最大bar数

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

    char buf[256]; //キー押下状態格納用配列
    int key[LANE_SIZE] = {
        KEY_INPUT_Z,
        KEY_INPUT_X,
        KEY_INPUT_C,
        KEY_INPUT_V
    };

    bool bar_f[LANE_SIZE][BAR_SIZE]; //barの存在フラグ
    float bar_y[BAR_SIZE]; //barのY座標
    for (int j = 0; j < BAR_SIZE; j++) {
        for (int i = 0; i < LANE_SIZE; i++) {
            bar_f[i][j] = false; //全てにfalseを代入
        }
    }
    LONGLONG start_time; //開始した時刻
    LONGLONG now_time; //現在のフレームの時刻
    int currentTime; //開始してからの経過時間
    int counter = 0;
    const int bpm = 120;

    SetMainWindowText("OtogeNoYounaNanika"); //ウインドウのタイトルを設定
    ChangeWindowMode(TRUE); //ウィンドウモードで起動
    SetGraphMode(WIN_W, WIN_H, 32); //画面の解像度指定
    SetWindowSizeChangeEnableFlag(FALSE); //画面サイズ変更不可
    if (DxLib_Init() == -1) { return -1; } //DxLib初期化処理
    SetDrawScreen(DX_SCREEN_BACK); //描画先を裏画面に設定

    //while(裏画面を表画面に反映, メッセージ処理, 画面クリア)
    while (ScreenFlip() == 0 && ProcessMessage() == 0 && ClearDrawScreen() == 0) {
        if (buf[KEY_INPUT_ESCAPE] == 1) {
            break; // 終了
        }
        //時間関係
        if (counter == 0) {
            start_time = GetNowHiPerformanceCount();
        }
        now_time = GetNowHiPerformanceCount();
        currentTime = (int)((now_time - start_time) / 1000); // ms
        //bar生成
        if (currentTime >= 60000 / bpm * counter) {
            for (int i = 0; i < LANE_SIZE; i++) {
                bar_f[i][counter % BAR_SIZE] = true;
                bar_y[counter % BAR_SIZE] = -100.f;
            }
            counter++;
        }
        //bar座標更新
        for (int i = 0; i < LANE_SIZE; i++) {
            for (int j = 0; j < BAR_SIZE; j++) {
                if (bar_f[i][j]) {
                    bar_y[j] += 1.f;
                    if (bar_y[j] > WIN_H + 10) {
                        bar_f[i][j] = false; //画面外に出たらfalse
                    }
                }
            }
        }
        // 判定
        GetHitKeyStateAll(buf);
        for (int i = 0; i < LANE_SIZE; i++) {
            for (int j = 0; j < BAR_SIZE; j++) {
                if (bar_f[i][j]) {
                    if (WIN_H / 5 * 4 - 30 < bar_y[j] && bar_y[j] < WIN_H / 5 * 4 + 30) { //barのY座標が判定ライン±30なら
                        if (buf[key[i]] == 1) {
                            bar_f[i][j] = false;
                        }
                    }
                }
            }
        }
        //判定ライン描画
        DrawLine(0, WIN_H / 5 * 4, WIN_W, WIN_H / 5 * 4, GetColor(255, 255, 255));
        //bar描画
        for (int i = 0; i < LANE_SIZE; i++) {
            for (int j = 0; j < BAR_SIZE; j++) {
                if (bar_f[i][j]) {
                    DrawBoxAA(100.f + i * 150.f, bar_y[j] - 10.f,
                        100.f + 72.f + i * 150.f, bar_y[j] + 10.f, GetColor(0, 255, 0), TRUE);
                }
            }
        }
    }
    DxLib_End(); //DxLibの終了処理
    return 0; //正常終了
}

※コードを全文載せると長くなるから良くないかな~、と思いましたが、(略)とか書くとコピペしづらいのでこの記事ではコードは全文載せることにしました。

f:id:iconcreator:20190119001322p:plain

これを実行してください。緑色のノーツが判定ライン付近まで落ちてきたときにZ, X, C, Vを押すと、そのキーに対応したレーンのノーツが消えると思います。これは//判定とコメントした部分で行っています。

発覚したバグを直す

さて、上の画像を見て、あるいはコピペして実行してみて、違和感を覚えた方もいるのではないでしょうか。1つのノーツを消すと他の3つのノーツの落ちる速度が少し遅くなり、2つのノーツを消すと残りの2つのノーツが遅くなり、3つのノーツを消すと残りのノーツがとても遅くなる...という現象が起きています。もちろんこんな挙動は期待していませんので、コードが間違っているということです。

プログラムは思った通りには動かない。書いたとおりに動くのだ。

とあるプログラマの格言です。軽く検索しましたが、誰が言った言葉なのか出てこなかったので恐らく詠み人知らずなのでしょう。
聡い方は既に気づいていらっしゃったかもしれませんが、

//bar座標更新
for (int i = 0; i < LANE_SIZE; i++) {
    for (int j = 0; j < BAR_SIZE; j++) {
        if (bar_f[i][j]) {
            bar_y[j] += 1.f;
            if (bar_y[j] > WIN_H + 10) {
                bar_f[i][j] = false; //画面外に出たらfalse
            }
        }
    }
}

ここでやらかしています。1

さて、このバグは「同じ行の4つのノーツのY座標は共有できるよねー」という発想でbar_y[]配列を1次元にしたにもかかわらず、forループでレーン毎に1ずつ足している(4レーンあるので+4している)ことが原因です。結果、その行のノーツが減っていけば足す回数も減っていくためにあんな挙動をしていたのです。
このバグの修正ですが、私が思いついたのは、

①4回も無駄にループを回しているので、内側のループから出す。

//bar座標更新
for (int j = 0; j < BAR_SIZE; j++) {
    bool flag = false; //フラグ
    for (int i = 0; i < LANE_SIZE; i++) {
        if (bar_f[i][j]) {
            if (bar_y[j] > WIN_H + 10) {
                bar_f[i][j] = false; //画面外に出たらfalse
            }
            flag = true; //ノーツが存在したらフラグを立てる
        }
    }
    if (flag) { //flagがtrue つまりその行には少なくともノーツが1つは存在する
        bar_y[j] += 4.f;
    }
}

②大人しく配列変数bar_yを2次元配列にする。

の2つです。①の方が修正箇所が少なくて済みますが、プログラムが小さいためまだ間に合うこと、今後レーンごとにノーツの落下速度を変える変態じみた仕様を作るかもしれないことから、②の方法にします。

修正したコードがこちらです。

コードを見る

#include "DxLib.h"

#define WIN_W 800 //ウインドウの横幅
#define WIN_H 600 //ウインドウの縦幅
#define LANE_SIZE 4 //レーンの数
#define BAR_SIZE 40 //1レーンあたりの最大bar数

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

    char buf[256]; //キー押下状態格納用配列
    int key[LANE_SIZE] = {
        KEY_INPUT_Z,
        KEY_INPUT_X,
        KEY_INPUT_C,
        KEY_INPUT_V
    };

    bool bar_f[LANE_SIZE][BAR_SIZE]; //barの存在フラグ
    float bar_y[LANE_SIZE][BAR_SIZE]; //barのY座標
    for (int j = 0; j < BAR_SIZE; j++) {
        for (int i = 0; i < LANE_SIZE; i++) {
            bar_f[i][j] = false; //全てにfalseを代入
        }
    }
    LONGLONG start_time; //開始した時刻
    LONGLONG now_time; //現在のフレームの時刻
    int currentTime; //開始してからの経過時間
    int counter = 0;
    const int bpm = 120;

    SetMainWindowText("OtogeNoYounaNanika"); //ウインドウのタイトルを設定
    ChangeWindowMode(TRUE); //ウィンドウモードで起動
    SetGraphMode(WIN_W, WIN_H, 32); //画面の解像度指定
    SetWindowSizeChangeEnableFlag(FALSE); //画面サイズ変更不可
    if (DxLib_Init() == -1) { return -1; } //DxLib初期化処理
    SetDrawScreen(DX_SCREEN_BACK); //描画先を裏画面に設定

    //while(裏画面を表画面に反映, メッセージ処理, 画面クリア)
    while (ScreenFlip() == 0 && ProcessMessage() == 0 && ClearDrawScreen() == 0) {
        if (buf[KEY_INPUT_ESCAPE] == 1) {
            break; // 終了
        }
        //時間関係
        if (counter == 0) {
            start_time = GetNowHiPerformanceCount();
        }
        now_time = GetNowHiPerformanceCount();
        currentTime = (int)((now_time - start_time) / 1000); // ms
        //bar生成
        if (currentTime >= 60000 / bpm * counter) {
            for (int i = 0; i < LANE_SIZE; i++) {
                bar_f[i][counter % BAR_SIZE] = true;
                bar_y[i][counter % BAR_SIZE] = -100.f;
            }
            counter++;
        }
        //bar座標更新
        for (int i = 0; i < LANE_SIZE; i++) {
            for (int j = 0; j < BAR_SIZE; j++) {
                if (bar_f[i][j]) {
                    bar_y[i][j] += 4.f;
                    if (bar_y[i][j] > WIN_H + 10) {
                        bar_f[i][j] = false; //画面外に出たらfalse
                    }
                }
            }
        }
        // 判定
        GetHitKeyStateAll(buf);
        for (int i = 0; i < LANE_SIZE; i++) {
            for (int j = 0; j < BAR_SIZE; j++) {
                if (bar_f[i][j]) {
                    if (WIN_H / 5 * 4 - 30 < bar_y[i][j] && bar_y[i][j] < WIN_H / 5 * 4 + 30) {
                        if (buf[key[i]] == 1) {
                            bar_f[i][j] = false;
                        }
                    }
                }
            }
        }
        //判定ライン描画
        DrawLine(0, WIN_H / 5 * 4, WIN_W, WIN_H / 5 * 4, GetColor(255, 255, 255));
        //bar描画
        for (int i = 0; i < LANE_SIZE; i++) {
            for (int j = 0; j < BAR_SIZE; j++) {
                if (bar_f[i][j]) {
                    DrawBoxAA(100.f + i * 150.f, bar_y[i][j] - 10.f,
                        100.f + 72.f + i * 150.f, bar_y[i][j] + 10.f, GetColor(0, 255, 0), TRUE);
                }
            }
        }
    }
    DxLib_End(); //DxLibの終了処理
    return 0; //正常終了
}

f:id:iconcreator:20190119110701p:plain

やったね!!!

ちなみに、bar_y[i][j] += 4.f;bar_y[i][j] += 1 + (float)i;として実行してみてください。なんか気持ち悪い挙動をすると思います。こんなことも出来るようになります。怒られるので、まず実装することはないと思いますが...

f:id:iconcreator:20190119111152p:plain

曲を流す

ゲームに欠かせない要素の一つ、曲(BGM)を流してみたいと思います。そもそも音ゲーのような何か、なのですから音楽が流れていないと意味がありません。次のコードを見てください。

(略)
if (DxLib_Init() == -1) { return -1; } //DxLib初期化処理
SetDrawScreen(DX_SCREEN_BACK); //描画先を裏画面に設定

int bgmHandle = LoadSoundMem("音楽ファイルのパス");

//while(裏画面を表画面に反映, メッセージ処理, 画面クリア)
while (ScreenFlip() == 0 && ProcessMessage() == 0 && ClearDrawScreen() == 0) {
    if (buf[KEY_INPUT_ESCAPE] == 1) {
        break; // 終了
    }
    //時間関係
    if (counter == 0) {
        PlaySoundMem(bgmHandle, DX_PLAYTYPE_BACK);
        start_time = GetNowHiPerformanceCount();
    }
    now_time = GetNowHiPerformanceCount();
    currentTime = (int)((now_time - start_time) / 1000); // ms
    (以下略)

あまりにも変更箇所が少なかったので、コード中の変更箇所のみお見せしています。上から5行目のint bgmHandle = LoadSoundMem("音ファイルのパス"); と、下から6行目のPlaySoundMem(bgmHandle, DX_PLAYTYPE_BACK);を追加しました。"音ファイルのパス"の部分にはお好きな曲を入れてください。私も好きな曲を入れていますが、著作権の関係でここには書けません。

ここで、LoadSoundMem()関数とPlaySoundMem()関数について見ていきましょう。LoadSoundMem()関数では、与えられた文字列で指定された音ファイルを読み込み、読み込んだ音の識別番号(サウンドハンドル)を返します。PlaySoundMem()関数は、与えられた識別番号をもとに音ファイルを再生します。ちなみに、第2引数は再生形式を指定するもので、今回はバックグラウンド再生を指定しています。
※詳細はDXライブラリのリファレンスページを見てください。

上から5行目ではbgmHandleという名前のint型の変数にLoadSoundMem()関数から返ってきたサウンドハンドルを入れて(変数を初期化して)います。この値を利用してPlaySoundMem()関数で曲を流しています。

ノーツが判定ラインに到達するタイミングを曲に合わせる

前回の記事で、ノーツの出現タイミングをbpmに合わせられるようにしました。しかし、ノーツの落下(移動)は1フレームあたり4としているので、判定ラインに何秒後に来るのかわかりません。2fpsが固定されているならともかく、実際はどんどんズレていきます。そこで、今回は、ノーツが判定ラインに到達するタイミングを曲に合わせてみたいと思います。こちらの記事(スライド)をご覧ください。(説明するのが難しかったので丸投げ)

この考え方をもとに修正したコードがこちらです。

コードを見る

#include "DxLib.h"

#define WIN_W 800 //ウインドウの横幅
#define WIN_H 600 //ウインドウの縦幅
#define LANE_SIZE 4 //レーンの数
#define BAR_SIZE 40 //1レーンあたりの最大bar数

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

    char buf[256]; //キー押下状態格納用配列
    int key[LANE_SIZE] = {
        KEY_INPUT_Z,
        KEY_INPUT_X,
        KEY_INPUT_C,
        KEY_INPUT_V
    };

    bool bar_f[LANE_SIZE][BAR_SIZE]; //barの存在フラグ
    float bar_y[LANE_SIZE][BAR_SIZE]; //barのY座標
    int bar_c[BAR_SIZE]; //barが判定ラインに到達すべき時刻
    for (int j = 0; j < BAR_SIZE; j++) {
        for (int i = 0; i < LANE_SIZE; i++) {
            bar_f[i][j] = false; //全てにfalseを代入
        }
    }
    LONGLONG start_time; //開始した時刻
    LONGLONG now_time; //現在のフレームの時刻
    int currentTime; //開始してからの経過時間
    int counter = 0;
    const int bpm = 120;
    const float speed = 3.f;
    const int offset = 0; // ms

    SetMainWindowText("OtogeNoYounaNanika"); //ウインドウのタイトルを設定
    ChangeWindowMode(TRUE); //ウィンドウモードで起動
    SetGraphMode(WIN_W, WIN_H, 32); //画面の解像度指定
    SetWindowSizeChangeEnableFlag(FALSE); //画面サイズ変更不可
    if (DxLib_Init() == -1) { return -1; } //DxLib初期化処理
    SetDrawScreen(DX_SCREEN_BACK); //描画先を裏画面に設定

    int bgmHandle = LoadSoundMem("音ファイルのパス");

    //while(裏画面を表画面に反映, メッセージ処理, 画面クリア)
    while (ScreenFlip() == 0 && ProcessMessage() == 0 && ClearDrawScreen() == 0) {
        if (buf[KEY_INPUT_ESCAPE] == 1) {
            break; // 終了
        }
        //時間関係
        if (counter == 0) {
            PlaySoundMem(bgmHandle, DX_PLAYTYPE_BACK);
            start_time = GetNowHiPerformanceCount();
        }
        now_time = GetNowHiPerformanceCount();
        currentTime = (int)((now_time - start_time) / 1000) - offset; // ms

        //bar生成
        while (currentTime >= (60000 / bpm * counter) - 3000) {
            for (int i = 0; i < LANE_SIZE; i++) {
                bar_f[i][counter % BAR_SIZE] = true;
                bar_c[counter % BAR_SIZE] = 60000 / bpm * counter;
            }
            counter++;
        }
        //bar座標更新
        for (int i = 0; i < LANE_SIZE; i++) {
            for (int j = 0; j < BAR_SIZE; j++) {
                if (bar_f[i][j]) {
                    bar_y[i][j] = (currentTime - bar_c[j]) / 5 * speed + WIN_H / 5 * 4;
                    if (bar_y[i][j] > WIN_H + 10) {
                        bar_f[i][j] = false; //画面外に出たらfalse
                    }
                }
            }
        }
        // 判定
        GetHitKeyStateAll(buf);
        for (int i = 0; i < LANE_SIZE; i++) {
            for (int j = 0; j < BAR_SIZE; j++) {
                if (bar_f[i][j]) {
                    if (WIN_H / 5 * 4 - 30 < bar_y[i][j] && bar_y[i][j] < WIN_H / 5 * 4 + 30) {
                        if (buf[key[i]] == 1) {
                            bar_f[i][j] = false;
                        }
                    }
                }
            }
        }
        //判定ライン描画
        DrawLine(0, WIN_H / 5 * 4, WIN_W, WIN_H / 5 * 4, GetColor(255, 255, 255));
        //bar描画
        for (int i = 0; i < LANE_SIZE; i++) {
            for (int j = 0; j < BAR_SIZE; j++) {
                if (bar_f[i][j]) {
                    DrawBoxAA(100.f + i * 150.f, bar_y[i][j] - 10.f,
                        100.f + 72.f + i * 150.f, bar_y[i][j] + 10.f, GetColor(0, 255, 0), TRUE);
                }
            }
        }
    }
    DxLib_End(); //DxLibの終了処理
    return 0; //正常終了
}

//bar座標更新bar_y[i][j] = (currentTime - bar_c[j]) / 5 * speed + WIN_H / 5 * 4;を見てください。bar_c[]にはノーツが判定ラインに到達すべき時刻が入っています。もし現在の時刻(currentTime)がbar_cに入っている時刻と全く同じだった場合、bar_yの値はWIN_H/5*4、つまり判定ラインのY座標と同じになります。(ちなみに、speedの前の/5はspeedの値を調整するためのものなので、本筋とは全く関係ありません。)

ノーツは、ノーツが判定ラインに到達すべき時刻よりも早く出現しなければならないため、//bar生成の部分ではwhile (currentTime >= (60000 / bpm * counter) - 3000)と、3000を引いています。(つまり3秒前にノーツを出現させています。) if文がwhile文に代わっているのは、最初の幾つかのノーツは同時に出現させる必要があるからです。(伝わってます?説明が下手ですみません...) そして、bar_cにそのノーツが判定ラインに到達すべき時刻を入れています。

//時間関係の部分の最後の行に- offsetという記述を追加しています。今はoffsetの値を0にしていますが、この値を1000や2000などにすると、ノーツが出現する時刻が1秒後、2秒後などと遅れていきます。少しわかりづらいところで、上手く説明できないのですが、とにかくcurrentTimeの値を減らすとノーツの出現時刻が遅れます。
百聞は一見に如かず、とりあえずconst int offset = 0;を1000にして実行してみてください。起動してから1秒後にノーツが判定ラインに到達する様子が確認できたと思います。

では、今度はconst int offset = 0;を4000にして実行してみてください。1000、2000、3000と同様に、起動してから4秒後にノーツが判定ラインに到達する様子が確認でき...

ません!!!
待てど暮らせどノーツは降ってまいりません!!!!!

さて、何が原因でしょうか?ここに私はしばらくハマりました。せっかくなので読者の皆様も何が原因なのか考えてみてください。
ヒントは「思い込みを捨てること」です。コードは書いたとおりにしか動きません!

はい、それでは答え合わせに参りたいと思います~!
※酔ってません。

原因は//時間関係の部分にあります。counterの値は最初0です。今までは最初のフレームで//bar生成の部分で+1されるので問題ありませんでしたが、offsetが3000を超えるとcurrentTime >= (60000 / bpm * counter) - 3000の条件を満たさなくなり、counterは最初のフレームで+1されません。すると、次のフレームでも//時間関係の部分でstart_timeが更新され、いつまで経ってもノーツが出現しないという現象が起きます。PlaySoundMem()関数を何度も繰り返したため音楽も流れませんでした。

これを回避する方法はいくつかありますが、一番ラクなのはint counter = 0;int counter = -1;にして、//時間関係の部分を

if (counter == -1) {
    PlaySoundMem(bgmHandle, DX_PLAYTYPE_BACK);
    start_time = GetNowHiPerformanceCount();
    counter = 0;
}

とすることだと思います。

無事、4000以降も期待通りの動作をするようになりました。

判定を時間依存にする

今までは、「判定ラインの上下30」を範囲としてきました。これは先程紹介した記事(スライド)の「フレーム依存」に相当すると思います。(厳密には違いますが...) しかし、(読者の皆様も薄々感じていらっしゃるかもしれませんが、) 私はフレームを全く信用していないので、時間依存に変更したいと思います。(ノーツも時間依存だし統一したいという思惑もあります。)

この変更はすぐにできます。//判定の部分を

if (bar_f[i][j]) {
    if (-200 < currentTime - bar_c[j] && currentTime - bar_c[j] < 200) {
        if (buf[key[i]] == 1) {
            bar_f[i][j] = false;
        }
    }
}

に変えれば良いです。-200と200はテキトーに設定した値なので、お好みで調節してください。

さいごに

まだ譜面とか出来ていませんが、記事が長くなり過ぎたので続きはvol.3に持ち越しとしたいと思います。一応曲とノーツを合わせることが出来たので許してください。
ここまで読んでいただきありがとうございました。最後に全文載せておきます。

コードを見る

#include "DxLib.h"

#define WIN_W 800 //ウインドウの横幅
#define WIN_H 600 //ウインドウの縦幅
#define LANE_SIZE 4 //レーンの数
#define BAR_SIZE 40 //1レーンあたりの最大bar数

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

    char buf[256]; //キー押下状態格納用配列
    int key[LANE_SIZE] = {
        KEY_INPUT_Z,
        KEY_INPUT_X,
        KEY_INPUT_C,
        KEY_INPUT_V
    };

    bool bar_f[LANE_SIZE][BAR_SIZE]; //barの存在フラグ
    float bar_y[LANE_SIZE][BAR_SIZE]; //barのY座標
    int bar_c[BAR_SIZE]; //barが判定ラインに到達すべき時刻
    for (int j = 0; j < BAR_SIZE; j++) {
        for (int i = 0; i < LANE_SIZE; i++) {
            bar_f[i][j] = false; //全てにfalseを代入
        }
    }
    LONGLONG start_time; //開始した時刻
    LONGLONG now_time; //現在のフレームの時刻
    int currentTime; //開始してからの経過時間
    int counter = -1;
    const int bpm = 120;
    const float speed = 3.f;
    const int offset = 4000; // ms

    SetMainWindowText("OtogeNoYounaNanika"); //ウインドウのタイトルを設定
    ChangeWindowMode(TRUE); //ウィンドウモードで起動
    SetGraphMode(WIN_W, WIN_H, 32); //画面の解像度指定
    SetWindowSizeChangeEnableFlag(FALSE); //画面サイズ変更不可
    if (DxLib_Init() == -1) { return -1; } //DxLib初期化処理
    SetDrawScreen(DX_SCREEN_BACK); //描画先を裏画面に設定

    int bgmHandle = LoadSoundMem("音ファイルのパス");

    //while(裏画面を表画面に反映, メッセージ処理, 画面クリア)
    while (ScreenFlip() == 0 && ProcessMessage() == 0 && ClearDrawScreen() == 0) {
        if (buf[KEY_INPUT_ESCAPE] == 1) {
            break; // 終了
        }
        //時間関係
        if (counter == -1) {
            PlaySoundMem(bgmHandle, DX_PLAYTYPE_BACK);
            start_time = GetNowHiPerformanceCount();
            counter = 0;
        }
        now_time = GetNowHiPerformanceCount();
        currentTime = (int)((now_time - start_time) / 1000) - offset; // ms
                                                             //bar生成
        while (currentTime >= (60000 / bpm * counter) - 3000) {
            for (int i = 0; i < LANE_SIZE; i++) {
                bar_f[i][counter % BAR_SIZE] = true;
                bar_c[counter % BAR_SIZE] = 60000 / bpm * counter;
            }
            counter++;
        }
        //bar座標更新
        for (int i = 0; i < LANE_SIZE; i++) {
            for (int j = 0; j < BAR_SIZE; j++) {
                if (bar_f[i][j]) {
                    bar_y[i][j] = (currentTime - bar_c[j]) / 5 * speed + WIN_H / 5 * 4;
                    if (bar_y[i][j] > WIN_H + 10) {
                        bar_f[i][j] = false; //画面外に出たらfalse
                    }
                }
            }
        }
        // 判定
        GetHitKeyStateAll(buf);
        for (int i = 0; i < LANE_SIZE; i++) {
            for (int j = 0; j < BAR_SIZE; j++) {
                if (bar_f[i][j]) {
                    if (-200 < currentTime - bar_c[j] && currentTime - bar_c[j] < 200) {
                        if (buf[key[i]] == 1) {
                            bar_f[i][j] = false;
                        }
                    }
                }
            }
        }
        //判定ライン描画
        DrawLine(0, WIN_H / 5 * 4, WIN_W, WIN_H / 5 * 4, GetColor(255, 255, 255));
        //bar描画
        for (int i = 0; i < LANE_SIZE; i++) {
            for (int j = 0; j < BAR_SIZE; j++) {
                if (bar_f[i][j]) {
                    DrawBoxAA(100.f + i * 150.f, bar_y[i][j] - 10.f,
                        100.f + 72.f + i * 150.f, bar_y[i][j] + 10.f, GetColor(0, 255, 0), TRUE);
                }
            }
        }
    }
    DxLib_End(); //DxLibの終了処理
    return 0; //正常終了
}

続き(?)はこちら


  1. お恥ずかしい限りなのですが、このような「一見問題なく動いているけれど、後から見つかるバグ」は高校時代からの付き合いなのです。「ちょっ、お前(プログラム)よく今まで動いてたな!」という気持ちですが、実際に動いていたのでもうどうしようもないですね…今気づけて良かった…みなさまもお気を付けください…

  2. 一応計算すればわかります。例えば、ノーツが出現するY座標は-100で、判定ラインのY座標がWIN_H/5*4=480なので、ノーツは580/4=145フレームで判定ラインに到達します。60fpsと仮定すると145/60=2.4166…秒後に判定ラインに到達するとわかります。つまり、2.4166…秒を考慮してノーツを出現させれば良いような気がします。しかし、小数点以下の誤差の影響が怖いですし、あまり美しくないので、この方法は採用しませんでした。