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.

  Категории: java, libgdx, Коддинг
  • http://winners-games.com Vitalik

    Прикольно, я как раз писал статью на тему создания игр.

  • Валера

    Прошу вас пишите каждый туториал с нуля, мне как абсолютному новичку в libGDX трудно разобраться! Сначала дебри, потом на основе этих дебрей основы! Туториалов на Русском нет вообще, этот один из немногих живых блогов, хочется тоже написать “своего бомбермена”.

    • http://suvitruf.ru Suvitruf

      Стараюсь как могу =/
      Если какие-то конкретные вопросы, задавайте. Буду по мере возможностей отвечать (;

  • Pingback: libGDX: Часть 5. Работа с сенсорным экраном и управление | Suvitruf's Blog()

  • shpaker

    Это не туториал, а венегрет. Перепишите его. Чтоб с создания проекта и до презентабельной сцены с перемещающимся актером. А этот пост только портит впечатление о всей серии и запутывает в таком важном вопросе)

    • http://suvitruf.ru Suvitruf

      А что именно не понятно? =/

    • Люк

      По моему понятно пишет все. Этот туториал пользу приносит ;)

  • XoXoX

    Не могу вывести анимацию в классе Player(который extends Actor),выводится только одна картинка(последняя) из спрайта….И в вашем коде почему в классе Player метод draw -batch не внутри batch.begin() -batch.end()??

    • http://suvitruf.ru Suvitruf

      И в вашем коде почему в классе Player метод draw -batch не внутри batch.begin() -batch.end()??

      Если вы юзаете переданный spriteBatch, то не надо. Если хотите ещё какой-то использовать, то для него надо begin и end.

      Не могу вывести анимацию в классе Player(который extends Actor),выводится только одна картинка(последняя) из спрайта

      Из спрайта или атласа?

  • gdf

    Интересная архитектура, спасибо за описание. Но как-то MVC понятнее для меня.

    • http://suvitruf.ru Suvitruf

      Мне тоже MVC ближе)

  • Inferno

    что вы посоветуйте новичку ? MVC или Scene2d ?

    • http://suvitruf.ru Suvitruf

      Сложно сказать.

      Если бы я был новичком, то наверно MVC бы выбрал. Хотя, я сейчас не новичок, но всё равно на MVC делаю, так как он мне ближе (:

  • Inferno

    я думаю лучше иметь длинные зато понятные скрипты чем короткие и непонятные.. спасибо за совет ;)

  • Леха

    Такой вот вопрос-а как изменить имя пакета ? У меня ничего не получается. То есть меняю иия пакета, потом в Манифесте тоже меняю-и опа-в эмуляторе черная метка-не работает. Причем кроме LibGDX во всех приложениях можно менять. Где то там проблема. А где ?

    • http://suvitruf.ru Suvitruf

      Не в LibGDX дело)

      Вы пакеты как меняете? Скрин дерева проекта и то что в манифесте написали покажите.

      • Леха

        Разобрался. Спасибо за Вашу работу.

  • ByKeks

    В урокке не указано, что в классе myGame надо дописать в метод create:

    public void create() {
    GameScreen game = new GameScreen();
    setScreen(game);
    }

    • http://suvitruf.ru Suvitruf

      Упустил из виду =/

  • Tor

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

    • http://suvitruf.ru Suvitruf

      Конкретнее?

  • q3er

    Согласен со многими комментариями по поводу сложности объяснения. Вообще большинство литературы по программированию написаны в таком ключе. Элементарные вещи объясняют так, что поймет только профессионал, а ему это и не нужно, он и так это знает, а новичок которому нужно не поймет. Такое чувство, пишут все только для себя, типо написал, удовлетворил жажду самовыражения и успокоился. Автор, без обид, это не только к тебе относится, да и больше не к тебе а к большинству “писателей-программистов”. Просто уже достало, перевожу свои LWPшки с andengine на libgdx и с элементарными вещами разобраться не могу. По andengine’у хоть книжку написали какие то 2 паренька, там все разжевали. И пара советов на будущее. Первое, если пишешь для нубов, прими как аксиому что нуб==полный ноль (коим и я сам являюсь), и если уж взялся объяснять тему, то объясняй с чистого листа, потому что ища конкретный пример, в лом читать предыдущие статьи и там выискивать начала всех начал. И второе, желательно что бы в конце статьи была простыня с исходниками всего что описано в статье, потому что каждый исходник в эклипс подгружать, да и просто качать устанешь.
    Удачи, и творческих успехов!

    • http://suvitruf.ru Suvitruf

      Просто уже достало, перевожу свои LWPшки с andengine на libgdx и с элементарными вещами разобраться не могу.

      Сложно эту грань найти, писать так, чтобы понимали все =/

      И второе, желательно что бы в конце статьи была простыня с исходниками всего что описано в статье, потому что каждый исходник в эклипс подгружать, да и просто качать устанешь.

      Так их можно удобно с гитхаба же подгружать.

  • http://playnoread.ru shubniggurath

    Скажите в дальнейших уроках используется stage2d или MVC?

    • http://suvitruf.ru Suvitruf

      Нет. Но, если интересно, могу аналог на stage2d сделать.

  • nibbler

    Как Вы считаете, если ли смысл совмещать scene2d и кастомную архитектуру (например на основе MVC), например для самой игры использовать свою, а для интерфейса, игровых контролов, кнопок и проч. использовать scene2d?

    • http://suvitruf.ru Suvitruf

      Не думаю, что есть смысл. Зачем юзать ещё что-то своё кастомное, при этом используя scene2d?
      Либо полностью своё лучше, либо scene2d. Если уж так хочется, то всегда можно свою архитектуру переделать под scene2d (:

  • Dmitry

    Опять про камеру )
    this.cam = new OrthographicCamera(World.CAMERA_WIDTH, World.CAMERA_HEIGHT);
    SetCamera(World.CAMERA_WIDTH / 2f, World.CAMERA_HEIGHT / 2f);
    для чего служат 2 данные строки?

    Я их закоментировал ничего не изменилось!
    Менял ширину длину тот же эффект

    • http://suvitruf.ru Suvitruf

      А вот это уже интересно)

      Сейчас сложно что-то сказать, так как когда эти статьи писал, сам изучал LibGDX )

      • Dmitry

        ну как-то так )

      • Dmitry

        я так понимаю вы это брали сами из примера, но мне кажется и пример на английском неверен, потому-что камера ничего не знает про относительные координаты она работает с абсолютными и весь пересчет лежит на плечах программиста (если он нужен). Изначально камера ставиться в позицию 0,0.

        • http://suvitruf.ru Suvitruf

          На основе примеров я делал свою архитектуру. А эта статья – результат переделки моей архитектуры на scene2d)

          • Dmitry

            я пропробовал менять параметры камеры в Stage припомощи getCamera там работает действительно все работает и вращение и перемещение (с абсолютными координатами)

  • http://tmin10.ru Tmin10

    Спасибо за уроки, но вы бы устранили ошибки в коде.
    Для тех, у кого с новой версией либы ничего не рисует, стоит посмотреть на метод draw класса Player, правильная сигнатура теперь такая:
    draw(Batch batch, float parentAlfa)

  • Gavolot

    Перейдя с Love2D на java, пишу игру на Game->Screen системе. Я явно не профессионал) однако эта система мне реально ближе, почитал тутор и комментарии. Тут дело не в том, что автор для себя пишет. Просто, если он использует систему контроллеров, то оно и понятно почему все так не развернуто.
    Как разберусь с подобным, обязательно подумаю о том как бы написать статью на эту тему. Как лично сам с этим работал, без претензий на неправильность подхода конечно) Если честно, подводя итоги, хочу сказать, что часто не могу понять людей, которым MVC ближе. Все-же разделение scene2D более “по человечески” правильно. Для каждого Screen свой Stage рисующий объекты. унаследовав их Actor пришлось писать еще свой “фундаментальный Actor”, а потом все пошло довольно хорошо. Например логика столкновения корабля с планетами я сделал прямо в корабле. Конечно мое решение мягко говоря возможно корявое, само исполнение я имею ввиду.

    protected void checkCollision(){
    if(this.getStage() != null){
    for(int i = 0; i<this.getStage().getActors().size; i++){
    @SuppressWarnings("rawtypes")
    Class nameClass = this.getStage().getActors().get(i).getClass();
    Planet planet;
    if(nameClass == Planet.class){
    planet = (Planet)this.getStage().getActors().get(i);
    if(AddMath.checkCircularCollision(this.getX(), this.getY(),
    planet.getX(), planet.getY(), this.getWidth()/3, planet.getWidth()/3)){
    if(this.getTeam() != planet.getTeam()){
    planet.setShips(planet.getShips()-this.getHealth());
    this.setDisposed(true);
    }
    else {
    planet.setShips(planet.getShips()+this.getHealth());
    this.setDisposed(true);
    }
    }

    }
    }
    }
    }

    Однако так как-то логичнее же, что такие вещи делаются внутри объекта и потом нигде не мазолят глаза) А сами уровни. Например уровень с рандомным расставлением сделать в RandomScreen, а для компаний уже сделать отдельные LevelOneScreen, LevelTwoScreen… и там вести всю особенную для этих уникальных уровней работу. Конечно если уровни именно предустановленные, а не сделанные в редакторе) Это уже другой вопрос. Это мне напоминает логику работы в том же конструкторе, только все как на ладоне в отличии от того же Game-maker.
    P.S. извините за мой русский и это не отговорка, что я пишу ночью. Просто я такой, какой есть) Безграмотный сами знаете кто.

    • Gavolot

      А вот спросить все хочу. Правильно ли я делаю, что все регионы текстуры загружаю в самом Game и пихаю их в статические методы класса ResourseControl, там в нем ассоциативный массив из которого я и вытягиваю уже загруженные регионы по имени. Не выйдет ли мне это боком потом? Или наоборот нормально, что не нужно брать регион и загружать все-время при создании объекта?

      • http://suvitruf.ru Suvitruf

        Все регионы и текстуры я сам гружу в Game один раз, а потом уже к ним обращаюсь. Проблемы не было, так как у меня графика 8-bit. По-сему, памяти они жрали не так много. Если же у вас HD текстуры, то при огромном их количестве и единовременной загрузке может случиться оверхед, приложение просто упадёт =/

  • Никита

    А есть ли возможность сделать так чтобы каждый актер перемещался: нажимаешь на одного, другим пальцем на другого и тащищ каждым пальцем каждого куда нужно? Просто сам пишу игру где нужно сделать одновременное перетаскивание объектов пальцами.

    • http://suvitruf.ru Suvitruf

      Ну да. Мультитач же поддерживается.

      • Никита

        Да это так. Но когда дело доходит до использования touchDragged (использую MVC) , то активным касанием будет последнее. То есть если мы проведем сначала одним пальцем ( pointer 0) потоп другим (pointer 1), то активно будет pointer=1. Не подскажите как можно сделать чтобы была обработка нескольких пальцев на touchDragged?

  • FatalError

    Автор, ты молодец, но без обид… У меня вопрос, на какой уровень программировантия расчитан материал? Если для учеников, то за каким у тебя есть фразы “кое-что добавлено, не хочу на этом останавливаться”, есть целые строки кода без обьяснений, за чем там эта строка, что она делает, что будет если ее удалить или перенести в другое место, как ее можно еще задействовать … Если на более проффесиональных програмеров, то на кой им учебник азов? Это как учителя по математике учить математики, а обезьянам пытаться обьяснить квантовую физику… Был очень рад найти подобный учебный материал, но чем дальше в лес тем больше дров…