Pull to refresh

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

Reading time8 min
Views44K

Здравствуй, хабр! Во второй части статьи я продолжу рассказ о том, как я писал клон игры 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 и книгу Стефана Дьюхерста «Скользкие места С++», таким образом подняв свой программистский уровень.
Tags:
Hubs:
+92
Comments19

Articles

Change theme settings