Schneller spielen!

Ich weiß ja nicht, ob Sie sich für Spieleprogrammierung interessieren und falls ja, ob Sie meinen Ausführungen zu SpriteKit gefolgt sind. So Sie jetzt zweimal laut „Ja“ gerufen haben, bin ich mir allerdings recht sicher, dass das zweite ein bisschen tiefer und gedämpfter klang. SpriteKit ist eine wirklich leistungsfähige, einfach zu bedienende 2D-Game Engine, aber mal ehrlich: Die Scenes und Sprites per Code zusammenschrauben – da verliert man schnell die Lust an der Sache, oder? Ein RAD, also ein Schnellentwicklungssystem wie Xojo sollte da radiger sein.

Wenn man einen Editor hätte, in dem das Leveldesign per Drag & Drop erfolgt, und dann nur die Feinheiten der Spiellogik in gewohnter Xojo-Einfachheit dazuprogrammieren müsste, stünden die Chancen doch einiges besser für mehr Xojo-angetriebene Spiele, zumindest auf den Mac-Plattformen.

Ich weiß, ich weiß: So langsam kennen Sie meine Billig-Rhetorik und wissen, was nun folgt, n’est-ce pas? Eine kurze Anleitung nämlich zum Einbinden von in XCode zusammengeklickten Scenes und damit der Weg zum Game Design in Windeseile. Sind Sie dabei? Prima, ich freu mich!

Kleiner Kniefall zu Beginn

Wir gehen nicht nur in Sachen Editor wildern: Das verwendete Material stammt aus einem Tutorial von Ray Wenderlich-Teammitglied Morten Farkroog. Erneut meinen allerherzlichsten Dank an Ray für die Erlaubnis, mich so bedienen zu dürfen. Wenn auch an anderer Stelle schon geschrieben: Unter raywenderlich.com finden Sie großartige Tutorials insbesondere für Apple- und Spieleentwickler. Wenn Sie sich für die Materie interessieren: Bis einschließlich 28.11.2016 gibt es dort PDFs und Videos um 50% rabattiert, das Schwarze Wochenende waltet wieder. Lohnen sich aber auch zum Vollpreis!

(Dass Sie im gleichen Zeitraum sämtliche Xojo-Lizenzen und -Zusätze 20% günstiger bekommen, wissen Sie ja ohnehin, oder?)

Klicken Sie doch bitte einmal auf den ersten Link. Wenn’s Ihnen gerade noch nicht unter den Nägeln brennt, in medias res zu gehen, dann studieren Sie das dortige Scene Editor-Tutorial gerne auch eingehend. Morten stellt hier XCode’s Scene Editor vor, den Bestandteil von XCode, mit dem sich SpriteKit-Scenes recht komfortabel zusammenklicken lassen.

Steht Ihnen der Sinn lieber nach schnellen Resultaten, können Sie das alles auch erst einmal ignorieren (aber holen Sie’s später nach!) und zu den Kommentaren am Ende des Beitrags scrollen. Dort finden Sie einen Beitrag von kvebeeck vom 3. September, der so freundlich war, die XCode-Projektdaten zusammenzutragen und zu aktualisieren, nämlich einmal als jungfräuliches Projekt und einmal fertiggestellt. Klicken Sie auf den 2. Link und laden Sie die Projektdatei herunter. Das Ergebnis sollte etwa so aussehen:

fear-the-dead-finder

Das ist der Inhalt des XCode-Ressourcenordners, den Sie im Fear The Dead – Final-Verzeichnis finden. Ganz ähnlich wie bei Xojo-Projekten finden Sie in einem XCode-Projekt nicht nur den Programmcode, sondern auch alle sonstigen Ressourcen. Unter anderem die Soundfiles (auf mp3 und wav endend), die Scene Editor-Scenes mit der Endung sks und die Bilddaten, ordentlich in einem Ordner abgelegt.

Das Original-Tutorial ist für iOS ausgelegt. Ich habe Ihnen aber versprochen, dass Sie plattformübergreifend verspielt sein können. Erstellen Sie daher doch bitte ein neues Xojo-Desktopprojekt, laden AppleLib (über den grünen Button „Clone/Download“) und öffnen das MacLib-Projekt. Kopieren Sie aus Xojos Navigator den Ordner „OSXLib“ in Ihr neues Projekt. Wenn Sie mögen, können Sie einige Bestandteile der  Ordner „Removable Contents“ aus „OSXLib“ und „Optionals“ aus „Shared Files for both libraries“ löschen, allerdings nicht die Verzeichnisse „SpriteKit“ und „AVFoundation“ aus letzterem und die „Responder controls“ aus den „custom controls“ von ersterem. Der Rest von AppleLib wird für dieses Projekt nicht benötigt. Wird das zu hakelig (ich muss mir einige Querreferenzen noch mal genauer anschauen), lassen Sie ruhig die ganze Lib drin. Was nicht benötigt wird, landet automatisch nicht im Build.

Und nun nehmen Sie die oben grün markierten Projektbestandteile und ziehen Sie aus dem Finder in Ihr neues Projekt. Ich habe den ImageAssets-Ordner einfach beibehalten und das AppIconSet gelöscht, Scenes und Sounds dafür in eigene Ordner im Navigator gelegt, der Übersicht halber.

Das Ergebnis sollte also in etwa so aussehen:

navigator-nach-import

Window1 wird nun das FearTheDeadWindow: Benennen Sie’s um, wenn Sie mögen, und achten Sie drauf, dass Maximize- und FullScreen-Button im Inspector True sind. Ziehen Sie nun aus der Library ein OSXLibSKView auf das Fenster, ziehen Sie es fensterinhaltgroß auf und setzen Sie alle vier Lock-Symbole im Inspector, um es stets fenstergroß zu halten.

Ein OSXLibSKView ist der Canvas-Ersatz, in dem SpriteKit-Inhalte dargestellt werden. Momentan habe ich noch nicht viele Features nach außen geführt, um es ganz Xojo-like zu machen. Die meisten Funktion im Folgenden werden daher sein AppleObject As AppleSKView ansprechen – das ist das eigentliche SKView, um den herum der Xojo-Wrapper gebastelt ist. Später sollen die meisten Funktionen auch ohne diesen Umweg erreichbar sein – aber das ist die Kür, nicht die Pflicht, und besteht aus endlosem Erneut-Tippen schon vorhandener Funktionen für mich, was ich zugunsten weiteren Debuggings erst einmal nach hinten stelle.

SKView präsentiert: die Szene!

Wie alle Canvas-Abkömmlinge (oder, technisch genauer, NSView-Subklassen) in AppleLib besitzt das OSXLibSKView einen Shown-Event. Der setzt ein bisschen später an als der Open-Event, nämlich dann, wenn derdiedas View Bestandteil des Fensters wurde. Hier sind also alle Parameter gesetzt, und wir können mit den View-Properties operieren. Das sollten wir auch, nämlich auf folgende Art:

Sub Shown() Handles Shown
   Dim GameScene As New AppleSKScene("GameScene")
   GameScene.Name = "GameScene"
   me.AppleObject.Present GameScene
End Sub

Stellen Sie die Build-Settings für OS X im Xojo-Navigator auf 64 Bit (unter MacOS ist 64 Bit Voraussetzung für SpriteKit. Unter iOS läuft es auch auf 32 Bit-Systemen) und klicken Sie auf Build. Wenn Sie das Programm dann ausführen, sollten Sie den Protagonisten des Spiels sehen, auf den mit einem dramatischen Zoom fokussiert wird, und die Hintergrundmusik ertönt – das Ganze sieht etwa so aus:

intro-small
Der Held der Geschichte. Bild- & Tonmaterial sowie Idee ©raywenderlich.com. Mit freundlicher Genehmigung.

Weiter passiert noch nichts – wir haben ja noch keinen Code für die Steuerung eingeben, und die Zombies leben auch noch nicht. Also noch nicht mal im untoten Sinn.

Für sehr, sehr wenig Programmieraufwand aber schon ganz schön viel Spielinhalt, oder? Es ist sogar mehr da, als Sie im Moment sehen können. Und das liegt an der Struktur einer SKScene, die sich kurz unter die Lupe zu nehmen lohnt:

Von Bäumen und Blättern

Kernobjekt aller Dinge, die sich in SpriteKit tummeln, ist eine SKNode – ein SpriteKitKnoten also. Solche Knoten können geometrische Objekte sein, Texte, Sprites, Emitter, Effekte – also alles, was am Ende zu sehen ist –, aber auch virtuelle Objekte wie Kameras, Klangquellen und Zuhörer. Knoten sind es, da sie sich zu Baumstrukturen zusammensetzen lassen. So ist an den Abenteurer oben eine SKLightNode angekoppelt, die sich automatisch mit der Spielfigur bewegt und die nähere Umgebung ausleuchtet. Außerdem kann eine SKNode als ListenerNode deklariert werden, um räumliche Klangeffekte zu erzeugen: Grunzt später ein Zombie von links, hören Sie das auch von der linken Seite. Aus Nodes lassen sich beliebige Baumstrukturen erzeugen, mit jeder Menge Verzweigungen und Verästelungen, ganz wie’s beliebt. Damit sind dann kinetische Modelle möglich, die sich anatomisch korrekt aussehend verhalten, und wenn Sie das XCode-Projekt einmal im Scene Editor betrachten, sehen Sie, dass dort aus mehreren Wänden ein Raum als eigenständiges SKS-File gespeichert und mehrfach in der GameScene verwendet wurde. Ganz schön komfortabel!

Nicht zu vergessen: Optional kann einer SKNode auch befohlen werden, einen SKPhysicsBody zu tragen, einen physikalischen Körper, der es ermöglicht, die Node der Wirkung diverser physikalischer Kräfte auszusetzen, und den man benötigt, um Kollisionen mit anderen Nodes abfragen zu können.

Ebensowenig zu vergessen: Auch eine SKScene ist nur eine Node – der Grundknoten der ganzen Scene also. Alle Objekte, die dort auftauchen sollen, müssen als Kind-Node an die Hierarchie angehängt werden. Das kann man per Code erledigen – davon künden ja die alten SpriteKit-Bewerbungen hier –, man kann’s aber auch bequem in XCodes Scene Editor zusammenschrauben, und das ist hier passiert.

Mit Dim GameScene As New AppleSKScene(„GameScene“) wurde diese Szene aus dem Projektbundle geladen – den Namen hat sie in Xcode bekommen. Und mittels OSXLibView.AppleObject.Present teilt man einem SKView mit, die Szene abzuspielen.

Theoretisch können wir auf alle Bestandteile der Scene im Folgenden zugreifen, indem wir einfach die Nodes suchen lassen, die einen bestimmten Namen tragen. Um dem Swift-Beispielprojekt zu folgen, gönnen wir dem FearTheDeadWindow aber ein paar Properties:

GameScene As AppleSKScene – ein Puffer für den schnellen Zugriff auf die Spielszene

Goal As AppleSKSpriteNode – das SpriteNode mit dem Ausgangstor

Pi As Double = 3.141592653589793 – benötigen wir für die Rotationsberechnungen

Player As AppleSKSpriteNode – das Sprite, das die Spielfigur repräsentiert

PlayerSpeed As Double = 150 – die Bewegungsgeschwindigkeit des Spielers

PlayerStartPos As Foundationframework.NSPoint – ein Speicher für die Startposition der Spielfigur

Zombies() As AppleSKSpriteNode – ein Array für die Zombie-Sprites

ZombieSpeed As Double = 75 – die Bewegungsgeschwindigkeit der Zombies.

ZombieStartPositions() As Foundationframework.NSPoint – Array analog PlayerStartPos für die Startpositionen der Zombies

Ersetzen Sie, um dieses Properties zu nutzen, den Shown-Event des SKViews daher mit

Sub Shown() Handles Shown
   GameScene = new AppleSKScene("GameScene")
   GameScene.Name = "GameSCene"
   me.AppleObject.Present GameScene

   // PlayerNode suchen, es zum Hörer machen, Startposition sichern:
   player = new AppleSKSpriteNode(me.AppleObject.Scene.ChildNode("player").id)
   me.AppleObject.Scene.Listener = player
   PlayerStartPos = player.Position

   // Jetzt alle Zombies suchen
   for q as integer = 0 to me.AppleObject.Scene.children.Count -1
      dim child as new AppleSKNode (me.AppleObject.Scene.Children.PtrAtIndex(q))
      if child.name = "zombie" then
         dim childnode as new AppleSKSpriteNode (child.id) //Spritenode darauf casten
         ZombieStartPositions.Append childnode.Position
         // AudioNode ranhängen:
         Dim audioNode as new AppleSKAudioNode ( "fear_moan.wav")
         Childnode.addChild(audioNode)
         // Zombie ins Array:
         zombies.append(childnode)
      end if
   next

   // Und noch das Ausgangstor sichern:
   goal = new AppleSKSpriteNode(me.AppleObject.Scene.ChildNode("goal").id)
End Sub

Damit stehen die wichtigen Sprites und ihre Startpositionen zum Schnellzugriff parat, und die Zombies grunzen, womit sie Hinweise auf ihre Position geben. Kompilieren Sie das Projekt ruhig noch einmal und schauen Sie sich’s an.

Runde Sache

Fehlt noch die Steuerung. Die können wir an mehreren Stellen sinnvoll in den SKView einklinken. Jede Scene durchläuft – im Normal- bzw. Idealfall – 60 mal in der Sekunde Ihren Renderzyklus. Dabei werden verschiedene Events ausgelöst, nämlich in Reihenfolge:

UpdateScene – am Beginn eines Renderzyklus

DidEvaluateActions – die an die Nodes angehängten SKActions wurden bearbeitet, also etwa Bewegung, Rotation, Sound …

DidSimulatePhysics – Physikalische Einflüsse und Kollisionen sind berechnet.

DidApplyConstraints – SKConstraints, die ähnlich wie bei iOS’ LayoutConstraints bestimmte Berechnungen bezüglich der relativen Position von Nodes zueinander beinhalten, sind nun „drin“.

FinishedSceneUpdate – alles fertig, das Ergebnis wird (wenn kein Code in diesem Eventhandler steht) nun ausgegeben.

Morten hat sich für den DidSimulatePhysics-Event entschieden, und deshalb nehmen wir den auch. Füllen Sie diesen Eventhandler also bitte mit

UpdatePlayer
UpdateZombies

Übrigens: Wenn Sie sich Apples Dokumentation durchlesen, werden Sie feststellen, dass diese Events eigentlich Teil der Scene sind. Aus Bequemlichkeitsgründen lenke ich die Events der präsentierten Szene auf den SKView um, desgleichen die Kollisions-Events der beteiligten PhysicsBodies.

Die beiden Methoden zum Update:

Protected Sub UpdatePlayer()
   Dim deltax, deltay as Double
   if Keyboard.AsyncKeyDown(&h7B) then deltax = -1
   if Keyboard.AsyncKeyDown(&h7C) then deltax = 1
   if Keyboard.AsyncKeyDown(&h7D) then deltay = -1
   if Keyboard.AsyncKeyDown(&h7E) then deltay = 1
   if deltax = 0 and deltay = 0 then
      Player.PhysicsBody.Resting = true
      return
   end if

Hier werden die Cursortasten abgefragt. Ist keine gedrückt, wird der Sprite angehalten (Resting = True) und die Methode wird beendet. Andererseits geht’s weiter:

   Dim currentPosition As FoundationFrameWork.NSPoint = player.position
   Dim nextposition As FoundationFrameWork.NSPoint = FoundationFrameWork.NSMakePoint( _
     currentPosition.x + deltax, currentPosition.y + deltay)
   // Die neue Position wurde bestimmt. Nun den Winkel von der jetzigen zur neuen berechnen:
   Dim angle as double = atan2(currentPosition.y - nextposition.y, _
      currentPosition.x - nextposition.x) + PI
   // und den Sprite in diese Richtung rotieren:
   Dim rotateAction as AppleSKAction = appleSKAction.RotateToAngle(angle + PI*0.5, 0)
   Player.RunAction rotateAction
   // X- und Y-Geschwindigkeit berechnen:
   dim velocityX as double = playerSpeed * cos(angle)
   dim velocityY as double = playerSpeed * sin(angle)
   // und dem Physicsbody des Sprites diese Geschwindigkeit geben:
   dim newVelocity as FoundationFrameWork.CGVector = FoundationFrameWork.CGMakeVector (velocityX, velocityY)
   player.physicsBody.velocity = newVelocity
   // Die Kamera soll immer über dem Sprite sein, daher:
   UpdateCamera
