Пакетная отрисовка

Во время игры вы могли поймать ощущение, что ввод тормозит. Предлагаю добавить FPS-счётчик, чтобы следить за скоростью отрисовки. FPS (Frames per second) — это количество кадров в секунду. Мы будем ориентироваться на 60 FPS.

Счётчик FPS

Начнём с добавления счётчика FPS. Эта задача состоит из двух частей:

  1. получение или вычисление значения FPS,
  2. отображение этого значения на экране.

Для первой ggez предоставляет метод получения FPS. Для второй в нашей системе отрисовки уже есть способ работы с текстом, так что нам нужно просто вывести количество кадров в секунду. Объединим эти знания в следующем коде:


#![allow(unused)]
fn main() {
// rendering_system.rs
{{#include ../../../code/rust-sokoban-c03-04/src/systems/rendering_system.rs:66}}
        ...

{{#include ../../../code/rust-sokoban-c03-04/src/systems/rendering_system.rs:114:118}}

        ...
{{#include ../../../code/rust-sokoban-c03-04/src/systems/rendering_system.rs:123}}
}

Запустите игру и немного подвигайтесь туда-сюда. Вы увидите, что FPS значительно отличается от ожидаемых 60. У меня это что-то в районе 20-30, но это зависит от мощности вашей машины.

low fps

Что вызывает падение частоты кадров?

Вы можете задаться вопросом, что же такого мы натворили, чтобы игра работала так медленно? У нас довольно простая игра — и логика передвижения и обработки событий клавиш совсем не такая и сложная. Кроме того, у нас нет большого количества сущностей и компонентов, чтобы оправдать значительное падение частоты кадров. Чтобы его объяснить, нам нужно немного углубиться в принцип работы нашей системы отрисовки.

Сейчас перед тем, как отрисовать сущность, мы должны понять, какое изображение ей соответствует. Это значит, что если нам, например, нужно отрисовать 20 плиток пола, то мы загрузим 20 картинок пола и сделаем 20 вызовов отрисовки. Это очень дорогостоящий процесс — и именно он вызывает падение частоты кадров.

Как это можно исправить? Мы воспользуемся магией пакетной отрисовки. Используя эту технику, мы загрузим изображение только один раз и сообщим ggez, что нам нужно отрисовать его на двадцати необходимых позициях. Это значительно ускорит процесс. В качестве примечания: некоторые движки делают это самостоятельно, без вашего вмешательства — но не 'ggez'. Здесь нам нужно позаботиться обо всём вручную.

Пакетная отрисовка

Вот что нам нужно сделать, чтобы реализовать пакетную отрисовку:

  • Для каждой отрисовываемой сущности мы должны понять, какое изображение нам нужно отрисовать и какой параметр DrawParam нужно использовать. Для ggez DrawParam будет служить индикатором места отрисовки.
  • После чего мы должны сохранить пару (image, DrawParams) в удобный формат.
  • Для каждого изображения пройдём по (image, DrawParams), отсортированным по z, и сделаем по одному вызову отрисовки

Прежде чем мы погрузимся в код отрисовки, мы должны сгруппировать и отсортировать нашу коллекцию. Для этого мы будем использовать крейт Itertools. Мы могли бы реализовать группировку самостоятельно, но нет никакой причины заново изобретать велосипед. Добавим Itertools как зависимость в наш проект.

// Cargo.toml
[dependencies]
ggez = "0.9.3"
glam = { version = "0.24", features = ["mint"] }
hecs = "0.10.5"

А также импортируем его в нашу систему отрисовки.


#![allow(unused)]
fn main() {
// rendering_system.rs
{{#include ../../../code/rust-sokoban-c03-04/src/systems/rendering_system.rs:11}}
}

Помните функцию get_image, которую мы написали в главе "Анимация", чтобы понять, какое изображение нужно для каждого кадра? Мы сможем использовать её снова — нужно только убедиться, что мы не загружаем изображение, а просто возвращаем его путь.


#![allow(unused)]
fn main() {
// rendering_system.rs
{{#include ../../../code/rust-sokoban-c03-04/src/systems/rendering_system.rs:36:53}}
}

Теперь определим формат, в котором мы будем хранить наши пакетные данные. Мы будем использовать HashMap<u8, HashMap<String, Vec<DrawParam>>>, где:

  • Первый ключ (u8) — это позиция z. Помните о том, что мы должны следить за ней и отрисовывать объекты начиная с самого большого z до самого маленького, чтобы соблюдать правильный порядок (например, чтобы полы были позади игрока).
  • Первое значение — это ещё одна HashMap, где второй ключ (String) — это путь к изображению.
  • Наконец, второе значение — это Vec<DrawParam>, где хранятся все параметры, с которыми мы должны отрисовать это изображение.

Теперь напишем код, чтобы заполнить нашу rendering_batches.


#![allow(unused)]
fn main() {
// rendering_system.rs
{{#include ../../../code/rust-sokoban-c03-04/src/systems/rendering_system.rs:66}}
        ...

{{#include ../../../code/rust-sokoban-c03-04/src/systems/rendering_system.rs:72:94}}

        ...
{{#include ../../../code/rust-sokoban-c03-04/src/systems/rendering_system.rs:123}}
}

И наконец, отрисуем пакеты. Мы больше не можем использовать функцию draw(image), которой мы пользовались раньше, но, к счастью, у ggez есть пакетный API — SpriteBatch. Также отметьте себе строчку sorted_by — это функция из Itertools.


#![allow(unused)]
fn main() {
// rendering_system.rs
{{#include ../../../code/rust-sokoban-c03-04/src/systems/rendering_system.rs:66}}
        ...

{{#include ../../../code/rust-sokoban-c03-04/src/systems/rendering_system.rs:96:112}}

        ...
{{#include ../../../code/rust-sokoban-c03-04/src/systems/rendering_system.rs:123}}
}

Ну вот и всё! Снова запустите игру — и вы увидите новенькие и блестящие 60 FPS! Теперь всё должно работать гораздо плавнее.

low fps

КОД: Увидеть весь код из данной главы можно здесь.