Русский
Русский
English
Статистика
Реклама

Grid

CSS Grid понятно для всех

05.08.2020 14:08:45 | Автор: admin

Что такое Grid?


Grid представляет собой пересекающийся набор горизонтальных и вертикальных линий один набор определяет столбцы, а другой строки. Элементы могут быть помещены в сетку, соответственно строкам и столбцам.

Поддержка браузерами


В 2020 году поддержка браузерами достигает 94 %



Grid контейнер


Мы создаем grid контейнер, объявляя display: grid или display: inline-grid на элементе. Как только мы это сделаем, все прямые дети этого элемента станут элементами сетки.

<body> <div class="row">  <div class="row__item header">   <h1>Header</h1>  </div>  <div class="row__item nav">   <h1>Navbar</h1>  </div>  <div class="row__item article">   <h1>Article</h1>  </div>  <div class="row__item ads">   <h1>Ads</h1>  </div> </div></body>

.row { display: grid; margin: auto; grid-template-rows: 60px 1fr ; grid-template-columns: 20% 1fr 15%; grid-gap: 10px; width: 1000px; height: 1000px; justify-items: center; justify-content: space-between; grid-template-areas: "header header header" "nav article ads"; }

grid-template-rows это CSS свойство, которое определяет названия линий и путь размера функции grid rows.

CSS свойство grid-row определяет с какой строки в макете сетки будет начинаться элемент, сколько строк будет занимать элемент, или на какой строке завершится элемент в макете сетки. Является сокращенным свойством для свойств grid-row-start и grid-row-end.

Свойство CSS grid-gap является сокращенным свойством для grid-row-gap и grid-column-gap, определяющего желоба между строками и столбцами сетки.

Свойство grid-template-areas определяет шаблон сетки ссылаясь на имена областей, которые заданы с помощью свойства grid-area.

Повторение названия области приводит к тому, что содержимое охватывает эти ячейки. Точка означает пустую ячейку. Сам синтаксис предоставляет визуализацию структуры сетки.

С помощью свойства grid-area мы можем назначить каждой из этих областей свое собственное имя. Именование областей еще не создает никакого макета, однако теперь у нас есть именованные области, которые мы можем в нем использовать.

.header{grid-area: header;}.nav{grid-area: nav;}.article{grid-area: article;}.ads{grid-area: ads;}

Создаем шаблон сайта с CSS Grid:




Изменяем шаблон


Вы можете изменить шаблон просто перераспределив грид-области в grid-template-areas.

Таким образом, если мы сменим на это:

grid-template-areas: "nav header header" "nav article ads"; }

То в результате получим такой шаблон:



Гриды с медиа запросами


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

Это делает CSS Grid идеальным для медиа запросов. Мы можем просто переназначить значения в ASCII-графике и обернуть результат в конечный медиа запрос.

@media all and (max-width: 575px) {.row {grid-template-areas:"header""article""ads""nav";grid-template-rows: 80px 1fr 70px 1fr ;grid-template-columns: 1fr;}}

В результате получим:



Таким образом, все дело состоит в переназначении значений в свойстве grid-template-areas.

Заключение


В данной статье мы рассмотрели всего лишь верхушку CSS Grid Layout айсберга. Иногда сложно поверить своим глазам какие штуки удается сделать при помощи CSS Grid. Это разрыв всех шаблонов. И мне это нравится.

Я вам советую обратить внимание на данную спецификацию и потратить немного своего времени на ее изучение. Поверьте, в будущем вам это точно пригодится и не важно, пишете вы на React, Angular, Vue (вставьте свое). Gridы пришли надолго.
Подробнее..

Полное визуальное руководство-шпаргалка по Flexbox и Grid туториал

18.11.2020 14:04:51 | Автор: admin


Доброго времени суток, друзья!

Представляю вашему вниманию полное визуальное руководство-шпаргалку по всем свойствам CSS-модулей Flexbox и Grid.

Основные источники: A Complete Guide to Flexbox, A Complete Guide to Grid.

В конце статьи вас ждет небольшой туториал по созданию тренажера по Flexbox.

Без дальнейших предисловий.



Flexbox (Flex, далее по тексту Флекс)


Терминология


Флекс это не отдельное свойство, но целый модуль, включающий набор свойств. Некоторые из этих свойств добавляются к контейнеру (родительскому элементу, известному как флекс-контейнер (flex container, далее по тексту контейнер)), другие к дочерним элементам (известным как флекс-элементы (flex items, далее по тексту элементы)).

Обычный макет основан на потоке из блочных и строчных элементов, а флекс-макет на флекс-потоке (flex-flow directions). Посмотрите на это изображение из спецификации, демонстрирующее ключевые идеи, лежащие в основе флекс-макета.



Элементы располагаются вдоль основной (главной) оси (main axis) (от main-start до main-end) или вдоль поперечной оси (cross axis) (от cross-start до cross-end).

  • основная ось главная ось контейнера, ось, вдоль которой располагаются элементы. Она не обязательно является горизонтальной; это зависит от свойства flex-direction (направление, см. ниже)
  • main-start | main-end элементы располагаются в контейнере от main-start до main-end
  • основной размер (main size) ширина или высота элемента в зависимости от основного измерения (main dimension), основной размер элемента
  • поперечная ось ось, перпендикулярная основной. Ее направление зависит от направления основной оси
  • cross-start | cross-end строки контейнера заполняются элементами, которые располагаются от cross-end до cross-start
  • поперечный размер (cross size) ширина или высота элемента в зависимости от основного измерения



Свойства флекс-контейнера


display

Данное свойство определяет флекс-контейнер; блочный или строчный в зависимости от присвоенного значения. Оно включает флекс-контекст для всех прямых потомков контейнера.

.container {  display: flex; /* или inline-flex */}

Обратите внимание, что свойства CSS-колонок в контейнере не работают.

flex-direction (направление)



Данное свойство определяет основную ось, направление, по которому размещаются элементы в контейнере. Флекс сам по себе (без расширяющих его функционал оберток) реализует концепцию однонаправленного макета. Поэтому элементы размещаются либо в горизональных строках, либо в вертикальных колонках.

.container {  flex-direction: row | row-reverse | column | column-reverse;}

  • row (по умолчанию): элементы располагаются слева направо в ltr или справа налево в rtl
  • row-reverse: обратный row порядок расположения элементов справа налево в ltr или слева направо в rtl
  • column: аналогично row, но сверху вниз
  • column-reverse: аналогично row-reverse, но снизу вверх

flex-wrap (перенос, переход, разделение)



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

.container {  flex-wrap: nowrap | wrap | wrap-reverse;}

  • nowrap (по умолчанию): все элементы располагаются на одной строке
  • wrap: элементы могут располагаться на нескольких строках сверху вниз
  • wrap-reverse: элементы могут располагаться на нескольких строках снизу вверх

flex-flow (поток)

Данное свойство является сокращением для flex-direction и flex-wrap, которые определяют основную и поперечную оси контейнера. Значением по умолчанию является row nowrap.

.container {  flex-flow: column wrap;}

justify-content (выравнивание контента в одной строке)



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

.container {  justify-content: flex-start | flex-end | center | space-between | space-around | space-evenly | start | end | left | right ... + safe | unsafe;}

  • flex-start (по умолчанию): элементы сдвигаются в начало контейнера вдоль основной оси
  • flex-end: элементы сдвигаются в конец контейнера
  • start: элементы сдвигаются в начало контейнера, определяемое значением свойства writing-mode (направление письма)
  • end: элементы сдвигаются в конец контейнера, определяемый значением свойства writing-mode
  • left: элементы прижимаются к левому краю контейнера; без flex-direction поведение аналогично start
  • right: элементы прижимаются к правому краю контейнера; без flex-direction поведение аналогично start
  • center: элементы выравниваются по центру
  • space-between: элементы выравниваются таким образом, что первый элемент находится в начале строки, последний в конце, а остальные элементы равномерно распределяются по оставшемуся пространству
  • space-around: элементы равномерно распределяются с одинаковым пространством по краям. Обратите внимание, что визуально пространство между элементами и краями контейнера не является одинаковым; это объясняется тем, что элементы занимают определенное пространство по обеим сторонам. Первый элемент занимает одну часть пространства от края контейнера, но две части до второго элемента, поскольку второй элемент также занимает одну часть пространства со стороны первого элемента
  • space-evenly: элементы размещаются таким образом, чтобы пространство между любыми двумя элементами являлось одинаковым

Обратите внимание, что поддержка перечисленных свойств разными браузерами различается. Самыми безопасными являются flex-start, flex-end и center.

Что касается дополнительных ключевых слов safe и unsafe, то использование safe позволяет избежать отрисовки элементов за пределами страницы, независимо от позиционирования, что, в свою очередь, исключает возможность появления прокрутки.

align-items (выравнивание элементов)



Данное свойство определяет, как элементы располагаются вдоль поперечной оси. Его можно сравнить с justify-content применительно к поперечной оси (перпендикулярной основной).

.container {  align-items: stretch | flex-start | flex-end | center | baseline | first baseline | last baseline | start | end | self-start | self-end + ... safe | unsafe;}

  • stretch (по умолчанию): элементы растягиваются, чтобы заполнить весь контейнер (зависит от их min-width/max-width)
  • flex-start / start / self-start: элементы смещаются к началу поперечной оси. Различия между указанными свойствами несущественны и зависят от flex-direction или writing-mode
  • flex-end / end / self-end: элементы смещаются в конец поперечной оси. Различия между указанными свойствами несущественны и зависят от flex-direction или writing-mode
  • center: элементы выравниваются по центру
  • baseline: элементы выравниваются вдоль их основной линии

Ключевые слова-модификаторы safe и unsafe могут использоваться в качестве дополнительных настроек, позволяющих избежать такого выравнивания элементов, которое сделает контент недоступным (недосягаемым, находящимся за пределами страницы).

align-content (выравнивание содержимого в нескольких строках)



Данное свойство определяет выравнивание строк контейнера при наличии свободного пространство вдоль поперечной оси. Оно похоже на justify-content, которое распределяет пространство между отдельными элементами вдоль основной оси.

Обратите внимание, что рассматриваемое свойство применяется только в отношении содержимого контейнера, располагающегося на нескольких строках, когда свойство flex-wrap установлено в значение wrap или wrap-reverse. В отношении однострочного контейнера (когда свойство flex-wrap имеет стандартное значение no-wrap) применение align-content не будет иметь никакого эффекта.

.container {  align-content: flex-start | flex-end | center | space-between | space-around | space-evenly | stretch | start | end | baseline | first baseline | last baseline + ... safe | unsafe;}

  • normal (по умолчанию): строки находится на обычных позициях
  • flex-start / start: строки сдвигаются в начало контейнера. flex-start зависит от flex-direction, а start от writing-mode
  • flex-end / end: строки сдвигаются в конец контейнера. flex-end зависит от flex-direction, а end от writing-mode
  • center: строки выравниваются по центру
  • space-between: строки располагаются таким образом, что первая строка находится в начале контейнера, последняя в конце, а остальные строки распределяются равномерно
  • space-around: строки располагаются с одинаковым пространством между ними
  • space-evenly: строки располагаются с одинаковым пространством вокруг каждой из них
  • stretch: строки растягиваются, занимая все доступное пространство

Ключевые слова-модификаторы safe и unsafe могут использоваться в качестве дополнительных настроек, позволяющих избежать такого выравнивания элементов, которое сделает контент недоступным (недосягаемым, находящимся за пределами страницы).



Свойства флекс-элементов


order (порядок)



По умолчанию, элементы располагаются в контейнере в том порядке, в котором указаны в разметке. Данное свойство позволяет этим управлять.

.item {  order: 5; /* по умолчанию равняется 0 */}

flex-grow (рост, расширение)



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

Если все элементы имеют свойство flex-grow со значением 1, доступное пространство будет между ними распределяться равномерно. Если значением flex-grow одного из элементов является 2, данный элемент будет занимать двойную порцию пространства по сравнению с остальными элементами (или, по крайней мере, попытается это сделать).

.item {  flex-grow: 4; /* по умолчанию 0 */}

Отрицательные значения невалидны.

flex-shrink (сжатие, сокращение)

Данное свойство определяет способность элемента к сжатию при необходимости.

.item {  flex-shrink: 3; /* по умолчанию 1 */}

Отрицательные значения невалидны.

flex-basis

Данное свойство определяет стандартный размер элемента перед распределением оставшегося пространства. Этим размером может быть длина (например, 20%, 5rem и т.д.) или ключевое слово. Ключевое слово auto означает использование значения свойства width или height элемента (раньше вместо auto использовалось main-size). Ключевое слово content означает учет содержимого элемента. Указанное ключевое слово пока плохо поддерживается, поэтому сложно определить разницу между min-content, max-content и fit-content.

.item {  flex-basis:  | auto; /* по умолчанию auto */}

Если значением этого свойства является 0, окружающее элемента пространство не принимается в расчет. Если значением является auto, доступное пространство распределяется согласно значению свойства flex-grow.

flex

Данное свойство является сокращением для flex-grow, flex-shrink и flex-basis. Второй и третий параметры (flex-shrink и flex-basis) являются опциональными. Значением по умолчанию является 0 1 auto, при этом auto можно опустить.

.item {  flex: none | [ <'flex-grow'> <'flex-shrink'>? || <'flex-basis'> ]}

Рекомендуется использовать данное сокращение вместо определения каждого свойства, это позволяет автоматически определять значения свойств в правильном порядке.

align-selt (выравнивание отдельного элемента)



Данное свойство позволяет перезаписывать дефолтное или установленное с помощью align-self выравнивание отдельного элемента.

См. объяснение align-items для доступных значений.

.item {  align-self: auto | flex-start | flex-end | center | baseline | stretch;}

Обратите внимание, что float, clear и vertical-align применительно к флекс-элементу не имеют никакого эффекта.

Примеры


Начнем с очень простого примера решения проблемы выравнивания элемента по центру.

.parent {  display: flex;  height: 300px; /* Или любое другое значение */}.child {  width: 100px;  /* Или любое другое значение */  height: 100px; /* Или любое другое значение */  margin: auto;  /* Волшебство! */}

Это работает благодарю тому, что свойство margin со значением auto поглащает все доступное пространство флекс-контейнера. Поэтому установка значения вертикальных внешних отступов в auto приводит к идеальному центрированию элемента по обеим осям.

Теперь попробуем использовать больше свойств. У нас имеется 6 элементов фиксированного размера, способных перестраиваться в зависимости от ширины экрана без участия медиа-запросов. Мы хотим, чтобы доступное пространство основной оси распределялось между элементами равномерно.

.flex-container {  /* Сначала мы создаем контекст флекс-макета */  display: flex;  /* Затем мы определяем направление потока,    позволяя элементам переходить на следующую строку при необходимости   * Указанное свойство и его значения аналогичны следующему:   * flex-direction: row;   * flex-wrap: wrap;   */  flex-flow: row wrap;  /* Наконец, мы определяем, как должно распределяться оставшееся пространство */  justify-content: space-around;}

Готово. Осталось немного стилизовать:



Попробуем что-нибудь еще. Предположим, что в шапке сайта у нас имеется навигационное меню, выровненное по правому краю, и мы хотим, чтобы оно распологалось по центру на средних экранах и выстраивалось в одну колонку на маленьких экранах. Проще простого.

/* Большие экраны */.navigation {  display: flex;  flex-flow: row wrap;  /* Это выравнивает элементы по правому краю вдоль основной оси */  justify-content: flex-end;}/* Средние экраны */@media (max-width: 800px) {  .navigation {    /* На средних экранах мы выравниваем меню по центру за счет равномерного распределения доступного пространства между элементами */    justify-content: space-around;  }}/* Маленькие экраны */@media (max-width: 500px) {  .navigation {    /* На маленьких экранах мы меняем направление основной оси с горизонтального (строка) на вертикальное (колонка) */    flex-direction: column;  }}


Поэкпериментируем с гибкостью элементов. Как насчет создания mobile-first трехколоночного макета с полноразмерной шапкой и подвалом, который не зависит от порядка расположения элементов в разметке.

.wrapper {  display: flex;  flex-flow: row wrap;}/* С помощью flex-basis мы устанавливаем значение ширины элементов в 100% */.wrapper > * {  flex: 1 100%;}/* Порядок расположения элементов в разметке следующий * 1. header * 2. article * 3. aside 1 * 4. aside 2 * 5. footer *//* Средние экраны */@media all and (min-width: 600px) {  /* Мы указываем сайдбарам делить между собой строку */  .aside { flex: 1 auto; }}/* Большие экраны */@media all and (min-width: 800px) {  /* Мы меняем первый сайдбар и main местами   * И указываем main занимать в 2 раза больше места, чем сайдбары   */  .main { flex: 2 0px; }  .aside-1 { order: 1; }  .main    { order: 2; }  .aside-2 { order: 3; }  .footer  { order: 4; }}


Полезные ресурсы



Поддержка



Grid (далее по тексту Грид или Сетка)


CSS Grid Layout на сегодняшний день является самым мощным средством создания макетов страниц. Он представляет собой двунаправленную систему. Это означает, что он может работать как со строками, так и с колонками, в отличие от Флекс, который умеет работать только со строками. Грид включает в себя свойства родительского элемента (Grid Container, грид-контейнер, далее по тексту контейнер) и свойства дочерних элементов (Grid Elements, грид-элементы, далее по тексту элементы).

Терминология


Грид-контейнер (Grid Container, далее по тексту контейнер)

Элемент, которому присвоено свойство display со значением grid, становится грид-контейнером. Данный контейнер является прямым предком всех грид-элементов. В следующем примере элемент div с классом container является грид-контейнером.

<div class="container">  <div class="item item-1"> </div>  <div class="item item-2"> </div>  <div class="item item-3"> </div></div>

Грид-линия (Grid Line, далее по тексту линия)

Разделительная линия, формирующая структуру Сетки. Она может быть вертикальной или горизонтальной и обозначает границу строки или колонки. Желтая линия на изображении ниже это пример вертикальной грид-линии (грид-линии колонки).



Грид-трек (Grid Track, далее по тексту трек или дорожка)

Пространство между двумя смежными линиями. Вы можете думать о треках как о строках или колонках Грида. Вот пример дорожки между второй и третьей линиями.



Грид-область (Grid Area, далее по тексту область)

Общее пространство между четырьмя линиями. Область может занимать любое количество грид-ячеек. Вот пример области между горизонтальными линиями 1 и 3 и вертикальными линиями 1 и 3.



Грид-элемент (Grid Element, далее по тексту элемент)

Дочерний элемент (прямой потомок) контейнера. В следующем примере элементы с классом item являются грид-элементами, а элемент с классом sub-item нет.

<div class="container">  <div class="item"> </div>  <div class="item">    <p class="sub-item"> </p>  </div>  <div class="item"> </div></div>

Грид-ячейка (Grid Cell, далее по тексту ячейка)

Пространство между двумя смежными горизонтальными и вертикальными линиями. Это элементарная единица Сетки. Вот пример ячейки между горизонтальными линиями 1 и 2 и вертикальными линиями 2 и 3.



Пример


Колонки с гибкой шириной, которые автоматически перестраиваются в зависимости от ширины экрана без медиа-запросов.

.grid {  display: grid;  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));  /* Для маленьких экранов можно добавить min() */  /* grid-template-columns: repeat(auto-fill, minmax(min(200px, 100%), 1fr)); */  grid-gap: 1rem;  /* Данное свойство является стандартным, но хуже поддерживается */  /* gap: 1rem */}


Свойства грид-контейнера


display

Данное свойство делает элемент грид-контейнером и устанавливает грид-контекст для его содержимого.

.container {  display: grid | inline-grid;}

  • grid блочная Сетка
  • inline-grid строчная Сетка

Обратите внимание, что возможность создания вложенных Гридов была перенесена в спецификацию CSS Grid второго уровня.

grid-template-columns, grid-template-rows

Данные свойства определяют колонки и строки Сетки с разделенными пробелами значениями. Значения представляют собой размер трека, а пробелы линию.

.container {  grid-template-columns:  ... |   ...;  grid-template-rows:  ... |   ...;}

  • <track-size> длина, проценты или фракции свободного пространства Грида (используется единица измерения fr)
  • <line-name> произвольное название

Когда вы оставляете пустое пространство между значениями треков, линиям автоматически присваиваются положительные и отрицательные числа:

.container {  grid-template-columns: 40px 50px auto 50px 40px;  grid-template-rows: 25% 100px auto;}



Вы можете явно указывать названия линий. Обратите внимание на квадратные скобки:

.container {  grid-template-columns: [first] 40px [line2] 50px [line3] auto [col4-start] 50px [five] 40px [end];  grid-template-rows: [row1-start] 25% [row1-end] 100px [third-line] auto [last-line];}



Линии могут иметь несколько названий. Здесь, например, вторая линия имеет два названия:

.container {  grid-template-rows: [row1-start] 25% [row1-end row2-start] 25% [row2-end];}

Если определение содержит повторяющиеся фрагменты, можно использовать инструкцию repeat для сокращения:

.container {  grid-template-columns: repeat(3, 20px [col-start]);}

Что эквивалентно следующему:

.container {  grid-template-columns: 20px [col-start] 20px [col-start] 20px [col-start];}

Если несколько линий имеют одинаковые имена, можно использовать название линии и количество таких линий.

.item {  grid-column-start: col-start 2;}

Единица fr позволяет установить размер трека как фракцию свободного пространства контейнера. В примере ниже ширина каждого элемента равняется трети ширины контейнера:

.container {  grid-template-columns: 1fr 1fr 1fr;}

Свободное пространство вычисляется после вычитания элементов фиксированного размера. В следующем примере пространство, доступное для распределения с помощью единицы fr не включает 50px:

.container {  grid-template-columns: 1fr 50px 1fr 1fr;}

grid-template-areas

Данное свойство определяет шаблон Сетки с помощью названий областей, определенных в свойствах grid-area. Повторение названий областей заставляет контент расширяться на указанное количество ячеек. Точка означает пустую ячейку. Такой синтаксис позволяет визуализировать структуру Сетки.

.container {  grid-template-areas:    " | . | none | ..."    "...";}

  • <grid-area-name> название области
  • . пустая ячейка
  • none область отсутствует

Пример:

.item-a {  grid-area: header;}.item-b {  grid-area: main;}.item-c {  grid-area: sidebar;}.item-d {  grid-area: footer;}.container {  display: grid;  grid-template-columns: 50px 50px 50px 50px;  grid-template-rows: auto;  grid-template-areas:    "header header header header"    "main main . sidebar"    "footer footer footer footer";}

Этот код создает Сетку шириной в четыре колонки и высотой в три строки. Первая (верхняя) строка полностью занимается шапкой. Средняя строка состоит из двух областей main, одной пустой ячейки и одной области sidebar. Последняя строка подвал.



Каждая строка в определении должна состоять из одинакового количества колонок.

Для обозначения пустой ячейки можно использовать любое количество точек. До тех пор, пока между точками нет проблема, они являются индикатором пустой ячейки.

Обратите внимание, что данный синтаксис применяется для обозначения областей, а не линий. При использовании этого синтаксиса линия каждой стороны области именуется автоматически. Если названием области является foo, то названием начальных (первых) линий (строчной и колоночной) области будет foo-start, а последних foo-end. Это означает, что некоторые линии могут иметь несколько названий, как в рассмотренном примере, где верхняя левая линия имеет три названия: header-start, main-start и footer-start.

grid-template

Данное свойство является сокращением для grid-template-rows, grid-template-columns и grid-template-areas.

.container {  grid-template: none | <grid-template-rows> / <grid-template-columns>;}

  • none сбрасывает значения всех трех свойств до дефолтных
  • <grid-template-rows> / <grid-template-columns> устанавливает свойствам grid-template-rows и grid-template-columns соответствующие значения, а свойству grid-template-areas значение none

Можно использовать более сложный, но в тоже время более удобный синтаксис для определения значений всех трех свойств:

.container {  grid-template:    [row1-start] "header header header" 25px [row1-end]    [row2-start] "footer footer footer" 25px [row2-end]    / auto 50px auto;}

Это эквивалентно следующему:

.container {  grid-template-rows: [row1-start] 25px [row1-end row2-start] 25px [row2-end];  grid-template-columns: auto 50px auto;  grid-template-areas:    "header header header"    "footer footer footer";}

Поскольку grid-template не сбрасывает неявные свойства Сетки (такие как grid-auto-columns, grid-auto-rows и grid-auto-flow), что требуется в большинстве случаев, вместо него рекомендуется использовать свойство grid.

column-gap, row-gap, grid-column-gap, grid-row-gap

Данные свойства определяют размеры линий. Другими словами, они определяют величину отступов между колонками/строками.

.container {  /* стандарт */  column-gap: <line-size>;  row-gap: <line-size>;  /* раньше */  grid-column-gap: <line-size>;  grid-row-gap: <line-size>;}

  • <line-size> величина отступов

Пример:

.container {  grid-template-columns: 100px 50px 100px;  grid-template-rows: 80px auto 80px;  column-gap: 10px;  row-gap: 15px;}



Отступы создаются только между колонками/строками, но не по краям Сетки.

Обратите внимание, что свойства grid-column-gap и grid-row-gap в настоящее время переименованы в column-gap и row-gap.

gap, grid-gap

Данное свойство является сокращением для row-gap и column-gap.

.container {  /* стандарт */  gap: <grid-row-gap> <grid-column-gap>;  /* раньше */  grid-gap: <grid-row-gap> <grid-column-gap>;}

  • <grid-row-gap> <grid-column-gap> размеры отступов

Пример:

.container {  grid-template-columns: 100px 50px 100px;  grid-template-rows: 80px auto 80px;  gap: 15px 10px;}

Если значение свойства row-gap не установлено, оно принимает значение свойства column-gap.

Обратите внимание, что свойство grid-gap в настоящее время переименовано в gap.

justify-items

Данное свойство определяет выравнивание элементов в ячейках вдоль строчной (горизонтально) оси (в противоположность своству align-items, определяющему выравнивание вдоль блочной (колоночной, вертикальной) оси). Значение этого свойства применяется ко всем элементам контейнера.

.container {  justify-items: start | end | center | stretch;}

  • start элементы сдвигаются в начало их ячеек (левая граница)
  • end элементы сдвигаются в конец ячеек (правая граница)
  • center элементы распологатся по центру
  • stretch элементы заполняют всю ширину ячеек

.container {  justify-items: start;}



.container {  justify-items: end;}



.container {  justify-items: center;}



.container {  justify-items: stretch;}



Расположение отдельного элемента вдоль строчной оси ячейки контролируется свойством justify-self.

align-items

Данное свойство определяет выравнивание элементов в ячейках вдоль блочной (колоночной, вертикальной) оси (в противоположность свойству justify-items, которое определяет выравнивание вдоль строчной (горизонтальной) оси). Значение этого свойства применяется ко всем элементам контейнера.

.container {  align-items: start | end | center | stretch;}

  • start элементы сдвигаются в начало их ячеек (верхняя граница)
  • end элементы сдвигаются в конеч ячеек (нижняя граница)
  • center элементы располагаются по центру
  • stretch элементы заполняют всю высоту ячеек

.container {  align-items: start;}



.container {  align-items: end;}



.container {  align-items: center;}



.container {  align-items: stretch;}



Расположение отдельного элемента вдоль колоночной оси ячейки контролируется свойством align-self.

place-items

Данное свойство является сокращением для align-items и justify-items.

.container {  place-items: <align-items> <justify-items>;}

  • <align-items> <justify-items> первое значение для align-items, второе для justify-items. Если второе значение отсутствует, первое значение присваивается обоим свойствам.

Пример:

.container {  place-items: center start;}

Элементы располагаются по центру ячеек вдоль колоночной оси и в начале вдоль строчной оси.

justify-content (выравнивание колонок)

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

.container {  justify-content: start | end | center | stretch | space-around | space-between | space-evenly;}

  • start сдвигает Сетку в начало контейнера (левая граница)
  • end сдвигает Сетку в конец контейнера (правая граница)
  • center Сетка располагается по центру
  • stretch колонки растягиваются таким образом, чтобы Сетка занимала всю ширину контейнера
  • space-around одинаковое пространство между колонками, и половина такого пространства по краям контейнера
  • space-between первая колонка сдвигается в начало контейнера, последняя в конец, свободное пространство равномерно распределяется между остальными колонками
  • space-evenly одинаковое пространство как между колонками, так и по краям контейнера

.container {  justify-content: start;}



.container {  justify-content: end;}



.container {  justify-content: center;}



.container {  justify-content: stretch;}



.container {  justify-content: space-around;}



.container {  justify-content: space-between;}



.container {  justify-content: space-evenly;}



align-content (выравнивание строк)

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

.container {  align-content: start | end | center | stretch | space-around | space-between | space-evenly;}

  • start сдвигает Сетку в начало контейнера (верхняя граница)
  • end сдвигает Сетку в конец контейнера (нижняя граница)
  • center Сетка располагается по центру
  • stretch строки растягиваются таким образом, чтобы Сетка занимала всю высоту контейнера
  • space-around одинаковое пространство между строками, и половина такого пространства по краям контейнера
  • space-between первая строка сдвигается в начало контейнера, последняя в конец, свободное пространство равномерно распределяется между остальными строками
  • space-evenly одинаковое пространство как между строками, так и по краям контейнера

.container {  align-content: start;}



.container {  align-content: end;}



.container {  align-content: center;}



.container {  align-content: stretch;}



.container {  align-content: space-around;}



.container {  align-content: space-between;}



.container {  align-content: space-evenly;}



place-content

Данное свойство является сокращением для align-content и justify-content.

.container {  place-content: <align-content> <justify-content>;}

  • <align-content> <justify-content> первое значение для align-content, второе для justify-content. Если второе значение отсутствует, первое значение присваивается обоим свойствам.

Пример:

.container {  place-content: center start;}

Строки располагаются по центру контейнера, колонки в начале.

grid-auto-columns, grid-auto-rows (размер неявных треков)

Данные свойства определяют размер автоматически создаваемых дорожек (неявных треков). Неявные треки создаются при наличии большего количества элементов, чем ячеек Сетки, или при размещении элемента за пределами Сетки.

.container {  grid-auto-columns: <track-size> ...;  grid-auto-rows: <track-size> ...;}

  • <track-size> длина, проценты или фракции свободного пространства Сетки (используется единица измерения fr)

Пример:

.container {  grid-template-columns: 60px 60px;  grid-template-rows: 90px 90px;}



Это создает Сетку размером 2х2.

Допустим, мы используем свойства grid-column и grid-row для позиционирования элементов следующим образом:

.item-a {  grid-column: 1 / 2;  grid-row: 2 / 3;}.item-b {  grid-column: 5 / 6;  grid-row: 2 / 3;}



Началом элемента с классом item-b является вертикальная линия 5, а концом вертикальная линия 6, но мы не определили эти линии. Поскольку мы ссылаемся на несуществующие линии, для заполнения отступов создаются неявные треки с нулевой шириной. Мы можем использовать свойство grid-auto-columns для определения ширины неявных дорожек:

.container {  grid-auto-columns: 60px;}



grid-auto-flow

Для позиционирования элементов, находящихся за пределами Сетки, можно использовать алгоритм автоматического размещения (auto-placement algorithm). Рассматриваемое свойство определяет, как должен работать данный алгоритм.

.container {  grid-auto-flow: row | column | row dense | column dense;}

  • row (по умолчанию) алгоритм заполняет текущую строку до предела и, при необходимости, когда ширины текущей строки оказалось недостаточно, создает новую строку
  • column алгоритм заполняет текущую колонку до предела и, при необходимости, когда высоты текущей колонки оказалось недостаточно, создает новую колонку
  • dense интеллектуальное заполнение Сетки при наличии элементов разного размера

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

Предположим, что у нас имеется такая разметка:

  <section class="container">  <div class="item-a">item-a</div>  <div class="item-b">item-b</div>  <div class="item-c">item-c</div>  <div class="item-d">item-d</div>  <div class="item-e">item-e</div></section>

Мы определяем Сетку, состоящую из пяти колонок и двух строк, и устанавливаем свойству grid-auto-flow значение row (которое является значением по умолчанию):

.container {  display: grid;  grid-template-columns: 60px 60px 60px 60px 60px;  grid-template-rows: 30px 30px;  grid-auto-flow: row;}

При размещении элементов в Сетке, мы определяем места только двух элементов:

.item-a {  grid-column: 1;  grid-row: 1 / 3;}.item-e {  grid-column: 5;  grid-row: 1 / 3;}

Поскольку значением свойства grid-auto-flow является row, наша Сетка выглядит так, как показано на изображении ниже. Обратите внимание на расположение элементов с классами item-b, item-c и item-d (предпочтение отдается строкам):



Если поменять значение свойства grid-auto-flow на column, Сетка будет выглядеть следующим образом (предпочтение отдается колонкам):



grid

Данное свойство является сокращением для grid-template-rows, grid-template-columns, grid-template-areas, grid-auto-rows, grid-auto-columns и grid-auto-flow (обратите внимание, что в одном объявлении могут содержаться только явные или неявные свойства).

  • none все свойства принимают значения по умолчанию
  • <grid-template> аналогично сокращению grid-template
  • <grid-template-rows> / [auto-flow && dense?] <grid-auto-columns>? определяет значение для grid-template-rows. Если справа от слэша имеется ключевое слово auto-flow, значением свойства grid-auto-flow становится column. Если в дополнение к auto-flow указано ключевое слово dense, алгоритм автоматического размещения упаковывает элементы соответствующим образом. Если значение для свойства grid-auto-columns опущено, его значением становится auto
  • [auto-flow && dense?] <grid-auto-rows>? / <grid-template-columns> определяет значение для grid-template-columns. Если слева от слэша имеется ключевое слово auto-flow, значением свойства grid-auto-flow становится row. Если в дополнение к auto-flow указано ключевое слово dense, алгоритм автоматического размещения упаковывает элементы соответствующим образом. Если значение для свойства grid-auto-rows опущено, его значением становится auto

Следующие два блока кода эквиваленты:

.container {  grid: 100px 300px / 3fr 1fr;}

.container {  grid-template-rows: 100px 300px;  grid-template-columns: 3fr 1fr;}

Следующие два блока кода эквиваленты:

.container {  grid: auto-flow / 200px 1fr;}

.container {  grid-auto-flow: row;  grid-template-columns: 200px 1fr;}

Следующие два блока кода эквиваленты:

.container {  grid: auto-flow dense 100px / 1fr 2fr;}

.container {  grid-auto-flow: row dense;  grid-auto-rows: 100px;  grid-template-columns: 1fr 2fr;}

И следующие два блока кода также эквиваленты:

.container {  grid: 100px 300px / auto-flow 200px;}

.container {  grid-template-rows: 100px 300px;  grid-auto-flow: column;  grid-auto-columns: 200px;}

В данном случае мы можем использовать более сложный, но в тоже время более удобный синтаксис для одновременного определения свойств grid-template-areas, grid-template-rows и grid-template-columns и установки прочих свойств в дефолтные значения. Что для этого нужно сделать, так это определить названия линий и размеры треков с соответствующими областями на одной строке. Это проще продемонстрировать на примере:

.container {  grid: [row1-start] "header header header" 1fr [row1-end]        [row2-start] "footer footer footer" 25px [row2-end]        / auto 50px auto;}

Это равнозначно следующему:

.container {  grid-template-areas:    "header header header"    "footer footer footer";  grid-template-rows: [row1-start] 1fr [row1-end row2-start] 25px [row2-end];  grid-template-columns: auto 50px auto;}

Свойства грид-элементов


Обратите внимание, что свойства float, display: inline-block, display: table-cell, vertical-align и column-*, применяемые к грид-элементу, не имеют никакого эффекта.

grid-column-start, grid-column-end, grid-row-start, grid-row-end

Данные свойства определяют положение элемента в Сетке через привязку к определенным линиям. grid-column-start / grid-row-start это начальные линии элемента, а grid-column-end / grid-row-end конечные.

.item {  grid-column-start: <number> | <name> | span <number> | span <name> | auto;  grid-column-end: <number> | <name> | span <number> | span <name> | auto;  grid-row-start: <number> | <name> | span <number> | span <name> | auto;  grid-row-end: <number> | <name> | span <number> | span <name> | auto;}

  • <line> может быть числом (привязка осуществляется к номеру линии) или названием (привязка осществляется к названию линии)
  • span <number> элемент будет растянут на указанное количество треков
  • span <name> элемент будет растягиваться до достижения линии с указанным названием
  • auto автоматическое расположение, автоматическое расширение или дефолтное растягивание на одну колонку

.item-a {  grid-column-start: 2;  grid-column-end: five;  grid-row-start: row1-start;  grid-row-end: 3;}



.item-b {  grid-column-start: 1;  grid-column-end: span col4-start;  grid-row-start: 2;  grid-row-end: span 2;}



Если значения свойств grid-column-end / grid-row-end не определены, элемент займет 1 трек по умолчанию.

Элементы могут перекрывать друг друга. Вы можете использовать свойство z-index для управления порядком наложения элементов.

grid-column, grid-row

Данные свойства являются сокращением для grid-column-start + grid-column-end и grid-row-start + grid-row-end, соответственно.

.item {  grid-column: <start-line> / <end-line> | <start-line> / span <value>;  grid-row: <start-line> / <end-line> | <start-line> / span <value>;}

  • <start-line> / <end-line> значения аналогичны значениям оригинальных свойств, включая span

Пример:

.item-c {  grid-column: 3 / span 2;  grid-row: third-line / 4;}



Если конечная линия не определена, элемент по умолчанию будет занимать 1 трек.

grid-area

Данное свойство определяет название элемента, которое используется в качестве значения в свойстве grid-template-areas. grid-area также может использоваться как сокращение для grid-row-start + grid-column-start + grid-row-end + grid-column-end.

.item {  grid-area: <name> | <row-start> / <column-start> / <row-end> / <column-end>;}

  • <name> произвольное название элемента
  • <row-start> / <column-start> / <row-end> / <column-end> могут быть числами или названиями линий

Присвоение названия элементу:

.item-d {  grid-area: header;}

Сокращение для grid-row-start + grid-column-start + grid-row-end + grid-column-end:

.item-d {  grid-area: 1 / col4-start / last-line / 6;}



justify-self

Данное свойство определяет выравнивание элемента в ячейке вдоль строчной оси (в противоположность свойству align-self, которое определяет выравнивание вдоль блочной (колоночной) оси). Это свойство применяется к элементу внутри отдельной ячейки.

.item {  justify-self: start | end | center | stretch;}

  • start элемент сдвигается в начало ячейки (левая граница)
  • end элемент сдвигается в конец ячейки (правая граница)
  • center элемент располагается по центру
  • stretch элемент заполняет всю ширину ячейки

.item-a {  justify-self: start;}



.item-a {  justify-self: end;}



.item-a {  justify-self: center;}



.item-a {  justify-self: stretch;}



Для управления выравниванием всех элементов Сетки вдоль строчной оси используется свойство justify-items.

align-self

Данное свойство определяет выравнивание элемента в ячейке вдоль блочной (колоночной) оси (в противоположность свойству justify-self, которое определяет выравнивание вдоль строчной оси). Это свойство применяется к элементу внутри отдельной ячейки.

.item {  align-self: start | end | center | stretch;}

  • start элемент сдвигается в начало ячейки (верхняя граница)
  • end элемент сдвигается в конец ячейки (нижняя граница)
  • center элемент располагается по центру
  • stretch элемент заполняет всю высоту ячейки

.item-a {  align-self: start;}



.item-a {  align-self: end;}



.item-a {  align-self: center;}



.item-a {  align-self: stretch;}



Для управления выравниванием всех элементов Сетки вдоль блочной (колоночной) оси используется свойство align-items.

place-self

Данное свойство является сокращением для align-self и justify-self.

  • auto значение по умолчанию
  • <align-self> / <justify-self> первое значение для align-self, второе для justify-self. Если второе значение опущено, первое значение применяется к обоим свойствам

.item-a {  place-self: center;}



.item-a {  place-self: center stretch;}



Специальные функции и ключевые слова

  • При определении размеров строк и колонок можно использовать не только обычные единицы измерения, такие как px, rem, % и т.д., но и такие ключевые слова, как min-content, max-content, auto и, пожалуй, самое полезное фракции (fr). grid-template-columns: 200px 1fr 2fr min-content
  • Вы также можете использовать функции, которые делают элементы масштабируемыми. Например, так можно определить элемент, шириной в 1fr, способный сжиматься до 200px: grid-template-columns: 1fr minmax(200px, 1fr)
  • Функция repeat() используется для быстрого определения повторяющихся элементов: grid-template-columns: repeat(10, 1fr) (десять колонок, шириной в 1fr)
  • Сочетание названных возможностей позволяет добиться невероятной гибкости макета, например: grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)) (из примера в начале раздела про Грид)

Полезные ресурсы



Поддержка



Туториал


В данном туториале мы создадим простой тренажер по основным свойствам Flexbox.



Разметка

<main>  <div id="controls">    <div id="buttons">      <button data-btn="addBtn">Add Item</button>      <button data-btn="removeBtn">Remove Item</button>    </div>    <fieldset id="flexContainerProps">      <legend>Flex Container Props</legend>      <label for="flexDirection">flex-direction</label>      <select id="flexDirection">        <option value="row" selected>row</option>        <option value="row-reverse">row-reverse</option>        <option value="column">column</option>        <option value="column-reverse">column-reverse</option>      </select>      <label for="flexWrap">flex-wrap</label>      <select id="flexWrap">        <option value="nowrap">nowrap</option>        <option value="wrap" selected>wrap</option>        <option value="wrap-reverse">wrap-reverse</option>      </select>      <label for="justifyContent">justify-content</label>      <select id="justifyContent">        <option value="flex-start">flex-start</option>        <option value="flex-end">flex-end</option>        <option value="center" selected>center</option>        <option value="space-between">space-between</option>        <option value="space-around">space-around</option>        <option value="space-evenly">space-evenly</option>      </select>      <label for="alignItems">align-items</label>      <select id="alignItems">        <option value="flex-start">flex-start</option>        <option value="flex-end">flex-end</option>        <option value="center" selected>center</option>        <option value="stretch">stretch</option>        <option value="baseline">baseline</option>      </select>      <label for="alignContent">align-content</label>      <select id="alignContent">        <option value="flex-start" selected>flex-start</option>        <option value="flex-end">flex-end</option>        <option value="center">center</option>        <option value="stretch">stretch</option>        <option value="space-between">space-between</option>        <option value="space-around">space-around</option>        <option value="space-evenly">space-evenly</option>      </select>    </fieldset>    <fieldset id="flexItemProps">      <legend>Flex Item Props</legend>      <label for="order">order</label>      <input        id="order"        type="number"        min="-5"        max="5"        step="1"        value="0"      />      <label for="flexGrow">flex-grow</label>      <input        id="flexGrow"        type="number"        min="0"        max="5"        step="1"        value="0"      />      <label for="flexShrink">flex-shrink</label>      <input        id="flexShrink"        type="number"        min="1"        max="6"        step="1"        value="1"      />      <label for="alignSelf">align-self</label>      <select id="alignSelf">        <option value="auto" selected>auto</option>        <option value="flex-start">flex-start</option>        <option value="flex-end">flex-end</option>        <option value="center">center</option>        <option value="stretch">stretch</option>        <option value="baseline">baseline</option>      </select>    </fieldset>  </div>  <div id="flexContainer">    <div class="flex-item selected">1</div>    <div class="flex-item">2</div>    <div class="flex-item">3</div>    <div class="flex-item">4</div>    <div class="flex-item">5</div>    <div class="flex-item">6</div>  </div></main>

Здесь у нас имеется флекс-контейнер (flexContainer) с шестью флекс-элементами (flex-item) и контейнер (controls) для управления переключением свойств флекс-контейнера (flexContainerProps) и выбранного (selected) флекс-элемента (flexItemProps). Также во втором контейнере у есть две кнопки (buttons): одна для добавление элемента во флекс-контейнер (addBtn), другая для удаления последнего флекс-элемента (removeBtn).

Стили

main {  display: flex;  justify-content: center;  align-items: center;}#controls {  margin-right: 0.4rem;}#buttons {  margin: 0.4rem;  display: flex;  flex-wrap: wrap;  justify-content: center;}button {  margin: 0.2rem;}label {  display: block;  margin: 0.4rem;}select {  width: 100%;}#flexContainer {  width: 600px;  height: 600px;  border: 1px dashed #222;  border-radius: 4px;  display: flex;  flex-wrap: wrap;  justify-content: center;  align-items: center;}.flex-item {  min-width: 178px;  min-height: 178px;  background: radial-gradient(circle, yellow, orange);  border: 1px solid #222;  border-radius: 4px;  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.4);  color: #222;  font-size: 2rem;  display: flex;  justify-content: center;  align-items: center;  user-select: none;  cursor: pointer;}.flex-item:nth-child(2n) {  min-width: 158px;  min-height: 158px;}.flex-item:nth-child(3n) {  min-width: 198px;  min-height: 198px;}.flex-item.selected {  background: radial-gradient(circle, lightgreen, darkgreen);}

Флекс-элементы должны быть разного размера и при этом гибкими. Мы добиваемся этого с помощью min-width/min-height и nth-child. Выбранный элемент имеет класс selected с другим цветом фона.

Скрипт

// Получаем контейнеры и кнопкиconst controls = document.querySelector('#controls')const buttons = document.querySelector('#buttons')const flexContainer = document.querySelector('#flexContainer')// Обрабатываем нажатие кнопкиbuttons.addEventListener('click', (e) => {  // Нас интересует только нажатие кнопки  if (e.target.tagName !== 'BUTTON') return  // Получаем название кнопки  const { btn } = e.target.dataset  switch (btn) {    // Если нажата кнопка для добавления элемента во флекс-контейнер    case 'addBtn':      // Максимальное количество элементов составляет 6 единиц      // При большем количестве произойдет переполнение флекс-контейнера      // Определяем порядковый номер элемента      const num = document.querySelectorAll('.flex-item').length + 1      // Проверяем, что в контейнере находится меньше 7 элементов      // Если это так      if (num < 7) {        // Создаем новый элемент "div"        const newItem = document.createElement('div')        // Добавляем ему соответствующий класс        newItem.className = 'flex-item'        // Нумеруем его        newItem.textContent = num        // И помещаем во флекс-контейнер        flexContainer.append(newItem)      }      break    // Если нажата кнопка для удаления флекс-элемента    case 'removeBtn':      // Определяем индекс последнего элемента      const index = document.querySelectorAll('.flex-item').length - 1      // Проверяем, что индекс последнего элемента больше 0      // Мы не хотим удалять единственный флекс-элемент      if (index > 0) {        // Определяем элемент, подлежащий удалению        const itemToRemove = document.querySelectorAll('.flex-item')[index]        // И удаляем его        itemToRemove.remove()      }      break  }})// Обрабатываем изменения селектов и инпутовcontrols.addEventListener('change', (e) => {  // Получаем название измененного свойства  const prop = e.target.id  // Получаем значение измененного свойства  const value = e.target.value  // Проверяем, какое свойство изменилось  // Свойство флекс-контейнера или свойство выбранного флекс-элемента  // И добавляем соответствующие свойства и значения  if (e.target.parentElement.id === 'flexContainerProps') {    flexContainer.style[prop] = value  } else {    const selectedItem = document.querySelector('.selected')    selectedItem.style[prop] = value  }})// Обрабатываем выбор флекс-элементаflexContainer.addEventListener('click', (e) => {  // Нас интересует только клик по не выбранному флекс-элементу  if (    e.target.className !== 'flex-item' ||    e.target.classList.contains('selected')  )    return  // Нам нужно удалить класс "selected" у текущего выбранного элемента  if (document.querySelector('.selected') !== null)    document.querySelector('.selected').classList.remove('selected')  // И добавить его новому выбранному элементу  e.target.classList.add('selected')  // При выборе флекс-элемента нам необходимо получить значения его "флекс-свойств"  // И присвоить их соответствующим инпутам и селекту  // Это позволяет изменять свойства выбранного флекс-элемента  // Переключаться на другой элемент  // Возвращаться к первому, и редактировать ранее измененные свойства  // Вспомогательная функция для получения значения определенного свойства указанного элемента  // Значением по умолчанию второго параметра функции является выбранный флекс-элемент  const getStyle = (property, element = e.target) =>    getComputedStyle(element).getPropertyValue(property)  // Воспользуемся тем, что мы можем обращаться к DOM-элементам, имеющим атрибут "id", напрямую  // Обратите внимание, что делать так не рекомендуется  order.value = getStyle('order')  flexGrow.value = getStyle('flex-grow')  flexShrink.value = getStyle('flex-shrink')  alignSelf.value = getStyle('align-self')})

Как видите, ничего сложного. Конечно, функционал получившегося тренажера весьма ограничен. Однако, полагаю, этого вполне достаточно для понимания работы основных свойств Flexbox.

Рекомендую попробовать создать аналогичный тренажер по основным свойствам Grid это будет отличной практикой и позволит как следует во всем разобраться.



Вам также может показаться интересным один из моих последних проектов Современный стартовый HTML-шаблон.

Благодарю за внимание и хорошего дня.
Подробнее..

Гексагональные тайлоыве миры

23.05.2021 20:21:44 | Автор: admin

Тайловость в играх очень распространенное явление, особенно в играх инди сегмента. Чаще всего используют квадратные тайлы в них проще всего задать необходимые данные, будь то карта уровня или инвентарь. Однако на квадратных и прямоугольных формах возможности подобной системы не ограничиваются.

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

Если вы не читали мою предыдущую статью про прямоугольные сетки, то рекомендую ознакомится, потому как я иногда буду опускать некоторые вещи, ссылаясь именно на нее. Дабы не изобретать велосипед, некоторые алгоритмы я взял с этой статьи на английском. Кто не хочет или не может в английский, есть ее перевод на хабре. Все демки и примеры сделаны на движке Godot Engine v 3.2.3 с использованием его встроенного языка.

Думаю в целом его синтаксис ясен, однако оставлю ссылки на некоторые функции:

Система координат

На протяжении всей статьи мы будем работать только с правильными шестиугольниками, у них все стороны равны. Работа с неправильными шестиугольниками лишена смысла в принципе. Если не брать всякие повороты и искажения, существует два вида шестиугольных сеток, вертикально и горизонтально ориентированных:

  • Такие я буду называть вертикальными (у ячейки есть явный вертикальный сосед):

  • А такие горизонтальными (у ячейки есть явный горизонтальный сосед):

Уже на этапе введения координат могут возникнуть проблемы. Дело в том, что в шестиугольной сетке невозможно ввести типичную декартову систему координат - всегда будет ось, вдоль которой у ячейки не будет явного соседа. На самом деле существует огромное количество систем координат для таких сеток. Более подробно о них рассказано в упомянутых вначале статьях. Я рассказывать про каждую не буду, все таки пост больше про использование сеток, а не их исследование.

Пожалуй первое, что приходит в голову, это таки впихнуть декартовы оси, однако для этого по одному из направлений координаты придется смещать:

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

Главная проблема этих координат состоит в выборе базисов. Для разных ячеек базис к следующей ячейке может отличаться от того, по которому мы к ней перешли. Если вы помните из статьи про прямоугольные тайлы, там мы обращали матрицу из базисных векторов и умножали на нее радиус-вектор пикселя, получая координаты ячейки. Здесь так не выйдет, провернув тоже самое мы просто получим квадратную сетку.

Вообще у сетки шестиугольников есть три ярко выраженных оси:

Что то напоминает, не находите? Тут три оси, прям как в пространстве. На самом деле гений тот человек - кто не просто увидел, что если взглянуть на куб под определенным углом, то получится шестиугольник, а еще и додумался использовать трехмерные координаты в двумерной сетке шестиугольников. Правда вот попробовав посчитать координаты на двумерной сетке, могут вскипеть мозги, ведь третья ось тут кажется лишней и ее использование будто только мешает. Для разрешения данной ситуации просто посмотрим откуда взялись кубы:

Для получения сетки шестиугольников надо взглянуть в изометрии на пирамидку, на каждом уменьшении высоты которой увеличивается количество кубиков в ряду на один. Включив фантазию, можно увидеть в каждом кубе шестиугольник. Включив внимательность, можно увидеть в левом нижнем углу координаты. Включив логику, можно увидеть, что сумма этих самых координат всегда равна некоторому числу, которое, кстати говоря, может быть любым, ведь мы сами решаем, откуда начнется система координат. В моем случае это 15, просто потому, что система отсчета в этом редакторе начинается с нижнего дальнего угла (в случае изометрического вида). Нам незачем таскать за собой лишнюю константу, поэтому возьмем ее за ноль. Таким образом, сумма трех координат всегда и везде равна нулю, поэтому третья нам попросту не нужна, ведь мы можем получить ее из двух других. Теперь и получается, что "мешающая" третья ось уходит. Наконец то мы можем спокойно направить базисы и у каждой ячейки всегда будет точный сосед, находящийся вдоль оси:

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

Преобразование координат

Пожалуй это то, на чем запарываются многие при попытке сделать нечто шестиугольное. Я слышал множество способов, порой они были очень забавные. Например, мне больше всего запомнился способ, где предлагалось сделать невидимую маску с такой же сеткой, где каждая ячейка окрашена в свой цвет, а каждому цвету соответствуют координаты. При смещении курсора смотрим в маску и по цвету определяем координаты. Сложно, с костылями, но работать может. А если мы захотим другой размер? Перерисовывать? Вы тут явно не за этим.

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

Оранжевые (маленькие) диагонали делятся пополам зелеными (большими), а зеленые оранжевыми, если последние провести из середины стороны. Уже получается, что мы можем разделить шестиугольник на 4 прямоугольника. Однако некоторые вершины в таком случае будут лежать где то между углами сетки, а ведь нам хотелось бы, чтобы все они попадали ровно в углы. На самом деле "где то", это ровно по серединке, поэтому разделим большие ячейки еще на пополам, тогда все вершины шестиугольника будут ложиться точно в углы прямоугольной сетки:

Желтую сетку в дальнейшем я буду называть вспомогательной. Для задания ее базисов будем использовать такие значения:

# Для горизонтальных шестиугольниковvar hex_size = 32var short = int(size*sqrt(3)/2) # 1/2 from short hex diagonalvar long = int(size/2) # 1/4 from long hex diagonal

Теперь мы можем выразить базисы шестиугольной сетки, используя базисы вспомогательной сетки:

Запишем все базисы в коде:

...# Transorm2D в godot - это матрица 3x2, где последняя строка указыает# смещение объекта, в дальнейшем она не будет использоваться совсем, # поэтому считайте это просто матрицей 2x2. Сделано это для удобства,# на объяснения никак не повлияет.# У нее есть два атрибута - x и y. Каждый из них это вектор. X - представляет# первый столбец матрицы 2x2 (крайняя строка не учитывается), Y - второй столбец.  var grid_basis = Transform2D() # Матрица базисов вспомогательной сеткиvar hex_basis = Transform2D() # Матрица базисов гексагональной сетки...  # Для вертикальной сеткиgrid_basis.x = Vector2(long, 0)grid_basis.y = Vector2(0, short)hex_basis.x = grid_basis.x*3 + grid_basis.yhex_basis.y = grid_basis.y*2# Для горизонтальной сеткиgrid_basis.x = Vector2(short, 0)grid_basis.y = Vector2(0, long)hex_basis.x = grid_basis.x*2hex_basis.y = grid_basis.x+grid_basis.y*3

Я пользуюсь именно встроенными средствами Godot для упрощения работы в целом. Все подобные места будут поясняться в общем виде, как бы это делалось без встроенных возможностей.

Шестиугольник в пиксель

Вот за что я люблю математику, так это за то, что если она работает, то работает везде. Так что для получения центра шестиугольника в пикселях из его координат на сетке надо просто умножить базисы на координаты:

func hex2pixel(hex):return hex.x*hex_basis.x + hex.y*hex_basis.y

Для получения каждой вершины просто прибавляем по нужным базисам:

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

Для вертикальных шестиугольников:

func _get_vert_hex_vertices(hex):var pixel = hex2pixel(hex)return PoolVector2Array([pixel+2*grid_basis.x,pixel+grid_basis.x+grid_basis.y,pixel-grid_basis.x+grid_basis.y,pixel-2*grid_basis.x,pixel-grid_basis.x-grid_basis.y,pixel+grid_basis.x-grid_basis.y])

Для горизонтальных шестиугольников:

func _get_hor_hex_vertices(hex):var pixel = hex2pixel(hex)return PoolVector2Array([pixel+grid_basis.x-grid_basis.y,pixel+grid_basis.x+grid_basis.y,pixel+2*grid_basis.y,pixel-grid_basis.x+grid_basis.y,pixel-grid_basis.x-grid_basis.y,pixel-2*grid_basis.y,])

Пиксель в шестиугольник

Наверно самая интригующая часть поста. На самом деле ничего нового почти не будет, ведь для получения вещественных координат ячейки все также обращаем матрицу и умножаем на нее радиус-вектор пикселя:

Для горизонтальной ориентации

В коде это записывается так:

func pixel2hex(pixel):var x = pixel.x/(2*cw) - pixel.y/(6*ch)var y = pixel.y/(3*ch)return round_hex(Vector2(x, y))
Для вертикальной ориентации

В коде это записывается так:

func pixel2hex(pixel):var x = pixel.x/(3*cw)var y = pixel.y/(2*ch) - pixel.x/(6*cw)return round_hex(Vector2(x, y))

Однако я буду пользоваться функцией affine_inverse у Transform2D, для того, что бы при изменении базисных векторов постоянно не менять функции преобразований, позже увидите зачем это надо. Вы скорее всего работаете в другой среде (и зря), поэтому вам придется писать обращение матрицы самостоятельно. Кто не знает как это делается, или забыл, может почитать тут, или переписать следующие функции в свой язык:

Функции
func invert_basis(basis:Transform2D): # обращение матрицыvar det = basis.x.x*basis.y.y - basis.y.x*basis.x.yvar idet = 1.0/det# Я не уверен что Transform2D передается по значению, по этому# копирую данные в новый объектvar res = basisres.y.y = basis.x.x*idetres.x.x = basis.y.y*idetres.x.y = -basis.x.y*idetres.y.x = -basis.y.x*idetreturn resfunc vec_mul_basis(vec:Vector2, basis:Transform2D): # умножение вектора на матрицуvar x = vec.x*basis.x.x + vec.y*basis.y.xvar y = vec.x*basis.x.y + vec.y*basis.y.yreturn Vector2(x, y)func pixel2hex(pixel):return round_hex(vec_mul_basis(pixel, invert_basis(hex_basis)))

Средствами Godot это можно записать всего в одну строчку:

func pixel2hex(pixel):return round_hex(hex_basis.affine_inverse().xform(pixel))

Тут .xform(Vector2) - это метод для умножения матрицы на переданный в него вектор, аналог vec_mul_basis из моего кода. Такой код работает для обеих ориентаций.

Если вы хотя бы бегло прочитали вышеприведенный код, то наверняка заметили функцию round_hex вместо типичных приведений к int. Дело в том, что полных координат у шестиугольника 3, и они обладают условием x + y + z = 0, а после округления каждой из них равенство может нарушиться. Поэтому необходимо задать координату с наибольшей ошибкой округления через две другие, тогда условие выполнится. Да, данный метод полностью слизан отсюда, однако зачем придумывать велосипед, если можно взять готовый? Так же тут используется именно round, а не приведение к int, ведь основание каждой ячейки находится в ее центре, а не в левом верхнем углу, как в случае с прямоугольными сетками:

func round_hex(hex:Vector2):var rx = round(hex.x)var ry = round(hex.y)var rz = round(-hex.x-hex.y) # z = -x-yvar x_diff = abs(hex.x-rx) # Ошибка округления xvar y_diff = abs(hex.y-ry) # Ошибка округления yvar z_diff = abs(-hex.x-hex.y-rz) # Ошибка округления zif x_diff > y_diff and x_diff > z_diff:rx = -ry-rz # Приведение под равенствоelif y_diff > z_diff:ry = -rx-rz # Приведение под равенствоreturn Vector2(rx, ry)

Работает все замечательно:

Вертикальная ориентация
Горизонтальная ориентация

Однако я надеюсь вы не думаете, что сетки, это вручную нарисованные текстуры. Я не самоубийца.

Рисование сеток

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

Для рисования сетки необходимо знать ее размеры. Размеры шестиугольной сетки будем задавать в координатах смещения, так просто понятнее. Тогда по горизонтали будет в два раза больше ячеек вспомогательной сетки, чем шестиугольников, ведь по горизонтали у шестиугольника 2 ячейки. Для нахождения вертикальных размеров заметим, что для перехода к нижнему шестиугольнику мы вниз проходим три вертикальных базиса, однако для крайнего шестиугольника соседа снизу нет, поэтому самая нижняя чать остается неучтенной, так что нужно прибавить единицу:

const hex_map_size = Vector2(7, 7) # размер сетки шестиугольниковvar grid_map_size:Vector2 # размер вспомогательной сетки...grid_map_size.x = hex_map_size.x*2grid_map_size.y = hex_map_size.y*3+1

Для вертикальных шестиугольников все аналогично, только формулы для вычисления ширины и высоты меняются местами:

...grid_map_size.x = hex_map_size.x*3+1grid_map_size.y = hex_map_size.y*2

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

Будем рисовать каждую составляющую по отдельности. Начнем с вертикальных линий. Можно заметить, что в каждом ряду линии рисуются с интервалом в 2 ячейки, а каждый четный по счету ряд начинается со второй, а не с первой ячейки. Также увидим то, что первый ряд начинается со со смещением в одну ячейку относительно верхей границы, а ряды разделяет одна ячейка. С учетом того, что длина штриха в две ячейки, между верхними концами отрезков находятся три ячейки. Тогда в цикле начинаем с единицы и идем до нижнего края карты с шагом 3, а во втором цикле начинаем со столбца, индекс которого обратен четности ряда, проще говоря 1-i%2, и идем до правого края карты, но на единицу больше, чтобы нарисовать таки крайние линии, с шагом в две ячейки. В кадой итерации второго цикла просто рисуем отрезок высотой две ячейки:

for i in range(1, grid_map_size.y, 3):for j in range(1-i%2, grid_map_size.x+1, 2):VisualServer.canvas_item_add_line(surf, grid_basis.x*j+grid_basis.y*i, grid_basis.x*j+grid_basis.y*(i+2), color, width, antialiasing)

Этот код будет рисовать только вертикальные линии. Теперь нужно нарисовать паттерн вершин. Есть всего два вида наклонных линий, от нижнего левого угла к верхнему правому (их я буду называть нижними диагоналями), или от верхнего левого к нижнему правому (их я буду называть верхними диагоналями), причем в одной строке они чередуются, а в следующей строке паттерн меняет четность. Четным я буду называть паттерн, начинающийся с нижней диагонали, нечетным - начинающийся в верхней диагонали.

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

Для рисования паттернов пробегаем каждую третью строку, начиная с нулевой, а в каждой строке пробегаемся по столбцам. Тогда для выбора нужной линии сравниваем четности строки и столбца, если они совпадают, то рисуем нижнюю диагональ, иначе верхнюю. Тут я считаю нужным показать, как задается каждый угол ячейки с координатами {j, i} , где j - столбец (как бы x), i - строка (как бы y). Размер ячейки увеличен только для демонстрации:

В коде этот алгоритм выглядит так:

# Drawing verticesfor i in range(0, grid_map_size.y, 3): # рисуем на каждой третьей строкеfor j in range(grid_map_size.x): # крайний столбец не захватываем, т.к. в коде прибавляется единицаif i%2 == j%2: # нижняя диагональCanvas.line(surf, grid_basis.x*j+grid_basis.y*(i+1), grid_basis.x*(j+1)+grid_basis.y*i, color, width, antialiasing)else: # верхняя диагональCanvas.line(surf, grid_basis.x*j+grid_basis.y*i-offset, grid_basis.x*(j+1)+grid_basis.y*(i+1), color, width, antialiasing)

Однако просто нарисовав на холсте сетку, получатся непонятки с координатами:

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

Однако и на этом не все. Если просто объеденить весь код выше в одну функцию, то при четных высотах она будет рисовать ненужные хвосты:

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

Соеденив все вместе, получим такую функцию:

func _draw_hor_rect_grid(surf:RID, color:Color, width=1.0, antialiasing=false):var offset = grid_basis.x+grid_basis.y*2# Drawing vertical linesfor i in range(1, grid_map_size.y, 3):for j in range(1-i%2, grid_map_size.x+1, 2):VisualServer.canvas_item_add_line(surf, grid_basis.x*j+grid_basis.y*i-offset, grid_basis.x*j+grid_basis.y*(i+2)-offset, color, width, antialiasing)# Drawing verticesfor i in range(0, grid_map_size.y, 3):for j in range(grid_map_size.x):if int(hex_map_size.y)%2 == 1 or not (i == grid_map_size.y-1 and (j == 0 or j == grid_map_size.x-1)):if i%2 == j%2:VisualServer.canvas_item_add_line(surf, grid_basis.x*j+grid_basis.y*(i+1)-offset, grid_basis.x*(j+1)+grid_basis.y*i-offset, color, width, antialiasing)else:VisualServer.canvas_item_add_line(surf, grid_basis.x*j+grid_basis.y*i-offset, grid_basis.x*(j+1)+grid_basis.y*(i+1)-offset, color, width, antialiasing)

При рисовании вспомогательной сетки кстати тоже используем смещение. На всякий случай оставлю тут и ее рисование, хотя это есть в моей статье про прямоугольные сетки:

func draw_auxiliary_grid(surf:RID, color:Color, width=1.0, antialiasing=false):var offset = grid_basis.x+grid_basis.y*2for i in grid_map_size.x+1:Canvas.line(surf, grid_basis.x*i-offset, grid_basis.x*i+grid_basis.y*grid_map_size.y-offset, color, width, antialiasing)for i in grid_map_size.y+1:Canvas.line(surf, grid_basis.y*i-offset, grid_basis.x*grid_map_size.x+grid_basis.y*i-offset, color, width, antialiasing)

И, как и обещал, функция для рисования вертикально-ориентированной сетки:

func _draw_vert_rect_grid(surf:RID, color:Color, width=1.0, antialiasing=false):var offset = grid_basis.x*2+grid_basis.y# Drawing horizontal linesfor i in range(1, grid_map_size.x, 3):for j in range(1-i%2, grid_map_size.y+1, 2):VisualServer.canvas_item_add_line(surf, grid_basis.x*i+grid_basis.y*j-offset, grid_basis.x*(i+2)+grid_basis.y*j-offset, color, width, antialiasing)# Drawing verticesfor i in range(0, grid_map_size.x, 3):for j in range(grid_map_size.y):if int(hex_map_size.x)%2 == 1 or not(i == grid_map_size.x-1 and (j == 0 or j == grid_map_size.y-1)):if j%2 == i%2:VisualServer.canvas_item_add_line(surf, grid_basis.x*(i+1)+grid_basis.y*j-offset, grid_basis.x*i+grid_basis.y*(j+1)-offset, color, width, antialiasing)else:VisualServer.canvas_item_add_line(surf, grid_basis.x*i+grid_basis.y*j-offset, grid_basis.x*(i+1)+grid_basis.y*(j+1)-offset, color, width, antialiasing)

Результат вполне неплох, нигде линии не рисуются дважды (сетка рисовалась немного прозрачной на черном фоне, а яркость линий везде оинакова):

Сетка вертикальных шестиугольников
Сетка горизонтальных шестиугольников

Однако рендерить такие сетки в реальном времени довольно затратно, тут рисуется множетсво отдельных отрезков, что сильно замедляет работу. Просто для примера, пустое черно окно у меня имеет fps около 950, а при рисовании белым цветом Color8(255, 255, 255, 200) шестиугольной сетки размера 10x10 и размером шестиугольнкиа 32 пикселя, fps примерно 260. Так что рисовать сетки процедурно резонно только на начальном этапе разработки, потом лучше отрендерить ее заранее и использовать как текстуру.

Рисование шестиугольной сетки шестиугольников

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

Рисование этой сетки лишь немного сложнее рисования обычной, хотя на первый взгляд мой код похож на код сатаны. Поробую объяснить что откуда взялось. Результат будет примерно таков:

Для начала нам конечно же нужны размеры сеток. Т.к. мы рисуем сетку в виде правильного шестиугольника, ее размер можно задать одним значением. В коде я использую тип Vector2 только для совмещения этой переменной с прямоугольнйо сеткой, при рисовании будет использоваться только X координата. Тогда для задания размеров вспомогательной сетки нужно найти диагональ большого шестиугольника. Для этого вспомним, что бОльшая диагональ шестиугольника в два раза больше его стороны. В нашем случае шестиугольник состоит из маленьких таких же. В таком случае центр будет учитываться два раза, поэтому вычтем единицу:

var hex_map_size = Vector2(5, <не имеет значения>)...var diagonal = hex_map_size.x*2-1

Размеры вспомогательной сетки задаются аналогично тому, что мы делали ранее. Для горизонтальной ориентации ширина будет в два раза больше диагонали, а высота в три раза и еще на единицу больше:

...grid_map_size.x = diagonal*2grid_map_size.y = diagonal*3+1

Для вертикальных значения меняются местами:

grid_map_size.x = diagonal*3+1grid_map_size.y = diagonal*2

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

Начнем с рисования вершин. Рисовать каждый слой по-отдельности не имеет сымсла, ведь фигура симметрична. Мы можем разделить всю вспомогательную сетку на четыре части и, нарисовав одну четверть, отобразить ее зеркально на все остальные. Сетка кстати всегда будет делиться ровно, и вот почему. По горизонтали понятно, ведь в формуле ширины мы удваиваем диагональ шестиугольной карты. А эта самая диагональ будет всегда нечетна, ведь мы от четного числа отнимаем единицу (hex_map_size.x*2-1). В формуле высоты вспомогательной сетки мы умножаем эту диагональ на 3, и результат получится тоже нечетным, а после прибавления единицы все выражение становится четным. Таким образом ширина и высота вспомогательной сетки всегда четны, и как следствие, ее можно всегда разделить на четыре одинаковые части:

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

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

Также вспомним, что каждый следующий паттерн рисуется со смещением в три ячейки от предыдущего, поэтому в цикле идем от нуля до половины высоты вспомогательной сетки с шагом в три, попутно вычисляя смещение для каждого ряда:

for i in range(0, grid_map_size.y/2, 3): # Drawing vertices  # тут i/3 потому что мы идем со смещением 3, а при расчетах нужен индекс  start = hex_map_size.x-1 - i/3  

Проходить по ширине будем также до середины вспомогательной сетки, начиная с выщитанного ранее смщения:

for i in range(0, grid_map_size.y/2, 3): # Drawing vertices  # тут i/3 потому что мы идем со смещением 3, у при расчетах нужен индекс паттерна  start = hex_map_size.x-1 - i/3    for j in range(start, grid_map_size.x/2):  pass # Пока ничего не делаем

Каждый паттерн при рисовании шестиугольной карты четный, а вот смещение чередует свою четность. Четность начального смещения напрямую зависит от четности размера карты - они противоположны. Двигаясь вниз по рядам паттерна, индекс ряда меняет четность, как и смещение для этого ряда. Если помните, для выбора диагонали при рисовании прямоугольной сетки мы сравнивали четность ряда и колонки. Тут же меняются обе четности, и при разных раземрах карты они будут то совпадать при начальных значениях, то нет.

Приведу пример. Мы рисуем нижнюю диагональ, если индексы ряда и колонки совпадают, иначе верхнюю. Поставим размер карты 5. Тогда начальное смещение будет четным, как и индекс первого ряда (i=0). Исходя из условия, рисуем нижнюю диагональ, как и должно быть. Однако поставив четный размер, скажем, 4, начальное смещение будет нечетным, а вот индекс первого ряда по прежнему четным. Тогда взглянув на условие компьютер выберет верхюю диагональ, а ведь нам все еще для начала нужна нижняя. Вот как это будет выглядеть:

Тут на самом деле всего лишь надо поменять четность паттерна, тогда все встанет на свои места. Получается, выбор условия рисвания нижней диагонали зависит от четности самого размера карты. Тут можно заметить, что разница четностей столбца и ряда в каждой первой диагонали ряда паттерна обратна четности размера карты. А при рисовании паттерна диагонали просто чередуются, как и чередуется четность столбца, и как следствие чередуется равенство разностей четностей ряда и столбца и четности размера карты. Поэтому для выбора диагонали используем равентво abs(i%2 - j%2) != parity, где parity - это остаток от деления размера карты на два. Если это условие верно, рисуем нижнюю диагональ, иначе верхнюю. Получим то что нужно, осталось отразить по красным линиям:

Код рисования четверти всего паттерна
func _draw_hor_hex_grid(surf:RID, color:Color):var parity = int(hex_map_size.x)%2var startfor i in range(0, grid_map_size.y/2, 3): # Drawing verticesstart = hex_map_size.x - i/3 - 1for j in range(start, grid_map_size.x/2):if abs(i%2 - j%2) != parity:# Down diagonalVisualServer.canvas_item_add_line(surf, grid_basis.x*j+grid_basis.y*(i+1), grid_basis.x*(j+1)+grid_basis.y*i, color)      else:# Top diagonalVisualServer.canvas_item_add_line(surf, grid_basis.x*(j)+grid_basis.y*(i), grid_basis.x*(j+1)+grid_basis.y*(i+1), color)

Для отражения точек рисования отнимаем от края вспомогательной сетки индекс точки, ничего сложного. А вот в коде это выглядит громоздко. Взгляните сами:

func _draw_hor_hex_grid(surf:RID, color:Color, width=1.0, antialiasing=false):var parity = int(hex_map_size.x)%2var startfor i in range(0, grid_map_size.y/2, 3): # Drawing verticesstart = hex_map_size.x - i/3 - 1for j in range(start, grid_map_size.x/2):if abs(i%2 - j%2) != parity:# Down diagonalVisualServer.canvas_item_add_line(surf, grid_basis.x*j+grid_basis.y*(i+1), grid_basis.x*(j+1)+grid_basis.y*i, color)VisualServer.canvas_item_add_line(surf, grid_basis.x*(grid_map_size.x-j)+grid_basis.y*(i+1), grid_basis.x*(grid_map_size.x-j-1)+grid_basis.y*i, color)VisualServer.canvas_item_add_line(surf, grid_basis.x*j+grid_basis.y*(grid_map_size.y-i-1), grid_basis.x*(j+1)+grid_basis.y*(grid_map_size.y-i), color)VisualServer.canvas_item_add_line(surf, grid_basis.x*(grid_map_size.x-j)+grid_basis.y*(grid_map_size.y-i-1), grid_basis.x*(grid_map_size.x-j-1)+grid_basis.y*(grid_map_size.y-i), color)else:# Top diagonalVisualServer.canvas_item_add_line(surf, grid_basis.x*(j)+grid_basis.y*(i), grid_basis.x*(j+1)+grid_basis.y*(i+1), color)VisualServer.canvas_item_add_line(surf, grid_basis.x*(grid_map_size.x-j)+grid_basis.y*(i), grid_basis.x*(grid_map_size.x-j-1)+grid_basis.y*(i+1), color)VisualServer.canvas_item_add_line(surf, grid_basis.x*(j)+grid_basis.y*(grid_map_size.y-i), grid_basis.x*(j+1)+grid_basis.y*(grid_map_size.y-i-1), color)VisualServer.ca

Но ничего страшного, ковид пережили - переживем и это. Зато получаем правильный паттерн для сеток с четными и нечетным размером:

Если вы что то поняли во всей этой мешанине четностей, то либо вы гений, либо у меня получилось что то объяснить. Однако дальше не легче, но радует то, что мы почти нарисовали сетку. Осталось добавить вертикальные линии - это будет финальным боссом в рассказе о рисовании сеток.

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

for i in range(1, grid_map_size.y, 3):for j in range(1-i%2, grid_map_size.x+1, 2):VisualServer.canvas_item_add_line(surf, grid_basis.x*j+grid_basis.y*i, grid_basis.x*j+grid_basis.y*(i+2), color, width, antialiasing)

