[Astro] #204 だるまさんが転んだに「見つめて押し戻す」と連鎖を足す(注視判定・波紋の連鎖・スコア・難易度)
はじめに
#203 で作った DARUMA は、「見ている間は止まる」だけのゲームでした。敵が 8 体を超えると、360° から囲まれて、見張りきれなくなります。画面に触れないゴーグルでは、ボタンで使うアイテムも使えません。そこで、見る行為だけで、敵に対処できる仕組みを足しました。
- 画面中央の
+を、1 体の敵に重ね続けると、その敵は出現位置まで押し戻される - 押し戻された敵の足元から、波紋が広がる。波紋に触れた敵も押し戻されて、そこからまた波紋が出る(連鎖)
- 押し戻した敵は 100 点。連鎖の段が 1 つ増えるごとに、点が倍になる
- 難易度で、ステージの敵の数が変わる
- 変更したのは、
daruma/daruma.jsと、common/cardboard.jsの 2 行です
1. 見つめて押し戻す
画面中央の +(カメラの前方向)が、敵に当たっているかを、角度で判定します。敵ごとに、「カメラから敵の胸の高さへ向かう方向」と「カメラの向き」の角度を測って、敵の大きさから決めた角度の中に入っていれば、当たりです。
stare: { sec: 0.9, aimY: 1.2, halfWidth: 0.5, minDeg: 3 },
// sec: 見つめる秒数 / aimY: 狙う高さ(m) / halfWidth: 敵の体の半幅(m) / minDeg: 遠い敵でも当たる最小の角度
const lim = Math.max(degToRad(S.minDeg), Math.atan(S.halfWidth / d)); // 遠いほど、狭くなる
const r = stFwd.angleTo(stV) / lim; // 0(真ん中)〜 1(縁)
if (r < 1 && r < bestR) { best = e; bestR = r; } // 一番、中心に近い 1 体だけ
- 当たっている敵が変わったら、見つめた時間を 0 に戻します
- 外れたら、見つめた時間を、1.5 倍の速さで減らします。頭のセンサーのぶれで、すぐにはリセットされません
- 見つめている間、その敵は「見えている」ので、止まります。ほかの敵は進みます。0.9 秒の間に、背後の敵が近づく、というのが駆け引きです
- 進み具合は、
cardboard.jsの+の輪に出します。注視ボタンを見ていないときも輪が満ちるように、gaze.extra(0〜1)を足しました
// cardboard.js(変更は 2 行)
const gaze = { dwell: 2.0, enabled: true, reticleOpacity: 1, extra: 0 };
drawReticle(Math.max(p, gaze.extra || 0));
2. 押し戻した敵の行き先
押し戻した敵は、出てくるときと同じ関数(spawnEnemy)で、別の場所に戻します。
- いまの向きから 100° 以上離れた場所(背後寄り)に、半径 10〜13m で出る
- 4 回に 1 回は、向きを問わない
- 他の敵とは、1.4m 以上離す
同じ向きに戻すと、戻った先が、毎回同じ位置になってしまいます。ランダムにすると、背後の扇形に敵が集まりやすくなり、群れができます。ぽつんといる敵を押し戻して、群れの近くに戻し、連鎖を狙う、という手が成り立ちます。
3. 波紋の連鎖
押し戻した敵の元の位置に、RingGeometry を床に寝かせて広げます。波紋が届いた敵も、押し戻して、その敵の元の位置から、新しい波紋を出します。
ripple: { radius: 2.2, speed: 5, decay: 0.9, maxDepth: 8, immune: 3 },
// radius: 最大の半径(m) / speed: 広がる速さ(m/秒) / decay: 連鎖 1 段ごとに半径へ掛ける倍率
// maxDepth: 連鎖の段数の上限 / immune: 押し戻された敵が、波紋に反応しなくなる秒数
const ringGeo = new THREE.RingGeometry(0.9, 1, 64).rotateX(-Math.PI / 2); // 半径 1 のリングを、スケールで広げる
// 毎フレーム: rp.r += speed * dt; mesh.scale = rp.r; opacity = 1 - r / maxR
// 敵との距離 <= rp.r なら、その敵を押し戻す(pushBack(e, depth + 1))
- 波紋は、1 つの波紋につき、1 体を 1 回しか押し戻しません(
hitの集合) - 連鎖するたびに、半径が
decay倍になります。範囲が広すぎると、全員が消えてしまうので、調整できるようにしています - 連鎖の段数は、
maxDepthで打ち切ります
押し付け合いの修正
遠く(10〜13m)にいる敵どうしが近くにいると、押し戻した先が、お互いの波紋の中になり、波紋を押し付け合って、maxDepth まで連鎖が続きました。押し戻された敵は、3 秒間、波紋に反応しないようにしました。見つめて押し戻すほうは、免疫があっても動きます。
e.immuneUntil = rippleClock + R.immune;
// 波紋の判定: if (!e.active || rp.hit.has(e) || (e.immuneUntil || 0) > rippleClock) continue;
4. スコア
押し戻した敵 1 体が 100 点で、連鎖の段が 1 つ増えるごとに倍になります。
const pts = 100 * Math.pow(2, depth); // 見つめて押し戻した敵 = depth 0(100 点)、連鎖 1 段めの敵 = 200、2 段めの敵 = 400…
- ゲーム中は、上の表示の 2 行目に
SCORE、連鎖で増えた合計(+700など)を、1.5 秒だけ添えます - ステージ中のスコアは、面をまたいで足します
- 最高スコアは、
localStorageに保存します。ステージは、難易度ごとに別(lainlab.daruma.score.stage-<難易度>)、SURVIVAL は 1 つです(lainlab.daruma.score.survival)
5. 効果音
連鎖の段が深いほど、音階が上がる 5 つの効果音を使います(ショット命中1.mp3〜5.mp3)。
| 場面 | 音 |
|---|---|
| 見つめて押し戻した | 1 |
| 連鎖 1 段め | 2 |
| 連鎖 2 段め〜 | 3、4、5(5 段め以降は 5 のまま) |
| SURVIVAL で敵が増えた | 高速移動.mp3 |
同じフレームで、何体も連鎖すると、音が重なって大きくなります。そこで、フレームごとに、押し戻した敵の連鎖の深さの最大を覚えておいて、そのフレームの終わりに、1 音だけ鳴らします。
let pendingHit = -1;
function flushHit() { if (pendingHit >= 0) { playSe('hit' + Math.min(pendingHit + 1, 5)); pendingHit = -1; } }
// pushBack の中: pendingHit = Math.max(pendingHit, depth);
6. 難易度
OPTION で選びます。ステージの敵の数が変わります。
| 難易度 | 1 面 | 1 面ごと | 10 面 |
|---|---|---|---|
| EASY | 2 | +1 | 11 体 |
| NORMAL(既定) | 2 | +2 | 20 体 |
| HARD | 2 | +4 | 38 体 |
| EXPERT | 10 | +10 | 100 体 |
const DIFFS = {
easy: { label: 'EASY', base: 2, step: 1 },
normal: { label: 'NORMAL', base: 2, step: 2 },
hard: { label: 'HARD', base: 2, step: 4 },
expert: { label: 'EXPERT', base: 10, step: 10 },
};
const stageCount = (stage) => DIFFS[settings.diff].base + DIFFS[settings.diff].step * (stage - 1);
- 最高のステージと、ステージのスコアは、難易度ごとの
localStorageのキーに分けます。難易度を切り替えると、読み直します - 歩く速さは、難易度では変えていません
7. 歩く速さと SURVIVAL の調整
押し戻しを入れる前は、ステージで 1 面ごとに +2 体、0.9 m/秒だと、4 面(8 体)あたりで見張りきれなくなりました。見つめる操作を足したうえで、次の値にしています。
| 項目 | 値 |
|---|---|
| ステージの歩く速さ | 1 面で 0.45 倍、10 面で 0.75 倍(面ごとに、直線で上げる) |
| SURVIVAL の歩く速さ | 0.7 倍 |
| SURVIVAL の増え方 | 12 秒ごとに 2 体(0.167 体/秒。50 体まで、4 分 48 秒) |
| 先読み | 6 体先まで読み込む |
SURVIVAL は、最初は 3 秒ごとに 1 体でした。この間隔だと、スポーン音が 3 秒ごとに鳴って、うるさくなります。同じペースのまま、まとめて増やす形(every 秒ごとに wave 体)にして、音は、増えた回に 1 回だけにしました。
survival: { start: 2, every: 12, wave: 2, speed: 0.7, max: 100, strikes: 3, ahead: 6 },
const want = Math.min(settings.survMax, V.start + V.wave * Math.floor(g.elapsed / V.every));
8. 間違いの記録
- 歩く速さを下げるだけで、難しさが直ると見た: 360° から囲まれると、見張る向きを回し続けるしかなく、遊びになりませんでした。速さではなく、「見る」で対処できる手を足す必要がありました
- 使い切りのアイテム(BOMB)を入れる案: 注視の操作では、アイテムを押す間、敵を見張れません。連鎖が、プレイヤーが作る BOMB の代わりになったので、入れていません
- 連鎖の押し付け合い: 押し戻した先が、近くの敵の波紋の中になりました。押し戻した敵に、波紋への免疫を持たせて直しました
9. 制約
| 項目 | 内容 |
|---|---|
| 10 面の通し | 実機で、全難易度の 10 面まで通して遊んだ確認は、まだ |
| EXPERT | 10 面で 100 体。BENCH の実測(iPhone 13 Pro、100 体で 26fps)のとおり、端末によっては重い |
| 端末 | 実測は、iPhone 13 Pro のみ |
| 見つめる範囲 | 敵の当たりは、体の幅と、胸の高さだけの近似。頭や足だけを見ても、当たらないことがある |
| BGM | 無い |
関連
- 前の記事: [Astro] #203 スマホの VR ゴーグルで遊ぶ「だるまさんが転んだ」
- ゲームの使い方: DARUMA ガイド
- ゴーグル用アプリの一覧: /mobile