2015年11月11日 星期三

使用LUA+LUABind+MyGUI實現類魔獸世界可讓非程式人員客製化處理GUI邏輯及資料的系統

//===================================================================
為何要客製化?對企劃來說:

  • 因為修改、測試、調整GUI處理流程進度會卡在程式人員(因為串接流程或顯示動態遊戲數據資料是透過程式接起來的)。
  • 新增功能的時候,往往只是處理流程的不同,卻往往要重新設計對應的邏輯系統。
//===================================================================
為何要客製化?對程式來說:

  • 要有超多份的Layout或GUI Widget處理邏輯程式碼,比方一堆*LayoutModifier.cpp及一堆以下的Code:
    ...
for(size_t szItemIndex=0;szItemIndex
{
strIndexName = UTILITY::ValueToString((int)(szItemIndex+1));
CManufactureInfo& REFManufactureInfo = m_vecManufactureInfo[szItemIndex];
//判斷一堆Widget Name,然後再填入正確的動態遊戲數據資料,好醜的Code...
REFManufactureInfo.m_cpInterface->GetQuilityIcon(strIconFileName);
pImageBox = static_cast
(REFGUIManager.GetWidget(m_strDestLayoutName,
"Building_Manufacture_BuildItemQualityImageBox_"+strIndexName));

assert(pImageBox);
pImageBox->setImageTexture(strIconFileName);

pImageBox = static_cast
(REFGUIManager.GetWidget(m_strDestLayoutName,
"Building_Manufacture_BuildItemImageBox_"+strIndexName));

assert(pImageBox);
pImageBox->setImageTexture(REFManufactureInfo.m_cpInterface->GetBaseIcon());

REFManufactureInfo.m_cpInterface->GetClassIcon(strIconFileName);
pImageBox = static_cast
(REFGUIManager.GetWidget(m_strDestLayoutName,
"Building_Manufacture_BuildItemClassImageBox_"+strIndexName));

assert(pImageBox);
pImageBox->setImageTexture(strIconFileName);

REFManufactureInfo.m_cpInterface->GetPartyFlagIcon(strIconFileName);
pImageBox = static_cast
(REFGUIManager.GetWidget(m_strDestLayoutName,
"Building_Manufacture_BuildItemPartTypeImageBox_"+strIndexName));

assert(pImageBox);
if(!strIconFileName.empty())
pImageBox->setImageTexture(strIconFileName);
else
pImageBox->setVisible(false);

pTextBox = static_cast
(REFGUIManager.GetWidget(m_strDestLayoutName,
 "Building_Manufacture_BuildItemCountTextBox_"+strIndexName));

assert(pTextBox);
REFManufactureInfo.m_uiHaveCount =
CaluculateHaveCount(REFManufactureInfo.m_iID);
}
  • 企劃每次一調整Widget的名稱,就會人仰馬翻,得在以上這種很醜的Code把相關對應的名稱修掉。
  • 一堆流程要串接,一堆函式綁定要處理:
MyGUI::Button* pButton = static_cast(pSender);
if(pButton->getName() == "BuyItem")
pButton->eventMouseButtonClick += MyGUI::newDelegate(this, &ItemManager::OnBuyItem);
else if(pButton->getName() == "SellItem")
        pButton->eventMouseButtonClick += MyGUI::newDelegate(this, &ItemManager::OnSellItem);
else if(pButton->getName() == "SelectItem")
        pButton->eventMouseButtonClick += MyGUI::newDelegate(this, &ItemManager::OnSelectItem);
...
  • 其實只是一些功能或流程不同的結合,還是得寫兩份,比方以下程式碼:
void ItemManager::ProcessSelectItem(int iItemID)
{
...
}
    void ItemManager::ProcessBuyItem(int iItemID)
    {
    ...
    }
      void ItemManager::OnSelectItem(MyGUI::Widget* pSender)
      {
          int ItemID = ConvertNameToID(pSender->getName());
          ProcessSelectItem(ItemID);
      }
        void ItemManager::OnBuyItem(MyGUI::Widget* pSender)
        {
            //真是無聊呀
            int ItemID = ConvertNameToID(pSender->getName());
            ProcessSelectItem(ItemID);
            ProcessBuyItem(ItemID);
        }
          void ItemManager::OnSelectItem(MyGUI::Widget* pSender)
          {
              //真是無聊呀
              int ItemID = ConvertNameToID(pSender->getName());
              ProcessSelectItem(ItemID);
              OnSellItem(ItemID);
          }
          //===================================================================

          有沒有解決方法?其實真正要處理的問題是以下三大類:
          • 如何取得GUI Widget的初始值?
          • 當遊戲進行時,如何動態通知對應的GUI Widget要刷新對應的數值?
          • 當GUI Widget觸發事件的時候(比方Click Button),要如何對應所要執行的程式邏輯功能(Delegate Function)

            以上三件事情用LUA+LUABind+MyGUI就可以做到。
          //===================================================================
          GUI Widget初始值的綁定:
          請參考以下MyGUI Layout檔的片斷:
          property key="TextAlign" value="Right VCenter"
          property key="FontHeight" value="24"
          property key="TextColour" value="1 1 1"
          //這是重點,使用了UserString的機制達到屬性值的綁定處理,Property代表是一個需要做屬性綁定的Widget,而Money則是屬性名稱
          userstring key="Property" value="Money"

          請參考以下程式碼片斷:(下面架構出來後,之後只要增新的對應處理函式及屬性名稱即可,比方"Reward"=>GetReward())
          //Widget初始值設定
          void CGUIManager::IntializeWidgetProperty(const std::string& strTypeName, MyGUI::Widget* pWidget, const std::string& strKey, const std::string& strKeyValue)
          {
          if(!m_pGUIManagementInterface)
          return;
          if(strKey != "Property")
          return;
          std::string strOutValue;
          //這裡使用了Interface的架構來取得一個屬性值,降低相依性
          if(m_pGUIManagementInterface->GetGUIPropertyValue(strKeyValue, strOutValue))
          UpdatePropertyValue(strTypeName, pWidget, strOutValue);
          //m_mapUserNameMapping是用來做名稱索引用,之後再解釋
          m_mapUserNameMapping[strKeyValue].push_back(CMappingInformation(m_strInLoadingLayoutName, pWidget));
          }
          //m_pGUIManagementInterface的實作
          bool CGameAPP::GetGUIPropertyValue(strPropertyName, strOutValue)
          {
          return m_GUIPropertyProcessor.RetrievePropertyValue(strPropertyName, strOutValue);
          }
          //m_GUIPropertyProcessor相關函式實作
          CGUIPropertyProcessor::CGUIPropertyProcessor(void)
          {
          m_mapProcessManualTradeFuncs["Money"] = &COnePieceGUIPropertyProcessor::GetMoney;
          m_mapProcessManualTradeFuncs["Diamond"] = &COnePieceGUIPropertyProcessor::GetDiamond;
          }
          bool CGUIPropertyProcessor::RetrievePropertyValue(const std::string& strPropertyName, std::string& strOutValue)
          {
          std::map::iterator it = m_mapProcessManualTradeFuncs.find(strPropertyName);
          if(it == m_mapProcessManualTradeFuncs.end())
          return false;
          (this->*(it->second))(strOutValue);
          return true;
          }
          void COnePieceGUIPropertyProcessor::GetMoney(std::string& OutstrValue)
          {
          const CPlayerInfo& REFPlayerInfo = CPlayerInfoManager::GetSingleton().GetPlayerInfo();
          UTILITY::ValueToString(REFPlayerInfo.m_iHoldGolds);
          OutstrValue = UTILITY::ValueToString(REFPlayerInfo.m_iHoldGolds);
          }
          void COnePieceGUIPropertyProcessor::GetDiamond(std::string& OutstrValue)
          {
          const CPlayerInfo& REFPlayerInfo = CPlayerInfoManager::GetSingleton().GetPlayerInfo();
          UTILITY::ValueToString(REFPlayerInfo.m_iHoldGolds);
          OutstrValue = UTILITY::ValueToString(REFPlayerInfo.m_iHoldDiamonds);
          }
          /補上UpdatePropertyValue的實作,目前只處理字串顯示的部份及更換貼圖的部份
          void CGUIManager::UpdatePropertyValue(const std::string& strTypeName, MyGUI::Widget* pWidget, const std::string& strValue)
          {
          if((strTypeName == "TextBox") || (strTypeName == "Button"))
          static_cast(pWidget)->setCaptionWithReplacing(strValue);
          else if(strTypeName == "ImageBox")
          static_cast(pWidget)->setImageTexture(strValue);
          }
          //===================================================================
          動態刷新GUI Widget的屬性值
          這邊就要提一下m_mapUserNameMapping了,看一下這個東西的定義:
          class CMappingInformation
          {
          public:
          std::string m_strReferenceLayoutName; //所對應的Layout檔名稱,用來處理移除Layout時,所對應的MappingInformation也要移除之用
          MyGUI::Widget* m_pTargetWidget; //Property所對應的Widget
          //std::string m_strValue;
          CMappingInformation(void){}
          CMappingInformation(const std::string& strReferenceLayoutName, MyGUI::Widget* pTargetWidget):m_strReferenceLayoutName(strReferenceLayoutName),
          m_pTargetWidget(pTargetWidget){}
          virtual ~CMappingInformation(void){}
          };
          //Second是一個vector,也就是能處理超過一個以上的Widget顯示相關屬性(比方在不同的Widget顯示玩家所擁有的金幣數量)
          std::map > m_mapUserNameMapping;
          void CGUIManager::IntializeWidgetProperty(const std::string& strTypeName, MyGUI::Widget* pWidget, const std::string& strKey, const std::string& strKeyValue)
          {
          if(!m_pGUIManagementInterface)
          return;
          if(strKey != "Property")
          return;
          std::string strOutValue;
          //這裡使用了Interface的架構來取得一個屬性值,降低相依性
          if(m_pGUIManagementInterface->GetGUIPropertyValue(strKeyValue, strOutValue))
          UpdatePropertyValue(strTypeName, pWidget, strOutValue);
          //m_mapUserNameMapping是用來做名稱索引用,在這裡做綁定
          m_mapUserNameMapping[strKeyValue].push_back(CMappingInformation(m_strInLoadingLayoutName, pWidget));
          }
          //在這裡做屬性值的設定,UpdatePropertyValue之前有提過
          bool CGUIManager::SetWidgetProperty(const std::string& strPropertyName, const std::string& strValue)
          {
          std::map >::iterator PropertyIt = m_mapUserNameMapping.find(strPropertyName);
          if(PropertyIt == m_mapUserNameMapping.end())
          return false;
          std::vector::iterator MappingInfoIt = PropertyIt->second.begin();
          while(MappingInfoIt != PropertyIt->second.end())
          {
          UpdatePropertyValue((*MappingInfoIt).m_pTargetWidget->getTypeName(), (*MappingInfoIt).m_pTargetWidget, strValue);
          ++MappingInfoIt;
          }
          return true;
          }
          //之後程式就可以很大方的直接刷新對應Widget的屬性值
          CGUIManager::GetSingleton().SetWidgetProperty("Money", UTILITY::ValueToString(iHoldGolds));
          CGUIManager::GetSingleton().SetWidgetProperty("Diamond", UTILITY::ValueToString(iHoldDiamonds));
          //===================================================================
          Script函式綁定(這裡使用了萬用的Command架構來做綁定,減少LUABind函式綁定所消耗的記憶體及複雜的函式綁定實作)
          這裡才開始用到了LUA及LUABind,請參考以下Layou檔的內容:
          Widget type="Button" skin="Button" position_real="0 0 0.205742 1" name="ShowPlayerInfoDetailButton"
                    Property key="ImageTexture" value="Mondy.png"
           //Script表示是一段LUA的程式碼,GameCommandProcessor是透過LUABind Binding的一個Global變數,可以當Singleton使用
           //,主要是用來分派命令用的一個處理器,ShowPlayerDetailInfo是一個CommandName,之後的參數用空白隔開
                    UserString key="Script" value="GameCommandProcessor:OnReceiveCommandString("ShowPlayerDetailInfo 1")"

          請參考以下的程式碼:
          ...
          //這裡將GUIManager裡的LUAState綁定了GameCommandProcessor及它的OnReceiveCommandString函式
          luabind::module(pLuaState)[
          luabind::class_("CGameCommandProcessor")
          .def("OnReceiveCommandString", &CGameCommandProcessor::OnReceiveCommandString)
          ];
          luabind::globals(pLuaState)["GameCommandProcessor"] = this;

          void CGUIManager::OnAddUserString(MyGUI::Widget* pSender, const std::string& strKey, const std::string& strKeyValue)
          {
          ...
          //在讀取UserString時,如果是個Script,則將其Click事件對應到CGUIManager::OnScripButtionClick
          if(strKey == "Script")
          {
          //eventMouseButtonReleased
          pButton->eventMouseButtonClick += MyGUI::newDelegate(this, &CGUIManager::OnScripButtionClick);
          }
          ...
          }
          //執行Script的地方
          void CGUIManager::OnScripButtionClick(MyGUI::Widget* pSender)
          {
          assert(m_pLUAContex);
          if(strScriptString.empty())
          return;
          m_pLUAContex->ResetFromString(strScriptString, false);
          }
          //當Script執行完後,這裡會被喚起
          void CGameCommandProcessor::OnReceiveCommandString(const std::string& strCommand)
          {
          if(strCommand.empty())
          return;
          std::vector vecArguments;
          UTILITY::StringSplit(strCommand, vecArguments, " ");
          if(!vecArguments.size())
          return;
          std::vector::iterator ArugmentIt = vecArguments.begin();
          std::string strCommandName = (*ArugmentIt);
          vecArguments.erase(ArugmentIt);
          std::map::iterator ProcessIt = m_mapProcessCommandFunc.find(strCommandName);
          if(ProcessIt != m_mapProcessCommandFunc.end())
          (this->*((*ProcessIt).second))(vecArguments);
          else if(m_pInterface)
          m_pInterface->OnReceiveUnDefineCommand(strCommandName, vecArguments);
          }
          //之後只要做命令綁定,不用做Widget綁定,而且CommandProcessor的觸發也不一定要從GUI的Script來,遊戲的CommandLine Edit也可以下Command,
          //所以做個CP值高,又不用認識WidgetName,摸蛤兼洗褲…
          void CGameCommandProcessor::RebornClientActor(const std::vector& vecArguments)
          {
          std::string ExtendModelPath;
          if(m_pInterface)
          m_pInterface->GetExtendModelPath(ExtendModelPath);
          if(!ExtendModelPath.empty())
          ExtendModelPath += "/";

          CTransformData TranformData;
          TranformData.m_LocationData.m_Position.ReadFromString(vecArguments[2]);
          TranformData.m_ScalerData.m_ScaleFactor = CVector2(1.2f, 1.2f);

          CEffectManager::GetSingleton().CreateEffectPackage(vecArguments[1], TranformData.m_LocationData.m_Position, CVector2::UNIT_SCALE, 0.0f);

          std::string strDestModelPathName = ExtendModelPath+vecArguments[0];
          CClientActorData ClientActorData(CModelData(TranformData, strDestModelPathName, "Idle",  UTILITY::StringToBool(vecArguments[3]))
          , "Idle", ENEMY_GROUP_TYPE_NPC, 10, 10);
          CModelManager::GetSingleton().CreateModel(ClientActorData);
          }
          //===================================================================
          後記:目前已經有將此開發方法應用在公司Cocos2d專案上(筆者之前已經有將此開發方法應
          用在自己的Ogre3d Base的Game Engine Project上),目前成效良好,當企劃有要調整流程或動
          態資料對應的時候,完全不用透過程式,除非有新的Widget實作需求、屬性或Command(重
          點是新的積木需求,而不是改改流程或名字),才需要程式下去進行。如果企劃初期不熟,
          程式自己在Layout加Command或對應屬性也不錯,因為至少不用 重新編譯程式碼,對手遊或
          IOS更新資料即可執行而不用發佈新的版本也有幫助。

          沒有留言:

          張貼留言