controllerface / bvge

Personal game dev experiments
0 stars 0 forks source link

Define Level Format/Implement World Generation #60

Closed controllerface closed 6 months ago

controllerface commented 8 months ago

Up until now, there has just been the basic "test tank" for spawning objects and generally just testing physics. This has worked fine, but it's getting quite boring 😄. In all honesty, I need to break up the physics work a bit and maybe focus on some things that will make the game world more.. world like.

I do not need a fully realized procedural generation scheme yet, really even a raw format I can preload with some data and load at startup. However, I do want to think about how the format will work later on, when there will be proc-gen, so it shouldn't be completely thrown together.

While I don't need a perfect scheme, I do want to make sure I consider speed of loading data and I want to explore changing the current "delete at edge" behavior such that perhaps data is instead saved into the level file. Then, when a section of the world is loaded again, the objects in that section should be pulled from the level file.

I will probably need to extend a secondary grid beyond the current main uniform grid so there is a buffer zone where objects don't move, but also aren't removed from memory right away. I need to be able to have objects become stationary before they are saved. I also probably don't want to have a ton of moving objects in the world by default anyway.

I don't know what kind of challenges this will entail, as I am trying very hard not to take a tile based approach to the general world layout, but for streaming data in an out, it's going to requires some degree of tiling or "chunking" so groups of objects can be loaded in large groups for efficiency.

controllerface commented 6 months ago

Have started in on world gen so taking notes here.

I now have a basic world generation setup, just some noise and mapping to block types to generate them in a grid, not unlike Terraria, though at the moment obviously a lot less refined.

This first implementation is very unoptimized though, and will require a number of improvements. At the moment, the addition of new objects is not properly synced with the command buffers. It pretty much works, but is technically race prone, so this needs to be fixed. Also, the way objects are transferred into the GPU buffers is extremely poor performing, each objects i placed into memory one at a time. This really should be done in batches.

The current design ahs the world broken into a number of 64 x 64 block "sectors". Each block is 32 pixels, so a sector occupies a fixed 2048 x 2048 region of space. As these sectors are touched by the outer uniform grid, they are loaded by running the Perlin noise generation algorithm. As it stands, it is pretty basic, but the effect is quite good, it looks a lot like caves/caverns and I have added some water that spawns in blobs. To round it out, any block that would spawn just above a space that would be empty, spawns as dynamic, so will fall when spawning in.

To make this a bit more workable, i am going to need to do two key things:

  1. sectors will need to be cached, and possibly pre-generated if memory permits. The output of loading a sector should be a batch of GPU store calls, which will all run in one kernel.
  2. sector load batches should be processed serially, with only a single sector loading per frame. This should help keep the game responsive, right now a sector load causes visible jerkiness.
controllerface commented 6 months ago

Been a few days, some optimizations in place now and things run a bit faster, but still not as fast as I'd like. I did learn a few things in this process, for one, 64 x 64 block sectors resulted in a LOT of extra blocks around the edges of the grid zone. I originally assumed that I would be able to easily create batches and that larger chunks would overall be a more efficient choice, since the boundary would be hit less often. As it turns out 8 x 8 block sectors are the fastest (though admittedly, I did not try smaller than that 😆) as they just result in a lot less "buffer" area which cuts down on the number of objects leaded at any given time, and the overhead of tracking more blocks/tris/liquids outweighs any benefit of less frequent loading.

Another thing I have noticed is that the model renderer is taking up actually quite a bit more frame time than I expected. I have been very quick to blame the collision code for slowing things down so much (and it does in fact take a lot of frame time) but doing some tests, disabling it and running just the hull renderer, shows a noticeable improvement to the rendering smoothness. It still can bog down a bit, but not as much and not as often. This shows I need to spend at least some amount of time trying to make that go faster.

To speed up polygon rendering, the best option is probably going to be combining them into a single renderer. Right now, there's three, one for blocks, one tris, and one for models (liquids are separate as they are circles). But I have plenty of texture space on the GPU to store multiple texture slots, and run them all in a single pass. They all use the same shader already, so I just need to add an input variable to select the texture channel to use.

controllerface commented 6 months ago

More baby steps, I have clawed back a few more FPS now, making just some minor tweaks.

In one case, I was able to use a vThread to calculate a value instead of reading it back from a kernel using the pinned_int read method, this is one of the most costly native methods to call right now, so it will be worth reevaluating this moving over to project panama/valhalla stuff later on, it may be that the read back gets a lot faster. I also am now filtering out objects in the outer reaches of the uniform grid from the rendering process, since they are not seen during normal gameplay, which also improved latency a little bit. For debugging, i can still enable hulls to get a visual of objects in that area if I want.

I also finally got around to generated the block model in code and while this is not a direct speed improvement, it does make the code for generating blocks cleaner and less hacky. There used to be a hard-coded * 5 of the size of a block due to a difference in how model vertices load from .fbx files vs the direct generation method. Now that is gone, and there's no full trip through the model loaded. I may remove the "baked uv" thing since that was only to support the block use case, but will hold off in case I need it for something.

I have made several tweaks to the world gen algorithm, and have finally found a variant that looks great for just "generic caves". I will definitely have to play with blending of the output of different noise passes, as I want to maybe explore doing something with "dirt" and or "sand" where they spawn in the "voids" of a first noise pass, but only in certain "bands" like a surface layer for example.

One major issue though, is that I am seeing intermittent system hangs. Just straight up full halt crashes, no error messages. I think I know where this is coming from, as I also sometimes rarely see a sector of the grid simply fail to load. There's something "racy" and it could be the hull filtering kernel. This kernel is still being used, but was marked for removal when I did the big refactor of the core memory object a while back. Even if this is not the final culprit, it is one place where I know there is a potential issue where the gl_ context can try and read memory that the cl_ context might be working with. I need to fix this and see if it the problem recurs. It's annoyingly hard to make it happen, but usually happens either right when the program starts up, and a few frames have rendered or in cases where there's just a lot of stuff going on on-screen.

controllerface commented 6 months ago

Another week of tinkering, and some progress. I believe I finally tracked down the source of the mysterious crash 🤞 and it's been a few days without it happening again. Turns out trying to call the sector load process inside the physics simulation step tick led to some kind of race condition. Moving the sector load back into the tick method, just before the mirror buffers are "reflected" for rendering, seems to be the only place where it doesn't cause issues. I had experimented with trying to get as much to happen at the end of the physics simulation step, including sector load, because it is the one place I know I could hide a little latency, as it is currently one of the few places where there's some asynchronous processing happening from the point of view of the main loop, but well.. no luck I guess.

I have also factored out the sector loading process into a a SectorLoader class which implements GameSystem. This system is now the very first one loaded, so all sector load activities are done just before physics is processed. This keeps it functionally equivalent to being inside the TestGame class where it was before, but just now it's not cluttering things up there.

On that topic, I was able to actually spin off one more asynchronous task to help speed things up just a bit (I gained about 2 or 3 FPS) and keep things more responsive. Instead of storing the entity batches for processing, and then looping through them when the SectorLoader's tick() event fires (which entails a serialized creating of each object to be spawned), now the sector load process happens in a background thread and batches get processed immediately, without waiting for the main loop. The difference now, is that there's a separate WorldBuffer which uses a separate CL queue and buffers.

What this allowed me to do was effectively spawn all the objects for the sector that will load next inside the WorldBuffer, which has it's own separate memory buffers with the exact same layout as the core memory class. Then, when the next next game tick happens, this buffer data is copied into the core memory buffers, with just one extra step that updates the offsets of the buffered objects to the proper offset they would need to have in order to be added to the core memory buffers, just as if they had been added directly.

This buffer merge process took me a few tries to nail down. There's a lot of points where command queue sync needs to happen as well as careful ordering of when/how objects get merged into the main buffer to ensure things don't just crash. One nice things about this is the new WorldContainer which bother the new buffer and existing core memory class implement. Now, when spawning entities using the existing PhysicsObjects utils, you pass in one of these container implementations, and the objects that are "spawned" will be resident in whatever object is passed instead of always going into core memory.

So yeah.. slow but steady progress as usual. Will make some notes in a bit on some next steps.

controllerface commented 6 months ago

OK, so now that I have tools to handle buffering of objects a little easier, the next order of business is going to be going the other way, so instead of sector loading, sector unloading. I expect this to be tricky as well because there's a bunch of specific that need to be addressed for it all to work right. Similarly to going into the world, objects coming out of the world when a sector unloads, will have offsets that won't be valid once the objects aren't in the core memory anymore. Also, unlike the input side WorldBuffer, objects coming out will not be contiguous. So it's not just a matter of the offsets needing to be adjusted uniformly by some easy to compute adjustment values.

I haven't actually written any code yet on this because I want to make sure I really understand what the math needs to be, it will have some of the same complexities as the delete process, which also can adjust objects in non-contiguous patterns. Here though, I am not sure something like the "shift buffers" will work as easily. So, my current thought is to try and "normalize" these objects, in other words, adjust all of their offsets to essentially to start at 0, as if every entity were the first and only one that is stored. I this format, the objects wouldn't be readily usable of course, as they would all (aside form the actual first entity) have incorrectly defined offsets.

However, if the normalized objects are at least packed into the buffer contiguously, the offsets could be computed using a custom scan kernel, similar to how the collision key bank buffers are calculated. Basically, I'm looking at two "phases" for sector unload; first an "extraction" phase where the objects are transferred from core memory into the "holding buffer" and then a second "adjustment" pass where the offsets are all fixed within the holding buffer. Once adjusted, assuming I used a similar pattern to world buffer, the objects would then be readily able to be dumped back into the world, for example if their sector gets loaded in again.

