TMXLoader v0.6.0 released

Introduction

TMXLoader is used for loading .tmx assets created by Tiled Map Editor. It’s a plugin for jMonkeyEngine3.

You can download it here: Releases · jmecn/TMXLoader · GitHub.
Or use the source.

dependency

IMPORTANT: package name changed from com.jme3.tmx to io.github.jmecn.tiled.

maven

<dependency>
    <groupId>io.github.jmecn</groupId>
    <artifactId>tmx-loader</artifactId>
    <version>0.6.0</version>
</dependency>
<dependency>
    <groupId>io.github.jmecn</groupId>
    <artifactId>tmx-renderer</artifactId>
    <version>0.6.0</version>
</dependency>

gradle

repositories {
    mavenCentral()
}

dependencies {
    implementation 'io.github.jmecn:tmx-loader:0.6.0'
    implementation 'io.github.jmecn:tmx-renderer:0.6.0'
}

Example

package io.github.jmecn.tiled.app;

import com.jme3.app.SimpleApplication;
import com.jme3.system.AppSettings;
import io.github.jmecn.tiled.TiledMapAppState;
import io.github.jmecn.tiled.TmxLoader;
import io.github.jmecn.tiled.core.TiledMap;

/**
 * Test loading tmx assets with TmxLoader.
 * @author yanmaoyuan
 *
 */
public class TmxLoaderExample extends SimpleApplication {

    @Override
    public void simpleInitApp() {
        // register it
        assetManager.registerLoader(TmxLoader.class, "tmx", "tsx");

        // load tmx with it
        TiledMap map = (TiledMap) assetManager.loadAsset("Desert/desert.tmx");

        // render it with TiledMapAppState
        stateManager.attach(new TiledMapAppState());

        TiledMapAppState tiledMap = stateManager.getState(TiledMapAppState.class);
        tiledMap.setMap(map);
        tiledMap.setViewColumn(20);
    }

    public static void main(String[] args) {
        AppSettings settings = new AppSettings(true);
        settings.setWidth(1280);
        settings.setHeight(720);
        settings.setSamples(4);
        settings.setGammaCorrection(false);

        TmxLoaderExample app = new TmxLoaderExample();
        app.setSettings(settings);
        app.start();
    }
}

Screenshoots

  • Orthogonal Map

  • Iso map

Changelog

v0.5.0

  • Support parallax scroll
  • Support z-standard compressed data
  • Rewrite a desktop application to show how to use the latest features.

v0.4.0

  • Add map grid to TiledAppState.
  • Highlight the current tile.
  • Support parallax scroll factor. (But you need to implements the camera behaviors in your own game, I plan to make a example to do it).
  • Support tile attributes.
  • Fix the screenToTileCoords result in StaggeredRenderer
  • Fix the tileSize issue in HexagonalMap, which leads to show unwanted lines between tiles.
  • Fix the texture-atlas clamp to edge issue.

v0.3

  • Add Image Layer support
  • Add Group Layer support
  • Add Tinting Color support

v0.2

  • Use Bucket.GUI instead of Bucket.Transparent, change whole scene from XOZ plane to XOY plane now.
  • Removed RPGCameraState, use only TiledMapAppState now.
  • Fixed issue #1 and #2: Removed BatchNode, use simple Node instead.
  • Fixed issue #3: Now tiles with flip mask can display correctly.
  • Add new feature for animated tiles.

animatedtile

v0.1

  • First release
16 Likes

Some show case.
The tmx files I use for testing comes from wbcyclist/SKTileMap

Orthgonal maps

Isometric Maps

Hexagonal Maps

Straggered Maps

15 Likes

This looks really cool, way to go!

1 Like

Yeah, very nice.

1 Like

looks great, nice work

1 Like

Thank you. I’m glad you like it.

I’m now tring to figure out some graphics problems. There are two main problems here.

  1. Cracks appears when moving or zooming camera.
  2. The transparency.

About the cracks

The cracks looks like this.

