Table of contents

  1. Collision Detection
    1. What is collision detection?
    2. Player collision detection with Map Tiles
    3. Player collision detection with Enhanced Map Tiles
    4. Player collision detection with NPCs
    5. Player collision detection with Triggers
    6. Player class reacting to a collision

Collision Detection

What is collision detection?

Collision detection is the ability for entities in the game, such as the player, to be able to detect “collisions” with other map entities, such as map tiles, NPCs, etc. This is achieved through checking if two entities “intersect” each other, at which point a collision has occurred. Games are built around collision detection – think of any video game and tell me if the game would still work if entities could not detect and react to colliding with other entities, and I guarantee you it cannot.

The hard part about collision detection is there are A LOT of different “scenarios” where a collision can occur between two entities, and each entity needs to react accordingly to the collision. All map entities in this game engine are rectangles (x, y, width, height) under the hood, which thankfully lessens a lot of the complexity that other shapes would introduce. Rectangles are the easiest shape to work with by far due to “rectangle math” being relatively simple, and it works out very nicely in a 2D space where only the x and y axis exist.

Also, collision detection is always the hardest part about creating a game, and anyone will tell you that writing your own collision detection is the WORST.. This is especially true when it comes to 3D games – it is difficult, complex math can be involved, and tiny errors randomly creep up just when you finally think you’ve gotten everything down pact. Luckily, with 2D games, things are a lot simpler, but still can be a real PITA.

Player collision detection with Map Tiles

tl;dr: This is probably the most complicated part of this game. Each frame the player moves by a specified amount unless it collides with a solid map tile, in which case it is told to stop moving. Use the moveXHandleCollision and moveYHandleCollision methods when moving an entity that needs to detect collision. An entity’s generic moveX and moveY methods do not account for collision.

The reason for needing extremely precise collision detection in a game is for the player to be able to traverse the map without being able to run through obstacles or map entities.

The GameObject class contains two special move methods named moveXHandleCollisions and moveYHandleCollision. These methods will move the player by a specified amount, and the Player calls these methods each frame to move it by a set amount based on which actions the player took (such as walking forward). Unlike the plain moveX and moveY methods which simply move a player by a specified amount with no questions asked, the moveXHandleCollision and moveYHandleCollision methods will stop the player from moving if it collides with a “solid” entity, such as a NOT_PASSABLE (“solid”) map tile. Each MapTile has an assigned tile type, and the move handle collisions methods will take these into account when moving the player to determine if a player is allowed to move to the new location or not.

Decimal movement makes this a bit more complex. While a player can be at a decimal location in game logic, in draw logic it cannot since a graphic cannot be physically drawn on half of a monitor pixel. For this reason, while decimal precision is still possible to implement, it just won’t always apply a graphics update if the decimal movement is small enough to not change the round up or down to the nearest whole number. For example, a decimal amount of 1.49 will round down to 1, but 1.5 will round up to 2.

So, how exactly do the moveXHandleCollision and moveYHandleCollision methods work? First thing they do is identify the amount of pixels the player is about to move, the direction to move in, and lastly calculate decimal values to figure out if a potential round up or round down change will occur. From there, the methods will move the player one pixel at a time over and over again until it reaches the total amount the player is supposed to move by. During each one pixel move, collisions are checked against the map tiles, and if a collision is detected the movement is stopped and all leftover movement is thrown away. Lastly, decimal values are recalculated and figured out – if a round up occurs, one more pixel is moved and collision detection is checked one last time.

The Player calls these two methods from the GameObject class each frame in its update logic:

// move player with respect to map collisions based on how much player needs to move this frame
lastAmountMovedY = super.moveYHandleCollision(moveAmountY);
lastAmountMovedX = super.moveXHandleCollision(moveAmountX);

The MapCollisionHandler class is used by the move handle collisions methods to determine if a collision occurred, and if a collision did occur the player’s position is adjusted to be right in front of the solid map tile it collided with.

The MapCollisionHandler contains three methods: getAdjustedPositionAfterCollisionCheckX, getAdjustedPositionAfterCollisionCheckY, and hasCollidedWithMapEntity.

