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
将进入的单位加到自身所处灯塔的单位组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();
...
...
}
这种采用迭代的兴趣列表的方式,性能会高一些。