Sprite-kit游戏生命周期、属性和全局变量?

3
在我的Sprite-Kit游戏中,我的游戏通过各种场景,我希望某些变量在这些场景中可用。请参见以下图表:

enter image description here

所以标题屏幕加载主游戏屏幕。当“波”完成时(所有目标完成或所有生命都丧失),将呈现结束场景并显示一些统计信息。然后,如果玩家还有剩余生命,则再次加载主游戏,否则再次呈现标题屏幕。
我希望玩家的得分和剩余生命数量可以在主游戏场景和结束场景中使用;高分应在所有场景中保留。
对于这些变量,我使用全局变量,而我的大多数变量都是在类下定义的属性。
第一次我不得不这样做(对于我的得分属性,在我添加高分或多个生命之前),我在每个场景上使用了一个得分属性,并在执行下一个场景的presentScene时在场景之间传递它,但这似乎很笨拙,特别是当我不得不添加更多变量时。
是否有最佳实践来处理此问题,例如应该使用/不使用全局变量,如果不使用,则是否有正确的处理属性的方法,例如由您在哪里初始化它们控制,例如在init与didMoveToView中比较,这将确定在再次显示场景时是否重新初始化它们?
编辑:
当我从结束波浪场景返回到主游戏屏幕时,是否有什么'错误'导致主屏幕被重新初始化?有没有一种方法可以保留它(包括所有属性),使其不完全重新初始化,而是再次调用didMoveToView函数?

这可能是你正在寻找的 https://thatthinginswift.com/singletons/ - 0x141E
2
我使用sharedInstance中的一个类,因为使用了NSCoding和iCloud保存。 - Simone Pistecchia
仍在努力阅读这些答案和评论,以弄清楚我在做什么! - Steve Ives
3个回答

8
我知道很多人遵循通用规则,比如“不要使用全局变量”或“单例模式是邪恶的”。这些智慧之言有时可能适用,但另一种看待它们的方式是将其视为工具,使您更有效地完成游戏开发。
在某些情况下,全局变量/单例模式是有用的。游戏和嵌入式系统遵循与传统应用程序不同的规则。这些通常是资源受限的“应用程序”,并带有性能要求。在每个项目中,它们都会使用全局变量、单例模式或两者都使用。
实际上,我使用单例模式而不是全局变量。主要是因为我不必担心把全局变量放在哪里以及它属于哪个头文件。需要注意的是,为什么要这样做的基础与所有Swift程序员不同。对我来说,这些东西主要是用C++完成的,还有一些Obj-C和Swift。
管理游戏生命周期的需求与SpriteKit无关。我在SO上看到的许多SpriteKit帖子都将游戏状态嵌入场景或VC中。他们通常面临着如何将状态移动到下一个场景的困境。虽然这种方法可能适用于我所谓的“本地状态”(即当前屏幕、阶段等的状态),但对于全局游戏状态来说并不好。例如,如果您从主屏幕转换到选项屏幕,然后返回到主屏幕,再进入游戏,那么您如何跟踪这些选项的更改(例如更改游戏难度)?是的,您肯定可以传递字典、结构体或其他东西。但在某个时候,您会发现为所有内容提供一个共同和方便的垃圾场更加方便。这个“垃圾场”就是全局变量/单例模式。
现在,在你们开始对我大喊大叫之前,有一个关于所有这些疯狂谈论全局变量和单例模式的陷阱。我不是说要创建很多全局变量/单例模式。相反,要控制好何时/是否使用全局变量/单例模式。
我不是在说要用无数个。我只是说像一个(尽管我通常会使用一些)。接下来,我们来看一个非常简单的太空射击游戏场景。假设我有多个屏幕,例如标题屏幕、主屏幕、游戏中屏幕和结束波浪屏幕。有一些信息对于传递给不同的屏幕非常有用。例如最高分或当前分数。这些可能是您想在各种屏幕/场景上显示的值。那么,您将分数存储在哪里?如何在它们之间传递分数?例如,如果所有屏幕上都显示了最后的最高分,那么我该如何保存该值?
class GameManager : NSObject {
    // Overall game state, GameState is probably some enum
    var state:GameState
    var lastState:GameState

