mcpField
E
ecs

ecs

github.com/drawcall-ai開発122 回インストール
リポジトリ

ecs について

ecsは、EliCS Entity Component SystemフレームワークでTypeScriptゲームを構築するためのスキルです。エンティティ、コンポーネント、システムの構成に加え、ライフサイクル、クエリ、状態とビューの同期を説明します。

主な機能

  1. 01EliCSのワールドとエンティティの作成
  2. 02データのみを持つコンポーネント設計
  3. 03システムの初期化と更新
  4. 04コンポーネントによるエンティティ検索
  5. 05システムの一時停止と再開
  6. 06入力・状態・ビューのレイヤー構成

活用シーン

  1. 01共通の振る舞いを持つゲームオブジェクトの整理
  2. 02フレームごとのゲーム状態更新の構成
  3. 03メニューや会話中のシステム停止

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/drawcall-ai/skills --skill ecs

インストール後、Claude Code で/ecsを入力して読み込まれたか確認します。

詳細

エンティティ・コンポーネント・システム

EliCS は、TypeScript 向けの軽量かつ高性能なエンティティ・コンポーネント・システムのフレームワークです。継承よりもコンポジションを重視するデータ指向設計を採用し、効率的なビットマスク処理と型付き配列ストレージを使用します。

主な概念: エンティティ(一意のオブジェクト)、コンポーネント(データコンテナー)、システム(ロジック処理器)、クエリ(エンティティセレクター)、ワールド(すべてを管理するコンテナー)。

導入する場面: 多数のオブジェクトが相互作用し、共通の振る舞い(敵、弾丸、アイテム、ユニットなど)を持つゲームや、状態駆動型の更新ループがあるゲームでは、ECS が役立ちます。クラスが複雑に絡み合うのではなく、ロジックをシステムに、状態をコンポーネントに分けて管理できるためです。専用オブジェクトが少数しかない小規模なシーンでは、ECS は余計なオーバーヘッドになります。通常のオブジェクトとレンダリングループで十分です。以下のセクションでは、EliCS を使って開発するのに必要な API を紹介します。正確なシグネチャと型の全容については、内容がパッケージより古くなっている可能性のあるこのページに頼らず、インストール済みの elics の型定義を参照してください。

推奨アーキテクチャ

Input、State、View の 3 層を使用します。

  • Input を State に反映する(Input Systems:ユーザー入力を受け取り、正規化して State に適用します。複雑な入力は複数のシステムに分割できます(例:生のイベント → 正規化されたアクション → 状態の更新)。各入力システムには、ユーザー入力をプログラムからシミュレートおよびテストできるよう、すべての入力を命令的に実行するメソッドを用意する必要があります。)
  • 各フレームで State を進める(State Systems:毎フレーム、ゲームの状態を進めます(例:プレイヤーを追いかける敵)。)
  • State を View に同期する(View Systems:State を View に反映します。必要に応じて初期化時にエンティティの作成・削除を購読し(例:3D オブジェクトや UI の作成・破棄)、頻繁に変化するデータを毎フレーム更新します。)
推奨ファイル構成
  • `src/components/*.ts`:コンポーネントごとに 1 ファイル(例:position.ts、velocity.ts)。コンポーネントには状態のみを保持し、振る舞いはシステムに記述します。
  • `src/systems/*.ts`:システムごとに 1 ファイル(例:input.ts、physics.ts)。各システムは初期化関数とフレームごとの更新関数をエクスポートします。
  • `src/seed.ts`:起動時のエンティティを作成してコンポーネントを追加し、ゲームをすぐにプレイできる状態にします。
  • `src/main.ts`:ゲームを構成します。シードを読み込み、システムを登録し、初期化を実行してから、毎フレームすべての状態を進め、View に同期します。

ワールドの作成

「ワールド」は、すべての ECS 要素が共存する場所です。

import { World } from 'elics'

const world = new World()

コンポーネントの定義

