Ich habe kürzlich die Freude an der Erstellung von reinen CSS-Spielen entdeckt. Es ist immer faszinierend, wie HTML und CSS die Logik eines kompletten Online-Spiels handhaben können, also musste ich es ausprobieren! Solche Spiele verlassen sich normalerweise auf den alten „Checkbox-Hack“, bei dem wir den geprüften/ungeprüften Zustand eines HTML-Inputs mit der Pseudoklasse :checked in CSS kombinieren. Mit dieser einen Kombination können wir viel Magie vollbringen!
Tatsächlich habe ich mir vorgenommen, ein komplettes Spiel ohne Checkbox zu bauen. Ich war mir nicht sicher, ob es möglich sein würde, aber es ist definitiv möglich, und ich zeige Ihnen, wie.
Zusätzlich zu dem Puzzlespiel, das wir in diesem Artikel behandeln werden, habe ich eine Sammlung von reinen CSS-Spielen erstellt, die meisten davon ohne den Checkbox-Hack. (Sie sind auch auf CodePen verfügbar.)
Möchtest du spielen, bevor wir anfangen?
Ich persönlich bevorzuge es, das Spiel im Vollbildmodus zu spielen, aber Sie können es unten spielen oder hier öffnen.
Cool, oder? Ich weiß, es ist nicht das beste Puzzlespiel, das du je gesehen hast™, aber es ist auch gar nicht schlecht für etwas, das nur CSS und ein paar Zeilen HTML verwendet. Sie können die Größe des Gitters leicht anpassen, die Anzahl der Zellen ändern, um den Schwierigkeitsgrad zu steuern, und jedes beliebige Bild verwenden!
Wir werden diese Demo gemeinsam nachbauen und dann am Ende noch etwas extra Glanz für den Spaß hinzufügen.
Die Drag-and-Drop-Funktionalität
Während die Struktur des Puzzles mit CSS Grid ziemlich einfach ist, ist die Möglichkeit, Puzzleteile per Drag-and-Drop zu verschieben, etwas kniffliger. Ich musste mich auf eine Kombination aus Übergängen, Hover-Effekten und Geschwister-Selektoren verlassen, um dies zu erreichen.
Wenn Sie über die leere Box in dieser Demo fahren, bewegt sich das Bild hinein und bleibt dort, auch wenn Sie den Cursor aus der Box bewegen. Der Trick besteht darin, eine große Übergangsdauer und Verzögerung hinzuzufügen – so groß, dass das Bild viel Zeit zum Zurückkehren in seine Ausgangsposition benötigt.
img {
transform: translate(200%);
transition: 999s 999s; /* very slow move on mouseout */
}
.box:hover img {
transform: translate(0);
transition: 0s; /* instant move on hover */
}
Die Angabe nur der transition-delay ist ausreichend, aber die Verwendung großer Werte sowohl für die Verzögerung als auch für die Dauer verringert die Wahrscheinlichkeit, dass ein Spieler das Bild jemals zurückwandern sieht. Wenn Sie auf 999s + 999s warten – das sind ungefähr 30 Minuten –, werden Sie sehen, wie sich das Bild bewegt. Aber Sie werden es nicht tun, oder? Ich meine, niemand wird so lange zwischen den Zügen brauchen, es sei denn, er verlässt das Spiel. Daher betrachte ich dies als einen guten Trick zum Wechseln zwischen zwei Zuständen.
Haben Sie bemerkt, dass das Hovern über das Bild ebenfalls die Änderungen auslöst? Das liegt daran, dass das Bild Teil des Box-Elements ist, was für uns nicht gut ist. Wir können dies beheben, indem wir pointer-events: none zum Bild hinzufügen, aber wir können es später nicht mehr ziehen.
Das bedeutet, wir müssen ein weiteres Element innerhalb der .box einführen
Diese zusätzliche div (wir verwenden eine Klasse von .a) nimmt die gleiche Fläche wie das Bild ein (dank CSS Grid und grid-area: 1 / 1) und ist das Element, das den Hover-Effekt auslöst. Und hier kommt der Geschwister-Selektor ins Spiel
.a {
grid-area: 1 / 1;
}
img {
grid-area: 1 / 1;
transform: translate(200%);
transition: 999s 999s;
}
.a:hover + img {
transform: translate(0);
transition: 0s;
}
Das Hovern über das Element .a bewegt das Bild, und da es den gesamten Platz innerhalb der Box einnimmt, ist es, als würden wir über die Box hovern! Das Hovern über das Bild ist kein Problem mehr!
Lassen Sie uns unser Bild in die Box ziehen und das Ergebnis sehen
Hast du das gesehen? Zuerst greifst du das Bild und bewegst es zur Box, nichts Besonderes. Aber sobald du das Bild loslässt, löst du den Hover-Effekt aus, der das Bild bewegt, und dann simulieren wir eine Drag-and-Drop-Funktion. Wenn du die Maus außerhalb der Box loslässt, passiert nichts.
Hm, deine Simulation ist nicht perfekt, weil wir auch über die Box hovern und den gleichen Effekt erzielen können.
Das stimmt, und wir werden das korrigieren. Wir müssen den Hover-Effekt deaktivieren und ihn nur zulassen, wenn wir das Bild in der Box loslassen. Wir werden mit den Abmessungen unseres .a-Elements spielen, um das zu erreichen.
Jetzt tut das Hovern über die Box nichts. Aber wenn du anfängst, das Bild zu ziehen, erscheint das Element .a, und sobald es innerhalb der Box losgelassen wird, können wir den Hover-Effekt auslösen und das Bild bewegen.
Lassen Sie uns den Code zerlegen
.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;
}
Das Klicken auf das Bild löst die Pseudoklasse :active aus, die das Element .a vollbildtauglich macht (es ist anfangs gleich 0). Der aktive Zustand bleibt *aktiv*, bis wir das Bild loslassen. Wenn wir das Bild in der Box loslassen, geht das Element .a zurück zu width: 0, aber wir lösen den Hover-Effekt aus, bevor es passiert, und das Bild fällt in die Box! Wenn du es außerhalb der Box loslässt, passiert nichts.
Es gibt eine kleine Eigenheit: Das Klicken auf die leere Box bewegt auch das Bild und bricht unsere Funktion. Derzeit ist :active mit dem Element .box verknüpft, sodass das Klicken darauf oder auf eines seiner Kindelemente es aktiviert; und indem wir dies tun, zeigen wir das Element .a an und lösen den Hover-Effekt aus.
Das können wir beheben, indem wir mit pointer-events spielen. Es erlaubt uns, jegliche Interaktion mit der .box zu deaktivieren, während die Interaktionen mit den Kindelementen beibehalten werden.
.box {
pointer-events: none;
}
.box * {
pointer-events: initial;
}
*Jetzt* ist unsere Drag-and-Drop-Funktion perfekt. Solange Sie keinen Weg finden, sie zu hacken, ist die einzige Möglichkeit, das Bild zu bewegen, es zu ziehen und in die Box fallen zu lassen.
Aufbau des Puzzles
Das Zusammenfügen des Puzzles wird im Vergleich zu dem, was wir gerade für die Drag-and-Drop-Funktion getan haben, ein Kinderspiel sein. Wir werden uns auf CSS Grid und Hintergrund-Tricks verlassen, um das Puzzle zu erstellen.
Hier ist unser Gitter, zur Bequemlichkeit in Pug geschrieben
- 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")
Der Code mag seltsam aussehen, aber er wird in einfachen HTML kompiliert
<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>
Ich wette, Sie fragen sich, was es mit diesen Tags auf sich hat. Keines dieser Elemente hat irgendeine besondere Bedeutung – ich finde einfach, dass der Code mit <z> viel einfacher zu schreiben ist als eine Reihe von <div class="z"> oder was auch immer.
So habe ich sie abgebildet
<g>ist unser Gittercontainer, derN*N<z>-Elemente enthält.<z>repräsentiert unsere Gitterelemente. Es spielt die Rolle des.box-Elements, das wir im vorherigen Abschnitt gesehen haben.<a>löst den Hover-Effekt aus.<b>repräsentiert einen Teil unseres Bildes. Wir wenden das Attributdraggabledarauf an, da es standardmäßig nicht gezogen werden kann.
Okay, registrieren wir unseren Gittercontainer auf <g>. Dies ist in Sass anstelle von 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);
}
Wir werden unsere Gitterkinder – die <z>-Elemente – tatsächlich auch zu Gittern machen und sowohl <a> als auch <b> im selben Gitterbereich haben
z {
aspect-ratio: 1;
display: grid;
outline: 1px dashed;
}
a {
grid-area: 1/1;
}
b {
grid-area: 1/1;
}
Wie Sie sehen, nichts Besonderes – wir haben ein Gitter mit einer bestimmten Größe erstellt. Der Rest des benötigten CSS ist für die Drag-and-Drop-Funktion, die erfordert, dass wir die Teile zufällig auf dem Spielfeld platzieren. Ich werde mich dafür wieder an Sass wenden, wiederum zur Bequemlichkeit, dass wir durch alle Puzzleteile mit einer Funktion iterieren und sie stylen können
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)
}
}
Sie haben vielleicht bemerkt, dass ich die Sass-Funktion random() verwende. So erhalten wir die zufälligen Positionen für die Puzzleteile. Denken Sie daran, dass wir diese Position deaktivieren werden, wenn wir über das Element <a> hovern, nachdem wir sein entsprechendes Element <b> in die Gitterzelle gezogen und fallen gelassen haben.
z a:hover ~ b {
transform: translate(0);
transition: 0s;
}
Im selben Loop definiere ich auch die Hintergrundkonfiguration für jedes Puzzleteil. Alle werden logischerweise das gleiche Bild als Hintergrund haben, und seine Größe sollte gleich der Größe des gesamten Gitters sein (definiert mit der Variablen --s). Unter Verwendung des gleichen background-image und einiger Mathematik aktualisieren wir die background-position, um nur einen Teil des Bildes anzuzeigen.
Das ist es! Unser reines CSS-Puzzlespiel ist technisch fertig!
Aber wir können immer besser werden, oder? Ich habe Ihnen in einem anderen Artikel gezeigt, wie man ein Gitter von Puzzleteilformen erstellt. Nehmen wir diese Idee und wenden sie hier an, sollen wir?
Puzzleteilformen
Hier ist unser neues Puzzlespiel. Gleiche Funktionalität, aber mit realistischeren Formen!
Dies ist eine Illustration der Formen auf dem Gitter

Wenn Sie genau hinschauen, werden Sie feststellen, dass wir neun verschiedene Puzzleteilformen haben: die **vier Ecken**, die **vier Kanten** und **eine für alles andere**.
Das Gitter von Puzzleteilen, das ich im anderen Artikel erstellt habe, auf den ich verwiesen habe, ist etwas einfacher
Wir können die gleiche Technik verwenden, die CSS-Masken und -Verläufe kombiniert, um die verschiedenen Formen zu erstellen. Falls Sie mit mask und Verläufen nicht vertraut sind, empfehle ich Ihnen dringend, diesen vereinfachten Fall zu überprüfen, um die Technik besser zu verstehen, bevor Sie mit dem nächsten Teil fortfahren.
Zuerst müssen wir spezifische Selektoren verwenden, um jede Gruppe von Elementen anzusprechen, die die gleiche Form haben. Wir haben neun Gruppen, also werden wir acht Selektoren verwenden, plus einen Standardselektor, der sie alle auswählt.
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 ist eine Abbildung, die zeigt, wie das auf unser Gitter abgebildet wird

Nun kümmern wir uns um die Formen. Konzentrieren wir uns darauf, nur ein oder zwei der Formen zu lernen, da sie alle die gleiche Technik verwenden – und so haben Sie etwas Hausaufgaben, um weiter zu lernen!
Für die Puzzleteile in der Mitte des Gitters, 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);
Der Code mag komplex aussehen, aber konzentrieren wir uns auf einen Verlauf nach dem anderen, um zu sehen, was passiert
Zwei Verläufe erzeugen zwei Kreise (im Demo grün und lila markiert), und zwei weitere Verläufe erzeugen die Schlitze, an die andere Teile anschließen (der blau markierte füllt den größten Teil der Form aus, während der rot markierte den oberen Teil ausfüllt). Eine CSS-Variable, --r, setzt den Radius der kreisförmigen Formen.

