Resources is a type-keyed dependency injection container for world-level shared state. It holds configuration, services, and context that processors need but that is not entity data.
class Resources:
def insert(self, resource: T) -> None: ...
def get(self, resource_type: type[T]) -> T | None: ...
def require(self, resource_type: type[T]) -> T: ...
def remove(self, resource_type: type[T]) -> T | None: ...
def contains(self, resource_type: type[T]) -> bool: ...
def items(self) -> ItemsView[type, object]: ...
How It Works¶
Resources are the shared state layer of the ECS. Where Components hold per-entity data in DataFrame columns, Resources hold per-world singletons that live outside the columnar storage path entirely.
The container is a dict[type, object] -- each resource is keyed by its concrete Python type. This means there is exactly one instance of any given type at a time. Inserting a second instance of the same type overwrites the first.
Every AsyncWorld owns a .resources instance. During tick execution, AsyncSystem.execute() passes it into each processor's process() method as a keyword argument.
world.resources.insert(SimConfig(...))
|
AsyncSystem.execute(resources=world.resources)
|
processor.process(df, resources=resources, tick=tick)
API¶
insert¶
Store a resource, keyed by its type:
world.resources.insert(SimConfig(gravity=9.8))
world.resources.insert(broker)
Calling insert() with a second instance of the same type replaces the first.
get¶
Retrieve a resource by type, returning None if absent:
broker = resources.get(CommandBroker)
if broker:
await broker.enqueue(world_id, cmd)
require¶
Retrieve a resource by type, raising KeyError if absent:
config = resources.require(SimConfig)
Use require() when the processor cannot function without the resource. Use get() when the resource is optional.
remove¶
Remove and return a resource, or None if not present:
old_config = resources.remove(SimConfig)
contains¶
Check whether a resource type is registered. Also supports the in operator:
resources.contains(SimConfig) # True
SimConfig in resources # True
What Goes in Resources¶
Resources are not entity data. They are the scaffolding around it:
| Category | Examples |
|---|---|
| Environment parameters | SimConfig(gravity=9.8), PhysicsConfig(...) |
| Shared services | CommandBroker, LabelingConfig |
| Simulation context | SamplingConfig, budget trackers |
In RL terms: MDP parameters, hyperparameters, shared infrastructure.
Usage in Processors¶
Processors receive resources as a keyword argument in process():
class PhysicsProcessor(AsyncProcessor):
components = (Position, Velocity)
priority = 5
async def process(self, df: DataFrame, resources: Resources = None, **kwargs) -> DataFrame:
config = resources.require(SimConfig) if resources else SimConfig()
return df.with_column(
"velocity__vy",
col("velocity__vy") - config.gravity,
)
Submitting Commands from Processors¶
Processors can access the CommandBroker through resources to submit commands (spawn entities, send messages) from within the simulation loop:
class SpawnerProcessor(AsyncProcessor):
components = (Agent,)
priority = 50
async def process(self, df, resources=None, tick=0, **kwargs):
broker = resources.get(CommandBroker) if resources else None
if broker:
cmd = Command(
type=CommandType.SPAWN,
tick=tick,
payload={"components": [Agent(name="child").to_payload()]},
)
await broker.enqueue("my_world", cmd)
return df
World Forking¶
When a world is forked, resources are copied to the new world. This lets you override parameters per-fork for counterfactual experiments:
fork = await world.fork()
fork.resources.insert(PhysicsConfig(gravity=0.0)) # zero-G variant
The fork runs with its own resource set while the original world is unaffected.
Source Reference¶
Resources: src/archetype/core/resources.py