Hunt the Wumpus или опыт написания классической игры для Android

в 7:38, , рубрики: android, android studio, java, Дизайн игр, логические игры, Программирование, разработка игр, разработка мобильных приложений, Разработка под android

image

Слышали ли вы когда-нибудь про Вампуса? Независимо от ответа — добро пожаловать в его владения!

В этой статье я хочу поведать вам свою историю создания игры под 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. Идея проекта

Содержание

Известно, что лучше всего обучение происходит на реальных задачах, а в особенности тех, что вызывают интерес. Для меня таким учебным проектом должна была стать игра, для которой я сформировал ряд требований:

  1. Реиграбельность.
  2. Простота игровой механики.
  3. Минимальное использование графики.
  4. Игровой процесс должен вынуждать игрока размышлять.
  5. Игровая партия не должна быть продолжительной.
  6. Быстрая реализация проекта (2 месяца).
  7. Простота и лёгкость UI.

Перебрав множество вариантов и объективно оценивая свои силы, помня при этом, что сколько бы не закладывал времени и ресурсов вначале, в действительности потребуется много больше, я пришёл к мысли о том, что в качестве обучения лучше всего взять за основу проверенную классику, нежели изобретать что-то своё. Создавать условную змейку мне совершенно не хотелось, и я стал изучать старые игры. Так мною была обнаружена любопытнейшая Hunt the Wumpus (Охота на Вампуса).

4. I SMELL A WUMPUS

Содержание

image

Охота на Вампуса – классическая текстовая игра, придуманная Gregory Yob в 1972. В том же году в журнальной статье им были даны описание игры и исходный код.

Суть игры в исследовании игроком лабиринта-додекаэдра, являющегося жилищем злобного Вампуса, и попытках угадать, на основе сообщений-индикаторов, выводящихся в игровой лог, что находится в комнатах-вершинах. Помимо самого Вампуса (издаёт неприятный запах) имеются летучие мыши (доносится шум), переносящие игрока в случайную комнату, и ямы (сквозит), попадание в которые приводит к завершению игры. Целью же игры является убийство Вампуса для чего у игрока есть 5 стрел, которые могут пролетать от 1 до 5 комнат за раз (игрок сам решает какую «силу» выстрела сделать). Таким образом, игроку доступно две действия: выстрелить из лука, перейти в комнату. Каков же будет результат зависит от доли везения и степени информированности.

В общем, механика мне понравилась: она простая, но в тоже время с элементами риска. В Google Play про Вампуса интересных игр не было (кроме свежей на тот момент игры по миру Лавкрафта, в которой, как я позже узнал, в основе лежала-таки механика Вампуса. А вот это статья про создание игры на Хабре), поэтому было принято решение взять именно Вампуса за основу. Целью я поставил сохранить классическую игру, но слегка её обновить и добавить новые функции.

5. Основа основ – структура проекта

Содержание

Первым делом я изучил правила классической игры и познакомился с различными реализациями Вампуса. После чего я составил схему с логикой игры:

Схема (кликабельна)

Hunt the Wumpus или опыт написания классической игры для Android - 3

На первых порах схема была полезна, т.к. позволяла проанализировать механику игры, устранить изъяны и внести что-то своё. Более того эта схема пригодилась, когда впоследствии я работал с художницей, чтобы объяснить суть игры.

Проект я разбил на 4 части, в каждой из которых решались разные задачи. Я приведу лишь некоторые из них.

1. Игровая механика

  • В каком виде будет храниться информация о подземелье?
  • Какого типа и сколько переменных нужно?
  • Написание алгоритмов: формирования подземелья, полёта стрелы, проверки результата стрельбы, полёта стрелы при неправильно набранной последовательности комнат, перемещения игрока, проверки комнаты при перемещении, перемещения игрока летучими мышами и т.д.
  • Как выводить информацию в игровой лог и в каком порядке?
  • В какой последовательности проводить проверку комнаты?

2. UI

  • Какие активити должны быть в приложении? Как должны выглядеть и какие элементы должны быть на них?
  • Какие параметры позволить изменять в настройках?
  • Нужны ли изображения в игре?
  • Какая, в целом, должна быть стилистика приложения (цвета, настроение, стиль сообщений)?
  • Какие шрифты использовать?

3. Прочее

  • Подключение к Google play services
  • Работа с XML файлами
  • Какие шрифты использовать?

4. Написание текста для игры

  • Игровые сообщения
  • Правила
  • Описание игры для Google Play

