UGS Polishing - UQdeco2800/2022-studio-1 GitHub Wiki

Summary

Following sprint 3 it was found that the UGS was not fully integrated with necessary systems which in turn limited its functionality. Examples of this included the main character and enemy not being included in the UGS, effectively meaning the player and enemies could traverse any part of the map and walk through structures and obstacles, heavily impacting the overall game play experience. Further issues which had similar effects on the overall game play included: the lack of isometric movement, and lack of extensive jUnit tests.

Overall, fixing the aforementioned issues is important to the studio as it will greatly improve the game play experience.

Fixes

Isometric Player Movement

Through feedback received from sprint 3 it was found that player movement was preferred to be fixed to four directions (i.e. the player can either move up, down, left or right). This movement system was ideal given the UGS was created as a grid layout. Implementing this feature is important for the studio given that it will allow the full utilisation of the UGS' features, allowing detection of objects and enemies in the player's movement path.

In implementing the isometric movement of the player, the physics component of the players movement was removed in replacement for completely grid based movement. The players movement direction is defined as a vector containing their x and y directions in the UGS. This vector is calculated by calculating the residual force of opposing directions. For example, if the player holds down A and D simultaneously, the residual force would be 0 in the x direction. This is implemented as follows:

if (gameTime > lastMovementTime) {
        float posX = movementKeyPressed[3] ? 1 : 0;
        float negX = movementKeyPressed[1] ? 1 : 0;
        float posY = movementKeyPressed[0] ? 1 : 0;
        float negY = movementKeyPressed[2] ? 1 : 0;

        Vector2 movement = new Vector2(posX - negX, posY - negY);
        movePlayerInUgs(movement);

        lastMovementTime = gameTime + movementTimeDelta;
}

The lastMovementTime is used to stop the player from continuously moving at an extremely high speed. This only allows there to be any input registered after a certain number of frames, determined by movementTimeDelta.

This movement vector is then handled by the UGS, in movePlayerInUGS():

ServiceLocator.getUGSService().moveEntity(player, playerCurrentPos, direction.x, direction.y);

more details on the moveEntity function can be found here. LINK TO MORE INFO.

Player / Enemy UGS integration

Another issue identified at the end of sprint 3 relating to the UGS was that enemies an players had not been fully integrated with the UGS, allowing them to move through obstacles and structures. This is intended to be remedied, and is important to the studio as it greatly hinders game play.

Enemy movement is now handled entirely based on its position in the UGS. Instead of using a Physics component that applied an impulse to the entity and changed its game world coordinates, the enemies now use a simplified version of Dijkstra's shortest-path algorithm to move towards the target.

This movement algorithm is attached to an entity via the AITaskComponent; for example, the MeleePursueTask implements this:

                                .addTask(new MeleePursueTask(target))

In MeleePursueTask:

public MeleePursueTask(Entity target) {
        this.target = target;
        dayNightCycleService = ServiceLocator.getDayNightCycleService();
        movementTask = new MovementTask(target.getCenterPosition());
}

/**
* start chasing
*/
@Override
public void start() {
    super.start();
    movementTask.create(owner);
    movementTask.start();
}

The algorithm works by checking all neighbouring tiles of the start position, and checking them against a heuristic to see if they're valid. If a valid tile is found, it is added to a list of open tiles with a record of its previous tile. All open tiles then have their neighbours checked against the heuristic, and so on, until the target is found. The path is defined by getting the previous tile of the last point in the valid tiles list, and repeating this until the origin is found.

The Heuristic used checks the following conditions to validate a tile:

  • The tile must be closer to the target tile than its origin tile (in terms of Manhattan Distance)
  • The tile must be a valid tile in the ugs (exists)
  • The tile must not already have an entity on it

Validation Code:

