javagame-developmentcollision

I have interesting bug with collision detection, where the player speed is doubled when they go down and collision doesn't detect


I'm programming a top-down 2d game in Java with 8 directional movements, and while writing my next big advancement which is collision detection I've run into a major bug. As described in the question the player character (Which is a box at this point) will double their speed and go out of the map boundaries and then the game will crash (but not close) the player will become stuck in place (see the image below).

figure 1

Additionally, an array out-of-bounds error gets thrown to the console originating from the CollisionDetection class saying it has reached the map boundaries which is 5x5 (for testing purposes).

Exception in thread "Thread-0" java.lang.ArrayIndexOutOfBoundsException: Index 5 of 5 out of bounds for length 5 at lu.embellishedduck.liminalmurmurs.engine.CollisionDetection.checkTileCollision(CollisionDetection.java:134) ...

Here is the code for the CollisionDetection class, the line that throws the error is marked by a header

public class CollisionDetection {

    //=======================
    // INSTANTIATE VARIABLES
    //=======================

    GamePanel gamePanel;


    //=============
    // CONSTRUCTOR
    //=============
    public CollisionDetection(GamePanel gamePanel) {

        this.gamePanel = gamePanel;

    }//End of Constructor


    //=======================================================
    // METHOD TO CHECK A TILE TO SEE IF IT WILL COLLIDE WITH
    // THE ENTITY PASSED IN
    //=======================================================

