6
votes

I'm trying to draw isometric tiles in Java and implement a tile picking system using the mouse cursor. I draw the tiles using these math formulas I found and adapted to my tile textures which you can find below. Tiles are 64x64px but flat tiles are only 32px height even if I draw them using the 64x64 sprite.

Tile sheet

The map is a simple 2d array where my tiles are represented by their id.

Here is the class I use to convert map coordinates to screen coordinates using the toIso() function. I pass my screen coordinates which represent the cursor position on the screen to the toGrid() function to convert them to map coordinates.

public class Utils {

private static int TILE_WIDTH = Tile.TILE_WIDTH;
private static int TILE_HEIGHT = Tile.TILE_HEIGHT;

private static int TILE_WIDTH_HALF = TILE_WIDTH/2;
private static int TILE_HEIGHT_HALF = TILE_HEIGHT/2;

private static int TILE_WIDTH_QUARTER = TILE_WIDTH_HALF/2;
private static int TILE_HEIGHT_QUARTER = TILE_HEIGHT_HALF/2;

public static int[] toIso(int x, int y){

    int i = (x - y) * TILE_WIDTH_HALF;
    int j = (x + y) * TILE_HEIGHT_QUARTER;

    //800 and 100 are temporary offsets I apply to center the map.
    i+=800;
    j+=100;
    
    return new int[]{i,j};
}

public static int[] toGrid(int x, int y){

    //800 and 100 are temporary offsets I apply to center the map.
    x-=800;
    y-=100;
    int i = ( x / ( TILE_WIDTH_HALF ) + y / ( TILE_HEIGHT_QUARTER )) / 2;
    int j = ( y / ( TILE_HEIGHT_QUARTER ) - ( x / ( TILE_WIDTH_HALF ))) / 2;
    
    return new int[]{i,j};
}}

I currently render my tiles by using two for loops and converting the map coordinates to screen coordinates using the toIso() function.

public void render(Graphics g){
    for(int x = 0;x<width;x++){
        for(int y = 0;y<height;y++){
            int[] isoCoords = Utils.toIso(x, y);
            int fx = isoCoords[0];//
            int fy = isoCoords[1];//
            if(world[x][y] == 0){
                Tile grass = new GrassTile(0);
                grass.render(g, grass.getId(), fx, fy);
            }else if(world[x][y] == 1){
                Tile water = new WaterTile(1);
                water.render(g, water.getId(), fx, fy);
            }
        }
    }
}

I get a diamond shape as I wanted rendered on the screen.

I finally update each tick which are the actual mouse coordinates on screen.

int[] coords = Utils.toGrid(mouseManager.getMouseX(), mouseManager.getMouseY());
tileX = coords[0];
tileY = coords[1];

The selected tile is finally rendered:

BufferedImage selectedTexture = Assets.selected;
int[] coordsIsoSelected = Utils.toIso(this.tileX, this.tileY);
g.drawImage(selectedTexture, coordsIsoSelected[0], coordsIsoSelected[1], Tile.TILE_WIDTH, Tile.TILE_HEIGHT, null);
    
g.drawRect(Utils.toIso(tileX, tileY)[0], Utils.toIso(tileX, tileY)[1]+Tile.TILE_HEIGHT/2, Tile.TILE_WIDTH, Tile.TILE_HEIGHT/2);//I draw a rectangle to visualize what's happening.

Finally, my tile detection isn't working as expected, it isn't fitting the tiles perfectly, however it seems to be in relation with the rectangle I draw. I can't figure out the solution to this problem, I thank you in advance for reading or any advice you could give to me. If you need more precisions, I would be glad to give you more informations.

Tile picking

Here is a video showing what is actually happening: youtu.be/baCVIfJz2Wo


EDIT:

Here is some of my code you could use to run an application like mine. Sorry for this very messy code, but I tried to make it as short as possible without disturbing the behavior of the "game".

You will need to put the sheet provided before into a "textures" folder created into the ressource folder of the project.

The gfx package:

package fr.romainimberti.isometric.gfx;

import java.awt.image.BufferedImage;


public class Assets {

    private static final int width = 64, height = 64;

    public static BufferedImage grass, water, selected;
    
    public static void init(){
        //Temp
        SpriteSheet tileSheet = new SpriteSheet(ImageLoader.loadImage("/textures/sheet.png"));
    
        grass = tileSheet.crop(width*2, 0, width, height);
        water = tileSheet.crop(width*9, height*5, width, height);
        selected = tileSheet.crop(0, height*5, width, height);
        //
    }
}

