手工修改Starling1.2以及其它提高效率的经验

敬告:
使用此篇文章内容进行手工优化Starling前,请做好备份工作,
在删除某些功能前请务必确认该功能对你的项目确实无用。

笔者是Starling的老用户,从Starling的最早公开版本0.9开始使用,
而截至此篇文章发布,Starling1.2刚刚发布不久。
1.2据官方称比1.1提高效率50%,笔者用一个压力测试项目实测,果然从12.5fps提高到18fps。
Starling的更新对笔者来说既是好事,也是麻烦事。
好在项目效率又能提升,
而麻烦在于,笔者使用的Starling是经手工修改过的,每次Starling更新无可避免地要花费3个小时多重新修改一次。

下文笔者将交流一些手工修改Starling的经验以及利用Starling框架的效率测试、优化方案。

1,类的名称
要说手工对Starling进行修改,得从类的名称谈起。
Starling类的名称与flash原装类名相同,
在实际应用场合中,尤其是移植项目以及stage/stage3D混用时将使程序员难以分清这个Sprite究竟是姓flash还是姓starling
因此在开始项目前,花出一点时间将Starling的类名改为易区分的名字是值得的。
FB等开发工具支持改名后自动更新对类的全部引用,因此这个改名过程是很快的。
笔者会将Starling的全部类名添加STL前缀,譬如Sprite换为STLSprite,也因而有STLTextField、STLEvent等。这种前缀式命名写法有点类似命名空间,Objective-C经常采用这个命名方法,无论cocos2d还是官方自带脚本都是这个格式。
如果还在使用FL的话,那么很可惜,在FL环境下修改starling类名并不是一个好主意。
副作用:项目将不能直接和其它Starling项目混用,以及每次Starling更新时,你需要和楼主一样花几个小时重新修改


2,使用预编译常数跳过错误检测
Starling提供了大量的报错信息,这将给程序增加更多的分支判断,也因此影响少量性能。
譬如原始的starling.display.DisplayObject构造函数中有如下一句
if(getQualifiedClassName( this)== "starling.display::DisplayObject") throw new AbstractClassError();
这是表示这个DisplayObject类不能被直接初始化,只能继承它(抽象类)
但是这是在运行时才进行检测的脚本,全部Starling显示对象继承自这个类,也就是你生成的任意一个Starling显示对象都会附带调用一次if、一次双等判断、以及一次getQualifiedClassName函数的调用。
Starling中于此类似的检错脚本还有很多,譬如getChildAt要检查索引是否超出范围或小于0、经常检查STLImage的材质是否是空等。从这点可以看出Starling的严密性,然而下面的写法将既能保持检错功能的严密性,又能避免在发布版本浪费效率。
在FB/FD上设置项目的“附加编译器参数”,关于“附加编译器参数”可于其他文献找到,本文不再累述。
在其中添加一句-define DEBUG::STL true
之后即可将上述例文改为
DEBUG::STL{
if(getQualifiedClassName( this)== "starling.display::DisplayObject") throw new AbstractClassError();
}
这是Flash编译器支持的极有限的预编译办法之一。这段代码可以被正常编译执行
但一旦将“附加编译器参数”设置为-define DEBUG::STL false
这段代码将不会被编译进项目。你需要做的只是记得在最终发布前修改此参数。

在FL中也能像FD/FB一样实现该功能,设置的位置在
文件->发布设置->flash->ActionScript3.0设置->编译常数
副作用:只要不在发布前忘记改回编译器参数,没有严重副作用。比较轻微的副作用是之后每当使用修改后的Starling包创建项目时,都需要手工添一行编译器参数DEBUG::STL


3,通过集中纹理来提高渲染效率(初学篇,熟练程序员请看下一条)
我们使用GPU渲染图像,首先要将多张纹理提交给GPU
然后通知GPU要换用哪张纹理、要用这张纹理的哪个部分、最后渲染到屏幕的哪个区域。
如果我们只有一张纹理图,那么即可省去第一步,这是一个显著的效率提升。
FlashCS6新增了导出序列图的功能。序列图内集中着一个应用所用到的多张纹理,另会生成一个xml文档来描述每个纹理的宽高、位置。
如果你的图片均在一张序列图中,使用Starling的STLTexture.uploadFromBitmap(Starling演示中的getTextureAtlas())可以一次性地将其提交给GPU,之后每次渲染的STLImage或STLMovieClip只要还在使用同一个STLTextureAtlas下的子纹理,即不会通知GPU更换纹理,也因而达到更好的效率。
Stage3D(而不是Starling)限制纹理图最大尺寸2048x2048像素。我们的一款应用可能需要不止一张纹理图。那么,尽量使同一场景下只使用一张纹理图。在不可避免同一场景使用多张纹理图时,让临近深度的显示对象使用相同的纹理图
Po4.png
上图中同颜色的显示对象使用相同的纹理图,尽量让它们相邻。
上图左将通知GPU切换纹理图6次(含进第一图形的一次切换),而上图右只需要2次。


4,使用序列图时极易疏忽的一点(晋级篇,初学请回头看上一条)
这条分享的经验是个坑,笔者虽然熟悉Starling和纹理上传,仍然在里面栽了2个月才发现。
我们已经阅读过上面一条,清楚如何靠集中纹理提高GPU执行 速度
STLTextField将使你无法使用单一纹理大图,或无法保证显示对象利用纹理大图的连续性。
深究STLTextField的非位图字体显示原理,
STLTextField首先生成一个传统TextField,向上面输入文字,并利用BitmapData.draw将其转成位图
之后调用STLTexture.uploadFromBitmapdata上传给GPU。
这个过程也就是上传了一张新的纹理,这张纹理是动态生成的,不同于你之前上传的任何一张
很多游戏需要用文本显示伤害值、血量等,甚至每个单位头上顶一个文本框,
那么在这种游戏最主要场景中将会出现非常频繁的“更换纹理”通知,
笔者进行压力测试,这至少是一个12fps和22fps的差别。


5,接上一条,如何解决文本纹理需要单独绘制的问题
使用位图字体是最好解决方案。Starling自带位图字体功能,其demo中也有使用样例
使用AngleCode开发的BMFont软件可以轻易制作位图字体,位图字体文件是一张png和一个xml(后缀名是.fnt)
Starling自带的例子中,位图字体是一张独立的纹理位图,这仍无法避免纹理切换
我们需要将这个png与我们主要场景使用的纹理大图混成一张,这样在主要场景可以避免或减少频繁更换纹理的现象。

笔者使用TexturePacker打包纹理,尚未测试过在CS6上的实用性:
将主场景用到的全部纹理碎片放在文件夹a内,
设使用BMFont导出了字体Enliven的英文字母和数字,图名Enliven.png,附带Enliven.fnt作为描述
将Enliven.png也放在a内一起打包
使用TexturePacker打包文件夹a内全部小图于一张大图main.png,附带main.xml作为描述
这样使用Starling注册字体的脚本就是
[ Embed(source= "Enliven.fnt",mimeType= "application/octet-stream")];
var Enliven_fnt :Class;
[ Embed(source= "main.xml",mimeType= "application/octet-stream")];
var main_xml :Class;
[ Embed(source= "main.png")];
var main_png :Class;
var mainTexture :STLTexture = STLTexture. fromBitmap( new main_png);
var mainXml :XML = XML( new main_xml);
var mainTextureAtlas :STLTextureAtlas = new STLTextureAtlas(mainTexture,mainXml);
var fontTeture :STLSubTexture = mainTextureAtlas.getTexture( "Enliven");
var fontXml :XML = XML( new Enliven_fnt);
STLTextField.registerBitmapFont( new BitmapFont(fontTeture,fontXml));
此段代码大意为,先从仅有的一张大图中抠取出包含所有文字的一块,在将这块当成一块整图注册给STLTextField
副作用:位图文本占用一定的纹理大小,另外位图文本只能绘在一张纹理图上(推荐在最复杂场景的纹理上),在当前纹理图不是这张时仍然会出现频繁切换纹理现象,但小于等于修改前的切换次数。

6,测试当前使用纹理数量以及分层是否合理的办法
在一个有多张纹理图的项目中,如何得知某时间的渲染中有多少纹理正被使用,
以及这些纹理是否因深度不连续而在被频繁切换?
如下修改可以解决该问题。
我们已在前文的第二条中学到了预编译命令的写法。
在模拟器上测试项目时效率非常高,并不在乎多声明几个变量或多几个trace
所以,为STLTexture类添加一个public属性name,当然是DEBUG::STL{public var name:String}
在STLTextureAtlas.getTexture的同时用getTexture的参数为这个属性赋值,这样所有的STLSubTexture就能记录自己的名字
判断是否切换问题路的代码在STLQuadBatch类上,
修改isStateChange方法,为其添加一行
DEBUG::STL{
    if(mTexture){
     if(mTexture.base != texture.base){
      trace(mTexture.name+"->"+texture.name);
     }
    }
   }
//这段代码只对Starling1.2有效,1.1之前没有base的定义。
到此,在场景中一旦出现材质的切换,你将看到频繁的输出信息,并提示是从哪张材质切换到哪张材质。
如果在这个输出过程中出现了一些无名材质,那么它们是STLTextField所上传的。
重申一次进行这种测试的必要性:
Air研究小组的大牛式神曾在交流会上说,这是一个2fps和60fps的区别。
式神使用了2000元件,一组每张切换一次纹理,一组永不切换纹理对比,这是个极端例子
个人用实际项目测试没有那么夸张,但是我的项目用这个方法仍然可以提速50%到100%不等。
副作用:同第二条


7,Starling在广播ENTER_FRAME事件。
广播的位置在STLStage.advanceTime方法中,这些事件最深会广播到STLQuad或STLImage上,
也就相当于在传统显示列表中广播到每个Sprite的Shape上。
然而有人对Shape添加过ENTER_FRAME事件监听器吗?
Starling的显示对象动辄上千,每帧都要用递归算法来广播这个不是什么好事。
可以考虑将其注释掉,只使用stage注册一个ENTER_FRAME监听器,
并采取事件总线来管理所有的ENTER_FRAME事件。
下文有一个时间总线的例子,该帖使用技术有限,仅供参考。
http://bbs.9ria.com/thread-85193-1-1.html
此外,如果关闭Starling自带的ENTER_FRAME广播,将不能使用Starling自带的帧频测试工具
关闭该广播对STLMovieClip播放没有影响。
副作用:你需要额外写一组事件管理器。Adobe的事件机制是广为诟病的,Starling为尽量贴近传统显示树,继承下一些糟粕是无奈之举。(Starling1.2的事件机制已经有所优化,但是只是减少创建Event数量,治标不治本的优化)


8,Starling的文本下面会自动绘制一个与文本同等大小、透明的矩形
该矩形mHitArea:STLQuad用作碰撞检测和计算文本框边框范围(getBounds)。
对RPG、RTS等传统游戏同场景50,100个文本框是常见事,笔者认为同样是一个可以优化的地方。
后面优化方法将同时保留该STLQuad用来计算边框的功能,以及减少显示对象数量
将STLTextField的构造函数中,
addChild(mHitArea);
改为
mHitArea.setParent(this);
经过这个改动,mHitArea不是STLTextField的child,因此不会在渲染时被递归到
但STLTextField是mHitArea的parent,可以正常地计算边框(getBounds)
修改副作用:文本框响应点击事件的区域将从根据文本框初始化时宽高计算的矩形,变为实际文字所占用矩形。这可能是一个有利的副作用。


