Java Swing双缓冲

3
我刚开始使用双缓冲,一切都很好,直到我想在屏幕上添加一个JScrollPane以便之后进行一些摄像机移动。所有东西都正常显示(我的精灵),除了JScrollPane的滚动条。但是我希望它们被显示出来!
然而,如果我调整窗口大小,则滚动条会闪烁,所以我知道它们存在!如果我足够快,甚至可以使用它们。为什么它们在渲染时不显示呢? :(
这是一个问题的SSCCE:
public class BufferStrategyDemo extends JFrame
{
    private BufferStrategy bufferStrategy;
    private JPanel gameField;
    private JScrollPane scroll;

    public BufferStrategyDemo()
    {

        this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);        
        this.getContentPane().setPreferredSize(new Dimension(800, 600));
        this.pack();
        this.createBufferStrategy(2);

        this.gameField = new JPanel();
        this.gameField.setPreferredSize(new Dimension( 1400, 600 ));

        this.scroll = new JScrollPane( gameField );
        this.add( scroll, BorderLayout.CENTER );

        this.setVisible( true );
        this.bufferStrategy = this.getBufferStrategy();

        setupGameFieldContent();

        new Renderer( this, scroll , bufferStrategy ).start();
    }

    // Add some contents to gameField that shows up fine
    private void setupGameFieldContent()
    {
        // For ( all my sprites )
        //      gameField.add( sprite );

    }

    private class Renderer extends Thread
    {
        private BufferStrategy bufferStrategy;
        private JFrame window;
        private JScrollPane scroll;
        private Image imageOfGameField;

        public Renderer( JFrame window, JScrollPane scroll, BufferStrategy bufferStrategy )
        {
            this.bufferStrategy = bufferStrategy;
            this.window = window;
            this.scroll = scroll;
        }

        public void run()
        {
            while ( true )
            {
                Graphics g = null;
                try 
                {
                    g = bufferStrategy.getDrawGraphics();
                    drawSprites(g);
                } 
                finally { g.dispose(); }

                bufferStrategy.show(); // Shows everything except the scrollbars..
                Toolkit.getDefaultToolkit().sync(); 

                try { Thread.sleep( 1000/60 ) ; } 
                catch(InterruptedException ie) {}
            }
        }

        private void drawSprites( Graphics g )
        {
            Graphics2D g2d=(Graphics2D)g;

            //Also tried using the JPanels (gameField) createImage with same result
            imageOfGameField = this.scroll.createImage(800, 600 ); 
            Graphics screen = (Graphics2D)imageOfGameField.getGraphics();

            /**
             * For all my sprites, draw on the image
            for ( Sprite current : sprites )
                screen.drawImage( current.getImage(), current.getX(), current.getY(), current.getWidth() , current.getHeight(), null);
            */

            g2d.drawImage( imageOfGameField, 0, 0 , null );

        }
    }
}

创建一个游戏,其中这些尺寸是当前级别的宽度和高度。我想在屏幕上显示800个像素,然后如果玩家到达任何边缘,则可以进一步滚动。 - Lucas Arrefelt
3个回答

5
绘图是在整个框架区域上执行的,而不是在“gameField”上执行的。实际上,临时出现“JScrollPane”的唯一原因是它在鼠标拖动处理程序中调用了“paintImmediately”。首先想到的是用“I_Love_Thinking”所写的方法替换“JPanel”为“Canvas”,然后从该画布实例获取“bufferStategy”并用于渲染。但不幸的是,它不能按预期工作。轻量级“JScrollPane”无法正确驱动重量级“Canvas”。Canvas拒绝在区域(0,0,viewport_width,viewport_height)之外绘制内容。这是已知的bug,它将不会修复。混合使用重量级组件和轻量级组件是一个坏主意。将“JScrollPane”替换为重量级{{link2: ScrollPane }}似乎有效。几乎。在某些情况下,滚动条上有明显的渲染伪影。
此时,我决定停止打破墙壁并放弃。滚动窗格是许多问题的源头,不值得用于懒惰滚动。现在是从另一个角度看滚动的时候了。 Canvas本身不是游戏场地,而是我们用来观察游戏世界的窗口。这是游戏开发中众所周知的渲染策略。画布的大小正好是可观察区域的大小。当需要滚动时,我们只需使用一些偏移量渲染世界。偏移量的值可以从某个外部滚动条中获取。
我建议进行以下更改:
  1. 放弃JScrollPane
  2. 直接将画布添加到框架中。
  3. 在画布下方添加单个水平JScrollBar
  4. 使用滚动条的值来进行渲染偏移。
该示例基于您的代码。此实现未考虑帧调整大小。在现实生活中,有些地方需要与视口大小同步(这就是之前为我们完成的滚动窗格所做的)。此外,示例违反了Swing线程规则,并从渲染线程中读取滚动条的值。
import java.awt.BorderLayout;
import java.awt.Canvas;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Toolkit;
import java.awt.image.BufferStrategy;
import java.awt.image.BufferedImage;

import javax.swing.JFrame;
import javax.swing.JScrollBar;

public class BufferStrategyDemo extends JFrame {
    private BufferStrategy bufferStrategy;
    private Canvas gameField;
    private JScrollBar scroll;

