Skip to main content

第12回 TITLE・PLAY・CLEARで仕上げよう

第12回 TITLE・PLAY・CLEARで仕上げよう

ここまでで、

  • ステージのルール
  • 画像表示
  • 滑らかな移動
  • 効果音
  • BGM

まで完成しました。

最後は、ゲーム全体を

TITLE
PLAY
CLEAR

の3つの状態に分けて仕上げます。

図12-01 TITLE・PLAY・CLEARの状態遷移

今回の流れ

TITLE
  ↓ ENTER
PLAY
  ↓ クリア
CLEAR
  ↓ ENTER
次のPLAY または TITLE

1.今のゲーム状態を表そう

今どの画面なのかを、GameStateという列挙型で表します。

enum GameState
{
    GAME_STATE_TITLE,
    GAME_STATE_PLAY,
    GAME_STATE_CLEAR
};

それぞれ、

GAME_STATE_TITLE
    → タイトル画面

GAME_STATE_PLAY
    → ゲームプレイ中

GAME_STATE_CLEAR
    → クリア画面

です。

WinMain()では、現在の状態をstateという変数で持ちます。

最初はタイトル画面なので、

GameState state = GAME_STATE_TITLE;

から始めます。


2.状態ごとの処理を関数へ分けよう

3つの状態では、更新処理と描画処理がそれぞれ違います。

TITLE
    TitleUpdate()
    TitleDraw()

PLAY
    PlayUpdate()
    PlayDraw()

CLEAR
    ClearUpdate()
    ClearDraw()

それぞれの役割は次の通りです。

TitleUpdate()は、ENTERが押されたらゲームを開始します。

TitleDraw()は、タイトル画面を描きます。

PlayUpdate()は、第11回まで作ってきたゲームの更新処理を行います。

PlayDraw()は、ゲーム画面を描きます。

ClearUpdate()は、ENTERが押されたら次のステージ、またはタイトルへ進みます。

ClearDraw()は、クリア画面を描きます。

先にプロトタイプ宣言を追加します。

void TitleUpdate(Map* map, int currentStage, GameSounds* sounds, GameState* state, int enterPressed);

void TitleDraw(void);

void PlayUpdate(Map* map, int currentStage, GameSounds* sounds, GameState* state, int retryPressed);

void PlayDraw(Map* map, int currentStage, GameImages* images);

void ClearUpdate(Map* map, int* currentStage, GameSounds* sounds, GameState* state, int enterPressed);

void ClearDraw(int currentStage);

では、状態ごとに作っていきましょう。


3.ゲームループを状態で切り替えよう

WinMain()では、switchを使って現在のstateに合う処理だけを呼びます。

ENTERとRキーは、どの状態でも同じゲームループ内で調べるので、switchより前で取得します。

// 状態に関係なく、ENTERとRは毎フレーム調べる
int enterPressed = IsKeyPressed(KEY_INPUT_RETURN);
int retryPressed = IsKeyPressed(KEY_INPUT_R);

switch (state)
{
case GAME_STATE_TITLE:
    // タイトルの更新と描画
    ________________________________;
    ________________________________;
    break;

case GAME_STATE_PLAY:
    // ゲーム中の更新と描画
    ________________________________;
    ________________________________;
    break;

case GAME_STATE_CLEAR:
    // クリア画面の更新と描画
    ________________________________;
    ________________________________;
    break;
}

ヒント

先ほど用意した、

TitleUpdate() / TitleDraw()
PlayUpdate()  / PlayDraw()
ClearUpdate() / ClearDraw()

を、それぞれの状態で呼びます。

模範解答を見る
case GAME_STATE_TITLE:
    TitleUpdate(&currentMap, currentStage, &sounds, &state, enterPressed);
    TitleDraw();
    break;

case GAME_STATE_PLAY:
    PlayUpdate(&currentMap, currentStage, &sounds, &state, retryPressed);
    PlayDraw(&currentMap, currentStage, &images);
    break;

case GAME_STATE_CLEAR:
    ClearUpdate(&currentMap, &currentStage, &sounds, &state, enterPressed);
    ClearDraw(currentStage);
    break;

4.TITLEを作ろう

まず、タイトル画面を作ります。

TITLEでは、

画面を表示
        ↓
ENTERを待つ
        ↓
ENTERが押された
        ↓
ステージを読み込む
        ↓
移動情報をリセット
        ↓
BGMを開始
        ↓
PLAYへ切り替える

という処理をします。

TitleUpdate

void TitleUpdate(Map* map, int currentStage, GameSounds* sounds, GameState* state, int enterPressed)
{
    if (enterPressed)
    {
        // 現在のステージを読み込む
        ________________________________;

        // 前の移動情報を残さない
        moveDir = DIR_NONE;
        lastMoveResult = MOVE_NONE;

        // ゲーム中のBGMを開始する
        ________________________________;

        // 次のフレームからPLAYにする
        *state = ________________________________;
    }
}

ヒント

ステージ読み込み → LoadStage(map, currentStage)
BGMループ再生    → PlaySoundMem(sounds->bgm, DX_PLAYTYPE_LOOP, TRUE)
次の状態         → GAME_STATE_PLAY
模範解答を見る
LoadStage(map, currentStage);
PlaySoundMem(sounds->bgm, DX_PLAYTYPE_LOOP, TRUE);
*state = GAME_STATE_PLAY;

TitleDraw

タイトル画面は文字で表示します。

void TitleDraw(void)
{
    DrawString(250, 215, "COSTCO MAN", GetColor(255, 255, 255));

    DrawString(210, 270, "WAREHOUSE PUZZLE", GetColor(255, 220, 100));

    DrawString(225, 340, "PRESS ENTER TO START", GetColor(200, 200, 200));
}

5.PLAYを作ろう

PLAYでは、第11回までのゲーム処理をそのまま使います。

大きく変える必要はありません。

処理の流れは、

移動アニメーション中?
    YES → 終わるまで待つ
        ↓ NO
クリアしている?
    YES → BGM停止
          クリア音
          CLEARへ
        ↓ NO
Rキーならステージを読み直す
        ↓
方向入力を調べる
        ↓
MoveObject()
        ↓
効果音
        ↓
移動できたら
moveStartPos / moveDir / moveStartTime / lastMoveResult を保存

です。

PlayUpdate

第11回のUpdateGame()を、PLAY専用のPlayUpdate()として使います。

void PlayUpdate(Map* map, int currentStage, GameSounds* sounds, GameState* state, int retryPressed)
{
    // 移動アニメーション中なら、終わるまで次の処理へ進まない
    if (GetMoveRate() < 1.0f)
        return;

    // アニメーションが終わったあとでクリア判定する
    if (CheckClear(map))
    {
        StopSoundMem(sounds->bgm);
        PlaySoundMem(sounds->clear, DX_PLAYTYPE_BACK, TRUE);

        // 次のフレームからCLEARにする
        *state = ________________________________;
        return;
    }

    if (retryPressed)
    {
        LoadStage(map, currentStage);
        moveDir = DIR_NONE;
        lastMoveResult = MOVE_NONE;
        return;
    }

    Direction dir = GetDirection();

    if (dir != DIR_NONE)
    {
        // MoveObject()を呼ぶ前に、移動前の位置を取得する
        Point startPos = GetPlayerPos(map);

        MoveResult result = MoveObject(dir, map);

        // 第11回で作った関数を呼び出す
        PlayMoveSound(result, sounds);

        if (result != MOVE_NONE)
        {
            moveStartPos = startPos;
            moveDir = dir;
            moveStartTime = GetNowCount();
            lastMoveResult = result;
        }
    }
}

ヒント

クリアしたら移る状態は、

GAME_STATE_CLEAR

です。

模範解答を見る
*state = GAME_STATE_CLEAR;

PlayDraw

描画は、第10回までに作ったDrawStageImage()をそのまま使います。

void PlayDraw(Map* map, int currentStage, GameImages* images)
{
    DrawFormatString(24, 20, GetColor(255, 255, 255), "STAGE %d / %d", currentStage + 1, STAGE_COUNT);

    DrawString(300, 20, "WASD : MOVE    R : RETRY", GetColor(200, 200, 200));

    DrawStageImage(map, images);
}

6.CLEARを作ろう

CLEARでは、ENTERが押されたらステージ番号を1つ進めます。

ENTER
  ↓
currentStageを1増やす
  ↓
まだ次のステージがある?
  ├─ YES → 次のステージを読み込み、PLAYへ
  └─ NO  → 0へ戻してTITLEへ

ClearUpdate

