libGDX. Урок 1. Описание игры, инициализация игровых объектов.

Описание игры

Игроку будет предоставлено на выбор три двери. За двумя будут скрываться козы, а за оставшейся – автомобиль! Задача игрока – выиграть автомобиль, отгадав за какой дверью он скрыт. Выбрав дверь, откроется другая (с козой). После чего уточняем у игрока: хочет ли он поменять свой выбор или нет. Повторив или поменяв свой выбор, игрок откроет дверь и узнает правильно ли он поступил.

 
Структурная схема рабочего процесса нашей игры выглядит следующим образом:

блок схема игры на android studio

Классы

Мы создадим несколько классов для нашего с вами удобства в папке core:

  • Door – класс для отображения двери, по которой игрок сможет нажимать для её открытия;
  • InputManager – этот класс будет обрабатывать ввод информации от игрока (нажатия) и обновление соответствующих логических структур игры;
  • TextManager – этот класс будет обрабатывать текстовые сообщения и их вывод на экран игроку;
  • GameManager — этот класс обрабатывает инициализацию/реинициализацию игровых объектов и различной логики игры.

Создаём экран игры. Реализуем класс Door.

Выполняем следующее руководство для реализации класса Door:

  • Создайте новый пакет в папке core и назовите его com.door.gameobjects;

new package android studio

  • В только что созданном пакете создайте Java Class и назовите его DoorClass;

new java class in package android studio

java class name

  • В файле DoorClass введите следующий код:
package com.door.gameobjects;

        import com.badlogic.gdx.graphics.g2d.Sprite;
        import com.badlogic.gdx.graphics.g2d.SpriteBatch;
        import com.badlogic.gdx.math.Vector2;

public class DoorClass {

    public Sprite openSprite; //создаём спрайт для отображения открытой двери
    public Sprite closeSprite; //создаём спрайт для отображения закрытой двери
    public boolean isOpen = false; //Переменная для определения, в каком положении дверь находится в данный момент
    public Vector2 position = new Vector2(); // позиция двери
    //размеры двери
    public float height; //высота двери
    public float width; //ширина двери

    public void render(SpriteBatch batch){
        if(isOpen){
            openSprite.draw(batch);
        }
        else{
            closeSprite.draw(batch);
        }
    }
}

Не забудьте, что вы могли назвать свой пакет по-другому, вам никто этого не запрещал. Поэтому может вылезти ошибка при копировании всего текста сверху (чтобы такого не возникало, копируйте весь код за исключением первой строки «package…»). В любом случае Android Studio укажет вам на ошибку. Также импортируйте недостающие библиотеки при необходимости (Alt+Enter).

 
В этом классе мы объявили два объекта класса Sprite, которые будут хранить в себе изображения открытой и закрытой дверей. Также мы объявили переменную типа Boolean с именем isOpen, чтобы хранить в ней информацию о статусе двери (открыта/закрыта) и инициализировали её значением false (закрыта). Переменная position нужна нам для указания места, где нам необходимо отобразить дверь. И создали две переменные для хранения размеров двери (height, width). Плюс мы объявили метод render, который в качестве параметра должен будет принять объект класса SpriteBatch, для вывода изображения на экран, основываясь на значении переменной isOpen.

Всякий раз, когда мы хотим отобразить что-то на экране, мы используем экземпляр класса SpriteBatch, чтобы сделать это. SpriteBatch выполняет задачу по загрузке текстур в графический процессор для рисования. Загрузка текстур – дорогостоящая по вычислительным ресурсам операция, а SpriteBatch в свою очередь оптимизирует это путём группировки текстур их в графический процессор пакетами.

Реализация класса GameManager

Создайте новый пакет, как мы это сделали ранее, и назовите его com.door.managers. Внутри этого пакета создайте новый Java Class с именем GameManager. Введите следующий код в файл GameManager.java:

package com.door.managers;

        import com.door.gameobjects.DoorClass;
        import com.badlogic.gdx.Gdx;
        import com.badlogic.gdx.graphics.Texture;
        import com.badlogic.gdx.graphics.g2d.Sprite;
        import com.badlogic.gdx.graphics.g2d.SpriteBatch;
        import com.badlogic.gdx.utils.Array;

