
2026/09/11 3:01
Cubacadabra の魔法の正体
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
Cubacadabra は、Swift、Kotlin、JavaScript の 3 つの個別クライアントを置き換えることで基盤構造自体を再構築しており、UniFFI バインディングと WASM を介して共有 Rust コアを囲む薄いネイティブシェルに移行しています。このシフトにより、iOS、Android、Web 全体にわたる重要なゲームロジック、レンダリング、および検証ルールが一元化され、危険なコードの重複が排除されました。Mozilla や Litter のような成功したオープンソースプロジェクトに触発されて、スタジオは今やアカウント管理、エフェクト、状態ストリーミング、および検証を共有クレートに依存しています。モバイル ABI は create、destroy、JSON 送信、スナップショット取得、エフェクトポーリング、出力ポインタ読み取りの 7 つの関数を提供し、安全性のために単一スレッドハンドルがスナップショットを必要とし、ブラウザ統合は wasm-bindgen を使用し、WebClient と WebRenderer の間でエンジンハンドルの共有を防ぐための独立した WASM モジュールランタイムを採用しています。ホストは認証、再接続バックオフ、移動のサーモルティング(制約)、およびサーフェスライフサイクルといったデバイス固有の任務を保持します。マルチプレイセッションロジックは複数のホストから統合され、1 つのコミットでリレーチェックポイントを簡素化するために 244 ラインの削除に対しアダプタ用として 74 ラインのみを追加しました。バックエンドでは Cloudflare Durable Objects を使用し、同様の状態に至るプレイヤー間の同時発生する競合を安全に解決するために compare-and-set シーケンス番号を採用しています。レンダリングには、Metal(iOS)、Vulkan/GLES(Android)、WebGPU/WebGL(ブラウザ)をサポートする共有 wgpu エンジンが使用され、128 バイトの固定インスタンスレイアウトが採用されています。キャラクターモデルは 50 のレンダラー専用キャラクター、32 MiB のメッシュ/インスタンスバッファー、および投影高さが 180 ピクセルと 70 ピクセルで切り替わる LOD を使用します。「cubacadabra-morphs」クレートは GLB ファイルに対する
.morph.json サイドカートを介してアセットを検証し、近接 >= 中距離 >= 遠方のような三角形数に関する制約を強制します。最後に、スタジオは頂点バッファのデコードやランタイムパックのコンパイル前にアセットメタデータをチェックする検証ループを提供し、共有レンダラーとの互換性を保証しています。ユーザーにとっては、最も大きな利点はデバイス選択にかかわらず完全に一貫したゲーム世界とルールセットを体験できることであり、これは複雑なエンジンを共有バックエンドで統合することで長期的なメンテナンス性とクロスプラットフォーム信頼性を確保するための業界の新基準を確立しています。本文
Rust を基盤としたクロスプラットフォームアーキテクチャ「Cubacadabra」
1. コードの共通化とアプローチ
「アブラカダブラ」はもはや魔法ではなく、問題解決のための戦略です。古代ローマの護符のように、問題を小さく削ぎ取るアプローチを現代のソフトウェア開発に応用します。
現状の問題点(「一つの製品の 3 つのクライアント」)
- 重複実装: iOS, Android, Web の 3 プラットフォームで同じ機能を別々に実装しています。
- 独自要件: 各プラットフォームには固有のバリデーション、リクエスト処理、エラーハンドリングが必要です。
- 自然な技術的負債: 意図的な設計ではなく、開発の過程で蓄積された差異がバグの原因となります(例:iOS と Android での保存失敗挙動の違い)。
解決策:Rust を薄い殻とする
- コアロジックの共有: ゲームエンジンやアカウント画面など、移植可能なロジックをすべて Rust クレート に移行します。
- プラットフォーム依存部のみ残す: ネイティブコントロール、デバイスサービス、ブラウザ統合に必要な最小限のスウィフト、コトリン(Kotlin)、JavaScript のみを使用します。
- DRY 原則の適用: 「同じコードは一度だけ書く」原則を、アプリケーション全体の決定論的な構造にも拡張します。
既存の実例
- Litter アプリ: Rust コア(セッション管理など)の上に、UniFFI を介したネイティブインターフェースが乗っています。
- Mozilla Firefox: デスクトップとモバイル版で共有する Rust コンポーネントを活用し、重複コードによる保守コストを削減しています。
2. システムの価値と投資対効果
長期的なアーキテクチャとして、Rust と共有コアがもたらすメリットは明確です。
なぜ Rust と共有コアが必要か?
- 複雑性の増大: プラットフォームが成長すると、コンテンツ互換性、サブスクリプション管理、接続切断時の回復など、相互作用が増加します。これらを個別に実装すると調整コストが膨大になります。
- 保守性の向上: 重複したロジックはバグの原因となりやすいため、単一の真理元(Source of Truth)を維持する必要があります。
実績と効果
- コード削減: 1 つの iOS インテグレーションコミットで、106 行の追加に対し 269 行が削除されました。
- インフラストラクチャの成長: Web インテグレーションも既存の仕組みを再利用し、効率的に拡張できています。
- 複雑性の一元管理: 振る舞いの決定を一つの実装で行い、ホストアダプタ(ネイティブ側の処理)を最小限に保ちます。
3. 「Save ボタン」の論理を共有する
cubacadabra-client と cubacadabra-app を分離し、ビジネスロジックをアプリ層で統合します。
アーキテクチャ構成
: エンジン向け(マルチプレイヤーセッション管理)。cubacadabra-client
: ゲームプレイ以外の移植可能な振る舞い(アカウント管理など)を所有。cubacadabra-app- 境界線: ユーザー名保存は
クレートの領域に属し、すべてのクライアント(iOS, Android, Web)が利用可能になります。app
動作フローの具体例
- ユーザーが名前変更を実行(例:
)。Dragon_7 - ホストが
およびUsernameChanged
イベントをディスパッチします。SaveUsername - Rust レイヤー:
- ドラフトを検証。
- HTTP エフェクトを発行(ID, アカウント ID, メソッドなどを指定)。
- ホストからのステータスとボディを受け取り、画面更新の可否を決定。
- 結果: 保存中の編集は新しいドラフトとして保持され、セッション置換が適切に行われます。
共通化されるルール
- アバター保存、カタログページネーション、ブロック/アンブロックなど、全ての複雑な振る舞いを一つの実装で管理。
とAppViewModel
は境界を明確に持ち、ゲームの再起動がアカウントデータを破損させないことを保証します。GameViewModel
4. メモリ管理とバインディング戦略
Cubacadabra は Litter と異なり、モバイルでは C インターフェース、ブラウザでは
wasm-bindgen を採用しています。
アプリケーションレイヤーの要件
- 必要な依存:
,serde
(データシリアライゼーション)。serde_json - 不要な依存: 非同期 HTTP ランタイムやレンダラーは含まれません。これにより、ゲーム開始前に共有ロジックを利用できます。
ネイティブアプリとの通信(ABI)
ネイティブ側には以下の 7 つの関数が定義されます:
(オブジェクト作成)create
(オブジェクト破棄)destroy- JSON のディスパッチ
- スナップショット JSON の取得
- エフェクト JSON のポーリング
- 出力ポインターと長さの読み取り
メモリとポインタの取り扱い
- 所有権: ハンドルは単一スレッドで動作し、データは Rust が管理します。
- コピー要件: ネイティブ側(Swift/JNI)では、次の変異前に Rust へのコピーが必要です(
型やバイト配列アダプタを使用)。Data - 互換性チェック: バインディングがスナップショットプロトコルのバージョンと合致するか確認し、不一致の場合は静かに失敗します。
ブラウザの注意点
- WebClient と WebRenderer は同じ WASM モジュール由来ですが、エンジンハンドルは共通のメモリアドレスを参照しません。
- アプリランタイムは別の WASM モジュールであり、アカウント編集時にレンダラーを再ロードする必要はありません。
5. セッション同期と競合解決
クライアントから抽出されたロジックにより、セッション管理の一貫性を確保します。
ホスト側の実装削減
- 異なるホスト(ブラウザなど)ごとにソケット翻訳やワールド変更ルーティングを実装する必要がありません。
- 一つの「修正すべきセッション実装」だけで済み、アダプタの変更も最小限に抑えられます。
ワールド状態の管理
- 共有セッション: 二人のプレイヤーが同時に同じ状態に達しても、ワールドは動き続けます(シリアライゼーションとコピーを活用)。
- 意図の重複処理:
のリダクサーは、すでにノードをキャプチャした場合Signal Run
を返します。nil- 重複した意図は消え、保留されたチャンネルは新規参入者へ展開されます。
- サーバー側の
タイマーが、クライアントの遅延に関わらず機能します。ageMs
競争ゲームへの適用
- クライアントはペイロードを提案しますが、バックエンド(サーバー)は最終的なルール検証を行います。
- 単純な順序付けだけでは真偽が決まらないため、サーバーサイドでの状態管理が必須です。
6. レンダリングとパフォーマンスの境界線
レンダリングコアは wgpu を通じて共有され、プラットフォームごとの差異は最小限に抑えます。
共有されたレンダラー基盤
- iOS: Metal
- Android: Vulkan / GLES
- ブラウザ: WebGPU / WebGL
- 同じキャラクター向けランタイムをこれらすべての環境で動作させます。
デバイス固有の制御
- 表面(Surface)のライフタイム、入力処理、オーディオ再生、OS 割り込みは依然として各ホストに属します。
- Android のような「表面が消える」現象は、エンジンがアプリ全体ではないことを示すリマインダーです。
キャラクターシステムの実装詳細
- カタログ管理: インデックスされたメッシュカタログを使用し、バッチ処理で効率化。
- LOD 制御: 投影高さに基づき LOD を選択(境界値:180px / 70px)。
- アニメーション: 固定された 15 関節階層でキャラクターを駆動。
- 物理演算:
- フード袖は頂点シェーダー内で曲げ処理。
- 髪は有界なスプリング運動を使用し、キャップ部は頭部に固定。
パフォーマンスの約束
- リソース制限: キャラクター予算(レンダラー用モデル 50、バッファ 32 MiB)は実装上の硬制約です。
- フレームレートの保証: 「全ての電話が一定の FPS を維持する」という主張ではなく、物理デバイスの限界を尊重します。
7. アセット検証(ハットとメッシュ)
Blender エクスポートされたアセットは、完全に動作するかを事前に厳密に検証します。
モーフデータの構造
- Blender ノード内の「Cylinder」形状でも、必要な LOD ノードが不足すると却下されます。
- 既存システムでは Rust 列挙型でボディと衣装をエンコードし、レンダラーが組み合わせを準備しますが、これがエンジン変更を必要とする場合は不便です。
cubacadabra-morphs
クレートの導入
cubacadabra-morphs- 管理機能: ID、カタログスキーマ、ロードアウツ、互換性判断を所有。
- チェッカー機能:
- サポートされるベースとリグの互換性確認。
- 共通なフィットプロファイル、占有されたスロットの確認。
- コンフリクトや
の有無チェック。mesh.rigid.v1
- Studio での活用: GPU を初期化せずとも検証が可能。
GLB とサイドカーの検証
- サイドカー (
):.morph.json- 資産、ジオメトリパス、LOD ノード情報、三角形数を宣言。
- アタッチメント変換と正規化された四元数までチェック。
- 条件:
の制約を満たす。near >= mid >= far > 0
- GLB インスペクター:
- glTF マジックナンバー、バージョン 2、ファイル長をチェック。
- JSON ノードやメッシュアクセッサの三角形数を計算し、サイドカーと照合。
- 制限: ソース 64 MiB、サイドカー 256 KiB を超えると却下。
今後のマイルストーン
- 目標: Rust ソースファイルを開くことなく、アーティストが新しいアセット(2 つ目のハットなど)を追加できること。
- 現在の計画: Blender エクスポート → Studio 検証/コンパイル → 共有レンダラーへの
展開。.morphpack
8. ゲームの拡張性と未来
このアーキテクチャは、ゲームロジックそのものにも適用されます。
プロジェクト構造
- ソース:
,manifest.json
, アセット。src/main.luau - Studio: メモリ上にプロジェクトを構築し、生成されたファイルをソースに戻さずに実行可能。
- ランタイムの違い:
- ネイティブビルド: MLua を通じてベンダードされた Luau。
- WASM: 純粋 Rust の
ランタイム。luaur-rt - 両者は同じゲーム API を公開し、パリティチェックに値します。
ゲームの柔軟性
- ルールの変更: 誰かが嵐を変える、リレーを馬鹿げたものにする、あるいはドラゴンに帽子をかぶせるといった変更を、3 つのクライアントパッチとバックエンドリリースを調整することなく実現したいです。
- 複雑性の蓄積: 新しいプラットフォームにはシェル(ネイティブラッパー)が必要ですが、製品ロジック自体は Rust で統一され続けます。
結論:古代の護符への回帰
昔の護符は文字を削ぎ取り、実質的に空っぽにしていました。 Cubacadabra も同様です。Swift, Kotlin, JavaScript は「違いが必要である部分」だけを残し、残りのすべてを Rust という強力な基盤に委ねます。