算法:游戏内AOI视野算法(十字链表)

为什么要有这个算法?

对于一个游戏场景内的实体来说,当战斗频繁的时候,可能存在上千实体的数据同步,例如上千实体移动时的坐标同步,大型战斗场景时全场景实体的属性的同步等等,这样就会造成一个问题,同步数据量非常多,但是对于客户端来说,我们可以看到的屏幕视野大小是固定的,对于视野之外的实体的所有数据我们并不需要知道,所以就不需要同步,这时候就需要一种算法,可以让我们快速定位到在我视野范围内的实体有哪些,然后为这些玩家同步相关的消息。

算法数据结构设计

1、为了保证链表元素的查找速度,链表使用跳表实现。
2、需要xlist,ylist两条链表,需要将玩家的x,y坐标分别有序的存储到这两个链表上。

实现细节

注:目前的设计仅仅基于二维

public AoiZone(float xLinksLimit, float yLinksLimit)
{
    _xLinks = new AoiLinkedList(limit: xLinksLimit);//x轴链表
    _yLinks = new AoiLinkedList(limit: yLinksLimit);//y轴链表
}

public AoiLinkedList(int maxLayer = 8, float limit = 0)
{
    _limit = limit;//坐标比较时的容错值
    _maxLayer = maxLayer;//跳表层数
    Add(float.MinValue);
}

public AoiNode Add(float target, AoiEntity entity = null)
{
    var rLayer = 1;

    if (_header == null)
    {
        //创建跳表
        rLayer = _maxLayer;

        var tempHeader = _header = new AoiNode(rLayer, target, entity);//头结点

        for (var layer = _maxLayer - 1; layer >= 1; --layer)
        {
            _header = _header.Down = new AoiNode(layer, target, top: _header);  //top:pre指针 down:next指针
        }

        _header = tempHeader;
        return null;
    }
    
    //随机选取某层插入节点
    while (rLayer < _maxLayer && _random.Next(2) == 0) ++rLayer;    
    //最高的一层(节点最少的一层)头结点
    AoiNode cur = _header, insertNode = null, lastLayerNode = null;

    for (var layer = _maxLayer; layer >= 1; --layer)
    {
        while (cur.Right != null && cur.Right.Value < target) cur = cur.Right;//与要插入位置的x/y值比较大小(可能有新插入数据,每次都得比较)

        if (layer <= rLayer)    //
        {
            insertNode = new AoiNode(layer, target, entity: entity, left: cur, right: cur.Right);   //

            if (cur.Right != null) cur.Right.Left = insertNode;
            cur.Right = insertNode;//节点插入

            if (lastLayerNode != null)    //跳表上下层指针维护
            {
                lastLayerNode.Down = insertNode;
                insertNode.Top = lastLayerNode;
            }

            lastLayerNode = insertNode;
        }

        cur = cur.Down;//下一层
    }

    Count++;//增加节点数目
    return insertNode;
}

游戏中的update函数实现:

//key:实体ID
//area:视野范围
//enter:此帧玩家坐标视野内的玩家
public AoiEntity Refresh(long key, Vector2, out HashSet enter)
{
    var entity = Refresh(key, area);
    enter = entity?.ViewEntity;
    return entity;
}

public AoiEntity Refresh(long key, float x, float y, Vector2 area)
{
    if (!_entityList.TryGetValue(key, out var entity)) return null;

    var isFind = false;

    if (Math.Abs(entity.X.Value - x) > 0)
    {
        isFind = true;
        _xLinks.Move(entity.X, ref x);
    }

    if (Math.Abs(entity.Y.Value - y) > 0)
    {
        isFind = true;
        _yLinks.Move(entity.Y, ref y);
    }

    if (isFind) Find(entity, ref area);
    return entity;
}

