tvOS 10:SpriteKit和Focus Engine入门

大约一年前,Apple发布了AppleTV 4,并通过tvOS 9首次为开发人员开放了该平台。tvOS和iOS之间最大的区别可能是用户与系统的交互方式。 在iOS上,他们通常使用手指,但在tvOS上,使用游戏控制器或iPhone的“ Remote”应用可通过随附的遥控器取消互动。

为了管理这种新的输入法,苹果公司创建了一种叫做焦点引擎的东西。 在焦点引擎内,屏幕上的任何一个元素都具有当前焦点,并且焦点在用户导航时在一个元素和另一个元素之间移动。 如果用户按下按钮,则输入将定向到当前关注的元素。

在tvOS 9中,焦点引擎仅在UIKit中受支持,因此SpriteKit开发人员可以滚动自己的系统。 在tvOS 10中,焦点引擎已扩展为支持SpriteKit,目的是使SpriteKit游戏中的交互性更符合UIKit和基于TVML的应用程序。

我目前正在将我的iOS游戏Mazy移植到tvOS,并且其中一部分采用了新的Focus引擎来为游戏菜单提供动力。 我想在这里分享我的经验,希望您在从事类似工作时能从中受益。

有2个WWDC视频,介绍SpriteKit中新的焦点引擎集成。 在tvOS上进行焦点交互(SpriteKit位从15:20开始,但是如果您不熟悉焦点引擎,则值得观看整个视频)。 SpriteKit的新增功能中也介绍了该引擎(焦点位从39:15开始)。

我有一个github存储库,您可以克隆以遵循以下示例。 你可以在这里找到它。 由于需要tvOS 10,它需要Xcode 8 beta,它也是使用Swift 3编写的。下面的每个部分都有一个场景。 只需在GameViewController.swift中更新此行以选择相关场景即可:

 让场景= SimpleMenuScene(大小:view.frame.size) 

SimpleMenuScene

首先让我们得到一个简单的菜单。 SimpleMenuButton是SKNode的子类,将显示一个字符串,并在聚焦时更改颜色。 我们将从垂直列表中的3个SimpleMenuButton开始。 为了使项目可聚焦,它必须实现UIFocusItem。 SKNode已经实现了此功能,因此我们可以在SimpeMenuButton子类中重写它,如下所示:

 覆盖public var canBecomeFocused:Bool { 
得到{
返回真
}
}

(请注意,在Swift 2.3中,canBecomeFocused是函数而不是属性)。

现在我们需要处理焦点更改,我们通过实现UIFocusEnvironment中的didUpdateFocus来实现。 您可以在关注的项目或其父项之一上实施此操作。 在这里,我已经在SimpleMenuScene上实现了它

 覆盖func didUpdateFocus(在上下文中:UIFocusUpdateContext,与协调者:UIFocusAnimationCoordinator){ 
让prevItem = context.previouslyFocusedItem
让nextItem = context.nextFocusedItem

如果让prevButton = prevItem为? SimpleMenuButton {
prevButton.buttonDidLoseFocus()
}
如果让nextButton = nextItem为? SimpleMenuButton {
nextButton.buttonDidGetFocus()
}
}

运行该项目,您会看到3个按钮,当前已聚焦的按钮为红色。 如果您在模拟器上,则可以使用箭头键在项目之间向上/向下移动。

理解焦点引擎的重要部分是了解它是如何制定焦点决策的。 幸运的是,在Xcode中可以直观地看到决策。 启动应用程序,然后在didUpdateFocus上设置一个断点。 进行焦点更改,然后对“ context”变量执行QuickLook(按空格键)。

您应该看到以下内容:

当前选择的焦点项目以红色突出显示。 下面的其他两个按钮显示为紫色。 这里要注意的重要一点是,我们没有专门设置这些可聚焦区域的大小,它们只是SKNode的大小。 具体来说,它们是由calculateAccumulatedFrame()返回的大小。

DiagonalMenuScene1

该场景与SimpleMenuScene相同,不同之处在于按钮已按如下所示放置在对角线上:

如果将GameViewController更新为指向DiagonalMenuScene1并启动应用程序,您将看到无法在项目之间导航。 如果您在didUpdateFocus上设置了一个断点,那么除了应用程序首次启动以设置初始焦点时,它从未被调用过。

这给我们带来了一些调试上的麻烦,因为似乎没有一种可视的方式来调试它。 但是,由于在SimpleMenu场景的可视化调试中看到了什么,我们确实知道发生了什么。

当焦点引擎从Button1接收到一个“下移”请求时,它会寻找与从Button1框架向下延伸的矩形相交的任何节点。 这与按钮2或3不相交,因此它们都不能聚焦。

DiagonalMenuScene2

要变通解决此问题,想到了两个选择。 可以在每个按钮后面设置一个足够宽的颜色背景节点,以使其重叠,或者覆盖calculateAccumulatedFrame,以使框架宽度重叠。 使用UIView时,如果焦点引擎未将alpha设置为零,则会将其排除在外,但是在SpriteKit中仍可以选择该节点(不确定这是故意的还是错误的……)

