一键导入
verekia-architecture
Day-to-day coding style and patterns for R3F game development with Miniplex ECS.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Day-to-day coding style and patterns for R3F game development with Miniplex ECS.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Poll for changes to any value and trigger React re-renders when it changes.
Verekia's preferred project setup with Next.js Pages Router, Tailwind 4, oxfmt, oxlint, and TypeScript.
Use Zustand as a simple state store for entity management (not a true ECS).
Attach meshes to bones of a skinned mesh, such as attaching a sword to a character's hand.
Shake the camera when the player takes damage or on impacts for visual feedback.
Display floating damage numbers that animate upward and fade out, with support for critical hits.
| name | verekia-architecture |
| description | Day-to-day coding style and patterns for R3F game development with Miniplex ECS. |
The core principle of R3F game development is separating game logic from rendering. React components are views, not the source of truth.
Systems contain all game logic:
Views (React components) only render:
<PlayerEntity>, <EnemyEntity> wrap models with ModelContainer, process any data needed and pass it as props to the model<PlayerModel>, <EnemyModel> are dumb and only render meshes via propsuseFrame in view components unless it is purely visual and should not be part of the core logicGames should be capable of running entirely without a renderer:
┌─────────────────────────────────────────┐
│ Game Logic Layer │
│ (Systems, ECS, World State, Entities) │
└─────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ View Layer (optional) │
│ React Three Fiber / DOM / Headless │
└─────────────────────────────────────────┘
This means:
From miniplex-react:
ECS.Entity - Don't use this componentECS.Component - Don't use this componentECS.world - Don't access world through ECS, use direct importuseEntities hook - Don't use thisFrom miniplex core:
onEntityAdded / onEntityRemoved - Prefer using data and systems to trigger things (e.g., timers, flags).where() - Don't use predicate-based filtering, prefer iterating over all entities that have the component no matter its value. For example iterate over all entities that have health and filter out entities that have health < 0 in the system rather than querying entities where health < 0 (which would require reindexing).Only use these:
world.add(entity) - Add a new entityworld.remove(entity) - Remove an entityworld.addComponent(entity, 'component', value) - Add component to existing entityworld.removeComponent(entity, 'component') - Remove component from entityworld.with('prop1', 'prop2') - Create queriescreateReactAPI(world) - Get Entities component for rendering// 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]
// Only destructure Entities from React API
export const { Entities } = createReactAPI(world)
Capture Three.js object references on entities using a wrapper component, allowing systems to manipulate objects directly.
Similar to the Redux container/component pattern:
*Entity components are smart wrappers that connect entity data to the view*Model components are dumb and only responsible for rendering┌─────────────────────────────────────────┐
│ PlayerEntity (smart) │
│ - Wraps with ModelContainer │
│ - Passes entity data as props │
│ │
│ ┌─────────────────────────────────┐ │
│ │ PlayerModel (dumb) │ │
│ │ - Pure rendering │ │
│ │ - Receives props │ │
│ │ - No knowledge of entities │ │
│ └─────────────────────────────────┘ │
└─────────────────────────────────────────┘
entity.three directly in useFrameThe component passed to <Entities> receives the entity directly as props:
// Dumb component - only renders, no entity knowledge
const CharacterModel = () => (
<mesh>
<sphereGeometry />
<meshBasicMaterial color="blue" />
</mesh>
)
// Smart wrapper - connects entity to model via ModelContainer
const CharacterEntity = (entity: CharacterEntity) => (
<ModelContainer entity={entity}>
<CharacterModel />
</ModelContainer>
)
// entities/entities.tsx (contains <Entities> for all renderable entities)
const isCharacterQuery = world.with('isCharacter')
export const CharacterEntities = () => <Entities in={isCharacterQuery}>{CharacterEntity}</Entities>
Define queries near where they are used (in the system file), not in a central file. But define them outside the loop at module scope:
import { world } from '@/lib/ecs'
// ✅ Query defined at module scope, near where it's used
const movingEntities = world.with('position', 'velocity')
type MovingEntity = (typeof movingEntities)['entities'][number]
Split logic into a "One" function (operates on a single entity) and the system (iterates and calls One):
import { world } from '@/lib/ecs'
// Query at module scope
const movingEntities = world.with('position', 'velocity')
type MovingEntity = (typeof movingEntities)['entities'][number]
// "One" function - single entity logic, easy to test
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
}
// System - just iteration
export const VelocitySystem = () => {
useFrame((_, dt) => {
for (const e of movingEntities) {
velocityOne(e, dt)
}
})
return null
}
Systems must iterate over queries tailored to their specific needs, not over entity types:
// ✅ GOOD - Query targets entities with the components the system needs
const entitiesWithVelocity = world.with('position', 'velocity')
const VelocitySystem = () => {
useFrame((_, delta) => {
for (const entity of entitiesWithVelocity) {
entity.position.x += entity.velocity.x * delta
}
})
return null
}
// ❌ BAD - Iterating over specific entity types
const VelocitySystem = () => {
useFrame((_, delta) => {
for (const player of players) {
/* ... */
}
for (const enemy of enemies) {
/* ... */
}
for (const projectile of projectiles) {
/* ... */
}
})
return null
}
The point of an ECS is that systems operate on a subset of entities matching exactly what they need. A VelocitySystem targets entities with velocity, not "players + enemies + projectiles".
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 stores are for state that doesn't belong in the ECS. Each store has a consistent API pattern:
// In React components (reactive)
const areSettingsOpen = useUI('areSettingsOpen')
// Outside React / in systems (non-reactive)
const settings = getUI().areSettingsOpen
// Setting values
setUI('areSettingsOpen', true)
setUI({ areSettingsOpen: true, debug: { drawCalls: 100 } })
// Reset to defaults
resetUI()
use* hooks for reactive access in React componentsget* for non-reactive access in systems or callbacksset* supports both single key-value and partial state updatesreset* restores default stateget* to window for debugging in browser consolestructuredClone(defaultState) to avoid mutation issues@react-three/fiber/webgpu, not @react-three/fiberuseFrame in view components: Most useFrame calls belong in systems*Entity components are smart wrappers, *Model components are dumb renderersentity.three positions/rotations<Entities> is the only React bridge: Only use this from miniplex-react(typeof query)['entities'][number]This skill is part of verekia's r3f-gamedev.