游戏引擎 Collison 位掩码... 为什么是 0x01 等?

Game Engine Collison Bitmask... Why 0x01 etc?

在 Sprite Kit(iOS 开发)和 Cocos2d-x(我知道这几乎是 Sprite Kit 的灵感来源,因此他们使用很多相同的东西)中遇到这种情况工具),我终于决定弄清楚为什么会这样:

当使用物理引擎时,我创建了一个 sprite,并向其添加了一个 physicsBody。大多数情况下,我了解如何设置类别、碰撞和接触位掩码,以及它们的工作原理。问题是实际的位掩码数:

SpriteKit:

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

Cocos2D-X:

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

我完全不明白为什么在任何一种情况下我都会写 0x01 或 0x1 << 0。我有点明白他们使用的是十六进制,它与 32 位整数有关。据我所知 google,0x01 在二进制中是 0001,在十进制中是 1。而 0x02 在二进制中是 0010,在十进制中是 2。好的,所以有这些转换,但为什么我要把它们用于简单的东西,比如类别?

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

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

是否有某种我不理解的优化?或者这只是他们使用的旧约定,但不需要?或者有什么事情是只有 2 个学期的大学课程 CS 培训的人无法理解的?

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

假设我们有这三个类别

  • 导弹0x1 << 0
  • 玩家0x1 << 1
  • 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 发生碰撞。位掩码为 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 不关心对象的类型或有多少类别。只要您坚持使用位掩码,就可以确定两个对象在理论上是否可以碰撞(忽略它们的位置,这是另一天的任务)。

很棒,但只是要走得更远,在其他方向...

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

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

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;

但是,出于 historical/cultural 的原因,使用十六进制表示法作为一种提醒代码的 oneself/other 读者的方式已成为程序员的普遍约定,即特定整数文字的位更重要模式比它的绝对值。 (此外,对于第二个 C 示例,您必须记住哪个位具有哪个位值,而使用 << 运算符或二进制文字,您可以强调该位置。)

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

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

许多有趣的物理游戏在场景中总共有超过 5 个对象,并且 运行 每秒 30 或 60 帧(或至少想要)。这意味着物理引擎必须在 16 毫秒内检查所有这些可能的碰撞对。或者最好是,远少于 16 毫秒,因为它还有其他 physics-y 事情要做 before/after 查找碰撞,并且游戏引擎需要时间来渲染,您可能需要时间 你的游戏逻辑也在那里。

位掩码比较非常快。像面具比较之类的东西:

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

...是您可以让 ALU 做的最快的事情之一——比如快一两个时钟周期。 (有人知道在哪里可以找到每条指令的实际周期数吗?)

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

挑战

由于位掩码是一种性能优化,因此它们也可能是(私有)实现细节。但是大多数物理引擎,包括 SpriteKit 的,都将它们保留为 API 的一部分。最好有一种在高层次上表达 "these are my categories, these are how they should interact" 的方式,并让其他人处理将该描述转换为位掩码的细节。 Apple's DemoBots 示例代码项目似乎有一个简化此类事情的想法(请参阅源代码中的 ColliderType)...请随意使用它设计您自己的。

回答你的具体问题

"why there are 32 different categories available? I thought a 32-bit integer had numbers 0-some billion number (unsigned of course). So why do I not have billions of different possible categories?"

答案是该类别始终被视为一个 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

所以人类平民可能有 categoryBitMask 为 1,人类士兵 5 (1 + 4),外星军官 6,普通子弹 16,导弹 80 (16 + 64),巨型死亡射线 96等等等等