枚举、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:
一直返回 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。顾名思义,它用于对整数变量中相同位置的位进行异或运算。仅当两个二进制值中的一个为真时,而不是两个同时为真,则两个二进制的异或才为真。这个对于位掩码来说具体非常重要的意义,因为它允许切换一个值。
x | y | x ^ 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)