3
\$\begingroup\$

[libgdx version 1.9.13 - Desktop Game]

This question seems to have been asked many times, but I have yet to find a decent solution. I want to display a map without edges, where the player moves in all directions on a 'world' which is toroidal.

I load my tiles into cells of an OrthogonalTiledMapRenderer, which I then render. This gives me a map which draws in the game window - works fine - but it has edges. The player can walk up to the edge of map.

So my questions:

  1. How do I get a tiled map to draw without edges (I.E. no background color around edge of tile map, as per attached screen shot, which shows character about to walk off the map)

enter image description here

  1. How do I then make the map toroidal - so the player moves off left map edge and is moved to right edge. Same for top/bottom. I can set the players coordinates to do this, but screen 'jumps' to the other edge.

So if the player continually presses a single up/down/left/right key, eventually they will come back to their starting position.

\$\endgroup\$
2
  • \$\begingroup\$ I think this is a good answer to Q1 - keep camera inside gameworld coordinates - gamedev.stackexchange.com/questions/74926/… \$\endgroup\$
    – DataMania
    Feb 3, 2021 at 5:19
  • 1
    \$\begingroup\$ "This question seems to have been asked many times, but I have yet to find a decent solution" — if you ask the same question, you'll tend to get the same answers you've previously found and deemed "indecent". Can you clarify what solutions you've researched from.past Q&A, and what leads you to conclude they are not decent for your case? This can help focus new answers on what they'll need to do differently to meet your decency criteria. \$\endgroup\$
    – DMGregory
    Feb 3, 2021 at 13:09

1 Answer 1

2
\$\begingroup\$

Wrapping a tiled map can be achieved by rendering the TiledMap multiple times if the camera position is closer to the borders of the map than the size of the camera viewport.

So for example on a map that is 16 world units wide and viewed by a camera with a viewport width of 8 and the camera is at X=2 then the map needs to first be renderer as normal using the camera (2, y), and then once more with a camera that is at (16 + x, y) (or (mapWidth + x, y)), because 2 is less than 8 so we know that the camera position is far enough to the left for us to require to render the right-most part of the map to the left of the map.

For when x is close to the right-hand side of the map the position is (x - mapWidth, y).

The same is replicated for y and there's also the case when both x and y are close to the edges, in which cases the map needs to be rendered once more for the corner.

enter image description here

It's also important to wrap the position of the main camera to always be inside of the map:

    if (position.x < 0.0f)
        position.x = mapWidth + position.x;
    if (position.x > mapWidth)
        position.x = mapWidth - position.x;

    if (position.y < 0.0f)
        position.y = mapHeight + position.y;
    if (position.y > mapHeight)
        position.y = mapHeight - position.y;

When the wrapping is done the "extra" rendering of the TiledMap might look like this:

    // Render normally first
    camera.update();
    mapRenderer.setView(camera);
    mapRenderer.render();

    // Now render the extra views depending on where the camera is
    if (position.x < camera.viewportWidth)
    {
        wrapCamera.position.set(mapWidth + position.x, position.y, 1.0f);
        wrapCamera.update();
        mapRenderer.setView(wrapCamera);
        mapRenderer.render();

        if (position.y < camera.viewportHeight)
        {
            wrapCamera.position.set(mapWidth + position.x, mapHeight + position.y, 1.0f);
            wrapCamera.update();
            mapRenderer.setView(wrapCamera);
            mapRenderer.render();
        }

        if (position.y > mapHeight - camera.viewportHeight)
        {
            wrapCamera.position.set(mapWidth + position.x, position.y - mapHeight,1.0f);
            wrapCamera.update();
            mapRenderer.setView(wrapCamera);
            mapRenderer.render();
        }
    }

    if (position.x > mapWidth - camera.viewportWidth)
    {
        wrapCamera.position.set(position.x - mapWidth, position.y, 1.0f);
        wrapCamera.update();
        mapRenderer.setView(wrapCamera);
        mapRenderer.render();

        if (position.y < camera.viewportHeight)
        {
            wrapCamera.position.set(position.x - mapWidth, mapHeight + position.y, 1.0f);
            wrapCamera.update();
            mapRenderer.setView(wrapCamera);
            mapRenderer.render();
        }

        if (position.y > mapHeight - camera.viewportHeight)
        {
            wrapCamera.position.set(position.x - mapWidth, position.y - mapHeight,1.0f);
            wrapCamera.update();
            mapRenderer.setView(wrapCamera);
            mapRenderer.render();
        }
    }

    if (position.y < camera.viewportHeight)
    {
        wrapCamera.position.set(position.x, mapHeight + position.y, 1.0f);
        wrapCamera.update();
        mapRenderer.setView(wrapCamera);
        mapRenderer.render();
    }

    if (position.y > mapHeight - camera.viewportHeight)
    {
        wrapCamera.position.set(position.x, position.y - mapHeight,1.0f);
        wrapCamera.update();
        mapRenderer.setView(wrapCamera);
        mapRenderer.render();
    } 

In the above example there are two Cameras, one called camera which is the main camera that follows the player, and wrapCamera that has the same viewport size as camera but has a position that is updated for each wrapped view that needs to be rendered.

Complete source code for the gif is:

package com.bornander.sandbox;

import com.badlogic.gdx.ApplicationAdapter;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Input;
import com.badlogic.gdx.assets.AssetManager;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.Sprite;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.maps.MapProperties;
import com.badlogic.gdx.maps.tiled.TiledMap;
import com.badlogic.gdx.maps.tiled.TiledMapRenderer;
import com.badlogic.gdx.maps.tiled.TiledMapTileLayer;
import com.badlogic.gdx.maps.tiled.TmxMapLoader;
import com.badlogic.gdx.maps.tiled.renderers.BatchTiledMapRenderer;
import com.badlogic.gdx.maps.tiled.renderers.OrthogonalTiledMapRenderer;
import com.badlogic.gdx.math.Vector2;

public class MyGdxGame extends ApplicationAdapter {
    TiledMap map;
    float mapWidth;
    float mapHeight;

    OrthogonalTiledMapRenderer mapRenderer;
    OrthographicCamera camera;
    OrthographicCamera wrapCamera;
    SpriteBatch batch;
    Texture tank;
    Vector2 position = new Vector2();
    Vector2 direction = new Vector2(1, 0);
    float speed = 0.0f;

    @Override
    public void create () {
        float aspectRatio = (float)Gdx.graphics.getWidth() / (float)Gdx.graphics.getHeight();
        // Set camera to view 16 tiles wide
        camera     = new OrthographicCamera(16, 16 * (1.0f / aspectRatio));
        wrapCamera = new OrthographicCamera(16, 16 * (1.0f / aspectRatio));
        batch = new SpriteBatch();
        tank = new Texture("tank_huge.png");
        TmxMapLoader mapLoader = new TmxMapLoader();
        map = mapLoader.load("map.tmx");
        mapWidth = map.getProperties().get("width", Integer.class);
        mapHeight = map.getProperties().get("height", Integer.class);
        mapRenderer = new OrthogonalTiledMapRenderer(map, 1.0f / 128.0f);
    }

    @Override
    public void render () {
        Gdx.gl.glClearColor(0, 0, 0, 1);
        Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);

        if (Gdx.input.isKeyPressed(Input.Keys.A))
            direction.rotate( 90f * Gdx.graphics.getDeltaTime());
        if (Gdx.input.isKeyPressed(Input.Keys.D))
            direction.rotate(-90f * Gdx.graphics.getDeltaTime());
        if (Gdx.input.isKeyPressed(Input.Keys.W))
            speed = Math.min(8.0f, speed + Gdx.graphics.getDeltaTime());

        if (Gdx.input.isKeyPressed(Input.Keys.S))
            speed = Math.max(-4.0f, speed - Gdx.graphics.getDeltaTime());

        position.add(direction.x * speed * Gdx.graphics.getDeltaTime(), direction.y * speed * Gdx.graphics.getDeltaTime());

        if (position.x < 0.0f)
            position.x = mapWidth + position.x;
        if (position.x > mapWidth)
            position.x = mapWidth - position.x;

        if (position.y < 0.0f)
            position.y = mapHeight + position.y;
        if (position.y > mapHeight)
            position.y = mapHeight - position.y;


        System.out.println(String.format("(%.2f, %.2f)", position.x, position.y));
        camera.position.set(position, 1.0f);

        // Render normally first
        camera.update();
        mapRenderer.setView(camera);
        mapRenderer.render();

        // Now render the extra views depending on where the camera is

        if (position.x < camera.viewportWidth)
        {
            wrapCamera.position.set(mapWidth + position.x, position.y, 1.0f);
            wrapCamera.update();
            mapRenderer.setView(wrapCamera);
            mapRenderer.render();

            if (position.y < camera.viewportHeight)
            {
                wrapCamera.position.set(mapWidth + position.x, mapHeight + position.y, 1.0f);
                wrapCamera.update();
                mapRenderer.setView(wrapCamera);
                mapRenderer.render();
            }

            if (position.y > mapHeight - camera.viewportHeight)
            {
                wrapCamera.position.set(mapWidth + position.x, position.y - mapHeight,1.0f);
                wrapCamera.update();
                mapRenderer.setView(wrapCamera);
                mapRenderer.render();
            }
        }

        if (position.x > mapWidth - camera.viewportWidth)
        {
            wrapCamera.position.set(position.x - mapWidth, position.y, 1.0f);
            wrapCamera.update();
            mapRenderer.setView(wrapCamera);
            mapRenderer.render();

            if (position.y < camera.viewportHeight)
            {
                wrapCamera.position.set(position.x - mapWidth, mapHeight + position.y, 1.0f);
                wrapCamera.update();
                mapRenderer.setView(wrapCamera);
                mapRenderer.render();
            }

            if (position.y > mapHeight - camera.viewportHeight)
            {
                wrapCamera.position.set(position.x - mapWidth, position.y - mapHeight,1.0f);
                wrapCamera.update();
                mapRenderer.setView(wrapCamera);
                mapRenderer.render();
            }
        }

        if (position.y < camera.viewportHeight)
        {
            wrapCamera.position.set(position.x, mapHeight + position.y, 1.0f);
            wrapCamera.update();
            mapRenderer.setView(wrapCamera);
            mapRenderer.render();
        }

        if (position.y > mapHeight - camera.viewportHeight)
        {
            wrapCamera.position.set(position.x, position.y - mapHeight,1.0f);
            wrapCamera.update();
            mapRenderer.setView(wrapCamera);
            mapRenderer.render();
        }

        batch.setProjectionMatrix(camera.combined);
        batch.begin();

        batch.draw(
                tank,
                position.x-0.5f,
                position.y-0.5f,
                0.5f, 0.5f, /* origin */
                1.0f, 1.0f, /* width, height */
                0.5f, 0.5f, /* scale */
                direction.angle() + 90,
                0, 0,
                tank.getWidth(), tank.getHeight(),
                false, false /* flip */);

        batch.end();
    }

}
\$\endgroup\$
1
  • 1
    \$\begingroup\$ awesome answer. Thanks. \$\endgroup\$
    – DataMania
    Feb 7, 2021 at 19:20

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .