javarandomlibgdxspawning

How to implement random enemy spawns libgdx


I am a beginner in Libgdx and I have a simple Galaga type game setup where the player earns points through waves with different level enemies with various stats. The player can then upgrade certain ship stats with these points. Mostly everything is done all I am focused on now is making the gameplay balanced between player ship stats and the different enemy stats as the player progresses. I want the game to be infinite as in the player can go on for as long as they can last but I can't seem to figure out how to set up the enemy spawning so that as the player progresses the enemies have different/harder stats and there are more enemies.

Here is my spawnEnemies method in my GameScreen class which adds the EnemyShip object to an array that is iterated through and then each ship is rendered in the render method.

public void spawnEnemies(float deltaTime) {
    waveTimer += deltaTime; // sets to currentTime

    if (waveTimer > timeBetweenWaves) { // if after time between waves
        enemySpawnTimer += deltaTime;
        if (enemySpawnTimer > timeBetweenEnemySpawns && enemiesSpawned < maxEnemies) { // after enemy spawn timer and only if its less than max enemies
            enemyShipList.add(enemyType()); // adds to enemy ship list which is then iterated through and rendered
            enemiesSpawned++;
            enemySpawnTimer -= timeBetweenEnemySpawns;
        }
    }

    if (enemiesDestroyed == maxEnemies) {
        nextWave();
    }
}

