一、提一个需求
方块是否可以被某些道具消除。
比如方块A可以被炸弹消除。
方块B可以被火箭消除。
方块C可以被炸弹和火箭消除。
二、不好的实现
直接在类里定义bool变量
cs
public bool isRocketTarget;
public bool isBombTarget;
public bool isLaserTarget;
数据会越来越杂,而且判断非常麻烦。
三、需求的常规实现
假如此时,需要设计一个方块是否可以成为一个道具的目标,
cs
public enum EnumTargetType
{
None = 0, // 不可以成为任何道具的目标
Rocket = 1, // 可以成为火箭的目标
Bomb = 2, // 可以成为炸弹的目标
Laser = 3, // 可以成为激光的目标
}
那么当我们想让一个方块既可以成为火箭的目标,又可以成为炸弹的目标,那么就要在代码中进行判断
cs
// block类
private bool canTargetRocket;
private bool canTargetBomb;
private bool canTargetLaser;
// 构造
public Block(List<EnumTargetType> types)
{
foreach (var type in types)
{
switch (type)
{
case EnumTargetType.Rocket:
canTargetRocket = true;
break;
case EnumTargetType.Bomb:
canTargetBomb = true;
break;
case EnumTargetType.Laser:
canTargetLaser = true;
break;
}
}
}
或者将bool变量变成方法,类中缓存List,实际上是一样的:
cs
public enum TargetType
{
Rocket,
Bomb,
Laser
}
public class BlockType : ScriptableObject
{
public List<TargetType> targetTypes; // 我能成为哪些道具的目标
bool IsRocketTarget(TileType type)
{
return type.targetTypes.Contains(TargetType.Rocket);
}
... 其他的目标判断
}
以上是常规做法。已经可以达到判断是否可以成为某个道具的目标了。
我们发现,每一个判断目标的方法里,都需要遍历一次targetTypes,这里有更优雅的实现方式,让一个判断方法从遍历list的O(n)变成O(1),并且要求用更优雅的书写方式。
四、需求的优雅实现
2.1 直观感受
假设有一个List<bool> list,
list[0]如果是true就代表他是火箭的目标。
list[1]如果是true就代表他是炸弹的目标。
list[2]如果是true就代表他是激光的目标.
...依次类推。
图实例:


那么如果一个方块,既可以被火箭消除,又可以被炸弹消除,就可以用下面的图例表示:

有点意思吧?
组合各种能力就是向对应的位置设置True即可,就像有一排按钮放在你的面前。
然后,
0和1的这种组合让你想到了什么?
那就是二进制。
2.2 二进制与左移运算符
我们观察,第一个能力是能否成为火箭目标,他的第一位是1,其他都是0.
第二个能力是能否成为炸弹目标,他的第二位是1,其他都是0.
那么顺序下去,就是每有一个新的能力,就定义为将1这个数字向左移动1格。

这种操作写做:1<<N
意义为将1向左移动N格。
比如
1<<0的结果是0001,十进制就是1;
1<<1的结果是0010,十进制就是2;
1<<2的结果是0100,十进制就是4
那么我可以定义一个枚举
cs
public enum TargetType
{
None = 0,
Rocket = 1 << 0, // 1 左移 0 位 = 1
Bomb = 1 << 1, // 1 左移 1 位 = 2
Laser = 1 << 2, // 1 左移 2 位 = 4
}
问题:为什么要采用1<<n,而不是任意数字X<<n?
如果是1<<n,就是让1向左移动,
如果是2<<n,那么就是让二进制0010向左移动,
如果是3<<n,那么就是让0011向左移动,
规则上任何整数都可以做左移运算符的左边,但在"能力标志(Flags)"的场景里,我们必须保证每个能力值只有一个 bit 为 1,否则组合和判断会混乱。
对于 位标志(Flags) 来说,我们希望"每个能力只占用一个独立的 bit 位",
1 << n 的结果永远是:只有第 n 位是 1,其他全是 0。
2.3 现在进行组合和判断从属关系(重点)
通过上面,我们已经知道,火箭是0001,炸弹是0010,那么如果是0011就代表既可以成为火箭目标的同时也可以成为炸弹的目标。
因为从右第一位是1代表火箭,第二位是1代表炸弹。
那么如何将0001 组合 0010 => 0011呢?
那就是用或(or) 运算。
1. or运算进行组合 叫做按位或
平时写代码的时候都明白,或运算中,if多个条件,只要有一个是true就返回true;都是false就返回false。
那么对于二进制:
0001和0010进行按位或运算,是将2者的每一位进行or运算。
0001的右边第一位是1,0010右边的第一位是0,1和0的或运算结果是1.
0001的右边第二位是0,0010右边的第二位是1,0和1的或运算结果是1.
0001的右边第三位是0,0010右边的第三位是0,0和0的或运算结果是0.
0001的右边第四位是0,0010右边的第四位是0,0和0的或运算结果是0.
结果就是0011.
cs
0001 (Rocket)
OR
0010 (Bomb)
= 0011 (同时具备 Rocket 和 Bomb)
我们也知道为什么叫"按位或",就是按照位置或
那么通过或运算,就可以将两个能力组合。
2. And运算进行判断(&) 叫做按位与
现在判断0011是否是成为火箭目标0001
回忆书写代码的时候,if多个条件,所有条件都是true,&&的结果才是true;只要有一个是false,就返回false。
那么对比0011(成为火箭和炸弹的目标)和0001(火箭目标)的每一位:
0011的右边第一位是1,0001右边的第一位是1,1和1的&运算结果是1.
0011的右边第二位是1,0001右边的第二位是0,1和0的&运算结果是0.
0011的右边第三位是0,0001右边的第三位是0,0和0的&运算结果是0.
0011的右边第四位是0,0001右边的第四位是0,0和0的&运算结果是0.
结果是0001,0001不是0。
cs
// 伪代码 火箭和炸弹确实包含了火箭
(0011 & 0001) = 0001
0001!=0
不等于0就代表他是,等于0就代表他不是
2.4 代码例子
多种能力组合成一个:
cs
// 我将rocket和bomb组合成了一个变量,
// 实际上是0001和0010的相加,
// 在转化为十进制就是1和2的相加。
tile.type.targetTypes = TargetType.Rocket | TargetType.Bomb;
判断:
cs
bool isRocket = (tile.type.targetTypes & TargetType.Rocket) != 0;
五.最后回到枚举
cs
public enum TargetType
{
None = 0,
Rocket = 1 << 0, // 1 左移 0 位 = 1
Bomb = 1 << 1, // 1 左移 1 位 = 2
Laser = 1 << 2, // 1 左移 2 位 = 4
}
我们恰巧发现,Rocket和Bomb组合的结果的是0011,转化为十进制是3,3恰巧在Bomb和Laser之间。
3 的确是介于 2(Bomb)和 4(Laser)之间
但我们一般不会在枚举里去写一个 RocketAndBomb = 3 这样的值
3 更多是"运行时组合出来的数",它就是多个标志一起结合的结果。
我们一般只在枚举里定义"单个能力"的值,组合由代码在运行时通过 | 得到。
向左运算法为我们将任意种组合留下了枚举值的空间。