DxLibグラフィック版スライドパズルを作っていくよ

コンソール版による試作品製作の意義

ここまでたどり着いた人は、多分だけどコンソール版ではスライドパズルが完成しているはず。
昔からゲーム作ってる人は良くやるんだけど、とりあえずコンソールで配列とかを駆使してプロトタイプ(試作品)を作って、それをグラフィック表示にしていくという開発手順である。
これをやってみることで、とりあえず

が確認できる。
後は、できたゲームにグラフィックを割り当てていったり、座標系とか描画とかグラフィック特有の処理を解決すればゲーム完成!
って流れである。(簡単だね)
後は、画面を作って、現在の盤面を表示できるようにDraw関数を作る。

Fig. 1: ゲーム画面

画面のクリックした位置のパネル番号を取得して、コンソール版の移動可能か調べる関数に渡して、
動かせそうなら、コンソール版のパネルをスペースと入れ替える関数に渡す。
コンソール版の動きと同じ処理をグラフィックで書いてあげるだけ(簡単に言うけどそれが難しいって話も。。。) なんかできそうじゃない?

準備1 ゲーム全体の流れを作る

後からやるととってもめんどくさいことになるので、まずゲームの流れをまるっと作ってゆく。
今回のゲームは、パズルゲームなのでとても簡単流れを考えると以下のようになると思う。。

  1. タイトル画面(⇒プレイ画面へ移行)
  2. プレイ画面(⇒クリアしたらクリア画面へ移行、⇒ゲームオーバーならゲームオーバー画面へ)
  3. クリア画面(⇒タイトルへ移行)
  4. ゲームオーバー画面(⇒タイトル画面へ移行)

スライドパズルのレベルデザイン的な部分は、難易度、手数制限、時間制限等で考えられるが、今回はクリアはあるけどゲームオーバー(手数とか時間制限はなし)で考える(ここは後付けでも簡単だし!)
授業でやったように、ゲームのステート(状態)を移行していくことでその場面に必要な関数を呼び分けるという作戦をとる。
すなわち、こんな感じのデザインになるかなと思う(CでもC++でも)

"状態遷移"
enum GAME_STATE
{
	TITLE, //タイトル画面(シーン)
	PLAY,  //プレイ画面(シーン)
	CLEAR, //クリア画面(シーン)
	GAMEOVER, //ゲームオーバー画面(シーン)
};
 
GAME_STATE state = GAME_STATE::TITLE; //状態の初期化 stateってグローバル変数に現在の状態を保存
 
 
	while (true)
	{
		switch (state)
		{
		case TITLE:
			TitleUpdate(&board);
			TitleDraw();
			break;
		case PLAY:
			PlayUpdate(&board);
			PlayDraw(&board);
			break;
		case CLEAR:
			ClearUpdate(&board);
			ClearDraw();
			break;
		case GAMEOVER:
			break;
		default:
                        //ここには来ない。。。はず
			break;
		}
	}

タイトル画面を表示するまで

とりあえずタイトル画面を出したいね。

各状態で呼び出す関数を以下のように作っていこうと思う。

とりあえずDxLibのプロジェクトを作って、こいつらを追加しちゃいます。
ほんとは、ソースコードとヘッダファイル分けたほうがいいけど、今回は全部メインに書いていきます。

ここでちょっとプロトタイプ宣言の話

宣言と定義は別だよねって話は授業の時にしたと思います。
プロトタイプ宣言は、関数の戻り値の型と、関数名、引数のリストを書いたものです。
(関数定義から、関数ブロック(処理内容)とってかわりにセミコロンつければいいよ。)
プロトタイプ宣言と、関数定義を書くと2度手間な気がするが、役割が違う。

初めのひな型

"全体像"
// DxLib 雛形:状態遷移 TITLE/PLAY/CLEAR/GAMEOVER
#include "DxLib.h"
#include <stdbool.h>
#include <string.h>
 
//---------------------------
// 状態定義
//---------------------------
enum GAME_STATE
{
    TITLE,
    PLAY,
    CLEAR,
    GAMEOVER,
};
 
