DirectX Графика в проектах Delphi

Полноэкранные приложения



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

Вы наверняка заметили, что профессионально написанные игры работают с удовлетворительной скоростью даже на компьютерах, оснащенных слабой видеокартой. И это при обилии графики, когда на экране мы видим десятки одновременно движущихся персонажей. Основной прием, которым достигается высокая скорость, заключается в том, что игра использует палитру из 256 цветов. Иногда кажется просто невероятным, но это действительно так. Профессиональные художники мастерски создают иллюзию богатства красок, опираясь всего лишь на 8-битную палитру. Чтобы закрепить эту иллюзию, заставки игр намеренно рисуются особенно красочными, подчас не ограничиваясь 256 цветами.
Конечно, при использовании 16-битного режима ваши приложения выиграют в эффектности, но если вы пишете масштабный проект и используете действительно много образов, то удовлетворительную скорость получите далеко не на каждом компьютере.
В проекте каталога Ех03, как и в большинстве остальных примеров книги, на основе DirectDraw используется режим в 256 цветов. Пример по функциональности очень похож на предыдущий, но вместо стрелки здесь мышью передвигается образ страшного дракона (рис. 3.3).

Рис. 3.3. Для фона используется 256-цветный рисунок

Чтобы не иметь проблем с масштабированием, размеры фонового рисунка равны 640x480 пикселов.
В проекте появилась свойственная всем приложениям, использующим 256-цветный режим, работа с палитрой. Для корректного вывода растра
нужно загрузить и установить на экране именно его палитру. Поэтому появился специальный объект:

FDDPal : IDirectDrawPalette;

Напомню, что этому специальному объекту в начале работы приложения должно быть присвоено значение nil, а в конце работы перед аналогичным присвоением должен вызываться метод Release.
Сразу после создания первичной поверхности устанавливаем в ней палитру, загружаемую из фонового изображения. Для загрузки набора цветов вызываем пользовательскую функцию незабвенного модуля DDUtil:


// Загружаем палитру растра
FDDPal := DDLoadPalette (FDD, groundBmp) ;
if FDDPal = nil then ErrorOut (DD_FALSE, 'DDLoadPalette');

Устанавливается палитра с помощью специального метода поверхности:

// Устанавливаем палитру
hRet := FDDSPrimary. SetPalette (FDDPal) ;
if Failed(hRet) then ErrorOut (hRet, 'SetPalette');

Растр намеренно выбран с подходящими размерами, чтобы не пришлось его масштабировать. Поэтому последние два аргумента DDLoadBitmap равны нулю:

FDDSBackGround := DDLoadBitmap (FDD, groundBmp, 0, 0) ;
if FDDSBackGround = nil then ErrorOut (DD_FALSE, 'DDLoadBitmap');

Дракон нарисован с черным контуром. Для цветового ключа берется цвет фона:



hRet := DDSetColorKey (FDDSImage, RGB(0, 255, 255)); if Failed (hRet) then ErrorOut (hRet, 'DDSetColorKey');

Поскольку фон не масштабируется, при восстановлении поверхностей перезагрузка фонового рисунка не приведет к зернистости. При восстановлении поверхностей следует также заново загружать и устанавливать палитру лишь при успешном восстановлении первичной поверхности:

function TfrmDD.RestoreAll : HRESULT;
var
hRet : HRESULT;
begin
hRet := FDDSPrimary._Restore;
if Succeeded (hRet) then begin
FDDPal := nil; // Удаляем старую палитру
FDDPal := DDLoadPalette (FDD, groundBmp); // Перезагружаем ее
if FDDPal <> nil then begin // Палитра перезагружена успешно
// Заново ее устанавливаем
hRet := FDDSPrimary.SetPalette(FDDPal);
if Failed (hRet) then ErrorOut(hRet, 'SetPalette'); end
else ErrorOut(DDERR_PALETTEBUSY, 'DDLoadPalette'); hRet := FDDSBackGround._Restore;
if Failed (hRet) then begin
Result := hRet; Exit;
end;
hRet := DDReLoadBitmap(FDDSBackGround, groundBmp);
if Failed (hRet) then ErrorOut(hRet, 'DDReLoadBitmap'); hRet := FDDSImage._Restore; if Failed (hRet) then begin Result := hRet;
Exit;
end;
Result := DDReLoadBitmap(FDDSImage, imageBmp); end else Result := hRet;
end;

При неудачной перезагрузке и установлении палитры нет смысла продолжать работу приложения. Константу для вывода сообщения о фатальной ошибке я взял произвольно из ряда ошибок, связанных с неудачной работой с палитрами.
Также не имеет смысла продолжать работу приложения, если не удается попытка заново загрузить файл растра. Он ведь может быть просто удален.
Изменился немного и обработчик перемещения курсора. Теперь проблема с положением курсора вблизи границ решена:



procedure TfrmDD.FormMouseMove(Sender: TObject; Shift: TShiftState;
X, Y: Integer);
begin
if X <= ScreenWidth - 64 then mouseX := X
else mouseX := ScreenWidth - 64; // Добавилась эта ветвь
if Y <= ScreenHeight - 64 then mouseY := Y
else mouseY := ScreenHeight - 64; // Этого тоже не было FormPaint (nil);
end;