The ship added to the list is determined by the current wave in this enemyType method:

    private EnemyShip enemyType() { 
    if (waveCounter >= 1 && waveCounter <= 2) {
        return new EnemyShip(StarshooterGame.random.nextFloat() * (WORLD_WIDTH - 10) + 5, WORLD_HEIGHT - 5, 25, 2, 2, 0, 34,45,0.8f, Assets.instance.enemyShips.ENEMY_BLACK_01, Assets.instance.lasers.LASER_BLUE_05,6f,.4f);
    } else if (waveCounter >= 3 && waveCounter <= 6) {
        return new EnemyShip(StarshooterGame.random.nextFloat() * (WORLD_WIDTH - 10) + 5, WORLD_HEIGHT - 5, 50, 4, 4, 1, 38,45,0.8f, Assets.instance.enemyShips.ENEMY_BLUE_03, Assets.instance.lasers.LASER_RED_05, 6f, .4f);
    }else if  (waveCounter >= 7 && waveCounter <= 10) {
        return new EnemyShip(StarshooterGame.random.nextFloat() * (WORLD_WIDTH - 10) + 5, WORLD_HEIGHT - 5, 75, 4, 5, 1, 42,50,0.8f, Assets.instance.enemyShips.ENEMY_BLACK_02, Assets.instance.lasers.LASER_BLUE_04, 6f, .4f);
    }else if  (waveCounter >= 11 && waveCounter <= 14) {
        return new EnemyShip(StarshooterGame.random.nextFloat() * (WORLD_WIDTH - 10) + 5, WORLD_HEIGHT - 5, 100, 6, 6, 2, 45,54,0.7f, Assets.instance.enemyShips.ENEMY_GREEN_03, Assets.instance.lasers.LASER_BLUE_05, 6f, .4f);
    }else if  (waveCounter >= 15 && waveCounter <= 18) {
        return new EnemyShip(StarshooterGame.random.nextFloat() * (WORLD_WIDTH - 10) + 5, WORLD_HEIGHT - 5, 125, 6, 6, 2, 48,58,0.7f, Assets.instance.enemyShips.ENEMY_RED_04, Assets.instance.lasers.LASER_GREEN_03, 6f, .4f);
    }else if  (waveCounter >= 19 && waveCounter <= 24) {
        return new EnemyShip(StarshooterGame.random.nextFloat() * (WORLD_WIDTH - 10) + 5, WORLD_HEIGHT - 5, 125, 8, 7, 3, 50,60,0.7f, Assets.instance.enemyShips.ENEMY_GREEN_04, Assets.instance.lasers.LASER_RED_03, 6f, .4f);
    }

    else if  (waveCounter >= 25 && waveCounter <= 28){
        return new EnemyShip(StarshooterGame.random.nextFloat() * (WORLD_WIDTH - 10) + 5, WORLD_HEIGHT - 5, 125, 8, 8, 3, 50,60,0.7f, Assets.instance.enemyShips.ENEMY_BLACK_02, Assets.instance.lasers.LASER_RED_05,6f,.4f);
    }else if  (waveCounter >= 29 && waveCounter <= 32) {
        return new EnemyShip(StarshooterGame.random.nextFloat() * (WORLD_WIDTH - 10) + 5, WORLD_HEIGHT - 5, 150, 8, 9, 3, 50,60,0.7f, Assets.instance.enemyShips.ENEMY_BLACK_04, Assets.instance.lasers.LASER_GREEN_13, 6f, .4f);
    }else if  (waveCounter >= 33 && waveCounter <= 35) {
        return new EnemyShip(StarshooterGame.random.nextFloat() * (WORLD_WIDTH - 10) + 5, WORLD_HEIGHT - 5, 150, 9, 10, 4, 54,64,0.6f, Assets.instance.enemyShips.ENEMY_RED_02, Assets.instance.lasers.LASER_BLUE_12, 6f, .4f);
    }else if  (waveCounter >= 36 && waveCounter <= 39) {
        return new EnemyShip(StarshooterGame.random.nextFloat() * (WORLD_WIDTH - 10) + 5, WORLD_HEIGHT - 5, 175, 9, 12, 4, 58,65,0.6f, Assets.instance.enemyShips.ENEMY_BLACK_05, Assets.instance.lasers.LASER_BLUE_10, 6f, .4f);
    }else if  (waveCounter >= 40 && waveCounter <= 45) {
        return new EnemyShip(StarshooterGame.random.nextFloat() * (WORLD_WIDTH - 10) + 5, WORLD_HEIGHT - 5, 175, 10, 12, 5, 60,68,0.6f, Assets.instance.enemyShips.ENEMY_GREEN_05, Assets.instance.lasers.LASER_RED_03, 6f, .4f);
    }else if  (waveCounter >= 6 && waveCounter <= 49) {
        return new EnemyShip(StarshooterGame.random.nextFloat() * (WORLD_WIDTH - 10) + 5, WORLD_HEIGHT - 5, 200, 12, 14, 5, 64,70,0.5f, Assets.instance.enemyShips.ENEMY_RED_05, Assets.instance.lasers.LASER_GREEN_12, 6f, .4f);
    }

    return new EnemyShip(StarshooterGame.random.nextFloat() * (WORLD_WIDTH - 10) + 5, WORLD_HEIGHT - 5, 220, 14, 1, 0, 68,80,0.1f, Assets.instance.enemyShips.ENEMY_BLUE_05, Assets.instance.lasers.LASER_BLUE_05, 6f, .4f);
}

I previously had separate EnemyShip subclasses (i.e level01Enemy, level02Enemy) but then changed it to just the parent EnemyShip since I thought there was no point to having separate classes as I was only changing the stats and ship/laser texture regions. I then hardcoded the stats in each. This is a temporary solution, but I want to write clean code and not have to hardcode all the stats. If I have to change my whole approach or I have terrible code let me know because, as I said, I am a beginner.


