Saturday, February 21, 2015

Terrain loading design

We can't keep an entire world loaded, but we need to make sure that the useful parts of the world (i.e. the parts near players) are loaded. How do we do this?

First, the world is divided into a regular cubic grid. Each cell on the grid is loaded and unloaded contiguously. Each cell is generated at several levels of detail (LODs), and the appropriate version is selected depending on distance from the viewer. This is called a geometry clipmap.

A SurroundingsIter struct iterates through all the cells in an NxNxN block; it iterates from the center outward in cubic "layers", so that terrain loads from near to far around a player.

This SurroundingsIter is wrapped up in a SurroundingsLoader struct, which is responsible for maintaining the cubic area around some (moving) point loaded. Its update function takes in a new central point, and produces Load and Unload events, which designate a cell to be unloaded or loaded at a specific LOD. In different places, these events are handled differently: the client responds by either requesting blocks from the server (for Loads) or by removing the block data from VRAM (for Unloads); the server responds to Loads by inserting the physical mesh data into the world for collision detection (note also that because the server doesn't care about visuals, it doesn't need to keep more blocks loaded than just the ones that the player touches).

Now, what if generating terrain takes too long? Should the server block? Probably not. Other updates and clients should be kept alive and well, regardless of terrain gen. Just load it when it's ready? What if the terrain is under the player? They'll fall through the world if it takes too long to load.

My solution so far has been to make unloaded areas solid near players; you simply can't move into a cell until it's loaded. This has some interesting side effects, though - what if one block isn't loaded, but the empty block above it is? Then I can use the solid unloaded block as a step to reach that chest I couldn't reach before (hypothetically).

The solidfying logic is surprisingly code-costly too: in the original client, blocks were either loaded both physically AND graphically (at some LOD), or not at all. The concept of "solid as a placeholder until the terrain gen is done" had to be threaded through all the code and data structures that dealt with loading, unloading, and levels of detail. In the end, the "cleanest" thing ended up being to add Placeholder as a somewhat special minimal LOD. This still exists in the server code, but thankfully the client doesn't worry about physics, and doesn't have to deal with this particular quirk.

I'd like to remove this logic, although there's no ideal replacement - if you stop anything from moving until the terrain around it is loaded, that can be exploited to, say, fire at an opponent from above. Minecraft just kind of pretends that unloaded areas are air until proven otherwise, and resolves inconsistencies by suffocating players that end up inside something. Although the idea of directly punishing players for slow servers tickles me, I feel like there's a better solution. Maybe the best solution is just to work hard at keeping players from encountering unloaded areas, and then it becomes less important what happens when they do.

The server code for keeping track of loaded blocks is overcomplicated too; since it doesn't care about graphics, it really only loads blocks at one LOD (max) for collision logic. The data structure(s) it uses are left over from the old client, though, and they can reason about many different owners requesting blocks at different LODs. Although right now Placeholder is still a LOD, so this functionality is kindasorta used, it really can get a lot simpler (which almost definitely means faster, too).

No comments:

Post a Comment