Skip to content

forge3d.ecs — Entity-Component System

The ECS (Entity-Component System) provides a data-driven architecture for building scenes with reusable, composable behaviours.


Core concepts

Concept Description
Entity An integer ID with no data of its own.
Component A data class attached to an entity (e.g. Transform, Rigidbody).
System Logic that processes all entities with a specific set of components.
EntityWorld The container that holds all entities, components, and systems.

EntityWorld

EntityWorld

ECS 월드 — 엔티티 생명주기와 컴포넌트 저장소.

create_entity

create_entity(*components: Component) -> Entity

컴포넌트 목록으로 새 엔티티를 생성한다.

destroy_entity

destroy_entity(e: Entity) -> None

엔티티와 그 컴포넌트를 모두 삭제한다.

is_alive

is_alive(e: Entity) -> bool

add_component

add_component(e: Entity, c: Component) -> None

remove_component

remove_component(e: Entity, typ: type[C]) -> None

get_component

get_component(e: Entity, typ: type[C]) -> C

has_component

has_component(e: Entity, typ: type[Component]) -> bool

query

query(*types: type[Component]) -> Iterator[tuple[Entity, ...]]

모든 지정 타입을 가진 엔티티와 컴포넌트 튜플을 순회한다.

반환: (entity, comp_type1_inst, comp_type2_inst, ...)

all_entities

all_entities() -> list[Entity]

components_of

components_of(e: Entity) -> dict[type[Component], Component]

step

step(dt: float) -> None

등록된 모든 System.update()를 순서대로 실행한다.

add_system

add_system(system: object) -> None

Built-in components

Transform dataclass

Bases: Component

6-DOF 변환 + 계층 관계.

rotation은 쿼터니언 [w, x, y, z] 형식.

local_matrix

local_matrix() -> np.ndarray

(4,4) 로컬 변환 행렬.

world_matrix

world_matrix(ew: EntityWorld) -> np.ndarray

(4,4) 월드 변환 행렬 (부모 계층 포함, 최대 깊이 64).

Rigidbody dataclass

Bases: Component

Collider dataclass

Bases: Component

MeshRenderer

Bases: Component

Visual mesh + material component for an ECS entity.

Two equivalent ways to construct:

.. code-block:: python

# Convenience: shape + size derive mesh_id automatically
f3d.MeshRenderer(shape="box", size=(1, 1, 1))
f3d.MeshRenderer(shape="sphere", size=(0.5,))

# Explicit mesh_id (advanced)
f3d.MeshRenderer(mesh_id="box_2x1x0.5", material_id="metal")

CameraComponent dataclass

Bases: Component

LightComponent dataclass

Bases: Component

Script dataclass

Bases: Component


Built-in systems

PhysicsSystem

Bases: System

Rigidbody 엔티티를 v1 World에 동기화하고 물리 스텝을 실행한다.

동작
  1. ECS Transform → v1 Body 위치 동기화
  2. v1 World.step()
  3. v1 Body 위치 → ECS Transform 역동기화

RenderSystem

Bases: System

ECS Transform + MeshRenderer → SceneSnapshot 생성.

last_snapshot 속성에 가장 최근 스냅샷을 저장한다.

ScriptSystem

Bases: System

Script 컴포넌트의 on_start/on_update 콜백을 실행한다.


Utilities

body_to_entity

body_to_entity(world: Any, body: Any, ew: EntityWorld) -> Entity

기존 v1 Body를 ECS 엔티티로 래핑한다 (파괴적 변환 없음).

Parameters:

Name Type Description Default
world Any

forge3d.World (v1 퍼사드)

required
body Any

forge3d.Body 인스턴스

required
ew EntityWorld

대상 EntityWorld

required

Returns:

Type Description
Entity

생성된 Entity ID

save_scene

save_scene(ew: EntityWorld, path: str | Path) -> None

EntityWorld를 JSON 파일로 저장한다.

load_scene

load_scene(path: str | Path) -> EntityWorld

JSON 파일에서 EntityWorld를 재구성한다.


Usage examples

Creating entities

import forge3d as f3d

ew = f3d.EntityWorld()

# A dynamic box at height 5 — pass components directly to create_entity()
box = ew.create_entity(
    f3d.Transform(position=[0, 0, 5]),
    f3d.Rigidbody(mass=1.0),
    f3d.MeshRenderer(shape="box", size=(1, 1, 1)),
    f3d.Collider(shape="box", size=(1, 1, 1)),
)

# A static ground plane — or add components after creation
ground = ew.create_entity()
ew.add_component(ground, f3d.Transform(position=[0, 0, 0]))
ew.add_component(ground, f3d.Rigidbody(mass=0))  # mass=0 → static
ew.add_component(ground, f3d.Collider(shape="box", size=(100, 100, 0.1)))

Stepping the world

for _ in range(600):           # 10 seconds at 60 Hz
    ew.step(dt=1/60)

tf = ew.get_component(box, f3d.Transform)
print(f"Box z = {tf.position[2]:.3f}")

Custom script component

class Spinner(f3d.Script):
    speed: float = 2.0

    def update(self, entity, ew, dt):
        tf = ew.get_component(entity, f3d.Transform)
        tf.rotation[2] += self.speed * dt   # yaw in place

spinner_e = ew.create_entity(f3d.Transform(), Spinner(speed=3.0))

Scripts are called by ScriptSystem every ew.step().

Query entities by components

# Find all entities that have both Rigidbody and MeshRenderer
for entity, (rb, mr) in ew.query(f3d.Rigidbody, f3d.MeshRenderer):
    print(f"Entity {entity}: mass={rb.mass}, shape={mr.shape}")

Bridge to physics World

world = f3d.World()
body = world.add_box(size=(1, 1, 1), position=(0, 0, 5), mass=1.0)

# Wrap an existing Body as an ECS entity
entity = f3d.body_to_entity(ew, body)
tf = ew.get_component(entity, f3d.Transform)
print(tf.position)   # synced from Body.position

Scene serialization

# Save current scene state
f3d.save_scene(ew, "scene.json")

# Load into a fresh EntityWorld
ew2 = f3d.EntityWorld()
f3d.load_scene(ew2, "scene.json")