Something else to consider here is that individual sectors may need to have their own buffers for this to work easily. That might be a bit challenging to manage though, so I am hesitant to jump into it without careful thought. In that scenario, it would be really important to ensure buffers are tracked and disposed of properly. If instead, there was one "unloaded" buffer where any objects that have been removed from the world reside, that problem would go away. That would mean though, that objects from many sectors would be comingled, meaning if objects needed to be restored from one sector in the holding buffer, but other objects in the buffer don't need to be restored, we're in the same boat as before with objects having incorrect offsets, and needing to be updated.

One scheme I'm considering is spawning a buffer for every sector as they become candidates for loading and caching it, so that all sectors that need to unload, can simply callup their specific buffer and unload into it. These buffers can then stick around for a bit for fast reload, and then eventually expire after they've been unloaded for some period of time or even , if the player gets some distance away from them. This will give me a very simple avenue to eventually dump this data to some persistence layer on disk, i.e. finally having an actual world file!

So.. got my work cut out for me it seems 😄

controllerface commented 6 months ago

I now have a first working version of level streaming 😎

There's still a few things to add to make complex models work, right now the only complex model is the player, which never unloads so the bone specific logic never runs, because blocks/shards/liquids are boneless. Even so, the actual work to pull bones out of the buffers is done, I just need to add a case block for complex models and test it to make sure the bones and animations all stream in and out properly.

In the end, I did not need a buffer for every sector, just a set of A/B buffers, which get swapped every frame. These buffers work really well and surprisingly, I'm seeing negligible performance impact. The fact that sector loads are not continuous really helps hide the latency quite a bit. Granted, I am still not putting anything on disk, there's probably some I/O overhead I'll have to deal with.

This feels like a pretty big milestone, but there's more yet to do. I still need to figure out the level format. I am still leaning toward using Xodus just due to familiarity, but also the useful "links" feature, which I think will be able to leverage for organizational purposes.

really jazzed with where things are, I think have level files is now within sight.

controllerface commented 6 months ago

I've decided to keep going in this story with some world gen and adjacent stuff. Since I want to make sure everything that loads in can also load out correctly, it is at least somewhat related to what this ticket was originally for.

On that note, I have adjusted how damaging objects works, and removed the code that tied it to collision with a "hand" hull. Instead, there is now a highlight system that tracks the mouse and the player. There's three "levels" of mouse states with corresponding highlights of red, green, and blue. The green state means that the object the mouse is touching is both in range, and the mouse is directly over the object. The blue state means that the object is in range, and the mouse cursor's innermost circle is touching the object (this may be useful for "multi-select" later). The red state means the cursor is directly over the object, but it is out of range.

When an object is lit green, the player can now click to damage it. They do still punch when doing this, but the effect is purely cosmetic now. As an object becomes damage, it takes on a darker and darker tint, until it's pretty much sold black, and then when the hull integrity reaches 0, the object is deleted from the world. This is the first basic "mining" ability for the player! ⛏

Now, what I want to do next is change from merely deleting the object, to also spawning smaller "chunks" that fall when the object is broken. These "rocks" should then be collectible from the world, which at first can just delete them, but should eventually be tracked as resources in an inventory system.

I have not forgotten about the level files either, but I want to keep stewing a bit as I get some of these basic world interactions working. I think Xodus really is going to be the tool, I dug back into Commander J code and re-familiarized myself with how it works, and I think It will map really well to the data that's in the Caffeine cache. Of course, I have named myself into a corner a bit, since Xodus also uses "Entity" and I have an Entity in the game code as well. When stored, it will be the case that entities from the game will be stored under an Xodus Entity, but then so will hulls, points, and everything else, so there will be some confusion there. I will consider a name change in the game code.

controllerface commented 6 months ago

Little more progress, I have implemented a kernel that extracts "broken" objects, in s similar fashion to objects that go out of the playable area, though a lot simpler in nature. This kernel right now just extracts the location, uv offset and model ID of the object, which is used to spawn smaller versions of the destroyed object. This visual effect is nice, with rocks accumulating around you as you mine through walls. I think this aesthetic will be one small thing that differentiates from something like terraria or starbound, which follow the minecraft approach, where the broken block usually just drops as an item. My approach looks a bit more realistic, and fits the feel I'm going for a bit better.

That said, I will need to implement some kind of collection mechanic, either just collect on touch when the object is small enough, or a way to mass collect items within range, probably via a button/key press. I want to be careful and balance the look of mining with the playability so it's not tedious. Though, to be fair, mining in any game is somewhat like that, so I won't bend over backwards to get it "perfect". Just not annoying.

controllerface commented 6 months ago

In light of the amount of time I've spent on this ticket, most of which was a different task than originally intended, I'm going to cut this one and merge back into main, I want to have a clean slate to work from for next steps.