用麦当劳玩具打造机器人朋友

2025-06-08

用麦当劳玩具打造机器人朋友

注意:您不需要麦当劳玩具来构建和玩这个游戏,但拥有这个玩具会给项目增添额外的乐趣:)

玩具

有一天,我的妻子在麦当劳给我们的孩子买了开心乐园餐,我不愿意承认,但我却是最喜欢这个玩具的人。

那是个简单的玩具,有点傻乎乎的:一个机器人模样,带着一张笑脸(我甚至不知道这宣传片是关于哪部电影/游戏的),一侧有个旋转手柄,底部有个洞:

玩具正面和底部的照片,显示有一个洞

起初,我以为这是一个夜光玩具......但其实不是。
 

这个玩具还有更多功能:它可以与麦当劳的应用程序“互动”。所以我下载了应用程序并进行了测试。功能很简单:

  1. 将玩具放在手机顶部(特定位置)
  2. 调暗房间灯光
  3. 在弹出的选项中选择
  4. 机器人“活了过来”,因此你可以与它互动。

当然,这个机器人并没有活过来。实际上,这个玩具是半透明的,底部有一个洞,里面有一些镜子(?)。所以,只要正确使用灯光,并将玩具放在手机上的特定位置,应用程序就可以将图像反射到玩具的屏幕/脸上。

我喜欢它。它融合了电子宠物和《超能陆战队》大白的元素。它可爱、巧妙,而且简单……简单到只限于餐厅应用程序中几个带有广告的选项,这有点可惜。而且它的基本理念似乎很容易开发。那么,如果……呢?

第一个版本

我打开浏览器,进入Codepen。我在编辑器上快速输入了四个 HTML 元素:

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

然后添加了一些基本样式。没什么特别的:

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

注意:我选择了in单位(英寸),所以它对所有设备都是绝对的。我不得不对实际值进行一些调整:我一开始是 1 英寸,但感觉有点小;我增加到 1.5 英寸,但又太大了。最后,我选择了 1.25 英寸,但或许可以再小一点,更适合这个特定的玩具。

整个过程耗时 5-10 分钟。它既不是交互式的,也不是动画,但玩具上的效果看起来与应用程序上的效果类似:

暗室中的玩具照片。它被放置在平板电脑上,一张笑脸照亮了它。

你好!
 

第一个故障和修正

谁会想到这么简单的事情竟然会出问题?但事实确实如此!一开始,有几件事引起了我的注意:

  • 图片被翻转了
  • 绘图在移动设备上缩放效果不佳
  • 浏览器栏太亮

我以为第一个问题是由于玩具内部使用了镜子,导致屏幕左侧显示的内容与玩具右侧显示的内容一致,反之亦然。虽然在显示人脸时这不会造成什么大问题,但如果以后我想显示文字或图片,可能会有问题。

scaleX解决方案是使用值为 -1 的变换来翻转面

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

在头部指定视口宽度解决了移动设备上显示效果不佳的问题。使用viewportmeta 标签很容易:

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

最后,浏览器顶部的栏太亮了。这通常不是什么问题,但考虑到这个玩具需要调暗灯光才能看得更清楚,所以这确实是一个问题,因为它可能会分散注意力。

theme-color幸运的是,可以使用元标记指定该栏的颜色

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

浏览器顶部栏现在是黑色(与主体背景颜色相同),使其与页面更加流畅并消除了令人讨厌的差异。

第一个动画

那时,机器人还太基础了。动画可以让它更讨喜、更富有表现力,而 CSS 正是实现这个目标的语言!

我最初做了两个动画:眨眼和嘴巴说话。

有很多方法可以实现眼睛的睁开和闭合(眨眼或眨眼)。一个简单的方法是将不透明度改为 0,然后再改回 1。这样,眼睛会短暂地消失,然后再次出现,从而产生眨眼的效果。

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

这是一个基本的动画,也可以通过将“yes”的高度改为零,然后再恢复到原始大小来实现(但我不太喜欢这种方法,因为我觉得看起来很假)。更好的方法是使用 clip-path 动画。只要点数匹配,浏览器就允许 clip-path 的过渡和动画。

@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

我没有选择剪辑路径选项,因为如果我稍后想让眼睛动起来以显示不同的表情,它看起来会很奇怪。

另一个选择是将眼睛的高度改为 0,然后再恢复到正常大小。不过,这会给人一种眨眼的感觉(这是我最终选择的方案,虽然它可能不是最好的。)

然后,我还通过嘴巴张开和闭合的动画来模拟玩具说话。我将嘴巴大小改为 0,然后恢复到原来的大小:

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

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

让玩具说话

到目前为止,所有内容都是 HTML 和 CSS。但使用 JavaScript 和语音合成 API,这个玩具就能说话了。我之前做过类似的项目,比如创建教学助理支持语音的搜索框,所以在这方面有一些经验。

我添加了这个talk函数,它接受一个字符串,然后浏览器会读取它:

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

如果我想在未来使用该玩具说西班牙语或其他语言,我会添加一个可选language参数(多语言玩具和游戏获胜!)。

需要注意的一点是,语音合成功能speak()需要用户激活才能工作(至少在 Chrome 中是这样)。这是一个安全功能,但一些网站和开发者滥用了它,造成了可用性问题。

