CodeNewbie Community 🌱

Cover image for Building a Robot Friend from a McDonald's Toy
Álvaro Montoro
Álvaro Montoro

Posted on • Originally published at alvaromontoro.com

Building a Robot Friend from a McDonald's Toy

Note: you don't need the McDonald's toy to build this game and play with it, but having the toy will add an extra fun factor to the project :)

The Toy

The other day, my wife got Happy Meals at McDonald's for our kids, and I hate to admit it, but it was me, the one who enjoyed the toy the most.

It was a simple toy. A silly one: a robot looking thing with a smiley face (I don't even know what movie/game the promotion was about), a rotating handle on one side, and a hole at the bottom:

Photos of the toy's front and bottom, showing a hole

At first, I thought it was a glow-in-the-dark toy... it wasn't.
 

There was more to the toy: it became "interactive" with the McDonald's app. So, I downloaded the app and tested it. The functionality was simple:

  1. Place the toy on top of the phone (in a specific position)
  2. Dim the room lights
  3. Select among the options that popped up
  4. And the robot "came to life" so you could interact with it.

Of course, the robot didn't come to life. In reality, the toy is translucent with a hole at the bottom and some mirrors(?) inside, so by using the lights correctly and placing the toy in a specific location on the phone, the app could reflect images into the toy's screen/face.

I liked it. It had some Tamagotchi mixed with Big Hero 6's Baymax vibes. It was cute, ingenious, and simple... So simple, it was a pity that it was limited to just a few ad-peppered options from the restaurant's app. And the basic idea seemed reasonably easy to develop. So, what if...?

First version

I opened a browser and went to Codepen. I quickly typed four HTML elements on the editor:

<div class="face">
  <div class="eye"></div>
  <div class="eye"></div>
  <div class="mouth"></div>
</div>
Enter fullscreen mode Exit fullscreen mode

And then added some basic styles. Nothing 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;
}
Enter fullscreen mode Exit fullscreen mode

Note: I picked the in unit (inches), so it was absolute to all devices. I had to play a little with the actual value: I started with 1 inch, but it seemed a bit little; I moved up to 1.5 inches, but that was too big. In the end, I settled for 1.25 inches, but probably it could be a little bit smaller and be a better fit for this particular toy.

It took 5-10 minutes in total. It was not interactive, and it was not animated, but the results looked (on the toy) similar to those on the app:

Photography of the toy in a dark room. It is placed on top of a tablet, and a smiling face is illuminating it

Well, hello there!
 

First glitches and corrections

Who would have said that something so simple could already have some issues? But it did! A few things caught my attention from the beginning:

  • The picture was flipped
  • The drawing scaled poorly on mobile
  • The browser bar was too bright

I assumed the first one was due to the use of mirrors inside the toy, which would make the left side on the screen be the right side on the toy, and vice versa. While this was not going to be a big issue while displaying a face, it could be problematic later if I wanted to show text or a picture.

The solution was to flip the face by using a scaleX transform with value -1:

.face {
  ...
  transform: scaleX(-1)
}
Enter fullscreen mode Exit fullscreen mode

Specifying a viewport width in the head solves the poor escalation on mobile. It was easy with the viewport meta-tag:

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

Finally, the browser top bar was too bright. This wouldn't usually be a problem, but considering that the toy requires dimming the lights to see it better, it is an issue because it can become a distraction.

Luckily, the color of that bar can be specified with the theme-color meta-tag:

<meta name="theme-color" content="#000" />
Enter fullscreen mode Exit fullscreen mode

The browser top bar was now black (the same color as the body background), making it more fluid with the page and removing the annoying difference.

First animations

At that point, the robot was too basic. Animations would make it likable and expressive, and CSS was the language for the job!

I did two animations at first: eyes blinking and mouth talking.

There are many ways to make the eyes open and close (blink or wink). An easy one is changing the opacity to 0 and then putting it back to 1. That way, the eyes will disappear for a short time and then come back again, which gives the blinking impression.

@keyframes blink {
  0%, 5%, 100% { opacity: 1; }
  2% { opacity: 0; }
}
Enter fullscreen mode Exit fullscreen mode

It is a basic animation that could also be done by changing the height of the yes to zero and then back to the original size (but I'm not a big fan of that method because it looks fake to me). A better one could be animating clip-path. Browsers allow for transitions and animations of the clip-path as long as the number of points matches.

@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%);
  }
}
Enter fullscreen mode Exit fullscreen mode

I didn't go with the clip-path option because it would look weird if I wanted to animate the eyes later to show different expressions.

