网游服务器端设计思考:游戏的错误提示

在网络游戏中常常会出现这样的情况,玩家在进行一些操作的同时是不能进行另一些操作的。比如说:正在移动的玩家不能释放带有吟唱的技能;死亡时不能交易;不能同时和两个玩家进行交易;不同阵营不允许交易等等。而很多需要交互的操作(比如交易)当一方取消或异常操作时,服务器会撤消双方的操作,并通知双方撤消的原因。在这就会面临一个问题,应该使用什么方式来通知客户端这些错误?

    在设计游戏错误提示模块时应该考虑一下几个问题:

  1. 易用性:每次添加新的错误提示都不费劲!
  2. 可维护性:来个新人和他说两句,下次添加的时候他想都不想就能找到在哪加!
  3. 可扩展性:今天加逻辑服务器给客户端的错误提示,明天要加连接服务器给客户端的错误提示也没有什么心理压力;哪天游戏走运了在海外运营,挣外国人钱的时候,错误提示改成鸟文也不会伤筋动骨!

    常见的有三种方法通知客户端错误:

一、向客户端发送错误码

    向客户端发送错误码是最常见的一种服务器向客户端发送错误通知的方法。服务器端在发现player的操作不合法时,向客户端发送错误码,这个错误码往往是整型数字,客户端收到错误码的协议后在本地查找配置文件,使用错误码查找配置文件的对应项,找到对应错误信息,然后把错误信息显示给玩家。

    客户端的配置文件可以写成这样的形式(server_error.ini):

[Trade]
0     = "成功无错误"

1     = “包裹已满”

2     = “对方正在交易”

3     = “对方拒绝进行交易”

………

 

    服务器端根据各种条件检查的结果向客户端发送对应的错误码,下面以交易操作为例,结合代码来看错误码方法:

Mangos

   1: void WorldSession::HandleInitiateTradeOpcode(WorldPacket& recvPacket)
   2: {
   3:     uint64 ID;
   4:     recvPacket >> ID;
   5:  
   6:     if (GetPlayer()->m_trade)
   7:         return;
   8:  
   9:     if (!GetPlayer()->isAlive())
  10:     {
  11:         SendTradeStatus(TRADE_STATUS_YOU_DEAD);
  12:         return;
  13:     }
  14:  
  15:     if (GetPlayer()->hasUnitState(UNIT_STAT_STUNNED))
  16:     {
  17:         SendTradeStatus(TRADE_STATUS_YOU_STUNNED);
  18:         return;
  19:     }
  20:  
  21:     if (isLogingOut())
  22:     {
  23:         SendTradeStatus(TRADE_STATUS_YOU_LOGOUT);
  24:         return;
  25:     }
  26:  
  27:     if (GetPlayer()->IsTaxiFlying())
  28:     {
  29:         SendTradeStatus(TRADE_STATUS_TARGET_TO_FAR);
  30:         return;
  31:     }
  32:  
  33:     Player* pOther = ObjectAccessor::FindPlayer( ID );
  34:  
  35:     if (!pOther)
  36:     {
  37:         SendTradeStatus(TRADE_STATUS_NO_TARGET);
  38:         return;
  39:     }
  40:  
  41:     if (pOther == GetPlayer() || pOther->m_trade)
  42:     {
  43:         SendTradeStatus(TRADE_STATUS_BUSY);
  44:         return;
  45:     }
  46:  
  47:     if (!pOther->isAlive())
  48:     {
  49:         SendTradeStatus(TRADE_STATUS_TARGET_DEAD);
  50:         return;
  51:     }
  52:  
  53:     if (pOther->IsTaxiFlying())
  54:     {
  55:         SendTradeStatus(TRADE_STATUS_TARGET_TO_FAR);
  56:         return;
  57:     }
  58:  
  59:     if (pOther->hasUnitState(UNIT_STAT_STUNNED))
  60:     {
  61:         SendTradeStatus(TRADE_STATUS_TARGET_STUNNED);
  62:         return;
  63:     }
  64:  
  65:     if (pOther->GetSession()->isLogingOut())
  66:     {
  67:         SendTradeStatus(TRADE_STATUS_TARGET_LOGOUT);
  68:         return;
  69:     }
  70:  
  71:     if (pOther->GetSocial()->HasIgnore(GetPlayer()->GetObjectGuid()))
  72:     {
  73:         SendTradeStatus(TRADE_STATUS_IGNORE_YOU);
  74:         return;
  75:     }
  76:  
  77:     if (!sWorld.getConfig(CONFIG_BOOL_ALLOW_TWO_SIDE_INTERACTION_TRADE) && pOther->GetTeam() !=_player->GetTeam() )
  78:     {
  79:         SendTradeStatus(TRADE_STATUS_WRONG_FACTION);
  80:         return;
  81:     }
  82:  
  83:     if (!pOther->IsWithinDistInMap(_player,10.0f,false))
  84:     {
  85:         SendTradeStatus(TRADE_STATUS_TARGET_TO_FAR);
  86:         return;
  87:     }
  88:  
  89:     // OK start trade
  90:     _player->m_trade = new TradeData(_player, pOther);
  91:     pOther->m_trade = new TradeData(pOther, _player);
  92:  
  93:     WorldPacket data(SMSG_TRADE_STATUS, 12);
  94:     data << (uint32) TRADE_STATUS_BEGIN_TRADE;
  95:     data << (uint64)_player->GetGUID();
  96:     pOther->GetSession()->SendPacket(&data);
  97: }

    可以看到上面华丽丽的九十多行代码就是玩家提出交易申请时服务器端的检查,条件不满足时就会调用SendTradeStatus ()函数向客户端发送对应的错误码。来看看错误码是怎么定义的:

   1: enum TradeStatus
   2: {
   3:     TRADE_STATUS_BUSY           = 0,
   4:     TRADE_STATUS_BEGIN_TRADE    = 1,
   5:     TRADE_STATUS_OPEN_WINDOW    = 2,
   6:     TRADE_STATUS_TRADE_CANCELED = 3,
   7:     TRADE_STATUS_TRADE_ACCEPT   = 4,
   8:     TRADE_STATUS_BUSY_2         = 5,
   9:     TRADE_STATUS_NO_TARGET      = 6,
  10:     TRADE_STATUS_BACK_TO_TRADE  = 7,
  11:     TRADE_STATUS_TRADE_COMPLETE = 8,
  12:     // 9?
  13:     TRADE_STATUS_TARGET_TO_FAR  = 10,
  14:     TRADE_STATUS_WRONG_FACTION  = 11,
  15:     TRADE_STATUS_CLOSE_WINDOW   = 12,
  16:     // 13?
  17:     TRADE_STATUS_IGNORE_YOU     = 14,
  18:     TRADE_STATUS_YOU_STUNNED    = 15,
  19:     TRADE_STATUS_TARGET_STUNNED = 16,
  20:     TRADE_STATUS_YOU_DEAD       = 17,
  21:     TRADE_STATUS_TARGET_DEAD    = 18,
  22:     TRADE_STATUS_YOU_LOGOUT     = 19,
  23:     TRADE_STATUS_TARGET_LOGOUT  = 20,
  24:     TRADE_STATUS_TRIAL_ACCOUNT  = 21, // Trial accounts can not perform that action
  25:     TRADE_STATUS_ONLY_CONJURED  = 22  // You can only trade conjured items... (cross realm BG related).
  26: };

