这是两个基本原理的演示,但基本上是一系列缓冲区,旨在减少paintComponent
does...
一般来说,速度比较快BLIT http://en.wikipedia.org/wiki/Bit_blit将图像复制到显卡上,然后“绘制”像素,考虑到这一点,这个例子做了两件事......
首先,它预渲染背景图。此示例在运行时只是随机生成地图,但创建的地图大约是全高清地图的 4 倍。
其次,它使用自己的双缓冲。 “视图”有两个缓冲区,一个active
and an update
. The active
buffer 是绘制到屏幕上的内容,update
缓冲区是由Engine
呈现输出的当前状态...
这很重要,因为视图的缓冲区始终与视图的大小相同,因此您永远不会渲染未出现在屏幕外的任何内容。
此示例将附加内容(如动画、特效)的渲染推送到Engine
...
我在我的 30" 显示器上以 2560x1600 运行此示例,几乎没有出现任何问题,运动增量非常小,因此我可以更快地平移,使其变大可以消除这些问题...
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);
}
}
}
在我的实验过程中,我确实注意到,如果您尝试渲染内容“超出”缓冲区的范围(即允许地图的顶部在缓冲区内滑落),您会得到令人讨厌的绘画效果,所以要小心您始终在缓冲区的可视区域内渲染...
可能还有其他区域需要整理,但这展示了基础知识......