    public BufferStrategyDemo() {
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        getContentPane().setLayout(new BorderLayout());
        getContentPane().setPreferredSize(new Dimension(800, 600));

        gameField = new Canvas();
        gameField.setIgnoreRepaint(true);
        gameField.setPreferredSize(new Dimension(800, 580));
        getContentPane().add(gameField, BorderLayout.CENTER);

        scroll = new JScrollBar(JScrollBar.HORIZONTAL);
        scroll.setPreferredSize(new Dimension(800, 20));
        scroll.setMaximum(1400 - 800); // image width - viewport width
        getContentPane().add(scroll, BorderLayout.SOUTH);

        this.pack();

        gameField.createBufferStrategy(2);
        bufferStrategy = gameField.getBufferStrategy();

        new Renderer().start();
    }

    private class Renderer extends Thread {
        private BufferedImage imageOfGameField;

        public Renderer() {
            // NOTE: image size is fixed now, but better to bind image size to the size of viewport
            imageOfGameField = new BufferedImage(1400, 580, BufferedImage.TYPE_INT_ARGB);
        }

        public void run() {
            while (true) {
                Graphics g = null;
                try {
                    g = bufferStrategy.getDrawGraphics();
                    drawSprites(g);
                } finally {
                    g.dispose();
                }

                bufferStrategy.show(); 
                Toolkit.getDefaultToolkit().sync();

                try {
                    Thread.sleep(1000 / 60);
                } catch (InterruptedException ie) {
                }
            }
        }

        private void drawSprites(Graphics g) {
            Graphics2D g2d = (Graphics2D) g;

            Graphics g2d2 = imageOfGameField.createGraphics();
            g2d2.setColor(Color.YELLOW); // clear background
            g2d2.fillRect(0, 0, 1400, 580); // again, fixed width/height only for SSCCE
            g2d2.setColor(Color.BLACK);

            int shift = -scroll.getValue(); // here it is - get shift value

            g2d2.fillRect(100 + shift, 100, 20, 20); // i am ugly black sprite
            g2d2.fillRect(900 + shift, 100, 20, 20); // i am other black sprite
                                                     // located outside of default view

            g2d.drawImage(imageOfGameField, 0, 0, null);

            g2d2.dispose();
        }
    }

    public static void main(String[] args) {
        EventQueue.invokeLater(new Runnable() {
            public void run() {
                BufferStrategyDemo mf = new BufferStrategyDemo();
                mf.setVisible(true);
            }
        });
    }
}

还有另一个例子,主要基于Java Games: Active Rendering文章中的代码。它正确地与JScrollBar的值和Canvas的大小同步。此外,它直接绘制到从BufferStrategy实例获得的Graphics(而不是中间的BuffereImage对象)。结果类似于这样:

Active Rendering Demo


1
这是一篇很棒的Java游戏:主动渲染文章。它包含了一个漂亮的带离屏图像的主动渲染演示。你可以将渲染部分作为模板,因为它产生的开销较小 - 渲染循环中只包含真正必要的操作。祝好运 :) - lxbndr
画布获取帧的尺寸,即使它的getX()是正确的。如果我调整帧的大小,如果 canvas < MAX_X,则画布会更新到新的大小。 - Lucas Arrefelt
整个方法都是错误的。JScrollPane 无法正确驱动 Canvas。我已经更新了我的答案。 - lxbndr
是的,那是我尝试的第一件事。重量级的ScrollPane和重量级的Canvas听起来不错。但不幸的是,当另一个滚动条由于框架调整大小而消失时,我看到了一个滚动条的绘图问题。我们想使用滚动窗格使事情变得容易。但相反,它成为问题的根源。我已经决定停止重新发明轮子,并使用在游戏开发中广为人知的方法。 - lxbndr
1
说实话,在这种简单的情况下,我不认为使用那个图像有任何理由。直接使用从缓冲策略中获取的图形应该更快。但是,使用图像可以轻松地进行一些后处理(例如在回合结束后平滑黑屏、各种艺术扭曲、程序截图保存等)。通常,BufferedImage 对于所有像素级操作都很有用。 - lxbndr
显示剩余9条评论

1

你的游戏在滚动窗格上绘制
调整窗口大小会显示你的滚动窗格一段时间,因为它调用了普通的绘图方法(没有双缓冲)
你有一些可能性...第二个方案更漂亮,但这是你的选择,我不知道你想做什么...

如果你想自己控制jframe中的所有组件:

  • 覆盖paint(Graphics g)方法,让它为空
  • 在你的绘图循环(方法'run')中包括滚动窗格的绘制

如果你想在另一个组件上使用双缓冲,而不必直接管理其他组件。
最适合此操作的组件是Canvas。它已经有了可以使用的双缓冲方法。
很久以前我做过这个,所以我无法准确地告诉你它是如何工作的。但只要查找一下,它并不难。


你是指覆盖JFrame的paint方法还是JPanel(gameField)的?至于第二个选项,我认为通过从滚动条中获取图像,scroll.createImage(..)就可以实现了。 - Lucas Arrefelt

1

一切都很好。我猜这可能会给我带来未来的问题,但它不是滚动条未显示的主要原因。 - Lucas Arrefelt
@Devon_C_Miller:Hasslam尝试利用“主动渲染”范例。虽然它不像传统的被动渲染那样广泛应用,但它确实有效。大多数问题出现在将常规Swing组件与手工渲染组件混合使用时。 - lxbndr
这就是我喜欢 Stack Overflow 的原因。我总是能学到新东西!我之前没有意识到“主动渲染”可以绕过 EDT 规则。 - Devon_C_Miller

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接