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

Реалистичный эффект пролёта сквозь звёздное поле — классический элемент для космических игр и демо-сцен. В Phaser его можно создать без использования тяжёлых 3D-движков, используя простую математику перспективы и оптимизированный объект `Blitter`. В этой статье мы детально разберём пример кода, который создаёт трёхмерный эффект звёзд, управляет их движением и позволяет встраивать этот эффект как независимую сцену в интерфейс вашей игры. Вы научитесь работать с камерами, создавать частицы с помощью `Blitter` и применять программный рендеринг для имитации глубины.

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

Живой запуск

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

Исходный код


class Stars extends Phaser.Scene {

    constructor (handle, parent)
    {
        super(handle);

        this.parent = parent;

        this.blitter;

        this.width = 320;
        this.height = 220;
        this.depth = 1700;
        this.distance = 200;
        this.speed = 6;

        this.max = 300;
        this.xx = [];
        this.yy = [];
        this.zz = [];
    }

    create ()
    {
        this.cameras.main.setViewport(this.parent.x, this.parent.y, Stars.WIDTH, Stars.HEIGHT);
        this.cameras.main.setBackgroundColor(0x000000);

        this.blitter = this.add.blitter(0, 0, 'star');

        for (let i = 0; i < this.max; i++)
        {
            this.xx[i] = Math.floor(Math.random() * this.width) - (this.width / 2);
            this.yy[i] = Math.floor(Math.random() * this.height) - (this.height / 2);
            this.zz[i] = Math.floor(Math.random() * this.depth) - 100;

            const perspective = this.distance / (this.distance - this.zz[i]);
            const x = (this.width / 2) + this.xx[i] * perspective;
            const y = (this.height / 2) + this.yy[i] * perspective;
            const a = (x < 0 || x > 320 || y < 20 || y > 260) ? 0 : 1;

            this.blitter.create(x, y);
        }

        const bg = this.add.image(0, 0, 'starsWindow').setOrigin(0);
    }

    update (time, delta)
    {
        const list = this.blitter.children.list;

        for (let i = 0; i < this.max; i++)
        {
            const perspective = this.distance / (this.distance - this.zz[i]);

            const x = (this.width / 2) + this.xx[i] * perspective;
            const y = (this.height / 2) + this.yy[i] * perspective;

            this.zz[i] += this.speed;

            if (this.zz[i] > this.distance)
            {
                this.zz[i] -= (this.distance * 2);
            }

            list[i].x = x;
            list[i].y = y;
            list[i].a = (x < 0 || x > 320 || y < 20 || y > 260) ? 0 : 1;
        }
    }

    refresh ()
    {
        this.cameras.main.setPosition(this.parent.x, this.parent.y);

        this.scene.bringToTop();
    }

}

Stars.WIDTH = 328;
Stars.HEIGHT = 266;

Архитектура сцены и настройка камеры

Класс Stars наследуется от Phaser.Scene, что делает его полностью самостоятельной сценой. Это удобно для изоляции логики эффекта и его повторного использования.

В конструкторе инициализируются ключевые параметры: размеры виртуального поля звёзд (width, height), глубина (depth), скорость (speed) и максимальное количество звёзд (max). Массивы xx, yy, zz хранят трёхмерные координаты для каждой звезды.

Метод create() отвечает за первоначальную настройку. Самое важное здесь — настройка вьюпорта камеры. Это позволяет разместить сцену со звёздами как «окно» внутри другой, родительской сцены.

this.cameras.main.setViewport(this.parent.x, this.parent.y, Stars.WIDTH, Stars.HEIGHT);
this.cameras.main.setBackgroundColor(0x000000);

Сначала камере устанавливается область отображения (viewport) на основе координат родительского объекта (parent.x, parent.y) и фиксированных размеров. Затем её фон задаётся чёрным цветом. Далее создаётся объект this.blitter — высокопроизводительный контейнер для отрисовки множества одинаковых спрайтов (в нашем случае — текстуры звезды 'star').

Генерация звёзд и математика перспективы

Сердце эффекта — преобразование трёхмерных координат в двумерные для создания иллюзии глубины. В цикле создаётся начальное положение для каждой звезды.

this.xx[i] = Math.floor(Math.random() * this.width) - (this.width / 2);
this.yy[i] = Math.floor(Math.random() * this.height) - (this.height / 2);
this.zz[i] = Math.floor(Math.random() * this.depth) - 100;

Координаты `xиyгенерируются относительно центра виртуального поля (от -width/2 до +width/2). Координатаz` определяет глубину: чем она больше, тем «ближе» звезда к наблюдателю.

Затем применяется формула перспективной проекции для расчёта конечной позиции на 2D-экране:

const perspective = this.distance / (this.distance - this.zz[i]);
const x = (this.width / 2) + this.xx[i] * perspective;
const y = (this.height / 2) + this.yy[i] * perspective;

Здесь distance — это дистанция от наблюдателя до плоскости проекции. Коэффициент perspective увеличивается по мере приближения zz[i] к distance, заставляя звёзды «лететь» на нас. Также сразу рассчитывается прозрачность (`a), чтобы звёзды, вылетевшие за границы заданного окна, становились невидимыми. Наконец, для каждой звезды вызываетсяthis.blitter.create(x, y), которая создаёт её экземпляр в буфереBlitter`.

Анимация и обновление в реальном времени

Логика движения реализована в методе update(time, delta), который вызывается на каждом кадре. Здесь происходит перерасчёт позиций и управление цикличностью звёздного поля.

const list = this.blitter.children.list;

Мы получаем прямой доступ к массиву созданных объектов (Bob-объекты) внутри Blitter для максимальной производительности.

В цикле для каждой звезды снова вычисляется её 2D-позиция на основе обновлённой координаты `z. Затем координатаzz[i]увеличивается наthis.speed`, создавая иллюзию движения вперёд.

this.zz[i] += this.speed;
if (this.zz[i] > this.distance)
{
    this.zz[i] -= (this.distance * 2);
}

Условный оператор обеспечивает бесконечность звёздного поля: когда звезда «пролетает» мимо наблюдателя (её `zстановится большеdistance), она перемещается далеко назад (вычитаетсяdistance * 2`). Это создаёт эффект бесконечного потока без необходимости перегенерации звёзд.

Рассчитанные координаты `xиy, а также прозрачностьa(0 или 1) напрямую присваиваются свойствам соответствующего объекта в спискеlist[i]`.

Интеграция с родительской сценой

Пример демонстрирует паттерн вложенных сцен. Класс Stars принимает в конструкторе ссылку на родительский объект (parent), который, скорее всего, является другой сценой или игровым объектом, управляющим позицией окна со звёздами.

Метод refresh() позволяет внешнему коду обновить позицию окна со звёздами.

this.cameras.main.setPosition(this.parent.x, this.parent.y);
this.scene.bringToTop();

Первая строка перепозиционирует вьюпорт камеры сцены Stars, следуя за координатами родителя. Вторая строка гарантирует, что сцена со звёздами будет отрисована поверх других элементов, что важно для UI-компонентов. Это делает эффект независимым и легко управляемым модулем.

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

Мы разобрали, как создать производительный и визуально убедительный эффект звёздного поля, используя только 2D-возможности Phaser. Ключевыми элементами стали: объект Blitter для быстрого рендеринга, простая математика перспективной проекции и циклическое обновление координат для бесконечной анимации. **Идеи для экспериментов:** 1. Измените текстуру звезды на спрайт с градиентом или анимированный Sprite Sheet для создания разноцветных или мерцающих звёзд. 2. Добавьте влияние управления игрока: пусть скорость потока звёзд (this.speed) меняется при нажатии на газ или повороте корабля. 3. Реализуйте несколько слоёв звёзд с разной скоростью и размером для усиления ощущения глубины (параллакс-эффект). 4. Используйте этот класс как основу для эффекта гиперпрыжка или warp-скорости, динамически увеличивая this.speed и искажая перспективу.