ZX-Ревю 1991 №11-12 1990 г.

Machine code - Сегодня своими приемами создания игровых программ в машинных кодах делится наш постоянный читатель Антон Александрович Яицкий.


MACHINE CODE

Сегодня своими приемами создания игровых программ в машинных кодах делится наш постоянный читатель Антон Александрович Яицкий.

Нам особенно приятно напечатать эту статью потому, что это первая ласточка от наших отечественных программистов. Ну не все же время читать Стива Тернера. Ведь есть же и у нас прекрасные профессионалы, имеющие свой опыт, свои секреты и приемы и, главное, готовые поделиться с теми, кто делает первые шаги в освоении глубин компьютера.

При создании программ в машинных кодах (например игровых) с использованием АССЕМБЛЕРа Z-80 очень трудно бывает сосредоточиться на самой программе, т.к. приходится рассеивать внимание на создании различных подпрограмм.

Есть весьма простой способ обойти это препятствие. И его алгоритм легко применим.

1) Сначала пишутся независимые подпрограммки - модули, которые являются законченными, т.е. оканчиваются кодом 201 (RET -возврат).

2) Потом они компилируются в машинный код с помощью компилятора (СИ, Ассемблер и т.п.) и размещаются в памяти таким образом, чтобы их координаты были известны.

3) На БЕЙСИКе пишется программа, которая управляет их работой. Это главная программа и ее сразу трудно написать в машинных кодах, поскольку ее приходится постоянно изменять, переделывать, корректировать и т.п.

4) Когда увязка процедур с помощью головной БЕЙСИК-программы закончена, переносим ее на бумагу со всеми точными данными - паузами, переходами, определениями. Это и будет алгоритм основного блока программы, другими словами "телом" будущей игры.

5) Далее переводим эту программу в язык низкого уровня, пользуясь ею как алгоритмом. Т.к. процедуры, вызываемые ею, уже отлажены, то ее отлаживать не так сложно. Во всяком случае нетрудно обнаружить место происхождения той или иной ошибки, раз алгоритм (сценарий игры) перед Вами и процедуры нижних этажей проверены и должны работать. Будут, конечно, ошибки, допущенные при переводе Бейсика в машинный код, но Вы с ними справитесь.

Располагать в памяти компьютера "тело" главного управляющего блока следует так, чтобы вместе с подготовленными ранее подпрограммами она представляла единое целое. После старта она будет вызывать в нужной последовательности те же процедура, что и БЕЙСИК-программа, повторяя ее алгоритм.

Какие при этой должны быть учтены нюансы?

1) Во-первых, есть разница в быстродействии. БЕЙСИК-программа работает гораздо медленнее, чем машинный код. Вследствие этого откомпилированная программа будет работать несколько не так, как ранее отлаженная в БЕЙСИКе ее предшественница. Это может повлиять на динамику управления программой и т.п. Поэтому заранее такое явление необходимо учитывать.

Надо предварительно продумать и определить, для каких частей Вашей программы зависимость от скорости БЕЙСИКа будет наиболее критической. Их лучше сразу написать на Ассемблере или СИ и откомпилировать, а из БЕЙСИКа вызывать через RANDOMIZE USR addr, где addr - адрес расположения данной подпрограммы.

2) Могут возникать ошибки при исполнении переходов. В Ассемблере адрес относительного перехода до инструкции JR n не должен отстоять более, чем на 128 байтов от точки исполнения перехода. Поэтому, если в независимых процедурах использовать эту команду желательно, то при написании головного блока ее использовать нельзя. Следует пользоваться абсолютным переходом JP nn или вызовом CALL.

Какие же модули в первую очередь нуждаются в быстродействии? Это прежде всего все выводы на экран (например спрайтов). Это смена экрана по команде LDIR (LDDR). Это процедуры компрессии/декомпрессии экранов и процедуры, управляющие

вводом/выводом информации.

Когда все будет сделано, Вам останется только дать команду: RANDOMIZE USR begin.

По всей видимости надо дать и несколько примеров перевода операторов БЕЙСИКа в стандартные конструкции в машинных кодах.

1) Часто встречающаяся в БЕЙСИКе команда PAUSE (пауза). Рассмотрим например PAUSE 200 (пауза на 4 секунды).

Для этой цели предлагается процедура WAIT. Она требует, чтобы заранее в регистр A (аккумулятор) было заслано число, определяющее длину этой паузы. LD A,200 CALL WAIT

Процедура WAIT будет ждать 4 секунды и вернется туда, откуда был сделан вызов. А вот как она выглядит:

WAIT LD HL,23672

LD (HL),0 WAIT1 CP (HL)

RET Z JR WAIT1

