【 Cocos Creator 项目实战】益智游戏《2048》(附带完整源码工程)

本文乃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

作者创作不易,您的支持让我创造出更多更好的作品。​:)

【 Cocos Creator 项目实战】系列文章链接:

【 Cocos Creator 项目实战】益智游戏《2048》

​​​​​​【Cocos Creator 项目实战 】消灭星星加强版

相关推荐
胖哥真不错5 天前
Python实现随机分布式延迟PSO优化算法(RODDPSO)优化DBSCAN膨胀聚类模型项目实战
python·项目实战·随机分布式延迟pso·roddpso·dbscan膨胀聚类模型
胖哥真不错6 天前
Python基于TensorFlow实现双向长短时记忆循环神经网络加注意力机制回归模型(BiLSTM-Attention回归算法)项目实战
python·tensorflow·attention·项目实战·bilstm·双向长短时记忆循环神经网络·注意力机制回归模型
胖哥真不错6 天前
Python基于TensorFlow实现双向循环神经网络GRU加注意力机制分类模型(BiGRU-Attention分类算法)项目实战
python·tensorflow·attention·项目实战·bigru·双向循环神经网络gru·注意力机制分类模型
胖哥真不错8 天前
Python基于TensorFlow实现简单循环神经网络回归模型(SimpleRNN回归算法)项目实战
python·rnn·tensorflow·项目实战·简单循环神经网络回归模型·simplernn回归算法
jc_caterpillar8 天前
Cocos使用精灵组件显示相机内容
cocos
胖哥真不错9 天前
Python基于TensorFlow实现GRU-Transformer回归模型(GRU-Transformer回归算法)项目实战
python·gru·tensorflow·transformer·回归模型·项目实战·gru-transformer
胖哥真不错11 天前
Python实现贝叶斯优化器(Bayes_opt)优化简单循环神经网络分类模型(SimpleRNN分类算法)项目实战
python·tensorflow·项目实战·贝叶斯优化器·bayes_opt·简单循环神经网络分类模型·simplernn分类算法
胖哥真不错11 天前
Python基于TensorFlow实现循环神经网络GRU分类模型(GRU分类算法)项目实战
python·rnn·tensorflow·项目实战·循环神经网络·gru分类模型
胖哥真不错11 天前
Python基于TensorFlow实现简单循环神经网络分类模型(SimpleRNN分类算法)项目实战
python·rnn·tensorflow·项目实战·简单循环神经网络分类模型·simplernn分类算法
胖哥真不错11 天前
Python实现贝叶斯优化器(Bayes_opt)优化简单循环神经网络回归模型(SimpleRNN回归算法)项目实战
python·项目实战·贝叶斯优化器·bayes_opt·简单循环神经网络回归模型·simplernn回归算法