While coding the "survival" mode for Dead Panic I encountered a common situation - I had to generate random waves of enemies to confront the player. This is a straightforward exercise, right? Pick a random enemy type, a random position at the screen's edge, and launch them at the player. Hmmn, that looks more like zombies popping off of an assembly line, not proper scary waves. Okay, randomize the timing too - wait a few seconds between zombies. Hmmn again. Now we're getting some boring lulls and occasional impossible-to-beat clumps.
It turns out you can't "random" your way out of this problem. If you want groups of enemies then you really and truly need to code groups, not falling snowflakes. It turns out that "random" isn't that much fun anyway. Dodging random rocks or fighting random objects feels, well, random. You may as well go roll dice for fun.
"Tuning" Your Algorithm
Suppose you write an algorithm to make music by picking notes at random, and ... it sounds terrible, like a cat walking on a piano keyboard. Ah-ha you say, I just need to "balance" it. Go find out the ratio of notes in "Stairway to Heaven" and match that ratio. OK, wow, lot of G's in there, that must make the song good. And... still sounds terrible. Why? Because good music has certain properties, just like good level design, as Daniel Sussman and Eric Brosius from Harmonix have pointed out: (Slides) (Audio)
As they note, good music and good level design comes from:
* Variation vs. Repetition
* Tension vs. Release
* Pacing & Progression
The man page for arc4random doesn't mention those elements. Graphic design and architecture students should recognize them - they're contrast, repetition, harmony, and variety extended into the fourth dimension (time). I expect the article "Was Frank Lloyd Wright a Good Level Designer?" any day now.
Amber Waves of Pain
I'm off topic, so what's the real solution to my waves of enemies problem? Once I started thinking in groups it was obvious: Create enemies in batches, position each enemy based on the position of the last enemy, determine the delay for this enemy by how long it will take to kill the previous enemy. The minimum delay before the next group depends on how long I predict it will take to kill this group, plus a few seconds to reload / reposition. This eliminates the "impossibly difficult grouping" that crops up with the random snowflake method.
What about tension and release? Where can I buy a little of that? If you said arc4random you're not paying attention. I can do a modulo of the current time, or perhaps a sin() if I want the intensity to rise and fall. Surprisingly, my final code only contained *one* call for a random number - to decide whether the new enemy should appear to the left or the right of the old enemy. Everything else - enemy type, distance from last - was designed, not random.
You might use randomness to vary these parameters a bit to add some variation, but the underlying structure must still come from the code, not from hoping that arc4random() will do the right thing. The law of averages says it won't. None of this is a replacement for a real, live, human level designer, but it will come in handy the next time someone asks you for "random" waves of enemies - you'll know that less random is mo better.
Bonus tip: You rarely want a random number between 0 and currentDifficulty; the average of that range would be half of currentDifficulty, meaning half of the results will be far easier than you intended. Ever play D&D? Notice the difference between 3d4 and 1d12? Unless you want wild fluctuations in difficulty you probably want currentDifficulty plus or minus a few percent.