SpriteKit From Scratch Técnicas avanzadas y optimizaciones

Introducción

En este tutorial, la quinta y última entrega de la serie SpriteKit From Scratch, analizamos 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 OS X 10.11.4 SDK. Para continuar, 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 gran fuente para encontrar ilustraciones y gráficos para tus juegos.

1. Atlas de textura

Con el fin de optimizar el uso de la memoria de su juego, SpriteKit proporciona la funcionalidad de los atlas de textura en forma de SKTextureAtlas clase. Estos atlas combinan efectivamente las texturas que usted especifica en una sola textura grande que ocupa menos memoria que las texturas individuales por sí mismas.. 

Afortunadamente, Xcode puede crear atlas de textura muy fácilmente para ti. Esto se hace en los mismos catálogos de activos que se utilizan para otras imágenes y recursos en sus juegos. Abre tu proyecto y navega a la Assets.xcassets Catálogo de activos. En la parte inferior de la barra lateral izquierda, haga clic en + botón y seleccione el Nuevo Sprite Atlas opción.

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 cambiar su nombre. Nombralo Obstáculos. A continuación, arrastre el Obstáculo 1Obstáculo 2 recursos en esta carpeta. También puede borrar el espacio en blanco. Duende activo que Xcode genera si lo desea, pero eso no es necesario. Cuando termines, tu expandido Obstáculos textura atlas debe tener este aspecto:

Ahora es el momento de usar el Atlas de texturas en el código. Abierto MainScene.swift y añadir la siguiente propiedad a la MainScene clase. Inicializamos un atlas de textura usando el nombre que ingresamos en nuestro catálogo de activos..

dejar obstáculosAtlas = SKTextureAtlas (llamado: "Obstáculos")

Si bien no es necesario, puede precargar los datos de un atlas de textura en la memoria antes de usarlos. Esto permite que su juego elimine cualquier retraso que pueda ocurrir al cargar el atlas de textura y recuperar la primera textura de él. La precarga de un atlas de textura se realiza con un solo método y también puede ejecutar un bloque de código personalizado una vez que se haya completado la carga.

En el MainScene clase, agregue el siguiente código al final de la didMoveToView (_ :) método:

anular la función didMoveToView (ver: SKView) ... obstáculosAtlas.preloadWithCompletionHandler // Hacer algo una vez que se haya cargado el atlas de textura

Para recuperar una textura de un atlas de textura, utiliza la texturaNamed (_ :) Método con el nombre que especificó en el catálogo de activos como un parámetro. Vamos a actualizar el spawnObstacle (_ :) método en el MainScene Clase para usar el atlas de textura que creamos hace un momento. Obtenemos 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, el valor más alto: 2) let texture = obstáculosAtlas.textureNamed ("Obstacle \ (spriteGenerator)") let obstacles = SKSpriteNode (textura: textura) obstacle.xScale = 0.3 obstacle.yScale = 0.3 permite que physicsBody = SKPhysicsBody (circleOfRadius: 15) physicsBody.contact ánimoPaciosvacunidadConsciencia de los Estados Unidos de América .width / 2.0, diferencia = CGFloat (85.0) var x: CGFloat = 0 let laneGenerator = GKShuffledDistribution (lowestValue: 1, highestValue: 3) switch laneGenerator.nextInt () caso 1: x = diferencia de centro caso 2: x = centro caso 3: x = centro + diferencia predeterminado: fatalError ("Número fuera de [1, 3] generado") obstacle.position = CGPoint (x: x, y: (player.position.y + 800)) addChild ( obstáculo) obstacle.lightingBitMask = 0xFFFFFFFF obstacle.shadowCastBitMask = 0xFFFF FFFF

Tenga en cuenta que, si su juego aprovecha los recursos bajo demanda (ODR), puede especificar fácilmente una o más etiquetas para cada atlas de textura. Una vez que haya accedido con éxito a la (s) etiqueta (s) de recursos correcta (s) con las API de ODR, puede usar su atlas de textura tal como lo hicimos en el spawnObstacle (_ :) método. Puede leer más acerca de los recursos a pedido en otro tutorial mío..

2. Guardando y cargando escenas

SpriteKit también le ofrece la posibilidad de guardar y cargar escenas fácilmente desde y hacia el almacenamiento persistente. Esto permite que los jugadores abandonen tu juego, lo vuelvan a lanzar en un momento posterior y sigan estando en el mismo punto de tu juego que antes..

El guardado y carga de tu juego es manejado por el NSCoding protocolo, que el SKScene La clase ya se ajusta a. La implementación de SpriteKit de los métodos requeridos por este protocolo permite automáticamente que todos los detalles en su escena se guarden y carguen muy fácilmente. Si lo desea, también puede anular estos métodos para guardar algunos datos personalizados junto con su escena.

Porque nuestro juego es muy básico, vamos a utilizar un sencillo Bool Valor para indicar si el coche se ha estrellado. Esto le muestra cómo guardar y cargar datos personalizados vinculados a una escena. Agregue los siguientes dos métodos de NSCoding protocolo a la MainScene clase.

// MARCAR: - NSCoding Protocol required init? (Coder aDecoder: NSCoder) super.init (coder: aDecoder) permite que carHasCrashed = aDecoder.decodeBoolForKey ("carCrashed") imprima ("car crashed: \ (carHasCrashing)") anulación func encodeWithCoder (aCoder: NSCoder) super.encodeWithCoder (aCoder) deja que carHasCrashed = player.hidden aCoder.encodeBool (carHasCrashed, forKey: "carCrashed")

Si no está familiarizado con el NSCoding protocolo, el encodeWithCoder (_ :) El método maneja el guardado de su escena y el inicializador con una sola NSCoder el parámetro maneja la carga.

A continuación, agregue el siguiente método a la MainScene clase. los saveScene () método crea un NSData Representación de la escena, utilizando el NSKeyedArchiver clase. 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 el MainScene clase con lo siguiente:

func didBeginContact (contact: SKPhysicsContact) if contact.bodyA.node == player || contact.bodyB.node == jugador si permite explosionPath = NSBundle.mainBundle (). pathForResource ("Explosion", ofType: "sks"), smokeyPath = NSBundle.mainBundle (). pathForResource ("Smoke", ofType: " sks "), deje explosion = NSKeyedUnarchiver.unarchiveObjectWithFile (explosionPath) como? SKEmitterNode, let smoke = NSKeyedUnarchiver.unarchiveObjectWithFile (smokePath) como? 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 a este método es editar el nodo del jugador categoríaBitMask En lugar de sacarlo de la escena por completo. Esto asegura que, al volver a cargar la escena, el nodo del jugador todavía esté allí, aunque no esté visible, pero no se detecten colisiones duplicadas. El otro cambio realizado es llamar al saveScene () método que definimos anteriormente una vez que se ejecutó la lógica de explosión personalizada.

Por fin abrir ViewController.swift y reemplazar el viewDidLoad () Método con la siguiente implementación:

anular func viewDidLoad () super.viewDidLoad () deja que skView = SKView (frame: view.frame) var scene: MainScene? si se permite savedSceneData = NSUserDefaults.standardUserDefaults (). objectForKey ("currentScene") como? NSData, deje savedScene = NSKeyedUnarchiver.unarchiveObjectWithData (savedSceneData) como? MainScene scene = savedScene else if let url = NSBundle.mainBundle (). URLForResource ("MainScene", withExtension: "sks"), permite newSceneData = NSDucios de los empacadores de los animales (empacado). ? 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 (estados: [left, middle, right]) stateMachine? .enterState (MiddleLane)

Al cargar la escena, primero verificamos si hay datos guardados en el estándar NSUserDefaults. Si es así, recuperamos estos datos y recreamos el MainScene objeto usando el NSKeyedUnarchiver clase. De lo contrario, obtenemos la URL del archivo de escena que creamos en Xcode y cargamos los datos de forma similar..

Ejecuta tu aplicación y encuentra un obstáculo con tu coche. En esta etapa, no ves una diferencia. Sin embargo, vuelva a ejecutar la aplicación y verá que su escena ha sido restaurada exactamente como estaba cuando se estrelló el auto..

3. El bucle de animación

Antes de que cada cuadro de su juego se renderice, SpriteKit ejecuta una serie de procesos en un orden particular. Este grupo de procesos se conoce como el bucle de animación. Estos procesos dan cuenta de las acciones, propiedades físicas y restricciones que ha agregado a su escena..

Si, por alguna razón, necesita ejecutar un código personalizado entre cualquiera de estos procesos, puede anular algunos métodos específicos en su SKScene subclase o especifique un delegado que se ajuste a la SKSceneDelegate protocolo. Tenga en cuenta que, si asigna un delegado a su escena, no se invocan las implementaciones de clase de los siguientes métodos.

Los procesos de bucle de animación son los siguientes:

Paso 1

La escena llama a su actualizar(_:) método. Este método tiene una sola NSTimeInterval parámetro, que le da la hora actual del sistema. Este intervalo de tiempo puede ser útil, ya que le permite calcular el tiempo que tardó en renderizar su cuadro anterior.

Si el valor es mayor a 1/60 de segundo, tu juego no se está ejecutando a los 60 cuadros por segundo (FPS) que SpriteKit apunta. Esto significa que es posible que necesite cambiar algunos aspectos de su escena (por ejemplo, partículas, número de nodos) para reducir su complejidad.

Paso 2

La escena ejecuta y calcula las acciones que ha agregado a sus nodos y las posiciona en consecuencia.

Paso 3

La escena llama a su didEvaluateActions () método. Aquí es donde puede realizar cualquier lógica personalizada antes de que SpriteKit continúe con el bucle de animación.

Etapa 4

La escena realiza sus simulaciones de física y cambia su escena en consecuencia.

Paso 5

La escena llama a su didSimulate Physics () método, que puede anular con el didEvaluateActions () método.

Paso 6

La escena aplica las restricciones que ha agregado a sus nodos..

Paso 7

La escena llama a su didApplyConstraints () Método, el cual está disponible para que lo invalides..

Paso 8

La escena llama a su didFinishUpdate () Método, que también se puede anular. Este es el método final en el que puede cambiar su escena antes de que finalice su aparición para ese fotograma.

Paso 9

Finalmente, la escena reproduce su contenido y actualiza su contenido. SKView en consecuencia.

Es importante tener en cuenta que, si usa un SKSceneDelegate Objeto en lugar de una subclase personalizada, cada método gana un parámetro adicional y cambia su nombre ligeramente. El parámetro extra es un SKScene objeto, que le permite determinar a qué escena se está ejecutando el método en relación con. Los métodos definidos por el SKSceneDelegate protocolo se nombran de la siguiente manera:

  • actualización (_: forScene :)
  • didEvaluateActionsForScene (_ :)
  • didSimulatePhysicsForScene (_ :)
  • didApplyConstraintsForScene (_ :)
  • didFinishUpdateForScene (_ :)

Incluso si no utiliza estos métodos para realizar cambios en la escena, pueden ser muy útiles para la depuración. Si su juego se retrasa constantemente y la velocidad de fotogramas disminuye en un momento determinado de su juego, podría anular cualquier combinación de los métodos anteriores y encontrar el intervalo de tiempo entre cada uno de los que se llama. Esto le permite encontrar con precisión si 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

Dibujo por lotes

Al representar su escena, SpriteKit, de forma predeterminada, recorre los nodos de su escena. niños array y los dibuja en la pantalla en el mismo orden en que están en el array. Este proceso también se repite y se repite para cualquier nodo secundario que un nodo particular pueda tener.

Enumerar individualmente a través de nodos secundarios significa que SpriteKit ejecuta una llamada de sorteo para cada nodo. Mientras que para escenas simples, este método de representación no afecta significativamente el rendimiento, a medida que su escena gana más nodos, este proceso se vuelve muy ineficiente..

Para hacer que la representación sea más eficiente, puede organizar los nodos de su escena en distintas capas. Esto se hace a través de la zPosición propiedad de la SKNode clase. Cuanto más alto es un nodo zPosición Es decir, el "más cerca" está de la pantalla, lo que significa que se representa sobre otros nodos de la escena. Igualmente, el nodo con el más bajo. zPosición en una escena aparece en la parte "atrás" y puede ser superpuesta por cualquier otro nodo.

Después de organizar los nodos en capas, puede establecer una SKView objetos ignoreSiblingOrder propiedad a cierto. Esto resulta en SpriteKit usando el zPosición valores para representar una escena en lugar del orden del niños formación. Este proceso es mucho más eficiente que cualquier nodo con el mismo zPosición se agrupan en un solo sorteo en lugar de tener uno para cada nodo.

Es importante tener en cuenta que la zPosición el valor de un nodo puede ser negativo si es necesario. Los nodos en tu escena todavía se están procesando en orden de aumento zPosición.

Evitar animaciones personalizadas

Ambos SKAcción y SKConstraint las clases contienen una gran cantidad de reglas que puedes agregar a una escena para crear animaciones. Al ser parte del marco de SpriteKit, están optimizados tanto como pueden y también encajan perfectamente con el bucle de animación de SpriteKit.

La amplia gama de acciones y restricciones que se le brindan le permiten casi cualquier animación 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 de nodos razonablemente grande, los campos de fuerza física también pueden producir el resultado que desea. Los campos de fuerza son incluso 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 adecuadas para los nodos de 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 físicas y la iluminación afectan los nodos de una escena..

Para cualquier par de nodos en una escena, independientemente de si colisionarán o no alguna vez, SpriteKit monitorea dónde se relacionan entre sí. Esto significa que, si se deja con las máscaras predeterminadas con todos los bits habilitados, SpriteKit está haciendo 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 físicas de SpriteKit definiendo las máscaras de bits adecuadas para que solo se rastreen las relaciones entre los nodos que potencialmente pueden colisionar..

Del mismo modo, una luz en SpriteKit solo afecta a un nodo si la lógica Y de su categoría máscaras de bits es un valor distinto de cero. Al editar estas categorías, para que solo los nodos más importantes de su escena se vean afectados por una luz particular, puede reducir enormemente la complejidad de una escena.

Conclusión

Ahora debe saber cómo puede optimizar aún más sus juegos SpriteKit mediante el uso de técnicas más avanzadas, como atlas de textura, dibujo por lotes y máscaras de bits optimizadas. También debe sentirse cómodo guardando y cargando escenas para brindar a sus jugadores una mejor experiencia general.

A lo largo de esta serie, hemos analizado muchas de las características y funcionalidades del framework SpriteKit en iOS, tvOS y OS X. Hay temas aún más avanzados que van más allá del alcance de esta serie, como OpenGL ES y Metal Shaders personalizados. Como campos de física y articulaciones..

Si desea obtener más información sobre estos temas, recomiendo comenzar con la Referencia del Marco SpriteKit y leer sobre las clases relevantes.

Como siempre, asegúrese de dejar sus comentarios y sugerencias en los comentarios a continuación.