文档章节

仿《雷霆战机》飞行射击手游开发--新手引导

雁惊寒
 雁惊寒
发布于 2017/03/03 08:44
字数 2633
阅读 1572
收藏 34

源码库:

https://git.oschina.net/thorqq/RaidenFree

 

      传统的新手引导方式一般是设置一个全局的静态变量来保存当前新手引导进度,然后在项目中每个可能出现新手引导的位置添加一句判断:若当前新手引导步骤等于我所期望的步骤就执行引导部分的逻辑,新手引导常常逻辑杂乱,而且跟界面的逻辑常常交叉在一块,弄的不好的话代码里到处都是if else,保存各种临时状态变量。

      本文将以仿雷霆战机游戏为例,说说其中关于新手引导的设计。

游戏效果

  下面先看游戏中比较典型的两类引导

1、 菜单引导

       当用于初次启动游戏时,通过两三个步骤引导让用户快速的进入游戏,体验游戏的快感,所以,这里采用了三个步骤:

a)  大xiong妹妹出没,吸引男性玩家的眼球,引导玩家进入主菜单;

b)  引导玩家选择默认飞机,并进入关卡选择界面;

c)   引导玩家选择第一关,并进入战斗界面;

2、 游戏引导

 

  在游戏过程中,当满足一定的条件时,就暂停游戏,弹出游戏道具(上图中时护盾,可以把屏幕上的子弹全部变成宝石)的使用方法说明,并引导玩家体验道具的效果。

  新手引导的显示效果为:

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

© 著作权归作者所有

雁惊寒
粉丝 20
博文 13
码字总数 20195
作品 1
南京
程序员
私信 提问
thorqq/RaidenFree

功能介绍 本游戏是一款基于Cocos2dx开发的纵版飞行射击单机手游。玩家可以控制一架飞机与敌机进行对战,飞机可以发射子弹、导弹甚至激光,除此之外,玩家还能对自己的飞机进行强化改造,提升...

thorqq
2017/01/20
0
0
cocos2d-x支持c++、js、lua开发

作者:左文 链接:https://www.zhihu.com/question/21130385/answer/21789568 来源:知乎 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 纯属个人观点 1 Unity3d支...

壹峰
2017/10/18
200
0
写给对 ”游戏开发” 感兴趣的朋友们

我们程序员能为世界带来什么? 有些程序员做出了 淘宝/Amazon,为世界带来了电子商务。 有些程序员做出了 微信/Twitter,为世界带来了社交平台。 而有些程序员做出的东西,虽然无法应用于我们...

程序员小灰
03/05
0
0
仿雷电飞行射击手游--Raiden

游戏介绍 本游戏是一款基于Cocos2dx开发的纵版飞行射击单机手游。玩家可以控制一架飞机与敌机进行对战,飞机可以发射子弹、导弹甚至激光,除此之外,玩家还能对自己的飞机进行强化改造,提升...

雁惊寒
2017/01/20
7.9K
6
手游开发神器 cocos2d-x editor 教程聚合和代码下载(持续更新中)

一 cocos2d-x editor工具下载和基础教程: 一 手游开发神器 cocos2d-x editor初识 二 手游开发神器 cocos2d-x editor工具下载和安装配置 三 手游开发神器 cocos2d-x editor 之基础工具 inte...

makeapp628
2014/02/11
0
0

没有更多内容

加载失败,请刷新页面

加载更多

UAVStack功能上新:新增JVM监控分析工具

UAVStack推出的JVM监控分析工具提供基于页面的展现方式,以图形化的方式展示采集到的监控数据;同时提供JVM基本参数获取、内存dump、线程分析、内存分配采样和热点方法分析等功能。 引言 作为...

宜信技术学院
9分钟前
1
0
MySQL的5种时间类型的比较

日期时间类型 占用空间 日期格式 最小值 最大值 零值表示 DATETIME 8 bytes YYYY-MM-DD HH:MM:SS 1000-01-01 00:00:00 9999-12-31 23:59:59 0000-00-00 00:00:00 TIMESTAMP 4 bytes YYYY-MM......

物种起源-达尔文
16分钟前
3
0
云服务OpenAPI的7大挑战,架构师如何应对?

阿里妹导读:API 是模块或者子系统之间交互的接口定义。好的系统架构离不开好的 API 设计,而一个设计不够完善的 API 则注定会导致系统的后续发展和维护非常困难。比较好的API设计样板可以参...

阿里云官方博客
19分钟前
1
0
Rancher + VMware PKS实现全球数百站点的边缘K8S集群管理

Sovereign Systems是一家成立于2007年的技术咨询公司,帮助客户将传统数据中心技术和应用程序转换为更高效的、基于云的技术平台,以更好地应对业务挑战。曾连续3年提名CRN,并且在2012年到2...

RancherLabs
24分钟前
2
0
6、根据坐标,判断该坐标是否在地图区域范围内

最近在写配送区域相关的代码,具体需求如下: 根据腾讯地图划分配送区域,总站下边设多个配送分站,然后将订单中的收货地址将其分配给不同的配送分站。 1、地图区域划分(腾讯地图) 1.1、H...

有一个小阿飞
26分钟前
2
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部