Hur jag gjorde ett rent CSS-pusselspel PlatoBlockchain Data Intelligence. Vertikal sökning. Ai.

Hur jag gjorde ett rent CSS-pusselspel

Jag upptäckte nyligen glädjen med att skapa CSS-spel. Det är alltid fascinerande hur HTML och CSS kan hantera logiken i ett helt onlinespel, så jag var tvungen att prova det! Sådana spel förlitar sig vanligtvis på det gamla Checkbox Hack där vi kombinerar det markerade/omarkerade tillståndet för en HTML-inmatning med :checked pseudoklass i CSS. Vi kan göra mycket magi med den ena kombinationen!

Faktum är att jag utmanade mig själv att bygga ett helt spel utan Checkbox. Jag var inte säker på om det skulle vara möjligt, men det är det definitivt, och jag ska visa dig hur.

Förutom pusselspelet vi kommer att studera i den här artikeln har jag gjort en samling rena CSS-spel, de flesta av dem utan Checkbox Hack. (De finns också tillgängliga på CodePen.)

Vill du spela innan vi börjar?

Jag personligen föredrar att spela spelet i helskärmsläge, men du kan spela det nedan eller öppna den här.

Coolt eller hur? Jag vet, det är inte det bästa pusselspelet du någonsin sett™, men det är inte heller dåligt alls för något som bara använder CSS och några rader HTML. Du kan enkelt justera storleken på rutnätet, ändra antalet celler för att kontrollera svårighetsgraden och använda vilken bild du vill!

Vi ska göra om den demot tillsammans och sedan lägga lite extra gnistan i den på slutet för några kickar.

Dra och släpp-funktionen

Även om strukturen i pusslet är ganska okomplicerad med CSS Grid, är möjligheten att dra och släppa pusselbitar lite knepigare. Jag var tvungen att förlita mig på en kombination av övergångar, svävningseffekter och syskonväljare för att få det gjort.

Om du håller muspekaren över den tomma rutan i den demon, flyttas bilden inuti den och stannar där även om du flyttar markören ut ur rutan. Tricket är att lägga till en stor övergångslängd och fördröjning - så stor att bilden tar mycket tid att återgå till sin ursprungliga position.

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

Anger endast transition-delay är tillräckligt, men att använda stora värden på både fördröjningen och varaktigheten minskar chansen att en spelare någonsin ser bilden flytta tillbaka. Om du väntar på 999s + 999s — vilket är ungefär 30 minuter — då kommer du att se bilden röra sig. Men det gör du inte, eller hur? Jag menar, ingen kommer att ta så lång tid mellan varven om de inte går bort från matchen. Så jag anser att detta är ett bra knep för att växla mellan två stater.

Har du märkt att hovring över bilden också utlöser ändringarna? Det beror på att bilden är en del av boxelementet, vilket inte är bra för oss. Vi kan fixa detta genom att lägga till pointer-events: none till bilden men vi kommer inte att kunna dra den senare.

Det betyder att vi måste introducera ett annat element inuti .box:

Det där extra div (vi använder en klass av .a) kommer att ta samma område som bilden (tack vare CSS Grid och grid-area: 1 / 1) och kommer att vara det element som utlöser hovringseffekten. Och det är där syskonväljaren kommer in i bilden:

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

Svävar på .a element flyttar bilden, och eftersom det tar upp all plats inuti rutan är det som att vi svävar över rutan istället! Att sväva över bilden är inte längre ett problem!

Låt oss dra och släppa vår bild i rutan och se resultatet:

Såg du att? Du tar först tag i bilden och flyttar den till rutan, inget fancy. Men när du släpper bilden utlöser du hovringseffekten som flyttar bilden, och sedan simulerar vi en dra och släpp-funktion. Om du släpper musen utanför lådan händer ingenting.

Hmm, din simulering är inte perfekt eftersom vi också kan sväva på rutan och få samma effekt.

Sant och vi kommer att rätta till detta. Vi måste inaktivera svävningseffekten och tillåta den endast om vi släpper bilden inuti rutan. Vi kommer att leka med dimensionen av vår .a element för att få det att hända.

Nu gör det ingenting att sväva på rutan. Men om du börjar dra bilden, .a elementet dyker upp, och när det väl har släppts i rutan kan vi utlösa hovringseffekten och flytta bilden.

