egret绘制环形图

之前分享过通过highcarts、echarts绘制饼图(环形图)。前段时间的H5游戏项目中,也通过egret绘制过环形图,下面就分享一下,如何通过egret绘制环形图。

效果图

先看一下效果图
egret绘制环形图

分析

这种绘制方法肯定不是只用一次,所以我们需要将这种方法封装到函数中,然后我们通过传入几个参数就可以绘制出环形图。
function drawCircle(obj, data) {}

先来分析一下这个图都有几部分组成。
首先我们可以看出环形图的颜色是分为深、浅两种颜色,中间是一个icon,下边部分是图例。

环形图主体部分

因为不是使用highcharts、echarts这种插件,所以所有的东西都需要我们自己去写,不能使用一些原有的配置。
在开始绘制之前,我们需要先算出来这个环形图要绘制成几部分,也就是传入的数据的length是多少,然后我们还需要算出绘制的起始和结束角度startDegendDeg

const length: number = data.length;
let startDeg = 0;
let endDeg = 0;
// 还有一些其他的,如要绘制的图形的宽、高等属性,根据需要自行设置即可    

在基本的配置完成后,我们需要先创建一个放置环形图及其图例的区域sprite,下面的objWidthwidth为前一步的自行设置,obj为传入的对象,后面就不在单独说明了。如果有不懂的地方,最后也有所有代码的汇总。

let percentBg: egret.Sprite = new egret.Sprite();
percentBg.width = width;
percentBg.height = height;
percentBg.x = (objWidth - width) / 2;
percentBg.y = 95;
obj.addChild(percentBg);

环形图的区域跟上面一样,也是创建一个sprite,然后放置到percentBg上面。

通过for循环进行外层浅色区域环形图的绘制,这里面同时也会将startDegendDeg分别赋值。

for(let i = 0; i < length; i++) {
    endDeg = (data[i].industryPercent).toFixed(2) * 3.6 + startDeg;
    circleBg.graphics.beginFill(GameConfig.lightColor[i]);
    circleBg.graphics.moveTo(width / 2, width / 2);
    circleBg.graphics.lineTo(width, width / 2);
    circleBg.graphics.drawArc(width / 2, width / 2, width / 2.3, (startDeg + 2) * Math.PI / 180, endDeg * Math.PI / 180);
    circleBg.graphics.lineTo(width / 2, width / 2);
    circleBg.graphics.endFill();
    startDeg = endDeg;
}

先通过传入数据中的配比情况,算出endDeg结束角度,然后通过egret的绘图API绘制。
内层深色区域绘制方法同上,只需要将半径变小即可。
绘制完成后,展现在我们面前的是下面的样子
只绘制了深色跟浅色区域
这个跟我们的需求还相差很远,这里就需要我们在以同样的圆心,绘制上一个白色的圆形来遮挡住中间的部分。

circleBg.graphics.beginFill(GameConfig.TextColors.white); //环形图中间白色部分
circleBg.graphics.moveTo(width / 2, width / 2);
circleBg.graphics.lineTo(width, width / 2);
circleBg.graphics.drawArc(width / 2, width / 2, width / 3.5, 0, 360 * Math.PI / 180);
circleBg.graphics.lineTo(width / 2, width / 2);
circleBg.graphics.endFill();

并且再以同样的圆心,绘制上一个icon。icon因为是固定的,所以建议使用图片Bitmap,因为绘制会比直接使用图片更加消耗性能。

let centerIcon = ZpyGameUtils.createBitmapByName("card_dark_icon_png"); //中心icon
centerIcon.x = width / 2 - centerIcon.width / 2 * 0.6;
centerIcon.y = width / 2 - centerIcon.height / 2 * 0.6;
centerIcon.scaleX = 0.6;
centerIcon.scaleY = 0.6;
circleBg.addChild(centerIcon);

环形图图例

绘制完主体部分,我们再来绘制一下图例部分。这个相对于主体环形图来说,就很简单了。icon是图例前面的小色块,label是图例文字部分。

let labelBg: egret.Sprite = new egret.Sprite();
labelBg.width = width - 40;
labelBg.x = 20;
labelBg.y = width;
for (let i = 0, len = data.length; i < len; i++) {
    let icon: egret.Shape = new egret.Shape();
    icon.graphics.beginFill(GameConfig.darkColor[i]);
    icon.graphics.drawRect(0, i * lineHeight, 20, 20);
    icon.graphics.endFill();
    labelBg.addChild(icon);

    let label: egret.TextField = ZpyGameUtils.createText(data[i].industryName + '  ' + (data[i].industryPercent).toFixed(2) + '%', GameConfig.TextColors.black, 20);
    label.x = 30;
    label.y = lineHeight * i;
    labelBg.addChild(label);
}

可滑动

因为环形图的数据可能很多,图例一页可能会显示不过来,所以我们还需要一个可以上下滑动的组件来扩展。

let percentTotalBg: egret.Sprite = new egret.Sprite();
percentTotalBg.width = width;

percentTotalBg.addChild(labelBg);
percentTotalBg.height = width + length * lineHeight;

var scrollView: egret.ScrollView = new egret.ScrollView();
scrollView.setContent(percentTotalBg);
scrollView.width = width;
scrollView.height = height;
percentBg.addChild(scrollView);

前面步骤中生成的所元素最终会放置到percentTotalBg上,然后声明一个滑动区域scrollView,通过setContent方法将所有元素的区域设置为可滑动的区域。

最终代码

function drawCircle(obj, data) {
    const objWidth: number = obj.width;
    const objHeight: number = obj.height;
    const width: number = obj.width * 0.8; //新创建的背景的宽度,宽高相同
    const height: number = obj.height * 0.8; //新创建的背景的高度
    const lineHeight: number = 35;
    const length: number = data.length; //数据长度
    let startDeg = 0; //扇形起始角度
    let endDeg = 0; //结束角度

    let percentBg: egret.Sprite = new egret.Sprite();
    percentBg.width = width;
    percentBg.height = height;
    percentBg.x = (objWidth - width) / 2;
    percentBg.y = 95;
    obj.addChild(percentBg);
    let percentTotalBg: egret.Sprite = new egret.Sprite();
    percentTotalBg.width = width;
    let circleBg: egret.Sprite = new egret.Sprite();
    percentTotalBg.addChild(circleBg);
    circleBg.width = width;
    circleBg.height = width;
    circleBg.y = 0;

    for (let i = 0; i < length; i++) { //环形图浅色部分
        endDeg = (data[i].industryPercent).toFixed(2) * 3.6 + startDeg;
        circleBg.graphics.beginFill(GameConfig.lightColor[i]);
        circleBg.graphics.moveTo(width / 2, width / 2);
        circleBg.graphics.lineTo(width, width / 2);
        circleBg.graphics.drawArc(width / 2, width / 2, width / 2.3, (startDeg + 2) * Math.PI / 180, endDeg * Math.PI / 180);
        circleBg.graphics.lineTo(width / 2, width / 2);
        circleBg.graphics.endFill();
        startDeg = endDeg;
    }
    for (let i = 0; i < length; i++) { //环形图深色部分
        endDeg = (data[i].industryPercent).toFixed(2) * 3.6 + startDeg;
        circleBg.graphics.beginFill(GameConfig.darkColor[i]);
        circleBg.graphics.moveTo(width / 2, width / 2);
        circleBg.graphics.lineTo(width, width / 2);
        circleBg.graphics.drawArc(width / 2, width / 2, width / 2.6, (startDeg + 2) * Math.PI / 180, endDeg * Math.PI / 180);
        circleBg.graphics.lineTo(width / 2, width / 2);
        circleBg.graphics.endFill();
        startDeg = endDeg;
    }
    circleBg.graphics.beginFill(GameConfig.TextColors.white); //环形图中间白色部分
    circleBg.graphics.moveTo(width / 2, width / 2);
    circleBg.graphics.lineTo(width, width / 2);
    circleBg.graphics.drawArc(width / 2, width / 2, width / 3.5, 0, 360 * Math.PI / 180);
    circleBg.graphics.lineTo(width / 2, width / 2);
    circleBg.graphics.endFill();

    let centerIcon = ZpyGameUtils.createBitmapByName("card_dark_icon_png"); //中心icon
    centerIcon.x = width / 2 - centerIcon.width / 2 * 0.6;
    centerIcon.y = width / 2 - centerIcon.height / 2 * 0.6;
    centerIcon.scaleX = 0.6;
    centerIcon.scaleY = 0.6;
    circleBg.addChild(centerIcon);
    percentTotalBg.addChild(circleBg);

    let labelBg: egret.Sprite = new egret.Sprite();
    labelBg.width = width - 40;
    labelBg.x = 20;
    labelBg.y = width;
    for (let i = 0, len = data.length; i < len; i++) {
        let icon: egret.Shape = new egret.Shape();
        icon.graphics.beginFill(GameConfig.darkColor[i]);
        icon.graphics.drawRect(0, i * lineHeight, 20, 20);
        icon.graphics.endFill();
        labelBg.addChild(icon);

        let label: egret.TextField = ZpyGameUtils.createText(data[i].industryName + '  ' + (data[i].industryPercent).toFixed(2) + '%', GameConfig.TextColors.black, 20);
        label.x = 30;
        label.y = lineHeight * i;
        labelBg.addChild(label);
    }
    percentTotalBg.addChild(labelBg);
    percentTotalBg.height = width + length * lineHeight;

    var scrollView: egret.ScrollView = new egret.ScrollView();
    scrollView.setContent(percentTotalBg);
    scrollView.width = width;
    scrollView.height = height;
    percentBg.addChild(scrollView);
}

如果是在namespace中,请不要忘记通过export方法使其可以在外部访问。

egret游戏引擎入门(七)

今天分享几个封装的egret方法。
之前也调研过createjs,感觉egret的绘画API跟createjs的相似,或者说,这一类都是大同小异的。虽然这些方法相较原声的canvas简单一点,但是感觉还是有些繁杂,可以将这些方法封装一下进行调用。
1. 根据资源名称创建图片Bitmap
当我们新建一个egret项目的时候,就会在Main.ts中发现这个方法。这个方法直接传入一个在default.res.json中定义好的图片资源名称,就可以返回一个egret.Bitmap对象。其实这也就是在提示我们将文字、绘图等方法进行封装,使用起来会更加高效。

function createBitmapByName(name: string): egret.Bitmap {
    const resource: egret.Bitmap = new egret.Bitmap();
    const texture: egret.Texture = RES.getRes(name);
    resource.texture = texture;
    return resource;
}

  1. 创建文本类TextFiled
/**
 * 创建文本类TextFiled
 *
 * @param {string} text 要显示的文字
 * @param {number} color 文字的颜色
 * @param {number} [size=20] 文字的大小
 * @param {number} [x=0] 文字的x坐标
 * @param {number} [y=0] 文字的y坐标
 */
function createText(text: string, color: number, size: number = 20, x: number = 0, y: number = 0): egret.TextFiled {
    const resource: egret.TextFiled = new egret.TextFiled();
    resource.text = text;
    resource.color = textColor;
    resource.size = size;
    resource.x = x;
    resource.y = y;
}
  1. 创建带有矢量绘制功能的显示容器sprite
    个人感觉这个方法使用的相当频繁,因为我是将sprite作为一个组件去使用的。打个比方来说,我要创建一张卡牌,那么我先创建一个sprite并将其添加到场景上,然后再创建一些卡牌的图案或者文字,添加到sprite上。每次要移动卡牌的时候,只需要操作sprite就好了,其内的文本类后者矢量绘图类都会随之移动。
    值得一提的是,如果是创建一个类继承自egret.Sprite,并且这个类有点击等监听事件,如果无法触发,请设置其touchEnable属性变为true;如果有些地方触发有效果有些地方无效果,就要创建一个同等大小的shape,并且将其的alpha设置为0,放置在sprite上即可。