//此函数主要是玩家每帧移动后,x,y坐标变换后将链表重新变成有序的过程
public void Move(AoiNode node, ref float target)
{
    var cur = node;

    #region Left

    if (target > cur.Value) //移动后的值大于当前节点的值
    {
        while (cur != null)
        {
            if (cur.Right != null && target > cur.Right.Value)
            {
                var findNode = cur;
                // Find the target node to be moved to.
                //此处如果节点很多,遍历可能会消耗一些性能
                while (findNode.Right != null && findNode.Right.Value < target) findNode = findNode.Right;    
                // Fuse the current node.
                CircuitBreaker(cur);//从当前位置移除
                // Move to the target node location
                cur.Left = findNode;
                cur.Right = findNode.Right;//加入到新的位置
                if (findNode.Right != null) findNode.Right.Left = cur;
                findNode.Right = cur;
            }

            cur.Value = target;
            cur = cur.Top;//调整跳表上面层的节点顺序
        }

        return;
    }

    #endregion Left

    #region Right

    while (cur != null)////移动后的值小于当前节点的值
    {
        if (cur.Left != null && target < cur.Left.Value)
        {
            // Find the target node to be moved to.
            var findNode = cur;
            while (findNode.Left != null && findNode.Left.Value > target) findNode = findNode.Left;
            // Fuse the current node.
            CircuitBreaker(cur);
            // Move to the target node location
            cur.Right = findNode;
            cur.Left = findNode.Left;
            if (findNode.Left != null) findNode.Left.Right = cur;
            findNode.Left = cur;
        }

        cur.Value = target;
        cur = cur.Top;
    }

    #endregion Right
}
private void Find(AoiEntity node, ref Vector2 area)
{
    //将上帧的可视实体存到备份HashSet集合里
    SwapViewEntity(ref node.ViewEntity, ref node.ViewEntityBak);

    #region xLinks
    
    //定位到此实体,然后再xlist,ylist上分别进行left、right指针遍历,将distance在指定范围内的实体存入此帧ViewEntity
    for (var i = 0; i < 2; i++)
    {
        var cur = i == 0 ? node.X.Right : node.X.Left;

        while (cur != null)
        {
            if (Math.Abs(Math.Abs(cur.Value) - Math.Abs(node.X.Value)) > area.X)    //cur.value为aoi网格节点的value
            {
                break;
            }

            if (Math.Abs(Math.Abs(cur.Entity.Y.Value) - Math.Abs(node.Y.Value)) <= area.Y)  //cur.Entity.Y.Value为当前网格节点上的实体value
            {
                if (Distance(
                    new Vector2(node.X.Value, node.Y.Value),
                    new Vector2(cur.Entity.X.Value, cur.Entity.Y.Value)) <= area.X)
                {
                    node.ViewEntity.Add(cur.Entity.Key);//在范围内的话
                }
            }

            cur = i == 0 ? cur.Right : cur.Left; //分别在左右进行遍历
        }
    }

    #endregion xLinks

    #region yLinks

    for (var i = 0; i < 2; i++)
    {
        var cur = i == 0 ? node.Y.Right : node.Y.Left;

        while (cur != null)
        {
            if (Math.Abs(Math.Abs(cur.Value) - Math.Abs(node.Y.Value)) > area.Y)
            {
                break;
            }

            if (Math.Abs(Math.Abs(cur.Entity.X.Value) - Math.Abs(node.X.Value)) <= area.X)
            {
                if (Distance(
                    new Vector2(node.X.Value, node.Y.Value),
                    new Vector2(cur.Entity.X.Value, cur.Entity.Y.Value)) <= area.Y)
                {
                    node.ViewEntity.Add(cur.Entity.Key);
                }
            }

            cur = i == 0 ? cur.Right : cur.Left;
        }
    }

    #endregion yLinks
}
//用于函数回调
//本帧相对上帧离开视野的人
public IEnumerable Leave => ViewEntityBak.Except(ViewEntity);
//本帧相对上帧进入视野的人
public IEnumerable NewEnter => ViewEntity.Except(ViewEntityBak);//本帧相对上帧进入视野的人

测试代码

private static void Main(string[] args)
  {
  var zone = new AoiZone(.001f, .001f);
  var area = new Vector2(3, 3);

  // 添加500个玩家。

  for (var i = 1; i <= 500; i++) zone.Enter(i, i, i);

  // 刷新key为3的信息。

  zone.Refresh(3, area, out var enters);

  Console.WriteLine("---------------id为3玩家当前视野范围的玩家列表--------------");

  foreach (var aoiKey in enters)
  {
      var findEntity = zone[aoiKey];
      Console.WriteLine($"X:{findEntity.X.Value} Y:{findEntity.Y.Value}");
  }

  // 更新key为3的坐标为(20,20)。

  var entity = zone.Refresh(3, 20, 20, new Vector2(3, 3), out enters);

  Console.WriteLine("---------------id为3玩家移动后离开玩家视野范围的玩家列表--------------");

  foreach (var aoiKey in entity.Leave)
  {
      var findEntity = zone[aoiKey];
      Console.WriteLine($"X:{findEntity.X.Value} Y:{findEntity.Y.Value}");
  }

  Console.WriteLine("---------------id为3玩家移动后新加入玩家视野范围的玩家列表--------------");

  foreach (var aoiKey in entity.NewEnter)
  {
      var findEntity = zone[aoiKey];
      Console.WriteLine($"X:{findEntity.X.Value} Y:{findEntity.Y.Value}");
  }

  // 离开当前AOI

  zone.Exit(50);}

测试结果

---------------id为3玩家当前视野范围的玩家列表--------------
X:4 Y:4
X:5 Y:5
X:2 Y:2
X:1 Y:1
---------------id为3玩家移动后离开玩家视野范围的玩家列表--------------
X:4 Y:4
X:5 Y:5
X:2 Y:2
X:1 Y:1
---------------id为3玩家移动后新加入玩家视野范围的玩家列表--------------
X:20 Y:20
X:21 Y:21
X:22 Y:22
X:19 Y:19
X:18 Y:18

现存问题

此十字链表设计目前存在一个问题:当项目中实体数目非常多,每帧实体移动频繁,每帧都会去进行x,y链表坐标排序,对性能消耗还是蛮大的,还在思考单纯用链表如何解决此问题。

引用: https://github.com/qq362946/A...

你可能感兴趣的:(c++算法)