# princes **Repository Path**: soulnew/princes ## Basic Information - **Project Name**: princes - **Description**: 点击类游戏,模仿动物餐厅 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2020-02-14 - **Last Updated**: 2026-09-06 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README --- title: "《princes》:用 Cocos Creator 写一款餐厅经营放置小游戏" date: 2020-09-05T21:00:00+08:00 draft: false summary: "介绍我用 Cocos Creator 2.3.4 + JavaScript 开发并开源的餐厅经营放置小游戏《princes》:顾客自动上门、排队等位、A* 寻路点餐、点击收金币、解锁食物家具顾客的完整循环,以及手写 A* 跨屏寻路、自研虚拟列表 ListView、餐桌排队模型、运行时程序化动画等技术实现细节。" tags: ["Cocos Creator", "游戏开发", "JavaScript", "微信小游戏", "放置游戏", "2D 游戏"] categories: ["游戏开发"] --- # 《princes》:用 Cocos Creator 写一款餐厅经营放置小游戏 继《去赶海吧》之后,把更早做的一款餐厅经营小游戏也整理开源了,照例写篇文章介绍玩法和实现上的技术点。仓库名叫 princes(历史遗留命名,内容其实是餐厅),代码里还住着不少从其他项目移民过来的残留,这次一并"考古"出来。 - 开源地址: - 技术栈:Cocos Creator 2.3.4 + JavaScript - 平台:微信小游戏(竖屏 720×1280) - 规模:39 个脚本,约 4200 行代码,单主场景 游戏核心玩法一句话概括:**开一家餐厅,顾客自动上门排队等位、走到餐桌点餐、用餐完留下金币,你点击收金币,再用金币解锁更多食物、餐桌和顾客皮肤**——放置 + 点击收菜的经典经营循环。 --- ## 一、玩法介绍 ### 1.1 核心循环 玩家几乎不直接操作角色,核心是"看店 + 点击收钱 + 花钱解锁": ```mermaid graph LR A[顾客自动上门
每15秒补1个] --> B{有空餐桌?} B -->|有| C[A*寻路走到餐桌] B -->|没有| D[门口排队等位] D -->|有空桌释放| C C --> E[头顶弹气泡点单] E --> F[用餐5秒
点击气泡可加速] 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 对"门坐标"缝合: ```mermaid 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 行: ```javascript 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 行)建立在这个事件中心上,数据按三态流转: ```mermaid graph LR A[unachieveMap
未达成 按类型分桶] -->|事件计数达标| B[waitPayMap
达成待领取] B -->|点击领取| C[已领取
写入存档] C -->|自动检查| D[链上下一档成就] ``` 事件一来,只检查对应桶里的成就,不用全表扫描;领取后自动把下一档(服务 10 人 → 服务 50 人 → ……)挪进未达成桶继续监听。 --- ## 三、整体架构一览 ```mermaid graph TD subgraph 场景层 L[loading.fire
微信分包加载] --> G[game.fire
主场景 103节点] end subgraph 入口与全局 G --> GAME[game.js
顾客生成/对象池] GL[global.js
playerData/event/realTimeData] GAME --> GL end subgraph 玩法层 GAME --> P[player.js
顾客AI: 排队/寻路/点单/巡游] GAME --> W[waiterPlayer.js
迎宾氛围组] GAME --> FM[FurnitureManager
家具摆放/餐桌注册] GAME --> GM[GoldManager
金币堆/拾取] P --> AS[AStar.js
21×13格子寻路] P --> SM[screenManager
格子坐标系/跨屏门] end subgraph 系统层 AM[AchievementManager
15条成就] DM[DailyTaskManager
每日任务随机6条] LV[ListView
自研虚拟列表] GUI[guide.js
HUD与五个面板] end subgraph 数据层 PD[player-data.js
6份json配置+localStorage存档] RT[realTimeData.js
餐桌队列/对象池/运行时状态] end GL --> AM GL --> DM GL --> PD GL --> RT GUI --> LV ``` 数据分三层:`defines.js` 常量表(配置路径 / 存档键 / 事件名)、`player-data.js` 持久层(6 份 json 配置 + localStorage 存档,配置是 Excel 手填导出的)、`realTimeData.js` 运行时状态(对象池、餐桌队列、zIndex 发号器)。首次进游戏送 400 金币,默认解锁每类第一个条目。 --- ## 四、找茬彩蛋 和《去赶海吧》一样,代码里的瑕疵原样保留,欢迎找茬: 1. **A* 代价函数传错参**(`AStar.js:87`):算邻居格移动代价时,`_costToMove(borderPosition, finish)` 把终点当成上一个节点传了进去,导致大量格子被按斜走 14 计费、g 值普遍虚高。好在曼哈顿 h 也偏小,错误互相稀释,路还是能找对,只是已经不是标准 A* 了。 2. **全项目调了一个不存在的方法**:事件中心只实现了 `removeListener`,但十几处 `onDestroy` 里都在调 `global.event.off(...)`——调用即 TypeError,只是主场景从不销毁所以没炸过。就算有 `off`,传进去的也是新建箭头函数,永远匹配不上原监听器。 3. **死代码雷区**(`utils/pathSearch.js`):另一套从未被实例化的 A*,里面有 `this.year = '2002'` 调试残留、原型方法引用闭包变量(调用即 ReferenceError)、严格模式下裸赋值,是从网上教程抄来没跑通的草稿。`paopao.js` 和 `food.js` 则是从泡泡龙/三消项目移民来的——`food.js` 的 `onLoad` 会调一个不存在的方法,挂上节点必崩,所以它谁也没挂。 4. **拼写宇宙**:`createPalyer`、`walktoTabel`、`distence`、`memuPanel` 全文通行;最惨的是 `awarddiamand`(成就表)和 `awarddiamond`(家具表)两种拼法并存,`AchievementItem.getAward` 只认前者,家具表里的钻石奖励永远读不到。 5. **每日任务污染成就存档**(`DailyTaskManager.js:220`):每日服务计数误写进了成就用的总计数键;`payAward` 的 switch 里还有两个永远进不去的 `undefined` case。 另外 `defines.js` 里挂着 9 个根本不存在的配置路径(`engineerNameConfig`……"工程师"暴露了这是从另一个项目复制来的),food.json 里 54 道菜的名字是"食物1"到"食物54"——明显的占位数值没来得及填。 --- ## 五、写在最后 从 git 历史能还原出开发顺序:跨屏寻路 → 桌子排队 → 成就系统 → 每日任务 → 钻石 → 金币特效与堆积优化 → 层级修正——先把核心玩法立住,再一层层包外围系统,最后打磨手感。 这是一个典型的个人开发者"麻雀型"项目:4000 行代码里同时住着写得相当讲究的虚拟列表、排队模型、跨屏寻路,和成堆的复制粘贴残留、拼写错误与失效代码。亮点和找茬各占一半,反而更适合做"真实项目代码考古"的样本——毕竟真实项目从来不是教科书。 源码在 ,欢迎 Star,也欢迎提 Issue 交流。