En introduksjon til GameplayKit Del 2

Dette er den andre delen av En introduksjon til GameplayKit. Hvis du ikke har gått gjennom den første delen, anbefaler jeg at du leser den første opplæringen før du fortsetter med denne.

Introduksjon

I denne opplæringen skal jeg lære deg om to funksjoner i GameplayKit-rammen du kan dra nytte av:

  • agenter, mål og oppførsel
  • stifinning

Ved å utnytte agenter, mål og oppførsel skal vi bygge i noen grunnleggende kunstig intelligens (AI) i spillet som vi startet i første del av denne serien. AI vil gjøre det mulig for våre røde og gule fiendens prikker å målrette mot og flytte til vår blåspiller prikk. Vi skal også implementere pathfinding for å utvide på denne AI for å navigere rundt hindringer.

For denne opplæringen kan du bruke kopien av det gjennomførte prosjektet fra første del av denne serien eller laste ned en ny kopi av kildekoden fra GitHub.

1. Agenter, mål og oppførsel

I GameplayKit brukes agenter, mål og oppførsel i kombinasjon med hverandre for å definere hvordan ulike objekter beveger seg i forhold til hverandre i hele scenen. For et enkelt objekt (eller SKShapeNode i vårt spill), begynner du med å skape en middel, representert av GKAgent klasse. Men for 2D-spill, som vår, må vi bruke betongen GKAgent2D klasse.

De GKAgent klassen er en underklasse av GKComponent. Dette betyr at spillet ditt må bruke en enhet- og komponentbasert struktur som jeg viste deg i den første opplæringen i denne serien.

Agenter representerer et objekts posisjon, størrelse og hastighet. Deretter legger du til en oppførsel, representert av GKBehaviour klasse, til denne agenten. Til slutt lager du et sett med mål, representert av GKGoal klassen, og legg dem til oppførselsobjektet. Mål kan brukes til å lage mange forskjellige spillelementer, for eksempel:

  • beveger seg mot en agent
  • flytte vekk fra en agent
  • gruppering tett sammen med andre agenter
  • vandre rundt en bestemt posisjon

Din oppførselsobjekt overvåker og beregner alle målene du legger til i det, og relayer disse dataene tilbake til agenten. La oss se hvordan dette fungerer i praksis.

Åpne ditt Xcode-prosjekt og naviger til PlayerNode.swift. Vi må først sørge for at PlayerNode klassen stemmer overens med GKAgentDelegate protokollen.

klasse PlayerNode: SKShapeNode, GKAgentDelegate ... 

Deretter legger du til følgende kodeblokk til PlayerNode klasse.

var agent = GKAgent2D () // MARK: Agent Delegat func agentWillUpdate (agent: GKAgent) hvis la agent2D = agent som? GKAgent2D agent2D.position = float2 (Float (posisjon.x), Float (position.y)) func agentDidUpdate (agent: GKAgent) hvis la agent2D = agent som? GKAgent2D self.position = CGPoint (x: CGFloat (agent2D.position.x), y: CGFloat (agent2D.position.y))

Vi starter med å legge til en eiendom til PlayerNode klasse slik at vi alltid har en referanse til nåværende spillers agentobjekt. Deretter implementerer vi de to metodene til GKAgentDelegate protokoll. Ved å implementere disse metodene sikrer vi at spillerens prikk som vises på skjermen, alltid vil speile de endringene som GameplayKit gjør.

De agentWillUpdate (_ :) Metoden kalles like før GameplayKit ser gjennom atferdens oppførsel og mål for å avgjøre hvor den skal bevege seg. På samme måte, agentDidUpdate (_ :) Metoden kalles rett etter at GameplayKit har fullført denne prosessen.

Implementeringen av disse to metodene sikrer at noden vi ser på skjermen, gjenspeiler endringene GameplayKit gjør, og at GameplayKit bruker den siste posisjonen til noden når den utfører sine beregninger.

Deretter åpne ContactNode.swift og erstatt filens innhold med følgende implementering:

import UIKit import SpriteKit import GameplayKit klasse KontaktNode: SKShapeNode, GKAgentDelegate var agent = GKAgent2D () // MARK: Agent Delegat func agentWillUpdate (agent: GKAgent) hvis la agent2D = agent som? GKAgent2D agent2D.position = float2 (Float (posisjon.x), Float (position.y)) func agentDidUpdate (agent: GKAgent) hvis la agent2D = agent som? GKAgent2D self.position = CGPoint (x: CGFloat (agent2D.position.x), y: CGFloat (agent2D.position.y))

Ved å implementere GKAgentDelegate protokoll i ContactNode klassen, tillater vi at alle de andre punktene i spillet vårt er oppdatert med GameplayKit, så vel som vår spillerpotte.

