
STEP05:map層の実装(25x25マップ描画・壁・敵・宝箱・施設)
STEP05では、探索RPGの心臓部になる 25x25マップ を扱う map層を実装します。
この層の目的は「マップのデータ管理」と「描画」と「移動できるか判定」を ゲーム本体の処理から切り離す ことです。
このSTEPでできるようになること
- 25x25のマップ(床・壁)を生成できる。
- 敵・宝箱・施設(宿・教会)を配置できる。
- マップを描画できる(プレイヤー位置は上書き表示)。
- 壁にぶつかる移動を防げる(当たり判定)。

今回追加するファイル
| ファイル | 役割 |
|---|---|
| map.h | map層の公開API(構造体、関数宣言、タイル定義) |
| map.c | 25x25マップ生成・配置・描画・判定の実装 |
タイル設計(床・壁・敵・宝箱・施設)
まず「マップ上の1マスが何を表すか」を決めます。
今回は扱いやすさ優先で 1文字(char) で表現します。
| 種類 | タイル文字 | 意味 | 移動可能 |
|---|---|---|---|
| 床 | . | 通路 | ○ |
| 壁 | # | 通れない | ✕ |
| 敵 | E | 戦闘が起きる | ○(踏める) |
| 宝箱 | T | アイテム入手 | ○(踏める) |
| 宿 | I | 全回復 | ○(踏める) |
| 教会 | C | 復活/蘇生 | ○(踏める) |
プレイヤーはマップ自体には書き込まず、描画時に P を重ねます(上書き表示)。
こうすると「プレイヤーが動いたとき、元の床や施設を壊さない」ので扱いがラクです。
map層で提供する関数一覧
| 関数 | 役割 |
|---|---|
| map_init | 25x25マップを初期化(床+外周壁+簡易迷路) |
| map_place_features | 宿・教会を固定配置 |
| map_place_random | 敵や宝箱をランダム配置 |
| map_draw | マップを描画(プレイヤー位置を上書き) |
| map_in_bounds | 座標が範囲内か判定 |
| map_is_walkable | そのマスに移動できるか判定(壁は不可) |
| map_get / map_set | タイル取得・設定 |
| map_try_move | 移動を試みる(壁なら止める)+踏んだタイルを返す |
map.h(公開ヘッダ)
/* map.h */
#ifndef MAP_H
#define MAP_H
#include "common.h" /* bool を使っている想定(無いなら stdbool.h に置き換えOK) */
#define MAP_W 25
#define MAP_H 25
/* タイル定義(1文字で表す) */
#define TILE_FLOOR '.'
#define TILE_WALL '#'
#define TILE_ENEMY 'E'
#define TILE_TREASURE 'T'
#define TILE_INN 'I'
#define TILE_CHURCH 'C'
typedef struct Map {
char cells[MAP_H][MAP_W];
} Map;
/* 初期化・配置 */
void map_init(Map *m);
void map_place_features(Map *m); /* 宿・教会など固定施設 */
void map_place_random(Map *m, int enemies, int treasures);
/* 描画 */
void map_draw(const Map *m, int px, int py);
/* 判定・取得 */
bool map_in_bounds(int x, int y);
bool map_is_walkable(const Map *m, int x, int y); /* 壁は移動不可 */
char map_get(const Map *m, int x, int y);
void map_set(Map *m, int x, int y, char tile);
/* 移動(成功したら座標更新、踏んだタイルを返す。失敗なら壁で止まり床扱いを返す) */
char map_try_move(const Map *m, int *px, int *py, int dx, int dy);
#endifmap.c(実装)
ポイントは次の3つです。
- マップ生成は「外周壁+床」でベースを作る
- 内部の壁は簡易パターン(迷路っぽく)にする(今回は分かりやすさ優先)
- 敵・宝箱は「床にだけ置く」「スタート地点と施設は避ける」
/* map.c */
#include "map.h"
#include <stdio.h>
#include <stdlib.h>
/* STEP02のplatform層があるなら使う(無ければこのままでも動く) */
void platform_set_color_default(void);
void platform_set_color_wall(void);
void platform_set_color_enemy(void);
void platform_set_color_treasure(void);
void platform_set_color_facility(void);
static int is_floor_like(char t) {
/* 敵・宝箱・施設も「踏める床」扱いにしたいので、壁以外は床側として扱う */
return (t != TILE_WALL);
}
bool map_in_bounds(int x, int y) {
return (x >= 0 && x < MAP_W && y >= 0 && y < MAP_H);
}
char map_get(const Map *m, int x, int y) {
if (!map_in_bounds(x, y)) return TILE_WALL; /* 範囲外は壁扱いで安全にする */
return m->cells[y][x];
}
void map_set(Map *m, int x, int y, char tile) {
if (!map_in_bounds(x, y)) return;
m->cells[y][x] = tile;
}
bool map_is_walkable(const Map *m, int x, int y) {
char t = map_get(m, x, y);
return (t != TILE_WALL);
}
/* 簡易な内部壁の生成(見た目を迷路っぽくする) */
static void build_simple_maze(Map *m) {
/* 縦壁:x=4,8,12,16,20 などに壁を作り、ところどころ穴を空ける */
for (int x = 4; x <= 20; x += 4) {
for (int y = 1; y < MAP_H - 1; y++) {
map_set(m, x, y, TILE_WALL);
}
/* 穴を空ける(通路) */
map_set(m, x, 3, TILE_FLOOR);
map_set(m, x, 9, TILE_FLOOR);
map_set(m, x, 15, TILE_FLOOR);
map_set(m, x, 21, TILE_FLOOR);
}
/* 横壁:y=6,12,18 に薄く壁を置いて変化を出す */
for (int y = 6; y <= 18; y += 6) {
for (int x = 1; x < MAP_W - 1; x++) {
if (x % 4 != 0) { /* 縦壁と完全に塞がないようにする */
map_set(m, x, y, TILE_WALL);
}
}
/* 穴を空ける */
map_set(m, 2, y, TILE_FLOOR);
map_set(m, 10, y, TILE_FLOOR);
map_set(m, 18, y, TILE_FLOOR);
map_set(m, 22, y, TILE_FLOOR);
}
/* スタート想定地点(1,1)周辺だけは通れるように軽く整える */
map_set(m, 1, 1, TILE_FLOOR);
map_set(m, 2, 1, TILE_FLOOR);
map_set(m, 1, 2, TILE_FLOOR);
map_set(m, 2, 2, TILE_FLOOR);
}
void map_init(Map *m) {
/* まず床で埋める */
for (int y = 0; y < MAP_H; y++) {
for (int x = 0; x < MAP_W; x++) {
m->cells[y][x] = TILE_FLOOR;
}
}
/* 外周を壁で囲う */
for (int x = 0; x < MAP_W; x++) {
m->cells[0][x] = TILE_WALL;
m->cells[MAP_H - 1][x] = TILE_WALL;
}
for (int y = 0; y < MAP_H; y++) {
m->cells[y][0] = TILE_WALL;
m->cells[y][MAP_W - 1] = TILE_WALL;
}
/* 内部に簡易迷路を作る */
build_simple_maze(m);
}
void map_place_features(Map *m) {
/* 宿と教会は固定配置(分かりやすい) */
/* 置く場所が壁だった場合は床に戻してから置く */
map_set(m, 3, 3, TILE_FLOOR);
map_set(m, 3, 3, TILE_INN);
map_set(m, 21, 21, TILE_FLOOR);
map_set(m, 21, 21, TILE_CHURCH);
}
/* 指定タイルを「床にだけ」ランダムに置く */
static void place_random_on_floor(Map *m, char tile, int count, int avoid_x, int avoid_y) {
int placed = 0;
int safety = 20000; /* 無限ループ防止 */
while (placed < count && safety-- > 0) {
int x = rand() % MAP_W;
int y = rand() % MAP_H;
/* 外周は壁なので避ける(ついでに範囲内で床を狙う) */
if (!map_in_bounds(x, y)) continue;
/* スタート地点は避ける */
if (x == avoid_x && y == avoid_y) continue;
/* 既に何か置いてある場所(床以外)は避ける */
if (map_get(m, x, y) != TILE_FLOOR) continue;
map_set(m, x, y, tile);
placed++;
}
}
void map_place_random(Map *m, int enemies, int treasures) {
/* スタート想定(1,1)は避ける */
place_random_on_floor(m, TILE_ENEMY, enemies, 1, 1);
place_random_on_floor(m, TILE_TREASURE, treasures, 1, 1);
}
/* 1マス描画(色があるならここで切り替える) */
static void draw_tile(char t) {
/* 色分け(platform層が未実装なら、これら関数は空実装にしてOK) */
if (t == TILE_WALL) {
platform_set_color_wall();
putchar('#');
platform_set_color_default();
return;
}
if (t == TILE_ENEMY) {
platform_set_color_enemy();
putchar('E');
platform_set_color_default();
return;
}
if (t == TILE_TREASURE) {
platform_set_color_treasure();
putchar('T');
platform_set_color_default();
return;
}
if (t == TILE_INN || t == TILE_CHURCH) {
platform_set_color_facility();
putchar(t);
platform_set_color_default();
return;
}
/* 床 */
platform_set_color_default();
putchar('.');
}
void map_draw(const Map *m, int px, int py) {
for (int y = 0; y < MAP_H; y++) {
for (int x = 0; x < MAP_W; x++) {
if (x == px && y == py) {
/* プレイヤーは上書き表示 */
platform_set_color_facility();
putchar('P');
platform_set_color_default();
} else {
draw_tile(m->cells[y][x]);
}
}
putchar('\n');
}
}
/* 移動を試みて、踏んだタイル(床/敵/宝箱/施設など)を返す */
char map_try_move(const Map *m, int *px, int *py, int dx, int dy) {
int nx = *px + dx;
int ny = *py + dy;
/* 壁なら止まる */
if (!map_is_walkable(m, nx, ny)) {
return TILE_FLOOR;
}
*px = nx;
*py = ny;
/* 踏んだ先のタイルを返す(イベント判定に使う) */
return map_get(m, nx, ny);
}platform層(色)がまだ無い場合の対処
map.c では platform_set_color_xxx() を呼んでいますが、STEP02でまだ色分け関数を用意していないなら、platform.c 側に空実装を置けばコンパイルできます。
/* platform.c に入れておく(暫定) */
void platform_set_color_default(void) {}
void platform_set_color_wall(void) {}
void platform_set_color_enemy(void) {}
void platform_set_color_treasure(void) {}
void platform_set_color_facility(void) {}後でANSIカラー対応やWindows用API対応を入れたら、ここを差し替えるだけでマップ表示が派手になります。
動作確認用 main.c(STEP05テスト)
STEP05の時点では「歩ける」「踏める」「描ける」が確認できればOKです。
入力は input層(STEP03)を使って、テンキー移動(8/2/4/6)で動かしてみます。
/* main.c(STEP05 動作確認用) */
#include <time.h>
#include "ui.h"
#include "input.h"
#include "map.h"
/* STEP02 */
void clear_screen(void);
void sfx_beep(int kind);
static void show_hint(void) {
printf("Move: 8=Up 2=Down 4=Left 6=Right 5=Status 0=Exit\n");
}
int main(void) {
srand((unsigned)time(NULL));
for (;;) {
UiTitleChoice c = ui_title_screen();
if (c == UI_TITLE_QUIT) break;
if (c == UI_TITLE_HOWTO) { ui_show_howto(); continue; }
/* マップ準備 */
Map m;
map_init(&m);
map_place_features(&m);
map_place_random(&m, 12, 8); /* 敵12、宝箱8 */
int px = 1, py = 1; /* スタート位置 */
for (;;) {
ui_print_header("ADVENTURE (STEP05 TEST)");
show_hint();
putchar('\n');
map_draw(&m, px, py);
putchar('\n');
int cmd = choose_menu("Command (0/2/4/5/6/8): ", 0, 8);
if (cmd == 0) {
sfx_beep(0);
break;
}
if (cmd == 5) {
sfx_beep(0);
UiStatusRow party[3] = {
{"Hero", 1, 30, 30, 10, 10},
{"Mage", 1, 18, 18, 25, 25},
{"Warrior", 1, 40, 40, 5, 5}
};
ui_print_header("STATUS");
ui_show_party_status(party, 3, 120);
ui_wait_enter("Press Enter to return...");
continue;
}
int dx = 0, dy = 0;
if (cmd == 8) dy = -1;
else if (cmd == 2) dy = 1;
else if (cmd == 4) dx = -1;
else if (cmd == 6) dx = 1;
else {
/* 1,3,7,9 を入れても無視(今回は使わない) */
continue;
}
char stepped = map_try_move(&m, &px, &py, dx, dy);
/* 踏んだタイルでイベントが起きる想定(STEP06以降で本実装) */
if (stepped == TILE_ENEMY) {
sfx_beep(1);
ui_print_header("ENCOUNTER (DUMMY)");
printf("Enemy appeared! (battle will be implemented later)\n\n");
ui_wait_enter("Press Enter...");
} else if (stepped == TILE_TREASURE) {
sfx_beep(0);
ui_print_header("TREASURE (DUMMY)");
printf("You found a treasure! (items will be implemented later)\n\n");
ui_wait_enter("Press Enter...");
} else if (stepped == TILE_INN) {
sfx_beep(2);
ui_print_header("INN (DUMMY)");
printf("Fully recovered! (services will be implemented later)\n\n");
ui_wait_enter("Press Enter...");
} else if (stepped == TILE_CHURCH) {
sfx_beep(0);
ui_print_header("CHURCH (DUMMY)");
printf("Revive service! (services will be implemented later)\n\n");
ui_wait_enter("Press Enter...");
}
}
}
ui_show_ending(0);
return 0;
}このSTEPのまとめ
- map層が「マップの持ち方」「描画」「当たり判定」を担当するようになった。
- プレイヤーはマップに書き込まず、描画で重ねる方式にした。
- 敵・宝箱・施設は “踏める” タイルとして扱い、イベント判定は後続STEPへ回せる形にした。
次のSTEP06へ(予告)
STEP06では、今ダミー表示になっているイベント部分を本格化します。
- services層(宿・教会の処理)
- items層(宝箱の中身、装備)
- enemies/battle層(敵との戦闘)
map層はこのまま「踏んだタイルを返す」だけにしておくと、後の拡張がすごくラクになります。
