freeeyes 发表于 2011-8-25 14:50:49

一个MMO游戏服务逻辑系统(二)

对于NPC的设计,一般的考量是,NPC属性和玩家属性是差不多的。或者很多是通用的,由于在游戏设计中存在各种NPC,各种血量,各种外形和各种AI,所以需要一个虚类来实现基础的NPC属性记录。而实际的NPC属性,则可以用一个结构体来实现。#define OBJECT_NAME_MAX 50   //对象最大名称长度


class IObjectAI;
class IObject
{
public:
        virtual bool Update()                                  = 0;      //对象执行,根据不同的状态执行相应的代码
        virtual bool SetState(int nState)                      = 0;      //设置对象状态(需要继承者自己实现)
        virtual intGetState()                              = 0;      //获得对象状态(需要继承者自己实现)
        virtual intGetNomalAttackMin()                     = 0;      //得到普通攻击力(最小)
        virtual intGetNomalAttackMax()                     = 0;      //得到普通攻击力(最大)
        virtual intSetHP(int nHP)                            = 0;      //设置一个HP的数值,返回当前HP
        virtual intGetHP()                                 = 0;      //得到当前HP的数值
        virtual IObjectAI* GetObjectAI()                     = 0;      //得到这个对象的AI引擎指针
        virtual intGetRunRatio()                           = 0;      //得到逃跑比例
        virtual void SetObjectID(int nObjectID)                = 0;      //设置ObjectID
        virtual intGetObjectID()                           = 0;      //获得ObjectID
        virtual void SetName(const char* pName)                = 0;      //设置对象的名称
        virtual char* GetName()                              = 0;      //得到对象的名称
        virtual void SetObjectPosIndex(int nIndex)             = 0;      //设置当前对象由哪个刷新点刷出
        virtual intGetObjectPosIndex()                     = 0;      //获取当前对象由哪个刷新点刷出
        virtual _MapPointPath* GetMapPointPath()               = 0;      //获取当前对象的路径参数
        virtual void SetCurrPoint(_MapPoint* pCurrPoint)       = 0;      //设置当前位置
        virtual _MapPoint GetCurrPoint()                     = 0;      //获取当前位置
        virtual void SetTimeStamp(unsigned long lNPCTimeStamp) = 0;      //设置当前对象处理时间
        virtual unsigned long GetTimeStamp()                   = 0;      //获得当前对象处理时间
        virtual void SetSpeed(int nSpeed)                      = 0;      //设置对象速度
        virtual intGetSpeed()                              = 0;      //获得对象速度
        virtual intGetStandTimeInterval()                  = 0;      //得到停留的时间长度
        virtual intGetRangeID()                              = 0;      //得到区块ID
        virtual void SetRangeID(int nRangID)                   = 0;      //设置区块ID

};

这里我把NPC需要的所有属性方法,主要是set和Get封装成了一个虚类,用于NPC对象的继承。对于一个NPC而言,它应该至少由两部分组成,一个是NPC的属性部分,一个是NPC AI处理部分。
首先,让我们来看看如何管理一个NPC对象是如何构成的。
class CNPCObject : public IObject
{
public:
        CNPCObject();
        ~CNPCObject(void);

        //初始化方法
        void Init(_NPCAttribute objNPCAttribute, IObjectAI* pObjectAI);
        void SetMapData(CMapBaseData* pMapBaseData);
        void SetRangeBaseManager(CRangeBaseManager* pRangeBaseManager);

        //对应属性值的各种读取与写入
        intGetState();
        bool SetState(int nState);
        intGetNomalAttackMin();
        intGetNomalAttackMax();
        intGetRunRatio();
        intSetHP(int nHP);
        intGetHP();
        void SetObjectID(int nObjectID);
        intGetObjectID();
        void SetName(const char* pName);
        char* GetName();
        void SetObjectPosIndex(int nIndex);
        intGetObjectPosIndex();
        _MapPointPath* GetMapPointPath();
        void SetCurrPoint(_MapPoint* pCurrPoint);
        _MapPoint GetCurrPoint();
        void SetTimeStamp(unsigned long lNPCTimeStamp);
        unsigned long GetTimeStamp();
        void SetSpeed(int nSpeed);
        intGetSpeed();
        intGetStandTimeInterval();
        intGetRangeID();
        void SetRangeID(int nRangID);

