Zephyrnet-logo

Hoe ik een puur CSS-puzzelspel heb gemaakt

Datum:

Ik heb onlangs ontdekt hoe leuk het is om games met alleen CSS te maken. Het is altijd fascinerend hoe HTML en CSS in staat zijn om de logica van een volledig online spel aan te kunnen, dus ik moest het proberen! Dergelijke spellen vertrouwen meestal op de oude Checkbox-hack, waarbij we de aangevinkte/niet-gecontroleerde staat van een HTML-invoer combineren met de :checked pseudo-klasse in CSS. Met die ene combinatie kunnen we heel wat toveren!

Ik heb mezelf zelfs uitgedaagd om een ​​hele game te bouwen zonder Checkbox. Ik wist niet zeker of het mogelijk zou zijn, maar dat is het zeker, en ik ga je laten zien hoe.

Naast het puzzelspel dat we in dit artikel zullen bestuderen, heb ik gemaakt een verzameling pure CSS-spellen, de meeste zonder de Checkbox Hack. (Ze zijn ook beschikbaar) op CodePen.)

Wil je spelen voordat we beginnen?

Persoonlijk geef ik er de voorkeur aan om het spel in volledig scherm te spelen, maar je kunt het hieronder spelen of open het hier.

Cool toch? Ik weet het, het is niet het beste puzzelspel dat je ooit hebt gezien™, maar het is ook helemaal niet slecht voor iets dat alleen CSS en een paar regels HTML gebruikt. U kunt eenvoudig de grootte van het raster aanpassen, het aantal cellen wijzigen om de moeilijkheidsgraad te regelen en elke gewenste afbeelding gebruiken!

We gaan die demo samen opnieuw maken, en er op het einde wat extra sprankeling in doen voor wat kick.

De functionaliteit voor slepen en neerzetten

Hoewel de structuur van de puzzel redelijk eenvoudig is met CSS Grid, is de mogelijkheid om puzzelstukjes te slepen en neer te zetten een beetje lastiger. Ik moest vertrouwen op een combinatie van overgangen, zweefeffecten en broers en zussen om het voor elkaar te krijgen.

Als u in die demo over het lege vak beweegt, beweegt de afbeelding erin en blijft daar ook als u de cursor uit het vak beweegt. De truc is om een ​​grote overgangsduur en vertraging toe te voegen - zo groot dat de afbeelding veel tijd nodig heeft om terug te keren naar zijn oorspronkelijke positie.

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

Alleen de . specificeren transition-delay is genoeg, maar het gebruik van grote waarden voor zowel de vertraging als de duur verkleint de kans dat een speler het beeld ooit terug ziet bewegen. Als je wacht op 999s + 999s - wat ongeveer 30 minuten is - dan zul je het beeld zien bewegen. Maar dat doe je niet, toch? Ik bedoel, niemand zal zo lang tussen de beurten zitten, tenzij ze weglopen van het spel. Dus ik beschouw dit als een goede truc om tussen twee staten te schakelen.

Is het je opgevallen dat het zweven van de afbeelding ook de wijzigingen activeert? Dat komt omdat de afbeelding deel uitmaakt van het dooselement, wat niet goed voor ons is. We kunnen dit oplossen door toe te voegen pointer-events: none naar de afbeelding, maar we kunnen deze later niet slepen.

Dat betekent dat we een ander element moeten introduceren in de .box:

Dat extra div (we gebruiken een klasse van .a) zal hetzelfde gebied innemen als de afbeelding (dankzij CSS Grid en grid-area: 1 / 1) en zal het element zijn dat het zweefeffect activeert. En dat is waar de broer of zus-selector in het spel komt:

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

Zweven op de .a element verplaatst de afbeelding, en aangezien het alle ruimte in de doos in beslag neemt, is het alsof we in plaats daarvan over de doos zweven! Over de afbeelding zweven is geen probleem meer!

Laten we onze afbeelding in de doos slepen en neerzetten en het resultaat bekijken:

Heb je dat gezien? Je pakt eerst de afbeelding en verplaatst deze naar de doos, niets bijzonders. Maar zodra u de afbeelding loslaat, activeert u het zweefeffect dat de afbeelding verplaatst, en vervolgens simuleren we een functie voor slepen en neerzetten. Als u de muis buiten de doos loslaat, gebeurt er niets.

Hmm, je simulatie is niet perfect omdat we de box ook kunnen laten zweven en hetzelfde effect krijgen.

Dat is waar en we zullen dit rechtzetten. We moeten het zweefeffect uitschakelen en het alleen toestaan ​​als we de afbeelding in de doos vrijgeven. We zullen spelen met de dimensie van onze .a element om dat mogelijk te maken.

