0

I have a really annoying bug in my game, the bottom of the frame seems to render earlier than the top of the frame, I'm not sure why it's happening.

I am using a JPanel which does repaint every game loop, my game loop is set to 60FPS. At the start of the paint function, it sets the player X and Y to a variable, which is then used to paint each element, (Since they are painted relative to the player, since the camera follows the player)

I can post any code if needed, to help diagnose the problem, but there is too much code, and I don't know which part of it is the problem; So I am mainly asking if anyone has any idea of what could be wrong, from my explanation.

I cannot post a video of the problem, since it doesn't pick up on video, however feel free to see it yourself in the game, link to game, and virus scan here

If you download the game, then when you open it, enter a name (or leave default) and click no when it asks about a server. When you move around with WASD, you should see a horizontal line flickering effect somewhere on the screen. If the game doesn't open, try again, there is a small chance of it being unable to open (This is a known bug, and im planning to fix it soon)

Sorry for the bad explanation, I find it difficult to describe my problem. I have been stuck with this for hours, and cannot find a solution even after searching the internet for one.

EDIT: Entire Source Code: Here

EDIT2: It requires the kryonet lib, located here

EDIT3: Github

  • 1
    See if you can produce a [runnable example](https://stackoverflow.com/help/mcve) which demonstrates your problem. This will result in less confusion and better responses. Links to compiled code really aren't help – MadProgrammer Aug 21 '14 at 06:44
  • @MadProgrammer The code for my game is rather large, and I think most of it is irrelevant to my problem, I'm probably wrong, but I don't think its appropriate to provide a runnable example.. – Ryan Ramsden Aug 21 '14 at 06:48
  • 1
    Well...unless we can determine how you are actually painting and how the program is generally put together, there is no hope for us to be able to do more than make wild guess. Painting in Swing is handled by the `RepaintManager` and is done from within the context of the Event Dispatching Thread, so unless you're doing something strange, the problem you have described shouldn't happen. But if you feel a runnable example would be a waste of time, that is, of course your choice... – MadProgrammer Aug 21 '14 at 06:51
  • 1
    @RyanRamsden Why would it be wrong to provide a runnable example? It helps others to faster reproduce your problem and faster find the solution. Just don't post your whole application but a Minimal, Complete and Verifiable example: http://stackoverflow.com/help/mcve – icza Aug 21 '14 at 06:52
  • @MadProgrammer Thankyou very much for helping me so far, I'm beyond confused by this problem, so I will post all of my source code. I am begging someone to help me here. The source code is in the OP. – Ryan Ramsden Aug 21 '14 at 06:54
  • My first recommendation would be not to put all you painting logic into the `paintComponent` method, instead, create two `BufferedImage`s, which will need to be the same size as `Frame`, so you will need to monitor for changes. Basically one will be the "active" buffer, which is rendered to the screen and one will be "update" buffer which you will paint within the game loop. When you are ready (instead of calling `frame.repaint`) you would swap the buffers, may be using a method that takes a `BufferedImage` and returns one. The returned `BufferedImage` becomes the new update buffer – MadProgrammer Aug 21 '14 at 07:17
  • You would need to synchronize the swap, so that the active is not actively been painted as you try and reassign it. Nice job by the way ;). I would encourage you to use key bindings over `KeyListener`, but otherwise, it looks quite good – MadProgrammer Aug 21 '14 at 07:19
  • @MadProgrammer as a novice programmer, your 'Nice job' means the world to me, thankyou very much; however, I have trouble understanding your solution. I created 2 BufferedImages, and I am setting the active to the buffer, 'frame.repaint()' and then updating the game, and then doing 'frame.paintGame()' onto the buffer. However this gives me a flickering effect. Where should I put 'frame.repaint()' using your solution, and how would I synchronize the swap? – Ryan Ramsden Aug 21 '14 at 07:46
  • Request a repaint within the synchronized swap portion. This way you only request a change when the buffers have changed. You could also use `VolitileImage` image instead of `BufferedImage`, but the management becomes more complex – MadProgrammer Aug 21 '14 at 08:41
  • @MadProgrammer I'm really sorry, but as a novice programmer I have a really hard time understanding your solution, not because of your explanation, but because of my in-capabilities in properly understanding synchronization. [This is my failed attempt.](http://pastebin.com/Le4PerSs) – Ryan Ramsden Aug 21 '14 at 08:51

4 Answers4

3

The is a demonstration of two basic principles, but is basically a series of buffers designed to reduce the amount of work that the paintComponent does...

Generally speaking, it is faster to BLIT a image onto the graphics card then it is to "paint" pixels, with this in mind, this example does two things...

Firstly, it pre-renders the background map. This example just randomly generates the map when it's run, but creates a map which is about 4 times that of full HD.

Secondly, it employees it's own double buffering. The "view" has two buffers, an active and an update. The active buffer is what gets painted to the screen, the update buffer is what's used by the Engine to render the current state of the output...

This is important, because the view's buffers are ALWAYS the same size of the view, so you are never rendering anything that doesn't appear off the screen.

This example pushes the rendering of additional content (like animation, special effects) to the Engine...

I had this example running on my 30" monitor at 2560x1600 with very little issues, the movement delta is very small so I can pan faster, making it large would have nullified these issues...