/**
 * 创建带有矢量绘制功能的显示容器sprite
 *
 * @param {number} width 显示容器的宽
 * @param {number} height 显示容器的高
 * @param {number} [x=0] 显示容器的x坐标
 * @param {number} [y=0] 显示容器的y坐标
 * @returns {egret.Sprite} 返回显示容器对象
 */
function createSprite(width: number, height: number, x: number = 0, y: number = 0): egret.Sprite {
    const resource: egret.Sprite = new egret.Sprite();
    resource.width = width;
    resource.height = height;
    resource.x = x;
    resource.y = y;
    return resource;
}
  1. 创建矢量图Shape
    我在最开始使用的时候,将x跟y直接用在drawRect中,但是通过egret wing自带的调试工具调试后发现,他的实际位置显示的有问题。所以现在在使用drawRect的时候,将x跟y都设置为0,再通过刚刚传入的x跟y坐标设置这个矢量图对象的位置。
    这个只是创建矩形、还有很多如圆弧、圆形等可以参照这个自行封装。
/**
 * 创建矢量图Shape
 *
 * @param {number} width 矢量图的宽
 * @param {number} height 矢量图的高
 * @param {number} color 矢量图的颜色
 * @param {number} [x=0] 矢量图的x坐标
 * @param {number} [y=0] 矢量图的y坐标
 * @param {number} [alpha=1] 矢量图的透明度
 * @returns {egret.Shape} 返回矢量图对象
 */
function createShape(width: number, height: number, color: number, x: number = 0, y: number = 0, alpha: number = 1): egret.Shape {
    const resource: egret.Shape = new egret.Shape();
    resource.graphics.beginFill(color, alpha);
    resource.graphics.drawRect(0, 0, width, height);
    resource.graphics.endFill();
    resource.x = x;
    resource.y = y;
    return resource;
}

另附一个自己封装的环形图代码:
这个实现了传入一个sprite对象以及固定格式的数据,将环形图及图例添加到传入对象的特定方法。

/**
     * 画饼图
     * obj,在哪个对象上绘制
     * data,饼图数据,格式固定
     *
     * @export
     * @param {any} obj
     * @param {any} data
     */
    export function drawCircle(obj, data) {
        const objWidth: number = obj.width;
        const objHeight: number = obj.height;
        const width: number = obj.width * 0.8; //新创建的背景的宽度,宽高相同
        const height: number = obj.height * 0.8; //新创建的背景的高度
        const lineHeight: number = 35;
        const length: number = data.length; //数据长度
        let startDeg = 0; //扇形起始角度
        let endDeg = 0; //结束角度

        let percentBg: egret.Sprite = new egret.Sprite();
        percentBg.width = width;
        percentBg.height = height;
        percentBg.x = (objWidth - width) / 2;
        percentBg.y = 95;
        obj.addChild(percentBg);
        let percentTotalBg: egret.Sprite = new egret.Sprite();
        percentTotalBg.width = width;
        let circleBg: egret.Sprite = new egret.Sprite();
        percentTotalBg.addChild(circleBg);
        circleBg.width = width;
        circleBg.height = width;
        circleBg.y = 0;

        for (let i = 0; i < length; i++) { //环形图浅色部分
            endDeg = (data[i].industryPercent).toFixed(2) * 3.6 + startDeg;
            circleBg.graphics.beginFill(GameConfig.lightColor[i]);
            circleBg.graphics.moveTo(width / 2, width / 2);
            circleBg.graphics.lineTo(width, width / 2);
            circleBg.graphics.drawArc(width / 2, width / 2, width / 2.3, (startDeg + 2) * Math.PI / 180, endDeg * Math.PI / 180);
            circleBg.graphics.lineTo(width / 2, width / 2);
            circleBg.graphics.endFill();
            startDeg = endDeg;
        }
        for (let i = 0; i < length; i++) { //环形图深色部分
            endDeg = (data[i].industryPercent).toFixed(2) * 3.6 + startDeg;
            circleBg.graphics.beginFill(GameConfig.darkColor[i]);
            circleBg.graphics.moveTo(width / 2, width / 2);
            circleBg.graphics.lineTo(width, width / 2);
            circleBg.graphics.drawArc(width / 2, width / 2, width / 2.6, (startDeg + 2) * Math.PI / 180, endDeg * Math.PI / 180);
            circleBg.graphics.lineTo(width / 2, width / 2);
            circleBg.graphics.endFill();
            startDeg = endDeg;
        }
        circleBg.graphics.beginFill(GameConfig.TextColors.white); //环形图中间白色部分
        circleBg.graphics.moveTo(width / 2, width / 2);
        circleBg.graphics.lineTo(width, width / 2);
        circleBg.graphics.drawArc(width / 2, width / 2, width / 3.5, 0, 360 * Math.PI / 180);
        circleBg.graphics.lineTo(width / 2, width / 2);
        circleBg.graphics.endFill();

        let centerIcon = ZpyGameUtils.createBitmapByName("card_dark_icon_png"); //中心icon
        centerIcon.x = width / 2 - centerIcon.width / 2 * 0.6;
        centerIcon.y = width / 2 - centerIcon.height / 2 * 0.6;
        centerIcon.scaleX = 0.6;
        centerIcon.scaleY = 0.6;
        circleBg.addChild(centerIcon);
        percentTotalBg.addChild(circleBg);

        let labelBg: egret.Sprite = new egret.Sprite();
        labelBg.width = width - 40;
        labelBg.x = 20;
        labelBg.y = width;
        for (let i = 0, len = data.length; i < len; i++) {
            let icon: egret.Shape = new egret.Shape();
            icon.graphics.beginFill(GameConfig.darkColor[i]);
            icon.graphics.drawRect(0, i * lineHeight, 20, 20);
            icon.graphics.endFill();
            labelBg.addChild(icon);

            let label: egret.TextField = ZpyGameUtils.createText(data[i].industryName + '  ' + (data[i].industryPercent).toFixed(2) + '%', GameConfig.TextColors.black, 20);
            label.x = 30;
            label.y = lineHeight * i;
            labelBg.addChild(label);
        }
        percentTotalBg.addChild(labelBg);
        percentTotalBg.height = width + length * lineHeight;

        var scrollView: egret.ScrollView = new egret.ScrollView();
        scrollView.setContent(percentTotalBg);
        scrollView.width = width;
        scrollView.height = height;
        percentBg.addChild(scrollView);
    }

