Skip to content

Buffs & Debuffs

raneechu edited this page Oct 5, 2021 · 16 revisions

Purpose

Buffs & Debuffs allow for the player to gain temporary enhancements and ailments which provide a platform for more interesting gameplay. Buffs will allow the player to counteract the increasing rate and frequency of obstacles and enemies as they progress through the game. Debuffs will provide the player with ailments which makes gameplay harder for a period of time.

To see the list of buffs and debuffs which are currently available, see the Buff Catalogue and Debuff Catalogue

Implementation

A BuffManager handles the tracking, removal, spawning and despawning of buffs and debuffs on the map. Buffs and debuffs are all Entity objects with particular Components applied. A BuffFactory handles the instantiation of the Entity, and registers the buff or debuff with the BuffManager. All buffs / debuffs have a BuffInformation object which keeps track of the buff and it's relevant attributes, such as it's Type, effectTime and Entity ID. Buffs and debuffs are retrieved by the player colliding with them.

A single buff / debuff spawns on the map every 3 seconds, and will time-out and disappear every 10 seconds. Some buffs are 'instant' and some are 'timed', ie, they impact the player exactly once immediately, or the effects of the buff remain on the player for a set period, respectively. The selection of the buff is entirely random. In future, the spawn and despawn rates may differ, and weights may be added to the selection process for more impactful buffs.

A PlayerBuffs class centrally holds the functionality of all buffs & debuffs which spawn on the map. This class also handles removing the lasting effects of timed buffs.

Currently effective timed buffs are displayed in the top-left player UI.

Instant & Timed Buffs

Instant buffs are those which are applied to the player once, at the time of impact. Some examples are:

  • Set the players health to full
  • Increase the players health
  • Decrease the players health

Timed buffs are those whose effects remain with the player for a set period. Currently, the standard time-out period of these effects is 5 seconds. Some examples are:

  • Make the player invincible for 5 seconds
  • Make all damage taken by the player doubled for 5 seconds
  • Make the player inflict double damage to enemies for 5 seconds.

For readability, buffs & debuffs are just referred to as 'buffs' here.

Spawning buffs

In order for the buffs to spawn on uneven terrain, a function within TerrainFactory was written to retrieve the y-coordinate of the surface tile with the given x-coordinate. The function reads the same file that was used when filling the surface tiles on the terrain, which gives information on the y-coordinate depending on the x-coordinate.

  public int getYOfSurface(int x, GdxGame.ScreenType screenType) {
    int y = 0;
    String filename = null;
    if (screenType == GdxGame.ScreenType.MAIN_GAME) {
      filename = "level-floors/levelOne.txt";
    } else if (screenType == GdxGame.ScreenType.LEVEL_TWO_GAME) {
      filename = "level-floors/levelTwo.txt";
    }
    try(BufferedReader br = new BufferedReader(new FileReader(filename))) {
      String line = br.readLine();
      int referenceX = 0, referenceY = 0, width = 0, distance = 0;
    try(BufferedReader br = new BufferedReader(new FileReader(filename))) {
      String line = br.readLine();
      int referenceX = 0, referenceY = 0, width = 0, distance = 0;

      while (line != null) {
        String[] values = line.split(" ");
        width = Integer.parseInt(values[0]);
        referenceX = Integer.parseInt(values[1]);
        referenceY = Integer.parseInt(values[2]);
        distance = (width * 2) + referenceX;
    
        // Checks if the given x-coordinate is between the x range given on this line of the file.
        if (x >= referenceX && x <= distance) {
          y = referenceY;
          break;
        } else {
          line = br.readLine();
        }
      } ...
    return y;
   }

When spawning the buffs in ForestGameArea or any other game a random x-position within the bounds of the map is chosen and it's corresponding y-coordinate is retrieved using the function above and the buff is spawned at that position.

Usage

If you would like to create a new buff, go to the BuffManager class and add your buff name to the BuffTypes enum. For consistency, the naming convention of buffs & debuffs is:

  • Buffs are preceded by a 'B' if they are not timed
  • Buffs which are timed are preceded by a 'BT'
  • Debuffs are preceded by a 'D' if they are not timed
  • Debuffs which are timed are preceded by a 'DT'

For example, the BuffTypes enum may look like:

 public enum BuffTypes {
    B_HP_UP,        // Increase the players HP on touch
    D_HP_DOWN,      // Decrease the players HP on touch
    BT_INVINCIBLE,  // Make the player invincible for 5 seconds
    DT_DOUBLE_HURT  // Make the player take double damage for 5 seconds
}

After the name is created, go to the BuffManager getTexture function. This function handles returning a filepath to the image for the buff type. Add your new BuffType to the switch statement with your filepath as the return. This file must have been loaded by the MainGameScreen for this to work. If a new file is not specified for a new buff, it will be given a default image.

For example, the switch statement inside the getTexture method may look like:

switch (type) {
    case BT_INVINCIBLE:
        return "images/invincibleBuff.png";
    case B_HP_UP:
        return "images/increaseHPBuff.png";
}

To specify the behaviour of the buff, a function must be created in PlayerBuffs. PlayerBuffs contains static functions which control the behaviour of buffs, including removing their effects (for timed buffs). Find the correct section of PlayerBuffs (either buffs or debuffs) and add in your new function.

For example, PlayerBuffs may look like:

/** --- Buffs --- ** /

public static void increasePlayerHP(Entity player) {
    // Actions to take to increase the players HP once
}

...

/** --- Debuffs --- ** /

public static void setDoubleHurt(Entity player) {
    // Actions to take to make the player take double damage
}

Once a PlayerBuffs function is defined, add this function call to the appropriate switch statement in the BuffManagers selectBuffFunctionality method. For example, selectBuffFunctionality may look like:

// Method calls for instant buffs
switch (type) {
    case B_HP_UP:
        PlayerBuffs.increasePlayerHP(this.player);
        return;
    case D_HP_DOWN:
        PlayerBuffs.reducePlayerHP(this.player);
        return;  
}

...

// Method calls for timed buffs
switch (type) {
    case BT_INVINCIBLE:
        PlayerBuffs.applyInvincibility(this.player);
        break;
    case DT_NO_JUMP:
        PlayerBuffs.noJumping(this.player);
        break;
}

Note that the PlayerBuffs function should only take the player as an argument.

And that's it! You've just created a new buff! Your buff is automatically added to the pool of buffs that will spawn. However..

If your buff was a timed buff

Timed buffs require some extra additions before they will work correctly. First, when adding new buff functionality to the PlayerBuffs class, you may have noticed the removeTimedBuff method. Add a new method below this which controls the behaviour of removing the effects of your buff. Note that this method should only take the player as an argument. For example;

/** --- Removing timed effects --- ** /

public static void removeInvincibility(Entity player) {
    // Actions to take to remove invincibility
}

Note: the PlayerBuffs class does not handle checking for buff time-outs. If a function which is removing the effects is called, then the timer is already up.

Add your new BuffType to the switch statement in the removeTimedBuff method, and call the buff-removal function with the player that is passed into removeTimedBuff as an argument.

For example, removeTimedBuff may look like:

switch (type) {
    case BT_INVINCIBLE:
        removeInvincibility(player);
        break;
    case DT_NO_JUMP:
        removeNoJumping(player);
        break;
}

The BuffInformation constructor must also be updated, to allow the UI to display the name of the currently active buffs. Add your new BuffType to the BuffInformation constructors' switch statement, and set your BuffName and effectTimeout.

The BuffName is a string which will be displayed when the player is under the effect of the timed buff or debuff. The effectTimeout is how long the timed buffs' effects will affect the player for.

For example, the switch statement may look like:

    switch (type) {
        case BT_INVINCIBLE:
            this.buffName = "Invincibility!"  // Tag to display when the player is invincible
            setEffectTimeout(5 * SECONDS);    // How long the player should stay invincible for after attaining buff
            break;
        case DT_NO_JUMP:
            this.buffName = "No Jumping!"
            setEffectTimeout(5 * SECONDS);
            break;
}

And you're done!

Spawning the buff

If a new level is created, within GetYofSurface() add the filename that was used to fill out the surface tiles:

    String filename = null;
    if (screenType == GdxGame.ScreenType.MAIN_GAME) {
      filename = "level-floors/levelOne.txt";
    } else if (screenType == GdxGame.ScreenType.LEVEL_TWO_GAME) {
      filename = "level-floors/levelTwo.txt";
    }

And then within spawnBuffDebuff inside the game area, make sure the correct level screen is passed as a parameter through to GetYOfSurface so that the correct file is read.

Further Reading

Why Entities over Classes per buff?

Using a separate class for every different buff / debuff was considered. But, since buffs / debuffs are something which should be easy to extend and add in to the game, it seemed easier to avoid having to manually create a new class every time a new buff or debuff was added. Most buffs / debuffs are similar in the way they alter the players state.

The benefit of classes is that information or attributes of particular buffs can be kept somewhere. This functionality is where the BuffInformation class comes in; a single class which holds all of the information which an individual buff or debuff class would. This means that whenever a new buff is added, only pre-existing code needs to be appended, resulting in fewer redundant & repeated classes, and ultimately a cleaner workspace.

UML documentation

Table of Contents

Home

Introduction

Main Menu

Main Game Screen

Gameplay

Player Movement

Character Animations

Enemy Monster Design and Animations

Game basic functionalities

User Testing

GitHub Wiki Tutorial

Game Engine

Getting Started

Documentation

Entities and Components

Service Locator

Loading Resources

Logging

Unit Testing

Debug Terminal

Input Handling

UI

Animations

Audio

AI

Physics

Game Screens and Areas

Terrain

Concurrency & Threading

Settings

Troubleshooting

MacOS Setup Guide

Clone this wiki locally