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

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/darkrabbit/article/details/82666493

返回总目录

第七章 寻路与地图对象(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)是否存在,只需要做位与运算,即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 List<MapCursor> m_Cursors = new List<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)
                {
                    for (int i = 0; i < m_Cursors.Count; i++)
                    {
                        ObjectPool.DespawnUnsafe(m_Cursors[i].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;
                }
    
                // TODO
    
                return 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。

猜你喜欢

转载自blog.csdn.net/darkrabbit/article/details/82666493