如何从 Sprite Kit 场景中引用当前的 Viewcontroller

Posted

技术标签:

【中文标题】如何从 Sprite Kit 场景中引用当前的 Viewcontroller【英文标题】:How to reference the current Viewcontroller from a Sprite Kit Scene 【发布时间】:2014-02-17 11:25:41 【问题描述】:

我很难找到这个问题的答案,我认为这并不难。

如何从 SKScene 引用视图控制器上定义的方法和属性?

并以此为基础:如何从另一个 SKScene 中加载的 SKScene 引用 ViewController?

【问题讨论】:

检索当前的 rootViewController 对你来说应该足够了,除非你需要做一些特定的事情。你可以这样做 [UIApplication sharedApplication].keyWindow.rootViewController; .其他解决方案是子类化 SKScene 并为其提供presentingVC 属性,在创建每个场景时设置此属性以发送它。 你应该避免从SKScene引用UIViewController,因为它破坏了MVC模式 就像@AndreyGordeev 提到的,你真的应该避免直接引用你的场景的viewController。它永远不必直接了解 viewController。然而,有意义的是在您的 SKScene 子类上实现一个委托协议,并将 viewController 设置为这个委托。或许您应该退后一步,告诉我们您想要实现的目标,而不是具体的实现方式? 我将 Game Center 身份验证逻辑添加到我的主 ViewController。仅供参考,我正在使用单个视图控制器,因为这是一个简单的游戏。我在我的视图控制器上保留了跟踪一般 GameCenter 可用性+经过身份验证的用户的属性。现在,当我的游戏结束时,我想向游戏中心报告我的分数。因此,我需要与我的视图控制器取得联系,以检查游戏中心功能是否可用以及用户是否已登录。我不想将此属性传递给我的 SKScene,因为我不确定当用户在播放过程中未经身份验证时会发生什么。 【参考方案1】:

比我的“反向引用”答案更好的选择是使用协议来明确定义 SKScene 子类期望 UIViewController 子类实现的方法。以下是您需要完成的步骤。

1) 定义UIViewController 需要采用并遵守的协议(合同):

protocol GameManager
    func loadHomeScene()
    func loadGameScene(level:Int)
    func loadHighScoreScene()

2) UIViewController 子类现在采用协议:

class GameViewController: UIViewController,GameManager

3) GameScene 获得一个引用 UIViewController 的 ivar:

var gameManager:GameManager?

4) 当GameManager 需要调用UIViewController 时,是这样的:

gameManager.loadHighScoreScene()

我们现在有一个明确定义的合同,即GameManager 是什么。在这个项目上工作的任何其他开发人员(以及当前开发人员未来的自己)现在都知道GameManager 应该实现哪些方法。这意味着我们可以轻松地将这段代码移植到不同的项目(因此也可以移植到不同的UIViewController),只要这个新的UIViewController 采用GameManager 协议,它仍然可以工作。

同样,如果我们决定将场景导航代码移出视图控制器,而是将其放在单例类中,则该单例类只需要采用GameManager 协议即可。

使用哪种方法?

这个帖子提供了 4 种解决问题的方法,我个人使用了所有 4 种方法。以下是我对它们的看法:

1) 硬编码对UIViewController 子类的调用:

(self.view!.window!.rootViewController as! GameViewController).loadGameScene(1)

优势:1行代码!

缺点:两个类的非常紧密的耦合,没有明确的契约。

2) 使用指向UIViewController的指针:

优点:只需要很少的代码,不需要协议。

缺点:2个类的紧密耦合。没有明确的合同。

3) 使用NSNotificationCenter

优势:松散耦合的类。

缺点:设置广播器、接收器和回调方法需要更多代码。更难跟踪和调试代码。没有明确的合同。通知的“一对多”功能很好,但在这里对我们没有帮助。

4) 创建协议:

优势:明确的合同。 MVC 的众神会很高兴的。

缺点:要编写更多代码(但比通知少得多)。

【讨论】:

【参考方案2】:

您应该避免从SKScene 引用UIViewController,因为它破坏了 MVC 模式。

作为替代方式,您可以使用NSNotificationCenter 通知UIViewController 高分:

ViewController:

- (void)awakeFromNib 
    [[NSNotificationCenter defaultCenter] 
        addObserver:self
        selector:@selector(reportScore:)
        name:@"ReportScore"
        object:nil];



