我打算用ScalaFX和canvas一起制作一个节奏游戏,当我尝试运行代码时,我发现它消耗了大量的GPU,有时帧速率下降到30 fps,即使我只在画布上绘制一个图像没有绘制任何动画音符,舞者,过程仪表等。
以下是我的代码
import scalafx.animation.AnimationTimer
import scalafx.application.JFXApp
import scalafx.scene.Scene
import scalafx.scene.canvas.{Canvas, GraphicsContext}
import scalafx.scene.image.Image
import scalafx.scene.layout.Pane
import scalafx.scene.paint.Color.Green
object MainApp extends JFXApp{
var MainScene: Scene = new Scene {
fill = Green
}
var MainStage: JFXApp.PrimaryStage = new JFXApp.PrimaryStage {
scene = MainScene
height = 720
width = 1280
}
var gameCanvas:Canvas = new Canvas(){
layoutY=0
layoutX=0
height=720
width=1280
}
var gameImage:Image = new Image("notebar.png")
var gc:GraphicsContext = gameCanvas.graphicsContext2D
MainScene.root = new Pane(){
children=List(gameCanvas)
}
var a:Long = 0
val animateTimer = AnimationTimer(t => {
val nt:Long = t/1000000
val frameRate:Long = 1000/ (if((nt-a)==0) 1 else nt-a)
//check the frame rate
println(frameRate)
a = nt
gc.clearRect(0,0,1280,720)
gc.drawImage(gameImage,0,0,951,160)
})
animateTimer.start()
}
如何在不使用画布的情况下改善性能或有更好的方法来做同样的事情?
有几个因素可能会降低帧速率:
关于帧速率,在下面的版本中,我记录第一帧的时间(以纳秒为单位),并计算绘制的帧数。当应用程序退出时,它会报告平均帧速率。这是一个更简单的计算,不会过多干扰动画处理程序内部的操作,并且是衡量整体性能的一个很好的指标。 (由于垃圾收集,其他进程,JIT编译改进等原因,每帧的时序会有相当大的变化。我们将尝试通过查看平均速率来跳过所有这些。)
我还更改了代码以仅清除图像占用的区域。
我还简化了你的代码,使其在使用ScalaFX时更加传统(例如,使用主类的stage
成员,以及更多地使用类型推断):
import scalafx.animation.AnimationTimer
import scalafx.application.JFXApp
import scalafx.scene.Scene
import scalafx.scene.canvas.Canvas
import scalafx.scene.image.Image
import scalafx.scene.layout.Pane
import scalafx.scene.paint.Color.Green
object MainApp
extends JFXApp {
// Nanoseconds per second.
val NanoPerSec = 1.0e9
// Height & width of canvas. Change in a single place.
val canvasHeight = 720
val canvasWidth = 1280
// Fixed canvas size.
val gameCanvas = new Canvas(canvasWidth, canvasHeight)
// The image.
val gameImage = new Image("notebar.png")
val gc = gameCanvas.graphicsContext2D
stage = new JFXApp.PrimaryStage {
height = canvasHeight
width = canvasWidth
scene = new Scene {
fill = Green
root = new Pane {
children=List(gameCanvas)
}
}
}
// Class representing an initial frame time, last frame time and number of frames
// drawn. The first frame is not counted.
//
// (Ideally, this would be declared in its own source file. I'm putting it here for
// convenience.)
final case class FrameRate(initTime: Long, lastTime: Long = 0L, frames: Long = 0L) {
// Convert to time in seconds
def totalTime: Double = if(frames == 0L) 1.0 else (lastTime - initTime) / NanoPerSec
def mean: Double = frames / totalTime
def update(time: Long): FrameRate = copy(lastTime = time, frames = frames + 1)
}
// Current frame rate.
private var frames: Option[FrameRate] = None
val animateTimer = AnimationTimer {t =>
// Update frame rate.
frames = Some(frames.fold(FrameRate(t))(_.update(t)))
// Send information to console. Comment out to determine impact on frame rate.
//println(s"Frame rate: ${frames.fold("Undefined")(_.mean.toString)}")
// Clear region of canvas.
//
// First clears entire canvas, second only image. Comment one out.
//gc.clearRect(0, 0, canvasWidth, canvasHeight)
gc.clearRect(0, 0, gameImage.width.value, gameImage.height.value)
// Redraw the image. This version doesn't need to know the size of the image.
gc.drawImage(gameImage, 0, 0)
}
animateTimer.start()
// When the application terminates, output the mean frame rate.
override def stopApp(): Unit = {
println(s"Mean frame rate: ${frames.fold("Undefined")(_.mean.toString)}")
}
}
(顺便说一下:尽可能避免在Scala中使用var
语句。使用JavaFX / ScalaFX时共享可变状态是不可避免的,但Property
s提供了更好的处理它的机制。尝试养成使用val
元素声明的习惯,除非他们真的,确实需要是var
s。如果你确实需要使用var
s,它们应该几乎总是被声明为private
以防止不受控制的外部访问和修改。)
对Java程序进行基准测试是一种艺术形式,但显然,每个版本运行的时间越长,平均帧速率就越高。在我的机器上(带有我自己的图像)我在运行应用程序5分钟后获得了以下相当不科学的结果:
仅清除图像,而不是整个画布看起来效果不大,让我感到惊讶。但是,输出到控制台对帧速率有很大影响。
除了使用画布之外,另一种可能性是简单地将图像定位在场景组中,然后通过改变其坐标来移动它。下面的代码如下(使用属性间接移动图像):
import scalafx.animation.AnimationTimer
import scalafx.application.JFXApp
import scalafx.beans.property.DoubleProperty
import scalafx.scene.{Group, Scene}
import scalafx.scene.image.ImageView
import scalafx.scene.layout.Pane
import scalafx.scene.paint.Color.Green
import scalafx.scene.shape.Rectangle
object MainApp
extends JFXApp {
// Height & width of app. Change in a single place.
val canvasHeight = 720
val canvasWidth = 1280
// Nanoseconds per second.
val NanoPerSec = 1.0e9
// Center of the circle about which the image will move.
val cX = 200.0
val cY = 200.0
// Radius about which we'll move the image.
val radius = 100.0
// Properties for positioning the image (might be initial jump).
val imX = DoubleProperty(cX + radius)
val imY = DoubleProperty(cY)
// Image view. It's co-ordinates are bound to the above properties. As the properties
// change, so does the image's position.
val imageView = new ImageView("notebar.png") {
x <== imX // Bind to property
y <== imY // Bind to property
}
stage = new JFXApp.PrimaryStage {
height = canvasHeight
width = canvasWidth
scene = new Scene {thisScene => // thisScene is a self reference
fill = Green
root = new Group {
children=Seq(
new Rectangle { // Background
width <== thisScene.width // Bind to scene/stage width
height <== thisScene.height // Bind to scene/stage height
fill = Green
},
imageView
)
}
}
}
// Class representing an initial frame time, last frame time and number of frames
// drawn. The first frame is not counted.
//
// (Ideally, this would be declared in its own source file. I'm putting it here for
// convenience.)
final case class FrameRate(initTime: Long, lastTime: Long = 0L, frames: Long = 0L) {
// Convert to time in seconds
def totalTime: Double = if(frames == 0L) 1.0 else (lastTime - initTime) / NanoPerSec
def mean: Double = frames / totalTime
def update(time: Long) = copy(lastTime = time, frames = frames + 1)
}
// Current frame rate.
var frames: Option[FrameRate] = None
val animateTimer = AnimationTimer {t =>
// Update frame rate.
frames = Some(frames.fold(FrameRate(t))(_.update(t)))
// Change the position of the image. We'll make the image move around a circle
// clockwise, doing 1 revolution every 10 seconds. The center of the circle will be
// (cX, cY). The angle is therefore the modulus of the time in seconds divided by 10
// as a proportion of 2 pi radians.
val angle = (frames.get.totalTime % 10.0) * 2.0 * Math.PI / 10.0
// Update X and Y co-ordinates related to the center and angle.
imX.value = cX + radius * Math.cos(angle)
imY.value = cY + radius * Math.sin(angle)
}
animateTimer.start()
// When the application terminates, output the mean frame rate.
override def stopApp(): Unit = {
println(s"Mean frame rate: ${frames.fold("Undefined")(_.mean.toString)}")
}
}
这使我在运行5分钟后产生59.86 fps的平均帧速率 - 几乎与使用画布完全相同。
在这个例子中,运动有点不稳定,这很可能是由垃圾收集周期引起的。也许尝试尝试不同的GC?
顺便说一下,我在这个版本中移动图像以强迫某些事情发生。如果属性没有改变,那么我怀疑图像不会更新那个帧。实际上,如果我每次都将属性设置为相同的值,则帧速率变为:62.05 fps。
使用画布意味着您必须确定绘制的内容以及如何重绘它。但是使用JavaFX场景图(如上例所示)意味着JavaFX负责确定是否需要重绘帧。它在这种特殊情况下并没有太大的区别,但如果连续帧之间的内容差异很小,它可能会加快速度。要记住一些事情。
这够快吗?顺便说一下,在这个特定的例子中,与内容相关的开销很大。如果在动画中添加其他元素对帧速率的影响非常小,我也不会感到惊讶。最好尝试一下,看看。给你...
(关于动画的另一种可能性,请参阅ScalaFX源附带的ColorfulCircles演示。)
更新:我在评论中提到过这一点,但在主要答案中可能值得强调:JavaFX的默认速度限制为60 fps,这也影响了上面的基准测试 - 这也解释了为什么没有更好地利用CPU和GPU。
要使您的应用程序以尽可能高的帧速率运行(如果您希望在笔记本电脑上最大限度地提高电池电量,或者为了提高整体应用程序性能,可能不是一个好主意),请在运行应用程序时启用以下属性:
-Djavafx.animation.fullspeed=true
请注意,此属性未记录且不受支持,这意味着它可能会在JavaFX的未来版本中消失。
我用这个属性集重新运行了基准测试,并观察了这些结果:
使用画布:
动画场景图:
这些结果显着改变了我原来的结论:
有关this answer财产的更多详情,请参阅javafx.animation.fullspeed
。