ICON公式ブログ

ICON公式ブログ

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

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

この記事は ICON Advent Calendar 2018 0日目の記事です。

はじめに

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

アドベントカレンダー2018企画は明日からなのですが、なにぶん初めての試みですから「どんな感じに書けばいいのかわからない!」という人の参考になるかと思い、この記事を企画発案者権限で無理やりねじ込み投稿しました。(プロ班長としてどうしても技術系の記事を書いておきたい!という思惑もあります)

さて、何かゲームを作ろうと思った時、「何でつくるか?」という疑問が生じると思います。その手段として、今年度のプロ班ではUnityというゲームエンジンを紹介していました。しかし、選択肢がこれだけというのもアレなので、今回は別の選択肢を紹介します。

それが、C言語 & DxLib です!!1

(2019/12/19 追記)

読者の方々を混乱させてしまう言い回しや、説明の足りなかった部分が多々あったため、修正・追記をしました。大変申し訳ございません。

また、自分で書いておいてこんな事を言うのもアレなのですが、「関数や構造体を使うとこれらの機能を知らないC言語初心者にはキツいかな」(Javaを使っている情報学部生は構造体になじみがないらしい)という偏見によって関数や構造体を使わないようにしていましたが、正直使った方が読みやすいコードになるので、vol.3では関数・構造体とC++の一部の機能を利用することにします。

対象

この記事が想定している読者

  • C言語習ったけど、コンソール画面ばっかでつまらないな...と思ってる工学部の人
  • C言語やってみたいな...と思っている情報学部の人
  • Windowsパソコン持っている人

この記事が想定していない読者

  • 長い記事を読むのがつらい人
  • Mac使い

関係の無い話は削ったり脚注に飛ばしたりしたんですが、そこそこ長いので気を付けてください。

DxLibとは

DXライブラリ(ディーエックス・ライブラリ、またはデラックス・ライブラリ)とは、2001年に山田巧がC++用に開発した、無料のコンピュータゲーム開発用ライブラリである。広義にはゲームエンジンに分類される。DxLibとも表記される。 DXライブラリ - Wikipedia

どうしたらC言語でゲームが作れるの?DXライブラリって何? - 新・ゲームプログラミングの館

(ライブラリって何?って人は ライブラリ - Wikipedia)

C, C++言語を用いてWindows用アプリケーション(特にゲーム)を開発するときに便利なライブラリだと思っていただければ。

ちなみに、DxLibはMacには対応していません。

環境整備

今年度のプロ班ではIDEとしてVisual Studioを勉強会などで導入しているので、今回はVisual Studio CommunityでDxLibを使います。

用意するもの

パソコンにVisual Studioをインストールして、C++が使える状態にしてください。やり方は検索すれば解説記事が出てきます。

次に、DxLibのダウンロードおよびVisual Studioに適用する方法ですが、まずDXライブラリの使い方解説のページの「DXライブラリ Windows版を使用される場合」の項目から自分のVisual Studioのバージョンにあったものを選んでページを開いてください。基本的にはそのページに書かれている通りに作業をすればいいです。文字ばっかりの説明がつらい人はこちら

※後日Visual Studio 2019での画像付き導入例を追記します。

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

では、早速音ゲーをつくっていきましょう!2

DxLibのおまじない

次のコードを見てください。

#include "DxLib.h"

#define WIN_W 800 //ウインドウの横幅
#define WIN_H 600 //ウインドウの縦幅

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
    SetMainWindowText("OtogeNoYounaNanika"); //ウインドウのタイトルを設定
    ChangeWindowMode(TRUE); //ウィンドウモードで起動
    SetGraphMode(WIN_W, WIN_H, 32); //画面の解像度指定
    SetWindowSizeChangeEnableFlag(FALSE); //画面サイズ変更不可
    if (DxLib_Init() == -1) { //DxLib初期化処理
        return -1; //異常終了
    }
    SetDrawScreen(DX_SCREEN_BACK); //描画先を裏画面に設定
    //while(裏画面を表画面に反映, メッセージ処理, 画面クリア)
    while (ScreenFlip() == 0 && ProcessMessage() == 0 && ClearDrawScreen() == 0) {
        //四角形を描画
        //DrawBoxAA(左上のX座標,左上のY座標,右下のX座標,右下のY座標,色,塗りつぶすかどうか)
        DrawBoxAA(300, 200, 500, 400, GetColor(0, 255, 0), TRUE);
    }
    DxLib_End(); //DxLib終了処理
    return 0; //正常終了
}

これを実行してみると、真っ黒な画面が表示されて、中央に緑色の四角形が表示されると思います。

f:id:iconcreator:20191220014357p:plain

では、上から説明していきます。

#include "DxLib.h"でDxLibをインクルードしています。これをしないとDxLibは使えません。
#define WIN_W 800 #define WIN_H 600では定数を定義しています。それぞれ、WIN_Wに800、WIN_Hに600という数値を設定しています。

int WINAPI WinMain(...はDxLibを使う場合のプログラムのエントリーポイントです。厳密には違いますが、C言語がわかる人はint main()Javaがわかる人はpublic static void main(String[] args)みたいなものだと思ってください。

あとはコード内に書かれているコメントの通りです。

さっさと音ゲーづくりに行きたいので、詳しい説明を聞きたい人は先程紹介した抹茶さんの記事か、下の記事の「DXライブラリ入門編」をご覧ください。各関数の詳しい説明はDXライブラリ 関数リファレンスページをご覧ください。

ノーツと判定ラインを描画する

今回は「上から下にノーツが落ちてくるタイプの音ゲー」をつくります。この音ゲーに最低限必要なものは、ノーツと判定ラインです。では、次のコードを見てください。

#include "DxLib.h"

#define WIN_W 800 //ウインドウの横幅
#define WIN_H 600 //ウインドウの縦幅
#define LANE_NUM 4 //レーンの数
#define BAR_NUM 40 //(画面に)同時に存在できる1レーンあたりのbarの最大数

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

    bool bar_f[LANE_NUM][BAR_NUM]; //barの存在フラグ
    float bar_y[BAR_NUM]; //barのY座標
    for (int j = 0; j < BAR_NUM; j++) {
        for (int i = 0; i < LANE_NUM; i++) {
            bar_f[i][j] = false; //全てにfalseを代入
        }
    }
    int loopCount = 0;
    int counter = 0;

    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) {
        //bar生成
        if (loopCount % 60 == 0) {
            for (int i = 0; i < LANE_NUM; i++) {
                bar_f[i][counter % BAR_NUM] = true;
                bar_y[counter % BAR_NUM] = -100.f;
            }
            counter++;
        }
        loopCount++;
        //bar座標更新
        for (int i = 0; i < LANE_NUM; i++) {
            for (int j = 0; j < BAR_NUM; j++) {
                if (bar_f[i][j]) {
                    bar_y[j] += 1.f;
                    if (bar_y[j] > WIN_H + 10) {
                        bar_f[i][j] = false; //画面外に出たらfalse
                    }
                }
            }
        }
        //判定ライン描画
        DrawLine(0, WIN_H / 5 * 4, WIN_W, WIN_H / 5 * 4, GetColor(255, 255, 255));
        //bar描画
        for (int i = 0; i < LANE_NUM; i++) {
            for (int j = 0; j < BAR_NUM; 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; //正常終了
}

これを実行すると、真っ黒な画面に白い判定ラインが引かれていて、緑色のノーツが4つずつ流れてくる様子が表示されると思います。

f:id:iconcreator:20191220014433p:plain

変数宣言している部分はだいたいコメントで書いた通りです。

//bar生成とコメントした部分では、毎フレームごとに1加算されるloopCountを60で割った余りが0のとき、つまり約1秒ごとに一列分のノーツ存在フラグをtrueにして(立たせて)、bar_yに-100を代入し、counterに1加算しています。

言い忘れていましたが、ScreenFlip()関数は自動的にモニタがリフレッシュするタイミングまで待って、リフレッシュのタイミングで表画面に反映させる関数なので、60Hzで動作しているモニタを使っていれば、待ち時間は1/60秒になります。なので、fps (frames per second)は60になるので、約1秒というわけです。

barを生成するたびにcounterが1加算されるので、counterの値はbarを生成するたびに、0,1,2, ...と無限に増えていきます。よって、counter % BAR_NUMの部分はbarを生成するたびに、0,1,2, ..., 39, 0, 1, ...と変化していきます。こうすることで、BAR_NUMが40しかなくてもずっとbarを生成し続けることができます。3

(追記 ここを「0,1,2, ..., 59, 60, 0, 1, ...」と書いていました。致命的なミスです。BAR_NUMは40なので59まで増えるわけが無いですし、そもそもたとえBAR_NUMが60だとしてもcounter % BAR_NUMは60にはなりません。ごめんなさい...)

//bar座標更新とコメントした部分では、全てのbar_fに対して存在フラグが立っているかどうか調べています。もしフラグが立っていたら、そのノーツのY座標に1を加算します。さらに、Y座標がWIN_Hを超えていたら、つまりノーツが画面外に出ていたら、そのノーツのフラグをfalseにして(折って、おろして)います。

//判定ライン描画とコメントした部分ではDrawLine()関数で判定ラインを表す線を引いています。

//bar描画とコメントした部分では、全てのbar_fに対して存在フラグが立っているかどうか調べて、フラグが立っていたらDrawBoxAA()関数でノーツを表す長方形を描いています。

ちなみに、所々に1.fとか72.fのような記述があると思いますが、これは「float型だよ~」とコンパイラ君に伝えるために書いています。72と書いてしまうとコンパイラ君に「int型だな」と思われてしまいますし、72.0と書いてしまうと「double型だな」と思われてしまいます。なので、明示的に72f 72.f 72.0fなどと書かなければなりません。4

テンポを合わせられるようにする

さて、ノーツは降ってくるようになりましたが、音ゲーは曲のリズムに合わせてノーツを叩くゲーム(叩かないものもありますが...)なので、約1秒ごとに降ってくる、なんて曖昧なものはだめなのです。

あれ?じゃあbpmっていう変数をつくって、これに曲のbpm (beats per minute) を代入して、loopCount % 60 == 0のところをloopCount % (60 * 60 / bpm) == 0ってすればいいんじゃないの?

こう思う人もいらっしゃると思います。
ですが、この方法は60fpsが固定されているという前提のもとに成り立っています。

わたしが先程から「約1秒ごと」なんて表現をしているように、実際はわずかなズレが生じます。それに、処理能力の低いパソコンで実行したときは60fpsなんて維持できません。まーズレるズレる。
ズレていく様子が見てみたい人は、活動日に私に話しかけてきてください。高校時代に60fps前提でつくった音ゲーで実演します。

じゃあどうするの?

実際の時間を取得するんです!!!!

次のコードを見てください。

#include "DxLib.h"

#define WIN_W 800 //ウインドウの横幅
#define WIN_H 600 //ウインドウの縦幅
#define LANE_NUM 4 //レーンの数
#define BAR_NUM 40 //(画面に)同時に存在できる1レーンあたりのbarの最大数

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

    bool bar_f[LANE_NUM][BAR_NUM]; //barの存在フラグ
    float bar_y[BAR_NUM]; //barのY座標
    for (int j = 0; j < BAR_NUM; j++) {
        for (int i = 0; i < LANE_NUM; 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 (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_NUM; i++) {
                bar_f[i][counter % BAR_NUM] = true;
                bar_y[counter % BAR_NUM] = -100.f;
            }
            counter++;
        }
        //bar座標更新
        for (int i = 0; i < LANE_NUM; i++) {
            for (int j = 0; j < BAR_NUM; j++) {
                if (bar_f[i][j]) {
                    bar_y[j] += 1.f;
                    if (bar_y[j] > WIN_H + 10) {
                        bar_f[i][j] = false; //画面外に出たらfalse
                    }
                }
            }
        }
        //判定ライン描画
        DrawLine(0, WIN_H / 5 * 4, WIN_W, WIN_H / 5 * 4, GetColor(255, 255, 255));
        //bar描画
        for (int i = 0; i < LANE_NUM; i++) {
            for (int j = 0; j < BAR_NUM; 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:20191220014800p:plain

GetNowHiPerformanceCount()関数は、Windowsが起動してから経過した時間をマイクロ秒単位で表した値を返してくれます。マイクロ秒単位なので精度は非常に良いですが、その分返ってくる値が大きくなってしまい、普通のint型では桁が足りないため、int型の2倍の桁を持つLONGLONG型で受け取っています。

//時間関係とコメントした部分では、counterが0のとき、つまりループの最初にstart_timeを取得し、毎フレームごとに時間を取得しているnow_timeからstart_timeを引いて、それを1000で割ることでミリ秒単位になおしてint型にキャストしています。

また、ノーツを生成している部分のif文の条件式がcurrentTime >= 60000 / bpm * counterに変っています。
例えば、今回のようにbpmが120だった場合、1秒間に2個ノーツが落ちてくる、つまり1個あたり0.5秒なのです。つまり「60秒 / bpm = 1個あたりの時間(秒)」です。しかしcurrentTimeはミリ秒単位なので5、ノーツとノーツの間隔は「60000ミリ秒 / bpm = 1個あたりの時間(ミリ秒)」なのです。これはbpmが60でも180でも同じです。

ぜひbpmの値を変えて実験してみてください。

おわりに

まだ判定も、キーでノーツを叩く部分もできていませんが、締め切りが迫ってきたのでここで一旦休憩とします。「はじめに」にも書いたように、この記事は「どんな風に書けばいいのかわからない!」という人に向けたものでもある以上、遅れるわけにはいきません。この続きはこのブログか、私のブログで投稿するので許してつかあさい...

to be continued ...

続きはこちら


  1. 他にも、私の知っている範囲では、DxLibと同じくC++ライブラリのSiv3D、ゲームエンジンUnreal Engineなどがあります。もちろんHSPでも出来ますし、JavaにはGUIを扱うJavaFXなるものがあるらしいです。そんな中でDxLibを選ぶのは時代に逆行している感がぬぐえませんが、C++&DxLibでゲームをつくるのは高校時代の私の憧れであったことを思い出したのでDxLibを選びました。単純にC, C++言語の勉強のためでもあります。

  2. なぜ音ゲーなのか:私は高校時代にHSPという言語で音ゲーをつくっていました。しかし、完全に独学だったので、判定がズレまくったり、一定時間フリーズしたりといった致命的な欠陥を抱えたゲームが出来上がってしまいました。おまけにソースコードは大量のグローバル変数と大量のラベルとgoto文によって手の付けられない闇と化してしまいました。その後受験期に入ってしまったこともあって、長らくその闇を放置していましたが、最近ふと「私がこれまで個人でつくったゲームの中でまともなものってあの音ゲーだけでは…?」ということに気づいてしまいました。噓です。結構前から気づいていました。丁度良い機会なので、音ゲーを書き直しながらついでに記事にしようと思ったのです。いや文章長いな。

  3. 試しにBAR_NUMを1か2にしてみてください。すると、上から流れてきたノーツが途中で消えると思います。これは、ノーツが流れている途中に//Bar生成の部分でbar_yの値が-100.fに書き換えられるためです。つまり、BAR_NUMは「(画面に)同時に存在できる1レーンあたりのBarの最大数」になります。そういう意味でコメントを書きましたが、確かに分かりづらいですね… (上手く説明できていない気もする…)

  4. なぜ1.fという書き方を選んだのか:プログラマのねねっち(NEWGAME)をリスペクトしてこの書き方をしています。まあ要は個人的な信条によるものです。パフォーマンスがいいかどうかは知りません。

  5. 今気づきましたが、わざわざint型にすることで精度が落ちているし、ミリ秒単位ならGetNowCount()関数で十分だったような…