[Из песочницы] Создание Zero Player Game, используя libgdx
Идея Игровое пространство — клетчатое поле ограниченное рамкой Существующие типы клеток: Пустая клетка — белый Стена — чёрный Зверь — красный След — коричневый Дом — зелёный Перемещение зверя оставляет неисчезающий след При запуске генерируется лабиринт Зверь знает состояние соседних клеток и на основании этого строит карту при перемещении При перемещении зверь расходует силы, которые восстанавливаются в доме (+5) или на любой клетке (+1) При столкновении звери объединяются в стаи (дома переносятся в соседние точки), если несколько зверей одновременно отдыхают в доме их карты объединяются (Не реализовано)Разные «кланы» случайным образом объединяются или воюют (Не реализовано)Генетический алгоритм для различных поведений зверей, случайно перемешивающиеся при размножении (+мутации), более перспективный вид выживет в войнах Почему libgdx? Разработку я вёл с разных устройств, домашний комп на ubuntu и планшет на win8, связка java + eclipse позволила делать это без проблем. Libgdx использован для удобства работы с камерой, возможности добавления графики в дальнейшем, а также для создания версии под андроид.В этой статье Заготовка игры, в которой реализовано: Игровое поле Генерация лабиринта Создание зверя кликом по карте Перемещение существа, обходя препятствия, по полю с целью построить полную карту местности. Результат:
[embedded content]
Начало После создания и импорта проекта в eclipse добавим необходимые поля (в зависимости от версии libgdx некоторые уже могут быть добавлены) SpriteBatch batch;//Класс для рисования спрайтов на игровом поле OrthographicCamera camera;//Перемещение по игровому полю Texture texture;//Текстура клетки (на видео это png изображение — белый квадрат с чёрной рамкой) В методе create () инициализируем их:
batch = new SpriteBatch (); batch.disableBlending (); camera = new OrthographicCamera (FIELD_SIZE, FIELD_SIZE); Добавим необходимые константы:
public static final int FIELD_SIZE = 51;//Размер поля (поле квадратное) public static float UPDATE_TIME = 0.001f;//интервал между «шагами» существ Далее пригодится абстрактный класс Cell, который будет описывать общий функционал клеток.
public abstract class Cell {
public Color color; Sprite sprite; public Cell (Texture texture, Color color){ this.color = color; sprite = new Sprite (texture); sprite.setColor (color); sprite.setSize (1, 1); } public abstract void update (Cell[][] map, int x, int y, Texture texture); public void setColor (Color color){ this.color = color; sprite.setColor (color); } public void draw (SpriteBatch batch, int x, int y){ sprite.setPosition (x-Main.FIELD_SIZE/2-sprite.getWidth ()/2, y-Main.FIELD_SIZE/2-sprite.getHeight ()/2); sprite.draw (batch); } } Сразу рассмотрим двух его наследников Wall и Empty.
public class Wall extends Cell {
public Wall (Texture texture) { super (texture, new Color (0f, 0f, 0f, 1)); }
@Override public void update (Cell[][] map, int x, int y, Texture texture) {
}
}
public class Empty extends Cell {
public Empty (Texture texture) { super (texture, new Color (1, 1, 1, 1)); }
@Override public void update (Cell[][] map, int x, int y, Texture texture) {
}
}
Теперь необходимо создать лабиринт. пояснять алгоритм не буду, он неплохо изложен тут. Этот алгоритм я выделил в отдельный класс MazeGenerator с единственным методом getMaze (int size), который возвращает двумерный массив нулей и единиц, где 0 — пустая клетка, 1 — стена.
Игровое поле будет храниться в простом двумерном массиве:
Cell[][] map; Создание поля выглядит так:
map = new Cell[FIELD_SIZE][FIELD_SIZE];
texture = new Texture (Gdx.files.internal («tile.png»));//не забываем подгрузить изображение
char[][] bmap = (new MazeGenerator ()).getMaze (FIELD_SIZE — 1); for (int i = 0; i < FIELD_SIZE; i++) for (int j = 0; j < FIELD_SIZE; j++) { if (bmap[i][j] == 0) map[i][j] = new Empty(texture); if (bmap[i][j] == 1) map[i][j] = new Wall(texture); } Теперь мы имеем случайный лабиринт при каждом запуске программы. Можно поиграться с константами и определить для себя лучшую комбинацию.
Да на этом скрине tile.png это просто белый квадрат.
Зверь Настало время наполнить мир жизнью.Создадим потомка Cell:
public class Unit extends Cell {
Cell[][] my_map = new Cell[3][3];//собственная карта, изначально известны только соседние клетки
float update_time = Main.UPDATE_TIME;//счётчик шага
int mapX = 1, mapY = 1;//координаты зверя на собственной карте
Vector
enum Action { left, right, up, down//список действий }
public Unit (Texture texture, Cell[][] map, int x, int y) { super (texture, new Color (1f, 0, 0, 1)); for (int i = x — 1; i <= x + 1; i++) for (int j = y - 1; j <= y + 1; j++) my_map[i - x + 1][j - y + 1] = map[i][Main.FIELD_SIZE - j - 1];
my_map[1][1] = this; homeX = 1; homeY = 1; } private int goRight (Cell[][] map, int x, int y, Texture texture) {…}//map — полная, истинная карта мира, x, y — расположение зверя на ней private int goLeft (Cell[][] map, int x, int y, Texture texture) {…} private int goUp (Cell[][] map, int x, int y, Texture texture) {…} private int goDown (Cell[][] map, int x, int y, Texture texture) {…} Не хочу загружать пост кодом, поэтому весь метод update приводить не буду.
Алгоритм работы прост: проверяем очередь действий, если она не пуста, то уменьшаем счётчик такта, если он пуст, заново увеличиваем его и выполняем действие и обновляем окрестности на карте. Если действий нет то строим новый маршрут, но об этом немного дальше, а сейчас рассмотрим шаг персонажа.
Для удобства создадим отдельный метод для шага в каждую сторону:
private int goRight (Cell[][] map, int x, int y, Texture texture) {…}//map — полная, истинная карта мира private int goLeft (Cell[][] map, int x, int y, Texture texture) {…}//x, y — расположение зверя на ней private int goUp (Cell[][] map, int x, int y, Texture texture) {…} private int goDown (Cell[][] map, int x, int y, Texture texture) {…} «Шаг» будет состоять из нескольких действий.
Проверка не надо ли расширить собственную карту
Расширение карты (создание нового увеличенного массива и копирование в него старой карты)
Перемещение н новую клетку
Запись изменений в mapX, mapY
Определение маршрута
На мой взгляд самое простое решение — волновой алгоритм, который троит маршрут в случайную пустую клеткуДля этого я добавил новый класс WavePath со статичным методом:
public static Vector
Финальные штрихи Теперь осталось только рисовть всё это на экран и, перебирая массив карты, обновлять состояние клеток @Override public void render () { this.update ();//обновление карты
Gdx.gl.glClearColor (0, 0, 0, 1); Gdx.gl.glClear (GL20.GL_COLOR_BUFFER_BIT);
batch.setProjectionMatrix (camera.combined);
batch.begin (); for (int i = 0; i < FIELD_SIZE; i++) for (int j = 0; j < FIELD_SIZE; j++) if(!(map[i][j] instanceof Wall))//не рисуем чёрные квадраты на чёрном фоне map[i][j].draw(batch, i, j); batch.end(); }
public void update () {
Input input = Gdx.input;
for (int i = 0; i < FIELD_SIZE; i++) for (int j = 0; j < FIELD_SIZE; j++) map[i][j].update(map, i, j, texture);//обновляем
if (input.isKeyPressed (Input.Keys.W))//сдвиг камеры, масштабирование, вращение, ускорение camera.zoom-=Gdx.graphics.getDeltaTime (); if (input.isKeyPressed (Input.Keys.S)) camera.zoom+=Gdx.graphics.getDeltaTime ();
if (input.isKeyPressed (Input.Keys.Q)) camera.rotate (Gdx.graphics.getDeltaTime ()*90); if (input.isKeyPressed (Input.Keys.E)) camera.rotate (-Gdx.graphics.getDeltaTime ()*90); if (input.isKeyPressed (Input.Keys.CONTROL_LEFT)) UPDATE_TIME+=Gdx.graphics.getDeltaTime (); if (input.isKeyPressed (Input.Keys.SHIFT_LEFT)) UPDATE_TIME-=Gdx.graphics.getDeltaTime (); if (input.isKeyPressed (Input.Keys.LEFT)) camera.translate (new Vector2(-Gdx.graphics.getDeltaTime ()*50,0)); if (input.isKeyPressed (Input.Keys.RIGHT)) camera.translate (new Vector2(Gdx.graphics.getDeltaTime ()*50,0)); if (input.isKeyPressed (Input.Keys.UP)) camera.translate (new Vector2(0, Gdx.graphics.getDeltaTime ()*50)); if (input.isKeyPressed (Input.Keys.DOWN)) camera.translate (new Vector2(0,-Gdx.graphics.getDeltaTime ()*50)); if (input.isKeyPressed (Input.Keys.SPACE)){//восстановление камеры UPDATE_TIME = 1f; camera = new OrthographicCamera (FIELD_SIZE, FIELD_SIZE); } camera.update (); if (input.isTouched ()) {//садим зверя на поле float stepX = Gdx.graphics.getWidth () / FIELD_SIZE; float stepY = Gdx.graphics.getHeight () / FIELD_SIZE; float x = input.getX (); float y = input.getY (); for (int i = 0; i < FIELD_SIZE; i++) for (int j = 0; j < FIELD_SIZE; j++) { if (x >= stepX * i && x <= stepX * (i + 1) && y >= stepY * j && y <= stepY * (j + 1)) if (map[i][FIELD_SIZE - j - 1] instanceof Empty) map[i][FIELD_SIZE - j - 1] = new Unit(texture, map, i, j); } }
} Заключение Заранее прошу прощения за ошибки, и не полное изложение материала. Исходники на github.Если кого-то заинтересовало, напишу продолжение.