
如果你也刷到过那段时间满屏的“合成大西瓜”大概会和我有同样的感受玩法一眼到底但真让自己动手写一个反而不知道从哪里开始。尤其是把它改成 3D 版以后事情变得更有意思——水果不再只是在 2D 平面里“啪嗒”一声叠上去而是真的会滚动、会堆叠、会因为碰撞角度不同而弹开。这里面的物理表现、碰撞合并、点击生成不只是一个小游戏更像一堂 Cocos Creator 3.x 的实战课。这篇文章我会带你用 Cocos Creator 3.8 TypeScript 完整制作一款 3D 版合成大西瓜。文章不是只给代码而是把背后的选型思路、场景搭建、物理配置、合并判定和调试方法都讲清楚。你照着做完以后会得到一个可以运行、可以继续扩展的 3D 合成果实 Demo同时也能理解 Cocos Creator 3.x 项目的核心组织方式。1. 做 3D 版合成大西瓜我建议你选 Cocos Creator 3.8先说判断如果你是想练手的小白或者准备做一款中轻度休闲游戏推向微信小游戏/安卓市场Cocos Creator 3.8 是一个非常合适的选择。如果你是想做 PC/主机级 3A 大型游戏那这个教程的方案就不适合你直接去用 Unity 或 Unreal 更划算。为什么这么判断合成大西瓜的核心体验依赖两点一是物理堆叠二是合并反馈。Cocos Creator 3.x 的物理系统经过多个版本迭代后已经足够稳定内置刚体、碰撞体、碰撞回调不需要自己写物理引擎。它的 TypeScript 脚本体系对新手也友好编辑器里挂组件、拖引用、改属性非常直观。最关键的是Cocos 生态天然面向小游戏和移动端做完的 Demo 可以直接构建为微信小游戏或者 Android APK。这一点对休闲游戏开发者来说比很多通用游戏引擎更省事。再来说这个项目的技术路线。很多初学者有个误解觉得“3D 版”就是从 2D 换成 3D 相机再把图片换成 3D 模型。实际上3D 版合成大西瓜的核心变化在物理层水果要从“2D 圆形碰撞体在一个圆形容器里堆叠”变成“3D 球体在一个立体碗状容器里堆叠”。这意味着每个水果需要挂 3D 刚体和球体碰撞体容器也需要有真实的静态碰撞墙。水果之间碰撞后根据等级判断是否合并合并位置取两个水果的世界坐标中点。这个方案有一个特别值得新手注意的点Cocos Creator 3.x 的 3D 物理默认使用 Y 轴向上的世界坐标系重力方向是(0, -9.8, 0)而 UI 的 Canvas 坐标体系和屏幕坐标不一样。所以当你点击屏幕生成水果时需要进行一次“屏幕坐标 → 世界坐标”的转换否则你会发现水果生成的位置和点击位置完全对不上。这个坐标转换我会在第 5 章专门讲。2. 合成大西瓜玩法拆解与 3D 改造难点我们先不看代码把游戏抽象成几个简单模块。2.1 核心玩法循环普通版的合成大西瓜玩法是这样的玩家点击容器上方某个位置当前水果掉落。水果落到容器底部和其他水果发生碰撞。两个相同等级的水果碰撞后合并生成更高一级的新水果。玩家持续点击最终目标是合成尽可能高等级的水果。如果水果摞到警戒线以上游戏结束。整个循环简化为三件事生成水果、物理掉落、碰到同类后合并。合并是玩法的核心反馈也是分数来源。2.2 3D 改造后的几个变化果体形状原来是 2D 圆形现在换成 3D 球体。容器形状原来是屏幕上一个平面圆形容器现在是三维碗状容器。相机视角2D 版本是平视视角3D 版本可以调整为斜上方 45 度视角能看到水果堆叠的厚度视觉冲击力更强。物理计算2D 物理只有 X/Y 两个坐标3D 物理多了一个 Z 轴。如果你把掉落平面限制在 X/Z 平面本质上还是一个 2D 玩法但表现层已经是 3D。真正容易踩坑的地方是3D 物理中水果和水果碰撞时碰撞回调的触发条件比你想的更严格。只有至少一个物体是动态刚体并且碰撞双方的分组互相检测回调才会触发。如果只是挂着碰撞体但没有刚体或者分组设置不对水果就会直接穿模掉下去这也是新手最容易卡住的问题。2.3 游戏数据设计每个水果需要一个等级从 0 开始递增。不同的等级对应不同的半径、模型颜色和合并分数。我这里用一个极简设计等级含义合并得分建议0最小的果子101第二级202第三级30.........9最高级不再合并100在设计脚本时等级只用一个整数表示分数用level * 10计算。这样数据结构简单也方便后续扩展。3. 环境准备与项目创建3.1 安装 Cocos Dashboard 和 3.8 编辑器Cocos Creator 3.8 的安装方式是先下载 Cocos Dashboard再在 Dashboard 中安装指定引擎版本。具体操作如下打开 Cocos Dashboard。在“编辑器”页签选择 Cocos Creator 3.8 版本并安装。安装完成后回到 Dashboard 首页点击“新建项目”。选择模板时建议选择Empty (3D)空项目模板。这个模板自带 3D 场景和相机省去很多初始化步骤。版本细节以实际安装为准本文重点演示通用思路。Cocos Creator 3.x 的界面和脚本体系在 3.0 到 3.8 之间差异不大只要你的编辑器是 3.x基本都能按本文操作。3.2 项目结构准备创建项目后在assets目录下建立几个文件夹assets/ ├─ scenes/ # 存放场景 ├─ scripts/ # 存放 TypeScript 脚本 ├─ prefabs/ # 存放水果预制体 └─ materials/ # 存放材质这些文件夹可以直接在编辑器的资源管理器里右键创建。3.3 开启物理系统Cocos Creator 3.x 中物理模块默认是启用的但仍然建议检查一下。打开编辑器菜单栏的“项目 → 项目设置 → 功能裁剪”确认“物理系统(3D)”处于启用状态。如果不开启RigidBody 和 Collider 组件不会生效。这个步骤很容易被忽略但一旦漏掉运行场景会发现水果直接穿过地面没有任何碰撞效果。4. 场景搭建与水果预制体制作4.1 搭建三维碗状容器在场景中新建一个空节点命名为GameField作为整个游戏区域的父节点。在这个节点下创建 5 个静态碰撞体节点底部一个平面或方块用于承接所有水果。前后左右四面墙用于限制水果掉落范围。每个碰撞体节点的处理方式创建一个Box3D 对象。删除它的渲染相关子节点可选如果不需要看到墙。添加BoxCollider组件。不需要挂 RigidBody作为静态碰撞体使用。调整缩放和位置让它们围成一个近似方形的碗。底部节点不要设置得太小建议至少 4x4 左右不然水果掉落后会堆得太挤游戏很快结束。四面墙的高度建议能看到明显堆叠过程大概 8 到 10 个单位。关于容器形状我要说一个实际取舍真正的原版圆形容器需要用圆柱碰撞体来模拟但 Cocos Creator 3.x 内置碰撞体里没有直接提供 CylinderCollider要自己做组合碰撞或者导入带碰撞的模型。作为入门 Demo用四面墙围成方形碗更简单也足够展示 3D 物理效果。如果你想做成圆柱形可以用 MeshCollider 配合导入的圆柱模型实现这部分作为进阶优化项。4.2 调整相机与光照把主相机的旋转角度调整为斜上方视角大约(-30, 0, 0)。相机位置放在(0, 12, 10)附近这样可以看到水果从上方掉落并堆叠的全过程。场景中如果没有光照3D 球体会是黑色一片。确认场景中有一个DirectionalLight方向光朝向地面不然完全看不出 3D 效果。4.3 制作水果预制体这是整个项目中比较繁琐但很重要的一步。水果预制体需要满足挂有 3D 球体模型节点。挂有RigidBody动态刚体组件。挂有SphereCollider球体碰撞体组件。挂有一个Fruit自定义脚本组件。不同等级使用不同颜色或材质。操作步骤在场景中创建一个Sphere3D 对象。调整缩放比如(0.5, 0.5, 0.5)。在该节点上添加RigidBody组件类型保持默认的 Dynamic。添加SphereCollider组件。如果模型和碰撞体大小对不上可以调整碰撞体的radius。添加Fruit脚本组件。新建材质修改 Base Color 颜色拖到球体上。将节点拖到assets/prefabs目录下生成预制体。复制多个预制体分别改颜色和大小代表不同等级。最终在你的fruitPrefabs数组中下标 0 是最小水果下标越大等级越高。4.4 创建 UI 分数标签创建一个Canvas在 Canvas 下新建一个Label节点命名为ScoreLabel用来显示当前分数。这个 Label 会在后面的GameManager脚本中引用。到了这里场景骨架已经搭好。接下来进入正题写 TypeScript 代码。5. 核心代码GameManager 与点击生成水果我习惯把游戏管理器放在一个独立空节点上。GameManager负责生成水果、管理分数、判定游戏结束。FruitSpawner负责监听点击事件并把屏幕坐标转换为世界坐标。5.1 GameManager.ts在assets/scripts下新建GameManager.tsimport { _decorator, Component, Prefab, instantiate, Node, Label, Vec3 } from cc; import { Fruit } from ./Fruit; const { ccclass, property } _decorator; ccclass(GameManager) export class GameManager extends Component { public static instance: GameManager null; property({ type: [Prefab] }) public fruitPrefabs: Prefab[] []; property({ type: Node }) public fruitRoot: Node null; property({ type: Label }) public scoreLabel: Label null; property public maxFruitLevel 9; property public spawnHeight 10; property public overLineY 7; public score 0; public gameOver false; private _overTimer 0; onLoad() { GameManager.instance this; } start() { this.score 0; this.gameOver false; this.updateScoreLabel(); } /** * 创建一个指定等级的水果并放到世界坐标世界位置 */ public createFruit(level: number, worldPos: Vec3) { if (this.gameOver) return; // 防止数组越界 if (level this.maxFruitLevel) { level this.maxFruitLevel; } const prefab this.fruitPrefabs[level]; if (!prefab) { console.warn(fruitPrefabs 中没有对应等级的水果, level); return; } const fruitNode instantiate(prefab); fruitNode.setParent(this.fruitRoot); fruitNode.setWorldPosition(worldPos); const fruit fruitNode.getComponent(Fruit); if (fruit) { fruit.init(level); } } /** * 合并后加分 */ public addScore(level: number) { this.score (level 1) * 10; this.updateScoreLabel(); } private updateScoreLabel() { if (this.scoreLabel) { this.scoreLabel.string 分数: ${this.score}; } } update(dt: number) { if (this.gameOver) return; // 检查是否有水果超过警戒线 let hasOverLine false; for (const child of this.fruitRoot.children) { if (child.worldPosition.y this.overLineY) { hasOverLine true; break; } } if (hasOverLine) { this._overTimer dt; if (this._overTimer 2) { this.onGameOver(); } } else { this._overTimer 0; } } private onGameOver() { this.gameOver true; console.log(Game Over); // 这里可以扩展弹出结束界面、重新开始按钮等 } }这段代码的关键点有几个。GameManager.instance是静态单例。这样在水果碰撞脚本中可以直接通过GameManager.instance调用合并和加分方法不需要在预制体上手动拖引用简化了准备工作。createFruit是核心入口。它接收等级和世界坐标实例化对应预制体挂到fruitRoot节点下然后调用Fruit.init初始化。为什么要把生成逻辑放在 GameManager 而不是点击脚本里因为合并水果时也需要生成新水果这两个场景复用同一个入口后续做生成预览、限时生成都会方便。update中遍历fruitRoot的子节点判断是否有水果高于警戒线。这是一个简单的 Game Over 判定。更复杂的实现可以用射线检测或者体积检测但作为入门 Demo高度检测已经足够直观。5.2 FruitSpawner.ts在assets/scripts下新建FruitSpawner.tsimport { _decorator, Component, input, Input, EventTouch, Camera, Vec3 } from cc; import { GameManager } from ./GameManager; const { ccclass, property } _decorator; ccclass(FruitSpawner) export class FruitSpawner extends Component { property({ type: Camera }) public mainCamera: Camera null; property public spawnHeight 10; private _gameManager: GameManager null; onLoad() { this._gameManager GameManager.instance; input.on(Input.EventType.TOUCH_START, this.onTouchStart, this); } onDestroy() { input.off(Input.EventType.TOUCH_START, this.onTouchStart, this); } private onTouchStart(event: EventTouch) { if (!this._gameManager || this._gameManager.gameOver) return; // getLocation 返回的是屏幕坐标原点在左下角 const screenPos new Vec3(event.getLocation().x, event.getLocation().y, 0); // 屏幕坐标转世界坐标 const worldPos this.mainCamera.screenToWorld(screenPos, new Vec3()); // 固定生成高度 worldPos.y this.spawnHeight; // 随机生成前几种低等级水果避免一上来就合成高级 const level Math.floor(Math.random() * 4); this._gameManager.createFruit(level, worldPos); } }这里最需要注意的是坐标转换。Cocos Creator 3.x 的事件系统里getLocation()返回的是屏幕坐标而screenToWorld方法的输入也是屏幕坐标所以两者可以直接搭配。如果你用getUILocation()拿到的是 UI 坐标在 Canvas 有缩放时会出现偏移。新手容易在这里踩坑生成位置总是不对点左边水果生成到右边。排查这个位置时先看用的到底是getLocation还是getUILocation。worldPos.y this.spawnHeight是为了让水果从固定高度掉下来而不是从点击处的真实世界高度生成。否则你点击屏幕不同位置生成高度不同游戏手感会很怪。5.3 挂载与配置在场景中新建一个空节点命名为GameManager。给它挂上GameManager脚本。把fruitPrefabs数组填上你制作好的水果预制体从 0 级到 9 级。把fruitRoot指向场景中的GameField节点。把scoreLabel指向 Canvas 下的 ScoreLabel。新建一个空节点命名为InputHandler挂上FruitSpawner脚本。把mainCamera指向场景中的主相机。6. 核心代码物理碰撞与水果合并6.1 碰撞回调怎么触发Cocos Creator 3.x 中3D 碰撞回调有两种常见方式一种是直接在碰撞体组件上监听onCollisionEnter等事件另一种是通过脚本生命周期回调。我推荐使用事件监听方式可读性更强。在水果预制体上SphereCollider需要监听碰撞事件。要保证事件能被触发有两个前提至少一个碰撞体挂在动态刚体上。水果都挂 RigidBody所以满足。碰撞双方的分组掩码互相包含。默认情况是所有分组互相碰撞所以初学阶段不用改。6.2 Fruit.ts 合并逻辑在assets/scripts下新建Fruit.tsimport { _decorator, Component, Collider, ICollisionEvent, Vec3 } from cc; import { GameManager } from ./GameManager; const { ccclass, property } _decorator; ccclass(Fruit) export class Fruit extends Component { public level 0; private _merged false; public init(level: number) { this.level level; this._merged false; const collider this.getComponent(Collider); if (collider) { collider.off(onCollisionEnter, this.onCollisionEnter, this); collider.on(onCollisionEnter, this.onCollisionEnter, this); } } private onCollisionEnter(event: ICollisionEvent) { if (this._merged) return; const otherCollider event.otherCollider; if (!otherCollider) return; const otherFruit otherCollider.getComponent(Fruit); if (!otherFruit) return; // 双方都已经被合并或者等级不一致都不处理 if (otherFruit._merged || otherFruit.level ! this.level) return; const gameManager GameManager.instance; if (!gameManager || gameManager.gameOver) return; // 标记双方都已合并避免重复触发 this._merged true; otherFruit._merged true; // 计算两个水果世界坐标的中点作为新水果生成位置 const midPos new Vec3(); Vec3.add(midPos, this.node.worldPosition, otherFruit.node.worldPosition); midPos.multiplyScalar(0.5); const newLevel this.level 1; if (newLevel gameManager.maxFruitLevel) { gameManager.createFruit(newLevel, midPos); } // 加分 gameManager.addScore(this.level); // 销毁两个旧水果 this.node.destroy(); otherFruit.node.destroy(); } }这段代码是游戏的核心我拆开解释。_merged是防重复合并的关键。两个水果发生碰撞时如果不做标记A 的碰撞回调里会让 B 合并B 的碰撞回调里又会触发一次合并最终导致生成两个新水果。所以在合并前必须检查双方状态合并时把双方都标记为_merged true。合并位置取两个旧水果世界坐标的中点。为什么不直接取this.node.worldPosition因为两个水果碰撞时不一定完全重合它们的位置有偏差。取中点可以让新水果看起来是从两个水果接触位置长出来的视觉上更自然。addScore(this.level)这里传入的是合并前等级。分数公式在GameManager中是(level 1) * 10等级越高加分越多。这里的分数设计可以随意调整关键是保持逻辑清晰。node.destroy()会延迟到帧末销毁节点。如果合并操作在碰撞回调中连续触发多次存在一定的安全间隔但不能过度依赖它所以_merged标记仍然是必要的。6.3 关于最高级水果newLevel gameManager.maxFruitLevel这个判断意味着最高等级水果不会再合并生成新水果只负责加分。如果maxFruitLevel是 9那么 9 级水果碰撞后不再生成新水果直接加一次高分。这样设计可以避免数组越界也符合休闲游戏的常规做法。6.4 为什么要在 init 里重新注册监听Fruit脚本挂在预制体上预制体每次instantiate后都会重新执行init。如果场景中手动把 Fruit 脚本挂上但没调用initlevel会保持默认值 0。所以在init里我会先off再on确保同一个节点重复使用时不会出现多个监听。如果你在制作预制体时直接在start中注册监听也不是不行但需要在start里同步初始化等级。我建议用显式init的方式等级参数从外部传入逻辑更明确。7. 运行验证与效果调试7.1 运行预览点击编辑器上方的预览按钮浏览器会打开游戏画面。此时你应该看到一个方形碗状容器底部是地面四面有墙。点击容器上方任意位置一个随机低等级水果从顶部掉落。水果落到地面后会有轻微弹跳然后逐步静止。当两个同等级水果接触时它们消失原地生成一个更大的新水果。分数会随每次合并增加。7.2 如何验证核心逻辑是否正常逐个验证比直接看整体效果更有用。第一个验证点点击生成位置是否准确。点击容器某个角落水果应该从对应位置掉落。如果偏差明显先检查FruitSpawner中用的是getLocation还是getUILocation再检查Camera.screenToWorld是否有正确的相机引用。第二个验证点水果能否正常掉落且不掉出容器。如果水果穿墙或穿地说明碰撞体配置有问题。第一步检查项目设置里的物理系统是否开启第二步检查水果上是否有 RigidBody 动态刚体第三步检查墙上是否有 Collider。这三个检查点能解决大多数“掉落异常”问题。第三个验证点合并逻辑是否只触发一次。两个同等级水果碰撞后如果生成了多个新水果或者分数异常增加说明_merged标记没有生效或者两个水果的回调都被执行了。检查Fruit.init中是否每次都重新注册监听避免旧监听残留。7.3 调试技巧Fruit 的合并逻辑是异步事件调试时建议在两个地方加打印// 在碰撞回调里先打印双方等级和状态 console.log(collision, this.level, otherFruit.level, this._merged, otherFruit._merged);// 在 createFruit 里打印新水果的等级和位置 console.log(create fruit level, level, at, worldPos);看到日志顺序你就能判断是物理没触发、等级判断不对还是坐标生成异常。如果水果在掉落过程中抖动得很厉害可以把RigidBody的linearDamping和angularDamping调大一点比如都设置为 1 到 2。阻尼会让水果更快停止滚动手感更稳定代价是物理效果会显得“粘”。8. 常见问题与排查思路这一节把新手最容易遇到的问题汇总成一张排查表。当你运行结果不对时优先从这里找方向。问题现象可能原因排查方式解决方案点击屏幕没有水果生成FruitSpawner 未挂载或 GameManager 未初始化检查场景节点组件和 Console 日志确认 InputHandler 节点上挂有脚本GameManager.instance 不为空水果生成位置和点击位置偏差大使用了 getUILocation 而不是 getLocation或相机引用错误打印 event.getLocation() 和 worldPos改用 getLocation并确认 mainCamera 指向实际使用的相机水果直接穿过地面或墙物理系统未开启或墙没有 Collider检查项目设置物理模块、节点组件开启物理模块给底部和墙添加 BoxCollider两个水果穿透后不合并碰撞回调没触发或等级不一致在 onCollisionEnter 中打印日志确认两个水果都有 RigidBody 和 Collider确认等级相同合并生成了多个新水果防重复标记失效观察 Console 中 createFruit 打印次数在 init 中先 off 再 on 监听合并前检查 _merged水果一直在抖动停不下来刚体阻尼太小或碰撞体重叠调低掉落高度观察碰撞体显示增大 linearDamping 和 angularDamping高级水果合成后数组越界fruitPrefabs 数量不足检查 GameManager 中数组长度确保预制体数组下标覆盖 0 到 maxFruitLevel这里尤其提醒一个问题很多新手在制作预制体时会给同一个节点挂多个Collider或者忘记删除原来的碰撞体导致水果节点上存在多个 SphereCollider碰撞回调会重复触发。建议每个水果预制体只保留一个动态刚体和一个球体碰撞体保持节点结构干净。9. 从 Demo 到成品的优化方向跑通这个 Demo 只是第一步。如果你想把它变成一款手感合格、可以上线的小游戏还有几个方向值得继续做。第一个是生成预览。现在的版本里玩家点击后水果直接掉落。正式游戏中通常会在点击前显示一个半透明水果跟随鼠标移动松手后才掉落。这个预览功能可以用一个独立的预览节点实现在FruitSpawner.onTouchMove中更新预览节点位置松手时隐藏并创建真实水果。第二个是合并动画。两个水果合成新水果时可以播放一个放大再回弹的缩放动画。Cocos Creator 3.x 支持 tween 动画系统也可以用动画编辑器的帧事件触发回调。热搜里提到的“动画帧事件怎么用”正好可以用在这个地方在合并动画的时间轴末尾挂一个帧事件事件触发后再调用GameManager.createFruit视觉上会比当前直接生成更自然。第三个是效果与变现。休闲小游戏的常见变现方式是插屏广告和激励视频广告。如果你要发到微信小游戏可以在游戏结束后弹出激励视频广告玩家看完广告可以获得一次复活机会。Cocos Creator 的微信小游戏构建流程已经比较成熟广告接入方式参考微信小游戏官方文档即可。这个功能与游戏核心逻辑互相独立不影响你现在的代码结构。第四个是构建 APK。Cocos Creator 3.8 在“项目 → 构建发布”中选择 Android 平台配置好 Android SDK 和 NDK 后可以生成 APK。这里要注意的是 Android 构建环境配置比较耗时建议按照官方文档一步一步操作。打包后如果发现性能不够优先检查水果节点数量及时销毁合并完成的水果避免场景里堆积过多节点。第五个是从“合成大西瓜”迁移到其他消除玩法。合成大西瓜本质是“合并消除”和消消乐这类“三消”玩法有一脉相承的设计思路检测相同元素、消除、生成新元素、刷新得分。掌握了本文的碰撞合并逻辑后你再去看消消乐案例会发现很多概念是通用的比如元素分组、消除判定、连锁反应这些都是同类休闲游戏的核心。最后给你一个实际项目建议先别急着加复杂功能把物理手感调好。同样的合并逻辑阻尼从 0 改成 2游戏的难度和观感会有巨大差异。物理参数没有绝对标准一定要在你自己的场景里反复试。每一轮修改前保留一个可运行版本这样调坏了一个参数不会陷入“改不回去”的尴尬。这个项目做完以后你对 Cocos Creator 3.8 的组件化开发、TypeScript 脚本生命周期、3D 物理系统和节点管理都会有一个比较完整的认知。建议把这个 Demo 保存下来后续不管是学习动画、接入广告还是迁移到其他休闲玩法都可以在它的基础上继续扩展。