Когда бревна высохли, Урфин Джюс принялся за работу. Он начерно обтесывал туловища, делал заготовки для рук и ног. Урфин задумал на первое время ограничиться пятью взводами солдат: он считал, что этого вполне достаточно, чтобы захватить власть над Голубой страной.
(А.Волков)
Недавно мы провели опрос на нашем сайте: какие новые рубрики хотели бы увидеть в журнале наши читатели. С хорошим отрывом от серебряного призера лидировала тема «Как сделать игру своими руками».
Как говорили древние римляне: «Глас народа - глас божий». И вот собрались мы в редакции и начали размышлять: как бы это получше выполнить
И решили мы, что нашему читателю наверняка уже постыли пустопорожние рассуждения «о том, как это в принципе делается» - а хотелось бы ему, то есть вам, получить простые и понятные рекомендации. С помощью которых можно было бы с нуля взять и сделать игру.
Дело это, как известно, требует участия разных специалистов: и сценаристов-дизайнеров, и художников-аниматоров, и программистов, и звуковиков. О работе каждого из них мы еще поговорим в свое время, но начнем мы все же с программирования. Потому что только программист может сделать из набора замыслов и набросков - игру.
Первая задача
Сегодня мы с вами должны суметь написать игровую программу. |
Итак, сегодня мы с вами должны суметь написать игровую программу. Давайте для начала определимся - какую именно.
Понятно, что это будет пока что не Doom III и не Heroes of Might & Magic V. Время трехмерных движков и искусственного интеллекта еще придет (и, между прочим, очень скоро). Но сперва нам надо бы освоиться с игровым программированием. Чтобы потом не утонуть в более сложных вещах.
С другой стороны, не хотелось бы заниматься каким-нибудь унылым «чет-нечетом». Во-первых, желающих в такое поиграть мы никогда в жизни не найдем, а во-вторых, устройство настоящих игр на «угадайки» ничуть не похоже.
А начнем мы с двумерной аркады. Это позволит нам, во-первых, попробовать на зуб плоский движок - впоследствии мы к нему приделаем и другие «начинки», например, стратегическую. Во-вторых, мы усвоим (а для тех, кто их знает - повторим) азы программирования.
А в-третьих…
В-третьих, мы познакомимся с игровым пакетом «ЛКИ-Creator», инструментом для игрового программирования, написанным специально для нашего журнала.
Что такое ЛКИ-Creator?
В наше время программы не принято писать «с чистого листа». Многие элементы будущего шедевра уже сделаны, и проще их позаимствовать, нежели создавать самостоятельно. Так с давних лет устроено программирование. И в первую очередь разработчики обычно стремятся пользоваться готовыми решениями для проектирования интерфейса. Этой цели служит, например, библиотека DirectX.
Но и ею в чистом виде пользуются достаточно редко. Над ней существует немало разных «надстроек». ЛКИ-Creator - одна из них, предназначенная специально для игр.
Заметим, что это не один из популярных в наши дни «игровых конструкторов». Это пакет, который встраивается в мощный и современный язык Delphi. Мы программируем в Delphi, пользуясь дополнительными преимуществами ЛКИ-Creator.
ЭТО ИНТЕРЕСНО: чем это лучше, чем язык «специально для игр»? Да тем, что такие языки всегда сильно урезаны в возможностях. С их помощью можно сделать только то, что заранее предположили их создатели - тогда как Delphi или, скажем, С++ пригодны для решения любой программистской задачи. Кроме того, код их редко бывает отшлифован так, как у трансляторов Delphi - а потому результат Delphi работает намного быстрее.
Пакет ЛКИ-Creator можно взять на компакт-диске нашего журнала, в разделе «О компакт-диске». В этом номере мы публикуем сокращенную версию - все для создания двумерного движка, в последующих же будем ее постепенно пополнять.
Теперь пакет ЛКИ-Creator можно скачать на сайте.
Почему Delphi?
В самом деле, почему за основу мы взяли Delphi, а не, скажем, С++?
Не хотелось бы вдаваться в спор о том, какой из двух языков лучше; и так куча форумов в интернете полна ругани на эту тему. У обоих языков есть свои преимущества. Вот какие достоинства Delphi заставили нас выбрать именно его: Программу на Delphi намного легче понимать и отлаживать, особенно для не очень опытного программиста. Освоить Delphi можно за несколько часов, а вот привыкнуть к С++ гораздо сложнее. Работая с Delphi, мы можем немалую часть работы выполнить «мышкой», просто расставив нужные элементы интерфейса по экрану. Аналогичные средства Visual C несколько менее удобны для начинающего.
Конечно, у С++ тоже есть свое преимущество: для него есть больше библиотек с готовым кодом. Но, во-первых, в последнее время Delphi его догоняет, а во-вторых, для этого-то нам и пригодится ЛКИ-Creator.
Популярное мнение, будто бы программы на С работают намного быстрее, нежели на Delphi, уже давно не соответствует действительности. Оно отстало от жизни примерно лет на 7-8. Тесты показывают, что код, создаваемый Delphi, обычно не отстает от кода Visual C, а зачастую его превосходит.
Что нужно знать, чтобы начать работу?
Вообще говоря - ничего. Мы не рассчитываем, что нашими читателями будут сплошь профессиональные программисты. Для тех, кто совсем не знаком с языком Delphi, в конце этой статьи есть приложение, где рассказано все самое главное о том, как работать на этом языке.
Однако если вы хотите по-настоящему научиться программировать, вам будет лучше все же не ограничиваться этой статьей. По Delphi есть много отличных учебников - например, авторства Фаронова или Бобровского, они лучше всего подходят для нашей цели. Не пожалейте времени и денег и купите их - будет проще. Тем более, что вам необязательно изучать их полностью: львиная доля текста там посвящена работе с конкретными элементами интерфейса, которые в игровом программировании вам не понадобятся. Ну, а всю специфику работы с игрой мы дадим вам в наших статьях.
Итак, если вы совсем не знакомы с Delphi, тогда начните чтение с приложения. Ну, а коль скоро какое-то представление имеется - тогда читайте дальше, а в приложение вы сможете заглянуть по мере необходимости. Если какое-то слово покажется вам незнакомым или непонятным - вероятно, его удастся найти в нашем «Микроучебнике Delphi».
Ну, а для тех, кто немного знает Delphi и хочет только научиться применять его к играм, трудностей вообще не должно возникнуть. Из этой статьи они узнают, как быстро включить в свою программу двумерный графический движок, основанный на DirectX, и написать игру в считанные минуты.
Звездный эскорт
Итак, вот проект игры, которую мы с вами напишем для начала.
Имеется космический транспорт, который медленно и мрачно ползет от одной звездной базы к другой. Транспорт неплохо защищен, но лишен оружия. Им управляет автоматика (которая двигает его из пункта А в пункт Б с постоянной скоростью), так что сам по себе он не может уклоняться от атак злых инопланетян. Спасать эту баржу от атак предстоит нам - верхом на верном истребителе.
С разных сторон время от времени налетают инопланетяне, которые стреляют по нам своими снарядами, а равно норовят протаранить - и нас, и транспорт. Мы вооружены аналогичными ракетами. Вражеские снаряды можно отстреливать ракетой, а можно от них уклоняться. Ни наши, ни чужие заряды не обладают самонаведением и через некоторое время тихо дохнут сами по себе.
Соответственно, если нас или транспорт уничтожили - это поражение. Если же баржа в относительной целости и сохранности доползла до базы - это победа.
Задача вроде бы ясна? Конечно, это не Doom IV, но тем не менее вполне полноценная аркада. А в дальнейшем мы сделаем ее поинтереснее - да и сейчас код будет у вас в руках, так что вы и сами сумеете развить идею.
Подготовка к работе
Нам понадобится установить Borland Delphi. Нам подойдет любая версия от 5.0 до 7.0 (и, скорее всего, более поздние сгодятся тоже). Я рекомендую взять 5-ю версию: большинство преимуществ 7-й нам в ближайшем будущем не пригодятся. Между тем «пятерка» компактнее, а ее справочная система не загромождена совершенно бесполезной для нас информацией.
Устанавливая программу, вы смело можете отказаться от всего, что связано с базами данных, отчетами, поддержкой CORBA и т.п.: все это нам ни к чему.
Создайте где-нибудь каталог для проектов. Назовем его, скажем, LKI.
Если все готово, возьмите с нашего компакт-диска (раздел «О компакт-диске», подраздел «Игра своими руками») файл с текстами программ и картинками и распакуйте его в каталог проектов.
У нас должно получиться три подкаталога. В одном - Units - хранятся библиотеки DirectX и модули пакета ЛКИ-Creator. В другом - Project - мы будем работать; туда заблаговременно положены картинки, которые нам понадобятся. В третьем - Escort - готовая программа, которая должна у нас получиться.
Установка пакета ЛКИ-Creator |
До начала работы осталось сделать последнее действие: установить пакет ЛКИ-Creator. На этом диске - его облегченная версия, только двумерная часть. В меню Delphi откройте пункт Component, в нем выберите Install Component. Перейдите на закладку Into new package и заполните пустые строчки, как показано на рисунке (в верхней строчке проще всего выбрать файл LKI2dEngine.pas с помощью кнопки Browse, а в нижней просто запишите LKI). После чего нажмите OK и выберите Install. В верхней панели Delphi у вас должна появиться закладка LKI.
Теперь осталось только начать наш проект. В меню File выбираем New Application…
Форма
Первое, что мы видим перед собой - проект окна будущей программы (тот, над которым написано Form1). В Delphi это называется формой.
Инспектор объектов |
Слева от нее - окошко инспектора объектов (см. рисунок). На нем мы можем редактировать свойства окна и тех элементов, которые мы на него поместим. Можно сразу же заменить заголовок окна (подредактировав пункт Caption) на «Звездный эскорт». Есть еще верхняя панель меню и окно кода, в котором можно редактировать саму программу.
Мышкой, как обычно делается в Windows, раздвинем форму пошире - чем больше будет наша игровая область, тем лучше. Откроем в верхней панели закладку LKI - там всего один значок, в виде пейзажика - выберем этот значок и поставим (щелчком мыши) на форму.
Это интересно: мы делаем все в окне, хотя чаще игры пишутся в полноэкранном режиме. Это не потому, что наш пакет такого не умеет; просто в оконном варианте будет намного проще найти ошибки. А перевестись в полноэкранный режим мы еще успеем. Кроме того, желающие смогут поставить на окно формы кнопки и другие интерфейсные элементы Delphi.
Теперь на нашей форме есть DirectX-движок (именно это представляет собой тот пунктирный прямоугольничек). Растянем его на всю - или почти всю - площадь окна. Сразу же в инспекторе объектов дадим ему какое-нибудь имя (свойство Name) покороче стандартного; например, Engine (в окно кода изменение вносится автоматически).
Если вы никогда не работали в Delphi, попробуйте сначала поиграться с другими элементами интерфейса, например - теми, что находятся в закладках Standard и Additional. Убедитесь, что по запуску проекта (клавиша F9) они оказываются вполне готовыми к работе.
Это важно: прежде, чем первый раз запустить приложение, не забудьте его сохранить (лучше всего под уникальным именем). Рекомендуется проставить перед запуском автосохранение (Tools - Environment options - Preferences). Кроме того, может понадобиться указать путь к нашим библиотекам: это делается в пункте меню Project - Options - Directories/Conditionals, нажмите кнопку справа от строки Search Path и выберите ваш каталог Units вручную.
Теперь запустим приложение с DirectX-экраном (кнопка F9); экран у нас пустой. В отличие от кнопок и прочих стандартных элементов, DirectX-окна требуют небольшой дополнительной работы. Их надо активировать после запуска программы и уничтожить при ее закрытии.
Зайдем в инспектор объектов и откроем в нем свойства формы (а не DirectX-экрана: верхнее окошко инспектора определяет, с каким из объектов мы сейчас работаем). Перейдем на закладку событий (Events).
Здесь нас интересуют два события. OnCreate и OnDestroy. Эти события вызываются, соответственно, когда наше приложение открывается и когда оно закрывается.
Это интересно: конечно, можно было бы объяснить поподробнее механизм событий Windows. Однако, работая с DirectX, мы почти никогда не будем к ним обращаться, и поэтому такого краткого знакомства пока что хватит. А тем, кому интересно разобраться в деле, стоит почитать упомянутые выше учебники.
Дважды щелкнем по первому из них (OnCreate). Мы попадаем в окно кода, и нам предлагается написать программу для этого события. Вставим туда для начала единственную строчку:
Engine.Init(Form1);
Эта строка велит нашему движку запуститься, а также сообщает ему, что он расположен на форме Form1.
C закрытием формы будет не сложнее:
Engine.ShutDown;
Теперь мы можем запустить наше приложение и убедиться в том, что экран DirectX заработал. Правда, на нем ничего особенного пока нет.
Это интересно: если угодно, можно перед переходом к следующему этапу поиграться со свойствами движка. Для этого попробуем в инспекторе объектов изменить значения полей ColouredBack, BkColour и FName. Первое определяет, будет ли у нас цветной фон или же «обои» (если true, рисуем однотонный цветной фон), BkColour - цвет этого фона, а FName - имя файла картинки, используемой в качестве «обоев».
Игровой мир
Теперь настала пора заселить наш мир. Для этого существует специальный класс объектов - TLKIGameWorld. Нам нужно будет породить от него класс-потомок, который унаследует все его свойства и приобретет дополнительные.
Предварительное действие: в разделе используемых модулей (самое начало файла, после слова Uses) надо добавить два модуля: DDUtil и DirectInput8.
Итак, направляемся в раздел интерфейса нашего модуля, и сразу после описания формы пишем следующий текст (см. «Класс TStarEscortWorld»).
Класс TStarEscortWorld
TStarEscortWorld = class(TLKIGameWorld)
сonstructor Create(aTick : integer;
aEngine : TLKI2dEngine);
function Process(Tick : integer) : boolean; override;
end;
Здесь мы описали две процедуры, которые нам придется переопределить: конструктор, задающий изначальные параметры игрового мира, и функцию Process, которая каждый такт игрового цикла проверяет состояние всех игровых объектов, опрашивает данные с клавиатуры и перерисовывает все, что должно отобразиться на экране.
Задаем начальные значения
Спустимся в раздел кода (Implementation), скопируем туда заголовок конструктора, снабдив его указанием на объект:
constructor TStarEscortWorld.Create(aTick : integer; aEngine : TLKI2dEngine);
В нашем конструкторе нам нужно: задать размеры игрового мира (в координатной сетке, совпадающей с экранными координатами); задать начальное положение игрового окна (в координатах игрового мира); загрузить в игру все картинки; поставить те объекты, что есть на экране с самого начала: базы, транспорт, наш корабль - ну и, для красоты, энное количество неподвижных звезд.
Посмотрим на код («Конструктор игрового мира»)…
Конструктор игрового мира
constructor TStarEscortWorld.Create(aTick : integer;
aEngine : TLKI2dEngine);
var i : integer;
begin
// Размеры и координаты
inherited Create(aTick, aEngine, 0, 0,
15000, 3000);
StartX := 1000;
StartY := 1100;
// Грузим картинки
AddSprite('Ship.bmp',2,true,5); //0
AddSprite('st1.bmp'); //1
AddSprite('st2.bmp'); //2
AddSprite('st3.bmp'); //3
AddSprite('st4.bmp'); //4
AddSprite('st5.bmp'); //5
AddSprite('Base.bmp'); //6
AddSprite('Trans.bmp',1,true,5); //7
AddSprite('Rocket.bmp',1,true,5); //8
AddSprite('Alien1.bmp',2,true,5); //9
AddSprite('Alien2.bmp',1); //10
AddSprite('Missile1.bmp',1,true,5); //11
AddSprite('Bang1.bmp'); //12
AddSprite('Bang2.bmp'); //13
AddSprite('Bang3.bmp'); //14
// Наш корабль
AddObj(true, 1, 1150, 1250, 30, 0, 150, 13);
Objects[0].AlwaysShow := true;
TurnObj(0, 90, true);
// Транспорт
AddObj(true, 2, 1590, 1350, 6, 7, 150, 12);
TurnObj(1, 90);
// Звезды
for i:=0 to 1700 do
AddObj(false, 0, Random(14950), Random(2950),
0, Random(5)+1, 1);
// Базы
AddObj(false, 0, 1170, 1350, 0, 6, 1000);
AddObj(false, 0, 10000, 1350, 0, 6, 1000);
end;
Сперва мы вызываем родной конструктор TLKIGameWorld. Его параметры - это текущее время (в миллисекундах, отсчитывается от момента загрузки компьютера), указатель на движок (объект Engine), координаты верхнего левого и нижнего правого углов игрового мира. У нас получилось, что в мире можно перемещаться от координат (0,0) до (15000, 3000). Эти величины, естественно, можно менять.
Это интересно: поскольку все время в движке отсчитывается по «тикам» от момента загрузки компьютера, игра на этом движке сбойнет, если запустить ее через 40 с чем-то дней непрерывной работы Windows. Но только я такого чуда никогда в жизни не видывал, а вы?
Установка стартовых координат тоже на наш выбор. Я поставил, как видно из кода, примерно в середину (1100 из 3000, не забудем, что это верхний угол) по вертикальной оси, и в левый край - по горизонтальной.
Теперь грузим все картинки, что у нас есть. Для этого служит метод AddSprite.
AddSprite
Эта процедура грузит спрайт (объект класса TLKISprite) из картинки (сохраняйте картинки как BMP, по возможности - в 24-битном цвете, хотя движок «понимает» и другие форматы).
У нее только один обязательный параметр - имя файла с картинкой. Есть еще несколько необязательных (перечисляем по порядку): Количество фаз. Если мы хотим, чтобы наш спрайтик дергал лапками, подмигивал глазом или еще что-нибудь этакое делал, нам понадобится сделать этот параметр отличным от 1. Фазы все рисуются в одну картинку, как показано на рисунке. На этот раз нам, вообще-то, была не очень нужна анимация спрайтов, но уж для полноты картины было решено показать, как это работает. Поэтому наш истребитель и зеленый инопланетный корабль снабжены анимированным пламенем из дюз.
Пример двухфазного спрайта. |
Совет: не наделяйте способностью к повороту все спрайты подряд. Этот процесс отнимает не так уж мало ресурсов. Хотя на современной машине вы едва ли заметите проблему, все же лучше с самого начала приучать себя к экономии. Даже если после этого вы спрайт ни разу не повернете, ресурсы будут отняты.
Это важно: необязательные параметры можно пропускать только с конца. Так что нельзя пропустить количество фаз, но определить возможность поворота. Придется указывать число фаз, даже если оно равно 1 (как и будет подставлено по умолчанию).
Загруженный спрайт помещается в массив Sprites игрового мира. Ему соответствует номер - они присваиваются в том порядке, в каком загружаются спрайты. Самый первый получает номер 0, следующий - 1, и так далее. Чтобы не ошибиться, в приведенном фрагменте кода проставлен комментарий - какой номер будет дан какому спрайту. Хотя вообще-то было бы правильнее и умнее сделать именные константы с номерами.
Пока что все, что мы сделали - это загрузили картинки в память. Чтобы поставить их на экран, надо создать для них игровые объекты. Это - экземпляры еще одного класса ЛКИ-Creator: TLKIGameObject.
В таком объекте хранится информация о картинке, месте в игровом мире, типе объекта (корабль, ракета, звезда и т.п.), угле поворота, скорости, хитах и так далее. Нам пока хватит базовых свойств TLKIGameObject, хотя для некоторых игр понадобится создавать потомков этого класса.
Итак, приступим. Для добавления в игру нового объекта существует процедура AddObj.
AddObj
Первый параметр процедуры - логический, он определяет, пассивный это объект или активный. Например, звезда - объект пассивный, ничего с нею сделать нельзя. А вот в корабль можно попасть, он может столкнуться с другим кораблем, и так далее - так что его надо отнести к активным. Кроме того, пассивные объекты по определению неподвижны.
Пассивными у нас будут звезды и обе базы - все остальное активно.
Следующий параметр - код объекта. Его мы задаем произвольно, стараясь, чтобы одни коды не путались с другими. По этому коду мы будем потом определять, с чем там наш корабль столкнулся - с ракетой, баржей или чем-нибудь еще…
Далее - координаты, в которые устанавливается объект, и его скорость (определяется она как расстояние в пикселах, которые проходит объект за 1/10 секунды.
Теперь - номер спрайта. Помните, мы еще выносили его в комментарий в строчках с командой AddSprite?
Затем идут хиты (у неактивных и хиты, и скорость можно смело ставить в 0) и очень интересный параметр - номер спрайта, который возникнет при его уничтожении. Если не хотите никакого - поставьте «-1». Ставить что-то другое есть смысл, если объект у вас обычно исчезает в облаке взрыва, или оставляет после себя череп и кости, или еще что-нибудь этакое.
Есть еще один необязательный параметр - время смерти спрайта, по умолчанию - 0 (естественной смертью спрайт не гибнет). Мы будем устанавливать его не в 0, например, для ракет, которым вовсе незачем летать по космосу бесконечно. Устанавливается он обычно в Tick + число миллисекунд, через которое ему следует погибнуть. Например, Tick + 5000 - спрайт уничтожится через 5 секунд после своего создания.
Это интересно: при таком способе уничтожения спрайт взрыва не появится.
Поставив на карту наш истребитель, мы делаем с ним еще две вещи. Во-первых, устанавливаем его параметр AlwaysShow в true. Этот параметр, если он равен true, заставляет движок смещать игровое окно так, чтобы объект никогда не пропадал из виду; если он подползет к краю окна, окно сдвинется.
Это важно: не вздумайте установить AlwaysShow для двух объектов сразу. Вам не понравится то, что вы увидите.
Еще один метод, с которым мы знакомимся в этой части кода - TurnObj. Он поворачивает объект с заданным номером (номера задаются так же, как для спрайтов, так что у нашего кораблика номер всегда будет 0) на заданный угол (в градусах) по часовой стрелке. Изначально все спрайты стоят так, как изображены на картинке, то есть поворот истребителя на 90 градусов направит его нос вправо - туда, куда мы собираемся гнать баржу.
Теперь точно так же установим транспорт. И повернем его на 90 градусов.
Это важно: учтите, что угол поворота определяет еще и направление движения объекта. Поэтому иногда поворачивать приходится и те объекты, картинка которых неспособна к повороту.
Мы установили на карту корабли, базы и звезды. И запустили игру. |
Наконец, установим звезды. Для этого воспользуемся циклом, чтобы расставить их сразу много, а координаты сделаем случайными. Нам пригодится функция Random, которая дает случайное число от 0 до N-1, где N - параметр функции (для порядка поставим в обработчик события OnCreate формы команду Randomize, которая инициализирует генератор случайных чисел).
Теперь поставим базы - и можно запускать игру, чтобы убедиться, что все рисуется на экране правильно. Выглядит это примерно так, как на картинке.
Только не забудьте перед запуском создать переменную World типа TStarEscortWorld и сделать ее инициализацию в событии OnCreate: World := TStarEscortWorld.Create (GetTickCount, Engine).
Это интересно: порядок описания пассивных объектов не случаен. От него зависит, в каком порядке они будут рисоваться. Поменяйте местами строки кода с описанием звезд и базы - и увидите, что звезды станут просвечивать через базу.
Обрабатываем такт
Нам осталось обработать изменения, которые происходят на карте с каждым мигом. Для этого напишем функцию Process нашего игрового мира.
Что она должна делать?
Во-первых, считывать информацию с клавиатуры и раздавать команды: на Escape - выход, на стрелку вверх - движение, стрелки влево и вправо - поворот, пробел - выстрел.
Далее, определять, не случилось ли победы или проигрыша; передвигать снаряды и отсчитывать попадания; двигать инопланетян и баржу; порождать новых инопланетян; и - абсолютно необходимое действие - вызывать унаследованный обработчик такта. Это нужно сделать при любом исходе остальных событий, кроме немедленного выхода из программы, потому что именно унаследованный обработчик отрисовывает спрайты на экране, а также делает реальное перемещение игровых объектов (мы же здесь укажем только направления движения).
Первым делом напишем в конце кода функции вызов унаследованного обработчика (inherited Process(Tick)) и присвоение результату функции истины (ложь будет только в том случае, если требуется срочный выход). Теперь займемся остальным.
Полный текст кода функции - на врезке («Обработка такта»). Обсудим, что и почему он делает.
Обработка такта
function TStarEscortWorld.Process(Tick : integer) : boolean;
var i,j : integer;
c : integer;
pr : boolean;
ax, ay, dfight, dtrans : integer;
begin
// Чтение клавиатурных команд
Engine.ReadImmediateKBD;
// Прежде всего остального обрабатываем
// выход из программы
if Engine.Keys[DIK_ESCAPE] then
begin
Engine.Deactivate;
Result := false;
Form1.Close;
exit;
end;
// Далее проверяем на состояние победы
// и поражения; если одно из них достигнуто -
// прекращаем обработку (ничего не движется)
if GameLost then
begin
inherited Process(Tick);
Result := true;
exit;
end;
if Objects[1].x >= 9750 then
begin
if NTxt<1 then
AddText(Победа!, clRed,
Engine.Width div 2 - 200, 200);
inherited Process(Tick);
Result := true;
exit;
end;
// Теперь считываем остальные команды: движение,
if Engine.Keys[DIK_UP] then MoveForward(0,40);
// повороты,
if Engine.Keys[DIK_LEFT] then TurnObj(0,-5);
if Engine.Keys[DIK_RIGHT] then TurnObj(0,5);
// стрельбу
if Engine.Keys[DIK_SPACE] and
(Tick-ShootTime>200) then
begin
AddObj(true, 3, Objects[0].x + 75,
Objects[0].y + 55,
60, 8, 1, 14, Tick+7000);
TurnObj(NObj-1, Objects[0].Angle);
MoveForward(NObj-1, 65);
FinishMove(NObj-1);
MoveForward(NObj-1, 3000);
ShootTime := Tick;
end;
// Двигаем баржу
MoveForward(1, 20);
// Инопланетяне двигаются и стреляют
for i:=0 to NObj-1 do
with Objects do
begin
if not (ID in [4,5]) then continue;
DFight := Dist(Objects[0]);
DTrans := Dist(Objects[1]);
if DFight + Random(200) < DTrans * 2 +
Random(150) then
c := 0
else
c := 1;
TurnObjTo(i, c);
MoveForward(i, 40);
if (Tick-ActionTick>350) and
(Dist(Objects[c])<600) then
begin
ActionTick := Tick;
AddObj(true, 6, x + 75, y + 55,
60, 11, 1, 14, Tick+7000);
TurnObj(NObj-1, Angle);
MoveForward(NObj-1, 65);
FinishMove(NObj-1);
MoveForward(NObj-1, 3000);
end;
end;
// Проверяем столкновения и попадания
for i:=0 to NObj-1 do
with Objects do
case ID of
0 : ;
4,5 : begin
CollideDamage(3, 5, 5);
CollideDamage(1, 25, 25);
CollideDamage(2, 25, 25);
end;
6 : begin
CollideDamage(3, 5, 5);
CollideDamage(1, 5, 5);
CollideDamage(2, 5, 5);
end;
else ;
end;
// Убираем дохлятинку
i := 0;
while i if Objects.Hits<=0 then
begin
if Objects.ID = 2 then
begin
GameLost := true;
AddText('Вы потеряли транспорт...',
clRed, Engine.Width div 2 - 200, 200);
RemoveObj(true, i);
end else
if Objects.ID = 1 then
begin
GameLost := true;
AddText('Вы погибли...',clRed,
Engine.Width div 2 - 200, 200);
RemoveObj(true, i);
end else
RemoveObj(true, i)
end
else inc(i);
// Порождаем новых инопланетян
if Tick-AlienTime > 5000 then
begin
ax := Random(250)+450;
if Random<0.5 then ax := -ax;
ay := Random(150)+300;
if Random<0.5 then ay := -ay;
if Random<0.7 then
AddObj(true, 4, Objects[0].x + ax,
Objects[0].y + ay,
20, 9, 10, 13)
else
AddObj(true, 5, Objects[0].x + ax,
Objects[0].y + ay,
15, 10, 25, 13);
AlienTime := Tick;
end;
// Возвращаемся к стандартной обработке
inherited Process(Tick);
Result := true;
end;
Чтение клавиатуры
В программах Windows для этого обычно применяется механизм событий, но нам он не подойдет - слишком медленный.
Процедура Engine.ReadImmediateKBD cчитывает нажатые клавиши в переменную Engine.Keys. Эта переменная - массив логических данных (да-нет), и если элемент, соответствующий клавише, равен истине, то клавиша нажата. Клавиши обозначаются константами, вроде DIK_SPACE или DIK_ESCAPE; так, логическая переменная Engine.Keys[DIK_SPACE] содержит истину, если клавиша пробел нажата.
Первым делом проверяем, не нажат ли Escape. Если нажат, нам нужен аварийный выход. Устанавливаем результат функции Process в ложь (иначе по выходе из нее программа попытается отрисовать экран) и закрываем приложение.
Если же нет, придется продолжить.
Стрелки влево и вправо приводят к уже знакомому нам действию - повороту кораблика на минус 5 и 5 градусов соответственно. Стрелка вперед - к движению корабля; стало быть, нам предстоит ознакомиться с новой процедурой.
Процедура MoveForward(n,s : integer) направляет объект с номером n на s единиц вперед (вперед - это то направление, куда он в этот момент «смотрит»). Это не значит, что он мгновенно перемещается туда; он только начинает движение со своей скоростью. Другими словами, мы могли бы задать и большее расстояние - кораблик не стал бы лететь быстрее, он только позже остановился бы после однократного нажатия клавиши стрелки.
Это интересно: почему бы не перенести кораблик сразу в новое место? Дело в том, что мы не знаем, как часто будет вызываться процедура Process. Это зависит от количества кадров в секунду в нашем приложении. А потому соответствие движения предметов их скорости возложено на встроенный обработчик Process. Там учитывается время, реально прошедшее со времени последнего вызова обработчика.
Пробел генерирует новый объект - ракету; это делается уже разобранной нами процедурой AddObj. Тут-то нам и пригодится параметр «времени смерти» объекта… Мы создаем ракету в точке центра корабля и поворачиваем ее в том же направлении, в котором направлен корабль. Затем сдвигаем ее вперед: процедура MoveForward задает место, а процедура FinishMove мгновенно переносит объект в заданную ранее маршрутную точку. Наконец, задаем новую цель движения - и ракета создана. Больше мы ее движением заниматься не будем: мы направили ее достаточно далеко, прежде, чем она туда долетит, она «умрет» сама по себе. Нам понадобится только учесть ее попадания в цель.
Есть одна небольшая тонкость. Процедура Process будет вызываться очень часто: на обычном среднем компьютере наша игрушка будет выдавать около 100 кадров в секунду. Это значит, что, если не предпринять мер предосторожности, пробел будет вызывать целый залп ракет. Не дело… Значит, надо ввести таймер: переменная ShootTime (мы опишем ее как поле класса TStarEscortWorld и в конструкторе объекта приравняем нулю) будет устанавливаться в текущее время (Tick). Если с момента предыдущего выстрела прошло меньше, скажем, 200 миллисекунд, мы не будем генерировать очередную ракету.
Это интересно: узнать, сколько FPS (кадров в секунду) делает наше приложение, очень просто: достаточно в инспекторе объектов установить для Engine свойство ShowFPS в true. Тогда счетчик будет показываться прямо в заголовке окна. Это полезно сделать - мы сразу же увидим, если какая-то операция начала «съедать» слишком много ресурсов.
Не пожалейте времени, чтобы поудобнее и покрасивее раскрасить окно редактора кода. Так куда удобнее его понимать. |
Порождение инопланетян
Этот код мы опишем поближе к концу процедуры, перед самым вызовом унаследованного обработчика.
Нам нужно отрегулировать появление врага раз в несколько секунд. Сделаем такой же таймер порождения противников, как и тот, что использовался для стрельбы истребителя - назовем его AlienTime. Случайным образом выберем, какой из двух вражеских кораблей создавать. Параметры корабля вы видите в коде; можно поиграться с ним, например, сделать его быстрее или медленнее, или более уязвимым, или лучше защищенным…
Перемещения
Движение ракет и нашего истребителя мы уже описали, остались баржа и инопланетяне. С баржей мы поступим просто: переместим ее немного вперед. А вот с врагами придется повозиться.
Переберем все объекты из списка Objects игрового мира (туда попадают только активные объекты). Обработаем только те, у которых код 4 или 5 - эти коды мы только что присвоили инопланетянам.
(Оператор with, не описанный в нашем учебнике, говорит компилятору: все, что мы делаем дальше, мы делаем с объектом, упомянутым в заголовке оператора. Поэтому в дальнейшем коде ID, например - это код именно этого объекта.)
Программа действий врага будет у нас простой: они выбирают цель - транспорт или истребитель - и двигаются к ней, чтобы расстрелять и протаранить.
Цель выберем случайно, но с учетом расстояния - чтобы враг не летел мимо нашего носа к далекой барже. Сперва вычислим расстояния: метод Dist позволяет определить дистанцию до любого игрового объекта. Добавим к ним случайные числа (эту формулу можно менять по вкусу) и сравним полученное. Теперь повернемся к цели (метод TurnObjTo поворачивает корабль к объекту номер N) и создадим ракету, точно так же, как и при стрельбе истребителя.
Попадания
Идет дуэль с инопланетянином. |
Тут нам предстоит перебрать все активные объекты и проверить, не столкнулись ли какие-то из них. Вообще-то стоило бы обработать все столкновения, но мы пока ограничимся столкновениями с вражескими кораблями и с ракетами. Причем примем, что свои ракеты не вредят, а столкновение своей ракеты с чужой приводит к исчезновению обеих.
Сделаем проверку по коду объекта. Нас интересуют вражеские корабли (коды 4 и 5) и вражеские ракеты (код 6).
Метод CollideDamage(code, damage_self, damage_other) делает сразу все, что нам необходимо: проверяет, не столкнулся ли выбранный объект с любым другим, у которого код равен Code, и если да - то наносит самому объекту повреждения в размере damage_self, а тому, с кем он столкнулся - damage_other. Таким образом, строчка:
CollideDamage(3, 5, 5);
расшифровывается так: если корабль столкнулся с земной ракетой (код 3), и ей, и кораблю наносится повреждение 5.
Это интересно: не всегда столкновение должно приводить именно к такому эффекту. Чтобы просто проверить на столкновение, нужна функция Collide: она выдает истину, если мы столкнулись с объектом, который введен в эту функцию в качестве параметра.
Гибель кораблей
Теперь уничтожим корабли и ракеты, у которых осталось меньше 1 хита. Для этого еще раз проверим все активные объекты. Если это вражеский корабль или ракета, просто уберем его (программа сама подставит эффект взрыва), если же это баржа или истребитель, придется признать игру проигранной - выставим флажок GameLost (а предварительно опишем его как поле класса TStarEscortGameWorld и приравняем false).
Тут мы знакомимся с еще одним новым методом - RemoveObj(active, num). В нем нет ничего сложного: первый параметр определяет, активный или пассивный объект удаляем, второй - его номер в соответствующем списке.
Есть только одна маленькая хитрость. Вы, возможно, заметили, что мы не стали пользоваться циклом for. Это потому, что в процессе количество объектов может сократиться, а индексы в массиве - измениться. Поэтому мы сперва приравниваем i нулю, а увеличиваем, только если не удалили текущий объект.
Ракеты после попадания самоуничтожаются. Именно от них остались облачка взрывов. |
Победа и поражение
Ну и, наконец, обработаем состояния победы и поражения. Это надо делать до считывания клавиш стрелок и пробела, и, конечно, до перемещений - нам ведь надо, чтобы при окончании игры все останавливалось - но все же после обработки клавиши Escape, чтобы сохранить возможность штатным образом выйти из игры.
Для обработки поражения достаточно проверить переменную GameLost, для победы - координаты баржи. В обоих случаях нам надо единожды вывести соответствующий текст (в случае поражения мы это уже сделали), вызвать унаследованный обработчик и прервать процедуру.
AddText - очень простая процедура, она вешает текстовое сообщение поверх окна. Ее параметры - строка текста, цвет и координаты сообщения. Чтобы убрать ее, нужна процедура RemoveText.
Итак?
Вот и все. Какая-то пара страничек кода - и у нас в руках полноценная игра.
Конечно, это далеко не предел совершенства. Не хотите ли попробовать сами ее улучшить? Вот кое-какие идеи: Есть куда совершенствовать графику - впрочем, это видно всякому. Например, неплохо бы убрать черные разводы вокруг кораблей - они видны, когда один объект накладывается на другой. Это можно сделать при помощи Photoshop, а можно - программно, но это потребует знаний о движке, которых у нас еще нет. Добавить проверку на столкновение с баржей и с базой (подсказка: базу придется перевести в статус активных объектов). Дать нашему истребителю несколько жизней и сделать в уголке индикатор оставшихся жизней. Ввести призы - летающие штучки, дающие временную неуязвимость и так далее. Дать несколько видов оружия - например, редко стреляющий, но убийственно мощный луч, проходящий сразу сквозь все ракеты.
Все это вам вполне по силам сделать самим. Но в следующем номере мы еще поговорим о том, как сделать аркаду увлекательнее. А затем займемся более популярными игровыми жанрами - и докажем, что сделать их самостоятельно вполне реально и очень интересно.
До встречи!
В будущих номерахВ следующих номерах мы поговорим о: продвинутых механизмах двумерных движков; трехмерных движках; основах AI; отладке программы; создании замысла и сценария игры, написании дизайн-документа; игровом балансе; продумывании игровых персонажей и их реплик; работе с Photoshop и трехмерными пакетами; анимации; музыке и озвучке; и многом другом. Все это вполне реально научиться делать своими руками. Вы скоро в этом убедитесь. |
|
Пока что наш истребитель может пролетать поверх транспорта. Попробуйте это исправить сами? |