Rendering a Tiled TMX map as a 3D World in LibGDX

I’ve been meaning to post this for a while, but I never got around to it. I was working on a dungeon crawler POC a while back with some friends and we decided to go with LibGDX as our game engine. For those who don’t know, LibGDX has a super cool new 3D API and since we needed to render a 3D world for our game I have been playing with it a bit lately.

After discussing how we were going to go about creating levels in our game, I was faced with the task of creating a quick and easy way for everyone to create levels that involved no programming. I decided on using Tiled because it’s easy and I have been using it to create levels and worlds in 2d games for a while now, plus LibGDX has some handy built-in functionality to easily handle the TMX files that Tiled exports. So, I needed a way to render a Tiled map export as a 3D world. When I started working on it I figured that someone would have come up with a solution for this already, but surprisingly I was not able to find any existing solutions that did what I needed. So, I hunkered down and wrote up my own world renderer.

The demo is heavily inspired by and built on top of existing code from TheInvader360‘s Arena Roamer tutorial in which a text file is used to render a 3D and a 2D arena with collectable items. Unfortunately, this game has not really gone anywhere since building this demo, but I figured I might as well release the code for the world renderer.

You can find the source code on GitHub

I’m not going to go through a whole tutorial here because the code wasn’t originally written to be a tutorial. However, I will post the important parts of the functionality here and try to explain them a bit. These are just some snippets for explanation purposes, for access to the full source clone from GitHub.

First, the World constructor loads the tmx file and sets up the layers to be looped through. Then the constructor calls generateLevel which loops through each tile in the map and checks for tile properties to tell whether it’s a wall, a floor, or the player starting position. Depending on what property the tile has generateLevel then creates a new object and adds it to the corresponding array, or it sets the player starting position.

World.java


public World(TiledWorldBuilder game) {
	this.game = game;
	player = new Player(this);
	
	tiledMap = new TmxMapLoader().load("maps/test.tmx");
	layer = (TiledMapTileLayer) tiledMap.getLayers().get(0);
	
	generateLevel(currentLevel);
}
	
private void generateLevel(int levelNumber) {
	walls.clear();
	floors.clear();
	exits.clear();
	gems.clear();
	doors.clear();
	keys.clear();
	
	//Level loading via tmx will go here
       for(int x = 0; x < layer.getWidth();x++){
           for(int y = 0; y < layer.getHeight();y++){
           	 cell = layer.getCell(x,y);
            	 
           	 if(cell.getTile().getProperties().containsKey("wall")){
            		 walls.add(new Wall(x, layer.getHeight() - y, 1f, 1f)); //Add a wall to the walls array
            	 }
            	 
            	 else if(cell.getTile().getProperties().containsKey("floor")){
            		 floors.add(new Floor(x, layer.getHeight() - y, 1f, 1f)); //Add a floor to the floors array
            	 }
            	 
            	 else if(cell.getTile().getProperties().containsKey("playerStart")){
            		 floors.add(new Floor(x, layer.getHeight() - y, 1f, 1f)); //Add a floor tile beneath the player
            		 player.getCenterPos().set(x, layer.getHeight() - y); //Set player starting pos
            	 }
            }
            
        }
        
        //Old level loading
		//inventoryGems = 0;
		//inventoryKeys = 0;
		//map.load("Level"+levelNumber+".map");
}

Then inside WorldRenderer.java the generateMap() method creates the model instances based on our arrays inside World and adds the newly created ModelInstances to some local arrays. Once the ModelInstances have been created, renderPlayArea() is called and we first add the player, then render the walls and floors arrays respectively.

WorldRenderer.java


private void renderPlayArea() {
		Gdx.gl.glClearColor(0, 0, 0, 1);
		Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT|GL20.GL_DEPTH_BUFFER_BIT);
		
		if(currentLevel != world.getLevel()){
			generateMap();
			currentLevel = world.getLevel();
		}
	
		//Update camera on world rendering
		camera.position.set(world.getPlayer().getCenterPos().x * 2f, (world.getPlayer().getCenterPos().y * 2f) -0, 0.75f);
		camera.rotate(world.getPlayer().getRotation(), 0, 0, 1);
		camera.update();
		camera.rotate(-world.getPlayer().getRotation(), 0, 0, 1);
		
		//Draw the models to the screen
		modelBatch.begin(camera);
			modelBatch.render(walls, environment);
			modelBatch.render(floors, environment);
		modelBatch.end();
	}

	public void generateMap(){
		for (int i = 0; i < world.getFloors().size; i++){
			//Create the model instance
			floorInstance = null;
			floorInstance = new ModelInstance(
				cube, 
				world.getFloors().get(i).centrePosX * 2f, 
				world.getFloors().get(i).centrePosY * 2f, 
				-2
			);
			
			//Load Texture
			materialLoader = floorInstance.materials.get(0);
			textureAttribute = new TextureAttribute(TextureAttribute.Diffuse, floorTexture);
			materialLoader.set(textureAttribute);
			
			//Add the model instance to the array
			floors.add(floorInstance);
		}
		
		for(int i = 0; i < world.getWalls().size; i++){
			//Create Instance
			wallInstance = null;
			wallInstance = new ModelInstance(
				cube, 
				world.getWalls().get(i).centrePosX * 2f, 
				world.getWalls().get(i).centrePosY * 2f, 
				0
			);
			
			//Load Texture
			materialLoader = wallInstance.materials.get(0);
			textureAttribute = new TextureAttribute(TextureAttribute.Diffuse, wallTexture);
			materialLoader.set(textureAttribute);
			
			//Add Instance to the array
			walls.add(wallInstance);
    	}
}

So there you have it, it's not too fancy and it's still just in prototype format. I haven't even built it to handle multiple levels yet. However, I wanted to make it available for people to use and improve upon since I wasn't using it for anything. An idea I had recently was to use JSON instead of TMX, but I haven't had any extra time to work on this. Perhaps someone can take this and manipulate it to use JSON.

Comments

Leave a comment

Leave a Reply

Your email address will not be published. Required fields are marked *