チャチャチャおもちゃの抹茶っちゃ

ゲームのこととかプログラミングのこととか。気が向いたら書く。ブログタイトルは友人が考えました。

【DxLib】Visual Studio とDxLibを用いてゲームを制作する【入門編】

目次

この記事の対象

  • C言語の基礎をやり終えてゲーム作ってみたいなって人
  • Windowsユーザー

DxLibとは

DXライブラリ - Wikipedia

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

要はC言語を用いてWindows用アプリケーションを開発するときに便利なライブラリである。1から作ろうとするとそれはもう大変な中、これを使うことで簡単にゲーム制作に着手することができ、学習もしやすい(と思う)。

PSVitaPS4にも対応しているらしく、最近の話でいうと今年(2018年)の6月にはNintendo Switchにも対応したらしい(よく知らない)。2001年からあるものなのに未だに更新続いてるって考えるとすごいね。

ところで、Windowsアプリケーションを作るやつなので、Macには対応していない。Macの人、ごめんね。。。

Visual Studio で DxLib を使う

うちのサークルではC言語を学ぶ際にIDEとしてVisual Studio を紹介している。ここでは、Visual Studio Community 2017をつかってDxLibを使う方法について触れる。

DxLib の導入

Visual Studio でDxLibを使うにはいくつか手順を踏む必要がある。具体的に言うとコンパイルするときにライブラリを追加したりするオプション周りの話。

基本的には「Visual Studio Community 2017 を使用した場合のDXライブラリの使い方」の通りに設定を行えばいいのだが、いかんせん文字のみの説明になっているのでいくつか設定過程の画像を貼っていく。

DxLibのダウンロード

DxLibをダウンロードするまではいいとして、これらのファイルをどこに置くかが問題である。このファイルは設定のときに毎回アクセスすることになるので、できるだけ浅い階層に置いておくことをおすすめする。具体的にはCドライブ直下とか。
f:id:mattyan1053:20180823163459p:plain
こんな感じ。

空のプロジェクトの作成

いつもどおり新規作成すればOK。
f:id:mattyan1053:20180823164145p:plain
画面がこんな感じになっていればOK。次の画面で空のプロジェクトを選択するのを忘れずに。

ソースファイルの追加

いつもどおりソースファイルを追加すればOK。C++ファイルで問題ない。
f:id:mattyan1053:20180823164402p:plain
ファイル名はお好みで。

プロジェクトの設定

ここからが一番面倒でわかりにくい部分。まずプロジェクトのプロパティの開き方から。 DxLibのページに書いてあることそのまま書くとメニューからプロジェクト(P)のところを開き、一番下の「(プロジェクト名)のプロパティ(E)」を開けば良い。文字で言うと探してしまうので画像を貼るとこう。プロジェクト名は「test」とした。
f:id:mattyan1053:20180823164854p:plain
あとは公式サイトで言われたとおりにいじっていくのだが、どこをいじるかと言うと、「「構成(C):」を「すべての構成」に」とか、「構成(C):」をいじれと言われたら左上をいじる。「左側のリスト」と言われたら左側の構成プロパティとかいろいろ書いてあるところ、右側と言われたら右側の一番でかい四角の中をいじる。
f:id:mattyan1053:20180823165559p:plain
「追加のインクルードディレクトリ」や「追加のライブラリディレクトリ」のところにDxLibを追加するときは、その項目をクリックして出てくる右の方の矢印を開いて、「編集」を押せば追加できる。先ほどダウンロードしたDxLibを置いたところを登録しよう。
f:id:mattyan1053:20180823165751p:plain
f:id:mattyan1053:20180823165804p:plain
あとは言われたとおりやるだけ。

以上でDxLibを扱うためのセッティングは終わり。今後、プロジェクトを作る度に毎回この操作を行うことになる。

DxLibを使ってゲームを作る

基本的には次のようなリファレンスページを見ながら必要な関数を用いてゲームを作っていくことになる。

DXライブラリ置き場 リファレンスページ

ここでは、試しに本当に簡単なシューティングの原型のようなものを作りつつ、関数の使い方を理解してもらいたい。

DxLibの開始・終了

DxLibを使うためにはプログラム内にいくつかお約束がある。次のように書いてほしい。

#include "DxLib.h"

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

    if (DxLib_Init() == -1) {
        return -1;
    }

    WaitKey();

    DxLib_End();

    return 0;

}

これを実行してみると、真っ黒な画面がフルスクリーンで表示されただろう。何かキーを押せば終了することができる。 #include "DxLib.h"については触れるまでもないだろう。DxLibを使うにはインクルードしなければならない。ここで、先ほどの設定をちゃんと行っていないと正常にインクルードすることができない。
DxLibを使う場合、プログラムのエントリーポイント(開始点)はWinMain関数から始まる。詳しい意味は考えなくてOK。
DxLibを使う場合にはお約束があり、DxLib_Init()ではじめて必ずDxLib_End()で終了しなければならない。DxLib_Init()のところにif文がついているのはなにか異常があったときは強制終了するためである。基本的にDxLibの関数は正常なら「0」、なにか異常があれば「-1」が返ってくる関数が多い。
WaitKey()はキー入力を待つためにいれた。これがないと勝手に始まって勝手に終了してしまう(この場合は一瞬で閉じることになる)。後々消す。

ウィンドウの設定とか

次のように入力してほしい。

#include "DxLib.h"

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

    SetMainWindowText("Practice");
    ChangeWindowMode(TRUE); //ウィンドウモードで起動
    SetGraphMode(400, 600, 32); //画面の解像度指定
    SetWindowSizeChangeEnableFlag(FALSE); //画面サイズ変更不可

    if (DxLib_Init() == -1) { //DxLibのスタート
        return -1;
    }

    WaitKey(); //キー入力待ち

    DxLib_End(); //DxLibの終了

    return 0;

}

実行してみると、「Practice」という名前の縦長のウィンドウが表示されたように思う。順番に解説しておく。
SetMainWindowText("Practice)はウィンドウの名前を指定することができる。 ChangeWindowMode(TRUE)はウィンドウモードにするか、フルスクリーンモードにするか指定できる。今回はウィンドウモードにしたが、基本的にはフルスクリーンモードでゲームはしたいよね()。 SetGraphMode(400, 600, 32)はゲームで使う画面の解像度を指定する。今回は横400縦600とした。ウィンドウモードの場合このまま表示され、フルスクリーンだと縦横に拡大縮小して表示されることになるだろう。 SetWindowSizeChangeEnableFlag(FALSE)`は画面のサイズを変更可能にするかどうかだ。ウィンドウモードのとき限定の設定で、基本的にはゲームだと変更されないほうが嬉しいと思う。
これらの関数はリファレンスの「ウィンドウモード関係」及び「その他画面操作系関数」というところに詳しく書いてあるので、そちらを参照するといいだろう。

ここまでが画面を作る準備のようなもの。ウィンドウモードにするかフルスクリーンにするかは、起動の度にダイアログで聞いたりすると好ましいかもしれない。

図形や画像を表示する

次の画像をダウンロードして、ソースファイルと同じところに配置してほしい。
f:id:mattyan1053:20180823173059j:plaincursor.bmp

f:id:mattyan1053:20180823173120p:plain
例のごとく最初にコードを掲載

#include "DxLib.h"

#define ENEMY_SIZE 30 //敵の一辺のサイズ

typedef struct {
    int x;
    int y;
        int r;
    bool flag;
} Bullet;

typedef struct {
    int x;
    int y;
    Bullet b[200];
} Character;

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

    Character p = {};
    Character e = {};

    //プレイヤーの座標
    p.x = 150;
    p.y = 400;

    //敵の座標
    e.x = 50;
    e.y = 50;


    SetMainWindowText("Practice");
    ChangeWindowMode(TRUE); //ウィンドウモードで起動
    SetGraphMode(400, 600, 32); //画面の解像度指定
    SetWindowSizeChangeEnableFlag(FALSE); //画面サイズ変更不可

    if (DxLib_Init() == -1) { //DxLibのスタート
        return -1;
    }

    int GrHandle = LoadGraph("cursor.bmp"); //画像のロード

    //敵の描画(正方形)
    DrawBoxAA(e.x, e.y, e.x + ENEMY_SIZE, e.y + ENEMY_SIZE, GetColor(255, 255, 255), TRUE);

    //プレイヤーの描画
    DrawRotaGraph(p.x, p.y, 0.3f, 0, GrHandle, TRUE);

    WaitKey(); //キー入力待ち

    DxLib_End(); //DxLibの終了

    return 0;

}

さて既にもう長くなってきた。構造体の定義とかは特に触れることもないだろう。弾とキャラクターのを作っておいたよって感じ。
まずは敵の描画について説明する。DxLibには図形を描画してくれる関数がいくつか存在する。リファレンスの図形描画関数のあたりを見るとよいだろう。ここでは敵キャラを正方形としたのでDrawBoxAA()関数を利用した。引数についてはリファレンスを見てほしいが簡単に言えば左上と右下の座標、色、塗りつぶすかどうかである。上の方で敵キャラの座標を設定しているので画面左上のほうに白い正方形が表示されているはずである。色はGetColor()関数でRGB値を指定してあげるとほしい値が返ってくるのでここでは白を指定している。
次に画像を貼ったりする処理である。まずはint型の変数GrHandleを作っておき、その領域にLoadGraph()関数を使って画像をロードしておく。DxLibをで画像を使用する際には必要な操作なので覚えよう。ちなみに使わなくなったらDeleteGraph()しておくと良い(リファレンス参照)。
ロードしておいた画像は、DrawGraph()関数など、いくつかの関数でウィンドウに描画することができるが、今回はDrawRotaGraph()関数を紹介する。なぜこの関数を紹介するかと言うと、ゲームを作ってく中でも便利だと思うからである。DrawGraph()関数は何もいじらずに画像を表示するのに対し、DrawRotaGraph()関数は拡大縮小、回転、反転ができ、更に中心の座標で指定できるので何かとやりやすいという点が良い。それぞれ拡大縮小のみや反転のみの関数も存在するがすべてこの関数で事足りてしまう強さがある。引数はリファレンスの通りなので割愛。回転角はラジアン(弧度法)指定なのでPIとかPI/2とか使っていくことになるだろう(PIは定数)。貼り付ける画像はLoadGraph()の返り値を保存しておいた変数を指定する。

ループの中で画像を描画する

先ほどまではただ画像を表示するだけだったが、実際にゲームを作る際は無限ループさせてその中で何度も画像を描画することになる。パソコンの中で何枚も紙芝居を書く感じ。次のページに進むごとにキャラクターとかの座標をずらしていけば結果的になめらかに移動する画像を描くことができる。
f:id:mattyan1053:20180823180124p:plain
更に、単に紙芝居にするのではなく、両面使える紙を何度も裏返して、再利用していく形を取る。どういうことかというと、
f:id:mattyan1053:20180823180726p:plain
このようにすると、結果的に見える部分は「あ」→「い」→「う」と変化していくことになる。言葉で言えば、「裏画面に描く」→「裏返す」→「裏画面に描く」→・・・・・・というように繰り返していくことになる。ちなみに、当然ながらこれをコンピューターにさせるとなると、一秒間に何十回というスピードで裏返していくことになるので、とてもなめらかに画像を動かしたりすることが可能だ。
さて、これを実際にプログラムの中で実装してみよう。

#include "DxLib.h"

#define WIDTH 400
#define HEIGHT 600

#define ENEMY_SIZE 30 //敵の一辺のサイズ

typedef struct {
    int x;
    int y;
        int r;
    bool flag;
} Bullet;

typedef struct {
    int x;
    int y;
    Bullet b[200];
} Character;

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

    Character p = {};
    Character e = {};

    //プレイヤーの座標
    p.x = 150;
    p.y = 400;

    //敵の座標
    e.x = 50;
    e.y = 50;


    SetMainWindowText("Practice");
    ChangeWindowMode(TRUE); //ウィンドウモードで起動
    SetGraphMode(WIDTH, HEIGHT, 32); //画面の解像度指定
    SetWindowSizeChangeEnableFlag(FALSE); //画面サイズ変更不可
    SetDrawScreen(DX_SCREEN_BACK); //描画先を裏画面に

    if (DxLib_Init() == -1) { //DxLibのスタート
        return -1;
    }

    int GrHandle = LoadGraph("cursor.bmp"); //画像のロード

    int cnt = 0; //ループ回数カウント用

    while (ProcessMessage() == 0 && cnt < 300) {

        //画面全体を黒で塗りつぶし
        DrawBoxAA(0, 0, WIDTH, HEIGHT, GetColor(0, 0, 0), TRUE);

        //敵の描画(正方形)、徐々に右へ
        DrawBoxAA(e.x + cnt, e.y, e.x + cnt + ENEMY_SIZE, e.y + ENEMY_SIZE, GetColor(255, 255, 255), TRUE);

        //プレイヤーの描画
        DrawRotaGraph(p.x, p.y, 0.3f, 0, GrHandle, TRUE);

        cnt++;

        ScreenFlip(); //画面裏返し
    }

    DxLib_End(); //DxLibの終了

    return 0;

}