void ClearUpdate(Map* map, int* currentStage, GameSounds* sounds, GameState* state, int enterPressed)
{
    if (enterPressed)
    {
        (*currentStage)++;

        if (*currentStage >= STAGE_COUNT)
        {
            // 全ステージ終了
            *currentStage = 0;
            moveDir = DIR_NONE;
            lastMoveResult = MOVE_NONE;

            // タイトルへ戻る
            *state = ________________________________;
        }
        else
        {
            // 次のステージを始める
            LoadStage(map, *currentStage);
            moveDir = DIR_NONE;
            lastMoveResult = MOVE_NONE;

            // BGMを再び開始する
            PlaySoundMem(sounds->bgm, DX_PLAYTYPE_LOOP, TRUE);

            // ゲームへ戻る
            *state = ________________________________;
        }
    }
}

ヒント

全ステージ終了 → GAME_STATE_TITLE
次のステージ   → GAME_STATE_PLAY
模範解答を見る
*state = GAME_STATE_TITLE;

// 次のステージがある場合
*state = GAME_STATE_PLAY;

ClearDraw

void ClearDraw(int currentStage)
{
    DrawString(295, 215, "CLEAR!", GetColor(255, 220, 80));

    if (currentStage + 1 >= STAGE_COUNT)
    {
        DrawString(195, 285, "ALL STAGES CLEAR!", GetColor(255, 255, 255));
    }
    else
    {
        DrawFormatString(225, 285, GetColor(255, 255, 255), "STAGE %d CLEAR!", currentStage + 1);
    }

    DrawString(220, 350, "PRESS ENTER", GetColor(200, 200, 200));
}

7.状態とBGMの関係を確認しよう

BGMはPLAYの間だけ流します。

TITLE
    BGMなし
      ↓ ENTER

PLAY
    BGM開始
      ↓ CLEAR

CLEAR
    BGM停止
      ↓ ENTER

次のPLAY
    BGM再開

または

TITLE
    BGMなし

そのため、

  • TitleUpdate()でPLAYへ入るときにBGM開始
  • PlayUpdate()でCLEARへ入るときにBGM停止
  • ClearUpdate()で次のPLAYへ入るときにBGM再開

となっています。


8.最初から最後まで遊んでみよう

最後に、ゲームを最初から最後まで確認します。

起動
↓
TITLE
↓ ENTER
PLAY
↓
移動・荷物押し・効果音・BGM
↓
ステージクリア
↓
CLEAR
↓ ENTER
次のPLAY
↓
全ステージクリア
↓
CLEAR
↓ ENTER
TITLE

確認する項目は次の通りです。

  • 起動するとTITLEから始まる
  • ENTERでPLAYへ進む
  • PLAY中はBGMが流れる
  • コスコマンと荷物が滑らかに動く
  • 移動結果に合った効果音が鳴る
  • クリアするとBGMが止まり、クリア音が鳴る
  • CLEARからENTERで次のステージへ進む
  • 最終ステージのあとTITLEへ戻る
  • TITLEへ戻ったとき、前の移動アニメーションが残らない

これでコスコマン完成です。


9.第12回の完成コード

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

#define SCREEN_WIDTH  720
#define SCREEN_HEIGHT 600

#define MAX_STAGE_WIDTH  10
#define MAX_STAGE_HEIGHT 10
#define STAGE_COUNT 3

enum OBJNAME
{
    FLOOR = 0,
    WALL = 1,
    LUGG = 2,
    GOAL = 3,
    HUMAN = 4,
    HUMAN_ON_GOAL = 5,
    LUGG_ON_GOAL = 6,
    OUTSIDE = 7
};

struct Point
{
    int x;
    int y;
};

struct Map
{
    int stage_width;
    int stage_height;
    int Dat[MAX_STAGE_HEIGHT][MAX_STAGE_WIDTH];
};

enum Direction
{
    DIR_UP,
    DIR_LEFT,
    DIR_DOWN,
    DIR_RIGHT,
    DIR_NONE
};

enum MoveResult
{
    MOVE_NONE,
    MOVE_WALK,
    MOVE_PUSH,
    MOVE_PUSH_GOAL
};

const Point DIR_VECTOR[] = {
    { 0, -1 },
    {-1,  0 },
    { 0, +1 },
    {+1,  0 },
    { 0,  0 }
};

enum GameState
{
    GAME_STATE_TITLE,
    GAME_STATE_PLAY,
    GAME_STATE_CLEAR
};

#define TILE_SIZE 64
#define STAGE_X 40
#define STAGE_Y 90

struct GameImages
{
    int floor;
    int wall;
    int goal;
    int luggage;
    int human;
};

#define MOVE_TIME 200

Point moveStartPos = { 0, 0 };
Direction moveDir = DIR_NONE;
int moveStartTime = 0;
MoveResult lastMoveResult = MOVE_NONE;

struct GameSounds
{
    int move;
    int push;
    int goal;
    int clear;
    int bgm;
};

const Map STAGES[STAGE_COUNT] = {
    {
        10, 7,
        {
            { 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 },
            { 1, 0, 0, 0, 0, 0, 0, 0, 0, 1 },
            { 1, 0, 0, 0, 3, 0, 0, 0, 0, 1 },
            { 1, 0, 0, 0, 2, 0, 0, 0, 0, 1 },
            { 1, 0, 0, 0, 4, 0, 0, 0, 0, 1 },
            { 1, 0, 0, 0, 0, 0, 0, 0, 0, 1 },
            { 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 }
        }
    },
    {
        10, 7,
        {
            { 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 },
            { 1, 0, 0, 0, 1, 0, 0, 0, 0, 1 },
            { 1, 0, 2, 0, 1, 0, 3, 0, 0, 1 },
            { 1, 0, 0, 2, 0, 0, 3, 0, 0, 1 },
            { 1, 0, 0, 0, 1, 1, 0, 0, 0, 1 },
            { 1, 0, 4, 0, 0, 0, 0, 0, 0, 1 },
            { 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 }
        }
    },
    {
        10, 7,
        {
            { 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 },
            { 1, 1, 1, 1, 0, 0, 1, 0, 4, 1 },
            { 1, 1, 1, 0, 0, 0, 1, 0, 0, 1 },
            { 1, 1, 1, 2, 0, 2, 0, 2, 0, 1 },
            { 1, 1, 1, 0, 2, 2, 1, 0, 0, 1 },
            { 1, 3, 3, 3, 3, 3, 0, 0, 1, 1 },
            { 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 }
        }
    }
};

void LoadStage(Map* map, int stageNumber);
Direction GetDirection(void);
Point GetPlayerPos(Map* map);
OBJNAME GetObjectNum(Point pos, Map* map);
void SetObjtoMap(OBJNAME obj, Point pos, Map* map);
int IsKeyPressed(int key);
MoveResult MoveObject(Direction dir, Map* map);
int CheckClear(Map* map);

void LoadGameImages(GameImages* images);
void DeleteGameImages(GameImages* images);
float GetMoveRate(void);
Point GetMoveDrawPos(Point startPos, Direction dir, float rate);
void DrawStageImage(Map* map, GameImages* images);

void LoadGameSounds(GameSounds* sounds);
void DeleteGameSounds(GameSounds* sounds);
void PlayMoveSound(MoveResult result, GameSounds* sounds);

void TitleUpdate(Map* map, int currentStage, GameSounds* sounds, GameState* state, int enterPressed);
void TitleDraw(void);

void PlayUpdate(Map* map, int currentStage, GameSounds* sounds, GameState* state, int retryPressed);
void PlayDraw(Map* map, int currentStage, GameImages* images);

void ClearUpdate(Map* map, int* currentStage, GameSounds* sounds, GameState* state, int enterPressed);
void ClearDraw(int currentStage);

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
    Map currentMap;
    GameImages images;
    GameSounds sounds;
    int currentStage = 0;
    GameState state = GAME_STATE_TITLE;

    ChangeWindowMode(TRUE);
    SetGraphMode(SCREEN_WIDTH, SCREEN_HEIGHT, 32);
    SetMainWindowText("Costco Man");

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

    SetDrawScreen(DX_SCREEN_BACK);

    LoadGameImages(&images);
    LoadGameSounds(&sounds);
    LoadStage(&currentMap, currentStage);

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

        // 状態に関係なく、ENTERとRは毎フレーム調べる
        int enterPressed = IsKeyPressed(KEY_INPUT_RETURN);
        int retryPressed = IsKeyPressed(KEY_INPUT_R);

        switch (state)
        {
        case GAME_STATE_TITLE:
            TitleUpdate(&currentMap, currentStage, &sounds, &state, enterPressed);
            TitleDraw();
            break;

        case GAME_STATE_PLAY:
            PlayUpdate(&currentMap, currentStage, &sounds, &state, retryPressed);
            PlayDraw(&currentMap, currentStage, &images);
            break;

        case GAME_STATE_CLEAR:
            ClearUpdate(&currentMap, &currentStage, &sounds, &state, enterPressed);
            ClearDraw(currentStage);
            break;
        }

        ScreenFlip();
    }

    StopSoundMem(sounds.bgm);
    DeleteGameSounds(&sounds);
    DeleteGameImages(&images);
    DxLib_End();
    return 0;
}

