Skip to main content

第1回 ゲームのシーケンス(画面遷移)を作る

第1回 ゲームループと画面の切り替えを作ろう

今回やること

今回から、コンソール版で作ったスライドパズルをDxLibのゲームへ作り替えていきます。

いきなりパズルの盤面を描くのではなく、まずはゲーム全体の流れを作ります。

今回のゲームでは、次の3つの画面を使います。

  • タイトル画面
  • プレイ画面
  • クリア画面

最初にこの3つを切り替えられるようにしておくと、このあと作るパズルの処理を「プレイ画面の中」へ順番に追加していけます。


1.ゲームの「状態」を作ろう

今、どの画面を表示しているのかを覚えておく必要があります。

そこで、enumを使ってゲームの状態をまとめます。

enum GameState
{
    GAME_STATE_TITLE,
    GAME_STATE_PLAY,
    GAME_STATE_CLEAR
};

現在の状態を保存する変数は、次のように作ります。

enum GameState state = GAME_STATE_TITLE;

最初はタイトル画面から始めるので、GAME_STATE_TITLEを入れておきます。


2.まず3つの画面を描く関数を作ろう

ゲームの状態によって、描く内容を変えたいので、画面ごとにDraw関数を作ります。

void TitleDraw(void);
void PlayDraw(void);
void ClearDraw(void);

まずは、どの画面なのか分かる文字だけを表示します。

void TitleDraw(void)
{
    DrawString(
        100, 150,
        "SLIDE PUZZLE",
        GetColor(255, 255, 255));

    DrawString(
        100, 200,
        "CLICK TO START",
        GetColor(255, 255, 255));
}

void PlayDraw(void)
{
    DrawString(
        100, 150,
        "PLAY",
        GetColor(255, 255, 255));
}

void ClearDraw(void)
{
    DrawString(
        100, 150,
        "CLEAR!",
        GetColor(255, 255, 255));
}

まだクリックでは切り替えません。

まずは、stateの値に応じて違うDraw関数を呼び出せるようにします。


3.ゲームループで表示する画面を切り替えよう

DxLibでは、ウィンドウを表示している間、同じ処理を何度も繰り返します。

while (ProcessMessage() == 0)
{
    ClearDrawScreen();

    // この中でゲームを更新・描画する

    ScreenFlip();
}

このゲームループの中で、現在のstateを調べます。

穴埋め

switch (state)
{
case GAME_STATE_TITLE:
    // タイトル画面を描く
    ______________________________;
    break;

case GAME_STATE_PLAY:
    // プレイ画面を描く
    ______________________________;
    break;

case GAME_STATE_CLEAR:
    // クリア画面を描く
    ______________________________;
    break;
}
模範解答を見る
switch (state)
{
case GAME_STATE_TITLE:
    TitleDraw();
    break;

case GAME_STATE_PLAY:
    PlayDraw();
    break;

case GAME_STATE_CLEAR:
    ClearDraw();
    break;
}

ここで実行してみよう

最初は、

enum GameState state = GAME_STATE_TITLE;

なので、タイトル画面が表示されます。

確認できたら、一時的に、

enum GameState state = GAME_STATE_PLAY;

へ変更して実行してみましょう。

PLAYと表示されたら、今度は、

enum GameState state = GAME_STATE_CLEAR;

にしてCLEAR!も確認します。

3つとも確認できたら、最後に、

enum GameState state = GAME_STATE_TITLE;

へ戻してください。

ここまでで、

stateの値によって表示する画面を切り替える

ところまでできました。


4.次はUpdateを作ろう

今はstateの値をソースコードで書き換えないと、画面が変わりません。

ゲーム中は、クリックなどの操作によってstateを変更したいので、今度はUpdate関数を作ります。

今回は、

  • Update:入力を調べたり、変数やゲームの状態を変更したりする
  • Draw:画面へ描く

という役割に分けます。

お約束:関数の中から、元の変数を書き換えたいとき

ここで、C言語のお約束を1つだけ覚えておきましょう。

普通に関数へ値を渡した場合、関数の中でその値を変更しても、呼び出し元にある元の変数は変わりません。

でも今回は、TitleUpdate()などの関数の中から、WinMain()にあるstateを書き換えたいです。