Solution

  • Randomize enemy spawnings

    To randomize the number of enemies that spawn you could simply decrease the value of timeBetweenEnemySpawns and increase the value of maxEnemies by some function. For example you could do this in your nextWave method:

    private void nextWave() {
        //...
        timeBetweenEnemySpawns *= 0.95;//decrease the time for enemies to spawn by 5% per wave
        maxEnemeies = (int) (1.05f * maxEnmeies);//increase the max number of enemies by 5% per wave
    }
    

    In a similar way you could increase the damage, health or other stats of your enemy ships.

    Clean Code

    Writing clean code is always a good idea, because otherwhise your code will get more and more ugly with every iteration, till you can no longer handle it and have to abandon the project. That's a lesson every coder has to learn :)

    Unfortunately writing clean code is not as easy as hard coding all EnemyShip stats for every wave, so don't be disappointed if you don't understand all of the following code directly.

    To create objects, with different parameters it's a good idea to use a Factory Pattern. So you just call a factory method (using very view parameters) to create the objects.

    A realy clean solution to configure the enemy ships would be to use a data driven approach. That means you don't configure the enemy ships in the code, but use (more structured) configuration files for this. I would recoment to use JSON for this.
    Another solution (which is not that clean but easier) would be to use an enum to configure the parameters for the enemy ships (like in the EnemyShipFactory.ShipLevel enum).

    Putting it all together it could result in a solution like this:

    EnemyShipFactory

    import com.badlogic.gdx.Gdx;
    import com.badlogic.gdx.files.FileHandle;
    import com.badlogic.gdx.utils.Json;
    import com.badlogic.gdx.utils.ObjectMap;
    
    public class EnemyShipFactory {
        
        private ObjectMap<String, EnemyShipStats> enemyShipStats;
        
        public EnemyShipFactory() {
            // load the enemy ship stats from the json file
            loadStats();
        }
        
        @SuppressWarnings("unchecked")
        private void loadStats() {
            Json json = new Json();// create a json object to load the json configuration file
            FileHandle configFileHandle = Gdx.files.internal("galaga/enemy_ship_stats.json");//references the json config file in the assets folder
            enemyShipStats = json.fromJson(ObjectMap.class, EnemyShipStats.class, configFileHandle);//load the config into objects
        }
        
        public EnemyShip createEnemyShip(int waveCount) {
            //here you still need to convert the waveCount to the level of enemy ships
            //I'll use an enum here, but you could also do this by using another configuration json file
            String level = ShipLevel.of(waveCount).name();
            EnemyShipStats stats = enemyShipStats.get(level);
            
            return new EnemyShip(stats);
        }
        
        private enum ShipLevel {
            
            LEVEL_1(1, 2),//
            LEVEL_2(3, 6),//
            LEVEL_3(7, 10);//
            //more levels...
            
            public final int minWaveCount;
            public final int maxWaveCount;
            
            private ShipLevel(int minWaveCount, int maxWaveCount) {
                this.minWaveCount = minWaveCount;
                this.maxWaveCount = maxWaveCount;
            }
            
            public static ShipLevel of(int waveCount) {
                for (ShipLevel level : values()) {
                    if (level.minWaveCount >= waveCount && level.maxWaveCount <= waveCount) {
                        return level;
                    }
                }
                return LEVEL_3;//return max level by default
            }
        }
    }
    

    EnemyShipStats

    public class EnemyShipStats {
        
        //replace this with the names and types of the stats that your need
        public float width;
        public float height;
        public float damage;
        public String texture;
        //...
    }
    

    EnemyShip

    public class EnemyShip {
        
        public EnemyShip(EnemyShipStats stats) {
            //create the enemy ship based on the stats
            //probably just call the constructor you currently use like this:
            this(stats.width, stats.height, stats.damage, stats.texture);
        }
        
        public EnemyShip(float width, float height, float damage, String texture) {
            //...
        }
        
        //...
    }
    

    enemy_ship_stats.json

    //put this file in the assets folder, inside a directory 'galaga'
    {
        // the keys are the names of the enum in EnemyShipFactory.ShipLevel
        LEVEL_1: {
            //the values are the EnemyShipStats objects
            width: 42,
            height: 42,
            damage: 42,
            texture: some_texture_name
        },
        LEVEL_2: {
            //the values are the EnemyShipStats objects
            width: 42,
            height: 42,
            damage: 42,
            texture: some_texture_name
        },
        LEVEL_3: {
            //the values are the EnemyShipStats objects
            width: 42,
            height: 42,
            damage: 42,
            texture: some_texture_name
        }
    }
    

    Now you can change your enemyType method like this:

    //declare this as a global field
    private EnemyShipFactory factory = new EnmeyShipFactory();
    
    private EnemyShip enemyType() { 
        return factory.createEnemyShip(waveCount);
    }