ГЛАВА 5 Пишем игру
Оригинальный сплэш
Космический истребитель
Игра "Меткий стрелок"
Работа с клавиатурой
Работа с мышью
Вывод текста
Создание консоли
Диалоговые окна
Использование отсечения в полноэкранном приложении
Библиотека CDX
Что вы узнали в этой главе
В данной главе освещаются вопросы использования знаний, полученных в предыдущих
главах, для разработки проекта-каркаса полноценной игры. Такие проекты обычно
называются "движками".
Примеры располагаются в каталоге \Examples\Chapter05.
Оригинальный сплэш
Так в обиходе программисты называют заставки, появляющиеся в начале работы приложения
(splash - красочное пятно, всплеск). Выводят их на время долгой инициализации
основного модуля, как правило, для того, чтобы скрасить секунды (или минуты)
ожидания пользователя. Иногда их использованием авторы преследуют цель поразить
воображение зрителя, заявить на весь мир о своих выдающихся художественных и
профессиональных способностях.
В этом разделе я приведу пример, который может стать основой для создания вашей
собственной оригинальной заставки. Это проект каталога Ex01. Во время его работы
посередине рабочего стола выводится изображение земного шара, на фоне которого
вращается фраза "DirectX". На неискушенных пользователей окна непрямоугольной
формы обычно производят сильное впечатление. Подобные окна можно создавать разными
способами, например, с помощью регионов. Мы же решим задачу обычным для DirectDraw
способом. Совсем необязательно должно получаться именно круглое окно, как в
моем примере.
Приемы, используемые в проекте, во многом вам знакомы по примерам предыдущих
глав, однако добавилось и кое-что новое.
Заставка должна появляться всегда посередине экрана, при любых установленных
разрешениях, поэтому в начале работы нам необходимо определить текущие размеры
рабочего стола, относительно середины которого выверить координаты вывода нашего
образа размером 256x256 пикселов:
HalfWidth := (GetSystemMetrics (SM_CXSCREEN) -256) div2;
HalfHeight := (GetSystemMetrics(SM_CYSCREEN) - 256) div 2;
Примечание
Конечно, если по ходу работы заставки пользователь поменяет настройки рабочего
стола, значения установок, полученные нами в начале работы, станут неактуальны.
Но нет смысла вычислять их значения беспрерывно, ведь в ситуации смены режима
дальнейший вывод будет невозможен, точно так же, как и для любого другого приложения,
использующего DirectDraw.
Уровень кооперации устанавливается нормальным, а очередной кадр не выходит
за границу предыдущего. Поэтому наша заставка эффектно располагается поверх
всех окон, и нет необходимости производить манипуляций с ее фоном (запоминать
и восстанавливать для каждого кадра).
Но для того, чтобы заставка не исчезла при изменениях на рабочем столе, ее необходимо
постоянно перерисовывать. Чтобы перерисовка протекала с большим эффектом, работают
с двумя образами: земного шара и вращающейся надписи.
Мы уже использовали прием с вращением изображения, основанный на непосредственном
доступе к 8-битной поверхности. Пример этой главы рассчитан на, минимум, 16-разрядную
глубину поверхности, а вызываемая нами тогда функция вращения для такого режима
требует корректировки.
Я переписал эту функцию. Теперь поворачивается содержимое передаваемого объекта
класса TBitmap, и возвращается объект такого же класса:
function TfrmDD.RotateBmp (const BitmapOriginal: TBitmap;
const iRotationAxis, jRotationAxis: Integer;
const AngleOfRotation: Single): TBitmap;
const
MaxPixelCount = 32768;
type
TRGBTripleArray = Array [0..MaxPixelCount-1] of TRGBTriple;
pRGBTripleArray = ATRGBTripleArray;
var
cosTheta Single;
i : Integer;
iOriginal : Integer;
iPrime : Integer;
j Integer;
jOriginal : Integer;
jPrime : Integer;
RowOriginal : pRGBTripleArray;
RowRotated : pRGBTRipieArray;
sinTheta : Single;
begin
Result := TBitmap.Create; // Создание результирующего растра
Result.Width := BitmapOriginal.Widths
Result .Height := BitmapOriginal.Height;
Result.PixelFormat := pf24bit; // Очень важно задать явно режим пиксела
sinTheta := sin (AngleOfRotation);
cosTheta := cos (AngleOfRotation);
// Расчет источника для пикселов повернутого растра
for j := Result.Height - 1 downto 0 do begin
RowRotated := Result.Scanline[j];
jPrime := j - JRotationAxis;
for i := Result.Width-1 downto 0 do begin
iPrime := i - iRotationAxis;
iOriginal := iRotationAxis + round(iPrime * CosTheta - jPrime *
sinTheta);
jOriginal := JRotationAxis + round(iPrime * sinTheta + jPrime *
cosTheta);
if (iOriginal >= 0) and (iOriginal <= BitmapOriginal.Width-1) and
(jOriginal >= 0) and (jOriginal <= BitmapOriginal.Height-1)
then begin
RowOriginal := BitmapOriginal.Scanline[jOriginal];
RowRotated[i] := RowOriginal[iOriginal]
end
else begin // "Новые" пикселы заполняются черным, цветом ключа
RowRotated[i].rgbtBlue := 0;
RowRotated[i].rgbtGreen := 0;
RowRotated[i].rgbtRed := 0
end
end
end;
end;
При перерисовке кадра поворачиваем первоначальное изображение на увеличивающийся угол, копируем полученный растр на вспомогательную поверхность, а затем формируем окончательную картинку:
function TfrmDD.UpdateFrame : HRESULT;
begin
// Повернутый растр копируем на поверхность
FDDSLogo with RotateBmp (wrkBitmap, 128, 128, Angle) do begin
DDCopyBitmap (FDDSLogo, Handle, 0, 0, Width, Height);
Free end;
Angle := Angle - 0.1;
// Наращиваем угол поворота
if Angle > - 2 * Pi then Angle := Angle + 2 * Pi;
// Теоретически возможные ошибки блиттинга игнорируем
// На заднем буфере подготавливаем итоговую картинку
FDDSBack.BltFast(О, О, FDDSImage, nil, DDBLTFAST_WAIT or
DDBLTFAST_SRCCOLORKEY); // Вывод фона, земной шар
FDDSBack.BltFast(О, О, FDDSLogo, nil, DDBLTFAST_WAIT or
DDBLTFAST_SRCCOLORKEY); // На фон накладываем повернутую надпись
// Вывод посередине экрана заставки
Result := FDDSPrimary.BitFast(HalfWidth, HalfHeight, FDDSBack,
nil, DDBLTFAST_WAIT or DDBLTFAST_SRCCOLORKEY);
end;
Перед окончанием работы заставку необходимо убрать с экрана. Обращаю внимание, как это делается: появился обработчик события onclose, в котором собственно окно приложения занимает всю область заставки:
procedure TfrmDD.FormClose(Sender: TObject; var Action: TCloseAction);
begin
Left := HalfWidth;
Top := HalfHeight;
Width := 256;
Height := 256;
end;
Космический истребитель
В этом разделе я представляю свою небольшую заготовку увлекательной игры. Проект
располагается в каталоге Ех02. Имитируется полет в космосе космического корабля
(рис. 5.1).
Рис. 5.1. Наконец-то мы подошли к серьезным примерам
С помощью клавиш перемещения курсора можно управлять направлением и скоростью
полета истребителя.
Для создания эффекта пространства звезды, на фоне которых располагается корабль,
разделены на три группы, различающиеся по яркости и скорости перемещения:
const
NuruStars =10; // Количество звезд в каждой группе
var
StepX : Integer =0; // Базовая скорость движения звезд
StepY : Integer = 1;
type
TCoord = record // Тип описания текущего положения звезды
X, Y : Integer;
end;
var // Массивы звезд
Starsl : Array [0..NumStars - 1] of TCoord;
Stars2 : Array [0..NumStars - 1] of TCoord;
Stars3 : Array [0..NumStars - 1] of TCoord;
В начале координаты звезд задаются, конечно, случайно. При каждом обновлении кадра они циклически движутся по экрану:
function TfrmDD.UpdateFrame : HRESULT;
var
i : Integer;
begin
ThisTickCount := GetTickCount;
if ThisTickCount - LastTickCount > 5 then begin
for i := 0 to NumStars - 1 do begin
// Первая группа звезд, самое медленное движение
Starsl [i].X := (Starsl [i].X + StepX);
if Starsl [i].X > ScreenWidth - 2 then Starsl [i].X := 0 else
if Starsl [i].X < 0 then Starsl [i].X := ScreenWidth - 2;
// Вторая группа звезд движется в два раза быстрее
Stars2 [i].X := (Stars2 [i].X + 2 * StepX);
if Stars2 [i].X > ScreenWidth - 2 then Stars2 [i].X := 0 else
if Stars2 [i].X < 0 then Stars2 [i].X := ScreenWidth - 2;
// Третья группа движется в три раза быстрее
Stars3 [i].X := (Stars3 [i].X + 3 * StepX);
if Stars3 [i].X > ScreenWidth - 2 then Stars3 [i].X := 0 else
if Stars3 [i].X < 0 then Stars3 [i].X := ScreenWidth - 2;
// Аналогично по координате Y
Starsl [i].Y := (Starsl [i].Y + StepY);
if Starsl [i].Y > ScreenHeight - 2 then Starsl [i].Y := 0 else
if Starsl [i].Y < 0 then Starsl [i].Y := ScreenHeight - 2;
Stars2 [i].Y := (Stars2 [i].Y + 2 * StepY);
if Stars2 [i].Y > ScreenHeight - 2 then Stars2 [i].Y := 0 else
if Stars2 [i].Y < 0 then Stars2 [i].Y := ScreenHeight - 2;
Stars3 [i].Y := (Stars3 [i].Y + 3 * StepY);
if Stars3 [i].Y > ScreenHeight - 2 then StarsS [i].Y := 0 else
if Stars3 [i].Y < 0 then Stars3 [i].Y := ScreenHeight - 2;
end;
LastTickCount := GetTickCount;
end;
Clear;
// Очистка заднего буфера
for i := 0 to NumStars - 1 do begin // Цикл рисования звезд
FDDSBack.BltFast (Starsl [i].X, Starsl [i].Y,
FDDSImagel, nil, DDBLTFAST_WAIT);
FDDSBack.BltFast (Stars2 [i].X, Stars2 [i].Y,
FDDSImage2, nil, DDBLTFAST_WAIT);
FDDSBack.BltFast (Stars3 [i].X, Stars3 [i].Y,
FDDSImageS, nil, DDBLTFAST_WAIT);
end;
// Рисование истребителя
Result := FDDSBack.BltFast (150, 140,
FDDSFighter, nil, DDBLTFAST_WAIT or DDBLTFAST_SRCCOLORKEY);
end;
Механизм поворота образа истребителя точно такой же, как в предыдущем примере, и основан на искажении первоначального растра. В данном примере нет необходимости обращаться к функции поворота в каждом кадре. Делается это только при нажатии клавиш:
procedure TfrmDD.FormKeyDown(Sender: TObject; var Key: Word;
Shift: TShiftState);
begin
if (Key = VK_ESCAPE) or (Key = VK_F12) then begin Close;
Exit;
end else
if Key = VK_LEFT then StepX := StepX + 1 else
if Key = VKJUGHT then StepX := StepX - I else
if Key = VK_UP then StepY := StepY + 1 else
if Key = VK_DOWN then StepY := StepY - 1;
// Ограничиваем углы поворота некоторыми пределами
if StepY < 1 then StepY := 1 else
if StepY > 3 then StepY := 3;
if StepX < -4 then StepX := -4 else
if StepX > 4 then StepX := 4;
// Копируем на поверхность истребителя новое изображение
with RotateBmp (wrkBitmap, 170, 135, arctan {StepX / StepY)) do begin
DDCopyBitmap (FDDSFighter, Handle, 0, 0, Width, Height);
Free end;
end;
При восстановлении поверхностей надо не просто восстановить содержимое поверхности истребителя, но и повернуть его образ соотвественно текущему положению:
function TfrmDD.RestoreAll : HRESULT;
var
hRet : HRESULT;
begin
hRet := FDDSPrimary._Restore;
if Succeeded (hRet) then begin
hRet := FDDSFighter._Restore;
if Failed (hRet) then begin
Result := hRet; Exit;
end;
// Поворот образа истребителя на текущий угол
with RotateBmp (wrkBitmap, 170, 135, arctan (StepX / StepY)) do begin
hRet := DDCopyBitmap (FDDSFighter, Handle, 0, 0, Width, Height);
Free end;
if Failed (hRet)
then ErrorOut(hRet, 'DDCopyBitmap');
hRet := FDDSImage3._Restore;
if Failed (hRet) then begin Result := hRet;
Exit;
end;
hRet := DDReLoadBitmap(FDDSImage3, starBmpS);
if Failed (hRet) then ErrorOut(hRet, 'DDReLoadBitmap');
hRet := FDDSImage2._Restore;
if Failed (hRet) then begin
Result := hRet;
Exit;
end;
hRet := DDReLoadBitmap(FDDSImage2, starBmp2);
if Failed (hRet) then ErrorOut(hRet, 'DDReLoadBitmap');
hRet := FDDSImagel._Restore;
if Failed (hRet) then begin
Result := hRet;
Exit;
end;
hRet := DDReLoadBitmap(FDDSImage1, starBmpl);
if Failed (hRet)
then ErrorOut(hRet, 'DDReLoadBitmap');
Result := DD_OK
end else Result := hRet;
end;
Ну что же, дорогой читатель, если вы истомились в ожидании "настоящих" примеров, то сейчас настало самое время встряхнуться и превратить этот пример в полноценную игру. Вам осталось добавить извергающиеся лучи мощного лазерного оружия, накатывающиеся астероиды и злобного и многочисленного противника. Сделайте все это, но не сейчас: прочтите до конца хотя бы эту главу.
Игра "Меткий стрелок"
При написании мало-мальски объемной игры необходимо применять приемы, которыми
новички часто пренебрегают, считая их малозначимыми. Знакомясь с примерами настоящей
главы, вы легко сможете убедиться, как важно придерживаться некоторых правил,
своими глазами вы увидите, как сильно выигрывает в этом случае приложение.
Следующий наш пример, проект каталога Ех03, является уже вполне законченной
игрой, хотя и носит своеобразный оттенок любительских творений.
Игра состоит в том, чтобы поразить всех монстров, беспрерывно появляющихся на
экране, пока их количество не достигнет какого-то предела. Вооруженный мощным
оружием воин располагается в нижней части экрана и способен передвигаться только
по горизонтали. Он может стрелять влево, вправо или вверх; с помощью клавиш
управления курсором можно передвигать его и задавать направление стрельбы (пробелом
осуществляется вертикальное направление стрельбы).
Чудовища мечутся по экрану, отталкиваясь друг от друга и от границ окна (предел
нижней границы области перемещений монстров чуть выше нижней границы окна).
Несмотря на свой ужасный вид, монстры вполне безобидны и не приносят никому
никакого вреда.
В игре присутствует два вида чудовищ, после попадания в монстра пули на месте
трагедии остается огненный сполох (рис. 5.2).
Рис. 5.2. Пример захватывающей игры "Меткий стрелок"
Данный пример иллюстрирует ваше умение создать, в принципе, несложную игру
без каких-либо особых ухищрений, опираясь на полученные знания. Игра работает
со вполне удовлетворительной скоростью даже на маломощных компьютерах (для достижения
этого используется 8-битный режим), хотя имеется значительный запас для оптимизации
ее работы.
Код построен на основе примера из предыдущей главы с проверкой столкновений.
Класс TBaseSprite является базовым для других классов спрайтов. Следуя логике
предыдущих примеров, каждый объект имеет собственную поверхность:
type
TBaseSprite = class
FSpriteSurface г IDirectDrawSurface?; // Поверхность
PosX, PosY : Integer; // Позиция
SpriteWidth : Integer; // Размеры
SpriteHeight. : Integer;
function GetRect : TRect; // Охватывающий прямоугольник
procedure Show; virtual; abstract; // Вывод private
rcRect : TRect; // Прямоугольник кадра
end;
Фон загружается из отдельного растра, все остальные образы берутся из компонентов класса Timage (рис. 5.3).
Рис. 5.З. Образы спрайтов располагаются в компонентах класса TImage
Классы воина, монстров и пуль являются дочерними базового класса:
type
TWarrior = class (TBaseSprite) // Класс воина
Direction : (dirLeft, dirRight); // Два направления
constructor Create (const Image : TImage); // Конструктор
function Restore (const Image : TImage) : HRESULT; // Восстановление
// Метод вывода определяется в каждом дочернем классе
procedure Show; override;
end;
Обратите внимание, что каждая пуля в моей игре является отдельным спрайтом:
type
TBullet = class (TBaseSprite)
Delay : DWORD; // Задержка, задает скорость полета пуль
constructor Create (const Image : Tlmage);
function Restore (const Image : Tlmage) : HRESULT;
procedure Show; override; // Вычисление нового положения и вывод
private
Xinc : Integer; // Наращивание по каждой оси
Yinc : Integer;
ThisTickCount : DWORD; // Локальный таймер для каждого спрайта
LastTickCount : DWORD;
end;
Для спрайтов монстров необходимо определять столкновения, их класс унаследовал очень многое от класса спрайтов из примера предыдущей главы:
type
TCollidelnfo = record
X, Y : Integer;
end;
TSprite = class (TBaseSprite)
Delay : DWORD;
AnimFrame : Integer; // Текущий кадр
FrameCount : Integer; // Всего кадров для этого вида монстров
Collide : BOOL;
Live : BOOL; // Флаг, сигнализирующий, не убит ли монстр
constructor Create (const Image : Tlmage; const SprDelay : DWORD;
const FrmCount : Integer);
function GetCenterX : Integer;
function GetCenterY : Integer;
function Restore : HRESULT;
procedure CalcVector;
procedure Hit(S : TSprite);
procedure Show; override; // Вычисление нового положения и вывод private
Xinc : Integer;
Yinc : Integer;
Collidelnfo : TCollidelnfo;
ThisTickCount : DWORD;
LastTickCount : DWORD;
end;
На экране может присутствовать до двухсот спрайтов одновременно (сто монстров и сто пуль), это большая цифра, и вы в состоянии увеличить эту цифру еще больше. Программа будет дольше инициализироваться, но воспроизведение получалось у меня с очень хорошей скоростью даже тогда, когда весь экран был забит спрайтами до отказа. А тестировал я эту игру на очень скромной машине:
const
DelayMonsters = 1000;// Через сколько миллисекунд появится новый монстр
MaxSprites = 100; // Ограничение количества спрайтов
var
Monsters : Array [0..MaxSprites - 1] of TSprite; // Массив чудовищ
Bullets : Array [0..MaxSprites - 1] of TBullet; // Массив пуль
Warrior : TWarrior; // Объект бойца
GlobalThisTickCount : DWORD; // Глобальный таймер
GlobalLastTickCount : DWORD;
NumMonsters : Integer =0; // Текущее количество монстров
NumBullets : Integer =0; // Текущее количество пуль
Создание отдельного спрайта (имеющего собственную поверхность) происходит очень долго, поэтому массивы спрайтов заполняются в начале работы приложения. Если же поступать так, как подсказывает логика, и создавать объекты только непосредственно перед их появлением на экране, картинка в такие моменты будет замирать на долю секунды. Создание двух сотен объектов будет долгим. Чтобы скрасить время ожидания, перед началом этого процесса я вывожу на первичную поверхность картинку фона, но можно было бы использовать и специальную заставку:
FDDSBackGround := DDLoadBitmap(FDD, bkBitmap, 0, 0); // Загружаем фон
if FDDSBackGround = nil then ErrorOut(hRet, 'DDLoadBitmap');
// Палитра предварительно загружена,
// устанавливается для всех поверхностей программы
hRet := FDDSBackGround.SetPalette(FDDPal);
if Failed (hRet) then ErrorOut(hRet, 'SetPalette');
// Прямоугольник, охватывающий весь экран
SetRect(bkRect, 0, 0, ScreenWidth, ScreenHeight);
// Сразу же после загрузки на экран выводится фон
FDDSPrimary.BltFast(0, 0, FDDSBackGround, ObkRect, DDBLTFAST_WAIT;
Randomize;
// Создание объекта воина
Warrior := TWarrior.Create (ImgWarrior);
// Заполняем массив монстров
for wrkl := Low (Monsters) to High (Monsters) do
if random > 0.5
then Monsters [wrkl] := TSprite.Create (ImgMosterl,
40+ random (40), 4)
else Monsters [wrkl] := TSprite.Create (ImgMoster2, 40 + random (20), 6);
// Заполняем массив пуль
for wrkl := Low (Bullets) to High (Bullets) do
Bullets [wrkl] := TBullet.Create (ImgBullet);
Чудовища в игре динамичны, каждый из них со временем меняется: шевелит глазами или раскрывает рот. Заготовки монстров содержат ленту кадров для его отображения. При создании монстра какой-то произвольный из них берется за начало цепочки кадров. Сделано это для того, чтобы не получилось, будто бы все чудовища синхронно меняются в своем обличий:
Constructor TSprite.Create (const Image : TImage; const SprDelay : DWORD;
const FrmCount : Integer);
var
DC : HOC;
ddsd : TDDSurfaceDesc2;
hRet : HResult;
begin
ZeroMemory (@ddsd, SizeOf (ddsd) ) ;
with ddsd do begin
dwSize := SizeOf (ddsd) ;
dwFlags := DDSD_CAPS or DDSD_HEIGHT or DDSD_WIDTH;
dwHeight := Image.Height;
dwWidth := Image.Width;
ddsCaps.dwCaps := DDSCAPS_OFFSCREENPLAIN;
end;
hRet := frmDD.FDD.CreateSurface(ddsd, FSpriteSurface, nil);
if Failed (hRet) then frrr.DD. ErrorOut (hRet, ' CreateSpriteSurface ' ) ;
if FSpriteSurface.GetDC(DC) = DD_OK then begin
BitBlt (DC, 0, 0, Image.Width, Image.Height, Image. Canvas .Handle,
0,0, SRCCOPY);
FSpriteSurface.ReleaseDC(DC) ;
end;
// Оба вида монстров нарисованы на зеленом фоне
DDSetColorKey (FSpriteSurface, RGB(0, 255, 0) ) ;
FSpriteSurface.SetPalette(frmDD.FDDPal);
SpriteHeight := Image.Height;
// Image содержит вcе кадры
SpriteWidth := Image.Width div FrmCount;
Collide := False;
PosX := random (640 - SpriteWidth);
PosY := random (426 - SpriteHeight);
CalcVector;
AnimFrame := random (FrmCount); // Текущий кадр - случайно
// Количество кадров для каждого вида монстров свое
FrameCount := FrmCount;
// Индивидуальная задержка смены кадров, передается случайное число
Delay := SprDelay;
// Прямоугольник кадра, фрагмент из ленты кадров
SetRect (rcRect, AnimFrame * SpriteWidth, 0,
AnimFrame * SpriteWidth + SpriteWidth, SpriteHeight);
Live := True;
LastTickCount := GetTickCount;
end;
Остальные методы классов спрайтов или схожи с предыдущими примерами, или тривиальны.
Подробно разбирать их, думаю, не стоит, обращу внимание только на некоторые
моменты.
Столкновение спрайтов определяется в программе просто выяснением наличия пересечения
охватывающих прямоугольников. Так получается быстрее, а на глаз зазор между
спрайтами в этом примере неразличим.
В рассматриваемом примере блиттинг спрайтов на задний буфер осуществляется с
флагом DDBLTFASTJDONOTWAIT, что редко для примеров этой книги.
Считаем, что задний буфер будет всегда доступным для вывода. При большом количестве
отображаемых образов ожидание доступности устройства является слишком большой
роскошью.
Каждый спрайт снабжен методом, связанным с восстановлением потерянной поверхности,
в котором по высоте спрайта определяем, с какой картинкой ассоциирован конкретный
объект:
function TSprite.Restore : HRESULT;
var
DC : HOC;
hRet : HRESULT;
Image : ТImage;
begin
hRet := FSpriteSurface .__Restore;
if Failed (hRet) then begin
Result := hRet;
Exit;
end;
// Пользуемся тем, что высота трех образов различна
if SpriteHeight = 15 then Image := frmDD.ImgMoster2 else
if SpriteHeight = 22 then Image := frmDD.ImgMosterl
else Image := frmDD.ImgDead;
// Копируем нужный образ на восстанавливаемую поверхность
if FSpriteSurface.GetDC(DC) = DD__OK then begin
BitBltfDC, 0, 0, Image.Width, Image.Height, Image.Canvas.Handle,
0, 0, SRCCOPY);
FSpriteSurface.ReleaseDC(DC);
end;
Result := FSpriteSurface.SetPalette(frmDD.FDDPal);
end;
Пули, долетевшие до края окна, должны удаляться из списка воспроизводимых образов:
procedure UpdateBul;
var
wrkl, wrkJ : Integer;
begin
for wrkl := 0 to NumBullets - 2 do
if (Bullets [wrkI].PosX >= 632) or (Bullets [wrkI].PosX <= 0) or
(Bullets [wrklJ.PosY <= 0) then begin
for wrkJ := wrkl to NumBullets - 1 do // Сдвигаем содержимое массива
with Bullets [wrkJ] do begin
PosX := Bullets [wrkJ + I].PosX;
PosY := Bullets [wrkJ + l].PosY;
Xinc := Bullets [wrkJ + 1].Xinc;
Yinc := Bullets [wrkJ + l].Yinc;
end;
NumBullets := NumBullets - 1;
end;
end;
Положение пули, попавшей в монстра, устанавливается за пределами экрана, чтобы
она не летела дальше, поражая других чудовиш.
Для погибшего монстра необходимо заменить размеры спрайта и ленту кадров. Все
эти действия следует производить максимально быстро. По возможности будем опираться
на конкретные числа; проверки успешности, равно как и академическое пересоздание
поверхности, опускаем:
procedure TfrmDD.DeadMonster (const Number : Integer);
var
DC : HDC;
ddsd : TDDSurfaceDesc2;
begin
ZeroMemory (@ddsd, SizeOf(ddsd));
with ddsd do begin
dwSize := SizeOf(ddsd);
dwFlags := DDSD__CAPS or DDSD_HEIGHT or DDSD_WIDTH;
dwHeight := ImgDead.Height;
dwWidth := ImgDead.Width;
ddsCaps.dwCaps := DDSCAPS_OFFSCREENPLAIN;
end;
with Monsters[Number] do begin
// Пересоздаем поверхность (без := nil)
FDD.CreateSurface(ddsd, FSpriteSurface, nil);
// Считаем, что ошибок не будет
FSpriteSurface.GetDC(DC);
// Конкретные числа размеров копируемого образа
BitBlt(DC, 0, 0, 100, 25, ImgDead.Canvas.Handle, О, О, SRCCOPY);
FSpriteSurface.ReleaseDC(DC);
// Ключ необходимо переустановить
DDSetColorKey (FSpriteSurface, RGB(0, 255, 0));
// Опять опираемся на конкретные числа
SpriteHeight := 25;
SpriteWidth := 25;
AnimFrame := 0;
FrameCount := 4;
Xinc := 0; // Погибший спрайт остается неподвижный
Yinc := 0;
Live := False;
end;
end;
Кадр перерисовывается непрерывно, но изменения в нем вносятся в соответствии с принятыми задержками:
function TfrmDD.UpdateFrame : HRESULT;
var
wrkl, si, s2 : Integer;
begin
GlobalThisTickCount := GetTickCount;
// Подошло время выпустить нового монстра
FDDSBack.BltFastfO, 0, FDDSBackGround, @bkRect, DDBLTFAST_WAIT);
if (GlobalThisTickCount - GlobalLastTickCount > DelayMonsters)
and (NumMonsters < High (Monsters) - 1) then begin Inc (NumMonsters);
GlobalLastTickCount := GlobalThisTickCount;
end;
// Обновить положения и воспроизвести монстров
for wrkl := 0 to NumMonsters - 1 do Monsters [wrkl].Show;
Warrior.Show; // Вывод воина поверх пролетающих, монстров
UpdateBul; // Удалить пули, вылетевшие за пределы экрана
// Обновить положения и отобразить пули
for wrkl := 0 to NumBullets - I do Bullets [wrkl].Show;
// Определить столкновение монстров и пуль
for s1 := 0 to NumMonsters - 1 do
for s2 := 0 to NumBullets - 1 do
if Monsters [s1].Live and SpritesCollidePixel (Monsters [s1],
Bullets [s2]) then begin
// Попавшая пуля перемещается за границы экрана
Bullets [s2].PosY := -10;
DeadMonster (s1); // Заменить образ монстра
end;
// Столкновения монстров, берутся в расчет только живые чудовища
for s1 := 0 to NumMonsters - 2 do
for s2 := si + 1 to NumMonsters - 1 do
if Monsters [s1].Live and Monsters [s2].Live and
SpritesCollidePixel (Monsters [si], Monsters[s2]) then begin
Monsters [si].Hit(Monsters [s2]);
Monsters [s2].Hit(Monsters [si]);
end;
Result := DDJDK;
end;
Больших усилий мне стоило при подготовке данного примера то, что не бросается
сразу же в глаза - уверенное восстановление поля игры. Для достижения этого
приходится восстанавливать поверхности всех спрайтов, и тех, что уже выводились,
и тех, что ни разу не появлялись на экране. Вследствие чего процесс восстановления
происходит тоже очень долго. Здесь я опять вывожу на первичную поверхность пустой
фон.
Итак, воспроизведение множества спрайтов выполняется с удовлетворительной скоростью.
Наиболее слабыми местами этой пробной игры являются чересчур долгая инициализация
и восстановление объектов. Также при каждом попадании в чудовище смена цепочки
кадров чересчур затягивается, и в такие моменты происходит ощутимое торможение
в работе игры.
Очередной пример является развитием предыдущего - это игра аналогичного жанра,
поменялись только фон и вид нашего бойца (рис. 5.4).
Рис. 5.4. В этом примере многое кардинально изменилось
Скорость работы игры повысилась существенно, инициализация и восстановление
происходят мгновенно, и нет ощутимой паузы в моментах замены картинки спрайтов.
Однажды я уже говорил, что, в случае применения множества образов, оптимальным
решением является использование одной поверхности. Если в предыдущем примере
каждый объект спрайта имеет собственную поверхность, то в этом проекте заведена
одна единственная поверхность, хранящая все используемые образы. Прием простой,
но, как видим, очень эффективный.
Образы спрайтов хранятся в единственном компоненте класса Timage (рис. 5.5).
Рис. 5.5. Все используемые образы теперь хранятся в единственном компоненте класса Timage
В принципе, это и не обязательно. Главное, повторюсь, то, что используется одна поверхность. Она может заполняться отдельными образами или единым, как в нашем примере, но при выводе спрайтов применяется не индивидуальная поверхность спрайта, а поверхность Foosimages, обслуживающая все спрайты. Вот как выглядит теперь код воспроизведения воина:
procedure TWarrior.Show;
begin
if Direction = dirRight
// rcRect устанавливается в координатах образа, хранящего все картинки
then SetRect (rcRect, 0, 70, SpriteWidth, 70 + SpriteHeight)
else SetRect (rcRect, SpriteWidth, 70, 2 * SpriteWidth, 70 +
SpriteHeight);
// Осуществляется блиттинг FDDSImages, а не поверхности спрайта
frmDD.FDDSBack.BltFast(PosX, PosY, frmDD.FDDSImages, @rcRect,
DDBLTFAST_DONOTWAIT or DDBLTFAST_SRCCOLORKEY);
end;
Также этот пример отличается от предыдущего тем, что пространство игры не ограничивается одним экраном, воин может продвигаться дальше правой границы, всего я использую два растровых фона, каждый размером 640x480 пикселов. Напоминаю, что некоторые видеокарты не позволяют создавать поверхности, превышающие в размерах первичную поверхность. Поэтому для хранения этих растров использую две поверхности - Foosone и FDDSTWO. Значение целочисленной переменной iftRect указывает ширину прямоугольника, вырезаемого из второй поверхности:
SetRect(rcRectOne, IftRect, 0, ScreenWidth, ScreenHeight);
// Первый фон
FDDSBack.BltFast(0, 0, FDDSOne, @rcRectOne, DDBLTFAST_WAIT);
if IftRect > 0 then begin // Присутствует ли часть второго фона
SetRect(rcRectTwo, 0, 0, IftRect, ScreenHeight);
FDDSBack.BltFast(ScreenWidth - IftRect, 0, FDDSTwo, SrcRectTwo,
DDBLTFAST_WAIT);
end;
Работа с клавиатурой
При изучении двух предыдущих примеров вам, наверняка, не понравилась скорость
перемещения нашего воина, и, возможно, вы гадали, почему я не установил величину
приращения побольше. Объяснение вы найдете в настоящем разделе.
Начинающие "игроделы" часто рассуждают так: если традиционный графический
вывод совершенно не годится для масштабной игры, и для обеспечения быстрой графики
надо искать другие пути, то управление, построенное на получении информации
обычными способами, вполне подходит. Имея опыт разработки программ бухучета,
вы не испытывали особых проблем со скоростью ввода данных, и, возможно, полагаете,
что, если ваша игра использует для ввода только клавиатуру и мышь, вам не стоит
напрягаться и изучать новые для вас методы организации ввода от традиционных
устройств. Если это так, то вас ждет большой сюрприз, вы сами убедитесь, как
сильно может улучшиться игра, если отказаться от привычных обработчиков событий,
связанных с устройствами ввода.
Обычно игры используют функции библиотеки Directlnput для организации управления,
с ними мы и бегло познакомимся в данном разделе. Эта библиотека является частью
DirectX и содержит набор функций для обеспечения пользовательского ввода с максимальной
скоростью. Высокая скорость работы даже с традиционными устройствами обеспечивается
тем, что Directlnput обходит часто применяемые механизмы операционной системы
и обращается к устройствам напрямую. Поэтому установленные в системе параметры,
такие как частота повтора символов для клавиатуры или чувствительность мыши,
не влияют на скорость ввода.
Directlnput использует модель СОМ. Посему, после изучения DirectDraw, нам будет
легко знакомиться с ним: мы встретим здесь знакомые понятия главного объекта
и интерфейсов.
Разбирая очередной пример (проект каталога Ех05), я попутно расскажу об основных
понятиях библиотеки Directlnput. По виду пример представляет собой обычное оконное
приложение, в компоненте класса тмето выводятся скан-коды нажимаемых клавиш,
нажатие кнопки Clear приводит к очистке его содержимого (рис. 5.6).
Рис. 5.6. Первый пример использования библиотеки Directlnput
В списке "uses помимо обычных для Delphi модулей мною вписан DirectlnputS.
Глобальная переменная Dlnput обеспечивает доступ к функциям Directinput:
var
Dinput : IDIRECTINPUT8 = nil; // Главный объект Directinput
// Интерфейс доступа к устройству ввода
DIKeyboard : IDIRECTINPUTDEVICE8 = nil;
Примечание
Впервые в наших примерах мы обращаемся к интерфейсам именно восьмой версии DirectX.
Обращу внимание на это событие, чтобы оно не прошло для вас незамеченным.
Следующая пользовательская функция предназначена для подготовки работы (обработку
ошибок оставлю только для первого действия):
function TfrmDX.Or.CreateDevlce : HRF.SULT;
var
hRet : HRESULT; // Результат действий
dipdw : TDIPROPDWORD; // Вспомогательная структура, задание параметров
begin
// Создание главного объекта Directlnput
hRet := DirectlnputSCreate (hlnstance, DIRECTINPUT_VERSION,
IID_IDirectInput8, DInput, nil);
if Failed (hRet) then begin
Result := hRet;
Exit
end;
// Создание объекта ввода информации от клавиатуры
hRet := DInput.CreateDevice (GUID_SysKeyboard, DIKeyboard, nil);
// Задаем формат данных, получаемых от устройства
hRet := DIKeyboard.SetDataFormat(c_dfDIKeyboard);
// Задаем уровень кооперации
hRet := DIKeyboard.SetCooperativeLevel(Handle, DISCL_NONEXCLUSIVE or
DISCL_BACKGROUND);
// Параметры для буферной схемы получения данных
ZeroMemory (Sdipdw, SizeOf (dipdw)); with dipdw do begin
diph.dwSize := SizeOf(TDIPROPDWORD);
diph.dwHeaderSize := SizeOf(TDIPROPHEADER);
diph.dwObj := 0;
diph.dwHow := DIPHJDEVICE;
dwData := SAMPLE_BUFFER_SIZE;
end;
// Задаем параметры буфера
hRet := DIKeyboard.SetProperty(DIPROP_BUFFERSIZE, dipdw.diph);
// Установили связь с устройством ввода
Result := DIKeyboard.Acquire;
end;
Для создания главного объекта из библиотеки Directlnput должна использоваться функция DirectlnputSCreate. Аргументы ее таковы:
Последний аргумент - указатель на показатель агрегирования (разновидность наследования;
термин, специфичный для СОМ) - обычно равен nil.
В случае удачи функция возвращает ноль. Такому значению соответствует константа
DI_OK, определенная в модуле Directinputs.
Метод CreateDevice главного объекта используется для создания нового объекта
устройства. У этого метода три аргумента:
В качестве идентификатора для клавиатуры передаем константу GUID_SysKeyboard.
Перед захватом устройства необходимо вызвать метод setoataFormat объекта, связанного
с устройством ввода. Здесь описывается формат, в котором вводимые данные возвращаются
устройством. Для стандартного устройства задаем стандартный формат.
Также обязательным действием является определение степени контроля над устройством,
задание уровня кооперации, другим словом. Для этого вызывается метод setcooperativeLevel,
первый аргумент которого - идентификатор окна приложения.
Прежде всего, необходимо указать, задается ли исключительный доступ к устройству
или нет (флаги DISCL_EXCLUSIVE и DISCL_NONEXCLUSIVE). В этом примере устанавливаю
неисключительный доступ. Для стандартного устройства разница между ними невелика,
библиотека Directlnput не может позволить никакому приложению захватить клавиатуру
монопольно. Просто эксклюзивный доступ может привести к помехам в работе с устройством
других приложений.
Помимо эксклюзивности обязательно необходимо задать активность режима (указать
один из флагов DISCL^BACKGROUND или DISCL_FOREGROOND). Первый флаг соответствует
режиму, когда приложение имеет доступ к устройству ввода всегда, даже когда
не имеет активности. Если вы запустите две копии этой программы, то обе они
будут реагировать на нажатие клавиш, и по нажатии клавиши <Esc> завершат
работу обе копии.
Следующие действия при инициализации связаны с выбранной схемой получения доступа
к данным. Можно использовать данные двух видов: непосредственные (immediate)
и буферизованные (buffered).
При работе с клавиатурой по первой схеме приложение периодически опрашивает
клавиши, получая данные о каждой из них: нажата она или нет. Вторая схема состоит
в том, что приложение считывает буфер, в котором хранятся данные о произошедших
со времени последнего опроса событиях связанных с устройством: какие клавиши
были нажаты, какие были отпущены.
Наш пример позволяет применить обе схемы, но первоначально настроен на вторую,
буферизованную, схему. Для нее надо задать размер буфера, и поэтому используется
вспомогательная структура, передающаяся аргументом метода setProperty. Размер
буфера мы задаем равным значению константы проекта:
const
SAMPLE_BUFFER_SIZE = 8;
Запомните, что для схемы непосредственного опроса эти действия не нужны.
Заканчивается код инициализации захватом устройства, получением доступа к нему,
вызовом метода Acquire объекта, связанного с устройством. Теперь мы можем получать
данные с устройства, если оно доступно и все подготовительные шаги были успешны.
Вызывается код инициализации при создании формы, в случае неудачи выводится
сообщение:
procedure TfrmDX.FormCreate(Sender: TObject);
var
hRet : HRESULT;
begin
hRet := OnCreateDevice; // Инициализация устройства
if Failed (hRet) then MessageDlg(DIErrorString(Error), mtError,
[mbAbort], 0);
end;
Ошибки возможны при неверном указании параметров, также они появляются при
занятости устройства. Если сейчас запущено приложение, имеющее исключительный
доступ к клавиатуре, то у нас могут возникнуть проблемы с захватом устройства.
В этой ситуации следует вызывать метод Acquire до тех пор, пока не будет установлена
связь.
В нашем примере, после установления связи с устройством происходит беспрерывный
вызов функции чтения буферизованных данных:
function TfrmDX.ReadBufferedData : HRESULT;
var
didod : Array [0..SAMPLE_BUFFER_SIZE - 1] of TDIDEVICEOBJECTDATA;
dwElements : DWORD;
i : DWORD;
hRet : HRESULT;
s : String;
begin
if DIKeyboard = nil then begin
Result := DI_OK;
Exit
end;
// Считываем данные из буфера
hRet := DIKeyboard.GetDeviceData (SizeOf(TDIDEVICEOBJECTDATA),
@didod, dwElements, 0);
if Failed (hRet) then begin // Восстанавливаем связь
hRet := DIKeyboard.Acquire;
while hRet = DIERR_INPUTLOST do
hRet := DIKeyboard.Acquire;
end;
// Буфер не пустой
if dwElements <> 0 then
for i := 0 to dwElements - 1 do begin
if didod[i].dwData and $80 <> 0 // Клавиша нажата
then s := 'D'
else s := 'U';
Memol.Lines.Add (Format ('Ox%02x%s', [didod[i].dwOfs, s] ) ) ;
if didod[i] .dwOfs = DIK__ESCAPE then Close;
end;
Result := DI_OK; // Нулевое значение, признак успешности
end;
Метод GetDeviceData объекта, ассоциированного с устройством, позволяет осуществить
собственно считывание данных из буфера. Смысл первого аргумента прозрачен: это
размер структуры, предназначенной для хранения. Второй аргумент - указатель
на массив элементов данной структуры. В качестве значения третьего аргумента
устанавливается количество считанных из буфера данных. Последний аргумент может
быть нулем или константой DIGDD_PEEK (во втором случае буфер не будет очищаться
после считывания данных).
Если функция возвращает ненулевое значение, то, скорее всего, потеряна связь
с устройством. Тогда необходимо снова установить эту связь, вызвав метод Acquire.
В библиотеке Directlnput отсутствуют какие-либо специальные методы восстановления,
а устанавливать связь можно сколько угодно раз, т. к. лишние вызовы этой функции
игнорируются.
Скан-коды клавиш содержатся в поле dwOfs структуры TDIDEVICEOBJECTDATA, значение
поля dwData позволяет узнать, какое событие произошло, нажата ли клавиша или
отпущена. Если это значение равно 128, то клавиша опущена. В нашем примере к
коду клавиши в этом случае приписывается буква "D", иначе - "U".
Вам не обязательно помнить наизусть коды всех клавиш, можете пользоваться символическими
константами. Для примера я показал, как выделить нажатие клавиши <Esc>.
После завершения работы освобождаем устройство и память, занятую объектами:
procedure TfrmDX.FormDestroy(Sender: TObject);
begin oif Assigned (DIKeyboard) then DIKeyboard.Unacquire; // Завершить диалог
if Assigned (DIKeyboard) then DIKeyboard := nil;
if Assigned (DInput) then DInput := nil;
end;
Поработайте с примером и обратите внимание, что можно отследить состояние максимум
четырех клавиш одновременно.
Непосредственная схема работы с клавиатурой используется чаще, чем буферизованная,
напоминаю, что состоит она в том, что в необходимые моменты происходит опрос
всех клавиш. Удалите из кода обработчика onidle вызов процедуры буферного опроса
клавиатуры и снимите комментарий со следующей далее строки. В коде инициализации
удалите все, связанное с заданием размера буфера. Запустите проект и нажмите
несколько клавиш (тоже максимум четыре) одновременно, в Memo выведутся коды
всех нажатых клавиш:
function TfrmDX.ReadlinmediateData : HRESULT;
var
hRet : HRESULT;
diks : Array [0..255] of BYTE; // Массив состояния клавиатуры
i : Integer;
sMulti : String;
begin
if DIKeyboard = nil then begin
Result := DI_OK;
Exit
end;
ZeroMemory(@diks, SizeOf(diks)); // Подготавливаем массив
hRet := DIKeyboard.GetDeviceState(SizeOf(diks), Sdiks); // Заполняем
if Failed (hRet) then begin // Требуется восстановить связь
hRet := DIKeyboard.Acquire;
while hRet = DIERR_INPUTLOST do
hRet := DIKeyboard.Acquire;
end;
sMulti := '';
for i := 0 to 255 do // Вывод кодов нажатых клавиш
if diks[i] and $80 <> 0
then sMulti := sMulti + ' ' + Format ('Ox%02x', [i]);
Memol.Lines.Add (sMulti);
Result := DI_OK;
end;
Непосредственная схема основана на использовании метода GetDeviceState, по вызову
которого массив заполняется данными о состоянии клавиш, точно также здесь возможны
значения 0 и 128.
В примере происходит опрос состояния всех клавиш, что не обязательно делать,
если вас интересуют только некоторые из них. Например, если требуется осуществить
выход по нажатии клавиши <Esc>, можно не пробегать в цикле по всем элементам
массива, а обратиться только к единственному:
if diks [DIK^ESCAPE] = 128 then Close;
С помощью непосредственной схемы доступа легко обрабатывать нажатие нескольких
клавиш одновременно, чем и пользуются часто в играх, как, например, в очередном
примере - проекте каталога Ех06. От предыдущего варианта нашей тестовой игры
пример отличается только тем, что здесь для управления используется библиотека
Directlnput. Скорость ввода стала чрезвычайно стремительной, воин теперь резво
передвигается по нажатии клавиш, буквально быстрее пули. Хотя шаг его нисколько
не увеличился. Одним махом происходит обработка нескольких клавиш, и можно,
например, стрелять вверх и вбок одновременно, или двигаться вправо и стрелять
вверх. Пули тоже вылетают на порядок быстрее, и ограничение в сотню расходуется
в считанные секунды.
Принципиально обработка ввода ничем не отличается от первого примера на основе
библиотеки Directlnput. Здесь используется непосредственная схема доступа, только
уровень доступа устанавливается в комбинацию
DISCL_FOKEGROUND or DISCL_EXCLUSIVE. Несмотря на негласное соглашение,
что неактивное приложение не будет считывать данные с клавиатуры, при запуске
двух копий программы обе они сильно замедлятся.
Работа с мышью
После знакомства с возможностями ввода с клавиатуры нам будет легко научиться
работать с мышью, принципы обработки ввода здесь точно такие же. Непосредственную
схему доступа изучим на конкретном примере - проекте каталога Ех07. Это еще
один вариант создания эффекта лупы, но более эффектный, чем предыдущие, поскольку
здесь добавлены сферические искажения пикселов (рис. 5.7).
Рис. 5.7. Пример иллюстрирует работу с мышью и создание сферических искажений
Представленные ниже константы и переменные связаны с параметрами искажений:
const
Diameter = 180; // Задает максимальный размер лупы
Scale =35; // Вспомогательный коэффициент
var
Radius : Integer = Diameter div 2; // Текущий размер лупы
SqrRad : Integer; // Вспомогательные величины
Sphere : Integer;
Вспомогательные переменные заполняются первоначально при создании формы:
SqrRad := Radius * Radius; // Квадрат радиуса
Sphere := (Radius * Radius) - (Scale * Scale); // Искажение
Во время перерисовки кадра накладываем фон, а искажения вносим сразу на поверхность заднего буфера:
function TfrmDD.UpdateFrame : HRESULT;
var
hRet : HRESULT;
begin
// Блиттинг фона
hRet := FDDSBack.BltFast (0, 0, FDDSBackGround, nil, DDBLTFAST_WAIT);
if Failed (hRet) then begin
Result := hRet;
Exit;
end;
hRet := Zoom; // Вызов функции создания эффекта
if Failed (hRet) then begin
Result := hRet;
Exit;
end;
Result := FlipPages; // Переключение буферов
end;
Эффект построен на простейшей математике - уравнениях круга и сферы:
function TfrmDD.Zoom : HRESULT;
var
descl : TDDSURFACEDESC2;
desc2 : TDDSURFACEDESC2;
X, Y : Integer;
XX,YY,YYXX : Integer;
mz : Single;
hRet : HRESULT;
begin
ZeroMemory (Sdescl, SizeOf(descl) );
descl.dwSize := SizeOf (descl);
ZeroMemory (@desc2, SizeOf(desc2));
desc2.dwSize := SizeOf (desc2);
hRet := FDDSBack.Lock (nil, descl, DDLOCK_WAIT, 0);
if Failed (hRet) then begin
Result := hRet;
Exit ;
end;
hRet := FDDSBackGround.Lock (nil, desc2, DDLOCK_WAIT, 0);
if Failed (hRet) then begin
Result := hRet;
Exit;
end;
for Y := -Radius to Radius do begin
YY := у * Y;
for X := -Radius to Radius do begin
XX := X * X; YYXX := YY + XX;
if YYXX < Sphere then begin // Точка внутри круга
mz := Scale / sqrt(SqrRad - YYXX); // Масштаб по третьей оси
// Пиксел на задней поверхности
PWord (Integer(descl.IpSurfасе) + (Y + mouseY) * descl.IPitch +
(mouseX + x) * 2)^ :=
// Источник на поверхности фона
PWord (Integer(desc2.IpSurfасе) +
trunc (mz * Y + mouseY) * desc2.IPitch +
trunc (mz * X + mouseX) * 2)^;
end;
end ;
end;
FDDSBackGround.Unlock (nil);
FDDSBack.Unlock (nil);
Result := DDJ3K;
end;
Для работы с устройством введены переменные уже знакомых нам типов:
DInput : IDIRECTINPUT8 = nil;
DIMouse : IDIRECTINPUTDEVICE8 = nil;
В коде подготовки устройства выполняются действия, аналогичные работе с клавиатурой, лишь поменялись константы:
function TfrmDD.OnCreateDevice : HRESULT;
var
hRet : HRESULT;
begin
hRet := DirectlnputBCreate (hlnstance, DIRECTINPUT_VERSION,
IID_IDirectInput8, DInput, nil) ;
// GUID соответствует устройству "мышь"
hRet := DInput.CreateDevice (GUID_SysMouse, DIMouse, nil);
hRet := DIMouse.SetDataFormat(c__dfDIMouse2); // Задаем формат данных
// Уровень кооперации задаем обычный
hRet := DIMouse.SetCooperativeLevel(Handle, DISCLJTONEXCLUSIVE or
DISCL__BACKGROUND) ;
Result := DIMouse.Acquire; // Захватываем устройство
end;
Опрос состояния мыши происходит непрерывно, перед каждым обновлением кадра:
procedure TfrmDD.ApplicationEventslIdle(Sender: TObject;
var Done: Boolean);
begin
if FActive then begin
ReadlmmediateData; // Ошибки игнорируем
if Failed (UpdateFrame) then RestoreAll;
end;
Done := False;
end;
При непосредственном доступе к мыши мы получаем данные о приращениях по осям, а не о координатах курсора на экране. При удерживаемой левой кнопки мыши радиус лупы увеличивается, в то время как правая кнопка позволяет его уменьшить:
function TfrmDD.ReadlmmediateData : HRESULT;
var
hRet : HRESULT;
dims2 : TDIMOUSESTATE2; // Структура хранения вводимых данных
begin
ZeroMemory(@dims2, SizeOf(dims2));
// Получаем сведения о состоянии мыши
hRet := DIMouse.GetDeviceState(SizeOf(TDIMOUSESTATE2), @dims2);
if Failed (hRet) then begin // Связь потеряна
hRet := DIMouse.Acquire; // Устанавливаем связь заново
while hRet = DTERR INPUTLOST do hRet := DIMouse. Acquire;
end;
// Массив rgbButtons хранит состояние дня каждой кнопки мыши
if dims2.rgbButtons[0] = 128 then begin // Нажата левая кнопка
Radius := Radius + 1; // Радиус увеличивается до некоторых пределов
if Radius > Diameter then Radius :=- Diameter;
SqrRad := Radius * Radius;
Sphere := (Radius * Radius) - (Scale * Scale);
end;
if dims2.rgbButtons[1] = 128 then begin // Нажата правая кнопка
Radius := Radius - 1; // Радиус уменьшается
if Radius < 0. then Radius := 0;
SqrRad := Radius * Radius;
Sphere := (Radius * Radius) - (Scale * Scale);
end;
// Полученное реальное приращение умножаем
mouseX := mouseX + 2 * dims2.1X;
if mouseX < Radius then mouseX := Radius else
if mouseX > ScreenWidth - Radius then mouseX := ScreenWidth - Radius;
mouseY := mouseY + 2 * dims2.1Y; if mouseY < Radius then mouseY := Radius
else
if mouseY > ScreenHeight - Radius then mouseY := ScreenHeight - Radius;
Result := DI_OK;
end;
const
imageBmp = '..\font.bmp1; // Растр шрифта
NumbLines =70; // Количество строк в файле
FileName = 'dictionary.txt'; // Файл словаря
Delay =50; // Пауза между появлениями очередной фразы
var
OutLiteral : String; // Очередная выводимая строка
StrList : Array [0..NumbLines - 1] of String; // Массив строк словаря
WinWidth, PosX : Integer; // Размеры экрана и позиция строки по X
WinHeight, PosY : Integer; // Размеры экрана и позиция строки по Y
tmpRect : TRECT; // Прямоугольник, связанный с текущей строкой
Избранные символы, с кодом большим 31, нарисованы в растре шрифта, высота каждого символа - 15 пикселов (рис. 5.8).
Рис. 5.8. В этой задаче не потребуются все 255 символов
Используется нормальный уровень кооперации. Для создания вспомогательной поверхности определяем текущие установки экрана:
procedure TfrmDD.FormCreate(Sender: TObject);
var
hRet : HRESULT;
ddsd : TDDSurfaceDesc2;
t : TextFile;
i, maxLength : Integer;
begin
FDDSWork := nil;
FDDSGround := nil;
FDDSFont := nil;
FDDSPrimary := nil;
FDD := nil;
hRet := DirectDrawCreateEx (nil, FDD, IDirectDrawV, nil);
if Failed(hRet) then ErrorOut(hRet, 'DirectDrawCreateEx');
// Уровень кооперации - нормальный
hRet := FDD.SetCooperativeLevel(Handle, DDSCL_NORMAL);
if Failed(hRet) then ErrorOut(hRet, 'SetCooperativeLevel');
ZeroMemory(@ddsd, SizeOf(ddsd));
with ddsd do begin
dwSize := SizeOf(ddsd);
dwFlags := DDSD_CAPS;
ddsCaps.dwCaps := DDSCAPS_PRIMARYSURFACE;
end;
hRet := FDD.CreateSurface(ddsd, FDDSPrimary, nil);
if Failed(hRet) then ErrorOut(hRet, 'Create Primary Surface');
// Загружаем растр со шрифтом
FDDSFont := DDLoadBitmap(FDD, imageBmp, 0, 0) ;
if FDDSFont = nil then ErrorOut(hRet, 'DDLoadBitmap');
// Узнаем текущие размеры экрана
WinWidth := GetSystemMetrics(SM_CXSCREEN);
WinHeight := GetSystemMetrics(SM_CYSCREEN);
// Поверхность для запоминания подложки выводимой фразы
ZeroMemory(@ddsd, SizeOf(ddsd));
with ddsd do begin
dwSize := SizeOf(ddsd);
dwFlags := DDSD_CAPS or DDSD_HEIGHT or DDSD_WIDTH;
ddsCaps.dwCaps := DDSCAPS_OFFSCREENPLAIN;
dwWidth := WinWidth;
dwHeight := WinHeight;
end;
hRet := FDD.CreateSurface(ddsd, FDDSGround, nil);
if Failed (hRet) then ErrorOut(hRet, 'CreateSurface');
// Считываем файл словаря, находим длину самой длинной фразы
AssignFile (t, FileName);
Reset (t);
maxLength := 0;
for i := 0 to NumbLines - 1 do begin
ReadLn (t, StrList [i]);
if length (StrList [i]) > maxLength then maxLength :=
length (StrList [i]);
end;
CloseFile (t);
// Поверхность для хранения растра фразы
ZeroMemory(@ddsd, SizeOf(ddsd));
with ddsd do begin
dwSize := SizeOf(ddsd);
dwFlags := DDSD__CAPS or DDSDJiEIGHT or DDSD_WIDTH;
ddsCaps.dwCaps := DDSCAPS_OFFSCREENPLAIN;
dwWidth := maxLength * 15; // Должны вместиться все фразы
dwHeight := 15;
end;
hRet := FDD.CreateSurface(ddsd, FDDSWork, nil);
if Failed (hRet) then ErrorOut(hRet, 'CreateSurface');
Randomize;
OutLiteral := StrList [random (NumbLines)]; // Генерируем первую фразу
GeneratePos; // Случайно генерируем позицию фразы на экоане
LastTickCount := GetTickCount;
end;
Для обеспечения максимальной скорости ошибки вообще не обрабатываются. Фразы побуквенно выводятся на вспомогательную поверхность, чтобы затем на первичной поверхности отобразить всю строку целиком:
procedure TfrmDD.ApplicationEventslIdle(Sender: TObject;
var Done: Boolean);
var
rcRect : TRECT;
i, X, Y : Integer;
// Вывод одного символа на вспомогательную поверхность
procedure OutChar (ch : Char; PosX : Integer);
var
chRect : TRECT;
wrkl : integer;
begin
// В растре шрифта представлены символы, начиная с пробела
wrkl := ord (ch) - 32;
chRect.Left := wrkl rriod 16 * 15; // Прямоугольник буквы в растре шрифта
chRect.Top := wrkl div 16 * 15;
chRect.Right := chRect.Left + 15;
chRect.Bottom := chRect.Top + 15;
// Вывод буквы на вспомогательную поверхность
FDDSWork.BltFast(PosX, 0, FDDSFont, @chRect, DDBLTFAST_DONOTWAIT);
end;
begin
ThisTickCount := GetTickCount;
Done := False;
// Подошло время выводить очередную строку словаря
if (ThisTickCount - LastTickCount) < Delay then
Exit;
// Ограничивающий прямоугольник
SetRect (rcRect, PosX, PosY, PosX + length (OutLiteral) * 15, PosY + 15);
// Запоминаем, что на экране находится в этом прямоугольнике
FDDSGround.BltFast(PosX, PosY, FDDSPrimary, SrcRect, DD3LTFAST_WAIT);
// Вывод строки
FDDSPrimary.BltFast(PosX, PosY, FDDSWork, @tmpRect, DDBLTFAST WAIT);
// Запоминаем текущее положение строки
X := PosX;
Y := PosY;
OutLiteral := StrList [random (NumbLines)]; // Генерация новой строки
GeneratePos; // Генерируем позицию на экране новой строки
// Подготавливаем поверхность новой строки
for i := 1 to length (OutLiteral) do
OutChar (OutLiteral [i], (i - 1) * 15);
SetRect (tmpRect, 0, 0, length (OutLiteral) * 15, 15);
// Стираем старую фразу на экране
FDDSPrimary.BltFast(X, Y, FDDSGround, SrcRect, DDBLTFAST_WAIT);
LastTickCount := GetTickCount;
end;
Итак, фраза на экране присутствует, пока выполняется код подготовки новой строки. Это очень малый промежуток времени. Конечно, некоторые строки будут потеряны, появившись и исчезнув быстрее, чем произошло обновление экрана. Для замедления процесса можно вставить вызов системной функции sleep с небольшой задержкой, но для небыстрых компьютеров это может привести к тому, что строки начнут неприятно мерцать по всему экрану.
Создание консоли
Консоль вы часто видели и использовали в профессиональных играх и, наверняка,
захотите создать и в своей игре. Пример данного раздела - проект каталога Ех09
- поможет вам в этом. Он является развитием нашей пробной игры: теперь по нажатии
клавиши <Таb> на экране появляется консоль, предназначенная для ввода
команд (рис. 5.9).
Рис. 5.9. Наша игра обзавелась консолью
Я предусмотрел реакцию только на одну команду, после ввода Exit приложение
завершает работу, все остальные вводимые строки просто вызывают эхо в консоли.
Моя консоль вмещает три строки, инициализируемые многозначительными фразами:
rcRectConsole : TRECT; // Вспомогательный прямоугольник
ConsoleHeight : Integer =0; // Текущий размер консоли
ConsoleLive : BOOL = False; // Флаг, связанный с присутствием
TextConsolel : String = '> Initialization....OK'; // Строки вывода
TextConsole2 : String = '> Loading .......OK';
TextConsole3 : String = '>_';
Для функционирования консоли я завел отдельную поверхность, закрашиваемую при
инициализации белым цветом:
ZeroMemory(@ddsd, SizeOf(ddsd));
with ddsd do begin
dwSize := SizeOf(ddsd);
dwFlags := DDSD__CAPS or DDSD_HEIGHT or DDSD_WIDTH;
ddsCaps.dwCaps := DDSCAPS_OFFSCREENPLAIN;
dwWidth := 640;
dwHeight := 100;
end;
hRet := FDD.CreateSurface(ddsd, FDDSConsole, nil);
if Failed (hRet) then ErrorOut(hRet, 'CreateSurface1);
hRet := FDDSConsole.SetPalette(FDDPal) ;
if Failed (hRet) then ErrorOut(hRet, 'SetPalette');
ZeroMemory(gddbltfx, SizeOf(ddbltfx));
ddbltfx.dwSize := SizeOf(ddbltfx);
ddbltfx.dwFillColor :=RGB (255, 255, 255);
FDDSConsole.Bit(nil, nil, nil, DDBLT COLORFILL or DDBLT WAIT, @ddbltfx);
SetRect (rcRectConsole, 0, 0, 640, 100);
Код воспроизведения монстров и пуль немного подправил, чтобы они не появлялись в области консоли. Сама же консоль воспроизводится в последнюю очередь, непосредственно перед переключением буферов:
if ConsoleLive then begin // Надо ли рисовать консоль
if (GlobalThisTickCount - GlobalLastTickCount > DelayConsole) then
begin // Плавное появление консоли
Inc (ConsoleHeight, 5);
if ConsoleHeight > 100 then ConsoleHeight := 100;
SetRect (rcRectConsole, 0, 0, 640, ConsoleHeight);
end;
// Собственно воспроизведение консоли
FDDSBack.BltFast(0, 0, FDDSConsole, @rcRectConsole, DDBLTFAST__WAIT);
end;
Текст в консоли выводится с помощью функций GDI:
procedure OutText (const X, Y : Integer; const TextCon : String);
var
DC : HOC;
begin
FDDSConsole.GetDC (DC) ;
SetBkColor(DC, RGB (255, 255, 255)); // Цвета фона и букв необходимо
SetTextColor (DC, 0); // задавать обязательно
TextOut (DC, X, Y, PChar(TextCon), length (TextCon));
FDDSConsole.ReleaseDC (DC);
end;
Немало хлопот принесла обработка нажатия клавиши <Backspace>: чтобы стереть старый текст, приходится воспроизводить ряд пробелов:
if diks [DIK_TAB] and $80 <> 0 then begin // Клавиша <Tab>
if not ConsoleLive then begin // Включение консоли
ConsoleHeight := 0; ConsoleLive := True;
end
else ConsoleLive := False; // Выключить консоль
Sleep(lOO); // Небольшая пауза
end;
if ConsoleLive then begin // Обработка клавиш для консоли
OutText (5, 10, TextConsolel); // Вывод трех строк в консоли
OutText (5, 30, TextConsole2); OutText (5, 50, TextConsole3);
if diks [DIK_RETURN] and $80 <> 0 then begin // Ввод команды
// Введена команда "Exit"; выход из программы
if (TextConsole3 = '>EXIT_') or (TextConsole3 = '> EXIT_') then Close;
// Введена другая команда, строки стираются и поднимаются наверх
TextConsolel := ' ';
OutText (5, 10, TextConsolel); // Затираем пробелами
TextConsolel := TextConsole2; // Строка сдвигается вверх
TextConsole2 := ' ' ;
OutText (5, 30, TextConsole2);
TextConsole2 := '> Command : ' + Copy (TextConsole3, 2,
length (TextConsoleS) - 2); // Реакция на все остальные команды -
// вывод эха
TextConsoleS := ' ';
OutText (5, 50, TextConsoleS);
TextConsoleS := '>_'; // Последняя строка превратилась в приглашение
Sleep(100);
end;
if diks [DIK_BACKSPACE] and $80 <> 0 then begin // Нажата клавиша
// <Backspace>
TextConsole3 := ' ';
OutText (5, 50, TextConsoleS); // Стираем последнюю строку
TextConsoleS := '>_';
OutText (5, 50, TextConsoleS);
end;
for i := DIK_Q to DIK_M do // Просматриваем буквенные клавиши
if diks [i] and $80 <> 0 then begin // Нажата какая-то клавиша с буквой
if length (TextConsoleS) < 20 then begin // Ограничение длины строки
// Перед символом подчеркивания вставляем букву нажатой клавиши
TextConsoleS := Copy (TextConsoleS, I, length (TextConsoleS) - 1) +
ScanToChar (i) +'_';
OutText (5, 50, TextConsoleS); // Вывод получившейся строки
Sleep(100);
end;
end;
end;
Поскольку обработка клавиатуры происходит необычайно быстро, с помощью процедуры sleep создаем искусственную паузу, чтобы не получить эффекта залипания клавиши. Эта пауза дает время пользователю отпустить нажатую клавишу.
Диалоговые окна
Многие приложения нуждаются в диалоговых окнах, поэтому уделим немного внимания
этому вопросу. Пример данного раздела (проект каталога Ех10) представляет собой
окончательную реализацию нашего хранителя экрана с плавающими рыбками. В развитие
предыдущего состояния добавлена поддержка пароля для входа в систему.
Установка и запрос пароля хранителя экрана являются системными действиями, но
вызов их не ограничивается одной строкой. Это означает, что диалоги установки
и ввода пароля не должны реализовываться программистом, пароль назначается для
всех хранителей экранов. Мы не можем самостоятельно запросить пароль, хранить
его в реестре, в определенном разделе, и самостоятельно организовывать ввод
пароля при нажатии клавиши или движении курсора мыши.
Следующая процедура предназначена для вызова системного диалога задания нового
пароля:
procedure TfrmDD.RunSetPassword;
type // Специальный тип функции, используется только в этой ситуации
TPCPAFunc = function(A : PChar; Parent : hWnd; В, С : Integer) :
Integer; stdcall;
var
Lib : THandle; // Ссылка на DLL
PCPAFunc : TPCPAFunc; // Загружаемая функция
begin
Lib := .LoadLibrary('MPR.DLL1); // Динамическая загрузка DLL
if Lib > 32 then begin // Проверка успешности загрузки
// Получаем адрес точки входа нужной функции
@PCPAFunc := GetProcAddress(Lib, 'PwdChangePasswordA');
// Задаем пароль хранителей экрана
if @PCPAFunc о nil then PCPAFunc('SCRSAVE', StrToInt(ParamStr(2)),
0, 0);
FreeLibrary(Lib); // Выгружаем библиотеку
end;
end;
В нашей программе эта процедура вызывается, если приложение запущено с параметром
/а, т. е. в ситуации, когда пользователь нажал кнопку Изменить на вкладке Заставка
(см. рис. 4.3).
При нажатии клавиши или движении курсора программа должна сама определить, установлен
ли пароль для хранителя экрана, и запустить системный диалог ввода пароля:
function TfrmDD.TestPassword : BOOL;
type
// Специальный тип, тоже используется только в этом, особом случае
TVSSPFunc = function(Parent : hWnd) : BOOL; stdcall;
var
Key : hKey;
D1,D2 : Integer;
Value : Integer;
Lib : THandle;
VSSPFunc : TVSSPFunc;
begin
Result := True;
// Загружаем информацию из реестра, используя функции API
if RegOpenKeyEx(hKey_Current_User, 'Control Panei\Desktop', 0,
Key_Read, Key) = Error_Success then begin
D2 := SizeOf(Value);
// Определяем, установлен ли пароль
if RegQueryValueEx(Key, 'ScreenSaveUsePassword', nil, @D1,
@Value,@D2) = Error_Success then begin if Value <> 0 then begin
// Динамически загружаем библиотеку ввода пароля
Lib := LoadLibraryf'PASSWORD.CPL');
if Lib > 32 then begin
// Получаем адрес точки входа
SVSSPFunc := GetProcAddress(Lib, 'VerifyScreenSavePwd');
// На время работы диалога включаем курсор
ShowCursor (True) ;
// Запускаем системный диалог
if @VSSPFunc <> nil then Result := VSSPFunc(Handle);
ShowCursor(False); // Это можно, в принципе, не делать
FreeLibrary(Lib); // Освобождаем память
end;
end;
end;
RegCloseKey(Key);
end;
end;
И теперь самое главное: диалоговое окно должно работать "поверх" первичной поверхности (рис. 5.10).
Рис. 5.10. В такой ситуации обычное окно должно всплыть перед нашим полноэкранным приложением
Чтобы пользователь увидел его, перед вызовом нашей пользовательской функции TestPassword нужно переключиться на воспроизведение в режиме GDI:
FDD.FlipTcGDISurface;
To есть в такой ситуации обязан вызываться метод главного объекта FlipToGDisurface,
а перерисовка экрана не должна осуществляться. К сожалению, мне встречались
хранители экрана, написанные профессионалами, авторы которых не позаботились
о корректной работе системного диалога: пароль приходится вводить "вслепую",
не видя окно ввода пароля, закрытое картинкой первичной поверхности. Памятуя
об этом, я многократно проверял работу своего хранителя на самых разных видеокартах,
и могу сказать, что не встретил ничего подобного.
Чтобы отключить клавиатуру, точнее, запретить работу комбинаций клавиш <Alt>+<Tab>
и <CtrI>+<Alt>+<Del>, на время работы приложения информируем
систему о том, что работает хранитель экрана, при запуске приложения выполняется
следующая строка кода:
SystemParametersInfo(SPI SCREENSAVERRUNNING, 1, nil, 0);
По окончании работы надо не забыть восстановить нормальную работу комбинаций
этих клавиш, для чего вызывается та же команда, но второй аргумент задается
нулевым значением.
Переключение на GDI-воспроизведение, надеюсь, сделает видимым диалоговое окно
у каждого читателя книги, но полноценным такое решение назвать нельзя. При перемещении
окна по экрану оно оставляет следы, расчищая первичную поверхность и обнажая
окно приложения (поэтому экран становится серым).
Такой способ общения с пользователем ущербен еще по той причине, что он не работает
в палитровом режиме. Из-за этого я не мог рекомендовать его для применения в
функции вывода сообщения об ошибке, используемой нами в предыдущих примерах.
Учтите, что наш хранитель экрана требует небольших доработок, для оконного режима
следует различать установленное разрешение рабочего стола. т. к. при 32-битной
глубине возможно искаженное масштабирование образов.
Использование отсечения в полноэкранном приложении
Имея дело с Delphi, вам наверняка жалко будет потерять все ее прелести и мощь
разработки диалоговых средств, расстаться с визуальными компонентами только
потому, что при перемещении окна могут остаться серые пятна.
Специально для этого случая я подготовил пример, иллюстрирующий, как можно эффектно
комбинировать обычный вывод средствами GDI и DirectDraw. Это проект каталога
Ex11, где посередине экрана отображается репродукция самого популярного шедевра
живописи, а по бокам разбросаны стандартные интерфейсные элементы (рис. 5.11).
Рис. 5.11. Пример можно назвать шедевром, но не благодаря искусному коду
В программе задается режим 800x600 пикселов, уровень доступа - исключительный.
При воспроизведении происходит блиттинг на первичную поверхность содержимого
вспомогательной поверхности, а для того, чтобы воспроизведение осуществлялось
только в пределах центральной области экрана, я использую отсечение, знакомое
нам по оконным приложениям. Но теперь оно ограничивает строго определенную область
экрана, а не связывается с окном приложения. Если ранее для связывания отсечения
с окном использовался метод SetHWnd, то в этом примере вызывается метод SetciipList.
В общем случае последний метод может применяться для создания и непрямоугольных
областей вывода, для описания их служат регионы:
var
rgn : TRgnData; // Вспомогательная переменная, описьшает набор регионов
wrk : TRECT; // Прямоугольник, описывающий наш единственный регион
...
SetRect {wrk, 230, 0, 620, 600); // Задаем область вывода на экране
with rgn.rdh do begin // Заполняем поля структуры
dwSize := SizeOf (RGNDATAHEADER); // Это обязательно, как всегда
iType := RDH_RECTANGLES; // Единственно возможное значение поля
nCount := 1; // Количество задействованных регионов
nRgnSize := Sizeof(TRECT); // Размер единицы информации
end;
PRECT(@rgn.Buffer)Л := wrk; // Заносим в буфер наш единственный регион
if FDD.CreateClipper(0, FDDClipper, nil) = DD_OK then begin
FDDClipper.SetClipList (@rgn, 0); // Задаем область отсечения
FDDSPrimary.SetClipper (FDDClipper) ;
end;
Замечу, что вспомогательная структура, представленная здесь, является системной
и не связана исключительно с DirectX. Заполняя поле buffer этой структуры, вы
можете получить холсты замысловатой формы.
Приведу еще один способ работы с методом SetciipList. Вот код, который способствует
отсечению, аналогичному отсечению предыдущего примера:
var
hrg : HRGN; // Регион
rgnDataBuffer: Array [0..1023] of BYTE; // Массив списка регионов
...
hrg := CreateRectRgn (230, 0, 620, 600); // Создание нужного региона
// Заполняем массив данными
GetRegionData(hrg, SizeOf(rgnDataBuffer), @rgnDataBuffer);
DeleteObject(hrg);
if FDD.CreateClipper(0, FDDClipper, nil) = DD_OK then begin
FDDClipper.SetClipList (@rgnDataBuffer, 0); // Задаем отсечение
FDDSPrimary.SetClipper(FDDClipper);
end;
Отсечение для полноэкранных приложений может использоваться и для того, чтобы полностью решить проблему с выводом образов вблизи границ. В проекте каталога Ех12 курсор заменен логотипом DirectX, динамически меняющим свой размер. Для такой ситуации задание положения курсора становится трудной задачей. Мы не можем пользоваться решениями предыдущих примеров с замененным курсором приложения. Присовокупление отсечения к экрану позволяет совершенно не задумываться о текущем размере образа курсора и его положении, при его воспроизведении вблизи границ образ отсекается совершенно корректно (рис. 5.12).
Рис. 5.12. Пример использования отсечения в полноэкранном приложении
Аналогично предыдущему примеру, объект отсечения строится на основе региона, но здесь регион представляет собой простой прямоугольник, связанный с размерами экрана:
SetRect (wrk, О, О, 800, 600);
with rgn.rdh do begin
dwSize := SizeOf (RGNDATAHEADER);
Type := RDH_RECTANGLES;
nCount := 1;
nRgnSize := Sizeof(TRECT);
end;
PRECT(@rgn.Buffer)Л := wrk;
if FDD.CreateClipper(0, FDDClipper, nil) = DD_OK then begin
FDDClipper.SetClipList (@rgn, 0);
FDDSBac k.SetClipper(FDDC1ippe r);
end;
При перерисовке кадра образ курсора растягивается на величину Scale:
function TfrmDD.UpdateFrame : HRESULT;
var
hRet : HRESULT;
wrkRect : TRECT;
begin
// Вывод фона
hRet := FDDSBack.Blt (nil, FDDSBackGround, nil, DDBLT WAIT, nil);
if Failed (hRet) then begin Result := hRet; Exit; end;
// Прямоугольник области образа курсора
SetRect (wrkRect, mouseX, mouseY, mouseX + Scale, mouseY + Scale);
// Масштабирование образа курсора, используется цветовой ключ
hRet := FDDSBack.Blt (SwrkRect, FDDSImage, nil, DDBLT_WAIT or
DDBLT_fCEYSRC, nil) ; if Failed (hRet) then begin
Result := hRet;
Exit;
end;
Result := FDDSPrimary.Flip(nil, DDFLIP_WAIT)
end;
Библиотека CDX
Наверняка многие из читателей этой книги захотят создать собственную вспомогательную
библиотеку, облегчающую программирование приложений на основе DirectDraw. Прежде
чем приступать к этому мероприятию, стоит познакомиться с уже готовыми решениями.
В данном разделе вас ждет краткий экскурс по функциям очень популярной библиотеки
CDX, реализованной по лицензии GNU. Библиотека написана на С, и я перевел на
Delphi лишь небольшую ее часть, а полностью библиотеку вы можете получить по
адресу http://www.cdx.sk/.
Начнем с проекта каталога Ех13, простейшего примера, выводящего на экране по
ходу своей работы разноцветные прямоугольники (рис. 5.13).
Рис. 5.13. Простейший пример на использование библиотеки СОХ
В списке uses добавилось подключение модуля coxscreenx, а перечень переменных, связанных с графикой, совсем короткий:
GameScreen : CDXScreen;
Вообще, код сильно упрощается, а инициализация графики сведена к одному оператору:
GameScreen := CDXScreen.CreateCDXScreenCustomBPP(Handle, ScreenWidth,
ScreenHeight, ScreenBitDepth);
Код перерисовки окна также состоит из одного действия:
GameScreen.GetAppFrontBuffer.Rect(random(ScreenWidth),
random(ScreenHeight),
random(ScreenWidth),
random(ScreenHeight),
random(254));
To есть, рисуем очередной прямоугольник прямо на переднем буфере.
Работа приложения завершается после нажатия любой клавиши. По окончании работы
приложения удаляется наш единственный объект:
if Assigned (GameScreen) then GameScreen.Destroy;
Подход, предлагаемый CDX, вам сильно напомнит то, что мы встречали в реализации
модуля DDUtil для восьмой версии DirectX, но исторически первой появилась CDX.
Библиотека специально предназначена для разработки игр и охватывает не только
DirectDraw, но и все остальные модули DirectX.
Следующий пример, проект каталога Ех14, иллюстрирует некоторые моменты работы
с фоном. Экран разбит на четыре части, в каждой из которых выводится своя фоновая
картинка (рис. 5.14).
Рис. 5.14. Пример сложного заполнения фона
По нажатии клавиш управления курсором ландшафт в каждом секторе циклически сдвигается. В программе появились дополнительные переменные, связанные с фоном и обработкой клавиатуры:
Gamelnput : CDXInput; // Объект, связанный с вводом
GameScreen : CDXScreen; // Главный объект вывода
Landscape : CDXTile; // Загружаемая картинка
Mapl : CDXMap; // Секторы, отдельные окна экрана
Мар2 : CDXMap;
МарЗ : CDXMap;
Мар4 : CDXMap;
MapScrollSpeed : Integer = 4; // Скорость передвижения фона
К Зафужаемая картинка объединяет четыре шаблона заполнения окна, каждый имеет размеры 64x64 пикселов:
procedure ТfrmDD.FormCreate(Sender: TObject);
begin
Gamelnput := CDXInput.CreateCDXInput; // Инициализация ввода
Gamelnput.Create(HInstance, Handle);
GameScreen := CDXScreen.CreateCDXScreenCustomBPP(Handle, ScreenWidth, ScreenHeight,
ScreenBitDepth);
GameScreen.LoadPalette('Anim.bmp1); // Палитровый режим
// Загрузка картинки с 4-мя шаблонами заполнения экрана, 64x64 пиксела
Landscape := CDXTile.CDXTileCustom(GameScreen,'Anim.bmp',64, 64, 4);
// Создаем четыре схемы заполнения фона
Mapl := CDXMap.CDXMap(Landscape, GameScreen);
Mapl.CreateMap(64, 64, 1);
Map2 := CDXMap.CDXMap(Landscape, GameScreen);
Map2.CreateMap(64, 64, 2) ;
МарЗ := CDXMap.CDXMap(Landscape, GameScreen);
Map3.CreateMap(64, 64, 3);
Map4 := CDXMap. CDXMap (Landscape, GameScreen);
Map4.CreateMap(64, 64, 4);
end;
Поскольку нумерация шаблонов основана на нуле, в картинки включают дополнительный,
неиспользуемый фон, идущий первым.
Код обработки клавиш легко читается, смысл всех действий вам должен быть понятен:
function KeyDown (Key : Byte): BOOL; // Вспомогательная функция
begin
Result := Gamelnput.Keys[Key] = 128; // Нажата ли клавиша
end;
procedure UpdateKeys; // Процедура обработки клавиатуры
begin
if KeyDown(DIK_RIGHT) then begin // Стрелка вправо
Mapl.WrapScrollRight(MapScrollSpeed); // Сдвиг вправо содержимого
Map2.WrapScrollRight(MapScrollSpeed); // Всех четырех окон
МарЗ.WrapScrollRight(MapScrollSpeed);
Мар4.WrapScrollRight(MapScrollSpeed);
end;
if KeyDown(DIK_LEFT) then begin // Стрелка влево
Mapl.WrapScrollLeft(MapScrollSpeed);
Map2.WrapScrollLeft(MapScrollSpeed);
МарЗ.WrapScrollLeft(MapScrollSpeed);
Map4.WrapScrollLeft(MapScrollSpeed);
end;
if KeyDown(DIK_UP) then begin // Стрелка вверх
Mapl.WrapScrollUp(MapScrollSpeed);
Map2.WrapScrollUp(MapScrollSpeed);
МарЗ.WrapScrollUp(MapScrollSpeed);
Map4.WrapScrollUp(MapScrollSpeed);
end;
if KeyDown(DIK_DOWN) then begin // Стрелка вниз
Mapl.WrapScrollDown(MapScrollSpeed);
Map2.WrapScrollDown(MapScrollSpeed);
МарЗ.WrapScrollDown(MapScrollSpeed);
Map4.WrapScrollDown(MapScrollSpeed); end; if KeyDown(DIK_ESCAPE) then begin
// Выход
GameScreen.FadeTo(255, 255, 255, 0); // Эффект угасания
GameScreen.FadeOut(4) ;
f rmDD.Close;
end;
end;
Обрабатывается нажатие нескольких клавиш одновременно, образы можно передвигать
по диагонали.
Вывод осуществляется в задний буфер, каждая карта отсекается по своему сектору:
function TfrmDD.UpdateFrame : HRESULT;
var
Windowl : TRECT; // Секторы окна
Window2 : TRECT;
Windows : TRECT;
Window4 : TRECT;
begin
SetRect (Windowl, 0, 0, 320, 240) ; // Четыре равные части экрана
SetRect (Window2, 320, 0, 640, 240);
SetRect (Window3, 0, 240, 640, 480); SetRect (Window4, 320, 240, 640, 480);
GameInput.Update; // Обновить данные о клавиатуре
OpdateKeys; // Обслужить нажатые клавиши
// Вывод в задний кадр четырех карт, отсекаемых по секторам
Map1.DrawClipped(GameScreen.GetAppBackBuffer, Windowl);
Map2.DrawClipped(GameScreen.GetAppBackBuffer, Window2);
МарЗ.DrawClipped(GameScreen.GetAppBackBuffer, Window3);
Map4.DrawClipped(GameScreen.GetAppBackBuffer, Window4);
Result := GameScreen.Flip; // Переключение страниц
end;
Для восстановления поверхностей используется метод Restore.
В продолжение нашего знакомства с библиотекой CDX разберем проект каталога Ех15,
помогающий постигнуть создание анимации. В качестве фона здесь используются
те же круги, что и в предыдущем примере, которые сменяют друг друга на экране.
Существует одна переменная, связанная с фоном, в нее загружаются различные фрагменты
растра:
GameMap := CDXMap.CDXMap(Landscape, GameScreen); // Создание лоскута
GameMap.CreateMap(MapSizeX, MapSizeY, 1) ;
GameMap.MoveTo(0, 0) ; Tile := 1;
for i := 0 to 63 do // Цикл заполнения карты
for j := 0 to 62 do begin // разными фрагментами
GameMap.SetTile (i, j, Tile);
Tile := Tile + 1;
if Tile > 4 then Tile := 1;
end;
Через некоторый промежуток времени экран заполняется новым фоном:
var
Delay : Integer =0; // Счетчик кадров
function TfrmDD.UpdateFrame : HRESULT;
var
wrk : TRECT; // Прямоугольник экрана
i, j, Tile : Integer;
begin
Game Input.Update;
UpdateKeys;
SetRect (wrk, 0, 0, ScreenWidth, ScreenHeight);
// Вывести текущее состояние фона
GameMap.DrawClipped (GameScreen.GetAppBackBuffer, wrk);
Inc (Delay);
if Delay > 40 then begin // Прошло 40 кадров
for i := 0 to 62 do
for j := 0 to 62 do begin
Tile := GaraeMap.GetTile(i, j); // Получить номер фрагмента
Inc (Tile); // Циклический сдвинуть в цепочке фрагментов
if Tile > 4 then Tile := 1;
GameMap.SetTile(i, j, Tile); // Задать новый фрагмент
end;
Delay := 0;
end;
Result := GameScreen.Flip;
end;
Код обработки клавиатуры в примере заметно короче по сравнению с предыдущим:
procedure UpdateKeys;
begin
if KeyDown(DIK_RIGHT) then GameMap.WrapScrollRight(MapScrollSpeed);
if KeyDown(DIK_LEFT) then GameMap.WrapScrollLeft(MapScrollSpeed);
if KeyDown(DIKJJP) then GameMap.WrapScrollUp(MapScrollSpeed);
if KeyDown(DIK_DOWN) then GameMap.WrapScrollDown(MapScrollSpeed);
if KeyDown(DIK_ESCAPE) then frmDD.Close;
end;
На рис. 5.15 запечатлен момент работы нашего очередного примера (проекта каталога Ех16), в котором на экране выводятся координаты пользовательского курсора.
Рис. 5.15. Пример вывода текста и обработки событий мыши
Для изображения курсора предназначена отдельная поверхность, для которой задается ключ:
GameCursor := CDXSurfасе.Create;
GameCursor.CreateCDXSurfaceFromFile(GameScreen,'Cur.bmp');
GameCursor.ColorKey(0);
Для заднего буфера задается конкретный шрифт:
GameScreen.GetAppBackBuffer.ChangeFont('Times', 16, 20, FW_BOLD);
Аналогично процедуре обработки клавиатуры, требуется процедура, связанная с
событиями мыши. Обратите внимание, как организована прокрутка изображения:
procedure UpDateMouse;
var
TempX, TempY : Integer;
begin
TempX := GameInput.Mouse.X; // Смещение по осям
TempY := Gamelnput.Mouse.Y;
CurX := CurX + 3 * TempX; // Текущие координаты курсора
CurY := CurY + 3 * TempY;
// Анализ положения курсора вблизи границ экрана
if CurX < 0 then CurX := 0 else
if CurX > ScreenWidth - MapSizeX then CurX := ScreenWidth - MapSizeX;
if CurY < 0 then CurY := 0 else
if CurY > ScreenHeight - MapSizeY then CurY := ScreenHeight - MapSizeY;
if CurX = 0 then begin
if TempX < 0 then GameMap.WrapScrollLeft(-TempX);
end else
if CurX = ScreenWidth - MapSizeX then
if TempX > 0 then GameMap.WrapScrollRight(TempX);
if CurY = 0 then begin
if TempY < 0 then GameMap.WrapScrollUp(-TempY);
end else
if CurY = ScreenHeight - MapSizeY then
if TempY > 0 then GameMap.WrapScrollDown(TempY);
end;
Вывод текста на экран осуществляется с помощью метода TextxY заднего буфера:
function TfrmDD.UpdateFrame : HRESULT;
var
wrk : TRECT;
begin
Gamelnput.Update;
UpdateKeys;
UpdateMouse;
SetRect (wrk, 0, 0, ScreenWidth, ScreenHeight);
GameMap.DrawClipped (GameScreen.GetAppBackBuffer, wrk);
// Вывод курсора
GameCursor.DrawFast(CurX, CurY, GameScreen.GetAppBackBuffer);
// Вьшод текста
GameScreen.GetAppBackBuffer.TextXYUO, 10, 255,
'CDX Example for Delphi');
GameScreen.GetAppBackBuffer.TextXY(10, 30, 255, PChar('X= ' +
IntToStr(CurX))); GameScreen.GetAppBackBuffer.TextXY(10, 50, 255, PChar('Y=
' +
IntToStr(CurY)));
Result := GameScreen.Flip;
end;
Последний пример на эту тему (проект катшюга Ех17) поможет нам разобраться в организации спрайтовой анимации. Здесь вид курсора меняется со временем так, что получается изображение страшного животного, раскрывающего зубастую пасть (рис. 5.16).
Рис. 5.16. При работе примера чудовище раскрывает и закрывает свою пасть
Фазу можно менять, используя метод setTile, как в одном из предыдущих примеров, или же напрямую задавая прямоугольник источника:
Inc (Delay);
if Delay = 10 then begin // Прошло 10 кадров
// Меняем прямоугольник в источнике
SetRect (GameCursor.SrcRect, 39 * wrkl, 0, 39 * (wrkl + 1), 36);
wrkl := (wrkl + 1) mod 3;
Delay := 0;
end;
В данном разделе мы рассмотрели лишь основные функции библиотеки CDX, все остальные
остаются вам для самостоятельного изучения.
Я не думаю, что здесь вы встретите особые проблемы, т. к. после проведенного
нами детального изучения механизмов DirectDraw знакомство с подобными библиотеками
(и исправление ошибок в исходном коде) превращается в приятное времяпрепровождение.
Что вы узнали в этой главе
Главное, что мы смогли выяснить в данной главе, можно сформулировать следующей
торжественной фразой: узнали все необходимое для программирования собственной
эффектной игры.
Мы научились работать с устройствами ввода настолько быстро, что приложение
мгновенно реагирует на изменение состояний устройств.
Спрайтовая анимация изучена нами до уровня, нужного для разработки игр.
Примеры несложных игр убедительно демонстрируют достигнутые нами высоты мастерства.
Мы познакомились с готовой библиотекой, использующейся профессиональными разработчиками
игр.