Главная | Все статьи | Дневник студента

Как устроены персонажи в моей игре

Время чтения статьи ~4 минуты
Статья написана студентом Хекслета. Мнение автора может не совпадать с позицией редакции
Как устроены персонажи в моей игре главное изображение

Как я начал писать игру на JS

Игра на JS. Часть 2

Сегодня расскажу о том, как устроены классы Hero и Enemy.

class Hero {
  constructor(name, imgPath, imgWidth, imgHeight, scaleMultiplier, baseAttack, health, deathSprites, takeHitSprites, armor = 1) {
    this.name = name;
    this.attack = baseAttack;
    this.health = health;
    this.imgPath = imgPath;
    this.imgWidth = imgWidth * scaleMultiplier;
    this.imgHeight = imgHeight * scaleMultiplier;
    this.numberOfSprites = imgWidth / imgHeight;
    this.animationTime = 800;
    this.animationTick = imgHeight * scaleMultiplier;
    this.animationInterval = 800 / (imgWidth / imgHeight);
    this.deathSprites = deathSprites;
    this.takeHitSprites = takeHitSprites;
    this.armor = armor;
  }
}

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

drawHero() {
    heroHTML.style.width = `${this.imgHeight}px`;
    heroHTML.style.height = `${this.imgHeight}px`;
    heroHpHTML.innerHTML = `${this.health}`;
    heroArmorHTML.innerHTML = `${this.armor}`;
  }

drawHero нужен для задания размеров контейнера персонажа и отображения здоровья и брони.

startTurn() {
    heroStatsHTML.style.pointerEvents = 'all';
  }

endTurn() {
    heroStatsHTML.style.pointerEvents = 'none';
  }

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

animateIdle() {
    heroHTML.style.backgroundImage = `url(${this.imgPath}/Idle.png)`;

    let position = 0;

    this.idle = setInterval(() => {
      heroHTML.style.backgroundPosition = `-${position}px`;

      if (position < this.imgWidth - this.animationTick) {
        position += this.animationTick;
      } else {
        position = 0;
      }
    }, this.animationInterval);
  }

stopAnimationIdle() {
    clearInterval(this.idle);
  }

персонаж

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

stopAnimationIdle прерывает анимацию покоя.

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

animateRun() {
    heroHTML.style.backgroundImage = `url(${this.imgPath}/Run.png)`;

    let position = 0;
    const translateTick = (100 / 2) / this.numberOfSprites;
    let translateX = 0;

    const run = setInterval(() => {
      heroHTML.style.backgroundPosition = `-${position}px`;
      heroHTML.style.transform = `translateX(${translateX}vw)`;
      if (position < this.imgWidth - this.animationTick) {
        position += this.animationTick;
        translateX += translateTick;
      } else {
        position = 0;
        clearInterval(run);
      }
    }, this.animationInterval);
  }

animateRun помимо анимации бега, сдвигает контейнер персонажа, поэтому тут добавляется переменная translateTick, в которой есть такая математическая операция (100 / 2). Пример того, как не надо писать код. Мне пришлось изрядно напрячься, чтобы вспомнить, что 50 это 50vw, т.е. половина экрана - место докуда добегает персонаж. "Половину экрана" мы делим на количество спрайтов анимации бега, которое нам пришлось указать в конструкторе, и в итоге персонаж "пробегает" все спрайты к концу своего забега.

animateRunBack() {
    heroHTML.style.backgroundImage = `url(${this.imgPath}/RunBack.png)`;

    let position = 0;
    const translateTick = (100 / 2) / this.numberOfSprites;
    let translateX = (100 / 2) - translateTick;

    const runBack = setInterval(() => {
      heroHTML.style.backgroundPosition = `-${position}px`;
      heroHTML.style.transform = `translateX(${translateX}vw)`;
      if (position < this.imgWidth - this.animationTick) {
        position += this.animationTick;
        translateX -= translateTick;
      } else {
        position = 0;
        clearInterval(runBack);
        this.animateIdle();
      }
    }, this.animationInterval);
  }

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

animateHitBar() {
    heroHitBarHTML.style.animation = 'hit 2.5s';
    setTimeout(() => {
      heroHitBarHTML.style.animation = 'none';
    }, 2500);
  }

// css
@keyframes hit {
  0% {
    transform: translateY(0);
    opacity: 1;
  }

  100% {
    transform: translateY(-50px);
    opacity: 0;
  }
}

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

Класс Enemy принципиально ничем не отличается от Hero, поэтому смысла рассматривать его отдельно нет.

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

Код проекта на GitHub.

А здесь можно поиграть в текущую версию.

Аватар пользователя Георгий Баратели
Георгий Баратели 03 февраля 2021
2
Рекомендуемые программы
профессия
от 6 300 ₽ в месяц
Разработка фронтенд-компонентов для веб-приложений
10 месяцев
с нуля
Старт 25 апреля
профессия
от 6 300 ₽ в месяц
Разработка веб-приложений на Django
10 месяцев
с нуля
Старт 25 апреля
профессия
от 6 183 ₽ в месяц
Ручное тестирование веб-приложений
4 месяца
с нуля
Старт 25 апреля
профессия
от 6 300 ₽ в месяц
Разработка приложений на языке Java
10 месяцев
с нуля
Старт 25 апреля
профессия
от 5 025 ₽ в месяц
новый
Сбор, анализ и интерпретация данных
9 месяцев
с нуля
Старт 25 апреля
профессия
от 6 300 ₽ в месяц
Разработка веб-приложений на Laravel
10 месяцев
с нуля
Старт 25 апреля
профессия
от 5 840 ₽ в месяц
Создание веб-приложений со скоростью света
5 месяцев
c опытом
Старт 25 апреля
профессия
от 9 900 ₽ в месяц
Разработка фронтенд- и бэкенд-компонентов для веб-приложений
16 месяцев
с нуля
Старт 25 апреля
профессия
от 6 300 ₽ в месяц
Разработка бэкенд-компонентов для веб-приложений
10 месяцев
с нуля
Старт 25 апреля
профессия
новый
Автоматизированное тестирование веб-приложений на JavaScript
8 месяцев
c опытом
в разработке
Старт 25 апреля
профессия
Верстка с использованием последних стандартов CSS
5 месяцев
с нуля
Старт в любое время