КОД: СКУЧНАЯ рутини – РОЗРОБКА ІГОР ДЛЯ ОС ANDROID

&nbsp

Отже, ви вже знаєте, що Android API підходить для розробки ігор Але вам все ще невідомо, як саме це робити Вже є ідеї по дизайну гри, проте процес перетворення його в виконуваний файл поки виглядає якимось чаклунством У наступних підрозділах я збираюся зробити для вас огляд складових компютерної гри Я використовую трохи псевдокоду для інтерфейсів, які ми реалізуємо пізніше за допомогою Android Інтерфейси – дуже корисна штука з двох причин: вони дозволяють нам сконцентруватися на семантиці, не відволікаючись на деталі реалізації, а також дають можливість при необхідності міняти спосіб цієї реалізації (наприклад, замість використання 2В-візуалізації ми для демонстрації містера Нома можемо задіяти OpenGL ES)

Будь-яка гра вимагає якоїсь базової середовища, що дозволяє абстрагуватися від низькорівневого взаємодії з операційною системою Зазвичай ця середу розбивається на кілька модулів

Управління вікнами Відповідає за створення вікна і бере на себе такі речі, як його закриття чи призупинення / відновлення роботи програми на Android

Введення Цей модуль повязаний з попереднім і відповідає за відстеження користувача введення (торкань, клавіатурного введення і рухів акселерометра)

Файловий ввід / вивід Дозволяє додатком отримувати ресурси, розташованих на носії

Графіка Мабуть, найбільш складна частина, якщо не вважати власне гру Цей модуль відповідальний за завантаження графіки і промальовування її на екрані

Звук Завантаження та відтворення всього, що здатне досягти наших вух

Ігровий фреймворк Поєднує в собі все вищеперелічене і пропонує зручну основу для написання ігор

Кожен з цих модулів складається з одного або декількох інтерфейсів, кожен з яких повинен мати як мінімум одну реалізацію, використовує семантику основообразующие платформи (у нашому випадку Android)

ПРИМІТКА

Ви не помилилися – я навмисно не включив підтримку мережі в даний список Ми не будемо розглядати в е питання створення багатокористувацьких ігор Це вельми складне питання, відповідь на який сильно залежить від типу гри Якщо вам цікава дана тема, в Мережі ви зможете знайти багато відповідної інформації (wwwgamedevnet – прекрасне місце для старту)

У подальшому ми будемо намагатися якомога менше залежати від конкретної версії платформи – ідеї будуть однакові для всіх її реалізацій

Управління додатком і вікнами

Гра, як і будь-яка компютерна програма, володіє користувача інтерфейсом, який міститься у вікні (якщо парадигма користувача інтерфейсу заснована на вікнах, що вірно для всіх відомих операційних систем) Вікно виступає в ролі контейнера, і ми будемо розглядати його як полотно, на якому буде намальовані всі складові гри

Більшість ОС дозволяють користувачам взаємодіяти з вікнами особливим чином (не рахуючи торкання користувача області або натискання кнопки) На компютерних системах ви зазвичай можете перетягувати їх, змінювати їх розмір і згортати в якому-небудь варіанті Панелі завдань У випадку з Android зміна розміру замінено на зміну орієнтації, а згортання реалізовано у вигляді переходу додатки у фоновий режим при натисканні кнопки Ноті (Додому) або вхідному дзвінку

Модуль управління додатком і вікнами вирішує, крім того, завдання налаштування вікна та забезпечення заповнення його компонентом інтерфейсу, що сприймає користувача введення у вигляді торкань і натискань клавіш Цей компонент може бути визуализирован або центральним процесором, або за допомогою апаратного прискорення (як у випадку з OpenGL ES)

Модуль управління додатком і вікнами не володіє якимось конкретним набором інтерфейсів Ми обєднаємо його з ігровим фреймворком пізніше Зараз нам необхідно запамятати стану та події вікна, якими необхідно управляти:

Create (Створити) – виникає один раз при відкритті вікна (а отже, і додатки)

Pause (Пауза) – зявляється при призупинення роботи програми-яким способом

Resume (Відновити) – виникає при поновленні роботи програми та повернення вікна на передній план

ПРИМІТКА

Деякі шанувальники Android можуть у цей момент округлити очі Чому використовується тільки одне вікно (активність на мові Android) Чому не застосовувати для гри більше одного віджета, щоб створювати складні користувальницькі інтерфейси Головним чином тому, що нам необхідний повний контроль над зовнішнім виглядом і відчуттям від гри Крім того, в цьому випадку я можу зосередитися на програмуванні гри для Android замість програмування інтерфейсів для Android Про даній темі ви можете почитати в інших джерелах

Введення

Звичайно, користувачеві необхідно буде взаємодіяти з грою-яким чином Саме цим займається модуль вводу У більшості операційних систем події введення (на кшталт торкання екрану або натискання кнопки) відсилаються до поточного вікна Далі це вікно перенаправляє дані події елементу інтерфейсу, на якому в даний момент знаходиться фокус Процес перенаправлення зазвичай дуже прозорий для нас Все, що нам необхідно, – отримувати події від сфокусованого компонента інтерфейсу API користувальницького інтерфейсу операційної системи пропонує механізм впровадження в систему перенаправлення подій, щоб ми могли їх легко реєструвати і зберігати Але що нам робити із записаною інформацією Існує два варіанти дій

Опитування При даному способі ми лише перевіряємо поточний стан пристрою введення Будь-які зміни стану між опитуваннями будуть для нас втрачені Даний вид обробки введення підходить для таких речей, як перевірка факту натискання користувачем певної кнопки Він не годиться для відстеження введення тексту, адже порядок введення символів буде загублений

Обробники подій Цей спосіб дає нам повну хронологію подій, що відбулися з моменту останньої перевірки Він добре підходить для обробки текстового введення або будь-який інший завдання, що залежить від порядку подій Метод також корисний для визначення першого торкання пальцем екрану чи підняття його

Які пристрої введення нам потрібно контролювати У випадку з Android існує три основні методи введення: сенсорний екран, клавіатура / трекбол і акселерометр Для перших двох способів можуть використовуватися обидва методи обробки подій Для акселерометра зазвичай застосовується тільки опитування Сенсорний екран здатний генерувати три події: торкання екрану – відбувається, коли палець стосується дисплея перетягування – Виконується, коли палець рухається по дисплею Виникненню цієї події завжди передує подія торкання відрив – відбувається, коли палець піднімається від дисплея

Кожна подія дотик несе додаткову інформацію: положення щодо компонентів інтерфейсу і індекс покажчика (використовується в мультитач-екранах для відстеження торкання кількох пальців)

Клавіатура генерує два типи подій: клавіша натиснута – відбувається при натисканні кнопки клавіша відпущена – виконується при піднятті пальця від клавіші

Клавіатурні події також містять додаткові дані Події типу клавіша натиснута зберігають код натиснутою кнопки, а події типу клавіша відпущена включають в себе код кнопки і Юнікод-символ Між ними існує різниця: у другому випадку враховується також стан інших клавіш (наприклад, клавіші Shift) Завдяки цьому у нас зявляється можливість визначати, ввів користувач велику чи маленьку літеру З подією натискання клавіші нам пощастило менше – у нас немає точних даних про те, який саме символ створюється

Нарешті, є ще акселерометр Його стан ми завжди отримуємо методом опитування Акселерометр повідомляє про зміну положення пристрою по одному з напрямків-осей, званих зазвичай х, у і z (рис 319)

Рис 319 Осі акселерометра на телефоні Android вісь z вказує вгору над телефоном

Прискорення по кожній з осей вимірюється в метрах в секунду за секунду (м/с2) З уроків фізики ви можете памятати, що при вільному падінні будь-який обєкт рухається з прискоренням 9,8 м/с2 На інших планетах значення прискорення вільного падіння відрізняється Для простоти будемо вважати, що наш додаток буде працювати тільки на планеті Земля Коли точка на осі віддаляється від центру Землі, значення прискорення буде зростати При зворотному переміщенні ми отримуємо негативну динаміку прискорення Наприклад, якщо ви тримаєте телефон вертикально в портретному режимі, значення на осі у дорівнюватиме 9,8 м/с2 На рис 319 таке значення буде по осі г, а осі хіу будуть повідомляти прискорення, рівне нулю

Тепер визначимо інтерфейси, що дозволяють опитувати події від сенсорного екрану, акселерометра і клавіатури, а також дають доступ до обробникам подій від дисплея і клавіатури (лістинг 31)

Лістинг 31 Інтерфейс Input і класи KeyEvent і TouchEvent

