Rotating an image in Slick2D in place in a tiled map?

Started by
9 comments, last by packetpirate 4 years, 10 months ago

I just finished writing a parser for TMX data (Tiled maps), and the way Tiled handles rotated tiles is by using three bit flags for flipping an image horizontally, vertically, and diagonally, to allow for rotation. Unless I'm wrong, flipping an image "diagonally" is just rotating 90 degrees CCW (counter-clockwise) and then flipping horizontally.

So I tried to implement this when I create the sub-image of the tileset for each tile:
 


public Image getImage(Image tileset, int w, int h) {
    if(tid == 0) return null;

    int nh = tileset.getWidth() / w; // Number of tiles in this tileset horizontally.

    // Offsets to be multiplied by tile width and height to get origin point of tile.
    int ox = ((tid - 1) % nh); // Subtract 1 because the TID numbers are 1-indexed (why use 0 to indicate nothing? why not -1? stupid Tiled...)
    int oy = ((tid - 1) / nh);

    Image sub = tileset.getSubImage((ox * w), (oy * h), w, h);

    // If any of the flip bits are set, flip the image.
    if(fh || fv) sub = sub.getFlippedCopy(fh, fv);
    if(fd) {
        // To flip diagonally, rotate counter-clockwise, then flip it horizontally.
        sub.setRotation(-90);
        sub = sub.getFlippedCopy(true, false);
    }

    return sub;
}

Specifically, the area where I set the rotation of the sub-image is what's causing my problem. Despite setting the rotation, the image is not rotated on the canvas. Doing a little digging into the library (what little information there is), it seems that the Image class' internal rotate methods don't actually do anything to the image, and most rotation is done using the Graphics context's rotate methods, which rotate the entire context and then draw the image.

The only problem with that is that if I rotate my graphics context, which is the map I'm constructing using these tiles, then the coordinates I give to draw the tile are no longer correct because of the rotated context, so my tiles come out all over the place.

What would be my solution here for rotating the images? Is there a transformation I can do to the coordinates once the graphics context is rotated so they draw in the correct position? I don't know anything about linear algebra, so I'm not sure what to do from here.

Advertisement

Some more info might be useful for those not familiar with Slick2D. I looked at some source code online. I'm not sure if it was the most up-to-date version, or if I was looking at the right code (I looked at Graphics.java and Image.java, specifically draw()), but I see some OpenGL fixed-function code there.

Does that match the code you're using? When you talk about rotating the graphics context and so on, are you talking about fixed-function OpenGL functionality, under the hood at least?

