Reputation: 57
I am using a JPanel to draw my game onto, using a fixed game loop, and double buffering; however I get a glitch somewhere on the screen. The glitch is a screen tearing visual artifact that stretches across X axis, and is about 20 pixels tall.
I have recreated the problem in 1 class, as shown below. To recreate the problem, you can run the code and move the square around with the arrow keys, when the square moves over the place where the visual tearing occurs, you should see the effect. (The location of the visual tearing seems to be random)
I recreated a single frame of what the image tearing looks like, however when running, it shows a flickering effect.
package main;
import java.awt.Color;
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.util.HashSet;
import java.util.Set;
import java.util.concurrent.locks.ReentrantLock;
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 Panel {
public static void main(String[] args) {
new Panel();
}
public Panel() {
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("Game!");
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;
}
@SuppressWarnings("serial")
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(800, 800);
}
@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 = 8;
public static final int Y_DELTA = 8;
//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;
public int x, y;
private boolean isGameFinished;
private View view;
private Set<KeyState> keyStates;
public Engine(View bufferRenderer) {
keyStates = new HashSet<>(4);
this.view = bufferRenderer;
}
public void gameStart() {
x = (800/2) - (60/2);
y = (800/2) - (60/2);
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;
}
while (now - lastRenderTime < TARGET_TIME_BETWEEN_RENDERS && now - lastUpdateTime < TIME_BETWEEN_UPDATES) {
Thread.yield();
try { Thread.sleep(1);
} catch (Exception e) {}
now = System.nanoTime();
}
}
}
protected void renderBuffer(BufferedImage buffer) {
if (buffer != null) {
Graphics2D g2d = buffer.createGraphics();
g2d.setColor(Color.BLACK);
g2d.fillRect(0, 0, buffer.getWidth(), buffer.getHeight());
g2d.setColor(Color.WHITE);
g2d.fillRect(x, y, 60, 60);
g2d.setColor(Color.WHITE);
g2d.drawString("FPS: "+Engine.fps, 0, 10);
g2d.dispose();
}
}
protected void gameUpdate(BufferedImage buffer) {
if (keyStates.contains(KeyState.DOWN)) {
y = y + Y_DELTA;
} else if (keyStates.contains(KeyState.UP)) {
y = y - Y_DELTA;
}
if (keyStates.contains(KeyState.RIGHT)) {
x = x + X_DELTA;
} else if (keyStates.contains(KeyState.LEFT)) {
x = x - X_DELTA;
}
}
public void addKeyState(KeyState state) {
keyStates.add(state);
}
public void removeKeyState(KeyState state) {
keyStates.remove(state);
}
}
@SuppressWarnings("serial")
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);
}
}
@SuppressWarnings("serial")
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);
}
}
}
Edit: There is a small chance that the screen tearing doesn't occur, (I believe this is because the frames are in sync with the screen)
Edit2: Some people have told me that this problem doesn't occur on their hardware, how could I fix this problem for my hardware?
Upvotes: 1
Views: 1087
Reputation: 21
Try moving the g2d.dipose()
from the paintComponent(g)
method. To go inside the try{}catch()
statement right after you draw the active image.
Upvotes: 0
Reputation: 347334
THIS IS A TEST - NOT A ANSWER
This is the most simplistic Swing based example I can come up with. I paints directly to the JPanel
and will try and update every 16 milliseconds (60fps)
I would suggest that you play with the X_DELTA
and Y_DELTA
values as well as commenting out Toolkit.getDefaultToolkit().sync();
in and out to see if it makes a difference...
nb- Both examples use a "direct" painting process. That is, rather the using a BufferedImage
(or other backing buffer), they paint directly to the Graphics
context
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.util.HashSet;
import java.util.Set;
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.Timer;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
public class BasicAnimation {
public static void main(String[] args) {
new BasicAnimation();
}
public BasicAnimation() {
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 enum KeyState {
UP, DOWN, LEFT, RIGHT;
}
public static class TestPane extends JPanel {
private Set<KeyState> keyStates;
private Point player = new Point(200, 200);
public static final int X_DELTA = 4;
public static final int Y_DELTA = 4;
public TestPane() {
keyStates = new HashSet<>(4);
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(KeyState.UP));
am.put("up_released", new RemoveState(KeyState.UP));
am.put("down_pressed", new AddState(KeyState.DOWN));
am.put("down_released", new RemoveState(KeyState.DOWN));
am.put("left_pressed", new AddState(KeyState.LEFT));
am.put("left_released", new RemoveState(KeyState.LEFT));
am.put("right_pressed", new AddState(KeyState.RIGHT));
am.put("right_released", new RemoveState(KeyState.RIGHT));
Timer timer = new Timer(16, new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
updateState();
repaint();
Toolkit.getDefaultToolkit().sync();
}
});
timer.start();
}
protected void updateState() {
if (keyStates.contains(KeyState.UP)) {
player.y -= Y_DELTA;
} else if (keyStates.contains(KeyState.DOWN)) {
player.y += Y_DELTA;
}
if (keyStates.contains(KeyState.LEFT)) {
player.x -= X_DELTA;
} else if (keyStates.contains(KeyState.RIGHT)) {
player.x += X_DELTA;
}
}
@Override
public Dimension getPreferredSize() {
return new Dimension(400, 400);
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g.create();
g2d.setColor(Color.BLACK);
g2d.fillRect(0, 0, getWidth(), getHeight());
g2d.setColor(Color.WHITE);
g2d.fillRect(player.x - 25, player.y - 25, 50, 50);
g2d.dispose();
}
public void addKeyState(KeyState state) {
keyStates.add(state);
}
public void removeKeyState(KeyState state) {
keyStates.remove(state);
}
@SuppressWarnings("serial")
public class AddState extends AbstractAction {
private final KeyState state;
public AddState(KeyState state) {
this.state = state;
}
@Override
public void actionPerformed(ActionEvent e) {
addKeyState(state);
}
}
@SuppressWarnings("serial")
public class RemoveState extends AbstractAction {
private final KeyState state;
public RemoveState(KeyState state) {
this.state = state;
}
@Override
public void actionPerformed(ActionEvent e) {
removeKeyState(state);
}
}
}
}
Included a Canvas
/BufferStrategy
example as well. Some deal, play with the X/Y_Delta
values and enable/disable Toolkit.getDefaultToolkit().sync();
import java.awt.Canvas;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.image.BufferStrategy;
import java.util.HashSet;
import java.util.Set;
import javax.swing.AbstractAction;
import javax.swing.JFrame;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
public class BasicAnimation {
public static void main(String[] args) {
new BasicAnimation();
}
public BasicAnimation() {
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 GameView());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public enum KeyState {
UP, DOWN, LEFT, RIGHT;
}
public static class GameView extends Canvas {
private Set<KeyState> keyStates;
private Point player = new Point(200, 200);
public static final int X_DELTA = 4;
public static final int Y_DELTA = 4;
private GameThread gt;
public GameView() {
keyStates = new HashSet<>(4);
gt = new GameThread(this);
setFocusable(true);
addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
switch (e.getKeyCode()) {
case KeyEvent.VK_UP:
addKeyState(KeyState.UP);
break;
case KeyEvent.VK_DOWN:
addKeyState(KeyState.DOWN);
break;
case KeyEvent.VK_LEFT:
addKeyState(KeyState.LEFT);
break;
case KeyEvent.VK_RIGHT:
addKeyState(KeyState.RIGHT);
break;
}
}
@Override
public void keyReleased(KeyEvent e) {
switch (e.getKeyCode()) {
case KeyEvent.VK_UP:
removeKeyState(KeyState.UP);
break;
case KeyEvent.VK_DOWN:
removeKeyState(KeyState.DOWN);
break;
case KeyEvent.VK_LEFT:
removeKeyState(KeyState.LEFT);
break;
case KeyEvent.VK_RIGHT:
removeKeyState(KeyState.RIGHT);
break;
}
}
});
}
@Override
public void addNotify() {
super.addNotify();
createBufferStrategy(2);
gt.start();
requestFocusInWindow();
}
@Override
public void removeNotify() {
gt.stop();
super.removeNotify();
}
public void updateState() {
if (keyStates.contains(KeyState.UP)) {
player.y -= Y_DELTA;
} else if (keyStates.contains(KeyState.DOWN)) {
player.y += Y_DELTA;
}
if (keyStates.contains(KeyState.LEFT)) {
player.x -= X_DELTA;
} else if (keyStates.contains(KeyState.RIGHT)) {
player.x += X_DELTA;
}
}
public void paintState(Graphics2D g2d) {
g2d.setColor(Color.BLACK);
g2d.fillRect(0, 0, getWidth(), getHeight());
g2d.setColor(Color.WHITE);
g2d.fillRect(player.x - 25, player.y - 25, 50, 50);
}
@Override
public Dimension getPreferredSize() {
return new Dimension(400, 400);
}
public void addKeyState(KeyState state) {
keyStates.add(state);
}
public void removeKeyState(KeyState state) {
keyStates.remove(state);
}
@SuppressWarnings("serial")
public class AddState extends AbstractAction {
private final KeyState state;
public AddState(KeyState state) {
this.state = state;
}
@Override
public void actionPerformed(ActionEvent e) {
addKeyState(state);
}
}
@SuppressWarnings("serial")
public class RemoveState extends AbstractAction {
private final KeyState state;
public RemoveState(KeyState state) {
this.state = state;
}
@Override
public void actionPerformed(ActionEvent e) {
removeKeyState(state);
}
}
}
public static class GameThread implements Runnable {
private volatile boolean keepRunning = true;
private GameView view;
private Thread currentThread;
public GameThread(GameView view) {
this.view = view;
}
public void start() {
if (currentThread == null) {
keepRunning = true;
currentThread = new Thread(this);
currentThread.start();
}
}
public void stop() {
keepRunning = false;
if (currentThread != null) {
try {
currentThread.join();
} catch (InterruptedException ex) {
}
}
}
@Override
public void run() {
while (keepRunning) {
view.updateState();
BufferStrategy bs = view.getBufferStrategy();
do {
Graphics2D g2d = (Graphics2D) bs.getDrawGraphics();
view.paintState(g2d);
g2d.dispose();
} while (bs.contentsLost());
bs.show();
Toolkit.getDefaultToolkit().sync();
try {
Thread.sleep(16);
} catch (InterruptedException ex) {
}
}
}
}
}
Upvotes: 1