让我们像在DiagonalMenuButton中那样重写calculateAccumulatedFrame:

 覆盖func computeAccumulatedFrame()-> CGRect { 
让框架= super.calculateAccumulatedFrame()
返回CGRect(来源:CGPoint(x:0,y:frame.origin.y),大小:CGSize(宽度:UIScreen.main.bounds.width,高度:frame.size.height))
}

将场景切换到DiagonalMenuScene2,该场景使用DiagonalMenuButton,然后再次执行并设置断点,您现在可以看到焦点按预期工作。

PositionedMenuScene1

我现在要警告您一些我挠挠了很长时间的事情! 将场景更改为PositionedMenuScene1。

这里唯一的区别是3个按钮已添加到“ menuStack” SKNode,并且menuStack的位置已设置为(100,100)。 运行项目并拉起上下文QuickLook,您将看到以下内容:

在这里,您可以看到焦点引擎认为节点与实际位置之间的对齐方式。 这里似乎正在发生的事情是,焦点引擎没有将节点的位置转换为根视图的坐标空间。

PositionedMenuScene2

只是为了演示由此引起的问题,将场景更改为PositionedMenuScene2,它与Scene1相同,但是具有第二个“堆栈”按钮。 如果您运行该项目,您会发现甚至无法选择第一个堆栈中的项目,因为焦点引擎会在同一位置看到两个堆栈。

PositionedMenuScene3

在这种情况下,我一直使用的解决方法是使用焦点引擎使用的“阴影”节点,但用户看不见它们。

在PositionedMenuScene3中,有一个新的PositionedMenuButton。 它具有将ShadowMenuButton添加到与其自身相同的位置,但直接附加到顶部父级的功能。

然后将ShadowMenuButton设置为交互式,然后将buttonDid * Focus函数仅传递回PositionedMenuButton。

  func addShadowNodeToTopParent(){ 

让shadowButton = ShadowMenuButton(大小:self.children.first!.frame.size,locatedMenuButton:self)
  var topParent:SKNode = self 
 而topParent.parent!= nil { 
topParent = topParent.parent!
}

shadowButton.position = topParent.convert(self.position,来自:self.parent!)

shadowButton.isUserInteractionEnabled = true

topParent.addChild(shadowButton)
}

ShadowMenuButton在此处已设置为蓝色,但在清除时的作用相同。

首选焦点环境

现在,我们可以在菜单中成功移动焦点了,在这篇文章中,我只想介绍几个其他主题。 首先是“优先关注环境”。

您可能已经在PositionedMenuScene3中注意到Button6是默认突出显示的按钮,这是通过让焦点引擎知道PreferredFocusEnvironments是什么来实现的。 当给出环境列表时,聚焦引擎将依次尝试每个环境,直到找到可以成功聚焦的环境为止。

preferredFocusEnvionments是UIFocusEnvironment协议的属性。 对于要询问场景的环境,主GameViewController需要响应preferredFocusEnvironment并给出场景作为其首选环境,如下所示:

 覆盖var preferredFocusEnvironment:[UIFocusEnvironment] { 
如果让场景= currentScene {
返回[场景]
}其他{
返回[]
}
}

然后PositionedMenuScene3可以使用环境列表进行响应,这里只是ShadowMenuButtons列表的相反顺序

 覆盖var preferredFocusEnvironment:[UIFocusEnvironment] { 
return button.map {$ 0.shadowButton! 如UIFocusEnvironment} .reversed()
}

处理选择— SelectableMenuScene

我在这里讨论的最后一件事是处理用户选择。 我们要检测的是用户按下触摸板。 为此,我们需要检测UIPress事件。

最简单的方法是利用UITapGestureRecgonizer。 默认情况下,它检测到“选择”按钮,但是您可以对其进行调整以检测您感兴趣的任何按钮。

切换GameViewController以使用SelectableMenuScene。 您可以看到识别器已添加,如下所示:

  func addTapGestureRecognizer(){ 
让tapRecognizer = UITapGestureRecognizer(target:self,action:#selector(self.tapped(sender :)))
self.view?.addGestureRecognizer(tapRecognizer)
}

在处理轻击时,我们利用UIScreen.main.focusedItem来获取当前焦点的项目,然后将轻击通过ShadowMenuButton传递到PositionedMenuButton。

 轻按了func(sender:AnyObject){ 
如果让focussedItem = UIScreen.main.focusedItem为? ShadowMenuButton {
focussedItem.positionedMenuButton?.tapped()
}
}

PositionedMenuButton只是打印出SKLabelNode的文本值。

感谢您阅读本文!

我希望您发现这篇文章有用,SpriteKit中的焦点引擎是全新的,并且在某些地方有点怪! 我花了几天的时间来完成所有这些工作,希望以上内容对您有所帮助。

反馈一如既往的赞赏!