若你的游戏服务器是使用其他语言开发的,这篇文章对你的帮助可能小一些,在我从业的这些年,早期的时候大量的游戏服务器都是使用c++开发的,特别是MMO类型的游戏。本文涉及的内容曾经在某游戏实际落地过,最核心的玩法是策划使用这套编辑器自行制作出来的,但由于涉及到具体的商业化产品,源码无法提供,只能主要讲述方法论以及提供部分无关紧要的示例代码。
c++的反射可以参考开源库:laiyongcong/cppfoundation: c++ basic library (github.com)
在游戏中,玩法通常千变万化,但是多年的总结下来后,策划的玩法思路跳不出这样一个圈圈:在特定条件下触发特定的事情,这些特定的事情触发后成为或者产生新的条件,然后触发后续的事情。这些“条件->事件”串行或并行地执行,构成了整个玩法。
游戏的条件可能是:玩家进入特定区域、怪物血量变化或者死亡、某些特定事件发生等等。
游戏的事件可能是:npc做一个表现、客户端播放一段动画、某个机关的状态发生改变、记录一个状态等等。
玩家的任务系统、副本系统、游戏世界级的地图玩法也可以采用这种方式组织,功能类似于虚幻的蓝图,但是粒度会比蓝图粗很多很多。
言归正传,我们可以把条件(Condition)封装成类,触发的事件动作(Action)封装成类,链接条件和动作构成的一个主体逻辑(Main)封装成类,一个玩法逻辑就可以用下面的图表示:
这些玩法逻辑是可以串并联起来的,组成一个复杂的玩法网络。
那这个跟C++反射有什么关系?这关系可大了,上图中的逻辑图的每个节点都需要经过反射在我们的程序中实例化,游戏逻辑编辑可以交给其他擅长界面编程的语言来完成(比如C#就不错),实现逻辑单位的拖放、连线,逻辑可以保存成一个json文件,json文件可以按以下方式组织:
主要是把逻辑图上所有的节点以及节点连线写成json文件,下一步我们只需要通过反射在游戏服务器的代码里把这个逻辑图实例化,这需要分成两步来完成:
第一步,我们可以通过反射,做json的最基本的解析,我们可以把整个json文件通过反射方式读入到一个结构体中(开源库里有示例),唯一要做的就是按照json文件的结构写结构体;
第二步:我们把读入的json结构体的信息,通过反射实例化成我们需要的Condition、Action、Main实例,并根据连线关系把他们组织起来(参数部分是个json,做了base64处理,现在的反射库json解析已经支持json的字段内容是一个json);
ILevelActionBase* GameLevelActionLogic::NewInstanceByNode(const FCondActionNode& node)
{
CHAR szJsonParam[MAX_PARAM_LEN] = { 0 };
UINT uLen = (UINT)sizeof(szJson);
if (!Base64Decode((UCHAR*)node.Base64Param, (UINT)strlen(node.Base64Param), (UCHAR*)szJsonParam, &uLen))
{
LOG_FATAL("Base64Decode failed, str:%s", node.Base64Param);
return NULL;
}
String strClass = "LevelAction_";
strClass += node.Type;
const Class* pClass = Class::GetClass(strClass.c_str());
AssertEX(pClass, NULL, "ClassName:%s not found", strClass.c_str());
ILevelActionBase* pBase = pClass->NewInstance<ILevelActionBase>();
AssertEX(pBase, NULL, "Class:%s new instance failed", strClass.c_str());
if (!pBase->FromJsonBuff(szJsonParam))
{
LOG_FATAL("FromJsonBuff failed, str:%s", szJson);
SAFE_DELETE(pBase);
return NULL;
}
pBase->SetID(node.ID);
return pBase;
}
在游戏运行过程中,我们只需要tick Main节点的逻辑,检查条件,如果条件成立则执行它关联的动作,同时修改Main节点的状态。
BOOL GameLevelActionLogic::Tick(UINT uTime)
{
INT nFinCount = 0;
INT nNodeSize = (INT)mTickNodes.size();
for (INT i = 0; i < nNodeSize; i++)
{
LevelAction_Main* pMain = mTickNodes[i];
if(pMain && pMain->mState != ELAState_Fin)
pMain->Tick(uTime);
else
{
nFinCount++;
}
}
//………………
}
void LevelAction_Main::Tick(UINT uTime)
{
if (mState == ELAState_Init)
mState = ELAState_Checking;
if (mState == ELAState_Checking)
{
if (mCondition == NULL || mCondition->Check())
{
mStartRunTime = uTime;
mState = ELAState_Runing;
Implement();
}
}
if (mState == ELAState_Runing)
{
if (mDuration <= 0 || mStartRunTime + mDuration < uTime)
{
mState = ELAState_Fin;
if (mResetSelf)
Reset();
}
}
}
条件我们需要满足与、或、非关系运算,具体需要看图上是怎么链接的,使用代码可以很容易包装这种条件运算,比如一个与操作:
class LevelActionCondition_And : public ILevelAction_Condition
{
public:
LevelActionCondition_And(){}
~LevelActionCondition_And() {}
virtual BOOL Check() override {
size_t nsize = mConditions.size();
for (size_t i = 0; i < nsize; i++) {
ILevelAction_Condition* pCond = mConditions[i];
if (!(pCond && pCond->Check())) {
return FALSE;
}
}
return TRUE;
}
void AddCondition(ILevelAction_Condition* pCond) {
if (pCond) mConditions.push_back(pCond);
}
protected:
vector<ILevelAction_Condition*>::type mConditions;
};
到这里,整个逻辑编辑系统核心部分应该讲清楚了,接下来的工作,我们需要封装各种Condition、Action,并扩展我们的编辑器,剩下的游戏逻辑制作就交给策划了。
当然,这个工具还需要跟地图编辑器联系起来,逻辑编辑是地图编辑的后续工作,地图编辑所摆放的各种游戏单位,都需要清楚标识出来,否则我们没法编辑他们的逻辑。