Ludum Dare 23 postmortem 3

(Part 1 | Part 2)

Making Enemies

Saturday I developed Enemy AI and Food. The first Enemy was actually done Friday, simply a renamed, re-colored (red) variant of Player with a collision event to handle contact with the Player, and the control scheme modified to have the Enemy bacteria follow the nearest Player bacterium, not the mouse.

What resulted from this was actually surprising: a very difficult to kill colony of enemy microbes that would follow the player closely, and then stand their ground, obeying their “too close” and “just right” rules to just park it near you and not press an attack. I was amazed at how lifelike this behavior seemed.

There would be a few collisions at the border edge, reminiscent of a skirmish between two large nations, but that was it. These were due in part to two things: 1) the jiggle bringing bacteria with very high closeness tolerance into contact in spite of their rules against being too close, and a performance optimization that I added which had the bacterium’s motion updated on a timer that went off randomly every couple steps instead of every single step — just enough time for to allow one to blunder into a target that it was really too close to.

I tried to kill this colony with mine, for several minutes, and was amazed at how resilient it was. I’d come close a few times, and it would just split between my advancing colony, and one side or the other would grow back while I tried to mop up the side I picked. Once or twice I was able to push the colony into the edge of the room and kill it off, but it was a real challenge. I liked that, though. It meant my balance was pretty much right on. And that’s not surprising considering the AI had equal strength and numbers, so it’s not like I was brilliant in conceiving the Enemy, more just a natural consequence of cloning. But I did like how the behavior of the Enemy felt. It felt very much like an unintelligent mass of bacteria reacting to its environment, fending off an invasion while not displaying any sort of strategy or awareness beyond the borders of the colony.

The next thing I did, I created a new version of this Enemy and got rid of the “too close” rule, and these guys became aggressive, kamizake-like units that would bombard you like antibodies. It still didn’t matter, since as long as you had enough bacteria in a colony, they’d just reproduce so fast that the birth rate would overwhelm the death rate — it’s just a matter of Perimeter < Area, simple as that.

I realized that without some means of capping birthrate, the game would be a stalemate. The solution for this was obvious: provide a Food resource and make replication cost food. I started each group with a small amount, and then added 2000 to the game as pickups, randomly strewn about the room. The random() function comes in handy a lot in the course of developing this game, it turns out. When I develop, I like to get my parameters close to where I think they need to be, and then fuzz them up a bit with a random value. There’s a very handy function called gauss() from GMLScripts.com that allows you to select a more natural random distribution, centering around a given value with statistical standard deviation.

Adding Food to the game effectively nerfed the Enemy AI . It would stand strong as long as it had food, but as soon as it ran out, you could wipe it out. The Player, being smart enough to grab food, was in no way being prevented from doing so by the Enemy. In fact, the Player ended up kindof being pushed toward the food by the advancing Enemy colony even if they weren’t smart enough, The Enemy colony just followed the Player around, and starve itself out while the Player swept up all the food in the room.

So to fix this, I created an Enemy variant that would target the nearest bit of food. Adding one of each variety to the room, I tested it out and found that I had a voracious and nearly unstoppable Enemy colony, which rapidly consumed all the food, spreading far faster than the Player could, and thereby had a immense edge in the all-important food resource in short order. The only way to win was to hope to start out close enough to the Enemy’s starting point that you could defeat it quickly before it had a chance to spawn many cells. The critical ones, of course, were the Hungry guys. If they got out of control they would spread over the screen exponentially, fueling the Enemy colony with so much food that it would overwhelm the Player, regardless of how hard the Player tried to gather his own food. So killing off the Hungry AI was an all-important strategy at this stage.

By now, I had a very challenging game, which was too hard and depended mostly on luck to give the player a chance of winning at all. Needed tweaking.

