我知道很多人遵循通用规则,比如“不要使用全局变量”或“单例模式是邪恶的”。这些智慧之言有时可能适用,但另一种看待它们的方式是将其视为工具,使您更有效地完成游戏开发。
在某些情况下,全局变量/单例模式是有用的。游戏和嵌入式系统遵循与传统应用程序不同的规则。这些通常是资源受限的“应用程序”,并带有性能要求。在每个项目中,它们都会使用全局变量、单例模式或两者都使用。
实际上,我使用单例模式而不是全局变量。主要是因为我不必担心把全局变量放在哪里以及它属于哪个头文件。需要注意的是,为什么要这样做的基础与所有Swift程序员不同。对我来说,这些东西主要是用C++完成的,还有一些Obj-C和Swift。
管理游戏生命周期的需求与SpriteKit无关。我在SO上看到的许多SpriteKit帖子都将游戏状态嵌入场景或VC中。他们通常面临着如何将状态移动到下一个场景的困境。虽然这种方法可能适用于我所谓的“本地状态”(即当前屏幕、阶段等的状态),但对于全局游戏状态来说并不好。例如,如果您从主屏幕转换到选项屏幕,然后返回到主屏幕,再进入游戏,那么您如何跟踪这些选项的更改(例如更改游戏难度)?是的,您肯定可以传递字典、结构体或其他东西。但在某个时候,您会发现为所有内容提供一个共同和方便的垃圾场更加方便。这个“垃圾场”就是全局变量/单例模式。
现在,在你们开始对我大喊大叫之前,有一个关于所有这些疯狂谈论全局变量和单例模式的陷阱。我不是说要创建很多全局变量/单例模式。相反,要控制好何时/是否使用全局变量/单例模式。
我不是在说要用无数个。我只是说像一个(尽管我通常会使用一些)。接下来,我们来看一个非常简单的太空射击游戏场景。假设我有多个屏幕,例如标题屏幕、主屏幕、游戏中屏幕和结束波浪屏幕。有一些信息对于传递给不同的屏幕非常有用。例如最高分或当前分数。这些可能是您想在各种屏幕/场景上显示的值。那么,您将分数存储在哪里?如何在它们之间传递分数?例如,如果所有屏幕上都显示了最后的最高分,那么我该如何保存该值?
class GameManager : NSObject {
var state:GameState
var lastState:GameState
var currentScene:SKScene?
var nextScene:SKScene?
var score: UInt
var highscore: UInt
var lives: Uint
var stage: Uint
var highscores:[Uint]
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 {
var state:GameState
var lastState:GameState
var currentScene:SKScene?
var nextScene:SKScene?
var player: Player
var stage: Uint
var highscores:[Uint]
static let sharedInstance = GameManager()
}
你可能会说:“但我不喜欢这种全局/单例方式。为什么不直接将这个对象传递给所有需要它的人呢?”答案是“你可以”。但在实践中,你会发现这变得很繁琐。你也可能会发现,你需要
GameManager
,但调用者从未传递
GameManager
。这意味着现在你需要重新设计一些方法来传递这个对象。
那么,全局变量/单例模式是邪恶的吗?你可以自己判断。每个人都有自己的选择方案。如果它适合你,就使用它。对我来说,选择它是毫无疑问的。在实现中,使用它可以获得
一致易用性,访问/管理全局游戏状态的好处。
因此,我在一个已经很长的答案中添加了更多信息,以尝试澄清一些事情。
我的单例或全局变量的选择取决于使用情况、平台和语言。它还取决于游戏代码库。它不应该成为任意数据的垃圾场。如果被利用,内容应该仔细考虑,尽可能地使用其他容器(例如,将它们包装在类中)。因此,虽然我的第一个例子中有悬空的玩家信息(这样做更容易传达想法),但实际上我会让
PlayerState
包含玩家数据。请注意,这可能不是完整的玩家,而是一些需要在玩家游戏生命周期之外存活的共享信息。
class PlayerState {
var score:Uint
var highscore:Uint
var lives:Uint
}
class GameManager {
var state:GameState
var lastState:GameState
var player:PlayerState
static let sharedInstance = GameManager()
}
此外,我会传递
PlayerState
实例而不是期望代码总是通过全局/单例来获取它。例如,我会像这样做:
func doSomething(playerState:PlayerState) {
var score = playerState.score
}
verus
func doSomething() {
var score = GameManager.player.score
}
有效地,我的做法是使用单例作为更容易访问数据对象的
锚点,如果需要,就将其传递。换句话说,当它们被使用时,我也不想在代码中充斥着
GameManager.sharedInstance
。
正如提到的,这种方法肯定有很多缺点之一就是并发。然而,我要指出,即使数据不是全局/单例,数据仍然可能发生并发。
我添加这个观点是因为游戏开发(以及编码)涉及实用性。你不仅在使用系统资源平衡来展示你的游戏,同时也要考虑游戏是为人们玩而制作的。而为了让游戏运行,你需要完成它。每个人都想制作一个完美的作品,但是我们必须权衡完美所需的时间成本。在回答开始时,我指出这种技术是一种工具。全局变量/单例不适合所有人。同时,我们也不应该盲目遵循设计口号。如果它可以成为有效的工具,并帮助你更快地完成游戏,那么它是值得做的。总会有另一个游戏要写,你可以根据经验决定哪些方案行得通,哪些不行。