Introduzione a GameplayKit parte 2

Questa è la seconda parte di Un'introduzione a GameplayKit. Se non hai ancora affrontato la prima parte, allora consiglio di leggere quel tutorial prima di continuare con questo.

introduzione

In questo tutorial, ho intenzione di insegnarti altre due funzionalità del framework GameplayKit che puoi sfruttare:

  • agenti, obiettivi e comportamenti
  • pathfinding

Utilizzando agenti, obiettivi e comportamenti, costruiremo alcune intelligenze artificiali di base (AI) nel gioco che abbiamo iniziato nella prima parte di questa serie. L'intelligenza artificiale consentirà ai nostri punti nemici rossi e gialli di puntare e passare verso il nostro punto giocatore blu. Stiamo anche implementando il path-finding per estendere questa IA per superare ostacoli.

Per questo tutorial, puoi utilizzare la tua copia del progetto completato dalla prima parte di questa serie o scaricare una nuova copia del codice sorgente da GitHub.

1. Agenti, obiettivi e comportamenti

In GameplayKit, gli agenti, gli obiettivi e i comportamenti vengono utilizzati in combinazione l'uno con l'altro per definire in che modo diversi oggetti si muovono in relazione l'uno con l'altro nella scena. Per un singolo oggetto (o SKShapeNode nel nostro gioco), si inizia creando un agente, rappresentato dal GKAgent classe. Tuttavia, per i giochi 2D, come il nostro, abbiamo bisogno di usare il cemento GKAgent2D classe.

Il GKAgent la classe è una sottoclasse di GKComponent. Ciò significa che il tuo gioco deve utilizzare una struttura basata su entità e componenti come ti ho mostrato nel primo tutorial di questa serie.

Gli agenti rappresentano la posizione, la dimensione e la velocità dell'oggetto. Quindi aggiungi un comportamento, rappresentato dal GKBehaviour classe, a questo agente. Alla fine, crei un set di obiettivi, rappresentato dal GKGoal classe e aggiungerli all'oggetto comportamento. Gli obiettivi possono essere utilizzati per creare diversi elementi di gioco, ad esempio:

  • verso un agente
  • allontanarsi da un agente
  • raggruppamento in stretta collaborazione con altri agenti
  • girovagando per una posizione specifica

L'oggetto del comportamento monitora e calcola tutti gli obiettivi che gli vengono aggiunti e li inoltra nuovamente all'agente. Vediamo come funziona in pratica.

Apri il tuo progetto Xcode e vai a PlayerNode.swift. Prima dobbiamo assicurarci che il PlayerNode classe conforme al GKAgentDelegate protocollo.

class PlayerNode: SKShapeNode, GKAgentDelegate ... 

Quindi, aggiungere il seguente blocco di codice al PlayerNode classe.

var agent = GKAgent2D () // MARK: Agent Delegate func agentWillUpdate (agent: GKAgent) se let agent2D = agent as? GKAgent2D agent2D.position = float2 (Float (position.x), Float (position.y)) func agentDidUpdate (agent: GKAgent) se let agent2D = agent as? GKAgent2D self.position = CGPoint (x: CGFloat (agent2D.position.x), y: CGFloat (agent2D.position.y))

Iniziamo aggiungendo una proprietà al PlayerNode classe in modo da avere sempre un riferimento all'oggetto agente del giocatore corrente. Successivamente, implementiamo i due metodi di GKAgentDelegate protocollo. Implementando questi metodi, ci assicuriamo che il punto del giocatore visualizzato sullo schermo rispecchi sempre le modifiche apportate da GameplayKit.

Il agentWillUpdate (_ :) il metodo è chiamato appena prima che GameplayKit guardi attraverso il comportamento e gli obiettivi di quell'agente per determinare dove dovrebbe muoversi. Allo stesso modo, il agentDidUpdate (_ :) il metodo viene chiamato subito dopo che GameplayKit ha completato questo processo.

La nostra implementazione di questi due metodi assicura che il nodo che vediamo sullo schermo rifletta le modifiche apportate da GameplayKit e che GameplayKit utilizzi l'ultima posizione del nodo quando esegue i suoi calcoli.

Avanti, aperto ContactNode.swift e sostituire il contenuto del file con la seguente implementazione:

import UIKit import SpriteKit import GameplayKit class ContactNode: SKShapeNode, GKAgentDelegate var agent = GKAgent2D () // MARK: Agent Delegate func agentWillUpdate (agent: GKAgent) if let agent2D = agent as? GKAgent2D agent2D.position = float2 (Float (position.x), Float (position.y)) func agentDidUpdate (agent: GKAgent) se let agent2D = agent as? GKAgent2D self.position = CGPoint (x: CGFloat (agent2D.position.x), y: CGFloat (agent2D.position.y))

Implementando il GKAgentDelegate protocollo nel ContactNode classe, permettiamo che tutti gli altri punti del nostro gioco siano aggiornati con GameplayKit così come il nostro punto giocatore.

È giunto il momento di impostare i comportamenti e gli obiettivi. Per farlo funzionare, dobbiamo occuparci di tre cose:

  • Aggiungi l'agente del nodo giocatore alla sua entità e imposta il suo delegato.
  • Configura agenti, comportamenti e obiettivi per tutti i nostri punti nemici.
  • Aggiorna tutti questi agenti al momento giusto.

In primo luogo, aperto GameScene.swift e, alla fine del didMoveToView (_ :) metodo, aggiungi le seguenti due righe di codice:

playerNode.entity.addComponent (playerNode.agent) playerNode.agent.delegate = playerNode

Con queste due righe di codice, aggiungiamo l'agente come componente e impostiamo il delegato dell'agente come il nodo stesso.

Quindi, sostituire l'implementazione di initialSpawn metodo con la seguente implementazione:

func initialSpawn () per point in self.spawnPoints let respawnFactor = arc4random ()% 3 // Produce un valore compreso tra 0 e 2 (incluso) var node: SKShapeNode? = nil switch respawnFactor caso 0: node = PointsNode (circleOfRadius: 25) nodo! .physicsBody = SKPhysicsBody (circleOfRadius: 25) nodo! .fillColor = UIColor.greenColor () caso 1: nodo = RedEnemyNode (circleOfRadius: 75) nodo! .physicsBody = SKPhysicsBody (circleOfRadius: 75) node! .fillColor = UIColor.redColor () caso 2: node = YellowEnemyNode (circleOfRadius: 50) nodo! .physicsBody = SKPhysicsBody (circleOfRadius: 50) nodo! .fillColor = UIColor.yellowColor ( ) default: break se let entity = node? .valueForKey ("entity") as? GKEntity, lasciare agent = node? .ValueForKey ("agent") come? GKAgent2D dove respawnFactor! = 0 entity.addComponent (agent) agent.delegate = node as? ContactNode agent.position = float2 (x: Float (point.x), y: Float (point.y)) agents.append (agent) let behavior = GKBehavior (obiettivo: GKGoal (toSeekAgent: playerNode.agent), peso: 1,0 ) agent.behavior = comportamento agent.mass = 0.01 agent.maxSpeed ​​= 50 agent.maxAcceleration = 1000 nodo! .position = point node! .strokeColor = UIColor.clearColor () nodo! .physicsBody! .contactTestBitMask = 1 self.addChild (nodo!)

Il codice più importante che abbiamo aggiunto si trova nel Se affermazione che segue il interruttore dichiarazione. Passiamo attraverso questo codice riga per riga:

  • Prima addizioniamo l'agente all'entità come componente e configuriamo il suo delegato.
  • Successivamente, assegniamo la posizione dell'agente e aggiungiamo l'agente a un array memorizzato, agenti. Aggiungeremo questa proprietà al GameScene classe in un momento.
  • Quindi creiamo a GKBehavior oggetto con un singolo GKGoal prendere di mira l'agente del giocatore attuale. Il peso il parametro in questo inizializzatore viene utilizzato per determinare quali obiettivi devono avere la precedenza sugli altri. Ad esempio, immagina di avere un obiettivo di indirizzare un determinato agente e un obiettivo di allontanarti da un altro agente, ma desideri che l'obiettivo di targeting abbia la priorità. In questo caso, potresti dare un peso al goal di targeting 1 e l'allontanamento obiettivo un peso di 0.5. Questo comportamento viene quindi assegnato all'agente del nodo nemico.
  • Infine, configuriamo il massamassima velocità, e MaxAcceleration proprietà dell'agente. Questi influenzano la velocità con cui gli oggetti possono muoversi e girare. Sentiti libero di giocare con questi valori e vedere come influenza il movimento dei punti nemici.

Quindi, aggiungere le seguenti due proprietà a GameScene classe:

var agent: [GKAgent2D] = [] var lastUpdateTime: CFTimeInterval = 0.0

Il agenti l'array verrà usato per mantenere un riferimento agli agenti nemici nella scena. Il LastUpdateTime la proprietà verrà utilizzata per calcolare il tempo trascorso dall'ultimo aggiornamento della scena.

Infine, sostituire l'implementazione del aggiornare(_:) metodo del GameScene classe con la seguente implementazione:

override func update (currentTime: CFTimeInterval) / * Chiamato prima che ogni frame sia reso * / self.camera?.position = playerNode.position se self.lastUpdateTime == 0 lastUpdateTime = currentTime lascia delta = currentTime - lastUpdateTime lastUpdateTime = currentTime playerNode.agent.updateWithDeltaTime (delta) per l'agente negli agenti agent.updateWithDeltaTime (delta)

Nel aggiornare(_:) metodo, calcoliamo il tempo trascorso dall'ultimo aggiornamento di scena e quindi aggiorniamo gli agenti con quel valore.

Crea ed esegui la tua app e inizia a muoverti nella scena. Vedrai che i punti nemici inizieranno lentamente a muoversi verso di te.

Come puoi vedere, mentre i punti nemici colpiscono il giocatore corrente, non navigano intorno alle barriere bianche, invece cercano di spostarsi attraverso di loro. Rendiamo i nemici un po 'più intelligenti con il path-finding.

2. Pathfinding

Con il framework GameplayKit, puoi aggiungere complessi path-finding al tuo gioco combinando corpi fisici con classi e metodi GameplayKit. Per il nostro gioco, lo sistemeremo in modo che i punti nemici puntino al punto del giocatore e allo stesso tempo navighino attorno agli ostacoli.

Pathfinding in GameplayKit inizia con la creazione di a grafico della tua scena. Questo grafico è una raccolta di singole posizioni, anche denominate nodi, e collegamenti tra queste posizioni. Queste connessioni definiscono come un particolare oggetto può spostarsi da una posizione all'altra. Un grafico può modellare i percorsi disponibili nella scena in tre modi:

  • Uno spazio continuo contenente ostacoli: Questo modello grafico consente percorsi regolari attorno agli ostacoli da una posizione all'altra. Per questo modello, il GKObstacleGraph la classe è usata per il grafico, il GKPolygonObstacle classe per gli ostacoli, e il GKGraphNode2D classe per nodi (posizioni).
  • Una semplice griglia 2D: In questo caso, le posizioni valide possono essere solo quelle con coordinate intere. Questo modello di grafico è utile quando la scena ha un layout grigliato distinto e non sono necessari percorsi uniformi. Quando si utilizza questo modello, gli oggetti possono muoversi solo orizzontalmente o verticalmente in una singola direzione in qualsiasi momento. Per questo modello, il GKGridGraph la classe è usata per il grafico e il GKGridGraphNode classe per i nodi.
  • Una raccolta di luoghi e le connessioni tra di loro: Questo è il modello grafico più generico ed è consigliato per i casi in cui gli oggetti si spostano tra spazi diversi, ma la loro posizione specifica all'interno di quello spazio non è essenziale per il gameplay. Per questo modello, il GKGraph la classe è usata per il grafico e il GKGraphNode classe per i nodi.

Perché vogliamo che il giocatore punti nel nostro gioco per navigare attorno alle barriere bianche, useremo il GKObstacleGraph classe per creare un grafico della nostra scena. Per iniziare, sostituire il spawnPoints proprietà nel GameScene classe con il seguente:

let spawnPoints = [CGPoint (x: 245, y: 3900), CGPoint (x: 700, y: 3500), CGPoint (x: 1250, y: 1500), CGPoint (x: 1200, y: 1950), CGPoint ( x: 1200, y: 2450), CGPoint (x: 1200, y: 2950), CGPoint (x: 1200, y: 3400), CGPoint (x: 2550, y: 2350), CGPoint (x: 2500, y: 3100), CGPoint (x: 3000, y: 2400), CGPoint (x: 2048, y: 2400), CGPoint (x: 2200, y: 2200)] var grafico: GKObstacleGraph!

Il spawnPoints la matrice contiene alcune posizioni di spawn modificate ai fini di questo tutorial. Questo perché attualmente GameplayKit può solo calcolare percorsi tra oggetti relativamente vicini tra loro.

A causa della grande distanza predefinita tra i punti in questo gioco, è necessario aggiungere un paio di nuovi punti di spawn per illustrare il percorso. Nota che dichiariamo anche a grafico proprietà di tipo GKObstacleGraph per mantenere un riferimento al grafico che creeremo.

Quindi, aggiungere le seguenti due righe di codice all'inizio del didMoveToView (_ :) metodo:

lascia ostacoli = SKNode.obstaclesFromNodePhysicsBodies (auto.children) graph = GKObstacleGraph (obstacles: obstacles, bufferRadius: 0.0)

Nella prima riga, creiamo una serie di ostacoli dai corpi fisici nella scena. Quindi creiamo l'oggetto grafico usando questi ostacoli. Il bufferRadius il parametro in questo inizializzatore può essere utilizzato per forzare gli oggetti a non rientrare in una certa distanza da questi ostacoli. Queste linee devono essere aggiunte all'inizio del didMoveToView (_ :) metodo, perché il grafico che creiamo è necessario dal momento in cui il initialSpawn il metodo è chiamato.

Infine, sostituire il initialSpawn metodo con la seguente implementazione:

func initialSpawn () let endNode = GKGraphNode2D (point: float2 (x: 2048.0, y: 2048.0)) self.graph.connectNodeUsingObstacles (endNode) per il punto in self.spawnPoints let respawnFactor = arc4random ()% 3 // Produrrà un valore compreso tra 0 e 2 (incluso) nodo var: SKShapeNode? = nil switch respawnFactor caso 0: node = PointsNode (circleOfRadius: 25) nodo! .physicsBody = SKPhysicsBody (circleOfRadius: 25) nodo! .fillColor = UIColor.greenColor () caso 1: nodo = RedEnemyNode (circleOfRadius: 75) nodo! .physicsBody = SKPhysicsBody (circleOfRadius: 75) node! .fillColor = UIColor.redColor () caso 2: node = YellowEnemyNode (circleOfRadius: 50) nodo! .physicsBody = SKPhysicsBody (circleOfRadius: 50) nodo! .fillColor = UIColor.yellowColor ( ) default: break se let entity = node? .valueForKey ("entity") as? GKEntity, lasciare agent = node? .ValueForKey ("agent") come? GKAgent2D dove respawnFactor! = 0 entity.addComponent (agent) agent.delegate = node as? ContactNode agent.position = float2 (x: Float (point.x), y: Float (point.y)) agents.append (agent) / * let behavior = GKBehavior (obiettivo: GKGoal (toSeekAgent: playerNode.agent), peso : 1.0) agent.behavior = behavior * / / *** BEGIN PATHFINDING *** / let startNode = GKGraphNode2D (point: agent.position) self.graph.connectNodeUsingObstacles (startNode) lascia pathNodes = self.graph.findPathFromNode (startNode, toNode: endNode) come! [GKGraphNode2D] if! PathNodes.isEmpty let path = GKPath (graphNodes: pathNodes, raggio: 1.0) let followPath = GKGoal (toFollowPath: path, maxPredictionTime: 1.0, forward: true) let stayOnPath = GKGoal (toStayOnPath: path, maxPredictionTime: 1.0) let behavior = GKBehavior (goals: [followPath, stayOnPath]) agent.behavior = comportamento self.graph.removeNodes ([startNode]) / *** END PATHFINDING *** / agent.mass = 0.01 agent.maxSpeed ​​= 50 agent.maxAcceleration = 1000 nodo! .Position = point node! .StrokeColor = UIColor.clearColor () nodo! .PhysicsBody! .ContactTestBitMask = 1 self.addChild (nodo!) Self.graph.removeNodes ([endNode]) 

Iniziamo il metodo creando un GKGraphNode2D oggetto con le coordinate di spawn del giocatore di default. Successivamente, colleghiamo questo nodo al grafico in modo che possa essere utilizzato per trovare i percorsi.

La maggior parte della initialSpawn il metodo rimane invariato. Ho aggiunto alcuni commenti per mostrarti dove si trova la porzione del path code del codice nel primo Se dichiarazione. Passiamo attraverso questo codice passo dopo passo:

  • Ne creiamo un altro GKGraphNode2D istanza e collegarlo al grafico.
  • Creiamo una serie di nodi che costituiscono un percorso chiamando il findPathFromNode (_: toNode :) metodo sul nostro grafico.
  • Se una serie di nodi di percorso è stata creata correttamente, creiamo quindi un percorso da essi. Il raggio parametro funziona in modo simile al bufferRadius parametro precedente e definisce quanto un oggetto può allontanarsi dal percorso creato.
  • Creiamo due GKGoal oggetti, uno per seguire il percorso e un altro per rimanere sul sentiero. Il maxPredictionTime parametro consente all'obiettivo di calcolare al meglio in anticipo se qualcosa interrompe l'oggetto dal seguire / rimanere su quel particolare percorso.
  • Infine, creiamo un nuovo comportamento con questi due obiettivi e lo assegniamo all'agente.

Noterai anche che rimuoviamo i nodi che creiamo dal grafico una volta che abbiamo finito con loro. Questa è una buona pratica da seguire in quanto garantisce che i nodi che hai creato non interferiscano con altri calcoli di identificazione dei percorsi più tardi.

Costruisci ed esegui la tua app un'ultima volta e vedrai due punti spawn molto vicini a te e inizi a muoverti verso di te. Potrebbe essere necessario eseguire il gioco più volte se entrambi generano punti verdi.

Importante!

In questo tutorial, abbiamo utilizzato la funzionalità di ricerca del percorso di GameplayKit per consentire ai punti nemici di colpire il punto del giocatore attorno agli ostacoli. Si noti che questo era solo per un esempio pratico di path-finding.

Per un gioco di produzione reale, sarebbe meglio implementare questa funzionalità combinando l'obiettivo di targeting del giocatore di prima in questo tutorial con un obiettivo di evitare l'ostacolo creato con il init (toAvoidObstacles: maxPredictionTime :) metodo di convenienza, che puoi leggere di più in GKGoal Riferimento di classe.

Conclusione

In questo tutorial, ti ho mostrato come puoi utilizzare agenti, obiettivi e comportamenti nei giochi che hanno una struttura di entità-componente. Mentre abbiamo creato solo tre obiettivi in ​​questo tutorial, ce ne sono molti altri disponibili, di cui puoi leggere di più su GKGoal Riferimento di classe.

Ti ho anche mostrato come implementare alcuni pathfinder avanzati nel tuo gioco creando un grafico, una serie di ostacoli e obiettivi per seguire questi percorsi.

Come puoi vedere, c'è una grande quantità di funzionalità messe a tua disposizione attraverso il framework GameplayKit. Nella terza e ultima parte di questa serie, ti insegnerò i generatori di valori casuali di GameplayKit e come creare il tuo sistema di regole per introdurre qualche logica fuzzy nel tuo gioco.

Come sempre, si prega di essere sicuri di lasciare i vostri commenti e feedback qui sotto.