Package: pecs-framework
PECSPECS is the ✨Respectably Muscled✨ Python ECS library that aims to provide a powerful, user-friendly, and fast-as-hell framework for game development.This library is the spiritual successor to my prior ECS library,ECStremity. Both this and its predecessor were inspired by the JavaScript ECS librarygeotic, created and maintained by@ddmills. I highly recommend checking out that project as well as the excellent resources cited in its README.What is ECS, you ask?Check it out!InstallationInstall the package from PyPI using pip:pip install pecs-frameworkOr grab it directly from this repository:pip install git+https://github.com/krummja/PECSUsage and ExamplesTo start flexing your PECS, import the library and set up some components. Components can be built as standard Python classes:importpecs_frameworkaspecsclassPosition(pecs.Component):"""Representation of an Entity's position in 2D space."""def__init__(self,x:int=0,y:int=0)->None:self.x=xself.y=y@propertydefxy(self)->tuple[int,int]:returnself.x,self.yAs extensions of existing components:importpecs_frameworkaspecsclassVelocity(Position):"""Representation of an Entity's velocity in 2D space."""Or as dataclasses:importpecs_frameworkaspecsfromdataclassesimportdataclass,field@dataclassclassHealth(pecs.Component):"""Representation of an Entity's health."""maximum:int=100current:int=field(init=False)def__post_init__(self)->None:self.current=self.maximumComponents can have as much or as little behavior as needed, although it is generally better to keep to a strict single-repsonsibility principle. We can even have components that have no behavior at all, representing boolean flags for queries:importpecs_frameworkaspecsclassIsFrozen(pecs.Component):"""Flag component denoting a frozen entity."""QueriesThe easiest way to build out systems is through world queries. To make a system that tracks and updates the components relevant to movement, we might query forPositionandVelocitycomponents. Because we want our entities to move, we want to exclude those marked with theIsFrozenflag. Perhaps we also want to grab only those entities that can fly throughWingsor swim throughFins:importpecs_frameworkaspecsecs=pecs.Engine()domain=ecs.create_domain("World")kinematics=domain.create_query(all_of=[Position,Velocity],any_of=[Wings,Fins],none_of=[IsFrozen],)Queries can specifyall_of,any_of, ornone_ofquantifiers. The query in the example above asks for entities that must havebothPositionandVelocity, may have (inclusive)WingsorFins, andmust nothaveIsFrozen.We can access the result set of the query and do some operation on them every loop cycle:defprocess(dt):forentityintargets.result:entity[Position].x+=entity[Velocity].x*dtentity[Position].y+=entity[Velocity].y*dtFor convenience, the library provides barebones system class that you can extend for your own purposes:importpecs_frameworkaspecsclassMovementSystem(pecs.BaseSystem):definitialize(self)->None:self.query('movable',all_of=[Position,Velocity],none_of=[IsFrozen],)defupdate(self)->None:movables=self._queriesforentityinmovables:entity[Position].x+=entity[Velocity].xentity[Position].y+=entity[Velocity].yWarning:Do not override the__init__method ofBaseSystem-- use the providedinitializemethod instead.Broadcasting Events to ComponentsComplex interactions within and among entities can be achieved by firing events on an entity. This creates anEntityEventthat looks for methods on all of the entity's methods prefixed withon_.zombie.fire_event('attack',{'target':survivor,'multiplier':1.5,})On thezombieentity, we might have attached anAttackercomponent with the following logic:classAttacker(pecs.Component):def__init__(self,strength:int)->None:self.strength=strengthdefon_attack(self,evt:pecs.EntityEvent)->pecs.EntityEvent:target:Entity=evt.data.targettarget.fire_event('damage_taken',{'amount':self.strength*evt.data.pultiplier,})evt.handle()returnevtWhen we executefire_eventwith the event nameattack, the event system will find allon_attackmethods on that entity's components. If we want the event propagation to stop at a particular component, we can callevt.handle()which will immediately break broadcasting down the component list. This means that we can potentially have any number of components respond to a specific event, although it may generally be safer to fire a secondary event to prevent ordering issues.Internally, theEntityEventclass puts together an instance of the classEventData, which provides access to the properties defined in thefire_eventcall.zombie.fire_event('attack',{'target':survivor,# <-- We defined 'target' here'multiplier':1.5# <-- and 'multiplier' here})defon_attack(self,evt:pecs.EntityEvent)->pecs.EntityEvent:target=evt.data.target# --> survivormultiplier=evt.data.multiplier# --> 1.5Actions can also be defined as a tuple and passed into thefire_eventmethod. This allows for easy abstraction over variables used in the event:attack_against=(lambdatarget:('attack',{'target':target,'multiplier':1.5}))zombie.fire_event(attack_against(survivor))Creating Entities from PrefabsPECS supports defining prefab entities with preconfigured component properties. Define prefabs as.jsonfiles and register them with the engine:{"name":"GameObject","inherit":[],"components":[{"type":"Position"},{"type":"Renderable","properties":{"ch":"?","bg":[0,0,0],}},{"type":"Noun"}]}importpecs_frameworkaspecsimportosROOTDIR=os.path.dirname(__file__)PREFABS=os.path.join(ROOTDIR,'prefabs')ecs=pecs.Engine()ecs.prefabs.register(PREFABS,'game_object')Now PECS will look for a file namedgame_object.jsonin the specified prefabs path and automatically load it for you. We can build an entity using this prefab very easily:game_object=ecs.domain.entities.create_from_prefab(template='GameObject',properties={'Position':{'x':15,'y':10,},'Renderable':{'fg':[255,0,255],},'Noun':{'text':'Test Object'}},alias='test_object_01',)Prefabs can specify other prefabs to inherit from as well. Prefabs can be defined as hierarchies of any depth and breadth. Note that properties will always be resolved from the most deeply embedded prefab to the least, overwriting with the most recent specification. If no properties are passed in the prefab or when creating from prefab, defaults from the component itself will be used.For examples, check out thetestsfolder in this repository.