Een inleiding tot gameplayKit deel 2

Dit is het tweede deel van Een inleiding tot GameplayKit. Als je het eerste deel nog niet hebt doorgenomen, raad ik aan eerst die tutorial te lezen voordat je verdergaat met deze.

Invoering

In deze tutorial ga ik je leren over nog twee andere functies van het GameplayKit-framework waar je gebruik van kunt maken:

  • agenten, doelen en gedrag
  • Padvinden

Door agenten, doelen en gedrag te gebruiken, gaan we wat basis kunstmatige intelligentie (AI) inbouwen in de game die we in het eerste deel van deze serie begonnen. De AI zorgt ervoor dat onze rode en gele vijandige stippen zich kunnen richten en naar onze blauwe spelerpunt kunnen bewegen. We gaan ook padvinden implementeren om op deze AI uit te breiden om obstakels te omzeilen.

Voor deze zelfstudie kunt u uw exemplaar van het voltooide project uit het eerste deel van deze serie gebruiken of een nieuw exemplaar van de broncode downloaden van GitHub.

1. Agenten, doelen en gedrag

In GameplayKit worden agenten, doelen en gedrag in combinatie met elkaar gebruikt om te definiëren hoe verschillende objecten ten opzichte van elkaar in de hele scène bewegen. Voor een enkel object (of SKShapeNode in onze game), begin je met het maken van een middel, vertegenwoordigd door de GKAgent klasse. Voor 2D-spellen, zoals de onze, moeten we echter het beton gebruiken GKAgent2D klasse.

De GKAgent class is een subklasse van GKComponent. Dit betekent dat je game een entiteit- en component-gebaseerde structuur moet gebruiken, zoals ik je in de eerste tutorial van deze serie liet zien.

Agents vertegenwoordigen de positie, grootte en snelheid van een object. Je voegt dan een toe gedrag, vertegenwoordigd door de GKBehaviour klasse, voor deze agent. Eindelijk, creëer je een set van doelen, vertegenwoordigd door de GKGoal klasse en voeg ze toe aan het gedragsobject. Doelen kunnen worden gebruikt om veel verschillende gameplay-elementen te maken, bijvoorbeeld:

  • op weg naar een agent
  • weg van een agent
  • groepering dicht bij elkaar met andere agenten
  • ronddwalen rond een specifieke positie

Uw gedragsobject bewaakt en berekent alle doelen die u eraan toevoegt en relais deze gegevens vervolgens terug naar de agent. Laten we kijken hoe dit in de praktijk werkt.

Open uw Xcode-project en navigeer naar PlayerNode.swift. We moeten eerst zorgen dat het PlayerNode klas voldoet aan de GKAgentDelegate protocol.

class PlayerNode: SKShapeNode, GKAgentDelegate ... 

Voeg vervolgens het volgende codeblok toe aan de PlayerNode klasse.

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) if let agent2D = agent as? GKAgent2D self.position = CGPoint (x: CGFloat (agent2D.position.x), y: CGFloat (agent2D.position.y))

We beginnen met het toevoegen van een eigenschap aan de PlayerNode klasse, zodat we altijd een verwijzing hebben naar het agentobject van de huidige speler. Vervolgens implementeren we de twee methoden van de GKAgentDelegate protocol. Door deze methoden te implementeren, zorgen we ervoor dat de stip van de speler die op het scherm wordt weergegeven, altijd overeenkomt met de wijzigingen die GameplayKit aanbrengt.

De agentWillUpdate (_ :) methode wordt aangeroepen juist voordat GameplayKit door het gedrag en de doelen van die agent kijkt om te bepalen waar het moet worden verplaatst. Evenzo, de agentDidUpdate (_ :) methode wordt meteen genoemd nadat GameplayKit dit proces heeft voltooid.

