【Cocos Creator 项目实战 】消灭星星加强版(附带完整源码工程)
本文乃Siliphen原创,转载请注明出处
目录
概述
游戏整体流程
游戏框架设计
单一职责的类
主要流程控制类
核心玩法模块
UI:
游戏世界:
本文项目的代码组织结构
作者项目实践总结
场景只有一个入口脚本
尽量少在节点上挂载脚本
构建游戏世界
ECS 设计
消除物
棋盘地图
逻辑计算和显示分离
消除的实现
查找联通分量
逐步由内向外扩张的消除动画
掉落的实现
合并的实现
道具的实现
本文的完整实现源码工程
概述
《消灭星星》是一个爆款休闲游戏,累计用户5亿+。
目前(2023.08.06)在 App Store 上39.6万个评分,评分4.6,益智解谜类第7名。

参考链接:App Store 上的“消灭星星全新版®”
本文讲解用 Cocos Creator 实现一款加强效果版的《消灭星星》的核心流程和算法。
本文实现的游戏效果如下:

可在这个地址运行体验下本文实现的版本:Cocos Creator | 消灭星星
文本末尾给出完整实现的源码工程。
游戏整体流程
游戏执行一轮玩家操作的流程:等待玩家点击操作 -> 用户点击 -> 消除-> 掉落 -> 合并 -> 等待玩家点击操作
以上流程是游戏玩家操作一次,游戏执行一轮的分解动作循环。
整个游戏的组成:游戏由N关组成,一关由N轮玩家操作组成。
游戏框架设计
单一职责的类
可以把一轮中的每个动作都独立成一个控制类,每个控制类只负责一种动作,比如A类只负责消除控制,B类只负责掉落控制。
这是敏捷开发中的重要原则:单一职责。一个类的功能越是单一,它就越内聚、越和其他系统解耦合。
每种控制类在它负责的单一动作执行完成后,用回调通知其他系统,它已经完成,可以进行下一步操作。
比如:消除控制系统处理消除完成后,用一个 onComplete 回调通知外界,已经完成了消除这个动作。
掉落控制系统监听消除控制系统的 onComplete ,处理消除后下一步的掉落控制。
主要流程控制类
从调用先后顺序开始依次如下:
| 类名 | 作用 |
| UiTouch | 处理用户触摸输入 |
| Eliminate | 处理消除。消除上下左右连色的实体。 |
| Fall | 消除后会留下空位,控制消除实体掉落下来。 |
| Merge | 掉落后,如果有空的列,那么向左边靠拢合并起来。 |
核心玩法模块
核心玩法分成2大部分:UI、游戏世界
UI:
包括:按钮、弹窗、分数显示、玩家输入 等部分。是所有用户界面的集合。
这个部分的开发有点类似于做 APP。可以用 MVC 等 APP 常用开发模式。
游戏世界:
这是游戏开发独有的部分。处理游戏世界中游戏实体的行为、游戏实体之间的关系和交互、游戏世界的规则等。
游戏核心玩法的开发主要关注这部分。
因为游戏世界不可能简单分为几个层,比如,什么显示层,逻辑层,数据层等。
有可能实体之间的关系和交互很复杂,MVC 等传统 APP 开发模式并不适用。一般大型游戏会采用 ECS 等设计。
本文项目的代码组织结构
如下图:

作者项目实践总结
场景只有一个入口脚本
一个场景只挂一个入口脚本,各种节点的引用使用 find、node.getChildByPath 等去查找。
就像 C/C++ 语言有一个唯一入口函数 main 。
这样做的好处是:在代码中初始化各个系统,有明确的初始化顺序。
在多个节点上挂多个脚本,默认情况下有个问题,哪个脚本先执行哪个脚本后执行。有时候执行顺序是非常重要的。
编辑器可以指定节点上挂的脚本的执行顺序,但这是额外的维护负担。不如在代码中指定的维护性好。
尽量少在节点上挂载脚本
少挂载脚本的好处是:
- 降低脚本Missing情况的维护成本。
- 节约性能。
- 提高项目移植性。比如移植到其他引擎上。
想象一个情况,一个场景中有很多节点,很多节点都挂有脚本。出于某些原因,脚本和节点的挂载关系丢失了。
编辑器节点上要么不显示脚本,要么脚本显示为丢失(Missing)。
场景简单还好,重新手动拖脚本到节点上。场景复杂,那就很麻烦。绝大部分情况,只是知道节点Missing了脚本,但不知道Missing的是哪个脚本。
为什么有时候会Missing脚本?原因很多,可能有如下几种:
* 用户误操作。比如 破坏了 *.meta 文件。
* 多人协作 *.meta 文件冲突,导致脚本丢失。
* 引擎版本多次低级高级来回切换。
* 一些说不清楚的,莫名其妙的情况。
构建游戏世界
《消灭星星》的游戏世界只有2个实体:消除物、棋盘地图。
棋盘没有画面表现。棋盘是消除物的容器,棋盘限定了消除物计算规则和运动规则。
后面的查找消除算法和掉落控制,都是作用在棋盘上计算的。
ECS 设计
本项目使用类似 ESC 的设计,非严格意义上的 ECS ,是如下定义:
Entity 是 Componet 的容器。Component 只有数据,没有逻辑。System 没有数据,只有逻辑。
实体和游戏世界的交互实现,实体和实体之间的交互实现,都放在 System 中。
这种设计的好处是:高扩展性。高维护性。易于移植到其他引擎。易于引擎升级。
消除物
定义如下:
// 消除物实体
export class Elimination
{// 类型IDpublic kindId = "" ;public presentaion = new EliminationPresentaion() ;}// 消除物表现组件
export class EliminationPresentaion
{// 根节点public node : Node = null ;// 动画public amin : Animation = null ; }
Elimination 是消除物实体类。EliminationPresentaion 是消除物实体的表现组件类。
实体类只是组件类的容器。实体类和组件类都只有定义,没有逻辑。
棋盘地图
棋盘数据本质是个二维数组。定义如下:
// 地图数据
export class MapData
{// 单件/* */public static ins = new MapData() ;// 数据网格public grid = new Array< Array< Cell > >() ;// 地图大小public size = new Size();// 是否是有效地坐标public isValid(coord : Vec2) : boolean{if( coord.x < 0 || coord.y < 0 || coord.x >= this.size.x || coord.y >= this.size.y ) return false ;return true ;}}// 地图单元格
export class Cell
{// 消除物public elimination : Elimination = null ;// 坐标public coord = new Vec2();// 在世界空间中单元格的位置。public pt = new Vec3(); }
二位数组对应的位置如下图:

左下角的索引是(0,0),右上角是(9,9)。
逻辑计算和显示分离
先计算好结果后再播放达到这个结果的过渡动画。逻辑计算和播放显示动画的分离可以让代码结构更清晰,维护性更高。
后面的处理都是先在内存中计算好地图状态:消除后地图哪些单元格为空,掉落后消除物实体都落在哪个单元格上 等。
计算好地图状态后再处理画面显示:播放消除动画,播放掉落动画等。
消除的实现
先看下文本实现的消除效果:

大部分《消灭星星》的实现都是点击后瞬间一起消除。
本文做了不一样的效果,从点击的消除物开始逐步由内向外扩张的消除。
不管是瞬间消除,还是某种控制动画消除,第一步都是“查找相邻的同类消除物”。
查找联通分量
术语“查找联通分量”很多《数据结构》的书都会有介绍。此处,我们用来查找相邻的同类消除物。
使用深度优先搜索(DFS)实现,输出一颗树。树的根结点是玩家点击的那个消除物。
为什么要输出一棵树?因为要按照树的层次进行消除才能实现逐步由内向外扩张的动画。
具体实现可查看工程源码的 ConnectionFind.find 函数。
这里为了讲解算法原理,用伪代码说明算法的核心思想。
// start 是点击的消除物
dfs( start )
{// 结果数据结构,用 Map 表示一棵树。key 是一个被发现的消除物,value 是这个消除物的父节点。let ret = new Map< Elimination , Elimination >() ; // 创建一个栈 stack q ;let q = new Stack() ; // 访问记录。该数据结构是为了防止重复访问那些已经访问过的消除物let visit = Set< Elimination >() ;q.push( start ) ; // 起始点入栈ret.set( start , null ) ; // 点击的消除物是根结点,根结点没有父节点。for( ; q.count > 0 ; ) // 栈不为空就一直循环{let t = q.pop() ; // 出栈一个节点let list = expand( t ) ; // 查找出栈节点上下左右4个方向相邻的同样的节点foreach( let t2 in list ) // 所有查找出来的节点入栈{if( visit.has( t2 ) ) continue ; // 跳过访问过的消除物q.push( t2 ) ; ret.add( t2 , t ) ; // 发现一个行节点t2,它的父节点是t。visit.add( t2 ) ;} // end for} // end forreturn ret ;
}
《消灭星星》最难的算法就是这个“查找联通分量”了。
如果一下子不理解也没关系,可以反复琢磨下本文作者的伪代码和具体实现。
或者是查阅数据结构或算法的书籍,深入、详细的学习下。加油!:)
逐步由内向外扩张的消除动画
在上一步中,我们获得了一颗消除物节点树。是一个键值对数据结构,key 表示发现的节点,value 表示发现的节点的父节点。
这里,我们处理这棵树结构为按照树的层次划分的数据结构:let levels = new Array< Array< Elimination > >()
levels[ 0 ] 表示树第 1 层的节点集合。树根只有一个起始节点。
levels[ 1 ] 表示树第 2 层的节点集合。
... 以此类推
间隔一层层的整体消除即可。
如何把 Map< Elimination , Elimination > 处理成 Array< Array< Elimination > > 的层次结构呢?
遍历这个 Map,对每个 key 向上查找,直到查到 null 遇到根结点为止。就可以得知当前 key 所在的层次。
按照层次放入对应的 Array 数组容器中即可。
具体实现查看源码工程的类 SeqCtrl。
掉落的实现
消除后,棋盘地图的一些被消除的消除物所在的单元格会被设为空。上面的消除物会掉落下来。
从棋盘底部向上一行行遍历,遇到一个消除物后,向下查找一个空位,如果能找到一个空位,就把这个消除物设置到那个空位上。
先设置棋盘的逻辑状态。后计算被移动的消除物的新的显示位置,做一个移动动画即可。
具体实现查看源码工程的类 Fall。
合并的实现
本文实现的合并效果如下图:

合并的处理在掉落之后。
遍历棋盘最底部的那一行,遍历顺序从左到右。
因为之前已经执行了掉落,最底部的一行有空位的话,就说明有棋盘地图有一列为空。
如果发现了一个空位,就说明需要合并,向后查找一个非空列,整体移动那一列的消除物到空位即可。
具体实现查看源码工程的类 Merge。
道具的实现
经典的消灭星星有3个道具:指定一个消除物替换为另一个指定的消除物、九宫格炸弹,全体消除物随机变换。
九宫格炸弹
具体实现查看源码工程的类 PropBombNine、TouchPropBombNine
全体消除物随机变换
遍历整个棋盘地图,随机替换消除物即可。
具体实现查看源码工程的类 PropChangeAll。
单点替换
这个道具的实现相对以上2个比较特殊,耦合了点击操作。
先要设置触摸模式为使用道具,然后玩家点击后,如果点击的是一个消除物,
就在这个消除物的上方显示替换UI,供玩家选择变换后的消除物。
具体实现查看源码工程的类 PropChangeOne、TouchPropChangeOne
本文的完整实现源码工程
源码工程下载地址:Cocos Store
作者创作不易,您的支持让我创造出更多更好的作品。:)
相关文章:
【Cocos Creator 项目实战 】消灭星星加强版(附带完整源码工程)
本文乃Siliphen原创,转载请注明出处 目录 概述 游戏整体流程 游戏框架设计 单一职责的类 主要流程控制类 核心玩法模块 UI: 游戏世界: 本文项目的代码组织结构 作者项目实践总结 场景只有一个入口脚本 尽量少在节点上挂载脚本 构…...
2023软件测试岗必问的100个面试题【含答案】
一、测试理论 1.什么是软件测试? 答:软件测试是通过执行预定的步骤和使用指定的数据,以确定软件系统在特定条件下是否满足预期的行为。 2.测试驱动开发(TDD)是什么? 答:测试驱动开发是一种开…...
MediaExtractor MediaCodec手动解码播放音乐
MediaExtractor MediaCodec手动解码播放音乐,笔记 private class DecodeAudio implements Runnable {Overridepublic void run() {//开始播放pcmaudioTrack.play();MediaExtractor extractor null;MediaCodec codec null;Log.i(TAG, "run: init");FileOutputStrea…...
element表格+表单+表单验证结合运用
目录 一、结果展示 二、实现代码 一、结果展示 1、图片 2、描述 table中放form表单,放输入框或下拉框或多选框等; 点击添加按钮,首先验证表单,如果存在没填的就验证提醒,都填了就向下添加一行表单表…...
亚马逊云科技发布Amazon HealthScribe,使用生成式AI技术实现临床文档的自动生成
近日,亚马逊云科技在纽约峰会上推出了Amazon HealthScribe,该服务符合HIPAA(《健康保险流通与责任法案》)的相关要求,可为医疗软件供应商提供一种基于语音和文字识别的生成式AI技术,帮助其创建能够自动生成…...
Windows11安装Linux子系统,并实现服务自启动,局域网访问,磁盘挂载
Windows11安装Linux子系统,并实现服务自启动,局域网访问,磁盘挂载 一、准备工作二、安装Linux子系统(wsl2)三、为Linux子系统设置桥接网络检查wsl版本在 Hyper-V 管理器中创建虚拟交换机创建 WSL 配置文件启动wsl 四、设置Windows开机自启动L…...
【Git】保姆级详解:Git配置SSH Key(密钥和公钥)到github
博主简介:22级计算机科学与技术本科生一枚🌸博主主页:是瑶瑶子啦每日一言🌼: “当人们做不到一些事情的时候,他们会对你说你也同样不能。”——《当幸福来敲门》 克里斯加德纳 Git配置SSH Key 一、什么是Git?二、什么…...
离线环境conda虚拟环境备份迁移--conda pack问题
1.第一步:创建虚拟环境 conda create -n pyenv --clone base 或者 conda create -n pyenv python3.8.5 --offline 命令执行结束,在路径/xxxx/anaconda/envs 下看到pyenv 或者 conda info --envs 查看罗列虚拟环境 2.第二步:打包环境 conda …...
挂载 IK 分词器至 Elasticsearch Docker 容器 - Docker Docker Compose 教程
简介 本博客将讲解如何在 Docker 和 Docker-Compose 中运行 Elasticsearch,并挂载 IK 分词器。 步骤 一、快速运行Elasticsearch:8.1.3 1.首先,我们需要创建一个新的 Docker 网络:"elastic"。这个网络会提供给我们接下来所要创…...
7.6 通俗易懂解读残差网络ResNet 手撕ResNet
一.举例通俗解释ResNet思想 假设你正在学习如何骑自行车,并且想要骑到一个遥远的目的地。你可以选择直接骑到目的地,也可以选择在途中设置几个“中转站”,每个中转站都会告诉你如何朝着目的地前进。 在传统的神经网络中,就好比只…...
robotframework+selenium 进行webui页面自动化测试
robotframework其实就是一个自动化的框架,想要进行什么样的自动化测试,就需要在这框架上添加相应的库文件,而用于webui页面自动化测试的就是selenium库. 关于robotframework框架的搭建我这里就不说了,今天就给大家根据一个登录的实…...
手机突然无法获取ip地址
在日常生活中,我们对手机的依赖越来越大,尤其是在联网方面。然而,有时候我们可能会遇到手机无法获取IP地址的问题,这给我们的正常使用带来了很多不便。当我们的手机无法获得IP地址时,我们将无法连接到互联网或局域网&a…...
C++——关于命名空间
写c项目时,大家常用到的一句话就是: using namespace std; 怎么具体解析这句话呢? 命名冲突: 在c语言中,我们有变量的命名规范,如果一个变量名或者函数名和某个库里面自带的库函数或者某个关键字重名&…...
怎么进行流程图制作?用这个工具制作很方便
怎么进行流程图制作?流程图是一种非常有用的工具,可以帮助我们更好地理解和展示各种复杂的业务流程和工作流程。它可以将复杂的过程简化为易于理解的图形和文本,使得人们更容易理解和跟踪整个流程。因此,制作流程图是在日常工作中…...
【ChatGPT 指令大全】怎么使用ChatGPT来辅助学习英语
在当今全球化的社会中,英语已成为一门世界性的语言,掌握良好的英语技能对个人和职业发展至关重要。而借助人工智能的力量,ChatGPT为学习者提供了一个有价值的工具,可以在学习过程中提供即时的帮助和反馈。在本文中,我们…...
Ubuntu20配置仅主机网络
Ubuntu20配置仅主机网络,使虚拟机与物理机网络联通且配置固定IP 进入终端:vim /etc/netplan/01-network-manager-all.yaml 修改为: network:ethernets:enp0s8:addresses: [192.168.138.108/24]dhcp4: false optional: truegateway4: 192.…...
调整奇数偶数顺序
调整数组使奇数全部都位于偶数前面。 题目: 输入一个整数数组,实现一个函数,来调整该数组中数字的顺序使得数组中所有的奇数位于数组的前半部分,所有偶数位于数组的后半部分。 思路: 1. 给定两个下标left和right&#…...
日志的规范
确定日志级别: 确保你的系统有一个明确的日志级别策略。通常,日志级别包括DEBUG,INFO,WARN,ERROR和FATAL。DEBUG级别的日志记录所有详细信息,适用于开发和调试环境。INFO级别的日志记录常规操作信息&#x…...
Spring AOP(AOP概念,组成成分,实现,原理)
目录 1. 什么是Spring AOP? 2. 为什么要用AOP? 3. AOP该怎么学习? 3.1 AOP的组成 (1)切面(Aspect) (2)连接点(join point) (3&a…...
Android WebView简单应用:构建内嵌网页浏览功能
在现代移动应用开发中,内嵌网页浏览功能是许多应用程序的常见需求。Android平台提供了WebView组件,它允许开发者将网页内容嵌入到应用中,并提供了丰富的功能和定制选项。本文将介绍如何在Android应用中使用WebView组件,帮助您快速…...
Flask RESTful 示例
目录 1. 环境准备2. 安装依赖3. 修改main.py4. 运行应用5. API使用示例获取所有任务获取单个任务创建新任务更新任务删除任务 中文乱码问题: 下面创建一个简单的Flask RESTful API示例。首先,我们需要创建环境,安装必要的依赖,然后…...
ESP32 I2S音频总线学习笔记(四): INMP441采集音频并实时播放
简介 前面两期文章我们介绍了I2S的读取和写入,一个是通过INMP441麦克风模块采集音频,一个是通过PCM5102A模块播放音频,那如果我们将两者结合起来,将麦克风采集到的音频通过PCM5102A播放,是不是就可以做一个扩音器了呢…...
Python如何给视频添加音频和字幕
在Python中,给视频添加音频和字幕可以使用电影文件处理库MoviePy和字幕处理库Subtitles。下面将详细介绍如何使用这些库来实现视频的音频和字幕添加,包括必要的代码示例和详细解释。 环境准备 在开始之前,需要安装以下Python库:…...
JUC笔记(上)-复习 涉及死锁 volatile synchronized CAS 原子操作
一、上下文切换 即使单核CPU也可以进行多线程执行代码,CPU会给每个线程分配CPU时间片来实现这个机制。时间片非常短,所以CPU会不断地切换线程执行,从而让我们感觉多个线程是同时执行的。时间片一般是十几毫秒(ms)。通过时间片分配算法执行。…...
python报错No module named ‘tensorflow.keras‘
是由于不同版本的tensorflow下的keras所在的路径不同,结合所安装的tensorflow的目录结构修改from语句即可。 原语句: from tensorflow.keras.layers import Conv1D, MaxPooling1D, LSTM, Dense 修改后: from tensorflow.python.keras.lay…...
NXP S32K146 T-Box 携手 SD NAND(贴片式TF卡):驱动汽车智能革新的黄金组合
在汽车智能化的汹涌浪潮中,车辆不再仅仅是传统的交通工具,而是逐步演变为高度智能的移动终端。这一转变的核心支撑,来自于车内关键技术的深度融合与协同创新。车载远程信息处理盒(T-Box)方案:NXP S32K146 与…...
HybridVLA——让单一LLM同时具备扩散和自回归动作预测能力:训练时既扩散也回归,但推理时则扩散
前言 如上一篇文章《dexcap升级版之DexWild》中的前言部分所说,在叠衣服的过程中,我会带着团队对比各种模型、方法、策略,毕竟针对各个场景始终寻找更优的解决方案,是我个人和我司「七月在线」的职责之一 且个人认为,…...
【Linux】Linux安装并配置RabbitMQ
目录 1. 安装 Erlang 2. 安装 RabbitMQ 2.1.添加 RabbitMQ 仓库 2.2.安装 RabbitMQ 3.配置 3.1.启动和管理服务 4. 访问管理界面 5.安装问题 6.修改密码 7.修改端口 7.1.找到文件 7.2.修改文件 1. 安装 Erlang 由于 RabbitMQ 是用 Erlang 编写的,需要先安…...
数据结构第5章:树和二叉树完全指南(自整理详细图文笔记)
名人说:莫道桑榆晚,为霞尚满天。——刘禹锡(刘梦得,诗豪) 原创笔记:Code_流苏(CSDN)(一个喜欢古诗词和编程的Coder😊) 上一篇:《数据结构第4章 数组和广义表》…...
【Pandas】pandas DataFrame dropna
Pandas2.2 DataFrame Missing data handling 方法描述DataFrame.fillna([value, method, axis, …])用于填充 DataFrame 中的缺失值(NaN)DataFrame.backfill(*[, axis, inplace, …])用于**使用后向填充(即“下一个有效观测值”)…...
