加载形式
FLASH加载文件有两种常规方式:一种是URLLoader,可以加载文本、二进制数据或 URL 编码变量形式的数据,然后转换成简单的文本形式或值对字符串形式;一种是Loader,可以将加载的图像文件转换成BitmapData,也可以解析SWF文件。(Socket, LocalConnection这些非常用类不在讨论范围)
加载的资源应被统一管理以方便调用和重用,管理方式一般有两种:一种是将资源全部打包进SWF;一种是加载分散资源通过配置进行管理。我将对这两种管理方式进行介绍。
资源打包成SWF
在编辑FLA文件时,我们可以导入各种图片,并为其设置链接名。除手工逐个操作外,我们可以借助JSFL进行自动化处理(请参考http://bbs.9ria.com/thread-31730-1-1.html)。
由此生成SWF后,再用Loader加载。这时候我们需要的不再是舞台上的内容,而是SWF应用域里包含资源的类。可以用Loader的contentLoaderInfo.applicationDomain.getDefinition()方法来获得这个类,并实例化。如果在执行加载方法时第二个参数LoaderContext设置成了ApplicationDomain.currentDomain,使得被加载的SWF与主SWF共享同一应用域,那么直接使用getDefinitionByName ()方法也可以获得这个类。
通过new的方式就可以实例化获得的BitmapData或MovieClip类,然后使用。因此项目中要想得到一个资源,只要知道它的链接名即可。链接名是自行设定的有意义的名字,完全可以当做资源的唯一ID。
需要注意的是,new的过程就是图片解压缩的过程。处于Class状态时,图片占用的内存和SWF文件中这个图片占用的磁盘空间一致,而一旦通过new解压成无压缩的BitmapData后,占用的内存会急剧增加。不管是PNG、JPG,还是矢量动画,new之后的体积都会比原来大得多,因此不要随便将资源实例化后暂存。这个实例化过程理所当然是比较费时的,可能会出现卡的现象,但预先实例化,内存占用上是有很大区别的。
此外,如果选择设置LoaderContext使得全部资源加载到同一个域的话,有冲突的链接名是以先来先到的原则处理,即如果两个资源链接名相同,以先加载的对象为准。
打包成SWF有一个优点,SWF可以让JPEG支持透明通道。一般来说,JPEG压缩率高而不支持透明通道,PNG压缩率低支持透明通道。将PNG导入FLA然后设置成JPEG压缩后,就能在压缩的同时保留透明通道,可以让支持透明通道的图片体积大大减小。
打包成SWF后,加载快且易于管理,是推荐方式。但这种做法限定你必须一次性加载所有资源,不能按需加载,有一定的局限性。比较适合加载UI皮肤,以及需要立即显示的图标等等。
还有一点需要注意:SWF舞台上的内容,即使不显示出来也会消耗资源,因此请务必保证在发布时舞台为空。
资源分散加载
如果文件要按需加载,或者不希望用SWF打包增加维护成本,或者有大量文本以致于不能用FLA导入,那我们只能逐个文件加载。
因为资源可能处于不同目录,命名也不规范,也会有扩展名,这样的路径在代码中作为ID存在是不合适的。所以一般都会有一个文本配置文件,将这些文件路径和一个名称对应起来,并提供给模块加载。加载完成后则是通过这个名称来获取资源。
不要直接用Loader加载文件
不同文件有不同的加载方式,文本和二进制文件只能通过URLLoader加载,而PNG、JPG、SWF等文件则可以通过Loader和URLLoader两种方式加载。如果资源需要长期保存,建议全部用URLLoader方式加载,在需要获取资源时,再通过Loader的loadBytes方法解析已经加载的二进制数据,之后再显示。
这样做目的是为了节省内存,因为Loader加载的资源会自动实例化(解码),PNG、JPG会展开成无压缩的BitmapData,SWF舞台的内容也会全部实例化,他们会占用大量内存。先用URLLoader将他们作为二进制数据加载,需要时再解码实例化,就不会出现这个问题。
并发加载
多文件加载还有一个问题:浏览器对并发下载数有限制,而这个限制和Flash Player的机制有冲突,所以一般情况下Flash Player同时发起的加载请求数最好不要超过5个, 否则加载事件可能会失效。为了解决这个问题,大部分人的解决方案都是采取队列加载,一次只加载一个文件。这在文件数量较小、单文件体积较大时并没有问题,但是当文件数量多、单文件体积小时,由于每次加载完一个文件后,重新请求下一个文件时需要等待服务器响应一段时间才开始加载,这样会浪费很多带宽,文件数量多时这个缺陷不能忽略,最多可能消耗2至3倍的加载时间。
为了解决这个问题,我们需要一种特殊的队列加载模块,可以同时加载,但是同时加载的文件数量不能超过某个值。基本思路就是在加载完一个文件后,检查正在进行加载的文件数量,小于定值就取队列中的下一个地址新建加载,否则就什么都不做。
BulkLoader(http://code.google.com/p/bulk-loader),
LoaderMax(http://www.greensock.com/loadermax)
都提供了这个功能,当然我的GhostCat也通过AssetManager(复合QueueLoadOper实现)提供了这个功能。复杂度实际上并没有比线性加载高多少。
这样做,加载时等待服务器返回时依然有文件在下载,多个加载过程会平衡消耗带宽,带宽就不会被浪费。一般同时加载数量为2就足够了。但如果你的文件特别零散,使得两个文件同时等待返回的几率也很高,也可以考虑设置2以上的值。
哈希表缓存
当加载文件数量特别大的时候(诸如数百个),注意不要只使用数组保存。你可以创建一个Dictionary,然后将名称作为键加载内容作为值,做一个哈希表,以后都直接通过名称从这个哈希表取值,会比遍历数组查找名称快很多。
使用ZIP或其他打包形式
如果你不喜欢SWF这种打包方式,也可以选择ZIP打包,详情请阅读http://nochump.com/blog/archives/15。
ZIP的优点是可以用winrar打开,不需要借助专门的工具,缺点则是解压需要时间。当然,你也可以考虑用二进制自定义一个封装格式,这样也能加密资源文件,但这需要你自己编写一个管理工具。当然,这也不算特别麻烦。
使用ShareObject缓存
虽然有浏览器缓存,但实际上这种缓存持续不了几天,因为浏览器一向都有最大缓存限制。一般你看几个视频,这个缓存空间就消耗得差不多了。为了不让Flash加载的文件缓存被冲洗掉,你可以将加载的文件的二进制数据(Loader是contentLoaderInfo.bytes,URLLoader则要用二进制方式加载获取其data属性)保存在ShareObject里,并添加版本号以便更新,下次加载就直接取这个数据。这个操作会请求大量ShareObject空间,因此FLASH会弹出提示让用户确认。如果你担心用户不确认,可以在游戏其他地方向用户说明情况并要求他们点击确认按钮。
现在不少游戏都采用了这种做法,效果还是可以的。
显示总体加载进度
我们可能在开始时会加载多个文件。比起每加载一个文件显示一次进度条从0%到100%的过程,显然是显示所有文件总体的加载进度,只进行一次从0%到100%的过程更具有实际意义。但是FLASH做这件事情并不太容易,因为它想要获得一个文件的大小就必须去加载它,而这个加载需要时间,你没有办法从一开始就立即获得所有文件的大小。
实现的方法只有一个,就是在程序中或者配置中写死所有文件的总大小,然后根据所有文件已经加载字节数的总和来计算和显示进度。
至于这个总和如何获得,可以在程序中写上trace,实际加载运行一次便能获得实际所有文件大小总和,也可以做专门工具计算,也可以用操作系统来直接查看总和,总之方法不是问题。而且,即使这个值不准确也无大碍,因为加载流程依然是按加载完所有文件作为依据,而这个进度只是用来显示,就算不准也不过是未到100%就结束或者到了100%也要等待一段时间才能结束。这无伤大雅。
之前所说的类库都提供了这样的功能。
主SWF加载问题
SWF必须加载完所有类后才能开始运行并显示图像,这样一来,第一个主SWF加载自己时就无法显示加载进度。解决这个问题有两种办法:
一种办法很老但是实用,就是创建一个小SWF先显示出来,专门用来加载主SWF,主SWF加载完毕后它就完成了使命。实际上这并不麻烦也是最稳定的一种方案。
另一种方式是利用Frame元标签。在主SWF类名上面添加
[Frame(factoryClass="加载类类名")]
即可指定一个类作为加载类,它会在主SWF未加载完之前显示。这个类是一个两帧MovieClip,当它自己加载完毕后,就可以反射出主SWF的内容并实例化。
可以看这篇文章:http://www.bit-101.com/blog/?p=946。
而我自己的加载类模板则是这个,可以作为参考:
一般程序开发完成后就进入了繁琐无趣的后期维护阶段,请不要以为一个不停更新的项目后期维护是一件轻松的事情,它会暴露出开发过程中的所有硬伤,不规范的写法、混乱的逻辑结构、高耦合导致地牵一发而动全身。虽然开发内容实际上减少了,但人力成本反而更高。
要提高这方面效率有很多技巧,本文介绍的内容只是起点--如何快速找到项目中需要修改的代码。
一般出现问题首先看到得是表现部分,例如对话框,关系到一些具体逻辑或某个服务端请求,即使不是很清晰的部分也一定有临近的区域。根据表现找到其对应代码,我将其称为定位。
搜索关键字:泛用但低效
搜索关键字是广泛使用的方法。例如,你在节目上看到某个图片,找到图片标志,在所有代码中搜索图片标志,一定可以找到调用这个图片的代码。再如,屏幕中显示的文本,也能找到对应的语言包标识,找到相关代码。然而,这种做法效率很低,因为你要找到标识的具体拼写,搜索项目代码查找关键字也需要时间。所以下面主要介绍如何不借助搜索直接找到目标代码。
包结构
类一般可以从两个维度分类,一个维度是结构类别,如模型、视图、控制器,甚至工具类、组件,另外一个维度是业务类别,如商店、人物、战斗等各种不同模块。
目录里的文件只有一个根,类似单继承,所以你只能用一个分类做为大类别。一般情况都是用结构类别做为大类别,因为结构类别一般是固定不变地,而业务类别可能会经常变化。例如下图所示情况:

这样做是为了避免大量类混杂在一起,只有分到不同目录才能彻底解决这个问题(目录可折叠,必要时子目录或文件命名可重复)。开发模块时,只需展开关心的目录,避免其他文件干扰。
文件命名
推荐文件名根据结构类别做前缀,如视图以UI开头、后台请求以Rpc开头等。如果没有前缀,命名时很容易遇到重复的情况。再以业务类型设定第二个前缀,使得没有目录时,按字母排序时同一系统中的类被排到一起,当然也可以防止重名。

恰当的命名会在利用代码提示引入类时提供便利,而且在打开类(ctrl+shift+T)对话框里也比较容易找到需要的类。当然,主要还是在查看包资源时,列表会比较整齐,方便找到特定文件。
以上是为了帮助你在知道类的功能和类别却不确定具体命名时使用的,你可能不记得具体名字,但应该可以判断出它所在的包,但仅仅这样是不够的。
逻辑分离是前提
逻辑要按一定规则分开到不同类中,否则你的查找目标本身就不存在。
MVC是实现逻辑分离的方法之一。根据MVC思想将类分开,你就会很清楚知道,与用户交互、显示有关的类是在视图中,与数据格式转换、获取特定数据(诸如获得图标实例)、判断(诸如isPropTask()之类)、修改数据逻辑(诸如修改经验值触发升级)是在模型中,而制定特定服务器请求、设置模型数据、引起数个视图更新的代码一定是在服务器请求类的result中。这样就能确定目标位置,即使不能确定具体某个类,也能界定到某个包范围内。
当然这是需要事先约定的,但只要开发者理解并遵守这个约定,就可以做到不依赖搜索关键字也能立即找到需要修改的代码。毫无疑问这种做法是值得的。
从视图着手
视图是最容易找到的部分,然后根据交互事件模拟用户操作过程,通过调用关系一层层查找,最后就能找到需要的部分。
无论你的代码结构如何,这种方法都是通用可行的。如果你的目标就是视图,需要修改的是诸如布局、颜色、数据填充逻辑,这样做就可以了。但如果你的目标不是视图,这并不是最好的办法,因为毕竟需要从视图一层层中转,会多几个步骤而不是直接找到,这自然影响效率。
模型
模型不只是数据。
很多人不明白为什么要在数据之外套一层模型。代码放哪里比较好要以可复用性做为标准,放在模型里的逻辑应该是和数据密切交流的,最基本的是数据序列化和反序列化。这些内容不少人认为应该写在请求完成函数里,而实际应该写在模型中,因为同一个模型可能会由不同请求生成,它们传入的数据格式是一样的,只有在模型中解析才能重复利用这段功能代码。
此外模型还需要负责和数据相关的逻辑,以某个RPG游戏为例:
- 人物属性变化:包括金钱变化(钱不够会失败并提示充值),经验变化(可能需要升级),还有数据更新后对应视图更新;
- 道具管理:增加/删除道具,获得道具数量,判断道具满;
- 计算:诸如保存地图模型可以提供计算A*的方法;
上述逻辑是针对游戏玩家的,只会存在一个模型,数据模型是固定的,所以无论放在哪里只要集中都容易查找,但显然放在模型里最好理解,而且这样即使出现多角色需求,需要修改的地方也很少。
模型通常也会提供一些简单的数据转换方法,例如:
- 获得图标,类似的方法有获取格式化文本、获得ToolTip信息等;
- 校验,一般一组条件的与 或关系或者大于 等于 小于判断,也可能有循环遍历数据进行统计等复杂表达式;
如果按照约定编码,将这些逻辑存放在模型中,这样就不能通过视图快速找到代码,但模型数量少、逻辑少,会比放在视图里找快很多。你也可以用查找引用方法找到调用模型方法的代码片段,以确定这个逻辑的入口,方便逆向追踪。这样只需修改一处,所有相关部分都会发生变化。
命令(Command/Action)
所谓命令,其实很大一部分都是用来请求服务端数据的,设计中有result方法可以在返回结果后处理一些事情,例如整理数据格式、设置模型、调用视图更新方法。虽然很多人将这些功能发明放在视图中,但显然放在Command里重用率更高且更易于定位。
服务端返回的数据会经常变换,虽然具体解析是在模型中完成,但返回的数据常常是多个数据拼凑在一起,可能是数组或者一些简单数据类型,这部分可以有Command处理,做特殊解析并给相关模型赋值。由于这部分需要与服务端沟通,是容易出错的地方,放在Command里容易找到,修改时也可以省去不少麻烦。
命令除了给模型赋值,也还会有一些触发操作。如果确定某个逻辑是请求返回一定执行的也可以放在这里。例如更新视图、触发附加逻辑、首次购买某物品的弹窗。这部分常发生变化,放在Command执行也易于调整。当然Command也可以与服务端请求无关,但道理是一样的。
总而言之,原则是尽可能不要将代码放在视图,而是放在联系更紧且数量少、代码少的一方,这样就能更快找到修改代码位置。Command一般用来调用模型和视图方法,是其他逻辑的入口,自己只有少量代码。
常量类
配置类数据只可能存在于三个地方:服务端,本地配置文件,常量类。
对于服务端数据配置,客户端只单纯接受,本地配置文件一般都保持经常更新的大量数据,所以零散数据配置通常都存在常量类里,例如等级上限、各级经验分配、特定功能花费。
很多开发者都喜欢偷懒,例如一个功能需要花费5元开启,开发者直接在代码里写5。这样看起来算不算神秘数字先不谈,要知道,这种数据即使说绝对不会变而它未来变化的几率也会超过30%。所以常量是必须有的,哪怕数值是1,也应该写成常量,因为未来可能变成2,在代码里保留数字始终会有隐患。如果你设置为常量,对于这类内容就可以直接找到常量修改,而不用关心其他部分,也很容易找到使用这个常量的代码片段。如果没有常量的话,就只能借助和这个数值相关的内容引导并借助关键字搜索以确保修改无遗漏了。
此外,有多版本时配置可能会变化,如果硬编码写死在程序中,需求会很难实现。
其他
这里介绍是以通用MVC为例,因为熟悉的人较多,容易理解。游戏中除用户界面外,结构会更复杂,虽然也有类似模型视图这样的概念,但层次需要分得更细致。其实这些分离方法都是约定的,既然要分离,就要更合理、更易于理解、更具有可复用性。高效修改首先要容易找到问题症结点,要达到这个目的,就要求代码结构整理。良好的结构也可以让约定更加简洁,易于记忆和理解。
上述这些内容,框架并不能帮到你,因为框架大多是限制你分成几部分,而没有也无法限定这几部分具体是什么内容。因此你需要刻意地约定,而这个刻意地行为,对于减少修改维护时的人力成本,比框架重要百倍。
GC和内存泄露无关
垃圾回收,这次是一个被无数人讨论过的传统话题。
Action Script使用的是和Java相似的内存管理机制,并不会即时回收废弃对象的内存,而是在特定时间统一执行一次GC(Gabage Collection)操作来释放废弃对象的内存,避免了重复判断是否需要回收产生的性能问题。
但要注意,这只是决定回收的时机,而不是回收的内容。这个延迟执行内存回收也就是个表面的现象,不管什么时候执行GC,能够回收的内存最终都能回收,不能回收的肯定不能回收。唯一的影响是,因为回收是延迟执行的,你在查看内存的时候不能直观地看到因为一个对象被废弃而回收内存的过程,会产生迷惑。
但这对于解决内存泄露是无关紧要的。
内存泄露指的就是当你销毁了一个对象的时候,它占用的内存却无法被回收,这会导致可用内存越来越小最终溢出,在内存紧张的环境中将会造成系统崩溃。其原因多种多样,但一般都是开发者的疏忽所致,没有提供给系统足够的可以销毁对象的依据。
执行GC虽然和内存泄露没有关系,但是如果不在测试前执行GC,你将看不到当时实际的不可回收内存的量,而内存泄露就是指不可回收内存的数量的增加。因此,测试内存回收将离不开GC方法。没有使用GC方法的测试用例是没有意义的,因为这其中掺杂了偶然性(什么时候执行GC)。不少荒谬的测试结果都是因为没有在正确的位置执行GC导致的。
Flash Player虽然没有开放发布状态的手动gc,但调试版本是可以使用的,正好可以让我们测试。此外下面的HACK代码也可以在发布阶段触发GC。
try {
new LocalConnection ().connect ( "gc" );
new LocalConnection ().connect ( "gc" );
} catch ( e:Error ) {}
但我再次强调,调用GC仅仅是用于测试。实际产品中调用GC基本没有意义(除了用于控制GC时机),总之如果你的程序出现了内存泄露,那一定和GC没有关系,请不要再在这种地方浪费宝贵的时间与精力。
只有在申请内存时才会触发自动GC
AVM2的GC是在每次申请内存时,根据当前内存占用来触发的。申请内存是一个必要因素。所以,如果你一直不进行申请内存的操作,就算内存达到了一个高值,它也不会进行GC。
这确实是个不合理的地方。但是,在实际环境中,一直不请求内存的情况是很少见的,就算出现,当时也未必处于内存的高值。这种情况主要出现在测试环境中,导致一些人会怀疑自动GC的功能是否正常。实际上这也是没有必要的。
Flash中垃圾回收的条件
在AVM2中,除去特殊的BitmapData必须调用dispose才能回收内存外,其他的部分都是用引用计数法和标记清除法作为判断是否应该回收内存的手段,而且并没有提供主动回收的API,详细部分请看这篇日志,我就不重复了。
http://www.cnblogs.com/cos2004/archive/2010/11/07/1870980.html
因此,你要回收一个对象,只要保证没有任何对象引用它,而且他的方法没有被当做事件函数——或者说,他和程序的其他部分已经没有任何联系,它就满足了引用计数法的标准,就一定会被回收。做到这一点的方法就是一般说的“执行removeChild,removeEventListener,将对他的引用设置为null”。
但是,实际上回收一个对象的要求并没有那样严格,就在于FP除了引用计数法,还包括标记清除法。标记清除法是从程序的根对象开始(stage,静态属性,活动的定时器和加载器,ExternalInface.callBack)一级一级遍历对象,只要遍历不到,即使不满足引用计数法的条件也可以回收。比如两个对象互相引用,但是和外界都没有关系,形成了孤岛,它们就可以被回收,尽管它们因为互相引用使得引用数不为0。比起单纯的引用计数,这种办法能确实能找到已经无法再访问到的实际上的闲置对象。所以,可以看到很多人的代码实际上并没有设置null,甚至没有removeEventListener,它一样可以被正常回收,少写这些代码可以使得程序更简洁,要全部符合标记清除法的条件,会很累。
“无法被根访问”,这种说法很暧昧,基本不能当做判断依据。所以我下面会举几个具体例子,来说明什么样的情况是符合标记清除法的要求的。
首先明确一点,标记清除法是只以能否能被根访问作为唯一依据的,并不需要关注被引用的次数,请不要混淆。
- 属性的相互引用是很明确的,一般都是一个对象包含着若干属性,那么这个对象自然可以维持它的属性的引用。如果这个类不会被回收(能够被根访问),他的所有属性也都不会被回收。同样的,如果这个类可以被回收的话(不能被根访问),也就不会妨碍属性的回收。所以你并不需要将所有属性设置为null,除非你希望在对象存在时候就回收其属性的内存,这种需求基本不存在。
- 静态属性是一个特殊的情况。静态属性本身就是根,所以你必须将其设置null才有可能被回收,没有别的办法。
- 至于在显示列表中的对象。既然根(stage)可以用getChildAt访问到自己的所有子对象,那么只要你在显示列表中,就肯定不会被回收。然而,如果显示对象的父层对象已经不再显示列表内,它的子对象就算还在父层对象之中也没有关系,因为它已经不能被stage访问到了。所以你不需要removeChild各层的全部对象,而只需要removeChild最高一层的父对象即可。
- A.addEventListener(“event”,B.handler),像这样添加过事件后,你可以认为B.handler成为了A的一个属性(因为A在需要的时候要能调用B.handler),这里也符合属性相互引用的原则。但是事件判断起来的确要比属性麻烦,因为相互引用的情况很多。在这里可以分为三种情况:
- 对自己监听自己的事件,这相当于用自己的属性保存自己引用,任何情况都不会阻碍自己被回收。
- 对自己的子对象(属性或者child)监听自己的事件。因为子对象本来就是自己在维持它的引用,那么即使它们会维持你的引用,也只会形成一个循环。一旦你和stage脱离了联系,子对象同样也会脱离联系,当然也无法妨碍你自己被回收了。除非子对象因为一些原因可以单独维持引用(诸如被保存在静态属性中),但这种情况很少见。
- 对自己的父对象(parent或者stage)监听自己的事件。因为这使得你成为了父对象的一个属性,只要parent或者stage不被回收,那么自己就不会被回收。尤其是stage,它肯定不会被回收。这种情况一般都会导致自己无法回收,是必须removeEventListener的。
总得来说,就是务必注意对stage,parent的事件监听,其他情况一般都是不会妨碍回收的。而对stage,parent的监听大多都是各种鼠标,键盘事件。数量并不多,专门注意这里可以杜绝大部分因为事件造成的内存泄露。
其实,内存泄露并不容易出现。按照普通的编程习惯,只有监听stage事件这种做法会造成意料之外的泄露,一般都是可以顺利回收的。这比每次都要手工回收内存要方便多了。
这里只有BitmapData是例外。除了遵从上面的规则外,要回收它的内存,必须手动调用dispose方法,习惯自动回收的人会很累。务必注意,Bitmap对象的bitmapData属性是需要手动销毁的,Loader加载的位图是需要手动销毁的,当你用一个生成的位图作为位图填充绘制平铺的图像后,在销毁这个图像后也必须销毁这个位图(所以你必须一直保存位图的引用)。BitmapData是32位的未经任何压缩的图像,随便一个体积都会非常大,不处理好它们的回收,一个BitmapData泄露就可以顶你数万个复杂对象的泄露。
如果出现非常明显的内存泄露,大部分时候都是位图泄露。所以在研究上面的引用计数法和标记清除法以及GC之前,请先保证位图部分不出问题。
弱引用时的例外
弱引用会改变垃圾回收的规则。如果使用了弱引用,addEventListener将不会影响对象回收,即使对stage添加监听,也不会导致自己被回收。但是这同时也是缺点,因为有的时候你就是希望用引用限制住对象的回收,使用弱引用会使得这个对象有时回收有时不回收。虽然极少出现,但一旦出现,这种不容易重现的错误是很难查出来的。因此我并不推荐使用弱引用。
弱引用在AVM2中只有两处:
- 一处是addEventListener的第5个属性,名为userWeakReference,设置为true,监听事件将不会影响对象回收。
- 一处是Dictionary的构造函数参数,名为weakKeys,设置为true,当键为复杂对象时,即使Dictionary存在,键依然可以被回收。注意,这里说的是键,不是值,值是不享受弱引用待遇的。这个属性也写得也很明白,是weakKeys。
内存泄露的查找方法
Flash Builder提供了一个概要分析工具,可以帮助我们查找内存泄露。大多数情况都可以帮助我们解决问题。可以查看下面的文章:
http://blog.csdn.net/bbmjfpig/archive/2010/12/30/6107347.aspx
关键点在于,检测内存泄漏应该是“创建,取样,销毁,再创建,取样”,然后以两次取样的对比数据来观察泄露。因为对象在第一次创建时会有一些缓存数据,它们在设计上就不会随着对象销毁而回收的,比如类定义的缓存,比如皮肤。它们只会创建一次,和我们看到的泄露并不是一回事。
必要时可以执行强制GC
因为每次GC都需要消耗性能,对象越多,GC越慢。我理解Flash Player禁用发布版本的System.gc()是为了避免开发者滥用这个方法,但有些时候我们的确需要手动控制GC时机,因为GC过程如果遇到大量可回收对象会让Flash Player卡住。
比如,我们需要在切换屏幕时回收一次内存,这时候卡是看不出来的,而不是切换完后播放动画时回收然后让动画顿住。或者,我们会定期在必要的时候执行一次GC,将GC需要的时间分担开。所以这时候用HACK方法强制执行一次GC也不失为一个选择。当然,这和内存泄露半点关系都没有。
Flash Player这个地方的设计特别的不好。它自己又不支持分步GC,一旦GC的时候没有办法避免卡的问题。结果GC的时机还不给控制……
微量剩余内存
测试中FLASH的确存在微量内存无限增加的问题,原因未知。我将50万个对象扔在一个数组中,销毁后确实会多出1M的内存占用(如果没扔在数组中不会),但这个数量很小,但达到能看得出来的100M内存需要5000万个对象,这个数额在通常情况下很难达到。
不过也有人说这只是对象销毁而内存并未全部释放的表现,实际上最后还是能完全释放的。或者是因为totalMemory的不精确所造成的。这个我就不清楚了。
不过就算这个的确是FlashPlayer的BUG,也无伤大雅吧。
FLASH与传统环境的不同点
MVC最早在1979年的时候第一次被人提出。不过,当时还不存在网络应用的概念。之后当万维网诞生之后,又过了很长时间……
它并不是自诞生就开始流行的,而改变的原因很简单——因为两个极其流行的开发框架包含了这种模式,它们就是:Struts 和 Ruby on Rails。之后,模仿者蜂拥而至。所以,在人们眼里看来,实际上是先有的Struts,然后才有的MVC,也无怪乎MVC的概念会始终沾染着Web概念,乃至和一些框架附加内容牵涉不清。
因为Struts很好用,别的不说,至少让HTML显得干净了很多。所以很多人都在用Struts,这未必是因为需要MVC模式,而是因为他们需要Struts。因此,当环境变化后,我们不使用Struts而是在使用一些其他的框架的时候,是否还应该像以前那样使用MVC框架就成为了一个问题。因为环境不同,即使在其他语言中使用MVC框架很普遍,也不代表在新环境里同样应该是如此。
AS3与传统语言的不同点:
- AS3是单一语言环境,多层代码混在一起问题没那么严重。
- AS3正常情况都是一次性编译全部代码,即使用了MVC框架还是需要一起编译。单独编译一个模块减少编译时间有别的办法,不需要依赖MVC。
- AS3本身的事件和动态特性和一些框架的功能重复。
- AS3目前的框架还很不成熟,没有提供比较醒目的功能。
结果是,至少,目前AS3的MVC框架比起传统语言并没有那么突出的作用,就算用了,也不会像Struts那样有质的变化。而且,至少在我看来,AS3的框架使用成本却不见得比Struts低。两者相减,结果就很麻烦了。
而且,AS3在不使用框架的时候有它自己的优势,使用框架会毁掉这些优点:
- 有一个相对还可以的调试器,使用了框架会调试上产生麻烦,主要体现在单步调试步骤变多的问题上。
- 阻碍使用IDE的功能。以Flex Builder为例,你可以通过Ctrl+单击(F4)跳转到指定方法的具体实现,通过搜索引用面板从方法的实现跳转到调用方法的位置。使用框架后,这些功能都会失效。
- Flex framework相关功能会难以使用,诸如绑定。而且,Flex Builder支持拖拽式的将数据接口绑定到视图的功能,可以部分实现零代码编程,框架也会阻碍这个过程。
此外,企业应用和网站还好说,游戏还有另一种情况。游戏的结构并不同于原来的专门用于呈现数据的结构,可能也就是其中的用户界面(User Interface)部分和以前的结构比较类似,其他的诸如地图,诸如人物,无论怎么想也无法套用MVC框架,首先从效率上就说不过去。举个例子,一个项目有3个客户端人员在开发,一个在做地图,一个在做战斗,一个在做UI。前两者都和MVC没什么关系,结果只有一个人在用MVC框架开发界面……而且,开发前两者的时候,开发以及协作难度其实是比开发界面要高的,既然他们都搞定了,为什么开发界面的人还必须靠框架辅助才能解决这个问题?
这使得FLASH比起一般的情况,会更加不适合使用MVC框架。
不使用现有框架并非无法实现MVC
既然我在说框架不好用。那么不用框架,我们又该怎么做呢?
实际上,如果你只是想实现单纯的模型—视图—控制器(Model View Controller)分工职守,它只是一个架构模式而已。将模型和视图的代码分开,并提出控制器的代码,然后互相调用各自方法就算完事了。Model的全部引用放在固定的位置,View的引用使用静态属性储存或者用管理类管理,Command可以作为函数或者类直接初始化并执行,亦可以通过反射。这并不需要专门的工具类来辅助,附加成本也比较小,自然就可以适用于任何规模的项目。
当然,你可以实现一个简单的通信框架,提供必要的功能,如果你需要的话。这和使用一些专门的MVC框架需要的成本是完全不同的。
然而,我的意见则是——MVC是非常好的架构模式,不管什么样的项目都建议尝试使用,但是用框架的话,请务必谨慎。
关于最简的MVC,最近看到一个让人很囧的例子。不过这个例子对大家理解MVC是有帮助的。

这玩意的确……基本算是MVC,只差一点而已。MVC是架构模式,至少结构上要分开,即使不分文件至少要让能看得出来谁是谁(原文可没有红字),所以只需要把这些代码分成三个文件,那就可以称得上是最简的MVC了。
这是我在他的下面补充的代码。

结果是,View只关心与自己相关的Model和Command,Command只关心与自己相关的View和Model,Model谁都不关心,这和一般情况需要的解耦目标是一致的。虽然这样并不算完全解耦,但是至少在思路和逻辑分离上是做到了,仅仅是协作方面存在问题,比如无法实现自由的并行开发,而这个加入简单的反射也可以解决。
所以,单纯的MVC并不困难,没什么要不要放弃一说。还有就是上面只是极端例子,但就算是这种东西,比起完全不实现MVC,也至少实现了50%以上的内容。
是否使用框架应当理性对待
程序员都是理科生,应当用理科生的思考方式(当然我并没有让你们都去模仿Sheldon)。在使用MVC框架的过程中,不管是觉得好,还是差,都要考虑清楚问题的源头在哪。
觉得MVC框架不好用,降低效率,是否曾经有过平行对比的例子,你能否确认不用它效率就确实能提高?效率低有没有可能是框架之外的原因?
觉得MVC框架好用,提高效率,是否有平行对比的例子?你怎么就知道是使用了框架的功劳,而不是规范了代码结构,制定了新的协作流程,甚至是开发人员水平提高的功劳?怎么知道MVC框架并没有起了反效果?
使用了框架,看到了结果,然后根据结果的好话直接判定框架的好坏,这太武断了,作为一个理科生,我们绝对不能这样做。至于那类连比较都没有,而是以“我用了框架,项目依然完成了,没有因为用了框架而失败”这种理由来支持使用某个框架的人,我无言以对。
推荐MVC框架
我并没有完全反对使用MVC框架。这要看你的项目类型,规模,人数。满足条件的时候当然可以使用。尤其是在企业应用里,如果你有幸出现六个客户端的话,没MVC框架可能还真是会出问题。
pureMVC和Cairngorm是两个较早出现的框架,目前我不建议再使用它们。pureMVC的问题在于过于强调分离而缺乏实际功能,提供的便利很难抵消它本身的消耗,性价比较低。Cairngorm的问题则在于过于强调模型更新视图的流程,限制太多,灵活程度不够。
后出的几个框架就好多了,Mate使用了一个全局事件定义,配合FLEX写法非常简略。Swiz则是用控制反转+依赖注入,也就是Spring的做法,而且元标签注入的方式很有趣,感兴趣的可自行查阅资料。
我这里要说的是Robotlegs。这是一个和Swiz非常相似的框架,但也有一些自己的特点。首先它是基于pureMVC的,你依然可以像pureMVC这样来使用它,对于相信pureMVC的团队它是很容易接受的代替品。他让pureMVC也同样拥有了控制反转和依赖注入,包装了大部分功能,配置代码大大减少,而且不管用不用FLEX framework都可以很自然地使用它。
Robotlegs的教程可以看这里:
http://wenku.baidu.com/view/42a08b235901020207409c60.html
但我得提醒大家,虽然我觉得Robotlegs很便利以及有趣,但是并没有在项目里使用它,因为我的项目规模不大,而且是游戏。实际上,我甚至自己实现了一个依赖注入框架,可以很简单的加入到现在的项目中,成本几乎为零,却依然没有去用。使用一个东西要看是否需要去用,而不是可以用就用,更不是“因为用了没有遇到问题所以就用”。用一个东西必须有收益才可以,尤其是在明明看到有损失的时候。仅仅是用“看起来更正规”这类自我满足的理由来决定自己的行为,太愚蠢了。
当然,如果你需要它,那就应该毫不犹豫的使用。不要受到抱怨框架的人影响,他们大部分都有自己的问题,提出的理由也未必是正确的,你并不一定会赴他们后尘——前提是你真的需要它,而且,要将使用框架需要的条件全部补齐。
就算是使用MVC框架也不需要完全解耦
解耦是一个扩展性要求,但扩展性要求并不是越多越好的。
这是一个普遍的误区。诸如使用pureMVC的人,很多都纠结于完全的解耦,以至于用了Command,在Command中改变View的时候还是必须要发一遍Notification。
Command这种类,一般都是在相关的View,Model完成后才开始编写的。比如普通的StartupCommand,OpenWindowCommand,没有对象又如何编写?它在编写顺序上应该是,就算不是也是可以放在View和Model之后的。那么在协作关系上,他就可以直接访问所有相关类,不需要为了这种原因而解耦。
虽然pureMVC将消息全局化了,但是消息实际上是分局部和全局的。比如一个Proxy发生变化要求所有监听某个消息的View更新,那么当然应该发一个叫做I_AM_CHANGED的Notification,并由不同的View来监听这个消息并更新,这个就应该是全局消息。但有些消息就是局部的,是一对一的,比如一个叫做SEND_DATA_TO_WINDOW1的消息,按它的字面意思就应该是刷新WINDOW1,那么由它来触发的Command,就应该直接耦合 WINDOW1这个View来设置值,而不是再发个类似REFRESH_WINDOW1的消息,因为SEND_DATA_TO_WINDOW1的名字已经确定是针对这个View了,如果最终它却没有操作这个View,那才是有问题的吧?
解耦归解藕,但是对于已经有了意义上的联系的模块,结果却不耦合,在任何时候都是没有意义的。即使需求变化,逻辑变化了,使得SEND_DATA_TO_WINDOW1最终不是改变WINDOW1的数据,而是WINDOW2的数据,那么这个Command连带相关Notification的名字就必须修改,也就是说,意义上的紧密联系,在实际操作上和耦合了是一回事。既然已经是这样了,再做成不耦合,给自己制造麻烦的又有什么意义呢?
除了上面的情况,我们也要考虑,真的有必要将项目拆得那么细致么,有没有必要为了1%以下的可能性来解耦两个相关性很强的部分?比如一个叫做ShopPanel的View和一个叫做ShopModel的Model,到底在什么情况下,ShopPanel会不去调用ShopModel,而是别的东西?而且Panel上可能还有各种文字,使得自己意义上只能调用商店的数据。真是要调别的东西,应该重新制作一个新的View吧?而且别忘了pureMVC是可以多个Mediator套用一个View的。这种情况下,我们直接在Mediator中耦合ShopModel,有什么不可以的?当然,反过来,ShopModel被多个View调用的情况很普遍,所以我们不能让它来耦合ShopPanel。这些规则实际上是很明确的,是完全可以预知的。就算预知错误,也是很容易修正的。
解耦是手段,而不是目的。盲目的最求扩展性,只会让自己的程序变成一盘散沙。正是适量的耦合,程序才能拥有一个确定的形态,才不会让人感到茫然。
普遍误解
其实现在使用框架的人群里,真的能够发挥框架长处的确实比较少,尤其是在水平层次较低的Action Script开发人员之中。一方面,这污染了框架的名声,同时也是不建议使用框架的理由之一,因为人员水平限制也是实际项目中不可回避的现实问题。
如果你决定使用MVC框架,就必须提高自己的认识。我拣几个最常见的问题来说吧。
- 并不是用了消息通信就算用了MVC
- 既然用了MVC框架,就不要图省事
- 是Mediator知道View的一切,View完全不知道Mediator,而不是相反
消息通信只是一个手段,只使用框架的通信功能在View之间发送消息的话,而将其他功能全部抛弃的话,直接使用事件更好,那还套一个MVC框架就是在没事找事了。
MVC关键还是在于代码逻辑的分配,通信只是个附赠品而已。要了附赠品而扔了原来的商品——咱们买的不是小浣熊干脆面,对吧。
但是如果你的目的就是赠品,其实也没什么。比如你就是想用的这个通信框架来发发消息,就不打算用它的MVC,又或者MVC部分是自己实现的。那么我还是建议你把消息部分干脆也自己实现了,别人的始终没有自己的好。
要清楚,松散耦合不仅仅是一个形式,目的在于减少模块间的联系。因此,如果你一方面在解除两个模块之间的耦合,一方面自己又没头没脑的将其他模块的内容耦合进来,就会使得你的行为变得没有意义。
现在一些人一方面在硬套框架,一方面又图省事而随意引入其他类,就属于这样的行为。那些类是可以引入,但你这样做,框架本身的意义就没有了。要不你就不用框架,要不就别这样干,这里只能二选一。
对于使用pureMVC的同僚们,我真是不明白你们到底是怎么把这个反过来理解成“View知道Mediator的一切,而Mediator完全不知道View”的,因为官方实例上写的很明白。估计是把mediator当成通信专用的模块类了吧。但是如果你放弃了mediator分离View代码的特性,只是用来通信的话,至少要保留原来的通信功能,就是让Mediator依然可以直接访问View。否则既然Mediator是用来通信的,它却不能操作View,结果还得设法和View再通信一次……
pureMVC要求“View完全不知道Mediator”是为了能够在不修改View的情况下更换Mediator,但这种需求并不多(多的是在Mediator不变的情况更换View,这个需要用接口或者条件判断解决),所以可以放宽点让他们互相引用,这样两者通信都能畅通。
pureMVC实现“View完全不知道Mediator”的方法是用Mediator直接去监听View的某个组件的鼠标事件。这只需要监听一次,也不需要传递消息。Mediator存在的期间,按pureMVC的标准View应该是没有任何监听逻辑的。
扩展阅读
http://baike.baidu.com/view/31.htm
http://www.360doc.com/content/09/0804/08/163747_4655702.shtml
http://hi.baidu.com/5%B1%CF%D2%B5%D2%D4%BA%F3/blog/item/2a018366a08e54cde6113af7.html
http://hi.baidu.com/lwcandwo/blog/item/8fb4b3036d01eb8ad43f7cf4.html
透过现象看本质
首先是个转场特效问题。

其实我早想到做法了,只是当时还没实践。增加混合(BlendMode.ADD)只要关系到光效,泛用性就很高,叠加着画上去看起来应该就是这个效果。
但是我还是先到群里问了下,然后一帮人就跟我说径向模糊。还有一帮人说以前搞过,拿我的原图上了个径向模糊发给我。但是径向模糊多慢用过PS都知道,怎么也不可能用在动画效果里的。这个东西的确有点像径向模糊,但是像什么就做什么,从表面着手,思考方式就有点单纯了。
实际上做法再简单不过,调整Matrix缩放和旋转图形,一点点放大,然后用增加混合模式将原图叠加着画上去即可。
var m:Matrix = new Matrix(); m.translate(-bmd.width / 2,-bmd.height / 2); m.scale(scale,scale); m.rotate(r); m.translate(bmd.width / 2,bmd.height / 2); screen.bitmapData.draw(bmd,m,new ColorTransform(1,1,1,0.2),BlendMode.ADD);
增加混合是个很有趣的东西,可以衍生出很多东西,这个以后再说。
短时效果,近似方式可以更大胆
某天看到了个很牛的玩意,一个哥们做了个很酷的模拟水滴从墙面上滴下来的效果,公布了详细的思路与做法,涉及不少的数学知识(http://jamesli.cn/blog/?p=631)。我们在这只就他这个效果一个很有趣的细节做讨论——两个水滴接近时会自动融合成一个。
水滴本身是一个多个节点的曲线,那么这个融合过程该怎么实现呢?这时候一个没看作者代码的人就说话了,而且还画了图。总之,他的办法就是当两个水滴相交时,先检测出相交面积,然后找到重叠部分的曲线的点,将这部分点删除,再将剩下的点平滑连接到一起,这样两个水滴就合二为一了。好吧,至少这个没有要求做曲线相交,但剔除点之后重连接还是要有一定数学知识,到底哪两个点连接?连接曲线的曲率是多少,怎么才算平滑?连接成一体的新的水滴怎么才能恢复类似圆的形状?
而作者的代码很简单。他的做法仅仅是让较小的水滴快速移动向大的水滴并渐隐,然后把两个水滴的面积相加求出新的半径,小水滴删除,大水滴直接变大。这个和真实融合的情况相差甚远,但是由于融合过程很快(现实中确实也很快),如果你不使用变速齿轮,自己也没有子弹时间似的超级视觉,根本看不出具体融合步骤,也就看不出破绽。

都说图形编程数学要好,这的确不错。但实际上除了一些特别恶心的需求,甚至都不需要动用高数的知识。而且如果需要用到高数知识,往往也是光高数知识搞不定的。如果一个问题觉得非得用高中程度之上的知识才能解决的话,有可能就是钻进了牛角尖。近似方案,足够了。
算法问题并非都要全部通过公式解决
这是一个实际问题。我曾经自己写了一个翻页效果,并用在了项目上。翻页实际上很难写,虽然都是解析几何的知识,但要判断多种情况,各种翻页方式的绘制方法是不一样的。所以最后我只实现了从左上向右下的翻页,当时足矣。
但是后来美术提要求了,他希望做个从右上向左下翻的动画(这是一般翻书的习惯),但因为这个是用解析几何计算出来的,在不同方向上,公式是不同的。而这段代码里全是已经移项后的方程,根本无法还原,隔得时间也太长,换个方向差不多就要重写。我头痛了。
后来我在那无聊,把整个绘制容器翻来翻去,设置scaleX,scaleY,然后看到,翻过来以后,虽然鼠标,图像全都倒置了,翻页本身的效果倒是挺正常的,只是翻转了。垂直翻转后,由左上向右下就成了由左下向右上,水平翻转后,则成了从右上向左下。好,我要的效果出来了。
鼠标和贴图不对?改下就好了,鼠标很好弄,贴图嘛,绘制的时候先翻转好就成。

终于不用写四次同样的算法了。虽然其他人写的翻页可能能用一套公式兼容多个方向的情况,但我的数学水平没那么好。但就算这样不也做出来了?条条大路通罗马,不修改绘制方向,而是改变整个容器的方向,虽然不是标准答案,却是很理想的应急方案。在这里,它几乎可以永久使用。
固定动态内容可转化为动画
大家小时候都玩过那种投色子在格子上前进的纸上游戏吧。我以前挺热衷的,还常常自己画棋盘,各种大小格子跳转。如果棋盘都是很整齐的直线和折角的话,那自然好说,但如果棋盘本身的线路就是扭来扭去的,或者说本来就是美术乱画的呢?
首先想到的是做锚点,棋盘就当做背景图了,旗子都是在这些看不到的点上移动。这里直线移动还是好做的,但是曲线……除非你打算用直线近似代替,否则又要搞什么贝尔法……
但如果你的这部分只是一个小游戏,不关心扩展的话,有一个只有FLASH才能做得到的方案,对于AS2时代的人可能会比较亲切,AS3时代进来的人可能根本就见过这种搞法。
咱把棋子移动整个做成一个动画吧。

有分支就跳转不同帧继续播放,棋子走的时候就gotoAndPlay(),移动仅仅是控制播放这个动画,这样什么转弯全都是动画,做些花哨的动作比如腾跃也都行了。这个的缺点就是只能做固定的行走路线,如果人物可以随时转向就有点困难(也不是不能做)。
小游戏有小游戏的搞法,FLASH也正是因为可以支持这些小游戏的搞法才能发展到现在。虽然是老掉牙的东西了,有些时候还是可以用用的。老的东西并不会被完全取代,毕竟它还是有自己的便利之处的。既然用了就能豁然开朗,那么它就是这时候的,最佳选择。
代码级简化
说得挺多了,最后再介绍一些写法上的简化方法吧:
- 正弦震动
- 九宫格方向
- 显示对象排序
实现一个元件的震动,物理方式模拟实现是最傻的,用Tween模拟多次缓动一样也好不到哪去。而震动指的都是正弦震动,所以我们用Math.sin()处理y轴就可以了。
你甚至不需要定义一个递增变量来处理时间,只需要在开始直接t = getTimer()来记录初始时间,然后像这样y = Math.sin((getTimer() - t) / T * 2 * Math.PI) * R(getTimer() - t得出的是经过的时间,T是震动周期,然后乘以2π,就是sin函数需要的参数,而再乘以振幅R即可,这都是中学知识)
停止用setTimeout执行一个函数即可,振幅一样可以用getTimer() - t作为变量递减。
一般人物动画用的8方向序列帧,需要根据鼠标指示的方向来显示对应方向的循环序列,平常的做法就是写上一组九个的IF语句,分别判断九种情况并设置九种序列帧。这没有问题,但实际上有更简单的做法。我们可以认为x轴方向有3种状态(左,中,右),y轴方向有3种状态(上,中,下),而这些状态相互组合形成了结果的9种状态。如果这三种状态分别以数字0,1,2表示的话,可以用公式y*3+x直接得出一个状态值(y是纵向的状态,x是横向的状态),而这个结果则是这样的:
0(左上) 1(上) 2(右上)
3(左) 4(中) 5(右)
6(左下) 7(下) 8(右下)
这是一个9宫格,拉成一行就是一个数组,可以将各个方向的序列帧存在这个数组中,然后判断一次x的状态值,一次y的状态值,然后直接用arr[y*3+x]就能取出对应的序列帧,这比写上一组case或者if都要简短得多。
最后是最标准的显示对象排序问题,只是单独排序一个物品并没什么技巧,从头到尾循环并比较就对了。但是如果是将一组混乱的数据按大小排列的话,不同排序方法的差异性就会体现出来。
有的人会用最简单的冒泡排序,但那个效率很不理想,所以有人就大张旗鼓地表示可以用分治(快速)排序来优化,但是分治排序写起来比较复杂,不少人都没背下来(包括我)
但实际上按时间复杂度来测试,Array.sort方法的结果看上去就很像分治排序。本来就是个系统函数,分治排序又没有缺点,应该用的就是这个,而且是原生方法速度也很快。但深度排序需要交换层,sort是个函数,其过程无法干预。
但只要使用参数Array.RETURNINDEXEDARRAY(记得也要用Array.NUMERIC指示按数字排序,否则中途会转换成字符串不仅错误而且慢),最终就会返回一个数组,下标是原位置,值是新位置,然后根据这个数组重新执行一遍setChildIndex就行了,显示对象序列也就和被排序的数组完成了同步。
顺便给份测试结果,毕竟换层有多种方案(setChildIndex, swapChildren, swapChildrenAt),整个排序主要的消耗都在交换层级上,因此换层的方式很影响效率。
最短路径原则,就是将复杂的问题简单化。
达到目标并不是只有一条路,眼前的那条往往也不是最短的一条。所以,解决问题前的第一步,应该是要找实现目标的最短路线。虽然有些人可能会喜欢完成些复杂的算法来获得成就感,但这就是另一个话题了。
要明白,我们是作为实现工具的工人,而不是授命在空中楼阁中研究的学者。
魔术师视角,而不是观众视角
首先是一个比较典型的例子。
那位兄台提出这个问题的时候,问的是碰撞检测。
而且是不规则形状,有凹的也有凸的碰撞检测。判断两个物体是否边缘匹配,可以拼在一起。最后还要在放下时自动检测周围的方块,并自动吸附。必须得说,这个课题真的很困难,倒不是说找不出方法,而是找不出效率可以接受的方法。优化的办法应该是有的,但是我不会,因为这个还得吸附啊,我哪知道哪边才是边。但后来我觉得不对劲,就问了下他实际要做的东西。
才知道这家伙原来是做——拼图。
所谓拼图,就是一个图片被拆成各种不规则形状然后打乱,然后让玩家重拼起来,需求就是这样。好吧,既然是这样,那么记下这些碎片的原始位置,然后判断你手上的碎片目前坐标是否接近这个位置不就好了。
电脑没有必要使用人的思维,设计者没必要使用玩家的思维。完全模拟,并不是最好的方案。
首先考虑简单的近似实现
老外的东西总是能有惊喜。老外的思路常常都很单纯,过度设计的情况很少见。不管怎么说,他们的经验还是要丰富一些的。虽然一个需求怎么都能实现,但细微之处的积累依然会产生大的变化。这次嘛,是关于一个同事在玩的SNS游戏,它里面捡钱会曲线飞到表示钱的数字上这个效果挺别致的。
国内的情况,连做个直线飞就已经多余了,更不要说曲线了。但曲线的效果的确比直线生动很多。然后我就在仔细观察它的轨迹,考虑它是怎么实现的,想做一个类准备着以后来用。一般的想法,是用二次贝尔法曲线公式计算出运动轨迹,虽然需要一定的数学公式但曾经做过应该不成问题,而且TweenMax也提供了这个功能,但既然是曲线就有曲率的问题,而参考的效果并不是固定的1/4圆,况且这种做法有些太小题大作了。然后就是考虑物理移动,但原效果看起来使用了圆缓冲,越接近目标越慢,而且是平行y轴结束,这个用物理模拟会比较困难……
最后我脑子突然灵光了。让X轴方向用匀速移动,Y轴方向减速圆缓冲移动,看起来貌似就是画面里的样子……
也就是代码:
TweenLite.to(target,{x:tx });
TweenLite.to(target,{y:ty,ease:Circ.easeIn });

其结果就是弧线,和原品一模一样的。很显然,这还要做类会很无聊。
即使有复杂的解决方案,也应该考虑是否有更简单的方案。复杂的方案只是适用范围更广泛而已,但既然有“这么简单”的,何必去费那个事呢。
到现在还不知道TweenLite是什么的参见以下地址:http://www.greensock.com/tweenlite/
“根据需求选择技术”VS“将适用的技术用于需求”
说到这里,正好提下四叉树的问题。四叉树的确是一个很经典的解决屏幕物品筛选遍历的方案。具体是怎么做的可以参考网上的其它资料,可惜原理讲起来有些拗口,实现也比较恶心,所以目前并没有被广泛使用。
但是这东西不就是筛选屏幕内物品用的么?因为有两个坐标,可以将物品按坐标归类。这里我们又犯了“将适用的技术用于需求”而不是“根据需求选择技术”的错误。四叉树的确可以用来快速筛选屏幕内的物品,但并不是说筛选屏幕内的物品就只能用四叉树。要知道四叉树可以处理任意缩放屏幕,以及无限大的坐标系内的快速筛选,但我们实际上需要的就是几十屏以内固定屏幕大小内的筛选。
实际上,有个很好理解的代替方案。我们可以创建一个二维数据,诸如地图是10000*10000的,屏幕大小是1000*1000,我们就创建一个20*20的二维数组,然后将坐标范围在(0-500,0-500)的物品存在数组的(0,0)项内,将(500-1000,0-500)存在数组的(1,0)项内,将(1000-1500,0-500)存在数组的(2,0)项内,将(1000-1500,500-1000)存在数组的(2,1)项内,将(1000-1500,1000-1500)存在数组的(2,2)项内……然后将地图里的所有物品按这个方式保存在二维数组里,物品移动时则更新数组。
到时候要取屏幕范围,就以屏幕中心坐标开始,除以500获得一个区块的坐标,然后取他周围的九个区块,这些区块内保持的物品实例肯定覆盖了整个屏幕。虽然会多出一些屏幕外的物品,但这点多余消耗是可以接受的。

这和使用四叉树的结果是相同的,都可以快速定位屏幕内物品,做到游戏中存在大量物体,但只要不同时出现在同屏,就不会耗费过多遍历性能的目的。
关于四叉树遍历可以参考衰人的日志:(http://wxsr.blogbus.com/logs/60788934.html)
特殊情况应当使用特殊处理
子弹会穿墙,这是个经典问题。
现实的子弹是线性移动的,电脑中的却不是。子弹的移动一定是间隔进行的,而子弹的速度很快,体积小,墙壁薄的时候,就有几率正好跨过去导致碰撞检测失效。这其实是满经典的问题,像以前玩沙罗曼蛇,全部吃加速吃到一定程度就可以玩穿墙了,毕竟碰撞检测是一个高消耗的操作,能简化就简化,尤其是在飞行射击游戏出现大量子弹的情况下,采用复杂的判断逻辑实在不合算。
需要注意到,在设定好的环境里,子弹穿墙的几率实际上是很低的。这种游戏子弹是要能看到并躲避的,速度就不能太快。就算那子弹的确比较快,只要你墙壁别薄到看不到,一般也是穿不过去的。如果这两个条件都满足,我只能认为你游戏设计稍微有点问题。
好吧,说到解决方案,最彻底的当然是计算移动轨迹并判断是否和障碍物的边缘相交,这并不算困难,但是性能消耗会翻倍,仅仅是为了特殊状况,确实有这样做必要么?
如果游戏里所有子弹都是会穿墙的速度,这游戏基本就不用玩了。所以,能穿墙的只会是某些特定的高速弹。仅仅为了这个而使其它正常速度的子弹的碰撞判断降低性能,这个做法并不妥当,那实际上可以怎么做呢?

你只要将你的特定子弹尾部延长就可以,就是修改子弹素材,在尾部添加一条透明的尾巴并参与碰撞。虽然看起来是点,却是线。这样要想穿墙难度就高很多了。而这个延长部分是感受不出来的,因为它是高速弹,来无踪去无影,不可能抓得到尾巴。
步骤的对调可能大幅简化逻辑
当时是心血来潮想做个爆炸效果。就是那种哐啷一声一块玻璃被切成一片片散落的样子。例子可以参考FFX的战斗切换,以及DMC3关卡结束的切换动画。
这个效果其它部分都很简单,难点在于切割。碎裂的方式是随机的,首先是创建一堆随机的点,然后三点组合成三角形,问题就在于如何组合。三角形当然是不能交叠的,所以你需要将接近的点组合成三角形,而且保证它们不会叠在一起,问题就转变为“什么样的点才叫接近的点”。这个算法倒是在哪看到过,不过我不记得就是了……
查也查不到,所以我就找个了近似方案代替。这里关键的问题在于点的随机性,只要点不是随机的,组合三角形就可以用((0,0),(0,1),(1,0)),((0,1),(1,0),(1,1))这个约定的组合,然后保证随机的时候不会交叉就可以了。
首先是创建出不考虑随机的状况,差不多就像下面这样,平均切成矩形,矩形再分成两个三角形。此时顶点到三角形的组合规律是固定的。

把这个作为原始状态,再调整顶点的位置,可以看到,即使将各个顶点分开移动,每个顶点只要还在矩形范围内活动,三角形就不会交叠。

其结果就是这样,虽然不是完全随机,但基本可以符合要求。

除去必须在边缘上的点之外,中间自由活动的点的坐标可以这样简单地求出来
x = dx * (i + Math.random()) y = dy * (j + Math.random())
其中dx,dy是分割的小矩形的长宽,i,j是顶点的x,y序号。
换个思路,就有新的方向。先创建顶点再组合三角形,和创建三角形再调整顶点,难易程度就会完全不同。
【编者按】随着富互联网技术(RIA)的潮流,Flash技术在互联网中的应用越来越多,国内Flash开发者数量也在以惊人的数量增长。本专题——务实主义,将主要介绍如何在Flash开发过程中尽量提高自己的开发效率,不为花哨,只为务实。文章中会提及许多flash开发中的实用技巧,期望对各位看官能有所帮助。
不需要的战争
很早以前就有一种说法——技术行业里最无聊的莫过于语言战争。最典型的就是Java与.net之间旷日持久的战争与和平,期间掺杂着公司的宣传手腕,永不休止的针对细枝末节的口水战。一件事情要争论起来始终都有素材,问题在于这种大规模的嘴仗,即使产生了结论也难以被沉淀下来。而且,其结论往往也无法产生任何有意义的效用。最终该用.net的还是用.net,该用Java的还是用Java,任你怎么宣传,也许能改变初级开发人员的流向,但真正决定一个语言是否被使用的是语言本身的特性,对特定需求的适应程度,而语言的使用数量则决定了开发者的需求量。这是一个完全相反的因果关系,开发人员对语言的影响,微乎其微。
所以说,讨论这些就是些无聊的事。语言之间的对战尚且如此,若是内战的话,岂不是太过愚蠢了。
这次的针对点不是Silver Light,也不是HTML5,焦点矛头正是FLASH本身。
FLASH在历史上复杂性和特殊性
也许对于程序员来讲,FLASH就是AS的代名词,但很长一段时间都不是这样。FLASH很长一段时间都是动画,甚至还不是交互式动画,说的直白点,就是网页广告。比起一般JPG,GIF方式存在的广告,不仅体积更小,效果体验也要领先很多。而普通视频流的广告由于水管原因实际上是不能实际应用的。所以FLASH便活了下来。之后的FLASH卡通动画之流都是副产品,毕竟缺乏商业价值。而由于FLASH自己基于元件和时间轴创作的特性,很适合向里面加入脚本引擎。虽然AS1时这个引擎提供的功能还很弱,但是基于有IF和GOTO就能写出任何功能的机理,AS1提供的功能已经很多了。所以网站小游戏一类自然也就冒了出来。再之后,有了AS3,基本解决了语言本身的障碍,开发稍大的项目成为了可能。虽说FLEX只是一个官方framework,属于AS3诞生的产物之一,这样的framework会继续出现,总有一天会被其他的各式框架淹没……但在现在看来确实扩展了企业应用方面的使用,一些网站和游戏还有应用程序类的也可以使用。至于AIR这个玩意儿……
可以看到,FLASH的功能一直在增加,但这种增加产生的影响除了增强原有功能,还有很大一部分是在扩展新的适用领域,并不是对原本内容的升级和取代。
新事物和旧事物的竞争与共存
诸如,自从AS2出现类之后,与原本一直使用的时间线编程的关系。
实际上,时间线编程是与动画结合最好的一种方式。时间线编程仅仅是把代码插入到动画中,只是让动画转变为交互动画。如果你的网站主要是动画,你的游戏很容易转换成动画的形式,那就很合适。即使在现在,制作一些游戏产品宣传用网站,时间线编程也是很适用的手段,优点自然是单文件,简单的自加载,直观快捷,即使是美术人员也很容易掌握,缺点则是表现受到限制,只适合用在固定动画上,而且美术和编程人员的协作模式在修改方面相当低效,只适用于基本不进行修改的一次性产品。只不过,这样的产品实际上非常多。
新事物想要取代旧事物,需要的条件并不是只是“比以前好”,而是“比以前好很多”。确实也应该是这样。技术的更替需要不小的成本,不仅仅是人员的培训成本,还包括使用风险一类,原有经验的不通用,流程的变化,如果最终结果还仅仅是比以前好,那这就算白忙活了。如果老老实实写外部类来做这种一次性的网站,虽然的确可行,也不一定会多费多少成本,但没有好处的话,为什么做?当然,对于并非一次性使用,需要常常更新,或者是一些需要反复根据需求变更,而且是程序和美术都需要变更的情况,实行代码与资源分离,不管实现起来有多麻烦,因为会“比以前好很多”,最终的时间成本都是合算的。这种情况依然固守旧模式,终将自食其果。
相对的,AS2与AS3的区别也在这里。如果说到更换成本,这个才是最高的。两者完全不相兼容,类库也不通用。但是,新功能和效能先不提,AS3提供的语法结构更加标准,更接近Java等语言,更加静态,也就更适合来编写大型程序。单是编译期间查错和代码提示就是无法取代的决定性功能,能够增强代码的稳定性,在多人以及修改时将减少大量时间成本。但是……如果你的程序基本就是一次成型的,程序规模也很小,诸如一些简单的小游戏,AS3的这些优势就很难发挥作用。但如果的程序规模较大,而且面临各种重用问题,继续AS2最后一定会后悔的。
综上所述,一个问题,在不同的情况下,就是有着不同的答案。任何一样东西在特定条件下都有它的用途,因此,谁都无法推翻或者取代谁,这就是争议背后的结论。
对于开发者
有人说,FLASH每次更新对开发人员来说就意味着所有人退回到同一起跑线。这是对的也是错的。如果开发人员永远只做一种东西,在这里其实是错的。但开发人员实际上是根据需求跑的,既然开始大量做大型WebGame,做的东西也越来越复杂,就已经做的不是同一个东西了。做不同的东西却始终想用同样的办法,那还能不死么?但是对于一些小游戏,乃至网站,真的有必要用和大型网游一样的方法来做么?这也许就是争论双方的矛盾所在。
但对于开发人员而言,学习新东西毕竟能增加自己的竞争力,对目前的工作没什么用,这不是不学它的理由。开发者本来就是一个相对“凄惨”的群体,偷懒,可是会死的。
真的会死的。
目前FLASH开发者类别(不包括美术路线)
- WebGame客户端工程师
- FLASH网站开发者
- 手机游戏开发者
- 自由游戏开发者
- 网页应用开发者
- 企业应用开发者
- 课件开发者
- 原型开发者
- Scaleform界面开发者
关联阅读
http://as3blog.com/as3/as3tip-new-philosophy/
http://wenwen.soso.com/z/q119628245.htm
关于作者
flashyiyi,9RIA.com天地会元老级舵主,真名唐翎,1986年出生,《升职记》客户端主程。目前在昆仑万维工作,之前曾在智明星通进行《开心宠物》等项目的开发。开源项目GhostCat作者:http://ghostcat.googlecode.com。个人空间:http://uh.9ria.com/space-12147.html
D语言是一种由Digital Mars公司创造的系统编程语言,它专注于“把C和C++的能力和高性能与先进的语言——像Ruby和Python——的高生产力结合起来。” 尽管它仍然是一种静态类型的语言,可以直接编译成本地代码,而且语法看起来与Java或者C#非常类似,但是它做出了很多有意义的改进。
这些改进包括明确地支持惰性参数。当我们使用关键字“lazy”标记一个形参时,就意味着相关的实参会被赋值0次或者多次。这与Haskell之类的语言不同,它会延迟对实参赋值0次或者1次,但不会再多。
为了保证程序的正确性,特别是在并行以及多线程的领域中的正确性,D语言支持把“immutable”和“const”作为一等概念。和C++不同的是,在D语言中的immutable和const修饰符是可传递的。如果我们用immutable标记了一个变量,那么不但不能对其进行变更,也不能变更通过它所能够访问的任何变量。const关键字的工作方式和immutable类似,不过特定的对象还是可以通过non-const的引用来修改变量的值。
D语言还包括了很多来自于其它语言的流行特性。例如,它拥有for-each循环、字符串转换、数组分片、闭包、try-catch-finally块,以及值类型和引用类型的分离等等。同时,它还考虑到了直接对内存的访问,80位的浮点型数字、结构成员排列控制以及内联程序集。
通常,因为安全性,把源代码转换为本地代码会付出高昂的代价。C和C++是最容易受到类似于缓冲区溢出之类的编码错误影响的,其它大多数本地语言也都一样。D语言通过既在编译时又在运行时执行数组边界检查来避免这些问题。同样,非零长度的数组的内容会自动初始化为它们的默认值。
关于内存管理,D语言和.NET非常类似。默认情况下,引用类型会存储在垃圾回收堆中,而值类型或者会存储在堆栈中,或者存储在其它对象中。D语言也可是分配堆栈来存储引用类型,但这不可避免要使用指向它们的指针。和.NET一样,你还可以让指针指向非托管的内存。让这些彼此分离非常重要,因为很多你能够在非托管的指针上能够执行的操作,在托管指针上执行是不安全的。
对于能够使用非快捷版Visual Studio的开发者来说,有一个项目叫做Visual D,它提供了Visual Studio 插件。Lloyd在一篇文章中介绍了如何在你的环境中安装Visual D。
当前D语言有四种实现。Digital Mars公司为Windows、Linux、OS X和FreeBSD提供了免费的编译器。还有一种Gnu D编译器,它是构建在gcc之上的。而LLVM也发布了他们的D语言编译器的beta版本,最后还有针对.NET的部分实现。