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也可以,哈哈。


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