返回总目录

第七章 寻路与地图对象(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)

八 优化地图(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)

我们主要修改一下调用了这些属性的地方。

FindMoveRangeFindPathDirect中,我们调用了判断是否可移动,而在这里我们有了新的属性。

        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,查看了源代码有些错误(暂时不影响使用):

  • PoolManagerDestroyPool方法判断写错了;

  • InstanceCollectionDispose中没有判断是否为空。

这暂时不影响我们,但请注意,在Destroy掉我们的Pool之前,最好要Despawn掉Pool中的所有Object。

SRPG游戏开发(三十)第七章 寻路与地图对象 - 八 优化地图(Optimize MapGraph)相关推荐

  1. SRPG游戏开发(二)第一章 FE4部分技术简述

    返回目录 第一章 FE4部分技术简述 本章节主要记录在开发FE4时,分析Rom的内容.我们从进入游戏后所见的顺序进行简述,详细的内容到开发时再谈论. 一    不再阐述的常用系统 这个部分的系统在所有 ...

  2. SRPG游戏开发(十)第五章 颜色映射与职业动画 - 二 颜色组(Color Chart)

    返回目录 第五章 颜色映射与职业动画 二       颜色组(Color Chart) 颜色组是保存许多颜色的一个容器,可以在Swapper中直接创建List<Color>或Color[] ...

  3. SRPG游戏开发(十六)第六章 基本框架 - 一 本章简介(Introduction)

    返回总目录 第六章 基本框架(Framework) 这一章主要编写各个游戏都有的基本框架. 关于源码.打包好的dll文件还没有上传,如何使用它们的Example工程也没有上传. 先来介绍一下源码的各个 ...

  4. SRPG游戏开发(六十三)第十一章 地图动作与地图事件 - 十二 完善地图信息与测试(Perfect MapEventInfo and Testing)

    返回<SRPG游戏开发>导航 第十一章 地图动作与地图事件(Map Action and Map Event) 我们已经有了剧本,而且可以运行剧本,但我们还缺少对地图的操作控制. 我们这一 ...

  5. SRPG游戏开发(六十一)第十一章 地图动作与地图事件 - 十 NPC操作(NPC Control)

    返回<SRPG游戏开发>导航 第十一章 地图动作与地图事件(Map Action and Map Event) 我们已经有了剧本,而且可以运行剧本,但我们还缺少对地图的操作控制. 我们这一 ...

  6. SRPG游戏开发(六十)第十一章 地图动作与地图事件 - 九 触发事件与切换回合(Trigger Events and Change Turn)

    返回<SRPG游戏开发>导航 第十一章 地图动作与地图事件(Map Action and Map Event) 我们已经有了剧本,而且可以运行剧本,但我们还缺少对地图的操作控制. 我们这一 ...

  7. SRPG游戏开发(二十六)第七章 寻路与地图对象 - 四 地图对象(Map Object)

    返回总目录 第七章 寻路与地图对象(Pathfinding and Map Object) 这一章主要进行寻路与地图对象的部分工作. 第七章 寻路与地图对象(Pathfinding and Map O ...

  8. SRPG游戏开发(六十四)间章 第十一点五章 总结(Summary)

    返回<SRPG游戏开发>导航 间章 第十一点五章 总结(Summary) 这一章,是对第十章与第十一章的一个补充性质的文章. 文章目录 间章 第十一点五章 总结(Summary) 一 说明 ...

  9. SRPG游戏开发(二十七)第七章 寻路与地图对象 - 五 搜索移动范围与路径(Search Move Range and Path)

    返回总目录 第七章 寻路与地图对象(Pathfinding and Map Object) 这一章主要进行寻路与地图对象的部分工作. 文章目录 第七章 寻路与地图对象(Pathfinding and ...

最新文章

  1. 虚拟机VMware下CentOS6.6安装教程图文详解
  2. 你没见过Java台式计算机和Java操作系统吧
  3. 大话数据结构 java源代码_大话数据结构(八)Java程序——双向链表的实现
  4. c#组元(Tuple)的使用
  5. Google-Guava-EventBus源码解读
  6. 【参与开源】J2EE开源项目JEECG快速开发平台,欢迎广大技术爱好者参与,第三期招募新成员
  7. 如何在Xubuntu 15.04中安装最新版Eclipse luna
  8. SQL SERVER 如何把1列多行数据 合并成一列显示
  9. 电脑删除文件需要管理员权限怎么办
  10. 扫描二维码 打开 小程序或是H5网页
  11. [system] Map key not configured
  12. Ubuntu 禁用Guest用户
  13. Spring Boot项目WebService接口发布、调用、以及常见错误详解
  14. Qt中添加资源文件及资源文件的使用
  15. 标准USB/Mini-USB接口,及OTG
  16. 我用Python抓取了自如上所有的租房信息,随心所欲的选房
  17. Ubuntu安装百度云盘
  18. 签名不对,请检查签名是否与开放平台上填写的一致。
  19. 大数据ui设计师_为什么设计师应该使用真实数据
  20. 2020-12-18:【黑盒测试用例设计】测试方法之场景法

热门文章

  1. 1分钟了解流程图、顺序图、状态图
  2. layui子页面创建一个新的页面
  3. 基于STM32音频解码MP3——vs1053
  4. 动态规划:面积最大正方形
  5. 冗余分析(RDA)中若包含生物学重复会怎样?
  6. docker常用命令大全(持续更新)
  7. 消耗卡路里的android程序,Movesum - 用食物来表示,走路消耗的卡路里 - Android 应用 - 【最美应用】...
  8. 胜博发公益:只要用手机就能随手做公益 苹果与许多公益团体合作
  9. (Miller Rabin算法)判断一个数是否为素数
  10. 洛谷P1486 [NOI2004] 郁闷的出纳员 题解