Наше визначення починається з двох класів – KeyEvent і TouchEvent Клас KeyEvent визначає константи, що кодують тип KeyEvent клас TouchEvent робить те ж саме Примірник KeyEvent зберігає його тип, код клавіші і Юнікод-код (якщо тип події KEY UP)

Код TouchEvent виконує аналогічну функцію – зберігає тип TouchEvent, позицію пальця щодо вихідного елемента інтерфейсу, і ID покажчика, виданий даному пальцю драйвером сенсорного екрана Цей ID буде зберігатися до тих пір, поки палець стосуватиметься дисплея При цьому перший коснувшийся екрану палець отримує ID, рівний 0, наступний – 1 і т д Якщо екрану стосуються два пальці і палець 0 піднято, ID другого залишається рівним 1 до тих пір, поки він стосується екрану Наступний палець отримує перший вільний номер, який в даному випадку може бути дорівнює 0

Наступні рядки коду – методи опитування інтерфейсу Input, які досить прозорі і не вимагають докладних пояснень InputisKeyPressedO отримує keyCode і повертає результат – натиснута відповідна кнопка в даний момент чи ні InputisTouchDownO, InputgetTouchXO і InputgetTouchYO ​​повертають стан переданого їм покажчика, його х-і г-координати Зверніть увагу – значення цих координат буде не визначено, якщо відповідний покажчик в даний момент не стосується екрану

Input getAccel Х, Input getAccel Y і InputgetAccelZO повертають відповідні значення прискорення для кожної осі

Останні два методи використовуються для реалізації обробників подій Вони повертають екземпляри KeyEvent і TouchEvent, що зберігають інформацію з останнього рази, коли ми викликали ці методи Події розташовані в порядку їх появи – найсвіжіше з них розміщено в кінці списку

З цим простим інтерфейсом і допоміжними класами у нас є все, щоб задовольнити наші потреби у введенні Тепер займемося файловими операціями

ПРИМІТКА

Хоча змінюються класи з відкритими методами викликають огиду, в даному випадку ми можемо їх залишити з двох причин: по-перше, Dalvik все ще занадто неквапливий при виклику методів (в даному випадку властивостей) по-друге, змінність класів подій не впливає на внутрішню роботу нашої реалізації вводу Просто запамятайте, що це – поганий стиль програмування, і більше не будемо про це згадувати

Файловий ввід-висновок

Читання і запис файлів – вельми необхідні речі для наших спроб в програмуванні ігор Враховуючи, що ми знаходимося в країні Java, здебільшого нам доведеться мати справу з екземплярами класів InputStream і OutputStream – стандартними Java-механізмами читання і запису даних у файли У нашому випадку нас більше цікавить читання файлів з пакета нашої гри (рівнів, зображень, звукозаписів) Із записом файлів ми будемо стикатися набагато рідше – вона знадобиться нам, тільки якщо ми захочемо зберегти результати, налаштування або гру, щоб потім продовжити з того місця, де урвалися Загалом, нам необхідний самий простий механізм доступу до файлової системи – такий, як в лістингу 32

Лістинг 32 Інтерфейс FilelO

Тут все просто і зрозуміло Ми просто визначаємо імя файлу і повертаємо для нього потік Як звичайно, в Java в разі непередбачених подій ми викликаємо виняток IOExcepti on Де саме ми будемо читати і записувати файли, залежить, звичайно, від реалізації інтерфейсу Ресурси можуть бути прочитані безпосередньо з АРК-файлу програми або з SD-карти (також званої зовнішнім сховищем)

Повертані екземпляри InputStreams і OutputStreams – старі добрі потоки Java Природно, після закінчення використання їх необхідно закривати

Звук

Хоча програмування звуку – досить складна тема, ми можемо використовувати для нього вельми просту абстракцію Ми не будемо реалізовувати складну обробку звуку достатньо буде забезпечити відтворення звукових ефектів і музики, завантажених з файлів (приблизно так само, як ми будемо завантажувати растрові зображення в графічному модулі)

Однак перед тим, як зануритися в інтерфейси звукового модуля, зробимо паузу і отримаємо деяке уявлення про те, що таке звук і як його представити в цифровому вигляді

Фізика звуку

Звук зазвичай описують як потік хвиль, що переміщаються в просторі подібно повітрю або воді Хвиля – це не фізично відчутний обєкт, а скоріше рух молекул в просторі Уявіть собі ставок, в який кинули камінь Коли камінь досягає поверхні води, він змушує рухатися безліч молекул, які, в свою чергу, передають свій рух сусідам В результаті ви побачите круги, що розходяться від того місця, куди впав камінь З подібних високонаукових експериментів, проведених вами в дитинстві, ви знаєте, що хвилі води можуть взаємодіяти один з одним – складатися або гасити один одного Все це вірно і для звукових хвиль Вони комбінуються для отримання різних тонів і мелодій, які ви чуєте як музику Гучність звуку визначається кількістю енергії, переданої молекулами своїм сусідам, поки вони не досягнуть вашого вуха

Запис і відтворення

Принцип запису і відтворення звуку в теорії досить простий: для запису ми відстежуємо кількість енергії, яка додається до певної точки молекулами, що формують звукові хвилі Відтворення – по суті, примус молекул повітря, оточуючих динамік, рухатися так само, як вони робили це при записі

Звичайно, на практиці все складніше Звук зазвичай записується одним з двох методів: аналоговим або цифровим В обох випадках звукові хвилі записуються за допомогою мікрофона, головним елементом якого є мембрана, перетворююча удари молекул в який-небудь вид сигналу Спосіб обробки та зберігання сигналу і становить різницю між аналогової і цифрової записом Ми будемо мати справу з цифровими записами, тому розглянемо другий варіант

При цифрового запису звуку стан мембрани мікрофону вимірюється за дискретний тимчасової такт і зберігається Залежно від стану оточуючих молекул мембрана може вигинатися всередину або назовні, повертаючись потім в нейтральний стан Процес вимірювання та збереження стану мікрофона називається семплюванням, оскільки ми зберігаємо семпли стану мембрани за дискретні такти часу Кількість отриманих семплів за одиницю часу називається частотою дискретизації Зазвичай часовий інтервал задається в секундах, а частота вимірюється в герцах (Гц) Чим більше семплів записується в секунду, тим вище якість запису Компакт-диски відтворюють звук з частотою дискретизації 44100 Гц (44,1 КГц) Більш низькі частоти дискретизації можна виявити при передачі голосу по телефону (найчастіше 8 КГц)

Частота дискретизації – аж ніяк не єдиний параметр, що визначає якість звуку Спосіб зберігання положень мембрани також має значення і теж є субєктом оцифровки Нагадаю, що такий стан мембрани – це розмір її відхилення від нейтрального положення Оскільки напрямок відхилення має значення, його значення зберігається зі знаком Отже, положення мембрани під час певного часового інтервалу – позитивне чи негативне число Ми можемо зберігати це число різними способами: як ціле 8 -, 16 – або 32-бітове число зі знаком або як 32-бітове (і навіть 64-бітове) число з плаваючою крапкою Кожен з цих типів даних має свою точність Наприклад, 8-бітове ціле число може зберігати значення від -128 до 12732-бітний тип i nteger пропонує набагато більший діапазон При зберіганні у вигляді f 1 oat положення мембрани зазвичай нормалізується, щоб потрапляти в діапазон від -1 до +1 При цьому максимальне і мінімальне значення відповідають найбільшому відхиленню мембрани від нейтрального положення в обидві сторони Положення мембрани також називається амплітудою Воно характеризує гучність записуваного звуку

З одним мікрофоном ми можемо записати тільки одноканальний звук (моно), в якому відсутнє відчуття простору Маючи два мікрофони, ми можемо вимірювати звукові хвилі в різних точках простору і отримувати таким чином так званий стереозвук Досягти цього можна, наприклад, розміщуючи мікрофони ліворуч і праворуч від джерела звуку Коли він потім відтворюється одночасно з двох динаміків, ми можемо почути просторовий компонент аудіозаписи Однак це також означає, що нам необхідно зберігати при записі вдвічі більше звукових семплів

Відтворення – загалом-то, нескладний процес Маючи звукові семпли в цифровому вигляді, збережені з конкретною частотою дискретизації у вигляді даних певного типу, ми можемо вивести їх на наше відтворює звук пристрій, який трансформує інформацію в сигнали для підключеного до нього динаміка Динамік дешифрує сигнал і перетворює його в коливання своєї мембрани, яка, в свою чергу, змушує рухатися молекули повітря навколо неї і таким чином генерувати звукові хвилі Все те ж саме, що і при записі, тільки у зворотному порядку

