Finding Boidy - A Deep Dive Into Fish AI

    This site uses cookies. By continuing to browse this site, you are agreeing to our use of cookies. More details

    • Finding Boidy - A Deep Dive Into Fish AI

      Hello again, my research associates! It’s been almost a year since I last spoke with you all. While I have been hard at work on many areas of R&D, I want to return us once more to the watery depths of the Crush Depth chapter.


      Since the last time we spoke, I have spent a fair amount of time developing our engine’s capabilities to simulate ambient AI. What is ambient AI you may ask? Excellent question, since as far as I am aware, it is a term I have coined myself (or I am bad at using a web browser).

      Ambient AIs are the artificial intelligences in our game world that essentially have no bearing on gameplay. For the most part Ambient AI can be considered wildlife; Birds, bugs, fish, small mammals like rats, etc. While not an incredibly important aspect to our game, ambient AI helps in making a game world feel more alive.

      A core philosophy I have when making a space in a game is that it should never feel static or flat. What I mean by that is in the spaces a player traverses, there should always be some dynamic element whether that be a fan spinning, a flickering light, or a plant swaying in the wind. Why is this important, you may ask? The sensation of motion helps make the environments feel more believable. In our games we want the places you traverse to feel like they are lived in, that they are real locations.

      So back to Ambient AI then – why is this important? Why not just make the games be filled with dynamic props like spinning fans and burst pipes spewing steam? While true that both these examples create motion and help make the world feel a bit less static, they do not help make a world feel like a space that is inhabited. Playing in a world where the only motion you see comes from machines or particle effects generally will create an isolated feeling, such as in Portal. This is not the direction we are aiming for. The world of Operation: Black Mesa and Guard Duty is very much alive, in whatever tragic state being alive may mean, so we need creatures powered by Ambient AI systems to share the world with you.


      When starting work on Crush Depth it was a known element that a bunch of alien fish would be needed as environmental detailing. It would be very boring to see a ton of empty tanks in an aquatics research lab devoted to Xenian life. The next step up would be to populate the tanks with known fish, which is exactly what I did. After the first pass of this chapter, our tanks were filled with Ichthyosaurs and Xen leeches. Unfortunately this didn’t quite solve our problem. It turns out that walking by a dozen tanks filled with the same models playing the same animations gets a bit old fast. And even outside a game feel standpoint, it looked ridiculous. The Black Mesa scientists had captured every single leech in Xen and stuck them in some past cold-war bunker, or at least that is what it felt like. It became obvious we need more fish and more life to fill not only Crush Depth, but Xen as well.

      But that is getting ahead of ourselves still. Its great to know we need fish, but first we need to know how are we planning on using the fish if we even had them. There are realistically three choices we could go with, or a combination thereof, each having their own pros and cons:

      Choice A – Use the func_fish_pool entity that comes with Source.

      Already exists – no work needed from the programming team.The entity is quite odd in how it is structured – creating very laggy movement of its fish.
      Very easy entity to work with inside of Hammer and Source.The entity is very limited in how the fish in its simulation move, to the point of it being emersion breaking.I could go on for a while here.
      Proven – as in, it is so old that any bugs are well known by this point so there is relatively little technical debt that this could create.Not much flexibility offered to the designer or artist outside of setting the model to use, and how many fish to create for the pool.
      The fish can only be spawned and swim in a sphere (really a flat circle) – there is no height.

      Choice B – Create pre-defined swimming animations for schools of fish.

      Offers extremely controlled movement of the creatures by animators to make interesting school simulations.If a tank changes size or shape during playtesting, an animation could be completely busted as it no longer fits the tank.
      Only requires a single draw call per school (a draw call is when you are asking the Graphics Card to do some work to get a model appearing on screen to put it simply).Offers no way for the fish to interact with what the player is doing – as they will all be swimming on pre-defined animation cycles.
      Proven – there are other Source Engine games that take this approach so it is very possible.Eats up a lot of animation time when that could be better spent on our much more important assets.
      No programming work whatsoever.Not usable by those who want to mod the game and make their own maps, really.

      Choice C – Make an AI specifically for fish.

      Allows fish to be reactive to the player’s actions (such as swimming away from a player if they get too close).Requires a draw call per each fish model, instead of per school.
      Is the most flexible system proposed in creating schools of fish.Requires more work from the programming team than any other solution.
      Light on the animation budget (only requires a single swim animation per fish model that the engine can then play with to avoid repetition).Is now a potential bug nest as is any new feature – so the most technical debt of any solution.
      Abstracts some of the work for artists and animators to the designers.Potential perf issues (as in – losing FPS).

      With all of these options in mind, we discussed our choices and elected to go with option C. The pros outweighed the cons. There were a few things going into this I knew needed to be objectives for the first of our Ambient AI systems to be worth the investment we were putting in:

      • The AI had to be extremely reusable. Our fish AI needed to be capable of supporting any possible fish an artist wanted.
      • The AI had to be as lightweight as possible. It would be no good to eat up a ton of CPU cycles on some trivial fish that are not core to gameplay.Thus, the simpler, the better.
      • If we develop AI for the fish, we should take advantage of it. The fish should to be reactive to the player to helps sell they are living things in the world and what they think of the player.
      • We cannot create a bottleneck of saving and loading, even if running the game itself is fine.

      An example of func_fish_pool. Note all fish are at the same height – one of the limitations of this entity provided by base Source.


      So, time to tackle each of these issues one at a time .A good starting point for our fish AI was to implement a basic boid algorithm (not to be confused with the Boid aliens in Half-Life). A boid algorithm is a fairly simple approach to creating schooling and flocking behavior – useful for fish and birds in particular. It has been used in hundreds of games at this point. There was no need to re-invent the wheel, especially when this solution has been proven to work with hundreds of different models – which is one objective here.

      I won’t bore you with technical details, so just know that referencing some online papers and some video examples, I had something up and running in a day. If you want an exact explanation of how boids work there are many fantastic tutorials out there, so I do not feel the need to go into the algorithm here. That said, there were a couple elements I changed from the traditional approach to suit our needs:

      • My implementation consists of two entities, npc_fish and env_fish_school.Only the env_fish_school can be placed by an artist or designer in Hammer. The env_fish_school in a way acts as the brain of the entire school, while each npc_fish is responsible for creating the realistic movement based on whatever the env_fish_school is broadcasting.
      • Relevant to the above, I exposed many, many settings to the designer which most other games hard-code and hide away to not overburden the artists and designers. Hard-coding something means you can only change values if you have access to the code and know how to do a small bit of programming.

      For the purpose of making this entity as reusable as possible– it has many more settings than most other NPCs.


      With the first pass of the fish AI system done , it was time to start testing it and seeing its limits. How many fish is too many fish? An important question for all programmers to ask themselves, surely.

      I started small.10 fish. It worked.30 fish? Looking good.50?Still going strong, no FPS about 100?Yep – we’re still working fine…and on a really old laptop to boot! It was now….3AM.I tend to get into my work and have destroyed any semblance of a sleep schedule. Having tested the system could support 100 fish without a hitch, I provided an update to the team, pushed my stuff out so anyone could tinker with it, and then went to bed. In my absence another designer, Brandon Smith, kept at it where I left off.300 fish.500 fish.1000 fish.

      It turns out we had hit a bottleneck. At 500 fish we started to get some perf loss but nothing terrible. At 1000 fish in a developer test map, the loss in perf was noticeable and eating up a decent chunk of the frametime. Some may say having 300 fish is enough. Those people would be wrong. We needed fish. Lots of fish. I do not have a problem.

      Something that is incredibly helpful in all fields of work is to get another person’s opinion. In programming this is generally referred to as doing a code-review. Knowing we had some limits I was not pleased with, I got another programmer on the team, Andrew Baay, to do a code review on the fish. We found some areas in my approach that could use some tweaks:

      • Instead of updating each fish every0.01 to 0.02 seconds, we could update each fish every 0.1 to 0.2 seconds. This makes the fish significantly less expensive. The difference is not even noticeable to a human eye, and requires significantly less work by the computer. A win for sure.
      • Fish check where the nearest 32 other fish are relative to them, in order to enable them to school together and avoid bumping into one another. We reduced the fish to only check its nearest 8 neighbors. This also significantly posted perf, while not affecting the fish in any noticeable way.
      • Some other minor changes that have to do with the rendering and putting work on the client vs the server which I will not detail here was looked into and tweaked.

      Changes in hand, we went in to test with a debugger running alongside the game in order to track how things were running in the code directly. With just a few tweaks, our 500 fish had no FPS drops. Our 1000 fish had no FPS drops. We could have up to around 1,500 fish before we started running into problems. And the good news was the problems were not even with the fish. Rather, we were asking the GPU to render way too many individual animated models. There are ways to get around even this limit, but then we would be fighting load times and have to do some fairly substantial engine reworks – so out of scope for what our goals are. Plus, there is no scenario where we need more than say 300 fish active and rendering at once anyways – and we can already do five times that without a hitch.

      Here you can see over one thousand fish being rendered, animating, creating dynamic shadows, and flocking all at once. This is way more than we need at any point in our games. The FPS counter is still holding strong on a fairly low-end machine.


      So cool – we now have a well running lightweight AI. As soon as we have models, we can start adding some new friends throughout the game. We’re good to go…right? Not quite.

      This is good, but it can be better. What we want is the fish to react to the player and the world around them, remember? That is one whole bonus by going down this route. Putting aside Xen, with Crush Depth in mind, you are not in the tanks with the fish. The problem becomes making fish interactive when you cannot actually get near them. What you can do though is shoot the tanks, so that is certainly an area to add in some unique behavior!

      Though you may be unaware, Source has a good AI sensory system -hearing, touch, sight, and yes even smell are all features in the engine. What I realized we could do here is make specific noises the fish care about, and if they hear this type of noise, react accordingly. The ability to scare a fish - this adds even more believability to the creatures, and is yet another property the designers and artists can play with to make every instance this AI is used feel unique to that creature. Maybe some of the bulkier fish do not care at all what you are doing to the glass, while others are quite skittish – if smart about this, the system could be designed to support both.

      Adding this interaction is fairly easy but requires further modification of the boid algorithm to now support fear and how to manage that as both a single unit and as a school. Through some trickery we got that working, via the addition of another sound type. When fish die, they emit a sound only NPCs can hear – and more specifically – that only other fish can hear. This makes it so if a fish is killed, other fish nearby may be alerted to this and swim away – even if it was silent kill such as a swipe of a knife.

      Here you can see the addition of the SOUND_FISH_DIED type.

      Above you can see the code here that says when a fish takes damage, it sends out the sound which means a fish has died.

      And one last bit of code to show is that melee weapons will only trigger the fish to get scared if they hit glass, or a surface underwater (so hitting the ground near a tank won’t scare fish – if they are set to be scare-able).


      The final issue to work out now is handling save data. Obviously, bloating a save file with 300 or more fish is not a good idea. That is a lot of data you are making the computer write to a file and load from a file whenever the map has to be reloaded. This process happens when closing down the game, respawning, etc. Luckily there is a simple way to manage this within Source.

      The fish entity is marked as something that should not be saved in a player’s save file. That means, when you load a game, all of the fish that were once in the map will be deleted since they were explicitly marked to not be preserved. The env_fish_school – the brains of the operation, is still saved however.

      When the map reloads, what happens to make this all run smooth is the env_fish_school will respawn its entities. This creates a small amount of slowdown for loading a map, but nothing compared to the alternative (which could crash the game on lower-end machines).The env_fish_school keeps track of how many fish were alive when the game was saved, and then when that save file is loaded, recreates to that count. So, say you killed 11 of 20 fish in a school. The game will remember that, and only spawn 9 fish next time you load the save file.

      This is a medium sized tank, and it already has dozens of fish here. This would be problematic to save them all.


      Back to the asset front now. By the time we were even at a point where the “lack of diversity in fish issue” became pressing enough to solve, one of our artists, Tyler Barker, had some spare time to lend between other tasks. While Tyler only had time to create a single fish, it would still be a big lift as it literally increased our number of possible fish to select from by 50%. The question became what should this fish be, and luckily enough for us, Valve had an answer. An old cut prototype enemy known as the Archer.

      The Archer as it appears in the Half-Life files (not used in-game).

      While not an enemy in our games, as that is out of the scope of what we are after here, it is still a nice callback for those hardcore Half-Life fans. Within about a week of starting the task, we had a fully modeled, textured, rigged, and animated aquatic friend in our hands. This is only a starting point to fill out the chapter, and indeed, Xen. One core element of Xen we want to capture as a team is the ecosystem of the border world. Despite having this goal, it would be an incredibly poor use of Tyler’s time to create dozens of Xen fish variants .But that’s where a secret weapon came in to fight this issue head on.

      A work in progress render of the Archer made by Tyler Barker. You can see the final version of this fish on the video posted alongside this dev blog.

      A while back we gained a voice actor our HECU and VOX – Jack McDade otherwise known as Amicus. Quickly recording all of the lines needed at the present time, Jack spent time teaching himself how to do 3D organics modeling. His art style in particular is incredibly well-suited for Xen. Hearing our fishy plight, Jack worked with some of Tyler’s sketches to create some more aquatic life to fill the tanks with. We soon had a rather diverse and believable ecosystem on our hands that is still continuing to grow.

      The Pefoy – One of our Xen fish designs, created by Jack McDade based on Tyler Barker’s concept art.


      As always, there is room for improvement, but it is important to not micro-optimize something like this and only address things as needed. Crush Depth and Xen will feel more alive and immersive than ever before, and we can’t wait for you all to visit the world beyond.

      And now…well…

      We have some tanks to fill. Expect a video of one of our tanks here in the near future.
    • I think you should release it if the game stream is completely finished. Because as technology progresses, you fall behind. As new things come out, you can't finish by trying to adapt them all the time. Do not be unfair to yourself! A lifetime is not worth spending on a game! I think you can make your fixes and major updates after the game is released. Most successful teams have done so. It would be arrogant for any player to find a piece of grass un realistic after all this time. In return, we can say that; Hey, player! are damned go to hell :)

    © 2023 Tripmine Studios. Valve, Steam, Gearbox, Half-Life Trademarks of Valve.