    // Sometimes tracking scenes is useful
    var currentScene:SKScene?
    var nextScene:SKScene? // Sometimes helpful if you need to construct the next scene and it takes a non-trivial amount of time

    // These are the globals we need to share to different "parts" of the game
    var score: UInt
    var highscore: UInt
    var lives: Uint
    var stage: Uint

    var highscores:[Uint]   // A list of all the high score for the user. You would probably want HS based on other users too

    // Egads a singleton!
    static let sharedInstance = GameManager()
}

如果这是一个全局变量,它将在任何范围之外具有实例的全局定义。例如:

var theGameManager : GameManager

所以,如果您想要更新分数,则应该执行以下操作:

theGameManager.score += 100

如果它是单例模式,则更新分数看起来会像这样:

GameManager.sharedInstance.score += 100

单例模式语法可能有点冗长,但或许比全局变量更易懂,因为可以知道它是在哪里/什么东西。

但是现在你可以拥有更多的控制权。假设当你添加分数时,每100000个分数你就会获得一个额外的生命。我可以轻松地利用setter,并使用该setter赚取额外生命。

例如这个:

GameManager.sharedInstance.score += 100 // 在setter中自动地处理了100000分数增加额外生命的魔力

与这个可能有些相似:

myScore += 100

// Do logic to find out if we get an extra life. 
// Is this done in a method and if so where does this method live?
// Or is it straight up code which will be duplicated?

像这样有助于管理全局游戏状态的代码都妥善地包含在 "GameManager" 中。换句话说,"GameManager" 不仅维护值,而且还提供了一种将功能封装在这些值周围的方式。 嗯,看起来 "currentScene" 也在这里?为什么会这样呢? "GameManager" 也是管理场景变化的绝佳方式。更可能是通过游戏状态完成。也许像这样: GameManager.sharedInstance.gameState = GameOverState 在幕后,"gameState" 的setter可以执行交换场景的魔法。
这只是这种方法实际性质的几个例子。
关于加载/存储的讨论,如果需要,也可以通过 "GameManager" 完成。虽然对我而言,我通常会根据 "GameManager" 中的某些数据将其独立处理。这带来了另一个区别。我通常会将想法封装在 "GameManager" 中。例如,我可能会有一个 "Player",这将使 "GameManager" 看起来像这样:
class GameManager : NSObject {
    // Overall game state, GameState is probably some enum
    var state:GameState
    var lastState:GameState

    // Sometimes tracking scenes is useful
    var currentScene:SKScene?
    var nextScene:SKScene? // Sometimes helpful if you need to construct the next scene and it takes a non-trivial amount of time

    // These are the globals we need to share to different "parts" of the game
    // Note player state is done through this object. This way I can pass around only player when needed.
    var player: Player

    var stage: Uint

    var highscores:[Uint]   // A list of all the high score for the user. You would probably want HS based on other users too

    // Egads a singleton!
    static let sharedInstance = GameManager()
}

你可能会说:“但我不喜欢这种全局/单例方式。为什么不直接将这个对象传递给所有需要它的人呢?”答案是“你可以”。但在实践中,你会发现这变得很繁琐。你也可能会发现,你需要GameManager,但调用者从未传递GameManager。这意味着现在你需要重新设计一些方法来传递这个对象。
那么,全局变量/单例模式是邪恶的吗?你可以自己判断。每个人都有自己的选择方案。如果它适合你,就使用它。对我来说,选择它是毫无疑问的。在实现中,使用它可以获得一致易用性,访问/管理全局游戏状态的好处

因此,我在一个已经很长的答案中添加了更多信息,以尝试澄清一些事情。

我的单例或全局变量的选择取决于使用情况、平台和语言。它还取决于游戏代码库。它不应该成为任意数据的垃圾场。如果被利用,内容应该仔细考虑,尽可能地使用其他容器(例如,将它们包装在类中)。因此,虽然我的第一个例子中有悬空的玩家信息(这样做更容易传达想法),但实际上我会让PlayerState包含玩家数据。请注意,这可能不是完整的玩家,而是一些需要在玩家游戏生命周期之外存活的共享信息。
class PlayerState {
    var score:Uint
    var highscore:Uint
    var lives:Uint
}

