简体   繁体   English

等距瓷砖绘图和采摘 - JAVA

[英]Isometric tiles drawing and picking - JAVA

I'm trying to draw isometric tiles in Java and implement a tile picking system using the mouse cursor. 我正在尝试用Java绘制等距图块并使用鼠标光标实现图块拾取系统。 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. 瓷砖是64x64px,但平面瓷砖只有32px高度,即使我使用64x64精灵绘制它们。

瓷砖

The map is a simple 2d array where my tiles are represented by their id. 地图是一个简单的二维数组,我的图块由其ID表示。

Here is the class I use to convert map coordinates to screen coordinates using the toIso() function. 这是我用来使用toIso()函数将地图坐标转换为屏幕坐标的类。 I pass my screen coordinates which represent the cursor position on the screen to the toGrid() function to convert them to map coordinates. 我将代表屏幕上光标位置的屏幕坐标传递给toGrid()函数,将它们转换为地图坐标。

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. 我目前通过使用两个for循环渲染我的tile,并使用toIso()函数将地图坐标转换为屏幕坐标。

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. 如果您需要更多精确度,我很乐意为您提供更多信息。

瓷砖采摘

Here is a video showing what is actually happening: youtu.be/baCVIfJz2Wo 这是一个显示实际情况的视频: 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. 您需要将之前提供的工作表放入创建到项目的ressource文件夹中的“textures”文件夹中。

The gfx package: gfx包:

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. 如果您需要它,您可以在这里找到我的项目架构,以便您可以正确组织所有文件。

项目架构

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 128 * 64瓷砖

After replacing the spritesheet with the new one with 128x64 pixels' tiles, I've been able to achieve the desired output partially... 使用128x64像素的瓷砖替换spritesheet后,我已经能够部分地实现所需的输出...

Why I say "partially" ? 为什么我说“部分” Because I've managed to get the desired result only from the half right part of the map. 因为我设法只从地图的右半部分获得了所需的结果。

在此输入图像描述

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 : 我相信它可能与绘制地图的方式有关,我不是母语为英语的人,所以我可能会误解OP的链接中“注释”部分:

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: 我在程序开头调用了方法toGrid()toIso() ,如下所示:

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: 我确定要修改Assets文件:

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 我还根据Java命名约定ALL_WORDS_UPPER_CASE_CONSTANTS )更改了变量名称并将其public而不是private

I also changed the Game file: 我还改变了Game文件:

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. Assets文件中使用这些常量并计算HALFQUARTER而不是硬编码。

I also believe xOffset shouldn't be public but private as well as some other variables on the program... 我也相信xOffset不应该是public而是private ,以及程序中的一些其他变量......

The tick() method, doesn't need to calculate the xOffset everytime, so we can get rid of this line inside it: tick()方法,不需要每次都计算xOffset ,所以我们可以在其中删除这一行:

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: 对于Tolso方程式,我将它们更改为:

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 };
}

just wanted to say that I finally solved it. 只是想说我终于解决了它。 It was just a conversion to int issue. 这只是转换为int问题。 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};
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM