libGDX. Урок 15. Random и оптимизация кода. Используем паттерн Object Pool

Random

В предыдущем уроке мы реализовали создание нескольких мячей и их падение сверху. Но появляются они все в одном и том же месте. Нам нужно усложнить игроку жизнь, так что давайте сделаем так, чтобы мячи падали с разных мест. Для этого нам понадобится класс Random. Создадим в классе SpawnManager экземпляр класса Random:

static List<Integer> removeIndices = new ArrayList<Integer>(); // хранит индексы шаров, которые нужно удалить
static Random random = new Random(); // объект класса Random для генерации случайных чисел

В данный момент в методе createNewBall() установка позиции мяча по координате x происходит в точке 0. Исправим это следующим образом (красную строку удаляем, зеленую добавляем):

ball.position.set(0.0f, height-ball.ballSprite.getHeight());
ball.position.set(random.nextInt((int) (width - ball.ballSprite.getWidth())), height-ball.ballSprite.getHeight());

 
Метод nextInt() класса Random принимает в качестве аргумента целочисленное значение (int). Метод выдает случайное значение между 0 и тем, которое принял метод в качестве аргумента. Например, если мы передадим 5 в качестве аргумента методу (nextint(5)), то он вернет нам случайное значение между 0 и 5. Мы же используем значение width – ball.ballSprite.getWidth(). В таком случае гарантируется появление мяча между левым(0) и правым краями экрана без его ухода за пределы экрана. Теперь можно запустить нашу игру и посмотреть, что получилось:

игра android studio libgdx

Оптимизация кода. Паттерн проектирования Object Pool

Чтобы оптимизировать наш код и приложение, мы собираемся воспользоваться стратегией, которая называется pooling. Согласно нынешнему коду время от времени мы будем создавать и удалять объекты. В долгосрочной перспективе (при длительной игре) это может привести к проблемам с памятью или производительностью, особенно на мобильных устройствах. Ключевой же особенностью стратегии pooling является повторное использование.

Для того, чтобы понять, как это реализовано, представьте себе сумку полную футбольных мячей. Всякий раз, когда ребенок хочет поиграть в мяч, он берет один из мешка. Когда он закончит играть с мячом, он положит его обратно. Следующий ребенок сделает тоже самое. Так же и мы собираемся сделать. Только в нашем случае не мешок, а pool. Всякий раз, когда нам нужно отобразить мяч в игре, мы будем обращаться к pool’у. А pool на наш запрос будет выдавать нам мяч из своей коллекции.

В случае, когда в pool’е нету свободных мячей, то будет создан новый объект мяча. Как только он нам не нужен будет, мы уберем его обратно в pool. Это увеличит производительность нашей игры так как мы не создаём новые объекты, а значит и не выделяем под них память. В libGDX существует класс Pool, которым мы и воспользуемся для реализации описанной выше стратегии. Скопируйте следующий код в класс SpawnManager:

private final static Pool<Ball> ballPool = new Pool<Ball>() {
    // этот метод запускается, когда требуется создание нового экземпляра класса Ball, т.е. когда pool пуст, а объект требуется

    @Override
    protected Ball newObject() {
        Ball ball = new Ball();
        // создание экземпляра спрайта мяча
        ball.ballSprite = new Sprite(ballTexture);
        return ball;
    }
};

 
Переменная ballPool – это объект класса Pool. Он будет создавать новый объект мяча, когда pool пуст и возвращать отработанный объект мяча из коллекции в противном случае. Мы переопределяем метод newObject(), который вызывается, когда требуется объект из pool’а и он пуст. Таким образом создается новый объект и возвращается вызвавшему коду. Здесь мы создаем новый объект класса Ball и его спрайт. Поэтому давайте переименуем метод createNewBall() в метод resetBall() и отредактируем в следующем виде:

public static Ball resetBall(Ball ball){

    Ball ball = new Ball();
    ball.ballSprite = new Sprite(ballTexture);
    ball.ballSprite.setSize(ball.ballSprite.getWidth() *(width/BALL_RESIZE_FACTOR), ball.ballSprite.getHeight()* (width/BALL_RESIZE_FACTOR));
    ball.ballSprite.setSize(ball.ballSprite.getTexture().getWidth()*(width/BALL_RESIZE_FACTOR),ball.ballSprite.getTexture().getHeight()*(width/BALL_RESIZE_FACTOR));
    ball.position.set(random.nextInt((int) (width - ball.ballSprite.getWidth())), height-ball.ballSprite.getHeight());
    ball.velocity.set(0, 0);
    ball.isAlive=true;
    Vector2 center = new Vector2();
    // установка центра вектора в центре спрайта мяча
    center.x=ball.position.x + (ball.ballSprite.getWidth()/2);
    center.y=ball.position.y + (ball.ballSprite.getHeight()/2);
    ball.ballCircle = new Circle(center, (ball.ballSprite.getHeight()/2));
    return ball;
}

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

android studio libgdx приложение игра

Поэтому теперь мы устанавливаем размеры мяча относительно текстуры. В методе run() нужно заменить код, где мы создаём новый мяч:

public static void run(Array<Ball> balls){
    // если счетчик delaycounter превысил значение в delayTime
    if(delayCounter>=delayTime){
        Ball ball= ballPool.obtain(); // получаем мяч из пула мячей
        resetBall(ball); // переиницциализация мяча
        balls.add(ball); // добавляем мяч к списку
        balls.add(createNewBall()); // создать новый мяч
        delayCounter=0.0f;// сбросить счетчик задержки
    }
    else{
        delayCounter += Gdx.graphics.getDeltaTime();
        //в противном случае аккумулируем счетчик задержки
    }
}

Теперь нужно добавить строку для очистки пула перед запуском игры в методе initialize() этого же класса:

delayCounter=0.0f; // сброс счетчика задержки
ballPool.clear(); // очистка pool'а объектов

Когда приходит время для появления нового мяча, мы делаем запрос на объект мяча из pool’а, переинициализируем его и добавляем в активный список мячей. Теперь в методе cleanup() вместо обычного удаления объектов мяча, мы будем возвращать их в pool с помощью метода free():

// Удаляем объекты ball из массива, согласно индексу
for (int i = 0 ; i<removeIndices.size(); i++) {
    balls.removeIndex(i);
    Ball ball= balls.removeIndex(i);
    ballPool.free(ball);// return the ball back to the pool
}

Если у вас есть желание посмотреть, как много новых объектов ball было создано в течение игры можете добавить строку вывода информации внутрь метода newObject():

@Override
protected Ball newObject() {
    Ball ball = new Ball();
    // создание экземпляра спрайта мяча
    ball.ballSprite = new Sprite(ballTexture);
    System.out.println("Hello! I am a new one!"); // для проверки
    return ball;
}

 
У меня получилось 4 мяча в pool’е:

object pool android studio libgdx

Вот алгоритм появления новых мячей и мячей из пула при необходимости:

object pool android studio libgdx

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

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