|
对于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 int GetState() = 0; //获得对象状态(需要继承者自己实现)
- virtual int GetNomalAttackMin() = 0; //得到普通攻击力(最小)
- virtual int GetNomalAttackMax() = 0; //得到普通攻击力(最大)
- virtual int SetHP(int nHP) = 0; //设置一个HP的数值,返回当前HP
- virtual int GetHP() = 0; //得到当前HP的数值
- virtual IObjectAI* GetObjectAI() = 0; //得到这个对象的AI引擎指针
- virtual int GetRunRatio() = 0; //得到逃跑比例
- virtual void SetObjectID(int nObjectID) = 0; //设置ObjectID
- virtual int GetObjectID() = 0; //获得ObjectID
- virtual void SetName(const char* pName) = 0; //设置对象的名称
- virtual char* GetName() = 0; //得到对象的名称
- virtual void SetObjectPosIndex(int nIndex) = 0; //设置当前对象由哪个刷新点刷出
- virtual int GetObjectPosIndex() = 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 int GetSpeed() = 0; //获得对象速度
- virtual int GetStandTimeInterval() = 0; //得到停留的时间长度
- virtual int GetRangeID() = 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);
- //对应属性值的各种读取与写入
- int GetState();
- bool SetState(int nState);
- int GetNomalAttackMin();
- int GetNomalAttackMax();
- int GetRunRatio();
- int SetHP(int nHP);
- int GetHP();
- void SetObjectID(int nObjectID);
- int GetObjectID();
- void SetName(const char* pName);
- char* GetName();
- void SetObjectPosIndex(int nIndex);
- int GetObjectPosIndex();
- _MapPointPath* GetMapPointPath();
- void SetCurrPoint(_MapPoint* pCurrPoint);
- _MapPoint GetCurrPoint();
- void SetTimeStamp(unsigned long lNPCTimeStamp);
- unsigned long GetTimeStamp();
- void SetSpeed(int nSpeed);
- int GetSpeed();
- int GetStandTimeInterval();
- int GetRangeID();
- 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
- {
- int m_nObjectID; //设置对象ID
- char m_szNpcName[NPC_MAX_NAME]; //当前NPC的名字
- int m_nHP; //当前NPC的HP
- int m_nAIType; //所绑定的AI策略ID
- int m_nAttackMin; //最小攻击力
- int m_nAttackMax; //最大攻击力
- int m_nRunRatio; //逃跑比例
- int m_nObjectState; //对象当前状态
- int m_nNPCTypeID; //NPC的类型ID
- int m_nObjectPosIndex; //刷新点Index
- int m_nBaseSpeed; //对象基础移动速度
- int m_nStandTimeInterval; //行走间隔停留时间
- int m_nRangeID; //对象所在区块ID
- int m_nScriptID; //此对象对应的脚本ID,用于NPC扩展行为
-
- _NPCAttribute()
- {
- m_nObjectID = OBJECT_VAILD_ID;
- m_nObjectPosIndex = OBJECT_VAILD_ID;
- m_szNpcName[0] = '\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("[map](%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("[map](%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("[CScene::LoadScene]Read CNPCObjectManager file error.\n");
- return false;
- }
- //加载刷新点信息
- if(false == m_objMapObjectPosManager.Init(objSenceInfo.m_szObjectPosFileName))
- {
- printf_s("[CScene::LoadScene]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("[CScene::LoadScene]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和地图,场景串联起来,成为一个有机的整体。 |
|