Как я писал Pacman’a и что из этого получилось. Часть 2

от автора


Здравствуй, хабр! Во второй части статью я продолжу рассказ о том, как я писал клон игры 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); 	} } 

С++ код для обращения к StoreManager через jni

Store.h:

#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/