在cocos2d-x游戏中实现tips功能

原文请猛戳:
http://galoisplusplus.coding.me/blog/2016/01/16/tips-in-cocos2d-x-game/

这次分享一个简单的小功能,用cocos2d-x实现tips效果,作为之前一篇博文的后续。tips的行为很简单:点击某个node(我们不妨称它为target_node)触发,当点击区域在target_node范围时出现tips,否则隐藏tips(有些情况需要指定有效点击范围不在某些node中,我们把这些node称为exclude_nodes);当target_node位于屏幕左半边时,tips出现在target_node右侧;否则tips就出现在target_node左侧,tipstarget_node有一个固定的水平间距(我们不妨定义为DEFAULT_TIPS_DIST);tipstarget_node底部对齐,但tips不能超过屏幕范围。

不废话,先上代码:

function setupTips(params)
    local targetNode = params.target_node
    local tips = params.tips
    local excludeNodes = params.exclude_nodes or {}

    local DEFAULT_TIPS_DIST = 10
    local TIPS_ZORDER = 1000

    if tolua.isnull(targetNode) or tolua.isnull(tips) then
        return
    end

    tips:setVisible(false)
    targetNode:setTouchEnabled(false)
    display.getRunningScene():addChild(tips, TIPS_ZORDER)

    targetNode:addNodeEventListener(cc.NODE_EVENT, function(event)
        if event.name == "exit" then
            local scene = display.getRunningScene()
            scene:performWithDelay(MyPackage.callbackWrapper({scene}, function()
                if not tolua.isnull(tips) then
                    tips:setVisible(false)
                end
            end), 0)

            local eventDispatcher = display.getRunningScene():getEventDispatcher()
            eventDispatcher:removeEventListenersForTarget(targetNode)
        end
    end)
    ------------------------------------------------------------
    local function setTipsPosition()
        local leftBottomPos = MyPackage.getPositionOfNode(targetNode, display.LEFT_BOTTOM)
        local targetNodePos = display.getRunningScene():convertToNodeSpace(targetNode:getParent():convertToWorldSpace(leftBottomPos))
        local targetNodeAnchorPoint = targetNode:getAnchorPoint()
        local tipsPos = targetNodePos
        local tipsAnchorPoint = cc.p(0, 0)
        local director = cc.Director:getInstance()
        local glView = director:getOpenGLView()
        local frameSize = glView:getFrameSize()
        local viewSize = director:getVisibleSize()

        if targetNodePos.x <= frameSize.width * 0.5 / glView:getScaleX() then
            -- show tips on the right of the targetNode if the targetNode is on the left screen
            tipsPos.x = tipsPos.x + targetNode:getContentSize().width + DEFAULT_TIPS_DIST
        else
            -- show tips on the left of the targetNode otherwises
            tipsPos.x = tipsPos.x - DEFAULT_TIPS_DIST
            tipsAnchorPoint.x = 1
        end

        if targetNodePos.y + tips:getContentSize().height > viewSize.height then
            tipsPos.y = viewSize.height
            tipsAnchorPoint.y = 1
        end
        if targetNodePos.y < 0 then
            targetNodePos.y = 0
        end

        tips:ignoreAnchorPointForPosition(false)
        tips:setAnchorPoint(tipsAnchorPoint)
        tips:setPosition(tipsPos)
    end
    ------------------------------------------------------------
    local function activeFunc()
        local scene = display.getRunningScene()
        -- NOTE: delay util the next frame in order to get the correct WorldSpace position
        scene:performWithDelay(MyPackage.callbackWrapper({scene, tips}, function()
            setTipsPosition()
            tips:setVisible(true)
        end), 0)
    end

    local function inactiveFunc()
        if not tolua.isnull(tips) then
            tips:setVisible(false)
        end
    end

    local function isTouchInNode(touch, node)
        if tolua.isnull(node) or tolua.isnull(touch) then
            return false
        end

        local localLocation = node:convertToNodeSpace(touch:getLocation())
        local width = node:getContentSize().width
        local height = node:getContentSize().height
        local rect = cc.rect(0, 0, width, height)
        return getCascadeVisibility(node) and node:isRunning() and cc.rectContainsPoint(rect, localLocation)
    end

    local function isActive(touch)
        local isExcluded = false
        for _, excludeNode in ipairs(excludeNodes) do
            isExcluded = isExcluded or isTouchInNode(touch, excludeNode)
        end
        return isTouchInNode(touch, targetNode) and not isExcluded
    end

    local function onTouchBegan(touch, event)
        if isActive(touch) then
            activeFunc()
            return true
        else
            return false
        end
    end

    local function onTouchMoved(touch, event)
        local scene = display.getRunningScene()
        scene:performWithDelay(MyPackage.callbackWrapper({scene}, function()
            if isActive(touch) then
                activeFunc()
            else
                inactiveFunc()
            end
        end), 0)
    end

    local function onTouchEnded(touch, event)
        local scene = display.getRunningScene()
        scene:performWithDelay(MyPackage.callbackWrapper({scene}, function()
            inactiveFunc()
        end), 0)
    end

    local listener = cc.EventListenerTouchOneByOne:create()
    listener:registerScriptHandler(onTouchBegan, cc.Handler.EVENT_TOUCH_BEGAN)
    listener:registerScriptHandler(onTouchMoved, cc.Handler.EVENT_TOUCH_MOVED)
    listener:registerScriptHandler(onTouchEnded, cc.Handler.EVENT_TOUCH_ENDED)
    listener:registerScriptHandler(onTouchEnded, cc.Handler.EVENT_TOUCH_CANCELLED)
    local eventDispatcher = display.getRunningScene():getEventDispatcher()
    eventDispatcher:removeEventListenersForTarget(targetNode)
    eventDispatcher:addEventListenerWithSceneGraphPriority(listener, targetNode)
end

关于MyPackage.getPositionOfNodeMyPackage.callbackWrapper等helper functions请参见之前某篇博文

上面的代码基本是很简单的,除了有几点需要额外说明一下:

1.几处调用到performWithDelay的地方。这是因为我们用target_node的WorldSpace坐标来确定tips的位置,当target_node位于某个可滚动的node(如ScrollView)中时,需要延迟到下一帧才能拿到它正确的WorldSpace坐标,所以我们用了quickx定义的Node:performWithDelay来做延时。之所以用这个函数而不用scheduler,是因为它在Node的生命周期中,我们不需要担心如何去安全销毁scheduler所产生的handler。事实上我们只要看一下quickx定义在NodeEx.lua中的Node:performWithDelay就一目了然了:

function Node:performWithDelay(callback, delay)
    local action = transition.sequence({
        cc.DelayTime:create(delay),
        cc.CallFunc:create(callback),
    })
    self:runAction(action)
    return action
end

2.我们指定target_node在收到exit事件时隐藏tips,这是因为target_node可能在某些ClippingNode(如ScrollView)中,当它超出区域不再显示时,tips也不应该被显示。

3.当同一个位置有多个具有tips行为的target_node时,需要判断当前的target_node是否有显示,这需要回溯看父节点的visibilitygetCascadeVisibility定义如下:

function getCascadeVisibility(node)
    if tolua.isnull(node) then
        return true
    end
    local visibility = node:isVisible()
    if visibility then
        local parent = node:getParent()
        visibility = visibility and getCascadeVisibility(parent)
    end
    return visibility
end

相关推荐