egret游戏引擎入门(六)

今天分享一下通信控制SocketManager。
在egret引擎中,websocket是作为第三方库存在的,需要在创建之初将websocket选项选中,或者参照官网教程,在egret项目的egretProperties.json中编写。官网-如何使用第三方库

通信控制也是采用单例模式,里面也会有几个方法:

当socket连接打开

this.webSocket.addEventListener(egret.Event.CONNECT, this.onSocketOpen, this);

当socket连接关闭,这个地方会有一些处理。如:掉线重连操作等

this.webSocket.addEventListener(egret.Event.CLOSE, this.onSocketClose, this);

当socket连接错误

this.webSocket.addEventListener(egret.IOErrorEvent.IO_ERROR, this.onSocketError, this);

当接收到后端传过来的消息,因为通信是通过protobuf,所以这个地方接收到消息后,会传入SocketDataHandler方法中按照cmd进行对应的解析,如果发送的消息很多的话,这里也要写很多。值得注意的是,书写的顺序必须与后端保持一致。这里我也没大有发言权,毕竟是另一个同事调研编写的。但是个人感觉通过protobuf相当麻烦,需要给每一条消息体都做相应的定义,加上处理数据需要写一遍、通过不同cmd执行不同方法写一遍,加起来就是要在收到消息这里编写三次。

this.webSocket.addEventListener(egret.ProgressEvent.SOCKET_DATA, this.onReceiveMessage, this);

通过url与后端进行连接

this.webSocket.connectByUrl(GlobalData.scUrl);

在消息传输的过程中发现了一个语言层面的巨大无比的坑…
ts中使用protobuf时需要定义一个该条消息的类型,类似string、int32、int64这种的。之前在定义userID的时候,将类型写为int64,结果发现userID尾数不是0的都变为了0,好像是第15为之后。找了各种资料才发现,js只能到15位,最终无奈将userID这个类型变为了string。


下次会分享几个封装的egret方法。

egret游戏引擎入门(五)

今天分享一下场景切换类ViewManager。说起场景切换不得不说与其相配合的场景切换事件ChangeSceneEvent。

场景切换事件:ChangeSceneEvent

该事件继承自egret.Event

class ChangeSceneEvent extends egret.Event {
    public static CHANGE_SCENE_EVENT: string = 'changesceneevent';
    public eventType: any; //事件类型
    public obj: any; //对象
    public constructor(type: string, bubbles: boolean = false, cancelable: boolean = false) {
        super(type, bubbles, cancelable);
    }
}

eventType是自定义的事件类型,即每个场景的类中的静态属性;
obj是指在哪个场景调用这个方法,然后将当前场景传过去,执行当前场景的end方法。
调用:

private goNextView() {
    SoundManager.getInstance().playClick();
    var changeEvent = new ChangeSceneEvent(ChangeSceneEvent.CHANGE_SCENE_EVENT);
    changeEvent.eventType = PkGameGuideView.PK_GAME_GUIDE_VIEW;
    changeEvent.obj = this;
    ViewManager.getInstance().dispatchEvent(changeEvent);
}

private end() {
    GameLayerManager.gameLayer().sceneLayer.removeChild(this);
}

在调用时,首先new一个ChangeSceneEvent事件,并传入事件名CHANGE_SCENE_EVENT,也就是之前事件类中的公共静态方法。然后将当前场景的静态属性(在这里是PkGameGuideView.PK_GAME_GUIDE_VIEW)作为eventType的值,这个会在场景控制ViewManager中调用。

场景控制类:ViewManager

场景控制类只有一个作用,那就是根据场景切换事件传过来的要切换成哪个场景,执行对应的操作。
这个类同样也只会存在一个,所以也是采用单例模式来写。
首先在初始化的时候将游戏中的所有场景都声明一遍并赋值

private firstGuideView: FirstGuideView;
private gameMainView: GameMainView;
private init() {
    this.gameMainView = GameMainView.getInstance();
    this.firstGuideView = FirstGuideView.getInstance();
}

然后不要忘了在初始化的同时,添加场景切换的事件监听

this.addEventListener(ChangeSceneEvent.CHANGE_SCENE_EVENT, this.onChangeScene, this);

最后,在onChangeScene方法中,通过传入的eventType来执行相对应的操作。当然在onChangeScene方法中也会先调用obj.end()方法,执行原有场景类中的end方法。如果怕页面上有东西还可以调用一下sceneLayer的removeChildren()方法,删除场景层上的所有内容。
贴一下完整代码:

class ViewManager extends egret.Sprite {

    private static instance: ViewManager;

    public constructor() {
        super();
        this.init();
    }

    public static getInstance(): ViewManager {
        if(ViewManager.instance == null) {
            ViewManager.instance = new ViewManager();
        }
        return ViewManager.instance;
    }

    //声明各个页面
    private firstGuideView: FirstGuideView; //第一次登陆页面
    private chooseCardGuideView: ChooseCardGuideView; //选牌页面新手引导模块
    private gameMainView: GameMainView; //游戏主页
    private connectingView: ConnectingView; //匹配页面
    //......
    //有多少场景就写多少

    public init() {
        this.gameMainView = GameMainView.getInstance();
        this.firstGuideView = FirstGuideView.getInstance();
        this.chooseCardGuideView = ChooseCardGuideView.getInstance();
        this.connectingView = ConnectingView.getInstance();
        //......
        //有多少场景就写多少
        this.addEventListener(ChangeSceneEvent.CHANGE_SCENE_EVENT, this.onChangeScene, this); //侦听场景改变事件
    }

