目前已经历了两款游戏的制作。而两款游戏的新手引导,都是由我来完成的。因此,想写篇文章记录制作新手引导过程中的一些心得。
http://blog.csdn.net/operhero1990/article/details/51482734
从触发方式上,引导分为强制引导和非强制引导。现在国产游戏上来就是一大段的强制引导,强制玩家点击某一区域来熟悉游戏。强制引导过程中,玩家没有多余的操作选择。这种方式虽然受到了很多的诟病,但依然大行其道,这背后的市场数据分析暂且不予讨论吧。而非强制指引,是根据游戏进度和剧情需要,触发不同的引导。再我看来,非强制指引,只不过是由某种特殊事件触发了一段强制指引行为而已。所以,强制指引是实质,非强制指引只是再次基础上做了灵活运用。
在来看看强制指引。大体上可以分为对白和指引点击两种情况。对白用于介绍剧情,指引点击则指引玩家具体操作。由于项目使用的是cocos引擎,且游戏逻辑使用lua脚本编写,因此下面将以cocos-lua方式距离说明。
如何构建一套完整的新手引导系统呢?最终要的就是为策划构建一张新手引导表格,再以此表格为依据,实现一个管理类guideMgr以及相应的表现UI(假设我们按上节提到的对白和指引的分类方式,则需要分别实现guideDlg和guideOper)。
<表格>
那一张新手引导表格需要哪些内容呢?这就需要设想新手引导会遇到哪些问题:
1、同步服务器?告诉服务器我完成到了哪一步,一次来获得相应奖励或判断完成新手引导
2、重启游戏后继续引导?因各种原因重启游戏后,需要从上次中断处继续引导,第一个问题解决了,这个问题也好解决
3、游戏随机性大,如何确保引导不出问题?因为新手引导需要确保流程的通畅,以为游戏中的偶然因素需要排除。可能需要做一些特殊的处理,比如构建一些虚拟数据来取缔原来的游戏随机数据
4、如何指引特定界面的特定按钮?如何确定上一步指引打开了指定界面,并且当前指引了特定的按钮
带着以上问题,我们来设计引导表格:
指引id。id有两个作用,第一个是用于客户端与服务器的同步,第二个是方便客户端使用id做特殊处理。这是为了解决问题1、2、3。
类型变量type。他的作用是指明这是什么指引类型(对白or操作指引)
指引所在场景ui。解决问题4。
指引按钮widget。解决问题4。
是否同步sync。解决问题1。
当然表格里还可以有其它内容,比如显示的图片路劲,文字内容以及面对各种复杂情况的需求(实际解决问题的时候,自行扩充吧)
假设我们需要指引玩家签到,正常流程是在主场景或主UI(MainScene)上点击签到按钮(btnSign),签到界面(SignUI)打开后,点击签到(btnOK),最后关闭签到界面(btnClose)。注意,如果玩家在1002步点击了签到按钮,这时候他退出了游戏,重进游戏后,应当判断他完成了签到指引而跳过1003步。所以1002步上传给服务器的同步id应该是1004。这样,重启游戏时,客户端从服务器获得同步id1004,从1004继续指引。
guideMgr首先需要同步服务器指引信息。通过syncFromServer(id)来同步从服务器发来的信息(在进入游戏时执行),syncToServer(id)来向服务器同步信息。
其次,guideMgr需要知道指定界面是否打开(由于某些界面需要网络返回,因此不是及时打开,这时候指引应该被挂起,直到等到特定界面)。如果你的游戏中有UIMgr,那这件事情就相当好办。当通过UIMgr创建一个界面时,调用guideMgr.addUI(uiname, uiInstance)方法,通知guideMgr你所需要的界面已经打开。通过UIMgr关闭界面时,调用guideMgr.removeUI(uiname)来删除记录。
其次,guideMgr提供方法stepEx()供外部调用。当指引所在场景的指引按钮被点击时,主动调用guideMgr.stepEx(),告诉guideMgr我完成这一步指引啦,快点开始下一步吧!
--
-- Author: operhero1990
--
local guideMgr = {}
local isGuiding -- 是否开启指引
local isSearchingUI -- 等待指引的场景打开,进行指引
local uis -- 打开的界面集合
local guideIndex = 1 -- 指引的顺序索引
local guideInfo -- 当前引导信息
local function startCurGuide()
if guideInfo and uis[guideInfo.ui] then
isSearchingUI = false
if guideInfo.guideType == 1 then
UIMgr:showUI("UI_guideDlg", guideInfo)
elseif guideInfo.guideType == 2 then
UIMgr:showEx("UI_guideOper", guideInfo, uis[guideInfo.ui])
end
else
isSearchingUI = true
end
end
local function finishCurGuide()
if guideInfo.guideType == 1 then
UIMgr:removeUI("UI_guideDlg")
elseif guideInfo.guideType == 2 then
if uis["UI_guideOper"] then
uis["UI_guideOper"]:onEnd()
UIMgr:removeUI("UI_guideOper")
end
end
if guideInfo.sync ~= -1 then
Framework.Uti:SendMsg("cSyncGuide " .. guideInfo.sync)
end
end
function guideMgr.init()
guideIndex = 1
isGuiding = false
isSearchingUI = false
uis = {}
guideInfo = nil
end
-- 添加当前打开的所有UI界面
function guideMgr.addUI(uiname,uiInstance)
uis[uiname] = uiInstance
if isSearchingUI then
startCurGuide()
end
end
-- 移除关闭的界面
function guideMgr.removeUI(uiname,ui)
if uis[uiname] then
uis[uiname] = nil
end
end
-- 同步服务器数据
function guideMgr.syncFromServer(idx)
-- 没有同步数据则从第一部开始指引
if idx == -1 then
guideInfo = game.data["Guide"][1]
else
for i = 1,#game.data["Guide"] do
if idx == game.data["Guide"][i].id then
guideInfo = game.data["Guide"][i]
guideIndex = i
break
end
end
end
if guideInfo == nil then
isGuiding = false
uis = {}
return
end
guideMgr.run()
end
-- 重进游戏后,一些断开的步骤需要重新打开之前的界面
function guideMgr.restartCheck()
end
-- 开始新手引导
function guideMgr.run()
isGuiding = true
guideMgr.restartCheck()
startCurGuide()
end
function guideMgr.stepEx()
if isGuiding == false then
return
end
finishCurGuide()
if guideIndex < #game.data["Guide"] then
guideIndex = guideIndex + 1
guideInfo = game.data["Guide"][guideIndex]
startCurGuide()
return
end
Framework.Uti:SendMsg("cSyncGuide over")
isGuiding = false
isSearchingUI = false
guideInfo = nil
end
-- 是否进行新手引导
function guideMgr.isGuiding()
return isGuiding
end
-- 获取当前引导信息
function guideMgr.getInfo()
return guideInfo
end
return guideMgr
这部分暂略吧,无法就是现实一些图片和一些文字,点击屏幕任意位置就结束现实,调用guideMgr.stepEx()进入下一步
在guideMgr的startCurGuide()方法中,创建了guideOper,并将指引信息(表中当前指引行数据)和需指引界面实力uiInstance传递给guideOper。uiInstance需要实现三个方法,uiInstance.onGuideBegin(id)、uiInstance.onWidgetTouched(id, touchEvent)、uiInstance.onGuideEnd(id),实现指引前的数据初始化(如果需要),点击操作的响应(需要相应地方调用guideMgr.stepEx()结束这一步的指引),指引完成后的处理(如果需要)。这部分也不打算贴代码了,因为实际游戏中,有些特殊情况需要特殊处理(比如有点击指引,也有拖拽指引。有高亮点击区域的美术需求,有指引特效等等),实际代码比较杂。guideOper是遮罩在游戏层之上的,他的底层baffleLayer会吞噬点击事件(让玩家不会乱点),并且依据需要指引的widget,初始化touchLayer大小与位置。
当touchLayer接收到点击事件后,会调用uiInstance.onWidgetTouched函数来向下传递点击事件。
源码实现比较简单,这里提供一段有用的代码,就是让点击区域高亮,其余地方用半透明黑色蒙版遮罩,先看效果
再上代码:
local function highlightWidget(widget)
local clip = cc.ClippingNode:create()
clip:setAnchorPoint(0, 0)
clip:setPosition(0,0)
clip:setName("guideClip")
clip:setInverted(true)
clip:setAlphaThreshold(0)
self.uiInstance.csbNode:getParent():addChild(clip)
local layerBg = cc.LayerColor:create(cc.c4b(0, 0, 0, 150))
clip:addChild(layerBg)
local stencil = widget:clone()
local ach = widget:getAnchorPoint()
local p1 = widget:convertToWorldSpace(cc.p(0,0))
stencil:setAnchorPoint(cc.p(0,0))
stencil:setPosition(p1)
clip:setStencil(stencil)
clip:setInverted(true)
clip:setAlphaThreshold(0.0)
end
看下cocos裁剪节点的知识就懂啦!