Webbteknik 2

Laboration 6D

Drag and Drop - Spel: isländska djur

Dags att prova på något riktigt. Vi ska bygga ett matchningsspel där spelaren kan dra bilder på djur till deras namn.

Det mesta för spelet finns redan i arbetsmaterialet för labben, uppgiften är att implementera drag and drop logiken som skall få spelet att fungera.

Logiken kommer att fungera på följande sätt:

  • När spelet startar slumpas tre olika djur fram och visas som bilder.
  • Användaren kan dra bilderna till rätt isländska namn.
  • Om användaren släpper bilden på rätt namn, markeras det som rätt och bilden kan inte dras mer.
  • För att avgöra rätt eller fel, jämförs djurets engelska namn (lagrat i data-id på bilden) med namnets id (lagrat i data-id på namnrutan).

0 - Förberedelser

Skapa en ny mapp lab-6d och ladda ner startmaterialet (lab6d.zip). Packa upp filerna i mappen. Du ska ha fått de vanliga filerna samt två nya mappar img och audio med bilder och ljudfiler.

  • Öppna mappen i VS Code, och öppna index.html med live-server.
  • Kolla igenom html- och CSS-filerna för att förstå hur sidan är uppbyggd och vilka css-klasser som finns för visuell feedback.

1 - Initiera spelet

Spelet går ut på att användaren får tre olika slumpade djur som hen skall dra till deras isländska namn.

Följande saker behöver hända när spelet startar:

  1. Slumpa fram 3 olika djur och uppdatera användargränssnittet (de tre bildelementen) med dessa djur. Vi behöver sätta src (för att visa rätt bild) och data-id (för att i spel-logiken veta vilket djur det är).
  2. Göra bilderna dragbara. (dvs dragstart och dragend lyssnare).
  3. Göra namnrutorna till dropzoner. (dvs koppla dragover, drop, dragenter och dragleave lyssnare).

Slumpa och uppdatera bilderna

Vi har en array med engelska namn: ["cat", "cow", "dog", "horse", "pig", "sheep"]. Vi behöver välja 3 unika.

Tips Om du har svårt att tänka ut en lösning, börja med att göra på ett enklare sätt, t.ex. genom att bara ta de tre första namnen i arrayen eller slumpa tre namn utan att bry dig om att de kan bli dubbletter..

Därefter hämtar vi bildelementen, och för varje bild sätter vi src till rätt bildfil och data-id till djurets namn.

Förslag på lösning
/* solution.js */
const englishNames = ["cat", "cow", "dog", "horse", "pig", "sheep"];

function initGame() {
  const selectedNames = [];

  // 1. Slumpa 3 unika namn
  while (selectedNames.length < 3) {
    const randomIndex = Math.floor(Math.random() * englishNames.length);
    const randomName = englishNames[randomIndex];
    if (!selectedNames.includes(randomName)) {
      selectedNames.push(randomName);
    }
  }

  // 2. Uppdatera bilderna
  const images = document.querySelectorAll("#draggables-container img");
  for (let index = 0; index < selectedNames.length; index++) {
    const img = images[index];
    img.src = `img/${selectedNames[index]}.png`;
    img.dataset.id = selectedNames[index];

    // TODO: Koppla dragstart och dragend
  }

  // TODO: Koppla dropzoner (se nästa del)
}

initGame();

Spelet bör nu visa tre slumpade djurbilder när sidan laddas: Bidlspel

Gör bilderna dragbara

Lägg till event listeners för dragstart och dragend på varje bild i loopen där du uppdaterar bilderna.

Förslag på lösning Lägg till detta i slutet av loopen som uppdaterar bilderna:

    img.addEventListener("dragstart", onDragStart);
    img.addEventListener("dragend", onDragEnd);

Lägg till funktionerna onDragStart och onDragEnd och en variabel för att hålla reda på elementet som dras utanför initGame:

let draggedElement = null; // Hålla reda på vad vi drar

function onDragStart(e) {
  draggedElement = e.target;
  draggedElement.classList.add("dragging");
}

function onDragEnd(e) {
  draggedElement.classList.remove("dragging");
  draggedElement = null;
}

Gör namnrutorna till dropzoner

Dropzonerna (namnen) finns redan i HTML:en och har klassen .name-box.

Vi behöver loopa igenom dem och lägga till händelsehanterare för drag and drop. Gör detta i slutet av initGame.

  • I dragenter och dragleave vill vi ge visuell feedback genom att lägga till/ta bort klassen drag-over.
  • I dragover behöver vi anropa e.preventDefault() för att tillåta släpp.
Förslag på lösning

I slutet av initGame:

  const dropzones = document.querySelectorAll(".name-box");
  for (const zone of dropzones) {
    zone.addEventListener("dragover", onDragOver);
    zone.addEventListener("drop", onDrop);
    zone.addEventListener("dragenter", onDragEnter);
    zone.addEventListener("dragleave", onDragLeave);
  }

Utanför initGame, lägg till funktionerna:

function onDragOver(e) {
  // TODO: tillåt endast släpp om namnet inte redan är använt
  e.preventDefault();
}

function onDragEnter(e) {
  // TODO: ge endast visuell feedback om namnet inte redan är använt
  e.target.classList.add("drag-over");
}

function onDragLeave(e) {
  e.target.classList.remove("drag-over");
}

function onDrop(e) {
  // TODO: implementera drop-logik
}

3 - Drop-Logik

Nu till själva matchningen i onDrop.

Följande saker behöver hända när ett djur släpps på en zon:

  • Vi behöver jämföra det dragna djurets id med zonens id. Om de är lika är det rätt matchning.
    • djurets id finns som data-attribut på elementet som dras (draggedElement.dataset.id)
    • zonens id finns som data-attribut på namn-rutan (e.target.dataset.id)
    • Vid rätt matchning:
      • Lägg till klassen correct på zonen.
      • Inaktivera det dragna djuret (sätt draggable = false och lägg till klassen matched).
    • Vid fel matchning:
      • Lägg till klassen incorrect på zonen, och ta bort den efter 500ms (använd setTimeout).
  • Vi behöver ta bort drag-over från zonen, eftersom vi är klara med dragningen.
Förslag på lösning
function onDrop(e) {
  const zone = e.target;
  zone.classList.remove("drag-over");

  const draggedId = draggedElement.dataset.id;
  const zoneId = zone.dataset.id;

  if (draggedId === zoneId) {
    // Rätt matchning!
    zone.classList.add("correct");
    
    // Inaktivera det dragna elementet
    draggedElement.draggable = false;
    draggedElement.classList.add("matched");
  } else {
    // Fel matchning
    zone.classList.add("incorrect");
    setTimeout(() => {
      zone.classList.remove("incorrect");
    }, 500);
  }
}

Fixa till visuell feedback i dragover och dragenter

I onDragOver och onDragEnter, vill vi endast ge visuell feedback om zonen inte redan är markerad som korrekt (dvs har klassen correct).

Förslag på lösning
function onDragOver(e) {
  if (!e.target.classList.contains("correct")) {
    e.preventDefault();
  }
}
function onDragEnter(e) {
  if (!e.target.classList.contains("correct")) {
    e.target.classList.add("drag-over");
  }
}

4 - Utmaning: Lägg till ljud

För en extra utmaning kan du lägga till ljud som spelas upp när användaren gör en rätt matchning.

  • I arbetsmaterialet finns en mapp audio med ljudfiler för varje djur, t.ex. cat.mp3, dog.mp3 osv.
  • Lägg in ett audio-element någonstans i html-filen (du kan sätta display: none på det i CSS så att det inte syns).
  • Vid drop, sätt src på audio-elementet till rätt ljudfil och anropa play().