    private onChangeScene(e: ChangeSceneEvent) {
        e.obj.end();
        GameLayerManager.gameLayer().sceneLayer.removeChildren();

        switch (e.eventType) {
            case FirstGuideView.FIRST_GUIDE_VIEW: //新手引导页
                GameLayerManager.gameLayer().sceneLayer.addChild(this.firstGuideView);
                break;

            case GameMainView.GAME_MAIN_VIEW: //游戏主页面
                GameLayerManager.gameLayer().sceneLayer.addChild(this.gameMainView);
                break;

            case ConnectingView.CONNECTING_VIEW: //匹配页面
                GameLayerManager.gameLayer().sceneLayer.addChild(this.connectingView);
                break;

            case ChooseCardGuideView.CHOOSE_CARD_GUIDE_VIEW: //选牌页面新手引导模块
                GameLayerManager.gameLayer().sceneLayer.addChild(this.chooseCardGuideView);
                break;

            //......
            //有多少场景就写多少
        }
    }
}

游戏的流程控制FlowManager以及配合使用的收到消息时间ReceiveMessageEvent写法跟用法跟本篇相似,就不在另行分享了。


下次分享一下通信控制SocketManager。

egret游戏引擎入门(四)

今天分享一下Controller中的场景管理类GameLayerManager。
同样采用单例模式,这个类中会使用eui创建多个图层,如:场景管理处SceneLayer,特效层EffectLayer等。

public sceneLayer: eui.UILayer = new eui.UILayer();
public loadLayer: eui.UILayer = new eui.UILayer();
public maskLayer: eui.UILayer = new eui.UILayer();
public panelLayer: eui.UILayer = new eui.UILayer();
public effectLayer: eui.UILayer = new eui.UILayer();

当一个页面同时存在多个图层时,不可避免的就会出现有些图层不能点击的问题。这个时候就需要通过设置touchThrough这个属性,来允许可以透过当前图层点击到下一图层的内容。这里的点击到下一图层指的是空白区域,

如:
touchThrough图例
如果上图的黑色透明部分没有颜色,即没有在黑色透明位置添加东西,那么下面的那张卡牌是可以点击的。但是需求是不能点击,所以给这里添加了黑色透明mask。

跟css中的z-index类似,图层也有个放置的顺序问题。在egret引擎中,后添加的层级要高于早添加的。也就是在本游戏中panelLayer弹窗层处于最上面,所以panelLayer要最后添加。因为游戏弹窗总是在场景的上方,所以要先添加sceneLayer,后添加panelLayer。

this.addChild(this.sceneLayer);
this.addChild(this.panelLayer);

这个类内容比较少,同样贴一下代码:

class GameLayerManager extends eui.UILayer {

    private static instance: GameLayerManager;
    public constructor() {
        super();
        this.init();
    }

    public static gameLayer(): GameLayerManager {
        if (!this.instance) {
            this.instance = new GameLayerManager();
        }
        return this.instance;
    }

    //创建场景图层
    public sceneLayer: eui.UILayer = new eui.UILayer(); //场景层,如各个页面
    public loadLayer: eui.UILayer = new eui.UILayer(); //加载遮罩层    
    public maskLayer: eui.UILayer = new eui.UILayer(); //遮罩层
    public panelLayer: eui.UILayer = new eui.UILayer(); //弹窗层,如签到,设置等
    public effectLayer: eui.UILayer = new eui.UILayer(); //特效层,如倒计时等

    private init() {
        this.touchThrough = true;
        this.loadLayer.touchThrough = true;
        this.sceneLayer.touchThrough = true;        
        this.maskLayer.touchThrough = true;
        this.effectLayer.touchThrough = true;        
        this.panelLayer.touchThrough = true;

        this.addChild(this.sceneLayer);
        this.addChild(this.loadLayer);
        this.addChild(this.maskLayer);
        this.addChild(this.effectLayer);
        this.addChild(this.panelLayer);
    }
}

下次会分享一下场景切换类ViewManager。

egret游戏引擎入门(三)

今天分享一下Controller中的SoundManager声音控制。
在游戏需求评审的时候,发现游戏原型图中控制声音的只有一个按钮,也就是背景音乐的播放以及音效的播放时统一控制的,跟常见的游戏有音效开关、声音开关以及音量大小调节完全不同。但是总感觉这里有坑,暂且分开开发,然后关联到一起。这样以后有多个按钮的需求时微微动下代码就能使用。

这里注意一下,egret引擎对音频资源比较挑剔,只能使用MP3格式的音频文件,如果播放不出来的话可以参照官网对于音频文件的说明,可以使用格式工厂进行码率转变。egret官网-音频说明
经过多个demo的迭代以及请教公司的后端,像控制器或者场景这种同时只能存在一个的这种Class,我都做成了单例模式。也就是每次都会调用getInstance方法来找到这个Class的实例。

首先是声明一个静态的私有属性,这个属性跟类名相同。即:private static instance: SoundManager;
其次是声明一个公共的静态方法,getInstance()。即:

public static getInstance(): SoundManager {
    if(SoundManager.instance == null) {
        SoundManager.instance = new SoundManager();
    }
    return SoundManager.instance;
}

这个方法通过判断SoundManager.instance这个属性时候为null,如果是null则实例化一个SoundManager,如果不为null直接返回该属性,从而返回当前类的实例。

web端不像app端,将音乐设置没后再次打开也没有音乐。为了模拟这个效果,只能将音乐是否播放存入浏览器的localStorage中:

egret.localStorage.setItem('ismusic','0');
egret.localStorage.getItem('ismusic');

这里的操作方法跟正常写页面的操作方法一样,设置就传入参数名(key)及参数值(value);获取就传入参数名(key)就可调用。

音乐的播放时调用play方法,传入两个参数startTime开始时间,loops循环次数(0为无限循环)

private bg: egret.Sound; //游戏背景音乐

private init() {
    this.bg = new egret.Sound(); //游戏背景音乐
    this.bg = RES.getRes('zpy_game_bg_mp3');
}

