О чем этот пример
Визуализация игровых элементов не всегда требует готовых спрайтов. Иногда проще и эффективнее создать графику программно. В этой статье разберем, как с помощью объекта `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() для заливки фигур сплошным цветом или градиентом.
