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