In this point i would like to say a big thanks to William Mora for his excellent libGDX tutorials. You might see similarities in the box2D parts of the tutorials, because i adopted his style of developing with libGDX.
How to create a character and make him move using box2D and libGDX
After creating our ground and walls, we will create our character and after that we will make him move, jump and slide! In the " WorldMisc " class, we create our character, the same way we did with our ground and walls.
package com.getest.game.misc;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.physics.box2d.Body;
import com.badlogic.gdx.physics.box2d.BodyDef;
import com.badlogic.gdx.physics.box2d.FixtureDef;
import com.badlogic.gdx.physics.box2d.PolygonShape;
import com.badlogic.gdx.physics.box2d.World;
import com.getest.game.box2d.GroundUserData;
import com.getest.game.box2d.LeftWallUserData;
import com.getest.game.box2d.RightWallUserData;
public class WorldMisc {
//Rest of the code
public Body createHero(World world) {
BodyDef hero = new BodyDef();
hero.type = BodyDef.BodyType.DynamicBody;
hero.position.set(new Vector2(8F, 2F));
PolygonShape heroShape = new PolygonShape();
heroShape.setAsBox(0.4F, 0.8F);
Body body = world.createBody(hero);
FixtureDef heroFixture = new FixtureDef();
heroFixture.density = 0.5F;
heroFixture.shape = heroShape;
heroFixture.friction = 0.0F;
body.createFixture(heroFixture);
body.setGravityScale(5F);
body.resetMassData();
body.setUserData(new HeroUserData(0.8F, 1.4F));
heroShape.dispose();
return body;
}
}
The difference is that our character is a dynamic body and he is placed in the middle of the screen, just above the ground. I created the fixture a bit different to show you your options. For example, i have defined a zero friction and a gravity scale. The usage of the gravity scale is for the jump to look smoother and more realistic. You will see the result later, after we create the jumping movement.
Next step as always, is to define a HERO type in the " UserDataType " class;
package com.getest.game.enums;
public enum UserDataType {
GROUND,
LEFTWALL,
RIGHTWALL,
HERO;
private UserDataType() {
}
}
And finally in the " box2d " package, we must create a " HeroUserData " class with the below code;
package com.getest.game.box2d;
import com.getest.game.enums.UserDataType;
public class HeroUserData extends UserData {
public HeroUserData(float width, float height) {
super(width, height);
userDataType = UserDataType.HERO;
}
}
Don't forget to go back to the WorldMisc class and import the HeroUserData class that we have just created.
Finally, go to our " GameStage " class and call the function we have created, to actually create our character.
package com.getest.game.stages;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.physics.box2d.Body;
import com.badlogic.gdx.physics.box2d.Box2DDebugRenderer;
import com.badlogic.gdx.physics.box2d.World;
import com.badlogic.gdx.scenes.scene2d.Stage;
import com.badlogic.gdx.utils.viewport.ExtendViewport;
import com.getest.game.camera.AndroidCamera;
import com.getest.game.misc.WorldMisc;
public class GameStage extends Stage{
private float accumulator, TIME_STEP;
private Box2DDebugRenderer renderer;
private WorldMisc wrl;
private World world;
private Body ground,leftWall,rightWall,hero;
public GameStage(){
super(new ExtendViewport(16f, 9f, new AndroidCamera(16f, 9f)));
accumulator = 0.0F;
TIME_STEP = 1/300F; // Recomended by libgdx
setupWorld();
}
private void setupWorld() {
wrl = new WorldMisc();
world = wrl.createWorld();
renderer = new Box2DDebugRenderer();
setupGround();
setupLeftWall();
setupRightWall();
setupHero();
}
private void setupGround(){
ground = wrl.createGround(world);
}
private void setupLeftWall(){
leftWall = wrl.createLeftWall(world);
}
private void setupRightWall(){
rightWall = wrl.createRightWall(world);
}
private void setupHero() {
hero = wrl.createHero(world);
}
// Rest of the code
}
Until now, we haven't made anything new. We just followed the same procedure as we did in the How To 5 tutorial to create our ground and walls. Run the game and you should get this;
Box2D world with a character |
What do we have until now? We have made a game screen using scene2D, but because we wanted to manipulate the stage, we also created a game stage that extends scene2D's stage. In this stage we are using box2D to create our game world. To create the movement, we will also manipulate the scene2D's actors. Create an " actors " package and inside create a class with the name
" GameActors ".
package com.getest.game.actors;
import com.badlogic.gdx.math.Rectangle;
import com.badlogic.gdx.physics.box2d.Body
;import com.badlogic.gdx.scenes.scene2d.Actor;
import com.getest.game.box2d.UserData;
public abstract class GameActors extends Actor {
protected Body body;
protected UserData userData;
public GameActors(Body body) {
this.body = body;
this.userData = (UserData)body.getUserData();
}
public abstract UserData getUserData();
}
The next step is to create 4 classes with names " GroundActor ", " LeftWallActor ",
" RightWallActor " and " HeroActor " inside the " actors " package and place these parts of codes in each respectively;
package com.getest.game.actors;
import com.badlogic.gdx.physics.box2d.Body;
import com.getest.game.box2d.GroundUserData;
public class GroundActor extends GameActors {
public GroundActor(Body body) {
super(body);
}
public GroundUserData getUserData() {
return (GroundUserData) userData;
}
}
package com.getest.game.actors;
import com.badlogic.gdx.physics.box2d.Body;
import com.getest.game.box2d.LeftWallUserData;
public class LeftWallActor extends GameActors {
public LeftWallActor(Body body) {
super(body);
}
public LeftWallUserData getUserData() {
return (LeftWallUserData) userData;
}
}
package com.getest.game.actors;
import com.badlogic.gdx.physics.box2d.Body;
import com.getest.game.box2d.RightWallUserData;
public class RightWallActor extends GameActors {
public RightWallActor(Body body) {
super(body);
}
public RightWallUserData getUserData() {
return (RightWallUserData) userData;
}
}
package com.getest.game.actors;
import com.badlogic.gdx.physics.box2d.Body;
import com.getest.game.box2d.HeroUserData;
public class HeroActor extends GameActors {
public HeroActor(Body body) {
super(body);
}
public HeroUserData getUserData() {
return (HeroUserData)userData;
}
}
Once again, go back to our " GameStage ", where we must make some changes. Instead of just creating our game bodies, the ground, the walls and the hero, we will create the actors with these bodies and add them to the stage!
package com.getest.game.stages;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.physics.box2d.Box2DDebugRenderer;
import com.badlogic.gdx.physics.box2d.World;
import com.badlogic.gdx.scenes.scene2d.Stage;
import com.badlogic.gdx.utils.viewport.ExtendViewport;
import com.getest.game.actors.GroundActor;
import com.getest.game.actors.HeroActor;
import com.getest.game.actors.LeftWallActor
;import com.getest.game.actors.RightWallActor;
import com.getest.game.camera.AndroidCamera;
import com.getest.game.misc.WorldMisc;
public class GameStage extends Stage{
private float accumulator, TIME_STEP;
private Box2DDebugRenderer renderer;
private WorldMisc wrl;
private World world;
private GroundActor ground;
private LeftWallActor leftWall;
private RightWallActor rightWall;
private HeroActor hero;
public GameStage(){
super(new ExtendViewport(16f, 9f, new AndroidCamera(16f, 9f)));
accumulator = 0.0F;
TIME_STEP = 1/300F; // Recomended by libgdx
setupWorld();
}
private void setupWorld() {
wrl = new WorldMisc();
world = wrl.createWorld();
renderer = new Box2DDebugRenderer();
setupGround();
setupLeftWall();
setupRightWall();
setupHero();
}
private void setupGround(){
ground = new GroundActor(wrl.createGround(world));
addActor(ground);
}
private void setupLeftWall(){
leftWall = new LeftWallActor(wrl.createLeftWall(world));
addActor(leftWall);
}
private void setupRightWall(){
rightWall = new RightWallActor(wrl.createRightWall(world));
addActor(rightWall);
}
private void setupHero() {
hero = new HeroActor(wrl.createHero(world));
addActor(hero);
}
@Override
public void act(float delta) {
super.act(delta);
accumulator += delta;
while (accumulator >= delta) {
world.step(TIME_STEP, 8, 4);
accumulator -= TIME_STEP;
}
}
@Override
public void draw() {
super.draw();
renderer.render(world, getViewport().getCamera().combined);
}
}
Run the game! Nothing changed? Don't worry, this is just normal. All we did was to call the same functions but in another way, in order for us to implement the movement.
- For the jump, we have to apply an instantaneous force to our hero on the y - axis and then the gravity will do the rest. The gravity scale that we had set for our hero, changes the way gravity applies only to him and will make the jump look smoother. Instantaneous force basically means that we will apply an impulse which will be linear as well. We need to define this linear impulse.
- For the slide, we will just rotate our hero 90 degrees for a certain amount of time, thus we need to define the slide duration.
- And finally for the movement (we want to move with constant velocity), we will apply impulses as well (in every step of the simulation) but in way for the movement to be constant.
So, to begin with, we modify the " HeroUserData " class and we define the above variables, the getters and the setters.
package com.getest.game.box2d;
import com.badlogic.gdx.math.Vector2;
import com.getest.game.enums.UserDataType;
public class HeroUserData extends UserData {
private Vector2 jumpingImpulse;
private float slideDuration;
private float speedImpulse;
public HeroUserData(float width, float height) {
super(width, height);
jumpingImpulse = new Vector2(0,11F);
slideDuration = 1.5F;
speedImpulse = 5.0F;
userDataType = UserDataType.HERO;
}
public void setJumpingImpulse(Vector2 jumpingImpulse) {
this.jumpingImpulse = jumpingImpulse;
}
public void setSlideDuration(float slideDuration) {
this.slideDuration = slideDuration;
}
public void setSpeedImpulse(float SpeedImpulse) {
this.speedImpulse = speedImpulse;
}
public Vector2 getJumpingImpulse() {
return jumpingImpulse;
}
public float getSlideDuration() {
return slideDuration;
}
public float getSpeedImpulse() {
return speedImpulse;
}
}
Because this tutorial is getting too long, lets just implement the movement part and let the jump and the slide for the next one.
Go to the " HeroActor " java class where we must create the actual functions of the movement;
package com.getest.game.actors;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.physics.box2d.Body;
import com.getest.game.box2d.HeroUserData;
public class HeroActor extends GameActors {
private Vector2 velocityEachTime;
private Vector2 movingLinearImpulse;
private float velocityX;
private float finalVel;
public HeroActor(Body body) {
super(body);
}
public HeroUserData getUserData() {
return (HeroUserData) userData;
}
public void moveRight() {
velocityEachTime = body.getLinearVelocity();
velocityX = velocityEachTime.x;
finalVel = getUserData().getSpeedImpulse() - velocityX;
movingLinearImpulse = new Vector2(finalVel, 0.0F);
body.applyLinearImpulse(movingLinearImpulse, body.getWorldCenter(), true);
}
public void moveLeft() {
velocityEachTime = body.getLinearVelocity();
velocityX = velocityEachTime.x;
finalVel = -getUserData().getSpeedImpulse() - velocityX;
movingLinearImpulse = new Vector2(finalVel, 0.0F);
body.applyLinearImpulse(movingLinearImpulse, body.getWorldCenter(), true);
}
public void moveStop() {
velocityEachTime = body.getLinearVelocity();
velocityX = velocityEachTime.x;
finalVel = 0.0F - velocityX;
movingLinearImpulse = new Vector2(finalVel, 0.0F);
body.applyLinearImpulse(movingLinearImpulse, body.getWorldCenter(), true);
}
}
This will do the job. What we have to do next, is to create 2 buttons for the movement. Download the packed version of all the buttons we are gonna need. It would be better to pack them yourself (you know how to do it if you have followed all the tutorials until now).
Note;; Don't forget to first click on the image and then download it, to get the original size.
Create the .atlas file by just creating a .txt file, pasting the code below and saving the file as
" inGameButtons.atlas ".
inGameButtons.png
size: 2048,512
format: RGBA8888
filter: Linear,Linear
repeat: none
jumpNorm
rotate: false
xy: 192, 2
size: 188, 398
orig: 188, 398
offset: 0, 0
index: -1
jumpPushed
rotate: false
xy: 2, 2
size: 188, 398
orig: 188, 398
offset: 0, 0
index: -1
leftNorm
rotate: false
xy: 1328, 3
size: 186, 397
orig: 186, 397
offset: 0, 0
index: -1
leftPushed
rotate: false
xy: 572, 2
size: 187, 398
orig: 187, 398
offset: 0, 0
index: -1
rightNorm
rotate: false
xy: 950, 2
size: 187, 398
orig: 187, 398
offset: 0, 0
index: -1
rightPushed
rotate: false
xy: 761, 2
size: 187, 398
orig: 187, 398
offset: 0, 0
index: -1
slideNorm
rotate: false
xy: 1139, 3
size: 187, 397
orig: 187, 397
offset: 0, 0
index: -1
slidePushed
rotate: false
xy: 382, 2
size: 188, 398
orig: 188, 398
offset: 0, 0
index: -1
Copy both files (the .png and the .atlas) inside the " skins " folder which is located in our " assets " folder, together with 2 .json files with names " leftButton.json " and " rightButton.json " with the below codes respectively;
{ "com.badlogic.gdx.scenes.scene2d.ui.ImageButton$ImageButtonStyle": { "default": { "up": "leftNorm", "down": "leftPushed", "unpressedOffsetY":-10 } } }
and
{
"com.badlogic.gdx.scenes.scene2d.ui.ImageButton$ImageButtonStyle": {
"default": {
"up": "rightNorm",
"down": "rightPushed",
"unpressedOffsetY":-10
}
}
}
For the final step, we have to actually create the move buttons and upon clicking them, we must call the movement functions. The " GameStage " class now becomes;
package com.getest.game.stages;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.graphics.g2d.TextureAtlas;
import com.badlogic.gdx.physics.box2d.Box2DDebugRenderer;
import com.badlogic.gdx.physics.box2d.World;
import com.badlogic.gdx.scenes.scene2d.InputEvent;
import com.badlogic.gdx.scenes.scene2d.Stage
;import com.badlogic.gdx.scenes.scene2d.ui.ImageButton;
import com.badlogic.gdx.scenes.scene2d.ui.Skin;
import com.badlogic.gdx.scenes.scene2d.utils.ActorGestureListener;
import com.badlogic.gdx.utils.viewport.ExtendViewport;
import com.getest.game.actors.GroundActor;
import com.getest.game.actors.HeroActor;
import com.getest.game.actors.LeftWallActor;
import com.getest.game.actors.RightWallActor;
import com.getest.game.camera.AndroidCamera;
import com.getest.game.misc.WorldMisc;
public class GameStage extends Stage{
private float accumulator, TIME_STEP;
private Box2DDebugRenderer renderer;
private WorldMisc wrl;
private World world;
private GroundActor ground;
private LeftWallActor leftWall;
private RightWallActor rightWall;
private HeroActor hero;
private Boolean right = false, left = false;
private Skin leftButtonSkin, rightButtonSkin;
private ImageButton leftButton, rightButton;
public GameStage(){
super(new ExtendViewport(16f, 9f, new AndroidCamera(16f, 9f)));
accumulator = 0.0F;
TIME_STEP = 1/300F; // Recomended by libgdx
setupWorld();
}
private void setupWorld() {
wrl = new WorldMisc();
world = wrl.createWorld();
renderer = new Box2DDebugRenderer();
Gdx.input.setInputProcessor(this);
setupGround();
setupLeftWall();
setupRightWall();
setupHero();
setupButtons();
}
private void setupGround(){
ground = new GroundActor(wrl.createGround(world));
addActor(ground);
}
private void setupLeftWall(){
leftWall = new LeftWallActor(wrl.createLeftWall(world));
addActor(leftWall);
}
private void setupRightWall(){
rightWall = new RightWallActor(wrl.createRightWall(world));
addActor(rightWall);
}
private void setupHero() {
hero = new HeroActor(wrl.createHero(world));
addActor(hero);
}
private void setupButtons() {
leftButtonSkin = new Skin(Gdx.files.internal("skins/leftButton.json"), new TextureAtlas(Gdx.files.internal("skins/inGameButtons.atlas")));
rightButtonSkin = new Skin(Gdx.files.internal("skins/rightButton.json"), new TextureAtlas(Gdx.files.internal("skins/inGameButtons.atlas")));
leftButton = new ImageButton(leftButtonSkin);
rightButton = new ImageButton(rightButtonSkin);
leftButton.setSize(2.35F, 5F);
rightButton.setSize(2.35F, 5F);
leftButton.setPosition(1.25F, -0.5F);
rightButton.setPosition(4.0F, -0.5F);
addActor(leftButton);
addActor(rightButton);
leftButton.addListener(new ActorGestureListener() {
public void touchDown(InputEvent event, float x, float y, int pointer, int button) {
left = true;
}
public void touchUp(InputEvent event, float x, float y, int pointer, int button) {
left = false;
}
});
rightButton.addListener(new ActorGestureListener() {
public void touchDown(InputEvent event, float x, float y, int pointer, int button) {
right = true;
}
public void touchUp(InputEvent event, float x, float y, int pointer, int button) {
right = false;
}
});
}
@Override
public void act(float delta) {
super.act(delta);
accumulator += delta;
while (accumulator >= delta) {
world.step(TIME_STEP, 8, 4);
accumulator -= TIME_STEP;
}
if (left) {
hero.moveLeft();
} else if (right) {
hero.moveRight();
} else {
hero.moveStop();
}
}
@Override
public void draw() {
super.draw();
renderer.render(world, getViewport().getCamera().combined);
}
}
What we did extra was to set an input processor to our stage, we created the left and right buttons and we added listeners to them. As i mentioned before, to move with a constant velocity we need to apply impulses to our hero, on every step of the simulation. Upon clicking the left button, a Boolean variable left becomes true, and in the act function we call the moveLeft() function in order for our hero to move left. When we move our finger away from the button, the Boolean becomes false and our hero stops moving. Same goes with the right button.
Run the game and move your awesome hero! This actually is the same method i followed to make Black Dodger move!
Feel free to ask any questions in the comments below;
There's a typo here:
ReplyDelete` public GameStage(){
super(new ExtendViewport(16f, 9f, new AndroridCamera(16f, 9f)));`
Notice the extra "r" where it's supposed to say `AndroidCamera(16f, 9f)`
Great notice! Thanks.
DeleteI'm thrilled that you are still active on this series 3 years later, so Thanks for that!
DeleteThanks! Feel free to ask anything else. Soon enough I will update the project to the latest Libgdx and Admob versions.
DeleteHad to take a break from my learning. I'm back! The *.json files for the buttons generated an error "JSON standard does not allow trailing comma", so I just removed it.
ReplyDeleteI want to thank you again for making the tutorial and I look forward to the update you mentioned on Aug 11. In the meantime, I've gone through the tutorial to the end of this lesson twice and found the same problem. When I draw the arrows, they are sized and positioned incorrectly. I resized and re-postioned them okay, but touching them seems to do nothing and I'm not sure what I've done wrong, or what might have changed. Do you have any suggestions? I can make my code available somewhere if you'd like.
ReplyDeleteFirsts thing first, you did great to remove the trailing comma. I updated the post based on that. Now, about the buttons.
Delete1) Did you make sure to download them on their original size? First click on the image and then download it.
2) Did you 100% set correctly the viewport?
3) If the answer is yes for both the above questions, does the button change when you press it? If yes then you most probably have something wrong with the impulses, if not then the problem is something else.
4) What's the aspect ratio of the device you are using to run the game?
5) You can upload your project (just the code portion) to dropbox or another media and send it to me either here or at skoufas1@hotmail.com
Because I was away for a while, I don't remember what I did, but I either downloaded or re-cut the images. If I re-cut them, I don't remember why. I'm using the atlas I made when I re-packed them.
DeleteI'm confident I have the viewport set correctly.
I have a phone with a ratio of 18:9 and I included the desktop launcher so I can test it there as well. I can re-size my desktop screen as I choose. On my 18:9 screen, I see the right wall box, as expected, but everything else seems okay. The button does not change when pressed. I uploaded my entire project here: https://drive.google.com/drive/folders/1q7_F6kkM1csBMV8ZFePq3Molh57Eojfi?usp=sharing
Some of the code is a little different. I did that to force myself to think a little more, so I'm not just copy/paste-ing my way through the lesson.
Hey, I just finished reviewing your code. Here are my notes;
Delete1) Extend viewport extends the world to fit our device's viewport. That basically means that we create a 16:9 world in your 18:9 mobile phone. Extend viewport will then extend the world (in one direction only, I think it is right when the ar of the device is bigger). We don't want that thought, the optimal in our case would be to extend the world in both directions and keep our 16:9 world centered! In the upcoming tutorials we do exactly that. So, if anything seems not to fit properly that could be the reason. If you don't want that you can either create your world with the same virtual dimensions as your phone, for example 18:9. That would have the same problem for a 16:9 device then, extending the world upwards in that case.
2) Notice the setSize(2.35F, 5F)? The first is the width and the second the height. Why is the height that much bigger? You can probably see in the video that the width is definitely bigger. Well, I needed a way to touch the button even if someone would press above it! That's why in the image the buttons actually have much bigger height but it's just transparent! You can see that in the atlas as well. I think that this is your problem, something is wrong with the image/atlas and the touching area has been messed up. I would suggest to just click in the image and then download it and afterwards just copy the same atlas as in the tutorial. If the problems persists please feel free to tell me.
3) If you don't understand the first point (which is 100% normal) you can download black dodger in your device. You will notice inside the game when you play a level that there are some UFO's on the right and the left. That's the custom viewport I told you about, extending the world in both direction when the ar of the device is bigger. The main world is 16:9, so, a device that has 16:9 ar doesn't see the UFO's at all.
You are amazing! Next time I'm at my desk, I'll try replacing the buttons and reset the height like yours. When I get through this, I'd like to buy you a cup of coffee or something!
DeleteDon't like coffee but I definitely can't say no to a glass of juice or a pizza!!
DeleteI sincerely hope that I am helping you perfect the tutorial, and not being a bother to you.
ReplyDeleteThe buttons were being drawn from the wrong areas of the png.
The atlas starts out with a size declaration of "size: 2048,512". The image on the site, when viewed full size, is 1600 x 400. I put it in an editor and stretched it to 2048, preserving aspect ratio and it came out to 512, so now it's right.
I have the correct two buttons, with a white border around each. The listeners still don't seem to be doing anything. The buttons do not react to being touched and the hero does not move. I'll research in the meantime, but if you see the problem, let me know. If it's my code, I'm sorry for taking your time. If it's your code, then we'll both benefit from having it fixed.
I was missing this line "Gdx.input.setInputProcessor(this);" Obviously a very important line. ;-)
ReplyDeleteI was about to review the code once again but as I can see you found it out! What is a bit strange is that I see the image as 2048 x 512. I will found out why and fix it. Thank you for your contributions, please feel free to mention anything that's wrong or doesn't make sense.
ReplyDelete