我也上去了!GodGuide!

『GodGuide——新手引导框架』是晓衡 2019 年发布在 GitChat 上的付费教程。代码在 Github 已经开源很久了,但最近有伙伴说 Github 打不开,我灵光一闪机会来了!

代码和完整文档发布到了 Cocos Store,同时也将原来的付费文档公开,希望能帮助到更多的人。如果你也有更好的新手引导解决方案、框架、Demo,也欢迎分享到全球最大的 Cocos 技术社交平台 Cocos Store

悄悄告诉大家, Cocos 官方布道师们制作的范例,正在陆续登录 Cocos Store ,不方便访问 GitHub 的问题,有救了!

正文之前,我先来上一段小视频

一、实现新手引导的困难

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

开发中的痛点

  1. 在正常游戏逻辑中插入引导代码,干扰流程;

  2. 引导代码的增加会影响原有代码逻辑,增加维护、测试成本;

  3. 界面或需求的变化会可能会使引导功能大幅修改,甚至重做;

  4. 手指提示对应的矩形区定位麻烦,不能简单使用固定的位置和矩形大小,而且只有游戏开发人员才做到;

  5. 编写引导功能代码困难,需要策划—程序之间高度配合,开发效率低下。

期望的编程体验

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

  1. 引导功能代码,不能混入正常游戏逻辑代码中,后患无穷,应尽量分离;(难以忍受优雅的代码被无情的打乱,更难忍受糟糕的代码被弄的支离破碎)

  2. 界面只发生简单的 UI 位移、Size 大小、节点层次的调整,不需要修改引导代码;

  3. 定位 UI 指引矩形区域应尽可能简单,能自适应不同的屏幕尺寸;

  4. 最好能做到策划人员都可以来制作部分流程引导;

  5. 在引导需求明确、游戏功能正常的情况下,制作一个常规的引导步骤应该非常快捷。

二、分离引导与游戏逻辑

上面对存在的问题难点进行了分析,也设定了期望目标,我们首要解决的问题是“如何分离引导与游戏之间的逻辑”,做到各不相关。

分析引导操作流程

引导的主要任务是在特定界面中,需要按照指定流程操作游戏,其核心是:

  1. 目标节点位置确定

  2. 目标节点操作提示

  3. 非目标节点的操作限制

  4. 目标节点操作完成监视

  5. 继续下一个目标节点

通常来说引导功能并不会影响游戏业务逻辑,只是限制游戏的操作流程,同时还需要在过程中以文字、动画、与后端通信等方式与玩家进行交互。

观察任意场景中的节点与事件—上帝模式

引导功能要限制游戏的流程,就必须站在更高的位置,统领整个游戏场景,我将它称之为上帝模式,如下图:

进入上帝模式很简单:

  1. 在当前场景最上层放置一个与屏幕相同大小的节点

  2. 在上帝引导节点上拦截触摸事件

拦截触摸事件具体细节我们下一节再讲,既然称之为「上帝」,还需要具有两项目能力:检索任意节点&节点事件监听

检索任意节点

使用引擎提供的 cc.find 就可以搞定,但这里为了对节点进行高效检索,从 CSS 中吸取了一点灵感,定义了一种称之为**“定位器”**的场景中节点表达方式。

例如:字符串Home > btn_home,描述的是 Home 节点下名为 btn_home 的节点,只要 btn_home 节点的名字在 Home 节点下是唯一的,不用理会它在 Home 下第几层,注意下图中绿色箭头:

enter image description here

检索节点并获取节点的位置、大小,用于我们的上帝引导层,进行有取舍的事件拦截、遮罩显示等, 关于定位器的详细内容,下一节会有详细说明。

任意节点事件监听

为了使表述简洁,我们这里重点只关注 UI 节点的点击触摸引导,监听任意节点的事件至少 2 种方法:

  1. 将引导层节点放置在场景最顶层,所有的触摸操作首先需要经过引导;

  2. 利用 JavaScript 语言的动态特性,对节点触摸分发函数进行 Hook(流程录制会使用到该技术)。

