第3回 まずコスコマンを動かしてみよう
第3回 まずコスコマンを動かしてみよう
前回は、Mapの数字を文字へ変えてステージを表示しました。
今回は、Mapの中にいるコスコマンを探して、まず実際に動かします。
ここでは、まだ壁や荷物の判定は作りません。
**「Mapを書き換えると、画面上の@も動いて見える」**ことを先に確認します。
今回の流れ
Mapからコスコマンの位置を探す
↓
押したキーから移動先を決める
↓
元のマスを床へ戻す
↓
移動先をコスコマンのマスにする
↓
Mapを描くと、@が移動して見える
1.位置をxとyでまとめよう
Mapの位置は、
x = 横
y = 縦
の2つで表せます。
そこで、Mapの前へPoint構造体を追加します。
struct Point
{
int x;
int y;
};
これで、
Point pos;
という1つの変数の中へ、xとyをまとめて持てます。
今回追加する関数
Pointを使えるようになったので、今回使う3つの関数を先に宣言します。
DrawStageText()の宣言の下へ、まとめて追加してください。
Point GetPlayerPos(Map* map);
void SetObjtoMap(OBJNAME obj, Point pos, Map* map);
int IsKeyPressed(int key);
役割は次の通りです。
GetPlayerPos() → Mapからコスコマンの位置を探す
SetObjtoMap() → 指定したMapの1マスを書き換える
IsKeyPressed() → キーを押した瞬間だけ1を返す
2.Mapからコスコマンを探そう
このゲームでは、コスコマンの位置を別の変数へ常に保存しておくのではなく、コスコマン自身もMapの1マスとして持っています。
床、壁、ゴール、荷物、コスコマンまでMapの数字で表すので、Mapを見るだけでその時点のゲーム状態が分かります。
そのため、移動するときはMap全体を調べて、現在のコスコマンの位置を探します。
穴埋め GetPlayerPos()を作ろう
DrawStageText()の下へ追加してください。
Point GetPlayerPos(Map* map)
{
Point result = { -1, -1 };
// ステージの上の行から下の行まで探す
for (int y = 0; y < ____________________; y++)
{
// 1行を左端から右端まで探す
for (int x = 0; x < ____________________; x++)
{
// 今調べているマスの数字を取り出す
int obj = ________________________________;
// コスコマンのマスを見つけたか調べる
if (obj == ____________________)
{
// 見つけた横位置を結果へ保存する
result.x = ____________________;
// 見つけた縦位置を結果へ保存する
result.y = ____________________;
return result;
}
}
}
return result;
}
ヒント
Mapを表示するときと同じ二重ループを使えます。今回は
@を表すマスを見つけたら、そのときのxとyを返します。
模範解答を見る
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)
{
result.x = x;
result.y = y;
return result;
}
}
}
return result;
}
位置を表示して確認
ゲームループのDrawStageText()の下へ、一時的に次の2行を追加してみましょう。
Point playerPos = GetPlayerPos(¤tMap);
DrawFormatString(24, 560, GetColor(255, 255, 0), "PLAYER x=%d y=%d", playerPos.x, playerPos.y);
最初のステージでは、@の位置に対応した数字が表示されます。
確認できたら、この2行はいったん消してください。
3.Mapの1マスを書き換える関数を作ろう
移動とは、画面上の文字を直接動かすことではありません。
Mapの中身を、
移動前
元のマス HUMAN
移動先 FLOOR
↓
移動後
元のマス FLOOR
移動先 HUMAN
のように書き換えます。
Mapを書き換える小さな関数を作ります。
先ほど宣言したSetObjtoMap()の中身を作ります。
穴埋め 指定したマスを書き換えよう
GetPlayerPos()の下へ追加してください。
void SetObjtoMap(OBJNAME obj, Point pos, Map* map)
{
// 指定されたy行・x列のマスへ、新しいオブジェクト番号を入れる
map->Dat[____________________][____________________] = ____________________;
}
ヒント
Point posにはxとyがあります。書き換える値は引数objで受け取っています。
模範解答を見る
void SetObjtoMap(OBJNAME obj, Point pos, Map* map)
{
map->Dat[pos.y][pos.x] = obj;
}
4.キーを押した瞬間だけ反応させよう
ゲームループは1秒間に何十回も繰り返されます。
CheckHitKey()だけで判定すると、キーを少し長く押しただけで何マスも進むことがあります。
そこで、
前のフレーム:押していない
今のフレーム:押している
ときだけ1を返す関数を用意します。
先ほど宣言したIsKeyPressed()の中身を作ります。
穴埋め 押した瞬間を調べよう
SetObjtoMap()の下へ追加してください。
int IsKeyPressed(int key)
{
static int previous[256] = { 0 };
// 今のフレームで、指定キーが押されているか調べる
int current = ________________________________;
// 今は押されていて、前は押されていなかったときだけ1にする
int pressed = ________________________________;
// 次のフレーム用に、今の状態を保存する
previous[key] = ____________________;
return pressed;
}
ヒント
DxLibで、指定したキーが現在押されているかを調べる関数を使います。調べるキーは引数
keyに入っています。「押した瞬間」は、現在と1フレーム前の状態を比べます。
模範解答を見る
int IsKeyPressed(int key)
{
static int previous[256] = { 0 };
int current = CheckHitKey(key);
int pressed = current != 0 && previous[key] == 0;
previous[key] = current;
return pressed;
}
5.まず右へ1マスだけ動かそう
いきなり4方向を書かず、最初はDキーだけにします。
ゲームループのClearDrawScreen();の下へ追加してください。
Point playerPos = GetPlayerPos(¤tMap);
Point nextPos = playerPos;
if (IsKeyPressed(KEY_INPUT_D))
{
// Dキーなので、現在位置から右へ1マス進んだ位置を作る
nextPos.x = ________________________________;
// 移動先をコスコマンのマスに書き換える
SetObjtoMap(____________________, nextPos, ¤tMap);
// 元いたマスを床へ戻す
SetObjtoMap(____________________, playerPos, ¤tMap);
}
ヒント
右へ1マスは
xが1増えます。移動後は、移動先にコスコマン、元の位置に床を置きます。
模範解答を見る
Point playerPos = GetPlayerPos(¤tMap);
Point nextPos = playerPos;
if (IsKeyPressed(KEY_INPUT_D))
{
nextPos.x = playerPos.x + 1;
SetObjtoMap(HUMAN, nextPos, ¤tMap);
SetObjtoMap(FLOOR, playerPos, ¤tMap);
}
実行して確認
Dキーを1回押すたびに@が右へ1マス動けば成功です。
ここで起きているのは、文字そのものを動かしているのではなく、Mapの数字を書き換えた結果、次の描画で@の場所が変わっているということです。
6.WASDをそのまま書いてみよう
右へ動く仕組みができたので、上下左右へ増やします。
さきほどのDキーだけの処理を、次の処理へ置き換えてください。
Point playerPos = GetPlayerPos(¤tMap);
Point nextPos = playerPos;
int move = 0;
if (IsKeyPressed(KEY_INPUT_W))
{
// 上へ1マス進むので、縦位置を1つ減らす
nextPos.y = ________________________________;
move = 1;
}
else if (IsKeyPressed(KEY_INPUT_A))
{
// 左へ1マス進むので、横位置を1つ減らす
nextPos.x = ________________________________;
move = 1;
}
else if (IsKeyPressed(KEY_INPUT_S))
{
// 下へ1マス進むので、縦位置を1つ増やす
nextPos.y = ________________________________;
move = 1;
}
else if (IsKeyPressed(KEY_INPUT_D))
{
// 右へ1マス進むので、横位置を1つ増やす
nextPos.x = ________________________________;
move = 1;
}
if (move &&
nextPos.x >= 0 && nextPos.x < currentMap.stage_width &&
nextPos.y >= 0 && nextPos.y < currentMap.stage_height)
{
// 移動先へコスコマンを置く
SetObjtoMap(____________________, nextPos, ¤tMap);
// 元の位置を床へ戻す
SetObjtoMap(____________________, playerPos, ¤tMap);
}
ヒント
Mapでは上へ行くほど
yが小さく、下へ行くほどyが大きくなります。左右はxを変えます。
模範解答を見る
Point playerPos = GetPlayerPos(¤tMap);
Point nextPos = playerPos;
int move = 0;
if (IsKeyPressed(KEY_INPUT_W))
{
nextPos.y = playerPos.y - 1;
move = 1;
}
else if (IsKeyPressed(KEY_INPUT_A))
{
nextPos.x = playerPos.x - 1;
move = 1;
}
else if (IsKeyPressed(KEY_INPUT_S))
{
nextPos.y = playerPos.y + 1;
move = 1;
}
else if (IsKeyPressed(KEY_INPUT_D))
{
nextPos.x = playerPos.x + 1;
move = 1;
}
if (move &&
nextPos.x >= 0 && nextPos.x < currentMap.stage_width &&
nextPos.y >= 0 && nextPos.y < currentMap.stage_height)
{
SetObjtoMap(HUMAN, nextPos, ¤tMap);
SetObjtoMap(FLOOR, playerPos, ¤tMap);
}
実行して、わざと問題を起こそう
WASDで動けるようになりました。
では、上にある荷物やゴールへそのまま進んでみてください。
今のプログラムは、移動先が何であっても、
SetObjtoMap(HUMAN, nextPos, ¤tMap);
で上書きします。
そのため、荷物もゴールも壁も消せてしまいます。
これはバグですが、今はそのままで構いません。
今は移動先の種類を調べていないため、この問題が起きます。
次回は、まず4方向に重複している計算を整理します。
7.第3回の完成コード
第3回の完成コードを見る
#include "DxLib.h"
#define SCREEN_WIDTH 720
#define SCREEN_HEIGHT 600
#define MAX_STAGE_WIDTH 10
#define MAX_STAGE_HEIGHT 10
#define TEXT_SIZE 32
#define TEXT_STAGE_X 40
#define TEXT_STAGE_Y 90
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];
};
const char OBJECT_CHAR[] = {
' ',
'#',
'$',
'.',
'@',
'+',
'*',
' '
};
const Map START_STAGE = {
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 }
}
};
void LoadStage(Map* map);
void DrawStageText(Map* map, int fontHandle);
Point GetPlayerPos(Map* map);
void SetObjtoMap(OBJNAME obj, Point pos, Map* map);
int IsKeyPressed(int key);
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
Map currentMap;
int fontHandle;
ChangeWindowMode(TRUE);
SetGraphMode(SCREEN_WIDTH, SCREEN_HEIGHT, 32);
SetMainWindowText("Costco Man");
if (DxLib_Init() != 0)
return -1;
SetDrawScreen(DX_SCREEN_BACK);
fontHandle = CreateFontToHandle("MS ゴシック", TEXT_SIZE, 1);
LoadStage(¤tMap);
while (ProcessMessage() == 0)
{
ClearDrawScreen();
Point playerPos = GetPlayerPos(¤tMap);
Point nextPos = playerPos;
int move = 0;
if (IsKeyPressed(KEY_INPUT_W))
{
nextPos.y = playerPos.y - 1;
move = 1;
}
else if (IsKeyPressed(KEY_INPUT_A))
{
nextPos.x = playerPos.x - 1;
move = 1;
}
else if (IsKeyPressed(KEY_INPUT_S))
{
nextPos.y = playerPos.y + 1;
move = 1;
}
else if (IsKeyPressed(KEY_INPUT_D))
{
nextPos.x = playerPos.x + 1;
move = 1;
}
if (move &&
nextPos.x >= 0 && nextPos.x < currentMap.stage_width &&
nextPos.y >= 0 && nextPos.y < currentMap.stage_height)
{
SetObjtoMap(HUMAN, nextPos, ¤tMap);
SetObjtoMap(FLOOR, playerPos, ¤tMap);
}
DrawString(24, 24, "COSTCO MAN", GetColor(255, 255, 255));
DrawStageText(¤tMap, fontHandle);
ScreenFlip();
}
DeleteFontToHandle(fontHandle);
DxLib_End();
return 0;
}
void LoadStage(Map* map)
{
map->stage_width = START_STAGE.stage_width;
map->stage_height = START_STAGE.stage_height;
for (int y = 0; y < map->stage_height; y++)
{
for (int x = 0; x < map->stage_width; x++)
{
map->Dat[y][x] = START_STAGE.Dat[y][x];
}
}
}
void DrawStageText(Map* map, int fontHandle)
{
char line[MAX_STAGE_WIDTH + 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];
line[x] = OBJECT_CHAR[obj];
}
line[map->stage_width] = '\0';
DrawStringToHandle(
TEXT_STAGE_X,
TEXT_STAGE_Y + y * TEXT_SIZE,
line,
GetColor(255, 255, 255),
fontHandle);
}
}
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)
{
result.x = x;
result.y = y;
return result;
}
}
}
return result;
}
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 = current != 0 && previous[key] == 0;
previous[key] = current;
return pressed;
}
第3回の変更点入り完成コードを見る
このコードは、上の説明なし完成コードと同じ処理です。今回追加・変更した場所を中心に、役割や理由もコメントで確認できます。
- Pointで位置を表し、Map全体からコスコマンを探す処理
- W・A・S・Dごとに移動先を直接計算する最初の移動処理
- 移動先の種類をまだ調べていないため、壁なども上書きできる段階であること
#include "DxLib.h"
#define SCREEN_WIDTH 720
#define SCREEN_HEIGHT 600
#define MAX_STAGE_WIDTH 10
#define MAX_STAGE_HEIGHT 10
#define TEXT_SIZE 32
#define TEXT_STAGE_X 40
#define TEXT_STAGE_Y 90
enum OBJNAME
{
FLOOR = 0,
WALL = 1,
LUGG = 2,
GOAL = 3,
HUMAN = 4,
HUMAN_ON_GOAL = 5,
LUGG_ON_GOAL = 6,
OUTSIDE = 7
};
// 【解説・今回追加】xとyを組にして、現在位置や移動先を1つの値として扱う。
struct Point
{
int x;
int y;
};
struct Map
{
int stage_width;
int stage_height;
int Dat[MAX_STAGE_HEIGHT][MAX_STAGE_WIDTH];
};
const char OBJECT_CHAR[] = {
' ',
'#',
'$',
'.',
'@',
'+',
'*',
' '
};
const Map START_STAGE = {
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 }
}
};
void LoadStage(Map* map);
void DrawStageText(Map* map, int fontHandle);
// 【解説・今回追加】Mapからコスコマンを探す関数。見つけた座標をPointで返す。
Point GetPlayerPos(Map* map);
// 【解説・今回追加】指定した座標へ、床やコスコマンなどの値を書き込む関数。
void SetObjtoMap(OBJNAME obj, Point pos, Map* map);
// 【解説・今回追加】キーを押し続けても1回だけ移動するよう、押した瞬間を調べる。
int IsKeyPressed(int key);
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
Map currentMap;
int fontHandle;
ChangeWindowMode(TRUE);
SetGraphMode(SCREEN_WIDTH, SCREEN_HEIGHT, 32);
SetMainWindowText("Costco Man");
if (DxLib_Init() != 0)
return -1;
SetDrawScreen(DX_SCREEN_BACK);
fontHandle = CreateFontToHandle("MS ゴシック", TEXT_SIZE, 1);
LoadStage(¤tMap);
while (ProcessMessage() == 0)
{
ClearDrawScreen();
// 【解説・流れ】移動を計算する前に、Map全体から現在のコスコマンを探す。
Point playerPos = GetPlayerPos(¤tMap);
// 【解説・理由】現在位置を残したまま、別のPointで移動先を作る。
Point nextPos = playerPos;
int move = 0;
// 【解説・今回追加】まずは方向ごとにxまたはyを直接増減させ、4方向移動を完成させる。
if (IsKeyPressed(KEY_INPUT_W))
{
nextPos.y = playerPos.y - 1;
move = 1;
}
else if (IsKeyPressed(KEY_INPUT_A))
{
nextPos.x = playerPos.x - 1;
move = 1;
}
else if (IsKeyPressed(KEY_INPUT_S))
{
nextPos.y = playerPos.y + 1;
move = 1;
}
else if (IsKeyPressed(KEY_INPUT_D))
{
nextPos.x = playerPos.x + 1;
move = 1;
}
// 【解説・判定】キー入力があり、移動先がMap内にあるときだけ書き換える。
// 【解説・注意】この回では移動先の種類を見ていないため、壁・荷物・ゴールも上書きできる。
if (move &&
nextPos.x >= 0 && nextPos.x < currentMap.stage_width &&
nextPos.y >= 0 && nextPos.y < currentMap.stage_height)
{
// 【解説・流れ】移動先へHUMANを書き、元の位置をFLOORへ戻して移動を表現する。
SetObjtoMap(HUMAN, nextPos, ¤tMap);
SetObjtoMap(FLOOR, playerPos, ¤tMap);
}
DrawString(24, 24, "COSTCO MAN", GetColor(255, 255, 255));
DrawStageText(¤tMap, fontHandle);
ScreenFlip();
}
DeleteFontToHandle(fontHandle);
DxLib_End();
return 0;
}
void LoadStage(Map* map)
{
map->stage_width = START_STAGE.stage_width;
map->stage_height = START_STAGE.stage_height;
for (int y = 0; y < map->stage_height; y++)
{
for (int x = 0; x < map->stage_width; x++)
{
map->Dat[y][x] = START_STAGE.Dat[y][x];
}
}
}
void DrawStageText(Map* map, int fontHandle)
{
char line[MAX_STAGE_WIDTH + 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];
line[x] = OBJECT_CHAR[obj];
}
line[map->stage_width] = '\0';
DrawStringToHandle(
TEXT_STAGE_X,
TEXT_STAGE_Y + y * TEXT_SIZE,
line,
GetColor(255, 255, 255),
fontHandle);
}
}
// 【解説・役割】二重forでMapを左上から調べ、HUMANを見つけた時点で座標を返す。
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)
{
result.x = x;
result.y = y;
return result;
}
}
}
return result;
}
// 【解説・役割】Pointのyを行、xを列としてDatへ書き込む。
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 = current != 0 && previous[key] == 0;
previous[key] = current;
return pressed;
}