在集成第三方库时,特别是像Box2DAS3这样的端口库,它们保留了父语言的编码和命名约定,而没有完全与目标语言集成,这是一个相当普遍的问题(例如:Box2DAS3使用getFoo()
和setFoo()
而不是.foo
getter/setter)。
简单回答你的问题,使用包装类不会对性能产生重大影响;除了在项目中看到的类层次结构外,不会有更多的影响。当然,如果你计时5百万次迭代的循环,你可能会看到一两毫秒的差异,但在正常使用中,你不会注意到它。
"内存消耗增加以容纳额外级别的类结构。"
像任何具有类继承的语言一样,后台将使用vtable,因此你将会有一些内存/性能上的小幅增加,但这是可以忽略的。
"由于初始化额外级别的成员而创建新对象时的性能影响?"
不比正常实例化多,所以除非你正在创建大量的对象,否则不需要担心。
就性能而言,通常情况下你应该没有问题(优先考虑可读性和可用性,除非你确实有问题),但我更愿意将其视为架构问题,并考虑在这方面扩展/修改外部库的类时可能会产生的负面影响,通常分为以下3个方面,具体取决于你要做什么:
修改库
由于Box2DAS3是开源的,所以没有任何阻止你随心所欲地重构所有类/函数名称。有时我也会认真考虑这样做。
优点:
- 你可以修改想要修改的任何内容 - 函数、类等等
- 你可以添加任何需要的缺失部分(例如:像素-米转换帮助程序)
- 你可以修复任何潜在的性能问题(我注意到一些东西如果按照“AS3”的方式完成,可以更好、更快)
缺点:
- 如果你计划保持版本最新,则需要手动合并/转换任何更新和更改。对于流行的库或那些经常变化的库,这可能是一个巨大的痛苦
- 这非常耗时 - 除了修改之外,你需要很好地理解正在发生的事情,以便在不破坏功能的情况下进行任何更改
- 如果有多个人使用它,则不能像使用外部文档/示例那样依赖于它,因为内部可能已经发生了更改
扩展类
在这里,你可以简单地创建自己的包装类,它们继承了基本的Box2D类。你可以按照需要添加属性和功能,包括实现将转换为基础类的自己的命名方案(例如
MyBox2DClass.foo()
可以简单地作为
Box2DClass.bar()
的一个包装器)。
优点:
- 你只需实现所需的类,并进行必要的更改
- 你的包装类仍然可以在基本的Box2D引擎中使用 - 也就是说,你可以将
MyBox2DClass
对象传递给接受
Box2DClass
的内部方法,你知道它会起作用
- 它是三种方法中最少的工作量
缺点:
- 同样,如果你打算保持版本更新,你需要检查任何更改不会破坏你的类。但通常不是什么大问题
- 如果你创建调用其Box2D等效函数的自己的函数(例如“我应该使用
setPos()
还是
SetPosition()
?”),可能会导致类别混乱。即使你正在自己的项目上工作,当你在6个月后回到你的类时,你会忘记的
- 你的类将缺乏一致性(例如,有些函数使用你的命名方法(
setPos()
),而其他函数使用Box2D的方法(
SetPosition()
))
- 你将被限制在Box2D中;如果你不打算更改,那么没有多少开发量。但是,如果你的类在整个项目中使用,则可能需要大量的开发
通过自己的类进行组合:
你可以创建自己的类,它们内部持有一个Box2D属性(例如,
MyPhysicalClass
将具有一个
b2Body
属性)。你可以按照自己的方式自由地实现接口,并且只暴露必要的内容。
优点:
- 你的类更干净,与你的引擎很好地融合在一起。只公开你感兴趣的功能
- 你不会受到Box2D引擎的约束;如果你想切换到Nape等其他引擎,你只需要修改自定义类;你的引擎和游戏的其余部分都不知情。其他开发人员也不需要学习Box2D引擎才能使用它
- 当你在那里时,你甚至可以实现多个引擎,并使用切换或接口在它们之间进行切换。同样,你的引擎和游戏的其余部分都不知情
- 与基于组件的引擎很好地配合使用 - 例如,你可以有一个包含
b2Body
属性的Box2D组件
缺点:
- 与仅扩展类不同,你实际上是在你的引擎和Box2D之间创建了一个中间层。理想情况下,在你的自定义类之外,不应该有对Box2D的引用。工作量取决于你在类中需要什么。
- 额外的间接层;通常不应该成为问题,因为Box2D将直接使用你的Box2D属性,但如果你的引擎经常调用你的函数,这是一种额外的性能损失。
在这三种方法中,我更喜欢采用组合方式,因为它提供了最大的灵活性并保持了你的引擎模块化的本质,即你有你的核心引擎类,并且你可以使用外部库来扩展功能。你还可以轻松切换库,这也是一个巨大的优势。这也是我在我的引擎中采用的技术,并将其扩展到其他类型的库中——例如广告。我有我的引擎Ad类,可以根据需要与Mochi、Kongregate等集成——我的游戏不关心我正在使用什么,这让我在整个引擎中保持了我的编码风格和一致性,同时仍然具有灵活性和可扩展性。
----- 更新于20/9/2013 -----
大型更新时间!所以我回去做了一些关于大小和速度的测试。我使用的类太大了,不能在这里粘贴,所以你可以在http://divillysausages.com/files/TestExtendClass.as下载它。
在其中,我测试了一些类:
- 一个
Empty
实例;一个只是继承了Object
并实现了一个空的getPostion()
函数的类。这将是我们的基准。
- 一个
b2Body
实例
- 一个
Box2DExtends
实例;一个继承了b2Body
并实现了一个仅返回GetPosition()
(b2Body
函数)的getPosition()
函数的类
- 一个
Box2DExtendsOverrides
实例;一个继承了b2Body
并重写了GetPosition()
函数(它只是返回super.GetPosition()
)的类
- 一个
Box2DComposition
实例;一个具有b2Body
属性和一个返回b2Body's
GetPosition()
的getPosition()
函数的类
- 一个
Box2DExtendsProperty
实例;一个继承了b2Body
并添加了一个新的Point
属性的类
- 一个
Box2DCompositionProperty
实例;一个既有b2Body
属性又有Point
属性的类
所有测试都在独立播放器中进行,在Windows 7上的FP v11.7.700.224版本,使用一台不太好的笔记本电脑。
测试1:大小
AS3有点烦人,如果你调用getSize()
,它会给你对象本身的大小,但任何内部属性也是Object
,只会导致4字节
的增加,因为它们只计算指针。我可以理解为什么它们这样做,只是让得到正确的大小有些棘手。
因此,我转向了flash.sampler
包。如果我们对创建对象进行采样,并将NewObjectSample
对象中的所有大小相加,我们将得到对象的完整大小(注意:如果您想看到被创建的内容和大小,请在测试文件中注释掉log
调用)。
- Empty的大小为56 // 扩展自Object
- b2Body的大小为568
- Box2DExtends的大小为568 // 扩展自b2Body
- Box2DExtendsOverrides的大小为568 // 扩展自b2Body
- Box2DComposition的大小为588 // 具有b2Body属性
- Box2DExtendsProperty的大小为604 // 扩展自b2Body并添加Point属性
- Box2DCompositionProperty的大小为624 // 具有b2Body和Point属性
这些大小都是以字节为单位的。以下是一些值得注意的点:
- 基本Object大小为40字节,因此仅类而没有其他内容为16字节。
- 添加方法不会增加对象的大小(它们基于类实现),而属性显然会增加大小。
- 仅扩展类并未添加任何内容。
- Box2DComposition的额外20字节来自于16个类和4个指向b2Body属性的指针。
- 对于Box2DExtendsProperty等,您有16个Point类本身,4个指向Point属性的指针,以及每个x和y属性Number的8个字节= 36个字节之间的差异Box2DExtends。
因此,显然大小的差异取决于您添加的属性,但总的来说,相当微不足道。
测试2:创建速度
对于这个,我只是使用了getTimer()和一个循环10000次的循环,它本身循环了10次(因此为100k)以获得平均值。在每组之间调用System.gc()以最小化垃圾收集时间。
- 创建Empty的时间为3.9毫秒(平均值)
- 创建b2Body的时间为65.5毫秒(平均值)
- 创建Box2DExtends的时间为69.9毫秒(平均值)
- 创建Box2DExtendsOverrides的时间为68.8毫秒(平均值)
- 创建Box2DComposition的时间为72.6毫秒(平均值)
- 创建Box2DExtendsProperty的时间为76.5毫秒(平均值)
- 创建Box2DCompositionProperty的时间为77.2毫秒(平均值)
这里没有太多要注意的。扩展/组成类需要稍微更长的时间,但像0.000007ms这样的时间(这是100,000个对象的创建时间)并不值得考虑。
测试3:调用速度
为此,我再次使用了
getTimer()
,循环执行
1000000
次,每次循环执行
10
次(即10m)以获得平均值。在每个集合之间调用
System.gc()
以最小化由于垃圾收集而产生的时间。所有对象都调用了它们的
getPosition()/GetPosition()
函数,以查看重载和重定向之间的差异。
- Empty的
getPosition()
时间为83.4ms(平均值)//空
- b2Body的
GetPosition()
时间为88.3ms(平均值)//正常
- Box2DExtends的
getPosition()
时间为158.7ms(平均值)//getPosition()
调用GetPosition()
- Box2DExtendsOverrides的
GetPosition()
时间为161ms(平均值)//重载调用super.GetPosition()
- Box2DComposition的
getPosition()
时间为160.3ms(平均值)//调用this.body.GetPosition()
- Box2DExtendsProperty的
GetPosition()
时间为89ms(平均值)//隐式super(即未被重载)
- Box2DCompositionProperty的
getPosition()
时间为155.2ms(平均值)//调用this.body.GetPosition()
这个结果有点让我惊讶,因为时间差异大约是两倍(尽管每次调用仍然只有0.000007ms
)。延迟似乎完全取决于类继承 - 例如,Box2DExtendsOverrides
只是调用了super.GetPosition()
,但它的速度却是Box2DExtendsProperty
的两倍,后者从其基类中继承了GetPosition()
。
我想这与函数查找和调用的开销有关,虽然我使用FlexSDK中的swfdump
查看了生成的字节码,并且它们是相同的,所以要么它在欺骗我(或没有包含它),要么有些东西我还不知道 :)虽然步骤可能相同,但它们之间的时间可能不同(例如,在内存中,它会跳转到您的类vtable,然后跳转到基类vtable等)
var v:b2Vec2 = b2Body.GetPosition()
的字节码非常简单:
getlocal 4
callproperty :GetPosition (0)
coerce Box2D.Common.Math:b2Vec2
setlocal3
当var v:b2Vec2 = Box2DExtends.getPosition()
(getPosition()
返回GetPosition()
)时:
getlocal 5
callproperty :getPosition (0)
coerce Box2D.Common.Math:b2Vec2
setlocal3
对于第二个例子,它没有显示对
GetPosition()
的调用,因此我不确定他们是如何解决这个问题的。如果有人想尝试解释一下,测试文件可供下载。
需要注意以下几点:
GetPosition()
实际上并没有做任何事情;它本质上是一个伪装成函数的getter,这也是为什么“额外类步骤惩罚”看起来如此之大的原因之一
- 这是在10m的循环中进行的,你不太可能在游戏中这样做。每次调用的惩罚并不值得担心
- 即使你真的担心这个惩罚,也要记住这是你的代码和Box2D之间的接口;Box2D内部将不受此影响,只有对你的接口的调用会受到影响
总之,我期望从扩展我的一个类中获得相同的结果,所以我不会真的担心它。实现最适合你解决方案的架构即可。