/
Блог
/
Дневник студента
/

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

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

3 февраля 2021 г.
2 минуты
2

Как я начал писать игру на 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.

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

Георгий Баратели

5 лет назад

2