《princes》:用 Cocos Creator 写一款餐厅经营放置小游戏
- 约 14 分钟读完 - 全文 5457 字《princes》:用 Cocos Creator 写一款餐厅经营放置小游戏
继《去赶海吧》之后,把更早做的一款餐厅经营小游戏也整理开源了,照例写篇文章介绍玩法和实现上的技术点。仓库名叫 princes(历史遗留命名,内容其实是餐厅),代码里还住着不少从其他项目移民过来的残留,这次一并"考古"出来。
- 开源地址:https://gitee.com/soulnew/princes
- 技术栈:Cocos Creator 2.3.4 + JavaScript
- 平台:微信小游戏(竖屏 720×1280)
- 规模:39 个脚本,约 4200 行代码,单主场景
游戏核心玩法一句话概括:开一家餐厅,顾客自动上门排队等位、走到餐桌点餐、用餐完留下金币,你点击收金币,再用金币解锁更多食物、餐桌和顾客皮肤——放置 + 点击收菜的经典经营循环。
一、玩法介绍
1.1 核心循环
玩家几乎不直接操作角色,核心是"看店 + 点击收钱 + 花钱解锁":
graph LR
A[顾客自动上门<br/>每15秒补1个] --> B{有空餐桌?}
B -->|有| C[A*寻路走到餐桌]
B -->|没有| D[门口排队等位]
D -->|有空桌释放| C
C --> E[头顶弹气泡点单]
E --> F[用餐5秒<br/>点击气泡可加速]
F --> G[桌边产出一堆金币]
G --> H[玩家点击拾取]
H --> I[解锁新食物/餐桌/顾客]
I --> A
几个关键设计:
- 餐桌是稀缺资源:场上最多 10 个顾客(
maxPlayer = 10),没桌子的顾客会在门口按队列序号排开站位,有空桌释放时队首补位、全队前移; - 点单匹配:每个顾客皮肤有自己的"可点菜单"(
foodList),随机点一道——这道菜已解锁就正常产金币,没解锁就显示"没有这道菜"的图标,等于告诉你"该去商店解锁了"; - 巡游消费:顾客吃完饭不直接走,而是从酒柜、古董花瓶、海滩桌等 6 类装饰家具里洗牌出一份"参观清单",逐件走过去参观,每件停留几秒再留下一笔金币——买装饰家具不只是好看,是真的能多赚钱;
- 邀请按钮:等不及可以点"邀请"立刻来一位顾客,这个入口同时服务每日任务和成就。
1.2 四屏地图与翻页
餐厅不是一张单屏地图,而是四个房间:海滩(left1)、大厅(mid)、剧院(right1)、花园(down1)。顾客在房间之间穿行时走"传送门"格子——比如大厅到海滩,先寻路到 (0,11) 这个门,切换父节点到海滩场景节点,再从海滩的入口格 (12,11) 继续寻路。
相机不是跟随式的,而是手动拖拽翻页:单轴锁定滑动,滑过屏宽 1/10 左右翻页、屏高 1/14 上下翻页。门口还有两个服务员(waiterPlayer),有顾客进门就弹出欢迎气泡 5 秒——不做实际传菜逻辑,纯氛围组。
1.3 成长线:解锁三件套
金币花在三个商店面板上:
| 商店 | 内容 | 解锁的意义 |
|---|---|---|
| 食物 | 54 道菜,价格 100~153 递增 | 顾客点单能匹配上,用餐才产金币 |
| 家具 | 大厅 6 组餐桌(每组 3 级升级)+ 各房间散桌 + 装饰家具 | 新餐桌 = 多一个接待座位;装饰家具 = 巡游产金币点 |
| 顾客 | 50 种皮肤 | 新皮肤进入上门顾客的随机池 |
顾客皮肤的名字很有梗:学士、水电工、警长、老板、小青龙、法师、熊猫、海贼、管道工、侦探、伯爵、外星人、厨神、牙医、流浪歌手、开膛手、屠夫、王少爷、腹肌男、特工K、机器人……基本是把能想到的梗都塞进去了。
1.4 成就与每日任务
- 15 条成就分 5 种类型:解锁指定物品、解锁 N 种某类物品、服务 N 个顾客、累计金币(500/5000/10000/30000)、邀请 N 次,奖励统一 5000 金币;
- 每日任务从 7 条里随机洗牌取 6 条(服务顾客 2 次、累计 800 金币、邀请 2 次、招待指定顾客 2 次等),按本地日期键
年_月_日翻日重置,还会随机指定"今天要招待某位顾客"。
两个系统都有红点提醒,领取后自动检查链上下一档成就。
二、技术亮点
项目不大(4000 行出头),但有几个点是自己撸出来的轮子,值得展开。
2.1 手写 A* + “传送门"式跨屏寻路
寻路没有用任何库,AStar.js(217 行)是手写的标准实现:
- 地图是
position.json里一个 21×13 的一维数组(273 个元素,0可走,其他障碍),screenManager按屏幕尺寸算出格子边长,做格坐标到像素坐标的换算; AStarStep封装g/h/position/last,f是g+h的 getter;启发函数用曼哈顿距离,八方向扩展,斜走 14、直走 10(经典的 √2×10 近似);- 拿到格子路径后,把每一格转成
cc.moveTo(1, 像素坐标)拼成cc.sequence播放——每格固定走 1 秒,移动节奏完全由动画序列驱动,不需要update里手动插值。
跨屏是这套寻路最有意思的部分——四个房间不是一张大地图,而是各自独立的节点树,靠 6 对"门坐标"缝合:
graph LR
A[当前房间] -->|寻路到门格 mid_left1| B[门 0,11]
B -->|切换父节点| C[目标房间]
C -->|从入口格 left1_mid 继续寻路| D[最终目标]
大地图拆成小地图 + 门对,每个房间的寻路空间始终只有 21×13,复杂度和内存都可控,是小工程里很实用的拆法。
2.2 自研虚拟列表 ListView
Cocos 2.x 官方没有现成的虚拟列表,商店、成就、每日任务的长列表就手写了一个(ListView.js,380 行,项目内最大文件):
- NodePool 复用:划出视野的 item 回池,进入视野的从池里取或 instantiate;
- 增量填充:
_filledIds记录当前树上已填充的索引,update()每帧用 content 的滚动偏移算起始 item 索引,变了才触发updateView,只回收越界的、只补缺失的; - Adapter 模式:
ListAdapter持有数据和 item 脚本名,updateView里item.getComponent(componentName).setData(data)——Android ListView 的味道很浓,View 和数据彻底分离; - 支持多列网格(
oneLineItemNumber)、上拉/下拉回调、监听refreshScollView事件做局部刷新。
家具面板没用这个 ListView,而是手写了一个更简的裁剪:视野内 opacity=255、视野外 opacity=0。同一项目里两种方案并存,算是"够用就好"和"造个轮子"的对照组。
2.3 餐桌分配:一个微缩排队系统
realTimeData.js 里藏着一个小小的服务台排队模型:
activeFurnitureData是"空闲桌池”,顾客来了随机抽一张(splice取出),用完push回去;- 没桌时顾客进
waitTableQuenue,按index/maxPlayer比例在门口插值站位——队伍有多长,人就排多密; - 空桌释放时遍历等待队列,队首补位、全队回调前移。
没有定时器轮询,全靠事件驱动,几十行代码把"等位"这个经营游戏的核心张力做出来了。
2.4 运行时程序化动画:两帧图集走天下
所有角色(50 个顾客皮肤 + 2 个服务员)的走路动画都是运行时代码创建的。AnimationBase.js 只有 23 行:
let clip = cc.AnimationClip.createWithSpriteFrames(frames, 1);
clip.wrapMode = cc.WrapMode.Loop;美术只需要给每个皮肤画两帧(icon/icon2),代码动态建 clip 循环播放,“两帧鬼畜踏步"反而有一种魔性的喜感。50 套皮肤 = 100 帧图 + 一段 23 行的代码,动画资源维护成本为零。这个思路和《去赶海吧》的皮肤动画一脉相承。
2.5 金币堆:对象池 + 聚合拾取
金币是这个游戏最高频的创建/销毁对象,处理上花了最多心思(git 历史里"金币堆积优化"“金币拾取动画效果修改"好几个 commit 都在打磨这块):
- 金币走
cc.NodePool(上限 50),生成时随机旋转、1 秒散落动画到桌边随机位置; - 同一格子的金币记入
nearGroup按堆聚合,面额累加进valueMap——十份饭钱堆成一坨,而不是十个独立节点; - 池满时
getGoldByMoveNearby从邻居堆"借"一个实体过来复用; - 点击一堆 → 整堆回收,合成一个 “+总额” 数字标签飞向屏幕左上角的 HUD,飞行时长按距离等比计算,落地才到账。
性能和手感都照顾到了:节点数有上限,拾取有"整堆收进腰包"的爽感。
2.6 事件驱动与成就三态流转
全局只有一个 50 行的事件中心(eventuality.js,出自 Crockford《JavaScript 语言精粹》里的 eventuality 函数),只有 fire/on/removeListener 三个方法,所有跨模块通信——loadover、addGold、unLock、serverPlayer、invite、arriveAtGate、showDialog——都走它。
成就/每日任务系统(两个 Manager 合计 660 行)建立在这个事件中心上,数据按三态流转:
graph LR
A[unachieveMap<br/>未达成 按类型分桶] -->|事件计数达标| B[waitPayMap<br/>达成待领取]
B -->|点击领取| C[已领取<br/>写入存档]
C -->|自动检查| D[链上下一档成就]
事件一来,只检查对应桶里的成就,不用全表扫描;领取后自动把下一档(服务 10 人 → 服务 50 人 → ……)挪进未达成桶继续监听。
三、整体架构一览
graph TD
subgraph 场景层
L[loading.fire<br/>微信分包加载] --> G[game.fire<br/>主场景 103节点]
end
subgraph 入口与全局
G --> GAME[game.js<br/>顾客生成/对象池]
GL[global.js<br/>playerData/event/realTimeData]
GAME --> GL
end
subgraph 玩法层
GAME --> P[player.js<br/>顾客AI: 排队/寻路/点单/巡游]
GAME --> W[waiterPlayer.js<br/>迎宾氛围组]
GAME --> FM[FurnitureManager<br/>家具摆放/餐桌注册]
GAME --> GM[GoldManager<br/>金币堆/拾取]
P --> AS[AStar.js<br/>21×13格子寻路]
P --> SM[screenManager<br/>格子坐标系/跨屏门]
end
subgraph 系统层
AM[AchievementManager<br/>15条成就]
DM[DailyTaskManager<br/>每日任务随机6条]
LV[ListView<br/>自研虚拟列表]
GUI[guide.js<br/>HUD与五个面板]
end
subgraph 数据层
PD[player-data.js<br/>6份json配置+localStorage存档]
RT[realTimeData.js<br/>餐桌队列/对象池/运行时状态]
end
GL --> AM
GL --> DM
GL --> PD
GL --> RT
GUI --> LV
数据分三层:defines.js 常量表(配置路径 / 存档键 / 事件名)、player-data.js 持久层(6 份 json 配置 + localStorage 存档,配置是 Excel 手填导出的)、realTimeData.js 运行时状态(对象池、餐桌队列、zIndex 发号器)。首次进游戏送 400 金币,默认解锁每类第一个条目。
四、找茬彩蛋
和《去赶海吧》一样,代码里的瑕疵原样保留,欢迎找茬:
- A 代价函数传错参*(
AStar.js:87):算邻居格移动代价时,_costToMove(borderPosition, finish)把终点当成上一个节点传了进去,导致大量格子被按斜走 14 计费、g 值普遍虚高。好在曼哈顿 h 也偏小,错误互相稀释,路还是能找对,只是已经不是标准 A* 了。 - 全项目调了一个不存在的方法:事件中心只实现了
removeListener,但十几处onDestroy里都在调global.event.off(...)——调用即 TypeError,只是主场景从不销毁所以没炸过。就算有off,传进去的也是新建箭头函数,永远匹配不上原监听器。 - 死代码雷区(
utils/pathSearch.js):另一套从未被实例化的 A*,里面有this.year = '2002'调试残留、原型方法引用闭包变量(调用即 ReferenceError)、严格模式下裸赋值,是从网上教程抄来没跑通的草稿。paopao.js和food.js则是从泡泡龙/三消项目移民来的——food.js的onLoad会调一个不存在的方法,挂上节点必崩,所以它谁也没挂。 - 拼写宇宙:
createPalyer、walktoTabel、distence、memuPanel全文通行;最惨的是awarddiamand(成就表)和awarddiamond(家具表)两种拼法并存,AchievementItem.getAward只认前者,家具表里的钻石奖励永远读不到。 - 每日任务污染成就存档(
DailyTaskManager.js:220):每日服务计数误写进了成就用的总计数键;payAward的 switch 里还有两个永远进不去的undefinedcase。
另外 defines.js 里挂着 9 个根本不存在的配置路径(engineerNameConfig……“工程师"暴露了这是从另一个项目复制来的),food.json 里 54 道菜的名字是"食物1"到"食物54”——明显的占位数值没来得及填。
五、写在最后
从 git 历史能还原出开发顺序:跨屏寻路 → 桌子排队 → 成就系统 → 每日任务 → 钻石 → 金币特效与堆积优化 → 层级修正——先把核心玩法立住,再一层层包外围系统,最后打磨手感。
这是一个典型的个人开发者"麻雀型"项目:4000 行代码里同时住着写得相当讲究的虚拟列表、排队模型、跨屏寻路,和成堆的复制粘贴残留、拼写错误与失效代码。亮点和找茬各占一半,反而更适合做"真实项目代码考古"的样本——毕竟真实项目从来不是教科书。
源码在 https://gitee.com/soulnew/princes,欢迎 Star,也欢迎提 Issue 交流。