CryENGINE3初探Entities (3)-初试Input输入系统,搭配Lua使用C++创建完整逻辑的entity
虽然已经放了寒假,但是感觉事情还是好多,我这两天算是跑动跑西,忙来忙去,啥都干了一通:班级小组信息系统答辩,我负责软件测试,折腾了半天Java MyEclipse,又是用ant JUnit做单元测试,什么checkstyle做各种规范检查,又是写注释,又是写用例,最后BuildAll生文档,整整生了一下午;接着就是操作系统答辩,抱着Linux源码讲解读了半天;然后又是MFC大作业答辩,休息之余,还跟小组一起参加了游戏蛮牛的比赛......吃了一个月的外卖啊,各种心酸一言难尽,不过还好,挺了过来~,这里呢,首先先给我们游戏蛮牛的作品做个宣传吧:
http://www.unitymanual.com/thread-36136-1-1.html?_dsign=cf1adcad
apk,大家没事可以下载下来玩玩......
不过绕来绕去,还是回来继续啃CE3咯。说到CE3,不得不说,俺真是个幸运的娃娃,加入了“无畏征途”游戏组,结识了组长HSK,在他的支持下,看到了CE3 4.5的源码,但是在编译的过程中还是出了很多问题,目前只能顺利编译出Profile 32位的版本。等我把其他版本编译出来,再整理一下和大家分享。
好了,言归正传,今天的内容还是非常丰富实用的,我们将在上一节的基础上,进一步了解C++和lua搭配使用创建自定义Entity的具体流程,因为我写博客,从来都没做过什么草稿,随手就来,所以可能不管是从逻辑思维上,还是内容的排版上都会出现一些小问题,比如上一篇文章中,就有几个小问题,不知道细心的你们发现没有,不过不要紧,等下我会一一指出。
首先说一下这篇文章看完,你能做些什么:
1.你可以使用C++创建一个地雷,当玩家或游戏中其他实体走进地雷的某个区域,会持续掉血。
2.大概认识C++的Input系统,鼠标单击,投掷地雷。(在游戏中实例化一个实体类)
3.搭配lua,不用重新编译,就可以调整地雷的触发范围。
4.大致了解到C++是如何调用Lua的脚本
那我们就开始进行吧!
**********************************************第一部分:C++创建地雷,走近掉血********************************************
这部分其实就是上篇文章的主要部分,这里我只把步骤罗列出来,不赘述。
1.创建筛选器->创建类:CProximityMine
头文件:CProximityMine.h
cpp文件:CProximityMine.cpp
父类:CGameObjectExtensionHelper < CProximityMine, IGameObjectExtension >
2. 实现其中纯虚函数,尤其注意初始化的两个,回收资源的那个,还有GetRMIBase()。
3. GameFactory InitGameFactory中注册类,注意添加头文件。这时候就已经成功创建了一个空的entity了。
4. 在PostInit()中为空的Entity添加模型。
5. 在Update()函数中实现逻辑。
然后简单说一下C++的模板类。我最近也在看C++Primer,学习这部分的内容,我觉得CGameObjectExtensionHelper 可以这样理解,它和vector<>一样,都只是一个模板,但不是类型,具体是啥类型,得看<>号里面的,我们的CGameObjectExtensionHelper尖括号里面的又不是简答的类型,是两个类的关系,其中的父类还是个接口,有大量的纯虚函数,所以我们如果最后想实例化对象出来,就得实现父类里面的纯虚函数啦。不知道这样想对不对,如果不对,还望大家能指出哈。如果想了解更多内容,可以翻翻C++Primer。
这一部分的代码,我就不贴了,其实就是上一篇博客里面的内容。上一次最后的代码有一处错误,可能是编码出了问题,出现了两个乱码:
这两个函数的参数乱码了
bool CMyTestEntity2::ReloadExtension(IGameObject * pGameObject, const SEntitySpawnParams ¶ms) { ResetGameObject(); return true; } void CMyTestEntity2::PostReloadExtension(IGameObject * pGameObject, const SEntitySpawnParams ¶ms) {}正确的应该是
bool CProximityMine::ReloadExtension(IGameObject * pGameObject, const SEntitySpawnParams & pparams) { ResetGameObject(); return true; } void CProximityMine::PostReloadExtension(IGameObject * pGameObject, const SEntitySpawnParams& params ) {}*******************************************第二部分:添加输入,鼠标单击投掷地雷********************************************
从这里开始,便是今天的新内容了。说明一下,关于用户输入系统,书上有一章的内容讲解,所以这里我们只要会用即可,不做深入。另外之前为了编译源码,我安装了VS2010,之后的内容全部都在2010上编译实现。可能是因为CE3本身就是2010实现的,加上CE3的整个系统的逻辑非常严密,之前用2013升级项目后,总觉得会出现问题,所以推荐大家使用2010版本。
1.使用助手,在GameSDK项目中搜索PlayerInput.cpp
2. 在这个cpp文件中找到OnAction(const ActionId& actionId, int activationMode, float value)函数。
第一个参数actionID很好理解,它就是用户输入的标识符,系统通过这个ID来判断用户的输入。
第二个参数描述的是用户是怎么输入的,比如等下我们就会看到熟悉的OnPress(按下键)
第三个参数我暂时也不知道是什么意思,官方文档也没给出相关描述。这个只能等以后我们清楚了再说。
3.在这个函数,添加下面的代码
if (actionId == "attack1" ) { if (activationMode == EActionActivationMode::eAAM_OnPress) { //实例化一个entity需要的参数,包括它的类,它的旋转、位置信息,它的名称 SEntitySpawnParams SP; SP.nFlags = EEntityFlags::ENTITY_FLAG_CALC_PHYSICS | EEntityFlags::ENTITY_FLAG_CASTSHADOW; SP.pClass = gEnv->pEntitySystem->GetClassRegistry()->FindClass("ProximityMine"); SP.qRotation = IDENTITY; SP.sName = "ProximityMine1"; SP.vPosition = m_pPlayer->GetEntity()->GetWorldPos() + Vec3(0,0,1.5); SP.vScale = Vec3(1,1,1); IEntity *pProximityMine = gEnv->pEntitySystem->SpawnEntity(SP); if (pProximityMine) { //添加物理 //物理参数的初始化 SEntityPhysicalizeParams PhysParams; PhysParams.density = 0; //密度 PhysParams.mass = 20; PhysParams.nSlot=0; PhysParams.type = PE_RIGID; pProximityMine->Physicalize(PhysParams); IPhysicalEntity *pPhyEnt = pProximityMine->GetPhysics(); if (pPhyEnt) { pe_action_impulse Impulse; Impulse.iApplyTime = 0; Vec3 Dir = m_pPlayer->GetEntity()->GetForwardDir().normalized(); Dir.SetLength(50); Impulse.impulse = Dir; pPhyEnt->Action(&Impulse); } } } }这部分代码一共完成三个任务:
1.实例化一个对象。
2.给实例化出来的对象添加物理引擎
3.添加推力
先说实例化对象这部分:要想实例化,需要实例化的参数SP;SP也是有很多参数:
1.nflags,他就是entity的flag,它为entity标记了是否使用阴影,是否能够保存等等情况,官方文档罗列出了flag的所有情况;
具体这部分内容,只能是在用的时候去查了。由于我了解的不全,这里暂时不做更多总结。
2.要指明对哪个类实例化,注意这里的类的名字是我们在GameFactory里面注册的类名,然后就是指明对 象的名字啊,位置啊,旋转,缩放信息
接着,就是要给实例化的对象添加物理了,它要完成投掷这个动作发出后的各种物理效果。同样的,要使用物 理的话需要初始化物理的参数:密度,质量,索引,类型。这里我们在实际写代码的时候,要灵活运用助手给提 供的信息,不明白的时候,查手册。
添加推力这部分,上节也有说道,这里也不多说。
另外,attack1表示左键 ,Onpress按下左键
完成了上述任务,你就可以调试打开编译器,测试一下了,如果不出意外,每当你按下左键,不仅会向以前一 样发射子弹,而且生成了一个这个对象,如果你给他设定了模型,应该能看得到模型从空中飞出,落到地上。
******************************第三部分:搭配Lua脚本,动态调整信息******************************************************
虽然使用C++可以实现所有功能,但是有个缺点是,每次修改,都得重新编译GameSDK,有些简单的内容可以直接交给脚本Lua完成。接下来我们就来体会一下,C++怎么搭配lua使用。
这里演示两个功能:
1.用Lua脚本指定对象的模型
2.用Lua脚本指定地雷Proximity的范围。
其实这两个功能都在围绕着CE3中C++如何调用Lua展开。那我们就开始吧:
1 .创建Lua文件
这里必须必须强调一点,我们只创建lua文件,不要创建.ent文件,如果看过前面的文章,你应该知道,我们只用Lua也可以创建一个entity,它分两步:1 .ent文件注册实体,它里面包含了脚本lua的位置,同时这个位置就是编辑器中创建的Entity的位置;2.创建lua脚本,完成各种功能。
如果我们在这一步创建.ent,就意味着又注册了一次entity,我试验的结果是,用.ent注册的实体会替换掉C++里面注册的实体,我们的C++代码将不会执行,所以请记住,这里只写lua文件。最终lua文件如下;
ProximityMine = { Properties = { Visual = { object_Model = "GameSDK/Objects/default/primitive_box.cgf"; }, Detonation = { fRadius = 100; }, }, }lua文件写完后,要返回到C++代码里的GameFactory类的void InitGameFactory(IGameFramework *pFramework)函数中,为注册的entity添加lua代码,修改原先的注册信息如下:
REGISTER_GAME_OBJECT(pFramework,ProximityMine,"Scripts/Entities/Weapons/Mines/ProximityMine.lua");
2.打开CProximityMine.cpp,找到PostInit()函数,在这里,我们曾经给实体entity赋予了cgf模型。修改函数如下:
void CProximityMine::PostInit(IGameObject * pGameObject) { pGameObject->EnableUpdateSlot(this,0); //slot 位置 IEntity *pEnt = GetEntity(); if (pEnt) { /*总结: SmartScriptTable 对应getValue; ScriptAnyValue 对应GetvalueAny */ IScriptTable *pSelfTable = pEnt->GetScriptTable(); ScriptAnyValue PropsTable; pSelfTable->GetValueAny("Properties",PropsTable); ScriptAnyValue VisualTable; PropsTable.table->GetValueAny("Visual",VisualTable); ScriptAnyValue ModelPath; VisualTable.table->GetValueAny("object_Model",ModelPath); pEnt->LoadGeometry(0,ModelPath.str,(const char*)NULL, IEntity::EEntityLoadFlags::EF_AUTO_PHYSICALIZE); } }就像代码里面写的,我们是通过IScriptTable,ScriptAnyValue两个类,和GetValueAny方法来完成整个过程。整体感觉和读取xml文件有点类似。我试验以后发现,ScriptAnyValue和SmartScriptTable有点类似,ScriptAnyValue对应的是GetValueAny方法,而SmartScriptTable对应的是GetValue方法,二者的具体差异,得研究一下IScriptSystem这个类,具体区分这里我也是一知半解,但有一点我很清楚,ScriptAnyValue可以得到任何对象,可以是一个表,可以是一个简单的int啊,string类型的变量。
所以同样道理,找到Update函数,我们为地雷范围做如下修改:
void CProximityMine::Update(SEntityUpdateContext& ctx, int updateSlot) { IEntity *pEnt = GetEntity();//获取当前的entity if (pEnt)//如果entity存在(获取成功) { //auto pSelfTable=pEnt->GetScriptTable(); IScriptTable* pSelfTable = GetEntity()->GetScriptTable(); ScriptAnyValue PropsTable; pSelfTable->GetValueAny("Properties",PropsTable); //SmartScriptTable DetonationTable;//detonation :爆炸 ScriptAnyValue DetonationTable;//detonation :爆炸 PropsTable.table->GetValueAny("Detonation",DetonationTable); ScriptAnyValue DetonationRadius; DetonationTable.table->GetValueAny("fRadius",DetonationRadius); //DetonationRadius=DetonationTable.table->GetValueAny("fRadius",DetonationRadius); SEntityProximityQuery Query;//proximity 距离比较靠近的,建立一个距离entity比较近的查询 Query.box = AABB(pEnt->GetWorldPos(), DetonationRadius.number);//具体距离设置为5单位 Query.nEntityFlags = 0; Query.pEntityClass = nullptr; gEnv->pEntitySystem->QueryProximity(Query); for (int i = 0; i < Query.nCount; i++) { IEntity *pQueryEnt = Query.pEntities[i]; /*Physics System Programming:区域内添加推力impulse*/ IPhysicalEntity *pQueryPhysEnt = pQueryEnt->GetPhysics(); if (pQueryPhysEnt && (pQueryEnt != pEnt)&&(pQueryEnt != gEnv->pGame->GetIGameFramework()->GetClientActor()->GetEntity()) &&(pQueryEnt->GetClass()!=gEnv->pEntitySystem->GetClassRegistry()->FindClass("ProximityMine"))) //这里的Class是刚刚注册过,填写的名称 { pe_action_impulse Impulse; Impulse.iApplyTime = 0; Vec3 Dir = (pQueryEnt->GetWorldPos() - pEnt->GetWorldPos()).normalized(); Dir.SetLength(300); Impulse.impulse = Dir; pQueryPhysEnt->Action(&Impulse); } //获得entity对应的actor并对其生命值做加减法 IActor *pQueryActor = gEnv->pGame->GetIGameFramework()->GetIActorSystem()->GetActor(pQueryEnt->GetId()); if (pQueryActor) { pQueryActor->SetHealth(pQueryActor->GetHealth() - 1); } } } }
道理都是一样的,里面的代码注释也写的很清楚,就这样吧!
到了这一步,你就可以调试打开编辑器测试了
我们可以再不需要重新编译的条件下修改这两个参数,别忘记点ReloadScript就可以啦!
*******************************************************************第四部分:总结***************************************************************
今天总结的这部分内容还是非常实用的,了解了这些,我们再看编辑器提供给我们的实例要容易得多,但是,也得需要我们花功夫去学习,人家的lua脚本是怎么实现的,C++脚本是怎么实现的,哪些内容要放到哪一边,这些都是个人习惯与经验,不是随随便便写一篇文章,读一篇文章就能搞定的。
其实Entity这一张进行到这里,也算是接近尾声了,但是还有一个遗留的问题,我们没有解决,我们可以实现用C++调用Lua这单向的关系,但是反过来呢,怎么用Lua调用C++的函数呢?比如怎么样只写一个Lua脚本完成给entity赋予模型,完成我们这个地雷的这些功能呢?这部分内容和最后的总结就放在下一次的文章中吧。
最后还是那句话,希望有大神能帮忙指点一下,也希望大家把自己的问题列出来,我们一起解决!哈哈,通过编译CE3的代码,我们可以一边学习CE3,一边巩固C++的知识,收获颇丰呐!
下次再见咯