写在前面
EUI是一套基于Egret核心显示列表的UI扩展库,它封装了大量的常用UI组件,能够满足大部分的交互界面需求,即使更加复杂的组件需求,您也可以基于EUI已有组件进行组合或扩展,从而快速实现需求。
为了展示EUI的功能,我借助白鹭官网中的一个卡牌游戏DEMO,从0开始编写完成这个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页面了~
主场景
主场景就是我们进入游戏的时候看到的第一个场景,其他的四个场景就是在点击下面不同的按钮的时候添加到当前的主场景上就好啦~
首先我们来把几个场景的组件搞定。
- 在resource目录下新建一个skins目录,用来存放我们创建的皮肤
- 在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中使用场景管理类来加载主场景
在到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
classHeroList_itemextendsEUI.ItemRenderer{
// 选择框
publicce_select:EUI.CheckBox;
publicconstructor() {
super()
// 把这个 类和皮肤 联系起来
this.skinName='resource/skins/skins_item/heroListItem.exml'
}
// 当数据改变时,更新视图
protecteddataChanged() {
// isSeleted 是我们提供数据的某个字段
this.ce_select.selected=this.data.isSelected
}
}
回到HeroScene.exml, 把list的条目皮肤设置为heroListItem,并给他们设置好id
在所有属性里面把水平滚动关掉,垂直滚动打开
打开英雄场景HeroScene.ts
classHeroSceneextendsEUI.Componentimplements EUI.UIComponent{
publicbtn_return:EUI.Button;
publicbtn_select:EUI.Button;
publicscr_hero:EUI.Scroller;
publiclist_hero:EUI.List;
publicconstructor() {
super();
}
protectedpartAdded(partName:string,instance:any):void
{
super.partAdded(partName,instance);
}
protectedchildrenCreated():void
{
super.childrenCreated();
// 原始数组
letdataArr: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数组
letEUIArr:EUI.ArrayCollection=newEUI.ArrayCollection(dataArr)
// 把list_hero数据源设置成EUIArr
this.list_hero.dataProvider=EUIArr
// 设置list_hero的项呈视器(这里直接写类名,而不是写实例)
this.list_hero.itemRenderer=HeroList_item
}
}
现在运行一下,能看到列表已经能正确的加载,而且数组中isSeleted字段为true的第三项也被默认选中了。
现在来实现手动更改列表的选中状态
Herolist_item.ts
classHeroSceneextendsEUI.Componentimplements EUI.UIComponent{
publicbtn_return:EUI.Button;
publicbtn_select:EUI.Button;
publicscr_hero:EUI.Scroller;
publiclist_hero:EUI.List;
publicconstructor() {
super();
}
protectedpartAdded(partName:string,instance:any):void
{
super.partAdded(partName,instance);
}
protectedchildrenCreated():void
{
super.childrenCreated();
// 原始数组
letdataArr: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数组
letEUIArr:EUI.ArrayCollection=newEUI.ArrayCollection(dataArr)
// 把list_hero数据源设置成EUIArr
this.list_hero.dataProvider=EUIArr
// 设置list_hero的项呈视器(这里直接写类名,而不是写实例)
this.list_hero.itemRenderer=HeroList_item
}
}
这里可能稍微有点儿绕,需要花一点时间好好理解一下代码执行的流程。
现在点击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)
// 拿到数据源
letdataProvider=this.list_hero.dataProvider
letarr:string[] =[]
// 遍历数据源中所有项
for(leti=0; i<dataProvider.length; i++) {
letitem=dataProvider.getItemAt(i)
if(item.isSelected) {
arr.push(item.name)
}
}
SceneManager.showInfo(arr)
}
SceneManager.ts
/**
* 在主场景显示选择的数据
*/
staticshowInfo(arr:string[]) {
lettext:string='你选择了: '
if(arr.length===0) {
text='厉害了什么都不选'
} else{
text+=arr.toString()
}
// 新建一个消息背景图
letimg:egret.Bitmap=newegret.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用来显示
letlabel:egret.TextField=newegret.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
lettimer:egret.Timer=newegret.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
最后
如果你按照之前的几篇文章一步一步的动手实现到了这里,那么你已经完成了
- EUI项目的创建
- exml皮肤界面的拖拽搭建
- EUI基本控件的使用方法:image button scorller list ……
- loading的实现
- 游戏场景切换:实现了场景管理类
- 游戏开发的一些思路,代码组织方式 (虽然我的不一定是对的,但是能参考一下)
你已经很强了,所以剩下的两个场景实现起来肯定毫无问题。
我的代码还有很多可以优化的地方, 比如场景管理里面每次都要判断删除其他场景, 完全可以封装一个方法来使用。
做完这个项目最好再好好看一遍自己写的代码,重点是要明确思路,理清代码逻辑。
源码
https://github/hedongshu/egret-EUI-DEMO
更多推荐
《菜鸟教程》 EUI卡牌游戏制作
发布评论