java - 在JFrame中显示移动 block

标签 java swing

我有一个创建用于玩俄罗斯方块简单游戏的JFrame的类,也有一个DrawSquare类,它完全按照您的想法去做,但是当我初始化DrawSquare类的新实例然后尝试将一个和所有其他对象都绘制到我的JFrame上,事情开始出错,该代码旨在在左上角绘制一个正方形,然后一次放下一行直到它到达框架的底部(这样),那么应该在框架顶部的第二列中以及在左下角的第一个正方形中绘制一个新的正方形,但是一旦它开始下拉第二列,我就会得到一系列对角线朝右上角绘制的正方形。目前,我计划要执行的代码是从每列的顶行开始放下一个正方形,并在到达框架的底部时停下来,我是否将类的实例存储在代码的错误位置?编辑:实际上,我非常确定这是我想在实例到​​达底部时存储该实例。该类的每个实例都需要自己的计时器吗?

public class Tetris extends JFrame {
    public static final int height = 20; //height of a square
    public static final int width = 20; //width of a square
    public int xPos = 0; //column number of the square
    public int yPos = 0; //row number of the square

    public static void main(String[] args){
            Tetris tet = new Tetris();
    }

    public Tetris() {
        DrawSquare square = new DrawSquare(xPos, yPos, width, height, false);
        add(square);
        DrawSquare.squares.add(square);
        setSize(220,440);
        setLocationRelativeTo(null);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setVisible(true);
    }
}


public class DrawSquare extends JPanel {
    public static List<DrawSquare> squares = new ArrayList<>();
    protected int xPos;
    protected int yPos;
    protected int width;
    protected int height;
    protected Timer timer = new Timer(200, new TimerListener());
    protected boolean endFall = false;


    public DrawSquare(int xPos, int yPos, int width, int height, boolean endFall) {
        this.xPos = xPos;
        this.yPos = yPos;
        this.width = width;
        this.height = height;
        this.endFall = endFall;
        this.timer.start();
    }

    class TimerListener implements ActionListener {
        @Override
        public void actionPerformed(ActionEvent e) {
            yPos++;
            if (yPos > 19) {
                yPos = 19;
                endFall = true;
            }
            if (endFall == true) {
                timer.stop();
                if (xPos > 8) {
                    xPos = 8;
                }
                xPos++;
                endFall = false;
                yPos = 0;
                DrawSquare newSqr = new DrawSquare(xPos, yPos, width, height, true);
                squares.add(newSqr);
                add(newSqr);
            }
            timer.start();
            repaint();
        }
    }

    protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        Iterator<DrawSquare> it = squares.iterator();
        while (it.hasNext()) {
            DrawSquare square = it.next();
            g.fillRect(square.xPos * square.width, square.yPos * square.height, square.width, square.height);
        }
    }
}

最佳答案

您正在举一个很好的例子,说明了初学者对挥杆(以及许多其他图形工具包)如何将内容呈现到屏幕上的基本误解。我将概述与您有关的内容,然后回答您的直接问题并解释如何修复您的代码。

我花了很长时间(很长时间)才弄清楚这些东西如何影响我的自我,所以请多多包涵。我希望通读这个答案比回答这个问题能以更广泛的方式帮助您。

异步绘图

与修改程序状态(主线程以及计时器和其他线程)的顺序相比,Swing绘制窗口的顺序(事件调度线程)完全不同。您可以在主线程中随意更改要绘制的对象的坐标,但是直到您通过在一个组件上调用JComponent.repaint()要求更改时,更改才会显示出来。通常,这将立即触发该组件的重新绘制,显示您的最新状态。

如果您在主线程中更改了JPanel之类的小部件的坐标,则它可能会立即显示。这是因为您用于设置位置的方法将在内部触发重新绘制请求。

重画请求进入队列,并最终由事件分配线程处理。这是调用paintComponent方法的地方。因此,paintComponent方法应仅绘制。它不应执行任何其他逻辑。如果它需要知道如何绘制一些专业的东西,则该信息应保存在其他线程之一可以访问的位置。

简而言之,您可以根据需要在主线程或计时器中进行计算并更新状态。然后,您可以通过paintComponent方法在事件分配线程中访问该状态。

计时器

您可以使用多种方式使用计时器来运行GUI,但是对于当前应用程序,您实际上只需要一种方式。在您的情况下,计时器只需要做两件事:


检查一个块是否已经完全下降并且不再需要移动。
触发面板的重新绘制。


如果块的位置是关于时间的简单方程式,则无需在定时器中计算块的更新位置。如果您知道某个块出现在屏幕上的时间以及当前时间,那么您就知道该块已移动了多远,因此您可以完全根据经过的时间在正确的位置绘制它。

如果您有一个更复杂的系统,并且其路径无法完全按时间预测,那么我建议您也将运动逻辑也纳入计时器事件中。在这种情况下,您可以考虑使用多个计时器,或切换到java.util.timer。但是同样,这不适用于您当前的情况(即使有多个块)。

模型和视图

程序的模型就是持有抽象状态的东西。在这种情况下,所有块的位置和其他元数据。视图是执行渲染的部分。分开这两件事通常是个好主意。 GUI经常有第三个组件,称为控制器,将模型和视图连接到用户。由于您尚未询问有关控制块的信息,因此我们将在此处忽略它。

