第2回 文字でステージを表示しよう
第2回 文字でステージを表示しよう
前回確認したコスコマンのステージを、今回はDxLibのウィンドウへ表示します。
目標は次の画面です。
##########
# #
# . #
# $ #
# @ #
# #
##########
まだ@は動きません。まずは、数字で持っているステージを文字へ変えて描くところまで作ります。
1.まず、プログラム全体の流れを見よう
ステージを表示するまでの大まかな流れは、次のようになります。
// ステージのデータと関数を用意する
int WINAPI WinMain(...)
{
// ゲームで使う変数とDxLibの準備
// ゲーム開始時に1回だけ
ステージを読み込む;
while (ゲーム中)
{
画面を消す;
ステージを描く;
画面を表示する;
}
// ゲーム終了時に1回だけ
後片付けをする;
}
// ステージを読み込む関数
// ステージを描く関数
今回の大きな流れは、
ゲーム開始時に1回
↓
ゲーム中に繰り返す
↓
ゲーム終了時に1回
です。
2.DxLibのウィンドウを表示しよう
main.cppを開いて、まずはDxLibのウィンドウを表示します。
次のコードを書いて実行してみましょう。
#include "DxLib.h"
#define SCREEN_WIDTH 720
#define SCREEN_HEIGHT 600
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
ChangeWindowMode(TRUE);
SetGraphMode(SCREEN_WIDTH, SCREEN_HEIGHT, 32);
SetMainWindowText("Costco Man");
if (DxLib_Init() != 0)
return -1;
SetDrawScreen(DX_SCREEN_BACK);
while (ProcessMessage() == 0)
{
ClearDrawScreen();
DrawString(24, 24, "COSTCO MAN", GetColor(255, 255, 255));
ScreenFlip();
}
DxLib_End();
return 0;
}
この時点でも、
DxLibを使えるようにする
↓
whileの中を繰り返す
↓
DxLibを終了する
という形になっています。
実行して確認
ウィンドウが開いて、左上にCOSTCO MANと出ればOKです。
3.1マスの種類に名前を付けよう
ステージの1マスには、床、壁、荷物、ゴール、コスコマンなどが入ります。
プログラムの中では、それぞれを0~7の数字で表します。
まず、ステージの最大サイズを決めます。
追加する場所:
SCREEN_HEIGHTの下
ここに置く理由:このあとMap構造体の配列サイズとして使うので、Mapを定義するより前に用意します。
#define MAX_STAGE_WIDTH 10
#define MAX_STAGE_HEIGHT 10
その下へ、1マスの種類を表すenum OBJNAMEを追加します。
ここに置く理由:ステージ全体で使う名前なので、
WinMain()の中ではなく、どの関数からでも使える場所へ置きます。
enum OBJNAME
{
FLOOR = 0,
WALL = 1,
LUGG = 2,
GOAL = 3,
HUMAN = 4,
HUMAN_ON_GOAL = 5,
LUGG_ON_GOAL = 6,
OUTSIDE = 7
};
enumを使うと、処理を書くときに、
if (obj == WALL)
のように意味が分かる名前で比較できます。
数字と意味、表示文字の対応は次の通りです。
| 値 | 名前 | 意味 | 文字 |
|---|---|---|---|
| 0 | FLOOR |
床 | |
| 1 | WALL |
壁 | # |
| 2 | LUGG |
荷物 | $ |
| 3 | GOAL |
ゴール | . |
| 4 | HUMAN |
コスコマン | @ |
| 5 | HUMAN_ON_GOAL |
ゴール上のコスコマン | + |
| 6 | LUGG_ON_GOAL |
ゴール上の荷物 | * |
| 7 | OUTSIDE |
ステージ外 | |
この記号の並びは、倉庫番系のステージ表現で使われてきたXsokoban形式と同じ考え方です。
4.ステージ全体をMapで持とう
ステージは、横幅、高さ、2次元配列の3つをまとめて扱います。
追加する場所:
enum OBJNAMEの下
ここに置く理由:このあとMap型のステージデータや変数を作るので、それより先に「Mapとは何か」を定義しておきます。
struct Map
{
int stage_width;
int stage_height;
int Dat[MAX_STAGE_HEIGHT][MAX_STAGE_WIDTH];
};
構造体を定義するときはstruct Mapと書きます。
変数を作るときは、
Map currentMap;
のようにMapを型名として使います。
Dat[y][x]には、Y行目・X列目のマスの数字が入ります。
5.Mapの数字を文字へ変える表を作ろう
画面には数字ではなく、#、$、@などの文字を表示します。
そこで、Mapの数字と表示文字を同じ番号で対応させます。
追加する場所:
Map構造体の下
ここに置く理由:この配列はステージを描く関数から使う共通データなので、WinMain()の外へ置きます。
const char OBJECT_CHAR[] = {
' ',
'#',
'$',
'.',
'@',
'+',
'*',
' '
};
enum OBJNAMEと同じ順番です。
例えば、
OBJECT_CHAR[2]
なら'$'、
OBJECT_CHAR[4]
なら'@'が取り出せます。
つまり、
Mapの数字
↓
同じ番号でOBJECT_CHARを見る
↓
表示する文字
です。
6.最初のステージを用意しよう
次に、ゲーム開始時のステージデータを用意します。
追加する場所:
OBJECT_CHARの下
ここに置く理由:START_STAGEはMap型のデータなので、Mapを定義したあとに置きます。ゲーム中に書き換える変数ではないので、WinMain()の外へ置きます。
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 }
}
};
数字だけを見ると、
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
です。
対応表を見れば、3がゴール、2が荷物、4がコスコマンだと分かります。
7.今回使う関数を先に宣言しよう
今回、このあと使う関数は2つです。
LoadStage()
元のステージを、ゲーム中に使うMapへコピーする
DrawStageText()
Mapの数字を文字へ変えて画面に描く
この2つの関数の中身は、WinMain()より下に書きます。
WinMain()から先に呼び出せるように、START_STAGEの下、WinMain()の前へプロトタイプ宣言を書きます。
void LoadStage(Map* map);
void DrawStageText(Map* map, int fontHandle);
プロトタイプ宣言は、
この名前と形の関数があります
と、先にコンパイラへ知らせるためのものです。
上に書いたから先に実行されるわけではありません。実際の処理は、それぞれの関数が呼び出されたときに行われます。
8.元のステージを、遊ぶためのMapへコピーしよう
このあとコスコマンを動かすと、Mapの中身は書き換わります。
元データまで直接書き換えると、最初の配置へ戻したいときに困ります。
そこで、
START_STAGE
最初の配置。書き換えない
↓ コピー
currentMap
ゲーム中に書き換える
と分けます。
8-1.ゲーム中に使うMapを用意する
追加する場所:
WinMain()の最初
ここに置く理由:currentMapは、ゲーム開始時の読み込みから、ゲーム中の描画までWinMain()が動いている間ずっと使う変数だからです。
Map currentMap;
8-2.LoadStage()の中身を作る
追加する場所:
WinMain()の下
ここに置く理由:WinMain()を先に読みやすくして、実際の処理を行う関数の中身をそのあとへまとめます。実行されるのは、LoadStage()を呼び出したときです。
void LoadStage(Map* map)
{
// 元ステージの横幅を、ゲーム中に使うMapへコピーする
map->stage_width = ________________________________;
// 元ステージの高さを、ゲーム中に使うMapへコピーする
map->stage_height = ________________________________;
// ステージの上から下まで、行を順番にコピーする
for (int y = 0; y < ____________________; y++)
{
// 1行の左から右まで、マスを順番にコピーする
for (int x = 0; x < ____________________; x++)
{
// 元ステージと同じ位置の数字をcurrentMapへ入れる
map->Dat[y][x] = ________________________________;
}
}
}
ヒント
mapはコピー先です。コピー元はSTART_STAGEです。ループの回数は、コピーした幅と高さを使えます。
模範解答を見る
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];
}
}
}
8-3.ゲーム開始時に1回だけ読み込む
LoadStage()は、ゲーム中ずっと繰り返す処理ではありません。
毎フレームSTART_STAGEをコピーすると、あとでコスコマンを動かしても、すぐ最初の配置へ戻ってしまいます。
追加する場所:
SetDrawScreen(DX_SCREEN_BACK);の下、whileの前
ここに置く理由:ステージの読み込みは、ゲームを始めるときに1回だけ行いたいからです。
// ゲームを始める前に、元ステージをプレイ用Mapへコピーする
LoadStage(____________________);
ヒント
LoadStage()は、書き換えるMapのアドレスを受け取ります。
模範解答を見る
LoadStage(¤tMap);
ここでビルド
まだステージは描いていないので画面は変わりません。
エラーなくビルドできれば、Mapの準備までできています。
9.文字の幅をそろえよう
ステージは文字をマスの代わりに並べます。
文字によって横幅が違うと、同じ列にある壁や荷物がずれてしまいます。
そこで、どの文字も同じ幅で表示される等幅フォントを使います。
今回はMS ゴシックを使います。
9-1.文字の大きさと表示位置を決める
追加する場所:
MAX_STAGE_HEIGHTの下
ここに置く理由:文字の大きさや表示位置は途中で変化する値ではなく、描画全体で使う固定値だからです。
#define TEXT_SIZE 32
#define TEXT_STAGE_X 40
#define TEXT_STAGE_Y 90
9-2.フォントハンドルを入れる変数を用意する
追加する場所:
WinMain()のMap currentMap;の下
ここに置く理由:作ったフォントをゲーム中の描画と、終了時の削除の両方で使うからです。
int fontHandle;
9-3.ゲーム開始時にフォントを作る
追加する場所:
SetDrawScreen(DX_SCREEN_BACK);の下、LoadStage()より前
ここに置く理由:フォントは毎フレーム作り直す必要がないので、whileへ入る前に1回だけ作ります。
// ステージの文字を同じ幅で描くためのフォントを作る
fontHandle = ____________________("MS ゴシック", TEXT_SIZE, 1);
ヒント
DxLibでフォントハンドルを作る関数を使います。関数名には
CreateFontが入っています。
模範解答を見る
fontHandle = CreateFontToHandle("MS ゴシック", TEXT_SIZE, 1);
ここまでで、whileへ入る前は、
フォントを作る
↓
ステージを読み込む
↓
ゲームループへ入る
となります。
この2つはどちらも「ゲーム開始時に1回だけ行う処理」です。
9-4.終了時にフォントを削除する
作ったフォントは、ゲームが終わるときに削除します。
追加する場所:
whileの後、DxLib_End();の前
ここに置く理由:ゲーム中はフォントを使い続けるので、ループを抜けてから後片付けします。
// 作ったフォントハンドルを終了前に削除する
DeleteFontToHandle(____________________);
ヒント
削除するのは、先ほど作ったフォントです。
模範解答を見る
DeleteFontToHandle(fontHandle);
10.Mapの1行を文字列にしよう
1マスずつ別々に描くのではなく、横1行を1本の文字列にして描きます。
例えば、
1 0 0 0 4 0 0 0 0 1
は、
# @ #
になります。
C言語の文字列は、最後に'\0'が必要です。
横幅が最大10文字なので、
char line[MAX_STAGE_WIDTH + 1];
とすれば、10文字と最後の'\0'を入れられます。
DrawStageText()の中身を作る
先ほど宣言したDrawStageText()の中身を作ります。
LoadStage()の関数本体の下へ追加してください。
void DrawStageText(Map* map, int fontHandle)
{
// ステージ1行分と、文字列の終わりを入れられる大きさにする
char line[MAX_STAGE_WIDTH + ____________________];
// ステージの上の行から下の行まで描く
for (int y = 0; y < ____________________; y++)
{
// 今の1行を、左端から右端まで文字へ変換する
for (int x = 0; x < ____________________; x++)
{
// 現在のマスに入っている数字を取り出す
int obj = ________________________________;
// その数字と同じ番号の表示文字をlineへ入れる
line[x] = ________________________________;
}
// 1行分を書き終えた位置へ、文字列の終わりを入れる
line[____________________] = ____________________;
DrawStringToHandle(
TEXT_STAGE_X,
// y行目になるよう、1行の高さずつ下へずらす
TEXT_STAGE_Y + ________________________________,
line,
GetColor(255, 255, 255),
fontHandle);
}
}
ヒント
外側は高さ、内側は幅まで繰り返します。Mapの数字を取り出したら、
OBJECT_CHARの添字として使います。文字列の最後には'\0'が必要です。
模範解答を見る
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);
}
}
11.ゲームループからステージを描こう
ステージは、ゲーム画面として毎フレーム表示します。
1フレームの描画順は、
前の画面を消す
↓
今の内容を描く
↓
描いた画面を表示する
です。
今のwhileでは、
ClearDrawScreen();
DrawString(24, 24, "COSTCO MAN", GetColor(255, 255, 255));
ScreenFlip();
となっています。
ステージの描画は、画面を消したあと、ScreenFlip()で表示する前に行います。
追加する場所:
whileの中、DrawString()の下
ここに置く理由:ステージは毎フレーム描き直す必要があり、ScreenFlip()より前に裏画面へ描いておく必要があるからです。
// プレイ用Mapを、作った等幅フォントで描く
DrawStageText(____________________, ____________________);
ヒント
第1引数はゲーム中に使っているMapのアドレス、第2引数はフォントハンドルです。
模範解答を見る
DrawStageText(¤tMap, fontHandle);
実行して確認
##########
# #
# . #
# $ #
# @ #
# #
##########
と表示されたら成功です。
第2回の実行例
実際に実行すると、ウィンドウ上部にCOSTCO MAN、その下に文字で作ったステージが表示されます。
この画面で確認できるのは、次の3点です。
- Mapの外周に入れた
WALLが#として並んでいる GOAL、LUGG、HUMANが、それぞれ.、$、@へ変換されている- すべての文字が等幅フォントで描かれ、縦と横の位置がそろっている
ウィンドウの見た目や文字の大きさは、Windowsの表示倍率やフォント環境によって少し違って見えることがあります。ステージの配置が同じなら問題ありません。
数字で持っていた2次元配列が、OBJECT_CHARを通して文字のステージになりました。
ここまでの実行順をもう一度見ると、
フォントを作る
↓
ステージを読み込む
↓
┌──────── 毎フレーム ────────┐
│ 画面を消す │
│ タイトルとステージを描く │
│ 画面を表示する │
└────────────────────────────┘
↓
フォントを削除する
↓
DxLibを終了する
となります。
次回は、Mapの中から@の位置を探して、実際に動かします。
12.第2回の完成コード
自分のコードと見比べたい場合だけ開いてください。
第2回の完成コードを見る
#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 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);
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();
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);
}
}
第2回の変更点入り完成コードを見る
このコードは、上の説明なし完成コードと同じ処理です。今回追加・変更した場所を中心に、役割や理由もコメントで確認できます。
- ステージを数値のMapとして用意し、表示用の文字へ変換する流れ
- 元のステージとゲーム中のMapを分け、開始時に1回だけコピーする理由
- 初期化・ゲームループ・終了処理の実行タイミング
#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
// 【解説・今回追加】1マスの種類を番号で表す。Mapにはこの番号を保存する。
// 【解説・役割】表示文字ではなく種類を保存するので、あとから判定にも同じMapを使える。
enum OBJNAME
{
FLOOR = 0,
WALL = 1,
LUGG = 2,
GOAL = 3,
HUMAN = 4,
HUMAN_ON_GOAL = 5,
LUGG_ON_GOAL = 6,
OUTSIDE = 7
};
// 【解説・今回追加】ステージの幅・高さと、各マスの状態を1つにまとめる。
struct Map
{
int stage_width;
int stage_height;
int Dat[MAX_STAGE_HEIGHT][MAX_STAGE_WIDTH];
};
// 【解説・流れ】OBJNAMEの番号を添字にして、画面へ出す1文字へ変換する表。
const char OBJECT_CHAR[] = {
' ',
'#',
'$',
'.',
'@',
'+',
'*',
' '
};
// 【解説・役割】書き換えない元ステージ。ゲーム中は、この内容をcurrentMapへコピーして使う。
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 }
}
};
// 【解説・今回追加】WinMainより下にある関数を先に呼べるよう、名前と引数を宣言する。
// 【解説・役割】LoadStageは読み込み、DrawStageTextは文字描画を担当する。
void LoadStage(Map* map);
void DrawStageText(Map* map, int fontHandle);
// 【解説・流れ】ここから、初期化→繰り返し処理→終了処理の順に実行される。
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);
// 【解説・理由】フォントは毎フレーム作らず、ゲームループへ入る前に1回だけ作る。
fontHandle = CreateFontToHandle("MS ゴシック", TEXT_SIZE, 1);
// 【解説・理由】元ステージを直接変更しないよう、開始時にゲーム用Mapへコピーする。
LoadStage(¤tMap);
// 【解説・流れ】入力受付・更新・描画を繰り返すゲームループ。
while (ProcessMessage() == 0)
{
ClearDrawScreen();
DrawString(24, 24, "COSTCO MAN", GetColor(255, 255, 255));
// 【解説・描画】現在のMapを毎フレーム描き直す。
DrawStageText(¤tMap, fontHandle);
ScreenFlip();
}
// 【解説・終了処理】ループを抜けたあと、作成したフォントを解放する。
DeleteFontToHandle(fontHandle);
DxLib_End();
return 0;
}
// 【解説・役割】幅・高さをコピーしたあと、二重forで全マスをコピーする。
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];
}
}
}
// 【解説・役割】Mapの1行を文字列へ変換し、上から順に描く。
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];
}
// 【解説・理由】文字列の終端を入れ、DrawStringToHandleが行の終わりを判断できるようにする。
line[map->stage_width] = '\0';
DrawStringToHandle(
TEXT_STAGE_X,
TEXT_STAGE_Y + y * TEXT_SIZE,
line,
GetColor(255, 255, 255),
fontHandle);
}
}