Cocos Creator 3.8 实战:用 TypeScript 打造 3D 合成大西瓜
Cocos Creator 3.8 做 3D 版合成大西瓜这个项目非常适合作为游戏开发入门 Demo。它把 TypeScript 逻辑、3D 场景、物理碰撞、预制体、UI 和构建发布全串在一起。做完以后你基本能理解一个小游戏从零到可玩要经过哪些环节。适合人群很明确刚接触 Cocos Creator、会一点 TypeScript 或至少愿意边写边查的人。如果只是听说过“合成大西瓜”但没玩过也没关系下面会先把玩法拆开。我先说结论3D 版不是把原来的 2D 玩法改成自由三维堆叠那样操作会很别扭。更稳妥的做法是用 3D 场景和 3D 模型但把物理运动限制在 X-Y 平面内Z 轴锁死。也就是说表现层是 3D玩法层还是经典合成大西瓜。理解这一点后后面的工程实现会清晰很多。下面按实际开发顺序拆开讲。1. 先拆需求3D版合成大西瓜到底要做什么1.1 经典合成大西瓜的核心玩法合成大西瓜的核心规则并不复杂点击掉落水果两个相同等级的水果碰到一起就合成一个更高等级的水果不同等级对应不同大小和分数水果堆得太高超过警戒线就结束。所以核心系统是四个生成、物理掉落、碰撞合并、堆叠判定。所有代码都围绕这四个点展开。很多新手一上来就找美术资源、调粒子特效这是反的。先让四个核心系统跑通再谈表现。因为生成逻辑决定你点一次是不是出一个球物理掉落决定球是不是会动碰撞合并决定玩法能不能成立堆叠判定决定一局什么时候结束。1.2 3D玩法和2D玩法的取舍当你决定做“3D版”时最容易犯的错是让水果在三维空间里自由滚动。从玩家的角度看合成大西瓜的操作本质是左右移动掉落位置如果水果还会前后滚动玩家很容易失控。所以我建议这样设计摄像机从侧面或斜上方观察但物理世界仍然是一个纵向平面物体只在这个平面内下落。实现上给刚体设置 Linear Factor把 Z 方向锁死角速度也只保留绕 Z 轴的旋转。这样既能展示 3D 空间感又不会牺牲可玩性。还有一个好处物理调参会简单很多。物体不会因为从屏幕里滚出来而丢失也不会在堆叠时因为前后错位产生奇怪的卡顿。1.3 为什么用 Cocos Creator 3.8 和 TypeScript3.x 版本把 2D/3D 统一在一起不需要像旧版本那样分开建项目。3.8 作为 3.x 的一个稳定版本社区案例和教程比较多遇到问题容易搜到。TypeScript 提供类型提示对避免低级命名错误很有帮助。引擎内置 Bullet 物理后端刚体、碰撞体、触发器都是可视化配置不需要自己写物理引擎。动画、UI、场景管理也都是现成的。对新手来说这套组合最大的优势是一个项目里能学到的内容足够完整但每个模块的复杂度又没有高到劝退。2. 环境准备建工程、开物理、搭一个能看的场景2.1 创建3D工程和基础结构用 Cocos Dashboard 安装 Cocos Creator 3.8然后新建项目模板选 Empty3D。不建议用 2D 模板因为我们要用 3D 相机和三维坐标。建完后项目结构里有 assets、scene 和 settings先打开默认场景。把默认节点清理一下保留 Main Camera 和 Directional Light或者自己也重建一份。建议一开始就给节点改好名字比如 Camera、Light、GameRoot。后面脚本挂载和查找节点都依赖名字和层级随手命名能省很多排查时间。相机位置也很重要。我常用的是把相机放在(0, 3, 8)看向(0, 2, 0)。这样场景有纵深感又能完整看到容器内部。如果你相机离得太近点击生成位置容易偏离得太远水果大小又看不清。2.2 开启物理系统和调试可视化Cocos Creator 3.8 默认有物理模块但正式开发前要确认两件事第一物理系统是否启用第二物理调试是否打开。物理系统如果关闭刚体不会动也不会产生碰撞事件脚本写得再对也没用。打开物理调试可视化非常关键。第一次做物理游戏看不到碰撞体边界很多问题没法判断。比如水果明明看着碰到了但就是没触发合并很可能是碰撞体半径太小或位置偏移。把物理调试打开物体边缘的绿色线框会直接显示真实碰撞范围。所以我一般建议搭建阶段就打开物理调试跑通基础掉落后再关掉。关掉只是一行配置但调错时开着能救命。2.3 搭一个简易容器地板、左右墙和顶部生成区因为玩法限制在 XY 平面我们只需要一个宽度有限的“桶”。场景节点可以这样组织GameRoot Floor LeftWall RightWall Camera DirectionalLightFloor 用一个 Plane 或 Box 都行挂在(0, 0, 0)位置添加BoxCollider。左右墙用两个竖立的长方体也是加BoxCollider。这里不需要给静态物体加RigidBody静态碰撞体直接参与碰撞。一个常见误区静态物体只有 Collider没有 RigidBody 也能正常阻挡动态物体。加了 RigidBody 反而可能变成动态物体被水果撞跑。容器尺寸要和你预估的水果大小匹配。我一般把生成区域宽度控制在 2 到 3 米高度 4 米左右。太宽会导致一局拖很久太窄前期就容易堆满。3. 创建水果预制体从一颗球到一套等级3.1 用Sphere先代替美术资源刚起步不需要美术模型。在场景中创建一个 Sphere改名为 Fruit_1。调整半径新建一个 Material把 Albedo 改成红色拖到 MeshRenderer 上。这就是第一个水果。创建完后拖到assets/resources/Fruits目录下生成预制体再把场景里的临时节点删掉。然后复制这个预制体修改材质颜色和半径做出 Fruit_2、Fruit_3 等。一般建议先做 5 到 6 个等级等核心玩法跑通后再扩充到 11 个。等级太多会增加美术、数值和物理调参的工作量。尤其是等级越高半径越大碰撞、堆叠、判定都会受影响。先用少量等级验证手感再慢慢加是最稳的路。3.2 给水果加刚体和碰撞体每个水果预制体上至少要有三个组件MeshRenderer负责显示RigidBody负责物理运动SphereCollider负责碰撞检测物理参数可以参考下面这张表参数建议值原因RigidBody TypeDYNAMIC要受重力并参与碰撞Use Gravitytrue否则不会掉落LinearDamping0.5~1.0减缓反弹降低晃动AngularDamping0.8~1.0抑制乱滚Linear Factorx1, y1, z0锁死Z轴保证2D玩法Angular Factorx0, y0, z1只允许平面内旋转Friction0.6堆叠时不容易滑走Restitution0.1反弹不要太高这些数值不是绝对标准但方向是对的。重点不是抄数字而是理解为什么设置阻尼和因子。合成大西瓜这个玩法需要水果“稳”不能像弹球一样疯狂反弹。阻尼调高一点手感会好很多。3.3 用配置脚本管理水果等级数据在写玩法逻辑之前先建一个FruitConfig.tsexport interface FruitConfig { level: number; prefab: Prefab; radius: number; score: number; }然后在 GameManager 里放一个property数组把水果预制体按等级顺序拖进去。为什么不在场景里手工管理每个水果节点因为后续合并时需要根据等级动态创建新水果脚本里有一个配置表比到处找节点更高效。等级和分数可以先简单设置等级 1 得 10 分等级 2 得 20 分依次递增。半径从 0.2 开始每级增加 0.1 到 0.15。这个不是最优数值但足够测试。4. 核心玩法脚本点击生成、碰撞合并、计分判定4.1 GameManager的职责划分一个全局单例脚本管理所有玩法逻辑。职责包括持有水果配置监听输入生成水果处理合并更新分数判定失败不要把合并逻辑散落到每个水果脚本里写死。水果脚本只负责上报“我和同级水果碰撞了”如何生成新水果、加分、判断结束由 GameManager 统一处理。这样后续改规则会省很多事。举个例子以后你想加“连击倍率”只需要改 GameManager 里的加分支线而不是去每个水果节点里找代码。4.2 点击生成坐标转换和等级随机生成逻辑注意两个坑。第一点击的屏幕坐标要转换成 3D 坐标不要直接拿触摸位置当世界坐标。第二新生成的水果建议随机 1 或 2让前期节奏舒服。如果直接随机生成最高等级游戏会在开始几秒内就结束。一个简化版的 GameManager 核心代码import { _decorator, Component, Prefab, instantiate, Vec3, input, Input, EventTouch, Camera, Node } from cc; const { ccclass, property } _decorator; ccclass(GameManager) export class GameManager extends Component { private static inst: GameManager; public static get instance() { return this.inst; } property({ type: [Prefab] }) fruitPrefabs: Prefab[] []; property spawnY 5; property minX -1.5; property maxX 1.5; private cfgs: FruitConfig[] []; onLoad() { GameManager.inst this; this.buildConfigs(); input.on(Input.EventType.TOUCH_START, this.onTouchStart, this); } private buildConfigs() { for (let i 0; i this.fruitPrefabs.length; i) { this.cfgs.push({ level: i 1, prefab: this.fruitPrefabs[i], radius: 0.2 i * 0.12, score: (i 1) * 10, }); } } private onTouchStart(event: EventTouch) { const uiPos event.getUILocation(); const camera Camera.main!; const worldPos camera.screenToWorld(new Vec3(uiPos.x, uiPos.y, 10)); const x Math.max(this.minX, Math.min(this.maxX, worldPos.x)); const level Math.random() 0.7 ? 1 : 2; this.spawnFruit(x, level); } public spawnFruit(x: number, level: number) { const cfg this.cfgs[level - 1]; const node instantiate(cfg.prefab); node.setParent(this.node); node.setWorldPosition(new Vec3(x, this.spawnY, 0)); const fruit node.getComponent(Fruit); fruit.level level; } }这里的screenToWorld第三个参数是深度。实际工程里要根据相机位置和容器平面调整。更严谨的做法是用射线和容器平面求交点但 Demo 阶段先用这种简化方式能跑通再改。4.3 碰撞合并同级合并、防重复Fruit 脚本监听碰撞事件import { _decorator, Component, Collider, ICollisionEvent } from cc; import { GameManager } from ./GameManager; const { ccclass } _decorator; ccclass(Fruit) export class Fruit extends Component { public level 1; public isMerging false; private collider: Collider null; onLoad() { this.collider this.getComponent(Collider); this.collider.on(onCollisionEnter, this.onCollisionEnter, this); } private onCollisionEnter(event: ICollisionEvent) { if (this.isMerging) return; const other event.otherCollider.getComponent(Fruit); if (!other || other.isMerging) return; if (other.level ! this.level) return; this.isMerging true; other.isMerging true; const pos this.node.worldPosition.clone() .add(other.node.worldPosition) .multiplyScalar(0.5); const selfNode this.node; const otherNode other.node; this.scheduleOnce(() { GameManager.instance.onMerge(pos, this.level); selfNode.destroy(); otherNode.destroy(); }, 0); } }为什么延迟到下一帧销毁因为在物理碰撞回调里直接销毁节点偶尔会出现空引用或者物理引擎还在处理该刚体的情况。先标记isMerging再调度一帧后销毁能避开大多数问题。为什么加isMerging因为同一组碰撞可能同时触发两次回调如果没有标记会在同一个物理步里重复生成高级水果。加了标记后第二个回调看到双方已经在合并状态直接返回。GameManager 里对应的 onMergepublic onMerge(worldPos: Vec3, level: number) { const nextLevel Math.min(level 1, this.cfgs.length); const cfg this.cfgs[nextLevel - 1]; this.addScore(cfg.score); const node instantiate(cfg.prefab); node.setParent(this.node); node.setWorldPosition(worldPos); const fruit node.getComponent(Fruit); fruit.level nextLevel; } private addScore(score: number) { // 这里更新你的UI Label或者先打印日志 console.log(score score); }4.4 计分、警戒线与游戏结束计分可以直接用 Canvas 里的一个 Label也可以先用console.log验证。每次合并得到新一级水果的分数即可。警戒线可以设置成一条透明长条节点也可以直接用一条世界坐标 Y 值判断。但要注意水果碰撞瞬间会有向上弹跳所以不能在“刚超过线”的瞬间立刻判定结束否则正常碰撞也会被误判。一种更稳定的做法在容器顶部放一个很薄的BoxCollider勾选为触发器。当任意水果进入触发区域就启动“倒计时结束”逻辑。用触发器比遍历所有节点更符合物理直觉代码也更简单。游戏结束后的处理可以先做两件事关掉点击生成弹出结算面板。至于重新开始直接重新加载当前场景是最快的做法。5. 从能跑到能玩对象池、物理调优和常见问题排查5.1 水果数量上来后先做对象池刚开始只有几个水果时每次instantiate和destroy问题不大。但当你把等级、计分、音效全做完一次连消可能会同时销毁十几个节点手机上就会卡。这时候再引入NodePool节点池。使用节点池时要注意重置状态。回收节点前先把刚体速度清零、禁用生成触发、把位置放回默认值。下次从池里取出时要重新设置等级、颜色、半径、刚体速度等。如果只缓存固定几种等级建议每个等级单独一个池子。一个常见的坑是对象池把水果节点回收后节点被pool.put(node)移出场景树。但它的 RigidBody 还保留着原来的速度下次取出来时如果不重置水果会带着旧速度飞走。5.2 常见问题排查表现象优先检查顺序水果不动刚体是否 DYNAMICUse Gravity 是否开启物理系统是否启用Collider 是否在同一个节点水果穿透碰撞体半径是否太小物理步长是否太大刚体速度是否过快是否在碰撞回调里改了位置合并重复触发Fruit 脚本是否加了 isMerging是否在 onCollisionEnter 里直接实例化和销毁两个水果节点是否重叠过多水果乱飞LinearDamping / AngularDamping 是否太小LinearFactor 是否没锁 Z 轴碰撞体半径是否不准点击位置不对相机深度参数screenToWorld 的 z 值Camera.main 是否取到正确相机生成后卡在墙里生成点是否在容器范围内预制体初始坐标是否被手改过排查顺序永远是先看现象再看输入再看环境最后才怀疑引擎或插件。尤其是水果不动很多时候不是代码错了而是某个组件没挂上或者物理系统没开启。5.3 如何判断性能是否够用不要只看编辑器里的帧率。真机上特别是低端安卓机物理物体一多就会出现堆叠抖动和帧率下降。我一般会看三点场景中动态水果不超过 30 个节点时能否稳定运行连续多次合并后是否出现明显卡顿打开 Profiler看 Game Logic 和 Physics 的耗时占比如果物理耗时明显偏高优先做三件事减少水果总等级缩短单局时长缩小容器宽度降低堆叠规模引入对象池减少 GC 造成的卡顿。不要一上来就调碰撞精度先确认问题是不是节点太多导致的。6. 从Demo到发布APK、UI和后续优化6.1 构建Android包前先核对工具链Cocos Creator 3.8 可以直接构建到 Android。流程大致是项目 - 构建发布 - 新建 Android 平台构建任务。真正容易出问题的不是编辑器而是本机的 Android SDK、NDK、Gradle 版本不匹配。如果你以前没用过原生 Android 工具链建议先按 Cocos 官方文档装对应版本不要随便用最新版。如果只是学习可以先构建 Web Mobile 版本在手机浏览器里看效果避免一上来就折腾原生工程。先验证玩法和性能再考虑打包会轻松很多。6.2 加UI、音效和广告位UI 用 Canvas 实现分数 Label、结算按钮、开始界面都是常规操作。音效可以在每次合并时播放AudioSource 直接挂在 GameRoot 上倒入一个短音频就行。广告接入要分平台微信小游戏、Android 原生 SDK 各不相同。以微信激励广告为例需要先在微信公众平台申请小游戏 AppID再按微信小游戏广告组件 API 接入。核心玩法没稳定前不建议先接广告否则每次调广告 API 都会多加一层排查成本。6.3 真正值得继续优化的方向合并时放粒子特效用 3D 模型和贴图替换 Sphere增加音效、震动反馈做排行榜和关卡模式尝试真正的 3D 立体堆叠玩法但数值和手感要重新调扩展前先把工程目录、脚本注释、场景节点命名整理好。越到后面项目结构混乱带来的成本越高。这个项目最大的价值不是做出一个“大西瓜”而是让你把刚体、碰撞、预制体、单例、事件回调这些概念串在一起。我做了几次之后最大的感受是看着只是一个小玩法实际上把资源管理、物理参数、碰撞状态机和事件回调全串起来了。真正需要慢慢磨的不是美术而是“合并判定到底会不会重复触发”这种细节。建议你先把单球掉落跑通再逐步加等级和计分不要一开始就追求 11 级水果、爆炸特效和排行榜。

相关新闻

最新新闻

日新闻

周新闻

月新闻