Здравствуй, хабр! Во второй части статью я продолжу рассказ о том, как я писал клон игры Pacman. Первую часть можно почитать здесь.
С момента, когда я последний раз работал над пакманом прошло порядка трех недель. Прошла большая часть сессии, стало немного больше времени и я решил продолжить. В этот момент появилось желание доделать игру до состояния, когда ее можно будет выложить в Google Play Market, хотя в самом начале разработки я об этом даже не помышлял. Кроме того, доделывание до играбельного состояния – неплохая тренировка. Где-то я слышал, что игры (да и вообще приложения) стоит доделывать.
Напомню, что разработка игры велась с использованием Android NDK (С++) и OpenGL ES 2.0.
Для начала я составил список того, что, как я считал, необходимо для окончания работы над игрой:
- Бонусы
- Вывод текста
- Музыка и звуки
- Перманентное сохранение данных
- Более красивая анимация и дизайн
Теперь подробнее, по пунктам:
Бонусы
Бонусы в игре нужны для разнообразия. Чтобы не тратить на них много времени, я ввел новый абстрактный класс Bonus
, от которого тут же унаследовал LifeBonus
. Как нетрудно догадаться, LifeBonus
дает игру одну жизнь. Надо сказать, бонусы весьма органично вписались в уже существующую иерархию:
На этом я пока остановился. Создать другие бонусы крайне легко, стоит лишь унаследовать их от Bonus’a.
В связи с бонусами стоит упомянуть класс Statistics
. Этот класс нужен для сбора различной статистики, такой как вход/выход/пауза уровня, подсчет набранных очков и времени внутри уровня. Вся эта статистика собирается и может быть использована для создания таблицы достижений или даже сетевых таблиц рекордов. Внутри класс Statistics
реализован в виде детерминированного конечного автомата.
Вывод текста
Сначала я хотел обойтись без текстовой информации вовсе, потому что (ИМХО) встраивание текста влечет за собой костыли. Оказалось, что обходиться без текста сложно, проще было реализовать его вывод.
Для вывода текста я воспользовался простым приемом: графическое представление символов моноширинного шрифта берется прямоугольниками из текстуры примерно такого вида, как на рисунке.
Первый символ – пробел, остальные идут подряд. Разлиновка на рисунке нужна лишь для удобства (видно базовую линию и то, что все символы выравнены). В приложении текстура такая же, но с прозрачным фоном. Правильнее было бы рендерить шрифт в текстуру на этапе выполнения, а не хранить статичную текстуру, но это только добавило бы сложности, т.к. непонятно, как выравнивать символы в прямоугольниках.
Для вывода текста разработан специальный элемент GUI — Label
, наследник Control
’a. Он используется в заголовке окна игры для вывода игровой статистики, в меню Win/GameOver для оповещения игрока о выигрыше или проигрыше соответственно.
Звук
Редкая игра обходится без звука (пожалуй, я с ходу не смогу назвать таких игр). Поэтому я решил добавить фоновую музыку и игровые звуки в свою игру тоже.
Техническая часть
До этого у меня не было опыта работы со звуком. Здесь есть как минимум 3 варианта:
- Использовать jni и проигрывать звуки, используя API, предоставляемые Android SDK
- Использовать OpenSL ES
- Использовать OpenAL
Первый вариант я отбросил сразу, поскольку посчитал, что это не совсем изящное решение. Выбор из двух оставшихся был сделан в пользу OpenSL ES (об этом я написал статью, заработав тем самым инвайт сюда).
Для работы с музыкой разработан класс Audio
, который имеет набор статических методов для включения той или иной фоновой музыки, быстрого проигрывания звуков и управления слышимостью музыки и звуков (по отдельности друг от друга).
Пользователь осуществляет управление из главного меню игры, в котором для этого есть подобия кнопок с состояниями – CheckBox
, который унаследован от Control
’a.
Композиторская часть
Сначала я хотел выбрать музыку и звуки из имеющихся в открытом доступе на огромном количестве музыкальных сайтов. Но эта затея провалилась, поскольку подобрать музыку оказалось проблематично для меня.
К счастью, ко мне на помощь пришел мой друг-музыкант Тимур Рамазанов, который согласился написать для меня треки. Лично мне музыка кажется очень подходящей к дизайну и настроению игры. Те, кому интересны другие его работы, могут ознакомиться с ними вконтакте или на soundcloud
Фоновая музыка разделена на две части: игровая и в меню. Она зациклена и сохранена в формате ogg. Игровые звуки сохранены в формате wav.
Сохранение информации
В процессе игры различная информация должна быть сохранена перманентно. Это, например, рекорды игрока или его настройки звука.
Для этого написана обертка над android.content.SharedPreferences. Обращение к обертке происходит через jni.
public class StoreManager { public static final String PACMAN_PREFERENCES = "com_zagayevskiy_pacman_store"; private Context context; /*Сохраним ссылку на контекст*/ public StoreManager(Context _context){ context = _context; } /*Методы для сохранения и загрузки целых чисел и булевых величин. При желании можно расширить и другими типами*/ public void saveBoolean(String key, boolean value){ SharedPreferences sp = context.getSharedPreferences(PACMAN_PREFERENCES, Context.MODE_PRIVATE); SharedPreferences.Editor editor = sp.edit(); editor.putBoolean(key, value); editor.commit(); } public boolean loadBoolean(String key, boolean defValue){ SharedPreferences sp = context.getSharedPreferences(PACMAN_PREFERENCES, Context.MODE_PRIVATE); return sp.getBoolean(key, defValue); } public void saveInt(String key, int value){ SharedPreferences sp = context.getSharedPreferences(PACMAN_PREFERENCES, Context.MODE_PRIVATE); SharedPreferences.Editor editor = sp.edit(); editor.putInt(key, value); editor.commit(); } public int loadInt(String key, int defValue){ SharedPreferences sp = context.getSharedPreferences(PACMAN_PREFERENCES, Context.MODE_PRIVATE); return sp.getInt(key, defValue); } }
#include <stdlib.h> #include <stdio.h> #include <jni.h> class Store { public: static void init(JNIEnv* env, jobject _storeManager); static void saveBool(const char* name, bool value); static bool loadBool(const char* name, bool defValue); static void saveInt(const char* name, int value); static int loadInt(const char* name, int defValue); private: static JavaVM* javaVM; static jobject storeManager; static jclass storeManagerClass; static jmethodID saveBoolId; static jmethodID loadBoolId; static jmethodID saveIntId; static jmethodID loadIntId; static JNIEnv* getJNIEnv(JavaVM* jvm); };
Store.cpp:
/*env и _storeManager передаются при инициализации нативной библиотеки*/ void Store::init(JNIEnv* env, jobject _storeManager){ /*Сохраним ссылку на Java-машину, понадобится позже*/ if(env->GetJavaVM(&javaVM) != JNI_OK){ LOGE("Can not Get JVM"); return; } storeManager = env->NewGlobalRef(_storeManager); if(!storeManager){ LOGE("Can not create NewGlobalRef on storeManager"); return; } storeManagerClass = env->GetObjectClass(storeManager); if(!storeManagerClass){ LOGE("Can not get StoreManager class"); return; } saveBoolId = env->GetMethodID(storeManagerClass, "saveBoolean", "(Ljava/lang/String;Z)V"); if(!saveBoolId){ LOGE("Can not find method saveBoolean"); return; } /*Аналогично для остальных методов*/ } } void Store::saveBool(const char* name, bool value){ LOGI("Store::saveBool(%s, %d)", name, value); JNIEnv* env = getJNIEnv(javaVM); if(!env){ LOGE("Can not getJNIEnv"); return; } jstring key = env->NewStringUTF(name); if(!key){ LOGE("Can not create NewStringUTF"); } env->CallVoidMethod(storeManager, saveBoolId, key, value); } bool Store::loadBool(const char* name, bool defValue){ LOGI("Store::loadBool(%s, %d)", name, defValue); JNIEnv* env = getJNIEnv(javaVM); if(!env){ LOGE("Can not getJNIEnv"); return defValue; } jstring key = env->NewStringUTF(name); if(!key){ LOGE("Can not create NewStringUTF"); } return env->CallBooleanMethod(storeManager, loadBoolId, key, defValue); } /*Аналогично реализуются оставшиеся два метода load/saveInt()*/ /*Получаем указатель на JNIEnv для текущего потока, используя ссылку на Java-машину*/ JNIEnv* Store::getJNIEnv(JavaVM* jvm){ JavaVMAttachArgs args; args.version = JNI_VERSION_1_6; args.name = "PacmanNativeThread"; args.group = NULL; JNIEnv* result; if(jvm->AttachCurrentThread(&result, &args) != JNI_OK){ result = NULL; } return result; }
Более красивая анимация и дизайн
Первоначально анимировался у меня только Pacman. Хотелось сделать анимацию более красивой (а не в 4 кадра), и сделать анимацию для бонусов и врагов. Все это в одном стиле.
В какой-то момент возникла идея сделать Pacman’a в виде огненного шара, а его врагов – в виде капель воды.
Самый идеальный вариант для меня был – сделать красивую покадровую анимацию. Проблем в программном плане это не представляет, но зато есть проблема рисования кадров. Я столкнулся с проблемой поиска дизайнера и объяснения, что именно я хочу. Эту проблему я не решил. Потом некоторое время подумал и решил сделать полностью программную анимацию. А у дизайнера заказал только тайлы разных размеров, что обошлось мне в $50.
Программная анимация
Для того, чтобы сделать анимацию удобной в использовании, я реализовал два класса-наследника уже упоминавшегося выше IRenderable
: Plume
для анимации «шлейфа» и Pulsation
для «пульcаций».
На скриншоте шлейфы различной длины имеют персонажи – Pacman и монстры, а пульсация – это точка большего размера в центре сердца. Так показана на карте дополнительная жизнь.
Идея обоих классов основана на эффекте «кисти». На каждом шаге объект класса Plume
получает координаты анимируемого объекта и запоминает (или не запоминает, в зависимости от желаемой длины шлейфа – чем чаще запоминания, тем короче шлейф) их в контейнер-очередь. Затем, используя уже запомненные координаты, рисуются круги с помощью текстуры, аналогичной представленной ниже.
Зелено-черный градиент соответствует градиенту альфа-канала. Зеленый — полная непрозрачность, черный – полная прозрачность. Эта текстура генерируется при инициализации игры при помощи фрагментного шейдера и рендера в текстуру.
Чем старше координаты, тем меньший радиус рисуемого круга. Круги рисуются с наложением текстуры, указанной при создании объекта-шлейфа. Текстурные координаты при этом смещаются в зависимости от рисуемых координат и, дополнительно, по формуле спирали Архимеда (для того, чтобы при остановке персонажей анимация не застывала).
Для анимации Pacman’a и монстров используются шлейфы разной длины, с разными текстурами. Дополнительное требование к текстурам воды и пламени — они должны быть «зациклены», т.е. не должно быть видно стыков. Сам Pacman так же использует покадровую анимацию движения челюстей.
Аналогичным образом реализована пульсация, в которой градиентные круги различных размеров просто сменяют друг друга с определенной частотой.
Название и иконка приложения
При выборе названия хотелось обыграть то, что игра – клон Pacman’a, причем Pacman – огненный. При этом надо было не обидеть Namco. Были различные варианты: Fireman, Fire Man, Pyro Man, Pacman: Jaws of Fire. В итоге я остановился на Pyroman: Jaws of Fire. А отсылку к игре Pac-Man оставил в описании.
Иконку приложения нарисовал в фотошопе, обыграв огненность Pacman’a. Получилось похоже на золотую рыбку и, по-моему, забавно=)
Так же хотелось рассказать об участии в прошедшем конкурсе The Tactrick Android Developer Cup, в номинации «Games». Но, по сути, рассказать нечего, так как конкурс кончился внезапно — вывешиванием плашек «WINNERS» победителям и письмом «Спасибо за участие» остальным. Я не претендовал на какие-либо призовые места, но интересно было, на каком месте в зачете буду. Пусть 66 из 66, но будет понятно, что как-то программы оценивали.
Игра доступна на github.
Благодарности
Хочу сказать спасибо моей девушке Юле, за понимание и поддержку. В её честь нарисован первый уровень
Так же хочу поблагодарить моего гуру и наставника — Булата Танирбергена за дружескую поддержку и убеждение, что всё в моих силах
Рамазанову Тимуру за треки к игре — спасибо.
Отдельные благодарности компании ZeptoLab, благодаря которой я теперь достаточно хорошо знаю Android NDK, прочитал книгу Сильвена Ретабоуила о NDK и книгу Стефана Дьюхерста «Скользкие места С++», таким образом подняв свой программистский уровень.
ссылка на оригинал статьи http://habrahabr.ru/post/178523/
Добавить комментарий