Byg en robotven fra et McDonalds-legetøj

Legetøjet

Forleden dag fik min kone Happy Meals på McDonald's til vores børn, og jeg hader at indrømme det, men det var mig, den der nød legetøjet mest.

Det var et simpelt legetøj. En dum ting:en robot, der ser ud med et smiley ansigt (jeg ved ikke engang, hvilken film/spil kampagnen handlede om), et roterende håndtag på den ene side og et hul i bunden:

Der var mere ved legetøjet:det blev "interaktivt" med McDonald's-appen. Så jeg downloadede appen og testede den. Funktionaliteten var enkel:

  1. Placer legetøjet oven på telefonen (i en bestemt position)
  2. Dæmp lyset i rummet
  3. Vælg blandt de muligheder, der dukkede op
  4. Og robotten "blev til live", så du kunne interagere med den.

Robotten kom selvfølgelig ikke til live. I virkeligheden er legetøjet gennemsigtigt med et hul i bunden og nogle spejle(?) indeni, så ved at bruge lysene korrekt og placere legetøjet et bestemt sted på telefonen, kunne appen reflektere billeder ind i legetøjets skærm/ansigt. .

Jeg kunne godt lide det. Den havde nogle Tamagotchi blandet med Big Hero 6's Baymax-vibes. Det var sødt, genialt og enkelt... Så enkelt, det var ærgerligt, at det kun var begrænset til et par annonce-pepperede muligheder fra restaurantens app. Og grundideen virkede rimelig let at udvikle. Så hvad nu hvis...?

Første version

Jeg åbnede en browser og gik til Codepen. Jeg skrev hurtigt fire HTML-elementer på editoren:

<div class="face">
  <div class="eye"></div>
  <div class="eye"></div>
  <div class="mouth"></div>
</div>

Og så tilføjet nogle grundlæggende stilarter. Ikke noget fancy:

html, body {
  background: #000;
}

.face {
  position: relative;
  width: 1.25in;
  height: 1.25in;
  overflow: hidden;
  margin: 5vh auto 0 auto;
  background: #fff;
  border-radius: 100% / 30% 30% 60% 60%;
}

.eye {
  position: absolute;
  top: 40%;
  left: 25%;
  width: 15%;
  height: 15%;
  background: black;
  border-radius: 50%;
}

.eye + .eye {
  left: 60%;
}

.mouth {
  position: absolute;
  top: 60%;
  left: 40%;
  width: 20%;
  height: 12%;
  background: black;
  border-radius: 0 0 1in 1in;
}

Det tog 5-10 minutter i alt. Det var ikke interaktivt, og det var ikke animeret, men resultaterne lignede (på legetøjet) dem på appen:

Første fejl og rettelser

Hvem ville have sagt, at noget så simpelt allerede kunne have nogle problemer? Men det gjorde det! Et par ting fangede min opmærksomhed fra begyndelsen:

  • Billedet blev vendt
  • Tegningen var dårligt skaleret på mobil
  • Browserbjælken var for lys

Jeg antog, at den første skyldtes brugen af ​​spejle inde i legetøjet, hvilket ville få venstre side på skærmen til at være højre side på legetøjet, og omvendt. Selvom dette ikke ville være et stort problem, mens jeg viser et ansigt, kunne det være problematisk senere, hvis jeg ville vise tekst eller et billede.

Løsningen var at vende ansigtet ved at bruge en scaleX transformer med værdi -1:

.face {
  ...
  transform: scaleX(-1)
}

Angivelse af en viewport-bredde i hovedet løser den dårlige eskalering på mobil. Det var nemt med viewport meta-tag:

<meta name="viewport" 
      content="width=device-width, initial-scale=1" />

Endelig var browserens øverste bjælke for lys. Dette ville normalt ikke være et problem, men i betragtning af at legetøjet kræver dæmpning af lyset for at se det bedre, er det et problem, fordi det kan blive en distraktion.

Heldigvis kan farven på den bjælke angives med theme-color meta-tag:

<meta name="theme-color" content="#000" />

Browserens øverste bjælke var nu sort (samme farve som kropsbaggrunden), hvilket gør den mere flydende med siden og fjerner den irriterende forskel.

Første animationer

På det tidspunkt var robotten for grundlæggende. Animationer ville gøre det sympatisk og udtryksfuldt, og CSS var sproget for jobbet!

Jeg lavede to animationer i starten:øjnene blinker og munden taler.

Der er mange måder at få øjnene til at åbne og lukke (blinke eller blinke). En nem er at ændre opaciteten til 0 og derefter sætte den tilbage til 1. På den måde vil øjnene forsvinde i kort tid og så komme tilbage igen, hvilket giver det blinkende indtryk.

