LeCodesdocs

Аспекты

Прикрепляемые возможности узла — композиция с лаконичностью методов. Всё, что узел умеет сверх своего трансформа (анимация, коллайдеры, физика, твоя игровая логика), прикрепляется как аспект, а не раздувает класс узла. Встроенные SpriteAnimation, Shape2D, Physics2D — это аспекты; твои поведения расширяют Aspect тем же способом.

Обзор

TypeScript
class Health extends Aspect<'health', Sprite> {
  hp = 100
  hurt(n: number) {
    this.hp -= n
    if (this.hp <= 0) this.node.destroy()
  }
}

const hero = new Sprite({ texture })
  .aspect(SpriteAnimation, { size: [32, 48], fps: 10, clips: { walk: [1, 2, 3] } })
  .aspect(Health, { hp: 80 })          // прикрепить + настроить; чейнится — возвращает узел
hero.anim.play('walk')                 // каждый аспект добавляет именованный аксессор
hero.health.hurt(10)

Прикрепление, доступ, открепление

TypeScript
node.aspect(Class, opts?): node & { name: instance }   // прикрепить + настроить (или перенастроить)
node.get(Class): instance | undefined                  // безопасный доступ — undefined, если не прикреплён
node.has(Class): boolean                               // проверка наличия И type guard
node.removeAspect(Class): node                         // открепить (запускает onDetach); чейнится
  • aspect() возвращает узел, типизированный как уже имеющий этот аспект, поэтому именованный аксессор работает без guard'а сразу после цепочки.
  • Именованный аксессор (hero.anim, hero.health, …) берётся из класса аспекта: встроенные объявляют имя через static aspect; для твоих аспектов сборка извлекает имя из первого дженерик-параметра (Aspect<'health', …>node.health).
  • has() — это type guard: внутри if (node.has(Physics2D)) тип node.physics считается присутствующим.
Note

вызов .aspect(Class, opts) на уже прикреплённом аспекте не прикрепляет заново — он просто делает Object.assign опций на существующий экземпляр. onAttach выполняется один раз, только при первом прикреплении.

Свой аспект

TypeScript
class Follow extends Aspect<'follow', Sprite> {   // <имя аксессора, вид целевого узла>
  target?: Node2D
  speed = 4                                       // поля класса = настраиваемые значения по умолчанию
  onAttach() { /* node установлен; opts уже присвоены */ }
  onDetach() { /* очистка: таймеры, слушатели */ }
  update(dt: number) {                            // dt в секундах
    if (this.target) this.node.position = this.node.position.lerp(this.target.position, this.speed * dt)
  }
}
sprite.aspect(Follow, { target: hero, speed: 6 })
  • this.node — узел, к которому прикреплён аспект, типизированный по второму дженерик-параметру. Прикрепление к узлу неверного вида — ошибка компиляции.
  • Аспекты создаёт движок через node.aspect(), никогда не new — не пиши конструктор. Инициализируй в onAttach (к этому моменту узел и опции уже установлены); настраиваемые параметры объявляй полями класса со значениями по умолчанию.

Покадровый update(dt)

Аспект, определяющий update(dt), тикает каждый кадр, пока прикреплён (dt = секунды с прошлого кадра). Обновления идут в одной из двух фаз:

  • LATE (по умолчанию) — после шага физики и синхронизации трансформов, прямо перед отрисовкой кадра. Чтение node.worldPosition даёт финальную отрисованную позицию, поэтому камеры и «преследователи» встают точно, без отставания на кадр.
  • EARLY (updateBeforePhysics = true) — до шага физики, поэтому записи скорости, сил или кинематических трансформов учитываются шагом того же кадра (нулевая задержка ввода).
TypeScript
class Steer extends Aspect<'steer', Sprite> {
  updateBeforePhysics = true      // этот аспект ПИТАЕТ симуляцию — выполнять до шага
  order = -10                     // порядок тика внутри фазы: по возрастанию; по умолчанию 0, при равенстве — порядок прикрепления
  update(dt: number) { /* пиши this.node.physics.velocity здесь */ }
}

Правило: аспекты, которые пишут в симуляцию, идут в EARLY; аспекты, которые читают результаты (камеры, преследователи, синхронизация UI), остаются в фазе LATE по умолчанию.

Note

updateBeforePhysics и order читаются один раз, при прикреплении — задавай их полями класса (или в опциях прикрепления), не позже.

Not implemented

updateWhenVisible объявлен, чтобы аспекты могли подписаться на отсечение по видимости, но сейчас это заглушка — каждый зарегистрированный аспект тикает независимо от того, на экране он или нет.

With<N, A> — типизация переменной с аспектами

TypeScript
let boss: With<Sprite, Health | Physics2D>    // Sprite, про который ИЗВЕСТНО, что у него оба аспекта
boss.health.hurt(10)                          // guard не нужен

Объединяй аспекты через union (не кортеж) — так читается естественно и повторяет рантайм-guard node.has(Health). Используй для полей и параметров функций, принимающих узлы, собранные в другом месте.

Смотрите также

  • Соглашения — стиль цепочек, в который вписываются аспекты.
  • События — узлы также являются эмиттерами (addEventListener).
  • Sprite — типичный хост для аспектов.