// ゲーム状態の変数と、状態の初期化
enum GAME_STATE state = TITLE;
 
//---------------------------
// 画面サイズ(中央寄せ用)
//---------------------------
#define SCREEN_W 1280
#define SCREEN_H 720
 
//---------------------------
// フォントハンドル
//---------------------------
int gFontTitle   = -1; // 大タイトル
int gFontLarge   = -1; // 大見出し
int gFontMedium  = -1; // サブ見出し
int gFontSmall   = -1; // 説明小さめ
 
//---------------------------
// 関数プロトタイプ
//---------------------------
void TitleUpdate(void);
void TitleDraw(void);
 
void PlayUpdate(void);
void PlayDraw(void);
 
void ClearUpdate(void);
void ClearDraw(void);
 
void GameOverUpdate(void);
void GameOverDraw(void);
 
// 中央寄せ文字描画(影つき)
void DrawCenteredString(const char* text, int y, int font,
                        unsigned int colMain, unsigned int colShadow, int shadowOffset);
 
//---------------------------
// メイン(WinMain)
//---------------------------
int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
{
    ChangeWindowMode(TRUE);
    SetGraphMode(SCREEN_W, SCREEN_H, 32);
    SetWindowText("DxLib State Template - Centered Bold UI");
 
    if (DxLib_Init() != 0) return -1;
    SetDrawScreen(DX_SCREEN_BACK);
 
    // フォント作成(太め&アンチエイリアス)
    // CreateFontToHandle(名前, サイズpx, 太さ, タイプ, 文字セット, エッジ幅)
    gFontTitle  = CreateFontToHandle("Meiryo", 96,  6, DX_FONTTYPE_ANTIALIASING_8X8, -1, -1);
    gFontLarge  = CreateFontToHandle("Meiryo", 64,  5, DX_FONTTYPE_ANTIALIASING_8X8, -1, -1);
    gFontMedium = CreateFontToHandle("Meiryo", 36,  4, DX_FONTTYPE_ANTIALIASING_8X8, -1, -1);
    gFontSmall  = CreateFontToHandle("Meiryo", 24,  2, DX_FONTTYPE_ANTIALIASING_8X8, -1, -1);
 
    while (ProcessMessage() == 0)
    {
        ClearDrawScreen();
 
        // 背景(淡い色)
        DrawBox(0, 0, SCREEN_W, SCREEN_H, GetColor(153, 204, 179), TRUE);
 
        // 状態に応じて描画(更新は空のまま)
        switch (state)
        {
        case TITLE:
            TitleUpdate(); // ←中身は空
            TitleDraw();
            break;
 
        case PLAY:
            PlayUpdate();  // ←中身は空
            PlayDraw();
            break;
 
        case CLEAR:
            ClearUpdate(); // ←中身は空
            ClearDraw();
            break;
 
        case GAMEOVER:
            GameOverUpdate(); // ←中身は空
            GameOverDraw();
            break;
 
        default:
            break;
        }
 
        ScreenFlip();
    }
 
    // フォント破棄
    if (gFontTitle  != -1) DeleteFontToHandle(gFontTitle);
    if (gFontLarge  != -1) DeleteFontToHandle(gFontLarge);
    if (gFontMedium != -1) DeleteFontToHandle(gFontMedium);
    if (gFontSmall  != -1) DeleteFontToHandle(gFontSmall);
 
    DxLib_End();
    return 0;
}
 
//==================== ここから Update(空) ====================
void TitleUpdate(void)   {}
void PlayUpdate(void)    {}
void ClearUpdate(void)   {}
void GameOverUpdate(void){}

ここでやったのは、

mainの前にいかを挿入

//==================== ヘルパー:中央寄せ(影つき) ====================
void DrawCenteredString(const char* text, int y, int font,
                        unsigned int colMain, unsigned int colShadow, int shadowOffset)
{
    int w = GetDrawStringWidthToHandle(text, (int)strlen(text), font);
    int h = GetFontSizeToHandle(font);
    int x = (SCREEN_W - w) / 2;
 
    // 影(オフセットがあれば)
    if (shadowOffset > 0) {
        DrawStringToHandle(x + shadowOffset, y + shadowOffset, text, colShadow, font);
    }
    // 本体
    DrawStringToHandle(x, y, text, colMain, font);
}

