[JavaScript, Разработка игр] SpaceShooter на Phaser 3

Автор Сообщение
news_bot ®

Стаж: 6 лет 9 месяцев
Сообщений: 27286

Создавать темы news_bot ® написал(а)
17-Июн-2021 17:31

Здравствуйте! Это перевод курса (ссылка на оригинал в конце статьи), который охватывает создание космического шутера с помощью Phaser 3. Курс будет состоять из 5 частей, в каждой из которых будет решаться определенная задача при создании проекта. Перед началом прохождения курса желательно знать основы JavaScript.Шаг первый. Настроить веб-серверПервое, что необходимо сделать, это настроить веб-сервер. Несмотря на то, что фазерные игры запускаются в браузере, к сожалению нельзя просто запустить локально html-файл непосредственно из файловой системы. При запросе файлов по протоколу http, безопасность сервера позволяет получить доступ только к тем файлам, которые вам разрешены. При загрузке файла из локальной файловой системы (file://) ваш браузер сильно ограничивает его по очевидным причинам безопасности. Из-за этого нам нужно будет разместить нашу игру на локальном веб-сервере. Вы можете использовать любой удобный для вас веб-сервер, будь то OpenServer или какой-либо другой.Шаг второй. Создать необходимы файлы и папкиНайдите где ваш веб-вервер размещает файлы сайтов и создайте в нём папку с вашим проектом. Назовите его как вам будет удобно. Внутри проекта создайте файл index.html. Наш индексный файл - это место, где объявим местоположение фазерного скрипта и остальных игровых скриптов.Далее нам нужно создать две новые папки: content (спрайты, аудио и др.) и js (фазерные и игровые скрипты). Теперь внутри папки js нужно создать 4 файла: SceneMainMenu.js, SceneMain.js, SceneGameOver.js, и game.js.На данный момент структура нашего проекта должна выглядеть следующим образом:
структура проекта в начале работыТеперь нужно загрузить контент для нашего проекта в папку content. Вы можете создать и загрузить свои собственные файла, либо воспользоваться готовыми (здесь).Необходимый контент:Sprites (images)
  • sprBtnPlay.png (Кнопка "Play")
  • sprBtnPlayHover.png (Кнопка "Play" когда мышь наведена)
  • sprBtnPlayDown.png (Кнопка "Play" когда кнопка нажата)
  • sprBtnRestart.png (Кнопка "Restart")
  • sprBtnRestartHover.png (Кнопка "Restart" когда мышь наведена)
  • sprBtnRestartDown (Кнопка "Restart" когда кнопка нажата)
  • sprBg0.png (фоновый слой звезд с прозрачностью вокруг звезд)
  • sprBg1.png (еще один фоновый слой звезд)
  • sprEnemy0.png (первый анимированный враг)
  • sprEnemy1.png (второй не анимированный враг)
  • sprEnemy2.png (третий анимированный враг)
  • sprLaserEnemy.png (выстрел лазера врагом)
  • sprLaserPlayer.png (выстрел лазера игроком)
  • sprExplosion.png (анимация взрыва)
  • sprPlayer.png (спрайт игрока)
Audio (.wav files)
  • sndExplode0.wav (первый звук взрыва)
  • sndExplode1.wav (второй звук взрыва)
  • sndLaser.wav (звук выстрела лазера)
  • sndBtnOver.wav (звук наведения мышки на кнопку)
  • sndBtnDown.wav (звук нажатия на кнопку)
Шаг третий. Загрузка фреймворкаТеперь необходимо загрузить актуальную версию самого Phaser. Это можно сделать здесь. В своем проекте вы можете использовать из phaser.js или phaser.min.js файл. Разница в том, что phaser.js имеет более удобный формат для чтения, но он и больше весит. Если вы не собираетесь вносить, какие-либо изменения в исходный код библиотеки, то можно использовать phaser.min.js. Он предназначен для распространения и сжимается для уменьшения размера файла. Загрузите файл в нашу папку js в проекте.Шаг четвертый. Index.htmlСледующее, что нужно сделать, это создать содержимое index.html, который лежит в корне проекта. Для этого используйте любой текстовый редактор или IDE по своему усмотрению.Откройте index.html и введите следующий код:
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta lang="en-us">
    <title>Space Shooter</title>
    <script src="js/phaser.js"></script> <!-- название файла должно соответствовать тому, который вы решили использовать. -->
  </head>
  <body>
    <script src="js/Entities.js"></script>
    <script src="js/SceneMainMenu.js"></script>
    <script src="js/SceneMain.js"></script>
    <script src="js/SceneGameOver.js"></script>
    <script src="js/game.js"></script>
  </body>
</html>
Обратите внимание на порядок подключения скриптов. Порядок очень важен, так как JavaScript интерпретируется сверху вниз. Мы будем ссылаться на код из файлов сцен (с приставкой Scene) в файле game.js.Шаг пятый. Инициализация игрыОткройте game.js и создайте объект следующим образом:
var config = {}
Этот объект будет содержать свойства конфигурации, которые мы будем передавать нашему экземпляру игры phaser. Внутри данного объекта добавьте:
type: Phaser.WEBGL,
width: 480,
height: 640,
backgroundColor: "black",
physics: {
  default: "arcade",
  arcade: {
    gravity: { x: 0, y: 0 }
  }
},
scene: [],
pixelArt: true,
roundPixels: true
Пока что, внутри нашего объекта конфигурации, мы говорим нашей игре, что она должна визуализироваться с помощью WebGL, а не с помощью обычной технологии рендеринга Canvas. Далее, параметры width и height, устанавливают ширину и высоту нашей игры, которые будет занимать наша игра на странице. Свойство backgroundColor устанавливает черный цвет фона. Следующее свойство, physics, определяет физический движок, который будет использоваться, а именно arcade. Аркадная физика хорошо работает, когда нам нужно базовое обнаружение столкновений без каких-либо особенностей. Внутри physics, мы также устанавливаем гравитацию (gravity) для нашего физического мира. Следующее свойство scene, мы определяем массивом, который заполним немного позже. Наконец, мы хотим, чтобы Phaser обеспечивал четкость пикселей (pixelArt и roundPixels) точно так же, как ностальгические видеоигры, которые мы знали и полюбили.
scene: [
  SceneMainMenu,
  SceneMain,
  SceneGameOver
],
Теперь объект конфигурации закончен и мы не будем больше к нему прикасаться. Последняя и самая важная строка в этом файле это инициализация игры Phaser, где мы передаем нашу конфигурацию:
var game = new Phaser.Game(config);
Файл game.js завершен! В итоге он должен выглядеть следующим образом:
var config = {
    type: Phaser.WEBGL,
    width: 480,
    height: 640,
    backgroundColor: "black",
    physics: {
      default: "arcade",
      arcade: {
        gravity: { x: 0, y: 0 }
      }
    },
    scene: [
        SceneMainMenu,
        SceneMain,
        SceneGameOver
    ],
    pixelArt: true,
    roundPixels: true
}
var game = new Phaser.Game(config);
Шаг шестой. Создание классов сценДавайте откроем SceneMainMenu.js и добавим в него следующий код:
class SceneMainMenu extends Phaser.Scene
{
  constructor() {
    super({ key: "SceneMainMenu" });
  }
  create() {
    this.scene.start("SceneMain");
  }
}
Здесь мы объявляем класс SceneMainMenu, который расширяет Phaser.Scene. Внутри данного класса есть две функции: constructor и create. Конструктор вызывается немедленно, при создании класса (подробнее с этим вы можете ознакомиться изучив принципы ООП). Внутри конструктора мы выполняется одна строчка кода:
super({ key: "SceneMainMenu" });
что фактически означает:
var someScene = new Phaser.Scene({ key: "SceneMainMenu" });
Вместо того, чтобы создавать экземпляр Phaser, мы определяем нашу сцену как класс, в котором мы можем создать пользовательские функции предварительной загрузки, создания и обновления. Функция create будет вызываться сразу же после создания сцены. Внутри функции create выполняется одна строка кода:
this.scene.start("SceneMain");
План состоит в том, чтобы перенаправить игрока на главную сцену игры, где происходит все действие. Было бы лучше начать сразу же с написания игры и вернуться к главному меню немного позже. Я думаю для большинства так будет интересней. Теперь надо закончить оставшиеся классы в файлах SceneMain.js и SceneGameOver.js.SceneMain.js:
class SceneMain extends Phaser.Scene {
  constructor() {
    super({ key: "SceneMain" });
  }
  create() {}
}
SceneGameOver.js:
class SceneGameOver extends Phaser.Scene {
  constructor() {
    super({ key: "SceneGameOver" });
  }
  create() {}
}
На данный момент, при запуске игры в браузере, у вас должен отобразиться черный прямоугольник:
Шаг седьмой. Загрузка игровых ресурсовНа этом шаге, в классе SceneMain нам нужно добавить новую функцию под названием preload. Эта функция должна быть размещена между функциями constructor и create. Теперь класс должен выглядеть следующим образом:
class SceneMain extends Phaser.Scene {
  constructor() {
    super({ key: "SceneMain" });
  }
  preload() {
  }
  create() {
  }
}
Внутри нашей новой функции предварительной загрузки нам нужно добавить код для загрузки наших игровых ресурсов. Чтобы загрузить файл изображения, внутри функции preload нужно ввести следующую строку:
this.load.image("sprBg0", "content/sprBg0.png");
Где первый параметр это imageKey. По нему далее в коде мы будем обращаться к загруженной картинке. А второй параметр - это путь к месту, где лежит файл изображения. Давайте загрузим остальные изображения, которые пригодятся нам для создания игры. После загрузки всех изображений, функция preload будет выглядеть следующим образом:
preload() {
  this.load.image("sprBg0", "content/sprBg0.png");
  this.load.image("sprBg1", "content/sprBg1.png");
  this.load.spritesheet("sprExplosion", "content/sprExplosion.png", {
    frameWidth: 32,
    frameHeight: 32
  });
  this.load.spritesheet("sprEnemy0", "content/sprEnemy0.png", {
    frameWidth: 16,
    frameHeight: 16
  });
  this.load.image("sprEnemy1", "content/sprEnemy1.png");
  this.load.spritesheet("sprEnemy2", "content/sprEnemy2.png", {
    frameWidth: 16,
    frameHeight: 16
  });
  this.load.image("sprLaserEnemy0", "content/sprLaserEnemy0.png");
  this.load.image("sprLaserPlayer", "content/sprLaserPlayer.png");
  this.load.spritesheet("sprPlayer", "content/sprPlayer.png", {
    frameWidth: 16,
    frameHeight: 16
  });
}
Обратите внимание, что в некоторых местах мы загружаем не image, а spritesheet. Это означает, что мы загружаем анимацию, а не статическое изображение. Spritesheet - это изображение с несколькими кадрами, расположенными бок о бок. Также в spritesheet мы определяем третьим аргументом ширину и высоту кадра в пикселях.Теперь нам нужно будет загрузить звуки. Это делается очень похоже на то, как мы загружали изображения. Добавьте в конец функции preload следующий код:
this.load.audio("sndExplode0", "content/sndExplode0.wav");
this.load.audio("sndExplode1", "content/sndExplode1.wav");
this.load.audio("sndLaser", "content/sndLaser.wav");
Шаг восьмой. Немного кода для анимации.После того, как мы загрузили контент, нам нужно добавить немного больше кода для создания анимации. Теперь в функции create() класса SceneMain создадим анимации:
this.anims.create({
  key: "sprEnemy0",
  frames: this.anims.generateFrameNumbers("sprEnemy0"),
  frameRate: 20,
  repeat: -1
});
this.anims.create({
  key: "sprEnemy2",
  frames: this.anims.generateFrameNumbers("sprEnemy2"),
  frameRate: 20,
  repeat: -1
});
this.anims.create({
  key: "sprExplosion",
  frames: this.anims.generateFrameNumbers("sprExplosion"),
  frameRate: 20,
  repeat: 0
});
this.anims.create({
  key: "sprPlayer",
  frames: this.anims.generateFrameNumbers("sprPlayer"),
  frameRate: 20,
  repeat: -1
});
Нам также нужно добавить звуки к какой-то переменной или объекту, чтобы мы могли ссылаться на него позже. Если существует более одного звука (скажем, три звука взрыва), я добавляю массив в качестве значения свойства explosions. Давайте добавим объект звукового эффекта:
this.sfx = {
  explosions: [
    this.sound.add("sndExplode0"),
    this.sound.add("sndExplode1")
  ],
  laser: this.sound.add("sndLaser")
};
Позже мы сможем воспроизводить звуковые эффекты с нашего объекта, например:
this.scene.sfx.laser.play();
Нам также придется загрузить некоторые изображения и звуки для главного меню и экрана Game Over. Открывайте SceneMainMenu.js и создайте функцию предварительной загрузки (preload()) внутри класса SceneMainMenu. Внутри новой функции предварительной загрузки добавьте следующее, чтобы добавить наши кнопки и звуки:Теперь мы можем вернуться к игре в браузере, и черный прямоугольник все еще должен отображаться. Откройте инструменты разработки в браузере, который вы используете. Если вы используете Chrome или Firefox, вы можете просто нажать F12, чтобы открыть его. Посмотрите во вкладке Консоли (Console), чтобы убедиться в отсутствии ошибок (они отображаются красным цветом.) Если вы не видите ошибок, мы можем приступить к добавлению игрока!Шаг девятый. Создание игрокаПрежде чем добавить космический корабль игрока в игру, мы должны добавить новый файл в нашу папку js под названием Entities.js. Этот файл будет содержать все классы для различных сущностей в нашей игре. Мы будем классифицировать игрока, врагов, лазеры и т. д., как сущности. Обязательно также добавьте ссылку на Entities.js в index.html перед SceneMainMenu.js. После этого откройте файл и объявите новый класс с именем Entity.
class Entity {
  constructor(scene, x, y, key, type) {}
}
Как вы можете увидеть, мы сразу определяем параметры которые будет принимать конструктор класса. Каждый из параметров, которые мы добавили в конструктор, будет важен, потому что мы будем расширять класс для всех сущностей, которые мы создадим. Очень похоже на то, как мы расширили Phaser.Scene когда мы начали эту игру. Теперь мы расширим класс Entity:
class Entity extends Phaser.GameObjects.Sprite
Как всегда при расширении класса, нам нужно будет добавить super в наш конструктор. Поскольку игрок, враги и различные снаряды, которые мы добавляем, будут иметь одни и те же базовые свойства, это помогает нам не добавлять избыточный, дублирующий код. Таким образом, мы унаследуем свойства и функции базового класса Phaser.GameObjects.Sprite для всех наших сущностей. Все наши сущности будут иметь одинаковые базовые свойства (scene, x, y, key и type, которую мы будем использовать для извлечения определенных сущностей, если нам это понадобится.) Давайте добавим super в наш конструктор, который должен выглядеть следующим образом:
super(scene, x, y, key);
После добавления ключевого слова super в конструктор на следующей строке добавьте строки:
this.scene = scene;
this.scene.add.existing(this);
this.scene.physics.world.enableBody(this, 0);
this.setData("type", type);
this.setData("isDead", false);
Этот фрагмент кода назначает сцену сущности, а также добавляет экземпляр сущности в очередь рендеринга сцены. Мы также включаем инстанцированные сущности в качестве физических объектов в физическом мире сцены. Наконец, мы определяем скорость игрока. С этим мы должны закончить добавление в наш класс сущностей. Теперь мы можем перейти к созданию вашего класса игроков.К настоящему времени вы должны знать, как добавить класс. Добавьте его сразу после класса Entity и назовите его Player и убедитесь, что он расширяет Entity. Добавьте конструктор в класс Player с параметрами: scene, x, y и key. Затем добавьте ключевое слово super в конструктор, предоставив ему следующие параметры:
super(scene, x, y, key, "Player");
Нам также понадобится способ определить скорость, с которой должен двигаться игрок. Добавив пару ключ/значение скорости игрока, мы можем обратиться к ней позже для наших функций движения. Под ключевым словом super добавьте следующее:
this.setData("speed", 200);
Мы также хотим добавить небольшой фрагмент кода для воспроизведения анимации игрока:
this.play("sprPlayer");
Чтобы добавить функции перемещения, добавьте следующие четыре функции после конструктора.
moveUp() {
  this.body.velocity.y = -this.getData("speed");
}
moveDown() {
  this.body.velocity.y = this.getData("speed");
}
moveLeft() {
  this.body.velocity.x = -this.getData("speed");
}
moveRight() {
  this.body.velocity.x = this.getData("speed");
}
Эти функции позволяют перемещаться игроку с установленной скоростью на экране по координатам x и y.Эти функции будут вызваны в функции update(). Добавьте функцию update() непосредственно под функцией moveRight. Внутри функции обновления пропишите:
this.body.setVelocity(0, 0);
this.x = Phaser.Math.Clamp(this.x, 0, this.scene.game.config.width);
this.y = Phaser.Math.Clamp(this.y, 0, this.scene.game.config.height);
Теперь мы закончили с классом игрока! В данном случае скорость игрока будет равна нулю. Если ни одна из клавиш перемещения не нажата, игрок останется неподвижным. Следующие две строки кода обновления игрока гарантируют, что игрок не сможет выйти за пределы экрана. На этом этапе мы можем создать экземпляр игрока в функции create главной сцены. Добавьте следующее в функцию создания главной сцены:
this.player = new Player(
  this,
  this.game.config.width * 0.5,
  this.game.config.height * 0.5,
  "sprPlayer"
);
Именно здесь мы создаем экземпляр игрока. Мы можем обратиться к игроку в любом месте SceneMain. Затем игрок располагается в центре холста. Если вы попытаетесь запустить игру, вы все равно не увидите, как игрок двигается. Это происходит потому, что сначала мы должны добавить функцию обновления в SceneMain и добавить проверки движения. Поскольку this.player теперь добавлен, теперь мы можем добавить функцию обновления. Добавьте функцию обновления прямо под функцией создания главной сцены и добавьте внутрь следующее:
this.player.update();
if (this.keyW.isDown) {
  this.player.moveUp();
}
else if (this.keyS.isDown) {
  this.player.moveDown();
}
if (this.keyA.isDown) {
  this.player.moveLeft();
}
else if (this.keyD.isDown) {
  this.player.moveRight();
}
Напомню, что this.player.update() запустит код обновления, который сохранит игрока неподвижным, а также гарантирует, что он не сможет переместиться за пределы экрана. В функции create() класса SceneMain добавьте следующее для инициализации наших ключевых переменных после инициализации игрока:
this.keyW = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.W);
this.keyS = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.S);
this.keyA = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.A);
this.keyD = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.D);
this.keySpace = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.SPACE);
Если мы запустим наш код сейчас, игрок должен иметь возможность перемещаться с помощью клавиш W, S, A, D. В следующей части мы добавим возможность для игрока стрелять лазерами (которые будут использовать клавишу пробела.) Шаг десятый. Добавление враговДавайте теперь откроем Entities.js и добавим классы врагов. В самом низу Entities.js под классом игрока добавьте три новых класса, называемых ChaserShip, GunShip и CarrierShip:
class ChaserShip extends Entity {
  constructor(scene, x, y) {
    super(scene, x, y, "sprEnemy1", "ChaserShip");
  }
}
class GunShip extends Entity {
  constructor(scene, x, y) {
    super(scene, x, y, "sprEnemy0", "GunShip");
    this.play("sprEnemy0");
  }
}
class CarrierShip extends Entity {
  constructor(scene, x, y) {
    super(scene, x, y, "sprEnemy2", "CarrierShip");
    this.play("sprEnemy2");
  }
}
Классы ChaserShip, GunShip и CarrierShip должны расширить класс Entity, который мы создали ранее. Затем мы вызываем конструктор с соответствующими параметрами. Для каждого класса врагов под ключевым словом super добавьте следующее:
this.body.velocity.y = Phaser.Math.Between(50, 100);
Приведенная выше строка устанавливает скорость противника как случайное целое число между 50 и 100. Мы будем создавать врагов в верхней части экрана, что заставит их двигаться вниз по холсту. Затем вернитесь к SceneMain.js. Нам нужно будет создать группу, чтобы удерживать наших врагов, лазеры, стреляющие врагами, и лазеры, стреляющие игроком. В функции create после установки строки this.keySpace добавьте:
this.enemies = this.add.group();
this.enemyLasers = this.add.group();
this.playerLasers = this.add.group();
Если мы запустим нашу игру, то там еще не будет никаких врагов. Сначала мы должны создать событие (оно будет действовать как таймер), которое породит наших врагов. После нашей группы playerLasers добавьте следующий код:
this.time.addEvent({
  delay: 100,
  callback: function() {
    var enemy = new GunShip(
      this,
      Phaser.Math.Between(0, this.game.config.width),
      0
    );
    this.enemies.add(enemy);
  },
  callbackScope: this,
  loop: true
});
Если мы попробуем запустить игру сейчас, то увидим множество врагов-боевых кораблей, движущихся вниз. Теперь мы дадим нашим противникам возможность стрелять. Во-первых, мы должны создать еще один класс под названием EnemyLaser сразу после класса игрока. Откройте Entities.js. Вражеский лазер также должен расширить класс Entity.
class EnemyLaser extends Entity {
  constructor(scene, x, y) {
    super(scene, x, y, "sprLaserEnemy0");
    this.body.velocity.y = 200;
  }
}
Теперь мы можем вернуться к нашему классу боевых кораблей, а именно к конструктору. В разделе, где мы устанавливаем скорость, мы можем добавить новое событие.
this.shootTimer = this.scene.time.addEvent({
  delay: 1000,
  callback: function() {
    var laser = new EnemyLaser(
      this.scene,
      this.x,
      this.y
    );
    laser.setScale(this.scaleX);
    this.scene.enemyLasers.add(laser);
  },
  callbackScope: this,
  loop: true
});
Обратите внимание, что мы присваиваем описанное выше событие переменной this.shootTimer. Мы должны создать новую функцию внутри GunShip под названием onDestroy. onDestroy - это не функция, используемая Phaser, поэтому вы можете назвать ее как угодно. Мы будем использовать эту функцию, чтобы уничтожить таймер стрельбы, когда враг будет уничтожен. Добавьте функцию onDestroy в наш класс GunShip и добавьте внутрь следующее:
if (this.shootTimer !== undefined) {
  if (this.shootTimer) {
    this.shootTimer.remove(false);
  }
}
Когда вы запустите игру вы должны увидеть:
Когда мы запустим игру, вы должны увидеть армию боевых кораблей, спускающихся с верхней части экрана. Все враги также должны стрелять лазерами. Теперь, когда мы видим, что все работает, мы можем сразу сократить количество боевых кораблей. Для этого перейдите в SceneMain.js файл и изменить задержку времени.
delay: 1000,
Теперь вернитесь обратно в Entities.js, нам нужно будет добавить немного кода в конструктор класса ChaserShip:
this.states = {
  MOVE_DOWN: "MOVE_DOWN",
  CHASE: "CHASE"
};
this.state = this.states.MOVE_DOWN;
Этот код делает две вещи: создает объект с двумя свойствами, которые мы можем использовать для установки состояния корабля-преследователя, а затем мы устанавливаем состояние в значение свойства MOVE_DOWN.Теперь мы можем добавить функцию обновления в класс ChaserShip. Функция обновления - это то, где мы будем кодировать ИИ для класса корабля-охотника. Сначала мы закодируем разведданные для вражеского истребителя, так как это немного сложнее. Перейдите обратно к Entities.js, а в функции обновления класса ChaserShip добавьте следующее:
if (!this.getData("isDead") && this.scene.player) {
  if (Phaser.Math.Distance.Between(
    this.x,
    this.y,
    this.scene.player.x,
    this.scene.player.y
  ) < 320) {
    this.state = this.states.CHASE;
  }
  if (this.state == this.states.CHASE) {
    var dx = this.scene.player.x - this.x;
    var dy = this.scene.player.y - this.y;
    var angle = Math.atan2(dy, dx);
    var speed = 100;
    this.body.setVelocity(
      Math.cos(angle) * speed,
      Math.sin(angle) * speed
    );
  }
}
С помощью этого кода враги-преследователи будут двигаться вниз по экрану. Однако, как только он окажется в пределах 320 пикселей от игрока, он начнет преследовать игрока. Если вы хотите, чтобы корабль-охотник вращался, добавьте следующее сразу после (или в конце) нашего условия погони:
if (this.x < this.scene.player.x) {
  this.angle -= 5;
} else {
  this.angle += 5;
}
Чтобы породить корабль-охотник, нам придется вернуться на SceneMain.js и добавьте новую функцию под названием getEnemiesByType. Внутри этой новой функции добавьте:
getEnemiesByType(type) {
  var arr = [];
  for (var i = 0; i < this.enemies.getChildren().length; i++) {
    var enemy = this.enemies.getChildren()[i];
    if (enemy.getData("type") == type) {
      arr.push(enemy);
    }
  }
  return arr;
}
Приведенный выше код позволит нам указать тип врага и получить всех врагов из группы врагов. Этот код проходит через группы врагов и проверяет, равен ли тип врага в цикле типу, заданному в качестве параметра. Как только мы добавили функцию getEnemiesByType, нам нужно будет изменить наше событие spawner. В анонимной функции свойства обратного вызова давайте изменим:на:
var enemy = null;
if (Phaser.Math.Between(0, 10) >= 3) {
  enemy = new GunShip(
    this,
    Phaser.Math.Between(0, this.game.config.width),
    0
  );
} else if (Phaser.Math.Between(0, 10) >= 5) {
  if (this.getEnemiesByType("ChaserShip").length < 5) {
    enemy = new ChaserShip(
      this,
      Phaser.Math.Between(0, this.game.config.width),
      0
    );
  }
} else {
  enemy = new CarrierShip(
    this,
    Phaser.Math.Between(0, this.game.config.width),
    0
  );
}
if (enemy !== null) {
  enemy.setScale(Phaser.Math.Between(10, 20) * 0.1);
  this.enemies.add(enemy);
}
Проходя через этот блок, мы добавляем условие, которое выбирает один из наших трех классов врагов: GunShip, ChaserShip или CarrierShip, который будет создан. Установив переменную enemy, мы затем добавляем ее в группу enemies. Если CarrierShip выбран для нереста, мы проверяем, чтобы было не более пяти ChaserShip, прежде чем нерестить еще один. Прежде чем добавить врага в группу, мы также применяем к нему случайную шкалу. Поскольку каждый враг расширяет наш класс Entity, который, в свою очередь, расширяет Phaser.GameObjects.Sprite мы можем установить масштаб для врагов, как и для любого другого Phaser.GameObjects.Sprite.В функции обновления нам нужно обновить врагов в группе this.enemies. Для этого в конце функции обновления добавьте следующее:
for (var i = 0; i < this.enemies.getChildren().length; i++) {
  var enemy = this.enemies.getChildren()[i];
  enemy.update();
}
Если мы попробуем запустить игру сейчас, то увидим, что корабли преследователей должны двигаться к кораблю игрока, как только они окажутся на расстоянии досягаемости.Шаг одиннадцатый. Дадим игроку возможность стрелятьВернитесь к классу Player и в конструкторе добавьте:
this.setData("isShooting", false);
this.setData("timerShootDelay", 10);
this.setData("timerShootTick", this.getData("timerShootDelay") - 1);
Мы устанавливаем то, что я бы назвал “ручным таймером”. Мы не используем события для способности игрока стрелять. Это происходит потому, что мы не хотим, чтобы задержка снималась при первоначальном нажатии клавиши пробела. В функции обновления класса игрока мы добавим остальную логику для нашего “ручного таймера”:
if (this.getData("isShooting")) {
  if (this.getData("timerShootTick") < this.getData("timerShootDelay")) {
    // каждое обновление игры увеличивайте timerShootTick на единицу, пока мы не достигнем значения timerShootDelay
    this.setData("timerShootTick", this.getData("timerShootTick") + 1);
  } else { // когда "ручной таймер" срабатывает:
    var laser = new PlayerLaser(this.scene, this.x, this.y);
    this.scene.playerLasers.add(laser);
    this.scene.sfx.laser.play(); // воспроизвести звуковой эффект лазера
    this.setData("timerShootTick", 0);
  }
}
Единственное, что нам осталось сделать, это добавить класс лазера игрока в наш Entities.js файл. Мы можем добавить этот класс прямо под классом Player и перед классом EnemyLaser. Это позволит сохранить наши классы, связанные с игроком, и наши классы, связанные с врагами, вместе. Создайте конструктор внутри класса PlayerLaser и добавьте в него тот же код, что и в классе EnemyLaser. Затем установите отрицательный знак в том месте, где мы установили значение скорости. Это приведет к тому, что лазеры игроков будут двигаться вверх, а не вниз. Лазерный класс игрока теперь должен выглядеть так:
class PlayerLaser extends Entity {
    constructor(scene, x, y) {
        super(scene, x, y, "sprLaserPlayer");
        this.body.velocity.y = -200;
    }
}
Последнее, что нам нужно сделать, чтобы позволить игроку стрелять, - это вернуться к SceneMain.js и добавьте следующее условие под нашим кодом движения:
if (this.keySpace.isDown) {
  this.player.setData("isShooting", true);
} else {
  this.player.setData("timerShootTick", this.player.getData("timerShootDelay") - 1);
  this.player.setData("isShooting", false);
}

