This one is going to be a big dump of things so buckle up.
A month after solving the UI problem of Construct 3
Let's start with a follow up to my last blog post. First I wanna thank everyone that participated in the discussion around UI. The post ended up being super well received by both Construct users and the Construct team. Ashley received a lot of these points pretty well and was very keen on trying to provide solutions for them which is very appreciated!
As a quick reminder: These were the points that I thought would require the most attention:
- A layout engine
- More work on layers
- An editor SDK
Layout engines
As I stated in my post, I imagined that Scirra would keep going down the route of making use of HTML as a layout engine. It's a very understandable decision, but I made it clear what were the limitations of such a decision. These limitations seem to have been taken into account and Ashley said he'd be working with them in mind.
As of the "make your own layout engine" route, it was swiftly rejected despite its clear long term advantages because HTML has been built and maintained by thousands of developers, and there's no point in trying to compete with them.
Competing with thousands of developers
I'm gonna do it anyway
I swear to god I'm this close to make that thing cost thousands of dollars if I'm ever done with it. I planned to make it free initially because I love "freely accessible technology" and also I dislike "gatekeeping things from marginalised developers" and "capitalism" but man the price so far has been my soul.
And you know what?
The hard part IS NOT THE LAYOUT ENGINE
I agree with Ashley, there is no point in making a custom layout engine from scratch, it's actually a dumb idea. If you can't beat them, join them.
All I needed was a JS based flexbox engine, not a custom one. I just need something that does not involve CSS and HTML, that's it. Turns out Facebook made one for their own purposes called Yoga engine.
It's a layout engine built with C++ meant to be cross platform. It has a JS library for use in web projects.
Now I have one big issue with Yoga, and that since it was built by Facebook its codebase would be huge and funnily enough not very flexible for my needs.
I found this JS port of Yoga engine that someone made however:
github.com/diegomura/flexbox-js
The idea is that it should be a simple port of Yoga layout but entirely in JS that I can then mess with, read and eventually hotswap for the official Yoga implementation later fairly easily.
Why do that?
It's much easier, simpler to iterate on, read through the engine and also I needed to know the exact limits of the engine. One thing I needed to know exactly for example were what CSS rules I was even allowed to use. Delving into this code made it a lot easier.
Thanks to that I was able to write an implementation agnostic style parser and validator for Yoga.
52 style rules, all with a details about what kind of arguments it supports and a style validator function to let you know if the style rule is valid.
That was the easy part.
Implementing this in C3
My initial thoughts were laughably credulous. In my perfect world, I'd create a behavior that would just take a style input and use scene graph to handle UI hierarchy.
The wonderful advantages of the C3 Editor SDK
- It exists
... Anyway, Ripping the C3 editor SDK apart
I soon came to realise that C3 was really not up to the task of letting me implement a cool layout engine I found in peace. For one, there is no SDK method to get scene graph parents and children for world objects.
There is also no SDK support for getting references to other world objects.
There is no SDK support for getting a list of layouts in the project.
There is no SDK support for destroying your own instance or at least knowing when it's getting destroyed.
Another cool detail is that since the C3 editor is Scirra's bread and butter, it is aggressively minified and uglified on publish, even for classes that are seemingly shared between runtime and editor (like some rendering classes).
Weird characters. What do they mean?
C3 released in March 2017. We're in June 2022. It has been five years since I started using this engine.
This means that the looming passing of time is inevitable and five years is a ridiculously large amount of time for what feels like short of 12 months.
This also means that I've had a lot of time to look into the internals of C3 and so I'm very used to reading minified code.
It's actually a super cool skill to have because this means I can do a bunch of very cool things.
- I understand obscure Javascript features on a pretty deep level. Stuff like functional programming and how parenthesis work, that you would never see outside of minification.
- I can reverse engineer a lot of things given I can make accurate assumptions about the structure of the code base. This leads to me making VERY educated guesses as to where everything is in the engine, and what parts of it I know the minifier doesn't touch
- I can write cool looking code that I know will minify to the ugliest fucking mess I have ever seen, and that only I can even comprehend even in unminified form.
- I can roughly understand what code does just by looking at its structure rather than how things are named.
Anyway, I can almost navigate C3's minified code as if it was regular source code. I am like tarzan and weird characters are my lianas.
This makes it even more frustrating then to know HOW MANY FEATURES the editor has. I could say the exposed SDK is the visible part of the iceberg but even that would be misleading. The reality is more akin to Scrooge McDuck handing you a dime and being snub about it.
You know me, I hate capitalism and so I'm gonna do what I can to steal from this rich minified code.
Here is the code to get a list of layout names in the project:
yeah...
Why does this need to be this complicated? Because C3's minification randomly changes variable names with each release of course!
I can't just use minified variable names and be on my merry way, since it's gonna break next week. What I need to do instead is find ways to get access to these variables without their name. The only way to do this is to find a series of backdoors in the code and find a way to define a variable by what it is rather than how it's named. In this case, I know exactly where layout names are located in the minified code, I just need to find the name of the keys that will let me get there, but each one of these keys has structural properties that a minifier cannot change.
Stuff like variable type, what the variable contains, where the variable is, how the variable is used. If the variable is a function, how it looks, and what makes it special.
This is the only way to get references to things in a name agnostic way, and it will keep working regardless of how the code is minified. Breaking this would require Scirra to change the actual structure of the code, which will take as much effort from them to do as it will for me to find new ways to get in. So I'm much safer this way and much more confident in my code's ability to stay relevant for periods longer than a week.
It is a massive pain in the ass to find, reverse engineer, and write the code necessary for this to work. So whenever possible I use other, simpler ways.
Here's my new method for getting layout names:
Writing classes that extend C3 classes and replacing them lets me create reliable backdoors pretty much anywhere I want in the exposed SDK.
The exposed SDK Classes are almost always just super simple classes that are given a secret magic object that contains all the data I need, and then methods just send me what Scirra agrees to give me from these objects. Subclassing the class means I can just get a hold of that secret object and just get what I need from it directly.
In some cases this also helps with getting more data from exposed parts of the SDK. The Layout class from the SDK does give me the name of the layout it's attached to.
I just have no way to get a list of all the layout objects that have been created.
I can just hack that in myself though.
It has advantages and inconvenients from the other method. I can't get everything I want, but at least I don't want to die.
The case of scene graph
Given I wanted access to scene graph, this is the first thing I reverse engineered. I was able to do it just fine using the methods I highlighted above, but one thing became very clear.
Getting the data is one thing.
Knowing when that data changes is another.
I meant for scene graph to be used as the hierarchy for my system, but very quickly I realised that UX for this was absolutely terrible. It would be absolutely unusable in anything vaguely resembling a real product.
I had to admit defeat.
Not defeated by thousands of developers, one was enough
This is me two days after making my initial plans for this addon
The solution would require that I write an external editor that would handle all of the data management. I would have full control over it. I quickly made a mockup of what I had in mind
and then I stopped.
Not defeated. Incapacited
When working on that, I was in Seoul, Korea. Right after posting that mockup, besides feeling burnt out from this project already, I was just about to embark on a 10 day journey accross the country.
My life at the moment is a pretty wild ride, for unrelated reasons. I wrote all about it in this post I made almost two weeks ago. Basically, major life changes are the reason I'm even able to spend this much time obsessing over Construct. They are also the reason I was unable to keep working on this.
When I came back, I eventually started working on this again, and at the time of writing this I have spent 10 days trying to get it to work.
Here is the current result.
I had to fix a lot of issues for it to even be possible to do that. Here's a short list:
- Making Vue render an app inside of C3 that itself uses Vue portals to render parts of itself in another part of the page
- Finding a way to disable CSP in C3 so CSS can load properly
- Making Winbox work cross addon despite being integrated into one, so I can do the same thing in future addons
- Making build tools that integrate with C3IDE so I can develop the editor separately and build it for C3
- Finding ways for the CSS included by Vuetify to not conflict with C3's CSS
- Adding keyboard shortcuts that don't conflict with C3's shortcuts
- Detecting when the project is closed so I can clean up all of my stuff
- Finding ways to save all the data inside the project
Anyway, I'm happy with the current result, but it's still not enough. There's still a lot of things to figure out, but I'm getting burnt out again, and I cannot justify working on this for any longer.
I will be back working on this in a few weeks when I feel ready, and feel like I've taken enough of a step back and I can come back with a fresh mind and fresh ideas.
More work on layers
Layers have always been a part of C3 that I felt always needed more love. They did get a bunch recently with layer groups. It's made layers a lot more powerful, and I have used them a lot in my projects, but I still feel we need a bit more.
I already went over my attempts at creating layers at runtime in my last blog post.
What's new is that I realised that creating layers at edittime was going to be a lot harder. A user can create them manually, sure, so that's fine. But I cannot create them using the editor SDK.
While experimenting with layers, I also went into a tangeant about pixel art with other members of the community.
When I made my post, I exposed the idea that per layer rendering modes would fix a bunch of issues with pixel art, but Overboy was quick to point out that using an effect could be better. Effect extraordinaire Mikal tried his hand at making that effect, and while he didn't manage to make what Overboy wanted, he did make this cool thing.
This is an effect that makes pixel art look crisp again even when not in nearest rendering mode. This is still a proof of concept and not usable on layers yet, but still a very interesting development.
While making the effects, Mikal realised that usually, pixel shaders use a built-in from a webgl extension, specifically fwidth(). The fun part is that this is a webgl only extension and was removed in webgl 2, because it was added as part of the core built-ins of webgl 2.
Great news! ...right?
Yeah! To use them all you need to do is add #version 300 es
at the start of the shader.
🙂
Adding #version 300 es
breaks compatibilty with webgl 1.
We have no way to tell C3 that a shader has been written for Webgl2 and not for 1.
The excellent part is that the same shader can be written just fine for webgl 1. What makes it impossible is being stuck in that weird no man's land between both versions.
How can this be fixed?
With WebGPU just around the corner, and the fact that Scirra are adding support for separate shader files for WebGPU, I hope they also had a way to separate shaders for WebGL 1 and 2.
WebGL 2 is supported in 90% of devices worldwide. We're not even limited by having to support the remaining 10%. We're limited by having to be compatible with them in a single shader.
Trying anyway
I tried my hand at writing Overboy's shader idea anyway, and I soon came to realise that the problem is much larger than what it seems like initially.
There are a lot of different moving parts in C3's rendering that make it impossible to fix all issues with pixel art in C3. You just can't.
The fun part is that I am now almost sure that per layer render settings is the best way to fix most of them.
Per layer render settings, again
Overconfident and cocky, perfect
🙂
I could not in fact do that. Well, making the engine believe it's using different render settings per layer is easy. Forcing each layer to render to a different render texture is also easy.
For some reason it just does not work though.
The C3 renderer is the one part I avoid like the plague when diving into the engine's code. It's the part of the engine I know the least about and it's by far the most complicated code in there. I hate it.
I thought I just needed to do 2 super simple things before a layer draws:
- Change sampling mode
- Change fullscreen quality
Now something deep in there is making it impossible for my change to matter. And I'm sure it's some random webgl related limitation or the code just assumes the sampling mode or fullscreen quality never changes from its initial value and it creates desyncs in the engine.
Anyway, it doesn't work.
Best case scenario, I'm dumb and I need to update like 2 things and it's super simple.
Worst case scenario... it requires a big rewrite of the renderer.
I'll probably look into this further in the future, so we'll see where this rabbit hole goes.
The end
Thanks for reading through this. I'm done with my big dump. Stay safe and have a nice day.