このように、

関数の中から、呼び出し元にある変数の値を変更したい

ときは、C言語ではポインタというものを使います。

ポインタの詳しい仕組みは、今はまだ分からなくて大丈夫です。

まず、すごく小さいプログラムで違いを見てみましょう。

このプログラムはポインタの動きを確認するためだけのサンプルです。
今作っているDxLibのソースへ追加する必要はありません。

値を渡す場合と、アドレスを渡す場合

まずは、次の図でイメージをつかんでみましょう。

値を渡す場合

左側は、同じゲームをもう1本コピーして、別のファミコンで遊んでいるイメージです。

最初はどちらも同じ状態ですが、コピーしたあとは別々のゲームです。

片方のキャラクターを動かしても、もう片方のキャラクターは動きません。

C言語でも同じように、

ChangeNumber(number);

と普通に値を渡すと、numberに入っている値が引数へコピーされて渡されます。

例えば、

#include <stdio.h>

void ChangeNumber(int value)
{
    value = 20;
}

int main(void)
{
    int number = 10;

    ChangeNumber(number);

    printf("%d\n", number);

    return 0;
}

では、numberに入っている10が引数valueへコピーされます。

関数の中で、

value = 20;

としても、変更しているのはコピーされたvalueです。

元のnumberは変わらないので、実行結果は、

10

になります。

値を渡すと、引数には値のコピーが渡されます。
コピーされた値を変更しても、元の変数は変わりません。

アドレスを渡す場合

右側は、1台のファミコンに2つのコントローラーをつないでいるイメージです。

コントローラーは別でも、操作しているのは同じゲームです。

そのため、どちらのコントローラーから操作しても、同じキャラクターの状態が変わります。

C言語で、関数の中から元の変数を変更したい場合は、

ChangeNumber(&number);

のように、値ではなく元のnumberがある場所を渡します。

#include <stdio.h>

void ChangeNumber(int* pNumber)
{
    *pNumber = 20;
}

int main(void)
{
    int number = 10;

    ChangeNumber(&number);

    printf("%d\n", number);

    return 0;
}

&numberは、

numberが置かれている場所

を表します。

そして、

*pNumber = 20;

とすると、その場所にある値を20へ変更できます。

今回はその場所にあるのが元のnumberなので、実行結果は、

20

になります。

アドレスを渡すと、元の変数がある場所を関数から見つけられます。
その場所の中身を書き換えるので、元の変数そのものが変わります。

今は、

値を渡す
    → コピーを変更するので、元は変わらない

アドレスを渡す
    → 元の場所の中身を変更するので、元も変わる

という違いが分かれば十分です。

ポインタの詳しい仕組みは、ここではまだ考えなくて大丈夫です。

今回のstateも、これと同じです。

// 呼び出す側:stateの場所を渡す
TitleUpdate(&state);

// 受け取る側:*を付けて受け取る
void TitleUpdate(enum GameState* state)

// 関数の中:*を付けて元のstateを書き換える
*state = GAME_STATE_PLAY;

まずは、この3つをセットで「お約束」として覚えてください。

記号の意味は、今のところ次のように考えておけば大丈夫です。

&state
    ↓
stateの場所を渡す

enum GameState* state
    ↓
stateの場所を受け取る

*state = ...
    ↓
その場所にある元のstateを書き換える

このあとも、

呼び出す側では &変数
受け取る側では 型* 変数名
書き換えるときは *変数名

という形が何度も出てきます。

今は「元の変数を書き換えたいときのC言語のお約束」として、この書き方を覚えておきましょう。

次の3つを追加します。

void TitleUpdate(enum GameState* state);
void PlayUpdate(enum GameState* state);
void ClearUpdate(enum GameState* state);

Updateにはstateの場所を渡します。

そのため、Updateの中から、

*state = GAME_STATE_PLAY;

のように、WinMain()にあるstateを書き換えられます。


5.クリックした瞬間を調べよう

マウスのボタンは、押している間は何フレームも「押されている」状態になります。

画面切り替えには「押している間」ではなく、「押した瞬間」だけを使います。

今回は次の関数をそのまま追加してください。