Мы закончили с добавлением возможности стрелять лазерами как для игрока, так и для врагов! Шаг двенадцатый. Немного оптимизацииПрежде чем мы перейдем к столкновениям объектов, будет хорошей идеей добавить то, что называется отбраковкой усеченного конуса. Отбраковка усеченного конуса позволит нам удалить все, что движется за пределами экрана, что высвободит вычислительную мощность и память. Без отбраковки фрустрации, если мы позволим нашей игре работать некоторое время, она будет выглядеть так:
Чтобы добавить отбраковку усеченного конуса, нам придется перейти к функции обновления главной сцены. В настоящее время у нас должен быть реализован цикл for, в котором мы обновляем врагов:
for (var i = 0; i < this.enemies.getChildren().length; i++) {
  var enemy = this.enemies.getChildren()[i];
  enemy.update();
}
После строки enemy.update(), нужно добавить следующий код:
if (enemy.x < -enemy.displayWidth ||
  enemy.x > this.game.config.width + enemy.displayWidth ||
  enemy.y < -enemy.displayHeight * 4 ||
  enemy.y > this.game.config.height + enemy.displayHeight) {
    if (enemy) {
      if (enemy.onDestroy !== undefined) {
        enemy.onDestroy();
      }
      enemy.destroy();
    }
}
Мы также можем добавить то же самое для вражеских лазеров и лазеров игрока:
        for (var i = 0; i < this.enemyLasers.getChildren().length; i++) {
            var laser = this.enemyLasers.getChildren()[i];
            laser.update();
        
            if (laser.x < -laser.displayWidth ||
                laser.x > this.game.config.width + laser.displayWidth ||
                laser.y < -laser.displayHeight * 4 ||
                laser.y > this.game.config.height + laser.displayHeight) {
                if (laser) {
                laser.destroy();
                }
            }
        }
    
        for (var i = 0; i < this.playerLasers.getChildren().length; i++) {
            var laser = this.playerLasers.getChildren()[i];
            laser.update();
        
            if (laser.x < -laser.displayWidth ||
                laser.x > this.game.config.width + laser.displayWidth ||
                laser.y < -laser.displayHeight * 4 ||
                laser.y > this.game.config.height + laser.displayHeight) {
                if (laser) {
                laser.destroy();
                }
            }
        }
