Створення Zero Player Game, використовуючи libgdx

Ідея

 
     
  1. Ігровий простір — картате поле обмежене рамкою
  2.  
  3. Існуючі типи клітин:
       
    • Порожня клітка — білий
    •  
    • Стіна — чорний

    •  
    • Звір — червоний
    •  
    • Слід — коричневий
    •  
    • Будинок — зелений
    •  
  4.  
  5. Переміщення звіра залишає незникаючий слід
  6.  
  7. При запуску генерується лабіринт
  8.  
  9. Звір знає стан сусідніх клітин і на підставі цього будує карту при переміщенні
  10.  
  11. При переміщенні звір витрачає сили, які відновлюються в будинку (+5) або на будь-якій клітині (+1)
  12.  
  13. При зіткненні звірі об'єднуються в зграї (будинки переносяться в сусідні точки), якщо декілька звірів одночасно відпочивають в будинку їх карти об'єднуються
  14.  
  15. (Не реалізовано) Різні «клани» випадковим чином об'єднуються або воюють
  16.  
  17. (Не реалізовано) Генетичний алгоритм для різних поводжень звірів, випадково перемішуються при розмноженні (+ мутації), більш перспективний вид виживе у війнах
  18.  
 
 
Чому 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 .
 
Якщо когось зацікавило, напишу продовження.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.