private boolean validateTile(LinkedPoint tile, LinkedPoint origin, LinkedPoint targetPoint) {
    UGS ugs = ServiceLocator.getUGSService();

    // Heuristic - Manhattan distance
    int xDiff = Math.abs(targetPoint.x - tile.x);
    int yDiff = Math.abs(targetPoint.y - tile.y);

    int orgnXDiff = Math.abs(targetPoint.x - origin.x);
    int orgnYDiff = Math.abs(targetPoint.y - origin.y);

    if ((xDiff + yDiff) > (orgnXDiff + orgnYDiff + 2)) {
      return false;
    }

    if (ugs.getTile(ugs.generateCoordinate(tile.x, tile.y)) == null) {
      return false;
    }

    if (ugs.getEntity(new GridPoint2(tile.x, tile.y)) == null) {
      return true;
    }
    return false;
  }

Enemies generate their path on spawning, and do not update the path to minimise lag.

Projectile -> UGS Handling

Prior to this sprint, projectiles that were shot by enemies were being functioned by old/deprecated code from box boy and thus didn't interact well with the the player very effectively anymore. To fix this, a constantly recurring update was implemented where the projectiles position is constantly being checked with the UGS. This allowed us to removed projectiles when the 'collided' with something in the UGS.

  public void update() {
    for (Entity entity : entities) {
      entity.earlyUpdate();
      entity.update();
      if (entity.getName() != null) {
        if (entity.getName().contains("tower") && entity.getComponent(CombatStatsComponent.class).getHealth() < 1) {
          entity.dispose();
        } else if (entity.getName().contains(")Projectile") && entity.getComponent(CombatStatsComponent.class).getHealth() == 1) {
          entity.dispose();
        }
      }
    }
  }

This also allowed us to then directly handle the damage dealt between player and projectile to fix older issues included recurring damage and projectile not getting removed.

public void checkCollision() {
                Entity projectile = getEntity();
                Vector2 worldPosOfProjectile = projectile.getPosition();
                GridPoint2 gridPosOfProjectile = ServiceLocator.getEntityService().getNamedEntity("terrain").
                        getComponent(TerrainComponent.class).worldToTilePosition(worldPosOfProjectile.x, worldPosOfProjectile.y);
                String ugsKey = UGS.generateCoordinate(gridPosOfProjectile.x, gridPosOfProjectile.y + 1);
                Entity underTheProjectile = ServiceLocator.getUGSService().getTile(ugsKey).getEntity();
                String ugsKey2 = UGS.generateCoordinate(gridPosOfProjectile.x - 1, gridPosOfProjectile.y + 1);
                Entity underTheProjectile2 = ServiceLocator.getUGSService().getTile(ugsKey2).getEntity();

                if (underTheProjectile != null && !underTheProjectile.getName().contains("Mr")) {
                        if (underTheProjectile.getName().equals("player")) {
                                underTheProjectile.getComponent(CombatStatsComponent.class).hit(projectile.getComponent(CombatStatsComponent.class));
                        }
                        projectile.getComponent(CombatStatsComponent.class).setHealth(1);
                } else if (underTheProjectile2 != null && !underTheProjectile2.getName().contains("Mr")) {
                        if (underTheProjectile2.getName().equals("crystal") ) {
                                //System.out.println(ServiceLocator.getEntityService().getNamedEntity("crystal").getComponent(CombatStatsComponent.class).getHealth());
                                underTheProjectile2.getComponent(CombatStatsComponent.class).hit(projectile.getComponent(CombatStatsComponent.class));
                                //System.out.println(ServiceLocator.getEntityService().getNamedEntity("crystal").getComponent(CombatStatsComponent.class).getHealth());
                        }
                        projectile.getComponent(CombatStatsComponent.class).setHealth(1);
                }
        }

Testing

Finally, further jUnit and mockito tests were created and integrated, and can be found here.

Future Expansion

In the future, to further expand upon these features should another development team take over, further user testing, and research amongst developers should be conducted to ideate what further polishing should occur.

⚠️ **GitHub.com Fallback** ⚠️