void LoadStage(Map* map, int stageNumber)
{
    map->stage_width = STAGES[stageNumber].stage_width;
    map->stage_height = STAGES[stageNumber].stage_height;

    for (int y = 0; y < map->stage_height; y++)
    {
        for (int x = 0; x < map->stage_width; x++)
        {
            map->Dat[y][x] = STAGES[stageNumber].Dat[y][x];
        }
    }
}

Direction GetDirection(void)
{
    // 4方向のキーが押された瞬間を調べる
    int up = IsKeyPressed(KEY_INPUT_W);
    int left = IsKeyPressed(KEY_INPUT_A);
    int down = IsKeyPressed(KEY_INPUT_S);
    int right = IsKeyPressed(KEY_INPUT_D);

    if (up)
    {
        return DIR_UP;
    }
    if (left)
    {
        return DIR_LEFT;
    }
    if (down)
    {
        return DIR_DOWN;
    }
    if (right)
    {
        return DIR_RIGHT;
    }

    return DIR_NONE;
}

Point GetPlayerPos(Map* map)
{
    Point result = { -1, -1 };

    for (int y = 0; y < map->stage_height; y++)
    {
        for (int x = 0; x < map->stage_width; x++)
        {
            int obj = map->Dat[y][x];

            if (obj == HUMAN || obj == HUMAN_ON_GOAL)
            {
                result.x = x;
                result.y = y;
                return result;
            }
        }
    }

    return result;
}

OBJNAME GetObjectNum(Point pos, Map* map)
{
    if (pos.x < 0 || pos.x >= map->stage_width || pos.y < 0 || pos.y >= map->stage_height)
        return OUTSIDE;

    return (OBJNAME)map->Dat[pos.y][pos.x];
}

void SetObjtoMap(OBJNAME obj, Point pos, Map* map)
{
    map->Dat[pos.y][pos.x] = obj;
}

int IsKeyPressed(int key)
{
    static int previous[256] = { 0 };
    int current = CheckHitKey(key);
    int pressed = 0;
    if (current != 0 && previous[key] == 0)
    {
        pressed = 1;
    }

    previous[key] = current;
    return pressed;
}

MoveResult MoveObject(Direction dir, Map* map)
{
    Point playerPos = GetPlayerPos(map);

    Point nextPos = {
        playerPos.x + DIR_VECTOR[dir].x,
        playerPos.y + DIR_VECTOR[dir].y
    };

    OBJNAME current = GetObjectNum(playerPos, map);
    OBJNAME next = GetObjectNum(nextPos, map);

    if (next == FLOOR || next == GOAL)
    {
        if (current == HUMAN_ON_GOAL)
            SetObjtoMap(GOAL, playerPos, map);
        else
            SetObjtoMap(FLOOR, playerPos, map);

        if (next == GOAL)
            SetObjtoMap(HUMAN_ON_GOAL, nextPos, map);
        else
            SetObjtoMap(HUMAN, nextPos, map);

        return MOVE_WALK;
    }

    if (next == LUGG || next == LUGG_ON_GOAL)
    {
        Point nextNextPos = {
            nextPos.x + DIR_VECTOR[dir].x,
            nextPos.y + DIR_VECTOR[dir].y
        };

        OBJNAME nextNext = GetObjectNum(nextNextPos, map);

        if (nextNext != FLOOR && nextNext != GOAL)
            return MOVE_NONE;

        if (current == HUMAN_ON_GOAL)
            SetObjtoMap(GOAL, playerPos, map);
        else
            SetObjtoMap(FLOOR, playerPos, map);

        if (next == LUGG_ON_GOAL)
            SetObjtoMap(HUMAN_ON_GOAL, nextPos, map);
        else
            SetObjtoMap(HUMAN, nextPos, map);

        if (nextNext == GOAL)
        {
            SetObjtoMap(LUGG_ON_GOAL, nextNextPos, map);
            return MOVE_PUSH_GOAL;
        }

        SetObjtoMap(LUGG, nextNextPos, map);
        return MOVE_PUSH;
    }

    return MOVE_NONE;
}