import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.HashSet;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.locks.ReentrantLock;
import javax.imageio.ImageIO;
import javax.swing.AbstractAction;
import javax.swing.ActionMap;
import javax.swing.InputMap;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.KeyStroke;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;

public class TestRender {

    public static void main(String[] args) {
        new TestRender();
    }

    public TestRender() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                try {
                    UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
                } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
                    ex.printStackTrace();
                }

                JFrame frame = new JFrame("Testing");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.add(new TestPane());
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

    public interface View {

        public BufferedImage switchBuffers();
        public int getWidth();
        public int getHeight();

    }

    public enum KeyState {
        UP, DOWN, LEFT, RIGHT;
    }

    public class TestPane extends JPanel implements View {

        private Engine engine;

        private BufferedImage active;
        private BufferedImage update;

        private ReentrantLock lckBuffer;

        public TestPane() {
            lckBuffer = new ReentrantLock();
            initBuffers();
            engine = new Engine(this);
            engine.gameStart();

            InputMap im = getInputMap(WHEN_IN_FOCUSED_WINDOW);
            im.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0, false), "up_pressed");
            im.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0, false), "down_pressed");
            im.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0, false), "left_pressed");
            im.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0, false), "right_pressed");
            im.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0, true), "up_released");
            im.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0, true), "down_released");
            im.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0, true), "left_released");
            im.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0, true), "right_released");

            ActionMap am = getActionMap();
            am.put("up_pressed", new AddState(engine, KeyState.UP));
            am.put("up_released", new RemoveState(engine, KeyState.UP));
            am.put("down_pressed", new AddState(engine, KeyState.DOWN));
            am.put("down_released", new RemoveState(engine, KeyState.DOWN));
            am.put("left_pressed", new AddState(engine, KeyState.LEFT));
            am.put("left_released", new RemoveState(engine, KeyState.LEFT));
            am.put("right_pressed", new AddState(engine, KeyState.RIGHT));
            am.put("right_released", new RemoveState(engine, KeyState.RIGHT));
        }

        protected void initBuffers() {
            if (getWidth() > 0 && getHeight() > 0) {
                try {
                    lckBuffer.lock();
                    active = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_ARGB);
                    update = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_ARGB);
                } finally {
                    lckBuffer.unlock();
                }
            }
        }

        @Override
        public void invalidate() {
            super.invalidate();
            initBuffers();
        }

        @Override
        public Dimension getPreferredSize() {
            return new Dimension(1920, 1080);
        }

        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            Graphics2D g2d = (Graphics2D) g.create();
            try {
                lckBuffer.lock();
                if (active != null) {
                    g2d.drawImage(active, 0, 0, this);
                }
            } finally {
                lckBuffer.unlock();
            }
            g2d.dispose();
        }

        @Override
        public BufferedImage switchBuffers() {
            try {
                lckBuffer.lock();
                BufferedImage tmp = active;
                active = update;
                update = tmp;
                repaint();
            } finally {
                lckBuffer.unlock();
            }
            return update;
        }

    }

    public static class Engine {

        public static final int MAP_WIDTH = 15 * 4;
        public static final int MAP_HEIGHT = 9 * 4;
        public static final int X_DELTA = 32;
        public static final int Y_DELTA = 32;

        //This value would probably be stored elsewhere.
        public static final double GAME_HERTZ = 60.0;
        //Calculate how many ns each frame should take for our target game hertz.
        public static final double TIME_BETWEEN_UPDATES = 1000000000 / GAME_HERTZ;
        //We will need the last update time.
        static double lastUpdateTime = System.nanoTime();
        //Store the last time we rendered.
        static double lastRenderTime = System.nanoTime();

        //If we are able to get as high as this FPS, don't render again.
        final static double TARGET_FPS = GAME_HERTZ;
        final static double TARGET_TIME_BETWEEN_RENDERS = 1000000000 / TARGET_FPS;

        //Simple way of finding FPS.
        static int lastSecondTime = (int) (lastUpdateTime / 1000000000);

        public static int fps = 60;
        public static int frameCount = 0;

        private boolean isGameFinished;

        private BufferedImage map;
        private BufferedImage tiles[];

        private View view;

        private int camX, camY;
        private Set<KeyState> keyStates;

        public Engine(View bufferRenderer) {
            keyStates = new HashSet<>(4);
            this.view = bufferRenderer;
            tiles = new BufferedImage[7];
            Random rnd = new Random();
            map = new BufferedImage(MAP_WIDTH * 128, MAP_HEIGHT * 128, BufferedImage.TYPE_INT_ARGB);
            Graphics2D g2d = map.createGraphics();
            for (int row = 0; row < MAP_HEIGHT; row++) {
                for (int col = 0; col < MAP_WIDTH; col++) {
                    int tile = rnd.nextInt(7);
                    int x = col * 128;
                    int y = row * 128;
                    g2d.drawImage(getTile(tile), x, y, null);
                }
            }
            g2d.dispose();
        }

        protected BufferedImage getTile(int tile) {
            BufferedImage img = tiles[tile];
            if (img == null) {
                try {
                    img = ImageIO.read(getClass().getResource("/" + tile + ".png"));
                    img = img.getSubimage(0, 64, 128, 128);
                } catch (IOException ex) {
                    ex.printStackTrace();
                }
                tiles[tile] = img;
            }
            return img;
        }

        public void gameStart() {

            Thread gameThread = new Thread() {
                // Override run() to provide the running behavior of this thread.
                @Override
                public void run() {
                    gameLoop();
                }
            };
            gameThread.setDaemon(false);
            // Start the thread. start() calls run(), which in turn calls gameLoop().
            gameThread.start();
        }

        public void gameLoop() {
            BufferedImage buffer = view.switchBuffers(); // initial buffer...
            while (!isGameFinished) {
                double now = System.nanoTime();
                lastUpdateTime += TIME_BETWEEN_UPDATES;
                gameUpdate(buffer);
                renderBuffer(buffer);
                buffer = view.switchBuffers(); // Push the buffer back
                frameCount++;
                lastRenderTime = now;

                int thisSecond = (int) (lastUpdateTime / 1000000000);
                if (thisSecond > lastSecondTime) {
                    fps = frameCount;
                    frameCount = 0;
                    lastSecondTime = thisSecond;
                }

                //Yield until it has been at least the target time between renders. This saves the CPU from hogging.
                while (now - lastRenderTime < TARGET_TIME_BETWEEN_RENDERS && now - lastUpdateTime < TIME_BETWEEN_UPDATES) {
                //Thread.yield();

                    //This stops the app from consuming all your CPU. It makes this slightly less accurate, but is worth it.
                    //You can remove this line and it will still work (better), your CPU just climbs on certain OSes.
                    //FYI on some OS's this can cause pretty bad stuttering. Scroll down and have a look at different peoples' solutions to this.
                    try {
                        Thread.sleep(1);
                    } catch (Exception e) {
                    }

                    now = System.nanoTime();
                }
            }
        }

        protected void renderBuffer(BufferedImage buffer) {
            if (buffer != null) {
                Graphics2D g2d = buffer.createGraphics();
                g2d.drawImage(map, camX, camY, null);
                g2d.dispose();
            }
        }

        protected void gameUpdate(BufferedImage buffer) {
            // render transient effects here
            if (keyStates.contains(KeyState.DOWN)) {
                camY -= Y_DELTA;
            } else if (keyStates.contains(KeyState.UP)) {
                camY += Y_DELTA;
            }
            if (camY < -(map.getHeight() - view.getHeight())) {
                camY = -(map.getHeight() - view.getHeight());
            } else if (camY > 0) {
                camY = 0;
            }
            if (keyStates.contains(KeyState.RIGHT)) {
                camX -= Y_DELTA;
            } else if (keyStates.contains(KeyState.LEFT)) {
                camX += Y_DELTA;
            }
            if (camX < -(map.getWidth() - view.getWidth())) {
                camX = -(map.getWidth() - view.getWidth());
            } else if (camX > 0) {
                camX = 0;
            }
        }

        public void addKeyState(KeyState state) {
            keyStates.add(state);
        }

        public void removeKeyState(KeyState state) {
            keyStates.remove(state);
        }

    }

    public class AddState extends AbstractAction {

        private Engine engine;
        private KeyState state;

        public AddState(Engine engine, KeyState state) {
            this.engine = engine;
            this.state = state;
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            engine.addKeyState(state);
        }

    }

    public class RemoveState extends AbstractAction {

        private Engine engine;
        private KeyState state;

        public RemoveState(Engine engine, KeyState state) {
            this.engine = engine;
            this.state = state;
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            engine.removeKeyState(state);
        }

    }

}