End Sub

UpdateZombies macht annähernd dasselbe, nur werden hier keine Tasten abgefragt. Der Zombie orientiert sich vielmehr an der Spielerposition:

Protected Sub UpdateZombies()
   dim targetPosition as FoundationFrameWork.NSPoint = player.position
   for each zombie as AppleSKSpriteNode in zombies
      dim currentPosition as FoundationFrameWork.NSPoint = zombie.position
      dim angle as double = atan2(currentPosition.y - targetPosition.y, currentPosition.x - targetPosition.x) + pi
      dim rotateAction as AppleSKAction = appleSKAction.RotatetoAngle(angle + PI*0.5, 0)
      zombie.RunAction(rotateAction)

      dim velocityX as double = zombieSpeed * cos(angle)
      dim velocityY as double = zombieSpeed * sin(angle)

      dim newVelocity as FoundationFrameWork.CGVector = FoundationFrameWork.cgmakevector ( velocityX, velocityY)
      zombie.physicsBody.velocity = newVelocity
   next
End Sub

Ihnen ist vielleicht aufgefallen, dass kein Sterbenscödchen gefallen ist hinsichtlich der Kollision mit Wänden. Darum kümmert sich die Physikengine schon ganz alleine – zwei (Physik)Körper können nicht zugleich den gleichen Raum einnehmen, erinnern Sie sich? Da die Wände fixiert sind, können sie nicht verschoben werden – also gehen weder Zombies noch Spieler durch die Wände. Es sei denn natürlich, Sie mögen das Spiel um Geister erweitern …

Wenn der Spieler eine neue Position bekommen hat, soll ja die Kamera folgen. Also auch noch dieser Code:

Protected Sub UpdateCamera()
  me.OSXLibSKView1.AppleObject.Scene.Camera.Position = player.Position
End Sub

Mal wieder Zeit zum Testen! Kompilieren Sie das Programm erneut, starten Sie’s und:

zombie-push

Schon ein Stückchen gruseliger, oder? Die Spielfigur wird jetzt gejagt. Allerdings ist das Ganze momentan noch ein inverses Seilziehen – es passiert nichts weiter, wenn Sie mit einem Zombie (oder dem Tor) kollidieren. Sie können sich auf ein gefahrloses Kräftemessen mit den Untoten einlassen.

Falls nach jedem Tastendruck ein Fehlergeräusch ertönt, dann ist die Property AcceptFocus des OSXLibSKView False. Stellen Sie sie auf True – keine Angst, es erscheint deswegen kein FocusRing –, und das Problem sollte Geschichte sein.

Was fehlt uns noch? Richtig, die

Kollisionen

Wie oben schon geschrieben: Kollisionen aus der präsentierten Scene – also streng genommen die ContactDelegate-Methoden der SKPhysicsWorld der Scene, falls Sie’s nachschlagen wollen – werden automatisch am SKView in Form von Events ausgegeben. Zeit also, dem View einen weiteren Event-Handler zu verpassen:

Sub ContactBegan(Contact As AppleSKPhysicsContact) Handles ContactBegan
  Dim firstBody, secondBody  as appleSKPhysicsBody

  // Den Körper mit der niedrigeren Kategorie zuerst betrachten:
  if contact.bodyA.categoryBitMask < contact.bodyB.categoryBitMask then
    firstBody = contact.bodyA
    secondBody = contact.bodyB
  else
    firstBody = contact.bodyB
    secondBody = contact.bodyA
  end if

  // und jetzt die Auswertung:
  if firstBody.categoryBitMask = player.physicsBody.categoryBitMask and _
    secondBody.categoryBitMask = zombies(0).physicsBody.categoryBitMask then
    // Player & Zombie
    gameOver(false)
  elseif firstBody.categoryBitMask = player.physicsBody.categoryBitMask and _
    secondBody.categoryBitMask = goal.physicsBody.categoryBitMask then
    // Player & Goal
    gameOver(true)
  end if
