Sunday, April 3, 2022

Spiders

I've explained and shown how rats were added to 3DWorld's procedural buildings in a previous blog post. I had a lot of fun adding rats, so I felt it was time to add another type of animal. This time I wanted the movement and animation to be different. Maybe something that could climb walls, with more legs. How about spiders? I've never added spiders to anything in 3DWorld yet, so this sounded like an interesting challenge.

It seems that adding spiders was a similar amount of work compared to adding rats. It's hard to say for sure because I'm not quite done with spiders, but I'm almost there. I was able to use a lot of the existing code from rat placement, movement, drawing, and animation. However, writing an AI that can walk on walls, floors, ceilings, doors, and room objects was far more difficult than navigating around on the floor alone. I spent over half my time on this task. The details can be found below.

Animations

The first step was to add leg animations. No, wait - the first step was to generate the geometry of a spider that I can then draw. I don't want to repeat that process I used for butterflies where I had to split the model into multiple parts so that I could animate it. I can construct a spider from ellipsoids (squished spheres) for the body/abdomen/eyes/joints, and cylinders for the legs. Then I can assign my own custom vertex attributes for the joints and leg segments to indicate which segment of which leg of which side of the body it's on. I can then use this information for animation inside the vertex shader without having to split or otherwise modify the model. It would be nice to have a framework for this alternate movement and animation system.

I had to watch some videos on YouTube (such as this one) to figure out how spider legs move. It looks like I only need to create a single leg's motion, apply it to alternating pairs of legs from back to front, then mirror it to the other pairs of legs and the other side of the spider. The first and third leg pairs move together, and the second and fourth leg pairs move 180 degrees out of phase. Similarly, the left and right legs are 180 degrees shifted from each other. I created several joints which I call the "hip", the "knee", the "ankle", and the "foot." I have no idea what the correct terms for these are when applied to spiders, so I decided to map the three joints and corresponding three leg segments to the parts of a human leg. The hip moves with the body, the knee moves relative to the hip, the ankle moves relative to the knee, and the foot moves relative to the ankle. I initially created a huge house-sized spider to test animations on. All it took was several hours attempting to fit sine waves to the X, Y, and Z dimensions of these three joints to get them moving properly. It's not perfect, because spider legs don't move in a perfect elliptical path, but I think it looks good enough. Their legs tend to move so quickly that you can't easily tell what the motion patterns are anyway.

Here's an initial spider animation and floor/ceiling/wall walking test.


Movement

The task of having spiders walk on the floor was trivial. All I had to do was copy the code from rat movement (or simply draw spiders instead of rats!) Handling walls and ceilings was far more difficult for several reasons. First, movement isn't within a plane. There are turns that have to be made gradually, without clipping through objects in the process. Second, the up direction has to change based on the orientation of the surface the spider is walking on. I can't always use +Z (vertical) for up like I do with rats. Third, it's far too easy for a spider to get stuck in the spaces between multiple nearby objects.

I was about to list "path finding is more difficult," but I stopped myself because that's a lie. As far as I'm aware, spiders are pretty dumb. They don't make complex path finding decisions or otherwise think ahead very much. They simply walk in one direction until they find something. Or at least that's what they appear to be doing, so I'm sure I can get away with making my spiders act as dumb as real spiders appear to be. I don't think I even have to make them chase the player. They can instead just sit there on the floor or hanging from webs on the ceiling, waiting for the player to walk by and get bitten. Their natural defense mechanism is to bite whatever is about to step on them.

Right. I suppose now I should explain how I got spiders to walk on the walls and ceilings. I worked on that code in between implementing all the other features I discuss in this post, and then again after I was done with everything else. And then again later for good measure. I suppose I have to write enough about this to get across the idea of how long this task actually took to figure out. You can skip over this next part if you find my technical content hard to follow.

I started out by iterating over all of the surfaces and objects of the house that were near each spider. This includes walls, ceilings, floors, doors, furniture, appliances, stairs, etc. If the spider hit something that was round or otherwise not a cube shape, it would bounce back and pick a new direction. (Cubes are much easier to start with.) I then found the surface the spider was currently walking on from among all of the remaining cubes based on the spider's up vector. In addition, I found the nearest secondary cube face, which represents the surface the spider is most likely to encounter next and must adjust its position and direction to account for. I calculated the distance between the current and next surface and used the relative distances to interpolate a smooth path for the forward and up directions to transition from one surface to the next.

This worked well for most cases involving floors, ceilings, and walls. Unfortunately, it didn't work at all for outside edges of walls (such as around door frames) or corners where three different cubes/surfaces came together. Obviously, if I'm only tracking the two nearest surfaces, I can't properly handle three surfaces at once. Sometimes the collision system would switch between two of the three surfaces each frame and the spider would turn around constantly. In addition, spiders were always getting stuck between furniture and walls because that system couldn't handle a spider simultaneously colliding with two surfaces of the same orientation. So any time these situations came up, the spider would either get stuck forever, jitter/spin around randomly, or clip through an object. Clearly that's no good.

I couldn't find an incremental fix for any of these issues, so I threw out the code and rewrote the whole thing from scratch. I treated the spider as a sphere (well, technically an ellipsoid because it was shorter than it was long/wide). The goal was to always have the sphere touch one or more surfaces and never intersect an object or float in space. If a movement pushed the sphere into an object, I used collision detection/resolution to push it out. If the spider moved into empty space, I moved it back and selected a different direction or pushed it to touch the closest object. Rather than trying to special case all the different cube faces/orientations, I simply generated 50 random movement vectors in the roughly forward direction and picked the one that moved the spider the furthest without colliding or entering empty space. I also added a small preference for motion in the "up" direction to induce more frequent climbing behavior.

