4. Реализация приложения
.1 Проектирование интерфейса
В минимально-полной комплектации игры с разобранной выше жанровой принадлежностью будет два основных состояния экрана: главное меню и игровой процесс. Первое представляет собой стандартное меню с кнопками перехода в другие состояния приложения и художественным задним планом. Пример этого экрана можно увидеть на рисунке 4.1. Экран же игрового процесса состоит из двух основных элементов - игровое поле и область информации о выбранном в текущий момент персонаже. Игровое поле внешне схоже с усложнёнными шахматами: прямоугольное поле из равных четырёхугольных сегментов, каждый из которых обозначает определённый тип местности - равнины, горы, лес, снег или вода. На клетках располагаются персонажи двух команд (игрока и искусственного интеллекта) визуально различимые по цвету флага рядом с ними. При выборе кликом определённого юнита на вторую область экрана выводится полная информация о его наименовании, его параметрах жизни, защиты и атаках. Этот экран показан на рисунке 4.2.
Рисунок 4.1 Главное меню
Рисунок 4.2 экран игрового процесса
4.2 Подготовка графических материалов
Создание графических материалов - наиболее сложная и затратная по времени часть создания игрового приложение, особенно если разговор идёт о одиночном разработчике или малой команде без специализированного художника. Оптимальным решением в такой ситуации может быть использование художественных материалов, распространяемых по свободной лицензии Creative Commons License [12]. Эта лицензия позволяет использовать и даже адаптировать защищённые ей материалы, в том числе и в коммерческих продуктах. Единственным условием является упоминание автора исходных материалов.
Было бы заблуждением считать, что игры с такими материалами не могут получить значительной аудитории. Достаточно часто такие игры можно находить в крупнейших системах цифрового распространения с преобладанием положительных отзывов и не рекордной, но значительной и стабильной аудиторией.
Хорошим примером игры от независимого разработчика, использующей свободные художественные материалы, будет Rogue’s Tale [13]. Игра вышла в 2014 году в системе Steam [5] и к настоящему времени получила множество положительных отзывов от пользователей. Игра выполнена в простой двухмерной стилистике и принадлежит к не имеющему корректного перевода на русский язык жанру rouge-like, который в последние годы переживает первый со времён псевдографических игр подъём популярности. Пример графического исполнения Rouge’s Tale можно увидеть на рисунке 4.3.
Рисунок 4.3 Rogue’s
Tale
Мною были изучены различные ресурсы, предназначенные для начинающих разработчиков, и отобраны графические и музыкальные материалы, наиболее подходящие разрабатываемой игре по стилистике. Этих материалов достаточно для начальных этапов развития проекта. Многие из графических элементов были вручную адаптированы для большего соответствия авторскому замыслу у упрощения последующей обработки. Примеры свободных материалов представлены на рисунках 4.4 и 4.5. Кроме того, были дополнительно разработаны некоторые собственные графические материалы, как например карта игрового мира, являющаяся подводкой к будущему сюжету игры. Эту карту можно увидеть на заднем плане на рисунке 4.1.
Рисунок 4.4 Текстуры для игровых персонажей
Рисунок 4.5 Текстуры для клеток игрового поля
.3 Написание кода приложения
В связи с особенностями архитектуры фреймворка libGDX полное следование парадигме MVC (Model-View-Control, «Модель-Отображение-Управление») не представляется возможным, так как представление и управление взаимосвязаны, а их функции объявляются в общих интерфейсах и абстрактных классах. Таким образом, программа разделяется на две основные части: контрольно-отображающий модуль, основанный на стандартных для libGDX классах и функциях, и модуль внутренней логики игрового процесса, написанный на стандартной комплектации языка Java. Также присутствуют модули сборки для платформ Windows и Android, но они генерируются фреймворком и требуют минимальных изменений со стороны разработчика. Отдельно можно выделить и класс Game1, который организует работу двух модулей и содержит наиболее важные игровые константы и статические функции.
Классы внутренней логики отображают такие элементы как карта, поле (клетка игрового поля), персонаж, тип атаки, сценарий (объединение карты и всех персонажей на ней) и так далее. Объект класса карты создаётся на основе двух входных файлов, которые отвечают за генерацию игрового поля и размещение юнитов. Стоит отметить, что в таких целях часто используют стандартные форматы передачи данных, такие как JSON или XML. Однако, по моему мнению, при относительно небольших объёмах информации эти форматы являются избыточными, так как увеличивают объём кода и время загрузки приложения, не обеспечивая ощутимых преимуществ взамен. Поэтому был использован собственный простейший текстовый формат. На рисунке 4.6 в окне Android Studio показан участок конструктора класса Map.java, который отвечает за разбор одного из двух текстовых файлов и генерации на его основе игрового поля, полностью готового к дальнейшей обработке и отображению.
Рисунок 4.6 Пример кода внутриигровой логики
Работа модуля отображения управляется объектом класс
GameStateManager.java,
который управляет стеком объектов класса State.java
и его наследников. Последние реализуют состояния приложения, описанные в пункте
4.1. Наследники класса State.java
- MenuState.java
и ScenarioState.java,
они представляют состояния игрового меню и игрового процесса соответственно.
Простая и эффективная реализация класса GameStateManager.java
можно увидеть в окне IDE
на рисунке 4.7.
Рисунок 4.7 Пример кода модуля отображения
Стоит также отдельно упомянуть искусственный интеллект. Выбор хода для юнитов команды противника выбирается следующим весовым методом: для каждого из персонажей команды рассматривается каждый возможный ход и рассчитывается его рейтинг. Рейтинг хода рассчитывается по следующим критериям: усиление контроля на карте, рассчитываемое от уменьшения расстояния до центральных клеток поля, создание опасности игроку, что означает приближение к персонажам игрока, которым можно нанести значительный урон, и создание опасности для самого юнита, что рассчитывается из приближения к персонажам игрока, способным нанести серьёзные повреждения на следующем ходу. Для каждого персонажа команды запоминается его лучший ход и после они оцениваются между собой, что обеспечивает выбор оптимального хода.
5. Вектор развития
игра приложение интерфейс графический
У любого игрового проекта есть множество способов расширить предлагаемые пользователям возможности и вместе с этим выводить проект на новые уровни популярности и прибыльности. В настоящее время для таких целей разработчики часто приходят к решению вводить в игру микротранзакции - небольшие платежи за дополнительный внутриигровое наполнение. Недостатки этого метода часто приводят к широкому недовольству игроков и это явление уже становится серьёзным кризисом видеоигровой индустрии.
Но существуют и другие направления для развития. Наиболее удачными на мой взгляд являются следующие:
создание сюжетного наполнения. Это наиболее приоритетное наполнение, так как игра, содержащая абстрактное поле боя, и игра, рассказывающая на этом поле боя полноценную историю - две совершенно разные вещи и по-разному воспринимаются игроками. В настоящее время сценарий к игре и дополняющие его графические материалы находятся в активной стадии разработки;
случайная генерация карт и сражений. В игровой журналистике широко распространён термин «реиграбельность», обозначающей способность игры поддерживать интерес при повторном прохождении. Внесение в игру значительного влияния элемента случайности, не лишая при этом игрока возможности осознанно влиять на ход событий - простейший и эффективнейший способ увеличить реиграбельность проекта, что неоднократно доказано известнейшими представителями пошаговых стратегий и других жанров;
переход от свободных графических материалов к собственным. Является необходимым этапом для проекта, выходящего на мировой уровень, но требует привлечения квалифицированных специалистов-дизайнеров, что делает этот пункт практически невозможным на ближайших стадиях развития игры;
создание редактора внутриигрового содержимого и открытие пользователям возможности создавать модификации к игре. Такое решение может оказаться полностью невостребованным в одной игре и великолепной возможностью для игроков в другой. Чаще всего это больше зависит не от разработчика, а специфики сообщества пользователей игры. В качестве примеров можно вспомнить такие всемирно известные игры, как Minecraft, ставший огромным виртуальным конструктором для игроков в том числе и в плане создания модификаций к самой игре, или серию The Elder Scrolls, количество модификаций к любой поздней части которой исчисляется десятками тысяч;
модернизация искусственного интеллекта. Более подробный расчёт ходов будет означать более высокую сложность игры, а это в свою очередь привлекает многих игроков. Впрочем, искусственный интеллект является не единственным способом достижения нужной сложности игры, многие стратегии придерживаются скриптовых моделей поведения противников игрока;
введение многопользовательского режима. Спорное решение, которое совмещает большое расширение потенциальной аудитории с многими минусами, такими как необходимость изменять игру для обеспечения баланса в сетевом режиме в ущерб уникальности и разнообразию игровых механик и необходимость обеспечения игры сетевым кодом и серверной частью;
Заключение
Игра, получившаяся в процессе реализации данного проекта ещё не готова быть опубликованным продуктом. Техническая часть нуждается в доработках, но уже практически достаточна для обеспечения серьёзного проекта. Большее внимание в ближайшем будущем стоить уделить игровому наполнению - от новой игры пользователи ожидают не меньше нескольких часов игрового процесса, наполненного выделяющимися стилистикой и игровыми механиками.
Развитие проекта по указанным в пятой главе направлениям обеспечит игре стабильное приближению в таких системах, как Google Play [2] и Steam [5], что уже само по себе гарантирует первоначальную аудиторию. Система отзыва в этих средах так же позволит сделать выводы о состоянии проекта и скорректировать направления разработки.
Также при реализации проекта был получен новый опыт работы с языком программирования Java, со средой разработки Android Studio и фреймворком libGDX, были усвоены базовые навыки по работе с двухмерной графикой и искусственным интеллектом.
Список использованных источников
1. SuperData Research [Электронный ресурс]: офиц. сайт. Режим доступа: https://www.superdataresearch.com/.
2. Google Play [Электронный ресурс]: офиц. сайт. Режим доступа: https://play.google.com/store.
. Приложение Braveland в Google Play [Электронный ресурс]. Режим доступа: https://play.google.com/store/apps/details?id=com.tortugateam.braveland.v2&hl=ru.
4. Warhammer 40,000: Deathwatch: Tyranid Invasion [Электронный ресурс]: офиц. сайт. Режим доступа: http://rodeogames.co.uk/deathwatch.
5. Steam [Электронный ресурс]: офиц. сайт. Режим доступа: store.steampowered.com.
. Приложение Battle for Wesnoth в Google Play [Электронный ресурс]. Режим доступа: https://play.google.com/store/apps/details?id=it.alessandropira.wesnoth112&hl=ru.
. Стандартная общественная лицензия GNU (GPL) [Электронный ресурс]: офиц. сайт. Режим доступа: https://www.gnu.org/licenses/gpl-3.0.ru.html.
. Приложение King’s Bounty: Legions в Google Play [Электронный ресурс]. Режим доступа: https://play.google.com/store/apps/details?id=com.kranx.kbl.
9. Eclipse [Электронный ресурс]: офиц. сайт. Режим доступа: https://eclipse.org/.
10. IntelliJ IDEA [Электронный ресурс]: офиц. сайт. Режим доступа: https://www.jetbrains.com/idea/.
11. Android Studio [Электронный ресурс]: офиц. сайт. Режим доступа: https://developer.android.com/studio/index.html.
12. Creative Commons License [Электронный ресурс]: офиц. сайт. Режим доступа: https://creativecommons.org/licenses/by/3.0.
. Приложение Rogue’s Tale в Steam [Электронный ресурс]. Режим доступа: http://store.steampowered.com/app/265990/Rogues_Tale/?l=russian.
Приложение 1
Коды классов приложения
Game1.java
package com.vlerof.game1;com.badlogic.gdx.ApplicationAdapter;com.badlogic.gdx.Gdx;com.badlogic.gdx.files.FileHandle;com.badlogic.gdx.graphics.Color;com.badlogic.gdx.graphics.GL20;com.badlogic.gdx.graphics.Pixmap;com.badlogic.gdx.graphics.Texture;com.badlogic.gdx.graphics.g2d.SpriteBatch;com.vlerof.game1.Scenario.Field;com.vlerof.game1.Scenario.Level;com.vlerof.game1.Scenario.Map;com.vlerof.game1.Scenario.Scenario;com.vlerof.game1.Scenario.Unit;com.vlerof.game1.States.GameStateManager;com.vlerof.game1.States.MenuState;class Game1 extends ApplicationAdapter {static final int WIDTH = 1080;static final int HEIGHT = 1920;static final int INTERFACE_HEIGHT = 160;static final int FIELD_HEIGHT = HEIGHT - INTERFACE_HEIGHT;static final int ATTACK_BORDER = WIDTH * 3 / 5;static final int SCALE = 32;static final String TITLE = "Game";static final Color COLOR_SIENNA = new Color(0.627f, 0.322f, 0.176f, 1f);static final Color COLOR_SADDLEBROWN = new Color(0.545f, 0.271f, 0.075f, 1f);static final Color COLOR_BLACK = new Color(0.0f, 0.0f, 0.0f, 1f);static Pixmap characterMap1;static Pixmap characterMap2;static Pixmap fieldMap;static Texture flagPlayer;static Texture flagEnemy;static Texture buttonStart;static Texture buttonExit;GameStateManager gsm;SpriteBatch batch;
@Overridevoid create () {= new SpriteBatch();= new GameStateManager();= new Pixmap(new FileHandle("Tiles/dg_classm32.png"));= new Pixmap(new FileHandle("Tiles/dg_uniques32.png"));= new Pixmap(new FileHandle("Tiles/dg_grounds32.png"));= new Texture(new FileHandle("flag1.png"));= new Texture(new FileHandle("flag2.png"));= new Texture(new FileHandle("start.png"));= new Texture(new FileHandle("exit.png"));.initLevels();.initTiles();.gl.glClearColor(0, 0, 0, 1);.push(new MenuState(gsm));
}
@Overridevoid render () {.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);.update(Gdx.graphics.getDeltaTime());.render(batch);
}
@Overridevoid dispose () {.dispose();.dispose();.dispose();.dispose();.dispose();.dispose();.dispose();.dispose();
}static Scenario getDefaultScenario() {sc = new Scenario();.setMap(new Map("Maps/map1.txt"));.setSpawns("Maps/respawn1.txt");unit;
//TODO: moves, attacks, hp, defs= new Unit("John", 15, new Tile(characterMap1, 3, 0));.addEnemyUnit(unit);= new Unit("Karl", 15, new Tile(characterMap1, 3, 1));.addEnemyUnit(unit);= new Unit("Guss", 15, new Tile(characterMap1, 3, 2));.addEnemyUnit(unit);= new Unit("Bob", 15, new Tile(characterMap1, 3, 3));.addEnemyUnit(unit);= new Unit("Ivan", 15, new Tile(characterMap1, 3, 4));.addEnemyUnit(unit);= new Unit("Jack", 15, new Tile(characterMap1, 3, 5));.addEnemyUnit(unit);= new Unit("Tzeentch", 15, new Tile(characterMap1, 4, 6));.addPlayerUnit(unit);= new Unit("Nurgle", 15, new Tile(characterMap2, 9, 5));.addPlayerUnit(unit);= new Unit("Malal", 15, new Tile(characterMap2, 5, 2));.addPlayerUnit(unit);= new Unit("Slaanesh", 15, new Tile(characterMap2, 4, 0));.addPlayerUnit(unit);= new Unit("Khorn", 15, new Tile(characterMap2, 6, 0));.addPlayerUnit(unit);sc;
}static String formatString(String s) {int length = s.length();char[] chars = s.toCharArray();(int i = 0; i < length; i++) {(chars[i] == '\r' || chars[i] == '\n') {[i] = ' ';
}
}str = new String(chars);str.replaceAll(" ", " ");
}
}
Tile.java
package com.vlerof.game1;com.badlogic.gdx.graphics.Pixmap;com.badlogic.gdx.graphics.Texture;class Tile {Pixmap pixmap;int xOffset;int yOffset;Tile(Pixmap _source, int _xOffset, int _yOffset) {= _source;= _xOffset;= _yOffset;
}Texture getTexture() {pxmp = new Pixmap(Game1.SCALE, Game1.SCALE, Pixmap.Format.RGBA8888);.drawPixmap(pixmap, 0, 0, xOffset * Game1.SCALE, yOffset * Game1.SCALE, Game1.SCALE, Game1.SCALE);new Texture(pxmp);
}Pixmap getPixmap() {pxmp = new Pixmap(Game1.SCALE, Game1.SCALE, Pixmap.Format.RGBA8888);.drawPixmap(pixmap, 0, 0, xOffset * Game1.SCALE, yOffset * Game1.SCALE, Game1.SCALE, Game1.SCALE);pxmp;
}
}
Map.java
package com.vlerof.game1.Scenario;com.badlogic.gdx.Gdx;com.badlogic.gdx.files.FileHandle;com.badlogic.gdx.graphics.Pixmap;com.badlogic.gdx.graphics.Texture;com.vlerof.game1.Game1;java.awt.Point;java.io.BufferedReader;java.io.File;java.io.FileInputStream;java.io.IOException;java.io.InputStreamReader;class Map {final int WIDTH;final int HEIGHT;Field map[][];Map(String _filename) {file = Gdx.files.internal(_filename);str = Game1.formatString(file.readString());spacePos = str.indexOf(" ");= Integer.parseInt(str.substring(0, spacePos));= str.substring(spacePos + 1);= str.indexOf(" ");= Integer.parseInt(str.substring(0, spacePos));= str.substring(spacePos + 1);= new Field[WIDTH][HEIGHT];fieldCode;(int y = 0; y < HEIGHT; y++) {(int x = 0; x < WIDTH - 1; x++) {= str.indexOf(" ");= str.substring(0, spacePos);= str.substring(spacePos + 1);[x][y] = parseField(fieldCode);
}(y == HEIGHT - 1) {= str.substring(0);
} else {= str.indexOf(" ");= str.substring(0, spacePos);= str.substring(spacePos + 1);
}[WIDTH - 1][y] = parseField(fieldCode);
}
}Field getField(int _x, int _y) {map[_x][_y];
}Field getField(Point _point) {map[_point.x][_point.y];
}Field parseField(String _code) throws StringIndexOutOfBoundsException {type;typeStr = _code.charAt(0);(typeStr) {'p': {= FieldType.PLAIN;;
}'f': {= FieldType.FOREST;;
}'m': {= FieldType.MOUNTAIN;;
}'w': {= FieldType.WATER;;
}'b': {= FieldType.BLOCKED;;
}'s': {= FieldType.SNOW;;
}: {= FieldType.BLOCKED;
}
}number = Integer.parseInt(_code.substring(1));new Field(type, number);
}Pixmap getPixmap() {pxmp = new Pixmap(Game1.SCALE * WIDTH, Game1.SCALE * HEIGHT, Pixmap.Format.RGBA8888);(int y = 0; y < HEIGHT; y++) {(int x = 0; x < WIDTH; x++) {.drawPixmap(map[x][y].getTile().getPixmap(), x * Game1.SCALE, y * Game1.SCALE, 0, 0, Game1.SCALE, Game1.SCALE);
}
}.setColor(0.3f, 0.3f, 0.3f, 0.5f);(int x = 1; x < WIDTH; x++) {.drawRectangle(x * Game1.SCALE, 0, 2, HEIGHT * Game1.SCALE);
}(int y = 1; y < HEIGHT; y++) {.drawRectangle(0, y * Game1.SCALE, WIDTH * Game1.SCALE, 2);
}pxmp;
}
}
Scenario.java
package com.vlerof.game1.Scenario;com.badlogic.gdx.Gdx;com.badlogic.gdx.files.FileHandle;com.badlogic.gdx.graphics.Pixmap;com.badlogic.gdx.graphics.g2d.SpriteBatch;com.vlerof.game1.Game1;java.awt.Point;java.util.ArrayList;class Scenario {Map map;ArrayList<Point> playerSpawn;ArrayList<Point> enemySpawn;ArrayList<Unit> playerTeam;ArrayList<Unit> enemyTeam;Scenario() {= new ArrayList<Point>();= new ArrayList<Point>();= new ArrayList<Unit>();= new ArrayList<Unit>();