実行してみると、白い正方形が少しずつ右側に移動していくのを見ることができるだろう。順に説明していこう。
まず、ウィンドウモードの設定などをしている部分に、SetDrawScreen(DX_SCREEN_BACK)というのがあるだろう。これは描画先を先ほどで言う裏側に設定するという意味だ。ちなみにこれを行っていない場合、表画面、つまり見えている部分で描画を逐次行っていくので、画面がちらついてしまうことがある。できるだけ描画作業は裏画面で行うようにしよう。while文の条件式の中にProcessMessage() == 0というものがある。これは意味はわからなくてもよいが、Windowsアプリケーションにつきまとう処理をやってくれるものなので、ループを指せる場合は同時に呼ぶように癖をつけると良い。
ループ内では最初に画面を塗りつぶしている。裏返した画面には前の画面がまだ残っているので、それを真っ黒に上から塗りつぶして隠しているということになる。それがDrawBoxAA(0, 0, WIDTH, HEIGHT, GetColor(0,0,0))だ。
あとは正方形が移動するように敵の描画のぶぶんのx座標にcntを加えたくらい。画面のサイズを#defineで定数化しておいた。

キー入力を受け取る

次は自キャラを動かしてみたい。自分の入力に応じて画面の中でものが動くのはとても楽しい。
次のようにコードを書き足してみる。

#include "DxLib.h"

#define WIDTH 400
#define HEIGHT 600

#define ENEMY_SIZE 30 //敵の一辺のサイズ

typedef struct {
    int x;
    int y;
        int r;
    bool flag;
} Bullet;

typedef struct {
    int x;
    int y;
    int vel;
    Bullet b[200];
} Character;

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

    char buf[256];

    Character p = {};
    Character e = {};

    //プレイヤーの初期状態
    p.x = 150;
    p.y = 400;
    p.vel = 5;

    //敵の座標
    e.x = 50;
    e.y = 50;


    SetMainWindowText("Practice");
    ChangeWindowMode(TRUE); //ウィンドウモードで起動
    SetGraphMode(WIDTH, HEIGHT, 32); //画面の解像度指定
    SetWindowSizeChangeEnableFlag(FALSE); //画面サイズ変更不可
    SetDrawScreen(DX_SCREEN_BACK); //描画先を裏画面に

    if (DxLib_Init() == -1) { //DxLibのスタート
        return -1;
    }

    int GrHandle = LoadGraph("cursor.bmp"); //画像のロード

    while (ProcessMessage() == 0) {

        //画面全体を黒で塗りつぶし
        DrawBoxAA(0, 0, WIDTH, HEIGHT, GetColor(0, 0, 0), TRUE);

        GetHitKeyStateAll(buf);

        //プレイヤーの移動
        if (buf[KEY_INPUT_LEFT] == 1) p.x -= p.vel;
        if (buf[KEY_INPUT_RIGHT] == 1) p.x += p.vel;
        if (buf[KEY_INPUT_UP] == 1) p.y -= p.vel;
        if (buf[KEY_INPUT_DOWN] == 1) p.y += p.vel;

        //敵の描画(正方形)、徐々に右へ
        DrawBoxAA(e.x, e.y, e.x + ENEMY_SIZE, e.y + ENEMY_SIZE, GetColor(255, 255, 255), TRUE);

        //プレイヤーの描画
        DrawRotaGraph(p.x, p.y, 0.3f, 0, GrHandle, TRUE);

        ScreenFlip(); //画面裏返し

        if (buf[KEY_INPUT_ESCAPE] == 1) break; //ESCが押されたら終了

    }

    DxLib_End(); //DxLibの終了

    return 0;

}

十字キーで移動と、ESCキーで終了をできるように実装してみた。解説すると、GetHitKeyStateAll()関数を呼び出すことでchar型の配列buf[256]にキーの押下状態が記録されるので、それぞれ必要なキーが押されているかif文で確認し、押されていれば適宜移動するだけである。p.velは速さのつもり。斜め移動もできるしそっちがすごい効率的な移動の扱いになるけど本筋からずれてしまうのでここでは許してほしい(√2走法なんて技ありましたね・・・・・・)。if文の条件のchar型変数bufの添字についてはリファレンスを参照してほしい。ほしいキーに該当する定数(KEY_INPUT_ESCAPEとか)を添え字に当てはめれば押下状態が確認できるという仕組みになっている。注意点はまずループの最初にキーの押下状態を取得するためにGetHitKeyStateAll()関数を呼び出すことである。
ちなみに、特定のキーだけ調べるCheckHitKey()関数もあるが、こちらは無駄の多い関数なので基本的にGetHitKeyStateAll()を使うので良いと思う(一つしかキーを使わないなら別だが・・・・・・)。

ゲームモードの設定

さて、コードも長くなってきたので、ここで一度プログラムを整理してみよう。 当たり前のことだが、起動していきなり始まるゲームは普通ない。そこで、よく使う手法としてメインループの初めで場合分けをするということがある。
次のコードを見てほしい。

#include "DxLib.h"

#define WIDTH 400
#define HEIGHT 600

#define ENEMY_SIZE 30 //敵の一辺のサイズ

//ハンドル
int GrHandle;

typedef struct {
    int x;
    int y;
        int r;
    bool flag;
} Bullet;

typedef struct {
    int x;
    int y;
    int vel;
    Bullet b[200];
} Character;

//キャラクター
Character p = {};
Character e = {};

//データのロード
void LoadData() {
    GrHandle = LoadGraph("cursor.bmp");
}

//初期化
void gameInit() {
    //プレイヤーの初期状態
    p.x = 150;
    p.y = 400;
    p.vel = 5;

    //敵の座標
    e.x = 50;
    e.y = 50;
}

//タイトル画面
int title(char buf[]) {

    DrawString(20, 250, "Enterでスタート", GetColor(255, 0, 0));

    if (buf[KEY_INPUT_RETURN] == 1) return 1; //Enterキーで次のモードへ
    return 0;
}

int game(char buf[]) {
    //プレイヤーの移動
    if (buf[KEY_INPUT_LEFT] == 1) p.x -= p.vel;
    if (buf[KEY_INPUT_RIGHT] == 1) p.x += p.vel;
    if (buf[KEY_INPUT_UP] == 1) p.y -= p.vel;
    if (buf[KEY_INPUT_DOWN] == 1) p.y += p.vel;

    //敵の描画(正方形)、徐々に右へ
    DrawBoxAA(e.x, e.y, e.x + ENEMY_SIZE, e.y + ENEMY_SIZE, GetColor(255, 255, 255), TRUE);

    //プレイヤーの描画
    DrawRotaGraph(p.x, p.y, 0.3f, 0, GrHandle, TRUE);

    if (buf[KEY_INPUT_ESCAPE] == 1) return 9; //ESCが押されたら終了
    return 1;
}

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

    char buf[256];

    char gamemode = 0;

    SetMainWindowText("Practice");
    ChangeWindowMode(TRUE); //ウィンドウモードで起動
    SetGraphMode(WIDTH, HEIGHT, 32); //画面の解像度指定
    SetWindowSizeChangeEnableFlag(FALSE); //画面サイズ変更不可
    SetDrawScreen(DX_SCREEN_BACK); //描画先を裏画面に

    if (DxLib_Init() == -1) { //DxLibのスタート
        return -1;
    }

    LoadData();
    gameInit();

    while (ProcessMessage() == 0 && gamemode != 9) {

        //画面全体を黒で塗りつぶし
        DrawBoxAA(0, 0, WIDTH, HEIGHT, GetColor(0, 0, 0), TRUE);

        GetHitKeyStateAll(buf); //キー入力取り込み

        switch (gamemode) {
        case 0:
            gamemode = title(buf);
            break;
        case 1:
            gamemode = game(buf);
            break;
        default:
            break;
        }

        ScreenFlip(); //画面裏返し

    }

    DxLib_End(); //DxLibの終了

    return 0;

}

今まですべてWinMain関数内でやってきたものを分割してswitch文を使ったシーン遷移で切り替えられるようにした。ゲーム本体の描画部分はgame()関数に分けている。元のプログラムと、書き換えたプログラムをじっくり見比べて、何が便利になっているか考えてほしい。
追加したのはgamemodeに関する部分のみであとはもともとあったコードを別のところに書いたりしただけなのがわかると思う。メリットとしては

  • シーンの追加がしやすい
  • ロードがひとまとまりになっている
  • 再度初期化するにも関数を呼び出せば良い

などがある。

弾の実装

さて、自キャラと敵キャラを実装したら倒せなければ意味がない。弾を実装して敵キャラを倒してみよう。
お気付きの通り、既に弾の構造体もあるし、キャラクターの構造体の中には弾の配列を組み込んである。あとはこれを実装してあげれば良さそうだ。
ここでは、C言語の「配列」という概念を、実際のゲームづくりでどのように役立てるか、というイメージを掴んでもらいたい。基本的にはループを使って一括管理をすることになるだろう。

弾の射出

まずは弾を出せるようにする。次のような関数をgame()関数の前にshoot()関数をMoveDraw()関数を追加して、game()関数を少し書き換える。

void shoot() {
    //弾の射出フラグを立て、座標をあわせる
    for (int i = 0; i < 200; i++) {
        if (p.b[i].flag == false) {
            p.b[i].flag = true;
            p.b[i].r = 2;
            p.b[i].x = p.x;
            p.b[i].y = p.y;
            break;
        }
    }
}

void MoveDraw() {
    //弾のフラグがオンのとき、画面外ならフラグ取り消し、画面内ならY座標を減らして描画。
    for (int i = 0; i < 200; i++) {
        if (p.b[i].flag == true) {
            if (p.b[i].y <= 0) {
                p.b[i].flag = false;
                continue;
            }
            p.b[i].y -= 10;
            DrawCircleAA(p.b[i].x, p.b[i].y, p.b[i].r, 32, GetColor(255, 0, 0), TRUE);
        }
    }
}

int game(char buf[]) {
    //プレイヤーの移動
    if (buf[KEY_INPUT_LEFT] == 1) p.x -= p.vel;
    if (buf[KEY_INPUT_RIGHT] == 1) p.x += p.vel;
    if (buf[KEY_INPUT_UP] == 1) p.y -= p.vel;
    if (buf[KEY_INPUT_DOWN] == 1) p.y += p.vel;

    //敵の描画(正方形)、徐々に右へ
    DrawBoxAA(e.x, e.y, e.x + ENEMY_SIZE, e.y + ENEMY_SIZE, GetColor(255, 255, 255), TRUE);

    //プレイヤーの描画
    DrawRotaGraph(p.x, p.y, 0.3, 0, GrHandle, TRUE);

    MoveDraw(); //弾の描画、移動

    if (buf[KEY_INPUT_SPACE] == 1) shoot(); //スペースが押されたら射出

    if (buf[KEY_INPUT_ESCAPE] == 1) return 2; //ESCが押されたら終了
    return 1;
}

game()関数のほうはいいだろう。それぞれ射出と描画、移動の処理を行う関数を呼び出しているだけである。
shoot()関数では、プレイヤーの弾で発射判定になっていない(flagがfalseな)弾を探してtrueにし、座標をプレイヤーに合わせている。MoveDraw()関数では射出されている(flagがtrue)な弾が画面外に出ていないか確認したあと、画面に描画している。画面外に出たときにフラグを戻す処理を忘れると、200発目以降弾がでなくなってしまうので注意が必要だ。

弾の当たり判定

最後に、弾に当たり判定をつけよう。それぞれの弾が敵にヒットしているか調べる関数を追加すればよさそうだ。
game()関数の前にcheckHit()関数を追加し、game()関数内で呼び出そう。

int checkHit() {
    for (int i = 0; i < 200; i++) {
        if (p.b[i].flag == true) {
            if (p.b[i].x >= e.x && p.b[i].x <= e.x + ENEMY_SIZE && p.b[i].y >= e.y && p.b[i].y <= e.y + ENEMY_SIZE) {
                return 1;
            }
        }
    }
    return 0;
}

int game(char buf[]) {
    //プレイヤーの移動
    if (buf[KEY_INPUT_LEFT] == 1) p.x -= p.vel;
    if (buf[KEY_INPUT_RIGHT] == 1) p.x += p.vel;
    if (buf[KEY_INPUT_UP] == 1) p.y -= p.vel;
    if (buf[KEY_INPUT_DOWN] == 1) p.y += p.vel;

    //敵の描画(正方形)、徐々に右へ
    DrawBoxAA(e.x, e.y, e.x + ENEMY_SIZE, e.y + ENEMY_SIZE, GetColor(255, 255, 255), TRUE);

    //プレイヤーの描画
    DrawRotaGraph(p.x, p.y, 0.3, 0, GrHandle, TRUE);

    MoveDraw(); //弾の描画、移動

    if (checkHit() == 1) return 2;

    if (buf[KEY_INPUT_SPACE] == 1) shoot(); //スペースを押したら射出

    if (buf[KEY_INPUT_ESCAPE] == 1) return 9; //ESCが押されたら終了
    return 1;
}

