【C++】コンソールアプリでのタイピングゲームの作り方|ランダム関数・色変更

サムネイル画像 C++

今回は、C++のコンソールアプリでのタイピングゲーム作り方を解説していきます。

ランダム関数の使い方
ANSIエスケープシーケンスでの色変更のやり方
便利なvectorの使い方

といった基本的な内容から、カーソル制御などについても解説していきます。

この記事を読み終えると、『色変更の仕方』『リアルタイムキー入力』などが自在に使えるようになります。ぜひ最後まで読んでいただけると嬉しいです。


完成版コード

#include <iostream>
#include <vector>
#include <ctime>
#include <cstdlib>
#include <conio.h>

// ANSIエスケープシーケンスで色を設定
constexpr const char* GREEN = "\033[32m";
constexpr const char* RESET = "\033[0m";
constexpr const char* HIDECURSOR = "\033[?25l";
constexpr const char* SHOWCURSOR = "\033[?25h";

//==============================================
// プロトタイプ宣言
//==============================================
void clearScreen();
void hideCursor();
void showCursor();
void title();
void drawGame(const std::string& hiragana, const std::string& romanji, size_t correctChars, int remainingTime, int missCount, int score, int totalWords);
void exitGame(int score, int missCount, int timeLimit, int remainingTime, const std::vector<std::pair<std::string, std::string>>& words);

//==============================================
// メイン関数
//==============================================
int main() {
    // 日本語(ひらがな)とローマ字のペア
    std::vector<std::pair<std::string, std::string>> words = {
        {"おすし", "osushi"}, {"はなびたいかい", "hanabitaikai"}, {"さくらのはな", "sakuranohana"},
        {"ふじさん", "fujisan"}, {"たいようこうはつでん", "taiyoukouhatsuden"}, {"うちゅうじん", "utyuujin"},
        {"としょかん", "tosyokan"}, {"かいせんどん", "kaisendon"}, {"ほっかいどう", "hokkaidou"}
    };
    const int totalWords = words.size();
    std::srand(static_cast<unsigned int>(std::time(nullptr))); // 乱数の初期化

    hideCursor();
    title();

    int score = 0;
    int missCount = 0;
    const int timeLimit = 30;
    int remainingTime = timeLimit;

    size_t correctChars = 0;
    std::pair<std::string, std::string> currentPair;
    if (!words.empty()) {
        int randomIndex = std::rand() % words.size();
        currentPair = words[randomIndex];
        words.erase(words.begin() + randomIndex);
    }

    int startTime = static_cast<int>(std::time(nullptr));

    // メインゲームループ
    while (remainingTime > 0) {
        clearScreen();
        remainingTime = timeLimit - (static_cast<int>(std::time(nullptr)) - startTime);
        if (remainingTime < 0) remainingTime = 0;

        drawGame(currentPair.first, currentPair.second, correctChars, remainingTime, missCount, score, totalWords);

        if (_kbhit()) {
            char ch = _getch();
            if (ch == currentPair.second[correctChars]) {
                ++correctChars;
                if (correctChars == currentPair.second.size()) {
                    ++score;
                    correctChars = 0;
                    // 次の単語
                    if (!words.empty()) {
                        int randomIndex = std::rand() % words.size();
                        currentPair = words[randomIndex];
                        words.erase(words.begin() + randomIndex);
                    } else {
                        break; // 全クリアで終了
                    }
                }
            } else {
                ++missCount;
            }
        }
    }

    showCursor();
    clearScreen();
    exitGame(score, missCount, timeLimit, remainingTime, words);

    return 0;
}

//==============================================
// 関数定義
//==============================================

// 画面クリア(Windows専用)
void clearScreen() { system("cls"); }

// カーソル制御
void hideCursor() { std::cout << HIDECURSOR; }
void showCursor() { std::cout << SHOWCURSOR; }

// タイトル画面表示
void title() {
    std::cout << "==============================\n";
    std::cout << "        タイピングゲーム       \n";
    std::cout << "==============================\n";
    std::cout << "画面に表示された日本語をローマ字で入力してください。\n";
    std::cout << "ローマ字は日本語の下に表示されます。\n";
    std::cout << "制限時間内にできるだけ多くの単語を入力しましょう!\n";
    std::cout << "ゲームを始めるにはEnterキーを押してください。\n";
    std::cin.ignore();
}

