"First we'll move our wee Vector2! This float pair will become dear to you. Once we have an x and a y, I think you and I, will have little work left to do."
- adapted from the libGDX Discord
Are you new to libGDX and are struggling with character movement in 2D? Well, fellow explorer of a seemingly rule-less world, you've come to the right place.
I'm 250 hours into programming a Stardew Valley inspired 2D RPG - and let me tell you, it's been a ride. I'm still relatively new to programming, so it's been cool learning about fancy software patterns, wrangle a big project, and become besties with Ms. Red Debug Circle.
But we definitely encountered some meltdowns along the way. Especially in the beginning I struggled with figuring out the big design decisions. libGDX gives you all the freedom in the world, but sometimes that can feel a little overwhelming.
That's why I'll be showing you today how I answered the question "But how do I get my character moving?!".
Laying the land
"libGDX is a cross-platform Java game development framework. [It] does not force a specific design or coding style on you; it rather gives you the freedom to create a game the way you prefer".
- from https://libgdx.com/
Before diving into the specifics of translating user input into 2D character movement, let's go over a few ground rules so we can keep this post focussed on the topic of movement.
This post assumes that you have a basic understanding of how to set up a libGDX project and create a game. If not, don't worry! There are many tutorials available, such as my personal favourite, ColdWild's Multiplatform tutorial.
I also will not be covering map creation in this post, but I highly recommend using Tiled for all your map-making needs.
Nevertheless, if you have any libGDX questions, feel free to reach out or comment on the blog. I'll do my best to answer and if it's a bigger topic, I might tackle it in a future post.
Oh, the places you'll go
- How to set up your Player Actor -
Player
Object
The first thing you'll want to do is create your Player
object - the representative of our user, the star of the show, the walking impersonation of main-character-syndrome. But instead of giving them a personality and a story, we set up:
- a Sprite (the picture of the
Player
), - the Actor (a 2D scene graphic node, aka a sprite that has a location and size that can be tracked by Scene2D and can
act
upon events, and - the Box2D body
Note: the below code has been stripped from everything not related to movement. Your Player
will probably be quite a big more extensive
//Player.java
public class Player extends Actor {
[...]
public Player(Vector2 startPosition) {
super();
sprite = [sprite retrieving logic];
// Set the actor's x and y coordinates, as well as width and height.
setBounds(startPosition.x, startPosition.y, sprite.getWidth(), sprite.getHeight());
// Create the Box2D body for the Player
physicalBody = [libgdx Body creation logic]
// connect Actor with Box2D body
physicalBody.setUserData(this);
[...]
}
In the next step, we will want to ensure that our Player
moves at the correct speed. To do so, we call trackMovement
whenever our Player
(which is an Actor
) is called upon to act.
//Player.java
public class Player extends Actor {
[...]
public float speed = 5 * Resources.TILE_SIZE; // = 5 * 48
private Vector2 currentVelocity = new Vector2(0, 0); // starting velocity is 0 in either direction x or y; aka we're standing still
@Override
public void act(float delta) {
super.act(delta);
trackMovement(delta);
}
private void trackMovement(float delta) {
// calculate how far the body has moved by multiplying the constant speed by how much time has elapsed
float movement = delta * speed;
// move the physical body by multiplying current velocity with the movement
physicalBody.setLinearVelocity(currentVelocity.cpy().scl(movement));
// move sprite by moving actor to the same coordinates as the physical body
this.setPosition(physicalBody.getPosition().x - Resources.WORLD_TILE_SIZE / 2f, physicalBody.getPosition().y - Resources.WORLD_TILE_SIZE / 2f);
}
Prize question: Where will our character move where while
currentVelocity == Vector2(0, 0)
?
Answer: Nowhere.
So how do we get out of The Waiting Place?
"Waiting for a train to go or a bus to come,
or a plane to go or the mail to come,
or the rain to go or the phone to ring,
or the snow to snow or waiting around for a Yes or No
or waiting for their hair to grow.Everyone is just waiting."
- Dr. Seuss
Capturing Inputs with an InputListener
Within Player
My Player
class listens to a set of InputListener
s; which one depends on its current state.
When the Player
is walking around freely, it listens to input events relating to movement, environment checks, etc. But, for example, when the Player
is sitting, movement inputs will be ignored while Player
reacts to other inputs directing sitting-related tasks.
For the sake of this tutorial, however, we'll only be adding one listener to our Player
: the FreeRoamingMovementListener
.
//Player.java
public Player(Vector2 startPosition) {
[...]
addListener(FreeRoamingMovementListener(this));
}
Within FreeRoamingMovementListener
This listener is all about capturing user input and translating it into movement. To do so, it overrides both the method keyDown
(so we keep applying movement while the key is pressed) as well as keyUp
(in order to stop the moment a key is released)
"
EventListeners
are added to actors to be notified about events. For example,InputListener
is provided for receiving and handling InputEvents. An actor just needs to add anInputListener
to start receiving input events. [It] has several methods that may be overridden."
- From the Wiki
@override keyDown()
When a key is pressed, it goes through several steps:
- Keep track of all pressed keys (in
pressedKeyCodes
) - Determines the state of the
Player
- is it moving North, East, South-West, ...? - Calculates the corresponding force/velocity on the
Player
's Box2DBody
&Sprite
- Informs
Player
of its new state
//FreeRoamingMovementListener.java
class FreeRoamingMovementListener extends InputListener {
private final Set<Integer> pressedKeyCodes = new HashSet<>(); // note: chose a set because we care about values being unique and want efficient lookups
[...]
@Override
public boolean keyDown(InputEvent event, int keycode) {
pressedKeyCodes.add(keycode);
// Step 1: Determining the state.
PlayerState state = getPlayerStateBasedOnCurrentlyPressedKeys();
if (state == null) {
pressedKeyCodes.remove(keycode);
return false;
}
// Step 2: Translate the direction the player is facing into velocity
Vector2 newVelocity = state.calculateDirectionVector();
// Step 3: Update velocity & state in `Player`
player.setStateAndVelocity(state, newVelocity);
return true;
}
Let's cover each step in more detail.
Step 1: Determining the state.
With the press of W
, S
, A
, or D
, our Player
can move North, South, West and East respectively; on an especially fancy day, we can even head North-East, South-West, and all points in between. The trick is to keep track of that state to determine things like the correct sprite, animation and interactionable-object search radius.
I use enum PlayerState
to keep track of a Player
's movement type and direction (see its definition under Step 2), and the following code helps us find the right state for the given input.
Note: This code needs a bit of fine-tuning to avoid preferring certain directions. But for now, it'll do the trick.
//FreeRoamingMovementListener.java
class FreeRoamingMovementListener extends InputListener {
[...]
private PlayerState getPlayerStateBasedOnCurrentlyPressedKeys() {
// get sprite based on walking direction
if (pressedKeyCodes.contains(Input.Keys.UP)) {
if (pressedKeyCodes.contains(Input.Keys.RIGHT)) {
return PlayerState.WALKING_NE;
} else if (pressedKeyCodes.contains(Input.Keys.LEFT)) {
return PlayerState.WALKING_NW;
} else {
return PlayerState.WALKING_N;
}
} else if (pressedKeyCodes.contains(Input.Keys.DOWN)) {
if (pressedKeyCodes.contains(Input.Keys.RIGHT)) {
return PlayerState.WALKING_SE;
} else if (pressedKeyCodes.contains(Input.Keys.LEFT)) {
return PlayerState.WALKING_SW;
} else {
return PlayerState.WALKING_S;
}
} else if (pressedKeyCodes.contains(Input.Keys.RIGHT)) {
return PlayerState.WALKING_E;
} else if (pressedKeyCodes.contains(Input.Keys.LEFT)) {
return PlayerState.WALKING_W;
} else {
return null;
}
}
Step 2: Translate the direction the Player
is facing into velocity
I use a Vector2
to help me determine which direction on a xy-axis the Player
is facing. If it's headed north, we'll increase the y-axis by setting DirectionVector = Vector2(0, 1);
. But if it's headed south, we'll have to go the opposite direction with DirectionVector = Vector2(0, -1);
.
//PlayerState.java
public enum PlayerState {
STANDING_S, STANDING_N, STANDING_E, STANDING_W,
WALKING_S, WALKING_N, WALKING_E, WALKING_W, WALKING_NE, WALKING_SE, WALKING_SW, WALKING_NW,
SITTING_W,
PICKUP_S, PICKUP_N, PICKUP_E, PICKUP_W,
HOLD_S,
;
private static final float ONE_ON_ROOT_TWO = (float) (1.0 / Math.sqrt(2));
public Vector2 calculateDirectionVector() {
return switch (this) {
case WALKING_N, STANDING_N -> new Vector2(0, 1);
case WALKING_S, STANDING_S -> new Vector2(0, -1);
case WALKING_E, STANDING_E -> new Vector2(1, 0);
case WALKING_W, STANDING_W -> new Vector2(-1, 0);
case WALKING_NE -> new Vector2(ONE_ON_ROOT_TWO, ONE_ON_ROOT_TWO);
case WALKING_NW -> new Vector2(-ONE_ON_ROOT_TWO, ONE_ON_ROOT_TWO);
case WALKING_SE -> new Vector2(ONE_ON_ROOT_TWO, -ONE_ON_ROOT_TWO);
case WALKING_SW -> new Vector2(-ONE_ON_ROOT_TWO, -ONE_ON_ROOT_TWO);
case SITTING_W -> new Vector2(0, 0);
case PICKUP_S, PICKUP_E, PICKUP_W, PICKUP_N -> new Vector2(0, 0);
case HOLD_S -> new Vector2(0, 0);
};
}
Step 3: Update velocity in Player
Now we've determined the direction our Player
should be heading, it's time to get moving!
Back in Player
class, we use the newly calculated velocity and set it as our currentVelocity
. And by applying it to the Box2d Body
, the Player
has started moving in the direction of our choice.
//Player.java
public class Player extends Actor {
private Vector2 currentVelocity = new Vector2(0, 0);
[...]
void setStateAndVelocity(PlayerState newState, Vector2 newVelocity) {
// Update our velocity and enact it on our physical body.
currentVelocity = newVelocity;
physicalBody.setLinearVelocity(currentVelocity);
// Update our state and let the sprite animator know of our new state.
[...]
// Trigger hooks upon a state change.
[...]
}
Remember the question I asked earlier when we looked at trackMovement()
? Let's turn it around.
What happens to Player
, while currentVelocity != Vector2(0, 0);
?
private void trackMovement(float delta) {
float movement = delta * speed;
physicalBody.setLinearVelocity(currentVelocity.cpy().scl(movement));
this.setPosition(physicalBody.getPosition().x - Resources.WORLD_TILE_SIZE / 2f, physicalBody.getPosition().y - Resources.WORLD_TILE_SIZE / 2f);
}
... Right! Our character starts going places!
@override keyUp()
When a key gets released, our Player
can take a break from moving in that direction. We remove the key from the pressedKeyCodes
and recalculate the Player
's state and velocity, just like we did when the key was first pressed.
//FreeRoamingMovementListener.java
@Override
public boolean keyUp(InputEvent event, int keycode) {
pressedKeyCodes.remove(keycode);
// Step 1: determine PlayerState based on remaining pressed keycodes (see description for Step 1 under `keyDown`)
PlayerState state = getPlayerStateBasedOnCurrentlyPressedKeys();
// Step 2: Set newVelocity back to `Vector2(0, 0)` and override it once more, if applicable (see description of Step 2 under `keyDown`)
Vector2 newVelocity = Vector2.Zero;
if (state != null) {
newVelocity = state.calculateDirectionVector();
}
// Step 3: Update the player's state (see description of Step 3 under `keyDown`)
updatePlayerState(state, newVelocity);
return true;
}
And that's it!
Concluding thoughts... get on your way!
"You tried to escape math while at school.
You copied, and you scraped, and you hid like a fool.
Well, enduring soul, as you shall see soon,
What once was a game now turned into
a living nightmare because Vector Math has become as inescapable as death and taxes.... and that's on you."
- by yours truly
Let's put it all together!
//Player.java
public class Player extends Actor {
public float speed = 5 * Resources.TILE_SIZE;
private Vector2 currentVelocity = new Vector2(0, 0);
public Player(Vector2 startPosition) {
super();
sprite = [sprite retrieving logic];
// Set the actor's x and y coordinates, as well as width and height.
setBounds(startPosition.x, startPosition.y, sprite.getWidth(), sprite.getHeight());
// Create the Box2D body for the Player
physicalBody = [libgdx Body creation logic]
// connect Actor with Box2D body
physicalBody.setUserData(this);
addListener(FreeRoamingMovementListener(this));
}
@Override
public void act(float delta) {
super.act(delta);
trackMovement(delta);
}
private void trackMovement(float delta) {
// calculate how far the body has moved by multiplying the constant speed by how much time has elapsed
float movement = delta * speed;
// move the physical body by multiplying current velocity with the movement
physicalBody.setLinearVelocity(currentVelocity.cpy().scl(movement));
// move sprite by moving actor to the same coordinates as the physical body
this.setPosition(physicalBody.getPosition().x - Resources.WORLD_TILE_SIZE / 2f, physicalBody.getPosition().y - Resources.WORLD_TILE_SIZE / 2f);
}
void setStateAndVelocity(PlayerState newState, Vector2 newVelocity) {
// Update our velocity and enact it on our physical body.
currentVelocity = newVelocity;
physicalBody.setLinearVelocity(currentVelocity);
// Update our state and let the sprite animator know of our new state.
[...]
// Trigger hooks upon a state change.
[...]
}
}
//FreeRoamingMovementListener.java
class FreeRoamingMovementListener extends InputListener {
private final Player player;
private final Set<Integer> pressedKeyCodes = new HashSet<>();
public FreeRoamingMovementListener(Player player) {
this.player = player;
}
@Override
public boolean keyDown(InputEvent event, int keycode) {
pressedKeyCodes.add(keycode);
// Step 1: Determining the state.
PlayerState state = getPlayerStateBasedOnCurrentlyPressedKeys();
if (state == null) {
pressedKeyCodes.remove(keycode);
return false;
}
// Step 2: Translate the direction the player is facing into velocity.
Vector2 newVelocity = state.calculateDirectionVector();
// Step 3: Update velocity & state in `Player`.
player.setStateAndVelocity(state, newVelocity);
return true;
}
@Override
public boolean keyUp(InputEvent event, int keycode) {
pressedKeyCodes.remove(keycode);
// Step 1: Determining the state.
PlayerState state = getPlayerStateBasedOnCurrentlyPressedKeys();
// Step 2: Translate the state of player into velocity or default to Vector2.Zero.
Vector2 newVelocity = Vector2.Zero;
if (state != null) {
newVelocity = state.calculateDirectionVector();
}
// Step 3: Update velocity & state in `Player`.
updatePlayerState(state, newVelocity);
return true;
}
private PlayerState getPlayerStateBasedOnCurrentlyPressedKeys() {
// get sprite based on walking direction
if (pressedKeyCodes.contains(Input.Keys.UP)) {
if (pressedKeyCodes.contains(Input.Keys.RIGHT)) {
return PlayerState.WALKING_NE;
} else if (pressedKeyCodes.contains(Input.Keys.LEFT)) {
return PlayerState.WALKING_NW;
} else {
return PlayerState.WALKING_N;
}
} else if (pressedKeyCodes.contains(Input.Keys.DOWN)) {
if (pressedKeyCodes.contains(Input.Keys.RIGHT)) {
return PlayerState.WALKING_SE;
} else if (pressedKeyCodes.contains(Input.Keys.LEFT)) {
return PlayerState.WALKING_SW;
} else {
return PlayerState.WALKING_S;
}
} else if (pressedKeyCodes.contains(Input.Keys.RIGHT)) {
return PlayerState.WALKING_E;
} else if (pressedKeyCodes.contains(Input.Keys.LEFT)) {
return PlayerState.WALKING_W;
} else {
return null;
}
}
}
//PlayerState.java
public enum PlayerState {
STANDING_S, STANDING_N, STANDING_E, STANDING_W,
WALKING_S, WALKING_N, WALKING_E, WALKING_W, WALKING_NE, WALKING_SE, WALKING_SW, WALKING_NW,
SITTING_W,
PICKUP_S, PICKUP_N, PICKUP_E, PICKUP_W,
HOLD_S,
;
private static final float ONE_ON_ROOT_TWO = (float) (1.0 / Math.sqrt(2));
public Vector2 calculateDirectionVector() {
return switch (this) {
case WALKING_N, STANDING_N -> new Vector2(0, 1);
case WALKING_S, STANDING_S -> new Vector2(0, -1);
case WALKING_E, STANDING_E -> new Vector2(1, 0);
case WALKING_W, STANDING_W -> new Vector2(-1, 0);
case WALKING_NE -> new Vector2(ONE_ON_ROOT_TWO, ONE_ON_ROOT_TWO);
case WALKING_NW -> new Vector2(-ONE_ON_ROOT_TWO, ONE_ON_ROOT_TWO);
case WALKING_SE -> new Vector2(ONE_ON_ROOT_TWO, -ONE_ON_ROOT_TWO);
case WALKING_SW -> new Vector2(-ONE_ON_ROOT_TWO, -ONE_ON_ROOT_TWO);
case SITTING_W -> new Vector2(0, 0);
case PICKUP_S, PICKUP_E, PICKUP_W, PICKUP_N -> new Vector2(0, 0);
case HOLD_S -> new Vector2(0, 0);
};
}
I really hope this blog post helps you along your journey.
Let me know if you have any questions, suggestions, ideas or want to just show me how your character moves around! I'm always keen to get in touch with other (game) devs!
For completeness sake
- aka some more code snippets -
In this post I sprinkled some pseudo-code in [ ]
brackets to keep the code focussed. That's why below, I added the actual code called by my pseudo-brackets. I hope this proves useful and am happy to answer questions.
Get Actor's Sprite
//Resources.java
private Resources() {
gameSprites = new TextureAtlas(Gdx.files.internal("packed/game.atlas"));
}
public Sprite createSpriteInTileSizeUnits(String spriteFileName) {
Sprite sprite = new Sprite(gameSprites.createSprite(spriteFileName));
resizeSpriteSizeToTileUnits(sprite);
return sprite;
}
private void resizeSpriteSizeToTileUnits(Sprite s) {
s.setSize(spriteUnitsToTileUnits(s.getWidth()), spriteUnitsToTileUnits(s.getHeight()));
}
public static float spriteUnitsToTileUnits(float val) {
return val / TILE_SIZE;
}
Create a Box2D body
//TiledObjectsUtil.java
public static Body createBox(World world, float x, float y, float width, float height, boolean isStatic) {
BodyDef def = new BodyDef(); // a description of all the physical aspects of the body (e.g. solid vs. dynamic, etc.)
if (isStatic) {
def.type = BodyDef.BodyType.StaticBody;
} else {
def.type = BodyDef.BodyType.DynamicBody; // non-zero velocity
}
def.position.set(x, y);
def.fixedRotation = true; // true --> will not be rotating when impacted by other bodies
Body pBody = world.createBody(def); // initialises body, puts it into world with def physical properties.
// give a shape to pBody enttiy
PolygonShape shape = new PolygonShape();
shape.setAsBox(width/2f, height/2f); // expands out from the "center", meaning that it's actually double that for width and height. So if you set only 32, it'll be 64 wide
pBody.createFixture(shape, 1f);
shape.dispose(); // keep it clean!
return pBody;
}
"Congratulations!
Today is your day.
You're off to Great Places!
You're off and away!"
- Dr. Seuss
Happy coding!
Mirjam is a self-taught software developer and passionate board gamer. She works as a backend developer at Digio, an Australian software consultancy.
You can reach her on LinkedIn if you wanna chat about anything and everything related to tech, game dev, boardgames or the latest reality TV show you binged.
Top comments (1)
Thanks for useful article.
Do you have any experience on combining ECS system (especially Ashley) with Scene2D. Some people believe that this is not best practice but I'm not among them and in my opinion it is very interesting interaction between ECS and Scene2D which decoupling UI from game engine.