Однако просто скопипастив его в нашу функцию, получим кривое рисование при четных размерах карты, ведь при них первый ряд должен иметь смещение в единицу, а при нечетных этого смещения быть не должно. Это вытекает из смещения первого шестиугольника в первом ряду, при четных значения оно нечетно поэтому и рисуем со смещением, и наоборот. Для выбора смещения сравним четности размера карты и ряда, если они отличаются, то рисуем без смещения, иначе со смещением. Пихать сюда условие не имеет смысла, ведь мы можем выбрать смещение через отличие четности карты и четности столбца конструкцией abs(parity-i%2). Просто напомню - parity это остаток от деления размера карты на два. Проверьте сами, при четных столбцах и нечетных размерах карты получается единица - то самое смещение. Запишем это выражение в смещение в цикле:

for i in range(1, grid_map_size.y, 3):for j in range(abs(parity-i%2), grid_map_size.x+1, 2):VisualServer.canvas_item_add_line(surf, grid_basis.x*j+grid_basis.y*i, grid_basis.x*j+grid_basis.y*(i+2), color, width, antialiasing)

Цель почти достигнута, осталось избавиться от лишних линий по углам:

Для этого при рисовании линий добавим некоторое условие, что мы хотим нарисовать линию в пределах каких-то границ. Для обозначения границ заметим, что с каждым рядом мы начинаем рисовать на ячейку ближе к левой границе, поэтому границу можно задать как смещение первого ряда минус индекс ряда:

...start = hex_map_size.x-1 - i/3

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

...start = (i-grid_map_size.y/2)/3

Это мы задали левые границы. Для правых просто отразим их в силу четности размеров вспомогательной сетки:

for i in range(1, grid_map_size.y, 3):if i <= grid_map_size.y/2:start = hex_map_size.x-1 - i/3else:start = (i-grid_map_size.y/2)/3for j in range(abs(parity-i%2), grid_map_size.x+1, 2):if j >= start and j <= grid_map_size.x-start: # избавляемся от лишних линийVisualServer.canvas_item_add_line(surf, grid_basis.x*j+grid_basis.y*i, grid_basis.x*j+grid_basis.y*(i+2), color, width, antialiasing)

Вот и все - финальный босс побежден. Осталось только добавить смещение для расположения сетки в начало координат, offset = grid_basis.x+grid_basis.y*2. Однако тут опять играет роль четность размера карты, так что когда она четна прибавляем к смещению горизонтальный базис ячейки.

Босса то убили, а вот лут забыли. С него мы получили рисование шестиугольных сеток шестиугольников:

Горизонтальная ориентация
func _draw_hor_hex_grid(surf:RID, color:Color, width=1.0, antialiasing=false):var parity = int(hex_map_size.x)%2var offset = grid_basis.x+grid_basis.y*2 + grid_basis.x*(1-parity)var startfor i in range(0, grid_map_size.y/2, 3): # Drawing verticesstart = hex_map_size.x - i/3 - 1for j in range(start, grid_map_size.x/2):if abs(i%2 - j%2) != parity:# Down diagonalVisualServer.canvas_item_add_line(surf, grid_basis.x*j+grid_basis.y*(i+1)-offset, grid_basis.x*(j+1)+grid_basis.y*i-offset, color)VisualServer.canvas_item_add_line(surf, grid_basis.x*(grid_map_size.x-j)+grid_basis.y*(i+1)-offset, grid_basis.x*(grid_map_size.x-j-1)+grid_basis.y*i-offset, color)VisualServer.canvas_item_add_line(surf, grid_basis.x*j+grid_basis.y*(grid_map_size.y-i-1)-offset, grid_basis.x*(j+1)+grid_basis.y*(grid_map_size.y-i)-offset, color)VisualServer.canvas_item_add_line(surf, grid_basis.x*(grid_map_size.x-j)+grid_basis.y*(grid_map_size.y-i-1)-offset, grid_basis.x*(grid_map_size.x-j-1)+grid_basis.y*(grid_map_size.y-i)-offset, color)else:# Top diagonalVisualServer.canvas_item_add_line(surf, grid_basis.x*(j)+grid_basis.y*(i)-offset, grid_basis.x*(j+1)+grid_basis.y*(i+1)-offset, color)VisualServer.canvas_item_add_line(surf, grid_basis.x*(grid_map_size.x-j)+grid_basis.y*(i)-offset, grid_basis.x*(grid_map_size.x-j-1)+grid_basis.y*(i+1)-offset, color)VisualServer.canvas_item_add_line(surf, grid_basis.x*(j)+grid_basis.y*(grid_map_size.y-i)-offset, grid_basis.x*(j+1)+grid_basis.y*(grid_map_size.y-i-1)-offset, color)VisualServer.canvas_item_add_line(surf, grid_basis.x*(grid_map_size.x-j)+grid_basis.y*(grid_map_size.y-i)-offset, grid_basis.x*(grid_map_size.x-j-1)+grid_basis.y*(grid_map_size.y-i-1)-offset, color)for i in range(1, grid_map_size.y, 3):if i <= grid_map_size.y/2:start = hex_map_size.x-1 - i/3else:start = (i-grid_map_size.y/2)/3for j in range(abs(parity-i%2), grid_map_size.x+1, 2):if j >= start and j <= grid_map_size.x-start:VisualServer.canvas_item_add_line(surf, grid_basis.x*j+grid_basis.y*i-offset, grid_basis.x*j+grid_basis.y*(i+2)-offset, color, width, antialiasing)
Вертикальная ориентация
func _draw_vert_hex_grid(surf:RID, color:Color, width=1.0, antialiasing=false):var parity = int(hex_map_size.x)%2var offset = grid_basis.x*2+grid_basis.y + (1-parity)*grid_basis.yvar startfor j in range(0, grid_map_size.x/2, 3): # Drawing verticesstart = hex_map_size.x - j/3 - 1for i in range(start, grid_map_size.y/2):if abs(i%2 - j%2) != parity:# Down diagonalVisualServer.canvas_item_add_line(surf, grid_basis.x*(j+1)+grid_basis.y*(i)-offset, grid_basis.x*(j)+grid_basis.y*(i+1)-offset, color)VisualServer.canvas_item_add_line(surf, grid_basis.x*(grid_map_size.x-j-1)+grid_basis.y*(i)-offset, grid_basis.x*(grid_map_size.x-j)+grid_basis.y*(i+1)-offset, color)VisualServer.canvas_item_add_line(surf, grid_basis.x*(j+1)+grid_basis.y*(grid_map_size.y-i)-offset, grid_basis.x*(j)+grid_basis.y*(grid_map_size.y-i-1)-offset, color)VisualServer.canvas_item_add_line(surf, grid_basis.x*(grid_map_size.x-j-1)+grid_basis.y*(grid_map_size.y-i)-offset, grid_basis.x*(grid_map_size.x-j)+grid_basis.y*(grid_map_size.y-i-1)-offset, color)else:# Top diagonalVisualServer.canvas_item_add_line(surf, grid_basis.x*(j)+grid_basis.y*(i)-offset, grid_basis.x*(j+1)+grid_basis.y*(i+1)-offset, color)VisualServer.canvas_item_add_line(surf, grid_basis.x*(grid_map_size.x-j)+grid_basis.y*(i)-offset, grid_basis.x*(grid_map_size.x-j-1)+grid_basis.y*(i+1)-offset, color)VisualServer.canvas_item_add_line(surf, grid_basis.x*(j)+grid_basis.y*(grid_map_size.y-i)-offset, grid_basis.x*(j+1)+grid_basis.y*(grid_map_size.y-i-1)-offset, color)VisualServer.canvas_item_add_line(surf, grid_basis.x*(grid_map_size.x-j)+grid_basis.y*(grid_map_size.y-i)-offset, grid_basis.x*(grid_map_size.x-j-1)+grid_basis.y*(grid_map_size.y-i-1)-offset, color)for i in range(1, grid_map_size.x, 3):if i <= grid_map_size.x/2:start = hex_map_size.x-1 - i/3else:start = (i-grid_map_size.x/2)/3for j in range(abs(parity-i%2), grid_map_size.y+1, 2):if j >= start and j <= grid_map_size.y-start:VisualServer.canvas_item_add_line(surf, grid_basis.x*i+grid_basis.y*j-offset, grid_basis.x*(i+2)+grid_basis.y*(j)-offset, color, width, antialiasing)

Пример:

Рисование шестиугольников

Тут на самом деле все просто, можете расслабиться. После этой жести с четностями ничего хуже уже не будет. Для рисования шестиугольников вспомним функции для получения вершин, которые я приводил выше и просто соеденим массив вершин шестиугольника линиями:

Функции для получения вершин, если лень мотать неаверх
func _get_vert_hex_vertices(hex):var pixel = hex2pixel(hex)return PoolVector2Array([pixel+2*grid_basis.x,pixel+grid_basis.x+grid_basis.y,pixel-grid_basis.x+grid_basis.y,pixel-2*grid_basis.x,pixel-grid_basis.x-grid_basis.y,pixel+grid_basis.x-grid_basis.y])func _get_hor_hex_vertices(hex):var pixel = hex2pixel(hex)return PoolVector2Array([pixel+grid_basis.x-grid_basis.y,pixel+grid_basis.x+grid_basis.y,pixel+2*grid_basis.y,pixel-grid_basis.x+grid_basis.y,pixel-grid_basis.x-grid_basis.y,pixel-2*grid_basis.y,])

И рисуем множество линий между точками, не забыв замкнуть цепь:

func _draw_hor_hex(hex, surf, color, width=1.0, antialiasing=false):var points = _get_hor_hex_vertices(hex)points.append(points[0]) # замыкаемVisualServer.canvas_item_add_polyline(surf, points, [color], width, antialiasing)func _draw_vert_hex(hex, surf, color, width=1.0, antialiasing=false):var points = _get_vert_hex_vertices(hex)points.append(points[0]) # замыкаемVisualServer.canvas_item_add_polyline(surf, points, [color], width, antialiasing)

Для заливки шестиугольника, по аналогии с прямоугольником, рисуем полигон:

func _fill_hor_hex(hex, surf, color, antialiasing=false):var points = _get_hor_hex_vertices(hex)VisualServer.canvas_item_add_polygon(surf, points, [color], [], RID(), RID(), antialiasing)func _fill_vert_hex(hex, surf, color, antialiasing=false):var points = _get_vert_hex_vertices(hex)VisualServer.canvas_item_add_polygon(surf, points, [color], [], RID(), RID(), antialiasing)

Выгялдит все это как то так:

Шестиугольные сетки в изометрии

Что может быть лучше сетки шестиугольников? Правильно, сетка шестиугольников в изометрии. Вы могли заметить, что ни в одной функции я не использовал числа - везде я работал с векторами. А это значит, что поменяв базис, автоматически поменяются функции - так и должно работать программирование. Помните, в части про преобразование координат я все вычисления автоматизировал? Так вот, наконец то нам это пригодится.

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

...const iso_scale = 2.0

Тогда для изменения вида делим y-координату каждого базиса вспомогательной сетки на это искажение:

# Вертикальная ориентацияgrid_basis.x = Vector2(long, 0)grid_basis.y = Vector2(0, short/iso_scale)# Горизонтальная ориентацияgrid_basis.x = Vector2(short, 0)grid_basis.y = Vector2(0, long/iso_scale)

И все, даже не нужно менять базисы шестиугольной сетки, ведь мы их задавали через базисы вспомогательной. Как же хороша эта ваша математика... Вот пример:

На самом деле, благодаря векторным преобразованиям мы можем делать с базисами вспомогательной сетки все что угодно, и все функции все равно будут работать. Поэтому давайте сделаем сетку не просто в изометрии, а еще и повернем ее на 45, также, как мы это делали в статье про прямоугольные сетки:

# для вертикальныхvar pw = int(long*cos(PI/4))var ph = int(short*cos(PI/4))grid_basis.x = Vector2(pw, pw/iso_scale)grid_basis.y = Vector2(-ph, ph/iso_scale)# для горизонтальныхvar pw = int(short*cos(PI/4))var ph = int(long*cos(PI/4))grid_basis.x = Vector2(pw, pw/iso_scale)grid_basis.y = Vector2(-ph, ph/iso_scale)

Напомню, что базисы шестиугольной сетки мы не трогаем, ведь они все так же заданы через базисы вспомогательной. Выглядит вся эта магия как то так:

Красиво, конечно, но игру на этом не сделать. Нужно также уметь что то на этих сетках делать.

Изометрические преобразования

С преобразованиями тоже мудрить ничего не стоит, ведь, напомню, шестиугольная сетка - это только обертка над обычной, поэтому все функции будут работать в штатном режиме. Единственное, что наверно чтоит рассмотреть, это получение ячейки вспомогательной сетки, левый верхний угол которой находится в центре шестиугольника.

Для этого вспомним то, как мы направляли базисы для различных ориентаций шестиугольников. Вот картинка из почти самого начала моего рассказа:

Посмотрев на нее, все сразу становится ясно. В случае горизонтальной ориентации при каждом перемещении по x на шестигольной сетке мы двигаемся на две ячейки вспомогательной, а при движении по y мы движемся на одну ячейку вправо и на три вниз. Для вертикальных применима эта же логика. По этим формулам написать функции можно проще простого:

# Для вертикальныхfunc get_center_cell(hex:Vector2):return Vector2(hex.x*3, hex.y*2+hex.x)# для горизонтальныхfunc get_center_cell(hex:Vector2):return Vector2(hex.x*2+hex.y, hex.y*3)

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

Расстояние на сетке

Часто бывает нужно найти расстояние между двумя шестиугольниками. Для этого заметим, что при перемещении в любой из соседних шестиугольников меняются сразу две координаты из трех, причем не единицу. Тогда сумма модулей всех трех координат либо меняется на два, либо не изменятся вовсе. В таком случае сеточное расстояние от начала координат можно найти как половину суммы модулей трех координат шетиугольника, а для нахождения расстояния между двумя шестиугольниками можно просто найти смещение одного относительно другого через разность. Получается вот такая небольшая функция:

func hex_distance(hex1:Vector2, hex2:Vector2):var dif = (hex2-hex1)return (abs(dif.x) + abs(dif.y) + abs(-dif.x-dif.y))/2 # z = -x-y

Сеточное направление

Начнем с самого простого - выбора направляющего шестиугольника. Кто не понимает о чем речь, заглянет в статью о прямоугольных тайловых мирах. Это один из немногих алгоритмов, где нам понадобится третья координата.

Для нахождения направляющей разделим сетку по трем осям и заметим, что в каждой части получившейся сетки одна из трех координат максимальна по модулю:

Теперь все, что нам нужно, это выбрать нужную часть и, сравнив модули не наибольших координат, выдать одно из осевых направлений, представленное в виде направляющей ячейки. Прямо на осях одна из координат всегда равна нулю, поэтому просто перехватим их как особые случаи. Для указания вдоль оси будем использовать знак разности конечного и начального шестиугольников. Также заметим, что на диагональных ячейках модули не наибольших координат равны, поэтому мы можем смещаться в любую из двух осей. Я выбрал смещение в сторону оси по часовой стрелке, в коде это выражается строгостью знаком сравнения. Для смещения против часовой стоит допустить равентсво. Так выглядит этот алгоритм в коде:

func direct_hex(hex1:Vector2, hex2:Vector2):var dx = hex2.x - hex1.xvar dy = hex2.y - hex1.yvar dz = -hex2.x-hex2.y + hex1.x+hex1.yif dx == 0: # Ось yreturn Vector2(0, sign(dy)) # Возвращаем ось yelif dy == 0: # Ось xreturn Vector2(sign(dx), 0) # Возвращаем ось xelif dz == 0: # Ось zreturn Vector2(sign(dx), sign(dy)) # Возвращаем ось zelse:if abs(dz) > abs(dx) and abs(dz) > abs(dy): # модуль разности по z оказался наибольшимif abs(dx) > abs(dy): # т.к. разность по x больше, значит мы отошли по x дальше, чем по y, значит выдаем ось xreturn Vector2(sign(dx), 0) # возвращаем ось xelse: # т.к. разность по y больше, значит мы отошли по y дальше, чем по x, значит выдаем ось yreturn Vector2(0, sign(dy)) # возвращаем ось y        elif abs(dy) > abs(dx): # модуль разности по y оказался наибольшимif abs(dz) > abs(dx): # по аналогииreturn Vector2(0, sign(dy)) # возвращаем y. Это связанно с представлением z-координаты через две другиеelse: # по аналогииreturn Vector2(sign(dx), sign(dy)) # возвращаем z        else: # модуль разности по x оказался наибольшимif abs(dy) > abs(dz): # по аналогииreturn Vector2(sign(dx), sign(dy)) # возвращаем zelse: # по аналогииreturn Vector2(sign(dx), 0) # возвращаем x

Опять же, тому кто читал мою статью про прямоугольные сетки, понять все это будет проще, ведь в ней я разбирал подобный алгоритм подробнее, однако, я надеюсь, что по комментариям принцип работы алгоритма понятен. Как мы видим, все рабоатет:

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

Поиск пути

Основной алгоритм поиска пути тот же, что и у обычной прямоугольной сетки, отличаются только соседи и проверки на нахождение точки внутри карты. Главный алгоритм - A*, его не трогаем совсем.

Соседи у шестиугольника выглядят как то так:

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

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

Начнем с прямоугольной карты. Для наглядности напомню как она выглядит:

Синим обозначены границы карты. Оси в такой сетке идут не параллельно сторонам прямоугольника, поэтому просто ограничить их нулем и границей карты не выйдет. Так сработает только для Y оси сетки, а горизонтальные границы зависят от смещения по Y. Перемещаясь вдоль оси Y, расстояние до левой границы в ячейках вспомогательной сетки увеличивается на единицу, значит на половину шестиугольника. Аналогично с правой границей, тоолько до нее расттояние уменьшается. При округлении левой границы используем floor, т.к. когда граница проходит ровно между шестиугольниками, мы вмыбираем тот, что внутри. По аналогии используем ceil для правой границы:

func _in_rect_grid_hor(hex):return hex.x >= -floor(hex.y/2) and hex.x < hex_map_size.x-ceil(hex.y/2) and hex.y < hex_map_size.y and hex.y >= 0

Для вертикальной ориентации логика точно такая же. Вот функция для нее:

func _in_rect_grid_vert(hex):return hex.x >= 0 and hex.x < hex_map_size.x and hex.y >= -floor(hex.x/2) and hex.y < hex_map_size.y-ceil(hex.x/2)

Теперь про шестиугольную карту. Ее вид:

Для простоты вычислений будем считать границы от центра карты. Просто потыкав и посмотрев на координаты я пришел к следующим формулам центров:

# для горизонтальныхfunc _get_hor_hex_map_center():return Vector2(int((hex_map_size.x-1)/2), hex_map_size.x-1)# для вертикальныхfunc _get_vert_hex_map_center():return Vector2(hex_map_size.x-1, int((hex_map_size.x-1)/2))

Каждому смещению по Y соответствует уменьшение длины ряда на единицу, так и будем задавать границы по x. В качестве размеров, ограничивающих карту, возьмем диагональ. Как ее вычислять я рассказывал ранее. Тогда по Y границами будут просто половины этих диагоналей, а по X одна из граней всегда параллельна оси Y, поэтому уменьшаться будет либо правая либо левая граница. Какая именно можно понять по вертикальной половине шестиугольника. Если мы находимся в верхней, то уменьшается левая граница (т.к. правая параллельна оси Y), если в нижней то уменьшается правая граница (т.к. левая паралельна оси Y). А если мы находимся прямо на горизонтальной диагонали то нам плевать, ведь на ней смещение по Y равно нулю.

