Take the 2-minute tour ×
Game Development Stack Exchange is a question and answer site for professional and independent game developers. It's 100% free, no registration required.

I'm stuck to find a good solution about spawning enemies. Think of it as a tower defense game.

What I'm trying to do is:

Create keypoints for enemy types and spawnrates, as

  • Wave0 = Enemy0 * 1.0
  • Wave5 = Enemy0 * 0.5 + Enemy1 * 0.5
  • Wave10 = Enemy1 * 0.5 + Enemy2 * 0.5

Interpolate these values and spawn enemies so that Wave3 would be Enemy0 * 0.7 + Enemy1 * 0.3

I'm trying to end up with a multidimensional array of int, representing enemy types and waves, such as;

EnemyWaves[0] = {0,0,0,0...} // array length = 20
EnemyWaves[3] = {0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0}
EnemyWaves[5] = {0,1,0,1,0,1...}
EnemyWaves[10] = {1,2,1,2,1,2,1...} 
share|improve this question
    
I have severely edited your question focusing on the problem and removed trivial and irrelevant parts. If you think I missed something please say so! –  Krom Stern yesterday

3 Answers 3

up vote 0 down vote accepted

First of all you need to interpolate Enemy ratios for level you need. This is quite trivial - for each enemy type, interpolate between ratios of known levels.

Knowing exact ratios for the level (e.g. (0.45, 0.45, 0.1)), there are several ways, each good for specific usage scenarios:

Batch

Add N items for Enemy0, where N = RoundUp(Enemy0_Ratio * ArrayLength). Repeat for Enemy1 and etc. We round up to make sure array is filled completely. You will end up with an (0,0,0,0,0,1,1,1,1,1) pattern. To make sure smaller ratios are not ending up outside the array - fill array with smaller ratios first, then reverse it. (0,0,0,0,1,1,1,1,1,2)

Interleaved

Add temp variables and make a while loop in which add EnemyX_Ratio to a variable X. When any X variable exceeds 1 add that EnemyX type to array and subtract 1 from X. Exit the loop once array is filled. This way you will have a fair distribution of enemy types alike (0,1,0,1,0,1,2,0,1,0)

Fairly Randomized

Do everything as in Batch approach, but add a final step to it - exchange random elements pairs with each other. This way you will have a randomly filled array, but with a proper ratio between item types. e.g. (1,0,2,0,1,1,0,1,0,1)

share|improve this answer

This problem is a bit complex. There are multiple approaches you can take. None of them are "wrong", per se, but each one is better suited for a certain number of enemies/level layout/(etc.).

Regardless of the approach you take I find it very useful to extend Unity's editor for cases like this. The approach I'd use for a simpler tower defense game would be based on a timeline file.

I wrote a small demo for a friend who was new to Unity using this approach.Excuse the naming conventions, which are all over the place for some reason that I can't recall. (Can I blame MonoDevelop?):

First I defined the spawned types in an enum:

public enum SpawnableTypes
{
    enemy_NullValue,
    enemy_Sponger,
    pickup_SlowTime
}

As you can see I only have one enemy type, and one pickup type (NullValue is just there to indicate a blank enemy).

I also used a convention that I'm not particularly proud of (this is a rather old, quick and dirty example I wrote). Each name starts with a prefix that defines the type of the spawned object. So the "enemy_" prefix actually has meaning. I wouldn't do that this if I were to do this again.

Each type would have to be defined here (part of why this is better suited for a simple tower defense game, where each enemy shares some common characteristics)

After I defined the enum, I defined a SpawnManager, and various SpawnController objects:

  • SpawnControllers are objects that spawn one of the types above. There might be one for enemies, one for powerups, and so on. It has a method SpawnChild that accepts a value of the type SpawnableTypes. The controller is to create a GameObject of the correct type when this method is called.

  • The SpawnManager is an object that will load up a file, read the timeline of objects that needed to be spawned over the course of the level, then call SpawnChild child on the appropriate SpawnController. It knows which SpawnControllerto call SpawnChild on using the prefix I mentioned above (again, there are many more elegant ways to do this).

