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

Создание интерактивных персонажей с живым, отслеживающим взглядом — мощный прием для вовлечения игрока. Эта статья разбирает элегантный пример из официальных демо Phaser, где глаза следят за курсором мыши. Вы научитесь использовать геометрические объекты Phaser (`Ellipse`, `Line`, `Vector2`) для расчета реалистичного движения, ограниченного областью глазного яблока, и освоите технику управления несколькими камерами и сценами для сложных UI-элементов или персонажей.

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

Живой запуск

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

Исходный код


class Eyes extends Phaser.Scene {

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

        this.parent = parent;

        this.left;
        this.right;

        this.leftTarget;
        this.rightTarget;

        this.leftBase;
        this.rightBase;

        this.mid = new Phaser.Math.Vector2();
    }

    create ()
    {
        const bg = this.add.image(0, 0, 'eyesWindow').setOrigin(0);

        this.cameras.main.setViewport(this.parent.x, this.parent.y, Eyes.WIDTH, Eyes.HEIGHT);

        this.left = this.add.image(46, 92, 'eye');
        this.right = this.add.image(140, 92, 'eye');

        this.leftTarget = new Phaser.Geom.Line(this.left.x, this.left.y, 0, 0);
        this.rightTarget = new Phaser.Geom.Line(this.right.x, this.right.y, 0, 0);

        this.leftBase = new Phaser.Geom.Ellipse(this.left.x, this.left.y, 24, 40);
        this.rightBase = new Phaser.Geom.Ellipse(this.right.x, this.right.y, 24, 40);
    }

    update ()
    {
        this.leftTarget.x2 = this.input.activePointer.x - this.parent.x;
        this.leftTarget.y2 = this.input.activePointer.y - this.parent.y;

        //  Within the left eye?
        if (this.leftBase.contains(this.leftTarget.x2, this.leftTarget.y2))
        {
            this.mid.x = this.leftTarget.x2;
            this.mid.y = this.leftTarget.y2;
        }
        else
        {
            Phaser.Geom.Ellipse.CircumferencePoint(this.leftBase, Phaser.Geom.Line.Angle(this.leftTarget), this.mid);
        }

        this.left.x = this.mid.x;
        this.left.y = this.mid.y;

        this.rightTarget.x2 = this.input.activePointer.x - this.parent.x;
        this.rightTarget.y2 = this.input.activePointer.y - this.parent.y;

        //  Within the right eye?
        if (this.rightBase.contains(this.rightTarget.x2, this.rightTarget.y2))
        {
            this.mid.x = this.rightTarget.x2;
            this.mid.y = this.rightTarget.y2;
        }
        else
        {
            Phaser.Geom.Ellipse.CircumferencePoint(this.rightBase, Phaser.Geom.Line.Angle(this.rightTarget), this.mid);
        }

        this.right.x = this.mid.x;
        this.right.y = this.mid.y;
    }

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

        this.scene.bringToTop();
    }

}

Eyes.WIDTH = 183;
Eyes.HEIGHT = 162;

Архитектура сцены: вложенность и коммуникация

Класс Eyes представляет собой отдельную сцену (Phaser.Scene), которая управляет отрисовкой и логикой двух глаз. Это не самостоятельная игровая сцена, а скорее компонент, встроенный в другую, «родительскую» сцену. Такой подход полезен для изоляции сложной логики.

Конструктор сохраняет ссылку на parent — объект из основной сцены, который содержит координаты (`x,y`) окна с глазами. Это позволяет корректно позиционировать viewport камеры.

constructor (handle, parent)
{
    super(handle);
    this.parent = parent;
    // ... объявление переменных для глаз и геометрии
}

Инициализация: создание глаз и их границ

В методе create() происходит базовая настройка. Ключевой момент — создание отдельного viewport для камеры этой сцены. Он «вырезает» прямоугольное окно в координатах родительского объекта, создавая иллюзию, что глаза нарисованы прямо в нем.

Затем создаются два спрайта глаза и геометрические объекты для них: - Phaser.Geom.Line (leftTarget, rightTarget): Линия от центра глаза до цели (курсора). Изначально конец линии в (0,0), он будет обновляться. - Phaser.Geom.Ellipse (leftBase, rightBase): Эллипс, описывающий область, внутри которой зрачок может двигаться свободно (белок глаза).

create ()
{
    this.cameras.main.setViewport(this.parent.x, this.parent.y, Eyes.WIDTH, Eyes.HEIGHT);
    this.left = this.add.image(46, 92, 'eye');
    this.right = this.add.image(140, 92, 'eye');
    this.leftBase = new Phaser.Geom.Ellipse(this.left.x, this.left.y, 24, 40);
    // ... аналогично для правого глаза
}

Сердце логики: метод update() и расчет позиции

Вся магия происходит каждый кадр в `update()`. Алгоритм для каждого глаза одинаков:
1.  **Обновление цели:** Координаты курсора (`this.input.activePointer`) переводятся в локальную систему координат сцены `Eyes` путем вычитания `this.parent.x` и `this.parent.y`.
2.  **Проверка нахождения внутри области:** Метод `Ellipse.contains()` проверяет, лежит ли целевая точка внутри эллипса «белка». Если да — зрачок (спрайт `eye`) просто помещается в эту точку.
3.  **Проецирование на границу:** Если точка снаружи, нужно найти точку на окружности эллипса, которая смотрит в направлении цели. Для этого используется статический метод `Phaser.Geom.Ellipse.CircumferencePoint()`. Ему передают эллипс, угол от центра глаза до цели (рассчитывается через `Phaser.Geom.Line.Angle()`), и вектор `this.mid`, куда записывается результат.
// Для левого глаза:
this.leftTarget.x2 = this.input.activePointer.x - this.parent.x;
this.leftTarget.y2 = this.input.activePointer.y - this.parent.y;

if (this.leftBase.contains(this.leftTarget.x2, this.leftTarget.y2))
{
    this.mid.set(this.leftTarget.x2, this.leftTarget.y2);
}
else
{
    Phaser.Geom.Ellipse.CircumferencePoint(this.leftBase, Phaser.Geom.Line.Angle(this.leftTarget), this.mid);
}
this.left.setPosition(this.mid.x, this.mid.y);

Синхронизация с родителем: метод refresh()

Поскольку позиция окна (this.parent.x, this.parent.y) может меняться извне (например, окно перетаскивается), сцена Eyes должна уметь синхронизировать свой viewport с новой позицией. Для этого служит публичный метод refresh().

Он переставляет камеру на новые координаты и, что важно, поднимает сцену наверх стека отрисовки командой this.scene.bringToTop(). Это гарантирует, что окно с глазами всегда будет поверх других элементов родительской сцены.

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

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

Разобранный подход — отличная основа для создания динамических UI-элементов или частей персонажа (не только глаз). Экспериментируйте: измените форму области с Ellipse на Circle или Rectangle, добавьте инерцию движению зрачков, используйте эту технику для прицелов или интерактивных указателей. Главное — вы теперь знаете, как использовать геометрические модули Phaser для точных и эффективных расчетов.