@keyframes blink {
  0%, 5%, 100% { opacity: 1; }
  2% { opacity: 0; }
}

Det er en grundlæggende animation, der også kunne udføres ved at ændre højden på ja til nul og derefter tilbage til den oprindelige størrelse (men jeg er ikke en stor fan af den metode, fordi den ser falsk ud til mig). En bedre kunne være at animere clip-path. Browsere tillader overgange og animationer af klipstien, så længe antallet af point matcher.

@keyframes blink {
  0%, 10%, 100% { 
    clip-path: polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%);
  }
  5% { 
    clip-path: polygon(0% 50%, 100% 50%, 100% 50%, 0% 50%);
  }
}

Jeg valgte ikke klipstimuligheden, fordi det ville se mærkeligt ud, hvis jeg ville animere øjnene senere for at vise forskellige udtryk.

Endnu en mulighed ville være at ændre højden af ​​øjnene til 0 og derefter tilbage til deres normale størrelse. Det ville dog give indtryk af et blink (og det er den mulighed, jeg endelig gik med, selvom det måske ikke er den bedste.)

Derefter simulerede jeg også, at legetøjet taler ved at animere munden, der åbner og lukker. Jeg gjorde det ved at ændre mundstørrelsen til 0 og vende den tilbage til dens oprindelige størrelse:

@keyframes talk {
  0%, 100% { height: 12%; }
  50% { height: 0%; }
}

.mouth {
  ...
  animation: talk 0.5s infinite;
}

Få legetøjet til at tale

Indtil videre har alt været HTML og CSS. Men ved at bruge JavaScript og Speech Synthesis API, vil legetøjet være i stand til at tale. Jeg havde allerede lavet noget lignende ved at oprette en lærerassistent eller et taleaktiveret søgefelt, så jeg havde lidt erfaring med det.

Jeg tilføjede denne talk funktion, der ville tage en streng, og browseren ville læse den:

function talk(sentence, language = "en") {
  let speech = new SpeechSynthesisUtterance();
  speech.text = sentence;
  speech.lang = language;
  window.speechSynthesis.speak(speech);
}

Jeg tilføjede en valgfri language parameter, hvis jeg ville bruge legetøjet til at tale spansk eller et andet sprog i fremtiden (flersproget legetøj og spil for at vinde!).

En vigtig ting at overveje er, at talesyntesen speak() kræver en brugeraktivering for at virke (det gør det i hvert fald i Chrome). Dette er en sikkerhedsfunktion, fordi websteder og udviklere misbrugte den og blev et problem med brugervenlighed.

Det betyder, at brugeren/spilleren bliver nødt til at interagere med spillet for at få robotten til at tale. Det kunne være et problem, hvis jeg ville tilføje en hilsen (der er måder at komme rundt på), men det burde ikke være et problem for resten af ​​spillet, da det vil kræve brugerinteraktion.

Der er en detalje mere:Der er en animation, der får robottens mund til at bevæge sig. Ville det ikke være fantastisk kun at anvende det, når det taler? Det er faktisk også ret simpelt! Jeg føjede animationen til .talking klasse og tilføj/fjern klassen, når talen starter/slutter hhv. Dette er ændringerne til talk fungere:

function talk(sentence, language = "en-US") {
  let speech = new SpeechSynthesisUtterance();
  speech.text = sentence;
  speech.lang = language;
  // make the mouth move when speech starts
  document.querySelector(".mouth").classList.add("talking");
  // stop the mouth then speech is over
  speech.onend = function() {
    document.querySelector(".mouth").classList.remove("talking");
  }
  window.speechSynthesis.speak(speech);
}

Grundlæggende spil

Robotten er øverst på siden, men den gør ikke så meget. Så det var på tide at tilføje nogle muligheder! Den første ting var at inkludere en menu, hvor spilleren kunne interagere. Menuen vil være nederst på siden, hvilket giver plads nok til, at legetøjet og menuen ikke roder med hinanden.

<div id="menu" class="to-bottom">
  <button>Jokes</button>
</div>
.to-bottom {
  position: fixed;
  left: 0;
  bottom: 5vh;
  width: 100%;
  display: flex;
  align-items: flex-end;
  justify-content: center;
}

button {
  margin: 0.5rem;
  min-width: 7rem;
  height: 3.5rem;
  border: 0;
  border-radius: 0.2rem 0.2rem 0.4rem 0.4rem;
  background: linear-gradient(#dde, #bbd);
  border-bottom: 0.25rem solid #aab;
  box-shadow: inset 0 0 2px #ddf, inset 0 -1px 2px #ddf;
  color: #247;
  font-size: 1rem;
  text-shadow: 1px 1px 1px #fff;
  box-sizing: content-box;
  transition: border-bottom 0.25s;
  font-family: Helvetica, Arial, sans-serif;
  text-transform: uppercase;
  font-weight: bold;
}

button:active {
  border-bottom: 0;
}

Resultatet ser lidt forældet ud (beklager, jeg er ikke særlig designer), men det virker til det, jeg ønsker:

Med hensyn til vittighederne, satte jeg dem i en række arrays (beklager, Data Structures-professorer) for nemheds skyld. Derefter oprettede en funktion, der tilfældigt vælger et element i det overordnede array og læser elementerne ved at tilføje en kort pause imellem (ved hjælp af setTimeout() for det forsinkede svar. Ellers ville jeg have brug for en ekstra brugerhandling for at fortsætte med at læse).

Koden ser sådan ud:

const jokes = [
  ["Knock, knock", "Art", "R2-D2"],
  ["Knock, knock", "Shy", "Cyborg"],
  ["Knock, knock", "Anne", "Anne droid"],
  ["Why did the robot go to the bank?", "He'd spent all his cache"],
  ["Why did the robot go on holiday?", "To recharge her batteries"],
  ["What music do robots like?", "Heavy metal"],
  ["What do you call an invisible droid?", "C-through-PO"],
  ["What do you call a pirate robot?", "Argh-2D2"],
  ["Why was the robot late for the meeting?", "He took an R2 detour"],
  ["Why did R2D2 walk out of the pop concert?", "He only likes electronic music"],
  ["Why are robots never lonely?", "Because there R2 of them"],
  ["What do you call a frozen droid?", "An ice borg"]
];

function tellJoke() {
  // hide the menu
  hide("menu");
  // pick a random joke
  const jokeIndex = Math.floor(Math.random() * jokes.length);
  const joke = jokes[jokeIndex];
  // read the joke with pauses in between
  joke.map(function(sentence, index) {
    setTimeout(function() { talk(sentence); }, index * 3000);
  });
  // show the menu back again
  setTimeout("show('menu')", (joke.length - 1) * 3000 + 1000);
}

Som du måske har bemærket, tilføjede jeg et par ekstra funktioner:show() og hide() der tilføjer og fjerner klassen "skjult", så jeg kan animere dem med CSS senere og fjerne dem fra visningsrammen (jeg ville forhindre brugere i at klikke to gange på knappen). Deres kode er ikke afgørende for denne øvelse, men du kan gennemgå det i demoen på CodePen.

Gør spillet mere tilgængeligt

Indtil videre er spillet grundlæggende og brugbart. Brugeren klikker på en mulighed, og robotten svarer med stemmen. Men hvad sker der, når brugeren er døv? De vil gå glip af hele pointen med spillet, fordi det hele er talt!

En løsning på det ville være at tilføje undertekster, hver gang robotten taler. På den måde vil spillet være tilgængeligt for flere mennesker.

For at gøre dette tilføjede jeg et nyt element til undertekster og udvidede talk fungere lidt mere:vis undertekster, når tale starter, og skjul dem ved taleslut (svarende til, hvordan mundbevægelsen sker):

function talk(sentence, language = "en-US") {
  let speech = new SpeechSynthesisUtterance();
  speech.text = sentence;
  speech.lang = language;
  // show subtitles on speech start
  document.querySelector("#subtitles").textContent = sentence;
  document.querySelector(".mouth").classList.add("talking");
  speech.onend = function() {
    // hide subtitles on speech end
    document.querySelector("#subtitles").textContent = "";
    document.querySelector(".mouth").classList.remove("talking");
  }
  window.speechSynthesis.speak(speech);
}

Flere muligheder

Det er nemt at udvide spillet:Tilføj flere muligheder til menuen og en funktion til at håndtere dem. Jeg tilføjede to muligheder mere:en med trivia-spørgsmål (talt) og en anden med flag-spørgsmål (også trivia, men denne gang med billeder).

Begge fungerer mere eller mindre på samme måde:

  • Vis et spørgsmål i tekstform
  • Vis fire knapper med potentielle svar
  • Vis resultaterne, når du har valgt en mulighed

Den største forskel er, at flagspørgsmålet altid vil have den samme tekst, og flaget vil blive vist på robottens ansigt (som noget andet). Men generelt er funktionaliteten af ​​begge muligheder ens, og de delte de samme HTML-elementer , bare interagerer lidt anderledes i JavaScript.

Den første del var at tilføje HTML-elementerne:

<div id="trivia" class="to-bottom hidden">
  <section>
    <h2></h2>
    <div class="options">
      <button onclick="answerTrivia(0)"></button>
      <button onclick="answerTrivia(1)"></button>
      <button onclick="answerTrivia(2)"></button>
      <button onclick="answerTrivia(3)"></button>
    </div>
  </section>
</div>

Det meste af stylingen er allerede på plads, men nogle yderligere regler skal tilføjes (se den fulde demo for det komplette eksempel). Alle HTML-elementer er tomme, fordi de er udfyldt med værdierne af spørgsmålene.

Og til det brugte jeg følgende JS-kode:

let correct = -1;
const trivia = [
  {
    question: "Who wrote the Three Laws of Robotics",
    correct: "Isaac Asimov",
    incorrect: ["Charles Darwin", "Albert Einstein", "Jules Verne"]
  },
  {
    question: "What actor starred in the movie I, Robot?",
    correct: "Will Smith",
    incorrect: ["Keanu Reeves", "Johnny Depp", "Jude Law"]
  },
  {
    question: "What actor starred the movie AI?",
    correct: "Jude Law",
    incorrect: ["Will Smith", "Keanu Reeves", "Johnny Depp"]
  },
  {
    question: "What does AI mean?",
    correct: "Artificial Intelligence",
    incorrect: ["Augmented Intelligence", "Australia Island", "Almond Ice-cream"]
  },
];

// ...

function askTrivia() {
  hide("menu");
  document.querySelector("#subtitles").textContent = "";
  const questionIndex = Math.floor(Math.random() * trivia.length);
  const question = trivia[questionIndex];

  // fill in the data
  correct = Math.floor(Math.random() * 4);
  document.querySelector("#trivia h2").textContent = question.question;
  document.querySelector(`#trivia button:nth-child(${correct + 1})`).textContent = question.correct;
  for (let x = 0; x < 3; x++) {
    document.querySelector(`#trivia button:nth-child(${(correct + x + 1) % 4 + 1})`).textContent = question.incorrect[x];
  }

  talk(question.question, false);
  show('trivia');
}

function answerTrivia(num) {
  if (num === correct) {
    talk("Yes! You got it right!")
  } else {
    talk("Oh, no! That wasn't the correct answer")
  }
  document.querySelector("#trivia h2").innerHTML = "";
  document.querySelector(".face").style.background = "";
  hide("trivia");
  show("menu");
}

Måden de forkerte svar er placeret på knapperne er langt fra ideel. De er altid i samme rækkefølge! Det betyder, at hvis brugeren er lidt opmærksom, kan de finde ud af, hvilken der er rigtig ved blot at se på svarene. Heldigvis for mig er det et spil for børn, så de vil nok ikke indse mønsteret... forhåbentlig.

Flagversionen byder på nogle tilgængelighedsudfordringer. Hvad hvis spillerne er blinde? Så kan de ikke se flaget, og spillet vil ikke give mening for dem. Løsningen var at tilføje noget visuelt skjult (men tilgængelig for en skærmlæser) tekst, der beskrev flagene og placerede lige efter spørgsmålet.

Hvad er det næste?

Jeg byggede en klon af McDonald's-spillet ved hjælp af deres legetøj, og det tog omkring et par timer. (McDonald's, hyr mig! :P) Det er grundlæggende (ikke at originalen er langt mere kompleks), men den kan nemt udvides.