Новый пример (проект каталога Ех04) позволит нам плавно перейти к теме анимации в приложениях. Изменим предыдущий пример таким образом, чтобы изображение беспрерывно обновлялось.
Для получения максимальной скорости обновления необходим обработчик события Onidie компонента класса TAppiicationEvents. Код, записанный в этом обработчике, будет выполняться беспрерывно, пока приложение находится в режиме ожидания сообщений.
Нам нужно будет в этом обработчике записать код, связанный с перерисовкой первичной поверхности. Однако в ситуации, когда приложение не активно или минимизировано, тратить ресурсы компьютера на заведомо безуспешное и ненужное действо совершенно ни к чему. Поэтому состояние активности будем отслеживать:

FActive : BOOL; // Переменная хранит информацию о текущем состоянии

Устанавливается эта переменная в True при активации окна приложения и при восстановлении его из минимизированного состояния:

procedure TfrmDD.ApplicationEventslRestore(Sender: TObject);
begin
WindowState := wsMaximized;
// После распахивания окна считаем его готовым к воспроизведению
FActive := True; end;
// Появился новый обработчик
procedure TfrmDD.FormActivate(Sender: TObject); begin
FActive := True; // После запуска приложения оно готово к работе
end;

Обработчик события onPaint нам теперь не нужен, а код, связанный с перерисовкой окна, разделим для удобства на две функции, отвечающие за непосредственную перерисовку окна и за переключение страниц:

function TfrmDD.UpdateFrame : HRESULT; // Функция перерисовки окна
var
hRet : HRESULT;
begin
// Заполняем фон
hRet := FDDSBack.BltFast (0, 0, FDDSBackGround, nil, DDBLTFAST_WAIT);
if hRet = DDERR_SURFACELOST then begin hRet := RestoreAll;
if Failed (hRet) then begin // Полная неудача Result := hRet;
Exit;
end;
end;
// Выводим изображение
hRet := FDDSBack.BltFast (mouseX, mouseY, FDDSImage, nil,
DDBLTFAST WAIT or DDBLTFAST SRCCOLORKEY);
if hRet = DDERR_SURFACELOST then begin hRet := RestoreAll; if Failed (hRet) then begin Result := hRet;
Exit;
end;
end;
Result := DD_OK;
end;
// Функция переключения страниц function TfrmDD.FlipPages : HRESULT;
begin
Result := FDDSPrimary.Flip(nil, DDFLIP_WAIT);
if Result = DDERR_SURFACELOST then Result := RestoreAll;
end;



Обращаю внимание, что зацикливание при каждом вызове методов блиттинга теперь не используется, при первой же неудаче покидаем функцию перерисовки окна. Код воспроизведения у нас и так выполняется беспрерывно, и на смену потерянному кадру тут же, после восстановления поверхностей, приходит новый кадр.
Но особая неприятность состоит в том, что при использовании зацикливания возникает неприятная проблема: после минимизации приложения оно перестает реагировать на сообщения и зацикливается на восстановлении поверхностей.
Последнее, и самое главное, что добавилось - это обработчик события ожидания:

procedure TfrmDD.ApplicationEventslIdle(Sender: TObject;
var Done: Boolean); begin
if FActive then // Только при активном состоянии приложения
if Succeeded (UpdateFrame) // Перерисовка окна прошла успешно
then FlipPages; // Переключаем страницы
// Посмотреть, не появились ли сообщения Done := False;
end;

Я видел немало приложений, использующих DirectDraw, написанных на Delphi. И очень многие из них не могли корректно минимизироваться или восстановиться. Хорошенько "погоняйте" этот пример, убедитесь, что в данных ситуациях он ведет себя корректно.
В примере непрерывно перерисовывается экран, положение образа на нем меняется только после передвижения курсора. Модифицируем проект, заставив двигаться картинку по кругу.
Переходим к проекту каталога Ех05. В нем удалены переменные, хранящие текущие координаты курсора, и введена вспомогательная переменная, содержащая текущее значение угла:

Angle : Single = 0;

Размер растра - 64x64 пиксела. Текущее положение на экране его центра опирается на значение переменной Angle:

FDDSBack.BltFast (320 + trunc (cos(Angle) * ISO) - 32,
240 + trunc (sin(Angle) * 150) - 32,
FDDSImage, nil,
DDBLTFAST_WAIT or DDBLTFAST_SRCCOLORKEY);

Менять значение Angle при каждой перерисовке окна будет неправильным решением, т. к. частота перерисовки экрана сильно зависит от конфигурации компьютера, поэтому на различных компьютерах картинка начнет передвигаться с разной скоростью. И самое неприятное здесь то, что на быстродействующих компьютерах изображение будет двигаться так быстро, что пользователь не сможет разглядеть на экране вообще ничего.
Традиционное решение состоит в том, что процесс смены положений опирается на системный таймер. Экран перерисовывается так часто, как это позволяет компьютер, но положение образов меняется лишь через определенные промежутки времени.
Класс формы дополнился двумя переменными, предназначенными для контроля промежутка времени:



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 величиной в три десятка, это очень хорошая цифра. Большие значения свидетельствуют о том, что у проекта есть еще существенный запас для обогащения экрана или алгоритма.
Еще одна тонкость получаемой величины связана с использованием цикла ожидания. Наивысшая скорость воспроизведения нашего приложения будет в случае, когда операционная система не слишком загружена, т. к. параллельная работа других приложений может серьезно снизить производительность нашего приложения.


Содержание раздела