Вот функции, реализующие данную логику для обеих ориентаций:

# для горизонтальныхfunc _in_hex_grid_hor(hex):    var center = _get_hor_hex_map_center()    var diag = int(hex_map_size.x*2 - 1)    hex -= center # Vector2 passed by value; getting hex regarding map center    if hex.y < 0:        return hex.x >= -diag/2+abs(hex.y) and hex.x <= diag/2 and hex.y >= -diag/2 and hex.y <= diag/2    else:        return hex.x >= -diag/2 and hex.x <= diag/2-abs(hex.y) and hex.y >= -diag/2 and hex.y <= diag/2# для вертикальныхfunc _in_hex_grid_vert(hex):    var center = _get_vert_hex_map_center()    var diag = int(hex_map_size.x*2 - 1)    hex -= center # Vector2 passed by value; getting hex regarding map center    if hex.x < 0:        return hex.y >= -diag/2+abs(hex.x) and hex.y <= diag/2 and hex.x >= -diag/2 and hex.x <= diag/2    else:        return hex.y >= -diag/2 and hex.y <= diag/2-abs(hex.x) and hex.x >= -diag/2 and hex.x <= diag/2

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

Отлично, теперь можно спокойно реализовывать алгоритм поиска пути:

Ищем путь истинный
class PriorityStack:var items:Arrayfunc _init():items = Array()func empty() -> bool:return items.size() == 0func put(item, priority:int) -> void:if empty():items.append([item, priority])elif priority <= items[0][1]:items.insert(0, [item, priority])elif priority > items[-1][1]:items.append([item, priority])else:for i in range(len(items)):if priority <= items[i][1]:items.insert(i, [item, priority])breakfunc take():return items.pop_front()[0]func in_map(hex):match grid_type:GridTypes.hex:if hex_type == HexTypes.hor:return _in_hex_grid_hor(hex)else: # Verticalreturn _in_hex_grid_vert(hex)GridTypes.rect:if hex_type == HexTypes.vert:return _in_rect_grid_vert(hex)else: # Hor orientationreturn _in_rect_grid_hor(hex)func can_stand(hex:Vector2, obsts:PoolVector2Array):return in_map(hex) and not (hex in obsts)func neighbors(hex_pos:Vector2, obsts:PoolVector2Array):var res:PoolVector2Array = []var _neighbors = PoolVector2Array([Vector2(-1, 0), Vector2(1, -1), Vector2(0, -1), Vector2(1, 0), Vector2(0, 1), Vector2(-1, 1)])for i in _neighbors:if can_stand(i+hex_pos, obsts):res.append(i+hex_pos)return resfunc find_path(start:Vector2, goal:Vector2, obsts:PoolVector2Array):var frontier = PriorityStack.new()frontier.put(start, 0)var came_from = {}var cost_so_far = {}came_from[start] = startcost_so_far[start] = 0var current:Vector2var new_cost:intif not can_stand(goal, obsts):return PoolVector2Array()while not frontier.empty():current = frontier.take()if current == goal:breakfor next in neighbors(current, obsts):new_cost = cost_so_far[current] + 1if not (next in cost_so_far) or new_cost < cost_so_far[next]:cost_so_far[next] = new_costfrontier.put(next, new_cost+hex_distance(goal, next))came_from[next] = currentif frontier.empty() and current != goal:return PoolVector2Array()current = goalvar path = PoolVector2Array([current])while current != start:current = came_from[current]path.append(current)path.invert()path.remove(0) # removes first positionreturn pathfunc hex_distance(hex1:Vector2, hex2:Vector2):var dif = (hex2-hex1)return (abs(dif.x) + abs(dif.y) + abs(-dif.x-dif.y))/2

Данный код я взял напрямую из своей реализации, так что некоторые моменты не освещены, за ненадобностью. Поглазеть на полный код сможете кликнув по ссылке в конце статьи. Так это выглядит:

Растеризация отрезка

Вот с растеризацией у шестиугольной сетки большие проблемы. Можно, конечно, придумать что нибудь с алгоритмом брезенхема для растеризации отрезков, однако я не думаю что вам нужно растеризовывать по 1000 отрезков за кадр, поэтому на оптимизацию позволим себе немного подзабить и воспользуемся линейной интерполяцией, которую кстати и предлагает автор популярной англоязычной статьи. Думаю почти все знают что такое линейная интерполяция, поэтому просто оставлю тут рализацию алгоритма:

Растеризуем нерастеризуемое
func rast_line(hex1, hex2):var N = hex_distance(hex1, hex2)if N == 0: return PoolVector2Array([hex1])var res = PoolVector2Array()for i in range(N+1):res.append(round_hex(lerp(hex1, hex2, i/N)))return res

Вот так это выглядит:

Пару слов в завершение

Вот и подошел столь запутанный рассказ к концу. Я постарался объяснить все максимально подробно и вставлял как можно больше картинок, надеюсь не зря. Никакую маленькую игру я делать не стал, ибо тут и так хаватаеn над чем подумать, поэтому просто оставлю код получившегося класса на почти 500 строк. Я подразумевал его использование через автозагрузку, как я собственно и делал во время работы над ним.

Если я забыл про что рассказать или упустил какие то важные моменты, или же просто ошибаюсь, напишете об этом в комментарии.

Я надеюсь эта статья позволит вам полностью реализовать давние мечты по созданию "убийы героев" или что она позволила просто интересно провести вечер. До скорого!

Подробнее..

Гексагональные тайловые миры

23.05.2021 22:14:34 | Автор: admin

Тайловость в играх очень распространенное явление, особенно в играх инди сегмента. Чаще всего используют квадратные тайлы в них проще всего задать необходимые данные, будь то карта уровня или инвентарь. Однако на квадратных и прямоугольных формах возможности подобной системы не ограничиваются.

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

Если вы не читали мою предыдущую статью про прямоугольные сетки, то рекомендую ознакомится, потому как я иногда буду опускать некоторые вещи, ссылаясь именно на нее. Дабы не изобретать велосипед, некоторые алгоритмы я взял с этой статьи на английском. Кто не хочет или не может в английский, есть ее перевод на хабре. Все демки и примеры сделаны на движке Godot Engine v 3.2.3 с использованием его встроенного языка.

Думаю в целом его синтаксис ясен, однако оставлю ссылки на некоторые функции:

Система координат

На протяжении всей статьи мы будем работать только с правильными шестиугольниками, у них все стороны равны. Работа с неправильными шестиугольниками лишена смысла в принципе. Если не брать всякие повороты и искажения, существует два вида шестиугольных сеток, вертикально и горизонтально ориентированных:

  • Такие я буду называть вертикальными (у ячейки есть явный вертикальный сосед):

  • А такие горизонтальными (у ячейки есть явный горизонтальный сосед):

Уже на этапе введения координат могут возникнуть проблемы. Дело в том, что в шестиугольной сетке невозможно ввести типичную декартову систему координат - всегда будет ось, вдоль которой у ячейки не будет явного соседа. На самом деле существует огромное количество систем координат для таких сеток. Более подробно о них рассказано в упомянутых вначале статьях. Я рассказывать про каждую не буду, все таки пост больше про использование сеток, а не их исследование.

Пожалуй первое, что приходит в голову, это таки впихнуть декартовы оси, однако для этого по одному из направлений координаты придется смещать:

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

Главная проблема этих координат состоит в выборе базисов. Для разных ячеек базис к следующей ячейке может отличаться от того, по которому мы к ней перешли. Если вы помните из статьи про прямоугольные тайлы, там мы обращали матрицу из базисных векторов и умножали на нее радиус-вектор пикселя, получая координаты ячейки. Здесь так не выйдет, провернув тоже самое мы просто получим квадратную сетку.

Вообще у сетки шестиугольников есть три ярко выраженных оси:

Что то напоминает, не находите? Тут три оси, прям как в пространстве. На самом деле гений тот человек - кто не просто увидел, что если взглянуть на куб под определенным углом, то получится шестиугольник, а еще и додумался использовать трехмерные координаты в двумерной сетке шестиугольников. Правда вот попробовав посчитать координаты на двумерной сетке, могут вскипеть мозги, ведь третья ось тут кажется лишней и ее использование будто только мешает. Для разрешения данной ситуации просто посмотрим откуда взялись кубы:

Для получения сетки шестиугольников надо взглянуть в изометрии на пирамидку, на каждом уменьшении высоты которой увеличивается количество кубиков в ряду на один. Включив фантазию, можно увидеть в каждом кубе шестиугольник. Включив внимательность, можно увидеть в левом нижнем углу координаты. Включив логику, можно увидеть, что сумма этих самых координат всегда равна некоторому числу, которое, кстати говоря, может быть любым, ведь мы сами решаем, откуда начнется система координат. В моем случае это 15, просто потому, что система отсчета в этом редакторе начинается с нижнего дальнего угла (в случае изометрического вида). Нам незачем таскать за собой лишнюю константу, поэтому возьмем ее за ноль. Таким образом, сумма трех координат всегда и везде равна нулю, поэтому третья нам попросту не нужна, ведь мы можем получить ее из двух других. Теперь и получается, что "мешающая" третья ось уходит. Наконец то мы можем спокойно направить базисы и у каждой ячейки всегда будет точный сосед, находящийся вдоль оси:

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

Преобразование координат

Пожалуй это то, на чем запарываются многие при попытке сделать нечто шестиугольное. Я слышал множество способов, порой они были очень забавные. Например, мне больше всего запомнился способ, где предлагалось сделать невидимую маску с такой же сеткой, где каждая ячейка окрашена в свой цвет, а каждому цвету соответствуют координаты. При смещении курсора смотрим в маску и по цвету определяем координаты. Сложно, с костылями, но работать может. А если мы захотим другой размер? Перерисовывать? Вы тут явно не за этим.

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

Оранжевые (маленькие) диагонали делятся пополам зелеными (большими), а зеленые оранжевыми, если последние провести из середины стороны. Уже получается, что мы можем разделить шестиугольник на 4 прямоугольника. Однако некоторые вершины в таком случае будут лежать где то между углами сетки, а ведь нам хотелось бы, чтобы все они попадали ровно в углы. На самом деле "где то", это ровно по серединке, поэтому разделим большие ячейки еще на пополам, тогда все вершины шестиугольника будут ложиться точно в углы прямоугольной сетки:

Желтую сетку в дальнейшем я буду называть вспомогательной. Для задания ее базисов будем использовать такие значения:

# Для горизонтальных шестиугольниковvar hex_size = 32var short = int(size*sqrt(3)/2) # 1/2 from short hex diagonalvar long = int(size/2) # 1/4 from long hex diagonal

Теперь мы можем выразить базисы шестиугольной сетки, используя базисы вспомогательной сетки:

Запишем все базисы в коде:

...# Transorm2D в godot - это матрица 3x2, где последняя строка указыает# смещение объекта, в дальнейшем она не будет использоваться совсем, # поэтому считайте это просто матрицей 2x2. Сделано это для удобства,# на объяснения никак не повлияет.# У нее есть два атрибута - x и y. Каждый из них это вектор. X - представляет# первый столбец матрицы 2x2 (крайняя строка не учитывается), Y - второй столбец.  var grid_basis = Transform2D() # Матрица базисов вспомогательной сеткиvar hex_basis = Transform2D() # Матрица базисов гексагональной сетки...  # Для вертикальной сеткиgrid_basis.x = Vector2(long, 0)grid_basis.y = Vector2(0, short)hex_basis.x = grid_basis.x*3 + grid_basis.yhex_basis.y = grid_basis.y*2# Для горизонтальной сеткиgrid_basis.x = Vector2(short, 0)grid_basis.y = Vector2(0, long)hex_basis.x = grid_basis.x*2hex_basis.y = grid_basis.x+grid_basis.y*3

Я пользуюсь именно встроенными средствами Godot для упрощения работы в целом. Все подобные места будут поясняться в общем виде, как бы это делалось без встроенных возможностей.

Шестиугольник в пиксель

Вот за что я люблю математику, так это за то, что если она работает, то работает везде. Так что для получения центра шестиугольника в пикселях из его координат на сетке надо просто умножить базисы на координаты:

func hex2pixel(hex):return hex.x*hex_basis.x + hex.y*hex_basis.y

Для получения каждой вершины просто прибавляем по нужным базисам:

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

Для вертикальных шестиугольников:

func _get_vert_hex_vertices(hex):var pixel = hex2pixel(hex)return PoolVector2Array([pixel+2*grid_basis.x,pixel+grid_basis.x+grid_basis.y,pixel-grid_basis.x+grid_basis.y,pixel-2*grid_basis.x,pixel-grid_basis.x-grid_basis.y,pixel+grid_basis.x-grid_basis.y])

Для горизонтальных шестиугольников:

func _get_hor_hex_vertices(hex):var pixel = hex2pixel(hex)return PoolVector2Array([pixel+grid_basis.x-grid_basis.y,pixel+grid_basis.x+grid_basis.y,pixel+2*grid_basis.y,pixel-grid_basis.x+grid_basis.y,pixel-grid_basis.x-grid_basis.y,pixel-2*grid_basis.y,])

Пиксель в шестиугольник

Наверно самая интригующая часть поста. На самом деле ничего нового почти не будет, ведь для получения вещественных координат ячейки все также обращаем матрицу и умножаем на нее радиус-вектор пикселя:

Для горизонтальной ориентации

В коде это записывается так:

func pixel2hex(pixel):var x = pixel.x/(2*cw) - pixel.y/(6*ch)var y = pixel.y/(3*ch)return round_hex(Vector2(x, y))
Для вертикальной ориентации

В коде это записывается так:

func pixel2hex(pixel):var x = pixel.x/(3*cw)var y = pixel.y/(2*ch) - pixel.x/(6*cw)return round_hex(Vector2(x, y))

Однако я буду пользоваться функцией affine_inverse у Transform2D, для того, что бы при изменении базисных векторов постоянно не менять функции преобразований, позже увидите зачем это надо. Вы скорее всего работаете в другой среде (и зря), поэтому вам придется писать обращение матрицы самостоятельно. Кто не знает как это делается, или забыл, может почитать тут, или переписать следующие функции в свой язык:

Функции
func invert_basis(basis:Transform2D): # обращение матрицыvar det = basis.x.x*basis.y.y - basis.y.x*basis.x.yvar idet = 1.0/det# Я не уверен что Transform2D передается по значению, по этому# копирую данные в новый объектvar res = basisres.y.y = basis.x.x*idetres.x.x = basis.y.y*idetres.x.y = -basis.x.y*idetres.y.x = -basis.y.x*idetreturn resfunc vec_mul_basis(vec:Vector2, basis:Transform2D): # умножение вектора на матрицуvar x = vec.x*basis.x.x + vec.y*basis.y.xvar y = vec.x*basis.x.y + vec.y*basis.y.yreturn Vector2(x, y)func pixel2hex(pixel):return round_hex(vec_mul_basis(pixel, invert_basis(hex_basis)))

Средствами Godot это можно записать всего в одну строчку:

func pixel2hex(pixel):return round_hex(hex_basis.affine_inverse().xform(pixel))

Тут .xform(Vector2) - это метод для умножения матрицы на переданный в него вектор, аналог vec_mul_basis из моего кода. Такой код работает для обеих ориентаций.

Если вы хотя бы бегло прочитали вышеприведенный код, то наверняка заметили функцию round_hex вместо типичных приведений к int. Дело в том, что полных координат у шестиугольника 3, и они обладают условием x + y + z = 0, а после округления каждой из них равенство может нарушиться. Поэтому необходимо задать координату с наибольшей ошибкой округления через две другие, тогда условие выполнится. Да, данный метод полностью слизан отсюда, однако зачем придумывать велосипед, если можно взять готовый? Так же тут используется именно round, а не приведение к int, ведь основание каждой ячейки находится в ее центре, а не в левом верхнем углу, как в случае с прямоугольными сетками:

func round_hex(hex:Vector2):var rx = round(hex.x)var ry = round(hex.y)var rz = round(-hex.x-hex.y) # z = -x-yvar x_diff = abs(hex.x-rx) # Ошибка округления xvar y_diff = abs(hex.y-ry) # Ошибка округления yvar z_diff = abs(-hex.x-hex.y-rz) # Ошибка округления zif x_diff > y_diff and x_diff > z_diff:rx = -ry-rz # Приведение под равенствоelif y_diff > z_diff:ry = -rx-rz # Приведение под равенствоreturn Vector2(rx, ry)

Работает все замечательно:

Вертикальная ориентация
Горизонтальная ориентация

Однако я надеюсь вы не думаете, что сетки, это вручную нарисованные текстуры. Я не самоубийца.

Рисование сеток

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

Для рисования сетки необходимо знать ее размеры. Размеры шестиугольной сетки будем задавать в координатах смещения, так просто понятнее. Тогда по горизонтали будет в два раза больше ячеек вспомогательной сетки, чем шестиугольников, ведь по горизонтали у шестиугольника 2 ячейки. Для нахождения вертикальных размеров заметим, что для перехода к нижнему шестиугольнику мы вниз проходим три вертикальных базиса, однако для крайнего шестиугольника соседа снизу нет, поэтому самая нижняя чать остается неучтенной, так что нужно прибавить единицу:

const hex_map_size = Vector2(7, 7) # размер сетки шестиугольниковvar grid_map_size:Vector2 # размер вспомогательной сетки...grid_map_size.x = hex_map_size.x*2grid_map_size.y = hex_map_size.y*3+1

Для вертикальных шестиугольников все аналогично, только формулы для вычисления ширины и высоты меняются местами:

...grid_map_size.x = hex_map_size.x*3+1grid_map_size.y = hex_map_size.y*2

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

Будем рисовать каждую составляющую по отдельности. Начнем с вертикальных линий. Можно заметить, что в каждом ряду линии рисуются с интервалом в 2 ячейки, а каждый четный по счету ряд начинается со второй, а не с первой ячейки. Также увидим то, что первый ряд начинается со со смещением в одну ячейку относительно верхей границы, а ряды разделяет одна ячейка. С учетом того, что длина штриха в две ячейки, между верхними концами отрезков находятся три ячейки. Тогда в цикле начинаем с единицы и идем до нижнего края карты с шагом 3, а во втором цикле начинаем со столбца, индекс которого обратен четности ряда, проще говоря 1-i%2, и идем до правого края карты, но на единицу больше, чтобы нарисовать таки крайние линии, с шагом в две ячейки. В кадой итерации второго цикла просто рисуем отрезок высотой две ячейки:

for i in range(1, grid_map_size.y, 3):for j in range(1-i%2, grid_map_size.x+1, 2):VisualServer.canvas_item_add_line(surf, grid_basis.x*j+grid_basis.y*i, grid_basis.x*j+grid_basis.y*(i+2), color, width, antialiasing)

Этот код будет рисовать только вертикальные линии. Теперь нужно нарисовать паттерн вершин. Есть всего два вида наклонных линий, от нижнего левого угла к верхнему правому (их я буду называть нижними диагоналями), или от верхнего левого к нижнему правому (их я буду называть верхними диагоналями), причем в одной строке они чередуются, а в следующей строке паттерн меняет четность. Четным я буду называть паттерн, начинающийся с нижней диагонали, нечетным - начинающийся в верхней диагонали.

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