checkHit()関数は当たっていれば1を返す関数にした。当たり判定は弾の中心座標を使ってみた。ほら、かすっても死なない感じで。あたっていたら次のゲームモードへ。

クリア画面の実装

単純にゲームモードを追加しただけ。WinMain()関数を編集し、その上にresult()関数を追加した。

int result(char buf[]) {

    DrawString(20, 250, "Clear!Enterで終了", GetColor(255, 0, 0));

    if (buf[KEY_INPUT_RETURN] == 1) return 9; //Enterキーで次のモードへ
    return 2;
}

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

    char buf[256];

    char gamemode = 0;

    SetMainWindowText("Practice");
    ChangeWindowMode(TRUE); //ウィンドウモードで起動
    SetGraphMode(WIDTH, HEIGHT, 32); //画面の解像度指定
    SetWindowSizeChangeEnableFlag(FALSE); //画面サイズ変更不可
    SetDrawScreen(DX_SCREEN_BACK); //描画先を裏画面に

    if (DxLib_Init() == -1) { //DxLibのスタート
        return -1;
    }

    LoadData();
    gameInit();

    while (ProcessMessage() == 0 && gamemode != 9) {

        //画面全体を黒で塗りつぶし
        DrawBoxAA(0, 0, WIDTH, HEIGHT, GetColor(0, 0, 0), TRUE);

        GetHitKeyStateAll(buf); //キー入力取り込み

        switch (gamemode) {
        case 0:
            gamemode = title(buf);
            break;
        case 1:
            gamemode = game(buf);
            break;
        case 2:
            gamemode = result(buf);
        default:
            break;
        }

        ScreenFlip(); //画面裏返し

    }

    DxLib_End(); //DxLibの終了

    return 0;

}

ここでは特に説明することもない。シーンの追加だけ。

確認

現在のコードは次のようになっている。少し長いが本来は分割とかすべきだろう(今回はDxLibの使う流れとかを知ってもらいたかったので割愛)。

#include "DxLib.h"

#define WIDTH 400
#define HEIGHT 600

#define ENEMY_SIZE 30 //敵の一辺のサイズ

//ハンドル
int GrHandle;

typedef struct {
    int x;
    int y;
    int r;
    bool flag;
} Bullet;

typedef struct {
    int x;
    int y;
    int vel;
    Bullet b[200];
} Character;

//キャラクター
Character p = {};
Character e = {};

//データのロード
void LoadData() {
    GrHandle = LoadGraph("cursor.bmp");
}

//初期化
void gameInit() {
    //プレイヤーの初期状態
    p.x = 150;
    p.y = 400;
    p.vel = 5;
    for (int i = 0; i < 200; i++) {
        p.b[i].flag = false;
    }

    //敵の座標
    e.x = 50;
    e.y = 50;
}

//タイトル画面
int title(char buf[]) {

    DrawString(20, 250, "Enterでスタート", GetColor(255, 0, 0));

    if (buf[KEY_INPUT_RETURN] == 1) return 1; //Enterキーで次のモードへ
    return 0;
}

void shoot() {
    //弾の射出フラグを立て、座標をあわせる
    for (int i = 0; i < 200; i++) {
        if (p.b[i].flag == false) {
            p.b[i].flag = true;
            p.b[i].r = 2;
            p.b[i].x = p.x;
            p.b[i].y = p.y;
            break;
        }
    }
}

void MoveDraw() {
    //弾のフラグがオンのとき、画面外ならフラグ取り消し、画面内ならY座標を減らして描画。
    for (int i = 0; i < 200; i++) {
        if (p.b[i].flag == true) {
            if (p.b[i].y <= 0) {
                p.b[i].flag = false;
                continue;
            }
            p.b[i].y -= 10;
            DrawCircleAA(p.b[i].x, p.b[i].y, p.b[i].r, 32, GetColor(255, 0, 0), TRUE);
        }
    }
}

int checkHit() {
    for (int i = 0; i < 200; i++) {
        if (p.b[i].flag == true) {
            if (p.b[i].x >= e.x && p.b[i].x <= e.x + ENEMY_SIZE && p.b[i].y >= e.y && p.b[i].y <= e.y + ENEMY_SIZE) {
                return 1;
            }
        }
    }
    return 0;
}

