Developing Multiplatform Game with LibGDX, part 13: blinking on damage

Adding Damage Effects

Right now, the only way to tell that the player or enemy has been damaged is by checking their hp. This is not very observable. Let’s try to implement basic blinking on player / enemy damage.

We are starting to notice more and more similarities between our enemy and a player. Let’s refactor this a bit and move common functions into an abstract parent class. In our logic/objects package, create a new abstract class, Character. It should inherit from Sprite. Let’s move the similar functions from both player and enemy into the character class.

We need to move following stuff from Player/Enemy to our Character Class:

Variable lives. Also, make this variable protected. Other stuff:

Create a Character constructor, that takes “lives” as a parameter; since both enemy and player have lives – it’s a good idea to move this variable into parent class.

TakeDamage function works the same way for player and enemy. getLives apply to both. That’s it. For now. Here’s the final look of all 3 classes.

Character.java:

public class Character extends Sprite {

    protected int lives;

    public Character(int _lives)
    {
        lives = _lives;
    }


    public int getLives()
    {
        return lives;
    }


    public void takeDamage(int amount) {
        lives -= amount;
        if (lives < 0)
        {
            lives = 0;
        }
    }
}

Player.java:

public class Player extends Character {

    private final int max_lives;
    protected int fieldX;
    protected int fieldY;

    public Player(int fx, int fy, Resources res, int _lives)
    {
        super(_lives);
        fieldX = fx;
        fieldY = fy;
        max_lives = _lives;
        set(res.player);
    }

    public int getFieldX() {
        return fieldX;
    }

    public void setFieldX(int fx) {
        fieldX = fx;
    }

    public int getFieldY() {
        return fieldY;
    }

    public void draw(SpriteBatch batch, SizeEvaluator sizeEval)
    {
        setPosition(sizeEval.getBaseScreenX(fieldX), sizeEval.getBaseScreenY(fieldY));
        super.draw(batch);
    }

    public void setFieldY(int fy) {
        fieldY = fy;
    }

    public void addLives(int amount) {
        lives += amount;
        if (lives > max_lives)
        {
            lives = max_lives;
        }
    }
}

Enemy.java:

public class Enemy extends Character {

    private static final float BASE_ATTACK_TIME = 3.0f;
    private static final int DEFAULT_ENEMY_LIVES = 10;
    private float timeSinceAttack;
    private float nextAttackTime;
    private EnemyAttackListener attackListener;

    private boolean targetTiles[][];

    public interface EnemyAttackListener
    {
        void OnAttack(boolean[][] tiles);
    }


    public Enemy(Resources res, EnemyAttackListener listener)
    {
        super(DEFAULT_ENEMY_LIVES);
        attackListener = listener;
        set(res.enemy);
        resetAttackTimer();
        targetTiles = new boolean[GameLogic.MAX_BASE_X + 1][];
        for (int i = 0; i <= GameLogic.MAX_BASE_X; i++) { targetTiles[i] = new boolean[GameLogic.MAX_BASE_Y + 1]; } } public void update(float delta) { timeSinceAttack += delta; if (timeSinceAttack > nextAttackTime)
        {
            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);
                }
            }

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

    private void resetAttackTimer()
    {
        timeSinceAttack = 0;
        nextAttackTime = BASE_ATTACK_TIME + MathUtils.random(2.0f);
    }


    public void draw(SpriteBatch batch, SizeEvaluator sizeEval)
    {
        setPosition(sizeEval.getEnemyX(this), sizeEval.getEnemyY(this));
        super.draw(batch);
    }
}

Cool. Now let’s start modifying stuff! The task is to make enemy and player blink whenever they are damaged. We’ll do it the following way: when character gets damaged, we record the current time of damage. For the next 0.5 seconds, it will be blinking. Sounds easy enough?

In our Character class, add two new variables:

protected float timeAlive;
private float timeOfDmgTaken;

The reasoning behind making timeAlive protected is that we may use it later somewhere. Outside of the character class. In our Character class, init those two variables:

timeAlive = 0;
timeOfDmgTaken = -1;

We want timeOfDmgTaken to represent a negative number for now, so that the game would not think that we took damage on second 0.

In our Character class, add an update function:

public void update(float delta)
{
    timeAlive += delta;
}

It simply adds to the total time alive. Now add a public constant, which would show how much time (total) we want our character to blink.

public static final float BLINK_TIME_AFTER_DMG = 0.25f;

Great, the character should be blinking for a quarter of a second. Now, for the blinking part. In our takeDamage function, set the valuje of timeOfDmgTaken to our current time.

timeOfDmgTaken = timeAlive;

And now we’ll have to check the player out. Now, right after the same place in GameLogic where we call enemy.update (GameLogic.update method), add player.update(delta) call.

public void update(float delta)
{
    gameTime += delta;
    effectEngine.update(delta);
    enemy.update(delta);
    player.update(delta);
…

Great. Speaking about enemy update: in our enemy’s update function, right at the start, add a call to our Character’s update function (super.update(delta)). Also, add an @Override keyword before update to indicate that we’re overriding the parent function.

Enemy.java:

@Override

@Override
public void update(float delta)
{
    super.update(delta);

Great. So how do we do the blinking? Since both enemy and player will blink the same way, we want such functionality function in Character. The issue is: player’s and enemy draw functions are different. We’ll need two functions: a function that we call before drawing and one that we call after.

Let’s call them preDraw and postDraw. Both should be inside the Character class.

public void preDraw()
{
    if (timeAlive < timeOfDmgTaken + BLINK_TIME_AFTER_DMG)
    {
        float t = (timeAlive - timeOfDmgTaken) / BLINK_TIME_AFTER_DMG;
        t = t * t;
        setColor(1, 1, 1, t);
    }
}

So what does it do? The idea is simple: as soon as character is hit, he disappears (his alpha channel iz zero). Then, he gradually reappears on the screen. The thing is, we don’t want him to reappear in linear timing. We want him to be transparent for a bit more time, and then to reappear rapidly. To do this, we apply something that is called an easing function. It makes animation/transaction look smoother. In our case, it’s Quadratic Easing (take the current time, remove the time of damage taken and divide it by the time when our sprite should be blinking. Then, we multiply the value with itself. In case of lower values (not much time passed) – we get even smaller transparency value: if our time has passed (timeAlive – timeOfDmgtaken is 0.05 seconds, then we have 0.05 / 0.25 = 0.2. After multiplying it with itself, we get 0.04 – much smaller transparency value). There are various formulas and various approaches to this: you can check out http://gizma.com/easing/ on how animation changes depending on an easing equation.

postDraw is much easier:

public void postDraw()
{
    setColor(1, 1, 1, 1);
}

We reset the transparency of the sprite. Sounds good! Now add it to both Player’s and Enemy draw functions.

Enemy:

public void draw(SpriteBatch batch, SizeEvaluator sizeEval)
{
    preDraw();
    setPosition(sizeEval.getEnemyX(this), sizeEval.getEnemyY(this));
    super.draw(batch);
    postDraw();
}

Player:

public void draw(SpriteBatch batch, SizeEvaluator sizeEval)
{
    preDraw();

    setPosition(sizeEval.getBaseScreenX(fieldX), sizeEval.getBaseScreenY(fieldY));
    super.draw(batch);

    postDraw();
}

Run the game! Even though we have not changed anything gameplay-wise, you can see that it feels a bit better now, because player is getting more feedback when something happens.

Blinking on damage!

Blinking on damage!

Relevant commit: https://github.com/vladimirslav/dodginghero/commit/04b22909fe89ef16ceff444db690d60fe14884da

Leave a Reply

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