I plan to make a rhythm game by using ScalaFX with canvas, When I try to run the code, I found that it consumes a lot of GPU, and sometimes the frame rate drop at 30 fps, even I only draw one image on the canvas without drawing any animated note, dancer, process gauge, etc.
Below is my code
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()
}
how can I improve the performance or is there any better ways to do the same thing without using canvas?
There are a few factors that are potentially slowing down the frame rate:
Regarding the frame rate, in the version below, I record the time (in nanoseconds) of the first frame, and count the number of frames drawn. When the application exits, it reports the average frame rate. This is a simpler calculation that doesn't interfere too much with the operations inside the animation handler, and is a good measure of overall performance. (There's going to be considerable variability in the timing of each frame, due to garbage collection, other processes, JIT compilation improvements, etc. We'll try to skip over all of that by looking at the average rate.)
I also changed the code to clear only the region occupied by the image.
I've also simplified your code a little, to make it slightly more conventional in its use of ScalaFX (using the stage
member of the main class, for instance, as well as making more use of type inference):
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)}")
}
}
(BTW: avoid use of var
statements in Scala whenever possible. Shared mutable state is unavoidable when using JavaFX/ScalaFX, but Property
s provide much better mechanisms for dealing with it. Try to get into the habit of using val
element declarations unless they really, really do need to be var
s. And if you do need to use var
s, they should nearly always be declared private
to prevent uncontrolled external access and modification.)
Benchmarking Java programs is an art form, but clearly, the longer you run each version, the better the average frame rate is going to be. On my machine (with an image of my own) I achieved the following, rather unscientific, results after running the application for 5 minutes:
Clearing just the image, rather than the whole canvas appears to have little effect, and surprised me a little. However, outputting to the console had a huge effect on the frame rate.
Aside from using a canvas, another possibility is to simply position an image within a scene group, and then move it around by changing its co-ordinates. The code to do that is below (using properties to indirectly move the image):
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)}")
}
}
This produces a mean frame rate for me, after 5 minutes of running, of 59.86 fps—almost exactly the same as using a canvas.
In this example, the motion is a little jerky, which could well be caused by garbage collection cycles. Maybe try experimenting with different GC's?
BTW, I move the image around in this version to force something to happen. If the properties don't change, then I suspected that the image would not be updated that frame. Indeed, if I just set the properties to the same value each time, the frame rate becomes: 62.05 fps.
Using the canvas means that you have to determine what is drawn, and how to redraw it. But using the JavaFX scene graph (as in the last example) means that JavaFX takes care of figuring out whether the frame even needs to be redrawn. It doesn't make a big difference in this particular case, but it might speed things up if there are few content differences between successive frames. Something to bear in mind.
Is that fast enough? BTW, there's a lot of overhead in relation to content in this particular example. I wouldn't be at all surprised if adding other elements to the animation had very little impact upon the frame rate. It would probably be best to try it and see. Over to you...
(For another possibility regarding animation, refer to the ColorfulCircles demo that comes with the ScalaFX sources.)
UPDATE: I mentioned this in a comment, but it's perhaps worth highlighting in the main answer: JavaFX has a default speed limit of 60 fps, which also impacts the benchmarking above—and which also explains why the CPU and GPU are not better utilized.
To enable your application to run at the highest possible frame rate (possibly not a great idea if you're looking to maximize battery charge on a laptop, or to improve overall application performance), enable the following property when running your application:
-Djavafx.animation.fullspeed=true
Note that this property is undocumented and unsupported, meaning that it may go away in a future version of JavaFX.
I re-ran the benchmarks with this property set, and observed these results:
Using a canvas:
Animating a scene graph:
These results change my original conclusions significantly:
Refer to this answer for further details about the javafx.animation.fullspeed
property.