title: Isometric tiles drawing and picking - JAVA
body: <p>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.</p>
  <p><img src="https://i.stack.imgur.com/FUOmX.png" alt="Tile sheet" /></p>
  <p>The map is a simple 2d array where my tiles are represented by their id.</p>
  <p>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.</p>
  <pre><code>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};
  }}
  </code></pre>
  <p>I currently render my tiles by using two for loops and converting the map coordinates to screen coordinates using the toIso() function.</p>
  <pre><code>public void render(Graphics g){
      for(int x = 0;x&lt;width;x++){
          for(int y = 0;y&lt;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);
              }
          }
      }
  }
  </code></pre>
  <p>I get a diamond shape as I wanted rendered on the screen.</p>
  <p>I finally update each tick which are the actual mouse coordinates on screen.</p>
  <pre><code>int[] coords = Utils.toGrid(mouseManager.getMouseX(), mouseManager.getMouseY());
  tileX = coords[0];
  tileY = coords[1];
  </code></pre>
  <p>The selected tile is finally rendered:</p>
  <pre><code>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.
  </code></pre>
  <p>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.</p>
  <p><img src="https://i.stack.imgur.com/S1IbA.png" alt="Tile picking" /></p>
  <p>Here is a video showing what is actually happening: <a href="http://youtu.be/baCVIfJz2Wo" rel="nofollow noreferrer">youtu.be/baCVIfJz2Wo</a></p>
  <hr />
  <h1>EDIT:</h1>
  <p>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 &quot;game&quot;.</p>
  <p>You will need to put the sheet provided before into a &quot;textures&quot; folder created into the ressource folder of the project.</p>
  <p>The gfx package:</p>
  <pre><code>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(&quot;/textures/sheet.png&quot;));
      
          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);
          //
      }
  }
  </code></pre>
  <hr />
  <pre><code>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;
      }
  }
  </code></pre>
  <hr />
  <pre><code>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);
      }
  }
  </code></pre>
  <p>The rest of the project:</p>
  <pre><code>package fr.romainimberti.isometric;
  
  public class Launcher {
  
      public static void main(String args[]){
          System.setProperty(&quot;sun.awt.noerasebackground&quot;, &quot;true&quot;);
          Game game = new Game(&quot;Isometric&quot;, 1280, 720);
          game.start();
      }
  }
  </code></pre>
  <hr />
  <pre><code>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;
      }
  }
  </code></pre>
  <hr />
  <pre><code>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&lt;world.length;i++){
              for(int j=0;j&lt;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&lt;world.length;x++){
              for(int y = 0;y&lt;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 &gt;= 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};
      }
  }
  </code></pre>
  <hr />
  <pre><code>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
      
      }
  }
  </code></pre>
  <p>If you need it, you can find here my project architecture so you can organize all the files correctly.</p>
  <p><img src="https://i.stack.imgur.com/6xUi3.png" alt="Project architecture" /></p>
  <p>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.</p>
  <p><a href="https://i.stack.imgur.com/glrFm.png" rel="nofollow noreferrer">128*64 tiles</a></p>