Autorin: Linda Görzen
Konzept
Ziel dieses Projektes war die Erstellung eines Spiels , das durch sämtliche Animationen bzw. Animationseffekte modern und dynamisch wirken sollte. Memory ist ein Spiel, das weltbekannt und nicht zu kompliziert zu programmieren ist. Im Internet gibt es sämtliche frei verfügbare Beispielcodes. Einer davon wurde in diesem Projekt verwendet.
Der Spielvorgang läuft folgendermaßen ab: Der Start des Spieles muss durch das Klicken eines Buttons ausgelöst werden. Danach hat man 30 Sekunden Zeit, um das Spiel zu gewinnen. Wenn man es nicht schafft, bricht das Spiel nach 30 Sekunden ab und man hat somit verloren.
Inhaltsverzeichnis
Aufbau
Das Spiel besteht aus drei Ebenen: 1. Startbildschirm 2. Spiel 3. Endbildschirm: gewonnen oder verloren
Für die Einfachheit und Übersichtlichkeit besteht das Spiel aus zwei HTML-Dateien. Die Datei game_start.html beinhaltet die erste Ebene. Die Datei game.html die Ebenen zwei und drei.
Startbildschirm
Hier erscheinen nacheinander ein Button, der zum Spiel weiterführen soll, ein Austronaut und ein Raumschiff. Im Hintergrund bewegen sich von oben nach unten und von unten nach oben kleine Punkte, die Sterne repräsentieren sollen. Der Button hat einen Hover-Effekt: Wenn man über ihn mit der Maus fährt, verändert sich der Text und die Farbe .
Memory-Spiel
Das eigentliche Spiel befindet sich im rechten Drittel des Bildschirms und besteht aus zwölf Karten, welche in drei Reihen gestapelt sind. Links vom Spiel sieht man den Astronauten und eine Sprechblase. Über den Astronaut schwebt ein Button, mit dem das Spiel neu gestartet werden kann. Beim Klicken auf die Karten, erscheint zusätzlicher Text in der Sprechblase. Nach ein paar Sekunden verschwindet dieser.
Gewonnen – Endbildschirm
Wenn man innerhalb der 30 Sekunden alle Paare gefunden hat, verschwinden die Sprechblase und die Karten. Stattdessen blendet das Programm Konfetti und Überschrift “6/6! Gut gemacht gemacht!” ein. Im Hintergrund wird einmalig Triumpfsound “Ta-Da” abgespielt.
Verloren – Endbildschirm
Wenn 30 Sekunden verstrichen sind und nicht alle Paare gefunden wurden, dann bricht das Spiel ab und statt der Sprechblase und der Karten erscheint die Überschrift “Verloren” und in der Sprechblase steht jetzt “Schande!”. Der Astronaut fängt zu weinen an.
Erläuterung des Codes
Erste Ebene
HTML und javascript
Für die Erzeugung bestimmter Animationen lohnt es sich auf JS-Bibliotheken zurückzugreifen. In diesem Projekt wurde GSAP eingesetzt. Bei GSAP handelt es sich um eine JS-Bibliothek für zeitleistenbasierte Animationen . Die GSAP Funktionen lassen sich über CDN laden, indem man ihn im Head-Bereich einbindet.
Der CDN für allgemeine Funktionen reicht aber nicht aus, um Bewegung entlang eines vorgeschriebenen Pfades zu erzeugen. Der Code muss zusätzlich mit dem Plugin für GSAP-MotionPath ergänzt werden.
<head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="description" content="Memory game done with javascript and css animation"> <meta name="keywords" content="game, interactive, avascript, css, html"> <title>Memory Game</title> <link rel="stylesheet" href="styles_start.css"> <!-- Nur für game_start --> <script src="https://cdn.jsdelivr.net/web-animations/latest/web-animations.min.js"></script> <!-- Polyfill --> <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.7.0/gsap.min.js"></script> <!-- Core Green Sock--> <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.7.0/MotionPathPlugin.min.js"></script> <!--Plugin für MotionPath von Green Sock--> </head>
Der HTML-Grundgerüst der ersten Ebene ist simpel aufgebaut. Es wurden nacheinander Elemente eingefügt, die in bestimmten Reihenfolge sichtbar werden sollten.
<div class="sternenhimmel-bild"> <!-- Sterne, die sich von unten nach oben und umgekehrt bewegen, insperiert von https://www.youtube.com/watch?v=aywzn9cf-_U --> <div id="stars"></div> <div id="stars2"></div> <div id="stars_2"></div> <div id="stars2_2"></div> <img id="astro" src="Bilder/astro.png"> <div> <a href="game.html"><button id="button1"></button></a> </div> <img id="spaceship" src="Bilder/futurama.png"> </div>
Auf der ersten Ebene befindet sich der Javascript-Code in der HTML-Datei . Dank GSAP kann man den Code für die schwebende Bewegung des Astronauten kurz halten. Mit GSAP wird auch das Erscheinen des Buttons animiert. Man darf nicht vergessen den MotionPathPlugin zu registrieren, sonst wird motionPath nicht ausgeführt. Mit gsap.timeline() geben wir die Reihenfolge der Animationen vor. Zuerst soll der Button und dann der Astronaut erscheinen. Der Astronaut soll daraufhin im Kreis um den Button schweben und dann stehen bleiben.
<script> //Plugin registrieren gsap.registerPlugin(MotionPathPlugin); //#astro gsap.timeline().from('#button1', {opacy: 0.05, scale: 0, rotation: 10, ease:'back', duration: 1.5, }) .from("#astro", {duration: 2, x: 200, opacity: 0, scale: 0.5, rotation: 10}) .to("#astro", { duration: 13, motionPath:{ path: [{x:220, y:440, scale:0.5, rotation:10}, {x:780, y:240,scale: 0.7, rotation:-30}, {x:470, y:10, scale:1, rotation:5}] } }); MotionPathHelper.create("#astro") </script>
CSS
Der Inhalt des Buttons wurde mit dem Pseudoelement ::after eingefügt. Das Ziel war es, den Text beim Hovern über den Button zu ändern. Beim Hovern sollen auch die Hindergrundfarbe und der Schatten sich verändern. Da :hover::after nur den Inhalt (also Text) ansteuert, muss man noch einmal :hover benutzen.
#button1 { display:inline-block; box-sizing: border-box; min-width: 11em; border-width: 4px; border-radius: 20px; background-color: rgba(204, 102, 255, 0.55); border-color: rgba(239, 204, 255); text-align: center; font-size: 31px; padding: 25px; position: absolute; top: 53%; left: 50%; transform: translate(-50%, -50%); box-shadow: 0 8px 16px 0 rgba(0,0,0,0.3), 0 6px 20px 0 rgba(0,0,0,0.29); } #button1::after{ content:'Eine Runde spielen?'; } #button1:hover::after{ content:"Los geht's!"; } #button1:hover { background-color: rgba(251, 41, 253, 0.9); cursor: pointer; box-shadow: 0 8px 70px 0 rgba(255,250,250,0.3), 0 8px 70px 0 rgba(255,250,250,0.3), 0 8px 70px 0 rgba(255,250,250,0.3), 0 8px 70px 0 rgba(255,250,250,0.3); transition: background-color 2s ease-out; }
Um den Hintergrund dynamischer wirken zu lassen, werden zwei Sterne-Animationen hinzugefügt. Die Idee der Erstellung der Sterne mit box-shadow und sie dann in Bewegung zu setzten stammt von diesem Youtube-Video . Im Spiel bewegen sich die Sterne sowohl von oben nach unten als auch von untern nach oben. Ermöglicht wird es durch zwei Keyframes.
/* von unten nach oben */ #stars { width: 3px; height: 3px; border-radius: 50%; background:transparent; animation: animStar 50s linear infinite; box-shadow: 789px 1341px #fff, 364px 52px #fff, 353px 596px #fff, 1412px 376px #fff, 451px 625px #fff, 521px 1931px #fff, 1087px 1871px #fff, 36px 1546px #fff, 132px 934px #fff, 1698px 901px #fff, 1418px 664px #fff, 1448px 1157px #fff, 1084px 232px #fff, 347px 1776px #fff, 1222px 343px #fff; /* 15*/ } #stars2 { width: 6px; height: 6px; border-radius: 50%; background:#bb33ff; animation: animStar 100s linear infinite; box-shadow: 1448px 320px #00ffff, 1775px 1663px #00ffff, 332px 1364px #00ffff, 878px 340px #00ffff, 569px 1832px #00ffff, 1422px 1684px #00ffff, 1946px 1907px #00ffff, 121px 979px #00ffff, 1044px 1069px #00ffff, 463px 381px #00ffff, 423px 112px #ffffb3, 523px 1179px #ffffb3, 779px 654px #ffffb3, 1398px 694px #ffffb3, 1085px 1464px #ffffb3; } @keyframes animStar{ from { transform: translateY(0px); } to { transform: translateY(-1000px); } } /* von oben nach unten*/ #stars_2 { width: 3px; height: 3px; border-radius: 50%; background:transparent; animation: animStar_2 50s linear infinite; box-shadow: 779px 1331px #fff, 324px 42px #fff, 303px 586px #fff, 1312px 276px #fff, 451px 625px #fff, 521px 1931px #fff, 1087px 1871px #fff, 36px 1546px #fff, 132px 934px #fff, 1698px 901px #fff, 1418px 664px #fff, 1448px 1157px #fff, 1084px 232px #fff, 347px 1776px #fff, 1722px 243px #fff; /* 15*/ } #stars2_2 { width: 6px; height: 6px; border-radius: 50%; background:#bb33ff; animation: animStar_2 100s linear infinite; box-shadow: 1448px 320px #9acd32, 1775px 1663px #9acd32, 332px 1364px #9acd32, 878px 340px #9acd32, 569px 1832px #9acd32, 1422px 1684px #9acd32, 1946px 1907px #9acd32, 121px 979px #9acd32, 1044px 1069px #9acd32, 463px 381px #9acd32, 423px 112px #ffffb3, 523px 1179px #ffffb3, 779px 654px #ffffb3, 1398px 694px #ffffb3, 1085px 1464px #ffffb3; } @keyframes animStar_2{ from { transform: translateY(-1000px); } to { transform: translateY(1000px); } }
Die Animation des Raumschiff ist ähnlich wie bei den Sternen umgesetzt. Die Keyframes teilen wir dieses Mal in Prozente ein.
#spaceship{ animation-delay: 4s; animation: futurama 10s linear forwards; } @keyframes futurama{ 0% { max-width: 1%; transform: translateX(-20em); } 25% { max-width: 4%; } 100% { transform: translateX(-10em); max-width: 14%; } }
Zweite Ebene
HTML
Für die zweite Ebene, also für die Datei game.html , fügen wir im Head-Bereich zusätzlich zu GSAP ein Script von LottieFiles hinzu . Dieser ermöglicht später eine Lottie-Animation zu starten.
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.7.0/gsap.min.js"></script> <!--Green Sock--> <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.7.0/MotionPathPlugin.min.js"></script> <!--Green Sock--> <script src="https://unpkg.com/@lottiefiles/lottie-player@latest/dist/lottie-player.js"></script> <!-- Lottiefiles-->
Der HTML-Code ist auch hier unkompliziert gestaltet. Der Div mit der class=”grid” ist der wichtigste Bestandteil dieses HTML-Codes. Über ihn erzeugt die app.js-Datei die Memory-Karten .
Bei der Lottie-Animation handelt es sich um eine Konfetti-Animation, die nur beim Gewinnen des Spiels über Javascript aktiviert wird. Sie ist in der HTML-Datei über den <lottie-player>-Tag eingebunden und über CSS angepasst.
<!-- Memory-Spiel --> <div class="grid"></div> <!-- Memorykarten werden über app.js erzeugt --> <img id="gameAstro" src="Bilder/astro.png"/> <!-- Astronaut --> <img id='Traene' src='Bilder/water.svg'> <!-- erscheint, wenn verloren --> <div id="score" > <h3> Du hast 30 Sekunden Zeit <br/> Dein Score:<span id="result"></span> </h3> <p id="message"></p> </div> <!--Spiel gewonnen--> <lottie-player src="https://assets6.lottiefiles.com/packages/lf20_u4yrau.json" id='confetti' background="transparent" speed="1.15" loop autoplay></lottie-player> <div id='gewonnen'><h1>6/6! Gut gemacht!</h1></div> <!--Spiel verloren--> <div id="Schande"><p>Schade!</p></div> <div id='verloren'><h1>Verloren</h1></div> <!-- "Neues Spiel" Button --> <a href="javascript:location.reload()"><div id="nochMal">Neues Spiel</div></a>
Javascript
Der grundlegende JS-Code für das Memory-Spiel stammt aus diesem YouTube-Video. Der Code wurde modifiziert und an eigene Bedürfnisse angepasst.
Das Programm führt folgende Schritte aus: createBoard() kreiert innerhalb des .grid-Divs Child-Elemete, die für die Erstellung der Karten benötigt werden. Timer löst nach 30 Sekunden die Funktion verloren() aus, die die Elemente .grid und und scoreDiv entfernt und stattdessen andere Elemente aktiviert. Die Funktion checkForMatches() erfüllt gleich mehrere Aufgaben: Sie vergleicht die angeklickten Karten und je nach Auswahl gibt einen Text in der Sprechblase aus. Wenn sie feststellt, dass alle Paare gefunden wurden, löscht bzw. deaktiviert sie bestimmte Elemente und blendet neue ein. Die flipCard()-Funktion sorgt dafür, dass die Karten “umgedreht” werden und man das verborgene Bild sieht. Es können nur zwei Karten gleichzeitig umgedreht werden.
document.addEventListener('DOMContentLoaded', () => { //Kartenauswahl const cardArray = [ { name: 'mond', img: 'Bilder/mond.svg' }, { name: 'alien', img: 'Bilder/alien.svg' }, { name: 'ufo', img: 'Bilder/ufo.svg' }, { name: 'rocket', img: 'Bilder/rocket.svg' }, { name: 'space', img: 'Bilder/space.svg' }, { name: 'sun', img: 'Bilder/sun.svg' }, { name: 'mond', img: 'Bilder/mond.svg' }, { name: 'alien', img: 'Bilder/alien.svg' }, { name: 'ufo', img: 'Bilder/ufo.svg' }, { name: 'rocket', img: 'Bilder/rocket.svg' }, { name: 'space', img: 'Bilder/space.svg' }, { name: 'sun', img: 'Bilder/sun.svg' } ]; cardArray.sort( () => 0.5 - Math.random()); const grid = document.querySelector('.grid'); const resultDisplay = document.querySelector('#result'); const wordCloud = document.querySelector('#message'); const scoreDiv = document.querySelector('#score'); const gameAstro = document.querySelector('#gameAstro'); let cardsChosen = []; let cardsChosenId = []; let cardsWon = []; //create your board function createBoard() { for (let i = 0; i < cardArray.length; i++) { const cardDiv = document.createElement('div') //Um die karten herum einen Div erstellen cardDiv.setAttribute('class', 'imgDiv') //Um die karten herum einen Div erstellen const card = document.createElement('img') card.setAttribute('class', 'Spielbilder') card.setAttribute('src', 'Bilder/logo.svg') card.setAttribute('data-id', i) card.addEventListener('click', flipCard) grid.appendChild(cardDiv).appendChild(card) //Um die Karten herum einen Div erstellen } }; //Timer. Falls das Spiel mach 30 Sekunden nicht gewonnen wurde, wird das Spiel abgebrochen var timer = setTimeout( function(){verloren();}, 30000); function verloren(){ grid.remove() scoreDiv.remove() gameAstro.style.marginTop = '17em' gameAstro.style.marginLeft = '7em' gameAstro.style.width = '10em' document.querySelector('#verloren').style.display = 'block' document.querySelector('#Schande').style.display = 'block' gsap.timeline().from("#gameAstro", {duration: 2, y: 200, opacity: 0, scale: 0, rotation: 180, ease:'back'}); document.querySelector('#Traene').style.display = 'block' }; //check for matches function checkForMatch() { const cards = document.querySelectorAll('img') const optionOneId = cardsChosenId[0] const optionTwoId = cardsChosenId[1] if(optionOneId == optionTwoId) { cards[optionOneId].setAttribute('src', 'Bilder/logo.svg') cards[optionTwoId].setAttribute('src', 'Bilder/logo.svg') wordCloud.textContent = 'Du hast die gleiche Karte angeklickt!' setTimeout(function(){ wordCloud.innerHTML=''; }, 2000); //Nach 2000 ms den text aus der Sprechblase entfernen } else if (cardsChosen[0] === cardsChosen[1]) { wordCloud.textContent = 'Du hast ein Paar gefunden!' setTimeout(function(){ wordCloud.innerHTML=''; }, 2000); //Nach 2000 ms den text aus der Sprechblase entfernen cards[optionOneId].removeAttribute('src', 'Bilder/logo.svg') cards[optionTwoId].removeAttribute('src', 'Bilder/logo.svg') cards[optionOneId].removeEventListener('click', flipCard) cards[optionTwoId].removeEventListener('click', flipCard) cardsWon.push(cardsChosen) } else { cards[optionOneId].setAttribute('src', 'Bilder/logo.svg') cards[optionTwoId].setAttribute('src', 'Bilder/logo.svg') wordCloud.textContent = 'Schade, versuch es noch mal' setTimeout(function(){ wordCloud.innerHTML=''; }, 2000); //Nach 2000 ms den text aus der Sprechblase entfernen } cardsChosen = [] cardsChosenId = [] resultDisplay.textContent = cardsWon.length var audio = new Audio('ta-da.mp3'); if (cardsWon.length === cardArray.length/2) { //Falls das Spiel innerhalb von 30 Sekunden gewonnen: clearTimeout(timer) //Timer aussetzen, damit das Spiel nicht abgebrochen wird audio.play(); grid.remove() scoreDiv.remove() gameAstro.style.marginTop = '17em' gameAstro.style.marginLeft = '7em' gameAstro.style.width = '10em' document.querySelector('img#gameAstro').style.animationName = 'none' //Animation entfernen, damit GreenSock-Animation abgespilt werden kann document.querySelector('#gewonnen').style.display = 'block' document.querySelector('#confetti').style.display = 'block' //Freudiges Springen gsap.timeline().from("#gameAstro", {duration: 2, y: 200, opacity: 0, scale: 0, rotation: 180, ease:'back'}) .to('#gameAstro', { duration: .25, y: -50, repeat: -1, yoyo: true, ease: "sine.inOut", autoRound: false}); } }; //flip your card function flipCard() { let cardId = this.getAttribute('data-id') cardsChosen.push(cardArray[cardId].name) cardsChosenId.push(cardId) this.setAttribute('src', cardArray[cardId].img) if (cardsChosen.length === 2) { setTimeout(checkForMatch, 300) } }; createBoard(); });
css
Das Aussehen der Karten wird über die Klassen .grid , .imgDiv und .Spielbilder bestimmt. Die Klasse .grid ist für die Verteilung der Karten auf dem Bildschirm zuständig. Hier empfielt es sich, die Einstellung display:flex zu wählen. Es ist der leichteste Weg ist, die Karten zu positionieren. Für die richtige Reihung der Karten ist es wichtig, dass die Breite des .grid-Containers und die Breite der einzelnen Karten, die von .imgDiv gesteuert werden, auf einander abgestimmt sind.
/* Während des Spiels*/ .grid{ display: flex; flex-wrap: wrap; width: 30em; margin-right: 5em; margin-top: 3em; float:right; } .imgDiv { background-color: rgba(204, 102, 255, 0.55); border-radius: .25em; width:7em; height:12em; margin:.25em; box-shadow: rgba(50, 50, 93, 0.65) 0px 50px 100px -20px, rgba(0, 0, 0, 0.6) 0px 30px 60px -30px, rgba(10, 37, 64, 0.65) 0px -2px 6px 0px inset; } .imgDiv:hover { box-shadow: rgba(153, 255, 102, 0.65) 0px 50px 100px -20px, rgba(0, 0, 0, 0.6) 0px 30px 60px -30px, rgba(153, 255, 102, 0.65) 0px -2px 6px 0px inset; } img.Spielbilder { max-height:100%; max-width:100%; padding:1em; }
Dritte Ebene
Die einzelnen Elemente der dritten Ebene werden je nach Ergebnis des Spieles durch den JS-Code aktiviert. Aus diesem Grund sind sie alle mit display:none versehen. Der JS-Code verändert sie dann mit document.querySelector zu display:block.
/* Spiel gewonnen*/ h1 { font-size: 5em; } #gewonnen { animation: Schweben 5s linear easy-in; display: none; position: absolute; left: 53em; top: 20em; z-index: 10; } #confetti{ display: none; width:75em; height: auto; float: right; }
/* Spiel verloren*/ #verloren { display: none; animation: Schweben 6s linear infinite; position: absolute; left: 53em; top: 20em; z-index: 10; float:left; } #Schande { display: none; animation: Schweben 13s linear infinite; clip-path: polygon(0% 0%, 100% 0%, 100% 75%, 69% 75%, 5% 100%, 27% 76%, 0% 75%); width: 16em; height:9em; margin-top: 13em; margin-left:4em; padding: 1em; float: left; background-color: rgba(204, 102, 255, 0.55); } #Schande p { font-size: 2em; text-align: center; } #Traene { display: none; width: 0em; position:absolute; left: 12.5em; top: 22em; z-index: 15; animation: traene 3s linear infinite; transition: ease-in; animation-delay: 3s; } @keyframes traene{ from { transform: translateY(0em); width: 0.5em; } to { transform: translateY(13em); width: 3em; } to { transform: translateY(15em); } to { transform: translateY(16em); opacity: 60%; } to { transform: translateY(17em); opacity: 7%; } }
Sowohl in der zweiten als auch in der dritten Ebene werden alle sichtbaren Elemente (bis auf die Memory-Karten) mit einer Schwebeanimation versehen, um den Eindruck zu vermitteln, dass die Objekte wie im echten Weltall schweben.
/*Alles soll schweben*/ @keyframes Schweben { 0% { transform: translatey(0px); } 25%{ transform: translatex(-5px); } 50% { transform: translatey(-10px); } 100% { transform: translatey(0px); } }
Dieser Beitrag ist im Studiengang Informationsmanagement an der Hochschule Hannover im Rahmen des Kurses Entwicklung von Multimediasystemen (Sommersemester 2021, Amy Linh Hoang, Prof. Dr.-Ing. Steinberg) entstanden. Verwendete Techniken sind HTML5, CSS3 und JavaScript. Die besten Tutorials stellen wir Euch hier in den nächsten Wochen nach und nach vor.