Создание 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.

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

ссылка на оригинал статьи http://habrahabr.ru/post/224525/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *