AOI视野管理

2018/09/19 Mmo-Game

AOI视野管理,采用九宫格

目录

为什么要进行视野管理?

1.如果不进行视野管理,流量上来看,假设平均每秒5个移动包(80字节),1个技能包(160字节),单人每秒多少?500kb。

2.客户端表现来看,客户端受屏幕大小影响,离得远的玩家看不见,没有必要发给客户端。客户端性能所限,玩家多,放各种 技能,当然会看。

3.从性能上考虑,服务端不用遍历全场景对象…也会大大减少性能消耗。

1.视野管理一个经典的实现–九宫格

一般九宫格的一个格子大小大概是100多个左右Grid格子大小,玩家只能看到他周围的九个格子和自身格子。

注意是每个格子都有个列表,玩家进入时插入,离开时删除。

当玩家发生切换格子的行为,那么势必会导致一些玩家看不到。

服务器进入和离开的人物数据需要发给客户端,对于玩家进入发actor enter消息,对于玩家离开发actor leave,发送玩家的一些数据包,这样人物模型就会在这个客户端重新创建出来。

2.十字链表方式

十字链表就是有两个排序链表,就是XY轴,一个是所有玩家X坐标排序链表,另外一个就是所有玩家Y轴的排序链表,每个玩家同时存在这两个链表中。

针对于大地图,人比较少的时候会采用十字链表方案。

3.两种方案简单对比

九宫格占用内存大,因为每个格子都有一个兴趣列表,大地图格子多,人很少的情况下就会造成大量的浪费。 十字链表占用内存少,有多少人,链表就多长。

补充九宫格边界存在问题:

边界对象处理问题:
当游戏角色或物体处于九宫格的边界上时,可能会同时属于多个AOI区域,这可能导致对象被重复处理或渲染,增加计算负担。
在某些情况下,物体可能会在不同的九宫格区域间快速切换,导致不连续的更新或视觉闪烁。

通信开销增加:
如果玩家或物体位于九宫格边界,服务器可能需要向多个区域的玩家广播该物体的状态,导致不必要的通信开销。
边界上的频繁移动可能会导致服务器和客户端之间的数据频繁交换,从而影响性能。

视野切换的滞后或不连续:
当玩家移动到九宫格的边界时,可能会出现视野切换不连续或滞后的情况,导致玩家体验不佳。
如果AOI算法没有处理好九宫格边界的更新,可能会导致玩家在边界处看到未更新或丢失的物体。

边界效应:
在九宫格边界区域,物体的状态更新可能会出现不一致的情况。例如,一个物体在一个格子中被认为是可见的,但在相邻格子中由于延迟或计算误差被认为不可见。

负载不均衡:
如果多个玩家集中在九宫格的某个边界区域,可能会导致该区域的服务器负载显著增加,而其他区域的服务器负载较轻,造成负载不均衡的问题。
这些问题通常需要通过优化AOI算法或调整服务器架构来解决,例如通过更细粒度的AOI划分、边界平滑处理、或者动态调整九宫格的大小来减少这些缺陷的影响。

我们游戏采用九宫灯塔实现视野管理并做了一些优化(增加兴趣列表)

1.先了解一些九宫灯塔概念

摘自: https://www.cnblogs.com/silvermagic/p/9373414.html

1.灯塔AOI坐标系

为什么需要灯塔AOI?假设我们想知道某点周围10格内有哪些对象,在没有灯塔AOI的情况下,我们需要遍历所有的对象计算其是否在范围内,随着地图内的对象越来越多,查找的效率也会越来越差,所以我们需要一种方法来过滤那些明显不需要参与计算的对象,所以我们将地图分割成一个个区域,在其中心放置一个假想的"灯塔",每个"灯塔"都会保存区域内的对象,这样当我们需要知道某点周围10格内有哪些对象时,我们只需要计算出范围内有哪些"灯塔",然后获取这些"灯塔"保存的对象列表,针对这些对象进行计算就能节省大量计算。为了方便表示和管理这些"灯塔",我们为其分配了新坐标(左下为(0,0)),这个新的坐标系即灯塔AOI坐标系(这个坐标系是用来做碰撞检测的)

