目前參與一款在研SLG研發,目前在開發實時戰鬥部分的表現,今天要分享的是實時戰鬥頭像自動排布算法的實現。
需求分析
- 部隊頭像的自動跟隨、
- 自動監測碰撞以避免頭像重疊、
- 需要滿足一定性能要求,不能產生過多的DrawCall和其他CPU開銷、
- 參考效果爲萬國覺醒實時戰鬥表現,當前方案基本實現相同效果。
設計思路
- 建立Root Canvas空間下的屏幕空間映射,進行虛擬網格拆分,作爲基礎Map<mapIndex,Area>,和單個頭像圖標的座標系。
- 提供1所建立的座標系的註冊和反註冊,用來記錄改空間內網格的佔用信息。
- 週期性進行Available檢測,不滿足則進行頭像位置變換。
- Available算法基於對象和Root的Relative Bounds 進行索引變換,查詢Area佔用情況。
- 每個對象內置固定個數的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();
}
}