新手引导_新手引导页 - CSDN
精华内容
参与话题
  • 早期的项目中晓衡遇到游戏终于要完成了,辛苦了一阵满以为可以稍微放松一下了,但策划、运营要求,增加一个他们认为非常“简单”且重要的功能:新手引导。 回想起当年,接到这个任务时的感觉是手脚冒汗、天晕地暗、...

    早期的项目中晓衡遇到游戏终于要完成了,辛苦了一阵满以为可以稍微放松一下了,但策划、运营要求,增加一个他们认为非常“简单”且重要的功能:新手引导

    回想起当年,接到这个任务时的感觉是手脚冒汗、天晕地暗、日月无光,游戏代码本来就千疮面孔,逻辑错综复杂,根本不知道该怎么下手?更困难的是,游戏本身功能和需求还不稳定,老板随便一个想法可能就会被改、改、改...,我该怎么办?

    在这种情艰难的情况下一定要,需要保持冷静,在痛定思痛之后,我开始了引导功能的开发,在做的过程中不断积累,编写了一套配置式、可编程的引导框架,然后交给其他开发人员或策划人员做具体的引导内容,真的是:“杀不死你的会使你更强大”

    实现新手引导的困难

    通常实现新手引导的困难在于,它与当前需求、功能密切相关,而且稍有不甚连正常流程都走不通,下面一起 看看新手引导到底有那些痛点。

    开发中的痛点

    1. 需要在正常流程中插入引导代码,干扰流程;
    2. 引导代码的增加会影响原有代码逻辑,增加维护难和成本;
    3. 界面或需求的变化会导致引导功能大幅修改,甚至重新制作;
    4. 手指提示对应的矩形区定位麻烦,不能简单使用固定的位置和矩形大小;
    5. 编写引导的代码也很困难,需要策划—程序之间高度配合。

    期望的编程体验

    在了解到传统的引导制作过程中的难点与弊端后,一直在思考没有更好的实现方式,我心中的引导功能的编程方式希望有以下几点:

    1. 引导功能代码,不能混入正常游戏逻辑代码中,后患无穷,应尽量分离;(难以忍受优雅的代码被无情的打乱,更难忍受糟糕的代码被弄的支离破碎)
    2. 界面只发生简单的UI位移、Size大小、节点层次的调整,不需要修改具引导代码;
    3. 定位UI指引矩形区域应尽可能简单,能自适应不同的屏幕尺寸;
    4. 最好能做到策划人员都可以来制作部分流程引导;
    5. 在引导需求明确、游戏功能正常的情况下,制作一个常规的引导步骤应该非常快捷。

    下面是我使用Cocos Creator 官方 demo-ui 工程上嵌入的引导案例演示:

    demo体验地址:
    http://game.ixuexie.com/godGuide

    这里有一个视频演示:
    https://www.bilibili.com/video/av60001770/

    框架要点

    演示中的引导操作,是使用下面JSON配置进行控制:

    module.exports = {
        name: '进入商店',
        debug: true,
        autorun: true,
        steps: [
            {
                desc: '文本提示',
                command: { cmd: 'text', args: ['欢迎体验Shawn的新手引导框架', '本案例演示:\n1.文本提示指令\n2.手指定位指令\n3.自动执行引导\n4.点击操作录像', '首先,请点击首页按钮'] },
            },
    
            {
                desc: '点击主界面主页按钮',
                command: { cmd: 'finger', args: 'Home > main_btns > btn_home'},
                delayTime: 0.5,
            },
    
            {
                desc: '文本提示',
                command: { cmd: 'text', args:  '点击主界面设置按钮' }
            },
    
            {
                desc: '点击主界面设置按钮',
                command: { cmd: 'finger', args: 'Home > main_btns > btn_setting'},
            },
    
            {
                desc: '文本提示',
                command: { cmd: 'text', args: '点击主界面商店按钮' }
            },
    }

    配置中的重点是 steps 数组项目,其中的 desc 是引导步骤的描述,主要用于调试,command是引导指令,这里实现的是一个手指指示指令:finger, 后面的args是指令参数,借助CSS中的选择器概念,我这里简单实现了一个节点获取的方法,称之为:定位器

    定位器

    点定位器的概念,其实它非常简单,如下图所示:

    你可能会想到,引擎提供的 cc.find 就搞定,代码如下:

    cc.find('Canvas/Home/lower/main_btns/layout/btn_home')

    节点路径字符串可以精确定位到 btn_home 节点,但在实际使用中时会感觉很繁琐:

    1. 字符串太长,容易出错;
    2. 节点名字、层级变化,节点路径字符串就失效了,容易被误伤。

    为了使路径表达更简洁可靠,笔者引入了两个定位符号:

    /: 右斜杠,代表1级子节点(与cc.find相同)
    >: 大于符号,表示1~n级子节点

    可以将上面btn_home节点的定位符改为

    godGuide.find('Canvas > btn_home');

    如果我们默认从Canvas节点开始检索,也可以直接写成下面这样:

    godGuide.find('btn_home');

    这样将从 Canvas 节点一层层开始遍历,想提高检索节点的效率可以改为:

    godGuide.find('Home > main_btns > btn_home');

    如果场景中有同名节点,也可以使用 '>'符号解决,但同一层级不能有同名节点(如果你需要检查的话)。

    自动引导

    引导的测试工作效率低下,既然有了可配置的引导,能否让它自动去执行呢?看下面视频:
    www.wityx.com

    https://v.qq.com/x/page/v3017l51xep.html

    晓衡最早只是在浏览器上实现了鼠标的点击模拟,后来扩展到了原生App上也可以使用。 自动引导,可以方便对引导流程的测试和验证。

    流程录制

    引导的核心是获取目标节点,我们是通过手写节点定位器(一种简化的节点路径表达方式)获取节点。如果实现一个功能,记录下我们点击的节点路径,是否可以实现自动生成引导流程呢?然后再让它自动播放出来?

    结语

    新手引导框架已经开源,并且支持最新版本的 Cocos Creator 2.2.0 下,Github仓库地址献上:
    https://github.com/ShawnZhang2015/GodGuide

    原创不易,如果觉得有帮助,请点个赞吧!

    展开全文
  • 新手引导

    2015-10-29 01:27:16
    转载自:http://www.cnblogs.com/marsz/p/4685522.html 前言... 2 版本... 2 ...作者......功能......类型......触发类型......步骤类型......实现......简要......策划方面......程序方面......流程图......详细技术方案

    转载自:http://www.cnblogs.com/marsz/p/4685522.html

    前言... 2

    版本... 2

    作者... 2

    功能... 2

    类型... 2

    触发类型... 2

    步骤类型... 3

    实现... 4

    简要... 4

    策划方面... 4

    程序方面... 4

    流程图... 5

    详细技术方案... 6

    程序主要逻辑... 6

    关键细节答疑... 6

     

     

     

     附:word版百度云盘下载

    http://pan.baidu.com/s/1DbNxs

    前言

    本文档描述Unity3d下支持策划灵活配置、多样性丰富的新手指引的相关说明,如有设计上不明白的地方,可登陆游戏(《魔霸西游》)体验。技术上有疑问的地方,可联系作者MarsZ解答,QQ569491198。

    2015年7月28日

    版本

    V1.2( 2015年7月29日11:33:09)

    作者

    MarsZ (QQ569491198)

     

    功能

    在指定任意条件下,触发某对应指引,指示玩家执行某相关操作(如点击、拖动或播放某指令)。指引期间除执行指定操作外,无法执行其他无关操作(如点击无关地方)。所有指引由策划灵活配置,包括什么时候触发、触发什么指引等。

    类型

    触发类型

    指引触发条件(主条件为可能触发指引的操作,副条件为判断指引能否触发时的附加判断条件)
    格式: type,value
    =====只为主条件, 后端用=====
    1: 完成某指引后,主界面触发 base_guide.guide_id
    2: 首通某关卡后,主界面触发 base_scene.scene_id
    =====只为主条件=====
    5: 关卡对话结束(关卡id#1,战胜;2,战败;3,胜或败;4,战前)
    6: 点击操作(界面#目标#索引 0无索引)
    7: 拖动操作(界面#目标1#索引1#目标2#索引2 0无索引)
    11:任意仙魔血量低于X(%) (只用于线性)
    25:章节领奖(关卡id#0,该关卡;1,除了该关卡)
    =====可为副条件=====
    10:进入区域(关卡id#区域12345)
    14:战斗&回合(关卡id # 1胜;2败;3超时;4战败 # 第n次)
    15:战斗&回合(除了关卡id # 1胜;2败;3超时;4战败 # 第n次)
    16:战前配置,已上阵X个仙魔 (不包括主角)
    20:主角的第1个激活属性激活了(1)(1不可修改)
    21:主角提升到了N级(N)
    =====只为副条件:
    12:检测任务状态(任务id#0,未完成;1,完成未领;2,完成已领;3,未触发;4,完成)
    17:检测指引状态(指引id#0,未完成;1,完成)
    18:检测PVP状态(界面#0,未开始;1,开始)
    22:检测战斗状态(关卡id#0,非战斗中;1,战斗中)
    23:检测敌方仙魔总血量高于百分比(关卡id#XX)
    24:检测模块是否解锁(模块id#0,未解锁;1,解锁;)

    ……

     

    步骤类型

    0: 纯对话
    1: 箭头指示
    2: 手指点击
    3: 手指拖动
    4: 手指双击
    7: 功能解锁/锁定
    9: 战斗暂停/继续
    10:发送仙魔位置
    11:对错提示
    12:播放解锁动画
    13:移到主界位置
    14:战斗界面动画
    15:切换头像
    16:点击移动
    17:箭头文本
    18:至某界面
    20:滑动地图

    ……

    实现

    简要

    策划方面

    策划主要通过配置2个表来实现对指引的设置和控制,分别为:

    base_guide表以及base_guide_step表。表结构分别为:

     

    图1 base_guide表结构

     

    图2 base_guide_step 表结构

    程序方面

    程序读取配置表,当用户执行某些特定类型的操作,如点击、拖动或接收/完成任务,通关某关卡等时,检测相关操作能满足哪个指引组的执行条件(如是否点击了对应的指引物体、是否拖动了对应的物体到对应的位置、是否接受/完成了对应的任务、是否通关了对应的关卡)。如满足某指引组执行条件,并且当前能执行指引(如不在加载界面,界面不在缓动等),则执行对应的指引组(base_guide表的guide_id的指引)。如不满足,则指引暂时结束。

    执行完某指引组的当前步骤的指引(base_guide_step表的guide_step_id的指引)后,判断是否已执行完当前指引组的所有指引步骤,如未指引完,则执行该指引组中的下一个指引步骤,否则判断是否能触发下个指引组。

    流程图

     

    图3 流程图

    详细技术方案

    程序主要逻辑

    1、根据策划配置的指引触发类型,设置对应的指引触发类型常量;

    2、根据策划配置的指引步骤类型,设置对应的指引步骤常量,设计相应的指引步骤界面(如需要界面,如点击和拖动等需要手指播放界面。如播放某动画需要全屏遮罩等);

    3、打开界面时:

     i、根据策划配置,检测该界面是否有能触发指引的GameObject,如有,给这些GameObject绑定GuideTriggerGameObject脚本,用于截获能触发指引的操作,如点击(OnClick)等。截获到操作时,发送触发指引消息。指引控制器检测该操作能否触发指引组,如可以,指引之。

     ii、根据策划配置,检测该界面是否有能执行指引的GameObject,如有,给这些GameObject绑定GuideGameObject脚本。当该GameObject正处于绑定的指引步骤执行中时,调整该GameObject的Layer和Depth等,让该GameObject可以接受点击或其他对应的指引操作。以及独立于其他显示单元进行高亮(如需要)。

    4、指引步骤执行完毕后,发送消息给控制器,控制器收到后,检测当前指引组是否还有下一个指引步骤,如有,继续执行下一个指引步骤,直至指引完该指引组的全部指引步骤。

    5、指引组的步骤全部指引完毕后,检测是否能触发下一个指引组,如能,执行之,否则指引暂停。

    关键细节答疑

    Q:关于指引时只让对应的指引物体可以接受点击、拖动等事件或者高亮。

    A:指引时全屏遮罩一个指引layer(层级高于普通GameObject的层级)的蒙版,屏蔽所有无关的GameObject,对应正处于指引步骤中的GameObject,调整其层级到layer层,调整其depth以满足深度合适(必要时可以借助UIPanel来设置depth),让其可以接受事件。

     

    Q:关于点击具有某属性值的物体触发特定指引。如点击某类型的某ID值的卡牌能触发特定指引。

    A:打开界面时,给能触发指引步骤的物体绑定GuideGOInfo(继承自MonoBehaviour)脚本,脚本上附上相关的属性值(这些属性在物体上的信息更新时也要即时更新)。当物体遇到触发条件时(如被点击等),检测上方的属性时是否也满足某些指引的触发条件,如果一切条件都满足,即可触发对应指引。

     

    Q:关于“播放动画”类型的指引步骤的执行方案。

    A:对于这类型的指引步骤,当触发时,发送消息事件给外部,请求“播放动画”或其他操作。当这类操作完毕时,发送消息事件给指引,告知指引步骤完毕,让指引继续执行即可。

     

    Q:关于断线重连后指引的继续。

    A:关键指引步骤会发送消息给后端(见base_guide_step表的to_server

    字段),请求后端保存该指引组和指引步骤。断线重连后,结合后端保存的指引组ID和指引步骤,计算出当前应该处于的指引组和指引步骤,执行之。如果该步骤是某界面中执行的,则先打开对应界面再执行指引步骤即可。

     

    Q:关于指引间隙误操作。

    A:要有个全局的指引遮罩蒙版,指引间隙间(如2步骤间等)让蒙版的作用。指引继续执行时或者指引结束后去掉该蒙版。

     

    Q:某GameObject如何知道自己处于当前指引步骤。

    A:该GameObject身上会有GuideGameObject脚本,继承自MonoBehaviour,且脚本中绑定有与本GameObject关联的指引组和指引步骤列表。当Update执行时,会判断其绑定的指引组和指引步骤列表中,是否有当前正在执行的指引组和指引步骤,如果有,则为正在执行与自身有关的指引。

     

    Q:如何知道某GameObject绑定有什么指引。

    A:在界面初始化时及界面中可能触发指引的GameObject变化时,发送一个重新更新绑定GuideGameObjectGuideTriggerGameObject的事件。收到事件后,循环遍历本界面GameObject上的所有Children GameObject,判断其名字和索引是否对应到了base_guide_stepbase_guide中,如果有,则绑定GuideGameObjectGuideTriggerGameObject脚本,并且更新脚本里存储的指引组和指引步骤列表。



    展开全文
  • 游戏新手引导的制作原理(上)

    千次阅读 2014-08-26 11:55:37
    任务描述:了解游戏中新手的制作原理及流程 难度:3   本章源码下载:http://www.iamsevent.com/zb_users/UPLOAD/GuideManager/Test1.zip   有人问我,都两年过去了,AS3 Cod

    http://www.iamsevent.com/post/58.html




    使用框架:AS3
    任务描述:了解
    游戏中新手的制作原理及流程

    难度:3

     

    本章源码下载:http://www.iamsevent.com/zb_users/UPLOAD/GuideManager/Test1.zip

     

    有人问我,都两年过去了,AS3 Coder系列怎么才出了10篇文章都不到?答案很简单:我TM懒得写!原计划出到10篇就洗手不写了,现在还有最后两篇,加把劲冲刺一下吧!

    新手引导基本上在每个游戏中都会出现,或长或短,或简单或复杂,当然,新手引导流程越长越容易出现BUG,且传统的新手引导做法会极大地破坏代码的耦合性,为了解决“不稳定”及“破坏耦合性”这两个问题,贫道想了一种相对较好一点的(到底是好还是不好,列位看完本文之后就仁者见仁智者见智了)方式,在本文中介绍给大家。

     

    传统的新手引导制作方式

    传统的新手引导方式一般是设置一个全局的静态变量来保存当前新手引导进度,然后在项目中每个可能出现新手引导的位置添加一句判断:若当前新手引导步骤等于我所期望的步骤就执行引导部分的逻辑。例如,一个游戏中的新手引导第四步是引导用户去打开一个A窗口,然后第五步引导则是引导用户点击A窗口中的某个按钮。那么在A窗口的“打开窗口”函数中就会加上对当前新手引导步骤的一个判断,若当前步骤等于5,就执行相应的新手引导逻辑(例如,给A中需要引导用户点击的按钮加上一个箭头神马的)

    public function onOpen():void

    {   

     if( GuideManager.currentStep == 5 )//GuideManager.currentStep是一个静态变量,用于存储当前新手引导进行到的步骤   

     {     //执行相应的引导逻辑   }

    }

    这种做法的弊端在于,它破坏了代码的耦合性,因为新手引导每涉及到一个组件,就需要在该组件中添加相应的判断语句及引导逻辑。而且当新手引导的步骤一多之后就会出现不稳定的情况,最烦人的是,一旦策划要求你在新手引导中插入几个步骤,那你几乎全部的涉及到新手引导的组件都会遭殃(原先if语句里的判断条件都会发生变动),而且你涉案组件那么多,难免会漏改几个位置。

     

    基于接口的编程——降低耦合度的最佳方式

    为了降低新手引导对项目耦合度的破坏性,有人提出了使用“继承”的方式,即游戏中的全部可视化组件都继承于同一个父类,然后在这个父类里面写上新手引导相关的逻辑代码,这样就可以一劳永逸了。不过话说回来,不一定全部涉及到新手引导的类都是继承自你这个共同的父类,而且使用你这种继承的方式来做,就会让全部继承自该共同父类的那些子类里面多出很多冗余的代码(因为只有极少的一部分子类会涉及到新手引导),尤其是在未启动新手引导时(玩家已经完成全部新手引导之后每次登陆游戏都是不会启动新手引导的)产生极大的浪费。

    使用“接口”(Interface)的编程思想来做,就可以仅给有需要出现的新手引导的类添加相应的代码,最大限度地避免浪费的产生。

    所谓“接口”的作用,就是让没有继承关系的类之间产生联系。让我们先来看一个小例子吧。现在我们有两个窗口类A与B,它们之间没有继承关系,虽然如此,但它们还是有共同之处的,就是都声明了一个名为“open”的打开窗口的方法,当执行该方法时,A/B窗口就会被打开。现在我想创建一个格式如下的方法:

    public function openWindow( win:* ):void

    {   

        win.open();

    }

    使用openWindow方法,我可以快速地打开一个窗口,而A和B类的对象都有可能被当做参数传入该方法中,但是由于A、B两个类之间没有共同的父类,所以我openWindow的参数类型只能写成通配符(*)。当然,使用通配符是存在隐患的,因为传入的参数很有可能不具备一个名为open的public方法,这样的话就会发生报错。

    为了将A、B联系起来,我们此时可以声明一个接口,该接口中声明了A、B类中所拥有的那些个同名函数和属性(在该例中A、B所拥有的同名函数只有一个open方法):

    public interface IWindow

    {

        /** 执行打开窗口逻辑 */

        function open():void;

    }

    声明完该接口之后,需要让A、B实现该接口:

    public class A extends AP implements IWindow

    {    

        public function open():void    

        {    

             //do something    

        }

    }

    //--------------------------------------------------------------//

    public class B extends BP implements IWindow

    {    

        public function open():void    

        {     

            //do something    

        }

    }

    现在,A、B类中的open方法都是对于接口IWindow的一个实现。之后,不论一个对象是A类型还是B类型,我们都可以使用IWindow的类型来引用它,这样的话,我们之前定义的openWindow方法就可以改成:

    public function openWindow( win:IWindow ):void

    {   

        win.open();

    }

    参数win的类型不再是全部类型(*),而是将范围缩小到了所有实现了IWindow接口的对象,由于所有实现了IWindow接口的对象中都一定会有open方法,所以我们可以放心大胆地调用win.open()而不必担心再报错了。

      “接口”不同于“继承”,继承必须按照从上至下的层级顺序,且一个类只能于一个父类,但接口却不同,一个类可以实现多个接口,如下类就同时实现了两个接口:

    public class A extends AP implements IWindow, IFucker

    {    

        //class body

    }

    正是因为接口的这个特性,使我们“仅给需要类添加代码”的假设成为了可能。下面,我们就来写一个接口,该接口约定了一些新手引导过程中将会用到的方法和属性:

     /** * 如果某个面板将在新手引导中出现,那么它必须实现该接口  * @author S_eVent *  */public interface IGuideComponent{/** 处理新手引导相关事务 * @param data执行当前步骤时需用到的数据 */function guideProcess(data:Object=null):void;/** 执行新手引导卸载相关清理工作 */function guideClear():void;/** 注册到GuideManager中的实例名 */function get instanceName():String;function set instanceName(value:String):void;}

    该接口定义了两个方法(guideProcess及guideClear)和一个属性(instanceName)。在新手引导过程中会用到的类,都需要实现该接口才可以。使用基于接口的编程的好处有以下三点:

    1.避免冗余代码的产生,哪个类需要实现新手引导的功能就让哪个类来实现该接口;

    2.方便查找:你只要在项目中搜索该接口的引用位置,就可以一次性找全全部涉及到新手引导的类;

    3.方便管理:全部与新手引导有关的逻辑代码都存放于名为guideProcess的函数中,某组件涉及到的新手引导步骤执行完毕后的清理工作都放在guideClear函数中。

     

    新手引导管理器

    为了便于管理和查询新手引导各步骤,我创建了一张XML表guide.xml用于记录新手引导的各个步骤,其格式如下:

    <?xml version="1.0" encoding="utf-8"?>
    <guide>
    <!-- Author:S_eVent
    说明:节点名请保证使用step,否则将不被识别。
    每个节点中的必须属性如下:
     sequence:显示此步骤出现的次序
     instanceName:此步骤所关联的实例名称
    每个节点中的可选属性如下:
     subSeq:子步骤。某些界面可能会涉及到多次引导步骤,在每次步骤时执行的逻辑都不一样。此时用该属性来识别当前步骤该干嘛
     -->
     
     <step sequence="0" instanceName="ButtomButtonBar" subSeq="1"/>
     <step sequence="1" instanceName="Window1" subSeq="1"/>
     <step sequence="2" instanceName="Window1" subSeq="2"/>
     <step sequence="3" instanceName="ButtomButtonBar" subSeq="2"/>
     
    </guide>

    sequence属性用以标示新手引导的步骤号;

    instanceName则表示负责展示该步引导的实例名;

    subSeq用于区分同一个组件展示出来的两个不同步骤。比如在上面的XML里面,“Window1”这个实例将负责展示步骤1和步骤2的新手引导,展示步骤1时,“Window1”这个窗口将引导用户点击窗口中的某个功能按钮(比如在该功能按钮上加一个箭头),而展示步骤2时,“Window1”就将引导用户点击窗口右上角的关闭按钮来关闭窗口。subSeq属性将会被传入"Window1"类的guideProcess方法中用于区分当前引导步骤应该执行哪个引导动作(是该引导用户点击窗口中某个功能按钮还是关闭按钮)。

     

    在某个用户登录游戏时,服务器端会判断该用户是否需要进行新手引导,若该用户需要进行新手引导,那么咱们Flash前端就需要加载guide.xml以获取新手引导的步骤数据。实现代码如下:

    private function onGameStart( e:Event ):void

    {

        if( _needGuide )

            loadGuideXML();

    }

    private function onResize( e:Event ):void

    {

        _globalVariables.stageWidth = stage.stageWidth;

        _globalVariables.stageHeight = stage.stageHeight;

    }

    private function loadGuideXML():void

    {

        var loader:URLLoader = new URLLoader();

        loader.addEventListener(Event.COMPLETE, onGuideXMLLoadComp);

        loader.load( new URLRequest("guide.xml") );

    }

    private function onGuideXMLLoadComp(e:Event):void

    {

        var data:XML = XML( (e.currentTarget as URLLoader).data );

        var guideData:Array = [];

        for each(var x:XML in data..step)

        {

            guideData.push( xml2Object(x) );

        }

        guideData.sortOn("sequence", Array.NUMERIC);

        function xml2Object( xml:XML ):Object

        {

            var obj:Object = {};

            var attributes:XMLList = xml.attributes();

            for each(var a:XML in attributes)

            {

                obj[a.name().toString()] = a.toString();

            }

            return obj;

        }

        GuideManager.setUp( guideData );

        GuideManager.start();

    }

     加载完guide.xml之后我们需要将XML中的每一个标签都转换成相应的Object对象便于使用,最后把全部步骤对象放进一个数组中传递给我们接下来要介绍的新手引导管理器GuideManager类的setUp方法进行新手引导的启动工作,稍后,在需要开始新手引导时调用GuideManager.start方法开始新手引导的播放。

    GuideManager类负责调度新手引导的暂停与播放,它提供了一系列static的静态方法,在项目中任意位置都可以调用到这些方法。

    /** 
     * 新手引导管理器。请确保只有需要进入新手引导时才调用其setUp方法。
     * @author S_eVent
     */ 

    public class GuideManager 

     /** 指示符容器。高亮边框、引导指针等指示符都会被添加于此容器之上。若不设置值,则无法显示指示符 */
     public static var stage:Stage;
     /** 新手引导完成一个步骤之后执行函数。此函数需接受一个Object型参数,代表当前完成步骤的配置数据 */
     public static var onStepFinish:Function;
     /** 新手引导播放完成后执行函数 */
     public static var onGuideFinish:Function;
     
     /** 注册成员地图。格式为{className1:IGuideComponent, className2:IGuideComponent, ......} */
     private static var _memberMap:Object = {};
     
     /** 新手引导播放队列,其中元素为每一步的实例 */
     private static var _guideQueue:Vector.<IGuideComponent>;
     
     private static var _isSetUp:Boolean = false;

     /** 当前执行的步骤索引 */
     private static var _currentStep:int=-1;
     
     /** 下一个将执行的步骤索引 */
     private static var _nextStep:int=0;
     
     /** 记录新手引导具体步骤的数组。其中元素为每一步的实例名 */
     private static var _sequenceArray:Array;
     
     /** 记录新手引导每步所包含数据的数组 */
     private static var _dataArray:Array;
     
     /** 完成步骤列表。键为步骤序号,值为true/false,表示是否完成 */
     private static var _finishList:Object;
     
     private static var _paused:Boolean;
     
     /** 存储一切当前使用的遮罩对象 */
     private static var _maskHome:Object = {};
     private static var _border:Shape;
     
     /** 启动新手引导 */
     public static function setUp( config:Array ):void
     {
      if( _isSetUp == false )
      {
       _isSetUp = true;
       _sequenceArray = [];
       _dataArray = [];
       _finishList = {};
       var len:int = config.length;
       
       for(var i:int=0; i<len; i++)
       {
    _sequenceArray[i] = config[i].instanceName.toString();
    _dataArray[i] = config[i];
       }
       
       _guideQueue = new Vector.<IGuideComponent>();
       for(i=0; i<len; i++)
       {
    _guideQueue[i] = _memberMap[_sequenceArray[i]];
       }
      }
     }
     
     /** 卸载新手引导 */
     public static function uninstall():void
     {
      if( _isSetUp )
      {
       _isSetUp = false;
       if( _currentStep >= 0 )
    doClear(_guideQueue[_currentStep]);
       _guideQueue = null;
       _currentStep = -1;
      }
     }
     
     /**
      * 注册一个 IGuideComponent 到GuideManager中,这样它就会出现在新手引导过程中。
      * GuideManager会根据注册对象的instanceName来注册对象类名。若注册时发现instanceName
      * 已被注册,则不执行接下来的注册过程
      * @param instance  欲注册对象
      * 
      */  
     public static function register(instance:IGuideComponent):void
     {
      if( instance )
      {
       var name:String = instance.instanceName;
    if( _memberMap[name] )
    {
     return;
    }
    _memberMap[name] = instance;

    //注册的时候若是发现新手引导已经启动,则搜索当前注册对象是否是新手引导的其中
    //一个步骤,若是,则加入到引导队列中
    if( _isSetUp )
    {
     var index:int = _sequenceArray.indexOf(name);
     while( index != -1 )
     {
      _guideQueue[index] = instance;
      //有时候,两个相邻步骤间会存在时间差。如步骤1执行完毕后调用nextStep发现步骤2
      //尚未注册,此时会导致GuideManager暂停运作,那么就等待步骤2在注册时重新启动
      //GuideManager的播放
      if( _nextStep == index )
      {
       nextStep(index);
      }
      
       index = _sequenceArray.indexOf(name, index+1);
     }
    }
      }
     }
     
     /** 开始新手引导
      * @param from 从第几部开始 */
     public static function start(from:uint=0):void
     {
      nextStep(from);
     }
     
     /**
      * 进行下一步引导 
      * @param designedStep 跳到指定的步骤。若该值为-1,则走到当前步骤的下一步。若将跳转到的步骤不存在,则结束新手引导
      * 
      */  
     public static function nextStep(designedStep:int=-1):void
     {
      //若在暂停时调用nextStep,则自动执行resume方法继续播放新手引导
      if( _paused )
      {
       resume();
       return;
      }
      
      if( designedStep < 0 )
      {
       _nextStep = _currentStep+1;
      }
      else
      {
       _nextStep = designedStep;
      }
      
      //若该方法是由start方法调用情况下(此时_currentStep==-1)不需要让上一部引导完成:
      if( _nextStep > 0 && _currentStep >= 0 )
      {
       markFinish(_currentStep);
      }
      
      if(  _nextStep < _guideQueue.length && _guideQueue[_nextStep] )
      {
       var data:Object = _dataArray[_nextStep];
       _guideQueue[_nextStep].guideProcess(data);

       _currentStep = _nextStep;
      }
      //若无法执行欲跳转到的步骤,则不改变_currentStep的值
      else
      {
       //播放结束
       if( _nextStep == _sequenceArray.length )
       {

    if( onGuideFinish != null )
     onGuideFinish();
    uninstall();
       }
      }
     }
     
     /** 暂停引导播放 */
     public static function pause():void
     {
      if( !_paused )
      {
       _paused = true;
      }
     }
     
     /** 继续引导播放 */
     public static function resume():void
     {
      if( _paused )
      {
       _paused = false;
       
       _guideQueue[_nextStep].guideProcess(_dataArray[_nextStep]);
       _currentStep = _nextStep;
      }
     }
     
     /**
      * 显示全屏遮罩以限制交互范围
      * @param showRect 唯一显示出来的能接受交互的矩形区域
      * @param maskAlpha 遮罩透明度
      * @param maskColor 遮罩颜色
      * @param parent   遮罩添加到的父容器。若留空,则父容器就等于GuideManager.indicatorContainer
      */  
     public static function showScreenMask( showRect:Rectangle=null, maskAlpha:Number=0.5, maskColor:uint=0, 
     parent:DisplayObjectContainer=null, maskName:String="hotgirl" ):void
     {
      if( !parent )
      {
       parent = stage;
       if( !parent )
    return;
      }
      var mask:Sprite = _maskHome[maskName];
      if( !mask )
      {
       //遮挡物必须是能够响应鼠标事件的类,如Sprite。否则鼠标点击之将会穿透它以触发其挡住的对象的鼠标事件
       mask = new Sprite();
       _maskHome[maskName] = mask;
      }
      var w:Number = parent == stage ? stage.stageWidth : parent.width;
      var h:Number = parent == stage ? stage.stageHeight : parent.height;
      var g:Graphics = mask.graphics;
      g.clear();
      g.beginFill(maskColor, maskAlpha);
      g.drawRect(0, 0, w, h);
      if( showRect )
      {
       //利用Graphics重叠绘制会消去重叠区域像素的原理进行挖洞动作
       g.drawRect(showRect.x, showRect.y, showRect.width, showRect.height);
      }
      g.endFill();
      if( !parent.contains(mask) )
       parent.addChild(mask);
      
     }
     
     /**
      *隐藏全屏遮罩 
      * 
      */  
     public static function hideScreenMask(maskName:String="hotgirl"):void
     {
      var mask:Sprite = _maskHome[maskName];
      if( mask && mask.parent )
      {
       mask.parent.removeChild(mask);
      }
     }
     
     
     /**
      * 显示一个高亮矩形边框,该边框会被添加到当前正在播放新手引导的组件上
      * @param bounds 矩形边框显示位置。该矩形的参考系是当前正在播放新手引导的组件的父容器
      * @param parent  边框添加到的父容器。若留空,则父容器就等于GuideManager.indicatorContainer
      */  
     public static function showRectBorder( bounds:Rectangle, parent:DisplayObjectContainer=null ):void
     {
      if( !parent )
      {
       parent = stage;
       if( !parent )
    return;
      }
      
      if( !_border )
      {
       _border = new Shape();
       _border.filters = [new GlowFilter(0xff911b, .8, 8, 8, 4, 2)];
      }
      if( !parent.contains(_border) )
      {
       parent.addChild(_border);
      }
      _border.graphics.clear();
      _border.graphics.lineStyle(1, 0xFFFF00);
      _border.graphics.drawRect(0, 0, bounds.width, bounds.height);
      _border.x = bounds.x;
      _border.y = bounds.y;
     }
     
     /**
      * 隐藏边框 
      * 
      */  
     public static function hideBorder():void
     {
      if( _border && _border.parent )
      {
       _border.parent.removeChild(_border);
      }
     }
     
    //------------------------------------------private functions--------------------------------------------------//
     
     private static function doClear(step:IGuideComponent):void
     {
      if( step )
       step.guideClear();
      hideBorder();
      hideScreenMask();
     }

     private static function markFinish(sequence:int):void
     {
      if( !_finishList[sequence] )
      {
       doClear(_guideQueue[sequence]);
       if( onStepFinish != null )
    onStepFinish(_dataArray[sequence]);
       _finishList[sequence] = true;
      }
     }
     
     /** 是否已启动新手引导 */
     public static function get isSetUp():Boolean
     {
      return _isSetUp;
     }

     /** 新手引导是否正被暂停 */
     public static function get paused():Boolean
     {
      return _paused;
     }

     /** 当前执行到的步骤 */
     public static function get currentStep():int
     {
      return _currentStep;
     }
     
     }

    }

     

    GuideManager是本章代码最多也是最复杂的一个类,如果你现在看不懂,没关系,你只需要学会如何使用就可以了,毕竟这个类也不是我一朝一夕就写出来的,也是经过了反复的修改才造就的。下图演示了GuideManager的大致工作原理:

    image

    主要需要解释的地方有如下几个:

    1.加载guide.xml后得到的新手引导数据先会被存放进一个数组中,之后该数组被作为实参传给GuideManager.setUp()方法供GuideManager使用。细心的朋友会注意到,GuideManager在调度每一步的新手引导执行顺序的时候是根据每一步在数组中的索引,而并不是按照每一步的sequence属性。换句话说,如果我guide.xml里面的XML标签中sequence属性的最小值不是1,而是100,那么该标签代表的步骤仍然是首先被播放的:

    <step sequence="100" instanceName="ButtomButtonBar" subSeq="1"/><!--第一步-->
     <step sequence="200" instanceName="Window1" subSeq="1"/><!--第二步-->
     <step sequence="300" instanceName="Window1" subSeq="2"/><!--第三步-->

     <step sequence="400" instanceName="ButtomButtonBar" subSeq="2"/><!--第四步-->

    因此,根据我的这种方法,在guide.xml里面配置的新手引导步骤的sequence属性不必遵循从0开始的连贯数值,这样就便于插入新的步骤数据。比如我在设计新手引导步骤时,考虑到两个步骤间有可能在今后会插入一些新的步骤,那么我就可以让这两个步骤的sequence值差距大一些:

    <step sequence="1" instanceName="ButtomButtonBar"/>
     <step sequence="20 instanceName="Window1"/>

    2.由于涉及到新手引导的组件不可能在程序刚启动的时候都已经准备好展示新手引导(如实例化完成时、被添加到舞台上时、摆好位置时等等),所以我需要提供一个regist方法来让外部调用,在涉案组件准备好时才会被注册到引导管理器中。当一个组件被注册到引导管理器时,引导管理器会检查当前是否正在播放新手引导,若正在播放,则会检查当前播放到的引导步骤是否是由当前被注册组件负责展示的,若是,则马上开始展示当前引导

    3.nextStep方法被调用时会让新手引导进入到下一步。若下一步的instanceName对应组件还未注册,则暂停引导,直到它被注册了再继续播放。若当前步是最后一步,则结束引导,执行卸载工作

     

    小试牛刀

    有了guide.xml,有了IGuideComponent和GuideManager之后我们的新手引导基本框架已经搭建完毕,接下来就是需要在咱们的项目中实际运用上这套框架来试试看效果如何了。

    在下面这个例子里,我希望能够引导用户逐个点击我游戏右下角摆放着的按钮条(ButtomButtonBar)中的四个按钮。于是我可以这样设计guide.xml表的内容:

    <step sequence="0" instanceName="ButtomButtonBar" subSeq="1"/>
     <step sequence="1" instanceName="ButtomButtonBar" subSeq="2"/>
     <step sequence="2" instanceName="ButtomButtonBar" subSeq="3"/>
     <step sequence="3" instanceName="ButtomButtonBar" subSeq="4"/>

    下面是文档类和ButtonBar的代码:

    /** 
     *   新手引导测试
     *   Created by S_eVent
     *   at 2013-5-27 
     */
    [SWF(backgroundColor="0xFFFFFF")]
    public class GuideTest extends Sprite
    {
     private var _uiContainer:Sprite = new Sprite();

     private var _buttonBar:ButtonBar = new ButtonBar();
     
     private var _globalVariables:GlobalVariables = GlobalVariables.instance;
     
     private var _needGuide:Boolean = true;
     
     public function GuideTest()
     {
      initUI();
      
      if( stage )
       onAdded(null);
      else
       addEventListener(Event.ADDED_TO_STAGE, onAdded);
     }
     
     private function initUI():void
     {
      addChild(_uiContainer);
      
      var dp:Array = [];
      for(var i:int; i<6; i++)
      {
       dp[i] = {label:"按钮" + (i+1)};
      }
      _buttonBar.dataProvider = dp;
      _uiContainer.addChild(_buttonBar);
      
     }
     
     private function onAdded( e:Event ):void
     {
      stage.scaleMode = StageScaleMode.NO_SCALE;
      stage.align = StageAlign.TOP_LEFT;
      
      stage.addEventListener(Event.RESIZE, onResize);
      onResize(null);
      Message.stage = stage;
      
      if( _needGuide )
       loadGuideXML();
     }
     
     private function onResize( e:Event ):void
     {
      _globalVariables.stageWidth = stage.stageWidth;
      _globalVariables.stageHeight = stage.stageHeight;
     }
     
     private function loadGuideXML():void
     {
      var loader:URLLoader = new URLLoader();
      loader.addEventListener(Event.COMPLETE, onGuideXMLLoadComp);
      loader.load( new URLRequest("guide.xml") );
     }
     
     private function onGuideXMLLoadComp(e:Event):void
     {
      var data:XML = XML( (e.currentTarget as URLLoader).data );
      var guideData:Array = [];
      
      for each(var x:XML in data..step)
      {
       guideData.push( xml2Object(x) );
      }
      
      guideData.sortOn("sequence", Array.NUMERIC);
      
      function xml2Object( xml:XML ):Object
      {
       var obj:Object = {};
       var attributes:XMLList = xml.attributes();
       for each(var a:XML in attributes)
       {
    obj[a.name().toString()] = a.toString();
       }
       return obj;
      }
      
      GuideManager.setUp( guideData );
      GuideManager.stage = stage;
      GuideManager.onStepFinish = onStepFinish;
      GuideManager.onGuideFinish = onGuideFinish;
      GuideManager.start();
     }
     
     private function onStepFinish(data:Object):void
     {
      Message.show("您已完成第" + data.sequence + "步");
     }
     
     private function onGuideFinish():void
     {
      Message.show("恭喜您,您已完全部新手引导步骤!");
     }

    }

     

    //---------------------------ButtonBar.as---------------------------------//

     

    /** 
     *   按钮条
     *   Created by S_eVent
     *   at 2013-5-27 
     */
    public class ButtonBar extends Sprite implements IGuideComponent
    {
     /** 当ButtonBar中的某个按钮被按下时调用。该函数接收一个代表按下按钮索引号的int型参数 */
     public var onBtnClick:Function;
     
     private var _dataProvider:Array;
     private var _buttons:Vector.<CustomButton> = new Vector.<CustomButton>();
     private var _gap:Number = 4;
     private var _globalVariables:GlobalVariables = GlobalVariables.instance;
     
     public function ButtonBar()
     {
      super();
      this.mouseEnabled = false;
      
      GuideManager.register(this);
      
      this.addEventListener(Event.ADDED_TO_STAGE, onAdded);
     }
     
     public function clear():void
     {
      var btn:CustomButton;
      while( _buttons.length > 0 )
      {
       btn = _buttons.pop();
       if( this.contains( btn) )
    this.removeChild( btn );
      }
     }
     
    //--------------------------------private functions-----------------------------------//
     
     private function onAdded( e:Event ):void
     {
      this.addEventListener(Event.REMOVED_FROM_STAGE, onRemoved);
      this.addEventListener(MouseEvent.CLICK, onClick);
      //侦听舞台尺寸发生变化事件
      _globalVariables.addEventListener(PropertyChangeEvent.PROPERTY_CHANGE, onPC);
     }
     
     private function onRemoved( e:Event ):void
     {
      this.removeEventListener(Event.REMOVED_FROM_STAGE, onRemoved);
      this.removeEventListener(MouseEvent.CLICK, onClick);
      _globalVariables.removeEventListener(PropertyChangeEvent.PROPERTY_CHANGE, onPC);
     }
     
     private function onClick( e:MouseEvent ):void
     {
      var btn:CustomButton = e.target as CustomButton;
      if( btn && onBtnClick != null )
      {
       onBtnClick( _buttons.indexOf(btn) );
      }
     }
     
     private function onPC( e:PropertyChangeEvent ):void
     {
      if( e.property == "stageWidth" || e.property == "stageHeight" )
      {
       this.x = _globalVariables.stageWidth - this.width;
       this.y = _globalVariables.stageHeight - this.height;
       if( _guideTarget )
       {
    var maskArea:Rectangle = _guideTarget.getBounds(stage);
    GuideManager.showScreenMask(maskArea);
       }
      }
     }
     
     private function layout():void
     {
      var crtW:Number = 0;
      for each(var btn:CustomButton in _buttons)
      {
       btn.x = crtW;
       crtW += btn.width + _gap;
      }
     }
     
    //-------------------------------get / set functions----------------------------------//
     
     public function get dataProvider():Array
     {
      return _dataProvider;
     }

     public function set dataProvider(value:Array):void
     {
      _dataProvider = value;
      
      clear();
      
      var len:int = _dataProvider.length, btn:CustomButton;
      for(var i:int; i<len; i++)
      {
       btn = new CustomButton( _dataProvider[i].label );
       addChild( btn );
       _buttons[i] = btn;
      }
      
      layout();
     }

     public function get gap():Number
     {
      return _gap;
     }

     public function set gap(value:Number):void
     {
      _gap = value;
      layout();
     }

    //-------------------------------interface implement----------------------------------//
     
     private var _instanceName:String = "ButtomButtonBar";
     private var _guideTarget:CustomButton;
     
     public function guideProcess(data:Object=null):void
     {
      _guideTarget = _buttons[data.subSeq-1];
      var maskArea:Rectangle = _guideTarget.getBounds(stage);
      GuideManager.showScreenMask(maskArea);
      _guideTarget.addEventListener(MouseEvent.CLICK, onNextStep);
     }
     
     public function guideClear():void
     {
      //没什么好做的这里
     }
     
     private function onNextStep( e:MouseEvent ):void
     {
      e.currentTarget.removeEventListener(MouseEvent.CLICK, onNextStep);
      GuideManager.nextStep();
     }
     
     public function get instanceName():String
     {
      return _instanceName;
     }
     
     public function set instanceName(value:String):void
     {
      _instanceName = value;
     }

    }

     

    对于文档类来说,它首先要做的,自然就是在需要启动新手引导的时候去加载guide.xml,之后将加载得到的XML数据转换成GuideManager能识别的Object数组并传递给GuideManager使用,之后马上开始新手引导的播放。

    对于按钮条ButtonBar来说,要让它成为一个能够展示新手引导的组件,必须实现之前我们所说的IGuideComponent接口,然后在文件末尾处写上实现IGuideComponent接口的两个方法及一个属性。由于在我的项目中,ButtonBar只可能有一个实例,所以它的instanceName我就直接在它内部写死了。如果在项目中存在多个ButtonBar实例,那么我们需要在外部动态地为每个ButtonBar实例的instanceName属性赋值才行。在guideProcess方法中我需要写出轮到ButtonBar展示引导时会发生什么事情,在本例中,它要做的就是根据引导数据的子步骤sebSeq的不同而引导用户点击不同的按钮。为了方便,我这里就不加什么箭头来指示用户了,直接用GuideManager里面自带的全屏遮罩(实现原理可参考《使用绘图API绘制镂空矩形》)来限制用户的点击范围为我需要让用户点击的区域。

    那么上述代码最终的实现效果如下:

    http://www.iamsevent.com/zb_users/UPLOAD/GuideManager/test1/GuideTest.html

     

    经过这个例子,差不多我们熟悉了一点这套新手引导框架的使用方式了,那么在下一篇教程中,我们将考虑新手引导的更多方面:如与服务器端进行同步的问题、新手引导组件注册时机不对导致引导箭头指向位置不正确的问题以及如何使用开放式引导的问题等等。如果遇到这些问题,你知道该如何解决吗?i know what to do....


    展开全文
  • 游戏新手引导的制作原理(下)

    千次阅读 2014-08-26 11:56:17
    在上一篇教程中,我们了解了一套我自创的新手引导管理框架的使用原理,那么在本篇教程中,我们将考虑新手引导制作中可能遇到的一些棘手问题及探讨其解决方案。Are you ready my baby? Let`s go!   新手...

    http://www.iamsevent.com/post/59.html



    上一篇教程中,我们了解了一套我自创的新手引导管理框架的使用原理,那么在本篇教程中,我们将考虑新手引导制作中可能遇到的一些棘手问题及探讨其解决方案。Are you ready my baby? Let`s go!

     

    新手引导组件注册时间不对导致引导指示器指示位置出错

    我在做一个游戏的新手引导的时候有时候会出现这样的一个问题,就是新手引导中指示玩家点击的位置是一个错误的位置,如下图所示:

    image

    可能作者的本意是让箭头指示到右上角那个叉叉代表的关闭按钮处,结果却因为种种原因让箭头指偏了位置,这是一个可以严重也可以不严重的问题。如果你使用的是强制性引导,就像我在上一篇教程中使用的那种使用全屏遮罩限制用户交互范围的方式的话,你一旦发生了位置偏移的问题,那么用户永远也无法点击到你期待他点击的东西了,这样就会造成引导进行不下去的严重后果。

    在我的GuideManager中自带的showScreenMask方法可以产生全屏遮罩,它接受的showRect参数代表全屏遮罩中唯一显示出来的能接受交互的矩形区域

     

    /**
     * 显示全屏遮罩以限制交互范围
     * @param showRect 唯一显示出来的能接受交互的矩形区域
     * @param maskAlpha 遮罩透明度
     * @param maskColor 遮罩颜色
     * @param parent   遮罩添加到的父容器。若留空,则父容器就等于GuideManager.indicatorContainer
     */  
    public static function showScreenMask( showRect:Rectangle=null, maskAlpha:Number=0.5, maskColor:uint=0, 
    parent:DisplayObjectContainer=null, maskName:String="hotgirl" ):void
    { ... }

     

    showRect指示的区域是相对于parent参数指示的容器的,一般来说,只要我在这一点上没有弄错,显示出来的区域应该也不会出错。比如我将让全屏遮罩直接显示在stage对象上面,那么我就可以这么写:

    var maskArea:Rectangle = _guideTarget.getBounds(stage);//getBounds方法的参数——参考系直接选stage对象

    GuideManager.showScreenMask(maskArea, 0.5, 0, stage);//showScreenMask方法的parent参数也选择stage对象,与上面取矩形区域的参考系一致

    但是有时候往往会事与愿违,我现在想创建一个三步的引导:点击右下角按钮弹出窗口 ——>点击窗口中按钮——>关闭窗口,那么guide.xml写成这样一定是没有问题的:

    <step sequence="0" instanceName="ButtomButtonBar" subSeq="1"/>
    <step sequence="1" instanceName="Window1" subSeq="1"/>

    <step sequence="2" instanceName="Window1" subSeq="2"/>

     

    下面是文档类主要代码:

     

    private function initUI():void
    {

     .....
     _buttonBar.onBtnClick = onButtonBarBtnClick;
    .....
    }

     

    private function onButtonBarBtnClick(index:int):void
    {
     var win:DisplayObject;
     switch(index)
     {
      case 0:
       win = PopUpManager.createPopUp(Window1);
       break;
     }
     
     PopUpManager.centerPopUp( Window1 );

    }

     

    这段代码给右下角按钮条添加了按钮点击侦听器,在侦听函数中我们判断,若索引位置为0的按钮被点击了,就弹出一个Window1的窗口,弹出窗口之后将其居中。

     

    下面给出Window1的代码:

     

    public class Window1 extends Window implements IGuideComponent
    {
     private var _btn:CustomButton;
     public function Window1()
     {
      super(200, 200, 0x000000, 1, "面板一号", false);
      showCloseButton = true;
      _btn = new CustomButton("按我以完成引导!");
      addChild( _btn );
      _btn.x = (this.width - _btn.width) / 2;
      _btn.y = (this.height - _btn.height) / 2;
      onClose = function():void{ PopUpManager.removePopUp(Window1); };
      
      GuideManager.register(this);
     }
     
     //-------------------------------interface implement----------------------------------//
     
     private var _instanceName:String = "Window1";
     private var _guideTarget:CustomButton;
     
     public function guideProcess(data:Object=null):void
     {
      if( data.subSeq == 1 )
      {
       _guideTarget = _btn;
      }
      else if( data.subSeq == 2 )
      {
       _guideTarget = closeButton;
      }
      
      _guideTarget.addEventListener(MouseEvent.CLICK, onNextStep);
      var maskArea:Rectangle = _guideTarget.getBounds(stage);
      GuideManager.showScreenMask(maskArea);
     }
     
     public function guideClear():void
     {
      //没什么好做的这里
     }
     
     private function onNextStep( e:MouseEvent ):void
     {
      e.currentTarget.removeEventListener(MouseEvent.CLICK, onNextStep);
      GuideManager.nextStep();
     }
     
     public function get instanceName():String
     {
      return _instanceName;
     }
     
     public function set instanceName(value:String):void
     {
      _instanceName = value;
     }

    }

     

    这个的写法事实上是仿造的ButtonBar的写法:在构造函数里就执行了引导注册的工作。然后运行代码后发现在执行到引导第二步:引导用户去点击Window1中的按钮时,全屏遮罩中开放的交互区域位置发生了偏移

    image

    这是为什么呢?为什么呢?哪位同学可以告诉我原因?哪位同学知道请举手,哦,奥特曼就别举手了,我怕死!

    好吧,没人回答我,那还是我来为各位同学讲一下谜底吧。首先,我们在上一章中介绍过GuideManager的工作流程:当执行nextStep方法跳转到下一步时,若下一步涉及组件未注册,则会暂停,直到下一步组件注册时才会重新开始播放引导。对于刚才案例中我们的窗口组件Window1来说,它的注册工作是在外部调用其构造函数时才去做的,即直到窗口打开时它才会被注册,那么在刚才的案例中我们的操作流程就可以用下图来表示:

    image

    我们看到,如果按照这个流程走,那么在Window1还未被弹出前全屏遮罩就会被添加到舞台上,此时,Window1由于还未被添加到舞台上,所以其stage属性为null,那么在Window1.guideProcess()方法中的_guideTarget.getBounds(stage)这条语句的执行结果肯定会出现问题,这就直接导致了显示出的全屏遮罩中给出的可交互区域位置发生问题。所以,总结一下,可交互区域位置错误的主要原因是因为Window1对象被注册的时间过早

    既然找到了原因,那么接下来就想办法拖延Window1注册到GuideManager中的时间就可以了,比如,我们可以在Window1实例被弹出并居中后再执行注册操作:

     

    private function onButtonBarBtnClick(index:int):void
    {
     var win:DisplayObject;
     switch(index)
     {
      case 0:
       win = PopUpManager.createPopUp(Window1);
       break;
     }
     
     PopUpManager.centerPopUp( Window1 );
     if( win is IGuideComponent && GuideManager.isSetUp )
     {
      GuideManager.register(win as IGuideComponent);
     }

    }

     

    这样一来,全屏遮罩显示出的可交互区域位置就正确了,当然,你不用担心同一个实例会被多次重复注册,在GuideManager的register方法中会自动忽略已注册过的组件。

    以上案例的在线演示地址:

    http://www.iamsevent.com/zb_users/UPLOAD/GuideManager/test2/GuideTest.html

    源码下载:

    http://www.iamsevent.com/zb_users/UPLOAD/GuideManager/Test2.zip

     

    新手引导步骤的记录

    在新手引导过程中万一玩家没有完成引导就退出了游戏或者关闭了页面或者掉线了怎么办?为了让用户下次登陆时能够“再续前缘”,我们需要在每完成一步时都记录用户当前进行到的引导步骤。

    一般来说,当前进行到的引导步骤都会记录在后端的数据库里面,但是在本例中由于没有后端可以让我通讯,所以我暂时把数据保存在本地Flash缓存SharedObject中。下面给出的SOManager就是负责存取缓存记录的:

    /** 
     *   本地存储管理器
     *   Created by S_eVent
     *   at 2013-5-30 
     */
    public class SOManager
    {
     private static var so:SharedObject = SharedObject.getLocal("GuideTest");
     
     /** 保存当前引导步骤 */
     public static function set step(value:int):void
     {
      so.data.step = value;
      so.flush();
     }
     
     /** 获取本地存储的引导步骤 */
     public static function get step():int
     {
      return so.data.step;
     }
     
     /** 清除本地存储记录 */
     public static function clear():void
     {
      so.clear();
     }

    }

    接下来,我们需要在游戏启动时取出上一次玩家下线时保存的引导步骤,根据它的值来设置新手引导是否需要播放或者从哪一步开始播放。

     

    private function onAdded( e:Event ):void
    {
     stage.scaleMode = StageScaleMode.NO_SCALE;
     stage.align = StageAlign.TOP_LEFT;
     
     stage.addEventListener(Event.RESIZE, onResize);
     onResize(null);
     Message.stage = stage;

     //新手引导最后一步的sequence是2,如果之前已完成步骤大于这个值,则表示玩家已经
     //完成新手引导,否则表示玩家还未完成引导,需要加载引导数据并启动引导
     if( SOManager.step <= 2 )
      loadGuideXML()

    }

    .....

     

    private function onGuideXMLLoadComp(e:Event):void
    {
    ......
     
     GuideManager.setUp( _guideData );
     GuideManager.stage = stage;
     GuideManager.onStepFinish = onStepFinish;
     GuideManager.onGuideFinish = onGuideFinish;
     //从上次离线时记录的步骤开始
     GuideManager.start( getStepIndexBySequence(SOManager.step) );
    }

    //根据步骤号获取索引号
    private function getStepIndexBySequence( s:int ):int
    {
     var len:int = _guideData.length;
     for(var i:int; i<len; i++)
     {
      if( _guideData[i].sequence == s )
      {
       return i;
      }
     }
     
     return 0;
    }

    //根据索引号获取步骤号
    private function getStepSequenceByIndex( index:int ):int
    {

       var len:int = _guideData.length;
       if( index >= len )return len;


     if( _guideData[index] )
     {
      return _guideData[index].sequence;
     }
     
     return 0;
    }

    private function onStepFinish(data:Object):void
    {
     Message.show("您已完成第" + data.sequence + "步");
     //当前步骤完成后需要将下一步步骤号存进本地缓存
     SOManager.step = getStepSequenceByIndex( GuideManager.currentStep+1 );

    }

     

    在理解上述代码时,我需要再提一下一个步骤的索引号和步骤号之间的区别。索引号指的新手引导各步骤的执行顺序,它是从0开始的连贯数值;而步骤号则等于guide.xml中配置的各步骤标签中的sequence属性,它是不连贯的数值,我们仅依靠它来给全部步骤进行排序以获取各步骤的索引号。要保存在本地/后端数据库中的数据是步骤号,而能被GuideManager识别并使用的是索引号。所以,我们在存取数据时还需要时刻记得进行它们两者之间的转换工作。

    运行一下上述代码,看起来一切工作正常,那是否我们就可以安枕无忧了呢?当然不是,考虑下面一种情况,我将进行的新手引导步骤如下:

    点击按钮弹出窗口——>点击窗口中的功能按钮——>点击窗口的关闭按钮关闭窗口

    如果我在进行到第二步的时候离线了,那么我本地/数据库中记录的步骤号为2,也就是说,下一次我登陆时会从步骤2开始。但是,从步骤2开始有一个坏处就是步骤2是由一个我未打开的窗口负责展示的,此时我上线后发现界面上没有任何箭头或者神马东西指示我去开启这个窗口,那此时作为一个新手玩家的我就会很困惑了,我不知道下一步该怎么做,不知道该点哪个按钮来打开步骤2所涉及的窗口。这一点难免会降低用户体验,为此,我们需要找一个方式来解决该问题,我们理想的情况是,当用户在未完成第三步之前离线,下次上线时依然从步骤1开始,因为步骤2和3是在刚上线时看不到的两个步骤,而步骤1则不然,要是我下次上线时给我从步骤1开始,我就能清楚地回想起我该点哪个按钮以继续上次未做完的新手引导。

    如果我想根据我之前的设想来做,那么就不能每一步引导做完后都去同步一下(意思就是将步骤号保存到本地/数据库),为了识别当前做完的步骤是否需要同步,我们再guide.xml中为每个步骤标签增加一个属性:noSynchro(完成该步时是否跳过与同步的工作,若标签中存在该属性且该属性非0,这表示在完成该步骤后不会进行同步工作)

    那么此时我们的guide.xml就可以写成这样:

     

     <step sequence="1" instanceName="ButtomButtonBar" subSeq="1" noSynchro="1"/>
    <step sequence="2" instanceName="Window1" subSeq="1" noSynchro="1"/>
    <step sequence="3" instanceName="Window1" subSeq="2"/>

     

     这样写的后果,就是当完成第1、2步时,不会做同步工作,即下次登陆时不会从第2/3步开始。改完了guide.xml后我们在文档类中再进行相应的修改:

     

    private function onStepFinish(data:Object):void
    {
     Message.show("您已完成第" + data.sequence + "步");
     //仅当不存在noSynchro属性或该属性值为0时才进行同步工作
     if( !data.noSynchro )
     {
      //当前步骤完成后需要将下一步步骤号存进本地缓存
      SOManager.step = getStepSequenceByIndex( GuideManager.currentStep+1 );
     }

    }

     

    只需要在进行同步工作之前加一条判断语句就可以了。此时,我们就可以测试一下,看看结果是否正如我们期望的那样。

    在线演示地址:(在进行到步骤2或3时刷新页面,看看第二次打开加载完成后新手引导步骤是从第几步开始的)

    http://www.iamsevent.com/zb_users/UPLOAD/GuideManager/test3/GuideTest.html

    源码下载:

    http://www.iamsevent.com/zb_users/UPLOAD/GuideManager/Test3.zip

     

    开放式引导

    我们之前的所有例子都属于强制性引导,即通过一个全屏遮罩或者别的方式来限制用户的可交互区域,强制用户点击你希望他点击的区域。强制引导的好处有二:一,实现起来简单;二,不容易出BUG。然而它的坏处就在于限制了用户的操作自由,遮挡了大部分好看的区域,降低了用户体验。

    为了增强用户体验,增加做新手引导时的自由度,有时我们需要实现一个开放式的引导。开放式引导虽然不会像强制性引导那样仅开放非常小的一块可交互范围给用户,但也不会完全开放用户的操作自由。开放式引导的主要难点在于,在某一时刻,哪些功能可用哪些功能不可用,那些不可用的功能又在什么时候会变成可用,这些事情实现起来是比较复杂的。

         首先让我们考虑下面一种情况,有两个将要展示引导的窗口Window1和Window2,Window1先展示,Window2后展示:

    点击按钮1打开Window1——>完成Window1中展示引导——>点击按钮2打开Window2——>完成Window2中展示引导

        那么我期待用户是点击按钮1先打开Window1,在做完了Window1中展示的引导后再去点击按钮2打开Window2,因此,我不希望用户在完成第二步前去打开Window2,否则引导顺序将会乱套。但是,由于我在按钮2上添加了鼠标点击事件CLICK的事件侦听,并在事件处理函数中写了弹出Window2的相关逻辑。如果用户执意要点按钮2,那岂不一定会触发CLICK事件,弹出Window2?对此,我有一个解决方案,就是在未执行到第三步时给按钮2添加一个优先级较高的CLICK事件侦听器,一起来看如下代码:

    //----------------------------ButtonBar.as-----------------------//

    public function guideProcess(data:Object=null):void
    {
     _guideTarget = _buttons[data.subSeq-1];
     var maskArea:Rectangle = _guideTarget.getBounds(stage);
     GuideManager.showRectBorder(maskArea);
     _guideTarget.addEventListener(MouseEvent.CLICK, onNextStep);
     this.addEventListener(MouseEvent.CLICK, onClickWhenGuiding, false, 1);
    }

    private function onClickWhenGuiding( e:MouseEvent ):void
    {
     if( e.target != _guideTarget )
     {
      e.stopImmediatePropagation();
      Message.show("别淘气!");
     }

    }

     

    addEventListener方法的第四个参数priority代表该事件侦听器的优先级,默认情况下优先级都是0,因此,如果我们在注册事件侦听器的时候传入一个大于0的值给addEventListener方法的第四个参数,那么我们此时注册的侦听函数就会在事件触发时优先被执行到。在事件处理函数中,我们将判断点击目标是否是我们期望用户点击的,若不是,就使用event.stopImmediatePropagation方法来立即停止事件的冒泡,其结果是除了当前事件处理函数外的其他事件处理函数都不再会被调用。在上例中,onClickWhenGuiding事件处理函数在触发CLICK事件时会被优先调用,若在onClickWhenGuiding函数中调用了event.stopImmediatePropagation方法,那么同样侦听CLICK事件的onClick方法就不再会被执行。使用这种方法就可以有效地限制用户进行那些不希望他们做的动作了。(不要直接在onClick方法里面判断当前点击对象是否是_guideTarget,这样会增加耦合性,对于onClick方法来说,它并不需要关心当前有没有在进行新手引导)

        使用这种方式来实现的开放式引导在线展示如下:

    http://www.iamsevent.com/zb_users/UPLOAD/GuideManager/test4/GuideTest.html

    源码下载:

    http://www.iamsevent.com/zb_users/UPLOAD/GuideManager/Test4.zip

     

        当然,上面只是说了一种限制用户交互的方式,可能还有更多的情况我没有考虑到,这还需要列位道友在实际开发过程中自己开动脑筋,想出一种耦合性不高又可靠的方案。

        本期教程就到这里吧,希望大家喜欢,我提出的这种新手引导方案不一定是最好的,但也希望列位能仔细读一读,取其精华去其糟粕,如有任何意见也可以留言给我哦。出这篇教程的初衷在于让更多的人不用再为做新手引导而头疼,像我以前一个同事,新手引导步骤发生了一些改变,结果他一改就改了好几天,这样的结果是我们谁都不愿意看到和亲身体会的。最后,祝大家六一儿童节快乐啦,哈哈!


    展开全文
  • 新手引导的实现原理

    千次阅读 2016-03-16 10:46:28
    2 进入页面,请求查询接口,查询indexTipHelp返回的数据,如果新用户,则显示新手引导(推荐使用intro.js插件,具体使用方法:http://blog.csdn.net/u011500781/article/details/50898312),如果是老用户,则不显示
  • 谈谈游戏中新手引导是如何制作的

    万次阅读 2016-05-23 21:05:21
    而两款游戏的新手引导,都是由我来完成的。因此,想写篇文章记录制作新手引导过程中的一些心得。 http://blog.csdn.net/operhero1990/article/details/51482734 一、新手引导的分类 从触发方式上,引导分为强制...
  • 如何实现“新手引导”效果

    千次阅读 2019-02-22 10:20:27
    前言:初次登录一些网站时,一般会有“新手指引”操作,用于指引新用户如何使用本网站的一些说明。在网上查阅资料,学习了《慕课网》的一个视频资料,现记录如下。“新手指引”操作,主要涉及html结构(黑色蒙版),...
  • 游戏功能模块——新手引导

    千次阅读 2020-05-04 15:44:29
    新手引导 流程图 数据 代码 后续更新
  • unity自带的功能也能实现简单的新手引导遮罩,只是unity自带的遮罩会有锯齿。之后再更新加shader的版本。以下是具体步骤:1、将MainCamera 的背景设为solidcolor.改为黑色。2、添加两张背景,内容一摸一样3、UGUI...
  • 【Unity】新手引导遮罩

    千次阅读 2016-06-02 17:31:43
    可参考这里UGUI 新手引导遮罩控件解释一下思路: public bool IsRaycastLocationValid(Vector2 sp, Camera eventCamera) { // 点击在箭头框内部则无效,否则生效 return !RectTransformUtility....
  • vue中用intro.js新手引导功能怎么实现? 报错:introJs is not defind,我js,和css都引入了啊, //新手引导功能触发 introStart(){ introJs().start(); }
  • 最近做的一个项目中,引入了首次进入程序时的显示新手引导的功能,全屏显示,可以手指拖动左右翻页。效果类似于淘宝Android客户端3.0版本中新手引导的效果。 下面说说这个需要全屏的新手引导都经历了一些什么变化吧...
  • vue 新手引导页功能

    千次阅读 2019-06-10 13:31:57
    背景:项目中 需要添加 新手引导页功能,效果如下图:解决方法:vue + vue-intro + intro.js1.安装依赖npm i vue-introjs npm i intro.js复制代码2.修改 webpack 文件 在 webpack.dev.conf.js webpack.prod.conf.js...
  • Android 新手引导蒙层效果实现

    千次阅读 2017-01-06 15:00:22
    先上效果图:这个效果一开始我是想直接让UI给个切图,后来发现这样不行,适配很差,达不到效果。所以就自己动手写代码,其实思路也很简单:在这个布局的父布局上面再手动添加一个view(通常LinearLayout比较方便),...
  • Unity3D新手引导开发手记

    千次阅读 2016-06-17 17:17:05
    最近开始接手新手引导的开发,记录下这块相关的心得 ...首先客户端是Unity,在接手前,前面的同学已经初步完成...我们的新手引导是由一个个强引导组成的,每个强引导都有一系列的步骤,这套框架实现的功能就是:
  • android实现App新手引导功能

    千次阅读 2017-01-20 17:01:28
    可以实现activity,fragment上任何控件的引导功能,可以自己任意改变样式。 特别注意在fragment里的方法有点不一样,要注意下。 源码下载地址: http://download.csdn.net/detail/zzq272804553/9742331 我...
  • Unity3D 新手引导

    千次阅读 2018-05-12 15:07:11
    关于Unit3D 新手引导的方式 网上已经有很多了。最近刚好 去实现了一个新手引导。在这里和大家 分享下!1&gt;. 第一种最方便的就是,新建立一层黑色的Image 并且Raycast Target设置为True 的 Mask.在 UI的最上层...
  • 前端新手指引插件,vue-tour

    千次阅读 2019-04-15 18:13:05
    vue-tour官网 vue-tour API
  • 工作笔记>新手引导制作

    千次阅读 2013-10-28 23:12:42
    新手引导时,遇到上层引导手指,不能准确的找到偏移控件的位置 解决方法 我试过很多,但效果都不怎么理想,配置起来非常麻烦,与虎哥商量后,发现一个比较牛逼的方法 实现效果:    界面被黑幕遮挡...
  • Android引导蒙层,安卓新手引导图,引导图层,支持椭圆,圆形,矩形多种形状,一行代码快速搞定
1 2 3 4 5 ... 20
收藏数 21,296
精华内容 8,518
关键字:

新手引导