I don’t really understand why it happens. I guess it is due to my shader. When moving the camera, inPosition in WorldViewProjection may be have some rounding error when calculate floating-point numbers.

code of tiled.vert

#import "Common/ShaderLib/GLSLCompat.glsllib"
#import "Common/ShaderLib/Instancing.glsllib"

attribute vec3 inPosition;
attribute vec2 inTexCoord;
attribute vec4 inColor;

varying vec2 texCoord;

void main(){
    #ifdef HAS_COLORMAP
        texCoord = inTexCoord;
    #endif

    vec4 modelSpacePos = vec4(inPosition, 1.0);

    gl_Position = TransformWorldViewProjection(modelSpacePos);
}

About the transparency problems

Look at the 2nd orthgonal maps I post.

I read this post again and again.

now it looks better.

But I still have some more problems. There is a screenshot of a straggered map that I didn’t post.

I think this it due to how I discard the fragcolor.

code of tiled.j3md

MaterialDef Tiled {

    MaterialParameters {
        Texture2D ColorMap
        Color Color (Color)
        Color TransColor
    }
    
    Technique {
        VertexShader GLSL100:   com/jme3/tmx/resources/Tiled.vert
        FragmentShader GLSL100: com/jme3/tmx/resources/Tiled.frag

        WorldParameters {
            WorldViewProjectionMatrix
            ViewProjectionMatrix
            ViewMatrix
        }

        RenderState {
            Blend Alpha
            FaceCull Off
            DepthWrite On
            DepthTest On
            ColorWrite On
        }
        
        Defines {
            HAS_COLORMAP : ColorMap
            HAS_COLOR : Color
            TRANS_COLOR: TransColor
        }
    }

}

code of tiled.frag

#import "Common/ShaderLib/GLSLCompat.glsllib"

#ifdef TRANS_COLOR
    uniform vec4 m_TransColor;
#endif

uniform vec4 m_Color;
uniform sampler2D m_ColorMap;

varying vec2 texCoord;

void main(){
    vec4 color = vec4(1.0);

    #ifdef HAS_COLORMAP
        color *= texture2D(m_ColorMap, texCoord);     
    #endif

    #ifdef TRANS_COLOR
        if(color.rgb == m_TransColor.rgb) {
            color.a = 0.;
        }
    #endif
    
    #ifdef HAS_COLOR
        color *= m_Color;
    #endif
    
    if (color.a < 0.01) {
    	discard;
    }
    
    gl_FragColor = color;
}

If I change the threshold to 0.5, it looks better for this map, but any other translucent objects with alpha < 0.5 will be discard.

Hope some one can help me…

1 Like

Very nice work!!

1 Like

For the transparency, order is very important.

Are your maps batched as one geometry or are they separate geometries? I guess they are not in the Gui bucket?

As to your gaps, I’m not sure what you mean about “moving the camera” as the most logical thing would have been to put all of this in the Gui bucket and just move them around instead of moving a camera. Gaps shouldn’t happen unless the right coordinate of one tile doesn’t exactly match the left coordinate of the next tile.

1 Like
  1. I use an int value tileZIndex to calculate the order. Each geometry will have different z order.
  2. I use BatchNode for TileLayer as most of the tiles in a layer will share one image.
  3. I use Node for ObjectLayer, because an object could be an Image, a tile or a shape such as polygon.
  4. I use Bucket.Transparent for almost every single spatials.