-(void)reportScore:(NSNotification *) notification 
    NSDictionary *userInfo = [notification userInfo];
    NSNumber *scores = (NSNumber *)[userInfo objectForKey:@"scores"];
    // do your stuff with GameCenter....



- (void) dealloc

    // If you don't remove yourself as an observer, the Notification Center
    // will continue to try and send notification objects to the deallocated
    // object.
    [[NSNotificationCenter defaultCenter] removeObserver:self];

SKScene:

- (void)gameOver 
    NSDictionary *userInfo = [NSDictionary dictionaryWithObject:
         [NSNumber numberWithInt:self.scores forKey:@"scores"];
    [[NSNotificationCenter defaultCenter]
         postNotificationName:@"ReportScore" object:self userInfo:userInfo];

【讨论】:

在 SKScene 中,您忘记了 NSNumber 的结束 ']'。顺便谢谢你的回答。【参考方案3】:

理论上你不应该这样做,但实际上......

self.view.window.rootViewController

【讨论】:

Swift 版本更丑(self.view!.window!.rootViewController as! GameViewController).loadGameScene()【参考方案4】:

感谢您的 NSNotification 方法将精灵套件场景退出到调用视图控制器。这是跳回 UIKit 并发布我的代码的简单方法。我的工作速度很快,刚刚为那些使用 Swift 的人添加了 4 行代码。这段代码是在 viewDidLoad 函数中添加到视图控制器的。

override func viewDidLoad() 
    super.viewDidLoad()

    if let scene = GameLevels.unarchiveFromFile("GameLevels") as? GameLevels
        // Configure the view.
        let skView = self.view as! SKView
        skView.showsFPS = true
        skView.showsNodeCount = true

        /* Sprite Kit applies additional optimizations to improve rendering performance */
        skView.ignoresSiblingOrder = true

        /* Set the scale mode to scale to fit the window */
        scene.scaleMode = .AspectFill

        //set up notification so scene can get back to this view controller
        NSNotificationCenter.defaultCenter().addObserver(self, selector: "quitToLevel:", name: "quitToLevelID", object: nil)
        skView.presentScene(scene)
    


// Function to pop this view controller and go back to my Levels screen
func quitToLevel(notification: NSNotification) 
    self.navigationController!.popViewControllerAnimated(true)

我将此代码添加到我的 SKscene 中的 touchesBegan 函数中以返回视图控制器

            else if nodeTouched.name == "quit"
        
            NSNotificationCenter.defaultCenter().postNotificationName("quitToLevelID", object: nil)
        

现在,当我点击“退出”标签节点时,它会让我在呈现我的场景的视图控制器中运行一个名为 quitToLevel 的函数。由于我使用的是导航控制器,因此此功能将我弹回主屏幕,但可用于任何目的。

我尝试了其他几种退出场景的方法,但如果我在多个场景中跳来跳去失去对视图控制器的引用,就会遇到问题。我可以在任何场景中使用此方法

【讨论】:

很好的解决方案,因为它非常简单,只需添加几行即可。谢谢。【参考方案5】:

这不是最优雅的解决方案,但是从您的 SKScene 子类到 GameViewController 的“反向引用”怎么样?像这样的:

// call from GameViewController
let scene = HomeScene(size:screenSize, scaleMode:SKSceneScaleMode.AspectFill, viewController: self)

您的 SKScene 子类如下所示:

class HomeScene: SKScene 
    var viewController:GameViewController
    init(size:CGSize, scaleMode:SKSceneScaleMode, viewController:GameViewController) 
        self.viewController = viewController
        super.init(size:size)
        self.scaleMode = scaleMode
    

及以后:

func gameOver()
    self.viewController.loadHighScoreScene()

SKNode 及其子类已经拥有.parent.scene 属性,所以这种方法并非闻所未闻。 是的,我们正在对依赖项进行硬编码并更紧密地耦合我们的代码,但我们正在编写 less 代码,所以它不会全是坏事。

【讨论】:

以上是关于如何从 Sprite Kit 场景中引用当前的 Viewcontroller的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Sprite Kit 中初始化所有场景?

如何枚举 Sprite Kit 场景中的所有节点?

如何在场景中放置场景 - Sprite Kit

xcode sprite kit 背景音乐

Sprite Kit:如何在运行场景之上添加场景

如何在我的 Sprite Kit 游戏应用中实现 Facebook 和 Twitter 发布?