Якість звуку і компресія

Та вже, суцільна теорія Навіщо нам взагалі про це знати Якщо ви читали уважно, то розумієте, що якість звукового файлу залежить від частоти дискретизації і типу даних, використаних для його збереження Чим вище частота дискретизації і більше точність типу даних, тим краще буде результат на виході Однак не варто забувати про збільшуються вимоги до місткості сховища

Уявіть, що ми записали один і той же аудіоролик довжиною 60 секунд двічі: у перший раз з частотою дискретизації 8 КГц і 8 бітами на семпл і вдруге з частотою 44 КГц і 16 бітами на семпл Як ви думаєте, скільки місця буде потрібно нам для зберігання в обох випадках Для першого ролика ми використовуємо 1 байт на семпл Помножимо це значення на частоту 8000 Гц і отримаємо 8000 байт в секунду Для 60-секундної звукозапису нам буде потрібно 480 000 байт, або приблизно 0,5 Мбайт Друга, високоякісна запис потребують більше місця: 2 байти на семпл, 2 рази по 44 000 байт в секунду Разом 88000 байт в секунду Множимо на 60 секунд і отримуємо в підсумку 5280000 байт (трохи більше 5 Мбайт) Стандартна трихвилинна поп-пісенька в такій якості займе приблизно 15 Мбайт (і це ми говоримо про режим моно, для стереокачества місця буде потрібно в два рази більше) Чи не забагато місця для дурної пісеньки

Розумні люди подбали про це і розробили різні способи зменшити кількість байт, необхідних для запису звуку Вони винайшли досить складні психоакустические алгоритми компресії, що аналізують вихідні запису і дають на виході їх стислі версії Цьому процесу зазвичай супроводжують втрати – деякі несуттєві частини оригінального запису віддаляються При використанні таких форматів, як МРЗ або OGG, ви насправді слухаєте стислі аудіозаписи з втратами, проте їх застосування допомагає зменшити кількість необхідного дискового простору

Як же ці стислі аудіозаписи відтворюються Хоча існують спеціальні апаратні чіпи для декодування різних аудіоформатів, в більшості випадків перед тим, як згодувати звукової карти запис, необхідно її спочатку прочитати і декомпрессіровать Ми можемо зробити це одного разу і зберігати всі стиснені звуки в памяті або ж витягувати фрагменти аудіофайлу в міру необхідності

&nbsp

На практиці

Ви переконалися, що навіть трихвилинні пісеньки можуть зайняти багато місця Коли ми відтворюємо музику з нашої гри, то витягаємо звукові фрагменти на льоту, замість того щоб завантажувати їх заздалегідь у память Зазвичай у нас відтворюється тільки один звуковий потік за раз, тому нам необхідний тільки одноразовий доступ до диску Для коротких звукових ефектів (наприклад, вибухів або пострілів) картина дещо інша – один і той же звуковий ефект ми будемо застосовувати неодноразово і одночасно Потокове відтворення звукових фрагментів з диска для кожного екземпляра ефекту – не найкраще рішення

Однак нам щастить – короткі звуки зазвичай займають мало місця в памяті Тому ми можемо завантажити їх у память, звідки будемо відтворювати у міру потреби

Отже, у нас такі вимоги Нам необхідні способи: завантаження аудіофайлів для потокового відтворення з памяті управління відтворенням потокового звуку управління відтворенням повністю завантаженого в память звуку

Все це безпосередньо транслюється в інтерфейси Audi о, Musi с і Sound (показані в лістингах 33-35 відповідно)

Лістинг 33 Інтерфейс Audio

Інтерфейс Audio – наш спосіб створювати нові інтерфейси Music і Sound Примірник Musi з представляє потоковий аудіофайл, інтерфейс Sound – короткий звуковий ефект, який ми можемо зберігати в памяті Методи AudionewMusicO і Audi о newSound приймають імя файлу як аргумент і викликають виключення IOExcepti on при збої процесу завантаження (наприклад, якщо потрібний файл не існує або пошкоджений) Імена файлів співвідносяться з файлами активів, що зберігаються в пакеті АРК нашого застосування

Лістинг 34 Інтерфейс Music

Інтерфейс Music трохи складніший Він включає в себе методи для початку відтворення музичного потоку, призупинення та припинення відтворення, а також циклічного відтворення (початок програвання звуку з початку відразу після закінчення) Крім того, ми можемо встановити гучність у вигляді типу даних float в діапазоні від 0 (тиша) до 1 (максимум) Інтерфейс також містить кілька методів отримання, дозволяють відстежити поточний стан екземпляра Music Після того як необхідність в обєкті Music відпаде, його необхідно утилізувати – це звільнить системні ресурси, а також зніме блокування з відтвореного звукового файлу

Лістинг 35 Інтерфейс Sound

Інтерфейс Sound, навпаки, дуже простий Все, що нам потрібно, – виклик методу play, що приймає як параметр гучність у вигляді числа Я oat Ми можемо викликати метод pi ау всякий раз, коли захочемо (Наприклад, при кожному пострілі або стрибку персонажа) Коли необхідність у примірнику Sound відпаде, його слід знищити з тих же причин – для звільнення та потенційно повязаних системних ресурсів

ПРИМІТКА

Ми досить глибоко занурилися у світ звуку, про програмування аудіо потрібно дізнатися ще дуже багато Щоб розділ не розрісся, мені довелося спростити деякі речі Наприклад, зазвичай ви не будете змінювати гучність звуку лінійно Однак у нашому випадку можна опустити подібні деталі Просто знайте, що в цьому питанні ще багато чого залишилося невивченим

Графіка

Останній великий модуль, який нам необхідно вивчити, – графіка Як ви, ймовірно, здогадалися, він відповідає за промальовування зображень (також відомих як растри) на нашому екрані Це може звучати просто, але якщо ви хочете використовувати високопродуктивну графіку, то повинні познайомитися для початку з основами роботи з графікою Почнемо зі старого доброго 2D

Перше питання, яке нам потрібно поставити собі, – яким чином взагалі зображення зявляються на екрані Відповідь на нього досить складний, і нам немає потреби вникати в усі деталі – просто побіжно розглянемо те, що відбувається в нашому компютері і на екрані

Про растрах, пікселах і фреймбуфер

Зараз всі дисплеї засновані на растрі – двомірної таблиці так званих елементів зображення Ви можете знати їх під імям пікселі, і ми будемо їх так називати далі Таблиця растрів обмежена по ширині і висоті, які виражені кількістю пікселів в рядку і стовпці Якщо ви допитливі, то можете включити ваш компютер і спробувати побачити на екрані окремі пікселі Відразу зауважу, що не несу відповідальності за ті пошкодження, які можуть отримати ваші очі

У пікселя є два атрибута: позиція в таблиці і колір Позиція пікселя виражається двомірними координатами в дискретної координатної системі Дискретність в даному випадку означає, що кожна координата виражена цілим числом Взагалі, пікселі визначаються у вигляді евклідовой координатної системи, накладеної на таблицю і що починається з лівого верхнього кута Значення координати х ростуть зліва направо, координати у – зверху вниз Остання обставина часто збиває з пантелику Але на це є проста причина, і скоро ми про неї поговоримо

Ігноруючи вісь у, ми побачимо, що через дискретної природи координат їх початок збігається з лівим верхнім кутом таблиці, розташованим за адресою (0, 0) Піксел праворуч від нього має координати (1 0), піксел під ним – (0 1) і т д (подивіться на ліву частину рис 320) Растрова таблиця дисплея небезмежна, тому кількість координат обмежено Координати з негативними значеннями знаходяться за межами екрану (як і координати, рівні і перевищують межі растра) Зверніть увагу – максимальне значення координати х одно ширині таблиці мінус 1, а максимальної координати у – висота мінус 1 Це відбувається через те, що координати починаються з нуля, а не одиниці (поширена причина непорозумінь при програмуванні графіки)

Рис 320 Спрощена схема таблиці растрів і VRAM

Дисплей отримує постійний потік інформації від графічного процесу Він кодує колір кожного пікселя екранної таблиці, як це визначається програмою або операційною системою при управлінні промальовуванням дисплея Екран оновлює свій стан кілька десятків разів на секунду (це значення називається частотою оновлення, яка виражається в герцах) Частота оновлення РК-дисплеїв становить зазвичай 60 Гц в секунду ЕПТ-монітори і плазмові панелі підтримують велику частоту

Графічний процесор має доступ до спеціальної області памяті, також відомої як відеопамять (або VRAM) Усередині VRAM, в свою чергу, зарезервована область для зберігання кожного пікселя, відображуваного на екрані

Ця область зазвичай називається фреймбуфер, а повне зображення екрану – фреймом Кожному пикселу таблиці растрів відповідає адреса в памяті під фреймбуфер, який зберігає його колір Коли ми хочемо щось змінити на екрані, то міняємо колірні значення в цій області VRAM

Прийшов час пояснити, чому вісь у в системі координат дисплея спрямована вниз Память (будь це VRAM або звичайна RAM) лінійна і односпрямована Уявляйте її у вигляді одновимірного масиву Як же нам розмістити двомірні координати пікселя в одновимірної комірці памяті На рис 320 показана вельми маленька таблиця 3×2 пікселя, а також її подання до VRAM (уявімо, що VRAM складається тільки з фреймбуфер) Виходячи з нього, ми можемо вивести формулу обчислення адреси памяті для пікселя з координатами х і у:

Ми можемо також піти іншим шляхом, розрахувавши окремо координати х і у пікселя:

Отже, вісь у направлена ​​вниз через компонування памяті квітів пікселів в VRAM Це щось на зразок спадщини, що дістався нам від епохи початку компютерної графіки Монітори тоді оновлювали колір кожного пікселя екрану, починаючи з лівого верхнього кута, рухаючись вправо до упору, повертаючись вліво на наступному рядку і так до нижньої межі екрану Було зручно мати таку структуру VRAM, яка спрощує передачу інформації про колір на монітор

ПРИМІТКА

Якби у нас був повний доступ до фреймбуфер, ми могли б використовувати випереджаючі розрахунки для написання повнофункціональної бібліотеки малювання пікселів, ліній, прямокутників, зображень в памяті і т д Сучасні операційні системи з різних причин не надають повний доступ до фреймбуфер Замість цього ми зазвичай малюємо в області памяті, яка потім копіюється в справжній фреймбуфер операційною системою Проте загальні принципи при цьому залишаються незмінними Якщо вам цікаво, як робляться подібні низькорівневі речі, пошукайте в інформацію про Брезенхеме і його алгоритмах малювання ліній і кіл

Vsync і подвійна буферизація

Отже, значення частот оновлення не занадто великі, і ми можемо записувати дані під фреймбуфер швидше, ніж оновлюється екран Таке цілком може статися Більше того – ми не знаємо точно, коли дисплей отримує останню копію кадру з VRAM, що може викликати проблеми, якщо ми знаходимося в процесі малювання чогось У цьому випадку дисплей покаже нам частині старого вмісту фреймбуфер упереміш з новими, що вкрай небажано Ви можете спостерігати даний ефект в багатьох іграх для PC, де його можна описати як розмитість (на екрані одночасно видно частини старого і нового кадру)

Перша частина рішення цієї проблеми – так звана подвійна буферизація Замість того щоб використовувати єдиний фреймбуфер, графічний процесор (GPU) управляє двома – основним і фоновим З основного буфера дисплей отримує інформацію про квіти пікселів Фоновий буфер потрібен для промальовування наступного кадру, поки екран перетравлює вміст основного буфера Після промальовування поточного кадру ми наказуємо GPU поміняти ці буфери місцями, що зазвичай означає команду поміняти місцями їх адреси в памяті У літературі з програмування графіки та документації по API ви можете виявити такі терміни, як переворот сторінки і обмін буферів, якраз і позначають описаний процес

Однак подвійна буферизація сама але собі не вирішує проблему повністю: обмін теж може відбутися в той момент, коли екран знаходиться в процесі оновлення свого вмісту Тутнам приходить на допомогу вертикальна синхронізація (також відома як vsync) При виклику методу обміну між буферами графічний процесор блокується до тих пір, поки дисплей не повідомляє про закінчення оновлення Після отримання цього сигналу GPU може безпечно змінювати адреси памяті буферів, і все буде добре

На щастя, зараз нам рідко доводиться піклуватися про ці наданих подробицях VRAM і тонкощі подвійний буферизації і вертикальної синхронізації безпечно приховані від нас, тому ми не можемо накоїти з ними справ Замість цього нам пропонується набір API, зазвичай обмежує нас у маніпуляціях вмістом нашого вікна програми Деякі з цих API (наприклад, OpenGL ES) використовують апаратне прискорення, а це зазвичай передбачає лише маніпулювання VRAM за допомогою спеціальних контурів графічного чіпа Так що, як бачите, ніякого чаклунства Причина, по якій ви повинні розуміти принципи роботи графіки (Принаймні на вищому рівні), полягає в тому, що це дозволяє вам розуміти закони, що керують продуктивністю вашої програми При включеному vsync ви ніколи не зможете перевищити частоту оновлення вашого дисплея, що може збити з пантелику, якщо ви хочете намалювати один-єдиний піксель

При використанні API без застосування апаратного прискорення ми безпосередньо не взаємодіємо з дисплеєм, замість цього малюючи один з наших Ш-компонентів у вікні У нашому випадку ми маємо справу з єдиним компонентом, розтягнутим на все вікно програми З цієї причини наша координатна система розтягується не на весь екран, а тільки на наш компонент Таким чином, наш компонент користувацького інтерфейсу стає заміною дисплею зі своїм власним фреймбуфер Операційна система керує композицією вмісту всіх видимих ​​вікон і забезпечує коректність їх передачі в частині, відповідні реальному фреймбуфер

Що таке колір

Ви, мабуть, помітили, що до цієї пори я ігнорував розмови про квіти Я створив тип col or на рис 320 і прикинувся, що все в порядку Тепер прийшов час дізнатися, що таке колір насправді

З точки зору фізики колір – реакція сітківки ока і кори головного мозку на електромагнітні хвилі Така хвиля характеризується довжиною і інтенсивністю У межах нашої видимості знаходяться хвилі довжиною від 400 до 700 нм Цей сегмент електромагнітного спектра, також відомий як видима частина спектра, укладається між фіолетовим і червоним кольорами (між ними знаходяться синій, жовтий і оранжевий) Передача кольором монітора полягає в трансляції електромагнітних хвиль певної частоти для кожного пікселя Різні види дисплеїв використовують різні способи досягнення цієї мети Спрощено цей процес можна представити так: піксель на екрані складається з трьох різних світяться частин, кожна з яких випромінює один з кольорів (червоний, синій і зелений) При оновленні дисплея ці частини світяться з різних причин (Наприклад, в ЕПТ-моніторах у них потрапляють електрони) При цьому дисплей контролює, які частини з набору світяться Якщо піксель повністю червоний, тільки червона його частина буде бомбардувати електронами з повною інтенсивністю У разі, коли нам потрібні кольори, відмінні від базових, це досягається змішуванням, яке, в свою чергу, реалізується управлінням інтенсивністю світіння частин пікселя Електромагнітні хвилі по шляху до сітківки нашого ока накладаються один на одного, і наш мозок інтерпретує цю суміш як певний колір Тому ми можемо визначити будь-який колір як змішання трьох базових кольорів різної інтенсивності

Кольорові моделі

Те, що ми обговорювали щойно, називається колірною моделлю (точніше, колірною моделлю RGB) RGB означає Red (Червоний), Green (Зелений), Blue (Синій) Існує безліч та інших колірних моделей (наприклад, YUV і CMYK) Однак у більшості API для програмування модель RGB є, по суті, стандартом, тому й ми будемо обговорювати тільки її

Колірна модель RGB називається адитивною через те, що фінальний колір виходить змішуванням і доповненням базових кольорів (червоного, синього, зеленого) Ви, швидше за все, експериментували зі змішанням квітів ще в школі Малюнок 321 демонструє кілька прикладів змішування кольорів в моделі RGB

Звичайно, ми можемо створювати набагато більше квітів, ніж показано на рис 321, варіюючи інтенсивність червоного, синього і зеленого кольорів Кожен компонент має інтенсивністю від 0 до максимального значення (Припустимо, 1) Якщо ми інтерпретуємо кожен компонент кольору як значення однієї з трьох осей евклідовой осі координат, то зможемо побудувати так званий колірний куб (рис 322) При зміні інтенсивності кожного компонента можна отримати ще більше квітів Колір розглядається як триплет (червоний, зелений, синій), де значення кожного елемента лежить в діапазоні від 0 до 1 Наприклад, 0,0 означає відсутність інтенсивності для даного кольору, а 1,0 – повну інтенсивність Чорний колір має значення (0, 0, 0), білий – (1 1 1)

