
ИСПОЛЬЗОВАНИЕ ДИНАМИЧЕСКОЙ ПАМЯТИ НА ПРИМЕРЕ РАБОТЫ СО СПРАЙТАМИ
ОГЛАВЛЕНИЕ
ВСТУПЛЕНИЕ ИЛИ ЗАЧЕМ ЭТО НАДО
УКАЗАТЕЛИ
ИСПОЛЬЗОВАНИЕ ДИНАМИЧЕСКОЙ ПАМЯТИ
НЕМНОГО ПРО КОМАНДЫ РАБОТЫ С КУЧЕЙ
КАК ЗАПИХАТЬ МНОГО ДАННЫХ В ПАМЯТЬ
ПОСЛЕСЛОВИЕ И НЕСКОЛЬКО ПОЛЕЗНЫХ СОВЕТОВ
Если вы хотите написать более или менее нормальную игру на Паскале или еще на чем, то вам так или иначе предстоит встретиться с динамической памятью. Так как Паскаль под переменные использует 64Kb оперативной памяти, вам ее скоро будет не хватать. Тут то вам и понадобиться динамическая память. По началу работа с динамической памятью пугает (сужу по своему опыту), и для смягчения стресса я приведу несколько преимуществ динамической памяти.
Надеюсь, теперь вы полны решимости, и готовы к первому шагу
Все данные, которые
хранятся в памяти компьютера, имеют адрес, указатель как раз и является переменной,
которая содержит этот самый адрес. В сове время, когда компьютеры были 16 битными,
а памяти было уже 640Kb (IBM PC 80286) и одним регистром (число от 0 до 65535
байт) нельзя было указать на адрес переменной превышающей границу 64Kb, было
принято решение о разбиении памяти на сегменты по 64 Kb для которых задавался
адрес в виде [сегмент : смещение]. Где сегмент являлся указателем на блок 64Kb,
а смещение являлось указателем на ячейку в этом блоке. В процессе совершенствования
компьютера разрядность процессора росла, и уже (IBM PC 80386) мог адресоваться
к 4Гб оперативной памяти, но походка оставалась все та же, хотя с некоторыми
изменениями, в 80386 процессоре был введен защищенный режим памяти, который
позволял работать с памятью несколько иначе. Но в эти дебри я лезть не буду,
потому что еще сам полностью не разобрался в том, как она работает.
Ну, так вот, из вышесказанного, надеюсь понятно, что УКАЗАТЕЛЬ - это
переменная адрес, который указывает на ту или иную ячейку памяти, (в Паскале
зачастую на переменную) а вот как она работает я постараюсь указать в примере.
{Пример
1: Объявление указателя (Бесполезно но наглядно)}
Var
A:Integer; {простая переменная}
Aptr:Pointer; {указатель без типа}
Aptr2:^Integer; {указатель именно на Integer}
{Пример
2: Операции с указателями}
Begin
Aptr:=@A; {создаем указатель на переменную A}
Aptr2:=Aptr; {теперь у нас Aptr и Aptr2 указывают на переменную A}
{Пример 3: Присвоение значений по адресу, на который указывает (угадайте что)}
Aptr2^:=666; {по указателю A заносим значение 666}
Writeln(A); {Угадайте что получится}
Aptr:=666; {а вот этот фокус приведет к ошибке, так как компилятор не знает на что указывает указатель Aptr и можно ли туда класть то, что мы его просим, кто не понял смотри пример 1}
End.
Как можно понять из вышеприведенного примера 1, в VAR'е мы объявляем нормальную переменную типа INTEGER, затем объявляем два указателя, один Aptr является просто указателем, а второй Aptr2 является указателем на переменную типа INTEGER. Далее в примере 2, указателю Aptr мы говорим, что он указывает на переменную A (типа INTEGER), точнее на ячейку памяти, в которой хранится значение переменной. Далее мы говорим, что указатель Aptr2 указывает на туже ячейку, что и указатель Aptr, а точнее на переменную A. Затем в примере 3 мы смотрим, как выглядит работа с указателями. По адресу Aptr2^ мы заносим значение 666. Тут надо обратить на маленький значок ^ после указания переменно, он говорит о том, что значение заносится в ячейку, на которую указывает наш указатель Aptr2, в противном случае компилятор выдал бы ошибку, так как переменная указатель это не INTEGER, а УКАЗАТЕЛЬ на integer. Ну так вот, занеся 666 по адресу на который указывает указатель мы получаем то, что переменная A становится равна значению 666, опа как.
ИСПОЛЬЗОВАНИЕ ДИНАМИЧЕСКОЙ ПАМЯТИ
Первое с чем нам
приходится столкнуться при написании игры, и большой объем данных, которые нам
нужно использовать (спрайты, карты, звук и т.д. и т.п.). При этом хочется и
спрайты разного размера и карты шире, чем 100х100 и т.д. и т.п. Вот именно об
этом и пойдет речь далее. Предположим у нас есть несколько спрайтов, которые
мы хотим загрузить в память, при этом все они разного размера. Для примера (5
спрайтов 100х100, 10х27, 64х23, 56х98). Вот тут то и встает вопрос, а как? Первое
что сделает неопытный программист, это предложит сделать несколько массивов
в соответствии с размерами спрайтов, или разбить спрайты на куски. Ни тот, ни
другой метод нас не устраивает, потому что это нудно, а в первом случае все
встанет, если у нас 100 разных спрайтов (поди, опиши столько массивов) а во
втором слишком уж нудно потом складывать эти маленькие кусочки и еще много чего.
Поэтому я предлагаю пойти более простым путем. Средства Паскаля позволяют резервировать
блоки памяти размером не более 64Kb (хотя можно и больше, но об этом потом,
сейчас главное понять, как) для этого используются команды New, GetMem и FreeMem
с Dispose, где GetMem и New резервируют память, а FreeMem и Dispose освобождают
ее. Попробую привести пример, как это работает.
Предположим у нас есть спрайтик размером 73x25, где на каждый цвет уходит 1
байт, то есть на весь спрайт нам понадобится 1825 байт, тут мы делаем следующее.
Вначале описываем тип спрайта:
Type
TSprite = Record
Xres,Yres : Word; {ширина и высота спрайта}
Data : Pointer; {указатель на данные}
End;
Далее
резервируем память под спрайт нужного нам размера
Procedure NewSprite(Xr,Yr:Word;Var Sp:TSprite);
Begin
{Сначала смотрим, а сможем ли мы это сделать, и если нет то выходим из процедуры сообщив об ошибке}
If LongInt(Xr)*Yr > $FFFF Then Begin
SpriteError:=Ture;
Exit;
End Else SpriteError:=False;
{Задаем ширину и высоту спрайта, она нам пригодиться для рисования спрайта}
Sp.Xres:=Xr;
Sp.Yres:=Yr;
{Выделяем нужный блок памяти}
GetMem(Sp.Data,Xr*Yr);
End;
А
теперь освободим память, выделенную под спрайт
Procedure DelSprite(Var Sp:TSprite);
Begin
{Освободим память из под спрайта}
FreeMem(Sp.Data,Sp.Xres*Sp.Yres);
{Обнулим ширину и высоту спрайта}
Sp.Xres:=0;
Sp.Yres:=0;
{На всякий случай}
Sp.Data:=Nil;
End;
Тут нужно обратить
внимание на NIL, который как бы говорит нам о том, что указатель не указывает
не на какой блок памяти. По идее все указатели по умолчанию равны NIL, но при
использовании переменных в процедурах и функциях значения локальных переменных
не присваиваются и могут содержать все что угодно, а точнее то, что храниться
в памяти на их месте, и это часто приводит к ошибкам.
Теперь, наверное, появится резонный вопрос, а как получить данные из этой памяти.
А просто, можно воспользоваться функцией MEM[Seg:Ofs] в следующем виде.
Pix(X,Y,Mem[Seg(Sp.Data^):Ofs(Sp.Data^)+10*Sp.Xres+2]
Где мы ставим по неким координатам точку из спрайта, а точнее в спрайте точка
по координатам (2,10). По другому это говорится так, взять 1 байт в сегменте
на который указывает Seg(Sp.Data^) и смещению, на которое указывает Ofs(Sp.Data^)
плюс 10 умноженное на ширину спрайта плюс 2. Вообще то Mem[Seg(Sp.Data^):Ofs(Sp.Data^)]
будет указывать на первый байт в блоке выделенной нами памяти.
Ну, так вот, примерно мы распределяем нашу динамическую память, которая в простонародии
и далее по тексту называется КУЧА (HEAP).
НЕМНОГО ПРО КОМАНДЫ РАБОТЫ С КУЧЕЙ
Процедура NEW резервирует память под указатель указанного типа, т.е. IPtr:^Integer; Такчто New(IPtr) выделит на переменную 2 байта, как раз столько, сколько занимает переменная типа INTEGER, а IPtr станет указвать на эти два байта
Процедура GetMem позволяет выделить под переменную столько памяти, сколько нам нужно, т.е. GetMem(P,32000) выделит блок памяти размером 32000 байт, но не более 64Kb
Процедура FreeMem освобождает заданное количество памяти, на которую указывает указатель, зачем это нужно я понятия не имею
Процедура Dispose память, полученную New на которую указывает указатель
Переменная MemAvail содержит в себе значение свободной памяти
Функции Mem
и MemW позволяют занести или получить значение в указанную ячейку памяти.
(Mem[$B800:10]:=24).
Функция Ptr возвращает указатель, который указывает на заданные нами сегмент
и смещение, т.е. после P:=Ptr($A000:0000) указатель P будет указывать на ячейку
памяти по адресу $A000:0000
Процедура MOVE, скопировать некоторый участок памяти MOVE(D^,S^,200) скопировать 20 байт на которые указывает указатель D по адресу на который указывает указатель S
Процедура FILLCHAR(S^,20,0) позволяет заполнить 20 байт на которые указывает указатель S нулями
Использование переменной со значком @, а точнее P:=@A, возвращает указатель на переменную A, вообще то эта фишка как то мало документирована, хотя она позволяет работать с переменными Паскаля как с указателями на Ц
При этом вам еще могут пригодиться следующая малодокументированная фишка Byte(P^):=10 - принимаем неопределенный указатель за байт, и заносим туда значение 10, или еще можно так Inc(Word(P)) увеличить смещение указателя на 1, только нужно помнить, что, потеряв указатель мы не почти ни как не сможем освободить память на которую он указывал
Остальные команды
можно найти в хелпе или какой-нибудь книге по Паскалю.
Ну, теперь думаю, хватит теории, займемся более интересными вещами, а точнее
тем, как использовать указатели в написании игр.
КАК ЗАПИХАТЬ МНОГО ДАННЫХ В ПАМЯТЬ
Конечно, указатель рульная штука, но предположим у нас есть 4000 спрайтов размером 10x10. (390kb) и нам в лом описывать 4000 указателя в блоке VAR (идиотская идея не правда ли). Ну, так вот, тут нам придется сделать финт ушами, Паскаль позволяет создать тип указатель, который указывает на другой тип, и потом этот указатель на тип использовать в этом типе как поле (во завернул). Приведу пример.
Type
TSpritePtr = ^TSprite;
TSprite = Record
Xres:Word;
Yres:Word;
Data:Pointer;
Next:TSprite; {указатель на тип TSprite (на самого себя)}
End;
Далее мы можем свободно создать такую переменную, затем при помощи команды NEW создать переменную Next на которую указывает поле Next (New(Sp.Next)) при этом имея указатель мы можем создать и следующую переменную, и т.д. и т.п., в общем получается цепочка из переменных, во как. Далее приведу несколько примеров а до остального вы додумаетесь сами,
Пример 1: Добавить переменную в цепочку.
Procedure Add(Var Sp:TSpritePtr;Sp1:TSprite);
Var T:TSpritePtr;
Begin
If Sp = Nil then Begin
{Вообще если в переменной не чего нет, то создаем ее заново}
New(Sp);
Sp1^:=Sp;End Else Begin
{Иначе дублируем указатель}
T:=Sp.Next;
{Ползем по цепочке до конца}
While T^.Next = Nil doT:=T^.Next;
{Создаем и присваиваем}
New(T^.Next);
T^.Next:=Sp1;
T^.Next^.Next:=Nil; {Это нужно чтобы не потерять конец цепочки}End;
End;
В общем-то, это кривой пример, так как значение полученного указателя указывает на параметры хранящиеся в SPL, а там есть указатель на поле данных спрайта, которое может быть изменено из вне, например, после дополнения спрайтом FreeMemSp1.Data,Sp1.Xres*Sp1.Yres) приведет к тому, что память, на которую указывает Data в нашей цепочке, будет освобождена, а в этом не чего хорошего нет. Но подобные фишки я оставляю на совести читателя, надо будет, сам сделает простенькую проверочку, или создаст новую ячейку памяти и скопирует туда данные спрайта. Хотя и из вышеприведенного примера можно выкроить кусочек вечяны, например если изменять данные спрайта один раз, они будут автоматически меняться для всех указателей указывающих на этот спрайт (можно использовать для некоторых вариантов АНИМАЦИИ или экономии памяти).
Удаляется это из памяти по следующему принципу
Procedure Del(Var Sp:TSpritePtr)
Var T:TSpritePtr;
Begin
T:=Sp.Next; {сохраняем указатель на следующий спрайт}
RepeatDispose(Sp); {сносим текущий спрайт}
Sp:=T; {делаем следующий спрайт текущим}
T:=T.Next; {сохраняем указатель на следующий спрайт}Until Sp=Nil; {и так пока не конец}
End;
Для нахождения нужного спрайта нужно найти нужный по счету или еще там по чему указатель и вернуть его. Примерно так (опять же в примере не каких проверок
Procedure GetSprite(SpL:TSpritePtr;Var Sp:TSpritePtr;N:Byte);
Var I:Word;
Begin
Sp:=Spl;
For i:=1 to N do
Sp:=Sp.Next;
End;
Удалить спрайт под некоторым номером можно по следующей логике, указателю на следующий спрайт в предыдущем спрайте если он есть присвоить значения следующего за удаляемым спрайтом спрайта и удаляемый спрайт первый то следующий за ним спрайт сделать основным а текущий удалить.
ПОСЛЕСЛОВИЕ И НЕСКОЛЬКО ПОЛЕЗНЫХ СОВЕТОВ
Старайтесь оптимизировать
работу с динамической памятью так, чтобы избежать ошибок и потери данных в памяти,
так как это может привести к незапланированным результатам и как это зачастую
бывает к зависанию компьютера. Более подробную информацию о динамической памяти
можно найти в любом описании Паскаля, так что дерзайте.
Если вы программируете на Borlan Paskal, то вы можете использовать под кучу
динамическую память выходящую за пределы 64Kb, тоесть всю, что у вас есть. Для
этого в строке главного меню CMPILE/TARGET надо выбрать режим Protect mode Application,
и ваша куча будет размером во всю свободную память компьютера. Но в защищенном
режиме есть некоторые проблемы, DMPI сервер контролирует всю используемую вами
память, и если вы обратитесь к памяти, не отведенной вам DMPI, появится ошибка
216, так что старайтесь программировать правильно (кстати, это повышает надежность
программы, во). :)
Ну вот примерно и все, пока.
Пример библиотеки по работе со спрайтами можно скачать www.vdargon-pas.chat.ru/xspr.zip