// Simplified GameManager
class GameManager {
    var state:GameState
    var lastState:GameState

    var player:PlayerState

    // Egads a singleton!
    static let sharedInstance = GameManager()
}

此外,我会传递 PlayerState 实例而不是期望代码总是通过全局/单例来获取它。例如,我会像这样做:
func doSomething(playerState:PlayerState) {
    var score = playerState.score   

    // Blah blah blah
}

verus

func doSomething() {
    // Whee I have singleton
    var score = GameManager.player.score

    // Blah blah blah
}

有效地,我的做法是使用单例作为更容易访问数据对象的锚点,如果需要,就将其传递。换句话说,当它们被使用时,我也不想在代码中充斥着GameManager.sharedInstance
正如提到的,这种方法肯定有很多缺点之一就是并发。然而,我要指出,即使数据不是全局/单例,数据仍然可能发生并发。
我添加这个观点是因为游戏开发(以及编码)涉及实用性。你不仅在使用系统资源平衡来展示你的游戏,同时也要考虑游戏是为人们玩而制作的。而为了让游戏运行,你需要完成它。每个人都想制作一个完美的作品,但是我们必须权衡完美所需的时间成本。在回答开始时,我指出这种技术是一种工具。全局变量/单例不适合所有人。同时,我们也不应该盲目遵循设计口号。如果它可以成为有效的工具,并帮助你更快地完成游戏,那么它是值得做的。总会有另一个游戏要写,你可以根据经验决定哪些方案行得通,哪些不行。

1
很高兴你觉得有帮助。总是好的去看待不同的方法作为可能的工具,并试着摆脱基于每个人都认为合适的第一反应。一个经常被忽略的问题是你需要完成你的项目,为此每个人都面临着权衡取舍。有时这些权衡会让人感到不安。没关系。现在,如果我是你,我可能会首先考虑单例模式和全局变量。最好的方法是尽可能多地构建,保持开放的心态,并从自己和他人的错误中学习! - Mobile Ben
@Confused 我不确定你指的是什么,如果你指的是他回答中倒数第四行,那么不,他没有涉及到我。就像我说的,代码是活的,所以游戏永远不会完成。看看今天所有成功的游戏,有多少自它诞生以来就发生了变化。 - Knight0fDragon
@Knight0fDragon:“全局变量/单例模式并不适合所有人。同时,也不应该盲目地遵循设计口号。” - Confused
@Confused 我现在正在使用手机,但如果你点击左上角的群组,选择你想要的交流群组,你会看到SO旁边有一个聊天选项,只需选择它,搜索Sprite Kit。如果没有出现,请点击类似于已归档的聊天之类的东西,以拉起由于不活动而关闭的聊天记录,然后在进入聊天时标记我。 - Knight0fDragon
@Knight0fDragon,你有你的观点。我的观点是基于参与过从数百人团队合作的大型游戏项目到小型项目的经验。人们应该吸收信息并形成自己的观点。尽管你认为不行,但在项目中可以成功使用全局变量/静态类/单例模式。如果你有机会,应该阅读那篇文章。在帖子中,其中一个人谈到了他们在《模拟人生》上所做的事情,你可能不喜欢他的观点(剧透)。 - Mobile Ben
显示剩余10条评论

6

经验之谈:如果能够避免使用全局变量,就要避免。

现在每个节点都有一个可变的字典,称为userData。它被设计用来保存节点的数据。

在创建新场景时,请从旧场景中转移所需的所有数据。

let scene = SKScene(fileNamed:....)
scene.userData["highscore"] = self.highscore
view.presentScene(scene)

现在在新场景中的didMove(view:)方法中,你需要从所述的userData读取。
正如0x141E指出的那样,请记得初始化userData。由于很可能你有一个自定义类,你可以在init方法中完成这个操作,只需记住覆盖所有指定的init方法,以便能够访问方便的方法。
另外,如Whirlwind和我之间的评论中提到的那样,SKNodeNSCoding兼容的,因此当你保存信息时,你可以将SKNode作为整体保存,userData也会一起保存。
这意味着你只需要编写一个SKNode扩展来保存/加载节点,就可以避免担心管理另一个类。