Рис 321 Змішування базових квітів

Рис 322 Колірний куб

Цифрове кодування квітів

Як ми можемо закодувати триплет квітів RGB в компютерній памяті Для початку необхідно визначитися з типом даних, який ми будемо використовувати для колірних компонентів Можна скористатися числами з плаваючою точкою і визначити діапазон між 0,0 і 1,0 Це дало б нам велику різноманітність варіантів кольорів На жаль, даний спосіб вимагає багато місця (3 рази по 4 або 8 байт на піксель залежно від того, чи ми використаємо 32 – або 64-бітові числа) Тому кращим рішенням буде відмовитися від усього можливого різноманіття кольорів, тим більше що дисплеї зазвичай здатні передавати обмежена їх кількість Замість чисел з плаваючою точкою ми можемо застосувати беззнакові цілі числа Інтенсивність кожного компонента варіюється від 0 до 255 У цьому випадку для одного пікселя нам буде потрібно лише 3 байта, або 24 біта, а значить, ми зможемо закодувати таким чином 2 в 24-й ступеня (16 777 216) кольорів Я б сказав, що цього цілком достатньо

Чи можемо ми заощадити ще Так, можемо Ми можемо запакувати кожен компонент в окреме 16-бітове слово, і в цьому випадку кожному пикселу потрібно 2 байта для зберігання Червоному кольору буде необхідно 5 біт, зеленому – 6, синій буде використовувати залишилися 5 біт Причина, по якій зеленому кольору потрібно більше біт, в тому, що наші очі розпізнають більше відтінків зеленого, ніж синього або червоного Всі біти разом дають 2 в 16-й ступеня (65 536) варіантів можливих кольорів На рис 323 показані три описаних вище способу кодування кольору

Рис 323 Кольорове кодування відтінку рожевого (на чорно-білому малюнку він, на жаль, виглядає сірим)

У випадку з типом float ми можемо використовувати 32-бітний тип даних Java При 24-бітному кодуванні у нас виникає невелика проблема: в Java немає 24-бітного типу integer, тому нам або доведеться зберігати кожен компонент в типі byte, або задіяти 32-бітний тип integer, залишаючи 8 його біт невикористаними При 16-бітному кодуванні ми можемо або застосовувати два окремих байта, або зберігати компоненти в даних типу short Зауважте також, що Java не має беззнакових типів даних, проте ми можемо спокійно використовувати знакові типи для зберігання беззнакових значень

Як при 16-бітному, так і при 24-бітному кодуванні необхідно також визначити порядок, в якому зберігатимуться три колірних компонента в типі даних short або integer Зазвичай використовуються два варіанти: RGB і BGR На рис 323 використовується RGB-кодування Синій компонент зберігається в нижніх 5 або 8 бітах, зелений компонент – в наступних 6 або 8 бітах, червоний – у верхніх 5 або 8 бітах При BGR-кодуванні порядок зворотний – зелений залишається на місці, червоний і синій міняються місцями У ті ми будемо користуватися RGB-порядком, оскільки в графічному API Android застосовують саме його

Отже, підібємо підсумки нашої дискусії про кодування кольорів Про 32-бітове RGB-кодування f1oat використовує 12 байт на піксель, інтенсивність при цьому змінюється в діапазоні від 0,0 до 1,0 Про 24-бітове RGB-кодування 1 nteger застосовує 3 або 4 байти на піксель, інтенсивність при цьому варіюється від 0 до 255 Порядок компонентів може бути RGB або BGR Іноді використовується маркування RGB888 або BGR888, де цифра 8 означає кількість бітів на компонент

16-бітове RGB-кодування Integer застосовує 2 байта на піксель інтенсивність червоного і синього кольорів змінюється від 0 до 31, зеленого – від 0 до 63 Порядок компонентів може бути RGB або BGR Іноді використовується маркування RGB565 або BGR565, де цифри 5 і 6 означають кількість бітів на відповідний компонент

Застосовуваний тип кодування також називається глибиною кольору Зображення, які ми створюємо і зберігаємо на диску або в памяті, володіють певною глибиною кольору, то ж відноситься і до фреймбуфер і самому дисплею Сучасні екрани зазвичай володіють глибиною кольору за умовчанням в 24 біта, але в деяких випадках можуть бути налаштовані на менше значення Фреймбуфер графічного обладнання також досить гнучкий і може використовувати різні глибини кольору Створені нами зображення, природно, можуть мати зовсім різною глибиною

ПРИМІТКА

Існує багато способів кодування колірної пиксельной інформації Крім квітів RGB ми можемо застосовувати відтінки сірого, що складаються з одного компонента Але оскільки ці способи широко не використовуються, ми в даний момент їх проігноруємо

Формати зображення і стиснення

На певному етапі процесу розробки гри наш художник запропонує нам зображення, створені за допомогою спеціального програмного забезпечення (наприклад, Gimp, PainNET або Photoshop) Ці зображення можуть зберігатися на диску в різних форматах Для чого така різноманітність Хіба ми не можемо зберігати растрові картинки на диску як простий набір байтів

Загалом-то, так, можемо Однак подивимося, скільки це займе місця Якщо нам необхідно кращу якість, то пікселі будуть кодуватися в RGB888 (24 біта на піксель) Припустимо, розмір зображення становить 1024 х 1024 пікселя Разом – 3 Мбайт на одну невелику картинку При використанні методу RGB565 ви заощадите, але небагато – розмір зменшиться на 1Мбайт

Як і у випадку зі звуком, над методами зменшення обсягу необхідної для зберігання зображення памяті була проведена велика робота Були розроблені алгоритми компресії, призначені для зберігання зображень і зберігають якомога більше інформації про колір Два найбільш популярних формату-JPEG і PNG JPEG – формат з втратою якості Це означає, що деяка частина вихідних даних в процесі стиснення втрачається, однак він пропонує більшу ступінь стиснення і таким чином економить більше місця на диску PNG відноситься до числа форматів без втрати даних, тому він відтворює зображення зі стовідсотковою точністю Вибір формату залежить, таким чином, переважно від обмежень ємності сховища

Як і у випадку зі звуковими ефектами, нам доведеться повністю декомпрессіровать зображення при завантаженні його в память Так що навіть якщо ваша картинка займає 20 Кбайт на диску, вам все одно знадобиться місце для приміщення її повної таблиці растрів в памяті

Після того як зображення завантажене і розархівувати, воно стає доступно у вигляді масиву піксельних кольорів (точно таким же чином, як фрейм-буфер існує в оперативній памяті) Єдині відмінності – Пікселі зберігаються у звичайній RAM, а глибина кольору може відрізнятися від глибини кольору фреймбуфер Завантажене зображення має таку ж систему координат, що і фреймбуфер – з віссю х, спрямованої зліва направо, і віссю у, спрямованої зверху вниз

Після завантаження зображення ми можемо перемалювати її з памяті у фрейм-буфер, просто передавши кольору пікселів з картинки в відповідні положення фреймбуфер Нам не доведеться робити це вручну: для реалізації цього функціонала існує відповідне API

Альфа-накладення і змішування

Перед тим як почати розробку графічних інтерфейсів, нам доведеться розібратися з ще одним питанням: накладенням зображень Припустимо, що у нас є фреймбуфер, який ми можемо використовувати для промальовування, а також набір завантажених в память картинок, які ми будемо розміщати в цей фреймбуфер На рис 324 показано просте фонове зображення, а також Боб – зомбі-вбивця

Рис 324 Простий фон і Боб, господар Всесвіту

Щоб намалювати світ Боба, для початку помістимо фон під фреймбуфер, а потім туди ж запустимо поверх нього нашого героя Порядок дій важливий, адже друга картинка перепише поточний зміст фреймбуфер Отже, що ми отримаємо в результаті Відповідь на рис 325

Рис 325 Накладення Боба на фон під фреймбуфер

Це явно не те, що нам було потрібно Зверніть увагу на рис 324 – Боб оточений білими пікселями Коли ми поміщаємо його поверх фону під фреймбуфер, ці білі пікселі теж там виявляються Що нам треба зробити, щоб Боб зявився на тлі один, без супутнього йому білого тла

Нам допоможе альфа-змішування Взагалі-то в випадку з Бобом технічно більш вірним буде говорити про альфа-маскуванні (що є підвидом альфа-змішування) Звичайно програма по обробці зображень дає нам можливість визначити не тільки RGB-значення пікселя, але і його напівпрозорість (будемо розглядати її як ще один компонент кольору пікселя) Ми можемо кодувати її так само, як і червоний, зелений і синій компоненти

Раніше я згадував, що ми можемо зберігати 24-бітний RGB-триплет в 32-бітному типі даних integer У цьому випадку у нас залишається 8 невикористаних біт, які можна використовувати для зберігання значення альфи Напівпрозорість пікселя може в даному випадку варіюватися від 0 (повна прозорість) до 255 (непрозорість) Такий спосіб кодування відомий як ARGB8888 або BGRA8888 (залежно від порядку) Звичайно, існують також формати RGBA8888 і ABGR8888

При використанні 16-бітного кодування ми стикаємося з невеликою проблемою – всі біти типу даних short зайняті колірними компонентами Визначимо якийсь формат ARGB4444 (за аналогією з ARGB8888), в якому 12 біт будуть відведені під RGB-значення (по 4 біта на елемент)

Ми можемо легко уявити собі, як може працювати метод промальовування повністю прозорих і повністю непрозорих пікселів У першому випадку ми просто ігноруємо пікселі з альфа-компонентою, рівний 0, під другий – просто перемальовували піксел кольором У разі проміжного значення альфа-компоненти пікселя все дещо складніше

Розглядаючи змішування з формальної точки зору, нам потрібно визначити кілька моментів: у змішування є два входи і один вихід, кожен з яких представлений у вигляді RGB-триплетів і альфа-значення (а) два входи називаються джерелом і метою Джерело – це піксель зображення, який ми хочемо намалювати поверх цілі (наприклад, під фреймбуфер) Мета – піксель, який ми хочемо частково перемалювати джерелом вихід – це теж колір, який визначається RGB-кодонів і альфа-значенням Зазвичай ми ігноруємо альфу, для простоти в цьому розділі зробимо так само щоб злегка спростити нашу математику, уявімо значення RGB-компонента і альфа в діапазоні від 0,0 до 1,0

Озброївшись цими визначеннями, ми можемо створити так звані рівняння змішування Найпростіше з них виглядає приблизно так:

src і dst – пікселі джерела і цілі, які ми хочемо обєднати Два кольори змішуються покомпонентно Зверніть увагу на відсутність у цих рівняннях альфа-значення цільового пікселя Спробуємо це на прикладі:

Малюнок 326 ілюструє попереднє рівняння Наше джерело пофарбований у відтінок рожевого, мета – у відтінок зеленого Обидва вони беруть участь у створенні підсумкового кольору (якогось відтінку оливкової)

Рис 326 Змішування двох пікселів

Два гідних джентльмена з іменами Портер і Дафф створили дуже багато рівнянь змішування Однак нам вистачить і описаного вище, оскільки його буде достатньо для більшості ситуацій Поекспериментуйте з ним на папері або в графічній програмі, щоб отримати більш чітке уявлення про змішування і накладення кольорів

ПРИМІТКА

Змішування – широке поле для діяльності Якщо ви хочете використовувати весь його потенціал, можете пошукати в Мережі роботи Портера і Даффа на цю тему Однак для ігор, які ми будемо писати, досить буде найпростішого рівняння

Зверніть увагу на велику кількість операцій множення, що беруть участь у вищезазначених рівняннях (шість, якщо бути точним) Множення – ресурсномістка операція, і її необхідно по можливості уникати При змішуванні ми можемо позбутися трьох операцій множення, попередньо помноживши RGB-значення пікселя-джерела на його ж альфа-значення Більшість програм з обробки графіки підтримують попереднє множення значень RGB на відповідне значення альфи Якщо підтримка таких операцій відсутній, ви можете зробити це в процесі завантаження картинки в память Однак при використанні графічного API для промальовування зображень із змішуванням, нам необхідно буде переконатися у використанні правильного рівняння змішування Наша картинка все ще буде містити альфа-значення, тому попереднє рівняння змішування видасть неправильні результати Альфа джерела не повинна множитися на колір джерела На щастя, всі графічні API для Android дозволяють нам повністю визначити, як ми хочемо змішувати наші зображення

У випадку з Бобом ми за допомогою графічного редактора просто встановлюємо альфа-значення всіх білих пікселів рівними 0, завантажуємо зображення у форматі ARGB8888 або ARGB4444 (можливо, попередньо перемноживши) і використовуємо метод промальовування, що виробляє альфа-змішування з правильним рівнянням Результат можна побачити на рис 327

Рис 327 Зліва Боб вже змішаний праворуч Боб в редакторі PaintNET шахівниця демонструє, що альфа-значення пікселів білого тла дорівнює нулю

ПРИМІТКА

Формат JPEG не підтримує зберігання альфа-значний для пікселів Використовуйте для таких випадків формат PNG

На практиці

Озброївшись всією цією інформацією, ми можемо, нарешті, почати розробляти інтерфейси нашого графічного модуля Визначимо для початку їх функціональність Зверніть увагу: коли я говорив про фреймбуфер, то насправді мав на увазі віртуальний фреймбуфер компонента створюваного нами користувача інтерфейсу Ми просто робимо вигляд, що застосовуємо реальний фреймбуфер Нам необхідно, щоб наш модуль виконував наступні дії: завантаження зображень з диска і зберігання їх в памяті для подальшої промальовування очистка фреймбуфер з кольором для видалення вмісту його минулого кадру Про установка певного кольори для певного положення пікселя у фрейм-буфері промальовування ліній і прямокутників під фреймбуфер промальовування раніше завантажених зображень під фреймбуфер Було б непогано мати можливість малювати не тільки зображення цілком, але і його частини Вам також необхідно вміти малювати картинки із змішуванням і без нього отримання розмірів фреймбуфер

Я пропоную два простих інтерфейсу: Graphics і Bitmap Почнемо з інтерфейсу Graphics (лістинг 36)

Лістинг 36 Інтерфейс Graphics

Починаємо з відкритого статичного перерахування Pi xmapFormat, необхідного для кодування різних підтримуваних нами форматів Далі у нас кілька методів інтерфейсу

GraphicsnewPixmapO – завантажує зображення в JPEG-або PNG-форматі Ми визначаємо бажаний формат для результуючого Pixmap, який буде використовуватися механізмом завантаження Ми робимо так, щоб мати можливість якимось чином управляти зліпком памяті наших зображень (тобто завантажуючи зображення форматів RGB888 або ARGB8888 у формати RGB565 або ARGB4444) Файл визначає ресурс, що міститься в АРК-файлі нашого застосування

Graphi cs clear – повністю очищає фреймбуфер з відповідним кольором Всі кольори в нашому маленькому фреймворці визначені у вигляді 32-бітних значень формату ARGB8888 (звичайно, Pixmaps може мати й інший формат)

Graphi csdrawPixel О – встановлює значення пікселя фреймбуфер з координатами (х у) в певний колір Координати за межами екрану будуть ігноруватися (це називається кліппінгом)

Graphics drawLine – аналогічний Graphi csdrawPixel Визначаємо початкову та кінцеву точку лінії, а також її колір Будь частина лінії, що виходить за межі фреймбуфер, буде ігноруватися

Graphi cs drawRect – малює прямокутник під фреймбуфер Координати (г, у) визначають позицію його лівого верхнього кута Аргументи wi dth і hei ght задають кількість пікселів розміру прямокутника Аргумент color вказує колір його заповнення

Graphics DrawPixmap – малює прямокутні ділянки Pi xmap під фреймбуфер Координати (х у) визначають позицію лівого верхнього кута розташування цілі Pixmap під фреймбуфер Аргументи srcX і srcY (виражені в координатної системі Pixmap) позначають відповідний лівий верхній кут ділянки прямокутника, використовуваного Pixmap Параметри srcWidth і srcHeight означають розмір ділянки, який ми витягаємо з Pi xmap

GraphicsgetWidth і Graphics GetHeight – повертають ширину і висоту фреймбуфер у точках

Всі методи промальовування, крім Graphics Clear, автоматично виконують змішування кожного пікселя, з яким вони працюють (як описано в попередньому вище) Ми можемо відключити змішування для кожного окремого пункту для деякого прискорення промальовування, однак це ускладнить нашу реалізацію Зазвичай в простих іграх начебто Містера Нома можна залишити змішування включеним

Інтерфейс Pi xmap описаний в лістингу 37

Лістинг 37 Інтерфейс Pixmap

Код: нудна рутина

Ми робимо його дуже простим і незмінний, оскільки всі накладення виконується під фреймбуфер

PixmapgetWidth і PixmapgetHeightO – повертають ширину і висоту обєкта Pixmap у точках

