koota について
Kootaは、TypeScript開発者向けのECS状態管理ツールです。エンティティと組み合わせ可能なトレイトでデータを構成し、クエリとシステムで状態を更新します。ロジックとビューの分離を重視しています。
主な機能
- 01トレイトによるエンティティ構成
- 02リレーションによるグラフ構築
- 03アーキタイプ別のエンティティ検索
- 04アクションによる状態変更
- 05リアクティブなシステム連携
- 06React連携
活用シーン
- 01アプリやゲームのエンティティ状態の整理
- 02エンティティデータの一括検索と更新
- 03コアロジックとビューの分離
- 04Reactでの状態変化の購読
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ pmndrs/ koota --skill kootaインストール後、Claude Code で/kootaを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/architecture.mdqueries.mdreact-hooks.mdreact-patterns.mdrelations.mdruntime.md詳細
Koota ECS
Koota は、合成可能なトレイトを持つエンティティを使って状態を管理します。
用語集
- エンティティ - トレイトで定義されたデータを指す、一意の識別子です。ワールドから生成されます。
- トレイト - 再利用可能なデータ定義です。スキーマベース(SoA)、コールバックベース(AoS)、またはタグとして定義できます。
- リレーション - エンティティ間の有向接続で、グラフを構築します。
- ワールド - すべてのエンティティとそのデータ(トレイト)のコンテキストです。
- アーキタイプ - エンティティが共有する、トレイトの一意な組み合わせです。
- クエリ - アーキタイプに一致するエンティティを取得します。状態を一括更新するための主な手段です。
- アクション - 離散的で同期的なデータ変更(作成、更新、破棄)です。任意の呼び出し箇所で再利用できます。
- システム - 状態の変化を監視し、非同期ワークフローを含む処理を調整するリアクティブなオーケストレーターです。フレームループまたはイベントコールバックで実行されます。
設計原則
データ指向
振る舞いとデータを分離します。データはトレイトとして定義し、エンティティはトレイトを組み合わせ、システムはクエリを通じてトレイトのデータを変更します。完全な例については、基本的な使い方を参照してください。
合成可能なシステム
すべてを順番に実行するモノリシックな関数ではなく、小さく単一の目的を持つ単位としてシステムを設計します。各システムは1つの関心事を担当し、振る舞いを個別に有効化・無効化できるようにします。
// Good: Composable systems - each can be enabled/disabled independently
function applyVelocity(world: World) {}
function applyGravity(world: World) {}
function applyFriction(world: World) {}
function syncToDOM(world: World) {}
// Bad: Monolithic system - can't disable gravity without disabling everything
function updatePhysicsAndRender(world: World) {
// velocity, gravity, friction, DOM sync all in one function
}
これにより、機能フラグ、デバッグ(問題を切り分けるために1つのシステムを無効化)、柔軟なランタイム設定が可能になります。
ビューとロジックを分離する
コア状態とロジック(「コア」)をビュー(「アプリ」)から分離します。
- レンダリングに依存せずロジックを実行する
- 状態を維持したままビューを切り替える(2D ↔ 3D)
- ワーカーまたはサーバー上でロジックを実行する
クラスよりトレイトとアクションを優先する
データと振る舞いのカプセル化にクラスを使うことは、なるべく避けてください。データにはトレイトを、振る舞いにはアクションを使います。外部ライブラリ(例:THREE.js のオブジェクト)で必要な場合、またはユーザーが希望する場合に限り、クラスを使います。
ディレクトリ構成
ユーザーが希望する構成があれば、それに従ってください。それ以外の場合は、以下の指針を使用してください。ディレクトリ構成は、アプリのデータモデルの整理方法を反映する必要があります。コア状態とロジックをビュー層から分離します。
- コア - 純粋な TypeScript。トレイト、システム、アクション、ワールドを含みます。ビューのインポートはありません。
- ビュー - ワールドから読み取り、アクションを通じて変更します。ドメインや機能ごとに整理します。
src/
├── core/ # Pure TypeScript, no view imports
│ ├── traits/
│ ├── systems/
│ ├── actions/
│ └── world.ts
└── features/ # View layer, organized by domain
ファイルは機能ごとではなく、役割ごとに整理します。トレイトとシステムは合成可能で、機能にきれいに対応づけられるものではありません。
詳細なパターンとモノレポ構成については、references/architecture.mdを参照してください。
トレイトの種類
| 種類 | 構文 | 使用する場面 | 例 |
|---|---|---|---|
| SoA(スキーマ) | trait({ x: 0 }) | 単純なプリミティブデータ | Position、Velocity、Health |
| AoS(コールバック) | trait(() => new Thing()) | 複雑なオブジェクト/インスタンス | Ref(DOM)、Keyboard(Set) |
| タグ | trait() | データがなく、フラグのみの場合 | IsPlayer、IsEnemy、IsDead |
トレイトの命名規則
| 種類 | パターン | 例 |
|---|---|---|
| タグ | Is で始める | IsPlayer、IsEnemy、IsDead |
| リレーション | 前置詞 | ChildOf、HeldBy、Contains |
| トレイト | 名詞 | Position、Velocity、Health |
リレーション
リレーションは、階層、インベントリ、ターゲティングなどのエンティティ間のグラフを構築します。
import { relation, trait } from 'koota'
const ChildOf = relation({ autoDestroy: 'orphan' }) // Hierarchy
const Contains = relation({ store: { amount: 0 } }) // With data
const Targeting = relation({ exclusive: true }) // One target only
// Build graph
const parent = world.spawn()
const child = world.spawn(ChildOf(parent))
const gold = world.spawn()
const silver = world.spawn()
const inventory = world.spawn(Contains(gold), Contains(silver))
// Query children of parent
const children = world.query(ChildOf(parent))
// Query all entities with any ChildOf relation
const allChildren = world.query(ChildOf('*'))
// Query relation targets
const targets = inventory.targetsFor(Contains)
// Filter by traits on the target entity
const IsRare = trait()
silver.add(IsRare)
// Target filters can use any legal query, not just a single trait
const rareInventories = world.query(Contains(IsRare))
// Get targets from entity
const items = inventory.targetsFor(Contains) // Entity[]
const target = child.targetFor(ChildOf) // Entity | undefined
詳細なパターン、トラバーサル、順序付きリレーション、アンチパターンについては、references/relations.mdを参照してください。
基本的な使い方
import { trait, createWorld } from 'koota'
// 1. Define traits
const Position = trait({ x: 0, y: 0 })
const Velocity = trait({ x: 0, y: 0 })
const IsPlayer = trait()
// 2. Create world and spawn entities
const world = createWorld()
const player = world.spawn(Position({ x: 100, y: 50 }), Velocity, IsPlayer)
// 3. Query and update
world.query(Position, Velocity).updateEach(([pos, vel]) => {
pos.x += vel.x
pos.y += vel.y
})
エンティティ
エンティティは、特性を構成する一意の識別子です。ワールドから生成されます。
// Spawn
const entity = world.spawn(Position, Velocity)
// Read/write traits
entity.get(Position) // Read trait data
entity.set(Position, { x: 10 }) // Write (triggers change events)
entity.add(IsPlayer) // Add trait
entity.remove(Velocity) // Remove trait
entity.has(Position) // Check if has trait
// Destroy
entity.destroy()
エンティティ ID
エンティティは内部的に、エンティティ ID、世代 ID(再利用のため)、ワールド ID をまとめた数値です。永続化やネットワーク通信のために、そのまま安全に保存できます。
entity.id() // Just the entity ID (reused after destroy)
entity // Full packed number (unique forever)
型付け
TraitRecord を使用すると、entity.get() が返す型を取得できます。
type PositionRecord = TraitRecord<typeof Position>
クエリ
クエリは、アーキタイプに一致するエンティティを取得するもので、状態をまとめて更新する主な方法です。
// Query and update
world.query(Position, Velocity).updateEach(([pos, vel]) => {
pos.x += vel.x
pos.y += vel.y
})
// Read-only iteration (no write-back)
const data: Array<{ x: number; y: number }> = []
world.query(Position, Velocity).readEach(([pos, vel]) => {
data.push({ x: pos.x, y: pos.y })
})
// Get first match
const player = world.queryFirst(IsPlayer, Position)
// Filter with modifiers
world.query(Position, Not(Velocity)) // Has Position but not Velocity
world.query(Or(IsPlayer, IsEnemy)) // Has either trait
データを持つクエリでは、for...of と entity.get() の組み合わせよりも、updateEach/readEach を優先してください。readEach では、2 番目の引数としてエンティティも取得できます。
注: updateEach/readEach が返すのは、データを持つ特性(SoA/AoS)のみです。タグ、Not()、リレーションフィルターは除外されます。
world.query(IsPlayer, Position, Velocity).updateEach(([pos, vel]) => {
// Array has 2 elements - IsPlayer (tag) excluded
})
変更の追跡、クエリのキャッシュ、高度なパターンについては、references/queries.md を参照してください。
React との統合
インポート: コア型(World、Entity)は 'koota' から、React フックは 'koota/react' からインポートします。
変更検出: entity.set() と world.set() は変更イベントを発生させ、useTrait などのフックを再レンダリングします。オブジェクトを直接変更する AoS 特性では、entity.changed(Trait) を手動で呼び出して通知してください。
React フックとアクションについては、references/react-hooks.md を参照してください。
コンポーネントのパターン(App、Startup、Renderer、ビューの同期、入力)については、references/react-patterns.md を参照してください。
ランタイム
システムはワールドにクエリを実行し、エンティティを更新します。フレームループ(連続)またはイベントハンドラー(離散)で実行します。
システム、フレームループ、イベント駆動型のパターン、時間管理については、references/runtime.md を参照してください。