During my experimentation, I did notice that, if you tried to render the content "beyond" the range of the buffer (ie allow the top of the map to slip down inside the buffer area), you would get nasty paint effects, so beware that you are always rendering within the viewable area of the buffer...

There are probably other areas that need tidying up, but this demonstrates the basics...

MadProgrammer
  • 343,457
  • 22
  • 230
  • 366
  • Wow! I really wasn't expecting someone to put so much effort into trying to solve my problem, I really appreciate the effort you have put in for me so far; however, I ran the code you posted, and it still shows the same problem i'm having.. – Ryan Ramsden Aug 21 '14 at 11:05
  • I uploaded a video from running your code, demonstrating that the problem still persists [link](https://www.youtube.com/watch?v=sD198_inpL8), just incase you wasn't getting the problem from your setup. Again, thankyou so much for putting this much effort into my answer! – Ryan Ramsden Aug 21 '14 at 11:14
  • It could be hardware issue. You could try using a `VolitileImage`, but that would require significant changes to the rendering pipeline – MadProgrammer Aug 21 '14 at 12:09
  • Again, I cannot thankyou enough for the time you have spent helping me with this. I've stayed up all night trying to work this problem out, so I will be going to bed soon, if it's possible could you link me to somewhere I can learn about VolitileImages and how I should go about implementing them into my game. Thankyou very much. – Ryan Ramsden Aug 21 '14 at 12:26
  • @RyanRamsden I ended up having to do another example in another answer, run out over limit. It has some links in, see what you think... – MadProgrammer Aug 22 '14 at 06:32
1

Sorry, run out room in my previous answer :P

Update with VolitileImage example

Now, before you get to excited, my experience with VolitileImage is about 20mins more than it took me to create this example, so it might not be the best possible example...

Scrolling

import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GraphicsConfiguration;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.Transparency;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.awt.image.BufferedImage;
import java.awt.image.VolatileImage;
import java.io.IOException;
import java.util.HashSet;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
import javax.imageio.ImageIO;
import javax.swing.AbstractAction;
import javax.swing.ActionMap;
import javax.swing.InputMap;
import static javax.swing.JComponent.WHEN_IN_FOCUSED_WINDOW;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.KeyStroke;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;

public class TestVolitile {

    public static void main(String[] args) {
        new TestVolitile();
    }

    public TestVolitile() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                try {
                    UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
                } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
                    ex.printStackTrace();
                }

                JFrame frame = new JFrame("Testing");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.add(new ViewPane());
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

    public interface View {

        public VolatileImage getOffscreenBuffer();

        public void show(VolatileImage img);

        public boolean isIncompatiable(VolatileImage img);

        public int getWidth();

        public int getHeight();

    }

    public enum KeyState {

        UP, DOWN, LEFT, RIGHT;
    }

    public class ViewPane extends JPanel implements View {

        private VolatileImage offscreen;
        private BufferedImage onscreen;
        private ReentrantLock lckBuffers;

        private Engine engine;

        public ViewPane() {
            lckBuffers = new ReentrantLock();

            engine = new Engine(this);
            engine.gameStart();

            InputMap im = getInputMap(WHEN_IN_FOCUSED_WINDOW);
            im.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0, false), "up_pressed");
            im.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0, false), "down_pressed");
            im.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0, false), "left_pressed");
            im.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0, false), "right_pressed");
            im.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0, true), "up_released");
            im.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0, true), "down_released");
            im.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0, true), "left_released");
            im.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0, true), "right_released");

            ActionMap am = getActionMap();
            am.put("up_pressed", new AddState(engine, KeyState.UP));
            am.put("up_released", new RemoveState(engine, KeyState.UP));
            am.put("down_pressed", new AddState(engine, KeyState.DOWN));
            am.put("down_released", new RemoveState(engine, KeyState.DOWN));
            am.put("left_pressed", new AddState(engine, KeyState.LEFT));
            am.put("left_released", new RemoveState(engine, KeyState.LEFT));
            am.put("right_pressed", new AddState(engine, KeyState.RIGHT));
            am.put("right_released", new RemoveState(engine, KeyState.RIGHT));
        }

        @Override
        public void invalidate() {
            super.invalidate();
            onscreen = null;
//            offscreen = null;
        }

        @Override
        public Dimension getPreferredSize() {
            return new Dimension(200, 200);
        }

        @Override
        protected void paintComponent(Graphics g) {

            super.paintComponent(g);
            try {
                lckBuffers.lock();
                // Make sure the buffer is okay for painting....
                if (onscreen != null) {
                    Graphics2D g2d = (Graphics2D) g.create();
                    g2d.drawImage(onscreen, 0, 0, this);
                    g2d.dispose();
                }
            } finally {
                lckBuffers.unlock();
            }

        }

        protected VolatileImage createVolatileImage(int width, int height, int transparency) {

            GraphicsConfiguration gc = getGraphicsConfiguration();
            VolatileImage image = null;

            if (gc != null && width > 0 && height > 0) {

                image = gc.createCompatibleVolatileImage(width, height, transparency);

                int valid = image.validate(gc);

                if (valid == VolatileImage.IMAGE_INCOMPATIBLE) {

                    image = this.createVolatileImage(width, height, transparency);

                }

            }

            return image;

        }

        @Override
        public VolatileImage getOffscreenBuffer() {
            if (isIncompatiable(offscreen)) {
                offscreen = createVolatileImage(getWidth(), getHeight(), Transparency.TRANSLUCENT);
            }

            return offscreen;
        }

        @Override
        public void show(VolatileImage img) {

            try {
                lckBuffers.lock();
                GraphicsConfiguration gc = getGraphicsConfiguration();
                if (gc != null) {
                    if (onscreen == null) {
                        onscreen = gc.createCompatibleImage(getWidth(), getHeight(), Transparency.TRANSLUCENT);
                    }
                    if (isOkay(img)) {
                        Graphics2D g2d = onscreen.createGraphics();
                        g2d.drawImage(img, 0, 0, this);
                        g2d.dispose();
                        repaint();
                    }
                }
            } finally {
                lckBuffers.unlock();
            }

        }

        @Override
        public boolean isIncompatiable(VolatileImage offscreen) {

            boolean isIncompatiable = true;
            GraphicsConfiguration gc = getGraphicsConfiguration();
            if (gc != null) {
                if (offscreen != null) {
                    if (offscreen.getWidth() == getWidth() && offscreen.getHeight() == getHeight()) {
                        if (offscreen.validate(gc) != VolatileImage.IMAGE_INCOMPATIBLE) {
                            isIncompatiable = false;
                        }
                    }
                }
            }

            return isIncompatiable;

        }

        public boolean isOkay(VolatileImage buffer) {

            boolean isOkay = false;
            GraphicsConfiguration gc = getGraphicsConfiguration();
            if (gc != null) {
                if (buffer != null) {
                    if (buffer.getWidth() == getWidth() && buffer.getHeight() == getHeight()) {
                        if (buffer.validate(gc) == VolatileImage.IMAGE_OK) {
                            isOkay = true;
                        }
                    }
                }
            }

            return isOkay;

        }
    }

    public static class Engine {

        public static final int MAP_WIDTH = 15 * 4;
        public static final int MAP_HEIGHT = 9 * 4;
        public static final int X_DELTA = 4;
        public static final int Y_DELTA = 4;

        public boolean isGameFinished = false;

        //This value would probably be stored elsewhere.
        public static final long GAME_HERTZ = 25;
        //Calculate how many ns each frame should take for our target game hertz.
        public static final long TIME_BETWEEN_UPDATES = Math.round(1000000000 / (double) GAME_HERTZ);
        //We will need the last update time.
        static long lastUpdateTime = System.nanoTime();
        //Store the last time we rendered.
        static long lastRenderTime = System.nanoTime();

        //If we are able to get as high as this FPS, don't render again.
        final static long TARGET_FPS = GAME_HERTZ;
        final static long TARGET_TIME_BETWEEN_RENDERS = Math.round(1000000000 / (double) TARGET_FPS);

        //Simple way of finding FPS.
        static int lastSecondTime = (int) (lastUpdateTime / 1000000000);

        public int fps = 60;
        public int frameCount = 0;

        private View view;
        private int camX, camY;
        private Set<KeyState> keyStates;

        private BufferedImage map;
        private BufferedImage tiles[];

        public Engine(View view) {
            this.view = view;
            keyStates = new HashSet<>(4);
            tiles = new BufferedImage[22];
            Random rnd = new Random();
            GraphicsConfiguration gc = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getDefaultConfiguration();
            map = gc.createCompatibleImage(MAP_WIDTH * 128, MAP_HEIGHT * 128, Transparency.TRANSLUCENT)

            Graphics2D g2d = map.createGraphics();
            for (int row = 0; row < MAP_HEIGHT; row++) {
                for (int col = 0; col < MAP_WIDTH; col++) {
                    int tile = rnd.nextInt(22);
                    int x = col * 128;
                    int y = row * 128;
                    g2d.drawImage(getTile(tile), x, y, null);
                }
            }
            g2d.dispose();
        }

        protected BufferedImage getTile(int tile) {
            BufferedImage img = tiles[tile];
            if (img == null) {
                try {
                    img = ImageIO.read(getClass().getResource("/" + tile + ".png"));
                    img = img.getSubimage(0, 64, 128, 128);
                    img = toCompatiableImage(img);
                } catch (IOException ex) {
                    ex.printStackTrace();
                }
                tiles[tile] = img;
            }
            return img;
        }

        public void gameStart() {

            Thread gameThread = new Thread() {
                // Override run() to provide the running behavior of this thread.
                @Override
                public void run() {
                    gameLoop();
                }
            };
            // Start the thread. start() calls run(), which in turn calls gameLoop().
            gameThread.start();
        }

        public void gameLoop() {
            while (!isGameFinished) {
                long startTime = System.nanoTime();
                lastUpdateTime += TIME_BETWEEN_UPDATES;
                updateGame();
                renerGame();
                frameCount++;
                lastRenderTime = startTime;

                long duration = System.nanoTime() - startTime;

                int thisSecond = (int) (lastUpdateTime / 1000000000);
                if (thisSecond > lastSecondTime) {
                    fps = frameCount;
                    frameCount = 0;
                    lastSecondTime = thisSecond;
                }

                if (duration < TARGET_TIME_BETWEEN_RENDERS) {

                    duration = TARGET_TIME_BETWEEN_RENDERS - duration;
                    long milli = TimeUnit.NANOSECONDS.toMillis(duration);
                    try {
                        Thread.sleep(milli);
                    } catch (InterruptedException ex) {
                    }
                }
            }
        }

        protected void updateGame() {
            if (keyStates.contains(KeyState.DOWN)) {
                camY -= Y_DELTA;
            } else if (keyStates.contains(KeyState.UP)) {
                camY += Y_DELTA;
            }
            if (camY < -(map.getHeight() - view.getHeight())) {
                camY = -(map.getHeight() - view.getHeight());
            } else if (camY > 0) {
                camY = 0;
            }
            if (keyStates.contains(KeyState.RIGHT)) {
                camX -= Y_DELTA;
            } else if (keyStates.contains(KeyState.LEFT)) {
                camX += Y_DELTA;
            }
            if (camX < -(map.getWidth() - view.getWidth())) {
                camX = -(map.getWidth() - view.getWidth());
            } else if (camX > 0) {
                camX = 0;
            }
        }

        protected void renerGame() {
            VolatileImage buffer = view.getOffscreenBuffer();
            if (buffer != null) {
                Graphics2D g2d = null;
                do {

                    if (view.isIncompatiable(buffer)) {
                        buffer = view.getOffscreenBuffer();
                    }

                    try {
                        g2d = buffer.createGraphics();
                    } finally {
                        if (g2d != null) {
                            g2d.drawImage(map, camX, camY, null);
                            // Draw effects here...

                            FontMetrics fm = g2d.getFontMetrics();
                            g2d.setColor(Color.RED);
                            g2d.drawString(Integer.toString(fps), 0, fm.getAscent());
                            g2d.dispose();
                        }
                    }

                } while (buffer.contentsLost());

                view.show(buffer);
            }
        }

        public void addKeyState(KeyState state) {
            keyStates.add(state);
        }

        public void removeKeyState(KeyState state) {
            keyStates.remove(state);
        }

        protected BufferedImage toCompatiableImage(BufferedImage img) {
            GraphicsConfiguration gc = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getDefaultConfiguration();
            BufferedImage compImg = gc.createCompatibleImage(img.getWidth(), img.getHeight(), img.getTransparency());
            Graphics2D g2d = compImg.createGraphics();
            g2d.drawImage(img, 0, 0, null);
            g2d.dispose();
            return compImg;
        }

    }

    public class AddState extends AbstractAction {

        private Engine engine;
        private KeyState state;

        public AddState(Engine engine, KeyState state) {
            this.engine = engine;
            this.state = state;
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            engine.addKeyState(state);
        }

    }

    public class RemoveState extends AbstractAction {

        private Engine engine;
        private KeyState state;

        public RemoveState(Engine engine, KeyState state) {
            this.engine = engine;
            this.state = state;
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            engine.removeKeyState(state);
        }

    }
}

