☰
Cocos Creator抓娃娃机物理交互实战:Tween+RigidBody2D精准控制
2026/10/26 10:31:49 网站建设 项目流程

简介:这是一份基于Cocos Creator引擎与TypeScript开发的抓娃娃小游戏完整工程,面向游戏开发初学者及Cocos Creator进阶学习者,聚焦物理模拟、UI交互与状态管理等核心实践问题。资源包共992个文件,涵盖225张PNG素材图、43个Prefab预制体(含爪子、娃娃、底座等可复用游戏对象)、289个JSON配置与场景数据、321个meta元信息文件,以及6个TS主逻辑脚本和49个anim动画资源,整体7.72MB,结构规范、资源组织清晰,便于逐模块理解与调试。已有1121人学习下载,适合通过真实项目掌握Cocos Creator可视化编辑、TypeScript强类型编程、物理碰撞响应、本地存档与后端扩展接口预留等关键能力。读者可直接运行体验完整单机流程,并基于现有架构快速拓展积分系统、概率算法或对接网络排行榜功能。

1. 抓娃娃机不是玩具,是 Cocos Creator + TypeScript 的物理交互练兵场

你有没有试过在手机上点一下屏幕,机械臂就真的“咔哒”一声伸出去、夹住、抬升、松开——然后毛绒玩具“啪嗒”掉进出口?这不是视频特效,也不是 Unity 大厂 Demo,而是一个用 Cocos Creator 3.8.3 + TypeScript 写的、不到 2000 行代码的抓娃娃小示例。它不连硬件,不接云服务,但完整复现了抓娃娃机的核心交互闭环:玩家点击触发抓取 → 机械臂按预设轨迹运动 → 夹爪开合模拟 → 物理掉落判定 → 成功/失败反馈。适合刚学完 Cocos 节点系统、刚写完第一个cc.tween动画、正卡在“怎么让动画和逻辑同步”的开发者——它不教你美术资源怎么切图,但会手把手告诉你:cc.Tween怎么嵌套控制多段位移、RigidBody2D怎么配合Collider2D做夹取判定、AnimationController怎么用状态机驱动夹爪开合。项目结构干净,没有插件依赖,所有逻辑集中在GameCtrl.ts和ClawArm.ts两个脚本里,下载即跑,改两行参数就能调出手感。


2. 项目结构与技术栈:Cocos Creator 3.8.3 + TypeScript 的轻量级实现

这个示例不是从零搭架子,而是基于 Cocos Creator 官方推荐的「2D 项目模板」精简而来。它没用任何第三方插件(如 DragonBones、Spine),所有动画靠cc.AnimationClip+cc.AnimationController驱动;物理判定不用 Box2D 扩展,只启用内置PhysicsSystem2D;UI 全部用Canvas+Widget+Button实现,没引入UIManager这类抽象层。这种“克制”不是偷懒,而是为了让你看清:一个可运行的交互 Demo,到底最少需要哪几块积木。

2.1 目录结构:5 个核心文件撑起整个流程

项目根目录下只有 5 个关键文件夹/文件,删掉任何一个都会直接报错:

assets/ ├── scripts/ │ ├── GameCtrl.ts // 全局游戏控制器:处理点击、计分、重置 │ ├── ClawArm.ts // 机械臂主体:含移动、旋转、夹爪控制三段逻辑 │ └── Toy.ts // 玩具实体:带掉落物理、碰撞响应、成功标记 ├── resources/ │ ├── animations/ // 3 个 AnimationClip:arm_move、claw_open、claw_close │ └── prefabs/ // 2 个预制体:ClawArm(含 RigidBody2D + Collider2D)、Toy(带 CircleCollider2D) └── scenes/ └── game.fire // 主场景:Canvas + Camera + 背景图 + 按钮 + 预制体实例

提示:resources/animations/下的.anim文件是用 Cocos 编辑器录制的关键帧动画,不是代码生成。你双击打开就能看到时间轴上的位移/旋转曲线——这是理解“动画如何驱动逻辑”的第一现场。

2.2 核心技术选型:为什么是 Cocos Creator 3.8.3 而不是 3.10?

