Categories

Thursday, January 10, 2013

Android games with LIBGDX (4) - gravity

In this part we will deal with jumping, falling and terrain interaction. In the end there's a small bonus!


Hi everybody! I've finally got the time to add Bob's jumping ability and also to restructure some parts of the code.

Also notice that I've changed the title of this post series since it was way too long.

If you want to download the code first to give it a try, go to the bottom of the page.

So let's start!


If you've read part 2 of the series then you already know how we're going to implement movement on the Y axis to simulate the effects of gravity. It's very similar to movement on the X axis, with one important exception: the velocity on the Y axis will vary in time!

More specifically, immediately after jumping, the velocity (actually its Y component, which is currently pointed upwards) will decrease in value. After reaching the highest point, it will start increasing back, but this time pointing to the ground and bringing poor Bob helplessly down.

Mathematically this can be implemented very easily by taking into account that velocity has a sign: when it is positive, it means Bob is moving up, and when it's negative, it means Bob is falling down. The absolute value of the velocity tells us how fast he is moving (be it up or down).


Things can get a little tricky when implementing this feature because there is more than one case to analyze.

Firstly, we have jumping. When you press the "jump" button (in our case, touch the upper half of the screen), we need to "push" Bob upwards by instantly setting his velocity on the Y axis equal to a constant value (the Bob.JUMP_VELOCITY constant). After that, the movement will be processed in the update() method just like the movement on the X axis (in that case, the trigger was pressing LEFT or RIGHT which was simply setting Bob's velocity on X). At the end, when Bob touches the ground (and we need to detect that), he needs to stop and set his Y velocity to be 0, but leave his X velocity unaffected.

Secondly, we have falling. By that I mean when you move Bob horizontally above a hole in the ground: suddenly there's nothing under his feet and just like the bad coyote in the cartoons he'll start going down. So we need to detect whenever there's nothing under his feet and start moving him down without any input from user.

And lastly, we need to consider that, when jumping, Bob might hit his head into a wall above him if he jumps too high.


If it seems complicated, don't worry! It's our daily job as programmers to implement complicated things in simple manners.

Fortunately, there is an easy way to do all those 3 things in just one, by implementing a very general movement computation inside the update() method.

Let me show you the code first, but not before mentioning a very important aspect: in order to simplify our job, we will deal separately with the movement on the X axis first, and then on the Y axis.


 public void update(float delta) {
  // X axis
  position.x += velocity.x * delta;
  updateBounds();
  float newX = position.x;
  boolean ranIntoBlocks = false;
  for (Block b : world.getBlocks()) {
   if (Intersector.overlapRectangles(bounds, b.getBounds())) {
    ranIntoBlocks = true;
   }
  }
  if ( newX< 0 || newX > 10 - bounds.getWidth() || ranIntoBlocks) {
   position.x -= velocity.x * delta; //undo the changes
   updateBounds();
  }
  
  // Y axis
  position.y += velocity.y * delta;
  updateBounds();
  float newY = position.y;
  boolean touchedGround = false;
  for (Block b : world.getBlocks()) {
   if (Intersector.overlapRectangles(bounds, b.getBounds())) {
    touchedGround = true;
   }
  }
  if ( newY< 0 || newY > 7 - bounds.getHeight() || touchedGround) {
   position.y -= velocity.y * delta; //undo the changes
   updateBounds();
   inAir = false;
   velocity.y = 0;
  }
  else {
   velocity.y += acceleration.y * delta;
   inAir = true;
  } 
 }


The first half (X axis) is easy and you already know it, only this time we work directly with the X component of the vectors. We move Bob horizontally and then we check to make sure he didn't bash his face into a wall or got off the screen. If so, we quickly move him back in order to fool the user into thinking Bob has stopped in time.


After correctly dealing with X-axis movement, we move on to the Y-axis. Notice here the importance of axis treatment separation: no matter what happens next on the Y-axis, the X axis movement is processed correctly according to which button (LEFT/RIGHT or none) was pressed. If Bob runs into a wall, he is pulled back, but only on this direction! He may continue falling if there's nothing under his feet.

On the second part, we update the Y position just as before and then check for collision (or running out of the screen, which will be changed in the future if we want to let Bob fall into abyss). If the collision exists, we put Bob back to his previous position and stop him there, making his Y-velocity 0. What happened is most likely that he hit the ground (thus the name of our variable). I introduced a new private member in the Bob class, the boolean inAir, which is true whenever Bob is not on the ground. In this case, inAir becomes false because we now assume Bob has completed his fall.


However, if there is no collision, it means Bob is still falling (inAir remains true, the purpose of the explicit assignment will be seen soon enough) and we need to modify his Y-velocity using the value of the Y-acceleration. Don't be fooled by the += sign. The Y-acceleration is equal to World.GRAVITY which is negative (pointed downwards, contrary to the Y axis which points upwards), so the speed decreases in algebraic way.


You can once again see why we separated the two axis here. Let's suppose we didn't, and we wrote something like position += velocity.mult(delta) which had the same effect as our code, on both components (X and Y) simultaneously. There's no problem so far, but when we check for collision, we will not know the exact cause of the collision: is it because Bob hit his nose in a wall or did he just touch the ground with his feet while falling? If you say it doesn't matter, you're wrong. Let's say it's a vertical wall to the right. Then after detecting the collision we move Bob back with position += velocity.mult(-delta) and if Bob has touched the wall while falling then while correcting the position not only do we move him back to the left but also back up!



Now check the input processing method and you'll see that when JUMP is pressed all we do is instantly increasing his Y-velocity to Bob.JUMP_VELOCITY which causes him to start going up immediately. 

 private void processInput() {
  if (keys.get(Keys.JUMP) && (!bob.isInAir())) {
   // jump is pressed and it's not currently jumping
   bob.getVelocity().y = Bob.JUMP_VELOCITY;
  }
  if (keys.get(Keys.LEFT)) {
   // left is pressed
   bob.setFacingLeft(true);
   bob.getVelocity().x = -Bob.getSpeed();
  }
  if (keys.get(Keys.RIGHT)) {
   // right is pressed
   bob.setFacingLeft(false);
   bob.getVelocity().x = Bob.getSpeed();
  }
  // need to check if both or none direction are pressed, then Bob is idle
  if ((keys.get(Keys.LEFT) && keys.get(Keys.RIGHT)) ||
    (!keys.get(Keys.LEFT) && !(keys.get(Keys.RIGHT)))) {
   // horizontal speed is 0
   bob.getVelocity().x = 0;
  }
 }



I said in the beginning that, besides jumping, there are two other cases we need to treat.

More work to do? Well, no! It's already done! Our code will correctly deal with stepping into a ground hole and with bumping Bob's head in a wall above. The former is treated because the Y-movement is done continuously (Bob always tries to fall but detecting the ground puts him immediately back, so when he's above a hole he just keeps falling and a succesful fall causes the inAir variable to become true!), while the latter is treated as normal collision which resets the Y-velocity to 0 (that would not be true if Bob's head would be as elastic as a ball, but let's say he's human).

I also modified the gravity and Bob's jumping speed so that you can play with him everywhere in our little world and check all collisions. You can play with the values and have fun.


If you already ran the program then you've notice a small added bonus: Bob actually turns left and right while moving (one of his hands grew bigger to point forwards). That's done very simply here by drawing the picture right to left. Find the code that does that as a small exercise for you!


You will notice through the code that I've done a little refactoring. Among others, I've deleted the states since I'm pretty sure they won't be necessary.

In the future we'll have to deal with an annoying bug: if you touch the screen in a region and release it in another, the code won't register a release in the original zone and that "button" will get stuck. Try not to do that for now, please. 

Next part will probably deal with animation, I'm not sure right now.

The complete source code is here: Star Assault source code

Have fun!

43 comments:

  1. Thank you again!
    Found it! Was in the worldRenderer: "spriteBatch.draw(bobTexture, bob.getPosition().x * ppuX + Bob.getSize() * ppuX, bob.getPosition().y * ppuY, -Bob.getSize() * ppuX, Bob.getSize() * ppuY);" ;)

    ReplyDelete
    Replies
    1. Exactly! No need to upload 2 pictures for left and right if you want them to be mirrored anyway.

      Delete
  2. Nice! Mind I suggest though that if the world has a lot of blocks, it will definitely lag when you iterate through every single block like that...twice.

    A better option would be to use two for loops: one for x, and one for y, for blocks that are within a certain range of you. You can leave that for a different tutorial: optimization though.

    ReplyDelete
    Replies
    1. Hi Source! Thanks for all the suggestions, but keep in mind that this is a tutorial intended to teach some basic stuff. The two iterations is just one of many things that need to be optimized. That will be done on the go so we shouldn't worry about that as long as the program is not yet big enough to even approach lagging.
      Thanks and keep in touch!

      Delete
  3. Also, where it says: position.add(velocity.tmp().mul(-delta));

    it should read: position.x -= velocity.x * delta;

    ReplyDelete
    Replies
    1. Already done that although its the same thing :-) you.re probably reading an older version

      Delete
  4. Also, if you're bonking your head against a block, it thinks you're on the ground.

    ReplyDelete
    Replies
    1. That's exactly what I intended and I explained why in the text :-)

      Delete
  5. if i hit a block above and keep the jump key pressed the player hovers in the air

    ReplyDelete
    Replies
    1. Hi Dee, thanks for discovering this small bug. It.s because when you hit the block above there is the smallest fraction of a second during which the inAir variable is false. That allows the input processor to issue the jump command right before the update method detects the inAir condition again.
      It will be fixed easily. Thanks

      Delete
    2. You know, I've been thinking about this: this accidental bug could actually be turned into a feature! We'll consider that if you keep jump pressed then Bob grabs the wall and holds himself with his hands. This would allow some cool movement in some levels where the only way to pass over a big hole would be just like this.

      Delete
    3. I guess it could, thanks for the tutorials btw. Maybe next you could do shooting enemies or loading different levels using a map tile editor since now the world is hard coded. Im new to libgdx starting learning this week so im just trying to soak up different stuff on it atm until im in a position to write a game.

      Delete
    4. Hey Dee,

      just stumbled over your comment. Here's how I solved the bug in case you don't want this feature ;)
      Just check if the player is moving upwards or downwards in the part of the update method handling y-axis movement. If the player moves upwards but hits an obstacle the player's still inAir, if the player moves downwards and hits something it's most likely the ground so inAir=false.
      Hope that helps.

      Also thanks for writing these tutorials! Keep up the work!

      Delete
  6. Thank you for the continuation of the tutorial,
    but you should add the code which was changed in World.java
    I tried to implement it myself, but bob didn't wan't to fall or jump.

    After looking around for a while i found that i have to set GRAVITY and allocate it to bobs y speed at the creation of the world.

    Which means adding

    public static final float GRAVITY = -4f;
    to World.java


    and


    bob.getAcceleration().y = GRAVITY;
    into createDemoWorld() in World.java


    you might want to add this to your tutorial for alle the people who don't want do download the source code again and do the changes by hand ;)



    Also i have a little request for you,
    would it be possible to make a short tutorial for a following camera?
    I tried what i can do and i have an idea, but my low java skills didn't help.

    (My idea was to include an UpdateCam() into the update function which gives the x & y positions of bob over a setter to the camera. It does not work because one is static and one is not.
    I hope i can figure it out myself though.) ;)

    All in all, GREAT TUTORIAL! Thank you very much!!

    ReplyDelete
    Replies
    1. Hi Rick!

      Thanks for all the suggestions! Usually I post all the modifications, but this time I did some refactoring here and there and it was difficult to keep track of everything, that's why I counted on people downloading the source code and see all the differences for themselves.

      After all, that the best way to learn it: read every single class and understand every single line of code!

      In the future I'll try as much as possible to post ALL the modification from previous versions.

      Right now please download the current source code to keep track with me further on.

      About the following camera. That's coming up very soon. I intend to do it inside the WorldRenderer where I will check whether Bob is about to go away from the screen (I'll use some limits left/right up to which he can go before moving the camera) and depending on those conditions I'll update camera's position.

      Cheers!

      Delete
  7. Hi Bogdan,

    Thanks a lot for this new Part!
    I think one line of code is missing to make it works. Indeed, we should set the acceleration.Y equal to World.GRAVITY, otherwise Bob can't fall at all.

    For example, in Bob's constructor:

    public Bob(Vector2 position) {
    this.position = position;
    this.bounds.height = getSize();
    this.bounds.width = getSize();
    this.acceleration.y = World.GRAVITY;
    updateBounds();
    }

    ReplyDelete
    Replies
    1. Hi Frederic,

      Unfortunately I have not posted all the code's modification here. That's why I must ask you to download the full source code from the link at the end of the post.

      Rick said the same thing previously, it's my mistake and in the future I'll be posting absolutely all the modifications I do.

      Sorry for the inconvenience once again. I'm new to blogging and I keep learning on that :)

      Delete
  8. I have one more problem.
    I loaded your project into eclipse to be on the same state as you. Now when i want to use star-assault-desktop (which wasn't included in the code.rar) it crashes with the warning: Exception in thread "LWJGL Application" com.badlogic.gdx.utils.GdxRuntimeException: Couldn't load file: bob_01.png"

    Do i have to link the assets folder in some other way?

    When starting the star-assault-android on the emulator or on my phone, the app immediatly crashes after pressing the "begin game" button.

    Here is my StarAssaultDesktop.java
    http://nopaste.info/ad60e69dfb.html

    Thanks!

    ReplyDelete
    Replies
    1. It's crashing because it doesnt find the resources. It's a leftover from how the project was created. The resource files (Bob's and block's pictures) are in the star-assault-desktop folder. So after unpacking my two folders from the archive, copy/paste your own star-assault-desktop folder near mine and it will work both on phone and desktop.

      Good thing you pointed that out, I'll move the resources in the main folder in the future.

      Delete
    2. I did exactly that, but it is not working. All 3 star-assault folders are direclty in my workspace.
      I thought all the assets have to be in the android folder for creating the app... Isn't that right?

      Delete
    3. I don't know what is your folder structure. I've double checked now and it appears that in my project the pictures are in the assets folder in the star-assault-android folder. The assets from the star-assault-desktop folder is just a link to the first one.

      So this means that if you extracted my folders it should work for you. It works for me in both phone and desktop.

      MAKE SURE that the assets folder from the star-assault-desktop folder is a link to the assets folder from the star-assault-android folder. If that's done then it must work on desktop.

      Why it crashes on your phone I don't know. You need to copy/paste me the LogCat error messages.

      Delete
    4. I re-linked the assets folder in star-assault-desktop to the asset folder in star-assault-android using "new folder" and the link feature of eclipse. It still gives me the same error as before.

      Delete
    5. Ok, i found out how to link it in the original tutorial part 1.
      Now the error is gone, but i keep getting another one when i start the desktop version.
      It shows the game window for a brief moment then it crashes and i get these errors:

      http://pastebin.com/GcKTJdan

      Thanks in advance ;)

      Delete
    6. So the phone version is working fine? If so, it's all good since I stated in the beginning of the tutorial that I am interested only in the Android application.

      However, since you are clearly also interested in the Desktop version (and since it should work anyway as it's using the same sources and resources) I'll try to give you a hand. I'll look over the code tonight to see why this error could appear to you.

      At first glance, I am pretty much sure there's something wrong with your folder structure. Have you rebuild the project from scratch?
      Note that your exception is actually an error. This means that the method specified was there in the code during compilation and everything was fine until runtime when sudenly the method could not be found. The referenced InputProcessor class is different from what you actually have in the code.

      My guess is you need to download the latest version of the LWJGL library again.

      This evening I'll upload the entire project with all the links and libs (all 3 folders) to start from 0.

      Delete
    7. Ok, thats nice, because the phone version isn't working either ^^

      Delete
    8. I modified the link at the end of the post. Download again and import the project into Eclipse from scratch. Let me know how it works.

      Delete
    9. Unfortunately it did not, i am still getting the same error as before, i even downloaded libgdx again and linked the libs all new, but that did not work either...

      Delete
    10. Rick, if I were you I would download the libgdx setup tool, its on aurielon ribbons blog (sp?) It will create your folders and downloads the source for you, etc. Create a project with it, run the project it should show a background w/ libgdx info. If it doesn't you need to uninstall eclipse and set it back up, as well as the android sdk. Also make sure you're using java 1.6 not 1.7 heard that screws stuff up before. If the proj you make with libgdx does work, then you can put the star assault packages/classes into it and tie them to the main class of the project.

      Delete
    11. I did not suceed with the setup tool... Creating and running the empty project works, but when i try to use it with the data from star-assault it shows the same error again -.-

      But i saw that the "star-assault-desktop" folder in the .rar Bodgan put up is named "starT-assault-desktop"... Can you look into that again?

      Thanks

      Delete
    12. Haha that's a misspell dating back months ago (when I created the project in Eclipse). It shouldn't matter anyway.

      Did you do just this:
      1.Download my .rar
      2.Extract it to a folder (let's call it BOGDAN) somewhere on the disk (NOT in the Eclipse workspace)
      3.Start Eclipse, close all projects
      4.File -> Import ---> Existing Projects into workspace
      5.Browse to the folder BOGDAN/star-assault; the project inside will be automatically detected. CHECK the "Copy projects into workspace" before clicking Finish! It's important
      6.Do the same for BOGDAN/starT-assault-desktop.
      7.File-> Import -> Existing Android projects into workspace
      8.Same thing as above in the folder BOGDAN/star-assault-android
      9.Run the app

      If you did EXACTLY that and it's still not working, all I can think about is a problem in library version or maybe java version. Or maybe even OS (I have Win7 x64) who knows.

      Delete
    13. I came across this thread while searching for an answer to the same resources not found error. I was originally attempting to run the project from the zip folder located at the bottom of this tutorial page:

      http://obviam.net/index.php/getting-started-in-android-game-development-with-libgdx-create-a-working-prototype-in-a-day-tutorial-part-1/

      But in that version not only are the desktop resources not found, the android version doesn't work either because it is depending on the gen folder which is not included in the zip for some reason. I found this page and download your rar which does have the gen folder and now the android works. I am posting because for me the solution to the desktop version not working was to give full paths to the texture initialization instead of just the names of the pngs. Not sure why or what will happen if I ever wanted to make an executable but that's what got it to run for me.

      Delete
  9. Hey Bogdan,
    Nice tutorial! I was wondering if I could send you some of my code? I've got animations working for left and right walking, but when I do the left walk, the sprite disappears and reappears about 20 pixels to the left. This is because of the width of the sprite and the width of my player class (bob). Do you know how to fix this?
    Also, I've set up a boolean for whether or not the player is changing direction (so I can show a turning animation) I can't determine what to do for this test, I tried to make an if statement that included the isFacingRight boolean, but don't know how to get it to check for a negative deceleration just after the player is facing right?

    ReplyDelete
    Replies
    1. Hi,
      First thing should be easy to fix. Implementing a turning animation is not hard but it requires some work.

      Send me the full project so I can load it and see what modifications need to be made.

      Delete
    2. Bogdan,
      How would i send you the project? via google plus? I don't use it much at all so I'm not exactly sure

      Delete
    3. Pack it up with WinRar and send it to my email: bogdan.alexandru.314@gmail.com

      Delete
    4. I imported your project into Eclipse, but it has some errors and it crashes immediately as it starts. Haven't had the time to debug it yet (I'm taking some exams for my master's degree these days) but I'll look into it this weekend.

      Delete
  10. Many thanks for this. I am following your tutorials with great interest. Keep up the good work !

    ReplyDelete
  11. It doesn't seem to resize properly. I think it is automatically accounted for somehow and the setSize method in WorldRenderer double counts the resize.

    ReplyDelete
    Replies
    1. Did you download my source code? If you did there should be nothing wrong.

      If you're still on the old code from obviam.net I remember there were some problems I have fixed, problems mainly connected to coordinate miscalculations. Also in my code the view is set for horizontal only.
      Or are you referring to the dekstop version?

      Delete
    2. I was referring to the desktop version, although I fixed the problem myself by calling:

      spriteBatch.setProjectionMatrix(cam.combined);

      then removing the ppuX/ppuY in the draw calls, as they now work in the cams units rather than pixels.

      Delete
    3. Good. In this case it's usually best to use cam units and let it do all the calculations for you.

      Delete