package fr.romainimberti.isometric.gfx;

import java.awt.image.BufferedImage;
import java.io.IOException;

import javax.imageio.ImageIO;


public class ImageLoader {

    public static BufferedImage loadImage(String path){
        try {
            return ImageIO.read(ImageLoader.class.getResource(path));
        } catch (IOException e) {
            e.printStackTrace();
            System.exit(1);
        }
        return null;
    }
}

package fr.romainimberti.isometric.gfx;

import java.awt.image.BufferedImage;

public class SpriteSheet {

    private BufferedImage sheet;

    public SpriteSheet(BufferedImage sheet){
        this.sheet = sheet;
    }

    public BufferedImage crop(int x, int y, int width, int height){
        return sheet.getSubimage(x, y, width, height);
    }
}

The rest of the project:

package fr.romainimberti.isometric;

public class Launcher {

    public static void main(String args[]){
        System.setProperty("sun.awt.noerasebackground", "true");
        Game game = new Game("Isometric", 1280, 720);
        game.start();
    }
}

package fr.romainimberti.isometric;

import java.awt.Canvas;
import java.awt.Dimension;

import javax.swing.JFrame;

public class Display {

    private JFrame frame;
    private Canvas canvas;

    private String title;
    private int width, height;

    public Display(String title, int width, int height){
        this.title = title;
        this.width = width;
        this.height = height;
    
        createDisplay();
    }

    private void createDisplay(){
        frame = new JFrame(title);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setResizable(true);
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    
        canvas = new Canvas();
        canvas.setPreferredSize(new Dimension(width, height));
        canvas.setMaximumSize(new Dimension(width, height));
        canvas.setMinimumSize(new Dimension(width, height));
        canvas.setFocusable(true);
        
        frame.add(canvas);
        frame.pack();
    }

    public Canvas getCanvas(){
        return canvas;
    }

    public JFrame getFrame(){
        return frame;
    }
}

package fr.romainimberti.isometric;

import java.awt.Graphics;
import java.awt.image.BufferStrategy;
import java.util.concurrent.ThreadLocalRandom;
import javax.swing.JFrame;
import fr.romainimberti.isometric.gfx.Assets;

public class Game implements Runnable {

    private Display display;
    private int width, height;

    public JFrame frame;

    private boolean running = false;
    private Thread thread;
    public String title;

    private BufferStrategy bs;
    private Graphics g;

    public int x, y;

    public int[][] world;

    public static final int TILE_WIDTH = 64;
    public static final int TILE_HEIGHT = 64;
    public static final int TILE_WIDTH_HALF = 32;
    public static final int TILE_HEIGHT_HALF = 32;
    public static final int TILE_WIDTH_QUARTER = 16;
    public static final int TILE_HEIGHT_QUARTER = 16;

    public int xOffset;

    //Input
    private MouseManager mouseManager;

    public Game(String title, int width, int height){
        this.width = width;
        this.height = height;
        this.mouseManager = new MouseManager(this);
        this.world = new int[10][10];
    }

    private void init(){
        display = new Display(title, width, height);
        display.getFrame().addMouseListener(mouseManager);
        display.getFrame().addMouseMotionListener(mouseManager);
        display.getCanvas().addMouseListener(mouseManager);
        display.getCanvas().addMouseMotionListener(mouseManager);
        this.frame = display.getFrame();
        Assets.init();
        xOffset = frame.getWidth()/2;
        //Fill the world
        for(int i = 0;i<world.length;i++){
            for(int j=0;j<world[0].length;j++){
                int r = ThreadLocalRandom.current().nextInt(0,1+1);
                if(r == 0)
                    world[i][j] = 0;
                else 
                    world[i][j] = 1;
            }
        }
    }

    private void tick(){
        mouseManager.tick();
        xOffset = frame.getWidth()/2;
    }

    private void render(){
        bs = display.getCanvas().getBufferStrategy();
        if(bs == null){
            display.getCanvas().createBufferStrategy(3);
            return;
        }
        g = bs.getDrawGraphics();
        //Clear Screen
        g.clearRect(0, 0, frame.getWidth(), frame.getHeight());
        //Draw Here

        //World render
        for(int x = 0;x<world.length;x++){
            for(int y = 0;y<world[0].length;y++){
                int[] isoCoords = toIso(x, y);
                int fx = isoCoords[0];//
                int fy = isoCoords[1];//
                if(world[x][y] == 0){
                    g.drawImage(Assets.grass, fx, fy, null);
                }else if(world[x][y] == 1){
                    g.drawImage(Assets.water, fx, fy, null);
                }
            }
        }
    
        //Selected tile render
        int[] coordsIsoSelected = toIso(x, y);
        g.drawImage(Assets.selected, coordsIsoSelected[0], coordsIsoSelected[1], TILE_WIDTH, TILE_HEIGHT, null);
    
        //End Drawing
        bs.show();
        g.dispose();
    }

