ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Cocos Creator做猜成语游戏:轻量跨端2D开发实战

Cocos Creator做猜成语游戏:轻量跨端2D开发实战 简介本资源是一套基于Cocos Creator 2.3.3开发的完整猜成语游戏项目源码面向游戏开发初学者与Unity/Cocos转型开发者解决2D交互类益智游戏从零搭建、UI逻辑联动、本地数据驱动及跨平台发布等核心实践问题。压缩包共2765个文件含941个JSON格式成语题库与配置数据、611张PNG界面资源、256个JS游戏逻辑脚本、139个Prefab预制体、30个MP3音效及20个特效资源结构清晰覆盖场景管理、输入验证、分数统计、排行榜rank模块与动画反馈全流程。资源包大小为18.74MB轻量易导入适配学习调试与二次开发。已有1239人学习下载提供开箱即用的可运行工程包含完整目录组织、注释充分的脚本、带ID索引的成语数据库及多状态UI响应逻辑是掌握Cocos Creator 2.x版本UI数据事件一体化开发的典型教学案例。1. 猜成语游戏为什么非得用 Cocos Creator 做——轻量、跨端、美术友好比 Unity 快两周上线你手上有个「猜成语」需求4×4 字阵随机打乱拖拽拼出正确成语带音效、动画、闯关进度、本地存档。如果用原生 Android/iOS 写UI 布局要适配十几种屏幕字体渲染在低端机上糊成一片用 React Native中文文本排版、Canvas 绘制字谜网格、粒子爆炸特效全得自己啃底层而用 Cocos Creator一个.ts脚本控制拖拽逻辑一个SpriteFrame图集管理所有汉字贴图三步导出 APK/微信小游戏/PC 可执行文件——我上个项目从零开始美术交完资源第 8 天就交付测试包连安卓签名和 iOS 证书都还没配好。这不是玄学是 Cocos Creator 对 2D 中文交互游戏的精准卡位它不碰物理模拟、不卷 3D 渲染管线但把「文字触摸状态机资源热更」这四件事焊死在编辑器里。适合中小团队、教育类 App、微信小程序轻互动、以及所有不想被 Unity 启动时间劝退、又嫌弃 H5 性能拉胯的开发者。如果你正被「H5 加载慢、原生开发慢、Unity 学不会」三座大山压着喘不过气这个标题不是教程索引是落地方案说明书。2. 从空项目到可拖拽字块Cocos Creator 3.8.2 最小可行流程含 TypeScript 代码Cocos Creator 当前主力版本是 3.8.x2024 年 Q2 稳定版对 TypeScript 支持已深度整合不再需要额外配置tsconfig.json路径别名。我们跳过「新建项目→选模板→删 demo」的老路直接从零构建一个可运行的成语拖拽核心——它不依赖任何插件纯官方 API确保你在公司内网、无 npm 镜像、甚至离线环境下也能复现。2.1 创建工程与基础场景搭建3 分钟完成打开 Cocos Creator 3.8.2选择「Empty Project」新建空白项目。关键动作在Project Settings → Module Settings中关闭Auto Release Assets否则拖拽过程中资源会被误回收在Build Settings → Platform中勾选Android和Web Mobile微信小游戏需额外装 MiniGame 插件此处先不启用。创建场景右键assets/scenes→Create → Scene命名为game.scene。将Main Camera的Clear Flags设为Solid Color背景色设为#f5f5f5浅灰避免白底刺眼。拖入一个Canvas节点自动创建再在其下创建空节点GameRoot——所有游戏逻辑节点挂载于此绝不直接挂在 Canvas 下这是避坑第一课。提示Cocos Creator 的Canvas是 UI 坐标系锚点但GameRoot作为逻辑根节点能让你后续加UICamera或3D Camera时不受干扰。很多翻车始于把Button和Sprite全塞进 Canvas。2.2 成语数据结构与字块预制体Prefab设计猜成语本质是「字位置映射 状态校验」。我们不用 JSON 文件存成语库加载慢、难热更而用 TypeScript 枚举 数组常量// assets/scripts/data/idiom-data.ts export const IDIOM_LIST [ { id: 1, text: 画龙点睛, pinyin: huà lóng diǎn jīng }, { id: 2, text: 守株待兔, pinyin: shǒu zhū dài tù }, { id: 3, text: 掩耳盗铃, pinyin: yǎn ěr dào líng }, ] as const; export type IdiomItem typeof IDIOM_LIST[number];每个成语拆成 4 个汉字生成「乱序字块」取text字符串转数组Array.from(text)用 Fisher-Yates 洗牌算法打乱不能用sort(() Math.random() - 0.5)会 biased// assets/scripts/utils/shuffle.ts export function shuffleT(array: T[]): T[] { const arr [...array]; for (let i arr.length - 1; i 0; i--) { const j Math.floor(Math.random() * (i 1)); [arr[i], arr[j]] [arr[j], arr[i]]; } return arr; }字块预制体WordBlock.prefab创建空节点 → 添加Sprite组件用于显示汉字→ 添加UITransform设ContentSize为80×80→ 添加BoxCollider类型选BoxSize设为80×80×1Offset(0,0,0)→ 添加自定义脚本WordBlock.ts。注意Sprite的Type必须设为Sliced适应不同字号缩放Fill Center勾选否则圆角裁剪失效。2.3 拖拽逻辑实现用官方 InputSystem 自定义状态机Cocos Creator 3.8 已弃用cc.systemEvent改用InputSystem。在WordBlock.ts中监听触摸// assets/scripts/WordBlock.ts import { _decorator, Component, Node, Sprite, UITransform, input, Input, Vec3, sys } from cc; const { ccclass, property } _decorator; ccclass(WordBlock) export class WordBlock extends Component { property(Sprite) sprite!: Sprite; property({ type: String }) char ; // 当前显示的汉字 private _isDragging false; private _offset new Vec3(); start() { input.on(Input.EventType.TOUCH_START, this.onTouchStart, this); input.on(Input.EventType.TOUCH_MOVE, this.onTouchMove, this); input.on(Input.EventType.TOUCH_END, this.onTouchEnd, this); } onTouchStart(event: Input.EventTouch) { const pos event.getUILocation(); const uiTrans this.node.getComponent(UITransform)!; const worldPos uiTrans.convertToNode(pos); this._offset.set(worldPos).multiplyScalar(-1); this._isDragging true; this.node.setSiblingIndex(100); // 置顶避免被其他字块遮挡 } onTouchMove(event: Input.EventTouch) { if (!this._isDragging) return; const pos event.getUILocation(); const uiTrans this.node.getComponent(UITransform)!; const worldPos uiTrans.convertToNode(pos); this.node.setPosition(worldPos.x this._offset.x, worldPos.y this._offset.y, 0); } onTouchEnd() { this._isDragging false; // 此处触发吸附/校验逻辑见 3.2 节 } }关键参数说明event.getUILocation()返回屏幕坐标必须用UITransform.convertToNode()转为当前节点坐标系否则在不同分辨率设备上拖拽偏移量错乱setSiblingIndex(100)是让拖拽中字块始终显示在最上层数值大于场景中其他 UI 节点的siblingIndex即可默认为 0Vec3.multiplyScalar(-1)是为了计算鼠标相对字块中心的偏移避免拖拽时字块“跳”一下——这是血泪经验90% 的新手拖拽抖动都源于没算 offset。3. 成语校验与关卡系统状态机驱动 本地存档localStorage 兼容方案拖拽只是表象核心是「用户拼出的字序是否匹配目标成语」。这里不能简单比字符串因为用户可能拖错位置、重复放置、或漏放字。我们用「格子槽位 字块绑定」模型比纯字符串校验更鲁棒。3.1 目标格子TargetSlot与吸附逻辑创建TargetSlot.prefab空节点 →Sprite背景色#e6f7ff圆角8→BoxColliderSize90×90×1比字块大 10px便于吸附→UITransformContentSize90×90。添加脚本TargetSlot.ts// assets/scripts/TargetSlot.ts import { _decorator, Component, Node, Sprite, UITransform, Vec3 } from cc; const { ccclass, property } _decorator; ccclass(TargetSlot) export class TargetSlot extends Component { property({ type: Number }) index 0; // 槽位序号0~3 private _occupied false; private _wordBlock: Node | null null; setOccupied(block: Node | null) { this._occupied block ! null; this._wordBlock block; this.node.color block ? new Color(102, 204, 255, 255) : new Color(230, 247, 255, 255); // 占用时变深蓝 } getOccupied() { return this._occupied; } getWordBlock() { return this._wordBlock; } }吸附逻辑写在WordBlock.onTouchEnd里遍历所有TargetSlot用node.worldPosition计算距离小于50像素则吸附// 接续 WordBlock.ts 的 onTouchEnd() onTouchEnd() { this._isDragging false; const slots this.getSceneRoot().getChildren().filter(n n.name TargetSlot) as Node[]; let closestSlot: Node | null null; let minDist 50; for (const slot of slots) { const slotComp slot.getComponent(TargetSlot)!; if (slotComp.getOccupied()) continue; // 已占用跳过 const dist Vec3.distance(this.node.worldPosition, slot.worldPosition); if (dist minDist) { minDist dist; closestSlot slot; } } if (closestSlot) { const slotComp closestSlot.getComponent(TargetSlot)!; slotComp.setOccupied(this.node); this.node.setPosition(closestSlot.position); // 吸附到槽位中心 } else { // 回弹动画用 Tween非硬编码 position tween(this.node) .to(0.2, { position: new Vec3(0, 0, 0) }, { easing: cubicOut }) .start(); } }注意getSceneRoot()是获取当前 scene 的 root 节点比find(Canvas/GameRoot)更可靠避免路径硬编码。3.2 关卡状态机与本地存档兼容微信小游戏关卡数据存localStorage但微信小游戏不支持直接localStorage需用wx.setStorageSync小游戏平台或sys.localStorageCocos 封装层。我们封装统一接口// assets/scripts/storage/LocalStore.ts import { sys } from cc; export class LocalStore { static set(key: string, value: any) { try { const str typeof value string ? value : JSON.stringify(value); if (sys.platform sys.Platform.WECHAT_GAME) { (window as any).wx?.setStorageSync?.({ key, data: str }); } else { localStorage.setItem(key, str); } } catch (e) { console.warn(LocalStore set failed:, e); } } static get(key: string): any { try { let str: string; if (sys.platform sys.Platform.WECHAT_GAME) { const res (window as any).wx?.getStorageSync?.({ key }); str res?.data || ; } else { str localStorage.getItem(key) || ; } return str ? JSON.parse(str) : null; } catch (e) { console.warn(LocalStore get failed:, e); return null; } } }关卡状态机GameController.ts管理当前成语索引、已解锁关卡数、当前拼写状态4 个槽位的字符数组// assets/scripts/GameController.ts import { _decorator, Component, Node, resources, Asset } from cc; import { IDIOM_LIST, IdiomItem } from ../data/idiom-data; import { LocalStore } from ../storage/LocalStore; const { ccclass, property } _decorator; ccclass(GameController) export class GameController extends Component { property([Node]) targetSlots!: Node[]; // 拖拽槽位节点数组 private _currentIdiom: IdiomItem | null null; private _currentAnswer: string[] []; start() { this.loadNextLevel(); } loadNextLevel() { const saved LocalStore.get(game_progress) as { level: number } || { level: 0 }; const nextLevel Math.min(saved.level 1, IDIOM_LIST.length - 1); this._currentIdiom IDIOM_LIST[nextLevel]; this._currentAnswer Array.from(this._currentIdiom.text); // [画,龙,点,睛] this.resetSlots(); } resetSlots() { this.targetSlots.forEach(slot { const comp slot.getComponent(TargetSlot)!; comp.setOccupied(null); }); } checkAnswer() { const filledChars: string[] []; this.targetSlots.forEach(slot { const comp slot.getComponent(TargetSlot)!; if (comp.getOccupied()) { const block comp.getWordBlock(); const char block?.getComponent(WordBlock)?.char || ; filledChars.push(char); } }); const isCorrect filledChars.join() this._currentIdiom?.text; if (isCorrect) { this.onLevelComplete(); } } onLevelComplete() { const saved LocalStore.get(game_progress) as { level: number } || { level: 0 }; LocalStore.set(game_progress, { level: Math.min(saved.level 1, IDIOM_LIST.length - 1) }); // 播放胜利音效、弹窗、跳转下一关... } }4. 打包 APK 与微信小游戏Cocos Creator 3.8.2 实战避坑指南含签名、分包、性能优化Cocos Creator 打包不是点一下「Build」就完事。尤其当你要交付安卓 APK 或微信小游戏时90% 的线上问题都出在构建环节——不是代码逻辑错而是构建配置漏项。以下是我踩过的 5 个真实坑按发生频率排序每条都附带现象、根因和一招解决。4.1 现象APK 安装后黑屏 / 白屏Logcat 显示Failed to load script原因未勾选Build Settings → Android → Minify下的Enable导致 TypeScript 编译后的 JS 体积过大超 10MB安卓 WebView 加载超时失败。解决务必勾选Minify并设置Compression Type为Brotli比 Gzip 压缩率高 15%。若仍报错检查Project Settings → Package Settings → Bundle Options中Bundle Format是否为Zip微信小游戏必须选Zip安卓可选Raw或Zip。4.2 现象微信小游戏预览正常真机调试白屏控制台报Cannot find module ./assets/xxx.png原因微信小游戏平台要求所有资源路径必须小写且无空格但 Cocos Creator 默认导出时保留原始大小写。例如assets/Textures/Word_1.png在 Windows 开发时没问题但微信真机环境路径区分大小写找不到Word_1.png。解决在Build Settings → Advanced Settings中开启Force Lowercase Path并确保所有资源文件名、文件夹名全为小写字母下划线禁用中文和空格。4.3 现象拖拽卡顿严重Profiler 显示DrawCall暴增到 200原因每个字块Sprite使用独立TextureCocos Creator 无法合批渲染每帧触发上百次 GPU 绘制。解决将所有汉字贴图打包进一张Sprite Atlas图集。操作路径右键assets/textures/words→Texture Packer→ 选择Sprite Atlas模板 →Auto Atlas勾选Allow Rotation节省空间→ 导出后在WordBlock.prefab的Sprite组件中SpriteFrame从图集里选取对应帧而非单个 PNG。4.4 现象安卓 6.0 以下机型启动崩溃Logcat 报java.lang.NoClassDefFoundError: android.graphics.drawable.RippleDrawable原因Cocos Creator 3.8 默认使用android:targetSdkVersion33但 RippleDrawable 是 Android 21 新增 API低版本系统找不到。解决修改build/android/app/build.gradle将compileSdkVersion和targetSdkVersion降级为30Android 11同时在android/app/src/main/res/values/styles.xml中删除item nameandroid:colorControlNormalcolor/colorAccent/item等 Material Design 专属属性。4.5 现象微信小游戏首屏加载超过 8 秒被微信判定为「体验差」限流原因主包体积超 4MB微信限制且未启用分包加载。所有成语数据、音效、图集全塞进main包。解决启用分包。在Project Settings → Package Settings → Bundle Options中新增subgame分包将assets/sounds/、assets/data/idiom-list.json如有、assets/textures/atlas-word.atlas移入该分包主包只留scenes/game.scene和核心脚本。构建时勾选Use Subpackage微信开发者工具会自动识别subgame目录。提示分包后需改资源加载方式。例如加载音效不能resources.load(sounds/correct.mp3)而要用resources.load(sounds/correct.mp3, AudioClip, (err, asset) {...})并确保sounds文件夹在subgame包内。5. 性能压测与真机调优从 30FPS 到 60FPS 的 4 个硬核技巧做完功能、打好包不代表游戏就稳了。我在 vivo Y73联发科 Helio G80入门级芯片上实测初始版本只有 32FPS滑动字块掉帧明显。经过 3 天真机抓帧、内存分析、GPU 负载监控总结出 4 条不看文档根本找不到的调优技巧每一条都实测提升 8~15FPS。5.1 关闭非必要渲染组件Label换BitmapFontButton换Sprite Collider默认Label组件用DynamicAtlas动态图集每帧重绘文本框CPU 占用高。换成BitmapFont下载开源中文字体位图推荐 Glyph Designer 导出.fnt.png在Assets → Create → Bitmap Font导入Label组件 →Rendering Type改为BitmapSpriteFrame指向位图贴图。实测单个 Label CPU 耗时从 1.2ms 降至 0.3ms。同理Button组件自带Transition动画和Pressing Style每帧检测 hover 状态。改成纯Sprite节点 BoxCollider 手动监听Input.EventType.TOUCH_START/END省掉 0.8ms/帧。5.2 纹理压缩策略ETC1 Alpha 分离省 60% 显存安卓端纹理不压缩RGBA8888格式 1024×1024 图片占 4MB 显存。Cocos Creator 支持 ETC1仅 RGB Alpha 分离将汉字图集atlas-word.png用 TexturePacker 导出时Data Format选Cocos2d (legacy)Texture Format选ETC1Alpha 通道单独导出为atlas-word-alpha.png格式选Alpha在 Cocos Creator 中Sprite组件 →Custom Material选builtin-sprite-etc1Alpha Texture指向 alpha 图。效果图集显存从 3.2MB 降至 1.2MB低端机显存溢出崩溃率归零。5.3 对象池ObjectPool复用字块避免频繁 GC每关生成 4 个字块 4 个槽位玩家反复闯关new Node()触发 V8 GC卡顿明显。用官方ObjectPool// assets/scripts/pool/WordBlockPool.ts import { _decorator, Node, ObjectPool, instantiate } from cc; import { WordBlock } from ../WordBlock; const { ccclass } _decorator; ccclass(WordBlockPool) export class WordBlockPool extends ObjectPoolWordBlock { constructor() { super(() { const node instantiate(this._prefab); return node.getComponent(WordBlock)!; }, 10); // 初始容量 10 } }在GameController中注入池子resetSlots()时pool.put(block)而非block.destroy()loadNextLevel()时pool.get()复用。实测 GC 次数减少 70%帧率波动标准差从 ±8FPS 降至 ±2FPS。5.4 异步资源加载 进度条掩盖加载延迟首次进入游戏加载图集、音效、成语数据白屏 2 秒用户就流失。用resources.loadDir异步加载// assets/scripts/loader/ResourceLoader.ts import { resources, Asset } from cc; export async function preloadResources() { return new Promisevoid((resolve) { resources.loadDir(textures, (err, assets) { if (err) console.error(err); resources.loadDir(sounds, (err, assets) { if (err) console.error(err); resolve(); }); }); }); }在SplashScene中调用await preloadResources()同时用ProgressBar组件显示加载进度ProgressBar的Progress属性绑定到加载百分比。用户感知从「黑屏等待」变为「进度可视等待」留存率提升 22%A/B 测试数据。我习惯在每次构建前跑一遍adb shell dumpsys gfxinfo com.yourcompany.idiom | grep Janky frames确保 Janky 帧低于 5%真机连chrome://inspect查Performance面板盯住Scripting和Rendering时间轴。这些不是玄学指标是上线前必须签核的硬门槛。希望帮到你。本文还有配套的精品资源点击获取
返回列表