Adding Characters and Movement in TiledMap with libgdx

After creating the map, the next step is to introduce the main character. Since we've covered how to integarte TiledMap with the Stage, handling the character becomes straightforward.

We can create a character class by extending Actor, but for simplicity, I'll use Image instead.

Edit your TMX file and add an object layer.

Object Layer

Add a shape where the player should appear.

Player Position

Name it play1.

Naming

Our player sprite is:

Player Sprite

The idea is to iterate over all objects in the map and, if the name matches play1, instantiate an Image with the same position as the object and add it to the stage.

Key code:

for (TiledObjectGroup group : map.objectGroups) {
    for (TiledObject object : group.objects) {
        if ("play1".equals(object.name)) {
            player = new Image(new TextureRegion(
                new Texture(Gdx.files.internal("map/player.png")),
                0, 0, 27, 40));
            player.x = object.x;
            player.y = tileMapRenderer.getMapHeightUnits() - object.y; // map origin is top-left, stage is bottom-left
            stage.addActor(player);
        }
    }
}

Result:

Player on Map

Now let's make the player move.

First, we need controls. On Android devices, touch is preferred. If we press and hold the forward area, the player moves forward; hold the upward area to move up.

How to determine which direction is pressed?

Direction Scheme

In the diagram, yellow is the stage, pink is the map border (partially visible), the green dot is the player, and the red dot is the touch point.

To determine the direction, I'll provide a method (not unique). I'll establish a new coordinate system with the player as the origin, compute new coordinates (x, y) for the touch point.

Coordinate System

Based on the quadrant and relative sizes of x and y, we deduce the direction.

Code:

Vector3 tmp = new Vector3(x, y, 0);
stage.getCamera().unproject(tmp);
float newX = tmp.x - player.x;
float newY = tmp.y - player.y;
if (newX > 0 && newY > 0) {
    if (newX > newY) {
        changeDirection(4); // right
    } else {
        changeDirection(1); // up
    }
} else if (newX > 0 && newY < 0) {
    if (newX > -newY) {
        changeDirection(4);
    } else {
        changeDirection(2); // down
    }
} else if (newX < 0 && newY > 0) {
    if (-newX > newY) {
        changeDirection(3); // left
    } else {
        changeDirection(1);
    }
} else {
    if (-newX > -newY) {
        changeDirection(3);
    } else {
        changeDirection(2);
    }
}

Moving the camera directly moves the map, but the player would disappear from view. The solution is to move all actors that should remain visible along with the camera.

private void cameraMove(Vector3 translation) {
    stage.getCamera().position.add(translation);
    for (Actor actor : stage.getActors()) {
        actor.x += translation.x;
        actor.y += translation.y;
    }
}

Complete code:

package com.example.game;

import com.badlogic.gdx.ApplicationListener;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.InputMultiplexer;
import com.badlogic.gdx.InputProcessor;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.GL10;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.BitmapFont;
import com.badlogic.gdx.graphics.g2d.TextureRegion;
import com.badlogic.gdx.graphics.g2d.tiled.TileAtlas;
import com.badlogic.gdx.graphics.g2d.tiled.TileMapRenderer;
import com.badlogic.gdx.graphics.g2d.tiled.TiledLoader;
import com.badlogic.gdx.graphics.g2d.tiled.TiledMap;
import com.badlogic.gdx.graphics.g2d.tiled.TiledObject;
import com.badlogic.gdx.graphics.g2d.tiled.TiledObjectGroup;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.math.Vector3;
import com.badlogic.gdx.scenes.scene2d.Actor;
import com.badlogic.gdx.scenes.scene2d.Stage;
import com.badlogic.gdx.scenes.scene2d.ui.Image;
import com.badlogic.gdx.scenes.scene2d.ui.Label;
import com.badlogic.gdx.scenes.scene2d.ui.Label.LabelStyle;

public class MyGame implements ApplicationListener, InputProcessor {

    private Stage stage;
    private float stageWidth;
    private float stageHeight;
    private TiledMap map;
    private TileAtlas tileAtlas;
    private TileMapRenderer mapRenderer;
    private Image player;
    private Vector3 cameraDirection = new Vector3(1, 1, 0);
    private Vector2 maxCameraPosition = new Vector2(0, 0);
    private Vector3 moveVector = new Vector3(0, 0, 0);
    private boolean isPressed;

