Технические аспекты сотворения мира. Часть 1
Посвящается разработчикам компьютерных игр
Должно быть каждый, кто играл в компьютерные игры, мечтал создать собственную игру. Сотворить целый мир, который живет по тем законам, которые ты установил. Можно ли придумать что-нибудь более захватывающее?! Нельзя. Именно по этой причине первым крупным проектом почти каждого начинающего программиста становится компьютерная игра.
Желание разработать собственную игру является мощным стимулом, заставляющим глубоко изучать программирование - то, что позволяет отразить в материи реальности миры, созданные воображением. Как появляется игра? Сначала рождается идея, которая со временем превращается в необъятную вселенную, заселенную удивительными образами. Человек - творец этой вселенной - связывает образы между собой, пытаясь превратить их в слаженный механизм. Неисчисляемое количество раз он запускает этот механизм, проверяя его работу, добавляет новые детали, убирает ненужные. Когда все готово, он садится за компьютер. Осталось самое сложное - реализовать мир, который существует пока только в воображении. Далеко не каждый способен это сделать. Трудности, которые обязательно возникнут, способны заставить отказаться от мечты создать собственную игру. Кто знает, сколько удивительных миров остались из-за этого неизвестны никому, кроме их создателей? Для того чтобы облегчить муки рождения новых вселенных, была написана эта статья. В ней будут даны решения многих проблем, с которыми придется столкнуться при разработке игр. Автор надеется, что статья поможет Вам уделить больше времени творчеству, а не серой рутине, и что Вы завершите свое дело. Ну, а сейчас пришло время испачкать руки глиной, из которой Вы сотворите собственный мир...
Разработка игр, работающих под Windows, не представляется сложной - специально для этого существует технология DirectX. Другое дело создать игру под DOS - тут уж появляется много технических сложностей. Может возникнуть вопрос: зачем делать что-то, работающее под DOS, если это можно сделать под Windows? Конечно, DOS медленно, но верно уходит в небытие, но, как правило, начинающие программисты создают свои первые программы именно для этой операционной системы. Что делать тем, кто еще не знаком с программированием под Windows, но желает безотлагательно приступить к созданию своей игры? Использовать то, что есть.
В большинстве учебных заведений изучается Pascal, поэтому исходные тексты приведенных в статье подпрограмм будут даны именно на этом языке. При этом будет предполагаться, что в качестве основного инструмента разработчика выступает пакет Turbo Pascal 7.0. Тому, кто не знаком с Pascal'ем, не стоит огорчаться - идеи, представленные в этой статье, могут быть легко реализованы на других языках программирования.
В статье будут рассмотрены следующие вопросы:
- Использование больших объемов памяти. Поскольку в составе стандартных библиотек Turbo Pascal 7.0 отсутствуют процедуры, позволяющие работать с расположенной за первым мегабайтом памятью, то в этой статье будут приведены два способа, позволяющие использовать всю имеющуюся в компьютере память.
- Быстрый вывод графики. Необходимость рассмотрения этого вопроса обусловлена тем, что хотя модуль Graph и предоставляет широкие возможности для работы с графикой, процедуры и функции этого модуля не могут обеспечить необходимое для динамичных игр быстродействие. В статье будет дано решение этой проблемы. На основе этого решения можно реализовать подпрограммы, не уступающие по функциональности модулю Graph. Здесь также будут представлены процедуры, позволяющие работать с палитрой, и даны исходные тексты подпрограмм, реализующих такие спецэффекты, как имитация восхода и захода солнца.
- Использование клавиатуры, мыши и джойстика. При разработке игр часто возникает необходимость в обработке одновременного нажатия нескольких клавиш, но с помощью стандартных средств Pascal'я этого сделать невозможно. В данной статье будет дано готовое решение этой проблемы. Что касается мыши и джойстика, то поскольку в Pascal'е поддержка данных устройств отсутствует вообще, здесь будут представлены подпрограммы, устраняющие это упущение.
- Воспроизведение звука. Pascal позволяет воспроизводить звук только посредством PC Speaker'а, но для того, чтобы игрок полностью проникся атмосферой игры, следует использовать возможности, предоставляемые Sound Blaster'ом. В статье, кроме процедур вывода звука через Sound Blaster, будет дана информация, позволяющая воспроизводить звуковые файлы.
В заключительной части будут рассмотрены некоторые другие вопросы.
Прежде, чем перейти к основной части статьи, следует сказать еще несколько слов. В статье не будет поясняться, КАК следует решать те или иные проблемы. Вместо этого будут представлены ГОТОВЫЕ подпрограммы, которые можно использовать при разработке игр или других программ.
Использование больших объемов памяти
В современных играх приходится использовать объемы памяти, измеряемые мегабайтами. С помощью стандартных средств Pascal'я невозможно получить доступ к памяти, расположенной за первым мегабайтом. Здесь будут описаны два способа, позволяющие решить эту проблему.
Первый способ основывается на использовании Expanded Memory Specification (EMS), которая описывает интерфейс работы с расширенной памятью. Эта память делится на страницы по 16 Кб, доступ к которым производится через окно размером 64 Кб, находящееся в адресном пространстве процессора 8086. В этом окне одновременно могут быть представлены любые четыре страницы расширенной памяти. За все манипуляции со страницами отвечает менеджер расширенной памяти (Expanded Memory Manager - EMM), который входит в пакет программ MS-DOS и находится в файле "emm386.exe". Для инсталляции менеджера файл "config.sys" должен содержать строку, загружающую этот файл.
Рассмотрим по шагам методику работы с расширенной памятью.
1. Вызывается функция EMSSupportPresence, которая определяет наличие поддержки EMS. Если результат вызова равен False, значит, поддержка отсутствует и работать с расширенной памятью, используя EMS, невозможно.
2. С помощью функции AvailableExpandedMemory определяется количество свободных страниц расширенной памяти. Следует помнить, что размер одной страницы равен 16 Кб. Если возникла необходимость определить общее количество страниц, то для этого используется функция TotalExpandedMemory.
3. Для выделения памяти вызывается процедура AllocateExpandedMemory. В качестве параметров указываются: желаемое количество страниц расширенной памяти (wMemory) и переменная, предназначенная для хранения дескриптора (wHandle), посредством которого будут производиться манипуляции с затребованными страницами.
4. Доступ к выделенным страницам осуществляется процедурой MoveExpandedMemory, которая перемещает нужные страницы в окно, расположенное в основной памяти. Сегмент окна определяется функцией FrameSegment. Окно имеет следующую структуру: часть его со смещения $0000 до смещения $3FFF отводится для нулевой физической страницы, $4000-$7FFF - для первой, $8000-$BFFF - для второй и $C000-$FFFF - для третьей. Выделенные страницы расширенной памяти называются логическими страницами и нумеруются, начиная с нуля. При вызове процедуре MoveExpandedMemory передаются следующие параметры: дескриптор (wHandle), полученный процедурой AllocateExpandedMemory, номер логической страницы (wLogicalPage), которую необходимо переместить в окно, и номер физической страницы (bPhysicalPage), в которую будет произведено перемещение. Доступ к физической странице может быть произведен с помощью Mem, MemW и MemL или через указатель Ptr(FrameSegment,$0000). К примеру, для обращения к двадцать четвертому по счету байту второй физической страницы можно использовать выражение Mem[FrameSegment:$8000-1+24]. Поскольку вызов функции FrameSegment занимает много процессорного времени, то вместо нее рекомендуется использовать переменную, которой предварительно присвоено возвращаемое этой функцией значение.
5. Перед завершением программы необходимо освободить занятые страницы памяти. Для этого вызывается процедура FreeExpandedMemory, которой в качестве параметра передается дескриптор освобождаемых страниц.
В некоторых случаях при вызове упомянутых выше подпрограмм могут возникать различные ошибки. По этой причине после вызова каждой процедуры или функции (исключая EMSSupportPresence) следует анализировать значение переменной bEMSStatus. Если она равна нулю, значит, вызов прошел успешно, в противном случае возможны следующие ситуации:
$80 - программная ошибка EMM;
$81 - аппаратная ошибка EMS;
$82 - EMM занят;
$83 - неверный дескриптор;
$85 - нет свободных дескрипторов;
$87 - количество запрошенных страниц превышает общее количество страниц;
$88 - количество запрошенных страниц превышает количество свободных страниц;
$89 - создание дескриптора без выделения страниц невозможно;
$8A - неверный номер логической страницы;
$8B - неверный номер физической страницы.
Перейдем теперь ко второму способу. В его основе лежит Extended Memory Specification (XMS), которая позволяет работать с наращиваемой памятью. Менеджер (Extended Memory Manager - XMM), отвечающий за манипуляции с этой памятью, находится в файле "himem.sys", который должен быть указан в файле "config.sys".
Рассмотрим работу с наращиваемой памятью.
1. Для определения наличия поддержки XMS вызывается функция XMSSupportPresence, которая в случае неудачи возвратит значение False. Следует отметить, что вызов этой функции необходим даже в случае, если поддержка XMS гарантирована, поскольку при ее вызове происходит инициализация переменной pXMSFunction, в которой хранится точка входа в обработчик функций XMS.
2. Функцией AvailableExtendedMemory определяется наибольший свободный блок наращиваемой памяти (в килобайтах). Поскольку свободных блоков может быть несколько, возможен многократный вызов этой функции.
3. Для того чтобы менеджер выделил блок памяти, следует вызвать процедуру AllocateExtendedMemory, которой передаются такие параметры, как желаемый размер блока в килобайтах (wMemory) и переменная (wHandle), в которой будет храниться дескриптор выделенного блока.
4. Доступ к наращиваемой памяти осуществляется с помощью процедуры Move-ExtendedMemory. Кроме копирования данных из наращиваемой памяти в основную и обратно, она может производить перемещение данных из одной части наращиваемой или основной памяти в другую. При вызове процедуре передается указатель на запись TXMSRecord, поля которой должны иметь следующие значения: lRegionLength - длина перемещаемой порции данных (значение должно быть четным), wSourceHandle и wDestinationHandle - дескрипторы источника и приемника (дескриптор основной памяти равен нулю), lSourceOffset и lDestinationOffset - указатели начала копируемых данных (для наращиваемой памяти эти значения задают смещение от начала блока, для основной - указатель вида СЕГМЕНТ:СМЕЩЕНИЕ). Поскольку lSourceOffset и lDestinationOffset имеют тип Longint, то при перемещении из или в основную память следует производить преобразование типа: Longint(P), где P - переменная типа Pointer. Если источник и приемник перекрываются, то адрес источника должен быть меньше. Запись в наращиваемую память осуществляется следующим образом: сначала в буфер, расположенный в пределах первого мегабайта, заносятся необходимые данные, затем выполняется перемещение данных за пределы основной памяти. Чтение выполняется аналогично: данные из наращиваемой памяти перемещаются в основную, а затем читаются стандартными средствами Pascal'я.
5. Последним шагом, который осуществляется перед завершением программы, является освобождение наращиваемой памяти. Для этого вызывается процедура FreeExtendedMemory, которой передается дескриптор освобождаемой памяти (wHandle).
Результат вызова описанных подпрограмм (кроме XMSSupportPresence) можно проверить, проанализировав значение переменной bXMSStatus. Если bXMSStatus меньше $80, значит, ошибок нет, в противном случае возможно одно из следующих значений:
$8E - общая ошибка XMM;
$8F - невосстановимая ошибка XMM;
$A0 - вся память занята;
$A1 - использованы все дескрипторы;
$A2 - неверный дескриптор;
$A3 - значение wSourceHandle задано неверно;
$A4 - значение lSourceOffset задано неверно;
$A5 - значение wDestinationHandle задано неверно;
$A6 - значение lDestinationOffset задано неверно;
$A7 - значение lRegionLength задано неверно;
$A8 - ошибка при перемещении с перекрытием.
Доступ к памяти через XMS осуществляется быстрее, чем через EMS. Более того, его можно ускорить. Для этого необходимо, чтобы значения lSourceOffset и lDestinationOffset были четными, а lRegionLength - кратно четырем.
Завершая часть, посвященную работе с памятью, следует заметить, что проблему использования больших объемов памяти можно решить и стандартными средствами Pascal'я, но для этого разрабатываемая программа должна функционировать в защищенном режиме, из-за чего могут возникать различные неудобства.
Продолжение следует
Сергей Иванчегло

