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

Визуализация игровых элементов не всегда требует готовых спрайтов. Иногда проще и эффективнее создать графику программно. В этой статье разберем, как с помощью объекта `Graphics` в Phaser можно рисовать сложные векторные фигуры, контуры и даже стилизованные логотипы прямо в рантайме. Этот подход полезен для создания динамических интерфейсов, эффектов, прототипирования и уникального художественного стиля вашей игры.

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    create ()
    {

        const graphics = this.add.graphics();

        graphics.lineStyle(2, 0x0000ea);

        //  ST
        graphics.beginPath();
        graphics.moveTo(150, 130);
        graphics.lineTo(265, 130);
        graphics.lineTo(293, 115);
        graphics.lineTo(298, 104);
        graphics.lineTo(287, 89);
        graphics.lineTo(275, 85);
        graphics.lineTo(277, 80);
        graphics.lineTo(318, 80);
        graphics.lineTo(300, 130);
        graphics.lineTo(342, 130);
        graphics.lineTo(355, 80);
        graphics.lineTo(385, 80);
        graphics.lineTo(385, 60);
        graphics.lineTo(267, 60);
        graphics.lineTo(237, 75);
        graphics.lineTo(227, 89);
        graphics.lineTo(238, 103);
        graphics.lineTo(247, 105);
        graphics.lineTo(245, 109);
        graphics.lineTo(172, 109);
        graphics.closePath();
        graphics.strokePath();

        //  A
        graphics.beginPath();
        graphics.moveTo(365, 130);
        graphics.lineTo(394, 60);
        graphics.lineTo(446, 60);
        graphics.lineTo(488, 130);
        graphics.lineTo(451, 130);
        graphics.lineTo(444, 120);
        graphics.lineTo(407, 120);
        graphics.lineTo(402, 130);
        graphics.closePath();
        graphics.strokePath();

        graphics.beginPath();
        graphics.moveTo(411, 100);
        graphics.lineTo(420, 80);
        graphics.lineTo(436, 100);
        graphics.closePath();
        graphics.strokePath();

        //  R
        graphics.beginPath();
        graphics.moveTo(488, 130);
        graphics.lineTo(527, 130);
        graphics.lineTo(518, 108);
        graphics.lineTo(585, 130);
        graphics.lineTo(663, 130);
        graphics.lineTo(642, 109);
        graphics.lineTo(571, 106);
        graphics.lineTo(584, 96);
        graphics.lineTo(573, 79);
        graphics.lineTo(533, 60);
        graphics.lineTo(465, 60);
        graphics.closePath();
        graphics.strokePath();

        graphics.beginPath();
        graphics.moveTo(510, 96);
        graphics.lineTo(540, 96);
        graphics.lineTo(549, 88);
        graphics.lineTo(548, 86);
        graphics.lineTo(535, 79);
        graphics.lineTo(501, 79);
        graphics.closePath();
        graphics.strokePath();

        //  W
        graphics.beginPath();
        graphics.moveTo(100, 232);
        graphics.lineTo(153, 232);
        graphics.lineTo(182, 209);
        graphics.lineTo(173, 232);
        graphics.lineTo(229, 232);
        graphics.lineTo(292, 148);
        graphics.lineTo(260, 148);
        graphics.lineTo(222, 180);
        graphics.lineTo(238, 148);
        graphics.lineTo(206, 148);
        graphics.lineTo(170, 180);
        graphics.lineTo(184, 148);
        graphics.lineTo(139, 148);
        graphics.closePath();
        graphics.strokePath();

        //  A
        graphics.beginPath();
        graphics.moveTo(250, 232);
        graphics.lineTo(300, 232);
        graphics.lineTo(314, 220);
        graphics.lineTo(362, 220);
        graphics.lineTo(370, 232);
        graphics.lineTo(416, 232);
        graphics.lineTo(385, 148);
        graphics.lineTo(314, 148);
        graphics.closePath();
        graphics.strokePath();

        graphics.beginPath();
        graphics.moveTo(320, 200);
        graphics.lineTo(360, 200);
        graphics.lineTo(347, 166);
        graphics.closePath();
        graphics.strokePath();

        //  RS
        graphics.beginPath();
        graphics.moveTo(411, 232);
        graphics.lineTo(460, 232);
        graphics.lineTo(455, 204);
        graphics.lineTo(534, 232);
        graphics.lineTo(672, 232);
        graphics.lineTo(699, 213);
        graphics.lineTo(686, 196);
        graphics.lineTo(633, 178);
        graphics.lineTo(613, 175);
        graphics.lineTo(609, 169);
        graphics.lineTo(692, 169);
        graphics.lineTo(672, 148);
        graphics.lineTo(580, 148);
        graphics.lineTo(558, 164);
        graphics.lineTo(566, 178);
        graphics.lineTo(609, 196);
        graphics.lineTo(628, 199);
        graphics.lineTo(630, 204);
        graphics.lineTo(563, 204);
        graphics.lineTo(523, 199);
        graphics.lineTo(553, 187);
        graphics.lineTo(546, 167);
        graphics.lineTo(500, 148);
        graphics.lineTo(411, 148);
        graphics.closePath();
        graphics.strokePath();

        graphics.beginPath();
        graphics.moveTo(448, 186);
        graphics.lineTo(496, 186);
        graphics.lineTo(512, 182);
        graphics.lineTo(509, 176);
        graphics.lineTo(487, 167);
        graphics.lineTo(443, 167);
        graphics.closePath();
        graphics.strokePath();

        graphics.lineStyle(2, 0x00d701);

        //  Tie Fighter 1

        // graphics.beginPath();
        // graphics.moveTo(448, 186);
        // graphics.lineTo(496, 186);

        // graphics.closePath();
        // graphics.strokePath();

    }
}

const config = {
    type: Phaser.WEBGL,
    parent: 'phaser-example',
    width: 800,
    height: 600,
    scene: Example
};

const game = new Phaser.Game(config);





Создание холста для рисования

Вся работа с векторной графикой в Phaser начинается с создания объекта Graphics. Это специальный игровой объект, который служит холстом для программного рисования.

const graphics = this.add.graphics();

Вызов this.add.graphics() создает и сразу добавляет объект на сцену. Все последующие операции рисования будут применяться к этому объекту. Его можно перемещать, масштабировать и вращать, как и любой другой GameObject.

Настройка стиля линии

Перед тем как рисовать, нужно задать параметры пера: толщину линии и ее цвет. Для этого используется метод lineStyle().

graphics.lineStyle(2, 0x0000ea);

Первый аргумент (`2) задает толщину линии в пикселях. Второй аргумент (0x0000ea) — это цвет в шестнадцатеричном формате. Здесь0x0000eaсоответствует синему цвету. Этот стиль будет применяться ко всем последующим операциям рисования, пока мы не изменим его новым вызовомlineStyle()`.

Принцип рисования контура

Рисование любой фигуры состоит из трех этапов: начало пути, определение точек и завершение с обводкой.

1. **Начало пути:** graphics.beginPath() сообщает системе, что мы начинаем описывать новую фигуру. 2. **Определение точек:** С помощью moveTo(x, y) и lineTo(x, y) мы указываем координаты. moveTo «перемещает перо» в начальную точку, не рисуя. Каждый lineTo рисует отрезок из текущей позиции пера в новую указанную точку. 3. **Завершение и обводка:** closePath() автоматически замыкает фигуру, проводя линию из последней точки к первой. strokePath() выполняет непосредственную отрисовку контура фигуры с текущим стилем линии.

Пример фрагмента, рисующего букву «A»:

graphics.beginPath();
graphics.moveTo(365, 130);
graphics.lineTo(394, 60);
graphics.lineTo(446, 60);
graphics.lineTo(488, 130);
graphics.lineTo(451, 130);
graphics.lineTo(444, 120);
graphics.lineTo(407, 120);
graphics.lineTo(402, 130);
graphics.closePath();
graphics.strokePath();

Сборка сложных форм

Один объект Graphics может содержать множество независимых путей. В исходном примере логотип «ATARI» и слово «WARS» собраны из последовательности таких путей. Каждый вызов beginPath() начинает новую фигуру, даже если предыдущая была не замкнута. Это позволяет создавать сложные композиции.

Обратите внимание на внутренний треугольник в букве «A». Он рисуется отдельным блоком после внешнего контура:

// Внешний контур буквы 'A'
graphics.beginPath();
graphics.moveTo(365, 130);
// ... другие lineTo
graphics.closePath();
graphics.strokePath();

// Внутренний треугольник (отверстие в букве 'A')
graphics.beginPath();
graphics.moveTo(411, 100);
graphics.lineTo(420, 80);
graphics.lineTo(436, 100);
graphics.closePath();
graphics.strokePath();

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

Динамическое изменение стиля

Стиль линии можно менять в любой момент. Это демонстрируется в самом конце примера, где для недорисованного «Tie Fighter» устанавливается другой цвет.

graphics.lineStyle(2, 0x00d701);
// Здесь мог бы быть beginPath() и рисование нового объекта зеленым цветом

Это открывает возможности для интерактивности: можно менять цвет объекта при наведении, выделении или в зависимости от игрового состояния.

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

Объект Graphics в Phaser — мощный инструмент для программного создания векторной графики. Он идеально подходит для рисования линий, простых и сложных фигур, что полезно для интерфейсов, эффектов и особого визуального стиля. **Идеи для экспериментов:** 1. Анимируйте координаты точек с помощью Tweens или в методе update(), чтобы фигуры плавно меняли форму. 2. Реагируйте на ввод игрока: меняйте цвет линии при клике на объект Graphics (используйте setInteractive()). 3. Создайте процедурно генерируемые узоры или фоны, комбинируя случайные фигуры в цикле. 4. Используйте fillPath() и fillStyle() для заливки фигур сплошным цветом или градиентом.