    @Override
    public void create() {
        String path = "map/";
        String mapName = "tilemap";
        map = TiledLoader.createMap(Gdx.files.internal(path + mapName + ".tmx"));
        tileAtlas = new TileAtlas(map, Gdx.files.internal(path));
        mapRenderer = new TileMapRenderer(map, tileAtlas, 10, 10);
        maxCameraPosition.set(mapRenderer.getMapWidthUnits(), mapRenderer.getMapHeightUnits());

        stageWidth = Gdx.graphics.getWidth();
        stageHeight = Gdx.graphics.getHeight();
        stage = new Stage(stageWidth, stageHeight, true);

        Label fpsLabel = new Label("FPS:", new LabelStyle(
            new BitmapFont(Gdx.files.internal("font/blue.fnt"),
                Gdx.files.internal("font/blue.png"), false), Color.WHITE), "fpsLabel");
        fpsLabel.y = stageHeight - fpsLabel.getPrefHeight();
        fpsLabel.x = 0;
        stage.addActor(fpsLabel);

        for (TiledObjectGroup group : map.objectGroups) {
            for (TiledObject object : group.objects) {
                if ("play1".equals(object.name)) {
                    player = new Image(new TextureRegion(
                        new Texture(Gdx.files.internal("map/player.png")),
                        0, 0, 27, 40));
                    player.x = object.x;
                    player.y = mapRenderer.getMapHeightUnits() - object.y;
                    stage.addActor(player);
                }
            }
        }

        InputMultiplexer inputMultiplexer = new InputMultiplexer();
        inputMultiplexer.addProcessor(this);
        inputMultiplexer.addProcessor(stage);
        Gdx.input.setInputProcessor(inputMultiplexer);
    }

    @Override
    public void dispose() {
    }

    @Override
    public void pause() {
    }

    @Override
    public void render() {
        Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT);
        OrthographicCamera camera = (OrthographicCamera) stage.getCamera();
        if (isPressed) {
            cameraMove(moveVector);
        }
        ((Label) stage.findActor("fpsLabel")).setText("FPS: " + Gdx.graphics.getFramesPerSecond());
        stage.act(Gdx.graphics.getDeltaTime());
        mapRenderer.render(camera);
        stage.draw();
    }

    private void cameraMove(Vector3 translation) {
        stage.getCamera().position.add(translation);
        for (Actor actor : stage.getActors()) {
            actor.x += translation.x;
            actor.y += translation.y;
        }
    }

    @Override
    public void resize(int width, int height) {
    }

    @Override
    public void resume() {
    }

    @Override
    public boolean keyDown(int keycode) {
        return false;
    }

    @Override
    public boolean keyTyped(char character) {
        return false;
    }

    @Override
    public boolean keyUp(int keycode) {
        return false;
    }

    @Override
    public boolean scrolled(int amount) {
        return false;
    }

    private void changeDirection(int typeId) {
        switch (typeId) {
            case 1:
                moveVector.set(0, 1, 0);
                Gdx.app.log("Direction", "Up");
                break;
            case 2:
                moveVector.set(0, -1, 0);
                Gdx.app.log("Direction", "Down");
                break;
            case 3:
                moveVector.set(-1, 0, 0);
                Gdx.app.log("Direction", "Left");
                break;
            case 4:
                moveVector.set(1, 0, 0);
                Gdx.app.log("Direction", "Right");
                break;
        }
    }

    @Override
    public boolean touchDown(int x, int y, int pointer, int button) {
        Vector3 tmp = new Vector3(x, y, 0);
        stage.getCamera().unproject(tmp);
        float newX = tmp.x - player.x;
        float newY = tmp.y - player.y;
        if (newX > 0 && newY > 0) {
            if (newX > newY) {
                changeDirection(4);
            } else {
                changeDirection(1);
            }
        } else if (newX > 0 && newY < 0) {
            if (newX > -newY) {
                changeDirection(4);
            } else {
                changeDirection(2);
            }
        } else if (newX < 0 && newY > 0) {
            if (-newX > newY) {
                changeDirection(3);
            } else {
                changeDirection(1);
            }
        } else {
            if (-newX > -newY) {
                changeDirection(3);
            } else {
                changeDirection(2);
            }
        }
        isPressed = true;
        return false;
    }

    @Override
    public boolean touchDragged(int x, int y, int pointer) {
        return false;
    }

    @Override
    public boolean touchMoved(int x, int y) {
        return false;
    }

    @Override
    public boolean touchUp(int x, int y, int pointer, int button) {
        isPressed = false;
        Gdx.app.log("Info", "touchUp: x:" + x + " y: " + y + " pointer: " + pointer + " button: " + button);
        return false;
    }
}

Final result (image loading may be slow):

Demo

I don't know how to record phone screen, so I used an emulator; but on a real device (ZTE V880) it runs smoothly.

For multiple characters, the same approach applies: just add more objects. Obviously, our ninja is very skilled—they walk across the map without obstacles! However, if you keep walking, part of the map will disappear, which will be addressed in upcoming articles.

Tags: libgdx TiledMap game development Android Character Movement

Posted on Fri, 14 Aug 2026 16:11:23 +0000 by whitepony6767