<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>ランタイム on Codigrowa Docs</title><link>https://codigrowa.batako.net/docs/runtime/</link><description>Recent content in ランタイム on Codigrowa Docs</description><generator>Hugo</generator><language>ja-jp</language><atom:link href="https://codigrowa.batako.net/docs/runtime/index.xml" rel="self" type="application/rss+xml"/><item><title>アーキテクチャ概観</title><link>https://codigrowa.batako.net/docs/runtime/architecture/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://codigrowa.batako.net/docs/runtime/architecture/</guid><description>&lt;p>Codigrowa のゲームエンジンは &lt;strong>GameLoop → Scheduler → World → Entity → Renderer&lt;/strong> という鎖で構成されています。Expo（React Native）上では &lt;code>requestAnimationFrame&lt;/code> を起点に時間が進み、React 側のコンポーネントは &lt;code>Game.onRender()&lt;/code> から受け取るスナップショットだけで描画を完結させます。&lt;/p>
&lt;pre tabindex="0">&lt;code>useGameLoop (engine/hooks/useGameLoop.ts)
 │
 ▼
┌──────────────┐
│ Game │ ← ランタイム中枢
│ • Scheduler │ ← 関数単位 tick
│ • World │ ← Entity 単位 tick
└──────────────┘
 │
 ▼
Canvas Renderer (components/game-canvas.tsx)&lt;/code>&lt;/pre>&lt;h3 id="データフロー">データフロー&lt;a class="anchor" href="#%e3%83%87%e3%83%bc%e3%82%bf%e3%83%95%e3%83%ad%e3%83%bc">#&lt;/a>&lt;/h3>
&lt;ol>
&lt;li>&lt;code>useGameLoop&lt;/code> が &lt;code>requestAnimationFrame&lt;/code> から &lt;code>dt&lt;/code>（秒）を算出し &lt;code>Game.update(dt)&lt;/code> を連続呼び出し。&lt;/li>
&lt;li>&lt;code>Game.update&lt;/code> はまず &lt;code>Scheduler.update(dt)&lt;/code> を処理し、待機中のコールバックを実行。&lt;/li>
&lt;li>続いて &lt;code>World.process(dt)&lt;/code> が全 &lt;code>Entity&lt;/code> の &lt;code>process()&lt;/code> を tick に合わせて回す。&lt;/li>
&lt;li>&lt;code>Game.notifyRender()&lt;/code> が World からのスナップショットをまとめ、&lt;code>onRender&lt;/code> 購読者（HUD や Skia Canvas）へ配布。&lt;/li>
&lt;/ol>
&lt;p>描画層は &lt;code>GameRenderPayload&lt;/code> を受け取るだけで済むため、World/Entity の内部状態を UI から直接 mutate しません。これにより、レンダリングとロジックの責務分離とデバッグ容易性を両立しています。&lt;/p></description></item><item><title>Game クラス</title><link>https://codigrowa.batako.net/docs/runtime/game/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://codigrowa.batako.net/docs/runtime/game/</guid><description>&lt;p>&lt;code>engine/Game.ts&lt;/code> はランタイムの司令塔です。World と Scheduler を束ね、1 tick ごとに状態更新と描画用スナップショット生成を行います。&lt;/p>
&lt;h2 id="ライフサイクル-api">ライフサイクル API&lt;a class="anchor" href="#%e3%83%a9%e3%82%a4%e3%83%95%e3%82%b5%e3%82%a4%e3%82%af%e3%83%ab-api">#&lt;/a>&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>メソッド&lt;/th>
 &lt;th>用途&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;code>start({ reset?: boolean })&lt;/code>&lt;/td>
 &lt;td>GameLoop 起動。&lt;code>reset&lt;/code> で &lt;code>elapsed&lt;/code> / &lt;code>tick&lt;/code> を初期化し即スナップショット配布。&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>pause()&lt;/code> / &lt;code>resume()&lt;/code>&lt;/td>
 &lt;td>&lt;code>update()&lt;/code> の実行を一時停止／再開。Zustand ストアから UI でトグル。&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>stop()&lt;/code>&lt;/td>
 &lt;td>ループ停止 + Scheduler タスク破棄。&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>update(dt: number)&lt;/code>&lt;/td>
 &lt;td>&lt;code>Scheduler.update(dt)&lt;/code> → &lt;code>World.process(dt)&lt;/code> → &lt;code>notifyRender()&lt;/code> の順で実行。&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;code>useGameLoop&lt;/code> から渡される &lt;code>dt&lt;/code>（秒）だけを頼りに Game は決定的に進行します。React 側で &lt;code>Game&lt;/code> や &lt;code>World&lt;/code> を直接触る場合は、状態破壊を避けるためなるべく API 経由（&lt;code>addEntity&lt;/code>, &lt;code>wait&lt;/code> など）で操作してください。&lt;/p>