Для рисования паттернов пробегаем каждую третью строку, начиная с нулевой, а в каждой строке пробегаемся по столбцам. Тогда для выбора нужной линии сравниваем четности строки и столбца, если они совпадают, то рисуем нижнюю диагональ, иначе верхнюю. Тут я считаю нужным показать, как задается каждый угол ячейки с координатами {j, i} , где j - столбец (как бы x), i - строка (как бы y). Размер ячейки увеличен только для демонстрации:

В коде этот алгоритм выглядит так:

# Drawing verticesfor i in range(0, grid_map_size.y, 3): # рисуем на каждой третьей строкеfor j in range(grid_map_size.x): # крайний столбец не захватываем, т.к. в коде прибавляется единицаif i%2 == j%2: # нижняя диагональCanvas.line(surf, grid_basis.x*j+grid_basis.y*(i+1), grid_basis.x*(j+1)+grid_basis.y*i, color, width, antialiasing)else: # верхняя диагональCanvas.line(surf, grid_basis.x*j+grid_basis.y*i-offset, grid_basis.x*(j+1)+grid_basis.y*(i+1), color, width, antialiasing)

Однако просто нарисовав на холсте сетку, получатся непонятки с координатами:

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

Однако и на этом не все. Если просто объеденить весь код выше в одну функцию, то при четных высотах она будет рисовать ненужные хвосты:

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

Соеденив все вместе, получим такую функцию:

func _draw_hor_rect_grid(surf:RID, color:Color, width=1.0, antialiasing=false):var offset = grid_basis.x+grid_basis.y*2# Drawing vertical linesfor i in range(1, grid_map_size.y, 3):for j in range(1-i%2, grid_map_size.x+1, 2):VisualServer.canvas_item_add_line(surf, grid_basis.x*j+grid_basis.y*i-offset, grid_basis.x*j+grid_basis.y*(i+2)-offset, color, width, antialiasing)# Drawing verticesfor i in range(0, grid_map_size.y, 3):for j in range(grid_map_size.x):if int(hex_map_size.y)%2 == 1 or not (i == grid_map_size.y-1 and (j == 0 or j == grid_map_size.x-1)):if i%2 == j%2:VisualServer.canvas_item_add_line(surf, grid_basis.x*j+grid_basis.y*(i+1)-offset, grid_basis.x*(j+1)+grid_basis.y*i-offset, color, width, antialiasing)else:VisualServer.canvas_item_add_line(surf, grid_basis.x*j+grid_basis.y*i-offset, grid_basis.x*(j+1)+grid_basis.y*(i+1)-offset, color, width, antialiasing)

При рисовании вспомогательной сетки кстати тоже используем смещение. На всякий случай оставлю тут и ее рисование, хотя это есть в моей статье про прямоугольные сетки:

func draw_auxiliary_grid(surf:RID, color:Color, width=1.0, antialiasing=false):var offset = grid_basis.x+grid_basis.y*2for i in grid_map_size.x+1:Canvas.line(surf, grid_basis.x*i-offset, grid_basis.x*i+grid_basis.y*grid_map_size.y-offset, color, width, antialiasing)for i in grid_map_size.y+1:Canvas.line(surf, grid_basis.y*i-offset, grid_basis.x*grid_map_size.x+grid_basis.y*i-offset, color, width, antialiasing)

И, как и обещал, функция для рисования вертикально-ориентированной сетки:

func _draw_vert_rect_grid(surf:RID, color:Color, width=1.0, antialiasing=false):var offset = grid_basis.x*2+grid_basis.y# Drawing horizontal linesfor i in range(1, grid_map_size.x, 3):for j in range(1-i%2, grid_map_size.y+1, 2):VisualServer.canvas_item_add_line(surf, grid_basis.x*i+grid_basis.y*j-offset, grid_basis.x*(i+2)+grid_basis.y*j-offset, color, width, antialiasing)# Drawing verticesfor i in range(0, grid_map_size.x, 3):for j in range(grid_map_size.y):if int(hex_map_size.x)%2 == 1 or not(i == grid_map_size.x-1 and (j == 0 or j == grid_map_size.y-1)):if j%2 == i%2:VisualServer.canvas_item_add_line(surf, grid_basis.x*(i+1)+grid_basis.y*j-offset, grid_basis.x*i+grid_basis.y*(j+1)-offset, color, width, antialiasing)else:VisualServer.canvas_item_add_line(surf, grid_basis.x*i+grid_basis.y*j-offset, grid_basis.x*(i+1)+grid_basis.y*(j+1)-offset, color, width, antialiasing)

Результат вполне неплох, нигде линии не рисуются дважды (сетка рисовалась немного прозрачной на черном фоне, а яркость линий везде одинакова):

Сетка вертикальных шестиугольников
Сетка горизонтальных шестиугольников

Однако рендерить такие сетки в реальном времени довольно затратно, тут рисуется множетсво отдельных отрезков, что сильно замедляет работу. Просто для примера, пустое черно окно у меня имеет fps около 950, а при рисовании белым цветом Color8(255, 255, 255, 200) шестиугольной сетки размера 10x10 и размером шестиугольнкиа 32 пикселя, fps примерно 260. Так что рисовать сетки процедурно резонно только на начальном этапе разработки, потом лучше отрендерить ее заранее и использовать как текстуру.

Рисование шестиугольной сетки шестиугольников

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

Рисование этой сетки лишь немного сложнее рисования обычной, хотя на первый взгляд мой код похож на код сатаны. Поробую объяснить что откуда взялось. Результат будет примерно таков:

Для начала нам конечно же нужны размеры сеток. Т.к. мы рисуем сетку в виде правильного шестиугольника, ее размер можно задать одним значением. В коде я использую тип Vector2 только для совмещения этой переменной с прямоугольнйо сеткой, при рисовании будет использоваться только X координата. Тогда для задания размеров вспомогательной сетки нужно найти диагональ большого шестиугольника. Для этого вспомним, что бОльшая диагональ шестиугольника в два раза больше его стороны. В нашем случае шестиугольник состоит из маленьких таких же. В таком случае центр будет учитываться два раза, поэтому вычтем единицу:

var hex_map_size = Vector2(5, <не имеет значения>)...var diagonal = hex_map_size.x*2-1

Размеры вспомогательной сетки задаются аналогично тому, что мы делали ранее. Для горизонтальной ориентации ширина будет в два раза больше диагонали, а высота в три раза и еще на единицу больше:

...grid_map_size.x = diagonal*2grid_map_size.y = diagonal*3+1

Для вертикальных значения меняются местами:

grid_map_size.x = diagonal*3+1grid_map_size.y = diagonal*2

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

Начнем с рисования вершин. Рисовать каждый слой по-отдельности не имеет смысла, ведь фигура симметрична. Мы можем разделить всю вспомогательную сетку на четыре части и, нарисовав одну четверть, отобразить ее зеркально на все остальные. Сетка кстати всегда будет делиться ровно, и вот почему. По горизонтали понятно, ведь в формуле ширины мы удваиваем диагональ шестиугольной карты. А эта самая диагональ будет всегда нечетна, ведь мы от четного числа отнимаем единицу (hex_map_size.x*2-1). В формуле высоты вспомогательной сетки мы умножаем эту диагональ на 3, и результат получится тоже нечетным, а после прибавления единицы все выражение становится четным. Таким образом ширина и высота вспомогательной сетки всегда четны, и как следствие, ее можно всегда разделить на четыре одинаковые части:

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

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

Также вспомним, что каждый следующий паттерн рисуется со смещением в три ячейки от предыдущего, поэтому в цикле идем от нуля до половины высоты вспомогательной сетки с шагом в три, попутно вычисляя смещение для каждого ряда:

for i in range(0, grid_map_size.y/2, 3): # Drawing vertices  # тут i/3 потому что мы идем со смещением 3, а при расчетах нужен индекс  start = hex_map_size.x-1 - i/3  

Проходить по ширине будем также до середины вспомогательной сетки, начиная с выщитанного ранее смщения:

for i in range(0, grid_map_size.y/2, 3): # Drawing vertices  # тут i/3 потому что мы идем со смещением 3, у при расчетах нужен индекс паттерна  start = hex_map_size.x-1 - i/3    for j in range(start, grid_map_size.x/2):  pass # Пока ничего не делаем

Каждый паттерн при рисовании шестиугольной карты четный, а вот смещение чередует свою четность. Четность начального смещения напрямую зависит от четности размера карты - они противоположны. Двигаясь вниз по рядам паттерна, индекс ряда меняет четность, как и смещение для этого ряда. Если помните, для выбора диагонали при рисовании прямоугольной сетки мы сравнивали четность ряда и колонки. Тут же меняются обе четности, и при разных раземрах карты они будут то совпадать при начальных значениях, то нет.

Приведу пример. Мы рисуем нижнюю диагональ, если индексы ряда и колонки совпадают, иначе верхнюю. Поставим размер карты 5. Тогда начальное смещение будет четным, как и индекс первого ряда (i=0). Исходя из условия, рисуем нижнюю диагональ, как и должно быть. Однако поставив четный размер, скажем, 4, начальное смещение будет нечетным, а вот индекс первого ряда по прежнему четным. Тогда взглянув на условие компьютер выберет верхюю диагональ, а ведь нам все еще для начала нужна нижняя. Вот как это будет выглядеть:

Тут на самом деле всего лишь надо поменять четность паттерна, тогда все встанет на свои места. Получается, выбор условия рисвания нижней диагонали зависит от четности самого размера карты. Тут можно заметить, что разница четностей столбца и ряда в каждой первой диагонали ряда паттерна обратна четности размера карты. А при рисовании паттерна диагонали просто чередуются, как и чередуется четность столбца, и как следствие чередуется равенство разностей четностей ряда и столбца и четности размера карты. Поэтому для выбора диагонали используем равентво abs(i%2 - j%2) != parity, где parity - это остаток от деления размера карты на два. Если это условие верно, рисуем нижнюю диагональ, иначе верхнюю. Получим то что нужно, осталось отразить по красным линиям:

Код рисования четверти всего паттерна
func _draw_hor_hex_grid(surf:RID, color:Color):var parity = int(hex_map_size.x)%2var startfor i in range(0, grid_map_size.y/2, 3): # Drawing verticesstart = hex_map_size.x - i/3 - 1for j in range(start, grid_map_size.x/2):if abs(i%2 - j%2) != parity:# Down diagonalVisualServer.canvas_item_add_line(surf, grid_basis.x*j+grid_basis.y*(i+1), grid_basis.x*(j+1)+grid_basis.y*i, color)      else:# Top diagonalVisualServer.canvas_item_add_line(surf, grid_basis.x*(j)+grid_basis.y*(i), grid_basis.x*(j+1)+grid_basis.y*(i+1), color)

Для отражения точек рисования отнимаем от края вспомогательной сетки индекс точки, ничего сложного. А вот в коде это выглядит громоздко. Взгляните сами:

func _draw_hor_hex_grid(surf:RID, color:Color, width=1.0, antialiasing=false):var parity = int(hex_map_size.x)%2var startfor i in range(0, grid_map_size.y/2, 3): # Drawing verticesstart = hex_map_size.x - i/3 - 1for j in range(start, grid_map_size.x/2):if abs(i%2 - j%2) != parity:# Down diagonalVisualServer.canvas_item_add_line(surf, grid_basis.x*j+grid_basis.y*(i+1), grid_basis.x*(j+1)+grid_basis.y*i, color)VisualServer.canvas_item_add_line(surf, grid_basis.x*(grid_map_size.x-j)+grid_basis.y*(i+1), grid_basis.x*(grid_map_size.x-j-1)+grid_basis.y*i, color)VisualServer.canvas_item_add_line(surf, grid_basis.x*j+grid_basis.y*(grid_map_size.y-i-1), grid_basis.x*(j+1)+grid_basis.y*(grid_map_size.y-i), color)VisualServer.canvas_item_add_line(surf, grid_basis.x*(grid_map_size.x-j)+grid_basis.y*(grid_map_size.y-i-1), grid_basis.x*(grid_map_size.x-j-1)+grid_basis.y*(grid_map_size.y-i), color)else:# Top diagonalVisualServer.canvas_item_add_line(surf, grid_basis.x*(j)+grid_basis.y*(i), grid_basis.x*(j+1)+grid_basis.y*(i+1), color)VisualServer.canvas_item_add_line(surf, grid_basis.x*(grid_map_size.x-j)+grid_basis.y*(i), grid_basis.x*(grid_map_size.x-j-1)+grid_basis.y*(i+1), color)VisualServer.canvas_item_add_line(surf, grid_basis.x*(j)+grid_basis.y*(grid_map_size.y-i), grid_basis.x*(j+1)+grid_basis.y*(grid_map_size.y-i-1), color)VisualServer.ca

Но ничего страшного, ковид пережили - переживем и это. Зато получаем правильный паттерн для сеток с четными и нечетным размером:

Если вы что то поняли во всей этой мешанине четностей, то либо вы гений, либо у меня получилось что то объяснить. Однако дальше не легче, но радует то, что мы почти нарисовали сетку. Осталось добавить вертикальные линии - это будет финальным боссом в рассказе о рисовании сеток.

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

for i in range(1, grid_map_size.y, 3):for j in range(1-i%2, grid_map_size.x+1, 2):VisualServer.canvas_item_add_line(surf, grid_basis.x*j+grid_basis.y*i, grid_basis.x*j+grid_basis.y*(i+2), color, width, antialiasing)

Однако просто скопипастив его в нашу функцию, получим кривое рисование при четных размерах карты, ведь при них первый ряд должен иметь смещение в единицу, а при нечетных этого смещения быть не должно. Это вытекает из смещения первого шестиугольника в первом ряду, при четных значения оно нечетно поэтому и рисуем со смещением, и наоборот. Для выбора смещения сравним четности размера карты и ряда, если они отличаются, то рисуем без смещения, иначе со смещением. Пихать сюда условие не имеет смысла, ведь мы можем выбрать смещение через отличие четности карты и четности столбца конструкцией abs(parity-i%2). Просто напомню - parity это остаток от деления размера карты на два. Проверьте сами, при четных столбцах и нечетных размерах карты получается единица - то самое смещение. Запишем это выражение в смещение в цикле:

for i in range(1, grid_map_size.y, 3):for j in range(abs(parity-i%2), grid_map_size.x+1, 2):VisualServer.canvas_item_add_line(surf, grid_basis.x*j+grid_basis.y*i, grid_basis.x*j+grid_basis.y*(i+2), color, width, antialiasing)

Цель почти достигнута, осталось избавиться от лишних линий по углам:

Для этого при рисовании линий добавим некоторое условие, что мы хотим нарисовать линию в пределах каких-то границ. Для обозначения границ заметим, что с каждым рядом мы начинаем рисовать на ячейку ближе к левой границе, поэтому границу можно задать как смещение первого ряда минус индекс ряда:

...start = hex_map_size.x-1 - i/3

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

...start = (i-grid_map_size.y/2)/3

Это мы задали левые границы. Для правых просто отразим левые в силу четности размеров вспомогательной сетки:

for i in range(1, grid_map_size.y, 3):if i <= grid_map_size.y/2:start = hex_map_size.x-1 - i/3else:start = (i-grid_map_size.y/2)/3for j in range(abs(parity-i%2), grid_map_size.x+1, 2):if j >= start and j <= grid_map_size.x-start: # избавляемся от лишних линийVisualServer.canvas_item_add_line(surf, grid_basis.x*j+grid_basis.y*i, grid_basis.x*j+grid_basis.y*(i+2), color, width, antialiasing)

Вот и все - финальный босс побежден. Осталось только добавить смещение для расположения сетки в начало координат, offset = grid_basis.x+grid_basis.y*2. Однако тут опять играет роль четность размера карты, так что когда она четна прибавляем к смещению горизонтальный базис ячейки.

Босса то убили, а вот лут забыли. С него мы получили рисование шестиугольных сеток шестиугольников:

Горизонтальная ориентация
func _draw_hor_hex_grid(surf:RID, color:Color, width=1.0, antialiasing=false):var parity = int(hex_map_size.x)%2var offset = grid_basis.x+grid_basis.y*2 + grid_basis.x*(1-parity)var startfor i in range(0, grid_map_size.y/2, 3): # Drawing verticesstart = hex_map_size.x - i/3 - 1for j in range(start, grid_map_size.x/2):if abs(i%2 - j%2) != parity:# Down diagonalVisualServer.canvas_item_add_line(surf, grid_basis.x*j+grid_basis.y*(i+1)-offset, grid_basis.x*(j+1)+grid_basis.y*i-offset, color)VisualServer.canvas_item_add_line(surf, grid_basis.x*(grid_map_size.x-j)+grid_basis.y*(i+1)-offset, grid_basis.x*(grid_map_size.x-j-1)+grid_basis.y*i-offset, color)VisualServer.canvas_item_add_line(surf, grid_basis.x*j+grid_basis.y*(grid_map_size.y-i-1)-offset, grid_basis.x*(j+1)+grid_basis.y*(grid_map_size.y-i)-offset, color)VisualServer.canvas_item_add_line(surf, grid_basis.x*(grid_map_size.x-j)+grid_basis.y*(grid_map_size.y-i-1)-offset, grid_basis.x*(grid_map_size.x-j-1)+grid_basis.y*(grid_map_size.y-i)-offset, color)else:# Top diagonalVisualServer.canvas_item_add_line(surf, grid_basis.x*(j)+grid_basis.y*(i)-offset, grid_basis.x*(j+1)+grid_basis.y*(i+1)-offset, color)VisualServer.canvas_item_add_line(surf, grid_basis.x*(grid_map_size.x-j)+grid_basis.y*(i)-offset, grid_basis.x*(grid_map_size.x-j-1)+grid_basis.y*(i+1)-offset, color)VisualServer.canvas_item_add_line(surf, grid_basis.x*(j)+grid_basis.y*(grid_map_size.y-i)-offset, grid_basis.x*(j+1)+grid_basis.y*(grid_map_size.y-i-1)-offset, color)VisualServer.canvas_item_add_line(surf, grid_basis.x*(grid_map_size.x-j)+grid_basis.y*(grid_map_size.y-i)-offset, grid_basis.x*(grid_map_size.x-j-1)+grid_basis.y*(grid_map_size.y-i-1)-offset, color)for i in range(1, grid_map_size.y, 3):if i <= grid_map_size.y/2:start = hex_map_size.x-1 - i/3else:start = (i-grid_map_size.y/2)/3for j in range(abs(parity-i%2), grid_map_size.x+1, 2):if j >= start and j <= grid_map_size.x-start:VisualServer.canvas_item_add_line(surf, grid_basis.x*j+grid_basis.y*i-offset, grid_basis.x*j+grid_basis.y*(i+2)-offset, color, width, antialiasing)

Пример:

Вертикальная ориентация
func _draw_vert_hex_grid(surf:RID, color:Color, width=1.0, antialiasing=false):var parity = int(hex_map_size.x)%2var offset = grid_basis.x*2+grid_basis.y + (1-parity)*grid_basis.yvar startfor j in range(0, grid_map_size.x/2, 3): # Drawing verticesstart = hex_map_size.x - j/3 - 1for i in range(start, grid_map_size.y/2):if abs(i%2 - j%2) != parity:# Down diagonalVisualServer.canvas_item_add_line(surf, grid_basis.x*(j+1)+grid_basis.y*(i)-offset, grid_basis.x*(j)+grid_basis.y*(i+1)-offset, color)VisualServer.canvas_item_add_line(surf, grid_basis.x*(grid_map_size.x-j-1)+grid_basis.y*(i)-offset, grid_basis.x*(grid_map_size.x-j)+grid_basis.y*(i+1)-offset, color)VisualServer.canvas_item_add_line(surf, grid_basis.x*(j+1)+grid_basis.y*(grid_map_size.y-i)-offset, grid_basis.x*(j)+grid_basis.y*(grid_map_size.y-i-1)-offset, color)VisualServer.canvas_item_add_line(surf, grid_basis.x*(grid_map_size.x-j-1)+grid_basis.y*(grid_map_size.y-i)-offset, grid_basis.x*(grid_map_size.x-j)+grid_basis.y*(grid_map_size.y-i-1)-offset, color)else:# Top diagonalVisualServer.canvas_item_add_line(surf, grid_basis.x*(j)+grid_basis.y*(i)-offset, grid_basis.x*(j+1)+grid_basis.y*(i+1)-offset, color)VisualServer.canvas_item_add_line(surf, grid_basis.x*(grid_map_size.x-j)+grid_basis.y*(i)-offset, grid_basis.x*(grid_map_size.x-j-1)+grid_basis.y*(i+1)-offset, color)VisualServer.canvas_item_add_line(surf, grid_basis.x*(j)+grid_basis.y*(grid_map_size.y-i)-offset, grid_basis.x*(j+1)+grid_basis.y*(grid_map_size.y-i-1)-offset, color)VisualServer.canvas_item_add_line(surf, grid_basis.x*(grid_map_size.x-j)+grid_basis.y*(grid_map_size.y-i)-offset, grid_basis.x*(grid_map_size.x-j-1)+grid_basis.y*(grid_map_size.y-i-1)-offset, color)for i in range(1, grid_map_size.x, 3):if i <= grid_map_size.x/2:start = hex_map_size.x-1 - i/3else:start = (i-grid_map_size.x/2)/3for j in range(abs(parity-i%2), grid_map_size.y+1, 2):if j >= start and j <= grid_map_size.y-start:VisualServer.canvas_item_add_line(surf, grid_basis.x*i+grid_basis.y*j-offset, grid_basis.x*(i+2)+grid_basis.y*(j)-offset, color, width, antialiasing)

Пример:

Рисование шестиугольников

Тут на самом деле все просто, можете расслабиться. После этой жести с четностями ничего хуже уже не будет. Для рисования шестиугольников вспомним функции для получения вершин, которые я приводил выше и просто соеденим массив вершин шестиугольника линиями:

Функции для получения вершин, если лень мотать неаверх
func _get_vert_hex_vertices(hex):var pixel = hex2pixel(hex)return PoolVector2Array([pixel+2*grid_basis.x,pixel+grid_basis.x+grid_basis.y,pixel-grid_basis.x+grid_basis.y,pixel-2*grid_basis.x,pixel-grid_basis.x-grid_basis.y,pixel+grid_basis.x-grid_basis.y])func _get_hor_hex_vertices(hex):var pixel = hex2pixel(hex)return PoolVector2Array([pixel+grid_basis.x-grid_basis.y,pixel+grid_basis.x+grid_basis.y,pixel+2*grid_basis.y,pixel-grid_basis.x+grid_basis.y,pixel-grid_basis.x-grid_basis.y,pixel-2*grid_basis.y,])

И рисуем множество линий между точками, не забыв замкнуть цепь:

func _draw_hor_hex(hex, surf, color, width=1.0, antialiasing=false):var points = _get_hor_hex_vertices(hex)points.append(points[0]) # замыкаемVisualServer.canvas_item_add_polyline(surf, points, [color], width, antialiasing)func _draw_vert_hex(hex, surf, color, width=1.0, antialiasing=false):var points = _get_vert_hex_vertices(hex)points.append(points[0]) # замыкаемVisualServer.canvas_item_add_polyline(surf, points, [color], width, antialiasing)

Для заливки шестиугольника, по аналогии с прямоугольником, рисуем полигон:

func _fill_hor_hex(hex, surf, color, antialiasing=false):var points = _get_hor_hex_vertices(hex)VisualServer.canvas_item_add_polygon(surf, points, [color], [], RID(), RID(), antialiasing)func _fill_vert_hex(hex, surf, color, antialiasing=false):var points = _get_vert_hex_vertices(hex)VisualServer.canvas_item_add_polygon(surf, points, [color], [], RID(), RID(), antialiasing)

Выгялдит все это как то так:

Шестиугольные сетки в изометрии

Что может быть лучше сетки шестиугольников? Правильно, сетка шестиугольников в изометрии. Вы могли заметить, что ни в одной функции я не использовал числа - везде я работал с векторами. А это значит, что поменяв базис, автоматически поменяются функции - так и должно работать программирование. Помните, в части про преобразование координат я все вычисления автоматизировал? Так вот, наконец то нам это пригодится.

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

...const iso_scale = 2.0

Тогда для изменения вида делим y-координату каждого базиса вспомогательной сетки на это искажение:

# Вертикальная ориентацияgrid_basis.x = Vector2(long, 0)grid_basis.y = Vector2(0, short/iso_scale)# Горизонтальная ориентацияgrid_basis.x = Vector2(short, 0)grid_basis.y = Vector2(0, long/iso_scale)

И все, даже не нужно менять базисы шестиугольной сетки, ведь мы их задавали через базисы вспомогательной. Как же хороша эта ваша математика... Вот пример:

На самом деле, благодаря векторным преобразованиям мы можем делать с базисами вспомогательной сетки все что угодно, и все функции все равно будут работать. Поэтому давайте сделаем сетку не просто в изометрии, а еще и повернем ее на 45, также, как мы это делали в статье про прямоугольные сетки:

# для вертикальныхvar pw = int(long*cos(PI/4))var ph = int(short*cos(PI/4))grid_basis.x = Vector2(pw, pw/iso_scale)grid_basis.y = Vector2(-ph, ph/iso_scale)# для горизонтальныхvar pw = int(short*cos(PI/4))var ph = int(long*cos(PI/4))grid_basis.x = Vector2(pw, pw/iso_scale)grid_basis.y = Vector2(-ph, ph/iso_scale)

Напомню, что базисы шестиугольной сетки мы не трогаем, ведь они все так же заданы через базисы вспомогательной. Выглядит вся эта магия как то так:

Красиво, конечно, но игру на этом не сделать. Нужно также уметь что то на этих сетках делать.

Изометрические преобразования

С преобразованиями тоже мудрить ничего не стоит, ведь, напомню, шестиугольная сетка - это только обертка над обычной, поэтому все функции будут работать в штатном режиме. Единственное, что наверно стоит рассмотреть, это получение ячейки вспомогательной сетки, левый верхний угол которой находится в центре шестиугольника.

Для этого вспомним то, как мы направляли базисы для различных ориентаций шестиугольников. Вот картинка из почти самого начала моего рассказа:

Посмотрев на нее, все сразу становится ясно. В случае горизонтальной ориентации при каждом перемещении по x на шестигольной сетке мы двигаемся на две ячейки вспомогательной, а при движении по y мы движемся на одну ячейку вправо и на три вниз. Для вертикальных применима эта же логика. По этим формулам написать функции можно проще простого:

# Для вертикальныхfunc get_center_cell(hex:Vector2):return Vector2(hex.x*3, hex.y*2+hex.x)# для горизонтальныхfunc get_center_cell(hex:Vector2):return Vector2(hex.x*2+hex.y, hex.y*3)

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

Расстояние на сетке

Часто бывает нужно найти расстояние между двумя шестиугольниками. Для этого заметим, что при перемещении в любой из соседних шестиугольников меняются сразу две координаты из трех, причем не единицу. Тогда сумма модулей всех трех координат либо меняется на два, либо не изменятся вовсе. В таком случае сеточное расстояние от начала координат можно найти как половину суммы модулей трех координат шетиугольника, а для нахождения расстояния между двумя шестиугольниками можно просто найти смещение одного относительно другого через разность. Получается вот такая небольшая функция:

func hex_distance(hex1:Vector2, hex2:Vector2):var dif = (hex2-hex1)return (abs(dif.x) + abs(dif.y) + abs(-dif.x-dif.y))/2 # z = -x-y

Сеточное направление

Начнем с самого простого - выбора направляющего шестиугольника. Кто не понимает о чем речь, заглянет в статью о прямоугольных тайловых мирах. Это один из немногих алгоритмов, где нам понадобится третья координата.

Для нахождения направляющей разделим сетку по трем осям и заметим, что в каждой части получившейся сетки одна из трех координат максимальна по модулю:

Теперь все, что нам нужно, это выбрать нужную часть и, сравнив модули не наибольших координат, выдать одно из осевых направлений, представленное в виде направляющей ячейки. Прямо на осях одна из координат всегда равна нулю, поэтому просто перехватим их как особые случаи. Для указания вдоль оси будем использовать знак разности конечного и начального шестиугольников. Также заметим, что на диагональных ячейках модули не наибольших координат равны, поэтому мы можем смещаться в любую из двух осей. Я выбрал смещение в сторону оси по часовой стрелке, в коде это выражается строгостью знаком сравнения. Для смещения против часовой стоит допустить равентсво. Так выглядит этот алгоритм в коде:

func direct_hex(hex1:Vector2, hex2:Vector2):var dx = hex2.x - hex1.xvar dy = hex2.y - hex1.yvar dz = -hex2.x-hex2.y + hex1.x+hex1.yif dx == 0: # Ось yreturn Vector2(0, sign(dy)) # Возвращаем ось yelif dy == 0: # Ось xreturn Vector2(sign(dx), 0) # Возвращаем ось xelif dz == 0: # Ось zreturn Vector2(sign(dx), sign(dy)) # Возвращаем ось zelse:if abs(dz) > abs(dx) and abs(dz) > abs(dy): # модуль разности по z оказался наибольшимif abs(dx) > abs(dy): # т.к. разность по x больше, значит мы отошли по x дальше, чем по y, значит выдаем ось xreturn Vector2(sign(dx), 0) # возвращаем ось xelse: # т.к. разность по y больше, значит мы отошли по y дальше, чем по x, значит выдаем ось yreturn Vector2(0, sign(dy)) # возвращаем ось y        elif abs(dy) > abs(dx): # модуль разности по y оказался наибольшимif abs(dz) > abs(dx): # по аналогииreturn Vector2(0, sign(dy)) # возвращаем y. Это связанно с представлением z-координаты через две другиеelse: # по аналогииreturn Vector2(sign(dx), sign(dy)) # возвращаем z        else: # модуль разности по x оказался наибольшимif abs(dy) > abs(dz): # по аналогииreturn Vector2(sign(dx), sign(dy)) # возвращаем zelse: # по аналогииreturn Vector2(sign(dx), 0) # возвращаем x

Опять же, тому кто читал мою статью про прямоугольные сетки, понять все это будет проще, ведь в ней я разбирал подобный алгоритм подробнее, однако, я надеюсь, что по комментариям принцип работы алгоритма понятен. Как мы видим, все рабоатет:

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

Поиск пути

Основной алгоритм поиска пути тот же, что и у обычной прямоугольной сетки, отличаются только соседи и проверки на нахождение точки внутри карты. Главный алгоритм - A*, его не трогаем совсем.

Соседи у шестиугольника выглядят как то так:

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

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

Начнем с прямоугольной карты. Для наглядности напомню как она выглядит:

Синим обозначены границы карты. Оси в такой сетке идут не параллельно сторонам прямоугольника, поэтому просто ограничить их нулем и границей карты не выйдет. Так сработает только для Y оси сетки, а горизонтальные границы зависят от смещения по Y. Перемещаясь вдоль оси Y, расстояние до левой границы в ячейках вспомогательной сетки увеличивается на единицу, значит на половину шестиугольника. Аналогично с правой границей, тоолько до нее расттояние уменьшается. При округлении левой границы используем floor, т.к. когда граница проходит ровно между шестиугольниками, мы вмыбираем тот, что внутри. По аналогии используем ceil для правой границы:

func _in_rect_grid_hor(hex):return hex.x >= -floor(hex.y/2) and hex.x < hex_map_size.x-ceil(hex.y/2) and hex.y < hex_map_size.y and hex.y >= 0

Для вертикальной ориентации логика точно такая же. Вот функция для нее:

func _in_rect_grid_vert(hex):return hex.x >= 0 and hex.x < hex_map_size.x and hex.y >= -floor(hex.x/2) and hex.y < hex_map_size.y-ceil(hex.x/2)

Теперь про шестиугольную карту. Ее вид:

Для простоты вычислений будем считать границы от центра карты. Просто потыкав и посмотрев на координаты я пришел к следующим формулам центров:

# для горизонтальныхfunc _get_hor_hex_map_center():return Vector2(int((hex_map_size.x-1)/2), hex_map_size.x-1)# для вертикальныхfunc _get_vert_hex_map_center():return Vector2(hex_map_size.x-1, int((hex_map_size.x-1)/2))

Каждому смещению по Y соответствует уменьшение длины ряда на единицу, так и будем задавать границы по x. В качестве размеров, ограничивающих карту, возьмем диагональ. Как ее вычислять я рассказывал ранее. Тогда по Y границами будут просто половины этих диагоналей, а по X одна из граней всегда параллельна оси Y, поэтому уменьшаться будет либо правая либо левая граница. Какая именно можно понять по вертикальной половине шестиугольника. Если мы находимся в верхней, то уменьшается левая граница (т.к. правая параллельна оси Y), если в нижней то уменьшается правая граница (т.к. левая паралельна оси Y). А если мы находимся прямо на горизонтальной диагонали то нам плевать, ведь на ней смещение по Y равно нулю.

Вот функции, реализующие данную логику для обеих ориентаций:

# для горизонтальныхfunc _in_hex_grid_hor(hex):    var center = _get_hor_hex_map_center()    var diag = int(hex_map_size.x*2 - 1)    hex -= center # Vector2 passed by value; getting hex regarding map center    if hex.y < 0:        return hex.x >= -diag/2+abs(hex.y) and hex.x <= diag/2 and hex.y >= -diag/2 and hex.y <= diag/2    else:        return hex.x >= -diag/2 and hex.x <= diag/2-abs(hex.y) and hex.y >= -diag/2 and hex.y <= diag/2# для вертикальныхfunc _in_hex_grid_vert(hex):    var center = _get_vert_hex_map_center()    var diag = int(hex_map_size.x*2 - 1)    hex -= center # Vector2 passed by value; getting hex regarding map center    if hex.x < 0:        return hex.y >= -diag/2+abs(hex.x) and hex.y <= diag/2 and hex.x >= -diag/2 and hex.x <= diag/2    else:        return hex.y >= -diag/2 and hex.y <= diag/2-abs(hex.x) and hex.x >= -diag/2 and hex.x <= diag/2

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

Отлично, теперь можно спокойно реализовывать алгоритм поиска пути:

Ищем путь истинный
class PriorityStack:var items:Arrayfunc _init():items = Array()func empty() -> bool:return items.size() == 0func put(item, priority:int) -> void:if empty():items.append([item, priority])elif priority <= items[0][1]:items.insert(0, [item, priority])elif priority > items[-1][1]:items.append([item, priority])else:for i in range(len(items)):if priority <= items[i][1]:items.insert(i, [item, priority])breakfunc take():return items.pop_front()[0]func in_map(hex):match grid_type:GridTypes.hex:if hex_type == HexTypes.hor:return _in_hex_grid_hor(hex)else: # Verticalreturn _in_hex_grid_vert(hex)GridTypes.rect:if hex_type == HexTypes.vert:return _in_rect_grid_vert(hex)else: # Hor orientationreturn _in_rect_grid_hor(hex)func can_stand(hex:Vector2, obsts:PoolVector2Array):return in_map(hex) and not (hex in obsts)func neighbors(hex_pos:Vector2, obsts:PoolVector2Array):var res:PoolVector2Array = []var _neighbors = PoolVector2Array([Vector2(-1, 0), Vector2(1, -1), Vector2(0, -1), Vector2(1, 0), Vector2(0, 1), Vector2(-1, 1)])for i in _neighbors:if can_stand(i+hex_pos, obsts):res.append(i+hex_pos)return resfunc find_path(start:Vector2, goal:Vector2, obsts:PoolVector2Array):var frontier = PriorityStack.new()frontier.put(start, 0)var came_from = {}var cost_so_far = {}came_from[start] = startcost_so_far[start] = 0var current:Vector2var new_cost:intif not can_stand(goal, obsts):return PoolVector2Array()while not frontier.empty():current = frontier.take()if current == goal:breakfor next in neighbors(current, obsts):new_cost = cost_so_far[current] + 1if not (next in cost_so_far) or new_cost < cost_so_far[next]:cost_so_far[next] = new_costfrontier.put(next, new_cost+hex_distance(goal, next))came_from[next] = currentif frontier.empty() and current != goal:return PoolVector2Array()current = goalvar path = PoolVector2Array([current])while current != start:current = came_from[current]path.append(current)path.invert()path.remove(0) # removes first positionreturn pathfunc hex_distance(hex1:Vector2, hex2:Vector2):var dif = (hex2-hex1)return (abs(dif.x) + abs(dif.y) + abs(-dif.x-dif.y))/2

Данный код я взял напрямую из своей реализации, так что некоторые моменты не освещены, за ненадобностью. Поглазеть на полный код сможете кликнув по ссылке в конце статьи. Так это выглядит:

Растеризация отрезка

Вот с растеризацией у шестиугольной сетки большие проблемы. Можно, конечно, придумать что нибудь с алгоритмом брезенхема для растеризации отрезков, однако я не думаю что вам нужно растеризовывать по 1000 отрезков за кадр, поэтому на оптимизацию позволим себе немного подзабить и воспользуемся линейной интерполяцией, которую, кстати, и предлагает автор популярной англоязычной статьи. Думаю почти все знают что такое линейная интерполяция, поэтому просто оставлю тут рализацию алгоритма:

Растеризуем нерастеризуемое
func rast_line(hex1, hex2):var N = hex_distance(hex1, hex2)if N == 0: return PoolVector2Array([hex1])var res = PoolVector2Array()for i in range(N+1):res.append(round_hex(lerp(hex1, hex2, i/N)))return res

Вот так это выглядит:

Пару слов в завершение

Вот и подошел столь запутанный рассказ к концу. Я постарался объяснить все максимально подробно и вставлял как можно больше картинок, надеюсь не зря. Никакую маленькую игру я делать не стал, ибо тут и так хаватаеn над чем подумать, поэтому просто оставлю код получившегося класса на почти 500 строк. Я подразумеваю его использование через автозагрузку, как собственно я и делал во время работы над ним.

Если я забыл про что рассказать или упустил какие то важные моменты, или же просто ошибаюсь, напишете об этом в комментарии.

Я надеюсь эта статья позволит вам полностью реализовать давние мечты по созданию "убийы героев" или что она позволила просто интересно провести вечер. До скорого!

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru