Spanish (Español) translation by Elías Nicolás (you can also view the original English article)
Introducción
En este tutorial, la quinta y última entrega de la serie SpriteKit From Scratch, observamos algunas técnicas avanzadas que puede utilizar para optimizar sus juegos basados en SpriteKit para mejorar el rendimiento y la experiencia del usuario.
Este tutorial requiere que esté ejecutando Xcode 7.3 o superior, que incluye Swift 2.2 y iOS 9.3, TVOS 9.2 y SDK OS X 10.11.4. Para seguir, puede usar el proyecto que creó en el tutorial anterior o descargar una copia nueva de GitHub.
Los gráficos utilizados para el juego en esta serie se pueden encontrar en GraphicRiver. GraphicRiver es una excelente fuente para encontrar ilustraciones y gráficos para tus juegos.
1. Atlas de texturas
Con
el fin de optimizar el uso de memoria de su juego, SpriteKit
proporciona la funcionalidad de atlas de textura en forma de la clase
SKTextureAtlas
. Estos atlas combinan eficazmente las texturas que
especifique en una
única textura grande que ocupa menos memoria que las texturas
individuales por sí mismas.
Afortunadamente, Xcode puede crear atlas de textura muy fácilmente para usted. Esto se hace en los mismos catálogos de activos que se usan para otras imágenes y recursos en sus juegos. Abra su proyecto y navegue hasta el catálogo de activos Assets.xcassets. En la parte inferior de la barra lateral izquierda, haz clic en el botón + y selecciona la opción New Sprite Atlas.



Como resultado, se agrega una nueva carpeta al catálogo de activos. Haga clic en la carpeta una vez para seleccionarla y vuelva a hacer clic para cambiarle el nombre. Nómbralo Obstacles. A continuación, arrastre los recursos de Obstacle 1 y Obstacle 2 a esta carpeta. También puede eliminar el activo Sprite en blanco que Xcode genera, si así lo desea, pero eso no es obligatorio. Cuando se haya completado, su atlas de textura Obstacles ampliado debería verse así:



Ahora es el momento de usar el atlas de textura en el código. Abra MainScene.swift y agregue la siguiente propiedad a la clase MainScene
Inicializamos un atlas de textura usando el nombre que
ingresamos en nuestro catálogo de activos.
let obstaclesAtlas = SKTextureAtlas(named: "Obstacles")
Si bien no es obligatorio, puede precargar los datos de un atlas de textura en la memoria antes de usarlo. Esto
le permite a su juego eliminar cualquier retraso que pueda ocurrir al
cargar el atlas de textura y recuperar la primera textura de él. La
carga previa de un atlas de textura se realiza con un único método y
también puede ejecutar un bloque de código personalizado una vez que se
haya completado la carga.
En la clase MainScene
, agregue el siguiente código al final del método didMoveToView(_:)
:
override func didMoveToView(view: SKView) { ... obstaclesAtlas.preloadWithCompletionHandler { // Do something once texture atlas has loaded } }
Para
recuperar una textura de un atlas de textura, utilice el método
textureNamed(_:)
con el nombre que especificó en el catálogo de
activos como parámetro. Actualicemos el método spawnObstacle(_:)
en la clase MainScene
para
usar el atlas de textura que creamos hace un momento. Buscamos la textura del atlas de textura y la usamos para crear un nodo de sprite.
func spawnObstacle(timer: NSTimer) { if player.hidden { timer.invalidate() return } let spriteGenerator = GKShuffledDistribution(lowestValue: 1, highestValue: 2) let texture = obstaclesAtlas.textureNamed("Obstacle \(spriteGenerator)") let obstacle = SKSpriteNode(texture: texture) obstacle.xScale = 0.3 obstacle.yScale = 0.3 let physicsBody = SKPhysicsBody(circleOfRadius: 15) physicsBody.contactTestBitMask = 0x00000001 physicsBody.pinned = true physicsBody.allowsRotation = false obstacle.physicsBody = physicsBody let center = size.width/2.0, difference = CGFloat(85.0) var x: CGFloat = 0 let laneGenerator = GKShuffledDistribution(lowestValue: 1, highestValue: 3) switch laneGenerator.nextInt() { case 1: x = center - difference case 2: x = center case 3: x = center + difference default: fatalError("Number outside of [1, 3] generated") } obstacle.position = CGPoint(x: x, y: (player.position.y + 800)) addChild(obstacle) obstacle.lightingBitMask = 0xFFFFFFFF obstacle.shadowCastBitMask = 0xFFFFFFFF }
Tenga
en cuenta que, si su juego aprovecha los Recursos a pedido (ODR), puede
especificar fácilmente una o más etiquetas para cada atlas de textura. Una
vez que haya accedido correctamente a la (s) etiqueta (s) de recursos
correctas con las API de ODR, puede usar su atlas de texturas tal como
lo hicimos en el método spawnObstacle(_:)
. Puede leer más sobre los
recursos a pedido en otro tutorial mío.
2. Guardar y cargar escenas.
SpriteKit también le ofrece la posibilidad de guardar y cargar escenas fácilmente desde y hacia el almacenamiento persistente. Esto permite a los jugadores abandonar su juego, relanzarlo más adelante y seguir en el mismo punto de su juego como antes.
El guardar y cargar tu
juego se maneja con el protocolo NSCoding
, que la clase SKScene
ya
cumple. La
implementación de SpriteKit de los métodos requeridos por este
protocolo automáticamente permite que todos los detalles en su escena
sean guardados y cargados muy fácilmente. Si lo desea, también puede
anular estos métodos para guardar algunos datos personalizados junto con
su escena.
Debido a que nuestro juego es muy básico, vamos a usar un
valor de Bool
simple para indicar si el auto se ha estrellado. Esto le
muestra cómo guardar y cargar datos personalizados que están vinculados a
una escena. Agregue los siguientes dos métodos del protocolo NSCoding
a
la clase MainScene
// MARK: - NSCoding Protocol required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) let carHasCrashed = aDecoder.decodeBoolForKey("carCrashed") print("car crashed: \(carHasCrashed)") } override func encodeWithCoder(aCoder: NSCoder) { super.encodeWithCoder(aCoder) let carHasCrashed = player.hidden aCoder.encodeBool(carHasCrashed, forKey: "carCrashed") }
Si no está
familiarizado con el protocolo NSCoding
, el método encodeWithCoder(_:)
maneja el guardado de su escena y el inicializador con un único
parámetro NSCoder
maneja la carga.
A continuación, agregue el siguiente método a la clase MainScene
. El
método saveScene()
crea una representación NSData
de la escena,
utilizando la clase NSKeyedArchiver
. Para mantener las cosas simples,
almacenamos los datos en NSUserDefaults
.
func saveScene() { let sceneData = NSKeyedArchiver.archivedDataWithRootObject(self) NSUserDefaults.standardUserDefaults().setObject(sceneData, forKey: "currentScene") }
A continuación, reemplace la implementación de didBeginContactMethod(_:)
en la clase MainScene
con lo siguiente:
func didBeginContact(contact: SKPhysicsContact) { if contact.bodyA.node == player || contact.bodyB.node == player { if let explosionPath = NSBundle.mainBundle().pathForResource("Explosion", ofType: "sks"), let smokePath = NSBundle.mainBundle().pathForResource("Smoke", ofType: "sks"), let explosion = NSKeyedUnarchiver.unarchiveObjectWithFile(explosionPath) as? SKEmitterNode, let smoke = NSKeyedUnarchiver.unarchiveObjectWithFile(smokePath) as? SKEmitterNode { player.removeAllActions() player.hidden = true player.physicsBody?.categoryBitMask = 0 camera?.removeAllActions() explosion.position = player.position smoke.position = player.position addChild(smoke) addChild(explosion) saveScene() } } }
El primer
cambio realizado en este método es la edición de la categoryBitMask
del
nodo de jugador en lugar de eliminarla por completo de la escena. Esto
asegura que al volver a cargar la escena, el nodo de jugador todavía
está allí, a pesar de que no es visible, pero que las colisiones
duplicadas no se detectan. El otro cambio
realizado es llamar al método saveScene()
que definimos anteriormente
una vez que se ha ejecutado la lógica de explosión
personalizada.
Finalmente, abra ViewController.swift y reemplace el
método viewDidLoad()
con la siguiente implementación:
override func viewDidLoad() { super.viewDidLoad() let skView = SKView(frame: view.frame) var scene: MainScene? if let savedSceneData = NSUserDefaults.standardUserDefaults().objectForKey("currentScene") as? NSData, let savedScene = NSKeyedUnarchiver.unarchiveObjectWithData(savedSceneData) as? MainScene { scene = savedScene } else if let url = NSBundle.mainBundle().URLForResource("MainScene", withExtension: "sks"), let newSceneData = NSData(contentsOfURL: url), let newScene = NSKeyedUnarchiver.unarchiveObjectWithData(newSceneData) as? MainScene { scene = newScene } skView.presentScene(scene) view.insertSubview(skView, atIndex: 0) let left = LeftLane(player: scene!.player) let middle = MiddleLane(player: scene!.player) let right = RightLane(player: scene!.player) stateMachine = LaneStateMachine(states: [left, middle, right]) stateMachine?.enterState(MiddleLane) }
Al cargar la escena, primero verificamos si hay datos guardados en NSUserDefaults
estándar. Si es así, recuperamos esta información y recreamos el objeto MainScene
usando la clase NSKeyedUnarchiver
. De lo contrario, obtenemos la URL del
archivo de escena que creamos en Xcode y cargamos los datos de la misma
manera.
Ejecuta tu aplicación y corre hacia un obstáculo con tu auto. En esta etapa, no ves la diferencia. Sin embargo, vuelva a ejecutar su aplicación, y debería ver que su escena ha sido restaurada exactamente como era cuando acaba de estrellar el automóvil.
3. El bucle de animación
Antes de renderizar cada cuadro de tu juego, SpriteKit ejecuta una serie de procesos en un orden particular. Este grupo de procesos se conoce como el bucle de animación. Estos procesos representan las acciones, las propiedades físicas y las restricciones que ha agregado a su escena.
Si,
por algún motivo, necesita ejecutar un código personalizado entre
cualquiera de estos procesos, puede anular algunos métodos específicos
en su subclase SKScene
o especificar un delegado que cumpla con el
protocolo SKSceneDelegate
. Tenga en cuenta que, si asigna un delegado a
su escena, las
implementaciones de la clase de los siguientes métodos no se invocan.
Los procesos de ciclo de animación son los siguientes:
Paso 1
La escena llama
a su método update(_:)
. Este método tiene un único
parámetro NSTimeInterval
, que le proporciona la hora actual del sistema. Este intervalo de tiempo puede ser útil ya que le permite calcular el
tiempo que tardó en renderizar su fotograma anterior.
Si el valor es mayor a 1/60 de segundo, tu juego no se ejecuta en los suaves 60 fotogramas por segundo (FPS) a los que aspira SpriteKit. Esto significa que puede necesitar cambiar algunos aspectos de su escena (por ejemplo, partículas, número de nodos) para reducir su complejidad.
Paso 2
La escena se ejecuta y calcula las acciones que ha agregado a sus nodos y los coloca en consecuencia.
Paso 3
La escena llama a
su método didEvaluateActions()
. Aquí es donde puede realizar cualquier
lógica personalizada antes de que SpriteKit continúe con el ciclo de
animación.
Paso 4
La escena realiza sus simulaciones físicas y cambia su
escena en consecuencia.
Paso 5
La escena llama a su método
didSimulatePhysics()
, que puede anular con el método didEvaluateActions()
.
Paso 6
La escena aplica las restricciones que ha agregado a sus
nodos.
Paso 7
La escena llama a su método didApplyConstraints()
, que está
disponible para que anule.
Paso 8
La escena llama a su método
didFinishUpdate()
, que también puede anular. Este es el método final en
el que puede cambiar su escena antes de que finalice su aparición para
ese cuadro.
Paso 9
Finalmente, la escena muestra sus contenidos y
actualizaciones conteniendo SKView
en consecuencia.
Es
importante tener en cuenta que, si utiliza un objeto SKSceneDelegate
en
lugar de una subclase personalizada, cada método gana un parámetro
adicional y cambia su nombre ligeramente. El parámetro adicional es un
objeto SKScene
, que le permite determinar en qué escena se está
ejecutando el método. Los métodos definidos por el protocolo
SKSceneDelegate
se nombran de la siguiente manera:
update(_:forScene:)
-
didEvaluateActionsForScene(_:)
didSimulatePhysicsForScene(_:)
-
didApplyConstraintsForScene(_:)
-
didFinishUpdateForScene(_:)
Incluso si no usa estos métodos para realizar cambios en su escena, aún pueden ser muy útiles para la depuración. Si tu juego se retrasa constantemente y la velocidad de fotogramas cae en un momento particular de tu juego, puedes anular cualquier combinación de los métodos anteriores y encontrar el intervalo de tiempo entre cada llamada. Esto le permite encontrar con precisión si específicamente sus acciones, física, restricciones o gráficos son demasiado complejos para que su juego se ejecute a 60 FPS.
4. Mejores prácticas de rendimiento
Lote de dibujo
Al
renderizar su escena, SpriteKit, de forma predeterminada, se ejecuta a
través de los nodos en la matriz de elementos secundarios children
de la escena y
los dibuja en la pantalla en el mismo orden en que están en la matriz. Este proceso también se repite y se enlaza para cualquier nodo hijo que
un nodo particular pueda tener.
La enumeración individual de nodos secundarios significa que SpriteKit ejecuta una llamada de extracción para cada nodo. Mientras que para escenas simples este método de renderizado no afecta significativamente el rendimiento, a medida que su escena gana más nodos este proceso se vuelve muy ineficiente.
Para que la renderización
sea más eficiente, puede organizar los nodos en su escena en distintas
capas. Esto se hace a través de la propiedad zPosition
de la clase
SKNode
. Cuanto
mayor sea la zPosition
de un nodo, más "cerca" estará de la pantalla,
lo que significa que se representa encima de otros nodos en su escena. Del mismo modo, el nodo con la zPosition
más baja en una escena
aparece en el mismo "reverso" y puede ser superpuesto por cualquier otro
nodo.
Después de organizar los nodos en capas, puede establecer la
propiedad ignoreSiblingOrder
de un objeto SKView
en true
. Esto
da como resultado que SpriteKit use los valores de zPosition
para
representar una escena en lugar del orden de la matriz children
. Este
proceso es mucho más eficiente ya que todos los nodos con la
misma zPosition
se agrupan en una única llamada de extracción en lugar
de tener uno para cada nodo.
Es importante tener en cuenta que el valor
zPosition
de un nodo puede ser negativo si es necesario. Los nodos en su
escena todavía están reproduciéndose en orden de aumentar
zPosition
.
Evitar animaciones personalizadas
Las clases SKAction
y
SKConstraint
contienen una gran cantidad de reglas que puede agregar a
una escena para crear animaciones. Siendo parte del framework SpriteKit,
están optimizados tanto como
pueden y también encajan perfectamente con el circuito de animación de
SpriteKit.
La amplia gama de acciones y restricciones que se le brindan permiten casi cualquier animación posible que pueda desear. Por estas razones, se recomienda que siempre utilice acciones y restricciones en sus escenas para crear animaciones en lugar de realizar cualquier lógica personalizada en otro lugar de su código.
En algunos casos, especialmente si necesita animar un grupo razonablemente grande de nodos, los campos de fuerza física también pueden incluso producir el resultado que desea. Los campos de fuerza son aún más eficientes ya que se calculan junto con el resto de las simulaciones físicas de SpriteKit.
Máscaras de bits
Sus escenas se pueden optimizar aún más utilizando solo las máscaras de bits apropiadas para los nodos en su escena. Además de ser crucial para la detección de colisiones físicas, las máscaras de bits también determinan cómo las simulaciones de física e iluminación comunes afectan a los nodos en una escena.
Para cualquier par de nodos en una escena, independientemente de si alguna vez colisionarán o no, SpriteKit monitorea dónde están relativos entre sí. Esto significa que, si se deja con las máscaras predeterminadas con todos los bits habilitados, SpriteKit realiza un seguimiento de dónde está cada nodo en su escena en comparación con cada otro nodo. Puede simplificar en gran medida las simulaciones de física de SpriteKit definiendo las máscaras de bits apropiadas para que solo se rastreen las relaciones entre nodos que potencialmente pueden colisionar.
Del
mismo modo, una luz en SpriteKit solo afecta a un nodo si el AND
lógico
de sus máscaras de bits de categoría es un valor distinto de cero. Al
editar estas categorías, de modo que solo los nodos más importantes
de su escena se vean afectados por una luz particular, puede reducir en
gran medida la complejidad de una escena.
Conclusión
Ahora debería saber cómo puede optimizar aún más sus juegos SpriteKit utilizando técnicas más avanzadas, como atlas de textura, dibujo por lotes y máscaras de bits optimizadas. También debería sentirse cómodo al guardar y cargar escenas para brindarles a sus jugadores una mejor experiencia general.
A lo largo de esta serie, hemos analizado muchas de las funciones y funcionalidades del marco SpriteKit en iOS, TVOS y OS X. Hay temas incluso más avanzados que están fuera del alcance de esta serie, tales como los sombreadores de OpenGL ES y Metal personalizados como campos de física y articulaciones.
Si desea obtener más información sobre estos temas, le recomiendo comenzar con la Referencia SpriteKit Framework y leer en las clases relevantes.
Como siempre, asegúrese de dejar sus comentarios y comentarios en los comentarios a continuación.