Skip to main content

第10回 荷物も滑らかに動かそう

第10回 荷物も滑らかに動かそう

第9回で、コスコマンは200msかけて1マス動くようになりました。

荷物を押したときは、コスコマンだけでなく荷物も同じ方向へ1マス動きます。

今回は、第9回で作った移動アニメーションをそのまま荷物にも使います。

今回の流れ

MoveObject()を実行
        ↓
荷物を押した移動だった?
        ↓ YES
荷物はコスコマンの1マス先から動く
        ↓
第9回のGetMoveDrawPos()で描く

新しい補間計算は作りません。


1.荷物を押したかどうかは移動結果で分かる

第6回で作ったMoveObject()は、移動した結果をMoveResultで返しています。

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

このうち、

MOVE_PUSH
MOVE_PUSH_GOAL

なら、コスコマンと一緒に荷物も1マス動いたことが分かります。

荷物の前が床かゴールかを、第10回でもう一度調べる必要はありません。
その判定は、すでにMoveObject()の中で行っています。


2.最後の移動結果を覚えておこう

描画するときにも「今の移動で荷物を押したか」を使いたいので、最後の移動結果をlastMoveResultに覚えておきます。

moveStartTimeの下へ追加してください。

MoveResult lastMoveResult = MOVE_NONE;

第9回で、移動できたときに次の3つを保存しました。

moveStartPos = startPos;
moveDir = dir;
moveStartTime = GetNowCount();

ここへ、移動結果の保存を1行追加します。

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

    // 今回の移動結果を覚えておく
    lastMoveResult = ____________________;
}

図10-01 プレイヤーと荷物の開始位置

ヒント

MoveObject()の結果はresultに入っています。

模範解答を見る
lastMoveResult = result;

ステージを読み直すときは、

lastMoveResult = MOVE_NONE;

へ戻しておきます。


3.荷物はコスコマンの1マス先から動く

荷物を押す前は、

コスコマン → 荷物

と並んでいます。

第9回で保存したmoveStartPosは、コスコマンの移動前の位置です。

移動方向を表すベクトルをmoveVectorとすると、

Point moveVector = DIR_VECTOR[moveDir];

荷物の移動前の位置は、その1マス先です。

この位置をluggageStartPosとすると、

luggageStartPos
= moveStartPos + moveVector

です。

Point luggageStartPos = {
    moveStartPos.x + ____________________,
    moveStartPos.y + ____________________
};

ヒント

1マス分の移動方向はmoveVector.x、moveVector.yです。

模範解答を見る
Point luggageStartPos = {
    moveStartPos.x + moveVector.x,
    moveStartPos.y + moveVector.y
};

MoveObject()を呼んだあと、Map上の荷物はすでにもう1マス先へ移動しています。

その位置をluggageEndPosとすると、

Point luggageEndPos = {
    luggageStartPos.x + moveVector.x,
    luggageStartPos.y + moveVector.y
};

となります。


4.荷物にも同じ描画処理を使おう

コスコマンは第9回で、

GetMoveDrawPos(moveStartPos, moveDir, rate)

を使って描きました。

荷物も開始位置が1マス違うだけです。

GetMoveDrawPos(luggageStartPos, moveDir, rate)

を使えば、同じ方向へ同じ速さで動きます。

Map上では荷物はすでにluggageEndPosへ移動しています。

そこで、荷物を描くところで、

移動アニメーション中
かつ
MOVE_PUSH または MOVE_PUSH_GOAL
かつ
今描こうとしている荷物が luggageEndPos

のときだけ、描画位置を変更します。

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)
    {
        // 第9回で作った関数をそのまま使う
        Point drawPos =
            ________________________________________________;

        drawX = drawPos.x;
        drawY = drawPos.y;
    }

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

図10-02 同じrateで一緒に動く

ヒント

開始位置だけluggageStartPosへ変えます。

模範解答を見る
Point drawPos =
    GetMoveDrawPos(luggageStartPos, moveDir, rate);

これで、

コスコマン
    moveStartPosから移動

荷物
    その1マス先のluggageStartPosから移動

となり、どちらも同じmoveDirとrateで動きます。


5.DrawStageImageの最初に位置を用意しよう

DrawStageImage()では、最初に第9回のrateを求め、続けて荷物の開始位置と移動後の位置を用意します。

float rate = GetMoveRate();
Point moveVector = DIR_VECTOR[moveDir];

Point luggageStartPos = {
    moveStartPos.x + moveVector.x,
    moveStartPos.y + moveVector.y
};

Point luggageEndPos = {
    luggageStartPos.x + moveVector.x,
    luggageStartPos.y + moveVector.y
};

荷物を押していないときは、これらの値は描画には使われません。

MOVE_PUSHまたはMOVE_PUSH_GOALのときだけ、先ほどの荷物描画の条件に入ります。


6.実行して確認しよう

次を確認してください。

  • 普通に歩くと、コスコマンだけ滑らかに動く
  • 荷物を押すと、コスコマンと荷物が同じ方向へ同時に動く
  • 押していない荷物は動かない
  • ゴールへ押し込むときも荷物が滑らかに動く
  • 壁などで押せなかったときは荷物が動かない
  • 第9回で作ったクリア判定のタイミングはそのまま動く

7.第10回の完成コード

第10回の完成コードを見る
#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 }
};

#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;

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 UpdateGame(Map* map, int* currentStage, int* clearFlag);
void DrawGame(Map* map, int currentStage, int clearFlag, GameImages* images);

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
    Map currentMap;
    GameImages images;
    int currentStage = 0;
    int clearFlag = 0;

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

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

    SetDrawScreen(DX_SCREEN_BACK);

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

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

        UpdateGame(&currentMap, &currentStage, &clearFlag);
        DrawGame(&currentMap, currentStage, clearFlag, &images);

        ScreenFlip();
    }

    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 UpdateGame(Map* map, int* currentStage, int* clearFlag)
{
    int enterPressed = IsKeyPressed(KEY_INPUT_RETURN);
    int retryPressed = IsKeyPressed(KEY_INPUT_R);

    if (GetMoveRate() < 1.0f)
        return;

    if (*clearFlag)
    {
        if (enterPressed)
        {
            (*currentStage)++;

            if (*currentStage >= STAGE_COUNT)
                *currentStage = 0;

            LoadStage(map, *currentStage);
            moveDir = DIR_NONE;
            lastMoveResult = MOVE_NONE;
            *clearFlag = 0;
        }

        return;
    }

    if (CheckClear(map))
    {
        *clearFlag = 1;
        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);

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

void DrawGame(Map* map, int currentStage, int clearFlag, 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);

    if (clearFlag)
    {
        DrawString(250, 540, "CLEAR!", GetColor(255, 220, 80));
        DrawString(355, 540, "ENTER : NEXT", GetColor(255, 255, 255));
    }
}