Armed with my new approach, I started reimplementing the Prolog side of the “logui” library.
I very quickly realized how much nicer this approach was.
I wrote a few macros (using term_expansion/2) to make defining components and systems pleasant (which was fun on its own) and dove into rebuilding the “drawing” demo (which you can now see a screenshot of at the top of this very post).
In the way that Bevy “systems” are “just functions”, I realized that in Prolog systems are “just predicates”, but even more so!
While Bevy systems use the Rust type system to declare what components they act on and the framework does the work to run that query and find the arguments with which to call the function, in Logui the query itself is just Prolog code, since we use the dynamic database to store components!
The ecs:system macro does a bit of rearranging on top of that, but all the logic of finding which components a given system acts upon happens for free, by the nature of Prolog!
That realization really vindicated Raf’s suggestion that this architecture would be a good fit.
Writing the demo app was so much more pleasant this time, for a few reasons. For one, when building things as more of a functional “tree” structure, it was natural to want to add behaviours & interactivity by putting callbacks on certain nodes (e.g. “on click”). However, that doesn’t work very well with Prolog, since it doesn’t really have the notion of functions in that way, leading to workarounds of varying ugliness. With this ECS system though, it’s very easy to write a predicate that queries for active events and the corresponding components that might want to act on them. No special-casing required!
ecs:component(set_current_draw_colour_view, colour).
ecs:component(canvas).
ecs:component(draw_colour, colour).
ecs:system(set_current_draw_colour_click, ( event(mouse_button_down, ClickState),
canvas(CanvasEntity),
set_current_draw_colour_view(Entity, SetColour),
point_inside(ClickState, Entity)
)) -->
update_draw_colour(CanvasEntity, SetColour).
To prove how simple the ecs:system macros is, here’s what that expands to!
?- listing(set_current_draw_colour_click).
drawing:set_current_draw_colour_click(A, B) :-
dcg_high_order:foreach((length(C, 0), []=C, drawing:(event(mouse_button_down, D), canvas(E), set_current_draw_colour_view(F, G), point_inside(D, F))), logui_ecs:optional(drawing:update_draw_colour(E, G)), A, B).
Cleaning up the formatting a bit and undoing the DCG expansion, that’s something like this:
drawing:set_current_draw_colour_click -->
dcg_high_order:foreach(
( length(C, 0), [] = C, drawing:event(mouse_button_down, D),
drawing:canvas(E),
drawing:set_current_draw_colour_view(F, G),
point_inside(D, F)),
logui_ecs:optional(drawing:update_draw_colour(E, G))).
Which we can see is essentially just looping over the results of of that query for events & components. A paper-thin layer!
So little special-casing was required in fact, that I was able to fully separate the core ECS stuff from the graphics bits.
There’s now a logui_ecs module that defines the basics, then the logui module just defines a bunch of components and systems that do the SDL stuff.
I had a lot of fun continuing to build out the drawing demo and add features.
I eventually made it draw proper lines by using an SDL_Surface that can have arbitrary pixels drawn on it, to get rid of the ugly artifacts that drawing with a bunch of little rectangles brings.
That was a fair amount of mucking around on the C++ interface side of things, but on the Prolog side, everything stayed nice and clean.
Indeed, I was very happy to find that for the most part as I went to add new features, it was all pretty “linear”:
I would write how I wanted to the API to work from the “consumer” side in the demo code, add whatever was needed to implement that in the library, and for the most part, that was it.
There were far few times when I realized that to do something this way, I’d need to add extra book-keeping, or change how something was stored, or whatever kind of nonsense had been happening with the prior approach.