public class GameManager {

    static Array<DoorClass> doors ; // массив из трёх дверей
    static Texture doorTexture; // текстура для изображения двери
    private static final float DOOR_RESIZE_FACTOR = 2500f;
    private static final float DOOR_VERT_POSITION_FACTOR = 3f;
    private static final float DOOR1_HORIZ_POSITION_FACTOR = 7.77f;
    private static final float DOOR2_HORIZ_POSITION_FACTOR = 2.57f;
    private static final float DOOR3_HORIZ_POSITION_FACTOR = 1.52f;
    static float width,height;

    public static void initialize(float width,float height){
        GameManager.width = width;
        GameManager.height = height;
        doorTexture = new Texture(Gdx.files.internal("door_close.png"));
        initDoors();
    }

    public static void renderGame(SpriteBatch batch){
        //Отобразить(нарисовать) каждую дверь
        for(DoorClass door : doors)
            door.render(batch);
    }

    public static void dispose() {
        //удалить (уничтожить) текстуру двери, чтобы исключить перегрузку памяти устройства
        doorTexture.dispose();
    }

    public static void initDoors(){
        doors = new Array<DoorClass>();

        //создать экземляр каждой двери и добавить их в массив doors
        for(int i=0;i<3;i++){
            doors.add(new DoorClass());
        }

        //установить позиции для отображения каждой двери
        doors.get(0).position.set(width/DOOR1_HORIZ_POSITION_FACTOR,height/DOOR_VERT_POSITION_FACTOR);
        doors.get(1).position.set(width/DOOR2_HORIZ_POSITION_FACTOR,height/DOOR_VERT_POSITION_FACTOR);
        doors.get(2).position.set(width/DOOR3_HORIZ_POSITION_FACTOR,height/DOOR_VERT_POSITION_FACTOR);

        for(DoorClass door : doors){
            // создаём для каждой двери нашего массива спрайт для её отображения
            door.closeSprite = new Sprite(doorTexture);
            door.width = door.closeSprite.getWidth()* (width/DOOR_RESIZE_FACTOR);
            door.height = door.closeSprite.getHeight()* (width/DOOR_RESIZE_FACTOR);
            door.closeSprite.setSize(door.width, door.height);
            door.closeSprite.setPosition(door.position.x,door.position.y);
        }

    }

}

Сначала мы создали массив объектов DoorClass, который будут содержать в себе экземпляры трех дверей. Затем мы объявили текстуру (Texture) изображения двери. После чего объявили переменную DOOR_RESIZE_FACTOR, которая нам понадобится для изменения размеров двери под разные Android-устройства. Затем объявили переменные, относящиеся к позиционированию дверей на устройстве. После этого создали метод initialize(), который собираемся использовать для реализации логики инициализации. Этот метод в качестве параметров принимает переменные width и height, которые на деле окажутся размерами области просмотра (viewport) нашей игры.

Далее мы создали экземпляр объекта doorTexture и инициализировали его изображением файла door_close.png:

doorTexture = new Texture(Gdx.files.internal("door_close.png"));

Все файлы необходимо скачать внизу под уроком и скопировать в папку assets вашего проекта:

assets folder android studio

Далее мы создали метод initDoors(), где написали логику инициализации дверей. В этом методе, после добавления объектов дверей в общий массив doors, мы устанавливаем позицию каждой двери:

//установить позиции для отображения каждой двери
doors.get(0).position.set(width/DOOR1_HORIZ_POSITION_FACTOR,height/DOOR_VERT_POSITION_FACTOR);
doors.get(1).position.set(width/DOOR2_HORIZ_POSITION_FACTOR,height/DOOR_VERT_POSITION_FACTOR);
doors.get(2).position.set(width/DOOR3_HORIZ_POSITION_FACTOR,height/DOOR_VERT_POSITION_FACTOR);

Вам необязательно использовать те же значения, что и у меня. Можете попробовать свои и посмотреть, что произойдёт.

Затем мы устанавливаем спрайт для каждой двери и её размеров:

for(DoorClass door : doors){
    // создаём для каждой двери нашего массива спрайт для её отображения
    door.closeSprite = new Sprite(doorTexture);
    door.width = door.closeSprite.getWidth()* (width/DOOR_RESIZE_FACTOR);
    door.height = door.closeSprite.getHeight()* (width/DOOR_RESIZE_FACTOR);
    door.closeSprite.setSize(door.width, door.height);
    door.closeSprite.setPosition(door.position.x,door.position.y);
}

В методе renderGame() к каждому объекту класса DoorClass  мы применяем метод render() этого же класса, который в качестве параметра принимает объект класса SpriteBatch. В методе dispose() избавляемся от всех созданных текстур, дабы не перегружать память устройства.

 

Разница между текстурой (texture) и спрайтом (sprite)

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

texture - sprite difference

Вот в общем-то и краткое объяснение.

Реализуем класс Door

Вот код для нашего основного класса – Door:

package com.myfirstgdx.game;

        import com.badlogic.gdx.ApplicationAdapter;
        import com.badlogic.gdx.Gdx;
        import com.badlogic.gdx.graphics.GL20;
        import com.badlogic.gdx.graphics.OrthographicCamera;
        import com.badlogic.gdx.graphics.g2d.SpriteBatch;
        import com.door.managers.GameManager;

public class Door extends ApplicationAdapter {

    private OrthographicCamera camera;
    private SpriteBatch batch;
    //переменные для хранения значений размеров
    //высоты и ширины области просмотра нашей игры
    private float w,h;

    @Override
    public void create() {
        //узнаём размеры для области просмотра
        w = Gdx.graphics.getWidth();
        h = Gdx.graphics.getHeight();
        //создаём экземпляр камеры и устанавливаем размеры области просмотра
        camera = new OrthographicCamera(w,h);
        // центруем камеру w/2,h/2
        camera.setToOrtho(false);
        batch = new SpriteBatch();
        //инициализируем игру
        GameManager.initialize(w,h);
    }

    @Override
    public void dispose() {
        //уничтожаем batch и вызываем метод dispose класса GameManager
        batch.dispose();
        GameManager.dispose();
    }

    @Override
    public void render() {
        // Очищаем экран
        Gdx.gl.glClearColor(1, 1, 1, 1);
        Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
        //устанавливаем вид spriteBatch созданному нами объекту camera
        batch.setProjectionMatrix(camera.combined);
        //отрисовываем игровые объекты путём вызова метода renderGame класса GameManager
        batch.begin();
        GameManager.renderGame(batch);
        batch.end();
    }

    @Override
    public void resize(int width, int height) {
    }

    @Override
    public void pause() {
    }

    @Override
    public void resume() {
    }

}

Так как наша игра будет в двух измерениях (2D), мы будем использовать класс OrtographicCamera. Камера – это практически глаз, через который мы будем смотреть на наш игровой мир. Используем SpriteBatch для помощи отрисовки наших спрайтов. Далее мы узнаём размеры устройства, на котором запустится игра, чтобы в последующем их использовать для установки области просмотра:

//узнаём размеры для области просмотра
w = Gdx.graphics.getWidth();
h = Gdx.graphics.getHeight();

Далее мы создаём экземпляр камеры и устанавливаем для него область просмотра. После чего центруем область просмотра width(w)/2, height(h)/2 следующей строкой:

camera.setToOrtho(false);

Далее вызываем метод initialize() класса GameManager для установки игровых объектов и их свойств. В методе render() мы сначала очищаем экран белым цветом следующими строками:

// Очищаем экран
Gdx.gl.glClearColor(1, 1, 1, 1);
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);

Затем передаём SpriteBatch координаты нашей камеры для визуализации:

//устанавливаем вид spriteBatch созданному нами объекту camera
batch.setProjectionMatrix(camera.combined);

Метод renderGame() класса GameManager вызывается между методами begin() и end() для отображения наших изображений (как и в принципе должна происходить отрисовка SpriteBatch).

 
А в методе dispose() мы уничтожаем созданный нами экземпляр batch класса SpriteBatch и вызываем метод dispose() класса GameManager для очистки памяти устройства после выхода игрока из нашей игры. Если вы сейчас запустите на своём устройстве или эмуляторе это приложение, то вот что вы должны увидеть:

наша игра в libGDX

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

Скачать исходники