☰
用 Vue 打造抓娃娃机:状态机与动画交互实战
2026/10/17 3:04:55 网站建设 项目流程

最近用 Vue 重新实现了抓娃娃机的完整前端交互流程,实测手感、动画和逻辑都比预期顺畅。抓娃娃机是个很有意思的前端项目:它集成了二维坐标移动、碰撞判定、定时器编排、动画状态机、概率控制和视觉反馈调度,几乎覆盖了前端日常开发会遇到的所有交互难题,但又比大型后台系统纯粹得多,适合拿来练手和沉淀组件。

这篇文章主要面向两类人:一是想用 Vue 练交互逻辑的初中级前端,二是需要做营销活动页、展会互动屏、儿童娱乐 H5 的团队。全文会从设计拆解开始,逐步讲到组件结构、核心算法、动画实现、常见坑位和扩展方向,所有关键步骤都能直接照着落地。

1. 项目拆解:抓娃娃机的核心诉求与设计思路

1.1 抓娃娃机到底在模拟什么

很多人第一反应是“做个能动的爪子”。但真正动手之后会发现,抓娃娃机的体验核心是两个词:可操控感、结果反馈。在真实机器前,用户按方向键控制爪子水平移动,停准后按确认键让爪子垂直下落,夹住娃娃后爪子上升,把娃娃运到出口上方再松开。整个链条里,每一步都是状态切换,每一次切换都伴随明确的视觉反馈。

前端模拟时最需要还原的不是物理真实性,而是“像不像一台机器”。也就是说,爪子要有限位、运动要有加减速、夹取要有成功和失败的差异化结果、整个过程要有一个清晰的时序。如果只是让爪子简单平移然后消失,用户一眼就会觉得是网页广告,不会产生继续把玩下去的欲望。

另一个容易忽略的点是“夹取判定”。现实中爪子能不能夹稳取决于爪齿间距、娃娃尺寸和摩擦力,前端里就得用坐标碰撞、偏移容差、随机概率三者叠加来模拟。这部分做得好,用户会觉得“有运气因素”,而不是“纯随机”或者“必然成功”。

1.2 为什么选 Vue 而不是原生 JS 或 React

这个项目用 Vue 的原因在于响应式状态和视图同步是天然匹配的。爪子坐标、娃娃坐标、当前状态这些数据每帧都在变化,如果用原生 JS,几乎每个变化都要手动查 DOM 更新样式,几十个娃娃的位移跟踪会让代码迅速膨胀。而 Vue 的响应式系统让“数据变化—视图更新”变成声明式操作,写起来非常接近“直接描述界面应该长什么样”。

相比 React,Vue 模板语法在表达这类强视觉项目时更直观。比如娃娃池里的每个娃娃用循环渲染,爪子状态用 v-show 切换,出口碰撞用计算属性判断,模板里可以直接看到结构关系。再加上单文件组件的天然隔离,机器外壳、娃娃池、爪子、控制面板各自成为一个组件,分工清晰,调试时也不用在一堆 JSX 里翻找关键节点。

当然,用原生 Canvas 也能做,但 Canvas 会把所有元素拉平,交互和组件化优势会大打折扣。DOM 实现不仅能方便地叠加 CSS 动画,还能利用现有组件生态做音频反馈、滚动容器、弹窗通知,这些都更适合抓娃娃机的“网页游戏”形态。

1.3 整体交互流程与状态机设计

抓娃娃机的核心是四个状态的循环:待机、左移/右移、下落、上升。待机时爪子悬在顶部,左右移动可操作;确认后锁定当前 X 坐标,进入下落状态;爪子下降到预设底部位置后进入夹取动作;随后携带结果上升,上升到顶后水平移动到出口上方,松爪,回到待机。

这个流程用状态机表达最稳。我在设计时定义了一个可枚举的状态值,任何时刻只有一个状态生效,每个状态里只允许触发特定的事件,非法操作直接忽略。这样不会出现“正在下落时还能左右移动”这种破坏体验的情况。

状态机的好处不只是避免 bug,还让动画时机变得可控。下落和上升的长短、夹取动作的停顿、水平移动的速度,全部拆成每个状态的配置参数。后续调手感时,只需要改对应状态的参数,完全不用动其他逻辑,这对项目维护非常重要。

