Saltar al contenido
Ende Sun
Volver al blog

Desarrollo móvil

SpriteKit, SceneKit y Unity: qué aprendí haciendo juegos móviles con tres motores

7 min de lectura

SpriteKit, SceneKit y Unity: qué aprendí haciendo juegos móviles con tres motores

Durante el máster de desarrollo móvil pasé un cuatrimestre haciendo juegos pequeños con tres herramientas distintas: SpriteKit para 2D en iOS, SceneKit para 3D en iOS y Unity para multiplataforma. Algunos de esos ejercicios siguen en la sección de archivo de esta web: un clon de air hockey, un laberinto, un tower defense. Ninguno es un juego comercial, pero construir lo mismo tres veces me enseñó más sobre cómo funcionan los juegos que cualquier motor por separado.

Esto es lo que me llevé, ordenado según los problemas que todo juego tiene que resolver.

El mismo esqueleto bajo tres API

Detrás de nombres de clase diferentes, los tres motores comparten un mismo modelo.

  • Una escena es un árbol de nodos. Cada objeto visible es un nodo con posición, escala, rotación e hijos. El orden de dibujado sigue la posición z, parecido a un z-index en la web.
  • El bucle del juego tiene dos fases. Update lee la entrada, mueve a los personajes y resuelve las interacciones; render dibuja el resultado. Los motores apuntan a 60 fotogramas por segundo; en juegos pesados suele ser mejor un 30 estable que un 60 inestable.
  • El tiempo es explícito. El movimiento debe multiplicarse por el tiempo transcurrido desde el fotograma anterior; si no, el juego va más rápido en dispositivos más rápidos. En SceneKit ese delta lo calculas tú en el delegado del renderer:
func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
    let deltaTime = time - (previousUpdateTime ?? time)
    previousUpdateTime = time
    // move nodes using deltaTime
}

En cuanto vi ese esqueleto, cambiar de motor pasó a ser sobre todo una cuestión de vocabulario.

Escenas y transiciones

SpriteKit hace que cambiar de escena sea casi trivial: cargas la siguiente escena y le das a la vista una transición.

let transition = SKTransition.flipVertical(withDuration: 1)
if let scene = SKScene(fileNamed: "GameScene"), let view = self.view {
    view.presentScene(scene, transition: transition)
}

En Unity el equivalente es SceneManager.LoadScene("GameScene"), con una trampa en la que caí pronto: la escena tiene que estar registrada en Build Settings o la carga falla en tiempo de ejecución. Unity además destruye todos los objetos al cambiar de escena, así que para conservar datos —créditos, puntuación, ajustes— usé un pequeño singleton marcado con DontDestroyOnLoad.

Físicas: elige la forma más simple que funcione

El vocabulario de físicas es el mismo en todas partes: un cuerpo (masa, fricción, velocidad), una forma para las colisiones, uniones entre cuerpos y un mundo con gravedad. Hay tres tipos de cuerpo: los dinámicos obedecen a la física, los estáticos nunca se mueven y los cinemáticos los mueve el código, pero aun así empujan a los demás.

La regla más útil que aprendí es que la forma de colisión no tiene por qué coincidir con la malla visual. En SceneKit una esfera es más barata que una caja, una caja más barata que una cápsula, y las mallas cóncavas personalizadas conviene evitarlas por completo. Una bola con mucho detalle puede colisionar como una esfera perfecta.

Unity añade dos ajustes que conviene conocer antes de depurar nada más: Interpolate, que elimina los temblores cuando una cámara sigue a un objeto físico, y Collision Detection, que se sube a continua en objetos rápidos para que dejen de atravesar paredes.

Colisiones y raycasts

Los dos motores avisan de los contactos mediante callbacks. En SceneKit adoptas un delegado de contactos; como no sabes qué cuerpo es A y cuál es B, compruebas los dos:

func physicsWorld(_ world: SCNPhysicsWorld, didBegin contact: SCNPhysicsContact) {
    if contact.nodeA.name == "projectile" || contact.nodeB.name == "projectile" {
        let damage = contact.collisionImpulse
        // apply damage
    }
}

Unity usa OnCollisionEnter, OnCollisionStay y OnCollisionExit, además de sus variantes de trigger para zonas invisibles: «la bola se ha salido del tablero» no es más que un OnTriggerExit. Para seleccionar objetos con el dedo, lanzas un rayo desde la cámara a través del punto tocado:

Ray ray = Camera.main.ScreenPointToRay(touchPosition);
if (Physics.Raycast(ray, out RaycastHit hit, 100f)) {
    hit.rigidbody?.AddForce(-hit.normal * 10f, ForceMode.Impulse);
}

Las categorías de «qué choca con qué» son máscaras de bits en los dos frameworks de Apple: cada bit es una categoría y cada cuerpo declara con qué categorías colisiona.

La interfaz es donde más se diferencian

SpriteKit no tiene sistema de interfaz para el contenido de la escena. Un botón lo construyes tú: un sprite que registra si el toque empezó dentro y solo se dispara si también terminó dentro. Escribir esa clase pequeña me enseñó todo lo que un framework de interfaz suele hacer por ti.

Unity sí tiene una capa de interfaz sobre un Canvas, y los dos conceptos que por fin hicieron que los layouts se comportaran fueron las anclas —dónde se sujeta un elemento respecto a su padre— y el pivote —qué punto del elemento se coloca en esa posición—. Un canvas también puede situarse en world space para que la interfaz viva dentro de la escena 3D.

Entrada: toques y acelerómetro

La entrada táctil es una lista de toques activos con posición y fase. El acelerómetro es un vector de tres componentes —Input.acceleration en Unity—, suficiente para controles por inclinación como una bola rodando por un laberinto. Los botones virtuales de Unity («Fire1» y compañía) son útiles para prototipar en escritorio antes de pasar al táctil.

Tamaños de pantalla: decide qué se puede recortar

Móviles y tabletas tienen relaciones de aspecto muy distintas, así que todo juego necesita una estrategia explícita:

  • Aspect fill para juegos con scroll en dos dimensiones, asegurándote de que lo importante quede dentro de la zona siempre visible.
  • Ancho fijo para juegos de scroll vertical y alto fijo para los de scroll horizontal.

En SpriteKit usé una pequeña extensión que mantiene la altura de la escena y calcula el ancho a partir de la proporción del dispositivo. En Unity la misma idea se aplica a través del campo de visión de la cámara. Además, los elementos del HUD se anclan a los bordes de la pantalla, y los menús van centrados, en vertical y con un tamaño pensado para la yema del dedo.

Recursos: sprite sheets y tilemaps

Hay dos optimizaciones que aparecen en todo juego 2D. Los sprite sheets agrupan todos los fotogramas de una animación en una sola textura (idealmente con dimensiones potencia de dos) y se generan con herramientas como TexturePacker. Los tilemaps construyen fondos grandes a partir de piezas pequeñas reutilizables en lugar de una imagen enorme; yo los diseñaba en Tiled, con capas de patrones para lo visual y capas de objetos para marcar puntos de aparición y zonas invisibles. El parallax —capas lejanas que se desplazan más despacio que las cercanas— da profundidad casi gratis.

Logros y marcadores

Game Center en iOS y Google Play Games en Android cubren logros y marcadores. El consejo de diseño que se me quedó: distinguir los logros de progreso, que empujan a terminar el juego, de los extra, que aumentan la rejugabilidad, y no gastar todo el presupuesto de logros en la primera versión: observa cómo juega la gente de verdad y añade logros después.

Un pequeño desvío por la realidad virtual

El último bloque del curso fue realidad virtual en Unity. Google Cardboard solo necesita su SDK y una cámara estéreo. Gear VR exigía firmar la build para el dispositivo de pruebas concreto, identificado con adb devices, antes de que el visor la aceptara: un recordatorio de que la mitad del desarrollo móvil es herramientas y aprovisionamiento, no código.

Lo que me diría antes de empezar

  • Aprende primero el modelo común —árbol de nodos, bucle del juego, delta de tiempo, cuerpos y formas— y cualquier motor se vuelve más fácil.
  • Elige las formas de colisión por rendimiento, no por estética.
  • Decide la estrategia de adaptación a pantalla antes de dibujar un solo recurso.
  • Elige el motor según el alcance: SpriteKit para un juego 2D concreto en iOS, SceneKit para 3D ligero en plataformas Apple, Unity cuando necesitas varias plataformas, un editor más completo o realidad virtual.