半年ぶりの実力確認テストでしたが如何でしたか。今回は学んだ事の応用がありますので、苦戦した方もいたのではないかと思っています。例え今回分からなくても、解答解説でなるほどとアハ体験することが大事なのだと思います。また、今回も私の提示する解答はひとつの見本ですので、こういうやり方もあるとか、確認しながら勧めて頂ければ幸いです。
※ マウスにコダワリがなければこれが断然オススメです!

【楽天】
ロジクール ワイヤレスマウス 無線 マウス M185 小型 電池寿命最大12ケ月 M185CGa M185BLa M185RDa 国内正規品 3年間無償保証
※ キーボードはこれを私は使っています。キーを叩く音が静かで気になりません。何万円もする高級キーボードを買うのでなければ、これで十分仕事にも使えてます。

【楽天】
ロジクール ワイヤレスキーボード K295GP K295OW 静音 耐水 キーボード 無線 Unifying windows chrome K295 国内正規品 2年間無償保証
- 状態遷移の応用
enum EAttackStatus { INP_HOR, INP_VER, ENTER }; EAttackStatus status = INP_HOR; // 攻撃目標を入力 printf("%sAttack ? : ", FWHITE); while (status != ENTER) { if (_kbhit()) { int key = tolower(_getch()); switch (status) { case INP_HOR: // 英字の入力 atkX = key - 'a'; if (0 <= atkX && atkX < FIELD_WIDTH) { printf("%c-", atkX + 'A'); status = INP_VER; } break; case INP_VER: // 数字の入力 if (key == 0x1b) { printf("\n"); status = INP_HOR; return; } else { atkY = key - '0'; if (0 <= atkY && atkY < FIELD_HEIGHT) { printf("%d", atkY); status = ENTER; } } break; } } }
ローカルな状態遷移は EAttackStatus 列挙体で管理します。そして、初期値は横方向の入力 INP_HOR とします。この場合はキャンセルはありません。数値が入力されたら、ローカルな状態管理を、縦方向の入力である INP_VER に切り替えます。INP_VER では、入力された文字が 0x1b、つまりは Escキーであれば、改行を表示して、状態を INP_HOR に戻してしまいます。これでキャンセル処理が出来た事になります。
さてさて、実は今回の問題ですが、ここまで大規模に改変しなくても、もっと簡単にキャンセルを実装する方法があります。それは、数字の入力時に 0x1b が入ってきたら、そのまま attackPlayer から return してしまうのです。ただ、表示位置がおかしくなるので printf("\n"); だけは実行しておきます。
これでなぜキャンセルとして正常に動作するかですが、遷移状態を変更せずに return しているので、もう一度同じ attackPlayer 関数に戻ってくるためです。その後の拡張を考慮しなければ、こういう変更でも良いかと思います。いきなり、こちらの実装方法を思いついた人は、逆にローカルで状態遷移する方法は応用が色々効くと思うので、覚えておくと良いと思います。
// 数字の入力 do { int key = _getch(); if (key == 0x1b) { printf("\n"); return; } atkY = key - '0'; } while (atkY < 0 || FIELD_HEIGHT <= atkY); printf("%d", atkY);
Logicool(ロジクール)
2021-06-10