        IObjectAI* GetObjectAI();


        //心跳
        bool Update();
       

private:
        IObjectAI*    m_pIObjectAI;               //对象AI
        _NPCAttribute m_NPCAttribute;             //对象属性
        _MapPointPath m_MapPointPath;             //对象路径属性
        _MapPoint   m_CurrPoint;                //当前NPC的位置
        unsigned long m_lNPCTimeStamp;            //NPC最后被服务器处理的时间戳
        int         m_nStateCooldown;         //状态执行最少持续时间
};


这里,所有的NPC属性是由一个结构体管理的。_NPCAttribut结构体负责存储指定NPC的一些信息。
//NPC所有属性的结构体
struct _NPCAttribute
{
        intm_nObjectID;               //设置对象ID
        char m_szNpcName;   //当前NPC的名字
        intm_nHP;                     //当前NPC的HP
        intm_nAIType;                   //所绑定的AI策略ID
        intm_nAttackMin;                //最小攻击力
        intm_nAttackMax;                //最大攻击力
        intm_nRunRatio;               //逃跑比例
        intm_nObjectState;            //对象当前状态
        intm_nNPCTypeID;                //NPC的类型ID
        intm_nObjectPosIndex;         //刷新点Index
        intm_nBaseSpeed;                //对象基础移动速度
        intm_nStandTimeInterval;      //行走间隔停留时间
        intm_nRangeID;                  //对象所在区块ID
        intm_nScriptID;               //此对象对应的脚本ID,用于NPC扩展行为
       

        _NPCAttribute()
        {
                m_nObjectID          = OBJECT_VAILD_ID;
                m_nObjectPosIndex    = OBJECT_VAILD_ID;
                m_szNpcName       = '\0';
                m_nHP                = 0;
                m_nAIType            = 0;
                m_nAttackMin         = 0;
                m_nAttackMax         = 0;
                m_nRunRatio          = 0;
                m_nNPCTypeID         = 0;
                m_nBaseSpeed         = 0;
                m_nStandTimeInterval = 0;
                m_nRangeID         = 0;
                m_nScriptID          = 0;
                m_nObjectState       = ENUM_OBJECTSTATE::OBJECTSTATE_INIT;
        };

        _NPCAttribute& operator = (const _NPCAttribute& ar)
        {
                this->m_nObjectID       = ar.m_nObjectID;
                this->m_nNPCTypeID      = ar.m_nNPCTypeID;
                this->m_nAIType         = ar.m_nAIType;
                this->m_nHP             = ar.m_nHP;
                this->m_nAttackMin      = ar.m_nAttackMin;
                this->m_nAttackMax      = ar.m_nAttackMax;
                this->m_nRunRatio       = ar.m_nRunRatio;
                this->m_nObjectState    = ar.m_nObjectState;
                this->m_nObjectPosIndex = ar.m_nObjectPosIndex;
                this->m_nBaseSpeed      = ar.m_nBaseSpeed;
                this->m_nRangeID      = ar.m_nRangeID;
                this->m_nScriptID       = ar.m_nScriptID;
                sprintf_safe(this->m_szNpcName, NPC_MAX_NAME, "%s", ar.m_szNpcName);
                return *this;
        }
};

