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).
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.
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!