这意味着用户/玩家必须与游戏互动才能让机器人说话。如果我想添加问候语,这可能会有问题(虽然有办法解决这个问题),但对于游戏的其他部分来说应该不成问题,因为它需要用户互动。

还有一个细节:有一个动画可以让机器人的嘴巴动起来。如果只在机器人说话时应用这个动画,那岂不是很棒?其实也很简单!我把动画添加到.talking类中,并在机器人说话开始/结束时分别添加/删除类。以下是对函数的修改talk

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

基本游戏

机器人位于页面顶部,但它的功能不多。所以是时候添加一些选项了!首先要添加一个菜单,方便玩家互动。菜单将位于页面底部,留出足够的空间,避免玩具和菜单互相干扰。

<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

结果看起来有点过时(抱歉,我不太懂设计师),但它满足我的要求:

标题为“笑话”的按钮截图。它带有一些阴影和斜面,营造出 3D 效果。

至于笑话,为了简单起见,我把它们放在一个数组的数组里(抱歉,数据结构教授们)。然后创建了一个函数,它会随机从父数组中选择一个元素,并在读取元素时添加一个短暂的停顿(用于setTimeout()延迟响应)。否则,我需要额外的用户操作才能继续阅读。

代码如下:

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

您可能已经注意到,我添加了几个额外的函数:show()hide()添加和删除“hidden”类,以便我可以稍后使用 CSS 为它们设置动画并将它们从视图框架中删除(我想防止用户单击按钮两次。)它们的代码对于本教程来说不是必需的,但您可以在CodePen 上的演示中查看它。

让游戏更容易上手

到目前为止,这款游戏功能基本可用。用户点击选项,机器人就会用语音回复。但如果用户是聋哑人士怎么办?他们会完全错过游戏的重点,因为游戏全是语音的!

一个解决方案是每次机器人说话时添加字幕。这样,更多人就能玩上这款游戏了。

为了做到这一点,我添加了一个用于字幕的新元素,并talk进一步扩展了功能:在语音开始时显示字幕,在语音结束时隐藏字幕(类似于嘴部运动的方式):

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

更多选项

扩展游戏很简单:在菜单中添加更多选项,并添加一个处理这些选项的函数。我确实添加了两个选项:一个是琐事问答(语音),另一个是旗帜问答(也是琐事问答,但这次是图片问答)。

两者的工作原理大致相同:

  • 以文本形式显示问题
  • 显示四个带有潜在答案的按钮
  • 选择选项后显示结果

主要区别在于,标记问题始终具有相同的文本,并且标记将显示在机器人的脸上(作为不同的东西)。但总的来说,这两个选项的功能是相似的,它们共享相同的 HTML 元素,只是在 JavaScript 中的交互略有不同。

第一部分是添加 HTML 元素:

<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

大部分样式已经设置完毕,但需要添加一些额外的规则(完整示例请参阅完整演示)。所有 HTML 元素均为空,因为它们已填充问题的值。

为此,我使用了以下 JS 代码:

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

错误答案在按钮上的排列方式远非理想。它们的顺序总是一成不变!这意味着,如果用户稍加注意,只需查看答案就能找出哪个是正确的。幸运的是,这是一款儿童游戏,所以他们可能不会意识到其中的规律……但愿如此。

旗帜版本带来了一些可访问性挑战。如果玩家是盲人怎么办?他们看不到旗帜,游戏对他们来说就无法理解。解决方案是添加一些视觉上隐藏(但屏幕阅读器可以访问)的描述旗帜的文字,并将其放在问题之后。

下一步是什么?

我使用他们的玩具构建了一个麦当劳游戏的克隆版,大约花了几个小时。(麦当劳,雇用我!:P)它很基础(并不是说原版游戏要复杂得多),但可以轻松扩展。

一开始有个问题:不是每个人都有玩具可以玩。没有它你仍然可以玩游戏(我需要添加一个选项来撤销角色的翻转),但这会失去一些乐趣。一个选择是自己动手制作玩具。我需要探索一下(如果连3D打印机都用不了,那还有什么用呢 :P)

另一个值得改进的地方是增加更好的动作过渡效果。例如,在讲述“敲门”笑话时,可以增加更长的停顿,让玩家的眼睛在笑容的映衬下左右转动,就像在期待对方问“谁在那里?”一样;或者,在从面部切换到其他图像(例如旗帜)时,可以添加一些小故障动画。这些微交互和动画效果非常出色。

除此之外,这款游戏扩展起来也很容易。如果我能把它做得更模块化,就能轻松地在菜单中添加新选项,并添加更多小游戏和乐趣,让游戏更加丰富。唯一的限制就是我们的想象力。

如果你有孩子(或学生),这是一个非常适合和他们一起开发的项目:它很简单,如果他们正在学习 Web 开发,会很棒,而且它有令人惊叹的元素,会让他们印象深刻。至少,我的孩子们就成功了。

这是包含完整代码的整个演示(其中包含的内容比这里解释的要多一些):

鏂囩珷鏉ユ簮锛�https://dev.to/alvaromontoro/building-a-robot-friend-from-a-mcdonalds-toy-lgm
PREV
构建交互式表单
NEXT
动画无元素打字机