当然,这只是一个例子,你可以自由添加你想要的元素,但是切记,需要对应的给IObject提供接口。
这里要说一个小技巧,对于NPC,一般分为两种,一种是战斗类型的NPC,一种是服务类型的NPC,其实这两种NPC并无绝对区分必要,既能在某一定条件下变为战斗NPC也可以在一定条件下变为服务性NPC是一个很灵活的属性,不是吗?其实对于主NPC而言,一般情况是由地图编辑器安插在地图点上的(也就是俗称的NPC刷新点,当某一指定的NPC不存在了,那么一定时间后会在这里复活)。那么在这种情况下,我的所有NPC ID实际不用实时生成,而是在地图编辑的时候就制定了,这样做的好处是,我在地图上生成的所有NPC,我根据ID都能做到心里有数。而且同一个ID不会出现两次。当然,你可以为了游戏观赏性,NPC召唤出小NPC辅助,这部分NPC实际也可以ID指定的。这个ID实际就是资源出生点的ID。类似,在魔兽世界中的采矿点的矿藏,你其实也大可看成一种NPC,和普通攻击的NPC没有多大区别,都是提供服务类型的NPC。在这里还可以引出一个小技巧。比如某一个法师发出一个范围大招,比如火雨,在这个区域内的所有玩家将会受到火雨的伤害,持续时间5秒。其实这个实现非常简单,就是在法师出技能的时候,创造一个时限性的NPC,这个NPC的AI就是在周围放一个火伤害的空间。当然,在游戏中你是看不见这个NPC的,但是实际上,服务器上是有这个对象存在的,5秒后自动销毁。借此,我们可以利用这个机制,实现几乎各种复杂的法术,范围技能,甚至叠加的效果。
好了,对于一个场景而言,我出现的NPC种类是有限的,那么,我怎么把NPC的刷新点,NPC类型封装在一个场景文件中呢?
看看我是怎么做的。
        //创建字典文件
        sprintf_s(pFileName, MAX_MAPNAME, "npc.dict");

        //打开NPC字典文件
        pFile = fopen(pFileName, "wb");

        int nNPCCount = 3;

        fwrite(&nNPCCount, 1, sizeof(int), pFile);

        _NPCAttribute npca1;
        sprintf_s(npca1.m_szNpcName, NPC_MAX_NAME, "自由之眼");
        npca1.m_nAttackMin         = 50;
        npca1.m_nAttackMax         = 70;
        npca1.m_nHP                = 230;
        npca1.m_nRunRatio          = 50;
        npca1.m_nAIType            = 0;
        npca1.m_nBaseSpeed         = 5;
        npca1.m_nNPCTypeID         = 1001;
        npca1.m_nScriptID          = 1001;
        npca1.m_nStandTimeInterval = 1000;
       
        fwrite(&npca1, 1, sizeof(_NPCAttribute), pFile);

        sprintf_s(npca1.m_szNpcName, NPC_MAX_NAME, "蓝翼");
        npca1.m_nAttackMin         = 30;
        npca1.m_nAttackMax         = 45;
        npca1.m_nHP                = 100;
        npca1.m_nRunRatio          = 0;
        npca1.m_nAIType            = 0;
        npca1.m_nBaseSpeed         = 6;
        npca1.m_nNPCTypeID         = 1002;
        npca1.m_nScriptID          = 1002;
        npca1.m_nStandTimeInterval = 1000;

        fwrite(&npca1, 1, sizeof(_NPCAttribute), pFile);

        sprintf_s(npca1.m_szNpcName, NPC_MAX_NAME, "天际");
        npca1.m_nAttackMin         = 300;
        npca1.m_nAttackMax         = 500;
        npca1.m_nHP                = 1200;
        npca1.m_nRunRatio          = 0;
        npca1.m_nAIType            = 0;
        npca1.m_nBaseSpeed         = 10;
        npca1.m_nNPCTypeID         = 1003;
        npca1.m_nScriptID          = 1003;
        npca1.m_nStandTimeInterval = 1000;

        fwrite(&npca1, 1, sizeof(_NPCAttribute), pFile);
        fflush(pFile);
        printf("(%s)Create NPC dict OK.\n", pFileName);
        fclose(pFile);


