游戏引擎Collison Bitmask …为什么0x01等?

在Sprite Kit(iOS开发版)和Cocos2d-x(我知道它几乎是Sprite Kit的灵感,因此他们使用了很多相同的工具)的情况下,我终于决定找出这个原因发生:

当使用物理引擎时,我创build一个精灵,并添加一个physicsBody到它。 大多数情况下,我知道如何设置类别,碰撞和联系位掩码以及它们的工作方式。 问题是实际的位掩码数字:

SpriteKit:

static const uint32_t missileCategory = 0x1 << 0; sprite.physicsBody.categoryBitMask = missileCategory; 

的Cocos2D-X:

 sprite->getPhysicsBody()->setCategoryBitmask(0x01); // 0001 

我完全困惑,为什么我会写0x01或0x1 << 0在任何情况下。 我有点得到,他们使用hex,这与32位整数有关。 而据我已经能够谷歌,0x01是二进制0001是十进制1。 0x02是二进制的0010,十进制是2。 好的,所以有这些转换,但为什么在这个世界上,我会用它们来做类似的事情?

就我的逻辑而言,如果我说出一个玩家类别,一个敌人类别,一个导弹类别和一个墙类别,这只是4个类别。 为什么不使用string的类别? 或者甚至只是二进制数字,任何非CS的人会理解像0,1,2和3?

最后,我很困惑为什么有32个不同的类别可用? 我以为一个32位的整数有0到几十亿的数字(当然是无符号的)。 那为什么我没有几十个不同的可能类别呢?

有没有我不理解的某种优化? 或者这只是他们使用的一个老惯例,但是不需要? 还是有什么事情发生,只有两个学期的大学课程CS培训的人不明白?

掩码的原因是它使您/程序能够轻松快速地计算两个对象之间发生碰撞或不发生碰撞。 因此: 是的,这是一些优化。

假设我们有三个类别

  • 导弹0x1 << 0
  • 播放器0x1 << 1
  • 墙壁0x1 << 2

现在我们有一个Player实例,它的类别被设置为player 。 其碰撞位掩码设置为missile | player | wall missile | player | wall 因为我们希望能够与所有三种types相撞:其他玩家,水平墙和子弹/导弹飞行。

现在我们有一个Missile类别设置为missile和碰撞位掩码设置为player | wall player | wall :它不会与其他导弹相撞,但会撞击玩家和墙壁。

如果我们现在想评估两个物体是否会相互碰撞,我们采用第一个类别的位掩码和第二个想要的碰撞位掩码,

上面描述的设置在代码中看起来如下所示:

 let player : UInt8 = 0b1 << 0 // 00000001 = 1 let missile : UInt8 = 0b1 << 1 // 00000010 = 2 let wall : UInt8 = 0b1 << 2 // 00000100 = 4 let playerCollision = player | missile | wall // 00000111 = 7 let missileCollision = player | wall // 00000101 = 5 

其后的推理基本上是:

 if player & missileCollision != 0 { print("potential collision between player and missile") // prints } if missile & missileCollision != 0 { print("potential collision between two missiles") // does not print } 

我们在这里使用一些算术运算,每一位代表一个类别。 你可以简单地枚举掩码1,2,3,4,5 …但是你不能对它们进行任何math运算。 因为你不知道一个5类别的位掩码是否真的是一个类别5,或者它是类别1和4的对象。

然而,仅使用比特,我们可以做到这一点:7的2的幂的唯一表示是4 + 2 + 1:因此无论对象具有碰撞位掩码7与类别4,2和1相冲突。 5只是第一类和第四类的组合 – 没有别的办法。

现在因为我们没有枚举 – 每个类别使用一个位,而正则整数只有32(或64)位,我们只能有32(或64)个类别。

看看下面的代码和更广泛的代码,这些代码演示了如何在更通用的术语中使用蒙版:

 let playerCategory : UInt8 = 0b1 << 0 let missileCategory : UInt8 = 0b1 << 1 let wallCategory : UInt8 = 0b1 << 2 struct EntityStruct { var categoryBitmask : UInt8 var collisionBitmask : UInt8 } let player = EntityStruct(categoryBitmask: playerCategory, collisionBitmask: playerCategory | missileCategory | wallCategory) let missileOne = EntityStruct(categoryBitmask: missileCategory, collisionBitmask: playerCategory | wallCategory) let missileTwo = EntityStruct(categoryBitmask: missileCategory, collisionBitmask: playerCategory | wallCategory) let wall = EntityStruct(categoryBitmask: wallCategory, collisionBitmask: playerCategory | missileCategory | wallCategory) func canTwoObjectsCollide(first:EntityStruct, _ second:EntityStruct) -> Bool { if first.categoryBitmask & second.collisionBitmask != 0 { return true } return false } canTwoObjectsCollide(player, missileOne) // true canTwoObjectsCollide(player, wall) // true canTwoObjectsCollide(wall, missileOne) // true canTwoObjectsCollide(missileTwo, missileOne) // false 

这里的重要部分是方法canTwoObjectsCollide不关心对象的types或者有多less类别。 只要你坚持使用掩码,你需要确定是否两个对象理论上可以相互碰撞(忽略他们的位置,这是另一个任务)。

luk2302的回答非常好,但只是走得更远,在其他方向上…

为什么hex符号? ( 0x1 << 2等)

一旦你知道位位置是重要的部分,那么(正如评论中所提到的)只是风格/可读性的问题。 你也可以这样做:

 let catA = 0b0001 let catB = 0b0010 let catC = 0b0100 

但是像这样的二进制文字(就苹果工具而言)是Swift新的,在ObjC中是不可用的。

你也可以这样做:

 static const uint32_t catA = 1 << 0; static const uint32_t catB = 1 << 1; static const uint32_t catC = 1 << 2; 

要么:

 static const uint32_t catA = 1; static const uint32_t catB = 2; static const uint32_t catC = 4; 

但是,由于历史/文化原因,程序员之间使用hex符号作为提醒自己/其他读者的代码的一种方式是,一个特定的整数字面量对于其位模式比绝对值更重要。 (另外,对于第二个C示例,您必须记住哪个位具有哪个位置值,而使用<<运算符或二进制文字可以强调该位置。)

为什么要模式? 为什么不 ___?

使用位模式/位掩码是性能优化。 为了检查碰撞,物理引擎必须检查世界上的每一个对象。 因为它是成对的,所以性能成本是二次的:如果你有4个对象,你有4 * 4 = 16个可能的冲突来检查… 5个对象是5 * 5 = 25个可能的条件,等等。一些明显的排除(不必担心物体碰撞自身,A碰撞B和B碰撞A等等),但是增长仍然二次方成正比 ; 也就是说,对于n个对象,你有O(n 2 )个可能的冲突来检查。 (请记住,我们正在计算场景中的所有对象,而不是类别。)

许多有趣的物理游戏在场景中总共有5个以上的对象,并且以每秒30或60帧(或者至less想要)的速度运行。 这意味着物理引擎必须在16毫秒内检查所有可能的碰撞对。 或者说,最好不到16ms,因为在发现冲突之前/之后,还有其他物理方面的工作要做,而且游戏引擎需要时间进行渲染,而且你也许还需要时间为你的游戏逻辑做好准备。

位掩码比较非常快。 就像面具比较:

 if (bodyA.categoryBitMask & bodyB.collisionBitMask != 0) 

…是ALU要做的最快的事情之一 – 就像一两个时钟周期一样快。 (任何人都知道在每个指令数字的哪个位置跟踪实际周期?)

相比之下,string比较本身就是一种algorithm,需要更多的时间。 (更不用提一些简单的方法来让这些stringexpression应该导致冲突的类别组合 。)

一个挑战

由于位掩码是性能优化,因此它们也可能是(私有)实现细节。 但是大多数物理引擎,包括SpriteKit,都把它们作为API的一部分。 有一种更高级的方式来expression“这些是我的分类,这些是他们应该如何互动”,让其他人处理把这个描述翻译成位掩码的细节。 苹果公司的DemoBots示例代码项目似乎有一个想法来简化这样的事情(请参阅源代码中的ColliderType )…随意使用它devise自己的。

回答你的具体问题

“为什么有32个不同的类别可用?我认为一个32位的整数有0到几十亿的数字(当然没有签名),那么为什么我没有几十个不同的可能的类别?

答案是这个类别总是被当作只有一个位应该被设置的一个32位的位掩码。 所以这些是有效的价值:

 00000000000000000000000000000001 = 1 = 1 << 0 00000000000000000000000000000010 = 2 = 1 << 1 00000000000000000000000000000100 = 4 = 1 << 2 00000000000000000000000000001000 = 8 = 1 << 3 00000000000000000000000000010000 = 16 = 1 << 4 00000000000000000000000000100000 = 32 = 1 << 5 00000000000000000000000001000000 = 64 = 1 << 6 00000000000000000000000010000000 = 128 = 1 << 7 00000000000000000000000100000000 = 256 = 1 << 8 00000000000000000000001000000000 = 512 = 1 << 9 00000000000000000000010000000000 = 1024 = 1 << 10 00000000000000000000100000000000 = 2048 = 1 << 11 . . . 10000000000000000000000000000000 = 2,147,483,648 = 1 << 31 

所以有32个不同的类别可用。 然而,你的categoryBitMask可以有多个位集,所以实际上可以是从1到任何最大的UInt32。 例如,在街机游戏中,您可能有类别,如:

 00000000000000000000000000000001 = 1 = 1 << 0 //Human 00000000000000000000000000000010 = 2 = 1 << 1 //Alien 00000000000000000000000000000100 = 4 = 1 << 2 //Soldier 00000000000000000000000000001000 = 8 = 1 << 3 //Officer 00000000000000000000000000010000 = 16 = 1 << 4 //Bullet 00000000000000000000000000100000 = 32 = 1 << 5 //laser 00000000000000000000000001000000 = 64 = 1 << 6 //powershot 

所以一个人类的平民可能有1个类别的位面怪物,一个人类士兵5(1 + 4),一个外星人军官6,一个普通子弹16,一个导弹80(16 + 64),巨型死亡射线96等等。