public playSoundBg() { //播放背景音乐
    if(this.IsMusic) {
        this.bgChannel = this.bg.play(0, 0);
    }
}

因为我这里没有写音量的控制,这里也说一下音量的问题:
首先要声明一个egret.SoundChannel类型的变量,该变量有个volume属性,当这个属性的值介于0-1之间。当属性值等于0时,音量最小,反之最大。

private bgChannel: egret.SoundChannel; //用来静音
public playSoundBg() { //播放背景音乐
    if(this.IsMusic) {
        this.bgChannel = this.bg.play(0, 0);
        this.bgChannel.volume = 0;
    }
}

贴一下这个类的代码:

class SoundManager {

    private static instance: SoundManager;    
    public constructor() {
        this.init();
    }

    public static getInstance(): SoundManager {
        if(SoundManager.instance == null) {
            SoundManager.instance = new SoundManager();
        }
        return SoundManager.instance;
    }

    //不同的音效
    private bg: egret.Sound; //游戏背景音乐
    private click: egret.Sound; //点击音效
    private getMoney: egret.Sound; //获得金币
    private banker: egret.Sound; //成为庄家
    private bgChannel: egret.SoundChannel; //用来静音

    //在SoundManager初始化时将这些音效赋值为相应的预加载的游戏音频资源。
    private init() {
        this.bg = new egret.Sound(); //游戏背景音乐
        this.bg = RES.getRes('zpy_game_bg_mp3');

        this.click = new egret.Sound(); //点击音效
        this.click = RES.getRes('zpy_game_click_mp3');

        this.getMoney = new egret.Sound(); //获得金钱
        this.getMoney = RES.getRes('zpy_game_getMoney_mp3');

        this.banker = new egret.Sound(); //成为庄家
        this.banker = RES.getRes('zpy_game_banker_mp3');
    }

    //音乐是否播放,保存设置
    public set IsMusic(value) {
        if(!value) {
            egret.localStorage.setItem('ismusic','0');
            this.stopSoundBg();
        }else {
            egret.localStorage.setItem('ismusic','1');
            this.playSoundBg();            
        }
    }
    public get IsMusic(): boolean {
        var b = egret.localStorage.getItem('ismusic');
        if(b == null || b == '') {
            return true;
        }else {
            return b == '1';
        }
    }

    //音效是否播放,保存设置,游戏一期对于声音只有一个控制按钮,所以在声音的模块中,停止背景音乐的同时,也将音效的声音关闭
    public set IsSound(value) {
        if(!value) {
            egret.localStorage.setItem('issound','0');
        }else {
            egret.localStorage.setItem('issound','1');
        }
    }
    public get IsSound(): boolean {
        var b = egret.localStorage.getItem('issound');
        if(b == null || b == '') {
            return true;
        }else {
            return b == '1';
        }
    }

    public playSoundBg() { //播放背景音乐
        if(this.IsMusic) {
            this.bgChannel = this.bg.play(0, 0);
        }
    }
    public stopSoundBg() { //停止背景音乐
        if(this.bgChannel != null) {
            this.bgChannel.stop();
        }
    }

    public playClick() { //点击音效
        if(this.IsSound) {
            this.click.play(0,1);
        }
    }

    public playBanker() { //成为庄家音效
        if(this.IsSound) {
            this.banker.play(0,1);
        }
    }

    public playGetMoney() {
        if(this.IsSound) {
            this.getMoney.play(0,1);
        }
    }

}

下次分享一下场景层管理类GameLayerManager。

2017年3月27日新增

预加载、自动播放无效
预加载、自动播放无效
如上表所示,经过简单的测试发现:预加载、自动播放的有效性受操作系统、浏览器、版本等影响,苹果官方规定必须由用户手动触发才会载入音频。即:用户手动随意点击任意位置即可播放音频。

egret游戏引擎入门(二)

今天分享一下游戏入口类的编写。
首先游戏中默认的入口为Main.ts,但是因为是游戏入口,我只想在这里看到游戏加载后进入哪个页面,不想看见乱七八糟的游戏加载等事件。所以在这里将游戏入口拆分成两个部分,一个是入口的基类BaseMain,一个是入口类Main。这个地方是借鉴的别人的思想。

游戏入口基类:BaseMain

先说一下基类吧,对于像我这样的不是计算机专业的,也没学过java这种语言的人来说,可能不知道什么是基类。百度的解释是:通过继承机制,可以利用已有的数据类型来定义新的数据类型。在本游戏中,游戏入口基类最要承担游戏资源的加载、egret自带的素材解析、egret自带的主题解析、以及加载完资源后的操作。

资源加载RES

游戏资源加载的作用是在游戏开始时预加载资源
其实egret自动生成的demo对这方面的解释也比较清晰,从每一个方法名来看就可以看出,这个方法的作用。
RES.ResourceEvent.CONFIG_COMPLETE是资源配置文件加载完成;

RES.addEventListener(RES.ResourceEvent.CONFIG_COMPLETE,this.onConfigComplete,this);

RES.ResourceEvent.GROUP_COMPLETE是资源组加载完成,之前调研的createjs也有类似的资源组。这里需要判断加载完什么资源之后要执行的方法。比如我在这里就有当loading资源组加载完成后,加载preload资源组,并且显示loading页面。

RES.addEventListener(RES.ResourceEvent.GROUP_COMPLETE,this.onResourceLoadComplete,this);

RES.ResourceEvent.GROUP_PROGRESS是资源组加载中,这里一般会做一个判断,如果不是loading资源组,就显示loading页面,并且显示所有游戏资源的加载情况(进度条);

RES.addEventListener(RES.ResourceEvent.GROUP_PROGRESS,this.onResourceProgress,this);

RES.ResourceEvent.GROUP_LOAD_ERROR是资源组加载出错;

RES.addEventListener(RES.ResourceEvent.GROUP_LOAD_ERROR,this.onResourceLoadError,this);

RES.ResourceEvent.ITEM_LOAD_ERROR是某个资源加载出错。

RES.addEventListener(RES.ResourceEvent.ITEM_LOAD_ERROR,this.onItemLoadError,this);

RES.loadGroup(groupname)是开始加载某个资源组。

RES.loadGroup("loading");

这里的private是这个类的私有属性方法,外部无法调用。就像

function _alert(){
    alert(1);
}

因为js中没有私有、公共这个概念,有时候就会使用” _ “来约定,该属性或方法是私有的,外部不要调用。有addEventListener就有removeEventListener,也就是移除事件监听。因为游戏对性能要求比较高,不用了的监听需要及时移除,省的监听多了造成游戏卡顿。
之前也看到过单独编写资源加载类的,但是自己看了下,没有采用。

其他方法

如打开socket连接、发送第一条请求数据、注入egret定义的素材解析器、添加游戏图层(GameLayerManager)等都会在游戏的基类中编写。
最终贴一下BaseMain的代码吧,因为也是从头开始接触这个,所以注释什么的写的也比较全面。

namespace base {

    export class BaseMain extends eui.UILayer {
        private loadingView: LoadingView; //加载页面

        public constructor() {
            super();
            this.addEventListener(egret.Event.ADDED_TO_STAGE, this._onAddToStage, this);
        }
        private _onAddToStage() {
            SocketManager.getInstance().connection(); //开始websocket连接

            this.stage.registerImplementation('eui.IAssetAdapter',new AssetAdapter()); //注入自定义的素材解析器
            var theme = new eui.Theme('resource/default.thm.json', this.stage);

            this.addChild(GameLayerManager.gameLayer()); //添加游戏图层

            RES.addEventListener(RES.ResourceEvent.CONFIG_COMPLETE,this.onConfigComplete,this); //加载资源配置文件
            RES.loadConfig('resource/default.res.json', 'resource/');
        }

        private onConfigComplete(event: RES.ResourceEvent): void {
            RES.removeEventListener(RES.ResourceEvent.CONFIG_COMPLETE,this.onConfigComplete,this); //移除监听事件            
            //代表什么时候加载资源,完成、预加载等
            RES.addEventListener(RES.ResourceEvent.GROUP_COMPLETE,this.onResourceLoadComplete,this); //加载完成            
            RES.addEventListener(RES.ResourceEvent.GROUP_PROGRESS,this.onResourceProgress,this); //加载进行中
            RES.addEventListener(RES.ResourceEvent.GROUP_LOAD_ERROR,this.onResourceLoadError,this); //加载组错误
            RES.addEventListener(RES.ResourceEvent.ITEM_LOAD_ERROR,this.onItemLoadError,this); //加载单个文件错误
            RES.loadGroup("loading");
        }

        private onResourceProgress(event: RES.ResourceEvent): void {
            if(event.groupName != 'loading') { //如果不是加载loading页面则显示进度条
                this.loadingView.createLoadingBar(event.itemsLoaded, event.itemsTotal);
            }
        }

        private onResourceLoadError(event: RES.ResourceEvent): void {
            console.error('资源[' + event.groupName + ']:加载失败');
            this.onResourceLoadComplete(event);            
        }

        private onItemLoadError(event: RES.ResourceEvent): void {
            console.error('Url:' + event.resItem.url + '加载失败');
        }

        private onResourceLoadComplete(event: RES.ResourceEvent): void {
            console.warn('资源[' + event.groupName + ']:加载完成');
            this.addChild(ViewManager.getInstance()); //添加场景切换控制

            if(event.groupName == 'loading') { //如果加载完成的是loading资源组
                this.loadingView = new LoadingView();
                GameLayerManager.gameLayer().loadLayer.addChild(this.loadingView);
                RES.loadGroup('preload');
            }else if(event.groupName == 'preload') { //如果加载完成的是preload资源组
                SoundManager.getInstance(); //预先加载声音,放到声音预加载之后再执行
                if(GamePlayer.playerInfo.isNew) { //判断是否是新用户
                    RES.loadGroup('guide');
                }else {
                    let timer = setInterval(() => {
                        if(SocketManager.getInstance().isConnection) {
                            clearInterval(timer);
                            this.start();
                            GameLayerManager.gameLayer().loadLayer.removeChild(this.loadingView);                            
                        }                 
                    },100)
                }

            }else if(event.groupName == 'guide') { //如果是新用户 加载guide资源组
                if(SocketManager.getInstance().isConnection && GamePlayer.playerInfo.isNew) {
                    this.firstStart();
                    GameLayerManager.gameLayer().loadLayer.removeChild(this.loadingView);
                }else if(SocketManager.getInstance().isConnection) {
                    GameLayerManager.gameLayer().loadLayer.removeChild(this.loadingView);                    
                    this.start();
                }
            }
            let timer = setInterval(() => {
                if(SocketManager.getInstance().isConnection) {
                    clearInterval(timer);
                    SocketManager.getInstance().sendData(GlobalData.Cmd.gameEnter);
                }
            },100);
        }

        public start(): void {} //走main.ts
        public firstStart(): void {} //走main.ts
    }


    class AssetAdapter implements eui.IAssetAdapter {
        /**
         * 解析素材
         * @param source 待解析的新素材标识符
         * @param compFunc 解析完成回调函数,示例:callBack(content:any,source:string):void;
         * @param thisObject callBack的 this 引用
         */
        public getAsset(source: string, compFunc:Function, thisObject: any): void {
            function onGetRes(data: any): void {
                compFunc.call(thisObject, data, source);
            }
            if (RES.hasRes(source)) {
                let data = RES.getRes(source);
                if (data) {
                    onGetRes(data);
                }
                else {
                    RES.getResAsync(source, onGetRes, this);
                }
            }
            else {
                RES.getResByUrl(source, onGetRes, this, RES.ResourceItem.TYPE_IMAGE);
            }
        }
    }