如上,我创建一个字典文件,这个字典主要是服务于当前场景可能出现的所有NPC种类(现在我这里有三种NPC,分别为自由之眼,蓝翼和天际),这里有这些NPC对应的血量,类型等等,如果你愿意,你可以添加NPC颜色,称号。这样更具备趣味性。这个字典是为刷新点文件做对照的,对于刷新点文件,我们可以这么做。
        //创建刷新点
        sprintf_s(pFileName, MAX_MAPNAME, "map1.pos");

        //打开NPC字典文件
        pFile = fopen(pFileName, "wb");

        int nObjectPosCount = 1500;

        fwrite(&nObjectPosCount, 1, sizeof(int), pFile);

        for(int i = 0; i < 500; i++)
        {
                _MapObjectPos objMapObjectPos1;
                objMapObjectPos1.m_nGuid         = 100001 + i;
                objMapObjectPos1.m_nPosX         = 5;
                objMapObjectPos1.m_nPosY         = 5;
                objMapObjectPos1.m_nObjectType   = 1001;
                objMapObjectPos1.m_nTimeInterval = 500;

                fwrite(&objMapObjectPos1, 1, sizeof(_MapObjectPos), pFile);
        }

        for(int i = 0; i < 500; i++)
        {
                _MapObjectPos objMapObjectPos2;
                objMapObjectPos2.m_nGuid         = 100501 + i;
                objMapObjectPos2.m_nPosX         = 10;
                objMapObjectPos2.m_nPosY         = 10;
                objMapObjectPos2.m_nObjectType   = 1002;
                objMapObjectPos2.m_nTimeInterval = 500;

                fwrite(&objMapObjectPos2, 1, sizeof(_MapObjectPos), pFile);
        }

        for(int i = 0; i < 500; i++)
        {
                _MapObjectPos objMapObjectPos3;
                objMapObjectPos3.m_nGuid         = 101001 + i;
                objMapObjectPos3.m_nPosX         = 15;
                objMapObjectPos3.m_nPosY         = 15;
                objMapObjectPos3.m_nObjectType   = 1003;
                objMapObjectPos3.m_nTimeInterval = 500;

                fwrite(&objMapObjectPos3, 1, sizeof(_MapObjectPos), pFile);
        }
       
        fflush(pFile);
        printf("(%s)Create map pos OK.\n", pFileName);
        fclose(pFile);


