Сергей Андрианов
12.02.2002
В предыдущих статьях мы учились устанавливать графический режим с 256 цветами и нужной палитрой, выводить спрайт и текст, а также измерять время и обеспечивать независимость скорости перемещения спрайта по экрану от производительности ПК. Однако в большинстве случаев одного спрайта на дисплее явно недостаточно, и поэтому мы переделаем нашу программу так, чтобы она могла выводить одновременно уже несколько спрайтов.
Существуют различные методы создания программ. При разработке сверху вниз сначала намечают структуру программы, определяют набор процедур, распределение их по модулям и только потом переходят к программированию. Так, как правило, делают все крупные проекты, причем можно заранее распределить весь объем работ между несколькими программистами. Однако при таком подходе требуется написать немало текста, прежде чем может быть откомпилирована первая исполняемая программа, ведь для каждой процедуры к данному моменту уже должна быть написана как минимум заглушка:
Procedure ProcName;
Begin
End;
Разработку программы снизу вверх чаще всего выполняет один программист. Вначале он пишет мало-мальски работоспособную программу, к которой затем постепенно добавляются все требуемые возможности. При таком методе разработки необходима своевременная и продуманная декомпозиция, т.е. сначала нужно написать основную программу, затем выделить из нее отдельные модули и уже потом, по мере наращивания функциональности модулей, производят, если понадобится, их разукрупнение. Именно такой подход и был выбран для написания нашей программы.
До сих пор все операции, выполняемые над спрайтами, находились в теле основной программы. Пора наконец вынести их в отдельный модуль. В предыдущих статьях в основном были приведены не полные листинги, а только их изменения (подробные тексты есть по адресу www.pcworld.ru). Если же изменения затрагивают почти весь текст модуля, то, конечно, следует привести его целиком. Поэтому целесообразно совместить переделку программы на вывод нескольких спрайтов с ее декомпозицией, а также привести полные листинги довольно сильно сократившейся основной программы (листинг 1) и нового модуля, названного sprites и выполняющего все действия над спрайтами (листинг 2).
При одновременном выводе нескольких спрайтов важно, чтобы каждый из них не только сохранял под собой фон, но и не портил другие при их перекрытии. Для этого все спрайты сортируются по «удаленности», означающей, что различные объекты на экране должны находиться на разных расстояниях от зрителя, а если они расположены в одной плоскости, то более близким будет считаться тот, который перекрывает остальные.
При отображении спрайтов на экран следует воспользоваться алгоритмом художника, т. е. рисовать их, начиная с самого дальнего и кончая самым ближним. Для каждого из спрайтов нужно обеспечить сохранение фона, что может выполняться лишь после вывода предыдущего спрайта. Таким образом, алгоритм вывода спрайтов должен иметь следующий вид.
Цикл 1 по спрайтам от дальнего до ближнего:
cохранить фон;
вывести спрайт;
конец цикла.
Последующее восстановление фона должно происходить в обратном порядке.
Цикл 2 по спрайтам от ближнего до дальнего:
восстановить фон;
конец цикла.
Зритель должен видеть экран со спрайтами, а не пустой фон, поэтому следует минимизировать время от начала цикла 2 до конца 1-го. Другими словами, все дополнительные операции (вычисление координат спрайтов, реакция на органы управления и т. п.), а также обеспечение синхронизации следует располагать в теле основного цикла между концом цикла 1 и началом цикла 2.
Вместо единственного спрайта введен массив, и кроме того, изменен способ обращения ко всем процедурам работы со спрайтом. Теперь необходимо указать процедуре, с каким именно спрайтом мы хотим работать.
Перед тем как запускать новую программу, требуется нарисовать с помощью графического редактора изображение еще одного спрайта и сохранить его на диске под именем sprt02.bmp.
Итак, на экране присутствуют два спрайта. А можно больше? Да!
Чтобы для работы этой демонстрационной программы не пришлось рисовать несколько сотен спрайтов, применена маленькая хитрость. Вы, наверное, обратили внимание, что в листинге при определении имен файлов спрайтов переменная NumSprites «закомментирована», а вместо нее указано число 2. Сами же спрайты создаются в цикле:
for i := 1 to NumSprites do
CreateSprite(NameSprt[(i mod 2)+1],random(320-Xsize),
random(200-Ysize),1,1,Sprt[i]);
где вместо i используется (i mod 2)+1.
Иначе говоря, все четные спрайты будут прочитаны из одного файла, а нечетные — из другого. Далее можно произвольно изменять количество спрайтов, варьируя переменную NumSprites и не заботясь о наличии достаточного числа файлов и об описании их имен в программе. Когда будете создавать игру, не забудьте назначить каждому спрайту свое уникальное имя.
Теперь можно увеличить количество спрайтов и посмотреть, что же произойдет. Здесь надо быть внимательным, ведь программа написана для среды DOS и может использовать примерно 450—500 Кбайт памяти, причем независимо от того, сколько ее установлено в ПК. Сейчас спрайт занимает два поля по 400 байт, т. е. около 0,78 Кбайт. В зависимости от производительности компьютера советую установить от 100 до 500 спрайтов.
Конечно, вряд ли одновременно понадобится столь много спрайтов на экране, но все равно результат наводит на грустные размышления: уж слишком снизилась частота кадров и появилось мерцание изображения. Следующий рассказ будет посвящен борьбе с этими неприятными явлениями.
Листинг 1
program Sprite;
uses dos, {для работы с прерыванием VideoBIOS}
crt, {для работы с клавиатурой}
pal, {для работы с палитрой}
text256, {для вывода текста}
timer18, {для измерения времени и синхронизации}
sprites; {для работы со спрайтами}
const
TextColor : byte = 0; {текущий цвет текста}
NumSprites = 2; {количество спрайтов}
NameSprt : array[1..2{NumSprites}]of string =
('sprt01.bmp','sprt02.bmp');
{имена файлов спрайтов}
var
Sprt : array[1..NumSprites]of SpriteType; {спрайты}
r : registers; {для вызова прерывания BIOS}
i : integer; {счетчик спрайтов}
s : string; {для вывода сообщений на экран}
FPS : single; {темп вывода кадров в секунду}
function sign(a:single):integer;
begin
if a = 0 then
sign := 0
else
if a > 0 then
sign := 1
else
sign := -1;
end;
begin
GetPal(p[0],0,256);
FadeOut(p);
randomize;
for i := 1 to NumSprites do
CreateSprite(NameSprt[(i mod 2)+1],random(320
-Xsize),random(200-Ysize),1,1,Sprt[i]);
r.ax := $13; { устанавливаем режим }
intr($10,r); { 320х200х256 цветов }
BlackPal;
PutBackGround; {рисуем фон}
FadeIn(p);
for i := 1 to NumSprites do begin
GetBuffer(Sprt[i]);{сохраняем фон под спрайтом}
PutSprite(Sprt[i]);{и рисуем на его месте спрайт}
end;
repeat {теперь спрайт будет двигаться по экрану}
{до тех пор, пока мы не нажмем на клавишу}
for i := NumSprites downto 1 do
PutBuffer(Sprt[i]); {восстанавливаем фон}
for i := 1 to NumSprites do begin
CalcSpritePosition(Sprt[i]);
GetBuffer(Sprt[i]); {сохраняем фон}
PutSprite(Sprt[i]); {выводим спрайт}
end;
inc(TextColor);
SetTextParm(TextColor div 16,
(TextColor + 48) div 16,1);
PutText(56,16,'Демонстрационная');
SetTextParm(TextColor and $F,0,0);
PutText(192,16,'программа');
SetTextParm(15,0,1);
FPS := GetFPS;
str(FPS:0:1,s);
PutText(120,184,' '+s+' fps ');
if FPS > 1 then {изменяем приращение}
for i := 1 to NumSprites do begin
Sprt[i].dx := sign(Sprt[i].dx)*round(70/FPS);
Sprt[i].dy := sign(Sprt[i].dy)*round(70/FPS);
end;
WaitVerticalRetrace;
{ожидаем обратный ход луча кадровой развертки}
until keypressed;
readkey; {чистим буфер клавиатуры}
FadeOut(p);
r.ax := $3;
intr($10,r); {возвращаемся в текстовый режим}
for i := NumSprites downto 1 do
DestroySprite(Sprt[i]);
end.
Листинг 2
unit sprites;
interface
uses bmpread;
const
Xsize = 20; {размеры спрайта, точек}
Ysize = 20;
TransparentColor = $FF; {"прозрачный" цвет}
type
SpriteArrayType =
array[0..Ysize-1,0..Xsize-1]of byte;
{массив, равный по размеру спрайту}
SpriteType = record
x,y : word; {текущие координаты спрайта}
dx,dy : integer; {приращения координат спрайта}
Img : ^SpriteArrayType;
{для массива с изображением спрайта}
Back : ^SpriteArrayType;
{для массива, хранящего фон под спрайтом}
end;
ScreenType = array[0..199,0..319]of byte;
{для экрана}
var
Scr : ^ScreenType; {экран}
p : array[0..767]of byte;
procedure GetBuffer(Sprite:SpriteType);
{сохранение фона под спрайтом в буфере}
procedure PutBuffer(Sprite:SpriteType);
{восстановление фона}
procedure PutSprite(Sprite:SpriteType);
{вывод спрайта на экран}
procedure CreateSprite(s:string; x,y,dx,dy:integer;
var Sprite:SpriteType); {"создание" спрайта}
procedure DestroySprite(Sprite:SpriteType);
{"уничтожение" спрайта}
procedure CalcSpritePosition(var Sprite:SpriteType);
{вычисление новых координат спрайта}
procedure PutBackground; {создание фона на экране}
implementation
procedure GetBuffer(Sprite:SpriteType);
{сохранение фона под спрайтом в буфере}
var
i,j : word; {переменные цикла}
begin
for j := 0 to Ysize-1 do
for i := 0 to Xsize-1 do
with Sprite do
Back^[j,i] := Scr^[j+y,i+x];
end;
procedure PutBuffer(Sprite:SpriteType);
{восстановление фона}
var
i,j : word; {переменные цикла}
begin
for j := 0 to Ysize-1 do
for i := 0 to Xsize-1 do
with Sprite do
Scr^[j+y,i+x] := Back^[j,i];
end;
procedure PutSprite(Sprite:SpriteType);
{вывод спрайта на экран}
var
i,j : word; {переменные цикла}
begin
for j := 0 to Ysize-1 do
for i := 0 to Xsize-1 do
with Sprite do
if Img^[j,i] <> TransparentColor then
{ставим только точки,}
{цвет которых отличается от "прозрачного"}
Scr^[j+y,i+x] := Img^[j,i];
end;
procedure CreateSprite(s:string; x,y,dx,dy:integer;
var Sprite:SpriteType); {"создание" спрайта}
var
f : file; {файл с изображением спрайта}
begin
getmem(Sprite.Img,sizeof(SpriteArrayType));
{выделяем память для спрайта}
getmem(Sprite.Back,sizeof(SpriteArrayType));
{выделяем память для буфера}
Readbmp(@(Sprite.Img^),Xsize,Ysize,@p,s);
Sprite.x := x;
Sprite.y := y; { задаем начальные значения }
Sprite.dx := dx; { координат и приращений }
Sprite.dy := dy;
end;
procedure DestroySprite(Sprite:SpriteType);
{"уничтожение" спрайта}
begin
{ возвращаем память }
freemem(Sprite.Back,sizeof(SpriteArrayType));
freemem(Sprite.Img,sizeof(SpriteArrayType));
end;
procedure CalcSpritePosition(var Sprite:SpriteType);
{вычисление новых координат спрайта}
begin { спрайта и их приращений}
{по достижении границы экрана делаем,}
{ чтобы спрайт "отразился" от нее}
with Sprite do begin
if (x + Xsize + dx) >= 319 then
dx := -dx; {вычисляем новые приращения}
if (x + dx) <= 0 then
dx := -dx; {реализующие "отражение"}
if (y + Ysize + dy) >= 199 then
dy := -dy; {спрайта от стенок}
if (y + dy) <= 0 then
dy := -dy;
x := x+dx; { вычисляем новые }
y := y+dy; { координаты спрайта }
end;
end;
procedure PutBackground; {создание фона на экране}
var
i,j : word; {переменные цикла}
begin
for j := 0 to 199 do
for i := 0 to 319 do
Scr^[j,i] := lo(i+j*8);
end;
begin
scr := ptr(SegA000,0); {указатель на экран}
end. |