Advertisement
  1. Code
  2. SpriteKit

Conceptos básicos de SpriteKit: Acciones y Física

Scroll to top
Read Time: 25 min
This post is part of a series called SpriteKit Basics.
SpriteKit Basics: Sprites
SpriteKit Basics: Putting It All Together

() translation by (you can also view the original English article)

En esta serie, estamos aprendiendo a usar SpriteKit para construir juegos 2D para iOS. En este post, aprenderemos sobre dos características importantes de SpriteKit: las acciones y la física.

Para seguir este tutorial, solo tienes que descargar el repo de GitHub que lo acompaña. Tiene dos carpetas: una para las acciones y otra para la física. Solo tienes que abrir cualquiera de los dos proyectos de inicio en Xcode y ya está todo listo.

Acciones

Para la mayoría de los juegos, querrás que los nodos hagan algo como mover, escalar o rotar. La clase SKAction fue diseñada con este propósito en mente. La clase SKAction tiene muchos métodos de clase que puede invocar para mover, escalar o rotar las propiedades de un nodo durante un período de tiempo.

También puedes reproducir sonidos, animar un grupo de texturas o ejecutar código personalizado utilizando la clase SKAction. Puedes ejecutar una sola acción, ejecutar dos o más acciones una tras otra en una secuencia, ejecutar dos o más acciones al mismo tiempo juntas como un grupo, e incluso repetir cualquier acción.

Movimiento

Hagamos que un nodo se mueva por la pantalla. Introduce lo siguiente dentro de Example1.swift.

1
class Example1: SKScene {
2
    let player = SKSpriteNode(imageNamed: "player")
3
    
4
    override func didMove(to view: SKView) {
5
        player.position = CGPoint(x: 200, y: player.size.height)
6
        addChild(player)
7
        
8
        let movePlayerAction = SKAction.moveTo(y: 900, duration: 2)
9
        player.run(movePlayerAction)
10
    }
11
}

Aquí creamos una SKAction e invocamos el método de la clase moveTo(y:duration:), que toma como parámetro la posición y a la que mover el nodo y la duración en segundos. Para ejecutar la acción, debes llamar al método run(_:) de un nodo y pasarle la SKAction. Si haces la prueba ahora, deberías ver que un avión se mueve por la pantalla.

Hay varias variedades de los métodos de movimiento, incluyendo move(to:duration:), que moverá el nodo a una nueva posición tanto en el eje x como en el y, y move(by:duration:), que moverá un nodo en relación con su posición actual. Te sugiero que leas la documentación de SKAction para conocer todas las variedades de los métodos de movimiento.

Cierres de finalización

Hay otra variedad del método de ejecución que te permite llamar a algún código en un cierre de finalización. Introduce el siguiente código dentro de Example2.swift.

1
class Example2: SKScene {
2
    let player = SKSpriteNode(imageNamed: "player")
3
    
4
    override func didMove(to view: SKView) {
5
        player.position = CGPoint(x: 200, y: player.size.height)
6
        addChild(player)
7
        
8
        let movePlayerAction = SKAction.moveTo(y: 900, duration: 2)
9
        player.run(movePlayerAction, completion: {
10
            print("Player Finished Moving")
11
        })
12
    }
13
}

El método run(_:completion:) permite ejecutar un bloque de código una vez que la acción ha terminado de ejecutarse. Aquí ejecutamos una simple sentencia de impresión, pero el código podría ser tan complejo como lo necesites.

Secuencias de acciones

A veces querrás ejecutar acciones una tras otra, y puedes hacerlo con el método sequence(_:). Añade lo siguiente a Example3.swift.

1
class Example3: SKScene {
2
    let player = SKSpriteNode(imageNamed: "player")
3
    
4
    override func didMove(to view: SKView) {
5
        
6
        player.position = CGPoint(x: 100, y: player.size.height)
7
        addChild(player)
8
        
9
        let movePlayerAction = SKAction.moveTo(y: 900, duration: 2)
10
        let scalePlayerAction = SKAction.scale(to: 3, duration: 2)
11
        let sequenceAction = SKAction.sequence([movePlayerAction, scalePlayerAction])
12
        player.run(sequenceAction)
13
        
14
    }
15
}

Aquí creamos dos SKActions: una utiliza el método moveTo(y:duration:), y la otra utiliza el método scale(to:duration:), que cambia la escala x e y del nodo. A continuación, invocamos el método sequence(_:), que toma como parámetro una matriz de SKActions que se ejecutarán una tras otra. Si haces la prueba ahora, deberías ver que el avión se mueve hacia arriba en la pantalla, y una vez que haya llegado a su destino, crecerá hasta triplicar su tamaño original.

Acciones agrupadas

En otras ocasiones, es posible que desees ejecutar las acciones juntas como un grupo. Añade el siguiente código a Example4.swift.

