B-Teck!

お仕事からゲームまで幅広く

Claude Codeに作ってもらったゲームが面白かったので、AIに作り続けてもらう仕組みを整えた

www.youtube.com

前回のスマホでもサクッと遊べるローグライトFPSがやりたかったので作った の続きで、Sector Dive を作ったときの話。
前回の記事でも言及したように、このゲームの実装は Claude Code に任せていて、自分の手は動かしていない。
( 冒頭のトレイラーも言ったら作ってくれた PR )

やったことは、出来たものをプレイして感想を返すことと、ルールやゲームの構成、作りの方針について相談しつつ決めていくこと。
ざっくりと、どのように開発が進んでいったのかを振り返ってみる。

思いつきを動く状態で実装してもらった

ふと思いついて、以下のプロンプトをClaudeに投げてみたのが始まりだった。

コーデッドアームズみたいなランダム生成のfps
ブラウザ上で動く
武器種、ボスを数種、ステージのニュアンスを複数種で
セーブを保持して繰り返し潜れる
ルートアイテムか、持ち込み可能な強化、拠点強化の仕組みを作って繰り返しプレイ可能とか

これをスマホで操作できる3Dの単ページ完結のゲームって作れるもんなの?

「作れるよ」と返ってきたので試しに作ってもらうと、15分ほどで現在のゲームの原型が出力された。
プレイしてみると思いのほか面白く、そこからは遊んで気になったことを伝えて直してもらう、というのを繰り返した。
例えば、

  • 敵の硬さやアップグレードのバランスに「強すぎる」「物足りない」と返す
  • ボスを倒したら進むか戻るかを選べて、ロストがある、というルールを足してもらう
  • スマホ操作、全画面、ボタン配置の編集を足す

など。

ファイル分割と仕様書

フィードバックを続けて、ステージやボスの数が増えてくると、それまでの1枚の HTML に全部押し込む形では内容が追いづらくなった。指示だけで修正していると、意図しないバグやデグレが混入する。そのうえ、何が原因で壊れたのか、どう直せばよいのかも分かりづらくなってきた。
そこで、ファイルを分けて、仕様書を作ることにした。仕様書には、数値や挙動などの仕様と、該当するコードの場所を書かせている。

仕様書を起こして正しい挙動を明示する、というのはこちらの指示だけど、それを受けて Claude が勝手にスモークテストを足していたのにはびっくりした。URL に #smoke を付けて開くと、全ステージと全ボスを自動で一周して、段差で詰まらないか、部屋を行き来できるか、中断して再開できるか、などを確かめてくれる仕組み。これを変更のたびに流して、テストが落ちたら自分で原因を調べて修正もしてくれていた。

自分が遊んで見つけた不具合を伝えると、同様にスモークテストに追加されていたので、基本的に壊れることを考えずにプレイしてフィードバック、という感じで回せていた。

engine の切り出し

指示を出しながら、たまに実装を覗いたりコミットの差分を眺めたりしていたんだけど、ロジックとデータが同じファイルに配置されていたり、散らばっているものが同時に修正されているのが目につくようになってきた。
また、AIに指示を出しながら作っていく、という体験が面白かったので、別のゲームも作ってみるかもな、ということも考えていた。

そこで、以下のような変更を加えた。

  • 敵キャラやアイテムの内部データ定義をデータ用のフォルダに集める
  • Unityをイメージしてゲームのメインループと実行される関数を切り離し、コア部分を engine/ として切り出した
  • JavaScript から TypeScript と ES モジュールに移行した

engine からゲームは import しない、という向きにして、ゲーム側のデータや処理は、登録する形で engine に渡す。挙動に変更を与えずそれぞれのリファクタを行う際にも、スモークテストが通ることを確認できたので安心して進められた。

最終的には、こういう2層になっている。

人に遊んでもらう

GitHub Pagesに置いて公開し、宣伝してみたところ、何人かの人に触ってもらえた。一部のフォロワーが結構な高難度まで遊んでくれて、感想やバランスへのフィードバックをくれている。動作確認では見られていないレベルのボス戦の難易度や、強化を進めた時の挙動の不具合など。こういう報告をまたClaudeに投げ、原因を調べたり直し方を考えながら改善を進めていった。

せっかく遊んでもらえているので、実験的にこのあたりを足している。

  • 結果画面から、プレイデータを記入済みの状態で開ける感想フォーム
  • GA4 でのアクセス解析
  • テストが通らなければ公開しない仕組み
  • コミットには更新履歴用の一行を付け、更新履歴を自動生成

書き方をそろえる

最初は1つのセッションでそのまま実装を進めていたのだけど、思ったよりコードベースが大きくなってきた。そこで、計画とレビューはメインのセッションで、手を動かす作業はサブエージェントで並行して進めるようにした。
サブエージェントには Sonnet や Haiku などを使うようにしていたので、実装の質が落ちないように、仕組みの側で担保するようにした。

  • 何でも入る型や ! を減らし、型をつけるようにした
  • 決まりは STYLE.md にまとめ、機械で確かめられるものは lint にして PR の CI で止めるようにした
  • 独自の仕組みで回していたテストを Vitest に移して、1件ずつのテストとして CI で結果を見られるようにした

読みやすさと状態の管理

書き方をそろえても、コードの読みやすさはまだ低いままだった。そこで、書式とファイルの分け方、名前を整理した。

  • 書式を Prettier でそろえ、大きいファイルを役割ごとに分ける
  • 人間には理解しづらい短い変数名や略称を、意味の分かる名前にする

あわせて、ゲームの状態の持ち方も見直した。データを書き換えられる入口を狭めておけば、ゲームの状態を管理したり確認したりするコストが下がる。ステージも、データを生成する部分と、それを組み立てる部分を分けておけば、テスト用のデータを差し込んで同じステージを再現できる。

  • セーブを書き換えてよい場所を決めて、それ以外の箇所から書き換えると ESLint で落ちるようにする
  • ステージは乱数のシードから作り直せるようにして、固定のマップも差し込めるようにする

動作や書き方についても、少しずつ機械に任せる範囲が広がっていった。最初はスモークテストを流して結果を見る形だったのが、今は PR を作ると型チェック・lint・テスト・ビルドが流れて、マージするともう一度同じ確認をしてから公開される。

作ってみて

どういう物を作りたいか、という部分は自分で考えつつ、細かい部分の調査や検討は Claude に案を出してもらい、判断を下すとゲームが出来ていく。ゲーム制作をしたことのない自分の手元でここまで動くものができる、というのは今まであまり味わったことのない楽しさがあった。

流用できそうなパーツも切り出したので、新しいゲームや別ジャンルのものもまた作ってみたいと思う。

スマホでもサクッと遊べるローグライトFPSがやりたかったので作った

高校生のころ、PSP の「Coded Arms」というゲームを遊んでいた。軽いプレイ感としっかりした撃ち合いに、ローグライトな強化を組み合わせたランダム生成マップの FPS だった。
続編がもうずっと出ていないこともあって、ああいうテイストのものを久しぶりに遊びたくなり、Claude Code を使って「Sector/Dive」を作った。せっかくなのでブログでも紹介しておく。

PC とスマホのどちらでも、ブラウザで以下のリンクを開けば遊べる。

https://beatdjam.github.io/mini-games/games/sector-dive/

ゲームの流れ

ランダムに生成された区画をハンドガン一丁で潜っていくところから始まる。
区画を3つ抜けた先にはボスが待っていて、奥に進むほど敵も強くなる。

そのぶん、こちらも道中で強くなっていく。
区画の敵を全滅させるかボスを倒すとチップが落ちて、ランダムに出る候補から1枚を選べる。与ダメージや連射速度が上がるものもあれば、弾が敵を貫通したり分裂したりするものもある。

武器はハンドガン・SMG・ショットガン・レールガン・ランチャーの5種類で、★のレアリティや+値、強化オプションが付いていて、深いところほどいいものが出やすい。

ボスは6種類。体力を半分まで削ると第二段階に入り、攻撃が激しくなる。倒すと「前進」と「帰還」の2つのゲートが開き、帰還を選べば集めたビット(お金)と拾った武器を拠点に持ち帰れる。 前進すればもっと深く潜れる。途中で敗北すると手に入れたビットが半分になり、バッグの中身と持ち出した武器を失う。

ステージ(セクター)は、地形やダメージ床、出てくる敵の種類が違うものが6種類ある。 深度ごとの順番は潜るたびに入れ替わる。
BGM と効果音は音楽ファイルを使わずにブラウザの中で合成していて、曲調はセクターごとに違う。 近くの敵が動き出すと、戦闘向けの鳴り方に切り替わる。

拠点強化と周回要素

持ち帰ったビットは拠点で使う。ステータスの永続強化や初期武器の改造に回せるので、倒れても次の潜行は少しずつ楽になる。

ひと通り強くなったあとのために、周回要素の「再起動」も入れてある。DEPTH 3 以降のボスを倒すと解放され、進行状況をリセットする代わりにずっと残るボーナスがもらえる。 そのぶん、再起動するたびに敵も強くなっていく。

結果のシェア

潜行が終わると、到達した深さや倒したボス、武器、チップを1枚の画像にまとめられる。

