Технические аспекты сотворения мира. Часть 5
Некоторые мелочи
С самого начала, когда писалась самая первая часть этой статьи, предполагалось, что после описания использования клавиатуры, мыши и джойстика будет следовать часть, посвященная воспроизведению звука через Sound Blaster, после чего статья будет завершена рассмотрением решений некоторых мелких проблем.
Просматривая материал, который уже был опубликован, автор пришел к выводу, что выбранная первоначально последовательность изложения несколько неудачна. Дело в том, что часто звук удобней добавлять в самом конце, когда игра практически готова, тогда как незначительные на первый взгляд проблемы могут очень сильно затормозить процесс создания игры. В связи с этим последнюю и предпоследнюю части статьи было решено поменять местами и рассмотреть здесь решения нескольких небольших проблем.
Первая проблема, которую стоит затронуть, связана с генерацией случайных чисел. Стандартные средства Pascal'я позволяют генерировать "случайные" числа, используя функцию Random. Слово "случайные" заключено в кавычки потому, что на самом деле последовательность чисел, которая получается посредством использования упомянутой функции, является не случайной, а псевдослучайной, то есть всего лишь похожей на случайную. При этом она не бесконечно длинная, а имеет длину лишь в несколько десятков тысяч элементов.
После того, как получен последний элемент, последовательность начинает повторяться и перестает быть даже псевдослучайной. Очевидно, что такой генератор чисел, как Random, плохо подходит для компьютерных игр, в которых требуется интенсивное использование случайных событий.
Для того чтобы решить эту проблему, необходимо разработать генератор, который мог бы генерировать если не бесконечную, то, хотя бы, очень длинную последовательность случайных чисел. Один из таких генераторов представлен дальше. Он генерирует последовательность, содержащую более четырех миллиардов элементов. Для криптографии этого, конечно же, было бы мало, но для игры вполне достаточно.
var lRandomSeed: Longint; procedure Randomize; assembler; asm mov ah,2 Ch int 21h push dx push cx db 66h <tab> pop ax db 66h mov Word(lRandomSeed),ax end;
function RandomLongint: Longint; assembler; asm db 66h mov a x,Word(lRandomSeed) db 66h,0BBh,0CDh,31h,16h,00h db 66h mul bx db 66h dec ax db 66h mov Word(lR andomSeed),ax db 66h push ax pop ax pop dx end;
function RandomReal: Real; var r1, r2: Real; begin r1:=RandomLong int; r2:=4294967296.0; if r1<0 then r1:=r1+r2; Rando mReal:=r1/r2 end;
function RandomWord(wLimit: Word): Word; assembler; asm call RandomLongint<r> mov ax,dx xor dx,dx div wLimit mov ax,dx end;
Ознакомимся с тем, как использовать представленные подпрограммы. Переменная lRandomSeed используется для хранения текущего значения случайной величины и однозначно определяет, какое значение будет следующим. Если при каждом запуске программы, использующей предложенные подпрограммы, значение lRandomSeed будет иметь одно и то же значение, то и генерируемая последовательность чисел будет одной и той же. Для того чтобы это избежать, в самом начале программы следует вызывать процедуру Randomize, которая будет инициализировать lRandomSeed значением, зависящим от текущего времени. После этого можно вызывать функции RandomLongint, RandomReal и RandomWord, которые будут возвращать случайные числа типов Longint, Real и Word соответственно. При этом числа типа Longint будут принимать все возможные значения, типа Real — лежать в промежутке от 0 до 1, включая первое число и исключая второе, а типа Word — от 0 до wLimit, также включая первое и исключая второе число.
Как уже говорилось, последовательность чисел, генерируемая приведенным генератором, имеет длину, чуть превышающую четыре миллиарда элементов. Если по какой-то причине этого недостаточно, то можно еще раз вызвать процедуру Randomize. Этот прием не изменит порядок следования чисел (ведь каждое последующее число однозначно определяется предыдущим), но зато внесет некоторую случайность.
Перейдем теперь к обсуждению другой проблемы. В играх часто возникает необходимость в реализации некоего периодического процесса, такого как, к примеру, счетчика времени.
Проще всего эту проблему решить таким способом: перехватить прерывание $1C, которое называется прерыванием пользовательского таймера и вызывается через каждые 55 миллисекунд (приблизительно 18 раз в секунду). Перехватить прерывание означает установить в качестве адреса обработчика этого прерывания адрес некоторой процедуры, которая будет выполнять то, что необходимо (скажем, выводить на экран показания счетчика). Далее приведены подпрограммы, реализующие описанную схему действий.
uses Dos;
var pInterrupt1ChVector: Pointer;
procedure Timer; interrupt; begin ... end;
procedure InitializeTimer; begin GetIntVec($1C, pInterrupt1ChVector); SetIntVec($1C, @Timer) end;
procedure UninitializeTimer; begin SetIntVec($1C, pInterrupt1ChVector) end;
Процедура Timer является той процедурой, которая будет вызываться каждый раз, когда будет возникать прерывание пользовательского таймера. В ее теле может находиться практически любой код, единственное, что по идее не будет работать, это та часть, которая реализует функции работы с клавиатурой. Процедура InitializeTimer предназначена для инициализации таймера. Только после ее вызова будет вызываться процедура Timer. UninitializeTimer выполняет действие, противоположное тому, что делает InitializeTimer. После вызова процедуры UninitializeTimer восстанавливается первоначальный адрес прерывания $1C, в результате чего процедура Timer перестает вызываться.
Завершая эту часть статьи, рассмотрим решение такой проблемы, как определение каталога, в котором находится игра. Это может понадобиться в тех случаях, когда необходимо провести какие-то манипуляции с файлами, содержащимися в каталоге игры, но когда этот каталог не является текущим.
Решение легко реализуется посредством стандартных средств Pascal'я. Для этого достаточно одной функции, которая приведена ниже.
uses Dos;
function MainDirectory: String; var Directory: DirStr; Name: NameStr; Extension: ExtStr; begin FSplit(FExpand(ParamStr(0)), Directory, Name, Exte nsion); MainDirectory:=Directory end;
Функция MainDirectory возвращает полный путь к каталогу, в котором находится программа, содержащая в себе эту подпрограмму.
Каталог определяется просто: сначала идентифицируется имя EXE-файла, содержащего запущенную программу (это достигается с помощью выражения ParamStr(0)), затем имя файла преобразуется в полное имя (выражение FExpand(…)), содержащее такие элементы, как каталог, имя и расширение файла, а затем полное имя раскладывается на составные части (выражение FSplit(…)), одна из которых и является тем, что необходимо найти (Directory).
Итак, в этой части были даны решения трех проблем. Поскольку проблемы были не очень большие, то и размер части оказался соответствующим.
Следующая часть завершит статью, посвященную техническим аспектам разработки компьютерных игр.
Окончание следует
Сергей Иванчегло

