如何在SpriteKit中创建一个垂直滚动菜单?

11

我想在我的游戏中(使用SpriteKit)创建一个商店,其中包含按钮和图像,但我需要让物品可滚动,以便玩家可以在商店内上下滚动(类似于UITableView,但每个单元格中有多个SKSpriteNode和SKLabelNode)。您有什么办法可以在SpriteKit中实现这一点吗?


丹,你解决了如何让按钮与Crashoverride777的滚动视图一起工作的问题了吗?我遇到了麻烦。 - Josh Schlabach
@JoshSchlabach 是的,我试过了。我记得这是因为要确保滚动视图完成滚动并变为静态,因为它在认为自己仍在滚动时无法识别触摸,尽管我发现有时会出现一些错误,所以最终我没有使用它! - user5669940
你当时用的是什么?我需要帮助来处理我的应用程序中的滚动视图! - Josh Schlabach
@JoshSchlabach 我确实找到了一种滚动的方法,但它没有普通滚动的惯性(当菜单项到达底部时不会反弹,当你松开触摸时菜单停止滚动等),我发现它感觉不好,所以我放弃了它并稍微改变了我的菜单设计。我猜你正在制作一种垂直滚动菜单? - user5669940
是的,我也在制作一个商店。 - Josh Schlabach
1
@JoshSchlabach 使用Crash的方法,但是在每个部分的末尾添加print("代码刚刚执行了什么操作的描述"),然后在尝试滚动时观察控制台,它应该告诉你出错的地方,并希望能让你意识到为什么按钮不能工作(至少这就是我做的)。很抱歉我无法提供更多帮助,我现在无法完全记起当时解决问题所采取的方法。 - user5669940
4个回答

21

如承诺的第二个答案,我刚解决了这个问题。

我建议从我的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)
在第一行的操作是使用场景尺寸初始化滚动视图助手。您还需要传递场景以供参考,以及在步骤2中创建的可移动节点。第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垂直滚动(如您所需),您不需要执行任何翻转和反向定位操作。
我制作了一些类函数,以便在需要在scrollView上覆盖另一个菜单时可以禁用scrollView。
CustomScrollView.enable()
CustomScrollView.disable()

最后,在切换到新场景之前,不要忘记从您的场景中删除滚动视图。这是在SpriteKit中处理UIKit时的一个问题。

scrollView?.removeFromSuperView()

要实现水平滚动,只需在初始化方法中将滚动方向更改为.horizontal(第2步)。

现在最大的问题是,定位元素时一切都是相反的。 因此,滚动视图从右到左移动。 因此,您需要使用scrollView“contentOffset”方法重新定位它,并将所有标签基本上按从右到左的相反顺序放置。 一旦理解发生的情况,再次使用SkNodes使这更加容易。

希望这有所帮助,很抱歉我发了一个庞大的帖子,但正如我所说,这是SpriteKit的一个痛点。 让我知道进展情况以及是否有遗漏的内容。

项目在gitHub上

https://github.com/crashoverride777/SwiftySKScrollView


不客气。这肯定会起作用的,我刚在xCode中使用基本模板尝试了一下以便恢复我的记忆。如果我漏掉了什么,请告诉我它的运行情况。 - crashoverride777
http://stackoverflow.com/users/4945232/crashoverride777 我该如何让滚动视图上的按钮工作? - Josh Schlabach
https://github.com/crashoverride777/Swift-SpriteKit-UIScrollView-Helper/blob/master/CustomScrollView/GameScene.swift - crashoverride777
1
非常感谢。如果您正在从此答案中复制代码,请不要这样做。请前往GitHub获取最新版本,因为我已经对其进行了一些更改和修复。 - crashoverride777
1
问题:我不想让ScrollView占据整个屏幕的大小(它似乎总是这样)。比如说,我想要列出每个玩家拥有的朋友。我可以将节点添加进去,点击等操作。但是我不希望它在屏幕顶部和底部滚动,只想让它显示在屏幕中央的一个200x300的框内。 - Ryann786
显示剩余7条评论

6

你有两个选项

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
}

然后使用以下代码设置touches moved,以按设置的速度将节点滚动到设置的限制:

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


谢谢,这真的很有用!这种方法能够允许UIScrollView带来的惯性吗?还是我需要自己尝试添加呢? - user5669940
很遗憾,你不会得到任何酷炫的效果。使用滚动视图在skscenes中非常麻烦。您必须进行大量的诡计来注册触摸事件,添加SKNodes而不是UIKit对象,当您在滚动视图上叠加菜单时,必须将其停用,除非您翻转滚动视图,否则物品会向相反方向滚动,因为SpriteKit kit具有与UKit不同的坐标系。这很混乱,但是可以做到。这取决于菜单对您的重要性。 - crashoverride777
我的意思是,如果你熟悉UIKit和storyboards,你可以创建另一个视图控制器并以这种方式完成。但是,如果你只想将其添加到SKScene中,那就很麻烦了。特别是定位,基本上是相反的。你可以自己决定,如果需要我可以给你展示一下? - crashoverride777
如果我使用Storyboard和UIKit创建了一个菜单,我可以用它来替换当前所在的SKScene吗?我的其他场景都是SKScenes。 - user5669940
抱歉,这是一个非常老的答案,我不再使用它了。我现在正在使用UIScrollView,请查看我的GitHub项目SwiftySKScrollView。我以前也没有使用过地图视图,但应该可以工作。 - crashoverride777
显示剩余6条评论

1
这是我们用来模拟 SpriteKit 菜单的 UIScrollView 行为的代码。
基本上,您需要使用一个与 SKScene 的高度相匹配的虚拟 UIView,然后将 UIScrollView 滚动和点击事件传递给 SKScene 进行处理。
令人沮丧的是,苹果公司没有提供本地支持,但希望其他人不必浪费时间重新构建此功能!
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

我喜欢添加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
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接