Der er et indledende problem:Ikke alle vil have legetøjet til at lege med det. Du kan stadig spille spillet uden det (jeg bliver nødt til at tilføje en mulighed for at fortryde karakterens flip), men det mister noget af det sjove. En mulighed ville være at skabe mit legetøj. Jeg bliver nødt til at udforske det (hvad nytter det at have en 3D-printer, hvis du ikke kan bruge den :P)

En anden ting, der ville være cool at forbedre spillet, ville være at tilføje bedre overgange til handlingerne. For eksempel, når den fortæller en bank-bank-joke, skal du tilføje længere pauser, hvor øjnene bevæger sig side til side med et stort smil, som at vente i forventning på personens "Hvem er der?" Eller en fejlanimation, når du skifter fra ansigtet til et andet billede som flagene. Disse mikro-interaktioner og animationer rækker langt.

Bortset fra det er spillet nemt at udvide. Det ville være nemt at tilføje nye muligheder til menuen og udvide spillet med flere minispil og sjov, hvis jeg gjorde det mere modulært. Den eneste grænse er vores fantasi.

Hvis du har børn (eller studerende), er dette et glimrende projekt at udvikle sammen med dem :det er enkelt, det kan være fantastisk, hvis de lærer webudvikling, det har en wow-faktor det vil imponere dem. Det virkede i hvert fald med mine børn.

Her er hele demoen med den komplette kode (som indeholder lidt mere end den, der er forklaret her):