This new system solved all of the previous problems, but also introduced some newer, lesser issues. For example, I still didn't have a good solution for the outside cube edge case. What I mean by this is if a spider is walking along a wall and encounters a doorway, it should walk around the edge of the door frame and onto the opposite side of the wall. The reason this case isn't handled is because only the current surface is being tracked. If the spider walks in a straight line it will go off the end of the wall before it collides with the edge of the door frame since the edge isn't in its path. I eventually came up with the idea of searching for the orthogonal edge of the previous surface when the spider had run into empty space. This at least works with right angle outside corners from the same cube. Then I realized I could simply move the spider to the closest point on the cube and it would somewhat follow the wall.

At least in theory - when I implemented this the spider just clipped through the door frame. I tried several approaches and they all had the same outcome. I was determined to make it work and stayed up until past 2AM trying to get this right. (Yes, it was a Friday.) I eventually gave up and went to sleep, then figured it out in about 10 min. the next day. The spider logic was right all along, it was the door frame logic that was wrong. I thought the door frame was supposed to be added as a thin wrapper along the edges of the wall, but instead it was added as an extension to the wall and was in fact hollow inside. Spiders were following the wall itself, and this is what made them clip through the door frame and get stuck in the empty space where the wall should have been. That explains why I had a similar problem with rats clipping through the door frames that I was never able to solve!

This was an easy fix, and after that it ... almost worked. There's still some instability where spiders will randomly switch between walking up and down along the door frame. It doesn't happen too often, which makes it that much more difficult to debug. I *think* what's going on is that some of the larger spiders are wider than the wall and can't quite balance on the edge of the wall without falling off one side or the other. When they do this, it triggers that same edge-of-wall following behavior and they switch directions and try again. This only lasts a few seconds until they either finally align to the center line of the wall, or eventually reach the top or bottom of the door frame. So maybe this temporary, um, indecisiveness is acceptable. Or I could make the spiders smaller, but then I would have to more accurately handle things like wall and door trim because they would start to clip through these thin objects. I think at this point I've spend enough time on this task and can move on to something easier.

I placed all of the spiders in the basement and on the first floors of houses and office buildings, just like I did with rats. However, spiders don't always stay on the first floor. They can climb the stairs, and eventually some of them make it to the upper floors.

Here is the result of my movement work.


Scalability

The next big question is, how many spiders can I put in a single house? At first it was very slow because I had forgotten to add view frustum culling and occlusion culling. Fortunately, this works the same for spiders and rats, so I was able to reuse the code. With some minor amount of code optimizations I had the system scaling to 1000 spiders with a minimal drop from 103 FPS to 89 FPS, which is something like 100 spiders in each room. That's ... a lot of spiders, especially when they're this large in size. Good luck trying to run through even one room without getting bitten! (Yes, I've tried it, and I can tell you that my survival rate was very low.) Anyway, you end up with something like this screenshot.

This is the result of adding 1000 spiders to the ground floor of a single house. There are about 80 spiders in every room (12 rooms total), all over the walls, ceilings, and floors!

What's even scarier than a room with 100 spiders? A spider on a web by the light that casts a huge shadow on the floor below. And what's even scarier than that? I don't know, but I'll let you know when I come up with something else to add. In the meantime, I leave the answer up to the reader's imagination.

A spider hanging from the ceiling light casts an ominous shadow on the floor. The light is a point light source, even though it really should be a rectangular area light.

Webs

I've added visible white web strands since taking that screenshot above, so now you can tell the spider is hanging rather than floating in midair. They like to drop down from the ceiling when they collide with each other or reach an obstacle they can't easily climb on such as round lights and the railings of stairs. (I haven't yet figured out how the movement logic works on curved surfaces like this.) Note that the player can also collide with spiders and push them around somewhat when they're on ground, on the wall, or on a web. My daughter suggested adding spider webs in the corners of rooms. Maybe I can add that at some later time.

Spider dropping on a strand of spider web from the top of the stairs.

Gameplay

Next, I had to figure out how spiders interact with the player in zombie gameplay mode. They don't make any noise, so that interaction mechanic is out. As I mentioned earlier, I didn't want them to actively chase or follow the player. They simply ignore the player and do their thing, but they will bite the player if stepped on or bumped into. This does a small amount of damage, but more importantly it poisons the player so that health drops slowly over time until the player is "healed." This means I also had to add medicine, and the best place for that is inside bathroom medicine cabinets, which can now be opened by the player with the interact key.

Medicine cabinets with mirrors that are placed above sinks in most house bathrooms can now be opened by the player, revealing medicine that will restore full health and cure poisoning.

I haven't yet added logic to allow the player to pick up, carry, and drop spiders. I'm not sure what sane person would actually attempt that with spiders of this size. Besides, I've already implemented that mechanic for rats so it wouldn't really be classified as a new feature anyway.

I'm sure there are many ways to continue with this direction of my work. I could add other crawling bugs now that I have the "N-legged climbing bugs" code. Maybe beetles or ladybugs? I could spend another few weeks adding flying birds or insects *inside* buildings. Or I could have a spider squishing mini-game for the player.