【楽天】
ロジクール ワイヤレスマウス 無線 マウス M185 小型 電池寿命最大12ケ月 M185CGa M185BLa M185RDa 国内正規品 3年間無償保証
- 問30. 状態遷移を関数ポインタで実装
状態遷移を管理番号ではなくて関数ポインタで実現する方法は、ズバリ、現在処理する関数アドレスを保存するようにして、メインからの呼び出しは全て関数ポインタ経由にしてしまう事です。そのための準備として、まずはグローバル変数として以下を宣言します。
最初に艦船の配置を初期化として実行しますので、初期値として deployShips を入れていますが、こちらはメイン側でちゃんと初期化を行う前提であれば、
typedef void(*FuncStatus)(void); FuncStatus pFunc = deployShips;
としてもらったほうが良いかもしれません。続いてメイン関数の改変です。今までだと状態遷移変数の値によって、呼び出す関数を変化させていましたので、switch で処理の流れを分岐させていましたが、今回は既に呼び出し関数アドレスがポインタとして変数に格納されている前提ですので、
FuncStatus pFunc = NULL;
このようにとてもシンプルになります。最後に状態の変更です。入れる値が状態管理番号から関数呼び出しポインタに変わるだけですので、
while (true) { pFunc(); }
このように pFunc に次に呼び出したい関数のポインタを格納するだけとなります。…ですが、実はこれだけだと関数名が見つからないとコンパイルエラーとなります。これは前方呼び出しができない C言語の文法上の問題です。これを解決する方法は既に C言語019 プロトタイプ宣言で学んでいますよね。関数名を extern を付けて事前に宣言しておくのです。ついでに main.cpp 内で使っているグローバル変数や定数定義系も全てヘッダファイルに書き出してしまいます。
void deployShips(void) { // 中略 pFunc = drawGameFiled; } void drawGameFiled(void) { // 中略 pFunc = sureDeploy; } void sureDeploy(void) { // 前略 if (key == 'y') { pFunc = attackPlayer; } else { // 中略 pFunc = deployShips; } }
これであとは main.cpp 側では
#pragma once #define FIELD_WIDTH 10 #define FIELD_HEIGHT 10 #define MAX 5 // 座標構造体 struct TPoint { int X; // X座標 int Y; // Y座標 }; typedef struct TPoint Point; // 艦船構造体 enum EShip { NONE, CARRIER, BATTLE, CRUISER1, CRUISER2, DSTSHIP, DESTROY = -1 }; struct TShip { const char* pName; // 名前 EShip Type; // タイプ Point Position; // 座標 bool IsVertical; // 縦方向か? }; typedef struct TShip Ship; typedef void(*FuncStatus)(void); // グローバル変数定義 extern EShip gSea[FIELD_WIDTH][FIELD_HEIGHT]; extern FuncStatus pFunc; // 関数定義 extern void Sleep(int msec); extern int GetShipLength(EShip type); extern void deployShips(void); extern void drawGameFiled(void); extern void attackPlayer(void); extern void sureDeploy(void); extern int nearZero(const void* n1, const void* n2); extern int main(void);
#include "main.h"
と記述するだけです。
細かい修正点が多いので、詳細はこちらの解答サンプルを御覧ください。
2023/08/10
Logicool(ロジクール)
2020-10-22

【楽天】
ロジクール ワイヤレスキーボード K295GP K295OW 静音 耐水 キーボード 無線 Unifying windows chrome K295 国内正規品 2年間無償保証
- 問31. コールバック関数の実装
nearZero を3通り作る。0 に近い順という点をどう処理するかとなります。こちらは簡単でしたかねぇ。まあ、コールバックを馴染む事と、解決方法は一つじゃないってのを理解するにも良いかと思いました。私ならとりあえず以下の4つを提示します。
※ キオクシアの高品質 M.2 SSD 1TB がここまで安くなってた!
これは愚直にマイナスならプラスの値に変換してから減算結果を返却しています。今まで習った範囲で最も簡単に解決する方法ではないかと思います。三項演算子を使えば、
int nearZero(const void* n1, const void* n2) { int num1 = *(int*)n1; int num2 = *(int*)n2; if (num1 < 0) num1 *= -1; if (num2 < 0) num2 *= -1; return num1 - num2; }
こんな感じでひとまとめに出来てもしまいます。要するに比較のために符号をなくせば良いのですから、もう少しスマートにライブラリの絶対値を取得する関数を呼び出して、
int nearZero(const void* n1, const void* n2) { int num1 = *(int*)n1; int num2 = *(int*)n2; return (num1 < 0 ? -num1 : num1) - (num2 < 0 ? -num2 : num2); }
このように書くことも出来ます。少し C言語に慣れている方なら、この書き方が一番しっくり来るのではないでしょうか。そして、最後にゲームプログラマ的な解答としては、
int nearZero(const void* n1, const void* n2) { int num1 = *(int*)n1; int num2 = *(int*)n2; return abs(num1) - abs(num2); }
int nearZero(const void* n1, const void* n2) { int num1 = *(int*)n1; int num2 = *(int*)n2; return num1 * num1 - num2 * num2; }
こんな書き方も出来ます。減算結果に意味はなくて、ようはどちらが値として大きいかだけを判定すればよいのですから、二乗してしまえば符号はなくなります。この二乗を使う方法は、円の当たり判定でよく使う計算式です。この辺りは機会があればまた別途説明します。
キオクシア(KIOXIA)
コメント