Скорее ненужно, нежели невозможно, описывать всё, поэтому я остановлюсь лишь на некоторых моментах, после чего покажу первый полученный результат.

6. Генерация лабиринта Вампуса и работа с ним

Содержание

Лабиринт Вампуса – додекаэдр, который можно представить в виде матрицы G размерностью 20х20. Вершины пронумеруем от 0 до 19. Если элемент матрицы равен 1 – между вершинами (комнатами) есть проход, иначе – нет.

Так же введём матрицу N размерностью 20х3, хранящую индексы соседей для каждой комнаты. Эта матрица ускорит работу с G.

Hunt the Wumpus или опыт написания классической игры для Android - 4

Матрицы 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;
}

Таким образом, получается следующая картина:

Hunt the Wumpus или опыт написания классической игры для Android - 5

Вектор 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. Первый результат

Содержание

Спустя месяц разработки была получена первая играбельная версия игры с полностью реализованным функционалом. Скриншоты прилагаю:

Скриншоты первой версии игры

Hunt the Wumpus или опыт написания классической игры для Android - 6

Hunt the Wumpus или опыт написания классической игры для Android - 7

Hunt the Wumpus или опыт написания классической игры для Android - 8

Hunt the Wumpus или опыт написания классической игры для Android - 9

На представленных скриншотах можно увидеть: главное меню, окно с правилами, окно с настройками, игровое окно.

Разумеется, я понимал, что результат вышел совсем неинтересный. Главное же то, что я получил практический опыт по AS и Java, что и было первозадачей.

Игрок, как и в классической игре, должен был взаимодействовать через консоль: вводить номер комнаты для перемещения либо маршрут для полёта стрелы. В настройки я сделал возможным менять размер шрифта и прозрачность подложки. Предполагал, что для каждого игрового окна будет своя картинка-подложка (чтобы немного разнообразить геймплей, ха!).

Далее я планировал заменить имеющиеся картинки (которые я бессовестно взял из интернета) на те, что нарисует художник. Потом я бы выпустил игру в Play market и благополучно забыл бы про неё, применяя полученный опыт уже к новым проектам. И я не мог тогда предположить, как сильно может измениться Вампус…

9. Даже маленькие игры достойны истории

Содержание

Когда человек подходит к работе с душой, то от этого проект только выигрывает. Мне повезло, что художница, Анастасия Фроликова, оказалась именно таким человеком. Т.е. вместо того, чтобы просто нарисовать то, что требовалось мне, она заинтересовалась миром игры и захотела понять, как он устроен. И вдруг оказалось, что никакого мира, по большому счёту, нет! Кто такой этот Вампус? И почему игрок должен его убить? Как выглядят комнаты Вампуса? И прочее, прочее над чем я не думал и что не планировал рассказывать игроку. В результате мы сошлись на том, что даже у такой, казалось бы, маленькой игры должна быть своя история. И она появилась.

Согласно нашей легенде Вампус хоть и древнее, но не злое мифическое существо, любящее подшучивать над людьми. Да, он живет в лабиринте, но этот лабиринт не в виде классического мрачного подземелья, а в виде невообразимого дома, состоящего из нагромождения комнат, содержание которых характеризует Вампуса. Так, например, в одной из комнат расположился кинотеатр, на стенах которого постеры его любимых фильмов, а в другой находится его каморка, где он готовит свои «розыгрыши».

Hunt the Wumpus или опыт написания классической игры для Android - 10

Игрок из безымянного охотника превратился в завсегдатая криптозоологического форума, который хочет доказать существование Вампуса. Мы заменили классические лук и стрелы на фотоаппарат и плёнку, а целью игры стало не убийство Вампуса, а получение его фотографии. К слову, главное меню было переделано под форум, где люди обсуждают Вампуса, а из обсуждения игрок может узнать про него.

Первая версия главного меню, выполненного в виде форума

Hunt the Wumpus или опыт написания классической игры для Android - 11

Что касаемо остальных аспектов, то они остались практически без изменений: мыши действуют так же, а попадание в яму стало приводить к падению, разбитию камеры и завершению игры (а не смерти игрока).

Hunt the Wumpus или опыт написания классической игры для Android - 12

