第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(¤tMap, currentStage);
while (ProcessMessage() == 0)
{
ClearDrawScreen();
UpdateGame(¤tMap, ¤tStage, &clearFlag);
DrawGame(¤tMap, 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));
}
}
No Comments