1
class Example4: SKScene {
2
    
3
    let player = SKSpriteNode(imageNamed: "player")
4
    
5
    override func didMove(to view: SKView) {
6
        player.position = CGPoint(x: 100, y: player.size.height)
7
        addChild(player)
8
        
9
        let movePlayerAction = SKAction.moveTo(y: 900, duration: 2)
10
        let scalePlayerAction = SKAction.scale(to: 3, duration: 2)
11
        let groupAction = SKAction.group([movePlayerAction, scalePlayerAction])
12
        player.run(groupAction)
13
    }
14
}

Aquí estamos utilizando los mismos métodos moveTo y scale que en el ejemplo anterior, pero además estamos invocando el método group(_:), que toma como parámetro un array de SKActions que se ejecutan al mismo tiempo. Si hicieras una prueba ahora, verías que el plano se mueve y escala al mismo tiempo.

Acciones inversas

Algunas de estas acciones pueden invertirse invocando el método reversed(). La mejor manera de averiguar qué acciones admiten el método reversed() es consultar la documentación. Una acción que es reversible es el fadeOut(withDuration:), que desvanecerá un nodo hasta hacerlo invisible cambiando su valor alfa. Hagamos que el plano se desvanezca y luego se vuelva a desvanecer. Añade lo siguiente a Example5.swift.

1
class Example5: SKScene {
2
    override func didMove(to view: SKView) {
3
        let player = SKSpriteNode(imageNamed: "player")
4
        player.position = CGPoint(x: 100, y: player.size.height)
5
        addChild(player)
6
 
7
        
8
        let fadePlayerAction = SKAction.fadeOut(withDuration: 2)
9
        let fadePlayerActionReversed = fadePlayerAction.reversed()
10
        let fadePlayerSequence = SKAction.sequence([fadePlayerAction, fadePlayerActionReversed])
11
        player.run(fadePlayerSequence)
12
    }
13
}

Aquí creamos una SKAction e invocamos el método fadeOut(withDuration:). En la siguiente línea de código, invocamos el método reversed(), que hará que la acción invierta lo que acaba de hacer. Prueba el proyecto y verás que el avión se desvanece y luego vuelve a aparecer.

Repetición de acciones

Si alguna vez necesitas repetir una acción un número específico de veces, los métodos repeat(_:count:) y repeatForever(_:) te tienen cubierto. Hagamos que el avión se desvanezca repetidamente y vuelva a entrar para siempre. Introduce el siguiente código en Example6.swift.

1
class Example6: SKScene {
2
    let player = SKSpriteNode(imageNamed: "player")
3
    override func didMove(to view: SKView) {
4
        player.position = CGPoint(x: 100, y: player.size.height)
5
        addChild(player)
6
 
7
        
8
        let fadePlayerAction = SKAction.fadeOut(withDuration: 2)
9
        let fadePlayerActionReversed = fadePlayerAction.reversed()
10
        let fadePlayerSequence = SKAction.sequence([fadePlayerAction, fadePlayerActionReversed])
11
        let fadeOutInRepeatAction = SKAction.repeatForever(fadePlayerSequence)
12
        player.run(fadeOutInRepeatAction)
13
    }
14
}

Aquí invocamos el método repeatForever(_:), pasando la fadePlayerSequence. Si haces la prueba, verás que el plano se desvanece y luego vuelve a repetirse para siempre.

Acciones de parada

Muchas veces necesitarás que un nodo deje de ejecutar sus acciones. Para ello puedes utilizar el método removeAllActions(). Hagamos que el nodo jugador deje de desvanecerse cuando tocamos en la pantalla. Añade lo siguiente dentro de Example7.swift.

1
class Example7: SKScene {
2
    let player = SKSpriteNode(imageNamed: "player")
3
    override func didMove(to view: SKView) {
4
        player.position = CGPoint(x: 100, y: player.size.height)
5
        addChild(player)
6
            
7
            
8
        let fadePlayerAction = SKAction.fadeOut(withDuration: 2)
9
        let fadePlayerActionReversed = fadePlayerAction.reversed()
10
        let fadePlayerSequence = SKAction.sequence([fadePlayerAction, fadePlayerActionReversed])
11
        let fadeOutInRepeatAction = SKAction.repeatForever(fadePlayerSequence)
12
        player.run(fadeOutInRepeatAction)
13
    }
14
    
15
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
16
        player.removeAllActions()
17
    }
18
}

Si tocas en la pantalla, el nodo del jugador tendrá todas las acciones eliminadas y ya no se desvanecerá.

Seguimiento de las acciones

A veces se necesita una forma de llevar la cuenta de las acciones. Por ejemplo, si ejecutas dos o más acciones en un nodo, puede que quieras una forma de identificarlas. Puedes hacer esto registrando una clave con el nodo, que es una simple cadena de texto. Introduce lo siguiente dentro del Example8.swift.

