Previous Tutorial
 
Posted on: 21-11-2012

Gravity Platformer Tutorial #10 : Switching Gravity

The zip with the source for this tutorial can be downloaded here


Switching gravity is the most important thing of this game, the whole game surrounds it and the puzzles will make use of it as well. It is also one of the "risks" of the game. This meaning that if we can't get this implemented and working properly, the game won't be playable.

One of the most difficult things that comes to this is that everything in the world (with exceptions possible) will be affected by the change of gravity. And there will be four states of gravity instead of the usual one.

Whenever we render something, we have to see what the gravity is to render the characters correctly. And then there is also the problem of objects moving around when we switch the gravity around.

There may be many solutions to these problems, but what I've decided to do is make gravity an argument of everything that has something to do with it. Whether it is the deceleration of objects, the falling of them, moving of characters, rendering and even jumping all have the gravity as an argument.

This comes down to a lot of changes in different sections of the code to reflect the gravity having an effect.

One of the things that we are going to see in this tutorial is that abstraction can be a real life safer. Some classes like Character and LevelObject will get some code that you might consider to be "ugly" code. But we don't build this game for good looking code, we build it for the game itself and we don't want to spend a lot of time on things we can always refactor.

Another way we see that our abstraction helps is that classes like Player and Objective class don't have any changes at all! But keep in mind, only use abstraction when you need it, no game can do everything and no game needs to! Its all about creating a balance, generally you just want to avoid double code.