コンポーネントは、エンティティのプロパティを定義するデータコンテナーです。

import { createComponent, Types } from 'elics'
import { world } from './world.ts'

// Available Types:
// - Scalars: Types.Int8, Types.Int16, Types.Float32, Types.Float64, Types.Boolean, Types.String
// - Vectors: Types.Vec2, Types.Vec3, Types.Vec4, Types.Color (RGBA 0-1)
// - Entity reference: Types.Entity
// - Enum: Types.Enum
// - Types.Object holds any JS value (a Three.js mesh, an array) — not an ECS reference

// Define enums for type-safe state management using const assertions
export const UnitType = {
  Infantry: 'infantry',
  Cavalry: 'cavalry',
  Archer: 'archer',
} as const

export const CombatState = {
  Idle: 'idle',
  Moving: 'moving',
  Attacking: 'attacking',
  Defending: 'defending',
} as const

export const Position = createComponent(
  'Position',
  {
    value: { type: Types.Vec3, default: [0, 0, 0] },
  },
  '3D position coordinates',
)
//register components directly after creating them
world.registerComponent(Position)

export const Velocity = createComponent(
  'Velocity',
  {
    value: { type: Types.Vec3, default: [0, 0, 0] },
  },
  '3D velocity vector',
)
world.registerComponent(Velocity)

export const Health = createComponent(
  'Health',
  {
    value: { type: Types.Float32, default: 100 },
  },
  'Entity health points',
)
world.registerComponent(Health)

export const Unit = createComponent(
  'Unit',
  {
    type: { type: Types.Enum, enum: UnitType, default: UnitType.Infantry },
    state: { type: Types.Enum, enum: CombatState, default: CombatState.Idle },
    damage: { type: Types.Float32, default: 10, min: 1, max: 100 },
    armor: { type: Types.Int16, default: 0, min: 0, max: 50 },
    morale: { type: Types.Float32, default: 1.0, min: 0.0, max: 1.0 },
  },
  'Military unit with combat statistics',
)
world.registerComponent(Unit)

// Entity references
export const Target = createComponent('Target', {
  entity: { type: Types.Entity, default: null },
})
world.registerComponent(Target)

重要: ベクトル型(Vec2、Vec3、Vec4、Color)には、getValue()/setValue() ではなく、getVectorView() を使ってアクセスする必要があります。誤ったアクセサーを使用すると、EliCS は実行時にエラーをスローします。

エンティティの作成

エンティティをインスタンス化し、コンポーネントを追加します。

const unit = world.createEntity()
unit.addComponent(Position, { value: [10, 20, 30] })
unit.addComponent(Velocity)
unit.addComponent(Health)
unit.addComponent(Unit, {
  type: UnitType.Infantry,
  state: CombatState.Moving,
  damage: 15,
  armor: 20,
  morale: 0.8,
})

// Reading and writing scalar values
const damage = unit.getValue(Unit, 'damage') // returns number
unit.setValue(Unit, 'state', CombatState.Attacking)

// Reading and writing vector values (returns TypedArray subarray)
const pos = unit.getVectorView(Position, 'value')
pos[0] = 100 // Modify X directly

// Check component existence
if (unit.hasComponent(Health)) {
  /* ... */
}

// Remove component or destroy entity
unit.removeComponent(Velocity)
unit.destroy() // entity.active becomes false

システムの作成

システムには、主要なロジックを記述します。

import { createSystem, Entity } from 'elics'
import { Object3D } from 'three'
import { scene } from './scene.ts'
import { unitModel } from './unit-model.ts'

const movementQueryConfig = {
  movables: { required: [Position, Velocity] },
}

export class MovementSystem extends createSystem(movementQueryConfig) {
  update(delta: number, time: number) {
    this.queries.movables.entities.forEach((entity: Entity) => {
      const position = entity.getVectorView(Position, 'value')
      const velocity = entity.getVectorView(Velocity, 'value')
      position[0] += velocity[0] * delta
      position[1] += velocity[1] * delta
      position[2] += velocity[2] * delta
    })
  }
}

