I've been looking a bit into the high-level engine APIs (again):
- comparing the example code from Game Engine Guides – GameFromScratch.com)
- evaluating what parts of gfx_scene/claymore can be salvaged
- checking the Amethyst status and API
I've got to admit, this is not obvious or easy... But I think I narrowed the way to go for a scalable Rust-ic engine design, and I'm sharing my ideas to get some feedback.
First of all, it appears that having ECS internally is the only way to get some nice scalability. ECS as a concept is not perfect, it doesn't match to all the use cases, but it's a decent tool to get advantage of, given the lack of proper alternatives.
Secondly, many engines provide an object-style API, aka ggez, where you basically define a few objects and some logic inside about how to update/render themselves. This is a popular concept, but it's hardly translatable to ECS. I mean, you can lay out the engine-internal components in an ECS and use it, but it seems quite difficult to incorporate the possible user components into this system without exposing the ECS. Thus, such engines are not scalable with regards to either performance or the user extension.
The alternative that is still on the table is - working directly with ECS. Obviously, that rises up the entrance bar as well as the learning curve. But then you'd see the benefits of this design:
- user extensions (ECS systems and components) are first class citizens
- engine is very efficient with large number of objects, automatically parallelized by the ECS
- the concept is simple, and the whole engine can be represented by a set of semi-individual modules/bricks, which just define new systems and components (Lego design FTW!).
Now, people familiar with Amethyst may wonder if I just described it as an ideal engine. I don't think Amethyst is exactly following this model, but it could be. If it stops trying to wrap specs, makes its different modules more independent, and brings a bit more focus to the type strength, it will be pretty close.
For example, one could use the following bricks:
- a base module that defines a transform component
- a render module defines a system that does rendering into a specific framebuffer associated with an owned camera, using a single PSO definition. It would read the positional data from the transform component and manage a defined drawable component. The user could have many instances of such systems, rendering to shadow maps, with different cameras, different passes, etc.
- a physics module defines a rigid body component as well as a system that does simulation/indegration of the physics forces. It writes the results into the transform component.
For now, I started designing and implementing some of the bricks, checking if they can be flexible enough and work with each other, so that I can port yasteroids and vange-rs on it. As I mentioned earlier, Rust encourages us to explore new API models, which also steer the implementation details, and I believe this Lego brick design may become idiomatic. In the end, I may converge to Amethyst or do something simpler, but either way the journey is exciting