Здесь использован младший байт системной переменной FRAMES (23672), которая как известно служит как бы внутренними часами компьютера.

2) Так же полезной может быть и процедура TXT, обеспечивающая вывод текстового сообщения на экран.

В вызывающей процедуре нужно сделать:

байтов из приведенного выше образца и встанет из-за компьютера с гордо поднятой головой. Да, действительно он уже программирует с использованием машинного кода и не заметил, как преодолел сложный психологический барьер.

Постепенно подготавливая одну за другой процедуры, можно незаметно перейти к качественно новому уровню работы с машиной.

2. Наглядность.

Это обстоятельство даже не нуждается в комментариях.

3. Структурность.

Важная особенность состоит в том, что Вы можете готовить программы коллективно.

Предположим, что вместе с группой единомышленников Вы решили подготовить интересную игровую программу. В одиночку на это уйдет год и Вы решили сократить время, распараллелив работу между людьми и поручили каждому подготовку процедур и данных по одному направлению:

- один пишет все процедуры, связанные с выдачей на экран текстовых сообщений;

- другой пишет все процедуры, связанные с управлением программой от клавиатуры или от джойстика;

- третий - управление работой звукового динамика;

- четвертый - процедуры, связанные с экраном - построение экрана, помещение спрайтов, перемещение спрайтов, компрессия и декомпрессия экранов; пятый вообще ничего писать не умеет (он - художник) и рисует в редакторе ARTSTUDIO экран за экраном; а шестой - пишет музыку.

Последний - руководитель проекта. Он собирает все то, что сделано остальными, объединяет разнородные куски в единое целое с помощью своего главного БЕЙСИКовского блока. Он координирует работу остальных, назначает адреса, через которые передаются параметры из одной процедуры в другую, отводит те или иные размеры памяти участникам проекта по их запросу.

Вот Вы уже и создали свою фирму. Теперь пару лет кропотливой работы и можете выходить на международный уровень. В Вашей фирме найдется место и художнику и композитору и специалисту по иностранным языкам и менеджеру и даже специалисту по проведению маркетинговых исследований. Добавьте отдел распространения, рекламный отдел, бухгалтерию и отдел кадров и назначьте себя Генеральным Директором или хотя бы президентом.

4. Из того факта, что программа сделана из многочисленных мало зависящих друг от друга модулей вытекает и возможность создания частных и коллективных библиотек.

Когда Ваша группа закончит работу над своим проектом и решит создать новый, то окажется, что значительный пакет процедур может быть перенесен из предыдущего проекта без каких-либо изменений.

Поэтому сразу надо начинать работу не над одной программой, а над сериалом и, прорабатывая процедуры, учитывать возможность их использования и в других программах тоже.

Так у Вас будет создана библиотека. Придет время и Вы напишете на БЕЙСИКе нехитрую программу, которая прокрутит на магнитофоне одну-две кассеты с Вашими библиотеками, выберет из них те процедуры, которые Вам нужны в сегодняшнем проекте, загрузит их в память компьютера и объединит в единый модуль.

И в заключение несколько практических советов "ИНФОРКОМа".

I

Мы рекомендуем всем, кто пишет в машинных кодах, завести для себя единый стандартный блок, разместив его в раз и навсегда отведенной для него области памяти. Мы называем этот блок - керналь (KERNAL). Вот для чего он нужен.

Вы, конечно, понимаете, что абсолютное большинство созданных Вами стандартных процедур должны обмениваться друг с другом и с головной программой какими то данными. Например, процедура, которая занимается переброской экрана должна как минимум получить два параметра - адрес, начиная с которого хранится блок (2 байта) и длину этого блока (2 байта). Более того, если экран хранится в компрессированном виде, то его еще надо декомпрессировать. Поэтому мало принять эти два параметра - их еще надо передать в процедуру декомпрессии. Так отведите раз и навсегда 4 байта памяти для хранения параметров, используемых процедурой переброски экрана. Пусть это будут скажем адреса с 60000 по 60003. далее отведите 4 байта для параметров, передаваемых в процедуру, выполняющую музыкальное сопровождение - с 60004 по 60007. и т.д. У Вас образуется некий блок размером 1...2 K, который Вы всегда сможете вставлять в любые свои программы. Сегодня Вы не знаете, в каких адресах у Вас завтра будет находиться звуковоспроизводящая процедура, но Вам об этом и не надо думать. Размещайте ее где угодно. Ей это все равно, если и сегодня и завтра и всегда она будет получать свои данные из одних и тех же адресов и отправлять результат работы в одни и те же ячейки. Вот данные в этих ячейках могут меняться, но сами адреса - никогда.