First, I tried nerfing the Hungry AI in several ways:

  1. Make them expensive. Hungry AI cost 5x as much as the others. (Later adjusted this down to 2x).
  2. Give them a smaller range. If there’s no food nearby, they can’t target it, and will revert to Aggressive behavior or Stand behavior. At first I cut the food detection range way down, then later dialed it back up a bit.
  3. Randomize split behavior. Originally an AI would split into itself and another AI of its same type. I changed this to allow each type to randomly pick what type it would split into, and weighted it slightly so that Hungry is half as common as the others. This was offset somewhat by the fact that now the other two types of AI could also split into Hungry AI, which meant that a strategy of focusing just on the Hungry ones to eliminate them quickly would fail, since the other types would replicate Hungry AI also.

Even so, the Player still had a hard time. I felt that in order to equalize the balance, the Player would need to have a Hungry variant as well. I did not plan for this originally, and if I had I would have implemented a bit differently, using a parent object to group the Player object variants under, in order to make things like counts and checks for 0 players a little easier, as well as to save somewhat on the amount of duplicated code in both variants. By the time I decided the Hungry Player variant was necessary, I was running out of time and needed to add it to the game quickly, so refactoring everything wasn’t going to fit. In retrospect, I’ll probably always assume going in that no matter how simple an object might be, it should probably at least have an empty parent object, for grouping purposes.

Now that I think about it, this grouping is necessary for targeting, as well — I believe my poor Enemies have a bug in their programming, by which I mean they’re never going to target your Hungry Players, which as it happens is a critical strategic error on their part. I’ll need to fix this in a later revision, bite the bullet, and refactor everything with proper inheritance.

I made the Hungry Player behave a little differently from the Hungry Enemy. First, the Hungry Enemy will always be hungry, chasing food as long as there is food within its range. If it runs out of nearby food, it turns into one of the other two types. But the Hungry Player, it has an appetite. Once it eats its fill, it stops being hungry. In fact, once it is full, it splits into as many normal Player objects as it ate food. These are bonus Players and do not consume food.

I only had a short amount of time to playtest at this point, but it felt like things had turned back to the Player’s favor a bit too much, and the challenge wasn’t as great as I had hoped. As well, the game doesn’t have as much variety or depth as I would have liked — there’s a simple, easy strategy to immediately start spamming Hungry players (by right-clicking near regular Players), let the Hungry ones spread and multiply, going to town on the food nearby until you had sufficient numbers to withstand and wipe out the Enemy.

Toward the end, I finally hit upon my preferred control setup, and raced the clock to get it implemented and debugged before the compo deadline. I ended up fixing my click-frenzy problem by leaving the “follow mouse arrow” function for those Player bacteria who do not have any target, but are within the range of influence of the mouse. Idle Players who have spawned too far away from the mouse, enemies, or food, will just sit there, jiggling, until the mouse comes close enough for them to sense it, at which point they’ll start heading towards it. Left click, and they’ll target the coordinates of the click and cluster there. Right click, and any Player bacteria within the “too close” distance will receive a signal to be Hungry.

Faking Performance

One other thing I did on Sunday was a performance tweak to alleviate slowdown if framerate does become an issue. I didn’t invent this, but had seen it in someone else’s code and thought it was clever, and it worked well in my game.

The technique involves checking the current fps against the room’s speed, and adjusting the speed of all the instances in motion by multiplying it by room_speed/fps. Thus, if fps dips below room_speed, the speed of motion increases to compensate for it, to give an apparent relatively constant speed. The technique doesn’t prevent slow frame rates, but does make a lagging frame rate less noticeable. There are a few liabilities, however, with the technique: if fps goes very low, speed increases to compensate high enough that it can result in missed collisions as objects pass over each other without actually intersecting. The other problem with the technique is that it does nothing for the animation frame rate of a sprite. In this game that’s irrelevant, because none of the sprites in the game is animated (the food sprite spins, and would appear to spin slower if fps dips, but that’s it, and it’s non-essential). I don’t believe that my objects are likely to move so fast that there are collision misses, but it’s possible.

There’s never enough time