Now, if this still gives you trouble, you could try replacing...

g2d.drawImage(map, camX, camY, null);

with...

BufferedImage clip = map.getSubimage(camX * -1, camY * -1, view.getWidth(), view.getHeight());
g2d.drawImage(clip, 0, 0, null);

This reduces any possible "overhang" which might exist within the graphics buffer. Not 100% sure if it will make a difference, but it can't hurt. You could do this in both examples...

If you still have tearing/shearing occurring, you could investigate trying to disabling directx or opengl rendering pipelines (these are command line options) to see if it makes a difference...

Take a look at VolatileImage, The Game Programming Wiki, Java:Tutorials:VolatileImage and Java 2D: Hardware Accelerating - Part 1 - Volatile Images for more ideas.

I also changed your game loop "timing" loop a little, have NO idea if it will make a difference, but Thread.sleep(1) always scares me...

Updated

I've updated the code so that there is only a single VolatileImage. The paintComponent uses an actually BufferedImage, optimised to the GraphicsConfiguration instead. This ensures that contents is always painted an only update as required (within the show method). Should help prevent flickering...

I've also optimised all the tiles as they are loaded to be optimised for the GraphicsConfiguration as well, this means that their color models don't need to be converted when they are rendered to the screen as they are the same, should help save some time...

Take a look at the toCompatiableImage method for more details

MadProgrammer
  • 343,457
  • 22
  • 230
  • 366
  • Thanks so much for your answer, however, this still hasn't fixed the problem for me, it has stopped the tearing problem, but it causes a jerking/skipping effect. I have tried the different solutions you have posted, including disabling the directx and/or opengl pipelines. I used `-Dsun.java2d.d3d=false -Dsun.java2d.opengl=false` as a VM argument, this only decreased performance. I believe my computer should be more than capable of handling this too. (i7, GTX 570, 16GB RAM). – Ryan Ramsden Aug 22 '14 at 09:15
  • Okay, time to consider using a BufferStrategy then – MadProgrammer Aug 22 '14 at 09:16
  • Hay, I'm running this stuff on a 5+ year old computer with 8 gig ;) – MadProgrammer Aug 22 '14 at 09:17
  • How would I go about implementing a BufferStrategy? I've never used one before. Also, this seems like a lot of effort just to render some 2D sprites onto a screen, would you advise using a library instead? – Ryan Ramsden Aug 22 '14 at 09:20
  • A BufferStrategy is probably simpler then the two other options I've demonstrated. From where you are right now, I'd say stick with what you're doing, it's a great learning curve. What graphics card are you using? Is it embedded? – MadProgrammer Aug 22 '14 at 09:42
  • Thanks! I will look into BufferStrategy's then, and my graphics card is [this](http://www.kfa2.com/gtx570.shtml). I am unsure if it is embedded or not. – Ryan Ramsden Aug 22 '14 at 10:00
  • I was thinking, always helps, there are areas of opportunity for optimization in both examples, in fact, I shouldn't be painting the volatile image in the paintComponent method, I'll try and update that later, when I get time to look at BufferStrategy – MadProgrammer Aug 22 '14 at 21:29
