
做棋牌游戏开发这几年我越来越觉得大厅场景才是最见功力的地方。新玩家下载游戏后第一眼看到的就是大厅房间列表、头像信息、游戏入口、活动弹窗全堆在一个界面里既要信息完整又要层级清楚还得保证切场景、刷数据不卡顿。前阵子我用Cocos Creator 3.8完整重做了一遍棋牌游戏大厅场景从场景搭建、UI适配、脚本逻辑到网页端测试和安卓APK打包整个过程踩了不少坑也沉淀了一套能直接照搬的方案。这篇实战笔记就把“5步搞定大厅场景开发”这条链路完整拆开每一步都配有可复用的完整代码和关键配置适合正在做棋牌类项目、或者想系统学习Cocos Creator场景开发的开发者参考。1. 项目概览与整体设计思路1.1 大厅场景在棋牌游戏中的角色定位很多新手拿到项目后第一件事就是冲进场景编辑器开始摆UI这是最容易返工的做法。在动手之前我建议先把大厅场景的职责边界搞清楚。大厅本质上是一个“导航中心 信息聚合页”它负责展示玩家基本信息头像、昵称、资产数量聚合所有游戏入口棋牌、捕鱼、休闲小游戏等提供常用功能入口设置、公告、商城、排行榜同时还要承载活动弹窗和每日签到这类运营内容。想清楚这个定位之后再去看Cocos Creator的项目结构思路就清晰了大厅场景不应该承载具体的游戏业务逻辑它只做三件事——展示数据、响应点击、跳转场景。所有和战斗、牌局、积分相关的逻辑都放到子游戏场景里大厅只保留调用入口。这样做的最大好处是后面迭代不慌运营要加一个新活动按钮你只需要在大厅节点树上加一个Button组件就能接好不需要动到核心代码。1.2 技术选型与版本选择我这次用的是Cocos Creator 3.8 LTS版本选它的原因很直接3.x系列对UI系统的重构比2.x稳定很多多分辨率适配方案更成熟而且3.8是长线支持版本社区反馈的问题少适合项目长期维护。如果你项目里用的是2.4.x后面不少属性和API名对不上需要做少量迁移但整体设计思路完全通用。棋牌类项目在技术选型上有一个容易被忽略的点低端安卓机的适配。这类项目的用户群体里中低端机器占比不低所以在选择组件时我坚持能不用WebView就不用、能不做实时阴影就不做图片资源优先图集打包UI节点数量控制在合理范围内。Creator 3.8的UI合批能力比旧版提升了不少但前提是你得按规范管理节点层级后面我会专门说节点树怎么排。1.3 大厅UI的节点树设计思路在编辑器里动手之前我先在纸上画了一张节点层级草图。大厅场景的根节点是Canvas下面按“背景层—内容层—弹窗层—顶层提示层”的顺序排列。分层规则我总结得很简单最底下放纯背景和装饰中间放大厅主体内容再往上放模态弹窗最顶上放飘字、Toast、加载遮罩这类全局提示。这个分层最大的价值在于层级冲突时不用满世界找问题。比如一个公告弹窗被按钮挡住了检查弹窗层是否在按钮层之上就能定位比如全屏加载遮罩显示不出来看看遮罩是不是被放进了内容层。实际开发里我见过太多项目把弹窗和按钮混在同一层级结果zIndex改来改去越改越乱。规范好层级后zIndex只在弹窗层内部使用全局冲突就很少发生了。2. 第一步项目创建与大厅场景骨架搭建2.1 新建项目与场景初始化打开Cocos Creator 3.8新建一个Empty2D项目模板里自带的2D场景可以直接复用。项目名叫什么不重要但包名建议一开始就定好比如com.yourcompany.gamename后面打APK的时候这个值要跟着签名配置走改晚了容易出错。场景初始化主要做两件事。第一删除模板场景中多余的Camera和Canvas系统会保留一个主Camera和Canvas这两个就够了。第二把Canvas下的子节点按照之前设计好的四层结构建好分别是BGLayer、ContentLayer、PopLayer、TipLayer。每一层都是一个空节点挂上UIOpacity和Widget组件空节点不需要渲染属性保持默认状态即可。Canvas ├── BGLayer背景层纯背景图、动态粒子、装饰光效 ├── ContentLayer内容层顶部栏、游戏入口、底部功能栏 ├── PopLayer弹窗层公告、设置、商城等所有模态弹窗 └── TipLayer提示层Toast、飘字、全屏Loading遮罩2.2 Canvas适配设计与设计分辨率这一步是大厅开发里最容易踩坑的地方。棋牌游戏需要在手机竖屏下流畅运行同时还要兼顾平板和网页端预览。我的适配参数如下设计分辨率设置为720乘1280适配策略选择Fit Width Fit HeightSHOW_ALLCanvas的ResolutionPolicy在代码里也可以通过配置动态修改但场景里直接配好最省心。为什么选720乘1280而不是750乘1334原因很简单向上适配更友好。在这个设计分辨率下UI元素放大到主流安卓机时画面依然精致缩到iPhone SE这类小屏设备时也只是等比缩小不会出现元素裁切。SHOW_ALL策略虽然会在极端宽高比的设备上产生黑边但我用背景图铺满加上代码微调背景缩放来解决效果比CROP裁切更好因为背景图是做死的裁切后反而会丢失重要视觉元素。适配相关的一个常见疑问是要不要代码动态改Canvas我的经验是能不改就不改。如果你在多个场景间切换每个场景的Canvas配置保持一致就不会出现切换后UI偏移的怪问题。极少数需要全屏铺底的界面我会用Widget组件让背景图四边对齐父节点而不是改Canvas的适配策略。2.3 搭好背景层与基础节点背景层BGLayer下只放两个节点BGImage和BGLine。BGImage是一张1920乘2560的背景大图挂Widget组件四周对齐到Canvas边界BGLine是底部的一条装饰带用于遮挡背景和内容层之间的接缝。这里有个实操细节背景图尽量做纯色渐变加几何装饰的风格不要放太多精细的元素因为这些视觉细节在低端机上需要纹理采样的开销对性能不友好而且后期想换背景主题纯色渐变的替换成本最低。我在BGImage上还挂了一个简单的ScaleBg脚本用来解决极端宽高比下的黑边问题。脚本逻辑很简单在onLoad里拿到实际屏幕宽高比如果宽高比小于设计值就把背景宽度按比例放大保证背景始终铺满画面。这个脚本两行核心代码但实测下来能解决百分之八十的“换手机后背景露白边”问题。// ScaleBg.ts import { _decorator, Component, Node, view } from cc; const { ccclass } _decorator; ccclass(ScaleBg) export class ScaleBg extends Component { onLoad() { const winSize view.getVisibleSize(); const designSize view.getDesignResolutionSize(); const ratio winSize.width / winSize.height; const designRatio designSize.width / designSize.height; if (ratio designRatio) { this.node.setScale(ratio / designRatio, ratio / designRatio, 1); } } }3. 第二步大厅UI控件布局与属性配置3.1 顶部信息栏与Widget对齐顶部信息栏在ContentLayer下是一个名为TopBar的空节点挂Widget组件Top对齐Top间距设为60左右间距设为40。TopBar下面包含四个子节点AvatarNode头像、NameLabel昵称、CoinLabel资产数量、HeadImg头像图片。布局时我习惯用固定间距锚点来排而不是依赖Layout组件自动排。棋牌大厅的顶部栏元素位置相对固定用Layout排反而会在不同分辨率下产生间隙。手动布好之后再给头像加一个点击范围点击后弹出个人资料弹窗这个交互几乎每个棋牌大厅都有。顶部栏的注意点之一文字标签的Overflow属性要设置为CLAMP或者SHRINK否则玩家昵称长了会把资产数字挤出屏幕。我给NameLabel加了一个Widget组件右对齐到CoinLabel的左边同时设置了最大宽度文字过长时就收缩字号这样不管昵称多长都不会把布局撑坏。3.2 游戏入口按钮区设计大厅中间的游戏入口区是点击率最高的区域我会用ToggleContainer配合网格布局来实现一排多个入口按钮。原理是每个入口按钮由Toggle组件控制选中态默认选中第一个游戏的按钮玩家点击其他按钮时切换选中样式同时更新下方入口区域的对应交互面板。实际编码时我给入口按钮区域绑定了一个SwitchGamePanel脚本脚本里通过节点树查找拿到所有子节点上的Toggle组件监听它们的节点事件。切换游戏时不需要重新实例化界面只需要更新几个Sprite的图片和透明度这样切起来非常流畅。这里有一个比较重要的性能认知Switch操作用“显隐切换”而不是“创建销毁”。如果每次切游戏都重新创建入口面板就会出现明显卡顿尤其在低端机上反复创建还会不断分配内存增加GC压力。正确的做法是把大厅各入口面板作为独立节点放在SwitchContainer下切换时setActive即可节点数不多内存开销极小。3.3 底部功能栏与弹窗挂点底部功能栏是三个Icon分别是设置、公告、客服挂在一个名为BottomBar的节点上底对齐到Canvas间距从底部40开始往上排。每个Icon下面跟一个文字Label点击热区我做了两层Icon的Button节点负责响应事件回调里再根据业务需求打开对应弹窗。弹窗统一挂在PopLayer下面。我给每个弹窗建了一个空的包装节点弹窗自身的内容全部作为该节点的子节点。这个包装节点默认setActive(false)需要打开时设为true关闭时再设为false。这里的技巧是关闭弹窗时不要用destroy()销毁节点保留节点复用下次打开就能立即显示没有重建的等待时间。弹窗的通用属性配置也值得一说。背景遮罩通常是一个半透明黑色Sprite尺寸覆盖全屏颜色setColor成0,0,0,160。弹窗内容居中放置挂Widget组件居中宽高按设计稿给定。为了在弹窗弹出时有点过渡效果我会在打开弹窗的代码里做一个scale从0.8到1的Tween动画配合FadeIn透明度变化视觉上会舒服很多。Cocos Creator 3.8自带的Tween系统在UI动画上性能不错直接用就行。4. 第三步大厅脚本编写与事件绑定4.1 HallManager脚本整体设计大厅的全局逻辑我收敛到一个HallManager.ts脚本里这个脚本挂在Canvas根节点上。它的主要职责有四个初始化顶部玩家信息监听各入口按钮的点击事件统一负责场景跳转管理弹窗开关为什么不把逻辑到处铺开写棋牌项目迭代快入口数量、功能入口频繁调整如果每个按钮各自处理点击和跳转后面查问题要跑遍每一个脚本。统一收口之后所有入口的点击事件都绑定到HallManager的同名方法上事件流清晰排查快。当然脚本会稍微长一点但换来的是可维护性值。4.2 大厅管理脚本完整代码下面是我整理后的HallManager.ts核心代码这个版本做了简化但可以直接运行。绑定时注意在编辑器的Button组件的ClickEvents列表里Target设置为Canvas节点Component选择HallManagerHandler选择对应方法点击事件的参数不需要传脚本里通过node.name区分具体点击来源。// HallManager.ts import { _decorator, Component, Node, Label, Sprite, director, tween, Vec3, UIOpacity } from cc; const { ccclass, property } _decorator; ccclass(HallManager) export class HallManager extends Component { property(Node) playerNameLabel: Node null; property(Node) coinLabel: Node null; property(Node) settingPop: Node null; property(Node) noticePop: Node null; property(Node) loadingMask: Node null; onLoad() { this.initPlayerInfo(); } initPlayerInfo() { // 实际项目中这里从登录接口获取数据 const nameLabel this.playerNameLabel.getComponent(Label); nameLabel.string 玩家_9527; const coin this.coinLabel.getComponent(Label); coin.string 资产88888; } // 入口按钮统一点击回调 onBtnClick(btnNode: Node, eventData: any null) { const btnName btnNode.name; switch(btnName) { case BtnSetting: this.showPop(this.settingPop); break; case BtnNotice: this.showPop(this.noticePop); break; case BtnQuit: this.quitHall(); break; default: // 子游戏入口走场景跳转 this.enterGame(btnName); break; } } // 弹窗通用打开方法 showPop(popNode: Node) { if (!popNode) return; popNode.active true; const opacity popNode.getComponent(UIOpacity) || popNode.addComponent(UIOpacity); opacity.opacity 0; tween(opacity).to(0.15, { opacity: 255 }).start(); // 内容节点做一个轻微缩放动画 const content popNode.getChildByName(Content); if (content) { content.setScale(0.8, 0.8, 1); tween(content).to(0.18, { scale: new Vec3(1, 1, 1) }, { easing: backOut }).start(); } } // 弹窗关闭方法 hidePop(popNode: Node) { if (!popNode) return; const opacity popNode.getComponent(UIOpacity); if (opacity) { tween(opacity).to(0.1, { opacity: 0 }).call(() { popNode.active false; opacity.opacity 255; }).start(); } else { popNode.active false; } } // 子游戏场景跳转 enterGame(gameName: string) { if (!this.loadingMask) { director.loadScene(gameName); return; } this.loadingMask.active true; director.preloadScene(gameName, () { director.loadScene(gameName); }); } quitHall() { // 弹确认框后退出项目里一般走原生层接口 console.log(退出大厅); } }这段代码里的关键点是进入子游戏前先做一个preloadScene预加载配合一个全屏Loading遮罩避免场景切换时出现短暂的资源加载白屏。遮罩节点挂一个Label显示“加载中”简单实用。4.3 场景跳转的生命周期与数据传递场景跳转是大厅开发的核心动作。Cocos Creator 3.x里director.loadScene()是异步的场景加载完成后旧场景会被替换。这里有一个重要原则不要在大厅脚本的onDestroy里做资源释放因为场景切换到子游戏时大厅的onDestroy会被调用如果你在里面清理了公共资源子游戏里用到这些资源时会重新走加载流程反而拖慢首屏。我当时踩过一个很隐蔽的坑在大厅onDestroy里释放了一个音频资源结果子游戏场景加载的时候同时播放背景音乐导致音频播放器状态冲突出现一声刺耳的爆音。后来改成不主动释放公共资源只释放大厅自己独有的临时节点问题就消失了。数据传递也容易出错。玩家在大厅选择的游戏模式、上次停留的房间号这些数据在切场景时不能简单存到模块变量里因为Cocos Creator场景切换时脚本实例会被销毁重建模块级变量也会被重置。我习惯用一个全局单例DataCenter.ts来持有跨场景共享数据它不会挂在任何场景节点上常驻内存数据在场景切换后依然保留。下面是一个简单示例// DataCenter.ts export class DataCenter { private static _instance: DataCenter null; static get instance(): DataCenter { if (!this._instance) { this._instance new DataCenter(); } return this._instance; } playerInfo: any {}; currentGameId: string ; lastRoomId: string ; soundEnabled: boolean true; }5. 第四步场景切换与资源加载的优化实践5.1 大厅场景的静态资源管理棋牌大厅的图片量不小背景、按钮、弹窗、图标、动画序列帧加起来可能上百个资源。我建议把所有大厅资源放到一个assets/Hall/目录下并设置为图集打包。Cocos Creator的图集打包工具在3.x里叫Auto Atlas选中目录右键即可创建图集配置。打成图集后多个Sprite共用一张大纹理渲染时只需要一次DrawCall对低端机来说提升非常明显。还有一种情况是图片特别多、但单张又很小比如各种小数量的图标。这类资源我一般手动用TexturePacker打图集不依赖编辑器自动图集因为自动图集对筛选规则的把控不够细容易把两张不太相干的图合到一个图集里导致图集体积变大。5.2 资源预加载与加载进度条大厅每次打开都要请求服务器拿玩家数据这个等待时间正好用来预加载子游戏场景。我上面的enterGame方法里已经写了preloadScene这里再补充一个首次进入大厅时的全局预加载思路在大厅展示出来之后用scheduler或者直接调用一次preloadScene把最常见的2个游戏场景提前加载好玩家点击入口时就能瞬时切换几乎不必等待。加载进度条一般用在一个游戏场景内部资源较多的情况。比如从大厅进入捕鱼场景场景里有很多帧动画贴图加载耗时可能超过一秒。我会在场景的启动脚本里监听场景的加载进度把0到1的进度映射到进度条Sprite的fillRange上。下面是一个常用的进度条更新片段// LoadingProgress.ts 片段 progressBar.getComponent(Sprite).fillRange progress; label.string Math.floor(progress * 100) %;进度条的颜色我用渐变绿到橙单纯为了视觉反馈更明显。做的时候注意fillType要设置为RANGE类型否则fillRange属性不生效这个坑我见过有人调试了半天才发现。5.3 音频管理与动效细节大厅里的音效主要是按钮点击声、弹窗弹出声、背景音乐。我习惯用一个AudioManager单例管理所有音频而不是在每个按钮上直接挂AudioSource。AudioManager会在启动场景中初始化节点并常驻提供playSfx、playBgm、stopBgm等接口。这样做的原因是如果每个按钮节点都挂AudioSource切场景或销毁节点时声音会突然断掉体验很差用常驻节点来播放声音的生命周期可以跨场景控制。动效方面大厅的入口按钮我加了TouchScale效果按下时缩放0.9松开弹回1。这个效果我用一个通用的ButtonScale组件挂在所有需要反馈的按钮上比放多个Tween动画更轻量。// ButtonScale.ts import { _decorator, Component, Node, tween, Vec3 } from cc; const { ccclass, property } _decorator; ccclass(ButtonScale) export class ButtonScale extends Component { onLoad() { this.node.on(Node.EventType.TOUCH_START, this.onTouchStart, this); this.node.on(Node.EventType.TOUCH_END, this.onTouchEnd, this); this.node.on(Node.EventType.TOUCH_CANCEL, this.onTouchEnd, this); } onTouchStart() { tween(this.node).to(0.05, { scale: new Vec3(0.9, 0.9, 1) }).start(); } onTouchEnd() { tween(this.node).to(0.08, { scale: new Vec3(1, 1, 1) }, { easing: backOut }).start(); } }6. 第五步网页版手机适配与APK打包6.1 网页版手机适配的实操配置棋牌游戏很常见的发布形式是网页版玩家扫码即玩不需要下载安装。Cocos Creator对Web Mobile的支持很成熟但在上传服务器之前有几个关键配置要确认。第一构建发布面板选择Web Mobile平台勾选“Mobile Browser”预览模式。第二资源服务器地址必须配置为相对路径否则部署到二级目录时资源会找不到。第三如果游戏需要本地存储玩家数据建议用localStorage的封装Cocos Creator提供的sys.localStorage可以跨页面保存数据比如记住玩家上次登录时输入过的昵称。网页版的手机上适配核心还是设计分辨率要设置好。我调试时经常遇到一个问题手机浏览器地址栏会遮挡画面底部。这个问题常见于浏览器全屏和游戏全屏并不一致的情况。我建议在Web预览模板里加入一段全屏适配代码强制页面占满整个视口同时在场景里对底部元素加上安全区偏移。这里有一段我常用的HTML模板片段style html, body { margin: 0; padding: 0; height: 100%; overflow: hidden; background: #000; } #GameDiv { width: 100%; height: 100%; position: fixed; inset: 0; } /style在代码里读取安全区时可以通过view.getSafeArea()获取。如果只在网页端运行还可以监听浏览器窗口的resize事件在宽高比变化时重新设置Canvas的适配策略避免旋转屏幕或者折叠屏导致UI错位。6.2 打包APK的完整流程与常用配置如果游戏要上安卓应用市场需要打APK。Cocos Creator 3.8打包APK的流程已经比较顺畅但有几个配置项我总结成清单构建平台选择Android包名必须唯一建议使用com.yourcompany.yourgame屏幕方向选择“竖屏”或者“自动跟随设备”最低SDK版本设置到23以上使用自己的签名文件不可以用调试签名上架打包时我习惯先build一次然后不急着直接出APK而是先用Android Studio打开生成的工程手动跑一次release构建。这样能看到详细的Gradle输出出错了容易定位。Cocos Creator打包失败很多情况是Gradle下载依赖超时在国内网络环境下需要配置镜像仓库这个在项目级别的build.gradle里改一下就行。打包成功后我会用adb install命令装到真机上测试重点看大厅切场景的流畅度、背景音乐是否正常、触摸按钮有没有延迟。特别是触摸反馈延迟这是网页版和APK版最容易表现不一致的地方需要重点检查。6.3 真机调试与性能检查清单APK装到手机上之后我有一套固定的检查流程连续十次快速点击入口按钮观察场景切换是否出现卡顿或白屏在低端机上运行打开大厅的帧率显示确认稳定在30fps以上反复开关多个弹窗观察内存占用是否有明显上涨有没有内存泄漏检查大厅切子游戏、子游戏返回大厅的完整闭环关于内存泄漏最常发生的场景是事件监听没有移除。比如大厅里给某个按钮绑定了自定义事件但在场景销毁时没有off掉。3.8版本里如果事件监听挂在节点上节点销毁时监听一般会自动移除但如果是挂在全局单例或者全局事件系统上的一定要手动off。我写过一个工具脚本在场景切换时统一dispose所有全局事件监听算是兜底方案。7. 常见问题与排查技巧实录7.1 大厅开发高频问题速查表下面这张表整理的几个问题都是我在实际开发里反复遇到、并且在社区里也经常看到的新手求助问题。对照排查基本能解决大部分日常困扰。现象可能原因解决方案UI在部分手机上偏移Canvas适配策略不一致或Widget配置缺失统一检查场景的Canvas适配参数确认所有全屏节点都挂Widget对齐按钮点击无反应节点上某个子节点遮挡了触控区域检查节点层级把遮挡的装饰节点设置setBlockInputEvents或者移出点击区域切场景后声音消失音频挂在场景节点上被销毁改用全局常驻AudioManager来播放音频加载场景时白屏资源未预加载或场景过大使用preloadScene预热场景或加加载进度条图片出现拉伸变形没有使用九宫格或Sprite尺寸设置不当优先用Sliced类型处理可拉伸UI固定尺寸图标用Raw或Simple打包APK失败Gradle依赖下载失败配置镜像仓库或离线手动导入依赖返回大厅数据丢失场景销毁导致脚本变量重置用全局单例DataCenter保存玩家数据和页面状态7.2 三个值得养成的开发习惯这段偏经验分享但真的能帮你省下很多排查时间。第一个习惯每个场景单独建一个DebugConfig脚本记录当前场景的设备分辨率、FPS、节点数量。这样在日常测试里一旦发现卡顿或者适配异常打开日志就能直接看到关键数据而不用事后靠猜。我在大厅的DebugConfig里会额外输出当前DrawCall数量超过20就提示自己注意合批优化。第二个习惯资源命名规范从第一天就定好。大厅所有UI资源用“ui_大厅模块_用途”的命名方式比如ui_hall_topbar_bg.png图集名和资源名严格对应。项目跑半年之后资源文件几百上千命名混乱会让你找资源找到怀疑人生。第三个习惯每个功能模块独立脚本不写上帝类。HallManager只做大厅导航逻辑弹窗逻辑单独写PopManager顶部栏单独写TopBarController。一开始会觉得文件多但后期需求迭代时你会发现改一个模块完全不影响另一个模块这段时间是值得花的。写在最后的个人体会这套大厅方案从最早的原型到现在的3.8版本前后迭代了快两年。回过头看最值钱的部分不是某个华丽特效而是那套“该常驻的常驻、该释放的释放、该统一的不散开”的架构原则。棋牌类项目最大的特点是版本迭代快、活动需求多架构上留足余地后面才不会被运营需求追着打。如果你正在做第一个棋牌项目一定要先把Canvas适配、全局单例、弹窗管理这三件事做扎实这三个基础打好了后面加功能会顺手很多。