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);
…. …. ….
}