SRPG游戏开发(三十)第七章 寻路与地图对象 - 八 优化地图(Optimize MapGraph)
返回总目录
第七章 寻路与地图对象(Pathfinding and Map Object)
这一章主要进行寻路与地图对象的部分工作。
- 第七章 寻路与地图对象(Pathfinding and Map Object)
- 八 优化地图(Optimize MapGraph)
- 1 优化格子数据(Optimize CellData)
- 1.1 打开与关闭开关(Switch On or Off)
- 1.2 检查开关(Check Switch)
- 1.3 修改数据属性(Modify Properties)
- 1.4 修改寻路中的代码(Modify Search Method)
- 1.5 修改光标显示方法(Modify Cursor Method)
- 2 优化测试代码(Modify Testing Code)
- 3 优化地图(Optimize MapGraph)
- 3.1 移动范围与攻击范围(Search Move\/Attack Range)
- 3.2 HashSet判断相等(IEqualityComparer)
- 3.3 显示光标(Show Cursor)
- 4 对象池的错误(ObjectPool Bug)
- 1 优化格子数据(Optimize CellData)
- 八 优化地图(Optimize MapGraph)
八 优化地图(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
)是否存在,只需要做位与运算,即0000 0111 & 0000 0010 = 0000 0010
;类似的我们希望知道地图对象(0000 1000
)是否存在,即0000 0111 & 0000 1000 = 0000 0000
;你会发现它们的区别,即存在的结果与输入的结果相同,而不存在的结果为0。
public bool CheckStatus(CellStatus status){return (m_Status & status) == status;}
这样我们确实可以检测了,但我们还希望能够知道是否有开关开启,比如我们希望知道是否有光标在格子上,这样检测就不可以了,但我们发现,只有关闭时才为0,主要有开启的就会大于0;例如,假设原始状态为0000 0101
,即0000 0101 & (0000 0100 | 0000 0010) = 0000 0100 > 0
,所以修改代码:
/// <summary>/// 开关是否开启 /// any:/// true 表示,判断在status中是否存在开启项/// false 表示,判断status中是否全部开启/// </summary>/// <param name="status"></param>/// <param name="any"></param>/// <returns></returns>public bool CheckStatus(CellStatus status, bool any){return any ? (m_Status & status) != 0 : (m_Status & status) == status;}
1.3 修改数据属性(Modify Properties)
我们有开关属性,例如m_HasTile
属性就可以删除了;而且还可以添加许多属性。
修改后的Common属性:
private Vector3Int m_Position;private MapObject m_MapObject;private CellStatus m_Status = CellStatus.None;/// <summary>/// 坐标位置/// </summary>public Vector3Int position{get { return m_Position; }}/// <summary>/// 是否有Tile/// </summary>public bool hasTile{get { return CheckStatus(CellStatus.TerrainTile, false); }set { SwitchStatus(CellStatus.TerrainTile, value); }}/// <summary>/// 是否有Cursor/// </summary>public bool hasCursor{get { return CheckStatus(CellStatus.MoveCursor | CellStatus.AttackCursor, true); }set { SwitchStatus(CellStatus.MoveCursor | CellStatus.AttackCursor, value); }}/// <summary>/// 是否有移动范围光标/// </summary>public bool hasMoveCursor{get { return CheckStatus(CellStatus.MoveCursor, false); }set { SwitchStatus(CellStatus.MoveCursor, value); }}/// <summary>/// 是否有攻击范围光标/// </summary>public bool hasAttackCursor{get { return CheckStatus(CellStatus.AttackCursor, false); }set { SwitchStatus(CellStatus.AttackCursor, value); }}/// <summary>/// 地图对象/// </summary>public MapObject mapObject{get { return m_MapObject; }set{m_MapObject = value;SwitchStatus(CellStatus.MapObject, value != null);}}/// <summary>/// 是否有地图对象/// </summary>public bool hasMapObject{get { return mapObject != null; }}/// <summary>/// 是否可移动/// </summary>public bool canMove{get { return hasTile && !hasMapObject; }}/// <summary>/// 获取状态开关/// </summary>/// <returns></returns>public CellStatus GetStatus(){return m_Status;}
虽然我们的属性有非常多,但其中大部分属性都保存在同1个字节中。
1.4 修改寻路中的代码(Modify Search Method)
我们主要修改一下调用了这些属性的地方。
在FindMoveRange
与FindPathDirect
中,我们调用了判断是否可移动,而在这里我们有了新的属性。
public override bool CanAddAdjacentToReachable(PathFinding search, CellData adjacent){//// 没有Tile//if (!adjacent.hasTile)//{// return false;//}//// 已经有对象了//if (adjacent.hasMapObject)//{// return false;//}// 是否可移动if (!adjacent.canMove){return false;}// 省略其它代码}
1.5 修改光标显示方法(Modify Cursor Method)
在MapGraph
中,显示范围光标与隐藏范围光标时,我们需要对格子进行设置,这样的好处是不必再分别保存光标了。
修改字段(Field):
///// <summary>///// 移动范围光标集合///// </summary>//private List<MapCursor> m_MapMoveCursors = new List<MapCursor>();///// <summary>///// 攻击范围光标集合///// </summary>//private List<MapCursor> m_MapAttackCursors = new List<MapCursor>();/// <summary>/// 光标集合/// </summary>private HashSet<MapCursor> m_Cursors = new HashSet<MapCursor>();
修改显示光标函数(Show Cursor Method):
/// <summary>/// 显示cursor/// </summary>/// <param name="cells"></param>/// <param name="type"></param>public void ShowRangeCursors(IEnumerable<CellData> cells, MapCursor.CursorType type){if (type == MapCursor.CursorType.Mouse){return;}foreach (CellData cell in cells){MapCursor cursor = CreateMapObject(runtimeCursorPrefab, cell.position) as MapCursor;if (cursor != null){cursor.name = string.Format("{0} Cursor {1}",type.ToString(),cell.position.ToString());cursor.cursorType = type;if (type == MapCursor.CursorType.Move){//m_MapMoveCursors.Add(cursor);cell.hasMoveCursor = true;}else if (type == MapCursor.CursorType.Attack){//m_MapAttackCursors.Add(cursor);cell.hasAttackCursor = true;}m_Cursors.Add(cursor);}}}
修改隐藏光标函数(Hide Cursor Method):
我们的光标已经由新的列表保存了,但我们更新格子信息不再这个函数里进行,而在光标本身进行。
/// <summary>/// 隐藏cursor/// </summary>public void HideRangeCursors(){//if (m_MapMoveCursors.Count > 0)//{// for (int i = 0; i < m_MapMoveCursors.Count; i++)// {// ObjectPool.DespawnUnsafe(m_MapMoveCursors[i].gameObject, true);// }// m_MapMoveCursors.Clear();//}//if (m_MapAttackCursors.Count > 0)//{// for (int i = 0; i < m_MapAttackCursors.Count; i++)// {// ObjectPool.DespawnUnsafe(m_MapAttackCursors[i].gameObject, true);// }// m_MapAttackCursors.Clear();//}if (m_Cursors.Count > 0){foreach (MapCursor cursor in m_Cursors){ObjectPool.DespawnUnsafe(cursor.gameObject, true);}m_Cursors.Clear();}}
我们在光标本身进行关闭开关,打开
MapCursor.cs
,继承OnDespawn
方法:public override void OnDespawn(){if (map != null && mapObjectType == MapObjectType.Cursor){CellData cellData = map.GetCellData(cellPosition);if (cellData != null){cellData.hasCursor = false;}}base.OnDespawn();}
2 优化测试代码(Modify Testing Code)
在EditorTestPathFinding
中,我们需要修改的地方不是很多,在搜寻移动范围时,可以让我们不用再保存显示的格子数据m_CursorCells
了。
在搜索移动范围后,我们二次搜索攻击范围使用的是各种cells.Contains
方法,这相对来说已经非常消耗性能了,经过属性的修改,我们不用再使用它了。而且在我们不需要顺序,这样性能不如用HashSet
来好。
删除或注释一切与m_CursorCells
有关的代码,然后修改移动范围代码:
/// <summary>/// 生成Cursors/// </summary>/// <param name="cells"></param>/// <param name="atk"></param>public void CreateTestCursors(IEnumerable<CellData> cells, bool atk){// 省略}/// <summary>/// 当左键按下时,Move类型的活动/// </summary>/// <param name="selectedCell"></param>/// <returns></returns>public List<CellData> ShowMoveRangeCells(CellData selectedCell){List<CellData> cells;//if (m_CursorCells.Count == 0 || !m_CursorCells.Contains(selectedCell))if (!selectedCell.hasMoveCursor){if (m_DebugInfo){Debug.LogFormat("MoveRange: start {0}, move point {1}, status ({2})",selectedCell.position.ToString(),m_MovePoint.ToString(),selectedCell.GetStatus().ToString());}ClearTestCursors();//m_CursorCells.Clear();m_TestClass.UpdatePosition(selectedCell.position);cells = new List<CellData>(m_Map.SearchMoveRange(selectedCell, m_MovePoint, m_MoveConsumption));//m_CursorCells.AddRange(cells);CreateTestCursors(cells, false);if (m_PathfindingType == TestPathfindingType.MoveAndAttack){// 移动范围后,进行查找攻击范围HashSet<CellData> attackCells = new HashSet<CellData>();foreach (var cell in cells.ToArray()){//foreach (var c in m_Map.SearchAttackRange(cell, m_AttackRange.x, m_AttackRange.y, true))//{// //if (!cells.Contains(c) && !attackCells.Contains(c))// if (!c.hasCursor)// {// attackCells.Add(c);// }//}attackCells.UnionWith(m_Map.SearchAttackRange(cell, m_AttackRange.x, m_AttackRange.y, true).Where(c => !c.hasCursor));}CreateTestCursors(attackCells, true);}}else{if (m_DebugInfo){Debug.LogFormat("Selected end {0} status ({1})", selectedCell.position,selectedCell.GetStatus().ToString());}ClearTestCursors();//m_CursorCells.Clear();Stack<CellData> pathCells = m_Map.searchPath.BuildPath(selectedCell);cells = new List<CellData>(pathCells);m_TestClass.animatorController.PlayMove();m_TestClass.StartMove(pathCells);CreateTestCursors(cells, false);}return cells;}
3 优化地图(Optimize MapGraph)
在测试代码中,我们移动范围的方法看起来已经比较完善了,经过少许修改就可以移动到MapGraph
中。只不过我们还没有数据,需要数据的地方先保留。
3.1 移动范围与攻击范围(Search Move\/Attack Range)
1 创建方法:
/// <summary>/// 搜寻移动范围与攻击范围/// </summary>/// <param name="cls"></param>/// <param name="nAtk">是否包含攻击范围</param>/// <param name="moveCells"></param>/// <param name="atkCells"></param>/// <returns></returns>public bool SearchMoveRange(MapClass cls, bool nAtk, out IEnumerable<CellData> moveCells, out IEnumerable<CellData> atkCells){moveCells = null;atkCells = null;if (cls == null){Debug.LogErrorFormat("MapGraph -> SearchMoveRange: `cls` is null.");return false;}CellData cell = GetCellData(cls.cellPosition);if (cell == null){Debug.LogErrorFormat("MapGraph -> SearchMoveRange: `cls.cellPosition` is out of range.");return false;}// TODOreturn true;}
2 搜寻移动范围
// TODO 搜索移动范围,从MapClass中读取数据float movePoint = 0;MoveConsumption consumption = null;List<CellData> rangeCells = SearchMoveRange(cell, movePoint, consumption);if (rangeCells == null){return false;}moveCells = rangeCells.ToArray();
3 搜寻攻击范围
if (nAtk /* TODO && 是否有武器 */){// TODO 搜索攻击范围,从MapClass中读取数据Vector2Int atkRange = Vector2Int.one;HashSet<CellData> atkRangeCells = new HashSet<CellData>();foreach (CellData moveCell in moveCells){rangeCells = SearchAttackRange(moveCell, atkRange.x, atkRange.y, true);if (rangeCells != null && rangeCells.Count > 0){atkRangeCells.UnionWith(rangeCells.Where(c => !c.hasCursor));}}atkCells = atkRangeCells;}
3.2 HashSet判断相等(IEqualityComparer)
在二次搜索范围的时候,我们使用了HastSet
来更快的添加光标。在HashSet
内部判断时,我们不必使用整个CellData
,使用坐标即可。
private CellPositionEqualityComparer m_CellPositionEqualityComparer = new CellPositionEqualityComparer();/// <summary>/// 判断两个Cell的Position是否相等/// </summary>public CellPositionEqualityComparer cellPositionEqualityComparer{get{if (m_CellPositionEqualityComparer == null){m_CellPositionEqualityComparer = new CellPositionEqualityComparer();}return m_CellPositionEqualityComparer;}}/// <summary>/// 判断两个Cell的Position是否相等/// </summary>public class CellPositionEqualityComparer : IEqualityComparer<CellData>{public bool Equals(CellData x, CellData y){return x.position == y.position;}public int GetHashCode(CellData obj){return obj.position.GetHashCode();}}
在创建时使用:
HashSet<CellData> atkRangeCells = new HashSet<CellData>(cellPositionEqualityComparer);
3.3 显示光标(Show Cursor)
/// <summary>/// 搜寻和显示范围/// </summary>/// <param name="cls"></param>/// <param name="nAtk">包含攻击范围</param>/// <returns></returns>public bool SearchAndShowMoveRange(MapClass cls, bool nAtk){IEnumerable<CellData> moveCells, atkCells;if (!SearchMoveRange(cls, nAtk, out moveCells, out atkCells)){return false;}if (moveCells != null){ShowRangeCursors(moveCells, MapCursor.CursorType.Move);}if (atkCells != null){ShowRangeCursors(atkCells, MapCursor.CursorType.Attack);}return true;}
4 对象池的错误(ObjectPool Bug)
我们的对象池似乎有bug,查看了源代码有些错误(暂时不影响使用):
PoolManager
的DestroyPool
方法判断写错了;在
InstanceCollection
的Dispose
中没有判断是否为空。
这暂时不影响我们,但请注意,在Destroy
掉我们的Pool之前,最好要Despawn
掉Pool中的所有Object。
SRPG游戏开发(三十)第七章 寻路与地图对象 - 八 优化地图(Optimize MapGraph)相关推荐
- SRPG游戏开发(二)第一章 FE4部分技术简述
返回目录 第一章 FE4部分技术简述 本章节主要记录在开发FE4时,分析Rom的内容.我们从进入游戏后所见的顺序进行简述,详细的内容到开发时再谈论. 一 不再阐述的常用系统 这个部分的系统在所有 ...
- SRPG游戏开发(十)第五章 颜色映射与职业动画 - 二 颜色组(Color Chart)
返回目录 第五章 颜色映射与职业动画 二 颜色组(Color Chart) 颜色组是保存许多颜色的一个容器,可以在Swapper中直接创建List<Color>或Color[] ...
- SRPG游戏开发(十六)第六章 基本框架 - 一 本章简介(Introduction)
返回总目录 第六章 基本框架(Framework) 这一章主要编写各个游戏都有的基本框架. 关于源码.打包好的dll文件还没有上传,如何使用它们的Example工程也没有上传. 先来介绍一下源码的各个 ...
- SRPG游戏开发(六十三)第十一章 地图动作与地图事件 - 十二 完善地图信息与测试(Perfect MapEventInfo and Testing)
返回<SRPG游戏开发>导航 第十一章 地图动作与地图事件(Map Action and Map Event) 我们已经有了剧本,而且可以运行剧本,但我们还缺少对地图的操作控制. 我们这一 ...
- SRPG游戏开发(六十一)第十一章 地图动作与地图事件 - 十 NPC操作(NPC Control)
返回<SRPG游戏开发>导航 第十一章 地图动作与地图事件(Map Action and Map Event) 我们已经有了剧本,而且可以运行剧本,但我们还缺少对地图的操作控制. 我们这一 ...
- SRPG游戏开发(六十)第十一章 地图动作与地图事件 - 九 触发事件与切换回合(Trigger Events and Change Turn)
返回<SRPG游戏开发>导航 第十一章 地图动作与地图事件(Map Action and Map Event) 我们已经有了剧本,而且可以运行剧本,但我们还缺少对地图的操作控制. 我们这一 ...
- SRPG游戏开发(二十六)第七章 寻路与地图对象 - 四 地图对象(Map Object)
返回总目录 第七章 寻路与地图对象(Pathfinding and Map Object) 这一章主要进行寻路与地图对象的部分工作. 第七章 寻路与地图对象(Pathfinding and Map O ...
- SRPG游戏开发(六十四)间章 第十一点五章 总结(Summary)
返回<SRPG游戏开发>导航 间章 第十一点五章 总结(Summary) 这一章,是对第十章与第十一章的一个补充性质的文章. 文章目录 间章 第十一点五章 总结(Summary) 一 说明 ...
- SRPG游戏开发(二十七)第七章 寻路与地图对象 - 五 搜索移动范围与路径(Search Move Range and Path)
返回总目录 第七章 寻路与地图对象(Pathfinding and Map Object) 这一章主要进行寻路与地图对象的部分工作. 文章目录 第七章 寻路与地图对象(Pathfinding and ...
最新文章
- 虚拟机VMware下CentOS6.6安装教程图文详解
- 你没见过Java台式计算机和Java操作系统吧
- 大话数据结构 java源代码_大话数据结构(八)Java程序——双向链表的实现
- c#组元(Tuple)的使用
- Google-Guava-EventBus源码解读
- 【参与开源】J2EE开源项目JEECG快速开发平台,欢迎广大技术爱好者参与,第三期招募新成员
- 如何在Xubuntu 15.04中安装最新版Eclipse luna
- SQL SERVER 如何把1列多行数据 合并成一列显示
- 电脑删除文件需要管理员权限怎么办
- 扫描二维码 打开 小程序或是H5网页
- [system] Map key not configured
- Ubuntu 禁用Guest用户
- Spring Boot项目WebService接口发布、调用、以及常见错误详解
- Qt中添加资源文件及资源文件的使用
- 标准USB/Mini-USB接口,及OTG
- 我用Python抓取了自如上所有的租房信息,随心所欲的选房
- Ubuntu安装百度云盘
- 签名不对,请检查签名是否与开放平台上填写的一致。
- 大数据ui设计师_为什么设计师应该使用真实数据
- 2020-12-18:【黑盒测试用例设计】测试方法之场景法
热门文章
- 1分钟了解流程图、顺序图、状态图
- layui子页面创建一个新的页面
- 基于STM32音频解码MP3——vs1053
- 动态规划:面积最大正方形
- 冗余分析(RDA)中若包含生物学重复会怎样?
- docker常用命令大全(持续更新)
- 消耗卡路里的android程序,Movesum - 用食物来表示,走路消耗的卡路里 - Android 应用 - 【最美应用】...
- 胜博发公益:只要用手机就能随手做公益 苹果与许多公益团体合作
- (Miller Rabin算法)判断一个数是否为素数
- 洛谷P1486 [NOI2004] 郁闷的出纳员 题解