0

JPanel does not wait for you to call repaint() when a change is made.

To prevent this, I think you can use RepaintManager as follows:-

RepaintManager.currentManager(yourJPanel).markCompletelyClean(yourJPanel)

There is another technique using two different JPanel instances. The basic idea is this:

  • Set one visible and other invisible.
  • Reflect the changes to the invisible (when invisible, panels does not update themselves)
  • When you finish your game loop iteration switch their visibility and start over

I don't know how the second one would affect the performance, though.

Seyf
  • 889
  • 8
  • 16
  • I have just tried your first solution, and unfortunately nothing has changed. I will try the second solution now, Thankyou very much for your answer though! – Ryan Ramsden Aug 21 '14 at 07:56
  • Can you try Component.setIgnoreRepaint(true) for your panel? The second one must be a little bit time consuming. – Seyf Aug 21 '14 at 08:04
  • I've added that in, underneath the `RepaintManager.currentManager(this).markCompletelyClean(this);` with still no luck. :( – Ryan Ramsden Aug 21 '14 at 08:06
  • Hey Ryan, how about the second solution? Have you implemented? – Seyf Aug 22 '14 at 06:27
0

And finally, a BufferStrategy implementation, this is as close to the hardware as you are going to get...

See BufferStrategy

Typically you do this, when you want COMPLETE control over the painting process...

