一种简单的AOI的实现

技术文档网 2021-05-17
简介

游戏中AOI(Area of Interest)通常用于逻辑事件触发,网络数据同步,优化逻辑计算。如触发NPC攻击进入视野玩家,计算手雷爆炸伤害范围等。常规的实现方式有网格十字链表,十字链表的方式实现比较复杂,这里不讨论。我的实现是采用根据查询坐标定位数组下标的方式,这种实现方式比较简单,并且比较方便任意区域的查询.

实现

主要接口有

void insert(objectid_t uid, float x, float y);
void update(objectid_t uid, float x, float y);
//width object视野宽度
//height object视野长度
//Handler:void(id). 查询结果回掉
void query(objectid_t uid, float width, float height, const Handler& callback);
void erase(objectid_t uid);

接口的实现非常简单,主要是增删改查。uid 表示一个AOI实体(玩家 NPC)的唯一ID。最核心的接口是query,许多逻辑操作都要通过这个接口完成。

使用方式

进出视野主要是通过比较两次查询结果的差异来计算的。如 第一次查询得到集合 A,第二次查询得到集合 B, 比较两个集合: 一个对象B包含而A没有包含,就是进视野;一个对象B没有包含而A包含,就是出视野


struct aoi_object_t
{
    float x;
    float y;
    int32_t handle;
};

using aoi_t = aoi< aoi_object_t>;

struct player
{
    //uid-version, version用于优化差异比较计算,通过2次遍历即可得出进出视野结果
    using entity_view_t = std::unordered_map<int32_t, int32_t>;
    int32_t version = 1;
    float view_w = 20.0f;//视野长宽
    float view_h = 20.0f;
    entity_view_t view;
};

void print_view_event(int32_t uid, const player& p)
{
    for (auto& v : p.view)
    {
        if (v.second == p.version)
        {
            //进入视野
            std::cout << "player " << v.first << " enter " << uid << "view" << std::endl;
        }
        else if (v.second != (p.version - 1))//出视野
        {
            std::cout << "player " << v.first << " leave " << uid << "view" << std::endl;
            //注意这里要从 p.view 删除已经出视野的玩家
        }
    }
}

void example()
{
    float length_of_area = 512.0f;
    aoi_t::rect rc(-length_of_area / 2.f, -length_of_area / 2.f, length_of_area, length_of_area);
    aoi_t aoi_(rc.x,rc.y, length_of_area,8);

    std::unordered_map<int32_t, player> players;

    auto insert = [&](int32_t uid, float x, float y) {
        aoi_.insert(uid, x, y);
        players.try_emplace(uid, player());
    };

    insert(1, 10.0f, 25.0f);
    insert(2, 10.0f, 20.0f);
    insert(3, 10.0f, 200.0f);
    insert(4, 100.0f, 200.0f);

    auto update = [&]()
    {
        for (auto& p : players)
        {
            p.second.version += 2;
            aoi_.query(p.first, p.second.view_w, p.second.view_h, [&p](int32_t id) {
                if (id == p.first)
                {
                    return;
                }
                auto res = p.second.view.try_emplace(id, p.second.version);//新进入玩家
                if (!res.second)//玩家已经在视野内
                {
                    res.first->second = p.second.version - 1;//更新已经在视野内玩家的版本号,比当前版本号小1
                }
            });
            print_view_event(p.first, p.second);//第一次遍历打印结果
        }
    };

    update();// 1 -><- 2;
    aoi_.update(3, 20, 35);
    update();// 1 -><- 3;
    aoi_.update(3, 25, 100);
    update();// 1 <--> 3; //离开
}
用法优化
上面根据版本号**不相等**判断出视野,可能会造成AOI边缘处的玩家频繁进出视野,可以采用如下办法优化:
根据玩家的速度动态设置一个版本号的差值范围, 根据这个差值判断出视野。
性能测试

下面是不同参数性能测试:

地图最小分割单元8,每次进行N次update,N次query, N = 实体数量

1.玩家较慢移动(speed = 2),单次移动大概率不会跨越地图最小分割单元

case 地图宽度 地图长度 实体数量 实体AOI宽 实体AOI长
1 512 512 1000 10 10
2 512 512 1000 50 50
3 512 512 1000 100 100
4 512 512 2000 10 10
5 512 512 2000 50 50
6 512 512 2000 100 100

结果:

case insert update query
1 167s 46us 70us
2 168us 48us 342us
3 168us 52us 988us
4 404us 110us 211us
5 419us 110us 1097us
6 393us 111us 2965us

2.玩家较快移动(speed = 10),单次移动大概率会跨越地图最小分割单元

case 地图宽度 地图长度 实体数量 实体AOI宽 实体AOI长
1 512 512 1000 10 10
2 512 512 1000 50 50
3 512 512 1000 100 100
4 512 512 2000 10 10
5 512 512 2000 50 50
6 512 512 2000 100 100

结果:

case insert update query
1 165us 82us 77us
2 169us 87us 368us
3 165us 90us 968us
4 365us 184us 209us
5 358us 183us 1071us
6 361us 176us 2909us

可见查询的性能主要受实体数量,和AOI视野范围影响;更新的新能主要受实体数量,和是否频繁跨域单元格影响。并且基本上是线性关系。可以多次测试调整地图最小分割单元,来满足自己情况。

相关文章

  1. 硅谷互联网公司的开发流程

    开发流程包括这么几个阶段: OKR 的设立; 主项目及其子项目的确立; 每个子项目的生命周期; 主项目的生命周期; 收尾、维护、复盘。 第一点,OKR 的设立 所有项目的起始,都应该从 Ro

  2. RESTful-表述性状态转移风格

    REST英文全拼:Representational State Transfer 面向资源编程 资源指的就是一类数据 产品表-&gt;就是产品资源 最重要的是如何表示一个资源 地址即

  3. 稳定性思考

    产品功能线 0-1: 当系统从无到有的时候,首要考虑的是研发效率,功能快速迭代,满足快速增长的业务需求 1-10 系统已经搭建起来,此时考虑的是系统的稳定性。 可用性:1.隔离:区分出核心和非核心功能

  4. Supervisor守护队列发邮件

    安装 CentOS: yum -y install supervisor Debien/Ubuntu适用:apt-get install supervisor 配置 修改主配置文件:vim /et

  5. 安装libsodium,让服务器支持chacha20等加密方式

    用chacha20加密方式需要安装libsodium 注意:libsodium从1.0.15开始就废弃了aes-128-ctr yum install wget m2crypto git libsod

随机推荐

  1. 硅谷互联网公司的开发流程

    开发流程包括这么几个阶段: OKR 的设立; 主项目及其子项目的确立; 每个子项目的生命周期; 主项目的生命周期; 收尾、维护、复盘。 第一点,OKR 的设立 所有项目的起始,都应该从 Ro

  2. RESTful-表述性状态转移风格

    REST英文全拼:Representational State Transfer 面向资源编程 资源指的就是一类数据 产品表-&gt;就是产品资源 最重要的是如何表示一个资源 地址即

  3. 稳定性思考

    产品功能线 0-1: 当系统从无到有的时候,首要考虑的是研发效率,功能快速迭代,满足快速增长的业务需求 1-10 系统已经搭建起来,此时考虑的是系统的稳定性。 可用性:1.隔离:区分出核心和非核心功能

  4. Supervisor守护队列发邮件

    安装 CentOS: yum -y install supervisor Debien/Ubuntu适用:apt-get install supervisor 配置 修改主配置文件:vim /et

  5. 安装libsodium,让服务器支持chacha20等加密方式

    用chacha20加密方式需要安装libsodium 注意:libsodium从1.0.15开始就废弃了aes-128-ctr yum install wget m2crypto git libsod