关于Egret引擎的一些思考
多态
对于一个Laya,Cocos,Egret都使用过的游戏从业者,要说一点Egret最令我印象深刻的一点就是多态。也即一个界面对应多种不同的状态。这真的大大提高了界面的开发进度,特别是对于换皮界面。
虽然初了解时真的很方便,但是真的不推荐这样用,因为打包后,收到到资源加载的影响,在开启界面的第一帧可能时初始界面状态,不是你想要的界面状态。并且在代码中实时修改界面的状态也会有各种各样的问题。
时间控制
1.Timer 计时器
假设有这样一个需求:游戏中设计一个倒计时功能,要求玩家在30秒内进行游戏操作,并达到一定要求方可过关。
在这个需求中,需要一个倒计时功能,在Egret中提供了计时器 Timer 来实现类似功能。
Timer 相关的有两个属性,三个方法和两个事件。
两个属性是 delay 与 repeatCount ,分别表示每次间隔的时间(以毫秒为单位)和执行的次数(如果次数为0,则表示不停的执行)。
三个方法为 start, reset 和 stop。作用分别是开始计时,重新计时和暂停计时。
两个事件分别为 TimerEvent.TIMER 和 TimerEvent.TIMER_COMPLETE 。分别在计时过程中触发和计时结束后触发。
示例演示:
class TimerDemo extends egret.DisplayObjectContainer {public constructor() {super();//创建一个计时器对象const timer: egret.Timer = new egret.Timer(500, 5);//注册事件侦听器timer.addEventListener(egret.TimerEvent.TIMER, this.timerFunc, this);timer.addEventListener(egret.TimerEvent.TIMER_COMPLETE, this.timerComFunc, this);//开始计时timer.start();}private timerFunc() {console.log("计时");}private timerComFunc() {console.log("计时结束");}
}
2.Ticker 心跳
startTick(停止对应stopTick)全局函数将以 60 帧速率回调函数。它与 ENTER_FRAME 事件不同。ENTER_FRAME 是每帧回调,改变帧率会改变回调速度;startTick是定时回调,改变帧率也不会影响回调速度。
示例代码:
class startTickerTest extends egret.DisplayObjectContainer {public constructor() {super();this.once(egret.Event.ADDED_TO_STAGE, this.onLoad, this);}private star: egret.Bitmap;private speed: number = 0.05;private time: number = 0;private onLoad(event: egret.Event) {const star: egret.Bitmap = new egret.Bitmap(RES.getRes("star"));this.addChild(star);this.star = star;this.time = egret.getTimer();egret.startTick(this.moveStar, this);}private moveStar(timeStamp: number): boolean {const now = timeStamp;const time = this.time;const pass = now - time;console.log("moveStar: ", (1000 / pass).toFixed(5));this.time = now;return false;}
}
startTick 函数有两个传入参数,第一个参数是回调函数,该回调函数要求有返回值,如果返回为true将在回调函数执行完成之后立即重绘,为false则不会重绘。第二个参数是this对象,通常传入this即可。
下面修改回调函数中的代码:
private function moveStar(timeStamp: number): boolean {const now = timeStamp;const time = this.time;const pass = now - time;console.log("moveStar: ", (1000 / pass).toFixed(5));this.star.x += this.speed * pass;if (this.star.x \> 300)egret.stopTick(this.moveStar, this);this.time = now;return false;
}
会得到类似上面的效果。startTick 函数的回调函数会传入一个参数为执行到当前所逝去的时间。
另外需要说明,上述代码中的全局函数 getTimer() 可以获取全局的 Egret 框架启动时间毫秒数。
3.帧事件
帧事件 ENTER_FRAME 在下一帧开始时回调。所以它的回调速率是跟帧率相关的。下面代码测试了不同帧率下的表现:
class startTickerTest extends egret.DisplayObjectContainer {public constructor() {super();this.once(egret.Event.ADDED_TO_STAGE, this.onLoad, this);}private timeOnEnterFrame: number = 0;private onLoad(event: egret.Event) {this.addEventListener(egret.Event.ENTER_FRAME, this.onEnterFrame, this);this.timeOnEnterFrame = egret.getTimer();}private onEnterFrame(e: egret.Event) {const now = egret.getTimer();const time = this.timeOnEnterFrame;const pass = now - time;console.log("onEnterFrame: ", (1000 / pass).toFixed(5));this.timeOnEnterFrame = egret.getTimer();}
}
修改不同的帧率,结果不同:
事件机制
Egret中的事件机制是一套业内标准的事件处理架构。所提供的事件模式也非常的清晰、强大和高效。
在Egret中,事件模型定义了一套标准的生成和处理事件消息的方法,使程序中的对象可以相互交互,通信,保持自身状态和相应变化。简单的说,数据的提供者只管发出数据对象,只要确保数据对象是 egret.Event 类或者子类的实例即可。这种数据对象 ,称为事件(Event)。数据对象的发出者,称之为事件发送者(Event dispatcher)。同时,接受事件的对象,称为 事件侦听者(Event listener)。
1.事件的执行流程
事件机制包含4个步骤:注册侦听器,发送事件,侦听事件,移除侦听器。这四个步骤是按照顺序来执行的。
注册侦听器,即指定事件由哪个对象的哪个方法来接受。在上一节约会的例子中,我们指定由男朋友来发送事件,由女朋友来接受事件。
只有在注册侦听器后,发送的事件才能被侦听。而且发送的事件必须和侦听器事件的类型匹配。在发送事件后,侦听器才能侦听到事件。
下面通过实例展示“约会”这个事件发送过程,以及代码编写过程。
2.Event类
Event类是所有事件类的基类。当创建一个自定义事件的时候,事件应该继承自Event类。同时Event类也包含一些事件。这些事件通常与显示列表,显示对象的状态有关。
在使用Event类时,有以下几个属性与方法需要注意。
首先是构造函数中的三个参数,type、bubbles和cancelable。
type指定事件的类型,在“约会”的例子中,type事件类型为“DATE”。我们经常使用的事件类型有“ADDED”、“COMPLETE”等。
bubbles是指定事件是否参与事件流的冒泡阶段,关于事件流,会在后面的小节中介绍。
cancelable表示是否可以取消与事件关联的默认动作。
另外还需要关注的属性是target,这个属性表示事件的目标,也就是事件的发送者。其他的一些方法都与事件流有关,后面的内容会进行详细介绍。
3.事件侦听器
事件侦听器也就是事件的处理者,负责接收事件携带的信息,并在接收到该事件后执行特定的代码。
Egret中,事件的侦听器必须是一个函数。事件的发送者必须是 egret.EventDispatcher 类或者子类的实例。只有事件发送者才能侦听事件,并且可以注册侦听器。
侦听事件分为两个部分,第一是建立侦听器,侦听器可以是独立的函数,也可以是某一个对象的方法。第二步是注册侦听器,使用事件发送者的 addEventListener() 将相应的事件分配给侦听器。
下面是注册侦听函数的定义。
public function addEventListener(type: string, listener: Function, thisObject: any, useCapture: boolean = false, priority: number = 0);
- type:事件类型,必选。
- listener:用来处理事件的侦听器,必选。
- thisObject:作用域,必选,一般填写this。因为TypeScript与JavaScript的this作用域不同,其this指向也会不同。如果不填写this的话,那么编译后的代码会发生错误。关于this的问题,可以学习JavaScript中的原型链。
- useCapture: 确定侦听器是运行于捕获阶段还是运行于冒泡阶段,可选。设置为
true,则侦听器只在捕获阶段处理事件,而不在冒泡阶段处理事件。设置为 false,则侦听器只在冒泡阶段处理事件。 - priority: 事件侦听器的优先级,可选。优先级由一个带符号的整数指定。数字越大,优先级越高。优先级为 n 的所有侦听器会在优先级为 n -1 的侦听器之前得到处理。如果两个或更多个侦听器共享相同的优先级,则按照它们的添加顺序进行处理。默认优先级为 0。
1.创建侦听器
侦听器必须是函数,它可以是一个独立函数,也可以是一个实例的方法。侦听器必须有一个参数,并且这个参数必须是 Event 类实例或其子类的实例, 同时,侦听器的返回值必须为空(void)。范例代码如下:
function listenerName(evt: Event): void { }
2.注册侦听器与移除侦听器
只有事件的发送者才可以注册侦听器,事件的发送者必须是 EventDispatcher 类或其子类的实例。移除侦听器也同理,通常情况下,注册侦听器与移除侦听器都是成对出现。
注册侦听器
事件发送者.addEventListener(事件类型, 侦听器, this);
移除侦听器
事件发送者.removeEventListener(事件类型, 侦听器, this);
3.检测侦听器
如果需要在逻辑中检测某一个事件发送者是否注册了侦听器,有两个方法可以使用。 一个是 hasEventListener ,另外一个是 willTrigger 。两个方法执行效果相同,都是判断一个事件发送者是否注册了某一个类型的事件。
如果该事件类型已经被注册过,返回 true,如果没有被注册过,返回 false。
事件发送者.hasEventListener(事件类型);
4.发送事件信号
事件发送者.dispatchEvent(事件类型);
颜色效果
1.混合模式
混合模式是指同一个显示容器中的两个显示对象重叠时,重叠区域如何呈现的方式,也就是两个显示对象的重叠区域像素如何混合产生结果像素。
模式 | 介绍 | 设置 | 效果 |
---|---|---|---|
覆盖混合 | 覆盖混合,表示为"normal",该显示对象出现在背景前面。显示对象的像素值将覆盖背景的像素值。在显示对象为透明的区域,背景是可见的。 | img.blendMode = egret.BlendMode.NORMAL | ![]() |
叠加混合 | 叠加混合,表示为"add" :将显示对象的原色值添加到它的背景颜色中,上限值为 0xFF。此设置通常用于使两个对象间的加亮溶解产生动画效果。 | img.blendMode = egret.BlendMode.ADD | ![]() |
擦除混合 | 擦除混合,表示为"erase" :根据显示对象的 Alpha 值擦除背景。即不透明区域将被完全擦除。 | img.blendMode = egret.BlendMode.ERASE | ![]() |
2.滤镜
1.发光滤镜
1.1.说明
使用 GlowFilter 类可以对显示对象应用发光效果。发光样式的选项包括内侧发光、外侧发光以及挖空模式。
这里创建一个函数用来对传入的显示对象用给定的参数进行发光滤镜处理。
在接下的示例代码中,要传给滤镜的参数,将首先以局部变量的方式定义,并把每个参数的含义在注释部分进行说明。然后将这些参数填充到滤镜的构造函数相应位置。注意,为了结构清晰便于理解代码,所定义的局部变量与滤镜的构造函数参数将一一对应,并且顺序也是完全一致的。
1.2.设置
白鹭小鸟在程序中用一张位图表示:
var img:egret.Bitmap;
创建滤镜:
const color: number = 0x33CCFF; /// 光晕的颜色,十六进制,不包含透明度
const alpha: number = 0.8; /// 光晕的颜色透明度,是对 color 参数的透明度设定。有效值为 0.0 到 1.0。例如,0.8 设置透明度值为 80%。
const blurX: number = 35; /// 水平模糊量。有效值为 0 到 255.0(浮点)
const blurY: number = 35; /// 垂直模糊量。有效值为 0 到 255.0(浮点)
const strength: number = 2; /// 压印的强度,值越大,压印的颜色越深,而且发光与背景之间的对比度也越强。有效值为 0 到 255。暂未实现
const quality: number = egret.BitmapFilterQuality.HIGH; /// 应用滤镜的次数,建议用 BitmapFilterQuality 类的常量来体现
const inner: boolean = false; /// 指定发光是否为内侧发光,暂未实现
const knockout: boolean = false; /// 指定对象是否具有挖空效果,暂未实现
const glowFilter: egret.GlowFilter = new egret.GlowFilter(color, alpha, blurX, blurY, strength, quality, inner, knockout);
最后对位图对象应用发光滤镜:
img.filters = [glowFilter];
1.3.效果
如图是使用滤镜前后的效果对比:
2.颜色矩阵滤镜
2.1.说明
ColorMatrixFilter–颜色矩阵滤镜(egret.ColorMatrixFilter) 在颗粒等级上提供更好的控制显示对象的颜色转换方式。ColorMatrixFilter为 4行5列的多维矩阵(20个元素的数组)。下图为等效的矩阵。
2.2.设置
下面是一个图片灰度化的示例。首先准备一张图片:
然后通过下面颜色转换矩阵代码添加一个“灰度化”的效果:
const hero: egret.Bitmap = new egret.Bitmap();
hero.texture = RES.getRes("hero_png");
this.addChild(hero);
//颜色矩阵数组
const colorMatrix = [0.3, 0.6, 0, 0, 0,0.3, 0.6, 0, 0, 0,0.3, 0.6, 0, 0, 0,0, 0, 0, 1, 0
];
const colorFlilter = new egret.ColorMatrixFilter(colorMatrix);
hero.filters = [colorFlilter];
在上述示例中,首先新建了一个显示对象,然后新建了一个颜色转换矩阵 ColorMatrixFilter,并通过显示对象的 filters 属性来设置滤镜。显示对象的 filters 属性包含当前与显示对象关联的每个滤镜对象的索引数组。
实现效果的关键是颜色转换矩阵的设置。上面我们将每个颜色通道都乘相同的系数来实现灰度效果。
2.3.效果
通过 ColorMatrixFilter 的 matrix 属性可以设置颜色矩阵。需要注意的是不能直接通过 colorFlilter.matrix[4] = 100; 这样的方式直接修改颜色矩阵。只能通过获得数组的引用然后修改,最后重置矩阵:
//获得数组。
const test = colorFlilter.matrix;
//修改数组中的值。
test[4] = 100;
//重置矩阵。
colorFlilter.matrix = test;
2.4.矩阵数据说明
在上面例子中我们实现了灰度图的效果,下面介绍颜色矩阵的含义:
实际的颜色值由下面的公式决定:
redResult = (a[0] * srcR) + (a[1] * srcG) + (a[2] * srcB) + (a[3] * srcA) + a[4];
greenResult = (a[5] * srcR) + (a[6] * srcG) + (a[7] * srcB) + (a[8] * srcA) + a[9];
blueResult = (a[10] * srcR) + (a[11] * srcG) + (a[12] * srcB) + (a[13] * srcA) + a[14];
alphaResult = (a[15] * srcR) + (a[16] * srcG) + (a[17] * srcB) + (a[18] * srcA) + a[19];
公式中 srcR、srcG、srcB、srcA 表示原始显示对象的像素值, a 是颜色矩阵。新的红绿蓝和alpha通道实际由颜色矩阵和原始图片的像素值同时决定。颜色矩阵中的 Off 可以直接设置偏移量加上相应的 R G B A 的值的乘积即为最终的颜色值。所以与原来完全相同的矩阵转换应该是下面这样的:
const colorMatrix = [1, 0, 0, 0, 0,0, 1, 0, 0, 0,0, 0, 1, 0, 0,0, 0, 0, 1, 0
];
下面是几个颜色矩阵设置示例:
设置红色偏移量
在颜色矩阵中直接设置每一行中最后一个值即可设置偏移量,直接设置红色通道的偏移量,结果整张图片变红。
const colorMatrix = [1, 0, 0, 0, 100,0, 1, 0, 0, 0,0, 0, 1, 0, 0,0, 0, 0, 1, 0
];
修改代码中的颜色矩阵数组,编译运行得到如下效果图:
需要注意的 R G B 通道对应的偏移量的值应该为 -255 ~ 255,Alpha 通道对应的偏移量取值范围为 -255 ~ 255.应避免传入除数字外其他类型的值,比如字符串等。
绿色加倍
如果想使绿色通道加倍,colorMatrix[6] 加倍即可:
const colorMatrix = [1, 0, 0, 0, 0,0, 2, 0, 0, 0,0, 0, 1, 0, 0,0, 0, 0, 1, 0
];
红色决定蓝色值
如果要使结果图像中的蓝色与原图的红色数量相等,将colorMatrix[10]设为1, colorMatrix[12]设为0 ,即结果的蓝色值完全由原始的红色值决定:
const colorMatrix = [1, 0, 0, 0, 0,0, 1, 0, 0, 0,1, 0, 0, 0, 0,0, 0, 0, 1, 0
];
增加亮度
增加亮度的最简单途径是给每个颜色值添加相同的偏移量。
const colorMatrix = [1, 0, 0, 0, 100,0, 1, 0, 0, 100,0, 0, 1, 0, 100,0, 0, 0, 1, 0
];
通过"颜色矩阵滤镜"可以完成除了亮度和灰度之外复杂的颜色调整,如调整对比度,饱和度和色相等。
3.模糊滤镜
3.1.设置
在 Egret 中,通过 BlurFilter 类设置模糊滤镜。
const hero: egret.Bitmap = new egret.Bitmap();
hero.texture = RES.getRes("hero_png");
this.addChild(hero);
const blurFliter = new egret.BlurFilter(1, 1);
hero.filters = [blurFliter];
和颜色转换矩阵类似,实例化一个 BlurFilter 并将其保存到显示对象的 filters 属性中即可。其中实例化 BlurFilter 有两个参数,即为 x 、y 方向的模糊度。 值越大效果越模糊。
3.2.效果
需要注意的是模糊滤镜对性能的消耗比较大,应谨慎使用。普通显示对象可以开启 cacheAsBitmap 提高一些性能。
显示对象的 filters 属性可以保存多个滤镜效果,比如同时使用 hero.filters = [blurFliter,colorFlilter]; 模糊和颜色矩阵滤镜效果。多个效果同时生效。
4.投影滤镜
4.1.说明
阴影算法基于模糊滤镜使用同一个框型滤镜。投影样式有多个选项,包括内侧或外侧阴影和挖空模式。
4.2.设置
白鹭小鸟在程序中用一张位图表示:
var img:egret.Bitmap;
创建滤镜,在定义局部变量时,对每一个参数的含义在注释部分进行简要的说明:
const distance: number = 6; /// 阴影的偏移距离,以像素为单位
const angle: number = 45; /// 阴影的角度,0 到 360 度
const color: number = 0x000000; /// 阴影的颜色,不包含透明度
const alpha: number = 0.7; /// 光晕的颜色透明度,是对 color 参数的透明度设定
const blurX: number = 16; /// 水平模糊量。有效值为 0 到 255.0(浮点)
const blurY: number = 16; /// 垂直模糊量。有效值为 0 到 255.0(浮点)
const strength: number = 0.65; /// 压印的强度,值越大,压印的颜色越深,而且阴影与背景之间的对比度也越强。有效值为 0 到 255。暂未实现
const quality: number = egret.BitmapFilterQuality.LOW; /// 应用滤镜的次数,暂无实现
const inner: boolean = false; /// 指定发光是否为内侧发光
const knockout: boolean = false; /// 指定对象是否具有挖空效果
const dropShadowFilter: egret.DropShadowFilter = new egret.DropShadowFilter(distance, angle, color, alpha, blurX, blurY, strength, quality, inner, knockout);
最后对位图对象应用投影滤镜:
img.filters = [dropShadowFilter];
4.3.效果
下图是使用滤镜前后的效果对比:
对比发光滤镜,可以看到投影滤镜的构造函数正好比发光滤镜多出前两个参数:distance 和 angle 。在投影滤镜的 distance 和 angle 属性设置为 0 时,投影滤镜与发光滤镜极为相似。