Onze implementatie van deze twee methoden zorgt ervoor dat het knooppunt dat we op het scherm zien de veranderingen weerspiegelt die GameplayKit maakt en dat GameplayKit de laatste positie van het knooppunt gebruikt bij het uitvoeren van zijn berekeningen.

Open vervolgens ContactNode.swift en vervang de inhoud van het bestand door de volgende implementatie:

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) if let agent2D = agent as? GKAgent2D self.position = CGPoint (x: CGFloat (agent2D.position.x), y: CGFloat (agent2D.position.y))

Door de GKAgentDelegate protocol in de ContactNode klasse, we staan ​​toe dat alle andere stippen in onze game up-to-date zijn met GameplayKit en onze speler-stip.

Het is nu tijd om het gedrag en de doelen in te stellen. Om dit te laten werken, moeten we voor drie dingen zorgen:

  • Voeg de agent van het spelersknooppunt toe aan zijn entiteit en stel zijn gemachtigde in.
  • Configureer agenten, gedragingen en doelen voor al onze vijandelijke stippen.
  • Werk al deze agents op het juiste moment bij.

Ten eerste open GameScene.swift en, aan het einde van de didMoveToView (_ :) methode, voeg de volgende twee regels code toe:

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

Met deze twee regels code voegen we de agent toe als een component en stelt de gemachtigde van de agent in als het knooppunt zelf.

Vervang vervolgens de implementatie van de initialSpawn methode met de volgende implementatie:

func initialSpawn () voor point in self.spawnPoints let respawnFactor = arc4random ()% 3 // Genereert een waarde tussen 0 en 2 (inclusief) var node: SKShapeNode? = nil switch respawnFactor case 0: node = PointsNode (circleOfRadius: 25) node! .physicsBody = SKPhysicsBody (circleOfRadius: 25) node! .fillColor = UIColor.greenColor () case 1: node = RedEnemyNode (circleOfRadius: 75) node! .physicsBody = SKPhysicsBody (circleOfRadius: 75) node! .fillColor = UIColor.redColor () case 2: node = YellowEnemyNode (circleOfRadius: 50) node! .physicsBody = SKPhysicsBody (circleOfRadius: 50) node! .fillColor = UIColor.yellowColor ( ) standaard: onderbreken als entiteit = knooppunt? .valueForKey ("entiteit") als? GKEntity, let agent = node? .ValueForKey ("agent") as? GKAgent2D waarbij respawnFactor! = 0 entity.addComponent (agent) agent.delegate = node als? ContactNode agent.position = float2 (x: Float (point.x), y: Float (point.y)) agents.append (agent) laat gedrag = GKBehavior (doel: GKGoal (toSeekAgent: playerNode.agent), gewicht: 1.0 ) agent.behavior = behaviour agent.mass = 0.01 agent.maxSpeed ​​= 50 agent.maxAcceleration = 1000 node! .position = point node! .strokeColor = UIColor.clearColor () node! .physicsBody! .contactTestBitMask = 1 self.addChild (knooppunt!)

De belangrijkste code die we hebben toegevoegd, bevindt zich in de als verklaring die volgt op de schakelaar uitspraak. Laten we deze code regel voor regel doorlopen:

  • We voegen eerst de agent toe aan de entiteit als een component en configureren zijn gemachtigde.
  • Vervolgens wijzen we de positie van de agent toe en voegen we de agent toe aan een opgeslagen array, agenten. We zullen deze eigenschap toevoegen aan de GameScene les in een moment.
  • We maken vervolgens een GKBehavior object met een single GKGoal om de agent van de huidige speler te targeten. De gewicht parameter in deze initialisator wordt gebruikt om te bepalen welke doelen voorrang op andere moeten krijgen. Stel je bijvoorbeeld voor dat je een doel hebt om een ​​bepaalde agent te targeten en een doel om van een andere agent af te wijken, maar je wilt dat het targetingdoel de voorkeur krijgt. In dit geval zou u het targetingdoel een gewicht van kunnen geven 1 en het verplaatsende doel een gewicht van 0.5. Dit gedrag wordt vervolgens toegewezen aan de agent van het vijandelijke knooppunt.
  • Ten slotte configureren we de massa-maximale snelheid, en maxAcceleration eigenschappen van de agent. Deze hebben invloed op hoe snel de objecten kunnen bewegen en keren. Voel je vrij om met deze waarden te spelen en zie hoe het de beweging van de vijandelijke stippen beïnvloedt.