Ещё момент про камеру. В классической игре игрок мог пустить стрелу на дальность от 1 до 5 комнат и это выглядело логично. У нас же вместо лука камера (фотографирующая от 1 до 3 комнат за раз, но работающая как классическая стрела, поражающая Вампуса). И это… выглядит странно, не находите? Была идея уменьшить дальность камеры до 1 комнаты, чтобы фотографировать можно было только соседнюю, но это, во-первых, усложнит игру, а, во-вторых, могут быть получены такие ситуации, когда игра не может быть выиграна, что неправильно. В общем, это тот момент, который лично мне не даёт покоя до сих пор, а решения я пока не нашёл.

Что касаемо стиля стиля и настроения игры. Практически все игры про Вампуса выполнены в скучных серых тонах, а действия происходят в тёмных локациях подземелий. Поэтому мы решили, что наша игра должна отличаться от всего этого и быть выполнена с юмором и в ярких красках.

Hunt the Wumpus или опыт написания классической игры для Android - 13

10. Преображение Вампуса и конечный результат

Содержание

Дальше нас ждали ещё 2 месяца работы над игрой. Так как у Васпуса 20 комнат, то для каждой был создан свой интерьер. Помимо этого, были нарисованы иконки достижений, иконки в игре, приняты решения по дизайну в целом и UI. Так же был дописан весь игровой текст, дополнен и оптимизирован код, были добавлены новые функции (например, появился блокнот для записей информации по ходу игры). В общем, Вампус подвергся серьёзным изменениям.

Комнаты, например, создавались следующим образом: (скриншот кликабелен):

Hunt the Wumpus или опыт написания классической игры для Android - 14

Комнат 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]. И они, фактически, связаны с «истинными» индексами додекаэдра, но для игрока каждую новую игру для одной и той же комнаты будут разные картинки. Зачем это нужно? Для того, чтобы дать возможность в конкретной игровой партии соотнести текстовую информацию с изображением конкретной комнаты («ага, помню, что в комнате Х, которая гостиная, был сквозняк») и чтобы не получалось так, что «а почему у меня всегда в комнате Х одна и также картинка?». Всё для разнообразия и помощи игроку (впрочем, как показал опыт, помощи от визуального запоминания нет, эффективнее работать с текстом).

В конечном счёте мы получили новую версию игры. И, знаете что? Вампус стал чертовски привлекателен, а самое главное, это всё тот же классический Вампус, но в новом уютном доме!

Скриншоты второй версии игры

Hunt the Wumpus или опыт написания классической игры для Android - 15

Hunt the Wumpus или опыт написания классической игры для Android - 16

Hunt the Wumpus или опыт написания классической игры для Android - 17

Hunt the Wumpus или опыт написания классической игры для Android - 18

На скриншотах: главное меню, окно с правилами, окно с настройками, игровое окно.

Что касаемо механики, то она лишь слегка изменена и переименована (ну в самом деле, есть ли разница: камера или лук, если делать нужно одно и тоже?). Самое заметное изменение в механике — это упрощение процесса взаимодействия между игроком и игрой – был убран классический ввод номеров комнат при помощи клавиатуры. Теперь для перехода между комнатами нужно выбрать во всплывающем окошке 1 из 3 чисел, а для формирования «маршрута» фотографирования достаточно прокрутить колёса на барабане:

Hunt the Wumpus или опыт написания классической игры для Android - 19

На видео ниже вы можете увидеть конечный результат:

11. Вывод

Содержание

Первая версия игры была получена мною за месяц разработки, выделяя по 1-2 часа времени после работы. При этом ни AS, ни Java не были мне ранее знакомы. Вторая версия игры потребовала ещё 2 месяца. Таким образом, всего 3 месяца неспешной работы.

Конечно, в таком виде игра не для широкого круга, т.к. она может показаться сложной и не захватывающей современному игроку, но важнее, наверное, сохранение духа классических игр, не находите?

Доволен ли я результатом? Однозначно, да. Я получил большой опыт как программирования, так и работы в команде. Мне нравится, как получившаяся механика игры, так и визуальная её составляющая. Есть ли что-то, что бы мне хотелось изменить? Разумеется, нет пределов совершенства и всегда можно что-то добавить/улучшить, но нельзя же этим заниматься вечно!

Интересна ли эта игра? Что ж, тут уж решать не мне. Но пусть Вампусу будет уютно в том доме, что мы для него выстроили с большой любовью и вниманием.

Желаю Вам успехов!

Спасибо за внимание и берегитесь Вампуса!

P.S. Постоянно возникающие задачи и радость от их решения — это именно то, за что я люблю программирование. Надеюсь, что и вы получаете не меньшее удовольствие.

Автор: Олег

Источник


* - обязательные к заполнению поля