第5回 壁とゴールを判定しよう
第5回 壁とゴールを判定しよう
前回までの移動は、移動先のマスを見ずにHUMANで上書きしていました。
そのため、壁も荷物もゴールも消せます。
📺 第動画で確認
壁・荷物・ゴールを無視して移動すると、どうなる?https://youtu.be/1ISUI_eju6E
移動するときは、次の順で考えます。
今回の流れ
現在位置
↓
移動先を計算
↓
移動先に何がある?
↓
動ける/動けないを決める
1.指定した位置に何があるか調べよう
Pointで位置を渡すと、そのマスの種類を返すGetObjectNum()を作ります。
OBJNAMEの値を再び
OBJNAMEは、Mapの1マスに何が入っているかを表す型です。
| 名前 | 値 | 意味 |
|---|---|---|
FLOOR |
0 | 何もない床 |
WALL |
1 | 壁 |
LUGG |
2 | 荷物 |
GOAL |
3 | ゴール |
HUMAN |
4 | コスコマン |
HUMAN_ON_GOAL |
5 | ゴールに乗っているコスコマン |
LUGG_ON_GOAL |
6 | ゴールに乗っている荷物 |
OUTSIDE |
7 | Mapの外側 |
| これ使って、マップの中だよ外だよって情報を、返す関数を完成させます! | ||
| マップ内の場合は、その位置にあるオブジェクト(ゲーム内のキャラやモノ)を返します。 |
穴埋め Mapの外も判定しよう
GetPlayerPos()の下、SetObjtoMap()の前へ追加してください。
OBJNAME GetObjectNum(Point pos, Map* map)
{
// xがMapの横方向の範囲から外れているか調べる
if (________________________________ ||
// yがMapの縦方向の範囲から外れているか調べる
________________________________)
{
// ステージ外を表す値を返す
return ____________________;
}
// 指定されたマスの数字をOBJNAMEとして返す
return (OBJNAME)________________________________;
}
ヒント
xは
0以上stage_width未満、yは0以上stage_height未満ならMapの中です。Mapの外には専用のOBJNAMEを用意してありました。
模範解答を見る
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];
}
2.「移動したか」を戻り値にしよう
これから移動処理は、
動けなかった
歩いた
という結果を返すようにします。
Directionの下へ追加してください。
enum MoveResult
{
MOVE_NONE,
MOVE_WALK
};
利用するときは、
MoveResult result;
と書きます。
今回追加する関数
第5回で使う2つの関数を、プロトタイプ宣言へまとめて追加します。
GetPlayerPos()の宣言の下へ追加してください。
OBJNAME GetObjectNum(Point pos, Map* map);
MoveResult MoveObject(Direction dir, Map* map);
GetObjectNum() → 指定した位置に何があるか調べる
MoveObject() → 移動できるか判断し、動いた結果を返す
3.まず床だけ歩けるようにしよう
MoveObject()の最初の形を作ります。IsKeyPressed()の下へ追加してください。
MoveResult MoveObject(Direction dir, Map* map)
{
Point playerPos = GetPlayerPos(map);
Point nextPos = {
// 現在位置から、指定された方向へ1マス進んだxを求める
________________________________,
// 現在位置から、指定された方向へ1マス進んだyを求める
________________________________
};
// 移動先のマスに何があるか調べる
OBJNAME next = ________________________________;
// 移動先が普通の床なら歩く
if (next == ____________________)
{
// 元の位置を床へ戻す
SetObjtoMap(____________________, playerPos, map);
// 移動先へコスコマンを置く
SetObjtoMap(____________________, nextPos, map);
return MOVE_WALK;
}
// 床以外には、まだ移動しない
return ____________________;
}
ヒント
移動先の計算は前回と同じです。今回は
GetObjectNum()で移動先を調べ、普通の床だったときだけMapを書き換えます。
模範解答を見る
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 next = GetObjectNum(nextPos, map);
if (next == FLOOR)
{
SetObjtoMap(FLOOR, playerPos, map);
SetObjtoMap(HUMAN, nextPos, map);
return MOVE_WALK;
}
return MOVE_NONE;
}
ゲームループの直接書き換えを、
Direction dir = GetDirection();
if (dir != DIR_NONE)
MoveObject(dir, ¤tMap);
へ置き換えます。
実行して確認
床は歩けますが、壁には入れなくなります。
荷物にも入れません。
ゴールにも、まだ入れません。
ここで、WALLだから止める処理を特別に書いていないことにも注目してください。
「歩いてよいものだけ処理する」ので、それ以外はMOVE_NONEになります。
4.ゴールにも入れるようにしよう
床とゴールは、どちらもコスコマンが入れるマスです。
ここではMoveObject()全体を書き直しません。手直しするのは、歩く処理の次の2か所です。
// ① 床だけを見ている条件を変更する
if (next == FLOOR)
{
SetObjtoMap(FLOOR, playerPos, map);
// ② 移動先へHUMANを置くこの1行を、下でif~elseへ変更する
SetObjtoMap(HUMAN, nextPos, map);
return MOVE_WALK;
}
まず、①の条件を変更します。
// 床またはゴールなら歩ける
if (next == ____________________ || next == ____________________)
ヒント
1つは普通の床、もう1つは荷物を置く目的地です。
模範解答を見る
if (next == FLOOR || next == GOAL)
次に②のSetObjtoMap(HUMAN, nextPos, map);を、移動先に応じた処理へ置き換えます。移動先がゴールなら、ただのHUMANではなく、
ゴールの上にコスコマンがいる
状態をMapへ残します。
if (next == GOAL)
{
// ゴールとコスコマンが重なっている状態をMapへ入れる
SetObjtoMap(____________________, nextPos, map);
}
else
{
// 普通の床へ移動したときのコスコマンをMapへ入れる
SetObjtoMap(____________________, nextPos, map);
}
ヒント
OBJNAMEには、ゴール上のコスコマン専用の状態があります。
模範解答を見る
if (next == GOAL)
{
SetObjtoMap(HUMAN_ON_GOAL, nextPos, map);
}
else
{
SetObjtoMap(HUMAN, nextPos, map);
}
実行して問題を見よう
ゴールへ入ると+になります。
そのあと、もう一度移動しようとしてみてください。
動かなくなります。
なぜでしょう。
5.HUMANだけ探すと、ゴール上で見失う
GetPlayerPos()を見直します。
今は、
if (obj == HUMAN)
だけを探しています。
ところがゴールへ入った瞬間、Mapの値はHUMAN_ON_GOALになります。
つまり、コスコマンはまだ画面にいるのに、GetPlayerPos()から見ると見つからなくなっています。
ここで、第1回で決め、第3回でも確認した「盤面の状態をMapにまとめる」という方針が効いてきます。
普通の床にいるコスコマン → HUMAN
ゴール上のコスコマン → HUMAN_ON_GOAL
のように、下にある床の状態まで含めて1マスの値で表すためです。
プレイヤー位置を別データにしていないので、Mapだけで状態を確認できます。その代わり、コスコマンを探す側では両方の状態を扱う必要があります。
条件を修正します。
// 普通の床にいる場合も、ゴール上にいる場合もプレイヤーとして扱う
if (obj == ____________________ || obj == ____________________)
ヒント
どちらも「コスコマンがいる状態」です。違うのは、その下が床かゴールかです。
模範解答を見る
if (obj == HUMAN || obj == HUMAN_ON_GOAL)
これでゴール上でもコスコマンの位置を取得できます。
6.ゴールから出たら、ゴールを戻そう
もう1つ問題があります。
ゴール上から出るとき、元の位置をいつもFLOORへ戻すと、ゴールそのものが消えてしまいます。
移動前のマスも調べます。
今回はMoveObject()の同じ歩く処理を、もう2か所だけ手直しします。
まず、今ある次の1行、
OBJNAME next = GetObjectNum(nextPos, map);
を、移動前と移動先の両方を取得する次の2行へ置き換えます。
// コスコマンが今いるマスの状態を調べる
OBJNAME current = ________________________________;
// これから入ろうとしているマスの状態を調べる
OBJNAME next = ________________________________;
ヒント
どちらも位置が分かっているので、先ほど作った
GetObjectNum()を使えます。
模範解答を見る
OBJNAME current = GetObjectNum(playerPos, map);
OBJNAME next = GetObjectNum(nextPos, map);
次に、歩く処理の中にある、
SetObjtoMap(FLOOR, playerPos, map);
の1行を、次の条件分岐へ置き換えます。
if (current == HUMAN_ON_GOAL)
{
// ゴール上から出るときは、下にあったゴールを残す
SetObjtoMap(____________________, playerPos, map);
}
else
{
// 普通の床から出るときは床へ戻す
SetObjtoMap(____________________, playerPos, map);
}
ヒント
HUMAN_ON_GOALは「コスコマンの下にゴールがある」という情報も持っています。
模範解答を見る
if (current == HUMAN_ON_GOAL)
{
SetObjtoMap(GOAL, playerPos, map);
}
else
{
SetObjtoMap(FLOOR, playerPos, map);
}
実行して確認
- 壁へは入れない
- 荷物へはまだ入れない
- ゴールへ入ると
+になる - ゴールから出ると
.が残る
まで確認してください。
📺 第5回の完成版を動画で確認
壁とゴールを判定できるようになった動きを確認しよう。
https://youtu.be/5fgL1xTc4d4
次回は、いよいよ荷物を押します。
7.第5回の完成コード
第5回の完成コードを見る
#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[] = {
' ',
'#',
'$',
'.',
'@',
'+',
'*',
' '
};
enum Direction
{
DIR_UP,
DIR_LEFT,
DIR_DOWN,
DIR_RIGHT,
DIR_NONE
};
enum MoveResult
{
MOVE_NONE,
MOVE_WALK
};
const Point DIR_VECTOR[] = {
{ 0, -1 },
{-1, 0 },
{ 0, +1 },
{+1, 0 },
{ 0, 0 }
};
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);
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 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();
Direction dir = GetDirection();
if (dir != DIR_NONE)
MoveObject(dir, ¤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);
}
}
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;
}
return MOVE_NONE;
}
第5回の変更点入り完成コードを見る
このコードは、上の説明なし完成コードと同じ処理です。今回追加・変更した場所を中心に、役割や理由もコメントで確認できます。
- Map外も含め、指定した位置のマスを安全に取得する処理
- 床・ゴールだけを移動可能とし、壁や荷物では動かさない判定
- HUMAN_ON_GOALを使って、移動前後のゴールを消さずに残す理由
#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[] = {
' ',
'#',
'$',
'.',
'@',
'+',
'*',
' '
};
enum Direction
{
DIR_UP,
DIR_LEFT,
DIR_DOWN,
DIR_RIGHT,
DIR_NONE
};
// 【解説・今回追加】移動できなかった場合と、歩いて移動した場合を戻り値で区別する。
enum MoveResult
{
MOVE_NONE,
MOVE_WALK
};
const Point DIR_VECTOR[] = {
{ 0, -1 },
{-1, 0 },
{ 0, +1 },
{+1, 0 },
{ 0, 0 }
};
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);
Direction GetDirection(void);
Point GetPlayerPos(Map* map);
// 【解説・今回追加】指定位置のマスを取得する。Map外もOUTSIDEとして扱う。
OBJNAME GetObjectNum(Point pos, Map* map);
void SetObjtoMap(OBJNAME obj, Point pos, Map* map);
int IsKeyPressed(int key);
// 【解説・今回追加】移動先の判定とMapの書き換えを、WinMainから独立させる。
MoveResult MoveObject(Direction dir, Map* map);
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();
Direction dir = GetDirection();
if (dir != DIR_NONE)
// 【解説・今回変更】WinMainは方向を渡すだけにし、詳しい移動規則はMoveObjectへ任せる。
MoveObject(dir, ¤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);
}
}
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;
}
// 【解説・役割】範囲確認を1か所へまとめ、安全にマスの種類を取得する。
OBJNAME GetObjectNum(Point pos, Map* map)
{
// 【解説・判定】配列へアクセスする前に範囲外を調べ、Map外ならOUTSIDEを返す。
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);
// 【解説・判定】この回で歩けるのは床またはゴールだけ。壁・荷物・Map外では処理しない。
if (next == FLOOR || next == GOAL)
{
// 【解説・理由】ゴールから離れる場合は、元のマスをFLOORではなくGOALへ戻す。
if (current == HUMAN_ON_GOAL)
SetObjtoMap(GOAL, playerPos, map);
else
SetObjtoMap(FLOOR, playerPos, map);
// 【解説・理由】ゴールへ入る場合はHUMAN_ON_GOALにし、1マスの値だけで両方の状態を残す。
if (next == GOAL)
SetObjtoMap(HUMAN_ON_GOAL, nextPos, map);
else
SetObjtoMap(HUMAN, nextPos, map);
return MOVE_WALK;
}
// 【解説・結果】どの移動条件にも当てはまらなければ、Mapを変更せずMOVE_NONEを返す。
return MOVE_NONE;
}