经过前面的制作,使用Egret的Wing很快完成了开始界面和选关卡界面,下面通常来说就是游戏界面,但此时界面切换和关卡数据还没有准备好,这次讲解界面的切换和关卡数据的解析。前面多次修改了Main.ts文件中startCreateScene的方法,这个方法就是当游戏的前置Loading全部完成之后,执行的开始方法,Main本身就是UI容器,所以直接Add进去做好的UI逻辑即可。
界面切换
这里涉及到一个界面切换的代码设计问题,以开始界面为例,当“开始游戏”按钮点击之后,应将开始界面移除,进入到选关卡界面,写法看起来简单,但是可能我们需要获取Main并将其移出的同时还要new一个新的选关界面进来,此时很多人的写法可能将Main保存,方便其他类访问,或是在Main里面写上一大堆的UI处理逻辑,这样看来代码就不怎么优雅了,而且,为了性能和安全问题,通常还要手动移出掉内部使用的EventListener,比较周全的做法是,添加RemoveStage监听,然后移除自己的同时,将子对象给移除,这就是为什么Main中LoadingUI处理时,出现了大量的addEventListener和removeEventListener的原因。
在本项目中,使用单例模式来保证代码的简洁性,让它看起来没有那么多调来调去的方法,单例会一直保证一个实例在内存中,如果这个对象会被频繁使用,而且游戏中一只有一个的话,就比较适用,比如说游戏中的界面类,以本项目来说,总计也就是3-4个界面,而且总是它们跳来跳去,所以,在这里使用单例控制,为SceneBegin和SceneLevels添加单例,以下是SceneBegin类的代码。
class SceneBegin extends eui.Component { //单例 private static shared: SceneBegin; public static Shared() { if(SceneBegin.shared == null) { SceneBegin.shared = new SceneBegin(); } return SceneBegin.shared; } private btn_begin:eui.Button; public constructor() { super(); this.skinName = "src/Game/SceneBeginSkin.exml"; this.btn_begin.addEventListener(egret.TouchEvent.TOUCH_TAP,this.onclick_begin,this); } private onclick_begin(){ //console.log("game begin!"); this.parent.addChild(SceneLevels.Shared()); this.parent.removeChild(this); } }
上面的onclick_begin()使用了一个常用的父对象便捷操作法,直接调用父对象的addChild把另外一个界面添加进来,同时把自己移除掉,比起获取全局对象处理跳转是不是简单清晰了呢?想进阶的童鞋,可以研究一下更深入的玩法,同样的SceneLevels就不写全了,直接在onclick_back()使用上面同样的技巧:
//单例 private static shared: SceneLevels; public static Shared() { if(SceneLevels.shared == null) { SceneLevels.shared = new SceneLevels(); } return SceneLevels.shared; }
private onclick_back() { this.parent.addChild(SceneBegin.Shared()); this.parent.removeChild(this); }
现在打开Main.ts把startCreateScene修改一下:
protected startCreateScene(): void { this.addChild(SceneBegin.Shared()); }
现在运行起来看看跳转效果吧,其他的类似的界面同理可以用这种方法,
关卡数据处理
下面就要考虑游戏数据问题,游戏的数据已经做好了一个json文件:questions.json
将这个文件添加到资源中,并保证提前读取完毕,在给出的项目代码中,是放在了项目根目录下,拖进去的时候会自动复制到assets目录下面一份。
当然,你也可以异步来处理,异步会带来更多逻辑复杂性,这里仅仅是简单的教程,以后再说更复杂的,现在创建一个LevelDataManager类,用来管理关卡数据:
//每个问题(关卡)的数据结构 class LevelDataItem{ public answer:string; public img:string; public word:string; public tip:string; public content:string; } //关卡数据管理器 class LevelDataManager { //单例 private static shared:LevelDataManager; public static Shared(){ if(LevelDataManager.shared == null){ LevelDataManager.shared = new LevelDataManager(); } return LevelDataManager.shared; } //一个关卡的保存数据组 private items:LevelDataItem[] = []; public constructor() { //使用RES读取和构建JSON数据,JSON数据可以直接解析到目标结构 this.items = RES.getRes("questions_json"); } //通过关卡号获得一个关的数据 public GetLevel(level:number):LevelDataItem{ if(level < 0) level = 0; if(level >= this.items.length) level = this.items.length - 1; return this.items[level]; } //获得当前的游戏最远进度 public get Milestone():number{ var milestone = egret.localStorage.getItem("CYDTZ_Milestone"); //如果没有数据,那么默认值就是第一关 if(milestone == "" || milestone == null){ milestone = "1"; } return parseInt(milestone); } //设置当前的游戏最远进度 public set Milestone(value:number){ egret.localStorage.setItem("CYDTZ_Milestone",value.toString()); } }
同样也使用了单例方式,我们利用这种方式来保存游戏的关卡数据,并能随时方便的访问它,而且在构造的时候就把数据给处理好了,使用egret.localStorage来管理游戏的进度,你会发现在最顶部写了一个LevelDataItem类,这个类用来解析每个关卡的具体数据,可以通过json来对应其中各种属性。
组合到选关界面中
那么我们现在有了游戏关卡数据,要组合到SceneLevels类中,做数据的访问,以方便在进入到选关界面的时候,能够正确的显示游戏关卡状态,所以要对SceneLevels.ts做一些修改:
1 class SceneLevels extends eui.Component { 2 //单例 3 private static shared: SceneLevels; 4 public static Shared() { 5 if(SceneLevels.shared == null) { 6 SceneLevels.shared = new SceneLevels(); 7 } 8 return SceneLevels.shared; 9 } 10 private btn_back: eui.Button; 11 private group_levels:eui.Group; 12 private img_arrow: eui.Image; 13 private sel_level: number = 0; 14 private LevelIcons:LevelIcon[] = []; 15 public constructor() { 16 super(); 17 this.skinName = "src/Game/SceneLevelsSkin.exml"; 18 this.btn_back.addEventListener(egret.TouchEvent.TOUCH_TAP,this.onclick_back,this); 19 //创建地图选项 20 var row = 20; 21 var col = 10; 22 var spanx = 720 / col; //计算行x间隔 23 var spany = 1136 / row; //计算列y间隔 24 var group = new eui.Group();//地图背景 25 group.width = 720; 26 group.height = (spany * 400 );//算出最大尺寸 27 //填充背景 28 for(var i = 0;i <= (group.height / 1138) ;i++) { 29 var img = new eui.Image(); 30 img.source = RES.getRes("GameBG2_jpg"); 31 img.y = i * 1138; 32 img.touchEnabled = false; 33 this.group_levels.addChildAt(img,0); 34 } 35 //以正弦曲线绘制关卡图标的路径 36 var milestone: number = LevelDataManager.Shared().Milestone; 37 for(var i = 0; i<400;i++){ 38 var icon = new LevelIcon(); 39 icon.Level = i + 1; 40 icon.y = spany * i /2; 41 icon.x = Math.sin(icon.y / 180 * Math.PI) * 200 + group.width / 2; 42 icon.y += spany * i /2; 43 icon.y = group.height - icon.y - spany; 44 group.addChild(icon); 45 icon.addEventListener(egret.TouchEvent.TOUCH_TAP,this.onclick_level,this); 46 //依据进度设置关卡显示 47 icon.enabled = i < milestone; 48 //保存到一个列表中 49 this.LevelIcons.push(icon); 50 } 51 //开启位图缓存模式 52 group.cacheAsBitmap = true; 53 this.group_levels.addChild(group); 54 //卷动到最底层 55 this.group_levels.scrollV = group.height - 1100; 56 //跟踪箭头 57 this.img_arrow = new eui.Image(); 58 this.img_arrow.source = RES.getRes("PageDownBtn_png"); 59 this.img_arrow.anchorOffsetX = 124 / 2 - group.getChildAt(0).width / 2; 60 this.img_arrow.anchorOffsetY = 76; 61 this.img_arrow.touchEnabled = false; 62 this.img_arrow.x = group.getChildAt(0).x; 63 this.img_arrow.y = group.getChildAt(0).y; 64 group.addChild(this.img_arrow); 65 66 } 67 private onclick_back() { 68 this.parent.addChild(SceneBegin.Shared()); 69 this.parent.removeChild(this); 70 } 71 private onclick_level(e:egret.TouchEvent){ 72 var icon = <LevelIcon>e.currentTarget; 73 if(this.sel_level != icon.Level){ 74 this.img_arrow.x = icon.x; 75 this.img_arrow.y = icon.y; 76 this.sel_level = icon.Level; 77 }else{ 78 //进入并开始游戏 79 } 80 } 81 //打开指定的关卡,如果大于最远关卡,则保存数据也跟着调整 82 public OpenLevel(level:number){ 83 var icon = this.LevelIcons[level - 1]; 84 icon.enabled = true; 85 if(level > LevelDataManager.Shared().Milestone){ 86 LevelDataManager.Shared().Milestone = level; 87 //同时将选定标记置于其上 88 this.img_arrow.x = icon.x; 89 this.img_arrow.y = icon.y; 90 this.sel_level = icon.Level; 91 } 92 } 93 }
对于这里简单讲一下,参看第47行,在创建图标的时候,我们将通过判断当前的进度来决定按钮的开启状态,71行的onclick_level()方法增加了一个当前选择sel_level变量来帮助完成选定后进入的关卡才开始游戏,最后,OpenLevel是为了将来做准备,当关卡完成后就会开启对应关卡icon。
数据创建优化
现在可以试试运行一下看看效果,但仍然不完美,因为当在开始界面点开始的时候,就会产生一次卡顿,是因为解析JSON数据源造成的,要知道有400多道题目,光json文件就有100多KB,卡顿是必然发生的事情,优化的方法有以下几种:
1、展现层面,在Loading的时候进行处理,如在资源读取完成之后,调用一下LevelDataManager.Shared(),这样就得要求预读
2、将数据源拆分,分开加载处理,50个题目一个文件,或者1道题1个文件,我的连连看数据就是这样处理
3、将数据源变成源码的一部分,如.ts或.js,这种方式读取处理效率倒是提升了,但是维护起来比较麻烦
本篇项目源码:ChengyuTiaozhan2.zip(由于博客园的文件大小限制,resource资源方面请到第二篇的后面下载)
本篇主要学习使用了单例设计模式、JSON数据使用、egret.localStorage储存游戏进度数据。