Технические аспекты сотворения мира. Часть 5

on февраля 28, 2001 - 00:00

Некоторые мелочи
С самого начала, когда писалась самая первая часть этой статьи, предполагалось, что после описания использования клавиатуры, мыши и джойстика будет следовать часть, посвященная воспроизведению звука через 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).
Итак, в этой части были даны решения трех проблем. Поскольку проблемы были не очень большие, то и размер части оказался соответствующим. 
Следующая часть завершит статью, посвященную техническим аспектам разработки компьютерных игр.

Окончание следует

Сергей Иванчегло

№ 14

Яндекс.Метрика