const unitViewQueryConfig = {
  units: { required: [Unit, Position] },
}

export class UnitViewSystem extends createSystem(unitViewQueryConfig) {
  public readonly models = new Map<Entity, Object3D>()

  init() {
    const create = (entity: Entity) => {
      const newModel = unitModel.clone()
      scene.add(newModel)
      this.models.set(entity, newModel)
    }
    const destroy = (entity: Entity) => {
      const oldModel = this.models.get(entity)!
      scene.remove(oldModel)
      this.models.delete(entity)
    }
    // Loop through existing and subscribe to new query entities
    this.queries.units.entities.forEach(create)
    this.queries.units.subscribe('qualify', create)
    this.queries.units.subscribe('disqualify', destroy)
  }

  update(delta: number, time: number) {
    this.queries.units.entities.forEach((entity) => {
      const model = this.models.get(entity)!
      const pos = entity.getVectorView(Position, 'value')
      model.position.set(pos[0], pos[1], pos[2])
    })
  }

  destroy() {
    // Cleanup when system is unregistered
  }
}

// Register with priority (lower runs first)
world.registerSystem(MovementSystem, { priority: 0 })
world.registerSystem(UnitViewSystem, { priority: 10 })

システムへのアクセス

world.getSystem(...) を使用して、登録済みのシステムインスタンスを取得します。

const enemySystem = world.getSystem(EnemySystem)!

// Access system properties and methods
enemySystem.spawnEnemy(position)

システムのリソース読み込み

このフレームワークは、registerSystem 時に init() を、world.update のたびに update() を呼び出します。load() ライフサイクルフックはありません。存在するのはこの 2 つだけです。非同期のリソース読み込みを行うには、システムに独自の load() メソッドを実装し、登録後、アニメーションループの開始前に自分で呼び出します。これは通常のメソッドであり、フレームワークのフックではありません。自分で呼び出さない限り、呼び出されることはありません。

export class EnemySystem extends createSystem(queryConfig) {
  private model!: Group

  init() {
    /* called by registerSystem() — synchronous only */
  }

  async load() {
    // your own method — you must await it yourself (see below)
    this.model = (await new GLTFLoader().loadAsync('/models/enemy.glb')).scene
  }

  update(delta: number, time: number) {
    /* called every frame by world.update */
  }
}

main.ts ではシステムを登録(これにより init() が実行)してから、ループの開始前に load() が完了するまで待機します。

world.registerSystem(EnemySystem) // runs init()
await world.getSystem(EnemySystem)!.load()
renderer.setAnimationLoop(() => { ... })

システムのライフサイクル制御

stop() と play() を使って、システムを動的に一時停止・再開します。

const enemySystem = world.getSystem(EnemySystem)!

// Pause the system (e.g., during cutscenes or menus)
enemySystem.stop()

// Check if system is paused
if (enemySystem.isPaused) {
  console.log('Enemy system is paused')
}

// Resume the system
enemySystem.play()

使用例: 会話シーン中に物理演算を一時停止し、プレイヤーが操作を再開できるようになったら再開します。

クエリ述語

where 句を使用して、コンポーネントの値でエンティティをフィルタリングします。

const queryConfig = {
  lowHealth: {
    required: [Health, Unit],
    excluded: [Target], // Exclude entities with this component
    where: [{ component: Health, key: 'value', op: 'lt', value: 30 }],
  },
}

演算子: eq(等しい)、ne(等しくない)、lt(より小さい)、le(以下)、gt(より大きい)、ge(以上)、in(配列に含まれる)、nin(配列に含まれない)。

ワールドの更新

delta と time でワールドを更新します。

function render() {
  ...
  const delta = clock.getDelta()
  ...
  world.update(delta, clock.elapsedTime)
  ...
}

コメント