// ゲーム画面描画
void drawGame(const std::string& hiragana, const std::string& romanji, size_t correctChars,
    int remainingTime, int missCount, int score, int totalWords) {
    std::cout << "残り時間: " << remainingTime << " 秒\n";
    std::cout << "クリア単語数: " << score << "/" << totalWords << "\n";
    std::cout << "MISS: " << missCount << "\n";
    std::cout << "ひらがな: " << hiragana << "\n";
    std::cout << "ローマ字: ";
    for (size_t i = 0; i < romanji.size(); ++i) {
        if (i < correctChars) {
            std::cout << GREEN << romanji[i] << RESET;
        } else {
            std::cout << romanji[i];
        }
    }
    std::cout << std::endl;
}

// 終了画面表示
void exitGame(int score, int missCount, int timeLimit, int remainingTime, const std::vector<std::pair<std::string, std::string>>& words) {
    std::cout << "\n==============================\n";
    std::cout << "        ゲーム終了!\n";
    std::cout << "あなたのスコア: " << (score * 10 - missCount * 5) * remainingTime << "\n";
    std::cout << "クリアした単語数: " << score << "\n";
    std::cout << "MISSの回数: " << missCount << "\n";
    if (words.empty()) {
        std::cout << "すべての単語をクリアしました!おめでとうございます!\n";
    }
    std::cout << "==============================\n";
    std::cin.ignore();
}

メインループ内の実行結果はこのようになります。

準備

インクルードファイル

#include <iostream>
#include <vector>
#include <ctime>
#include <cstdlib>
#include <conio.h>

それぞれざっくり説明していきます。

#include <iostream>

C++の標準入出力ストリームを使うためのヘッダです。

std::cout(画面に出力)や std::cin(キーボードからの入力)などを使用するために必要です。

#include <vector>

可変長の配列(動的配列)である std::vector を使うための標準ライブラリです。
タイピングゲームでは、出題する単語リストなどに利用しています。

#include <ctime>

時刻を取得するのにしようしています。

#include <cstdlib>

C言語由来の標準ライブラリ関数を利用するためのヘッダで、
rand() やなどの乱数生成を使う際に必要です。

#include <conio.h>

Windows環境で「リアルタイムにキーボード入力を処理する」ために使います。

  • _kbhit() でキー入力の有無を判定
  • _getch() でキー入力を1文字取得(Enter不要)

LinuxやMacでは使えないため、Windows専用のコードとなります。

定数定義

constexpr const char* GREEN = "\033[32m";
constexpr const char* RESET = "\033[0m";
constexpr const char* HIDECURSOR = "\033[?25l";
constexpr const char* SHOWCURSOR = "\033[?25h";

constexpr const char*でcharポインターの定数定義を行います。

#defineで定義するやり方もありますが、constexpr constで定義した方が安全性やスコープの明確性などの観点から定数定義の際は、constexpr constを使用しましょう。

また、色の定義はANSIエスケープシーケンスで行っています。