Die Form der Puzzleteile in der Mitte (in der Abbildung mit 0 markiert) ist am schwierigsten herzustellen, da sie vier Verläufe verwendet und vier Krümmungen hat. Alle anderen Teile jonglieren mit weniger Verläufen.
Zum Beispiel verwenden die Puzzleteile am oberen Rand des Puzzles (in der Abbildung mit 2 markiert) drei Verläufe anstelle von 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);
Wir haben den ersten (oberen) Verlauf entfernt und die Werte des zweiten Verlaufs angepasst, sodass er den entstandenen Raum ausfüllt. Sie werden keinen großen Unterschied im Code feststellen, wenn Sie die beiden Beispiele vergleichen. Es sei darauf hingewiesen, dass wir verschiedene Hintergrundkonfigurationen finden können, um die gleiche Form zu erstellen. Wenn Sie anfangen, mit Verläufen zu spielen, werden Sie mit Sicherheit etwas anderes finden als das, was ich getan habe. Vielleicht schreiben Sie sogar etwas prägnanteres – wenn ja, teilen Sie es in den Kommentaren!
Zusätzlich zur Erstellung der Formen werden Sie auch feststellen, dass ich die Breite und/oder Höhe der Elemente wie unten erhöhe
height: calc(100% + var(--r));
width: calc(100% + var(--r));
Die Puzzleteile müssen ihre Gitterzelle überlappen, um sich zu verbinden.

Finale Demo
Hier ist die vollständige Demo noch einmal. Wenn Sie sie mit der ersten Version vergleichen, sehen Sie die gleiche Code-Struktur zur Erstellung des Gitters und der Drag-and-Drop-Funktion, plus den Code zur Erstellung der Formen.
Mögliche Verbesserungen
Der Artikel endet hier, aber wir könnten unser Puzzle mit noch mehr Funktionen weiter verbessern! Wie wäre es mit einem Timer? Oder vielleicht einer Art Glückwunsch, wenn der Spieler das Puzzle abschließt?
Ich werde all diese Funktionen vielleicht in einer zukünftigen Version berücksichtigen, also behalten Sie mein GitHub-Repo im Auge.
Zusammenfassung
Und CSS ist keine Programmiersprache, sagen sie. Ha!
Ich versuche nicht, damit #HotDrama zu entfachen. Ich sage das, weil wir wirklich knifflige logische Dinge getan und viele CSS-Eigenschaften und -Techniken behandelt haben. Wir haben mit CSS Grid, Übergängen, Maskierung, Verläufen, Selektoren und Hintergrundeigenschaften gespielt. Ganz zu schweigen von den wenigen Sass-Tricks, die wir verwendet haben, um unseren Code einfach anzupassen.
Das Ziel war nicht, das Spiel zu bauen, sondern CSS zu erforschen und neue Eigenschaften und Tricks zu entdecken, die Sie in anderen Projekten verwenden können. Ein Online-Spiel in CSS zu erstellen, ist eine Herausforderung, die Sie dazu bringt, CSS-Funktionen im Detail zu erkunden und zu lernen, wie man sie verwendet. Außerdem macht es einfach viel Spaß, dass wir am Ende etwas zum Spielen haben.
Ob CSS eine Programmiersprache ist oder nicht, ändert nichts daran, dass wir immer durch das Bauen und Erstellen innovativer Dinge lernen.
Auf Firefox macOS wird beim Versuch, ein Teil zu ziehen, nur der Drag-and-Drop-Vorgang auf dem Bild aktiviert.
Auf Chrome iPad funktioniert es nicht, man kann nicht ziehen und fallen lassen
Das von mir entwickelte Spiel ist eine Art CSS-Experiment. Es ist nicht dafür gedacht, in allen Browsern zu funktionieren, also melden Sie bitte keine Browserprobleme.
Verwenden Sie für das beste Erlebnis Chrome (oder Firefox) unter Windows oder Linux Desktop.
Vergessen Sie mobile oder Touch-Geräte und all die MacOs- und Apple-Sachen.
Ausgezeichnete Fähigkeiten und beeindruckendes Ergebnis, danke für das Teilen!
Das ist so cool! Vielen Dank für die ausführliche Erklärung.
Was das Ziehen und Ablegen angeht – ich habe festgestellt, dass, wenn ich einen Ziehvorgang einleite, aber kurz bin (loslasse, bevor er die Box erreicht), das
aaktiv bleibt und ich das Bild dann durch Hovern über dasa„teleportieren“ kann.Unglaublich für CSS-Fans!
Es wäre vielleicht eine coole Idee, etwas wie Folgendes einzurichten
Damit deine Teile langsam davonlaufen, wenn du nicht schnell genug baust...
Außerdem frage ich mich, ob es einen möglichen Mechanismus gibt, um zu erkennen, wann das Puzzle fertig ist und das Ganze darauf reagiert.
Ich habe ein paar Ideen, wie man erkennen kann, wann das Puzzle abgeschlossen ist und etwas tut (eine „Bravo“-Nachricht oder eine Animation), aber ich wollte das nicht in den Artikel aufnehmen, um ihn nicht zu lang zu machen.
Sie können sich immer mein GitHub-Repo ansehen (oder meinem Twitter folgen), wenn Sie diese Version des Puzzles nicht verpassen möchten ;)
Das ist großartig! Ich habe deinen ursprünglichen Pen gesehen und es hat eine Weile gedauert, bis ich den Trick mit dem langen Übergang verstanden habe; das habe ich noch nie zuvor gesehen.
Tolle Aufschlüsselung!
Alle Übergänge können in eine Animation umgewandelt werden. Sie können
animation-fill-mode: forwards;verwenden. So bleibt der Endzustand der Animation bestehen, nachdem sie beendet ist.