This is how I order the tiles in OrthogonalRenderer

	public Spatial render(TileLayer layer) {
	int startX = 0;
	int startY = 0;
	int endX = width - 1;
	int endY = height - 1;

	int incX = 1, incY = 1;
	int tmp;
	RenderOrder renderOrder = map.getRenderOrder();
	switch (renderOrder) {
	case RightUp: {
		// swap y
		tmp = endY;
		endY = startY;
		startY = tmp;
		incY = -1;
		break;
	}
	case LeftDown: {
		// swap x
		tmp = endX;
		endX = startX;
		startX = tmp;
		incX = -1;
		break;
	}
	case LeftUp: {
		// swap x
		tmp = endX;
		endX = startX;
		startX = tmp;
		incX = -1;

		// swap y
		tmp = endY;
		endY = startY;
		startY = tmp;
		incY = -1;
		break;
	}
	case RightDown: {
		break;
	}
	}
	endX += incX;
	endY += incY;

	int tileZIndex = 0;

	BatchNode bathNode = new BatchNode(layer.getName());
	for (int y = startY; y != endY; y += incY) {
		for (int x = startX; x != endX; x += incX) {
			final Tile tile = layer.getTileAt(x, y);
			if (tile == null || tile.getVisual() == null) {
				continue;
			}

			Spatial visual = tile.getVisual().clone();
			visual.setLocalTranslation(x * tileWidth, tileZIndex++, y
					* tileHeight);
			bathNode.attachChild(visual);
		}
	}
	bathNode.batch();
	// make it thinner
	if (tileZIndex > 0) {
		bathNode.setLocalScale(1, 1f / tileZIndex, 1);
	}

	return bathNode;
	}

Here is the results.

I use a parallel projection camera looking at the XOZ plane, moveing camera means change it’s location from (x1, 0, z1) to (x2, 0, z2);

1 Like

Just a note: once you’ve batched tiles together then they are rendered in the order they were batched.

Also a note: distance based sorting in the Transparent bucket is based on nearest distance to camera.

For tiles, it should be 100% possible to sort them properly presuming you can get JME to render them in the proper order. You shouldn’t even need any alpha discard threshold.

The Gui bucket is more forgiving for 2D stuff as it will sort based solely on the world Z of the object and not “nearest point to camera”.

Edit: also because there is no z-buffer in the GUI bucket there is no ambiguity about tile order. It’s either rendered properly or not, not partially rendered with odd borders.

1 Like

Thank you. I’ll try Gui bucket later.

I set the QueueBucket of BatchNode to Transparent before batch, then some thing interesting happend.
The spatial become transparent when I move camera location into their “area”.

The orthogonal map

The staggered map.

I think I need some other solutions before I can handle this messs. I need to read the source of com.jme3.renderer.*. The transparency makes me crazy.:chimpanzee_lobotized:
Maybe I can paint the layers to an Image with ImageRaster, or move to XOY plane again and use Gui bucket as your advice.

Before everything I’m going to play DOTA2 and kill as many noobs as I can…:chimpanzee_mad:

1 Like

It’s not hard. The renderer has nothing to do with this.

Spatials get sorted. They are sorted before rendering. So they are rendered in sorted order.

In the opaque bucket, they are rendered front to back… based on the closest point to the camera.

In the transparent bucket, they are rendered back to front… based on the closest point to the camera. Note this may not always be what you might logically think is the closest point. For 2D objects it is the single dumbest way to sort objects. I mean, other than just randomly sorting them however.

In the Gui bucket, they are rendered back to front by their getWorldTransform().z position. This is the simplest sorting ever and the simplest to control… and makes perfect sense in 2D objects. Also, there is no z-buffer… so you won’t ever (never ever) get the weird ‘halos leaking colors’ stuff you do in other buckets.

Once objects are batched, they are never sorted again. Never. They are batched. They are part of one big mesh now. The mesh will always render those triangles in exactly the same order… the order they were batched. Setting the bucket before is identical to setting the bucket after. It makes no difference.

Only the outer batched spatial is ever sorted… by ‘point nearest to camera’… which is now even more bizarre than it was before.

Ultimately, you will be a thousand times better off (and a thousand times more efficient) creating your own already-batched meshes in exactly the order you want… then putting things in the Gui bucket.

2 Likes

Thank you, I will rebuild the MapRenderer later and try Gui bucket.

Edit: Now I understand what you mean by this.

1 Like

Thank you paul, it works! This is what I did (after killing 12 noobs in DOTA2).

First I change every geometry’s queuebucket to Bucket.Gui.

Geometry geometry = new Geometry(name, mesh);
geometry.setQueueBucket(Bucket.Gui);

if (useSharedImage) {
	geometry.setMaterial(sharedMat);
} else {
	geometry.setMaterial(tile.getMaterial());
}

Then render the map as usual.

	Node node = new Node("Tiled Map");
	int len = map.getLayerCount();

	int layerCnt = 0;

	for (int i = 0; i < len; i++) {
		Layer layer = map.getLayer(i);

		// skip invisible layer
		if (!layer.isVisible()) {
			continue;
		}

		Spatial visual = null;
		if (layer instanceof TileLayer) {
			visual = mapRender.render((TileLayer) layer);
		}

		if (layer instanceof ObjectLayer) {
			visual = mapRender.render((ObjectLayer) layer);
		}

		if (layer instanceof ImageLayer) {
			visual = mapRender.render((ImageLayer) layer);
		}

		if (visual != null) {
			node.attachChild(visual);

			// this is a little magic to make let top layer block off the
			// bottom layer
			visual.setLocalTranslation(0, layerCnt++, 0);
		}
	}

Rotate the map π/2 clockwise alone the X axis.

	node.rotate(FastMath.HALF_PI, 0, 0);

move it to screen space.

	Point size = mapRender.getSize();
	int w = cam.getWidth();
	int h = cam.getHeight();
	node.move((w-size.x) * 0.5f, (h+size.y)*0.5f, 0);

Attach the map to rootNode

	rootNode.attachChild(node);

Something else

  • The Nodes (include BatchNode) also need to setQueueBucket(Bucket.Gui);
    I tried only set Geometrys’ bucket to gui, this is what I got:

  • The map node should be in the cam’s view frustum, or nothing will display on the screen.

Now I need to make another camera control.

2 Likes

That always helps :smiley:

1 Like

Yeah, the batch node basically ignores the queue bucket on the children. There is nothing sensible it can do with that information anyway.

2 Likes

Some progress today.

Progress

  • Remove RPGCamAppState.
  • Add support for animated tiles.
  • Tiles flipped now can display correctly.

TODO

  • Some object’s position displayed wrongly, trying to correct them.
  • Add interface to let control tiles and objects in the scene.
2 Likes

v0.2 released.

Changelog

  • Use Bucket.GUI instead of Bucket.Transparent, change whole scene from XOZ plane to XOY plane now.
  • Removed RPGCameraState, use only TiledMapAppState now.
  • Fixed issue #1 and #2: Removed BatchNode, use simple Node instead.
  • Fixed issue #3: Now tiles with flip mask can display correctly.
  • Add new feature for animated tiles.

TODO

  • Fixed issue #4
  • Add interface to let control tiles and objects in the scene.

How to use:

package net.jmecn.tmx;

import com.jme3.app.SimpleApplication;
import com.jme3.tmx.TiledMapAppState;
import com.jme3.tmx.TmxLoader;
import com.jme3.tmx.core.TiledMap;

/**
 * Test loading tmx assets with TmxLoader.
 * @author yanmaoyuan
 *
 */
public class TmxLoaderExample extends SimpleApplication {

	@Override
	public void simpleInitApp() {
		// register it
		assetManager.registerLoader(TmxLoader.class, "tmx", "tsx");

		// load tmx with it
		TiledMap map = (TiledMap) assetManager.loadAsset("Models/Examples/Desert/desert.tmx");
		assert map != null;

		// render it with TiledMapAppState
		stateManager.attach(new TiledMapAppState());
		
		TiledMapAppState tiledMap = stateManager.getState(TiledMapAppState.class);
		tiledMap.setMap(map);
	}

	public static void main(String[] args) {
		TmxLoaderExample app = new TmxLoaderExample();
		app.start();
	}

}
11 Likes

Hey that is great.

I will check it out soon.

Well done so far.

1 Like

I’m doing exactly what you are but I seem to be getting an error in some way.

When I load my asset like you are, it goes smoothly, no issues, but when I then try to render the map with

TiledMapAppState tiledMap = stateManager.getState(TileMapAppState.class);
tiledMap.setMap(map);

Nothing actually appears in my screen.

Any ideas?

1 Like