利用<<左移运算符优雅的设计游戏能力的任意组合和判断

一、提一个需求

方块是否可以被某些道具消除。

比如方块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 更多是"运行时组合出来的数",它就是多个标志一起结合的结果。

我们一般只在枚举里定义"单个能力"的值,组合由代码在运行时通过 | 得到。

向左运算法为我们将任意种组合留下了枚举值的空间。

相关推荐
曹牧2 小时前
C#中,#region和#endregion
开发语言·c#
czhc11400756632 小时前
c# 1121 构造方法
java·javascript·c#
da_vinci_x3 小时前
PS 3D Viewer (Beta):概念美术的降维打击,白模直接在PS里转光打影出5张大片
人工智能·游戏·3d·prompt·aigc·材质·游戏美术
在路上看风景4 小时前
2.3 C#装箱和拆箱
开发语言·c#
め.4 小时前
用Unity复刻童年经典游戏—愤怒的小鸟
游戏
喵了几个咪5 小时前
游戏字体渲染
开发语言·python·游戏
张丶大帅6 小时前
别踩白块游戏(附源代码)
c语言·游戏
2501_940094026 小时前
模拟器全部BIOS合集 RetroArch BIOS 解决模拟器提示缺少bios的问题 通用所有游戏模拟器
游戏