Road to ECS
This is about teaching myself Entity-Component-System (ECS) pattern. My current goal is to implement a few basic features one by one to build a code snippet library, starting from just drawing things on screen. Code will be in my GitHub/road-to-ecs and also as cartridges in this blog thread.
Some personal background:
ECS is a pattern for dividing data and functionality in independent components. It fullfills the Single Responsibility Principle nicely: a functionality (= a system) can do one thing and not care about others.
The pieces of ECS are:
- Entities = "things in the world"
- Components = data
- Systems = functionality
Systems do their thing for Entities which have Components required by the System. For example we could have a creature on screen that moves according to its current speed. The creature is an entity, speed is a component and movement is the system.
I was about to write more but really, this post explains everything better than I could: gamedev.stackexchange: What is the role of “systems” in a component-based entity architecture?
One thing I've picked up so far is that I should implement a system first. Required components will reveal themselves quite naturally when coding the system.
- BBS/KatrinaKitten: Tiny ECS Framework v1.1
- BBS/selfsame: Entity Component System
- BBS/alexr: ECS POC 1 v. 0.5
- BBS: Entity Component Systems
- gamedev.stackexchange: What is the role of “systems” in a component-based entity architecture?
- Mark Jordan: Entities, components and systems
- gamedev.net/Klutzershy: Understanding Component-Entity-Systems
Hmm. The links above don't look like links but they work anyway.
My First ECS
The cartride has a state machine (states: menu, game, gameover), selfsame's ECS framework, plenty of extras from @alexr's ECS POC 2 1.0 and my constants. I also did some premature optimization (never do that, that's bad. seriously.) and changed all loops to use pairs() instead of other iterator functions just because I read that it is faster. Not everything is in ECS pattern, mostly because it was becoming too complex for my first project.
A couple of weeks after I coded ecs-topdown1 @KatrinaKitten posted her Tiny ECS Framework. Now I'm doing these "code katas" with that and limiting my scope. First, let's see how to draw some entities on screen with systems.
There are two entities: tables e_player and e_thing. Both have a few components. Finally there's two systems functions s_draw() and s_randpos(). Entities are created in init() and systems are run in update() and draw(). I also reformatted the ECS framework code to fit my style... it does not fit on PICO screen any more that nicely, sorry about that :)
Oh, the "thing" sprite and sfx 0 (not used) are made by my 5 year-old daughter :)
Open questions or things I'm still wondering a bit:
- Why are the component and system factory functions created inside "loader" functions load_components() and load_systems()? First I tried to put those to global level but run into scope/visibility problems. KatrinaKitten's code had those inside loader so I copied that instead of moving code around in file. Is visibility the only reason?
- Metatables are still a bit hazy for me... I think I get what they do but have been using them only in copied code so far.
- How to do CI/CD build for .p8 files? Could I use GitHub Actions?
- How to do releases? Using Maven just for Maven Release seems a bit much. Are there any good options with Gradle? (my obsession with VCS and release management are because of my dayjob :) )
- Why the inline code format does not work in BBS?
code blocks are OK
code is broken.
> Why are the component and system factory functions created inside "loader" functions load_components() and load_systems()? First I tried to put those to global level but run into scope/visibility problems. KatrinaKitten's code had those inside loader so I copied that instead of moving code around in file. Is visibility the only reason?
Lua is not a compiled language, it executes sequentially - if the ECS library is included after those definitions, including in a later tab like in my platformer example, the functions will not be defined yet when you try to create entities, components, or systems as globals. I wrapped the creation in "loader" functions so that I could call them from _init(), which is called after everything is executed the first time through and the constructor functions are defined. If you include the ECS library before your ECS definitions, calling them before _init() shouldn't cause any issues =)
Thanks for the confirmation! Seems like a good trick working with visibility/scope. In my first ecs_topdown1 I had to put the ECS lib first just because of that.
Controlling the Player Avatar Entity
A basic game feature is controlling a player avatar on screen. The controlled entity e_player is the only one which has the c_control component. That one is required by s_control(). c_control is an empty component with no data except name. Basically it's just a key or a flag to match s_control() requirements.
Drawing the Map
This one is a bit boring because I think map drawing can't be a system: My function draw_map(e_map, e_player) needs two entities. Question is: is there any point trying to make draw_map() a system?
I adjust camera based on player location and draw the whole map at once. The camera moves in 1 screen increments. Entity e_map stores the camera position and max x,y coordinates. Player entity e_player has the player position on map.
I added shortcut pointers to e_player and e_map. In a bigger cart those would be useful but I guess they do break the ECS pattern a bit.
- Components are 1) only data, 2) as small as possible and 3) never share data (normalized).
- Entities are only containers for components.
- Systems are logic: manipulate component data.
- Map tiles are not entities.
- MBoffin's top-down game tutorial brick wall sprite is really useful :)
Pointless ponderings: Is it a "map tile" or a "map cell"? The manual uses both but "cell" comes first. OTOH "tile based" is an established term... Opinions?
Bonus pro tip for including images in GitHub README.md
- add an Issue to the repo, eg. "README.md images"
- drag & drop the image file to the issue
- copy the resulting Markdown link pointing to https://user-images.githubusercontent.com/...
Caveat: there's no way to delete these images from GitHub storage but at least they don't clutter the repository with binary data.
There's a ton of examples and material about collision detection so I'm not going to repeat that. Mostly I read this: Owen Pellegrin: Simple Collision Detection and looked at this: mboffin.itch.io/pico8-overlap, and ended up using MBoffin's elegant function overlap().
The example code compares player entity's attempted next position with all other entities' (which have sprite and position) position in a loop and allows or denies the move based on that. There is a consideration of sprite width and height to allow non-8x8-elements.
It's nice to allow the player entity to slide along the edge of the sprite it is colliding with. To allow this I check the X and Y axis collision separately.
I don't know if there's a better way? Looping through all entities (twice) on every move seems like a waste of resources. But hey, premature optimization is the root of all evil so I'll cross that bridge when I get there.
I had the same question! Asked on discord and someone told me that I shouldn’t iterate through all entities, but through all components (or maybe all components of a specific kind, say update components vs draw components). That works if each component has a reference to its entity.
[Please log in to post a comment]