verekia-architecture
R3F と Miniplex ECS を使用したゲーム開発における日常的なコーディングスタイルとパターン。
description の原文を見る
Day-to-day coding style and patterns for R3F game development with Miniplex ECS.
SKILL.md 本文
アーキテクチャ
R3F ゲーム開発の基本原則は、ゲームロジックをレンダリングから分離することです。React コンポーネントはビューであり、唯一の情報源ではありません。
システムとビュー
システム はすべてのゲームロジックを含みます:
- 移動、物理演算、衝突検出
- エンティティの生成と破棄
- 状態変更(ヘルス、スコア、タイマー)
- AI と動作
- Three.js オブジェクトとエンティティ状態の同期
ビュー(React コンポーネント)のみレンダリングします:
<PlayerEntity>、<EnemyEntity>はModelContainerでモデルをラップし、必要なデータを処理して props としてモデルに渡します<PlayerModel>、<EnemyModel>はダムコンポーネントで、props 経由でメッシュをレンダリングするだけです- コアゲームロジックは含まず、ビジュアルロジックのみです
- ビューコンポーネントに
useFrameを使用しないでください(純粋にビジュアルで、コアロジックの一部であるべきではない場合を除く)
ヘッドレスファースト の考え方
ゲームはレンダラーなしで完全に実行できる必要があります:
┌─────────────────────────────────────────┐
│ ゲームロジック層 │
│ (システム、ECS、ワールド状態、エンティティ) │
└─────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ ビュー層(オプション) │
│ React Three Fiber / DOM / ヘッドレス │
└─────────────────────────────────────────┘
これは以下を意味します:
- すべての状態は React コンポーネントではなく、ワールド/ECS に存在します
- システムはエンティティを反復処理し、状態を変更します
- ビューは状態を購読してそれに応じてレンダリングします
- R3F を DOM 要素に置き換えたり、ヘッドレスでテストを実行できます
Miniplex:使用しないもの
miniplex-react から:
ECS.Entity- このコンポーネントを使用しないでくださいECS.Component- このコンポーネントを使用しないでくださいECS.world- ECS 経由でワールドにアクセスしないでください。直接インポートを使用してくださいuseEntitiesフック - これを使用しないでください- レンダープロップパターン - これを使用しないでください
miniplex コアから:
onEntityAdded/onEntityRemoved- データとシステムを使用してトリガーする方が優れています(例:タイマー、フラグ).where()- 述語ベースのフィルタリングを使用しないでください。コンポーネント値に関係なくコンポーネントを持つすべてのエンティティを反復処理する方が優れています。たとえば、ヘルスを持つすべてのエンティティを反復処理し、システム内でヘルス < 0 のエンティティを除外する方が、ヘルス < 0 のエンティティをクエリする方(再インデックスが必要)よりも優れています。
Miniplex:推奨メソッド
これらのみを使用してください:
world.add(entity)- 新しいエンティティを追加world.remove(entity)- エンティティを削除world.addComponent(entity, 'component', value)- 既存エンティティにコンポーネントを追加world.removeComponent(entity, 'component')- エンティティからコンポーネントを削除world.with('prop1', 'prop2')- クエリを作成createReactAPI(world)- レンダリング用Entitiesコンポーネントを取得
エンティティタイプとクエリ
// lib/ecs.ts
import { World } from 'miniplex'
import createReactAPI from 'miniplex-react'
type Entity = {
position?: { x: number; y: number; z: number }
velocity?: { x: number; y: number; z: number }
isCharacter?: true
isEnemy?: true
three?: Object3D | null
}
export const world = new World<Entity>()
export const characterQuery = world.with('position', 'isCharacter', 'three')
export type CharacterEntity = (typeof characterQuery)['entities'][number]
// React API から Entities のみを分割代入
export const { Entities } = createReactAPI(world)
ModelContainer パターン
ラッパーコンポーネントを使用して Three.js オブジェクトの参照をエンティティにキャプチャし、システムがオブジェクトを直接操作できるようにします。
Redux コンテナ/コンポーネントパターンに似ています:
*Entityコンポーネントはエンティティデータをビューに接続するスマートなラッパーです*Modelコンポーネントはダムで、レンダリングにのみ責任があります
┌─────────────────────────────────────────┐
│ PlayerEntity(スマート) │
│ - ModelContainer でラップ │
│ - エンティティデータを props として渡す │
│ │
│ ┌─────────────────────────────────┐ │
│ │ PlayerModel(ダム) │ │
│ │ - 純粋なレンダリング │ │
│ │ - props を受け取る │ │
│ │ - エンティティの知識なし │ │
│ └─────────────────────────────────┘ │
└─────────────────────────────────────────┘
- Ref コールバックが Three.js オブジェクトをエンティティに保存
- クリーンアップ関数がアンマウント時に参照を削除
- システムが
useFrame内でentity.threeに直接アクセス - モデルは分離可能で、テスト可能です
エンティティアズプロップス パターン
<Entities> に渡されるコンポーネントはエンティティを props として直接受け取ります:
// ダムコンポーネント - レンダリングのみ、エンティティ知識なし
const CharacterModel = () => (
<mesh>
<sphereGeometry />
<meshBasicMaterial color="blue" />
</mesh>
)
// スマートラッパー - ModelContainer 経由でエンティティをモデルに接続
const CharacterEntity = (entity: CharacterEntity) => (
<ModelContainer entity={entity}>
<CharacterModel />
</ModelContainer>
)
// entities/entities.tsx(すべてのレンダリング可能なエンティティの <Entities> を含む)
const isCharacterQuery = world.with('isCharacter')
export const CharacterEntities = () => <Entities in={isCharacterQuery}>{CharacterEntity}</Entities>
システムとクエリ
クエリの配置
クエリは 使用される場所の近く(システムファイル内)で定義しますが、ループの外のモジュールスコープで定義します:
import { world } from '@/lib/ecs'
// ✅ モジュールスコープで定義されたクエリ、使用される場所の近く
const movingEntities = world.with('position', 'velocity')
type MovingEntity = (typeof movingEntities)['entities'][number]
ワンプラス システム パターン
ロジックを 「One」関数(単一エンティティで動作)と システム(反復して One を呼び出す)に分割します:
import { world } from '@/lib/ecs'
// モジュールスコープのクエリ
const movingEntities = world.with('position', 'velocity')
type MovingEntity = (typeof movingEntities)['entities'][number]
// 「One」関数 - 単一エンティティロジック、テスト容易
const velocityOne = (e: MovingEntity, dt: number) => {
e.position.x += e.velocity.x * dt
e.position.y += e.velocity.y * dt
e.position.z += e.velocity.z * dt
}
// システム - 反復処理のみ
export const VelocitySystem = () => {
useFrame((_, dt) => {
for (const e of movingEntities) {
velocityOne(e, dt)
}
})
return null
}
コンポーネント別クエリ、型別ではない
システムはエンティティ型ではなく、特定のニーズに合わせたクエリを反復処理する必要があります:
// ✅ 良い - クエリはシステムが必要とするコンポーネントを持つエンティティをターゲット
const entitiesWithVelocity = world.with('position', 'velocity')
const VelocitySystem = () => {
useFrame((_, delta) => {
for (const entity of entitiesWithVelocity) {
entity.position.x += entity.velocity.x * delta
}
})
return null
}
// ❌ 悪い - 特定のエンティティ型を反復処理
const VelocitySystem = () => {
useFrame((_, delta) => {
for (const player of players) {
/* ... */
}
for (const enemy of enemies) {
/* ... */
}
for (const projectile of projectiles) {
/* ... */
}
})
return null
}
ECS の重点は、システムが必要なものと一致するエンティティの部分集合で動作することです。VelocitySystem は「プレイヤー + 敵 + プロジェクタイル」ではなく、velocity を持つエンティティをターゲットにします。
ThreeSystem - Three.js の同期
const threeEntities = world.with('position', 'three')
type ThreeEntity = (typeof threeEntities)['entities'][number]
const threeOne = (e: ThreeEntity) => {
e.three.position.set(e.position.x, e.position.y, e.position.z)
}
export const ThreeSystem = () => {
useFrame(() => {
for (const e of threeEntities) {
threeOne(e)
}
})
return null
}
エンティティの生成
const SpawnSystem = () => {
useEffect(() => {
world.add({ position: { x: 0, y: 0, z: 0 }, isCharacter: true })
}, [])
return null
}
Zustand ストアの使用
Zustand ストアは ECS に属さない状態用です。各ストアは一貫した API パターンを持ちます:
// React コンポーネント内(リアクティブ)
const areSettingsOpen = useUI('areSettingsOpen')
// React 外/システム内(非リアクティブ)
const settings = getUI().areSettingsOpen
// 値の設定
setUI('areSettingsOpen', true)
setUI({ areSettingsOpen: true, debug: { drawCalls: 100 } })
// デフォルトにリセット
resetUI()
use*フックを React コンポーネント内でリアクティブアクセスに使用get*をシステムやコールバック内の非リアクティブアクセスに使用set*は単一キーバリューと部分的な状態更新の両方をサポートreset*はデフォルト状態を復元- ブラウザコンソールでのデバッグ用に
get*をwindowにアタッチ - ミューテーション問題を避けるために
structuredClone(defaultState)を使用
キー原則
- R3F は WebGPU エントリポイントからインポート:
@react-three/fiberではなく@react-three/fiber/webgpuからインポート - ビューコンポーネントに
useFrameなし:ほとんどのuseFrame呼び出しはシステムに属します - エンティティ/モデル分離:
*Entityコンポーネントはスマートラッパー、*Modelコンポーネントはダムレンダラー - システムが Three.js を同期:システムはエンティティ状態と
entity.threeの位置/回転の両方を更新 - 完全に分離:すべてのビューコンポーネントを削除しても、ゲームは機能するべき
- コンポーネント別クエリ、型別ではない:システムは必要なコンポーネントに基づくクエリを反復処理
- ワールドとクエリは平文モジュールエクスポート:React コンテキストではない
<Entities>は唯一の React ブリッジ:miniplex-react からこれのみを使用- 型付きエンティティをクエリから導出:
(typeof query)['entities'][number] - クエリはそれが使用される場所の近くで定義:システムファイル内、モジュールスコープで
- システムロジックを分割:単一エンティティ用に「One」関数、反復処理用にシステム
このスキルは verekia の r3f-gamedev の一部です。
ライセンス: MIT(寛容ライセンスのため全文を引用しています) · 原本リポジトリ
詳細情報
- 作者
- verekia
- リポジトリ
- verekia/r3f-gamedev
- ライセンス
- MIT
- 最終更新
- 2026/5/12
Source: https://github.com/verekia/r3f-gamedev / ライセンス: MIT
関連スキル
doubt-driven-development
重要な判断はすべて、本番環境への展開前に新しい視点から対抗的レビューを実施します。速度より正確性が重要な場合、不慣れなコードを扱う場合、本番環境・セキュリティに関わるロジック・取り消し不可の操作など影響度が高い場合、または後でバグを修正するよりも今検証する方が効率的な場合に活用してください。
apprun-skills
TypeScriptを使用したAppRunアプリケーションのMVU設計に関する総合的なガイダンスが得られます。コンポーネントパターン、イベントハンドリング、状態管理(非同期ジェネレータを含む)、パラメータと保護機能を備えたルーティング・ナビゲーション、vistestを使用したテストに対応しています。AppRunコンポーネントの設計・レビュー、ルートの配線、状態フローの管理、AppRunテストの作成時に活用してください。
desloppify
コードベースのヘルスチェックと技術負債の追跡ツールです。コード品質、技術負債、デッドコード、大規模ファイル、ゴッドクラス、重複関数、コードスメル、命名規則の問題、インポートサイクル、結合度の問題についてユーザーが質問した場合に使用してください。また、ヘルススコアの確認、次の改善項目の提案、クリーンアップ計画の作成をリクエストされた際にも対応します。29言語に対応しています。
debugging-and-error-recovery
テストが失敗したり、ビルドが壊れたり、動作が期待と異なったり、予期しないエラーが発生したりした場合に、体系的な根本原因デバッグをガイドします。推測ではなく、根本原因を見つけて修正するための体系的なアプローチが必要な場合に使用してください。
test-driven-development
テスト駆動開発により実装を進めます。ロジックの実装、バグの修正、動作の変更など、あらゆる場面で活用できます。コードが正常に動作することを証明する必要がある場合、バグ報告を受けた場合、既存機能を修正する予定がある場合に使用してください。
incremental-implementation
変更を段階的に実施します。複数のファイルに影響する機能や変更を実装する場合に使用してください。大量のコードを一度に書こうとしている場合や、タスクが一度では完結できないほど大きい場合に活用します。