20 秒で終わるゲームを作るときの設計
小さいループほど、入力の手触りが全てになります。判定・演出・音の優先順位を整理しました。
このサイトに置いてある「格子ハンター」は 1 プレイ 20 秒です。 短いゲームを作ってみて分かったことを書きます。
短いと、システムを説明する余地がない
20 秒しかないので、ルール説明を読ませる時間はありません。 プレイヤーは最初の 1 秒で何をすればいいか分かる必要があります。
格子ハンターでは、盤面のセルのうち 1 つだけが色付きで脈打ちます。 それ以外は全部同じ灰色です。この差だけで「光っているものを押す」と伝わります。 チュートリアルは書いていません。
逆に言うと、ルールを 1 つに絞れないゲームは 20 秒には収まりません。 思いついた要素を足したくなったら、その都度「これは説明なしで伝わるか」で切りました。
優先順位は 判定 > 演出 > 音
限られた時間で作るとき、この順に手をかけると失敗しません。
判定が最優先です。押した場所と当たり判定がずれていると、それだけでゲームが壊れます。 特に六角形のグリッドは、矩形と違って座標からセル番号を出す式が素直ではありません。 奇数行を半セルずらす方式にしたので、逆算はこうなります。
const row = Math.floor(y / cellH);
const col = Math.floor((x - (row % 2 ? cellW / 2 : 0)) / cellW);
演出が次点。当たったかどうかが、スコアの数字を見なくても分かる必要があります。 クリック地点から広がるリングを 0.6 秒かけて消しています。これがないと、 「押したのに反応がない」と感じられてしまいます。
音は最後です。あると気持ちいいですが、無くてもゲームは成立します。 実際、公開時点ではまだ入れていません。
タイマーは描画ループと分ける
残り時間のカウントは setInterval で 1 秒ごとに減らし、描画は
requestAnimationFrame で回しています。
最初は描画ループの中で経過時間から残り秒数を計算していたのですが、
タブが裏に回ると requestAnimationFrame が止まるため、戻ってきたときに
時間が飛びませんでした。「裏に回している間は時間が止まる」のは、
ゲームとしては都合が良すぎます。
とはいえ setInterval もタブが裏だと間隔が伸びます。厳密にやるなら
開始時刻を記録して実時間との差分で計算すべきで、そこは今後直す予定です。
ミスに罰を置くかどうか
外したときにスコアを 1 減らす仕様にしています。これがないと、 連打が最適解になってゲームが成立しません。
ただし 0 未満にはしません。序盤に外し続けたプレイヤーが、 挽回不可能な気分になるのを避けるためです。
this.setState(st => ({ score: Math.max(0, st.score - 1) }));
短いゲームでは、1 回のミスの重みが相対的に大きくなります。 罰は「連打を止める」のに必要な最小限にとどめるのがちょうどよさそうでした。