SpriteKit From Scratch: Físicas e Colisões
() translation by (you can also view the original English article)
Introdução
Neste tutorial, a terceira parte da série SpriteKit From Scratch, vamos dar uma olhada detalhada na funcionalidade de simulação de física do SpriteKit e como ela pode ser utilizada em seus jogos 2D.
Este tutorial requer que você esteja rodando o Xcode 7.3 ou superior, que inclui o Swift 2.2 e os SDKS do iOS 9.3, tvOS 9.2 e OS X 10.11.4.
Para me seguir, você pode usar o projeto criado no tutorial anterior ou baixar uma nova cópia no GitHub.
As imagens usadas pra o jogo desta série podem ser encontradas no GraphicRiver. O GraphicRiver é uma ótima fonte para encontrar trabalhos artísticos e imagens para seus jogos.
1. Mundos Físicos
A primeira parte de qualquer simulação de física no SpriteKit é a propriedade physicsWorld
da cena atual. Esta propriedade é um objeto SKPhysicsWorld
que define propriedades como a gravidade, velocidade da simulação da física e representante de contato da cena. Os mundos físicos também pode definir articulações entre dois objetos para fixar de forma eficaz vários nós em pontos específicos.
Gravidade
Para o jogo no estilo de vista superior como estamos criamos nesta série, queremos mudar o valor padrão de gravidade fornecido pelo SpriteKit. A gravidade padrão é planejada para um jogo de vista frontal com um valor de (0, -9.8) que simula a gravidade da Terra, que é, aceleração horizontal 0 e uma aceleração vertical de 9.8m/s². Pra nosso jogo, precisamos de uma gravidade vertical de 0, assim o carro não vai começar a acelerar para baixo uma vez que atribuirmos suas propriedades físicas
Abra o MainScene.sks e clique no fundo cinza para selecionar a cena. Em seguida, abra o Attributes Inspector e altere o Gravity em ambos os componentes, X e Y, para 0.



É importante observar que apenas a gravidade da propriedade mundo físico pode ser alterada usando o editor de cena do Xcode. Qualquer outra propriedade precisa ser alterada via programação.
Representante do contato
Para que o jogo possa detectar colisões entre dois objetos, precisamos atribuir a propriedade contactDelegate
do mundo físico da cena. Este representando pode ser qualquer objeto que esteja em conformidade com o protocolo SKPhysicsContactDelegate
. O protocolo define dois métodos, didBeginContact(_:)
e didEndContact(_:)
. Você pode usar estes método para executar ações baseadas nos objetos que estão colidindo na cena.
Para manter o nosso código em um único lugar, vamos tornar a instância da MainScene
o seu próprio representante de contato. Abra o MainScene.swift e edite a definição da classe MainScene
para ficar em conformidade com o protocolo SKPhysicsContactDelegate
.
1 |
import UIKit |
2 |
import SpriteKit |
3 |
|
4 |
class MainScene: SKScene, SKPhysicsContactDelegate { |
5 |
|
6 |
...
|
7 |
|
8 |
}
|
No didMoveToView(_:)
, definimos a instância MainScene
como o representante de contato da propriedade physicsWorld
.
1 |
override func didMoveToView(view: SKView) { |
2 |
|
3 |
...
|
4 |
|
5 |
physicsWorld.contactDelegate = self |
6 |
}
|
Iremos implementar os métodos do protocolo SKPhysicsContactDelegate
mais tarde. Primeiro precisamos configurar as propriedades de física dos nós na cena.
2. Corpo físico
Qualquer nó no SpriteKit que você queira simular a física de alguma forma deve ser atribuído um único objeto SKPhysicsBody
. Os corpos físicos contém várias propriedades, incluindo:
- massa
- densidade
- área
- atrito
- velocidade
Em seus jogos, você também pode atribuir 32 categorias únicas e um corpo físico pode ser atribuído para qualquer uma dessas categorias. As categorías são muito úteis para determinar quais nós em sua cena podem interagir com outros em termos de colisões.
Nos corpos físicos, estas categorias são representadas pelas propriedades categoryBitMask
e collisionBitMask
, que ambas contem o valor 0xFFFFFFFF por padrão. Isso significa que todos os nós pertencem a todas as categorias. É importante notar que neste valor, cada dígito hexadecimal F é uma forma de taquigrafia e representa o número 15 em dígitos binários (1111) cada, dos quais correspondem a uma das 32 categorias que você pode usar.
Quando dois nós se colidem, uma operação lógica AND
é processada entre a colissionBitMask
e a categoryBitMask
. Se o resultado for um valor diferente de zero, então o SpriteKit executa sua simulação entre o corpo dos dois nós.
Note que este cálculo AND
é executado duas vezes com os dois corpos trocados. Por exemplo.
- Calculo 1.
bodyA.collisionBitMask & bodyB.categoryBitMask
- Calculo 2:
bodyB.collisionBitMask & bodyA.categoryBitMask
Se você não sabe como um operador AND
funciona, então aqui esta um exemplo simples escrito em Swift.
1 |
let mask1 = 0x000000FF |
2 |
let mask2 = 0x000000F0 |
3 |
|
4 |
let result = mask1 & mask2 // result = 0x000000F0 |
O operador AND
determina quais partes das máscaras de bits são os mesmos e retorna um novo valor de máscara de bits que contém as partes correspondentes.
Uma coisa importante é que estas mascaras de bits afetam apenas o lado da simulação de física do SpriteKit e você não será notificado de colisões detectadas dessa maneira. Isso significa que os corpos podem interagir entre si, mas nenhum dos métodos do representante de contato serão chamados.
Para que estes métodos sejam executados, você deve especificar um contactTestBitMask
para cada corpo, que gera um valor diferente de zero quando um operador AND
atuar neles. Para todos os corpos físicos, esta mascara de bits tem um valor padrão de 0x00000000, o que significa que você não pode ser notificado de qualquer colisão que os corpos físicos façam parte.
Copos físicos, incluindo suas várias mascaras de bits, podem ser configuradas no editor de cena. Abra o MainScene.sks, selecione o carro e abra o Attributes Inspector na direita. Desça até a seção Physics Definition.



Como o Body Type está definido como None, nenhuma das propriedades relacionadas a física são visíveis. Para alterar isso, precisamos atribuir um valor diferente de None ao Body Type. Três tipos de corpos estão disponíveis:
- bounding rectangle
- bounding circle
- alpha mask



Estes três tipos de corpos físicos são os mais comuns no SpriteKit. O Bounding rectangle e o Bounding circle funcionam criando uma barreira em torno do sprite para ser usado na simulação da física. Isso significa que o sprite colide com outro nó sempre que o limite da sua forma delimitadora atingir o corpo físico de outro nó.
A borda de um retângulo delimitador é exatamente do mesmo tamanho do nó mostrado no editor de cena. Se você selecionar Bounding circule, entretanto, você verá um fino circulo azul representando a forma do circulo delimitador.



O Alpha mask funciona um pouco diferente e verifica a textura real da imagem do sprite para determinar as bordas do corpo físico. Este tipo de corpo é de longe o mais preciso no SpriteKit, mas ele tem um grande impacto na performance do seu jogo, em particular quando se está usando sprite com formas complexas.
Para nosso jogo, já que usamos apenas o sprite de um carro e nossa cena não é particularmente complexa, iremos usar o corpo do tipo Alpha mask. Não é recomendando usar este tipo de corpo para todos os sprites em sua cena, mesmo sendo mais preciso. Quando você seleciona esta opção, você poderá ver uma linha azul aparecendo ao redor das bordas do carro.



É importante notar que outros tipos de corpos físicos podem ser criados via programação, tais como corpos de objetos CGPath
, bem como círculos e retângulos de tamanhos personalizados.
Ainda no Attributes Inspector, você poderá ver agora mais opções disponíveis para você na seção Physics Definition. A única propriedade que precisamos mudar é o Contact Mask. Altere seu valor para 1.



Com o corpo físico do carro configurado, podemos começar colocando alguns obstáculos no jogo para colidir com o carro.
3. Detectando colisões
Antes de implementarmos os métodos do protocolo SKPhysicsContactDelegate
, precisamos adicionar alguns obstáculos para o carro evitar. Para fazer isso, iremos colocar na frente do carro um novo obstáculo a cada três segundos e posicionar os obstáculos em uma pista aleatória.
Abra o MainsScene.swift e adicione uma instrução de import para o framework GameplayKit, assim podemos usar o gerador de números aleatórios fornecido pelo GameplayKit.
1 |
import GameplayKit |
Em seguida, adicione o seguinte método na classe MainScene
:
1 |
func spawnObstacle(timer: NSTimer) { |
2 |
if player.hidden { |
3 |
timer.invalidate() |
4 |
return
|
5 |
}
|
6 |
|
7 |
let spriteGenerator = GKShuffledDistribution(lowestValue: 1, highestValue: 2) |
8 |
let obstacle = SKSpriteNode(imageNamed: "Obstacle \(spriteGenerator.nextInt())") |
9 |
obstacle.xScale = 0.3 |
10 |
obstacle.yScale = 0.3 |
11 |
|
12 |
let physicsBody = SKPhysicsBody(circleOfRadius: 15) |
13 |
physicsBody.contactTestBitMask = 0x00000001 |
14 |
physicsBody.pinned = true |
15 |
physicsBody.allowsRotation = false |
16 |
obstacle.physicsBody = physicsBody |
17 |
|
18 |
let center = size.width/2.0, difference = CGFloat(85.0) |
19 |
var x: CGFloat = 0 |
20 |
|
21 |
let laneGenerator = GKShuffledDistribution(lowestValue: 1, highestValue: 3) |
22 |
switch laneGenerator.nextInt() { |
23 |
case 1: |
24 |
x = center - difference |
25 |
case 2: |
26 |
x = center |
27 |
case 3: |
28 |
x = center + difference |
29 |
default: |
30 |
fatalError("Number outside of [1, 3] generated") |
31 |
}
|
32 |
|
33 |
obstacle.position = CGPoint(x: x, y: (player.position.y + 800)) |
34 |
addChild(obstacle) |
35 |
}
|
Este método é chamado a cada três segundos. Vamos destrincha-lo abaixo para ver o que esta acontecendo. Se o nó do jogador estiver invisível, o que acontecerá quando o carro atingir um obstáculo, então invalidamos o timer e paramos a criação dos obstáculos.
Obtemos um número aleatorio entre 1 e 2, e o usamos para criar um nó de sprite com um dos dois sprites de obstáculos disponíveis no projeto. Então alteramos a escala do obstáculos, para que eles sejam pequenos o suficiente para que o carro possa desviar.
Em seguida, criamos um corpo físico para este novo obstáculo com um circulo com um raio de 15 e uma máscara de contato 0x00000001. Atribuímos true
para a pinned
e false
para a allowsRotation
, assim o obstáculo ficará no lugar e não se moverá. O corpo físico é então atribuído ao obstáculo.
Geramos outro número aleatorio entre 1 e 3, para determinar em qual pista o obstáculo irá aparecer e damos ao obstáculo sua posição calculada e o adicionamos a cena.
Perceba que a diferença na horizontal, 85, usado no spawnObstacle(_:)
é diferente da que usamos quando movemos o carro, 70. Fazemos isso para dar um pouco mais de espaço para o carro se mover entre os obstáculos.
Com a lógica de aparição dos obstáculos no lugar, podemos adicionar o seguinte código no final do método didMoveToView(_:)
.
1 |
override func didMoveToView(view: SKView) { |
2 |
|
3 |
...
|
4 |
|
5 |
let timer = NSTimer(timeInterval: 3.0, target: self, selector: #selector(spawnInObstacle(_:)), userInfo: nil, repeats: true) |
6 |
NSRunLoop.mainRunLoop().addTimer(timer, forMode: NSRunLoopCommonModes) |
7 |
|
8 |
let camera = SKCameraNode() |
9 |
self.camera = camera |
10 |
camera.position = CGPoint(x: center, y: player.position.y + 200) |
11 |
let moveForward = SKAction.moveBy(CGVectorMake(0, 100), duration: 1.0) |
12 |
camera.runAction(SKAction.repeatActionForever(moveForward)) |
13 |
addChild(camera) |
14 |
|
15 |
player.xScale = 0.4; player.yScale = 0.4 // Makes car smaller to fit better between obstacles |
16 |
}
|
Criamos um timer para executar o spawObstacle(_:)
a cada três segundos e adicionamos ele ao loop principal de execução. Também criamos um SKCameraNode
que atua como a câmera para a cena e a atribuímos para a propriedade camera
da cena. Isso faz com que a cena seja renderizada a partir do ponto de vista do nó da câmera. Veja que a câmera é centralizada na horizontal e ligeiramente acima do carro.
Adicionamos também um ação idêntica para a câmera, para que ela fique alinhada acima do carro. Adicionamos a câmera como um nó filho da cena como qualquer outro nó regular. Por último mas não menos importante, reduzimos a escala do carro para que ele possa se encaixar um pouco melhor entre os obstáculos.
A última parte para a detecção de colisão é implementar um dos métodos do protocolo SKPhysicsContactDelegate
. No nosso caso, como queremos ser notificados assim que o carro acerta um obstáculo vamos implementar o método didBeginContact(_:)
. Adicione o seguinte método à classe MainScene
.
1 |
func didBeginContact(contact: SKPhysicsContact) { |
2 |
if contact.bodyA.node == player || contact.bodyB.node == player { |
3 |
player.hidden = true |
4 |
player.removeAllActions() |
5 |
camera?.removeAllActions() |
6 |
}
|
7 |
}
|
Você pode ver que este método recebe um parâmetro SKPhysicsContact
. Este método contém as principais informações sobre a colisão, incluindo sua direção, impulso e os objetos envolvidos.
Neste código, estamos preocupados apenas sobre quais nós estão envolvidos na colisão. Verificamos se um deles é o carro, se for, tornamos o carro invisível e paramos o movimento do carro e da câmera.
Compile e execute o seu app para jogar. Você verá que, quando o jogador acerta um obstáculo o carro desaparece e a cena para de se mover.



Conclusão
Agora você deve saber como configurar os corpos físicos para os nós em sua cena e usar-los para simular físicas realistas. Você também deve estar confortável para configurar a detecção de colisão em sua cena, definindo um representante de contato e atribuir máscaras de bits para o teste de contato dos nós que você espera que se colidem entre si.
No próximo tutorial de SpritKit From Scratch, iremos olhar a funcionalidade visual mais avançado em SpriteKit , incluindo sistema de partículas, luzes e filtros.
Como sempre, deixa seus comentários e feedback nos comentários abaixo.
Seja o primeiro a saber sobre novas traduções–siga @tutsplus_pt no Twitter!