Låt oss dissekera koden:

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

Genom att klicka på bilden aktiveras :active pseudo-klass som gör .a element full bredd (det är initialt lika med 0). Det aktiva tillståndet förblir aktiv tills vi släpper bilden. Om vi ​​släpper bilden inuti rutan, .a element går tillbaka till width: 0, men vi kommer att utlösa svävningseffekten innan det händer och bilden kommer att falla in i rutan! Om du släpper den utanför lådan händer ingenting.

Det finns en liten egenhet: att klicka på den tomma rutan flyttar också bilden och bryter vår funktion. För närvarande, :active är kopplad till .box element, så att klicka på det eller någon av dess barn aktiveras det; och genom att göra detta, slutar vi med att visa .a element och utlöser hovringseffekten.

Det kan vi fixa genom att leka med pointer-events. Det tillåter oss att inaktivera all interaktion med .box samtidigt som interaktionen med barnelementen bibehålls.

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

Nu vår dra och släpp-funktion är perfekt. Om du inte kan hitta hur man hackar den, är det enda sättet att flytta bilden att dra den och släppa den i rutan.

Bygga pusselrutnätet

Att lägga pusslet kommer att kännas enkelt jämfört med vad vi precis gjorde för dra och släpp-funktionen. Vi kommer att förlita oss på CSS-rutnät och bakgrundsknep för att skapa pusslet.

Här är vårt rutnät, skrivet i Pug för bekvämlighet:

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

Koden kan se konstig ut men den kompileras till vanlig 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>

Jag slår vad om att du undrar vad det är med de där taggarna. Inget av dessa element har någon speciell betydelse - jag tycker bara att koden är mycket lättare att skriva med <z> än ett gäng <div class="z"> eller vad som helst.

Så här har jag kartlagt dem:

  • <g> är vår gallercontainer som innehåller N*N <z> element.
  • <z> representerar våra rutnätsartiklar. Den spelar rollen som .box element vi såg i föregående avsnitt.
  • <a> utlöser svävningseffekten.
  • <b> representerar en del av vår bild. Vi tillämpar draggable attribut på den eftersom den inte kan dras som standard.

Okej, låt oss registrera vår rutnätsbehållare på <g>. Detta är i Sass istället för 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);
}

Vi kommer faktiskt att göra våra rutnätsbarn - de <z> element — rutnät också och har båda <a> och <b> inom samma nätområde:

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

Som du kan se, inget fancy - vi skapade ett rutnät med en specifik storlek. Resten av CSS vi behöver är för drag och släpp-funktionen, som kräver att vi slumpmässigt placerar bitarna runt brädet. Jag kommer att vända mig till Sass för detta, igen för bekvämligheten av att kunna gå igenom och styla alla pusselbitar med en funktion:

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

Du kanske har märkt att jag använder Sass random() fungera. Det är så vi får de randomiserade positionerna för pusselbitarna. Kom ihåg att vi kommer att göra det inaktivera den positionen när du håller musen över <a> element efter att ha dragit och släppt motsvarande <b> element inuti rutnätscellen.

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

I samma loop definierar jag också bakgrundskonfigurationen för varje pusselbit. Alla kommer logiskt att dela samma bild som bakgrunden, och dess storlek bör vara lika med storleken på hela rutnätet (definierad med --s variabel). Använder samma background-image och lite matematik uppdaterar vi background-position för att bara visa en del av bilden.

Det är allt! Vårt CSS-bara pusselspel är tekniskt klart!

Men vi kan alltid göra bättre, eller hur? jag visade dig hur man gör ett rutnät av pusselbitsformer i en annan artikel. Låt oss ta samma idé och tillämpa den här, eller hur?

Former av pusselbitar

Här är vårt nya pusselspel. Samma funktionalitet men med mer realistiska former!

Det här är en illustration av formerna på rutnätet:

Hur jag gjorde ett rent CSS-pusselspel

Om du tittar noga kommer du att märka att vi har nio olika pusselbitsformer: den fyra hörn, den fyra kanteroch en för allt annat.

Rutnätet med pusselbitar som jag gjorde i den andra artikeln jag hänvisade till är lite mer okomplicerat:

Vi kan använda samma teknik som kombinerar CSS-masker och gradienter för att skapa de olika formerna. Om du inte är bekant med mask och gradienter, jag rekommenderar starkt att du kollar det förenklade fallet för att bättre förstå tekniken innan du går vidare till nästa del.

Först måste vi använda specifika väljare för att rikta in oss på varje grupp av element som delar samma form. Vi har nio grupper, så vi kommer att använda åtta väljare, plus en standardväljare som väljer alla.

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

Här är en bild som visar hur det mappar till vårt rutnät:

Hur jag gjorde ett rent CSS-pusselspel PlatoBlockchain Data Intelligence. Vertikal sökning. Ai.
Hur jag gjorde ett rent CSS-pusselspel

Låt oss nu ta itu med formerna. Låt oss fokusera på att lära oss bara en eller två av formerna eftersom de alla använder samma teknik - och på så sätt har du lite läxor att fortsätta lära dig!

För pusselbitarna i mitten av rutnätet, 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);

Koden kan se komplex ut, men låt oss fokusera på en gradient i taget för att se vad som händer:

Två gradienter skapar två cirklar (markerade grönt och lila i demon), och två andra gradienter skapar spåren som andra bitar ansluter till (den markerade blå fyller upp det mesta av formen medan den markerade röd fyller den övre delen). en CSS-variabel, --r, ställer in radien för de cirkulära formerna.

Hur jag gjorde ett rent CSS-pusselspel PlatoBlockchain Data Intelligence. Vertikal sökning. Ai.
Hur jag gjorde ett rent CSS-pusselspel

Formen på pusselbitarna i mitten (markerade 0 i illustrationen) är svårast att göra eftersom den använder fyra gradienter och har fyra krökningar. Alla de andra bitarna jonglerar med färre gradienter.

Till exempel pusselbitarna längs den övre kanten av pusslet (markerade 2 i illustrationen) använder tre gradienter istället för fyra:

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

Vi tog bort den första (översta) gradienten och justerade värdena för den andra gradienten så att den täcker det kvarlämnade utrymmet. Du kommer inte att märka någon stor skillnad i koden om du jämför de två exemplen. Det bör noteras att vi kan hitta olika bakgrundskonfigurationer för att skapa samma form. Om du börjar spela med gradienter kommer du säkert att hitta på något annat än vad jag gjorde. Du kan till och med skriva något som är mer kortfattat - om så är fallet, dela det i kommentarerna!

Förutom att skapa formerna kommer du också att upptäcka att jag ökar bredden och/eller höjden på elementen som nedan:

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

Pusselbitarna måste svämma över sin rutnätscell för att ansluta.

Hur jag gjorde ett rent CSS-pusselspel PlatoBlockchain Data Intelligence. Vertikal sökning. Ai.
Hur jag gjorde ett rent CSS-pusselspel

Sista demo

Här är hela demon igen. Om du jämför den med den första versionen kommer du att se samma kodstruktur för att skapa rutnätet och dra-och-släpp-funktionen, plus koden för att skapa formerna.

Möjliga förbättringar

Artikeln slutar här men vi kan fortsätta att förbättra vårt pussel med ännu fler funktioner! Vad sägs om en timer? Eller kanske någon form av gratulationer när spelaren är klar med pusslet?

Jag kan överväga alla dessa funktioner i en framtida version, så håll ett öga på min GitHub-repo.

Inslagning upp

Och CSS är inte ett programmeringsspråk, de säger. ha!

Jag försöker inte få igång något #HotDrama med det. Jag säger det för att vi gjorde några riktigt knepiga logiska saker och täckte många CSS-egenskaper och -tekniker längs vägen. Vi spelade med CSS Grid, övergångar, maskering, gradienter, väljare och bakgrundsegenskaper. För att inte tala om de få Sass-tricken vi använde för att göra vår kod lätt att justera.

Målet var inte att bygga spelet, utan att utforska CSS och upptäcka nya egenskaper och knep som du kan använda i andra projekt. Att skapa ett onlinespel i CSS är en utmaning som driver dig att utforska CSS-funktioner i detalj och lära dig hur du använder dem. Dessutom är det bara väldigt roligt att vi får något att leka med när allt är klart.

Om CSS är ett programmeringsspråk eller inte, ändrar inte det faktum att vi alltid lär oss genom att bygga och skapa innovativa saker.

Tidsstämpel:

Mer från CSS-tricks