【问题标题】:How to create a vertical scrolling menu in spritekit?如何在 spritekit 中创建垂直滚动菜单?
【发布时间】:2015-12-11 18:07:00
【问题描述】:

我希望在我的游戏(在 SpriteKit 中)中创建一个带有按钮和图像的商店,但我需要这些项目是可滚动的,以便玩家可以上下滚动商店(像 UITableView 但有多个 SKSpriteNodes 和每个单元格中的 SKLabelNodes)。知道如何在 SpriteKit 中做到这一点吗?

【问题讨论】:

  • Dan,你知道如何让按钮与 Crashoverride777 的滚动视图一起工作吗?我遇到了麻烦
  • @JoshSchlabach 是的,我做到了,我似乎记得这与确保滚动视图完成滚动并变为静态有关,因为它认为它仍在滚动时无法识别触摸,虽然我发现它有时有点错误所以我最终没有使用它!
  • 你当时用的是什么?我的应用需要滚动视图方面的帮助!
  • @JoshSchlabach 我确实找到了一种滚动方式,但它没有任何正常滚动的惯性(菜单项在到达底部时没有反弹,菜单停止滚动一旦你抬起你的触摸等)并发现它感觉不好,所以我实际上放弃了它并稍微改变了我的菜单设计。我猜你正在制作一个垂直滚动菜单?
  • @JoshSchlabach 使用 Crash 的方法,但在每个部分的末尾添加:print("description of what code has just done"),然后在您尝试滚动时观察控制台,它应该会告诉您它在哪里搞砸了,希望允许您会意识到为什么按钮不起作用(或者至少我就是这样做的)。抱歉,我无法提供更多帮助,我不太记得当时我做了什么来解决这个问题。

标签: swift sprite-kit


【解决方案1】:

第二个答案如约而至,我刚刚想通了。

我建议始终从我的 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 的子类并设置它的基本属性。它有自己的 touches 方法,可以传递到相关场景。

步骤 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 行是您设置滚动视图的内容大小的地方,在这种情况下,它是屏幕高度的两倍。

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

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

在此示例中,标签将位于滚动视图的第二页。这是您必须使用标签和定位的地方。

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

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

我做了一些类函数,所以如果你需要禁用你的滚动视图,以防你在滚动视图上覆盖另一个菜单。

CustomScrollView.enable()
CustomScrollView.disable()

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

scrollView?.removeFromSuperView()

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

现在最大的痛苦是在放置东西时一切都颠倒了。所以滚动视图从右到左。所以你需要使用scrollView“contentOffset”方法来重新定位它,并且基本上从右到左以相反的顺序放置你的所有标签。一旦您了解发生了什么,再次使用 SkNodes 会使这变得容易得多。

希望这对大量帖子有所帮助和抱歉,但正如我所说,这对 spritekit 来说有点痛苦。如果我错过了什么,请告诉我。

项目在 gitHub 上

https://github.com/crashoverride777/SwiftySKScrollView

【讨论】:

  • 这真是太棒了,我明天会去实现它,我会告诉你它是如何进行的。非常感谢你,你是一个绝对的救星
  • 不客气。这应该可以正常工作,我只是在 xCode 中尝试使用基本模板来刷新我的记忆。请让我知道它是怎么回事,以防我错过了什么。
  • 今天早上尝试实施它,它就像做梦一样。实际上也是一个非常聪明的方法,使用可移动节点并将节点添加到可移动节点确实有效。再次非常感谢 - 您提供了巨大的帮助并完成了出色的工作!
  • 非常感谢。万一你从这个答案中复制代码,不要。去 gitHub 获取最新版本,自从这个答案以来我已经做了一些更改和修复。
  • 问题,我不希望 ScrollView 成为屏幕的全尺寸(它似乎总是如此)。假设我想列出玩家拥有的每个朋友。我的节点很好,点击它们等等。但我不希望它滚动到屏幕的顶部和底部,只需在屏幕中间说一个 200x300 的框。
【解决方案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

【讨论】:

  • 谢谢你,这真的很有用!这种方法会允许 UIScrollView 自带的惯性吗?还是我必须自己尝试添加?
  • 不幸的是,你不会得到任何酷炫的效果。使用滚动视图,在 skscenes 中使用非常痛苦。您必须做很多技巧来注册触摸,添加 SKNodes 而不是 UIKit 对象,当您在滚动视图顶部覆盖菜单时,您必须停用它,除非您翻转滚动视图,否则事情会朝相反的方向滚动,您必须以不同的方式定位东西,因为 SpriteKit 套件的坐标与 UKit 不同。这很混乱,但它是可行的。取决于菜单对您的重要性
  • 看起来您之前推荐的方法就是答案!我想知道为什么 SpriteKit 中关于 ScrollView 的文档这么少...
  • 我的意思是,如果你对 UIKit 和故事板感到满意,你可以制作另一个视图控制器并这样做。但是,如果您只是想将其添加到 SKScene 中,那就太痛苦了。尤其是定位基本是反的。如果你愿意,我可以给你看吗?
  • 如果我使用故事板和 UIKit 创建了一个菜单,我可以用它来替换菜单当前所在的 SKScene 吗?我所有的其他场景都是 SKScenes。
【解决方案3】:

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

基本上,您需要使用与SKScene 高度匹配的虚拟UIView,然后将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)
    }
}

【讨论】:

    【解决方案4】:

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

    【讨论】:

      猜你喜欢
      • 2021-02-22
      • 2017-08-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-02-04
      相关资源
      最近更新 更多