Nu, het zweven van de doos doet niets. Maar als u de afbeelding begint te slepen, .a element verschijnt, en eenmaal losgelaten in de doos, kunnen we het zweefeffect activeren en de afbeelding verplaatsen.

Laten we de code ontleden:

.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;
}

Als u op de afbeelding klikt, wordt de :active pseudo-klasse die de . maakt .a element over de volledige breedte (het is aanvankelijk gelijk aan 0). De actieve status blijft actieve totdat we de afbeelding vrijgeven. Als we de afbeelding in de doos vrijgeven, wordt de .a element gaat terug naar width: 0, maar we activeren het zweefeffect voordat het gebeurt en de afbeelding valt in de doos! Als je het buiten de doos loslaat, gebeurt er niets.

Er is een kleine eigenaardigheid: door op het lege vak te klikken, wordt ook de afbeelding verplaatst en wordt onze functie verbroken. Momenteel, :active is gekoppeld aan de .box element, dus door erop te klikken of op een van de onderliggende elementen wordt het geactiveerd; en door dit te doen, tonen we uiteindelijk de .a element en activeert het hover-effect.

We kunnen dat oplossen door te spelen met pointer-events. Het stelt ons in staat om elke interactie met de . uit te schakelen .box terwijl de interactie met de onderliggende elementen behouden blijft.

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

Nu onze drag-and-drop-functie is perfect. Tenzij je kunt vinden hoe je het kunt hacken, is de enige manier om de afbeelding te verplaatsen, hem in de doos te slepen en neer te zetten.

Het puzzelraster bouwen

Het samenstellen van de puzzel zal gemakkelijk aanvoelen in vergelijking met wat we net hebben gedaan voor de functie slepen en neerzetten. We gaan vertrouwen op CSS-raster- en achtergrondtrucs om de puzzel te maken.

Hier is ons raster, voor het gemak geschreven in 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") 

De code ziet er misschien vreemd uit, maar wordt gecompileerd in gewone HTML:


 
   
   
 
 
   
   
 
 
   
   
 
  

Ik wed dat je je afvraagt ​​wat er met die tags aan de hand is. Geen van deze elementen heeft een speciale betekenis - ik vind gewoon dat de code veel gemakkelijker te schrijven is met dan een stelletje

of wat dan ook.

Zo heb ik ze in kaart gebracht:

  • is onze rastercontainer die bevat N*N elementen.
  • staat voor onze rasteritems. Het speelt de rol van de .box element dat we in de vorige sectie zagen.
  • activeert het hover-effect.
  • vertegenwoordigt een deel van ons beeld. Wij passen de draggable attribuut erop omdat het standaard niet kan worden gesleept.

Oké, laten we onze rastercontainer registreren op . Dit is in Sass in plaats van 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);
}

We gaan eigenlijk onze rasterkinderen maken - de elementen — ook rasters en hebben beide en binnen hetzelfde rastergebied:

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

Zoals je kunt zien, niets bijzonders - we hebben een raster gemaakt met een specifieke grootte. De rest van de CSS die we nodig hebben, is voor de functie slepen en neerzetten, waarbij we de stukken willekeurig over het bord moeten plaatsen. Ik ga hiervoor naar Sass, opnieuw voor het gemak om alle puzzelstukjes met een functie te kunnen doorlopen en stylen:

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)
   }
}

Je hebt misschien gemerkt dat ik de Sass . gebruik random() functie. Zo krijgen we de willekeurige posities voor de puzzelstukjes. Onthoud dat we zullen 'disable' die positie wanneer u over de zweeft element na het slepen en neerzetten van de bijbehorende element in de rastercel.

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

In diezelfde lus definieer ik ook de achtergrondconfiguratie voor elk puzzelstukje. Ze zullen allemaal logischerwijs dezelfde afbeelding delen als de achtergrond, en de grootte moet gelijk zijn aan de grootte van het hele raster (gedefinieerd met de --s variabel). Hetzelfde gebruiken background-image en wat wiskunde, we updaten de background-position om slechts een deel van de afbeelding te tonen.

Dat is het! Ons puzzelspel met alleen CSS is technisch klaar!

Maar het kan altijd beter, toch? ik liet je zien hoe maak je een raster van puzzelstukvormen in een ander artikel. Laten we datzelfde idee nemen en het hier toepassen, zullen we?

Puzzelstukjes vormen

Hier is ons nieuwe puzzelspel. Dezelfde functionaliteit maar met meer realistische vormen!

Dit is een illustratie van de vormen op het raster:

Als je goed kijkt, zie je dat we negen verschillende puzzelstukjes hebben: de vier hoeken vier randen en één voor al het andere.

Het raster van puzzelstukjes dat ik heb gemaakt in het andere artikel waarnaar ik verwees, is iets eenvoudiger:

We kunnen dezelfde techniek gebruiken die CSS-maskers en verlopen combineert om de verschillende vormen te maken. Voor het geval je niet bekend bent met mask en verlopen, raad ik ten zeerste aan om te controleren dat vereenvoudigde geval om de techniek beter te begrijpen voordat u naar het volgende deel gaat.

Ten eerste moeten we specifieke selectors gebruiken om elke groep elementen met dezelfde vorm te targeten. We hebben negen groepen, dus we zullen acht selectors gebruiken, plus een standaardselector die ze allemaal selecteert.

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 */

Hier is een afbeelding die laat zien hoe dat op ons raster wordt weergegeven:

Laten we nu de vormen aanpakken. Laten we ons concentreren op het leren van slechts een of twee van de vormen, omdat ze allemaal dezelfde techniek gebruiken - en op die manier heb je wat huiswerk om te blijven leren!

Voor de puzzelstukjes in het midden van het raster, 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);

De code ziet er misschien ingewikkeld uit, maar laten we ons concentreren op één verloop tegelijk om te zien wat er gebeurt:

Twee gradiënten creëren twee cirkels (groen en paars gemarkeerd in de demo), en twee andere gradiënten creëren de sleuven waarmee andere stukken verbinding maken (de blauwe gemarkeerde vult het grootste deel van de vorm terwijl de rood gemarkeerde het bovenste gedeelte vult). Een CSS-variabele, --r, stelt de straal van de cirkelvormen in.

De vorm van de puzzelstukjes in het midden (gemarkeerd 0 in de afbeelding) is het moeilijkst te maken omdat het vier hellingen gebruikt en vier krommingen heeft. Alle andere stukken jongleren met minder hellingen.

Bijvoorbeeld de puzzelstukjes langs de bovenrand van de puzzel (gemarkeerd 2 in de afbeelding) gebruikt drie hellingen in plaats van vier:

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);

We hebben de eerste (bovenste) gradiënt verwijderd en de waarden van de tweede gradiënt aangepast zodat deze de resterende ruimte bedekt. U zult geen groot verschil in de code merken als u de twee voorbeelden vergelijkt. Opgemerkt moet worden dat we verschillende achtergrondconfiguraties kunnen vinden om dezelfde vorm te creëren. Als je met gradiënten gaat spelen, kom je zeker met iets anders op de proppen dan wat ik deed. Je kunt zelfs iets schrijven dat beknopter is - als dat zo is, deel het dan in de reacties!

Naast het maken van de vormen, zul je ook merken dat ik de breedte en/of de hoogte van de elementen vergroot zoals hieronder:

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

De stukjes van de puzzel moeten hun rastercel overlopen om verbinding te maken.

Laatste demo

Hier is de volledige demo nog een keer. Als je het vergelijkt met de eerste versie, zie je dezelfde codestructuur om het raster en de functie voor slepen en neerzetten te maken, plus de code om de vormen te maken.

Mogelijke verbeteringen

Het artikel eindigt hier, maar we kunnen onze puzzel blijven verbeteren met nog meer functies! Wat dacht je van een timer? Of misschien een soort van felicitatie als de speler de puzzel af heeft?

Ik kan al deze functies in een toekomstige versie overwegen, dus houd mijn GitHub-opslagplaats in de gaten.

Afsluiten

En CSS is geen programmeertaal, ze zeggen. Ha!

Ik probeer daarmee geen #HotDrama op te wekken. Ik zeg het omdat we een aantal heel lastige logische dingen hebben gedaan en onderweg veel CSS-eigenschappen en -technieken hebben behandeld. We speelden met CSS Grid, overgangen, maskering, verlopen, selectors en achtergrondeigenschappen. Om nog maar te zwijgen van de paar Sass-trucs die we hebben gebruikt om onze code gemakkelijk aan te passen.

Het doel was niet om het spel te bouwen, maar om CSS te verkennen en nieuwe eigenschappen en trucs te ontdekken die je in andere projecten kunt gebruiken. Het creëren van een online game in CSS is een uitdaging die je ertoe aanzet om CSS-functies tot in detail te verkennen en te leren hoe je ze kunt gebruiken. Bovendien is het gewoon heel leuk dat we iets krijgen om mee te spelen als alles is gezegd en gedaan.

Of CSS nu een programmeertaal is of niet, verandert niets aan het feit dat we altijd leren door innovatieve dingen te bouwen en te creëren.

spot_img

Laatste intelligentie

spot_img

Chat met ons

Hallo daar! Hoe kan ik u helpen?