Слышали ли вы когда-нибудь про Вампуса? Независимо от ответа — добро пожаловать в его владения!
В этой статье я хочу поведать вам свою историю создания игры под Android. В зависимости от компетенции читателя передаваемые мною опыт, мысли и решения будут более или менее полезными. Однако я надеюсь, что мой рассказ, как минимум, будет небезынтересным.
Содержание
1. Введение
2. Выбор средств
3. Идея проекта
4. I SMELL A WUMPUS
5. Основа основ – структура проекта
6. Генерация лабиринта Вампуса и работа с ним
7. Хранение игровых сообщений и вывод их игроку
8. Первый результат
9. Даже маленькие игры достойны истории
10. Преображение Вампуса и конечный результат
11. Вывод
1. Введение
Для начала несколько слов о себе. Программирование, к сожалению, не является основным моим видом деятельности, но я с удовольствием посвящаю ему своё свободное время.
По некоторым причинам я выбрал путь создания мобильных игр. Мои предыдущие проекты для мобильных устройств создавались в среде Qt в связке с языками QML и C++. От этой тройки я получал большое удовольствие, однако, анализирую свои идеи я понял, что в будущем решение некоторых задач известными мне средствами потребует слишком много времени и сил. Поэтому, обдумывая следующий проект, я решил найти новые, более подходящие инструменты для разработки и получить опыт работы с ними.
2. Выбор средств
Ранее я уделял внимание лишь Android и в новом проекте я решил сконцентрироваться на этой ОС, познакомиться с «родной» для неё Android Studio, попробовать новый для себя Java (а в будущем, если понравится, ещё и перспективный Kotlin).
Ни AS, ни Java ранее я не использовал и передо мной предстал гигантский фронт работы с множеством новых задач, и я уже готов был ринуться в бой, осталось лишь придумать проект.
3. Идея проекта
Известно, что лучше всего обучение происходит на реальных задачах, а в особенности тех, что вызывают интерес. Для меня таким учебным проектом должна была стать игра, для которой я сформировал ряд требований:
- Реиграбельность.
- Простота игровой механики.
- Минимальное использование графики.
- Игровой процесс должен вынуждать игрока размышлять.
- Игровая партия не должна быть продолжительной.
- Быстрая реализация проекта (2 месяца).
- Простота и лёгкость UI.
Перебрав множество вариантов и объективно оценивая свои силы, помня при этом, что сколько бы не закладывал времени и ресурсов вначале, в действительности потребуется много больше, я пришёл к мысли о том, что в качестве обучения лучше всего взять за основу проверенную классику, нежели изобретать что-то своё. Создавать условную змейку мне совершенно не хотелось, и я стал изучать старые игры. Так мною была обнаружена любопытнейшая Hunt the Wumpus (Охота на Вампуса).
4. I SMELL A WUMPUS
Охота на Вампуса – классическая текстовая игра, придуманная Gregory Yob в 1972. В том же году в журнальной статье им были даны описание игры и исходный код.
Суть игры в исследовании игроком лабиринта-додекаэдра, являющегося жилищем злобного Вампуса, и попытках угадать, на основе сообщений-индикаторов, выводящихся в игровой лог, что находится в комнатах-вершинах. Помимо самого Вампуса (издаёт неприятный запах) имеются летучие мыши (доносится шум), переносящие игрока в случайную комнату, и ямы (сквозит), попадание в которые приводит к завершению игры. Целью же игры является убийство Вампуса для чего у игрока есть 5 стрел, которые могут пролетать от 1 до 5 комнат за раз (игрок сам решает какую «силу» выстрела сделать). Таким образом, игроку доступно две действия: выстрелить из лука, перейти в комнату. Каков же будет результат зависит от доли везения и степени информированности.
В общем, механика мне понравилась: она простая, но в тоже время с элементами риска. В Google Play про Вампуса интересных игр не было (кроме свежей на тот момент игры по миру Лавкрафта, в которой, как я позже узнал, в основе лежала-таки механика Вампуса. А вот это статья про создание игры на Хабре), поэтому было принято решение взять именно Вампуса за основу. Целью я поставил сохранить классическую игру, но слегка её обновить и добавить новые функции.
5. Основа основ – структура проекта
Первым делом я изучил правила классической игры и познакомился с различными реализациями Вампуса. После чего я составил схему с логикой игры:
На первых порах схема была полезна, т.к. позволяла проанализировать механику игры, устранить изъяны и внести что-то своё. Более того эта схема пригодилась, когда впоследствии я работал с художницей, чтобы объяснить суть игры.
Проект я разбил на 4 части, в каждой из которых решались разные задачи. Я приведу лишь некоторые из них.
1. Игровая механика
- В каком виде будет храниться информация о подземелье?
- Какого типа и сколько переменных нужно?
- Написание алгоритмов: формирования подземелья, полёта стрелы, проверки результата стрельбы, полёта стрелы при неправильно набранной последовательности комнат, перемещения игрока, проверки комнаты при перемещении, перемещения игрока летучими мышами и т.д.
- Как выводить информацию в игровой лог и в каком порядке?
- В какой последовательности проводить проверку комнаты?
2. UI
- Какие активити должны быть в приложении? Как должны выглядеть и какие элементы должны быть на них?
- Какие параметры позволить изменять в настройках?
- Нужны ли изображения в игре?
- Какая, в целом, должна быть стилистика приложения (цвета, настроение, стиль сообщений)?
- Какие шрифты использовать?
3. Прочее
- Подключение к Google play services
- Работа с XML файлами
- Какие шрифты использовать?
4. Написание текста для игры
- Игровые сообщения
- Правила
- Описание игры для Google Play
Скорее ненужно, нежели невозможно, описывать всё, поэтому я остановлюсь лишь на некоторых моментах, после чего покажу первый полученный результат.
6. Генерация лабиринта Вампуса и работа с ним
Лабиринт Вампуса – додекаэдр, который можно представить в виде матрицы G размерностью 20х20. Вершины пронумеруем от 0 до 19. Если элемент матрицы равен 1 – между вершинами (комнатами) есть проход, иначе – нет.
Так же введём матрицу N размерностью 20х3, хранящую индексы соседей для каждой комнаты. Эта матрица ускорит работу с G.
Матрицы G и N вшиты в код игры и не изменяются (разумеется, хранение G излишне, т.к. можно работать только с N, но сейчас оставим всё так). Эти «истинные» индексы вершин раз и навсегда заданного додекаэдра. Для игрока же формируются «игровые» индексы, являющиеся своего рода маской «истинных», в вектор V размерностью 20 следующим образом:
// обнуляем "игровой" вектор перед игрой
for (byte i = 0; i < 20; i++) {
V[i] = i;
}
// перемешиваем индексы в "игровом" векторе
for (int i = 0; i < 20; i++) {
int tmpRand = random.nextInt(20);
byte tmpVar = V[i];
V[i] = V[tmpRand];
V[tmpRand] = tmpVar;
}
Таким образом, получается следующая картина:
Вектор V формируется каждую новую игру, что даёт игроку «новое» подземелье.
Для установления соответствия между «истинным» и «игровым» индексом комнаты используется метод преобразования indByNmb:
public byte indByNmb(int room) {
byte ind = -1;
for (byte i = 0; i < V.length; i++) {
if (V[i] == room) {
ind = i;
break;
}
}
return ind;
}
На входе метод indByNmb получает «игровой» индекс комнаты room, а на выходе даёт «истинный» ind.
После генерации структуры подземелья размещаем: 2 стаи летучих мышей, 2 ямы, Вампуса и игрока:
byte[] randomRooms = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19};
for (int i = 0; i < 20; i++) {
int tmpRand = random.nextInt(20);
byte tmpVar = randomRooms[i];
randomRooms[i] = randomRooms[tmpRand];
randomRooms[tmpRand] = tmpVar;
}
P = randomRooms[0];
W = randomRooms[1];
Pits[0] = randomRooms[2];
Pits[1] = randomRooms[3];
Bats[0] = randomRooms[4];
Bats[1] = randomRooms[5];
Подобное размещение гарантирует, что в одной комнате не будет двух обитателей, а игрок не будет с самого начала закинут в комнату к Вампусу.
Полная генерация подземелья выглядит следующим образом:
byte[] V = new byte[20]; // "игровой" лабиринт
int P; // Положение игрока,
byte W; // Положение Вампуса
byte[] Bats = new byte[2]; // Комнаты с летучими мышами,
byte[] Pits = new byte[2]; // Комнаты с ямами
public void generateDungeons() {
resetVars(); // этот метод обнуляет все данные
for (int i = 0; i < 20; i++) {
int tmpRand = random.nextInt(20);
byte tmpVar = V[i];
V[i] = V[tmpRand];
V[tmpRand] = tmpVar;
}
byte[] randomRooms = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19};
for (int i = 0; i < 20; i++) {
int tmpRand = random.nextInt(20);
byte tmpVar = randomRooms[i];
randomRooms[i] = randomRooms[tmpRand];
randomRooms[tmpRand] = tmpVar;
}
P = randomRooms[0];
W = randomRooms[1];
Pits[0] = randomRooms[2];
Pits[1] = randomRooms[3];
Bats[0] = randomRooms[4];
Bats[1] = randomRooms[5];
}
Теперь можно реализовывать все алгоритмы игровой механики. Так, например, происходит вывод соседних комнат при попадании игрока в комнату с индексом currentRoom:
public void printNearRooms(byte currentRoom) {
byte ind = indByNmb(currentRoom);
appendText(V[N[ind][0]], V[N[ind][1]], V[N[ind][2]]);
}
На входе метод printNearRooms получает текущий «игровой» индекс комнаты currentRoom.
Рассмотрим механику на примере. Пусть игрок перешёл в новую комнату и появилось сообщение: «Теперь я в комнате 8». Число 8 это «игровой» индекс. «Истинный» же индекс комнаты — 6 (см. скриншоты выше). В коде ведётся работа именно с «истинным» индексом, т.е. 6. Для 6 определяются индексы «истинных» соседей: 2, 5, 7. «Игровыми» же, соответственно, будут: 10, 0, 7. Игроку показываем в логе: «Я могу перейти в комнаты 10, 0, 7».
Таким образом, формируя каждую новую игру вектор V и работая с «истинными» и «игровыми» индексами графа лабиринта, создаётся видимость того, что каждая игра уникальна.
Благодаря функции appendText сообщения выводятся через заданный интервал. С ней мы познакомимся позже.
А вот пример проверки комнаты на близость мышей:
public boolean isBatsNear() {
boolean answer = false;
byte indP = indByNmb(P);
for (int i = 0; i < 3; i++) {
if ((V[N[indP][i]] == Bats[0]) || (V[N[indP][i]] == Bats[1])) {
answer = true;
break;
}
}
return answer;
}
7. Хранение игровых сообщений и вывод их игроку
Классическая игра представляла из себя процесс взаимодействия игрока с консолью, т.е. игра была чисто текстовой. Я эту особенность хотел сохранить для чего все игровые сообщения разбил на блоки, например:
- Первое сообщение новой игры.
- Сообщение при перемещении игроком.
- Сообщение при близости ям.
- Сообщение при перемещении Вампуса.
- Сообщение при попадании в яму.
Текст хранится в XML файле. Каждый блок имеет несколько вариантов сообщения в угоду разнообразия геймплея.
Пример блока сообщения, которое выводится при наличии в одной из соседних комнат ямы:
<string name="g_pitsNear_1">— Чувствую сквознякn</string>
<string name="g_pitsNear_2">— Из соседней комнаты дуетn</string>
<string name="g_pitsNear_3">— Ощутил дуновение на своём лицеn</string>
<string name="g_pitsNear_4">— Ногам холодно, сквозитn</string>
<string name="g_pitsNear_5">— А здесь сквозитn</string>
<string-array name="g_pitsNear">
<item>@string/g_pitsNear_1</item>
<item>@string/g_pitsNear_2</item>
<item>@string/g_pitsNear_3</item>
<item>@string/g_pitsNear_4</item>
<item>@string/g_pitsNear_5</item>
</string-array>
При такой структуре можно легко редактировать имеющиеся либо добавлять новые сообщения, не затрагивая при этом Java код.
Если, к примеру, при проверке комнаты показанный ранее метод isBatsNear вернул true, то достанем из XML нужный блок сообщений, а затем случайным образом возьмём одно в качестве аргумента для appendText:
if (isBatsNear()) {
String[] g_batsNear = getResources().getStringArray(R.array.g_batsNear);
appendText(g_batsNear[random.nextInt(g_batsNear.length)]);
}
Вывод игровых сообщений производится в консоль, которая является объектом TextView. Давайте посмотрим на метод appendText.
public void appendText(final String str) {
msgBuffer.add(str);
if (!isTimerGameMsgWork) {
mTimerGameMsg.run();
isTimerGameMsgWork = true;
}
}
Когда возникает необходимость вывести игровое сообщение, то вызывается метод appendText, принимающий его в качестве аргумента. В самом методе сначала происходит добавление строки в буфер msgBuffer. Затем следует проверка булевой переменной isTimerGameMsgWork. Она принимает true в случаях, когда запущен таймер mTimerGameMsg. Когда работает этот таймер, то из буфера msgBuffer по принципу FIFO (First In First Out) достаются с заданным интервалом mIntervalGameMsg сообщения и добавляются в игровой лог — txtViewGameLog.
Код вывода сообщений целиком:
ArrayList<String> msgBuffer = new ArrayList<>();
Handler mHandlerGameMsg;
private int mIntervalGameMsg = 1000;
boolean isTimerGameMsgWork = false;
public void appendText(final String str) {
msgBuffer.add(str);
if (!isTimerGameMsgWork) {
mTimerGameMsg.run();
isTimerGameMsgWork = true;
}
}
final Runnable mTimerGameMsg = new Runnable() {
@Override
public void run() {
if (msgBuffer.size() == 0) {
mHandlerGameMsg.removeCallbacks(mTimerGameMsg);
isTimerGameMsgWork = false;
} else {
txtViewGameLog.append(msgBuffer.get(0));
msgBuffer.remove(0);
mHandlerGameMsg.postDelayed(mTimerGameMsg, mIntervalGameMsg);
}
}
};
8. Первый результат
Спустя месяц разработки была получена первая играбельная версия игры с полностью реализованным функционалом. Скриншоты прилагаю:
На представленных скриншотах можно увидеть: главное меню, окно с правилами, окно с настройками, игровое окно.
Разумеется, я понимал, что результат вышел совсем неинтересный. Главное же то, что я получил практический опыт по AS и Java, что и было первозадачей.
Игрок, как и в классической игре, должен был взаимодействовать через консоль: вводить номер комнаты для перемещения либо маршрут для полёта стрелы. В настройки я сделал возможным менять размер шрифта и прозрачность подложки. Предполагал, что для каждого игрового окна будет своя картинка-подложка (чтобы немного разнообразить геймплей, ха!).
Далее я планировал заменить имеющиеся картинки (которые я бессовестно взял из интернета) на те, что нарисует художник. Потом я бы выпустил игру в Play market и благополучно забыл бы про неё, применяя полученный опыт уже к новым проектам. И я не мог тогда предположить, как сильно может измениться Вампус…
9. Даже маленькие игры достойны истории
Когда человек подходит к работе с душой, то от этого проект только выигрывает. Мне повезло, что художница, Анастасия Фроликова, оказалась именно таким человеком. Т.е. вместо того, чтобы просто нарисовать то, что требовалось мне, она заинтересовалась миром игры и захотела понять, как он устроен. И вдруг оказалось, что никакого мира, по большому счёту, нет! Кто такой этот Вампус? И почему игрок должен его убить? Как выглядят комнаты Вампуса? И прочее, прочее над чем я не думал и что не планировал рассказывать игроку. В результате мы сошлись на том, что даже у такой, казалось бы, маленькой игры должна быть своя история. И она появилась.
Согласно нашей легенде Вампус хоть и древнее, но не злое мифическое существо, любящее подшучивать над людьми. Да, он живет в лабиринте, но этот лабиринт не в виде классического мрачного подземелья, а в виде невообразимого дома, состоящего из нагромождения комнат, содержание которых характеризует Вампуса. Так, например, в одной из комнат расположился кинотеатр, на стенах которого постеры его любимых фильмов, а в другой находится его каморка, где он готовит свои «розыгрыши».
Игрок из безымянного охотника превратился в завсегдатая криптозоологического форума, который хочет доказать существование Вампуса. Мы заменили классические лук и стрелы на фотоаппарат и плёнку, а целью игры стало не убийство Вампуса, а получение его фотографии. К слову, главное меню было переделано под форум, где люди обсуждают Вампуса, а из обсуждения игрок может узнать про него.
Что касаемо остальных аспектов, то они остались практически без изменений: мыши действуют так же, а попадание в яму стало приводить к падению, разбитию камеры и завершению игры (а не смерти игрока).
Ещё момент про камеру. В классической игре игрок мог пустить стрелу на дальность от 1 до 5 комнат и это выглядело логично. У нас же вместо лука камера (фотографирующая от 1 до 3 комнат за раз, но работающая как классическая стрела, поражающая Вампуса). И это… выглядит странно, не находите? Была идея уменьшить дальность камеры до 1 комнаты, чтобы фотографировать можно было только соседнюю, но это, во-первых, усложнит игру, а, во-вторых, могут быть получены такие ситуации, когда игра не может быть выиграна, что неправильно. В общем, это тот момент, который лично мне не даёт покоя до сих пор, а решения я пока не нашёл.
Что касаемо стиля стиля и настроения игры. Практически все игры про Вампуса выполнены в скучных серых тонах, а действия происходят в тёмных локациях подземелий. Поэтому мы решили, что наша игра должна отличаться от всего этого и быть выполнена с юмором и в ярких красках.
10. Преображение Вампуса и конечный результат
Дальше нас ждали ещё 2 месяца работы над игрой. Так как у Васпуса 20 комнат, то для каждой был создан свой интерьер. Помимо этого, были нарисованы иконки достижений, иконки в игре, приняты решения по дизайну в целом и UI. Так же был дописан весь игровой текст, дополнен и оптимизирован код, были добавлены новые функции (например, появился блокнот для записей информации по ходу игры). В общем, Вампус подвергся серьёзным изменениям.
Комнаты, например, создавались следующим образом: (скриншот кликабелен):
Комнат 20, все они уникальны, а игрок каждую игру получает «новый» лабиринт. Как сделать так, чтобы каждую новую игру картинки привязывались к новым комнатам? Самое простое, это использовать тот же подход «истинных» и «игровых» индексов:
public void changeImgOfRoom() {
ImageView img = findViewById(R.id.imgRoom);
int ind = indByNmb(P);
String imgName = "room_" + ind;
int id = getResources().getIdentifier(imgName, "drawable", this.getPackageName());
Glide.with(this)
.load(id)
.transition(DrawableTransitionOptions.withCrossFade())
.into(img);
}
Картинки квадратного формата (для уменьшения искажения при просмотрах на разных экранах) хранятся в ресурсах с названиями [room_0; room_1; ..., room_19]. И они, фактически, связаны с «истинными» индексами додекаэдра, но для игрока каждую новую игру для одной и той же комнаты будут разные картинки. Зачем это нужно? Для того, чтобы дать возможность в конкретной игровой партии соотнести текстовую информацию с изображением конкретной комнаты («ага, помню, что в комнате Х, которая гостиная, был сквозняк») и чтобы не получалось так, что «а почему у меня всегда в комнате Х одна и также картинка?». Всё для разнообразия и помощи игроку (впрочем, как показал опыт, помощи от визуального запоминания нет, эффективнее работать с текстом).
В конечном счёте мы получили новую версию игры. И, знаете что? Вампус стал чертовски привлекателен, а самое главное, это всё тот же классический Вампус, но в новом уютном доме!
На скриншотах: главное меню, окно с правилами, окно с настройками, игровое окно.
Что касаемо механики, то она лишь слегка изменена и переименована (ну в самом деле, есть ли разница: камера или лук, если делать нужно одно и тоже?). Самое заметное изменение в механике — это упрощение процесса взаимодействия между игроком и игрой – был убран классический ввод номеров комнат при помощи клавиатуры. Теперь для перехода между комнатами нужно выбрать во всплывающем окошке 1 из 3 чисел, а для формирования «маршрута» фотографирования достаточно прокрутить колёса на барабане:
На видео ниже вы можете увидеть конечный результат:
11. Вывод
Первая версия игры была получена мною за месяц разработки, выделяя по 1-2 часа времени после работы. При этом ни AS, ни Java не были мне ранее знакомы. Вторая версия игры потребовала ещё 2 месяца. Таким образом, всего 3 месяца неспешной работы.
Конечно, в таком виде игра не для широкого круга, т.к. она может показаться сложной и не захватывающей современному игроку, но важнее, наверное, сохранение духа классических игр, не находите?
Доволен ли я результатом? Однозначно, да. Я получил большой опыт как программирования, так и работы в команде. Мне нравится, как получившаяся механика игры, так и визуальная её составляющая. Есть ли что-то, что бы мне хотелось изменить? Разумеется, нет пределов совершенства и всегда можно что-то добавить/улучшить, но нельзя же этим заниматься вечно!
Интересна ли эта игра? Что ж, тут уж решать не мне. Но пусть Вампусу будет уютно в том доме, что мы для него выстроили с большой любовью и вниманием.
Желаю Вам успехов!
Спасибо за внимание и берегитесь Вампуса!
P.S. Постоянно возникающие задачи и радость от их решения — это именно то, за что я люблю программирование. Надеюсь, что и вы получаете не меньшее удовольствие.
Автор: Олег