Developing Multiplatform Game with LibGDX, part 18: adding enemy types

Our game starts to take shape – let’s adjust the gameplay by adding multiple enemies with different attacks!

Loading the sprites

After developing the persistence and basic progression, it’s time to diversify the gameplay: our enemy quickly becomes boring. I think it makes sense to adjust the attack patterns.

Let’s cut out 4 new characters from our character spritesheet. (We’ve used it before: http://opengameart.org/content/tiny-16-basic )

Here’s what I have:

bat ghost skeleton slime

My idea is to make different attack patterns for different enemies. We have vertical lines for spider. Let’s make horizontal lines for ghost, diagonal lines for bat, random attack pattern for slime and all four possibilities for skeleton.

Before we load the resources in game, let’s adjust the Resources and Enemy class constructor: it is going to accept the type of enemy now. In our Resources class, make a public static final int constants, that would enumerate the enemy types.

public static final int ENEMY_VERTICAL = 0;
public static final int ENEMY_HORIZONTAL = 1;
public static final int ENEMY_DIAGONAL = 2;
public static final int ENEMY_RANDOM = 3;
public static final int ENEMY_UNIVERSAL = 4;

We are using the int’s instead of Enum because there’s no easy way in java to convert int to enum (and we are going to generate a random int to determine enemy type). Now, add the type variable to our Enemy class and adjust the Enemy constructor.

public final int type;

public Enemy(Resources res, EnemyAttackListener listener, int _type)
{
    super(GameProgress.getEnemyLives());
    type = _type;

Depending on type, we’re going to load the appropriate enemy sprite. Now, to load them, we could declare 5 different sprites, but let’s think of a more optimal approach (so it would be easy to lookup later). In our Resources class, Let’s remove our enemy sprite declaration. Instead, let’s make a HashMap of <Integer, Sprite> pair, which would link our enemy type integers to actual enemy sprite.

public HashMap<Integer, Sprite> enemySprites;

Remove our enemy sprite loading line of code in Resources constructor. Instead of this, let’s initialize the enemySprites HashMap and fill it with values:

enemySprites = new HashMap<Integer, Sprite>();
enemySprites.put(ENEMY_VERTICAL, gameSprites.createSprite("spider"));
enemySprites.put(ENEMY_HORIZONTAL, gameSprites.createSprite("ghost"));
enemySprites.put(ENEMY_DIAGONAL, gameSprites.createSprite("bat"));
enemySprites.put(ENEMY_RANDOM, gameSprites.createSprite("slime"));
enemySprites.put(ENEMY_UNIVERSAL, gameSprites.createSprite("skeleton"));

A bit repetitive, but I’m sure it will pay off 🙂 Now, let’s actually go to Enemy class and replace the

set(res.enemy);

command with something more advanced:

set(res.enemySprites.get(type));

We grab the sprite according to the type. Careful! In bigger projects, it’s better to create a getter function, which would check if enemySprites has the value of (type) first, in order to avoid nasty errors of all sorts when you add a new enemy.

Let’s make an in-between test run. However, the game won’t compile now. That’s because we’ve changed the Enemy constructor. In our GameLogic, when we create a new Enemy object, let’s add an extra parameter:

enemy = new Enemy(game.res, this, MathUtils.random(Resources.ENEMY_UNIVERSAL));

The enemy is going to be chosen randomly. Run the game. Repeat it a few times. The enemy sprite should be randomly chosen on launch now. Also, if you defeat an enemy (and progress to the next round), the enemy is chosen randomly too. Pretty cool, huh?

Adjusting the attack time

Meanwhile, let’s reduce the time between enemy attacks to make the game more dynamic. First, let’s save the player from being attack as the round starts. Add the constant:

private static final float WARM_UP_TIME = 2.0f;

Adjust the enemy update function a bit, the moment when we determine to attack:

if (timeAlive > WARM_UP_TIME && timeSinceAttack > nextAttackTime)
{

You can see that I’ve added the first condition: we don’t want an enemy to attack for the first two seconds, let’s call it a warm-up time for player to understand that the game started / level changed.

Then, change

private static final float BASE_ATTACK_TIME = 3.0f;

To

private static final float BASE_ATTACK_TIME = 1.0f;

 

It’s going to be OK, because we add 0..2 seconds between attacks in resetAttackTimer function. This should make the game much more dynamic.

Adjusting the attack patterns

If you check our Enemy class, update function, you’ll see where we determine the attack coordinates. You can imagine, if we add 4 different attack types, the function will get quite bloated. Let’s delegate it to a separate functions.

Move the code inside the

if (timeAlive > WARM_UP_TIME && timeSinceAttack > nextAttackTime)
{
    // FROM HERE
    int col1 = MathUtils.random(GameLogic.MAX_BASE_X);
    int col2 = 0;
    do {
        col2 = MathUtils.random(GameLogic.MAX_BASE_X);
    } while (col2 == col1);
    // not very effective, but guaranteed to get different results

    for (int x = 0; x <= GameLogic.MAX_BASE_X; x++)
    {
        for (int y = 0; y <= GameLogic.MAX_BASE_Y; y++)
        {
            targetTiles[x][y] = (col1 == x || col2 == x);
        }
    }
    // UP UNTIL HERE

    attackListener.OnAttack(targetTiles);
    resetAttackTimer();
}

Except for the two last lines! Into separate function, let’s name it performVerticalLineAttack();

Now, let’s make a similar function, which we’ll call performHorizontalLineAttack (it’s going to be pretty similar, so let’s copy performVerticalLineAttack and adjust it accordingly). In similar way, horizontal attack will choose 2 horizontal lines and mark them. Here’s how it looks:

private void performHorizontalLineAttack()
{
    int row1 = MathUtils.random(GameLogic.MAX_BASE_Y);
    int row2 = 0;
    do {
        row1 = MathUtils.random(GameLogic.MAX_BASE_Y);
    } while (row2 == row1);

    for (int x = 0; x <= GameLogic.MAX_BASE_X; x++)
    {
        for (int y = 0; y <= GameLogic.MAX_BASE_Y; y++)
        {
            targetTiles[x][y] = (row1 == y || row2 == y);
        }
    }
}

Now, let’s get to diagonal function. Let’s make a separate function which would fill a row (based on selected direction).

private void fillDiagonal(int xstart, int dx)
{
    for (int i = 0; i <= GameLogic.MAX_BASE_Y; i++) { int nx = xstart + dx * i; if (nx > GameLogic.MAX_BASE_X)
        {
            nx = nx - GameLogic.MAX_BASE_X - 1;
        }

        if (nx < 0)
        {
            nx = nx + GameLogic.MAX_BASE_X + 1;
        }

        targetTiles[nx][i] = true;
    }
}

We pass the initial x and the direction (dx). Then we go through full height of the field and pick one tile that is positioned diagonally to the previous one. All that is left is to make the function that actually picks two x coordinates and directions.

private void performDiagonalAttack()
{
    int dx1 = -1 + MathUtils.random(1) * 2; // either -1 or 1
    int dx2 = -1 + MathUtils.random(1) * 2; // either -1 or 1

    int col1 = 0;
    int col2 = 0;
    do {
        col1 = MathUtils.random(GameLogic.MAX_BASE_Y);
    } while (col2 == col1);

    // reset all the tiles to false
    for (int x = 0; x <= GameLogic.MAX_BASE_X; x++)
    {
        for (int y = 0; y <= GameLogic.MAX_BASE_Y; y++)
        {
            targetTiles[x][y] = false;
        }
    }

    // mark the necessary ones
    fillDiagonal(col1, dx1);
    fillDiagonal(col2, dx2);
}

3 done, 2 more to go! The random one is quite simple: let’s not care about filling the repeated tiles. It’s part of life. Instead, just pick 10 random tiles on the field and mark them as used for attack. Here’s the final function:

private void performRandomAttack()
{
    for (int x = 0; x <= GameLogic.MAX_BASE_X; x++)
    {
        for (int y = 0; y <= GameLogic.MAX_BASE_Y; y++)
        {
            targetTiles[x][y] = false;
        }
    }

    for (int i = 0; i < 10; i++)
    {
        int nx = MathUtils.random(GameLogic.MAX_BASE_X);
        int ny = MathUtils.random(GameLogic.MAX_BASE_Y);
        targetTiles[nx][ny] = true;
    }
}

The easiest one so far: reset the tiles, then generate random coordinates for 10 times and mark the tiles as “under attack” accordingly.

The final one will be a mix of all 4:

private void performUltimateAttack()
{
    int rnd = MathUtils.random(4);
    switch (rnd)
    {
        case 0:
            performVerticalLineAttack();
            break;
        case 1:
            performHorizontalLineAttack();
            break;
        case 2:
            performDiagonalAttack();
            break;
        default:
            performRandomAttack();
    };
}

The only thing left is to add a check on enemy type (and performing an attack based on that type). Do it in Enemy update function, after our attack timing check:

if (timeAlive > WARM_UP_TIME && timeSinceAttack > nextAttackTime)
{
    switch (type)
    {
        case Resources.ENEMY_VERTICAL:
            performVerticalLineAttack();
            break;
        case Resources.ENEMY_HORIZONTAL:
            performHorizontalLineAttack();
            break;
        case Resources.ENEMY_DIAGONAL:
            performDiagonalAttack();
            break;
        case Resources.ENEMY_RANDOM:
            performRandomAttack();
            break;
        default:
            performUltimateAttack();
            break;
    }

We’re done! Now, I’ve noticed that we’re drawing effects below the player. In this case, it won’t be a good idea because we need to explicitly show the red warning signs (and they are poorly visible otherwise). Move the

gameStage.getCamera().position.set(gameStage.getWidth() / 2, gameStage.getHeight() / 2, 0);

Line below

player.draw(batch, sizeEvaluator);
enemy.draw(batch, sizeEvaluator);
batch.end();
// place it here!

Great. Now the warning signs are going to be shown above player (timely telling him to get out of the blast zone!). The other thing: the delay of 0.5 seconds is not enough to react appropriately. Go to WarningEffect.java and change WARNING_TIME constant to be equal to 0.75f (up from 0.5f).

Relevant github commit: https://github.com/vladimirslav/dodginghero/commit/012b57d39ed7c12e902acc83a5e7f351069753db

Leave a Reply

Your email address will not be published. Required fields are marked *