The first two methods getAdjustedPositionAfterCollisionCheckX and getAdjustedPositionAfterCollisionCheckY do some “rectangle math” to determine which tiles in the map need to be checked for collisions. A common mistake newer game developers make is writing collision checking code that checks against every single tile in the map every single frame – this is a huge waste of resources and can severely impact the game’s FPS as the game logic step can end up taking too long. Instead, this code will determine the direction the player is moving, and based on that only check the immediate tile(s) in the vicinity. Just like with sorting algorithms, lowering the amount of comparisons is key to have a smoothly running game! After determining which tiles could possibly be collied with, the third method hasCollidedWithMapEntity is called on each map tile that to see if a collision has occurred based on the map tile’s tile type.

The hasCollidedWithMapEntity method is very simple compared to the monstrous other two adjust position methods. It determines if the tile a player intersects with is considered a “collision” or not. This is 100% based on the tile’s tile type. A tile type of NOT_PASSABLE would return true to there being a collision if the player intersects with it. PASSABLE tiles cannot be collided with and would always return false.

// based on tile type, perform logic to determine if a collision did occur with an intersecting tile or not
private static boolean hasCollidedWithMapEntity(GameObject gameObject, MapEntity mapEntity, Direction direction) {
    if (mapEntity instanceof MapTile) {
        MapTile mapTile = (MapTile)mapEntity;
        switch (mapTile.getTileType()) {
            case PASSABLE:
                return false;
            case NOT_PASSABLE:
                return gameObject.intersects(mapTile);
            default:
                return false;
        }
    }
    else {
        return mapEntity.intersects(gameObject);
    }
}

More tile types can easily be added to this method to determine if a collision occurred or not. The other two methods don’t really have to be modified at all, as new tiles types added to this hasCollidedWithMapEntity method will work just as the existing three tile types do.

Player collision detection with Enhanced Map Tiles

Additionally, these methods check against all active enhanced map tiles in the map, as they are treated like regular map tiles in terms of collision detection. So for example, the game’s green horizontal moving platform would also be checked against for possible collisions during this area of the code.

Although standard collision detection on enhanced map tiles is done here, EnhancedMapTile classes can run their own update logic to do other actions when intersecting with a player. For example, the PushableRock class carries out its own logic upon determining that the player walked into it, so it can move itself to make it appear like it is being pushed.

pushing-rock.gif

Player collision detection with NPCs

Each NPC has its own defined bounds, which acts as their “hurtbox”. This makes collision detection very easy – the player is not allowed to walk through an NPC’s bounds.

Player collision detection with Triggers

If a player walks on top of a trigger, the player will be stopped in its tracks, and the trigger script will be set to active and begin executing.

Player class reacting to a collision

The GameObject method provides two methods that are intended to be overridden by a subclass: onEndCollisionCheckX and onEndCollisionCheckY. After a collision check has occurred, the collision methods will let the Player class (or any other GameObject subclass like the NPC class that can move while checking for collisions) know if a collision occurred and what direction the collision happened from (left, right, up, or down).

At the moment, these aren’t being used in the Player class (or any other entity), but they can be really useful for other future features.. Here’s how they would be used:

@Override
public void onEndCollisionCheckY(boolean hasCollided, Direction direction, MapEntity entityCollidedWith) {
    // if player collides with something
    if (hasCollided) {
        // if player collided with something below it
        if (direction == Direction.DOWN) {
            // ... do something here
        }
    }
}

The entityCollidedWith parameter can be checked against using the instanceof keyword like in the below example:

if (hasCollided && entityCollidedWith instanceof MapTile) {
    MapTile tileCollidedWith = (MapTile)entityCollidedWith;
    // do whatever you want from here
}

The above also works with enhanced map tiles, NPCs, etc.:

if (hasCollided && entityCollidedWith instanceof EnhancedMapTile) {
    EnhancedMapTile tileCollidedWith = (EnhancedMapTile)entityCollidedWith;
    // do whatever you want from here
}