Более того, если Вы делаете программу, в которой нет ни стрельбы ни бомбежки, а в Вашем кернале есть ячейки, отвечающие за передачу параметров в процедуры бомбометания и стрельбы, пусть они там и будут. Оставьте их, чтобы не сдвигать раз и навсегда сделанный керналь. Невелика потеря памяти, зато в конечном итоге экономятся годы труда.

И, наконец, в кернале можно хранить не только данные. В нем же можно хранить и адреса каких-то процедур. Посмотрите, как организован вектор переходов в статье "Потоки и каналы". Одним словом, принцип керналя - тот же, по которому организован раздел системных переменных в "Спектруме", только там он сделан К. Синклером в поддержку его ПЗУ, а Вы сделаете его в поддержку всех своих программ и он будет постепенно год от года наращиваться по мере накопления вами библиотек и личного опыта.

II

И последний практический совет. Он касается хранения текстовых сообщений (впрочем то же относится и к хранению в памяти экранов и прочих данных).

В целях упрощения подачи материала наш автор в примере процедуры TXT смешал в процедуре и машинный код и данные. То есть текстовое сообщение "ZX SPECTRUM" им размещено там же в процедуре, где и сам код. Адрес начала сообщения, как Вы видите из примера, передается через стек командой POP HL (а значит кто-то должен был заранее позаботиться и заготовить его там). Длина сообщения не определена, вместо этого маркером конца сообщения служит CHR$ 0 (операция проверки на конец - CP 0).

В качестве примера этот вариант годится, да видимо именно так и надо представлять идею, чтобы не усложнять ее излишними деталями, но теперь, в качестве комментария, мы должны сказать, что при написании реальных программ делают не так.

Размещать текстовые сообщения в процедурах вообще- то нельзя. Дело в том, что текст сообщения, как и любые данные, Вам потом может захотеться изменить, и тогда если изменится его длина, то после его вставки в процедуру переместятся команды, и в результате "поплывут" все команды относительных переходов JR, что очень неприятно.

Поэтому все сообщения хранят в едином блоке (таблице), отведя ей какой-то фиксированный участок памяти, например с 53000 и выше (только последите, чтобы он не перекрыл керналь или другую какую-либо таблицу). Более того, сам адрес начала этой таблицы текстовых сообщений можно хранить в кернале, отведя ему раз и навсегда 2 байта.

Теперь Вы можете разместить в таблице сколько хотите сообщений.

Например:

58000 - Сообщение1

58007 - Сообщение2

59567 - Сообщение250

Вызывается процедурой TXT то или иное сообщение по его номеру, который должен быть установлен предварительно в аккумуляторе. Например вызов 15-го сообщения пройдет так:

LD A,15 CALL TXT

Теперь осталось выяснить каким образом процедура TXT найдет пятнадцатое сообщение, если адрес его ей не известен. Во-первых, ей известно начало таблицы сообщений (его можно взять из керналя). Во вторых, вводится еще одна таблица -указательная таблица, в которой хранится величина смещения начала каждого сообщения от начала таблицы сообщений. На каждое смещение надо дать 2 байта и тогда если в Вашей программе 250 сообщений, то указательная таблица займет 500 байтов. Предположим, что мы ее разместим, начиная с адреса 57500.

Дальше логика такова. Номер сообщения Вам известен - 15. Перед ним стоят 14 сообщений. Поскольку в указательной таблице на каждое сообщение уделено 2 байта, то его надо удвоить - получим 28. Начало указательной таблицы Вам известно - 57500 (кстати его тоже надо брать из керналя, ведь оно может меняться от программы к программе). Теперь определяем сумму 57500+28=57528. Значит в адресах 57539 и 57530 хранится величина смещения в таблице сообщений. Определим ее. Пусть мы получили 1560. Теперь из керналя вводим базовый адрес таблицы сообщений. У нас это 58000. " Прибавляем к нему величину смещения 1560 и получаем 59560 - адрес, начиная с которого содержится ваше искомое сообщение.

А вот как это выглядит на практике см. Листинг-1.

И ряд дополнительных практических замечаний:

1. Хранение и вызов экранов по их номеру организуется точно так же.

2. Атрибуты, с которыми печатается текст (цвет фона, символа, признаки мигания, яркости, координаты позиции печати и т.п. хранятся вместе с текстом).

3. Управляющие коды, такие как CHR$ 13, CHR$ 6 и т.п. хранятся вместе с текстом.

4. В приведенных примерах не задана длина текстового сообщения. Компьютер определяет, что оно закончилось по появлению кода CHR$ 0 (NOP). Это не вполне экономично, т.к. сколько у Вас в программе сообщений, столько байтов Вы потратите на хранение этих маркеров. Обычно применяют другой способ - такой, какой применен в ПЗУ компьютера. Последний символ каждого сообщения увеличивают на 128, т.е. включают старший (седьмой бит). При проверке же на конец сообщения проверяют седьмой бит и если он включен, то печатают последний символ и выполняют возврат.

