Previous Tutorial
Next Tutorial
Posted on: 03-11-2012

Gravity Platformer Tutorial #5 : Collision Detection part 2

In the last part we created a model that holds our tiles and we've also defined the axis aligned bounding shape.

Lets start using it!

  1. We start off by introducing another class. This class is called "LevelObject" and will be located in the "game.level" package.

    The LevelObject class is going to be the superclass of all the dynamic objects in our game, including characters, enemies and all other objects that we will introduce.

    We still start off by defining its attributes:

    1. protected float x;
    2. protected float y;
    3. protected BoundingShape boundingShape;
    4.  
    5. protected float x_velocity = 0;
    6. protected float y_velocity = 0;
    7. protected float maximumFallSpeed = 1;
    8.  
    9. protected boolean onGround = true;

    It is more common to call the x and y velocities the horizontal and vertical velocity, but as we will have 4 different gravities it would only be confusing.

    The full class will look like this:

    1. public abstract class LevelObject {
    2.  
    3. protected float x;
    4. protected float y;
    5. protected BoundingShape boundingShape;
    6.  
    7. protected float x_velocity = 0;
    8. protected float y_velocity = 0;
    9. protected float maximumFallSpeed = 1;
    10.  
    11. protected boolean onGround = true;
    12.  
    13. public LevelObject(float x, float y){
    14. this.x = x;
    15. this.y = y;
    16.  
    17. //default bounding shape is a 32 by 32 box
    18. boundingShape = new AABoundingRect(x,y,32,32);
    19. }
    20.  
    21. public void applyGravity(float gravity){
    22. //if we aren't already moving at maximum speed
    23. if(y_velocity < maximumFallSpeed){
    24. //accelerate
    25. y_velocity += gravity;
    26. if(y_velocity > maximumFallSpeed){
    27. //and if we exceed maximum speed, set it to maximum speed
    28. y_velocity = maximumFallSpeed;
    29. }
    30. }
    31. }
    32.  
    33. public float getYVelocity() {
    34. return y_velocity;
    35. }
    36.  
    37. public void setYVelocity(float f){
    38. y_velocity = f;
    39. }
    40.  
    41. public float getXVelocity(){
    42. return x_velocity;
    43. }
    44.  
    45. public void setXVelocity(float f){
    46. x_velocity = f;
    47. }
    48.  
    49. public float getX(){
    50. return x;
    51. }
    52.  
    53. public float getY(){
    54. return y;
    55. }
    56.  
    57. public void setX(float f){
    58. x = f;
    59. updateBoundingShape();
    60. }
    61.  
    62. public void setY(float f){
    63. y = f;
    64. updateBoundingShape();
    65. }
    66.  
    67. public void updateBoundingShape(){
    68. boundingShape.updatePosition(x,y);
    69. }
    70.  
    71. public boolean isOnGround(){
    72. return onGround;
    73. }
    74.  
    75. public void setOnGround(boolean b){
    76. onGround = b;
    77. }
    78.  
    79. public BoundingShape getBoundingShape(){
    80. return boundingShape;
    81. }
    82.  
    83. }

    There are 2 things I'd like to clarify, the first begin the updateBoundingShape().

    The updateBoundingShape() is a method that we could override if our bounding box is not situated at the same position as the object itself. This will be useful for our player as you will see later.

    The second thing is the applyGravity, this is a method that tries to apply a gravitational force to our object, it makes sure we don't fall faster than our maximum falling speed.

  2. Because we will be working with velocities now, our character movement needs an overhaul as well, we can't just update the x anymore.

    1. public abstract class Character extends LevelObject {
    2.  
    3. protected HashMap<Facing,Image> sprites;
    4.  
    5. protected HashMap<Facing,Animation> movingAnimations;
    6. protected Facing facing;
    7. protected boolean moving = false;
    8. protected float accelerationSpeed = 1;
    9. protected float decelerationSpeed = 1;
    10. protected float maximumSpeed = 1;
    11.  
    12. public Character(float x, float y) throws SlickException{
    13. super(x,y);
    14. //in case we forget to set the image, we don't want the game to crash, but it still has to be obvious that something was forgotten
    15. setSprite(new Image("data/img/placeholder_sprite.png"));
    16.  
    17. //default direction will be right
    18. facing = Facing.RIGHT;
    19. }
    20.  
    21. protected void setMovingAnimation(Image[] images, int frameDuration){
    22. movingAnimations = new HashMap<Facing,Animation>();
    23.  
    24. //we can just put the right facing in with the default images
    25. movingAnimations.put(Facing.RIGHT, new Animation(images,frameDuration));
    26.  
    27. Animation facingLeftAnimation = new Animation();
    28. for(Image i : images){
    29. facingLeftAnimation.addFrame(i.getFlippedCopy(true, false), frameDuration);
    30. }
    31. movingAnimations.put(Facing.LEFT, facingLeftAnimation);
    32.  
    33. }
    34.  
    35. protected void setSprite(Image i){
    36. sprites = new HashMap<Facing,Image>();
    37. sprites.put(Facing.RIGHT, i);
    38. sprites.put(Facing.LEFT , i.getFlippedCopy(true, false));
    39. }
    40.  
    41. public boolean isMoving(){
    42. return moving;
    43. }
    44.  
    45. public void setMoving(boolean b){
    46. moving = b;
    47. }
    48.  
    49. //move towards x_velocity = 0
    50. public void decelerate(int delta) {
    51. if(x_velocity > 0){
    52. x_velocity -= decelerationSpeed * delta;
    53. if(x_velocity < 0)
    54. x_velocity = 0;
    55. }else if(x_velocity < 0){
    56. x_velocity += decelerationSpeed * delta;
    57. if(x_velocity > 0)
    58. x_velocity = 0;
    59. }
    60. }
    61.  
    62. public void jump(){
    63. if(onGround)
    64. y_velocity = -0.4f;
    65. }
    66.  
    67. public void moveLeft(int delta){
    68. //if we aren't already moving at maximum speed
    69. if(x_velocity > -maximumSpeed){
    70. //accelerate
    71. x_velocity -= accelerationSpeed*delta;
    72. if(x_velocity < -maximumSpeed){
    73. //and if we exceed maximum speed, set it to maximum speed
    74. x_velocity = -maximumSpeed;
    75. }
    76. }
    77. moving = true;
    78. facing = Facing.LEFT;
    79. }
    80.  
    81. public void moveRight(int delta){
    82. if(x_velocity < maximumSpeed){
    83. x_velocity += accelerationSpeed*delta;
    84. if(x_velocity > maximumSpeed){
    85. x_velocity = maximumSpeed;
    86. }
    87. }
    88. moving = true;
    89. facing = Facing.RIGHT;
    90. }
    91.  
    92. public void render(){
    93.  
    94. //draw a moving animation if we have one and we moved within the last 150 miliseconds
    95. if(movingAnimations != null && moving){
    96. movingAnimations.get(facing).draw(x-2,y-2);
    97. }else{
    98. sprites.get(facing).draw(x-2, y-2);
    99. }
    100. }
    101.  
    102. }

    As you can see, there are quite a lot of changes here. Lets walk through them.

    • One of the most important changes is that the moveLeft and moveRight now use the x_velocity instead of directly updating our characters position. We will have a special class later that will handle the movement of all objects in the level and that will use collision detection to check if we can move.
    • Another change in the moveLeft and moveRight methods is that we set a moving attribute. I thought about this a lot and decided to remove the lastTimeMoved because it would be to inaccurate for the deceleration of our character.
    • Which brings us to the acceleration and deceleration speeds, this is something that makes our characters able to move a bit smoother. After all, are you instantly at full speed when you start running?
    • The decelerate method handles the deceleration of the character, an important thing here is to check which way we are going (we can have a positive x velocity, which moves us right but also a negative one that moves us left).
    • And last but not least, the jump method that sets a y_velocity if we are on the ground

  3. Now the last changes are to the Player class:

    1. public class Player extends Character {
    2.  
    3. public Player(float x, float y) throws SlickException{
    4. super(x,y);
    5. setSprite(new Image("data/img/characters/player/player.png"));
    6.  
    7. setMovingAnimation(new Image[]{new Image("data/img/characters/player/player_1.png"),new Image("data/img/characters/player/player_2.png"),
    8. new Image("data/img/characters/player/player_3.png"),new Image("data/img/characters/player/player_4.png")}
    9. ,125);
    10. boundingShape = new AABoundingRect(x+3, y, 26, 32);
    11.  
    12. accelerationSpeed = 0.001f;
    13. maximumSpeed = 0.15f;
    14. maximumFallSpeed = 0.3f;
    15. decelerationSpeed = 0.001f;
    16. }
    17.  
    18. public void updateBoundingShape(){
    19. boundingShape.updatePosition(x+3,y);
    20. }
    21.  
    22. }

    Here is where the updateBoundingShape method comes in handy. Our character is not exactly 32 pixels wide, so we want our bounding rectangle to be a bit thinner. This also means that our bounding shape will not be at the same location as our player, it has to be moved 3 pixels to the right to be in the correct spot.

    Something that you might think is that the acceleration speed and the deceleration speed are really low. They are not that low if you keep the delta in mind, which will multiply all those values by at least 16 (at 60 frames per second).

  4. The last thing we do is update our MouseAndKeyBoardPlayerController class to tell our player that he isn't moving whenever we don't have a movement key down and add a key for jumping:

    1. private void handleKeyboardInput(Input i, int delta){
    2. //we can both use the WASD or arrow keys to move around, obviously we can't move both left and right simultaneously
    3. if(i.isKeyDown(Input.KEY_A) || i.isKeyDown(Input.KEY_LEFT)){
    4. player.moveLeft(delta);
    5. }else if(i.isKeyDown(Input.KEY_D) || i.isKeyDown(Input.KEY_RIGHT)){
    6. player.moveRight(delta);
    7. }else{
    8. //we dont move if we don't press left or right, this will have the effect that our player decelerates
    9. player.setMoving(false);
    10. }
    11.  
    12. if(i.isKeyDown(Input.KEY_SPACE)){
    13. player.jump();
    14. }
    15. }