1
class Example8: SKScene {
2
    
3
    let player = SKSpriteNode(imageNamed: "player")
4
    override func didMove(to view: SKView) {
5
        player.position = CGPoint(x: 100, y: player.size.height)
6
        addChild(player)
7
        
8
        
9
        let fadePlayerAction = SKAction.fadeOut(withDuration: 2)
10
        let fadePlayerActionReversed = fadePlayerAction.reversed()
11
        let fadePlayerSequence = SKAction.sequence([fadePlayerAction, fadePlayerActionReversed])
12
        let fadeOutInRepeatAction = SKAction.repeatForever(fadePlayerSequence)
13
        player.run(fadeOutInRepeatAction, withKey: "faderepeataction")
14
        
15
    }
16
    
17
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
18
        if player.action(forKey: "faderepeataction") != nil {
19
            player.removeAction(forKey: "faderepeataction")
20
        }
21
    }
22
}

Aquí estamos invocando el método run(_:withKey:) del nodo, que, como se ha mencionado, toma una simple cadena de texto. Dentro del método touchesBegan(_:with:), estamos invocando action(forKey:) para asegurarnos de que el nodo tiene la clave que le hemos asignado. Si lo tiene, invocamos a .removeAction(forKey:), que toma como parámetro la clave que ha establecido previamente.

Acciones de sonido

Muchas veces querrás reproducir algún sonido en tu juego. Puedes lograr esto usando el método de la clase playSoundFileNamed(_:waitForCompletion:). Introduce lo siguiente dentro de Example9.swift.

1
 class Example9: SKScene {
2
    
3
    override func didMove(to view: SKView) {
4
        let planeSound = SKAction.playSoundFileNamed("planesound", waitForCompletion: false)
5
        run(planeSound)
6
        
7
    }
8
}

La acción playSoundFileNamed(_:waitForCompletion:) toma como parámetros el nombre del archivo de sonido sin la extensión, y un booleano que determina si la acción esperará hasta que el sonido esté completo antes de continuar.

Por ejemplo, supón que tienes dos acciones en una secuencia, siendo el sonido la primera acción. Si waitForCompletion fuera true, la secuencia esperaría hasta que el sonido terminara de reproducirse antes de pasar a la siguiente acción dentro de la secuencia. Si necesitas más control sobre tus sonidos, puedes usar un SKAudioNode. No cubriremos el SKAudioNode en esta serie, pero es definitivamente algo que deberías echar un vistazo durante tu carrera como desarrollador de SpriteKit.

Animación de fotogramas

Animar un grupo de imágenes es algo que requieren muchos juegos. El animate(with:timePerFrame:) te tiene cubierto en esos casos. Introduce lo siguiente dentro de Example10.swift.

1
class Example10: SKScene {
2
    let explosion = SKSpriteNode(imageNamed: "explosion1")
3
    
4
    override func didMove(to view: SKView) {
5
        
6
        addChild(explosion)
7
        var explosionTextures:[SKTexture] = []
8
        
9
        for i in 1...6 {
10
            explosionTextures.append(SKTexture(imageNamed: "explosion\(i)"))
11
        }
12
    
13
       let explosionAnimation = SKAction.animate(with: explosionTextures,
14
                                           timePerFrame: 0.3)
15
       explosion.run(explosionAnimation)
16
    }
17
}

La acción animate(with:timePerFrame:) toma como parámetro un array de SKTextures, y un valor de timePerFrame que será el tiempo que tarda entre cada cambio de textura. Para ejecutar esta acción, se invoca el método de ejecución de un nodo y se le pasa el SKAction.

Acciones con código personalizado

El último tipo de acción que veremos es el que permite ejecutar código personalizado. Esto puede ser útil cuando necesitas hacer algo en medio de tus acciones, o solo necesitas una forma de ejecutar algo que la clase SKAction no proporciona. Introduce lo siguiente dentro de Example11.swift.

1
class Example11: SKScene {
2
    
3
    
4
    override func didMove(to view: SKView) {
5
        let runAction = SKAction.run(printToConsole)
6
        run(runAction)
7
    }
8
    
9
    
10
    func printToConsole(){
11
        print("SKAction.run() invoked")
12
    }
13
14
}

Aquí invocamos el método run(_:) de la escena y pasamos una función printToConsole() como parámetro. Recuerda que las escenas también son nodos, por lo que puedes invocar el método run(_:) sobre ellas también.

Esto concluye nuestro estudio de las acciones. Hay muchas cosas que puedes hacer con la clase SKAction, y te sugiero que después de leer este tutorial sigas explorando la documentación sobre SKActions.

Física

