枚举、Flags和位运算符

如果你是一个游戏开发者,你可能很熟悉描述一个特性的不同变化的需要。无论它是哪种攻击类型(近战、冰、火、药丸。。。),或是敌人的状态(空闲、警戒、追逐、攻击、休息。。。),你都无法避免。实现这一点最简单的方法就是使用常量:

public static int NONE = 0;
public static int MELEE = 1;
public static int FIRE = 2;
public static int ICE = 3;
public static int POISON = 4;

public int attackType = NONE;

缺点就是你无法实际控制分配给attackType的值。它可能是整型值,你也可以做点危险的事情,比如attackType++。

枚举构造

幸运的是,C#有个构造叫做enum(结构体),它专门为以下情况进行设计的:

 1 // Outside your class
 2 public enum AttackType {
 3     None,
 4     Melee,
 5     Fire,
 6     Ice,
 7     Poison
 8 }
 9 
10 // Inside your class
11 public AttackType attackType = AttackType.None;

enum的定义创建了一种仅支持限定范围或限定值的类型。为清晰起见,这些值被赋予符号标签,并且在需要的时候做为sting进行返回:

1 attackType = AttackType.Poison;
2 Debug.Log("Attack: " + attackType); # Prints "Attack: Poison"

在内部,每个标签都有一个整型值。枚举从0开始,每个标签被分配给下一个整数值。None是0,Melee是1,Fire是2,以此类推。你可以显式地更改标签的值。

1 public enum AttackType {
2     None,     // 0
3     Melee,    // 1
4     Fire,     // 2
5     Ice = 4,  // 4
6     Poison    // 5
7 }

将enum类型转换为int类型会返回它的整型值。公平来说,枚举实际上就是整数。

枚举之所以如此有趣,是因为它们自动集成在Unity检查器中。如果公共字段是一个枚举,它将方便地显示为一个下拉菜单:

Enums and Flags

广大开发人员使用枚举就像我们以前使用的那样。我们可以用它们做更多的事情。第一个限制就是标准的枚举只能一次赋予一个值。如果我们用的是带火攻的近战武器(火剑)呢?!)?为了解决这个问题,可以用[Flags]来修饰枚举。这允许它们被视为位掩码,在它们之间存储多个值:

1 [Flags] public enum AttackType {
2     None   = 0,
3     Melee  = 1,
4     Fire   = 2,
5     Ice    = 4,
6     Poison = 8
7 }
8 // ...
9 public AttackType attackType = AttackType.Melee | AttackType.Fire;

在上面的示例中,attackType同时拥有Melee和Fire两个值。我们将在稍后会看到如何检索这些值。但是,首先我们需要弄懂它们实际是如何工作的。枚举做为整型进行存储;当你有一组连续的数字,它们的位表示如下:

1 // Consecutive
2 public enum AttackType {
3     // Decimal     // Binary
4     None   = 0,    // 000000
5     Melee  = 1,    // 000001
6     Fire   = 2,    // 000010
7     Ice    = 3,    // 000011
8     Poison = 4     // 000100
9 }

如果我们想使用[Flags],最好的情况是,我们应该只使用2的幂次方做为标签的值。正如你在下面看到的,这意味着每个非零标签都有一个1在它的二进制表示中,它们都处于不同的位置:

1 // Powers of two
2 [Flags] public enum AttackType {
3     // Decimal     // Binary
4     None   = 0,    // 000000
5     Melee  = 1,    // 000001
6     Fire   = 2,    // 000010
7     Ice    = 4,    // 000100
8     Poison = 8     // 001000
9 }

上文中,变量attackType被看作一系列的比特位,每一位表示它是否具有某种属性。如果第一位是1,那么它是melee混战;如果第二位是1,它是fire火攻;如果第三位是1,它是ice冰攻,等等。为了能使用这个进行工作,重点要注意的是,标签必须手动初始化为2的幂次方。我们将在文章的后面看到如何更优雅地做到这一点。最后,因为枚举一般会被存储为Int32类型,所以说使用超过32个不同标签的枚举是不明智的。

公平来说,即使没有[Flags]也可以使用枚举。它唯一能做的就是当打印的时候可以有更好的输出。

Bitwise operators位操作符

位掩码本质上是一个整数值,其中多个二进制属性(yes/no)独立存储在对应的位中。为了装箱和拆箱我们需要一些特殊的操作符。C#将它们叫做bitwise operator位操作符,因为它们是逐位进行工作的,忽略了不太可能的加法和减法运算符。

Bitwise OR

使用位操作符OR设置属性是可行的:

attackType = AttackType.Melee | AttackType.Fire;
// OR
attackType = AttackType.Melee;
attackType |= AttackType.Fire;

位操作符OR所做的就是如果任一操作数的第i位置是1则将第i位置设计为1.

1 // Label          Binary   Decimal
2 // Melee:         000001 = 1
3 // Fire:          000010 = 2
4 // Melee | Fire:  000011 = 3