2.灯塔视野和玩家视野

"灯塔"的视野越小,在碰撞检测时能过滤的无效对象就越多,但是整张地图"灯塔"的数目也就越多,消耗的内存就越大,而且对象进出灯塔的计算量就越多
"灯塔"的视野越大,在碰撞检测时能过滤的无效对象就越少,碰撞检测的计算量就越大,想象下只有一个"灯塔"的情况,即退回了没有灯塔AOI系统的情况
由于玩家的视野一般是固定的(屏幕显示区域大小一般固定),所以灯塔视野大小一般是玩家视野的1/2或1/3比较合适。

3.灯塔AOI逻辑

对象进入(角色登入、生成怪物):
根据对象坐标计算对象所属灯塔,将对象添加到灯塔的对象列表,如果灯塔上绑定了观察者,则通知观察者有对象进入
找出对象视野范围的灯塔,将自身绑定为其观察者,绑定灯塔会将自身现有对象列表发送给对象
对象离开(角色登出、怪物被杀死):
根据对象坐标计算对象所属灯塔,将对象从灯塔的对象列表中移除,如果灯塔上绑定了观察者,则通知观察者有对象离开
找出对象视野范围的灯塔,解除其观察者绑定,解除绑定灯塔会将自身现有对象列表发送给对象
对象移动
如果对象所属灯塔没变,则不做任何操作
如果对象所属灯塔改变,则对旧灯塔执行对象离开逻辑,对新灯塔执行对象进入逻辑,但是要注意的是对视野的处理,前后视野交集内的灯塔不需要执行解绑和绑定操作

2.我们游戏实现

AOI增加兴趣列表,每次广播时不再遍历九宫格,而是直接遍历兴趣列表。这么做的好处是,原本有单位进入或者离开,那么需要遍历
该单位为中心的九宫格内的灯塔里(单位组和玩家组)所有对象告诉他们进入或者离开了,每次都遍历九宫格太耗了。而增加了兴趣列表,
可以直接遍历当前单位身上的兴趣列表就可以直接通知到其他单位。兴趣列表是以玩家为中心的九宫格内所有感兴趣的单位,装进兴趣列表,
每个单位身上维护着一个自己的兴趣列表。
但是兴趣列表会增加额外的内存开销(现在虚拟机一般内存足够大了,可以接受),这是典型的利用空间换时间。
NPC的AI策略调整为当九宫内有玩家才激活AI,离开一段时间后再关闭。

例如:假设当前玩家坐标为(3,3),然后九宫格每个格子大小为1 * 1,地图大小为 4 * 5。

那么玩家所处九宫格位置就为 3 / 1 = 3, 3 / 1 = 3, 所以格子位置就为(3,3)所在的位置。

对应到我们游戏处理,就是Tower[3][3]所处的灯塔。

1.场景初始化AOI模块(伪代码)

public void init(int width,int height)
{
    ...
    ...
    //场景最大的可移动尺寸
    this.width = width;
    this.height = height;

    int tWidth;
    int tHeight;
    AbstractBPScene scene = getScene();
    if (scene != null && scene.getDictSceneDefine() != null)
    {
        DictSceneDefineData dictSceneDefine = scene.getDictSceneDefine();
        towerSize = dictSceneDefine.getTowerSize();
        if (towerSize < 1000 || towerSize > 10000)
        {
            towerSize = GameConstant.sceneTowerSize;
        }
    }
    else
    {
        towerSize = GameConstant.sceneTowerSize;
    }

    //多少个灯塔(场景最大尺寸 / 每个九宫格子尺寸 = 多少个)
    //towerSize 灯塔的像素长度(相当于一个九宫格子所在大小,默认为2500像素也就是25米)
    tWidth = (int) Math.ceil((double) width / towerSize);
    tHeight = (int) Math.ceil((double) height / towerSize);

    //灯塔组初始化长度(场景所有的九宫格)
    dic = new Tower[tWidth][tHeight];
    marks = new boolean[tWidth][tHeight];

    //数组是从0开始的,所以长度减一个,防止放入数组时越界
    tXMax = tWidth - 1;
    tYMax = tHeight - 1;
    ...
    ...
}

初始化主要是根据场景的长和宽将场景分成一个一个格子大小(这里的格子是只每个九宫格大小,默认为2500像素也就是25米,而不是grid网格大小)。

每个九宫格坐标位置会用二维数组来确定Tower[][],这里初始化了二维数组的长度Tower[tWidth][tHeight]。

九宫格位置处于第几个格子最大长度减1,因为数组是从0开始的,长度减一个,防止放入数组时越界。

2.单位进入

/** 单位进入 */
public void unitEnter(BPObject unit,int createType)
{
    ObjectTypeEnum unitObjecttype = unit.getObjectType();
    if(openCheck)
    {
        if(unitCountDic.adjustOrPutValue(unit.getObjectID(),1,1)!=1)
        {
            BPLog.BP_SCENE.error("单位重复添加",unit.getObjectID(), unitObjecttype);
        }
    }

    //所处第几个灯塔(玩家当前坐标 / 每个九宫格灯塔尺寸大小 = 所处第几个灯塔)
    int tx = unit.getX() / towerSize;
    int ty = unit.getZ() / towerSize;

    //判断是否超出边界格子,容错处理
    if (tx < 0)
    {
        tx = 0;
    }
    if (tx > tXMax)
    {
        tx = tXMax;
    }

    if (ty < 0)
    {
        ty = 0;
    }
    if (ty > tYMax)
    {
        ty = tYMax;
    }

    //设置当前生物位于哪个格子
    unit.setTowerPos(tx,ty);
    
    Tower st;
    
    //以当前生物中心,计算左边,右边,上边,下边的格子
    int left = tx - 1;
    if (left < 0)
    {
        left = 0;
    }

    int right = tx + 1;
    if (right > tXMax)
    {
        right = tXMax;
    }

    int up = ty - 1;
    if (up < 0)
    {
        up = 0;
    }

    int down = ty + 1;
    if (down > tYMax)
    {
        down = tYMax;
    }

    Tower[][] tempDic = dic;

    //遍历九宫格
    for (int dx = left; dx <= right; ++dx)
    {
        for (int dy = up; dy <= down; ++dy)
        {
            st = tempDic[dx][dy];

            //遍历到的当前格子已经放置了灯塔
            if (st != null)
            {
                    //当前灯塔内单位组不为空
                    if(!st.getSet().isEmpty())
                    {
                        //获取单位组
                        BPObject[] keys = st.getSet().getKeys();
                        BPObject peer;

                        //如果进入单位是玩家,迭代单位组,单位组有怪物需要激活怪物
                        for (int i = keys.length - 1; i >= 0; --i)
                        {
                            if ((peer = keys[i]) != null)
                            {
                                if (unitObjecttype == ObjectTypeEnum.ACTOR)
                                {
                                    if (peer.getObjectType() == ObjectTypeEnum.NPC)
                                    {
                                        peer.activeMonster();
                                    }
                                }

                                //进入单位和迭代的单位互相加入到各自兴趣列表
                                //每个生物身上各自维护一个兴趣列表
                                addInterestEachOther(unit, peer);
                            }
                        }
                    }

                //当前进入单位是npc,迭代单位组里有玩家,npc激活自身
                if (unitObjecttype == ObjectTypeEnum.NPC)
                {
                    //有角色
                    if(!st.getPlayerSet().isEmpty())
                    {
                        unit.activeMonster();
                    }
                }
            }
        }
    }


    //先判断当前进入单位格子内是否放置了灯塔
    st = dic[tx][ty];

    //没有新new一个灯塔放入
    if (st == null)
    {
        st = new Tower();
        dic[tx][ty] = st;
    }
    
    ...
    ...

    AbstractHuman master = unit.getMasterActor();

    //当前进入的生物是需要广播类型,那么设置广播为true
    if (master != null && master.getRadioReady())
    {
        unit.toSetRadioReady(true);
    }

    //forEachIndex维护的一个索引,防止在迭代周围九宫格的时候又有新单位进入。
    //每次跌代单位组或兴趣列表时,forEachIndex先自增,迭代完自减。
    if (forEachIndex == 0)
    {
        toUnitEnter(unit,st,createType);
    }
    else
    {
        //如果forEachIndex不等于0正在迭代单位组或兴趣列表,那么将要加入的单位先放到临时列表里
        //等迭代完forEachIndex == 0时,如果发现tempAddDic有待加入对象,那么重新调用toUnitEnter将单位加入
        tempAddDic.put(unit,createType);
    }
    ...
    ...
}

单位进入:

1.根据当前生物所处坐标计算所在格子位置,并计算周围格子。

2.以当前进入单位为中心遍历九宫格(每个九宫格放置了一个灯塔,也可以理解为遍历九宫灯塔)

3.迭代每个九宫格内都放置了灯塔,灯塔里包含所有处于当前九宫格子内单位,迭代单位组。

4.进入的单位是玩家或怪物需要根据迭代到的单位做相应处理,例如:玩家进入,迭代到怪物,激活怪物。

5.进入单位和迭代的单位(单位为中心的九宫格)互相加入到各自兴趣列表,每个生物身上各自维护一个兴趣列表。

6.判断当前进入单位格子内是否放置了灯塔,没有新new一个灯塔放入。

7.当前进入的生物是需要广播类型,那么设置广播为true

8.forEachIndex维护的一个索引,防止在迭代周围九宫格的时候又有新单位进入。

每次跌代单位组或兴趣列表时,forEachIndex先自增,迭代完自减。(特别注意!!!)

9.如果forEachIndex不等于0正在迭代单位组或兴趣列表,那么将要加入的单位先放到临时列表tempAddDic里。

等迭代完forEachIndex == 0时,如果发现tempAddDic有待加入对象,那么重新调用toUnitEnter将单位加入。

private void toUnitEnter(BPObject unit,Tower st,int createType)
{
    if(openCheck)
    {
        if(units.contains(unit))
        {
            throwError("单位重复添加:"+unit.getObjectID());
            return;
        }
    }
    
    //AOI内所有单位加入Set<BPObject>,指九宫格内所有单位
    units.add(unit);

    //将进入的单位加到自身所处灯塔的单位组SSet<BPObject>里
    st.getSet().add(unit);
    
    if(unit.isPlayerUnit())
    {
        //如果是玩家需要额外再加入自身所处灯塔角色单位组SSet<BPObject>里
        st.getPlayerSet().add(unit);
    }
    
    if(unit.isRadioAll())
    {
        //加入到被可见组
        radioAllDic.add(unit);
    }

    //给周围玩家推送创建,当前单位进入了
    sendCreate(unit,createType);
}

AOI内所有单位加入Set,指九宫格内所有单位,可以直接contains判断是否重复添加。

将进入的单位加到自身所处灯塔的单位组SSet里。

如果是玩家需要额外再加入自身所处灯塔角色单位组SSet里。

加入到被可见组。

给周围玩家推送创建,当前单位进入了。

3.执行视野更新(玩家移动就会判断是否执行视野更新)

