You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Web-Dev-For-Beginners/translations/ru/6-space-game/3-moving-elements-around/README.md

37 KiB

Создание космической игры, часть 3: добавление движения

Подумайте о ваших любимых играх что делает их увлекательными? Это не только красивые графики, но и то, как всё движется и реагирует на ваши действия. Сейчас ваша космическая игра похожа на красивую картину, но мы собираемся добавить движение, чтобы оживить её.

Когда инженеры NASA программировали компьютер навигации для миссий «Аполлон», они сталкивались с похожей задачей: как заставить космический корабль реагировать на команды пилота, одновременно автоматически корректируя курс? Принципы, которые мы изучим сегодня, перекликаются с этими концепциями управление движением игрока в сочетании с автоматическим поведением системы.

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

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

Тест перед лекцией

Тест перед лекцией

Понимание движения в играх

Игры оживают, когда объекты начинают двигаться, и существует два основных способа, как это происходит:

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

Заставить объекты двигаться на экране компьютера проще, чем вы думаете. Помните те координаты x и y из уроков математики? Именно с ними мы будем работать. Когда Галилей отслеживал спутники Юпитера в 1610 году, он фактически делал то же самое фиксировал позиции во времени, чтобы понять закономерности движения.

Движение объектов на экране похоже на создание анимации в виде книжки с картинками нужно следовать этим трём простым шагам:

  1. Обновить позицию изменить местоположение объекта (например, переместить его на 5 пикселей вправо)
  2. Стереть старый кадр очистить экран, чтобы не видеть следы предыдущего кадра
  3. Нарисовать новый кадр разместить объект в новом месте

Делайте это достаточно быстро, и вуаля! У вас есть плавное движение, которое кажется естественным для игроков.

Вот как это может выглядеть в коде:

// Set the hero's location
hero.x += 5;
// Clear the rectangle that hosts the hero
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Redraw the game background and hero
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = "black";
ctx.drawImage(heroImg, hero.x, hero.y);

Что делает этот код:

  • Обновляет координату x героя на 5 пикселей для горизонтального движения
  • Очищает всю область холста, чтобы удалить предыдущий кадр
  • Заполняет холст чёрным цветом фона
  • Перерисовывает изображение героя на новой позиции

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

Обработка событий клавиатуры

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

События клавиатуры происходят на уровне окна, то есть весь ваш браузер слушает эти нажатия клавиш. Щелчки мыши, с другой стороны, могут быть привязаны к конкретным элементам (например, клик по кнопке). Для нашей космической игры мы сосредоточимся на управлении с клавиатуры, так как это придаёт игре классическое аркадное ощущение.

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

Чтобы обработать событие, нужно использовать метод addEventListener() окна и передать ему два параметра. Первый параметр это название события, например, keyup. Второй параметр это функция, которая должна быть вызвана в результате произошедшего события.

Вот пример:

window.addEventListener('keyup', (evt) => {
  // evt.key = string representation of the key
  if (evt.key === 'ArrowUp') {
    // do something
  }
});

Разбор происходящего:

  • Слушает события клавиатуры на всём окне
  • Захватывает объект события, который содержит информацию о нажатой клавише
  • Проверяет, совпадает ли нажатая клавиша с определённой (в данном случае стрелка вверх)
  • Выполняет код, если условие выполнено

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

  • key - строковое представление нажатой клавиши, например 'ArrowUp'
  • keyCode - числовое представление, например 37, соответствует ArrowLeft

Манипуляция событиями клавиш полезна не только в разработке игр. Какие ещё применения этого метода вы можете придумать?

Особые клавиши: обратите внимание!

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

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

const onKeyDown = function (e) {
  console.log(e.keyCode);
  switch (e.keyCode) {
    case 37:
    case 39:
    case 38:
    case 40: // Arrow keys
    case 32:
      e.preventDefault();
      break; // Space
    default:
      break; // do not block other keys
  }
};

window.addEventListener('keydown', onKeyDown);

Понимание этого кода предотвращения:

  • Проверяет определённые коды клавиш, которые могут вызвать нежелательное поведение браузера
  • Предотвращает стандартное действие браузера для стрелок и пробела
  • Позволяет другим клавишам работать нормально
  • Использует e.preventDefault(), чтобы остановить встроенное поведение браузера

Движение, вызванное игрой

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

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

const id = setInterval(() => {
  // Move the enemy on the y axis
  enemy.y += 10;
}, 100);

