经过一段时间的设计与完善,我们游戏的AI服务器已经达到了基本的性能要求,目前单个AI进程可同时运行4000+个频繁的AI对象。

在前面一篇博客中已经提到过,AI服务器的主逻辑循环是单线程的,这个线程上运行了数千个用户级线程,每个用户级线程运行一个AI对象。AI对象被激活之后就会运行一段lua脚本,以实现AI逻辑.

之所以采用用户级线程(windows下是fiber,linux下使用ucontext)的方案,是因为AI的实现使用了大量的远程调用,如果使用同步调用势必导致主线程的阻塞,从而影响AI服务器的性能。采用异步调用又导致了逻辑的过分复杂。而用户级线程正好解决了这些问题,向上提供了一个同步调用的接口,又不会导致主线程的阻塞(当一个用户级线程处于等待结果的状态下,调度器可以选择另一个用户级线程来运行)。

AI服务器的主要构件是用户级线程调度器,和一个用户级线程池,服务器启动后会产生一组用户级线程序,并且在每个线程上创建一个lua虚拟机。

基本的设计思路已经介绍完毕,下面介绍各个主要的组成部分:

首先是主循环:

void CAIApp::Process()
{psarmor l_pa(*this);Scheduler::Init();while(!GetExitTaskFlag() && l_pa(psobj::realtime)){//如果到game的连接断开,执行错误处理并尝试重连while(!m_flag2Game){//连接断了,要清除所有已经绑定的Ai对象//g_AiObjMap为空的话不可能有任务在运行if(!g_AiObjMap.empty()){    //连接已经断开,停掉所有运行的AI
                {std::map<uLong,rptr<AiAvatar> >::iterator it =  g_AiObjMap.begin();std::map<uLong,rptr<AiAvatar> >::iterator end =  g_AiObjMap.end();for( ; it != end; ++it)it->second->StopAi();}//清理active列表
                Scheduler::ClearActiveList();//清理timeout列表
                Scheduler::ClearTimeOut();{std::cout << "到gameserver的连接断开,清除所有绑定对象" << std::endl;std::map<uLong,rptr<AiAvatar> >::iterator it =  g_AiObjMap.begin();std::map<uLong,rptr<AiAvatar> >::iterator end =  g_AiObjMap.end();for( ; it != end; ++it)it->second = 0;g_AiObjMap.clear();}//清理aigroup
                {std::map<long,rptr<AiGroup> >::iterator it =  g_GroupMap.begin();std::map<long,rptr<AiGroup> >::iterator end =  g_GroupMap.end();for( ; it != end; ++it)it->second = 0;g_GroupMap.clear();}}m_pToGame = 0;while(m_pToGame._nil()){rptr<DataSocket> l_sock    =g_aiapp->Connect(g_aiapp->m_config.m_gameip,g_aiapp->m_config.m_gameport);if(l_sock._nil()){std::cout << "连接game失败!5秒后重试..." << std::endl;}else{printf("连接game成功...");WPacket    l_wpk    =g_aiapp->GetWPacket();l_wpk.WriteCmd(CMD_AM_AILOGIN);l_wpk.WriteShort(g_aiapp->m_config.m_mapcount);for( int i = 0;i < g_aiapp->m_config.m_mapcount; ++i){l_wpk.WriteString(g_aiapp->m_config.m_names[i].c_str());}l_sock->SendData(l_wpk);m_pToGame = l_sock;m_flag2Game = true;break;}Sleep(5000);}}Scheduler::Schedule();PeekPacket(50);}Scheduler::Destroy();
}

上面代码的主要作用就是尝试连接gameserver,如果连接成功就在循环中调用调度器的调度函数以选择合适的用户级线程运行。PeekPacket(50);会从网络层提取网络包,如果没有网络包则会休眠最多50毫秒.

下面在来看看调度器:

void Scheduler::Schedule()
{//将所有等待添加到m_activeList中的纤程都添加进去
    {for(unsigned int i = 0; i < pending_index; ++i){uthread *ut = m_uthreads[m_pendingAdd[i]];ut->SetNext(0);if(m_active_tail){m_active_tail->SetNext(ut);m_active_tail = ut;}else{m_active_head = m_active_tail = ut;}}pending_index = 0;}uthread *cur = m_active_head;uthread *pre = NULL;while(cur){g_aiapp->PeekPacket(0);m_curuid = cur->GetUid();SwitchToFiber(cur->GetUContext());m_curuid = -1;unsigned char status = cur->GetStatus();//当纤程处于以下状态时需要从可运行队列中移除if(status == DEAD || status == SLEEP || status == WAIT4EVENT || status == UNACTIVED || status == YIELD){//删除首元素if(cur == m_active_head){//同时也是尾元素if(cur == m_active_tail)m_active_head = m_active_tail = NULL;elsem_active_head = cur->Next();}else if(cur == m_active_tail){pre->SetNext(NULL);m_active_tail = pre;}elsepre->SetNext(cur->Next());uthread *tmp = cur;cur = cur->Next();tmp->SetNext(0);//如果仅仅是让出处理器,需要重新投入到可运行队列中if(status == YIELD)Add2Active(tmp);}else{pre = cur;cur = cur->Next();}}//看看有没有timeout的纤程
    {uLong now = dbc::GetTickCount();while(m_timeoutlist.Min() !=0 && m_timeoutlist.Min() <= now){st_timeout *timeout = m_timeoutlist.PopMin();if(timeout->ut->GetStatus() == WAIT4EVENT || timeout->ut->GetStatus() == SLEEP){timeout->ut->wakeuptick = timeout->_timeout;Add2Active(timeout->ut);}}}
}

调度器首先将重新处于激活态的线程投入到运行队列中,然后遍历可运行队列,运行其中的线程,调度器的最后将处理所有处于休眠状态的线程,如果线程的休眠时间到了,则将线程重新投入到可运行队列中。在这里使用了一个极小堆来处理超时。

从上面的代码可以看出,当调度器挑选了一个线程运行之后,代码路径就跳转到线程中,当线程需要阻塞时,就会设置一个状态(YIELD, WAIT4EVENT或SLEEP)并将运行权又重新交回给调度器,当调度器重新获得运行权后,代码会从SwitchToFiber(cur->GetUContext());中返回,调度器需要根据上次运行的线程的状态,或者将线程投入休眠队列(SLEEP),或者重新将线程投入到队列的末尾(YIELD)或者从运行队列中删除(WAIT4EVENT).

下面再看看一个同步调用的例子:

以移动为例,假设AI请求移动到某给位置,则需要向gameserver发送移动请求,直到到达目标点,或者发现移动失败才会从调用中返回:

int AiAvatar::Move(Point3D &pt,short cntx,uLong ms)
{class PosBlock : public BlockStruct{    public:PosBlock(Point3D &pos,AiAvatar *ava):m_ava(ava),m_targetpos(pos){}//返回true则纤程从阻塞中恢复bool WakeUp(){//到达了请求点,恢复if(m_targetpos.x == m_ava->GetPos().x &&m_targetpos.y == m_ava->GetPos().y ){return true;}return false;}private:Point3D m_targetpos;AiAvatar *m_ava;};//printf("开始移动/n");//向GameServer发送移动请求WPacket    l_wpk = g_aiapp->GetWPacket();l_wpk.WriteCmd(CMD_AM_BEGMOV);l_wpk.WriteLong(pt.x);l_wpk.WriteLong(pt.y);l_wpk.WriteLong(pt.z);l_wpk.WriteShort(cntx);Send2Game(this,l_wpk);//阻塞所在fiber直到pos到达要求的值/或者收到移动失败消息/或则AI被请求停止PosBlock pb(pt,this);Scheduler::Block(&pb,ms);//接到了停止AI的命令if(!isAiRunning())return -1;bool ret = (pt.x == m_pos.x && pt.y == m_pos.y);return ret ? 1:0;
}

函数首先创建了一个阻塞条件的结构,然后阻塞在这个条件上,在这里是判断AI对象是否到达了目标点。然后将移动请求发送出去并阻塞在条件上。当gameserver把对象移动到正确的点之后,会把对象的坐标通过网络同步到AI服务器,处理网络包的时候发现那个对象对应的线程正被阻塞,就会调用阻塞条件的WakeUp函数尝试唤醒线程,此时如果条件满足,WakeUp就会返回true,线程被重新投入到可运行队列中,否则线程就会继续被阻塞。

最用来看一段AI脚本,当一个AI对象被激活(进入玩家的视野),就会为这个对象分配一个线程,这个线程就会马上运行与这个对象相关的lua入口函数:

function monster_routine(this)        --出生点local start_pos = {}start_pos.x,start_pos.y,start_pos.z = getbegpos(this)local c = 1--巡逻点local points = {{x=start_pos.x+300,y=start_pos.y,z=start_pos.z},{x=start_pos.x,y=start_pos.y,z=start_pos.z}}--生成状态机stateMachine = AiStateMachine:new()stateMachine.owner = this--初始化tracestateMachine.state_trace = trace:new():init(this,stateMachine,start_pos)--stateMachine.state_trace:init(this,stateMachine,start_pos)--初始化partolstateMachine.state_partol = partol:new():init(this,stateMachine,start_pos,points)--stateMachine.state_partol:init(this,stateMachine,start_pos,points)--初始化attackstateMachine.state_attack = attack:new():init(this,stateMachine)--stateMachine.state_attack:init(this,stateMachine)--初始化gobackstateMachine.state_goback = goback:new():init(this,stateMachine,start_pos)--stateMachine.state_goback:init(this,stateMachine,start_pos)--初始化helpstateMachine.state_help = help:new():init(this,stateMachine)--stateMachine.state_help:init(this,stateMachine)stateMachine.cur_state = stateMachine.state_partolwhile isAiRunning(this) == true doif isdead(this) == true thensc_yield()else    stateMachine.cur_pos.x,stateMachine.cur_pos.y,stateMachine.cur_pos.z = getpos(this)stateMachine.target = get_target(this)if stateMachine.target == nil thenstateMachine.target = select_target(this)end--查看是否有消息要处理local senderlocal recverlocal msglocal sendticksender,recver,msg,sendtick = PopMsg(this)if sender ~= nil thenprint("消息队列非空")if msg == "help" then--如果自己没有目标才处理帮助请求if stateMachine.target == nil thenstateMachine.target = senderstateMachine.cur_state = stateMachine.state_helpendendendlocal ret = 0ret,stateMachine.cur_state = stateMachine.cur_state:execute()    if ret == -1 thenreturnendsc_yield()endend
end

AI主入口函数首先创建了一个状态机,并选择一个初始状态运行。下面再看看追击状态的处理:

trace = {
owner = 0,
StateMachine = 0,
start_pos = 0
}function trace:init(owner,statemachine,start_pos)self.owner = ownerself.StateMachine = statemachineself.start_pos = start_posreturn self
end --追击
function trace:execute()if self.StateMachine.target == nil then--判断离出生点的距离,太远了就回出生点local dis2begpos = calDistance(self.start_pos.x,self.start_pos.y,self.StateMachine.cur_pos.x,self.StateMachine.cur_pos.y)if dis2begpos >= 500 thenreturn 0,self.StateMachine.state_gobackelse--没有目标,巡逻return 0,self.StateMachine.state_partolendelselocal dis2begpos = calDistance(self.start_pos.x,self.start_pos.y,self.StateMachine.cur_pos.x,self.StateMachine.cur_pos.y)if dis2begpos >= 4000 thenreturn 0,self.StateMachine.state_gobackelse--取得目标当前点            local target_pos ={}target_pos.x,target_pos.y,target_pos.z = getpos(self.StateMachine.target)local dis = calDistance(self.StateMachine.cur_pos.x,self.StateMachine.cur_pos.y,target_pos.x,target_pos.y)    if dis <= 200 then--print("选择攻击点")--选择攻击点--if cur_pos.x ~= self.cur_pos and cur_pos.y ~= self.cur_pos.y thenlocal d_x,d_y = gen_pos_circle(target_pos.x,target_pos.y,200)if -1 == mov(self.owner,d_x,d_y,target_pos.z,804,1000) thenreturn -1,nilend--面向目标turnface(self.owner,self.StateMachine.target)--切换到攻击态--endreturn 0,self.StateMachine.state_attackelse--在目标半径2米内随机选择一个点,作为目标点local d_x,d_y = gen_pos_line(self.StateMachine.cur_pos.x,self.StateMachine.cur_pos.y,target_pos.x,target_pos.y,200,100)if dis <= 300 then--离目标点小于3米直接过去if -1 == mov(self.owner,d_x,d_y,target_pos.z,804,1000) thenreturn -1,nilendelse                            local ttx,tty = forword(self.owner,d_x,d_y,300)if -1 == mov(self.owner,ttx,tty,target_pos.z,804,1000) thenreturn -1,nilendendendendendreturn 0,self.StateMachine.state_trace
endfunction trace:new(o)o = o or {}   setmetatable(o, self)self.__index = selfreturn o
end

在追击状态下,根据各种条件或者执行追击,或者返回下一个状态.

AI服务器的设计与实现相关推荐

  1. 访中科曙光智能计算技术总监许涛:重新认识面向未来的AI服务器和云计算中心...

    雷锋网消息,2018年11月20日,NVIDIA在苏州召开了GTC China 2018大会.与会者对这场技术大会的期待不仅在于NVIDIA本身,对于其产品和技术在国内的具体应用也倍加关注. 在本次大 ...

  2. 浪潮信息的ai服务器有何不同,浪潮信息新品AI服务器 可支持最新NVIDIA A100 PCIe Gen4...

    浪潮在技术研发层面从不落于人后,所研发的新品AI服务器,无论是实用性还是功能性都有完善表现,受到了业内外的关注.浪潮新品AI服务器支持最新NVIDIA® Ampere架构A100 PCIe Gen4的 ...

  3. MLPerf基准测试再发榜,浪潮AI服务器刷新18项纪录

    近日,全球倍受瞩目的权威AI基准测试MLPerf公布今年的推理测试榜单,其中浪潮AI服务器NF5488A5一举创造18项性能纪录,在数据中心AI推理性能上遥遥领先其他厂商产品. MLPerf是当前全球 ...

  4. 重新定义 AI 服务器架构

    作者 | 琥珀 出品 | AI科技大本营(公众号ID:rgznai100) 得益于迅速增长的计算能力.海量数据,以及神经网络前所未有的突破,AI 变得无处不在,也成为未来十年最具颠覆性的技术.根据 G ...

  5. 中国AI服务器,刷新全球18项性能基准测试纪录

    允中 发自 凹非寺 量子位 编辑 | 公众号 QbitAI 美国东部时间10月21日,全球倍受瞩目的权威AI基准测试MLPerf公布今年的推理测试榜单,浪潮AI服务器NF5488A5一举创造18项性能 ...

  6. 全球权威MLPerf基准测试再发榜,浪潮AI服务器创18项AI性能纪录

    美国东部时间10月21日,全球备受瞩目的权威AI基准测试MLPerf公布今年的推理测试榜单,浪潮AI服务器NF5488A5一举创造18项性能纪录,在数据中心AI推理性能上遥遥领先其他厂商产品. MLP ...

  7. 浪潮ai服务器最新数据,浪潮发布的最新AI服务器,将GPU资源利用率提升至前所未有的水平...

    浪潮在ISC20大会期间发布支持最新NVIDIA® Ampere架构A100 PCIe Gen4的AI服务器NF5468M6和NF5468A5,为AI用户提供兼具超强性能与灵活性的极致AI计算平台. ...

  8. gpu超算算法_英伟达推GPU加速Arm服务器参考设计!微软Azure启动GPU超算实例

    芯潮(ID:aichip001)文 | 韦世玮 芯潮11月20日消息,昨天,英伟达在2019国际超级计算大会(SC19)上推出用于构建GPU加速Arm服务器的参考设计,以及用于优化网络和存储数据处理的 ...

  9. 浪潮信息边缘服务器测试,浪潮信息边缘AI服务器全面支持NVIDIA EGX平台

    12 月 18日,在苏州举行的GTC CHINA 2019期间,浪潮信息(000977,股吧)公布两款边缘计算服务器全面支持NVIDIA EGX平台.NVIDIA EGX平台是一个高性能的云原生平台, ...

最新文章

  1. C 语言回顾,数组指针的使用(小鸡肋的使用)
  2. 配置Tomcat使用https协议
  3. java中gson的简单使用
  4. 函授计算机与科学论文,函授计算机科学与技术论文
  5. android activity动态显示不出来,uiautomatorviewer 获取不到动态页面解决办法
  6. C++对象的赋值和复制
  7. 全数字实时仿真平台SkyEye目标码覆盖率关键技术
  8. Kali Linux工具文档翻译计划
  9. 宛如造句,小学生怎么用宛如造句?
  10. 运维自动化之使用PHP+MYSQL+SHELL打造私有监控系统(七)
  11. 【DL小结2】CNN前向、反向传播及常用结构
  12. C# 如何将Excel表格复制到Word中并保留格式
  13. 关于Pycharm进行pytorch分布式训练代码
  14. 信庭嵌入式工作室-ARM应用技术之体系结构应用(下)
  15. AndroidStudio 自带avd模拟器WiFi网络受限无法连接
  16. ajax post请求导致的跨域和浏览器兼容性问题
  17. 24 个很酷的 Linux/Unix 工具
  18. 如何快速提高视唱练耳能力
  19. CentOS7:配置防火墙
  20. 一个关于ul 及 li 横向滚动的故事!

热门文章

  1. php redis删除所有key,redis中批量删除key的方法
  2. Python发展迅猛,如何在Python热中脱颖而出了?
  3. Python趣味编程小技巧,图片转炫酷字符画,一看就是高手
  4. loadrunner录制0事件_LoadRunner脚本异常捕获处理
  5. docker初体验:docker部署nginx负载均衡集群
  6. 【Java】牛客网 链表分割
  7. [CQOI2007]涂色PAINT
  8. 数论基础--矩阵快速幂 及其例题
  9. 图论 ---- E. Pairs of Pairs(构造+无向图的dfs树的性质)
  10. js插值计算_Python IDW插值计算及可视化绘制