好了,比如我创造了1500个NPC,按照上面的字典一样500个,这里objMapObjectPos3.m_nGuid就是我的NPC场景中的唯一ID,m_nPosX和m_nPosY是出生点,m_nObjectType会对应NPC字典文件中的npca1.m_nNPCTypeID。这样我就知道这个刷新点上的NPC是一个什么家伙。npca1.m_nAIType指的是绑定AI的类型,我可以在服务器上提供一批AI供NPC选择,比如激进的,保守的,勇敢的,怯懦的,爱主人的。。。这样,在地图编辑器上,我就可以实现策划随意使用我创建的NPC和AI,这两个对象是独立的,可以组合的,这样就能组合成丰富多彩的NPC体系。
那么或许你会问我,为什么要将刷新点和NPC字典分开,呵呵,这是从地图编辑器上考虑,更多的时候,策划是在调整地图上的NPC对象而非创造NPC,这两个事情完全可以并行去做。分为两个文件对于游戏开发者更适合专注的去做好其一。
好了,NPC和刷新点的文件已经创造好了,让我们来看看游戏服务器是怎么读取的。
        //加载NPC字典信息
        if(false == m_objNPCObjectManager.Init(objSenceInfo.m_szNPCDictFileName, objSenceInfo.m_nMaxObjectCount))
        {
                printf_s("Read CNPCObjectManager file error.\n");
                return false;
        }

        //加载刷新点信息
        if(false == m_objMapObjectPosManager.Init(objSenceInfo.m_szObjectPosFileName))
        {
                printf_s("Read m_objMapObjectPosManager file fail.\n");
                return false;
        }

        //根据刷新点,创建相应的NPC
        for(int i = 0; i < m_objMapObjectPosManager.GetCount(); i++)
        {
                _MapObjectPos* pMapObjectPos = m_objMapObjectPosManager.GetObjectPos(i);
                if(NULL != pMapObjectPos)
                {
                        int nNPCID = m_objNPCObjectManager.CreateNPCObject(pMapObjectPos->m_nGuid, pMapObjectPos->m_nObjectType);
                        CNPCObject* pNPCObject = (CNPCObject* )m_objNPCObjectManager.GetNPCObject(nNPCID);
                        if(NULL != pNPCObject)
                        {
                                _MapPoint objCurrPoint;
                                objCurrPoint.m_nX = pMapObjectPos->m_nPosX;
                                objCurrPoint.m_nY = pMapObjectPos->m_nPosY;
                                pNPCObject->SetCurrPoint(&objCurrPoint);
                                pNPCObject->SetObjectPosIndex(i);
                                pNPCObject->SetState(ENUM_OBJECTSTATE::OBJECTSTATE_NOMAL);
                                pNPCObject->SetMapData((CMapBaseData* )&m_objMapData);
                                pNPCObject->SetTimeStamp(GetTimeStamp());
                                pNPCObject->SetRangeBaseManager((CRangeBaseManager* )&m_objRangeManager);
                                pNPCObject->GetObjectAI()->Init();

                                printf("Add NPC (%s).\n", pNPCObject->GetName());
                        }
                }
        }

我根据上述原则,先加载所有的NPC字典信息,然后加载刷新点文件,根据文件的定义,我把NPC创建出来,并还原到地图的点上,细心的你,可能会发现SetRangeBaseManager()这个东东是干什么的。呵呵,这是一个"区域"管理器,用于NPC移动广播周边信息的内容。这个东东我会在下一讲详细的说明,姑且你先当它是一个NPC位置相关的属性好了。
好了,NPC的基础数据还原了,让我们看看AI是怎么做的。
首先,我们要理解什么是AI,AI实际并不是一组数据,更确切的讲,AI是一种在一定事件发生后,你需要做怎样的反应。拿《游戏编程精粹6》里面所讲的Quick3,当NPC出生后,我先要确定周围的情况(OnLookAround),然后做出思考,我应该怎么设计我的移动路线(OnMove),当我在移动过程中,受到了攻击的时候(OnBeAttack),我应该如何处理?或者说当我看见敌人(OnSeeTargetIn),我应该区分是敌是友,决定是否靠近到攻击距离(OnApproach),然后攻击或者防御(OnAttack)。如果敌人离开我的视觉范围(OnSeeTargetLeave),我会怎么办?最后,敌人被我打死了(OnTargetDead),我是因该怎么处理?亦或者,我死了(OnDead),我因该在死的时候做些什么动作?
看看,随着想象,你已经非常清楚的知道了,一个AI应该包含多少个基础方法,至于实现,我们只需要填充这些事件产生后的处理结果即可。其实仔细想想,这些事件几乎能覆盖你所接触到的基本反应需求。
那么我来设计一个通用的AI接口吧,所有的AI处理,都可以继承于此。
class IObjectAI
{
public:
        IObjectAI()
        {
                m_pObject = NULL;
        };

        virtual ~IObjectAI() {};

        //设置类型本体,AI拥有者对象指针
        void SetObject(IObject* pObject)
        {
                m_pObject = pObject;
        };

        //设置AI初始化信息
        virtual bool Init()                            = 0;

    //进入视野,pTarget为进入视野的对象
        virtual bool OnSightInEvent(IObject* pTarget)= 0;

        //离开视野,pTarget为离开视野的对象
        virtual bool OnSightOutEvent(IObject* pTarget) = 0;