int CheckClear(Map* map)
{
    for (int y = 0; y < map->stage_height; y++)
    {
        for (int x = 0; x < map->stage_width; x++)
        {
            if (map->Dat[y][x] == LUGG)
                return 0;
        }
    }

    return 1;
}


void LoadGameImages(GameImages* images)
{
    images->floor = LoadGraph("images/floor.png");
    images->wall = LoadGraph("images/wall.png");
    images->goal = LoadGraph("images/goal.png");
    images->luggage = LoadGraph("images/luggage.png");
    images->human = LoadGraph("images/human.png");
}

void DeleteGameImages(GameImages* images)
{
    DeleteGraph(images->floor);
    DeleteGraph(images->wall);
    DeleteGraph(images->goal);
    DeleteGraph(images->luggage);
    DeleteGraph(images->human);
}

float GetMoveRate(void)
{
    if (moveDir == DIR_NONE)
        return 1.0f;

    int elapsed = GetNowCount() - moveStartTime;
    float rate = (float)elapsed / MOVE_TIME;

    if (rate < 0.0f)
        rate = 0.0f;

    if (rate > 1.0f)
        rate = 1.0f;

    return rate;
}

Point GetMoveDrawPos(Point startPos, Direction dir, float rate)
{
    Point moveVector = DIR_VECTOR[dir];

    Point drawPos = {
        STAGE_X + startPos.x * TILE_SIZE + (int)(moveVector.x * TILE_SIZE * rate),
        STAGE_Y + startPos.y * TILE_SIZE + (int)(moveVector.y * TILE_SIZE * rate)
    };

    return drawPos;
}

void DrawStageImage(Map* map, GameImages* images)
{
    float rate = GetMoveRate();
    Point moveVector = DIR_VECTOR[moveDir];

    // 荷物を押したとき、荷物はプレイヤーの1マス先から動く
    Point luggageStartPos = {
        moveStartPos.x + moveVector.x,
        moveStartPos.y + moveVector.y
    };

    // Map上では荷物はすでに1マス移動している
    Point luggageEndPos = {
        luggageStartPos.x + moveVector.x,
        luggageStartPos.y + moveVector.y
    };

    for (int y = 0; y < map->stage_height; y++)
    {
        for (int x = 0; x < map->stage_width; x++)
        {
            int obj = map->Dat[y][x];
            int drawX = STAGE_X + x * TILE_SIZE;
            int drawY = STAGE_Y + y * TILE_SIZE;

            if (obj == WALL)
                DrawGraph(drawX, drawY, images->wall, TRUE);
            else if (obj == GOAL || obj == HUMAN_ON_GOAL || obj == LUGG_ON_GOAL)
                DrawGraph(drawX, drawY, images->goal, TRUE);
            else
                DrawGraph(drawX, drawY, images->floor, TRUE);
        }
    }

    for (int y = 0; y < map->stage_height; y++)
    {
        for (int x = 0; x < map->stage_width; x++)
        {
            int obj = map->Dat[y][x];
            int drawX = STAGE_X + x * TILE_SIZE;
            int drawY = STAGE_Y + y * TILE_SIZE;

            if (obj == HUMAN || obj == HUMAN_ON_GOAL)
            {
                if (rate < 1.0f)
                {
                    Point drawPos = GetMoveDrawPos(moveStartPos, moveDir, rate);
                    drawX = drawPos.x;
                    drawY = drawPos.y;
                }

                DrawGraph(drawX, drawY, images->human, TRUE);
            }
            else if (obj == LUGG || obj == LUGG_ON_GOAL)
            {
                // 荷物を押した移動の途中で、今回動いた荷物だけ描画位置を変える
                if (rate < 1.0f &&
                    (lastMoveResult == MOVE_PUSH ||
                     lastMoveResult == MOVE_PUSH_GOAL) &&
                    x == luggageEndPos.x &&
                    y == luggageEndPos.y)
                {
                    Point drawPos = GetMoveDrawPos(luggageStartPos, moveDir, rate);
                    drawX = drawPos.x;
                    drawY = drawPos.y;
                }

                DrawGraph(drawX, drawY, images->luggage, TRUE);
            }
        }
    }
}





void LoadGameSounds(GameSounds* sounds)
{
    sounds->move = LoadSoundMem("sounds/move.wav");
    sounds->push = LoadSoundMem("sounds/push.wav");
    sounds->goal = LoadSoundMem("sounds/goal.wav");
    sounds->clear = LoadSoundMem("sounds/clear.wav");
    sounds->bgm = LoadSoundMem("sounds/bgm.wav");
}

void DeleteGameSounds(GameSounds* sounds)
{
    DeleteSoundMem(sounds->move);
    DeleteSoundMem(sounds->push);
    DeleteSoundMem(sounds->goal);
    DeleteSoundMem(sounds->clear);
    DeleteSoundMem(sounds->bgm);
}

void PlayMoveSound(MoveResult result, GameSounds* sounds)
{
    if (result == MOVE_WALK)
        PlaySoundMem(sounds->move, DX_PLAYTYPE_BACK, TRUE);
    else if (result == MOVE_PUSH)
        PlaySoundMem(sounds->push, DX_PLAYTYPE_BACK, TRUE);
    else if (result == MOVE_PUSH_GOAL)
        PlaySoundMem(sounds->goal, DX_PLAYTYPE_BACK, TRUE);
}


void TitleUpdate(Map* map, int currentStage, GameSounds* sounds, GameState* state, int enterPressed)
{
    if (enterPressed)
    {
        LoadStage(map, currentStage);
        moveDir = DIR_NONE;
        lastMoveResult = MOVE_NONE;
        PlaySoundMem(sounds->bgm, DX_PLAYTYPE_LOOP, TRUE);
        *state = GAME_STATE_PLAY;
    }
}

void TitleDraw(void)
{
    DrawString(250, 215, "COSTCO MAN", GetColor(255, 255, 255));
    DrawString(210, 270, "WAREHOUSE PUZZLE", GetColor(255, 220, 100));
    DrawString(225, 340, "PRESS ENTER TO START", GetColor(200, 200, 200));
}

void PlayUpdate(Map* map, int currentStage, GameSounds* sounds, GameState* state, int retryPressed)
{
    if (GetMoveRate() < 1.0f)
        return;

    if (CheckClear(map))
    {
        StopSoundMem(sounds->bgm);
        PlaySoundMem(sounds->clear, DX_PLAYTYPE_BACK, TRUE);
        *state = GAME_STATE_CLEAR;
        return;
    }

    if (retryPressed)
    {
        LoadStage(map, currentStage);
        moveDir = DIR_NONE;
        lastMoveResult = MOVE_NONE;
        return;
    }

    Direction dir = GetDirection();

    if (dir != DIR_NONE)
    {
        // 移動する前のプレイヤー位置を先に覚えておく
        Point startPos = GetPlayerPos(map);

        MoveResult result = MoveObject(dir, map);
        PlayMoveSound(result, sounds);

        if (result != MOVE_NONE)
        {
            moveStartPos = startPos;
            moveDir = dir;
            moveStartTime = GetNowCount();
            lastMoveResult = result;
        }
    }
}

void PlayDraw(Map* map, int currentStage, GameImages* images)
{
    DrawFormatString(24, 20, GetColor(255, 255, 255), "STAGE %d / %d", currentStage + 1, STAGE_COUNT);
    DrawString(300, 20, "WASD : MOVE    R : RETRY", GetColor(200, 200, 200));
    DrawStageImage(map, images);
}

void ClearUpdate(Map* map, int* currentStage, GameSounds* sounds, GameState* state, int enterPressed)
{
    if (enterPressed)
    {
        (*currentStage)++;

        if (*currentStage >= STAGE_COUNT)
        {
            *currentStage = 0;
            moveDir = DIR_NONE;
            lastMoveResult = MOVE_NONE;
            *state = GAME_STATE_TITLE;
        }
        else
        {
            LoadStage(map, *currentStage);
            moveDir = DIR_NONE;
            lastMoveResult = MOVE_NONE;
            PlaySoundMem(sounds->bgm, DX_PLAYTYPE_LOOP, TRUE);
            *state = GAME_STATE_PLAY;
        }
    }
}

void ClearDraw(int currentStage)
{
    DrawString(295, 215, "CLEAR!", GetColor(255, 220, 80));

    if (currentStage + 1 >= STAGE_COUNT)
        DrawString(195, 285, "ALL STAGES CLEAR!", GetColor(255, 255, 255));
    else
        DrawFormatString(225, 285, GetColor(255, 255, 255), "STAGE %d CLEAR!", currentStage + 1);

    DrawString(220, 350, "PRESS ENTER", GetColor(200, 200, 200));
}