Что делает этот код движения:

  • Создаёт таймер, который запускается каждые 100 миллисекунд
  • Обновляет координату y врага на 10 пикселей каждый раз
  • Сохраняет идентификатор интервала, чтобы можно было остановить его позже, если нужно
  • Перемещает врага вниз по экрану автоматически

Игровой цикл

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

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

Эта концепция напоминает, как ранние аниматоры, такие как Уолт Дисней, должны были перерисовывать персонажей кадр за кадром, чтобы создать иллюзию движения. Мы делаем то же самое, только с помощью кода, а не карандашей.

Вот как обычно выглядит игровой цикл, выраженный в коде:

const gameLoopId = setInterval(() => {
  function gameLoop() {
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    ctx.fillStyle = "black";
    ctx.fillRect(0, 0, canvas.width, canvas.height);
    drawHero();
    drawEnemies();
    drawStaticObjects();
  }
  gameLoop();
}, 200);

Понимание структуры игрового цикла:

  • Очищает весь холст, чтобы удалить предыдущий кадр
  • Заполняет фон сплошным цветом
  • Рисует все игровые объекты в их текущих позициях
  • Повторяет этот процесс каждые 200 миллисекунд для создания плавной анимации
  • Управляет частотой кадров, контролируя интервал времени

Продолжение космической игры

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

Возьмите код, на котором мы остановились в предыдущем уроке (или начните с кода в папке Part II- starter, если вам нужен новый старт).

Вот что мы создаём сегодня:

  • Управление героем: Стрелки будут управлять вашим космическим кораблём на экране
  • Движение врагов: Эти инопланетные корабли начнут своё наступление

Давайте начнём реализацию этих функций.

Рекомендуемые шаги

Найдите файлы, которые были созданы для вас в подпапке your-work. Она должна содержать следующее:

-| assets
  -| enemyShip.png
  -| player.png
-| index.html
-| app.js
-| package.json

Начните ваш проект в папке your-work, введя команду:

cd your-work
npm start

Что делает эта команда:

  • Переходит в директорию вашего проекта
  • Запускает HTTP-сервер по адресу http://localhost:5000
  • Обслуживает файлы вашей игры, чтобы вы могли протестировать их в браузере

Эта команда запустит HTTP-сервер по адресу http://localhost:5000. Откройте браузер и введите этот адрес, сейчас он должен отображать героя и всех врагов; пока ничего не движется но скоро начнёт!

