My game consists of 'waves' of objects called 'spawners', which once every certain amount of time (their firetime), move to a new place on the screen and spawn an enemy. Each wave has 4 important properties:
1: the amount of spawners the wave has
2: the interval of time between creating new spawners
3: the total length of the wave
4: the types of spawners that can appear in a wave (represented as a std::vector<std::pair<std::string, int>> where the string is the spawner name, and the integer is its spawn weight.

The game works by picking a random spawner from the possible spawner types (with a weighted rng) every new spawner interval. Currently waves are set and are loaded from a file at runtime.

My problem is that I cannot find a good way to randomly generate waves that feel balanced and natural. Currently, I am trying to generate waves based on a difficulty value, mostly using weighted random number generation. However, this does not produce balanced waves that correspond well to the target difficulty. Even after trying several different techniques, I am unable to get a system that generates waves that fell balanced and natural (like the ones hand made).

Is there any way to generate waves that feel natural, based off of the difficulty value? How should I approach this problem?

Also if it's of any help, each spawner also defines its own difficulty value.

Example Waves:

Easier wave:
SpawnerTypes = {'BasicSpawner': 100} (basic spawner has a difficulty of 10)
SpawnerAmount = 5
NewSpawnerInterval = 4
WaveLength = 30
This is a good easy wave because it the spawner it has is simply shoots bullets that move in one direction at a slow speed, which are easy for the player to dodge. It also does not overwhelm the player with large amounts of them, and the wave is not too long

Medium Wave
SpawnerTypes = {'LaserSpawner': 60, 'BasicSpawner': 40} (basic spawner has a difficulty of 10, laser spawner has a difficulty of 20)
SpawnerAmount = 8
NewSpawnerInterval = 3
WaveLength = 30
This is a good medium wave because its combination of spawners requires the player to be paying close attention to multiple parts of the screen other than just right near the character, while still remaining relatively easy. It also spawners more spawners, but balances that by having a small interval between spawners and a small wave length.

Hard Wave
SpawnerTypes = {'RicochetSpawner': 100} (ricochet spawner has a difficulty of 60)
SpawnerAmount = 2
NewSpawnerInterval = 10
WaveLength = 30
Note that this wave has only one spawner type and only 2 of them, but is still much harder than the other listed waves This wave is hard because the spawner it contains is challenging to keep track of, and in large numbers quickly because almost impossible, so it only spawners two spawners. However, the wave still lasts for 30 seconds, which provides the player with a decent challenge while not completely overwhelming them.

Extremely Challenging Wave SpawnerTypes = {'RandomSpawner': 30, 'LoopSpawner : 70} (random spawner has a difficulty of 90, loop spawner has a difficulty of 60)
SpawnerAmount = 4
NewSpawnerInterval = 5
WaveLength = 40
This wave is a perfect example of a wave which is extremely challenging, but is possible for a skilled player. It has two very hard spawners, one of which is very unpredictable (hence the name 'random'). It also spawns 4 spawners, and the wave lasts a total of 20 seconds longer than when the last spawners is created

  • \$\begingroup\$ The first step here is to unpack what feels "balanced" and "natural" to you, for your particular game and the way you play it. "Like the ones hand made" is a good inroad to use. List out some hand-crafted waves that feel the way you want them to feel. Then record some random results you got that feel bad. Start examining what patterns distinguish these two lists, or what attributes we can use to quantify the suitability of different waves. Add that info to your question, and we can propose methods that generate results more like your good list and less like your bad list, and evaluate them. \$\endgroup\$ – DMGregory Feb 25 '20 at 1:03
  • \$\begingroup\$ @DMGregory I added some examples of good waves \$\endgroup\$ – NipIsTrue Feb 25 '20 at 23:56
  • \$\begingroup\$ Those are some numbers. In order to help strangers who have never played your game understand what makes those numbers good, please try to elaborate with some of the analysis I asked you to do above. \$\endgroup\$ – DMGregory Feb 26 '20 at 0:12
  • \$\begingroup\$ @DMGregory I tried to add a basic explanation of why each wave is classified how it is. If you have any more suggestions for info that would help, please let me know \$\endgroup\$ – NipIsTrue Feb 26 '20 at 0:38

One of the most rigorous ways to ensure that a procedurally generated wave, or other form of procedurally generated levels is balanced to a particular standard is to use automated testing. When you make a wave, you (quickly) run an AI through it, with the reaction times and level of spatial awareness that you want the player to need, and if the AI can't beat the wave, you throw that wave away and generate a new one. I've only seen this approach actually done a small handful of times, most people prefer to just tune their generators by ear until they get something that's generally good enough.

Waves in bullet hells usually look geometrically interesting, so I think making things look more "natural" might mostly just come down to placing your spawners according to interesting math.


Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.