I ended up uploading my entry with about 2 minutes remaining. Just under the wire! There were more than a few things I didn’t get to that I’d hoped to. As a result, I felt a little down about dropping features, at the same time as I felt elated about getting it done. I probably could have had a few of these in the project if I’d not had stuff I needed to do on Saturday, and devoted the entire weekend to LD48:

  • If I continue developing this, I’ll need to sit down and brainstorm some other things that bacteria can do in order to add the variety I wanted back into the game. I can probably come up with a few ideas.
  • I also had wanted to make a multi-way game, such that there could be several competing AIs in the petri dish along with the Player. Implementing this would be less than straightforward, due to the need to target several different types of enemy-relative-to-me without being able to utilize parent grouping, but still can be done, just with a lot more complex conditionals statements for target acquisition.
  • I wanted to add a poison or waste material which would inhibit growth or kill off bacteria that touch it. I didn’t have time to build that. I did get started on something that I was going to turn into that, but I didn’t complete it. I don’t remember exactly when the idea came to me, but during the weekend as I tried to brainstorm ideas for variety, I decided that when bacteria die they should leave behind a corpse. I created a Waste object, and coded it to be created when a bacteria dies. It was an empty object, but I intended to come up with a way for them to be poisonous or perhaps slow down bacteria that end up in collision with it. However, adding all these dead objects upped my object count too much, and as a result I had to be concerned with performance. To alleviate the performance hit, I utilized a drawing surface technique (basically blitting). I had to modify my plan for the Waste object, in the following manner: I created a drawing surface the size of the room, and a special object to control it. In the Waste object’s create event, I had it switch the drawing target to this drawing surface, then immediately destroy itself. The surface controller would then draw the surface to the screen in every step of the main game loop. This alleviated potentially hundreds of separate Draw events for the Waste objects to each draw themselves every step, as well as whatever other Events I might have programmed for them. But this unfortunately took away the possibility of using the Waste objects for something else. I liked the way they looked when added to the game, it helped give the background, which is otherwise just a flat color, a little more variety.
  • I had also intended to add a room_clear and leveling system, but abandoned it in the last hour due to the time crunch. If I go back and implement this, I think a neat technique would be to use the background surface to modify the odds for the food to appear in the next level. I have two notions on this: one, to turn last level’s waste into a nutrient concentration for next level; or two, use the waste areas as dead zones where nothing will grow, and where bacteria will be slowed or die if they enter. This would be a fun challenge to pick up and try to implement, because at the moment I don’t actually know how to program it, and it would be fun to figure it out. I would basically treat the drawing surface like a height map, and allow the height to alter the distribution of the food. Beyond that, I might need to come up with some kind of invisible object, or dynamically create a sprite out of the many sprites of the dead corpses, and use precise collisions to handle my slow or poisoning routines. This could actually be very cool if I can get it to work, and I love figuring out the puzzles to get new things that I’ve never tried before to work.
  • I also didn’t get to make a Start screen or a Game Over screen.

Conclusion

At the time I submitted my entry, I was simultaneously relieved that I had squashed all the bugs (that I knew about) by deadline, yet disappointed that I hadn’t made the game everything I’d envisioned. I felt the game lacked depth and variety. It’s playable, but not that interesting as it stands. On the plus side, it looks nice. I did some cool things with color and transparency, made very effective use of a variety of stochastic techniques, came up with a few interesting AI behaviors.

Yeah, I dropped some features, and I didn’t even try to make music — a nice ambient style would have fit the game really well. Music is my greatest weak point as a solo developer — I basically can’t do it. Not well enough to be worthwhile and not annoying. I plan to keep working at it until I can come up with stuff that doesn’t hurt my ears too bad, but until then my games will have a silent soundtrack, apart from sound effects.

That said, I’m very happy that I participated and finished a project at least to the point where it was playable and not horrible. I would be surprised if it ranks very highly, but having looked at a number of other entrants in LD48 #23, I feel better about what I accomplished, and I feel that it stacks up against the others reasonably well. I’m looking forward to seeing how it does in the voting.

Leave a Reply