Difference between revisions of "Entity Component System"
(Link to archive) |
HexDecimal (talk | contribs) (Added an example implementation) |
||
(3 intermediate revisions by 2 users not shown) | |||
Line 1: | Line 1: | ||
{{stub}} | |||
An entity component system is a way to implement your game objects so that you can build their functionality through composition instead of object-oriented inheritance. The prevailing wisdom in current game development seems to be that complex games should have one of those instead of an inheritance-based object system that is likely to lead to unmaintainable blobs of a class. A roguelike is probably a complex enough game that you should use an entity component system in one. | An entity component system is a way to implement your game objects so that you can build their functionality through composition instead of object-oriented inheritance. The prevailing wisdom in current game development seems to be that complex games should have one of those instead of an inheritance-based object system that is likely to lead to unmaintainable blobs of a class. A roguelike is probably a complex enough game that you should use an entity component system in one. | ||
Line 5: | Line 7: | ||
This makes it easy to have data-driven object construction and allows very diverse combinations of component loadouts in objects that still coexist in the same game world as the same fundamental type of program object. | This makes it easy to have data-driven object construction and allows very diverse combinations of component loadouts in objects that still coexist in the same game world as the same fundamental type of program object. | ||
== | == Minimal Implementation == | ||
This is Python example showing a minimal ECS implementation. This uses Python's dictionary types to store components in a structure similar to a sparse set.<ref>[https://skypjack.github.io/2020-08-02-ecs-baf-part-9/ Sparse sets and EnTT]</ref> This example is missing several features needed to more easily write a game with, but it does meet the definition of a vanilla ECS implementation.<ref>[https://ajmmertens.medium.com/why-vanilla-ecs-is-not-enough-d7ed4e3bebe5 Why Vanilla ECS Is Not Enough]</ref> | |||
<syntaxhighlight lang="Python"> | |||
"""Worlds smallest ECS implementation. An example to help demystify ECS. Is missing features needed for serious projects.""" | |||
from __future__ import annotations | |||
__author__ = "https://github.com/HexDecimal" | |||
__license__ = "http://creativecommons.org/publicdomain/zero/1.0" | |||
from collections import defaultdict | |||
from typing import Any, Iterable, TypeAlias, TypeVar | |||
T = TypeVar("T") | |||
EntityID: TypeAlias = object | |||
"""Entity unique ID can be any hashable object. Use ``object()`` to create a new entity.""" | |||
ComponentType: TypeAlias = type[T] | |||
"""The type-hint for a component type. Can be any runtime type-hint such as a custom class.""" | |||
class Registry: | |||
"""A minimal unoptimized spare-set ECS registry tracking which components are held by which entities. | |||
Example:: | |||
>>> registry = Registry() | |||
>>> entity = object() # new unique id | |||
>>> registry.has_component(entity, int) | |||
False | |||
>>> registry.set_component(entity, int, 10) | |||
>>> registry.has_component(entity, int) | |||
True | |||
>>> registry.get_component(entity, int) | |||
10 | |||
>>> registry.set_component(entity, str, "foo") | |||
>>> for e in registry.query([str]): | |||
... str_value = registry.get_component(e, str) | |||
... registry.set_component(e, str, str_value + "bar") | |||
>>> registry.get_component(entity, str) | |||
'foobar' | |||
""" | |||
def __init__(self) -> None: | |||
"""Initialize a new registry with no components or entities.""" | |||
self._components_by_entity_by_type: defaultdict[ComponentType[Any], dict[EntityID, Any]] = defaultdict(dict) | |||
"""Used for the storage and random access of entity component values. | |||
Keys of inner dict are used to track which entities have which components, this is used for queries. | |||
Internal syntax is: ``_components_by_entity_by_type[ComponentType][EntityID] = component_value`` | |||
""" | |||
def set_component(self, entity: EntityID, component_type: ComponentType[T], value: T) -> None: | |||
"""Assign `value` to the `component_type` held by `entity`.""" | |||
self._components_by_entity_by_type[component_type][entity] = value | |||
def remove_component(self, entity: EntityID, component_type: ComponentType[Any]) -> None: | |||
"""Delete the `component_type` from `entity`. | |||
Raises `KeyError` if `component_type` does not exist. | |||
""" | |||
del self._components_by_entity_by_type[component_type][entity] | |||
def get_component(self, entity: EntityID, component_type: ComponentType[T]) -> T: | |||
"""Return the value of the `component_type` held by `entity`. | |||
Raises `KeyError` if `component_type` is missing. | |||
""" | |||
return self._components_by_entity_by_type[component_type][entity] # type: ignore[no-any-return] | |||
def has_component(self, entity: EntityID, component_type: ComponentType[Any]) -> bool: | |||
"""Return True if an `entity` has a `component_type`.""" | |||
return entity in self._components_by_entity_by_type[component_type] | |||
def query(self, component_types: Iterable[ComponentType[Any]]) -> set[EntityID]: | |||
"""Return a set of entities which have all `component_types`.""" | |||
# Collect the keys of the inner dict which act like a set of entities, grouped for each component type | |||
entities_grouped_by_type = [ | |||
self._components_by_entity_by_type[component_type].keys() for component_type in component_types | |||
] | |||
if not entities_grouped_by_type: | |||
raise ValueError("'components' must have at least one item.") | |||
# The matching entities are the intersection of all sets held by entities_for_components | |||
# Note that typical ECS implementations will cache this result until relevant components are added or removed | |||
matching_entities = set(entities_grouped_by_type.pop()) | |||
matching_entities.intersection_update(*entities_grouped_by_type) | |||
return matching_entities | |||
</syntaxhighlight> | |||
== External Links == | |||
* Good overview and links on Stack Overflow: http://stackoverflow.com/questions/1901251/component-based-game-engine-design | * Good overview and links on Stack Overflow: http://stackoverflow.com/questions/1901251/component-based-game-engine-design | ||
Line 19: | Line 113: | ||
* [http://gameprogrammingpatterns.com/component.html Game Programming Patterns: Component], a straightforward explanation of component based architecture, along with example implementation. | * [http://gameprogrammingpatterns.com/component.html Game Programming Patterns: Component], a straightforward explanation of component based architecture, along with example implementation. | ||
* [https://www.youtube.com/watch?v=U03XXzcThGU Brian Bucklew: Data-Driven Engines of Qud and Sproggiwood], IRDC 2015 talk about a data- and component-driven roguelike architecture. | |||
* [https://kyren.github.io/2018/09/14/rustconf-talk.html Catherine West: RustConf 2018 Closing Keynote: Using Rust for Game Development] ([https://www.youtube.com/watch?v=aKLntZcp27M video], [https://kyren.github.io/rustconf_2018_slides/index.html slides]), works through various alternatives for game engine structure in Rust and ends up at an ECS architecture as the best option for a medium to large game in Rust. | |||
* [https://github.com/SanderMertens/ecs-faq Sander Mertens Entity Component System FAQ] | |||
== References == | |||
<references /> | |||
[[Category:Articles]] | |||
[[Category:Developing]] | [[Category:Developing]] |
Latest revision as of 13:42, 22 February 2024
- This page is a stub. Please help RogueBasin by expanding it. Click here to edit this page.
An entity component system is a way to implement your game objects so that you can build their functionality through composition instead of object-oriented inheritance. The prevailing wisdom in current game development seems to be that complex games should have one of those instead of an inheritance-based object system that is likely to lead to unmaintainable blobs of a class. A roguelike is probably a complex enough game that you should use an entity component system in one.
The basic idea is that game objects at their core are just unique identifiers (UIDs), plain strings or integers, instead of complex objects. The complex data and functionality is moved into multiple components, which are stored in their own containers and associated with the object UIDs.
This makes it easy to have data-driven object construction and allows very diverse combinations of component loadouts in objects that still coexist in the same game world as the same fundamental type of program object.
Minimal Implementation
This is Python example showing a minimal ECS implementation. This uses Python's dictionary types to store components in a structure similar to a sparse set.[1] This example is missing several features needed to more easily write a game with, but it does meet the definition of a vanilla ECS implementation.[2]
"""Worlds smallest ECS implementation. An example to help demystify ECS. Is missing features needed for serious projects."""
from __future__ import annotations
__author__ = "https://github.com/HexDecimal"
__license__ = "http://creativecommons.org/publicdomain/zero/1.0"
from collections import defaultdict
from typing import Any, Iterable, TypeAlias, TypeVar
T = TypeVar("T")
EntityID: TypeAlias = object
"""Entity unique ID can be any hashable object. Use ``object()`` to create a new entity."""
ComponentType: TypeAlias = type[T]
"""The type-hint for a component type. Can be any runtime type-hint such as a custom class."""
class Registry:
"""A minimal unoptimized spare-set ECS registry tracking which components are held by which entities.
Example::
>>> registry = Registry()
>>> entity = object() # new unique id
>>> registry.has_component(entity, int)
False
>>> registry.set_component(entity, int, 10)
>>> registry.has_component(entity, int)
True
>>> registry.get_component(entity, int)
10
>>> registry.set_component(entity, str, "foo")
>>> for e in registry.query([str]):
... str_value = registry.get_component(e, str)
... registry.set_component(e, str, str_value + "bar")
>>> registry.get_component(entity, str)
'foobar'
"""
def __init__(self) -> None:
"""Initialize a new registry with no components or entities."""
self._components_by_entity_by_type: defaultdict[ComponentType[Any], dict[EntityID, Any]] = defaultdict(dict)
"""Used for the storage and random access of entity component values.
Keys of inner dict are used to track which entities have which components, this is used for queries.
Internal syntax is: ``_components_by_entity_by_type[ComponentType][EntityID] = component_value``
"""
def set_component(self, entity: EntityID, component_type: ComponentType[T], value: T) -> None:
"""Assign `value` to the `component_type` held by `entity`."""
self._components_by_entity_by_type[component_type][entity] = value
def remove_component(self, entity: EntityID, component_type: ComponentType[Any]) -> None:
"""Delete the `component_type` from `entity`.
Raises `KeyError` if `component_type` does not exist.
"""
del self._components_by_entity_by_type[component_type][entity]
def get_component(self, entity: EntityID, component_type: ComponentType[T]) -> T:
"""Return the value of the `component_type` held by `entity`.
Raises `KeyError` if `component_type` is missing.
"""
return self._components_by_entity_by_type[component_type][entity] # type: ignore[no-any-return]
def has_component(self, entity: EntityID, component_type: ComponentType[Any]) -> bool:
"""Return True if an `entity` has a `component_type`."""
return entity in self._components_by_entity_by_type[component_type]
def query(self, component_types: Iterable[ComponentType[Any]]) -> set[EntityID]:
"""Return a set of entities which have all `component_types`."""
# Collect the keys of the inner dict which act like a set of entities, grouped for each component type
entities_grouped_by_type = [
self._components_by_entity_by_type[component_type].keys() for component_type in component_types
]
if not entities_grouped_by_type:
raise ValueError("'components' must have at least one item.")
# The matching entities are the intersection of all sets held by entities_for_components
# Note that typical ECS implementations will cache this result until relevant components are added or removed
matching_entities = set(entities_grouped_by_type.pop())
matching_entities.intersection_update(*entities_grouped_by_type)
return matching_entities
External Links
- Good overview and links on Stack Overflow: http://stackoverflow.com/questions/1901251/component-based-game-engine-design
- A Handful of Components, descriptions of some useful components by Michael A. Carr-Robb-John
- Entity Systems Project, an entity component system wiki
- Entity Systems: what makes good Components? good Entities?, a post describing an architecture that has both entity components and global system objects for the game state
- Case Study: Bomberman Mechanics in an Entity-Component-System, a post on actually implementing a small ECS.
- Game Programming Patterns: Component, a straightforward explanation of component based architecture, along with example implementation.
- Brian Bucklew: Data-Driven Engines of Qud and Sproggiwood, IRDC 2015 talk about a data- and component-driven roguelike architecture.
- Catherine West: RustConf 2018 Closing Keynote: Using Rust for Game Development (video, slides), works through various alternatives for game engine structure in Rust and ends up at an ECS architecture as the best option for a medium to large game in Rust.