1
我想知道有多少人实际上使用/了解这个属性... 不过你得到了我的点赞,因为对于非敏感数据,每个节点都可以使用这个属性。如果我没记错的话,使用这个属性就相当于将数据保存在磁盘上的 plist 文件中,对吗? - Whirlwind
1
有时候我觉得你是游戏引擎中最简短、最突兀的评论员。而另一些时候,我认为你挖掘了一些宝藏,应该成为一名大使。这次主要是属于后者。你能用“因为”开始的短语来继续第一句话(声明),并给出一个充分的理由,然后以支持性链接结束吗? - Confused
2
你需要在使用之前创建userData字典。另外,在SKNode子类添加到场景或非SpriteKit类或函数中之前,如何访问此数据? - 0x141E
1
随着Swift和Sprite Kit不断发展,我已经没有耐心了。现在获取信息已经很困难了,因为网站不会保持最新状态,并且从来不说它是与Swift 1、1.2、2、2.2、3等一起使用的。然后祝你好运,要解释所有苹果的错误和解决方法(顺便说一句,永远不要向苹果报告错误,如果你认为我简短而突兀,他们更糟糕,而且对自己的错误无知)。如果Stack Exchange开设教程交流,那将是太棒了,因为我们可以获得所需的信息,社区也可以保持更新。 - Knight0fDragon
1
@Knight0fDragon - 很好。我总是忘记使用getter/setter,然后之后后悔不已 :-) - Steve Ives
显示剩余21条评论

4

在游戏执行过程中,可能会发生很多事情,例如游戏崩溃、玩家关闭应用程序、网络断开连接或者某个节点被其父节点暂停(因此在场景处理动作时将被跳过)。因此,我同意Simone Pistecchia的评论:你可以准备一个“存储管理器”类来加载和保存你的玩家统计数据,使用NSCoding来声明和初始化它们,并使用共享实例类来在整个项目中处理它们。通过这种方式,您可以在需要时加载和保存变量,并在游戏外(例如在主菜单中的“高分榜”场景等任何地方)显示它们。


1
Ray Wenderlich在这里也解释了这个参数 https://www.raywenderlich.com/63235/how-to-save-your-game-data-tutorial-part-1-of-2 - Alessandro Ornano
2
@SteveIves,是否过度设计是个有趣的问题。 大多数人不了解的是,游戏需要以可塑性的方式构建。 今天只是当前得分。但是明天呢?事情将会改变比你想象的要多得多。假设你发布了一款游戏,它表现很好,但人们渴望储存的能力提升,如果你采用“过度简化”的方法,那么你现在该如何实现这一点呢?你肯定可能会让它起作用,但是代价是什么呢?更严重的变化,意味着未预见到的错误风险更高。 - Mobile Ben
2
@Knight0fDragon,澄清一下,我讨论的不是优化。相反,这是应该从一开始就做的事情。最终,它提供了更好的数据封装、代码局部性和灵活性,另外一个附加好处是编码时不那么繁琐。使用Swift(或者Obj-C)编写单例并不需要太多的工作(或者正确地使用它)。人们一直在高喊“单例模式是邪恶的”这样的口号。它们是一种被过度妖魔化的有效工具。 - Mobile Ben
2
@Knight0fDragon,我不敢苟同。这确实是特定类型数据的封装。我并不是说可以随便往里面塞任何东西。在我的回答中,我实际上引用了我不只使用一个项目的例子。我还有其他的做法,但是列出来只会增加更多的困惑。代码局部性也是有的。因为GameManager管理数据。顺便说一下,我从事这方面的工作。移动设备、PC、游戏机都有。我不是在瞎猜。这些方法在实际应用中被广泛使用,但并不适合所有人。 - Mobile Ben
1
Swift 3,Foundation,OperationQueue有一个称为主操作队列的单例(WWDC 2016-Tony Parker)。它们致力于改进您在2016年使用Swift中的Cocoa和Cocoa Touch API的方式,他们赞扬Cocoa,而不是建议“越来越少地使用Swift的Cocoa”。我不知道你的信息来源是什么,我说的是Session 207,WWDC 2016。 - Alessandro Ornano
显示剩余18条评论

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