新手引导系统设计

经历了两个 cocos2d-x 游戏项目,在新手引导系统的设计上也有了一点经验,但经历更多的是坑。我想在这里记录一个更加成熟一点方案,并在现有的 unity3d 项目上实现出来。

0x00 框架

新手引导系统应该是独立的,与其它系统之间是低耦合的,理想状态下不需要在其它业务逻辑中放置额外的代码——例如在按钮逻辑中去调用新手引导系统的接口来驱动引导进入下一步骤。

如果将新手引导系统拟人化,试想它看着一个新玩家进入游戏,然后根据游戏状态的决定是否开始引导,然后一步一步引导玩家操作界面上的元素,直到教会玩家一些基本操作。

没错,这就是观察者模式。我们不需要在游戏业务中到处插入新手引导系统的代码,而是让新手引导系统主动订阅游戏状态的变化,根据状态来决定引导流程。在我的 PureMVC 实现中,我通过劫持 Facade.sendNotification() 方法来获得游戏状态的变化。由于整个游戏的所有消息都经过这里,于是引导系统就可以在这里获知它感兴趣的信息,从而推动引导进程。

local originFacadeSendNotification = pm.Facade.sendNotification
function pm.Facade:sendNotification(notificationName, body, type)
    originFacadeSendNotification(self, notificationName, body, type)
    GuideManager:Dispatch({
        catalog = "notification",
        name = notificationName,
        body = body,
        type = type,
    })
end

GuideManager:Dispatch() 是一个用于被动更新游戏状态的接口,当状态变化后,新手引导系统检测引导配置,判断是否需要激活引导。

当新手引导系统关闭的时候,Dispatch 不做额外的操作,不太需要担心性能问题。Dispatch 方法中会捕获一些引导需要用到的状态,例如当前所处界面、玩家等级经验、背包中的道具等等。如果劫持 sendNotification 还不足以满足需要,还可以主动调用 Dispatch 方法向新手引导系统推送具体信息。

0x01 遮罩层实现

当引导开始后,根据配置文件中的步骤,在屏幕上显示引导文字及指针。通常除了需要操作的按钮周围是亮着的,其它部分会被遮挡,并且屏幕的其它部分是无法操作的。这样简单的一个需求,实现起来却并不容易。

考虑到新手引导的独立性和与其它系统的低耦合度,当引导过程中需要锁定特定按钮,应该是主动去检索按钮的位置信息,而不是在界面逻辑中提供按钮位置。于是在配置文件中需要指定按钮在场景中的绝对路径,然后引导系统使用 GameObject.Find("/path/to/game/object/with/button") 以一定频率来轮询按钮,直到获取到指定对象或者超时。除了切换或加载场景之外一般都能一次命中。

引导系统定位到按钮后,需要在该位置绘制遮罩层,用于遮挡屏幕上其它部分。该遮罩层还有一个作用,就是阻止触摸事件向下传递,从而禁用所有界面操作。那么如何让我们指定的按钮可以操作呢?可以通过一个透明的代理对象(Event Trigger)将点击事件转发到目录按钮上(Target Button),这个代理对象被叠加到目标按钮上方,并实时跟踪目标按钮的位置和大小。

这样的好处是,定位到目标按钮后,即使按钮在位置变化、或者被部分遮档。该代理对象的触摸区域一样能将事件准确无误地传达给目标按钮。此外,引导层的上方还可以放置一些诸如对话框,引导手势之类的其它组件。

至于如何在遮照层上绘制与按钮大小相近的缕空区域,这里有一个的方案:

  =======          <- Dialog Panel
            ^^^    <- Proxy Button (Event Trigger)
------------///--- <- Guide Mask (Raw Image, camera[culling mask = guide])
            ~~~    <- Shape (tag = guide, shader = cutout)
                
============***=== <- UI Panel, * = Target Button

Canvas 下分为两个层,一个是 UIMain 用来放界面,在其之上是 UIOverlay 用来放一些叠加层,例如引导层。

引导层的主体是一个 Raw Image, 其 texture 是由 Camera 组件绘制的。Camera 首先用一个半透明黑色清空屏幕,然后在目标按钮的区域绘制一个形状,该形状是一个简单的 quad 并指定了一个混合模式为 blend Zero OneMinusSrcAplha 的 shader —— 用于在半透明黑色屏幕上缕空(cutout)出目标按钮区域。形状作为代理对象的子节点,跟随代理对象移动和缩放,于是缕空区域就随着变化。

Event Trigger 除了可以传递点击事件,还可以实现拖拽事件。甚至可以将事件代理给 ScrollRect 等对象。

0x02 引导配置

通过配置文件来建立引导所需要的步骤,可以方便程序和策划制作和修改新手引导,并且可以降低引导系统的耦合度。

一个引导是由一组起动条件、一组目标状态和一系列步骤组成。起动条件决定引导是否激活,目标状态决定引导是否已经完成;而这些步骤则是达成目标所需要的操作。

例如引导新手进入游戏进行一场战斗的起动条件是:总经验 = 0;目标状态是:总经验 > 0。这些状态可以直接从游戏数据中获得,不需要服务端做额外的记录。若玩家的总经验 > 0,表明他至少完成过一场战斗,这步的引导就可以标记为完成状态了。此外还可以读取关卡的进度、甚至是用指定引导的状态来作为起动条件。

引导可以有多个入口,其中一个由主场景开始,可以防止玩家中途退出游戏,重登后失去引导。一般情况下引导过程可能在游戏中的某个场景开始。

目标状态是在引导步聚中完成的,但是有时候完成目标后,引导还需要做一些后继操作,例如几句温馨提示,或者引导玩家回到主场景。

引导配置的结构很大程度决定了引导系统的灵活性,这部分的内容需要在项目中慢慢摸索。待今后总结出更多经验再来详述。

参考资料

  1. Unity3D新手引导开发手记

标签: game design, unity3d, guide

已有 11 条评论

  1. jakey jakey

    好文

  2. 新手引导确实是一个非常复杂的系统, 应当与业务逻辑完全解耦, 如果设计的不合理的话, 代码中就会导出充斥着if判断. 在配置上又要考虑连贯性,也要考虑中断行, 所以一个好的配置应该是树形的. 树杈的引导点是关键点, 而树枝上的点为非关键点, 如果中断则要考虑舍弃.

  3. xuanyuan xuanyuan

    引导层的主体是一个 Raw Image, 其 texture 是由 Camera 组件绘制的。Camera 首先用一个半透明黑色清空屏幕,然后在目标按钮的区域绘制一个形状,该形状是一个简单的 quad 并指定了一个混合模式为 blend Zero OneMinusSrcAplha 的 shader —— 用于在半透明黑色屏幕上缕空(cutout)出目标按钮区域。形状作为代理对象的子节点,跟随代理对象移动和缩放,于是缕空区域就随着变化。

    这一块的实现方式能分享下吗,shader是什么样的

    1. 这是 shader 的代码,https://gist.github.com/mutoo/56ddba95bd6a5ef62e43 就是一个默认的透明贴图 shader 加上一句 blend Zero OneMinusSrcAplha

      1. xuanyuan xuanyuan

        真是太棒了,实现了镂空,灰常感谢啊

        1. xuanyuan xuanyuan

          是通过"BroadcastMessage" 这个来传递的吗?

  4. xuanyuan xuanyuan

    Event Trigger是如何代理传递点击事件到目标对象的呢?

    1. 令 targetBtnCom 为指定的按钮组件,直接 targetBtnCom.onClick.Invoke (); 就行了。

      1. xuanyuan xuanyuan

        我EventTrigger代理的目标对象 未必都是按钮 也有可能是 Image、Panel等
        这样的方式 只能针对于按钮 才有onClick

      2. xuanyuan xuanyuan

        targetDisplay.BroadcastMessage("OnPointerClick", eventData);
        我现在用这样的方式 完成了 点击消息的传递

        1. 嗯,也是可行的。

评论已关闭