Русский
Русский
English
Статистика
Реклама

Space

SpaceShooter на Phaser 3

17.06.2021 14:15:17 | Автор: admin

Здравствуйте! Это перевод курса (ссылка на оригинал в конце статьи), который охватывает создание космического шутера с помощью 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(vari=0;i<this.enemies.getChildren().length;i++){varenemy=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{//когда "ручной таймер" срабатывает:    varlaser=newPlayerLaser(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. Затем установите отрицательный знак в том месте, где мы установили значение скорости. Это приведет к тому, что лазеры игроков будут двигаться вверх, а не вниз. Лазерный класс игрока теперь должен выглядеть так:

classPlayerLaserextendsEntity{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(vari=0;i<this.enemyLasers.getChildren().length;i++){varlaser=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(vari=0;i<this.playerLasers.getChildren().length;i++){varlaser=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();  }  elseif(this.keyS.isDown){  this.player.moveDown();  }  if(this.keyA.isDown){  this.player.moveLeft();  }  elseif(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(vari=0;i<5;i++){//создание пяти слоев фона  varbg=newScrollingBackground(this,"sprBg0",i*10);  this.backgrounds.push(bg);}

Попробуйте запустить игру, вы должны увидеть звезды позади игрока. Теперь мы можем вернуться в Entities.js и добавьте функцию обновления в класс фона со следующим кодом внутри:

if(this.layers.getChildren()[0].y>0){  for(vari=0;i<this.layers.getChildren().length;i++){  varlayer=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,"SPACESHOOTER",{fontFamily:'monospace',fontSize:48,fontStyle:'bold',color:'#ffffff',align:'center'});

Чтобы центрировать заголовок, мы можем установить начало текста на половину ширины и половину высоты. Мы можем сделать это, написав следующее под определением title:

this.title.setOrigin(0.5);

На этом статья заканчивается. Это моя первая статья на Хабре и я старался максимально точно донести смысл оригинала. Если вы заметите где-то неточности или ошибки, напишите в комментариях и мы это обсудим.

Ссылка на оригинал статьи

Ссылка на исходники оригинала

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

Подробнее..

Погружение в JetBrains Space Applicaitons

22.03.2021 14:08:01 | Автор: admin

Привет, Хабр.

Недавно JetBrains представили свой новый продукт под названием Space, о чем был своевременный пост на Хабре. Прошло немного времени и уже пора бы попробовать некоторые его особенности как платформы. В этой статье речь пойдет о Space Applications.

Space Applications - это расширения серверной и клиентской частей. Applications позволяют взаимодействовать с разными компонентами платформы и расширять её функциональность.

Первым делом рассмотрим расширения серверной части, которые предоставляет Space. Основное ограничение - Application нельзя запустить непосредственно внутри окружения Space. Для предоставления доступа между Space и Application используется так называемый Endpoint, в котором мы указываем Endpoint URI - адрес нашего плагина, на который Space будет отправлять запросы и коммуницировать с помощью Space HTTP API.

На данный момент известно несколько видов Applications:

Chatbot

Cтандартный чатбот, с которым взаимодействие происходит в приватном с ним чате. На данный момент кастомизация сильно уступает, например, Telegram. Нельзя добавлять бота в канал (групповой чат), а из интерактивного интерфейса пока доступны только кнопки.

Slash commands

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

Client applications

Используют функционал Space, чтобы получать от него разную информацию или взаимодействовать с разными модулями и компонентами, например, открывать/закрывать issues, писать сообщения, отправлять посты в канал и так далее.

Custom menus

Позволяют расширить стандартные меню новыми элементами. На момент написания статьи api для них еще не доступен.

Также можно свободно комбинировать сразу несколько типов Applications. Например, создать бота, у которого будет кнопка для запуска билда проекта на CI/CD сервере, и результат которого вернется в общий канал.

Для начала Application требуется создать. Делается это через Administration menu. При создании можно указать, какими правами оно будет обладать, чтобы приложение имело доступ только к тем ресурсам, которые нами явно указаны.

После создания в табе Authentication требуется выбрать один из возможных Authentication flows, в зависимости от типа приложения. Кратко каждый из них можно описать так:

  • Client Credentials Flow - самый простой способ. Работает через client id и client secret. Наше приложение будет работать от своего лица и не сможет получить доступ к некоторым компонентам платформы. Используется в полностью серверном приложении.

  • Authorization Code Flow - логинимся в приложение через Space, получаем код авторизации, который приложение использует чтобы из него получить токен и работать от лица пользователя.

  • Implicit Flow - идея таже, что и Authorization Code Flow, только клиент логинится на стороне браузера.

Client applications

В данном разделе мы используем функционал Space, чтобы получить от сервера разную информацию: каналы, приложения, пользователи, проекты, и т.д.

build.gradle.kts
repositories {    mavenCentral()    maven("https://kotlin.bintray.com/kotlinx")    maven("https://maven.pkg.jetbrains.space/public/p/space/maven")}dependencies {    implementation(kotlin("stdlib"))    // Space api sdk    // https://www.jetbrains.com/help/space/space-sdk.html    val space_version = "61400-beta"    implementation("org.jetbrains:space-sdk-jvm:$space_version")    // Ktor (http client)    // https://github.com/ktorio/ktor    val ktor_version = "1.4.3"    implementation("io.ktor:ktor-client-core:$ktor_version")    implementation("io.ktor:ktor-client-core-jvm:$ktor_version")    implementation("io.ktor:ktor-client-cio:$ktor_version")}

Сначала нам требуется создать и настроить клиент. Мы будем использовать Client Credentials Flow:

private const val spaceInstanceUrl = "https://makentoshe.jetbrains.space"val spaceClient = SpaceHttpClient(HttpClient(CIO)).withServiceAccountTokenSource(    ClientCredentialsFlow.clientId, ClientCredentialsFlow.clientSecret, spaceInstanceUrl)object ClientCredentialsFlow {    const val clientId: String = TODO(Put your client_id here)    const val clientSecret: String = TODO(Put your client_secret here)}

Ниже приведены примеры некоторых расширений и запросов.

// There are some examples of retrieving several data from the Space instance.// We can process this info as we want - create analytics, office dashboards, and so on.fun main() = runBlocking {    val channels = spaceClient.chats.channels.listAllChannels("").data    println("Channels: ${channels.map { "${it.name}(${it.channelId})" }}")    // View application rights allows to see all applications    // If rights were not accepted - the application can see only itself.    val applications = spaceClient.applications.getAllApplications("")    println("Applications: ${applications.map { "${it.name}(${it.id})" }}")    // Works only with View member profile rights    val profiles = spaceClient.teamDirectory.profiles.getAllProfiles().data    println("Profiles: ${profiles.map { "${it.username}(${it.id})" }}")    // Works only with View project parameters rights    // These rights can be managed for selected projects or for whole projects at one time.    val projects = spaceClient.projects.getAllProjects().data    println("Projects: ${projects.map { "${it.name}(${it.id})" }}")                // Works only with Project Issues: View issues rights    val issues = projects.firstOrNull()?.let { getProjectIssues(it) }    println("Issues: ${issues?.map { "${it.title}(${it.id})" }} ")}private suspend fun getProjectIssues(project: PR_Project): List<Issue> {    return spaceClient.projects.planning.issues.getAllIssues(        project = ProjectIdentifier.Id(project.id),        assigneeId = emptyList(),        statuses = emptyList(),        sorting = IssuesSorting.CREATED,        descending = true    ).data}
Output
Channels: [Booruchan(4UHs4I3yyno1), general(10xDLp0yqy4w), Habrachan(4HyHLw3SnO9Y), Sipichan(15A2hA1RpAsp)]Applications: [client(2czEkY3AIaV0), chatbot(d3Q8Z0UeVCF)]Profiles: [Makentoshe(2iqI4p3gzufl)]Projects: [Booruchan(qN0K31awqo6), Habrachan(1tJHqn2A76Yf), Sipichan(35BreB35gvdA)]Issues: [Add custom Run Configuration and support ngrok startup(3S09oT4JHvpC), Add Client template for Space plugin with Gradle (atQRe1SIklB), Add Blank template for Space application plugin support for Gradle(4REln04HAo5k)]

В зависимости от настроенных прав полученные данные могут отличаться. Стоит обратить внимание, что почти все данные возвращаются через .data. Изначально возвращается объект Batch, который используется для пагинации и содержит текущий набор данных, ключ к следующему набору, и общее количество элементов. При запросах стоит это учитывать, однако для простоты в данной статье пагинация опущена.

Исходники проекта доступны на github и со временем будут пополняться.

Chatbot + slash command Applications

Серверные Applications, по сравнению с клиентскими, требуют дополнительной подготовки. Первым делом нам нужно серверное приложение, на котором будет хоститься наш бот. Мы уже использовали HTTP клиент Ktor, так что можем создавать на нем и серверную часть. Создание конфигурационных файлов для Ktor здесь будет опущено, но есть документация, на которую можно опереться.

Когда мы поднимем наш сервер, мы сможем получить его URL, чтобы Space мог его использовать. Покупать хост, или искать бесплатный не комильфо, поэтому мы пойдем обходным путем - будем использовать туннельный сервис. В официальной документации используется ngrok, однако лично я предпочитаю localtunnel, из-за его возможности указывать постоянный адрес.

// localtunnelnpx localtunnel --port 8080 --subdomain makentoshe// ngrokngrok http 8080

В любом случае мы получим URL - это как раз то, что нам нужно.


Возвращаемся в настройки Applications в нашем Space. В табе Endpoint в поле Endpoint URI нужно будет указать наш url. Для меня это https://makentoshe.loca.lt/api/chatbot. Про то, зачем нужен /api/chatbot будет дальше.

В том же табе существует два способа верифицировать наши запросы:

  • Verification token - этот токен кладется в каждый запрос от Space. Нам остается сравнить эти токены, и если они совпадают - мы общаемся с нашим Space.

Пример ответа с Verification token
{      "className": "ListCommandsPayload",      "accessToken": "",      "verificationToken": "d415ca5965b37f4f0cac59fd33de7b94e396284e897d0fb8a070d0a5e1b7f2d3",      "userId": "2kawvQ4F6GM6"}
  • Signing key - более продвинутый метод. Для каждого запроса создается хеш, который кладется в его заголовок. Когда мы получаем запрос мы также вычисляем хеш, и если они совпадают - все ок. Подробнее об алгоритме - здесь.

Пример ответа с Signing key
POST /api/chatbot HTTP/1.1Host: 12345abcdef.ngrok.ioUser-Agent: Space (61355) Ktor http-clientContent-Length: 163Accept: */*Accept-Charset: UTF-8Content-Type: application/jsonX-Forwarded-For: 123.456.123.456X-Forwarded-Proto: httpsX-Space-Signature: 2aa8cba6217a28686de0ca8dcfe2a1d0795e343d744a0c5307308e43777593a5X-Space-Timestamp: 1607623492912Accept-Encoding: gzip{"className":"ListCommandsPayload","accessToken":"","verificationToken":"d415ca5965b37f4f0cac59fd33de7b94e396284e897d0fb8a070d0a5e1b7f2d3","userId":"2kawvQ4F6GM6"}

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

object Endpoint {    const val verificationToken: String = TODO("Place your verification_token")    const val signingKey: String = TODO("Place your signing_key")    fun verify(payload: ApplicationPayload): Boolean {        return payload.verifyWithToken(verificationToken)    }    fun verify(timestamp: String, signature: String, body: String): Boolean {        val hmacSha256 = Mac.getInstance("HmacSHA256")        hmacSha256.init(SecretKeySpec(signingKey.toByteArray(), "HmacSHA256"))        val hash = hmacSha256.doFinal("$timestamp:$body".toByteArray()).toHexString()        return hash == signature    }    private fun ByteArray.toHexString() = joinToString("") { (0xFF and it.toInt()).toString(16).padStart(2, '0') }}

Первым делом для сервера нам нужно указать Routing. Это тот самый Endpoint для Space, который он будет использовать, чтобы обращаться к нашему боту.

@Suppress("unused") // Referenced in application.conf@kotlin.jvm.JvmOverloadsfun Application.module(testing: Boolean = false) {    install(Routing) {        chatbot()    }}fun Routing.chatbot() {    post("api/chatbot") {        val receiveBody = call.receiveText()        val timestamp = call.request.headers["x-space-timestamp"]            ?: return@post call.respond(HttpStatusCode.BadRequest)        val signature = call.request.headers["x-space-signature"]            ?: return@post call.respond(HttpStatusCode.BadRequest)        if (!Endpoint.verify(timestamp, signature, receiveBody)) {            return@post call.respond(HttpStatusCode.Unauthorized)        }        val payload = readPayload(receiveBody)        if (!Endpoint.verify(payload)) {            return@post call.respond(HttpStatusCode.Unauthorized)        }        try {            processChatbotPayload(payload)        } catch (unknownCommand: IllegalStateException) {            LoggerFactory.getLogger("Chatbot").error(unknownCommand.message)        }    }}

Как только к серверу происходит обращение по заданному адресу, первым делом мы проверяем, что запрос пришел именно от нашего Space. Если это не так - возвращаем 401 Unauthorized.

Далее боту следует обработать полученный payload. На момент написания статьи существует 5 имплементаций ApplicationPayload:

  • MessagePayload - передается нам, когда пользователь отправляет обычное сообщение.

  • ListCommandsPayload - передается нам, когда пользователь начинает сообщение с "/" и затем вводит команду посимвольно. Здесь происходит запрос все существующих команд, которые нам нужно будет вернуть в виде json. На ввод каждого символа передается новый Payload.

  • MessageActionPayload - передается, когда пользователь нажимает на интерактивный элемент сообщения, например, на кнопку. Корректно срабатывают только элементы из последнего сообщения. Все предыдущие элементы будут "стерты"(например, их actionId всегда будут пустой строкой).

  • MenuActionPayload - передается, когда пользователь нажимает на кастомный элемент на одном из меню (ProjectMenu, LocationMenu, ChannelMessageMenu, ChannelAttachmentMenu и т.д.). На момент написания статьи все еще не доступен.

  • ListMenuExtensionsPayload - имеет ту же идею, что и ListCommandsPayload, только для меню. На момент написания статьи все еще не доступен.

private suspend fun PipelineContext<*, ApplicationCall>.processChatbotPayload(payload: ApplicationPayload) {    when (payload) {        is MessagePayload -> {            processChatbotMessagePayload(payload)        }        is MessageActionPayload -> {            processChatbotMessageActionPayload(payload)        }        is ListCommandsPayload -> {            processChatbotListCommandsPayload(payload)        }    }}

Когда пользователь отправляет нам сообщение - скорее всего это команда, которую боту надо выполнить. Команду можно описать одним дата классом:

data class Command(    val name: String,    val info: String,    val action: suspend (payload: MessagePayload) -> Unit) {       fun toCommandDetail() = CommandDetail(name, info)}

Предлагаю в качестве примера реализовать три простых команды:

  • help - выводит сообщение со списком всех команд;

  • echo - выводит переданное сообщение обратно пользователю;

  • interactive <type> - выводит пример выбранного типа интерактивного элемента, например, button.

object Commands {    val help = Command(        "help",        "Show this help",    ) { payload ->        val context = HelpContext.from(payload)        printToChat(context, message {            section {                text(                    """Help message:                    name - Show this help                    ${echo.name} - ${echo.info}                    ${interactive.name} - ${interactive.info}                """.trimIndent()                )            }        })    }    val echo = Command(        "echo",        "Echoing the input string",    ) { payload ->        val context = EchoContext.from(payload)        val body = context.message.body        printToChat(context, message = if (body is ChatMessage.Text) {            message { section { text(body.text) } }        } else {            message { section { text("Skip the Block body") } }        })    }    val interactive = Command(        "interactive",        "Displaying available message interactive elements"    ) { payload ->       // TODO finish later    }    val list = listOf(help, echo, interactive)    val commands: Commands        get() = Commands(list.map { it.toCommandDetail() })}

Сначала разберемся, что происходит в командах help и echo. Из ApplicationPayload каждой команды мы можем извлечь нужные данные для обработки и положить их в соответствующий Context, который мы сами определяем. Этот класс помогает нам аккумулировать данные в одном месте.

Context.kt
interface UserContext {    val userId: String}fun userContext(applicationPayload: ApplicationPayload) = object: UserContext {    override val userId = applicationPayload.userId ?: throw IllegalArgumentException("Payload does not contains user id")}data class EchoContext(override val userId: String, val message: MessageContext) : UserContext {    companion object {        fun from(payload: ApplicationPayload): EchoContext? = when (payload) {            is MessagePayload -> from(payload)            else -> null        }        fun from(payload: MessagePayload): EchoContext {            return EchoContext(payload.userId, payload.message)        }    }}data class HelpContext(override val userId: String): UserContext {    companion object {        fun from(payload: ApplicationPayload): HelpContext {            return HelpContext(payload.userId!!)        }    }}data class InteractiveContext(override val userId: String): UserContext {    companion object {        fun from(payload: ApplicationPayload): InteractiveContext {            return InteractiveContext(payload.userId!!)        }    }}

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

suspend fun printToChat(context: UserContext, message: ChatMessage) {    val member = ProfileIdentifier.Id(context.userId)    spaceClient.chats.messages.sendMessage(MessageRecipient.Member(member), message)}

Любое сообщение в Space представлено в виде класса ChatMessage. Этот класс является sealed и имеет 2 наследника: ChatMessage.Text и ChatMessage.Block.

ChatMessage.Text является простым текстовым сообщением поддерживающим markdown.

ChatMessage.Text(  """  **bold**  `code`  *italic*  @{2iqI4p3gzufl, Maksim Hvostov}   @{2iqI4p3gzufl, Makentoshe}   @{2iqI4p3gzufl, any string may be replaced with my name}   [\#general](im/group/10xDLp0yqy4w)   >quote  """.trimIndent())
Output

ChatMessage.Block существует для более сложных сообщений, которые могут быть разбиты на секции, иметь разделители, и другие продвинутые элементы форматирования.

Для него существует специальный DSL, который мы и используем. Всё начинается с функции message - это корень нашего сообщения, в котором мы можем указать:

  • MessageOutline - это дополнительная подпись под именем отправителя и, по желанию, иконка, которая задается через строку. Какой именно должна быть эта строка пока не понятно, поэтому вместо иконки передаем null.

  • MessageStyle - изменяет некоторые цвета в сообщении, в соответствии со стилем.

  • Для чего нужна messageData я пока так и не понял. Чтобы не было передано в сообщении нашему боту, это поле всегда будет null.

message {this.outline = MessageOutline(null, "Outline text")  this.style = MessageStyle.PRIMARYsection {this.text("Primary message")}}
Outputs

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

В секции нам доступны:

  • обычное текстовое поле через метод text.

  • текстовое поле с тегом, для которого можно добавить отдельный стиль.

  • текстовое поле с изображением справа от секции.

  • текстовое поле с иконкой, в которую опять же передается строка.

  • footer и header.

  • конструкция поле значение через fields

  • интерактивные элементы, из которых на момент написания статьи доступна только кнопка.

Как это выглядит:

message {    this.outline = MessageOutline(null, "Outline text")    this.style = MessageStyle.PRIMARY    section {        this.header = "Section header"        this.footer = "Section footer"        this.text("Plain text")    }    divider()    section {        header = "This tag may indicate something not good"        this.textWithTag("Text with tag", "error tag", MessageStyle.ERROR)    }    section {        this.text("Plain text just to fill some space.")        this.textWithImage("Text with image", "https://www.jetbrains.com/space/img/feedback-section/video-preview.png")    }    section {        this.textWithIcon("Text with icon", "", MessageStyle.WARNING)    }    section {        header = "Fields"        this.fields {            this.field("field1", "value1")            this.field("field2", "value2")            this.field("field3", "value3")            this.field("field4", "value4")        }        this.controls {            this.button("Disabled button without any action", PostMessageAction("", ""), disabled = true)        }    }}
Output

Осталось реализовать команду interaction и соединить все вместе.

val interactive = Command(        "interactive",        "Displaying available message interactive elements"    ) { payload ->        val context = InteractiveContext.from(payload)        val arguments = payload.commandArguments()        if (arguments == null || arguments.isBlank()) {            return@Command printToChat(context, message {                section {                    text("Specify one of the selected ui elements:\nbutton")                }            })        }        printToChat(context, message {            section {                header = "Available message interactive elements"                controls {                    when (arguments) {                        "button" -> {                            val primaryAction = PostMessageAction("ButtonPrimaryActionId", "InteractiveButtonPayloadPrimary")                            button("Primary", primaryAction, MessageButtonStyle.PRIMARY)                            val secondaryAction = PostMessageAction("ButtonSecondaryActionId", "InteractiveButtonPayloadSecondary")                            button("Secondary", secondaryAction, MessageButtonStyle.SECONDARY)                            val regularAction = PostMessageAction("ButtonRegularActionId", "InteractiveButtonPayloadRegular")                            button("Regular", regularAction, MessageButtonStyle.REGULAR)                            val dangerAction = PostMessageAction("ButtonDangerActionId", "InteractiveButtonPayloadDanger")                            button("Danger", dangerAction, MessageButtonStyle.DANGER)                        }                    }                }            }        })    }

Теперь мы можем подключать обработку разных ApplicationPayload's. Для простого сообщения от пользователя мы ищем команду из списка и выполняем её.

private suspend fun PipelineContext<*, ApplicationCall>.processChatbotMessagePayload(payload: MessagePayload) {    Commands.list.find { it.name == payload.command() }?.action?.invoke(payload)        ?: return call.respond(HttpStatusCode.NotFound)    call.respond(HttpStatusCode.OK)}

Для показа списка команд мы возвращаем список всех команд. Здесь же мы используем подключенный ранее Jackson.

private suspend fun PipelineContext<*, ApplicationCall>.processChatbotListCommandsPayload(payload: ListCommandsPayload) {    call.respondText(ObjectMapper().writeValueAsString(Commands.commands), ContentType.Application.Json)}

Для нажатия на интерактивный элемент мы будем принтить actionId этой команды.

private suspend fun PipelineContext<*, ApplicationCall>.processChatbotMessageActionPayload(payload: MessageActionPayload) {    printToChat(userContext(payload), message { section { text(payload.actionId) } })    call.respond(HttpStatusCode.OK)}

Исходники чатбота доступны по ссылке на github.

Итого

Почти все, что может сделать пользователь используя Space через пользовательский интерфейс, можно сделать и через предоставленный Api(если на то будут предоставлены разрешения).

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

Вот остальные способы расширения функционала платформы:

Подробнее..
Категории: Kotlin , Space , Jetbrains space

Space публичный релиз командной среды от JetBrains

10.12.2020 18:18:39 | Автор: admin

Привет, Хабр!

Мы рады представить публичную версию Space универсального и расширяемого инструмента для командной работы, разработки ПО, общения, управления проектами и командами.

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

Релиз публичной версии Space состоялся 9 декабря, и ниже вы можете посмотреть запись нашей презентации. (Можно выбрать русские субтитры).

Какие проблемы решает Space?

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

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

Все для команд в одном инструменте

Space объединяет в себе инструменты, необходимые для поддержки полного цикла разработки ПО любого масштаба:

  • Хостинг Git-репозиториев.

  • Код-ревью с поддержкой merge-реквестов и критериев качества.

  • Автоматизация сборки и развертывания приложений.

  • Инструменты для управления проектами: чеклисты для планирования, баг-трекер и визуальные доски задач.

  • Реестры пакетов и контейнеров для публикации артефактов.

А главное все это идет вкупе с инструментами для эффективной коммуникации и взаимодействия:

Больше пользы. Меньше отвлекающих факторов.

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

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

Адаптация к изменениям

В 2020 году JetBrains, как и миллионам других компаний по всему миру, пришлось быстро адаптироваться к условиям удаленной работы. И Space вовремя доказал свою эффективность на деле.

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

Что говорят наши первые пользователи

С тех пор, как мы открыли программу раннего доступа на KotlinConf в декабре 2019 года, к Space присоединилось около 25 тысяч организаций. Пользователи раннего доступа отправили более 1,5 миллиона сообщений, создали 38+ тысяч задач и 13+ тысяч репозиториев, открыли 12+ тысяч код-ревью.

Мы хотим сказать огромное спасибо всем пользователям ранних версий Space! Без вашей помощи Space не был бы таким, каким вы его видите сегодня.

Вы можете посмотреть, что о нас говорят наши пользователи, а также прочитать их истории.

Что было сделано за 2020

В этом году команда Space приложила много усилий для воплощения нашего видения того, как должна выглядеть интегрированная среда для командной работы.

Вот, что мы внедрили за 2020 год:

Давайте посмотрим на Space в действии!

Space как платформа и экосистема партнеров

С самого начала мы разрабатывали Space как платформу и с точки зрения расширяемости, и с точки зрения бизнес-возможностей. Мы стремимся дать нашим пользователям как можно больше возможностей кастомизации и расширения.

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

Каждая организация уникальна, поэтому Space есть все необходимое для поддержки самых разных процессов. Наши партнеры могут помогать своим клиентам адаптировать Space к их потребностям и извлечь из продукта максимум пользы. Мы предлагаем три партнерские программы, которые охватывают большинство моделей бизнес-партнерства: партнерство по продажам, сервисное партнерство и технологическое партнерство.

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

Планы на 2021

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

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

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

Вот, что еще входит в наши планы на 2021 год:

  • Локальная версия и удобный переход с облачной на локальную версию Space. Вы можете попробовать облачную версию уже сейчас, а затем плавно перейти на локальную версию.

  • Локализация. Нам важно, чтобы среда, в которой вы работаете, говорила с вами на одном языке.

  • Видеозвонки, как встроенные, так и из внешних инструментов посредством интеграций.

  • Продолжим совершенствовать, корректировать и адаптировать существующую функциональность Space.

  • Добавим больше способов миграции и интеграций, в частности синхронизацию с Outlook/Office 365 и Google Календарем.

  • Сделаем более тесную интеграцию различных частей Space и расширим интеграцию с внешними инструментами с помощью приложений.

Цены

Space доступен бесплатно для неограниченного количества пользователей. Более широкий функционал доступен на платных подписках от 8$ за активного пользователя в месяц. Подробнее о стоимости, разнице подписок и спецпредложениях для образовательных учреждений мы рассказываем на этой странице.

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

Попробуйте Space бесплатно

Чтобы начать знакомиться с Space, необязательно настраивать инструмент для всей организации. Работайте с репозиториями, отслеживайте задачи, управляйте командами, редактируйте документы вместе с коллегами или просто общайтесь в чате.

Начните использовать Space бесплатно и пригласите всю свою команду.

Подробнее..

Китайские SpaceX и Blue Origin наступают

18.06.2020 10:17:09 | Автор: admin

На фото космическая компания Galactic Energy тестирует разделение створок головного обтекателя. Источник: Galactic Energy.

Китайская частная космонавтика весьма молодое явление. В 2014 году Китай открыл сектор космической отрасли для частного капитала и за прошедшие годы было создано более 140 коммерческих аэрокосмических компаний, разрабатывающих ракеты-носители, небольшие спутниковые платформы, спутниковые группировки дистанционного зондирования и связи, наземные станции и различные части цепочек поставок.

Ряд китайских частных пусковых компаний сообщили о прогрессе в разработке ряда ракет-носителей для растущего коммерческого космического сектора (в том числе и многоразовых).

Landspace и iSpace сообщают о прогрессе с метановыми ракетными двигателями, в то время как Galactic Energy приближается к запуску своей ракеты-носителя Ceres-1. Тем временем Deep Blue Aerospace получила финансирование для разработки целой серии жидкостных ракет-носителей.

iSpace многоразовый носитель и тесты вертикальной посадки


Базирующаяся в Пекине iSpace, которая вышла на орбиту еще в июле 2019 года, также продвинулась в разработке собственного кислород-метанового двигателя.


Концепция Hyperbola-2 подразумевает 9 двигателей на первой ступени, решетчатые рули, посадочные опоры, в точь как на Falcon9. Источник: iSpace.

19 мая двигатель JD-1 с тягой 19 тонн-сил прошел 200-секундное огневое испытание. Двигатель прошел испытание на повторный запуск 27 мая, открыв дорогу для будущих испытаний на вертикального взлета и посадки. Двигатели JD-1 будут приводить в действие многоразовую ракету-носитель Hyperbola-2, способную доставить 1900 кг на низкую околоземную орбиту (НОО).

Компания намерена на первом этапе провести 100-километровые испытания взлета и посадки Hyperbola-2 в конце этого года. Полноценный орбитальный испытательный полет запланирован на первую половину 2021 года.




Вице-президент iSpace Яо Боуэн заявил китайским СМИ после миссии SpaceX Demo-2, что SpaceX еще дальше, но мы разделяем их видение отправки пассажиров в космос с помощью экономически эффективных разработок.

Landspace


Landspace в начале мая завершила три огневых испытания двигателя на криогенном метане и жидком кислороде SkyLark (Tianque-12) с тягой на уровне 80 т. Tianque-11, меньший 10-тонный двигатель также на жидким кислороде и метане, 5 июня успешно завершил прожиг длительностью 2 000 секунд.



Двигатели будут приводить в действие двухступенчатую ракету-носитель Zhuque-2 высотой 48,8 метра, способную доставить 4000 кг на 200-километровую НОО. Также Zhuque-2 может поднимать 2000 кг на 500-километровую солнечно-синхронную орбиту (ССО).

Ведущий инженер LandSpace по двигательным установкам заявляет, что успех испытаний Tianque-12 является первым тестом совместимости двигателя и системы управления, подтверждая летную конфигурацию двигателя для Zhuque-2. Теперь носитель вступает в фазу интеграционного тестирования перед первым запуском в конце 2020 или начале 2021 года.


Компания LandSpace проводит огневые испытания кислородно-метанового двигателя для ракеты Tianque-12. Источник: LandSpace

Landspace была основана в 2015 году, а в октябре 2018 года была предпринята первая попытка частного китайского запуска с помощью твердой ракеты Zhuque-1.

Galactic Energy близка к запуску Ceres-1


Galactic Energy, созданная в феврале 2018 года и являющаяся одной из новых частных китайских компаний, быстро прогрессирует и в этом году уже планирует осуществить первый запуск. Четырехступенчатая твердотопливная ракета Ceres-1 сможет доставить до 350 кг полезной нагрузки на 200-километровую орбиту.

Генеральный директор и основатель Galactic Energy Лю Байки считает, что сейчас правильный момент для экспансии в сфере космических пусков и мы скоро увидим космическую экономику на триллион долларов. Лю приводит прогнозы о том, что в период с 2018 по 2025 год в мире будет запущено около 20 000 спутников, причем значительная их часть приходится на Китай. Таким образом, рынок требует увеличения поставок недорогих, высоконадежных ракет-носителей.

По словам Лю, запуск ракеты Ceres-1 обойдется клиентам в фиксированную сумму 4 миллиона долларов США, и мы работаем над снижением цены запуска до уровня менее 10 000 долларов США за килограмм. В то время как Falcon 9 от SpaceX с частичным повторным использованием предлагает более низкие цены за килограмм, меньшие спутники, которые являются попутной нагрузкой, зависят расписания и выбранной орбиты основной полезной нагрузки.


Твердотопливные ускорители для Ceres-1 в сборочном цехе. Источник Galactic Energy.

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

Galactic Energy была нацелена на запуск Ceres-1 в июне, но пандемия COVID-19 неизбежно привела к задержкам. Теперь запуск с космодрома Jiuquan ожидается не раньше августа-сентября.

В декабре фирма получила финансирование в размере 21,5 млн. Долл. США для запуска ракеты-носителя Ceres-1 и разработки ракеты-носителя Pallas-1 на керосине (RP-1) и жидком кислороде. Обе ракеты в стильном черном цвете напоминают Electron от Rocket Lab. Частично многоразовый Pallas-1, названный в честь большого астероида, сможет запустить 4 метрических тонны на НОО или две тонны для солнечно-синхронную орбиту. Как и Falcon9, первая ступень сможет приземлиться вертикально после запуска благодаря кластеру из семи двигателей переменной тяги, решетчатых рулей и посадочных опор, а затем запущена повторно.

На вопрос, почему Galactic Energy выбрала RP-1, а китайские фирмы Landspace и iSpace разрабатывают двигатели на жидком метане, Лю привел в пример SpaceX. Ракета SpaceX Falcon 9 на жидком кислороде/керосине показывает, что этот вариант пригоден для повторного использования, говорит Лю.



Galactic Energy планирует вывести Pallas-1 на стартовую площадку к концу 2022 года. Компания завершила огневые испытания нового газогенератора собственной разработки для двигателя Cangqiong с тягой 40 тонн-сил.

Deep Blue Aerospace


Пекинская компания Deep Blue Aerospace Technology Co., Ltd. объявила в пятницу, что ей удалось получить начальное финансирование на сумму более 14,1 млн. Долларов (100 млн. Юаней). Финансирование будет использовано для исследований и разработок, проверки технологии вертикальной посадки и испытаний керосин-кислородных жидкостных двигателей.

Основанная в 2017 году, Deep Blue Aerospace разрабатывает жидкостные ракеты-носители Nebula-1 и многоразовые Nebula-2. Первая ступень Nebula-1 будет оснащаться девятью двигателями Leiting-20, а на второй одним двигателем, что полностью повторяет концепцию ракеты Falcon9 от компании SpaceX и позволяет имея не самый мощный двигатель быстро разработать и протестировать дизайн РН.

Nebula-1 диаметром 2,25 метра и Nebula-2 диаметром 3,35 метра будут способны запускать 500 кг на 500 км солнечно-синхронную орбиту и 4500 кг на НОО, соответственно. Дата проведения тестовых полетов не указана.

Национальная комиссия Китая по развитию и реформам на апрельском заседании добавила спутниковый интернет в дорожную карту развития Новых инфраструктур. Спутниковый интернет, 5G, Интернет вещей и искусственный интеллект все это приоритетные объекты для инвестиций. Этот шаг воспринимается как попытка стимулировать инновации в технологическом секторе и ещё больше стимулировать экономику Китая.

Источник

Также есть новость не из Китая о новой ракете Vulcan от компании ULA, которая будет использовать топливную пару метан-кислород на первой ступени:
Все части ракеты-носителя для первого полета Vulcan почти готовы, заявил Тори Бруно, генеральный директор и президент ULA, который также отметил, что ULA не пропустит сроки, тогда как Vulcan готовится к своему первому запуску в начале 2021 со стартовой площадки SLC-41 во Флориде. В первой миссии Vulcan запустит посадочный аппарат Peregrine от компании Astrobotic на Луну, которая, в свою очередь, является частью программы CLPS НАСА по отправке полезных грузов на поверхность нашего спутника.
Подробнее..

Заметка о специфике разработки электроники в космической отрасли

18.05.2021 14:13:42 | Автор: admin

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

Оловянные усы и защитное покрытие

Начнем с необычного. Автор не сомневается, что всем разработчикам сверхнадежных систем хорошо знакомо явление под названием оловянные усы (tin whiskers) тонкие длинные кристаллы-нити, которые могут вырастать из поверхностей выводов микросхем или припоя.

Пример оловянных усов (tin whiskers)Пример оловянных усов (tin whiskers)

Растут они довольно быстро, настолько, что спустя месяцы эти усы могут просто замкнуть соседние выводы. Один из вариантов нейтрализации этого процесса нанесение определенного защитного покрытия, выбор которого не нетривиален.

Пример применения защитного покрытия (conformal coating)Пример применения защитного покрытия (conformal coating)Процесс нанесения защитного покрытияПроцесс нанесения защитного покрытия

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

Механические нагрузки

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

Применение клея по периметру BGA микросхемыПрименение клея по периметру BGA микросхемы

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

Полная заливка модуляПолная заливка модуля

Температура в космосе

С температурой не всё так однозначно, потому что многие факторы играют роль: локация узла в пределах спутника, орбита, инсоляция, ориентация спутника. Если мы говорим об аппарате на LEO (Low Earth Orbit), сам блок стоит внутри аппарата, а система термоконтроля и термобаланса адекватна задаче, то температура большую часть времени будет в среднем держаться в районе 15C. При уходе спутника в тень Земли на 30 минут она может падать до 5C-10C, а потом, при возвращении на освещенную сторону, снова подниматься до 15C-20C.

Если мы говорим об устройстве, расположенном на внешней стороне аппарата, например, об антенне SAR (Synthetic-aperture radar), то при повороте в сторону глубокого космоса температура может упасть до -70C в пределах десятков минут или часов. Это представляет опасность, поскольку речь заходит о предельных значениях не только для automotive grade компонентов (которые часто идут в ход в молодых New Space стартапах), но и вполне себе Military grade, которые сертифицируются до -55C.
BGA корпуса особенно не любят таких перепадов температуры чем больше размером корпус, тем больше шансов, что крайние контактные площадки просто оторвёт, или треснут шарики припоя.

Кросс-секция платы и BGA корпуса, демонстрирующая отрыв шара от падаКросс-секция платы и BGA корпуса, демонстрирующая отрыв шара от падаВнутренняя структура CCGA корпусов.Внутренняя структура CCGA корпусов.

Если говорить более предметно о платах и CADах, то стоит упомянуть, что вся космическая электроника в плане механики это платы, плотно упакованные в металлический корпус. Иначе нельзя конвекции в космосе нет, если что-то греется, то это тепло необходимо сбрасывать через термоинтерфейс. При этом корпус должен быть компактен, зачастую иметь специфическую форму, включать в себя множество деталей, разной электроники словом, тут крайне важна интеграция электронного и машинного проектирования; тут стоят очень серьезные задачи в плане соотнесения 3D компонентов.

Радиация

Вариант взаимодействия излучения с веществом, эффект КомптонаВариант взаимодействия излучения с веществом, эффект Комптона

С радиацией всё ещё сложнее: это, например, и фактор поглощенной дозы, и эффекты однократного действия, то есть явления, связанные с высоко энергичными заряженными частицами и их влиянием на электронику. Скажем, пролет заряженной частицы может вызвать сбой в ячейке памяти, когда единственный бит меняется с 0 на 1 и аппарат выдает ложные показания (bit flip). В большинстве случаев ситуация не критична и последствия устраняются перезагрузкой системы. Куда серьезнее, например, защелкивание выходного каскада, когда сквозной ток может выжечь микросхему и привести к катастрофическому отказу оборудования. Тут перезагрузка уже не поможет, а прилететь и починить плату в космосе понятно некому.

Пример механизма взаимодействия космической высокоэнергичной частицы с внутренней структурой MOSFET.Пример механизма взаимодействия космической высокоэнергичной частицы с внутренней структурой MOSFET.

Фактор поглощенной дозы по-разному влияет на электронику, изготовленную по технологиям КМОП и БиКМОП. Влияет даже скорость набора порой быстро накопленная доза менее опасна своими последствиями, чем та же самая доза, накопленная в течение продолжительного времени, а иногда наоборот. Вообще, на этот счет хочу порекомендовать статьи на Хабре за авторством доки аналогового дизайна радхардных чипов Валерия amartology Шункова.

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

В New Space скорость появления MVP (Minimum Viable Product) критична, и проведение испытаний на радиационную стойкость в специальной лаборатории in vitro может быстро дать оценку того, что ждет устройство в условиях космоса. Опять же, тут нужно определить, что называть радиацией: 510 krad часто современные микросхемы уровня мейнстримовых микроконтроллеров спокойно переживают такую накопленную дозу без значительных последствий, а это уровень набора за несколько месяцев, а иногда пары лет в случае LEO.

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

Old Space же это хорошо известные нам керамические корпуса, радиационная стойкость, поистине космические ценники на каждый элемент конструкции, бесконечное тестирование и работа со статистикой, и так далее в результате один единственный чип может стоить десятки, а иногда и сотни тысяч долларов. Зачастую трудно сказать, для какой задачи какой подход более оптимален, но можно порадоваться, что обе эти сферы развиваются параллельно, и в итоге займут свой спектр ниш.

Выбор компонентов

Это фантастически обширная область для обсуждения, а полноценное описание возможных failure modes, последствий и важных моментов при выборе компонентов потянет на серьезный труд во много сотен страниц.
Однако же, с чего-то нужно начинать, и, поскольку статья носит ознакомительный характер, то будет справедливо выбрать MLCC в качестве предмета обсуждения, поскольку их можно встретить в любом мыслимом устройстве, а также этот класс компонентов является лидером причин катастрофических отказов в электронных узлах.

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

Трещина в структуре MLCC ввиду изгиба платыТрещина в структуре MLCC ввиду изгиба платы

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

Простейшие, дешевые и очевидные трюки для снижения вероятности и/или катастрофичности последствий можно подсмотреть как раз в automotive сериях MLCC.
Например, проводится редукция проводящих слоев, что даёт отступ от края терминала области, где вероятность появления трещин максимальна. Open Mode Design более-менее общеупотребительный термин для описания подобной техники, идея которой самоочевидна: трещина, возникшая в непосредственной близости к терминалам, не приводит к короткому замыканию.
Из отрицательных последствий применения техники можно упомянуть снижение доступной емкости ввиду уменьшения площади обкладок.

Выбор материалов

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

Трещина в паянном узле, вопрочем, полостной дефект наверняка внёс дополнительный вклад :)Трещина в паянном узле, вопрочем, полостной дефект наверняка внёс дополнительный вклад :)

А, например, недальновидность при выборе материалов для сборки структуры PCB неизбежно приводят к понижению надежности самых уязвимых структур VIA.

Даже выбор поставщика может быть критически важным: например, ненадлежащие хранение материалов для PCB или же самих готовых устройств может приводить к пренеприятнейшим эффектам, например, к появлению проводящих структур внутри сэндвича печатной платы, известных под названием CAF (conductive anodic filament).

Замыкание соседних via посредством CAFЗамыкание соседних via посредством CAF

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

Прогар в сэндвиче PCB, вызванный CAF: КЗ произошло в толще материала между шиной питания +48V и землей. Выделяющиеся при горении газы распёрли сэндвич платы изнутри, что и обуславливало "вспученность" в области прогара.Прогар в сэндвиче PCB, вызванный CAF: КЗ произошло в толще материала между шиной питания +48V и землей. Выделяющиеся при горении газы распёрли сэндвич платы изнутри, что и обуславливало "вспученность" в области прогара.

А вот так этот прогар выглядел с обратной стороны.

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

Моделирование

Разумеется, помимо превентивных мер по повышению надежности огромное внимание отдается моделированию. Считают всё: и температуру, и механически нагрузки, и радиационные эффекты.

Моделирование температурного профиляМоделирование температурного профиляМоделирование механических нагрузокМоделирование механических нагрузокЕще пример моделирования, сравнение влияния различных конфигураций крепёжных винтовЕще пример моделирования, сравнение влияния различных конфигураций крепёжных винтов

О моделировании SI/PI даже упоминать не приходится: применение этих практик безусловно для сколь-нибудь серьезных проектов.

Моделирование Signal IntegrityМоделирование Signal IntegrityМоделирование Power IntegrityМоделирование Power Integrity

Project management, коллектив и инструментарий

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

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

Заключение

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

Мой канал в Телеграм об интересном из мира электроники Something Interesting in Electronics.

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru