Skip to main content

第9回 コスコマンを滑らかに動かそう

第9回 コスコマンを滑らかに動かそう

第8回では、文字で描いていたステージを画像へ置き換えました。

ただし、Map上のコスコマンは1マスずつ移動するため、そのまま描くと画面でも一瞬で次のマスへ移動します。

今回は、ゲームのルールはそのままにして、描画位置だけを少しずつ動かします。

ゲームのルールと描画を分ける

今回の流れ

移動する前の位置を覚える
        ↓
今まで通りMoveObject()で1マス移動する
        ↓
移動した方向と開始時刻を覚える
        ↓
経過時間から0.0~1.0の進み具合を求める
        ↓
移動前の位置から、DIR_VECTORの方向へ少しずつ描く

第4回で作ったDirectionとDIR_VECTORを、ここでも使います。


1.Mapは今まで通り1マスずつ動かそう

滑らかに動かすために、Mapそのものを少しずつ動かす必要はありません。

ゲームのルールは、今まで通りです。

キーを押す
    ↓
MoveObject()を呼ぶ
    ↓
Map上では1マス移動する

変えるのは画面へ描く位置だけです。

Map上
移動前        移動後
  □  ─────→   □
       1回で移動

画面上
移動前 → 途中 → 途中 → 移動後
          200ms

このようにすると、これまで作った移動処理を作り直さずに済みます。


2.移動前の位置・方向・開始時刻を覚えよう

アニメーション中の位置を計算するには、

どこから移動したか
どちらへ移動したか
いつ移動を始めたか

を覚えておきます。

移動前の位置は、MoveObject()を呼ぶ前ならそのまま取得できます。
あとから移動ベクトルを引いて求め直す必要はありません。

移動時間は200ミリ秒にします。

#define MOVE_TIME 200

移動前の位置はmoveStartPos、移動した方向はmoveDir、移動を始めた時刻はmoveStartTimeに覚えておきます。

GameImagesの定義より後ろ、STAGESより前へ追加してください。

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

最初はまだ移動していないので、moveDirはDIR_NONEにしておきます。

今回追加する関数

今回は、次の2つの関数を追加します。

GetMoveRate()は、移動がどこまで進んだかを0.0~1.0で求める関数です。

GetMoveDrawPos()は、指定したオブジェクトを、今この瞬間どこへ描けばよいかを求める関数です。

先にプロトタイプ宣言を追加します。
第8回までのプロトタイプ宣言が並んでいる場所へ、次の2行を追加してください。

float GetMoveRate(void);
Point GetMoveDrawPos(Point startPos, Direction dir, float rate);

では、1つずつ実装してみましょう。


3.移動がどこまで進んだか求めよう

まず、GetMoveRate()を実装します。

この関数では、

移動を始めてから何msたったか
        ↓
200msのうち、どこまで進んだか

を順番に求めます。

DxLibのGetNowCount()を使うと、現在の時刻をミリ秒単位で取得できます。

int now = GetNowCount();

たとえば、

移動開始時刻    1200ms
現在時刻        1300ms

なら、

経過時間をelapsedとすると、

elapsed = 1300 - 1200
        = 100ms

です。

次に、移動の進み具合をrateとすると、

elapsed = 100ms
MOVE_TIME = 200ms

rate = 100 ÷ 200
     = 0.5

となり、半分まで進んでいることが分かります。

rate = 0.0 → 移動開始
rate = 0.5 → 半分
rate = 1.0 → 移動完了

C言語では整数同士を割ると小数部分がなくなるので、elapsedをfloatへ変換してから割ります。

(float)elapsed

先ほどプロトタイプ宣言したGetMoveRate()を、DeleteGameImages()の下へ追加してください。

float GetMoveRate(void)
{
    // まだ一度も移動していなければ、アニメーションはしていない
    if (moveDir == DIR_NONE)
        return 1.0f;

    // 現在時刻から移動開始時刻を引いて、経過時間elapsedを求める
    int elapsed = ________________________________;

    // elapsedを移動時間で割って、進み具合rateを求める
    float rate = ________________________________;

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

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

    return rate;
}

ヒント

現在時刻        → GetNowCount()
移動開始時刻    → moveStartTime
移動時間        → MOVE_TIME

elapsed = 現在時刻 - 移動開始時刻
rate    = (float)elapsed ÷ MOVE_TIME
模範解答を見る
int elapsed = GetNowCount() - moveStartTime;
float rate = (float)elapsed / MOVE_TIME;

4.移動前の位置から描画位置を計算しよう

次に、GetMoveDrawPos()を実装します。

この関数には、移動前の位置startPos、移動方向dir、進み具合rateを渡します。

そして、今この瞬間に画像を描く画面座標を返します。

第4回で作った移動ベクトルをもう一度使おう

第4回では、方向をDIR_VECTORで表しました。

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

たとえば右へ移動するとき、

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

moveVector = DIR_VECTOR[DIR_RIGHT]
           = (1, 0)

です。

今回は、移動する前の位置をmoveStartPosへ保存してあります。

そのため、描画位置は、

移動前の位置
+
移動ベクトル × TILE_SIZE × rate

でそのまま求められます。

右へ1マス移動するときなら、

rate = 0.0 → 移動前の位置
rate = 0.5 → 32ピクセル進んだ位置
rate = 1.0 → 64ピクセル進んだ位置

となります。

先ほどプロトタイプ宣言したGetMoveDrawPos()を、GetMoveRate()の下へ追加してください。

Point GetMoveDrawPos(Point startPos, Direction dir, float rate)
{
    // 第4回で作ったDIR_VECTORから、移動ベクトルを取り出す
    Point moveVector = ________________________________;

    // 移動前の位置から、rateの割合だけ進めた画面座標を求める
    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;
}

ヒント

移動ベクトル → DIR_VECTOR[dir]
模範解答を見る
Point moveVector = DIR_VECTOR[dir];

5.求めた位置にコスコマンを描こう

GetMoveDrawPos()ができたので、次は第8回で作ったDrawStageImage()へ組み込みます。

まず、先ほど作ったGetMoveRate()を呼び出して、現在の進み具合を求めます。

float rate = GetMoveRate();

rateが1.0より小さい間だけ、まだアニメーション中です。

コスコマンを描く部分を次のように変更してください。

if (obj == HUMAN || obj == HUMAN_ON_GOAL)
{
    // rateが1.0より小さければ、まだ移動アニメーション中
    if (rate < 1.0f)
    {
        // 今この瞬間の描画位置をdrawPosとする
        Point drawPos =
            ________________________________________________;

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

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

ヒント

GetMoveDrawPos()へ渡すものは、

移動前の位置 → moveStartPos
移動方向      → moveDir
進み具合      → rate

です。

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

これで、Map上では1マス移動したまま、画面では200msかけて滑らかに動くようになります。


6.移動したときに方向と時刻を覚えよう

最後に、既存のUpdateGame()へアニメーション開始の処理を追加します。

ここでは新しい移動関数を作りません。
第6回までに作ったMoveObject()をそのまま使います。

処理は、

GetPlayerPos()で移動前の位置を取得
        ↓
MoveObject()を呼ぶ
        ↓
本当に移動できた?
    NO  → 何もしない
        ↓ YES
移動前の位置をmoveStartPosへ保存
        ↓
移動方向をmoveDirへ保存
        ↓
現在時刻をmoveStartTimeへ保存

です。

また、前のアニメーションが終わる前に次の移動を始めないように、

if (GetMoveRate() < 1.0f)
    return;

を入力処理より前へ入れます。

UpdateGame()の移動部分を、次の形に変更してください。

// まだ前の移動アニメーション中なら、次の入力へ進まない
if (GetMoveRate() < 1.0f)
    return;

// ここから下は、今までのクリア・リトライ処理
...

Direction dir = GetDirection();

if (dir != DIR_NONE)
{
    // 移動前の位置をstartPosとして、MoveObject()の前に取得する
    Point startPos = ________________________________;

    // 今まで作った移動処理をそのまま使う
    MoveResult result = ________________________________;

    // 実際に移動できたときだけ、アニメーションを開始する
    if (result != MOVE_NONE)
    {
        moveStartPos = ____________________;
        moveDir = ____________________;
        moveStartTime = ________________________________;
    }
}

ヒント

移動前の位置      → GetPlayerPos(map)
移動する            → MoveObject(dir, map)
保存する移動前位置  → startPos
移動した方向        → dir
現在時刻            → GetNowCount()
模範解答を見る
Point startPos = GetPlayerPos(map);
MoveResult result = MoveObject(dir, map);

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

ステージを読み直すとき

次のステージへ進むときや、Rキーでやり直すときは、

moveDir = DIR_NONE;

へ戻しておきます。

新しいステージでは、前の移動アニメーションを引き継がないためです。


7.クリア判定は移動が終わってから行おう

荷物をゴールへ押した瞬間、Map上ではすでにクリア状態になります。

しかし、画面ではまだ200msの移動途中です。

そのため、

アニメーション中
    ↓
入力もクリア判定も待つ
    ↓
rate = 1.0
    ↓
CheckClear()を行う

という順番にします。

UpdateGame()では、

if (GetMoveRate() < 1.0f)
    return;

if (CheckClear(map))
{
    *clearFlag = 1;
    return;
}

の順にしてください。

これで、見た目の移動が終わってからCLEARが表示されます。


8.実行して動きを見よう

次を確認してください。

  • WASDでコスコマンが200msかけて1マス移動する
  • 移動中に次のキーを押しても、途中で次の移動が始まらない
  • 壁へぶつかったときはアニメーションが始まらない
  • 荷物はまだ一瞬で移動する
  • 最後の荷物をゴールへ押したとき、移動が終わってからCLEARになる

荷物の動きは、次の第10回で滑らかにします。


9.第9回の完成コード

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

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();

    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)
            {
                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;
            *clearFlag = 0;
        }

        return;
    }

    if (CheckClear(map))
    {
        *clearFlag = 1;
        return;
    }

    if (retryPressed)
    {
        LoadStage(map, *currentStage);
        moveDir = DIR_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();
        }
    }
}

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));
    }
}