(Sorry if the answers to these question should be obvious. If so, maybe someone who's more familiar with Slick2D can answer.)

28 minutes ago, Zakwayda said:

Some more info might be useful for those not familiar with Slick2D. I looked at some source code online. I'm not sure if it was the most up-to-date version, or if I was looking at the right code (I looked at Graphics.java and Image.java, specifically draw()), but I see some OpenGL fixed-function code there.

Does that match the code you're using? When you talk about rotating the graphics context and so on, are you talking about fixed-function OpenGL functionality, under the hood at least?

(Sorry if the answers to these question should be obvious. If so, maybe someone who's more familiar with Slick2D can answer.)

The code in my original post is called in the constructMap() method of my map object. It uses the ID of the tile from Tiled to calculate the offsets of the image in the tileset, then gets a subimage from the tileset containing just that tile. Then, it's SUPPOSED to rotate it if the flip diagonal bit is set, which as I've already said, it doesn't. If you're curious to see the constructMap() method that calls the above method, this is it:


public void constructMap() {
	try {
		Image tileset = AssetManager.getManager().getImage("grave_wave_tiles");
		context = map.getGraphics();
		context.setBackground(Color.black);
		context.clear();

		for(int i = 0; i < layers.size(); i++) {
			TLayer layer = layers.get(i);
			for(int y = 0; y < mapHeight; y++) {
				for(int x = 0; x < mapWidth; x++) {
					TTile tile = layer.getTile(x, y);
					Image img = tile.getImage(tileset, tileWidth, tileHeight);
					if(img != null) context.drawImage(img, (x * tileWidth), (y * tileHeight));
				}
			}
		}

		context.flush();
	} catch(SlickException se) {
		System.err.println("ERROR: Could not construct map!");
		se.printStackTrace();
	}
}

In the above code, context is a Graphics object and map is an Image (the image everything is rendered to). This method is called after the data is loaded from the TMX file. An Image object is used as a buffer to draw the tiles to it so I only have to do it once. That way, in the future, I can just render the image.

What I meant when I mentioned rotating the graphical context is that I had attempted another method of rotating the tiles where I did something like this instead in the constructMap() method:


public void constructMap() {
	try {
		Image tileset = AssetManager.getManager().getImage("grave_wave_tiles");
		context = map.getGraphics();
		context.setBackground(Color.black);
		context.clear();

		for(int i = 0; i < layers.size(); i++) {
			TLayer layer = layers.get(i);
			for(int y = 0; y < mapHeight; y++) {
				for(int x = 0; x < mapWidth; x++) {
					TTile tile = layer.getTile(x, y);
                    Image img = tile.getImage(tileset, tileWidth, tileHeight);
                    if(img != null) {
                        if(tile.isFlipped(TTile.FLIP_DIAGONAL)) {
                            // If we should flip this diagonally, first rotate by 90 degrees.
                            context.rotate(((x * tileWidth) + (tileWidth / 2)), ((y * tileHeight) + (tileHeight / 2)), 90);
                            // Then flip the image horizontally...
                            Image flipped = img.getFlippedCopy(true, false);
                            context.drawImage(flipped, (x * tileWidth), (x * tileHeight));
                            // Reset the rotation of the canvas.
                            context.rotate(((x * tileWidth) + (tileWidth / 2)), ((y * tileHeight) + (tileHeight / 2)), -90);
                        } else {
                            context.drawImage(img, (x * tileWidth), (y * tileHeight));
                        }
                    }
				}
			}
		}

		context.flush();
	} catch(SlickException se) {
		System.err.println("ERROR: Could not construct map!");
		se.printStackTrace();
	}
}

The problem with this code is, as I said, that it rotates the canvas, so the (x, y) coordinates for the tile are no longer the right position, and when it rotates back, now the tile I just drew is in a completely different place than it should be.

I have a feeling what I want could be achieved by transforming the (x, y) position of the tile once the canvas is rotated, but I don't know exactly what to do with the coordinates to get what I want.

Does that clear anything up? If not, I can just link you to the relevant classes on Github so you can take a better look.

9 minutes ago, packetpirate said:

If not, I can just link you to the relevant classes on Github so you can take a better look.

That might be helpful, if you don't mind doing so.

4 minutes ago, Zakwayda said:

That might be helpful, if you don't mind doing so.

Here is the TMap class, which is the container for all the map data.

https://github.com/packetpirate/Generic-Zombie-Shooter-Redux/blob/master/src/com/gzsr/tmx/TMap.java

And here is the TTile class, which parses the tile IDs and gives the TMap class the image to render to the map graphics context.

https://github.com/packetpirate/Generic-Zombie-Shooter-Redux/blob/master/src/com/gzsr/tmx/TTile.java

There are only two other classes relevant to dealing with the TMX data, but they aren't relevant.

I should say that the second method of rotating the canvas, where I rotate the graphics context directly, DOES work, but only for some tiles. It seems like depending on whether it's a horizontal or vertical flip, it either works, or doesn't.

13 hours ago, Zakwayda said:

That might be helpful, if you don't mind doing so.

I made some more changes to the rotation method used by constructMap() so that it now looks like this:


public void constructMap() {
	try {
		Image tileset = AssetManager.getManager().getImage("grave_wave_tiles");
		context = map.getGraphics();
		context.setBackground(Color.black);
		context.clear();

		for(int i = 0; i < layers.size(); i++) {
			TLayer layer = layers.get(i);
			for(int y = 0; y < mapHeight; y++) {
				for(int x = 0; x < mapWidth; x++) {
					TTile tile = layer.getTile(x, y);
					Image img = tile.getImage(tileset, tileWidth, tileHeight);
					if(img != null) {
						if(tile.isFlipped(TTile.FLIP_DIAGONAL)) {
							// If we should flip this diagonally, first rotate by 90 degrees.
							boolean fh = tile.isFlipped(TTile.FLIP_HORIZONTAL);
							boolean fv = tile.isFlipped(TTile.FLIP_VERTICAL);
							int angle = 270; // Default to CCW for vertically flipped tiles.
							if(fh) angle = 90; // Clockwise (CW) for horizontally flipped tiles.

							context.rotate(((x * tileWidth) + (tileWidth / 2)), ((y * tileHeight) + (tileHeight / 2)), angle);
							// Then flip the image horizontally...
							Image flipped = img.getFlippedCopy(!fh, !fv);
							context.drawImage(flipped, (x * tileWidth), (y * tileHeight));
							// Reset the rotation of the canvas.
							context.resetTransform();
						} else {
							context.drawImage(img, (x * tileWidth), (y * tileHeight));
						}
					}
				}
			}
		}

		context.flush();
	} catch(SlickException se) {
		System.err.println("ERROR: Could not construct map!");
		se.printStackTrace();
	}
}

And this DOES show the correct rotation, but only for the tiles where the "flip horizontal" bit is set.

The basic formula is:

(Horizontal, Diagonal) = Horizontal Flip + CW Rotation + Vertical Flip

(Vertical, Diagonal) = Vertical Flip + CCW Rotation + Horizontal Flip

The "flip vertical" tiles seem to be at weird angles. I did the rotations out in Aseprite just to show that they were the correct rotations and flips to use to get the desired effect, and Aseprite shows the correct end result, so I'm not sure why I don't get what I want in the game...

I've attached a couple screenshots showing where rotations went right, and where they went wrong. The tiles that went wrong are the ones where the vertical flip bit was set. Any ideas?

 

gzsr_gameplay_49.png

gzsr_gameplay_50.png

I may not have an answer regardless, but I'm still a little unsure about some aspects of the code. For example, here:


context.rotate(((x * tileWidth) + (tileWidth / 2)), ((y * tileHeight) + (tileHeight / 2)), angle);

Is rotate() your code, or is it part of Slick2D? If it's part of Slick2D, could you link to the Slick2D code? (Assuming it's publicly available.)

1 minute ago, Zakwayda said:

I may not have an answer regardless, but I'm still a little unsure about some aspects of the code. For example, here:



context.rotate(((x * tileWidth) + (tileWidth / 2)), ((y * tileHeight) + (tileHeight / 2)), angle);

Is rotate() your code, or is it part of Slick2D? If it's part of Slick2D, could you link to the Slick2D code? (Assuming it's publicly available.)

That's a method of the Graphics class, which is part of Slick.

https://github.com/ariejan/slick2d/blob/master/src/org/newdawn/slick/Graphics.java

51 minutes ago, Zakwayda said:

I may not have an answer regardless, but I'm still a little unsure about some aspects of the code. For example, here:



context.rotate(((x * tileWidth) + (tileWidth / 2)), ((y * tileHeight) + (tileHeight / 2)), angle);

Is rotate() your code, or is it part of Slick2D? If it's part of Slick2D, could you link to the Slick2D code? (Assuming it's publicly available.)

Hmm... I think the reason the flips aren't working as expected are because it's not actually flipping the pixel data... it's just changing the offset and width of the image object. Might get muddled when also dealing with rotations.

8 hours ago, Zakwayda said:

I may not have an answer regardless, but I'm still a little unsure about some aspects of the code. For example, here:



context.rotate(((x * tileWidth) + (tileWidth / 2)), ((y * tileHeight) + (tileHeight / 2)), angle);

Is rotate() your code, or is it part of Slick2D? If it's part of Slick2D, could you link to the Slick2D code? (Assuming it's publicly available.)

Figured it out, and I feel so indescribably stupid right now.

So the problem tiles were tiles that had the horizontal and diagonal flip bits set, right? Well I set a conditional breakpoint for the TTile constructor for any tiles where the horizontal boolean got set, and guess what? Not a single tile. Then I noticed something..... the horizontal bit is at 0x80000000, which is 2,147,483,648. The problem with that being... I was storing my hex constants in integers...

So the 0x80000000 constant would probably have overflown and it would have been comparing a completely different value with the TID.

Anyways, it works now. I also changed the drawing method to avoid all the confusing flipping nonsense and just hardcoded a rotation value for each flip condition.

This topic is closed to new replies.

Advertisement