如何实现随机敌人生成 libgdx

How to implement random enemy spawns libgdx

我是 Libgdx 的初学者,我有一个简单的 Galaga 类型的游戏设置,玩家可以通过波浪与具有不同统计数据的不同级别的敌人获得积分。然后玩家可以用这些点数升级某些船舶统计数据。基本上所有的事情都完成了,我现在关注的是随着玩家的进步让玩家飞船统计数据和不同敌人统计数据之间的游戏平衡。我希望游戏是无限的,因为玩家可以继续下去,只要他们能持续下去,但我似乎无法弄清楚如何设置敌人的产卵,以便随着玩家的进步,敌人有 different/harder 统计数据和更多的敌人。

这是我的 GameScreen class 中的 spawnEnemies 方法,它将 EnemyShip 对象添加到迭代的数组中,然后每艘船都在渲染方法。

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();
    }
}

添加到列表中的船由这个 enemyType 方法中的当前 wave 决定:

    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);
}

我之前有单独的 EnemyShip subclasses(即 level01Enemy、level02Enemy),但后来将其更改为仅父 EnemyShip,因为我认为没有必要单独classes 因为我只是改变了统计数据和 ship/laser 纹理区域。然后我硬编码了每个统计数据。这是一个临时解决方案,但我想编写干净的代码,而不必对所有统计信息进行硬编码。如果我必须改变我的整个方法或者我的代码很糟糕,请告诉我,因为正如我所说,我是初学者。

随机生成敌人

要随机生成敌人的数量,您可以简单地通过某种函数降低timeBetweenEnemySpawns的值并增加maxEnemies的值。例如,您可以在 nextWave 方法中执行此操作:

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
}

以类似的方式,您可以增加敌舰的伤害、生命值或其他数据。

清洁代码

编写干净的代码总是一个好主意,因为否则你的代码会随着每次迭代变得越来越丑陋,直到你无法再处理它而不得不放弃项目。这是每个程序员都必须学习的一课:)

不幸的是,编写干净的代码并不像对每一波的所有 EnemyShip 统计数据进行硬编码那么容易,所以如果您不理解以下所有代码,请不要失望直接。

要创建具有不同参数的对象,最好使用 Factory Pattern。所以你只需调用一个工厂方法(使用非常视图参数)来创建对象。

配置敌舰的一个真正干净的解决方案是为此使用 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
另一种解决方案(不是 clean 但更简单)是使用枚举来配置敌舰的参数(如 EnemyShipFactory.ShipLevel 枚举)。

将它们放在一起可能会产生如下解决方案:

敌舰工厂

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;
    //...
}

敌舰

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
    }
}

现在您可以像这样更改 enemyType 方法:

//declare this as a global field
private EnemyShipFactory factory = new EnmeyShipFactory();

private EnemyShip enemyType() { 
    return factory.createEnemyShip(waveCount);
}

从某种意义上说,这是一个“好问题”,但对于 Stack Overflow 来说却是一个糟糕的问题——它实际上是 100% 基于意见的(我投票决定关闭它)。

游戏数据结构建模是一门庞大的学科 - 与其说是一门科学,不如说是一门艺术 - 你的决定将强烈 影响你游戏未来发展的可能性(你是否想要生成关卡?关卡编辑器?live-debug?为特定插曲添加 in-game 脚本的可能性?主题?更复杂的 gfx?更复杂的规则?- 其中一些目标是独家的!)。

您可以尝试并在我看来对您有帮助的一件事是阅读 Ashley 教程并重构您的代码以使用它。 Ashley 是一个 Entity/Component/System 库,它与 LibGDX 很好地集成(甚至由设置 gui 提供)并且在生态系统中常用。它一定会激发您以较少分离的方式设计您的代码,并且可以在框架内巧妙地完成建模“波浪”、不同类型的船只、随机生成点等事情。