Unity開發-SLG實時戰鬥頭像自適應算法

目前參與一款在研SLG研發,目前在開發實時戰鬥部分的表現,今天要分享的是實時戰鬥頭像自動排布算法的實現。


需求分析

  1. 部隊頭像的自動跟隨、
  2. 自動監測碰撞以避免頭像重疊、
  3. 需要滿足一定性能要求,不能產生過多的DrawCall和其他CPU開銷、
  4. 參考效果爲萬國覺醒實時戰鬥表現,當前方案基本實現相同效果。

設計思路

  1. 建立Root Canvas空間下的屏幕空間映射,進行虛擬網格拆分,作爲基礎Map<mapIndex,Area>,和單個頭像圖標的座標系。
  2. 提供1所建立的座標系的註冊和反註冊,用來記錄改空間內網格的佔用信息。
  3. 週期性進行Available檢測,不滿足則進行頭像位置變換。
  4. Available算法基於對象和Root的Relative Bounds 進行索引變換,查詢Area佔用情況。
  5. 每個對象內置固定個數的Available Points,運行時根據部隊朝向進行最優排序,週期選取最優點爲對象跟隨點。

實現方案要點

  • 建立Map<Index,Area>空間索引映射關係,作爲座標系管理和查詢的基礎
  • 可以直接通過Bounds和AreaSize計算對應座標,近似Hash算法的查詢複雜度
  • 週期性的Available檢測,避免高併發的運算
  • Vector.Dot對AvailablePoints進行排序
  • 使用緩存變量避免GC Alloc

PotraitBoard 可以放在UI任意節點,指定好控制參數,即可在Enable Disable時自動註冊和反註冊

PotraitManager掛載於場景常駐節點

PotraitManager._root需要爲UI節點下AnchorPreset爲Strench/Strench(鋪滿全屏)

PotraitManager.areasCount爲屏幕空間拆分的行列數

代碼略長,不過註釋充分,直接貼上了……

 

using System.Collections.Generic;
using System.Text;
using DG.Tweening;
using UnityEngine;
using XLua;

public class PotraitBoard : MonoBehaviour
{
    [LuaCallCSharp]
    public enum DefaultPos
    {
        Left = 1,
        Right = 2,
    }
    
    /// <summary>
    /// controll target
    /// </summary>
    public RectTransform target;

    /// <summary>
    /// available points
    /// </summary>
    public List<RectTransform> availablePoints = new List<RectTransform>();
    
    /// <summary>
    /// best sort of points
    /// </summary>
    public List<int> bestSortOfPoints = new List<int>();
    
    /// <summary>
    /// position illegle check duration
    /// </summary>
    private float checkDuration = 2f;

    /// <summary>
    /// pos of potrait sets
    /// </summary>
    private int curPos = 0;

    /// <summary>
    /// pos of potrait sets
    /// </summary>
    private int curIndex = 0;

    /// <summary>
    /// troop`s direction
    /// </summary>
    private Vector2 direction = Vector2.left;
    
    /// <summary>
    /// move to next pos
    /// </summary>
    /// <param name="nextPos"></param>
    private void MoveToNext(int nextPos)
    {
        target.DOLocalMove(availablePoints[nextPos].localPosition, 0.5f).SetEase(Ease.OutQuad);
    }

    /// <summary>
    /// move to next available point in availablePoints in default sequence, if cur is not available
    /// </summary>
    private void MoveToNextIfNotAvailable()
    {
        if (PotraitBoardManager.Instance.IsPointAvailable(availablePoints[curPos]))
            return;
        
        int sign = curPos;
        while (!PotraitBoardManager.Instance.IsPointAvailable(availablePoints[curPos]))
        {
            curPos++;
            curPos = curPos % availablePoints.Count;
            if (sign == curPos)
                return;
        }

        //Debug.Log("PotraitBoard MoveToNextIfNotAvailable Move From [" + sign + "] To Pos: [" + curPos + "]");
        
        PotraitBoardManager.Instance.UnRegisterPotrait(curIndex,availablePoints[sign]);
        curIndex = PotraitBoardManager.Instance.RegisterPotrait(availablePoints[curPos]);
        
        MoveToNext(curPos);
    }

    /// <summary>
    /// move to next best point available
    /// </summary>
    private void MoveToNextBestAvailable()
    {
        if (bestSortOfPoints.Count <= 0)
            ReSortPoints();

        int pIndex = 0;

        for (int i = 0; i < bestSortOfPoints.Count; i++)
        {
            if (PotraitBoardManager.Instance.IsPointAvailable(availablePoints[bestSortOfPoints[i]]))
            {
                pIndex = i;
                break;
            }
        }

        if (curPos != bestSortOfPoints[pIndex])
        {
            PotraitBoardManager.Instance.UnRegisterPotrait(curIndex,availablePoints[curPos]);

            curPos = bestSortOfPoints[pIndex];

            curIndex = PotraitBoardManager.Instance.RegisterPotrait(availablePoints[curPos]);
            
            MoveToNext(curPos);
        }
    }
    
    private void OnEnable()
    {
        curIndex = PotraitBoardManager.Instance.RegisterPotrait(availablePoints[curPos]);
        //MoveToNextBestAvailable();
    }


    private float timeCounting = 0f;
    void Update()
    {
        if (timeCounting < 0.001f)
        {
            //adjust real index before available check
            curIndex = PotraitBoardManager.Instance.CheckIndexCorrect(curIndex,availablePoints[curPos]);

            // #region Move next by sequence
            //     MoveToNextIfNotAvailable();
            //     timeCounting = checkDuration;
            // #endregion
            
            #region Move next with best
                MoveToNextBestAvailable();
                timeCounting = checkDuration;
            #endregion
        }
        else
        {
            timeCounting -= Time.deltaTime;
        }
    }
    
    private void OnDisable()
    {
        PotraitBoardManager.Instance.UnRegisterPotrait(curIndex,availablePoints[curPos]);
        bestSortOfPoints.Clear();
    }

    private void OnDestroy()
    {
        
    }

    #region about direction and points sort

    /// <summary>
    /// set default pos
    /// should call before enable
    /// </summary>
    /// <param name="defaultPos"></param>
    public void SetDefaultPosition(DefaultPos defaultPos)
    {
        switch (defaultPos)
        {
            case DefaultPos.Left:
                target.localPosition = availablePoints[1].localPosition;
                curPos = 1;
                break;
            case DefaultPos.Right:
                target.localPosition = availablePoints[5].localPosition;
                curPos = 5;
                break;
            default:
                break;;
        }
    }
    
    
    /// <summary>
    /// injert direction data from entity
    /// </summary>
    /// <param name="dir"></param>
    public void SetDirection(Vector2 dir)
    {
        direction = dir.normalized;
        //Debug.Log("SetDirection direction:" + direction.ToString() + " mag:" + direction.magnitude);
        // resort the available points
        ReSortPoints();
    }

    /// <summary>
    /// resort the best point
    /// </summary>
    private void ReSortPoints()
    {
        bestSortOfPoints.Clear();

        for (int i = 0; i < availablePoints.Count; i++)
        {
            bestSortOfPoints.Add(i);
        }
        
        bestSortOfPoints.Sort(RectCompare);
        
        //temp code
        StringBuilder sb = new StringBuilder();
        sb.Append("BestSortOfPoints: ");
        for (int i = 0; i < bestSortOfPoints.Count; i++)
        {
            sb.Append(" [ ");
            sb.Append(bestSortOfPoints[i]);
            sb.Append(" ] ");
        }
        //Debug.Log(sb.ToString());
    }

    private Vector2 pDir1 = Vector2.zero;
    private Vector2 pDir2 = Vector2.zero;
    
    /// <summary>
    /// rect compare 
    /// </summary>
    /// <param name="a"></param>
    /// <param name="b"></param>
    /// <returns></returns>
    private int RectCompare(int a, int b)
    {
        pDir1.x = availablePoints[a].localPosition.x;
        pDir1.y = availablePoints[a].localPosition.y;
        pDir1 = pDir1.normalized;
        
        pDir2.x = availablePoints[b].localPosition.x;
        pDir2.y = availablePoints[b].localPosition.y;
        pDir2 = pDir2.normalized;

        return Vector2.Dot(pDir1, direction) > Vector2.Dot(pDir2, direction) ? 1 : -1;
    }

    #endregion
    
    
}
using System.Collections.Generic;
using System.Text;
using UnityEngine;

public class PotraitBoardManager : MonoBehaviour
{
    /// <summary>
    /// scene area define
    /// </summary>
    private class Area
    {
        /// <summary>
        /// index of area
        /// </summary>
        public int index;
        
        /// <summary>
        /// area bounds
        /// </summary>
        public Bounds bounds;
        
        /// <summary>
        /// trans in area
        /// </summary>
        public Dictionary<RectTransform, Bounds> contents;

        /// <summary>
        /// contains target pos
        /// </summary>
        /// <param name="pos"></param>
        /// <returns></returns>
        public bool Contains(Vector3 pos)
        {
            return bounds.Contains(pos);
        }

        /// <summary>
        /// stringbuilder 
        /// </summary>
        private StringBuilder sb;
        
        public string ToString()
        {
            if (sb == null)
                sb = new StringBuilder();
            sb.Clear();

            sb.Append(" Index:" + index);
            sb.Append(" Bounds center: " + bounds.center.ToString());
            sb.Append(" Bounds size: " + bounds.size.ToString());
            
            return sb.ToString();
        }
    }
    
    /// <summary>
    /// map dictionary
    /// </summary>
    private Dictionary<int,Area> _areas = new Dictionary<int,Area>();
    
    /// <summary>
    /// space root, define the local space
    /// </summary>
    private RectTransform _root;
    
    /// <summary>
    /// local size of the space 
    /// </summary>
    private Vector2 canvasSize = Vector2.one;
    
    /// <summary>
    /// areas count define
    /// </summary>
    private Vector2 areasCount = new Vector2(10,20);
    
    /// <summary>
    /// size of area 
    /// </summary>
    private Vector2 areaSize = Vector2.one;
    
    /// <summary>
    /// Instance
    /// </summary>
    private static PotraitBoardManager _instance;

    public static PotraitBoardManager Instance
    {
        get { return _instance; }
        set { _instance = value; }
    }

    /// <summary>
    /// container cache 
    /// </summary>
    private Vector2[] probesCache = new Vector2[5]; 
    
    /// <summary>
    /// whether this trans`s area is available
    /// </summary>
    /// <param name="trans"> rect transform </param>
    /// <returns></returns>
    public bool IsPointAvailable(RectTransform trans)
    {
        Bounds bounds = GetRectReleativeBound(trans);
        probesCache[0] = new Vector2(bounds.center.x, bounds.center.y);
        probesCache[1] = new Vector2(bounds.center.x - bounds.extents.x, bounds.center.y - bounds.extents.y);
        probesCache[2] = new Vector2(bounds.center.x - bounds.extents.x, bounds.center.y + bounds.extents.y);
        probesCache[3] = new Vector2(bounds.center.x + bounds.extents.x, bounds.center.y - bounds.extents.y);
        probesCache[4] = new Vector2(bounds.center.x + bounds.extents.x, bounds.center.y + bounds.extents.y);

        int Index = -1;
        for (int i = 0; i < 5; i++)
        {
            Index = Pos2Index(probesCache[i]);
            
            // if this area exist other points
            if (_areas.ContainsKey(Index) && _areas[Index].contents.Count > 0)
            {
                if (!_areas[Index].contents.ContainsKey(trans) && _areas[Index].contents.Count > 0)
                    return false;
                
                if (_areas[Index].contents.ContainsKey(trans) && _areas[Index].contents.Count > 1)
                    return false;
            }
        }
        
        return true;
    }
    
    /// <summary>
    /// whether this trans`s area is available
    /// </summary>
    /// <param name="bounds"></param>
    /// <returns></returns>
    public bool IsPointAvailable(Bounds bounds)
    {
        int Index = Pos2Index(new Vector2(bounds.center.x,bounds.center.y));
        
        return _areas[Index].contents.Count <= 0 ;
    }
    
    /// <summary>
    /// register the trans to area
    /// </summary>
    /// <param name="trans"></param>
    /// <param name="bounds"></param>
    public void RegisterPotrait(RectTransform trans ,Bounds bounds)
    {
        int Index = Pos2Index(new Vector2(bounds.center.x,bounds.center.y));
        if (_areas.ContainsKey(Index))
            _areas[Index].contents.Add(trans, bounds);
        else
            Debug.LogWarning("RegisterPotrait Out Of Index Range:" + Index);
        //Debug.Log("PotraitBoardManager->RegisterPotrait Center:" + bounds.center.ToString() + "Push To Area: " + Index);
    }
    
    /// <summary>
    /// register the trans to area
    /// </summary>
    /// <param name="trans"></param>
    public int RegisterPotrait(RectTransform trans)
    {
        Bounds bounds = GetRectReleativeBound(trans);
        int Index = Pos2Index(new Vector2(bounds.center.x,bounds.center.y));
        if (_areas.ContainsKey(Index))
            _areas[Index].contents.Add(trans, bounds);
        else
            Debug.LogWarning("RegisterPotrait Out Of Index Range:" + Index);
        //Debug.Log("PotraitBoardManager->RegisterPotrait Center:" + bounds.center.ToString() + "Push To Area: " + Index);
        return Index;
    }

    /// <summary>
    /// register the trans to area
    /// </summary>
    /// <param name="trans"></param>
    /// <param name="bounds"></param>
    /// <returns></returns>
    public bool TryRegisterPotrait(RectTransform trans, Bounds bounds)
    {
        int Index = Pos2Index(new Vector2(bounds.center.x,bounds.center.y));

        if (_areas[Index].contents.Count <= 0)
        {
            _areas[Index].contents.Add(trans, bounds);
            //Debug.Log("PotraitBoardManager->TryRegisterPotrait Center:" + bounds.center.ToString() + "Push To Area: " + Index);
            return true;
        }
        else
        {
            return false;
        }
    }
    
