AI 서버의 설계와 실현

30463 단어 서버

일정 기간의 설계와 보완을 거쳐 우리 게임의 AI 서버는 이미 기본적인 성능 요구에 도달했고 현재 단일 AI 프로세스는 4000+개의 빈번한 AI 대상을 동시에 운행할 수 있다.
앞의 블로그에서 이미 언급된 바와 같이 AI 서버의 주 논리 순환은 단일 라인이다. 이 라인에는 수천 개의 사용자급 라인이 운행되고 각 사용자급 라인은 하나의 AI 대상을 운행한다.AI 객체가 활성화되면 AI 로직을 위한 루아 스크립트가 실행됩니다.
유저급 스레드(windows 아래는 fiber, linux 아래는 ucontext)를 사용하는 방안은 AI의 실현이 대량의 원격 호출을 사용했기 때문에 동기화 호출을 사용하면 반드시 주 스레드의 막힘을 초래하여 AI 서버의 성능에 영향을 미치기 때문이다.비동기 호출을 채택한 것은 또 논리의 지나치게 복잡함을 야기했다.한편, 사용자급 루틴은 이러한 문제를 해결하고 위에 동기화 호출 인터페이스를 제공하여 주 루틴의 막힘을 초래하지 않는다(한 사용자급 루틴이 결과를 기다리는 상태에서 스케줄러는 다른 사용자급 루틴을 선택하여 실행할 수 있다).
AI 서버의 주요 구성요소는 유저급 스레드 스케줄러와 하나의 유저급 스레드 풀로 서버가 시작되면 유저급 스레드 프로그램이 만들어지고 각 스레드에 루아 가상 머신이 만들어진다.
 
기본적인 디자인 사고방식은 이미 소개를 마쳤고 다음은 각 주요 구성 부분을 소개한다.
우선 주 순환:
 
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);네트워크 계층에서 패키지를 추출합니다. 패키지가 없으면 최대 50ms 동안 휴면합니다.
다음은 스케줄러를 살펴보겠습니다.
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;
                else
                    m_active_head = cur->Next();
            }
            else if(cur == m_active_tail)
            {
                    pre->SetNext(NULL);
                    m_active_tail = pre;
            }
            else
                pre->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)에 투입하거나 대기열의 끝에 다시 스레드를 투입하거나 실행 대기열에서 삭제해야 합니다. (WAIT4EVENT)
      
동기화 호출의 예는 다음과 같습니다.
이동의 경우 AI 요청이 주어진 위치로 이동한다고 가정하면 목표점에 도달할 때까지 게임서버에 이동 요청을 보내거나 이동 실패가 발견되어야 호출에서 되돌아온다.
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 대상이 활성화(유저의 시야에 들어간다)되면 이 대상에게 하나의 라인을 분배하고 이 라인은 이 대상과 관련된 루아 입구 함수를 즉시 실행한다.
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
    -- trace
    stateMachine.state_trace = trace:new():init(this,stateMachine,start_pos)
    --stateMachine.state_trace:init(this,stateMachine,start_pos)
    -- partol
    stateMachine.state_partol = partol:new():init(this,stateMachine,start_pos,points)
    --stateMachine.state_partol:init(this,stateMachine,start_pos,points)
    -- attack
    stateMachine.state_attack = attack:new():init(this,stateMachine)
    --stateMachine.state_attack:init(this,stateMachine)
    -- goback
    stateMachine.state_goback = goback:new():init(this,stateMachine,start_pos)
    --stateMachine.state_goback:init(this,stateMachine,start_pos)
    -- help
    stateMachine.state_help = help:new():init(this,stateMachine)
    --stateMachine.state_help:init(this,stateMachine)
    
    stateMachine.cur_state = stateMachine.state_partol
    
    while isAiRunning(this) == true do
                    
        if isdead(this) == true then
            sc_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 then
                stateMachine.target = select_target(this)
            end
            
            
            -- 
            local sender
            local recver
            local msg
            local sendtick
            sender,recver,msg,sendtick = PopMsg(this)
            if sender ~= nil then
                print(" ")
                if msg == "help" then
                    -- 
                    if stateMachine.target == nil then
                        stateMachine.target = sender
                        stateMachine.cur_state = stateMachine.state_help
                    end
                end
            end
            
            local ret = 0
            ret,stateMachine.cur_state = stateMachine.cur_state:execute()    
            if ret == -1 then
                return
            end
            sc_yield()
        end
        
    end
end

 
AI 마스터 엔트리 함수는 먼저 상태기를 만들고 초기 상태 실행을 선택합니다.추격 상태의 처리를 살펴보자.
trace = {
owner = 0,
StateMachine = 0,
start_pos = 0
}
 
function trace:init(owner,statemachine,start_pos)
    self.owner = owner
    self.StateMachine = statemachine
    self.start_pos = start_pos
    return 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 then
            return 0,self.StateMachine.state_goback
        else
              --return 0,self.StateMachine.state_partol
        end
    else
        local dis2begpos = calDistance(self.start_pos.x,self.start_pos.y,self.StateMachine.cur_pos.x,self.StateMachine.cur_pos.y)
        if dis2begpos >= 4000 then
            return 0,self.StateMachine.state_goback
        else
              --             
            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 then
                    local 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) then
                        return -1,nil
                    end
                    -- 
                      turnface(self.owner,self.StateMachine.target)
                    -- 
                  --end
                return 0,self.StateMachine.state_attack
            else
                -- 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) then
                          return -1,nil
                      end
                else                            
                    local ttx,tty = forword(self.owner,d_x,d_y,300)
                      if -1 == mov(self.owner,ttx,tty,target_pos.z,804,1000) then
                          return -1,nil
                      end
                end
            end
        end
    end
    return 0,self.StateMachine.state_trace
end
        
function trace:new(o)
  o = o or {}   
  setmetatable(o, self)
  self.__index = self
  return o
end

 
 
추격 상태에서 각종 조건에 따라 추격을 집행하거나 다음 상태로 돌아간다.
 
 
 
 

좋은 웹페이지 즐겨찾기