    public void run(){
        init();
        int fps = 120;
        double timePerTick = 1000000000 / fps;
        double delta = 0;
        long now;
        long lastTime = System.nanoTime();
        while(running){
            now = System.nanoTime();
            delta += (now - lastTime) / timePerTick;
            lastTime = now;
            if(delta >= 1){
                tick();
                render();
                delta--;
            }
        }
        stop();
    }

    public MouseManager getMouseManager(){
        return mouseManager;
    }

    public int getWidth(){
        return width;
    }

    public int getHeight(){
        return height;
    }

    public synchronized void start(){
        if(running)
            return;
        running = true;
        thread = new Thread(this);
        thread.start();
    }

    public synchronized void stop(){
        if(!running)
            return;
        running = false;
        try {
            thread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static int[] toIso(int x, int y){

        int i = (x - y) * TILE_WIDTH_HALF;
        int j = (x + y) * TILE_HEIGHT_QUARTER;
        i+=xOffset;
    
        return new int[]{i,j};
    }

    public static int[] toGrid(int x, int y){
    
        x-=xOffset;
        int i = ( x / ( TILE_WIDTH_HALF ) + y / ( TILE_HEIGHT_QUARTER )) / 2;
        int j = ( y / ( TILE_HEIGHT_QUARTER ) - ( x / ( TILE_WIDTH_HALF ))) / 2;
    
        return new int[]{i,j};
    }
}

package fr.romainimberti.isometric;

import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;


public class MouseManager implements MouseListener, MouseMotionListener {

    private boolean leftPressed, rightPressed;
    private int mouseX, mouseY;
    private Game game;
    public MouseManager(Game game){
        this.game = game;
    }

    public void tick(){
        game.x = game.toGrid(mouseX, mouseY)[0];
        game.y = game.toGrid(mouseX, mouseY)[1];
    }

    // Getters

    public boolean isLeftPressed(){
        return leftPressed;
    }

    public boolean isRightPressed(){
        return rightPressed;
    }

    public int getMouseX(){
        return mouseX;
    }

    public int getMouseY(){
        return mouseY;
    }

    // Implemented methods

    @Override
    public void mousePressed(MouseEvent e) {
        if(e.getButton() == MouseEvent.BUTTON1)
            leftPressed = true;
        else if(e.getButton() == MouseEvent.BUTTON3)
            rightPressed = true;
    }

    @Override
    public void mouseReleased(MouseEvent e) {
        if(e.getButton() == MouseEvent.BUTTON1)
            leftPressed = false;
        else if(e.getButton() == MouseEvent.BUTTON3)
            rightPressed = false;
    
    }

    @Override
    public void mouseMoved(MouseEvent e) {
        mouseX = e.getX();
        mouseY = e.getY();
    }

    @Override
    public void mouseDragged(MouseEvent e) {
        // TODO Auto-generated method stub
    
    }

    @Override
    public void mouseClicked(MouseEvent e) {
        // TODO Auto-generated method stub
    
    }

    @Override
    public void mouseEntered(MouseEvent e) {
        // TODO Auto-generated method stub
    
    }

    @Override
    public void mouseExited(MouseEvent e) {
        // TODO Auto-generated method stub
    
    }
}

If you need it, you can find here my project architecture so you can organize all the files correctly.

Project architecture

Again, sorry for this very, very messy code but I had to split all the usefull parts of my game to reduce it's size. Also don't forget to download and place correctly the sheet file. Hope this will help.

128*64 tiles

2
"If you need more precisions, I would be glad to give you more informations." The highest precision thing you could bring is a minimal reproducible example, please post one. Also, I can't completely understand your question, what are you trying to achieve exactly? Btw, are you using Swing?Frakcool
Where is your mouse hovering over? What should happen when you hover them? What is happening now? If you could provide a MCVE it would be easier to understandFrakcool
Alright, the video made the problem clear to me, but we still need a minimal reproducible example so we can copy-paste and see the error ourselves and try to solve it too :) Could you try and make one? This isn't your whole working code, it's a new short sample (not code snippets as above) that we can copy-paste, need to import nothing (i.e. include imports in your MCVE), this way we'll be able to provide more information / answers and of more qualityFrakcool
What is a code snippet? They are parts of code that show an specific part of the program and generally are short. What is a whole program? It includes all the classes the program needs to be run but it's really big. What is a MCVE? It is a middle point between the two, it's a whole program with the minimal code needed to replicate your problem, one that we can easily copy-paste in our IDEs and actually see the problem. You may read the link of Minimal, Complete... I posted twice above and Short, Self Contained, Correct Example (SSCCE) to understanding what I'm sayingFrakcool
Thank you very much for your help @Frakcool , I will try to simplify my code to make it short and executable easely ! I'll notify you when that will be done ! thank you ! :)RomainImberti

2 Answers

3
votes

just wanted to say that I finally solved it. It was just a conversion to int issue. These are the final methods that I use. Hope it will help people who are trying to work with isometric tiles. Thank you !

public static int[] toIso(int x, int y){

    int i = (x - y) * TILE_WIDTH_HALF;
    int j = (x + y) * TILE_HEIGHT_QUARTER;

    i += xOffset-TILE_WIDTH_HALF;
    j+=yOffset;

    return new int[]{i,j};
}

public static int[] toGrid(double i, double j){

    i-=xOffset;
    j-=yOffset;

    double tx = Math.ceil(((i / TILE_WIDTH_HALF) + (j / TILE_HEIGHT_QUARTER))/2);
    double ty = Math.ceil(((j / TILE_HEIGHT_QUARTER) - (i / TILE_WIDTH_HALF))/2);

    int x = (int) Math.ceil(tx)-1;
    int y = (int) Math.ceil(ty)-1;

    return new int[]{x, y};
}
1
votes

After replacing the spritesheet with the new one with 128x64 pixels' tiles, I've been able to achieve the desired output partially...

Why I say "partially"? Because I've managed to get the desired result only from the half right part of the map.

enter image description here

I believe it could have something to do with how the map is being painted, I'm not a native English speaker so I might be misunderstanding what the "Notes" section says in OP's link:

Notice that the "origin" of the isometric tile is the top corner. But usually when we draw a sprite it's from the top-left corner

I've called the methods toGrid() and toIso() at the beginning of the program as follows:

int[] coordinates = Game.toIso(2, 1);
System.out.println(coordinates[0] + "-" + coordinates[1]);

int[] coordinates2 = Game.toGrid(coordinates[0], coordinates[1]);
System.out.println(coordinates2[0] + "-" + coordinates2[1]);

And got the following results, (Which indeed are what we were expecting), so we know the methods work correctly:

64-96
2-1

I was sure to modify the Assets file:

public static final int WIDTH = 128, HEIGHT = 64;

Where I also changed the variable names following the Java naming conventions (ALL_WORDS_UPPER_CASE_CONSTANTS) and made it public instead of private

I also changed the Game file:

public static final int TILE_WIDTH = Assets.WIDTH;
public static final int TILE_HEIGHT = Assets.HEIGHT;
public static final int TILE_WIDTH_HALF = TILE_WIDTH / 2;
public static final int TILE_HEIGHT_HALF = TILE_HEIGHT / 2;
public static final int TILE_WIDTH_QUARTER = TILE_WIDTH / 4;
public static final int TILE_HEIGHT_QUARTER = TILE_HEIGHT / 4;

To use those constants on the Assets file and calculate the HALF and QUARTER instead of hardcoding it.

I also believe xOffset shouldn't be public but private as well as some other variables on the program...

The tick() method, doesn't need to calculate the xOffset everytime, so we can get rid of this line inside it:

xOffset = frame.getWidth() / 2 - 65;

I also changed the way you paint the tile you're selecting as:

// Selected tile render
int[] coordsIsoSelected = toIso(x, y);
g.drawImage(Assets.selected, coordsIsoSelected[0], coordsIsoSelected[1], TILE_WIDTH, TILE_HEIGHT, null);

And for the Tolso equations, I changed them to:

public static int[] toIso(int x, int y) {
    int i = (x - y) * TILE_WIDTH_HALF;
    int j = (x + y) * TILE_HEIGHT_HALF;

    i += xOffset;

    return new int[] { i, j };
}

Below I adjusted the parenthesis locations:

public static int[] toGrid(int x, int y) {
    x -= xOffset;

    int i = ((x / TILE_WIDTH_HALF) + (y / TILE_HEIGHT_HALF)) / 2;
    int j = ((y / TILE_HEIGHT_HALF) - (x / TILE_WIDTH_HALF)) / 2;

    return new int[] { i, j };
}