STEP04:ui層の実装(タイトル・エンディング・ステータス表示)

STEP04では、画面に表示する処理をまとめる ui層 を作ります。
ゲーム本体(探索・戦闘など)の処理から「表示の細かい手順」を追い出して、コードを読みやすく・直しやすくするのが目的です。

このSTEPで実装するものは次の3つです。

  • タイトル画面(開始/遊び方/終了)
  • エンディング表示(クリア演出・メッセージ)
  • ステータス表示(パーティのHP/MPなどの一覧表示)

前STEPまでの土台も活用します。

  • platform層:画面クリア、色、効果音
  • input層:安全な数値入力、メニュー選択

ui層の役割

主な責務
input入力を安全に受け取るchoose_menu()
platformOS差分(画面クリア、色、音)clear_screen(), platform_set_color()
ui見た目・表示の統一タイトル、枠、ステータス表

ui層は「ゲームのルール」を持ちません。
「どう表示するか」だけに集中させます。

今回追加するファイル

ファイル内容
ui.hui層の公開関数(宣言)
ui.cタイトル・エンディング・ステータス表示の実装

ui.h(公開API)

ここでは、後続STEPでも使いやすい形にしておきます。

  • タイトルの戻り値は enum(どれを選んだか)
  • ステータス表示は「UI側専用の軽い構造体」を使う(後でcharacters層ができても繋げやすい)
/* ui.h */
#ifndef UI_H
#define UI_H

#include "common.h"

/* タイトルメニューの選択結果 */
typedef enum UiTitleChoice {
    UI_TITLE_START = 1,
    UI_TITLE_HOWTO = 2,
    UI_TITLE_QUIT  = 3
} UiTitleChoice;

/* 表示用ステータス(UIに必要な最低限だけ) */
typedef struct UiStatusRow {
    const char *name;
    int level;
    int hp, hp_max;
    int mp, mp_max;
} UiStatusRow;

/* 画面表示の基本部品 */
void ui_wait_enter(const char *prompt);
void ui_print_header(const char *title);

/* タイトル/エンディング */
UiTitleChoice ui_title_screen(void);
void ui_show_howto(void);
void ui_show_ending(int is_clear);

/* ステータス表示 */
void ui_show_party_status(const UiStatusRow *rows, int count, int gold);

#endif

ui.c(実装)

ポイントは「見た目を固定化する小さな部品」を用意しておくことです。

  • 画面上部の見出し(ヘッダ)
  • Enter待ち
  • ステータス表の描画

ここでは platform層の関数名を次のように仮定します(あなたのSTEP02の名前に合わせて読み替えてOKです)。

  • clear_screen():画面クリア
  • sfx_beep(kind):ビープ(kindは0=決定など)
  • platform_set_color_xxx / platform_reset_color:色(もし用意済みなら使う)

色関数がまだ無い・名前が違う場合でも、色部分はコメントアウトしても動きます。

/* ui.c */
#include "ui.h"
#include "input.h"

/* STEP02のplatform層関数(名前はあなたの実装に合わせて調整) */
void clear_screen(void);
void sfx_beep(int kind);

/* 色がある場合だけ使う想定(無いなら削除/コメントアウトでOK) */
void platform_set_color_title(void);
void platform_set_color_accent(void);
void platform_reset_color(void);

static void print_line(int width) {
    for (int i = 0; i < width; i++) putchar('=');
    putchar('\n');
}

void ui_wait_enter(const char *prompt) {
    char buf[8];
    if (prompt && prompt[0]) printf("%s", prompt);
    (void)read_line(buf, (int)sizeof(buf));
}

void ui_print_header(const char *title) {
    clear_screen();

    /* platform_set_color_title(); */
    print_line(60);
    printf("%s\n", title);
    print_line(60);
    /* platform_reset_color(); */

    putchar('\n');
}

UiTitleChoice ui_title_screen(void) {
    for (;;) {
        ui_print_header("BATTLE RPG");

        /* platform_set_color_accent(); */
        printf("1) Start\n");
        printf("2) How to play\n");
        printf("3) Quit\n");
        /* platform_reset_color(); */

        putchar('\n');

        int sel = choose_menu("Select (1-3): ", 1, 3);
        sfx_beep(0);

        return (UiTitleChoice)sel;
    }
}

