本記事は Fujitsu Advent Calendar 2018 23日目の記事です。2019年12月23日だとばかり思っておりました(嘘です遅れてごめんなさい)
記事は全て個人の見解です。会社・組織を代表するものではありません。
今回は、Linux環境で動くタイピングゲームをC言語で書いてみました。
きっかけ
- 高専の頃、C言語で書いたタイピングゲームのソースコードを見つけました
- エスケープコードが認識できるWindowsのコマンドプロンプトでしか動作しないものでした
- なんか色々ひどかったんです(変数名が hantei_all_1 とか、int zenbu_kesu=0; とか涙を禁じ得ない)
- オープンソースの仕事を経て、なんか一度原点に戻ってオープンなソースを公開しようと思った(僕にとっての原点はC言語)
- せっかくだから、Linuxで動作できるように改造してみようかな
開発環境
- Ubuntu16.04
- screen + vim + ctags = ネ申
- ロギング関数を適当に作って、ひたすらログを仕込んでデバッグするスタイル
レシピ(★:新たに追加する仕様)
★ LinuxやMacOS上で動作可能
★ 画面制御機能を使って、何らかのアニメーションを付ける
- 使う言語はC言語のみ。徹頭徹尾レトロな感じで
- ファミコンのような感じを目指します
- 問題文は自由に編集可能
- この機能を持つタイピングゲームが作りたかったんです!
- 日本語文章と、そのルビ(半角英数)をそれぞれ用意するというお粗末な仕様ですが
- ステージは全部で5つ。ステージ毎に問題文の長さやクリアのための条件を調整する
- メニュー画面からステージ・オプション選択を行わせる
- ステージ選択後はカウントダウンを行い、その後ゲームスタート
- 制限時間内にタイピングを行い、スコアを元にクリアかどうかを判定
- スコアに応じたランクと、ゲームマスターからのコメントを表示させる
- 例:ランクS: Awesome !!!
- 制限時間は変更可能にする
- スコアに応じたランクと、ゲームマスターからのコメントを表示させる
- ハイスコアは10位まで記録・表示させる
- 一分間の入力文字数、正打率
- 全面クリアしたら、ご褒美AAを表示
出来上がったもの
- "TYPIST":https://github.com/fkawa-play/retro-typing
- 動作確認済のOS:Requirementsを見てください
構成
出来上がったコードをシンプル化してざっくりと説明していきます。
コメント文に色々書き加えています。
まずmain関数内ではメニュー画面を表示させ、キー入力に応じて関数の呼び出しを行っています。
int main(void) {
Typing types; // 1プレイごとの入力結果(成功打数、ミスタイプなど)を格納する構造体
Score result; // 1プレイごとのスコアを格納する構造体
Question Q[STAGES]; // ステージごとの問題文を格納する構造体
initialize(Q); // ここで問題文のロードを行います
print_title(); // タイトル画面の表示
wait_key_hit(ENTER_KEY); // エンターキーが押されるとメニュー画面へ遷移
while (TRUE) {
types.stage = select_stage(); // キー入力を受け取ります。
switch(types.stage) {
case STAGE1:
break;
case STAGE2:
break;
case STAGE3:
break;
case STAGE4:
break;
case STAGE5:
break;
case 'r': // 'r' の場合はランキング画面へ遷移します
print_ranking();
continue;
case 't': // 't' の場合は制限時間の設定画面へ遷移します
timelimit = select_timelimit();
continue;
case 'c': // 'c' の場合はクレジット画面へ遷移します
print_credit();
continue;
case 's': // 's' の場合は、全面クリアしている場合のみ ご褒美AAを表示します
if (flag_completed) print_aa();
continue;
case 'q': // 'q' が押されるとバイバイ画面へ遷移します
print_goodbye();
break;
default: // それ以外のキー入力はスルーします
continue;
}
if (types.stage == 'q') break; // バイバイ画面のあとはそのままbreakしてwhileループを脱出
run(&types, Q); // '1'〜'5'が押された場合、ゲーム開始です
}
finalize(Q); // 色々開放しまくってゲーム終了です
return 0;
}
- run() の中身は以下です:
void run(Typing *t, Question *q) {
print_countdown(); // Ⅲ...Ⅱ...Ⅰ... START! とカウントダウンを行います
typing(t, q); // ここでタイピング処理(下記参照)を行います
noecho(); // ゲーム終了後はキー入力を標準出力に "出さない” ようにします
print_end(); // END と表示します(そのまんま)
calc_result(t); // 結果を計算します。tには総入力数、成功入力数、ミスタイプ数などが格納されています。
}
タイピング処理
上記 typing() 関数の中身を紹介します。
キー入力した文字とファイルから読み込んだ文字列を先頭から一文字ずつ比較していきます。ポインタ変数を使って配列の中身を参照していきます。
このルーチンとは別に制限時間を制御できれば良かったのですが、僕にはシグナルハンドラを扱えるような知識はなく、仕方なくtyping()関数内で制限時間を制御することに決めました。
// 問題文の最大数を入力しきるか、制限時間が来るまでループします
for(i = 0; i < MAX_QUESTIONS; i++) {
// **①制限時間更新**(後述)
// ランダムに問題文を選択('r' には 0〜<最大問題数>までの乱数が格納されています)
// ruby は "ルビ"、つまり問題文の半角英数字表記です
target_key = Q[s].ruby[r];
// ポインタ変数が文字列の最後 '\n'に来るまでループします
// ヌル文字 '\0'で判定しない理由としては、ッターン!(改行)を押させたくなかったからです
while (*target_key != '\n') {
// **②制限時間更新**(後述)
// キー入力を受け取ります
pressed = getch();
// 成功した場合(入力したキーと問題文の文字が一致した場合)
if (pressed == *target_key) {
...
// コンボカウンターをインクリメント
// **③制限時間更新**(後述)
target_key++; // 問題文の次の文字列へ遷移します(ポインタ変数のインクリメント)
} else { // ミスタイプした場合
...
// **④制限時間更新**(後述)
// コンボカウンターをリセット
// ミスした箇所に "Miss!!" と画面表示する
}
}
}
タイピングゲーム中の画面はこんな感じです(ステージ4: Elephant steps)
ぶち当たる壁
コーディングしているうちに、いくつか壁にぶち当たりました。
制限時間の更新処理(kbhitがないよ!)
- プレイ画面右上に制限時間を秒単位でカウントダウンさせます
- 制限時間を更新するにあたり、まずはタイピング中の状態遷移について考えてみました
- タイピング中の状態遷移は大きく以下の3つです:
- typing()内の制限時間更新処理 ①, ②, ③, ④ がそれぞれ該当します
- ポイントは、A は待機状態、 B, Cは一瞬の状態で、すぐに A に遷移するという点です
- それぞれの状態で制限時間を更新すれば、制限時間の表示はできそうです
ここで一つ壁にぶち当たります。 ② のキー入力がない状態をどうやって判定すれば良いのだろう?
高専時代の秘伝のコードでは kbhit() なるメソッドを使って判定していました。これは、キー入力があったかどうかを判定するメソッドです。ということで解決.....
しませんでした。実はkbhit()が実装されているライブラリは、Windows以外では使えないようでした。ならばこそ、偉大なる方々が作っているに違いないと思い調べてみたところ、こちらを見つけました。公開されていたkbhit()を、作者の方に連絡を取って使用許可をいただきました。改めてありがとうございました、GAMIさん。
makeファイルがうまく動かないよ!
WEBで調べた通りに実行してもうまく動かない・・・なぜだ・・・
→ 半角スペースでインデントしていたのが原因でした。Makefileはタブ文字によるインデントなんですね。
→ RubyやPythonでのコーディングがメインでしたので、 vimで set expandtab
しちゃってました。
タイピングゲームを作るのに必要だったコト
以下の知識を使って、タイピングゲームを作ることができました。
必要な知識など | MEMO |
---|---|
ファイル入出力 | 問題文の読み込み、ランキングの読み書き、ログの出力 |
ポインタ、配列のポインタ、関数ポインタ | 本ゲームの最重要要素の一つ。入力した文字と問題文の文字列の比較用カウンタにポインタ変数を使いました |
動的メモリ確保(malloc) | 配列を確保する際は必須。mallocについて調べたら僕の師匠が出てきたのでビックリ |
構造体 | タイピングの結果格納用や、スコアの格納用などに |
関数プロトタイプ宣言 | int main(void)だけで作ろうだなんて甘かったです |
乱数 | 問題文をランダムで選択する際に |
時間(time, sys/time) | 制限時間やログ出力時の時間に |
ncurses | 画面操作、カーソル・マウス移動をつかさどるライブラリ。本作の要。使うとハッピーになれる(個人差があります) |
AAを書く根気 | vim使いじゃなかったら腱鞘炎になってたと思います |
新たに調べた知識
- Makefileの作り方
- TABに要注意
- ヘッダファイルの分割
- フリー素材の画像から、AAを作るフリーソフトの使い方
勉強不足で使えなかった(使わなかった)知識
- シグナルハンドラ、インターバルタイマ
- 制限時間はこれで制御・実装したかったんです。。
- 僕が試しに実装してみたところ、ゲーム終了してもタイマーが止まらずに永遠30秒毎に END!! と出続けました
- volatileとか
- volatile知らないんですか!?と入社一年目に先輩から怒られたのは良い思い出
教訓
- 一番良く知っているはずの自分が書いたコードでも、全然読めないし分からない
- コメントは本当に大事です
- 変数名は意味のあるものに
- 今回はほぼ作り直しに近かったです
- ログは大事だけど、ログだけでは全ては分からない
- printデバッグには限界がありました。ちゃんとデバッガ使いましょうね
- 一番詰まったのは、Macだけに起きた領域確保系のエラー(mallocで領域確保する際のバグ)
- ある程度妥協は必要
- わからないものは分からないので "あと回し"
- ncursesで画面表示をイジると本当に楽しいのですが、時間がなくなってくると中身がお粗末に
- 断腸の思いで切り上げた結果、マジックナンバーのオンパレードになりましたサーセン
- 流用する場合は、必ず許可・確認を取ること
- 最低限の礼儀ですね。あとは著作権とか著作権とか著作権とか
フィードバック
作ったゲームをテストプレイしてもらいました。テスターの方々、どうもありがとうございました。
-
環境について
- Ubuntu14.04の64bit(VM)でやってみましたが、できました
- git clone...で、gitがねーよ!と怒られたので、apt installに加えるとより親切かもしれません
- makeで、ncurses.hなんてファイルないよ!と怒られました。。aptitudeで探していろいろ検討した結果、libncurses5-devをインストールしたらできました
-
ゲームについて
- もう少し時間長くても良いかな~と思いました。
- 何の文字だったか忘れたけど、私が普段打つローマ字と違うのがあったので、設定できると良いかも?
- 文のチョイスが何とも独特
- 絵もかわいい
- 始まった時点でタイピングゲームであることがよくわかるキーボード風のデザイン良き
- 頑張ってアニメーションが作られてる所良き
- 「ん」「し」「しゃ」等の入力が固定なのが辛い。問題文をいじるのはやりたくない。。。最重要タスク
- クリアの条件がよくわからない。スコアだけで合否が決まる?失敗した時にクリアまで遠いのか近いのかわからないので頑張れない
- ゲーム開始方法が一瞬わからなかった。CLEAR列でカーソル動かして選択する方式かと
- 色付けられるのすごいね
- メモリリークのバグを見つけたのでこちらで治しておきました
最後に
良ければ一度タイピングゲームで遊んでみてください。
テスターさんからのフィードバックでもそれ以外でも、PRお待ちしています。
ここまで読んでくださって(スクロールして下さって)本当にありがとうございました!良いお年を!!