\033でESC文字。つまり、コントロールシーケンスの始まりを示しています。
[でコントロールシーケンスの開始。
32は緑色を表すカラーコードで、mで先ほどの緑色に変更する命令をしています。

カーソル制御は、

\033でESC文字。つまり、コントロールシーケンスの始まりを示しています。
[でコントロールシーケンスの開始。
?がデバイス制御、25がカーソル制御の番号。
lでreset。つまり、非表示に。
hでset。つまり、表示しています。

プロトタイプ宣言

コンパイラに下記関数がこの後出てくることを先に伝えるために、プロトタイプ宣言を行います。

void clearScreen();
void hideCursor();
void showCursor();
void title();
void drawGame(const std::string& hiragana, const std::string& romanji, size_t correctChars, int remainingTime, int missCount, int score, int totalWords);
void exitGame(int score, int missCount, int timeLimit, int remainingTime, const std::vector<std::pair<std::string, std::string>>& words);

今回は、main関数の下に関数を書いているのでプロトタイプ宣言が必要ですが、main関数の上に関数を書く場合は、プロトタイプ宣言は不要です。

各関数

clearScreen()

void clearScreen() { system("cls"); }

cstdlibのsystem()関数を用いて画面をクリアしています。

system("cls")はwindows専用なので、macOSやLinuxではsystem("clear")となります。

hideCursor(), showCursor()

void hideCursor() { std::cout << HIDECURSOR; }
void showCursor() { std::cout << SHOWCURSOR; }

定義したANSIエスケープシーケンスでのカーソル制御を呼び出します。

title()

void title() {
    std::cout << "==============================\n";
    std::cout << "        タイピングゲーム       \n";
    std::cout << "==============================\n";
    std::cout << "画面に表示された日本語をローマ字で入力してください。\n";
    std::cout << "ローマ字は日本語の下に表示されます。\n";
    std::cout << "制限時間内にできるだけ多くの単語を入力しましょう!\n";
    std::cout << "ゲームを始めるにはEnterキーを押してください。\n";
    std::cin.ignore();
}

タイトル画面の表示をしています。

\nで改行。

std::cin.ignore()キー入力待ちを行っています。

drawGame()

drawGame は、ゲーム画面を描画(表示)するための関数です。
タイピングゲームでは、単語の進捗状況やスコア、残り時間などをリアルタイムで更新してプレイヤーに見せる必要があります。
この関数がそれを担当しています。

void drawGame(const std::string& hiragana, const std::string& romanji, size_t correctChars,
    int remainingTime, int missCount, int score, int totalWords) {
    std::cout << "残り時間: " << remainingTime << " 秒\n";
    std::cout << "クリア単語数: " << score << "/" << totalWords << "\n";
    std::cout << "MISS: " << missCount << "\n";
    std::cout << "ひらがな: " << hiragana << "\n";
    std::cout << "ローマ字: ";
    for (size_t i = 0; i < romanji.size(); ++i) {
        if (i < correctChars) {
            std::cout << GREEN << romanji[i] << RESET;
        } else {
            std::cout << romanji[i];
        }
    }
    std::cout << std::endl;
}

ゲームのメイン部分の描画を行っています。

処理の流れとしては、
画面をクリア => 残り時間や打ち込む単語など、タイピングゲームに必要な情報を出力 => ローマ字で打ち込んだ文字が正しかったら緑色に変更

といった順です。緑色に変更しなくても動きはしますが、ビジュアル的にわかりやすくするために色を変えています。

exitGame()

exitGame 関数は、ゲーム終了後に結果を表示するための処理をまとめた関数です。
ゲームが終わったときに呼び出され、スコアやミス回数、クリア状況をプレイヤーに知らせます。

void exitGame(int score, int missCount, int timeLimit, int remainingTime, const std::vector<std::pair<std::string, std::string>>& words) {
    std::cout << "\n==============================\n";
    std::cout << "        ゲーム終了!\n";
    std::cout << "あなたのスコア: " << (score * 10 - missCount * 5) * remainingTime << "\n";
    std::cout << "クリアした単語数: " << score << "\n";
    std::cout << "MISSの回数: " << missCount << "\n";
    if (words.empty()) {
        std::cout << "すべての単語をクリアしました!おめでとうございます!\n";
    }
    std::cout << "==============================\n";
    std::cin.ignore();
}

words.empty()をif文で判定して、全クリア判定を行っています。

メイン関数

この main() 関数は、ゲーム全体の流れを制御する中枢です。
単語の準備 → 初期設定 → メインループ → 結果表示 という一連の処理を行います。

int main() {
    // 日本語(ひらがな)とローマ字のペア
    std::vector<std::pair<std::string, std::string>> words = {
        {"おすし", "osushi"}, {"はなびたいかい", "hanabitaikai"}, {"さくらのはな", "sakuranohana"},
        {"ふじさん", "fujisan"}, {"たいようこうはつでん", "taiyoukouhatsuden"}, {"うちゅうじん", "utyuujin"},
        {"としょかん", "tosyokan"}, {"かいせんどん", "kaisendon"}, {"ほっかいどう", "hokkaidou"}
    };
    const int totalWords = words.size();
    std::srand(static_cast<unsigned int>(std::time(nullptr))); // 乱数の初期化

    hideCursor();
    title();

    int score = 0;
    int missCount = 0;
    const int timeLimit = 30;
    int remainingTime = timeLimit;

    size_t correctChars = 0;
    std::pair<std::string, std::string> currentPair;
    if (!words.empty()) {
        int randomIndex = std::rand() % words.size();
        currentPair = words[randomIndex];
        words.erase(words.begin() + randomIndex);
    }

    int startTime = static_cast<int>(std::time(nullptr));

    // メインゲームループ
    while (remainingTime > 0) {
        clearScreen();
        remainingTime = timeLimit - (static_cast<int>(std::time(nullptr)) - startTime);
        if (remainingTime < 0) remainingTime = 0;

        drawGame(currentPair.first, currentPair.second, correctChars, remainingTime, missCount, score, totalWords);

        if (_kbhit()) {
            char ch = _getch();
            if (ch == currentPair.second[correctChars]) {
                ++correctChars;
                if (correctChars == currentPair.second.size()) {
                    ++score;
                    correctChars = 0;
                    // 次の単語
                    if (!words.empty()) {
                        int randomIndex = std::rand() % words.size();
                        currentPair = words[randomIndex];
                        words.erase(words.begin() + randomIndex);
                    } else {
                        break; // 全クリアで終了
                    }
                }
            } else {
                ++missCount;
            }
        }
    }

    showCursor();
    clearScreen();
    exitGame(score, missCount, timeLimit, remainingTime, words);

    return 0;
}

それぞれ解説していきます。

単語リストの準備

std::vector<std::pair<std::string, std::string>> words = {
    {"おすし", "osushi"}, {"はなびたいかい", "hanabitaikai"}, {"さくらのはな", "sakuranohana"},
    {"ふじさん", "fujisan"}, {"たいようこうはつでん", "taiyoukouhatsuden"}, {"うちゅうじん", "utyuujin"},
    {"としょかん", "tosyokan"}, {"かいせんどん", "kaisendon"}, {"ほっかいどう", "hokkaidou"}
};
const int totalWords = words.size();

std::vector<std::pair<std::string, std::string>>

  • ひらがなと対応するローマ字をセットで管理

ただのstringだけだと、セットで文字列を管理できないため、vectorを用いてセットで管理しています。

totalWords で総単語数を記録しておきます。

単語は出題済みになったら erase で削除され、同じ単語が出ない仕様です。

初期設定

std::srand(static_cast<unsigned int>(std::time(nullptr))); // 乱数の初期化
hideCursor();
title();

乱数初期化std::srand)で毎回異なる順番の単語を出題。
std::timeで現在の時刻を取得することで

カーソルを非表示hideCursor())して見た目を整える。

