О чем этот пример

При разработке игр на Phaser управление памятью часто становится критически важным, особенно в проектах с большим количеством ресурсов. Игнорирование очистки текстур может привести к утечкам памяти, замедлению работы и, в конечном итоге, к падению производительности. В этой статье разберем практический пример с последовательной загрузкой и выгрузкой 500 текстур, чтобы понять, как правильно использовать `this.textures.remove()` и безопасно переключаться между сценами, не перегружая память браузера.

Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.

Живой запуск

Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.

Исходный код


class Scene1 extends Phaser.Scene {
    constructor() {
        super("scene1");
    }

    preload() {
        
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
for (let i = 1; i<= 500; i++) {
            // this.load.image('apple' + i, 'assets/sprites/apple.png')
            this.load.image('apple' + i, 'assets/pics/uv-grid.jpg')
        }
    }

    create() {

        this.add.text(100, 100, 'Scene 1', { color: '#00ff00', align: 'left' });

        for (let i = 1; i <= 32; i++)
        {
            let x = Phaser.Math.Between(0, 800);
            let y = Phaser.Math.Between(0, 600);

            this.add.image(x, y, 'apple' + i);
        }

        this.input.once('pointerdown', () => {

            for (let i = 1; i<= 500; i++)
            {
                this.textures.remove('apple' + i)
            }

            this.scene.start('scene2');

        });
    }
}

class Scene2 extends Phaser.Scene {
    constructor() {
        super("scene2");
    }

    create() {

        this.add.text(100, 100, 'Textures Cleared', { color: '#00ff00', align: 'left' });

        this.input.once('pointerdown', () => {

            this.scene.start('scene3');

        });

    }
}

class Scene3 extends Phaser.Scene {
    constructor() {
        super("scene3");
    }

    create() {

        this.add.text(100, 100, 'Click to start', { color: '#00ff00', align: 'left' });

        this.input.once('pointerdown', () => {

            this.scene.start('scene1');

        });

    }
}

var gameConfig = {
    type: Phaser.CANVAS,
    scale: {
        width: 800,
        height: 600
    },
    parent: 'phaser-example',
    antialias: true,
    mipmapFilter: 'LINEAR_MIPMAP_LINEAR',
    scene: [Scene3, Scene1, Scene2]
}

new Phaser.Game(gameConfig);

Проблема: накопление текстур в памяти

В примере в Scene1 в методе preload() в цикле загружается 500 изображений. Каждое получает уникальный ключ от 'apple1' до 'apple500'. Это имитирует типичную ситуацию, когда игра загружает много ресурсов для уровня, меню или мини-игры.

Если просто переключать сцены без очистки, все 500 текстур останутся в кэше Phaser (this.textures). При множественных переходах память будет заполняться одними и теми же или разными текстурами, что в итоге приведет к утечке.

for (let i = 1; i<= 500; i++) {
    this.load.image('apple' + i, 'assets/pics/uv-grid.jpg')
}

Решение: ручное удаление текстур через `this.textures.remove()`

Перед переходом на Scene2 в обработчике клика выполняется очистка. Метод this.textures.remove() удаляет текстуру из кэша Phaser по её ключу, освобождая связанную с ней память. Важно вызывать его для каждого загруженного ресурса, который больше не нужен.

В примере это делается в том же цикле, что и загрузка, но уже в create(). После очистки всех 500 текстур происходит переход на следующую сцену.

this.input.once('pointerdown', () => {
    for (let i = 1; i<= 500; i++) {
        this.textures.remove('apple' + i)
    }
    this.scene.start('scene2');
});

Архитектура сцен и порядок переключения

В конфигурации игры сцены передаются в массиве в порядке [Scene3, Scene1, Scene2]. Это означает, что первой инициализируется Scene3 (с текстом 'Click to start'), затем Scene1, и, наконец, Scene2. Однако стартует игра с Scene3, так как она первая в списке. Это демонстрирует, что порядок в массиве влияет на инициализацию, но не на стартовую сцену, если не указано иное.

Переходы организованы по цепочке: Scene3 -> Scene1 -> Scene2 -> Scene3. В Scene1 происходит основная работа с памятью.

scene: [Scene3, Scene1, Scene2]

Практические рекомендации по управлению памятью

1. **Используйте `preload()` для загрузки только необходимых ресурсов.** Не загружайте всё сразу, если в этом нет необходимости.
2. **Чистите текстуры, аудио и другие ресурсы, когда они больше не нужны.** Помимо `this.textures.remove()`, есть методы для звука (`this.sound.removeByKey()`) и данных (`this.cache.remove()`).
3. **Переключайте сцены после очистки.** Как в примере, сначала удалите ресурсы, затем вызовите `this.scene.start()`.
4. **Тестируйте на реальных устройствах.** Инструменты разработчика в браузере (вкладка Memory) помогут отследить утечки.
5. **Рассмотрите использование `Scene.sleep()` и `Scene.wake()`.** Если вы планируете возвращаться к сцене, можно не удалять её ресурсы, а переводить сцену в спящий режим.

Что попробовать дальше

Правильное управление памятью — ключ к стабильной работе игр на Phaser. Регулярно очищайте кэш текстур и других ресурсов при смене игровых состояний. Для экспериментов попробуйте: удалять только часть текстур, использовать динамическую загрузку через this.load.once('complete', ...), или создать менеджер ресурсов, который автоматически выгружает неиспользуемые ассеты.