寫在前面
隨着越來越多的新人開始接觸白鷺引擎,創作屬於自己的游戲。考慮到初學者會遇到的一些實際操作問題,我們近期整理推出《菜鳥教程》系列的文檔,以便更好的讓這些開打着們快速上手,Egret大神們可以忽略此類內容。本文作者是我們技術支持部門的同事“熊貓少女”。看文的小伙伴如果有問題可以來白鷺官方論壇與之交流。
EUI是一套基於Egret核心顯示列表的UI擴展庫,它封裝了大量的常用UI組件,能夠滿足大部分的交互界面需求,即使更加復雜的組件需求,您也可以基於EUI已有組件進行組合或擴展,從而快速實現需求。為了展示EUI的功能,我借助白鷺官網中的一個卡牌游戲DEMO,從零開始編寫完成這個DEMO。其實這並不是一個完整的游戲,只是一個演示DEMO,親自實現這個DEMO之后,應該能熟悉大部分的EUI操作,並且增加對egret游戲開發的理解。
點進去之后可以體驗一下,把所有地方都點一點,感受一下所有的功能,接下來我們就要自己去動手實現了。(這里可以下載源碼素材)不過這里的源碼是編譯之后的並不是源代碼,不過我們只需要用到里面的素材,具體實現就讓我一步一步自己來吧~~~
初始化
新建好EUI項目(480 * 800),把剛剛下載的文件里面的 source > resource > art 這個文件夾放在我們自己新建的項目中
按照之前的流程,現在刪除新建的項目中多余的代碼
上面還有個函數的調用要記得一起刪掉
現在項目干凈了~
Let's do it!
場景搭建
EUI最方便的地方就是能快速的搭建界面,只需要用鼠標拖動就可以搭建靜態的界面。先來把游戲的素材放到加載組里
打開default.res.json
新建兩個資源組分別放hero_goods和loading,其他的都放到preload組就好
現在稍微分析一下
這個游戲一共有五個場景:
主場景
玩家場景
英雄場景
物品場景
關於場景
loading
在主場景之前,其實還有一個loading需要先顯示出來,這樣用戶就不會看到一片黑乎乎的加載過程,提高用戶體驗
根據我們之前設置的幾個資源組,preload是游戲初始化需要加載的資源,我們的loading則需要在它之前就加載好,這樣用戶就能先看到loading頁,so現在去寫個loading
項目其實自帶了一個很簡陋的loading,我們可以再這個基礎上來寫
打開main.ts
先加載這個資源組里面的資源,然后再把loadingView添加到舞台
打開loadingUi.ts
class LoadingUI extends egret.Sprite implements RES.PromiseTaskReporter { public constructor() { super(); // 當被添加到舞台的時候觸發 (被添加到舞台,說明資源組已經加載完成) this.addEventListener(egret.Event.ADDED_TO_STAGE, this.createView, this) } private textField: egret.TextField; // 文本 private bgImg: egret.Bitmap // 背景圖 private loadImg: egret.Bitmap // loading圖標 private createView(): void { this.width = this.stage.stageWidth this.height = this.stage.stageHeight // 背景圖 this.bgImg = new egret.Bitmap() this.bgImg.texture = RES.getRes('loading_jpg') this.addChild(this.bgImg) // loading圖標 this.loadImg = new egret.Bitmap() this.loadImg.texture = RES.getRes('loading2_png') this.loadImg.anchorOffsetX = this.loadImg.width / 2 this.loadImg.anchorOffsetY = this.loadImg.height / 2 this.loadImg.x = this.width / 2 this.loadImg.y = this.height / 2 this.addChild(this.loadImg) // 文本 this.textField = new egret.TextField(); this.addChild(this.textField); this.textField.width = 480; this.textField.height = 20 this.textField.y = this.height / 2 - this.textField.height / 2; this.textField.size = 14 this.textField.textAlign = "center"; // 監聽幀事件,每幀都讓loading圖片轉動 this.addEventListener(egret.Event.ENTER_FRAME, this.updata, this) } private updata() { // 旋轉 this.loadImg.rotation += 5 } // 這個函數在加載中會自動調用 public onProgress(current: number, total: number): void { // 計算百分比 let per = Math.floor((current / total) * 100) this.textField.text = `${per}%`; } }
loading實現完成~現在調試看看,在終端輸入 egret run -a(-a 表示修改代碼保存后自動編譯,你只需要在瀏覽器刷新就可以看到修改后的效果)
能看到loading頁面了~
主場景
主場景就是我們進入游戲的時候看到的第一個場景,其他的四個場景就是在點擊下面不同的按鈕的時候添加到當前的主場景上就好啦~
首先我們來把幾個場景的組件搞定。
- 1. 在resource目錄下新建一個skins目錄,用來存放我們創建的皮膚
- 2. 在scr目錄下單擊右鍵,新建一個EUI組件
創建主場景
相對於我們之前先創建exml再創建對應的ts文件要方便很多,而且這樣不需要在ts文件中指定skinName,因為直接創建EUI組件的時候它在配置文件中已經幫你指定好了。
可以在default.thm.json文件里面看到
現在點開自動生成的MainScene.exml
設置好寬高 480 * 800
把圖片拖進來,然后再快捷約束里面點擊左對齊和上對齊,圖片就自動調整好了
現在需要去設置場景下方的四個按鈕
每個按鈕都有兩種狀態,正常和按下。 這里需要單獨去新建單個按鈕的皮膚,下面以玩家按鈕為例
新建mbtnPlayer.exml ,設置寬高 111 * 80點擊下面狀態旁邊的 + 號,給這個皮膚設置不同的狀態up是正常狀態, down是按下狀態, disabled是禁用狀態這里我們先設置up狀態
在up狀態中,把正常狀態的背景圖和按鈕圖片拖進來。
同理,把按下狀態也搞定。(記得按下狀態的背景圖不一樣)
現在我們得到了一個自定義的組件皮膚,回到 MainScene.exml,為了更好管理四個按鈕,所以先拖進來一個Group,並給他一個id Group_mbtn
給這個Group設置好布局,一會兒里面的按鈕就會自動排列好,不用手動去拖動
往Group中放置一個 ToggleButton ,把皮膚換成我們剛剛自定義的mbtnPlayer
照葫蘆畫瓢,把其他三個都搞定吧~
主場景搭建完畢
做完了主場景,現在開始寫一些關於主場景的邏輯
上面忘了說要記得給那幾個按鈕設置id
打開 MainScene.ts
class MainScene extends EUI.Component implements EUI.UIComponent { public Group_mbtn:EUI.Group; public mgtnPlayer:EUI.ToggleButton; public mbtnHero:EUI.ToggleButton; public mbtnGoods:EUI.ToggleButton; public mbtnAbout:EUI.ToggleButton; public constructor() { super(); } protected partAdded(partName:string,instance:any):void { super.partAdded(partName,instance); } protected childrenCreated():void { super.childrenCreated(); // 讓Group可以點擊 this.Group_mbtn.touchEnabled = true // 事件委托, 點擊按鈕的時候觸發toggleBtn this.Group_mbtn.addEventListener(egret.TouchEvent.TOUCH_TAP, (e)=> { let theBtn = <EUI.ToggleButton>e.target // 在點擊觸發這個事件的時候,點擊的那個btn已經變成了選中狀態 // 判斷theBtn是否存在theBtn.selected屬性且為true if (theBtn.selected && theBtn.selected != undefined) { this.toggleBtn(theBtn) } else { // 當selected為false的時候,說明按鈕在點擊之前就是選中狀態 // 點擊后變成了false,所以這里改回選中狀態 theBtn.selected = true } }, this) } /** * 切換按鈕 */ public toggleBtn(btn:EUI.ToggleButton) { // 先把所有的按鈕都設置為不選中 for (let i = 0; i < this.Group_mbtn.numChildren; i++) { let theBtn = <EUI.ToggleButton>this.Group_mbtn.getChildAt(i) theBtn.selected = false } // 把傳進來的btn設置為選中狀態 btn.selected = true } }
在Main.ts里,把主場景添加到舞台
protected createGameScene(): void { this.addChild(new MainScene()) }
現在運行就可以看到切換按鈕的效果
玩家場景
現在開始創建 玩家場景 PlayerScene 組件
設置寬高 680*800, 拖拽圖片和button
注意紅色框的是button
這三個按鈕在點擊的時候會變大,這個效果我們可以在制作皮膚的時候就完成
首先用鼠標點中返回按鈕,然后點擊左上方的 源碼
這一段就是那個返回按鈕的源碼,修改成下圖
width.down = "100%" 表示當按鈕按下的時候寬度為 100%,其他情況下寬度90%
horizontalCenter="0" verticalCenter="0" 表示讓圖片以中心放大
可以看到紅框部分是一個可以拖動的窗口,所以我們需要放置一個 scroller
把scroller里面自帶的group刪掉,加上一個數據容器list
現在創建list里面的某一個子項的皮膚 zhuangbeiItem.exml
寬高設為 87*130 ,拖入一張圖片和一個labl控件還有一個image控件,設置好label的樣式
這個皮膚里面的label和image控件的值都是需要我們去提供的
需要在標簽里面寫 {data.xx} 其實很像js里面某些框架的插值寫法
給label 的標簽寫上 {data.name} , 給image 的資源名寫上 {data.image}
裝備item完成,回到剛剛的PLayerScene, 把list的條目皮膚設置為剛剛創建的裝備item
把list布局設置成水平布局
給scroller和list都取個id,便於后面使用
這個scroller只需要左右拖動,所以我們去 ‘所有屬性’ 打開它的水平滾動,關掉垂直滾動
打開PLayerScene.ts
class PlayerScene extends EUI.Component implements EUI.UIComponent { public btn_return:EUI.Button; public btn_zhuangbei:EUI.Button; public btn_qianghua:EUI.Button; public scr_zhuangbei:EUI.Scroller; public list_zhuangbei:EUI.List; public constructor() { super(); } protected partAdded(partName:string,instance:any):void { super.partAdded(partName,instance); } protected childrenCreated():void { super.childrenCreated(); // 數組數據 let dataArr:any[] = [ {image:"resource/art/profile/skillIcon01.png",name:"旋龍幻殺"}, {image:"resource/art/profile/skillIcon02.png",name:"魔魂天咒"}, {image:"resource/art/profile/skillIcon03.png",name:"天魔舞"}, {image:"resource/art/profile/skillIcon04.png",name:"痴情咒"}, {image:"resource/art/profile/skillIcon05.png",name:"無間寂"}, {image:"resource/art/profile/skillIcon06.png",name:"霸天戮殺"}, {image:"resource/art/profile/skillIcon07.png",name:"滅魂狂飆"} ] // 把數組數據轉成EUI數組 let EUIArr:EUI.ArrayCollection = new EUI.ArrayCollection(dataArr) // 把EUI數組作為list的數據源 this.list_zhuangbei.dataProvider = EUIArr // 隱藏進度條 this.scr_zhuangbei.horizontalScrollBar.autoVisibility = false } }
到這里, 玩家場景也創建完成啦~
場景管理類
我們有了兩個場景,可以來做場景之間的切換了
還記得之前說的思路嗎,舞台上先有一個主場景,然后點擊不同按鈕的時候把對應的場景添加到主場景上
這里有個需要注意的地方,子場景添加進來默認層級是高於主場景的,所以會把主場景給擋住了,而我們需要點擊主場景的按鈕。所以我們需要把主場景中放置按鈕的Group的層級提高。
我用一個場景管理類來管理這些場景
新建一個SceneManager.ts,采用的是單例模式,要使用這個類的時候不要new SceneManager實例,而是用 SceneManager.instance 來獲取到這個類的實例
這樣可以保證場景管理類有且只有一個實例,便於管理操作
(使用 static修飾的方法都是靜態方法,簡單的說就是調用的時候不是通過實例調用,而是直接用類名來調用, 類名.方法名)
下面是一個基礎的場景管理類,現在來逐步完善它的功能
/**
* 場景管理類
*/
class SceneManager { private _stage:egret.DisplayObjectContainer // 設置所有場景所在的舞台(根) private mainScene:MainScene //主場景 private playerScene:PlayerScene //玩家場景 // 在構造函數中創建好場景,保存到屬性 constructor() { this.mainScene = new MainScene() this.playerScene = new PlayerScene() } /** * 獲取實例 */ static sceneManager:SceneManager static get instance(){ if(!this.sceneManager) { this.sceneManager = new SceneManager() } return this.sceneManager } /** * 設置根場景 */ public setStage(s:egret.DisplayObjectContainer) { this._stage = s } // 這里補充代碼…… }
首先需要管理的場景是主場景,SceneManager.instance.mainScene是獲取到主場景的實例
SceneManager.instance 獲取到場景管理類的實例
然后再.mainScene 獲取到構造方法constructor中的 this.mainScene = new MainScene() 得到的主場景 的實例
/** * 主場景 */ static toMainScene() { let stage:egret.DisplayObjectContainer = this.instance._stage // (根) 舞台 let mainScene = SceneManager.instance.mainScene // 主場景 // 判斷主場景是否有父級(如果有,說明已經被添加到了場景中) if(!mainScene.parent){ // 未被添加到場景 // 把主場景添加到之前設置好的根舞台中 stage.addChild(mainScene) } }
現在到main.ts中使用場景管理類來加載主場景
/** * 創建場景界面 * Create scene interface */ protected createGameScene(): void { // 把this設置為場景管理器的根舞台 SceneManager.instance.setStage(this) // 調用SceneManager的靜態方法 SceneManager.toMainScene() }
現在運行,可以看到主場景已經被添加到舞台中了
現在只有一個場景,需要把第二個場景也加進來,並實現切換
在場景管理類中再添加一個靜態方法
/** * 玩家場景 */ static toPlayerScene() { let stage:egret.DisplayObjectContainer = this.instance._stage // 把玩家場景添加到主場景中 this.instance.mainScene.addChild(this.instance.playerScene) }
當在主場景點擊玩家按鈕的時候,調用這個方法,切換到玩家場景
這里需要稍微改動一下之前的點擊函數
打開MainScene.ts
/** * 切換按鈕 * @param btn 參數是EUI.ToggleButton的時候切換按鈕, 參數是0的時候設置為全部不選中 */ public toggleBtn(btn:EUI.ToggleButton | number) { // 先把所有的按鈕都設置為不選中 for (let i = 0; i < this.Group_mbtn.numChildren; i++) { let theBtn = <EUI.ToggleButton>this.Group_mbtn.getChildAt(i) theBtn.selected = false } if(btn===0){ return } // 把傳進來的btn設置為選中狀態 btn = <EUI.ToggleButton>btn btn.selected = true // 獲取當前點擊的按鈕的下標, 用來實現不同按鈕對應的功能 // 0 1 2 3 對應 玩家, 英雄, 物品, 關於 let index = this.Group_mbtn.getChildIndex(<EUI.ToggleButton>btn) switch (index) { case 0: // 調用靜態方法切換到玩家場景 SceneManager.toPlayerScene() // 把按鈕的層級提高 // this.numChildren表示所有的子元素數量 this.setChildIndex(this.Group_mbtn, this.numChildren) break default: break } }
點擊玩家按鈕就可以正常切換到玩家場景了,現在來實現其中的返回按鈕
點擊返回按鈕,回到主場景,並且下面的按鈕全都變成未選中狀態
打開PlayerScene.ts
// 給返回按鈕添加事件 this.btn_return.addEventListener(egret.TouchEvent.TOUCH_TAP, this.returnMain, this) /** * 回到主場景 */ private returnMain() { SceneManager.toMainScene() } 點擊按鈕跳轉回到主場景,其實就是刪除掉當前覆蓋在主場景上的玩家場景,主場景就能顯示出來了 so,打開SceneManager.ts,完善一下剛剛的函數 /** * 主場景 */ static toMainScene() { let stage:egret.DisplayObjectContainer = this.instance._stage // (根) 舞台 let mainScene = SceneManager.instance.mainScene // 主場景 // 取消所有按鈕的選中狀態 mainScene.toggleBtn(0) // 判斷主場景是否有父級(如果有,說明已經被添加到了場景中) if(!mainScene.parent){ // 未被添加到場景 // 把主場景添加到之前設置好的根舞台中 stage.addChild(mainScene) } // 判斷玩家場景是否有父級(是否在場景中) if(SceneManager.instance.playerScene.parent) { // 如果有就刪除玩家場景 mainScene.removeChild(SceneManager.instance.playerScene) } }
保存文件
去瀏覽器里就能看到效果了~
英雄場景
開始制作英雄場景 HeroScene.exml
到源碼部分,修改一下兩個按鈕的效果
在中間放置一個Scroller,然后里面放一個List,
跟之前玩家場景其實差不多啦,現在去創建 heroListItem.exml
這里需要注意,有個checkBox,用來選中某個list
把里面的數據插值寫好 {data.image} {data.name} {data.value}
checkBox是個例外,它的值不能用{data.xx}的方式來指定,我們需要創建一個單獨的類
Herolist_item.ts
// 必須要繼承自 EUI.ItemRenderer class HeroList_item extends EUI.ItemRenderer{ // 選擇框 public ce_select:EUI.CheckBox; public constructor() { super() // 把這個 類和皮膚 聯系起來 this.skinName = 'resource/skins/skins_item/heroListItem.exml' } // 當數據改變時,更新視圖 protected dataChanged() { // isSeleted 是我們提供數據的某個字段 this.ce_select.selected = this.data.isSelected } } 回到HeroScene.exml, 把list的條目皮膚設置為heroListItem,並給他們設置好id 在所有屬性里面把水平滾動關掉,垂直滾動打開 打開英雄場景HeroScene.ts class HeroScene extends EUI.Component implements EUI.UIComponent { public btn_return:EUI.Button; public btn_select:EUI.Button; public scr_hero:EUI.Scroller; public list_hero:EUI.List; public constructor() { super(); } protected partAdded(partName:string,instance:any):void { super.partAdded(partName,instance); } protected childrenCreated():void { super.childrenCreated(); // 原始數組 let dataArr:any[] = [ {image: 'resource/art/heros_goods/heros01.png', name: '亞特伍德', value: '評價: 很特么厲害, 為所欲為', isSelected: false}, {image: 'resource/art/heros_goods/heros02.png', name: '亞特伍德', value: '評價: 很特么厲害, 為所欲為', isSelected: false}, {image: 'resource/art/heros_goods/heros03.png', name: '亞特伍德', value: '評價: 很特么厲害, 為所欲為', isSelected: true}, {image: 'resource/art/heros_goods/heros04.png', name: '亞特伍德', value: '評價: 很特么厲害, 為所欲為', isSelected: false}, {image: 'resource/art/heros_goods/heros05.png', name: '亞特伍德', value: '評價: 很特么厲害, 為所欲為', isSelected: false}, {image: 'resource/art/heros_goods/heros06.png', name: '亞特伍德', value: '評價: 很特么厲害, 為所欲為', isSelected: false}, {image: 'resource/art/heros_goods/heros07.png', name: '亞特伍德', value: '評價: 很特么厲害, 為所欲為', isSelected: false} ] // 轉成EUI數組 let EUIArr:EUI.ArrayCollection = new EUI.ArrayCollection(dataArr) // 把list_hero數據源設置成EUIArr this.list_hero.dataProvider = EUIArr // 設置list_hero的項呈視器 (這里直接寫類名,而不是寫實例) this.list_hero.itemRenderer = HeroList_item } }
現在運行一下,能看到列表已經能正確的加載,而且數組中isSeleted字段為true的第三項也被默認選中了。
現在來實現手動更改列表的選中狀態
Herolist_item.ts
// 必須要繼承自 EUI.ItemRenderer class HeroList_item extends EUI.ItemRenderer{ // 選擇框 public ce_select:EUI.CheckBox; public constructor() { super() // 把這個 類和皮膚 聯系起來 this.skinName = 'resource/skins/skins_item/heroListItem.exml' // 當組件創建完成的時候觸發 this.addEventListener(EUI.UIEvent.CREATION_COMPLETE, this.onComplete, this) } private onComplete() { // 當select的選中狀態發生改變的時候觸發 this.ce_select.addEventListener(EUI.UIEvent.CHANGE, (e) => { // this.data 就是綁定的數據, this.data.isSelected = this.ce_select.selected // 把數據打印出來看看 console.log(this.data); }, this) } // 當數據改變時,更新視圖 protected dataChanged() { // isSeleted 是我們提供數據的某個字段 this.ce_select.selected = this.data.isSelected } }
這里可能稍微有點兒繞,需要花一點時間好好理解一下代碼執行的流程。
現在點擊checked框的時候就能正確的修改數據的isSelected了
繼續完成返回和確定選擇的按鈕
返回按鈕其實就是把場景切換到主場景去,在場景控制類中寫個方法就好
確定選擇的按鈕其實也是要切換回到主場景,再獲取一下數據里面isSelected為true的項,並把它們顯示到屏幕上
設置返回按鈕,直接在點擊事件中調用場景管理的方法就好了,把按鈕的選擇狀態都清除
HeroScene.ts
// 點擊返回按鈕,回到主場景 this.btn_return.addEventListener(egret.TouchEvent.TOUCH_TAP, (e)=>{ SceneManager.toMainScene() SceneManager.instance.mainScene.toggleBtn(0) }, this) 選擇功能,點擊按鈕的時候獲取到數據源中的isSelected為true的項都保存到數組中,然后把這個數組作為參數傳到場景管理里面。拿到數組就創建對應的消息顯示出來就好了 HeroScene.ts // 點擊確定按鈕,回到主場景,顯示出選擇的項 this.btn_select.addEventListener(egret.TouchEvent.TOUCH_TAP, this.onClickSelect, this) /** * 點擊確定按鈕 */ onClickSelect(e) { SceneManager.toMainScene() SceneManager.instance.mainScene.toggleBtn(0) // 拿到數據源 let dataProvider = this.list_hero.dataProvider let arr:string[] = [] // 遍歷數據源中所有項 for(let i = 0; i < dataProvider.length; i ++) { let item = dataProvider.getItemAt(i) if (item.isSelected) { arr.push(item.name) } } SceneManager.showInfo(arr) } SceneManager.ts /** * 在主場景顯示選擇的數據 */ static showInfo(arr:string[]) { let text:string = '你選擇了: ' if (arr.length === 0) { text = '厲害了什么都不選' } else { text += arr.toString() } // 新建一個消息背景圖 let img:egret.Bitmap = new egret.Bitmap() img.texture = RES.getRes('toast-bg_png') SceneManager.instance.mainScene.addChild(img) img.x = SceneManager.instance.mainScene.width / 2 - img.width / 2 img.y = 500 img.height = 40 // 新建一個label用來顯示 let label:egret.TextField = new egret.TextField(); label.text = text label.size = 20 SceneManager.instance.mainScene.addChild(label) label.x = SceneManager.instance.mainScene.width / 2 - label.width / 2 label.y = 510 label.height = 40 // 創建一個定時器,1000毫秒后刪除label let timer:egret.Timer = new egret.Timer(1000, 1) timer.start() timer.addEventListener(egret.TimerEvent.TIMER_COMPLETE, (e)=>{ SceneManager.instance.mainScene.removeChild(label) SceneManager.instance.mainScene.removeChild(img) }, this) }
到這里,我們的游戲卡牌項目的重點已經基本完成了。
后面還有兩個場景,物品場景,關於場景
物品場景其實就是EUI搭建好場景后,加上一個Scroller,然后里面放上數據列表就好,跟前面的操作都一樣
關於場景根本就是添加一個圖片到場景就ok
最后
如果你按照之前的幾篇文章一步一步的動手實現到了這里,那么你已經完成了
1. EUI項目的創建
2. exml皮膚界面的拖拽搭建
3. EUI基本控件的使用方法: image button scorller list ……
4. loading的實現
5. 游戲場景切換:實現了場景管理類
6. 游戲開發的一些思路,代碼組織方式 (雖然我的不一定是對的,但是能參考一下)
你已經很強了,所以剩下的兩個場景實現起來肯定毫無問題。
我的代碼還有很多可以優化的地方, 比如場景管理里面每次都要判斷刪除其他場景, 完全可以封裝一個方法來使用。
做完這個項目最好再好好看一遍自己寫的代碼,重點是要明確思路,理清代碼邏輯。
源碼
創建主場景
相對於我們之前先創建exml再創建對應的ts文件要方便很多,而且這樣不需要在ts文件中指定skinName,因為直接創建EUI組件的時候它在配置文件中已經幫你指定好了。
可以在default.thm.json文件里面看到
現在點開自動生成的MainScene.exml
設置好寬高 480 * 800
把圖片拖進來,然后再快捷約束里面點擊左對齊和上對齊,圖片就自動調整好了
現在需要去設置場景下方的四個按鈕
每個按鈕都有兩種狀態,正常和按下。 這里需要單獨去新建單個按鈕的皮膚,下面以玩家按鈕為例
新建mbtnPlayer.exml ,設置寬高 111 * 80
點擊下面狀態旁邊的 + 號,給這個皮膚設置不同的狀態
up是正常狀態, down是按下狀態, disabled是禁用狀態
這里我們先設置up狀態
在up狀態中,把正常狀態的背景圖和按鈕圖片拖進來。
同理,把按下狀態也搞定。(記得按下狀態的背景圖不一樣)
現在我們得到了一個自定義的組件皮膚
回到 MainScene.exml
為了更好管理四個按鈕,所以先拖進來一個Group,並給他一個id Group_mbtn
給這個Group設置好布局,一會兒里面的按鈕就會自動排列好,不用手動去拖動
往Group中放置一個 ToggleButton ,把皮膚換成我們剛剛自定義的mbtnPlayer
照葫蘆畫瓢,把其他三個都搞定吧~
主場景搭建完畢
做完了主場景,現在開始寫一些關於主場景的邏輯
上面忘了說要記得給那幾個按鈕設置id
打開 MainScene.ts
class MainScene extends EUI.Component implements EUI.UIComponent { public Group_mbtn:EUI.Group; public mgtnPlayer:EUI.ToggleButton; public mbtnHero:EUI.ToggleButton; public mbtnGoods:EUI.ToggleButton; public mbtnAbout:EUI.ToggleButton; public constructor() { super(); } protected partAdded(partName:string,instance:any):void { super.partAdded(partName,instance); } protected childrenCreated():void { super.childrenCreated(); // 讓Group可以點擊 this.Group_mbtn.touchEnabled = true // 事件委托, 點擊按鈕的時候觸發toggleBtn this.Group_mbtn.addEventListener(egret.TouchEvent.TOUCH_TAP, (e)=> { let theBtn = <EUI.ToggleButton>e.target // 在點擊觸發這個事件的時候,點擊的那個btn已經變成了選中狀態 // 判斷theBtn是否存在theBtn.selected屬性且為true if (theBtn.selected && theBtn.selected != undefined) { this.toggleBtn(theBtn) } else { // 當selected為false的時候,說明按鈕在點擊之前就是選中狀態 // 點擊后變成了false,所以這里改回選中狀態 theBtn.selected = true } }, this) } /** * 切換按鈕 */ public toggleBtn(btn:EUI.ToggleButton) { // 先把所有的按鈕都設置為不選中 for (let i = 0; i < this.Group_mbtn.numChildren; i++) { let theBtn = <EUI.ToggleButton>this.Group_mbtn.getChildAt(i) theBtn.selected = false } // 把傳進來的btn設置為選中狀態 btn.selected = true } } 在Main.ts里,把主場景添加到舞台 protected createGameScene(): void { this.addChild(new MainScene()) }
現在運行就可以看到切換按鈕的效果
玩家場景
現在開始創建 玩家場景 PlayerScene 組件
設置寬高 680*800, 拖拽圖片和button
注意紅色框的是button
這三個按鈕在點擊的時候會變大,這個效果我們可以在制作皮膚的時候就完成
首先用鼠標點中返回按鈕,然后點擊左上方的 源碼
這一段就是那個返回按鈕的源碼,修改成下圖
width.down = "100%" 表示當按鈕按下的時候寬度為 100%,其他情況下寬度90%
horizontalCenter="0" verticalCenter="0" 表示讓圖片以中心放大
可以看到紅框部分是一個可以拖動的窗口,所以我們需要放置一個 scroller
把scroller里面自帶的group刪掉,加上一個數據容器list
現在創建list里面的某一個子項的皮膚 zhuangbeiItem.exml
寬高設為 87*130 ,拖入一張圖片和一個labl控件還有一個image控件,設置好label的樣式
這個皮膚里面的label和image控件的值都是需要我們去提供的
需要在標簽里面寫 {data.xx} 其實很像js里面某些框架的插值寫法
給label 的標簽寫上 {data.name} , 給image 的資源名寫上 {data.image}
裝備item完成,回到剛剛的PLayerScene, 把list的條目皮膚設置為剛剛創建的裝備item
把list布局設置成水平布局
給scroller和list都取個id,便於后面使用
這個scroller只需要左右拖動,所以我們去 ‘所有屬性’ 打開它的水平滾動,關掉垂直滾動
打開PLayerScene.ts
class PlayerScene extends EUI.Component implements EUI.UIComponent { public btn_return:EUI.Button; public btn_zhuangbei:EUI.Button; public btn_qianghua:EUI.Button; public scr_zhuangbei:EUI.Scroller; public list_zhuangbei:EUI.List; public constructor() { super(); } protected partAdded(partName:string,instance:any):void { super.partAdded(partName,instance); } protected childrenCreated():void { super.childrenCreated(); // 數組數據 let dataArr:any[] = [ {image:"resource/art/profile/skillIcon01.png",name:"旋龍幻殺"}, {image:"resource/art/profile/skillIcon02.png",name:"魔魂天咒"}, {image:"resource/art/profile/skillIcon03.png",name:"天魔舞"}, {image:"resource/art/profile/skillIcon04.png",name:"痴情咒"}, {image:"resource/art/profile/skillIcon05.png",name:"無間寂"}, {image:"resource/art/profile/skillIcon06.png",name:"霸天戮殺"}, {image:"resource/art/profile/skillIcon07.png",name:"滅魂狂飆"} ] // 把數組數據轉成EUI數組 let EUIArr:EUI.ArrayCollection = new EUI.ArrayCollection(dataArr) // 把EUI數組作為list的數據源 this.list_zhuangbei.dataProvider = EUIArr // 隱藏進度條 this.scr_zhuangbei.horizontalScrollBar.autoVisibility = false } }
到這里, 玩家場景也創建完成啦~
場景管理類
我們有了兩個場景,可以來做場景之間的切換了
還記得之前說的思路嗎,舞台上先有一個主場景,然后點擊不同按鈕的時候把對應的場景添加到主場景上
這里有個需要注意的地方,子場景添加進來默認層級是高於主場景的,所以會把主場景給擋住了,而我們需要點擊主場景的按鈕。所以我們需要把主場景中放置按鈕的Group的層級提高。
我用一個場景管理類來管理這些場景
新建一個SceneManager.ts,采用的是單例模式,要使用這個類的時候不要new SceneManager實例,而是用 SceneManager.instance 來獲取到這個類的實例
這樣可以保證場景管理類有且只有一個實例,便於管理操作
(使用 static修飾的方法都是靜態方法,簡單的說就是調用的時候不是通過實例調用,而是直接用類名來調用, 類名.方法名)
下面是一個基礎的場景管理類,現在來逐步完善它的功能
/** * 場景管理類 */ class SceneManager { private _stage:egret.DisplayObjectContainer // 設置所有場景所在的舞台(根) private mainScene:MainScene //主場景 private playerScene:PlayerScene //玩家場景 // 在構造函數中創建好場景,保存到屬性 constructor() { this.mainScene = new MainScene() this.playerScene = new PlayerScene() } /** * 獲取實例 */ static sceneManager:SceneManager static get instance(){ if(!this.sceneManager) { this.sceneManager = new SceneManager() } return this.sceneManager } /** * 設置根場景 */ public setStage(s:egret.DisplayObjectContainer) { this._stage = s } // 這里補充代碼…… }
首先需要管理的場景是主場景,SceneManager.instance.mainScene是獲取到主場景的實例
SceneManager.instance 獲取到場景管理類的實例
然后再.mainScene 獲取到構造方法constructor中的 this.mainScene = new MainScene() 得到的主場景 的實例
/** * 主場景 */ static toMainScene() { let stage:egret.DisplayObjectContainer = this.instance._stage // (根) 舞台 let mainScene = SceneManager.instance.mainScene // 主場景 // 判斷主場景是否有父級(如果有,說明已經被添加到了場景中) if(!mainScene.parent){ // 未被添加到場景 // 把主場景添加到之前設置好的根舞台中 stage.addChild(mainScene) } }
現在到main.ts中使用場景管理類來加載主場景
當在主場景點擊玩家按鈕的時候,調用這個方法,切換到玩家場景
這里需要稍微改動一下之前的點擊函數
打開MainScene.ts
/** * 切換按鈕 * @param btn 參數是EUI.ToggleButton的時候切換按鈕, 參數是0的時候設置為全部不選中 */ public toggleBtn(btn:EUI.ToggleButton | number) { // 先把所有的按鈕都設置為不選中 for (let i = 0; i < this.Group_mbtn.numChildren; i++) { let theBtn = <EUI.ToggleButton>this.Group_mbtn.getChildAt(i) theBtn.selected = false } if(btn===0){ return } // 把傳進來的btn設置為選中狀態 btn = <EUI.ToggleButton>btn btn.selected = true // 獲取當前點擊的按鈕的下標, 用來實現不同按鈕對應的功能 // 0 1 2 3 對應 玩家, 英雄, 物品, 關於 let index = this.Group_mbtn.getChildIndex(<EUI.ToggleButton>btn) switch (index) { case 0: // 調用靜態方法切換到玩家場景 SceneManager.toPlayerScene() // 把按鈕的層級提高 // this.numChildren表示所有的子元素數量 this.setChildIndex(this.Group_mbtn, this.numChildren) break default: break } } 點擊玩家按鈕就可以正常切換到玩家場景了,現在來實現其中的返回按鈕 點擊返回按鈕,回到主場景,並且下面的按鈕全都變成未選中狀態 打開PlayerScene.ts // 給返回按鈕添加事件 this.btn_return.addEventListener(egret.TouchEvent.TOUCH_TAP, this.returnMain, this) /** * 回到主場景 */ private returnMain() { SceneManager.toMainScene() } 點擊按鈕跳轉回到主場景,其實就是刪除掉當前覆蓋在主場景上的玩家場景,主場景就能顯示出來了 so,打開SceneManager.ts,完善一下剛剛的函數 /** * 主場景 */ static toMainScene() { let stage:egret.DisplayObjectContainer = this.instance._stage // (根) 舞台 let mainScene = SceneManager.instance.mainScene // 主場景 // 取消所有按鈕的選中狀態 mainScene.toggleBtn(0) // 判斷主場景是否有父級(如果有,說明已經被添加到了場景中) if(!mainScene.parent){ // 未被添加到場景 // 把主場景添加到之前設置好的根舞台中 stage.addChild(mainScene) } // 判斷玩家場景是否有父級(是否在場景中) if(SceneManager.instance.playerScene.parent) { // 如果有就刪除玩家場景 mainScene.removeChild(SceneManager.instance.playerScene) } }
保存文件
去瀏覽器里就能看到效果了~
英雄場景
開始制作英雄場景 HeroScene.exml
到源碼部分,修改一下兩個按鈕的效果
在中間放置一個Scroller,然后里面放一個List,
跟之前玩家場景其實差不多啦,現在去創建 heroListItem.exml
這里需要注意,有個checkBox,用來選中某個list
把里面的數據插值寫好 {data.image} {data.name} {data.value}
checkBox是個例外,它的值不能用{data.xx}的方式來指定,我們需要創建一個單獨的類
Herolist_item.ts
// 必須要繼承自 EUI.ItemRenderer class HeroList_item extends EUI.ItemRenderer{ // 選擇框 public ce_select:EUI.CheckBox; public constructor() { super() // 把這個 類和皮膚 聯系起來 this.skinName = 'resource/skins/skins_item/heroListItem.exml' } // 當數據改變時,更新視圖 protected dataChanged() { // isSeleted 是我們提供數據的某個字段 this.ce_select.selected = this.data.isSelected } }
回到HeroScene.exml, 把list的條目皮膚設置為heroListItem,並給他們設置好id
在所有屬性里面把水平滾動關掉,垂直滾動打開
打開英雄場景HeroScene.ts
class HeroScene extends EUI.Component implements EUI.UIComponent { public btn_return:EUI.Button; public btn_select:EUI.Button; public scr_hero:EUI.Scroller; public list_hero:EUI.List; public constructor() { super(); } protected partAdded(partName:string,instance:any):void { super.partAdded(partName,instance); } protected childrenCreated():void { super.childrenCreated(); // 原始數組 let dataArr:any[] = [ {image: 'resource/art/heros_goods/heros01.png', name: '亞特伍德', value: '評價: 很特么厲害, 為所欲為', isSelected: false}, {image: 'resource/art/heros_goods/heros02.png', name: '亞特伍德', value: '評價: 很特么厲害, 為所欲為', isSelected: false}, {image: 'resource/art/heros_goods/heros03.png', name: '亞特伍德', value: '評價: 很特么厲害, 為所欲為', isSelected: true}, {image: 'resource/art/heros_goods/heros04.png', name: '亞特伍德', value: '評價: 很特么厲害, 為所欲為', isSelected: false}, {image: 'resource/art/heros_goods/heros05.png', name: '亞特伍德', value: '評價: 很特么厲害, 為所欲為', isSelected: false}, {image: 'resource/art/heros_goods/heros06.png', name: '亞特伍德', value: '評價: 很特么厲害, 為所欲為', isSelected: false}, {image: 'resource/art/heros_goods/heros07.png', name: '亞特伍德', value: '評價: 很特么厲害, 為所欲為', isSelected: false} ] // 轉成EUI數組 let EUIArr:EUI.ArrayCollection = new EUI.ArrayCollection(dataArr) // 把list_hero數據源設置成EUIArr this.list_hero.dataProvider = EUIArr // 設置list_hero的項呈視器 (這里直接寫類名,而不是寫實例) this.list_hero.itemRenderer = HeroList_item } }
現在運行一下,能看到列表已經能正確的加載,而且數組中isSeleted字段為true的第三項也被默認選中了。
現在來實現手動更改列表的選中狀態
Herolist_item.ts
// 必須要繼承自 EUI.ItemRenderer class HeroList_item extends EUI.ItemRenderer{ // 選擇框 public ce_select:EUI.CheckBox; public constructor() { super() // 把這個 類和皮膚 聯系起來 this.skinName = 'resource/skins/skins_item/heroListItem.exml' // 當組件創建完成的時候觸發 this.addEventListener(EUI.UIEvent.CREATION_COMPLETE, this.onComplete, this) } private onComplete() { // 當select的選中狀態發生改變的時候觸發 this.ce_select.addEventListener(EUI.UIEvent.CHANGE, (e) => { // this.data 就是綁定的數據, this.data.isSelected = this.ce_select.selected // 把數據打印出來看看 console.log(this.data); }, this) } // 當數據改變時,更新視圖 protected dataChanged() { // isSeleted 是我們提供數據的某個字段 this.ce_select.selected = this.data.isSelected } }
這里可能稍微有點兒繞,需要花一點時間好好理解一下代碼執行的流程。
現在點擊checked框的時候就能正確的修改數據的isSelected了
繼續完成返回和確定選擇的按鈕
返回按鈕其實就是把場景切換到主場景去,在場景控制類中寫個方法就好
確定選擇的按鈕其實也是要切換回到主場景,再獲取一下數據里面isSelected為true的項,並把它們顯示到屏幕上
設置返回按鈕,直接在點擊事件中調用場景管理的方法就好了,把按鈕的選擇狀態都清除
HeroScene.ts
// 點擊返回按鈕,回到主場景 this.btn_return.addEventListener(egret.TouchEvent.TOUCH_TAP, (e)=>{ SceneManager.toMainScene() SceneManager.instance.mainScene.toggleBtn(0) }, this) 選擇功能,點擊按鈕的時候獲取到數據源中的isSelected為true的項都保存到數組中,然后把這個數組作為參數傳到場景管理里面。拿到數組就創建對應的消息顯示出來就好了 HeroScene.ts // 點擊確定按鈕,回到主場景,顯示出選擇的項 this.btn_select.addEventListener(egret.TouchEvent.TOUCH_TAP, this.onClickSelect, this) /** * 點擊確定按鈕 */ onClickSelect(e) { SceneManager.toMainScene() SceneManager.instance.mainScene.toggleBtn(0) // 拿到數據源 let dataProvider = this.list_hero.dataProvider let arr:string[] = [] // 遍歷數據源中所有項 for(let i = 0; i < dataProvider.length; i ++) { let item = dataProvider.getItemAt(i) if (item.isSelected) { arr.push(item.name) } } SceneManager.showInfo(arr) } SceneManager.ts /** * 在主場景顯示選擇的數據 */ static showInfo(arr:string[]) { let text:string = '你選擇了: ' if (arr.length === 0) { text = '厲害了什么都不選' } else { text += arr.toString() } // 新建一個消息背景圖 let img:egret.Bitmap = new egret.Bitmap() img.texture = RES.getRes('toast-bg_png') SceneManager.instance.mainScene.addChild(img) img.x = SceneManager.instance.mainScene.width / 2 - img.width / 2 img.y = 500 img.height = 40 // 新建一個label用來顯示 let label:egret.TextField = new egret.TextField(); label.text = text label.size = 20 SceneManager.instance.mainScene.addChild(label) label.x = SceneManager.instance.mainScene.width / 2 - label.width / 2 label.y = 510 label.height = 40 // 創建一個定時器,1000毫秒后刪除label let timer:egret.Timer = new egret.Timer(1000, 1) timer.start() timer.addEventListener(egret.TimerEvent.TIMER_COMPLETE, (e)=>{ SceneManager.instance.mainScene.removeChild(label) SceneManager.instance.mainScene.removeChild(img) }, this) }
到這里,我們的游戲卡牌項目的重點已經基本完成了。
后面還有兩個場景,物品場景,關於場景
物品場景其實就是EUI搭建好場景后,加上一個Scroller,然后里面放上數據列表就好,跟前面的操作都一樣
關於場景根本就是添加一個圖片到場景就ok
最后
如果你按照之前的幾篇文章一步一步的動手實現到了這里,那么你已經完成了
1. EUI項目的創建
2. exml皮膚界面的拖拽搭建
3. EUI基本控件的使用方法: image button scorller list ……
4. loading的實現
5. 游戲場景切換:實現了場景管理類
6. 游戲開發的一些思路,代碼組織方式 (雖然我的不一定是對的,但是能參考一下)
你已經很強了,所以剩下的兩個場景實現起來肯定毫無問題。
我的代碼還有很多可以優化的地方, 比如場景管理里面每次都要判斷刪除其他場景, 完全可以封裝一個方法來使用。
做完這個項目最好再好好看一遍自己寫的代碼,重點是要明確思路,理清代碼邏輯。
源碼
https://github.com/hedongshu/egret-EUI-DEMO