タイトル画面title())を表示してゲーム開始前に説明。

ゲーム用の変数

int score = 0;
int missCount = 0;
const int timeLimit = 30;
int remainingTime = timeLimit;

size_t correctChars = 0;
std::pair<std::string, std::string> currentPair;

最初の単語をセット

if (!words.empty()) {
    int randomIndex = std::rand() % words.size();
    currentPair = words[randomIndex];
    words.erase(words.begin() + randomIndex);
}

ランダムに1単語を選択し、currentPair にセット。

出題済み単語は erase で削除。

残り時間の計測

int startTime = static_cast<int>(std::time(nullptr));

ゲーム開始時刻を記録。

毎ループで現在時刻との差を計算して 残り時間を更新します。

メインゲームループ

    while (remainingTime > 0) {
        clearScreen();
        remainingTime = timeLimit - (static_cast<int>(std::time(nullptr)) - startTime);
        if (remainingTime < 0) remainingTime = 0;

        drawGame(currentPair.first, currentPair.second, correctChars, remainingTime, missCount, score, totalWords);

        if (_kbhit()) {
            char ch = _getch();
            if (ch == currentPair.second[correctChars]) {
                ++correctChars;
                if (correctChars == currentPair.second.size()) {
                    ++score;
                    correctChars = 0;
                    // 次の単語
                    if (!words.empty()) {
                        int randomIndex = std::rand() % words.size();
                        currentPair = words[randomIndex];
                        words.erase(words.begin() + randomIndex);
                    } else {
                        break; // 全クリアで終了
                    }
                }
            } else {
                ++missCount;
            }
        }
    }

clearScreen() で毎回画面をリフレッシュ。

残り時間をリアルタイムで計算し、ゼロ以下なら終了。

drawGame() を呼び出し、現在の状況を描画。

_kbhit():キー入力があるか確認。

_getch():1文字入力を即時取得(Enter不要)。

正解の文字なら correctChars++

  • 単語を全部打ち終えたらスコア加算し、次の単語をランダムに選ぶ。

間違えたら missCount++

単語がなくなったら break してループ終了。

ゲーム終了処理

showCursor();
clearScreen();
exitGame(score, missCount, timeLimit, remainingTime, words);

カーソルを再表示showCursor()

画面をクリアclearScreen()

結果表示関数 exitGame() を呼び出して終了メッセージを出力。


まとめ

このプログラムを通して、C++の以下の要素を実践的に学ぶことができます:

  • ランダムな出題rand() % words.size()
  • リアルタイム入力_kbhit, _getch
  • 制限時間の計算std::time を使用)
  • 単語管理のための std::vector 操作(erase)
  • ゲームループ構造の実装

初心者にとっては少し長めのコードですが、遊びながら実践できるため、学びのモチベーションも高まりやすい構成になっています。


学習を深めるためのアレンジ例

さらに理解を深めるために、次のような要素を追加するのもおすすめです。

  • 入力ミスが続いたらペナルティで時間を減らす
  • 終了後にタイピング速度を表示する
  • ランキング機能を追加して人とスコアを競えるようにする
  • 画面のちらつきをダブルバッファ等を用いて無くす

こういった工夫を加えていくことで、C++の理解をより実践的に深めることができます。
ぜひ、自分なりのアレンジを楽しみながらプログラミング力を磨いてみてください!

コメント