Герои в браузере

Долго, сложно и невыносимо интересно.

Герои в браузере

Долго, сложно и невыносимо интересно.

Меня зовут

Коротаев Александр

  1. Работаю в Tinkoff.ru
    Делаю внутренние сервисы и кабинет для бизнеса
  2. Делаю SPB-Frontend Drinkcast
    Подкаст o фронтенде за кружкой пива
  3. Хожу на митапы 🤓

Демо: http://homm.lekzd.ru/

Каждые 2 месяца, на гитхабе появляется 1 клон героев

  1. sfia-andreidaniel/heroes3
  2. mwardrop/HOMM3Clone
  3. potmdehex/homm3tools
  4. openhomm/openhomm
  5. ⚠️vcmi/vcmi

Но никто из них не доделывает до конца и не стремится объединяться

Какие цели ставил

  1. Сделать нечто, прыгнуть выше своей головы

4. сделать красиво

Из чего состоит игра?

  1. Модель данных (персонажи, карта, хранение состояния)
  2. Игровой цикл (game loop)
  3. Обработка ввода (мышь, клавиатура, джойстик...)
  4. Отрисовка (renderer)

А если представить это ввиде кода, то

			const me = {name: 'Alex', left: 0}
			...
			setInterval(() => update(), 1000)
			...
			window.addEventListener('keyup', () => me.left++)
			...
			requestAnimationFrame(() => draw())
		

Модель

			const me = {name: 'Alex', left: 0}
			...
			setInterval(() => update(), 1000)
			...
			window.addEventListener('keyup', () => me.left++)
			...
			requestAnimationFrame(() => draw())
		

Game loop

			const me = {name: 'Alex', left: 0}
			...
			setInterval(() => update(), 1000)
			...
			window.addEventListener('keyup', () => me.left++)
			...
			requestAnimationFrame(() => draw())
		

Обрабтка ввода

			const me = {name: 'Alex', left: 0}
			...
			setInterval(() => update(), 1000)
			...
			window.addEventListener('keyup', () => me.left++)
			...
			requestAnimationFrame(() => draw())
		

Отрисовка

			const me = {name: 'Alex', left: 0}
			...
			setInterval(() => update(), 1000)
			...
			window.addEventListener('keyup', () => me.left++)
			...
			requestAnimationFrame(() => draw())
		

Подробне про gamedev на JS в книге
Сюрреализм на JavaScript

Краткая история разработки

Что мы имеем из входных данных?

  1. Оригинальная игра
  2. Редактор карт
  3. FizMig — большой справочник по всем игровым механикам
  4. Много форумов с ребятами, по многу лет копавшимися в "героях"
  5. Распаковщик ресурсов 😎

В начале, был рендеринг зеленого поля

Далее — данные

Карты, персонажи, навыки, умения...

Затем — алгоритмы...

#1 Парсинг карт

В начале был PHP

  1. Разные версии форматов карт
  2. Опциональные блоки, размер которых нельзя посчитать
  3. 157 unknown bytes
  4. Побитовые маски и прочие ухищрения с байтами

Но тут я наткнулся на homm3tools...

Это набор библиотек и утилит, для работы с картами

  1. Парсер разных форматов
  2. Генератор карт
  3. Рендер надписей из деревьев
  4. И, даже, игра "змейка"

...и сделал свой конвертор

h3m-map-convertor

Данных становилось все больше, кол-во объектов росло...

#2 JS тормозит!?

Вся карта — это сетка

* а точнее — тайлы

Каждый тайл это

  1. Тип (вода, земля, дерево)
  2. Проходимость/стоимость перемещения
  3. Наличие события
  4. Флаг "Кем занят"
  5. ...

Представим это в коде

			const map = [
			    [{...}, {...}, {...}, {...}, {...}, {...}],
			    [{...}, {...}, {...}, {...}, {...}, {...}],
			    [{...}, {...}, {...}, {...}, {...}, {...}],
			    ...
			]
			const tile = map[1][3]
		

ООП — это плохо

А простые массивы — хорошо

[🌲, 🌲, 🌳, 🌲, 🌳, 🌲, 🔥, 🌲]
[0, 1, 0, 1, 1, 1, 0, 1]

Этот паттерн называется "Flyweight"

Подробнее об этом в книге Game Programming Patterns

  1. Как быстро считать столкновения
  2. Что такое сервисы и обсерверы
  3. Как организовать систему событий
  4. И много отличных иллюстраций