ここで指定するフォントは、ソースコード内で生成している

//---------------------------
// フォントハンドル
//---------------------------
int gFontTitle   = -1; // 大タイトル
int gFontLarge   = -1; // 大見出し
int gFontMedium  = -1; // サブ見出し
int gFontSmall   = -1; // 説明小さめ
 
    // フォント作成(太め&アンチエイリアス)
    // CreateFontToHandle(名前, サイズpx, 太さ, タイプ, 文字セット, エッジ幅)
    gFontTitle  = CreateFontToHandle("Meiryo", 96,  6, DX_FONTTYPE_ANTIALIASING_8X8, -1, -1);
    gFontLarge  = CreateFontToHandle("Meiryo", 64,  5, DX_FONTTYPE_ANTIALIASING_8X8, -1, -1);
    gFontMedium = CreateFontToHandle("Meiryo", 36,  4, DX_FONTTYPE_ANTIALIASING_8X8, -1, -1);
    gFontSmall  = CreateFontToHandle("Meiryo", 24,  2, DX_FONTTYPE_ANTIALIASING_8X8, -1, -1);

こいつらを使います。


各Update、Draw関数の準備

プロトタイプ宣言まで完了したが、関数本体=定義、がないため呼び出せない。
関数宣言の中身を空にしてると、呼び出してもスルーするだけなのでエラーにはならない。
なので、とりあえず処理部を空にして定義をすべて作っておく。
プロトタイプ宣言をまるっとドラッグで選択して、右クリックして、「クイックアクションとリファクタリング」⇒「宣言/定義の作成」をえらぶと、空の定義が自動でできるので便利!
そうするとこの状態になる。

"関数定義まで"
//上のほう省略
//プロトタイプ宣言たち
void TitleUpdate();
void TitleDraw();
 
void PlayUpdate();
void PlayDraw();
 
void ClearUpdate();
void ClearDraw();
 
void Main()
{
	// 背景の色を設定する | Set the background color
	Scene::SetBackground(ColorF{ 0.6, 0.8, 0.7 });
	//タイトル画面とスタートボタンのフォント(ンでそのままほかのシーンに使いまわし)
	FontAsset::Register(U"TITLE_FONT", FontMethod::SDF, 40, Typeface::Bold);
	FontAsset::Register(U"BUTTON_FONT", FontMethod::SDF, 20, Typeface::Mplus_Heavy);
 
	while (System::Update())
	{
       //省略
	}
}
//この関数定義が自動で生成されるよ!
void TitleUpdate()
{
}
 
void TitleDraw()
{
}
 
void PlayUpdate()
{
}
 
void PlayDraw()
{
}
 
void ClearUpdate()
{
}
 
void ClearDraw()
{
}

タイトル画面の作成

DrawCenteredString関数は、

DrawCenteredString("描画する文字",
        描画する高さ, フォントハンドル, 前景色, 影の色, 影の位置);

で、画面の中央に文字を描画する関数です。適当にこれ使ってそれぞれでタイトル画面作ってみよう。
(それか授業でやったみたいに、画像を表示してもいいよ)

タイトル画面
Fig. 2: タイトル画面の作成(サンプル)

とにかくこんな感じでタイトルを作る!
実際の処理は、TitleDrawの中に描画処理を書いてやります。
画像読んだり、アニメーションしたりいろいろやってみて!

"タイトル画面の描画"
void TitleUpdate()
{
	//特になし
}
 
void TitleDraw(void)
{
    // タイトル
    DrawCenteredString("THE SLIDE PUZZLE", SCREEN_H/2 - 60,
        gFontTitle,  GetColor(255,255,255), GetColor(0,0,0), 4);
 
    // サブテキスト
    DrawCenteredString("Click to START",
        SCREEN_H/2 + 60, gFontMedium, GetColor(0,0,0), GetColor(255,255,255), 0);
}

これでタイトル画面が表示されるか確認する!