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

时间:2016-01-20 15:52:41

标签: ios sprite-kit cocos2d-x bitmask

在Sprite Kit(iOS开发)和Cocos2d-x(我知道它几乎是Sprite Kit的灵感,因此他们为什么使用大量相同的工具)中遇到这种情况,我终于决定想为什么会发生这种情况:

使用物理引擎时,我会创建一个精灵,并为其添加一个physicsBody。在大多数情况下,我了解如何设置类别,碰撞和联系位掩码,以及它们如何工作。问题是实际的位掩码数:

SpriteKit:

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

的Cocos2D-X:

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

我完全不知道为什么要写0x01或0x1&lt;&lt;两种情况都为0。我有点认为他们使用的是十六进制,它与32位整数有关。至于我已经能够谷歌,0x01是二进制的0001,十进制是1。 0x02是二进制的0010,十进制为2。好的,所以有这些转换,但为什么在世界上我会将它们用于像类别一样简单的东西?

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

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

我不理解某种优化吗?或者这只是他们使用的旧约定,但不是必需的?或者有什么事情发生在一个只有2个学期的CS大学课程CS培训的人不会理解?

3 个答案:

答案 0 :(得分:9)

位掩码的原因在于它使您/程序能够轻松快速地计算两个对象之间发生或不发生碰撞的情况。因此:这是某种优化。

假设我们有三个类别

  • 导弹0x1 << 0
  • 玩家0x1 << 1
  • wall 0x1 << 2

现在我们有一个Player实例,其类别设置为player。它的碰撞位掩码设置为missile | player | wall+而不是|也可以),因为我们希望能够与所有三种类型碰撞:其他玩家,关卡墙和子弹/导弹飞来飞去。

现在我们有Missile类别设置为missile且碰撞位掩码设置为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 ......但是您无法对它们进行任何数学运算。因为您不知道5类别的位掩码是否真的是类别5,或者它是1类和4类的对象。

然而,只使用位我们可以做到这一点:根据7的2的幂的唯一表示是4 + 2 + 1:因此无论什么对象具有冲突位掩码7与类别4,2和1冲突。与bitmask 5完全相同但只有1类和4类的组合 - 没有别的办法。

既然我们没有枚举 - 每个类别使用一位而常规整数只有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不关心对象的类型或有多少类别。只要您坚持使用位掩码就可以确定是否有两个物体在理论上会发生碰撞(忽略它们的位置,这是另一天的任务)。

答案 1 :(得分:7)

luk2302's answer很棒,但只是为了进一步向其他方向发展......

为什么使用十六进制表示法? (0x1 << 2等)

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

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

但像这样的二进制文字(就Apple工具而言)是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;

但是,由于历史/文化原因,使用十六进制表示法作为一种提醒自己/您的代码的其他读者的一种方式,特定的整数文字对于其位模式而言​​比其绝对值更重要,这已经成为程序员的共同惯例。 (另外,对于第二个C示例,您必须记住哪个位具有哪个位值,而使用<<运算符或二进制文字则可以强调位置。)

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

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

许多有趣的物理游戏在场景中总共有超过5个对象,并以每秒30或60帧(或至少想要)的速度运行。这意味着物理引擎必须在16毫秒内检查所有可能的冲突对。或者优选地,远小于16毫秒,因为它在发现碰撞之前/之后仍然有其他物理因素要做,并且游戏引擎需要时间来渲染,并且您可能想要时间来进行您的游戏那里也有逻辑。

比特掩码比较非常快。像掩码比较的东西:

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

...是你可以要求ALU做的最快的事情之一 - 比如快一个或两个时钟周期。 (任何人都知道在哪里追踪每个指令数据的实际周期?)

相比之下,字符串比较本身就是一种算法,需要更多的时间。 (更不用说让这些字符串表达应该导致冲突的类别的组合的简单方法。)

挑战

由于位掩码是性能优化,因此它们也可能是(私有)实现细节。但是大多数物理引擎,包括SpriteKit,都将它们作为API的一部分。有一种方法可以说“这些是我的类别,这些是他们应该如何互动”的高级别,并让其他人处理将该描述转换为位掩码的细节。 Apple's DemoBots示例代码项目似乎有一个简化此类事情的想法(请参阅源代码中的ColliderType)...随意使用它设计自己的。

答案 2 :(得分:2)

回答您的具体问题

  

&#34;为什么有32种不同的类别?我以为32位   整数有0到数十亿的数字(当然是无符号的)。所以为什么   我没有数十亿种不同的可能类别吗?&#34;

答案是该类别始终被视为32位位掩码,其中应设置 ONLY ONE 位。所以这些是有效值:

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等等