libGDX: Часть 2.1. Архитектура игры на основе scene2d

Та архитектура, что я привёл довольно хороша, но избыточна. Зачем изобретать велосипед? В действительности в libGDX есть пакет scene2d, который позволяет на его основе создать хорошую архитектуру.

scene2d

Пакет scene2d представляет собой классы для реализации графа для двухмерной сцены, которые могут быть полезны для управления группой иерархически связанных актеров (сущностей). Некоторые особенности:

  • Вращение и масштабирование группы объектов. Дочерние объекты работают в своей системе координат, так что родительские преобразования для них прозрачны.
  • Упрощенный 2D рендеринг с использованием SpriteBatch. Каждый объект существует в своей невращаемой нескалируемой системе координат, где 0,0 нижний левый угол объекта.
  • Очень гибкая система событий, позволяющая обрабатывать события родителя перед/после обработки событий дочерних объектов.
  • Система воздействий для лёгкой манипуляции над объектами в любой момент. Воздействия можно объединять для более сложных эффектов.

Главный недостаток scene2d — объекты совмещают и модель, и представление. Поэтому я и не стал сразу рассматривать этот пакет. Такое сцепление логики не позволяет полноценно использовать MVC. Хотя, с другой стороны, всё, что касается объекта, теперь в классе объекта, что довольно удобно.

У scene2d три центровых класса: Actor, Group, Stage.

Stage

Класс Stage являет собой «корневую» группу(Group) куда приложение может добавлять своих актеров. У класса есть своя камера и упаковщик (SpriteBatch). Stage реализует интерфейс InputProcessor и отсылает события дочерним элементам.

Теперь подгоним наше приложение под эти классы. Так как логика и рендеринг объектов теперь в самих объектах, то надобность в классах WorldRenderer и WorldController отпадает. Теперь изменим класс World. Ранее он у нас был контейнером объектов, по сути тоже самое, что и Stage. Так что модифицируем его, унаследуем от Stage и переопределим события нужные:

        @Override
	public boolean touchDown(int x, int y, int pointer, int button) {
		super.touchDown(x, y, pointer, button);

		//передвигаем выбранного актёра
		moveSelected(x,y);

		return true;
	} 
	
	/**
	 * Передвижение выбранного актёра
	 * @param x
	 * @param y
	 */
	private void moveSelected(float x, float y){
		if(selectedActor != null && selectedActor instanceof Player)
		{
			((Player)selectedActor).ChangeNavigation(x,this.getHeight() -y);
		}
	}
	
	/**
	 * Сбрасываем текущий вектор и направление движения
	 */
	private void resetSelected(){
		if(selectedActor != null && selectedActor instanceof Player)
		{
			((Player)selectedActor).resetWay();
		}
	}
	
	@Override
	public boolean touchUp (int x, int y, int pointer, int button) {
		super.touchUp(x, y, pointer, button);
		 resetSelected();
    	return true;
    }
	
	public Actor hit(float x, float y, boolean touchable) {

		Actor  actor  = super.hit(x,y,touchable);
		//если выбрали актёра
		if(actor != null)
			//запоминаем
			selectedActor = actor;
		return actor;

	}

В самом классе ещё по мелочи кое-что добавлено, не хочу на этом останавливаться. Теперь надо модифицировать класс GameScreen. Ну, во-первых, убираем все ненужные ссылки на классы, которые мы удалили. Из класса, отвечающего за рендеринг перетаскиваем текстуры, регионы и их генерацию в этот класс, и камеру. Меняем ключевые методы:

@Override
	public void resize(int width, int height) {
		this.width = width;
		this.height = height;
		world.setViewport(width, height, true);
	}

@Override
	public void render(float delta) {
		Gdx.gl.glClearColor(1, 1, 1, 1);
		Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT);
		world.update(delta);
		world.draw();
	}

world.draw() вызывает отрисовку мира нашего, который унаследован от Stage. Метод будет выполнен иерархически для всех дочерних элементов. Теперь нам надо лишь модифицировать нашего игрока.

Group и Actor

Класс Group является актером который может содержать других актеров. Вращение и масштабирование Group соответствующим образом отражается на его дочерних актерах. Класс Group делегирует рисование и события ввода соответствующим дочерним актерам. Он превращает координаты событий ввода так, что дочерние элементы получают эти координаты в своей собственной системе координат.

Я не буду останавливаться на классе Group, он схож с Actor по функционалу, так что сразу расскажу про Actor. У нас уже был класс Player, унаследуем его от Actor. Для начала перетащим методы по смене направления из контроллера, который у нас за этим раньше следил. Зададим метод hit() для определения того, нажали ли мы на этот объект:

public Actor hit(float x, float y, boolean touchable) {
		//Процедура проверки. Если точка в прямоугольнике актёра, возвращаем актёра.
		return x > 0 && x < getWidth() && y> 0 && y < getHeight()?this:null;
	}

Когда вы вызываете метод hit() у класса Stage, он вызывает метод hit() у корневой группы. Корневая группа вызывает метод hit() у дочерних актеров. Первое возвращаемое значение типа Actor и будет возвращаемым результатом класса Stage.

Так же реализуем метод по отрисовке актёра:

	@Override
	public void draw(SpriteBatch batch, float parentAlfa) {
		
		if (this.equals(world.selectedActor)) {
			batch.setColor(0.5f, 0.5f, 0.5f, 0.5f);
		}
		
		batch.draw(world.textureRegions.get("player"), getX(), getY(),getWidth(), getHeight());
		batch.setColor(1, 1, 1, 1);
	}

Ещё необходимо подписаться на события для обрабокти нажатий на тачскрине, если надо:

addListener(new InputListener() {
	        public boolean touchDown (InputEvent event, float x, float y, int pointer, int button) {
	                return true;
	        }
	        
	        public void touchUp (InputEvent event, float x, float y, int pointer, int button) {
	        }
	});

Когда происходит вызов метод touchDown() для класса Stage, вызывается метод touchDown() для корневой группы. Корневая группа вызывает метод hit() для дочерних актеров. Метод touchDown() вызывается для первого актера, который возвращается методом hit(). Если вызов touchDown() для этого актера возвращает false, это значит, что актер игнорирует событие и процесс продолжается для следующего актера. Если touchDown() возвращает true, актер получает фокус ввода.

Метод touchDown() для класса Group рассылает сообщения touchDown дочерним актерам. Если этот метод переопределен, необходимо вызвать базовый метод super().touchDown(), иначе актеры не получат ни одного события.

Подобно методу hit(), координаты, которые передаются методам-обработчикам управления, даются в системе координат актёра.

Если поле touchable выставлено в false для актёра, класс Group не будет вызывать методы для обработки управления для этого актера.

Если запустите игру, то увидите двух персонажей. При клике на определённого персонажа, он станет полупрозрачным. После этого он будет двигаться в зависимости от того, куда вы нажмёте относительно него.

libgdxlesson2.1

В заключение

Хотя подобная архитектура не позволяет реализовать MVC, но вполне годная. Все объекты крутятся в собственной системе координат. Отрисовка и логика внутри самих объектов, что позволяет реализовывать их независимо друг от друга, да и от самого приложения в целом.

Собственно всё. Можете скачать исходники урока Libgdxtutorial-lesson2.1.rar.