microecs.entity
entity.py - A view of an entity with all its fields from the pool it belongs to in the world
1"""entity.py - A view of an entity with all its fields from the pool it belongs to in the world""" 2from __future__ import annotations 3from dataclasses import fields 4from typing import Any, Iterable 5import numpy as np 6from .pool import Pool 7from .component import ComponentType 8from .utils import EntityId 9from .command_buffer import CommandBuffer, Command, CommandType 10 11# Note: if Entity gets new fields, add them here! Otherwise the user code may overwrite them e.g. ent._eid_to_pool_ix=xx 12_ENTITY_INTERNAL_ATTRS = {"entity_id", "_eid_to_pool_ix", "_pool_to_components", "_world_command_buffer"} 13 14class Entity: 15 """ 16 A view of an entity with all its fields from the pool it belongs to in the world. 17 Note: Consistent to internal pool changes, however it always must check where it belongs so it's slow!! 18 """ 19 def __init__(self, entity_id: EntityId, eid_to_pool_ix: dict[EntityId, tuple[Pool, int]], 20 pool_to_components: dict[Pool, list[ComponentType]], world_command_buffer: CommandBuffer): 21 self.entity_id = entity_id 22 self._eid_to_pool_ix = eid_to_pool_ix 23 self._pool_to_components = pool_to_components 24 self._world_command_buffer = world_command_buffer # the world command buffer, needed for add/remove_component 25 26 def add_component(self, component: ComponentType, **kwargs): 27 """Adds a component to an entity. Component data is sent to kwargs. Lazy; call world.update()""" 28 self._world_command_buffer.append(Command(CommandType.ADD_COMPONENT, self.entity_id, 29 args={"component": component, **kwargs})) 30 31 def remove_component(self, component: ComponentType): 32 """Removes a component from an entity given its id. Lazy; call update()""" 33 self._world_command_buffer.append(Command(CommandType.REMOVE_COMPONENT, self.entity_id, 34 args={"component": component})) 35 36 def has_component(self, component: ComponentType) -> bool: 37 """Checks if this entity has a component""" 38 return component in self.get_components() 39 40 def get_components(self) -> list[ComponentType]: 41 """get the components of this entity. Note: they may change, so call this every time, don't store it""" 42 pool, _ = self._locate(names=[]) 43 return self._pool_to_components[pool] 44 45 def get_fields(self) -> set[str]: 46 """gets the fields of this entity. Note: they may change, so call this every time, don't store it.""" 47 pool, _ = self._locate(names=[]) 48 return pool.fields_set 49 50 def set_data(self, **data): 51 """Eagerly (no world.update()) sets the data of this entity. Multiple columns can be updated at once e.g. 52 entity.set_data(a=x, b=y). Checks are done so object doesn't crash midway (e.g. bad dtype/shape etc.).""" 53 pool, pool_index = self._locate(names=data.keys()) 54 55 if len(data) == 1: # fast path because we don't need to do exhaustive tests on a single k->v data set 56 k, v = next(iter(data.items())) 57 pool.data[k][pool_index] = v 58 return 59 60 # convert the data in numpy array of proper shape before calling pool.data[k][ix]=v so we don't have to revert 61 ready: list[tuple[str, np.ndarray]] = [] 62 for k, v in data.items(): 63 col = pool.data[k] 64 # no need to call the slow np.broadcast_to if v is already in proper shape and broadcastable and all. 65 if not (isinstance(v, np.ndarray) and v.dtype == col.dtype and v.shape == col.shape[1:]): 66 v = np.broadcast_to(np.asarray(v, dtype=col.dtype), col.shape[1:]) 67 ready.append((k, v)) 68 69 # finally set the data after the conversion was done 70 for k, v in ready: 71 pool.data[k][pool_index] = v 72 73 def _locate(self, names: Iterable[str]) -> tuple[Pool, int]: 74 try: 75 pool, index = self._eid_to_pool_ix[self.entity_id] 76 except KeyError: 77 raise AttributeError(f"Entity {self.entity_id} not in world. Call `world.update()` if it was just added.") 78 79 if not (flds := pool.fields_set).issuperset(names): 80 raise AttributeError(f"Not all of {list(names)} are fields (entity id: {self.entity_id}). " 81 f"\n- Components: {[c.__name__ for c in self.get_components()]}\n- Fields: {flds}") 82 return pool, index 83 84 def to_dict(self, serialization_field: str | None = None) -> dict[str, Any]: 85 """ 86 Serializes a single entity. Assumes fields are numpy. numerics are converted via `.tolist()`. objects are 87 converted via `.item()`. 88 Parameters: 89 - `serialization_field` An optional special field added at World-level (e.g.: 'serializable'). If set, then we 90 only serialize this entity's fields where the serialization_field is True. If not set, all fields are dumped. 91 """ 92 components = self.get_components() 93 res = {"components": [c.__name__ for c in components], "data": {}} 94 for component in components: 95 for field in fields(component): 96 # the magic key that we have added in extra_metadata at World level. If not set, all fields are dumped. 97 if serialization_field is not None and field.metadata[serialization_field] is False: 98 continue 99 if field.metadata["dtype"] == "object": # dtype=object is for... non-numeric data (mostly dicts) 100 res["data"][field.name] = self.__getattr__(field.name).item() 101 else: 102 res["data"][field.name] = self.__getattr__(field.name).tolist() 103 return res 104 105 def __getattr__(self, name: str) -> np.ndarray: 106 try: # note: try/catch is cheaper than self._locate(name=[names]) 107 pool, index = self._eid_to_pool_ix[self.entity_id] 108 return pool.data[name][index] 109 except KeyError: 110 if self.entity_id not in self._eid_to_pool_ix: 111 raise AttributeError(f"Entity {self.entity_id} not in world. Perhaps call `world.update()`.") 112 raise AttributeError(f"Entity {self.entity_id} may not have field: {name} (fields: {self.get_fields()})") 113 114 def __setattr__(self, name: str, value: np.ndarray): 115 if name in _ENTITY_INTERNAL_ATTRS: 116 super().__setattr__(name, value) 117 return 118 119 try: # note: try/catch is cheaper than self._locate(name=[names]) 120 pool, index = self._eid_to_pool_ix[self.entity_id] 121 pool.data[name][index] = value 122 except KeyError: 123 if self.entity_id not in self._eid_to_pool_ix: 124 raise AttributeError(f"Entity {self.entity_id} not in world. Perhaps call `world.update()`.") 125 raise AttributeError(f"Entity {self.entity_id} may not have field: {name} (fields: {self.get_fields()})") 126 127 def __reduce__(self): 128 raise TypeError("Entity is a live view into the world's pools; it cannot be copied or pickled. " 129 "Use entity.to_dict(), or serialize the world.") 130 131 def __repr__(self): 132 return f"EID-{self.entity_id}" 133 134ENTITY_RESERVED_NAMES = _ENTITY_INTERNAL_ATTRS | {n for n in vars(Entity) if not n.startswith("__")}
15class Entity: 16 """ 17 A view of an entity with all its fields from the pool it belongs to in the world. 18 Note: Consistent to internal pool changes, however it always must check where it belongs so it's slow!! 19 """ 20 def __init__(self, entity_id: EntityId, eid_to_pool_ix: dict[EntityId, tuple[Pool, int]], 21 pool_to_components: dict[Pool, list[ComponentType]], world_command_buffer: CommandBuffer): 22 self.entity_id = entity_id 23 self._eid_to_pool_ix = eid_to_pool_ix 24 self._pool_to_components = pool_to_components 25 self._world_command_buffer = world_command_buffer # the world command buffer, needed for add/remove_component 26 27 def add_component(self, component: ComponentType, **kwargs): 28 """Adds a component to an entity. Component data is sent to kwargs. Lazy; call world.update()""" 29 self._world_command_buffer.append(Command(CommandType.ADD_COMPONENT, self.entity_id, 30 args={"component": component, **kwargs})) 31 32 def remove_component(self, component: ComponentType): 33 """Removes a component from an entity given its id. Lazy; call update()""" 34 self._world_command_buffer.append(Command(CommandType.REMOVE_COMPONENT, self.entity_id, 35 args={"component": component})) 36 37 def has_component(self, component: ComponentType) -> bool: 38 """Checks if this entity has a component""" 39 return component in self.get_components() 40 41 def get_components(self) -> list[ComponentType]: 42 """get the components of this entity. Note: they may change, so call this every time, don't store it""" 43 pool, _ = self._locate(names=[]) 44 return self._pool_to_components[pool] 45 46 def get_fields(self) -> set[str]: 47 """gets the fields of this entity. Note: they may change, so call this every time, don't store it.""" 48 pool, _ = self._locate(names=[]) 49 return pool.fields_set 50 51 def set_data(self, **data): 52 """Eagerly (no world.update()) sets the data of this entity. Multiple columns can be updated at once e.g. 53 entity.set_data(a=x, b=y). Checks are done so object doesn't crash midway (e.g. bad dtype/shape etc.).""" 54 pool, pool_index = self._locate(names=data.keys()) 55 56 if len(data) == 1: # fast path because we don't need to do exhaustive tests on a single k->v data set 57 k, v = next(iter(data.items())) 58 pool.data[k][pool_index] = v 59 return 60 61 # convert the data in numpy array of proper shape before calling pool.data[k][ix]=v so we don't have to revert 62 ready: list[tuple[str, np.ndarray]] = [] 63 for k, v in data.items(): 64 col = pool.data[k] 65 # no need to call the slow np.broadcast_to if v is already in proper shape and broadcastable and all. 66 if not (isinstance(v, np.ndarray) and v.dtype == col.dtype and v.shape == col.shape[1:]): 67 v = np.broadcast_to(np.asarray(v, dtype=col.dtype), col.shape[1:]) 68 ready.append((k, v)) 69 70 # finally set the data after the conversion was done 71 for k, v in ready: 72 pool.data[k][pool_index] = v 73 74 def _locate(self, names: Iterable[str]) -> tuple[Pool, int]: 75 try: 76 pool, index = self._eid_to_pool_ix[self.entity_id] 77 except KeyError: 78 raise AttributeError(f"Entity {self.entity_id} not in world. Call `world.update()` if it was just added.") 79 80 if not (flds := pool.fields_set).issuperset(names): 81 raise AttributeError(f"Not all of {list(names)} are fields (entity id: {self.entity_id}). " 82 f"\n- Components: {[c.__name__ for c in self.get_components()]}\n- Fields: {flds}") 83 return pool, index 84 85 def to_dict(self, serialization_field: str | None = None) -> dict[str, Any]: 86 """ 87 Serializes a single entity. Assumes fields are numpy. numerics are converted via `.tolist()`. objects are 88 converted via `.item()`. 89 Parameters: 90 - `serialization_field` An optional special field added at World-level (e.g.: 'serializable'). If set, then we 91 only serialize this entity's fields where the serialization_field is True. If not set, all fields are dumped. 92 """ 93 components = self.get_components() 94 res = {"components": [c.__name__ for c in components], "data": {}} 95 for component in components: 96 for field in fields(component): 97 # the magic key that we have added in extra_metadata at World level. If not set, all fields are dumped. 98 if serialization_field is not None and field.metadata[serialization_field] is False: 99 continue 100 if field.metadata["dtype"] == "object": # dtype=object is for... non-numeric data (mostly dicts) 101 res["data"][field.name] = self.__getattr__(field.name).item() 102 else: 103 res["data"][field.name] = self.__getattr__(field.name).tolist() 104 return res 105 106 def __getattr__(self, name: str) -> np.ndarray: 107 try: # note: try/catch is cheaper than self._locate(name=[names]) 108 pool, index = self._eid_to_pool_ix[self.entity_id] 109 return pool.data[name][index] 110 except KeyError: 111 if self.entity_id not in self._eid_to_pool_ix: 112 raise AttributeError(f"Entity {self.entity_id} not in world. Perhaps call `world.update()`.") 113 raise AttributeError(f"Entity {self.entity_id} may not have field: {name} (fields: {self.get_fields()})") 114 115 def __setattr__(self, name: str, value: np.ndarray): 116 if name in _ENTITY_INTERNAL_ATTRS: 117 super().__setattr__(name, value) 118 return 119 120 try: # note: try/catch is cheaper than self._locate(name=[names]) 121 pool, index = self._eid_to_pool_ix[self.entity_id] 122 pool.data[name][index] = value 123 except KeyError: 124 if self.entity_id not in self._eid_to_pool_ix: 125 raise AttributeError(f"Entity {self.entity_id} not in world. Perhaps call `world.update()`.") 126 raise AttributeError(f"Entity {self.entity_id} may not have field: {name} (fields: {self.get_fields()})") 127 128 def __reduce__(self): 129 raise TypeError("Entity is a live view into the world's pools; it cannot be copied or pickled. " 130 "Use entity.to_dict(), or serialize the world.") 131 132 def __repr__(self): 133 return f"EID-{self.entity_id}"
A view of an entity with all its fields from the pool it belongs to in the world. Note: Consistent to internal pool changes, however it always must check where it belongs so it's slow!!
20 def __init__(self, entity_id: EntityId, eid_to_pool_ix: dict[EntityId, tuple[Pool, int]], 21 pool_to_components: dict[Pool, list[ComponentType]], world_command_buffer: CommandBuffer): 22 self.entity_id = entity_id 23 self._eid_to_pool_ix = eid_to_pool_ix 24 self._pool_to_components = pool_to_components 25 self._world_command_buffer = world_command_buffer # the world command buffer, needed for add/remove_component
27 def add_component(self, component: ComponentType, **kwargs): 28 """Adds a component to an entity. Component data is sent to kwargs. Lazy; call world.update()""" 29 self._world_command_buffer.append(Command(CommandType.ADD_COMPONENT, self.entity_id, 30 args={"component": component, **kwargs}))
Adds a component to an entity. Component data is sent to kwargs. Lazy; call world.update()
32 def remove_component(self, component: ComponentType): 33 """Removes a component from an entity given its id. Lazy; call update()""" 34 self._world_command_buffer.append(Command(CommandType.REMOVE_COMPONENT, self.entity_id, 35 args={"component": component}))
Removes a component from an entity given its id. Lazy; call update()
37 def has_component(self, component: ComponentType) -> bool: 38 """Checks if this entity has a component""" 39 return component in self.get_components()
Checks if this entity has a component
41 def get_components(self) -> list[ComponentType]: 42 """get the components of this entity. Note: they may change, so call this every time, don't store it""" 43 pool, _ = self._locate(names=[]) 44 return self._pool_to_components[pool]
get the components of this entity. Note: they may change, so call this every time, don't store it
46 def get_fields(self) -> set[str]: 47 """gets the fields of this entity. Note: they may change, so call this every time, don't store it.""" 48 pool, _ = self._locate(names=[]) 49 return pool.fields_set
gets the fields of this entity. Note: they may change, so call this every time, don't store it.
51 def set_data(self, **data): 52 """Eagerly (no world.update()) sets the data of this entity. Multiple columns can be updated at once e.g. 53 entity.set_data(a=x, b=y). Checks are done so object doesn't crash midway (e.g. bad dtype/shape etc.).""" 54 pool, pool_index = self._locate(names=data.keys()) 55 56 if len(data) == 1: # fast path because we don't need to do exhaustive tests on a single k->v data set 57 k, v = next(iter(data.items())) 58 pool.data[k][pool_index] = v 59 return 60 61 # convert the data in numpy array of proper shape before calling pool.data[k][ix]=v so we don't have to revert 62 ready: list[tuple[str, np.ndarray]] = [] 63 for k, v in data.items(): 64 col = pool.data[k] 65 # no need to call the slow np.broadcast_to if v is already in proper shape and broadcastable and all. 66 if not (isinstance(v, np.ndarray) and v.dtype == col.dtype and v.shape == col.shape[1:]): 67 v = np.broadcast_to(np.asarray(v, dtype=col.dtype), col.shape[1:]) 68 ready.append((k, v)) 69 70 # finally set the data after the conversion was done 71 for k, v in ready: 72 pool.data[k][pool_index] = v
Eagerly (no world.update()) sets the data of this entity. Multiple columns can be updated at once e.g. entity.set_data(a=x, b=y). Checks are done so object doesn't crash midway (e.g. bad dtype/shape etc.).
85 def to_dict(self, serialization_field: str | None = None) -> dict[str, Any]: 86 """ 87 Serializes a single entity. Assumes fields are numpy. numerics are converted via `.tolist()`. objects are 88 converted via `.item()`. 89 Parameters: 90 - `serialization_field` An optional special field added at World-level (e.g.: 'serializable'). If set, then we 91 only serialize this entity's fields where the serialization_field is True. If not set, all fields are dumped. 92 """ 93 components = self.get_components() 94 res = {"components": [c.__name__ for c in components], "data": {}} 95 for component in components: 96 for field in fields(component): 97 # the magic key that we have added in extra_metadata at World level. If not set, all fields are dumped. 98 if serialization_field is not None and field.metadata[serialization_field] is False: 99 continue 100 if field.metadata["dtype"] == "object": # dtype=object is for... non-numeric data (mostly dicts) 101 res["data"][field.name] = self.__getattr__(field.name).item() 102 else: 103 res["data"][field.name] = self.__getattr__(field.name).tolist() 104 return res
Serializes a single entity. Assumes fields are numpy. numerics are converted via .tolist(). objects are
converted via .item().
Parameters:
serialization_fieldAn optional special field added at World-level (e.g.: 'serializable'). If set, then we only serialize this entity's fields where the serialization_field is True. If not set, all fields are dumped.