AI學習 2011.3.23
一、遊戲服務器玩家對象AI(控制層面AI)
以玩家爲例,玩家對象的狀態的切換,靠對象狀態管理器類來完成。
1、玩家對象的狀態機的構造
對象狀態管理器類UBI_CStateManager管理所有類別對象(玩家對象、怪物對象、寵物對象等)的狀態;該類內部靠下面數據結構爲每一個類別的對象都維繫一個狀態機:
struct StateHead* m_pStateMatrix[OBJECT_TYPE_NUMBER]; // 每種Object類型一個狀態轉換矩陣
/////////////////////////////////////////////////////////////
// 狀態轉換數據結構:
//
// StateHead 1 -> StateElement 3 ->StateElement 2 -> ...
// |
// StateHead 2 -> StateElement 1 ->StateElement 3 -> ...
// |
// StateHead 3 -> StateElement 1 ->StateElement 2 -> ...
//
// - StateHead鏈表:
// 表示某種對象可以有的狀態列表;
//
// - StateElement的子鏈表:
// 每個StateHead都有一個元素爲StateElement的子鏈表,
// 用於表示當前狀態可以轉換的狀態列表;
/////////////////////////////////////////////////////////////
struct StateElement
{
UBI_INT m_StateType; // 狀態類型
UBI_INT m_nLevel; // 狀態轉換優先級
UBI_WCHAR* m_pScript; // 執行腳本
UBI_BOOL m_bInternal; // 是否配置爲內部執行函數
PAIJudgeFunction m_pFunction; // 內部實現函數
struct StateElement* m_pNext; //可轉換下一狀態結點的指針
}
struct StateHead
{
UBI_INT m_StateType; // 狀態類型
UBI_WCHAR* m_pExcuteScript; // 該狀態的邏輯執行腳本
UBI_BOOL m_bInternal; // 是否配置爲內部執行函數
PAIActionFunction m_pFunction; // 內部實現函數
struct StateElement* m_pNextElement; // 後序狀態隊列
struct StateHead* m_pNextHead; // 對象允許下一狀態結點指針
}
成員函數UBI_CStateManager::_LoadObjectStateSwitchTable負責解析表格HumanStateSwitch.tab(縱向看),生成狀態機結構:
// 行列相同,即寫對角線上定義的爲當前狀態下執行的操作
if (nLineIndex == nColumnIndex)
{
if (pHead[nColumnIndex]->m_pExcuteScript!= UBI_NULL)
{
if (UBI_CTools::Strcmp(pHead[nColumnIndex]->m_pExcuteScript,INTERNAL_FUNCTION) == 0)
{
pHead[nColumnIndex]->m_pFunction= GETAISYSTEMPTR->GetAIObject((OBJECT_TYPE)index)->GetAIActionFunction(pHead[nColumnIndex]->m_StateType);
pHead[nColumnIndex]->m_bInternal = UBI_TRUE;
}
}
}
else
{
UBI_CStateManager::StateElement *pElement= UBI_NULL;
UBI_NEW_RETURN(pElement,UBI_CStateManager::StateElement(pData, pHead[nLineIndex]->m_StateType),UBI_FALSE);
if (pElement->m_pScript != UBI_NULL)
{
if (UBI_CTools::Strcmp(pElement->m_pScript, INTERNAL_FUNCTION)== 0)
{
// 若配置爲內部函數, 則初始化pFunction指針
pElement->m_pFunction= GETAISYSTEMPTR->GetAIObject((OBJECT_TYPE)index)->GetAIJudgeFunction(
pHead[nColumnIndex]->m_StateType, pElement->m_StateType);
pElement->m_bInternal= UBI_TRUE;
}
}
GETSTATEMANAGERPTR->AddStateElement((OBJECT_TYPE)index,pHead[nColumnIndex], pElement);
}
上面中的GetAIActionFunction是獲取狀態執行函數,GetAIJudgeFunction是獲取狀態切換判斷函數。他們註冊如下:
// 註冊:狀態切換判斷函數
static AIJudgeFunctionMap sAIJudgeFunctionMap[] = {
REGISTER_JUDGE_FUNCTION(HUMAN_STATE_DEAD, HUMAN_STATE_IDLE,
static_cast<PAIJudgeFunction>(&UBI_CAIHuman::CanDeadToIdle)),
REGISTER_JUDGE_FUNCTION(HUMAN_STATE_IDLE, HUMAN_STATE_DEAD,
static_cast<PAIJudgeFunction>(&UBI_CAIHuman::CanIdleToDead)),
REGISTER_JUDGE_FUNCTION(HUMAN_STATE_IDLE, HUMAN_STATE_FIGHT,
static_cast<PAIJudgeFunction>(&UBI_CAIHuman::CanIdleToFight)),
REGISTER_JUDGE_FUNCTION(HUMAN_STATE_FIGHT, HUMAN_STATE_DEAD,
static_cast<PAIJudgeFunction>(&UBI_CAIHuman::CanFightToDead)),
REGISTER_JUDGE_FUNCTION(HUMAN_STATE_FIGHT, HUMAN_STATE_IDLE,
static_cast<PAIJudgeFunction>(&UBI_CAIHuman::CanFightToIdle)),
};
// 註冊:狀態執行函數
// 基礎AI
m_pAIActionFunctionMap[HUMAN_STATE_IDLE]
=REGISTER_ACTION_FUNCTION(static_cast<PAIActionFunction>(&UBI_CAIHuman::Idle_Action));
m_pAIActionFunctionMap[HUMAN_STATE_DEAD]
=REGISTER_ACTION_FUNCTION(static_cast<PAIActionFunction>(&UBI_CAIHuman::Dead_Action));
m_pAIActionFunctionMap[HUMAN_STATE_STALL]
=REGISTER_ACTION_FUNCTION(static_cast<PAIActionFunction>(&UBI_CAIHuman::Stall_Action));
m_pAIActionFunctionMap[HUMAN_STATE_STALL]
=REGISTER_ACTION_FUNCTION(static_cast<PAIActionFunction>(&UBI_CAIHuman::Transfer_Action));
m_pAIActionFunctionMap[HUMAN_STATE_CHARGE]
=REGISTER_ACTION_FUNCTION(static_cast<PAIActionFunction>(&UBI_CAIHuman::Charge_Action));
m_pAIActionFunctionMap[HUMAN_STATE_GATHER]
=REGISTER_ACTION_FUNCTION(static_cast<PAIActionFunction>(&UBI_CAIHuman::Gather_Action));
m_pAIActionFunctionMap[HUMAN_STATE_CHANNEL]
=REGISTER_ACTION_FUNCTION(static_cast<PAIActionFunction>(&UBI_CAIHuman::Channel_Action));
m_pAIActionFunctionMap[HUMAN_STATE_FOLLOW]
=REGISTER_ACTION_FUNCTION(static_cast<PAIActionFunction>(&UBI_CAIHuman::Follow_Action));
m_pAIActionFunctionMap[HUMAN_STATE_SEEK]
=REGISTER_ACTION_FUNCTION(static_cast<PAIActionFunction>(&UBI_CAIHuman::Seek_Action));
// 擴展AI
m_pAIActionFunctionMap[HUMAN_STATE_FIGHT]
= REGISTER_ACTION_FUNCTION(static_cast<PAIActionFunction>(&UBI_CAIHuman::Fight_Action));
m_pAIActionFunctionMap[HUMAN_STATE_MOVE]
= REGISTER_ACTION_FUNCTION(static_cast<PAIActionFunction>(&UBI_CAIHuman::Move_Action));
經過上面過程,就把玩家對象的狀態機構造出來了(玩家不同狀態間的切換條件、以及切換到某種狀態後執行的操作都被解析完畢)。
2、玩家對象的狀態機的使用
在AI基類AIObject的心跳中,調用:
1) UBI_CAIObject::UpdateObjectState( UBI_CObject* pObject, UBI_UINT uTime),這裏主要做如下工作:
(1) 根據當前對象狀態,調用對應的狀態執行函數;
UBI_CStateManager::StateHead* pExcuteState = GETSTATEMANAGER.GetExcuteState(pObject->GetObjectType(), nState);
(this->*(pExcuteState->m_pFunction))(pObject,uTime); //調用狀態執行函數
(2) 做狀態切換;
UBI_CStateManager::StateElement*pStateList = pExcuteState->m_pNextElement;
while(pStateList != UBI_NULL)
{
if(UBI_TRUE== (this->*(pStateList->m_pFunction))(pObject))//調用狀態切換判斷函數
{
pObject->ChangeState(pStateList->m_StateType);
break;
}
pStateList= pStateList->m_pNext;
}
那麼上面的ChangeState裏做了什麼呢?這裏主要是UBI_CAIObject::LeaveState(preState),和UBI_CAIObject::EnterState(curState),除了做了一些狀態變化時的附加操作,裏面會調用腳本函數以外,主要就是調用:pObject->SetState(curState);和pObject->ClearState(preState);服務器上玩家對象狀態變化後,就會給客戶端發包UBI_CSCUpdateObjectStatePacket。
那麼可見ChangeState至關重要,什麼時候調用它呢?
A、一種是人爲調用,比如玩家進入擺攤邏輯了,那麼就調用:pHuman->ChangeState(HUMAN_STATE_STALL);
B、另一種就是上面的,在tick中輪詢檢查狀態切換條件滿足否,用到一些早已被註冊的切換判斷函數,例如玩家從“戰鬥狀態”切換到“死亡狀態”的判斷函數:
UBI_BOOL UBI_CAIHuman::CanFightToDead(UBI_CObject* pObject)
{
if ( UBI_FALSE == pObject->IsSetState(HUMAN_STATE_FIGHT))
{
Assert(0);
UBI_LOG(LM_DEBUG,"Wrong State Switch Call... %d", pObject->GetBaseState());
return UBI_FALSE;
}
UBI_INT nHP = static_cast<UBI_CObjectHuman*>(pObject)->GetHP();
if (nHP <= 0)
{
DoDie(pObject);
// 可以切換至死亡狀態
return UBI_TRUE;
}
return UBI_FALSE;
}
從“戰鬥狀態”切換到“休閒狀態”的判斷函數:
UBI_BOOL UBI_CAIHuman::CanFightToIdle(UBI_CObject* pObject)
{
UBI_CObjectHuman* pHumanObject= (UBI_CObjectHuman*)pObject;
UBI_CTimer* pFightStateTimer= pHumanObject->GetFightStateTimer();
if (UBI_NULL != pFightStateTimer)
{
if (UBI_TRUE == pFightStateTimer->IsSetTimer())
{
if ( UBI_TRUE == pFightStateTimer->IsReachTerm(pObject->GetCurrentTime()))
// 計時器器到時, 切換狀態
return UBI_TRUE;
else
return UBI_FALSE;
}
else
{
// 戰鬥狀態計時器未啓動, 切換狀態
return UBI_TRUE;
}
}
return UBI_FALSE;
}
從“休閒狀態”切換到“戰鬥狀態”的判斷函數:
UBI_BOOL UBI_CAIHuman::CanIdleToFight(UBI_CObject* pObject)
{
UBI_CObjectHuman* pHumanObject= (UBI_CObjectHuman*)pObject;
UBI_CTimer* pFightStateTimer= pHumanObject->GetFightStateTimer();
if (UBI_NULL != pFightStateTimer)
{
if (UBI_TRUE == pFightStateTimer->IsSetTimer())
{
if (UBI_TRUE != pFightStateTimer->IsReachTerm(pObject->GetCurrentTime()))
{
// 戰鬥狀態計時器啓動, 切換狀態
return UBI_TRUE;
}
}
}
return UBI_FALSE;
}
上面的戰鬥狀態計時器在UBI_CObjectHuman::FightStateTrigger調用後開始計時。
2) 在心跳中,除了使用UpdateObjectState輪詢基礎狀態切換外(這種輪詢效率上能再改進嗎?),還調用了UpdateObjectExtendState進行擴展狀態輪詢。
擴展狀態與基礎狀態之間不是互斥關係、可共存,擴展輪詢裏面會調用狀態執行函數(有一部分的擴展狀態已經註冊好了執行函數),以及腳本函數FinishState。它與基礎狀態輪詢不同之處在於:它不做狀態切換,基礎狀態的切換是在輪詢中完成的,設置狀態等也是在輪詢中做的,那麼擴展狀態的切換是在哪做的呢?
擴展狀態一般是由手工調用ChangeState完成切換的,例如:當客戶端發包CSMovePacket過來,LogicServer在Handler裏試圖調用ChangeState(HUMAN_STATE_MOVE),如果成功的話,玩家就變成“移動狀態”了,在下一個tick中再次執行UpdateObjectExtendState時,發現玩家是“移動狀態”,那麼就執行相應的狀態執行函數:Move_Action,因爲已經註冊了:
m_pAIActionFunctionMap[HUMAN_STATE_MOVE]
= REGISTER_ACTION_FUNCTION(static_cast<PAIActionFunction>(&UBI_CAIHuman::Move_Action));
3) 擴展狀態之間的互斥
前面提到HumanStateSwitch.tab表格中只配置了基礎狀態的轉換關係,。。。。
4) 狀態更新包UBI_CSCUpdateObjectStatePacket
二、遊戲客戶端對象AI(模擬層面AI)
1、以基礎AI爲例說明,在UBI_CObjectCharacterAI::Tick_BaseAI中做了如下(1)和(2)兩方面工作:
(1) 從BaseAI 命令列表m_listBaseAICommand中取出一條AI基礎命令,根據這個AI命令的具體類型,去做相應處理,例如:
switch(pObjectAICommand->m_nAICommandID)
{
case OBJECT_AI_COMMAND_MOVE:
BeginProcessAIMove(pObjectAICommand);
}
BeginProcessAIMove中會做:
(a) 狀態更新:用當前狀態去更新基礎AI的上一狀態m_nObjectBaseAILastState,並把當前狀態m_nObjectBaseAICurrentState更新成OBJECT_AI_STATE_MOVE;
(b) 更新當前運行的基礎AI命令m_pCurrentBaseAICommand爲pObjectAICommand。
(2) 此外,根據當前的基礎AI命令類型,去真正邏輯處理,例如:
switch(GetCurrentBaseAIState())
{
case OBJECT_AI_STATE_MOVE:
ProcessAILogicMove(nTime);
}
ProcessAILogicMove中會做:
(a) 調用PlayAction(OBJECT_AI_STATE_MOVE)播放“移動”動作;
(b) 解析當前基礎AI命令m_pCurrentBaseAICommand。
(3) 從上面可知,基礎AI基於是命令方式的,因爲AI狀態的更新是由AI命令來控制的,那麼AI命令又是怎麼來的呢?也就是說上面的m_listBaseAICommand裏面的數據來源是哪呢?
一般是在UBI_CSC****Packet包發過來時,客戶端根據具體的邏輯行爲,生成相應的AI命令後在Push到m_listBaseAICommand中的,例如:
UBI_VOID WINAPI UBI_CPacketExecute::SCNewMoveHumanObjecExecute(UBI_CPacket* pPacket) //創建玩家OBJ 移動
{
//先停止當前移動
…… …
//添加新的移動命令
command.m_nCommandID= OBJECT_AI_COMMAND_MOVE;
vector<UBI_WORLD_POS>pathNode;
pathNode.push_back(pSCMoveHumanObjec->GetTargetPosition());
command.m_apParam[0]= &pathNode;
command.m_auParam[1]= (UBI_UINT)pathNode.size();
command.m_anParam[2]= MOVE_GOAL_COMMAND_NULL;
pObject->ProcessCharacterAICommand(&command, pSCMoveHumanObjec->GetMoveLogicCount());
}
客戶端會調用ProcessCharacterAICommand,其中會根據具體命令類型,調用CreateObjectAICommand構造一個具體命令實例,例如對於“移動命令”,會構造出一個UBI_CObjectAICommandMove命令,之後把它放入m_listBaseAICommand命令隊列中,之後就交由上面的(1)和(2)處理。
2、 那麼客戶端的AI狀態和服務器發過來的對象狀態有啥關係呢?服務器同步過來的對象狀態怎麼利用的呢?
在UBI_CPacketExecute::UpdateObjectStateExecute中,主要做了:
pCharacterData->SetState / ClearState(pObjectState->GetState());
pObject->UpdateSateEvent(pObjectState->GetState(),UBI_TRUE / UBI_FALSE);
客戶端通過UBI_CObjectCharacterData::IsSetState來利用服務器發來的那些狀態,進行一些邏輯判斷操作,例如:
UBI_INT UBI_CObjectMyselfAI::GetActionIDByAIState(ObjectAIState actionType,UBI_INT nSkillID)//通過AIState得到ActionID
{
UBI_INT nActionID= INVALID_VALUE;
UBI_CObjectCharacterData* pCharacterData= m_pObject->GetCharacterData();
if(OBJECT_AI_STATE_MOVE== actionType)
{
if(pCharacterData->IsSetState(HUMAN_STATE_WALK))
nActionID = ACTION_WALK;
else
nActionID = ACTION_RUN;
}
else if(OBJECT_AI_STATE_IDLE==actionType)
{
//根據優先級播放狀態動作(先假定:擊倒>昏迷>恐懼>迷惑>戰鬥>站立)
// 擊倒
if (UBI_TRUE == pCharacterData->IsSetState(HUMAN_STATE_DOWN))
nActionID= ACTION_DOWN;
// 昏迷
else if (UBI_TRUE == pCharacterData->IsSetState(HUMAN_STATE_COMA))
nActionID = ACTION_COMA;
// 恐懼
else if (UBI_TRUE == pCharacterData->IsSetState(HUMAN_STATE_FEAR))
nActionID = ACTION_FEAR;
// 迷惑
else if (UBI_TRUE == pCharacterData->IsSetState(HUMAN_STATE_PUZZLE))
nActionID = ACTION_PUZZLE;
// 戰鬥
else if (UBI_TRUE == pCharacterData->IsSetState(HUMAN_STATE_FIGHT))
nActionID = ACTION_COMBAT;
// 站立
else
nActionID = ACTION_STAND;
}
else
nActionID = UBI_CObjectCharacterAI::GetActionIDByAIState(actionType,nSkillID);
…. …. ….
}