我制作了一个类,能够在多边形上以最平滑的方式移动多边形。免责声明:这是学校项目的一部分(因此使用多边形而不是图像),但是这个代码完全超出了作业的范围,作业已经被分级了。
这个项目是一个镶有宝石的克隆体,因此动画师能够完成该克隆所需的两种动画:它可以使用两个ColoredPolygons (一个简单的包装器围绕一个颜色和一个多边形)并交换它们的位置,或者交换一个ColoredPolygons列表,并将它们全部翻译成dx/dy。在这个动画中,一个内置的假设是,一次只有一个动画是活动的,并且动画覆盖的区域只包含传入的多边形。
package cad97.bejeweled.animation;
import javax.swing.*;
import java.awt.*;
import java.awt.geom.Area;
import java.util.List;
/**
* Animate Polygons around on a Graphics object.
*
* @author Christopher Durham
* @date 11/25/2016
*/
public final class Animator {
public static final Animator INSTANCE = new Animator();
private final static int ANIMATION_FRAMES = 30;
private final static int ANIMATION_FPS = 60;
private final static int ANIMATION_INTERVAL = 1000 / ANIMATION_FPS;
/**
* Convenience class bundling a {@link Color} and a {@link Polygon}
*/
public static class ColoredPolygon {
final Color color;
final Polygon polygon;
public ColoredPolygon(Color color, Polygon polygon) {
this.color = color;
this.polygon = polygon;
}
}
/**
* Animate the swap of two Polygons on a Graphics object.
* <p>
* The parameter Polygons are mutated during the execution of this method.
* <p>
* A redraw of the animated area in the {@code after} callback is suggested,
* as the final frame of this animation is not guaranteed to be fully finished.
* (E.g. the frame before a fully finished position.)
*
* @param bgColor the background Color used to erase frames
* @param first the first ColoredPolygon being switched
* @param second the second ColoredPolygon being switched
* @param g the Graphics object to draw on
* @param after callback run on UI thread after animation is finished
*/
public void translateSwap(final Color bgColor, final ColoredPolygon first, final ColoredPolygon second,
final Graphics g, final Runnable after) {
new SwingWorker<Void, Void>() {
@Override
protected Void doInBackground() throws Exception {
final Rectangle b1 = first.polygon.getBounds();
final Rectangle b2 = second.polygon.getBounds();
final int deltaX = (b1.x - b2.x) / ANIMATION_FRAMES;
final int deltaY = (b1.y - b2.y) / ANIMATION_FRAMES;
final Rectangle animationBounds = b1.union(b2);
for (int i = 0; i < ANIMATION_FRAMES; i++) {
first.polygon.translate(-deltaX, -deltaY);
second.polygon.translate(deltaX, deltaY);
SwingUtilities.invokeAndWait(() -> {
g.setColor(bgColor);
g.fillRect(animationBounds.x, animationBounds.y, animationBounds.width, animationBounds.height);
g.setColor(first.color);
g.fillPolygon(first.polygon);
g.setColor(second.color);
g.fillPolygon(second.polygon);
});
Thread.sleep(ANIMATION_INTERVAL);
}
SwingUtilities.invokeLater(after);
return null;
}
}.execute();
}
/**
* Translate a group of Polygons a direction on a Graphics object.
* <p>
* The passed Polygons are mutated during the execution of this method.
* <p>
* A redraw of the animated area in the {@code after} callback is suggested,
* as the final frame of this animation is not guaranteed to be fully finished.
* (E.g. the frame before a fully finished position.)
*
* @param bgColor the background Color used to erase frames
* @param polygons a list of ColoredPolygons to translate
* @param dx the delta x to translate the polygons
* @param dy the delta y to translate the polygons
* @param g the Graphics object to draw on
* @param after callback run on UI thread after animation is finished
*/
public void batchTranslate(final Color bgColor, final List<ColoredPolygon> polygons,
final int dx, final int dy, final Graphics2D g, final Runnable after) {
new SwingWorker<Void, Void>() {
@Override
protected Void doInBackground() throws Exception {
final Area animationBounds = polygons.stream().sequential()
.map(it -> it.polygon).map(Polygon::getBounds).peek(it -> {
it.grow(dx / 2, dy / 2);
it.translate(dx / 2, dy / 2);
}).map(Area::new).reduce((lhs, rhs) -> {
if (lhs == null) return rhs;
rhs.add(lhs);
return rhs;
})
.orElseThrow(AssertionError::new);
final int deltaX = dx / ANIMATION_FRAMES;
final int deltaY = dy / ANIMATION_FRAMES;
for (int i = 0; i < ANIMATION_FRAMES; i++) {
polygons.forEach(it -> it.polygon.translate(deltaX, deltaY));
SwingUtilities.invokeAndWait(() -> {
g.setColor(bgColor);
g.fill(animationBounds);
polygons.forEach(it -> {
g.setColor(it.color);
g.fill(it.polygon);
});
});
Thread.sleep(ANIMATION_INTERVAL);
}
SwingUtilities.invokeLater(after);
return null;
}
}.execute();
}
}我以前从未做过实际的动画,所以这可能是一个违背最佳实践的讨厌的解决方案。这是在Swing上下文中,所以任何使用Swing库而不仅仅是AWT的建议都是受欢迎的。
下面是Gist上这段代码的最小线束. --这不是审查的一部分,只是一种方便测试的方法。在创建这个线束时,我注意到这个类的第102行上有一个bug,修复在Gist中有注释。
发布于 2016-12-10 14:34:57
您已经将动画FPS硬编码为ANIMATION_FPS = 60。这是个大的不-不!您假设您的代码将被精确地以1000/ANIMATION_FPS = 16.7 ms间隔调用。但是您使用的是Thread.sleep(long),即:
取决于系统定时器和调度器的精度和准确性。
请记住,在Windows上,调度时间片约为10 ms (请看这里,其中Vista系统有15 ms)。所以这意味着你在动画上的调用精度和你的时间步骤差不多是一个数量级,这是个坏消息!你的动画要么是紧张的,要么是时间的幻灯片。这不是特定于Windows的,所有的OSes都有一定的时间切片,有些可能有动态的。它们只是在这些切片的长度上有所不同,但总的来说,它们是在千禧年的第二个尺度上。即使操作系统睡眠是准确的,如果CPU很忙,并且无法以足够快的速度跟上画图,导致动画中的时间膨胀,那么使用固定睡眠的方法也会崩溃。
你真正应该做的是对你的绘画算法的每一次调用计算出上次调用后的时间,然后乘以你希望你的图形移动/旋转的速度。
伪码:
void paint(float deltaTimeSeconds){
int xPos = oldXPos + velocityX*deltaTimeSeconds;
drawAt(XPos);
}如果你不这样做,你可能会遇到问题,如你的动画感觉紧张或动画“幻灯片”,并没有保持正确的速度在屏幕上。
您正在后台线程中执行画图计算,然后在间隔内将结果发送给主线程。在我看来,这似乎是不必要的工作。我相信,通过简单地在应用程序线程中进行绘图,您将有更好的性能和更少的麻烦。
是的,我意识到您使用后台线程来触发绘图,因为您不能从UI线程中触发它。但是有一种更好的方法,只需使用javax.swing.Timer。
从文件中:
示例use是一个使用计时器作为触发器绘制其框架的动画对象。
同样有趣的是:
虽然所有计时器都使用一个共享线程(由执行的第一个Timer对象创建)执行他们的等待,但是定时器的动作事件处理程序在另一个线程上执行--事件分派线程。这意味着定时器的操作处理程序可以安全地对Swing组件执行操作。然而,这也意味着处理程序必须快速执行以保持GUI响应。
AnimationPanel中的示例实现(需要某些程序集):
long lastFrameNs;
boolean animationCompleted = false;
Timer animationTimer = new Timer(ANIMATION_INTERVAL, event -> {
long currentFrameNs = System.currentNano();
long deltaNs = currentFrameNs - lastFrameNs;
lastFrameNs = currentFrameNs;
float deltaS = deltaNs /1E9f;
// Update polygon positions here using `deltaS`
// Detect when the animation finished....
if(done){
animationCompleted = true;
}
// Tell SWING that the component is dirty and to repaint it
// Bonus: Figure out the affected `Rectangle` and pass that into
// repaint to avoid repainting the whole shebang.
repaint();
}); // Repeats by default
// Add method to trigger the animation and parameters
void animateSwap(Jewel A, Jewel B, ...){
// Setup necessary data here...
animationCompleted = false;
lastFrameNs = System.currentNano();
animationTimer.start();
}
@Override
void paint(Graphics g){
// Draw all the jewels at their positions here...
if(animationCompleted){
animationTimer.stop();
animationCompleted = false;
}
}在这一点上,我觉得对代码的进一步审查对我来说是没有意义的,因为上面的内容已经突出了一些问题,这些问题将要求重新编写代码,从而使进一步的审查变得过时。
发布于 2016-12-12 21:13:55
Timer而不是SwingWorker为工作使用正确的工具将大大简化您的任务,并使其不容易出错。目前,您正在使用javax.swing.SwingWorker“运行”动画。SwingWorker不是适合这项工作的工具。javax.swing.Timer是。
SwingWorker用于运行冗长的后台任务,例如加载大文件,这些任务将在后台执行,然后以不规则的间隔更新UI。Timer用于定期触发事件线程上的操作,这对于简单的动画非常好。根据经验,调用Thread.sleep()总是会让您三思而后行。避免Thread.sleep()。在另一次审查中已经提到了sleep()的问题。
您可以通过使用Timer实现更流畅的动画。使用SwingWorker时,即使使用invokeAndWait(),图形更新也会异步进行,因为更新请求发生在与AWT事件线程不同的线程上。在AWT事件线程上运行某项请求所需的时间是不确定的。
如果使用Timer,您已经处于AWT事件线程中,您可以根据启动系统时间和当前系统时间计算当前动画帧的准确位置。
final中的bug
当我在我的机器上播放动画时,有些动画会留下痕迹,直到重新绘制后才会变得光滑。我没有钻研边界计算,但它们可能有问题。
translateSwap和batchTranslate这两种方法非常相似。您可能可以将其中的大部分提取到单独的方法中。甚至通过将差异作为策略传递到一个方法中(请参见策略设计模式;您已经在使用它了,translateSwap和batchTranslate的最后一个参数是策略设计模式的一个示例)。
为了获得更流畅的动画,您可能需要考虑双缓冲。目前,您直接使用Graphics的JPanel在屏幕上进行绘图。这意味着用户看到一个闪烁的动画,首先是背景,然后是重新出现的多边形。
https://docs.oracle.com/javase/tutorial/extra/fullscreen/doublebuf.html
getGraphics()我从来不需要调用组件的getGraphics()。我总是设法在paint()或paintComponent()方法中做任何需要做的事情。我发现在提供Graphics的上下文之外使用它是可疑和脆弱的。例如,如果稍后向应用程序添加一个可选的全屏模式,并为用户添加一个选项,以便在全屏模式和窗口模式之间切换,我将假设每当发生这种切换时,Graphics对象就会变得无效。
如果根据修改者在Java语言规范中的外观进行排序,则读取Java源代码会更快。在本例中,这意味着对于某些常量,它应该是private static final而不是private final static。
流是函数式编程的一个元素。函数式程序设计的核心是避免副作用。在peek()中看到操作矩形边界的副作用,感觉很奇怪。虽然这在技术上是可能的,在您的情况下技术上是正确的代码,但它感觉就像一个陷阱。我宁愿使用一个在操作后返回相同对象的map():
map()之后,流包含了其他内容。peek()之后,流包含了其他内容。如果方法短,代码通常更容易理解。对于大多数方法,我建议在Java中每个方法不超过3行。
https://codereview.stackexchange.com/questions/148936
复制相似问题