
STEP03:input層の実装(安全な数値入力・コマンド選択の仕組み)
STEP03では、プレイヤー入力をまとめて扱う input層 を作ります。
RPGの各画面(タイトル・探索・戦闘・宿・教会など)では「数字で選ぶ」「コマンドを選ぶ」操作が大量に出てきますが、毎回 scanf や getchar を直書きし始めると、すぐにバグの温床になります。
そこでこのSTEPでは、
- 文字列として1行入力して
- 数値に安全に変換し
- 範囲チェックもして
- “番号選択” を共通化する
という土台を input層に閉じ込めます。
input層でやりたいこと(役割)
| 入力の課題 | ありがちな失敗 | input層での解決 |
|---|---|---|
| 数値入力が壊れる | scanf("%d",&n) で不正入力→入力バッファが詰む | 1行読み→ strtol で変換 |
| 範囲外が来る | 0〜3のはずが 999 が通る | min/max を必ずチェック |
| Enterや空文字 | 空入力で変換失敗 | 空は弾いて再入力 |
| メニュー選択が散らかる | 画面ごとに入力処理がバラバラ | choose_menu(min,max) を共通化 |
STEP03で登場するファイル
| ファイル | 役割 |
|---|---|
| input.h | input層の公開関数(宣言) |
| input.c | 安全な行入力・数値変換・メニュー選択 |
※ common.h は既にある前提です(STEP02で用意済み)。
input.h:公開する関数を宣言する
この教材RPGで「まず欲しい入力機能」は次の3つです。
- 1行入力(文字列)
- 安全な整数入力(再入力つき)
- 範囲つきのメニュー選択(コマンド選択の仕組み)
掲載:input.h
/* input.h */
#ifndef INPUT_H
#define INPUT_H
#include "common.h"
/* 1行入力(末尾の改行は取り除く) */
int read_line(char *buf, int buflen);
/* 安全な整数入力(失敗したら再入力) */
int read_int_safely(const char *prompt, int *out);
/* min〜max の範囲で選ばせるメニュー選択 */
int choose_menu(const char *prompt, int min, int max);
#endifinput.c:安全な入力の実装
ポイントは「scanf禁止」じゃなく「教材として事故りにくい形」
scanf("%d") は簡単ですが、不正入力時に入力が残ってループが壊れやすいのが弱点です。
そこで input層では必ず
- fgets で1行読み取る。
- strtol で変換する。
- 変換失敗・範囲外なら再入力
この流れに統一します。
掲載:input.c(read_line / read_int_safely / choose_menu)
/* input.c */
#include "input.h"
#include <ctype.h>
#include <errno.h>
#include <limits.h>
/* 末尾の改行を除去する */
static void chomp_newline(char *s) {
size_t n = strlen(s);
if (n > 0 && s[n - 1] == '\n') s[n - 1] = '\0';
}
/* 空白だけの文字列か? */
static int is_blank(const char *s) {
while (*s) {
if (!isspace((unsigned char)*s)) return 0;
s++;
}
return 1;
}
int read_line(char *buf, int buflen) {
if (!fgets(buf, buflen, stdin)) {
return 0; /* EOFなど */
}
chomp_newline(buf);
return 1;
}
int read_int_safely(const char *prompt, int *out) {
char buf[128];
for (;;) {
if (prompt && prompt[0]) {
printf("%s", prompt);
fflush(stdout);
}
if (!read_line(buf, (int)sizeof(buf))) {
return 0; /* 入力終了(EOF) */
}
if (buf[0] == '\0' || is_blank(buf)) {
printf("Please enter a number.\n");
continue;
}
errno = 0;
char *end = NULL;
long v = strtol(buf, &end, 10);
/* 変換失敗:数字が1文字も無い、途中で止まった、オーバーフローなど */
if (end == buf) {
printf("Invalid input. Please enter a number.\n");
continue;
}
if (errno == ERANGE || v < INT_MIN || v > INT_MAX) {
printf("Number out of range.\n");
continue;
}
/* 末尾に余計な文字がある(例: 12abc) */
while (*end) {
if (!isspace((unsigned char)*end)) {
printf("Invalid input. Please enter a number.\n");
goto retry;
}
end++;
}
*out = (int)v;
return 1;
retry:
continue;
}
}
int choose_menu(const char *prompt, int min, int max) {
int n;
for (;;) {
if (!read_int_safely(prompt, &n)) {
return min; /* EOF時はとりあえずmin扱い(ゲーム側で終了処理してもOK) */
}
if (n < min || n > max) {
printf("Please choose %d - %d.\n", min, max);
continue;
}
return n;
}
}実装の読み解き(重要ポイント)
➀ read_line:fgets を使う理由
| 方法 | 問題点 |
|---|---|
| scanf | 不正入力が残りやすい、空白で止まる、改行処理が面倒 |
| fgets | 1行まるごと受け取れる、入力の事故が起きにくい |
fgets は改行 \n も含めて読むので、chomp_newline で削っています。
➁ read_int_safely:strtol を使う理由
strtol は「どこまで数として読めたか」が end に返ってくるので、
- 数字が無かった(end==buf)
- 途中で文字が混ざった(12abc)
- オーバーフロー(errno==ERANGE)
を判定できます。
| チェック | 例 | どう判定する? |
|---|---|---|
| 数字が無い | abc | end == buf |
| 途中に文字 | 12abc | *end に非空白が残る |
| 範囲外 | 999999999999 | errno == ERANGE |
➂ choose_menu:コマンド選択の仕組み
ゲームでよくある「番号で選ぶ」を1つの関数にまとめます。
例:戦闘で 1=Attack, 2=Skill, 3=Item, 4=Run ならこう書けます。
int cmd = choose_menu("Command (1-4): ", 1, 4);これだけで、
- 数字じゃない入力を弾く
- 1〜4以外を弾く
- 正しく入力されるまで再入力
が全部揃います。
呼び出し例(ゲーム側のイメージ)
例1:タイトル画面の選択
printf("1) Start\n");
printf("2) How to play\n");
printf("3) Quit\n");
int sel = choose_menu("Select (1-3): ", 1, 3);例2:移動入力(テンキー入力 2/4/6/8)
printf("Move: 8=Up 2=Down 4=Left 6=Right\n");
int dir = choose_menu("Input (2/4/6/8): ", 2, 8);
/* このままだと 3,5,7 も入るので、ゲーム側で弾くか、別関数にしてもOK */※ この「2/4/6/8だけ許す」は、次STEP以降で choose_from_list のような関数を足すとさらに綺麗になります。
よくある落とし穴(STEP03で潰しておく)
| 落とし穴 | 症状 | STEP03の実装でどう回避? |
|---|---|---|
| scanfループ地獄 | 入力ミスすると無限に同じエラー | fgets + strtol |
| 空入力で変換失敗 | Enterだけで壊れる | 空や空白を弾く |
| 12abcが通る | 想定外の入力が混ざる | end以降に非空白があれば弾く |
| EOF(Ctrl+Z/Ctrl+D) | そのまま落ちる | read_line が 0 を返す |
次のSTEPへのつながり(STEP04の予告)
次の STEP04 では ui層(表示の共通化)に進みます。
- メニュー表示
- メッセージ枠
- 色付き表示(STEP02/03で用意した土台を活用)
input層ができたことで、ui層側は「表示→choose_menuで選択」の流れがスッキリ書けるようになります。
