Дек
23

AndEngine. Кэширование объектов. Пулы

logo_100

Кэширование объектов просто необходимо использовать, делая игры для мобильных устройств.

Делая игры на Flash, я почти никогда не кэшировал объекты, а следовало бы :(. Экономить память и ускорять работу игры следует на любом языке программирования.

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

Самый простой пример объекта, который обязательно нужно кэшировать — это пули. Во время игры из оружия героя может вылететь "миллион" пуль. Если поступить не по-умному, то можно насоздавать "миллион" объектов и засорить память. Через некоторое время за работу возьмётся Сборщик Мусора (Garbage Collector), чтобы осовободить память, занятую уже давно вылетевшими объектами-пулями.

gc1

А работа Сборщика Мусора на Андроиде — это просто беда для игры. Игра будет часто подвисать на 100-300 мсек, а то и дольше. Играть будет не приятно.

Важнейшая цель при создании игры для Андроид — избегать запуска Сборщика Мусора. А для этого не нужно создавать объекты, а потом оставлять их "в небытии". Другими словами, нужно постоянно хранить связь (ссылки) с созданными объектами.

gc2

В AndEngine для этой цели используются Пулы (Pools). Пул — это класс, в котором организован кэш в виде ArrayList.

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

Если же кэш пустой, Пул создаёт новый объект и возвращает нам. И при этом Пул не помещает этот объект в кэш.

Запомните! Мы должны самостоятельно возвращать использованные и ненужные уже нам объекты обратно в кэш.

Вот как это выглядит в коде:

private static final GenericPool<Bullet> _Bullets_Pool = new GenericPool<Bullet>() {

	@Override
	protected Bullet onAllocatePoolItem() {
		return new Bullet();
	}
};

...

/**
* Получаем новую пулю.
* Будет она создана заново или возьмётся из кэша - решает _Bullets_Pool
* Это нас уже не волнует
*/
private Bullet GetNewBullet(final float x, final float y, final float direction)
{
	final Bullet _bullet = _Bullets_Pool.obtainPoolItem();
	_bullet.Init(x, y, direction);
	return _bullet;
}

...
// Где-то в коде, после "смерти" пули возвращаем её в кэш
	_bullet.setVisible(false); //это не обязательно делать здесь.
	_bullet.setIgnoreUpdate(true); //можно в классе пули создать метод, например, kill()
	_Bullets_Pool.recyclePoolItem(_bullet);

Я таким образом кэширую все спрайты в игре, которые используются от уровня к уровню. И не спрайты тоже 🙂

СОВЕТ. Во время игры не обязательно удалять спрайты со Сцены. Ведь при восстановлении спрайта из кэша его снова нужно будет добавить на Сцену. А это лишние "телодвижения".

К тому же, как я писал ранее, удаление спрайта нужно проводить в "new Runnable". Видите слово new? Опять засорение памяти!

Так что я во время игры не удаляю спрайты со Сцены. Перед созданием нового уровня я перебираю все спрайты на Сцене и в зависимости от их Класса отправляю в тот или иной Пул. Потом просто очищаю всю Сцену (методом detachChildren ()).




22 коммент. к записи “AndEngine. Кэширование объектов. Пулы”

  • Посты про AndEngine безумно интересны! Жду продолжения и спасибо большое за то что уже написано!

  • А если думать в теории, можно ли во флэши как-то реализовать собственный класс Pool?

    • Кончено можно. Наверно даже уже есть реализации.

  • onAllocatePoolItem — сработает когда в кеше ничего нет?

  • У меня такой вопрос. У меня есть объекты с разными характеристиками,которые появляются на экране и исчезают. Если использовать Ваш пример,то к примеру, 10 видов разных пуль с разными характеристиками и спрайтами.

    Вопрос в том — использовать ли мне один пул для хранения разных типов объектов(а потом переписать obtainPoolItem (), чтобы он принимал тип объекта, искал внутри только по данному типу из массива со всеми пулями) или использовать 10 пулов, которые хранят каждый свой тип пули?

    Заранее спасибо)

    • Лучше наверно 10 разных пулов 🙂

      • А если 40 и больше видов разных объектов и каждый со своей текстурой, т.е. параметр текстуры должен при создании ему передаться. То как делать, не использовать пулы?

        • Для каждого объекта свой пул делать 🙂 У меня тоже их десятки

  • А как быть например с отрисовкой линий? В onManagedUpdate я делаю вызов нативной функции, в которой обновляю положение линий, затем мне нужно их нарисовать, делаю так

    for (int i = 0;i<line_count;i++)

    {

    Line line = new Line (NP_GetLine (0,i), NP_GetLine (1,i), NP_GetLine (2,i), NP_GetLine (3,i),1.0f);

    attachChild (line);

    }

    И в результате получаю хороший такой вис всей системы. До использования Andengine использовал OpenGL для отрисовки и аналогичный код не тормозил.

    • А что же Вы хотите получить раз в ~20 мс (каждый раз когда перерисовывается спрайт) создавая кучу линий да еще и помещая их на сцену? Разберитесь, зачем, собственно, нужен onManagedUpdate и когда он случается.

      • Решил и дальше OpenGL использовать, к тому же такой подход делает проект почти (часть кода все же на java) кроссплатформенным (игровая логика на С++, рендеринг — GLES).

    • Да и когда Вы исполльзуете AndEngine Вы таки используете OpenGL!

  • Респект за статьи! Хорошее дело затеяли хоть и не благодарное, НО вот с этим не согласен: "...К тому же, как я писал ранее, удаление спрайта нужно проводить в «new Runnable»... " Категорически так не делайте ни Вы и ни кто другой! Удаляйте спрайты в том же потоке где и ваша мэйнактивити и лучше всего это делать в onUpdate — самой сцены. Заведите arraylist со спрайтами и каждый раз его вычищайте, естественно используя while а не for. А с new Runnable — что-то совсем уж новенькое))

    • А примерчик можно? Или ссылку на пример? Там как я понял придется еще использовать потокобезопасный arraylist?

    • На форуме, кстати пишут что надо в runOnUpdateThread детачить спрайт www.andengine.org/forums/...%20detach#p21305 и тут www.andengine.org/forums/...%20detach#p18860

      Хочется разобраться в этом вопросе

      • Ну как таковой пример будет у всех разный, это зависит от того как ваш спрайт реализован. В простейшем случае есть мэйнактивти в ней объявляете arraylist mDetachable (например), в процессе игры или чего там у вас есть, не нужные спрайты кидаете в этот самый arraylist — add (your_sprite). Я объявляю массив как protected и через instance активти просто делаю add в любом месте программы.

        В основном апдейтхендлере сцены делаете примерно следующее:

        while (MyGameActivity.this.mDetachable.size () > 0) {

        Sprite deletable = MyGameActivity.this.mDetachable

        .get (0);

        mScene.detachChild (deletable);

        mDetachable.remove (deletable);

        }

        естественно после вызова этой конструкции более не пытайтесь обращаться к убитым спрайтам. Я ставлю ее в конце функции, а до супер-а или после значения не имеет. По моему, жутко просто и без всяких (new Runnable) Кстати на том же форуме есть это решение.

  • УУУУПСССС ГОСПОДА! DISCLAIMER!

    Все что я написал верно, НО возможна ситуация когда вам просто НЕЗАЧЕМ перекрывать onUpdate сцены. Тогда безусловно runOnUpdateThread, ведь оно теже яйца тока в профиль 😉 Каюсь, каюсь у меня просто такого случая еще не возникало честно-честно, всегда приходилось перекрывать. Простите великодушно!

  • Заметил серьезную проблему с

    private static final GenericPool _Bullets_Pool = new GenericPool ()

    Это перезапуск программы.

    Если просто выйти из игры, а потом снова зайти, то пул не очищается (потому как поле static, а Android не успел еще полностью стереть программу в оперативке).

    В нем остаются старые объекты, причем он их возвращает как и положено.

    Но так как по факту у нас был перезапуск, у нас все объекты движка новые и возвращенный пулом объект не отображается на экране (не говоря уже о засорении памяти устройства, т.к. возвращенные объекты не участвуют по сути в игре и рано или поздно вновь создастся рабочий набор объектов).

    Решение — пересоздавать пулы при каждом запуске программы.

    • Для таких случаев я полностью убиваю процесс с игрой

  • А подскажите, пожалуйста, как в таком случае, при использовании пулов, лучше всего организовать коллизии? Например когда есть игрок, пули, враги, бонусы и надо реализовать коллизии пуль с врагами, врагов с игроком, пуль с бонусами.

    Я на данный момент пытаюсь использовать массивы Enemy[], Bonuses[], Bullets[] и т.д. пересматривая коллизии в onSetValues модификатора MoveModifier, но что-то мне подсказывает, что я в чем-то не прав.

    Благодарю.

    • Подход к коллизиям — в принципе правильный. Проверку можно запускать в методе onManagedUpdate сцены.

Прокомментировать



ЗАДАЙ СВОЙ ВОПРОС