Изометрия - о реализации
от RblSb
Этим летом решил уделить немного времени на изометрическую проекцию и попробовать сделать свой велосипед.
Первое что пришло в голову - генерировать тайлы из выбранной текстурки, накладывая ее на стороны тайла программно, на подобии штуки Magatino для изо-майнкрафта. Деформация делается достаточно просто.
Теперь можно отрисовывать карту. После нескольких неудачных попыток, все получается даже без включения мозга (⚓).
Но в таком варианте карта будет отрисовываться наполовину за экраном, а не с координат 0,0. Чтобы это исправить, нужно при отрисовке учитывать этот самый выезд за экран. Вынесем его в две переменные:
И поправим координаты для Map.prototype.draw:
Чтобы в дальнейшем не мучатся с данной формулой отрисовки, предлагаю вынести это в отдельные методы getTileX и getTileY, куда аргументами передавать обычные значения x,y,z, получая на выходе лишь x или y. А заодно, добавим туда учитывание игровой камеры, которую обозначим переменными cx и cy. И как бонус, сделаем еще пару методов для конвертации обычных координат в изометрические (это будет полезно например, чтобы сделать выбор тайлов карты курсором). Всю эту гадость я, пожалуй, запихну в спойлер.
К сожалению, все оказалось не так просто. Хромог совсем не захотел в отрисовку изображений с отключенным сглаживанием, что весьма заметно, если нижние стороны тайлов очень контрастные или вовсе отсутствуют.
Возможные решения проблемы:
1) Растягивать текстуры на несколько пикселей в стороны.
В таком случае одиночные тайлы, например слоями выше, будут залезать за свою территорию, в большинстве вариаций это будет не критично.
2) Использовать готовые пиксельные маски, накладывая на них текстуры
Есть несколько вариаций стыковок тайлов (
), которыми пользуются большинство художников, полностью избавляясь от сглаживания по краям.
В конечном варианте я решил использовать маски без текстур, лишь выбирая необходимую форму (блок/полублок/склон) и изменяя ее цвет для создания нужного тайла.