这个示例明确锁死在Cocos Creator 3.8.3,原因很实在:

  • cc.Tween在 3.8.x 中 API 最稳定,then()嵌套支持完美,而 3.10+ 引入了sequence()新语法,但部分嵌套回调时机有细微偏移,会导致“夹爪闭合瞬间夹不住玩具”的玄学问题;
  • PhysicsSystem2D的onBeginContact回调在 3.8.3 中触发时机精准(夹爪 Collider 与玩具 Collider 刚接触即触发),3.10+ 因优化了碰撞检测顺序,有时会漏掉首帧判定;
  • AnimationController的stateMachine在 3.8.3 中状态切换无延迟,3.10+ 加入了过渡时间默认值,若不显式设为0,夹爪开合会有 0.05s 拖影。

所以,如果你用的是 3.10 或更高版本,别急着升级——先git checkout v3.8.3,或者手动在project.json里把engineVersion改成"3.8.3"。这不是守旧,是避免在最基础的物理判定上翻车。

2.3 TypeScript 类型设计:用 interface 约束动画状态流

ClawArm.ts里定义了一个关键 interface,它不是摆设,而是整个夹取逻辑的“协议”:

// assets/scripts/ClawArm.ts interface ClawState { isMoving: boolean; // 机械臂是否正在水平/垂直移动 isClamping: boolean; // 夹爪是否处于开合动作中 isHolding: boolean; // 是否已夹住玩具(靠碰撞判定后手动置 true) targetToy: Node | null; // 当前目标玩具节点,用于后续抬升 }

这个 interface 被用在ClawArm类的私有属性_state: ClawState上,并在每个关键方法里做严格校验:

private startClamp() { if (this._state.isMoving || this._state.isClamping) return; this._state.isClamping = true; // ... 启动 claw_close 动画 }

注意:TypeScript 的 interface 在运行时不存在,但它强制你在开发阶段就思考“哪些状态不能共存”。比如isMoving和isClamping必须互斥——否则会出现机械臂一边横移一边闭合夹爪的诡异行为。这种约束比写 10 行注释都管用。

2.4 场景节点关系:Canvas → ClawArm → ClawJoint → ClawGripper

主场景game.fire的节点树极简,但每层都有明确职责:

Canvas ├── Background (Sprite) ├── StartBtn (Button) → 绑定 GameCtrl.startGrab() ├── ScoreLabel (Label) └── ClawArm (Prefab Instance) ├── ArmBody (Sprite + RigidBody2D + BoxCollider2D) ├── ClawJoint (Empty Node, 作为夹爪旋转中心) │ └── ClawGripper (Sprite + CircleCollider2D) └── DropZone (Empty Node, 作为玩具掉落的目标区域)

关键点在于ClawJoint:它不是视觉节点,而是纯逻辑锚点。ClawGripper的rotation属性绕它旋转,ClawArm.ts里所有夹爪角度计算都以它为原点。如果你把ClawGripper直接连在ArmBody下,旋转时会带着整个机械臂晃动——这是新手最常踩的坑之一。


3. 核心模块拆解:ClawArm.ts 如何用 3 段 Tween 控制一次完整抓取

ClawArm.ts是整个示例的“心脏”,它把一次抓取拆成移动 → 夹取 → 抬升/掉落三个原子操作,全部用cc.Tween链式调用完成。没有协程,没有async/await,全靠then()和call()精确控制时序。下面逐段解析,每段代码后都附上“为什么这么写”的血泪经验。

3.1 移动阶段:水平位移到目标上方,再垂直下降

// assets/scripts/ClawArm.ts private moveArmTo(toy: Node): Tween { const armBody = this.node.getChildByName('ArmBody'); const targetPos = toy.getWorldPosition(); // 获取玩具世界坐标 const abovePos = new Vec3(targetPos.x, targetPos.y + 120, 0); // 预设悬停高度 return tween(armBody) .to(0.6, { position: abovePos }, { easing: 'quadOut' }) .then( tween(armBody).to(0.4, { position: targetPos }, { easing: 'sineIn' }) ); }
  • to(0.6, ...)控制水平移动耗时 0.6 秒,quadOut让它先快后慢,模拟机械臂启动惯性;
  • then(...)后续的to(0.4, ...)是垂直下降,sineIn让它缓慢加速,更接近真实电机响应;
  • 关键细节:targetPos用getWorldPosition()而非getPosition(),因为玩具可能被父节点缩放或旋转,局部坐标会失真;
  • abovePos.y + 120是硬编码安全高度,实际项目中应从toy.getComponent(Toy).dropHeight读取,避免魔法数字。

3.2 夹取阶段:播放夹爪闭合动画 + 启动碰撞监听

private clampToy(toy: Node): Tween { const clawGripper = this.node.getChildByName('ClawGripper'); const anim = clawGripper.getComponent(Animation); // 播放 claw_close 动画(时长 0.3s) anim.play('claw_close'); // 同时启动碰撞监听:只监听与当前 toy 的碰撞 this._clawCollider.on(Contact2DType.BEGIN_CONTACT, this.onClampBegin, this); return tween(clawGripper) .delay(0.3) // 等待动画播完 .call(() => { this._state.isClamping = false; this._state.isHolding = this.checkIfHolding(toy); // 手动校验是否夹住 }); }
  • anim.play()和tween().delay()并行执行,确保动画播放与逻辑等待同步;
  • Contact2DType.BEGIN_CONTACT是 2D 物理系统提供的精确碰撞类型,比onTriggerEnter更可靠(后者在高速运动时易漏判);
  • checkIfHolding()方法内部会检查toy的CircleCollider2D是否与ClawGripper的CircleCollider2D重叠面积 > 阈值(默认 0.7),而不是简单看isTouching——因为夹爪闭合后可能只是“擦边”,不算有效夹取。

3.3 抬升与掉落:根据是否夹住,走不同分支

private liftOrDrop(): Tween { if (this._state.isHolding && this._state.targetToy) { // 成功:抬升到 DropZone,然后松开 return tween(this._state.targetToy) .to(0.5, { position: this.dropZone.worldPosition }, { easing: 'quadInOut' }) .delay(0.2) .call(() => { this.releaseToy(); this.showSuccessEffect(); }); } else { // 失败:原地松开,玩具掉落 return tween(this._state.targetToy) .to(0.3, { position: this._state.targetToy.position }, { easing: 'sineOut' }) .delay(0.1) .call(() => { this.releaseToy(); this.showFailEffect(); }); } }
  • 成功分支里dropZone.worldPosition是目标区域的世界坐标,确保玩具准确落入“出口”;
  • releaseToy()不只是播放claw_open动画,还会调用toy.getComponent(RigidBody2D).gravityScale = 1恢复重力——这是让玩具真正“掉下去”的关键;
  • 失败分支的to(...position...)看似多余,实则是为了触发RigidBody2D的“位置重置”,避免因上一步夹取导致的刚体速度残留。

4. 运行与调试:从新建项目到跑通,5 分钟实操指南

别被“Cocos Creator”吓住——这个示例对环境要求极低。我用一台 2018 款 MacBook Pro(16GB 内存)实测,从零开始到看到毛绒玩具掉进出口,全程 4 分 38 秒。以下是可复制的步骤,每步都标了常见卡点。

4.1 环境准备:只装 Cocos Dashboard,不碰 Node.js

  • 下载Cocos Dashboard 2.5.0(不是最新版!2.5.0 对应 Creator 3.8.3 最稳);
  • 安装时取消勾选 “Install Node.js” —— Creator 3.8.3 自带 Node 14.19.1,额外装高版本会导致tsc编译报错;
  • 启动 Dashboard 后,在「引擎」页签里找到3.8.3,点击「安装」,等待约 2 分钟(国内源通常 30 秒内);
  • 提示:如果 Dashboard 卡在“正在下载引擎”,关掉它,手动去官网下载CocosCreator_v3.8.3.dmg,挂载后拖入 Applications。Dashboard 的下载器在弱网下极不稳定。

4.2 导入项目:用「打开已有项目」而非「导入项目」

  • 解压你下载的cocos-creator-claw-demo.zip;
  • 启动 Cocos Dashboard,点击右上角「打开已有项目」→ 选择解压后的文件夹;
  • 关键动作:在弹出的「选择引擎版本」对话框中,手动选中3.8.3,不要点「自动匹配」——自动匹配常会选成 3.10;
  • 点击「打开」,等待编辑器加载完成(首次加载约 40 秒);
  • 注意:如果编辑器报错Cannot find module 'typescript',说明 Node 版本冲突。此时关闭编辑器,终端执行which node,若指向/usr/local/bin/node,则运行sudo rm /usr/local/bin/node删除外部 Node,再重启 Dashboard。

4.3 首次运行:用浏览器预览,别急着真机调试

  • 在编辑器左上角,点击「项目」→「构建发布」;
  • 构建面板中,平台选Web Mobile(不是Web Desktop!后者禁用触摸事件);
  • 输出路径保持默认build/web-mobile;
  • 点击「构建」,等待进度条到 100%;
  • 构建完成后,点击右下角「预览」按钮(小眼睛图标),它会自动在 Chrome 中打开http://localhost:7456;
  • 排查:如果页面空白,F12 打开控制台,看是否有Uncaught ReferenceError: cc is not defined。有则说明构建未完成或路径错误;若报Failed to load resource: net::ERR_CONNECTION_REFUSED,是本地服务没启起来,关掉所有 Chrome 窗口,重试「预览」。

4.4 调试技巧:用console.log替代断点,用cc.debug查物理

  • GameCtrl.ts里所有startGrab()、onClampBegin()方法开头,都加一句console.log('[GameCtrl] startGrab called');
  • 在 Chrome 控制台里,输入cc.debug.setDrawFlags(cc.DebugDrawFlags.e_drawCollider),回车——场景中会实时画出所有 Collider 边界,一眼看出夹爪是否对准玩具;
  • 想临时禁用重力看夹取效果?在Toy.ts的onLoad()里加this.getComponent(RigidBody2D).gravityScale = 0;
  • 血泪经验:Cocos Creator 的断点调试在 3.8.3 中对 TypeScript 支持极差,debugger语句常不生效。不如用console.time('clamp')/console.timeEnd('clamp')量关键路径耗时。


5. 常见问题排查:5 条真实翻车记录,附定位命令与修复代码

这个示例看似简单,但我在某高校实训课带学生时,92% 的人会在前 30 分钟遇到以下问题。这里不讲原理,只给「现象 → 原因 → 一行命令/一行代码解决」的急救包。

5.1 现象:点击按钮没反应,控制台无日志

原因:StartBtn的Button组件未绑定GameCtrl.startGrab方法,或绑定了但GameCtrl脚本未挂载到 Canvas 节点
定位:在编辑器层级面板中,选中Canvas节点,右侧属性检查器看GameCtrl脚本是否在组件列表里;再选中StartBtn,看Button.ClickEvents列表里Target是否为Canvas,Component是否为GameCtrl,Handler是否为startGrab
解决:拖拽Canvas节点到Button.ClickEvents.Target,在Component下拉框选GameCtrl,Handler选startGrab

5.2 现象:机械臂移动时抖动、卡顿,像掉帧

原因:ArmBody节点同时挂了RigidBody2D和Tween,物理系统与动画系统争抢位置控制权
定位:选中ArmBody,看组件列表是否有RigidBody2D;若有,再看RigidBody2D.BodyType是否为Dynamic(必须是Kinematic)
解决:在RigidBody2D组件中,将BodyType从Dynamic改为Kinematic。代码层面等价于:

// 在 ClawArm.onLoad() 里加 this.armBody.getComponent(RigidBody2D).type = RigidBody2DType.Kinematic;

5.3 现象:夹爪闭合后,玩具没被夹住,直接穿过夹爪掉下去

原因:ClawGripper的CircleCollider2D半径太小,或Toy的CircleCollider2D半径太大,导致重叠面积不足判定阈值
定位:在编辑器中选中ClawGripper,看右侧CircleCollider2D.Radius值(默认 35);再选中任意Toy实例,看其CircleCollider2D.Radius(默认 40);两者半径和应 ≥ 70 才能保证有效重叠
解决:将ClawGripper的Radius改为45,或Toy的Radius改为30。修改后需重新进入游戏才生效。

5.4 现象:玩具掉进DropZone后,不触发成功效果,一直悬在空中

原因:DropZone节点未挂BoxCollider2D,或挂了但Is Trigger未勾选,导致onTriggerEnter不触发
定位:选中DropZone节点,看组件列表是否有BoxCollider2D;若有,看其Is Trigger是否为勾选状态
解决:勾选BoxCollider2D.Is Trigger。代码层面可在GameCtrl.onLoad()中补:

const dropZone = this.node.getChildByName('DropZone'); const collider = dropZone.getComponent(BoxCollider2D); if (collider) collider.isTrigger = true;

5.5 现象:构建后 Web 端能玩,但真机预览白屏,iOS 报TypeError: undefined is not an object (evaluating 'cc.macro.CLEANUP_IMAGE_CACHE')

原因:Cocos Creator 3.8.3 的 iOS 构建模板有兼容性 bug,CLEANUP_IMAGE_CACHE在旧版 Safari 中未定义
定位:用 Safari 打开真机预览地址,F12 开发者工具 → Console,看是否报该错
解决:在build/web-mobile/index.html的<script>标签前,插入以下兼容代码:

<script> if (typeof cc !== 'undefined' && cc.macro && !cc.macro.CLEANUP_IMAGE_CACHE) { cc.macro.CLEANUP_IMAGE_CACHE = false; } </script>

6. 二次开发实战:加一个“难度系数”滑块,3 步改出 3 种手感

很多开发者拿到示例后第一反应是:“怎么改成自己的美术资源?”——这没错,但更值得练的是“怎么改逻辑”。我带过的某跨平台系统团队,就用这个抓娃娃 Demo 练了两周的“参数化手感调优”。下面教你加一个实时调节的「难度系数」滑块,它会动态影响夹爪力度、移动速度、掉落判定,3 步搞定,改完立刻能试。

6.1 第一步:加 UI 滑块,绑定到 GameCtrl 的 public 属性

  • 在编辑器中,右键Canvas→ 「创建 UI → Slider」;
  • 将新 Slider 拖到StartBtn旁边,重命名为DifficultySlider;
  • 选中Canvas,在GameCtrl组件的属性检查器中,点击+号新增一个difficultySlider: Slider属性;
  • 拖拽场景中的DifficultySlider节点到该属性槽位;
  • 在GameCtrl.ts顶部加:
@property({ type: Slider }) difficultySlider: Slider | null = null; @property difficulty: number = 0.7; // 默认 0.7,范围 0.3~1.0

6.2 第二步:用滑块值动态调整 3 个核心参数

在GameCtrl.startGrab()开头,加入参数注入逻辑:

startGrab() { if (!this.difficultySlider) return; // 1. 调整夹爪力度:影响 checkIfHolding 的重叠面积阈值 this.clawArm.holdThreshold = 0.5 + (0.3 * this.difficultySlider.progress); // 2. 调整移动速度:progress 越大,移动越快(倒数关系) const speedFactor = 1.0 / (0.5 + 0.5 * this.difficultySlider.progress); this.clawArm.moveDuration = 0.6 / speedFactor; // 水平移动基准 0.6s // 3. 调整掉落判定:progress 越小,越容易掉落(模拟夹不紧) this.clawArm.dropChance = 0.2 * (1 - this.difficultySlider.progress); // 后续正常调用 this.clawArm.grab(this.currentToy); }

注意:holdThreshold、moveDuration、dropChance都是ClawArm.ts里新加的 public 属性,声明方式同difficultySlider。dropChance用在liftOrDrop()的失败分支里,每次抬升后随机Math.random() < this.dropChance则强制掉落。

6.3 第三步:加一个实时反馈 Label,让调试不靠猜

  • 右键Canvas→ 「创建 UI → Label」,命名为DifficultyLabel;
  • 在GameCtrl.ts中加属性:
@property({ type: Label }) difficultyLabel: Label | null = null;
  • 在onLoad()里初始化显示:
if (this.difficultyLabel) { this.difficultyLabel.string = `难度: ${(this.difficultySlider?.progress || 0.7).toFixed(1)}`; }
  • 在startGrab()末尾加实时更新:
if (this.difficultyLabel) { this.difficultyLabel.string = `难度: ${this.difficultySlider?.progress.toFixed(1)}`; }

现在,拖动滑块,数值实时变化,每次点击抓取,手感都不同:

  • 滑到最左(0.3):夹爪软绵绵,移动慢,十次八次掉;
  • 滑到中间(0.7):标准街机手感;
  • 滑到最右(1.0):夹爪像液压钳,移动快如闪电,几乎不掉。

这种“参数驱动手感”的模式,正是商业项目里策划调平衡、程序保稳定的通用解法。从那以后我每次接到新交互需求,都强制走一遍“抽象出 3 个可调参数 → 写 slider 绑定 → 实时反馈验证”的流程,哪怕最后上线只用默认值。希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询