再来看看SendTradeStatus ()函数,可以看到只有当状态正常进行转化时,协议带参数发给客户端,如果出现错误就把错误码直接发给客户端。

   1: void WorldSession::SendTradeStatus(TradeStatus status)
   2: {
   3:     WorldPacket data;
   4:  
   5:     switch(status)
   6:     {
   7:         case TRADE_STATUS_BEGIN_TRADE:
   8:             data.Initialize(SMSG_TRADE_STATUS, 4+8);
   9:             data << uint32(status);
  10:             data << uint64(0);
  11:             break;
  12:         case TRADE_STATUS_OPEN_WINDOW:
  13:             data.Initialize(SMSG_TRADE_STATUS, 4+4);
  14:             data << uint32(status);
  15:             break;
  16:         case TRADE_STATUS_CLOSE_WINDOW:
  17:             data.Initialize(SMSG_TRADE_STATUS, 4+4+1+4);
  18:             data << uint32(status);
  19:             data << uint32(0);
  20:             data << uint8(0);
  21:             data << uint32(0);
  22:             break;
  23:         case TRADE_STATUS_ONLY_CONJURED:
  24:             data.Initialize(SMSG_TRADE_STATUS, 4+1);
  25:             data << uint32(status);
  26:             data << uint8(0);
  27:             break;
  28:         default:
  29:             data.Initialize(SMSG_TRADE_STATUS, 4);
  30:             data << uint32(status);
  31:             break;
  32:     }
  33:  
  34:     SendPacket(&data);
  35: }

天龙

      根据流出的代码(交易命名为exchange害我找了老半天………好吧,我鸟文不好):
   1: uint CGExchangeApplyIHandler::Execute( CGExchangeApplyI* pPacket, Player* pPlayer )
   2: {
   3:     __ENTER_FUNCTION
   4:     GamePlayer* pGamePlayer = (GamePlayer*)pPlayer ;
   5:     Assert( pGamePlayer ) ;
   6:  
   7:     Obj_Human* pHuman = pGamePlayer->GetHuman() ;
   8:     Assert( pHuman ) ;
   9:  
  10:     Scene* pScene = pHuman->getScene() ;
  11:     if( pScene==NULL )
  12:     {
  13:         Assert(FALSE) ;
  14:         return PACKET_EXE_ERROR ;
  15:     }
  16:  
  17:     //检查线程执行资源是否正确
  18:     Assert( MyGetCurrentThreadID()==pScene->m_ThreadID ) ;
  19:  
  20:     ObjID_t        TargetID = pPacket->GetObjID();
  21:     Obj_Human* pSourceHuman = pHuman;//交易发起者
  22:     Obj_Human* pDestHuman = pScene->GetHumanManager()->GetHuman( TargetID );//交易对象
  23:  
  24:     //验证
  25:     if( pDestHuman == NULL )
  26:     {
  27:         Assert(FALSE);
  28:         return PACKET_EXE_CONTINUE;
  29:     }
  30:     // 不同阵营,不让查看
  31:     if( pSourceHuman->IsEnemy( pDestHuman ) )
  32:     {
  33:         g_pLog->FastSaveLog( ........ ) ;
  34:         return    PACKET_EXE_CONTINUE;
  35:     }
  36:  
  37:     INT iSettingData = pDestHuman->GetDB()->GetSetting(SETTING_TYPE_GAME)->m_SettingData;
  38:     if(SETTINGFLAGISTRUE(iSettingData, GSF_REFUSE_TRADE))
  39:     {
  40:         GCExchangeError Msg;
  41:         Msg.SetID(EXCHANGE_MSG::ERR_REFUSE_TRADE);
  42:         pGamePlayer->SendPacket(&Msg);
  43:         g_pLog->FastSaveLog( ........ ) ;
  44:         return PACKET_EXE_CONTINUE;
  45:     }
  46:     if(pSourceHuman->m_ExchangBox.m_Status >= ServerExchangeBox::EXCHANGE_SYNCH_DATA)
  47:     {//发起者正在交易中
  48:         GCExchangeError Msg;
  49:         Msg.SetID(EXCHANGE_MSG::ERR_SELF_IN_EXCHANGE);
  50:         pGamePlayer->SendPacket(&Msg);
  51:         g_pLog->FastSaveLog( ........ ) ;
  52:         return PACKET_EXE_CONTINUE;
  53:     }
  54:     if(pDestHuman->m_ExchangBox.m_Status >= ServerExchangeBox::EXCHANGE_SYNCH_DATA)
  55:     {//目标正在交易中
  56:         GCExchangeError Msg;
  57:         Msg.SetID(EXCHANGE_MSG::ERR_TARGET_IN_EXCHANGE);
  58:         pGamePlayer->SendPacket(&Msg);
  59:         g_pLog->FastSaveLog( ........ ) ;
  60:         return PACKET_EXE_CONTINUE;
  61:     }
  62:  
  63:     //操作
  64:     //发送消息向目标申请
  65:     GCExchangeApplyI Msg;
  66:     Msg.SetObjID(pSourceHuman->GetID());
  67:     pDestHuman->GetPlayer()->SendPacket(&Msg);
  68:     g_pLog->FastSaveLog( ........ ) ;
  69:         return PACKET_EXE_CONTINUE ;
  70:  
  71:     __LEAVE_FUNCTION
  72:  
  73:         return PACKET_EXE_ERROR ;
  74: }

    还是华丽丽的很多检查,如果条件不满足则使用Msg.SetID()设置错误码。错误码声明如下:

   1: enum
   2: {
   3:     ERR_ERR = 0,
   4:     ERR_SELF_IN_EXCHANGE,
   5:     ERR_TARGET_IN_EXCHANGE,
   6:     ERR_DROP,
   7:     ERR_ALREADY_LOCKED,
   8:     ERR_ILLEGAL,
   9:     ERR_NOT_ENOUGHT_ROOM_SELF,
  10:     ERR_NOT_ENOUGHT_ROOM_OTHER,
  11:     ERR_NOT_ENOUGHT_EXROOM,
  12:     ERR_NOT_ENOUGHT_MONEY_SELF,
  13:     ERR_NOT_ENOUGHT_MONEY_OTHER,
  14:     ERR_TOO_FAR,
  15:     ERR_REFUSE_TRADE,
  16:     ERR_PET_LEVEL_TOO_HIGH,
  17:  };

