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