import java.awt.BufferCapabilities;
import java.awt.Canvas;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GraphicsConfiguration;
import java.awt.GraphicsEnvironment;
import java.awt.Transparency;
import java.awt.event.ActionEvent;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.image.BufferStrategy;
import java.awt.image.BufferedImage;
import java.awt.image.VolatileImage;
import java.io.IOException;
import java.util.HashSet;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
import javax.imageio.ImageIO;
import javax.swing.AbstractAction;
import javax.swing.ActionMap;
import javax.swing.InputMap;
import static javax.swing.JComponent.WHEN_IN_FOCUSED_WINDOW;
import javax.swing.JFrame;
import javax.swing.KeyStroke;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;

public class TestVolitile {

    public static void main(String[] args) {
        new TestVolitile();
    }

    public TestVolitile() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                try {
                    UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
                } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
                    ex.printStackTrace();
                }

                JFrame frame = new JFrame("Testing");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.add(new ViewPane());
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

    public interface View {

        public int getWidth();

        public int getHeight();

        public BufferStrategy getBufferStrategy();

    }

    public enum KeyState {

        UP, DOWN, LEFT, RIGHT;
    }

    public class ViewPane extends Canvas implements View {

        private VolatileImage offscreen;
        private BufferedImage onscreen;

        private Engine engine;

        public ViewPane() {

            engine = new Engine(this);
            engine.gameStart();

            setFocusable(true);
            requestFocusInWindow();
            addMouseListener(new MouseAdapter() {
                @Override
                public void mouseClicked(MouseEvent e) {
                    requestFocusInWindow();
                }
            });

            addKeyListener(new KeyAdapter() {

                @Override
                public void keyPressed(KeyEvent e) {
                    switch (e.getKeyCode()) {
                        case KeyEvent.VK_UP:
                            engine.addKeyState(KeyState.UP);
                            break;
                        case KeyEvent.VK_DOWN:
                            engine.addKeyState(KeyState.DOWN);
                            break;
                        case KeyEvent.VK_LEFT:
                            engine.addKeyState(KeyState.LEFT);
                            break;
                        case KeyEvent.VK_RIGHT:
                            engine.addKeyState(KeyState.RIGHT);
                            break;
                    }
                }

                @Override
                public void keyReleased(KeyEvent e) {
                    switch (e.getKeyCode()) {
                        case KeyEvent.VK_UP:
                            engine.removeKeyState(KeyState.UP);
                            break;
                        case KeyEvent.VK_DOWN:
                            engine.removeKeyState(KeyState.DOWN);
                            break;
                        case KeyEvent.VK_LEFT:
                            engine.removeKeyState(KeyState.LEFT);
                            break;
                        case KeyEvent.VK_RIGHT:
                            engine.removeKeyState(KeyState.RIGHT);
                            break;
                    }
                }

            });

        }

        @Override
        public void addNotify() {
            super.addNotify();
            createBufferStrategy(3);
        }

        @Override
        public void invalidate() {
            super.invalidate();
            onscreen = null;
//            offscreen = null;
        }

        @Override
        public Dimension getPreferredSize() {
            return new Dimension(200, 200);
        }

    }

    public static class Engine {

        public static final int MAP_WIDTH = 15 * 4;
        public static final int MAP_HEIGHT = 9 * 4;
        public static final int X_DELTA = 4;
        public static final int Y_DELTA = 4;

        public boolean isGameFinished = false;

        //This value would probably be stored elsewhere.
        public static final long GAME_HERTZ = 25;
        //Calculate how many ns each frame should take for our target game hertz.
        public static final long TIME_BETWEEN_UPDATES = Math.round(1000000000 / (double) GAME_HERTZ);
        //We will need the last update time.
        static long lastUpdateTime = System.nanoTime();
        //Store the last time we rendered.
        static long lastRenderTime = System.nanoTime();

        //If we are able to get as high as this FPS, don't render again.
        final static long TARGET_FPS = GAME_HERTZ;
        final static long TARGET_TIME_BETWEEN_RENDERS = Math.round(1000000000 / (double) TARGET_FPS);

        //Simple way of finding FPS.
        static int lastSecondTime = (int) (lastUpdateTime / 1000000000);

        public int fps = 60;
        public int frameCount = 0;

        private View view;
        private int camX, camY;
        private Set<KeyState> keyStates;

        private BufferedImage map;
        private BufferedImage tiles[];

        public Engine(View view) {
            this.view = view;
            keyStates = new HashSet<>(4);
            tiles = new BufferedImage[22];
            Random rnd = new Random();
            GraphicsConfiguration gc = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getDefaultConfiguration();
            map = gc.createCompatibleImage(MAP_WIDTH * 128, MAP_HEIGHT * 128, Transparency.TRANSLUCENT);

            Graphics2D g2d = map.createGraphics();
            for (int row = 0; row < MAP_HEIGHT; row++) {
                for (int col = 0; col < MAP_WIDTH; col++) {
                    int tile = rnd.nextInt(22);
                    int x = col * 128;
                    int y = row * 128;
                    g2d.drawImage(getTile(tile), x, y, null);
                }
            }
            g2d.dispose();
        }

        protected BufferedImage getTile(int tile) {
            BufferedImage img = tiles[tile];
            if (img == null) {
                try {
                    img = ImageIO.read(getClass().getResource("/" + tile + ".png"));
                    img = img.getSubimage(0, 64, 128, 128);
                    img = toCompatiableImage(img);
                } catch (IOException ex) {
                    ex.printStackTrace();
                }
                tiles[tile] = img;
            }
            return img;
        }

        public void gameStart() {

            Thread gameThread = new Thread() {
                // Override run() to provide the running behavior of this thread.
                @Override
                public void run() {
                    gameLoop();
                }
            };
            // Start the thread. start() calls run(), which in turn calls gameLoop().
            gameThread.start();
        }

        public void gameLoop() {
            while (!isGameFinished) {
                long startTime = System.nanoTime();
                lastUpdateTime += TIME_BETWEEN_UPDATES;
                updateGame();
                renerGame();
                frameCount++;
                lastRenderTime = startTime;

                long duration = System.nanoTime() - startTime;

                int thisSecond = (int) (lastUpdateTime / 1000000000);
                if (thisSecond > lastSecondTime) {
                    fps = frameCount;
                    frameCount = 0;
                    lastSecondTime = thisSecond;
                }

                if (duration < TARGET_TIME_BETWEEN_RENDERS) {

                    duration = TARGET_TIME_BETWEEN_RENDERS - duration;
                    long milli = TimeUnit.NANOSECONDS.toMillis(duration);
                    try {
                        Thread.sleep(milli);
                    } catch (InterruptedException ex) {
                    }
                }
            }
        }

        protected void updateGame() {
            if (keyStates.contains(KeyState.DOWN)) {
                camY -= Y_DELTA;
            } else if (keyStates.contains(KeyState.UP)) {
                camY += Y_DELTA;
            }
            if (camY < -(map.getHeight() - view.getHeight())) {
                camY = -(map.getHeight() - view.getHeight());
            } else if (camY > 0) {
                camY = 0;
            }
            if (keyStates.contains(KeyState.RIGHT)) {
                camX -= Y_DELTA;
            } else if (keyStates.contains(KeyState.LEFT)) {
                camX += Y_DELTA;
            }
            if (camX < -(map.getWidth() - view.getWidth())) {
                camX = -(map.getWidth() - view.getWidth());
            } else if (camX > 0) {
                camX = 0;
            }
        }

        protected void renerGame() {
            BufferStrategy bs = view.getBufferStrategy();
            if (bs != null) {
                do {

                    Graphics2D g2d = (Graphics2D) bs.getDrawGraphics();
                    if (g2d != null) {
                        g2d.drawImage(map, camX, camY, null);
                        // Draw effects here...

                        FontMetrics fm = g2d.getFontMetrics();
                        g2d.setColor(Color.RED);
                        g2d.drawString(Integer.toString(fps), 0, fm.getAscent());
                        g2d.dispose();
                    }

                } while (bs.contentsLost());

                bs.show();
            }
        }

        public void addKeyState(KeyState state) {
            keyStates.add(state);
        }

        public void removeKeyState(KeyState state) {
            keyStates.remove(state);
        }

        protected BufferedImage toCompatiableImage(BufferedImage img) {
            GraphicsConfiguration gc = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getDefaultConfiguration();
            BufferedImage compImg = gc.createCompatibleImage(img.getWidth(), img.getHeight(), img.getTransparency());
            Graphics2D g2d = compImg.createGraphics();
            g2d.drawImage(img, 0, 0, null);
            g2d.dispose();
            return compImg;
        }

    }

}
MadProgrammer
  • 343,457
  • 22
  • 230
  • 366
  • Thanks again for your input! After running the code (And changing the hertz to 60), it removes the normal problem, however creates a more noticeable one, bottom of the screen renders before the top, and creates a tearing effect that moves upwards. For some reason any software used to record this, records it fine, it's only what I see that is wrong, so I had to use my phone to record the effect. I tried to disable the Rendering pipelines for DirectX and OpenGL, this removed that problem, however it created a certain jerkiness to it. [video](https://www.youtube.com/watch?v=1nooSrIW4gY) – Ryan Ramsden Aug 24 '14 at 06:57
  • Do you think [this](http://stackoverflow.com/questions/14528657/strange-bufferstrategy-issue-game-runs-fast-only-on-intel-gpus) might help? – Ryan Ramsden Aug 24 '14 at 07:10
  • I would say at this point, anythings worth a try. BufferStrategy is the closet you will get to the hardware from with in Java. You could also try more or less buffers (2 or 4 for example) as the update cycle might be to hard on the hardware (hennce the drop from 60 to 25 fps in my tests) ;) – MadProgrammer Aug 24 '14 at 07:24