Det er nå på tide å sette opp atferd og mål. For å gjøre dette arbeidet må vi ta vare på tre ting:

  • Legg spillerens knutepunkt til sin enhet og sett sin delegat.
  • Konfigurer agenter, oppførsel og mål for alle våre fiendens prikker.
  • Oppdater alle disse agenter på riktig tidspunkt.

For det første, åpne GameScene.swift og på slutten av didMoveToView (_ :) metode, legg til følgende to linjer med kode:

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

Med disse to kodelinjene legger vi agenten til som en komponent og setter agentens delegat til å være selve noden.

Neste, erstatt implementeringen av initialSpawn metode med følgende implementering:

func initialSpawn () for punkt i self.spawnPoints la respawnFactor = arc4random ()% 3 // Vil produsere en verdi mellom 0 og 2 (inkluderende) var node: SKShapeNode? = null bryter respawnFactor case 0: node = PointsNode (circleOfRadius: 25) node! .physicsBody = SKPhysicsBody (circleOfRadius: 25) node! .fillColor = UIColor.greenColor () tilfelle 1: node = RedEnemyNode (circleOfRadius: 75) node! .physicsBody = SKPhysicsBody (circleOfRadius: 75) node! .fillColor = UIColor.redColor () tilfelle 2: node = YellowEnemyNode (circleOfRadius: 50) node! .physicsBody = SKPhysicsBody (circleOfRadius: 50) node! .fillColor = UIColor.yellowColor ) standard: break hvis la enheten = node? .valueForKey ("entity") som? GKEntity, la agent = node? .ValueForKey ("agent") som? GKAgent2D hvor respawnFactor! = 0 entity.addComponent (agent) agent.delegate = node som? ContactNode agent.position = float2 (x: Float (punkt.x), y: Float (point.y)) agents.append (agent) la oppførsel = GKBehavior (mål: GKGoal (toSeekAgent: playerNode.agent), vekt: 1,0 ) agent.behavior = oppførsel agent.mass = 0.01 agent.maxSpeed ​​= 50 agent.maxAcceleration = 1000 node! .position = punktnode! .strokeColor = UIColor.clearColor () node! .physicsBody! .contactTestBitMask = 1 self.addChild (node!)

Den viktigste koden vi har lagt til er plassert i hvis uttalelse som følger bytte om uttalelse. La oss gå gjennom denne kodelinjen etter linje:

  • Vi legger først agenten til enheten som en komponent og konfigurerer sin delegat.
  • Deretter tilordner vi agentens posisjon og legger agenten til et lagret array, agenter. Vi legger til denne egenskapen i GameScene i et øyeblikk.
  • Vi lager deretter en GKBehavior objekt med en enkelt GKGoal å målrette mot aktørens nåværende agent. De vekt parameter i denne initialisatoren brukes til å bestemme hvilke mål som skal ha forrang over andre. For eksempel, tenk at du har et mål å målrette mot en bestemt agent og et mål å flytte bort fra en annen agent, men du vil at målrettingsmålet skal foretrekkes. I dette tilfellet kan du gi målmål målet en vekt på 1 og flytting bort målet en vekt på 0.5. Denne oppførselen er da tilordnet til fiendens nodes agent.
  • Til slutt konfigurerer vi massemaksimal hastighet, og maxAcceleration egenskaper av agenten. Disse påvirker hvor raskt objektene kan bevege seg og snu. Føl deg fri til å leke med disse verdiene og se hvordan det påvirker bevegelsen av fiendens prikker.

Deretter legger du til følgende to egenskaper til GameScene klasse:

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

De agenter array vil bli brukt til å holde en referanse til fiendens agenter i scenen. De lastUpdateTime Egenskapen vil bli brukt til å beregne tiden som har gått siden scenen ble sist oppdatert.

Endelig, erstatt implementeringen av Oppdater(_:) metode av GameScene klasse med følgende implementering:

overstyr func-oppdatering (currentTime: CFTimeInterval) / * Kalt før hver ramme gjengis * / self.camera?.position = playerNode.position hvis self.lastUpdateTime == 0 lastUpdateTime = currentTime la delta = currentTime - lastUpdateTime lastUpdateTime = currentTime playerNode.agent.updateWithDeltaTime (delta) for agent i agenter agent.updateWithDeltaTime (delta)

I Oppdater(_:) metode beregner vi tiden som har gått siden siste sceneoppdatering og oppdaterer agenter med den verdien.

Bygg og kjør appen din, og begynn å flytte rundt på scenen. Du vil se at fiendens prikker vil sakte begynne å bevege seg mot deg.

Som du kan se, mens fiendens prikker målretter mot den nåværende spilleren, navigerer de ikke rundt de hvite barrierer, i stedet prøver de å bevege seg gjennom dem. La oss gjøre fiender litt smartere med å finne veien.

2. Pathfinding

Med GameplayKit-rammen kan du legge til komplisert pathfinding til spillet ditt ved å kombinere fysikklegemer med GameplayKit klasser og metoder. For vårt spill skal vi sette det opp slik at fiendens prikker vil målrette spillerens prikk og samtidig navigere rundt hindringer.

Pathfinding i GameplayKit begynner med å skape en kurve av scenen din. Denne grafen er en samling av individuelle steder, også referert til som noder, og forbindelser mellom disse stedene. Disse forbindelsene definerer hvordan en bestemt gjenstand kan bevege seg fra ett sted til et annet. En graf kan modellere de tilgjengelige banene i scenen din på en av tre måter:

  • En kontinuerlig plass som inneholder hindringer: Denne grafmodellen muliggjør jevne baner rundt hindringer fra ett sted til et annet. For denne modellen, den GKObstacleGraph klassen brukes til grafen, den GKPolygonObstacle klasse for hindringer, og GKGraphNode2D klasse for noder (steder).
  • Et enkelt 2D-nett: I dette tilfellet kan gyldige steder bare være de med heltallskoordinater. Denne grafmodellen er nyttig når scenen din har et klart gridoppsett, og du trenger ikke glatte baner. Når du bruker denne modellen, kan objekter bare bevege seg horisontalt eller vertikalt i en enkelt retning til enhver tid. For denne modellen, den GKGridGraph klassen brukes til grafen og GKGridGraphNode klasse for noder.
  • En samling av steder og forbindelsene mellom dem: Dette er den mest generiske grafmodellen, og anbefales for tilfeller der objekter beveger seg mellom forskjellige mellomrom, men deres spesifikke plassering innenfor dette rommet er ikke avgjørende for spillingen. For denne modellen, den GKGraph klassen brukes til grafen og GKGraphNode klasse for noder.

Fordi vi vil at spilleren prikken i spillet vårt for å navigere rundt de hvite barrierene, skal vi bruke GKObstacleGraph klassen for å lage en graf av vår scene. For å begynne, erstatt spawnPoints eiendom i GameScene klasse med følgende:

la 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: 3400), CGPoint (x: 1200, y: 2950), CGPoint (x: 1200, y: 3400), CGPoint (x: 2550, 3100), CGPoint (x: 3000, y: 2400), CGPoint (x: 2048, y: 2400), CGPoint (x: 2200, y: 2200)] var graf: GKObstacleGraph!

De spawnPoints array inneholder noen endrede gyte steder i forbindelse med denne opplæringen. Dette skyldes at GameplayKit for øyeblikket bare kan beregne stier mellom objekter som er relativt nær hverandre.

På grunn av den store standardavstanden mellom prikker i dette spillet må et par nye gytepunkter legges til for å illustrere pathfinding. Legg merke til at vi også erklære en kurve type eiendom GKObstacleGraph for å holde en referanse til grafen vi skal opprette.

Deretter legger du til følgende to kodelinjer ved starten av didMoveToView (_ :) metode:

la hindringer = SKNode.obstaclesFromNodePhysicsBodies (self.children) graph = GKObstacleGraph (hindringer: hindringer, bufferRadius: 0.0)

I første linje lager vi en rekke hindringer fra fysikkorganene i scenen. Vi lager deretter grafobjektet ved hjelp av disse hindringene. De bufferRadius parameter i denne initialisatoren kan brukes til å tvinge objekter til å ikke komme innenfor en viss avstand fra disse hindringene. Disse linjene må legges til ved starten av didMoveToView (_ :) metode, fordi grafen vi lager er nødvendig for tiden den initialSpawn Metoden kalles.

Endelig, erstatt initialSpawn metode med følgende implementering:

