本文乃Siliphen原创,转载请注明出处
目录
游戏介绍
《2048》是一款曾经风靡全球的数字益智游戏。
目前(2023.08.14)在 App Store 的情况如下图:
关于这个游戏的更多情况可看看百度百科:百度百科-验证
概述
本文讲解用 Cocos Creator 实现经典《2048》的核心流程和算法。
Cocos Creator 版本:Cocos Creator 3.8.0
本文实现的游戏效果如下:
可以随意调整地图大小。可随意调整方块移动速度。
上图分别演示了 4 x 4 ,7 x 10 地图大小的效果。
可在这个地址运行体验下本文实现的版本:Cocos Creator | 2048
文本末尾给出完整实现的源码工程。
游戏整体流程
游戏执行一轮玩家操作的流程:等待玩家输入操作 -> 用户滑动屏幕 -> 移动数字方块 -> 合并方块 -> 空白地方随机出现一个数字方块 -> 等待玩家输入操作
以上流程是游戏玩家操作一次,游戏执行一轮的分解动作循环。
游戏通关条件:合成数字方块2048。
游戏失败条件:当整个棋盘都填满数字方块,且没有可以合并的方块。
游戏框架设计
主要流程控制类
从调用先后顺序开始依次如下:
|------------------------|-------------------|
| 类名 | 作用 |
| UiTouch | 处理用户触摸输入 |
| Merge | 处理移动、合并的逻辑和动画。 |
| FlowRound.fillEntity() | 在地图空白处随机生成一个数字方块。 |
| FlowRound.judge() | 判断输赢。 |
本文项目的代码组织结构
构建游戏世界
《2048》的游戏世界只有2个实体:数字方块、棋盘地图。
棋盘是数字方块的容器。后面的移动和合并算法,都是作用在棋盘上计算的。
数字方块
TypeScript
// 实体
export class Entity {
// 实体所代表的数值
public val : number ;
// 表现
public presentation = new EntityPresentation() ;
}
// 实体表现
export class EntityPresentation {
public root : Node ;
}
地图
地图数据本质是个二维数组。定义如下:
TypeScript
export class Map {
// 单件
public static ins : Map = null ;
// 地图单元格
public grid = new Array< Array< MapCell > >() ;
// 格子宽高
public size = new Size() ;
// 表现
public presentation = new MapPresentation() ;
}
// 地图表现
export class MapPresentation {
// 根节点
public root : Node ;
}
// 地图单元格
export class MapCell {
// 单元格上的实体
public entity : Entity = null ;
// 单元格所在的局部空间的坐标
public pos : Vec3 = null ;
}
我们规定地图单元格(0,0)的位置在地图显示的左下角。x , y 的增长分别向右边和上边延伸。如下图:
触摸手势识别
防触摸抖动
在触摸按下时记录按下的坐标,在触摸结束时用结束时的坐标减去按下时的坐标,得到一个向量。
判断这个向量的长度,大于某个数值后,就认为是有效的输入。
如果只是个很小的滑动,可能是抖动造成的,为了防止玩家误操作,可以丢弃这种输入。
判断用户输入的方向
用上一步减法得到的向量就可以判断用户操作的方向。
主要是用到 Math.atan2 这个系统函数。atan2 判断一个向量与 x 轴正方向的夹角,单位是弧度。
注意:atan2 的参数是 ( y , x ) , 不是( x , y ),y 是第一个参数。
不习惯使用弧度的话,可以转换成角度。
判断角度代码如下:
TypeScript
// 手势识别
export class GestureRecognition {
// 返回:上左下右 1234 从上开始顺时针。0 无效方向
public static exe( v : Vec2 ) : number {
let rad = Math.atan2( v.y , v.x ) ;
let degree = rad * ( 180 / Math.PI ) ;
if( 45 < degree && degree < 135 ){
return 1 ;
} else if( -45 < degree && degree < 45 ){
return 2 ;
} else if( -135 < degree && degree < -45 ){
return 3 ;
}else if( 135 < degree && degree < 180 ||
-180 < degree && degree < -135 ){
return 4 ;
}
// console.log( "度数:" + degree ) ;
return 0 ;
}
}
地图
任意大小的地图
本文的实现,可设置任意地图大小。
如下图:
上图展示了这些尺寸的地图大小效果:3 x 3 , 5 x 5 , 6 x 4 , 7 x 10
不同地图尺寸对应不同的地图根结点缩放值。
要实现可指定任意大小的地图的前提是,动态绘制地图。
先初始化地图二维数组结构的大小,然后,地图绘制类再处理地图的绘制。
初始化地图大小
TypeScript
// 地图数据初始化
export class MapInit {
public static exe( map : Map ) {
map.presentation.root = find( "Canvas/Map" ) ;
// 地图宽高
let mapWidth = 3 ;
let mapHeight = 3 ;
map.size = new Size( mapWidth , mapHeight ) ;
for( let y = 0 ; y < mapHeight ; ++y ) {
let row = new Array< MapCell >( ) ;
map.grid.push(row) ;
for( let x = 0 ; x < mapWidth ; ++x ){
let cell = new MapCell() ;
row.push( cell ) ;
} // end for
} // end for
}
}
地图绘制
本文实现的中心对其的地图布局,地图的几何中心点与其父节点的原点重叠。
算法是,先算出整个地图的大小,然后宽高分别除以2,先算出 ( 0 , 0 ) 起始逻辑坐标单元格的位置。
先算出左下角的起始单元格的位置,后续可以统一处理其他单元格位置。仅仅是通过不断累加间隔就行。
地图绘制 具体实现查看源码工程的类 MapDraw
设置地图大小位置:MapInit.exe 函数
合并和移动
这个是2048的核心玩法实现,也是最难的部分。
合并和移动的逻辑
可以先算方块逻辑上的合并,后算方块逻辑上的移动。
也可以合并和移动合并在一起计算。
上下左右的合并和移动要分别处理。
这里列举用户向左( <- )滑动的处理算法,其他3个方向的以此类推。为了说明原理和简单起见以下为描述性伪代码。
TypeScript
// 一行行遍历地图。从左到右(->)
for( let y = 0 ; y < map.size.height ; ++y ) {
for( let x = 0 ; x < map.size.width ; ++x ) {
let cell = map.grid[ y ][ x ] ;
// 如果单元格上没有实体,略过。因为我们只处理实体。不处理空格。
if( cell.entity == null ) continue ;
let cell2 = 向右(->)查找一个最近的实体所在的单元格。
if( cell2 != null && cell2 和 cell 的实体数字相同 )
{ // 这表示找到一个可以合并的实体
2个实体合并,合并和的实体放在 cell 单元格的位置上。
}
以 cell 单元格为起点向左(<-)查找一个连续的空位的最右边的那个空位
这个空位便是 cell 上的方块实体要移动到的位置。
} // end for
} // end for
算法图示:
丝滑的合并和移动动画
如果只是按照上一步说的先在逻辑上计算合并和移动的结果,然后直接更新画面显示,会显得很生硬。
大部分的瞬间更新结果都会让画面显得生硬。好的做法是,有个滑动和合并的移动缓动动画。
加入动画后,流程就变成了:
所有的方块都会先移动到一边,然后进行合并,如果合并后留出了空位,需要再移动。保证移动后,中间不留空位。
这个流程需要对以上的逻辑处理进行改造。
具体实现查看源码工程的类 Merge
本文的完整实现源码工程
源码工程下载地址:Cocos Store
作者创作不易,您的支持让我创造出更多更好的作品。:)