Михаил Краснов - Графика DirectX в Delphi
Традиционное решение состоит в том, что процесс смены положений опирается на системный таймер. Экран перерисовывается так часто, как это позволяет компьютер, но положение образов меняется лишь через определенные промежутки времени.
Класс формы дополнился двумя переменными, предназначенными для контроля промежутка времени:
ThisTickCount : DWORD; // Текущее "время" LastTickCount : DWORD; // Время последнего обновления
При активизации приложения запоминаем текущее значение системного времени. Функция GetTickCount возвращает количество миллисекунд, прошедших со времени запуска операционной системы:
LastTickCount := GetTickCount;
Функция перерисовки кадра начинается с того, что мы выясняем, подошло ли время смены положения образа:
ThisTickCount := GetTickCount; // Текущее "время"
if ThisTickCount - LastTickCount > 60 then begin // Пора менять место
Angle := Angle + 0.05; // Для плавности смены положения образа
// Для предотвращения переполнения
if Angle > 2 * Pi then Angle := Angle - 2 * Pi;
LastTickCount := GetTickCount; // Запомнили время смены положения end;
Итак, картинка сменяется с максимальной частотой, но образ передвигается только по истечении некоторого промежутка времени. Значение задержки мы задаем сами, добиваясь плавности движения.
Теперь займемся подсчетом количества воспроизводимых в секунду кадров (FPS, Frames Per Second) в проекте каталога Ех06. Здесь добавились вспомогательные переменные, связанные с подсчетом кадров:
Frames : Integer =0; // Счетчик кадров FPS : PChar = ''; // Выводимая строка
При каждом воспроизведении увеличиваем счетчик, а через установленный промежуток времени подсчитываем частоту воспроизведения:
Inc (Frames); // Увеличиваем счетчик, воспроизводим очередной кадр if ThisTickCount - LastTickCount > 60 then begin
Angle := Angle + 0.05;
if Angle > 2 * Pi then Angle := Angle - 2 * Pi;
// Определяем и форматируем частоту
FPS := PChar ('FPS = ' + Format('%6.2f,
[Frames * 1000 / (ThisTickCount - LastTickCount)]));
Frames := 0; // Обнуляем счетчик
LastTickCount := GetTickCount; end;
Заполнив фон, выводим на экран найденную величину с помощью функции GDI Textout. He станем тратить время на особые украшения, текст выводится черным по белому:
if Succeeded (FDDSBack.GetDC (DC)) then begin //DC получен
TextOut (DC, 20, 20, FPS, 12); // Выводим строку длиной в 12 символов FDDSBack.ReleaseDC (DC); // DC обязательно должен освобождаться
end;
Найденная частота воспроизведения не соответствует, конечно, действительной частоте появления кадров на экране. Ведь, если эта цифра получается величиной несколько сотен, то она превышает максимальную частоту развертки монитора. Мы никак не сможем вывести на экран так много кадров за одну секунду. FPS в действительности отражает частоту обновления экранного буфера.
Конечно, чем больше эта величина, тем больше радостных чувств она вызывает у разработчика, если ваше масштабное приложение имеет FPS величиной в три десятка, это очень хорошая цифра. Большие значения свидетельствуют о том, что у проекта есть еще существенный запас для обогащения экрана или алгоритма.
Еще одна тонкость получаемой величины связана с использованием цикла ожидания. Наивысшая скорость воспроизведения нашего приложения будет в случае, когда операционная система не слишком загружена, т. к. параллельная работа других приложений может серьезно снизить производительность нашего приложения.
Частичное обновление экрана
Частичное обновление экрана используется для повышения быстродействия, т. к. при каждой смене положения образа обновляется только участок поверхности, занимаемый им ранее.
Посмотрим на практике, как это можно осуществить. Проект каталога Ех07 является модификацией предыдущего примера, проекта с подсчетом FPS.
Получающееся теперь значение FPS может вам показаться огромным, но, с очень небольшой долью лукавства, его вполне можно считать истинным. Лукавство состоит в том, что экранный буфер обновляется частично, а не целиком.
Теперь только в начале работы и при восстановлении первичной поверхности на передний и задний буферы помещается растровое изображение, соответствующее фону:
if FDDSBack.BltFast (0, 0, FDDSBackGround, nil, DDBLTFAST_WAIT) = DDERR__SURFACELOST then Close;
if FDDSPrimary.BltFast (0, 0, FDDSBackGround, nil, DDBLTFAST_WAIT) = DDERR_SURFACELOST then Close;
Обновление кадра объединяет собственно воспроизведение и переключение страниц. Хоть функция перерисовки кадра и вызывается все также беспрерывно, но при каждом вызове на экране только выводится текущее значение FPS, а изменения в картинку вносятся через некоторые промежутки времени, при перемещении образа:
function TfrmDD.UpdateFrame : HRESULT;
var
DC : HOC; wrkRect : TRECT;
begin
Result := DD_FALSE;
ThisTickCount := GetTickCount;
Inc (Frames) ;
if ThisTickCount - LastTickCount > 60 then begin
// Прямоугольник, соответствующий старому положению образа SetRect (wrkRect, 288 + trunc (cos (Angle) * 150),
208 + trunc (sin (Angle) * 150),
352 + trunc (cos (Angle) * 150),
272 + trunc (sin (Angle) * 150));
Angle := Angle + 0.05;
if Angle > 2 * Pi then Angle := Angle -2 * Pi;
//На задней поверхности выводим образ в новом месте if FDDSBack.BltFast (288 + trunc (cos(Angle) * 150),
208 + trunc (sin(Angle) * 150),
FDDSImage, nil,
DDBLTFAST_WAIT or DDBLTFAST_SRCCOLORKEY) = DDERR_SURFACELOST then if Failed (RestoreAll) then Exit;
FPS := PChar ('FPS = ' + Format('%6.2f ,
[Frames * 1000 / (ThisTickCount - LastTickCount)]));
Frames := 0;
LastTickCount := GetTickCount;
// Переключаем страницы, на переднем буфере образ в новом месте if FDDSPrimary.Flip(nil, DDFLIP_WAIT) = DDERR_SURFACELOST
then if Failed (RestoreAll) then Exit;
// Стираем образ на заднем буфере
if FDDSBack.Blt (SwrkRect, FDDSBackGround, @wrkRect, DDBLT_WAIT, nil) = DDERR_SURFACELOST then if Failed (RestoreAll) then Exit;
end;
if Succeeded (FDDSPrimary.GetDC (DC)) then begin
TextOut (DC, 20, 20, fps, 12);
FDDSPrimary.ReleaseDC (DC);
end;
Result := DD_OK;
end;
Приводимый здесь код немного отличается от действительного, я сократил изнурительные проверки результата.
Значение FPS выводится непрерывно, при каждом обновлении кадра. Этого, в принципе, можно и не делать, а отображать его только при смене положения образа. Тогда значение FPS станет еще больше. Просто в этом случае под его значением нельзя понимать частоту обновления экранного буфера, ведь в экранную память большую часть времени не будут вноситься вообще никакие изменения.
Вы можете значительно уменьшить интервал паузы между перемещениями образа, повышая тем самым частоту (частичного) обновления экрана, но получающееся значение FPS все равно будет значительным, всегда большим, чем в предыдущем примере.
В данном разделе мы рассмотрели один из приемов, используемых профессиональными разработчиками игр. Иногда на медленных компьютерах, при скроллинге экрана или быстром перемещении, хорошо заметно "торможение" воспроизведения, вызванное тем, что при этом перерисовывается весь экран, а не его часть. Для ослабления такого эффекта дизайнеры часто уменьшают игровой экран, располагая по границе его различные панели и меню.
Непосредственный доступ к пикселам оверхности
Прямой доступ к графическим данным обеспечивает максимум быстродействия, и предоставляет разработчику возможность реализации любых, или почти любых, действий с изображением.
На время прямого доступа поверхность должна быть заблокирована, после работы поверхность необходимо разблокировать. Во время блокировки поверхности операционная система находится в особом режиме, поэтому блокировка должна применяться в течение максимально короткого промежутка времени.
Блокирование поверхности является одним из самых спорных моментов в DirectDraw. Фактически она означает исключительный доступ к разделу памяти, связанному с поверхностью. Если для работы с обычными переменными, например, при копировании одной строки в другую, нам не приходится блокировать память, ассоциированную с данными, то почему же при прямом доступе к памяти поверхности нам непременно следует блокировать эту память? Запирать поверхность необходимо, поскольку позиция поверхности в системной памяти может меняться, системный менеджер памяти по каким-то своим соображениям может перемещать блоки памяти.
Работа с данными, размещаемыми в видеопамяти, в принципе отличается от привычной. Позже мы узнаем, как избавиться от блокирования поверхности, размещенной в системной памяти, если это необходимо.
Перейдем к проекту каталога Ех08. Смысл примера таков: не будем использовать растровое изображение в качестве фона, а для заполнения его, получив адрес поверхности заднего буфера в памяти, заполним нулем блок памяти этого буфера.
Память поверхности всегда организована линейно, поэтому обращение к данным сильно упрощается.
В коде удалены все фрагменты, связанные в предыдущем примере с фоном, включая палитру. Добавилась функция быстрой очистки заднего буфера:
function TfrmDD. Clear : HRESULT; var
desc : TDDSURFACEDESC2; // Вспомогательная структура
hRet : HRESULT; begin
Result := DD_FALSE;
ZeroMemory (@desc, SizeOf (desc) ) ; // Обычные действия с записью
desc.dwSize := SizeOf (desc) ;
// Запираем задний буфер
hRet := FDDSBack. Lock (nil, desc, DDLOCK_WAIT, 0) ;
if Failed (hRet) then begin
Result := hRet;
Exit;
end;
// Заполняем нулем блок памяти заднего буфера