Unity to Godot Part 3: Overworld Generation
And the log continues for this week...
Input, continued
The refactoring axe has been sharp and merciless. The old input code has been a victim, and about 50 classes have been reduced to 2, and 10 files into 2. It still can't be tested, but appending it to the to-test list (gets longer by the minute!). The purpose? Everything is now contained and the abstractions are much simpler and to-the-point. There's a list of debugging actions that might trigger during unhandled input, there's a list of "proper" actions that can do pretty much anything, and all of them are associated with particular "input states" like world, level, tile selection, menus, etc. The hierarchy tree got significantly flattened. The input handling happens in the player GUI system, after we establish that 1) user input is allowed 2) player entity is alive 3) we expect player input. There's possibly some things that need fixing here (e.g. we should listen to input if player is dead -- it's just going to be limited to fewer things), but that's for later to avoid theorycrafting solutions.
Partial texture updates
I did a bit of proof of concept for being able to create a texture array and being able to selectively update individual slices in there and ... all good. Phew. Which means, the sprite composing code needs to be slightly refactored to remove the use of unity textures and NativeExtras.SubArray.
Profiler Marker, Screenshot capture
These low-hanging fruits had escaped somehow, they've been easily refactored to something non-Unity.
Refactor/porting: beginning of the end
Alright, now to port/refactor the hard stuff. The good thing about this next refactor is that, at the end of it, things should start working again, one by one. So, what's left, conceptually?
- Camera/texture functionality used for render-to-texture (RTT)
- GameObject creation/handling throughout code
- Arbitrary rendering throughout code
- Allocating/updating GPU buffers
- Shaders!
There are a few "scenes" that need to be fixed, and each scene utilises several/all of the above. In order of ascending complexity/spaghettisation:
- Autotile mask creation tool
- Mountain sprite creation tool
- Overworld creation scene/tool
- Game
Autotile mask creation tool
Here, we run a shader on a quad lots of times with slightly different parameters and we save the resulting square image on file. Simple!
... But, in practice, lots of hours of work and hair pulling, not so simple. My problem was "how to reload shaders while running the application". Easy, right? It should be, and it is, but lack of documentation got me. Long story short, to properly reload shaders, you should use the ResourceLoader.Load with the cache flag set to ignore (that's the only that works). I had it on replace and I tried every combination under the sun on every other aspect, e.g. making resources local to scenes, manually emitting changed signals, etc.
Ok, problem number two. There's some nice functionality where you set a viewport to update once, where it automatically sets itself to disabled afterwards. That's fantastic, and better than what Unity provided. Problem is, it's buggy and doesn't work. Oh well.
Problem number three. Trying a workaround to tha above, and getting the viewport texture to an image. But no, I can't get the viewport texture to an image because "Viewport is not set". But I just set it in the initialisation. Turns out, it's a bug too. Damn this is not going well. 1 hour later, I realise I was doing something wrong, which had the exact same side-effects. Oh well. Rollercoast of emotions here.
Port of tool is complete, and can generate autotile mask variations. I'm not going to go ahead and dig prehistoric code to see how to validate these now, all I care is RTT is working as expected, and I'll see visual results soon. Onwards to next tool.
Mountain sprite generation tool
This tool port started with a hitch again (it's a pattern you see), as my go-do DeepCopy method for some reason failed with a mysterious "Unable to find assembly" error. As far as I understand, there's only one assembly for the godot code so I don't know why this happened. In the spirit of "I don't want to deal with this right now", I did some basic research and found a library that does deep copies. NuGet FTW. I plugged that in, worked immediately, awesome.
Problem number two: while I quickly got results out, the results were ... broken. There's something going on with either the noise function or some other math, and the mountains look ugly with flat plateaus and little variation. After a bit of digging, I realised the the Dotnetnoise library by default was using some weird settings, instead of a simple simplex that I was expecting. Kinda fixed that (documentation is not great) so I'm getting something that looks more like the actual mountains.
Overworld generation tool
This one is going to be tough to port, because:
- I'm using plenty of render targets, and not just typical RGB8/RGBA8 ones
- There's a clear chain of rendering, where one pass is input to another pass, which is input to another pass, etc. I need explicit control of that.
- Some passes are CPU-only, so I have to grab the texture data, work on it, and generate some new texture data, while everything else is going on
How to deal with those issues?
- Apparently, Godot does not yet support 16/32 bits-per-channel viewports. Apparently "it's coming in 4.x". Oh well, need to get the creative juice flowing. Won't be that hard, but needs a bit of work. I'm either using RGBA32 buffers (ok), or R32F (not ok), but the latter can be written to the same memory space with a bit of bit fiddling.
- Utilise RenderingServer frame_post_draw and frame_pre_draw
.... But there is an alternative: Compute shaders. This can actually allow us to run on-the-spot rendering, and the lack of R32F rendertarget won't even be an issue. Diving in! A couple hours later, a few things to report:
- I'm definitely going to achieve what I need with compute shaders
- Code becomes quite low level, not a problem, but it's a bit more verbose
- I encountered a bug already: compute shaders don't reload from disk while the app is running, even when setting the cache settings appropriately. But I found a workaround, by dynamically loading the text file contents, mapping them to a "shader source" object and using that.
- ...and and annoying limitation: compute shaders don't support include files, yet at least. Annoying, but the workaround is to 1) add the #include directive anyway 2) process it in code and copy-paste the include in the source.
This means ... more porting time! Several hours later, I've ported all overworld generation pass shaders (9 of them), some common shader code files, and the utility class that manages the creation and resource management of all passes. This has resulted in reading/understanding/writing some low-level Vulkan-ish Godot rendering device code, so that's ... refreshing!
Add a few more hours for debugging, resource handling and QoL features, and I finally got the overworld generation working again. It's like the Unity implementation, but faster. It's actually closer to [the original implementation from 2017(https://byte-arcane.github.io/sigil-of-kings-website/2017/12/14/overworld-map-generation/) (gulp).
And here's a video of me using it to create maps in real-time.