WIP: mach<->ECS integration, ECS type safety, high-level application API proposal #349
No reviewers
Labels
No labels
CI
all
basisu
blog
bug
build
contributor-friendly
core
correctness
deferred
dev
direct3d-headers
docs
driver-os-issue
duplicate
dxcompiler
editor
examples
experiment
feature-idea
feedback
flac
freetype
gamemode
gkurve
glfw
gpu
gpu-dawn
harfbuzz
help welcome
in-progress
infrastructure
invalid
libmach
linux-audio-headers
long-term
mach
mach.gfx
mach.math
mach.physics
mach.testing
model3d
needs-triage
object
opengl-headers
opus
os/linux
os/macos
os/wasm
os/windows
package-manager
priority
proposal
proposal-accepted
question
roadmap
slipped
stability
sysaudio
sysgpu
sysjs
validating-fix
vulkan-zig-generated
wayland-headers
website
wontfix
wrench
www
x11-headers
xcode-frameworks
zig-update
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
hexops/mach!349
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "sg/ecs-type-safety-1"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Context
I've been thinking non-stop about, and exploring, a lot of overlapping questions:
mach/ecsinto the higher-levelmach/src/API look like?rendererandphysics2dmodules both want to provide a "location" component of a different type (vec3vsvec2) - how do we handle that? Is there a convention of prefixing component names like "renderer.vec3", or do we do something else?Painting the whole picture (or at least 60% of it)
The thing that hit me like an overwhelming bag of bricks is that there are just so many different ways we can tackle these problems. Like, literally, hundreds if not thousands of combinations. And each choice has far-reaching implications for other parts of the system. If we don't consider them in total, all combined together, we're not seeing the whole picture we're painting.
Two good examples of this:
ecs.Entities(T)if we were to try and tackle type-safety that way.fn serialize(component: *anyopaque, writer: anytype) !voidfunction - but do we want that?This PR
This is an exploratory PR, if we're happy with how this looks/feels generally, then I'll work on sending separate smaller/cleaner PRs to land this same idea into
main. It's not 100% thought out, and so it wouldn't be merged directly.This PR takes several ideas from everyone I've talked to: Levy, MasterQ32, Ayush, Ali, Zargio, and more into account.
A standard high-level Mach application
Now looks like this:
Where
physics2d.modulelooks like this:First impressions
The first thing you'll notice is that:
modulesup-front, at compile-time. You'll have to write out all of the modules you intend to use in your application, and each module has to write out all of the ECS components it intends to use.pub const App = mach.App(modules, init);, more on this below..setComponent(player, .renderer, .location, .{.x = 0, .y = 0, .z = 0})find a component called.locationin the.renderermodule and set that component on the entity..renderer, .locationparameters it knows the exact type the component must be at comptime. Similarly,.getComponent(player, .renderer, .location)returns the concrete type.I highly encourage reading the examples/ecs-app source as it explains in greater detail some other concepts like namespacing of modules.
The type-safety/explicitness vs. flexibility tradeoff
There is a very real tradeoff here around type-safety: requiring modules to be declared up-front, requiring components you might use to be enumerated by modules, etc. can be tedious, it adds some real overhead.
For example, modules must live in a namespace. And if two modules have the same namespace, we need to let the consumer of them rename them. This is fine, it's just a comptime parameter
namespace: anytypebut it means all of your module code that intends to work with the ECS needs to have thatnamespace: anytypeparameter so it can pass it to.setComponent,.getComponent, etc.:All of this adds a real level of mental overhead for the programmer. It will be stuff to learn, and will make writing a Mach application out-of-the-box harder. Is it worth it?
Initially, my impression was no. I actually set out to create this PR as evidence and proof that it is not worth it for when anyone complains in the future. But, after implementing and reflecting on all aspects it actually seems quite nice.
It's possible I change my mind again later, but it seems fair to go with the most restrictive approach first (you have to define all modules/components globally), try and push that as far as we can and make it as nice an API as we can, and go to a less restrictive one later if we truly need to.
Unimplemented aspects
Not implemented in this PR is a few important things:
Initialization
Module initialization is something I haven't implemented yet. This would be easy to handle, though, because we have all the type information. For example, we could call an
initfunction defined in the module and require it return a valid type to us.Systems
Order of system execution, multi-threading, communication
None of these are solved problems, but I have some ideas I plan to explore. The fact of this PR having us declare modules at comptime globally gives us the widest range of possibilities here in general. What I will explore is mapping the Elm data model (which I recently learned was used in the PSVR Dreams game, apparently?!) into ECS a bit.
Serialization
We have full type information with this approach, and so we could serialize ECS components as we see fit using that information. How exactly we do that is not settled, but I am leaning towards a custom binary format. The important part here is how this interacts with the future editor, which one can imagine talking to your game over a socket as it runs. In this scenario, the editor and the game need to speak the same serialization format in order for the editor to instruct the game to e.g. change a components value.
Other learning: high-level vs low-level apps
This might at first appear to be a thing I am going back on after our discussion in https://github.com/hexops/mach/issues/309#issuecomment-1140479760:
It's important to note that this is just "mach uses the low-level API to provide the high-level API". The only change from our agreed upon API earlier is that there is no special 'high level' API:
update,deinit, etc. low-level functions."Additionally, there is no "I forgot
pub" issue either like before, because we can always assert thatAppis defined - which we were already going to require in low-level applications.In short, I think it'll be a "eh that's a little weird, but OK" by anyone initially looking at it. But "with this you get android/ios/webassembly" seems a reasonable answer to a one-line oddity.
Thoughts?
I'd love to hear them. Overall I'm convinced this is the right path forward, at least as an idea to try out for now, but if others object heavily I might reconsider.
Follow-up thoughts:
I posted a Twitter thread soliciting feedback: https://twitter.com/slimsag/status/1536189712821456897
We can eliminate the namespace renaming logic (the
namespace: anytypeparameter passed toModulefunctions in e.g. the physics2d module) if we establish naming conventions (maybe enforce them even), e.g.<prefix>_<module name>for third-party modules,<module name>for Mach standard modules.Modules would still need the set of modules passed to them, so they can operate on
ecs.World(modules), unless we use@import("root").App.modulesmagic to access it - which may be worth it? We could eliminate all comptime parameters to modules then.Takeaways from Twitter feedback:
ecs.Modules?" (2 people)try(Srekel).module(.physics2d)wrapper API? Discussed in Matrix and would alleviate this questionWriting better examples/documentation in the future:
.{}struct prefix or_ = fooin examples/tutorials material (2-3 people not coming from Zig background don't understand it easily)Using a more constrained type here would better convey intent. You could also use
all_componentsto derive the two types which constrains the interface further.Merged via:
Pull request closed