2. 环境准备与组件结构规划

2.1 开发环境与工程搭建

搭建工程时我直接选择了 Vite 配 Vue 3 的组合,相比旧版 Vue 2 工程,Vite 的预构建和热更新在调试动画时体验好很多。安装依赖只需要创建一个标准 Vite 项目模板,再安装 Vue Router 都不需要,单页面 demo 足够,连状态管理库都可以先用最普通的 ref / reactive 解决。

如果你的需求是后续对接后端积分、排行榜、礼物兑换,再考虑引入 Pinia 也不迟。抓娃娃机的交互界面本身是集中的状态突变场景,用 Pinia 掌管“当前用户金币、当前游玩状态、历史中奖记录”这一类跨组件数据会更清晰。但这属于工程扩展,不是起步的必需项。

2.2 组件划分:机器外壳、娃娃池、爪子、控制面板

我最终的组件结构是这样的:

  • MachineFrame:最外层容器,负责整体背景、装饰灯、窗口边框。
  • Cabinet:娃娃展示池,负责承载所有娃娃实体、记录娃娃初始位置、提供碰撞出口区域。
  • Claw:爪子组件,接收父级下发的坐标和状态,负责自身渲染和开合动画。
  • ControlPanel:控制面板,包含方向按钮和确认按钮,对外派发操作事件。
  • GameStore(普通 store):管理金币、抓取次数、结果队列和全局状态机流转。

组件拆分的核心依据是“变化是否独立”。爪子要频繁变坐标,娃娃在自己位置上被单独拖走,控制面板只发事件不关心效果。拆开后,每个组件的更新范围都被限定在自己内部,不会因为一次移动触发无关区域的重新渲染。

提示:娃娃池里的每个娃娃也建议做一个小组件,至少包裹一层自定义样式容器。不要直接在循环里操作裸坐标,因为后续要支持多场景娃娃、旋转娃娃、出彩特效时,一个独立的娃娃组件会让你省很多事。

2.3 数据结构设计

数据结构直接决定逻辑好不好写。我设计了这样几个核心模型:

  • 娃娃数据:{ id, x, y, size, img, state },其中 state 标记当前是在池中还是被抓走,x/y 是它在池中的基准坐标。
  • 爪子状态:{ phase, x, y, clawOpen },phase 是待机/下落/上升/移动,x/y 是爪子左上角的位置,clawOpen 是布尔值,控制爪齿展开角度。
  • 游戏会话:{ coins, playing, result },result 记录最近一次抓取是否成功。

爪子坐标用整数精度,娃娃坐标用浮点配合 CSS transform 渲染。移动时按帧步进量去改坐标,每帧前进多少像素取决于用户按住按钮的时长和当前状态允许的最大速度,保证节奏稳定。

还有一个重要数据是“出口区域矩形”。它不是一个图形,而是一组判定范围数值:当上升中的爪子带着娃娃横向移动到出口上方,且爪子纵坐标小于出口顶边时,就触发松爪逻辑。这个出口矩形事先用常量配置好,避免在逻辑代码里散落魔法数字。

3. 核心逻辑与动画实现

3.1 吊钩移动与方向控制

水平移动是抓娃娃机的第一步操作。真实机器里,方向键按住不放就持续移动,松开就停止,但会有一定的惯性缓冲。前端实现时我用 requestAnimationFrame 做逐帧位移,而不是直接 setInterval 改定时器,因为 rAF 的帧率与屏幕刷新同步,动画更平滑。

每一帧的执行逻辑很简单:判断当前是否有方向输入,有就把爪子 X 坐标加上或减去一个步长,同时用边界值 clamp 防止爪子跑出窗口。边界值不是机器内框宽度,而是爪子活动范围宽度减去爪子自身宽度的一半,这样爪子边缘不至于溢出轨道。

我特别建议把移动步长拆成“起步加速度”和“最大速度”。用户刚按下按钮时步长小一点,持续按压后逐渐加大,松开后再缓慢归零。这样爪子在移动时会有明显的“重量感”,比匀速直来直去真实得多。控制面板上虽然只显示左右两个按钮,但键盘监听同样可以接进来,方便桌面端调试。

3.2 下落、夹取判定与夹力模拟