Voeg vervolgens de volgende twee eigenschappen toe aan de GameScene klasse:

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

De agenten array wordt gebruikt om een ​​verwijzing naar de vijandelijke agenten in de scène te houden. De lastUpdateTime property wordt gebruikt om de tijd te berekenen die is verstreken sinds de scène voor het laatst is bijgewerkt.

Ten slotte, vervang de implementatie van de bijwerken(_:) methode van de GameScene klasse met de volgende implementatie:

override func update (currentTime: CFTimeInterval) / * Bel voor elk frame wordt weergegeven * / self.camera?.position = playerNode.position als self.lastUpdateTime == 0 lastUpdateTime = currentTime laat delta = currentTime - lastUpdateTime lastUpdateTime = currentTime playerNode.agent.updateWithDeltaTime (delta) voor agent in agents agent.updateWithDeltaTime (delta)

In de bijwerken(_:) methode berekenen we de tijd die is verstreken sinds de laatste scène-update en werken de agenten vervolgens bij met die waarde.

Bouw je app en voer deze uit, en begin je rond te bewegen. Je zult zien dat de vijandelijke stippen langzaam naar je toe zullen bewegen.

Zoals je kunt zien, terwijl de vijandige stippen de huidige speler targeten, navigeren ze niet rond de witte barrières, maar proberen ze er doorheen te bewegen. Laten we de vijanden een beetje slimmer maken met pathfinding.

2. Pathfinding

Met het GameplayKit-framework kun je complexe padvinden aan je spel toevoegen door physics bodies te combineren met gameplayKit-klassen en -methoden. Voor ons spel gaan we het zo instellen dat de vijandelijke stippen op de punt van de speler gericht zijn en tegelijkertijd rond obstakels navigeren.

Pathfinding in GameplayKit begint met het maken van een diagram van je scène. Deze grafiek is een verzameling afzonderlijke locaties, ook wel aangeduid als nodes, en verbindingen tussen deze locaties. Deze verbindingen bepalen hoe een bepaald object van de ene locatie naar de andere kan gaan. Een grafiek kan de beschikbare paden in uw scène op drie manieren modelleren:

  • Een doorlopende ruimte met obstakels: Dit grafiekmodel zorgt voor vloeiende paden rond obstakels van de ene locatie naar de andere. Voor dit model, de GKObstacleGraph klasse wordt gebruikt voor de grafiek, de GKPolygonObstacle klasse voor obstakels, en de GKGraphNode2D klasse voor knooppunten (locaties).
  • Een eenvoudig 2D-raster: In dit geval kunnen geldige locaties alleen die met integer coördinaten zijn. Dit grafiekmodel is handig wanneer uw scène een andere rasterindeling heeft en u geen vloeiende paden nodig hebt. Bij gebruik van dit model kunnen objecten alleen horizontaal of verticaal in één richting tegelijk bewegen. Voor dit model, de GKGridGraph klasse wordt gebruikt voor de grafiek en de GKGridGraphNode klasse voor knooppunten.
  • Een verzameling locaties en de onderlinge verbindingen: Dit is het meest generieke grafiekmodel en wordt aanbevolen voor gevallen waarin objecten tussen verschillende spaties bewegen, maar hun specifieke locatie binnen die ruimte is niet essentieel voor het spelen van het spel. Voor dit model, de GKGraph klasse wordt gebruikt voor de grafiek en de GKGraphNode klasse voor knooppunten.

Omdat we willen dat de punt van de speler in ons spel rond de witte barrières navigeert, gaan we de GKObstacleGraph klas om een ​​grafiek van onze scène te maken. Vervang om te beginnen de spawnPoints eigendom in de GameScene les met het volgende:

laat 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 graph: GKObstacleGraph!

De spawnPoints array bevat enkele gewijzigde spawn-locaties ten behoeve van deze tutorial. Dit komt omdat GameplayKit momenteel alleen paden kan berekenen tussen objecten die relatief dicht bij elkaar staan.

Vanwege de grote standaardafstand tussen de stippen in dit spel, moeten een aantal nieuwe spawn-punten worden toegevoegd om padvervaging te illustreren. Merk op dat we ook a diagram eigendom van het type GKObstacleGraph om een ​​verwijzing naar de grafiek te behouden die we zullen maken.

Voeg vervolgens de volgende twee regels code toe aan het begin van de didMoveToView (_ :) methode:

laat obstakels = SKNode.obstaclesFromNodePhysicsLichamen (self.children) graph = GKObstacleGraph (obstakels: obstakels, bufferRadius: 0.0)

In de eerste regel creëren we een reeks obstakels van de fysische lichamen in de scène. Vervolgens maken we het grafiekobject met behulp van deze obstakels. De bufferRadius parameter in deze initialisator kan worden gebruikt om objecten te dwingen niet binnen een bepaalde afstand van deze obstakels te komen. Deze regels moeten aan het begin van de didMoveToView (_ :) methode, omdat de grafiek die we maken, nodig is tegen de tijd dat de initialSpawn methode wordt genoemd.

Vervang tot slot de initialSpawn methode met de volgende implementatie:

func initialSpawn () let endNode = GKGraphNode2D (punt: float2 (x: 2048.0, y: 2048.0)) self.graph.connectNodeUsingObstacles (endNode) voor punt in self.spawnPoints let respawnFactor = arc4random ()% 3 // Zal produceren een waarde tussen 0 en 2 (inclusief) var node: SKShapeNode? = nil switch respawnFactor case 0: node = PointsNode (circleOfRadius: 25) node! .physicsBody = SKPhysicsBody (circleOfRadius: 25) node! .fillColor = UIColor.greenColor () case 1: node = RedEnemyNode (circleOfRadius: 75) node! .physicsBody = SKPhysicsBody (circleOfRadius: 75) node! .fillColor = UIColor.redColor () case 2: node = YellowEnemyNode (circleOfRadius: 50) node! .physicsBody = SKPhysicsBody (circleOfRadius: 50) node! .fillColor = UIColor.yellowColor ( ) standaard: onderbreken als entiteit = knooppunt? .valueForKey ("entiteit") als? GKEntity, let agent = node? .ValueForKey ("agent") as? GKAgent2D waarbij respawnFactor! = 0 entity.addComponent (agent) agent.delegate = node als? ContactNode agent.position = float2 (x: Float (point.x), y: Float (point.y)) agents.append (agent) / * let behavion = GKBehavior (doel: GKGoal (toSeekAgent: playerNode.agent), weight : 1.0) agent.behavior = behaviour * / / *** BEGIN PATHFINDING *** / laat startNode = GKGraphNode2D (point: agent.position) self.graph.connectNodeUsingObstacles (startNode) laat pathNodes = self.graph.findPathFromNode (startNode, toNode: endNode) as! [GKGraphNode2D] if! PathNodes.isEmpty let path = GKPath (graphNodes: pathNodes, radius: 1.0) let followPath = GKGoal (toFollowPath: path, maxPredictionTime: 1.0, forward: true) let stayOnPath = GKGoal (toStayOnPath: path, maxPredictionTime: 1.0) laat gedrag = GKBehavior (doelen: [followPath, stayOnPath]) agent.behavior = behaviour self.graph.removeNodes ([startNode]) / *** END PATHFINDING *** / agent.mass = 0.01 agent.maxSpeed ​​= 50 agent.maxAcceleration = 1000 node! .Position = puntknooppunt! .StrokeColor = UIColor.clearColor () node! .PhysicsBody! .ContactTestBitMask = 1 self.addChild (node!) Self.graph.removeNodes ([endNode]) 

We beginnen de methode door een GKGraphNode2D object met de standaard spawn-coördinaten van de speler. Vervolgens verbinden we dit knooppunt met de grafiek zodat het kan worden gebruikt bij het vinden van paden.

Meeste van de initialSpawn methode blijft ongewijzigd. Ik heb enkele opmerkingen toegevoegd om u te laten zien waar het padvindende gedeelte van de code zich in de eerste bevindt als uitspraak. Laten we deze code stap voor stap doorlopen:

  • We creëren een andere GKGraphNode2D exemplaar en verbind dit met de grafiek.
  • We maken een reeks knooppunten die een pad vormen door de findPathFromNode (_: toNode :) methode op onze grafiek.
  • Als een reeks padknooppunten met succes is gemaakt, maken we vervolgens een pad vanaf deze knooppunten. De radius parameter werkt vergelijkbaar met de bufferRadius parameter van voor en definieert hoeveel een object zich van het gemaakte pad kan verwijderen.
  • We creëren er twee GKGoal objecten, een voor het volgen van het pad en een ander voor het blijven op het pad. De maxPredictionTime parameter stelt het doel in staat zo snel mogelijk te berekenen of iets het object zal onderbreken van het volgen / blijven op dat specifieke pad.
  • Ten slotte creëren we een nieuw gedrag met deze twee doelen en wijzen we dit toe aan de agent.

Je zult ook opmerken dat we de knooppunten die we creëren verwijderen uit de grafiek zodra we klaar zijn met hen. Dit is een goede methode om te volgen, omdat het ervoor zorgt dat de knooppunten die u hebt gemaakt, later geen invloed hebben op andere routeverkenningen.

Bouw en run je app een laatste keer en je ziet twee puntjes spawnen heel dicht bij je en begin naar je toe te bewegen. Mogelijk moet je het spel meerdere keren uitvoeren als ze allebei spawnen als groene stippen.

Belangrijk!

In deze tutorial gebruikten we de functie Pathfinding van GameplayKit om vijandige stippen in staat te stellen om de spelerpunt rond obstakels te richten. Merk op dat dit alleen voor een praktisch voorbeeld van pathfinding was.

Voor een echte productiegame zou het het beste zijn om deze functionaliteit te implementeren door het targetdoel van de speler van eerder in deze tutorial te combineren met een obstakel-ontwijkend doel gemaakt met de init (toAvoidObstacles: maxPredictionTime :) gemaksmethode, waarover u meer kunt lezen in de GKGoal Klasse Referentie.

Conclusie

In deze zelfstudie heb ik je laten zien hoe je agenten, doelen en gedrag kunt gebruiken in games die een entiteit-componentstructuur hebben. Hoewel we in deze zelfstudie slechts drie doelen hebben gemaakt, zijn er veel meer voor u beschikbaar, waarover u meer kunt lezen in de GKGoal Klasse Referentie.

Ik heb je ook laten zien hoe je een aantal geavanceerde routeplanning in je spel kunt implementeren door een grafiek, een reeks obstakels en doelen te maken om deze paden te volgen.

Zoals je ziet, is er een enorme hoeveelheid functionaliteit beschikbaar gemaakt via het GameplayKit-framework. In het derde en laatste deel van deze serie zal ik je leren over de random value-generators van GameplayKit en hoe je je eigen regelsysteem kunt maken om wat fuzzy logic in je spel te introduceren.

Laat zoals altijd uw opmerkingen en feedback hieronder achter.