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

Phaser предлагает гибкую систему плагинов и загрузчиков, позволяющую расширять его функциональность под свои нужды. В этой статье мы разберем практический пример создания пользовательского типа файла для загрузчика. Вы научитесь создавать плагин, который регистрирует новый метод `.leet()` для `this.load` и автоматически преобразует загруженный текст в leet-speak (хакерский сленг). Этот паттерн полезен для предобработки любых данных (например, шифрования, сжатия, парсинга специфичных форматов) прямо в процессе загрузки, не засоряя логику основной сцены.

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

Живой запуск

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

Исходный код


class LeetSpeak {

    constructor ()
    {
        this.alphabetBasic = {
            'a': '4',
            'b': '8',
            'e': '3',
            'f': 'ph',
            'g': '6', // or 9
            'i': '1', // or |
            'o': '0',
            's': '5',
            't': '7' // or +
        };

        this.alphabetAdvanced = {
            'c': '(', // or k or |< or /<
            'd': '<|',
            'h': '|-|',
            'k': '|<', // or /<
            'l': '|', // or 1
            'm': '|\\/|',
            'n': '|\\|',
            'p': '|2',
            'u': '|_|',
            'v': '/', // or \/
            'w': '//', // or \/\/
            'x': '><',
            'y': '\'/'
        };

        this.alphabetReversed = [
            [/(\|\\\/\|)/g, 'm'],
            [/(\|\\\|)/g, 'n'],
            [/(\()/g, 'c'],
            [/(<\|)/g, 'd'],
            [/\|-\|/g, 'h'],
            [/(\|<)/g, 'k'],
            [/(\|2)/g, 'p'],
            [/(\|_\|)/g, 'u'],
            [/(\/\/)/g, 'w'],
            [/(><)/g, 'x'],
            [/(\|)/g, 'l'],
            [/(\'\/)/g, 'y'],
            [/(\/)/g, 'v'],
            [/(1)/g, 'i'],
            [/(0)/g, 'o'],
            [/(3)/g, 'e'],
            [/(4)/g, 'a'],
            [/(5)/g, 's'],
            [/(6)/g, 'g'],
            [/(7)/g, 't'],
            [/(8)/g, 'b'],
            [/(ph)/g, 'f'],
        ];
    }

    convert (text, useAdvanced = 'n')
    {
        for (let i = 0; i < text.length; i++)
        {
            let alphabet;
            let letter = text[i].toLowerCase();

            if (useAdvanced.toLowerCase() === 'y')
            {
                // Use advanced l33t speak alphabet
                alphabet = (this.alphabetBasic[letter]) ? this.alphabetBasic[letter] : this.alphabetAdvanced[letter];
            }
            else
            {
                // Use basic l33t speak alphabet
                alphabet = this.alphabetBasic[letter];
            }

            if (alphabet)
            {
                text = text.replace(text[i], alphabet);
            }
        }

        return text;
    }
}

class LeetTextFile extends Phaser.Loader.FileTypes.TextFile {

    constructor (loader, key, url, xhrSettings)
    {
       super(loader, key, url, xhrSettings);
    }

    onProcess ()
    {
        //  Leetify it
        this.leet = new LeetSpeak();

        this.data = this.leet.convert(this.xhrLoader.responseText);

        this.onProcessComplete();
    }

}

class LeetSpeakPlugin extends Phaser.Plugins.BasePlugin {

    constructor (pluginManager)
    {
        super(pluginManager);

        this.leet = new LeetSpeak();

        //  Register our new Loader File Type
        pluginManager.registerFileType('leet', this.leetTextFileCallback);
    }

    convert (text)
    {
        return this.leet.convert(text);
    }

    leetTextFileCallback (key, url, xhrSettings)
    {
        if (Array.isArray(key))
        {
            for (var i = 0; i < key.length; i++)
            {
                //  If it's an array it has to be an array of Objects, so we get everything out of the 'key' object
                this.addFile(new LeetTextFile(this, key[i]));
            }
        }
        else
        {
            this.addFile(new LeetTextFile(this, key, url, xhrSettings));
        }

        return this;
    }

}

const config = {
    type: Phaser.AUTO,
    parent: 'phaser-example',
    width: 800,
    height: 600,
    plugins: {
        global: [
            { key: 'LeetSpeakPlugin', plugin: LeetSpeakPlugin, start: true }
        ]
    },
    scene: {
        preload: preload,
        create: create
    }
};

let game = new Phaser.Game(config);

function preload ()
{
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
    this.load.leet('story', 'assets/text/hibernation.txt');
}

function create ()
{
    // let leet = this.plugins.get('LeetSpeakPlugin');
    // let txt = leet.convert("Hello World! Let's hack the gibson!");

    let txt = this.cache.text.get('story');

    this.add.text(4, 4, txt, { font: '16px Courier', fill: '#00ff00' });
}

Анатомия пользовательского типа файла в Phaser

Чтобы добавить в загрузчик Phaser поддержку нового типа файлов, необходимо создать три основных компонента: 1. **Класс-обработчик данных**, наследующий от Phaser.Loader.File (или его специализированных потомков, как TextFile). 2. **Класс плагина**, наследующий от Phaser.Plugins.BasePlugin. Он будет управлять жизненным циклом и регистрировать новый тип файла. 3. **Функция обратного вызова (callback)**, которая сообщает загрузчику, как создавать экземпляры нашего файла.

В нашем примере за основу взят Phaser.Loader.FileTypes.TextFile, который уже умеет загружать текстовые файлы через XHR. Наша задача — перехватить момент, когда данные загружены, но еще не помещены в кеш (onProcess), и модифицировать их.

class LeetTextFile extends Phaser.Loader.FileTypes.TextFile {
    onProcess () {
        this.leet = new LeetSpeak();
        this.data = this.leet.convert(this.xhrLoader.responseText);
        this.onProcessComplete();
    }
}

Метод onProcess вызывается автоматически после успешной загрузки. Мы создаем экземпляр переводчика LeetSpeak, преобразуем сырой ответ (this.xhrLoader.responseText) и сохраняем результат в this.data. Важно не забыть вызвать this.onProcessComplete(), чтобы сигнализировать загрузчику об окончании обработки.

Создание и регистрация плагина

Плагин служит точкой входа для нашего функционала. Он инстанцируется при старте игры (если указан в конфигурации с start: true) и делает две ключевые вещи: - Предоставляет метод convert для прямого преобразования строк в коде. - Регистрирует новый тип файла в загрузчике с помощью pluginManager.registerFileType.

class LeetSpeakPlugin extends Phaser.Plugins.BasePlugin {
    constructor (pluginManager) {
        super(pluginManager);
        this.leet = new LeetSpeak();
        pluginManager.registerFileType('leet', this.leetTextFileCallback);
    }
    convert (text) { return this.leet.convert(text); }
}

Метод registerFileType принимает два аргумента: строковый ключ (имя нового метода загрузчика, в нашем случае 'leet') и функцию обратного вызова. Эта callback-функция будет вызвана, когда мы в коде сцены напишем this.load.leet(...).

Функция leetTextFileCallback ответственна за создание экземпляров LeetTextFile и их добавление в очередь загрузчика через this.addFile(...). Она также обрабатывает как одиночные загрузки, так и массивы файлов, следуя конвенциям API Phaser.

leetTextFileCallback (key, url, xhrSettings) {
    if (Array.isArray(key)) {
        for (var i = 0; i < key.length; i++) {
            this.addFile(new LeetTextFile(this, key[i]));
        }
    } else {
        this.addFile(new LeetTextFile(this, key, url, xhrSettings));
    }
    return this;
}

Подключение плагина и использование в сцене

Плагин подключается глобально в конфигурации игры. Это значит, что он будет доступен из любого места через this.plugins.get('LeetSpeakPlugin').

const config = {
    plugins: {
        global: [
            { key: 'LeetSpeakPlugin', plugin: LeetSpeakPlugin, start: true }
        ]
    },
    scene: { preload: preload, create: create }
};

В методе preload сцены мы можем использовать наш новый тип файла. Метод .leet() теперь доступен у объекта this.load. Его сигнатура идентична стандартным методам загрузки: первый параметр — ключ для кеша, второй — URL.

function preload () {
    this.load.leet('story', 'assets/text/hibernation.txt');
}

После завершения загрузки преобразованный текст будет доступен в текстовом кеше под ключом 'story'. Мы можем получить его и отобразить, как любой другой загруженный ресурс.

function create () {
    let txt = this.cache.text.get('story');
    this.add.text(4, 4, txt, { font: '16px Courier', fill: '#00ff00' });
}

Кроме того, плагин можно использовать напрямую для преобразования строк в реальном времени, без загрузки файлов.

let leetPlugin = this.plugins.get('LeetSpeakPlugin');
let hackedText = leetPlugin.convert("Hello World!");

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

Создание пользовательского типа файла в Phaser — мощный паттерн для инкапсуляции логики предобработки данных. Вы можете адаптировать этот пример для загрузки и автоматического парсинга JSON в специфичные модели игровых объектов, декодирования бинарных форматов, применения простых шифров к игровым сохранениям или конфигурациям. Для экспериментов попробуйте: 1. Создать тип файла, который загружает CSV и сразу преобразует его в массив объектов. 2. Реализовать плагин, который сжимает/распаковывает текстовые данные (например, используя простой RLE) прямо в процессе загрузки. 3. Добавить в callback-функцию поддержку дополнительных параметров конфигурации, которые будут передаваться в ваш класс-обработчик файла.