按下确认键后,爪子锁定当前 X 坐标,开始垂直下落。下落的速度也不需要全程一致,前半段加速,接近底部时缓慢降速,模拟机械臂的减速过程。到预设的最低 Y 坐标后,爪子暂停,进入夹取动作。

夹取判定是这个项目的精髓。我用的判定方式结合了距离检测和概率补偿。距离检测很简单:遍历娃娃池,找出爪子几何中心与每个娃娃几何中心的最近距离,若距离小于阈值则视为“碰到”。但仅靠碰撞还不够,因为没有“摩擦力”和“松紧度”的概念,所以我在碰撞命中之后再叠加一个夹取成功概率。

概率计算放在玩家确定下落的瞬间,而不是等到爪子闭合后才计算。原因在于真实机器在爪子落到半空时就已经决定了结果,后面只是演出动画。这样每次抓取结果都是确定的、不可反悔的,不会出现闭合后抽搐、娃娃瞬间被吸走这类违和操作。成功概率可以做成越高级的奖品概率越低,普通娃娃概率高,这个值后续可以由后端下发,前端只执行结果。

3.3 娃娃被抓后的位移动画与掉落判定

夹取成功时,娃娃需要跟着爪子上升、再水平移动、最后在出口处落下。这里最大的问题是如何让娃娃“黏”在爪子上。

我的做法是成功夹取后把娃娃实例的父级从 Cabinet 直接挂载到 Claw 组件内部。这样娃娃的坐标系变成相对爪子,上升和水平移动时它天然跟随,不需要额外同步坐标。掉落的瞬间再把娃娃挂回 Cabinet,并把 x/y 设置为出口外部的固定位置,配合 CSS 过渡做掉落动画。

掉落判定不能只看坐标,得考虑娃娃的尺寸和出口的宽度。如果娃娃比较大,爪子把它运到出口正上方时不见得能落下,可能卡在边缘。所以我在判定时会把娃娃的宽度与出口矩形宽度做比较,如果娃娃过宽,即使到了出口中心也不会判定为掉落,而是让爪子松爪,娃娃留在池边,这就是一种“差一点”的挫败反馈。

这种细节看上去只是加了一个宽度判断,实际对玩家心理影响很大。真实抓娃娃机里的“卡边”情况很多,前端还原这个效果会让用户觉得机器更接近实体,而不是过于仁慈。

3.4 CSS 过渡与 JS 动画的配合选择

抓娃娃机里有两类动画:一类是连续位移,一类是状态突变。连续位移比如爪子平移、娃娃掉落,我直接用 JS 逐帧更新坐标,再用 transform 的 translate3d 应用坐标,保证 GPU 合成,避免 layout 抖动。状态突变比如爪齿开合、灯光闪烁、按钮按压,则用 CSS transition 和 keyframes 动画,省去手动控制补间帧的麻烦。

在实际编码中,有一个需要格外注意的冲突点:如果同时用 JS 更新坐标和 CSS transition,会出现动画拖动感或跳跃感。因为 transition 会尝试对 transform 当前值与目标值之间做差值,而 JS 每帧都在改写目标值,transition 根本追不上。

正确做法是分清动画所有权。连续位移类动画的 transform 由 JS 直接控制,不添加任何 transition;状态变化类动画的 transform 或其他属性由 CSS 独立控制。两者不要混在同一元素上。如果一定要混用,比如爪子下落时爪齿闭合,也得把位移放在外层容器、开合放在内层子元素,各自独立。

4. 交互细节与手感优化

4.1 按键响应与定时器管理

抓娃娃机的手感很大程度上由按键响应的即时性和可靠程度决定。按一下确认键,如果页面有 100 毫秒以上的延迟,用户就会觉得“卡了一下”。实现时确认键在按下时立即触发状态切换,不用等 touch 事件结束,这可以明显减小可感知延迟。

有一个容易被忽略的配置是移动按键的“锁定间隔”。用户在手机上可能连续按好几下,左右按钮会同时高亮。我在底层用一个输入映射表记录当前按压方向,同一次状态区间内后按压的方向覆盖前一个方向,但不会同时生效。这样即使两个手指同时按住左右键,爪子也不会左右颤抖,只会选择一个方向移动。

