原文地址:
Java2D: Hardware Accelerating - Part2 - Buffer Strategies
利用我们已经学到的东西
昨天的帖子(
Java2D: Hardware Acceletating - Part1 - Volatile Images)里,我讨论了
java.awt.image.VolatileImage,以及如何使用它们(实现)双缓冲Java 2D的代码。如果你还不能实现,请先看一下那个帖子。今天的小提示将扩展引用那个提示里提供的例子。为了演示如何使用VolatileImage对象,我使用了一种极普通的代码格式来处理Java 2D代码 - 在例如java.awt.Canvas这样的对象中重载 paint(Graphics)方法,并在控件内部使用Graphics对象执行自定义画图。记住,正如前面的帖子中提到的以及对它的回复所述那样,如果你在做标准的AWT/Swing开发的话,这种重载并非很有用。在这种情况下,你通常应该依赖于已有的控件实现来优化渲染。我今天(以及昨天)所说的,只对你开发低层图像渲染,并且需要自己实现缓冲策略的工作很有用。通常这种情况发生在当你在使用Java开发2D游戏的时候。从这点出发我会与编写游戏结合讨论这一概念 - 我认为这是我可以提供的最实在的例子之一。
任何曾经实际写过游戏(使用几乎任何编程语言)的人可能都熟悉“游戏循环:game loop”这个概念。游戏循环实质就是这样的一系列步骤(我对此随便的命名,请原谅):
- 过程阶段:基于用户输入和/或游戏事件更新任意“状态”变量
- 渲染阶段:基于“状态”变量执行UI渲染
- 同步阶段:睡眠直到某时刻以确保一致的帧速率
这个对游戏循环步骤地描述是很高层次的 - 但毕竟这样的过程易于理解 -
计算,渲染,等待。很不幸,我们的
非恒定图像例子由于自身的设计不能支持。我们的渲染逻辑严格地捆绑在控件上,而不是我们的游戏循环。以下是用第一部分里面CustomGUI对象来实现的我们的游戏循环:
public class MyGame {
private static final int FRAME_DELAY = 20; // 20ms。对应于50FPS (1000/20) = 50
private static Canvas gui;
public static void main(String[] args) {
JFrame frame = new JFrame();
gui = new CustomGUI(); // 创建我们自己的改造过渲染逻辑的Canvas对象
frame.getContentPane().add(gui);
frame.setSize(500, 300);
Thread gameThread = new Thread(new GameLoop());
gameThread.setPriority(Thread.MIN_PRIORITY);
gameThread.start(); // 开始游戏过程
frame.setVisible(true); // 开始AWT绘制
}
private static void updateGameState() {
// 游戏逻辑
}
private static class GameLoop implements Runnable {
boolean isRunning = true;
public void run() {
int cycleTime = System.currentTimeMillis();
while(isRunning) {
updateGameState();
MyGame.gui.repaint(); // 引发“paint”以在AWT线程中调用CustomGUI对象。(引发了帧的渲染)
cycleTime = cycleTime + MyGame.FRAME_DELAY;
long difference = cycleTime - System.currentTimeMillis();
try {
Thread.sleep(Math.max(0, difference));
} catch(InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
总结上面所发生的事情就是 - 我们实质上有两个重要线程 - AWT事件分发线程(处理绘制),以及游戏线程(处理游戏逻辑)。两个线程通过“repaint()”调用汇合。游戏逻辑在一个线程里,渲染代码在另外一个里。一旦游戏逻辑执行完毕,在AWT这边就请求一次repaint,实质是渲染我们游戏的另一“帧”。目前为止,这个逻辑也不是太复杂(除了昨天我们看到的CustomeGUI对象中恶心的VolatileImage逻辑)。然而,当我们开始尝试游戏逻辑与渲染之间的双向交流时,所有的东西就都错综复杂变得稀奇古怪了。为了让我们的基础变得更扎实,请允许我扩展昨天的例子。这里我打算把自定义的渲染分离成一个方法,这样就更容易分辨渲染的代码及更新的内容:
// 也可扩展其它类 - 不过通常会选择Canvas
public class CustomGUI extends Canvas {
private VolatileImage volatileImg;
// ...
public void paint(Graphics g) {
// 创建硬件加速图像
createBackBuffer();
// 主渲染循环。VolatileImage对象可能失去内容。
// 这个循环会不断渲染(如果需要的话并制造)VolatileImage对象
// 直到渲染过程圆满完成。
do {
// 为该控件的Graphics配置验证VolatileImage的有效性。
// 如果VolatileImage对象不能匹配GraphicsConfiguration
// (换句话说,硬件加速不能应用在新设备上)
// 那我们就重建它。
GraphicsConfiguration gc = this.getGraphicsConfiguration();
int valCode = volatileImg.validate(gc);
// 以下说明设备不匹配这个硬件加速Image对象。
if(valCode==VolatileImage.IMAGE_INCOMPATIBLE){
createBackBuffer(); // 重建硬件加速Image对象。
}
Graphics offscreenGraphics = volatileImg.getGraphics();
doPaint(offscreenGraphics); // 调用核心paint方法。
// 把缓冲画回主Graphics对象
g.drawImage(volatileImg, 0, 0, this);
// 检查内容是否丢失
} while(volatileImg.contentsLost());
}
// 封装了渲染逻辑的新方法。
private void doPaint(Graphics g) {
g.setColor(Color.WHITE);
g.fillRect(0, 0, getWidth(), getHeight());
g.setColor(Color.BLACK);
g.drawLine(0, 0, 10, 0); // 任意的渲染逻辑
}
// 以下创建新的VolatileImage对象
private void createBackBuffer() {
GraphicsConfiguration gc = getGraphicsConfiguration();
volatileImg = gc.createCompatibleVolatileImage(getWidth(), getHeight());
}
public void update(Graphics g) {
paint(g);
}
}
这个例子和昨天的代码唯一的差别就是我把实际绘制的代码拽出来成了doPaint(Graphics)方法。
接下来,让我们把线动起来。你问哪条线?哦,在doPaint方法的第四行我们画了一条10个像素的线。让我们把它动起来。(我知道,我真的知道,一条能动的线是枯燥乏味的 -你就假想它是在一点点啃食的蠕虫吧)。
让我们的线移动的第一步是把线的x坐标放在一个地方让我们的自定义画布渲染代码
和更新代码都能访问到。很多方法都可以使用,不过目前最简单的就是作为MyGame类自身的静态变量:
public class MyGame {
public static int lineX = 0;
// ...
}
这看上去就像是“入侵行为:hack”(的确是),但那也刚好演示了把渲染代码分离开来的第一个问题。必须实现复杂的结构,从而可以与渲染逻辑共享游戏算法的结果。我已经意识到了还有许多更优雅的方式解决这样的问题 - 不过我会稍后再描述一个更好的方法,因此请耐心等待。
接下来,我们要实现updateGameState()方法 - 记住,这个逻辑将在每一帧执行一次:
private void updateGameState() {
lineX++;
}
因此,长话短说,我们的小蠕虫将每一帧移动一个像素。现在我们只需更新doPaint方法基于此值渲染即可:
private void doPaint(Graphics g) {
g.setColor(Color.WHITE);
g.fillRect(0, 0, getWidth(), getHeight());
g.setColor(Color.BLACK);
g.drawLine(MyGame.lineX, 0, MyGame.lineX + 10, 0); // 画图
}
大功告成,我们现在实现了一个简单的小动画程序。我们没有处理用户输入或其它什么事,但至少我们已经让游戏循环全速工作了。
消除混淆
仍跟从我的人或许感觉我已经走到某条疯狂的歧路上并还在不断叫嚣着游戏开发呢吧。实际上,我还不至于失去职业操守(?原文:I haven't lost site of the prize)。我要使用我昨天讨论的东西实现所有这些逻辑,并使用今天小提示的原点 - 缓冲策略 - 把它们封装在一起。
当前使用于GUI代码的缓冲的模式(重载paint以实现VolatileImage对象)让人感觉不自然。我们在自定义的java.awt.Canvas对象内部实现绘图代码 - 然而按照道理说绘图逻辑应该是游戏代码的一部分,而不是Canvas对象自身的部分。那样的话,把这些逻辑移出来更好。另外,VolatileImage对象和缓冲策略感觉应该放在一起。当然某些比我们更聪明的人已经实现了这些缓冲算法,而且或许比我们做的还要好。
java.awt.image.BufferStrategy类可以填充我们当前实现的这两条缝隙 - 1.渲染逻辑可以与控件实现解耦,并且,2.缓冲策略可以与渲染逻辑解耦(并可以作为充分测试和优化的代码被重用)。
使用BufferStrategy类的一个最好的副作用是渲染代码可以在我们的游戏循环线程中序列执行(哦,好吧 - 伪序列地)。带来的好处是更好的状态变量可访问性(如lineX)因为我们不再需要跨越线程/类边界,而且也是的代码的交互更为清晰。
BufferStrategy对象可以由Frame和Canvas对象创建。一旦创建了,你就能对缓冲策略绘图(而不用考虑其实际的缓冲),然后把缓冲“冲”入主绘图对象。缓冲策略有各种全面的使用方法,不过在我们代码的例子里,我们只是简单的想把我们的复杂的缓冲逻辑用某些更干净更清晰的代码替代。我们想要的是能够被可用的硬件优化了的“双缓冲”(正如我们昨天提到的),并且我们可以使用现存的渲染逻辑做渲染。下面的是改编自
java.awt.image.BufferStrategy的javadoc的简单例子:
JFrame frame = new JFrame();
Canvas canvas = new Canvas();
frame.getContentPane().add(canvas);
frame.setVisible(true);
canvas.createBufferStrategy(2); // 创建2个缓冲(双缓冲)的缓冲策略
BufferStrategy strategy = canvas.getBufferStrategy();
Graphics g = strategy.getDrawGraphics();
// 绘制到Graphics对象
g.dispose();
// 把缓冲“冲”到主绘图区
strategy.show();
比我们的CustomGUI怪兽要简单一点儿,哦?请注意我们一旦在缓冲策略中创建了Graphics对象,就可以像使用非恒定图像对象那样使用(渲染的算法没有改变)。同样请注意
不像之前的那样,绘制代码
不在事件分发线程中(尽管在这一幕的背后绘制仍然是在事件分发线程中完成的)。
那么这对我们的例子意味着什么呢?哦,简要的说这意味着我们可以几乎完全扔掉我们的CustomGUI画布子类了。我们唯一要保留的就是绘制代码本身!我家下来要奉献的就是使用缓冲策略对象把所有这些工作并为单一程序的集合体。主要目标之一就是去掉入侵的static变量 - 并且能够如此容易地做到这点部分要归功于使用BufferStrategy。那么,参见下面的代码 - 所有这些讨论的最终的结合体 - 使用了缓冲策略的我们的“动画”程序。
public class MyGame {
private static final int FRAME_DELAY = 20; // 20ms。对应于50FPS (1000/20) = 50
public static void main(String[] args) {
JFrame frame = new JFrame();
Canvas gui = new Canvas();
frame.getContentPane().add(gui);
frame.setSize(500, 300);
frame.setVisible(true); // 开始AWT绘制
Thread gameThread = new Thread(new GameLoop(gui));
gameThread.setPriority(Thread.MIN_PRIORITY);
gameThread.start(); // 开始游戏过程
}
private static class GameLoop implements Runnable {
boolean isRunning;
int lineX;
Canvas gui;
long cycleTime;
public GameLoop(Canvas canvas) {
gui = canvas;
isRunning = true;
lineX = 0;
}
public void run() {
cycleTime = System.currentTimeMillis();
gui.createBufferStrategy(2);
BufferStrategy strategy = gui.getBufferStrategy();
// 游戏循环
while (isRunning) {
updateGameState();
updateGUI(strategy);
synchFramerate();
}
}
private void updateGameState() {
lineX++;
}
private void updateGUI(BufferStrategy strategy) {
Graphics g = strategy.getDrawGraphics();
g.setColor(Color.WHITE);
g.fillRect(0, 0, gui.getWidth(), gui.getHeight());
g.setColor(Color.BLACK);
g.drawLine(lineX, 0, lineX + 10, 0); // 任意渲染逻辑
g.dispose();
strategy.show();
}
private void synchFramerate() {
cycleTime = cycleTime + FRAME_DELAY;
long difference = cycleTime - System.currentTimeMillis();
try {
Thread.sleep(Math.max(0, difference));
} catch(InterruptedException e) {
e.printStackTrace();
}
}
}
}