Código fuente de la App de Ejemplo
Apenas minutos después del anuncio de la llegada de Swift en el WWDC 2014 ya se sentía en el Moscone Center una mezcla de duda, pánico y entusiasmo. Creo que mi sentimiento era de duda pues con lo poco que conozco de Objective C y de programación en general no sufro el pánico por lo conocido en ObjC pero tampoco entiendo el entusiasmo promovido por Craig con las nuevas características de Swift. Para poder evaluar realmente las ventajas o desventajas de Swift mi mejor opción es simplemente experimentar.
Hasta ahora mi experiencia para gráficos 2D ha sido con Cocos2D usando SpriteBuilder para el montaje de las escenas y TexturePacker para preparar los sprite sheets. Me pareció una buena idea aprovechar mis experimentos con Swift e integrar SpriteKit, principalmente para probar lo de per pixel collision.
Creando un nuevo proyecto en XCode 6
Comencemos creando un nuevo proyecto en XCode 6. Como template vamos a elegir las opciones iOS -> application y Game. Luego le colocamos nombre y definimos Swift como language y como Game Technology utilizaremos SpriteKit con Swift.
Limitemos en este caso la orientación a las opciones Landscape derecha e izquierda. Aunque la opción de cambiar la versión de iOS no está deshabilitada, este tutorial no funciona con versiones anteriores a iOS 8.0 por lo que debe quedar Deployment Target en 8.0 y en caso de probarlo en un dispositivo éste debe tener instalado iOS 8 también.
Este template tiene, además de las imágenes, los archivos de soporte y las carpetas de test y productos cinco archivos. Tres son swift, un storyboard y una escena. En este tutorial sólo vamos a agregar imágenes y editar los archivos GameScene.swift, GameScene.sks y GameViewController.swift. En GameScene.swift borremos el contenido de las funciondidMoveToView y en touchesBegan borramos lo que está dentro del for para quedar así:
import SpriteKit class GameScene: SKScene { override func didMoveToView(view: SKView) { } override func touchesBegan(touches: NSSet, withEvent event: UIEvent) { /* Called when a touch begins */ for touch: AnyObject in touches { } } override func update(currentTime: CFTimeInterval) { /* Called before each frame is rendered */ } }
La primera línea que vamos a colocar simplemente va a guardar en location la posiciones para cada contacto en la pantalla.
for touch: AnyObject in touches { let location = touch.locationInNode(self); }
El punto y coma es completamente opcional pero después de años de PHP, JavaScript y ObjC se me hace automático colocarlo. Para poder ver en la cosola y comprobar que algo está sucediendo agreguemos la siguiente línea:
for touch: AnyObject in touches { let location = touch.locationInNode(self); println("\(location.x.description),\(location.y.description)") }
Es hora de compilar! Mi recomendación es hacerlo en un dispositivo en vez del simulador. En el dispositivo podemos probar multitoques y manipular la aplicación tal cual lo haría el usuario. Una vez inicia la aplicación y hagamos algunos toques en la pantalla debemos ver en la consola algo así:
314.0,263.5 162.0,202.5 458.5,279.5 373.5,292.0 436.0,291.5
Hasta ahora, aunque sí estamos cargando la escena definida en GameScene.sks, no hemos escrito nada que utilice SpriteKit. Arrastremos los archivos de imagen guanabara.jpg, redentor.png y brazuca_mini.png a la carpeta Supporting Files y luego, agreguemos touchesBegan las siguientes líneas:
for touch: AnyObject in touches { let location = touch.locationInNode(self); println("\(location.x.description),\(location.y.description)") let balon = SKSpriteNode(imageNamed:"brazuca_mini.png") balon.xScale = 0.50 balon.yScale = 0.50 balon.position = location self.addChild(balon) }
Si compilamos ahora podemos ver que al correr la aplicación, para cada toque en la pantalla, además de imprimir las coordenadas en la consola veremos también una brazuca en la pantalla.
Todas quietas, sin gravedad, sin impulso, sin efecto de fricción, sin posibilidad de gol. Vamos a agregarle algo de física. Justo antes deaddChild colocamos:
for touch: AnyObject in touches { let location = touch.locationInNode(self); println("\(location.x.description),\(location.y.description)") let balon = SKSpriteNode(imageNamed:"brazuca_mini.png") balon.xScale = 0.50 balon.yScale = 0.50 balon.position = location balon.physicsBody = SKPhysicsBody(circleOfRadius:40) balon.physicsBody.dynamic = true self.addChild(balon) }
Aquí lo único que estamos haciendo es darle al sprite un cuerpo y diciéndole a este cuerpo que responde a propiedades físicas como gravedad y colisiones. Probemos que sucede ahora (cmd + b). Al correr la aplicación notamos que ahora la gravedad hace caer los balones y si los dejamos tocar podemos ver que obedecen también a las colisiones entre ellos. Es un poco difícil lograr una colisión si no tenemos un piso que detenga la caída así que agregaremos uno. Para eso vamos a agregar la siguiente función:
func agregaSolidoEnPunto(elPunto: CGPoint, ancho: CGFloat, alto: CGFloat) { let tamanio = CGSize(width: ancho, height: alto) let laPared = SKNode() laPared.position = CGPoint( x: elPunto.x + tamanio.width * 0.5, y: elPunto.y - tamanio.height * 0.5) laPared.physicsBody = SKPhysicsBody(rectangleOfSize: tamanio) laPared.physicsBody.dynamic = false laPared.physicsBody.collisionBitMask = 0 self.addChild(laPared) }
Con esta función podemos agregar sólidos rectangulares fijos en cualquier punto de la pantalla (o fuera de ella). Probemos llamando la función agregando adidMoveToView las dos líneas a continuación:
override func didMoveToView(view: SKView) { let origen = CGPoint(x: 100, y: 100) agregaSolidoEnPunto(origen,ancho:20,alto:200) }
Ahora tenemos un cuerpo sólido de 20 x 200 colocado en el punto 100,100 pero el sólido es invisible.
Podemos notar su presencia si tiramos muchos balones en el lado izquierdo de la pantalla pero es mucho mejor, para poder entender que está sucediendo, utilizar la ayuda que nos brinda SKView con la propiedadshowsPhysics . Vamos a abrir el archivo GameViewController.swift y dentro de viewDidLoad , luego deshowsFPS yshowsNodeCount , agregamos skView.showsPhysics = true para tener algo así:
class GameViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() if let scene = GameScene.unarchiveFromFile("GameScene") as? GameScene { // Configure the view. let skView = self.view as SKView skView.showsFPS = true skView.showsNodeCount = true skView.showsPhysics = true ...
Si compilamos ahora podemos demostrar nuestra destreza con el balón equilibrándolo sobre una pared invisible.
Ahora pongamos un escenario propicio para estos balones. Vamos a abrir GameScene.sks y primero cambiamos el ancho y alto de la escena en el panel de utilities a 576 x 320 como indica la imagen.
Ahora arrastramos desde la carpeta Supporting Files el archivo guanabara.jpg y lo dejamos caer en el centro de la escena. Asegurémonos que la imagen (node) está seleccionada y ajustemos la posición según vemos en la captura de pantalla.
Este es un fondo estático. No va a tener ninguna influencia sobre la dinámica de los balones y tampoco recibirá los efectos de la gravedad en la escena. Por esta razón tenemos que ajustar sus propiedades físicas a Body Type: none.
Vamos a agregar ahora otro nodo, agreguemos al Cristo Redentor arrastrando el archivo redentor.png a la escena. Ajustemos la posición como lo indica la imagen y probemos la aplicación ahora.
Vemos que los toques envían sus coordenadas a la consola pero es posible que los balones no se estén creando. En realidad si están siendo creados pero están detrás de fondo. Hagamos un pequeño ajuste para corregir esto. En GameScene.sks seleccionamos el nodo donde está la imagen de fondo y en panel de propiedades verifiquemos que zPosition está en 0, luego en GameScene.swift asignemos al balón creado un zPosition = 10 para asegurarnos de que vaya a quedar frente al fondo.
... balon.physicsBody.dynamic = true balon.zPosition = 10 self.addChild(balon) }
Probemos nuevamente y vemos como los balones caen frente al paisaje y chocan con el redentor cuando caen sobre ese nodo. El problema aquí es que el nodo es rectangular y no interactúa como esperado con los balones. Para lograr colisiones más precisas sería necesario desglosar la forma del redentor en partes más sencillas y unirlas o crear un polígono complejo pero aún así los resultados probablemente sería aproximados.
Utilizando “per-pixel collision”
Para resolver esto ahora tenemos “per-pixel collision”, un método de cálculo de colisiones que utiliza el canal alpha para determinar el borde del sólido. Hay que resaltar que este método consume muchos más recursos que los rectangulares o circulares, usar con moderación.
Si quieres dejar tu aplicación algo más presentable puedes comentar las líneas auxiliares SKView en GameViewController.swift funciónviewDidLoad :
let skView = self.view as SKView //skView.showsFPS = true //skView.showsNodeCount = true //skView.showsPhysics = true
Ahora podemos ver al redentor haciendo ciertos trucos con las brazucas. Nada que evite un 7 a 1 desafortunadamente.
Al comparar Swift con Objective-C en algo sencillo como esto no veo mucha diferencia, de hecho rápidamente olvido que no es Objective-C pues al trabajar con SpriteKit la sintaxis es bastante similar. Puede que la razón de esto es que aún abordamos todo a la ObjC y dejamos de ver o aprovechar ventajas propias de Swift. Creo que, como dice mi mujer, “amanecerá y veremos”.