Создайте бумажный самолет с помощью Phaser 3 и аркадной физики

Введение

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

Основы

Настройка фазера

Поскольку предполагаемой платформой является Интернет, я решил использовать популярную Фазер 3 игровой движок, работающий с HTML5 и JavaScript. Для удобства я начал с шаблона / стартового набора Phaser. Однако обратите внимание, что при желании можно начать с нуля.
Для справки я использовал Стартовый шаблон Phaser 3 ES6 для этого проекта.

Для простоты проект состоит из основной сцены и сцены загрузки (заставки). Это соответствует обычной практике загрузки ресурсов в сцену загрузчика, а затем переключения на основную сцену.

планер.jpg

Сцена игры

Игровая сцена (game-scene.js) представляет собой очень простую схему земля/небо с несколькими объектами на земле для ориентации. Мы используем функцию create() для создания и заполнения всех объектов и установки размеров камеры и мира. В этом случае мир в два раза шире экрана, поэтому мы можем прокручивать, чтобы просмотреть любую его часть.

class GameScene extends Phaser.Scene {
  constructor() {
    super({key: 'Game'});
  }

  
  create() {

      let gameW= this.sys.game.config.width;
      let gameH= this.sys.game.config.height;
      let worldSizeW = gameW*2;

      
      let skyBg = this.add.tileSprite(0, 0, worldSizeW, gameH, 'skyTile');
      skyBg.setOrigin(0, 0);

      let platforms = this.physics.add.staticGroup();

      
      let ground= this.add.tileSprite(0,0, worldSizeW, 64, 'grass');
      ground.setOrigin(0,0);
      ground.setPosition(0, gameH-60);
      platforms.add(ground);

      this.add.image(gameW-200, gameH-100, 'redFlag');
      this.add.image(worldSizeW*0.8, gameH-100, 'greenFlag');

      this.glider = new Glider(this);

      
      this.physics.add.collider(this.glider, platforms);

      
      this.cameras.main.setBounds(0, 0, worldSizeW, gameH);
      this.cameras.main.startFollow(this.glider, true, 0.5, 0.5 ); 

    }
}

Последние две строки задают встроенной камере прокрутку в пределах границ мира и слежение за объектом-планером, чтобы он оставался в поле зрения во время движения.

Планер

Чтобы получить красивую и чистую реализацию, я решил инкапсулировать большую часть функциональности в один компонент — объект планера. Это прямо спрайт Phaser: объект отображения, содержащий изображение, с добавлением физического тела. Расширение Sprite и добавление пользовательского кода — это то, что нужно.

Я использовал изображение бумажного самолета (справа) и загрузил его как «планер».
pplane10m.png
Как и любой объект в Phaser, мы можем подключиться к методам жизненного цикла объекта, а именно к созданию и обновлению. В то время как create вызывается один раз при инициализации, update вызывается для каждого кадра. В Sprite мы также можем использовать конструктор для создания.

Ниже приведен исходный класс Glider, который инициализируется изображением планера, которое было загружено ранее.
Точка вращения спрайта устанавливается в центр, физическое тело включено, и спрайт добавляет себя к родительской сцене.

class Glider extends Phaser.GameObjects.Sprite {
  
  constructor(scene) {
    super(scene, 0, 0, 'glider');

    this.setOrigin(0.5);

    
    scene.physics.world.enable(this);
    scene.add.existing(this);
    this.body.setGravityY(0);
    this.body.setBounceY(0.2);
  }

  update(t, td) {
    
  }
}

Глубокое погружение: физика планера

Физическая модель

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

Скользящие силы.png

Подводя итог: гравитация толкает планер вниз, а подъемная сила толкает его перпендикулярно плоскости планера. Кроме того, существует (гораздо меньшая) сила сопротивления или трение воздуха, толкающая в направлении, противоположном движению.

Основная цель состоит в том, чтобы добиться циклического движения, при котором планирующий объект циклически повторяется между спуском и подобным образом:
скольжение-шаблон-маленький.png

В реальной жизни есть мера контроля над планером за счет наклона самолета, но в нашем случае нас больше интересует воспроизведение движения с помощью простой активации. По этой причине я решил смоделировать бумажный самолет, поскольку он использует ту же модель.

Подделайте это с помощью упрощенной модели

Внедрение такой модели может быть трудным и трудоемким, и это не входит в задачи этой статьи. Вместо этого мы хотим упростить его, чтобы добиться достаточно близких результатов с гораздо меньшими усилиями.
Для этого мы хотели бы воспользоваться преимуществами собственной системы Arcade Physics от Phaser, чтобы справиться с физическим моделированием и избежать ручных вычислений.

Ограничения аркадной физики