public void unitUpdate(BPObject unit,int x,int y)
{
    //当前y坐标其实传入的是z坐标
    if(openCheck)
    {
        if(!units.contains(unit))
        {
            throwError("单位不存在:"+unit.getObjectID());
            return;
        }
    }

    //获取当前坐标所在的灯塔位置(第几个)(九宫格内,每个格子有一个灯塔)
    int tx = x / towerSize;
    int ty = y / towerSize;

    if (tx < 0)
    {
        tx = 0;
    }
    if (tx > tXMax)
    {
        tx = tXMax;
    }

    if (ty < 0)
    {
        ty = 0;
    }
    if (ty > tYMax)
    {
        ty = tYMax;
    }

    //原先所在的灯塔位置(九宫格内,每个格子有一个灯塔)
    int otx = unit.getTowerX();
    int oty = unit.getTowerY();

    //需要(现在所处的灯塔和之前所处的灯塔位置不一样,执行视野更新)
    if (tx != otx || ty != oty)
    {
        toUnitUpdate(unit,otx,oty,tx,ty);
    }
}

判断当前生物所处的格子和之前记录所处的格子是否不同,不同说明格子移动了,需要执行视野更新。

/** 执行单位视野更新 */
private void toUnitUpdate(BPObject unit,int otx,int oty,int tx,int ty)
{
    ...
    ...
    //广播序号,迭代单位组可以根据广播序号判断是否自身
    int index = ++radioIndex;

    //屏蔽自身,联动主角
    unit.setRadioIndex(index);

    //额外推送组也需要设置同一个广播序号
    //额外推送组,是指两个单位强制绑定,那么需要互相添加到各自的额外推送组
    if(!unit.getRadioExSet().isEmpty())
    ...
    ...

    //当前执行视野更新单位是否玩家
    boolean isPlayerUnit = unit.isPlayerUnit();

    Tower[][] tempDic = dic;
    Tower st;
    
    //更新单位所处的新的九宫格
    int left = tx - 1;
    if (left < 0)
    {
        left = 0;
    }

    int right = tx + 1;
    if (right > tXMax)
    {
        right = tXMax;
    }

    int up = ty - 1;
    if (up < 0)
    {
        up = 0;
    }

    int down = ty + 1;
    if (down > tYMax)
    {
        down = tYMax;
    }

    // 标记新的格子所处位置,用一个boolean二维数组标记
    // 在迭代旧的九宫格和新的九宫格重复部分不需要通知
    // 如果迭代旧的九宫格时,当前mks为true说明旧的九宫格和新的九宫格有重复部分
    boolean[][] mks = marks;
    for (int dx = left; dx <= right; ++dx)
    {
        for (int dy = up; dy <= down; ++dy)
        {
            mks[dx][dy] = true;
        }
    }

    //遍历旧(移除)
    int oleft = otx - 1;
    if (oleft < 0)
    {
        oleft = 0;
    }

    int oright = otx + 1;
    if (oright > tXMax)
    {
        oright = tXMax;
    }

    int oup = oty - 1;
    if (oup < 0)
    {
        oup = 0;
    }

    int odown = oty + 1;
    if (odown > tYMax)
    {
        odown = tYMax;
    }

    ClientNetPacket m = null;
    Message message = null;
    
    updateRemoveListCache.resetQuick();
    ...
    ...

    for (int dx = oleft; dx <= oright; ++dx)
    {
        for (int dy = oup; dy <= odown; ++dy)
        {
            //未标记,说明没有重复需要通知
            if (!mks[dx][dy])
            {
                st = tempDic[dx][dy];

                if (st != null)
                {
                    //迭代到的当前灯塔里有单位是玩家
                    if(st.hasPlayerUnit())
                    {
                        //迭代玩家组
                        BPObject[] keys = st.getPlayerSet().getKeys();
                        BPObject obj;

                        for (int i = keys.length - 1; i >= 0; --i)
                        {
                            if ((obj = keys[i]) != null)
                            {
                                //不是自己的单位
                                if (obj.getRadioIndex() != index)
                                {
                                    if (obj.getRadioReady() && !unit.isRadioAll() &&
                                    !unit.isHidden())
                                    {
                                        if (m == null)
                                        {
                                            //创建移除单位协议,告诉当前格子里的玩家,当前单位离开了
                                            message = createRemoveUnitMsg(unit);
                                            m = ClientEncode.assemblyClientNetPacket(PacketIDConst.SCRemoveUnitList, message);
                                        }
                                        
                                        obj.sendPacket(m,message);
                                    }

                                    // 从兴趣列表互相删除可见
                                    removeInterestEachOther(unit, obj);
                                }
                            }
                        }
                    }

                    //如果执行视野更新的单位是玩家
                    if(isPlayerUnit)
                    {
                        //迭代当前灯塔单位组
                        BPObject[] keys = st.getSet().getKeys();
                        BPObject obj;

                        for (int i = keys.length - 1; i >= 0; --i)
                        {
                            if ((obj = keys[i]) != null)
                            {
                                //不是自己的单位
                                if (obj.getRadioIndex() != index)
                                {
                                    // 从兴趣列表移除
                                    removeInterestEachOther(unit, obj);
                                    
                                    if(!obj.isRadioAll())
                                    {
                                        if(!obj.isHidden())
                                        {
                                            //同时还需要告诉自己,我离开了,这些单位就看不见了
                                            updateRemoveListCache.add(obj.getObjectID());
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
            else
            {
                //关闭标记(和旧的灯塔重合部分置为false,下次判断新的灯塔时只需要通知没有重的部分即mks还为true的部分)
                mks[dx][dy] = false;
            }
        }
    }

    //同时还需要告诉自己,我离开了,这些单位就看不见了
    if (isPlayerUnit && !updateRemoveListCache.isEmpty() &&
            unit.getRadioReady())
    {
        // TODO 重构
        //发自己
        BPAOI.SCRemoveUnitList.Builder builder = getScene().getBuilderCache().getRemoveUnitListBuilder();
        builder.clear();
        int size = updateRemoveListCache.size();
        for (int i = 0; i < size; i++)
        {
            builder.addObjectIds(updateRemoveListCache.get(i));
        }
        
        updateRemoveListCache.resetQuick();

        unit.sendPacket(PacketIDConst.SCRemoveUnitList, builder.build());
    }

    
    //同时还需要告诉自己,我离开了,这些单位就看不见了
    if (isPlayerUnit && !updateRemoveListCache.isEmpty() &&
            unit.getRadioReady())
    {
        // TODO 重构
        //发自己
        BPAOI.SCRemoveUnitList.Builder builder = getScene().getBuilderCache().getRemoveUnitListBuilder();
        builder.clear();
        int size = updateRemoveListCache.size();
        for (int i = 0; i < size; i++)
        {
            builder.addObjectIds(updateRemoveListCache.get(i));
        }
        
        updateRemoveListCache.resetQuick();

        unit.sendPacket(PacketIDConst.SCRemoveUnitList, builder.build());
    }

    //执行更新的单位旧的所处格子位置
    st=tempDic[otx][oty];
    //不应该为空
    if (st != null)
    {
        //旧的灯塔单位组合玩家组需做相应的移除操作
        st.getSet().remove(unit);
        
        if(unit.isPlayerUnit())
        {
            st.getPlayerSet().remove(unit);
        }
    }
    
    //执行视野更新的单位设置进入新的格子位置
    unit.setTowerPos(tx,ty);
    //如果当前位置还没放置灯塔,new一个新的灯塔放入
    st=tempDic[tx][ty];
    if (st == null)
    {
        st = new Tower();
        tempDic[tx][ty] = st;
    }
    
    //添加到新的灯塔里的单位组里
    st.getSet().add(unit);
    //如果是玩家额外添加到新的灯塔里的玩家组里
    if(unit.isPlayerUnit())
    {
        st.getPlayerSet().add(unit);
    }
    
    boolean has=false;

    //置空--客户端传输的网络包
    m = null;

    //遍历新
    for (int dx = left; dx <= right; ++dx)
    {
        for (int dy = up; dy <= down; ++dy)
        {
            //还有标记,说明当前的格子是新的格子,新的所处九宫格和旧的九宫格重复部分mks都被设置false了。
            //新的格子才需要通知,新的所处九宫格和旧的九宫格重复部分不用通知,因为灯塔里已经记录了。
            if(mks[dx][dy])
            {
                //当前灯塔
                st = tempDic[dx][dy];

                if (st != null)
                {
                    //有玩家组
                    if(st.hasPlayerUnit())
                    {
                        //迭代玩家组
                        BPObject[] keys = st.getPlayerSet().getKeys();
                        BPObject obj;

                        for (int i = keys.length - 1; i >= 0; --i)
                        {
                            if ((obj = keys[i]) != null)
                            {
                                //不是自己的单位
                                if (obj.getRadioIndex() != index)
                                {
                                    if (obj.getRadioReady() && !unit.isRadioAll() &&
                                    !unit.isHidden())
                                    {
                                        if (m == null)
                                        {
                                            //告诉新的格子里远本的玩家,我这个单位进来了
                                            message=createAddUnitMsg(unit);
                                            m = ClientEncode.assemblyClientNetPacket(PacketIDConst.SCAddUnitList, message);
                                        }
                                        obj.sendPacket(m,message);
                                    }
                                    // 添加到兴趣列表
                                    addInterestEachOther(unit, obj);
                                }
                            }
                        }
                    }

                    //如果执行视野更新的单位是玩家
                    if(isPlayerUnit)
                    {
                        //迭代灯塔里所有单位
                        BPObject[] keys=st.getSet().getKeys();
                        BPObject obj;

                        for (int i = keys.length - 1; i >= 0; --i)
                        {
                            if ((obj = keys[i]) != null)
                            {
                                //激活怪物
                                obj.activeMonster();
                                
                                //不是自己的单位
                                if (obj.getRadioIndex() != index)
                                {
                                    // 添加到兴趣列表
                                    addInterestEachOther(unit, obj);

                                    if(!obj.isRadioAll() && !obj.isHidden())
                                    {
                                        //获取单位数据,添加到待发送列表
                                        Object data = obj.getUnitData(false, 0);
                                        
                                        ...
                                        ...
                                        ...
                                        //有需要发送的列表
                                        has = true;
                                    }
                                }
                            }
                        }
                    }
                }
                
                //关闭标记,不能忘了
                mks[dx][dy] = false;
            }
        }
    }
    
    if(isPlayerUnit && has && unit.getRadioReady())
    {
        // TODO 重构
        BPAOI.SCAddUnitList.Builder builder = getScene().getBuilderCache().getAddUnitListBuilder();
        builder.clear();
        builder.addAllActors(actorList);
        builder.addAllNpcs(npcList);
        builder.addAllPets(petList);
        builder.addAllMirror(mirrorList);
        builder.addAllTraps(trapList);
        builder.addAllOperation(operationList);
        builder.addAllSpecialObject(specialObjectList);
        builder.addAllKnight(knightList);
        unit.sendPacket(PacketIDConst.SCAddUnitList, builder.build());
    }
}

执行视野更新:

1.计算需要更新单位所处的新的九宫格,标记新的格子所处位置,用一个boolean二维数组标记,置为true。

在迭代旧的九宫格和新的九宫格重复部分不需要通知。

如果迭代旧的九宫格时,当前mks为true说明旧的九宫格和新的九宫格有重复部分。

2.计算需要更新单位所处的旧九宫所在位置,迭代旧九宫格并且与新的九宫格没有重复的单位需要通知,

创建移除单位协议,告诉当前格子里的玩家,单位离开了,从兴趣列表互相删除可见。

如果执行视野更新的单位是玩家,迭代灯塔单位组,从兴趣列表移除,同时还需要告诉自己,我离开了,这些单位就看不见了。

3.关闭标记(和旧的灯塔重合部分置为false,下次判断新的灯塔时只需要通知没有重的部分即mks还为true的部分)

4.删除所处旧的灯塔,依据是否玩家,单位组和玩家组需做相应的移除操作。

5.添加到新的灯塔里的单位组里。

6.遍历新的九宫格子并且和旧的九宫格不重复的部分。

7.告诉新的格子里远本的玩家,我这个单位进来了,互相添加到兴趣列表。

8.如果执行视野更新的单位是玩家,迭代灯塔里所有单位,激活怪物…,获取单位数据,添加到待发送列表…

9.关闭mks标记,不能忘了。

总结我们最重要明白,新的九宫格和旧的九宫格重叠部分是不需要通知的:

玩家往左移动了一个格子(不是gird格子,而是九宫格大小的一个格子),那么通知离开的时候,只需告诉旧的九宫格白色圆圈箭头那一列我离开了就行。

同时要通知自己白色的那一列我看不到了。

遍历新的九宫格时,只需要告诉黑色那一列,我进来了就可以。重复的都不用通知。

4.单位离开

/** 单位离开 */
public void unitLeave(BPObject unit)
{
    ...
    ...
    int tx = unit.getTowerX();
    int ty = unit.getTowerY();

    ...
    ...
    Tower st = dic[tx][ty];
    ...
    ...
}
计算所处灯塔位置
private void toUnitLeave(BPObject unit,Tower st)
{
    if(openCheck)
    {
        if(!units.contains(unit))
        {
            throwError("单位不存在:"+unit.getObjectID());
            return;
        }
    }

    BPLog.BP_SCENE.debug("删除单位,{},{}",unit.getObjectType(),unit.getObjectID());
    //先广播删除
    sendDelete(unit);
    
    // 遍历兴趣列表中的对象,让对象将自己删除,同时也清空自己的兴趣列表
    removeInterestAll(unit);

    //从aoi中全部单位中移除
    units.remove(unit);

    //自己所在灯塔里单位组移除
    st.getSet().remove(unit);

    //如果是玩家
    if(unit.isPlayerUnit())
    {
        //自己所在灯塔里玩家组移除
        st.getPlayerSet().remove(unit);

        if(!unit.getRadioReady())
        {
            BPLog.BP_SCENE.warn("未ready就移除的AOI情况");
        }
        
        //关广播准备
        unit.setRadioReady(false);
        ((Actor)unit).getAOIModule().clearScene();
    }
    
    
    if(unit.isRadioAll())
    {
        radioAllDic.remove(unit);
        
        unit.setRadioAll(false);
    }
}

遍历兴趣列表中的对象,让对象将自己删除,同时也清空自己的兴趣列表

从aoi中全部单位中移除

自己所在灯塔里单位组移除,如果是玩家,自己所在灯塔里玩家组移除

5.判断两个单位是否在九宫格内

/** 返回两个单位是否在九宫范围内 */
public boolean isNear(BPObject unit,BPObject target)
{
    return unit.getInterestSet().contains(target);
}

现在有了兴趣列表,可以直接通过兴趣列表contains来判断两个单位是否在九宫范围内,

而不用去遍历九宫格了,大大提高性能。

6.遍历视野内单位(圆形)

也采用的是遍历兴趣列表,而不是九宫格。

7.广播(性能较高的一种方式)

private void toRadioNormalPerformance(short packetID, com.google.protobuf.Message message, AOIRadioType type, BPObject self)
{
    ...
    ...
    SSet<BPObject> interestSet = self.getInterestSet();
    ...
    ...
}

这种采用迭代的兴趣列表的方式,性能会高一些。

Search

    Table of Contents