如果你打印attackType,你将得到一个自定义结果:Melee | Fire。不幸地是,检查器没有触发;Unity不会认定新的类型,仅仅是将字段设置为空。

如果你希望混合类型出现,你需要手动地在enum中定义它们,如下:

 1 [Flags] public enum AttackType {
 2     // Decimal                  // Binary
 3     None         = 0,           // 000000
 4     Melee        = 1,           // 000001
 5     Fire         = 2,           // 000010
 6     Ice          = 4,           // 000100
 7     Poison       = 8,           // 001000
 8 
 9     MeleeAndFire = Melee | Fire // 000011
10 }

Bitwise AND

 OR位运算符的互补运算符是AND。它以完全相同的方式工作,当应用于两个整数时,它只保留两个整数都共有的位。位运算会OR用于设置位,而位AND通常用于拆分先前存储在整数中的属性。

1 attackType = AttackType.Melee | AttackType.Fire;
2 bool isIce = (attackType & AttackType.Ice) != 0;

在attackType和AttackType.Ice之间应用AND操作符时,会将所有位设置为0,除了与AttackType.Ice自身有关的位。它们的最终值是由attackValue决定的。如果attackvalue里面有一个ice,则结果肯定会包含AttackType.Ice;否则就是0:

1 // Label                Binary   Decimal
2 // Ice:                 000100 = 4
3 // MeleeAndFire:        000011 = 3
4 // MeleeAndFire & Ice:  000000 = 0
5 
6 // Fire:                000010 = 2
7 // MeleeAndFire:        000011 = 3
8 // MeleeAndFire & Fire: 000010 = 2

如果按位运算符让你晕头转向。NET 4.0引入了HasFlag函数,其可以如下方便地使用:

attackType.HasFlag(AttackType.Ice);

现在,你需要注意的一个事实是None一直代表着值为0。结果就是,我们的原始方法无法检测到None:

(attackType & AttackType.None) != 0

 一直返回 false,因为attackType & 0 一直为0。避免发生的可能方式是检测原始值,如下:

1 bool isNone = (attackType & AttackType.None) == AttackType.None;

当使用None时,这种行为可能是也可能不是你想要的。请注意,HasFlag的标准.NET实现是使用的我们最新的示例。如果你不想发疯,你也可以定义None为1。请记住,在枚举中最好总是有一个零值。

Bitwise NOT

 还有一个有用的位运算符,它就是NOT运算符。它所做的只是将整数的位进行反转。这个很有用,比如,不设置位。假设我们希望我们的攻击不再是“火”,而是变成了“冰”:

1 attackType = AttackType.Melee | AttackType.Fire
2 
3 attackType &= ~ AttackType.Fire;   //~AttackType.Fire 二进制表达为:111101    attackType &= ~ AttackType.Fire; 二进制表达为:000001(即仅有Melee没有变化,取消了Fire;)
4 attackType |= AttackType.Ice;      //此时的attackType值为Melee

通过否定AttackType.Fire的属性,除了在与fire属性相关的位置有个0外,剩下一个全为1的位掩码。当将AND用于attackType时,所有其它的位都没有变,只是取消了fire的属性。

Bitwise XOR

在OR、AND和NOT之后,我们不得不提一下XOR。顾名思义,它用于对整数变量中相同位置的位进行异或运算。仅当两个二进制值中的一个为真时,而不是两个同时为真,则两个二进制的异或才为真。这个对于位掩码来说具体非常重要的意义,因为它允许切换一个值。

xyx ^ y
0 0 0
0 1 1
1 0 1
1 1 0
1 attackType = AttackType.Melee | AttackType.Fire;
2 
3 attackType ^= AttackType.Fire; // Toggle fire
4 attackType ^= AttackType.Ice;  // Toggle ice

Bitwise shifts

使用位掩码的最后两个运算符是移位运算符。取一个数,他们会将它的位向右(>>)或向左(<<)移动。如果你有一个十进制数字,比如说“1”,你向左移到一个位置,就变成了“10”,再移一位,就得到了“100”。如果以十为基数移动一个位置相当于乘以(或除以)十,以二为基数移动一个位置相当于乘以(或除以)二。这就是为什么按位移动可以用于创建2的幂次方。

1 [Flags] public enum AttackType {
2     //               // Binary  // Dec
3     None   = 0,      // 000000  0
4     Melee  = 1 << 0, // 000001  1
5     Fire   = 1 << 1, // 000010  2
6     Ice    = 1 << 2, // 000100  4
7     Poison = 1 << 3, // 001000  8
8 }

再比如,将42进行移位操作:

42 = 101010 (In Binary)

Bitwise Left Shift procedure on 42:

42 << 1 = 84 (In binary 1010100)
42 << 2 = 168 (In binary 10101000)
42 << 4 = 672 (In binary 1010100000)

同样是操作42,看一下右移操作:

42 >> 1 = 21 (In binary 010101)
42 >> 2 = 10 (In binary 001010)
42 >> 4 = 2 (In binary 000010)

 

posted @ 2022-09-24 12:50  chenlight  阅读(434)  评论(0编辑  收藏  举报