    public void checkTileCollision(Entity entity) {

        //----------------------------------------------------
        // FIRST UP THE ROW AND COL OF THE HIT-BOX ARE NEEDED
        //----------------------------------------------------

        int entityLeftWorldX = entity.getWorldX() + entity.getHitBox().x;
        int entityRightWorldX = entity.getWorldX() + entity.getHitBox().x + entity.getHitBox().width;
        int entityTopWorldY = entity.getWorldY() + entity.getHitBox().y;
        int entityBottomWorldY = entity.getWorldY() + entity.getHitBox().y + entity.getHitBox().height;

        int entityLeftCol = entityLeftWorldX / gamePanel.getTILE_SIZE();
        int entityRightCol = entityRightWorldX / gamePanel.getTILE_SIZE();
        int entityTopRow = entityTopWorldY / gamePanel.getTILE_SIZE();
        int entityBottomRow = entityBottomWorldY / gamePanel.getTILE_SIZE();

        int tileNum1, tileNum2;

        //-----------------------
        // SWITCH STATEMENT TIME
        //-----------------------

        switch (entity.getDirection()) {

            case "up-left" :

                entityTopRow = (entityTopWorldY - entity.getMovementSpeed()) / gamePanel.getTILE_SIZE();

                tileNum1 = gamePanel.getTileManager().getMapTileNum()[entityLeftCol][entityTopRow];

                if (gamePanel.getTileManager().getTiles()[tileNum1].isCollision()) {

                    entity.setCollisionOn(true);

                }//End of If Statement

                break;

            case "up-right" :

                entityTopRow = (entityTopWorldY - entity.getMovementSpeed()) / gamePanel.getTILE_SIZE();

                tileNum1 = gamePanel.getTileManager().getMapTileNum()[entityRightCol][entityTopRow];

                if (gamePanel.getTileManager().getTiles()[tileNum1].isCollision()) {

                    entity.setCollisionOn(true);

                }//End of If Statement

                break;

            case "down-left" :

                entityBottomRow = (entityBottomWorldY + entity.getMovementSpeed()) / gamePanel.getTILE_SIZE();

                tileNum1 = gamePanel.getTileManager().getMapTileNum()[entityLeftCol][entityBottomRow];

                if (gamePanel.getTileManager().getTiles()[tileNum1].isCollision()) {

                    entity.setCollisionOn(true);

                }//End of If Statement

                break;

            case "down-right" :

                entityBottomRow = (entityBottomWorldY + entity.getMovementSpeed()) / gamePanel.getTILE_SIZE();

                tileNum1 = gamePanel.getTileManager().getMapTileNum()[entityRightCol][entityBottomRow];

                if (gamePanel.getTileManager().getTiles()[tileNum1].isCollision()) {

                    entity.setCollisionOn(true);

                }//End of If-Statement

                break;

            case "up" :

                entityTopRow = (entityTopWorldY - entity.getMovementSpeed()) / gamePanel.getTILE_SIZE();

                //This essentially predicts where the player will be and checks for collision with the if statement
                tileNum1 = gamePanel.getTileManager().getMapTileNum()[entityLeftCol][entityTopRow];
                tileNum2 = gamePanel.getTileManager().getMapTileNum()[entityRightCol][entityTopRow];

                if (gamePanel.getTileManager().getTiles()[tileNum1].isCollision() || gamePanel.getTileManager().getTiles()[tileNum2].isCollision()) {

                    entity.setCollisionOn(true);

                }//End of If Statement

                break;

            case "down" :

                entityBottomRow = (entityBottomWorldY + entity.getMovementSpeed()) / gamePanel.getTILE_SIZE();

// The line below is the erroneous one

                tileNum1 = gamePanel.getTileManager().getMapTileNum()[entityLeftCol][entityBottomRow]; // THIS LINE
                tileNum2 = gamePanel.getTileManager().getMapTileNum()[entityRightCol][entityBottomRow];

                if (gamePanel.getTileManager().getTiles()[tileNum1].isCollision() || gamePanel.getTileManager().getTiles()[tileNum2].isCollision()) {

                    entity.setCollisionOn(true);

                }//End of If Statement

                break;

            case "right" :

                entityRightCol = (entityRightWorldX + entity.getMovementSpeed()) / gamePanel.getTILE_SIZE();

                //This essentially predicts where the player will be and checks for collision with the if statement
                tileNum1 = gamePanel.getTileManager().getMapTileNum()[entityRightCol][entityTopRow];
                tileNum2 = gamePanel.getTileManager().getMapTileNum()[entityRightCol][entityBottomRow];

                if (gamePanel.getTileManager().getTiles()[tileNum1].isCollision() || gamePanel.getTileManager().getTiles()[tileNum2].isCollision()) {

                    entity.setCollisionOn(true);

                }//End of If Statement

                break;

            case "left" :

                entityLeftCol = (entityLeftWorldX - entity.getMovementSpeed()) / gamePanel.getTILE_SIZE();

                //This essentially predicts where the player will be and checks for collision with the if statement
                tileNum1 = gamePanel.getTileManager().getMapTileNum()[entityLeftCol][entityTopRow];
                tileNum2 = gamePanel.getTileManager().getMapTileNum()[entityLeftCol][entityBottomRow];

                if (gamePanel.getTileManager().getTiles()[tileNum1].isCollision() || gamePanel.getTileManager().getTiles()[tileNum2].isCollision()) {

                    entity.setCollisionOn(true);

                }//End of If Statement

                break;

        }//End of Switch-Case Statement

    }//End of Method

}//End of Class

The MapTileNum is a 2D array of type int[][] where the first part of the x is equal to the maximum number of tile columns that a world map can have, the second part is the exact same as the first except equal to the maximum number of tile rows. Every displayed tile is 96x96 pixels these have been multiplied by a factor of 0.75 from the sprite size of 128. As mentioned previously the maximum number of columns and rows is 5 (for a 5x5 world map size)

Additionally, it could be a problem with the movement handling in the Player class which is shown below:

public class Player extends Entity {

    //======================
    // Instantiate Variables
    //======================

    GamePanel gamePanel;
    KeyHandler keyHandler;

    //These are hardcoded values that tell the program where on the screen the player character should start when they enter the game
    private final int screenX;
    private final int screenY;


    //=============
    // CONSTRUCTOR
    //=============

    public Player(GamePanel gp, KeyHandler keyHandler) {

        this.gamePanel = gp;
        this.keyHandler = keyHandler;

        //This logic makes that player always start in the middle of the screen
        screenX = gp.getSCREEN_WIDTH() / 2 - (gp.getTILE_SIZE() / 2);
        screenY = gp.getSCREEN_HEIGHT() / 2 - (gp.getREAL_TILE_SIZE() / 2);

        setHitBox(new Rectangle());
        getHitBox().x = 16;
        getHitBox().y = 32;
        getHitBox().width = 64;
        getHitBox().height = 64;

        setDefaultValues();
        getPlayerImage();

    }//End of Constructor


    //===========================================================
    // Method to set the default values for the player variables
    //===========================================================

    public void setDefaultValues() {

        //Obviously there will be more stats here in the future but those will have to be loaded to and from a save file
        setMovementSpeed(4);

        //Here the player's position is set relative to tile location on the world map
        //for now I'm just setting this to 100 because I haven't made any world yet.
        //Also, eventually there will be a save file system which will let the player
        //continue from where they left off on the map.

        //Should be mainPanel.getTileSize() * row/col

        setWorldX(gamePanel.getTILE_SIZE() * 1);
        setWorldY(gamePanel.getTILE_SIZE() * 2);

    }//End of Method


    //=======================================
    // Method to get the image of the player
    //=======================================

    public void getPlayerImage() {

        //Eventually there will be image IO things in here

    }//End of Class


    //==============================================================
    // Method which handles the updating of the player's key inputs
    //==============================================================

    public void update() {

        //-----------------
        // Local Variables
        //-----------------

        int diagonalSpeed = (int) Math.round(getMovementSpeed() / Math.sqrt(2));

        //----------------------------------------------------
        // If Statement barrage! This time handling all cases
        //----------------------------------------------------

        if (keyHandler.isUpPressed() || keyHandler.isLeftPressed() || keyHandler.isRightPressed() || keyHandler.isDownPressed()
            || keyHandler.isUpLeftPressed() || keyHandler.isUpRightPressed() || keyHandler.isDownLeftPressed() || keyHandler. isDownRightPressed()) {

            if (keyHandler.isUpLeftPressed()) {

                setDirection("up-left");

            } else if (keyHandler.isUpRightPressed()) {

                setDirection("up-right");

            } else if (keyHandler.isDownLeftPressed()) {

                setDirection("down-left");

            } else if (keyHandler.isDownRightPressed()) {

                setDirection("down-right");

            } else {
                //Handling the primary movement
                if (keyHandler.isUpPressed()) {

                    setDirection("up");

                }
                if (keyHandler.isDownPressed()) {

                    setDirection("down");
                    worldY += getMovementSpeed();
                }
                if (keyHandler.isRightPressed()) {

                    setDirection("right");

                }
                if (keyHandler.isLeftPressed()) {

                    setDirection("left");

                }

            }//End of Primary Movement Else Statement

            //CHECK TILE COLLISION
            setCollisionOn(false);
            gamePanel.getCollisionDetector().checkTileCollision(this);

            //IF COLLISION IS FALSE PLAYER CAN MOVE
            if (!isCollisionOn()) {

                //Another switch case which will deal with the player movement speed

                switch (getDirection()) {

                    case "up-left" :

                        worldX -= diagonalSpeed;
                        worldY -= diagonalSpeed;

                        break;

                    case "up-right" :

                        worldX += diagonalSpeed;
                        worldY -= diagonalSpeed;

                        break;

                    case "down-left" :

                        worldX -= diagonalSpeed;
                        worldY += diagonalSpeed;

                        break;

                    case "down-right" :

                        worldX += diagonalSpeed;
                        worldY += diagonalSpeed;

                        break;

                    case "up" :

                        worldY -= getMovementSpeed();

                        break;

                    case "down" :

                        worldY += getMovementSpeed();

                        break;

                    case "right" :

                        worldX += getMovementSpeed();

                        break;

                    case "left" :

                        worldX -= getMovementSpeed();

                        break;

                }//End of Switch-Case Statement

            }//End of If Statement

        }//End of If Statement Barrage

    }//End of Method