在您当前的代码中,您试图用扩展名JPanel和现有块的static列表来表示块。尽管JPanel是显示带有一些自定义图形(例如图标)的矩形块的便捷方法,但我建议您首先使用传递给GraphicspaintComponent对象直接绘制这些块。至少在一开始,它将帮助您将绘图代码和游戏逻辑视为独立的实体。

代码转储前的最终回合

我已经对您的代码进行了重写,以将之前所做的所有改动都封装到代码中。关于我所做的事情,以下是一些其他要点,可能有助于解释我的推理:


调用JFrame.add(...)将组件添加到JFrame时,实际上是在调用JFrame.getContentPane().add(...)。内容窗格是90%的常规挥杆部件进入窗口的位置。因此,我们可以将将进行渲染的JPanel设置为您的内容窗格,也可以将其添加到当前的内容窗格中。我选择了后者,以便以后可以添加其他小部件,例如记分板。
类名通常应为名词,而方法通常为动词。这不是绝对的规则(实际上什么都不是),但是以这种方式命名事物通常可以帮助您以更有意义的方式可视化对象之间的交互。因此,我已将DrawSquare重命名为GamePiece
GamePiece不再是JPanel的任何理由。它只需要知道自己的宽度,高度和出现时间。
试图DrawSquare绘制自身的另一个问题是组件只能在其自己的边界框中真正绘制。因此,您真的想覆盖所有包含矩形的paintComponent
呈现类维护对两个GamePiece列表的引用。一种是用于移动的物体,另一种是用于坠落的物体。在列表之间移动它们的逻辑在计时器中。这比说要向GamePiece添加标志更好,因为它有助于增量重画。我将在这里仅部分说明这一点,但是有一个重新绘制的版本,仅要求绘制一个小的区域。这将有助于加快运动速度。




public class Tetris extends JFrame
{
    public static final int height = 20; //height of a square
    public static final int width = 20; //width of a square
    public static final int x = 0;

    private GamePanel gamePanel;

    public static void main(String[] args)
    {
        Tetris tet = new Tetris();
        // Normally you would tie this to a button or some other user-triggered action.
        tet.gamePanel.start();
        tet.gamePanel.addPiece(new GamePiece(width, height, x));
    }

    public Tetris()
    {
        getContentPane().setLayout(new BorderLayout());
        gamePanel = GamePanel();
        add(gamePanel, BorderLayout.CENTER);
        setSize(220,440);
        setLocationRelativeTo(null);
        setDefaultCloseOperation(EXIT_ON_CLOSE);
        setVisible(true);
    }
}

public class GamePanel extends JPanel
{
    private List<GamePiece> moving;
    private List<GamePiece> still;
    private Timer timer;

    public GamePanel()
    {
        moving = new ArrayList<>();
        still = new ArrayList<>();
        timer = new Timer(100, new TimerListener());
    }

    public addPiece(int width, int height, int x)
    {
        moving.add(new GamePiece(width, height, x));
    }

    public void start()
    {
        timer.start();
    }

    @Override
    public void paintComponent(Graphics g)
    {
        Rectangle clip = g.getClipBounds(null);
        Rectangle rectToDraw = new Rectangle();

        // I prefer this, but you can make the call every
        // time you call `GamePiece.getY()`
        long time = System.currentTimeMillis();

        for(GamePiece piece : this.moving) {
            rectToDraw.setSize(piece.width, piece.height)
            rectToDraw.setLocation(piece.x, piece.getY(time))
            if(rectangleToDraw.intersects(clip))
                ((Graphics2D)g).fill(rectToDraw)
        }

        for(GamePiece piece : this.still) {
            rectToDraw.setSize(piece.width, piece.height)
            rectToDraw.setLocation(piece.x, piece.getY(time))
            if(rectangleToDraw.intersects(clip))
                ((Graphics2D)g).fill(rectToDraw)
        }
    }

    private class TimerListener implements ActionListener
    {
        @Override
        public void actionPerformed(ActionEvent e)
        {
            long time = System.currentTimeMillis();
            // Using non-iterator loop to move the pieces that
            // stopped safely. Iterator would crash on in-loop move.
            for(int i = 0; i < moving.size(); i++) {
                piece = moving.get(i);
                if(piece.getY(time) > 440 - piece.height) {
                    moving.remove(i);
                    still.add(piece);
                    i--;
                }
            }
            repaint();
        }
    }
}

public class GamePiece
{
    public final int width;
    public final int height;
    public final long startTime;
    public int x;

    public GamePiece(int width, int height, int x)
    {
        this.width = width;
        this.height = height;
        this.startTime = System.currentTimeMillis();
        this.x = x;
    }

    public int getY(long time)
    {
        // This hard-codes a velocity of 10px/sec. You could
        // implement a more complex relationship with time here.
        return (int)((time - this.startTime) / 100.0);
    }
}

关于java - 在JFrame中显示移动 block ,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/41047521/

相关文章:

java - 是否可以读取正在运行的 java 应用程序的内存?

java - MapReduce 以编程方式获取当前作业 ID

java - 双击 vs java -jar MyJar.jar

java - 如何将 Java 中的二维矩阵映射到 Hibernate/JPA?

java - 协助关键事件

java - 获取我的文本区域的值

java - 以编程方式对 JTable 进行排序

java - 撤消 java mkdirs() 的效果

java - JTextField 中的数字数组 - Java

java - 为什么这个线程代码不能在 GUI 中正常工作? [Java Swing] [线程]