Portuguese (Português) translation by David Batista (you can also view the original English article)
Está é a segunda parte do Uma Introdução ao GameplayKit. Se você ainda não passou pela primeira parte, então eu recomendo a leitura deste primeiro tutorial antes de continuar com este.
Introdução
Neste tutorial, eu irei ensinar a você sobre mais duas funcionalidades do framework GameplayKit que você pode tirar vantagem:
- agentes, metas e comportamentos
- busca de caminho
Ao utilizar agentes, metas e comportamentos, nós iremos construir uma inteligência artificial (IA) básica no jogo que iniciamos na primeira parte desta serie. A IA permitira que nossos inimigos, ponto vermelho e ponto amarelo, se direcionem e se movam em direção ao nosso jogador, ponto azul. Nós também iremos implementar uma busca de caminho para ajudar esta IA a navegar em torno dos obstáculos.
Para este tutorial, você pode usar sua copia do projeto completado na primeira parte desta série ou fazer o download de uma copia fresquinha do nosso código no GitHub.
1. Agentes, metas e comportamentos
No Gameplaykit, agentes, metas e comportamentos são usados em combinação uns com os outros para definir como os diferentes objetos se movem em relação uns aos outros em todo seu cenário. Para um único objeto (ou SKShapeNode
em nosso jogo), você começa criando um agente, representado pela classe GKAgent
. Porém, para jogos 2D, como o nosso, nós precisamos usar a classe GKAgent2D
.
A classe GKAgent
é uma subclasse da GKComponent
. Isso significa que seu jogo precisa estar usando uma estrutura baseada em entidade-componente, como mostrei a você no primeiro tutorial desta série.
Agentes constituem uma posição, tamanho e velocidade do objeto. Você também pode adicionar um comportamento, representado pela classe GKBehaviour
, para este agente. Por ultimo, você cria um conjunto de metas, representados pela classe GKGoal
, e adiciona elas ao objeto comportamento. Metas podem ser usadas para criar vários elementos diferentes de gameplay, por exemplo:
- movimentar em direção a um agente
- movimentar para longe de um agente
- agrupar em conjunto com outros agentes
- vagar em uma posição específica
Seu objeto comportamento, monitora e calcula todas as metas que você adicionar a ele e então retransmite esse dado de volto ao agente. Vamos ver como isso funciona na prática.
Abra o seu projeto no Xcode e navegue para o arquivo PlayerNode.Swift. Primeiro precisamos certificar que a classe PlayerNode
está em conformidade com o protocolo GKAgenteDelegate
.
1 |
class PlayerNode: SKShapeNode, GKAgentDelegate { |
2 |
...
|
Em seguida, adicione o bloco de código abaixo à classe PlayerNode
.
1 |
var agent = GKAgent2D() |
2 |
|
3 |
// MARK: Agent Delegate
|
4 |
func agentWillUpdate(agent: GKAgent) { |
5 |
if let agent2D = agent as? GKAgent2D { |
6 |
agent2D.position = float2(Float(position.x), Float(position.y)) |
7 |
}
|
8 |
}
|
9 |
|
10 |
func agentDidUpdate(agent: GKAgent) { |
11 |
if let agent2D = agent as? GKAgent2D { |
12 |
self.position = CGPoint(x: CGFloat(agent2D.position.x), y: CGFloat(agent2D.position.y)) |
13 |
}
|
14 |
}
|
Nós começamos adicionando uma propriedade à classe PlayerNode
de modo que nós sempre temos uma referência ao objeto agente do jogador atual. Em seguida, implementamos os dois métodos do protocolo GKAgentDelegate
. Através da implementação destes métodos, nós garantimos que o ponto do jogador mostrado na tela sempre espelhará as alterações que o GameplayKit fizer.
O método agentWillUpdate(_:)
é chamado um pouco antes do GameplayKit verificar através dos comportamentos e metas deste agente para determinar para onde ele deve ser mover. Da mesmo forma, o método agentDidUpdate(_:)
é chamado logo após o GameplayKit ter completado esse processo.
Nossa implementação desses dois métodos garante que o nó que vemos na tela reflete as alterações feitas pelo GameplayKit e que o GameplayKit usa a ultima posição do nó quando for executar seus cálculos.
Em seguida, abra o arquivo ContactNode.swift e substitua o seu conteúdo com a implementação abaixo:
1 |
import UIKit |
2 |
import SpriteKit |
3 |
import GameplayKit |
4 |
|
5 |
class ContactNode: SKShapeNode, GKAgentDelegate { |
6 |
|
7 |
var agent = GKAgent2D() |
8 |
|
9 |
// MARK: Agent Delegate
|
10 |
func agentWillUpdate(agent: GKAgent) { |
11 |
if let agent2D = agent as? GKAgent2D { |
12 |
agent2D.position = float2(Float(position.x), Float(position.y)) |
13 |
}
|
14 |
}
|
15 |
|
16 |
func agentDidUpdate(agent: GKAgent) { |
17 |
if let agent2D = agent as? GKAgent2D { |
18 |
self.position = CGPoint(x: CGFloat(agent2D.position.x), y: CGFloat(agent2D.position.y)) |
19 |
}
|
20 |
}
|
21 |
}
|
Implementando o protocolo GKAgentDelegate
na classe ContactNode
, permitimos que todos os outros pontos do nosso jogo vão estar em dia com o GameplayKit, assim como nosso ponto do jogador.
Agora é hora de definir os comportamentos e metas. Para fazer funcionar, nós precisamos tomar cuidado com três coisas:
- Adicionar o agente do nó do jogador a esta entidade e definir seu delegate.
- Configurar os agentes, comportamentos e metas para todos os nossos pontos inimigos.
- Atualizar todos esses agentes em tempo de execução.
Primeiramente, abra o arquivo GameScene.swift e no final do método didMoveToView(_:)
, adicione as duas linhas de código abaixo:
1 |
playerNode.entity.addComponent(playerNode.agent) |
2 |
playerNode.agent.delegate = playerNode |
Com estas duas linhas de código, nós adicionamos o agente como um componente e definimos para o delegate do agente ser o próprio nó.
Em seguida, substitua a implementação do método initialSpawn
com a implementação abaixo:
1 |
func initialSpawn() { |
2 |
for point in self.spawnPoints { |
3 |
let respawnFactor = arc4random() % 3 // Will produce a value between 0 and 2 (inclusive) |
4 |
|
5 |
var node: SKShapeNode? = nil |
6 |
|
7 |
switch respawnFactor { |
8 |
case 0: |
9 |
node = PointsNode(circleOfRadius: 25) |
10 |
node!.physicsBody = SKPhysicsBody(circleOfRadius: 25) |
11 |
node!.fillColor = UIColor.greenColor() |
12 |
case 1: |
13 |
node = RedEnemyNode(circleOfRadius: 75) |
14 |
node!.physicsBody = SKPhysicsBody(circleOfRadius: 75) |
15 |
node!.fillColor = UIColor.redColor() |
16 |
case 2: |
17 |
node = YellowEnemyNode(circleOfRadius: 50) |
18 |
node!.physicsBody = SKPhysicsBody(circleOfRadius: 50) |
19 |
node!.fillColor = UIColor.yellowColor() |
20 |
default: |
21 |
break
|
22 |
}
|
23 |
|
24 |
if let entity = node?.valueForKey("entity") as? GKEntity, |
25 |
let agent = node?.valueForKey("agent") as? GKAgent2D where respawnFactor != 0 { |
26 |
|
27 |
entity.addComponent(agent) |
28 |
agent.delegate = node as? ContactNode |
29 |
agent.position = float2(x: Float(point.x), y: Float(point.y)) |
30 |
agents.append(agent) |
31 |
|
32 |
let behavior = GKBehavior(goal: GKGoal(toSeekAgent: playerNode.agent), weight: 1.0) |
33 |
agent.behavior = behavior |
34 |
|
35 |
agent.mass = 0.01 |
36 |
agent.maxSpeed = 50 |
37 |
agent.maxAcceleration = 1000 |
38 |
}
|
39 |
|
40 |
node!.position = point |
41 |
node!.strokeColor = UIColor.clearColor() |
42 |
node!.physicsBody!.contactTestBitMask = 1 |
43 |
self.addChild(node!) |
44 |
}
|
45 |
}
|
O código mais importante que nós adicionamos está localizado na instrução if
que segue a instrução switch
. Vamos percorrer este código linha por linha:
- Primeiro adicionamos o agente à entidade como um componente e configuramos seu delegate.
- Em seguida, nós atribuímos a posição do agente e adicionamos o agente ao array de armazenamento,
agents
. Vamos adicionar esta propriedade à classeGameScene
em um momento. - Nós então criamos um objeto
GKBehavior
com um únicoGKGoal
para direcionar ao agente do jogador corrente. O parâmetroweight
(peso) neste inicializador é usado para determinar quais metas devem ter precedência sobre outras. Por exemplo, imagine que você tenha uma meta para se direcionar a um agente em particular e uma meta para se afastar de outro agente, mas você quer que a meta de direcionamento tenha preferência. Neste caso, você pode dar à meta de direcionamento um peso de1
e à meta de se afastar um peso de0.5
. Este comportamento será então atribuído ao agente do nó inimigo. - Por ultimo, nós configuramos as propriedades
mass
,maxSpeed
emaxAcceleration
do agente. Isto afeta o quão rápido os objetos podem se mover e virar. Sinta-se livre para brincar com esses valores e ver como isso afeta o movimento dos pontos inimigos.
Em seguida, adicione as duas propriedades a seguir à classe GameScene
:
1 |
var agents: [GKAgent2D] = [] |
2 |
var lastUpdateTime: CFTimeInterval = 0.0 |
O array agents
será usado para manter uma referência dos agentes inimigo no cenário. A propriedade lastUpdateTime
será usada para calcular o tempo que passou desde a ultima atualização do cenário.
Por último, substitua a implementação do método update(_:)
da classe GameScene
com a seguinte implementação:
1 |
override func update(currentTime: CFTimeInterval) { |
2 |
/* Called before each frame is rendered */
|
3 |
self.camera?.position = playerNode.position |
4 |
|
5 |
if self.lastUpdateTime == 0 { |
6 |
lastUpdateTime = currentTime |
7 |
}
|
8 |
|
9 |
let delta = currentTime - lastUpdateTime |
10 |
lastUpdateTime = currentTime |
11 |
|
12 |
playerNode.agent.updateWithDeltaTime(delta) |
13 |
|
14 |
for agent in agents { |
15 |
agent.updateWithDeltaTime(delta) |
16 |
}
|
17 |
}
|
No método update(_:)
, nós caculamos o tempo desde a ultima atualização do cenário e então atualizamos os agentes com esse valor.
Compile e rode seu app, e comece a se mover pelo cenário. Você notará que os pontos inimigos lentamente começaram a se mover atrás de você.
Como você pode ver, apesar dos pontos inimigos terem o jogador como alvo, eles não caminham em torno das barras brancas, ao invés disso, eles tentam se mover através delas. Vamos deixar os inimigos um pouco mais espertos com a busca de caminho.
2. Busca de Caminho
Com o framework Gameplaykit, você pode adicionar uma complexa lógica de busca de caminho em seu jogo combinando corpos físicos com classes e métodos do GameplayKit. Para o nosso jogo, nós iremos configurar para que os pontos inimigos se direcionarem ao ponto de jogador e ao mesmo tempo caminhem em torno dos obstáculos.
A busca de caminho no GameplayKit começa com a criação de um grafo do seu cenário. Este grafo é uma coleção de localizações individuais, também conhecidas como nós, e conexões entre estas localizações. Estas conexões definem como um objeto em particular pode se mover para outra localização. Um grafo pode modelar os caminhos disponíveis em seu cenário de três formas:
- Um espaço contínuo contendo obstáculos: Este modelo de grafo permite caminhos suaves ao redor de obstáculos de um local para outro. Para este modelo, a classe
GKObstacleGraph
é usada para o grafo, a classeGKPolygonObstacle
para os obstáculos e a classeGKGraphNode2D
para os nós (localizações). - Uma simples grade 2D: Neste caso, localização validas podem ser apenas aquelas com coordenadas inteiras. Este modelo de grafo é útil quando seu cenário tem um layout de grade distinta e você não precisa de caminhos suaves. Ao usar este modelo, objeto podem se mover apenas na horizontal ou vertical em uma unica direção em qualquer momento. Para este modelo, a classe
GKGridGraph
é usada para o grafo e a classeGKGridGraphNode
para os nós. - Uma coleção de localizações e as conexões entre elas: Este é o modelo gráfico mais genérico e é recomendado para casos onde objetos se movem entre espaços distintos, mas a sua localização específica dentro desse espaço não é essencial para o gameplay. Para este modelo, a classe
GKGraph
é usada para o grafo e a classeGKGraphNode
para os nós.
Como queremos que o ponto azul do jogador em nosso jogo navegue em torno das barras brancas, nós iremos usar a classe GKObstacleGraph
para criar um grafo do nosso cenário. Para começar, substitua a propriedade spawnPoints
da classe GameScene
com o seguinte:
1 |
let spawnPoints = [ |
2 |
CGPoint(x: 245, y: 3900), |
3 |
CGPoint(x: 700, y: 3500), |
4 |
CGPoint(x: 1250, y: 1500), |
5 |
CGPoint(x: 1200, y: 1950), |
6 |
CGPoint(x: 1200, y: 2450), |
7 |
CGPoint(x: 1200, y: 2950), |
8 |
CGPoint(x: 1200, y: 3400), |
9 |
CGPoint(x: 2550, y: 2350), |
10 |
CGPoint(x: 2500, y: 3100), |
11 |
CGPoint(x: 3000, y: 2400), |
12 |
CGPoint(x: 2048, y: 2400), |
13 |
CGPoint(x: 2200, y: 2200) |
14 |
]
|
15 |
var graph: GKObstacleGraph! |
O array spawnPoints
contém alguns locais de spawn alterados para os fins deste tutorial. Isso ocorre porque atualmente o GameplayKit só pode calcular caminhos entre os objetos que estão relativamente próximos uns dos outros.
Devido à grande distância padrão entre os pontos neste jogo, alguns novos pontos de spawn deve ser adicionado para ilustrar a busca de caminho. Perceba que nós também declaramos uma propriedade graph
do tipo GKObstacleGraph
para manter uma referência do grafo que iremos criar.
Em seguida, adicione as duas linhas de código a seguir no inicio do método didMoveToView(_:)
.
1 |
let obstacles = SKNode.obstaclesFromNodePhysicsBodies(self.children) |
2 |
graph = GKObstacleGraph(obstacles: obstacles, bufferRadius: 0.0) |
Na primeira linha, nós criamos um array de obstáculos dos corpos físicos no cenário. Nós então criamos o objeto grafo usando estes obstáculos. O parâmetro bufferRadius
neste inicializador pode ser usado para forçar os objetos há não entrarem a uma certa distância destes obstáculos. Estas linhas precisam ser adicionadas no inicio do método didMoveToView(_:)
, por que o grafo que criamos será necessário na hora em que o método initialSpawn
for chamado.
Por último, substitua o método initialSpawn
com a implementação abaixo:
1 |
func initialSpawn() { |
2 |
let endNode = GKGraphNode2D(point: float2(x: 2048.0, y: 2048.0)) |
3 |
self.graph.connectNodeUsingObstacles(endNode) |
4 |
|
5 |
for point in self.spawnPoints { |
6 |
let respawnFactor = arc4random() % 3 // Will produce a value between 0 and 2 (inclusive) |
7 |
|
8 |
var node: SKShapeNode? = nil |
9 |
|
10 |
switch respawnFactor { |
11 |
case 0: |
12 |
node = PointsNode(circleOfRadius: 25) |
13 |
node!.physicsBody = SKPhysicsBody(circleOfRadius: 25) |
14 |
node!.fillColor = UIColor.greenColor() |
15 |
case 1: |
16 |
node = RedEnemyNode(circleOfRadius: 75) |
17 |
node!.physicsBody = SKPhysicsBody(circleOfRadius: 75) |
18 |
node!.fillColor = UIColor.redColor() |
19 |
case 2: |
20 |
node = YellowEnemyNode(circleOfRadius: 50) |
21 |
node!.physicsBody = SKPhysicsBody(circleOfRadius: 50) |
22 |
node!.fillColor = UIColor.yellowColor() |
23 |
default: |
24 |
break
|
25 |
}
|
26 |
|
27 |
if let entity = node?.valueForKey("entity") as? GKEntity, |
28 |
let agent = node?.valueForKey("agent") as? GKAgent2D where respawnFactor != 0 { |
29 |
|
30 |
entity.addComponent(agent) |
31 |
agent.delegate = node as? ContactNode |
32 |
agent.position = float2(x: Float(point.x), y: Float(point.y)) |
33 |
agents.append(agent) |
34 |
|
35 |
/*let behavior = GKBehavior(goal: GKGoal(toSeekAgent: playerNode.agent), weight: 1.0)
|
36 |
agent.behavior = behavior*/
|
37 |
|
38 |
/*** BEGIN PATHFINDING ***/
|
39 |
let startNode = GKGraphNode2D(point: agent.position) |
40 |
self.graph.connectNodeUsingObstacles(startNode) |
41 |
|
42 |
let pathNodes = self.graph.findPathFromNode(startNode, toNode: endNode) as! [GKGraphNode2D] |
43 |
|
44 |
if !pathNodes.isEmpty { |
45 |
let path = GKPath(graphNodes: pathNodes, radius: 1.0) |
46 |
|
47 |
let followPath = GKGoal(toFollowPath: path, maxPredictionTime: 1.0, forward: true) |
48 |
let stayOnPath = GKGoal(toStayOnPath: path, maxPredictionTime: 1.0) |
49 |
|
50 |
let behavior = GKBehavior(goals: [followPath, stayOnPath]) |
51 |
agent.behavior = behavior |
52 |
}
|
53 |
|
54 |
self.graph.removeNodes([startNode]) |
55 |
/*** END PATHFINDING ***/
|
56 |
|
57 |
agent.mass = 0.01 |
58 |
agent.maxSpeed = 50 |
59 |
agent.maxAcceleration = 1000 |
60 |
}
|
61 |
|
62 |
node!.position = point |
63 |
node!.strokeColor = UIColor.clearColor() |
64 |
node!.physicsBody!.contactTestBitMask = 1 |
65 |
self.addChild(node!) |
66 |
}
|
67 |
|
68 |
self.graph.removeNodes([endNode]) |
69 |
}
|
Nós iniciamos o método criando um objeto GKGraphNode2D
com as coordenadas de onde o jogador irá aparecer. Em seguida, nós conectamos ela ao grafo de modo que ele possa ser usado quando buscarmos os caminhos.
A maior parte do método initialSpawn
permanece inalterado. Eu adicionei alguns comentários para mostrar á você onde a parte do código da busca de caminho está localizada na primeira instrução if
. Vamos passar por este bloco passo a passo:
- Nós criamos outra instância
GKGraphNode2D
e conectamos ela ao grafo. - Nós criamos uma série de nós que compõem um caminho, chamando o método
findPathFromNode(_:toNode:)
em nosso grafo. - Se a série de nós caminho for criada com sucesso, então nós criamos um caminho a partir deles. O parâmetro
radius
trabalho de forma similar ao parâmetrobufferRadius
de antes e define quanto um objeto pode se mover para fora do caminho criado. - Nós criamos dois objetos
GKGoal
, um para seguir o caminho e outra para ficar no caminho. O parâmetromaxPredictionTime
permite à meta calcular o melhor que puder antes do tempo se nada vai interromper o objeto a seguir/ficar nesse caminho particular. - Por último, nós criamos um novo comportamento com estas duas metas e atribuímos ele ao agente.
Você irá notar que nós removemos os nós que criamos a partir do grafo uma vez que terminamos com eles. Esta é uma boa prática a seguir, pois garante que os nós que você criou não interfiram mais tarde com qualquer outros cálculos de busca de caminho.
Compile e execute seu app uma ultima vez e você verá dois pontos aparecendo muito perto de você e começando a se mover atrás de você. Você pode ter que executar o jogo várias vezes se ambos aparecerem como pontos verdes.
Importante!
Neste tutorial, nós usamos a funcionalidade de busca de caminho do GameplayKit para permitir que os pontos inimigos se direcionem em sentido ao ponto do jogador em torno dos obstáculos. Note que se trata apenas de um exemplo prático de busca de caminho.
Para um jogo de produção real, seria melhor implementar essa funcionalidade combinando a meta de direcionamento usado mais cedo neste tutorial com a meta de evitar os obstáculo criados com o método de conveniência init(toAvoidObstacles:maxPredictionTime:)
, qual você pode ler mais na Referência da classe GKGoal
.
Conclusão
Neste tutorial, eu mostrei à você como você pode utilizar os agentes, metas e comportamentos em seu jogo que tiver uma estrutura entidade-componente. Apesar de criarmos apenas três metas neste tutorial, há muito mais formas disponíveis, que você pode ler na Referência da classe GKGoal
.
Eu também mostrei como implementar uma busca de caminho avançada em seu jogo criando um grafo, um conjunto de obstáculos e metas para seguir estes caminhos.
Como você pode ver, existe uma grande quantidade de funcionalidades disponíveis para você através do framework GameplayKit. Na terceira e final parte desta série, eu irei ensinar sobre geradores de valor aleátorio do GameplayKit e como criar seu próprio sistema de regras para introduzir uma lógica fuzzy em seu jogo.
Como sempre, por favor deixe seu comentário e feeedback abaixo.
Seja o primeiro a saber sobre novas traduções–siga @tutsplus_pt no Twitter!