这次我们来看一个非常典型的小游戏项目:用Cocos Creator 3.8制作 3D 版合成大西瓜。玩法大家都很熟,点击或触摸投放水果,相同等级的水果碰到一起就合并成下一等级,一路合成到“大西瓜”。区别在于这次不是 2D 平面逻辑,而是完整走 3D 物理引擎:刚体下落、球形碰撞、碰撞合并、警戒线失败判定,全套在 TypeScript 里实现。
对新手来说,这个项目最大的价值不是“能做一个游戏”,而是把 Cocos Creator 3.8 里最常用的 3D 开发链路完整走了一遍:场景搭建、Prefab 实例化、RigidBody 刚体配置、Collider 碰撞监听、触摸坐标转 3D 世界坐标、UI 分数更新、微信小游戏构建发布。这些能力学会了,后面做 3D 跳一跳、3D 消消乐、物理沙盒原型都能直接复用。
整篇文章会按“核心能力、适用场景、环境准备、场景搭建、物理配置、TS 脚本、功能测试、发布扩展、问题排查、最佳实践”的顺序展开。代码会给出可复制版本,但引擎 API 细节要以你本机安装的 3.8.x 版本为准,版本之间存在少量差异,下面开始。
1. 核心能力速览
| 能力项 | 说明 |
|---|---|
| 引擎版本 | Cocos Creator 3.8.x |
| 开发语言 | TypeScript(TS) |
| 项目类型 | 3D 物理休闲小游戏 |
| 核心玩法 | 点击/触摸投放水果,相同等级碰撞合并,合成大西瓜 |
| 学习重点 | 3D 场景搭建、RigidBody 刚体、SphereCollider 碰撞、Prefab 实例化、触摸转世界坐标 |
| 运行平台 | 编辑器预览窗口、Web 浏览器、微信小游戏等构建平台 |
| 电脑门槛 | 普通 PC 即可,能流畅运行 Cocos Creator 3.8 就行,3D 物理合并不需要高配独立显卡 |
| 输入交互 | 鼠标点击 / 手机触摸 |
| 扩展方向 | 微信小游戏激励广告、排行榜、每日任务、对象池优化 |
这个项目不需要联网,不依赖第三方后端,所有核心玩法都在本地模拟。你只需要装好 Cocos Creator 3.8,新建一个 3D 项目,就能按本文步骤跑起来。
2. 适用场景与使用边界
这个项目非常适合三类人:刚接触 Cocos Creator 的新手、想从 2D 转向 3D 的开发者、需要交游戏课设或做微信小游戏原型验证的团队。
它能解决的实际问题包括:理解 3D 游戏里“静态刚体”和“动态刚体”的区别,理解碰撞回调为什么不是在所有情况下都触发,理解触摸屏幕坐标怎么映射到 3D 场景,理解相同类型节点碰撞后如何避免重复合并。
它不擅长解决的问题也要说清楚:如果要做完善的联网对战、实时排行榜、复杂三消关卡,这个项目还需要大量扩展。它的定位是“核心玩法原型”,不是完整商业项目。
合规层面必须注意几点:第一,水果素材、背景图、音效如果来自网络,商用前要确认授权;第二,如果后续接入微信小游戏激励广告,必须使用微信官方广告组件,并且在小游戏后台申请广告位,不能绕过平台私自弹广告;第三,不涉及用户隐私数据采集,不需要额外隐私协议,但一旦加入账号系统、好友排行,就必须补全个人信息保护和平台审核要求。
3. 环境准备与前置条件
3.1 安装 Cocos Dashboard
Cocos Creator 现在的安装方式统一走 Cocos Dashboard。去 Cocos 官网下载对应系统的 Dashboard 安装包,安装完成后登录 Cocos 账号,在 Dashboard 的“编辑器”页面选择 3.8.x 版本下载。建议下载 3.8 LTS 版本,稳定性和文档匹配度都更好。
3.2 新建 3D 项目
打开 Dashboard,点击“新建项目”,项目模板选择“3D”。创建后会进入 Cocos Creator 3.8 编辑器。项目里默认会有一个场景文件,后续的 3D 物体、相机、灯光都在这个场景中搭建。
创建项目时注意项目路径不要包含中文和空格,Cocos 对非 ASCII 路径偶尔会有兼容问题。这是新手最容易忽略的点。
3.3 确认物理模块可用
Cocos Creator 3.8 的 3D 物理不是一个默认就完整可用的状态,需要确认两件事:
第一,在“项目设置 -> 功能裁剪”里,确认 3D 物理相关模块被勾选;第二,在“项目设置 -> 物理”里,选择一个可用的 3D 物理后端。不同物理后端在碰撞精度和性能上有差异,但对合成大西瓜这种简单刚体场景,默认配置就够用。
如果不确认模块开启情况,最直接的方法是跑一个最简单的物理测试:创建一个 Box 节点,挂上 RigidBody 和 BoxCollider,点击预览,看它是否正常下落到地面。能下落,说明物理环境没问题。
3.4 目录结构规划
建议在 assets 下按功能建目录:
assets/ scenes/ 场景文件 scripts/ TS 脚本 prefabs/ 水果 Prefab materials/ 材质 textures/ 图片资源 audio/ 音效资源这样做的原因是后面做微信小游戏构建时,资源查找、分包加载、异常排查都会清晰很多。不要把所有资源都放在 assets 根目录,尤其是水果 Prefab 和材质一多起来,根目录会非常混乱。
4. 搭建 3D 场景与基础物体
4.1 调整场景相机
新建 3D 项目后,场景里默认有一个 Main Camera 和一个 Directional Light。合成大西瓜需要一个俯视视角:相机放在场景上方偏后的位置,向下看。
选中 Main Camera,在属性检查器中设置:
- position 大约在
(0, 15, 14) - rotation 大约在
(-45, 0, 0)
然后微调位置,让整个“容器”出现在画面中间。手机竖屏玩家要看到的是整个容器和水果下落区域,所以相机视口要保证左右墙和底部都在画面内。
4.2 创建地面和墙体
合成大西瓜需要一个开放式的“碗”:底部有地面,左右有墙,后侧有墙,前方不封口,这样玩家才能看到水果。如果四周完全封闭会挡住视线。
在场景中创建 Box 节点,分别命名 Ground、LeftWall、RightWall、BackWall。Box 默认是 1×1×1,直接改 scale 和 position 来拼出容器尺寸。
一个可以参考的配置:
| 节点 | Scale | Position |
|---|---|---|
| Ground | (12, 0.5, 10) | (0, -0.25, 0) |
| LeftWall | (0.5, 8, 10) | (-6, 4, 0) |
| RightWall | (0.5, 8, 10) | (6, 4, 0) |
| BackWall | (13, 8, 0.5) | (0, 4, -5) |
每个墙体节点都要添加 BoxCollider 组件,并添加 RigidBody 组件,刚体类型选择STATIC。因为墙和地面不运动,不需要动态模拟,静态刚体性能更好,也不会被水果撞飞。这里最容易犯的错误是只加 Collider 不加 RigidBody,或者刚体类型保持默认动态,然后发现整个容器被水果撞得乱跑。
4.3 添加基础材质
为区分地面、墙体和水果,建议创建几个不同颜色的材质。在 assets 下新建材质,选择 Surface 类型,调整 Base Color 后拖到对应节点上。不需要太复杂,颜色区分清楚就行。
主要看效果,不需要一上来就调 PBR 参数。
5. 物理与碰撞配置
5.1 给水果创建 Prefab
水果不能每个都手动创建节点。正确做法是先做一个基础水果 Prefab,后续通过instantiate实例化。
新建一个 Sphere 节点,命名为 Fruit,添加 SphereCollider 和 RigidBody。RigidBody 类型保持DYNAMIC,这样水果才能受重力下落。然后把这个节点拖到 assets/prefabs 目录下,生成 Fruit.prefab。
如果要做多种等级水果,可以复制多个 Prefab,每个用不同颜色和大小,也可以只用一个 Prefab,在运行时通过setScale修改大小、通过材质实例修改颜色。对新手来说,多个 Prefab 更直观,代码量也更少。
5.2 设置物理材质
水果之间会有弹跳,但弹跳太大会导致合并不稳定。可以在 assets 下创建一个 PhysicsMaterial,把摩擦系数调低,恢复系数调到 0.2 左右,然后拖到水果 Prefab 的 SphereCollider 材质属性里。这样水果掉落到地面后不会一直弹跳,物理模拟会更接近真实“合成大西瓜”的感觉。
5.3 碰撞分组
Cocos Creator 3D 物理默认分组是DEFAULT。简单项目不需要自定义分组,让所有水果和墙体的碰撞分组都处于默认分组,并且碰撞矩阵中该分组的“与自己碰撞”和“与其他默认分组碰撞”都开启即可。复杂项目再考虑把水果、墙、警戒线分成不同 group。
5.4 警戒线节点
合成大西瓜的失败条件是“水果堆到警戒线以上”。实现方式有两种:一种是普通碰撞传感器;另一种是逻辑判断。这里推荐逻辑判断:不需要额外创建不可见碰撞体,直接在代码里设置一个loseY数值,每帧检查活跃水果是否有静止在警戒线上方的情况。
6. TS 脚本实现核心玩法
6.1 FruitItem.ts 水果节点脚本
创建一个脚本FruitItem.ts,挂在水果 Prefab 上。这个脚本负责记录水果等级、监听碰撞、触发合并事件。
import { _decorator, Collider, Component, ICollisionEvent, Vec3 } from 'cc'; const { ccclass, property } = _decorator; @ccclass('FruitItem') export class FruitItem extends Component { // 水果等级:0 最小,逐级递增 @property level = 0; private merged = false; onLoad() { const collider = this.getComponent(Collider); if (collider) { collider.on('onCollisionEnter', this.onCollisionEnter, this); } this.merged = false; } private onCollisionEnter(event: ICollisionEvent) { if (this.merged) return; const other = event.otherCollider; if (!other) return; const otherFruit = other.getComponent(FruitItem); if (!otherFruit) return; if (otherFruit.merged) return; if (otherFruit.level !== this.level) return; // 两个节点的碰撞回调都会触发,只让其中一个节点执行合并 if (this.node.uuid > otherFruit.node.uuid) return; // 再检查一次节点有效性,避免销毁后继续处理 if (this.node.destroyed || otherFruit.node.destroyed) return; this.merged = true; otherFruit.merged = true; const mergePos = new Vec3( (this.node.position.x + otherFruit.node.position.x) / 2, (this.node.position.y + otherFruit.node.position.y) / 2, (this.node.position.z + otherFruit.node.position.z) / 2 ); // 向外层发送合并事件,由 GameManager 生成新的水果 this.node.emit('fruit-merge', { level: this.level + 1, position: mergePos }); this.node.destroy(); otherFruit.node.destroy(); } }核心思路:每个水果都有单独的level。碰撞发生时,判断对方是不是水果、等级是否相同、是不是 already 合并过。三个条件都满足,才执行合并。
特别注意:碰撞双方都各自注册了回调,如果不加merged标记,同一个碰撞对会被处理两次,导致生成两个大水果。这里的this.node.uuid > otherFruit.node.uuid是一种简单但可靠的“单边处理”策略。
6.2 GameManager.ts 游戏管理脚本
创建一个空节点 GameManager,挂GameManager.ts。这个脚本负责:监听触摸、投放水果、随机等级、接收合并事件、更新分数、判断游戏结束。
import { _decorator, Camera, Component, EventTouch, input, Input, instantiate, Label, Node, Prefab, RigidBody, Vec3 } from 'cc'; import { FruitItem } from './FruitItem'; const { ccclass, property } = _decorator; @ccclass('GameManager') export class GameManager extends Component { // 水果 Prefab 数组,按等级排列 @property({ type: Prefab }) fruitPrefabs: Prefab[] = []; // 水果节点统一挂载的父节点 @property({ type: Node }) spawnRoot: Node = null!; // 分数 UI @property({ type: Label }) scoreLabel: Label = null!; // 投放高度 @property dropY = 12; // 警戒线高度 @property loseY = 8; private score = 0; private fruits: Node[] = []; private isOver = false; onLoad() { 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.isOver) return; const camera = Camera.main; if (!camera) return; // 获取触摸在屏幕上的位置 const uiPos = event.getUILocation(); const screenPos = new Vec3(uiPos.x, uiPos.y, 10); const worldPos = camera.screenToWorld(screenPos); // 只使用点击得到的 X 坐标,Y 固定为投放高度,Z 固定为 0 const spawnPos = new Vec3(worldPos.x, this.dropY, 0); this.spawnFruit(spawnPos); } private spawnFruit(pos: Vec3) { const level = this.randomLevel(); const prefab = this.fruitPrefabs[level]; if (!prefab) return; const fruitNode = instantiate(prefab); fruitNode.setParent(this.spawnRoot); fruitNode.setPosition(pos); const fruit = fruitNode.getComponent(FruitItem); if (fruit) { fruit.level = level; } const scale = this.getScaleByLevel(level); fruitNode.setScale(scale, scale, scale); fruitNode.on('fruit-merge', this.onFruitMerge, this); this.fruits.push(fruitNode); } private randomLevel(): number { // 用概率表控制随机等级,开局尽量出小水果 const table = [0, 0, 0, 0, 1, 1, 1, 2, 2, 3]; const idx = Math.floor(Math.random() * table.length); return table[idx] ?? 0; } private getScaleByLevel(level: number): number { return 0.5 + level * 0.15; } private onFruitMerge(data: { level: number; position: Vec3 }) { // 如果已经超过最大等级,只加分,不再生成新水果 if (data.level >= this.fruitPrefabs.length) { this.addScore(50); return; } const prefab = this.fruitPrefabs[data.level]; if (!prefab) return; const fruitNode = instantiate(prefab); fruitNode.setParent(this.spawnRoot); fruitNode.setPosition(data.position); const fruit = fruitNode.getComponent(FruitItem); if (fruit) { fruit.level = data.level; } const scale = this.getScaleByLevel(data.level); fruitNode.setScale(scale, scale, scale); fruitNode.on('fruit-merge', this.onFruitMerge, this); this.fruits.push(fruitNode); this.addScore(data.level * 10); // 清理已被销毁的节点引用 this.fruits = this.fruits.filter((n) => n.isValid); } private addScore(value: number) { this.score += value; if (this.scoreLabel) { this.scoreLabel.string = `分数:${this.score}`; } } update() { if (this.isOver) return; for (const node of this.fruits) { if (!node || !node.isValid) continue; if (node.position.y <= this.loseY) continue; const rigidBody = node.getComponent(RigidBody); if (rigidBody && rigidBody.linearVelocity.length() < 0.5) { this.gameOver(); return; } } } private gameOver() { this.isOver = true; if (this.scoreLabel) { this.scoreLabel.string = `游戏结束,最终分数:${this.score}`; } // 这里可以继续扩展:重新开始按钮、广告复活、上传分数等 } }这个脚本有几个点值得单独说明。
第一,randomLevel用了固定概率表,前几级出现概率高。直接Math.random()会偶尔连续出大水果,新手测试时体验会很差。概率表实现简单,也能让游戏前期更平滑。
第二,camera.screenToWorld需要传入一个带深度信息的屏幕坐标。这里深度取 10,是因为相机距离场景中心较远,取一个合适的前方深度能算出场景内的点击位置。实际项目中要根据相机位置调整这个深度参数。如果发现点击生成的水果 X 坐标偏了,优先检查这里。
第三,linearVelocity.length()用来判断水果是否接近静止。警戒线判断不能只看位置,因为刚掉落到警戒线附近的动态水果速度很大,应该允许它继续下落。只有当水果停在警戒线上方且速度很小,才判定游戏结束。这是一种比“碰撞传感器”更容易控制的失败判定方式。
6.3 在编辑器中关联属性
写完脚本后,回到编辑器:
- 把 GameManager 脚本挂到场景中的 GameManager 节点。
- 创建 Canvas 下的 Score 节点,添加 Label 组件,绑定到 GameManager 的 scoreLabel 属性。
- 创建 SpawnRoot 空节点,设置为水果的统一父节点。
- 把多个水果 Prefab 按等级顺序拖到 fruitPrefabs 数组里。
- 调整 dropY 和 loseY 的值,让投放高度和警戒线高度匹配你的场景尺寸。
没有在编辑器中正确关联 Prefab 数组,是新手最容易遇到的空引用报错。运行时如果看到 “fruitPrefabs[level] is null” 或 “Cannot read property” 这类错误,先回到编辑器检查属性是否绑定成功。
7. 功能测试与效果验证
7.1 基础投放测试
点击编辑器上方的预览按钮,浏览器打开游戏后,点击场景任意位置,观察是否有一个水果从顶部掉下来。判断标准:
- 水果受重力下落,而不是卡在原位。
- 水果落在底部后不会无限弹跳。
- 连续点击多个水果,它们会堆叠而不是重叠穿透。
穿透通常是因为 Collider 尺寸不对或物理步长过大。水果是球形,推荐使用 SphereCollider 并勾选“跟随节点缩放”相关配置。
7.2 合并测试
投放两个相同等级的水果,让它们碰撞。观察它们是否消失,并生成一个等级更高的新水果。判断标准:
- 相同等级碰撞,且只生成一个大水果。
- 不同等级碰撞不合并。
- 同一个碰撞对不会生成两个新水果。
如果生成了两个大水果,说明merged标记或 uuid 单边处理逻辑没生效。检查 FruitItem 里是否在onCollisionEnter开头就 return 了已经合并过的节点。
7.3 分数测试
每次合并后,分数应该增加。到最高等级时会触发加分但不生成新水果的逻辑,此时调试日志或分数面板应该能看到对应变化。
7.4 游戏结束测试
连续投放大量水果,让水果堆到警戒线上方并静止。观察是否弹出“游戏结束”。判断标准:
- 水果运动过程中即使超过警戒线,也不会立刻判定失败。
- 水果静止在警戒线上方时,才会触发失败。
- 游戏结束后再点击,不再生成新水果。
如果游戏刚开始就判定失败,说明失败判定没有判断速度,或者loseY设置过低、新水果生成时已经处于警戒线上方。把投放高度dropY和警戒线loseY拉开差距即可。
7.5 性能观察
在编辑器预览窗口左侧打开 Profiler,重点关注节点数和物理耗时。合成大西瓜的水果数量完全由玩家投放和合并产生,游戏越到后期,场景中水果越多,DrawCall 和物理计算压力越大。
如果出现明显卡顿,常见的优化顺序是:关闭阴影、减少粒子、给水果节点做对象池而不是频繁 instantiate/destroy、把墙体碰撞体合并成更少的静态碰撞体。
8. 发布构建与接口扩展
8.1 构建 Web 版本
在 Cocos Creator 编辑器顶部菜单点击“项目 -> 构建发布”,平台选择 Web Desktop 或 Web Mobile。构建完成后,在输出目录里会生成可以直接部署到静态服务器上的 Web 文件。
Web 版本适合快速分享和真机预览。构建后建议用本地静态服务器打开,不要直接用 file 协议双击 index.html,避免浏览器安全策略导致资源加载异常。
8.2 构建微信小游戏版本
在构建发布面板中,平台选择“微信小游戏”,填入小游戏的 AppID,点击构建。构建完成后用微信开发者工具打开构建输出目录。
这里有一个重要判断:微信小游戏运行环境对 WebGL 的支持和桌面浏览器不完全一致。如果真机出现黑屏、水果不显示,优先检查物理后端和渲染后端是否在目标平台可用。
8.3 微信小游戏激励广告扩展方向
合成大西瓜常见的商业化玩法是“看广告复活”或“看广告加分”。实现上使用的是微信小游戏官方接口,和小程序广告基本一致。
// 仅在微信小游戏环境调用 if (typeof wx !== 'undefined' && wx.createRewardedVideoAd) { const ad = wx.createRewardedVideoAd({ adUnitId: '测试广告位ID' }); ad.onClose((res: any) => { // res.isEnded 为 true 表示完整播放 if (res && res.isEnded) { // 发放复活或加分奖励 } else { // 中途退出,不发奖励 } }); ad.show().catch(() => { ad.load().then(() => ad.show()); }); }注意三件事:广告位 ID 必须在微信小游戏后台申请,开发时可以使用测试广告位;广告播放前要在游戏 UI 中明确提示玩家“观看视频可获得奖励”,不能诱导点击;广告相关代码要做好平台判断,浏览器预览环境下没有wx对象,不能直接调用。
8.4 排行榜与后端接口
如果要做排行榜,可以接微信小游戏开放数据域,或者自建后端 HTTP 接口。Cocos Creator 里发请求可以用fetch或XMLHttpRequest,逻辑和普通 Web 开发一致。核心注意点:不要在客户端直接信任分数值,最终排名数据必须由服务端校验;如果只是学习演示,本地用云开发数据库即可。
9. 常见问题与排查方法
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 水果直接穿过地面 | 地面缺少 RigidBody 或 Collider | 检查地面节点的物理组件 | 给地面添加静态刚体和 BoxCollider |
| 水果被碰撞后弹飞 | 物理材质恢复系数过高 | 查看 PhysicsMaterial 参数 | 把 restitution 调到 0.2 左右 |
| 两个水果同时生成大水果 | 合并事件被重复处理 | 检查 FruitItem 事件回调 | 添加上面提到的 merged 标记和 uuid 单边处理 |
| 点击位置和水果 X 坐标偏差大 | screenToWorld 深度参数不对 | 打印 worldPos 和 spawnPos | 调整屏幕坐标的 z 值,或按相机距离计算 |
| 水果卡在半空不动 | 刚体类型不是 DYNAMIC | 选中水果查看 RigidBody | 把刚体类型改为 DYNAMIC |
| 预览时有物理但无重力 | 物理后端配置异常 | 检查项目设置里的物理选项 | 切换可用后端,或创建一个测试 Box 验证 |
| 微信小游戏打开白屏 | 构建平台设置或渲染后端问题 | 打开微信开发者工具控制台 | 切换 WebGL 版本,确认功能裁剪模块包含 3D 物理 |
| 游戏结束时点击仍有水果生成 | isOver 逻辑没生效 | 检查 onTouchStart 开头判断 | 确认 gameOver 里 isOver 置为 true |
| 分数更新不及时 | Label 引用未绑定 | 检查 GameManager 属性面板 | 重新绑定 scoreLabel 节点组件 |
| 水果节点销毁后回调报错 | 碰撞回调中访问了已销毁节点 | 在回调中打印 node.isValid | 增加 destroyed 和 isValid 判断 |
10. 最佳实践与使用建议
10.1 先跑通最小闭环
不要一上来就做十几种水果 Prefab 和复杂 UI。先创建一个 Sphere 水果,一个地面,一个 GameManager 脚本。跑通“点击 -> 投放 -> 下落 -> 碰撞 -> 销毁”这五步,再逐步加等级、分数、警戒线、音效。最小闭环可以帮你把“物理环境是否正常”和“脚本事件链路是否正常”两个最容易出问题的部分先解决掉。
10.2 使用对象池管理水果
当前代码为了演示清晰直接instantiate和destroy。项目进入中后期后,建议写一个对象池:
- 创建水果时从对象池取节点,取不到才
instantiate。 - 销毁水果时把节点回收,而不是直接
destroy。 - 对象池统一管理,避免频繁创建销毁导致 GC 压力。
对象池改造对移动端性能提升非常明显,尤其是微信小游戏里,小游戏包运行内存受限,对象池几乎是必须的优化项。
10.3 配置表驱动水果数据
水果等级、半径、分数、出现概率、颜色这些数据不要全部散落在脚本里。建议使用 JSON 或 TS 配置对象统一维护:
const FRUIT_CONFIG = [ { level: 0, score: 1, probability: 30, scale: 0.5 }, { level: 1, score: 3, probability: 25, scale: 0.65 }, { level: 2, score: 5, probability: 15, scale: 0.8 } // 后续等级继续补充 ];需要调整数值时只改配置,不改逻辑。这对调游戏手感和平衡性非常重要。
10.4 打开物理调试线框
Cocos Creator 的 3D 物理调试线框很实用。项目设置里开启物理调试后,场景中会直接显示所有 Collider 的实际范围。如果水果看起来碰到了墙却没反弹,基本就是 Collider 位置或尺寸和模型错位,通过线框一眼就能看出来。
10.5 真机测试要提前
不要在开发最后一天才构建到手机。Web 预览正常不代表微信小游戏真机正常,物理后端、渲染后端、资源加载方式都可能有差异。建议从初期开始,每周至少做一次微信小游戏构建,确认核心玩法在目标平台上没有明显问题。
10.6 资源和素材合规
水果贴图可以自己画,也可以用可商用图库。音效同理。如果做公开演示或发布小游戏上线版本,必须在项目工程里记录素材来源和授权情况。不要从搜索引擎随便找图就用,尤其是水果这种带品牌形象的商品图片,存在版权风险。
11. 总结与下一步
这个项目最值得尝试的点,是用最少的代码把 Cocos Creator 3.8 的 3D 物理链路完整跑通。它比 2D 合成大西瓜更有“手感”差异,也比单纯跟着文档写“Hello Box”更有驱动力。
你最先应该验证的功能是“点击投放 -> 水果下落 -> 两个同等级水果碰撞合并”这条核心链路。只要这条链路通了,分数、失败判定、UI、广告都是水到渠成的增量。
最容易踩的坑有三个:水果 Prefab 缺少 RigidBody 导致不落下、碰撞回调缺少merged标记导致重复合并、触摸坐标转 3D 世界坐标的深度参数不对。这三个问题在本文第 6 章和第 9 章都有对应说明,建议收藏备用。
下一步可以按兴趣继续扩展:给水果加对象池和音效,接入微信小游戏激励广告做复活功能,加“重新开始”按钮和排行榜云存储,或者把玩法从“投放合成”改成“拖拽瞄准发射”,适配更多 3D 物理玩法。
3D 合成大西瓜本身是一个很好的学习型项目,做完之后你对 Cocos Creator 3.8 的 3D 场景管理、TypeScript 组件通信、物理碰撞和构建发布的理解,会比单纯看文档扎实得多。接下来打开编辑器,先建一个 Sphere 和地面,把最小闭环跑起来。