博客文章

如何完成一个比较完善的新手引导

作者: andy.      时间: 2020-06-01 01:48:03

    星期天想了一天,才完成这么一个引导。一直想抽时间写下来,但是最近实在太忙了。第一次用这么大的一个标题,谈谈对新手引导这个模块的一点儿看法和实现思路。里面的伪代码都以Cocos Creator为准,同样的,Cocos2dx也是一样的。主要还是思路,

    看看新手引导需要完成的内容:

    1、屏蔽层和半透明屏蔽层。

    2、新手引导聚焦(指定按钮等等)的位置需要镂空。

    3、可以完成几种情况的点击:1、点击指定位置跳转到下一个引导。2、点击屏幕任意位置跳转到下一步。3、文字引导。

    4、同一按钮可能需要点击多次才跳转到下一步步骤。

    5、需要某个界面打开后(也可以是等待网络消息等)才能开始新手引导步骤。

    6、所有上述信息可以配置相应配置表,相应配置表修改后不需要修改代码。


    那么构造我们撸代码的大概思路(略过不需要新手引导,跳过新手引导等等):

            ->开始一个步骤新手引导(可能从最开始,也可能是从某个步骤接着引导)。

            ->是否需要等待界面打开(如果需要的话,等待界面打开,期间屏蔽所有操作)。

            ->开始该步骤。

            ->渲染屏蔽层(半透明的话,镂空位置等等)。

            ->是否弹出文字提示。

            ->监听玩家点击和点击数量。

            ->此步骤引导结束。

            ->开始下一步骤新手引导。

    思路非常的清晰,为了应对这些操作,我们首先需要设计新手引导的配置表。根据上面的叙述,每一步骤的新手引导需要的信息如下:

    1、Id 引导Id。

    2、NextId 下一个引导的Id。

    3、RestartId 中断后重新开始的Id。

    4、Interrupt 点击的方式:不屏蔽所有的操作,全屏点击触发,只允许玩家点击监控的控件。

    5、Language 如果有文本,文本内容。

    6、TriggerNeedOperate 引导需要的条件,比如:某个界面打开。

    7、StartOperate 如果需要第六个字段,那么,由事件来触发这个引导开始。

    8、MonitorOperate 监控的操作。这个字段表达,玩家需要点击哪个按钮、滑动屏幕还是怎么的。预设定义在程序代码中,如果后面修改的引导还需要这个步骤的话,不修改代码也可以完成引导的修改。

    9、IsMaskScreen 是否显示遮罩。比如某些引导不显示遮罩,弹出一段话,点击后,引导结束。

    10、ClickTimesContinue 点击多少次后开始下个引导。比如某些挂机点点点的操作。

    好了,准备工作都做完了,比如我们游戏的开始界面上面有两个按钮:

    

image.png

    我们要点击完按钮1后点击按钮2。我们就可以加入2条配置:

{
    1: { Id: 1, NextId: "2", ReStartId: 2, Interrupt: 2, Language: "点击 button1", TriggerNeedOperate: "-1", StartOperate: "-1", MonitorOperate: "ClickButton1", IsMaskScreen: true, ClickTimesContinue: 2,  },
    2: { Id: 2, NextId: "3", ReStartId: 3, Interrupt: 2, Language: "点击 button2", TriggerNeedOperate: "-1", StartOperate: "-1", MonitorOperate: "ClickButton2", IsMaskScreen: true, ClickTimesContinue: 1,  },
}

    开始引导效果:

image.png

    我们有一个GuideManager管理整个引导过程,在引导过程中,有一个GuideNode盖在整个界面上面。开始引导的时候,如果不是中断开始或者引导已经完成了,就读取第一条引导配置开始执行逻辑。

StartGuide() {
    if (!this.currentNoviceConfig || this.currentNoviceConfig.StartOperate == "-1")
        this.StartNoviceGuide();
    else {
        // 移除黑色遮罩
        this.noviceNodeComponent.RemoveWordText((() => {
            if (this.currentNoviceConfig.TriggerNeedOperate != -1)
                // 发送事件让相应操作完成
        }).bind(this));
    }
}
StartNoviceGuide() {
    if (!this.currentNoviceConfig) {
        this.noviceNode.active = false;
        return;
    }
    this.noviceNode.active = true;
    this.noviceNodeComponent.SetShieldAllTouchs(true);
    this.noviceNodeComponent.SetNoviceGuideConfig(this.currentNoviceConfig);
    // 显示文字
    this.noviceNodeComponent.ShowWordText((() => {

    }).bind(this));
    // 显示遮罩
    this.noviceNodeComponent.ShowMask();
}

    第一条配置需要把button1突出显示出来(对于形状不规则的图形突出显示效果还不错),实现思路:监控的操作是ClickButton1,所以我们可以在代码里面配置:ClickButton1 = { WatchNode: "Canvas/button1", };这里表示监控的是button1这个node。这一条配置的好处是在于如果后面的引导还会引导点击按钮1的话,在配置表里面直接配置监控的操作就行,不用修改代码了。这里就比较简单了:我们用一个FrameBuffer来渲染一张贴图(Opengl里面术语,不知WebGL里面叫什么,就直接用了)。1、Clear ColorBuffer。2、我们将button1克隆一个放在引导的面板上面。3、设置克隆后的按钮混合模式,sfactor的混合模式为GL_ZERO,dfactor的混合模式为GL_ONE_MINUS_SRC_COLOR。    

    这样克隆按钮位置为Zero,就不会显示,因为克隆按钮的alpha值是255,dfactor也不会显示,这个位置就空了一块。实现镂空的效果。显示出来。大概代码:

var nodeWatchNode = cc.find(operationConfig.WatchNode, cc.director.getScene()); // operationConfig就是ClickButton1 = { WatchNode: "Canvas/button1", };
nodeWatchNode = cc.instantiate(nodeTarget);
nodeWatchNode.active = true;
nodeWatchNode.getComponent(cc.Sprite).srcBlendFactor = cc.macro.BlendFactor.ZERO;
nodeWatchNode.getComponent(cc.Sprite).dstBlendFactor = cc.macro.BlendFactor.ONE_MINUS_SRC_ALPHA;

    在引导node上面监听点击:

// 这里的operationConfig同上面。
var nodeWatchNode = cc.find(operationConfig.WatchNode, cc.director.getScene());
var touchPosition = node.parent.convertToNodeSpaceAR(touchPosition);
if (!nodeWatchNode.getBoundingBox().contains(touchPosition)) {
    this.node.nodeShield._touchListener.setSwallowTouches(true);
    return;
}
this.node.nodeShield._touchListener.setSwallowTouches(false);

    没点中就把点击吞噬掉就OK了。

    最后在按钮的点击回调中告知guideManager这个点击完成了:

noviceGuideManager.CompleteOperate("ClickButton1");
// 下面部分为guideManager
CompleteOperate(operateKey) {
    if (!this.currentNoviceConfig)
        return;
    if (this.currentNoviceConfig.MonitorOperate == operateKey)
        this.TriggerCurrentOperation();
}
TriggerCurrentOperation() {
    ++this.noviceClickTimes;
    if (this.noviceClickTimes >= this.currentNoviceConfig.ClickTimesContinue) {
        this.noviceClickTimes = 0;
        this.EndCurrentGuideStep();
    }
}
EndCurrentGuideStep() {
    this.noviceNodeComponent.SetShieldAllTouchs(true);

    // 这里需要保存当前进度
    
    var nextNoviceConfig = NoviceGuideConfig[this.currentNoviceConfig.NextId];
    var removeCallBack = function() {
        this.currentNoviceConfig = nextNoviceConfig;
        this.StartGuide();
    }.bind(this);
    if (!nextNoviceConfig || nextNoviceConfig.Language != null)
        this.noviceNodeComponent.RemoveWordText(removeCallBack);
    else
        removeCallBack();
}

    接着就开始下一步骤的引导。

    需要界面打开和这里的Compete引导类似。

    xiuxile