Advanced Damage Application

by Marty Wallace

Damage is a component of almost any game. Your ship receives it in Space Invaders, your towers receive it in Clash Royale, your party receives it in Final Fantasy and your character receives it in any typical role-playing game. In most cases damage will directly affect your survival in the game and enough of it without restoration will end the game.

When developing a game of your own, the implementation seems easy enough at a glance: creatures have health and "damage" is simply reducing this value but the amount of damage you want to do:

attack() {
  // Deal 5 "damage" to the player.
  player.health -= 5;
}

Easy right? If we want to introduce the concept of death (e.g. when the health reaches zero) then that seems like a pretty simple addition too:

attack() {
  // Deal 5 "damage" to the player.
  player.health -= 5;
  
  if (player.health <= 0) {
    // The player has died.
    endGame();
  }
}

Nice one. Now when we want to implement this on enemies, buildings that can be destroyed or any other type of object that could be affected by our concept of damage, we simply copy and paste this code everywhere that will need it. For simple games it's likely that all of the code that would cause the players to take damage will be at worst in a base enemy class, and all of the code that deals with enemies taking damage will be in the player class. Not an awful solution for something small, but what about when we start wanting things like:

  • Enemies and players able to both deal damage to buildings or other obstacles.
  • Enemies able to deal damage to each other.
  • Abilities or spells that can deal damage to the user (e.g. sacrificial abilities).
  • Some state that causes damage over time (e.g. poison, bleeding).

These examples force you to copy and paste this code now in many locations e.g.

  • The code for how a building reacts when it should be destroyed has to live both in the player and enemy objects.
  • The code for how enemies die now has to live in the player and enemy objects.
  • The code for how anything can die has to be declared by itself to handle self-damaging.
  • The code for damage over time has to belong in a completely new type of object.

What a mess, obviously this is not going to be maintainable at all. Every time you want to change how something dies or is destroyed, that change is going to have to be made everywhere that could possibly be a source of applying damage.

This doesn't seem like a tough problem to solve, right? We can simply create a die() function on everything that can receive damage and use that whenever we run out of health!

class Player {
  public die(): void {
    // End the game when the player dies.
    endGame();
  }
}

class Enemy {
  public die(): void {
    // Drop loot and increase the game score when
    // enemies die.
    dropLoot();
    increaseGameScore();
  }
}

Now our code everywhere just has to be changed to:

attack() {
  target.health -= 5;
  
  if (target.health <= 0) {
    target.die();
  }
}

Great! Now although we still have to copy and paste this small block of code everywhere as we did before, at least the implementation of dying or being destroyed lives in one logical place. This is much easier to maintain! Problem solved right? Well, sure. If this is as far as you're going to take your damage logic then it is an OK approach. If you want to do things like:

  • Implement cooldowns on how often damage can be received.
  • Scale the damage based on some criteria e.g. the "type" of damage relative to the receivers resistance to that damage type.
  • Add some effect e.g. blood splatter when damage is received.
  • Easily identify the dealer of the damage both when applied and when the target dies or is destroyed.
  • Implement special circumstances e.g. protection, invincibility, etc.

This is only the tip of the iceberg of what side affects receiving damage could have and we obviously don't want to copy and paste the potential book of code everywhere noted above.

Let's look at how I would approach this problem.

All code samples will be in TypeScript for a nice balance of simplicity and clarity thanks to the type annotations.

Damage is a type

The first major step is to think of damage as an entire structure of data rather than just a number we subtract from another. This will allow us to properly flesh out potential things like damage type, side effects, element and so on. In our example, damage will have a type and an element associated with it which we can declare as enums:

enum DamageType {
  Normal,
  Piercing,
  Healing
};

enum Element {
  Physical,
  Fire
}

Which inside of our proposed damage structure looks like:

interface Damage {
  readonly type: DamageType;
  readonly element: Element;
  readonly value: number;
}

Damageables

The next step is to create an abstraction for an object that is able to receive damage.