Pixmap getFormat – повертає формат Pixel Format, використовуваний для зберігання Pixmap в оперативній памяті

Pixmap Di spose – екземпляри Pixmap застосовують память і потенційно інші системні ресурси Якщо вони нам більше не потрібні, їх слід знищити за допомогою даного методу

З цим простим графічно модулем ми зможемо в подальшому реалізувати Містера Нома Закінчимо цю главу обговоренням самого ігрового фреймворка

Ігрова середу

Після виконання всієї чорнової роботи ми можемо поговорити про те, як, власне, будемо створювати гру

Гра поділяється на екрани, що виконують одні й ті ж завдання: обробка користувача введення, відповідну зміну стану екрана, промальовування ігрової сцени Деякі екрани можуть не потребувати у підтримці користувача введення, однак реалізують перехід на інший екран після деякого проміжку часу (наприклад, екран заставки)

Екранами необхідно будь-яким чином управляти (наприклад, нам потрібно відстежувати поточний екран і мати можливість переходити з нього на другий, що насправді означає знищення першого екрану і установку другого екрану в якості основного)

Гра повинна надати екранам доступ до різних модулів (для графіки, звуку, введення і т д), щоб вони могли завантажувати ресурси, відслідковувати дії користувача, промальовувати фреймбуфер і т д

Оскільки всі наші ігри проходитимуть в режимі реального часу (що означає постійний рух і оновлення компонентів), нам необхідно забезпечити якомога більш часте оновлення стану поточного екрана Зазвичай ми робимо це всередині циклу, званого основним Даний цикл переривається, коли користувач виходить з гри Одна ітерація циклу називається кадром Кількість кадрів в секунду (fps), яке ми можемо розрахувати, називається частотою оновлення

Говорячи про час, нам також необхідно відстежувати проміжок часу, що минув з часу появи останнього кадру Це потрібно для кадро-незалежного руху (яке ми скоро обговоримо)

У грі також необхідно відстежувати стан вікна (наприклад, поставлена ​​гра на паузу чи ні) та інформувати поточний екран про ці події

Ігрова середу буде мати справу з налаштуванням вікна і створенням компонента користувача інтерфейсу, який ми будемо промальовувати і від якого будемо отримувати користувача введення

Давайте зробимо з усього цього трохи псевдокоду, поки ігноруючи такі аспекти управління вікном, як постановка на паузу і повернення з неї:

Ми почали зі створення вікна для нашої гри і компонента користувача інтерфейсу для промальовування і отримання користувача введення Далі ми инициализируем всі необхідні для низькорівневої роботи модулі Ми створюємо наш стартовий екран і робимо його поточним, а також записуємо поточний час Далі відбувається вхід у головний цикл, переривався в той момент, коли користувач демонструє своє бажання вийти з гри

Усередині основного циклу ми розраховуємо так званий дельта-інтервал – час, який пройшов з початку минулого кадру Потім записуємо час початку поточного кадру Дельта-інтервал і поточний час зазвичай вимірюються в секундах У випадку з екраном дельта-інтервал показує, як багато часу пройшло з моменту останнього його поновлення Ця інформація необхідна нам для кадро-незалежного руху (про яке ми дуже скоро поговоримо)

Нарешті, ми просто оновлюємо стан поточного екрану і демонструємо його користувачеві Це оновлення залежить від дельта-інтервалу та стану отже, ми передаємо ці дані екрану Подання складається з візуалізації стану екрана під фреймбуфер, а також з відтворення будь-якого запитуваної екраном аудіоресурсів (наприклад, звуку пострілу) Метод подання іноді також повинен знати, скільки часу пройшло з моменту його останнього дзвінка

При перериванні роботи головного циклу ми можемо очистити і звільнити всі ресурси, після чого закрити вікно

Саме таким чином практично кожна гра поводиться на високому рівні – обробка введення, оновлення стану, подання його користувачеві і повторення всього цього до безкінечності (або до того моменту, коли гравцеві набридне)

Додатки з інтерфейсом користувача на сучасних операційних системах зазвичай не працюють у режимі реального часу Вони використовують подієву парадигму, при якій операційна система оповіщає додаток про наступаючих події введення, а також про необхідність візуалізації Це досягається за допомогою функцій зворотного виклику, які додаток реєструє в операції при старті Це вони відповідають за обробку які попередження про події Все це відбувається в рамках так званого користувальницького потоку – головного потоку додатки Це взагалі хороша ідея – повертати відповіді від функцій зворотного виклику якомога частіше, тому нам не варто реалізовувати наш головний цикл в одній з них

Замість цього ми розміщуємо головний цикл гри в окремому потоці, створюваному при старті гри Це означає, що нам необхідно дотримати деякі запобіжні заходи при отриманні подій з користувальницького потоку (таких як введення або події вікна) Але цими деталями ми займемося пізніше, коли будемо реалізовувати ігровий фреймворк на Android Просто памятайте, що нам необхідно буде синхронізувати користувальницький потік і головний цикл програми в певні моменти

Ігрові та екранні інтерфейси

Озброївшись отриманими знаннями, спробуємо розробити інтерфейс гри Він повинен реалізовувати такі функції: створення вікна і компонента користувача інтерфейсу, а також функцій зворотного виклику для обробки екранних і користувальницьких подій запуск потоку головного циклу програми відстеження поточного екрану, оновлення та подання його в кожній ітерації головного циклу (тобто в кадрі) відстеження будь-яких подій вікна (наприклад, постановки на паузу і відновлення гри) з потоку для користувача інтерфейсу і передача їх екрану для відповідної зміни стану надання доступу до всіх раніше розробленим модулям: Input, FilelO, Graphics і Audio

Як розробники ігор ми байдужі до того, в якому потоці працює наш головний цикл і чи потрібно йому синхронізуватися з інтерфейсним потоком Нам би просто хотілося створити різні ігрові екрани з невеликою допомогою низькорівневих модулів і оповіщень про віконні подіях Тому ми створимо дуже простий інтерфейс Game, що ховає від нас всі складні подробиці, а також абстрактний клас Screen, який буде використовуватися для реалізації наших екранів (код інтерфейсу показаний в лістингу 38)

Лістинг 38 Інтерфейс Game

Як ви, ймовірно, й очікували, тут описані кілька методів-одержувачів, які повертають екземпляри низькорівневих модулів (які наша реалізація Game буде створювати і відстежувати) Метод Game setScreen дозволяє встановити для гри поточний екран Ці методи будуть реалізовані один раз (одночасно із створенням внутрішніх потоків, управлінням вікном і головним циклом для оновлення та подання екрану) Метод GamegetCurrentScreenO повертає поточний активний екран

Для реалізації інтерфейсу Game ми пізніше будемо використовувати абстрактний клас AndroidGame, який реалізує всі методи, крім GamegetStartScreen – він залишиться абстрактним Коли ми створимо екземпляр Androi dGame для нашої гри, то успадкуємо і реалізуємо в ньому метод Game getStartScreen, повернувши екземпляр першого екрану гри

Щоб ви отримали деяке враження про легкість, з якою буде створюватися наша гра, подивіться на приклад (уявіть, що ми вже реалізували клас Androi dGame):

Вражає, чи не так Все, що нам необхідно зробити, – реалізувати екран, з якого повинна починатися наша гра, а далі Androi dGame, від якого ми успадковувалися, зробить все інше Заглядаючи вперед – він же буде змушувати екран MySuperAwesomeStartScreen оновлюватися і промальовуватися в потоці головного циклу Зверніть увагу – у нашій реалізації Screen ми передали конструктору MyAwesomeGame сам екземпляр

ПРИМІТКА

Якщо у вас виникло питання, що ж насправді створює наш клас MyAwesomeGame, я вам підкажу: AndroidGame буде успадковуватися від Activity, автоматично створюваного операційною системою Android при запуску користувачем гри

Останнім елементом нашої головоломки буде абстрактний клас Screen Ми реалізуємо його у вигляді класу, а не інтерфейсу, оскільки ми вже виконали для нього деяку чорнову роботу У цьому випадку нам доведеться писати менше шаблонного коду в реальних реалізаціях Screen Абстрактний клас Screen показаний в лістингу 39

Лістинг 39 Клас Screen

Тут зясовується, що попередня робота нам знадобиться Конструктор отримує екземпляр Game і зберігає його в члені з модифікатором final, доступному всім підкласам Завдяки використанню цього механізму ми досягаємо двох цілей: отримуємо доступ до низькорівневих модулям Game для відтворення звуку, промальовування екрану, отримання реакції користувача, а також читання і запису файлів