int IsMouseLeftPressed(void)
{
    static int previousInput = 0;

    int currentInput = GetMouseInput();

    int pressed =
        (currentInput & MOUSE_INPUT_LEFT) != 0 &&
        (previousInput & MOUSE_INPUT_LEFT) == 0;

    previousInput = currentInput;

    return pressed;
}

この関数では、

GetMouseInput()

でマウスの入力状態を取得しています。

ここで出てくる&は、さっきのアドレスを表す&ではありません。

currentInput & MOUSE_INPUT_LEFT

の&は、ANDの論理演算として使っています。

また、

&&

は、2つの論理式を結合する演算子です。

左側の条件が真
かつ
右側の条件も真

のときだけ、全体の結果が真になります。

ここで、

currentInput

は現在の入力状態、

previousInput

は1フレーム前の入力状態です。

つまり、

currentInput & MOUSE_INPUT_LEFT

は、

現在、左クリックしているか?

を調べています。

一方、

previousInput & MOUSE_INPUT_LEFT

は、

1フレーム前、左クリックしていたか?

を調べています。

今回やりたいのは、

1フレーム前は左クリックしていない
        かつ
現在は左クリックしている

という状態です。

ということは、

int pressed =
    (currentInput & MOUSE_INPUT_LEFT) != 0 &&
    (previousInput & MOUSE_INPUT_LEFT) == 0;

の、

(currentInput & MOUSE_INPUT_LEFT) != 0

は、

現在は左クリックしている

そして、

(previousInput & MOUSE_INPUT_LEFT) == 0

は、

1フレーム前は左クリックしていない

という意味になります。

この2つを&&でつなぐことで、

前のフレームでは押していなかったのに、今のフレームでは押している

つまり、左クリックした瞬間だけpressedが真になります。

最後に、

previousInput = currentInput;

として、現在の入力状態を保存しておきます。

次のフレームでは、この値が「1フレーム前の入力」として使われます。


6.タイトル画面からプレイ画面へ進もう

まずはタイトル画面のUpdateを作ります。

穴埋め

void TitleUpdate(enum GameState* state)
{
    // マウスの左ボタンを押した瞬間か調べる
    if (______________________________)
    {
        // ゲームの状態をプレイ画面へ変更する
        ______________________________;
    }
}
模範解答を見る
void TitleUpdate(enum GameState* state)
{
    if (IsMouseLeftPressed())
    {
        *state = GAME_STATE_PLAY;
    }
}

7.PLAYとCLEARも切り替えよう

今回は状態遷移そのものを確認したいので、

TITLE → PLAY → CLEAR → TITLE

と、クリックするたびに次の画面へ進むようにします。

PLAYからCLEARへ進む処理は、あとで本当のクリア判定に置き換えます。

穴埋め

void PlayUpdate(enum GameState* state)
{
    // マウスの左ボタンを押した瞬間か調べる
    if (______________________________)
    {
        // ゲームの状態をクリア画面へ変更する
        ______________________________;
    }
}

void ClearUpdate(enum GameState* state)
{
    // マウスの左ボタンを押した瞬間か調べる
    if (______________________________)
    {
        // ゲームの状態をタイトル画面へ変更する
        ______________________________;
    }
}
模範解答を見る
void PlayUpdate(enum GameState* state)
{
    if (IsMouseLeftPressed())
    {
        *state = GAME_STATE_CLEAR;
    }
}

void ClearUpdate(enum GameState* state)
{
    if (IsMouseLeftPressed())
    {
        *state = GAME_STATE_TITLE;
    }
}

8.ゲームループからUpdateも呼ぼう

Drawだけだったゲームループへ、Updateを追加します。

穴埋め

switch (state)
{
case GAME_STATE_TITLE:
    // タイトル画面の更新処理を呼ぶ
    ______________________________;

    // タイトル画面を描く
    TitleDraw();
    break;

case GAME_STATE_PLAY:
    // プレイ画面の更新処理を呼ぶ
    ______________________________;

    // プレイ画面を描く
    PlayDraw();
    break;

case GAME_STATE_CLEAR:
    // クリア画面の更新処理を呼ぶ
    ______________________________;

    // クリア画面を描く
    ClearDraw();
    break;
}
模範解答を見る
switch (state)
{
case GAME_STATE_TITLE:
    TitleUpdate(&state);
    TitleDraw();
    break;

case GAME_STATE_PLAY:
    PlayUpdate(&state);
    PlayDraw();
    break;

case GAME_STATE_CLEAR:
    ClearUpdate(&state);
    ClearDraw();
    break;
}

