Как я создал игру-головоломку на чистом CSS PlatoBlockchain Data Intelligence. Вертикальный поиск. Ай.

Как я создал игру-головоломку на чистом CSS

Недавно я открыл для себя радость создания игр только на CSS. Всегда поразительно, как HTML и CSS могут обрабатывать логику целой онлайн-игры, поэтому я должен был попробовать! Такие игры обычно основаны на старом «взломе флажка», где мы комбинируем состояние «проверено/не проверено» ввода HTML с :checked псевдокласс в CSS. Мы можем сотворить много волшебства с помощью одной этой комбинации!

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

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

Хотите поиграть до того, как мы начнем?

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

Круто, да? Я знаю, что это не лучшая игра-головоломка, которую вы когда-либо видели™, но она также неплоха для того, что использует только CSS и несколько строк HTML. Вы можете легко настроить размер сетки, изменить количество ячеек, чтобы контролировать уровень сложности, и использовать любое изображение, которое вы хотите!

Мы вместе переделаем это демо, а в конце добавим в него немного блеска для прикола.

Функция перетаскивания

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

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

img {
  transform: translate(200%);
  transition: 999s 999s; /* very slow move on mouseout */
}
.box:hover img {
  transform: translate(0);
  transition: 0s; /* instant move on hover */
}

Указание только transition-delay достаточно, но использование больших значений как задержки, так и длительности снижает вероятность того, что игрок когда-либо увидит, что изображение движется назад. Если вы ждете 999s + 999s — что составляет примерно 30 минут — затем вы увидите, как изображение движется. Но ты не будешь, верно? Я имею в виду, что никто не будет делать так много времени между ходами, если только он не выйдет из игры. Итак, я считаю это хорошим трюком для переключения между двумя состояниями.

Вы заметили, что наведение курсора на изображение также вызывает изменения? Это потому, что изображение является частью элемента box, что не очень хорошо для нас. Мы можем исправить это, добавив pointer-events: none к изображению, но мы не сможем перетащить его позже.

Это означает, что мы должны ввести еще один элемент внутри .box:

Это лишнее div (мы используем класс .a) будет занимать ту же площадь, что и изображение (благодаря CSS Grid и grid-area: 1 / 1) и будет элементом, вызывающим эффект наведения. И здесь в игру вступает селектор родственного элемента:

.a {
  grid-area: 1 / 1;
}
img {
  grid-area: 1 / 1;
  transform: translate(200%);
  transition: 999s 999s;
}
.a:hover + img {
  transform: translate(0);
  transition: 0s;
}

Парящий на .a element перемещает изображение, и, поскольку оно занимает все пространство внутри блока, вместо этого мы как будто наводим курсор на блок! Наведение изображения больше не проблема!

Давайте перетащим наше изображение внутрь поля и посмотрим на результат:

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

Хм, ваша симуляция не идеальна, потому что мы также можем навести курсор на коробку и получить тот же эффект.

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

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

Разберем код:

.a {
  width: 0%;
  transition: 0s .2s; /* add a small delay to make sure we catch the hover effect */
}
.box:active .a { /* on :active increase the width */
  width: 100%;
  transition: 0s; /* instant change */
}
img {
  transform: translate(200%);
  transition: 999s 999s;
}
.a:hover + img {
  transform: translate(0);
  transition: 0s;
}

Щелчок по изображению запускает :active псевдокласс, который делает .a элемент на всю ширину (изначально он равен 0). Активное состояние останется активный пока мы не выпустим изображение. Если мы отпустим изображение внутри коробки, .a элемент возвращается к width: 0, но мы вызовем эффект наведения до того, как это произойдет, и изображение упадет внутрь коробки! Если выпустить его за пределы коробки, ничего не произойдет.

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

Мы можем исправить это, играя с pointer-events. Это позволяет нам отключить любое взаимодействие с .box при сохранении взаимодействия с дочерними элементами.

.box {
  pointer-events: none;
}
.box * {
  pointer-events: initial;
}

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

Создание сетки головоломки

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

Вот наша сетка, для удобства написанная на мопсе:

- let n = 4; /* number of columns/rows */
- let image = "https://picsum.photos/id/1015/800/800";

g(style=`--i:url(${image})`)
  - for(let i = 0; i < n*n; i++)
    z
      a
      b(draggable="true") 

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

<g style="--i: url(https://picsum.photos/id/1015/800/800)">
 <z>
   <a></a>
   <b draggable="true"></b>
 </z>
 <z>
   <a></a>
   <b draggable="true"></b>
 </z>
 <z>
   <a></a>
   <b draggable="true"></b>
 </z>
  <!-- etc. -->
</g>

Бьюсь об заклад, вам интересно, что случилось с этими тегами. Ни один из этих элементов не имеет особого значения — я просто считаю, что код гораздо проще писать, используя <z> чем куча <div class="z"> или что-то еще.

Вот как я их наметил:

  • <g> наш грид-контейнер, который содержит N*N <z> элементов.
  • <z> представляет наши элементы сетки. Она играет роль .box элемент, который мы видели в предыдущем разделе.
  • <a> запускает эффект наведения.
  • <b> представляет собой часть нашего изображения. Мы применяем draggable атрибут на нем, потому что его нельзя перетащить по умолчанию.

Хорошо, давайте зарегистрируем наш грид-контейнер на <g>. Это в Sass вместо CSS:

$n : 4; /* number of columns/rows */

g {
  --s: 300px; /* size of the puzzle */

  display: grid;
  max-width: var(--s);
  border: 1px solid;
  margin: auto;
  grid-template-columns: repeat($n, 1fr);
}

На самом деле мы собираемся сделать наши дочерние элементы сетки — <z> элементы — также сетки и имеют оба <a> и <b> в пределах одной области сетки:

z {
  aspect-ratio: 1;
  display: grid;
  outline: 1px dashed;
}
a {
  grid-area: 1/1;
}
b {
  grid-area: 1/1;
}

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

b {
  background: var(--i) 0/var(--s) var(--s);
}

@for $i from 1 to ($n * $n + 1) {
  $r: (random(180));
  $x: (($i - 1)%$n);
  $y: floor(($i - 0.001) / $n);
  z:nth-of-type(#{$i}) b{
    background-position: ($x / ($n - 1)) * 100% ($y / ($n - 1)) * 100%;
    transform: 
      translate((($n - 1) / 2 - $x) * 100%, (($n - 1)/2 - $y) * 100%) 
      rotate($r * 1deg) 
      translate((random(100)*1% + ($n - 1) * 100%)) 
      rotate((random(20) - 10 - $r) * 1deg)
   }
}

Вы могли заметить, что я использую Sass. random() функция. Вот как мы получаем рандомизированные позиции для частей головоломки. Помните, что мы будем запрещать это положение при наведении курсора на <a> элемент после перетаскивания соответствующего <b> элемент внутри ячейки сетки.

z a:hover ~ b {
  transform: translate(0);
  transition: 0s;
}

В том же цикле я также определяю конфигурацию фона для каждой части головоломки. Логично, что все они будут использовать одно и то же изображение в качестве фона, а его размер должен быть равен размеру всей сетки (определяемой параметром --s переменная). Используя тот же background-image и немного математики, мы обновляем background-position показать только часть изображения.

Вот и все! Наша игра-головоломка только на CSS технически готова!

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

Формы кусочков головоломки

Вот наша новая игра-головоломка. Та же функциональность, но с более реалистичными формами!

Это иллюстрация фигур на сетке:

Как я создал игру-головоломку на чистом CSS

Если вы присмотритесь, то заметите, что у нас есть девять различных форм кусочков головоломки: четыре угла, четыре краяи один для всего остального.

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

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

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

z  /* 0 */

z:first-child  /* 1 */

z:nth-child(-n + 4):not(:first-child) /* 2 */

z:nth-child(5) /* 3 */

z:nth-child(5n + 1):not(:first-child):not(:nth-last-child(5)) /* 4 */

z:nth-last-child(5)  /* 5 */

z:nth-child(5n):not(:nth-child(5)):not(:last-child) /* 6 */

z:last-child /* 7 */

z:nth-last-child(-n + 4):not(:last-child) /* 8 */

Вот рисунок, показывающий, как это соотносится с нашей сеткой:

Как я создал игру-головоломку на чистом CSS PlatoBlockchain Data Intelligence. Вертикальный поиск. Ай.
Как я создал игру-головоломку на чистом CSS

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

Для кусочков головоломки в центре сетки, 0:

mask: 
  radial-gradient(var(--r) at calc(50% - var(--r) / 2) 0, #0000 98%, #000) var(--r)  
    0 / 100% var(--r) no-repeat,
  radial-gradient(var(--r) at calc(100% - var(--r)) calc(50% - var(--r) / 2), #0000 98%, #000) 
    var(--r) 50% / 100% calc(100% - 2 * var(--r)) no-repeat,
  radial-gradient(var(--r) at var(--r) calc(50% - var(--r) / 2), #000 98%, #0000),
  radial-gradient(var(--r) at calc(50% + var(--r) / 2) calc(100% - var(--r)), #000 98%, #0000);

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

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

Как я создал игру-головоломку на чистом CSS PlatoBlockchain Data Intelligence. Вертикальный поиск. Ай.
Как я создал игру-головоломку на чистом CSS

Форма кусочков головоломки в центре (отмечены 0 на иллюстрации) сложнее всего сделать, так как он использует четыре градиента и имеет четыре кривизны. Все остальные части жонглируют меньшим количеством градиентов.

Например, кусочки головоломки вдоль верхнего края головоломки (отмечены 2 на иллюстрации) использует три градиента вместо четырех:

mask: 
  radial-gradient(var(--r) at calc(100% - var(--r)) calc(50% + var(--r) / 2), #0000 98%, #000) var(--r) calc(-1 * var(--r)) no-repeat,
  radial-gradient(var(--r) at var(--r) calc(50% - var(--r) / 2), #000 98%, #0000),
  radial-gradient(var(--r) at calc(50% + var(--r) / 2) calc(100% - var(--r)), #000 98%, #0000);

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

Помимо создания фигур, вы также обнаружите, что я увеличиваю ширину и/или высоту элементов, как показано ниже:

height: calc(100% + var(--r));
width: calc(100% + var(--r));

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

Как я создал игру-головоломку на чистом CSS PlatoBlockchain Data Intelligence. Вертикальный поиск. Ай.
Как я создал игру-головоломку на чистом CSS

Финальная демонстрация

Вот полная демонстрация снова. Если вы сравните его с первой версией, вы увидите ту же структуру кода для создания сетки и функции перетаскивания, а также код для создания фигур.

Возможные улучшения

На этом статья заканчивается, но мы могли бы продолжать улучшать нашу головоломку, добавляя еще больше возможностей! Как насчет таймера? Или, может быть, какое-то поздравление, когда игрок закончит головоломку?

Я могу рассмотреть все эти функции в будущей версии, поэтому следите за моим репозиторием на GitHub.

Подведение итогов

И, CSS не язык программирования, они говорят. Ха!

Я не пытаюсь этим разжечь #HotDrama. Я говорю это, потому что мы сделали несколько очень сложных логических вещей и рассмотрели множество свойств и методов CSS. Мы играли с CSS Grid, переходами, маскированием, градиентами, селекторами и свойствами фона. Не говоря уже о нескольких хитростях Sass, которые мы использовали, чтобы упростить настройку нашего кода.

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

Является ли CSS языком программирования или нет, это не меняет того факта, что мы всегда учимся, создавая и создавая инновационные вещи.

Отметка времени:

Больше от CSS хитрости