SpriteKit ofrece un robusto motor de física fuera de la caja, con poca configuración requerida. Para empezar, solo tienes que añadir un cuerpo de física a cada uno de tus nodos y listo. El motor de física está construido sobre el popular motor Box2d. Sin embargo, la API de SpriteKit es mucho más fácil de usar que la API original de Box2d.

Empecemos añadiendo un cuerpo físico a un nodo y veamos qué ocurre. Añade el siguiente código a Example1.swift.

1
    ...
2
    let player = SKSpriteNode(imageNamed: "enemy1")
3
4
    override func didMove(to view: SKView) {
5
        ...
6
        player.physicsBody = SKPhysicsBody(circleOfRadius: player.size.width/2)
7
        player.physicsBody?.affectedByGravity = false
8
        player.position = CGPoint(x: size.width/2 , y: size.height - player.size.height)
9
        addChild(player)
10
    }
11
12
13
    
14
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
15
        ...
16
	    player.physicsBody?.affectedByGravity = true
17
    }

Continúa y prueba el proyecto ahora. Verás el avión sentado en la parte superior de la escena. Una vez que pulses en la pantalla, el avión caerá de la pantalla, y seguirá cayendo para siempre. Esto demuestra lo sencillo que es empezar a utilizar la física: solo tienes que añadir un cuerpo físico a un nodo y ya está todo listo.

La physicsBody del cuerpo

La propiedad physicsBody es de tipo SKPhysicsBody, que va a ser un contorno aproximado de la forma de tu nodo... o un contorno muy preciso de la forma de tu nodo, dependiendo del constructor que utilices para inicializar esta propiedad.

Aquí hemos utilizado el inicializador init(circleOfRadius:), que toma como parámetro el radio del círculo. Hay varios otros inicializadores, incluyendo uno para un rectángulo o un polígono de un CGPath. Incluso puedes utilizar la propia textura del nodo, lo que haría que el physicsBody fuera una representación casi exacta del nodo.

Para ver lo que quiero decir, actualiza el archivo GameViewController.swift con el siguiente código. He comentado la línea a añadir.

1
 override func viewDidLoad() {
2
    super.viewDidLoad()
3
        
4
    let scene = GameScene(size:CGSize(width: 768, height: 1024))
5
    let skView = self.view as! SKView
6
    skView.showsFPS = false
7
    skView.showsNodeCount = false
8
    skView.ignoresSiblingOrder = false
9
    skView.showsPhysics = true // THIS LINE ADDED

10
    scene.scaleMode = .aspectFill
11
    skView.presentScene(scene)
12
}

Ahora el physicsBody del nodo aparecerá delineado en verde. En la detección de colisiones, la forma del physicsBody es lo que se evalúa. Este ejemplo tendría el círculo alrededor del plano guiando la detección de colisiones, lo que significa que si una bala, por ejemplo, golpeara el borde exterior del círculo, entonces eso contaría como una colisión.

circle bodycircle bodycircle body

Ahora añade lo siguiente a Example2.swift.

1
override func didMove(to view: SKView) {
2
    player.physicsBody = SKPhysicsBody(texture: player.texture!, size: player.size)
3
    player.physicsBody?.affectedByGravity = false
4
    player.position = CGPoint(x: size.width/2 , y: size.height - player.size.height)
5
    addChild(player)
6
}

Aquí estamos usando la textura del sprite. Si pruebas el proyecto ahora, deberías ver que el contorno ha cambiado a una representación casi exacta de la textura del sprite.

texture bodytexture bodytexture body

Gravedad

En los ejemplos anteriores establecimos la propiedad affectedByGravity de physicsBody en false. Tan pronto como añada un cuerpo de física a un nodo, el motor de física se hará cargo. ¡El resultado es que el avión cae inmediatamente cuando se ejecuta el proyecto!

También puedes establecer la gravedad en base a cada nodo, como tenemos aquí, o puedes desactivar la gravedad por completo. Añade lo siguiente a Example3.swift.

1
let player = SKSpriteNode(imageNamed: "enemy1")
2
    
3
override func didMove(to view: SKView) {
4
    ...
5
    player.physicsBody = SKPhysicsBody(texture: player.texture!, size: player.size)
6
    player.position = CGPoint(x: size.width/2 , y: size.height - player.size.height)
7
    addChild(player)
8
        
9
    physicsWorld.gravity = CGVector(dx:0, dy: 0)
10
}
11
    
12
    
13
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
14
    ...
15
    physicsWorld.gravity = CGVector(dx:0 , dy: -9.8)
16
}

Podemos establecer la gravedad utilizando la propiedad de gravity de physicsWorld. La propiedad gravity es de tipo CGVector. Establecemos los componentes dx y dy a 0, y luego cuando se toca la pantalla establecemos la propiedad dy a -9,8. Los componentes se miden en metros, y el valor por defecto es (0, -9,8), que representa la gravedad de la Tierra.

