Як я створив чисту гру-головоломку CSS PlatoBlockchain Data Intelligence. Вертикальний пошук. Ai.

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

Нещодавно я відчув радість створювати ігри лише з CSS. Завжди вражає, як HTML і CSS здатні обробляти логіку цілої онлайн-гри, тож мені довелося спробувати! Такі ігри зазвичай покладаються на старий Checkbox Hack, де ми поєднуємо позначений/не позначений стан введення HTML із :checked псевдоклас в CSS. Ми можемо зробити багато чарівництва за допомогою цієї однієї комбінації!

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

На додаток до головоломки, яку ми вивчатимемо в цій статті, я створив колекція чистих ігор CSS, більшість з них без Checkbox Hack. (Вони також доступні на 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 хвилин — тоді ви побачите рух зображення. Але ти не будеш, правда? Я маю на увазі, що ніхто не збирається займати стільки часу між ходами, якщо не піде з гри. Отже, я вважаю це хорошим прийомом для перемикання між двома станами.

Ви помітили, що наведення курсора на зображення також викликає зміни? Це тому, що зображення є частиною елемента коробки, що не добре для нас. Ми можемо виправити це, додавши 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 елемент переміщує зображення, і оскільки воно займає весь простір усередині поля, ми ніби наводимо курсор на поле! Наведення зображення більше не проблема!

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

Ти це бачив? Ви спочатку берете зображення та переміщуєте його в коробку, нічого особливого. Але коли ви відпускаєте зображення, ви запускаєте ефект наведення, який переміщує зображення, а потім ми моделюємо функцію перетягування. Якщо ви відпустите мишу за рамку, нічого не станеться.

Хм, ваша симуляція не ідеальна, тому що ми також можемо навести курсор і отримати той самий ефект.

Правда, і ми це виправимо. Нам потрібно вимкнути ефект наведення та дозволити його, лише якщо ми відпустимо зображення всередині поля. Ми будемо грати з розміром нашого .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 і фонові прийоми.

Ось наша сітка, написана мовою Pug для зручності:

- 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. Вертикальний пошук. Ai.
Як я створив гру-головоломку на чистому 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. Вертикальний пошук. Ai.
Як я створив гру-головоломку на чистому 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. Вертикальний пошук. Ai.
Як я створив гру-головоломку на чистому CSS

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

Ось знову повна демонстрація. Якщо ви порівняєте його з першою версією, ви побачите ту саму структуру коду для створення сітки та функції перетягування, а також код для створення фігур.

Можливі вдосконалення

На цьому стаття закінчується, але ми можемо продовжувати покращувати нашу головоломку ще більше можливостей! Як щодо таймера? Або, можливо, якесь привітання, коли гравець закінчує головоломку?

Я можу розглянути всі ці функції в майбутній версії, тому слідкуйте за моїм репо GitHub.

Підводячи підсумок

і CSS не є мовою програмування, вони кажуть. Ха!

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

Мета полягала не в створенні гри, а в дослідженні CSS і виявленні нових властивостей і прийомів, які можна використовувати в інших проектах. Створення онлайн-ігри на CSS — це складне завдання, яке спонукає вас детально вивчити функції CSS і навчитися ними користуватися. Крім того, це просто дуже весело, що ми отримуємо щось для гри, коли все сказано та зроблено.

Незалежно від того, чи є CSS мовою програмування, це не змінює того факту, що ми завжди вчимося, будуючи та створюючи інноваційні речі.

Часова мітка:

Більше від CSS-хитрощі