int game(char buf[]) {
    //プレイヤーの移動
    if (buf[KEY_INPUT_LEFT] == 1) p.x -= p.vel;
    if (buf[KEY_INPUT_RIGHT] == 1) p.x += p.vel;
    if (buf[KEY_INPUT_UP] == 1) p.y -= p.vel;
    if (buf[KEY_INPUT_DOWN] == 1) p.y += p.vel;

    //敵の描画(正方形)、徐々に右へ
    DrawBoxAA(e.x, e.y, e.x + ENEMY_SIZE, e.y + ENEMY_SIZE, GetColor(255, 255, 255), TRUE);

    //プレイヤーの描画
    DrawRotaGraph(p.x, p.y, 0.3, 0, GrHandle, TRUE);

    MoveDraw(); //弾の描画、移動

    if (checkHit() == 1) return 2;

    if (buf[KEY_INPUT_SPACE] == 1) shoot(); //スペースを押したら射出

    if (buf[KEY_INPUT_ESCAPE] == 1) return 9; //ESCが押されたら終了
    return 1;
}

int result(char buf[]) {

    DrawString(20, 250, "Clear!Enterで終了", GetColor(255, 0, 0));

    if (buf[KEY_INPUT_RETURN] == 1) return 9; //Enterキーで次のモードへ
    return 2;
}

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

    char buf[256];

    char gamemode = 0;

    SetMainWindowText("Practice");
    ChangeWindowMode(TRUE); //ウィンドウモードで起動
    SetGraphMode(WIDTH, HEIGHT, 32); //画面の解像度指定
    SetWindowSizeChangeEnableFlag(FALSE); //画面サイズ変更不可
    SetDrawScreen(DX_SCREEN_BACK); //描画先を裏画面に

    if (DxLib_Init() == -1) { //DxLibのスタート
        return -1;
    }

    LoadData();
    gameInit();

    while (ProcessMessage() == 0 && gamemode != 9) {

        //画面全体を黒で塗りつぶし
        DrawBoxAA(0, 0, WIDTH, HEIGHT, GetColor(0, 0, 0), TRUE);

        GetHitKeyStateAll(buf); //キー入力取り込み

        switch (gamemode) {
        case 0:
            gamemode = title(buf);
            break;
        case 1:
            gamemode = game(buf);
            break;
        case 2:
            gamemode = result(buf);
        default:
            break;
        }

        ScreenFlip(); //画面裏返し

    }

    DxLib_End(); //DxLibの終了

    return 0;

}

実際に動かしてみると
f:id:mattyan1053:20180823221843p:plain
とまあこんな感じで十字キーで動いてスペースキーで弾を撃ち、白い正方形にあてるとクリアとなるゲームが完成する。

ゲーム性を高める

見ての通りこんなもの誰がプレイしても面白くないクソゲーだ。しかしココまでくれば、敵の数を増やしたり動かしたり、自分の攻撃を増やしてみたり、ステージ追加してみたり、動きに制限(重力とか)つけてみたり、あるいはブロック崩しや横スクロールのように別のゲームをつくることもできる。なぜなら画像や図形を好きな位置にはれて、動かせるようになったからだ。ぜひ、いろいろ拡張してみて面白いものを作って欲しい。

必要なスキル

ここまでで、DxLibの関数を使ってみる一例を示した。基本的には用意されている関数で描画しつつ、システム的な計算などは基礎として学んできた知識を活かすことになるだろう。
重要なのは、自分のしたいことをリファレンスなどから見つけてきて、そこを読めば実装できるようにする能力だ。決して覚えたりすることではない(何度もやっていれば覚えては来るが・・・・・・)。

最後に

急ごしらえな記事なのでだいぶ汚いコードと雑な説明になってしまった(まじでごめんなさい;)。けれども、これらを扱えればなんかしら形になるものは作れると思う。画像を動かしたりして表示するのにループと裏画面をつかってみたり、シーン遷移させたり、というセオリーについては触れるという点は達成できたはずだ。というか裏画面うんぬんとか一度説明されないとわからなくない?
なれてきたら画像処理とか(これも関数がある)でエフェクトをつけてみたり、回転とか複雑な動きをさせてみたり、音を付けてみたりして、ステップアップしていけば良いと思う。 最後に一つ。いきなり大きなもの作ろうとしてもマジで、マジでうまくいかないので、
小さな一歩を大事に