Egret 开发斗地主游戏全程讲解

前言:本来定这个主题的时候,想着写个三五页也就好了,一个没刹住车,巴拉巴拉写了一大堆。

预览地址:http://132.232.2.225/

本文从一个 HTML5 游戏引擎 Egret 开始谈起,从 Egret 项目的搭建到它选用的语言,到游戏的核心逻辑及设计思路,再到游戏的界面开发,最后讲一下游戏的打包发布。简单整理一下,在这里做个目录。

一、项目简介:介绍项目运行方式和运行效果

二、Egret 简介:介绍 Egret 及环境搭建

三、一起写个 Hello World:介绍如何新建及运行游戏项目

四、TypeScript 简介:介绍 TypeSctipt 语法特性

五、斗地主游戏规则及核心逻辑:详细说明游戏核心逻辑设计思路及具体实现

六、斗地主游戏(单机版)界面开发:主要介绍游戏各界面开发思路

七、游戏打包发布:介绍游戏打包到各平台的方法及运行效果

一、项目简介

1.1 项目说明

本项目实现了斗地主游戏的单机版、联机版核心功能。

由于文章篇幅问题,本文只讲解单机版斗地主的开发过程。根据情况联机版会考虑在下一期 Chat 中讲解(含界面、服务端、数据库的主要逻辑)。

项目运行方式:git 直接克隆上面的源码,使用 Egret Wing 打开运行即可(别急,Egret Wing 的使用马上告诉你)。

由于本人只是个码农,不擅长 UI 和审美,界面设计在我的审美能力限制内用 Adobe Illustrator 简单形容了一下。觉得欣赏不动的同志们可以自己设计一套 UI,欢迎分享你们实现的效果(坏笑)。

由于个人能力和代码风格的不同,欢迎大家对项目提出意见或建议。

1.2 预览效果

1.开始界面

2.游戏大厅界面

3.游戏界面

二、Egret 简介

Egret 是一套 HTML5 游戏开发解决方案,使用 Egret 开发的游戏可以轻松发布到 HTML5、iOS、Android、微信小游戏、Facebook、QQ 玩一玩、百度小游戏、Blockchain Game 等各个平台运行。

官网地址:https://www.egret.com/

Egret 包含以下生态:

Egret Engine:遵循 HTML5 标准的 2D 引擎及全新打造的 3D 引擎。

Egret Wing:支持主流开发语言与技术的编辑器。

Egret Native:将基于 Egret 引擎开发的 HTML5 游戏轻松转换为 Android 或 iOS 的原生游戏。

ResDepot:可视化资源管理工具。

Texture Merger:纹理集打包和动画转换工具。

Egret Inspector:供 Chrome 开发者使用的插件。

Egret Feather:粒子编辑器。

DragonBones:面向设计师的 2D 游戏动画和富媒体内容创作平台。

比较人性化的是,这么一堆东东不需要你一个一个去找,只要下载个 Egret Launcher 就好了。

Egret Launcher 相当于一个启动器,负责 Egret 所有构件的管理和使用。

三、一起写个 Hello World

3.1 安装 Egret Launcher(启动器)

官方下载地址:https://www.egret.com/products/engine.html

下载安装好 Egret Launcher 后,打开,界面是这样子的:

可以看到,首页会有很多介绍和教程的链接,如果懒得去官方翻文档,在这里一样可以找到你想要到了解的东西。

3.2 安装 Egret Engine(2D 引擎)

切到“引擎页面”,选择一个引擎安装。选一个,直接下载就行了,我项目中用的是 5.2.4 版本,你可以选择用 5.2.4 或更高的版本。

3.3 安装 Egret Wing(编辑器)

切到 "工具" 界面,选择安装 Egret Wing。这个界面可以看到 Egret 的所有构件,有兴趣的可以挨个鼓捣鼓捣,挺好玩的。

3.4 新建项目

安装完引擎和编辑器后,就可以写我们的的 Hello World 了,切到 “项目” 界面,点击“创建项目”,创建完成后,在项目列表中点击右边的翅膀图标即可在 Egret Wing 中愉快地写代码了。

在 “创建项目” 界面,注意:

填写项目名称,选择项目路径

项目类型要选 Egret 游戏项目

其他设置按默认的来

如下图:

3.5 看一眼代码

(图 3.5)

Egret Wing 界面介绍

快捷菜单:有保存、发布、版本控制、搜索等菜单

文件列表:显示当前项目的目录结构

编辑区:写代码的地方

控制台:代码调试、日志输出、命令行输入的地方

帮助文档:显示当前文件的一些信息,包含一些教程的链接

快捷启动按钮组:有调试项目、运行项目、编译项目、发布项目等快捷按钮

3.6 运行项目

点击“运行项目”,终端会出现如下提示,并自动打开浏览器。注意,当我们在开发过程中,代码发生变化后页面并不会实时显示变化,要先点击 "编译项目" 后刷新页面。

如果没有自动打开浏览器,使用浏览器访问终端输出的地址(如本例地址为:http://192.168.31.105:3000/index.html) 即可看到效果,运行效果图如下:

好了,至此,我们的 Hello World 项目运行完毕,对照着图 3.5 (往前数第 3 张图,发现文章中用到的图有点多,尽量一图多用吧)蓝字标注的代码,我们简单了解下 Egret Engine2D 的语法格式。

这里做下简单说明,具体语法参照官方文档:

http://developer.egret.com/cn/github/egret-docs/Engine2D/update/update5213/index.html

整个界面分为五部分:

背景图

//创建一张背景图

let sky = this.createBitmapByName("bg_jpg");

        //将背景图加载到画布上

        this.addChild(sky);

        //取画布的宽

        let stageW = this.stage.stageWidth;

        //取画布的高

        let stageH = this.stage.stageHeight;

        //设置背景图的宽度等于画布的宽

        sky.width = stageW;

        //设置背景图的高度等于画布的高

        sky.height = stageH;

遮罩层

//创建一个图形

let topMask = new egret.Shape();

        //设置图形填充为黑色,透明度0.5

        topMask.graphics.beginFill(0x000000, 0.5);

      //设置该图形为矩形,相对坐标(0,0),与画布同宽,高度172

        topMask.graphics.drawRect(0, 0, stageW, 172);

        //结束矩形绘制

        topMask.graphics.endFill();

        //设置矩形y坐标为33

        topMask.y = 33;

        //将矩形添加到画布

        this.addChild(topMask);

图标

//创建一个位图

let icon = this.createBitmapByName("egret_icon_png");

        //将位图添加到画布

        this.addChild(icon);

        //设置位图x坐标是26

        icon.x = 26;

        //设置y坐标为33

        icon.y = 33;

分割线

//创建一个图形对象

let line = new egret.Shape();

        //设置线宽为2,颜色为白色

        line.graphics.lineStyle(2, 0xffffff);

        //从相对坐标(0,0)开始绘图

        line.graphics.moveTo(0, 0);

        //向下划117长的线段

        line.graphics.lineTo(0, 117);

        //结束绘图

        line.graphics.endFill();

        //设置图形x坐标为172

        line.x = 172;

        //设置图形y坐标为61

        line.y = 61;

        //将文本对象添加到画布

        this.addChild(line);

文字

//创建一个文本对象

let colorLabel = new egret.TextField();

        //设置文本颜色为白色

        colorLabel.textColor = 0xffffff;

        //设置文本宽度为画布宽度-172

        colorLabel.width = stageW - 172;

        //设置文本对齐方式为 水平居中

        colorLabel.textAlign = "center";

        //设置要显示的文字为 Hello Egret

        colorLabel.text = "Hello Egret";

        //设置字体大小为24

        colorLabel.size = 24;

        //设置文本x坐标为172

        colorLabel.x = 172;

        //设置文本y坐标为80

        colorLabel.y = 80;

        //将文本对象添加到画布

        this.addChild(colorLabel);

这就是新建一个项目后自动生成的代码,通过阅读代码我们发现,Egret 2D 引擎实际上相当于使用 HTML5 的 canvas 在画布上绘图,通过控制位图、文字、图形的移动、显示与隐藏来实现游戏效果。

Canvas 参考:

http://www.runoob.com/html/html5-canvas.html

好了,我们的 Hello World 写完了(虽然我们实际上没有写一行代码), 你会发现这个例子使用的还是 JS 语法,如果我们写个完整的游戏,是不是去搞令人头疼的 JS 呢?(反正我是一想到要写 JS 脑袋就疼)

答案是否定的。

选择 Egret 的原因就是,我能摆脱用 JS 写网页游戏的噩梦,Egret 使用的是 TS,TypeScript。

What?别慌,没坑你,嘿嘿。

四、TypeScript 简介

4.1 TypeScript 是什么

以下是官方比较书面化的一些介绍:

TypeScript 是一种由微软开发的自由和开源的编程语言。它是 JavaScript 的一个超集,而且本质上向这个语言添加了可选的静态类型和基于类的面向对象编程。安德斯 · 海尔斯伯格,C# 的首席架构师,已工作于 TypeScript 的开发。

TypeScript 扩展了 JavaScript 的语法,所以任何现有的 JavaScript 程序可以不加改变的在 TypeScript 下工作。TypeScript 是为大型应用之开发而设计,而编译时它产生 JavaScript 以确保兼容性。

TypeScript 支持为已存在的 JavaScript 库添加类型信息的头文件,扩展了它对于流行的库如 jQuery、MongoDB、Node.js 和 D3.js 的好处。

其实巴拉巴拉这么多,我们看了可能还是不知道这是个什么鬼,可以简单地理解为:

TypeScript 在 JavaScript 的基础上进行了扩展,增加了类型,同时完全兼容 JavaScript 语法。

TypeScript 在 JavaScript 的基础上添加了基于类的面向对象编程。

TypeScript 是 JavaScript + 类型声明

直接放段代码:

    public getRandomArray():Array{

        //定义数字类型的数组arrayA

        let arrayA:Array = [1,2,3,4,5,6,7,8];

        //定义数字类型的数组arrayB

        let arrayB:Array = new Array();

        let index:number;

        //从0到9循环

        for(let i = 0 ; i < 10 ; i ++){

            //生成一个比arrayA长度小的随机数,作为下标

            index = Math.floor(Math.random() * arrayA.length);

            //取出arrayA指定下标的值放入arrayB

            arrayB.push(arrayA[index]);

        } 

        console.log("已随机生成一个数组",arrayB);

        return arrayB;

    }

这段代码实现了一个方法,用来生成一个随机的数组:

先定义了一个数组 arrayA,赋值为 1 到 8;

又定义了一个空的数组 arrayB;

循环 10 次,每次都随机取 arrayA 中的一个元素,放入 arrayB;

循环完毕后,控制台输出 arrayB,并将其作为方法的返回值返回。

通过代码的介绍,你会发现,这里面除了方法和变量的定义时后面跟了一个:Array、:number外,完全就是一段标准的 JavaScript 代码。

这就是 TypeScript 的第一个特点:

在 JavaScript 的基础上进行了扩展,增加了类型,同时完全兼容 JavaScript 语法。

JavaScript + 面向对象

我们再来看下面这段代码:

//用户实体类

class User {

    //姓名 字符串类型

    private name:string;

    //年龄 数字类型

    private age:number;

    //带参数的构造方法 传入参数为 姓名,年龄

    public constructor(name:string,age:number) {

        this.name = name;

        this.age = age;

    }

    //获取姓名

    public getName():string{

        return this.name;

    }

    //获取年龄

    public getAge():number{

        return this.age;

    }

}

这段代码是用 TypeScript 声明的一个实体类,Java 程序员们可能会惊叹一下:我去!你在写 Java 吗!?

这段代码定义了一个 User 对象,它有两个属性:name、age,有个构造方法,除了类型声明后置了以外,简直和 Java 中实体类的声明一模一样。

当然,它们只是长得像,用起来还是使用 JavaScript 的语法格式(TypeScript 真的是要让 Java 和 JavaScript 傻傻分不清楚了呢)。

这就是 TypeScript 的第二个特点:

在 JavaScript 的基础上添加了基于类的面向对象编程。

4.2 为什么要用 TypeScript

经过上面的介绍,我们知道了 TypeScript 其实就是增强版的 JavaScript,那么随着 jQuery、vue、node.js 等框架的兴起,我们很多的前端编程使用 JavaScript 就足够了,为什么还要用 TypeScript 呢?

我的回答是:

为了掌控我们自己写的代码。

看到我的回答,你可能感到诧异:我自己写的代码我还掌控不了吗?答案是肯定的。

JavaScript 的弱类型

由于 JS 是一门弱类型语言,所有变量和方法的返回值都可以是任意类型的。你定义好了一个变量或者方法,在你使用时,还必须返回来看看它究竟是个啥。

因为它是弱类型的,编辑器没法帮你做代码检查(比如,即使你要调用变量不存在的属性编辑器也不会提示错误,因为编辑器也不知道它是什么类型的、有什么属性),必须你亲力亲为,会出现你在总是在写代码时一片太平盛世的景色,一运行,各种能想到的想不到的错误。

JavaScript 的不可控性

即使你严格检查你的代码,保证所有变量和方法用的都对,一切逻辑都正确,也不一定能得到你想要的结果,比如那个经典的 0.2+0.4 的问题:

    console.log(0.2 + 0.4 === 0.6);

    //运行结果过为:false  0.2 + 0.4 = 0.6000000000000001

这个案例在这里就不详细解释了,有兴趣的可以网上查一下,有很多经典的 JS 问题。

一句话:

JavaScript 有太多不可控性。

TypeScript 有益身心健康

因为 TS 加入了类型的概念,在写代码时,编辑器就能对你引用的方法和变量做类型检查,在 Egret Wing 中有很好的代码提示功能和代码检查功能,避免这些编码问题在运行时才暴露,同时也使你的代码逻辑清晰,易读易维护。

开发时满屏都是清晰明了的代码,想想都兴奋,心情好了,精神足了,气色红润了,不觉间人生也充满了乐趣。

而面向对象的编程,更让人激动,如果你原本就是使用面向对象语言的开发者,那么你会第一次感觉到写 JS(你没看错,TS 语言最终也是被编译成 JS 运行在浏览器的)也使这么爽的事情。

TS 让你像写 Java 一样写 JS,是不是很神奇。当你要开发一款游戏时,代码量巨大,而且不容许有任何不确定的错误产生,TS 是代替 JS 的首选,没有之一。

4.3 TypeScript 的学习成本有多大

每种编程语言都有很多共通的地方,当你了解 JS,那么你需要补充一些面向对象的知识。

当然,我们说过 TS 能完全兼容 JS,你直接用 JS 语法写一样可行,但这样你就感受不到 TS 的灵魂所在(小伙子,这样的代码是没有灵魂滴)。

当你了解面向对象语言,又了解 JS 时,恭喜你,看一眼文档,直接上手就行。

TypeScript 推荐文档链接:

https://www.tslang.cn/docs/handbook/basic-types.html

其实在这里我很想给大家详细讲一下 TS,篇幅!篇幅!篇幅!重要的话说三遍。推荐大家看看 TS 的文档,相信会给你带来惊喜。下面,我们就讲一下如何用 TS 实现斗地主的核心逻辑,通过代码让大家熟悉 TS。

五、斗地主游戏规则及核心逻辑

说明:这节用到的代码都在项目的 /src/utils 文件夹下,什么?项目在哪?请翻到页面顶部从头再看一遍。

不想翻?好吧,再贴一下:

https://github.com/tianlanlandelan/poker

Git 克隆下来,直接用 Egret Wing 打开就行,够意思吧。

5.1 牌面说明

54 张牌,2~10、A、J、Q、K 各 4 张(红桃、方块、黑桃、梅花),小王、大王各 1 张。

从小到大排序依次为:3、4、5、6、7、8、9、10、J、Q、K、A、2、小王、大王。

5.2 牌型说明

编号牌型最大张数最小张数牌型规则说明牌型大小说明

1王炸22大小王王炸最大

2炸弹444 张相同牌炸弹能压除王炸外的任意牌,炸弹间比较时,牌面大的大

3单张11一张任意牌只能压相同牌型,牌面大的大

4对子22两张相同牌同上

5三张22三张相同牌同上

6三带一44三张相同牌 + 一张任意牌同上

7三带一对55三张相同牌 + 两张任意牌同上

8顺子512至少连续 5 个单张(最小为 3,最大为 A)同上

9连对620至少连续 3 个对子(最小为 3,最大为 A)同上

10飞机618至少连续 2 个三张(最小为 3,最大为 A)同上

11飞机带单张820至少连续 2 个三张(最小为 3,最大为 A)+ 与三张相同数量的单张同上

12飞机带对子1020至少连续 2 个三张(最小为 3,最大为 A)+ 与三张相同数量的对子同上

13四带二664 张相同牌 + 2 个单张同上

14四带两对884 张相同牌 + 1 个对子同上

5.3 生成牌逻辑

对局时首先要有一副牌,一副洗好的牌,为了实现这个目标,我们需要定义一个牌的对象,并想办法生成一副牌出来。

定义 poker 对象–Poker.ts

class Poker {

      public constructor(id:number,orderValue:number) {

            this.id = id;

            this.orderValue = orderValue;

        }

        /**

        * 牌面ID

        */

        private id:number;

        /**

        * 牌大小排序值 0-14  数值越小,表示牌越大

        */

        private orderValue:number;

这段代码定义一个对象,包含两个属性:ID(牌面 ID,这张牌是哪张牌)、orderValue(牌大小排序值,这张牌有多大)。

看到这里你可能会问:你这不能体现可视化啊,这张牌是红桃 3 还是梅花 A 呢?这就涉及到这张牌的图片属性(image)了,这张牌的 image 可以和 ID 一样放在 Poker 对象里作为一个属性存在。

在这里我们使用另一种方式:将牌的图片文件以 ID 命名(如 1.png、2.png),这样在使用时就可以直接使用 ID 来指定它的 image 了。

定义牌面 ID——PukerUtils.ts

牌面 ID 作为一张牌的唯一标示,可以和牌面的图片绑定。

    private static pukerIds:Array = [

            1,2,3,4,//A

            5,6,7,8,//2

            9,10,11,12,//3

            13,14,15,16,//4

            17,18,19,20,//5

            21,22,23,24,//6

            25,26,27,28,//7

            29,30,31,32,//8

            33,34,35,36,//9

            37,38,39,40,//10

            41,42,43,44,//J

            45,46,47,48,//Q

            49,50,51,52,//K

            53,54];//King

这段代码定义一副牌的 ID, 我们以 A、2、3、4、5、6、7、8、9、10、J、Q、K 小王、大王的顺序依次定义了 54 张牌的 ID。

定义牌的排序值 PukerUtils.ts

    private static pukerSortValues:Array =  [

            12,12,12,12,//A

            13,13,13,13,//2

            1,1,1,1,//3

            2,2,2,2,//4

            3,3,3,3,//5

            4,4,4,4,//6

            5,5,5,5,//7

            6,6,6,6,//8

            7,7,7,7,//9

            8,8,8,8,//10

            9,9,9,9,//J

            10,10,10,10,//Q

            11,11,11,11,//K

            14,15//King

        ];

这段代码和上段代码相似,定义一副牌的大小排序值。定义的顺序要和 pukerIds 的顺序相同,1~15 的数字分别对应牌 3、4、5、6、7、8、9、10、J、Q、K、A、2、小王、大王的大小值。

随机生成一副扑克 PukerUtils.ts

    public static getRandomPokers():Array{

        //定义Poker数组,这是最终要生成的一副扑克牌

        let pokers:Array =new Array();

        //选中的牌的下标

        let index:number;

        //将pukerIds重新复制一份作为选牌期间要处理的数组

        let newArray:Array = this.pukerIds.slice();

        //选中的牌的ID

        let pukerIndex:number;

        //选中的牌的ID组成的数组

        let array:Array = [];

        //遍历扑克牌ID数组

        for(let i = 0 ; i < this.pukerIds.length ; i ++){

              //随机生成一个小于要处理的数组长度的整数,作为选中的牌的下标

              index = Math.floor(Math.random() * newArray.length);

              //取数选中的ID

              pukerIndex = newArray[index];

              //将选中的ID放入数组

              array.push(pukerIndex);

              //将选剩下的牌重新组成一个数组

              newArray = ArrayUtils.slice(newArray,0,index).concat(ArrayUtils.slice(newArray,index + 1,newArray.length));

        } 

        //遍历选中的扑克ID

        for(let j = 0 ; j < array.length ; j ++){

              //从pukerIds和pukerSortValues中取出对应的属性值组成一幅扑克牌

              let poker:Poker = new Poker(this.pukerIds[array[j] - 1],this.pukerSortValues[array[j] -1])

              pokers.push(poker);

        }

        console.log("已随机生成一副牌",pokers.toString());

        return pokers;

    }     

前几段代码定义好了 Poker 对象以及一副牌的 ID 和大小,通过这段代码我们实现了随机生成一副扑克牌的功能,每一行都有说明,通过这步操作,我们能得到一幅完整的扑克牌,接下来我们只需要将这些牌发给每个玩家就可以了。

5.4 牌型判断逻辑

对局中,当对方或我们出牌时,首先得知道选的几张牌是否是一手牌,不能乱出,这就涉及到了牌型的判断,在这里我们着重介绍下牌型的一些判断逻辑:

定义牌型对象–PukerType.ts

    class PukerType {

    /**

    * 牌型

    */

    private type:string;

    /**

    * 牌型大小排序值

    */

    private sort:number;

    … …

    }

该对象包含两个属性:type(牌型:这是手什么牌)、sort(牌型大小:这手牌有多大)。

定义牌型 PukerTypeUtils.ts

    public static typeBoom:string          = "typeBoom";

    public static typeKingBoom:string      = "typeKingBoom";

    public static typeSingle:string        = "typeSingle";

    public static typePair:string          = "typePair";

    public static typeThree:string          = "typeThree";

    public static typeThreeSingle:string    = "typeThreeSingle";

    public static typeThreePair:string      = "typeThreePair";

    public static typeStraight:string      = "typeStraight";

    public static typeStraightPairs:string  = "typeStraightPairs";

    public static typePlane:string          = "typePlane";

    public static typePlane2Single:string  = "typePlane2Single";

    public static typePlane2Pairs:string    = "typePlane2Pairs";

    public static typeFour2Single:string    = "typeFour2Single";

    public static typeFour2Pairs:string    = "typeFour2Pairs";

在这里统一定义了 14 种牌型的名称。

判断牌型 PukerTypeUtils.ts

每种牌型有不同的判断逻辑,在这里列举其中一个牌型做一说明,其他所有牌型判断均在 PukerTypeUtils.ts 文件里。

所有牌型的判断思路都是:

先将一手牌按牌大小进行排序;

然后依照不同牌型的判断规则判断该手牌是否符合规则;

如果不符合规则,返回 -1;如果符合规则,则返回该手牌的大小。

    /**

    * 判断一手牌是否是顺子

    * 如果不是,返回-1; 如果是,返回该牌型的大小排序值

    */

    public static isStraight(array:Array):number{

        if(array.length < 5 || array.length >12) return -1;//少于5张或多余12张,牌形不正确

        if(array[0].getOrderValue() > PukerUtils.AValue) return -1;//如果最大的牌大于A,牌形不正确

        for(let i = 0 ; i < array.length - 1; i ++){

              if(array[i].getOrderValue()  != array[i+1].getOrderValue() + 1) return -1;//后一张牌不比前一张牌递次小1,牌形不正确

        }

        return array[0].getOrderValue();

    }

这段代码实现的是对一手牌是不是顺子进行判断:

检查张数。顺子要求至少 5 张牌,最大呢,天顺:3 到 A,数一数,是 12 张。所以当这手牌少于 5 张或多于 12 张时就不对。

检查这手牌最大的牌。顺子的每张牌中最大的是 A,所以,当这手牌最大的牌比 A 还要大时,就不用往下看了,直接 Pass 掉。

检查这手牌是不是连续的,如果是连续的,恭喜你,符合顺子的判断规则,返回这手牌中最大的那一张作为该手牌的大小值。

5.5 牌型比较逻辑

牌有了,也能知道一手牌是什么牌了,我们列举几种情况:

你出一个 5 张的顺子,我也出一个 5 张的顺子,怎么判断我的顺子能不能压住你的顺子;

你出一个顺子,我出一个连对,行不行;

你出一个顺子,我出一个炸弹,行不行;

你出一个 5 张的顺子,我出一个 7 张的顺子,行不行。

经过以上思考,在牌型比较的过程中,我们把所有牌型分为两类:能压其他牌型的(炸弹和王炸)、不能压其他牌型的(其他所有牌型)。

这样我们的思路就有了:

第一种牌型中,我们具体情况具体判断,在第二种牌型中,我们首先要判断这两手牌是不是同一种牌型。

牌型比较的逻辑都在 PukerCompareUtils.ts 文件中,具体代码如下:

    /**

        * 比较两手牌的大小

        * a b ,当a大于b时返回true

        * 王炸通吃

        * 炸弹仅次于王炸

        * 其他牌必须牌型相等才能比较

        */

      public static comparePukers(a:Array,b:Array):boolean{

          let aSort:Array = PukerUtils.sortDescPokers(a);

          let bSort:Array = PukerUtils.sortDescPokers(b);

          if(aSort.length < 1 || bSort.length < 1) return false;//空

          if(PukerTypeUtils.isKingBoom(bSort) != -1) return false;//b是王炸

          if(PukerTypeUtils.isKingBoom(aSort) != -1) return true;//a是王炸

          if(PukerTypeUtils.isBoom(bSort) != -1){//b是炸弹

              if(PukerTypeUtils.isBoom(aSort) != -1) return this.compareOne(aSort[0],bSort[0]);//a 也是炸弹

              return false;//a 不是炸弹

          }

          if(PukerTypeUtils.isBoom(aSort) != -1) return true;//a 是炸弹

          if(a.length != b.length) return false;//已经排除了炸弹的可能,长度不相等,不能比较

          let aType:PukerType = PukerTypeUtils.getType(aSort);

          let bType:PukerType = PukerTypeUtils.getType(bSort);

          if(aType == null || bType == null || aType.getType() != bType.getType()) return false;//牌型3不相等

          return aType.getSort() > bType.getSort();

      }

      /**

        * 比较单牌的大小

        */

      private static compareOne(a:Poker,b:Poker):boolean{

          if(a.getOrderValue() > b.getOrderValue()){

              return true;

          }else{

              return false;

          }

      }

5.6 自动选牌

单机游戏的系统出牌和联机游戏的托管功能都要用到系统自动出牌的机制,它的核心就是,从你手里的一堆牌中挑出合适的牌型压住上家出的牌,在这里我们简单介绍一下:

思路是:

先判断上家的牌型,要是上家是王炸,那想都别想了,无解;

如果不是王炸,那就还有机会,先判断对方出的啥牌型,然后从自己的牌中扒拉扒拉看有没有能压住这些牌的牌;

如果没有,就不出;如果有,就弄他吖的。

        /**

            * TODO

            * 从一手牌中判断有没有比指定牌大的牌

            * 这个方法是单机游戏的核心

            * 需要持续优化

            * aHandPuker 一手牌

            * b 指定的牌形

            */

          public static seekRight(aHandPuker:Array,pukerType:Array):Array{

              let myPuker:Array = PukerUtils.sortDescPokers(aHandPuker);

              let puker:Array = PukerUtils.sortDescPokers(pukerType);

              console.log("seekRight",myPuker,puker);

              let bType = PukerTypeUtils.getType(pukerType);

              let mask:number = 0;

              let rightPuker:Array = null;

              if(bType.getType() === PukerTypeUtils.typeKingBoom)            rightPuker =  null;

              else if(bType.getType() === PukerTypeUtils.typeSingle)        rightPuker =  this.seekSingle(myPuker,bType.getSort());

              else if(bType.getType() === PukerTypeUtils.typePair)          rightPuker =  this.seekPairs(myPuker,bType.getSort());

              else if(bType.getType() === PukerTypeUtils.typeThree)          rightPuker =  this.seekThree(myPuker,bType.getSort());

              else if(bType.getType() === PukerTypeUtils.typeThreeSingle)    rightPuker =  this.seekThreeSingle(myPuker,bType.getSort());

              else if(bType.getType() === PukerTypeUtils.typeThreePair)      rightPuker =  this.seekThreePair(myPuker,bType.getSort());

              else if(bType.getType() === PukerTypeUtils.typeStraight)      rightPuker =  this.seekStraight(myPuker,bType.getSort(),puker.length);

              else if(bType.getType() === PukerTypeUtils.typeStraightPairs) rightPuker =  this.seekStraightPairs(myPuker,bType.getSort(),puker.length);

              else if(bType.getType() === PukerTypeUtils.typePlane)          rightPuker =  this.seekPlane(myPuker,bType.getSort());

              else if(bType.getType() === PukerTypeUtils.typePlane2Single)  rightPuker =  this.seekPlane2Single(myPuker,bType.getSort());

              else if(bType.getType() === PukerTypeUtils.typePlane2Pairs)    rightPuker =  this.seekPlane2Pairs(myPuker,bType.getSort());

              else if(bType.getType() === PukerTypeUtils.typeFour2Single)    rightPuker =  this.seekFour2Single(myPuker,bType.getSort());

              else if(bType.getType() === PukerTypeUtils.typeFour2Pairs)        rightPuker =  this.seekFour2Pairs(myPuker,bType.getSort());

              //TODO 当玩家没有同类型的可出的牌时,在适当的时机判断是否有炸弹、王炸可以出

              //选好可出的牌型后,从玩家的牌中按照牌形挑选牌面

              if(rightPuker != null && rightPuker.length > 0){

                  return rightPuker

              }else{

                  return [];

              }

          }       

具体的实现逻辑都在 PukerSeekUtils.ts 文件中,这个逻辑需要持续优化,比如什么时候该出、什么时候不该出等,要做得智能一点,还需要很长的路要走。

写完了游戏逻辑,我们来画界面。

六、斗地主游戏(单机版)界面开发

6.1 项目结构说明

让我们打开项目,看文件列表

项目中重要的文件夹是 resource(资源文件夹)和 src(源码文件夹),其他都是新建项目后生成的文件和文件夹,不要随意改动。

resource 说明

resource 文件夹存放项目中用到的多媒体文件、图片等资源,项目中通过 default.res.json

对这些资源进行调度。default.res.json 文件一般不需要手动维护,当你往这个文件夹下添加资源文件时,系统会提示要不要更新

default.res.json,那么什么是一般情况呢?

解释一下:当你把项目打包发布成 iOS、Android 或者网页应用时,直接将资源文件打包在项目里是很好的选择方式,这时你的

default.res.json

使用系统维护的即可。如果你要打包成微信小程序或者其他限制包大小的应用,资源文件也打包进去肯定会超限,这时候你就可以将本地资源上传到云存储服务器,然后替换

default.res.json 里的资源地址,这就要手动维护了。

src 说明

介绍 src 目录结构之前,先聊聊我们开发游戏界面的思路:

游戏场景中我们会用到很多图片、文字等部件,不能说每用到一个图片就啪啪啪敲上一段代码吧,重用很关键,这就涉及到了 TypeScript 的特性之一:面向对象编程。我们要把共同的一些东西抽出来,封装成对象,使用时一行代码调用就 OK 了。

该游戏中涉及到以下几种对象,分别放在不同的目录中:

object:基本部件,例如按钮、头像、菜单、提示文字、扑克牌等。这些对象的特征是会重复用到的不可分割的小零件,只负责展示,没有复杂的操作逻辑。

container:容器,例如出牌区、头像展示区、计数器、定时器、按钮组等。这些对象是把一些基本部件组合在一起,加上一些控制逻辑形成的一个模块。

scene:场景,例如开始游戏界面、游戏大厅、单机游戏场景、联机游戏场景等。这些对象是把不同的容器组合在一起,实现整个界面的展示效果。

简单地说就是:对象组成容器,容器组成场景,不同场景的切换实现游戏效果。

6.2 开始游戏界面

先分析下怎么实现:

如果这是你拿到的设计图,分析下我们怎么做:

这个界面由背景图、LOGO、按钮组成

对照下资源文件,哎?LOGO 是在背景图上画着的,更简单了,就一个背景图和按钮了

背景图直接加载,按钮放中间,还要加上点击动作

… …

这样一步步分析下,是不是发现很简单?代码见:/src/scene/StartScene.ts,这个场景写好后,要作为游戏的第一个场景,工作还没有结束,我们要在 Main.ts 中调用它:

    /**

    * 创建游戏场景

    * Create a game scene

    */

    private createGameScene() {

        //获取配置中的画布大小 我更喜欢称之为舞台,展现我们代码风姿的舞台

        let layout = RES.getRes("layout_json").layout;

        //设置横屏效果

        this.stage.orientation = egret.OrientationMode.LANDSCAPE;

        //设置舞台大小

        this.stage.setContentSize(layout.stageWidth,layout.stageHeight);

        console.log("舞台大小:",this.stage.stageWidth,"*",this.stage.stageHeight);

        //调用游戏开始界面

        let startScene:StartScene = new StartScene(this.user);

        //给它取个名字,方便以后操作

        startScene.name = "startScene";

        //加载游戏开始界面

        this.addChild(startScene);

    }

注意了:最好给你调用的组件都取个名字,方便你操作它,总不能操作的时候喊那个谁谁谁去干嘛干嘛吧(虽然没名字你也能找到它,但何苦坑自己呢)

点击开始游戏时,加载游戏大厅界面,移除当前界面,后面每个界面的切换也都这么干就行了:

    /**

    * 点击开始游戏按钮,移除开始界面,进入游戏大厅

    */

    private startGame(evt:egret.TouchEvent):void{

        let gameHall:GameHall = new GameHall(this.user);

        gameHall.name = "gameHall";

        this.parent.addChild(gameHall);

        this.parent.removeChild(this);

    }

6.3 游戏大厅界面

和上面一样,先分析效果图:

5 部分:上、下、左、右、中原地带

左、右、下三部分有些神似

中原地带貌似有些重复的东西

上边好像有点丑

… …

分析完毕,我们可以把相似的东西抽成基本对象,把这 5 部分封装成容器,这个界面就好像没那么复杂了,代码见:/src/scene/GameHall.ts

6.4 游戏界面

分析:

(⊙o⊙) 好像东西有点多

5 部分,上、下、左、右、中间

上边是返回按钮、底牌,好像还可以加点别的什么的

下边是自己的牌

左边是上家头像、上家牌数、自己头像

右边是下家头像、下家排数

中间:上家不出牌提示文字、下家出的牌、出牌按钮,好像还有自己出的牌没展示的吧

… …

经过分析,确实有点复杂,我们还是按照那一套:抽基本组件、封装容器、布置界面就好,代码见:/src/scene/gamemodel/StandaloneModel.ts

七、游戏打包发布

在 Egret Wing 右下角点击“发布项目”,会弹出发布界面,如图:

7.1 打包成 HTML5

发布成 HTML5 应用时,输入版本号(默认版本号是当前时间),找到打包后的文件,用浏览器打开 index.html 即可,可以放到你的 Web 服务器直接访问。效果图就不放了,前面咱们一直看的就是 HTML5 版本的效果。

7.2 打包成 iOS

打包成 iOS 应用,需要输入应用名称和包名,然后会在项目的同级目录下生成一个 iOS 项目,嘿嘿,这个项目需要使用 Xcode 编译运行(关于 Xcode,首先,你要有一台 Mac,然后要对 iOS 软件开发有一些了解)。

运行效果如下(我使用的是大华为,用 Xcode 自带的模拟器运行一下看看效果就好)。

7.3 打包成 Android

同样,会在项目的同级目录下生成一个安卓项目,使用 Android Studio 编译就可以运行(关于 Android Studio 的使用,这里就不做介绍了,只需要知道所有安卓手机上的软件都是用这家伙写出来的就行了)。

安装到我大华为上是这样的效果:

7.4 打包成微信小程序

打包微信小程序,需要你提前在微信公众平台上注册个小程序,将生成的 AppID 填入,然后设置项目名称,生成微信小程序项目。

然后使用微信小程序开发工具编译发布即可。

官方下载地址:

https://developers.weixin.qq.com/miniprogram/dev/devtools/download.html)

微信小程序预览效果图:

好了,感谢大家耐心看完我的第一篇 Chat,有什么问题都可以在读者圈讨论,我会及时回答给大家回应,再次感谢大家的支持_

附上项目源码(喜欢请点颗星星):

https://github.com/tianlanlandelan/poker

你可能感兴趣的:(Egret 开发斗地主游戏全程讲解)