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

Создание игровых интерфейсов, редакторов уровней или визуальных эффектов часто требует отрисовки простых геометрических фигур. Встроенная система Graphics в Phaser мощна, но для базовых форм есть более простой способ — готовые Game Object'ы: `circle`, `rectangle`, `ellipse`, `star` и `line`. Эта статья разберет практический пример интерактивного рисования, где вы научитесь создавать, изменять, перемещать фигуры и динамически менять их цвет прямо во время выполнения. Вы получите готовый код-конструктор, который можно адаптировать под свои проекты.

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    swatchData;
    color = new Phaser.Display.Color();
    cursors;
    index = 0;
    shape = null;
    current = 1;
    isDown = false;
    shapes = [];

    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.image('bg1', 'assets/skies/gradient4.png');
        this.load.image('dp', 'assets/swatches/gradient-palettes.png');
    }

    create ()
    {
        this.add.image(0, 0, 'bg1').setOrigin(0);

        //  Create the swatch
        const src = this.textures.get('dp').getSourceImage();
        this.swatchData = this.textures.createCanvas('swatch', src.width, src.height);
        this.swatchData.draw(0, 0, src);

        const swatch = this.add.image(800, 0, 'dp').setOrigin(0).setDepth(1000);

        swatch.setInteractive();

        swatch.on('pointerdown', this.changeColor, this);
        swatch.on('pointermove', this.updateColor, this);

        this.input.keyboard.on('keydown-C', this.setCircle, this);
        this.input.keyboard.on('keydown-R', this.setRectangle, this);
        this.input.keyboard.on('keydown-E', this.setEllipse, this);
        this.input.keyboard.on('keydown-S', this.setStar, this);
        this.input.keyboard.on('keydown-L', this.setLine, this);
        this.input.keyboard.on('keydown-DELETE', this.deleteShape, this);
        this.input.keyboard.on('keydown-TAB', this.changeShape, this);

        this.cursors = this.input.keyboard.createCursorKeys();

        this.input.on('pointerdown', this.drawStart, this);
        this.input.on('pointermove', this.drawUpdate, this);
        this.input.on('pointerup', this.drawStop, this);
    }

    update ()
    {
        if (!this.shape)
        {
            return;
        }

        if (this.input.keyboard.checkDown(this.cursors.left, 100))
        {
            this.shape.x -= (this.cursors.left.shiftKey) ? 10 : 1;
        }
        else if (this.input.keyboard.checkDown(this.cursors.right, 100))
        {
            this.shape.x += (this.cursors.right.shiftKey) ? 10 : 1;
        }

        if (this.input.keyboard.checkDown(this.cursors.up, 100))
        {
            this.shape.y -= (this.cursors.up.shiftKey) ? 10 : 1;
        }
        else if (this.input.keyboard.checkDown(this.cursors.down, 100))
        {
            this.shape.y += (this.cursors.down.shiftKey) ? 10 : 1;
        }
    }

    changeColor (pointer, x, y, event)
    {
        this.swatchData.getPixel(x, y, this.color);

        if (this.shape)
        {
            this.shape.setFillStyle(this.color.color);
        }

        event.stopPropagation();
    }

    updateColor (pointer, x, y, event)
    {
        if (!pointer.isDown)
        {
            return;
        }

        this.swatchData.getPixel(x, y, this.color);

        if (this.shape)
        {
            this.shape.setFillStyle(this.color.color);
        }

        event.stopPropagation();
    }

    deleteShape ()
    {
        if (this.shape)
        {
            this.shape.destroy();
            this.shape = null;
        }
    }

    changeShape ()
    {
        if (this.shapes.length < 2)
        {
            return;
        }

        this.index++;

        if (this.index >= this.shapes.length)
        {
            this.index = 0;
        }

        this.shape = this.shapes[this.index];
    }

    drawStart (pointer)
    {
        if (this.current === 0)
        {
            return;
        }

        this.isDown = true;

        switch (this.current)
        {
            case 1:
                this.shape = this.add.circle(pointer.x, pointer.y, 4, this.color.color);
                break;

            case 2:
                this.shape = this.add.rectangle(pointer.x, pointer.y, 4, 4, this.color.color);
                break;

            case 3:
                this.shape = this.add.ellipse(pointer.x, pointer.y, 4, 4, this.color.color);
                break;

            case 4:
                this.shape = this.add.star(pointer.x, pointer.y, 5, 2, 4, this.color.color);
                break;

            case 5:
                this.shape = this.add.line(pointer.x, pointer.y, 0, 0, 4, 0, this.color.color);
                break;
        }
    }

    drawUpdate (pointer)
    {
        if (!this.isDown)
        {
            return;
        }

        switch (this.current)
        {
            case 1:
                this.shape.radius = pointer.getDistance();
                break;

            case 2:
                this.shape.setSize(pointer.x - pointer.downX, pointer.y - pointer.downY);
                break;

            case 3:
                this.shape.setSize((pointer.x - pointer.downX) * 2, (pointer.y - pointer.downY) * 2);
                break;

            case 4:
                this.shape.scaleX = pointer.x - pointer.downX;
                this.shape.scaleY = pointer.y - pointer.downY;
                break;

            case 5:
                this.shape.setTo(0, 0, pointer.x - pointer.downX, pointer.y - pointer.downY);
                break;
        }
    }

    drawStop ()
    {
        this.isDown = false;

        this.shapes.push(this.shape);

        this.index++;
    }

    setCircle ()
    {
        if (this.isDown)
        {
            return;
        }

        this.current = 1;
        this.shape = null;
    }

    setRectangle ()
    {
        if (this.isDown)
        {
            return;
        }

        this.current = 2;
        this.shape = null;
    }

    setEllipse ()
    {
        if (this.isDown)
        {
            return;
        }

        this.current = 3;
        this.shape = null;
    }

    setStar ()
    {
        if (this.isDown)
        {
            return;
        }

        this.current = 4;
        this.shape = null;
    }

    setLine ()
    {
        if (this.isDown)
        {
            return;
        }

        this.current = 5;
        this.shape = null;
    }
}

const config = {
    type: Phaser.AUTO,
    parent: 'phaser-example',
    width: 1010,
    height: 600,
    backgroundColor: '#efefef',
    scene: Example
};

const game = new Phaser.Game(config);


Инициализация и загрузка ресурсов

Класс Example расширяет Phaser.Scene. В preload загружаются два изображения: фон (bg1) и палитра цветов (dp). Палитра — это спрайтшит с градиентами, который позже будет использоваться как интерактивный выбор цвета.

preload ()
{
    this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
    this.load.image('bg1', 'assets/skies/gradient4.png');
    this.load.image('dp', 'assets/swatches/gradient-palettes.png');
}

В методе create фон добавляется на сцену. Затем ключевой момент: из текстуры палитры создается Canvas-текстура swatchData. Это нужно для возможности считывания цвета пикселя под курсором методом getPixel. Сама палитра добавляется на сцену как изображение swatch и становится интерактивной.

create ()
{
    this.add.image(0, 0, 'bg1').setOrigin(0);

    const src = this.textures.get('dp').getSourceImage();
    this.swatchData = this.textures.createCanvas('swatch', src.width, src.height);
    this.swatchData.draw(0, 0, src);

    const swatch = this.add.image(800, 0, 'dp').setOrigin(0).setDepth(1000);
    swatch.setInteractive();
}

Обработка ввода: мышь и клавиатура

В create настраиваются обработчики событий. Для палитры: pointerdown и pointermove меняют цвет активной фигуры. Для клавиатуры: клавиши C, R, E, S, L переключают тип создаваемой фигуры (круг, прямоугольник, эллипс, звезда, линия). Клавиши стрелок будут перемещать активную фигуру, а TAB — переключаться между уже созданными фигурами. DELETE удаляет активную фигуру.

swatch.on('pointerdown', this.changeColor, this);
swatch.on('pointermove', this.updateColor, this);

this.input.keyboard.on('keydown-C', this.setCircle, this);
this.input.keyboard.on('keydown-R', this.setRectangle, this);
this.input.keyboard.on('keydown-E', this.setEllipse, this);
this.input.keyboard.on('keydown-S', this.setStar, this);
this.input.keyboard.on('keydown-L', this.setLine, this);
this.input.keyboard.on('keydown-DELETE', this.deleteShape, this);
this.input.keyboard.on('keydown-TAB', this.changeShape, this);

this.cursors = this.input.keyboard.createCursorKeys();

this.input.on('pointerdown', this.drawStart, this);
this.input.on('pointermove', this.drawUpdate, this);
this.input.on('pointerup', this.drawStop, this);

Метод update обрабатывает перемещение активной фигуры стрелками. Метод checkDown проверяет, нажата ли клавиша с учетом интервала (здесь 100 мс). Зажатый Shift ускоряет движение в 10 раз.

if (this.input.keyboard.checkDown(this.cursors.left, 100))
{
    this.shape.x -= (this.cursors.left.shiftKey) ? 10 : 1;
}

Механика рисования фигур

Процесс рисования состоит из трех этапов, привязанных к событиям мыши.

1. **Начало (drawStart)**: При нажатии кнопки мыши создается фигура выбранного типа (this.current) в точке нажатия (pointer.x, pointer.y) с минимальным размером и текущим цветом. Созданный объект сохраняется в this.shape.

case 1:
    this.shape = this.add.circle(pointer.x, pointer.y, 4, this.color.color);
    break;

2. **Обновление (drawUpdate)**: При движении мыши с зажатой кнопкой (this.isDown) размеры фигуры изменяются в зависимости от расстояния от начальной точки (pointer.downX/Y). Для каждой фигуры своя логика: у круга меняется radius, у прямоугольника — setSize, у линии — setTo.

case 1:
    this.shape.radius = pointer.getDistance();
    break;

3. **Завершение (drawStop)**: При отпускании кнопки фигура добавляется в массив this.shapes для последующего переключения через TAB, а флаг рисования сбрасывается.

drawStop ()
{
    this.isDown = false;
    this.shapes.push(this.shape);
    this.index++;
}

Динамическое изменение цвета

Цвет фигуры можно менять в реальном времени, кликая или перетаскивая курсор по палитре. Методы changeColor и updateColor работают схоже. Они используют this.swatchData.getPixel(x, y, this.color) для получения объекта Phaser.Display.Color из пикселя палитры под курсором. Затем этот цвет применяется к активной фигуре через setFillStyle.

changeColor (pointer, x, y, event)
{
    this.swatchData.getPixel(x, y, this.color);
    if (this.shape)
    {
        this.shape.setFillStyle(this.color.color);
    }
    event.stopPropagation();
}

event.stopPropagation() предотвращает срабатывание других обработчиков событий (например, pointerdown на сцене, который начал бы рисовать новую фигуру). Метод updateColor дополнительно проверяет, что кнопка мыши зажата (pointer.isDown), чтобы менять цвет только при перетаскивании по палитре.

Управление фигурами: удаление и переключение

Управление созданными фигурами реализовано через клавиши DELETE и TAB.

Метод deleteShape уничтожает активную фигуру через destroy() и обнуляет ссылку this.shape.

deleteShape ()
{
    if (this.shape)
    {
        this.shape.destroy();
        this.shape = null;
    }
}

Метод changeShape переключает активную фигуру (this.shape) на следующую в массиве this.shapes. Это позволяет редактировать параметры (например, цвет или позицию) любой ранее созданной фигуры.

changeShape ()
{
    if (this.shapes.length < 2) return;
    this.index++;
    if (this.index >= this.shapes.length) this.index = 0;
    this.shape = this.shapes[this.index];
}

Методы setCircle, setRectangle и другие просто меняют значение this.current, которое определяет тип следующей создаваемой фигуры. Проверка if (this.isDown) не позволяет сменить тип в процессе рисования.

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

Вы разобрали готовый интерактивный редактор фигур на Phaser. Его основа — использование простых Game Object'ов для фигур и работа с Canvas-текстурой для пиксельного выбора цвета. Этот код — отличная база для экспериментов. Попробуйте добавить новые фигуры (например, triangle через add.polygon), сохранять/загружать сцену в JSON, реализовать отмену действия (Ctrl+Z) или добавить слайдеры для точной настройки размера и прозрачности. Для создания полноценного редактора уровней этот пример станет прочным фундаментом.