But enough with the wall of text, lets check out the important changes.

  1. One of the first things I did is create an attribute for the current gravity situation, this is situated in our Level class. I also made this gravity an enumeration with the following values: UP,DOWN,LEFT,RIGHT.

  2. Next up I started adding the gravity in different spots, first being the applyGravity method in LevelObject:

    1. public void applyGravity(float force, Gravity gravity){
    2. switch(gravity){
    3. case DOWN:
    4. //if we aren't already moving at maximum speed
    5. if(y_velocity < maximumFallSpeed){
    6. //accelerate
    7. y_velocity += force;
    8. if(y_velocity > maximumFallSpeed){
    9. //and if we exceed maximum speed, set it to maximum speed
    10. y_velocity = maximumFallSpeed;
    11. }
    12. }
    13. break;
    14. case UP:
    15. if(y_velocity > -maximumFallSpeed){
    16. //accelerate
    17. y_velocity -= force;
    18. if(y_velocity < -maximumFallSpeed){
    19. //and if we exceed maximum speed, set it to maximum speed
    20. y_velocity = -maximumFallSpeed;
    21. }
    22. }
    23. break;
    24. case RIGHT:
    25. if(x_velocity < maximumFallSpeed){
    26. //accelerate
    27. x_velocity += force;
    28. if(x_velocity > maximumFallSpeed){
    29. //and if we exceed maximum speed, set it to maximum speed
    30. x_velocity = maximumFallSpeed;
    31. }
    32. }
    33. break;
    34. case LEFT:
    35. if(x_velocity > -maximumFallSpeed){
    36. //accelerate
    37. x_velocity -= force;
    38. if(x_velocity < -maximumFallSpeed){
    39. //and if we exceed maximum speed, set it to maximum speed
    40. x_velocity = -maximumFallSpeed;
    41. }
    42. }
    43. break;
    44. }
    45. }

    As you can see, there are four different cases here, but they are all fairly similar.

    The LEFT and RIGHT cases are manipulating the horizontal movement of the character and are opposites. The same goes for UP and DOWN but they manipulate the vertical movement of the character.

  3. Changing just this little thing does not make the gravity work yet, remember that we only apply gravity if we are not on the ground.
    Remember the method we made that checked the ground collission? We have to alter that one too!

    1. private boolean isOnGroud(LevelObject obj, Tile[][] mapTiles, Gravity gravity){
    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, gravity);
    4.  
    5. int xMovement = 0;
    6. int yMovement = 0;
    7. //determine what the ground actually is
    8. switch(gravity){
    9. case DOWN:
    10. yMovement = 1;
    11. break;
    12. case UP:
    13. yMovement = -1;
    14. break;
    15. case LEFT:
    16. xMovement = -1;
    17. break;
    18. case RIGHT:
    19. xMovement = 1;
    20. break;
    21. }
    22.  
    23. //we lower the the bounding object a bit so we can check if we are actually a bit above the ground
    24. obj.getBoundingShape().movePosition(xMovement, yMovement);
    25.  
    26. for(Tile t : tiles){
    27. //not every tile has a bounding shape (air tiles for example)
    28. if(t.getBoundingShape() != null){
    29. //if the ground and the lowered object collide, then we are on the ground
    30. if(t.getBoundingShape().checkCollision(obj.getBoundingShape())){
    31. //don't forget to move the object back up even if we are on the ground!
    32. obj.getBoundingShape().movePosition(-xMovement, -yMovement);
    33. return true;
    34. }
    35. }
    36. }
    37.  
    38. //and obviously we have to move the object back up if we don't hit the ground
    39. obj.getBoundingShape().movePosition(-xMovement, -yMovement);
    40.  
    41. return false;
    42. }

    What we see here is that instead of moving the character down one pixel to check if he is on the ground, we have to move him 1 pixel in the direction on whatever side represents the "ground". So if we say that the ceiling is the ground, we actually have to move the character up by one pixel to check if he is on "the ground".

    We also have to edit the method getGroundTiles() to include the gravity, this method gave us the tiles that were below our character, but this "below" our character has become more dynamic.

    1. public ArrayList<Tile> getGroundTiles(Tile[][] tiles, Gravity gravity) {
    2.  
    3. ArrayList<Tile> tilesUnderneath = new ArrayList<Tile>();
    4.  
    5. int i = 0;
    6. int j = 0;
    7.  
    8. //because of different gravity cases, we can have different "ground" tiles
    9. switch(gravity){
    10. case DOWN:
    11. j = (int) (y+height+1);
    12. for(i = (int) x; i <= x+width+(32-width%32); i+=32){
    13. tilesUnderneath.add(tiles[i/32][j/32]);
    14. }
    15. break;
    16. case UP:
    17. j = (int) (y-1);
    18. for(i = (int) x; i <= x+width+(32-width%32); i+=32){
    19. tilesUnderneath.add(tiles[i/32][j/32]);
    20. }
    21. break;
    22. case RIGHT:
    23. i = (int) (x+width+1);
    24. for(j = (int) y; j <= y+height+(32-height%32); j+=32){
    25. tilesUnderneath.add(tiles[i/32][j/32]);
    26. }
    27. break;
    28. case LEFT:
    29. i = (int) (x-1);
    30. for(j = (int) y; j <= y+height+(32-height%32); j+=32){
    31. tilesUnderneath.add(tiles[i/32][j/32]);
    32. }
    33. break;
    34. }
    35.  
    36. return tilesUnderneath;
    37. }

    Again, we have the same code we had for our original in the DOWN case. The other beings the tiles one pixel above the character, or the tiles one pixel left or right.

  4. Before we look at any other methods, lets fix up the controls.

    1. public void update(GameContainer container, StateBasedGame sbg, int delta) throws SlickException {
    2.  
    3. //every update we have to handle the input from the player
    4. playerController.handleInput(container.getInput(), delta, level);
    5.  
    6. //we want to pass on the gravity here, and only here in case some levels decide to do things differently (like disabling the gravity device for example)
    7. physics.handlePhysics(level, delta, level.getCurrentGravity());
    8. }

    The first thing we have to do is pass on the gravity from the level, and for the controls we need to pass on the level itself as we need to be able to change the gravity.

    1. private void handleKeyboardInput(Input i, int delta, Level level){
    2. //we can both use the arrow keys to move around, obviously we can't move both left and right simultaneously
    3. switch(level.getCurrentGravity()){
    4. case UP: case DOWN:
    5. if(i.isKeyDown(Input.KEY_LEFT)){
    6. player.moveLeft(delta,level.getCurrentGravity());
    7. }else if(i.isKeyDown(Input.KEY_RIGHT)){
    8. player.moveRight(delta,level.getCurrentGravity());
    9. }else{
    10. //we dont move if we don't press left or right, this will have the effect that our player decelerates
    11. player.setMoving(false);
    12. }
    13. break;
    14. case LEFT: case RIGHT:
    15. if(i.isKeyDown(Input.KEY_UP)){
    16. player.moveLeft(delta,level.getCurrentGravity());
    17. }else if(i.isKeyDown(Input.KEY_DOWN)){
    18. player.moveRight(delta,level.getCurrentGravity());
    19. }else{
    20. //we dont move if we don't press left or right, this will have the effect that our player decelerates
    21. player.setMoving(false);
    22. }
    23. break;
    24. }
    25.  
    26.  
    27. if(i.isKeyPressed(Input.KEY_SPACE)){
    28. player.jump(level.getCurrentGravity());
    29. }
    30.  
    31. //switching the gravity device clockwise
    32. if(i.isKeyPressed(Input.KEY_Q)){
    33. //down becomes left, left becomes up, up becomes right and right becomes down
    34. switch(level.getCurrentGravity()){
    35. case DOWN:
    36. level.setGravity(Gravity.LEFT);
    37. break;
    38. case LEFT:
    39. level.setGravity(Gravity.UP);
    40. break;
    41. case UP:
    42. level.setGravity(Gravity.RIGHT);
    43. break;
    44. case RIGHT:
    45. level.setGravity(Gravity.DOWN);
    46. break;
    47. }
    48. }else if(i.isKeyPressed(Input.KEY_E)){
    49. //and anti clockwise
    50. //down becomes right, right becomes up, up becomes left and left becomes down
    51. switch(level.getCurrentGravity()){
    52. case DOWN:
    53. level.setGravity(Gravity.RIGHT);
    54. break;
    55. case RIGHT:
    56. level.setGravity(Gravity.UP);
    57. break;
    58. case UP:
    59. level.setGravity(Gravity.LEFT);
    60. break;
    61. case LEFT:
    62. level.setGravity(Gravity.DOWN);
    63. break;
    64. }
    65. }
    66.  
    67. }

    There are quite a few changes here, first of all jumping has to have knowledge of the gravity, else how would we know what direction to move our character to when we jump?

    More important is the switching of gravity. This is something that I actually did some research on and we will discuss this in greater detail in the next tutorial. For now it will suffice to know that the Q key changes the gravity clockwise and the E key changes the gravity counter clockwise.

    And last but not least, moving our character, another thing that I gave some thought. Whenever we are walking on a wall (gravity is LEFT or RIGHT), moving with the left and right arrow keys seemed a bit awkward, so what I decided to do is using the up and down arrow keys whenever we are walking on a wall. Notice again the gravity argument in moveRight and moveLeft.

  5. For objects in general we only have to do one main thing, decelerate them whenever they are moving. Again this method requires gravity as an argument:
    1. public void decelerate(int delta, Gravity gravity) {
    2. //if we are on the down or up state we have to decelerate horizontally, else vertically
    3. switch(gravity){
    4. case DOWN: case UP:
    5. if(x_velocity > 0){
    6. x_velocity -= decelerationSpeed * delta;
    7. if(x_velocity < 0)
    8. x_velocity = 0;
    9. }else if(x_velocity < 0){
    10. x_velocity += decelerationSpeed * delta;
    11. if(x_velocity > 0)
    12. x_velocity = 0;
    13. }
    14. break;
    15. case LEFT: case RIGHT:
    16. if(y_velocity > 0){
    17. y_velocity -= decelerationSpeed * delta;
    18. if(y_velocity < 0)
    19. y_velocity = 0;
    20. }else if(y_velocity < 0){
    21. y_velocity += decelerationSpeed * delta;
    22. if(y_velocity > 0)
    23. y_velocity = 0;
    24. }
    25. break;
    26. }
    27. }

    Simple and effective, the only new thing here is that LEFT and RIGHT will decelerate the vertical velocity, we already had the horizontal velocity deceleration.

    We also have to apply this in our physics class, the handleObjects method will look like this now:

    1. private void handleLevelObjects(Level level, int delta, Gravity gravity){
    2. for(LevelObject obj : level.getLevelObjects()){
    3.  
    4. //just decelerate objects in general
    5. obj.decelerate(delta,gravity);
    6.  
    7. handleGameObject(obj,level,delta,gravity);
    8. }
    9. }

Lets take a little break here and assess what we have accomplished up until here.

  • We can change our gravity
  • We apply the right gravity and detect whenever an object is on the "ground"
  • We decelerate the objects in a correct way
Whats missing now is the rendering and the changes in the Character class.
  1. Lets start off with the jump method:

    1. public void jump(Gravity gravity){
    2. if(onGround){
    3. switch(gravity){
    4. case DOWN:
    5. y_velocity = -0.4f;
    6. break;
    7. case UP:
    8. y_velocity = 0.4f;
    9. break;
    10. case LEFT:
    11. x_velocity = 0.4f;
    12. break;
    13. case RIGHT:
    14. x_velocity = -0.4f;
    15. break;
    16. }
    17. }
    18. }

    More of the same here.

  2. Now for the moving left and right:

    1. public void moveLeft(int delta, Gravity gravity){
    2. //if we aren't already moving at maximum speed
    3. switch(gravity){
    4. case UP: case DOWN:
    5. if(x_velocity > -maximumSpeed){
    6. //accelerate
    7. x_velocity -= accelerationSpeed*delta;
    8. if(x_velocity < -maximumSpeed){
    9. //and if we exceed maximum speed, set it to maximum speed
    10. x_velocity = -maximumSpeed;
    11. }
    12. }
    13. break;
    14. case LEFT: case RIGHT:
    15. if(y_velocity > -maximumSpeed){
    16. //accelerate
    17. y_velocity -= accelerationSpeed*delta;
    18. if(y_velocity < -maximumSpeed){
    19. //and if we exceed maximum speed, set it to maximum speed
    20. y_velocity = -maximumSpeed;
    21. }
    22. }
    23. break;
    24. }
    25. moving = true;
    26. facing = Facing.LEFT;
    27. }
    28.  

    Again we see that we can no longer only move horizontal, but also vertical.

  3. And thats all the moving objects, applying gravity when it changes and so forth.

    Now for the other thing we still had left, the rendering. This is actually quite tricky, I didn't want to create sprites for every facing and gravity, we didn't do that even for facing left!

    So what we first have to do is update our setSprite() method, and this one is quite the beast:

    1. protected void setSprite(Image i) throws SlickException{
    2. sprites = new HashMap<Facing,HashMap<Gravity,Image>>();
    3. sprites.put(Facing.LEFT, new HashMap<Gravity,Image>());
    4. sprites.put(Facing.RIGHT, new HashMap<Gravity,Image>());
    5.  
    6. Image i2;
    7.  
    8. sprites.get(Facing.RIGHT).put(Gravity.UP, i.getFlippedCopy(false, true));
    9. sprites.get(Facing.RIGHT).put(Gravity.DOWN, i);
    10. i2 = new Image(i.getResourceReference());
    11. i2.rotate(90);
    12. i2.draw(-50,50);
    13. sprites.get(Facing.RIGHT).put(Gravity.LEFT, i2);
    14. i2 = i.getFlippedCopy(false, true);
    15. i2.rotate(90);
    16. i2.draw(-50,50);
    17. sprites.get(Facing.RIGHT).put(Gravity.RIGHT, i2);
    18.  
    19. sprites.get(Facing.LEFT).put(Gravity.UP, i.getFlippedCopy(true, true));
    20. sprites.get(Facing.LEFT).put(Gravity.DOWN, i.getFlippedCopy(true, false));
    21.  
    22. i2 = i.getFlippedCopy(true, false);
    23. i2.rotate(90);
    24. i2.draw(-50,50);
    25. sprites.get(Facing.LEFT).put(Gravity.LEFT, i2);
    26. i2 = i.getFlippedCopy(true, true);
    27. i2.rotate(90);
    28. i2.draw(-50,50);
    29. sprites.get(Facing.LEFT).put(Gravity.RIGHT, i2);
    30. }

    Uggh, what is going on in here?

    One of the first things you might notice is that the map for sprites is no longer just a "HashMap" but now a "HashMap". This makes it incredibly easy to get the right image for rendering!

    But now for the mess below there, it really comes down to getting that one image, and rotating and flipping it so that we get all eight different ways.

    You are probably also wondering why I am calling the draw function in this method and drawing them outside the screen, this is because of some optimisation that is somewhere in Slick2D or LWJGL in that the rotation we apply only gets applied when it needed, and it caused some strange glitchy stuff to happen whenever we first render a specific image. So in essence it is to force the rotation to happen.

    In case you might be wondering why this happens, the way rotations happen is that the object is moved back to its origin and rotated around that spot and after that it is moved back towards it original position.

    The setMovingAnimation method works in the same way, but is even more messy because of the animation object not begin able to rotate.


And there we go, we can switch gravity in our game!

Closing notes

I hope you've learned something in this tutorial, my main goal in this tutorial was to learn you the concept of switching gravity and an example of how to implement it.

Next time we will look into solving a few problems that we have introduced in this tutorial, the fast switching of gravity allowing you to float around and the convergence of all objects in the same location.

Categories: Game Development, Java, Tutorial

Comments

jL said: (25-11-2012)
Oh, that was a lot! Probably going to have to re-read it a few times later. Again, thanks for the tutorials, you're doing an awesome job!
Sam said: (15-04-2013)
Fantastic work on this! it has been my most helpful source on slick and beginning game development
Nyhm said: (12-09-2013)
Great tutorial.
When will you finish it?
Also I downloaded the source, but its got a ton of errors to do with the Gravity class because its not there yet.
Please continue this.
tom said: (09-02-2014)
the best tutorial for starting game development that i have ever found on the internet.



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