Underground 2 Revived
Play Need for Speed: Underground 2 (2004) in Chrome and Firefox with WebGPU graphics, Web Audio, up to 120 fps widescreen, controller support, free roam and Online races with friends. Fan project, not
翻译准备中
尚未检测到更新。GitHub 发布版本后会显示在这里。
个人 GitHub 账号下公开仓库的所有者可以登录并验证所有权,自行管理介绍,无需管理员批准。
登录并生成验证文件,在 30 分钟内将其提交到此仓库的默认分支。其他分支或分叉仓库中的文件无效。然后在下方检查文件,即可无需管理员批准直接管理项目。请保留文件以维持管理权限。
如需登录,将前往登录页面。登录后请回到此处,再次点击所选的验证按钮。
在项目文件夹中运行 /nomorevibe verify,即可验证所有权并自行管理介绍。
一分钟完成验证正在加载完整 README。 以下为已保存的摘录。
就活の「お祈りメール」やライブの落選メールでモヤっとした気持ちを、ゲームでスカッと発散するアプリです。 • 就活のお祈りメールをもらってモヤっとしている学生 • ライブの抽選に落選してモヤっとしている学生 • タイトル画面: @ちゃんぐー_川口雄矢 • ゲーム画面: @こうた_奥川広大, @いぬぽん(メンター)查看完整 README ↗
就活の「お祈りメール」やライブの落選メールでモヤっとした気持ちを、ゲームでスカッと発散するアプリです。
モバイルブラウザ
TITLE → 「スタート」 → MAIL_INPUT → 「ゲーム開始」 → GAME
src/scenes/MailInputScene.js がメール本文入力画面。ブロックの生成は一切行わず、
入力文字列をトリムしたものを onStartGame(mailText) で渡すだけに徹している。src/main.js の App.goTo() が onStartGame の戻り先として
GameScene.setMailText(mailText) を呼んでから mount() する
(ResultScene.setResult() と同じ「mount前にsetterで値を渡す」パターン)。GameScene._setupWall() が実際のブロック生成場所(空中の棒の上に積む)。
積む行は _buildRows() が返す「行(文字配列)の配列」に一本化してあり、
this.mailText が未設定(null)の場合(「遊び方」から直接ゲームへ進んだ
場合など)もランダムな文面を同じ手順で行分解する。つまり「1文字=1ブロック」の
経路は常に1本だけ。GameScene._splitIntoRows() が行の組み方を1箇所に集約している。
MAX_ROWS(現在6行)を超えて積み上がりそうな
文字数になったら、行数を増やさず折り返し幅(列数)だけを増やして横に広げる。Array.from() を使う。Block)で埋めている。行ごとに実際の文字数で中央寄せすると、
短い行の上に長い行が乗ったときに支えのないオーバーハングができて自重で
崩れてしまうため、必ず全行同じ列数・左詰めで配置している。MAX_MAIL_BLOCKS(現在100文字。MailInputScene側のMAX_MAIL_LENGTHと
揃えてある)を超える入力は先頭から切り詰めている(空白ブロックの分は
この上限に含めない)。row=0が最初の行)だが、
壁は下から積み上がるので、そのままだと最初の行が最下段になって上から下に
読むと逆順になってしまう。_spawnBlock() で高さを反転している。src/scenes/GameScene.js, src/game/Bar.js)Bar(幅8m・高さ0.3m・
奥行き1.2mの直方体、src/game/Bar.js)の上に積んである。棒はmass: 0の
完全固定ボディで、球が当たっても棒自体は動かない。棒が落ちる設計にすると、
乗っている壁が一撃で全部落ちて8球という球数設計が壊れるため。Blockにあった耐久値・破壊判定(HIT_DURABILITYなど)は撤去した。
ブロックは衝突では絶対に壊れず、棒の上面より1m下(CRUSH_FALL_Y)まで
落ちたら「粉砕された」扱いにしてfallingBlocksに移す。このゲームに
スコアは無く、代わりにGameScene._markCrushed()がそのブロックの
sourceIndex(元のメール本文での文字位置)をcrushedIndicesに記録する。
記録は1回きりで、その後はGameScene._resolveLandedBlocks()が回収するだけの
存在になる。爆弾が爆発したとき(_resolveBombs())も同様に記録する。FLOOR_Y = -20)に
当たり判定だけの床を敷き、そこに着いた時点でブロックを破棄している。
カメラ位置(0,9,28)・フォグ(35〜50)から計算すると、画面の一番下の視線が
y=-20に届くのはカメラから59.7m先(フォグの上限50mより外)なので、
この床は画面のどこからも絶対に見えない。GameScene._setupBar() が new Bar(..., this.wallWidth) を呼ぶ)。
短いと端のブロックが棒に乗らず開始直後に落ち、長いと端に何も乗っていない
余りが見えてしまう。文字数で壁の幅が変わるため、BAR_SIZE.x は既定値の
扱いになっている。GameScene._applyCameraFraming())。縦画面のように横方向の視野が狭い
ときは横幅基準の距離が、横長画面では高さ基準の距離が効く。画面回転時
(_onResize)にも引き直す。CAMERA_BASE_POSITION(0,9,28) /
CAMERA_BASE_LOOK_AT(0,7,0) からは向きと最短距離だけを取り出しており、
24文字のときはこの最短距離が下限として効くので従来の見え方と変わらない。
影の範囲とフォグもカメラ距離に追従させている。LAUNCH_ORIGIN は壁からの距離(z=25m)なので、カメラを引いても球の届く
距離には影響しない。ただし AimController の仰角レンジは壁の高さを前提に
実測した値なので、MAX_ROWS を増やして壁を高くしたら測り直しが必要(下記)。文字数が増えると列が増え、壁が横に広がり、収めるためにカメラが下がる。 その結果ブロックの文字は小さくなる。iPhone 14(390×844)の縦画面での実測値:
| 文字数 | 列 × 行 | 壁の幅 | カメラ距離 | 画面上の文字の高さ |
|---|---|---|---|---|
| 24 | 5 × 5 | 8.0m | 28.1m | 20.3px |
| 40 | 7 × 6 | 11.2m | 33.8m | 16.9px |
| 60 | 10 × 6 | 16.0m | 48.3m | 11.8px |
| 100 | 17 × 6 | 27.2m | 82.1m | 6.9px |
上限いっぱいの100文字では文字がほぼ読めない。 読みやすさを優先するなら
MAX_MAIL_BLOCKS を下げる(MailInputScene の MAX_MAIL_LENGTH も一緒に)。
20px を確保できるのは40文字弱まで。なお「文字数に応じてブロック自体を縮小する」
案は過去に試して、カメラを引いた分と相殺してかえって読みにくくなることを
実機確認済み。
src/game/AimController.js)MAX_ROWS = 6
なので文字数によっては6行(頂点 y≒11.9)になり、壁が約1m高くなる。28度は
もともと壁の頂点よりやや高く着弾する設定なので届く見込みだが、
実機で最上段に当たるか確認したい。本文に GameScene.BOMB_WORDS の語が含まれていると、その語を構成する文字の
ブロックが爆弾になる。語を増やすときはこの配列に足すだけでよい。1文字だけの
語も指定できる。現在の内訳は次の通り。
| 系統 | 語 |
|---|---|
| 抽選・チケット | 抽選 当選 落選 外れ 用意 希望 |
| 選考・就活 | 選考 不採用 見送 内定 残念 期待 厳正 慎重 検討 |
| 単体でも落選を連想させる字 | 縁 祈 選 残 |
単漢字ではなく語で持たせているのは、「抽選」「見送」のように隣り合うブロックが まとまって赤くなったほうが、何が爆弾なのかを目で読み取れるため。「結果」のように それ自体は中立な語は、手がかりが薄れるので入れていない。
語どうしは範囲が重なってよい。検出はフラグを立てるだけなので、'選' と '抽選'
の両方に当たっても二重には効かない。重ねて持つ意味はあって、たとえば '抽' が
爆弾になるのは「抽選」と続いたときだけになる。
同梱の EMAIL_TEXTS 10文面では全体の約18%が爆弾になり、どの文面にも必ず
1個以上の爆弾が出る。
Block.js の BOMB_* 系の配色)。文字はそのまま読める。collide を監視して Block.hitByBall を立てる
(BOMB_TRIGGER_SPEED = 1.5 未満のかすった接触は無視する)。
GameScene._resolveBombs() が毎フレームこれを見て爆発させる。GameScene._explode() が爆心から半径 EXPLOSION_RADIUS 以内の動的なボディ
(ブロックと飛んでいる球の両方)を外向きに押す。力は距離に応じて線形に弱まり、
外周でゼロになる。遮蔽は見ていない。EXPLOSION_RADIUS(どこまで巻き込むか)と EXPLOSION_IMPULSE
(どれだけ飛ばすか)の2つ。ブロックの質量は 1.5 なので、現在の力積 30 は
爆心でおよそ 20 m/s の初速にあたる。半径 6.0m は MAX_ROWS = 6 の壁の高さを
ちょうど覆う大きさ。検出は GameScene._markBombs() が担当し、行に分割し終えた文字を平坦に並べ直して
から語を探している。_splitIntoRows() は改行と自動折り返しで行を切るため、
語の途中で行が変わることがあるが、この方式なら両方の文字を拾える。
その場合、離れた位置の2ブロックがそれぞれ独立した爆弾になる。
「厳正なる選考の結果 / 今回はご期待に添えず」(19文字・折り返し幅5)
行1: [考] の 結 果 □ ← 「考」だけ
行0: 厳 正 な る [選] ← 「選」だけ
元の本文で語の間にスペースが入っていた場合も同じ扱いになる(空白は行分割の
時点で除去済みのため)。なお結合した文字列への indexOf ではなく要素単位で
比較している。サロゲートペア(絵文字など)があると文字列上の位置と要素の
添字がずれるため。
引き金は球との衝突だけなので、爆風が他の爆弾を誘爆させることはない。 吹き飛ばされたブロックが爆弾にぶつかっても何も起きない。
ただし爆風は球も吹き飛ばすので、弾かれた球が別の爆弾に当たれば、そちらは 普通に爆発する。連鎖らしきものが起きるとすればこの経路だけ。
src/scenes/ResultScene.js)スコアの代わりに、入力したメール本文そのものが粉砕された姿を見せる画面。 「100点しか取れなかった=下手だった」という新しいモヤモヤを生まないよう、 数値は出しても評価ではなく戦果(例:「87文字を粉砕した」)として提示する。
GameScene._splitIntoRows() が返すpositionsはrowsと同じ形の配列で、
各セルに元のメール本文(Array.from基準、サロゲートペア対応)での
文字位置を持つ。行を揃えるための空白パディングのセルは位置を持たずnull
になる。「空白除去後の配列でのインデックス」ではなく「元の本文での位置」
を持たせている点が肝で、そうしないとリザルト側で本文を復元できない。GameScene._spawnBlock()経由でBlock.sourceIndexとして
ブロック自身に持たせる。棒から取り除かれた瞬間(_resolveFallenBlocks()
で落ちた時/_resolveBombs()で爆発した時)にGameScene._markCrushed()が
sourceIndexをcrushedIndices(Set)へ記録する。sourceIndexがnullの
パディングブロックは記録されない=粉砕数に数えない。GameScene._checkGameOver())、実際に壁として採用した本文
(resolvedMailText。入力が空でランダム文面にフォールバックした場合は
そちらの文面)とcrushedIndices、粉砕対象になり得た総文字数
(totalCrushableChars)、後述の背景スナップショットをonGameOver()経由で
渡す。main.jsのApp.goTo()がこれをResultScene.setResult()へ橋渡しする
(GameScene.setMailText()と同じ「mount前にsetterで値を渡す」パターン)。ResultScene._renderMail()が本文を1文字ずつspanに分解して描画する。
改行はGameScene._splitIntoRows()と同じ規則(\r\nはまとめて1つ、単独の
\r・\nもそれぞれ1つ)で<br>に変換し、見た目の改行を本文どおりに保つ。
空白文字はそもそも棒のブロックにならない(粉砕も残留もしない)ので、
素通しして幅だけ確保する。
crushedIndicesに含まれる文字=粉砕済み。もやから切り離された1片として、
はっきり読めるチップの背景に載せる(is-crushed)。is-intact)。is-completeを
付け、見出しを「完全粉砕!」に変え、パネルをオレンジのグローで
脈打たせる特別演出にする。transformとopacity中心にし、最大100文字ぶんのspanが
同時に動いても軽いままにしている。--haze-delay(CSS変数)で、まだ靄が
残っている文字ごとにanimation-delayを少しずつ振ることで、一斉にではなく
「じわじわ靄がかかっていく」見た目にしている。.result-panel自体をビューポート高の90%までに抑え、本文の.result-mail
だけを内側でoverflow-y: autoにしてスクロールさせる。ボタンが画面外に
出ないことをiPhone相当の390×844の画面幅で確認済み。GameSceneは
シーンが破棄される前に一度だけthis.renderer.render()を呼び直してから
this.canvas.toDataURL('image/jpeg', 0.85)でスナップショットを撮り、
backgroundImageとして渡す(rendererにpreserveDrawingBufferを立てて
いないため、直前に描画し直さないと撮る時点で描画バッファが失われている
ことがある)。はこれをのに
設定し、下地の単色は文字のコントラストを保つための薄い暗色()にとどめている。パネル自体は別途不透明度のある背景を持つため、
本文の可読性はそちらで確保している。ブロックに書かれた文字は DOM のオーバーレイではなく、CanvasTexture として
ブロックの面そのものに焼き込んでいる(src/game/Block.js の
createCharacterTexture())。文字がメッシュの一部になるため、ブロックが崩れて
回転しても文字が追従し、手前のブロックが奥の文字を正しく隠す。
BoxGeometry のマテリアル配列は [+X, -X, +Y, -Y, +Z, -Z] の順。GameScene の characterTextures で使い回している。
ブロック間で共有しているので Block.dispose() では破棄せず、
GameScene.unmount() でまとめて破棄する。タイトル画面の背景(src/game/TitleBackground.js)のブロックも、同じ見た目を
Block.js から借りている。ブロックの見た目を変えるときに触る場所は Block.js の
次の3つで、ここを直せばゲーム画面と背景の両方に反映される。
| 公開しているもの | 内容 |
|---|---|
BLOCK_SIZE | ブロックの実寸(1.4 x 0.9 x 1.0) |
createCharacterTexture() | 1文字を焼き込んだ CanvasTexture |
createBlockMaterials() | 6面ぶんのマテリアル(文字面2枚+断面4枚) |
一方で Block クラスそのものは背景では使っていない。爆弾かどうか
(isBomb / hitByBall)というゲーム固有の責務を持っており、背景には不要なため。
背景側は mesh と body を自前で組み立てている。テクスチャのキャッシュも
TitleBackground が自前の characterTextures で持ち、unmount() で破棄する。
爆風の強さ(EXPLOSION_RADIUS / EXPLOSION_IMPULSE)は計算で置いた初期値で、
実機で詰め切れていない。弱ければブロックがほとんど動かず、強すぎるとタワーが
一撃で吹き飛ぶ。まずここを触るとよい。
ゲーム画面の背景は濃紺(0x1a1d2e)、タイトル画面の背景は青空と芝生で、
世界観がつながっていない。ブロック自体の見た目は揃えたが、周囲の景色は未対応。
TitleBackground の変数名は bricks / _spawnBrick / MAX_BRICKS のように
レンガ時代の名前が残っている。中身はゲーム画面と同じ紙ブロックなので、
いずれ block 系の名前に揃えるとよい(今回は diff を膨らませないため見送り)。
背景のブロックは質量 1 のまま寸法だけゲーム側(1.4 x 0.9 x 1.0)に合わせたので、
以前より密度が下がっている。鉄球に当たったときの吹き飛び方が軽く感じるなら
TitleBackground._spawnBrick() の mass を調整する。
HOWTO(遊び方)からゲームへ直行する導線は残っており、その場合はメール本文が
ないのでランダム文面が積まれる。HOWTO → MAIL_INPUT に変える案は未着手。
MAX_MAIL_BLOCKS(100文字)は読みやすさの面で攻めすぎている可能性が高い。
上の「文字数と読みやすさのトレードオフ」の表のとおり、100文字だと画面上の
文字が約6.9pxになる。「もらったメールを貼り付ける」という売りと、
「文字が読める」ことの、どちらを取るかの判断が要る。40文字前後が妥協点。
壁が6行になったときに AimController の仰角レンジ(14度〜28度)で
最上段まで届くか、実機で未確認。
球数(TOTAL_BALLS = 8)は24文字・5行の壁を前提に決めた値で、文字数上限を
100に戻したぶん壁が大きくなっている。実際のメール文長の分布を見ながら
再調整すると良さそう。
このリポジトリには Claude Code 用のスキルを同梱しています。clone すればそのまま使えます(追加インストール不要)。
/grill-me — 実装に入る前に、計画や設計を Claude に徹底的に問い詰めてもらいます。仕様が固まっていない機能に着手する前や、実装方針で迷ったときに使ってください。詳細は .claude/skills/README.md を参照。
ResultScene.result-screenbackground-imagergba(10, 12, 24, 0.35)