End Sub

Aufgrund der CollisionBitMasks, die im Scene Editor festgelegt wurden, gibt es nur zwei mögliche Arten von Kollisionen: Spieler mit Zombie und Spieler mit Ziel. Entsprechend wird die noch anzulegende Methode GameOver mit einer Booleschen Variablen für die erfolgreiche Flucht aufgerufen:

Protected Sub GameOver(didWin as Boolean)
  GameScene.Paused = true
  Dim menuScene as new AppleSKScene (OSXLibSKView1.AppleObject.FrameSize)
  menuScene.ScaleMode = AppleSKScene.SKSceneScaleMode.FillProportional
  menuScene.BackgroundColor = AppleColor.BlackColor
  dim label as new AppleSKLabelNode("Press Space to play again!")
  label.fontName = "AvenirNext-Bold"
  label.fontSize = 55
  label.fontColor = AppleColor.WhiteColor
  label.position = menuScene.Frame.center
  menuScene.AddChild label

  dim Soundaction as  AppleSKAction
  if didWin then
    Soundaction = AppleSKAction.PlaySound("fear_win")
  else
    Soundaction = AppleSKAction.PlaySound("fear_lose")
  end if

  OSXLibSKView1.AppleObject.PresentWithTransition menuScene, AppleSKTransition.FlipVertical(1)
  menuScene.RunAction Soundaction
End Sub

Hier wird die Spielszene angehalten und eine neue Scene erzeugt, diesmal mit den Dimensionen des OSXLibSKViews. Auf schwarzem Hintergrund wird ein weißer Text eingeblendet, und je nach Sieg oder Niederlage eine andere Melodie gespielt.

Um die Weitermachbedingung abzufragen, ersetzen Sie bitte den DidSimulatePhysics-Eventhandler durch

Sub DidSimulatePhysics(Scene as AppleSKScene) Handles DidSimulatePhysics
  if scene.Name = "GameScene" and not scene.Paused then
    UpdatePlayer
    UpdateZombies
  else
    if Keyboard.AsyncKeyDown(&h31) then
      ResetPositions
      me.AppleObject.PresentWithTransition GameScene, AppleSKTransition.Doorway(1)
      GameScene.Paused = false
    end if
  end if
End Sub

Jetzt läuft die Spielroutine also nur, wenn auch die GameScene angezeigt wird. Andernfalls dreht sich das Spiel in der Abfrageschleife, bis Space gedrückt wird, und stellt dann die Originalpositionen wieder her, blendet zur Spielszene um und macht dann weiter.
Fehlt noch eine allerletzte Methode, und zwar

Protected Sub ResetPositions()
  for q as integer = 0 to ZombieStartPositions.Ubound -1
    Zombies(q).Position = ZombieStartPositions(q)
  next
  Player.Position = PlayerStartPos
  UpdateCamera
End Sub

Wenn Sie dann dem Fenster noch einen weiteren Eventhandler verpassen mögen, …

Sub Open() Handles Open
  me.FullScreen = true
End Sub

… startet es fortan im Vollbild und wirkt schon ganz schön wie ein ausgewachsenes Spiel.

Damit haben wir das Demo-Programm vollständig nachgebildet. Wenn Sie von hier ein wenig weiterspielen wollen:
Wie wäre es mit Animationen, (Wurf)waffen, Schatzkisten, individuell agierenden Zombies, verschachtelteren Levels, schneller werdenden Gegnern …?

Falls es irgendwo Probleme gab: Hier ist das Projekt mit abgemagerter OSXLib zum Download.

Kommentar verfassen

Trage deine Daten unten ein oder klicke ein Icon um dich einzuloggen:

WordPress.com-Logo

Du kommentierst mit Deinem WordPress.com-Konto. Abmelden / Ändern )

Twitter-Bild

Du kommentierst mit Deinem Twitter-Konto. Abmelden / Ändern )

Facebook-Foto

Du kommentierst mit Deinem Facebook-Konto. Abmelden / Ändern )

Google+ Foto

Du kommentierst mit Deinem Google+-Konto. Abmelden / Ändern )

Verbinde mit %s