如何在spritekit中创建垂直滚动菜单?

时间:2015-12-11 18:07:00

标签: swift sprite-kit

我想用我的游戏(In SpriteKit)创建一个带有按钮和图像的商店,但我需要这些项目可以滚动,这样玩家就可以在商店中上下滚动(就像一个UITableView,但有多个SKSpriteNodes和每个单元格中的SKLabelNodes)。知道我怎么能在SpriteKit中做到这一点吗?

4 个答案:

答案 0 :(得分:17)

承诺的第二个答案,我只是想出了问题。

我建议总是从我的gitHub项目中获取此代码的最新版本,因为我做了更改,因为这个答案,链接在底部。

第1步:创建一个新的swift文件并粘贴此代码

import SpriteKit

/// Scroll direction
enum ScrollDirection {
    case vertical // cases start with small letters as I am following Swift 3 guildlines.
    case horizontal
}

class CustomScrollView: UIScrollView {

// MARK: - Static Properties

/// Touches allowed
static var disabledTouches = false

/// Scroll view
private static var scrollView: UIScrollView!

// MARK: - Properties

/// Current scene
private let currentScene: SKScene

/// Moveable node
private let moveableNode: SKNode

/// Scroll direction
private let scrollDirection: ScrollDirection

/// Touched nodes
private var nodesTouched = [AnyObject]()

// MARK: - Init
init(frame: CGRect, scene: SKScene, moveableNode: SKNode) {
    self.currentScene = scene
    self.moveableNode = moveableNode
    self.scrollDirection = scrollDirection
    super.init(frame: frame)

    CustomScrollView.scrollView = self
    self.frame = frame
    delegate = self
    indicatorStyle = .White
    scrollEnabled = true
    userInteractionEnabled = true
    //canCancelContentTouches = false
    //self.minimumZoomScale = 1
    //self.maximumZoomScale = 3

    if scrollDirection == .horizontal {
        let flip = CGAffineTransformMakeScale(-1,-1)
        transform = flip
    }
}

required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
   }
}

// MARK: - Touches
extension CustomScrollView {

/// Began
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {

    for touch in touches {
        let location = touch.locationInNode(currentScene)

        guard !CustomScrollView.disabledTouches else { return }

        /// Call touches began in current scene
        currentScene.touchesBegan(touches, withEvent: event)

        /// Call touches began in all touched nodes in the current scene
        nodesTouched = currentScene.nodesAtPoint(location)
        for node in nodesTouched {
            node.touchesBegan(touches, withEvent: event)
        }
    }
}

/// Moved
override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {

    for touch in touches {
        let location = touch.locationInNode(currentScene)

        guard !CustomScrollView.disabledTouches else { return }

        /// Call touches moved in current scene
        currentScene.touchesMoved(touches, withEvent: event)

        /// Call touches moved in all touched nodes in the current scene
        nodesTouched = currentScene.nodesAtPoint(location)
        for node in nodesTouched {
            node.touchesMoved(touches, withEvent: event)
        }
    }
}

/// Ended
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {

    for touch in touches {
        let location = touch.locationInNode(currentScene)

        guard !CustomScrollView.disabledTouches else { return }

        /// Call touches ended in current scene
        currentScene.touchesEnded(touches, withEvent: event)

        /// Call touches ended in all touched nodes in the current scene
        nodesTouched = currentScene.nodesAtPoint(location)
        for node in nodesTouched {
            node.touchesEnded(touches, withEvent: event)
        }
    }
}

/// Cancelled
override func touchesCancelled(touches: Set<UITouch>?, withEvent event: UIEvent?) {

    for touch in touches! {
        let location = touch.locationInNode(currentScene)

        guard !CustomScrollView.disabledTouches else { return }

        /// Call touches cancelled in current scene
        currentScene.touchesCancelled(touches, withEvent: event)

        /// Call touches cancelled in all touched nodes in the current scene
        nodesTouched = currentScene.nodesAtPoint(location)
        for node in nodesTouched {
            node.touchesCancelled(touches, withEvent: event)
        }
     }
   }
}

// MARK: - Touch Controls
extension CustomScrollView {

     /// Disable
    class func disable() {
        CustomScrollView.scrollView?.userInteractionEnabled = false
        CustomScrollView.disabledTouches = true
    }

    /// Enable
    class func enable() {
        CustomScrollView.scrollView?.userInteractionEnabled = true
        CustomScrollView.disabledTouches = false
    }
}

// MARK: - Delegates
extension CustomScrollView: UIScrollViewDelegate {

    func scrollViewDidScroll(scrollView: UIScrollView) {

        if scrollDirection == .horizontal {
            moveableNode.position.x = scrollView.contentOffset.x
        } else {
            moveableNode.position.y = scrollView.contentOffset.y
        }
    }
}

这将成为UIScrollView的子类并设置它的基本属性。它有自己的触摸方法,可以传递到相关的场景。

步骤2:在您想要使用的相关场景中,您可以创建滚动视图和可移动节点属性,如此

weak var scrollView: CustomScrollView!
let moveableNode = SKNode()

并将它们添加到didMoveToView

中的场景中
scrollView = CustomScrollView(frame: CGRect(x: 0, y: 0, width: self.frame.size.width, height: self.frame.size.height), scene: self, moveableNode: moveableNode, scrollDirection: .vertical)
scrollView.contentSize = CGSizeMake(self.frame.size.width, self.frame.size.height * 2)
view?.addSubview(scrollView) 


addChild(moveableNode)

您在第1行中执行的操作是使用场景维度初始化滚动视图助手。您还传递场景以供参考,以及您在步骤2中创建的moveableNode。 第2行是设置scrollView的内容大小的地方,在这种情况下,它是屏幕高度的两倍。

步骤3: - 添加标签或节点等并定位它们。

label1.position.y = CGRectGetMidY(self.frame) - self.frame.size.height
moveableNode.addChild(label1)

在此示例中,标签将位于scrollView的第2页上。这是您必须使用标签和定位的地方。

我建议如果滚动视图中有很多页面,并且有很多标签要执行以下操作。在滚动视图中为每个页面创建一个SKSpriteNode,并使每个页面都与屏幕大小相同。将它们称为page1Node,page2Node等。您可以将第二页上所需的所有标签添加到page2Node。这里的好处是你基本上可以像往常一样在page2Node中定位你所有的东西,而不仅仅是在scrollView中定位page2Node。

你也很幸运,因为垂直使用scrollView(你说你想要的)你不需要做任何翻转和反向定位。

我制作了一些类func,所以如果你需要禁用你的scrollView,你可以在scrollView上覆盖另一个菜单。

CustomScrollView.enable()
CustomScrollView.disable()

最后不要忘记在转换到新场景之前从场景中删除滚动视图。在spritekit中处理UIKit时的痛苦之一。

scrollView?.removeFromSuperView()

对于水平滚动,只需将init方法的滚动方向更改为.horizo​​ntal(步骤2)。

现在最大的痛苦是当定位东西时一切都反过来。所以滚动视图从右到左。所以你需要使用scrollView&#34; contentOffset&#34;重新定位它的方法,基本上从右到左以相反的顺序放置所有标签。一旦了解了最新情况,再次使用SkNodes可以更轻松。

希望这对大量帖子有所帮助和抱歉,但正如我所说,spritekit有点痛苦。让我知道它是怎么回事,如果我错过了什么。

项目在gitHub上

https://github.com/crashoverride777/SwiftySKScrollView

答案 1 :(得分:4)

您有2个选项

1)使用UIScrollView

这是更好的解决方案,因为你可以免费获得动量滚动,分页,弹跳效果等等。但是,您必须使用大量的UIKit内容或进行一些子分类才能使其与SKSpritenodes或标签一起使用。

在gitHub上查看我的项目以获取示例

https://github.com/crashoverride777/SwiftySKScrollView

2)使用SpriteKit

Declare 3 class variables outside of functions(under where it says 'classname': SKScene):
var startY: CGFloat = 0.0
var lastY: CGFloat = 0.0
var moveableArea = SKNode()

设置你的didMoveToView,将SKNode添加到场景中并添加2个标签,一个用于顶部,一个用于底部以查看它是否有效!

override func didMoveToView(view: SKView) {
    // set position & add scrolling/moveable node to screen
    moveableArea.position = CGPointMake(0, 0)
    self.addChild(moveableArea)

    // Create Label node and add it to the scrolling node to see it
    let top = SKLabelNode(fontNamed: "Avenir-Black")
    top.text = "Top"
    top.fontSize = CGRectGetMaxY(self.frame)/15
    top.position = CGPoint(x:CGRectGetMidX(self.frame), y:CGRectGetMaxY(self.frame)*0.9)
    moveableArea.addChild(top)

    let bottom = SKLabelNode(fontNamed: "Avenir-Black")
    bottom.text = "Bottom"
    bottom.fontSize = CGRectGetMaxY(self.frame)/20
    bottom.position = CGPoint(x:CGRectGetMidX(self.frame), y:0-CGRectGetMaxY(self.frame)*0.5)
    moveableArea.addChild(bottom)
}

然后设置你的触摸开始存储你第一次触摸的位置:

override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
    // store the starting position of the touch
    let touch: AnyObject? = touches.anyObject();
    let location = touch?.locationInNode(self)
    startY = location!.y
    lastY = location!.y
}

然后设置使用以下代码移动的触摸,以设置的速度将节点滚动到限制集:

override func touchesMoved(touches: NSSet, withEvent event: UIEvent) {
    let touch: AnyObject? = touches.anyObject();
    let location = touch?.locationInNode(self)
    // set the new location of touch
    var currentY = location!.y

    // Set Top and Bottom scroll distances, measured in screenlengths
    var topLimit:CGFloat = 0.0
    var bottomLimit:CGFloat = 0.6

    // Set scrolling speed - Higher number is faster speed
    var scrollSpeed:CGFloat = 1.0

    // calculate distance moved since last touch registered and add it to current position
    var newY = moveableArea.position.y + ((currentY - lastY)*scrollSpeed)

    // perform checks to see if new position will be over the limits, otherwise set as new position
    if newY < self.size.height*(-topLimit) {
        moveableArea.position = CGPointMake(moveableArea.position.x, self.size.height*(-topLimit))
    }
    else if newY > self.size.height*bottomLimit {
        moveableArea.position = CGPointMake(moveableArea.position.x, self.size.height*bottomLimit)
    }
    else {
        moveableArea.position = CGPointMake(moveableArea.position.x, newY)
    }

    // Set new last location for next time
    lastY = currentY
}

所有功劳归于本文

http://greenwolfdevelopment.blogspot.co.uk/2014/11/scrolling-in-sprite-kit-swift.html

答案 2 :(得分:0)

这是我们用来模拟UIScrollView菜单的SpriteKit行为的代码。

基本上,您需要使用与UIView的高度匹配的虚拟SKScene,然后将UIScrollView滚动和点击事件馈送到SKScene进行处理。

令人沮丧的是,Apple并未本地提供此功能,但希望没有其他人可以浪费时间来重建此功能!

class ScrollViewController: UIViewController, UIScrollViewDelegate {
    // IB Outlets
    @IBOutlet weak var scrollView: UIScrollView!

    // General Vars
    var scene = ScrollScene()

    // =======================================================================================================
    // MARK: Public Functions
    // =======================================================================================================
    override func viewDidLoad() {
        // Call super
        super.viewDidLoad()

        // Create scene
        scene = ScrollScene()

        // Allow other overlays to get presented
        definesPresentationContext = true

        // Create content view for scrolling since SKViews vanish with height > ~2048
        let contentHeight = scene.getScrollHeight()
        let contentFrame = CGRect(x: 0, y: 0, width: UIScreen.main.bounds.size.width, height: contentHeight)
        let contentView = UIView(frame: contentFrame)
        contentView.backgroundColor = UIColor.clear

        // Create SKView with same frame as <scrollView>, must manually compute because <scrollView> frame not ready at this point
        let scrollViewPosY = CGFloat(0)
        let scrollViewHeight = UIScreen.main.bounds.size.height - scrollViewPosY
        let scrollViewFrame = CGRect(x: 0, y: scrollViewPosY, width: UIScreen.main.bounds.size.width, height: scrollViewHeight)
        let skView = SKView(frame: scrollViewFrame)
        view.insertSubview(skView, at: 0)

        // Configure <scrollView>
        scrollView.addSubview(contentView)
        scrollView.delegate = self
        scrollView.contentSize = contentFrame.size

        // Present scene
        skView.presentScene(scene)

        // Handle taps on <scrollView>
        let tapGesture = UITapGestureRecognizer(target: self, action: #selector(scrollViewDidTap))
        scrollView.addGestureRecognizer(tapGesture)
    }

    // =======================================================================================================
    // MARK: UIScrollViewDelegate Functions
    // =======================================================================================================
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        scene.scrollBy(contentOffset: scrollView.contentOffset.y)
    }


    // =======================================================================================================
    // MARK: Gesture Functions
    // =======================================================================================================
    @objc func scrollViewDidTap(_ sender: UITapGestureRecognizer) {
        let scrollViewPoint = sender.location(in: sender.view!)
        scene.viewDidTapPoint(viewPoint: scrollViewPoint, contentOffset: scrollView.contentOffset.y)
    }
}



class ScrollScene : SKScene {
    // Layer Vars
    let scrollLayer = SKNode()

    // General Vars
    var originalPosY = CGFloat(0)


    // ================================================================================================
    // MARK: Initializers
    // ================================================================================================
    override init() {
        super.init()
    }


    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }


    // ================================================================================================
    // MARK: Public Functions
    // ================================================================================================
    func scrollBy(contentOffset: CGFloat) {
        scrollLayer.position.y = originalPosY + contentOffset
    }


    func viewDidTapPoint(viewPoint: CGPoint, contentOffset: CGFloat) {
        let nodes = getNodesTouchedFromView(point: viewPoint, contentOffset: contentOffset)
    }


    func getScrollHeight() -> CGFloat {
        return scrollLayer.calculateAccumulatedFrame().height
    }


    fileprivate func getNodesTouchedFromView(point: CGPoint, contentOffset: CGFloat) -> [SKNode] {
        var scenePoint = convertPoint(fromView: point)
        scenePoint.y += contentOffset
        return scrollLayer.nodes(at: scenePoint)
    }
}

答案 3 :(得分:0)

我喜欢添加SKCameraNode来滚动菜单场景的想法。我建立了this article真的很有用。您只需要更改相机位置即可移动菜单。在Swift 4中

var boardCamera = SKCameraNode()

override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
    for touch in touches {
        let location = touch.location(in: self)
        let previousLocation = touch.previousLocation(in: self)
        let deltaY = location.y - previousLocation.y
        boardCamera.position.y += deltaY
    }
}