我计划通过在画布上使用ScalaFX来制作节奏游戏, 当我尝试运行代码时,我发现它消耗了大量的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()
}
如何提高性能,或者有什么更好的方法可以在不使用画布的情况下做同样的事情?
答案 0 :(得分:3)
有一些因素可能会降低帧速率:
关于帧速率,在下面的版本中,我记录第一帧的时间(以纳秒为单位),并计算绘制的帧数。当应用程序退出时,它将报告平均帧速率。这是一种更简单的计算,不会过多地影响动画处理程序内部的操作,并且是总体性能的良好衡量标准。 (由于垃圾回收,其他进程,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
提供了更好的机制来处理它,请养成使用val
元素声明的习惯,除非它们确实确实需要{ {1}}。如果确实需要使用var
,则应该几乎总是将它们声明为var
,以防止不受控制的外部访问和修改。)
对 Java 程序进行基准测试是一种艺术形式,但显然,每个版本的运行时间越长,平均帧速率就越好。在我的机器上(带有我自己的图像),在运行该应用程序5分钟后,我获得了以下结果,这是不科学的:
仅清除图像而不是清除整个画布似乎效果不大,这让我有些惊讶。但是,输出到控制台会对帧速率产生巨大影响。
除了使用画布之外,另一种可能性是简单地将图像放置在场景组内,然后通过更改其坐标来移动它。下面的代码(使用属性间接移动图像)如下:
private
在运行5分钟后,这为我产生了59.86 fps的平均帧频,几乎与使用画布相同。
在此示例中,动作有点生涩,这很可能是由垃圾收集周期引起的。也许尝试尝试使用不同的 GC ?
顺便说一句,我在此版本中移动图像以强制发生某些事情。如果属性没有更改,那么我怀疑该图像将不会在该帧中更新。确实,如果我每次都将属性设置为相同的值,则帧速率将变为:62.05 fps。
使用画布意味着您必须确定要绘制的内容以及如何重新绘制它。但是使用 JavaFX 场景图(如上一个示例所示)意味着 JavaFX 负责弄清楚是否甚至需要重绘框架。在这种特殊情况下,它并没有太大的区别,但是如果连续帧之间的内容差异很小,则可能会加快处理速度。要记住的事情。
够快吗?顺便说一句,在此特定示例中,与内容相关的开销很大。如果在动画中添加其他元素对帧速率几乎没有影响,我一点也不惊讶。最好尝试一下看看。交给你...
(有关动画的另一种可能性,请参阅 ScalaFX 源随附的 ColorfulCircles 演示。)
更新:我在评论中提到了这一点,但可能值得在主要答案中强调一下: JavaFX 的默认速度限制为60 fps,这也会影响上述基准测试,并且也解释了为什么不能更好地利用CPU和GPU。
要使您的应用程序以尽可能高的帧速率运行(如果您希望最大程度地增加笔记本电脑的电池电量或提高整体应用程序性能,可能不是一个好主意),请在运行应用程序时启用以下属性:
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)}")
}
}
请注意,此属性未记录且不受支持,这意味着它可能在 JavaFX 的未来版本中消失。
我使用该属性集重新运行了基准测试,并观察到以下结果:
使用画布:
动画场景图:
这些结果大大改变了我的原始结论:
有关-Djavafx.animation.fullspeed=true
属性的更多详细信息,请参见this answer。