Нет ничего быстрее циклов...

			for (let i = 0; i < 9000; i++) {
			    result.push(items[i])
			}
		

Почему бы не добавить их еще больше

			for (let y = 0; y < 9000; y++) {
			    for (let x = 0; x < 9000; x++) {
			        result.push(items[x][y])
			    }
			}
		

Или лучше сразу сделать доступ к данным проще?

			mapGrid.forEach((x, y) => {
			    result.push(itemsGrid.get(x, y))
			}
		

А за удобным апи будет эффективное хранение данных

например в плоском массиве

			[123, 343, 35, 0, 391 ... ]
		

или вообще в ArrayBuffer

			[01001101010100110001010101010]
		

Минутка геометрии

Минутка героеометрии 🏇

Чтобы достичь одинаковой скорости...


4 шага

~6 шагов

setTimeout(() => setTimeout())

vs

setInterval()

vs

requestAnimationFrame()

Задачи < микрозадачи



Promise()



Подробнее об этом в статье Джека Арчибальда

Возьмем все и сразу

			new Promise(resolve => {
			    setTimeout(() => {
			        // расчеты для анимации
			        requestAnimationFrame(() => /* рисование */)
			        resolve()
			    })
			})
		

Построим из этого последовательность

			startAnimation()
			    .then(step)
			    .then(step)
			    .then(step)
			    .then(step)
			    .then(doAction)
			    .then(endAnimation)
		

А лучше сразу декларативную абстракцию

			AsyncSequence([
			    startAnimation, [
			        step
			        step
			        ...
			        doAction],
			    endAnimation
			])
		

Абстракции делают код лучше

Они позволяют мыслить на новом уровне, решать все более сложные задачи, в коде читается бизнес-логика

			const mapGrid = MapModel.grid
			const activeHero = player.getSelectedHero()
			const mainTown = player.getMainTown()
			const path = AStar.getPath(mapGrid, activeHero, mainTown)
		

Первая версия должна быть какой угодно

Лишь бы работало

			// TODO: переписать на графы, когда будет время
			while (hasChunks) { ... }
			// TODO: создать отдельные классы для этого
			function Knight(id, name) { ... }
			// TODO: тут почему-то тормозит
			var f = getFactorial(23423423)
		

#3 Взять и переписать!

Я несколько раз устраивал глобальные рефакторинги

  1. Функции -> ES6 классы
  2. RequireJS -> ES6 импорты
  3. CoffeeScript -> ES6
  4. ES6 -> TypeScript
  5. Command Pattern -> Стейты

Напрмер, в начале я создавал UI-элементы так

			const okButton = new Buttton(0, 10, 'Ok')
			okButton.addEventListener('click', () => { ... })
			const cancellButton = new Buttton(0, 10, 'Cancel')
			cancellButton.addEventListener('click', () => { ... })
		

Кол-во параметров росло и росло...

			const okButton = new Buttton({
			    left: 0,
			    top: 10,
			    onClick: () => { ... }
			})
			const cancellButton = new Buttton({...})
		

Их становилоь все больше, появился JSON...

			[
			    {
			         id: 'okButton',
			         options: {
			             left: 0,
			             top: 10,
			             onClick: () => { ... }
			         },
			    },
			...
			]
		

Но далее я вспомнил, что есть XML

			<button id="okButton"
			        left="0"
			        top="10"
			        onClick="{doSomething()}"
			/>
		

При сборке он собирается в JSON с предыдущего слайда

			<group id="main" ... >
			    <group id="header" ... >
			        <text-block ... />
			        <button ... />
			    </group>
			    <group id="footer" ... >
			        <any-component ... />
			        <button ... />
			    </group>
			</group>
		

При сборке он собирается в JSON с предыдущего слайда

Прямо как в react-canvas

Как соxранить мотивацию?

Баги:

  1. ...
  2. ...
  3. ...
  4. ...

Фичи:

  1. ...
  2. ...
  3. ...
  4. ...

Рефаторинг:

  1. ...
  2. ...
  3. ...
  4. ...

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

  1. саморазвитие
  2. выходы за рамки привычных рабочих задач
  3. расширение кругозора
  4. знакомство с фанатиками

Зачем делать игры

  1. куда интереснее чем сайтики
  2. это алгоритмы
  3. это красиво


Степень мастерства прямо пропорциональна времени, которое можно потратить на работу вглубь

Вопросы