Yet another option would be to change the height of the eyes to 0 then back to their regular size. However, that would give the impression of a blink (and that's the option I finally went with, although it may not be the best one.)

Then, I also simulated the toy talking by animating the mouth opening and closing. I did it by changing the mouth size to 0 and reverting it to its original size:

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

.mouth {
  ...
  animation: talk 0.5s infinite;
}
Enter fullscreen mode Exit fullscreen mode

Making the toy talk

So far, everything has been HTML and CSS. But using JavaScript and the Speech Synthesis API, the toy will be able to talk. I had already done something similar creating a teaching assistant or a speech-enabled searchbox, so I had some experience with it.

I added this talk function that would take a string and the browser would read it:

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

I added an optional language parameter if I wanted to use the toy to speak in Spanish or another language in the future (multilingual toys and games for the win!).

One important thing to consider is that the speech synthesis speak() requires a user activation to work (at least it does in Chrome). This is a security feature because sites and developers were abusing it, becoming a usability issue.

This means that the user/player will have to interact with the game to make the robot talk. That could be a problem if I wanted to add a greeting (there are ways of going around it), but it shouldn't be an issue for the rest of the game as it will require user interaction.

There's one more detail: there is an animation to make the robot's mouth move. Wouldn't it be great to apply it only when it's talking? That is actually pretty simple too! I added the animation to the .talking class and add/remove the class when speech starts/ends respectively. These are the changes to the talk function:

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);
}
Enter fullscreen mode Exit fullscreen mode

Basic game

The robot is at the top of the page, but it doesn't do much. So it was time to add some options! The first thing was including a menu for the player to interact. The menu will be at the bottom of the page, leaving enough space for the toy and the menu to not mess with each other.

<div id="menu" class="to-bottom">
  <button>Jokes</button>
</div>
Enter fullscreen mode Exit fullscreen mode
.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;
}
Enter fullscreen mode Exit fullscreen mode

The result looks a bit dated (sorry, I'm not much of a designer), but it works for what I want:

Screenshot of a button that reads 'Jokes.' It has some shadows and bevel to give a 3D-look

As for the jokes, I put them in an array of arrays (sorry, Data Structures professors) for simplicity. Then created a function that randomly picks an element within the parent array and reads the elements adding a short pause in between (using setTimeout() for the delayed response. Otherwise, I would need an additional user action to continue reading).

The code looks like this:

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);
}
Enter fullscreen mode Exit fullscreen mode

As you may have noticed, I added a couple of extra functions: show() and hide() that add and remove the class "hidden," so I can animate them with CSS later and remove them from the view frame (I wanted to prevent users from clicking twice on the button.) Their code is not essential for this tutorial, but you can review it at the demo on CodePen.

Making the game more accessible

So far, the game is basic and usable. The user clicks on an option, and the robot replies using voice. But what happens when the user is deaf? They will miss the whole point of the game because it is all spoken!

A solution for that would be to add subtitles every time the robot talks. That way, the game will be accessible to more people.

In order to do this, I added a new element for subtitles, and expanded the talk function a little bit more: display subtitles when speech start and hide them on speech end (similar to how the mouth movement happens):

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);
}
Enter fullscreen mode Exit fullscreen mode

More options

Extending the game is easy: add more options to the menu and a function to handle them. I did add two more options: one with trivia questions (spoken) and another with flag questions (also trivia, but this time with images).

Both work more or less in the same way:

  • Display a question in text form
  • Display four buttons with potential answers
  • Show the results after picking an option

The main difference is that the flag question will always have the same text, and the flag will be displayed on the robot's face (as something different.) But in general, the functionality of both options is similar, and they shared the same HTML elements, just interacting slightly differently in JavaScript.

The first part was adding the HTML elements:

<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>
Enter fullscreen mode Exit fullscreen mode

Most of the styling is already in place, but some additional rules need to be added (see the full demo for the complete example). All the HTML elements are empty because they are populated with the values of the questions.

And for that, I used the following JS code:

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");
}
Enter fullscreen mode Exit fullscreen mode

The way the incorrect answers are placed on the buttons is far from ideal. They are always in the same order! This means that if the user pays a little bit of attention, they can find out which one is correct just by looking at the answers. Luckily for me, it's a game for kids, so they probably won't realize the pattern... hopefully.

The flag version presents some accessibility challenges. What if the players are blind? Then they cannot see the flag, and the game won't make sense to them. The solution was adding some visually hidden (but accessible for a screen reader) text describing the flags and placed right after the question.

What's next?

I built a clone of the McDonald's game using their toy, and it took around a couple of hours. (McDonald's, hire me! :P) It is basic (not that the original is far more complex), but it can be expanded easily.

There's an initial problem: not everyone will have the toy to play with it. You can still play the game without it (I'll need to add an option to undo the character's flip), but it loses some of the fun factor. One option would be to create my toys. I'll need to explore it (what good is having a 3D printer if you cannot use it :P)

Another thing that would be cool to improve the game would be to add better transitions to the actions. For example, when it tells a knock-knock joke, add longer pauses in which the eyes move side to side with a large smile, like waiting in anticipation for the person's "Who's there?" Or a glitch animation when changing from the face to a different image like the flags. Those micro-interactions and animations go a long way.

Apart from that, the game is easily expandable. It would be easy to add new options to the menu and extend the game with more mini-games and fun if I made it more modular. The only limit is our imagination.

If you have kids (or students), this is an excellent project to develop with them: it is simple, it can be great if they are learning web development, it has a wow-factor that will impress them. At least, it worked with my kids.

Here is the entire demo with the complete code (which includes a bit more than the one explained here):

Top comments (0)