        //被攻击,pTarget是攻击者名称,nType为攻击类型,nSkillID为技能ID
        virtual bool OnBeAttackEvent(IObject* pAttack, int nType, int nSkillID) = 0;

        //死亡,被那个攻击者杀死,pAttack为攻击者对象
        virtual bool OnDeadEvent(IObject* pAttack)   = 0;

        //逃跑,打不过的时候
        virtual bool OnRunEvent(IObject* pAttack)      = 0;

        //设置地图指针,用于AI寻路等计算
        virtual void SetMapBaseData(CMapBaseData* pMapBaseData) = 0;

        //设置曲别针指针,用于区域对象搜索
        virtual void SetRangeBaseManager(CRangeBaseManager* pRangeBaseManager) = 0;

        //平常调用,当什么事件都没有发生的时候会调用此
        virtual bool Update()                        = 0;

public:
        IObject* m_pObject;
};

这里我只是举例,实际情况中,你完全可以丰富我的基类,提供更多的基础事件。
对于AI,我并不是只需要算法就够了,我还可能需要一个列表,举例来说,如果我身边出现了多个敌人,我需要多个敌人来一个顺序,先打哪个,再打哪个,亦或,我需要一个队友列表。如此等等,根据策划的衍生来决定这个基类的构造。
那么,我来做一个真正的AI吧。
class CNPCAI : public IObjectAI
{
//AI事件相关接口
public:
        CNPCAI();
        ~CNPCAI();

        bool Init();

        void SetNPCObjectManager(CObjectManager* pNPCObjectManager);
        void SetMapBaseData(CMapBaseData* pMapBaseData);
        void SetRangeBaseManager(CRangeBaseManager* pRangeBaseManager);
        CObjectManager* GetNPCObjectManager();

        bool OnSightInEvent(IObject* pTarget);
        bool OnSightOutEvent(IObject* pTarget);
        bool OnBeAttackEvent(IObject* pAttack, int nType, int nSkillID);
        bool OnDeadEvent(IObject* pAttack);
        bool OnRunEvent(IObject* pAttack);

        bool Update();

private:
        bool DoRandomPathFound();       //随机路径寻找
        bool DoRandomMove();            //按照指定的路径移动
        bool DoLookAround();            //查看周围目标对象
        bool DoStandState();            //站立状态,如果是被动状态这里只等待一段时间,主动状态切换用户到OBJECTSTATE_FIND状态,开始搜索身边的对象。
        bool DoAccack();                //进入进攻状态

//对象列表相关接口,AI需要根据事件存储相应的其他对象列表
public:
        //添加关注的敌人
        bool AddEnemy(int nObjectID);

        //删除关注的敌人
        bool DelEnemy(int nObjectID);

        //得到当前的敌人,先进先出
        IObject* GetCurrEnemy();

        //清除所有敌人目标
        bool ClearEnemy();


       
private:
        CRandom            m_Random;                   //随机数生成器
        IObjectList      m_ObjEnemyList;             //敌人列表
        CObjectManager*    m_pNPCObjectManager;      //NPC对象管理器
        CMapBaseData*      m_pMapBaseData;             //地图接口类指针
        CRangeBaseManager* m_pRangeBaseManager;      //区域曲别针类指针
        _RangeSearch*      m_pRangeSearch;             //周边区域块信息指针
};

在这里,我提供了对虚类定义的实现,你可以参考我的NPC类,里面创建的时候会从AI池中获取一个空闲的AI指针。在里面实现相关的动作。
所有操作都是基于NPC的状态来实现的,bool Update();AI运算的接口,和场景OnUpdate()心跳保持一致。
这样,我就可以根据策划需求,实现一整套NPC的逻辑和AI。
下一讲,我将会讲,怎么把NPC和地图,场景串联起来,成为一个有机的整体。
页: [1]
查看完整版本: 一个MMO游戏服务逻辑系统(二)