    class ThemeAdapter implements eui.IThemeAdapter {
        /**
         * 解析主题
         * @param url 待解析的主题url
         * @param compFunc 解析完成回调函数,示例:compFunc(e:egret.Event):void;
         * @param errorFunc 解析失败回调函数,示例:errorFunc():void;
         * @param thisObject 回调的this引用
         */
        public getTheme(url:string,compFunc:Function,errorFunc:Function,thisObject:any):void {
            function onGetRes(e:string):void {
                compFunc.call(thisObject, e);
            }
            function onError(e:RES.ResourceEvent):void {
                if(e.resItem.url == url) {
                    RES.removeEventListener(RES.ResourceEvent.ITEM_LOAD_ERROR, onError, null);
                    errorFunc.call(thisObject);
                }
            }
            RES.addEventListener(RES.ResourceEvent.ITEM_LOAD_ERROR, onError, null);
            RES.getResByUrl(url, onGetRes, this, RES.ResourceItem.TYPE_TEXT);
        }
    }

}

入口类:Main

游戏入口类继承自BaseMain。
刚刚的游戏入口类中,在preload资源组以及guide资源组加载完成之后,分别调用start跟firstStart方法。但是在基类中,这两个方法没有方法体。那么如何调用呢?
在入口类Main中,有两个公共方法,一个是start方法,一个是firstStart方法。因为这个类是继承自BaseMain的,所以在游戏一开始运行时会先执行基类的方法,等到资源加载完成要调用相应的start跟firstStart方法时,就会先调用当前入口类Main中的方法。如果Main中没有该方法,就会执行基类也就是BaseMain中的方法。
这个没有什么好说的,直接贴一下代码:

class Main extends base.BaseMain {

    public constructor() {
        super();
    }

    public firstStart(): void { //新玩家
        var changeEvent = new ChangeSceneEvent(ChangeSceneEvent.CHANGE_SCENE_EVENT);
        changeEvent.eventType = FirstGuideView.FIRST_GUIDE_VIEW;
        changeEvent.obj = this;
        ViewManager.getInstance().dispatchEvent(changeEvent);
        SoundManager.getInstance().playSoundBg(); //播放背景音乐
    }

    public start(): void { //老玩家
        var changeEvent = new ChangeSceneEvent(ChangeSceneEvent.CHANGE_SCENE_EVENT);
        changeEvent.eventType = GameMainView.GAME_MAIN_VIEW;
        changeEvent.obj = this;
        ViewManager.getInstance().dispatchEvent(changeEvent);
        SoundManager.getInstance().playSoundBg(); //播放背景音乐
    }

    private end(): void {

    }

}

下次会分享一下关于声音控制的方法。

egret游戏引擎入门(一)

最初调研egret其实也是迫不得已。因为当时公司突然有了做游戏的需求,然而全公司上下竟然没有一个有开发游戏的经验。然后需求被boss接下来之后分配给了我,然后说可以看一下createjs。
经过一个月的调研+demo制作之后发现,英文的API也能凑合看一看,但是大部分功能还是自己各种尝试。但是因为社区活跃度太低了,加了还几个群,网上各种找资料,没弄懂的依旧没弄懂。打个比方来说屏幕的适配问题,搞了三四天依旧没有搞定。
在这期间,闲着没事儿的时候就会看网上各种关于web游戏引擎的评价、对比。最终选定egret还是因为觉得egret有一整套的工具。比如:ResDepot可以可视化管理游戏资源的,Texture Merger将小图合成大图,ios/android-support等等。

在正式开发之前,也看过好多好多教程、代码,自己写的demo也从最开始的流水式函数式从上到下垒代码,到第五遍demo的时候变成了个人认为比较符合MVC设计结构的代码。
写这一系列文章也是想让那些没有游戏开发经验,但是有这个需求的小白前端开发者们快速的趟过我走过的那些坑。至于egret的API官网上都有,可以直接参照。当然在游戏开发方面我也是小白,这些都是自己看过好多代码好多教程总结出来的,如有问题希望各位大大们多多指教。

游戏的框架:

game_MVC

Model:

先说一下Model吧,游戏所有的数据包括新手引导的数据、游戏的设置、客户端维护的玩家对象、pk赛游戏中客户端维护的其他玩家对象组以及后端返回数据的操作都会作为Model部分。每一个都有对应的Class或者namespace(module)。

View:

因为采用了egret的eui,所以这里的View像ps中的一个一个的图层那样。每一层都会显示不同的内容。比如:场景层SceneLayer,游戏内的各个场景会放置在这一层中进行展示,这一层也是最为重要的一层;弹窗层PanelLayer,一个游戏肯定会有很多类似弹窗、弹框这种的提示信息或模块,这个时候就需要将这些东西放置到弹窗层中进行统一控制;遮罩层MaskLayer及效果层EffectLayer是用来存放遮罩及游戏动效的;加载层LoadLayer只有在游戏最开始加载的时候会用到,进入游戏大厅之后就会销毁。当然也可以当做场景放置在SceneLayer中。

Controller:

最后再说一下Controller。本游戏主要通过声音、通信、图层、流程、场景等几个纬度来进行Controller的设计的。声音控制SoundManager,用于游戏内声音、音效的控制;通信控制SocketManager以及附属的HandleSocket(用于解析protobuf的)来控制游戏的连接以及数据传输的解析;图层控制GameLayerManager,用于控制游戏的各个图层的层级关系,如PanelLayer高于SceneLayer,也可以理解为脱离文档留之后的z-index;场景控制ViewManager,用于控制游戏场景之间的切换;流程控制FlowManager,用在接收后端信息后执行对应的操作。

最后:egret引擎是采用typescript开发的,在调研egret的同时,顺便也学习了一下typescript。所以感觉这个游戏学习上手成本很高,但是也多会了一些平常写页面接触不到的。关于typescript的内容我会令外整理,本教程只分享egret引擎相关的内容。
因为是网络游戏,考虑到信息传输的速度问题,后端决定采用谷歌的二进制传输协议protobuf。protobuf是另外一个前端同事调研的,对于这块儿我也不是很了解,但是这里面会有很多坑,而且调试起来非常的不方便。其实感觉用json也可以,哈哈。


下次会分享关于游戏入口类的编写。