можемо встановлювати новий поточний екран, викликаючи при необхідності метод Game setScreen (наприклад, при натисканні кнопки переходу на інший екран)

Перший пункт досить очевидний: нашою реалізації Screen необхідний доступ до цих модулів, щоб робити дійсно важливі речі (на кшталт промальовування стада скажених єдинорогів)

Друга можливість дозволяє нам легко реалізувати переходи між екранами (екземплярами Screen) Кожен екземпляр може приймати рішення про перехід на інший визначений примірник Screen, грунтуючись на своєму стані (наприклад, при натисканні кнопки)

Призначення методів Screen update і Screen present цілком зрозуміло: вони оновлюють стан екрану і представляють його користувачеві відповідно Примірник Game викликає їх один раз при кожній ітерації головного циклу

Методи Screenpause і Screen, resume викликаються при постановці гри на паузу і виході з неї Ці дії також виробляються екземпляром Game і відносяться до поточного активного екрану

Метод Screen di spose викликається екземпляром Game при виклику Game setScreen У результаті поточний екземпляр Screen звільняє системні ресурси (наприклад, графічні активи, що зберігаються в Pi xmaps), щоб отримати вільне місце в памяті для нового екрану Крім того, виклик Screen di spose – останній шанс для екрану переконатися, що вся необхідна інформація збережена

Простий приклад

Продовжимо з нашим прикладом MySuperAwesomeGame – ось їх дуже проста реалізація класу:

Код: нудна рутина

Розглянемо, що цей клас робить у звязці з MySuperAwesomeGame

1 При створенні MySuperAwesomeGame він створює вікно, компонент інтерфейсу (який ми прорисовуємо і від якого отримуємо події користувача введення), функції зворотного виклику для обробки подій, а також потік головного циклу Нарешті, цей клас викликає власний метод MySuperAwesomeGame getStartScreen, який повертає екземпляр класу MySuperAwesomeStartScreen

2 У конструкторі MySuperAwesomeStartScreen завантажуємо растрове зображення з диска і зберігаємо його в змінній-члені Таким чином, завершується установка нашого екрану, і управління повертається класу MySuperAwesomeGame

3 Потік головного циклу тепер постійно буде викликати методи MySuperAwesome StartScreen updateC) і MySuperAwesomeStartScreen render щойно створеного нами екземпляра

4 У методі MySuperAwesomeStartScreenupdateC) збільшуємо значення змінної х на кожному новому кадрі Ця змінна зберігає координату х зображення, яке ми хочемо промальовувати Її значення обнуляється, коли стає рівним більше 100

5 У методі MySuperAwesomeStartScreen render очищаємо фреймбуфер, заповнюючи його чорним кольором (0x00000000 = 0), після чого прорисовуємо обєкт Pi xmap в позиції (хг, 0)

6 Потік головного циклу повторює кроки 3-5 до тих пір, поки користувач не вийде з гри, натиснувши на пристрої кнопку Назад У цьому випадку екземпляр Game викличе метод MySuperAwesomeStartScreen di spose, очищающий Pixmap

І ось вона, наша перша гра Все, що користувач побачить на її екрані, – картинку, що рухається зліва направо Не надто вражаючий геймплей, але ми продовжимо над ним працювати Зауважте, що в Android гра може бути припинена і відновлена ​​в будь-який момент Наша реалізація MyAwesomeGame викликає в цих випадках методи MySuperAwesomeStartScreen pause і MySuperAwesomeStartScreen resume, що призводить до призупинення виконання головного циклу програми на час паузи

Остання проблема, яку нам зараз необхідно обговорити, – кадронезавісімое рух

Кадронезавісімое рух

Уявімо, що користувач запустив нашу гру на пристрої, що підтримує частоту оновлення 60 кадрів в секунду Оскільки ми збільшуємо значення MySuperAwesomeStartScreen х на 1 у кожному кадрі, наш Pixmap переміститься на 100 пікселів за 100 кадрів При частоті відновлення 60 кадрів в секунду (fps) досягнення положення (100 0) займе приблизно 1,66 секунди

Тепер припустимо, що інший користувач грає в нашу гру на іншому пристрої, що забезпечує частоту оновлення 30 кадрів в секунду У даному випадку наш Pixmap буде переміщатися в секунду на 30 пікселів, тому точка з координатами (100,0) буде досягнута через 3,33 секунди

Це погано У нашій простій грі це може не мати значення, але уявіть на місці Pi xmap Супер Маріо і подумайте, що може для нього означати така залежність від апаратних можливостей Наприклад, ми натискаємо стрілку Вправо, і Маріо біжить в ту ж сторону У кожному кадрі він просувається на 1 піксель (як і Pi xmap) На пристрої з частотою кадрів 60 кадрів в секунду Маріо буде бігти вдвічі швидше, ніж на телефоні з частотою 30 кадрів в секунду Таким чином, характеристики апарату можуть повністю змінювати показники продуктивності гри Нам необхідно це виправити

Вирішення цієї проблеми – кадронезавісімое рух Замість переміщення нашого обєкта Pi xmap (або Маріо) на фіксовану величину за один кадр, ми визначимо швидкість його руху в юнитах за секунду Припустимо, ми хочемо, щоб наш Pi xmap переміщався на 50 пікселів в секунду Крім цього значення нам також потрібна інформація про час, що пройшов з останнього переміщення Pixmap І це якраз той момент, коли вступає в гру той дивний дельта-інтервал Він показує нам, скільки часу пройшло з останнього оновлення Таким чином, наш метод MySuperAwesomeStartScreenupdateO повинен виглядати приблизно так:

Тепер, якщо наша гра проходить при частоті 60 кадрів в секунду, переданий методу дельта-інтервал завжди буде дорівнювати приблизно 0,016 секунди (1/60) Таким чином, в кожному кадрі буде просування на 0,83 (50 0,016) пікселя, а за секунду – близько 100 пікселів (60 х 0,85) Перевіримо результати при частоті 30 кадрів в секунду: 1,66 пікселя (50 х 1/30) Множачи на 30, знову отримуємо 100 пікселів в секунду Отже – неважливо, на якому пристрої запускається наша гра, вся анімація і рух в ній будуть завжди відповідати поточному часу

Однак якщо ви спробували перевірити ці розрахунки на попередньому коді, то побачили, що наш Pixmap насправді взагалі не рухається при частоті 60 кадрів в секунду Так відбувається через помилки в нашому коді Спробуйте вгадати, де саме Це досить малопомітна, але поширена пастка, часто чатує розробників ігор Мінлива х, яку ми збільшуємо в кожному кадрі, визначена як 1 nteger Додаток 0,83 до типу integer не дає ніякого ефекту Для виправлення цієї неприємності потрібно просто замінити тип даних змінної х на float Крім того, необхідно додати суму при виклику Graphi cs drawPi xmapO

ПРИМІТКА

Хоча розрахунки значень з плаваючою точкою в Android зазвичай здійснюються повільніше, ніж для цілочисельних значень, ця різниця звичайно незначна, тому ми можемо не звертати на неї уваги

І на цьому все про нашу ігровому середовищі Ми можемо безпосередньо перетворити екрани нашого містера Нома в класи та інтерфейси фреймворка

Підводячи підсумок

Вивчивши до цього моменту стільки насичених та інформативних сторінок і, ви повинні отримати непогане уявлення про те, з яких етапів складається процес створення гри Ми вивчили декілька найпопулярніших на Android Market-жанрів і зробили деякі висновки Ми вирішили намалювати гру з нуля, використовуючи тільки ножиці, ручку і кілька аркушів паперу Нарешті, ми досліджували теоретичні основи процесу розробки ігор і навіть створили набір інтерфейсів і абстрактних класів, які будемо використовувати в е далі для реалізації наших ідей, заснованих на теоретичних ідеях Якщо ви думаєте, що вам необхідно просунутися далі меж, зверніться за допомогою до Мережі – всі ключові слова ви знаєте Розуміння принципів – ключ до розробки стабільних і високопродуктивних додатків Тепер давайте реалізовувати нашу ігрову середу для Android

Джерело: Mario Zechner / Маріо Цехнер, «Програмування ігор під Android», пров Єгор Сидорович, Євген зазноби, Видавництво «Пітер»

Схожі статті:


Сподобалася стаття? Ви можете залишити відгук або підписатися на RSS , щоб автоматично отримувати інформацію про нові статтях.

Коментарів поки що немає.

Ваш отзыв

Поділ на параграфи відбувається автоматично, адреса електронної пошти ніколи не буде опублікований, допустимий HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*

*