定时器方面,我全程没有使用裸 setInterval 管理状态流转,而是用 rAF 循环驱动所有逐帧逻辑,同时用一个小型延时工具管理“到达底部后停顿 300ms 再闭合爪子”这类一次性延迟。关键是每次状态切换时,把上一轮的 rAF 循环和延迟器全部清掉,避免状态急速切换时出现残留定时器导致爪子重复下落。

4.2 夹取概率控制在哪儿计算

前面说了概率在下落时确定,但具体怎么计算值得多说一句。我使用的策略是“基础概率 + 强控间隔”。基础概率由每个娃娃的稀有度决定,稀有度越低基础概率越高。再叠加一个全局计数器,如果玩家连续失败了 N 次,下一次的隐藏概率被强制提高,这叫保底机制。

保底机制对抓娃娃机的体验非常重要。如果没有保底,玩家会认为机器是黑箱、永远抓不到,从而放弃重复游玩。有了保底,短期的挫败感会被未来的成功预期抵消。在营销活动场景里,这个机制直接影响转化率。

保底参数建议做成配置对象,由 Store 管理,方便临时调整。不要在逻辑代码里直接写死判断数字,因为你很可能要在 A/B 测试中调出不同的概率策略,配置化以后只需要改一行数据。

4.3 视觉反馈:摇晃、灯光、成功弹窗

状态机的每个阶段都要有对应的视觉反馈,否则用户不知道当前能否操作。待机时控制面板的确认键常亮,表示可以按;下落时确认键变灰且不可点击;上升时爪子轻微左右晃动,模拟负重;成功掉落后机器外壳灯光闪烁,弹窗提示获得奖励。

我做了一个很有意思的优化:爪子在空手上升时是不晃的,带着娃娃上升时会有幅度很小的左右摆动,这个摆动是正弦波叠加在 X 坐标上的。用户能明显感觉到“爪子这次抓到东西了”,即使还没到出口,已经会产生期待。这种提前反馈放大了游戏的悬念感,比等结果出来再展示好得多。

成功弹窗不能阻挡玩家继续操作,否则会打断连续游玩节奏。我的做法是弹窗显示 2 秒后自动收起,同时提供可点击的“再来一局”按钮。弹窗本身用 teleport 挂到 body 顶层,避免被机器的 overflow 裁剪。

5. 实际踩坑与问题排查实录

5.1 经典问题一:抓取时娃娃穿透爪子

这是第一个版本里最严重的问题。表现为爪子下落碰到娃娃后,娃娃没有跟爪子走,而是被推倒在旁边或者直接穿透。

排查后发现原因在于夹取判定只检测了“爪子中心与娃娃中心的距离”,没有检测“爪子底部夹齿是否真的覆盖娃娃”。当爪子下落到最低点时,爪子夹齿在娃娃下方,中心点已经越过娃娃,距离计算反而偏大,判定失败。

解决方法是把夹取判定点从爪子中心改到“夹齿底边中点”,同时加入预留高度:爪子底部到达娃娃顶部上方若干像素时就触发闭合动作,闭合动画的 100 毫秒里爪子继续下沉一小段,视觉上衔接更自然。这个改动让命中率突然就正常了。

5.2 经典问题二:动画序列错乱

第二版里频繁出现“爪子还在下落时娃娃已经出现在出口”“爪子上升时夹齿提前张开”这类动画错乱。问题的根子是事件回调顺序不稳定。夹取动作分了好几步:暂停、闭合、提拉、上升。每一步靠各自的延时器启动下一步,如果某个延时器被外部操作打断,后续步骤就不按顺序执行。

我重构时把所有流程动作写进一个链式调度器,每一步返回一个 Promise,只有前一个 Promise resolve 后才进入下一步。这样即使过程中被外部操作中断,也会先 reject 当前流程,再清空后续步骤,不会出现一半旧流程、一半新流程的混乱状态。

注意:如果项目里已经大量使用 setTimeout 管理动画时序,建议尽早抽成一个统一的 delay 工具。不要到处散落魔法数字的 setTimeout,否则排查动画错乱时你会在代码里翻到怀疑人生。

5.3 经典问题三:定时器残留导致状态失控

这个坑在从“单机 demo”扩展成“多局连续游玩”时爆发。第一局结束后,第二局一开始爪子就莫名其妙自己往下掉。排查发现是上一局的移动监听没有在结束回调时解绑,rAF 循环依然在跑,新的状态覆盖后,旧循环还在按旧坐标更新爪子。

修复方式是在跨状态切换的统一入口里,先执行一个 clearAllLoop 函数,把所有 rAF 和 delay 全部取消,再进入新状态初始化。这个函数成了整个项目里最关键的保命 API,每次调用状态机入口必须先执行它。

5.4 移动端适配的几个坑

移动端主要遇到三个问题。一是触摸事件在按钮上默认有高亮闪块,需要设置 touch-action 和取消默认 click 延迟。二是页面在系统字体放大时,娃娃图片容器会轻微溢出,视觉上机器边框对不齐,我统一改用容器像素尺寸而不是 rem 绘制游戏区域。三是屏幕高度较矮时机器整体会被裁切,我给外层容器设置了 min-height 并让机器按比例缩放,而不是强制全屏拉伸。

还有一个容易被忽略的细节:进口播放背景音乐时,移动端系统会自动暂停音频播放,需要监听 touch 事件解锁音频上下文。这个小问题不处理的话,会有一半用户在滑动屏幕后音乐突然消失。

6. 项目扩展与更多玩法参考

6.1 从单机版到联机积分版

单机版完成之后,最自然的扩展方向是接入后端做成联机积分版。前端只负责展示和游玩逻辑,后端负责下发概率、记录积分、排行计算、奖励发放。这需要把 Store 里的会话数据和概率配置都改造成异步接口获取,同时把夹取结果交给后端校验,防止前端篡改。

联机版在架构上和单机版最大的不同是状态同步。我的建议是不要实时同步每个玩家的爪子坐标,只同步结果,观看者看到的是对方机器上的简化回放。这样后端压力小,前端也不用处理复杂的多人实时协调逻辑。

6.2 加入物理引擎

如果想让抓娃娃机更“真实”,可以考虑引入轻量物理引擎做娃娃的堆叠效果。娃娃之间会有碰撞、滑动、挤压,爪子碰到娃娃时会受到阻力。典型的前端方案是使用小型 2D 物理库,娃娃用物理刚体初始化,爪子用运动学刚体驱动,触碰时触发夹取约束。

但需要清醒地认识到:引入物理引擎的成本和收益不一定成正比。蝴蝶效应会让娃娃堆叠形态不稳定,每次刷新页面布局都不同,反而会增加夹取判定的复杂度。我的建议是保持现有配置布局,在娃娃掉落出口后、跌入奖品筐时用一段坠落动画模拟物理感,成本和收益比最划算。

6.3 抓娃娃机的营销场景应用

抓娃娃机在营销活动页里是非常成熟的产品形态,常见玩法是消耗积分抓“实体优惠券”“虚拟道具”“盲盒”。这类活动对前端交互的要求和纯游戏不同:必须提供可追溯的凭证、必须有克制的中奖提示、必须处理并发场景下同一用户连点多次请求的问题。

在营销版里我会把夹取概率和结果校验全部移到服务端,前端在下落动画时发起请求,返回什么东西由服务端决定,前端只负责演出。连点问题可以在客户端加状态锁,在收到响应前屏蔽确认按钮,同时在服务端做幂等校验。这样才能同时满足体验和业务安全。

结尾与实操心得

做这个项目踩过最深的一个坑是“上手就想直接写动画,结果逻辑和动画全耦合在一起”。我第一版把所有状态和动画代码堆在一个巨型组件里,调试时每次改一点都要全量刷新,别提多痛苦。后来强制自己先画状态流转草稿图,再拆组件,最后才写动画,整个项目才变得像一台真正可以被维护的“机器”。

另一个心得是手感调优一定不能在状态逻辑稳定之前做。状态机没理顺就调速度参数,每次改完都可能引发连锁 bug,根本没法判断是参数问题还是逻辑问题。先保证所有状态切换绝对不失控,再开始打磨移动速度、停顿时长、晃动幅度,手感会顺利很多。

如果下一步想继续扩展,我建议从这个项目里抽出通用的“状态机 + 时序调度器 + 组件角色划分”模型,你会发现在 H5 小游戏、AR 试戴、互动营销页面里这套模型都能快速复用到新项目。抓娃娃机只是一个开始,背后全是前端工程师每天都在跟状态、反馈、时序打交道的基本功。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询