Привет. Меня зовут Александр Птичкин. Вот уже 8 лет я обучаю созданию мультфильмов и анимации; 3 года из них я посвятил разработке своего игрового 2.5D HTML5 движка под названием PointJS. За 8 лет вращения в этой индустрии я наработал много материала, которым хочу поделиться тут с вами в блоге. Это мой первый пост на Хабр. Судите строго :) Ваши предложения по улучшению дальнейших статьей пишите в комментариях. Поехали! Меня часто спрашивают, почему я в своем игровом движке PointJS перерисовываю весь CANVAS заново, а не обновляю только ту часть, на которой произошли изменения. В этой статье я не претендую на абсолютную истину - как говорится, сколько людей столько и мнений. Я лишь поделюсь своими мыслями, идеями и наработками, которые подвигли меня склониться к полной перерисовке CANVAS. Буду рад в комментариях услышать ваши мнения по этому поводу, чтобы на их основании оптимизировать движок. Ведь в общении рождается истина. Поначалу звучит очень логично. Где то на краю экрана пробежал вражеский персонаж, и только ту часть CANVAS мы и хотим перерисовать, сэкономив ресурсы. Это было настолько правдоподобно, что я за ночь изрисовал лист A4 в поисках оптимального решения этой задачи. И вот что получилось. Перерисовывать часть CANVAS мы можем только при выполнении двух условий:  Игровой фон должен быть разбит на части и не являться целым изображением. Судите сами: если фон - целое изображение, то его все равно придется где-то перерисовать. И раз оно цельное, то перерисовать придется полностью, а значит, ни о какой перерисовке отдельной части CANVAS речи быть не может. Дальше в статье вы поймете, что я имею в виду и почему так Камера в игре должна стоять. Потому что если камера движется, то и вся сцена движется, а значит, всю ее и надо перерисовать Уже два этих условия выше отсекают уйму игр и механик. А мы еще даже не начали разбираться. Итак, с фоном для игры мы определились. Как его подготовить, сейчас расскажу. Я беру готовую сцену из Maya (персонажей, фоны и анимацию я создаю в программе Autodesk Maya, а потом рендерю в формате 2D). Это облегчает дальнейшую работу со сценой и смену ракурса. Но об этом в другом посте. Затем отключаю все элементы сцены, кроме земли, и вывожу ее изображение. Рендерю с разрешением 3000 на 3000px. Но на изображении также есть пустые png-области, которые не нужны. Можно было, конечно, поправить в Maya, но мне лень. Я удаляю их с помощью скрипта на php. Таким образом на выходе у меня ровное изображение размером 2786 на 1998. Да, кстати, я написал уйму полезных скриптов для ускорения работы и для того, чтобы не использовать Photoshop, а делать все быстрее и проще. Получился такой собственный Photoshop, но на php :) Вообще идея разбить фоновое изображение и другие большие объекты на части уже давно используется в играх (и не только). Например, разработчики Diablo 2 делали то же самое. Ведь у них все было растровое (спрайты), и была создана лишь иллюзия 3D. А идея в следующем. Фоновое изображение может быть 10000px на 10000px, экраны же пользователей чаще всего 1920 на 1080px (мобильные и того меньше). Т.е. за пределами экрана, где мы ничего не видим, все равно будет происходить перерисовка фона. На рисунке СИНЕЕ - это рамка камеры в игре (рамка камеры равна размеру экрана монитора пользователя). Все, что за рамкой, мы видеть не должны. Нам надо от этого избавляться, ведь лишняя перерисовка - лишняя нагрузка. Даже программа анимации Anime Studio Pro (Moho) начинает висеть, если части тела персонажа состоят из картинок 1920 на 1080 (в программе мы их, конечно, уменьшим, но оригиналы-то никуда не денутся и вес файла тоже). Для человеческой руки достаточно иметь изображение 300 на 300px. Что уж говорить о браузерной игре... Давайте большое изображение разобьем в Photoshop на равные квадраты или прямоугольники (равные, чтобы потом при сборке не устраивать себе танцы с бубном). Например, разобьем на 8 строк и 8 столбиков. Так мы получили 8*8 = 64 прямоугольника размером 348 на 250px. Как разбить изображение в Photoshop, у меня рассказано в этом уроке. Теперь у нас есть много картинок gif, из которых можно воссоздать, как пазл, изображение фона. Теперь давайте соберем этот пазл на javascript в нашей игре. Для этого переименуем все файлы в папке, присвоив им числовые названия, начиная с 1 или 0 (как вам удобно.) Я бы делал с 1, так как потом надо будет делать цикл, начиная с 0, и сам 0 выпадет, потому что к переменной при первой итерации добавится 1. Руками переименовывать бессмысленно и долго, а у нас все-таки 21 век на дворе. На MAC Book я это делаю за один клик в программе Name Manager. Например, есть папка с фотографиями под именем img. Я просто перетаскиваю эту папку на экран программы, указываю номер первой фотографии и жму Rename. Если будете делать руками, не перепутайте порядок! Когда Photoshop рендерил нам разбитый фон, он делал это тоже в определенном порядке, последовательно проходя циклом строки, а потом столбцы. И если вы будете ставить нумерацию кое-как, то потом нужные куски пазла могут оказаться где угодно. Кстати это неплохой способ зашифровать игру (или усложнить ее чтение), создав свой порядок сборки фоток из папки. Языку Javascript без разницы, в каком порядке пробегать столбики и строки, так что лишней нагрузки вы не добавите. Вернемся к примеру: у нас есть последовательность от 0 до 64 – следовательно, 64 кусочка. Далее пишем на javascript простой код, который соберет все это в единое изображение. Для этого стоит создать массив под названием fon = [], в который впоследствии поместить все изображения в нужном порядке, и создать переменную xy, которая в будущем станет номером фотографии (xy будет принимать участие в формировании ссылки на путь к файлу). Затем создаем два цикла, один из которых будет идти по X и добавлять фотографии по горизонтали, а другой - по Y, переходить на новую строку и тем самым добавлять фотографии по вертикали. Цикл по x<8 говорит о том, что изображение было разбито на 8 строк. А цикл по y - что было 8 столбиков. Умножив 8 на 8, мы должны точно получить количество картинок в папке img. А их напомню 64. При каждой итерации этих циклов мы будем прибавлять единичку к xy и добавлять в массив fon выбранное определенным образом изображение. В игровом движке PointJS есть метод, который добавляет в игру изображение game.newImageObject({ });. Отмечу, что подробнее о том, как устроен движок, о его командах и уроки по нему вы можете посмотреть в соответствующем плей листе. Итак, теперь мы будем каждый раз добавлять в массив fon новый game.newImageObject. Давайте посмотрим на код целиком: Давайте посмотрим на строку file : "img/" + xy + ".gif". Благодаря конкатенации строк и тому, что xy у нас переменная, которая всегда увеличивается на 1, мы формируем в ней динамический путь к изображениям: Получили следующее. Это уже мы смотрим в окне браузера Теперь я предлагаю вам для наглядности чуть подпортить фон и сделать в формуле выше w=346, а h=248. Это нужно, чтобы между клетками появился зазор 2px, и мы смогли увидеть, из чего все же этот фон состоит. Это, разумеется, еще не все. Пока мы никак не оптимизировали игру - мы лишь расчленили и заново собрали одно изображение. И даже наоборот: еще больше нагрузили компьютер циклами. А теперь сделаем интересную операцию - будем рисовать только те квадраты, которые видим на экране (т.е. которые попадают в рамку камеры в движке). Для тех, кто не знает, за отрисовку всего, что есть в движке, отвечает команда draw();. Напишем условие: Смотрим, что получилось. Все блоки, что не попали в камеру (на рисунке они прозрачные), больше не рисуются. Вот это и есть первая оптимизация фонового изображения. Дальнейшие изображения я не смогу показать из окна браузера, так как рамка камеры - это и есть размер экрана, а я физически не могу показать то, что находится за экраном. Но я могу нарисовать это, например, в After Effects. Еще одна полезнейшая программа для жизни аниматора и монтажа. Однако, если вы помните, мы говорили о другом - как очищать CANVAS? Целиком или кусочком?. Сейчас мы провернули хорошую подготовительную работу, чтобы четко ответить на этот вопрос. Потому что предварительный ответ мы получили в начале статьи: если камера движется, то картинка сменяется полностью и тогда придется рисовать все без исключения. Приступим. Я предлагаю разместить на нашем верно отрисованном фоне несколько объектов: персонажей, дома и деревья. Теперь представим ситуацию: мы пьем чай и не играем, следовательно, наш персонаж не двигается. Однако вокруг него бегает другой персонаж (наверняка с дурными намерениями). Вот я нарисовал траекторию, по которой он пробежал (сделал за ним шлейф): Посмотрим, какие блоки он задел и что надо перерисовать на сцене. Эти участки я выделил красным. По рисунку видно, какую область надо перерисовать (она красная). Все остальное можно не трогать, так как оно не изменилось. Как бы теперь я построил алгоритм, который смог бы определить затронутые блоки и участки, нуждающиеся в перерисовке? Обсудим на уровне идеи. Допустим, мы создадим квадрат/прямоугольник по размеру бегущего персонажа. Хотя нет, даже не так. Зачем создавать, когда можно просто вызывать StaticBox() этого объекта? Да, в движке есть команда getStaticBox(); - область загруженного изображения, равная размерам этого изображения. Теперь напишем простое условие, в котором будем проверять, каких объектов сцены коснулся этот StaticBox();. Вы помните, что у нас уже был массив fon = [];? А помните, мы загружали деревья и дома? Их нельзя разместить в массиве fon. Значит, для них создадим отдельный массив obj, и будем проверять сразу оба массива. Итак, пишем код: Уже на этой стадии обработка циклов может отнять у компьютера много ресурсов. Однако мы двигаемся дальше. Я специально на картинке выше провел персонажа так, чтобы он не коснулся дома, но коснулся клетки фона. Зачем? Все просто: если перерисовать в этом месте фон, а потом персонажа, не тронув дома, то фон окажется выше этого дома И вот главный вывод статьи:Значит нам нужно проверять еще и родительские связи!   Перерисовав фон и персонажа, нам следует узнать, а не перекроет ли этот фон еще что-нибудь. Т.е. необходимо пробежаться циклом по всем элементам, что попали в массивы collisionFon и collisionObj, и посмотреть, каких еще объектов они касаются на сцене, а точнее - какие объекты находятся над ними. В моем движке есть функция проверки оси Z. По сути там реализована эмуляция оси Z через сравнение осей Y, но сейчас это не столь важно. Самое главное, что это большой блок, который тоже будет поглощать ресурсы. Ведь теперь все блоки, которые предполагается перерисовать, нужно собрать в общий массив, чтобы сравнить их оси Y, чтобы выяснить, кто из них выше, а кто ниже. Короче, я даже формулы писать не буду. Думаю, вы поняли: расчеты будут громоздкими и нагрузят компьютер больше, чем если бы мы просто стерли весь CANVAS и нарисовали его заново. Я даже сомневаюсь, что смогу их провести, а в том, что это безотказно отработает в MMORPG-играх, вообще не уверен. С учетом всех наложений и косвенных (не на прямую) попаданий на здания, к красной зоне я добавил и зеленю область которую тоже нужно перерисовать.  При этом вы заметили, что в примере пробежал только один вражеский персонаж. И мы рассмотрели лишь частный случай, когда игрок стоит на месте, а камера не двигается, чтобы выполнить условие выше. А сколько движухи получилось! Даже если мы все же сделаем расчеты, придется перерисовывать почти половину CANVAS. Так зачем тратить столько сил, менять механику движка, обработав при этом небольшой частный случай и в итоге перерисовав полCANVAS? Я еще понимаю, если бы перерисовке подверглись два-три блока, но, кажется даже при самом простом варианте малой кровью не отделаться. Впрочем, решение, конечно, за вами! Что ж, на этом я завершаю свою первую статью. Надеюсь, вы не очень утомились. Если есть какие-то мнения по поводу вышеизложенного - пишите в комментариях, обсудим. Повторюсь, в общении рождается истина. А она нужна, чтобы улучшать движок. Всем спасибо и до встречи в новых статьях. Автор текста и всех материалов статьи: Александр Птичкин, основатель образовательного проекта Mult-uroki. Написано специально для habr.com. Копирование или иное использование материала без письменного разрешения автора запрещено. Основатель проекта mult-uroki. Автор уроков