Добавление кода

  1. Добавьте отдельные объекты для hero, enemy и game object, они должны иметь свойства x и y. (Помните раздел о Наследовании или композиции).

    ПОДСКАЗКА game object должен быть объектом с x и y и способностью рисовать себя на холсте.

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

    class GameObject {
      constructor(x, y) {
        this.x = x;
        this.y = y;
        this.dead = false;
        this.type = "";
        this.width = 0;
        this.height = 0;
        this.img = undefined;
      }
    
      draw(ctx) {
        ctx.drawImage(this.img, this.x, this.y, this.width, this.height);
      }
    }
    

    Понимание базового класса:

    • Определяет общие свойства, которые разделяют все игровые объекты (позиция, размер, изображение)
    • Включает флаг dead для отслеживания, должен ли объект быть удалён
    • Предоставляет метод draw(), который отображает объект на холсте
    • Устанавливает значения по умолчанию для всех свойств, которые могут быть переопределены дочерними классами

    Теперь расширьте этот GameObject, чтобы создать Hero и Enemy:

    class Hero extends GameObject {
      constructor(x, y) {
        super(x, y);
        this.width = 98;
        this.height = 75;
        this.type = "Hero";
        this.speed = 5;
      }
    }
    
    class Enemy extends GameObject {
      constructor(x, y) {
        super(x, y);
        this.width = 98;
        this.height = 50;
        this.type = "Enemy";
        const id = setInterval(() => {
          if (this.y < canvas.height - this.height) {
            this.y += 5;
          } else {
            console.log('Stopped at', this.y);
            clearInterval(id);
          }
        }, 300);
      }
    }
    

    Основные концепции этих классов:

    • Наследует от GameObject с помощью ключевого слова extends
    • Вызывает конструктор родителя с помощью super(x, y)
    • Устанавливает конкретные размеры и свойства для каждого типа объекта
    • Реализует автоматическое движение врагов с использованием setInterval()
  2. Добавьте обработчики событий клавиш, чтобы управлять движением героя (вверх/вниз, влево/вправо)

    ПОМИНИТЕ, что это декартова система, верхний левый угол это 0,0. Также не забудьте добавить код для остановки стандартного поведения.

    Совет: Создайте функцию onKeyDown и прикрепите её к окну:

    const onKeyDown = function (e) {
      console.log(e.keyCode);
      // Add the code from the lesson above to stop default behavior
      switch (e.keyCode) {
        case 37:
        case 39:
        case 38:
        case 40: // Arrow keys
        case 32:
          e.preventDefault();
          break; // Space
        default:
          break; // do not block other keys
      }
    };
    
    window.addEventListener("keydown", onKeyDown);
    

    Что делает этот обработчик событий:

    • Слушает события нажатия клавиш на всём окне
    • Регистрирует код клавиши, чтобы помочь вам отладить, какие клавиши нажимаются
    • Предотвращает стандартное поведение браузера для стрелок и пробела
    • Позволяет другим клавишам работать нормально

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

  3. Реализуйте паттерн Pub-Sub, это поможет сохранить ваш код чистым, следуя оставшимся частям.

    Паттерн Publish-Subscribe помогает организовать ваш код, разделяя обнаружение событий и их обработку. Это делает код более модульным и удобным для поддержки.

    Для выполнения этого последнего шага вы можете:

    1. Добавить слушатель событий на окно:

      window.addEventListener("keyup", (evt) => {
        if (evt.key === "ArrowUp") {
          eventEmitter.emit(Messages.KEY_EVENT_UP);
        } else if (evt.key === "ArrowDown") {
          eventEmitter.emit(Messages.KEY_EVENT_DOWN);
        } else if (evt.key === "ArrowLeft") {
          eventEmitter.emit(Messages.KEY_EVENT_LEFT);
        } else if (evt.key === "ArrowRight") {
          eventEmitter.emit(Messages.KEY_EVENT_RIGHT);
        }
      });
      

    Что делает эта система событий:

    • Обнаруживает ввод с клавиатуры и преобразует его в пользовательские игровые события
    • Разделяет обнаружение ввода и игровую логику
    • Упрощает изменение управления позже без влияния на игровой код
    • Позволяет нескольким системам реагировать на один и тот же ввод
    1. Создайте класс EventEmitter, чтобы публиковать и подписываться на сообщения:

      class EventEmitter {
        constructor() {
          this.listeners = {};
        }
      
        on(message, listener) {
          if (!this.listeners[message]) {
            this.listeners[message] = [];
          }
          this.listeners[message].push(listener);
        }
      
      
    2. Добавьте константы и настройте EventEmitter:

      const Messages = {
        KEY_EVENT_UP: "KEY_EVENT_UP",
        KEY_EVENT_DOWN: "KEY_EVENT_DOWN",
        KEY_EVENT_LEFT: "KEY_EVENT_LEFT",
        KEY_EVENT_RIGHT: "KEY_EVENT_RIGHT",
      };
      
      let heroImg, 
          enemyImg, 
          laserImg,
          canvas, ctx, 
          gameObjects = [], 
          hero, 
          eventEmitter = new EventEmitter();
      

    Понимание настройки:

    • Определяет константы сообщений, чтобы избежать опечаток и упростить рефакторинг
    • Объявляет переменные для изображений, контекста холста и состояния игры
    • Создаёт глобальный EventEmitter для системы Pub-Sub
    • Инициализирует массив для хранения всех игровых объектов
    1. Инициализируйте игру

      function initGame() {
        gameObjects = [];
        createEnemies();
        createHero();
      
        eventEmitter.on(Messages.KEY_EVENT_UP, () => {
          hero.y -= 5;
        });
      
        eventEmitter.on(Messages.KEY_EVENT_DOWN, () => {
          hero.y += 5;
        });
      
        eventEmitter.on(Messages.KEY_EVENT_LEFT, () => {
          hero.x -= 5;
        });
      
      
  4. Настройте игровой цикл

    Переработайте функцию window.onload, чтобы инициализировать игру и настроить игровой цикл с хорошим интервалом. Вы также добавите лазерный луч:

    window.onload = async () => {
      canvas = document.getElementById("canvas");
      ctx = canvas.getContext("2d");
      heroImg = await loadTexture("assets/player.png");
      enemyImg = await loadTexture("assets/enemyShip.png");
      laserImg = await loadTexture("assets/laserRed.png");
    
      initGame();
      const gameLoopId = setInterval(() => {
        ctx.clearRect(0, 0, canvas.width, canvas.height);
        ctx.fillStyle = "black";
        ctx.fillRect(0, 0, canvas.width, canvas.height);
        drawGameObjects(ctx);
      }, 100);
    };
    

    Понимание настройки игры:

    • Ожидает, пока страница полностью загрузится, перед началом
    • Получает элемент холста и его 2D-контекст рендеринга
    • Загружает все изображения асинхронно с использованием await
    • Запускает игровой цикл с интервалом 100 мс (10 FPS)
    • Очищает и перерисовывает весь экран каждый кадр
  5. Добавьте код, чтобы перемещать врагов через определённый интервал

    Переработайте функцию createEnemies(), чтобы создать врагов и добавить их в новый класс gameObjects:

    function createEnemies() {
      const MONSTER_TOTAL = 5;
      const MONSTER_WIDTH = MONSTER_TOTAL * 98;
      const START_X = (canvas.width - MONSTER_WIDTH) / 2;
      const STOP_X = START_X + MONSTER_WIDTH;
    
      for (let x = START_X; x < STOP_X; x += 98) {
        for (let y = 0; y < 50 * 5; y += 50) {
          const enemy = new Enemy(x, y);
          enemy.img = enemyImg;
          gameObjects.push(enemy);
        }
      }
    }
    

    Что делает создание врагов:

    • Рассчитывает позиции для центрирования врагов на экране
  • Создает сетку врагов с помощью вложенных циклов
  • Назначает изображение врага каждому объекту врага
  • Добавляет каждого врага в глобальный массив игровых объектов

и добавьте функцию createHero(), чтобы выполнить аналогичный процесс для героя.

```javascript
function createHero() {
  hero = new Hero(
    canvas.width / 2 - 45,
    canvas.height - canvas.height / 4
  );
  hero.img = heroImg;
  gameObjects.push(hero);
}
```

Что делает создание героя:

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

и, наконец, добавьте функцию drawGameObjects(), чтобы начать отрисовку:

```javascript
function drawGameObjects(ctx) {
  gameObjects.forEach(go => go.draw(ctx));
}
```

Понимание функции отрисовки:

  • Итерируется по всем игровым объектам в массиве
  • Вызывает метод draw() для каждого объекта
  • Передает контекст холста, чтобы объекты могли сами себя отрисовать

Ваши враги должны начать наступление на ваш космический корабль!
}
}
```

and add a `createHero()` function to do a similar process for the hero.

```javascript
function createHero() {
  hero = new Hero(
    canvas.width / 2 - 45,
    canvas.height - canvas.height / 4
  );
  hero.img = heroImg;
  gameObjects.push(hero);
}
```

и, наконец, добавьте функцию drawGameObjects(), чтобы начать отрисовку:

```javascript
function drawGameObjects(ctx) {
  gameObjects.forEach(go => go.draw(ctx));
}
```

Ваши враги должны начать наступление на ваш космический корабль!


Вызов агента GitHub Copilot 🚀

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

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

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

Узнайте больше о режиме агента здесь.

🚀 Вызов

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

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

  • Группировка связанных функций вместе с четкими заголовками комментариев
  • Разделение обязанностей отделите игровую логику от отрисовки
  • Использование последовательных соглашений именования переменных и функций
  • Создание модулей или пространств имен для организации различных аспектов вашей игры
  • Добавление документации, объясняющей назначение каждого основного раздела

Вопросы для размышления:

  • Какие части вашего кода труднее всего понять, когда вы возвращаетесь к ним?
  • Как вы могли бы организовать свой код, чтобы другим было проще внести свой вклад?
  • Что произойдет, если вы захотите добавить новые функции, такие как усиления или разные типы врагов?

Викторина после лекции

Викторина после лекции

Обзор и самостоятельное изучение

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

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

Что стоит изучить:

  • Как игровые движки организуют код вы будете поражены их умными подходами
  • Трюки для повышения производительности, чтобы игры на холсте работали плавно
  • Современные функции JavaScript, которые могут сделать ваш код чище и более поддерживаемым
  • Различные подходы к управлению игровыми объектами и их взаимосвязями

Задание

Комментируйте ваш код


Отказ от ответственности:
Этот документ был переведен с использованием сервиса автоматического перевода Co-op Translator. Хотя мы стремимся к точности, пожалуйста, учитывайте, что автоматические переводы могут содержать ошибки или неточности. Оригинальный документ на его родном языке следует считать авторитетным источником. Для получения критически важной информации рекомендуется профессиональный перевод человеком. Мы не несем ответственности за любые недоразумения или неправильные интерпретации, возникающие в результате использования данного перевода.