此次給你們帶來的是經過Egret實現密室逃生小遊戲的教程。該遊戲包括人物狀態機、MVC設計模式和單例模式,該遊戲在1.5s內經過玩家點擊操做尋找安全點,方可進入下一關,關卡無限,分數無限。下面是具體的模塊介紹和代碼實現。git
該遊戲主要內容包括github
開始遊戲場景json
遊戲場景設計模式
遊戲結束結算場景數組
全局常量類安全
人物狀態機類dom
遊戲源碼素材下載:https://github.com/shenysun/RoomRun模塊化
在全部舞臺搭建以前先寫一個全局的靜態方法類,取名爲GameConst。這個類裏面的方法和常量能夠供全局使用,例如舞臺寬高、經過名字獲取位圖、經過名字獲取紋理集精靈等等。這個類能夠大大減小後期的代碼量,下降總體的耦合度。函數
/**經常使用常量類 */ class GameConst { /**舞臺寬度 */ public static StageW:number; /**舞臺高度 */ public static StageH:number; /**根據名字建立位圖 */ public static CreateBitmapByName(name:string):egret.Bitmap { let texture:egret.Texture = RES.getRes(name); let bitmap:egret.Bitmap = new egret.Bitmap(texture); return bitmap; } /** * 根據name關鍵字建立一個Bitmap對象。此name 是根據TexturePacker 組合成的一張位圖 */ public static createBitmapFromSheet(name:string, sheetName:string):egret.Bitmap { let texture:egret.Texture = RES.getRes(`${sheetName}_json.${name}`); let result:egret.Bitmap = new egret.Bitmap(texture); return result; } public static getTextureFromSheet(name:string, sheetName:string):egret.Texture { let result:egret.Texture = RES.getRes(`${sheetName}_json.${name}`); return result; } /**移除子類方法 */ public static removeChild(child:egret.DisplayObject) { if(child && child.parent) { if((<any>child.parent).removeElement) { (<any>child.parent).removeElement(<any>(child)); } else { child.parent.removeChild(child); } } } }
若是遊戲中設置圖片錨點較多也能夠在這個類裏面加一個設置錨點的方法,傳入對象,橫軸錨點和縱軸錨點座標三個參數。oop
開始頁面比較簡潔,有一個LOGO和兩個按鈕分別是開始遊戲,更多遊戲。
/**遊戲開始場景 */ class StartGameLayer extends egret.Sprite { /**開始按鈕 */ private startBtn:MyButton; /**更多按鈕 */ private moreBtn:MyButton; /**LOGO */ private titleImage:egret.Bitmap; public constructor() { super(); this.init(); } private init():void { /**添加遊戲LOGO */ this.titleImage = GameConst.createBitmapFromSheet("logo_mishitaosheng", "ui"); this.titleImage.x = 51; this.titleImage.y = 161; this.addChild(this.titleImage); //開始按鈕設置 this.startBtn = new MyButton("btn_y", "btn_kaishi"); this.addChild(this.startBtn); this.startBtn.x = (GameConst.StageW - this.startBtn.width) / 2; this.startBtn.y = GameConst.StageH / 2 - 75; this.startBtn.setClick(this.onStartGameClick); //更多按鈕設置 this.moreBtn = new MyButton("btn_b", "btn_gengduo"); this.moreBtn.x = (GameConst.StageW - this.startBtn.width) / 2; this.moreBtn.y =GameConst.StageH / 2 + 75; this.addChild(this.moreBtn); this.moreBtn.setClick(this.onMoreBtnClick); //文本 let tex:egret.TextField = new egret.TextField(); tex.width = GameConst.StageW; tex.textAlign = egret.HorizontalAlign.CENTER; tex.strokeColor = 0x403e3e; tex.stroke = 1; tex.bold = true; tex.y = GameConst.StageH / 2 + 250; tex.text = "Powered By ShenYSun"; this.addChild(tex); } private onStartGameClick() { GameControl.Instance.onGameScenesHandler(); } private onMoreBtnClick() { console.log("更多遊戲"); platform.GetInfo(); } }
/**遊戲管理 */ class GameControl extends egret.Sprite { private static _instance:GameControl; public static get Instance() { if(!GameControl._instance) { GameControl._instance = new GameControl(); } return GameControl._instance; } /**當前場景 */ private currentStage:egret.DisplayObjectContainer; //開始遊戲 private startGame:StartGameLayer; /**遊戲場景 */ private gameScenes:GameScenesLayer; /**結束場景 */ private overScenes:GameOverLayer; /**背景 */ private bgImg:egret.Bitmap; public constructor() { super(); this.startGame = new StartGameLayer(); this.gameScenes = new GameScenesLayer(); this.overScenes = new GameOverLayer(); } public setStageHandler(stage:egret.DisplayObjectContainer):void { /**設置當前場景的背景 */ this.currentStage = stage; this.bgImg = GameConst.CreateBitmapByName("bg_jpg"); this.bgImg.width = GameConst.StageW; this.bgImg.height = GameConst.StageH; //把背景添加到當期場景 this.currentStage.addChild(this.bgImg); } /**開始遊戲的場景 */ public startGameHandler():void { if(this.gameScenes && this.gameScenes.parent) { GameConst.removeChild(this.gameScenes); } if(this.gameScenes && this.overScenes.parent) { GameConst.removeChild(this.overScenes); } this.currentStage.addChild(this.startGame); GameApp.xia.visible = true; } /**遊戲場景 */ public onGameScenesHandler():void { if(this.startGame && this.startGame.parent) { GameConst.removeChild(this.startGame); } if(this.overScenes && this.overScenes.parent) { GameConst.removeChild(this.overScenes); } this.currentStage.addChild(this.gameScenes); GameApp.xia.visible = false; } /**遊戲結束場景 */ public showGameOverSceneHandler():void{ if(this.startGame && this.startGame.parent){ GameConst.removeChild(this.startGame) } if(this.gameScenes && this.gameScenes.parent){ GameConst.removeChild(this.gameScenes) } this.currentStage.addChild(this.overScenes); GameApp.xia.visible = true; } public getGameOverDisplay():GameOverLayer { return this.overScenes; } }
場景切換貫穿遊戲全局,封裝成類方便調用,以及後期擴展只須要加上新場景類的實例,即可以切換自如。
不難發現上面的開始遊戲界面的按鈕是MyButton
類型,在MyButton
類的構造函數中傳入背景圖和顯示文字,建立出一個按鈕。此類有一個設置點擊事件的方法,按鈕調用此公開方法傳入觸發事件便可設置點擊事件。
/**自定義按鈕類 */ class MyButton extends egret.Sprite { private _bg:egret.Bitmap; private title:egret.Bitmap; private onClick:Function; public constructor(bgName:string, titleName:string) { super(); this._bg = GameConst.createBitmapFromSheet(bgName, "ui"); this.addChild(this._bg); this.title = GameConst.createBitmapFromSheet(titleName, "ui"); this.title.x = (this._bg.width - this.title.width) >> 1; this.title.y = (this._bg.height - this.title.height) >> 1; this.addChild(this.title); } //設置點擊觸發事件 public setClick(func:Function):void { this.touchEnabled = true; this.addEventListener(egret.TouchEvent.TOUCH_TAP, this.onClickEvent, this); this.onClick = func; } //點擊觸發的事件 private onClickEvent() { this.onClick(); } public setTitle(title:string):void { this.title = GameConst.CreateBitmapByName(title); } public get bg() { return this._bg; } public set bg(bg:egret.Bitmap) { this._bg = bg; } }
通常遊戲中的分數、時間等數字組成的UI爲了美觀都會使用位圖文本,可是當遊戲邏輯跑起來須要不斷的刷新遊戲的分數,每次改變分數的時候都要從紋理集裏面調用對應位圖,在時間上是一個大大的浪費,因此建立一個特殊字符類SpecialNumber
,讓這個類替咱們實現轉換特殊字符。
具體代碼以下:
/**特殊字符數字類 */ class SpecialNumber extends egret.DisplayObjectContainer { public constructor() { super(); } public gap:number = 0; /**設置顯示的字符串 */ public setData(str:string):void { this.clear(); if(str == "" || str == null) { return; } //把全部數字每個都存進數組中 let chars:Array<string> = str.split(""); let w:number = 0; //全部的長度 let length:number = chars.length; for(let i:number = 0; i < length; i++) { try { let image:egret.Bitmap = GameConst.createBitmapFromSheet(chars[i], "ui"); if(image) { image.x = w; w += image.width + this.gap; this.addChild(image); } } catch (error) { console.log(error); } } this.anchorOffsetX = this.width / 2; } public clear() { while(this.numChildren) { this.removeChildAt(0); } } }
在體驗過遊戲的時候會發現任務會根據不同的牆體高度擺不同的poss,這才poss全是來自於幀動畫紋理集,只須要把對應一套的動畫解析出來人物就會跳起舞來。下面是人物狀態類。
人物共有五個狀態,其中一個是默認狀態state
爲跳舞狀態STAGE1
,還有設置當前狀態的方法setState
/**角色動做類 */ class Role extends egret.Sprite{ //狀態 public static STATE1:number = 0; public static STATE2:number = 1; public static STATE3:number = 2; public static STATE4:number = 3; public static STATE5:number = 4; /**人物狀態集合 */ public static FRAMES:Array<any> = [ ["0020003", "0020004", "0020005", "0020006","0020007"], ["0020008"], ["0020009", "0020010"], ["0020011", "0020012"], ["xue0001", "xue0002", "xue0003", "xue0004", "xue0005"] ] //身體 private Body:egret.Bitmap; private state:number; private currFrames:Array<any>; private currFramesIndex:number = 0; private runFlag:number; private isLoop:boolean; public constructor() { super(); this.Body = new egret.Bitmap; //人物初始狀態 this.Body = GameConst.createBitmapFromSheet("Role.FRAMES[0][0]", "Sprites"); //設置錨點 this.Body.anchorOffsetX = this.Body.width * 0.5; this.addChild(this.Body); } /**設置狀態 */ public setState(state:number) :void { this.state = state; //死亡狀態 if(this.state == Role.STATE5) { this.isLoop = false; this.Body.anchorOffsetY = this.Body.height * 0; }else{ this.isLoop = true; this.Body.anchorOffsetY = this.Body.height * 1; } if(this.state == Role.STATE3 || this.state == Role.STATE4){ this.currFrames = []; if(Math.random() > 0.5){ this.currFrames.push(Role.FRAMES[this.state][0]); }else{ this.currFrames.push(Role.FRAMES[this.state][1]); } }else{ this.currFrames = Role.FRAMES[this.state]; } this.currFramesIndex = 0; this.setBody(); } private setBody() { this.Body.texture = GameConst.getTextureFromSheet(this.currFrames[this.currFramesIndex], "Sprites"); this.Body.anchorOffsetX = this.Body.width * 0.5; if(this.state == Role.STATE5){ this.isLoop = false; this.Body.anchorOffsetY = this.Body.height * 0; }else{ this.isLoop = true; this.Body.anchorOffsetY = this.Body.height * 1; } } public run():boolean{ this.runFlag ++; if(this.runFlag > 4){ this.runFlag = 0; } if(this.runFlag != 0){ return; } var gotoFrameIndex:number = this.currFramesIndex + 1; if(gotoFrameIndex == this.currFrames.length){ if(this.isLoop){ gotoFrameIndex = 0; }else{ gotoFrameIndex = this.currFramesIndex; } } if(gotoFrameIndex != this.currFramesIndex){ this.currFramesIndex = gotoFrameIndex; this.setBody(); } return false; } public play():void{ egret.startTick(this.run,this); this.runFlag = 0; } public stop():void{ egret.stopTick(this.run,this); } }
一切工做準備就緒,下面就是本文的重點-----遊戲場景的搭建以及邏輯的實現。先看一下游戲內的主要內容
首先是藍色的遊戲背景,和開始遊戲界面背景一模一樣不用更換,在場景管理的時候注意背景保留一下繼續使用。
其次分數、關卡、上背景圖等等這些只須要調用常量類的獲取紋理集圖片的方法調整位置便可實現。
最後重點介紹一下內容:
牆體生成和運動
人物運動和狀態切換
分數和關卡數改變並記錄最高分數
下面是重要代碼片斷
牆體分別包括上半部分和下半部分
/**上部分牆體容器 */ private topContianer:egret.Sprite; /**下部分牆體容器 */ private bottomContianer:egret.Sprite;
容器內又包含了上下部分的牆體圖片,上下邊界線
/**上下牆體填充圖 */ private topSprite:egret.Sprite; private bottomSprite:egret.Sprite; /**上下邊界線 */ private topLine:egret.Shape; private bottomLine:egret.Shape;
把填充圖和邊界線加到容器內(以上邊界爲例)
this.topContianer = new egret.Sprite(); this.addChild(this.topContianer); this.topSprite = new egret.Sprite(); this.topContianer.addChild(this.topSprite); this.topContianer.addChild(this.topLine);
定義一個top和bottom範圍區間,隨機在舞臺範圍內取值。
let min:number = 150; let flag:boolean = false; let len:number = 8; let w:number = GameConst.StageW / len; for(let i:number = 0; i < len; i++) { var h:number = min + Math.floor(Math.random() * 8) * 10; this.bottomRects.push(new egret.Rectangle(i * w, GameConst.StageH - h, w, h)); h = GameConst.StageH - h; if (Math.random() < 0.2 || (!flag && i == len - 1)) { var index:number = Math.floor(Math.random() * this.spaceArr.length); h -= this.spaceArr[index]; flag = true; } this.topRects.push(new egret.Rectangle(i * w, 0, w, h)); }
這是隨機取區域已經完成,不過都是理想的區域,並無填充實際上的圖片,下面寫一個方法經過區域來填充背景牆。
private fullFront(bgSptite:egret.Sprite, rects:Array<egret.Rectangle>, isBottom:boolean = false):void { bgSptite.cacheAsBitmap = false; this.clearBg(bgSptite); var len:number = rects.length; for (var i:number = 0; i < len; i++) { var rec:egret.Rectangle = rects[i]; var bitmap:egret.Bitmap; if (this.bgBitmaps.length) { bitmap = this.bgBitmaps.pop(); } else { bitmap = new egret.Bitmap(); bitmap.texture = this.bg; } bitmap.scrollRect = rec; bitmap.x = rec.x; bitmap.y = rec.y; bgSptite.addChild(bitmap); } }
關鍵代碼bitmap.scrollRect = rec
是把位圖按照區域進行分割,顯示對象的滾動矩形範圍。顯示對象被裁切爲矩形定義的大小,當您更改 scrollRect 對象的 x 和 y 屬性時,它會在矩形內滾動。
上下背景位圖填充完畢,下面但是畫上下邊界線,一樣是寫了一個方法(以上邊界爲例),以下:
private drawLine():void { var lineH:number = 10; this.topLine.graphics.clear(); this.topLine.graphics.lineStyle(lineH, 0x33E7FE); this.bottomLine.graphics.clear(); this.bottomLine.graphics.lineStyle(lineH, 0x33E7FE); this.drawTopLine(lineH / 2); this.drawBottomLine(lineH / 2); this.topLine.graphics.endFill(); this.bottomLine.graphics.endFill(); } private drawTopLine(lineH:number):void { var len:number = this.topRects.length; for (var i:number = 0; i < len; i++) { var rec:egret.Rectangle = this.topRects[i]; if (i == 0) { this.topLine.graphics.moveTo(rec.x, rec.height); this.topLine.graphics.lineTo(rec.x + rec.width, rec.height); } else { this.topLine.graphics.lineTo(rec.x, rec.height); this.topLine.graphics.lineTo(rec.x + rec.width, rec.height); } } }
let self = this; setTimeout(function() { // self.shakeRun(); //上面的模塊往下運動 egret.Tween.get(this.topContianer).to({"y":0}, 100).call(function():void { self.landOver(); }) }, 1500);
人物運動:給舞臺添加點擊事件,判斷點擊位置並移動。
/**點擊事件 */ private onClick(e:egret.TouchEvent):void { let len:number = this.bottomRects.length; for(let i:number = 0; i < len; i++) { let rec:egret.Rectangle = this.bottomRects[i]; if(e.stageX > rec.x && e.stageX < rec.x + rec.width) { this.setRolePos(i); break; } } }
private setRolePos(index:number, offY:number = 17, offX:number = 0, isInit:boolean = false):void { if (!isInit) { //人物每次移動一個格子 if (this.rolePosIndex > index) { index = this.rolePosIndex - 1; } else if (this.rolePosIndex < index) { index = this.rolePosIndex + 1; } } this.rolePosIndex = index; var rec:egret.Rectangle = this.bottomRects[index]; //一次只移動一格 this.role.x = rec.x + rec.width / 2 + offX; this.role.y = rec.y + offY; }
狀態切換:
牆體運動完畢以後,經過人物所在位置下標找到上半部分牆體和下半部分牆體對應的位置的差值,並根據差值判斷人物是否存活,若是存活應該表現出什麼狀態。
獲取人物所在位置上下牆體的距離:
privategetSpace():number{ lettop:egret.Rectangle=this.topRects[this.rolePosIndex]; letbottom:egret.Rectangle=this.bottomRects[this.rolePosIndex]; returnGameConst.StageH-top.height-bottom.height; }
根據返回的距離差值判斷人物的狀態:
privatecheckState() { letspace:number=this.getSpace(); if(space==0) { this.role.setState(Role.STATE5); } elseif(space==this.spaceArr[2]) { this.role.setState(Role.STATE4); } elseif(space==this.spaceArr[0]) { this.role.setState(Role.STATE3); } elseif(space==this.spaceArr[1]) { this.role.setState(Role.STATE2); } if(space==0) { this.setRolePos(this.rolePosIndex, -10, 4); } }
根據返回的距離判斷遊戲狀態,若返回值爲0,遊戲結束;不爲0,進入下一關:
/**檢驗這關結束主角是否存活 */ privatecheckResult() { letspace:number=this.getSpace(); letself=this; if(space==0) { this.dieNum++; if(this.dieNum==1) { this.role.stop(); setTimeout(function() { //遊戲結束 GameControl.Instance.getGameOverDisplay().setGameOverDataHandler(self.score, self.curretMaxScore); GameControl.Instance.showGameOverSceneHandler(); }, 500); return; } } //進入下一關 else{ this.curretLevel++; this.score+=10; if(this.score>this.curretMaxScore) { this.curretMaxScore=this.score; } //刷新成績 this.refreshScore(); } setTimeout(function() { self.refurbish() }, 1000); }
接着上一步此時若是人物存活進入下一關,那麼就要刷新遊戲成績和關卡數,並檢驗此時是否爲最高成績:
/**刷新成績數據 */ privaterefreshScore() { this.LvNum.setData(this.curretLevel.toString()); this.recodeNum.setData(this.score.toString()); }
遊戲進入下一關卡:
/**刷新遊戲關卡 */ privaterefreshPoint() { this.initData(); this.start(); }
遊戲結算界面效果圖
和開始界面差很少,有所不一樣的是須要從遊戲場景中傳入本局分數和作高分數,在這個頁面寫一個公開的setGameOverDataHandler
方法,遊戲結束是調用此方法傳入數值。
/**遊戲結束頁面分數最高分數 */ publicsetGameOverDataHandler(score:number=0, maxScore:number=0):void{ this.scoreNum.setData(score.toString()); this.maxScore.setData(maxScore.toString()); }
遊戲場景內當牆體要下落的時候牆體會晃動一下,晃動牆體不只提醒玩家這個牆體即將下落,同時也增長了這個遊戲的可玩性,下面是控制牆體晃動的Shake
類。
/**牆體晃動 */ classShake{ privateinitY:number; privateshakeNum:number; privateoverFunc:Function; privateobj:egret.DisplayObject; privatenum:number; privateflag:number; publicrun(obj:egret.DisplayObject, shakeNum:number, overFunc:Function=null) { this.obj=obj; this.initY=obj.y; this.shakeNum=shakeNum; this.overFunc=overFunc; egret.startTick(this.loop, this); this.num=0; this.flag=0; } privateloop():boolean{ if(this.flag==0) { if(this.obj.y<=this.initY) { this.obj.y+=5; } else{ this.obj.y-=5; } if(this.obj.y==this.initY) { this.num++; if(this.num==this.shakeNum) { egret.stopTick(this.loop, this); if(this.overFunc) { this.overFunc(); } } } } this.flag++; if(this.flag==2) { this.flag=0; } returnfalse; } }
小結
本文經過對一個簡單小遊戲進行模塊化的分析,並介紹了模塊化的好處,下降這些模塊之間的耦合度,後期若是要加新功能對舊的代碼無需進行大的修改。