今も少しずつ更新していて、変更点は拠点の設定タブにある更新履歴から見られる。
実装はほぼバイブコーディングで進めていて、その体験も面白かったので、別の記事にまとめる予定。

AndroidからワンタップでObsidianのDailyNoteにメモできるようにした

背景

日々の細かいメモはObsidianのDailyNoteに集約していて、入力は Thino を使っている。

PCでは常時Obsidianを起動しているので問題ないんだけど、モバイル環境で追記するときに都度アプリを起動して、Thinoの画面を開いて書き込む、という動作に若干面倒さを感じていた。

既存のスタイルに合わせて利用できるアプリやプラグインが無いかを探したが、ちょうど良いものがなかったので仕組みを作ってみることにした。

使うもの

Advanced URI

Obsidianのコミュニティプラグイン。
obsidian://advanced-uri?... という形式のURIを叩くと、ノートを開いたり作ったり追記したりを外から実行できる。

今回使うのは追記の部分。
daily=true を付けるとDailyNoteが対象になって、mode=append でファイルの末尾に追記される。URIさえ組み立てられれば、それだけでメモが残る。

MacroDroid

「トリガー(いつ)」「アクション(何を)」「条件」を組み合わせてマクロを作るAndroidの自動化アプリ。 今回は入力ダイアログを出す・文字列を置換する・URIを開く、あたりのアクションを使う。 おそらく無料版でも大丈夫なはず。

マクロの構成

実際に作成したマクロは以下

書き込みたいタイミングですぐ起動したいので、トリガーはフローティングボタンにしてみた。
常に画面上に表示されているので、どこからでもすぐ呼び出せる。

アクションは以下の順に並べた。

  1. ローカル変数を設定(文字列)… memo をユーザープロンプトで入力させる
  2. 文字列処理 … memo 内の # を %23 に全て置換
  3. 文字列処理 … memo 内の \n を %0A%09 に全て置換
  4. Webサイトを開く … 組み立てたURIを開く
  5. 変数の値を消去 … memo をクリア

実際に組み立てているURIはこれ。 普段のThinoの記録に合わせて時刻の後に改行を入れることにした。
また、ダイアログ内での改行は改行+Tabに置換することで同じ時刻のブロックに入るようにしている。

obsidian://advanced-uri?vault={{your_vault}}&daily=true&data=-%20{hour_0}:{minute}:{second}%20%0A%09{lv=memo}&mode=append

{{your_vault}} は自分のVault名に読み替える。{hour_0} {minute} {second} はMacroDroidの組み込み変数で、{lv=memo} が入力したテキストになる。

つまったところ

1. # がセクション指定として扱われる

# を含むメモを送信したところ、DailyNoteの中身が空になり、入力した文字列だけが書き込まれるという壊れ方をした。

細かい挙動までは追っていないけど、URIにおける # はフラグメントの区切りだし、Advanced URI自身も見出し指定にこの記法を使う。
そのあたりが噛み合っていないんだと思う。

原因はさておき既存の記録が消えるのは困るので、URIに乗せる前に # を %23 にエスケープしておくことにした。

URIの区切り文字(& や =)も同じように壊れそうだけど、メモにはあんまり書かないので今のところ # だけ対応している。困ったら同じ要領で置換を足せばいいかな、くらいの気持ち。

2. 改行とインデントを入れる

Thinoの表示に合わせて、時刻の後で改行してから本文をインデントさせたかった。

- 19:47:39
    改行
    テスト

やることは、改行とタブをURLエンコードした %0A%09 を時刻の直後に置くだけ。
これで時刻の次の行に、インデント付きで本文が始まる。

ダイアログで複数行打ったとき用に、本文中の改行もアクション3で同じ %0A%09 に置き換えている。これで2行目以降も同じ深さに揃う。

インデントはThinoの表示に合わせてタブ(%09)にした。

インテント送信ではなく「Webサイトを開く」にした

MacroDroidには「インテントを送信」というアクションもあって、android.intent.action.VIEW でカスタムスキームを投げられる。こっちでも試したけど、最終的には「Webサイトを開く」に落ち着いた。

「Webサイトを開く」でも obsidian:// はそのままOSのスキーム解決に回るので普通に動く。
設定項目が少ないぶん、こっちのほうが楽だった。

使ってみて

フローティングボタンを押すと、こんなダイアログが出る。

打ってOKを押すと、DailyNoteに追記されてThinoにこう並ぶ。

書き込む際にはObsidianが起動するので、若干の待ち時間が発生するものの、入力自体を待たされるわけではないのでだいぶ体験として良くなった。
また、副産物的なものとして、Thinoの画面に移動しなくてもそのままどんどん書き込めて便利。

関連記事

Obsidianとモバイルの共存は過去にもこのあたりで触れている