9,尽量避免使用Embed
这条不是对Starling的修改,但是对为Starling程序节约内存有帮助。
我们了解Air或flashPlayer都是将swf整个加载到内存中之后才开始运行的
swf默认使用zip压缩,也会在载入内存时解压。
全部使用Embed嵌入的资源,包括图片、声音等,此时都以各自的压缩形式放入内存(png、mp3等格式)。
在实际应用一张图片时,Air会再为BitmapData解压png压缩格式位图。
STLTexture的uploadFromBitmap在上传图片后,纹理图只使用显存,不使用内存,可以释放掉BitmapData,但是内存中的png始终存在、没有作用。
如果使用将png用打包的形式放在主程序之外则能减少内存压力。
具体办法为,将需要使用的png放在swf的src文件夹下,并在打包发布时勾选中这张图片,这样即可以用Loader用相对路径加载
每加载一张png,使用STLTexture的uploadFromBitmap上传纹理,并及时调用Loader的unload和gc
修改副作用:加载一张图片要比直接使用Embed的图片慢,尽量将加载步骤放在Loading中。


10,对进行一切优化仍无法保证帧频的项目采取主动丢帧策略
有时我们已经用尽一切成本内的办法,然而在主场景内的帧频仍然无法全速运行。降低帧频是不得以为之的办法。
我们无法通过简单调整stage.frameRate降低帧频
因为这不只意味动画开销降低,也意味着ENTER_FRAME事件被放慢了。
Stage3D的Context3D.present();方法在IOS设备上占用时间长达7ms左右,远超过了其它函数,
而Starling的render方法每帧都要调用它
在cpu繁忙时跳过一些帧的渲染,藉主动“丢帧”可以避免“拖帧”,两害取其轻。

给Starling的核心类starling.core.Starling添加一个属性public var frameSkipOption:STLFrameSkipOption;
同时给其onEnterFrame方法添加如下脚本(如果读者仍需使用Starling广播的ENTER_FRAME事件,那么下面代码改添加到render方法中。)
if(this.frameSkipOption){
    if(this.frameSkipOption.requestFrameSkip()){
         return;
   }
}
也即使如果requestFrameSkip返回true时跳过后续函数执行。
STLFrameSkipOption是一个抽象类,后面贡献一个AutoSkip类,这个类模仿PS2模拟器PCSX2的跳帧策略。



public class AutoSkip extends STLFrameSkipOption {
         private var lastTimer :int;
         private var deadLine :int;
         private var minContiguousFrames :int;
         private var maxContiguousSkips :int;
         private var framesRendered :int;
         private var framesSkipped :int;
         /**
         *一种模仿PCSX2的跳帧策略
         * @param deadLineRate 当两次调用之间时差高于标准值多少开始跳帧,推荐1.2以上
         * @param minContiguousFrames  进行跳帧前最少连续渲染了多少帧,推荐1以上
         * @param maxContiguousSkips  最多可以连续跳过的帧数,推荐1
         *
         */

         public function AutoSkip(stage :Stage,deadLineRate :Number= 1.20,minContiguousFrames :int= 1,maxContiguousSkips :int= 1){
                super();
                this.lastTimer= 0;
                this.deadLine=Math.ceil(( 1000/stage.frameRate)*deadLineRate);
                this.minContiguousFrames=minContiguousFrames;
                this.maxContiguousSkips=maxContiguousSkips;
                framesRendered= 0;
                framesSkipped= 0;
        } //end of Function
         public function requestFrameSkip() :Boolean{
                var rt :Boolean = false;
                var timer :int = getTimer();
                var dtTimer :int = timer-lastTimer;
                if(dtTimer>deadLine&&framesRendered>=minContiguousFrames&&framesSkipped<maxContiguousSkips){
                         //如果满足一系列条件才能批准跳帧
                        rt= true;
                        framesRendered= 0;
                        framesSkipped+= 1;
                } else {
                        framesSkipped= 0;
                        framesRendered+= 1;
                } //end if
                lastTimer=timer;
                return rt;
        } //end of Function
} //end of class
上述丢帧策略中,需尽量保证deadLineRate>=1.2,因为在移动设备上(尤其是安卓设备上)帧频并不稳定,较高的deadLineRate可以减少因为偶然的帧频毛刺造成丢帧策略的误判断。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值