How to create a vertical scrolling menu in spritekit?

后端 未结 4 1251
悲&欢浪女
悲&欢浪女 2020-12-02 13:56

I\'m looking to create a shop in my game (In SpriteKit) with buttons and images, but I need the items to be scrollable so the player can scroll up and down the shop (Like a

相关标签:
4条回答
  • 2020-12-02 14:26

    I like the idea of add a SKCameraNode to scroll my menu-scene. I've founded this article really useful. You just have to change the camera position to move your menu. In 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
        }
    }
    
    0 讨论(0)
  • 2020-12-02 14:29

    The second answer as promised, I just figured out the issue.

    I recommend to always get the latest version of this code from my gitHub project incase I made changes since this answer, link is at the bottom.

    Step 1: Create a new swift file and paste in this code

    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
            }
        }
    }
    

    This make a subclass of UIScrollView and sets up the basic properties of it. It than has its own touches method which get passed along to the relevant scene.

    Step2: In your relevant scene you want to use it you create a scroll view and moveable node property like so

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

    and add them to the scene in 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)
    

    What you do here in line 1 is you init the scroll view helper with you scene dimensions. You also pass along the scene for reference and the moveableNode you created at step 2. Line 2 is where you set up the content size of the scrollView, in this case its twice as long as the screen height.

    Step3: - Add you labels or nodes etc and position them.

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

    in this example the label would be on the 2nd page in the scrollView. This is where you have to play around with you labels and positioning.

    I recommend that if you have a lot pages in the scroll view and a lot of labels to do the following. Create a SKSpriteNode for each page in the scroll view and make each of them the size of the screen. Call them like page1Node, page2Node etc. You than add all the labels you want for example on the second page to page2Node. The benefit here is that you basically can position all your stuff as usual within page2Node and than just position page2Node in the scrollView.

    You are also in luck because using the scrollView vertically (which u said you want) you dont need to do any flipping and reverse positioning.

    I made some class func so if you need to disable your scrollView incase you overlay another menu ontop of the scrollView.

    CustomScrollView.enable()
    CustomScrollView.disable()
    

    And finally do not forget to remove the scroll view from your scene before transitioning to a new one. One of the pains when dealing with UIKit in spritekit.

    scrollView?.removeFromSuperView()
    

    For horizontal scrolling simply change the scroll direction on the init method to .horizontal (step 2).

    And now the biggest pain is that everything is in reverse when positioning stuff. So the scroll view goes from right to left. So you need to use the scrollView "contentOffset" method to reposition it and basically place all your labels in reverse order from right to left. Using SkNodes again makes this much easier once you understand whats happening.

    Hope this helps and sorry for the massive post but as I said it is a bit of a pain in spritekit. Let me know how it goes and if I missed anything.

    Project is on gitHub

    https://github.com/crashoverride777/SwiftySKScrollView

    0 讨论(0)
  • 2020-12-02 14:38

    You have 2 options

    1) Use a UIScrollView

    Down the road this is the better solution as you get things such as momentum scrolling, paging, bounce effects etc for free. However you have to either use a lot of UIKit stuff or do some sub classing to make it work with SKSpritenodes or labels.

    Check my project on gitHub for an example

    https://github.com/crashoverride777/SwiftySKScrollView

    2) Use 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()
    

    Set up your didMoveToView, add the SKNode to the scene and add 2 labels, one for the top and one for the bottom to see it working!

    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)
    }
    

    Then set up your touches began to store position of your first touch:

    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
    }
    

    Then set up touches moved with the following code to scroll the node by to the limits set, at the speed set:

    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
    }
    

    All credit goes to this article

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

    0 讨论(0)
  • 2020-12-02 14:39

    Here's the code we used to simulate UIScrollView behavior for SpriteKit menus.

    Basically, you need to use a dummy UIView that matches the height of the SKScene then feed UIScrollView scroll and tap events to the SKScene for processing.

    It's frustrating Apple doesn't provide this natively, but hopefully no one else has to waste time rebuilding this functionality!

    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)
        }
    }
    
    0 讨论(0)
提交回复
热议问题