5. При переброске экранов удобнее не маркировать конец экрана, а хранить в указательной таблице длину каждого закомпрессированного экрана или вычислять ее через разность соседних значений в указательной таблице.

6. При переброске спрайтов указательная таблица вообще не нужна. Спрайты имеют как правило стандартный размер, скажем 32 байта каждый. Тогда по номеру спрайта всегда в таблице спрайтов можно найти его начало и конец. Адрес начала таблицы спрайтов хранится в кернале. Там же может храниться и максимальное число спрайтов в Вашей программе.

7. После всего вышесказанного Вы можете представить структуру своей будущей программы примерно в таком виде: (см. рис. 1)._

_Головной блок._

_Рабочие процедуры._

Область Ваших программных

_переменных._

Указательная таблица экранов. Таблица закомпрессированных

_экранов._

Таблица графики спрайтов. Таблицы шрифтов (если они

_нужны)_

Указательная таблица

_сообщений_

Таблица текстовых сообщений К Е Р Н А Л Ь

Рис. 1

Обратите внимание на то, что еще до начала написания программы Вы должны примерно прикинуть распределение памяти и решить сколько байтов и на что Вы отведете. При программировании же должны тщательно следить за тем, как Вы укладываетесь в намеченные пределы. Впрочем, это было лучше описано у Стива Тернера в статье "Профессиональный подход".

Обратите внимание также на то, что адреса границ этих программных областей - это тоже вещь, которую можно хранить в кернале.

Мы надеемся, что материал этой статьи не вызовет больших сложностей в понимании у начинающих, а профессионалам напомним, что мы ориентируемся в первую очередь не на них.

Зато спешим порадовать всех. В развитие данной темы в будущем году (причем в первом полугодии) мы напечатаем замечательную книгу Стюарта Николса "Применение АССЕМБЛЕРа для создания игровых и других скоростных программ на компьютере "ZX SPECTRUM".

Перевод подготовлен по нашему заказу специально для "ZX-РЕВЮ" Пашориным Валерием Ивановичем (г. Балашов, Саратовской обл.)

В значительной степени книга и посвящена вопросам конверсии БЕЙСИКа в машинный код.

Листинг 1.

Предположим, что в кернале базовый адрес указательной таблицы хранится, скажем, в 61245, а базовый адрес таблицы сообщений - в 61247.

Этой безобидной операцией мы сбрасываем флаг переноса без изменения аккумулятора (это делается на всякий случай). В пару HL загружается начальный адрес указательной таблицы

Удвоение содержимого аккумулятора. Обратим внимание на то, что если в нем было установлено число большее, чем 127, то после удвоения аккумулятор переполнится и включится флаг переноса. Удвоенное содержимое аккумулятора пересылается в регистровую пару DE. Если аккумулятор не переполнялся, то следующую операцию надо обойти. В противном случае надо увеличить регистр D на единицу.

Теперь в DE хранится удвоенный номер нашего сообщения. Прибавим его к начальному адресу указательной таблицы, хранящемуся в HL.

Ищем в нем смещение. Младший байт смещения отправляем в регистр E. Переходим к старшему байту. Отправляем его в регистр D. В пару HL загружается начальный адрес таблицы Ваших текстовых сообщений. Прибавив его к взятой из таблицы величине смещения, находим адрес начала сообщения Загружаем в аккумулятор первый символ сообщения.

Далее все также, как у автора статьи.

AND A

TXT

LD HL,(61245)

ADD A,A

LD E,A LD D,00 JR NC,BYPASS

INC D ADD HL,DE

BYPASS

LD E,(HL)

INC HL LD E,(HL) LD HL,(61247)

ADD HL,DE

LD A,(HL)

TXLOOP

CP 0

JR Z,TXEND RST 16 INC HL JR TXLOOP INC HL RET

TXEND




СОДЕРЖАНИЕ:


  Оставте Ваш отзыв:

  НИК/ИМЯ
  ПОЧТА (шифруется)
  КОД



Темы: Игры, Программное обеспечение, Пресса, Аппаратное обеспечение, Сеть, Демосцена, Люди, Программирование

Похожие статьи:
Оттяг - Винни-Пух и все-все-все - 2.
Авторы - Авторы журнала и контакты редакции.
От автора - Встречайте новую рубрику, новые обзорчики.
Премьера - нашумевшая игра Doom, новая версия коммандера SW-Commander.
Party report: ZX Party 2000; Wroclaw, Poland; 25-27 August 2000

В этот день...   21 ноября