源碼庫:git
https://git.oschina.net/thorqq/RaidenFreeide
傳統的新手引導方式通常是設置一個全局的靜態變量來保存當前新手引導進度,而後在項目中每一個可能出現新手引導的位置添加一句判斷:若當前新手引導步驟等於我所指望的步驟就執行引導部分的邏輯,新手引導經常邏輯雜亂,並且跟界面的邏輯經常交叉在一塊,弄的很差的話代碼裏處處都是if else,保存各類臨時狀態變量。函數
本文將以仿雷霆戰機遊戲爲例,說說其中關於新手引導的設計。測試
下面先看遊戲中比較典型的兩類引導動畫
一、 菜單引導ui
當用於初次啓動遊戲時,經過兩三個步驟引導讓用戶快速的進入遊戲,體驗遊戲的快感,因此,這裏採用了三個步驟:this
a) 大xiong妹妹出沒,吸引男性玩家的眼球,引導玩家進入主菜單;spa
b) 引導玩家選擇默認飛機,並進入關卡選擇界面;.net
c) 引導玩家選擇第一關,並進入戰鬥界面;設計
二、 遊戲引導
在遊戲過程當中,當知足必定的條件時,就暫停遊戲,彈出遊戲道具(上圖中時護盾,能夠把屏幕上的子彈所有變成寶石)的使用方法說明,並引導玩家體驗道具的效果。
新手引導的顯示效果爲:
a) 突出顯示須要用戶點擊的控件(例如:按鈕),其餘部門所有下降亮度
b) 用動畫箭頭指向該控件,方便玩家識別
c) 顯示文字說明,告知玩家相關的操做步驟或道具使用方法
要讓界面變暗很簡單,覆蓋一個半透明的層就能夠了。那麼怎麼才能讓某個控件變亮呢?easy,把這個控件從父節點上摘下來,掛到半透明層上就能夠了。
原理介紹完畢,下面該講講代碼設計了。
接口調用
菜單引導通常放在界面的onEnter函數中,這樣,當玩家進入該界面時就會馬上觸發新手引導邏輯。下面是出現美女歡迎界面的函數,怎麼樣,只有幾行代碼,很簡單吧。
void MenuMainUI::onEnter() { Layer::onEnter(); //美女新手引導 Guide* pGuide = GuideMgr::getInstance()->show(GuideType::WelcomeGirl, this, nullptr, [this](){ GuideMgr::getInstance()->show(GuideType::WelcomeClickButtonLevel, this, m_pBtnLevel); }); }
在上面的代碼中,咱們使用了這個函數:
Guide* GuideMgr::show(int guideType, Node* parent, Node* widget, const std::function<void()>& callback)
其中:
1) guideType是引導類型,有以下的取值:其中:
const int WelcomeGirl = 10; //第一次進入遊戲後的歡迎說明 const int WelcomeClickButtonLevel = 20; //點擊闖關模式 const int WelcomeClickMissionOne = 30; //點擊第一關 const int GameShield = 140; //護盾引導
2) parent是父節點,通常是當前的界面層。半透明層就是要掛在這個節點下。
3) widget是須要高亮顯示的控件節點。對於美女引導來講,沒有控件須要高亮顯示,因此傳入nullptr;對於第二個引導界面來講,是「闖關模式「按鈕節點。
4) callback是lambda表達式。當用戶點擊引導界面的任意處使得引導界面消失時,就會調用callback。在這個例子中,咱們使用這個參數來實現兩個引導界面連續出現。
GuideMgr::show()函數返回Guide對象指針,這是什麼鬼?咱們後面再說。
咱們再看下護盾使用引導界面的調用代碼:
void HubLayer::displayGuideShield() { Guide* pGuide = GuideMgr::getInstance()->show(GuideType::GameShield, this->getParent(), m_pBtnShield); if (pGuide) { m_pPlaneLayer->pause(); } } void HubLayer::menuShieldCallback(Ref* pSender) { if (GuideMgr::getInstance()->destory(GuideType::GameShield)) { m_pPlaneLayer->resume(); } }
輯很簡單,彈出引導界面,並暫停遊戲。點擊護盾按鈕後,銷燬引導層,恢復遊戲運行,這時,滿屏的子彈嘩啦啦的變成了寶石。
新手引導接口調用貌似看着挺簡單的,彷佛也不太會破壞原有代碼的美觀。那麼其內部邏輯是否是很複雜呢?
實現原理
新手引導實現代碼設計以下幾個類:
1) PopupLayer類:顯示半透明層,響應用戶的各類觸摸操做
2) Guide類:顯示新手引導的主邏輯
3) GuideMgr類:管理全部的新手引導Guide類
4) GuideWelcomeGirl/ GuideWelcomeClickButtonLevel/ GuideWelcomeClickMissionOne/ GuideGameShield類:繼承自Guide類,用於實現各類引導。
下面咱們逐個分析這些類。
半透明層
class PopupLayer : public LayerColor { public: PopupLayer(); ~PopupLayer(); virtual bool init(); virtual bool doInit() = 0; //touch事件監聽 屏蔽向下觸摸 virtual bool onTouchBegan(Touch *touch, Event *event) override; virtual void onTouchMoved(Touch *touch, Event *event) override; virtual void onTouchEnded(Touch* touch, Event* event) override; virtual std::function<bool(Touch*, Event*)> getTouchBeganFunc() { return CC_CALLBACK_2(PopupLayer::onTouchBegan, this); }; virtual std::function<void(Touch*, Event*)> getTouchMovedFunc() { return CC_CALLBACK_2(PopupLayer::onTouchMoved, this); } virtual std::function<void(Touch*, Event*)> getTouchEndedFunc() { return CC_CALLBACK_2(PopupLayer::onTouchEnded, this); } protected: EventListenerTouchOneByOne* m_pListener; };
實現類:
PopupLayer::PopupLayer() : m_pListener(nullptr) { } PopupLayer::~PopupLayer() { Director::getInstance()->getEventDispatcher()->removeEventListener(m_pListener); } bool PopupLayer::init() { if (!LayerColor::init()) { return false; } m_pListener = EventListenerTouchOneByOne::create(); m_pListener->setSwallowTouches(true); m_pListener->onTouchBegan = getTouchBeganFunc(); m_pListener->onTouchMoved = getTouchMovedFunc(); m_pListener->onTouchEnded = getTouchEndedFunc(); auto dispatcher = Director::getInstance()->getEventDispatcher(); dispatcher->addEventListenerWithSceneGraphPriority(m_pListener, this); setColor(Color3B(0, 0, 0)); setOpacity(160); return doInit(); } bool PopupLayer::onTouchBegan(Touch *touch, Event *event) { return true; } void PopupLayer::onTouchMoved(Touch *touch, Event *event) { } void PopupLayer::onTouchEnded(Touch* touch, Event* event) { }
上面這段代碼經過繼承LayerColor,並使用setColor(Color3B(0, 0, 0)); setOpacity(160);來實現一個半透明層的遮蓋,而具體的界面初始化和觸摸響應,則由子類來重寫doInit/onTouchBegan/ onTouchMoved/ onTouchEnded來實現。
新手引導基類
先上代碼
#define DECLARE_GUIDE(name)\ private:\ static bool mInit; \ static int mType; \ public:\ static Guide* create()\ {\ return new name(); \ }\ static bool checkCondition(); #define REGISTER_GUIDE(type, name) \ bool name::mInit = GuideMgr::getInstance()->registerFunc(type, name::checkCondition, name::create); \ int name::mType = type; class Guide : public PopupLayer { public: Guide(); public: //顯示 Guide* show(int GuideType, Node* pRoot, Node* pTarget, const std::function<void()>& callback = nullptr); //還原target void restore(); //設置文字描述 void setPrompt(const std::string& prompt); //設置玩家觸摸屏幕任意一點後的響應,例如:銷燬當前的引導並彈出新的引導 virtual void onTouchEnded(Touch *touch, Event *event) override; virtual std::function<void(Touch*, Event*)> getTouchEndedFunc() override { return CC_CALLBACK_2(Guide::onTouchEnded, this); } virtual void onEnter() override; virtual void onExit() override; protected: //在最上層建立一個GuideBg,而後把target按鈕從原節點上拿下來,添加到GuideBg上 virtual bool doInit() override; protected: int m_iGuideType; std::function<void()> m_callback; Node* m_pRoot; Node* m_pTarget; int m_targetLocalZOrder; Vec2 m_targetPos; Node* m_targetParent; ui::Text* m_pTextPrompt; std::string m_prompt; bool m_touchEnable; };
這個類中,最重要的就是doInit函數,其功能包括了:把目標按鈕從原來的父節點上摘下來,掛到半透明層上。
m_targetPos = m_pTarget->getPosition(); m_targetParent = m_pTarget->getParent(); m_targetLocalZOrder = m_pTarget->getLocalZOrder(); //Vec2 pos = m_pRoot->convertToWorldSpace(m_pTarget->getPosition()); Vec2 pos = m_pTarget->convertToWorldSpace(Vec2::ZERO); pos += m_pTarget->getAnchorPointInPoints(); //將target移到本layer上 m_pRoot->addChild(this); m_pTarget->retain(); this->addChild(m_pTarget); m_pTarget->release(); m_pTarget->setPosition(pos);
添加指示箭頭動畫
//m_pTarget的中心位置(根據錨點進行轉換) float deltaX = (0.5 - m_pTarget->getAnchorPoint().x) * m_pTarget->getAnchorPointInPoints().x / m_pTarget->getAnchorPoint().x; float deltaY = (0.5 - m_pTarget->getAnchorPoint().y) * m_pTarget->getAnchorPointInPoints().y / m_pTarget->getAnchorPoint().y; //添加指示箭頭 Sprite* pArrow = Sprite::createWithSpriteFrameName("Guide_Arrow.png"); const Size& targetSize = m_pTarget->getContentSize(); const Size& arrowSize = pArrow->getContentSize(); bool bArrowTop = true; Node* pDialogBox = nullptr; Sprite* pDialogBoxBg = nullptr; ui::Text* m_pTextPrompt = nullptr; if (m_prompt.length() > 0) { pDialogBox = Preload::getInstance()->getUI("GameUI_Dialogbox.csb"); FIND_UI_CONTROL_RE(ui::Text*, "Text_Content", m_pTextPrompt, pDialogBox); FIND_UI_CONTROL_RE(Sprite*, "GameUI_Dialogbox_Bg", pDialogBoxBg, pDialogBox); m_pTextPrompt->setTextAreaSize(Size(360, 110)); m_pTextPrompt->ignoreContentAdaptWithSize(false); } if (m_pTarget->getPositionY() < CONSTANT::DESIGN_RES_HEIGHT / 2 + 100) { //箭頭在控件的上方 bArrowTop = true; pArrow->setPosition(m_pTarget->getPosition().x + deltaX, m_pTarget->getPosition().y + targetSize.height / 2 + arrowSize.height / 2 + 20 + deltaY); if (pDialogBox) { pDialogBox->setPosition(CONSTANT::DESIGN_RES_WIDTH / 2, CONSTANT::DESIGN_RES_HEIGHT - pDialogBoxBg->getContentSize().height / 2); m_pTextPrompt->setString(m_prompt); this->addChild(pDialogBox); } } else { //箭頭在控件的下方 bArrowTop = false; pArrow->setFlippedY(true); pArrow->setPosition(m_pTarget->getPosition().x + deltaX, m_pTarget->getPosition().y - targetSize.height / 2 - arrowSize.height / 2 - 20 + deltaY); if (pDialogBox) { pDialogBox->setPosition(CONSTANT::DESIGN_RES_WIDTH / 2, pDialogBoxBg->getContentSize().height / 2); m_pTextPrompt->setString(m_prompt); this->addChild(pDialogBox); } } this->addChild(pArrow); //箭頭動畫 DelayTime* pDelay = DelayTime::create(0.4f); MoveBy* pMove1 = MoveBy::create(0.15f, Vec2(0, -10.0f)); MoveBy* pMove2 = MoveBy::create(0.15f, Vec2(0, 10.0f)); Sequence* pSeq = nullptr; if (bArrowTop) { pSeq = Sequence::create(pDelay, pMove1, pMove2, nullptr); } else { pSeq = Sequence::create(pDelay, pMove2, pMove1, nullptr); } RepeatForever* pRepeat = RepeatForever::create(pSeq); pArrow->runAction(pRepeat); m_touchEnable = false;
銷燬新手引導層,將目標按鈕掛到原來的父節點上。
//還原target void Guide::restore() { if (m_pTarget) { m_pTarget->retain(); this->getChildren().eraseObject(m_pTarget); m_targetParent->addChild(m_pTarget); m_pTarget->setLocalZOrder(m_targetLocalZOrder); m_pTarget->setPosition(m_targetPos); m_pTarget->release(); } this->removeFromParent(); }
新手引導子類
這裏咱們簡單介紹第一個美女歡迎和主菜單這兩個新手引導。
//////////////////////////////////////////////////////////////// // // 首次進入遊戲的介紹 // //////////////////////////////////////////////////////////////// class GuideWelcomeGirl : public Guide { public: DECLARE_GUIDE(GuideWelcomeGirl); public: virtual bool doInit() override; }; //////////////////////////////////////////////////////////////// // // 首次進入遊戲,點擊闖關模式 // //////////////////////////////////////////////////////////////// class GuideWelcomeClickButtonLevel : public Guide { public: DECLARE_GUIDE(GuideWelcomeClickButtonLevel); public: virtual bool doInit() override; };
//////////////////////////////////////////////////////////////// // // 首次進入遊戲的介紹 // //////////////////////////////////////////////////////////////// REGISTER_GUIDE(GuideType::WelcomeGirl, GuideWelcomeGirl); bool GuideWelcomeGirl::checkCondition() { switch (GuideConstant::GuideDisplay) { case GuideTestNoGuide: return false;//用於測試 case GuideTestNecessary: return true; //用於測試 default: return !GameData::getInstance()->checkGuide(mType) && GameData::getInstance()->getLevelFinish(Difficulty::Easy) == 0; } } bool GuideWelcomeGirl::doInit() { Guide::setPrompt(GlobalData::getInstance()->getParameter(GuideConstant::StringGuideWelcome)); if (!Guide::doInit()) { return false; } return true; } //////////////////////////////////////////////////////////////// // // 首次進入遊戲,點擊闖關模式 // //////////////////////////////////////////////////////////////// REGISTER_GUIDE(GuideType::WelcomeClickButtonLevel, GuideWelcomeClickButtonLevel); bool GuideWelcomeClickButtonLevel::checkCondition() { switch (GuideConstant::GuideDisplay) { case GuideTestNoGuide: return false; //用於測試 case GuideTestNecessary: return true; //用於測試 default: return !GameData::getInstance()->checkGuide(mType) && GameData::getInstance()->getLevelFinish(Difficulty::Easy) == 0; } } bool GuideWelcomeClickButtonLevel::doInit() { Guide::setPrompt(GlobalData::getInstance()->getParameter(GuideConstant::StringGuidePressLevelMode)); if (!Guide::doInit()) { return false; } return true; }
仔細比較這兩個新手引導代碼,最大的區別點就在checkCondition函數,也就是判斷當前的條件是否須要顯示引導。而後,設置下須要現實的文字說明,接着,沒了。
引導管理類
class GuideMgr { public: static GuideMgr* getInstance(); bool registerFunc(int type, const std::function<bool()>& checkFunc, const std::function<Guide*()>& createFunc); Guide* show(int GuideType, Node* parent, Node* widget, const std::function<void()>& callback = nullptr); bool destory(int GuideType); void incGuideCnt(); void decGuideCnt(); bool hasGuide(); protected: GuideMgr(); private: std::map<int, std::function<bool()>> m_condMap; std::map<int, std::function<Guide*()>> m_createMap; std::map<int, Guide*> m_objectMap; int m_iCnt; static GuideMgr* m_pInstance; };
GuideMgr* GuideMgr::m_pInstance = nullptr; GuideMgr* GuideMgr::getInstance() { if (!m_pInstance) { m_pInstance = new GuideMgr(); } return m_pInstance; } GuideMgr::GuideMgr() : m_iCnt(0) { } bool GuideMgr::registerFunc(int type, const std::function<bool()>& checkFunc, const std::function<Guide*()>& createFunc) { m_condMap.insert(std::map<int, std::function<bool()>>::value_type(type, checkFunc)); m_createMap.insert(std::map<int, std::function<Guide*()>>::value_type(type, createFunc)); return true; } Guide* GuideMgr::show(int guideType, Node* parent, Node* widget, const std::function<void()>& callback) { if (hasGuide()) { return nullptr; } auto itCond = m_condMap.find(guideType); auto itCreate = m_createMap.find(guideType); if (itCond != m_condMap.end() && itCreate != m_createMap.end()) { if (itCond->second()) { Guide* pGuide = itCreate->second(); pGuide->autorelease(); if (pGuide) { auto it = m_objectMap.find(guideType); if (it != m_objectMap.end()) { m_objectMap.erase(it); } m_objectMap.insert(std::map<int, Guide*>::value_type(guideType, pGuide)); return pGuide->show(guideType, parent, widget, callback); } } } return nullptr; } bool GuideMgr::destory(int GuideType) { auto it = m_objectMap.find(GuideType); if (it != m_objectMap.end()) { it->second->restore(); m_objectMap.erase(it); return true; } else { return false; } } void GuideMgr::incGuideCnt() { ++m_iCnt; } void GuideMgr::decGuideCnt() { --m_iCnt; if (m_iCnt < 0) { m_iCnt = 0; } } bool GuideMgr::hasGuide() { return m_iCnt > 0; }
沒啥好說的,一個單例,兩個map變量(m_condMap/m_createMap),經過引導類型來查找對應引導的條件判斷和建立函數,而後建立Guide,並保存到m_objectMap中。
好了,本遊戲中的新手引導相關設計介紹完了,固然這並非一個最好的設計,若是有好的設計和模式,但願能一塊兒討論。
有任何疑問可聯繫:thorqq@163.com