...

четверг, 29 мая 2014 г.

[Из песочницы] Создание Zero Player Game, используя libgdx

Идея





  1. Игровое пространство — клетчатое поле ограниченное рамкой

  2. Существующие типы клеток:

    • Пустая клетка — белый

    • Стена — чёрный

    • Зверь — красный

    • След — коричневый

    • Дом — зелёный



  3. Перемещение зверя оставляет неисчезающий след

  4. При запуске генерируется лабиринт

  5. Зверь знает состояние соседних клеток и на основании этого строит карту при перемещении

  6. При перемещении зверь расходует силы, которые восстанавливаются в доме(+5) или на любой клетке(+1)

  7. При столкновении звери объединяются в стаи(дома переносятся в соседние точки), если несколько зверей одновременно отдыхают в доме их карты объединяются

  8. (Не реализовано)Разные «кланы» случайным образом объединяются или воюют

  9. (Не реализовано)Генетический алгоритм для различных поведений зверей, случайно перемешивающиеся при размножении(+мутации), более перспективный вид выживет в войнах






Почему libgdx?



Разработку я вёл с разных устройств, домашний комп на ubuntu и планшет на win8, связка java + eclipse позволила делать это без проблем. Libgdx использован для удобства работы с камерой, возможности добавления графики в дальнейшем, а также для создания версии под андроид.

В этой статье




Заготовка игры, в которой реализовано:


  • Игровое поле

  • Генерация лабиринта

  • Создание зверя кликом по карте

  • Перемещение существа, обходя препятствия, по полю с целью построить полную карту местности.


Результат:



Начало




После создания и импорта проекта в 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<Action> queue = new Vector<Action>();//список действий для выполнения

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<Action> getPath(Cell[][] my_map, int x, int y, int nx,int ny){...}

Этот метод возвращает возвращает последовательность шагов для достижения случайно выбранной точки.


Финальные штрихи




Теперь осталось только рисовть всё это на экран и, перебирая массив карты, обновлять состояние клеток

@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.

Если кого-то заинтересовало, напишу продолжение.


This entry passed through the Full-Text RSS service — if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.


Комментариев нет:

Отправить комментарий