void ui_show_howto(void) {
    ui_print_header("HOW TO PLAY");

    printf("Move on the map with numeric keypad.\n");
    printf("  8: Up   2: Down   4: Left   6: Right\n");
    putchar('\n');
    printf("On your journey, you may find:\n");
    printf("  - Enemies (battle)\n");
    printf("  - Treasure chests (items)\n");
    printf("  - Inn (full recovery)\n");
    printf("  - Church (revive)\n");
    putchar('\n');

    ui_wait_enter("Press Enter to return...");
    sfx_beep(0);
}

void ui_show_ending(int is_clear) {
    if (is_clear) {
        ui_print_header("THE END");
        /* platform_set_color_title(); */
        printf("Congratulations!\n");
        printf("You defeated the final enemy and saved the world.\n");
        /* platform_reset_color(); */
        putchar('\n');
        sfx_beep(3); /* 勝利 */
    } else {
        ui_print_header("GAME OVER");
        printf("Your party was defeated...\n");
        putchar('\n');
        sfx_beep(4); /* 警告など */
    }

    ui_wait_enter("Press Enter to quit...");
}

void ui_show_party_status(const UiStatusRow *rows, int count, int gold) {
    /* 画面のどこで呼ぶかはゲーム側次第なので、ここでは「表示だけ」 */
    printf("[Party Status]\n");
    print_line(60);

    printf("%-12s %5s  %12s  %12s\n", "Name", "Lv", "HP", "MP");

    for (int i = 0; i < count; i++) {
        const UiStatusRow *r = &rows[i];
        char hpbuf[32];
        char mpbuf[32];

        snprintf(hpbuf, sizeof(hpbuf), "%d/%d", r->hp, r->hp_max);
        snprintf(mpbuf, sizeof(mpbuf), "%d/%d", r->mp, r->mp_max);

        printf("%-12s %5d  %12s  %12s\n", r->name, r->level, hpbuf, mpbuf);
    }

    print_line(60);
    printf("Gold: %d\n", gold);
    putchar('\n');
}

main.c 側の最小動作例(UIの動作確認)

STEP04の時点では、探索や戦闘がまだ無くてもOKです。
「タイトル → 遊び方 → 戻る」「開始を選んだらダミーでステータス表示」「終了」だけで土台確認できます。

/* main.c(STEP04の動作確認用の例) */
#include "ui.h"

int main(void) {
    for (;;) {
        UiTitleChoice c = ui_title_screen();

        if (c == UI_TITLE_QUIT) {
            ui_show_ending(0);
            break;
        }

        if (c == UI_TITLE_HOWTO) {
            ui_show_howto();
            continue;
        }

        /* START を選んだ場合:今はダミー表示 */
        ui_print_header("ADVENTURE (DUMMY)");

        UiStatusRow party[3] = {
            {"Hero", 1, 30, 30, 10, 10},
            {"Mage", 1, 18, 18, 25, 25},
            {"Warrior", 1, 40, 40, 5, 5}
        };

        ui_show_party_status(party, 3, 120);
        ui_wait_enter("Press Enter to return to title...");
    }

    return 0;
}

このSTEPの狙い(後の拡張がラクになる)

ui層にまとめておくと、後でこういう変更が簡単になります。

変更したいことui層があると
表示を豪華にしたいui.cだけ直せばよい
ステータス項目を増やすUiStatusRowと表示を更新
画面の統一感を出したいui_print_headerを全画面で使う
色や効果音の演出を増やすplatform呼び出しをui側に追加

次のSTEPへのつながり(STEP05予告)

次のSTEP05では、探索に必要な「マップ表示」や「カーソル移動」など、表示+入力の組み合わせが増えていきます。
STEP04の ui層があると、探索画面でも

  • ui_print_header(...)
  • ui_show_party_status(...)
  • choose_menu / 安全入力

という形で、処理の見通しがかなり良くなります。