Bucles de borde

Tal y como está ahora, cualquier nodo que se añada a la escena solo saldrá de la pantalla para siempre. Podemos añadir un bucle de borde alrededor de la escena usando el método init(edgeLoopFrom:). Añade lo siguiente a Example4.swift.

1
let player = SKSpriteNode(imageNamed: "enemy1")
2
    
3
override func didMove(to view: SKView) {
4
    ...
5
    player.physicsBody = SKPhysicsBody(texture: player.texture!, size: player.size)
6
    player.position = CGPoint(x: size.width/2 , y: size.height - player.size.height)
7
    addChild(player)
8
        
9
    physicsWorld.gravity = CGVector(dx:0, dy: 0)
10
    physicsBody = SKPhysicsBody(edgeLoopFrom: frame)
11
}
12
    
13
 override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
14
    ...
15
    physicsWorld.gravity = CGVector(dx:0 , dy: -9.8)
16
}

Aquí hemos añadido un cuerpo físico a la propia escena. El init(edgeLoopFrom:) toma como parámetro un CGRect que define sus aristas. Si pruebas ahora, verás que el avión sigue cayendo; sin embargo, interactúa con este bucle de borde y ya no cae fuera de la escena. También rebota e incluso gira un poco sobre su lado. Este es el poder del motor de física: obtienes toda esta funcionalidad de forma gratuita. Escribir algo así por tu cuenta sería bastante complejo.

Rebote

Hemos visto que el avión rebota y gira sobre su lado. Puedes controlar el rebote y si el cuerpo físico permite la rotación. Introduce lo siguiente en Example5.swift.

1
let player = SKSpriteNode(imageNamed: "enemy1")
2
    
3
override func didMove(to view: SKView) {
4
    ...
5
        
6
    player.physicsBody = SKPhysicsBody(texture: player.texture!, size: player.size)
7
    player.physicsBody?.restitution = 0.8
8
    player.physicsBody?.allowsRotation = false
9
    player.position = CGPoint(x: size.width/2 , y: size.height - player.size.height)
10
    addChild(player)
11
        
12
    physicsWorld.gravity = CGVector(dx:0, dy: 0)
13
    physicsBody = SKPhysicsBody(edgeLoopFrom: frame)
14
}
15
    
16
    
17
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
18
	...
19
    physicsWorld.gravity = CGVector(dx:0 , dy: -9.8)
20
}

Si lo pruebas ahora, verás que el player rebota mucho y tarda unos segundos en asentarse. También notarás que ya no gira. La propiedad restitution toma un número de 0.0 (menos rebote) a 1.0 (muy rebote), y la propiedad allowsRotation es un simple booleano.

Fricción

En el mundo real, cuando dos objetos se mueven uno contra otro, hay un poco de fricción entre ellos. Puedes cambiar la cantidad de fricción que tiene un cuerpo físico-esto equivale a la "rugosidad" del cuerpo. Esta propiedad debe estar entre 0.0 y 1.0. El valor por defecto es 0.2. Agrega lo siguiente a Example6.swift.

1
let player = SKSpriteNode(imageNamed: "enemy1")
2
override func didMove(to view: SKView) {
3
    
4
    player.physicsBody = SKPhysicsBody(texture: player.texture!, size: player.size)
5
    player.physicsBody?.allowsRotation = false
6
    player.physicsBody?.restitution = 0.0
7
    player.name = "player"
8
    player.position = CGPoint(x: size.width/2 , y: size.height - player.size.height)
9
    addChild(player)
10
        
11
        
12
    let rectangle = SKSpriteNode(color: .blue, size: CGSize(width: size.width, height: 20))
13
    rectangle.physicsBody = SKPhysicsBody(rectangleOf: rectangle.size)
14
    rectangle.physicsBody?.isDynamic = false
15
    rectangle.zRotation = 3.14 * 220 / 180
16
    rectangle.physicsBody?.friction = 0.0
17
    rectangle.position = CGPoint(x: size.width/2, y: size.width/2)
18
    addChild(rectangle)
19
        
20
    physicsWorld.gravity = CGVector(dx:0, dy: 0)
21
    physicsBody = SKPhysicsBody(edgeLoopFrom: frame)
22
}
23
    
24
    
25
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
26
    ...
27
    physicsWorld.gravity = CGVector(dx:0 , dy: -9.8)
28
}

Aquí creamos un Sprite rectangular y establecemos la propiedad de friction en su physicsBody a 0.0. Si haces la prueba ahora, verás que el avión se desliza muy rápidamente por el rectángulo girado. Ahora cambia la propiedad de fricción a 1.0 y prueba de nuevo. Verás que el avión no se desliza tan rápido por el rectángulo. Esto se debe a la fricción. Si quieres que se mueva más lentamente, puedes aplicar más fricción al physicsBody del player's (recuerda que el valor por defecto es 0.2).

Densidad y masa

Hay un par de propiedades más que puedes cambiar en el cuerpo físico, como la densidad y la masa. Las propiedades de density y mass están interrelacionadas, y cuando cambias una, la otra se recalcula automáticamente. Cuando se crea un cuerpo físico por primera vez, la propiedad de area del cuerpo se calcula y nunca cambia después (es solo de lectura). La densidad y la masa se basan en la fórmula mass = density * area.

Cuando se tiene más de un nodo en una escena, la densidad y la masa afectarían a la simulación de cómo los nodos rebotan entre sí e interactúan. Piensa en un balón de baloncesto y una bola de bolos: tienen aproximadamente el mismo tamaño, pero la bola de bolos es mucho más densa. Cuando chocan, la pelota de baloncesto cambia de dirección y velocidad mucho más que la bola de bolos.

Fuerza e impulso

Puedes aplicar fuerzas e impulsos para mover el cuerpo físico. Un impulso se aplica inmediatamente y solo una vez. Una fuerza, en cambio, suele aplicarse para obtener un efecto continuo. La fuerza se aplica desde el momento en que se añade la fuerza hasta que se procesa el siguiente fotograma de la simulación. Para aplicar una fuerza continua, tendrías que aplicarla en cada fotograma. Añade lo siguiente a Example7.swift.

1
 let player = SKSpriteNode(imageNamed: "enemy1")
2
override func didMove(to view: SKView) {
3
     ...
4
        
5
    player.physicsBody = SKPhysicsBody(texture: player.texture!, size: player.size)
6
    player.physicsBody?.allowsRotation = false
7
    player.name = "player"
8
    player.position = CGPoint(x: size.width/2 , y: size.height - player.size.height)
9
    addChild(player)
10
  
11
        
12
        
13
    physicsWorld.gravity = CGVector(dx:0, dy: 0)
14
    physicsBody = SKPhysicsBody(edgeLoopFrom: frame)
15
}
16
    
17
    
18
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
19
    ...
20
    physicsWorld.gravity = CGVector(dx:0 , dy: -9.8)
21
	if(touchedNode.name == "player"){
22
            player.physicsBody?.applyImpulse(CGVector(dx:0 , dy: 100))
23
    }
24
}

Ejecuta el proyecto y espere a que el reproductor se detenga en la parte inferior de la pantalla, y luego pulsa sobre el reproductor. Verás que el reproductor vuela hacia arriba en la pantalla y finalmente vuelve a posarse en la parte inferior. Aplicamos un impulso mediante el método applyImpulse(_:), que toma como parámetro un CGVector y se mide en Newton-segundos.

¿Por qué no probar lo contrario y añadir una fuerza al nodo del jugador? Recuerda que tendrás que añadir la fuerza de forma continua para que tenga el efecto deseado. Un buen lugar para hacerlo es en el método update(_:) de la escena. Además, puedes probar a aumentar la propiedad de restitución en el jugador para ver cómo afecta a la simulación.

1
override func update(_ currentTime: TimeInterval) {
2
    //APPLY FORCE HERE
3
}

Detección de colisiones

El motor de física tiene un robusto sistema de detección de colisiones y contactos. Por defecto, dos nodos con cuerpos de física pueden colisionar. Has visto esto en ejemplos anteriores, no se requería ningún código especial para decirle a los objetos que interactuaran. Sin embargo, puedes cambiar este comportamiento estableciendo una "categoría" en el cuerpo de física. Esta categoría se puede utilizar para determinar qué nodos colisionarán entre sí y también se puede utilizar para informarle cuando ciertos nodos están haciendo contacto.

La diferencia entre un contacto y una colisión es que un contacto se utiliza para saber cuándo dos cuerpos físicos se están tocando. Una colisión, por otro lado, evita que dos cuerpos físicos se crucen en el espacio del otro, cuando el motor de física detecta una colisión, aplicará impulsos opuestos para separar los objetos de nuevo. Hemos visto las colisiones en acción con el jugador y el bucle de borde y el jugador y el rectángulo de los ejemplos anteriores.

Tipos de physicsBodies

Antes de pasar a configurar nuestras Categorías para los cuerpos de física, debemos hablar de los tipos de physicsBodies. Hay tres:

  1. Un volumen dinámico simula objetos con volumen y masa. Estos objetos se ven afectados por fuerzas y colisiones en el mundo de la física (por ejemplo, el avión de los ejemplos anteriores).
  2. Un volumen estático no se ve afectado por fuerzas y colisiones. Sin embargo, como tiene volumen propio, otros cuerpos pueden rebotar e interactuar con él. Establece la propiedad isDynamic del cuerpo físico a false para crear un volumen estático. Estos volúmenes nunca son movidos por el motor de física. Ya vimos esto en acción con el ejemplo seis, donde el avión interactuaba con el rectángulo, pero el rectángulo no se veía afectado por el avión ni por la gravedad. Para ver lo que quiero decir, vuelve al ejemplo seis y elimina la línea de código que establece rectangle.physicsBody?.isDynamic = false.
  3. El tercer tipo de cuerpo físico es un borde, que es un cuerpo estático y sin volumen. Hemos visto este tipo de cuerpo en acción con el bucle de aristas que creamos alrededor de la escena en todos los ejemplos anteriores. Las aristas interactúan con otros cuerpos de volumen, pero nunca con otra arista.

Las categorías utilizan un entero de 32 bits con 32 banderas individuales que pueden estar activadas o desactivadas. Esto también significa que solo puedes tener un máximo de 32 categorías. Esto no debería suponer un problema para la mayoría de los juegos, pero es algo a tener en cuenta.

Creación de categorías

Crea un nuevo archivo Swift yendo a Archivo > Nuevo > Archivo y asegurándote de que el Archivo Swift esté resaltado.

New FileNew FileNew File

Introduce PhysicsCategories como nombre y pulsa Crear.

Physics CategoriesPhysics CategoriesPhysics Categories

Introduce lo siguiente en el archivo que acabas de crear.

1
struct PhysicsCategories {
2
    static let Player : UInt32 = 0x1 << 0
3
    static let EdgeLoop : UInt32 = 0x1 << 1
4
    static let Redball : UInt32 = 0x1 << 2
5
}

Utilizamos una estructura PhysicsCategories para crear categorías para Player, EdgeLoop y RedBall. Usamos el cambio de bits para activar los bits.

Ahora introduce lo siguiente en Example8.swift.

1
 ...
2
let player = SKSpriteNode(imageNamed: "enemy1")
3
var dx = -20
4
var dy = -20
5
override func didMove(to view: SKView) {
6
    player.physicsBody = SKPhysicsBody(texture: player.texture!, size: player.size)
7
    player.physicsBody?.allowsRotation = false
8
    player.physicsBody?.restitution = 0.0
9
    player.physicsBody?.categoryBitMask = PhysicsCategories.Player
10
    player.physicsBody?.contactTestBitMask = PhysicsCategories.RedBall
11
    player.physicsBody?.collisionBitMask = PhysicsCategories.EdgeLoop
12
    player.position = CGPoint(x: size.width/2 , y: size.height - player.size.height)
13
    addChild(player)
14
    player.physicsBody?.applyImpulse(CGVector(dx: dx, dy: dy))
15
        
16
        
17
    let redBall = SKShapeNode(circleOfRadius: 100)
18
    redBall.fillColor = SKColor.red
19
    redBall.position = CGPoint(x: size.width/2, y: size.height/2)
20
    redBall.physicsBody = SKPhysicsBody(circleOfRadius: 100)
21
    redBall.physicsBody?.isDynamic = false
22
    redBall.physicsBody?.categoryBitMask = PhysicsCategories.RedBall
23
    addChild(redBall)
24
        
25
    physicsWorld.gravity = CGVector(dx:0, dy: 0)
26
    physicsBody = SKPhysicsBody(edgeLoopFrom: frame)
27
    physicsWorld.contactDelegate = self
28
    physicsBody?.categoryBitMask = PhysicsCategories.EdgeLoop
29
    physicsBody?.contactTestBitMask = PhysicsCategories.Player
30
}
31
32
33
func didBegin(_ contact: SKPhysicsContact) {
34
    var firstBody: SKPhysicsBody
35
    var secondBody: SKPhysicsBody
36
        
37
    if(contact.bodyA.categoryBitMask < contact.bodyB.categoryBitMask){
38
        firstBody = contact.bodyA
39
        secondBody = contact.bodyB
40
    }else{
41
        firstBody = contact.bodyB
42
        secondBody = contact.bodyA
43
        }
44
        
45
    if((firstBody.categoryBitMask & PhysicsCategories.Player != 0) && (secondBody.categoryBitMask & PhysicsCategories.RedBall != 0)){
46
        print("Player and RedBall Contact")
47
      
48
    }
49
        
50
    if((firstBody.categoryBitMask & PhysicsCategories.Player != 0) && (secondBody.categoryBitMask & PhysicsCategories.EdgeLoop != 0)){
51
        print ("Player and EdgeLoop Contact")
52
        dx *= -1
53
        dy *= -1
54
        player.physicsBody?.applyImpulse(CGVector(dx: dx, dy: dy))
55
    }
56
}

Aquí creamos el player como de costumbre, y creamos dos variables dx y dy, que serán utilizadas como componentes de un CGVector cuando apliquemos un impulso al player.