func initialSpawn () la endNode = GKGraphNode2D (punkt: float2 (x: 2048.0, y: 2048.0)) self.graph.connectNodeUsingObstacles (endNode) for punkt i self.spawnPoints la respawnFactor = arc4random ()% 3 // Vil produsere en verdi mellom 0 og 2 (inkluderende) var node: SKShapeNode? = null bryter respawnFactor case 0: node = PointsNode (circleOfRadius: 25) node! .physicsBody = SKPhysicsBody (circleOfRadius: 25) node! .fillColor = UIColor.greenColor () tilfelle 1: node = RedEnemyNode (circleOfRadius: 75) node! .physicsBody = SKPhysicsBody (circleOfRadius: 75) node! .fillColor = UIColor.redColor () tilfelle 2: node = YellowEnemyNode (circleOfRadius: 50) node! .physicsBody = SKPhysicsBody (circleOfRadius: 50) node! .fillColor = UIColor.yellowColor ) standard: break hvis la enheten = node? .valueForKey ("entity") som? GKEntity, la agent = node? .ValueForKey ("agent") som? GKAgent2D hvor respawnFactor! = 0 entity.addComponent (agent) agent.delegate = node som? ContactNode agent.position = float2 (x: Float (punkt.x), y: Float (point.y)) agents.append (agent) / * la oppførsel = GKBehavior (mål: GKGoal (toSeekAgent: playerNode.agent), vekt : 1.0) agent.behavior = oppførsel * / / *** BEGIN PATHFINDING *** / la startNode = GKGraphNode2D (punkt: agent.position) self.graph.connectNodeUsingObstacles (startNode) la pathNodes = self.graph.findPathFromNode (startNode, toNode: endNode) som! [GKGraphNode2D] hvis! PathNodes.isEmpty let path = GKPath (graphNodes: pathNodes, radius: 1.0) la følgePath = GKGoal (toFollowPath: path, maxPredictionTime: 1.0, forward: true) la stayOnPath = GKGoal (toStayOnPath: path, maxPredictionTime: 1.0) la oppførsel = GKBehavior (mål: [followPath, stayOnPath]) agent.behavior = oppførsel self.graph.removeNodes ([startNode]) / *** END PATHFINDING *** / agent.mass = 0.01 agent.maxSpeed ​​= 50 agent. Maxx = acceleration 

Vi starter metoden ved å lage en GKGraphNode2D objekt med standard spilleren gyte koordinater. Deretter kobler vi denne noden til grafen slik at den kan brukes når vi finner stier.

Mesteparten av initialSpawn Metoden forblir uendret. Jeg har lagt til noen kommentarer for å vise deg hvor passorddelen av koden er plassert i den første hvis uttalelse. La oss gå gjennom denne koden trinn for trinn:

  • Vi lager en annen GKGraphNode2D forekomst og koble dette til grafen.
  • Vi lager en rekke noder som utgjør en sti ved å ringe findPathFromNode (_: toNode :) metode på grafen vår.
  • Hvis en rekke sti knutepunkter er blitt opprettet, lager vi en sti fra dem. De radius parameteren fungerer i likhet med bufferRadius parameter fra før og definerer hvor mye en gjenstand kan bevege seg bort fra den opprettede banen.
  • Vi lager to GKGoal objekter, en for å følge stien og en annen for å holde seg på banen. De maxPredictionTime parameteren tillater målet å beregne så godt det kan forut for tiden om noe skal forstyrre objektet fra å følge / forbli på den aktuelle banen.
  • Til slutt lager vi en ny oppførsel med disse to målene og tilordner dette til agenten.

Du vil også legge merke til at vi fjerner noder vi lager fra grafen når vi er ferdige med dem. Dette er en god praksis å følge da det sikrer at knutepunktene du har opprettet, ikke forstyrrer andre beregninger av veiviser senere.

Bygg og kjør appen din en gang, og du vil se to prikker gyte deg veldig nær deg og begynne å bevege seg mot deg. Du må kanskje kjøre spillet flere ganger hvis de begge gyter som grønne prikker.

Viktig!

I denne opplæringen brukte vi GameplayKit's pathfinding-funksjon for å aktivere fiendens prikker for å målrette spilleren dot rundt hindringer. Vær oppmerksom på at dette bare var et praktisk eksempel på opplæring.

For et faktisk produksjonsspill, ville det være best å implementere denne funksjonaliteten ved å kombinere målrettet mål for mål fra tidligere i denne opplæringen med et hinder-unngått mål opprettet med init (toAvoidObstacles: maxPredictionTime :) bekvemmelighetsmetode, som du kan lese mer om i GKGoal Klasse referanse.

Konklusjon

I denne veiledningen viste jeg deg hvordan du kan bruke agenter, mål og oppførsel i spill som har en enhetskomponentstruktur. Mens vi bare opprettet tre mål i denne opplæringen, er det mange flere tilgjengelige for deg, som du kan lese mer om i GKGoal Klasse referanse.

Jeg viste deg også hvordan du implementerer noen avanserte pathfinding i spillet ditt ved å lage en graf, et sett med hindringer og mål å følge disse banene.

Som du kan se, er det en stor mengde funksjonalitet som er tilgjengelig for deg gjennom GameplayKit-rammen. I den tredje og siste delen av denne serien vil jeg lære deg om GameplayKits tilfeldige verdi generatorer og hvordan du lager ditt eget regelsystem for å introdusere litt fuzzy logikk inn i spillet ditt.

Som alltid, vær så snill å legge igjen dine kommentarer og tilbakemelding nedenfor.