enter image description here

如上图所示,通过定位节点的位置和大小,我们在上帝引导层上“挖”开一个空洞,当点击到空洞位置,继续分发触摸事件,使用户操作进入正常的游戏层。

同时还需要监听目标节点的触摸事件发生(仅一次),当点击操作发生,当前目标节点的引导完成。

引导任务

这里把一个完整的引导流程进行分解和抽象,引入两个概念:步骤&任务

步骤 step

将引导流程中的一个最小操作称之为步骤 step,比如提示点击某个按钮、显示一行提示文本等。

任务 task

任务步骤的有序集合 ,一个任务中可能存在一个或多个步骤,当任务中的步骤全部引导完成,当前任务就算完成。

任务进度管理

多个任务又可以组成任务组,比如引导用户购买游戏中的道具是一个任务,将道具装备到身上又是一个任务,它们之前有明确的先后关系。

对于任务组的管理,外部使用 index 表示正在执行第 n 个任务,当任务被完成 index++,重新进入游戏将不会再执行 index 之前的任务,如果一个任务中的步骤没有被执行完,重新进入游戏则从该任务的第一个步骤开始执行。

因此在每一个任务的最后一个步骤完成时,我们还需要将 index 变量持久化下来,一种是保存到本地,一种是保存到后台服务器。

引导任务的 JSON 表达

用 JSON 配置的方式表示引导任务,如下:

tasks:[
 {
  name:'从主界面进入商店,购买道具', 
  steps: [{步骤 1}, {步骤 2} ... {步骤 n}],
    },
    {
  name:'将购买的道具,装备到玩家角色上', 
  steps: [{步骤 1}, {步骤 2} ... {步骤 n}],
    },
    ...
    {
  name:'50 级引导玩家,进入竞技场', 
  steps: [{步骤 1}, {步骤 2} ... {步骤 n}],
    }
]

steps 中的最后一个步骤,通常会与后台服务器进行网络通信,此前的操作流程将不能再被回溯,此时用来结束一个相对完整的引导任务,而且还可以将上面的任务单元编写在不同的文件中,具体的做法在后面小节中有详细说明。

三、点击操作的引导实现细节

游戏引导最多的是UI 点击操作,首要解决的是对 UI 节点的定位,获取节点对象,取出节点的位置(Position)、包围盒(BoundingBox)、锚点(AnchorPoint)等信息,如何才能拿到任意节点呢?

这里有两种方法:

  1. 遍历场景树,将节点检索出来;

  2. 在游戏逻辑代码中,将目标节点对象传递到引导逻辑代码中。

因为要保持引导与游戏尽可能分离,这里选择的是第一种方式。

定位器的设计实现
cc.find 的局限

Cocos Creator 引擎提供了 cc.find 函数,传入节点路径可搜索出 Canvas 下任意节点,代码如下:

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

下图是在浏览器控制台中进行测试:

其中 cc.find 的第一个参数节点路径字符串可以精确地定位到 btn_home 节点,但是在实际开发中会感觉很繁琐,有下面两个原因:

  1. 当界面层级较深时,节点路径字符串太长,拼写容易出错;

  2. 节点名字、层级变化,节点路径字符串就失效了,特别容易被误伤。

定位器字符串

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

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

我们可以将上面 btn_home 节点的节点路径字符串改为,如下方式:

godGuide.find('Canvas > btn_home');

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

//注意:确保场景中只有一个名字为 btn_home 的节点
godGuide.find('btn_home');

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

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

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

'>'符号也可与'\'混合使用,如下:

let btn_home  = godGuide.find('Home>main_btns/btn_home');
let btn_level = godGuide.find('Home>main_btns/btn_level');

需要注意的是,节点名命不要使用'>''\'这两个字符 。

异步

在实际开发中会遇到这样一种现象,发出了一个节点定位指令,但此时节点并未出现在场景中,我们得到的是一个 null 对象,因此还需要将 godGuide.find 函数设计成一个异步方式,通过回调函数返回检查到的节点,当第一次定位不到节点时,间隔一定时间继续定位节点:

godGuide.find('Home>layout/btn_home', (error, node) => {
    cc.log('定位节点成功:', node.name);
});

godGuide.find 内部设置一个超时,通过 error 返回错误。

节点遮罩的实现

不少游戏,为了突出引导节点,需要将不能操作的区域叠上一层半透明的遮罩,镂空引导节点,引擎提供的 cc.Mask 组件可以帮助我们实现这个功能。

在这里创建一个 GodGride 的预制体,在内部放置一个 cc.Mask 节点,看下图:

enter image description here

Mask 组件内部是用 cc.Graphics 实现,成员变量 _graphics 是 cc.Graphics 的实例。

开启 Mask 组件的 inverted 反转属性,使用_graphics 绘制节点矩形,即可实现镂空遮罩效果,下面是具体代码:

start() {
    //获取 Mask 组件 
    this._mask = this.node.getComponentInChildren(cc.Mask);
    this._mask.inverted = true;
},

//镂空指定节点
_focusToNode(node) {
    this._mask._graphics.clear();
    let rect = node.getBoundingBoxToWorld();
    let p = this.node.convertToNodeSpaceAR(rect.origin);   
    rect.x = p.x;
    rect.y = p.y;
    this._mask._graphics.fillRect(rect.x, rect.y, rect.width, rect.height);
}

下面是运行效果,可以看到图中左下角设置按钮位置被镂空。

监听全局触摸事件

一旦定位到目标节点,目标节点的位置大小就可以通过遮罩显示出来,接下来要做的就是对触摸事件的拦截放行监听(点击确认),成为真正的上帝引导层,掌控整个游戏。

拦截与放行

只需要将 GodGride 预制节点放到场景的最顶层,并为它添加触摸事件即可,看下面代码:

start() {
    //在引导层上注册 TOUCH_START 触摸事件
 this.node.on(cc.Node.EventType.TOUCH_START, (event) => {
        //目标节点不存在,拦截
        if (!this._targetNode) {
            this.node._touchListener.setSwallowTouches(true);
            return;
        }

        //目标区域存在,击中放行
        let rect = this._targetNode.getBoundingBoxToWorld();
        if (rect.contains(event.getLocation())) {
            this.node._touchListener.setSwallowTouches(false);
            cc.log('未命中目标节点,放行')
        } else {
            this.node._touchListener.setSwallowTouches(true);
            cc.log('未命中目标节点,拦截');
        }
    }, this);
}

上面代码中 this._targetNode 变量是定位到的目标节点,通过事件对象 event.getLocation 获取的是触摸到的世界坐标点,因此可直接使用 node.getBoundingBoxToWorld 方法获取节点在世界坐标上的包围盒矩形。当点击的位置正好落在矩形区内,使用 node._touchListener.setSwallowTouches(false) 放行触摸事件(不吞噬),事件继续向下层节点派发,当未命中节点则使用 setSwallowTouches(true) 拦截当前触摸事件(吞噬事件),停止事件的派发,下图是运行效果:

目标节点的触摸监听

最后一步,如何知晓目标节点被点击呢?其实很简单,只需在目标节点上注册个触摸监听 TouchEnd 事件即可,看下面代码:

//定位到目标节点
godGuide.find('Home > main_btns > btn_home', (node) => {
    //定位成功,注册 TouchEnd 事件,仅响应一次
    node.once(cc.Node.EventType.TOUCH_END, () => {
        cc.log('节点被点击');
        callback();
    });
});

我们这里使用 node.once进行注册,仅接收一次点击事件,当该事件发生后,callback 通过回调函数进行确认(第四小节引导任务框架中详细讲解流程),当前步骤处理完毕,进入一下个引导步骤,直到所有步骤全部被完成。

四、未完待续

由于篇幅较长,为了不给大家造成阅读负担,以及我想多安利一些伙伴,我会在公众号上多分享几次,感谢大家的支持!

如果感兴趣的伙伴,可以扫码进入 Cocos Store 资源介绍阅读完整全文。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值