Sunday, February 14, 2021

Procedural City: Adding Building Basements

I'm starting to work on adding actual user gameplay for 3DWorld's procedural city environment. I made people inside buildings somewhat behave like zombies, and I added features for the player to steal objects from buildings. It's all pretty silly at this point, but that's fine for experimenting. However, I do want to make this at least a tiny bit scary.

The problem is, the interiors of these buildings are too bright for a horror game because they have so many windows. What I need are basements that I can make darker, especially when the lights are off. I'm starting with adding basements to houses as an extra level below the ground floor that may be a subset of the first floor footprint. This means they can be smaller in area than the house itself. The basement is connected to the first floor by a set of stairs with a door. I used my random number generator to add basements to 50% of houses to add some variety and reduce the runtime and memory penalty of having more building geometry.

Basements are drawn a bit differently from the rest of the house. Exterior walls are underground, so they don't need to be drawn at all as they're never visible by the player. Interior walls are always white rather than the brighter colors used on the main floors of the house. I also plan to change the ceiling and floor textures to something that looks more like a basement, possibly bare concrete in some cases. I haven't done this yet. I'll have to experiment with that later. Lighting also works differently, as discussed below.

Basement laundry room with washer, dryer, sink, table, and bookcase, with a bathroom in the back.

While this seems like something that should have been easy to add, it took quite a while to work through the various problems and get basements properly integrated. Some of these problems were expected, while others were a surprise to me.

First, there was the task of adding a new level to a house below the ground floor without breaking how the ground floor was handled. There were dozens of special cases in the code to treat rooms with zmin (the elevation of the floor of the room) equal to the building bounding cube zmin as rooms on the ground floor. This logic was used to add exterior doors, assign the living and dining rooms, control object placement, affect occlusion culling, etc. When I added a level below the ground floor, I had to update the building bounding cube to make it draw correctly, and that caused the placement system to add doors in the wrong locations and other strange failures. It took several hours to find and fix all of these cases. In fact, I found and fixed another one of these (fence placement) that I came across in the process of capturing screenshots for this post.

The next problem was handling terrain. This is drawn as part of an independent system that's separate from building drawing. Basements are below the terrain, and I had to add stairs to connect them. But the terrain was drawn over the entrance to the stairs because it was no longer hidden under the floor of the house. How exactly am I supposed to make the terrain system omit drawing areas around the basement entrance stairs? I already have a system to cut holes in the terrain for tunnel entrances by writing all zeros to the splat map texture, which will cause the fragment shader to discard those pixels. It's fine to iterate over a few tunnels for each terrain tile, but I think it would be too slow to iterate over thousands of houses to find the terrain texels that are over basement stairs.

Yes, the basement really is underground. You can see the basement stairs and some objects below the level of the grass.

I decided to use the same hack I've used in other areas of the code. Since the terrain mesh is drawn after buildings, I can write to the depth buffer between building and terrain drawing to prevent the terrain's fragment shader from writing to that area of the frame buffer. This is accomplished by drawing two transparent quads, one directly below the stairs cutout in the floor, and the other directly above. That seemed to work well enough. There's still a minor issue where the player can see the terrain when these two quads clip through the near plane of the view frustum, but I'm ignoring that for now.

Next, I realized that the AI I wrote for people in buildings couldn't figure out how to use the basement stairs. It was an existing bug in the path finding logic that was triggered when the AI attempted to plot a path through the stairs where the top and bottom levels had different floorplans. That took me many hours to debug and fix, partly because there were multiple related bugs in the code. It also didn't help that this code was difficult to test.

Stairs leading to the basement, with a door at the bottom. Maybe I need to add walls around these stairs or move them closer to the windows at some point.

I added a door to the bottom of the basement stairs to help isolate the two parts of the house. This is a new type of door because it separates stairs from a room, rather than separating two rooms (interior doors) or separating the interior of the building from the exterior (exterior doors). In addition, these doors were closed by default. The player must use the 'q' key to open doors so that they can pass through. Once again, I had to go back and update the building AI logic.

I added a config file option to control whether or not AIs could open doors. If they can't open the basement door, then this must break the connectivity between the first floor and the basement. If the player opens or closes the basement door, the navigation graph must be rebuilt to reflect the change in building connectivity. If the player closes the door after the AI has plotted its path, it will have to stop and wait at the door, or turn around and choose a new destination. On the other hand, if the AI can open doors, then the navigation graph doesn't need to know about them. The person can simply open the door when they're about to collide with it.

Basement storage room filled with boxes, crates, and small shelves with more items.

One issue I ran into is that the building generator was not always able to connect the basement to the first floor with stairs. Sometimes the basement was very small, or had many small rooms, and there wasn't enough space to place stairs. Stairs can only be added if they don't intersect a wall or obstruct a doorway on the floors above and below. This stairs placement failure happened in about 221 out of ~5000 houses with basements. That's not too often, but I really want this to be perfect. My first attempted fix was to allow basement stairs to be steeper (and therefore more compact) than normal house and building stairs. This definitely helped, reducing the number of failures to only 48. The final fix was to apply the "big hammer" to the problem: If the generator can't connect the basement with stairs, throw away the entire interior and generate a new one. This was very effective, and 42 of the 48 cases were fixed by the third attempt. One last pesky case required 12 attempts to finally connect the basement, but now there are none left unconnected. This additional work had a negligible impact on building generation time because it only required regenerating a hundred or so buildings out of over ten thousand.

I was expecting to have to change the player collision detection logic to somehow disable terrain collision detection when the player was in the basement. But the existing code was already skipping terrain collision detection when the player was in a building, so there was nothing I had to change.

Once I had these various issues resolved I worked on room assignment and object placement. Basements are mostly full of storage rooms, boxes, tables, bathrooms, and laundry rooms. Some have couches and TVs. There are no windows, plants, bedrooms, or other objects that don't belong in basements. A few of the rooms are left empty and/or unassigned until I can come up with new purposes for basement rooms.

I finally had basements correctly placed, connected with stairs, with working AI navigation. What was I doing this for again? Oh, right, I wanted some dark areas in a house to make the experience a bit scarier. Yes, I needed to make basements darker. It was relatively easy to adjust the ambient and indirect lighting levels to nearly zero so that the only light was direct illumination from room lights. If the player or AI turned the lights off, this left basements very dark. Even with the lights on they were filled with shadows.

The basement is very dark when all the room lights are off. There's still a tiny amount of ambient light.

I did have to fix the light transition though. At this point there was an abrupt switch between the two lighting models exactly as the player entered or exited the basement stairs. I experimented with various changes such as setting ambient light levels based on elevation. I settled on a time-based transition where light levels would slowly change over a period of several seconds after the player crossed the basement dividing line in elevation (the level of the floor in the ground floor, or the basement ceiling). This was sort of like the way a person's eyes adjust to changes in light level, but reversed. The transition was gradual enough that it wasn't too obvious what was going on.

That completes my work on house basements. Now I believe I'm ready to get into the core of gameplay mode. This involves writing the "zombie AI", which basically follows the player based on sight, lights, and noise. Then there's all of the item pickup (stealing) gameplay logic. Some of this is already implemented. I'll write more details about these topics in my next blog post.