8

In my game engine I use Box2D for physics. Box2D's naming conventions and poor commenting ruin the consistent and well documented remainder of my engine which is a little frustrating and presents poorly when you're using it.

I've considered making a set of wrapper classes for Box2D. That is, classes which extend each of the common Box2D objects and have their functions rewritten to follow the naming conventions of the rest of my engine, and to have them more clearly and consistently commented. I have even considered building ontop of some of the classes and adding some bits and pieces (like getters for pixel-based measurements in the b2Vec2 class).

This is fine but I am not 100% sure what the negative impacts of this would be and the degree to which those would affect my applications and games. I'm not sure if the compiler alleviates some of my concerns to a degree or whether I do need to be considerate when adding somewhat unnecessary classes for the sake of readability and consistency.

I have some suspicions:

  • More memory consumption to accommodate the extra level of class structure.
  • Performance impact when creating new objects due to initializing an extra level of members?

I am asking specifically about runtime impacts.

4

5 回答 5

6

在集成第三方库时,这是一个非常常见的问题,尤其是作为端口的库(如 Box2DAS3),它们保持母语言的编码和命名约定,而不是与目标语言完全集成(例如: Box2DAS3 使用getFoo()andsetFoo()而不是.foogetter/setter)。

为了快速回答您的问题,不,制作包装类不会对性能产生重大影响;不会比您在自己项目的类层次结构中看到的更多。当然,如果您对 500 万次迭代的循环进行计时,您可能会看到一两毫秒的差异,但在正常使用中,您不会注意到它。

“更多的内存消耗以适应额外级别的类结构。” 与任何具有类继承的语言一样,vtable 将在幕后使用,因此您的内存/性能会略有增加,但可以忽略不计。

“由于初始化额外级别的成员而创建新对象时的性能影响?” 不超过正常的实例化,所以除非您创建大量对象,否则无需担心。

性能方面,您通常应该没有问题(除非您确实遇到问题,否则优先考虑可读性和可用性而不是性能),但我会将其更多地视为架构问题,考虑到这一点,我会考虑扩展/修改外部库的类的负面影响通常分为 3 个方面,具体取决于您想要做什么:

  • 修改库
  • 扩展类
  • 与您自己的课程组合

修改库

由于 Box2DAS3 是开源的,因此没有什么能阻止您跳入其中并将所有类/函数名称重构为您心中的内容。我有时认真考虑过这样做。

优点:

  • 你可以修改你想要的 - 函数,类,你命名它
  • 您可以添加所需的任何缺失部分(例如像素米转换助手)
  • 您可以解决任何潜在的性能问题(我注意到如果以“AS3”方式完成一些事情可以做得更好更快)

缺点:

  • 如果您打算使您的版本保持最新,则需要手动合并/转换任何更新和更改。对于流行的图书馆,或者那些变化很大的图书馆,这可能是一个巨大的痛苦
  • 这非常耗时 - 除了修改之外,您还需要对正在发生的事情有一个很好的了解,这样您就可以在不破坏功能的情况下进行任何更改
  • 如果有多个人在使用它,他们就不能过多地依赖外部文档/示例,因为内部可能已经改变

扩展类

在这里,您只需创建自己的包装类,它扩展基本 Box2D 类。您可以根据需要添加属性和函数,包括实现您自己的命名方案,该方案可以转换为基类(例如MyBox2DClass.foo(),可以简单地作为 的包装器Box2DClass.bar()

优点:

  • 您只实现所需的类,并进行必要的更改
  • 您的包装类仍然可以在基本 Box2D 引擎中使用 - 即您可以将MyBox2DClass对象传递给采用 a 的内部方法,Box2DClass并且您知道它会起作用
  • 在所有三种方法中,这是最少的工作量

缺点:

  • 同样,如果您打算使您的版本保持最新,您需要检查任何更改都不会破坏您的课程。不过一般问题不大
  • 如果您创建自己的函数来调用它们的 Box2D 等效函数(例如“我应该使用setPos()还是SetPosition()?),可能会给课堂带来混乱。即使您自己工作,当您在 6 个月后回到课堂时,您会忘记
  • 您的类将缺乏连贯性和一致性(例如,某些函数使用您的命名方法(setPos()),而其他函数使用 Box2D(SetPosition()))
  • 你被 Box2D 困住了;没有大量开发人员就无法更改物理引擎,具体取决于您的类在整个项目中的使用方式。如果您不打算切换,这可能没什么大不了的

与您自己的课程组合

您创建自己的类,这些类在内部拥有一个 Box2D 属性(例如MyPhysicalClass将有一个属性b2Body)。您可以根据需要自由实现自己的接口,并且只实现必要的。

优点:

  • 您的课程更干净,并且非常适合您的引擎。仅公开您感兴趣的功能
  • 你不依赖于 Box2D 引擎;例如,如果你想切换到 Nape,你只需要修改你的自定义类;你的引擎和游戏的其余部分都被遗忘了。其他开发人员也无需学习 Box2D 引擎即可使用它
  • 当您在那里时,您甚至可以实现多个引擎,并使用切换或界面在它们之间切换。同样,您的引擎和游戏的其余部分都没有注意到
  • 与基于组件的引擎很好地配合使用 - 例如,您可以拥有一个 Box2DComponent 来保存一个b2Body属性

缺点:

  • 更多的工作不仅仅是扩展类,因为您实际上是在引擎和 Box2D 之间创建一个中间层。理想情况下,在您的自定义类之外,不应该有对 Box2D 的引用。工作量取决于你在课堂上需要什么
  • 额外的间接级别;通常它不应该是一个问题,因为 Box2D 将直接使用您的 Box2D 属性,但是如果您的引擎经常调用您的函数,那么这是一个额外的步骤,性能明智

在这三者中,我更喜欢组合,因为它提供了最大的灵活性并保持引擎的模块化特性不变,即您拥有核心引擎类,并且您可以使用外部库扩展功能。您可以毫不费力地切换库这一事实也是一个巨大的优势。这是我在自己的引擎中采用的技术,我还将它扩展到其他类型的库 - 例如 Ads - 我有我的引擎 Ad 类,可以根据需要与 Mochi、Kongregate 等集成 -我的游戏的其余部分并不关心我使用的是什么,这让我在整个引擎中保持我的编码风格和一致性,同时仍然是灵活和模块化的。

----- 更新 20/9/2013 -----

大更新时间!所以我回去对尺寸和速度进行了一些测试。我使用的类太大,无法在这里粘贴,因此您可以在http://divillysausages.com/files/TestExtendClass.as下载它

在其中,我测试了许多类:

  • 一个Empty实例;一个仅扩展Object和实现空getPostion()函数的类。这将是我们的基准
  • 一个b2Body实例
  • 一个Box2DExtends实例;一个扩展b2Body并实现一个getPosition()只返回GetPosition()的函数的类(b2Body函数)
  • 一个Box2DExtendsOverrides实例;一个扩展b2Body和覆盖GetPosition()函数的类(它只是返回super.GetPosition()
  • 一个Box2DComposition实例;一个类,它有一个b2Body属性和一个getPosition()返回b2Body's GetPosition()
  • 一个Box2DExtendsProperty实例;扩展b2Body并添加新Point属性的类
  • 一个Box2DCompositionProperty实例;同时具有b2Body属性和Point属性的类

所有测试都是在独立播放器 FP v11.7.700.224、Windows 7 中在一台不太出色的笔记本电脑上完成的。

测试1:尺寸

AS3 有点烦人,如果你调用getSize(),它会给你对象本身的大小,但是任何内部属性也Objects只会导致4 byte增加,因为它们只计算指针。我明白他们为什么这样做,只是让获得合适的尺寸有点尴尬。

于是我转向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字节。
  • 添加方法不会增加对象的大小(无论如何它们都是在类的基础上实现的),而属性显然会
  • 只是扩展类并没有添加任何东西
  • 额外的20字节Box2DComposition来自16类和4指向b2Body属性的指针
  • 对于Box2DExtendsProperty等,你有16Point本身,4指向Point属性的指针,以及8每个xy属性Numbers=36字节之间的差异Box2DExtends

因此,显然大小的差异取决于您添加的属性,但总而言之,可以忽略不计。

测试二:创作速度

为此,我简单地使用getTimer()了 ,带有 的循环10000,它本身循环10(所以 100k)次来获得平均值。System.gc()在每组之间调用以最小化由于垃圾收集而导致的时间。

  • Empty 的创建时间是 3.9ms (av.)
  • b2Body 的创建时间是 65.5ms (av.)
  • Box2DExtends 的创建时间为 69.9 毫秒(平均)
  • Box2DExtendsOverrides 的创建时间为 68.8 毫秒(平均)
  • Box2DComposition 的创建时间是 72.6ms (av.)
  • Box2DExtendsProperty 的创建时间是 76.5ms (av.)
  • Box2DCompositionProperty 的创建时间为 77.2ms (av.)

这里没有一大堆需要注意的地方。扩展/组合类需要稍长一些,但它就像0.000007ms(这是 100,000 个对象的创建时间),所以它真的不值得考虑。

测试 3:通话速度

为此,我getTimer()再次使用1000000, 本身循环10(所以 10m)次来获得平均值。System.gc()在每组之间调用以最小化由于垃圾收集而导致的时间。所有对象都getPosition()/GetPosition()调用了它们的函数,以查看覆盖和重定向之间的区别。

  • 空的 getPosition() 时间是 83.4ms (av.) // 空的
  • b2Body 的 GetPosition() 时间为 88.3ms (av.) // 正常
  • Box2DExtends 的 getPosition() 时间为 158.7ms (av.) // getPosition() 调用 GetPosition()
  • Box2DExtendsOverrides 的 GetPosition() 时间为 161ms (av.) // 覆盖调用 super.GetPosition()
  • Box2DComposition 的 getPosition() 时间是 160.3ms (av.) // 调用 this.body.GetPosition()
  • Box2DExtendsProperty 的 GetPosition() 时间为 89 毫秒(平均)// 隐式超(即未覆盖)
  • Box2DCompositionProperty 的 getPosition() 时间为 155.2ms (av.) // 调用 this.body.GetPosition()

这个让我有点吃惊,时间差约为 2 倍(尽管仍然是0.000007ms每次通话)。延迟似乎完全取决于类继承——例如Box2DExtendsOverrides简单的调用,但速度是从其基类继承的super.GetPosition()的两倍。Box2DExtendsPropertyGetPosition()

我想这与函数查找和调用的开销有关,尽管我查看了swfdump在 FlexSDK 中使用生成的字节码,它们是相同的,所以要么是在骗我(或者不包括它),或者我缺少一些东西:)虽然步骤可能相同,但它们之间的时间可能不同(例如,在内存中,它跳转到您的类 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()并没有真正做任何事情;它本质上是一个伪装成函数的吸气剂,这就是“额外类步惩罚”看起来如此之大的原因之一
  • 这是在 10m 的循环上,您不太可能在游戏中这样做。每次通话的罚款并不值得担心
  • 即使您确实担心惩罚,请记住这是您的代码和 Box2D 之间的接口;Box2D 内部将不受此影响,只有对您的界面的调用

总而言之,我希望扩展我自己的一个类也能得到相同的结果,所以我不会真的担心。实施最适合您的解决方案的架构。

于 2013-09-18T22:40:20.860 回答
1

我知道这个答案没有资格获得赏金,因为我懒得写基准。但是在 Flash 代码库上工作后,我可能会给出一些提示:

avm2 是一种动态语言,所以在这种情况下编译器不会优化任何东西。将调用包装为子类调用将产生成本。但是,该成本将是恒定的时间且很小。

对象创建成本也最多会受到恒定的时间和内存量的影响。此外,与基本成本相比,时间和金额可能微不足道。

但是,就像许多事情一样,魔鬼在细节中。我从未使用过 box2d,但如果它使用任何类型的对象池,则可能无法再正常工作了。一般来说,游戏应该在运行时尝试在没有对象分配的情况下运行。所以要非常小心,不要为了更漂亮而添加分配对象的函数。

function addvectors(a:vec,b:vec,dest:vec):void

可能很难看,但比

function addvectors(a:vec,b:vec):vec

(我希望我的 AS3 语法正确......)。更有用和更丑陋的可能是

function addvectors(a:Vector.<vec>, b:Vector.<vec>, dest:Vector.<vec>, offset:int, count:int):void

所以我的回答是,如果你只是为了可读性而包装,那就去吧。这是一个很小但不变的成本。但是要非常非常小心地改变函数的工作方式。

于 2013-09-15T06:34:00.347 回答
0

我认为扩展不会对性能产生很大影响。是的,有一些成本,但只要你不使用合成,它就不会那么高。即,您无需直接扩展 Box2d 类,而是创建该类的一个实例并在您的类中使用它。例如这个

public class Child extends b2Body {
    public function Child() {
        // do some stuff here
    }
}

而不是这个

public class Child  {
    private var _body:b2Body;
    public function Child() {
        ...
        _body = _world.CreateBody(...);
        ...
    }
}

我猜你知道创建的对象越少越好。只要您保持创建的实例数量,您将获得相同的性能。

从另一个角度来看:a) 添加更多抽象层可能会大大改变 Box2d。如果你在一个团队中工作,这可能是个问题,因为其他开发人员应该了解你的命名 b) 小心中间人代码的味道。通常,当您开始包装已经存在的功能时,您最终会得到只是委托者的类。

于 2013-09-15T07:25:22.690 回答
0

我不知道对实例化时间是否有很大影响,但我会以不同的方式回答您的问题:您还有哪些其他选择?他们似乎会做得更好吗?

Jackson Dunstan 对函数性能做了一个漂亮的基准测试:http: //jacksondunstan.com/articles/1820

把它们加起来:

所以,如果你不想使用继承,也许你必须用静态调用来替换它,这对性能不利。就我个人而言,我将扩展这些类,并为我在运行时需要的所有对象添加一个急切的实例:如果它很大,则制作一个漂亮的加载屏幕......

另外,请查看后字节码优化,例如 apparat:http ://code.google.com/p/apparat/

于 2013-09-15T07:05:57.087 回答
0

这里有一些很好的答案,但我要投入两分钱。

您必须认识到两个不同的概念:扩展类和实现类。

这是一个扩展的例子MovieClip

public class TrickedOutClip extends MovieClip {
   private var rims = 'extra large'
   public function TrickedOutClip() { 
     super();
   }
}

这是一个实现 MovieClip 的示例

public class pimpMyClip {
   private var rims = 'extra large';
   private var pimpedMovieClip:MovieClip;
   public function pimpMyClip() { 
     pimpedMovieClip = new MovieClip();
     pimpedMovieClip.bling = rims;
   }

 public function getPimpedClip() { 
     return pimpedMovieClip;
   }
}

我认为您可能不想扩展这些 box2D 类,而是实现它们。这是一个粗略的大纲:

 public class myBox2DHelper {
   private var box2d = new Box2D(...);

  public function MyBox2DHelper(stage) {

  }

   public function makeBox2DDoSomeTrickyThing(varA:String, varB:Number) { 
       // write your custom code here
     }

   public function makeBox2DDoSomethingElse(varC:MovieClip) { 
       // write your custom code here
     }
  }

祝你好运。

于 2013-09-19T14:23:31.010 回答