Аркадная физика сама по себе представляет собой упрощенную физическую систему, предназначенную для выполнения обычных задач с минимальной настройкой. Важно понимать, что он ограничен простым API, который опускает некоторые из более сложных факторов и не обрабатывает приложение сил напрямую или правильное трение.
Однако он позволяет установить ускорение, пропорциональное силе. Если пренебречь массой объекта, ускорение будет эквивалентно силе. Обратите внимание, что мы в основном говорим о постоянном ускорении, а не о силе во времени.
Трение в Аркаде работает только тогда, когда тело имеет нулевое ускорение (без приложения сил), поэтому оно не очень полезно.
Еще одно ограничение, о котором стоит упомянуть, связано с коллизиями: Arcade может обрабатывать только прямоугольные тела с AABB (ограничивающей рамкой, выровненной по оси), что означает, что рамка столкновения не может вращаться вместе с телом. К счастью, в нашем случае это не представляет никаких проблем.

Как обращаться с силами в Arcade

Поскольку нам приходится иметь дело с приложением сил, есть два случая:

  1. Постоянная сила. Достигается путем установки ускорения и оставления его без изменений.
  2. Импульсная (мгновенная) сила — То же самое, но в течение фиксированного времени (например, 1 секунда)

Для импульса мы используем прием сброса ускорения после временной задержки. Таким образом, физическая система будет просто использовать импульс объекта до тех пор, пока он не начнет падать, что эквивалентно обнулению сил.

Мы добавляем следующую функцию в класс Glider:


  applyImpulseForce(forceVec, duration=1) {
    this.body.setAcceleration(forceVec.x, forceVec.y);
    this.scene.time.delayedCall(duration*1000, 
              () => this.body.setAcceleration(0,0));
  }

Практика: Собираем код воедино

Начать моделирование

Начнем с очень простого запуска бумажного самолетика в воздух. Физика аналогична стрельбе снарядом из пушки: один раз прикладывается усилие.
Добавление функции запуска, которая принимает такой вектор силы:

  launch(force, dir) {
    this.curState= GlideStates.LAUNCH;
    this.body.setGravityY(200);

    let forceVec = new Phaser.Math.Vector2(dir); 
    forceVec.scale(force); 

    this.applyImpulseForce(forceVec, 1.1);
  }

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

Обновление с течением времени: обработка вращения плоскости

Для функции обновления нам нужно использовать параметр дельта-времени — dt, который содержит время, прошедшее в миллисекундах с момента последнего кадра.

 update(t, dt) {
 
    this.updateRotation(dt);
  }

Итак, теперь у нас есть базовое движение, но чего-то не хватает: самолет не меняет шаг (поворот), как ожидается от реального самолета.
Чтобы направить плоскость в локальном направлении «вперед», нам понадобится что-то вроде этой функции, которая вычисляет угол по вектору скорости:

  updateRotation(dt) {
    const dir= this.body.velocity;
         
    
    if (dir.x > 0.05) {
      this.rotation = dir.angle();
    }
  }

В основном это работает, но есть пара проблем. Во-первых, угол наклона иногда колеблется беспорядочно. Причина этого в том, что угол меняется от почти нуля до более чем 2PI, поэтому нам нужно его нормализовать. В математическом классе Phaser есть функция для этого: Math.Angle.Wrap().

      
      if (dir.x > 0.05) {
        this.rotation = Phaser.Math.Angle.Wrap( dir.angle());
      }

Последние штрихи: более плавное вращение

Это исправляет ситуацию, но вращение выглядит очень нервно и не кажется естественным. Мы справимся с этим, добавив ступенчатое вращение с линейной интерполяцией, используя функцию Math.Linear.

Линейная интерполяция, или сокращенно Lerp, является распространенным методом изменения значения с течением времени, который создает хороший плавный поворот путем постепенного увеличения интерполированного значения в каждом кадре. Параметр шага управляет скоростью вращения или долей на кадр. Умножая это на (dt/1000), мы нормализуем время, чтобы использовать секунды.
Обратите внимание, что мы можем сделать вращение тугим или свободным, изменив размер шага.

Хорошей метрикой является dt/1000 для 1 секунды, 2 * dt/1000 для полсекунды и так далее.

    updateRotation(dt) {
      const dir= this.body.velocity;
      const step = dt * 0.001 * 2; 
      const targetRot = Phaser.Math.Angle.Wrap( dir.angle());

      
      if (dir.x > 0.05) {
        this.rotation = Phaser.Math.Linear(this.rotation, targetRot, step);
      }
    }

Окончательный результат можно увидеть здесь:
GliderTest2.gif

Помимо основного движения

Текущий код показывает только начальный запуск, но его можно расширить.
Мы могли бы, например, использовать функцию applyImpulseForce и вызывать ее с различной силой и направлением.

На ум приходит несколько возможных идей для расширения, в том числе:

  • Добавьте прицел к запуску, чтобы игрок мог задавать направление и силу
  • Дополнительные циклы вытягивания на основе действий игрока
  • Обнаружение, когда самолет достигает пика и собирается нырнуть вниз
  • Измерьте максимальное расстояние, на котором самолет пролетел до удара о землю

Однако они слишком длинны, чтобы описывать их здесь, и фактические детали для них могут быть рассмотрены в отдельном посте.

Подведение итогов

Полный код проекта доступен на Github: github.com/amosl/PaperPlane

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

Похожие записи

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *