Разработка 256-цветной палитры
Существует несколько различных подходов к разработке палитр для 256-цветных игр. Один метод, обычно используемый в играх приключенческого типа заключается в том, чтобы определить часть палитры, неизменной для всех изображений. Эти цвета используются для изображений, появляющихся более чем на одном игровом экране (например, это могут быть элементы пользовательского интерфейса, персонажи и некоторые другие объекты). Обычно для этого достаточно 64 цветов. Оставшиеся 192 цвета палитры будут изменяться от экрана к экрану в зависимости от декораций, сопровождающих игру. Другой подход состоит в создании единой палитры, используемой для всех пейзажей, объектов и персонажей игры. Этот метод мы здесь и обсудим.
Наилучшие для использования цвета
Если вы разрабатываете все изображения своей игры с нуля, у вас есть обширный выбор цветов для создания собственной игровой палитры. Однако, как правило, вы будете применять следующие диапазоны цветов:
§ Оттенки серого (от чисто-белого до чисто-черного);
§ Все оттенки основных цветов (красного, желтого, синего);
§ Оттенки каждого из вторичных цветов (оранжевого, зеленого, фиолетового);
§ Телесные тона для персонажей;
§ Земляные краски для земли, леса и т. д.
Последовательный порядок цветов
Что я понимаю под словами «диапазон цветов» или «последовательный порядок цветов»? Рассмотрите файл ЕХАМР01.РСХ. К примеру, вы хотите изобразить красный шар так, чтобы он выглядел действительно круглым, объемным. Он должен располагаться на черном фоне и освещаться с правой фронтальной стороны. На плоскости этот шар был бы представлен кругом, заполненным однородным цветом, средним оттенком красного. Такое представление шара показано в верхней части на рисунке 16.1.
Едва ли это убеждает, не так ли? Чтобы шар выглядел объемным, оттенки красного должны плавно переходить от светлого к темному, что придаст объекту соответствующую форму и подсветку.
Тот же самый технический прием применяется к прямоугольным и к цилиндрическим формам, как показано на рисунке.
Вот такой плавный переход оттенков одного цвета от светлого к темному и называю диапазоном или последовательным порядком цветов. Рассматривая файл ЕХАМР01.РСХ, обратите внимание, что палитра для него имеет несколько различных цветовых диапазонов. Попробуйте поэкспериментировать с это палитрой и изменить ее в соответствии с вашим цветовым предпочтением.
Сохранение цвета для прозрачности
При разработке палитры ни в коем случае нельзя забывать о том, что вы должны зарезервировать «прозрачный» цвет для вывода спрайтов. Если увидите пустоту в каком-то изображении, то, скорее всего, вы допустили ошибку. Помните, что «прозрачный» цвет при наложении спрайтов не изменит цвет фона.
Применение палитр других изображений
Существует немало коммерческих библиотек готовых изображений и фотографии, которые можно использовать без выплаты дополнительного вознаграждения их создателям. Однако, чтобы быть уверенным в том, что у вас действительно есть право на использование этих изображений в собственных программах, необходимо прежде всего очень внимательно прочитать липензионное соглашение для таких коллекций. Если у вас возникнут на этот счет какие-либо сомнения, всегда входите в контакт с издателем библиотеки.
Ни в коем случае не рекомендуется использование изображений, взятых из ББС или других коммуникационных служб. Многие находящиеся там графические файлы были просто скопированы из журналов, передач и фильмов без соблюдения соответствующих авторских прав. Постарайтесь ничего не испольвать в вашей игре без разрешения, полученного из сопроводительной документации. Безопаснее всего, конечно, употреблять только собственноручно изготовленные изображения и фотографии, либо, если нет возможности заниматься этим самостоятельно, нанять кого-нибудь специально, для разработки графики.
В предыдущем разделе мы обсуждали, какие цвета лучше всего размещать в палитре, чтобы иметь наиболее широкие возможности использования цветового диапазона.
Если вы попытаетесь какую- либо фотографию подогнать под имеющуюся палитру, то очень скоро убедитесь, что ее внешний вид окажется несколько изменен. Это может стать величайшим образцом поп-арта, но вряд ли будет пригодно для вашей игры. Чтобы обойти такое затруднение, необходимо создать палитру, включающую цвета, которые присутствуют в изображении.
Если вы намерены использовать в вашей игре фотографии и другие оцифрованные изображения, набора в 256 цветов хватит ненадолго. Запомните несколько правил:
§ Подбирайте изображения, имеющие сходную цветовую палитру, причем стремитесь, чтобы она была по возможности ближе к той, которую вы хотите использовать в своей игре. Это правило позволит вашим картинкам отображаться более точно;
§ Выбирайте изображения достаточно контрастные и в то же время с непротиворечивым цветовым решением. Если вы возьмете изображение, светлое с одного края и темное — с другого, это создаст лишние сложности в использовании принципа мозаичности, необходимого для трехмерных игр. Мы обсудим способы получения мозаичных изображений чуть позже;
§ Не берите изображения с большим количеством мелких деталей. К примеру, у вас есть фотография, выполненная с высоким разрешением, изображающая компанию людей. Когда это изображение уменьшится до размеров 64х64 пикселя, оно, скорее всего, будет сильно отличаться от оригинала. Лица людей на нем могут оказаться представленными всего одним пикселем, а то и вовсе пропадут!
Вероятно, хорошей идеей будет отмасштабировать ваши фотографии или другие оцифрованные изображения до окончательного размера, в котором они предстанут в игре. Изменять масштаб лучше всего перед тем, как вы начнете оптимизировать палитру. На то есть две причины:
§ Во-первых, после изменения масштаба пропадет часть цветов, что облегчит оптимизацию палитры;
§ Во-вторых, когда придет время собирать разрозненные изображения единый PCX-файл с общей палитрой, этот процесс пойдет намного легче.
В зависимости от того, используете ли вы 256-цветный или 24-битный графический редактор, нужно по-разному подходить и к созданию окончательной 256-цветной палитры.
Как можно скомбинировать палитры нескольких картинок без ухудшения изображения при использовании 256-цветной программы рисования? Один подход состоит в том, чтобы уменьшить количество цветов, насколько это возможно без ущерба для качества.
Позвольте мне продемонстрировать это на примере. Взгляните на рисунок 16.2. Слева изображена комната, как она выглядит при дневном освещении, а справа показан тот же самый вид ночью. Рисунок 16.3 ничем не отличается от предыдущего за исключением того, что палитра перекомпонована по градиентам, Сохранение цветов, уже имеющихся в изображении, упростит создание трехмерного изображения от руки.
Теперь посмотрим на рисунок 16.4 (файл ЕХАМР04.РСХ на дискете). В этом файле количество цветов, содержащихся в изображении, может быть уменьшено до 128. Это приведет к небольшому ухудшению качества изображения, но не сильно испортит общий вид рисунка. Зато у вас высвободится половина цветов, которые могут быть использованы для других картинок.
Посмотрим, что произойдет, если мы уменьшим количество цветов еще наполовину, то есть до 64. Взгляните на рисунок 16.5 (файл ЕХАМР05.РСХ). Качество изображения снова ухудшилось и теперь даже может потребоваться слегка подкрасить его вручную. Сокращение палитры можно продолжать и дальше, и только вы решаете, остается ли качество все еще достаточно приемлемым для вашей игры.
Подобным образом нужно обработать все изображения, которые вы хотите ввести в игру. Но не забывайте, что общее количество цветов после объединения всех рисунков не может превышать 256.
Следующим шагом было бы неплохо собрать все изображения, палитр которых вы сократили, в одном графическом файле.
Некоторые программ позволяют объединять палитры нескольких изображений в одной, и вы можете использовать ее для разработки оставшихся картинок вручную.
В 24-битовом графическом редакторе несколько проще создать новое изображение, включающее в себя все или почти все собранные вами промасштабированпые картинки. Например, на экране с разрешением 640х480 точек без труда можно разместить от 50 до 60 изображений размером 64х64 пикселя, даже если оставлять между ними для большей наглядности некоторый зазор. Кроме того, работая с 24-битовой программой, вам не нужно беспокоиться насчет сжатия палитры каждого изображения до нескольких цветов. Вы можете поместить все картинки в один файл, а затем просто отконвертировать его в 256-цветное изображение. Программа сама отберет для палитры те цвета, которые наиболее часто встречаются в изображениях.
Специальные соображения по поводу цифрового видео
Цифровые видеокадры значительно чаще используются для представления персонажей, чем образы, нарисованные от руки. С приходом Microsoft Video for Windows объединение оцифрованных видеофрагментов в играх и другом программном обеспечении является вполне обычным делом. Если ваша игра должна включать цифровые видеопоследовательности, убедитесь, что в каждый кадр вставляется одна и та же палитра. Для выполнения этой задачи у вас должны иметься соответствующие инструментальные средства. Усечение цветов в цифровой видеопоследовательности — нелегкое дело, особенно если отснято достаточно большое количество кадров. Обычно неплохо транслируются последовательности типа неподвижной «говорящей головы», потому что цвета в каждом кадре довольно хорошо согласуются между собой. Также можно снимать персонаж, расположив его перед синим экраном и осветив его так, чтобы на фон не падало никаких теней. Затем образ персонажа может быть наложен на любой другой фон с помощью специального технического приема, называемого Chromakeing (хромакей). Такой прием обычно используется в телевизионных передачах, например, для совмещения изображения ведущего с картами погоды (более подробно об этой процедуре говорилось в восьмой главе, «Высокоскоростные трехмерные спрайты»),
РАЗРАБОТКА МУЗЫКИ ДЛЯ КОМПЬЮТЕРНЫХ ИГР
Теперь вы знаете о методах оптимизации и других графических трюках и хитростях. Вы уже научились тому, как сделать графику ослепительной, как наделить игру искусственным разумом и, может быть, даже умеете считывать данные из портаджойстика. В этой главе мы расскажем о самом мощном методе длительного эмоционального воздействия на игрока. Звук и музыка позволят вам непосредственно управлять эмоциями пользователя в соответствии с контекстом вашей игры. Хорошо разработанные звуковые эффекты могут создать даже более яркую «виртуальную реальность», чем самая изощренная трехмерная графика. Эмоциональное воздействие музыки очень разнообразно, она может передавать ощущение страха, волнения, победы, грусти и даже ужаса.
Музыкальные и звуковые эффекты - это наиболее мощный инструмент, способный воздействовать на игрока на уровне эмоций. Если для вас это не очевидно, посмотрите снова фильм Спилберга «Парк юрского периода», В тот момент, когда тиранозавр появляется, чтобы съесть Ленд Ровера, выключите звук и тогда вы поймете, что весь ужас ситуации полностью создается за счет музыки и звука. Если графика позволяет игроку увидеть вашу «виртуальную реальность», то музыка и звуковые эффекты дают ему возможность почувствовать ее.
Хотя для многих из нас все это интуитивно понятно, вы должны практически проверить это на себе. Возьмите, например, «Терминатор II», «Конан-Варвар», «Звездные войны», «Чужие» или «Парк юрского периода». Каждый раз, когда по ходу фильма ваше сердце учащенно забьется, закройте глаза и попробуйте проанализировать только звуковое сопровождение, которое вы сейчас слышите. Послушайте, как тесно переплетается основная мелодия музыкальной партитуры фильма с его отдельными эпизодами, позволяя им зарождаться и развиваться. Как-нибудь, во время действительно шумной сцены, выключите звук. Вы почувствуете, что напряженность ситуации исчезла, словно в темной комнате вдруг зажгли свет.
Нет ничего из того, что создается в фильмах звуком и музыкой, что нельзя было бы повторить с помощью мультимедиа.
За одним только исключением. Наши звуковые эффекты и музыка должны быть интерактивны и должны совпадать с контекстом игры. Добавление интерактивных элементов в звуковое сопровождение игры многократно увеличивает ее эмоциональную напряженность.
Звуковое сопровождение содержит четыре основных компонента: диалоги, звуковые эффекты, фон и музыку. Ниже мы коротко рассмотрим каждый из этих компонентов и их роль в компьютерной игре.
Диалоги. Большинство диалогов, которые вы слышите в кино, переделываются в студии уже после того, как сцена была отснята. Эта процедура позволяет актеру (или актрисе) сфокусироваться на том, как звучат его (или ее) слова и, кроме того, позволяет звукорежиссеру контролировать точный аудиобаланс в окончательной звуковой дорожке. Все ваши диалоги должны быть сделаны профессионально и, желательно, в студии звукозаписи, Однако если у вас нет продюсера с громадным капиталом, вы, вероятно, не сможете ангажировать актеров Голливуда и снять классную студию звукозаписи. Однако вы можете привлечь к своей работе одного из специалистов в области мультимедиа. Обычно такие специалисты предлагают полный спектр услуг по озвучиванию компьютерных программ, включая наем актеров, звуковые эффекты на заказ и микширование звуковой дорожки. Кроме того, они могут представить звуковые данные в том формате, который требуется для вашего программного и аппаратного обеспечения.
Звуковые эффекты. Несмотря на доступность все большего количества звуковых библиотек, во многих случаях все же требуется создание своих оригинальных звуковых эффектов. Естественно, если что-то из библиотек подходит для вашей игры, вы можете это использовать, но в любом случае стоит проконсультироваться с опытным,в области мультимедиа специалистом, чтобы создать для своего проекта ряд оригинальных звуковых эффектов.
Фон. Фон - это те шумовые эффекты, на которые вы при просмотре фильма не обращаете внимания, однако их отсутствие ощущается мгновенно. Фон включает в себя звуки шагов, шумы автомобилей, ветра, щебетание птиц и другие звуки, окружающие нас в повседневной жизни.
Само собой разумеется, что фон должен быть связан с контекстом фильма. В кино каждый шаг и фабричный гудок добавляется в звуковую дорожку и покадрово синхронизируется уже после того, как фильм снят.
Шумовые эффекты создают большее ощущение «виртуальной реальности», чем точнейшая компьютерная графика. Воздействие шумовых эффектов многократно усиливается, если они проходят специальную цифровую обработку типа Qsound или реверберации. Реверберация — это метод обработки звукового сигнала, в результате которого на исходный звук накладывается эхо и отражение от поверхностей, так же, как это происходит в реальной обстановке. Например, для звуковой карты Creative Labs AWE 32 можно запрограммировать точные геометрические характеристики комнаты для каждого момента времени. Это делается с помощью механизма событий MIDI, при этом все шумовые эффекты звучат так, как если бы они происходили в настоящей комнате определенной формы и размеров.
Фон и цифровые эффекты - самая интерактивная составляющая звуковой дорожки. Благодаря шумовым эффектам игрок слышит звук шагов, грохот выстрелов, рычание приближающихся монстров, завывание ветра, щебет птиц, шум улицы. Воспроизведение таких звуков в реальном времени и в полном соответствии с вашей «виртуальной реальностью», позволяет игроку глубоко погрузиться в созданный вами мир. Удачное фоновое оформление усиливает ощущение «виртуальной реальности» больше, чем всевозможные очки, перчатки, шлемы и другие приспособления для компьютерных игр. Пренебрегая возможностями звуковых эффектов, вы отбрасываете пользователя во времена немого кино.
Музыка. Уже много было сказано по поводу того, как адаптировать музыку к интерактивной природе продуктов мультимедиа. В фильме события происходят последовательно. Поэтому композитор точно знает, с какого момента необходимо начинать нагнетать страсти. В компьютерных играх этот момент неизвестен; он зависит от того, когда же, наконец, игрок откроет ту самую «дверь, ведущую в Ад». Поэтому в одних играх просто в разных эпизодах звучат разные мелодии, тесно не связанные с происходящим, в других же авторы пробуют экспериментировать с разветвлением MIDI-музыки, пытаясь создать плавные переходы от одного сюжета к другому.
Были даже попытки создавать алгоритмическую музыку, то есть музыку, синтезируемую компьютером в реальном времени.
Вероятно, наиболее оптимальным решением будет собрать вместе всевозможные эмоции, возникающие по ходу игры, и обратиться к профессиональному композитору. То есть поступить так, как если бы вы снимали фильм. Композитор создаст музыкальный фон для каждой из ситуаций, а вы сможете его использовать по контексту игры. Таким образом, с одной стороны, эти мелодии не будут написаны для четко определенных моментов игры, а с другой, они все же будут связаны с эмоциональным контекстом. Поэтому, когда во время игры пользователь окажется в нервозной ситуации или попадет в опасность, вы с помощью музыки сможете усилить эти ощущения.
Другой подход к музыкальному сопровождению заключается в использо вании музыки просто для передачи общего настроения текущего эпизода. Основной же упор в этом случае делается на фон, диалоги и звуковые эффекты. Несомненно, звуки выстрелов, взрывов или вопли ужаса -отличный способ доходчиво рассказать игроку о том, что же происходит в игре в данный момент.
Нам, как разработчикам игр, очень повезло, что мы имеем в своем распоряжении очень мощные инструменты для создания звукового оформления. А ведь еще пару лет назад вы не смогли бы добиться от компьютера ничего, кроме бибиканья, свиста и треска. Естественно, что такое звуковое сопровождение вызывало желание побыстрее отыскать клавишу «выключить звук». Возможности первого поколения звуковых карт не многим отличались от возможностей встроенного динамика персонального компьютера. Несмотря на то, что звуковые карты, типа Adiib Personal Music System позволяли добавить в игру Интерактивные звуковые эффекты, их эмоциональное воздействие было еще очень ограниченным. Основная слабость пресловутых частотных синтезаторов состояла в том, что эмоциональный накал их музыки мог соревноваться разве что с мелодией, исполненной на детской дудочке.
К счастью, положение вещей кардинально улучшилось с появлением дисков CD-ROM, цифровых звуковых карт и MIDI-синтезаторов с волновыми таблицами.
Теперь компьютерные звуки и музыка могут поспорить в эмоциональном наполнении даже с картинами Спилберга. Я не оговорился, действительно могут! Ведь когда мы смотрим картины Спилберга, мы не можем принимать непосредственного участия в событиях. Мы видим, как динозавр атакует Ленд Ровера, но мы не можем вмешаться в ситуацию. В компьютерной же игре мы можем попытаться спастись от динозавра. Причем, когда мы будем убегать от злобного зверя, музыка и звуковые эффекты тесно сплетутся с нашими действиями. В результате наши нервы будут напряжены до предела — и не стоит забывать, что такого единства действия и звука можно достичь только в интерактивном мире!
Игра DOOM фирмы Id Software - один из лучших примеров использования интерактивного цифрового звука; Кто из вас не сжимался от страха на своем стуле, слыша, как из какого-нибудь угла раздается жуткое рычание и храпение монстра. Обратите внимание на то, что вы так бурно реагируете, даже еще не видя, а только слыша чудовище. Эмоциональное напряжение так велико, что когда, наконец, монстр выскакивает из-за укрытия, а затем, зашатавшись, падает под градом ваших пуль, вы испытываете самое настоящее чувство огромного облегчения. Все это достигается в немалой степени именно за счет звуковых эффектов, создающих у игрока соответствующее настроение. Удачный эффект помогает пользователю еще глубже окунуться в мир созданной вами игры.
Но я должен вас предостеречь. Насколько правильное использование звуковых эффектов и музыки усиливает игру, настолько же их плохое качество портит ее. Неудачные и не соответствующие эмоциональному настрою игры эффекты и музыка - это потеря денег, времени и дискового пространства. Даже если плохой, непрофессионально сделанный звук и не погубит окончательно вашу игру, он, несомненно, произведет на игрока отрицательное впечатление независимо от качества всей остальной игры.
Для того чтобы избежать подобных ошибок и сделать музыку и звук в вашей игре максимально эффективными, поступайте следующим образом:
§
Используйте звуковые эффекты, созданные профессионалами. Для этого либо обращайтесь к специалисту по мультимедиа, либо очень тщательно отбирайте эффекты из звуковых библиотек. Не воруйте звуковые эффекты из фильмов, с пластинок и телевидения. Все они защищены авторскими правами. Распространение вашей игры в этом случае будет запрещено и вам даже возможно придется объясняться в суде. Если вы считали замечательные звуковые эффекты из «Звездных войн» с BBS — это еще не значит, что вы имеете право их использовать в игре. Если в библиотеках звуковых клипов вам не удается найти звуковые эффекты, полностью удовлетворяющие вашим требованиям, остается только обратиться к специалисту по мультимедиа;
§ Используйте музыку, написанную профессионалами. Для этого либо обратитесь к композитору, создающему компьютерную музыку, либо используйте подходящие к вашей игре высококачественные клипы. Учтите, что композитор должен работать именно в жанре компьютерной музыки, иначе у него возникнут такие же трудности, как у бас-гитариста, который вдруг решил играть на саксофоне. Поэтому то, что данный человек — великий музыкант, еще не значит, что он способен создать классную midi-композицию для вашей игры. Необходимо, чтобы композитор умел обходить все слабые стороны и эффективно использовать все преимущества MIDI;
§ Убедитесь, что в любой момент времени музыка поддерживает эмоциональное напряжение в игре и не расходится с контекстом. Помните об интерактивной природе вашей музыки и о том, что она должна быть связана с контекстом игры. Чем больше внимание вы уделите этому, тем сильнее будет эффект;
§ Посмотрите фильмы, похожие на вашу игру. Каждый раз, когда вы будете их смотреть, попробуйте осознать, каким образом ваши эмоции усиливаются звуковыми эффектами и музыкой. Если вы не обладаете хорошим музыкальным слухом или если вы четко не представляете, как эффективно использовать звук и музыку в игре, вам следует передать эту часть проекта кому-нибудь другому;
§ Прислушивайтесь к мнению своего композитора. Найдите композитора с опытом работы с вашим аппаратным обеспечением и хорошо ориентирующегося в мультимедиа. Очень четко объясните композитору, что вам нужно. Дайте ему образцы музыки с CD или из фильмов, эмоционально совпадающих с вашей игрой. Ваше сотрудничество будет плодотворнее, а музыка лучше, если вы ясно изложите композитору, чего вы с помощью его музыки хотите достичь в своей игре. Если же композитор не поймет вашего замысла, то вы только понапрасну потратите деньги, нервы и время.
Именно с помощью эффективного использования звуковых эффектов и музыки преодолевается тот самый психологический барьер, который мешает пользователю забыть, что он «всего лишь играет» и полностью погрузиться в созданный вами мир. Я думаю, что вы уже на собственном опыте убедились, насколько важны для восприятия игры удачное сочетание звуковых эффектов и музыки.
Разрешающая способность и количество цветов
Некоторые игры используют 16 цветов, другие - 256. В этой книге мы будем концентрироваться на разработке 256-цветпых игр. Разрешение экрана может быть 320х200, 320х240, 640х480 и выше. Применение высокой разрешающей способности связано с рядом проблем достижения необходимого быстродействия, если только вы не используете высокопроизводительный компьютер но принципы разработки графики в любом случае остаются одними и теми же.
Реализация многозадачности при помощи прерываний
При работе под операционной системой DOS, если и существует какой-нибудь способ, с помощью которого мы можем реализовать многозадачность, так это только использование механизма прерываний. Некоторые прерывания вырабатываются в результате внешних событий, в то время как другие евязаны с событиями внутренними. Для примера, давайте сначала рассмотрим, пожалуй самое популярное прерывание — прерывание от клавиатуры.
Всякий раз, когда происходит нажатие клавиши, ваша программа, что бы она при этом ни делала, останавливается и начинает работать процедура обслуживания прерываний клавиатуры. (Готов спорить, вы и не подозревали, что ваша программа останавливается при каждом нажатии на клавишу, однако это действительно так!) После окончания процедуры обслуживания прерывания управление снова передается вашей программе. Во время всего этого процесса ваша программа, данные и все остальное остается целым и невредимым. Для любой процедуры обслуживания прерываний это Правило Номер Один; без определенной цели ничего не должно уничтожаться. Так, например, если ваша процедура обслуживания прерывания использует для своей работы регистры процессора, вам первым делом следует сохранить содержимое этих регистров, затем осуществить обработку прерывания и снова восстановить содержимое регистров в точно таком же виде, каким оно было до прерывания.
Прежде чем мы начнем разбираться с основными принципами написания и установки обработчика прерываний, давайте взглянем, какие же прерывания есть у персонального компьютера. Посмотрите на таблицу 12.1.
Таблица 12.1. Прерывания ПК.
Номер |
Адрес |
Функция | |||
0h |
000-003h |
Деление на ноль | |||
1h |
004-007h |
Пошаговое выполнение | |||
2h |
008-00Bh |
Немаскируемуе прерывание | |||
3h |
00C-00Fh |
Точка останова | |||
4h |
010-013h |
Переполнение | |||
5h |
014-017h |
Печать содержимого экрана | |||
6h |
018-01Bh |
Зарезервировано | |||
7h |
01C-01Fh |
Зарезервировано | |||
8h |
020-023h |
Таймер 18.2 | |||
9h |
024-027h |
Клавиатура | |||
0Ah |
028-02Bh |
Зарезервировано | |||
0Bh |
02С-02Fh |
RS-232 Порт 1 | |||
0Ch |
030-033h |
RS-232 Порт 0 | |||
0Dh |
034-03Bh |
Жесткий диск | |||
0Eh |
038-03Bh |
Дискета | |||
0Fh |
03C-03Fh |
Зарезервировано | |||
10h |
040-043h |
Функция видеовывода | |||
11h |
044-047h |
Проверка оборудования | |||
12H |
048-04ВН |
Проверка памяти | |||
13Н |
04C-04FH |
Функции ввода/вывода на дискету | |||
14Н |
050-053Н |
Функции ввода/вывода последовательного порта | |||
15Н |
054-057Н |
Функции ввода/вывода на кассетный магнитофон | |||
16Н |
058-05ВН |
Функции ввода клавиатуры | |||
17Н |
05C-05FH |
Функции вывода на принтер | |||
18Н |
060-063Н |
Точка входа в ROM BIOS | |||
19Н |
064-067Н |
Процесс загрузки | |||
1АН |
068-06ВН |
Получение информации о времени | |||
1ВН |
06C-06FH |
Управление прерыванием | |||
1СН |
070-073Н |
Управление таймером | |||
1DH |
074-077Н |
Таблица инициализации видеосистемы | |||
1ЕН |
078-07ВН |
Таблица параметров дискеты | |||
1FH |
07C-07FH |
Таблица графических символов | |||
20Н |
080-083Н |
Завершение DOS программы | |||
21Н |
084-087Н |
Универсальные функции DOS | |||
22Н |
088-08ВН |
Адрес завершения DOS | |||
2ЗН |
08C-08FH |
Адрес обработчика Ctrl+Break | |||
24Н |
090-093Н |
Адрес обработчика критических ошибок DOS | |||
25Н |
094-097Н |
Абсолютное чтение с диска DOS | |||
26Н |
098-09ВН |
Абсолютная запись на диск DOS | |||
27H |
09C-09FH |
Установка резидентной программы DOS | |||
28-3FH |
0A0-0FFH |
Зарезервировано для DOS | |||
40-7FH |
100-1FFH |
Не используется | |||
80-F0H |
200-ЗСЗН |
Зарезервировано для Бейсика | |||
F1-FFH |
3C4-3FFH |
Не используется |
Таблица 12.1 - это таблица векторов прерываний. Она занимает первые 1024 байт памяти каждого персонального компьютера. Всего в этой таблице 256 элементов, каждый из которых имеет размер 4 байта и представляет собой значение дальнего указателя на процедуру обслуживания прерывания. Как вы могли заметить, персональный компьютер не использует все 256 прерываний. Однако число задействованных прерываний постоянно растет.
Персональный компьютер поддерживает прерывания как на аппаратном, так и на программном уровне. Программные прерывания создаются с помощью расширенного набора инструкций процессора 80х86. Они были разработаны специально для того, чтобы дать возможность не только физическим устройствам мгновенно прерывать исполнение текущей программы. Большинство прерываний на персональном компьютере осуществляются программным путем. Однако некоторые осуществляются только с помощью аппаратуры (к ним относятся немаскируемые прерывания и прерывания от клавиатуры). С точки зрения программиста оба типа прерываний работают одинаково, поэтому нас это деление затрагивать не будет.
Внимание!
При использовании прерываний будьте очень осторожны: вы играете с огнем. Если вы допустите ошибку, компьютер может «зависнуть», что иногда приводит к потере важных данных. Будьте внимательны!
Итак, в соответствии с характером нашей игры мы должны выбрать нужные нам прерывания. Как их выбирать, еще не ясно, но мы разберемся с этим чуть позже. Затем нам нужно будет зарегистрировать (установить) свою собственную процедуру обработки прерываний. Вот, собственно, и все.
Единственное, чего нам не хватает для начала, это самой процедуры обработки прерываний, поэтому давайте разбираться, как она создается на языке Си.
Реализация отсектеля лучей
Я написал совершенно полную реализацию алгоритма отсечения лучей. Он закомментирован и даже содержит какую-то логику. Эта программа включена в комплекте поставки на дискете в файле RAY. С. Демонстрационная программа загружает двухмерную карту мира в виде ASCII-файла. На рисунке 6.30 приведена подобная карта. Она создается с помощью обычного текстового редактора.
Вся программа слишком длинна, чтобы включить ее в книгу, поэтому здесь приведена только ее самая интересная часть: процедура отсечения лучей. Попробуйте с ней разобраться и понять, что это и зачем так сделано.
|
Листинг 6.4. Процедура отсечения лучей (RAYLIST.C)
void Ray_Caster(long x, long y,long view_angle)
{
// эта функция выполняет расчет 320 лучей и строит игровой
// экран на основе их пересечений со стенами. Расчет производится
// таким образом, что все лучи отображаются на поле просмотра
// с углом 60 градусов
// после расчета траекторий лучей, рассчитываются координаты
// их пересечений со стенами. Координаты первой точки пересечения
// запоминаются. Ближайшие к игроку точки используются для
// построения битового образа изображения. Расстояния используются
// для определения высоты текстурных фрагментов и вертикальных линий
// Примечание: эта процедура использует функции стандартной
// библиотеки компилятора для работы с плавающей точкой
// (это работает медленно). Код не оптимизирован (еще медленнее),
// и в довершение всего обращается к функциям графической
// библиотеки компилятора фирмы Microsoft (что уж вовсе
// никуда не годится!) // Однако все это имеет определенную цель - легкость
// для понимания того, как работает процедура
int rcolor;
long xray=0, // счетчик вертикальных пересечений
yray=0, // счетчик горизонтальных пересечений
next у cell, // используются для вычисления
next_x cell, // номера следующей ячейки по ходу луча
cell_x, // координаты текущей ячейки
се11_у, // луча
x_bound, // следующие вертикальная
у_bound, // и горизонтальная точки пересечения
xb_save, // в этих переменных запоминаются
yb_save, // координаты точек пересечения
x_delta, // эти переменные показывают, на сколько
y_delta, // надо сместиться для перехода к следующей ячейке
ray, // текущий луч для отсечения
casting=2, // показывает компоненты Х и Y луча
x_hit_type, // координаты блока, с которым пересекся луч
y_hit_type, // используются при отрисовке
top, // верхняя и нижняя координаты области,
bottom; // отрисовываемой как стена (с помощью текстуры)
float xi, // используется для определения х- и у-пересечений
yi,
xi_save, // используется для сохранения
// точек пересечения Х и У
yi_save,
dist_x, // расстояние до х- и у-пересечениЙ
dist_у, // от точки просмотра scale;
// масштаб
//СЕКЦИЯ 1 ////////////////////////////////////////
// инициализация
// вычисляет начальный угол от игрока. Поле просмотра 60 градусов.
// Таким образом, рассматриваем только половину - 30 градусов
if ( (view_angle-=ANGLE_360) < 0)
// разворачиваем вектор направления взгляда
view_angle=ANGLE_360 + view_angle;
} // конец оператора if
// выбираем цвет для луча
rсо1оr=1 + rand()%14;
//СЕКЦИЯ 2 ////////////////////////////////////////
// цикл для всех 320 лучей
for (ray=0; ray<320; ray++)
// вычислить первое х-пересечение
if (view_angle >= ANGLE_0 && view_angle < ANGLE_180)
{
// вычислить первую линию, которая пересекается с лучом.
// Примечание: эта линия должна быть выше (впереди
// на игровом поле) игрока.
y_bound = CELL_Y_SIZE + CELL_Y_Sf2E * (у / CELL_Y_SI2E);
// вычислить смещение для перехода к следующей
// горизонтальной линии
y_delta = CELL_Y_SIZE; // размер ячейки по вертикали (ред.)
// основываясь на первой возможной горизонтальной линии отсечения,
// вычислить Х-пересечение и начать расчет
xi = inv_tan_table[view_angle] * (y_bound - у) + х;
// установить смещение
next_у_cell = 0;
} // конец обработки верхней половины плана
else
{ // вычислить первую горизонтальную линию, которая может
// пересекаться с лучом. Это будет позади игрока
y_bound = CELL_Y_SI2E * (у / CELL_Y_SIZE);
// вычислить смещение для следующей горизонтальной линии
y_delta = -CELL_Y_SIZE;
// основываясь на первой возможной горизонтальной линии отсечения,
// вычислить Х-пересечение и начать расчет
xi = inv_tan_table[view_angle] * (y_bound - у) + х;
next_y_cell = -1;
} // конец обработки нижней половины плана
//СЕКЦИЯ 3 ////////////////////////////////////////
// вычислить первое х-пересечение
if (view_angle < ANGLE_90 || view_angle >= ANGLE_270) {
// вычислить первую вертикальную линию, которая будет
// пересекаться с лучом. Она должна быть справа от игрока
x_bound = CELL_X_SIZE + CELL_X_SIZE * (х / CELL_X__SIZE);
// вычислить смещение
x_delta = CELL_X_SIZE;
// основываясь на первой возможной вертикальной линии отсечения,
// вычислить Y-пересечение и начать расчет
yi = tan_table[view_angle] * (x_bound - х) + у;
next_x_cell = 0;
} // конец обработки правой половины плана
else
{
// вычисляем первую вертикальную линию, которая может быть
// пересечена лучом. Она должна быть слева от игрока
x_bound = CELL_X_SIZE * (х / CELL_X_SIZE);
// вычислить расстояние до следующей вертикальной линии
x_delta = -CELL_X_SIZE;
// основываясь на первой возможной вертикальной линии отсечения,
// вычислить Y-пересечение
yi = tan_table[view_angle] * (x__bound - x) + у;
next_x_cell = -1;
}
// начать отсечение
casting =2; // два луча для одновременного отсечения
хrау = уrау = 0; // сбросить флаги пересечения
//СЕКЦИЯ 4 ////////////////////////////////////////
while(casting)
{
// продолжить отсечение лучей
if (xray!=INTERSECTION_FOUND)
{
// тест на совпадение луча с асимптотой
if (fabs (y_step[view_angle])==0)
xrау = INTERSECTION_FOUND;
casting--;
dist_x = 1e+8;
} // конец проверки на совпадение с асимптотой
// вычислить текущую позицию карты для проверки
сеll_х
= ( (x_bound+next_x_cell) / CELL_X_SIZE);
cell_y = (long)(yi / CELL_Y_SIZE) ;
// проверить, есть ли в этом месте блок
if ((x_hit_type = world[(WORLD_ROWS-1) - cell_y][cell_x])!=0)
{
// вычислить
расстояние
dist_x = (yi - y) * inv_sin__table[view angle];
yi_save == yi;
xb_save = x_bound;
// закончить х-отсечение
хrау = INTERSECTION_FOUND;
casting--;
} // конец проверки попадания луча на стену блока
else
{
// вычислить следующее Y-пересечение
yi += y_step[view_angle];
} //конец оператора else
} // конец проверки на отсечение по оси Х
//СЕКЦИЯ 5 ////////////////////////////////////////
if (yray!=INTERSECTION_FOUND)
{
// тест на попадание луча на асимптоту
if (fabs(x_step[view_angle])==0)
{
уrау = INTERSECTION_FOUND;
casting--;
dist_y=1e+8;
}
// вычислить
позицию карты
ceil_x = (long)(xi / CELL_X_SI2E);
cell_y = ( (y_bound + next_y_cell) / CELL_Y_SIZE) ;
// проверить, находится ли в этом месте блок
if ((y_hit_type = world[(WORLD_ROWS-1) - cell_y] [cell_x] ) !=0)
{
// вычислить
расстояние
dist_y = (xi - х) * inv_cos_table[view angle];
xi_save = xi;
yb_save = y_bound;
// закончить вычисление Y-пересечения
yray = INTERSECTION_FOUND;
casting--;
} // конец обработки попадания луча на блок
else
{
// вычислить следующее Х-пересечение
xi += x_step,[view_angle];
} // конец оператора
else
} // конец проверки на отсечение по оси У
// перейти к следующей точке пересечения
x_bound += x__delta;
y_bound += y_delta;
}
//СЕКЦИЯ 6 ////////////////////////////////////////
// выяснить, какая из стен ближе - вертикальная или горизонтальная
// и затем нарисовать ее
// Примечание: в дальнейшем мы заменим вертикальную линию на
// текстурный блок, пока же достаточно того, что есть
if (dist_x < dist_y)
{
sline(x,y,(long)xb_save,(long)yi_save, rcolor);
// вертикальная стена ближе горизонтальной
// вычислить масштаб и умножить на поправочный коэффициент
// для устранения сферических искажений
scale = cos_table[ray]*15000/(1e-10 + dist_x);
// вычислить координаты верха и низа
if ( (top = 100 - scale/2) < 1) top =1;
if ( (bottom = top+scale) > 200) bottom=200;
// нарисовать
фрагмент стены
if ( ((long)yi_save) % CELL_Y_SIZE <= 1 ) _setcolor(15);
else
_setcolor(10);
_moveto((int)(638-ray),(int)top);
_lineto((int)(638-ray),(int)bottom);
}
else // сначала надо нарисовать горизонтальную стену
{
sline(x,y,(long)xi_save,(long)yb_save,rcolor) ;
// вычислить
масштаб
scale = cos_table[ray]*15000/(le-10 + dist_y);
// вычислить координаты верха и низа
if ( (top = 100 - scale/2) < 1)
top = 1;
if ( (bottom = top+scale) > 200) bottom=200;
// нарисовать
фрагмент стены
if (((long)xi_save) % CELL_X_SIZE <= 1 )
_setcolor(15) ;
else
_setcolor(2);
_moveto((int)(638-ray),(int)top);
_lineto((int)(638-ray),(int)bottom) ;
} //конец оператора else
//СЕКЦИЯ 7 //////////////////////////////////////
//отсечь
следующий луч
if (++view_angle>=ANGLE_360)
{
// установить угол в 0
view_angle=0;
} // конец оператора if
} // конец цикла for
по лучам
} // конец функции
Реализация алгоритма отсечения лучей разделена на семь основных частей, чтобы легче было понять предназначение каждой из них.
§ Первая часть произведет инициализацию всех массивов и внутренних переменных. В этой части определяется направление взгляда игрока и на его основании вычисляется стартовый угол.
Также в этой секции выбирается случайное число, задающее цвет трассируемого луча;
§ Вторая и третья части вычисляют Х- и Y-пересечения текущего луча с периметром ячейки, в которой находится игрок. После того как найдено первое пересечение как с осью X, так и с осью Y, устанавливается значение нескольких переменных для определения траектории луча по отношению к осям координат. Информация, полученная в этих частях, используется в четвертой и пятой части;
§ Четвертая и пятая части продолжают проверку пересечений. Каждое из пересечений с координатной осью проверяется на пересечение с объектом. Если это происходит, то вычисляется дистанция и запоминается для дальнейшего использования. Эта информация может быть использована для тексту рирования объектов. Хотя рассмотренный нами трассировщик лучей и не делает этого, он предоставляет достаточно информации для текстурирования. Например, если луч пересекает середину стены блока, это означает, что мы должны вывести на экран 32-ю вертикальную полосу соответствующей текстуры. Более подробно этот вопрос будет рассмотрен позднее, в главе, посвященной описанию игры Warlock;
§ В шестой части заканчивается обработка луча. К этому моменту мы уже вычислили горизонтальные и вертикальные пересечения и запомнили расстояния до них. Следовательно, мы готовы нарисовать на экране соответствующую лучу вертикальную полосу. Для этого мы определяем, которое из пересечений находится ближе всего. Горизонтальная позиция для отрисовки соответствует номеру текущего луча и меняется в диапазоне от 0 до 319. Высота рисуемого фрагмента вычисляется на основании расстояния до игрока и с некоторыми корректировками для улучшения впечатления;
§ Седьмая часть увеличивает текущий угол и осуществляет переход к первой части. Цикл выполняется до тех пор, пока все 320 лучей не будут отсечены.
Вообще, то, что мы написали - неплохая штука. Она не рисует фактур и не просчитывает освещение, но это несложно реализовать. Теперь, когда у нас появилась работающая программа, есть смысл поговорить об оптимизации.
Реализм
Чтобы небо выглядело более реалистично, звезды, находящиеся на меньшем расстоянии от корабля, на котором, мы летим (от нашей виртуальной точки обзора), должны светить ярче. Я предлагаю следующий алгоритм создания трехмерного звездного неба:
1.
Для описания звезды создать структуру, содержащую информацию о ее расположении, цвете и скорости перемещения;
2. Создать массив звезд, расположенных по всему экрану и движущихся с разной скоростью;
3. Сделать так, чтобы звезды двигались из центра экрана к краю;
4. Сделать так, чтобы за каждый <?тик» таймера звезда немного ускорялась и становилась ярче;
5. Сделать так, чтобы, достигнув границы экрана, звезда появлялась вновь в иной случайной позиции и перемещалась с другой скоростью;
6. Нарисовать звезды;
7. Вновь вернуться к пункту 4.
Этот метод позволяет получить очень натурально выглядящие звезды и достаточно просто осуществляется. Однако и он тоже не лишен недостатков:
§ Во-первых, вы не можете повернуть и должны всегда двигаться вперед;
§ Во-вторых, начальная скорость звезды может быть слишком велика по отношению к скорости объекта, летящего по звездному небу.
Первая проблема действительно серьезная. .Если по сценарию игры вам требуется изменять направление полета, то надо воспользоваться каким-нибудь другим алгоритмом. Вторая же проблема, по моему мнению, чисто академическая. Кого волнует, что наш корабль летит со скоростью, во много раз превышающей световую? Это игра, и мы вольны строить такую вселенную, какая нам больше нравится!
Редактор поля WarEdit
Мы говорили о некоторых доступных инструментальных средствах, которые необходимы программистам игр и о том, как они используются. Теперь я хочу рассказать о тех инструментах, которые помогут в создании нашей собственной игры. Мы будем делать трехмерную игру под названием Warlock. Она будет очень проста, и я до сих пор даже не уверен в том, что в ней будут хоть какие-нибудь противники. Тем не менее, я собираюсь изготовить инструмент который позволит нам легко создавать уровни для игры.
Игровое пространство Warlock представляет собой трехмерный мир, с набором кубов, вертикальные грани которых разукрашены определенной фактурой. Общая площадь нашего мира приблизительно составляет 200х200 сторон кубов как показано па рисунке 15.2. Мы могли бы представить это пространство виде текстового файла и набить 40000 символов. Однако я не думаю, что сам , был бы этому рад. Вместо этого я решил создать простой редактор поля, который позволяет рисовать план игрового пространства, рассматриваемого сверху.
Редактор поля позволит нам легко создавать новые уровни и новые обстановки», в которых будут «жить» наши игровые объекты. Вы можете использовать мышь, чтобы рисовать поверхность, которая будет представлять собой полную игровую среду. Цвет, которым вы рисуете изображение, различает структуры и объекты, находящиеся в игровом пространстве. Существуют несколько управляющих функций редактирования поля. Коротко остановимся на их операциях.
Я решил представить игровое пространство, используя технику базовых элементов. Следовательно, все что мне нужно сделать, это создать двухмерную матрицу, представляющую собой игровое пространство, и заполнить ее целыми бедами, которые будут обозначать типы объектов, находящихся в соответствующих позициях. Объектами могут быть стены, «пища», «лекарство», монстры, «свитки» или двери.
Теперь, если стена или дверь помещены в определенную ячейку матрицы, они заполняют квадрат полностью. Однако есть и другие объекты, такие, наример, как «пища» или «свиток», которые имеют гораздо меньшие размеры, чем стена (которая в действительности представляет собой шестигранный куб; верх и низ его никогда не видны).
Помещая один из таких объектов на карту игрового поля, мы можем разместить его только где-то внутри квадрата. Более точно определить его положение мы не в состоянии. Когда программа встречает в базе данных такой объект, она случайным образом помещает его внутрь квадрата. Лучшим решением было бы создать редактор, который имеет более передовую структуру данных, с тем, чтобы мелкие объекты могли быть размещены более аккуратно. Однако того, что редактор делает сейчас, для наших целей вполне достаточно.
Вы можете создавать уровни, рисуя стены, двери и т. п. различными цветами, выбирая объект путем изменения цвета. Я применил цвета для пред. ставления объектов потому, что это самый легкий путь моделирования, хотя использование уменьшенных образов (иконок) объектов был бы, конечно намного предпочтительнее. Но при таком подходе сразу же возникает проблема которая заключается в том, что для представления матрицы размером 200х200 иконок понадобилось бы поле, во много раз превосходящее размеры экрана и для работы с ним потребовалось бы позаботиться о механизме прокрутки. Я не хотел моделировать все типы инструментов и смирился с цветами, как средством представления стен, дверей и всех прочих объектов. Тем не менее, чтобы видеть какой элемент игрового пространства вы устанавливаете, при выборе цвета соответствующий объект помещается в окно предварительного просмотра.
Признаюсь, что программа практически сырая. Я писал ее три дня. Изготовление настоящего редактора изображений с полным набором функций могло бы отнять месяцы кропотливого труда.
Редактор ID для создания игры DOOM отнял шесть месяцев на разработку и отладку. Но хотя изготовление хорошего инструмента и расход времени на него являются критическими величинами, WarEdit будет удовлетворять нашим потребностям. Он даст нам возможность рисовать уровни, размещать объекты в игровом пространстве и сохранять результат на диске.
В качестве последней особенности, WarEdit имеет окно детализации изображения, которое в увеличенном масштабе показывает область вокруг курсора.Это помогает размещению дверей и созданию тонких деталей.
Редакторы изображений
Графика и образы сегодняшних видеоигр должны быть максимально выразительными. Они должны быть объемными и выглядеть реалистично. Конечно, нет такого инструмента, который возмещает недостаток художественных способностей, а без настоящего художника ваша графика может выглядеть похожей на рисунки импрессионистов!
Можете использовать любую программу, какую пожелаете, но здесь приведен список тех минимальных средств, которыми она должна обладать:
§
Желательно, чтобы программа могла работать в любом графическом режиме, и обязательно — в режиме 13h (то есть 320х200х256);
§ Желательно, чтобы программа понимала различные форматы файлов как при чтении, так и при записи;
§ Интерфейс должен быть удобным, позволяющим рисовать образы в увеличенном масштабе (ZOOM), если в этом возникнет необходимость;
§ Цвет является важной деталью. Вам нужно иметь полный набор операций, управляющих цветовой палитрой;
§ В программе должна иметься возможность вывода изображений на принтер. Неплохо, если программа будет поддерживать цветные лазерные принтеры;
§ Редактор должен уметь выполнять различные геометрические преобразования изображений, такие как вращение, масштабирование, вырезание и растяжка;
§ Желательно, чтобы программа поддерживала возможность борьбы с неровностями контуров (сглаживание краев) и имела бы как можно больше цветовых эффектов;
Наконец, желательно, чтобы программа имела составные страницы, и чтобы, художник мог их отрезать и склеивать, а в дальнейшем получать окончательное изображение, оперируя изготовленными ранее графическими шаблонами.
Сегодня наблюдается тенденция постепенного вытеснения битовых графических редакторов иллюстративными, которые в основном оперируют объектами и векторным представлением образов. Независимо от ваших пристрастий, поиск удовлетворительного инструментария может отнять некоторое время, прежде чем вы найдете хорошую надежную программу. Я могу посоветовать вам приобрести такие продукты как Electronic Art's Deluxe Paint или Deluxe Animation, но, в крайнем случае, подойдет PC Paintbrush.
Регистры процессора
Регистры общего назначения. Данные регистры используются во время выполнения программ и во многих случая являются взаимозаменяемыми. Кроме того, каждый из них имеет определенное предназначение.
АХ - 16 бит, общего назначения, часто именуется аккумулятором;
ВХ - 16 бит, общего назначения и индексный;
СХ - 16 бит, общего назначения и счетчик;
DX - 16 бит, общего назначения;
ВР - 16 бит, общего назначения, используется для хранения смещения и индексов, часто называется регистром базы;
SI - 16 бит, общего назначения, используется в операциях с памятью (SI — source issue — регистр источника, используется для хранения смещения Источника при выполнении строковых команд);
DI - 16 бит, общего назначения, используется в операциях с памятью (DI - destination issue — регистр приемника, используется для хранения смещения пункта назначения при выполнении строковых команд).
Сегментные регистры. Данные регистры используются как указатели на сегменты. Сегмент - это блок размером в 64К, который предназначен для определенных целей: для хранения программного кода, данных и т. д.
DS - сегмент данных;
CS - сегмент кода;
ES - дополнительный сегмент;
SS - сегмент стека;
IP - счетчик.
Рисование линий
Нас совершенно не интересует написание функций для рисования линий с применением сложностей типа алгоритма Брезинхема. Более того, в наших играх вообще не будет линий, проходящих под произвольным углом. Напротив, у нас будет множество вертикальных линий и, может быть, немного горизонтальных. На самом деле, большинство игр вообще обходится без рисования линий, так как в них используются битовые массивы.
Мы напишем две функции. Одна из них рисует горизонтальные линии слева направо, а другая — вертикальные сверху вниз. Рисунок 5.4 показывает, как они выглядят в видеобуфере.
Поскольку горизонтальные линии рисовать легче, то с них мы и начнем. Как видно из рисунка 5.4, горизонтальную линию можно получить, заполняя ряд пикселей в матрице 320х200. Чтобы это проделать, мы должны найти начальный адрес строки и заполнить ее значениями пикселей от начальной до конечной позиции. Для этого стоит использовать функцию rnemset. Это один из самых быстрых способов. Листинг 5,6 содержит код такой функции.
Листинг 5.6. Рисование горизонтальной линии.
void H_Line (int х1, int x2, int y, unsigned int color)
{
// функция рисует горизонтальную линию, используя memset()
// x2 должно быть больше х1
_fmemset ((char far *) (video_buffer + ((у << 8) + (у
<<6)) + х1), color, x2 - х1 + 1);
} // конец функции
Следует кое-что запомнить:
§
Мы используем функцию _fmemset, поскольку она, в отличие от memset, корректно работает с дальними (FAR) указателями;
§ Мы вычисляем начальный адрес линии, и функция заполняет определенное количество байтов заданным значением цвета;
§ Количество байтов равно длине строки, которую мы вычисляем как разность между правым и левым концами линии. При этом нужно прибавить единицу, чтобы не потерять последнюю точку.
К сожалению, мы не можем использовать семейство функций memset для рисования вертикальных линий, поскольку они работают только с непрерывными областями памяти.
В случае вертикальных линий каждый следующий пиксель отстоит на 320 байт от предыдущего. Если вы находитесь в какой-то точке и хотите рисовать линию вниз, то вам надо прибавлять 320, а если линия рисует вверх, то надо вычитать 320 из текущего адреса. Таким образом, мы можем создать цикл, который увеличивает адрес на 320 и рисует вертикальную линию в виде пикселей сверху вниз. Листинг 5.7 демонстрирует код этой функции.
Листинг 5.7. Рисование вертикальной линии.
void V_Line(int yl,int y2,int x,unsigned int color)
{
//рисуем вертикальную линию (у2 больше yl)
unsigned int line offset,
index;
// вычисляем начальную позицию
line_offset = ((y1<<8) + (y1<<6)) + x;
for (index=0; index<=y2-y1; index++)
{
video_buffer[line_offset] = color;
line_offset+=320;
// переходим к следующей линии
} // конец цикла for
} // конец функции
Функция V Line несколько длиннее Н Line, поскольку она сама производит все адресные вычисления. Эта функция фантастически эффективна - вспомните пример использования сдвига вместо умножения (кстати, если вы до сих пор не поняли смысл двоичного сдвига, не отчаивайтесь - в восемнадцатой главе, «Техника оптимизации», мы это подробно изучим).
Прежде чем перейти к следующей теме, я хочу дать вам замечательную программу, которая создает новую палитру и выводит ее на экран, используя функции рисования вертикальных линий. В ней встречается уже известная функция Set_Mode() , описанная во второй главе, поэтому здесь я не включил ее исходный код. При желании вы можете взять его из второй главы. Более того, эта функция объявлена в программе как EXTERNAL, так что ее можно просто прилинковать. Листинг 5.8 содержит необходимый код программы Show_Palette.
Примечание
Я обнаружил некоторую проблему с чтением регистра палитры на VGA-картах. Похоже, что вы не всегда можете получить доступ к требуемому вам регистру. Это ошибка «железа», и она характерна для отдельных VGA-карт.
Я решил эту проблему довольно примитивным способом: просто дважды читаю каждый регистр. Вроде, мне это помогло. В своих программах вы можете попробовать включать дополнительные проверки и использовать разные функции для чтения регистров палитры в случае обнаружения ошибки. Во всяком случае, время, потраченное на решение данной задачи с лихвой окупится в процессе выполнения программы.
Листинг 5.8. Создание и отображение цветовой палитры (PALDEMO.C).
// ВКЛЮЧАЕМЫЕ ФАЙЛЫ ///////////////////////////////////////////////////////////////////////
#include <io.h>
#include <conio.h>
#include <stdio.h>
#include <stdlib.h>
#include <dos.h>
#include <bios.h>
#include <fcntl.h>
#include <memory.h>
#include <math.h>
#include <string.h>
// определения
/////////////////////////////////////
#define ROM_CHARSET_SEG 0xF000
#define ROM_CHAR_SET_OFF 0xFA6E
#define VGA256 0x13
#define TEXT_MODE 0х03
#define PALETTE_MASK ОхЗc6
#define PALETTE_REGISTER_RD.Ox3c7 #define PALETTE_REGISTER_WR 0x3c8
#define PALETTE_DATA 0x3c9
#define SCREEN_WIDTH (unsigned int)320
#define SCREEN_HEIGHT (unsigned int)200
// структуры данных////////////////////////////////////////
// структура, сохраняющая RGB
typedef struct RGB_color_typ
{
unsigned char red; // красный
компонент
0-63
unsigned char green; // зеленый
компонент
0-63
unsigned char blue; // синий
компонент
0-63
} RGB_Color, *RGB_color_ptr;
// ВНЕШНИЕ ФУНКЦИИ //////////////////////////////////
extern Set_Mode(int mode);
// ПРОТОТИПЫ //////////////////////////////////////////
void Set_Palette Register(int index, RGB_color_ptr color);
void Get_Palette_Register(int index, RGB_color_ptr color);
void Create_Cool__Palette();
void V_Line(int y1,int y2,int x,unsigned int color);
// ГЛОБДЛЬНЫЕ ПЕРЕМЕННЫЕ ////////////////////////////
// указатель на начало видеопамяти (для операций с байтами)
unsigned char far *video_buffer = (char far *)0xA0000000L;
// указатель на начало видеопамяти (для операций со словами)
unsigned int far *video_buffer_w= (int far *)0xA0000000L;
// ФУНКЦИИ //////////////////////////////////////////
void Set_Palette_Register (int index, RGB_color_ptr color)
{
// Эта функция устанавливает значение одного элемента таблицы
// цветов. Номер регистра задается переменной index, структура
// color содержит значения красной, зеленой и синей составляющих
// цвета
// указываем VGA карте, что мы будем обновлять содержимое
// регистра палитры
_outp(PALETTE_MASK,Oxff);
// какой из регистров мы хотим обновить?
_outp(PALETTE_REGISTER_WR, index) ;
// теперь обновляем RGB
_outp(PALETTE_DATA,color->red);
_outp(PALETTE_DATA,color->green);
_outp(PALETTE_DATA,color->blue);
} // конец функции
/////////////////////////////////////////////////////
void Get_Palette_Register(int index, RGB_color_ptr color)
{
// эта функция читает данные элемента таблицы цветов и помещает их
// в поля структуры color
// установить маску регистра палитры
_outp(PALETTE_MASK,Oxff);
// сообщаем VGA, какой из регистров мы будем читать
_outp(PALETTE_REGISTER_RD, index);
// читаем
данные
color->red = _inp(PALETTE_DATA);
color->green = _inp(PALETTE_DATA);
color->blue = _inp(PALETTE_DATA);
} // конец
функции /////////////////////////////////////////////////
void Create_Cool_Palette(void) {
// эта функция создает палитру, содержащую по 64 оттенка серого,
// красного, зеленого и синего цветов
RGB_color color;
int index;
// проходим по элементам таблицы цветов и создаем 4 банка
// по 64 элемента
for (index=0; index < 64; index++)
{
// оттенки
серого
color.red = index;
color.green = index;
color.blue = index;
Set_Palette_Register(index, (RGB_color_ptr)&color);
// оттенки красного
color.red = index;
color.green = 0;
color.blue = 0;
Set_Palette_Register(index+64, (RGB_color_ptr)&color) ;
// оттенки зеленого
color.red = 0;
color.green = index;
color.blue = 0;
Set_Palette_Register(index+128, (RGB_color_ptr)&color);
// оттенки синего
color.red = 0;
color.green = 0;
color.blue = index;
Set_Palette_Register(index+192, (RGB_color_ptr)&color);
} // конец цикла for
} // конец функции
///////////////////////////////////////////////////////
void V_Line(int y1,int y2,int x,unsigned int color)
{
// рисуем вертикальную линию у2 > yl
unsigned int line_offset, index;
// вычисляем начальную позицию
line_offset
= ((y1<<8) + (y1<<6)} + x;
for (index=0; index<=y2-y1; index++)
{
video_buffer[line_offset] = color;
line_offset+==320; // переходим к следующей линии
} // конец цикла for
} // конец функции
// ОСНОВНАЯ ПРОГРАММА /////////////////////////////////
void main(void)
{
int index;
RGB_color color,color_1;
// установить режим 320х200х256
Set_Mode(VGA256) ;
// создать палитру цветов
Create_Cool_Palette();
// рисуем по одной вертикальной линии для каждого цвета
for (index=0; index<320; index++) V_Line(0,199,index,index);
// ждем реакции пользователя
while(!kbhit())
{
Get_Palette_Register(0,(RGB_color_ptr)&color 1) ;
Get_Palette_Register(0,(RGB_color_ptr)&color_l);
for (index=0; index<=254; index++)
{
Get_Palette_Register(index+l,(RGB_color_ptr)&color);
Get_Palette__Register(index+l, (RGB_color_ptr)&color) ;
Set Palette Register(index,(RGB color_ptr)&color) ;
} // конец
цикла for
Set_Palette_Register(255,(RGB_color_ptr)&color_1);
} // конец цикла while
// переходим обратно в текстовый режим
Set_Mode(TEXT_MODE);
} // конец функции
Программа из Листинга 5.8 создает новую палитру, которая содержит 64 оттенка всех основных цветов, включая серый. Затем, она разделяет каждый цвет вертикальными линиями и после этого перемешивает их.
Вроде, хватит об этом. Теперь стоит поговорить о том, как целиком прочитать файл с образом. Начнем с формата PCX-файлов.
Рисование лучей
Очевидно, что лучи, которые мы отсекаем, на самом деле представляют собой линии. Они начинаются в точке зрения игрока, совпадающей с его позицией на двухмерной карте. Мы решили иметь поле просмотра равным 60°. Таким образом, нам нужно составить таблицу соответствия для всех возможных лучей, которые можно отсечь с любого угла просмотра. Эта таблица должна содержать значения наклонов всех возможных лучей по отношению к плану просмотра. Исходя из значения наклона, мы сможем произвести отсечение луча из точки наблюдения игрока.
Формула 6.1. Подсчет количества элементов в таблице значений наклонов.
Перед нами стоит вопрос — сколько элементов должно быть в таблице наклонов и как эти наклоны рассчитать? Для составления таблицы требуется знать, сколько в ней будет элементов. Когда игрок смотрит на мир, построенный отсечением лучей, то 320 лучей (количество горизонтальных точек экрана) вместе составят дугу в 60°. Таким образом, мы должны иметь таблицу с 1920 элементами или значениями наклонов. Это вычисляется по следующей формуле:
размер таблицы = ширина_экрана х 360 / поле_просмотра
В нашем случае ширина экрана составляет 320 пикселей, а поле_просмотра - 60 градусов, поэтому результат будет равен 320х360/60 = 1920.
Рисование точки
Однажды я сказал: «Дайте мне адрес видеобуфера и я переверну экран. ..». Это весьма правдивое высказывание. Во всех системах с отображением адресов видеопамяти на область адресов памяти обычной, как это делается в ПК; рендеринг был бы более простой и доступной вещью, если бы видеобуфер имел хоть каплю логики в своей организации. Собственно, организация и логика есть: видеобуфер - это один большой массив. И все.
Как мы узнали чуть раньше, для рисования точки нам достаточно вычислить адрес смещения относительно начала видеобуфера (А000:0000) и записать байт, отображающий ее цвет. Больше ничего делать не надо. Листинг 5.4 содержит фрагмент кода, который рисует точку определенного цвета с координатами Х и Y.
Листинг 5.4. Рисование точки в позиции (х,у).
void Plot_Pixel(int x,
int у, unsigned char color)
{
// эта функция отображает точку выбранного цвета. Каждая строка
// занимает 320 байт, поэтому для вычисления адреса надо умножить Y
// на 320 и прибавить значение Х
video_buffer[y*320+x]=color;
} // конец функции
Итак, рисование точки довольно просто. Я думал, все окажется сложнее, и поэтому так много написал об этом, но то, что мы использовали режим 13h, значительно упростило дело. Функция Plot_Pixel получилась простой, это всего одна строка кода. Тем не менее, давайте попробуем ее оптимизировать.
В книге есть целая глава, посвященная оптимизации, но эта единственная строка, содержащая операцию умножения, меня сводит с ума. Давайте избавимся от умножения. Кстати, возьмем за правило избегать операций умножения и вообще, откажемся от действий с плавающей запятой. Итак, посмотрим, что мы можем сделать с вычислением у х 320?
Вспомним, что мы используем двоичную арифметику и все числа в ПК также представлены в двоичном виде. Если вы берете двоичное число и сдвигаете его влево или вправо, это аналогично его умножению или делению на два.
Рисунок 5.3 поясняет это.
Поскольку операция сдвига выполняется примерно в 2-10 раз быстрее, чем умножение, то мы получим быстрые функции для рисования. Единственная сложность состоит в том, что число 320 — это не степень двух, но чтобы выйти из положения, мы применим маленькую хитрость. Представим выражение 320 х у, как 256 х, у + 64 х у. Листинг 5.5 показывает код для быстрого рисования точки.
Листинг 5.5. Программа быстрого рисования точки.
void Plot_Pixel_Fast ( int x, int y, unsigned char color )
{
// эта функция рисует точку несколько быстрее за счет замены
// операции умножения на сдвиг
// учитываем, что 320*у=256*у+64*у=у<<8+у<<б
video_buffer[((у<<8) +(у<<6)) + х ] = color;
} // конец функции
Эта функция работает примерно в два раза быстрее — вот что значит оптимизация. Позже мы научимся оптимизировать программы так, что парни из Microsoft перестанут нам верить.
Теперь, когда у нас есть функция рисования точки, надо дополнить нашу графическую библиотеку функцией рисования линии.
СЕКРЕТЫ VGA-КАРТ
Когда мы пишем видеоигры, то используем компьютер для создания миров и VGA-карту для рисования разнообразных объектов. В этом случае VGA-карта является «связующей нитью» между компьютером и экраном дисплея. В данной главе мы поговорим о следующем:
§
Обзор VGA-карт;
§ 256-цветный режим;
§ Конфигурация видеопамяти;
§ Таблицы цветов;
§ Переопределение цветовой палитры;
§ Цветовое вращение;
§ Рисование точек;
§ Рисование линий;
§ Файлы PCX;
§ Вывод битовых образов (бит-блиттинг);
§ Спрайты;
§ Вывод текста
§ Дублирующее буферизирование;
§ Синхронизация с вертикальной разверткой;
§ Игра Tombstone
Шаблонные мысли
На заре создания видеоигр программисты сталкивались с серьезными проблемами, пробуя нанести точки на экран. Они радовались по поводу удачной имитации тактики боя и написания набора алгоритмов, управляющих существами в их мире.
Увы, они думали, что могут имитировать разумное поведение с помощью шаблонов, то есть описывая рядом цифр траектории перемещения созданий. К примеру, в игре Galaxian маленькие космические корабли делают несколько кругов, стреляют в вас некоторое время, а затем возвращаются на прежнее место в стаю своих сородичей. Посмотрим на рисунок 13.1.
Когда они выполняют эти движения, то всего лишь следуют командам, заранее написанных шаблонов. Конструирование шаблонов - чрезвычайно легкое занятие и они нашли использование во многих сценариях. Как пример, используем шаблоны и случайные числа для моделирования «разума», который можно использовать, например, для управления небольшим космическим кораблем. Он пытается уклониться от вас, и в то же время пробует прицельно стрелять. То, что можно для этого сделать, показано в Алгоритме 13.3.
Алгоритм 13.3. Шаблоны со случайным выбором.
// Предположим, что pattern
- это массив, содержащий набор команд
// для реализации десяти различных шаблонов поведения
while(идет игра)
{
...код программы
// Проверяем, закончена ли обработка текущего шаблона
if (если обработка команд текущего шаблона закончена)
{
// Выбираем
новый шаблон
current_pattern = pattern[rand()%10];
позиция противника = старая позиция +следующий элемент текущего шаблона
Увеличиваем на единицу значение индекса элементов шаблона
...код программы
}
Алгоритм 13.3 кажется сложнее предыдущих Алгоритмов Преследования и Уклонения, но на самом деле это не так. В сущности:
§
Случайным образом выбирается некоторый шаблон;
§ Движение созданий в каждом проходе цикла изменяется в соответствии с направлением, указанным в шаблоне;
§ Затем мы переходим к следующему элементу шаблона.
Каждый шаблон может включать в себя произвольное количество элементов. Некоторые из них имеют 10 элементов, а иные — 1000.
Важно одно - когда «создание» исчерпывает набор команд, задаваемый одним шаблоном - оно переходит к другому.
Наконец, мы могли бы связать выбор случайного номера шаблона с действиями на основе некой другой логики - например, описываемой Алгоритмом 13.1.
Добавив к Алгоритму 13.3 последний шаг, мы придали ему некоторый аспект, который придал ему большую комплексность. Появилось подобие мыслительного процесса, состоящего из двух стадий. Случайное число подастся на вход селектора шаблонов, который затем выбирает новый шаблон для игры.
По аналогии с нервной системой, случайное число можно представить как нервный импульс, а выбранный шаблон действий - как реакцию на него. Посмотрите на рисунок 13.2, чтобы увидеть тот процесс, о котором я говорю.
Для усовершенствования тактики можно добавить измерение расстояния до игрока: если существо находится за пределами некоторого радиуса, используется Алгоритм Преследования, однако, когда оно оказывается достаточно близко, то начинает перемещаться по шаблонным траекториям, выбирая их случайным образом. Эта идея применена в Алгоритме 13.4.
Алгоритм 13.4. Преследование и Танец.
while(идет игра)
{
...код программы
if (игрок вне круга с радиусом 50 точек) then
преследуем его
else
выбираем случайный шаблон и реализуем его
...код программы
}
Для демонстрации использования шаблонов я переработал программу из Листинга 13.1 и привел ее в соответствие с Алгоритмом 13.4. Новая программа показана в Листинге 13.2. Когда враг приближается к игроку, он выбирает один из трех шаблонов и выполняет его до завершения. Далее, в зависимости от расстояния до игрока, противник либо преследует игрока, либо выбирает другой шаблон.
Листинг 13.2. Муха (FLY.C).
// ВКЛЮЧАЕМЫЕ ФАЙЛЫ //////////////////////////////////////////
#include <stdio.h>
#include <graph.h>
#include <math.h>
// ГЛОБАЛЬНЫЕ ПЕРЕМЕННЫЕ ////////////////////////////////////////
//указатель на системную переменную, содержащую
//значение таймера. Содержимое этой 32-битовой ячейки
//обновляется 18.2 раз в секунду
usigned int far *clock=(unsigned int far *)0x0000046C;
// Х- и Y- компоненты шаблонов траекторий, по которым будет
// двигаться "муха"
int patterns_x[3] [20]={1,1,1,1,1,2,2,-1,-2,-3,-1, 0,0,1,2,2,-2,-2,-1,0, 0,0,1,2,3,4,5,4,3,2,1,3,3,3,3, 2,1,-2,-2,-1, 0,-l,-2,-3,-3,-2,-2, 0,0,0,0,0,0,1,0,0,0,1,0,1};
int patterns_y[3] [20]={0,0,0,0,-1,-1,-1,-1,-1, 0,0,0,0,0,2,2,2,2,2,2, 1,1,1,1,1,1,2,2,2,2,2, 3,3,3,3,3,0,0,0,0, 1,1,1,2,2,-1,-1,-1,-2,-2, -1,-1,0,0,0,1,1,1,1,1};
/////////////////////////////////////////////
void Timer(int clicks)
{
// Эта функция использует значение таймера для формирования
// задержки. Необходимое время задержки задается в "тиках"
// интервалах в 1/18.2 сек. Переменная, содержащая 32-битовое
// текущее значение системного таймера, расположена
// по адресу 0000:0:46Ch
unsigned int now;
// получить текущее время
now = *clock;
// Ничего не делать до тех пор, пока значение таймера не
// увеличится на требуемое количество "тиков".
// Примечание: один "тик" соответствует примерно 55мс.
while(abs(*clock - now) < clicks) {}
} // конец функции Timer
// ОСНОВНАЯ ФУНКЦИЯ //////////////////////////////////////
void main(void)
{
int px=160,py=100, // начальные координаты игрока
ex=0,ey=0; // начальные координаты противника
int done=0, // флаг окончания работы программы
doing_pattern=0, // флаг выполнения команд шаблона
current_pattern, // номер выполняемого шаблона,
// принимает значение в интервале 0-2
pattern_element; <// номер выполняемой команды шаблона
_setvideomode(_MRES256COLOR,) ;
printf(" The Fly - Q to Quit");
// главный игровой цикл
while(!done)
{
// очищаем точки
_setcolor(0) ;
_setpixel(px,py);
_setpixel(ex,ey) ;
// Перемещение игрока
if (kbhit())
{
// Определяем направление движения
switch(getch())
{
case 'u': // Вверх
{
py—2;.
} break;
case 'n': // Вниз
{
py+=2;
} break;
case 'j': // Вправо
{
px+=2 ;
} break;
case 'h': // Влево
{ px-=2 ;
} break;
case 'q':
{
done=1;
} break;
} // конец
оператора
switch
} // конец обработки нажатия клавиши
// Теперь перемещаем противника
// Начинается работа "мозга"
if (!doing_pattern)
{
if(px>ex) ex++;
if(px<ex) ex--;
if(py>ey) ey++;
if(py<ey} ey--;
// Теперь проверяем, не надо ли начать выполнять
// шаблон. Он начинает выполняться, если игрок
// оказывается в радиусе 50 точек от противника.
if (sqrt(.1+(рх-ех)*(рх-ех)+(ру-еу)*(ру-еу))<15)
{
// Даже не думайте использовать функцию sqrt в
// настоящей игре!
// Получаем случайный номер шаблона
curent_pattern = rand()%3;
// Переводим "мозг" в режим действий по шаблону
doing_pattern = 1;
pattern_element=0;
} // конец проверки на попадание игрока
// в "радиус действия"
} // конец действий, для случая, когда шаблон не выполнялся
else {
// перемещаем противника, используя следующий
// элемент
текущего шаблона
ex+=patterns_x[current_pattern][pattern_element];
ey+=patterns_y[current_pattern] [pattern_element];
// мы закончили обработку шаблона?
if (++pattern_element==20)
{
pattern_element = 0;
doing_pattern = 0;
} // конец проверки на окончание шаблона
} // конец оператора else
//конец работы, "мозга"
// рисуем точки
_setcolor(9);
_setpixel(px,py);
_setcolor(12);
_setpixel(ex,ey) ; // Немного
подождем...
Timer(1);
} // конец
цикла while
// восстановление начального видеорежима
_setvideomode(_DEFAULTMODE) ;
}// конец функции main
Когда вы запустите программу из Листинга 13.2, то поймете, почему я назвал ее "Муха". Точка, бегающая по экрану и в самом деле напоминает муху. Она приближается к вам и вдруг начинает быстро летать вокруг. И такое поведение персонажа воплощено всего в нескольких строках программы с применением описанных выше алгоритмов. (Гм, так как вы думаете, может быть люди - это и в самом деле комплекс действий и реакций?) Теперь рассмотрим случайные передвижения.
Синхронизация состояния ввода/вывода
Синхронизация состояния ввода/вывода является методом, при котором статус устройств ввода данных передается на другой ПК в реальном времени. Все, что игрок делает на одном компьютере, принимающая система воспринимает как входные данные, которые использует для корректировки в своем игрового пространстве поведения образа отдаленного игрока. Рисунок 14.8 пояснее сказанное.
Этот метод четко работает до тех пор, пока обе системы остаются синхронизированными и не происходит никаких случайных изменений игровой ситуации. Если же подобное произойдет, то другая машина не сможет «узнать» об этом, потому как данный способ не предназначен для передачи такого рода информации.
Если вам все же потребуется, чтобы происходили какие-то случайные события, вы должны воспользоваться первым методом, чтобы сообщить об изменениях другой машине. В дальнейшем мы объединим оба способа синхронизации вместе - это совершенно неизбежно.
Для синхронизации состояния ввода/вывода необходимо;
§
Опросить текущее состояние устройств ввода данных, будь то джойстик или клавиатура;
§ Объединить их вместе в пакет и послать через коммуникационный канал.
Термин пакет подразумевает объединение разносортной информации. Поэтому для пересылки пакетов мы должны принять ряд соглашений, чтобы последовательные коммуникационные системы «знали», что означает та или иная часть информации. Скажем, мы решили передать через коммуникационный канал положение ручки джойстика одновременно с состоянием его кнопок.
Формат пакета для передачи этих данных мог бы выглядеть примерно так, как это Показано в таблице 14.5.
Таблица 14.5. Образец пакета информационного пространства ввода/вывода.
№ байта Обозначение Смысл
0 J Установленное состояние джойстика
1 data_x Байт Х-координаты джойстика
2 data_y Байт Y-координаты джойстика
3 buttons Байт состояния кнопок
4 (period) Конец передачи
Составить такой пакет чрезвычайно просто. Не сложно и заставить программу передавать его на другую машину с частотой, необходимой для сохранения синхронизации. В то же самое время, другая машина могла бы предоставить в распоряжение первой собственный пакет состояния джойстика.
Здесь есть один не вполне очевидный нюанс. Дело в том, что оба компьютера в равной мере думают друг о друге как об удаленном игроке. (Есть в этом что-то от фантастических романов, описывающих путешествия в параллельные миры и во времени, - частенько путаешься и начинает болеть голова.)
Это все, что я хотел сказать по поводу синхронизации состояния ввода/вывода. Мы еще продолжим разговор о сохранении режима синхронизации, но сейчас нам нужно обсудить такую малоизученную область, как временная синхронизация.
Синхронизация вектора состояния
Реализовать синхронизацию вектора состояния несложно. Для этого достаточно непрерывно передавать другой машине данные о состоянии игрового пространства и принимать ответную информацию, чтобы скорректировать обстановку.
Давайте в качестве примера рассмотрим некоторую игру, в которой двое участников на разных компьютерах ведут дуэль с астероидами. Чтобы передать состояние одной машины на другую, мы должны учесть и местоположение, и скорость, и размер каждого астероида, а также не забыть передать и координаты самого игрока. Если игрок открыл огонь, мы должны передать соответствующее сообщение и об этом, а также описать атрибуты оружия. Кроме того, если в игровом пространстве на одной из машин появился новый объект, мы должны сообщить об этом другому компьютеру, чтобы и он создал аналогичный объект.
Таким образом, мы как бы делаем фотографии игрового пространства и постоянно передаем их на другую машину. Это нужно делать в разумном темпе, так, чтобы избежать всевозможных несогласованных ситуаций. Например, может же случиться так, что один игрок взрывает астероид в то время, как другой на него только нацелился. Как вы понимаете, в этом случае нужно удалить астероид из игрового пространства прежде, чем снаряд второго игрока достигнет уже не существующей цели. На рисунке 14.7 с некоторым преувелинием показано, что может произойти,. если система выйдет из состояния синхронизации.
Синхронизация вектора состояния работает прекрасно и совершенно надежно, потому что абсолютно все происходящее на одной машине передается на другую. Однако, как я подозреваю, это не так легко реализовать: ведь во внимание принимаются все возможные характеристики состояния игры, и в результате итоговая информация, передаваемая через коммуникационный канал, оказывается достаточно объемистой.
Следующий метод, о котором мы сейчас поговорим, более легок для понимания и называется синхронизацией состояния ввода/вывода.
Скан-коды
Давайте теперь поговорим о такой вещи как скан-коды. Если вы считаете, что при нажатии клавиши А обработчик клавиатуры также получает код символа А, то вы ошибаетесь. К сожалению, это не так. Обработчику посылается скан-код. Более того, он посылается дважды — при нажатии и отпускании клавиши. В видеоиграх нас будут интересовать не столько ASCII-коды, сколько нажатия клавиш A, S, Пробел, которые обычно отвечают за маневры, стрельбу и т. д. Таким образом, нам надо знать, как получить именно скан-коды. И это все, что требуется. В таблице 3.2 перечислены скан-коды клавиш.
Таблица 3.2. Таблица скан-кодов.
Клавиша | Скан-код | Клавиша | Скан-код | Клавиша | Скан-код | Клавиша | Скан-код | ||||||||
Esc | 1 | I | 22 | Z | 43 | F7 | 64 | ||||||||
1 | 2 | O | 23 | X | 44 | F8 | 65 | ||||||||
2 | 3 | P | 24 | C | 45 | F9 | 66 | ||||||||
4 | 4 | [ | 25 | V | 46 | F10 | 67 | ||||||||
5 | 5 | ] | 26 | B | 47 | F11 | 133 | ||||||||
6 | 6 | Enter | 27 | N | 48 | F12 | 134 | ||||||||
7 | 7 | Ctrl | 28 | M | 49 | Num Lock | 69 | ||||||||
8 | 8 | A | 29 | Запятая | 50 | Scroll Lock | 70 | ||||||||
9 | 9 | S | 30 | Точка | 51 | Home | 71 | ||||||||
0 | 10 | D | 31 | / | 52 | Up | 72 | ||||||||
- | 11 | F | 32 | Правый Shift | 53 | PgUp | 73 | ||||||||
= | 12 | G | 33 | Print Screen | 54 | Серый - | 74 | ||||||||
Backspace | 13 | H | 34 | Alt | 55 | Left | 75 | ||||||||
Tab | 14 | J | 35 | Пробел | 56 | 5 на цифр. клав. | 76 | ||||||||
Q | 15 | K | 36 | Caps Lock | 57 | Right | 77 | ||||||||
W | 16 | L | 37 | F1 | 58 | Серый + | 78 | ||||||||
E | 17 | ; | 38 | F2 | 59 | End | 79 | ||||||||
R | 18 | Апостроф | 39 | F3 | 60 | Down | 80 | ||||||||
T | 19 | ~ | 40 | F4 | 61 | PgDn | 81 | ||||||||
Y | 20 | Левый Shift | 41 | F5 | 62 | Ins | 82 | ||||||||
U | 21 | \ | 42 | F6 | 63 | Del | 83 |
Если вы внимательно изучали таблицу, то должны, были заметить, что клавиши; имеющие двухсимвольную кодировку, обладают, тем не менее, только одним скан-кодом. Это происходит потому, что каждый скан-код может быть дополнен информацией о статусе клавиш. Кроме того, благодаря таблице 3.2, мы теперь сами можем по скан-коду определять код ASCII.
Случайные передвижения
Что я понимаю под случайным передвижением? Это выполнение персонажем какого-нибудь совершенно непредсказуемого действия. Нечто подобное мы уже сделали в программе из Листинга 13.2. Однако, мы можем расширить эту концепцию не только выбирая способ реагирования, но и определяя, нужна ли реакция как таковая. Почему бы нам не добавить еще один тип поведения нашей "Мухе" - случайное передвижение.
Случайные перемещения используются во многих видеоиграх как один из способов реагирования виртуального мира на действия игрока. Такой образ Действий можно назвать "неявной логикой". Неявная логика помогает принять Рвение при отсутствии части необходимой информации, или вообще информации как таковой.
В видеоиграх мы можем использовать случайные перемещения и неявную логику для выбора траекторий или действий наших созданий.
К примеру, когда вы сталкиваетесь с решением некоторого вопроса, такого как обход препятствия, возникшего на вашем пути, вы попытаетесь избежать встречи с ним, двигаясь направо или налево. Если все варианты равны между собой и одинаково возможны, решение, скорее, случайно и, следовательно является «неявным решением». Если бы кто-нибудь спросил вас, например почему вы решили обойти столб справа, а не слева, то вряд ли вы сумели бы внятно объяснить мотивы своего поведения. В другом случае мы можем использовать случайные переменные для выбора направления движения нашей маленькой «Мухи». Алгоритм 13.5 создает маленькую точку, которой теперь полагается летать, двигаясь в случайном направлении.
Алгоритм 13.5. Случайное передвижение.
while(идет игра)
{ ...код программы
if (перемещение точки по текущей траектории закончено) then
{
выбор новой траектории, иными словами, выбираем новый фактор преобразования координат
}
двигаем точку несколько раз по новой траектории
...код программы
}
Алгоритм 13.5 моделирует «грубый разум», который выбирает направление дальнейшего движения случайным образом. Напишем программу, которая создает одиноко летящую точку в пространстве с помощью этого алгоритма, Посмотрим, как это работает, на примере программы из Листинга 13.3.
Листинг 13.3. Одинокая муха. (DFLY.C).
// ВКЛЮЧАЕМЫЕ ФАЙЛЫ ////////////////////////////////////////
#include <stdio.h>
#include <graph.h>
#include <math.h>
// ГЛОБАЛЬНЫЕ ПЕРЕМЕННЫЕ ////////////////////////////////////////
// Указатель на системную переменную, содержащую
// значение таймера. Содержимое этой 32-битовой ячейки
// обновляется 18.2 раза в секунду
unsigned int far *clock=(unsigned int far *)0x0000046C;
//////////////////////////////////////////
void Timer(int clicks)
{
// эта функция использует значение таймера для формирования
// задержки. Необходимое время задержки задается в "тиках"
// интервалах в 1/18.2 сек. Переменная, содержащая 32-битовое
// текущее значение системного таймера расположена
// по
адресу 0000:046Ch
unsigned int now;
// получить текущее время
now = *clock;
// Ничего не делать до тех пор пока значение таймера не
// увеличится на требуемое количество "тиков".
// Примечание: один "тик" соответствует примерно 55 мсек.
while(abs(*clock - now) < clicks){}
} // конец функции Timer
// ОСНОВНАЯ ФУНКЦИЯ /////////////////////////////////////
void main(void)
{
int ex=160,ey=100; // начальная позиция "Мухи"
int curr_xv=1,curr_yv=0, // текущие факторы
// преобразования Координат.
clicks=0; // время, через которое
// "Муха" прекращает движение
//в произвольном направлении.
_setvideomode(_MRES256COLOR) ;
printf(" The Dumb Fly - Any Key to Quit");
//главный игровой цикл
while(!kbhit())
{
// очищаем точки на экране
_setcolor(0);
_setpixel(ex,ey); // перемещение "Мухи"
// "мозговой штурм"
// закончить ли движение в текущем направлении?
if (++clicks—20)
{
curr_xv = -5 + rand()%10; // от -5 до +5
curr_yv = -5 + rand()%10; // от -5 до +5
clicks=0;
} // конец задания нового направления
// перемещаем "Муху"
ex+=curr_xv;
ey+=curr_yv;
// убеждаемся, что "Муха" находится в пределах экрана
if (ex>319) ex=0; if (ех<0) ех=319;
if (ey>199) ey=0; if (еу<0) еу=199;
// конец "мозгового штурма"
// рисуем "Муху"
_setcolor(12);
_setpixel(ех,еу);
// небольшая задержка...
Timer(1);
} // конец
цикла while
_setvideomode(_DEFAULTMODE);
} // конец функции main
Думаю, после запуска программы вы согласитесь с тем, что мы, наконец получили все необходимое для моделирования летающего «разумного существа». Несколько простых правил, шаблоны и случайные числа помогли нам создать довольно правдоподобную имитацию полета насекомого. Это просто чудесно! Теперь настало время поговорить о конечных автоматах.
Смещение мозаичных изображений
Одной из проблем, возникающей при выводе повторяющегося смещаемого изображения, является большой объем памяти, требуемый для размещения битовых карт. Практически, размер никакого отдельного изображения в D0b не может превышать 64К. Этого достаточно для изображений площадью 320х200 или 640х100 пикселей (как я уже говорил, в режиме 13h каждый пиксель занимает один байт). Но даже если бы вам и представилась возможность иметь изображения больших размеров, вы все равно очень скоро исчерпали бы память, поскольку в вашем распоряжении имеется максимум 640К.
Не волнуйтесь: мы не будем здесь углубляться в изучение 16-битного кода, расширенной памяти или сегментации. Скажу только, что ограничения использования памяти неизменно сопутствуют старому доброму реальному режиму работы микропроцессора. Когда-нибудь это может измениться, но сейчас мы вынуждены примириться с таким положением вещей.
Существует довольно умное решение проблемы недостатка памяти — это применение мозаичных изображений. Вместо использования цельного образа, вы создаете виртуальную битовую карту, составленную из множества меньших картинок, как. из кирпичиков. Эти «кирпичики» могут быть рассмотрены как блоки здания, из которых составляется большее изображение. В основе этого технического приема лежит составление специальной справочной таблицы. Обычно она представляет собой массив, содержащий данные о расположении маленьких частей внутри большого виртуального изображения. Справочная таблица не требует много памяти (приблизительно один или два байта на элемент) и поэтому может иметь практически любую протяженность.
Представьте себе смещающееся виртуальное изображение, состоящее из 5400х12 «кирпичиков», каждый из которых имеет размер всего 16х16 пикселей. Это означает, что площадь виртуального изображения составит 86400х192 пикселя, что намного больше, максимальных допустимых размеров любого отдельного изображения.
Единственным ограничением этого метода является то обстоятельство, что размер цельного образа полностью зависит от размеров отдельных «кирпичиков».
Каждый из них должен быть небольшим и достаточно типовым, чтобы имелась возможность использования его в различных местах изображения. Тем не менее, «кирпичики» должны быть достаточно интересными, чтобы из них можно было сформировать привлекательное изображение.
По практическим соображениям кирпичики должны иметь ширину, равную степени числа 2. То есть их размер по горизонтали должен составлять 2, 4, 16 и так далее вплоть до 320 пикселей в режиме 13h. Важность этих ограничений вы поймете позже.
Одни из «кирпичиков», составляющих изображение, могут включать в себя «прозрачные» пиксели, а другие — нет. Последние наиболее пригодны для изображения дальних слоев, а также ближних планов, у которых отсутствуют «прозрачные» области, в то время как «кирпичики», содержащие «прозрачные» пиксели, служат для рисования частей изображения, имеющих пустоты. Но на самом деле «прозрачными» могут быть не только отдельные пиксели, но и целые "кирпичики", которые при выводе вообще пропускаются. Они могут располагаться в тех участках рисунка, где полностью отсутствует какое-либо изображение.
Помните, что вывод мозаичных слоев, включающих в себя "прозрачные" области, выполняется медленнее, потому что добавочные накладные расходы требуются для проверки существования «прозрачных» пикселей. В этой главея не показываю программу вывода «прозрачных» мозаичных слоев, но вы можете сделать это сами, применив технику вывода «прозрачных» пикселей. В своих собственных программах вы можете выбирать любые комбинации цельных и мозаичных слоев, «сплошных» или включающих в себя «прозрачные» пиксели, только не забывайте о различиях в способах их вывода.
При создании мозаичного изображения нужно учитывать тот факт, что на экране никогда не присутствует более двух «кирпичиков», выведенных не полностью. Рисунок 17.2 поясняет это свойство.
Это свойство несколько упрощает построение мозаичного изображения. Таким образом, этот процесс состоит из трех шагов:
1.
Рисование первого (возможно, не полностью выведенного) «кирпичика»;
2. Рисование нескольких (полностью выведенных) «кирпичиков»;
3. Рисование последнего (возможно, не полностью выведенного) «кирпичика» .
Также нам необходимо иметь в программе целочисленный счетчик, который будет указывать местоположение видимого экрана внутри виртуального изображения.
Программа из Листинга 17.6 (TILES.С) демонстрирует моделирование мозаичных слоев. Самые ближние слои состоят из нескольких «кирпичиков». Определение виртуального изображения сохранено в файле TILEMAP.DAT, который представляет собой обычный ASCII-файл и обрабатывается во время инициализации. Цифры в файле представляют собой закодированные имена PCX-файлов. Обратите внимание, что код 0 зарезервирован для «прозрачного кирпичика». Рисунок 17.3 показывает небольшой пример мозаичного изображения.
Важным отличием между этой программой и демонстрационной программой двойного параллакса в Листинге 17.3 является добавление функции DrawTile().
Эта подпрограмма изображает «кирпичик» в указанном месте экрана. Два аргумента offset и width определяют соответственно начальный столбец и ширину для вывода не полностью помещающихся на экране «кирпичиков».
Для частично выведенных кирпичиков:
§ offset -первый столбец, в котором будет нарисован «кирпичик»;
§ width - некоторое значение меньше ширины «кирпичика».
Для полностью выведенных «кирпичиков»:
§ offset - 0;
§ width - определяет ширину «кирпичика».
Программа из Листинга 17.6 также использует курсорные клавиши для управления движением и клавишу Esc для выхода. В демонстрационной программе на переднем плане появляется стена дома, составленная из отдельных «кирпичиков», а за ней видны хорошо известная горная гряда и линия неба на самом дальнем плане. Выполняется эта программа немного медленнее из-за использования в ней функции вывода мозаичного изображения, но только на пару кадров в секунду.
Листинг 17. 5 содержит файл заголовка для программы Построения мозаичного изображения, представленной в Листинге 17.6. В заголовке определены константы и прототипы функций для демонстрационной Программы.
Листинг 17.5. Заголовок мозаичного смещения (TILES.Н).
// Этот файл содержит определения, используемые программой
// прокрутки мозаичных изображений
#define NUM_TILES 17 // количество файлов,
// содержащих "кирпичики"
#define TILE_WIDTH 16 //ширина "кирпичиков"
#define TILE_HEIGHT 16 // высота "кирпичиков"
#define TILE_COLS 40 //ширина мозаичного изображения
#define TILE_ROWS 6 // высота мозаичного изображения
#define TILES_TOTAL (TILE_COLS*TILE_ROWS)
#define TILES_PER_ROW (VIEW_WIDTH/TILE_WIDTH)
#define shift 4
ftifdef _cplusplus extern "C"
{
#endif
void ReadTiles(void);
void FreeTiles(void);
void ReadTileMap(char *);
void DrawTile(char *,int,int,int,int);
void DrawTiles(int,int);
#ifdef __cplusplus
}
#endif
Поскольку программа из Листинга 17.6 практически повторяет 17.3, она приводится без комментариев.
Листинг 17.6. Демонстрационная программа мозаичного смещающегося слоя (TILES.C).
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <dos.h>
#include "paral.h"
#include "tiles.h"
char *MemBuf,
*BackGroundBmp,
*ForeGroundBnip,
*VideoRam;
PcxFile pcx;
int volatile KeyScan;
int frames=0,
PrevMode;
int background,
foreground, position;
char *tiles[NUM_TILES+l];
int tilemap[TILES_TOTAL] ;
void interrupt (*OldInt9)(void);
//
//
int ReadPcxFile(char *filename,PcxFile *pcx)
{
long i;
int mode=NORMAL,nbytes;
char abyte,*p;
FILE *f;
f=fopen(filename,"rb");
if(f==NULL)
return PCX_NOFILE;
fread(&pcx->hdr,sizeof(PcxHeader),l,f);
pcx->width=1+pcx->hdr.xmax-pcx->hdr.xmin;
pcx->height=1+pcx->hdr.ymax-pcx->hdr.ymin;
pcx->imagebytes= ( unsigned int) (pcx->width*pcx->height) ;
if(pcx->imagebytes > PCX_MAX_SIZE)
return PCX_TOOBIG;
pcx->bitmap= (char*)malloc(pcx->imagebytes);
if(pcx->bitmap == NULL)
return PCX_NOMEM;
p=pcx->bitmap;
for(i=0;i<pcx->imagebytes;i++)
{
if(mode == NORMAL)
{
abyte=fgetc(f);
if((unsigned char)abyte > Oxbf)
{ nbytes=abyte & 0x3f;
abyte=fgetc(f);
if(-—nbytes > 0) mode=RLE ;
}
}
else if(--nbytes ==0)
mode=NORMAL;
*p++=abyte;
}
fseek(f,-768L,SEEK_END);
fread(pcx->pal,768,1, f) ;
p=pcx->pal;
for(i=0;i<768;i++) *p++=*p>>2;
fclose(f) ;
return PCX_OK;
}
//
void _interrupt NewInt9(void) {
register char x;
KeyScan=inp(Ox60);
x=inp(0х61) ;
outp(0x61,(x|0x80));
outp(0x61,x) ;
outp(0х20,0х20);
if(KeyScan == RIGHT__ARROW__REL ||
KeyScan == LEFT__ARROW_REL)
KeyScan=0;
}
//
void RestoreKeyboard(void) {
_dos_setvect(KEYBOARD,OldInt9);
}
//
void InitKeyboard(void)
{
Oldlnt9=_dos_getvect(KEYBOARD) ;
_dos_setvect(KEYBOARD,Newlnt9);
}
//
void SetAllRgbPalettefchar *pal)
{
struct SREGS s;
union REGS r;
segread(&s) ;
s.es=FP_SEG((void far*)pal);
r.x.dx=FP_OFF((void far*)pal);
r.x.ax=0xl012;
r.x.bx=0;
r.x.cx=256;
int86x(0xl0,&r,&r,&s) ;
}
//
void InitVideo()
{
union REGS r;
r.h.ah=0x0f;
int86(0xl0,&r,&r); PrevMode=r.h.al;
r.x.ax=0xl3;
int86(0xl0,&r,&r);
VideoRam=MK_FP(0xa000,0);
}
//
void RestoreVideo()
{
union REGS r;
r.x.ax=PrevMode;
int86(0xl0,&r,&r) ;
}
//
int InitBitmaps()
{
int r;
background=foreground=l;
r=ReadPcxFile("backgrnd.pcx",&pcx) ;
if(r != PCX_OK) return FALSE;
BackGroundBnip=pcx.bitmap ;
SetAllRgbPalette(pcx.pal); ,
r=ReadPcxFile("foregrnd.pcx",&pcx);
if(r != PCX_OK) return FALSE;
ForeGroundBmp=pcx.bitmap;
MemBuf=malloc(MEMBLK) ;
if(MemBuf == NULL) return FALSE;
memset(MemBuf,0, MEMBLK) ;
return TRUE;
) //
void FreeMem()
{
free(MemBuf);
free(BackGroundBmp) ;
free(ForeGroundBmp) ;
FreeTiles(};
}
//
void DrawLayers()
{ OpaqueBlt(BackGroundBmp,0,100,background) ;
TransparentBIt(ForeGroundBmp,50,100,foreground) ;
DrawTiles(position,54) ;
}
//
void AnimLoop() {
while(KeyScan != ESC_PRESSED)
{
switch(KeyScan)
{ case RIGHT_ARROW_PRESSED:
position+=4;
if(position > TOTAL_SCROLL) {
position=TOTAL_SCROLL;
break;
}
background-=1;
if(background < 1)
background+=VIEW_WIDTH;
foreground-=2; if(foreground < 1)
foreground+=VIEW_WIDTH;
break;
case LEFT_ARROW_PRESSED:
position-=4;
if(position < 0) {
position=0;
break;
}
background+=1;
if(background > VIEW_WIDTH-1) background-=VIEW_WIDTH;
foreground+=2 ;
if (foreground > VIEW_WIDTH-1) foreground-=VIEW_WIDTH;
break;
default:
break;
} DrawLayers();
memcpy(VideoRam,MemBuf,MEMBLK) ;
frames++;
} }
//
void Initialize()
{
position=0;
InitVideo(} ;
InitKeyboard();
if(!InitBitmaps())
{
Cleanup();
printf("\nError loading bitmaps\n");
exit(l);
} ReadTileMap("tilemap.dat");
ReadTiles();
}
// void Cleanup() {
RestoreVideo() ;
RestoreKeyboard();
FreeMem();
}
void ReadTiles(void)
{
PcxFile pcx;
char buf[80];
int i,result;
tiles[0]=NULL;
for(i=l;i<=NUM_TILES;i++)
{
sprintf(buf,"t%d.pcx",i);
result=ReadPcxFile(buf,&pcx);
if(result != PCX_OK) ( printf("\ nerror reading file: %s\n",buf);
exit(1);
} tiles[i]=pcx.bitmap;
} }
void FreeTiles() { int i;
for(i=0;i<NUM_TILES;i++) free(tiles[i]) ;
}
void ReadTileMap(char *filename)
{
int i;
FILE *f;
f=fopen(filename,"rt") ;
for (i=0; i<TILES__TOTAL; i-H-) {
fscanf(f,"%d",&(tilemap[i])) ;
}
fclose(f);
}
//
void DrawTile(char *bmp,int x,int y,int offset, int width)
{
char *dest;
int i;
if(bmp == NULL) return;
dest=MemBuf+y*VIEW_WIDTH+x;
bmp+=offset;
for(i=0;i<TILE_HEIGHT;i++) {
memcpy(dest,bmp,width);
dest+=VIEW_WIDTH;
bmp+=TILE_WIDTH;
} }
//
void DrawTiles(int VirtualX,int Starty)
{
int i,x,index,offset,row,limit;
index=VirtualX>>SHIFT;
offset=VirtualX - (index<<SHIFT) ;
limit=TILES_PER_ROW;
if(offset==0)
limit--;
for(row=Starty;row<Starty+TILE_HEIGHT*TILE_ROWS;
row+=TILE_HEIGHT) {
x=TILE_WIDTH-of£set;
DrawTile(tiles[tilemap[index]],0,row,offset,
TILE_WIDTH-offset);
for(i=index+l;i<index+limit;i++)
{
DrawTile(tiles [tilemap [i]], x, row, 0, TILE_WIDTH) ;
x+=TILE_WIDTH;
} DrawTile(tiles [tilemap[i] ] ,x, row, 0,offset);
index+=TILE_COLS;
}
}
//
int main() { clock_t begin,fini;
Initialize() ;
begin=clock();
AnimLoop() ;
fini=clock() ;
Cleanup() ;
printf("Frames: %d\nfps: %f\n",frames,
(float)CLK_TCK*frames/(fini-begin)) ;
return 0;
)
Собираем все вместе
Итак, мы рассмотрели основы работы с прерываниями, многозадачность и некоторые приемы программирования, которые несколько облегчают реализацию архитектуры компьютерной игры. Причем представленная здесь коллекция приемов программирования далеко не самая полная и лучшая. Мы просто обсудили эти вопросы для того, чтобы вы смогли представить себе различные способы организации игры. Впрочем, вскоре вы, наверняка, придумаете и свои Методы и трюки. Главная цель этой главы состоит в том, чтобы дать вам некую Справную точку и направление для ваших размышлений. Теперь я хочу просуммировать все полученные вами знания и разобрать несколько примеров применения рассмотренных нами методов. После этого для окончательного закрепления материала я приведу еще два примера программ.
В этой главе мы узнали о реализации многозадачности на персональных компыотерах с помощью прерываний. Прерывание — это всего-навсего мгновенная передача управления от выполняющейся программы к процедуре обслуживания прерывания (ISR), которая выполняет все действия, относящиеся к событию, вызвавшему прерывание. Прерывания полезны тем, что они позволяют реализовать на персональном компьютере некое подобие многозадачности. Более того, они могут быть использованы для управления задачами, зависящими от времени и событий, контролировать которые главной программе крайне затруднительно именно в силу самой природы этих задач. Обычно процедуры обслуживания прерываний используются для таких вещей как обработка ввода с клавиатуры или передача данных через последовательный порт. Однако мы можем использовать их и для других задач (работа со звуком, система ввода/вывода, обслуживающие функции и так далее).
Управление задачами, зависящими от времени и событий, осуществляется за счет подпрограмм обработки тех прерываний, которые происходят по наступлению определенного момента времени или в результате какого-либо события. В этом смысле очень полезным оказывается прерывание системного таймера 0х1С. Мы можем поместить адрес нашего обработчика в таблицу векторов по адресу, соответствующему прерыванию, которое генерируется при каждом системном «тике» (то есть 18.2 раза в секунду).
Это гарантирует нам, что независимо от загруженности системы, наша подпрограмма обработки прерываний будет вызываться всегда с неизменной частотой. Кроме того, мы знаем, что системный таймер можно запрограммировать и на другую частоту, например, 20Гц, 30Гц, 60Гц и так далее. Нам это может понадобиться, если мы собираемся использовать прерывание для чего-то, что требует более частого выполнения.
И, наконец, мы узнали, как использовать прерывания для изменения глобальных переменных, являющихся, по сути, сообщениями, на которые могут реагировать другие функции той же программы. Это может пригодиться в тех случаях, когда в игре надо выполнять критичные по времени исполнения операции, синхронизацию событий и тому подобные задачи.
Кроме того, мы обсудили как построить цикл игры. На самом деле это просто один из методов организации программы и входящих в нее функций таким образом, что все действия выполняются в определенной последовательности и в соответствии с определенной логикой. Мы узнали, что программа игры должна иметь раздел инициализации, за которым следует главный цикл событий. Внутри этого цикла осуществляется стирание графического изображения, получение входных данных от пользователя, выполнение необходимых графических преобразований, формирование и вывод на экран графического изображения. Конечно, порядок действий может быть несколько иным, как, впрочем, и входящие в него элементы, однако нам всегда следует создавать цикл событии, внутри которого эффективно и логически обоснованно делается все, что должно быть сделано.
Рассмотрев цикл игры, мы перешли к обсуждению достаточно отвлеченных на первый взгляд вопросов, относящихся к приемам программирования. Мы изучили два тесно связанных между собой типа функций, которые назвали автономными функциями, и функциями ответа. Эти приемы программирования были приведены в качестве иллюстрации, как много действий и процессов в компьютерной игре могут быть организованы независимо друг от друга и иметь свои собственные данные.
Компьютерные игры настолько сложны, что нам часто приходится писать функции, работающие без постоянного контроля со стороны главной программы. В этом нам помогают автономные функции. Кроме того мы рассмотрели еще и функции ответа. Эти функции «откликаются» на определенные события соответствующими действиями.
Обсуждение всех этих вопросов призвано помочь вам осознать, как писать программы, работающие в «реальном режиме времени». Иными словами, как организовать выполнение нужных действий таким образом, чтобы создать впечатление одновременности происходящего. В компьютерных играх без этого не обойтись.
Напоследок мы узнали, как перепрограммировать системные часы, реализованные микросхемой таймера 8253. Мы узнали, что в счетчике 0 хранится 16-разрядное значение, которое рассматривается как делитель поступающего сигнала с частотой 1.19318МГц. Результирующий сигнал используется для генерации «тиков», своего рода пульса, который инициирует прерывание по времени. Это прерывание может быть перехвачено с помощью вектора прерывания 0х1С. В ста процентах игр этот таймер перепрограммируется на более подходящую частоту вроде 30Гц или 60Гц. Таким образом, работа программы синхронизируется по новой базовой частоте. Помимо этого, входящая в состав игры подсистема вывода звуковых эффектов и подсистема ввода/вывода реа лизуются в виде фоновых задач, которые реализованы в виде процедур обслуживания таймерного прерывания 0х1С. Благодаря этому ввод/вывод и музыка исполняются с неизменной скоростью даже тогда, когда сама игра несколько замедляется вследствие выполнения вычислений или сложных графических эффектов.
Писать эту главу было непросто, поскольку в ней мне пришлось изложить вам очень много новых вещей. На самом деле, следует иметь в виду, что только опыт поможет вам разобраться с миллионами тонкостей, которые приходится учитывать при программировании компьютерных игр. Тем не менее, перед вами уже открылись новые возможности. Сейчас вы готовы мыслить по-новому и это Уже неплохо.
Как я и обещал, сейчас мы напишем еще несколько подпрограмм обслуживания прерываний и сделаем с их помощью несколько потрясающих вещей.
Содержание и общее настроение игры
Подумайте, что будет представлять собой ваша игра. Если это будет, например, космическое путешествие, то может потребоваться комбинация ярких и темных тонов: яркие будут представлять высокотехнологичные металлические конструкции, а более темные - звездные дали космического пространства. Средневековые сцены можно передать богатой палитрой земляных красок для отображения прошлого. Игры-ужастики могли бы, вероятно, потребовать темных, мрачных тонов. Если в вашей игре должны встретиться сцены как дневного, так и ночного освещения, это также необходимо учесть при разработке палитры.
Соединение через нуль-модем
Как я уже сказал в самом начале, у нас не хватит времени вникать во все тонкости использования модема. Существует слишком много тем, которых мы слегка коснулись, и все они имеет отношение к дизайну видеоигр. Что я намерен сделать вместо этого, так это создать коммуникационную систему, использующую соединение типа нуль-модем, в котором модем, как таковой, отсутствует. Нуль-модем — простое соединение, которое связывает два компьютера через последовательные порты. Рисунок 14,1 демонстрирует такое соединение.
Чтобы изготовить такое соединение, мы должны взять нуль-модемный кабель с надлежащими разъемами. Это может оказаться не слишком просто, но если вы будете осторожны, то заработает с первой попытки. Мы будем использовать только три типа разъема:
§
Линию передачи данных;
§ Линию приема данных;
§ Землю.
На рисунке 14.2 показано, как сделать нуль-модемный кабель для разных типов разъемов.
Если вы не хотите изготавливать нуль-модемный кабель, вы можете заплатить за него в любом из компьютерных магазинов. (Пожалуйста, не платите больше 15$. Я не могу спокойно смотреть, как люди платят за кусок провода и пластик даже 1.50$). Сейчас официальный нуль-модемный кабель использует более трех шин, которые я перечислил. Он задействует все шины, имитируя тем самым модемное соединение. Пока у нас есть TXD, RXD и земля, мы тоже при деле. (Дополнительные линии используются для аппаратного обеспечения «рукопожатия», но мы не будем ими пользоваться.)
Отлично, теперь у нас есть нуль-модемный кабель, и мы знаем, какие Регистры что делают в UART'e. Я думаю, что теперь самое время начать писать коммуникационное программное обеспечение.
Сохранение игры
Физики верят, что если бы они знали координаты и состояние всех частиц Вселенной, то смогли бы предсказывать будущее. Возможно, это и так. Перед нами же стоит задача попроще. Нам надо уметь записывать состояние нашей игры с тем, чтобы при необходимости его можно было восстановить.
Итак, проблема заключается в следующем: как запомнить игру так, чтобы в любой момент мы могли восстановить ее в прерванном месте? Ответ довольно Прост: надо создать список всех игровых объектов и факторов, которые могут измениться в течение игры. Затем создать файл определенного формата и записать в него всю эту информацию. Теперь, когда игрок хочет сохранить игру, мы просто записываем необходимую информацию на диск.
Для примера рассмотрим, как можно сохранить игру в DOOM или Wolfenstein 3-D:
§
В каждой игре присутствует множество неподвижных объектов. Поэтому их сохранять не надо. Однако во время сохранения игры нам необходимо помнить, что они существуют;
§ Также мы должны принимать во внимание настройки игры. Для каждой настройки мы должны запомнить копию ее структуры. Эти структуры должны содержать положение, состояние и т. д.;
§ Наконец, надо запомнить, какой инвентарь имеет при себе играющий на данный момент, а также каково его состояние (позиция, здоровье и т. д.), иными словами, то, что необходимо для правильного сохранения статуса игры.
Это все. Если кратко, мы делаем моментальный снимок нашего мира и представляем его в оцифрованном виде.
Соотношение пространства образов и пространства объектов
Алгоритм Z-буфера хорошо работает и может быть легко реализован. Единственная проблема состоит в том, что он работает на уровне пикселей и является алгоритмом обработки образа. Это значит, что он не рассматривает геометрических свойств объекта. Это требует наличия некоторого гибридного алгоритма для использования в специальных случаях. Такой алгоритм должен учитывать геометрические свойства объекта перед простым удалением невидимых поверхностей. Теперь давайте поговорим о том, как придать поверхности наших трехмерных объектов большую реалистичность.
Состояния бытия
Теперь я хотел бы обсудить одну философскую тему. Это и в самом деле философия, поскольку каждый из профессионалов делает это по-своему. Например, я пришел к выводу, что удобно включить в набор переменных, представляющих игровой объект, переменную «состояния». Эта переменная помогает оценивать объект с точки зрения того, должен ли он как-то меняться в зависимости от пространства или окружающей среды.
Например, мне всегда нравится в каждой структуре иметь переменную, которая описывает состояние объекта: жив, мертв, голоден и т, д. Эта переменная помогает в написании функций, создающих картину того, как объект погибает или воскресает вновь.
Поле состояния в структуре данных объекта надо иметь также и для того, чтобы использовать некоторые функции, изображающие объект в тот момент, когда он совсем здоров и когда он умирает. Эта особая информация берется из переменных состояния. Более подробно мы рассмотрим это в тринадцатой главе, «Искусственный интеллект». Однако сейчас нам надо знать, что система или объект в компьютерной игре (как и в реальной жизни) может проходить ряд состояний. Для того чтобы этого достичь, мы будем использовать переменные состояния в структуре данных каждого объекта.
Как пример использования состояния для оценки некоторых аспектов объекта игры давайте посмотрим на маленькую программу, создающую муравья. Муравей может ходить в разные стороны, пока не наткнется на стену или камень, после чего он выбирает новое направление (на север, юг, запад или восток). Так как четыре направления отслеживаются достаточно просто, мы можем воздержаться от использования переменных состояния. Однако движение на север, юг, запад и восток могут быть легко заменены такими эмоциями как чувство голода, усталость, счастье или гнев.
Для наглядности, используя наш метод имитации игровых объектов, я создал целый муравейник. Листинг 11.4 показывает эту программу.
Листинг 11.4. Муравейник (ANTS.С).
// ВКЛЮЧАЕМЫЕ ФАЙЛЫ
/////////////////////////////////////////
#include <dos.h>
#include <bios.h>
#include <stdio.h>
#include <math.h>
#include <conio.h>
#include <graph.h>
// определения /////////////////////////////////////////////
#define ANT_NORTH 0
#define ANT_EAST 1 #define ANT_SOUTH 2
#define ANT_WEST 3 #define NUM_ANTS 50
// структуры ////////////////////////////
// структура муравья
typedef struct ant_typ
{
int x,y; // позиция муравья
int state; // состояние муравья
unsigned char color; // цвет муравья: красный или зеленый
unsigned back_color; // фон
под муравьем
} ant, *ant_ptr;
// ГЛОБАЛЬНЫЕ
ПЕРЕМЕННЫЕ
///////////////////////////////////
unsigned char far *video_buffer: = (char far *)0xA0000000L;
// указатель
на видеобуфер
unsigned int far *clock = (unsigned int far *)0x0000046C;
// указатель на внутренний таймер
// наши маленькие муравьи
ant ants[NUM_ANTS];
// ФУНКЦИИ /////////////////////////////////////////////////
void Timer(int clicks)
{
// эта функция использует внутренний таймер с частотой 18.2 "тик"/с.
//32-битовое значение этого таймера находится по адресу 0000:046Ch
unsigned int now;
//получаем текущее время
now = *clock;
//Ожидаем до истечения указанного периода времени.
//Заметьте; что каждый,"тик" имеет длительность примерно в 55мс.
while(abs(*clock - now) < clicks){}
) // конец Timer
////////////////////////////////////////////////////////////
void Plot_Pixel_Fast(int x,int y,unsigned char color)
{
// эта функция рисует точку заданного цвета несколько быстрее чем
// обычно, за счет применения операции сдвига вместо операции
// умножения
// используется тот факт, что 320*у = 256*у + б4*у = у<<8 + у<<6
video_buffer[ ( (у<<8) + (у<<6) ) + х] = color;
} // конец Plot_Pixel_Fast ////////////////////////////////////////////////////////////
unsigned char Read_Pixel_Fast(int x,int у)
{
// читаем значение пикселя из видеобуфера
return (video_buffer [((у<<8) + (у<<6) ) + х]);
} // конец Read_Pixel_Fast
///////////////////////////////////////
void Draw_Ground(void)
{
int index;
// эта функция рисует разбросанные по экрану серые камешки
for (index=0; index<200; index++)
{
Plot_Pixel_Fast(rand()%320,rand()%200, 7 + rand()%2);
} // конец
цикла
} // конец Draw_Ground ///////////////////////////////////////////////
void Initialize_Ants(void)
{
int index;
for (index=0; index<NUM_ANTS; index++)
{
// выбираем случайным образом начальную позицию, цвет и состояние
// для каждого муравья, а также определяем его фон
ants[index].х = rand(}%320;
ants[index].у = rand()%200;
ants[index].state = rand()%4;
if (rand()%2==1)
ants[index].color = 10;
else
ants[index].color = 12;
// сканирование фона
ants[index].back_color = Read_Pixel_Fast(ants[index].х, ants[index].y);
} // конец цикла
} // конец Initialize_Ants ////////////////////////////////////////////////////////////
void Erase_Ants(void)
{
int index;
// в цикле обрабатывается массив муравьев, все муравьи замещаются
// точками, цвета соответствующего фона
for (index=0; index<NUM_ANTS; index++)
{
Plot_Pixel_Fast(ants[index].х, ants[index].y,
ants[index].back_color) ;
} // конец цикла
} // конец Erase Ants
////////////////////////////////////////////////////////////
void Move_Ants(void)
{
int index,rock;
//в цикле обрабатывается массив муравьев, каждый муравей перемещается
//в соответствии со своим состоянием
for (index=0; index<NUM ANTS; index++)
{
// каково состояние муравья?
switch(ants[index].state)
{
сазе ANT_NORTH:
{
ants[index].у—;
} break;
case ANT_SOUTH:
{
ants[index].y++;
} break;
case ANT_WEST:
{
ants[index].x--;
} break;
case ANT_EAST:
{
ants[index].x++;
} break;
} // конец оператора switch
// проверка, не столкнулся ли муравей
// с границами экрана или с камнем
if (ants[index].x > 319) ants[index].x = 0;
else if (ants[index].x <0)
ants[index].x = 319;
if (ants[index].у > 200)
ants[index].у = 200;
else if (ants[index].у <0)
ants[index].у = 199;
// здесь проверяем, не столкнулся ли муравей с камнем
rock = Read_Pixel_Fast(ants[index].x, ants[index].у);
if (rock)
{
// изменение состояния
ants[index].state =rand()%4; // выбор нового состояния
} // конец оператора if
} // конец цикла
} // конец Move_Ants
////////////////////////////////////////////////
void Behind_Ants(veid)
{ int index;
// в цикле обрабатывается массив муравьев,
// определяется цвет фона для каждого муравья
for (index=0; index<NUM_ANTS; index++)
{ // читается пиксель и его значение сохраняется
// для
дальнейшего использования
ants[index].back_color = Read_Pixel_Fast(ants[index].x, ants[index].y) ;
} // конец
цикла
} // конец Behind_Ants ////////////////////////////////////////////////////////////
void Draw_Ants(void)
{
int index;
// в цикле обрабатывается массив муравьев, рисуется каждый
// муравей соответствующим цветом
for (index=0;index<NUM_ANTS; index++)
{
Plot_Pixel_Fast(ants[index].x, ants[index].y, ants[index].color);
} // конец
цикла
} // конец Draw_Ants
// ОСНОВНАЯ ПРОГРАММА //////////////////////////////////////
void main
(void)
{
// установка видеорежима 320х200х256
_setvideomode(_MRES256COLOR);
_settextposition(2,0);
printf("Hit any key to exit.");
//построение игрового пространства
Draw_Ground() ;
/ создаем
муравьев
Initialize_Ants();
while(!kbhit())
{ // стираем всех муравьев
Erase_Ants
(};
// перемещаем всех муравьев
Move_Ants
();
// определяем фон под муравьями
Behind_Ants() ;
// рисуем всех муравьев
Draw_Ants
(),;
// немного подождем
Timer(2);
} // конец оператора while
// восстановление первоначального видеорежима
_setvideomode(_DEFAULTMODE);
} // конец функции main
Если вы запустите программу, то увидите кучу точек на экране. Красные и зеленые точки — это муравьи, а серые — это камни. Когда муравьи натыкаются на камни, они меняют направление движения. Эта программа показывает использование переменных состояния.Позже мы узнаем, как использовать переменные состояния для создания более сложных для вычисления структур, называемых состоянием машины.
Теперь изменим тему и поговорим о том, как выглядит игра с точки зрения игрока.
Создание бесшовных мозаичных изображений
К бесшовным относятся те изображения, которые можно разместить один за другим так, что не будет заметно, где заканчивается одно и начинается другое.
В некоторых играх изображения имеют только горизонтальную бесшовность (то есть они стыкуются друг с другом в непрерывную горизонтальную линию) Однако в некоторых играх требуется, чтобы изображения не имели швов не только по горизонтали, но и по вертикали. Это может понадобиться, наприме, в играх типа имитаторов полета для составления карты местности. Для создания пейзажа в таких случаях используются бесшовные «кирпичики», которые плавно переходят один в другой и по горизонтали и по вертикали.
На рисунке 16.17 показано, что получается, если заранее не предусмотреть возможности бесшовного соединения картинок.
Здесь наша горная цепь, отмасштабированная до размера 256х64 точек (размер четырех кирпичиков) помещена вслед за своей копией. Обратите внимание, что в месте соединения изображений появился хорошо заметный шов:рисунки не перетекают плавно один в другой. Этот шов необходимо отредактировать так, чтобы объединенные изображения выглядели как одна непрерывная картинка.
Существует два основных подхода к созданию бесшовных «кирпичиков». Во-первых, что вполне применимо к данному примеру, можно сделать зеркальную копию исходного рисунка. После этого изображения хорошо состыкуются друг с другом и их можно затем показать в игровом пространстве, как на рисунке 16.18.
Однако этот метод будет работать не для всех изображений. В тех случаях, когда вы хотите расположить рядом два совершенно одинаковых «кирпичика». потребуется отредактировать изображение в местах соединений. Подбирая цвета из области, окружающей шов, исправьте рисунок так, чтобы переход от одного изображения к другому не был заметен. На рисунке 16.19 показано, как выглядит соединение после модификации изображений.
Затем новое изображение, совпадающее по размерам с исходным «кирпичиком» (в нашем случае 256х64 пикселя), вырезается откуда-нибудь из середины общего изображения. Убедитесь, что отредактированная область попадает внутрь отрезаемой части. Полученный в итоге бесшовный «кирпичика показан на рисунке 16.20.
Приведенные выше иллюстрации
можно увидеть в цвете, просмотрев файл ЕХАМРЮ.РСХ.
Создание модели освещения
Сейчас мы уже изучили всю физику, которую надо знать для формирования хорошо выглядящей модели освещения. Мы знаем, что:
§
Чем дальше находится источник, тем меньше света он дает;
§ Если поверхность расположена под углом к источнику света, она отражает меньше света;
§ Если изменяется уровень рассеянного освещения, это сказывается на всех объектах в комнате. Все эти три фактора, взятые в совокупности, и формируют модель освещения.
Нам известны: угод, под которым виден источник света, уровень рассеянного освещения и расстояние до каждой из стен. Возникает вопрос: все ли это нам нужно? Может быть все, а может быть и нет. Это зависит от того, что называть реалистичным. Наша главная задача — сформировать затенение стен так, чтобы они выглядели реальными, но, в то же время, изображение стены формируется с помощью текстуры.
Существует два пути устранения этого противоречия.
Мы могли бы рассчитывать интенсивность каждого пикселя "на лету" и окрашивать его в соответствующий цвет. Единственная, возникающая при этом проблема, — недостаточное количество одновременно отображаемых цветов. Мы располагаем только 256 регистрами цвета, следовательно, одновременно на экране не может присутствовать больше 256 цветов. Это заставляет нас использовать цвет с большой осторожностью.
Тем не менее, это вполне работоспособный метод, дающий хорошие реэультаты. Я оставляю целиком на ваше усмотрение применимость к получающимся изображениям термина «реалистичные», но они достаточно хороши для видеоигр на ПК. Мы знаем, что должны изменять оттенок цвета стен в зависимости от угла, под которым они видны, и от их расстояния до игрока. Мы также знаем, что интенсивность окраски стен зависит от уровня рассеянного света. Теперь давайте сконцентрируемся на эффектах, связанных с углом обзора поверхности и расстоянием до нее.
Выполняя трассировку лучей, мы выводим на экран фрагмент текстуры.
Ее вид определяется содержимым регистров RGB в таблице цветов (см. пятую главу). Для выполнения операции затенения мы должны знать угол, образуемый поверхностью с направлением взгляда, и расстояние до нее. К счастью, нам известно и то и другое. Теперь осталось только выяснить, как использовать имеющуюся информацию для формирования затенения.
На практике нам нет необходимости использовать оба параметра в алгоритме затенения. Мы можем использовать или расстояние от стены до игрока или угол между направлением взгляда и поверхностью стены для получения вполне реалистичного изображения. Единственная проблема состоит в том, как получить все возможные эффекты затенения с помощью только 256 цветов?
Для решения этой проблемы мы можем создать палитру со следующими свойствами:
§ Первые 16 цветов - стандартные цвета EGA;
§ Дополнительные цвета помещаются в следующие 56 ячеек палитры. Это те цвета, с которыми мы будем работать, и единственные цвета, присутствующие в игре. Эти 56 цветов должны быть выбраны так, чтобы их хватило в качестве базовых для изображения всех игровых объектов. Более того, эти цвета будут самыми яркими в игре и они должны создаваться с учетом этого факта;
§ Теперь некоторая хитрость. Оставшиеся 184 цвета разбиваются на 3 банка по 56 цветов в каждом и банк из 16 цветов в конце палитры.
Три дополнительных банка по 56 цветов будут заполняться в процессе работы программы и использоваться механизмом формирования затенения. Дополнительный банк из 16 цветов используется для анимации палитры. Таким образом, палитра будет выглядеть как Представлено в таблице.
Таблица 6.1. Цветовая палитра для формирования затенения.
Регистры цвета |
Функция |
0 – 15 |
Базовые цвета EGA |
16 - 71 |
Первичные цвета |
72 – 127 |
Вторичные затененные производные первичных цветов |
128-239 |
Третичные затененные производные |
240 – 255 |
Дополнительные цвета для цветовой ротации и т.п. |
Механизм затенения работает следующим образом: при отрисовке каждого вертикального фрагмента к значениям его пикселей добавляется некая константа. Например, если отрисовывается текстура из одного цвета с номером 16, то производные цвета будут иметь значения 16+56, 16+2х56 и 16+3х56 (или 72, 128 и 184 соответственно). В общем, механизм затенения берет за основу цвета текстуры и модифицирует их за счет использования других регистров цвета, номера которых определяются как сумма исходного цвета и константы. Если объект нарисован в первых 16 цветах, добавление константы не производится. Теперь мы имеем по четыре варианта каждого из 56 первичных цветов. Этого вполне достаточно для формирования реалистичного изображения. Три «затененных», банка цветов заполняются во время работы программы путем некоторого уменьшения интенсивности базовых цветов. Таким образом, всего получаются четыре банка из одних и тех же цветов с убывающей интенсивностью.
Теперь возникает вопрос, как пользоваться полученной таблицей и как готовить битовые карты? Когда вы будете рисовать свои битовые карты, рисуйте те объекты, которые впоследствии планируете затенять, с использованием 56 первичных цветов. Те объекты, которые затеняться не будут, рисуются в первых 16 цветах — они не затрагиваются механизмом затенения.
Кстати, так же создается и эффект локального освещения. Освещенные или светящиеся области рисуются с использованием первых 16 цветов и, соответственно, их цвет не изменяется при затенении.
Последняя проблема состоит в том, насколько мы должны затенять цвет. Как я уже говорил раньше, мы можем использовать или угол между поверхностью и лучом зрения или расстояние до игрока. Я рекомендую попробовать оба варианта и выбрать тот, который вам больше понравится. Что бы вы ни использовали, эта величина должна быть разбита на зоны. Если величина попадает в первую зону, используйте первую производную цвета, во второй зоне — вторую и т. д. Например, вы можете решить, что если угол составляет меньше 20°, то используется первая производная, если он оказывается в диапазоне 20-300 — вторая и т.
д.
Та же логика может быть применена и к расстоянию. Надо только учитывать, что интенсивность цвета падает нелинейно. В качестве примера можно привести разбивку расстояния на зоны, представленную в таблице 6.2.
Таблица 6.2.
Дистанция (условных ед.) |
Банк цвета |
0-20 |
Банк 0(цвета 16 – 71) |
20-50 |
Банк 1(цвета 72 – 127) |
50-100 |
Банк 2(цвета 128 – 183) |
100-бесконечность |
Банк 3(цвета 184 – 239) |
Коротко повторим: цветовая палитра разбивается на шесть областей.
§ Первая и последняя области содержат цвета, не используемые при затенении;
§ Четыре средних области представляют собой банки по 56 цветов и каждый из этих банков представляет собой производные одних и тех же первичных цветов.
Когда изображение выводится на экран, каждый пиксель рисуется наиболее ярким цветом (банк 0), к которому добавляется некоторая константа, для формирования требуемой производной. Эта константа зависит от модели освещения, определяемой либо расстоянием от объекта до игрока, либо углом, под которым игрок смотрит на поверхность.
Вот и все.
Создание пользовательского интерфейса с системой распознавания голоса
Я бы хотел немного остановиться на новых формах пользовательского интерфейса, базирующихся на звуке. Сейчас уже очевидно, что мы в состоянии использовать системы распознавания голоса в разработке интерфейса программы. Я бы хотел дать несколько общих советов тем, кто собирается включить распознавание голоса в свои прикладные программы.
Управление голосом является наиболее естественным способом общения с компьютером. Почему же до сих пор это не очень получается? Большинство современных голосовых систем пассивны; они работают на заднем плане, пытаясь угадать, что же вы хотели им сказать. Проявившись же, они стирают файл или делают что-нибудь похожее по идиотизму.
Большинство скажет, что проблема в самих программах распознавания голоса. Но это не так. Проблема в дизайне пользовательского интерфейса.
Интерфейс с распознаванием голоса должен быть активным. Он должен переспрашивать пользователя при сомнениях и подтверждать получение команды. Он так же должен персонифицироваться через технику общения и используемые выражения.
Попробуйте пообщаться с приятелем, который никогда не отвечает - у вас появится ощущение, что вас игнорируют. Как минимум, вам нужно иногда услышать «Да-а» или «Ага», чтобы знать, что вас слушают. Или возьмите другой пример. Отец просит ребенка что-то сделать, а в ответ не слышит:
«Хорошо, папа». В таком случае папа обычно начинает злиться и кричать: «Эй, ты меня слышишь?!» (так, по крайней мере, происходит в моем доме.) При разговоре нам необходима ответная реакция, чтобы знать, что нас поняли. Интерфейс с распознаванием голоса должен использовать реплики типа: «Конечно», «Хорошо, шеф», «Простите?»
Несколько подобных фраз могут сделать интерфейс более естественным и позволят нам управлять сложными системами набором нескольких простых голосовых команд. Сегодня не существует реальной проблемы с системами распознавания голоса. Обычно они распознают отдельные слова достаточно аккуратно. Существующие коммерческие программы позволяют нам совершать очень сложные действия, используя несколько меню и пиктограмм.
Хорошо разработанная система с голосовым меню может быть не менее мощной и быть гораздо привлекательнее в использований.
И, наконец, последнее замечание о голосовом взаимодействии с компыотером. Говорящие машины заслужили плохую репутацию в начале 80-х из-за японских говорящих автомобилей: «Дверь открыта! Дверь открыта!». Все их ненавидели. Люди гордились тем, что им удалось «сломать» свои новые машины и выключить надоедливый голос. Заметьте, что проблема состояла не в плохом произношении машины, а в плохом пользовательском интерфейсе. Многих из нас еще в детстве научили, что не следует заговаривать с кем-либо, пока к вам не обратились. То же относится и к говорящим машинам. Если машина надоедает вам, прерывает ход ваших мыслей и мешает тому, что вы сейчас делаете, это очень раздражает. Однако если вы можете о чем-либо спросить машину и получить от нее ответ, это очень классная машина. Если машина хочет сообщить вам что-то действительно важное, она не должна врываться без приглашения, прерывая ваши мысли или разговор. Нет, она должна вежливо сказать «Гхм» или «Простите» либо просто прочистить свое электронное горло. Когда вы найдете время ответить и скажете: «Да, Саймон», компьютер может выдать накопившуюся информацию. Если системы с распознаванием голоса не являются достаточно естественными, они практически бесполезны. Мы будем продолжать использовать клавиатуру и мышь там, где это необходимо, но системы с распознаванием голоса должны стать более общительными, интерактивными и, самое главное, естественными.
Для персонификации машины одинаково подходит и использование разговорного языка, и акцент, и юмор. Обычно юмора боятся, вдруг кто-то подумает, что программа несерье;й1ая. Но кому понравится в очередной раз слушать идеальный, безжизненный голос «голосовой почты». Вместо этого, ваш компьютер мог бы сказать; «ОК, босс, все что пожелаете!» или «Конечно, Пап, нет проблем!» или «Инициализирую последовательность сохранения файла, доволен?» Конечно, это только примеры, и их не надо понимать буквально, просто они показывают, что общение с компьютером тоже может быть веселым.А ведь сделать общение с компьютером более естественным, простым, менее пугающим — и есть основная цель хорошо продуманного интерфейса.
В заключение, я хочу сказать, что приведенные здесь примеры представлены только для подстегиваиия вашей изобретательности. При разработке голосового интерфейса, просто задайте себе один вопрос: «Как бы это выглядело, если б я разговаривал с живым человеком?» Затем, учитывая технические ограничения, сделайте нечто максимально похожее на живую речь. Помните, что хотя машина может «слышать» лишь ограниченное число слов, «сказать» же она может все, что угодно. Составьте простой набор команд, и сделайте его минимально подверженным ошибкам. Старайтесь по возможности учитывать пожелания пользователя. Пусть ваши голосовые ответы варьируются и, самое главное, будут естественными.
Создание съемочного мини-павильона
Вам необходимо:
§ Во-первых, чистое, просторное место, чтобы установить видеокамеру и подставку. Расстояние между ними должно быть не менее четырех шагов. Это позволит вам поэкспериментировать с фокусом;
§ Во-вторых, я считаю, что комната для съемок должна быть по возможности одноцветной. Я не советую вам снимать в комнате с розовыми и голубыми стенами. Больше всего в данной ситуации подойдут белые стены и темный потолок;
§ Затем сделайте небольшую площадку для фотографии, фон которой вы сможете менять так, как это показано на рисунке 8.11. Я вырезал два кусочка оргстекла и установил их под прямым углом друг к другу. Затем я использовал цветную бумагу, для создания пола и фона для объекта.
Фон
В кино используется голубой фон, что позволяет отфильтровать синие тона в изображении. Хотя этот метод широко известен, у меня возникли с ним проблемы. В конце концов, я вынужден был использовать черный экран. Синий же фон имеет смысл использовать, если вы будете снимать черные объекты. Затем я подбирал освещение до тех пор, пока изображение объекта не получилось максимально контрастным по отношению к фону.
Платформа
Раз уж у вас есть камера и импровизированная недорогая студия, то вы должны сделать для своего объекта подходящую платформу. Она не должна портить изображение и впоследствии легко должна удаляться из полученной картинки. Вначале я использовал прозрачную подставку из оргстекла, но быстро убедился, что отражения на ее поверхности создают причудливые световые эффекты. Тогда я приклеил с нижней стороны подставки бумагу. Затем я проделал маленькое отверстие снизу моей модельки и закрепил ее на платформе. Получилось совсем неплохо. Правда, я мог поворачивать модель только вокруг одной оси и не мог наклонить ее. Однако, меня это вполне устраивало, так как мне и не требовались такие сложные ракурсы.
Освещение
Теперь поговорим об освещении. Когда вы начинаете оцифровывать видеоизображение, то быстро понимаете, насколько важно хорошее освещение и как малейшее его изменение может повлиять на внешний вид объекта. В результате я остановился на галогеновых лампах и вспышке, которая играла роль точечного источника света (вспышка имитировала солнце).
Создание студии
Сделать миниатюрную студию удивительно трудно, если вы не имеете для этого остаточных возможностей. Если вы, как и я, живете в обычной квартире, то сразу же обнаружите, что в ней нет удобного места для того, чтобы пилить, cверлить или рисовать. Хотя, как я уже говорил выше, мне хватило для создания студии лишь нескольких листов оргстекла, соединенных под прямым углом.
Если "вы не очень любите мастерить, можете пойти и купить несколько листов белой, черной и синей конструкторской бумаги. Я думаю, что стоит поэкспериментировать со всеми тремя цветами и выбрать для себя лучший. Приклейте бумагу к стене и поставьте вашу модельку перед ней на пол на какую-нибудь подставку. Для начала это вполне подойдет, но будет немного неудобно, потому что вам придется много ползать по полу на коленях.
Я думаю, что окончательно студия должна выглядеть так, как это показано на рисунке 8.12. Это должен быть стол того же цвета, что и параболически изогнутый; экран фона. Вращающийся экран должен быть закреплен на шарнирах, чтобы вы могли изменять его наклон и положение вместе с наклоном и положением объекта. Более того, желательно иметь две камеры, расположенные под углом друг к другу, чтобы снимать объект одновременно в двух разных ракурсах.
Создание внешних ссылок
Когда вы пишите модуль на Си, в котором встречаются переменные или функции, определенные в других модулях, вы должны использовать ключевое слово EXTERN, сообщающее компилятору, что переменные или функции будут определены позже (па этапе компоновки). MASM 5.0 и более старшие версии также поддерживают эту возможность.
В наших ассемблерных функциях может понадобиться передать значение глобальной переменной обратно в вызывающую Си программу. В принципе, мы можем ее передавать как параметр каждый раз и не думать о внешних переменных. Но данный способ критичен по времени и весьма архаичен. В видеоиграх, как ни в каких других программах, мы должны использовать как можно больше глобальных переменных. Причина очевидна — скорость выполнения. Нет смысла терять драгоценные такты процессора на выполнение команд PUSH и POP в момент вызова функций.
Синтаксис директивы EXTRN следующий:
EXTRN symbol: type, symbol: type,...
где symbol — имя переменной, a type — ее размер (например, BYTE, WORD, DWORD).
Директива EXTRN разрешает разместить переменную в вашем Си-коде и получить к ней доступ через параметры. Это имеет и обратную сторону: переменная, обозначенная как EXTRN означает, что она занимает текущий сегмент данных, адресуемых через регистр DS. Если вы все будете делать в модели SMALL или MEDIUM, то не стоит беспокоиться, если же вы работаете в модели LARGE, то никто не гарантирует, что вы получите доступ к вашей глобальной переменной, используя текущее значение регистра DS. Чтобы избежать этого, всегда применяйте модели SMALL и MEDIUM.
Давайте для примера напишем процедуру, которая складывает два целых числа и помещает результат в третье. Фокус заключается в том, что все эти величины будут глобальными и по отношению к ассемблерной процедуре - внешними. Листинг 2.6 демонстрирует код Си для этой программы, а в Листинге 2.7-показана ее реализация на ассемблере.
Листинг 2.6. Си-часть примера.
#include <stdio.h>
int first = 1, second = 2, third = 0;
// Это те числа, с которыми
// мы хотим работать
void main (void)
{
printf ("\nBefore adding third = %d\ third);
Add_Ext();
//вызываем ассемблерную процедуру
printf("\nAfter adding third = %d",third);
} // конец функции main
Листинг 2.7. Ассемблерная часть примера.
.MODEL MEDIUM ; будем использовать MEDIUM модель
EXTRN first:WORD, second:WORD, third:WORD
.CODE ; начало кодового сегмента
_Add_Ext PROC FAR ;процедура имеет тип FAR (дальняя)
mov AX, first ; помещаем первое число в аккумулятор
add AX, second ; прибавляем второе число
mov third, AX ; помещаем результат в переменную third
_Add_Ext ENDP ; конец процедуры
END ; конец кодового сегмента
Листинги 2.6 и 2.7 - это примеры использования внешних переменных first, second и third. Программа вызывает Add_Ext, которая складывает переменные first и second и сохраняет результат в third. Это подводит нас к теме возврата результатов обратно в программу на Си, которая вызывала ассемблерную процедуру.
Создание звездного неба с использованием целых чисел
Теперь поговорим о том, как можно создать звездное небо используя только целые числа. Мы могли бы для получения более точной картины использовать числа с плавающей запятой, такие, например, как 2,5. Однако мы знаем, что это снижает производительность системы. Нам же требуется создать и визуализировать 50-100 звезд как можно быстрее, чтобы остальное время работы процессора потратить на масштабирование спрайтов и развитие сюжета игры.
Если создавать звездное небо, привлекая только целые числа как значения скоростей звезд, то едва ли вы заметите количественную ошибку, обусловленную заменой чисел с плавающей запятой на целые.
Конечно, мы могли бы принять компромиссный вариант и использовать числа с фиксированной запятой, но в данном случае овчинка выделки не стоит. Я считаю, что при достаточно высоких скоростях звезд потеря дробной части числа несущественна. Наверное, я уже утомил вас, повторяя одно и то же: в компьютерных играх все и всегда должно быть реализовано самым простым и быстрым путем. С другой стороны, такое примитивное на первый взгляд оформление игры, как экран, усыпанный звездами, может отнять у программиста удивительно много времени и сил. Но не забывайте, что отличное оформление - залог того, что игрок погрузится в игру с головой!
Листинг 8.3 содержит полный текст программы, позволяющей перемещать космический корабль по трехмерному звездному небу (отметим, что сам звездолет находится в плоскости X-Z). Для перемещения корабля используется вспомогательная цифровая клавиатура. Клавиши Left и Right вращают корабль, а клавиши Up и Down замедляют и ускоряют его движение. Для выхода из .программы нажмите клавишу Q.
При компоновке исполняемого файла вы должны объединить модуль FINVYREN.C с графической библиотекой GRAPHICS.С.
Листинг 8.3. Последний Шаттл (FINVYREN.C).
// ВКЛЮЧАЕМЫЕ ФАЙЛЫ//////////////////////////////
#include <io.h>
#include <conio.h>
#include <stdio.h>
#include <stdlib.h>
#include <dos.h>
#include <bios.h>
#include <fcntl.h>
#include <memory.h>
#tinclude <malloc.h>
#include <math.h>
#include <string.h>
#include <graph.h>
#include "grapics.h" // включаем нашу графическую библиотеку
// прототипы /////////////////////////////////////
void Create_Scale_Data_X(int scale, int far *row) ;
void Create_Scale_Data_Y(int scale, int *row);
void Build_Scale_Table(void);
void Scale_Sprite(sprite_ptr sprite,int scale);
void Clear_Double_Buffer(void);
void Timer(int clicks);
void Init_Stars(void) ;
void Move_Stars(void);
void Draw_Stars(void) ;
// ОПРЕДЕЛЕНИЯ /////////////////////////////////////////////
#define NUM_STARS 50 // количество звезд на небе
#define MAX_SCALE 200 // максимальный размер спрайта
#define SPRITE_X_SIZE 80 // размеры текстуры спрайта
#define SPRITE_y_SIZE 48
// СТРУКТУРЫ /////////////////////////////////////////////// // это звезда
typedef struct star_typ
{
int x,y; // позиция звезды
int xv,yv; // скорость звезды
int xa,ya; // ускорение звезды
int color; // цвет звезды
int clock; // число "тиков", которое звезда существует
int acc_time; // количество "тиков" до ускорения звезды
int acc_count; // счетчик ускорения
} star, *star_ptr;
// ГЛОБАЛЬНЫЕ ПЕРЕМЕННЫЕ ///////////////////////////////////
unsigned int far *clock = (unsigned int far *)0х0000046CL;
// указатель на внутренний таймер 18.2 "тик"/с
sprite object;
// обобщенный спрайт, который содержит кадры корабля
pcx_picture text_cells; // файл PCX с кадрами корабля
int *scale_table_y[MAX_SCALE+l] ;
// таблица предварительно рассчитанных коэффициентов масштабирования
int far *scale_table_x[MAX_SCALE+l];
// таблица предварительно рассчитанных коэффициентов масштабирования
star star_field[NUM_STARSj; // звездное небо
// ФУНКЦИИ //////////////////////////////
void Timer(int clicks)
{ // эта функция использует внутренний таймер с частотой 18.2 "тик"/с
// 32- битовое значение этого таймера находится по адресу 0000:046Ch
unsigned int now;
// получаем текущее время
now = *clock;
// Ожидаем до истечения указанного периода времени.
// Заметьте, что каждый "тик" имеет длительность примерно 55 мс.
while(abs(*clock - now) < clicks){}
} // конец Timer
////////////////////////////////////
void Init_Stars(void)
{
// эта функция инициализирует структуру данных звезд
// при старте программы
int index,divisor;
for (index=0; index<NUM_STARS; index++)
{
star_field[index].x = 150 + rand() % 20;
star_field[indexl.у = 90 + rand() % 20;
if (rand()%2==1)
star_field[index].xv = -4 + -2 * rand() % 3;
else
star_field[index].xv = 4 + 2 * randO % 3;
if (rand()%2==1)
star_field[index].yv = -4 + -2 * rand() % 3;
else
star_field[index].yv = 4 + 2 * randO % 3;
divisor = 1 + rand()%3;
star_field[index].xa = star_field[index].xv/divisor;
star_field[index] .ya = star_field [index] .yv/divisor;
star_field[index].color = 7;
star_field[index].clock = 0;
star_field[index].acc_time = 1 + rand() % 3;
star_field[index].acc_count = 0;
} // конец цикла
} // конец Init Stars
////////////////////////////////////////////////////////////
void Move_Stars(void) {
// Эта функция перемещает звезды и проверяет, не вышла ли звезда
// за пределы экрана. Если да, звезда создается вновь.
int index,divisor;
for (index=0; index<NUM STARS; index++)
{
star_field[index].x += star_field[index].xv;
star field[index].y += star_field[index].yv;
// проверка выхода звезды за пределы экрана
if(star_field[index].x>=SCREEN_WJDTH || star_field[index].x<0 ||
star_field[index].y>=SCREEN_HEIGHT || star_field[index].y<0)
{
// восстановление звезды
star_field[index].x = 150 + rand() % 20;
star_field[index].у = 90 + randf) % 20;
if (rand()%2==l)
star_field[index].xv = -4 + -2 * rand() % 3;
else
star_field[index] .xv = 4 + 2 * rand() % 3;
if (rand()%2==l)
star_field[index].yv = -4 + -2 * rand() % 3;
else
star_field[index].yv = 4 + 2 * rand() % 3;
divisor = 1 + rand()%3;
star_field[index].xa = star_field[index].xv/divisor;
star_field [index] .ya = star_field.[index] .yv/divisor;
star_field[index].color = 7;
star_field[index].clock = 0;
star_field[index].acc_time = 1 + rand() % 3;
star field[index].ace_count = 0;
} // конец оператора if
// не пора ли ускорить движение звезды
if (++star_field[index].acc_count==star_field[index].асc_time)
{
// обнуляем счетчик
star_field[indexl.acc_count=0;
// ускоряем
star_field[index].xv += star field[index].xa;
star_field[index].yv += star_field[index].ya;
} // конец оператора if
//не пора ли изменить цвет звезды
if (++star_field[index].clock > 5)
{
star_field[index].color = 8;
} // конец оператора if (> 5)
else
if (star_field[index].clock > 10)
{
star_field[index].color =255;
} // конец оператора if (> 10)
else
if (star_field[index].clock> 25)
{
star_field[index].color = 255;
} // конец оператора if (> 25)
} // конец цикла
} // конец Move_Stars
//////////////////////////////////////////////
void Draw_Stars(void)
{
// эта функция рисует звезды в дублирующем буфере
int index;
for (index=0; index<NUM_STARS; index++)
{
Plot_Pixel_Fast_D(star_field[index].х,star_field[index].y, (unsigned char)star_field[index].color) ;
} // конец
цикла
} // конец Draw_Stars
////////////////////////////////////////////////////////////
void Create_Scale_Data_X (int scale, int far *row)
{
// эта функция масштабирует полосу текстуры всех возможных
// размеров и создает огромную таблицу соответствий
int х;
float x_scale_index=0, х_scale_step;
// рассчитываем шаг масштабирования или число исходных пикселей
// для отображения на результирующее изображение за цикл
x_scale_step = (float) (sprite_width)/(float)scale;
x_scale_index+=x_scale_step;
for (x=0; x<scale; x++) {
// помещаем данные в массив для последующего использования
row[x] = (int) (x_scale_index+.5);
if (row[x] > (SPRITE_X_SIZE-1)) row[x] = (SPRITE_X_SI2E-1);
// рассчитываем следующий индекс
х_scale_index+=x_scale_step;
} // конец
цикла
} // конец Create Scale_Data X
////////////////////////////////////////////////////////////
void Create_Scale_Data_Y(int scale, int *row)
{
// эта функция масштабирует полосу текстуры всех возможных
// размеров и создает огромную таблицу соответствий
int у;
float y_scale_index=0, y_scale step;
// рассчитываем шаг масштабирования или число исходных пикселей
// для отображения на результирующее изображение за цикл
y_scale_step = (float)(sprite_height)/(float)scale;
y_scale_index+=y_scale_step;
for (у=0; y<scale; y++)
{ // помещаем данные в массив для последующего использования
row[y] = ((int)(y_scale_index+.5)) * SPRITE_X_SIZE;
if (row[y] > (SPRITE_Y_SIZE-1)*SPRITE_X_SIZE) row[y]] = (SPRITE_Y_SIZE-1)*SPRITE_X_SIZE;
// рассчитываем следующий индекс
y_scale_index+=y_scale_step;
} // конец
цикла
} // конец Create_Scale_Data_Y ////////////////////////////////////////
void Build_Scale_Table(void)
{
// эта функция строит таблицу масштабирования путем расчета
// коэффициентов масштабирования для всех возможных размеров
// от 1 до 200 пикселей
int scale;
// резервируем
память
for (scale=1; scale<=MAX_SCALE; scale++)
{
scale_table_y[scale] = (int *)malloc(scale*sizeof(int)+1);
scale_table_x[scale] = (int far *)_fmalloc(scale*sizeof(int)+1);
} // конец цикла
// создаем Таблицу масштабирования для осей Х и У
for (scale=l; scale<=MAX_SCALE; scale++)
{
// рассчитываем коэффициент для данного масштаба
Create_Scale_Data_Y (scale, (int *)scale_table_y[scale]);
Create_Scale_Data_X (scale, (int far *)scale_table x[scale]);
} // конец
цикла
} // конец Build_Scale_Table
////////////////////////////////////////////////////////////
void Scale_Sprite(sprite_ptr sprite,int scale)
{
// эта функция масштабирует спрайт (без отсечения). Масштабирование
// производится с использованием заранее рассчитанной таблицы,
// которая определяет, как будет изменяться каждый вертикальный
// столбец. Затем другая таблица используется для учета
// масштабирования этих столбцов по оси Х
char far *work_sprite; // текстура
спрайта
int *row_y; // указатель на масштабированные
// по оси У данные (заметьте, что
// это ближний указатель)
int far *row_x;. // указатель на масштабированные
// по оси Х данные (заметьте, что
// это дальний указатель)
unsigned char pixel; // текущий
текстель
int x, // рабочие переменные
y,
column, work_offset, video_offset, video_start;
// если объект слишком мал, то и рисовать его не стоит
if (scale<l) return;
// рассчитываем необходимые для масштабирования данные
row_y = scale_table_y[scale];
row_x = scale_table_x[scale];
// выбираем соответствующий кадр спрайта
work_sprite = sprite->frames[sprite->curr_frame];
// рассчитываем начальное смещение
video_start
= (sprite->y << 8) + (sprite->y << 6) + sprite->x;
// изображение рисуется слева направо и сверху вниз
for (х=0; x<scale; x++)
{
// пересчитываем адрес следующего столбца
video_offset = video_start + x;
// определяем, какой столбец должен быть отображен,
// исходя из индекса масштабирования по оси Х
column = row_x[x];
// Наконец рисуем столбец обычным образом
for (y=0; y<scale; у++)
{
// проверка на "прозрачность"
pixel = work_sprite[work_offset+column];
if (pixel)
double_buffer[video_offset] = pixel;
// индекс следующей строки экрана и смещение в
// области хранения текстуры
video_offset
+= SCREENJHDTH;
work_offset = row_y[y];
} // конец цикла по Y
} // конец цикла по Х
} // конец Scale_Sprite
////////////////////////////////////////////////////////////
void Clear_Double_Buffer(void)
{
// угадали что это?
_fmemset(double_buffer, 0, SCREEN_WIDTH * SCREEN_HEIGHT + 1) ;
} // конец Clear_Double_Buffer
// ОСНОВНАЯ ПРОГРАММА //////////////////////////////////////
void main(void)
{
// корабль помещается в космическое пространство и игрок
// может перемещать его среди звезд
int done=0, // флаг
выхода
scale=64,
direction=6; // направление корабля (текущий кадр)
float sсale_distance = 24000,
view distance
= 256,
// произвольные константы для согласования масштабирования
// плоской текстуры в трассированном пространстве
х=0, // позиция корабля в пространстве
у=0,
z=1024,
xv=0,zv=0, // скорость корабля в плоскости Х-Z
angle=180,
// угол поворота корабля
ship_speed=10; // величина скорости корабля
// установка видеорежима 320х200х256
_setvideomode(_MRES256COLOR) ;
// все спрайты будут иметь этот размер
sprite_width = 80;
sprite_height = 48;
// создание таблицы соответствия для подсистемы масштабирования
Build_Scale_Table();
// инициализация
файла PCX, который
содержит все
кадры
PCX_Init((pcx_picture_ptr)&text_cells);
// загрузка файла PCX, который содержит все кадры
PCX_Load("vyrentxt.pcx", (pcx_picture_ptr)&text_cells,1) ;
// резервирование памяти для дублирующего буфера
Init_Double_Buffer();
// инициализация звездного неба
Init_Stars() ;
// установка направления и скорости корабля
angle=direction*30+90;
xv = (float)(ship_speed*cos(3.14159*angle/180));
zv = (float)(ship_speed*sin(3.14159*angle/180));
Sprite_Init((sprite_ptr)&object,0,0,0,0,0,0);
// загрузка 12 кадров космического корабля
PCX_Grap_Bitmap((pcx_picture_ptr)&text_cells, (sprite_ptr)&object,0,0,0) ;
PCX_Grap_Bitmap((pcx_picture_ptr)&text_cells, (sprite_ptr)&object, 1, 1, 0);
PCX_Grap_Bitmap((pcx_picture_ptr)&text_cells,
(sprite_ptr)&object,2,2, 0) ;
PCX_Grap_Bitmap ((pcx_picture_ptr) &text_cells,
(sprite_ptr)Sobject,3,0,1) ;
PCX_Grap_Bitmap((pcx_picture_ptr)&text cells,
(sprite_ptr)&object,4,1,1);
PCX_Grap_Bitmap((pcx_picture_ptr)&text cells,
(sprite_ptr)&object,5,2,1) ;
PCX_Grap_Bitmap ((pcx_picture_ptr) &text_cells,
(sprite_ptr)&object,6,0,2) ;
PCX_Grap_Bitmap ((pcx_picture_pfcr) stext_cells,
(sprite_ptr)&object,7,1,2);
PCX_Grap_Bitmap((pcx picture ptr)&text cells,
(sprite_ptr)Sobject,8,2,2);
PCX_Grap_Bitmap((pcx_picture_ptr)&text_cells,
(sprite_ptr)&object,9,0,3);
PCX_Grap_Bitmap((pcx_picture_ptr)&text_dells,
(sprite_ptr)&object,10,1,3) ;
PCX_Grap_Bitmap ((pcx_picture_ptr) &text_cells,
(sprite_ptr)&object,11,2,3);
// инициализация положения корабля
object.curr_frame = 0;
object.x = 0;
object.у =0;
Clear_Double_Buffer() ;
// отслеживание действий игрока и прорисовка корабля
while(!done) {
// нажал ли игрок клавишу?
if (kbhit()) {
switch(getch()) {
case '4' // повернуть корабль влево
{
if (++direction==12)
{
direction==0;
} // конец оператора if
} break;
case'6': // повернуть корабль вправо
{
if (--direction < 0)
{
direction=11;
} // конец оператора if
} break;
case '8' : // ускорить корабль
{
if (++ship_speed > 20) ship_speed=20;
} break; case
'2': // замедлить корабль
{
if (--ship_speed < 0)
ship_speed=0;
} break;
case 'q': // выход из программы
{
done=l;
} break;
default: break;
} // конец
оператора
switch
// векторы направления и скорости
angle=direction*30+90;
xv = (float)(ship_speed*cos(3.14159*angle/180));
zv = (float)(ship_speed*sin(3.14159*angle/180)};
} // конец оператора if
// переместить корабль
x+=xv;
z+=zv;
// ограничить по ближней плоскости
if (z<256)
Z=256;
// рассчитать размер изображения
scale = (int)( scale_distance/z );
// на основании размера изображения
// рассчитать проекции Х и Y
object.х = (int) ((float)x*view_distance / (float)z) + 160 - (scale>>1);
object.у = 100 - (((int) ( (float) y*view_distance / (float)z) + (scale>>1)) );
// ограничить рамками экрана if
(object.х < 0)
object.х
= 0;
else if (object.x+scale >= SCREEN_WIDTH)
object.x = SCREEN_WIDTH-scale;
if (object.у < 0) object.у
= 0;
else
if (object.y+scale >= SCREEN_HEIGHT)
object.у
= SCREEN_HEIGHT-scale;
// выбрать
нужный кадр
object.curr_frame = direction;
// очистить дублирующий буфер
Clear_Double_Buffer() ;
Move_Stars();
Draw_Stars();
// масштабировать спрайт до нужного размера
Scale_Sprite((sprite_ptr)&object,scale) ;
Show_Double_Buffer(double_buffer);
// вывести на экран информацию для игрока
_settextposition(23,0) ;
printf("Position=(%4.2f,%4.2f,%4.2f) ",x,y,z) ;
// немного подождать
Timer(1) ;
} // конец while
// удалить файл
PCX PCX_Delete((pcx_picture_ptr)&text_cells) ;
// восстановить текстовый режим
_setvideomode(_DEFAULTMODE);
} // конец функции main
Как и прежде, в теле самой программы располагаются только вызовы функций. Это сделано для облегчения понимания работы алгоритма. При написании реальной, а не демонстрационной программы вам не обязательно поступать так же. Если вы сделаете свою программу чересчур модульной, то может оказаться, что время, потраченное на вызов некоторых функций, станет больше времени их выполнения!
Теперь, после того, как мы узнали математику и механику вывода на экран преобразований трехмерных спрайтов, давайте посмотрим, как я создавал артинки для программ этой главы.
Специальные эффекты
Под специальными эффектами обычно понимают такие зрительные эффекты, которые, выходят за рамки обычного и хорошо запоминаются. Современные компьютерные игры все больше и больше походят на настоящее кино и имеют весьма впечатляющую графику. Однако всегда можно найти что-то еще, что заставит игрока удивится. Поэтому попытаемся добиться от него возгласа:
"Ого!!!"
Специальные эффекты могут быть какими угодно, начиная от ярких взрывов и кончая сверхъестественными явлениями, психоделической графикой и всем-чем-хотите. Например, сейчас я работаю над компьютерной моделью системы частиц для того, чтобы монстр разлетался на биллионы маленьких атомов, несущихся каждый по своей орбите. Это должно выглядеть отлично! Я не уверен, что я сделаю это, но если мне это удастся, то это будет сводить с ума!
Специальные эффекты — это та область, в которой вы должны полностью Проявить себя, потому что это самая творческая часть любой компьютерной игры. Запомните одну вещь: в любой игре должен быть по-крайней мере один потрясающий воображение эффект, такой эффект, который игрок не скоро забудет! Итак, к делу, тем более что в этом разделе мы поговорим и о других важных вещах.
Способы воспроизведения звука
Рассмотрим способы, с помощью которых вы можете воспроизводить звук на персональных компьютерах.
Цифровой звук. Выпуск фирмой Creative Labs платы Sound Blaster заложил фундамент для использования оцифрованных звуков персональными компьютерами. Все последующие звуковые карты (такие как Covox Speech Thing, The Walt Disney Sound Source или Media Vision ProAudio Spectrum) поддерживали эту возможность. Цифровой звук позволяет вашей программе исполнять все, что может быть записано с помощью микрофона, включая звуковые эффекты, человеческую речь и музыку. Конечно, цифровой звук требует огромных объемов памяти и дискового пространства, но воспроизведения в играх звуковых эффектов и диалогов очень эффективно.
Цифровой звук - это как раз то, что вы слышите при воспроизведении музыкальных записей с компакт-дисков. Звук представляет собой волну определенной формы. Звук, воспроизводимый динамиком, — это, фактически, напряжение, подаваемое на катушку динамика и заставляющее мембрану двигаться вперед и назад в соответствии с силой сигнала. Движения мембраны динамика генерируют в воздухе звуковую волну. И наоборот, звуковая волна может быть переведена в цифровую форму, пугем преобразования уровня напряжения в числовые значения. Этот процесс называется аналого-цифровым преобразованием или АЦП. Форма звуковой волны приближенно описывается некоторым множеством повторяющихся выборок. Таким образом, звуковая волна, преобразованная в цифровую форму, имеет две важные характеристики: число использованных битов для представления данных и частоту, с которой звуковая волна была оцифрована. На стандартный звуковой компакт-диск музыка записывается с разрешением 16 бит и частотой 44КГц в стереозвучании. Это значит, что каждая выборка имеет 32 бита и выборки производятся 44 тысячи раз в секунду. Таким образом, одна секунда звука на компакт-диске требует 176000 байт. Одна минута - уже свыше 10 мегабайт!
Обычно компьютерные игры используют звуковые данные со значительно более низким разрешением, так как проигрывание цифрового звука с качеством компакт-диска потребует всех ресурсов машины.
Как правило, игры имеют дело с 8-битными выборками на частоте 11 КГц. Но даже на такой частоте совсем короткий звук легко займет всю память машины. Так что, хотя цифровой звук и является лучшим способом создания разнообразных звуковых эффектов, таких как выстрелы и взрывы, использовать его для музыкального оформления компьютерных игр чересчур накладно.
Другая проблема заключается в том, что, хотя оцифровка звука и позволяет проигрывать музыкальные записи, с ее помощью практически невозможно интерактивно управлять музыкальным фоном в соответствии с текущим контекстом игры. Для устройств MIDI такой проблемы не возникает, так как воспроизведение осуществляется под полным контролем со стороны программного обеспечения. Другими словами, вы можете в реальном времени посылать компьютеру эквивалент нотной записи.
Частотные синтезаторы. Самой первой популярной звуковой картой для персонального компьютера была плата Adiib Personal Music System. Она содержала синтезатор частотной модуляции Yamaha YM3812 (OPL2) FM. Э".э квазипрограммируемое устройство способно с помощью осцилляторов формировать вид звуковой волны, используя частотную модуляцию, генератор белого шума и операторы нарастания, удержания, затухания и освобождения звука. Это звучит замысловато не только на словах, но и на деле. Устройство феноменально сложно программируется. Даже приложив всю вашу изобретательность при программировании частотного синтезатора, вам вряд ли удастся достичь приемлемых результатов. К счастью, с появлением нового поколения MIDI-синтезаторов с волновыми таблицами прямое программирование частотных синтезаторов уходит в прошлое. Существует „много .систем, позволяющих YM3812 эмулировать устройство MIDI, избавляя программистов от необходимости работать с замысловатой логикой этого устройства.
MIDI. Спецификация интерфейса электромузыкальных инструментов (MIDI) - это международный стандарт «де-факто», который определяет последовательный интерфейс для объединения музыкальных синтезаторов, музыкальных инструментов и компьютеров.
MIDI поддерживается Ассоциацией производителей MIDI (Лос-Анджелес, Калифорния). Он опирается на аппаратное (каналы ввода/вывода и соединительные кабели) и программное обеспечение (тон, громкость и так далее). В соответствии со спецификацией, принимающее устройство в MIDI-системе должно интерпретировать музыкальные данные даже в том случае, если у передающего устройства нет возможности узнать, что способно делать принимающее устройство. Это чревато проблемами, если приемник не способен корректно интерпретировать данные. В обобщенном MIDI эта проблема решается предварительной идентификацией возможностей аппаратуры.
Все устройства обобщенного MIDI поддерживают звучание музыкальных и ударных инструментов, вместе со 128 звуковыми эффектами. Системы обобщенного MIDI поддерживают одновременное использование 16 MIDI-каналов с минимум 24 нотами в каждом и имеют определенный набор музыкальных контроллеров. Это означает, что в случае обобщенного MIDI передающее устройство знает, что ожидать от принимающего. Точно так же и файл, созданный одним устройством, соответствующим спецификациям обобщенного MIDI, распознается любым другим подобным устройством без потери нот или нарушения баланса инструментов при воспроизведении. Это, однако, справедливо только в теории. На практике же файл MIDI обычно имеет характеристики, существенно зависящие от конкретного устройства. Это обусловлено тем, что если сами инструменты и стандартизованы в обобщенном MIDI, то их баланс, тембр и качество — нет. Таким образом, каждый обобщенный MIDI-инструмент играет с разными характеристиками. При написании произведения MIDI для качественного исполнения на любом устройстве композитору не следует использовать специфические особенности конкретного синтезатора. Так как файлы MIDI обычно небольшие, то я, чтобы полностью использовать все возможности каждого синтезатора, обычно делаю различные версии для наиболее популярных MIDI-синтезаторов.
Большинство поставщиков придерживаются обобщенного стандарта MIDI, в том числе Roland (Roland Sound Canvas, Roland RAP-10), Creative Labs (AWE 32), Logitech- (Logitech SoundWave), Ensoniq (SoimdScape), Gravis (Gravis Ultrasound), Turtle (Turtle Beach Maui), Sierra Semiconductor (Aria).
Кроме того, устройства с частотными синтезаторами, такие как Sound Blaster, также могут эмулировать устройства MIDI с помощью набора драйверов типа MIDPAK. Видимо, в будущем вся интерактивная музыка будет придерживаться стандарта .MIDI. Благодаря этому вы сможете качественно воспроизводить с помощью разнообразных звуковых карт полнооркестровые произведения, созданные для вас профессиональными композиторами. Файлы данных MIDI небольшие, требуют сравнительно небольшого числа прерываний для обслуживания и не сильно загружают процессор. Большинство современных MIDI-синтезаторов используют алгоритмы. Алгоритмы — это оцифрованные записи реальной игры на разнообразных инструментах. Алгоритмы хранятся в постоянном запоминающем устройстве синтезатора. И когда MIDI-файл описывает, например, звучание пианино, то используется оцифрованный звук настоящего пианино. Благодаря этому мы получаем удивительно реалистично звучащую музыку. Полный набор алгоритмов, включающий в себя хоровые эффекты и эффекты реверберации, придаст вашей музыке дополнительный колорит.
MOD. Файлы MOD стали популярны благодаря компьютерам Amiga фирмы Commodore. Феноменальные для своего времени возможности этого игрового компьютера просто поражали. Аппаратное обеспечение этой машины поддерживало четыре канала цифрового звука. Многие создатели игр, сотрудничавшие с фирмой Amiga, хотели, чтобы в их играх звучала музыка. Поэтому они придумали схему, позволяющую получать полнооркестровую музыку, а файлы, содержащие звуковые данные, получили наименование MOD. Практически, этот метод базируется на программной эмуляции частотного синтеза с использованием алгоритмов (о них мы рассказывали выше в разделе MIDI). Надеюсь, вы еще помните, что алгоритмы содержат оцифрованную запись игры на разнообразных инструментах и хранятся в ПЗУ. Когда требуется воспроизвести звук пианино, то модулируется частота настоящей, исполненной на фортепиано, оцифрованной ноты. При использовании технологии MOD-файлов, делается все то же самое, только программным путем.
Это требует достаточного количества оперативной памяти для хранения оцифрованных звуковых эффектов и программного обеспечения для частотной модуляции в реальном времени. Такая задача отнимает много памяти и ресурсов процессора. Кроме того, написание музыки в виде MOD-файлов значительно более трудоемкая задача, чем создание композиций для MIDI.
Так что, хотя файлы MOD и являются наиболее полным, наиболее реалистичным методом создания музыки для простых устройств типа Sound Blaster, они все же не могут соревноваться с настоящим синтезатором MIDI. Кроме того, когда цифровой канал звуковой платы занят исполнением музыки, усложняется воспроизведение шумовых эффектов. Так как современный рынок движется в сторону MIDI-синтеза, наиболее разумным будет писать музыку в формате MIDI, а фоновые и шумовые эффекты воспроизводить с помощью оцифрованных звуков. Пакеты программ MIDPAK и DIGPAK как раз и предназначены для этих целей.
Красная книга компакт-дисков.
Одно из преимуществ дисководов CD-ROM, которые становятся все популярнее, состоит в том, что они могут проигрывать стандартные компакт-диски. Однако вы не можете сделать так, чтобы ваша игра одновременно и работала с диска CD-ROM и воспроизводила музыку оттуда же. Доступ к записанным на CD-ROM данным и к звуковым дорожкам является полностью взаимоисключающим. Также невозможно добиться плавной работы программы при постоянном переходе от данных к звуку и обратно. Однако многие разработчики любят помещать музыкальные фрагменты из игр на звуковые дорожки CD-ROM, и вы также можете взять это себе на вооружение.
Программное обеспечение цифрового микшироваиия.
За исключением Gravis Ultrasound и Creative Labs AWE32, почти все звуковые карты поддерживают только один канал для цифрового звука. Однако в интерактивной игре вам может потребоваться воспроизвести одновременно несколько звуков. Этого вы можете добиться, применяя программное обеспечение, которое осуществляет цифровое микширование. Так как звук аддитинен, этот процесс удивительно прост.
Все необходимые звуки, которые должны звучать в данный момент, складываются в буфере, отсекаются точки, вышедшие за установленные пределы, а затем результат передается звуковой карте. Некоторые инструментальные пакеты для разработки программного обеспечения включают поддержку программного цифрового микширования в свои программные интерфейсы.
Загружаемые алгоритмы инструментов и звуковых эффектов.
При использовании звуковых карт Gravis Ultrasound и Creative Labs AWE32 программа может загружать музыкальные инструменты или эффекты в память самой платы. После этого вы можете манипулировать ими, простоинициируя события MIDI. Это очень мощный метод, потому что он позволяет не только осуществлять поддержку многоканальности и использовать оригинальные инструменты и эффекты при низкой загрузке процессора и экономном расходовании памяти. Данный метод еще дает возможность манипулировать звуковыми эффектами в реальном времени, применяя сдвиг высоты звука, управление ударными инструментами и даже хоровые эффекты и эффекты реверберации.
Спрайты
Вы можете спросить: «Что такое спрайт?». Знаете, есть такой газированный напиток... Снова шучу. На самом деле спрайты - это такие маленькие объектики, которые находятся на игровом поле и могут двигаться. Этот термин
прижился с легкой руки программистов фирмы Atari и Apple в середине, 70-х годов. Теперь поговорим о спрайтах и их анимации. В будущем мы еще вернемся к этой теме в седьмой главе, «Продвинутая битовая графика и специальные эффекты. Именно с этой мыслью я создал несколько небольших спрайтов, которые мы будем использовать в дальнейшем.
Спрайты - это персонажи в играх для ПК, которые могут без труда перемещаться по экрану, изменять цвет и размер. Все это звучит как мечта программиста. Но надо помнить, что в IBM-совместимых компьютерах нет спрайтов! Во нормальных компьютерах существует аппаратная поддержка спрайтов. Такие машины как Atari, Amiga, Commodore и последние модели Apple имеют эту возможность, а вот ПК - нет. Поэтому мы вынуждены делать это самостоятельно.
М-да. Нам будет чем заняться.
Конечно, мы не станем заниматься разработкой аппаратной поддержки спрайтов. Все, что нам нужно, это понять, каким образом помещать образ на экран, сохраняя при этом возможность его перемещений и видоизменений. Поскольку спрайт — это довольно сложный объект, то стоит подумать о том, как это реализовать на программном уровне. Мы вовремя об этом заговорили:. вспомните разработку игры «Астероиды».
Вот что нам надо:
§
Мы должны уметь извлекать матрицу пикселей из загруженного РСХ-образа и сохранять ее в буфере, связанном со спрайтом;
§ Более того, хотелось бы считывать сразу несколько образов из PCX-файла и загружать их в массив, связанный с одним спрайтом. Это позволит нам оптимизировать программу по скорости выполнения.
Рисунок 5.10 показывает последовательность кадров, которые приводят в движение ковбоя. Мы воспользуемся ею позже.
После того как мы загрузим данные из PCX-файла, нам необходимо иметь возможность показывать спрайт в любой позиции на экране.
Делать это нужно осторожно, поскольку запись пикселей в видеобуфер разрушает то, что было на их месте. Поэтому, мы должны уметь сохранять ту часть изображения, которая окажется закрыта спрайтом, чтобы в дальнейшем иметь возможность восстановить первоначальный вид экрана.
Давайте на этом Месте остановимся и поговорим чуть-чуть об анимации. В играх для ПК применяется два способа обновления экрана:
§ Мы можем перерисовывать весь экран целиком, как это сделано; в игре Wolfenstein 3D;
§ Можно перерисовывать лишь участки экрана.
Какой из способов лучше выбрать, зависит от типа игры. Если мы перерисовываем весь экран, то это нужно делать по возможности быстро, поскольку 64000 пикселей - все же довольно много. Если мы перерисовываем только участки экрана, то желательно быть уверенным, что фон после прохождения спрайта не изменится. Поскольку все игры для ПК отличаются друг от друга, то для решения конкретных специфических задач всегда надо выбирать наиболее подходящую технику.
Давайте рассмотрим способ, которым мы будем пользоваться в настоящей главе - это перерисовка участков экрана. Посмотрите на рисунок 5.11, чтобы представить последовательность событий, позволяющих спрайту правильно перемещаться по экрану.
Теперь, когда мы знаем что делать, надо попробовать это реализовать. Для начала создадим структуру данных спрайта. Листинг 5.11 содержит необходимый для этого код.
Листинг 5.11. Структура спрайта с полями для анимации.
typedef struct sprite_typ
{
int x,y; // текущая позиция спрайта
int x_old, y_old; // предыдущая позиция спрайта
int width,height; // размеры спрайта
int anim_clock; // время анимации
int anim_speed; // скорость анимации
int motion_speed; // скорость движения
int motion_clock; // время
движения
char far *frames [MAX_SPRITE_FRAMES] ; // массив
указателей
//на кадры
int curr_frame; // отображаемый кадр
int num_frames; // общее число кадров
int state; // статус спрайта
char far *background; // фон
под спрайтом
}sprite, *sprite_ptr;
Структура спрайта имеет поля для сохранения позиции и размеров образа и несколько других элементов. Сейчас мы уже готовы написать функцию для работы со спрайтами.
Прежде всего мы должны извлечь битовую карту из PCX-файла и поместить ее в массив, хранящий образы спрайта. Если вы помните, я создал файл в формате PCX (CHARPLATE.PCX), в который вы можете дорисовать свои картинки и героев. Функция, извлекающая битовые карты из РСХ-образа подразумевает, что вы создали свои образы с помощью этого файла. Программа из Листинга 5.12 позволяет перемещать спрайт, который вы хотите изменить, в указанные координаты.
Листинг 5.12. Функция извлечения спрайта из загруженного PCXфайла.
void PCX_Grap_Bitmap(pcx_picture_ptr image, sprite_ptr sprite, int sprite_franie, int grab_x, int grab_y)
{
// функция выделяет одно изображение из буфера в который
// загружен PCX-файл
// функция исходит из предположения, что в действительности массив
// пикселей размером 320х200 разделен на отдельные изображения
// размером 24х24 пикселя
int x_off,y_off, х,у, index;
char far *sprite_data;
// вначале выделяем память для хранения спрайта в структуре спрайта
sprite->frames[sprite_frame] = (char far *)malloc(SPRITE_WIDTH * SPRITE_HEIGHT);
// создаем альтернативный указатель на эту область памяти
// для
ускорения доступа
sprite_data = sprite->frames[sprite_frame];
// теперь перемещаем битовый образ спрайта из области PCX-файла
// в выделенную память
// мы должны выбрать, какой именно спрайт мы копируем
// помните, что в действительности файл представляет собой массив.
// 12х8 элементов, каждый из которых имеет размер 24х24 пикселя.
// Индекс (0,0) соответствует верхнему левому углу спрайта,
// (11,7) - нижнему правому
х_off = 25 * grab_x
+ 1;
у_off = 25 * grab_y + 1;
// вычисляем начальный адрес
y_off
=y_off * 320;
for (y=0; y<SPRITE_HEIGHT; y++)
{
for (x=0; x<SPRITE_WIDTH; X++)
{
// получить очередной байт текущей строки и поместить его
//в следующую позицию буфера спрайта
sprite_data[у*24 + х] = image->buffer[y_off + x_off + х];
} // конец копирования строки
// перейти к следующей строке
y_off+=320;
} // конец копирования
// инкрементировать счетчик кадров
sprite->num_frames++;
} // конец функции
Эта функция по указателю на спрайт определяет его расположение в загруженном файле. Далее она выделяет память для хранения образа и инициализирует структуру данных (я решил делать спрайты размером 24 на 24 пикселя, но вам ничто не мешает изготавливать любые другие спрайты). Теперь, когда у нас подготовлены образы, следующим шагом будет их отображение на экране монитора. Для этого нам надо:
§ Вычислить начальную позицию спрайта согласно его координатам (х,у);
§ Преобразовать полученные координаты в адрес видеобуфера;
§ Переместить байты изображения в видеобуфер.
Для рисования спрайта мы должны выполнить все операции с текущим кадром анимации. Код в Листинге 5.13 делает все перечисленное.
Листинг 5.13. Функция рисования спрайта.
void DrawSprite (sprite_ptr sprite)
{
// функция, рисующая спрайт на экране строка за строкой,
// очень быстро. Вместо умножения используется сдвиг
char far *work_sprite;
int work_offset=0,offset,x,у;
unsigned char data;
// создаем альтернативный указатель на спрайт для ускорения доступа
work_sprite = sprite->frames[sprite->curr frame];
// вычислить смещение спрайта в видеобуфере
offset = (sprite->y << 8) + (sprite->y << 6) + sprite->x;
for (y=0; y<SPRITE_HEIGHT; y++)
{
for (x=0; x<SPRITE_WIDTH; x++)
{
// Проверка на "прозрачный" пиксель (с кодом 0).
Если пиксель
// "непрозрачный" - выводим его на экран.
if ((data=work_sprite[work_offset+x])) video_buffer[offset+xj = data;
} // конец вывода строки
// перейти к следующему ряду пикселей в видеобуфере
//в буфере
спрайта
offset += SCREEN_WIDTH;
work_offset += SPRITE_WIDTH;
} // коней вывода спрайта
} // конец функции
Эта функция работает примерно так же, как и Plot_Pixel_Fast из Листинга 5.5. Сначала вычисляется стартовый адрес расположения спрайта, а затем все его байты строка за строкой переписываются в видеобуфер.
Следует учесть, что здесь потребуется некоторая оптимизация. Почему бы не использовать функцию memcpy, чтобы копировать всю строку разом (а всего 24 строки)? Однако этого сделать нельзя, так как при выводе спрайта нам необходимо применить технику, использующую "прозрачные" пиксели. Что это дает? Взгляните на рисунок 5.12.
Спрайт слева выведен вместе с фоном, а при рисовании правого спрайта использовалась техника «прозрачных» пикселей. При этом пиксели с цветом фона (черный, имеющий код 0) пропускались, а прорисовывались только данные непосредственного изображения. Это и создает эффект «прозрачности» фона.
Следующая функция, которую я собираюсь вам предложить, будет сохранять фон перед тем, как мы выведем спрайт на экран. Помните, когда мы что-то записываем в видеобуфер, данные или образ, находящийся там, обязательно теряются. Поэтому мы и должны сохранять фон под спрайтом прежде чем поместим его в видеобуфер, чтобы позже восстановить прежний вид экрана. Код в Листинге 5.14 именно это и делает.
Листинг 5.14. Сохранение фона под спрайтом.
void Behind_Sprite(sprite_ptr sprite)
{ // функция сохраняет область видеопамяти, в которую будет
// выводиться
спрайт
char far *work_back;
in work_offset=0,offset,y;
// создаем альтернативный указатель для ускорения доступа
work_back = sprite->background;
// вычисляем смещение в видеобуфере
offset = (sprite->y << 8) + (sprite->y << 6) + sprite->x;
for (y=0; y<SPRITE_HEIGHT; y++)
{
// копируем строку видеопамяти в буфер
_fmemcpy((char far *)&work_back[work_offset], (char far *)&video_buffer[offset], SPRITE_WIDTH);
// переходим к следующей строке
offset += SCREEN_WIDTH;
work_offset += SPRITE_WIDTH;
} // конец цикла for
} // конец функции
Функция Behind_Sprite считывает матрицу размером 24х24, где должен находится спрайт. Данные после считывания находятся в поле background структуры спрайта. Это поле является указателем на область памяти, где находится спрайт.
Собственно, это все, что нам нужно для создания и перемещения маленьких образов по экрану. Для анимации мы должны изменять поле curr_frame в структуре спрайта, перед тем, как его начать рисовать. Мы обсудим процесс анимации в этой главе, но, я думаю, вы и сами догадываетесь, как это сделать: надо стереть спрайт, передвинуть его, снова нарисовать и т. д. Вот и все.
Средства связи видеоигр
К этому моменту вы уже должны быть готовы писать видеоигры. Вы от начала и до конца научились тому, что для этого необходимо знать. Однако мы до сих пор упускали один важный момент: как написать игру, чтобы в нее могли играть сразу несколько человек? Это нам покуда совершенно неизвестно.
Существуют видеоигры, поддерживающие модемное соединение, но большинство людей или не пользуются этой возможностью, или чувствуют, что она слишком сложна, чтобы к ней прибегать. В любом случае, вопрос в этой области все еще остается открытым и вам предстоит проделать немало работы, если действительно займетесь программированием игр.
Поскольку нас первоначально интересует написание видеоигр для потребителей, мы будем концентрировать наши усилия на использовании серийных портов как средства коммуникации (а не сетевые коммуникации типа IPX/SPX или NETBIOS). Я выбираю параллельный порт в отличие от использования нескольких Ethernet плат по следующим причинам:
§ Во-первых, у каждого компьютера есть свой параллельный порт;
§ Во-вторых, у многих людей есть свои модемы, с помощью которых они обладают основным средством для игры вдвоем.
Если мы собираемся писать видеоигры для двух или более игроков, то должны рассчитывать на это с самого начала. Вы не можете написать сложную игру и только потом вдруг решить, что делаете ее как игру для нескольких игроков. Необходимо помнить, что разработка двух независимых игр, которые запускаются и синхронно работают на двух различных ПК, требует приличных трудовых затрат и предварительного планирования.
Статус клавиш
Мы должны иметь возможность определять:
§ Была ли нажата какая-нибудь клавиша;
§ Какая была нажата клавиша;
§ Статус клавиши Shift.
Статус клавиш — это просто битовый вектор (последовательность), содержащий информацию о клавишах Shift, Alt, Ctrl и других. Эта последовательность находится в памяти по адресам 417h и 418h. Мы не будем читать эти ячейки напрямую, а воспользуемся BIOS и Си.
Листинг 3.4 содержит код, позволяющий получить статус клавиш.
Листинг 3.4. Получение статуса клавиш.
#define SHIFT_R 0х0001
#define SHIFT_L 0х0002
#define CTRL 0х0004
#define ALT 0х0008
#define SCROLL_LOCK_ON 0х0010
#define NUM_LOCK_ON 0х0020
#define CAPS_LOCK_ON 0х0040
#define INSERT_MODE 0х0080
#define CTRL_L 0х0100
#define ALT_L 0х0200
#define CTRL_R 0х0400
#define ALT_R 0х0800
#define SCROLL_LOCK_DWN 0х1000
#define NUM_LOCK_DWN 0х2000
#define CAPS_LOCK_DWN 0х4000
#define SYS_REQ_DWN 0х8000
unsigned int Get_Control_Keys(unsigned int mask)
{
// функция возвращает статус интересующей нас управляющей клавиши
return(mask &_bios_keybrd(_KEYBRD_SHIFTSTATUS));
} // конец функции
В листинге 3.4 функция Get_Control_Key() использует вызов BIOS из Си для определения статуса клавиш. В строки #define включены описания масок для определения статусных клавиш, благодаря чему вы можете вызывать функцию Get_Control_Key(), не задумываясь о значении битов состояния. Более того, используя маски и логический оператор AND, за один вызов можно получить сразу несколько состояний.
Стратегия игровых коммуникаций
Соединение двух ПК и запуск на них сетевой игры является комплексной задачей, не имеющей какого-то общего решения. Все зависит от конкретной цели, которую вы перед собой поставите и решение проблемы, скорее всего, будет меняться от игры к игре. Однако, существует несколько правил, которые всегда нужно принимать во внимание. Именно об этом мы и поговорим в ближайшее время.
В игре для двух участников, которая запускается на одном компьютере, оба игрока имеют равные шансы влиять на игровую ситуацию. Рисунок 14.4 показывает два различных представления этой разновидности игр.
Однако если такая игра запускается на разных машинах, взаимоотношения игроков с внутренним миром компьютеров представляется более сложным, и это отражено на рисунке 14.5.
Проблемы, возникающие при такой конфигурации, в основном связаны с отсутствием в непосредственной близости другого игрока (как-нибудь мы осветим тему дистанционного управления состоянием компьютера). Кроме того компьютер должен получить достаточно информации, чтобы он смог показать действия игрока за другим компьютером.
Для преодоления этих проблем существуют следующие пути:
§
Можно передать на другой компьютер полную информацию о действиях игрока. В этом случае коммуникационный порт будет напоминать виртуальное устройство ввода данных, управляемое другим компьютером. Когда игрок тронет клавиши, переместит мышь или повернет ручку джойстика это действие тут же передается по кабелю другой машине, которая па основе полученных данных может скорректировать виртуальное местоположение игрока в собственном игровом пространстве;
§ Второй метод называется «синхронизацией вектора состояния». В этом случае вместо передачи данных от устройств ввода/вывода, мы передаем «состояние» игрового пространства в целом, так что принимающий компьютер может синхронизироваться с передающим, как показано на рисунке 14.6. Этот метод работает достаточно хорошо, однако при его использовании может значительно увеличиться количество передаваемой информации.
Вскоре мы разберемся с каждым из методов более, детально, а сейчас стоит поговорить о наиболее типичных ошибках, встречающихся при соединении двух ПК:
§ Наибольшая проблема возникает, когда две машины теряют синхронизацию. Скажем, одна из них имеет 586-й процессор, а другая - 386-й. При этом один ПК неизбежно окажется впереди другого и синхронизация будет потеряна. Этот фактор должен быть принят во внимание еще на этапе разработки игры;
§ Следующая потенциальная проблема может быть вызвана так называемым «недетерминированным эффектом наложения» (я расскажу лишь о некоторых лежащих иа поверхности вещах, однако этого достаточно для понимания сути проблемы). Обе игры должны быть полностью детерминированы. Это значит, например, что мины на разных компьютерах не могут оказаться в различных местах. Если на одной машине мина расположена скажем, в центре игрового поля, то и на другой машине ей лучше бы оказаться в том же месте. Точно так же, при использовании генератора случайных чисел для управления поведением существ, необходимо, чтобы на обеих машинах генерировалась одна и та же последовательность случайных величин. Единственным путем преодоления этой проблемы может служить передача полной информации об игровой ситуации, так чтобы даже случайные события, происходящие на одной машине, без искажений отражались на другой,
Эти проблемы действительно очень серьезны и вам необходимо их тщательно проработать. Мы кратко обсудили основные методы синхронизации и теперь уже можно поговорить о них более подробно.
Структуры данных, используемые для представления игрового пространства
Если бы мы решили воссоздать некий мир во всех его подробностях, то эта глава заняла бы, наверное, миллион страниц. Поэтому в играх мы имеем дело с приближенной к действительности реальностью. Зачем нам создавать весь мир в нашей игре? Нам достаточно смоделировать только то, что имеет непосредственное отношение к сюжету.
Поскольку в качестве прообраза нашей игры мы выбрали Wolfenstein и DOOM, давайте поговорим о том, как создавался мир этих игр. А затем используем эту концепцию как фундамент для нашей собственной компьютерной игры. Но прежде чем сделать это, вкратце обсудим «стандартные» способы представления трехмерного мира на основе многоугольников.
В качестве примера возьмем гипотетические трехмерные скачки. В такой игре нам понадобятся деревья, дома, беговая дорожка, трибуны и пункт контроля. При стандартном способе представления пространства, состоящего из многоугольников, эта информация должна быть представлена на уровне объектов, то есть для каждого трехмерного объекта нам потребуется его модель. Эти модели будут представлены в виде списка многоугольников, заданных их вершинами. Затем каждому объекту надо будет сопоставить координаты, ориентацию и масштаб. Следовательно, если мы хотим, чтобы у нас было десять разных зданий, мы должны определить десять разных объектов: по одному на каждый тип зданий. Затем мы можем взять каждый объект и создать связанный список.
Завершив список, мы получим возможность строить (или визуализировать) каждый объект. Подобные же списки следует создать для структур данных деревьев, трибун и т. д.
Наконец, непрерывная беговая дорожка будет представлена списком прилегающих многоугольников. Таким образом, весь мир игры будет представлен в виде списков объектов. На рисунке 11.1 показана схема такого обьектно-прогтуанственного представления игрового пространства.
Это неплохой и вполне рабочий способ представления мира. Однако, представляя игровое пространство подобным образом, достаточно сложно определять столкновения объектов, создавать новые уровни и хранить данные.
В компьютерных играх желательно иметь возможность быстро создавать новые Уровни на основе простых элементов. Поэтому стоит взглянуть на структуры Данных и методы их представления с точки зрения простоты создания нового варианта игрового пространства. Используя такой подход, мы можем проиграть в гибкости, но это окупится долгой жизнью игры.
Возьмем тот же пример скачек, реализуем его в виде двухмерного пространства, а затем придадим ему видимость трехмерности. В четырнадцатой главе, «Связь», мы подробно рассмотрим создание законченной компьютерной игры с «клеточным» пространством. Сейчас же просто попробуем разобраться в том, что собой представляет этот метод.
В большинстве компьютерных игр различные объекты и изображения многократно повторяются в разных комбинациях. Если вы внимательно посмотрите на игру РасМап, то заметите, что, несмотря на обилие лабиринтов, каждый из них состоит из одних и тех же объектов. Это таблетки, которые возвращают игроку силу, точки, повороты налево и направо, прямые стены и так далее, Пристальный анализ каждого экрана игры РасМап, покажет вам, что вся графика составлена из ряда «клеточек». Эти «клетки» в совокупности образуют повороты, прямые ходы и все остальное. Преимущество такого метода заключается в том, что он позволяет создать большой и разнообразный мир на основании очень небольшого количества исходных данных- Кроме того, такой метод позволяет легко определять столкновения объектов в нашем игровом пространстве.
Для того чтобы создать «клеточный» мир, мы должны сделать следующее:
§ Во-первых, решить, из какого количества клеток будет состоять наше пространство. Например, пусть мир нашей игры будет размером 10х10 клеток;
§ Затем мы должны задать размер клетки. Для растрового двухмерного изображения можно, например, выбрать 16х16 пикселей для одной клетки. Таким образом, наше игровое пространство будет выглядеть так, как это показано на рисунке 11.2.
§ Затем мы должны нарисовать варианты клеток для нашей игры. Мы можем нарисовать клетки, содержащие изображения маленького дерева, стены, еды и так далее;
§ После этого мы должны пронумеровать фрагменты. Таким образом, у каждого из них будет свой целочисленный идентификатор;
§ Затем мы должны задать структуру данных (обычно в виде двухмерной матрицы) и заполнить ее соответствующими идентификаторами.
Преставление данных для нашего примера «клеточного» игрового пространства показано на рисунке 11.3.
Таким образом, мы разбили одно сложное растровое изображение в нашей игре на набор фрагментов. Затем мы'создали игровую матрицу, которая задает растровое изображение как совокупность определенных фрагментов, используя Для их идентификации целые числа (идентификаторы фрагментов),
Теперь мы можем легко визуализировать игровое пространство: у нас есть его матрица, по которой несложно выбрать необходимые фрагменты изображения. Мы также имеем сами эти фрагменты (клетки). Фактически, мы просматриваем матрицу, клетка за клеткой, и в соответствии с идентификаторами воссоздаем необходимое растровое изображение.
На рисунке 11.4 показано построение растрового изображения «клеточного» игрового пространства.
Как вы можете видеть из рисунка 11.4:
§ Мы заполнили структуру данных игры (обычно это двухмерная матрица) Целыми числами, причем каждое число - это идентификатор того фрагмента, изображение которого мы хотим поместить в соответствующей клетке;
§ Затем во вложенном цикле определяем идентификаторы клеток пространства;
§ В соответствии с каждым идентификатором визуализируем необходимый фрагмент растрового изображения.
Несколько таких фрагментов будут составлять изображение здания, дороги, объекта игры и т. д.
Мы должны только побеспокоиться о правильном позиционировании каждого фрагмента. В нашем примере при переходе к следующей клетке мы должныпередвигаться сразу на величину размера клетки, то есть на 16 пикселей.Нарисовав весь экран (10х10 клеток), мы получим растровое изображение160х160 пикселей.
Используя клеточное построение игрового пространства, легко создавать новые уровни в игре. Более того, такой метод упрощает создание инструментов для изготовления новых игровых пространств. Несложно написать программу, которая будет рисовать на экране сетку, а затем с помощью пиктограмм или цветов представлять то растровое изображение игрового пространства, которое вам нужно. То есть вы размещаете пиктограммы или помечаете цветом нужные клетки, а программа транслирует их в соответствующие идентификаторы фрагментов и записывает результирующую матрицу. Таким образом, в рекордно короткие сроки можно создать десятки различных уровней для своей игры, причем каждый из них займет минимум оперативной памяти.
Единственный недостаток данного метода заключается в том, что мы не можем поместить объект в произвольное место. Он обязательно должен вписываться в границы клетки. Однако обычно это не является проблемой.
Подобная техника была использована в DOOM и Wolfenstem. Только вместо фрагментов, представляющих плоское растровое изображение, там используются трехмерные кубы, составленные из одной или нескольких текстур. Создавая нашу игру, мы можем поступить так же.
Если вы играли в Wolfenstein, то наверняка заметили, что мир этой игры создан с - помощью набора кубов, которые покрыты текстурой с четырех вертикальных сторон. Каждый из этих кубов в действительности является клеткой в двухмерной решетке. Идентификаторы каждого модуля задают текстуры, которые будут расположены соответственно по четырем сторонам. Программа считывает эти значения и на их основании создает трехмерный мир.
Создать трехмерный мир с помощью двухмерной решетки помогает её регулярная структура и одинаковая высота всех кубов. Таким образом, подобное игровое пространство получается как бы в результате экструзии плоского клеточного мира.
Клеточное пространство удовлетворяет практически всем требованиям реальной игры. Однако иногда вам могут потребоваться некоторые дополнительные возможности, которые мы уже разбирали при обсуждении объектно-пространственного метода (метода многоугольников). С помощью клеточного подхода удобно создавать окружающую среду игры. Однако иногда ограничения этого метода делают невозможным создание определенных типов объектов, и тогда целесообразно использовать другую структуру данных. Например, иная структура просто необходима, если по ходу игры требуется размещать мелкие объекты в произвольных местах.
В трехмерном мире DOOM также имеются двухмерные стационарные объекты, такие как еда, оружие и т. д. Я подозреваю, что эти объекты имеют другую структуру данных, возможно связанную с конкретным помещением. Эта дополнительная структура данных задает определенное положение и тип объекта, который может быть расположен в данной комнате.
В любом случае, используйте те методы, которые позволяют реализовать ваши замыслы. Однако не забывайте о том, что новые уровни и новые объекты Должны создаваться как можно более простым путем. Вы не должны каждый Уровень строить с нуля. Вы должны иметь для этого инструменты и максимально использовать уже готовые элементы.
Теперь, создав игровое пространство, посмотрим как его объекты взаимодействуют между собой.
Структуры данных в компьютерных играх
Теперь я хотел бы обсудить, какие структуры данных для представления объектов используются в компьютерных играх. Объектами игры может быть оружие, фантастические создания, различные предметы, которые нужно искать ц т. д. Создавая объект для игры, вы должны продумать, какими свойствами он должен обладать и отразить это в структуре данных.
Для примера рассмотрим, что требуется иметь для статического объекта. Под статическими я понимаю объекты, которые игрок может подобрать по ходу игры (в то время как динамическими я буду называть те объекты, которые могут перемещаться самостоятельно). Статический объект не совершает никаких действий, он просто лежит в определенном месте игрового пространства. Когда ваш герой находит такой объект, он поправляет свое здоровье, подкрепляет упавшие силы, пополняет арсенал и т. д. Листинг 11.1 содержит возможную структуру данных такого объекта.
Листинг 11.1. Структура данных статического объекта.
typedef struct static typ
{
int x,y; //позиция объекта
int type; // тип объекта: еда, энергия или здоровье
char *data; // указатель на растровое изображение объекта
int state; // состояние объекта
int amount; // поле для уточнения типа объекта
} static, *static ptr;
Для начала вполне можно использовать Листинг 11.1, а потом дополнять его необходимыми полями. Мы могли бы создать связанный список с использованием указателей, однако если объектов в игре немного, то лучше использовать массив, описав его следующим образом:
static stuff
[10]
Осталось инициализировать массив stuff, а затем, работая со статическими объектами, ссылаться на него.
Другой пример. Давайте рассмотрим, чем обладает в игре сам игрок. В нашей демонстрационной игре Warlock игрок - это волшебник, он имеет заклинания, здоровье, несколько жизней, а также может собирать различные предметы. Кроме этого, нам понадобятся различные растровые изображения его рук, видимых на переднем плане экрана. Листинг 11.2 показывает первое приближение обходимой структуры данных.
Листинг 11.2.
Структура данных игрока.
typedef struct player_typ
{
int x,y; // Позиция игрока
int lifes; // Количество оставшихся жизней
int health; // Уровень здоровья
int weapons[3]; // Массив, содержащий типы оружия, которые
// есть у игрока
int spells [10] ; // Массив, содержащий заклинания, которые
// имеются у игрока
char *hands_stationary; // Растровое изображение рук игрока, когда
// он ничего не делает
char *hand_motion[4]; // Четыре растровых изображения рук игрока
// для выполнения заклинаний
int state; // Состояние игрока: жив, мертв, умирает
} player, *player_ptr;
(Мы коротко расскажем о состояниях объектов чуть позже в этой главе, а более подробно —в тринадцатой главе, "Искусственный интеллект").
Как вы можете видеть, мы определили большинство необходимых переменных. Кое-что мы, конечно, добавим или выкинем позже, но для начала эта структура вполне подойдет. Это простой пример того, как следует создавать объекты в своей игре. Исходя из того, какая информация вам понадобится, организуйте соответствующую структуру и двигайтесь дальше.
Если в вашей игре есть как статические, так и динамические объекты, вы должны написать код, который будет рисовать, передвигать, отрабатывать столкновения и уничтожать объекты. Однако вам наверняка не захочется писать новые функции для каждого отдельного дерева! Давайте посмотрим, как можно клонировать игровые объекты.
Сверхскоростная очистка экрана
Экран в режиме 13h отображается в области памяти, начиная с адреса А000:0000 и по A000:FBFF. При этом каждый пиксель задается одним байтом. Давайте посмотрим на рисунок 2.3, чтобы лучше понять, как это происходит. В данной конфигурации каждый пиксель может принимать одно из 256 значений, но эти значения не являются, как можно было бы подумать, кодом цвета пикселя. Они представляют собой видимое значение цвета.
Теперь поговорим о том, как это работает. Значение пикселя, а это байт, адрес которого определяет положение пикселя на экране, используется в качестве индекса в гигантской таблице цветов. Таким образом, значение пикселя 26 не означает цвет номер 26. Наоборот, это значит «индекс, указывающий на 26-й элемент таблицы и использующий значение этого поля».
Примечание
Таблица цветов содержит по одному байту для каждого из первичных цветов, Однако реально используются только шесть первых бит каждого байта. Таким образом, каждый из элементов таблицы состоит из трех байтов, определяющих значения трех основных цветов; красного (R - Red), зеленого (G - Green) и голубого (В — Blue), которые в сумме позволяют представить 262114 цветов. Однако размер таблицы ограничен 256-ю элементами, поэтому и на экране может одновременно присутствовать не более 256 цветов.
В видеоиграх, которые нас привлекают, экран перерисовывается от 15 до 30 раз в секунду. Таким образом, перед тем как нарисовать что-то новое на экране, нам необходимо удалить старое изображение. Для того чтобы это делать, надо найти способ быстрого заполнения видеобуфера каким-нибудь значением цвета, например, цветом фона.
Это значение будет заполнять всю видеопамять, а, следовательно, и весь экран в виде цвета, Самый быстрый способ сделать это - воспользоваться ассемблерной инструкцией STOSW. Вы можете спросить: «А зачем использовать STOSW, когда каждый пиксель — это байт, в то время как STOWS оперирует со словами (WORD)?". На этот вопрос можно дать два ответа:
§ Во-первых, коль мы можем записать один байт в видеопамять, то значит, можем записать и два;
§ Во-вторых, нам надо минимизировать количество обращений к видеопамяти, поскольку она работает примерно в 10 раз медленнее, чем обычная память. Поэтому, предпочтительнее писать не по одному байту, а сразу по два.
Листинг 2.10 показывает ассемблерную функцию для заполнения экрана определенным цветом, а Листинг 2.11 содержит программу на Си, тестирующую ее.
Листинг 2.10. Процедура, заполняющая экран (FILLA.ASM).
screen_ram EQU 0A000h ; видеопамять в этом режиме начинается
; по адресу A000:0000h
.MODEL MEDIUM, С ;устанавливаем модель памяти MEDIUM,
; соглашения по вызову языка Си
.CODE ; начало кодового сегмента
PUBLIC Fill_Screen ; объявляем процедуру общедоступной,
Fill_Screen PROC FAR С color : WORD ;функция принимает один параметр
mov AX, screen_ram ;ES:DI должно указывать на видеопамять
mov ES,AX
xor di,di ;обнуляем DI
mov CX,320*200/2
;количество слов, которое надо вывести
mov AL,BYTE PTR color ;помещаем в регистр AL код цвета
mov AH,AL ; этот же код помещаем в регистр АН
rep STOSW ;эта команда заполняет видеопамять
; выбранным цветом с максимально
; возможной скоростью
RET ;выход из процедуры
Fill_Screen ENDP ;конец процедуры
END ;конец кодового сегмента
Листинг 2.11, Программа на Си для тестирования программы 2.10 (FILLC.C).
#inciude <stdio.h>
#define VGA256 0х13
#define TEXT_MODE 0х03
extern Set_Mode(int mode);
extern Fill_Screen(int color);
void main(void)
{
int i;
// устанавливаем режим 320х200 точек, 256 цветов (режим 13h)
Set_Mode(VGA256);
// заполняем экран цветом с кодом 1 (в,палитре, устанавливаемой
// по умолчанию, это соответствует синему цвету)
for (t=0; t<1000; t++) Fill_Screen(1) ;
// ждем нажатия любой клавиши
while(!kbhit()) {}
// возвращаемся в текстовый режим работы экрана
Set_Mode(TEXT_MODE);
} // конец функции main
Эти программы чистят экран с максимальной скоростью.
Примечание
Я произвел замер скорости работы этих функций на своем компьютере и получил значение 22 кадра в секунду. Это представляется невероятно медленным, и я сильно забеспокоился. Однако при ближайшем рассмотрении выяснилось, что причина задержки — крайне низкое быстродействие видеопамяти. Собственно, процессор мог бы обеспечить скорость до 250 кадров в секунду. Однако, увы, он часто вынужден ждать, пока видеопамять соизволит откликнуться на его обращение.
На прилагаемой к этой книге дискете вы найдете программу под названием GAUGE.EXE, Вы можете использовать ее для замера производительности вашей видеосистемы.
Наш курс ассемблера проходит отлично. Я уже сам узнал кучу нового и надеюсь, что вы тоже. Теперь нам осталось узнать еще про одну возможность программирования на ассемблере: об использовании встроенного (in-line) ассемблера,
Связь
Так же, как единственная клетка мозга не могла бы сделать больших успехов в шахматах, играть с компьютером в одиночестве не очень интересно. В связи с последними достижениями в области телекоммуникаций, видеоигры для нескольких игроков становятся все более распространенными. В настоящее время многие программы поддерживают игру двух человек через модем. В этой главе мы начнем изучать системы связи между ПК и приемы, необходимые для создания видеоигр, рассчитанных на нескольких игроков.
В этой главе будут изучены следующие темы:
§
Средства связи видеоигр;
§ Последовательный интерфейс ПК;
§ Функции поддержки последовательного порта ROM BIOS;
§ Соединение через нуль-модем;
§ Создание коммуникационных библиотек;
§ Стратегия коммуникационных видеоигр;
§ Синхронизация вектора состояния;
§ Синхронизация состояния порта ввода/вывода;
§ Синхронизация по времени;
§ Модем;
§ Написание игры Net-Tank (Сетевой танк) для двух игроков в замкнутом пространстве.
В этой главе мы сконцентрируем внимание на проблемах дизайна игр для нескольких игроков, по возможности не вдаваясь в управление коммуникациями как таковыми. К сожалению, у нас нет времени на изучение методов осуществления связи через модем в полном объеме. Однако мы научимся управлять связью через последовательный порт с помощью нуль-модема. С этими знаниями, потратив дополнительно несколько ночей, вы научитесь связываться через модем. У вас будут все элементы, достаточные для того, чтобы получить нечто работающее. Мы хотим научиться писать видеоигры для двух и более игроков сидящих за своими компьютерами и играющими одновременно без потери синхронизации и прочих проблем. Поэтому нас больше будет интересовать тактика коммуникации в архитектуре видеоигр, а не физическая коммуникация сама по себе.
Связь мультипликации с контекстом
Как это ни удивительно, но на связь мультипликации с контекстом крайне редко обращают внимание в компьютерных играх. Контекст означает «связь с окружением или основой». В играх контекстом являются разнообразные дейст вия по ходу игры. Что бы ни делал персонаж в игре: умирал, прыгал, стрелял или еще что-нибудь, -- все это часть контекста игры. Мультипликация и различные эффекты, которые связаны с контекстом, придают игре дополнительную реалистичность и размах.
Попробую пояснить на примере, что я имею в виду. Представьте, что мы имеем игру, в которой герой может гулять, прыгать, бегать и стрелять. Независимо от того, перепрыгивает ли он через паука или озеро, во время прыжка мультипликация обычно бывает всегда одна и та же. Но что если для каждого случая, или соответствующего контекста мы сделаем отдельный мультик? Например, когда герой перепрыгивает просто через лужу, то он не совершает ничего кроме собственно прыжка. А вот когда ему приходится перепрыгивать через паука, то он может, например, издать пронзительный крик ужаса и показать изумительную сноровку в прыжках. (И действительно, приземление на большого склизкого паука, возможно, не самый лучший опыт для маленького героя, которого заманили в виртуальный компьютерный мир.)
Итак, контекстнозависимая мультипликация, используя набор соответствующих вариантов движений, учитывает тем самым окружение и внешние обстоятельства в каждый момент игрового действия, что дополнительно разнообразит зрительное восприятие игры.
Таблицы цветов
Как я уже говорил, мы можем записать по рассчитанному адресу число от 0 до 255. А что означает это число? Ответ прост - это цвет пикселя, который мы хотим отобразить.
VGA-карта способна одновременно отобразить на экране до 256 цветов. Цвет, который мы хотим получить, должен быть представлен числом от 0 до 255. Это здорово, но какая связь между числом и действительным цветом? Число используется как индекс в таблице цветов, хранящей действительные значения цвета, который мы увидим на экране.
Всего же VGA-карта способна отобразить 262144 цвета. Таким образом, если нам надо записать в видеобуфер значение цвета, то нам понадобится три байта для представления числа такой длины. Впрочем, и видеобуфер в этом случае будет просто огромен. Поэтому, создатели карты предусмотрели возможность переадресации графического адаптера.
Переадресация означает, что одна числовая величина используется в качестве адреса другого значения (примерно, как именованный указатель в Си). Вместо одновременного воспроизведения всех 262144 цветов, разработчики VGA-карт дали возможность использовать подмножество из 256 цветов. В результате VGA-карта имеет таблицу отображения цветов, включающую 256 значений. Каждое из этих значений состоит из 256 элементов размером в один байт, содержащих значения красного, синего и зеленого для выбранного цвета (помните, красный, зеленый и синий в комбинации могут образовывать любой цвет.)
Таблица цветов состоит из 768 байт (3х256). Например, когда карта считывает из видеобуфера число 72, то она проверяет адрес 72 в таблице цветов. Адрес 72 находится по смещению 72х3 от начала таблицы, поскольку каждое из значений занимает три байта. Затем значения зеленого, красного и синего считываются из таблицы и используются как значения сигналов. Давайте рассмотрим рисунок 5.2 для более подробного ознакомления.
Было бы прекрасно, если б мы имели прямой доступ к таблице соответствия, как к обычной памяти. Но, к сожалению, регистры цвета доступны только через порты ввода-вывода VGA-карты. На самом деле это плохо, поскольку весьма усложняет нам жизнь. Теперь нам предстоит узнать, как менять значения в таблице цветов.
Тайминг
Я попытался придумать хорошую демонстрацию перемещения спрайтов и решил, что для этого подойдет маленький городок с ковбоем, который ходит по улице. Не так уж плохо. Я хотел еще, чтобы он время от времени стрелял, но позже решил не усложнять дело. Для осуществления реалистичной анимации мы должны уделять большое внимание таймингу, то есть задержке между выводом кадров. Если образ имеет 10 анимационных кадров и мы будем их менять слишком быстро, то персонаж станет похож на лунатика. Поэтому мы должны иметь в программе счетчики времени, чтобы наши персонажи выполняли определенные действия с заданной скоростью. В нашем случае мы используем четыре переменные для сохранения счетчиков движения и анимации:
§
anim_clock
§ anim_speed
§ motion_clock
§ motion_speed
Переменные скорости - это константы, а переменные времени обновляются при каждом проходе через главный цикл. Когда переменная времени оказывается больше переменной скорости, то мы осуществляем какое-либо действие: перемещаем спрайт или сменяем кадр. Переменные времени дри этом обнуляются. Это позволяет нам сделать перемещение спрайта по экрану и смену фаз движения независимым друг от друга и от быстродействия машины. Мы еще вернемся к этому в седьмой главе.
Технические приемы анимации
Внимательно изучив рисунок 16.31, вы можете заметить, что определенные части каждого кадра остаются относительно неподвижными. Положение головы и туловища человека не изменяются от кадра к кадру. Чтобы оживить персонаж было несколько легче, надо предпринять следующие действия:
1. В первую очередь нарисуйте области, остающиеся неизменными,
2. Затем сделайте необходимое количество копий кадра.
3. Наконец, дополните каждый кадр недостающими движущимися частями.
Этот прием дает вам одно преимущество: он помогает правильно расположить изображения фаз движения одно за другим. Мультипликация базируется иа неподвижных частях изображения и, выводя полученные таким способом кадры, вы можете получить достаточно плавное движение. Помните, что сдвиг неподвижной части изображения даже на один пиксель может привести к cитyaции, когда движения вашего персонажа будут выглядеть дерганными.
ТЕХНИКА ОПТИМИЗАЦИИ
В прошлом году я просил у Санта Клауса подарить мне суперкомпьютер Cray XPM, но, как обычно, он не принес ничего. И если вы не будете использовать компьютер с фемисекундным циклом выполнения команд и террабайтами оперативной памяти, нам обоим придется примириться с ПК.
Следовательно, мы должны делать наши программы видеоигр настолько быстрыми, настолько возможно. Для этого мы должны постараться выжать из ПК каждую унцию его мощности. В данной главе мы охватим следующие темы, связанные с приемами оптимизации:
§ Передача параметров;
§ Глобальные переменные;
§ Указатель и ценность псевдоимени;
§ Использование регистров;
§ Оптимизация компилятора;
§ Развертка циклов;
§ Бинарное умножение;
§ Таблицы поиска;
§ Математика с фиксированной точкой;
§ Встроенный ассемблер;
§ Предмет изучения;
§ Оптимизация построения пикселя;
§ Оптимизация изображения пикселя.
Введение
Прежде чем углубиться в изучение приемов оптимизации видеоигр для ПК, я хочу дать вам несколько советов. Когда вы оптимизируете свои программы, не пытайтесь разом провести полную оптимизацию. В первую очередь обратите внимание на те части, время выполнения которых критично и где встречается наибольшее количество циклов. Возьмите эти функции и работайте с ними до тех пор, пока их быстродействие не начнет вас удовлетворять.
Попытка оптимизации всей игры за раз, как правило, приводит к полному беспорядку, и вы никогда не сможете ни отладить программу, ни добавить что-либо в игру. Оптимизация должна быть взвешенной с точки зрения наличия других факторов. Если оптимизация функции принесет ей лишние три процента быстродействия, но при этом сделает ее вдвое запутаннее, то стоит попробовать найти другое место для оптимизации.
Не пытайтесь свалить все операторы, в кучу и свести десяток строк программы к одной. Ненавижу смотреть на выражения типа
* (х+(у++) >=*(& (х++)>>2)+(char far *)y;
Получили представление? Такая запись не только выглядит коряво с точки зрения Си, но и Codeview вряд ли поможет вам в отладке строк, подобных этой.
Поскольку мы разрабатываем видеоигры, я могу гарантировать, что 90% времени будет затрачено не на осуществление игровой логики, а на различные графические преобразования. Поэтому нужно сделать все функции рисования настолько быстрыми, насколько это возможно, а строки, управляющие игрой, имеет смысл оставить простыми и понятными. Функция, рисующая на экране точку, может представлять собой «черный ящик», а игровая программа в целом — нет. Оставляйте ее удобочитаемой. С помощью приемов оптимизации, которые мы изучим в этой главе, быстродействие практически любой функции может быть увеличено от 2 до 10 раз. Не говоря о том, что вы получите замечательные результаты, еще и произведете впечатление на друзей.
Передача параметров
Из второй главы «Основы языка ассемблерам (да и из собственного опыта) вы должны помнить, что передача параметров функциям не является свободной в полном смысле слова. Параметры должны быть помещены в стек, сделаны доступными через использование указателя базы и, наконец, взяты со стека. Если вам нужна функция, которая складывает 8 чисел, передаваемых как параметры, то потребовалось бы написать что-нибудь вроде этого:
int Add_Em_All(int nl,int n2,int n3,int n4,
int n5,int n6,int n7,int n8)
{
return (n1+n2+n3+n4+n5+n6+n7+n8);
}
(Конечно, это не является реальной функцией. Я привел ее только в качестве наглядного примера.) После компиляции этой функции ее тело будет выглядеть примерно так:
clc
mov ax,00h
adc ax,n1
adc ax,n2
adc ax,n3
adc ax,n4
adc ax,n5
adc ax,n6
adc ax,n7
adc ax,n8
Конечно, в самом начале приведенного фрагмента необходимо создать фрейм стека и уничтожить его в конце. Однако суть в том, что на восемь выталкиваний и извлечений параметров уходит немало времени. Если вы посмотрите на скорость выполнения команд PUSH и POP, то обнаружите, чти они отнимают в три раза больше тактов процессора, чем ADC. Как видите, в данном случае передача параметров отнимает больше времени, чем выполнение самой функции. Это показывает нам, что мы должны стараться передавать только те переменные, которые действительно необходимы.
Также никогда не передавайте структуры как значения. Если вы определите структуру, написав что-либо похожее на это:
typedef struct point_typ
{
int x[10],y[10],z[10];
}point,point_ptr;
сделаете вызов
Display(point point_1);
то в стек будет помещена целая структура! Это очень плохо. Чтобы избежать подобного, при передаче структур всегда применяйте ссылки на них, а «передачу значением» используйте только для целых и прочих стандартных типов данных Си.
Может быть, вы спросите: «Почему не использовать глобальные переменные вместо параметров?» Рассмотрим эту идею более внимательно.
Глобальные переменные
Мы пишем видеоигры, правила которых иногда должны изменяться. Всякий, кто где-нибудь учился, знает, что глобальных переменных стоит по возможности избегать. Конечно, всякое бывает, но лучше, когда их очень мало, и прекрасно, когда они вообще отсутствуют. Здесь приведена точка зрения одного крутого знатока в программировании игр; «Используйте глобальные переменные всегда, когда они помогают в увеличении быстродействия, но при этом сохраняйте чувство меры». К примеру, у нас есть набор функций, которые рисуют точки, изображают линии и окружности.
Эти функции требуют передачи им различного количества параметров: для построения точки нужно знать две координаты, а для рисования линии — целых четыре. Что же касается цвета, то он, вероятно может быть одним и тем же как для линий, так и для окружностей. Почему же не сделать, чтобы каждая функция рисовала текущим глобальным цветом? С этой целью можно задать переменную и назвать ее, например, draw_color. Если вы измените текущий цвет и сделаете сотню вызовов функции, то при этом изменить цвет достаточно будет только один раз. В результате вы сумеете избежать порядка двухсот операций обмена со стеком.
Но учтите, что применение глобальных переменных может оказаться немного похожим на употребление наркотика: чем больше его принимаешь, тем больше он нужен. Хороший программист всегда может сбалансировать использование глобальных переменных и увеличить с их помощью быстродействие программы на 5, а то и 10 процентов.
Указатели и использование псевдоимен
Эту тему условно можно назвать тактическим приемом, которым пользуются некоторые программисты, в то время как многие о нем даже и не подозревают. Например, у вас есть фрагмент такой программы:
for(t == y->stars[index].left;
t < stars[index].left + 100; t++)
{
position = point->x + point->y -point->z;
pitch = point->x * point->y * point->z;
roll-= point->ang + sin(t) * point->ang;
}
Этот фрагмент хоть и выглядит достаточно компактным, но, тем не менее, и к нему может быть применен способ оптимизации, связанный с использованием псевдоимен. Вы видите, что в этом примере присутствует несколько команд, ссылающихся на указатель. Тактика, которой мы здесь воспользуемся, заключается в замене всех указателей, встречающихся более двух раз, простыми переменными. Вышеуказанная функция в результате может быть переписана так;
t1=y->stars[index].left;
x=point->x;
y=point->y;
z=point->z;
ang=point->ang;
for(t=t1;t<t1+100;t++)
{
position=x+y+z;
pitch=x*y*z;
roll=ang+sin(t)*ang;
}
Несмотря на то, что новая версия длиннее, она выполняется быстрее, так как содержит только пять ссылок вместо 800. Конечно, доступ к переменным х, у и z отнимает некоторое время, но порядок его величины меньше, чем ссылки на структуры и указатели. И этим нужно воспользоваться.
Использование регистров
В конечном счете, все программы, которые мы пишем на Си, будут переведены в последовательность машинных команд. После этого превращения некоторые из регистров общего назначения окажутся занятыми выполнением задачи, определяемой программой. Однако, мы не можем быть уверенными, что регистры будут использоваться вместо медленной стековой памяти. Чтобы быть уверенным, что функции используют регистры для переменной индекса (или какой-нибудь другой), давайте попробуем заставить компилятор по мере возможности делать это. Необходимо всегда помнить, что доступ к регистрам процессора во много раз быстрее, чем к оперативной памяти. Это происходит потому, что регистры находятся внутри ЦПУ, а память - нет. Мы можем использовать ключевое слово register, чтобы скомандовать компилятору использовать регистр. Как пример, напишем функцию, которая делает перестановку без регистров.
void Swap(int..num l,num 2)
{
int temp;
temp=num_1;
num_l=num_2; num_2=temp;
}
А теперь перепишем ее, используя регистр как временную переменную.
Swap(int num_1,num_2)
{
register int temp;
temp=num_1;
num_1=num_2;
num_2=temp;
}
Если компилятор может, то он будет использовать регистр в качестве регистровой переменной и программа увеличит скорость выполнения на 10-15 процентов. В связи с использованием ключевого слова register нужно учитывать два момента:
§ Компилятор не создает регистры, он использует стандартные регистры ЦПУ;
§ Иногда форсирование компилятора для использования регистров делает программу медленнее. Будьте осторожны. Обычно не стоит применять переменные типа register в маленьких функциях.
Теперь поговорим об основных приемах оптимизации компилятора.
Оптимизация компилятора
Фирма Microsoft уверяет, что ее компилятор является оптимизирующим. Это предельно правдивое высказывание. Несмотря на то, что оптимизатор иногда в состоянии сделать вашу программу медленнее или даже привнести ошибки, он проводит классическую оптимизацию (с точки зрения ученых-компьютерщиков). Однако мы, как создатели игр, не можем доверять автоматической оптимизации. Следовательно, мы никогда не будем использовать никакие опции оптимизатора (хорошо, может быть только некоторые из них). Стоит попробовать поиграть с ними только когда ваша видеоигра уже полностью готова и свободна от ошибок. Но ни в коем случае не рассчитывайте на оптимизацию во время разработки программы. Это не панацея. Единственное, что может принести реальную пользу, так это опция отключения контроля переполнения стека.
Обычно компилятор в начале любой процедуры вставляет небольшие фрагменты кода, называемые прологом, которые служат для проверки достаточности стекового пространства для размещения локальных переменных. Но если установить размер стека в несколько килобайт, то у вас никогда не будет проблем. Поэтому вы можете выключить контроль переполнения стека, что немного сократит время обращения к функции (для этого можно использовать директиву компилятора -GS).
Я считаю, что оптимизатор на самом деле может доставить хлопот больше, нежели разрешить проблем. Вскоре я специально напишу эффективную программу, чтобы доверить ее оптимизатору и посмотреть, что из этого получится. Иногда оптимизатор может без зазрения совести внести в вашу программу пару-тройку ошибок, и это является еще одной причиной, почему им не стоит пользоваться слишком активно. (Я уже задокументировал дюжину ошибок в новом компиляторе Microsoft — Visual C/C++ 1.5, так что это высказывание имеет под собой серьезное обоснование. Обычно я до сих пор использую C/C++ 7.0, находя его более надежным.)
Развертка циклов
Это очень старо. В далеком прошлом, в конце 70-х годов, когда Apple безраздельно правил миром ПК, королем среди процессоров считался процессор 6502.
Люди всегда находили интересные пути, чтобы заставить его работать быстрее. Один из трюков, который был найден, называется разверткой циклов. Это технический прием, где программист на самом деле отменяет структуру цикла вручную, разбивая саму задачу цикла. Структура цикла сама по себе имеет небольшой заголовок и мы можем использовать это в наших целях. К примеру, мы хотели бы инициализировать поле men в 10000 структур. Можно было бы поступить так:
for (index=0;index<10000;index++)
{
player[index].men=3;
}
и это будет прекрасно работать. Однако переменная index инкрементируется и сравнивается 10000 раз. Поскольку цикл повторяется 10000 раз, то это означает, что будет 10000 переходов типа NEAR. Мы можем развернуть цикл и получить небольшой выигрыш в скорости его выполнения. Вот что мы могли бы сделать:
for(index=0;index<1000;index+=10)
{
player[index].men=З;
player[index+1].men=3;
player[index+2].men=3;
player [index+3].men=3;
player[index+4].men=3;
player [index+5].men=3;
player [index+6].men=3;
player[index+7].men=3;
player [index+8].men=3;
player[index+9].men=3;
}
Теперь цикл выполняется только 1000 раз. Следовательно, index изменяется и сравнивается только 1000 раз, и при этом выполняется только 1000 переходов типа NEAR.
Этот пример написан мною еще и для того, чтобы показать вам, что развертка цикла может иметь и отрицательные стороны. Посмотрите внимательно на новую программу. Здесь к каждому индексу в каждой последующей операции присваивания добавляется смещение, и время, которое уйдет на это, может свести на нет выгоду от разворачивания циклов. Однако, чтобы исправить это, мы можем применить маленькую хитрость. Введем для индексирования структуры вторичную переменную new_index, которую будем увеличивать после каждого присваивания. Это приведет к увеличению скорости. Взгляните:
new_index=0;
for(index=0;index<1000;index+=10)
{
player[new_index++].men=3;
player[new_index++].men=3;
player[new_index++].men=3;
player[new_index++].men=3;
player[new_index++].men=3;
player[new_index++].men=3;
player[new_index++].men=3;
player[new_index++].men=3;
player[new_index++].men=3;
player[new_index++].men=3;
}
Новая программа работает быстрее, чем старая. Неплохо? Развертка циклов настолько эффективна, что у вас может возникнуть желание постоянно прибегать к этой уловке. Однако и здесь нужно знать меру. Дело в том, что машинные команды кэшируются внутри современных CPU, и слишком «массированное» разворачивание циклов может привести к проблемам переполнения кэша. Но если вы пользуетесь этим бережно (то есть ограничиваетесь тремя-восемью итерациями), то должны получить хорошие результаты.
Теперь поговорим о другом старом трюке - использовании операций сдвига для перемножения чисел.
Бинарное умножение
Впервые мы столкнулись с этим трюком в пятой главе, «Секреты VGA-карт». На ПК (да и вообще почти на любом компьютере на этой планете) система двоичных чисел используется для представления чисел в компьютере (хотя, я слышал и о «троичных» компьютерах). Поскольку разряды двоичных чисел являются степенью двух и каждое число помещается в слове как набор двоичных цифр, сдвиг слова влево или вправо смещает каждый его разряд на соседнее место. Эти операции автоматически удваивают число или делят его на два, соответственно. Взгляните на рисунок 18.1, чтобы увидеть это на примере.
Сдвигая число влево, вы умножаете его каждый раз на 2. Проделав это четыре раза, вы умножите его на 16, пять раз — на 32. Таким путем мы можем умножить число на любую степень двух, но как насчет других'чисел, таких, например, как 26? Для выполнения этого мы разобьем умножение на группы умножений по степеням двух. Число 26 может быть представлено как 16+8+2. Таким образом, если мы умножим произвольное значение Х на 16, добавим к нему X, умноженное на 8 и, наконец, добавим X, умноженное на 2, то ответ должен быть таким же, как если бы мы умножили на 26.
Взгляните:
Y=X*26=X*16+X*8+X*2=X<<4+X<<3+X<<1
Кстати, именно так поступают ученые-ракетчики, которые прекрасно знают, что сдвиг гораздо быстрее умножения, и мы также можем выиграть кучу времени, используя эту «реактивную» технику. Увеличение скорости вычислений достигается за счет того, что сдвиг — простая бинарная операция, в то время как умножение является действием гораздо более комплексным.
В связи со сказанным стоит упомянуть еще вот о чем;
§ Во-первых, деление также может быть выполнено путем сдвига числа, но только вправо. Правда, деление сдвигом выполняется не так часто, как умножение, поскольку делитель разбить на составляющие обычно бывает значительно сложнее.
§ Во-вторых, эта техника работает только с целыми числами без плавающей запятой. Значения типа FLOAT и DOUBLE хранятся в памяти в так называемом IEEE-формате, и их сдвиг приведет к переполнению разрядной .сетки.
Теперь мы можем перейти к следующей технике оптимизации — к таблицам поиска. Что ж, поищем их...
Таблицы поиска
Таблицы поиска, как следует из их названия, служат для поиска некоторых фактов. С их помощью можно искать что угодно. Суть применения таблиц поиска состоит в том, что вместо выполнения расчетов в процессе работы программы, мы предварительно выполняем все возможные вычисления, в которых может возникнуть необходимость, и сохраняем их в гигантской таблице. Далее, во время выполнения программы, мы смотрим в таблицу, используя параметр вычисления для поиска в ней конечного результата. Наиболее классическое использование справочных таблиц — предварительное вычисление трансцендентных функций, таких как синус, косинус, тангенс и т. д., поскольку они отнимают много времени для вычисления, даже с математическим сопроцессором.
Приведу пример использования справочных таблиц. Скажем, в программе нам необходимо вычислять синус и косинус угла, который может быть любым от 0° до 360° и является целым (то есть у нас не будет вычислений для углов с десятичной частью, таких как 3.3).
Следующая программа создает справочную таблицу значений синусов и косинусов:
float sin_table[360],cos_table[360] ;
for(index=0;index<360;index++)
{
sin_table[index]=sin(index*3.14159/180);
cos_table[index]=cos (index*3.14159/180) ;
}
Этот программный фрагмент создает две таблицы, содержащих по 360 заранее вычисленных значений синусов и косинусов.
Теперь посмотрим, как использовать справочные таблицы, К примеру взглянем на это выражение:
x=cos(ang*3.14159/180)*radius;
y=sin(ang*3.14159/180)*radius;
Используя наши новые справочные таблицы, мы могли бы иметь:
x=cos_table[ang]*radius;
y=sin_table[ang]*radius;
Применение справочных таблиц может значительно увеличить быстродействие ваших игр. Единственная сложность заключается в том, что они отнимают много места. К примеру, игра Wing Commander использует справочные таблицы, которые содержат предварительно вычисленные виды кораблей для всех углов поворота. DOOM использует справочные таблицы, чтобы помочь в вычислении видимого расположения всех объектов игрового пространства.
При построении справочной таблицы учитывается все, что игрок может увидеть на экране. Необходимые для графических построений данные вычисляются перед началом игры или загружаются в готовом виде с диска. Затем, во время игры справочные таблицы получают привязку к окружающему игровому пространству и если табличные данные сообщают, что некоторую часть изображения невозможно увидеть, то графическая система устранения скрытых поверхностей удалит их.
Я сторонник идеи, что полноценная видеоигра может быть сделана только с огромным количеством справочных таблиц, и лишь небольшим программным кодом, отвечающим за осуществление игровой логики, а то и вовсе без такового. Как и в электротехнике, в видеоиграх используются конечные автоматы (с которыми мы столкнулись в тринадцатой главе, «Искусственный интеллект»). Однако вместо использования алгоритмов, моделирующих поведение объекта для следования из одного состояния в другое (как мы делали в тринадцатой главе), для управления существами можно привлечь справочные таблицы.
Этим техническим приемом в вычислительной технике пользуются для конструирования КА и мы также можем делать это в наших программах. Все что нам необходимо, это таблица, которая содержит текущее состояние и следующие состояния.
Итог: справочные таблицы великолепны. Применяйте их во всех случаях компьютерной жизни пока позволит объем памяти. Кроме того, когда вы составляете справочные таблицы, попробуйте, где это только возможно, использовать симметрию для уменьшения размера таблицы. Например, синус и косинус
по сути являются одной и той же функцией с различием в 90о. Взгляните на рисунок 18.2. Мы видим, что синус и косинус выглядят почти одинаково. Фактически, они связаны следующими формулами:
sin (angle)=cos(angle-90) cos (angle)=sin(angle+90)
Поэтому мы могли бы создать единственную справочную таблицу для значений косинусов, а потом, когда нам понадобится вычислить синус угла, достаточно к исходной величине угла добавить 90е и использовать новый результат как индекс. Конечно, при этом нужно быть осторожным — новый угол может оказаться больше 360°. В этом случае вы должны выполнить его циклический возврат к нулю:
if(angle>360) angle=angle-360;
Для, доказательства того, что справочные таблицы могут увеличить скорость выполнения программ, я создал программку, которая использует как обычные функции sin и cos, так и справочную таблицу, хранящую заранее вычисленные значения синусов и косинусов, изображающую 100 окружностей. Программа, показанная в Листинге 18.1, начинается с заполнения таблиц. Затем она чертит 1000 окружностей, используя внутренние функции. Потом она ждет нажатия клавиши, после чего рисует 1000 окружностей, используя табличные данные. Запустив программу, вы обязательно почувствуете разницу. Также обратите внимание, что эта программа достаточно компактна и очень эффективна с точки зрения вывода графики.
Листинг 18.1. Сравнение выполнения программы с использованием справочных таблиц и встроенных функций sin и cos (LOOKNUP.C).
#include <math.h>
#include <stdio.h>
#include <graph.h>
float sin_table[360], cos_table[360];
main()
{
int index, x,y,xo,yo,radius,color,ang;
char far *screen = (char far *)0xA0000000;
// использовать библиотеку Microsoft для перехода
// в режим 320х200х256
_setvideomode(_MRES256COLOR);
// создать таблицы быстрого доступа
for (index=0; index<360; index++)
{
sin_table[index]= sin(index*3.14159/180} ;
cos_table[index]= cos(index*3.14159/180);
}
// нарисовать 1000 окружностей, используя встроенные
// функции sin и cos
for (index=0; index<1000; index++)
(
// получить случайные числа
radius = rand()%50;
xo = rand()%320;
yo = rand(}%200;
color = rand()%256;
for (ang=0; ang<3 60; ang++)
{
x = xo + cos(ang*3.14159/180) * radius;
У = yo + sin(ang*3.14159/180) * radius;
// нарисовать точку окружности
screen[(y<<6) + (y<<8) + x] = color;
}
}// все, ждать пока пользователь нажмет клавищу
printf("\nHit, a key to see circles drawn twith look up tables.");
getch();
_setvideomode(_MRES256COLOR);
// нарисовать 1000 окружностей, используя таблицы поиска
for (index=0; index<1000; index++)
{
// нарисовать случайную окружность
radius = rand()%50;
хо = randO %320;
уо = rand()%200;
color = rand()%256;
for (ang=0; ang<3 60; ang++)
{
x = хо + cos table[ang] * radius;
у = уо + sin_table[ang] * radius;
// нарисовать точку окружности
screen[(y<<6) + (y<<8) + x] = color;
} }
// подождать, пока пользователь нажмет любую клавишу
printf("\nHit any key to exit."};
getch();
_setvideomode(_DEFAULTMODE);
}
После запуска LOOKNUP.C вы должны согласиться, что справочные таблицы крайне удобны и могут здорово увеличить скорость выполнения программы.
Следующая тема будет касаться математики с фиксированной запятой.
Математика с фиксированной запятой
Математика с фиксированной запятой? Нет, это не новая точка зрения на дробные числа. Просто это немного другой путь рассмотрения компьютерной математики.
Существуют две формы математики, предназначенные для компьютера:
• Математика целых чисел;
• Математика с плавающей запятой.
Первая использует значения типов CHAR, INTEGER, LONG и т. д. Вторая оперирует числами FLOAT, DOUBLE и т. п. Разница между двумя этими видами математики заключается в том, как представлены числа в памяти и какие именно числа - целые или дробные - принимают участие в расчетах. Целые числа представлены в компьютере непосредственно в двоичной форме, без какого-либо кодирования. Как вы знаете, они могут быть как положительными, так и отрицательными, и у них отсутствует дробная часть. С другой стороны, числа с плавающей запятой должны иметь десятичные части.
Но к чему такая забота по поводу чисел? Объясняю: ПК может выполнять математические вычисления весьма быстро, но «весьма быстро» еще не значит «достаточно быстро для видеоигр». Даже с математическим сопроцессором ПК до сих пор оставляет желать лучшего в режиме реального времени при работе с трехмерной графикой. Известно, что вычисления с целыми выполняются гораздо быстрее, чем с дробными числами.
Вы можете спросить: «Почему мы обязательно должны использовать числа с плавающей запятой?» Ответ заключается в том, что по природе того типа программирования, которым мы занимаемся (трехмерная компьютерная графика), мы оказываемся перед неизбежной необходимостью достижения максимальной точности в наших вычислениях. Это заставляет нас использовать и дробные числа тоже.
Вычисления с плавающей запятой пожирают так много времени из-за способа представления чисел, с которыми они оперируют. Эти числа не являются в полном смысле двоичными, напротив, они хранятся в специальном формате IEEE, в котором характеристика (целая часть) и мантисса (экспонента) представлены в жутко свернутой форме, и прежде чем число использовать в вычислениях, его нужно еще расшифровать. Числа — это только инструмент для представления игровых объектов. Если бы вы захотели, то могли бы разработать свои собственные форматы хранения десятичных чисел.
Это та область, где в бой вступает математика с фиксированной запятой. Например, можно представить число содержащим как целую, так и десятичную часть внутри отдельного целого. Как это сделать? Притворимся, что десятичные части существуют где-то внутри целого и что двоичные цифры слева являются целой частью числа, а все, что находится справа, это десятичная часть. Рисунок 18.3 должен помочь вам представить это.
Где именно вы поместите десятичную часть, зависит от вас. Важно, чтобы позиция десятичной точки и выбор базового типа данных удовлетворяли нашим потребностям. Я предлагаю использовать тип данных LONG, который имеет 32 бита точности, а десятичную точку поместить где-нибудь посередине. Скажем, отведем для десятичной части восемь младших битов, тогда целая часть займет 24 старших разряда. Этого более чем достаточно для наших потребностей, поскольку в видеоиграх не нужна очень высокая точность. Пары знаков после запятой будет вполне достаточно. Чтобы использовать математику с фиксированной запятой, нам необходимо только разобраться, как выполнять с ней несколько операций:
§ присваивание;
§ сложение;
§ вычитание;
§ умножение;
§ деление;
§ сравнение.
Прежде чем разобраться в способах осуществления этих операций, мы все-таки должны понять как объявить число с фиксированной запятой. Взгляните, это действительно трудно:
long fix_1,fix_2,fix_3;
Что, обманул вас на секунду, да? Не правда ли, проще не придумаешь? Как я сказал ранее, мы используем тип данных LONG для чисел с фиксированной точкой и только предполагаем наличие десятичной части. И это все, что нам нужно для определения числа с фиксированной запятой.
Присваивание
Теперь поговорим о присваивании. Если мы хотим присвоить целую часть фиксированного числа, мы делаем .следующее:
int а=300;
long fix_1=0;
// в двоичном виде - 0000 0000 0000 0000 0000 0000 0000 0000
fix_1=((long)a << 8);
Сложнее обстоит дело с присваиванием дробных чисел. Для этого мы должны использовать умножение с плавающей запятой и записать число в LONG. Ниже показано, как выполнить такую операцию присваивания:
long fix_1 = (long) (23.4*256)
Мы умножаем на 256 потому, что это эквивалентно сдвигу на восемь разрядов влево (помните, я уже говорил, что нет смысла сдвигать числа с плавающей запятой).
Сложение и вычитание
Чтобы складывать или вычитать числа с фиксированной запятой, мы можем использовать обычные операторы Си. К примеру, чтобы сложить два числа и записать результат в третье, мы могли бы сделать так:
fix_3=fix_1+fix_2
Вычитание получается точно так же. Кроме того, вы можете использовать и отрицательные значения, поскольку внутреннее представление типа данных LONG учитывает знак, и это вполне применимо также и к числам с фиксированной запятой.
Умножение
Самая сложная из всех операций — это умножение. Здесь мы должны соблюдать осторожность: существует несколько нюансов, которые могут наплодить множество ошибок. Например, когда умножаются два числа с фиксированной запятой, для сохранения результата может потребоваться в два раза больше битов, нежели содержат сомножители. Другими словами, если оба сомножителя были 32-битными, не исключена возможность получить 64-битный результат. То есть мы должны следить за величиной чисел с фиксированной запятой, которые перемножаются. Как и обычно, для этого мы будем использовать оператор умножения. Однако когда умножение выполнено, мы должны сдвинуть результат на 8 позиций назад вправо. Это нужно сделать потому, что когда мы задаем число с фиксированной запятой, то искусственно перемножаем его на 256 (помните, восемь младших разрядов заняты под десятичную часть).
Поэтому мы должны сдвинуть окончательный результат назад восемь раз вправо, иначе говоря, результат должен быть разделен на 256. Если этого не сделать, то, умножая 2 на 5, мы получим 2х5х256 вместо правильного ответа равного 10. Здесь приводится способ, каким делать умножение.
fix_1=(fix_2*fix_3)>>8;
Если вы хотите вычислить сумму произведений, то нет надобности после каждого умножения сдвигать результат. Достаточно сделать это только один раз в самом конце расчетов. Рассмотрим пример:
fix_1=(fix_2*fix_3+fix_4*fix_5)>>8;
Это свойство чисел с, фиксированной запятой могло бы пригодиться, если вы захотите оптимизировать приведенный фрагмент и дальше с целью избавиться от всех сдвигов.
Деление
При выполнении деления я предлагаю вместо использования символа деления умножать на обратную величину. Как правило, это несложно сделать. Запомните, что деление — всегда более медленная операция, чем умножение, независимо от того, применяете вы числа с фиксированной или с плавающей запятой. Здесь приводится пример того, как могло бы быть выполнено деление:
fix_1=(long)(256*1/34);
fix_2=(fix_3*fix_1)>>8 ;
Прежде чем мы перейдем к следующей теме, мне бы хотелось затронуть некоторые детали, о которых прочие авторы обычно не любят говорить. Это точность и максимальное цифровое представление.
Точность
Поскольку мы договорились, что в нашем формате чисел с фиксированной запятой восемь младших разрядов будут содержать десятичную часть, то самым маленьким числом, которое можно представить, окажется значение 1/256 или примерно 0.004. Следовательно, максимальная ошибка будет получаться при умножении двух чисел. Наибольшее число, которое мы можем получить в произведении, равно 32761. Следовательно, наибольшая ошибка, которая может закрасться в расчеты, это 0.004х32761 или 131.044. Ого! Это слишком много. Однако в действительности у нас никогда не будет ошибок такой величины. Только вы не должны с одним и тем же числом выполнять больше 2-5 умножений, и сомножители не должны превышать 32761.
Как правило, в большинстве случаев ошибки не будут превышать 0.01-0.5, что вполне допустимо, поскольку 90 процентов всех расчетов направлены на определение местоположения пикселей на экране и результаты все равно округляются.
Хватит насчет точности. Перейдем к определению максимального числа, которое может быть представлено в нашей системе с фиксированной запятой.
Максимальное цифровое представшие
Поскольку у нас есть 24 бита для целой и 8 бит для десятичной части числа, вы можете подумать, что таким образом можно представить значения вплоть до 224 или 16777216. Почти, но не совсем. Так как число с фиксированной запятой может быть и положительным и отрицательным, мы располагаем числами в диапазоне от -8388608 до +8388608. Мы можем без всяких проблем складывать и вычитать числа из этого диапазона, но при умножении, должны быть исключительно осторожны, чтобы не переполнить тип LONG.
Когда я изучал математику с фиксированной запятой в первый раз и пытался алгоритмизовать ее, то допустил ошибку. Я использовал схему, похожую на нашу (то есть 24 бита для целой части и 8 бит для десятичной) и думал, что наибольшие числа, пригодные для умножения, могут быть любыми, лишь бы результат укладывался в 24 бита. Это означало бы, что можно перемножить 4096 на 4096 и получить правильный ответ. Ошибочка! Я забыл об остальных 8 битах десятичной части- Следовательно, в действительности я умножил 4096х256х4096х256, что составляет примерно 1.09х1012. Поскольку тип LONG состоит из 32-х битов, то он может представлять числа от -2147483648 до +2147483648 (которые в 1000 раз меньше полученного результата). Мораль сей басни такова, что числа с фиксированной запятой остаются принадлежащими типу LONG, и если в них записать числа, интерпретируемые как LONG, то при умножении возникнет переполнение.
Наибольший результат умножения, который может быть получен в нашей системе с фиксированной запятой равен 32761 или 181 в степени 2. Число 181 было получено исходя из следующих соображений: это число, которое, будучи умноженным на 256 и возведенным в квадрат, не должно превышать размерности типа LONG (+2147483648).
Мы используем 256, так как у нас есть восемь двоичных цифр, a 28 равно 256.
Как, ни странно, но наибольшее число, которое может быть получено в результате умножения — это 32761, а величина чисел для сложения может достигать примерно 8000000? Ну что ж... где-то мы нашли, а где-то потеряли.
Чтобы помочь вам в экспериментах с числами с фиксированной запятой и увидеть некоторые их интересные свойства, я создал небольшую библиотеку и программу main() для демонстрации их использования (Листинг 18.2). Я предлагаю вам потратить немного времени, чтобы получить действительно целостное понимание сути чисел с фиксированной запятой, поскольку это очень важно и про это мало кто знает. Никогда не надо забывать, что десятичная запятая является воображаемой!
Листинг 18.2. Функции библиотеки системы с фиксированной запятой (FIX.C).
// ВКЛЮЧАЕМЫЕ ФАЙЛЫ ///////////////////////////////
#include <math.h>
#include <stdio.h>
// определим наш новый тип чисел с фиксированной запятой
typedef long fixed;
//ФУНКЦИИ ///////////////////////////////////////////
fixed Assign_Integer(long integer)
{
return((fixed)integer << 8);
} // конец функции присваивания целочисленного значения ////////////////////////////////////////////////////////////
fixed Assign_Float(float number)
{
return((fixed)(number * 256)};
} // конец функции присваивания значения с плавающей запятой ////////////////////////////////////////////////////////////
fixed Mul_Fixed(fixed fl,fixed f2)
{
return ((fl*f2) >> 8);
} //конец функции умножения ////////////////////////////////////////////////////////////
fixed Add_Fixed(fixed fl,fixed f2)
{
return(f1+f2);
} // конец функции сложения ////////////////////////////////////////////////////////////
Print_Fixed(fixed fl)
{
printf("%ld.%ld", f1>>8, 100*(unsigned long) (f1 & 0x00ff)/256);
} // конец функции вывода числа с фиксированной запятой
// ОСНОВНАЯ ПРОГРАММА //////////////////////////////////////
main(}
{
fixed f1,f2,f3;
fl = Assign_Float(15);
f2 = Assign_Float(233.45);
f3 = Mul_Fixed(f1,f2);
printf("\nf1:");
Print_Fixed(f);
printf("\nf2:");
Print_Fixed(f2) ;
printf("\nf3:");
Print_Fixed(f3) ;
} // конец функции main
Конечно, в собственных программах вы фактически не будете использовать функции сложения и умножения. Я поместил их здесь только для того, чтобы вы могли поэкспериментировать со свойствами чисел с фиксированной запятой.
Теперь перейдем к встроенному ассемблеру.
Встроенный ассемблер
Ассемблировать или не ассемблировать: вот в чем вопрос. Почти шекспировская задача и нам ее сейчас предстоит разрешить.
Как я говорил ранее, вы должны использовать MASM и встроенный ассемблер, только если в этом действительно возникает необходимость. Ведь все равно операции, которые вы выполняете, являются машинно-зависимыми по своей природе. Вам не нужен ассемблер ни для чего, кроме создания графики и звука. Ни в коем случае не стоит использовать ассемблер для реализации игровой логики и алгоритмов. Кроме того, если ассемблер вам необходим для увеличения быстродействия, применяйте вместо MASM встроенный ассемблер. Он проще в использовании и его применение отнимает меньше времени при разработке программ. Мы обсудим несколько методов, которые увеличат скорость выполнения ваших программ во много раз.
Теперь рассмотрим некоторые из функций, которые мы написали в предыдущих главах и оптимизируем их с помощью новых технических приемов.
Оптимизация рисования пикселей
Первое, что мы должны сделать, это насколько возможно оптимизировать по быстродействию функцию рисования пикселей. Ведь на ней базируются, все остальные графические построения! Давайте возьмем функцию Plot_Pixel() из Листинга 5.4 (вы найдете ее в пятой главе «Секреты VGA-карт») и поглядим можем ли мы оптимизировать ее дальше. Листинг 18.3 содержит новую функцию.
Листинг 18.3. Функция для быстрого рисования пикселей.
void Plot_Pixel_Fast(int x,int y,unsigned char color)
(
// Функция рисует на экране точку несколько быстрее
// за счет использования сдвига вместо умножения:
// 320 * у = 256 * у + 64 * у = у << 8 + у << 6
video_buffer[((у<<8}+(у<<6) )+х]=соlоr;
}
Все. С точки зрения Си эта функция уже оптимизирована настолько, насколько возможно. Я могу вам предложить только следующее:
§ Перепишите программу на встроенном ассемблере;
§ Не передавайте параметры;
§ Возможно, создайте справочную таблицу из 64000 элементов, каждый из которых содержит адрес видеобуфера, соответствующий координатам Х и Y.
Правда, я думаю, что использование справочной таблицы на самом деле только замедлит выполнение программы. Ведь операция индексирования таблицы может отнять больше времени, чем два сдвига и сложение, необходимые для вычисления адреса. Но вот первые два пункта заслуживают внимания. Начнем с использования глобальных переменных, заменяющих передачу параметров. Например, определим такие глобальные переменные:
int plot_x, plot_у, plot_color;
Тогда нашу функцию можно переписать, как показано в Листинге 18.4
Листинг 18.4. Другая версия функции построения пикселя.
void Plot_Pixel_Global(void)
{
video_buffer[((plot_y<<8) + (plot_y<<6))+plot_x]=plot_color;
}
Это уже будет работать значительно быстрее, но нам все равно перед вызовом нужно выполнить операцию присваивания для переменных plot_x, plot_у и plot_color. Следовательно, вопрос в следующем - отнимет ли операция присваивания глобальных переменных меньше времени, чем передача параметров, создание и удаление фрейма стека вместе со ссылками на параметры? Может да, а может и нет. Все зависит от ситуации. Но цель заслуживает того, чтобы попробовать испытать и этот метод.
Теперь перепишем процедуру на ассемблере. Результат показан в Листинге 18.5
Листинг 18.5. Версия функции рисования пикселя на ассемблере.
Plot_Pixel_Asm(int x,int у,int color)
{
_asm{
les di,video_buffer // загрузить регистр es значением
// сегмента видеобуфер
mov di,y // поместить в di у-координату пикселя
shl di,6 // умножить на 64
mov bx,di // сохранить результат
shl di,2 // умножить еще на 8 (итого, на 256)
add di,bx // сложить результаты
add di,x // прибавить х-компонент
mov al,BYTE PTR color // записать цвет в регистр аl
mov es:[di],al // нарисовать пиксель
}
}
Ладно, покончим с этим. Ассемблерный вариант работает всего лишь на 2 процента быстрее, чем версия на Си. На то есть две причины:
§ Во-первых, компилятор Си проделывает неплохую работу, транслируя программу в машинные коды. Хотя, как вы знаете, мы можем сделать это вручную, используя ассемблер;
§ Во-вторых, когда вы используете встроенный ассемблер, он сохраняет все регистры и позже восстанавливает их. Единственный способ избавления от этого - написать внешние ассемблерные функции с использованием MASM. В данном случае это вполне допустимо, поскольку мы оптимизируем такую важную операцию как построение пикселя.
Наконец, я хочу показать вам последний пример оптимизации, которая позволяет ускорить вывод на экран в два раза.
Оптимизация изображаемой линии
Игры типа DOOM и Wolfenstein 3-D не используют все известные техники трехмерных графических преобразований, как это делают, например, имитаторы полетов. В них применяются совершенно гениальные методы для создания трехмерных образов. Эти методы базируются на изображении большого количества линий, проходящих в одном направлении. Обычно рисуются обычно вертикальные или горизонтальные линии и только в удаленных предметах присутствуют диагональные прямые. Следовательно, мы должны научиться максимально быстро проводить горизонтальные и вертикальные линии. Сейчас мы поговорим о горизонтальных прямых.
Как мы проходили в пятой главе «Секреты VGA-карт», для изображения горизонтальной линии лучше всего применить функцию memcpy () (см. Листинг 5.6). Начальный и конечный адреса вычисляются из Х-координат крайних точек линии. Вернувшись назад к этому методу, приходится признать, что он не совсем совершенен, поскольку memcpyO для перемещения данных использует тип BYTE. Но профессионалы в программировании игр знают, что VGA-карта работает быстрее с данными типа WORD. Следовательно, нужно попытаться исправить этот недостаток и записывать в видеобуфер WORD вместо BYTE. Сейчас я собираюсь переписать функцию изображения горизонтальных линий, учитывая это пожелание.
Это не так легко, как кажется, потому что конечной целью должна быть забота о работоспособности. Вы видите, что когда вы пишите WORD в видеобуфер, вы в сущности строите два пикселя. Вы должны быть осторожны, принимая это во внимание во время рисования линий. Скажем, мы хотели изобразить линию, тянущуюся от точки (51,100) до точки (100,100). У нас получится линия, которая выглядит чем-то похожей на рисунок 18.4.
Завершим анализ изображения этой линии тем, что каждая конечная точка в действительности занимает один байт, а не два. Следовательно, мы должны написать программу, которая может управлять состоянием, когда конечные точки BYTE или WORD граничат. Программа из Листинга 18.6 делает это.
Листинг 18.6. Программа, изображающая горизонтальную линию и использующая для этого тип WORD (HLINEF.C).
H_Line_Fast(int xl,int x2,int у,unsigned int color)
{
unsigned int first_word, middle_word, last_word,line_offset,
index ;
// тестируем 1 бит начальной х-координаты
if ( (x1 & 0х0001))
{
first_word = (color << 8);
}
else
{
// заменить цвет в обоих байтах
first_word = ( (color<<8) | color) ;
}
// тестируем первый бит в х2
if( (х2 & 0х0001) )
{
last_word = ((color<<8) | color);
)
else
{
// поместить цвет только в старший байт
last_word = color;
}
// теперь мы можем рисовать горизонтальную линию,
// выводя сразу по два пикселя
line_offset = ( (у<<7) + (у<<5) ); // у*160, поскольку в линии 160 слов
// вычислить цвет в середине
middle_word = ((color<<8) | color);
// левая граница
video_buffer_w[line_offset + (x1>>1)]= first_word;
// середина линии
for (index=(x1>>1)+l, index<(x2>>l); index++)
video_buffer_w[line_offset+index] = middle_word;
// правая граница video_buffer_w[line_offset+(х2>>1)] = last_word;
}
В начале работы функция проверяет, находятся ли конечные точки на границе байта (BYTE)? Основываясь на результатах проверки, функция создает два слова (WORD): одно будет началом линии, а другое ~ концом. В зависимости от результатов начального теста эти два слова содержат либо один, либо два байта, представляющих цвет. Затем функция изображает линию. Это выполняется следующим образом:
§ Пишется первый WORD, соответствующий левой границе;
§ Создается цикл FOR, который предназначен для записи в WORD середины линии;
§ Пишется WORD, соответствующий правой границе линии.
Хотя эта функция почти в два раза длиннее, чем первоначальная Н_Line, она почти в два раза быстрее.
(Существуют, правда, небольшие накладные расходы при вычислении границы). Чтобы сделать ее еще быстрее, я мог бы переписать часть, которая изображает середину на встроенном ассемблере, но я думаю, что вы и сами сделаете это в качестве легкого упражнения.
ИТОГ
Мы узнали множество полезных вещей, и даже если вы никогда не начнете писать игры, определенно станете более умелым программистом, поскольку приобрели новые навыки. У вас появилось несколько мощных методов оптимизации. Только не думайте, что лучше этих методов не бывает или что предложенные способы оптимизации годятся на все случаи жизни.
Наше длинное путешествие в мир оптимизации подошло к концу, и я хочу закончить его приглашением в следующую, девятнадцатую главу, где мы, наконец, на примере игры Warlock применим все, чему уже научились.Итак, до следующей главы.
ТЕХНИКА СОЗДАНИЙ ПАРАЛЛАКСА
Вы, конечно, не раз обращали внимание, глядя из окна автомобиля, что близлежащие объекты перемещаются гораздо быстрее, чем удаленные. Этот эффект получил пугающее название — параллакс. В повседневной жизни вы настолько часто наблюдаете это явление, что, скорее всего, считаете его чем-то само собой разумеющимся.
Параллакс является только одним из многих эффектов нашего визуального восприятия. Другой хорошо известный эффект - это перспектива. Перспектива и параллакс вкупе с другими ощущениями, такими как равновесие и слух, формируют завершенную картину окружающей среды.
Что же такое параллакс и каким боком он связан с программированием игр? Параллаксное смещение - это технический прием, применяемый в видеографике, когда два или более слоя графического изображения перемещаются в одну сторону, но с различными скоростями. Такое относительное смещение слоев обеспечивает некие визуальные эффекты, необходимые для реалистичной имитации глубины пространства и движения.
Теория игр
Следующая тема не слишком тесно связана с видеоиграми, а, скорее, относится к стратегическим играм типа шахмат. Тем не менее, я хочу осветить ее, чтобы сообщить вам некоторые интересные идеи.
Теория игр является разделом математики, слишком трудным и слишком абстрактным, для того чтобы применять ее в видеоиграх. Она оперирует набором правил для вычисления оптимального решения или алгоритма, позволяющего игроку победить. Обычно это делается с помощью множества матриц, вероятностей и линейного программирования. Тем не менее, мы можем извлечь из теории игр немало полезного.
Как правило, требуется решить проблему путем оценки текущего положения или игровой ситуации, подразумевая, что игрок точно оценивает свои действия в данной точке в данный момент времени и далее старается улучшить свою позицию. Чтобы выполнить это на компьютере, нам надо:
§
Во-первых, мы создаем несколько видов целевых функций, которые могли бы оценивать нашу текущую позицию;
§ Далее мы пробуем применить новую тактику или изменить позицию с тем, чтобы посмотреть, улучшит ли это наше положение;
§ Если это достигается, мы выполняем это действие;
§ Если нет, испытываем следующую тактику.
Именно по такой схеме работают шахматные программы. Компьютер постоянно ведет подсчет своих собственных очков и очков противника, зависящих от текущего расположения фигур, и пытается найти наиболее оптимальный ход. Этот «мыслительный процесс» может в действительности идти на многих уровнях. Компьютер будет проверять каждую из возможностей: если я сделаю так, то мой противник мог бы предпринять то-то..., а если я поступлю эдак, соперник сделает следующее... Так может продолжаться до бесконечности, хотя человек может проиграть даже в том случае, если компьютер будет делать только два шага «проверка-действие».
В видеоиграх эта тактика может быть использована как самое высокоуровневое управление всеми остальными функциями. К примеру, каждую минуту или около этого, компьютер мог бы просчитывать ходы и оценивать, как он действует. Если все его ходы удачны, он мог бы действовать так и дальше. Однако, если какая-то попытка оказалась неудачной, значит, пришло время для пересмотра тактики.
Теперь все вместе: Демонстрационная программа работы с клавиатурой
Как теперь насчет того, чтобы собрать все написанное про клавиатуру в одну кучу? В Листинге 3.7 представлена демонстрационная программа, которая состоит из вызовов уже написанных в этой главе функций. Она показывает скан-коды нажатых клавиш и состояние клавиш Ctrl и Alt. Если вы нажмете Q, программа завершит свою работу.
Листинг 3.7. Демонстрационная программа работы с клавиатурой (KEY.C).
// ВКЛЮЧАЕМЫЕ ФАЙЛЫ ///////////////////////////////////////
#include <dos.h>
#include <bios.h>
#include <stdio.h>
#include <math.h>
#include <conio.h>
#include <graph.h>
// ОПРЕДЕЛЕНИЯ ///////////////////////////////////////////////
// битовые маски для управляющих клавиш
#define SHIFT_R 0х0001
#define SHIFT_L 0х0002
#define CTRL 0х0004
#define ALT 0х0008
#define SCROLL_LOCK_ON 0х0010
#define NUM_LOCK_ON 0х0020
#define CAPS_LOCK_ON 0х0040
#define INSERT_MODE 0х0080
#define CTRL_L 0х0100
#define ALT_L 0х0200
#define CTRL_R 0х0400
#define ALT_R 0х0800
#define SCROLL_LOCK_DWN 0х1000
#define NUM_LOCK_DWN 0х2000
#define CAPS_LOCK_DWN 0х4000
#define SYS_REQ_DWN 0х8000
// Значения скан-кодов. Внимание: каждая клавиша продуцирует только
// один скан-код, поэтому определения даны только для символов
// нижнего регистра. Например, одна и та же клавиша соответствует
// символу "1" (нижний регистр) и символу "!" (верхний регистр).
// Однако пользоваться надо все равно определением SCAN_1
#define SCAN_ESC 1
#define SCAN_1 2
#define SCAN_2 3
#define SCAN_3 4
#define SCAN_4 5
#define SCAN_5 6
#define SCAN_6 7
#define SCAN_7 8
#define SCAN_8 9
#define SCAN_9 10
#define SCAN_0 11
#define SCAN_MINUS 12
#define SCAN_EQUALS 13
#define SCAN_BKSPаааааааа 14
#define SCAN_TABааааааааа 15
#define SCAN_Qааааааааааа 16
#define SCAN_Wааааааааааа 17
#define SCAN_Eааааааааааа 18
#define SCAN_Rааааааааааа 19
#define SCAN_Tааааааааааа 20
#define SCAN_Yаааааааа 21
#define SCAN_Uаааааааа 22
#define SCAN_Iаааааааа 23
#define SCAN_0аааааааа 24
#define SCAN_Pаааааааа 25
# define SCAN_LFT_BRACKET 26
#define SCAN_RGT_BRACKET 27
#define SCAN_ENTERаааа 28
#define SCAN_CTRLааааа 29
#define SCAN_Aаааааааа 30
#define SCAN_Sаааааааа 31
#define SCAN_Dаааааааа 32
#define SCAN_Fаааааааа 33
#define SCAN_Gаааааааа 34
#define SCAN_Hаааааааа 35
#define SCAN_Jаааааааа 36
#define SCAN_Kаааааааа 37
#defane SCAN_Lаааааааа 38
#define SCAN_SEMIааааа 39
#define SCAN_APOSааааа 40
#define SCANJTILDEаааа 41
#define SCAN_LEFT_SHIFT 42
#define SCAN_BACK_SLASH 43
#define SCAN_Zаааааааа 44
#define SCAN_Xаааааааа 45
#define SCAN_Cаааааааа 46
#define SCAN_Vаааааааа 47
#define SCAN_Bаааааааа 48
#define SCAN_Nаааааааа 49
#define SCAN_Mаааааааа 50
#define SCAN_COMMAаааа 51
#define SCAN_PERIODааа 52
#define SCAN_FOWARD_SLASH 53
#define SCAN_RIGHT_SHIFT 54
#define SCAN_PRT_SCRNа 55
#define SCAN_ALTаааааа 56
#define SCAN_SPACEаааа 57
#define SCAN_CAPS_LOCK 58
#define SCAN_F1ааааааа 59
#define SCAN_F2ааааааа 60
#define SCAN_F3ааааааа 61
#define SCAN_F4ааааааа 62
#define SCAN_F5ааааааа 63
#define SCAN_F6ааааааа 64
#define SCAN_F7ааааааа 65
#define SCAN_F8ааааааа 66
#define SCAN_F9ааааааа 67
#define SCAN_F10аааааа 68
#define SCAN_Fllаааааа 133
#define SCAN_Fl2аааааа 134
#define SCAN_NUM_LOCKа 69
#define SCAN_SCROLL_LOCKа 70
#define SCAN_HOMEаааааааа 71
#define SCAN_UPаааааааааа 72
#define SCAN_PGUPаааааааа 73
#define SCAN_NUM_MINUSааа 74
#define SCAN_LEFTаааааааа 75
#define SCAN_CENTERаааааа 76
#define SCAN_RIGHTааааааа 77
#define SCAN_NUM_PLUSаааа 78
#define SCAN_ENDааааааааа 79
#define SCAN_DOWNаааааааа 80
#define SCAN_PGDWNааааааа 81
#define SCAN_INSааааааааа 82
#define SCAN_DELааааааааа 83
// LL=¦гLL /////////////////////////////////////////////
unsigned char Get_Ascii_Key(void)
{
// +ёыш т сєЇхЁх ъыртшрЄєЁv хёЄ№ ёшьтюы, ЇєэъЎш тючтЁр•рхЄ хую
// ASCII-ъюф. +ёыш ёшьтюыр эхЄ, тючтЁр•рхЄё 0.
if (_bios_keybrd(_KEYBRD_READY)) return(_bios_keybrd(_KEYBRD_READ)) ;
else return(0);
} // ъюэхЎ ЇєэъЎшш
////////////////////////////////////////////////////////
unsigned int Get_Control_Keys(unsigned int mask)
(ааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааа
// ЇєэъЎш тючтЁр•рхЄ ёюёЄю эшх ы¦сющ єяЁрты ¦•хщ ъыртш°ш
return(mask & _bios_keybrd(_KEYBRD_SHIFTSTATUS));
}//ъюэхЎ ЇєэъЎшш ////////////////////////////////////////////////////////
unsigned char Get_Scan_Code(void) (
// ЇєэъЎш тючтЁр•рхЄ ёърэ-ъюф эрцрЄющ ъыртш°ш // шёяюы№чєхЄё тёЄЁюхээvщ рёёхьсыхЁ
_asm{
mov ah,01hааа ;ЇєэъЎш 01h - яЁютхЁър эрцрЄш ъыртш°ш
int 16hаааааа ;тvчют яЁхЁvтрэш
jz emptyааааа ;эхЄ ёшьтюыр - тvїюфааааааааа
mov ah,00hааа ;ЇєэъЎш 0 - яюыєўхэшх ёърэ-ъюфр
int 16hаааааа ;тvчют яЁхЁvтрэш
mov al,ahаааа ;яхЁхьх•рхь Ёхчєы№ЄрЄ шч L= т AL
xor ah,ahаааа ;юсэєы хь L=
jmp doneааааа ;Ёхчєы№ЄрЄ тючтЁр•рхЄё т L-
empty:
xor ax,axааа ;юсэєы хь AX
done:
} //ъюэхЎ рёёхьсыхЁэюую сыюър
} // ъюэхЎ ЇєэъЎшш
// +T=+T=L- ¦¦++¦L¦¦L ///////////////////////////////////////
void main(void)
{
unsigned char key;
int done=0;
unsigned int control;
_clearscreen(_GCLEARSCREEN);
while(!done)
{
_settextposition(2,0);
if ( (key = Get_Scan_Code()) ) printf("%c %dа ",key,key);
// яЁютхЁър эр эрцрЄшх Ctrl ш Alt
if (Get_Control_Keys(CTRL))
printf("\ncontrol key pressed");
if (Get_Cbntrol_Keys(ALT))
printf("\nalt key pressedааа ");
if (key==16) done=1); // 16 Ч ¤Єю ёърэ-ъюф ъыртш°ш Q
}// ъюэхЎ Ўшъыр while
} // ъюэхЎ ЇєэъЎшш main
Точки
Мы уже дали определение точке. Она представляет собой позицию на плоскости, которую можно описать парой координат X и Y. Давайте напишем маленькую программу на Си, рисующую точки на экране. Листинг 4.1 показывает такую программу.
Листинг 4.1. Программа, рисующая точки (POINTY.C).
#include <stdio.h>
#include <graph.h>
void main(void)
{
int х, у, index, color;
// перевести компьютер в графический режим
_setvideomode(_VRES16COLOR); // режим 640х480, 16 цветов
// нарисовать 10000 точек на экране, расположенных случайным образом
for(index = 0; index<10000; index++)
{
// получить случайные координаты и цвет
х = rand()%640;
у
= rand()%480;
color = rand()%16;
_setcolor(color); // установить цвет для рисования точки
_setpixel(х,у); // нарисовать точку
} // конец цикла for
// ждать нажатия клавиши
while(!kbhit()){}
// восстановить текстовый режим
_setvideоmоde(_DEFAULTMODE) ;
} // конец функции main
Теперь разберемся, что делает эта программа:
Компьютер переводится в режим VGA с помощью вызова функции Си _setvideomode ( VRES16COLOR). Это функция из графической библиотеки Microsoft. После этого программа входит в главный цикл. В структуре
FOR каждый раз случайным образом генерируются три числа: одно для цвета и два других для координат (х,у) позиции точки, которую мы хотим нарисовать:
§ Затем мы используем библиотечную функцию _setpixel(х,у), чтобы нарисовать точку на экране. Программа делает это 10000 раз, а потом останавливается;
§ Затем программа ждет нажатия любой клавиши, после чего происходит выход в DOS.
Если вы запустите программу несколько раз, то сможете заметить, что точки все время оказываются в одних и тех же местах. Как это получается? Дело в том, что мы пользуемся функцией rand (), которая не является в полном смысле генератором случайных чисел. Она возвращает так называемые псевдослучайные числа. Чтобы избежать этого, вам надо всякий раз при запуске устанавливать
генератор случайных чисел с разными начальными значениями. Вставьте в начало программы функцию srand(int) — и все будет в порядке,
Точки, линии и области
Все мы видели игры типа Asteroids, Spectre и Major Havoc. Многие из них имеют общие черты первых видеоигр — все они выполнены линиями и все они, как правило, плоские. Кстати, в предыдущих главах мы еще ничего не делали для рисования проекции кроме отображения точки.
Точки, линии, многоугольники и объекты в трехмерном пространстве
Как мы уже видели, точка в трехмерном пространстве имеет три координаты (x,y,z). Этой информации достаточно, чтобы ее однозначно определить в пространстве.Будет логично, если следующим объектом, который мы определим, станет линия. Линией называют отрезок, соединяющий две точки в трехмерном пространстве. Мы можем даже написать структуры данных, определяющие точку и линию.
Листинг 6.1. Определение точки и линии в трехмерном пространстве.
// структура, описывающая точку в трехмерном пространстве
typedef struct point_typ
{
float x,y,z // координаты точки
} point, *point_ptr;
// структура, описывающая линию в трехмерном пространстве
typedef struct line_typ
{
point start, end; // линия задается двумя точками
} line, *line_ptr;
Используя структуры из Листинга 6,1, давайте определим линию, которая начинается в точке (0,0,0) и идет в точку (100,200,300)
line linel;
linel.start.x = 0;
linel.start.у= 0;
linel.start.z = 0;
linel.end.x = 100;
linel.end.у = 200;
linel.end.z = 300;
Теперь мы имеем описание линии. Если мы захотим, то сможем создать трехмерный мир, состоящий из линий и точек, но это будет скучным и тоскливым занятием.
Нам нужен больший уровень абстракции для моделирования объектов, и для этого нам пригодятся многоугольники. Как вы поняли из четвертой главы, многоугольник - это множество вершин, соединенных отрезками прямых. Вершины определяют границы многоугольника. В трехмерном пространстве Многоугольники очень похожи на своих двухмерных собратьев. Попробуем определить трехмерный треугольник. Он может выглядеть так, как это изображено иа рисунке 6.2.
Как вы можете видеть, на листе бумаги весьма несложно представить трехмерный объект. Мы будем использовать для описания «диагональный вид». Позже,мы к этому еще вернемся, а сейчас важно понять идею.
Описать многоугольник довольно просто: мы применим старое определение многоугольника и просто добавим к нему несколько атрибутов для создания новой законченной структуры. В Листинге 6.2 показана такая структура.
Листинг 6.2. Определение трехмерного многоугольника.
// структура, описывающая многоугольник
typedef struct polygon_typ
{
int num_vertices; // число
вершин
vertices[MAX VERTICES]; // координаты
вершин
int color; // цвет многоугольника
}polygon, *polygon_ptr;
Как можно заметить, в структуре описаны вершины и цвета. Эти составляющие необходимы для правильного отображения. Теперь, когда у нас есть структура, описывающая многоугольник, следующим шагом будет определение объекта на основе многоугольников. На рисунке 6.3 продемонстрирован один из таких объектов.
Теперь мы можем добавить еще один уровень к нашему описанию. Объект - это набор многоугольников. Создадим структуру, которая бы поддерживала эту концепцию:
Листинг 6.3. Описание трехмерного объекта на основе многоугольников.
// структура, описывающая объект
typedef struct object_typ
{
int num_faces; // число
граней
polygon faces[max_faces]; // грани, представленные многоугольниками
float xo,yo,zo; // координаты объекта в пространстве
int visible; // виден ли объект на экране?
} object, *object_ptr;
Структура данных в Листинге 6.3 описывает объект, который образован , множеством многоугольников или поверхностей. Используя эти структуры данных и определения, мы можем создать несколько трехмерных объектов: космический корабль, планету и окружающее космическое пространство.
Чтобы поместить объекты в трехмерное пространство, мы должны знать их пространственное расположение. То есть мы должны определить значения хо, уо и zo для каждого предмета. Так же, как и в случае с двухмерными объектами (которые мы уже обсуждали в четвертой главе), пространственные объекты мы будем определять в собственных локальных системах координат (0,0,0). Затем, когда мы будем перемещать объект, мы его просто переведем в конечную позицию.
Для наших структур это будет точка (xo,yo,zo).
Решением этой задачи будет простой перенос каждой из точек объекта, так же, как мы это делали для двухмерных объектов. Мы можем проверить этот метод и для объемных фигур. Например, представим себе куб, с вершиной в точке (2,2,2) (см. рис. 6.4). Если мы посмотрим на куб, то увидим, что он состоит из восьми вершин и шести поверхностей. Используя наши структуры данных, мы можем описать куб как объект с шестью гранями. Проблема, возникающая в данном случае, состоит в том, что это не самый лучший способ описания объекта. Ведь любая поверхность ограничена четырьмя точками и каждая из этих точек является общей еще для двух поверхностей. Это значит, что описание избыточно.
Возможно, более удачной окажется структура данных, содержащая список вершин. В этом случае избыточности не возникает. Однако при этом структура станет более общей и сложной, поскольку:
§ Мы должны будем иметь указатели либо индексы, или то и другое вместе для ссылки на вершины, необходимые для построения геометрической фигуры. Это увеличивает время распознавания данных объектов;
§ Наши структуры могут использовать заранее определенные массивы для хранения вершин и многоугольников. Это неэффективно использует память. Массивы должны быть одного размера, так как, независимо от того, используем ли мы один элемент массива или весь массив, нам необходимо отводить место под максимальное число элементов.
Эти факты надо принимать во внимание, когда создаете структуры для трехмерных объектов. Таким образом, для наших целей структуры данных из Листингов 6.2 и 6.3 являются наиболее простыми для работы. Если же вы хотите создать набор реальных трехмерных структур, то должны использовать другую тактику.
В общем случае представление двух- и трехмерных объектов сильно зависит от игры, которую вы пишете, от размера используемой памяти и т. д. (Наша цель сейчас - понять механизмы трехмерной графики и рендеринга, а не поиск наиболее эффективных способов представления данных в компьютере.
Это зависит от используемых алгоритмов и структур данных.)
Просуммируем все вышесказанное:
§ Трехмерные объекты состоят из вершин;
§ Эти вершины соединяются поверхностями или многоугольниками, которые задают границы объекта;
§ Объекты описываются относительно начала координат;
§ Существует много способов представления трехмерных объектов и вы должны выбрать тот, который устраивает вас по скорости и объему памяти.
Трансляция объектов
Трансляцией объекта будем называть его перемещение, при котором не меняется ни угол поворота, ни размер объекта. Давайте воспользуемся нашей структурой данных для определения конкретного объекта, с которым будем экспериментировать и в дальнейшем. К примеру, пусть это будет астероид. На рисунке 4.7 показан его внешний вид. Листинг 4.5 содержит фрагмент, описывающий наш астероид.
Листинг 4.5. Описание астероида.
Object asteroid;
// определим поля
asteroid.num_vertices = 6; //шести вершин будет достаточно
asteroid.color
= 1; //цвет астероида - синий
asteroid.х0 = 320; // поместить астероид в центр экрана
asteroid.у0 = 240;
//теперь задаем координаты вершин как смещения относительно точки х0, у0
asteroid.vertices[0].х = 4.0;
asteroid.vertices[0].у = 3.5;
asteroid.vertices[1].х = 8.5;
asteroid.vertices[1].у = -3.0;
asteroid.vertices[2].x = 6;
asteroid.vertices[2].у = -5;
asteroid.vertices[3],x = 2;
asteroid.vertices[3].у = -3;
asteroid.vertices[4].х = -4;
asteroid.vertices[4].у = -6;
asteroid.vertices[5].х = -3.5;
asteroid.vertices[5].у = 5.5;
Конечно, в настоящих играх вам не придется так определять все свои объекты. Напротив, вы можете загрузить координаты вершин из файла или сгенерировать их (например, AutoCad использует формат DXF, содержащий списки вершин вместе с другими свойствами объекта; после того как DXF-файл загружен, координаты вершин считываются из него в соответствующие структуры). Но поскольку мы создаем всего один астероид, то можно описать его и вручную.
Теперь давайте чуть-чуть подумаем. Мы можем нарисовать вершины объекта относительно его положения на экране, которое описывается как (хо,уо). Если же мы хотим передвинуть объект, то можно сделать так:
x0=x0+dx
y0=y0+dy
где dx и dy — это количество пикселей, на которое мы хотим переместить объект по оси Х или Y.
Это все, что можно сказать о трансляции объектов. Теперь поговорим о масштабировании.