错误码方式的优点:

    a) 扩展性比较好,不同语言版本的客户端只要替换server_error.ini文件就可以了。

    b) 错误码分类、维护都比较简单。

错误码方式的缺点:

    a) 错误码必须服务器端和客户端事先确定好。

    b) 如果遇到比较复杂的逻辑,可能服务器需要判断很多条件,如果每种条件判断失败都发送一个错误码给客户的话,客户端程序估计会哭。

二、状态冲突方法

    该方法需要在客户端和服务器端都维护一张相同的状态冲突表,表中定义了在不同状态下能就行的操作。客户端对于不需要交互的操作可以做预先的判断,比如正在摆摊的时候玩家进行骑马操作,客户端可以查找冲突表,发现玩家在摆摊的状态下不能进行骑马操作,这个时候显示一条错误信息,这个错误信息也可以写在配置文件里。状态冲突表如表2-1

image

表 2-1

状态冲突方法的优点:

    a) 客户端能够快速的提示玩家的错误操作,不需要等待服务器端的回应。

    b) 易用性、扩展性比较好,新加的错误只需在状态冲突表里加上一项,多语言版本只需要替换对应的错误信息文件即可。

状态冲突方法的缺点:

    a) 涉及有交互的操作无法用状态冲突表解决,比如交易时对方主动拒绝。

    b) 一些重要的操作方法客户端预先判断还是有点不放心…… 再者,可能不需要别人拿到这些状态的冲突信息。

三、服务器直接发送错误信息

    第三种方法就是由服务器直接以字符串流的形式发送给客户端,客户端不需要关系错误的内容,直接显示给玩家即可。这样做能省去客户端的工作,服务器在处理较为复杂的逻辑时,错误处理可以随时加:-),但是这样做会付出比较大的代价:首先网络传输量变大,而且很多错误信息都是重复的,对于租用机房特别是双线的机房,流量那可是白花花的银子啊。另外一方面,客户端有时候需要根据错误码来识别一些特殊的操作错误,以便做出对应的处理,比如吟唱中被打断,可以根据错误码来取消读条,切换动作姿态等等。

声明:本文所使用的代码均从互联网上获得,本人没有传播及利用其获取任何商业利益。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值