Существует несколько факторов, которые могут замедлить частоту кадров:
- Вы выводите частоту кадров на консоль каждый кадр.Это очень медленная операция, которая также может замедлить частоту кадров.(Вероятно, это самый большой удар по производительности.)
- Вы вычисляете частоту кадров во время рендеринга кадров.С философской точки зрения, хороший пример принципа неопределенности Гейзенберга , поскольку, измеряя частоту кадров, вы влияете на нее и замедляете ее ...; -)
- Вы очищаете всюcanvas каждый раз, когда вы хотите перерисовать ваше изображение, а не только ту его часть, которая занимает изображение.(Изначально это оказалось не слишком важным фактором в моей версии вашего кода, но когда я отключил ограничение скорости JavaFX *1011* - см. Обновление ниже - оказалось, чтоРазница.)
Что касается частоты кадров, в приведенной ниже версии я записываю время (в наносекундах) первого кадра и подсчитываю количество нарисованных кадров.Когда приложение выходит, оно сообщает о средней частоте кадров.Это более простое вычисление, которое не слишком сильно мешает операциям внутри обработчика анимации и является хорошим показателем общей производительности.(Временные рамки каждого кадра будут значительно различаться из-за сбора мусора, других процессов, улучшений компиляции 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)}")
}
}
(Кстати: избегайте использования операторов var
в Scala всякий раз, когда это возможно. Общее изменяемое состояние неизбежно при использовании JavaFX / ScalaFX , но Property
s предоставляют гораздо лучшие механизмы для борьбы с ним.чтобы привыкнуть к использованию val
объявлений элементов, если они действительно, действительно не должны быть var
s. И если вам действительно нужно использовать var
s, они почти всегда должны быть объявлены private
для предотвращениянеконтролируемый внешний доступ и модификация.)
Сравнительный анализ Java программ - это искусство, но очевидно, что чем дольше вы будете запускать каждую версию, тем лучше будет средняя частота кадров.На моей машине (с моим собственным изображением) я достиг следующих, довольно ненаучных результатов, после запуска приложения в течение 5 минут:
- Очистка всего холста и запись в консоль: 39.69 fps
- Очистка всего холста без вывода на консоль: 59,85 кадров в секунду
- Очистка только изображения без вывода на консоль: 59,86 кадров в секунду
Очистка только изображения, а не всего холстакажется, мало влияет, и немного удивил меня.Тем не менее, вывод на консоль оказал огромное влияние на частоту кадров.
Помимо использования холста, другая возможность состоит в том, чтобы просто расположить изображение в группе сцен, а затем переместить его, изменив его совместное изображение.ординаты.Код для этого приведен ниже (используя свойства для косвенного перемещения изображения):
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 кадров в секунду - почти точно так же, какиспользование холста.
В этом примере движение немного прерывистое, что вполне может быть вызвано циклами сборки мусора .Может быть, попробуйте поэкспериментировать с различными GC ?
Кстати, я перемещаю изображение в этой версии, чтобы что-то произошло.Если свойства не меняются, то я подозревал, что изображение не будет обновлено в этом кадре.Действительно, если я просто каждый раз устанавливаю свойства на одно и то же значение, частота кадров становится равной: 62,05 кадров в секунду.
Использование холста означает, что вы должны определить, что нарисовано, и как его перерисовать.Но использование графа сцены JavaFX (как в последнем примере) означает, что JavaFX заботится о том, нужно ли перерисовывать кадр.Это не имеет большого значения в данном конкретном случае, но может ускорить процесс, если между последовательными кадрами мало различий в содержании.Что-то иметь в виду.
Это достаточно быстро?Кстати, в этом конкретном примере много накладных расходов на контент.Я совсем не удивлюсь, если добавление других элементов в анимацию окажет очень мало влияния на частоту кадров.Вероятно, было бы лучше попробовать и посмотреть.Обращаемся к вам ...
(Еще одна возможность, касающаяся анимации, приведена в демонстрационной версии ColorfulCircles , которая поставляется с источниками ScalaFX .)
ОБНОВЛЕНИЕ : я упоминал об этом в комментарии, но, возможно, стоит выделить его в основном ответе: JavaFX имеет значение по умолчанию ограничение скорости из60 кадров в секунду, что также влияет на сравнительный анализ, описанный выше, и объясняет, почему ЦП и ГП не используются лучше.
Чтобы приложение могло работать с максимально возможной частотой кадров (возможно, это не очень хорошая идея, если выВы хотите максимально увеличить заряд батареи на ноутбуке или повысить общую производительность приложения), включите следующее свойство при запуске приложения:
-Djavafx.animation.fullspeed=true
Обратите внимание, что это свойство недокументировано ине поддерживается, это означает, что он может исчезнуть в будущей версии JavaFX .
Я повторно запустил тесты с этим набором свойств и наблюдал эти результатыs:
Использование холста:
- Очистка всего холста и запись в консоль: 64,72 кадров в секунду
- Очистка всего холста, без вывода на консоль: 144,74 кадров в секунду
- Очистка только изображения без вывода на консоль: 159,48 кадров в секунду
Анимация графика сцены:
- Нет вывода на консоль: 217,68 кадров в секунду
Эти результаты существенно меняют мои первоначальные выводы:
- Рендеринг изображений - и даже их анимация - использование графа сцены намного эффективнее (на 36% лучше с точки зрения частоты кадров), чем лучшийРезультат получен при нанесении изображения на холст.Это не является неожиданным, учитывая, что график сцены оптимизирован для повышения производительности.
- При использовании холста очистка только области, занятой изображением, имеет примерно на 10% (в этом примере) лучшую частоту кадров, чем очисткавесь холст.
См. этот ответ для получения дополнительной информации о свойстве javafx.animation.fullspeed
.