This defines the basis of generating waves. The SpawnableTypes don't have to be enemies and pickups. It'd be very easy to define one to display a message when a new wave is started, and actually defining a new wave is as simple as defining a large delay between two objects in the timeline.

Now I have to backtrack to how the time line is defined:

I used a simple file format (from the comments of my editor):

/* File Format:
 * Line 1: Object 1     Prefab Name
 * Line 2: Time until Object 2 is spawned

 * Line 3: Object 2     Prefab Name
 * Line 4: Time til Object 3 is spawned
 * ... 
 */

While it is human readable, it would also be a tedious and error-prone process to generate this file by hand. So I wrote a small editor to generate it automatically: Editor image

The script is rather simple. I posted it up here so you can take a look, but it's reflects the questionable choices I made, just use it as a base of how to form your GUI.

I have an example of all of this in motion:

The webplayer.

The timeline file that demo uses.

I'm avoiding posting the implementations of the SpawnManager and SpawnController classes because they probably won't be useful. They're somewhat specific to the type of demo I was doing, and they aren't indicative of how they should be implemented (what they do is correct, but how they do it is suboptimal at best)

share|improve this answer
    
Also note that all of this applies to 3D as well as 2D (The demo was made before Unity had 2D support, so everything is actually happening in 3D) –  Assorted Trailmix yesterday
    
Well, thanks a lot for the long and detailed explanation, but i guess it's not quite relevant about my actual problem, since what you suggest is to define every enemy to be spawned with a text file. Please correct me if I'm wrong. I'm having difficulties mainly on converting one data to another, or at least a proper way of doing something similar. –  user45629 yesterday
    
That's where the SpawnController class comes in. My example uses it to spawn specific enemies. You would be using it to define the rate at which enemies spawn. –  Assorted Trailmix yesterday

I had a very similar requirement for a game I'm working on. Here are some code snippets (although its in Swift and not C#) with some explanations:

Input state:

before filling in gaps
wave 0 = [1.0, 0.0, 0.0, 0.0]
wave 1 = 
wave 2 = 
wave 3 = 
wave 4 = 
wave 5 = [0.5, 0.5, 0.0, 0.0]
wave 6 = 
wave 7 = [0.3, 0.3, 0.3, 0.0]
wave 8 = 
wave 9 = [0.75, 0.0, 0.0, 0.25]

Output state:

after filling in gaps
wave 0 = [1.0, 0.0, 0.0, 0.0] --> [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
wave 1 = [0.9, 0.1, 0.0, 0.0] --> [1, 1, 1, 1, 1, 1, 1, 2, 1, 1]
wave 2 = [0.8, 0.2, 0.0, 0.0] --> [2, 2, 1, 1, 1, 1, 1, 1, 1, 1]
wave 3 = [0.7, 0.3, 0.0, 0.0] --> [2, 1, 1, 1, 2, 1, 1, 1, 2, 1]
wave 4 = [0.6, 0.4, 0.0, 0.0] --> [1, 1, 2, 2, 2, 1, 1, 1, 2, 1]
wave 5 = [0.5, 0.5, 0.0, 0.0] --> [2, 1, 1, 2, 2, 2, 2, 1, 1, 1]
wave 6 = [0.4, 0.4, 0.15, 0.0] --> [2, 1, 1, 3, 3, 1, 2, 1, 2, 2]
wave 7 = [0.3, 0.3, 0.3, 0.0] --> [2, 3, 2, 1, 1, 2, 3, 1, 3]
wave 8 = [0.525, 0.15, 0.15, 0.125] --> [2, 4, 1, 1, 1, 1, 3, 3, 1, 2]
wave 9 = [0.75, 0.0, 0.0, 0.25] --> [1, 4, 1, 1, 1, 4, 1, 1, 1, 4]

Some basic data structures

A basic function to interpolate two values

/// interpolates a value between start and end, for specified step
/// step varies between 0 and 1.0
/// for step = 0.5 for start 0 and end 10 should give 5
func interpolate(start:Double, end:Double, step:Double) -> Double {
    return start + (step)*(end-start)
}

Represents a wave, takes in an array of double values that represent the enemy distribution for a wave. It also has a utility function that generates a set of enemies of a specific size based on the distribution

class Wave {
    var distribution:[Double]

    /// initialize with the distribution
    init(distribution:[Double]) {
        self.distribution = distribution
    }


    /// get a set of enemies of specified size
    // based on the distribution
    func enemies(size:Int) -> [Int] {
        var e = [Int]() // list of enemies to generate
        for (index, d) in enumerate(distribution) {
            let count = Int(round((d * Double(size))))
            for i in 0..<count {
                e.append(index+1)
            }
        }

        // due to round up , we can have e.count > size
        // e.g. if distribution is 0.25, 0.75, for size 10, we end up with 11 elements
        if e.count > size {

            // remove an item from the largest distribution
            var dlargest = distribution[0]
            var largetEnemyType = 1
            for (index,d) in enumerate(distribution) {
                if dlargest < d {
                    dlargest = d
                    largetEnemyType = index+1
                }
            }

            // now remove one element from e where e[x] == largestEnemyType
            var indexToRemove = 0
            for (index, enemyType) in enumerate(e) {
                if e[index] == largetEnemyType {
                    indexToRemove = index
                    break
                }
            }

            e.removeAtIndex(indexToRemove)
        }

        return e
    }

    var description:String { return "\(distribution)" }
}

A simple struct used to hold interpolation blocks for between waves. An interpolation block is used to identify missing chunks of waves.

struct InterpolationBlock {
    var start:Int
    var end:Int
}

Algorithm

To start off, initialize our waves array to empty. This example assumes 10 waves

// initialize our waves with nil
var waves = [Wave?]()

for var i = 0; i < 10; i++ {
    waves.append(nil)
}

We define 4 enemies, so all distributions will have 4 values. Also, once the waves are initialized to nil (empty) pick and set which waves we need. For this snippet example, we need to have the first and last waves set to some values in the minimum. All other intermediate values for wave distributions will be interpolated.

let totalEnemies:Int = 4

// set up specific wave distributions
// the first and last ones must be defined
waves[0] = Wave(distribution: [1.0,0,0,0])
waves[5] = Wave(distribution: [0.5,0.5,0,0])
waves[7] = Wave(distribution: [0.3,0.3,0.3,0])
waves[9] = Wave(distribution: [0.75,0.0,0.0,0.25])

Pass 1 - identify missing wave chunks and store them in interpolation blocks

var blocks = [InterpolationBlock]()
var currentBlock = InterpolationBlock(start: 0, end: 0)
for var i = 1; i < waves.count; i++ {
    if waves[i] != nil {
        currentBlock.end = i
        blocks.append(currentBlock)
        currentBlock.start = i
    }
}

Pass 2 - fill up the waves array using interpolatation to find missing waves using the block start and end indexes

for block in blocks {
    // for current block, find out how many missing waves
    // needs to be filled up
    let missingWaves = Double(block.end - block.start)

    let startWave = waves[block.start]!
    let endWave = waves[block.end]!

    for var missingWaveIndex = block.start+1, i=1; missingWaveIndex < block.end; missingWaveIndex++, i++ {
        // interpolate values for missing
        // enemy distribution
        var missingDistribution = [Double]()
        for var ed = 0; ed < totalEnemies; ed++ {
            let start = startWave.distribution[ed]
            let end = endWave.distribution[ed]
            let step = Double(i)/missingWaves
            let gapFill = interpolate(start, end, step)
            missingDistribution.append(gapFill)
        }
        waves[missingWaveIndex] = Wave(distribution: missingDistribution)
    }
}

At this stage, all waves are filled up. To generate a set of enemies, say 10 enemies, we could call wave[2].enemies(10) which would give [1, 1, 1, 1, 1, 1, 1, 1, 2, 2] and if you want a randomized set, use a shuffle algorithm like Fischer-Yates.

share|improve this answer

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

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