    /// <summary>
    /// UnRegister the trans to area
    /// </summary>
    /// <param name="Index"></param>
    /// <param name="trans"></param>
    public void UnRegisterPotrait(int Index, RectTransform trans)
    {
        if (_areas.ContainsKey(Index))
            _areas[Index].contents.Remove(trans);
        else
            Debug.LogWarning("UnRegisterPotrait Out Of Index Range:" + Index);
        //Debug.Log("PotraitBoardManager->UnRegisterPotrait from:"  + Index);
    }
    
    /// <summary>
    /// UnRegister the trans to area
    /// </summary>
    public void UnRegisterPotrait(RectTransform trans, Bounds bounds)
    {
        int Index = Pos2Index(new Vector2(bounds.center.x,bounds.center.y));
        if (_areas.ContainsKey(Index))
            _areas[Index].contents.Remove(trans);
        else
            Debug.LogWarning("UnRegisterPotrait Out Of Index Range:" + Index);
    }

    /// <summary>
    /// calculate the rect Bounds relative to root rect 
    /// </summary>
    /// <param name="rect"></param>
    /// <returns></returns>
    public Bounds GetRectReleativeBound(RectTransform rect)
    {
        return RectTransformUtility.CalculateRelativeRectTransformBounds(_root, rect);
    }

    /// <summary>
    /// check cur index and trans pos is matched or not
    /// if not, unregister old from area and register to a new one
    /// return the new index
    /// </summary>
    /// <param name="indexOld"></param>
    /// <param name="trans"></param>
    /// <returns></returns>
    public int CheckIndexCorrect(int indexOld, RectTransform trans)
    {
        Bounds bounds = GetRectReleativeBound(trans);
        int Index = Pos2Index(new Vector2(bounds.center.x,bounds.center.y));
        if (Index != indexOld)
        {
            //Debug.Log("PotraitBoardManager->CheckIndexCorrect CheckIndexCorrect from [" + indexOld + "] to [" + Index + "]");
            PotraitBoardManager.Instance.UnRegisterPotrait(indexOld,trans);
            PotraitBoardManager.Instance.RegisterPotrait(trans,bounds);
        }

        return Index;
    }
    
    

    /// <summary>
    /// Init area in troop local space
    /// </summary>
    private void InitAreas()
    {
        _areas.Clear();
        
        canvasSize = _root.rect.size;
        Debug.Log("PotraitBoardManager->InitAreas size:" + canvasSize.ToString());

        areaSize = new Vector2(canvasSize.x / areasCount.y, canvasSize.y / areasCount.x);
        Debug.Log("PotraitBoardManager->InitAreas areaSize:" + areaSize.ToString());
        
        for (int i = 0; i < areasCount.x; i++)
        {
            for (int j = 0; j < areasCount.y; j++)
            {
                Area area = new Area();
                area.contents = new Dictionary<RectTransform, Bounds>();
                area.index = i * Mathf.RoundToInt(areasCount.y)  + j;
                area.bounds = CalculateBounds(i,j);
                _areas.Add(area.index,area);
                
                //Debug.Log("PotraitBoardManager InitAreas Creat Area:" + area.ToString());
            }
        }
    }

    /// <summary>
    ///  get Area Bounds
    /// </summary>
    /// <param name="i">row index</param>
    /// <param name="j">collumn index </param>
    /// <returns></returns>
    private Bounds CalculateBounds(int i, int j)
    {
        Bounds bounds = new Bounds();
        Vector2 min = new Vector2(i * areaSize.x - canvasSize.x / 2, j * areaSize.y - canvasSize.y / 2);
        Vector2 max = new Vector2((i + 1) * areaSize.x - canvasSize.x / 2, (j + 1) * areaSize.y - canvasSize.y / 2);
        bounds.size = max - min;
        bounds.center = (max + min) / 2f;
        bounds.extents = bounds.size / 2f;
        bounds.min = bounds.center - bounds.extents;
        bounds.max = bounds.center + bounds.extents;

        return bounds;
    }

    /// <summary>
    /// calculate pos 2 index 
    /// </summary>
    /// <param name="pos"></param>
    /// <returns></returns>
    private int Pos2Index(Vector2 pos)
    {
        int i, j;
        i = Mathf.FloorToInt((pos.x + canvasSize.x / 2) / areaSize.x) ;
        j = Mathf.FloorToInt((pos.y + canvasSize.y / 2) / areaSize.y) ;
        return j * Mathf.RoundToInt(areasCount.y)  + i;
    }
    
    private void Awake()
    {
        PotraitBoardManager.Instance = this;
        
        if (_root == null)
        {
            _root = GameObject.Find("Root/UIRoot/SceneLayer/TitleRoot/MyTroop").transform as RectTransform;
        }
    }
    
    void Start()
    {
        InitAreas();
    }
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章