interface Damageable {
  receiveDamage(sender: any, damage: Damage): void;
}

The receiveDamage function expects to be provided with the sender i.e. the object who is causing the damage, as well as an instance of our new Damage interface. We can implement this on our objects who are able to be damaged and provide the actual implementation depending on the context. For example our Player can become this in its simplest form:

class Player implements Damageable {
  public health: number;

  public receiveDamage(sender: any, damage: Damage): void {
    this.health -= damage.value;

    if (this.health <= 0) {
      this.die();
    }
  }

  public die(): void {
    endGame();
  }
}

Dealing damage

Actually dealing damage now changes from directly altering the targets health to calling receiveDamage:

player.receiveDamage(enemy, {
  type: DamageType.Normal,
  element: Element.Physical,
  value: 5
});

Which has the advantages of:

  • All code dealing with damage application is self-contained within the object it relates to which makes maintenance extremely easy.
  • We can make the player's health read-only as we no longer have to (and never really should have been in the first place) directly changing this.

Complete example

Here's a complete example using the piercing and healing damage types we defined, health clamping within 0 and max health and damage application cooldown:

abstract class Creature implements Damageable {
  private _health: number;
  private _maxHealth: number;
  private _defense: number;
  private _damageCooldown: number;
  private _bloodSplatterParticles: ParticleSystem;

  public receiveDamage(sender: Creature, damage: Damage): void {
    if (damage.type === DamageType.Healing) {
      // Healing damage type indicates health addition vs subtraction.
      this._health += damage.value;
    } else {
      if (this._damageCooldown <= 0) {
        // Prevent rapid consecutive damage application.
        this._damageCooldown = 10;

        if (damage.type === DamageType.Normal) {
          // Normal damage takes defense into account.
          this._health -= Math.max(damage.value - this._defense);
        } else {
          // Peircing damage doesn't consider defense.
          this._health -= damage.value;
        }

        // Create blood splatter.
        this._bloodSplatterParticles.play();
      }
    }

    // Clamp health between 0 and the max.
    this._health = Math.min(Math.max(this._health, 0), this._maxHealth);

    if (this._health === 0) {
      // The creature has died.
      this.die();
    }
  }

  protected abstract die(): void;
  
  public get health(): number {
    return this._health;
  }
}

Much better. Now we only need to maintain the receiveDamage method in each Damageable object.

Of course, this can be further abstracted depending on the complexity of the damage system in your game, all the way up to making Damage an actual class containing modifiers which are traversed for the final resulting damage, e.g.

class Damage {
  private _value: number;
  private _modifiers: ReadonlyArray<DamageModifier>;

  constructor(value: number, modifiers: ReadonlyArray<DamageModifier>) {
    this._value = value;
    this._modifiers = modifiers;
  }
  
  public calculateDamageValue(damageable: Damageable): void {
    let value: number = this._value;
    
    for (let modifier of modifiers) {
      value = modifier.apply(value, damageable);
    }
    
    return value;
  }
}

interface DamageModifier {
  calculate(value: number, damageable: Damageable): number;
}

class HealingModifier implements DamageModifier {
  public apply(value: number, damageable: Damageable): number {
    if (damageable.classification !== 'undead') {
      // Flip value for healing, except for undead enemies.
      return -value;
    }
    
    return value;
  }
}

class PiercingModifier implements DamageModifier {
  public apply(value: number, damageable: Damageable): number {
    // Increase the damage by the target's defense to "pierce" it.
    return value + damageable.defense;
  }
}

// Piercing damage.
const piercingDamage: Damage = new Damage(10, [new PiercingModifier()]);

// Healing.
const heal: Damage = new Damage(15, [new HealingModifier()]);

Conclusion

As you can see, there are a lot of levels this can be brought to with some thought applied to it. Simply reducing the targets health directly should never be a solution even for the most trivial implementation possible, as that will just cause you pain down the line.

I hope this helps some beginner game developers out there.

  • systems