Пришло время делать игрока и проверку тайлов на глубину. Алгоритм очень прост - проверять координаты всех тайлов с игроком перед отрисовкой, чтобы точно знать, какие тайлы рисовать за игроком, а какие - перед. Для сортировки я использовал нативный метод Array.prototype.sort() с данной функцией.
Но все-таки, проверять, например, карту 10х10х10 с игроком - это сортировать 1000 тайлов каждый кадр, что явно отрицательно скажется на производительности. Мы может откинуть некоторые z-слои карты, на которых игрока точно не будет, или снизить кол-во тайлов до минимума, стерев те, что не видны игроку, но в данном случае есть способ проще.
Достаточно проверять лишь те тайлы, за которые возможно зайти, а всю карту рисовать одной фоновой пикчей.
Окей, с помощью проверки на соседние тайлы при единственной отрисовке карты в фон, мы определились с тайлами, за которые игрок может зайти и начали рисовать их поверх остальной карты. Как же сделать корректную отрисовку в данном случае? Довольно просто
Изначально я обрезал тайлы, имеющие глубину таким образом (красными линиями обведена часть тайла, которая останется после обрезания, оставшаяся часть тайла становилась прозрачной), но гораздо надежнее - просто маскировать каждый тайлик с глубиной под фон.
Для этого воспользуемся свойством канваса, под названием globalCompositeOperation.
По умолчанию, если мы рисуем сначала синий квадрат, а потом красный круг, то они отобразятся таким образом:
Данное свойство отвечает за перекрытие цветов при отрисовке. Установим ему значение 'source-atop' и получим необходимый результат
, осталось лишь заменить квадрат на наш тайл с глубиной, а круг - на игровую карту. Можно сказать, мы вырезаем из карты нужный нам фрагмент нужной формы и рисуем только его в наш тайлик.
Как это выглядит в игре
Вариант с подсвеченными тайлами
Тайлы с глубиной без фона
Что ж, техническая часть подходит к концу. Я хотел бы рассказать про реализацию коллизии, но лично я смог сделать столкновение лишь с полными блоками, скопировав говнокод из портал мобиле. Делается это так же как и в 2D платформерах, с двумя лишними проверками на пол и потолок.
Из-за практически полного отсутствия геймплея и графики, я с гордостью называю эту демку в честь No Man's Sky, удачного брождения по картам.
http://mssite.org/projects/dev/NoPonySky/
WASD / стрелки / пробел - передвижение
Кнопка 9 - отображение только тайлов с глубиной
FAQ по редактору карт
Как итог, стоит заметить, что реализовывать сложную изометрию в 2D сейчас довольно глупо. В первую очередь это будет касаться освещения и теней в игре (ладно, я просто не знаю, как можно сделать тень у игрока в текущем демо), да и более динамические и красивые вещи намного проще будет сделать в 3D. Зато теперь у меня есть редактор!
Ставь класс, если тратил по полмесяца на бесполезную хрень
Первое что пришло в голову - генерировать тайлы из выбранной текстурки, накладывая ее на стороны тайла программно, на подобии штуки Magatino для изо-майнкрафта. Деформация делается достаточно просто.
- function createTile(img) {
- //создаем канвас размером с тайлик
- var isometric = document.createElement("canvas");
- isometric.width = img.width * 2;
- isometric.height = img.height * 2;
- var g = isometric.getContext("2d"); //косплей j2me
- g.setTransform(1, -0.5, 1, 0.5, 0, img.height/2);
- g.drawImage(img, -1, 0, img.width+1, img.height+1); //верхняя часть блока
- //раскомментировать в 2020-ом
- //g.filter = "brightness(75%)";
- var temp = brightnessImage(img, 75); //задаем яркость
- g.setTransform(1, 0.5, 0, 1, 0, 0);
- g.drawImage(temp, 0, temp.height/2); //левая часть блока
- //g.filter = "brightness(50%)";
- temp = brightnessImage(img, 50);
- g.setTransform(1, -0.5, 0, 1, 0, 0);
- g.drawImage(temp, temp.width, temp.height*1.5); //правая часть блока
- return isometric;
- }
- /* Так как свойство filter довольно плохо поддерживается канвасом, я использовал альтернативную реализацию */
- function brightnessImage(img, adjustment) {
- var canvas = document.createElement("canvas");
- canvas.width = img.width;
- canvas.height = img.height;
- var g = canvas.getContext("2d");
- g.drawImage(img, 0, 0);
- var imageData = g.getImageData(0, 0, canvas.width, canvas.height);
- var d = imageData.data;
- for (var i=0; i<d.length; i+=4) { //правка rgb каналов, скип альфы
- d[i] = d[i]/100*adjustment;
- d[i+1] = d[i+1]/100*adjustment;
- d[i+2] = d[i+2]/100*adjustment;
- }
- g.putImageData(imageData, 0, 0);
- return canvas;
- }
Теперь можно отрисовывать карту. После нескольких неудачных попыток, все получается даже без включения мозга (⚓).
- Map.prototype.init = function() {
- //настоящий размер тайлов 64x64
- this.tsizex = 64;
- this.tsizey = this.tsizex / 2;
- this.tsizez = this.tsizex;
- }
- Map.prototype.draw = function() {
- for (var iz = 0; iz < this.d; iz++)
- for (var iy = 0; iy < this.h; iy++)
- for (var ix = 0; ix < this.w; ix++) {
- var id = this.layers[iz][iy][ix]; //трехмерный массив карты
- var x = Math.floor(ix * this.tsizex - iy * this.tsizex);
- var y = Math.floor(iy * this.tsizey + ix * this.tsizey - iz*this.tsizez);
- if (id > 0) g.drawImage(this.tiles[id], x, y); //this.tiles - массив картинок
- }
- }
Но в таком варианте карта будет отрисовываться наполовину за экраном, а не с координат 0,0. Чтобы это исправить, нужно при отрисовке учитывать этот самый выезд за экран. Вынесем его в две переменные:
- this.offsetx = this.tsizex*(this.h-1);
- this.offsety = this.tsizez*(this.d-1);
- var x = Math.floor(ix * this.tsizex - iy * this.tsizex + this.offsetx);
- var y = Math.floor(iy * this.tsizey + ix * this.tsizey - iz*this.tsizez + this.offsety);
Чтобы в дальнейшем не мучатся с данной формулой отрисовки, предлагаю вынести это в отдельные методы getTileX и getTileY, куда аргументами передавать обычные значения x,y,z, получая на выходе лишь x или y. А заодно, добавим туда учитывание игровой камеры, которую обозначим переменными cx и cy. И как бонус, сделаем еще пару методов для конвертации обычных координат в изометрические (это будет полезно например, чтобы сделать выбор тайлов карты курсором). Всю эту гадость я, пожалуй, запихну в спойлер.
Открыть спойлер
К сожалению, все оказалось не так просто. Хромог совсем не захотел в отрисовку изображений с отключенным сглаживанием, что весьма заметно, если нижние стороны тайлов очень контрастные или вовсе отсутствуют.
Возможные решения проблемы:
1) Растягивать текстуры на несколько пикселей в стороны.
В таком случае одиночные тайлы, например слоями выше, будут залезать за свою территорию, в большинстве вариаций это будет не критично.
2) Использовать готовые пиксельные маски, накладывая на них текстуры
Есть несколько вариаций стыковок тайлов (

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

Пришло время делать игрока и проверку тайлов на глубину. Алгоритм очень прост - проверять координаты всех тайлов с игроком перед отрисовкой, чтобы точно знать, какие тайлы рисовать за игроком, а какие - перед. Для сортировки я использовал нативный метод Array.prototype.sort() с данной функцией.
- function sortObjects(a, b) {
- var alen = a.x + a.y + a.z;
- var blen = b.x + b.y + b.z;
- if (alen > blen) return 1;
- else if (alen < blen) return -1;
- else if (a.type) return 1; //это свойство имеет лишь объект игрока, добавляя ему немного приоритета
- }
Достаточно проверять лишь те тайлы, за которые возможно зайти, а всю карту рисовать одной фоновой пикчей.
Окей, с помощью проверки на соседние тайлы при единственной отрисовке карты в фон, мы определились с тайлами, за которые игрок может зайти и начали рисовать их поверх остальной карты. Как же сделать корректную отрисовку в данном случае? Довольно просто

Изначально я обрезал тайлы, имеющие глубину таким образом (красными линиями обведена часть тайла, которая останется после обрезания, оставшаяся часть тайла становилась прозрачной), но гораздо надежнее - просто маскировать каждый тайлик с глубиной под фон.
Для этого воспользуемся свойством канваса, под названием globalCompositeOperation.
По умолчанию, если мы рисуем сначала синий квадрат, а потом красный круг, то они отобразятся таким образом:

Данное свойство отвечает за перекрытие цветов при отрисовке. Установим ему значение 'source-atop' и получим необходимый результат

Как это выглядит в игре
Вариант с подсвеченными тайлами
Тайлы с глубиной без фона
Что ж, техническая часть подходит к концу. Я хотел бы рассказать про реализацию коллизии, но лично я смог сделать столкновение лишь с полными блоками, скопировав говнокод из портал мобиле. Делается это так же как и в 2D платформерах, с двумя лишними проверками на пол и потолок.
Из-за практически полного отсутствия геймплея и графики, я с гордостью называю эту демку в честь No Man's Sky, удачного брождения по картам.
http://mssite.org/projects/dev/NoPonySky/
WASD / стрелки / пробел - передвижение
Кнопка 9 - отображение только тайлов с глубиной
FAQ по редактору карт
Как итог, стоит заметить, что реализовывать сложную изометрию в 2D сейчас довольно глупо. В первую очередь это будет касаться освещения и теней в игре (ладно, я просто не знаю, как можно сделать тень у игрока в текущем демо), да и более динамические и красивые вещи намного проще будет сделать в 3D. Зато теперь у меня есть редактор!
Ставь класс, если тратил по полмесяца на бесполезную хрень