9.動作確認しよう

実行して、次の順番を確認します。

  1. 起動するとSLIDE PUZZLEが表示される
  2. クリックするとPLAYへ変わる
  3. もう一度クリックするとCLEAR!へ変わる
  4. もう一度クリックするとタイトルへ戻る
  5. 押しっぱなしにしても、一気に複数の画面を通過しない

ここまで動けば、第1回は完成です。


10.第1回の完成コード

途中で分からなくなった場合は、完成した形と比べて確認してください。

第1回の完成コードを見る
#include "DxLib.h"

#define SCREEN_WIDTH  480
#define SCREEN_HEIGHT 480

enum GameState
{
    GAME_STATE_TITLE,
    GAME_STATE_PLAY,
    GAME_STATE_CLEAR
};

int IsMouseLeftPressed(void);

void TitleUpdate(enum GameState* state);
void TitleDraw(void);

void PlayUpdate(enum GameState* state);
void PlayDraw(void);

void ClearUpdate(enum GameState* state);
void ClearDraw(void);

int WINAPI WinMain(
    HINSTANCE hInstance,
    HINSTANCE hPrevInstance,
    LPSTR lpCmdLine,
    int nCmdShow)
{
    enum GameState state = GAME_STATE_TITLE;

    ChangeWindowMode(TRUE);
    SetGraphMode(SCREEN_WIDTH, SCREEN_HEIGHT, 32);
    SetMainWindowText("Slide Puzzle");

    if (DxLib_Init() != 0)
        return -1;

    SetDrawScreen(DX_SCREEN_BACK);

    while (ProcessMessage() == 0)
    {
        ClearDrawScreen();

        DrawBox(
            0, 0,
            SCREEN_WIDTH, SCREEN_HEIGHT,
            GetColor(70, 100, 110),
            TRUE);

        switch (state)
        {
        case GAME_STATE_TITLE:
            TitleUpdate(&state);
            TitleDraw();
            break;

        case GAME_STATE_PLAY:
            PlayUpdate(&state);
            PlayDraw();
            break;

        case GAME_STATE_CLEAR:
            ClearUpdate(&state);
            ClearDraw();
            break;
        }

        ScreenFlip();
    }

    DxLib_End();

    return 0;
}

int IsMouseLeftPressed(void)
{
    static int previousInput = 0;

    int currentInput = GetMouseInput();

    int pressed =
        (currentInput & MOUSE_INPUT_LEFT) != 0 &&
        (previousInput & MOUSE_INPUT_LEFT) == 0;

    previousInput = currentInput;

    return pressed;
}

void TitleUpdate(enum GameState* state)
{
    if (IsMouseLeftPressed())
    {
        *state = GAME_STATE_PLAY;
    }
}

void TitleDraw(void)
{
    DrawString(
        100, 150,
        "SLIDE PUZZLE",
        GetColor(255, 255, 255));

    DrawString(
        100, 200,
        "CLICK TO START",
        GetColor(255, 255, 255));
}

void PlayUpdate(enum GameState* state)
{
    if (IsMouseLeftPressed())
    {
        *state = GAME_STATE_CLEAR;
    }
}

void PlayDraw(void)
{
    DrawString(
        100, 150,
        "PLAY",
        GetColor(255, 255, 255));
}

void ClearUpdate(enum GameState* state)
{
    if (IsMouseLeftPressed())
    {
        *state = GAME_STATE_TITLE;
    }
}

void ClearDraw(void)
{
    DrawString(
        100, 150,
        "CLEAR!",
        GetColor(255, 255, 255));
}

今回できるようになったこと

ゲームループの中で、現在の状態に応じて処理を切り替えられるようになりました。

TITLE
  ↓
PLAY
  ↓
CLEAR
  ↓
TITLE

さらに、

Update
    入力や状態の変更

Draw
    画面への描画

と役割を分けました。

次回から、PLAYの中へコンソール版で作ったスライドパズルを入れていきます。