Dentro de didMove(to:), configuramos el jugador y añadimos categoryBitMask, contactBitMask y collisionBitMask. El categoryBitMask debería tener sentido-este es el jugador, así que lo establecemos como PhysicsCategories.Player. Estamos interesados en el momento en que el jugador hace contacto con la redBall, por lo que establecemos el contactBitMask a PhysicsCategories.RedBall. Por último, queremos que colisione y se vea afectado por la física con el edge loop, por lo que establecemos su collisionBitMask a PhysicsCategories.EdgeLoop. Por último, aplicamos un impulso para que se mueva.

En la redBall, solo establecemos su categoryBitMask. Con el edgeLoop, establecemos su categoryBitMask, y como nos interesa cuando el player hace contacto con él, establecemos su contactBitMask.

Al configurar el contactBitMask y el collisionBitMask, solo es necesario que uno de los cuerpos haga referencia al otro. En otras palabras, no es necesario configurar ambos cuerpos como en contacto o colisión con el otro.

Para el edgeLoop, lo configuramos para que esté en contacto con el jugador. Sin embargo, podríamos haber configurado en su lugar el jugador para interactuar con el edgeLoop utilizando el operador bitácora o (|). Con este operador se pueden configurar múltiples máscaras de contacto o de colisión. Por ejemplo:

1
player.physicsBody?.contactTestBitMask = PhysicsCategories.RedBall | PhysicsCategories.EdgeLoop

Para poder responder cuando dos cuerpos hacen contacto, tienes que implementar el protocolo SKPhysicsContactDelegate. Es posible que hayas notado esto en el código de ejemplo.

1
class Example8: SKScene,SKPhysicsContactDelegate{
2

3
    physicsWorld.contactDelegate = self
4
...

Para responder a los eventos de contacto, puedes implementar los métodos didBegin(_:) y didEnd(_:). Serán llamados cuando los dos objetos hayan comenzado a hacer contacto y cuando hayan terminado el contacto respectivamente. Nos quedaremos con el método didBegin(_:) para este tutorial.

Aquí está el código una vez más para el método didBegin(_:).

1
func didBegin(_ contact: SKPhysicsContact) {
2
    var firstBody: SKPhysicsBody
3
    var secondBody: SKPhysicsBody
4
        
5
    if(contact.bodyA.categoryBitMask < contact.bodyB.categoryBitMask){
6
        firstBody = contact.bodyA
7
        secondBody = contact.bodyB
8
    }else{
9
        firstBody = contact.bodyB
10
        secondBody = contact.bodyA
11
    }
12
        
13
    if((firstBody.categoryBitMask & PhysicsCategories.Player != 0) && (secondBody.categoryBitMask & PhysicsCategories.RedBall != 0)){
14
        print("Player and RedBall Contact")
15
    }
16
        
17
    if((firstBody.categoryBitMask & PhysicsCategories.Player != 0) && (secondBody.categoryBitMask & PhysicsCategories.EdgeLoop != 0)){
18
        print ("Player and EdgeLoop Contact")
19
        dx *= -1
20
        dy *= -1
21
        player.physicsBody?.applyImpulse(CGVector(dx: dx, dy: dy))
22
    }
23
}

En primer lugar, establecemos dos variables firstBody y secondBody. Los dos cuerpos en el parámetro de contacto no se pasan en un orden garantizado, por lo que utilizaremos una sentencia if para determinar qué cuerpo tiene un contactBitMask más bajo y lo estableceremos como firstBody.

Ahora podemos comprobar y ver qué cuerpos de física están haciendo contacto. Comprobamos con qué cuerpos de física estamos tratando al juntar (&&) la categoryBitMask de los cuerpos con la PhysicsCategorys que configuramos previamente, y si el resultado es distinto de cero sabemos que tenemos el cuerpo correcto.

Finalmente, imprimimos qué cuerpos están haciendo contacto. Si fue el jugador y edgeLoop, también invertimos las propiedades dx y dy y aplicamos un impulso al jugador. Esto mantiene al jugador en constante movimiento.

Esto concluye nuestro estudio del motor de física de SpriteKit. Hay muchas cosas que no se cubrieron como SKPhysicsJoint, por ejemplo. El motor de física es muy robusto, y te sugiero encarecidamente que leas todos sus aspectos, empezando por SKPhysicBody.

Conclusión

En este post hemos aprendido sobre las acciones y la física, dos partes muy importantes del framework SpriteKit. Hemos visto un montón de ejemplos, pero todavía hay mucho que se puede hacer con las acciones y la física, y la documentación es un gran lugar para aprender.

En la próxima y última parte de esta serie, pondremos en común todo lo que hemos aprendido creando un sencillo juego. Gracias por leer, ¡y nos vemos allí!

Mientras tanto, echa un vistazo a algunos de nuestros completos cursos sobre el desarrollo de Swift y SpriteKit.

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.