Игровой конструктор, часть 2. Интерфейс и управление
В прошлой статье цикла мы приступили к созданию трехмерного движка с помощью Delphi и GLScene. Сегодня мы продолжим закладывать кирпичи в высокое здание компьютерной игры и создадим собственное игровое управление и интерфейс (или HUD). Товарищи, те, кто присоединился к нам только с этого номера, все внимание сюда! Чтобы понять все, что написано в этой статье, вам просто необходимо прочитать первый материал цикла. Так же и для всех последующих частей: чтобы их понять, нужна только первая статья, остальные не обязательны. Поэтому первая часть статьи отныне и до момента завершения цикла будет базироваться на наших CD/DVD в разделе “ИнфоБлок”. Тем же, кто с нами с самой первой статьи, совет: перед прочтением каждой следующей статьи цикла перечитывайте первую часть. Джойстик носатый и мышь хвостатая! В любой игре геймер управляет своим заэкранным альтер эго с помощью разнообразных устройств: клавиатуры, мыши, джойстика, геймпада, виртуальными перчатками, а иногда даже взглядом! Чем больше игра поддерживает устройств управления, тем лучше. Стандарт де-факто для стратегических игр — мышь и клавиатура. В экшенах и аркадах к необходимому минимуму добавляются джойстик и геймпад. В вашей игре просто обязан быть этот минимум. Вы спросите, зачем, к примеру, той же “Контре” джойстик, если настоящие джедаи выбирают мышь? У разных людей разные вкусы. В США, например, самым популярным игровым устройством является вовсе не мышь, а геймпад. Надеюсь, я убедил вас, что поддержка хотя бы этих четырех устройств необходима. А теперь поговорим о том, как эту самую поддержку внедрить в движок. Для начала решим один очень важный вопрос: куда мы поместим код, обрабатывающий устройства ввода? Вроде бы его стоит поместить туда, где и требуется проверка реакции игрока: в код, который отвечает за оружие; в код, который отвечает за движение; в код, который отвечает за… И получается, что код управления будет размазан по всему движку. А как же наш принцип кирпичиков? Не дело это. Код управления должен быть цельным. Лучше место для кода — обработчик TGLCadencer.
Как ни странно, с точки зрения GLScene все эти устройства — джойстики.
Многие начинающие программисты недоумевают, зачем вообще нужен этот компонент?! Казалось бы, код надо привязывать к событиям, а не к какому-то там таймеру. Но Модулятор (именно так переводится Cadencer) не просто таймер. Интервал, с которым срабатывает его событие, постоянно меняется и зависит от скорости рендера. В обработчик передается важнейшая переменная deltaTime — время, которое прошло с момента предыдущего рендера. Именно к этому времени надо привязывать все движения в игре. Поясню на примере. Допустим, вы делаете гоночный симулятор, и в каком-то неопределенном пока обработчике клавиатуры написали что-то вроде: Если нажата клавиша “вперед”, положение машины:=положение машины + 10 Так как обработчик будет срабатывать только в перерывах между рендером кадров, а приращение координаты машины каждый раз одно и то же, скорость машины будет зависеть от FPS, то есть в конечном счете — от производительности компьютера. На быстрых компьютерах машина будет ездить со скоростью 300 км/ч, на медленных выдавать только 100, а на графических станциях вообще полетит со скоростью ракеты! В довершение картины представьте, что вы с вашим другом играете по сети, и у него машина едет быстрее только потому, что мощнее компьютер. Согласитесь, несправедливо? Вот чтобы такого не случалось, придумали Cadencer. В его обработчике вы можете написать так: Если нажата клавиша “вперед”, положение машины:=положение машины + 0,01* deltaTime Коэффициент перед deltaTime подбирается экспериментально. Вот теперь машина будет ездить с одинаковой скоростью на любых компьютерах. Это же относится и к любым другим движущимся или меняющимся во времени игровым объектам. Поэтому TGLCadencer — один из центральных элементов нашего движка, и код работы с устройствами мы поместим именно в его обработчик. Казалось бы, с клавиатурой и мышью никаких проблем нет — назначаем соответствующие события OnKeyDown и OnMouseMove главной формы и там описываем все необходимые действия… И нарываемся на подводный камень. Так делать категорически нельзя, особенно в том случае, если игра будет
В action-играх HUD, как правило, незамысловатый: индикатор патронов, здоровья и фрагов — что еще нужно для геймерского счастья? В некоторых случаях цифры индикаторов лучше делать не с помощью THUDText, а с помощью отдельных спрайтов.
работать в полноэкранном режиме. Для полноэкранного режима GLScene создает особый тип окна, который несколько неадекватно реагирует на стандартные обработчики событий клавиатуры и мыши. Чаще всего в момент нажатия клавиши или кнопки мыши рендерер “запинается”, геймеру кажется, что игра тормозит. Поэтому код для работы с устройствами нам придется написать самостоятельно, благо дело это не особенно сложное. Разработчики GLScene предусмотрели многие препятствия, которые могут попасться на нашем пути, и загодя постелили соломку. А соломка эта выражается в специальном модуле Keyboard.pas. Пусть вас не смущает название — там не только о клавиатуре. Подключите его в uses модуля, в котором будете обрабатывать сообщения от устройств. Давайте посмотрим, какие возможности дает модуль. Клавиатура. Чтобы определить, нажата ли какая-то клавиша в данный момент времени, используйте функцию IsKeyDown. Например: If IsKeyDown(VK_UP) then Tank.Go(deltaTime) Клавишу для проверки надо задавать виртуальным кодом. Полный список виртуальных кодов клавиш вы найдете в справке Delphi. Самые распространенные коды приведены на отдельном текстовом блоке. Еще одна полезная пара функций из этого модуля: KeyNameToVirtualKeyCode и VirtualKeyCodeToKeyName. Они конвертируют виртуальный код клавиши в символьное имя, принятое в системе, и наоборот. Эти функции пригодятся, когда вы будете делать переназначение клавиш. Игрок должен иметь возможность переназначить управление по своему вкусу. Реализовать это можно разными способами. В конце главы я приведу ключевую функцию из одной из моих игр, которая как раз и реализует переназначение клавиш. Мышь. Здесь нам надо решить две задачи: обработка нажатий кнопок мыши и отслеживание положения указателя мыши (курсора или прицела). Первая задача решается очень просто — с помощью все того же IsKeyDown. Коды левой, средней и правой клавиши мыши соответственно VK_LBUTTON , VK_MBUTTON и VK_RBUTTON. Вторая задача не намного сложнее. Сперва объявите в var нужного обработчика вот такую переменную: p:TPoint; В TPoint.X и TPoint.Y мы будем сохранять горизонтальную и вертикальную координаты мыши. Теперь с помощью команды GetCursorPos из WinAPI можно легко получить искомые координаты: getcursorpos§; На практике часто важно знать не координаты мыши, а изменение координат с предыдущей проверки. Для этого объявите глобальные переменные: mx, my,dx,dy : Integer ; А теперь в том же коде сразу после вызова GetCursorPos напишите: dx:=p.X-mx; dy:=p.Y-my; Вот наглядный пример того, как эти изменения координат можно использовать: Player.Weapon.MoveUp(dx); Player.Weapon.MoveLeft(dy); Естественно, если dx отрицательный, MoveUp вопреки названию не поднимет оружие, а опустит. То же относится и к MoveLeft. Объекты Player и Weapon здесь просто придуманы, для движка они ничего не значат. Более подробно о том, как правильно организовать иерархию классов движка, мы поговорим в следующей статье цикла. Джойстик. Давайте условимся, что за джойстики у нас с вами сойдут любые устройства, имеющие некоторое количество кнопок и некоторое количество рычажков с некоторым количеством степеней свободы. С точки зрения программирования геймпады, рули, педали и прочие подобные девайсы ничем от джойстиков не отличаются.
Иногда разработчики интегрируют HUD и игровые объекты. Это способствует погружению в игровой мир.
Для работы с джойстиками в GLScene есть специальный компонент TJoystick. Перед употреблением настройте его свойство Threshold (порог чувствительности), а свойству NoCaptureError присвойте true , иначе, в случае если джойстик в системе не установлен, будет выдаваться ошибка. Перед началом работы присвойте свойству Capture значение true и можете снимать данные. За отклонение главной палки джойстика отвечают свойства TJoystick.XPosition и TJoystick.YPosition. Если эти свойства равны нулю, отклонения джойстика нет. Положительное значение свойства соответствует отклонению вверх или вправо, отрицательное — вниз или влево, а само значение пропорционально углу отклонения. Все нажатые в данный момент клавиши джойстика автоматически заносятся в набор JoyButtons. С этим типом данных вы еще не встречались. Определить, содержится ли элемент в наборе, можно таким образом: If (элемент in имя_набора) then {элемент в наборе} Элементы, соответствующие кнопкам джойстика, называются jbButton1 , jbButton2 , jbButton3 и т.д. И, наконец, вот так вы сможете определить, нажата ли та или иная кнопка: if (jbButton1 in Joystick.JoyButtons) then {нажата первая кнопка} Напоследок, как и обещал, приведу свой код для переназначения клавиш: _function IsItKeyDown(sKey:string):boolean; begin if KeyNameToVirtualKeyCode(sKey) <>-1 then begin if IsKeyDown(KeyNameToVirtualKeyCode(sKey)) then result:=true else result:=false; exit; end else begin result:=false; if (jbButton1 in form1.joystick.JoyButtons) and (sKey=‘jbButton1’) then result:=true; if (jbButton2 in form1.joystick.JoyButtons) and (sKey=‘jbButton2’) then result:=true; if (jbButton3 in form1.joystick.JoyButtons) and (sKey=‘jbButton3’) then result:=true; if (jbButton4 in form1.joystick.JoyButtons) and (sKey=‘jbButton4’) then result:=true; end; end; _Везде, где нужно проверить, нажата ли определенная клавиша, пользуйтесь таким вызовом: If IsItKeyDown (c_forward) then {нажата клавиша вперед} В с_forward перед началом работы игры должно хранится название назначенной для действия “Вперед” клавиши. Аналогично поступайте со всеми остальными действиями, которые подлежат переназначению.
В стратегиях и тактических играх HUD обычно куда серьезнее, чем в играх жанра action. В таких случаях для каждого смыслового экрана игрового интерфейса удобнее выделить отдельный TGLDummyObject, чтобы их можно было легко показывать и убирать с экрана.
От общего к частному Что общего у шлема боевого летчика и компьютерной игры? У обоих есть HUD! Это словосочетание расшифровывается как Head Up Display и на русский язык однозначно не переводится. На шлемах боевых летчиков многих современных самолетов есть прозрачные дисплеи, на которых показываются параметры вооружения, целеуказатель и многие другие данные, необходимые в воздушном бою. С легкой руки разработчиков авиасимуляторов этот термин стал относиться не только к спецдисплею, но и к экранному интерфейсу вообще. Теперь под страшным словом HUD скрывается все, что находится на экране во время игры, но непосредственно не относится к игровому миру: очки, жизни, фраги, пиктограммки оружия, радар, списки юнитов и зданий для постройки и многое другое. Как правило, HUD двумерный, то есть рисуется спрайтами поверх всей остальной сцены. По-настоящему трехмерные игровые интерфейсы встречаются крайне редко. Мы поговорим о создании классических двумерных HUD. Для создания HUD в GLScene есть два класса — TGLHUDSprite и TGLHUDText для спрайтов и текста соответственно. Эти объекты обладают рядом особенностей. Во-первых, они существуют только в двумерном пространстве, а их координата Z определяет только порядок отрисовки, то есть какие объекты будут поверх в случае пересечения. Во-вторых, положение HUD-объектов на экране не зависит от того, куда направлена камера — их координаты абсолютны. Точка с координатой (0,0) находится в верхнем левом углу экрана, ось X идет вправо, а ось Y — вниз. В-третьих, у них особые отношения к свойствам прозрачности. Вряд ли ваш HUD будет состоять из одного-двух спрайтов. В среднестатистической игре их как минимум десяток. Так как перечень объектов, входящих в HUD (давайте условимся для краткости называть их просто спрайтами), неизменен в течение всей игры, их можно создать на этапе разработки игры, добавив нужные объекты с помощью GLScene Editor (напоминаю, для вызова этого редактора надо два раза кликнуть по иконке компонента TGLScene ). Только не стоит добавлять объекты, относящиеся к HUD, в корень. Создайте объект-пустышку TGLDummyСube , сделайте его невидимым и уже в список его подобъектов добавляйте все, что относится к HUD. Теперь просто изменив свойство Visible у TGLDummyСube, вы можете одним махом убрать весь интерфейс с экрана (он ведь не нужен во время показа заставочных роликов или когда геймер находится в игровом меню) и так же легко его вновь показать.
Перед проектированием HUD обязательно нарисуйте скетч-макет. На нем очень удобно планировать взаимодействие и сопряжение различных спрайтов, из которых состоит игровой интерфейс.
Нужное количество спрайтов мы создали, теперь подумаем о том, как их инициализировать. Ни в коем случае не присваивайте жесткие координаты спрайтам через Object Inspector , если только ваша игра не будет поддерживать одно-единственное разрешение (а это дурной тон). Причем тут поддержка разрешений? А вот причем. Допустим, вы создаете игру в разрешении 1024х768 и жестко задали все характеристики спрайтов. Игрок, переключившись в разрешение 800х600, увидит, что все спрайты сбились в кучу и стали неимоверно большими. Если он переключится в 1600х1200, то все спрайты будут мелкими, да и к тому же спрайты, которые должны быть с правой и нижней сторон экрана, неожиданно окажутся в центре. Проще говоря, весь интерфейс развалится на кусочки. Поэтому координаты и размеры спрайтов должны быть привязаны к текущему разрешению. В одной из следующих статей цикла мы поговорим о том, как работать с разрешениями, так как это отдельная интересная тема. Пока представьте себе, что у нас есть две глобальные переменные res_x и res_y , в которых хранятся ширина и высота экрана в пикселах. В тестовых целях вы можете присвоить им нужные значения вручную. Есть два подхода к обработке разрешений. В первом случае вы создаете столько копий каждого спрайта, сколько разрешений будет поддерживать игра. Каждая копия будет “заточена” под определенное разрешение. Этот способ дает очень качественный интерфейс, но… придется потратить много времени на отрисовку. Во втором случае вы программно растягиваете спрайты до нужных размеров. Качество слегка страдает, но и хранить ничего дополнительно не нужно. Обычно эти два способа сочетаются: игроделы создают несколько копий спрайтов для базовых разрешений, а для всех остальных разрешений ближайшая по размерам копия растягивается до нужного размера.
Структура будущего HUD в окошке GLScene Editor. Все HUD-спрайты являются подобъектами “пустышки” Radar.
Для примера приведу код для второго способа. Выберите какое-либо базовое разрешение, для которого будете создавать спрайты. Допустим, базовое разрешение — 1024х768. Тогда в коде, который вызывается при запуске игры, напишите для каждого HUD-спрайта: HUDSprite.Material.Texture.Disabled:=false; HUDSprite.Material.Texture.Image.LoadFromFile(‘HUDSprite.bmp’); HUDSprite.Material.BlendingMode:=bmTransparency; HUDSprite.Material.Texture.ImageAlpha:=tiaSuperBlackColorTransparent; HUDSprite.Material.Texture.TextureMode:=tmModulate; HUDSprite.Position.X:=120res_x/1024; HUDSprite.Position.Y:=100res_y/768; HUDSprite.Width:= HUDSprite.Material.Texture.Image.Widthres_x/1024; HUDSprite.Height:= HUDSprite.Material.Texture.Image.Heightres_y/768; Цифры 120 и 100 соответствуют координатам данного спрайта в разрешении 1024х768. Простая школьная пропорция перемещает спрайт в нужное место и масштабирует его при смене разрешения. Проблему с разными разрешениями мы решили. А теперь поговорим о не менее важной вещи — разных уровнях прозрачности. В коде выше вы наверняка заметили несколько строк, посвященных режимам прозрачности. Ни один более или менее сложный HUD не может состоять только из квадратных спрайтов. Приборные щитки, фигурные меню, радар, изгибы панелей — как реализовать все эти нестандартные формы? Здесь нам поможет первый уровень прозрачности. Какую-то часть квадратного спрайта мы должны сделать абсолютно прозрачной, тогда спрайт будет иметь фигурную форму. Проще всего это сделать так. Нарисуйте нужный спрайт, и все места, которые должны быть прозрачными, закрасьте абсолютно черным цветом (цвет RGB(0,0,0)). Внимание! Если в видимой части спрайта также используется черный цвет, в этом месте будет дырка. Поэтому вместо черного цвета используйте чуть-чуть серый (к примеру, RGB(1,0,0)). “На глаз” от черного такой цвет отличить невозможно.
Свойства TGLHUDSprite, которые относятся к текстурам и прозрачности. Обязательно изучите их.
Теперь загружайте эту картинку в объект спрайта, а остальное сделают вот эти две строчки: HUDSprite.Material.BlendingMode:=bmTransparency; HUDSprite.Material.Texture.ImageAlpha:= tiaSuperBlackColorTransparent; Первая устанавливает режим прозрачности, а вторая указывает, какие именно области изображения будут прозрачными. В нашем случае — все области, закрашенные черным цветом. Есть и другие способы задания прозрачных зон. К примеру константа tiaTopLeftPointColorTransparent устанавливает режим, при котором цветом прозрачности будет цвет верхней левой точки изображения. Часто бывает необходимо сделать полупрозрачным весь спрайт. За общую прозрачность отвечает свойство AlphaChannel. Значение 1 соответствует абсолютной непрозрачности, значение 0 — абсолютной прозрачности. Чтобы спрайт стал полупрозрачным, задайте какое-нибудь дробное число от 0 до 1. Есть еще один способ задания прозрачности спрайта, с помощью которого можно реализовать любой тип прозрачности. К примеру, часть спрайта будет полностью прозрачной, а часть — полупрозрачной, причем эта полупрозрачность будет градиентной. С помощью этого способа можно делать поразительные эффекты. Цена — повышенная ресурсоемкость по сравнению с уже рассмотренными способами. Речь идет об альфа-маске изображения. Самые популярные форматы, в которых есть альфа-маска — bmp32 и tga. Файлы обоих форматов вы можете создать, например, в Photoshop. Мы рекомендуем использовать формат tga , потому что в нем предусмотрено сжатие. Использовать файлы с альфа-маской в GLScene совсем несложно: HUDSprite.Material.BlendingMode:=bmTransparency; HUDSprite.Material.Texture.Image.LoadFromFile(‘HUDSprite.tga’); Все остальные свойства, связанные с прозрачностью, не трогайте — в них должны оставаться значения по умолчанию. С помощью последнего приема вы сможете сделать любые интерфейсы, какие только придумаются. Однако если какой-то конкретный спрайт можно реализовать с помощью первого способа — делайте именно так, ведь производительность дороже всего. Напоследок приведу одно важнейшее правило работы с любыми текстурами, в том числе и для спрайтов. О нем, увы, забывают порой даже профессиональные разработчики, что кончается серьезными графическими глюками. У любой текстуры должны быть стороны, кратные степеням двойки, то есть из ряда 4, 8, 16, 32, 64, 128, 256, 512, 1024 и далее по списку. Это требование связано с особенностями организации видеопамяти. Самое малое, что вы можете получить, нарушив его, — темные полосы непонятного происхождения по краям полупрозрачных спрайтов. И если на каком-то этапе разработки у вас на каких-то текстурах вдруг появились непонятные артефакты — в первую очередь проверьте, какие у них размеры. __ Коды клавиш Клавиша Код Клавиша Код
Стрелка вперед VK_UP Enter VK_RETURN
Стрелка назад VK_DOWN Esc VK_ESCAPE
Стрелка вправо VK_RIGHT Alt VK_MENU
Стрелка влево VK_LEFT Ctrl VK_CONTROL
Пробел VK_SPACE Shift VK_SHIFT
*** * *** Интерфейс игры готов, более того — движок умеет общаться не только с клавиатурой, но и с мышкой, а также со всевозможными рулями, джойстиками, геймпадами… В следующем номере вы узнаете, как правильно организовать иерархию игровых классов и объектов, чтобы не было мучительно больно за долгие месяцы, проведенные за программированием того, что при другом подходе можно было сделать одной строчкой. На компакт мы снова выкладываем последнюю протестированную версию GLScene.