&lt;h2 id="world--scheduler-依存差し替え">World / Scheduler 依存差し替え&lt;a class="anchor" href="#world--scheduler-%e4%be%9d%e5%ad%98%e5%b7%ae%e3%81%97%e6%9b%bf%e3%81%88">#&lt;/a>&lt;/h2>
&lt;p>コンストラクタでは &lt;code>new Game({ world, scheduler })&lt;/code> としてテスト用のモックを注入できます。既存インスタンスを入れ替える場合は &lt;code>setWorld()&lt;/code> / &lt;code>setScheduler()&lt;/code> を利用します。&lt;/p></description></item><item><title>World &amp; Entity</title><link>https://codigrowa.batako.net/docs/runtime/world-entity/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://codigrowa.batako.net/docs/runtime/world-entity/</guid><description>&lt;p>&lt;code>engine/World.ts&lt;/code> と &lt;code>engine/entities/Entity.ts&lt;/code> はエンジンの心臓部です。World が Entity を束ね、Entity は tickRate ごとに &lt;code>_process()&lt;/code> を実行します。&lt;/p>
&lt;h2 id="world-の責務">World の責務&lt;a class="anchor" href="#world-%e3%81%ae%e8%b2%ac%e5%8b%99">#&lt;/a>&lt;/h2>
&lt;ul>
&lt;li>&lt;code>addEntity(entity)&lt;/code> / &lt;code>removeEntity(id)&lt;/code> / &lt;code>clear()&lt;/code> で登録を管理。&lt;/li>
&lt;li>&lt;code>process(dt)&lt;/code> で全 Entity の &lt;code>process(dt)&lt;/code> を呼び出し。&lt;/li>
&lt;li>&lt;code>getSnapshots()&lt;/code> が描画層へ渡す &lt;code>EntitySnapshot[]&lt;/code> を生成。&lt;/li>
&lt;/ul>
&lt;p>World は順序保証のため &lt;code>Map&lt;/code> を使っています。Entity 追加時に &lt;code>_onEnterWorld()&lt;/code> が走り、&lt;code>_ready()&lt;/code> が 1 度だけ実行されます。除去時は &lt;code>_onExitWorld()&lt;/code> で &lt;code>_ready()&lt;/code> の再実行を許可します。&lt;/p>
&lt;h2 id="entity-抽象クラス">Entity 抽象クラス&lt;a class="anchor" href="#entity-%e6%8a%bd%e8%b1%a1%e3%82%af%e3%83%a9%e3%82%b9">#&lt;/a>&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-ts" data-lang="ts">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">abstract&lt;/span> &lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">Entity&lt;/span>&amp;lt;&lt;span style="color:#f92672">TSnapshot&lt;/span> &lt;span style="color:#a6e22e">extends&lt;/span> &lt;span style="color:#a6e22e">EntitySnapshot&lt;/span>&amp;gt; {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">tickRate&lt;/span>: &lt;span style="color:#66d9ef">number&lt;/span>; &lt;span style="color:#75715e">// デフォルト 1/60 秒
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> &lt;span style="color:#66d9ef">private&lt;/span> &lt;span style="color:#a6e22e">tickTimer&lt;/span> &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>; &lt;span style="color:#75715e">// 経過時間累積
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">process&lt;/span>(&lt;span style="color:#a6e22e">dt&lt;/span>: &lt;span style="color:#66d9ef">number&lt;/span>) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">this&lt;/span>.&lt;span style="color:#a6e22e">tickTimer&lt;/span> &lt;span style="color:#f92672">+=&lt;/span> &lt;span style="color:#a6e22e">dt&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">while&lt;/span> (&lt;span style="color:#66d9ef">this&lt;/span>.&lt;span style="color:#a6e22e">tickTimer&lt;/span> &lt;span style="color:#f92672">&amp;gt;=&lt;/span> &lt;span style="color:#66d9ef">this&lt;/span>.&lt;span style="color:#a6e22e">tickRate&lt;/span>) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">this&lt;/span>.&lt;span style="color:#a6e22e">tickTimer&lt;/span> &lt;span style="color:#f92672">-=&lt;/span> &lt;span style="color:#66d9ef">this&lt;/span>.&lt;span style="color:#a6e22e">tickRate&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">this&lt;/span>.&lt;span style="color:#a6e22e">_process&lt;/span>(&lt;span style="color:#66d9ef">this&lt;/span>.&lt;span style="color:#a6e22e">tickRate&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">protected&lt;/span> &lt;span style="color:#66d9ef">abstract&lt;/span> &lt;span style="color:#a6e22e">_process&lt;/span>(&lt;span style="color:#a6e22e">elapsed&lt;/span>: &lt;span style="color:#66d9ef">number&lt;/span>)&lt;span style="color:#f92672">:&lt;/span> &lt;span style="color:#66d9ef">void&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">abstract&lt;/span> &lt;span style="color:#a6e22e">getSnapshot&lt;/span>()&lt;span style="color:#f92672">:&lt;/span> &lt;span style="color:#a6e22e">TSnapshot&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="tickrate-と決定性">tickRate と決定性&lt;a class="anchor" href="#tickrate-%e3%81%a8%e6%b1%ba%e5%ae%9a%e6%80%a7">#&lt;/a>&lt;/h3>
&lt;p>&lt;code>tickRate&lt;/code> は Entity ごとの更新周期です。World は &lt;code>dt&lt;/code> をそのまま流すのではなく、&lt;code>tickTimer&lt;/code> に蓄積した上で tickRate を超えた回数だけ &lt;code>_process()&lt;/code> を実行します。これにより、RAF のフレームレートが揺らいでもロジックは固定周期で進行します。&lt;/p></description></item><item><title>Scheduler</title><link>https://codigrowa.batako.net/docs/runtime/scheduler/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://codigrowa.batako.net/docs/runtime/scheduler/</guid><description>&lt;p>&lt;code>engine/Scheduler.ts&lt;/code> は時間ベースの関数実行を担当します。GameLoop の &lt;code>dt&lt;/code> を受け取り、内部に積んだタスクの &lt;code>remaining&lt;/code> 時間を減算し、0 を下回ったタイミングで実行します。&lt;/p>
&lt;h2 id="タスクモデル">タスクモデル&lt;a class="anchor" href="#%e3%82%bf%e3%82%b9%e3%82%af%e3%83%a2%e3%83%87%e3%83%ab">#&lt;/a>&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-ts" data-lang="ts">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">type&lt;/span> &lt;span style="color:#a6e22e">SchedulerTask&lt;/span> &lt;span style="color:#f92672">=&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">id&lt;/span>: &lt;span style="color:#66d9ef">string&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">callback&lt;/span>&lt;span style="color:#f92672">:&lt;/span> () &lt;span style="color:#f92672">=&amp;gt;&lt;/span> &lt;span style="color:#66d9ef">void&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">remaining&lt;/span>: &lt;span style="color:#66d9ef">number&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">interval&lt;/span>: &lt;span style="color:#66d9ef">number&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">repeat&lt;/span>: &lt;span style="color:#66d9ef">boolean&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;ul>
&lt;li>&lt;code>wait(delay, callback)&lt;/code> は一度だけ実行するタスクを登録。&lt;/li>
&lt;li>&lt;code>schedule(callback, interval, { repeat })&lt;/code> はデフォルトで繰り返し（&lt;code>repeat: true&lt;/code>）。&lt;/li>
&lt;li>&lt;code>cancel(id)&lt;/code> / &lt;code>clear()&lt;/code> で後始末。&lt;/li>
&lt;/ul>
&lt;p>&lt;code>Game.update&lt;/code> が Scheduler を World より先に呼び出すため、タスク内で Entity の状態を変更しても同じ tick 内で描画に反映されます。&lt;/p>
&lt;h2 id="使い所">使い所&lt;a class="anchor" href="#%e4%bd%bf%e3%81%84%e6%89%80">#&lt;/a>&lt;/h2>
&lt;ul>
&lt;li>HUD へ定期的にログを出す（&lt;code>components/game-canvas.tsx&lt;/code> 内参照）。&lt;/li>
&lt;li>Cutscene のように「数秒待ってから次アクション」を表現する。&lt;/li>
&lt;li>Entity 内から &lt;code>this.game.wait()&lt;/code> を呼びたい場合は依存注入で Game インスタンスを受け取り、&lt;code>_ready()&lt;/code> でタスク登録・ &lt;code>_onExitWorld()&lt;/code> で &lt;code>cancelTask()&lt;/code> する。&lt;/li>
&lt;/ul>
&lt;h2 id="エラーハンドリング">エラーハンドリング&lt;a class="anchor" href="#%e3%82%a8%e3%83%a9%e3%83%bc%e3%83%8f%e3%83%b3%e3%83%89%e3%83%aa%e3%83%b3%e3%82%b0">#&lt;/a>&lt;/h2>
&lt;p>タスク実行中に例外が起こっても Scheduler 自身は落ちません。&lt;code>console.warn()&lt;/code> を出した上で、単発タスクは削除、繰り返しタスクは次のインターバルへ進みます。長時間走り続けるサービスを意識し、repeat タスクでは必ず &lt;code>cancel()&lt;/code> のタイミングを決めておきましょう。&lt;/p>
&lt;h2 id="パフォーマンスのコツ">パフォーマンスのコツ&lt;a class="anchor" href="#%e3%83%91%e3%83%95%e3%82%a9%e3%83%bc%e3%83%9e%e3%83%b3%e3%82%b9%e3%81%ae%e3%82%b3%e3%83%84">#&lt;/a>&lt;/h2>
&lt;ul>
&lt;li>タスク数は &lt;code>Map&lt;/code> で管理しているため数百件程度までなら問題なし。大量登録が必要なら &lt;code>getTaskCount()&lt;/code> を監視し、デバッグ HUD から過剰登録を検知する仕組みを追加すると安全です。&lt;/li>
&lt;li>&lt;code>update(dt)&lt;/code> はタスクが無い場合 O(1) で即 return するため、必要なタイミングでこまめに &lt;code>cancel()&lt;/code> しておくと無駄なループを避けられます。&lt;/li>
&lt;/ul></description></item><item><title>GameLoop と状態共有</title><link>https://codigrowa.batako.net/docs/runtime/game-loop/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://codigrowa.batako.net/docs/runtime/game-loop/</guid><description>&lt;p>React 側では &lt;code>engine/hooks/useGameLoop.ts&lt;/code> と &lt;code>engine/stores/useGameStore.ts&lt;/code> がゲームエンジンを UI に接続します。&lt;/p>
&lt;h2 id="usegameloop-フック">useGameLoop フック&lt;a class="anchor" href="#usegameloop-%e3%83%95%e3%83%83%e3%82%af">#&lt;/a>&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-ts" data-lang="ts">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a6e22e">useGameLoop&lt;/span>((&lt;span style="color:#a6e22e">dt&lt;/span>) &lt;span style="color:#f92672">=&amp;gt;&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">game&lt;/span>.&lt;span style="color:#a6e22e">update&lt;/span>(&lt;span style="color:#a6e22e">dt&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}, &lt;span style="color:#a6e22e">isRunning&lt;/span> &lt;span style="color:#f92672">&amp;amp;&amp;amp;&lt;/span> &lt;span style="color:#a6e22e">game&lt;/span>.&lt;span style="color:#a6e22e">isRunning&lt;/span>);&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;ul>
&lt;li>&lt;code>requestAnimationFrame&lt;/code> の &lt;code>time&lt;/code> から &lt;code>dt&lt;/code>（秒）を算出。&lt;/li>
&lt;li>&lt;code>isRunning&lt;/code> が &lt;code>false&lt;/code> のときは RAF を解除し、&lt;code>lastTimeRef&lt;/code> をリセット。&lt;/li>
&lt;li>ループ中に最新のコールバック参照を使うため &lt;code>useRef&lt;/code> で callback を保持。&lt;/li>
&lt;/ul>
&lt;p>必要に応じて UI 側から任意の &lt;code>Game&lt;/code> インスタンスを渡せます。Expo DevTools のホットリロードでも問題なく再初期化されるよう、&lt;code>useEffect&lt;/code> のクリーンアップで &lt;code>cancelAnimationFrame&lt;/code> を確実に呼んでいます。&lt;/p>
&lt;h2 id="zustand-ストア">Zustand ストア&lt;a class="anchor" href="#zustand-%e3%82%b9%e3%83%88%e3%82%a2">#&lt;/a>&lt;/h2>
&lt;p>&lt;code>engine/stores/useGameStore.ts&lt;/code> は &lt;code>Game&lt;/code> を 1 つだけ生成し、UI からは &lt;code>start/pause/resume/stop&lt;/code> をディスパッチできるようにしています。&lt;/p>
&lt;ul>
&lt;li>&lt;code>setRenderPayload(payload)&lt;/code> が &lt;code>Game.onRender&lt;/code> から届いたデータを &lt;code>tick/elapsed/entities&lt;/code> に反映。&lt;/li>
&lt;li>&lt;code>toggleRunning()&lt;/code> で UI トグルスイッチと同期。&lt;/li>
&lt;li>ストアはクライアントサイドのみで使うため、サーバーレンダリングの心配は不要。&lt;/li>
&lt;/ul>
&lt;p>&lt;code>GameCanvas&lt;/code> 以外の画面（HUD、デバッグパネル、外部ツール）でも同じストアを購読すれば、最新の World スナップショットを共有できます。&lt;/p></description></item></channel></rss>