第七章 寻路与地图对象(Pathfinding and Map Object)
这一章主要进行寻路与地图对象的部分工作。
八 优化地图(Optimize MapGraph)
这一节我们来优化地图相关的代码。
1 优化格子数据(Optimize CellData)
当我们显示移动范围后,再次点击地图角色将移动到指定位置,在我们之前的EditorTestPathFinding
中,我们是保存了移动范围,然后判断点击的格子是否在集合中;而有没有Tile我们使用了一个bool
变量保存;在这里我们可以做一些文章。
其实可以想到,格子有几种状态:
有没有Tile
有没有移动范围网格
有没有攻击范围网格
有没有地图对象
这些属性我们时常需要进行判断,每一个我们都单独会写变量保存状态,这里我们可以整合变量,使用一个二进制来保存它们,每一位表示一个开关。
首先,建立一个二进制Enum
:
using System;
namespace DR.Book.SRPG_Dev.Maps
{
/// <summary>
/// 格子状态
/// </summary>
[Serializable, Flags]
public enum CellStatus : byte
{
/// <summary>
/// 没有任何东西, 0000 0000
/// </summary>
None = 0,
/// <summary>
/// 有TerrainTile, 0000 0001
/// </summary>
TerrainTile = 0x01,
/// <summary>
/// 移动光标, 0000 0010
/// </summary>
MoveCursor = 0x02,
/// <summary>
/// 攻击光标, 0000 0100
/// </summary>
AttackCursor = 0x04,
/// <summary>
/// 地图对象, 0000 1000
/// </summary>
MapObject = 0x08
// 如果有其它需求,在这里添加其余4个开关属性
/// <summary>
/// 全部8个开关, 1111 1111
/// </summary>
All = byte.MaxValue
}
}
这样你会看到,我们有8个开关的位置,而byte
只占1个字节,即1个字节保存了8个“bool”变量。
如果你需要更多的开关位置,请继承相应数字的无符号类型(有符号类型有负数参与,你需要更深入的了解二进制补码知识与计算),例如你需要32个开关,你可以:
using System;
[Serializable, Flags]
public enum YourSwitch : UInt32
{
// your switches
/// <summary>
/// 1111 1111 1111 1111 1111 1111 1111 1111
/// </summary>
All = UInt32.MaxValue
}
然后,在CellData
中添加这个变量:
private CellStatus m_Status = CellStatus.None;
最后,我们来说说如何打开和关闭它们。
1.1 打开与关闭开关(Switch On or Off)
之前我们已经使用过二进制的Enum(Direction
),但没有过多详细介绍过它。这里我们将介绍一下二进制的位运算。
我们的基本类型byte
,1字节有8位,取值范围为[0, 255],即二进制的区间[0000 0000, 1111 1111],每一位都可表示成一个开关。
想要更了解进制之间的转换与计算方式,可以访问我的另一篇文章编程基础 - 进制转换(Binary Conversion)
每一位上的二进制,1都代表打开(存在),0都代表关闭(不存在)。我们的开关m_Status
默认为CellStatus.None
即全部是关闭的,二进制位 0000 0000
。
打开开关:
要打开其中某一个开关,根据位运算规则,我们需要或(|)操作这个位置的值;举例来说,如果此格子有移动光标,从右数第2位表示此开关,只需要或(|)上
0000 0010
即可打开开关,即0000 0000 | 0000 0010 = 0000 0010
。关闭开关:
我们先假设Tile、移动光标与攻击光标都存在,即
0000 0111
(游戏中不会出现这种状态,移动光标与攻击光标是不能同时出现的)。我们希望能够关闭移动光标,即希望结果为
0000 0101
,而移动光标为0000 0010
;发现4种位运算都不能满足我们的情况,不过很容易看到0000 0111 - 0000 0010 = 0000 0101
可以达到我们的效果。不过这产生了一个新的问题,当我们的开关已经是关闭状态了,就得不到我们的需求。例如我们希望关闭
MapObject
,即0000 0111 - 0000 1000 = 1111 1111
,我的天啊,开关居然全部都打开了。这可不是我们需要的,我们希望开关都能保持不变,即依然是0000 0111
。要解决这个问题有两种方式:
其一,我们可通过观察,相减的结果再次位与(&)开关原始状态即可得到结果,即
(0000 0111 - 0000 0010) & 0000 0111 = 0000 0101
,关闭MapObject
为(0000 0111 - 0000 1000) & 0000 0111 = 0000 0111
。但这种运算使用了减法,在某些语言中(比如C#)是不支持byte
运算的,需要类型转换,这有些麻烦,所以我们采用第二种方式。其二,我们需要将它关闭,其它开关打开,即取反(~)得到
1111 1101
,然后再与开关做位与(&)运算,即0000 0111 & 1111 1101 = 0000 0101
;再次试验关闭MapObject
,即0000 0111 & (~ 0000 1000) = 0000 0111
。这样就解决了关闭问题。
基于以上分析,我们的代码为:
/// <summary>
/// 设置状态开关
/// </summary>
/// <param name="status"></param>
/// <param name="isOn"></param>
public void SwitchStatus(CellStatus status, bool isOn)
{
if (isOn)
{
m_Status |= status;
}
else
{
m_Status &= ~status;
}
}
这样,我们就可以控制是否打开开关与关闭开关了,拥有了设置开关属性,我们还要能够利用开关,即知道开关的状态。
1.2 检查开关(Check Switch)
要知道开关是否被打开,只需要其对应的二进制数字是否为1即可。这只需要一个位与(&)运算即可。
例如,假设原始状态为0000 0111
;我们希望知道攻击光标(0000 0010
)是否存在,只需要做位与运算