    //============================================================================
    // Method which handles the drawing of the player to whichever panel calls it
    //============================================================================

    public void draw (Graphics2D graphics2D){

        graphics2D.setColor(Color.WHITE);
        graphics2D.fillRect(screenX, screenY, gamePanel.getTILE_SIZE(), gamePanel.getTILE_SIZE());

        graphics2D.setColor(Color.RED);
        graphics2D.fillRect(screenX + 16, screenY + 32, 64, 64);

    }//End of Method


    //=========
    // GETTERS
    //=========

    public GamePanel getGamePanel() {
        return gamePanel;
    }
    public KeyHandler getKeyHandler() {
        return keyHandler;
    }
    public int getScreenX() {
        return screenX;
    }
    public int getScreenY() {
        return screenY;
    }


    //=========
    // SETTERS
    //=========

    public void setGamePanel(GamePanel gamePanel) {
        this.gamePanel = gamePanel;
    }
    public void setKeyHandler(KeyHandler keyHandler) {
        this.keyHandler = keyHandler;
    }


    //=================
    // toString METHOD
    //=================
    @Override
    public String toString() {

        return "Player{" +
                "gp=" + gamePanel +
                ", keyHandler=" + keyHandler +
                ", screenX=" + screenX +
                ", screenY=" + screenY +
                '}';

    }//End of toString Method

}//End of Class

Also, there is a tiny glitch that I noticed where the player's box will collide with the hitbox of the wall with the direct directional input (i.e. left) and if the player then attempts to move diagonally it moves the player by about 4 pixels into the wall and they become veritably stuck in between two imaginary hitboxes. I think this is related to the main problem the collision is erroneous.

I've tried adjusting the actual player speed and reversing some of the math operators in the tileNum fields but those just generated more bugs and so I undid them. Other than that I've caught a few missing break; statements for the switch case. I've looked at other sources on the internet but didn't find anything

the glitch I have 0 clue what could be the cause of that.


Solution

  • Well it seems I've once again answered my own question. As it turns out it wasn't an error in the way I was handling collision. But rather a problem with the way I handled the diagonal movement, you see if I had just simply rearranged some stuff in the Player's update() method I could've avoided writing 4 extra cases of Gobble D. Gyuck.

    However it should be noted that should the player somehow exit the map the program will crash and throw the out of bounds exception. So that will need to be handled somehow, most likely logging the error and preventing the game from having a sissy fit.

    Here are the fixed classes (without the array error solution) |

    Collision Detector (Formerly CollisionDetection) |

    public class CollisionDetector {
    
        //=======================
        // INSTANTIATE VARIABLES
        //=======================
    
        GamePanel gamePanel;
    
    
        //=============
        // CONSTRUCTOR
        //=============
        public CollisionDetector(GamePanel gamePanel) {
    
            this.gamePanel = gamePanel;
    
        }//End of Constructor
    
    
        public void checkTileCollision(Entity entity) {
    
            //----------------------------------------------------
            // FIRST UP THE ROW AND COL OF THE HIT-BOX ARE NEEDED
            //----------------------------------------------------
    
            int entityLeftWorldX = entity.getWorldX() + entity.getHitBox().x;
            int entityRightWorldX = entity.getWorldX() + entity.getHitBox().x + entity.getHitBox().width;
            int entityTopWorldY = entity.getWorldY() + entity.getHitBox().y;
            int entityBottomWorldY = entity.getWorldY() + entity.getHitBox().y + entity.getHitBox().height;
    
            int entityLeftCol = entityLeftWorldX / gamePanel.getTILE_SIZE();
            int entityRightCol = entityRightWorldX / gamePanel.getTILE_SIZE();
            int entityTopRow = entityTopWorldY / gamePanel.getTILE_SIZE();
            int entityBottomRow = entityBottomWorldY / gamePanel.getTILE_SIZE();
    
            int tileNum1, tileNum2;
    
            //-----------------------
            // SWITCH STATEMENT TIME
            //-----------------------
    
            switch (entity.getDirection()) {
    
                case "up" :
    
                    entityTopRow = (entityTopWorldY - entity.getMovementSpeed()) / gamePanel.getTILE_SIZE();
    
                    //This essentially predicts where the player will be and checks for collision with the if statement
                    tileNum1 = gamePanel.getTileManager().getMapTileNum()[entityLeftCol][entityTopRow];
                    tileNum2 = gamePanel.getTileManager().getMapTileNum()[entityRightCol][entityTopRow];
    
                    if (gamePanel.getTileManager().getTiles()[tileNum1].isCollision() || gamePanel.getTileManager().getTiles()[tileNum2].isCollision()) {
    
                        entity.setCollisionOn(true);
    
                    }//End of If Statement
    
                    break;
    
                case "down" :
    
                    entityBottomRow = (entityBottomWorldY + entity.getMovementSpeed()) / gamePanel.getTILE_SIZE();
    
                    tileNum1 = gamePanel.getTileManager().getMapTileNum()[entityLeftCol][entityBottomRow];
                    tileNum2 = gamePanel.getTileManager().getMapTileNum()[entityRightCol][entityBottomRow];
    
                    if (gamePanel.getTileManager().getTiles()[tileNum1].isCollision() || gamePanel.getTileManager().getTiles()[tileNum2].isCollision()) {
    
                        entity.setCollisionOn(true);
    
                    }//End of If Statement
    
                    break;
    
                case "right" :
    
                    entityRightCol = (entityRightWorldX + entity.getMovementSpeed()) / gamePanel.getTILE_SIZE();
    
                    tileNum1 = gamePanel.getTileManager().getMapTileNum()[entityRightCol][entityTopRow];
                    tileNum2 = gamePanel.getTileManager().getMapTileNum()[entityRightCol][entityBottomRow];
    
                    if (gamePanel.getTileManager().getTiles()[tileNum1].isCollision() || gamePanel.getTileManager().getTiles()[tileNum2].isCollision()) {
    
                        entity.setCollisionOn(true);
    
                    }//End of If Statement
    
                    break;
    
                case "left" :
    
                    entityLeftCol = (entityLeftWorldX - entity.getMovementSpeed()) / gamePanel.getTILE_SIZE();
    
                    tileNum1 = gamePanel.getTileManager().getMapTileNum()[entityLeftCol][entityTopRow];
                    tileNum2 = gamePanel.getTileManager().getMapTileNum()[entityLeftCol][entityBottomRow];
    
                    if (gamePanel.getTileManager().getTiles()[tileNum1].isCollision() || gamePanel.getTileManager().getTiles()[tileNum2].isCollision()) {
    
                        entity.setCollisionOn(true);
    
                    }//End of If Statement
    
                    break;
    
            }//End of Switch-Case Statement
    
        }//End of Method
    
    }//End of Class
    

    Player Class |

    public class Player extends Entity {
    
        //======================
        // Instantiate Variables
        //======================
    
        GamePanel gamePanel;
        KeyHandler keyHandler;
    
        //These are hardcoded values that tell the program where on the screen the player character should start when they enter the game
        private final int screenX;
        private final int screenY;
    
    
        //=============
        // CONSTRUCTOR
        //=============
    
        public Player(GamePanel gp, KeyHandler keyHandler) {
    
            this.gamePanel = gp;
            this.keyHandler = keyHandler;
    
            //This logic makes that player always start in the middle of the screen
            screenX = gp.getSCREEN_WIDTH() / 2 - (gp.getTILE_SIZE() / 2);
            screenY = gp.getSCREEN_HEIGHT() / 2 - (gp.getREAL_TILE_SIZE() / 2);
    
            setHitBox(new Rectangle());
            getHitBox().x = 16;
            getHitBox().y = 32;
            getHitBox().width = 64;
            getHitBox().height = 64;
    
            setDefaultValues();
            getPlayerImage();
    
        }//End of Constructor
    
    
        //===========================================================
        // Method to set the default values for the player variables
        //===========================================================
    
        public void setDefaultValues() {
    
            //Obviously there will be more stats here in the future but those will have to be loaded to and from a save file
            setMovementSpeed(4);
    
            //Here the player's position is set relative to tile location on the world map
            //for now I'm just setting this to 100 because I haven't made any world yet.
            //Also, eventually there will be a save file system which will let the player
            //continue from where they left off on the map.
    
            //Should be mainPanel.getTileSize() * row/col
    
            setWorldX(gamePanel.getTILE_SIZE() * 1);
            setWorldY(gamePanel.getTILE_SIZE() * 2);
    
        }//End of Method
    
    
        //=======================================
        // Method to get the image of the player
        //=======================================
    
        public void getPlayerImage() {
    
            //Eventually there will be image IO things in here
    
        }//End of Class
    
    
        //==============================================================
        // Method which handles the updating of the player's key inputs
        //==============================================================
    
        public void update() {
    
            //-----------------------
            // INSTANTIATE VARIABLES
            //-----------------------
    
            int diagonalSpeed = (int) Math.round(getMovementSpeed() / Math.sqrt(2));
    
            int originalWorldX = this.getWorldX();
            int originalWorldY = this.getWorldY();
    
            //-----------------------
            // If Statement barrage!
            //-----------------------
    
            if (keyHandler.isUpPressed() || keyHandler.isDownPressed() || keyHandler.isRightPressed() || keyHandler.isLeftPressed()) {
    
                if (keyHandler.isUpPressed()) {
                    setDirection("up");
                    worldY -= getMovementSpeed();
                }
                if (keyHandler.isDownPressed()) {
                    setDirection("down");
                    worldY += getMovementSpeed();
                }
                if (keyHandler.isRightPressed()) {
                    setDirection("right");
                    worldX += getMovementSpeed();
                }
                if (keyHandler.isLeftPressed()) {
                    setDirection("left");
                    worldX -= getMovementSpeed();
                }
    
                //Now collision must be handled before anything is continued, additionally the original worldX and worldY values must be stored
                //to preserve diagonal movement
                setCollisionOn(false);
                gamePanel.getCollisionDetector().checkTileCollision(this);
    
                //If collision is false player can move
                if (isCollisionOn()) {
    
                    this.setWorldX(originalWorldX);
                    this.setWorldY(originalWorldY);
    
                }//End of If Statement
    
            }//End of If Statement
    
        }//End of Method
    
    
        //============================================================================
        // Method which handles the drawing of the player to whichever panel calls it
        //============================================================================
    
        public void draw (Graphics2D graphics2D){
    
            graphics2D.setColor(Color.WHITE);
            graphics2D.fillRect(screenX, screenY, gamePanel.getTILE_SIZE(), gamePanel.getTILE_SIZE());
    
        }//End of Method
    
    
        //=========
        // GETTERS
        //=========
    
        public GamePanel getGamePanel() {
            return gamePanel;
        }
        public KeyHandler getKeyHandler() {
            return keyHandler;
        }
        public int getScreenX() {
            return screenX;
        }
        public int getScreenY() {
            return screenY;
        }
    
    
        //=========
        // SETTERS
        //=========
    
        public void setGamePanel(GamePanel gamePanel) {
            this.gamePanel = gamePanel;
        }
        public void setKeyHandler(KeyHandler keyHandler) {
            this.keyHandler = keyHandler;
        }
    
    
        //=================
        // toString METHOD
        //=================
        @Override
        public String toString() {
    
            return "Player{" +
                    "gp=" + gamePanel +
                    ", keyHandler=" + keyHandler +
                    ", screenX=" + screenX +
                    ", screenY=" + screenY +
                    '}';
    
        }//End of toString Method
    
    }//End of Class
    

    Happy Coding!