Шаг тринадцатый. Столкновения объектовЧтобы добавить столкновения, мы перейдем к нашему SceneMain.js и взглянем на нашу функцию create. Нам нужно будет добавить то, что называется коллайдером, ниже события появления нашего врага. Коллайдеры позволяют добавить проверку столкновения между двумя игровыми объектами. Таким образом, если происходит столкновение между двумя объектами, будет вызван указанный вами обратный вызов, и вы получите два экземпляра, которые столкнулись в качестве параметров. Мы можем создать коллайдер между лазерами игрока и врагами. В коде мы напишем это так:
this.physics.add.collider(this.playerLasers, this.enemies, function(playerLaser, enemy) {
});
Если мы хотим, чтобы враг был уничтожен при попадании лазера игрока, мы можем написать внутри анонимной функции:
if (enemy) {
  if (enemy.onDestroy !== undefined) {
    enemy.onDestroy();
  }
  enemy.explode(true);
  playerLaser.destroy();
}
Если мы запустим это, то получим ошибку, так как explode - это не функция. Впрочем, не беспокойтесь, мы можем просто вернуться в Entities.js и посмотреть на класс Entity. В классе Entity нам нужно добавить новую функцию под названием explode. Мы будем принимать canDestroy в качестве единственного параметра этой новой функции. Параметр canDestroy определяет, будет ли при вызове explode объект уничтожен или просто установлен невидимым. Внутри функции explode мы можем добавить:
explode(canDestroy) {
  if (!this.getData("isDead")) {
    // устанавливаем анимацию взрыва для текстуры
    this.setTexture("sprExplosion");  // это относится к тому же ключу анимации, котрый мы добавляли в this.anims.create ранее
    this.play("sprExplosion"); // запускаем анимацию
    // использовать случайный звук взрыва который мы определили в this.sfx в SceneMain
    this.scene.sfx.explosions[Phaser.Math.Between(0, this.scene.sfx.explosions.length - 1)].play();
    if (this.shootTimer !== undefined) {
      if (this.shootTimer) {
        this.shootTimer.remove(false);
      }
    }
    this.setAngle(0);
    this.body.setVelocity(0, 0);
    this.on('animationcomplete', function() {
      if (canDestroy) {
        this.destroy();
      } else {
        this.setVisible(false);
      }
    }, this);
    this.setData("isDead", true);
  }
}
Если мы запустим игру, вы можете заметить, что игрок все еще может двигаться и стрелять, даже если корабль игрока взорвется. Мы можем исправить это, обернув проверкой логику движения игрока в SceneMain.js Конечный результат должен выглядеть следующим образом:
if (!this.player.getData("isDead")) {
  this.player.update();
  if (this.keyW.isDown) {
    this.player.moveUp();
  }
  else if (this.keyS.isDown) {
    this.player.moveDown();
  }
  if (this.keyA.isDown) {
    this.player.moveLeft();
  }
  else if (this.keyD.isDown) {
    this.player.moveRight();
  }
  if (this.keySpace.isDown) {
    this.player.setData("isShooting", true);
  }
  else {
    this.player.setData("timerShootTick", this.player.getData("timerShootDelay") - 1);
    this.player.setData("isShooting", false);
  }
}
На этом основная часть кода уже реализована. Далее останется дописать некоторые штрихи. Шаг четырнадцатый. Финальные действияМы реализовали добавление врагов, лазеров игрока, вражеских лазеров, отбраковку усеченного конуса и столкновения. Есть несколько вещей, которые мы закончим на этом шаге, чтобы завершить этот курс. Мы добавим фон прокрутки, заполним главное меню и создадим игру поверх экрана.Мы начнем с добавления фона прокрутки. Фон прокрутки будет иметь несколько слоев с разной скоростью. Во-первых, давайте перейдем к нашему Entities.js. В нижней части файла мы можем добавить новый класс, прокручивающий фон. Ему не нужно ничего расширять.
class ScrollingBackground {
  constructor(scene, key, velocityY) {
  }
}
Наш конструктор будет принимать сцену, в которой мы создаем прокручивающийся фон, и ключ изображения нашего звездного фона. Сначала мы установим сцену экземпляра из нашего параметра, который мы приняли. Мы также будем хранить ключ в экземпляре прокрутки фона. Добавьте в кнутри конструктора:
this.scene = scene;
this.key = key;
this.velocityY = velocityY;
Мы будем реализовывать функцию под названием createLayers. Однако прежде чем мы это сделаем, нам еще нужно создать группу внутри нашего конструктора.
this.layers = this.scene.add.group();
Теперь создадим функцию createLayers и добавим внутри следующий код для создания спрайтов из ключа изображения:
for (var i = 0; i < 2; i++) {
  var layer = this.scene.add.sprite(0, 0, this.key);
  layer.y = (layer.displayHeight * i);
  var flipX = Phaser.Math.Between(0, 10) >= 5 ? -1 : 1;
  var flipY = Phaser.Math.Between(0, 10) >= 5 ? -1 : 1;
  layer.setScale(flipX * 2, flipY * 2);
  layer.setDepth(-5 - (i - 1));
  this.scene.physics.world.enableBody(layer, 0);
  layer.body.velocity.y = this.velocityY;
  this.layers.add(layer);
}
Приведенный выше код повторяется через каждый ключ, который мы принимаем. Для каждого ключа мы создаем спрайт с ключом на каждой итерации цикла for. Затем мы добавляем спрайт в нашу группу слоев. Затем мы применяем нисходящую скорость, при которой каждый слой тем медленнее, чем дальше назад на значении i. Затем мы можем вызвать createLayers в нижней части нашего конструктора.
this.createLayers();
Теперь мы можем вернуться в SceneMain.js и инициализируйте фон прокрутки. Вставьте следующий код перед созданием this.player и добавьте его после определения this.sfx.
this.backgrounds = [];
for (var i = 0; i < 5; i++) { // создание пяти слоев фона
  var bg = new ScrollingBackground(this, "sprBg0", i * 10);
  this.backgrounds.push(bg);
}
Попробуйте запустить игру, вы должны увидеть звезды позади игрока. Теперь мы можем вернуться в Entities.js и добавьте функцию обновления в класс фона со следующим кодом внутри:
if (this.layers.getChildren()[0].y > 0) {
  for (var i = 0; i < this.layers.getChildren().length; i++) {
    var layer = this.layers.getChildren()[i];
    layer.y = (-layer.displayHeight) + (layer.displayHeight * i);
  }
}
for (var i = 0; i < this.backgrounds.length; i++) {
  this.backgrounds[i].update();
}
Это завершает добавление нашего фона в игру! Если мы запустим игру, то увидим несколько фоновых слоев, прокручивающихся вниз с разной скоростью.Мы можем закончить, добавив наше главное меню и экран GameOver. Перейдите к SceneMainMenu и удалите строку, которая начинается с SceneMain. Однако прежде чем мы продолжим, мы должны создать объект звукового эффекта для SceneMainMenu. Добавьте следующее в самую верхнюю часть функции create:
this.sfx = {
  btnOver: this.sound.add("sndBtnOver"),
  btnDown: this.sound.add("sndBtnDown")
};
Затем мы можем добавить кнопку воспроизведения в функцию создания, добавив спрайт.
this.btnPlay = this.add.sprite(
  this.game.config.width * 0.5,
  this.game.config.height * 0.5,
  "sprBtnPlay"
);
Чтобы запустить SceneMain, нам нужно сначала установить наш спрайт как интерактивный. Добавьте следующее непосредственно ниже, где мы определили this.btnPlay:
this.btnPlay.setInteractive();
Поскольку мы настроили наш спрайт как интерактивный, теперь мы можем добавлять указатели на события, такие как over, out, down и up. Мы можем выполнить код, когда каждое из этих событий запускается мышью или нажатием клавиши. Первое событие, которое мы добавим, - это pointerover. Мы изменим текстуру кнопки на наше изображение sprBtnPlayHover.png, когда указатель находится поверх кнопки. Добавьте следующее после того, как мы установили нашу кнопку как интерактивную:
this.btnPlay.on("pointerover", function() {
  this.btnPlay.setTexture("sprBtnPlayHover"); // установка текстуры для кнопки
  this.sfx.btnOver.play(); // проигрывание звука при наведении на кнопку
}, this);
Теперь мы можем добавить событие pointerout. В этом случае мы сбросим текстуру обратно к обычному изображению кнопки. Добавьте следующее в разделе где мы определяем указатель на событие:
this.btnPlay.on("pointerout", function() {
  this.setTexture("sprBtnPlay");
});
Если мы снова запустим игру и наведем курсор мыши на кнопку, а затем уведем его, то увидим, что текстура кнопки сброшена на изображение по умолчанию.Далее мы можем добавить событие pointerdown. Здесь мы изменим текстуру кнопки запуска на sprBtnPlayDown.png.
this.btnPlay.on("pointerdown", function() {
  this.btnPlay.setTexture("sprBtnPlayDown");
  this.sfx.btnDown.play();
}, this);
Затем мы можем добавить событие pointerup для сброса текстуры кнопки после нажатия.
this.btnPlay.on("pointerup", function() {
  this.setTexture("sprBtnPlay");
}, this);
Мы можем дополнить логику внутри нашего события pointerup, чтобы начать основную сцену. Окончательное событие pointerup должно выглядеть следующим образом:
this.btnPlay.on("pointerup", function() {
  this.btnPlay.setTexture("sprBtnPlay");
  this.scene.start("SceneMain");
}, this);
Когда мы запускаем игру и нажимаем кнопку воспроизведения, теперь она должна начать главную сцену!Теперь есть только пара вещей, которые мы можем сделать, чтобы закончить наше главное меню. Первое - это добавление заголовка. Чтобы добавить заголовок, мы можем создать текст. Добавьте следующее под событием pointerup:
this.title = this.add.text(this.game.config.width * 0.5, 128, "SPACE SHOOTER", {
  fontFamily: 'monospace',
  fontSize: 48,
  fontStyle: 'bold',
  color: '#ffffff',
  align: 'center'
});
Чтобы центрировать заголовок, мы можем установить начало текста на половину ширины и половину высоты. Мы можем сделать это, написав следующее под определением title:
this.title.setOrigin(0.5);
На этом статья заканчивается. Это моя первая статья на Хабре и я старался максимально точно донести смысл оригинала. Если вы заметите где-то неточности или ошибки, напишите в комментариях и мы это обсудим.Ссылка на оригинал статьиСсылка на исходники оригинала
===========
Источник:
habr.com
===========

Похожие новости: Теги для поиска: #_javascript, #_razrabotka_igr (Разработка игр), #_phaser_3, #_gamedev, #_space, #_shooter, #_javascript, #_razrabotka_igr (
Разработка игр
)
Профиль  ЛС 
Показать сообщения:     

Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы

Текущее время: 22-Ноя 09:53
Часовой пояс: UTC + 5