If you run the game now, you will notice that we "broke" the movement. But technically it isn't broken, our characters get the right movement. Its just that we don't move them yet according to their speeds.

  1. So lets fix this problem by introducing a Physics class to our game, this class will go into "game.physics" (who would've guessed?)

    1. public class Physics {
    2.  
    3. private final float gravity = 0.0015f;
    4.  
    5. public void handlePhysics(Level level, int delta){
    6.  
    7. }
    8.  
    9. }

    The handlePhysics will handle all the physics within a level, and of course we don't have to forget about the delta!

    I've also already defined a gravity here, we will make use of this later.

  2. Before we implement the physics, lets actually add it to our LevelState. This will mean we add an attribute, create a Physics object in the constructor.

    Last thing we have to do here is call the "handlePhysics" method in the update:

    1. public void update(GameContainer container, StateBasedGame sbg, int delta) throws SlickException {
    2. //every update we have to handle the input from the player
    3. playerController.handleInput(container.getInput(), delta);
    4. physics.handlePhysics(level, delta);
    5. }
  3. Now we are going to create a few helpful methods in our Physics class, the first being "checkCollision(LevelObject obj, Tile[][] mapTiles)". This method will get all the tiles occupied by the object, and check collisions with them.

    1. private boolean checkCollision(LevelObject obj, Tile[][] mapTiles){
    2. //get only the tiles that matter
    3. ArrayList<Tile> tiles = obj.getBoundingShape().getTilesOccupying(mapTiles);
    4. for(Tile t : tiles){
    5. //if this tile has a bounding shape
    6. if(t.getBoundingShape() != null){
    7. if(t.getBoundingShape().checkCollision(obj.getBoundingShape())){
    8. return true;
    9. }
    10. }
    11. }
    12. return false;
    13. }

    And here we see our hard work from the first part paying back.

  4. The next method is going to be "isOnGround(LevelObject obj, Tile[][] mapTiles)". This method will be a bit more complex than the previous one.

    1. private boolean isOnGroud(LevelObject obj, Tile[][] mapTiles){
    2. //we get the tiles that are directly "underneath" the characters, also known as the ground tiles
    3. ArrayList<Tile> tiles = obj.getBoundingShape().getGroundTiles(mapTiles);
    4.  
    5. //we lower the the bounding object a bit so we can check if we are actually a bit above the ground
    6. obj.getBoundingShape().movePosition(0, 1);
    7.  
    8. for(Tile t : tiles){
    9. //not every tile has a bounding shape (air tiles for example)
    10. if(t.getBoundingShape() != null){
    11. //if the ground and the lowered object collide, then we are on the ground
    12. if(t.getBoundingShape().checkCollision(obj.getBoundingShape())){
    13. //don't forget to move the object back up even if we are on the ground!
    14. obj.getBoundingShape().movePosition(0, -1);
    15. return true;
    16. }
    17. }
    18. }
    19.  
    20. //and obviously we have to move the object back up if we don't hit the ground
    21. obj.getBoundingShape().movePosition(0, -1);
    22.  
    23. return false;
    24. }

    What we do in this method, is make use of our getGroundTiles method from the first part of the tutorial and then check if we are directly above them.

    The way we check that is, is to move the bounding shape of the object down 1 pixel. Then we check if we have a collision. This is a simple way of checking if there is something below our object.

  5. Now comes the tough part, the actual movement of our objects in the level.

    The first thing we do is define two addiotional methods, "handleCharacters(Level level, int delta) and "handleGameObject(LevelObject obj,Level level, int delta).

    Lets take a look at the first method:

    1. private void handleCharacters(Level level, int delta){
    2. for(Character c : level.getCharacters()){
    3.  
    4. //and now decelerate the character if he is not moving anymore
    5. if(!c.isMoving()){
    6. c.decelerate(delta);
    7. }
    8.  
    9. handleGameObject(c,level,delta);
    10. }
    11. }

    The main reason why we have this method is because our characters have the ability to accelerate, and therefore have to decelerate if they are no longer attempting to accelerate.

    We also have to update our handlePhysics method to make use of our newly created method

    1.  
    2. public void handlePhysics(Level level, int delta){
    3. handleCharacters(level,delta);
    4. }
  6. As for now, we have to implement our handleGameObject method. But before we do lets just write down what it should be doing.

    1. Determine if our character is on the ground
    2. If we are not on the ground or if we are about to make a jump (meaning we have a y_velocity) we have to apply gravity
    3. Now we have to calculate how much we are moving, make using of the delta.
    4. And when we have calculated this, we will move our character alternating x and y and stopping when we hit something

    This last step is easier said than done, so lets see how it works.

    Lets say our character would want to move 50 pixels to the right in one frame (this could happen when we have a lower framerate). If we would just move our character and then check for collisions, he could end up walking through 32 pixel wide walls. So what we can do to remedy this is that we move him for 50 pixels, but only 1 pixel at a time. That way we will encounter any obstacles along the way and can stop right in front of them.

    Another thing we have to keep in mind is that we also have vertical movement, so we want to calculate the slope of the movement (y_movement divided by x_movement equals the slope). We also have to make sure that both the x_step and the y_step are a maximum of 1 pixel.

    This first illustration is the movement of an object by using the slope of 1 pixel right and 2 pixels up, which comes down to 0.5 pixels right and 1 pixel up (we only wanted 1 pixel to be our maximum step).

    This second illustration is how we can calculate the slope (basic linear algebra)

  7. So now that we know what we have to implement, here we go:

    1. private void handleGameObject(LevelObject obj, Level level, int delta){
    2.  
    3. //first update the onGround of the object
    4. obj.setOnGround(isOnGroud(obj,level.getTiles()));
    5.  
    6. //now apply gravitational force if we are not on the ground or when we are about to jump
    7. if(!obj.isOnGround() || obj.getYVelocity() < 0)
    8. obj.applyGravity(gravity*delta);
    9. else
    10. obj.setYVelocity(0);
    11.  
    12. //calculate how much we actually have to move
    13. float x_movement = obj.getXVelocity()*delta;
    14. float y_movement = obj.getYVelocity()*delta;
    15.  
    16. //we have to calculate the step we have to take
    17. float step_y = 0;
    18. float step_x = 0;
    19.  
    20. if(x_movement != 0){
    21. step_y = Math.abs(y_movement)/Math.abs(x_movement);
    22. if(y_movement < 0)
    23. step_y = -step_y;
    24.  
    25. if(x_movement > 0)
    26. step_x = 1;
    27. else
    28. step_x = -1;
    29.  
    30. if((step_y > 1 || step_y < -1) && step_y != 0){
    31. step_x = Math.abs(step_x)/Math.abs(step_y);
    32. if(x_movement < 0)
    33. step_x = -step_x;
    34. if(y_movement < 0)
    35. step_y = -1;
    36. else
    37. step_y = 1;
    38. }
    39. }else if(y_movement != 0){
    40. //if we only have vertical movement, we can just use a step of 1
    41. if(y_movement > 0)
    42. step_y = 1;
    43. else
    44. step_y = -1;
    45. }
    46.  
    47. //and then do little steps until we are done moving
    48. while(x_movement != 0 || y_movement != 0){
    49.  
    50. //we first move in the x direction
    51. if(x_movement != 0){
    52. //when we do a step, we have to update the amount we have to move after this
    53. if((x_movement > 0 && x_movement < step_x) || (x_movement > step_x && x_movement < 0)){
    54. step_x = x_movement;
    55. x_movement = 0;
    56. }else
    57. x_movement -= step_x;
    58.  
    59. //then we move the object one step
    60. obj.setX(obj.getX()+step_x);
    61.  
    62. //if we collide with any of the bounding shapes of the tiles we have to revert to our original position
    63. if(checkCollision(obj,level.getTiles())){
    64.  
    65. //undo our step, and set the velocity and amount we still have to move to 0, because we can't move in that direction
    66. obj.setX(obj.getX()-step_x);
    67. obj.setXVelocity(0);
    68. x_movement = 0;
    69. }
    70.  
    71. }
    72. //same thing for the vertical, or y movement
    73. if(y_movement != 0){
    74. if((y_movement > 0 && y_movement < step_y) || (y_movement > step_y && y_movement < 0)){
    75. step_y = y_movement;
    76. y_movement = 0;
    77. }else
    78. y_movement -= step_y;
    79.  
    80. obj.setY(obj.getY()+step_y);
    81.  
    82. if(checkCollision(obj,level.getTiles())){
    83. obj.setY(obj.getY()-step_y);
    84. obj.setYVelocity(0);
    85. y_movement = 0;
    86. break;
    87. }
    88. }
    89. }
    90. }

    The calculation of the x_step and y_step were a bit more complicated then I illustrated, this is because we can have both positive and negative movement and both have to be capped to a maximum of 1 (either positive or negative).

And there we go, we now have collision detection, basic gravity and jumping in our game!

Closing Notes

Again I hope I was clear in my explanation, so if you have any questions, don't hesitate and ask away.

One thing you could try to understand this a bit better is to grab a piece of paper with a grid on it, then draw some kind of object on the grid (being your obstacle). Then grab a small piece of paper and try to move it around the way we do in the code, it might clear things up.
Categories: Game Development, Java, Tutorial

Comments

jL said: (06-11-2012)
Thanks for the tuts! Took a minute, but I got this one. Keep 'em comin'!
QuantumScience said: (09-11-2012)
I've read a bunch of stuff about collision detection but this one made me understood everything. Thank you!
mikey said: (02-12-2012)
my character is stuck running in place as noted in step 4 after following this as precisely as i could. compiles and runs fine however. what are some common errors?
Frums said: (02-12-2012)
This is because we broke that on purpose, we want to have the controls manipulate the velocity of the player, and then have our physics object do the actual moving of the objects in our world (including our player).

If you implement the physics object as well you will see that it works again :)
Kyle Herrid said: (14-12-2013)
I have been avidly following these tutorials, but both a friend and I have encountered the following problem in this chapter:

After (what should be) completing this and the previous chapters, I get a strange error that appears to stem from calling physics.handlePhysics(level, delta) in the update function of game.state.LevelState.

the console spits out the following message:
"INFO:Slick Build #237
INFO:LWJGL Version: 2.9.0
INFO:OriginalDisplayMode: 1600 x 900 x 32 @60Hz
INFO:TargetDisplayMode: 1280 x 720 x 0 @0Hz
INFO:Starting display 1280x720
INFO:Use Java PNG Loader = true
INFO:Controllers not available
ERROR:null
java.lang.NullPointerException
at state.LevelState.update(LevelState.java:47)
at org.newdawn.slick.state.StateBasedGame.update(StateBasedGame.java:266)
at org.newdawn.slick.GameContainer.updateAndRender(GameContainer.java:663)
at org.newdawn.slick.AppGameContainer.gameLoop(AppGameContainer.java:411)
at org.newdawn.slick.AppGameContainer.start(AppGameContainer.java:321)
at main.Game.main(Game.java:28)
Sat Dec 14 20:21:32 PST 2013 ERROR:Game.update() failure - check the game code.
org.newdawn.slick.SlickException: Game.update() failure - check the game code.
at org.newdawn.slick.GameContainer.updateAndRender(GameContainer.java:669)
at org.newdawn.slick.AppGameContainer.gameLoop(AppGameContainer.java:411)
at org.newdawn.slick.AppGameContainer.start(AppGameContainer.java:321)
at main.Game.main(Game.java:28)"

Even after completely replacing every file in the physics package with the files in the following tutorial, it does not get any better.

Any help?
Brian said: (25-02-2014)
I'm running into the same error as Kyle.

P.S. Im lovin' the tutorials.
Brian said: (26-02-2014)
Got it! I forgot to call the constructor for Physics when I added the attribute to the LevelState class.
I had:
'Physics physics;'
rather than
'Physics physics = new Physics();'
Alex said: (24-11-2014)
Mine threw an error, and it seems like the Tile object doesn't properly return anything for the getBoundingShape() method. What should be put there?
Mike said: (30-03-2015)
Mine does not move left and right until I jump, This was fixed by adding the character to the Level at one pixel higher(415 instead of 416).
MasterSnipes said: (20-06-2015)
I can't seem to be moving after all of the code in this tutorial was put in. I can't do anything and even Mike's solution didn't work. Please help! Also, i'm using 70x70 tiles and i have changed things accordingly



What is the name of the website? (to counter the spam)