This content was uploaded by our users and we assume good faith they have the permission to share this book. If you own the copyright to this book and it is wrongfully on our website, we offer a simple DMCA procedure to remove your content from our site. Start by pressing the button below!
SPIELE ENTWICKELN FÜR iPAD, iPHONE UND iPOD TOUCH thomas LUCKA
EMPFOHLEN VON
Lucka Spiele entwickeln für iPad, iPhone und iPod touch
v
Bleiben Sie einfach auf dem Laufenden: www.hanser.de/newsletter Sofort anmelden und Monat für Monat die neuesten Infos und Updates erhalten.
Thomas Lucka
Spiele entwickeln für iPad, iPhone und iPod touch
Der Autor: Thomas Lucka, Berlin
Alle in diesem Buch enthaltenen Informationen, Verfahren und Darstellungen wurden nach bestem Wissen zusammengestellt und mit Sorgfalt getestet. Dennoch sind Fehler nicht ganz auszuschließen. Aus diesem Grund sind die im vorliegenden Buch enthaltenen Informationen mit keiner Verpflichtung oder Garantie irgendeiner Art verbunden. Autoren und Verlag übernehmen infolgedessen keine juristische Verantwortung und werden keine daraus folgende oder sonstige Haftung übernehmen, die auf irgendeine Art aus der Benutzung dieser Informationen – oder Teilen davon – entsteht. Ebenso übernehmen Autoren und Verlag keine Gewähr dafür, dass beschriebene Verfahren usw. frei von Schutzrechten Dritter sind. Die Wiedergabe von Gebrauchsnamen, Handelsnamen, Warenbezeichnungen usw. in diesem Buch berechtigt deshalb auch ohne besondere Kennzeichnung nicht zu der Annahme, dass solche Namen im Sinne der Warenzeichen- und Markenschutz-Gesetzgebung als frei zu betrachten wären und daher von jedermann benutzt werden dürften.
Bibliografische Information Der Deutschen Nationalbibliothek Die Deutsche Nationalbibliothek verzeichnet diese Publikation in der Deutschen Nationalbibliografie; detaillierte bibliografische Daten sind im Internet über http://dnb.d-nb.de abrufbar.
Dieses Werk ist urheberrechtlich geschützt. Alle Rechte, auch die der Übersetzung, des Nachdruckes und der Vervielfältigung des Buches, oder Teilen daraus, vorbehalten. Kein Teil des Werkes darf ohne schriftliche Genehmigung des Verlages in irgendeiner Form (Fotokopie, Mikrofilm oder ein anderes Verfahren) – auch nicht für Zwecke der Unterrichtsgestaltung – reproduziert oder unter Verwendung elektronischer Systeme verarbeitet, vervielfältigt oder verbreitet werden.
Einleitung – Let there be … games! .......................................................................... 1 Warum überhaupt Spiele für das iPhone programmieren? ...................................................... 1 Willkommen in der Welt von Apple – das iPhone-Phänomen ................................................ 2 Die iPhone-Family – Spezifikationen...................................................................................... 4 Aller Anfang ist leicht … ........................................................................................................ 5
2.1 2.2 2.3 2.4 2.5 2.6 2.7 2.8
Grundlagen – Wie funktioniert das alles denn bloß? ................................................. 9 Die Quelle der Macht – das Apple Dev Center ....................................................................... 9 Xcode und das iOS SDK herunterladen und installieren ....................................................... 10 Xcode-Vorlagen einsetzen..................................................................................................... 11 Hello World I mit Konsolenausgabe ..................................................................................... 12 Hello World II mit Text auf einer View-Instanz.................................................................... 20 Keine Angst vor Objective-C – ein 15-Minuten-Schnellkurs................................................ 24 Der Lebenszyklus einer App ................................................................................................. 31 Breite Unterstützung: Universale Apps ................................................................................. 34
Spiele entwickeln – von 0 auf 180 ........................................................................... 51 Wie funktionieren Spiele? ..................................................................................................... 51 Das 2D-Koordinatensystem................................................................................................... 53 Ein Beispielprojekt aufsetzen ................................................................................................ 55 Zeichenkurs – einfache Formen rendern ............................................................................... 58 Bilder einbinden, laden und anzeigen.................................................................................... 65 Game Loop und Frames – die Bilder zum Laufen bringen.................................................... 72 Clipping und Animationen .................................................................................................... 78 Kollisionskontrolle, bitte!...................................................................................................... 91 User-Input............................................................................................................................ 100 Und jetzt alle(s) zusammen: GameManager und Sprite-Verwaltung................................... 110 Zappp, Brzzz, Booom, Pennng! Hintergrundmusik und Sound-Effekte.............................. 133 Datenspeicherung ................................................................................................................ 147 Verbindung gesucht: Multiplayer-Spiele mit Game Kit und Game Center ......................... 151
OpenGL ES – der Turbo-Gang.............................................................................. 153 Warum OpenGL ES? ...........................................................................................................153 Was ist OpenGL ES, und wie ist es aufgebaut? ...................................................................154 OpenGL ES – grundlegende Fragen.....................................................................................156 Ein Template erstellen – OpenGL ES richtig einbinden.......................................................157 Das OpenGL-Koordinatensystem ........................................................................................164 Einfache Zeichenoperationen ...............................................................................................167 Exkurs: Mathe-Plotter ..........................................................................................................173 Und Bilder? Wie wär's mal mit Texturen!............................................................................175 Ab in die Matrix: die Transformationsfunktionen................................................................188 Animationen mit Textur-Clipping........................................................................................191 Unendliche Weiten: Scrolling und Parallax-Scrolling..........................................................195 Lassen wir's krachen: ein OpenGL ES-Shooter....................................................................204
5.1 5.2 5.3 5.4 5.5 5.6 5.7 5.8 5.9 5.10 5.11
Die dritte Dimension: 3D-Spiele ............................................................................ 237 Wie sind 3D-Spiele aufgebaut? ............................................................................................237 Das Grundgerüst...................................................................................................................239 Das 3D-Koordinatensystem .................................................................................................241 Einfache Formen zeichnen ...................................................................................................242 Texturierung von Flächen ....................................................................................................245 Texturierung von 3D-Körpern..............................................................................................248 Es werde Licht......................................................................................................................253 3D-Modelle erzeugen, laden und einbinden.........................................................................257 Weitere 3D-Modelle mit Textur...........................................................................................267 Ego-Perspektive: Kamera erstellen und einsetzen................................................................271 Spaceflight: ein 3D-Spiel entsteht ........................................................................................275
6.1 6.2 6.3 6.4
Waiting round to be a millionaire ........................................................................... 307 Das Tor zur Welt – iTunes Connect .....................................................................................307 Testen, Testen, Testen: Aber wie kommt das Spiel auf mein Gerät? ...................................308 Release und Distribution ......................................................................................................308 Marketing-Pläne? .................................................................................................................310
Vorwort Ausgerechnet Microsoft bewahrte Apple 1997 mit einer Investition von 150 Millionen Dollar vor dem Zusammenbruch. 13 Jahre später hat sich der Wert der Apple-Aktien mehr als verzehnfacht, und der Software-Riese Microsoft wird erstmals als weltgrößtes Technologieunternehmen (nach Marktwert) abgelöst – diesmal von der Firma mit Hauptsitz in Cupertino, Kalifornien. Als Steve Jobs 2007 das Mikrofon auf einer Bühne in San Francisco in die Hand nahm, um ein neues „revolutionäres“ Handy vorzustellen, und behauptete, dessen Technologie sei allen Mitbewerbern um zwei Jahre voraus, konnte man noch nicht ahnen, dass er sich offenbar selbst ein wenig unterschätzt hatte: Auch 2010 gelten iPhone, iPod touch und das iPad als Symbole für technologischen Fortschritt. Dabei haben andere Hersteller wie Nokia, Samsung, Sony Ericsson oder Google längst weitaus leistungsfähigere Handhelds auf den Markt gebracht. Mit ein Hauptgrund für die herausragende Stellung Apples liegt daher auch weniger in der Hardware als vielmehr in dem nahtlosen ökonomischen System rund um den App Store, der mittlerweile mehr als 200.000 Spiele und Programme bereithält und damit als der größte Download-Markt in der Mobilindustrie gilt. Die Grundlage für den App Store legte Apple bereits einige Jahre zuvor mit dem iTunes-Portal, das durch die Verbindung von Hardware (MP3-Player) und Software (iTunes) anderen Musik-Download-Plattformen stets eine Nasenlänge voraus war. Als dann im Sommer 2008 (ein Jahr nach Einführung des iPhones) mit dem iPhone SDK unabhängigen Entwicklern Zugriff auf die iPhone-Hardware gegeben wurde, war schnell klar, dass sich hier tatsächlich eine kleine Revolution vollzog: Mit kleinen Apps konnte der Funktionsumfang des nativen Betriebssystems beliebig erweitert werden, und die (damals noch) einzigartige Touch- und Sensorsteuerung erlaubte völlig neue Spielerfahrungen. Nun ist es ja nicht so, dass Apple die Sache mit den Apps erfunden hat, aber das von Apple strikt vorgegebene Zusammenspiel von Entwicklung, Deployment und Distribution ermöglichte jedem noch so kleinen Hobbyentwickler, an den Vorzügen der digitalen Vertriebsform unmittelbar teilzuhaben. Und ja, es ist – trotz der unüberschaubaren Anzahl – tatsächlich immer noch möglich, mit Spielen im App Store viel Geld zu verdienen. Wenn Sie die in diesem Buch vorgestellten Beispielprogramme nachvollziehen und die Grund-
IX
Vorwort lagen der Spieleentwicklung in eigene kleinere Spieleprojekte umsetzen, sollten Sie zumindest in Lage sein, den Kaufpreis dieses Buches wieder einzuspielen. Lassen Sie uns also keine Zeit verlieren! Ein Buch wie dieses entsteht nicht im luftleeren Raum, und so möchte ich einigen Personen meinen herzlichen Dank aussprechen: Fernando Schneider, der mit der Idee zu einem iPhone-Spielebuch an mich herantrat und dabei auf mehr als offene Ohren traf. Holger Patz, ohne den die Spiele in diesem Buch weitaus weniger gut ausgesehen hätten, maßgeblich das 3D-Spiel Spaceflight, die Pixelzombies und der 360-Grad-Shooter im OpenGL ES 2D-Kapitel (für die weniger gelungenen Spielegrafiken bin ich selbst verantwortlich). Janina Sieslack, die sich wie immer fleißig (und oftmals verzweifelt) über die Erstfassungen des Manuskriptes beugte. Markus Maaßen und Juan Pao, ohne deren Begeisterung für Apple-Produkte ich mir 2008 kein MacBook gekauft hätte. Unverzichtbaren Input zum Buch haben beigesteuert: Jörg Büttner, Sandra Gottmann, Bernd Hein, Patrick Hennies, Alexander Hüsgen, Sascha Kolewa, Marco Kraus, Anita Nagy, Julia Stepp, Szymon Ulewicz und Marcus Weidl. Ganz besonders danken möchte ich auch meiner Mutter, die mir bereitwillig den Rücken freigehalten hat, wann immer ich den Weg von Berlin nach Kassel auf mich nahm und das Schreiben der unvermeidlichen Arbeit im elterlichen Garten vorzog. Nun wünsche ich Ihnen aber viel Spaß beim Lesen und natürlich ganz viel Erfolg mit Ihren ersten eigenen Spielen im App Store – mit diesem Buch haben Sie den ersten Schritt getan. Happy Coding! (^^)/ St. Peter Ording, im August 2010
Thomas Lucka
X
1 1 Einleitung – Let there be … games! 1.1
Warum überhaupt Spiele für das iPhone programmieren? Auf den ersten Blick wirken iPhone, iPod touch und iPad nicht wie typische Gaming Devices: Es gibt keinen Joystick, ein Game Pad oder überhaupt irgendwelche Tasten (abgesehen vom Home-Button, dem Ein-/Aus-Schalter und dem Lautstärkeregler). Und dennoch sorgen Spiele mit für den größten Umsatz im App Store. 28% aller iPad-Nutzer gaben unlängst in einer Studie (Resolve Market Research, 2010) an, das Gerät hauptsächlich für Games zu verwenden. Dies ist umso erstaunlicher, wenn man bedenkt, dass der Spieleanteil im App Store nur knapp 17% ausmacht: Der Web-Katalog Apptism, der nahezu den kompletten Content des App Stores im Internet abbildet, listet im August 2010 insgesamt 213.292 Apps, davon lediglich 36.008 Spiele (http://www.apptism.com/). Sicherlich erschwert das Fehlen der klassischen Eingabemöglichkeiten das Spielen von typischen Genre-Vertretern wie zum Beispiel Jump’n’Runs oder Shoot’em Ups. Dennoch bietet die berührungsempfindliche Oberfläche der iOS-Geräte die Möglichkeit, einen fehlenden Joystick oder ein fehlendes Game Pad zu emulieren: Beide Varianten kommen unter anderem im C64-Emulator von Manomio zum Einsatz (http://c64.manomio.com/). Trotzdem bleibt die Touch-Steuerung gerade bei Neuauflagen älterer Retro-Titel aus den 80ern, die oft eine schnelle und präzise Reaktion des Spielers erfordern, den aus der Spielhalle entlehnten Arcade-Controls unterlegen: Zu hakelig, zu ungenau ist die TouchSteuerung, und ein Teil des Bildschirms wird zudem durch die virtuellen Kontrollmöglichkeiten verdeckt.
Aber genau in diesem Manko liegt auch die Herausforderung: Das Bedienkonzept von iPhone & Co. zwingt Spieleentwickler zum Umdenken, erfordert neue Ideen und innovative Steuerungsmöglichkeiten. Gelungene Beispiele, die zeigen, wie man das Touch-Display und den Bewegungssensor für Spiele einsetzen kann, gibt es mittlerweile genug.1 Da die Spiele auf das iPhone iOS zugeschnitten sein müssen, sorgt die Plattform zwangsläufig für neue und einmalige Spielerfahrungen, die sich nicht ohne Weiteres auf andere Spielkonsolen übertragen lassen oder umgekehrt von diesen für das iPhone adaptiert werden können.
1.2
Willkommen in der Welt von Apple – das iPhonePhänomen Am Anfang war … Snake! Snake? Der Weg vom ersten schwarz-weißen Handyspiel (Nokia 6110, 1997) bis hin zum iPhone ist gar nicht so lang: Bereits 2003 konnte man auf den ersten Nokia Smartphones 3D-Multiplayer-Spiele spielen und kurze Zeit später sogar erste Erfahrungen mit berührungsempfindlichen Displays oder Bewegungssensoren (teilweise sogar emuliert über den Kamera-Input von Symbian-Geräten) sammeln. Aber erst das iPhone brachte die Summe der Teile geschickt zusammen und sorgte – verbunden mit einer griffigen Marketing-Kampagne („Es gibt für alles eine App.“) – für die nötige Marktdurchdringung. Ein einziges Gerät, das bereits eine vollständige Entwicklungsplattform mit mehreren Millionen Nutzern bietet – dies war ein Novum in der bis dahin durch zahlreiche Modelle stark fragmentierten mobilen Industrie.
1
Einen guten Überblick bieten Webzines wie zum Beispiel Touch Arcade (http://toucharcade.com/) oder Pocket Gamer (http://www.pocketgamer.co.uk/latest.asp?sec=7).
2
1.2 Willkommen in der Welt von Apple – das iPhone-Phänomen Mittlerweile ist das erfolgreiche iPhone-Konzept auch auf nicht für Telefonie taugliche Geräte übertragen worden, wie den MP3-Player iPod und das iPad. Damit stehen die Apple-Geräte in direkter Konkurrenz zu dedizierten mobilen Gaming Devices wie GameBoy, Nintendo DS oder Sony PSP Portable. Der digitale Vertriebsweg der Spiele über den App Store bietet Apple und natürlich auch den Entwicklern eine viel kostengünstigere und schnellere Alternative zu den traditionellen Game Cartridges oder Discs und dem Ladenverkauf. Man könnte behaupten, dass mehr Freiheiten auch mehr Verantwortung erfordern, doch die Realität ist wohl eher, dass Apple alle Geschäftsbereiche kontrolliert, um den Umsatz zu steigern. Diese Restriktionen kommen andererseits aber auch der Benutzerfreundlichkeit zugute: Der Kauf neuer Spiele erfolgt ausschließlich über das Gerät selbst. Auch kostenlose Spiele können ausschließlich über den App Store installiert werden. Der Installationsprozess ist komplett vor dem User verborgen (keine Popups oder Installationshinweise), lediglich ein Fortschrittsbalken unter dem App-Icon signalisiert den Download-Vorgang. Entwickler brauchen nur einen einzigen Vertriebskanal zu beliefern. Apple kontrolliert, welche Software in den App Store gelangt und welche nicht. Da neue Software (offiziell) nur über den App Store verkauft wird, ist Apple am Umsatz beteiligt (derzeit 30% vom Kaufpreis einer App). Apple bestimmt das Look & Feel des eigenen Shops. Benutzer erhalten einen zuverlässigen und beständigen Einstiegspunkt, um nach neuer Software zu suchen. Andere Hersteller bieten dagegen völlig offene Plattformen, wie zum Beispiel Nokia mit dem Linux-basierten N900: Den Usern stehen hier vielfältige Wege offen, um neue Software zu installieren, vom Ovi Store über unabhängige Drittanbieter bis hin zur Selbstinstallation. Aber auch die Entwicklungswerkzeuge sind nahezu frei wählbar – Apple hingegen erlaubt nur Objective-C-basierte Apps, die wiederum nur mit der IDE Xcode auf einem Mac OS X-Rechner erstellt werden können. Doch der Erfolg scheint Apple Recht zu geben, Apple-Produkte sind einfach Kult: Als das iPhone 3GS im Juni 2009 in acht Ländern eingeführt wurde, wurden innerhalb einer Woche 1,6 Millionen Geräte abgesetzt. Google verkaufte im gleichen Zeitraum lediglich 20.000 Einheiten seines Android-Handys Nexus One (Flurry 2010, http://blog.flurry.com). Anfang 2010 wurden von GigaOM 58 Millionen aktive App Store-User verzeichnet (davon iPhone: 34 Millionen, iPod touch: 24 Millionen), die innerhalb eines Monats ungefähr 280 Millionen Apps herunterladen, ¼ davon bezahlter Content. Allein in der Weihnachtssaison 2009/10 wurden jeden Tag 500 neue Apps von ungefähr 28.000 iOS-Entwicklern
3
1 Einleitung – Let there be … games! hinzugefügt.2 Mittlerweile dürfte die Zahl der Entwickler bei ungefähr 35.000 liegen, und durch dieses Buch kommen hoffentlich noch ein paar weitere dazu.
1.3
Die iPhone-Family – Spezifikationen Auch die Fragmentierung innerhalb der iPhone iOS-kompatiblen Geräte nimmt immer mehr zu. Jedes Jahr im Sommer erscheint eine neue iPhone-Version, und es ist nicht unwahrscheinlich, dass Apple zukünftig neben dem iPad noch weitere neue Produktgattungen einführt. Die gute Nachricht ist, dass alle Geräte untereinander (bedingt) kompatibel sind, wenn Sie die jeweiligen Geräteeigenschaften berücksichtigen. Alle Beispiele dieses Buches sind selbstverständlich auf allen Geräten der iPhone-Familie lauffähig und intensiv mit unterschiedlichen iOS-Versionen getestet worden. Aktuell lassen sich neun Geräte nach drei Gattungen unterscheiden: iPhone: iPhone (1st Gen), iPhone 3G, iPhone 3GS, iPhone 4 iPod touch: iPod touch (1st Gen), iPod touch 2G, iPod touch 3G, iPod touch 4 iPad: iPad (1st Gen) Abgesehen von iPad und iPhone 4 verfügen die Geräte über die folgenden gemeinsamen Eigenschaften: kapazitiver Multi-Touchscreen 320x480-Pixel-LCD-Screen, 18 Bit (262.144 Farben), 163 Pixel pro Inch Arbeitsspeicher (RAM): mindestens 128 MB eDRAM Flash-Speicher: mindestens 8 GB NAND Alle Geräte verfügen neben der Haupt-CPU über einen 3D-Grafikchip, der mindestens OpenGL ES 1.1 unterstützt.3 Sensoren: Beschleunigungssensor (Accelerometer), Umgebungslichtsensor, Entfernungssensor (nur iPhone). WLAN 802.11b/g Mindestens Bluetooth 2.0+EDR (alle Modelle, außer 1st Gen iPod touch)
2 Die Umsatzzahlen des App Stores dürften einen großen Anteil an der Erfolgsstory des iPhones ausmachen. Unter http://www.distimo.com/appstores/ finden Sie einen Vergleich der vier großen DownloadStores (App Store von Apple, Ovi Store von Nokia, PlayNow von Sony Ericsson, Android Market von Google). 3 Haupt-CPU: iPhone 3GS und iPod touch 3G: 833 MHz (underclocked auf 600 MHz), ARM Cortex-A8, andere Modelle: 620 MHz (underclocked auf 400–412 MHz) Samsung 32 Bit RISC ARM, außer iPod touch 2G, welches minimal schneller ist (underlocked auf 532 MHz). 3D-Grafik-CPU: iPhone 3GS und iPod touch 3G: PowerVR MBX Lite 3D GPU, die anderen Modelle laufen mit der minimal langsameren PowerVR SGX GPU.
4
1.4 Aller Anfang ist leicht … Das iPad verfügt darüber hinaus über ein 1024x768er-Display und iPhone 4 und iPod touch 4 über 960x640 Pixel. Alle drei Geräte laufen mit einem A4-Prozessor mit 1 GHz, sind also deutlich schneller als die übrigen Geräte. Während iPhone 3GS, iPod touch 3G und iPad bereits über 256 MB Arbeitsspeicher verfügen, stellen iPhone 4 und iPod touch 4 komfortable 512 MB zur Verfügung. Alle Geräte lassen sich über iTunes kostenlos auf die jeweils neueste iOS-Version updaten. Wer dennoch unterschiedliche iOS-Versionen berücksichtigen möchte, findet im Apple Dev Center den „SDK-based Development“-Guide:
https://developer.apple.com/iphone/prerelease/library/documentation/DeveloperTools/Con ceptual/cross_development/Introduction/Introduction.html Die Beispiele für dieses Buch wurden für iOS 4.x entwickelt und getestet, sind in den meisten Fällen aber auch abwärtskompatibel zu früheren iOS-Versionen. Entsprechende Hinweise finden Sie in den nachfolgenden Kapiteln.
1.4
Aller Anfang ist leicht … Was braucht man, um mit der Entwicklung von Spielen für das iPhone iOS anfangen zu können? Idealerweise würden wir ja zu gerne antworten: Das vorliegende Buch und sonst nichts! Andererseits dürfte jeder Leser über unterschiedliche Voraussetzungen verfügen, und wir wollen dieses Buch nicht mit redundanten Informationen füllen, die sich bereits an anderer Stelle zur Genüge finden lassen. Um die Beispiele in diesem Buch nachvollziehen zu können, sollten Sie zumindest Kenntnisse in einer objektorientierten Sprache (Java, C#, C++) besitzen. Wenn Ihnen Begriffe wie Objekt, Vererbung, Klasse oder Methode spanisch vorkommen, dann greifen Sie vielleicht vorher zu einem Java-Tutorial. Warum Java? Java ist zwar nicht die Sprache des iPhones, aber bestens geeignet, um die Grundlagen der Programmierung zu verstehen, und wird zu diesem Zweck auch als erste Sprache an den meisten Universitäten gelehrt. Tiefergehende OOP-Kenntnisse werden für dieses Buch allerdings nicht benötigt. Wir haben uns bemüht, die Quelltexte so einfach und übersichtlich wie möglich zu gestalten. Rudimentäre Programmiererfahrungen wären aber auch schon die einzigen Theoriekenntnisse, die Sie mitbringen sollten. Da die meisten Leser durch das iPhone das erste Mal mit einem Mac oder Objective-C 2.0, der Sprache des Mac OS, in Berührung kommen dürften, haben wir einen kompakten Schnellkurs hinzugefügt, um alle Leser auf den gleichen Stand zu bringen. Sie werden merken, dass wir (absichtlich) nicht den gesamten Sprachumfang einsetzen, sodass sich die Grundlagen schnell verinnerlichen lassen. Daher setzen wir auch keine weiteren Kenntnisse über das iPhone iOS und dessen Frameworks voraus. Wir werden hier allerdings auch nicht in die Tiefe gehen und uns nur auf die für die Spieleentwicklung wesentlichen Bestandteile konzentrieren. Wer ausufernde Erläuterungen der zahlreichen APIs, zur Bedienung von Xcode oder gar des Interface Builders und zur GUIGestaltung sucht, kann parallel zu einem der zahlreichen App-Entwicklungsbücher greifen.
5
1 Einleitung – Let there be … games! In diesem Buch werden Spiele entwickelt, und daher werden vor allem das Cocoa Touch Framework, Core Graphics und OpenGL ES berücksichtigt – stets mit praktischem Bezug auf die konkreten Anforderungen eines Spieleentwicklers. Damit Sie die Buchbeispiele nicht nur theoretisch nachvollziehen können, benötigen Sie: Einen Intel-basierten Mac-Computer (oder MacBook oder Mac Mini) mit Mac OS X Snow Leopard ab 10.6.2. Wenn Sie die Beispiele nicht nur im Simulator ausprobieren möchten, brauchen Sie zusätzlich noch ein iPhone, iPod touch oder iPad, das Sie kostenlos über iTunes auf die neueste OS-Version updaten sollten (das ist allerdings keine Voraussetzung: die meisten Beispiele des Buches laufen ab iOS 3.1.2). Um die Beispiele auf einem iPhone, iPod oder iPad installieren zu können, brauchen Sie zusätzlich noch Zugang zum Apple Developer Programm (zum Preis von derzeit 79 Euro pro Jahr).4 Eine Internet-Verbindung, um das iPhone SDK und die Buchbeispiele herunterzuladen. Buchbeispiele downloaden Für dieses Buch wurden zahlreiche kleinere und größere Apps entwickelt: Insgesamt 28 verschiedene iOS-Projekte (inkl. drei größeren Spielen). Jede App stellt einen kleinen Teilbereich aus dem komplexen Gebiet der Spieleentwicklung in überschaubarer Form vor. In den jeweiligen Kapiteln wird auf das zugrunde gelegte Beispielprojekt hingewiesen. Um die Erläuterungen im Buch besser nachvollziehen zu können, empfiehlt es sich, die Beispiele parallel auf einem Rechner parat zu haben. Sie finden die Beispiele auf der Website zum Buch unter:
www.qioo.de/projekte/buch/iphonegames Die Beispiele sind als Zip-Archiv gepackt und passwortgeschützt. Das Passwort finden Sie hier im Buch, befolgen Sie dazu einfach die Anweisung auf der Website.
4 Wer sich nicht dem Diktat von Apple beugen will, kann alternativ mit der Entwicklung von WebApps (JavaScript, HTML und CSS) einsteigen, vgl. [Lobacher]. Das iPhone iOS verfügt über einen leistungsfähigen WebKit-Browser. Nach der Installation über das Internet wirken WebApps wie native Apps. Allerdings können Sie diese nicht ohne einen Objective-C-Wrapper über den App Store vertreiben (offiziell verbietet Apple diese Möglichkeit sogar). Ihnen bleibt hier also nur der Weg über das Internet oder über die WebApp-Homepage von Apple: http://www.apple.com/webapps/ Mit Dashcode stellt Apple sogar eine Entwicklungsumgebung bereit. Seien Sie aber gewarnt: Die Performance von WebApps reicht für die Spieleentwicklung nicht aus, es sei denn, es handelt sich um ganz einfache Spiele mit nur wenigen animierten Grafiken.
6
1.4 Aller Anfang ist leicht …
Feedback Fachbücher wie dieses richten sich an interessierte Leser und sprechen daher nur einen überschaubaren Leserkreis an. Ihre Meinung ist uns daher sehr wichtig. Scheuen Sie sich also nicht, mit mir, dem Autor, oder dem Hanser-Verlag in Kontakt zu treten. Sie erreichen uns über [email protected] Die für das Buch entwickelten Beispiele stammen aus der Praxis, wurden jedoch didaktisch aufbereitet, um für eine möglichst große Leserschaft verständlich zu sein. CodeOptimierungen wurden zugunsten einer besseren Lesbarkeit nur an den wirklich unverzichtbaren Stellen vorgenommen. Aufgrund der noch jungen iOS-Plattform bietet dieses Buch eine Fülle an exklusiven Informationen, die bisher weder in der deutsch- noch englischsprachigen Literatur veröffentlicht wurden. Teilen Sie uns bitte mit, wenn Sie Fehler entdeckt haben, und unterstützen Sie uns, zukünftig noch bessere Bücher zu publizieren: Welche Erwartungen hatten Sie an das Buch, wurden diese erfüllt? Gab es Themen, die Sie vermisst haben oder überflüssig fanden? An welchen Stellen des Buches hätten Sie sich ausführlichere Erklärungen gewünscht? Fanden Sie den Schwierigkeitsgrad beim Lesen zu hoch oder zu niedrig? Konnten Sie die Beispielprogramme mit Hilfe des Buches problemlos nachvollziehen und für eigene Projekte einsetzen? Wir sind immer daran interessiert, konstruktive Kritik zu erhalten oder bei Problemen mit dem Buch hilfreich zur Seite zu stehen; nehmen Sie dazu jederzeit Kontakt mit uns auf. Und schauen Sie von Zeit zu Zeit auch auf der Website zum Buch (siehe oben) nach, um Informationen über Updates zu erhalten.
7
2 2 Grundlagen – Wie funktioniert das alles denn bloß? 2.1
Die Quelle der Macht – das Apple Dev Center Apple stellt neben der Entwicklungsumgebung Xcode auch jede Menge Tutorials und Beispielcode auf den Seiten des Apple Developer Centers bereit. iPhone-spezifische Themen finden Sie unter:
http://developer.apple.com/iphone Dort finden Sie zahlreiche Infos zu den wichtigen Bibliotheken des iOS SDK 4 wie Cocoa Touch, Core Graphics und Core Audio, aber auch zu Fremdbibliotheken wie OpenGL ES oder OpenAL. Daneben bietet das Apple Dev Center aber auch Hilfestellungen zu Objective-C im Allgemeinen und zum Vertrieb über iTunes Connect, falls Sie später vorhaben, Ihre App auf einem Device zu testen oder über den App Store zu veröffentlichen. Um Zugriff auf die einzelnen Bereiche zu bekommen, sollten Sie sich eine kostenlose Apple-ID anlegen. Klicken Sie dazu auf der Startseite einfach auf den Button „Register“.
9
2 Grundlagen – Wie funktioniert das alles denn bloß?
Abb. 2.1: Startseite des Apple Developer Centers
2.2
Xcode und das iOS SDK herunterladen und installieren Bevor wir mit dem Entwickeln von Apps anfangen können, müssen wir zunächst das iOS SDK (vormals „iPhone SDK“ genannt) herunterladen. Üblicherweise ist dieses bereits gebündelt mit der Entwicklungsumgebung Xcode. Sie finden die jeweils aktuellste Version unter:
http://developer.apple.com/iphone/index.action#downloads Nach Anklicken des *.dmg-Installationspaketes und erfolgreicher Installation finden Sie Xcode auf Ihrem Mac in diesem Verzeichnis:
/Developer/Applicatons/Xcode.app Neben Xcode enthält das iOS SDK aber auch noch weitere Tools wie zum Beispiel den Interface Builder, Instrument oder den iPhone-/iPad-Emulator.
10
2.3 Xcode-Vorlagen einsetzen Beispielcode von Apple herunterladen und ausprobieren Um zu testen, ob die Installation geklappt hat, probieren Sie am besten den Beispielcode dieses Buches oder den vom Apple Dev Center aus. Einen guten Überblick über die Zeichenmethoden von Core Graphics liefert die QuartzDemo:
http://developer.apple.com/iphone/library/samplecode/QuartzDemo/index.html Laden Sie die QuartzDemo.zip-Datei herunter, und entpacken Sie diese durch Doppelklick. Im App-Ordner klicken Sie dann auf "QuartzDemo.xcodeproj", um das Projekt innerhalb von Xcode zu starten. Das Kompilieren und Starten des Emulators erfolgt über einen Klick auf „Build and Go“. Der iPhone-Emulator startet automatisch im Vordergrund. Den iPhone-Simulator einsetzen Der iPhone-Simulator unterstützt alle Geräte der iOS-Familie. Sobald sich der Emulator im Vordergrund befindet, können Sie über das Hintergrundfenster das Hardware-Verhalten simulieren. Klicken Sie dazu auf den „Hardware-Tab“: Turn left: Simulator wird nach links gedreht (Tastaturkürzel: cmd + Pfeiltaste links) Turn right: Simulator wird nach rechts gedreht (Tastaturkürzel: cmd + Pfeiltaste rechts) Simulator minimieren (Tastaturkürzel): cmd + H Simulator schließen (Tastaturkürzel): cmd + Q Daneben bietet der Simulator noch Unterstützung für Lifecycle-Events. Diese werden wir uns später noch genauer ansehen.
2.3
Xcode-Vorlagen einsetzen Xcode bietet vorinstalliert bereits eine Reihe von Vorlagen, die Sie als Basis für eigene Projekte verwenden können, wie zum Beispiel die „Window-based Application“ oder die Open GL ES-App. Die Vorlagen finden Sie über „File -> New Project“, in der oberen, linken Ecke muss „iPhone OS -> Application“ angewählt sein. Beim Speichern können Sie einen eigenen Namen vergeben. Der Speicherort spielt keine Rolle, Sie können später das Projekt samt Xcode durch Anklicken von "MyiPhoneTestApp.xcodeproj" starten. Aus jedem neuen Projekt können Sie wiederum eine neue Vorlage machen Kopieren Sie das Projekt dazu einfach in den Template-Ordner:
2 Grundlagen – Wie funktioniert das alles denn bloß?
2.4
Hello World I mit Konsolenausgabe In diesem Kapitel wollen wir direkt in die App-Entwicklung einsteigen, in deren Folge Ihre Programmiergrundkenntnisse gefordert sein werden. Zunächst zeigen wir nur den prinzipiellen Ablauf der Erstellung und den Aufbau von iOS-Apps. Mit der eigentlichen Spieleentwicklung werden wir erst in einem späteren Kapitel beginnen. Machen Sie sich also keine Sorgen, wenn Sie jetzt noch nicht alles zu 100% verstehen. Sobald wir gelernt haben, wie man eine App anlegen, ausführen und den Quelltext bearbeiten kann, werden wir die Eigenheiten der Programmiersprache Objective-C in einem Schnellkurs vorstellen, sodass Sie eigenständig experimentieren können.1 Immerhin wissen wir bereits, wie Programme in Xcode aussehen und gestartet werden können. Daher wollen wir nun als erstes eigenes Programm eine "Hello World"-App in Angriff nehmen. Legen Sie zunächst ein neues Projekt an, indem Sie die IDE Xcode starten und in der Menüleiste „File -> New Project“ wählen. Es öffnet sich das bereits bekannte TemplateAuswahlfenster. Links wählen Sie „iPhone OS“ und „Application“, um die vorhandenen iOS-TemplateProjekte aufzurufen. Als Vorlage entscheiden wir uns für die „Window-based Application“, die den reduziertesten Startpunkt für Anwendungen darstellt. Klicken Sie auf "Choose…", um einen Dateinamen zu vergeben, zum Beispiel „HelloWorld1“, und geben Sie einen Speicherort für das Projekt an (das fertige Projekt finden Sie – wie die anderen Beispiele des Buches – auf der vorne im Buch angegebenen Download-Seite). Delegates und main()-Methode Nachdem wir das Projekt angelegt haben, präsentiert sich dieses im aufgeklappten XcodeZustand wie folgt: Rechts befindet sich die Editor-Ansicht, und im "Classes"-Folder (linke Seite) finden wir die jeweiligen Quelltexte. Die Template-Vorlage stellt also zwei TextFiles für uns bereit, nämlich die Klasse HelloWorld1AppDelegate, bestehend aus Header (*.h, Deklaration) und Implementierung (*.m): HelloWorld1AppDelegate.h HelloWorld1AppDelegate.m
Dabei handelt es sich, wie bereits an der Namensgebung zu erkennen ist, um den Delegate der App. Die beiden Text-Files bzw. Quelltexte hat Xcode für uns bereits folgendermaßen mit Inhalt gefüllt:
1
12
Sollten Sie das erste Mal mit Objective-C konfrontiert werden, können Sie den Schnellkurs aber auch zuerst lesen und dann zu diesem Kapitel zurückkehren.
Beide Dateien werden wir weiter unten genauer vorstellen, zuvor aber noch etwas Theorie. (Sie können die folgende Einführung auch überspringen, wenn Sie sich nur für die praktische Umsetzung interessieren.) Wir haben bereits das Delegate-Konzept erwähnt. Was also ist ein Delegate? Ein Delegate ist eine Art Mittelsmann, der dem Entwickler einerseits Zugriff auf das komplexere Application-Objekt UIApplication gewährt, andererseits aber auch Nachrichten von diesem stellvertretend verarbeiten kann. Eine Delegate-Klasse implementiert das -Protokoll und verfügt damit über vorab definierte Schnittstellen, die es der UIApplication-Instanz ermöglichen, Nachrichten an den Delegate zu senden – falls bestimmte Ereignisse eingetreten sind, deren Behandlung delegiert werden soll. Von was für Ereignissen ist hier die Rede? Dies kann zum Beispiel ein eingehender Telefonanruf sein, der die laufende Anwendung unterbricht, oder der Anwender hat den HomeButton gedrückt, um die App zu schließen. Aber auch der Start unserer Anwendung über das App-Icon stellt ein solches Ereignis dar. Weiter unten werden wir näher auf die Lebenszyklen einer App und weitere Nachrichten des Protokolls eingehen. Objekte, die eine Nachricht an die UIApplication-Instanz senden, müssen nicht wissen, dass diese die Nachricht nicht selbst behandelt, sondern stattdessen an den App-Delegate
13
2 Grundlagen – Wie funktioniert das alles denn bloß? weiterreicht. In der Informatik ist das Delegate-Pattern ein häufig eingesetztes Entwurfsmuster: Der Delegierer (UIApplication) nimmt die Anfrage (zum Beispiel „App gestartet“) entgegen und delegiert sie an die Delegate-Instanz. Der Delegierte (App-Delegate) führt die Anfrage letztlich aus (zum Beispiel was passieren soll, wenn die App gestartet wurde). Da der Delegierte eine Referenz des Delegierers erhält, können Nachrichten auch in die entgegengesetzte Richtung geschickt werden. Jede iPhone-App verfügt über eine solche Delegate-Klasse, die damit aus Entwicklersicht den eigentlichen Beginn des Programmablaufs markiert: Die applicationDidFinishLaunching:-Methode (siehe unten), die über das -Protokoll vereinbart wurde, wird nämlich genau dann aufgerufen, wenn unsere App in einen Zustand versetzt wurde, der das Abarbeiten der Programmbefehle ermöglicht (unter anderem wird die Sandbox angelegt, ein Speicherbereich, in dem die aktuell laufende App keinen Schaden anrichten kann. Dieser schützt dadurch Daten anderer Programme vor unerlaubten Zugriffen). Dazu später mehr. Wer sich genauer dafür interessiert, was vor dem Einstieg in den Programmfluss passiert, kann einen Blick in den Ordner "Other Sources" werfen. Dort befindet sich nämlich die main()-Methode, die – wie in C/C++ – vom Betriebssystem aufgerufen wird, sobald das Programm gestartet wird. Üblicherweise gibt es keinen Grund, an main.m etwas zu ändern. Der Code stellt sich ohnehin sehr übersichtlich dar: Listing main.m #import int main(int argc, char *argv[]) { NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init]; int retVal = UIApplicationMain(argc, argv, nil, nil); [pool release]; return retVal; }
Aber auch die main()-Methode bildet nicht den eigentlichen Anfang. Wir müssen noch weiter zurückgehen. Im Resources-Ordner finden Sie: MainWindow.xib HelloWorld1-Info.plist
Bei der ersten Datei, einem sogenannten Nib-File, handelt es sich um eine XML-Datei, die den Aufbau des Hauptfensters unserer Anwendung enthält. Um sich den Inhalt anzusehen, gehen Sie mit Rechtsklick auf die Datei und wählen „Open As -> Plain Text File“. Wie Sie erkennen können, wurde die Datei mit dem Interface Builder angelegt und vom XcodeTemplate für uns bereitgestellt. Die Datei enthält einen Verweis auf die Delegate-Klasse.
14
2.4 Hello World I mit Konsolenausgabe Und ansonsten stellt Sie lediglich eine weiße Fläche dar, einen sogenannten View. Was es mit diesem View auf sich hat, werden wir uns später ebenfalls noch genauer ansehen. Was sind xib- bzw. NIB-Files? Und wieso fangen manche Klassen mit NS an? Das Kürzel NS steht für NeXTStep, ein Betriebssystem, das ab 1986 von NeXT Computer entwickelt wurde (erster Release 1989). Die Firma wurde von Steve Jobs und mehreren weiteren ehemaligen Apple-Mitarbeitern gegründet. 1996, als Jobs zu Apple zurückkehrte und NeXT von Apple für 402 Millionen US-Dollar aufgekauft wurde, entschied man sich, NeXTStep als Basis für ein neues OS zu nehmen, eben Mac OS X. Die Namensgebung vieler Klassen erinnert also auch heute noch an diese Ursprünge. Die Abkürzung NIB steht folgerichtig für NeXT Interface Builder, XML-Dateien also, die mit dem Interface Builder angelegt wurden. Um deutlich zu machen, dass es sich bei den NIB-Files um ein reines XML-Textformat handelt, werden die NIB-Dateien mit der Dateiendung xib abgespeichert. Häufig werden beide Namen synonym verwendet. Wir werden in diesem Buch auf den Einsatz des Interface Builders verzichten und die benötigten GUI-Elemente programmatisch erzeugen. Der Interface Builder ist ein WYSIWYG (What-You-See-Is-What-YouGet)-Editor für GUI-Elemente, der ähnlich aufgebaut ist wie beispielsweise ein HTMLEditor. Der Programmfluss, Frameworks und die App-Datei Lassen Sie sich nicht von den vielen, neuen Begriffen verwirren – der grundlegende Aufbau einer App ist stets gleich, sodass wir zukünftig nur wenige Änderungen daran vornehmen müssen. Von Bedeutung ist an dieser Stelle die Plist-Datei HelloWorld1-Info.plist, bei der es sich ebenfalls um eine XML-Datei handelt. Plist steht für Property List. Diese Datei stellt eine Art Projekt-Manifest dar und gibt dem Betriebssystem vor Ausführung grundlegende Informationen zur Hand, wie zum Beispiel die aktuelle Versionsnummer, den Namen des Programms und – Sie haben es vielleicht bereits geahnt – den Verweis auf das Hauptfenster: Main nib file base name = MainWindow
In der XML-Datei sieht das dann so aus: NSMainNibFile <string>MainWindow
Xcode erleichtert uns die Arbeit mit der Plist insofern, als uns hier, anders als beim MainWindow.xib, eine tabellarische Darstellung zur Verfügung steht. Das xib-File können Sie dagegen über den Interface Builder editieren. Dieser öffnet sich, wenn Sie auf MainWindow.xib doppelklicken (für die Entwicklung von Spielen benötigen wir dies aber nicht). Der Ablauf unseres Grundgerüstes stellt sich also wie folgt dar: 1. Zuerst wird die Plist-Datei ausgewertet. 2. Anhand des dort referenzierten NIB-Files wird das Hauptfenster MainWindow.xib identifiziert.
15
2 Grundlagen – Wie funktioniert das alles denn bloß? 3. Zusammen mit der UiApplication-Instanz landet die Referenz auf das Hauptfenster über die main()-Methode und den dortigen Aufruf von UIApplicationMain schließlich bei der Delegate-Klasse. Bevor wir uns dieser zuwenden, möchten wir noch auf den Framework-Ordner innerhalb von Xcode hinweisen. Die Basis-Frameworks sind bereits eingebunden: UIKit.framework – Fenstermanagement, UI-Elemente, Touch-Handling etc. Foundation.framework – Basisfunktionalität, z.B. Strings, Threads CoreGraphics.framework – Schnittstelle zu Quartz 2D (Grafik) Wir werden später noch zeigen, wie weitere Frameworks, zum Beispiel für die AudioWiedergabe oder OpenGL ES, hinzugefügt werden können. Wenn Sie auf die Häkchen links neben dem Dateinamen klicken, sehen Sie die einzelnen Bestandteile des jeweiligen Frameworks in Form der Header-Dateien. Weiterhin finden Sie unter „Products“ das ausführbare Programm: HelloWorld1.app
Die Datei ist derzeit noch rot markiert, da die App noch nicht existiert. Wir müssen diese erst noch bauen. Klicken Sie dazu auf den „Build and Go“-Button, und es sollte sich automatisch das iPhone-Simulatorfenster öffnen und einen leeren, weißen Screen zeigen. Wenn Sie nun auf den Home-Button klicken, können Sie die App wieder schließen, ganz so wie auf einem realen Device. Der Dateiname HelloWorld1.app sollte sich nun schwarz eingefärbt haben. Bei der App-Datei handelt es sich lediglich um ein Verzeichnis, in dem sich unsere Quelltexte und Ressourcen befinden. Anzeigen lässt sich der Inhalt von HelloWorld1.app, wenn Sie per Rechtsklick "Reveal in Finder" und dann auf der Programmdatei nochmals per Rechtsklick "Paketinhalt zeigen" wählen. Der Aufbau der Delegate-Klasse und die Implementierung der Konsolenausgabe Da unsere Hello-World-App einen Text über die Konsole ausgeben soll, müssen wir dies natürlich noch programmieren. Das Template erzeugt lediglich eine weiße Fläche. Doch wo finden wir die Konsole überhaupt? Ganz einfach: Wählen Sie in Xcode in der oberen Menüleiste "Run -> Console". Damit sich die Konsole zukünftig bei jedem Programmstart automatisch öffnet, stellen Sie dies am besten in den Voreinstellungen ein: "Xcode -> Preferences -> Debugging". Wählen Sie "On Start: Show Console". Ganz nützlich ist es auch, wenn Sie das Häkchen bei "Auto Clear Debug Console" setzen, damit Sie mit jedem Start des Simulators ein frisches, unbeschriebenes Konsolenfenster vorfinden. Doch bevor wir unsere "Hello World"-Meldung ausgeben, sehen wir uns nun nochmals den (leicht veränderten) Aufbau des Delegaten an:
16
2.4 Hello World I mit Konsolenausgabe Listing HelloWorld1AppDelegate.h #import @interface HelloWorld1AppDelegate : NSObject { UIWindow *window; } @end
Wie Sie feststellen, haben wir die nicht benötigte Zeile @property (nonatomic, retain) IBOutlet UIWindow *window;
entfernt. Diese wurde vom Template für uns angelegt und legt bestimmte Eigenschaften der vom Interface Builder erzeugten Instanzvariablen window fest, außerdem können über die @property-Direktive implizit erzeugte Setter-/Getter-Methoden angefordert werden.2 Damit wird im Header lediglich die window-Instanz deklariert. Da diese Teil des UIKitFrameworks ist, muss auch der UIKit-Header importiert werden über: #import
Des Weiteren wird mit HelloWorld1AppDelegate : NSObject
festgelegt, dass unser Delegate von der Standardoberklasse NSObject abgeleitet ist und das UIApplicationDelegate-Protokoll implementiert (Protokolle werden in Java als "Interfaces" integriert, in Objektive-C genügen spitze Klammern). Ein Protokoll ist nichts weiter als eine Implementierungsvorschrift, die in unserem Fall darauf hinweist, dass wir die applicationDidFinishLaunching:-Methode in unserer Klasse implementieren werden. Sehen wir uns die (wiederum leicht veränderte) Klasse an: Listing HelloWorld1AppDelegate.m #import "HelloWorld1AppDelegate.h" @implementation HelloWorld1AppDelegate - (void)applicationDidFinishLaunching:(UIApplication *)application { [window makeKeyAndVisible]; NSLog(@"Hello World!"); } - (void)dealloc {
2
Wenn Sie lediglich Setter/Getter erzeugen wollen und ansonsten mit den Default-Eigenschaften eines Members leben können, reicht der Aufruf von @property NSString* text. Die Defaulteigenschaften sind readwrite (also Setter UND Getter, nur ein Getter: readonly. Sollen eigene Namen für Setter/Getter verwendet werden, kann man das explizit als Eigenschaft angeben, z.B. setter = mySetterMethod, getter = myGetterMethod), assign (Verhalten bei Zuweisungen, andere mögliche Werte sind retain, copy), atomic (Verhalten bei Thread-Zugriffen, atomic: Zugriffe werden sequenziell abgearbeitet, alternativ: nonatomic, d.h., parallele Zugriffe sind erlaubt).
17
2 Grundlagen – Wie funktioniert das alles denn bloß? [window release]; [super dealloc]; } @end
Durch das Protokoll ist – wie bereits beim Delegate-Konzept erläutert – vereinbart, dass unsere Klasse Nachrichten von anderen Klassen empfangen kann, und zwar genau über diejenigen Methoden, die in dem Protokoll definiert sind. In diesem Fall handelt es sich um ein informelles Protokoll – das bedeutet, wir müssen nicht alle vorgegebenen Methoden implementieren, sondern nur diejenigen, die für uns interessant sind. Da wir wissen wollen, wann unser Programm mit der Arbeit anfangen kann, reicht die applicationDidFinishLaunching:-Methode vorerst aus. In der Implementierungsdatei haben wir außerdem die Zeile @synthesize window; entfernt. Über die @synthesize-Direktive werden die im Header angeforderten Eigenschaften und Setter-/Getter-Pärchen verfügbar gemacht. Wir haben dafür keine Verwendung und brauchen uns daher nicht weiter damit zu beschäftigen. Wie es sich für einen Delegaten gehört, müssen wir an die eigentliche Application-Instanz herankommen. Und genau dieses regelt die applicationDidFinishLaunching:-Methode, die als Parameter die UIApplication-Instanz erwartet. Im Moment haben wir dafür zwar noch keine Verwendung, aber das wird sich später noch ändern. Wie an der Namensgebung zu erkennen ist, wird diese Methode aufgerufen, sobald der Ladeprozess unserer Anwendung abgeschlossen ist. Damit bildet die Methode den Startpunkt einer iPhone-App und ist somit auch der ideale Ort für unseren "Hello World"Text. Wir haben den Aufruf wie folgt platziert: - (void)applicationDidFinishLaunching:(UIApplication *)application { [window makeKeyAndVisible]; NSLog(@"Hello World!"); }
Um Text in die Konsole zu schreiben, verwenden wir die NSLog()-Funktion: NSLog(@"Hello World!");
Der Funktion können ein String sowie mehrere Parameter, ähnlich der printf()-Funktion, übergeben werden. Strings werden in Objective-C immer mit der @-Direktive eingeleitet. Außerdem finden Sie in der Klasse noch die dealloc-Methode, die unsere Klasse von NSObject geerbt hat. Sie wird hier überschrieben, um den Speicherplatz des Fensters mit [window release]; wieder freizugeben. Danach folgt dann die Deallokation der Instanz unserer Delegate-Klasse mittels [super dealloc]. Übrigens, wenn Ihnen die Syntax von Objective-C noch etwas fremd erscheint – keine Sorge, wir werden dazu gleich noch ein kleines Tutorial nachschieben.
18
2.4 Hello World I mit Konsolenausgabe Um nun unsere Konsolenausgabe in Aktion zu sehen, müssen wir das Projekt neu bauen und klicken erneut auf "Build and Go". Der iPhone-Simulator startet unser Programm erneut, und in der Konsole erscheint wie erwartet unsere "Hello World"-Textausgabe: [Session started at 2010-04-18 21:20:12 +0200.] 2010-04-18 21:20:14.866 HelloWorld1[3167:207] Hello World!
Natürlich können Sie zum Debuggen einer Anwendung auch den in Xcode integrierten Debugger starten ("Run -> Debugger"), nachdem Sie zuvor per Rechtsklick an der Seitenleiste des Code-Fensters einen Breakpoint gesetzt haben. Oft reicht aber bereits eine einfache Konsolenausgabe aus, um einen Fehler im Programmablauf festzustellen. Um beispielsweise einen Integer-Wert auszugeben, programmieren Sie Folgendes: int value = 22; NSLog(@"Wert von value: %i", value);
Dies liefert die folgende Ausgabe: 2010-04-18 21:28:38.192 HelloWorld1[3241:207] Wert von value: 22
Sie können auch mehrere Werte kommasepariert übergeben: NSString *myString = @"ein String"; float fvalue = 22.7; NSLog(@"Ausgabe: %f, String: %@, Window: %@", fvalue, myString, window);
Als Ausgabe erhalten Sie dann: 2010-04-18 21:33:26.446 HelloWorld1[3283:207] Ausgabe: 22.700001, String: ein String, Window: >
Im String werden Variablen stets mit dem Prozentzeichen als Platzhalter bestimmt, gefolgt vom Variablentyp, also i für int, f für float und @ für ein Objekt (Strings sind auch Objekte).3 Nach dem String werden dann alle Variablen übergeben, in der Reihenfolge, wie sie im String stehen. Die Anzahl der Variablen ist unbegrenzt, achten Sie aber unbedingt auf den passenden Formatspezifizierer. Wie Sie an der Konsolenausgabe sehen können, ist das window-Objekt bereits für die Konsolenausgabe vorbereitet worden und gibt sinnvollerweise ein paar Felder mit aus: >
So enthält frame beispielsweise die Größe des Fensters: 320x480 Pixel. Bei nicht vorbereiteten Objekten liefert die Konsolenausgabe lediglich die Speicheradresse zurück, also etwa .
3
Eine Übersicht aller Formatspezifizierer finden Sie unter: http://developer.apple.com/iphone/library/ documentation/Cocoa/Conceptual/Strings/index.html
19
2 Grundlagen – Wie funktioniert das alles denn bloß?
2.5
Hello World II mit Text auf einer View-Instanz Bevor wir im nächsten Kapitel auf einige Besonderheiten von Objective-C eingehen (Was bedeuten diese eckigen Klammern? Wieso muss ich Speicher freigeben? Was ist überhaupt eine View-Instanz?), zeigen wir in diesem Kapitel, wie man eine Hello World-Meldung auf den Bildschirm des iPhones schreiben kann.4 Um überhaupt irgendetwas auf dem Screen anzeigen zu können, wird ein Zugriff auf den Bildschirm-Kontext benötigt, um eine Art Zeichenfläche zu erhalten. Das UIKitFramework stellt zu diesem Zweck die UIView-Klasse zur Verfügung. Das NIB-Window aus dem ersten Hello World-Beispiel war bereits eine UIWindow-Instanz, die wiederum von UIView abgeleitet ist. Zunächst legen wir wieder ein neues Basisprojekt an ("Window-based Application"). Per Rechtsklick auf den "Classes"-Ordner wählen wir "Add -> New File …", und dann erzeugen wir eine neue Klasse: "iPhone OS -> Cocoa Touch Class -> Objective-C class". Als Namen für den View wählen wir „MainView“. Achten Sie darauf, dass das Häkchen bei "Also create 'MainView.h'" gesetzt ist. Um aus der neuen Klasse einen UIView zu machen, wandeln wir die von Xcode erstellte Vorlage wie folgt ab: Listing MainView.h #import @interface MainView : UIView { } @end
Wir sorgen dafür, dass unsere Klasse von UIView abgeleitet wird, und importieren den UIin dem die UIView-Klasse deklariert ist.
Kit-Header,
Die Implementierung der Klasse ist übersichtlich. Wir überschreiben hier lediglich die von UIView geerbte drawRect:-Methode. Diese Methode wird immer dann aufgerufen, wenn ein Ereignis eintritt, dass der UIView-Instanz mitteilt, dass es etwas Neues auf den View zu zeichnen gibt: Listing MainView.m #import "MainView.h" @implementation MainView
4
20
Sollten Sie das erste Mal mit Objective-C in Berührung kommen, können Sie den Objective-CSchnellkurs zuerst lesen – dort finden Sie auch Links zu weiteren Tutorials.
2.5 Hello World II mit Text auf einer View-Instanz - (void) drawRect: (CGRect) rect { //hier soll der Text gezeichnet werden } @end
Der Zeichenvorgang muss also durch ein Ereignis angestoßen werden. Doch wie lösen wir dieses Ereignis aus? Dafür gibt es verschiedene Möglichkeiten. Da wir die drawRect:Methode nur einmal ausführen müssen, reicht es, den Render-Vorgang über die MainViewKlasse zu „triggern“. Dazu müssen wir den Delegate anpassen: Listing HelloWorld2AppDelegate.h #import #import "MainView.h"
Während wir in der Header-Datei lediglich den MainView deklarieren, passiert in der Implementierung einiges mehr. Mit mainView = [[MainView alloc] initWithFrame: [UIScreen mainScreen].applicationFrame];
21
2 Grundlagen – Wie funktioniert das alles denn bloß? sorgen wir dafür, dass der mainView alloziiert und schließlich in der Größe des aktuellen Screens angelegt wird. Statt den obigen Aufruf in eine Zeile zu schreiben, hätten wir alternativ Folgendes machen können: id viewId = [MainView alloc]; mainView = [viewId initWithFrame: CGRectMake(0,0,320,480)];
Bei id handelt es sich in Objective-C um einen generischen Objektdatentyp, der für jeden Objekttyp stehen kann. Zunächst sorgt die alloc-Anweisung dafür, dass das Objekt viewId Speicherplatz zugewiesen bekommt. Im nächsten Schritt erfolgt die eigentliche Initialisierung des Objektes (Objective-C besitzt keinen Klassennamen-Konstruktor wie etwa Java). Die initWithFrame:-Methode stammt von der UIView-Klasse und erwartet als Parameter ein Rechteck vom Typ CGRect. Wir erzeugen also direkt ein Rechteck mit 320x480 Pixeln. Tatsächlich wird der Screen-Bereich aber etwas kleiner ausfallen, da die Statusleiste am oberen Bildrand ein paar Pixel beansprucht (wir werden uns natürlich noch ansehen, wie man die Leiste ausblenden kann). Seit dem Erscheinen von iPad und iPhone/iPod 4, die über einen größeren Screen verfügen, empfiehlt es sich, die jeweils auf dem Device verfügbare Größe mittels [UIScreen mainScreen].applicationFrame abzufragen. Nachdem die MainView-Instanz erzeugt wurde, brauchen wir diese nur noch sichtbar zu machen, indem wir mainView als Subview über das Hauptfenster window legen: [window addSubview: mainView]; [window makeKeyAndVisible];
Die makeKeyAndVisible-Methode sorgt schließlich dafür, dass das Elternfenster aktiviert wird und Touch-Events erhalten kann. Außerdem triggert die addSubview:-Methode einmalig die drawRect:-Methode, sodass wir mit dem eigentlichen Zeichenvorgang des „Hello World“-Textes beginnen können: - (void) drawRect: (CGRect) rect { CGContextRef gc = UIGraphicsGetCurrentContext(); CGContextSetRGBFillColor(gc, 1, 1, 1, 1); NSString *str = @"Hello World II"; UIFont *uif = [UIFont systemFontOfSize: 40]; [str drawAtPoint: CGPointMake(40, 200) withFont: uif]; }
Um auf einem View Inhalte zu rendern, setzen wir das Core Graphics-Framework ein. Deshalb besorgen wir uns zunächst den aktuellen Grafikkontext über die UIGraphicsGetCurrentContext()-Funktion und rufen auf diesem dann die CGContextSetRGBFillColor()-Funktion auf, um die Zeichenfarbe festzulegen. Die Farbe übergeben wir dabei als RGB-Tripel plus Alphawert, jeweils im Bereich von 0 bis 1: Red, Green, Blue: 0.0 (Schwarz) bis 1.0 (Weiss) Alpha: 0.0 (durchsichtig) bis 1.0 (undurchsichtig) Da ein View voreingestellt über einen schwarzen Hintergrund verfügt, wählen wir als Vordergrundfarbe Weiß. Über den nächsten Schritt können wir den String auf dem Screen
22
2.5 Hello World II mit Text auf einer View-Instanz anzeigen: Die NSString-Klasse besitzt zu diesem Zweck die drawAtPoint:withFont:Methode. Für den ersten Parameter wird eine CGPoint-Struktur erwartet, die bestimmt, dass der String an der Position x = 40 y = 200
gezeichnet werden soll. Als zweiten Parameter kann man einen Font angeben. Wir sind nicht wählerisch und wählen schlicht die voreingestellte Systemschrift des iPhones: Das UIFont-Objekt haben wir über die systemFontOfSize:-Methode mit einer Schriftgröße von 40 Pixeln erstellt.
Abb. 2.2: Die HelloWorld2-App, gestartet über Xcode
Im Hinblick auf die Spieleentwicklung sind wir mit dieser zweiten Hello World-Variante einen weiteren Schritt vorangekommen: Wir wissen nun, wie man auf dem Screen etwas rendern kann, auch wenn es sich dabei nur um einen Text gehandelt hat. Für unsere späteren Spieleprojekte werden wir die UIView-Klasse ebenfalls als Malfläche verwenden. Dabei werden wir ausführlich auf die jeweiligen Core Graphics-Konzepte eingehen, die in diesem ersten Überblick noch zu kurz gekommen sind. Und für all diejenigen, die mit der Syntax von Objective-C das erste Mal in Berührung gekommen sind, wollen wir nun genauer auf die Kernsprache des iOS eingehen und einige Besonderheiten vorstellen, sodass Sie als Leser für die kommenden Kapitel mit dem nötigen Rüstzeug gewappnet sind.
23
2 Grundlagen – Wie funktioniert das alles denn bloß?
2.6
Keine Angst vor Objective-C – ein 15-Minuten-Schnellkurs Zugegeben, auf den ersten Blick mag Objective-C etwas befremdlich wirken. Doch schon ein zweiter Blick zeigt, dass sich gängige Konzepte und Muster anderer Sprachen auch in Objective-C wiederfinden und lediglich eine etwas andere Syntax besitzen. Wenn Sie mit Java, C# oder C/C++ vertraut sind, sollten Sie nach diesem Schnellkurs keine Probleme haben, Objective-C-Quelltexte nachvollziehen zu können. Wir werden natürlich nicht den gesamten Sprachumfang darstellen, sondern unser Augenmerk auf diejenigen Bereiche legen, die für die Spieleentwicklung und die Projekte in diesem Buch benötigt werden. Einstiegshilfen Wer an einer grundlegenden Einführung interessiert ist und/oder noch gar keine Programmiererfahrung besitzt, findet im Internet mittlerweile zahlreiche Tutorials zu Objective-C, wie zum Beispiel dieses hier (sogar auf Deutsch):
http://www.infobliss.at/objc/obc001_index.htm Auf Englisch: http://www.cocoadevcentral.com/d/learn_objectivec/ Etwas anspruchsvoller ist dagegen schon die von Apple selbst angebotene "Einführung in die Objektive-C-Programmiersprache":
http://developer.apple.com/mac/library/documentation/cocoa/conceptual/ObjectiveC/Intro duction/introObjectiveC.html Doch Achtung: Bevor man sich näher mit Objective-C beschäftigt, sollten die Grundlagen der objektorientierten Programmierung (OOP) bekannt sein, da die meisten Tutorials diese bereits voraussetzen. Zum Thema OOP finden sich zahlreiche Tutorials im Web, Sie können sich aber auch ein beliebiges Java-Programmierbuch besorgen, da sich OOPKonzepte in dieser Sprache unseres Erachtens besonders einfach vermitteln lassen. Wie Objective-C entstanden ist Objective-C wurde in den frühen 80ern entworfen und basiert auf C, erweitert dieses jedoch, wie der Name bereits ahnen lässt, um objektorientierte Sprachelemente, die von Smalltalk abstammen. Außerdem können in Xcode-Projekten C++-Klassen eingebunden werden (*.mm für C++-Klassen statt *.m für Objective-C-Klassen, das reicht dem Compiler schon). Da Objective-C als Standardsprache auf dem Betriebssystem NeXTStep eingesetzt wurde, bildet die Sprache auch die Basis des Nachfolgers Mac OS X und wird daher hauptsächlich in der Mac-Programmierung und natürlich dem iOS eingesetzt. Methodensyntax Das vielleicht auf den ersten Blick ungewöhnlichste Merkmal ist die Klammersyntax von Objective-C. Ein typischer Methodenaufruf könnte zum Beispiel so aussehen: [myObject printString: @"Objective-C ist cool!"];
24
2.6 Keine Angst vor Objective-C – ein 15-Minuten-Schnellkurs Zuerst wird der Name der Objektinstanz angegeben, deren Methode aufgerufen werden soll. Die Objektinstanz ist myObject, und der Methodenname lautet printString:. Nach dem Doppelpunkt folgt dann der Parameter, der an die Methode übergeben werden soll. Der Doppelpunkt ist Teil des Methodennamens, sofern die Methode einen Parameter erwartet. Bei einer parameterlosen Methode wird der Doppelpunkt weggelassen. In Objektive-C spricht man anstelle von Methoden auch von Nachrichten: Die Nachricht „Gib diesen String aus!“ wird also an das Objekt myObject gesendet. Wie in anderen Sprachen auch können beliebig viele Parameter übergeben werden. Das sieht dann so aus: int result = [myObject multiplyFirstValue: 2 withSecondValue: 4 andAddThirdValue: 8];
Die eckigen Klammern bilden quasi den Rahmen für den Methodenaufruf. Weitere Parameter erfordern im Unterschied zu Java oder C/C++ auch eine Erweiterung des Methodennamens. Die obige Methode lautet also multiplyFirstValue:withSecondValue:andAddThirdValue:
und liefert einen Integer-Wert als Rückgabe. Die Lesbarkeit von Quelltexten kann durch aussagekräftige Methodennamen verbessert werden: Ohne einen Blick auf die Deklaration zu werfen, können wir erkennen, dass die Methode offenbar den Wert 2 mit 4 multipliziert und 8 dazu addiert. Der Rückgabewert wäre also 16. Ob Sie die einzelnen Teile des Methodennamens untereinander oder in eine Zeile schreiben (getrennt durch Space), hängt von Ihren Vorlieben (und dem verfügbaren Platz!) ab. Deklaration von Klassen Das zweite wichtige Merkmal von Objective-C ist die Verwendung von @-Direktiven. Dabei handelt es sich um Steuerbefehle für den Compiler, die mit einem @-Zeichen beginnen. So werden Strings stets mit einer @-Direktive eingeleitet. Und sie finden auch bei der Klassendeklaration Verwendung: Listing MyObject.h @interface MyObject : NSObject { float member1; float member2; } - (id) init; - (void) printString: (NSString *) string; - (int) multiplyFirstValue: (int) value1 withSecondValue: (int) value2 andAddThirdValue: (int) value3; @end
25
2 Grundlagen – Wie funktioniert das alles denn bloß? Der Header wird mit @interface eingeleitet und mit @end beendet. Member-Variablen (auch Instanzvariablen genannt) werden innerhalb der geschweiften Klammern deklariert, Methoden dagegen außerhalb. Die Methoden werden ohne geschweifte Klammern deklariert, also ohne den Implementierungsblock. Nach dem Minus, das die Methode als Instanzmethode kennzeichnet, folgen der Rückgabewert und dann die Methodensignatur. Member-Variablen dürfen nicht initialisiert werden. float member1 = 0.2; würde zum Beispiel zu einem Compiler-Fehler führen. Instanzvariablen sind automatisch als @protected deklariert. Andere mögliche Sichtbarkeitsbereiche wären zum Beispiel @public oder @private. Der Doppelpunkt in der ersten Zeile bedeutet, dass MyObject von NSObject (der Elternklasse aller Objekte) abgeleitet ist. MyObject "erbt" also alle Member und Methoden von NSObject. Listing MyObject.m #import "MyObject.h"
Analog dazu wird die Implementierung der Methoden durch @implementation eingeleitet und mit @end wieder beendet. Sowohl im Header als auch in der Implementierungsdatei
26
2.6 Keine Angst vor Objective-C – ein 15-Minuten-Schnellkurs müssen referenzierte Klassen bzw. Variablen importiert werden: Dazu muss der jeweilige Header, in dem diese Klassen bzw. Variablen deklariert sind, per Importanweisung, also durch #import "MyObject.h", angegeben werden. Im Gegensatz zu selbst definierten Klassen werden die mitgelieferten Bibliotheken des SDK von spitzen Klammern umgeben: #import . Erzeugung von Objekten Nun haben wir bereits einige Methoden in Aktion gesehen und die zugehörige Klasse vorgestellt. Doch wie wird nun aus der Klasse eine Objektinstanz erzeugt? Ganz einfach: MyObject *myObject = [[MyObject alloc] init]; //Object wurde erzeugt und kann nun Nachrichten verarbeiten [myObject printString: @"Objective-C ist cool!"];
Schauen Sie noch einmal kurz in die Implementierung der Klasse: - (id) init { member1 = 0.1; member2 = 0.2; return self; }
Zunächst einmal wird das Objekt mit alloc alloziiert, es wird Speicherplatz bereitgestellt, und wir erhalten eine Objektinstanz, auf die wir dann die init-Methode aufrufen. Objective-C verfügt über keine Konstruktoren; stattdessen müssen Sie selbst dafür sorgen, dass Instanzvariablen mit den gewünschten Werten initialisiert werden. Diesen Zweck erfüllt die init-Methode, wir hätten aber auch einen anderen Namen wählen und Parameter übergeben können. In diesem Fall wählen wir jedoch init und überschreiben damit die NSObject-init-Methode. Da wir das Objekt mit alloc bereits angelegt haben, steht uns mit self bereits eine Referenz auf uns selbst zur Verfügung. Falls bei der Allokation etwas schiefgeht (zum Beispiel nicht genug Speicher), können Sie auf Nummer sicher gehen und in der init-Methode nochmals prüfen, ob die Referenz5 tatsächlich vorhanden ist: if (self) { //alles OK } else { //Fehler: self = nil }
bedeutet, dass das Objekt nicht vorhanden ist und deshalb den Wahrheitswert false besitzt (Achtung: nil ist nicht dasselbe wie NULL und kann daher auch als legitimer Parameter verwendet werden). Der Rückgabewert von self ( = unsere Objektreferenz) ist id – eine id ist ein typenloses Objekt, sozusagen eine Wildcard für Objekte. Da wir das Objekt mit nil
MyObject *myObject = [[MyObject alloc] init];
5
Für den Fall, dass Sie nicht von NSObject ableiten und explizit den Konstruktor der Elternklasse einbeziehen wollen, können Sie auch das folgende Konstrukt verwenden: if (self = [super init]) {...}.
27
2 Grundlagen – Wie funktioniert das alles denn bloß? erzeugen, machen wir aus dem typenlosen id-Objekt, das uns die init-Methode zurückgeliefert hat, ein typisiertes Objekt vom Typ MyObject. Das *-Zeichen markiert die Variable als Zeiger. Üblicherweise werden Objekte stets über Ihren Speicherbereich referenziert. myObject ist also eine Zeigervariable und zeigt beispielsweise auf den Speicherbereich 0x5366a09. Instanz- und Klassenmethoden Wie Sie oben bereits gesehen haben, markiert das Minuszeichen unsere Methoden als Instanzmethoden. Deshalb können diese nur auf einem Objekt angewendet werden. Nun stellt sich die Frage, wie denn dann die alloc-Methode arbeitet, die ja nicht auf einem Objekt aufgerufen wird? Sie ahnen es vielleicht bereits: alloc ist als Klassenmethode deklariert. Klassenmethoden können direkt aufgerufen werden, ohne dass ein zugehöriges Objekt vorhanden ist, und werden durch ein vorangestellt Pluszeichen gekennzeichnet (bitte nicht verwechseln mit dem static-Keyword, das uns später noch begegnen wird). Der Blick in die Deklaration der alloc-Methode (Rechtsklick auf den Methodennamen im Quelltext -> "Jump to Definition") bestätigt dies: + (id) alloc;
Die Methode ist in NSObject implementiert und kann für jede davon abgeleitete Klasse direkt aufgerufen werden. liefert folgerichtig ein id-Objekt zurück. Wir hätten daher die myObject-Instanz alternativ auch so erzeugen können: [MyObject alloc]
Der id-Datentyp ist bereits als Zeiger-Struktur6 definiert, deshalb muss das *-Zeichen bei id weggelassen werden. Üblicherweise verwendet man für die Objekterzeugung aber den obigen verschachtelten Aufruf. Nun aber noch schnell ein kleines Beispiel, wie man eine Klassenmethode anlegen kann. Die Deklaration erfolgt im Header: + (int) multiply: (int) value1 with: (int) value2;
Und schließlich die Verwendung: int multiplyResult = [MyObject multiply: 16 with: 2]; NSLog(@"multiplyResult : %i", multiplyResult); //32
Sie können die multiply:with:-Methode also jederzeit direkt einsetzen, ohne erst auf die Objektinstanz zugreifen zu müssen. Dazu sind Sie allerdings auch gezwungen. Anders als
6 typedef struct objc_object { Class isa; } *id;
28
2.6 Keine Angst vor Objective-C – ein 15-Minuten-Schnellkurs bei den statischen Methoden in Java können Sie in Objective-C Klassenmethoden nicht auf einer Objektinstanz aufrufen, ohne dass sich der Compiler beschwert (dies gilt für C++ ebenso). Gedächtnistraining: Die Speicherverwaltung Ein wichtiges Thema für Neueinsteiger ist die fehlende Garbage Collection. Anders als bei der Mac OS X-Entwicklung oder in Programmiersprachen wie Java wird im iOS der benötigte Speicher für Objekte nicht automatisch wieder freigegeben, wenn diese nicht mehr benötigt werden. Dies kann zu Memory Leaks, also Speicherlöchern, führen. Mithilfe der Instrument-Anwendung (in Xcode: Run -> Run with Performance Tool -> Leaks) kann man den Speicherverbrauch zur Laufzeit eines Programms analysieren. Das Thema darf aber auch nicht überbewertet werden: Falls Ihr Programm nicht permanent neue Speicherbereiche anfordert und diese nicht freigibt, wird im schlimmsten Fall die Anwendung vom iPhone OS geschlossen. Danach muss der Anwender neu starten, und das Spiel geht von vorne los. Bevor Ihre Anwendung gewaltsam geschlossen wird, erhalten Sie freundlicherweise noch die applicationDidReceiveMemoryWarning:-Meldung. In dieser Methode, falls Sie diese in der Delegate-Klasse implementieren möchten, könnten Sie dann einen letzten Versuch zur Rettung unternehmen und Speicher freigeben. Aber für kleinere Spiele dürfte es Ihnen schwerfallen, die minimal vorhandenen 80 bis 128 MB an Speicherplatz auszureizen (ein Teil davon wird vom iOS belegt). Bei kleineren Speicherlöchern wird Ihre App auch nach mehreren Minuten noch problemlos weiterlaufen. Für professionelle Anwendungen empfiehlt sich natürlich, es gar nicht erst so weit kommen zu lassen. Doch wie gibt man Speicher eigentlich frei, und wann muss man darauf achten? Der wahrscheinlichste Anwendungsfall in der Spieleentwicklung wird das Erzeugen von Objekten zur Laufzeit eines Spieles sein: Neue Feinde erscheinen auf dem Screen und wollen den Spieler attackieren, werden besiegt, und weitere Feindeswellen rücken an. Wenn Sie sich entscheiden, die Objekte während des Spiels zu erzeugen, dann sollten Sie natürlich auch dafür sorgen, diese aufzuräumen, wenn sie nicht mehr gebraucht werden. Die Speicherfreigabe können Sie unmittelbar mit der release-Nachricht einleiten: MyObject *myObject = [[MyObject alloc] init]; ... [myObject release];
Als Faustregel kann man sich merken, dass für jedes Objekt, das mit alloc erzeugt wird, auch irgendwann ein release-Aufruf erfolgen muss. In diesem Zusammenhang ist Ihnen vielleicht die deallocate-Methode aufgefallen, die vom NSObject geerbt wird. Erhält ein beliebiges von NSObject abgeleitetes Objekt die release-Nachricht, so wird die des Objektes aufgerufen, und der vom Objekt eingenommene Speicherplatz wird freigegeben. Für größere Klassen können Sie daher die release-Aufrufe sammeln und in der überschriebenen dealloc-Methode gemeinsam aufrufen: dealloc-Methode
29
2 Grundlagen – Wie funktioniert das alles denn bloß? - (void)dealloc { //dealloc überschreiben [window release]; //aufräumen [super dealloc]; //Speicher freigeben }
So wird in der Delegate-Klasse das window-Objekt freigegeben, sobald auch das DelegateObjekt freigegeben wird. Mit [super dealloc] rufen Sie dann die Elternversion der Methode auf, die das Objekt zerstört. Wenn Sie die dealloc-Methode überschreiben, müssen Sie auch die super-Methode aufrufen, ansonsten würde der Speicher nicht freigegeben werden. Die Sache hat allerdings einen kleinen Haken: dealloc wird nur dann aufgerufen, wenn sich keine weiteren Referenzen mehr auf ein Objekt finden lassen. Dazu bietet Objective-C einen Referenzzähler an – retainCount. Mithilfe der retainCount-Nachricht erhalten Sie jederzeit die aktuelle Anzahl der Referenzen auf Ihr Objekt. Die release-Methode sorgt nicht unmittelbar für die Speicherfreigabe, sondern vermindert den Referenzzähler lediglich um 1. Sie können den Referenzzähler auch manuell erhöhen: Denn mit der retain-Nachricht erhöht sich der Wert um 1. Sobald der Referenzzähler den Wert 0 erreicht, wird automatisch die dealloc-Methode aufgerufen. Ein Beispiel: 1. Objekt obj wird mit alloc erzeugt: retainCount = 1 2. [obj release]; // retainCount = 0; 3. Die dealloc-Methode von obj wird aufgerufen, das Objekt wird zerstört. So liefert der folgende Code-Ausschnitt die angezeigte Konsolenausgabe: MyObject *test1 = [[MyObject alloc] init]; MyObject *test2 = [[MyObject alloc] init]; [test2 retain]; //um 1 erhöhen NSLog(@"retainCount test1: %i", [test1 retainCount]); NSLog(@"retainCount test2: %i", [test2 retainCount]); [test1 release]; [test2 release]; //um 1 vermindern NSLog(@"retainCount test2: %i", [test2 retainCount]); [test2 release]; //erst jetzt wird test2 dealloziert
Noch ein weiterer Hinweis zu Arrays: Wenn Sie mit NSArray arbeiten und Objekte in einem Array hinzufügen, wird der retainCount des Objektes um 1 erhöht. Sie müssen also unmittelbar danach die release-Nachricht an das Objekt senden. Erhält das NSArray
30
2.7 Der Lebenszyklus einer App wiederum die release-Nachricht, vermindert es den retainCount der gespeicherten Objekte lediglich um 1.7 Damit wollen wir unseren kleinen Rundumschlag in Sachen Objective-C vorerst beenden. Wichtige Themen wie Properties, Protokolle oder Kategorien haben wir ausgespart, da wir diese im weiteren Verlauf des Buches nicht benötigen. Andere Fragen wie zum Beispiel
Wie werden globale Konstanten angelegt? Wozu kann ich den Präprozessor nutzen? Wie funktioniert das mit den Arrays? haben wir in diesem Kapitel ausgespart und werden diese nachfolgend anhand konkreter Beispielprojekte beantworten.
2.7
Der Lebenszyklus einer App Die Hauptaufgabe der Delegate-Klasse besteht darin, auf Zustandsänderungen der App mit dem gewünschten Verhalten zu reagieren. Es bleibt Ihnen überlassen, inwiefern Sie davon Gebrauch machen möchten. Zum Beispiel können Sie ein laufendes Spiel unterbrechen, wenn die Anwendung auf einem iPhone läuft, das gerade einen Anruf oder eine SMS erhält oder aber in den Sleep-Modus gewechselt ist (zum Beispiel beim Schach, wenn längere Zeit keine Nutzereingabe erfolgt ist).
Abb. 2.3: iOS Application-Lebenszyklus Eine iOS-Anwendung kann sich in verschiedenen Zuständen befinden (siehe Grafik), wobei zwei davon für uns als App-Entwickler in den meisten Fällen ohne Bedeutung sind: Die Zustände "App wird initialisiert" und "App ist beendet" können wir ignorieren. Und den Wechsel in den Zustand "App ist aktiv" haben wir bereits in den bisherigen Beispielen 7
Manchmal, zum Beispiel wenn Sie mit Threads arbeiten, wissen Sie nicht genau, wann die Lebenszeit eines Objektes beendet werden soll. Für diesen Anwendungsfall bietet Objective-C den NSAutoreleasePool an.
31
2 Grundlagen – Wie funktioniert das alles denn bloß? behandelt. Alternativ kann hierbei die application:didFinishLaunchingWithOptions:Methode einsetzt werden (siehe Beispiel unten). Der Zustand "App ist inaktiv" bezieht sich auf die eingangs erwähnten UnterbrecherEreignisse: Erhält ein iPhone-Nutzer während des Spiels eine SMS oder einen Anruf, wird über das laufende Spiel ein Pop-up eingeblendet mit der Frage, ob der Spieler den Anruf annehmen oder die SMS jetzt lesen will. Bejaht er, wird die Anwendung bis iOS 3.x oder früher beendet. Verneint er, verschwindet das Pop-up. Für beide Ereignisse steht jeweils eine eigene Nachricht zur Verfügung: App wird unterbrochen -> applicationWillResignActive: App wird wieder aufgenommen -> applicationDidBecomeActive: Während der Anzeige des Pop-ups läuft die App allerdings im Hintergrund weiter, der User kann lediglich keine Eingaben mehr machen – wir müssen uns also selbst darum kümmern, beispielsweise ein gerade laufendes Spiel anzuhalten und den Spielstand sicherheitshalber zu speichern. Da das iPhone ab iOS 4.0 Anwendungen erlaubt, die im Hintergrund bestimmte Aufgaben ausführen können, während gleichzeitig andere Apps aktiv sind (Stichwort Multitasking), wurden hierfür zwei zusätzliche Nachrichten eingeführt: applicationWillEnterBackground: und das entsprechende Gegenstück applicationWillEnterForeground. Multitasking-Anwendungen dürften einen Sonderfall in der Spieleprogrammierung darstellen und werden daher hier von uns nicht weiter berücksichtigt. Die Nachricht applicationWillTerminate: wird für den Zustand "Die App wird geschlossen" gesendet. Ungefähr fünf Sekunden haben wir nun Zeit, um wichtige Daten zu speichern, zum Beispiel den aktuellen Spielstand. Eine weitere Nachricht bezieht sich nicht auf einen Zustand, sondern auf einen wichtigen Hinweis: applicationDidReceiveMemoryWarning:. Auch wenn Apple empfiehlt, diese Meldung zu implementieren, um mit dem Freigeben von Speicher angemessen reagieren zu können, sollten Spieleentwickler es gar nicht erst so weit kommen lassen. In einem laufenden Spiel können Sie nicht plötzlich die Anzahl der Feinde verringern, nur weil gerade der Speicher knapp wird. Hier helfen nur sorgfältiges Testen im Vorfeld und bei Bedarf das Anpassen des Spielkonzeptes. Insgesamt stehen uns damit sechs relevante Methoden zur Verfügung, die wir in einer Bespielanwendung wie folgt implementiert haben: Listing: Lifecycle-App @implementation LifecycleAppDelegate //App ist einsatzbereit, Variante 1 - (BOOL) application: (UIApplication *) application didFinishLaunchingWithOptions: (NSDictionary *) launchOptions { NSLog(@"application:didFinishLaunchingWithOptions: called."); [window makeKeyAndVisible]; return YES;
32
2.7 Der Lebenszyklus einer App } //App ist einsatzbereit, Variante 2 - (void) applicationDidFinishLaunching: (UIApplication *) application { NSLog(@"applicationDidFinishLaunching: called."); } //App wurde unterbrochen, läuft im Hintergrund aber weiter - (void) applicationWillResignActive: (UIApplication *) application { NSLog(@"applicationWillResignActive: called."); } //App wurde unterbrochen, die Unterbrechung ist nun aber wieder vorbei - (void) applicationDidBecomeActive: (UIApplication *) application { NSLog(@"applicationDidBecomeActive: called."); } //Der Speicher wird knapp - (void) applicationDidReceiveMemoryWarning: (UIApplication *) application { NSLog(@"applicationDidReceiveMemoryWarning: called."); } //App wird beendet (z.B. durch Drücken des Home-Buttons) - (void) applicationWillTerminate:(UIApplication *) application { NSLog(@"applicationWillTerminate: called."); } - (void) dealloc { [window release]; [super dealloc]; } @end
Der iPhone-Simulator erlaubt das Simulieren der einzelnen Zustände. Sehen Sie sich dazu die folgenden Konsolenausgaben an: 1. Anwendung wird gestartet über "Build and Run": 2010-04-26 22:20:05.780 Lifecycle[1617:207] application:didFinishLaunchingWithOptions: called. 2010-04-26 22:20:05.781 Lifecycle[1617:207] applicationDidBecomeActive: called.
2. Knapper Speicher wird simuliert. Gehen Sie im iPhone-Simulator-Menü auf „Hardware“ und dann auf "Speicherwarnhinweis simulieren": 2010-04-26 22:20:16.289 Lifecycle[1617:207] Received simulated memory warning. 2010-04-26 22:20:16.290 Lifecycle[1617:207] applicationDidReceiveMemoryWarning: called.
33
2 Grundlagen – Wie funktioniert das alles denn bloß? 3. Das Unterbrechen der Anwendung kann über "Sperren" simuliert werde. Klicken Sie „Hardware -> Sperren“: 2010-04-26 called.
22:20:21.932
Lifecycle[1617:207]
applicationWillResignActive:
4. Zum Entsperren schieben Sie nun den Slider nach rechts, "Slide to unlock": 2010-04-26 called.
22:20:23.743
Lifecycle[1617:207]
applicationDidBecomeActive:
5. Drücken Sie die Home-Taste des iPhone-Simulators: 2010-04-26 22:20:27.118 Lifecycle[1617:207] applicationWillTerminate: called.
Um die jeweiligen Methoden mit sinnvollem Code zu füllen, fehlt uns derzeit noch einiges an Wissen: Wie werden zum Beispiel Daten dauerhaft gespeichert? Wie kann ein laufendes Spiel unterbrochen werden? Beide Themen werden wir in späteren Kapiteln aufgreifen. Haben Sie noch ein wenig Geduld, denn als Nächstes werfen wir einen Blick auf das nicht minder wichtige „universale“ Thema iPhone vs. iPod touch vs. iPad.
2.8
Breite Unterstützung: Universale Apps Im Vergleich zu anderen mobilen Plattformen deckt das iOS SDK nur eine Handvoll Geräte ab. Trotzdem hat Apple mit dem Erscheinen des iPads den Begriff der "Universalen App" eingeführt, um eine Technik zu beschreiben, mit der alle Geräte der iPhone-Familie über eine einzige Binärdatei unterstützt werden können.8 Eine Einführung zu universalen Apps finden Sie unter
http://devimages.apple.com/iphone/resources/introductiontouniversalapps.pdf Insgesamt stehen Ihnen drei verschiedene Strategien zur Verfügung, um ein Spiel im App Store zu veröffentlichen:
8
34
1.
Als iPhone App: Das Spiel wird entweder für die jeweiligen Eigenschaften der verschiedenen iPhone- und iPod touch-Versionen optimiert oder orientiert sich an dem schwächsten Gerät der iOS-Devices, dem iPhone der ersten Generation. Dieses ist aufwärtskompatibel zu allen anderen Modellen. Die Unterstützung für iPad oder iPhone 4 bzw. iPod touch 4 erfolgt ohne weitere Anpassung automatisch: Spiele laufen in 320x480er-Auflösung zentriert in der Bildschirmmitte. Optional kann der User über einen Vergrößerungsbutton die Auflösung verdoppeln, sodass das Spiel Fullscreen angezeigt wird (die einzelnen Pixel werden dabei vergrößert).
2.
Als iPad App: Natürlich können Sie sich auch dafür entscheiden, Ihre App nur für das iPad oder das iPhone 4 zu entwickeln. Ab dem iPhone SDK 3.2 (iPad) bzw.
Auch in diesem Kapitel werden wir noch nicht auf spielespezifische Themen eingehen. Die behandelten Inhalte sind zudem nicht für das Verständnis der späteren Abschnitte nötig, sodass Sie dieses Kapitel auch zu einem späteren Zeitpunkt lesen können.
2.8 Breite Unterstützung: Universale Apps iOS SDK 4.0 (iPhone 4) ist dies möglich. Die anderen Geräte der iPhone-Familie müssen explizit ausgeschlossen werden, wenn das Spiel über iTunes Connect veröffentlicht wird. Dies ist zwingend, da die App ansonsten von Apple nicht in den App Store aufgenommen wird: Eine für 1024x768 Pixel optimierte ScreenAuflösung wäre nicht kompatibel zu den kleineren Modellen. 3.
Als Universale Applikation: Mit dem iPhone SDK 3.2 können Sie sowohl das iPad als auch die iPhone-/iPod touch-Geräte über ein einziges Binary unterstützen. Sie vereinen hierbei also die Resultate aus Variante 1 und 2. Dies wird unter anderem erreicht über verschiedene Techniken, mit denen Sie zur Laufzeit entscheiden, auf welchem Gerät das Spiel gerade läuft. Wir werden ein Beispiel dazu zeigen.
Abb. 2.3: Ein Spiel im iPad Simulator 3.2: Links Originalgröße, rechts: 2-fach vergrößert
Da Variante 2 nur das iPad oder wahlweise iPhone 4 und iPod touch 4 abdeckt, ist diese nur für Spiele zu empfehlen, die allein auf der größeren Auflösung Sinn machen (zum Beispiel ein Vier-Personen-iPad-Spiel, bei dem alle gemeinsam an einem Gerät sitzen). Aufgrund der geringeren Geräteabdeckung ist diese Variante trotzdem nicht unbedingt zu empfehlen. Variante 1 ist die unkomplizierteste Lösung, Sie verzichten dabei aber darauf, die Vorteile der größeren Screens von iPad/iPhone 4/iPod touch 4 zu unterstützen. Variante 3 hingegen sollten Sie nur in Betracht ziehen, wenn Sie den höheren Aufwand in Kauf nehmen wollen (größere Grafiken, andere Screen-Aufteilung, weniger/mehr Spielelemente auf dem Bildschirm etc.).
35
2 Grundlagen – Wie funktioniert das alles denn bloß? Außerdem sollten Sie bei Ihrer Planung unbedingt berücksichtigen, dass Sie für die explizite iPad-Unterstützung sowohl Breit- als auch Hochkantformat (Landscape- bzw. PorträtModus) berücksichtigen sollten. Für iPhone- bzw. iPod touch-Apps spricht Apple derzeit keine derartige Empfehlung aus. Um den Code der späteren Beispielprojekte nicht unnötig zu überfrachten, werden wir für die Beispiele in diesem Buch weitestgehend Variante 1 wählen und für die Zielauflösung 320x480 entwickeln sowie jeweils nur eine Screen-Orientierung unterstützen. Schließlich sollen die Beispiele möglichst übersichtlich und einsteigerfreundlich gehalten sein. Variante 1 garantiert außerdem, dass die Spiele ohne Anpassungsaufwand auf allen iOS-Geräten lauffähig sind. Welche Vorteile bietet das iPad für die Spieleentwicklung? Während für 320x480er-iPhone-Spiele auf dem zweifach größeren Display des iPhone 4 lediglich die Pixelanzahl durch das iOS verdoppelt wird, fällt die Verdopplung auf dem iPad stärker auf, da der Bildschirm ein anderes Seitenverhältnis besitzt und wesentlich größer dimensioniert ist. Dies hat Auswirkungen auf die Gestaltung von Apps – auch aus der Sicht der Spieleentwicklung ergeben sich aus der größeren Bildschirmabmessung des iPads9 einige Vorteile: Brettspiele, Strategiespiele oder Simulationsspiele profitieren vom größeren Screen, da mehr Informationen direkt auf dem Spielbildschirm platziert werden können. Für das eigentliche Spielgeschehen bleibt trotzdem noch genug Platz. Die Touch-Steuerung ist präziser, und es können kompliziertere Gesten eingesetzt werden, mit Beteiligung von vier oder fünf Fingern gleichzeitig (auch beidhändig). Split-Views wie beim Nintendo DS: So können gleichzeitig Übersichtskarten und das aktuelle Spielgeschehen dargestellt werden. Multiplayer-Spiele: Der größere Screen bietet genug Platz, um zwei bis vier Personenspiele an einem Gerät durchzuführen. Nicht nur für Hersteller von klassischen Brettspielen (wie Monopoly, Trivial Pursuit, Mensch ärgere Dich nicht, Schach) dürften sich hier neue Absatzwege ergeben. Für Strategiespiele mit vielen On-Screen-Interface-Elementen steht mehr Platz zur Verfügung, sodass auf verschachtelte Untermenüs verzichtet werden kann. Außerdem stehen neue iPad-spezifische UI-Elemente zur Verfügung, wie zum Beispiel Split View oder Popover.
9
36
Wer weitere Informationen über die iPad-App-spezifische Entwicklung sucht, findet unter http://developer.apple.com/ipad/sdk/ u.a. den "iPad Programming Guide", iPad Sample Code und die "iPad Human Interface Guidlines (HIG)".
2.8 Breite Unterstützung: Universale Apps Wie man plattformunabhängig programmiert Nun stellt sich natürlich die Frage, wie man denn universale Apps entwickelt. Folgende Voraussetzungen sollten erfüllt sein: Das Spiel läuft auf einem iPhone iPod touch/iPhone 4/iPod touch 4 automatisch in der 320x480er-Auflösung (alle Geräte verfügen in etwa über die gleichen ScreenAbmessungen; iPhone/iPod 4: Pixel werden verdoppelt). Auf einem iPad wird das Spiel nicht optional hochskaliert, sondern läuft direkt nach dem Start mit einer 1024x768-Screen-Auflösung. Sowohl Landscape als auch Porträt-Modus sollten unterstützt werden (dies liegt daran, dass es beim iPad keine Default-Orientierung gibt, beim iPhone und iPod touch ist die Default-Orientierung der Porträt-Modus). Conditional Coding Unterschiedliche Plattformen bringen nicht nur unterschiedliche Hardware-Spezifikationen mit sich, sondern auch unterschiedliche APIs. An dieser Stelle setzt die konditionale Programmierung an. Zur Laufzeit des Programms wird entschieden, ob eine bestimmte Schnittstelle zur Verfügung steht oder nicht. Je nach Ergebnis wird dann alternativer Code ausgeführt. So könnte eine App beispielsweise prüfen, ob die Kamera zur Verfügung steht. Auf einem iPod touch, der über keine Kamera verfügt, könnte stattdessen auf die PhotoLibrary des Users zugegriffen werden. Um zu überprüfen, ob eine Klasse existiert, können Sie die NSClassFromString()Funktion verwenden. Wird die Klasse nicht gefunden, liefert die Funktion nil zurück. Ist eine Klasse vorhanden, können zusätzlich deren Methoden überprüft werden. Jede von NSObject abgeleitete Klasse verfügt über die beiden Methoden instancesRespondToSelector und respondsToSelector, die YES oder NO zurückliefern, je nachdem, ob die angefragte Methode existiert oder nicht. Funktionen können direkt auf NULL geprüft werden. Mithilfe von UI_USER_INTERFACE_IDIOM() können Sie direkt abfragen, ob die App auf einem iPad läuft oder nicht. Im Falle eines iPads liefert die Funktion den Wert UIUserInterfaceIdiomPad. Schließlich können Sie noch mithilfe des Präprozessors den kompilierten Code verzweigen, sodass sich mit einem Xcode-Projekt unterschiedliche Versionen einer App bauen lassen. Zum Beispiel mit dem Makro #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 30200: Nachfolgender Code wird in diesem Fall nur dann kompiliert, wenn das Base SDK >= 3.2 ist. Das Base SDK kann in den Projekt-Settings eingestellt werden. Den aktuellen Gerätetyp und die iOS-Versionsnummer können Sie übrigens auch direkt abfragen:
37
2 Grundlagen – Wie funktioniert das alles denn bloß? UIDevice * device = [UIDevice currentDevice]; NSLog(@"Model: %@", device.model); NSLog(@"iOS Version: %@", device.systemVersion);
Dies liefert beispielsweise: 2010-05-03 16:45:09.455 My Universal App[7772:207] Model: iPad Simulator 2010-05-03 16:45:09.462 My Universal App[7772:207] iOS Version: 3.2
Apple empfiehlt jedoch, Code-Verzweigungen nicht allein auf Basis dieser Angaben durchzuführen. Falls Sie dies dennoch tun, sollten Sie Klassen, deren Existenz Sie nicht voraussetzen können, mit id dynamisch typisieren, anstatt den Klassennamen direkt zu verwenden ("id meineKlasse" anstatt zum Beispiel "CADisplayLink meineKlasse"). Ansonsten erhalten Sie einen Compiler-Fehler – vorausgesetzt, Sie haben unter Deployment Target den passenden Simulator ausgewählt. Wird die Klasse mit id typisiert, werden unbekannte Methodenaufrufe auf dieser Instanz lediglich mit einer Warnung quittiert, sollten aber dennoch nicht auf dem Device ausgeführt werden. Um die Ausführung zu verhindern, müssen Sie den Code anhand der Versionsnummer zur Laufzeit verzweigen. Wie vergleicht man nun aber die verschiedenen Versionsnummern? Ganz einfach: Da die iOS-Versionsnummer vom Datentyp NSString ist, können wir für einen Vergleich die NSString-Methode compare:options: einsetzen: NSString *deviceOS = [[UIDevice currentDevice] systemVersion]; if ([deviceOS compare: @"3.0" options: NSNumericSearch] == NSOrderedAscending) { //iOS < 3.0 } else { //iOS >= 3.0 }
Mit der optionalen Angabe NSNumericSearch legen wir fest, dass alle Zahlenwerte innerhalb eines Strings als numerischer Wert interpretiert werden sollen. Das Ergebnis des Vergleichs kann drei mögliche Werte liefern: NSOrderedAscending: der zu vergleichende String ist kleiner als das compareArgument (aufsteigende Ordnung) NSOrderedSame: beide String-Werte sind gleich NSOrderedDescending: der zu vergleichende String ist größer als das compareArgument (absteigende Ordnung) Ist nun eine neue Klasse ab einem bestimmten iOS verfügbar, so können Sie über die festlegen, dass deren Methoden nur für die neueren Systeme verwendet werden. Wir werden später im Kapitel zum Thema Spielschleife noch einen praktischen Anwendungsfall vorstellen. if-else-Verzweigung
Beispiel: Eine universale App entwickeln So viel zur Theorie. Sehen wir uns nun einmal ein Grundgerüst an, das die genannten Kriterien für universale Applikationen erfüllt. Ohne diese würde unsere App lediglich im sogenannten iPad-Kompatibilitätsmodus laufen, also von den iPad-spezifischen Gegebenheiten keinen Gebrauch machen.
38
2.8 Breite Unterstützung: Universale Apps Um bestehende Projekte anzupassen, bietet Xcode die Option "Project > Upgrade Current Target for iPad". Wir wollen für dieses Beispiel jedoch ein neues Grundgerüst anlegen. Legen Sie daher ein neues Projekt an über "File -> New Project". Erneut wählen wir als Grundlage die "Window-based Application". Wichtig ist nun, dass Sie im unteren Fensterabschnitt unter "Product" den Eintrag "Universal" auswählen. Voreingestellt ist "iPhone", alternativ können Sie auch nur "iPad" wählen, wenn Sie kleinere Auflösungen ausschließen wollen. Als Nächstes geben Sie den Dateinamen des neuen Projektes an, wir wählen dafür den Namen "UniversalApp", unter diesem Namen finden Sie das Projekt auch in den Buchbeispielen. Anschließend wählen wir unter "Project -> Set Active SDK" den iPhone Simulator 3.2 aus, sofern nicht bereits voreingestellt. Nach Ausführen des Grundgerüstes mittels "Build and Run" öffnet sich dann das Projekt im iPad-Simulator, wobei wir feststellen, dass das Fenster den gesamten Bereich des Simulators einnimmt und die 2x-Vergrößern-Taste fehlt. Es hat also geklappt, unser Projekt läuft nun in der 1024x768er-Auflösung des iPads. Beim Blick in die Projektstruktur fällt auf, dass Xcode je einen iPad- und einen iPhoneOrdner angelegt hat, in dem sich jeweils ein eigener App-Delegate und eine an die jeweilige Fenstergröße angepasste NIB-Datei befinden. Außerdem befindet sich die Plist-Tabelle nun in dem Ordner "Shared": Hierüber wird gesteuert, welcher App-Delegate mit welcher Fenstergröße nach Start der App ausgeführt wird. In der Plist-Tabelle finden Sie die neuen Einträge: Main nib file base name: MainWindow_iPhone Main nib file base name (iPad): MainWindow_iPad
Und in der XML-Ansicht: NSMainNibFile <string>MainWindow_iPhone NSMainNibFile~ipad <string>MainWindow_iPad
Hierüber wird das jeweils gültige Basisfenster angegeben. Darunter finden sich Angaben über die jeweils unterstützte Screen-Orientierung: "Supported interface orientations" enthält nur den Eintrag "Portrait (bottom home button)" und legt damit fest, dass für iPhone- und iPod touch-Geräte die Anwendung nur den Porträt-Modus unterstützen soll (der Home-Button befindet sich dann an der unteren Seite des Gerätes). Unter dem Eintrag "Supported interface orientations (iPad)" finden Sie wie erwartet vier mögliche Einträge: Portrait (bottom home button) Portrait (top home button) Landscape (left home button) Landscape (right home button) In der XML-Ansicht der Plist sieht das so aus: UISupportedInterfaceOrientations <array>
39
2 Grundlagen – Wie funktioniert das alles denn bloß? <string>UIInterfaceOrientationPortrait UISupportedInterfaceOrientations~ipad <array> <string>UIInterfaceOrientationPortrait <string>UIInterfaceOrientationPortraitUpsideDown <string>UIInterfaceOrientationLandscapeLeft <string>UIInterfaceOrientationLandscapeRight
Um die vier verschiedenen Orientierungen zu unterstützen, müssen Sie einen ViewController einsetzen. Tipp In Idealerweise sollten Sie Code-Verdopplungen beim Entwickeln einer universalen App vermeiden. Um eine gemeinsame Code-Basis nutzen zu können, legen Sie unter "Groups & Files" per Rechtsklick mit "Add -> New Group" einen neuen Ordner "Common" an. In diesen können Sie dann gemeinsam genutzten Code anlegen, auf den Sie vom jeweiligen AppDelegate verzweigen. Beim Importieren von Header-Files brauchen Sie den Ordner-Namen nicht explizit anzugeben.
Idealerweise sollten Sie Code-Verdopplungen beim Entwickeln einer universalen App vermeiden. Um eine gemeinsame Code-Basis nutzen zu können, legen Sie unter "Groups & Files" per Rechtsklick mit "Add -> New Group" einen neuen Ordner "Common" an. In diesen können Sie dann gemeinsam genutzten Code anlegen, auf den Sie vom jeweiligen App-Delegate verzweigen. Beim Importieren von Header-Files brauchen Sie den OrdnerNamen nicht explizit anzugeben. Für das Beispielprojekt passen wir zunächst die Plist an und fügen für den iPhone-Eintrag "Supported interface orientations" die vier möglichen Orientierungen hinzu. Alternativ können Sie für Universal Apps den Eintrag explizit mit der iPhone-Variante überschreiben, also "Supported interface orientations (iPhone)". Für den neu angelegten "Common"-Ordner wählen wir per Rechtsklick "Add -> New File" und wählen die UIViewController-Unterklasse aus. Wie der Name schon vermuten lässt, können Sie mit dieser Klasse die verschiedenen Views Ihrer App managen; unter anderem bietet die Klasse auch einen Listener für unterschiedliche Screen-Orientierungen. Das Häkchen unter der Option "Targeted for iPad" können Sie ignorieren. Den entstandenen Code passen wir dann wie folgt an (unser Augenmerk liegt dabei auf der shouldAutorotateToInterfaceOrientation:-Methode, die die Screen-Orientierung des Gerätes überwacht): Listing ViewController.h #import @interface ViewController : UIViewController { } @end
40
2.8 Breite Unterstützung: Universale Apps Listing ViewController.m #import "ViewController.h" @implementation ViewController - (BOOL) shouldAutorotateToInterfaceOrientation: interfaceOrientation { if (interfaceOrientation NSLog(@"Orientation: } if (interfaceOrientation NSLog(@"Orientation: } if (interfaceOrientation NSLog(@"Orientation: } if (interfaceOrientation NSLog(@"Orientation: }
Bei jeder Änderung der Orientierung (die über den Bewegungssensor des jeweiligen Gerätes erkannt wird) wird die neue Orientierung als enum-Konstante vom Typ UIInterfaceOrientation an die Methode gesendet. Wir brauchen den Wert dann nur noch mit der jeweiligen Konstante zu vergleichen, um herauszubekommen, in welcher Orientierung sich die App gerade befindet. UIInterfaceOrientationPortrait UIDeviceOrientationPortraitUpsideDown UIDeviceOrientationLandscapeRight UIDeviceOrientationLandscapeLeft Die vier möglichen Orientierungen decken sich dabei mit den Angaben aus der Plist. Als Rückgabewert geben wir YES zurück, um deutlich zu machen, dass die App jede Auflösung unterstützt.
41
2 Grundlagen – Wie funktioniert das alles denn bloß? Nun müssen wir den View-Controller nur noch in den jeweiligen App-Delegates von iPhone und iPad einbinden. Der Code ist bei beiden bis auf die _iPad/_iPhone-Suffixe identisch: Listing AppDelegate_iPad.h #import #import "ViewController.h" @interface AppDelegate_iPad : NSObject { UIWindow *window; ViewController *viewController; } @end Listing AppDelegate_iPad.m #import "AppDelegate_iPad.h" @implementation AppDelegate_iPad - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { NSLog(@"App launched on iPad!"); viewController = [[ViewController alloc] init]; [window addSubview: [viewController view]]; [window makeKeyAndVisible]; return YES; } - (void)dealloc { [viewController release]; [window release]; [super dealloc]; } @end
iPhone und iPad – Testen im Simulator Wenn Sie die App auf einem Device ausführen, wird automatisch die passende NIB-Datei eingebunden und so der passende Delegate ausgeführt. Doch wie testen wir dies im Simulator? Wenn Sie, wie bereits gezeigt, als "Active SDK" den iPhone Simulator 3.2 wählen, startet die Anwendung automatisch als iPad-App. Ab dem iOS SDK 4.0 sind ältere Simulatoren nicht mehr direkt anwählbar. Sie können stattdessen mit dem iPhone Simulator 4.0 für iPhone/iPod touch testen und mit dem iPhone Simulator 3.2 für iPad.
42
2.8 Breite Unterstützung: Universale Apps Über die Project Settings können drei verschiedene Angaben zum SDK gemacht werden: Architectures -> Base SDK: Sollte möglichst aktuell sein, also z.B. iPhone Device 3.2. Deployment -> Targeted Device Family: Hier muss bei Universal Apps "iPhone/iPad" ausgewählt sein. Ansonsten wählen Sie entweder nur "iPhone" oder "iPad". Diese Angabe bestimmt das SDK zwar nicht direkt, hat aber Einfluss auf die Einträge in der Plist-Tabelle (der UIDeviceFamily-Key wird dynamisch vom Build-System gesetzt). Deployment -> iPhone OS Deployment Target: Hier sollten Sie eine möglichst niedrige Angabe wählen. Dies bedeutet allerdings auch, dass die App nur auf Geräten ab dem angegebenen iOS läuft, auf älteren iPhone-Betriebssystemen startet die App nicht. Nun wird es Zeit, unsere Universal App auszuprobieren. Die Orientierung des Simulators können Sie in der Simulator-App unter Hardware -> Links drehen bzw. Hardware -> Rechts drehen ändern. Je nach gewähltem Simulator unter Active SDK erhalten Sie dann beispielsweise die folgenden Konsolenausgaben: [Session started at 2010-05-02 17:34:58 +0200.] 2010-05-02 17:35:01.405 UniversalApp[3750:207] App launched on iPad! 2010-05-02 17:35:01.432 UniversalApp[3750:207] Orientation: UIInterfaceOrientationPortrait 2010-05-02 17:35:06.950 UniversalApp[3750:207] Orientation: UIDeviceOrientationLandscapeLeft 2010-05-02 17:35:09.061 UniversalApp[3750:207] Orientation: UIDeviceOrientationPortraitUpsideDown 2010-05-02 17:35:11.463 UniversalApp[3750:207] Orientation: UIDeviceOrientationLandscapeRight 2010-05-02 17:35:13.592 UniversalApp[3750:207] Orientation: UIInterfaceOrientationPortrait [Session started at 2010-05-02 17:36:24 +0200.] 2010-05-02 17:36:25.149 UniversalApp[3800:207] App launched on iPhone! 2010-05-02 17:36:25.151 UniversalApp[3800:207] Orientation: UIInterfaceOrientationPortrait 2010-05-02 17:36:28.090 UniversalApp[3800:207] Orientation: UIDeviceOrientationLandscapeLeft 2010-05-02 17:36:30.183 UniversalApp[3800:207] Orientation: UIDeviceOrientationPortraitUpsideDown 2010-05-02 17:36:32.552 UniversalApp[3800:207] Orientation: UIDeviceOrientationLandscapeRight 2010-05-02 17:36:34.493 UniversalApp[3800:207] Orientation: UIInterfaceOrientationPortrait
43
2 Grundlagen – Wie funktioniert das alles denn bloß?
Abb. 2.4: Unabhängig von der Orientierung des Gerätes befinden sich Text und Statusleiste immer am oberen Rand.
Wie Sie sehen, haben wir die Statusleiste des Gerätes nicht ausgeblendet und können bei jeder Orientierungsänderung sehen, wie sich der Screen dreht, die Leiste aber dennoch am oberen Rand positioniert wird. Außerdem füllt die Anwendung auf dem iPad den kompletten Screenbereich aus (abzüglich Statusleiste). Wenn Sie nur eine bestimmte Orientierung für Ihr Spiel zulassen wollen, ist dies natürlich ebenfalls möglich. Für ein Spiel, dass nur im Landscape-Modus spielbar sein soll, ohne dass der Screen automatisch rotiert wird, müssen Sie die shouldAutorotateToInterfaceOrientation:-Methode anpassen: if (interfaceOrientation == UIDeviceOrientationLandscapeRight) { return YES; } else return NO;
Nur für die Landscape-Right-Orientierung wird YES zurückgegeben, alle anderen Orientierungen werden ignoriert. Die Methode wird mehrmals aufgerufen, so lange, bis eine unterstützte Orientierung gefunden wird; ansonsten wird als Fallback die Default-Orientierung gewählt. Deshalb können Sie hierüber beispielsweise den Start der Anwendung in der Landscape-Auflösung erzwingen. Die aktuelle Orientierung eines Gerätes kann jederzeit über das UIDevice abgefragt werden: if ([UIDevice currentDevice].orientation == UIDeviceOrientationLandscapeLeft) { NSLog(@"Current orientation: UIDeviceOrientationLandscapeLeft"); }
Die orientation-Eigenschaft der UIDevice-Instanz enthält stets die aktuelle Orientierung (Enum-Typ: UIDeviceOrientation). Insgesamt gibt es sieben verschiedene Werte. Sie
44
2.8 Breite Unterstützung: Universale Apps können so unter anderem abfragen, ob das Gerät gerade flach auf dem Tisch liegt und mit dem Display nach unten zeigt (zum Beispiel um einen automatischen Pause-Zustand des Games zu ermöglichen). Statusleiste ausschalten Für Spiele wollen Sie den Bildschirm wahrscheinlich in Fullscreen-Auflösung beanspruchen. Die Statusleiste kann direkt in der applicationDidFinishLaunching:-Methode deaktiviert werden, indem Sie dort die folgende Nachricht an die UIApplication-Instanz senden: [application setStatusBarHidden:YES];
Allerdings wissen wir ja bereits, dass die applicationDidFinishLaunching:-Methode relativ spät aufgerufen wird, nämlich wenn die App bereits initialisiert ist. Während dieser Start-up-Phase wäre die Leiste also noch zu sehen. Um direkt nach Klick auf den HomeButton die Leiste zu deaktivieren, sollten Sie einen entsprechenden Eintrag in der PlistDatei setzen: Fügen Sie eine Zeile hinzu und wählen als Key "Status bar is initially hidden". Als Wert brauchen Sie dann nur noch ein Häkchen zu setzen. Ressourcen hinzufügen: App-Icons und Splash-Images Bevor wir uns nun weiter mit der Orientierung beschäftigen und zeigen, wie man die Zeichenfläche automatisch drehen kann, sehen wir uns an, wie wir der App ein Icon hinzufügen können, das im Home-Screen angezeigt wird. Bis jetzt ist ja lediglich ein weißes Default-Bild zu sehen. Apple gibt als Empfehlung an, Bilddateien als PNG mit 24 Bit zu erstellen (also je 8 Bit für Rot, Grün, Blau plus Alphakanal für Transparenz). Größe App Icon iPhone: 57x57 Pixel Größe App Icon iPad: 72x72 Pixel Die Größenangaben stammen von Apple. Falls Ihr Icon eine andere Größe hat, wird es automatisch skaliert. Für das iPhone brauchen Sie lediglich eine Datei namens "Icon.png" einzubinden. Optional können Sie für das iPhone 4 noch ein Icon der Größe 114x114 Pixel hinzufügen. Jedoch: Da das Bild automatisch passend skaliert wird, können Sie es sich auch einfach machen und lediglich ein Icon für alle Geräte hinzufügen. Wir zeigen hier nur das generelle Vorgehen zum Einbinden unterschiedlicher Icon-Größen. Zukünftig werden sicherlich noch weitere Geräte mit weiteren Auflösungen hinzukommen. Trotzdem heißt es aufpassen: Das PNG darf keine transparenten Anteile besitzen, und es sollte eckige Kanten haben und keinen Gloss-Effekt besitzen, denn um allen Apps einen einheitlichen Look zu geben, werden diese Effekte vom iOS automatisch erzeugt (bei Bedarf können die Icon-Eigenschaften dennoch über die Plist konfiguriert werden).
45
2 Grundlagen – Wie funktioniert das alles denn bloß? Wir erzeugen mit einem Bildbearbeitungsprogramm (zum Beispiel Pixen oder Photoshop) zwei Icons: Icon.png Icon_72x72.png Doch wie fügt man die Bildressourcen unserem Xcode-Projekt hinzu? Nun, wir kopieren die Bilder zunächst in den Projekt-Ordner, und zwar außerhalb von Xcode über den Finder, da Xcode eine eigene File-Referenzierung verwendet (das sogenannte Application Bundle). Nun müssen wir die Dateien (das können auch Filme, Musikdateien oder HTMLFiles sein) über Xcode einbinden. Gehen Sie per Rechtsklick auf Groups & Files, und legen Sie einen neuen Ordner ("Add -> New Group") an: "Resources", dann nochmals per Rechtsklick die Bilddateien über "Add -> Existing Files" hinzufügen. Hier wählen Sie die Dateien aus, die bereits im Projektordner liegen. Um an die jeweilige Größe angepasste Icons zu verwenden, müssen wir ebenfalls die Plist verwenden. Wir löschen dort den Eintrag "Icon File", fügen stattdessen "Icon files" hinzu und legen die folgenden Key-Value-Paare an: Key Item 0: Icon.png Key Item 1: Icon_72x72.png Der Name für das iPad-Icon kann beliebig gewählt werden, er muss nur mit der Angabe in der Plist übereinstimmen. Das iPhone iOS wählt dann anhand der Plist die jeweils passenden Icons aus, indem es die Größe der Icons prüft. Für die Splash-Images können wir ähnlich vorgehen. Dabei handelt es sich um eine Grafik in Display-Größe, die während der Start-up-Phase der App angezeigt wird. Wenn Sie vor Anzeige des ersten Views noch weitere Ressourcen laden wollen, bleibt das Splash-Image weiterhin sichtbar und dient somit quasi als Preloader-Screen. Wir könnten uns das Leben nun wieder einfach machen und lediglich eine Datei namens "Default.png" hinzufügen. Genau wie beim Icon würde dieses Bild dann automatisch als Splash-Screen für alle Auflösungen angezeigt werden. Für das iPad sollten wir allerdings in der Plist noch zusätzlich die initiale Orientierung festlegen, also beispielsweise Initial interface orientation: Portrait (bottom home button)
Das iPhone startet ohnehin immer im Porträt-Modus. Doch wie gehen wir vor, wenn wir je nach Orientierung ein anderes Startbild zeigen wollen? Für die Universal-App soll je nach iPad-Orientierung das passende Start-up-Image angezeigt werden. Die Auswahl des passenden Bildes nimmt das iPhone iOS anhand der Größe des Bildes und der Namensgebung vor. Durch den Bewegungssensor weiß das Gerät, wie das iPad gehalten wird. Wir brauchen für Landscape bzw. Portrait also nur jeweils ein Bild anzufertigen. Da wir die Statusleiste nicht ausgeblendet haben, reduziert sich die Höhe/Breite von 1024 auf 1004. Splash iPhone: Default.png (320x480) Splash iPad 1: Default-Portrait.png (768x1004) Splash iPad 2: Default-Landscape.png (1004x768)
46
2.8 Breite Unterstützung: Universale Apps Wenn Sie die App nun im Simulator 3.2 (iPad) oder 4.0 (iPhone) ausführen, sehen Sie jeweils das passende Icon und den Splash-Screen in der richtigen Auflösung. Das iPhone startet stets im Porträt-Modus, während das iPad die zuletzt gewählte Orientierung erneut startet. Probieren Sie alle vier möglichen Orientierungen aus, und achten Sie darauf, dass das richtige Splash-Image angezeigt wird. Da wir nicht viele Ressourcen zu laden haben, ist das Splash-Image allerdings nur sehr kurz zu sehen. Apple empfiehlt daher, als StartBild den Folgescreen der App anzuzeigen, damit der Übergang nicht zu plötzlich erfolgt. In der Praxis gibt es jedoch meistens genug zu laden, sodass das Bild lange genug stehen bleibt. Viele Firmen zeigen hier zum Beispiel ein Hersteller-Logo an.
Abb. 2.5: Einträge in der Plist-Datei
App Name setzen Wie Sie vielleicht bemerkt haben, wird als Name unter dem Icon der Projektname angezeigt. Das ist nicht immer gewünscht. Der angezeigte Name verbirgt sich in der Plist unter dem Eintrag
47
2 Grundlagen – Wie funktioniert das alles denn bloß? Bundle display name: ${PRODUCT_NAME}
Um den voreingestellten String für ${PRODUCT_NAME} zu ändern, wählen Sie in Xcode "Project -> Edit Active Target 'UniversalApp'". Der nun geöffnete Build-Tab enthält unter "Packaging" den gewünschten Eintrag. Für "Product Name" wählen Sie beispielsweise als Value "My Universal App". Zu lange Namen werden in der Home-Screen-Ansicht durch "..." abgekürzt. Beim nächsten Start der App erscheint der neue Name unter dem Icon. Auflösung abfragen und Text ausgeben Um das Grundgerüst zu vervollständigen, fügen wir ein Hauptfenster hinzu, auf das wir später dann unser Spiel zeichnen können. Wir orientieren uns dabei an dem Hello World2Beispiel, bei dem ein Text auf einem View ausgegeben wurde. Da der View sowohl für iPad als auch iPhone genutzt werden soll, legen wir den View im Common-Ordner an, um Code-Verdopplungen zu vermeiden: Listing MainView.h #import @interface MainView : UIView { } @end Listing MainView.m #import "MainView.h" @implementation MainView - (void) drawRect: (CGRect) rect { int w = rect.size.width; int h = rect.size.height; NSLog(@"MainView -> drawRect: %ix%i", w, h); CGContextRef gc = UIGraphicsGetCurrentContext(); CGContextSetRGBFillColor(gc, 1, 1, 1, 1); NSString *str = @"Hello little bird!"; UIFont *uif = [UIFont systemFontOfSize:40]; [str drawAtPoint:CGPointMake(10, 10) withFont: uif]; } @end
Wie Sie sehen können, geben wir zunächst die aktuell zur Verfügung stehende ScreenAuflösung über einen NSLog-Aufruf aus. Die drawRect:-Methode wird, wie Sie bereits im Hello World2-Beispiel gesehen haben, stets mit dem Parameter CGRect aufgerufen. Dabei handelt es sich um den aktuell zur Verfügung stehenden rechteckigen Screen-Ausschnitt.
48
2.8 Breite Unterstützung: Universale Apps Wir können also, wenn wir unser Spiel rendern wollen, hierüber mit int w = rect.size.width; int h = rect.size.height;
die aktuelle Auflösung abfragen und die Positionen, an denen die Spielobjekte gezeichnet werden sollen, entsprechend anpassen. Um zu überprüfen, ob der View korrekt orientiert wird, geben wir den Text "Hello little bird!" unabhängig von der Geräteausrichtung in der oberen, linken Ecke aus, bei x = 10, y = 10. Wir werden im nächsten Kapitel noch genauer auf das Koordinatensystem und die Textausgabe eingehen. Für uns ist erst einmal nur wichtig, dass sich das Koordinatensystem bei jedem Orientierungswechsel automatisch mitdreht. Um den neuen View anzuzeigen, verwenden wir diesmal den View-Controller. Dieser enthält bereits selbst einen View, den wir mit dem neuen View überschreiben. Wir fügen dazu in der ViewController.m-Datei die folgende Methode hinzu: - (void) viewDidLoad { mainView = [[MainView alloc] initWithFrame: [UIScreen mainScreen].applicationFrame]; mainView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; self.view = mainView; [super viewDidLoad]; }
Die viewDidLoad-Methode entstammt der Elternklasse UIViewController und wird aufgerufen, sobald der View-Controller geladen ist. Die im Header angelegte mainViewVariable wird wie gewohnt initialisiert. Die Initialisierungsmethode initWithFrame: [UIScreen mainScreen].applicationFrame] liefert uns dabei über UIScreen als Ausgangsgröße die aktuelle Display-Größe. Die Angabe mainView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
in der nächsten Zeile ist besonders wichtig, da wir ja für jeden Orientierungswechsel die neuen Abmessungen verwenden wollen. Über die Bit-Maske wird dem View mitgeteilt, dass sich dieser bei jedem Render-Vorgang flexibel an die zur Verfügung stehende Auflösung anpassen soll. Mit self.view = mainView; ersetzen wir schließlich den View des View-Controllers durch unseren eigenen. Wir hätten stattdessen auch mit [[self view] addSubview: mainView]; einfach einen weiteren View darüber legen können, aber in der Spieleprogrammierung kommt man in der Regel mit einem View gut aus. Schließlich fügen wir in der shouldAutorotateToInterfaceOrientation:-Methode noch die Nachricht [mainView setNeedsDisplay];
hinzu, um sicherzustellen, dass der View bei jedem erneuten Orientierungswechsel neu gezeichnet wird. Die Nachricht setNeedsDisplay triggert die drawRect:-Methode des MainViews.
49
2 Grundlagen – Wie funktioniert das alles denn bloß? Wenn wir den Screen drehen, erhalten wir beispielsweise die folgenden Meldungen: 2010-05-03 16:16:38.396 My Universal App[7594:207] MainView -> drawRect: 1024x748 2010-05-03 16:16:39.974 My Universal App[7594:207] MainView -> drawRect: 768x1004
Da die Statusleiste am oberen Rand eingeblendet wird, ist die Auflösung für Landscape auf 1024x748 und für Portrait auf 768x1004 begrenzt. In der Praxis zeigt sich, dass die Unterstützung von unterschiedlichen Auflösungen einen erheblichen Mehraufwand bedeuten kann. Während des Spiels einen Orientierungswechsel zuzulassen, ist ohnehin keine gute Idee. Auf dem iPhone ist davon eher abzuraten, da hier – anders als beim iPad – keine Feststelltaste vorhanden ist (lediglich per Software ist dies möglich). Wenn Sie Ihr Spiel nur für eine Orientierung entwickeln möchten, können Sie zumindest darüber nachdenken, ob Sie eine 180-Grad-Drehung erlauben wollen. Hersteller wie Gameloft bieten dieses Feature standardmäßig an, und es ist, wie gezeigt, problemlos und ohne Zusatzaufwand zu realisieren. Sie müssen dafür nur jeweils die beiden Landscapebzw. Portrait-Orientierungen in der shouldAutorotateToInterfaceOrientation:Methode als unterstützt angeben, also zum Beispiel UIDeviceOrientationPortrait und UIDeviceOrientationPortraitUpsideDown, falls das Spiel nur im Porträt-Modus spielbar sein soll. Da der Screen automatisch rotiert, ändern sich die Auflösung und der Ursprung des Koordinatensystems nicht. Natürlich können Sie die wechselnde Bildschirmorientierung auch kreativ für Spiele einsetzen. Ein passendes Spielprinzip wäre zum Beispiel NOM (Living Mobile), ein Java ME-One-Thumb-J'n'R, das bisher noch nicht für iPhone & Co. portiert wurde: Der Spieler läuft hier jeweils am unteren Screen-Rand entlang. Erreicht man den rechten Rand, muss man das Device drehen. Für die iPhone-Umsetzung könnte man dies als Bedingung vorgeben, damit die Figur nicht stehen bleibt.
50
3 3 Spiele entwickeln – von 0 auf 180 3.1
Wie funktionieren Spiele? Nachdem wir nun einige Vorbereitungen getroffen haben und wissen, wie man XcodeProjekte und universale Applikationen für iPhone/iPod touch und iPad anlegen und ausführen kann, wollen wir uns nun mit dem eigentlichen Kernthema des Buches beschäftigen. Spiele sind grafische Anwendungen und haben daher auch ganz spezielle Anforderungen, die sich von der herkömmlichen Anwendungsentwicklung unterscheiden. So lassen sich Spiele zum Beispiel nicht mit dem Interface Builder erstellen bzw. durch den Einsatz der diversen UI-Komponenten, die das iOS SDK bereitstellt. Es sei denn, es handelt sich um ganz einfache Spiele, wie Rate- oder Quizspiele. Doch wie werden Spiele denn dann gemacht? In gewisser Weise wie Filme: Das bewegte Bild entsteht durch die schnelle Abfolge von Einzelbildern (Frames). Jedes Frame repräsentiert einen bestimmten Zustand des Spiels. Beispiel: Frame 1 Abfrage: Wo hat der User den Screen berührt? Bewege den Spieler und die Spielfiguren Bewege die Gegner Abfrage: Haben sich Gegner und Spieler berührt? Addiere die Punkte für getroffene Gegner … Zeichne die aktuellen Positionen der Spielelemente auf dem Screen
51
3 Spiele entwickeln – von 0 auf 180 Frame 2 Abfrage: Wo hat der User den Screen berührt? Bewege den Spieler und die Spielfiguren Bewege die Gegner Abfrage: Haben sich Gegner und Spieler berührt? Addiere die Punkte für getroffene Gegner … Zeichne die aktuellen Positionen der Spielelemente auf dem Screen Frame 3 Abfrage: Wo hat der User den Screen berührt? Bewege den Spieler und die Spielfiguren Bewege die Gegner Abfrage: Haben sich Gegner und Spieler berührt? Addiere die Punkte für getroffene Gegner … Zeichne die aktuellen Positionen der Spielelemente auf dem Screen Und so weiter. Bevor die Spielelemente auf dem Screen gerendert werden, wird also das Touch-Display abgefragt – Spiele sind schließlich interaktive Anwendungen und müssen auf Benutzereingaben reagieren. Weiterhin wird der aktuelle Spielzustand berechnet und auf dem Screen angezeigt. Da sich dieser Vorgang mehrere Male pro Sekunde wiederholt, spricht man dabei auch von einer Spielschleife (Game Loop). Um die Positionen der Spielelemente zwischen den Frames zu berechnen, existieren verschiedene Ansätze. In der Praxis wird häufig ein Offset definiert, der pro Frame von einer Spielfigur zurückzulegen ist, sozusagen die Geschwindigkeit. Angenommen, ein PongBall soll von links nach rechts fliegen, und die Geschwindigkeit beträgt 5 Pixel pro Frame, dann würden wir die x-Position des Balls pro Frame so berechnen: x = x + 5
Dies würde beispielsweise die folgenden Positionen des Balls zur Folge haben: Frame 1: x = 0 Frame 2: x = 5 Frame 3: x = 10 …
Nun, damit hätten Sie dann schon Ihr erstes Objekt über den Bildschirm bewegt. Jetzt müssen Sie nur noch herausfinden, wie man ein Objekt auf dem Display anzeigen kann und eine Spielschleife erstellt. Daneben gibt es aber noch weitere Fragestellungen, die Sie beantworten müssen, unabhängig von der jeweiligen Plattform, für die Sie Ihr Spiel entwickeln wollen.
52
3.2 Das 2D-Koordinatensystem Was muss ich überhaupt wissen? Nützliche Fragestellungen Bevor man mit der Entwicklung für eine neue Plattform beginnt, ist es ratsam, sich erst einmal klarzumachen, was man überhaupt wissen möchte – zu schnell verliert man sich sonst in den zahlreichen Unter-APIs und lernt, wie man Internet-Verbindungen aufbaut oder eine SMS abschickt, verliert dabei aber doch recht schnell das eigentliche Ziel aus den Augen. Da es sich bei Spielen um grafische Anwendungen mit bewegten Bildern handelt, stellen sich uns die folgenden Fragen: Wie werden Grafiken wie zum Beispiel PNGs auf dem Screen zur Anzeige gebracht? Wie können Benutzereingaben, wie zum Beispiel Berührungen des Displays, erkannt und verarbeitet werden? Wie kann der Bildschirm regelmäßig neu gezeichnet werden, um eine Spielschleife zu erzeugen? Wie können Kollisionen zwischen verschiedenen Spielelementen festgestellt werden? Wenn Sie diese Fragen beantworten können, steht einem ersten Spiel (fast) nichts mehr im Weg. Natürlich werden wir im Folgenden auch weitergehende Themen ansprechen, wie zum Beispiel das Abspielen von Sounds, die dauerhafte Speicherung von Spielständen und die Erzeugung von animierten Sprites.
3.2
Das 2D-Koordinatensystem Alle Teile eines Spiels müssen auf dem Screen "gezeichnet" werden. Damit stellt die Screen-Größe sozusagen die Spielwiese dar, auf der wir uns grafisch und spielerisch austoben können. Diese Fläche wird auch als Leinwand (Canvas) oder Viewport (der sichtbare Bereich) bezeichnet – oder eben schlicht als View. Wie wir bereits im Kapitel über die Universal Apps gesehen haben, können je nach Gerätetyp unterschiedliche Orientierungen und Auflösungen unterstützt werden. Alle iOS-Geräte sind kompatibel zu der kleinsten Auflösung 320x480 (Breite x Höhe) im Porträt-Modus oder – seitlich gehalten wie bei einer klassischen mobilen Spielkonsole – 480x320 Pixel. Das Koordinatensystem, über das die Elemente auf dem Screen angeordnet werden können, geht dabei stets vom Ursprung in der linken, oberen Ecke des Screens aus. Dieser hat folgerichtig die Koordinaten x = 0 und y = 0. Wie im aus der Schule bekannten Koordinatensystem gibt es eine horizontale x-Achse und eine vertikale y-Achse. Die jeweilige Screen-Größe begrenzt den sichtbaren Bereich beider Achsen. Mit dem im Universal Apps-Kapitel beschriebenen Verfahren können Sie den Viewport je nach Bedarf nur im Quer- oder im Hochformat ausrichten – oder aber beide Orientierungen zulassen. Dadurch, dass sich dabei der Koordinatenursprung nicht ändert (er befindet sich
53
3 Spiele entwickeln – von 0 auf 180 immer oben links, egal wie Sie das Gerät ausrichten), vereinfacht sich die Unterstützung für unterschiedliche Auflösungen – Sie müssen nur noch zur Laufzeit den aktuell zur Verfügung stehenden Rahmen feststellen: Wie breit ist der Screen und wie hoch? Beide Werte können je nach Orientierung vertauscht sein. Auf keinen Fall sollten Sie in Ihrem Programmcode jedoch mit hart kodierten Breiten- und Höhenangaben operieren, um die spätere Anpassung an unterschiedliche Auflösungen nicht unnötig zu erschweren. Wie Sie die Breite bzw. die Höhe des Screens auslesen können, haben wir bereits kennengelernt. Dabei wird auch die Abmessung der Statusleiste (Uhrzeit, Batteriestand etc.) berücksichtigt. Für Spiele schaltet man diese jedoch in der Regel aus und erhält so den maximal verfügbaren Screen-Bereich.
Abb. 3.1: Das iOS-Koordinatensystem. Links: iPhone 3GS, rechts: iPad
Angenommen, Sie wollen einen Punkt in der Mitte des Screens zeichnen, so ergeben sich die xy-Koordinaten unabhängig von Orientierung und Bildschirmgröße wie folgt: int x = screenWidth/2; int y = screenHeight/2;
Und falls Sie ein Rechteck oder ein Bild stets mittig darstellen wollen, so ergäben sich folgende Koordinaten: int x = screenWidth/2-rectWidth/2; int y = screenHeight/2-rectHeight/2;
Wie Sie vielleicht bereits wissen, werden Rechtecke bzw. Bilder immer ausgehend von der linken, oberen Ecke des Rechtecks bzw. Bildes auf den Screen gerendert. Natürlich hört das Koordinatensystem nicht an den seitlichen Rändern auf: Sie können auch ein Objekt an die Position x = 9999, y = 9999 zeichnen, nur wäre es dann nicht sichtbar. Andererseits können Sie so Objekte von oben nach unten auf dem Screen "einfliegen" lassen. Doch Vorsicht: Ein Objekt an der Position x = 600, y = 600 mit einer Größe von
54
3.3 Ein Beispielprojekt aufsetzen 50x50 Pixeln wäre auf dem iPhone nicht sichtbar, auf dem iPad dagegen schon, allerdings nur, wenn Sie Ihre App als universale App oder explizit auf die iPad-Auflösung ausrichten. Das iOS SDK ist sehr genügsam. Wenn Sie Ihr Spiel nur für die Standardauflösung von 320x480 Pixeln entwickeln, wird die Anzeige auf anderen Geräten wie z.B. dem iPad optional vergrößert dargestellt (der User kann auf den 2x-Vergrößern-Button klicken). Dadurch ändert sich nicht die Breite/Höhe des Screens. Lediglich die Grafiken werden vom iPhone iOS hochskaliert, die Screen-Abmessungen bleiben gleich. Dies ist nicht selbstverständlich: Auf anderen mobilen Plattformen müssen in der Regel Anpassungen vorgenommen werden. Apple sorgt jedoch (derzeit noch) für die Abwärtskompatibilität und sichert neuen Produkten wie dem iPad oder dem iPhone 4 dadurch natürlich auch Zugang zum uneingeschränkten App-Portfolio des App Stores. Umgekehrt geht dies (noch) nicht: Spieleprojekte, die ausschließlich für das iPad oder iPhone 4 entwickelt wurden, lassen sich auf einem iPhone oder iPod touch nicht herunterladen.
3.3
Ein Beispielprojekt aufsetzen Unabhängig vom jeweiligen Spielkonzept gibt es wiederkehrende Aufgaben, die für alle Spiele gleich sind. Daher lohnt es sich, ein Template anzulegen, das einige elementare Bausteine enthält. Nach unserem jetzigen Kenntnisstand wären das: Ein App-Delegate Ein View zum Zeichnen des Spiels Eine Screen-Abfrage der aktuellen Bildschirmabmessung Ein Standard-Splash-Screen, der später bei Bedarf ausgetauscht werden kann Ein App-Icon für den Home-Screen Dieses Template werden wir auch im weiteren Verlauf des Buches einsetzen und fortlaufend erweitern. Xcode bietet Unterstützung für Template-Projekte, sodass Sie das Beispielprojekt direkt in den New Project-Template-Wizard integrieren können. Da wir bereits gesehen haben, dass der Splash-Screen und das Home-Screen-Icon ohne Anpassungen auch auf dem iPad eingesetzt werden können (und automatisch vom iOS skaliert werden), wählen wir die 320x480-Porträt-Auflösung, die von allen Geräten der iPhone-Familie ausgeführt werden kann. Im Universal App-Kapitel können Sie nachlesen, wie Sie andere Auflösungen und Orientierungen "nachrüsten" können. Nach dem Vorbild des Hello World2-Beispiels legen wir ein neues Projekt mit einem AppDelegate und einem View an ("File -> New Project -> Product: iPhone -> Template: Window-based Application"), wir nennen das Projekt "MyGame". Über "Add -> New File" fügen wir zusätzlich zum App-Delegaten, den das Template für uns erzeugt, noch die MainView-Klasse hinzu. Danach passen wir den Code des Delegates und der MainView-Klasse an, sodass dieser identisch zum Hello World2-Beispiel ist. Wir verzichten dabei auf iOS 4.0-Methoden wie applicationDidEnterBackground: oder
55
3 Spiele entwickeln – von 0 auf 180 applicationWillEnterForeground:, da unser Spiel auch auf älteren iOS-Versionen laufen soll und das Multitasking-Feature nicht benötigt wird. Da sich User ein Update ihres iOS jederzeit kostenlos auf ihr Gerät spielen können, ist es nicht unbedingt nötig, kompatibel zum Ur-iOS 2.0 zu entwickeln, andererseits sollte man nicht ausschließlich das neueste iOS unterstützen. Selbst Apple empfiehlt, das Deployment Target auf eine ältere Version zu setzen. Und das wollen wir hiermit auch tun: Ein guter Kompromiss ist das iPhone iOS 3.1.2.
Über "Project -> Edit Active Target 'MyGame'" legen wir die folgenden projektspezifischen Angaben im Build-Tab fest: Architectures -> Base SDK: iPhone Device 4.0 Deployment -> Targeted Device Family: iPhone Deployment -> iPhone OS Deployment Target: iPhone OS 3.1.2 Packaging -> Product Name (z.B.): "Mein erstes Spiel" In der Plist-Datei können wir den Bundle Identifier vervollständigen com.yourcompany.$ {PRODUCT_NAME:rfc1034identifier}. Tragen Sie hier anstelle von yourcompany einfach den Namen Ihrer Homepage ein, also zum Beispiel com.qioo.${PRODUCT_NAME: rfc1034identifier}. Die Variable für PRODUCT_NAME haben Sie ja bereits unter "Product Name" in den "Active Target Settings" vorgenommen. Über die "Bundle ID" wird Ihr Projekt vom iPhone iOS identifiziert, und der Produktname wird unter dem App-Icon auf dem Gerät angezeigt, dieser muss also nicht identisch mit dem Projektnamen sein. Schalten Sie nun noch die Statusleiste initial aus, indem Sie den Key "Status bar is initially hidden" hinzufügen und unter "Value" das Häkchen setzen. Die restlichen Einstellungen lassen wir unverändert. Nun fügen wir noch App-Icon und Titelbild hinzu. Dazu brauchen wir lediglich zwei Dateien in unser Projektverzeichnis zu kopieren und in Xcode über "Add -> Existing Files" hinzuzufügen: Icon.png: 57x57 Pixel Default.png: 320x480 Pixel Wenn Sie das Projekt starten, sollten Sie den bereits bekannten Screen aus dem Hello World2-Beispiel sehen. Wie legt man globale Variablen an? Aufgrund der unterschiedlichen Abmessungen des Displays, ist es nicht ratsam, Breite und Höhe hart codiert in den Quelltexten einzusetzen. Stattdessen können Sie einmalig globale Variablen anlegen. Diese müssen in Objective-C außerhalb der Implementierung angelegt werden. Für den externen Zugriff aus anderen Klassen setzen wir das extern-Keyword ein: Listing MainView.h #import
56
3.3 Ein Beispielprojekt aufsetzen extern int W; extern int H; @interface MainView : UIView {} @end Listing MainView.m #import "MainView.h" int W = 320; //Default-Werte zuweisen int H = 480; @implementation MainView - (void) drawRect: (CGRect) rect { W = rect.size.width; //Aktuelle Abmessungen des Screens H = rect.size.height; NSLog(@"W %i",W); ... } @end
Wir deklarieren W und H außerhalb des Interface- bzw. Implementierungsblocks, um bequem aus jeder Klasse, die MainView.h importiert, die Variablen W und H verwenden zu können. Außerdem weisen wir in der drawRect:-Methode die Werte neu zu, sodass wir für jeden Aufruf die tatsächlichen Abmessungen berücksichtigen können. W, H sind daher global verwendbar, aber dennoch veränderbar (auch wenn wir bereits wissen, das sich die Werte für unser derzeitiges Projekt nicht ändern werden). Um unveränderliche Konstanten zu erzeugen, können Sie extern zusammen mit dem Schlüsselwort const einsetzen. Voilà, damit steht bereits unser erstes Template. Damit wir dieses auch wiederverwenden können, brauchen wir den gesamten Projekt-Folder lediglich in das Template-Verzeichnis von Xcode zu verschieben:
/Developer/Platforms/iPhoneOS.platform/Developer/Library/Xcode/ Project Templates/Application/ Duplizieren Sie per Rechtsklick den Projekt-Folder im Finder, und ziehen Sie den Ordner in das Template-Verzeichnis. Wenn Sie nun ein neues Projekt anlegen, erscheint das MyGame-Projekt im Template-Fenster.
57
3 Spiele entwickeln – von 0 auf 180
Abb 3.2: Xcode-Template-Fenster mit eigenem Template
Nach dem Vorbild der anderen Templates in diesem Verzeichnis können Sie zusätzlich noch eine XML-basierte Template-Plist-Datei ("TemplateChooser.plist") anlegen, die zusätzliche Optionen zulässt. Außerdem können Sie den Platzhalter ___PROJECTNAMEASIDENTIFIER___ überall dort einsetzen, wo Sie den neuen Projektnamen übernehmen wollen, zum Beispiel in Kommentaren, als Dateinamen oder direkt im Code: @interface ___PROJECTNAMEASIDENTIFIER___AppDelegate : NSObject { UIWindow *window; ... }
Wir verzichten der Einfachheit halber jedoch darauf und begnügen uns damit, nach dem Anlegen eines neuen Projektes den öffentlichen Produktnamen manuell in den "Project Target Settings" zu vergeben. Der Xcode-Projektname muss ohnehin beim Anlegen eines neuen Projektes von Ihnen zwingend gesetzt werden.
3.4
Zeichenkurs – einfache Formen rendern Beginnen wir nun mit der Programmierung der Zeichenoberfläche. Öffnen Sie Xcode, und legen Sie ein neues Projekt an, indem Sie das eben angelegte MyGame-Template anwählen ("File -> New Project -> MyGame -> Choose"). Geben Sie als Projektnamen "Zeichenkurs" an. Ändern Sie den öffentlichen Projektnamen: Project -> Edit Active Target "Zeichenkurs". Und dann im Build-Tab: Packaging -> Product Name: Zeichenkurs
Sie überschreiben damit den zuvor vergebenen Produktnamen des Tempates, also beispielsweise "Mein Spiel". Sobald wir das Projekt im Simulator starten, sehen wir ein neues Icon im Home-Screen, das diesmal mit "Zeichenkurs" untertitelt sein sollte.
58
3.4 Zeichenkurs – einfache Formen rendern Also dann, fangen wir an und legen wir unser Augenmerk auf die MainView-Klasse, die uns bereits den Zugriff auf den iOS-Screen erlaubt: - (void) drawRect: (CGRect) rect { W = rect.size.width; H = rect.size.height; }
Bisher passiert nicht viel: Die drawRect-Methode des Views wird einmalig aufgerufen und enthält mit dem rect-Parameter die Abmessungen der uns zur Verfügung stehenden Zeichenfläche WxH. Um nun tatsächlich etwas auf dem Display anzeigen zu können, müssen wir uns zunächst den Grafikkontext besorgen. Über diesen haben wir dann Zugriff auf das Bildschirmgerät und können mithilfe des Core Graphics Frameworks geometrische Formen, Bilder und Text ausgeben. Da Objective-C auf C aufsetzt, ist die Vermischung mit reinem C-Code ein typisches Merkmal des iOS SDK. Auch die Core Graphics bzw. Quartz 2D API macht da keine Ausnahme und enthält jede Menge C-Funktionen, aber keine Klassen. Quartz ist der Name der Grafik-Engine, auf die Core Graphics aufsetzt, beide Begriffe werden daher häufig synonym verwendet. Zum Nachschlagen empfiehlt sich der "Quartz 2D Programming Guide", den Sie unter
http://developer.apple.com/Mac/library/documentation/GraphicsImaging/Conceptual/ drawingwithquartz2d/Introduction/Introduction.html abrufen können. Die "Core Graphics Framework Reference" finden Sie hier:
http://developer.apple.com/iphone/library/documentation/CoreGraphics/Reference/ CoreGraphics_Framework/ Wie werden nicht die gesamten Möglichkeiten der Bibliothek auflisten, sondern stattdessen exemplarische Anwendungsfälle vorstellen, anhand derer Sie die Verwendung der API nachvollziehen und erste eigene Spielideen verwirklichen können. Wir interessieren uns für die grafischen Grundmuster: Linien, Rechtecke, Kreise und die Farbgebung dieser Elemente. Wie zeichnet man Linien? Zunächst ein einfaches Beispiel – um eine Linie mit Core Graphics von links oben nach rechts unten zu zeichnen, müssen Sie den folgenden Code in der drawRect:-Methode aufrufen: - (void) drawRect: (CGRect) rect { … CGContextRef gc = UIGraphicsGetCurrentContext(); CGContextSetRGBStrokeColor(gc, 1, 1, 1, 1); CGContextSetLineWidth(gc, 10.0); CGContextMoveToPoint(gc, 0, 0); CGContextAddLineToPoint(gc, W, H); CGContextStrokePath(gc); }
59
3 Spiele entwickeln – von 0 auf 180 Sechs Zeilen, nur um eine Linie zu zeichnen? Das mag etwas viel erscheinen, der zugrunde liegende Ablauf ist jedoch (fast) immer gleich aufgebaut: 1. Den aktuellen Grafikkontext holen. 2. Zeichenfarbe festlegen. 3. Linienstärke festlegen. 4. Einen Pfad anlegen. 5. Den Pfad auf den Screen mit den angegebenen Parametern rendern. Zunächst erhalten wir mit der parameterlosen UIGraphicsGetCurrentContext()-Funktion des UIKits eine Referenz vom Typ CGContextRef. Doch wozu benötigen wir überhaupt eine Referenz auf den aktuellen Kontext? Die Antwort ist einfach: Es gibt verschiedene Kontexte, auf denen Sie zeichnen können, zum Beispiel könnten Sie auf ein Offline-Bild rendern, das nur im Speicher vorhanden ist (einen Anwendungsfall werden wir später im OpenGL ES-Kapitel sehen). Im Moment wollen wir aber Zugriff auf das Display haben und holen uns daher die aktuell gültige Screen-Referenz. Diese wird dann für alle nachfolgenden Operationen an die jeweiligen Funktionen durchgereicht, wie zum Beispiel der CGContextSetRGBStrokeColor()-Funktion, die ebenfalls als ersten Parameter einen Kontext erwartet. Da die Default-Hintergrundfarbe des Screens Schwarz ist, wählen wir eine weiße Vordergrundfarbe. Core Graphics unterscheidet zwischen der Füllfarbe, die Sie zum Füllen von Formen verwenden können, und einer Strichfarbe: CGContextSetRGBStrokeColor(CGContextRef context, CGFloat red, CGFloat green, CGFloat blue, CGFloat alpha) CGContextSetRGBFillColor(CGContextRef context, CGFloat red, CGFloat green, CGFloat blue, CGFloat alpha)
Für Linien benötigen wir daher die Stroke-Variante. Farben und Transparenz angeben Farben werden nach dem RGB-Farbschema angegeben, der erste Parameter enthält den Rotanteil, der zweite den Grünanteil und der dritte den Blauanteil. Der Anteil reicht dabei von 0.0 (0% Farbanteil) bis 1.0 (100% Farbanteil): CGContextSetRGBStrokeColor(gc, 1, 0, 0, 1); //Rot CGContextSetRGBStrokeColor(gc, 0, 1, 0, 1); //Grün CGContextSetRGBStrokeColor(gc, 0, 0, 1, 1); //Blau
Analog dazu verhält sich der letzte Parameter, der von 0.0 (vollkommen transparent) bis 1.0 (nicht transparent) reicht. Diesen Wert bezeichnet man auch als Alphawert, und Sie können damit festlegen, wie durchsichtig ein Objekt wie beispielsweise eine Linie sein soll. Eine Linie mit Alphawert von 0.5 wäre semitransparent, und darunter liegende Linien würden am Schnittpunkt durchscheinen. Sie können sich diesen Parameter vorstellen als die Farbmenge, die benutzt wird, um eine Linie zu zeichnen: Mit Alpha = 0.1 würde nur ganz wenig Farbe aufgetragen werden, die Linie wäre fast unsichtbar. Alpha = 0.9 entspräche dagegen einer sehr hohen Deckkraft, die darunter liegende Zeichnungen nahezu vollkommen verdeckt.
60
3.4 Zeichenkurs – einfache Formen rendern Linienstärke Als Nächstes bestimmen wir die Linienstärke in Pixeln. Mit CGContextSetLineWidth(gc, 1.0);
legen Sie die Stärke auf einen Pixel fest, für unser Beispiel haben wir mit 10.0 dagegen eine ziemlich breite Linie gezeichnet. Um nun eine Linie zu zeichnen, müssen sowohl Anfangs- also auch Endpunkt der Linie angegeben werden. Aus der Sicht von Core Graphics legen Sie jedoch lediglich den Startpunkt fest und geben danach den Befehl, eine Linie zu einem nachfolgenden Punkt zu zeichnen. So würde der nachfolgende Aufruf CGContextMoveToPoint(gc, W/2, 0); CGContextAddLineToPoint(gc, 0, H/2); CGContextAddLineToPoint(gc, W, H/2); CGContextAddLineToPoint(gc, W/2, 0);
ein Dreieck auf dem Screen zur Anzeige bringen. Der Zeichenstift wird gleichsam nicht abgesetzt, jeder Endpunkt wird zum neuen Startpunkt. Bis zu diesem Zeitpunkt haben wir allerdings lediglich einen Pfad gesetzt. Um diesen Pfad tatsächlich Pixel für Pixel mit Farbe zu füllen, bedarf es abschließend des Aufrufes von: CGContextStrokePath(gc);
Also: Zeichne den Pfad mit den bereits angegebenen Parametern. Dabei verhält sich Core Graphics wie eine State-Machine – jeder neue Funktionsaufruf verändert den bereits vorhandenen Zustand des Systems. Wenn Sie einmalig eine Farbe setzen, so bleibt diese Farbe erhalten, bis die Farbe durch eine neue Farbe überschrieben wird. Zufallszahlen und abstrakte Kunst Mit diesem Wissen und einer for-Schleife sollten wir bereits in der Lage sein, ein paar zufällige Linien auf den Screen zu zeichnen, nicht wahr? Zufallszahlen lassen sich mithilfe der arc4random()-Funktion aus der Standardbibliothek erzeugen. Gegenüber random() bietet diese Variante den Vorteil, dass kein Startwert gesetzt werden muss (seed). Die Funktion gibt allerdings mitunter sehr hohe Zahlen zurück, wie beispielsweise 1918726114. Diesen Zahlen können wir jedoch mithilfe des Modulo-Operators zu Leibe rücken. Eine Methode, die Zufallszahlen zwischen einer Untergrenze (bottom) und einer Obergrenze (top), einschließlich der Grenzen, zurückliefert, kann folgendermaßen angelegt werden: - (int) getRndBetween: (int) bottom and: (int) top { int rnd = bottom + (arc4random() % (top+1-bottom)); return rnd; }
Fügen Sie diese Methode der MainView.m-Implementierung hinzu, und deklarieren Sie diese im Header: @interface MainView : UIView {}
61
3 Spiele entwickeln – von 0 auf 180 - (int) getRndBetween: (int) bottom and: (int) top; @end
Dann kann der Spaß losgehen – rufen wir die Zufallszahlen-Methode doch mal testweise auf: for (int i=0; i<5; i++) { NSLog(@"%i", [self getRndBetween: -5 and: 5]); }
Der Aufruf könnte zum Beispiel die folgenden Werte ausspucken: 2010-05-09 2010-05-09 2010-05-09 2010-05-09 2010-05-09
Nun brauchen wir nur noch für jeden neuen Punkt der Linie einen zufälligen Wert innerhalb des sichtbaren Screen-Bereiches zu wählen, und fertig ist unser erstes "Kunstwerk": CGContextMoveToPoint(gc, W/2, H/2); for (int i=0; i<10; i++) { int x = [self getRndBetween: 0 and: W]; int y = [self getRndBetween: 0 and: H]; CGContextAddLineToPoint(gc, x, y); }
Experimentieren Sie doch einfach mal mit verschiedenen Farb- und Alphawerten. Sie werden damit zwar nicht zum nächsten Picasso, aber für Ihre ersten Versuche in der Welt der iOS-Computergrafik sehen die Ergebnisse doch gar nicht so schlecht aus. Wie zeichnet man Rechtecke? Neben Linien gibt es natürlich noch weitere Grundformen, die Sie einsetzen können. Klicken Sie im Quelltext einfach mal per Rechtsklick (Alternativ: Ctrl + Mausklick) auf eine CG...-Funktion und wählen "Jump to Definition" – im angezeigten CGContext.h finden Sie einige der verfügbaren Schnittstellen. Rechtecke sind als Formstruktur in CGGeometry.h deklariert. Diese können Sie auch direkt über "Groups & Files -> Frameworks -> CoreGraphics.frameworks" im Editor-Fenster anzeigen. Rechtecke stellen eine Struktur (Schlüsselwort struct) dar, bestehend aus einem Punkt (origin, oben rechts) und einer Größenangabe (size, Breite x Höhe): struct CGRect { CGPoint origin; CGSize size; }; typedef struct CGRect CGRect;
Um diese Struktur auf dem Screen zu rendern, benötigen wir eine spezielle CG-Funktion: void CGContextAddRect(CGContextRef c, CGRect rect);
Kombinieren wir beides, so können wir ein Rechteck auf dem Screen rendern: CGRect rectangle = CGRectMake (100,100,50,50); CGContextAddRect(gc, rectangle);
62
3.4 Zeichenkurs – einfache Formen rendern Damit zeichnen Sie ein Quadrat mit 50x50 Pixeln an die Position x = 100, y = 100. Wie Sie sich erinnern, wird die gleiche Rechteckstruktur auch an die drawRect:-Methode übergeben. Um den Display-Hintergrund einzufärben, genügt ein Aufruf von: CGContextSetRGBFillColor(gc, 1, 1, 1, 1); CGContextAddRect(gc, rect); CGContextDrawPath(gc, kCGPathFillStroke);
Die Funktion CGContextDrawPath() sorgt dafür, dass die Füllfarbe durch die Konstante kCGPathFillStroke zusätzlich zur Linienstärke beim Rendern berücksichtigt wird. Sie können diesen Aufruf daher alternativ zu CGContextStrokePath() verwenden. Dadurch werden alle Rechtecke mit der aktuellen Linienfarbe und Füllfarbe gezeichnet, sofern Sie gefüllte Rechtecke erzeugen wollen. Beachten Sie beim Rendern mit Core Graphics, dass jeder Funktionsaufruf den aktuellen Zustand verändert. Verwenden Sie also die Rechteck-Beispiele zusammen mit dem LinienBeispiel, wird aus allen Pfaden ein gemeinsames Bild erzeugt. Dies kann zu interessanten Resultaten führen. Ein Aufruf von CGContextDrawPath(gc, kCGPathFillStroke) sorgt zum Beispiel bei sich überschneidenden Linien ebenfalls für gefüllte Flächen. Sie können dies vermeiden, indem Sie einen Pfad durch den unmittelbaren Aufruf von CGContextDrawPath(gc, kCGPathStroke) zur Anzeige bringen (kCGPathStroke bewirkt, dass die Linien ohne die Flächen zu füllen gezeichnet werden). Es ist empfehlenswert, die jeweiligen Zeichenvorgänge in eigenständige Methoden auszulagern. Zusätzlich stehen Ihnen mit CGContextSaveGState() und CGContextRestoreGState() Funktionen zur Verfügung, um den gegenwärtigen Zustand abzuspeichern (save) und wieder herzustellen (restore). Wie zeichnet man Kreise? Kreise oder auch Ellipsen werden ebenfalls über Rechtecke gezeichnet. „Moment“, werden Sie sich denken, „wie das?“ Nun, man gibt ein Rechteck vor, und die CGContextAddEllipseInRect()-Funktion zeichnet eine Ellipse passgenau in das Rechteck, sodass diese alle vier Seiten des Rechtecks berührt. Ein Kreis ist demnach ein Spezialfall, der genau dann entsteht, wenn es sich bei dem Rechteck um ein Quadrat handelt. Da wir einen Kreis in den meisten Fällen an seinem Mittelpunkt ausrichten wollen, legen wir eine neue Methode an, die für uns das passende Rechteck erzeugt: - (void) drawCircle: (int) x y: (int) y radius: (int) radius gc: (CGContextRef) gc { CGRect rect = CGRectMake (x-radius, y-radius, radius*2, radius*2); CGContextSetRGBFillColor(gc, 0,0,0,0); CGContextAddEllipseInRect(gc, rect); }
Die Methode rufen wir dann wie folgt auf: [self drawCircle: 100 y:200 radius: 50 gc: gc];
63
3 Spiele entwickeln – von 0 auf 180 Wir übergeben also den bereits abgefragten Grafikkontext gc sowie den Radius und die xy-Koordinaten, an denen der Mittelpunkt des Kreises positioniert werden soll. Interessant ist nun, dass wir erneut die Füllfarbe mit einem Alphawert von 0 angeben: Dadurch überschreiben wir die bereits gesetzte Hintergrundfarbe des aktuellen Zustandes, und wir stellen sicher, dass der Kreis ungefüllt gezeichnet wird. Bei nachfolgenden Grafikoperationen müssen wir dann natürlich aufpassen, dass die Füllfarbe bei Bedarf jeweils neu angepasst werden muss.1 Textausgabe (Fonts und Schriftgröße) Für unser Hello World-Beispiel haben wir ja bereits gesehen, wie wir Texte auf dem Screen rendern können: CGContextSetRGBFillColor(gc, 1, 1, 1, 1); NSString *str = @"Zeichenkurs"; UIFont *uif = [UIFont systemFontOfSize: 40]; [str drawAtPoint:CGPointMake(10, 10) withFont: uif];
Die Schriftfarbe wird allerdings nicht anhand der Linienfarbe bestimmt, sondern durch die Füllfarbe. Um den String zu rendern, greifen wir nicht auf die Core Graphics API zu, sondern verwenden die UIFont-Klasse des UIKits, die einfacher zu handhaben ist. Wie Sie an dem Beispiel sehen können, kapselt die Klasse auch den Zugriff auf verschiedene Schriftarten, in diesem Fall setzen wir lediglich die Systemschrift, zusammen mit einer Größenangabe. Den Render-Vorgang übernimmt dann die drawAtPoint:withFont:-Methode des NSString-Objektes. Um eine andere Schriftart zu wählen, rufen Sie die Klassenmethode fontWithName:size: der UIFont-Klasse auf, Beispiel: UIFont *uif = [UIFont fontWithName:@"Verdana-Italic" size: 40]; //Schriftstil: Verdana, kursiv
Die Methode erwartet als Schriftstil vordefinierte Strings. Eine Übersicht der vorhandenen Schriftarten finden Sie unter:
1 Core Graphics bietet darüber hinaus zahlreiche weitere Funktionen, so können Grafiken beispielsweise auch gekachelt ausgegeben werden – ein Effekt, der an eine Tilemap für Spielhintergründe erinnert. Sie können aber auch Farbverläufe erzeugen mit Alphaeffekten usw. Einen guten Überblick finden Sie in dem Beispielprogramm "QuartzDemo" von Apple, das Sie im Apple Dev Center finden:
Bilder einbinden, laden und anzeigen Auch für Bilder bzw. Bitmap-Grafiken bietet Core Graphics eine entsprechende Schnittstelle. Häufiger wird in der Praxis – wie bei Fonts auch – allerdings das Pendant des UIKits eingesetzt: UIImage. Da es sich bei Bildern um externe Ressourcen handelt, müssen wir diese zunächst in unser Xcode-Projekt einbinden. Doch woher bekommen wir eigentlich geeignete Grafiken für unser Spieleprojekt? Wenn Sie nicht zufällig über einen befreundeten Grafiker verfügen, kann die Erstellung professioneller Grafiken schnell teurer als die eigentliche Entwicklung werden. Aber aus der Not kann man auch eine Tugend machen. Die sogenannten Kritzelspiele haben in iTunes mittlerweile sogar eine eigene Rubrik bekommen: Spiele, die aussehen, als wären sie wie zufällig während einer langweiligen Schulstunde entstanden. Ein bekanntes Beispiel ist zum Beispiel Doodle Jump. Wenn Sie über mehr Zeit und Muße verfügen, können Sie mit einem Zeichenprogramm wie Pixen, Gimp oder Photoshop eigene Pixelgrafiken erstellen. Das iPhone unterstützt weiche Alphakanten, sodass besonders bei kleinen Grafiken saubere und gerade Linien möglich sind, mit Schatteneffekten an den Rändern. Häufig wird aber auf solche Effekte verzichtet, um einen möglichst klaren Pixelstil zu kreieren, der an die großen Klasser aus der Nintendo NES-Ära erinnert.2
Abb 3.3: Entstehungsprozess des Autos: Zuerst die Outlines, dann flächig einfärben und Gradients hinzufügen
Typischerweise beginnt man beim Erstellen einer Spielegrafik zunächst mit der Outline, wie in dem obigen Beispiel zu sehen ist. Danach werden die Flächen einfarbig mit dem Füllwerkzeug ausgefüllt und schließlich mit Lichteffekten graduell verändert, um dem Objekt mehr Tiefe zu geben. Anschießend erfolgt bei Bedarf das Feintuning mit den Wedelund/oder Wischwerkzeugen des jeweiligen Grafikprogramms. Häufig setzen Grafiker hierbei auch Grafiktabletts ein.3 2 Wer sich für Grafiken im 8-Bit-Style interessiert, findet im Buch des Grafikers NFGMan Inspiration, [NFGMan], http://www.rotovision.com/description.asp?isbn=2-940361-12-6. Grundlagen zum Erstellen eigener Spielegrafiken vermittelt das kostenlose Buch von [Feldman]. 3 Tipp: Getreu dem Motto "für fast alles gibt es eine App", können Sie auch aus dem iPad ein Grafiktablett machen. Der App Store wimmelt vor Zeichenprogrammen, wie zum Beispiel der kostenlosen Software Adobe Ideas, die leicht zu bedienen ist und mit der sich Outlines für Spielegrafiken bequem mit dem Finger zeichnen lassen. Die Ergebnisse können später sogar exportiert werden, um zum Beispiel in Photoshop weiterbearbeitet zu werden. Ähnliche Funktionen, jedoch speziell ausgelegt auf Pixelschubserei, bieten Apps wie DotEDITOR oder C64 Paint XL. Apple selbst empfiehlt die folgenden GrafikTools: http://itunes.apple.com/WebObjects/MZStore.woa/wa/viewRoom?fcId=363454307
65
3 Spiele entwickeln – von 0 auf 180 Um unnötiges Freistellen zu vermeiden, sollten Sie die Grafiken vor einem volltransparenten Hintergrund anfertigen. Das bevorzugte Format für die iPhone-Spieleentwicklung ist dabei das PNG-Format mit 24 Bit Farbtiefe (RGB plus Alpha). Sind die Bilder fertig gepixelt (und freigestellt), ist das Anzeigen auf dem Display ähnlich einfach wie das Rendern geometrischer Formen und erfolgt direkt in der drawRect:Methode des Views. Zuvor müssen die Grafiken jedoch in das Xcode-Projekt eingebunden werden. Wir haben für das folgende Beispiel ein neues Projekt anhand des MyGame-Templates angelegt und schlicht "Bilder" genannt. Sie finden es wie immer im Download-Ordner. Es besteht aus dem obligatorischen App-Delegate und einer View-Klasse. Das oben gezeigte Auto-Beispiel färben wir in Photoshop mit dem Hue-/Saturation-Tool um, sodass wir drei verschiedene Farbvarianten (car_blue.png, car_green.png, car_red.png) erhalten. Wie Sie ja bereits erfahren haben, müssen Ressourcen und damit auch Grafiken in das Xcode-Projekt eingebunden werden. Damit Sie alle Projektressourcen immer zusammen mit Ihrem Projekt aufbewahren können, empfiehlt es sich, die drei PNGs zunächst in den Projekt-Ordner zu kopieren. In Xcode können Sie nun einen extra Grafikordner anlegen oder aber die Bilder in den bereits existierenden "Resources"-Ordner einbinden. Achten Sie darauf, dabei über Rechtsklick "Add -> Existing Resources" zu wählen, um die Grafiken als bereits vorhanden einzubinden. Achten Sie auf die im Screenshot gemachten Einstellungen:
Abb 3.4: Ressourcen über Xcode hinzufügen
Danach tauchen die Grafiken in der linken Groups & Files-Ansicht auf und werden in der Xcode-eigenen Vorschau angezeigt. Innerhalb von Xcode spielt der Ordner, in dem Sie die Bilder ablegen, keine Rolle. Das iPhone iOS sucht die Bilder selbstständig innerhalb des Ressourcenbündels.
66
3.5 Bilder einbinden, laden und anzeigen Um die Grafik auf das Display zu bringen, benutzen wir die UIImage-Klasse des UIKits. Fügen Sie den folgenden Code in der drawRect:-Methode hinzu: - (void) drawRect: (CGRect) rect { @try { UIImage *carBlueImage = [UIImage imageNamed: @"car_blue.png"]; [carBlueImage drawAtPoint: CGPointMake(140, 140)]; } @catch (id theException) { NSLog(@"ERROR: Bild nicht gefunden."); } }
Das war doch nicht schwer, oder? Die UIImage-Klasse bietet mit imageNamed eine Klassenmethode, die die Initialisierung anhand des Bildnamens vornimmt. Wie Sie sehen, wird keine Pfadangabe benötigt. Die Anzeige auf dem View erfolgt ebenso einfach über die drawAtPoint:-Methode, die als Punkt eine CGPoint-Struktur erwartet. Wenn Sie den Code ausführen, wird das Bild über die imageNamed:-Methode geladen und an der Position x = 140, y = 140 angezeigt. Um Fehler abzufangen, können Sie sicherheitshalber noch eine try-catch-Anweisung um den Code herumlegen.
Abb 3.5: Links: car_blue.png im iPhone-Simulator, rechts: zufällig erzeugter Fuhrpark
67
3 Spiele entwickeln – von 0 auf 180 Aus eins mach zwei mach drei ... Stellen wir doch mal einen ganzen Fuhrpark zusammen. Sobald Sie die UIImage-Instanz erzeugt haben, können Sie das Bild beliebig oft einsetzen. UIImage *carRedImage = [UIImage imageNamed: @"car_red.png"]; UIImage *carGreenImage = [UIImage imageNamed: @"car_green.png"]; for (int i=0; i<10; i++) { int x, y; x = [self getRndBetween: 0 and: W]; y = [self getRndBetween: 0 and: H]; [carBlueImage drawAtPoint: CGPointMake(x, y)]; x = [self getRndBetween: 0 and: W]; y = [self getRndBetween: 0 and: H]; [carRedImage drawAtPoint: CGPointMake(x, y)]; x = [self getRndBetween: 0 and: W]; y = [self getRndBetween: 0 and: H]; [carGreenImage drawAtPoint: CGPointMake(x, y)]; }
Nachdem wir zwei weitere Bilder geladen haben, setzen wir die getRndBetween:and:Methode aus dem letzten Kapitel ein, um zufällige Screen-Koordinaten zu erzeugen. In der for-Schleife rendern wir die jeweilige Auto-Instanz über deren drawAtPoint:-Methode an einer zufälligen Position. Wie Sie sehen, kann die Methode beliebig oft aufgerufen werden und bringt die Grafik auf dem View zur Anzeige. Weiterhin bietet uns die UIImage-Klasse die Möglichkeit, die Abmessungen eines bereits geladenen Bildes abzufragen: int picW = carBlueImage.size.width; int picH = carBlueImage.size.height; NSLog(@"Abmessungen Pic: %ix%i", picW, picH);
Das Bild wird normalerweise in der Originalgröße gerendert, über die drawInRect:Methode können Sie die Grafik allerdings auch skalieren – nachfolgend zweifach vergrößert: //Grafik skalieren [carBlueImage drawInRect: CGRectMake(120, 250, picW*2, picH*2)];
Die Grafik wird so skaliert, dass sie das angegebene Rechteck ausfüllt (PNG-Grafiken haben ja grundsätzlich ebenfalls eine rechteckige Form, auch wenn die transparenten Bereiche dies kaschieren). Ebenso einfach lassen sich Bilder transparent überblenden. Sowohl für die drawAtPoint:als auch die drawInRect:-Methode stehen Varianten bereit, für die zusätzlich ein Alphawert und eine Konstante für den Blendeffekt (hier: kCGBlendModeNormal) angegeben werden können: //Grafik transparent anzeigen [carBlueImage drawAtPoint: CGPointMake(40, 140) blendMode: kCGBlendModeNormal alpha: 0.4];
Abb 3.6: Das Auto vergrößert und 2-fach überblendet (links oben)
Da die Grafik leicht versetzt positioniert wird, scheint das darunter liegende Bild durch. Nicht gerührt, gedreht bitte! Für Rotationen bietet die UIImage-Klasse leider keine direkte Unterstützung an, aber mit ein paar kleinen Tricks lassen sich auch diese bewerkstelligen. Zunächst entscheiden wir uns dafür, den Code in eine eigene Methode auszulagern, die wir wie folgt deklarieren: - (void) rotateImage: x: y: angle:
(NSString*) picName (int) x (int) y (int) a;
Die Methode erwartet den Namen eines Bildes, die xy-Koordinaten, an denen die obere, linke Ecke des Bildes gezeichnet werden soll, und den Winkel in Grad (0° bis 360°), um den die Grafik – bezogen auf ihren Mittelpunkt – rotiert werden soll. Da Winkel in der Computergrafik häufig im Bogenmaß angegeben werden (Radiant), legen wir zuvor zwei Hilfsmethoden zur Umrechnung an: - (float) getRad: (float) grad { float rad = (M_PI / 180) * grad; return rad; } - (float) getGrad: (float) rad { float grad = (180 / M_PI) * rad; return grad; }
Wir benötigen dabei nur die getRad:-Methode, das Gegenstück haben wir nur der Vollständigkeit halber hinzugefügt. Wie Sie vielleicht noch aus der Schule wissen, entsprechen
69
3 Spiele entwickeln – von 0 auf 180 180 Grad der Kreiszahl Pi, daher auch die Bedeutung des Bogenmaßes. Anders ausgedrückt: float pi = [self getRad: 180];
180 Grad in Radiant umgerechnet ergibt Pi. Wie Sie an der Implementierung der Methoden sehen, steht uns über die BSD-Library (eine C-Library, die Teil des Core OS ist) mit M_PI bereits eine Pi-Konstante zur Verfügung, deklariert im math.h, zusammen mit weiteren mathematischen Funktionen, wie zum Beispiel Sinus: sin(), Cosinus: cos(), Logarithmus: log(), Rundung: floor() usw. Die Rotation des Bildes realisieren wir über einen Umweg: Statt des Bildes rotieren wir das gesamte Koordinatensystem. Wir greifen dabei auf Core Graphics zurück. Da wir ja bereits gesehen haben, dass Core Graphics wie eine kleine State-Machine arbeitet, müssen wir hier aufpassen: Ist das Koordinatensystem einmal rotiert, werden auch alle nachfolgenden Zeichenvorgänge beeinflusst. Stellen Sie sich ein Blatt Papier vor, auf das Sie Rechtecke zeichnen, jetzt rotieren Sie das Blatt um die obere, linke Ecke: Zeichnen Sie nun erneut auf dieses Blatt, orientieren Sie sich wahrscheinlich trotz der schiefen Lage an der Unterkante des Bildes. Damit nachfolgende Zeichnungen von der Rotation unbeeinflusst bleiben, müssen wir das Koordinatensystem an seine ursprüngliche Position schieben bzw. den alten Zustand wieder herstellen. Core Graphics bietet sowohl für die Rotation des Koordinatensystems als auch für dessen Zustandsspeicherung die folgenden Funktionen an: -
void void void void
CGContextSaveGState(CGContextRef c) CGContextRestoreGState(CGContextRef c) CGContextRotateCTM(CGContextRef c, CGFloat angle) CGContextTranslateCTM(CGContextRef c, CGFloat tx, CGFloat ty)
Da Rotationen des Koordinatensystem um den Ursprung (Display oben, links) herum herfolgen, benötigen wir zusätzlich eine Translate-Funktion, um das zu drehende Objekt mittig auf diesem Ursprung zu positionieren. Denn normalerweise befindet sich unser Objekt ja irgendwo auf dem Screen und nicht zufälligerweise zentriert an der Position x = 0, y = 0. Sowohl Rotation als auch Translation (= Verschiebung) beziehen sich auf die aktuelle Transformationsmatrize (Current Transformation Matrix, abgekürzt: CTM). Die Parameter tx bzw. ty geben an, um wie viel Pixel der Ursprung verschoben werden soll. Mit diesem Wissen können wir einen Algorithmus entwickeln, mit dem wir die Rotation einer beliebigen Grafik, die am Punkt xy gezeichnet werden soll, ausführen können: - (void) rotateImage: x: y: angle:
(NSString*) picName (int) x (int) y (int) a {
UIImage *pic = [UIImage imageNamed: picName]; if (pic) { int w = pic.size.width; int h = pic.size.height;
70
3.5 Bilder einbinden, laden und anzeigen
//1. aktuellen Grafikkontext speichern CGContextRef ctx = UIGraphicsGetCurrentContext(); CGContextSaveGState(ctx); //2. Neuer Koordinatenursprung: x+w/2, y+h/2 CGContextTranslateCTM(ctx, x+w/2, y+h/2); //3. Koordinatensystem rotieren CGContextRotateCTM(ctx, [self getRad: a]); //4. Grafik rendern an der Position 0-w/2, 0-h/2 [pic drawAtPoint: CGPointMake(0-w/2, 0-h/2)]; //5. alten Grafikkontext wieder herstellen CGContextRestoreGState(ctx); } }
Das Ziel der Methode ist, die Grafik um ihren Mittelpunkt herum zu drehen, deshalb benötigen wir zunächst die Abmessungen des Bildes (w, h). Danach speichern wir den aktuellen Grafikkontext. Dadurch können wir nun nach Belieben das Koordinatensystem ändern. Wir wissen, dass die Rotation um den Punkt 0, 0 herum erfolgt, deshalb verschieben wir den Ursprung mit der Translate-Methode an den Mittelpunkt der Grafik (bezogen auf das alte Koordinatensystem). Nun können wir beliebig rotieren und das Bild rendern, allerdings an der Position des neuen Koordinatenursprungs, das Bild wird mit 0-w/2, 0-h/2 mittig auf dem neuen Ursprung gezeichnet, der gleichzeitig auch der ursprünglich beabsichtigten Position auf dem Display entspricht – allerdings mit anderen Koordinaten (siehe Translate). Wir brauchen jetzt nur noch den alten Grafikkontext wieder herzustellen, und der Ursprung befindet sich wieder in der linken, oberen Ecke: Somit stimmen die Koordinaten des rotierten Bildes wieder. Den ganzen Vorgang packen wir schließlich in eine if-Abfrage: if (pic) { ... }
Falls der übergebene Bildname nicht gefunden wird, wird so die UIImage-Instanz mit dem Wert nil initialisiert, und wir können daher auf true oder false prüfen (nil entspricht dem Boole'schen Wahrheitswert false).
71
3 Spiele entwickeln – von 0 auf 180
Abb 3.7: Zufällig rotierte Autos Um die Methode aufzurufen, platzieren wir die folgende for-Schleife in der drawRect:-Methode des Views und erzeugen damit 20 zufällig rotierte Autos. for (int i=0; i<20; i++) { [self rotateImage: @"car_green.png" x: [self getRndBetween: 0 and: W] y: [self getRndBetween: 0 and: W] angle: [self getRndBetween: 0 and: 360]]; }
Da die Rotation über den Umweg des Koordinatensystems stattfindet (die Grafik wird eigentlich gar nicht selbst rotiert), können Sie nach diesem Prinzip natürlich auch andere Elemente rotieren, wie zum Beispiel Text oder geometrische Formen.
3.6
Game Loop und Frames – die Bilder zum Laufen bringen Die Game-Loop-Schleife bildet das Herz, den Schrittmacher eines jeden Spiels und sorgt dafür, dass sich die einzelnen Spielobjekte (Charaktere, Gegner, Gegenstände, Hintergründe etc.) für das Auge bewegen. Das Prinzip ähnelt einem Filmprojektor. Wir sagen zwar, dass ein Film "läuft", doch eigentlich läuft nur die Filmrolle. Der Filmstreifen selbst besteht nur aus einer Abfolge stehender Einzelbilder. Die Bewegung entsteht dadurch, dass die Einzelbilder (auch "Frames" genannt) sehr schnell hintereinander abgespielt werden. Um eine flüssige Bewegungsillusion zu erzeugen, benötigt man für das menschliche Auge 33 Einzelbilder pro Sekunde, abgekürzt als 33 FPS (Frames per Second). Damit die geforderten 33 FPS erreicht werden können, müssen mitunter Zugeständnisse an das Spieldesign gemacht werden, zu viele Gegner oder aufwändige Animationen können die Framerate nach unten ziehen: Das Spiel fängt an zu ruckeln.
72
3.6 Game Loop und Frames – die Bilder zum Laufen bringen Um eine stabil laufende Spielschleife zu implementieren, gibt es verschiedene Techniken. Die wichtigste Aufgabe besteht darin, jedes Frame in möglichst gleichmäßigen Abständen anzuzeigen. Anders als beim Film, kommt bei Spielen die Bewegung nicht aus der "Konserve", sondern wird während des Spiels pro Frame neu berechnet. Ein Frame entspricht dabei dem, was in der drawRect:-Methode des aktuellen Views zur Anzeige gebracht wird. Zeitbasierte Spielschleifen berechnen dabei pro Durchlauf die Zeit, die für den Rendervorgang benötigt wird, und passen die Bewegungsabläufe der Spielfiguren entsprechend an: Dadurch legen diese stets die gleichen Pixelentfernungen zurück, unabhängig davon, ob das Spiel auf einem sehr schnellen oder sehr langsamen System gespielt wird. Allerdings ist der Bewegungsablauf hierbei nicht unbedingt flüssig und abhängig von der Ausführungsgeschwindigkeit. Diese Schwankungen sind für schnelle Action-Spiele nicht akzeptabel. In der Praxis setzt man daher eher auf die frame-basierte Variante: Läuft das Spiel zu langsam, sinkt die Framerate, aber die Spielmechanik bleibt pro Frame unverändert und damit konsistent. Portierungen für langsame Systeme müssen bei Bedarf durch Anpassungen des Spieldesigns wegoptimiert werden (weniger Hintergrundgrafiken, keine Alphaschatten etc.). Das iOS SDK bietet zwei verschiedene Timer-Klassen an, die das gleichmäßige Aufrufen der drawRect:-Methode ermögichen. NSTimer: Auf allen iOS-Versionen verfügbar CADisplayLink: Seit iOS 3.1, Bestandteil des QuartzCore-Frameworks Der Einsatz der CADisplayLink-Klasse wird zwar von Apple empfohlen und bietet eine bessere Performance, ist aber nicht auf älteren iOS-Versionen verfügbar. Sehen wir uns daher zunächst die NSTimer-Variante an. Anhand des MyGame-Projektes legen wir ein neues Projekt namens "GameLoop" an. In der Delegate-Klasse fügen wir die folgenden Methoden hinzu: Listing MyGameAppDelegate.m #import "MyGameAppDelegate.h" @implementation MyGameAppDelegate - (void)applicationDidFinishLaunching:(UIApplication *)application { mainView = [[MainView alloc] initWithFrame: [UIScreen mainScreen].applicationFrame]; mainView.backgroundColor = [UIColor grayColor]; ... } - (void) startGameLoop { timer = [NSTimer scheduledTimerWithTimeInterval: target: selector: userInfo: repeats: }
Das war's schon. Damit ist die Spielschleife komplett. Die timer-Instanz haben wir im Header mit id timer;
deklariert (anstatt über NSTimer *timer;, da wir die timer-Instanz später je nach iOSVersion anhand einer anderen Klasse initialisieren werden). Die zweite Besonderheit liegt darin, dass wir mit mainView.backgroundColor = [UIColor grayColor];
explizit eine Hintergrundfarbe für den View setzen. Wenn Sie diese Angabe weglassen, wird der View für jeden erneuten Aufruf nicht komplett neu gezeichnet, sodass bewegte Grafiken Schlieren bilden. Besitzt der View dagegen eine Hintergrundfarbe, so wird die Fläche, die der View einnimmt, für jeden Rendervorgang komplett neu eingefärbt, so als ob wir vor einer komplett weißen Leinwand stünden, auf der wir unser Spiel zeichnen können (unabhängig vom vorherigen Frame). In diesem Fall wählen wir jedoch nicht die Farbe Weiß, sondern die Farbe Grau, die wir mithilfe der UIColor-Klasse direkt angeben können. Kommen wir zur eigentlichen Spielschleife. Wie Sie ja bereits wissen, kann der Spielfluss beispielsweise durch einen Telefonanruf unterbrochen werden. Damit das Spiel im Hintergrund nicht weiterläuft, setzen wir zum Starten und Stoppen der Spielschleife die Lifecycle-Methoden applicationDidBecomeActive: und applicationWillResignActive: ein. Wie Sie sehen, genügt zum Starten die Initialisierung des Timer-Objektes: timer = [NSTimer scheduledTimerWithTimeInterval: target: selector: userInfo: repeats:
0.0303 //33 FPS self @selector( loop ) nil YES];
Dieser "feuert" einen Aufruf an die unter target angegebene Klasseninstanz, in diesem Fall also die Delegate-Klasse. Die Nachricht wird über die Selektor-Direktive gesetzt – wir legen hier fest, dass die loop:-Methode der Delegate-Klasse aufgerufen werden soll, und zwar ununterbrochen und 33 Mal pro Sekunde: Das Intervall wird als double-Werte
74
3.6 Game Loop und Frames – die Bilder zum Laufen bringen (genauer: NSTimeInterval) in Sekunden angegeben: 1/33 = 0.0303. Den Parameter userInfo können wir ignorieren. Übrigens: Wenn Sie die tatsächliche Zeit messen wollen, die für die Ausführung des Rendervorgangs benötigt wird, so können Sie dies über die Systemzeit erreichen – das ist diejenige Zeit in Millisekunden, die seit dem 1. 1. 1970, dem Beginn der Zeitrechnung des Computerzeitalters, vergangen ist: double startTime = [[NSDate date] timeIntervalSince1970]; //rendern double endTime = [[NSDate date] timeIntervalSince1970];
Wenn Sie nun die Differenz zwischen startTime und endTime bilden, erhalten Sie die tatsächliche Ausführungsgeschwindigkeit. Sie könnten hier beispielsweise eine Warnung ausgeben, falls dieser Wert 33 FPS überschreitet. Um den Timer zu stoppen, rufen wir die invalidate-Methode auf und setzen die Instanz auf nil. Fertig. Den eigentlichen Rendervorgang stoßen wir in der loop:-Methode an, die über mainView.setNeedsDisplay;
unserem Haupt-View mitteilt, dass dieser neu gezeichnet und die drawRect:-Methode aufgerufen werden soll. Damit wir überprüfen können, dass die Spielschleife wie erwartet funktioniert, geben wir in der drawRect:-Methode an, wie oft die Methode aufgerufen wurde, und lassen zusätzlich ein Auto von oben nach unten fahren, mit einer Geschwindigkeit von 3 Pixeln pro Frame: - (void) drawRect: (CGRect) rect { W = rect.size.width; H = rect.size.height; static int cnt = 0; cnt++; NSLog(@"Game Loop: %i", cnt); if (!carBlueImage) { carBlueImage = [UIImage imageNamed: @"car_blue.png"]; } static int y = 0; y += 3; if (y > H) y = -100; [carBlueImage drawAtPoint: CGPointMake(W/2, y)]; }
Die Grafik haben wir diesmal im Header per UIImage *carBlueImage; deklariert, Falls die Variable carBlueImage noch nicht initialisiert wurde, laden wir diese über die imageNamed:-Methode und rendern die Grafik anschließend über drawAtPoint:. Wie Sie sehen, haben wir die y-Koordinate des Autos pro Aufruf um den Wert 3 erhöht. Ist y größer als die Höhe des Displays, setzen wir den Wert zurück, und das Auto fährt erneut von oben nach unten.
75
3 Spiele entwickeln – von 0 auf 180
Abb 3.8: Links: Schlierenbildung, da die Hintergrundfarbe (mainView.backgroundColor) nicht gesetzt wurde, rechts: saubere Zeichenfläche
Damit wir die y-Variable nicht im Header deklarieren müssen, haben wir hier, ebenso wie bei der cnt-Variablen, das static-Keyword eingesetzt. Dieses bewirkt, dass wir die Variable einmalig innerhalb der Methode deklarieren können. Wird die Methode aufgerufen, wird die Zeile ignoriert (die Variable ist bereits statisch initialisiert), und wir können die Variable wie eine Instanzvariable verwenden. Praktisch, oder? Anhand der cnt-Variable sehen Sie in der Debug-Konsole, dass die Spielschleife mehrmals pro Sekunde aufgerufen wird. Nun fehlt nur noch die Implementierung der CADisplayLink-Variante, die uns zusätzliche Performance-Stabilität verschafft. Da sich die Klasse ähnlich der NSTimer-Variante verhält, müssen wir lediglich die startGameLoop:-Methode anpassen: - (void) startGameLoop { NSString *deviceOS = [[UIDevice currentDevice] systemVersion]; if ([deviceOS compare: @"3.1" options: NSNumericSearch]
Anhand der iOS-Versionsnummer verzweigen wir für ältere Systemversionen auf die NSTimer-Variante, ab OS 3.1 ist dagegen die CADisplayLink-Klasse vorhanden und kann verwendet werden. Wenn Sie den Code mit einem 3.0-Simulator starten, können Sie über die NSLog-Ausgabe sehen, dass die NSTimer-Instanz verwendet wird. Die CompilerWarnungen bei Verwendung des 3.0-Simulators zeigen, dass wir alles richtig gemacht haben: Der else-Teil darf nicht ausgeführt werden. Zudem haben wir die timer-Variable als id-Typ deklariert und können so dynamisch den Klassentyp bestimmen. Hätten wir die Variable als CADisplayLink-Typ deklariert, würde der Compiler hier seinen Dienst verweigern. Da die CADisplayLink-Klasse Teil des QuartzCore-Frameworks ist, müssen wir dieses per Rechtsklick und "Add -> Existing Frameworks" hinzufügen. Falls Sie den Pfad überprüfen wollen, er lautet
/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOSXX.sdk/System/ Library/Frameworks/QuartzCore.framework XX steht für die verwendete SDK-Version, also zum Beispiel 3.2 oder 4.0. Um das Framework einzusetzen, brauchen Sie dieses dann nur noch im Header der DelegateKlasse bekannt zu machen über #import
Da das Framework auf allen iPhone iOS-Versionen verfügbar ist, müssen hier keine Probleme befürchtet werden. Die Initialisierung der CADisplayLink-Klasse erfolgt aus Kompatibilitätsgründen über den NSClassFromString()-Initialisierer. Danach setzen wir wie bisher die Target-Klasse und die aufzurufende Methode über die Selektor-Direktive. Das Intervall geben wir direkt an und fügen die timer-Instanz dem NSRunLoop-Pool hinzu. Das Stoppen des Loops erfolgt wie bisher über die invalidate-Nachricht. Der Vorteil der CADisplayLink-Klasse besteht darin, dass diese mit dem Display besser verlinkt ist und von daher für uns den passenden Spezialfall darstellt, da wir ja schließlich den View rendern wollen. Für nicht Display-bezogene Timer-Aufgaben können Sie dagegen weiterhin die NSTimer-Klasse einsetzen. Dies kommt allerdings in der Spieleprogrammierung so gut wie nie vor, da das Spiel und damit alle Elemente, die auf dem
77
3 Spiele entwickeln – von 0 auf 180 Spielfeld zur Anzeige gebracht werden, stets im Takt des Schrittmachers (= Game Loop) laufen sollen. Die hier gezeigte Implementierung ist universal für alle Geräte der iPhone-Plattform lauffähig. Allerdings sollten Sie berücksichtigen, dass die dynamische Zuweisung der timerInstanz Performance-Unterschiede zur Folge haben kann (je nach iOS des Zielgerätes, die NSTimer-Variante arbeitet schließlich etwas langsamer). Deshalb sollten Sie unbedingt beide Varianten testen. Andererseits können Sie sich natürlich auch nur für eine Variante entscheiden. NSTimer wäre dann die universale Lösung, um sicherzustellen, dass sich das Spiel auf allen iOS-Geräten gleich verhält. Falls Sie ausschließlich auf die CADisplayLinkKlasse setzen, würden Sie jedoch User mit älteren Geräten zu einem Update des iPhone iOS zwingen. Beachten Sie außerdem, dass die CADisplayLink-Variante sensibel auf NSLog-Aufrufe reagiert und dieses mit einem leichten Stottern quittieren kann. Die NSTimer-Methode ist dagegen in der Regel etwas immuner. Da es sich bei diesem Buch um ein Lehrbuch handelt und wir häufig NSLog-Konsolenausgaben zur Veranschaulichung des Programmablaufs einsetzen (wir gehen davon aus, dass Sie die Beispiele mit angeschlossenem USBKabel im Debug-Mode ausführen), werden wir bei den meisten nachfolgenden Beispielen die NSTimer-Variante über einen Flag erzwingen: bool forceTimerVariant = TRUE; if (forceTimerVariant || [deviceOS compare: @"3.1" options: NSNumericSearch] == NSOrderedAscending) { //use NSTimer } else { //use CADisplayLink }
Sie brauchen also nur den Flag forceTimerVariante auf FALSE zu setzen, um beide Varianten je nach iOS auszuprobieren.
3.7
Clipping und Animationen Wie man eine Grafik über den Bildschirm bewegen kann, haben wir nun gesehen. Aber was machen wir, wenn die Grafik selbst animiert sein soll? Zum Beispiel ein über den Screen spazierender Spielcharakter? Stellen wir uns ein Daumenkino vor: Pro Seite wird ein anderes Bild angezeigt – die Bewegungssequenz wird so in einzelne Bewegungsabschnitte unterteilt, die hintereinander abgespielt werden. Genau dieses Prinzip werden wir auch einsetzen. In der Spieleprogrammierung hat es sich dabei eingebürgert, die benötigten Einzelbilder für eine Animationssequenz in einer einzigen Bilddatei zusammenzufassen. Als Beispiel für dieses Kapitel haben wir einen kleinen, laufenden Zombie in Vogelperspektive erstellt:
78
3.7 Clipping und Animationen
Abb 3.9: Die Grafik "zombie_4f.png" mit vier Frames
Die Animation besteht aus vier Frames, der linke und der rechte Arm des Pixelzombies werden abwechselnd vorgestreckt sowie dazu entgegengesetzt die Füße, um die Laufbewegung darzustellen. Jedes Frame wird ausgehend von der oberen, linken Ecke gezeichnet. Um den Zombie auf dem iPhone-Screen zum (untoten) Leben zu erwecken, müssen wir zweierlei bewerkstelligen: 1) Clipping Das jeweilige Frame muss aus dem 4er-Filmstreifen herausgeschnitten werden. Dies können wir zur Laufzeit erledigen mittels Clipping. Wir werden gleich genauer darauf eingehen. 2) Die passende Frame-Nummer finden Warum dieses? Nun, die Animation muss geloopt abgespielt werden, um den Eindruck einer fortlaufenden Bewegung zu erzeugen. Bei einer Abspielgeschwindigkeit von 33 Frames pro Sekunde, die von der Game-Loop-Schleife getaktet werden, können die Frames nicht nacheinander hochgezählt werden. Der dadurch entstehende Bewegungsablauf wäre viel zu schnell und kaum sichtbar (so als würde Superman rennen). Wir müssen dafür sorgen, dass die Frames "verlangsamt" abgespielt werden. Das Ziel wäre zum Beispiel die folgende Variante: Frame Frame Frame Frame Frame Frame usw.
Jedes Frame wird als länger angezeigt oder, anders ausgedrückt: Der Wechsel des ZombieFrames erfolgt langsamer als die Taktung der Game-Loop-Frames. Im obigen Beispiel wird das Frame des Zombies alle drei Frames hochgezählt. Das erste Frame bleibt also für drei Frames unverändert sichtbar, bevor der nächste Abschnitt des Bewegungsablaufes angezeigt wird. Ein "Frame-Step" von fünf Frames hätte das folgende Muster: 00000, 11111, 22222, 33333, 00000, 11111, 22222, usw.
Eine derartige Zahlenfolge lässt sich durch einen einfachen Zähler realisieren. int int int int
cnt; frameNr; frameCnt; frameStep;
//interner Zaehler //aktuelles Frame //Anzahl Frames im Filmstreifen //Anzahl Frames pro Durchlauf
79
3 Spiele entwickeln – von 0 auf 180 - (int) updateFrame { if (frameStep == cnt) { cnt = 0; frameNr++; if (frameNr > frameCnt-1) { frameNr = 0; } } cnt++; return frameNr; }
Die Hilfsmethode wird pro Game-Loop-Frame aufgerufen und gibt die jeweils aktuelle Frame-Nummer frameNr zurück. Als Zähler fungiert dabei die Member-Variable cnt, die fortlaufend hochgezählt wird, so lange, bis das letzte Frame erreicht ist: frameCnt-1. Danach wird erneut von 0 hochgezählt. Über die if-Abfrage prüfen wir, ob der interne Zähler den Wert von frameStep erreicht hat. frameStep gibt an, wie oft eine Grafik unverändert angezeigt werden soll. Schließlich gibt uns die Methode die jeweils aktuelle frameNr zurück. Sie sehen, die gewünschte Zahlenfolge ist algorithmisch sehr einfach realisiert: Für die Geschwindigkeit, mit der eine Bewegungssequenz abläuft, kommt es in der Regel nicht auf eine millisekundengenaue Abfolge an. Stattdessen reicht die Angabe des Frame-Steps: Der minimale Wert von 1 entspricht der schnellstmöglichen Abfolge und ist synchron zum Takt der Spielschleife. Pro Frame wird ein Frame der Bewegungssequenz angezeigt. Je höher der Wert für frameStep gewählt wird, desto langsamer läuft die Animation ab. Normalerweise reicht es, mit diesem Wert zu experimentieren und denjenigen Wert zu wählen, der dem gewünschten Ergebnis am nächsten kommt. Wenn Sie die Zeit zwischen den Sequenzabfolgen genauer bestimmen wollen, können Sie dies anhand der Geschwindigkeit der Game-Loop-Schleife tun: Bei 33 FPS und frameStep = 33 würde pro Sekunde ein weiteres Bild des Filmstreifens angezeigt bzw. über Clipping isoliert dargestellt; bei 66 Frame-Steps alle zwei Sekunden und so weiter. Doch wie funktioniert diese Technik namens "Clipping" eigentlich? Wie so oft, erreichen wir das Ziel über einen kleinen Umweg. Das Ziel ist, mittels Clipping Bildteile in Form eines Rechtecks aus einem vorhandenen Bild "auszuschneiden" und auf dem Display zu rendern. Core Graphics enthält die CGContextClipToRect()-Funktion: void CGContextClipToRect(CGContextRef c, CGRect rect)
Die Methode erwartet einen Grafikkontext und eine Rechteckstruktur (x, y, Breite, Höhe), über die der Bereich des Bildschirms angegeben werden kann, der für nachfolgende Zeichenoperationen reserviert sein soll. Das Herausschneiden eines Frames aus unserem 4er-Zombie-Filmstreifen muss also über einen kleinen Trick erfolgen: UIImage *pic; CGPoint pos;
//Filmstreifen mit Frames //aktuelle Position
- (void) drawFrame { int picW = pic.size.width; int frameW = picW/frameCnt; int frameH = pic.size.height;
Die obige Code-Ausschnitt setzt ein UIImage namens pic voraus, wir werden später noch die zugehörige Klasse vorstellen. Da die CGContextClipToRect()-Funktion einen Bereich aus dem aktuellen Grafikkontext herausclippt, speichern wir vor Aufruf mittels CGContextSaveGState(ctx) den aktuellen Zustand und stellen diesen nach Abschluss des Render-Vorgangs wieder her mittels CGContextRestoreGState(ctx). Interessant sind nun einerseits der Rechteckausschnitt, der geclippt werden soll, sowie die Position, an der das Frame gezeichnet werden soll. Sehen wir uns die Parameter an. Der reservierte Screen-Bereich mittels Clipping: pos.x, pos.y: Die aktuelle Position des Zombies. Entspricht wie bei einem Einzelbild der linken, oberen Ecke. Da wir den Clipping-Bereich pixelgenau angeben wollen, runden wir die x-Position zuvor ganzzahlig mithilfe der rintf()-Funktion auf bzw. ab. Für die Renderposition ist ohnehin nur ein Integer-Wert möglich, da es keine halben Pixel gibt. frameW: Die tatsächliche Breite des Zombies. Da wir alle Bewegungssequenzen der Zombieanimation wie bei einem Filmstreifen horizontal angeordnet haben, können wir anhand der Anzahl der in unserem Fall vier Frames die tatsächliche Breite eines Zombie-Frames ermitteln. frameH: Die Höhe des Bildausschnitts entspricht der Höhe des Filmstreifens, da wir die Sequenz nur horizontal aufgereiht haben. Dies bedeutet also, dass wir auf unserer Zeichenfläche lediglich Platz für ein ZombieFrame freigeben. Nun müssen wir den Filmstreifen nur noch so positionieren, dass der mittels Clipping erlaubte Screen-Bereich deckungsgleich mit dem gewünschten Ausschnitt des Filmstreifens ist: x-Position des Filmstreifens: pos.x-frameNr*frameW y-Position des Filmstreifens: pos.y Die y-Position entspricht der Position des Sprites, logisch, denn wir haben ja nur horizontal angeordnete Frames. Die x-Position des Filmstreifens verschieben wir jedoch nach links, und zwar exakt um die Anzahl der Frames, die links vom gewünschten Ausschnitt stehen (frameNr*frameW). Würden wir das erste Zombie-Frame anzeigen wollen, so ergibt sich: x-Position des Filmstreifens: pos.x-0*frameW => pos.x Und für das letzte Frame: x-Position des Filmstreifens: pos.x-3*frameW
81
3 Spiele entwickeln – von 0 auf 180 Die Position lässt sich also sehr leicht anhand der Breite eines Frames und der FrameNummer bestimmen. Damit für nachfolgende Zeichenvorgänge wieder der komplette Screen-Bereich zur Verfügung steht, stellen wir mittels CGContextRestoreGState(ctx) abschließend wieder den alten Zustand des Grafikkontextes her. Die Clipping-Funktion arbeitet sehr tolerant. Wenn Sie außerhalb des Clipping-Bereiches zeichnen, wird der Rendervorgang ignoriert. Man kann sich das so vorstellen, dass der für Zeichenoperationen erlaubte Screen-Bereich mit dem Clipping-Bereich zusammenfällt: Alles, was in diesem Bereich gezeichnet wird, ist sichtbar, alles andere nicht. Teile des Clipping-Bereiches, die außerhalb des sichtbaren Screen-Bereiches bzw. des Viewports liegen, haben keine Auswirkung, sind aber auch erlaubt. Der geclippte Bereich ist immer rechteckig, außerhalb liegende Grafiken werden an den Grenzen des Bereichs "abgeschnitten". Diesen Effekt haben wir uns für die Animation zunutze gemacht. Prinzipiell können Sie durch Clipping-Operationen den zur Verfügung stehenden Screen-Bereich verkleinern, mit dem Unterschied, dass der außerhalb liegende Bereich noch sichtbar bleibt – natürlich nur, sofern der Clipping-Bereich kleiner als der Screen-Bereich ist. Clipping und Tiles Gerade auf kleineren Displays reicht der Platz für das Spielgeschehen oft nicht aus, und die Hintergründe müssen gescrollt werden. Falls Sie vorhaben, besonders große Spielwelten zu erzeugen, können Sie den Spielhintergrund auch aus einzelnen Kacheln, sogenannten Tiles, zusammensetzen. Sind die Tiles sehr kleinteilig, zum Beispiel verschiedene Bodentexturen mit je 24x24 Pixeln, dann können Sie diese ebenfalls wie in einem Filmstreifen anordnen und das jeweilige Tile über die Clipping-Technik herausschneiden. Für komplexere Hintergründe, beispielsweise ein Labyrinth mit unüberwindbaren Mauern, können Sie die Spielelemente anhand der Tile-Nummer identifizieren. Um solche Spielwelten zusammenzusetzen, werden klassischerweise Level-Editoren eingesetzt wie zum Beispiel das von der GameBoy-Entwicklung bekannte Programm Tile-Studio, das die jeweiligen Tiles anhand des Tile-Filmstreifens identifiziert. Ein weiterer Vorteil tile-basierter Level-Gestaltung ist die Speicherplatzersparnis, da keine großflächigen Bilder geladen werden müssen. Für die iPhone- und insbesondere die iPadEntwicklung steht jedoch in der Regel genügend Arbeitsspeicher zur Verfügung, und auch das Display bietet Platz genug, um auf scrollbare Hintergründe zu verzichten, was sich insbesondere auf die Übersichtlichkeit und eine leichter handhabbare Touch-Steuerung auswirkt. Denken Sie an Erfolgstitel wie Fieldrunners (Subatomic Studios) oder Flight Control (Firemint). Sprites anlegen Nach diesen theoretischen Überlegungen wird es nun wieder Zeit für ein weiteres Praxisbeispiel. Ausgehend von unserem Game-Loop-Grundgerüst, das Sie in den TemplateOrdner von Xcode ziehen können, legen wir ein neues Projekt an, das wir "Animation"
82
3.7 Clipping und Animationen nennen. Wie immer finden Sie dieses auch im Beispielordner, den Sie auf der BuchWebsite herunterladen können. Da wir bereits gesehen haben, dass für die Animation eines Spielobjektes mehrere Variablen benötigt werden (unter anderem die aktuelle Frame-Nummer, die Anzahl der Steps pro Frame usw.), empfiehlt es sich, hierfür eine eigene Klasse zu verwenden. Bewegte und/oder animierte Spielobjekte wie unseren kleinen Pixelzombie nennt man in der Spieleprogrammierung "Sprites". Also dann, legen wir flugs mittels Rechtsklick auf den Classes-Folder per "Add -> New File" eine neue Objective-C-Klasse hinzu, vergeben Sie den Namen "Sprite", und passen Sie den Header danach folgendermaßen an: Listing Sprite.h #import @interface Sprite : UIImage *pic; CGPoint speed; CGPoint pos; int cnt; int frameNr; int frameCnt; int frameStep; }
NSObject { //Filmstreifen mit Frames //Pixelgeschwindigkeit pro Frame in x-, y-Richtung //aktuelle Position //interner Zaehler //aktuelles Frame //Anzahl Frames im Filmstreifen //Anzahl Frames pro Durchlauf
Jedes Sprite soll über einen Filmstreifen verfügen, den wir schlicht "pic" nennen. Sodann verfügt jedes Sprite über eine xy-Screen-Position, die wir in einer CGPoint-Struktur vorhalten und zur Laufzeit des Spiels aktualisieren werden. Analog dazu verwenden wir die speed-Variable, über die wir festlegen, wie schnell sich ein Sprite über den Screen bewegen soll. Dies werden wir uns gleich noch genauer ansehen. Die restlichen Member der Klasse sollten Ihnen bekannt vorkommen und beziehen sich auf die Animationsphasen des Sprites.
83
3 Spiele entwickeln – von 0 auf 180 Auch zwei der vier Methodensignaturen sollten Ihnen bekannt vorkommen. Unser Sprite muss mit individuellen Werten initialisiert werden, dafür legen wir die initWithPic:frameCnt:frameStep:speed:pos:-Methode als Konstruktor an, die fünf Parameter entgegennimmt, mit denen wir die Eigenschaften eines Sprites bestimmen: das zu verwendende Bild in Form eines Filmstreifens (picName), die Anzahl der im Filmstreifen enthaltenen Frames (framCnt) sowie Geschwindigkeit der Animation: frameStep, die Sprite-Geschwindigkeit (speed) und die aktuelle Position, bezogen auf die linke, obere Ecke eines Frames (pos). Die draw-Methode sorgt dafür, dass das Sprite gerendert wird, und ruft die drawFrame-Methode auf, die sich wiederum über die Hilfsmethode updateFrame die aktuelle Frame-Nummer holt. Listing Sprite.m #import "Sprite.h" @implementation Sprite -(id) initWithPic: frameCnt: frameStep: speed: pos:
Die Implementierung enthält keine Überraschungen. drawFrame und updateFrame übernehmen wir unverändert. Die init-Methode weist wie erwartet den Membern die übergebenen Werte zu und ruft mit if (self = [super init]) { ... }
die init-Methode der Elternklasse NSObject auf. Da wir das Sprite alloziieren müssen (wie jedes andere Objekt auch), geben wir die Objektinstanz mittels return self zurück, und zwar, wie Sie an der Signatur sehen können, als generischen Objekttyp id. Diese Vorgehensweise ist für Objective-C üblich und erlaubt eine dynamischere Handhabung der Objekte. In der draw-Methode wird das Objekt schließlich entsprechend der speed-Struktur positioniert:
85
3 Spiele entwickeln – von 0 auf 180 pos.x+=speed.x; pos.y+=speed.y;
speed.x legt die Pixel-Entfernung fest, die pro Aufruf entlang x-Achse zurückgelegt werden sollen. Positive Werte bewegen das Sprite nach rechts, negative nach links. speed.y gibt dagegen die Pixel-Geschwindigkeit in Bezug auf die y-Achse an: Positive Werte lassen das Sprite nach unten wandern, negative nach oben. Mit CGPointMake(0, -2) können wir eine Punktstruktur erzeugen. Da der x-Wert gleich 0 ist und y = –2, bewegt sich das Objekt nach oben, mit einer Geschwindigkeit von 2 Pixeln pro Frame, falls wir die draw-Methode für jedes Frame aufrufen. Ein Aufruf von CGPointMake(-3, -3) würde das Sprite dagegen diagonal nach links oben wandern lassen. Die CGPointMake()-Funktion erwartet praktischerweise Parameter vom Typ CGFloat, falls Ihnen der Zombie mit einem Pixel pro Frame also immer noch zu schnell ist, können Sie ihn mit Werten zwischen –1 und 0 beliebig verlangsamen, beispielsweise durch CGPointMake(0, -0.5). Sie sehen, jedes Sprite-Objekt bewegt sich gleichsam autark vorwärts. Innerhalb der draw-Methode könnten Sie daher auch Kollisionsprüfungen mit Hindernissen oder anderen Sprites vornehmen. Während Core Animation für UI-Elemente eine große Hilfe beim Erstellen von Bedienoberflächen darstellt, fällt in der Spieleentwicklung die Nähe zur realen Welt stärker ins Gewicht. Einmal erzeugt, können Sie Sprites auf Ihre Spielwelt loslassen und mit etwas künstlicher Intelligenz bei jedem Schritt die Umwelt analysieren: Trifft die Spielfigur auf eine Goldmünze, kann es diese einsammeln, ein Mauerhindernis führt zu einer Richtungsänderung, ein Gegner wird automatisch attackiert usw. Für die Fortbewegung von Sprites empfiehlt es sich daher, den Bewegungsalgorithmus innerhalb des Objektes zu kapseln. Sobald das Sprite zum Leben erweckt wurde, kümmert es sich gleichsam um sich selbst. Sehr praktisch, diese Art der Elternschaft, nicht wahr? Zurück zu unserer Sprite-Klasse: Bevor wir diese einsetzen können, müssen wir noch das als Ressource in Xcode einbinden über Rechtsklick "Add -> Existing Files". zombie_4f.png
- (void) drawRect: (CGRect) rect { W = rect.size.width; H = rect.size.height; if (!zombie) { zombie = [[Sprite alloc] initWithPic: frameCnt: frameStep: speed: pos: } [zombie draw]; }
3.7 Clipping und Animationen Um unseren Zombie nun auf die Menschheit loszulassen, platzieren wir den obigen Code in der drawRect:-Methode der MainView-Klasse. Sie wissen ja, diese wird entsprechend des Taktes der Spielschleife 33 Mal pro Sekunde aufgerufen. Zur Kontrolle der Animation lassen wir uns noch pro Spielschleifen-Frame die aktuelle Frame-Nummer des Sprites ausgeben, die den folgenden Output in der Konsole erzeugt: 2010-05-24 2010-05-24 2010-05-24 2010-05-24 2010-05-24 2010-05-24 2010-05-24 2010-05-24 2010-05-24 2010-05-24 2010-05-24 2010-05-24 usw.
Es hat geklappt – jedes Frame wird beginnend beim ersten Frame = 0 fünfmal angezeigt, hochgezählt und beginnt wieder bei 0. Die Geschwindigkeit eines Zyklus ist gebunden an die eingestellten FPS und kann per Frame-Step variiert werden. Wir arbeiten also tatsächlich wie beim Film: Ein Frame wird nach dem anderen abgearbeitet, und der Projektor ( = Spielschleife) bestimmt, wie schnell der Film abgespielt wird. Die Deklaration des Zombies haben wir im Header mittels Sprite *zombie;
vorgenommen. Sofern der Zombie noch nicht initialisiert wurde, führen wir die ifAbfrage aus und übergeben einmalig die gewünschten Parameter. Die Startposition geben wir mit x = W/2 und y = 400 an, der Zombie wird also halbwegs in der Screenmitte platziert und hat genug Platz, um nach oben zu entweichen. Um den Zombie loslaufen zu lassen, genügt der Aufruf von [zombie draw];
87
3 Spiele entwickeln – von 0 auf 180
Abb 3.10: Zombies walk the earth …
Wie Sie sehen, ist unser Zombie vom Typ Sprite. Wir können beliebig viele Instanzen der Sprite-Klasse bilden und so unsere Zombiearmee vergrößern. Natürlich können Sie anstelle des Zombie-Filmstreifens auch ein anderes Bild übergeben und so weitere Sprites mit ganz anderen Charakteristika erzeugen. Wenn Sie Ihre Sprites um individuelle Fähigkeiten erweitern wollen, dann sollten Sie neue Klassen erstellen, die von Sprite abgeleitet sind. Wir werden später noch ein komplexeres Beispiel dafür zeigen. Organisation ist alles – Arrays Angenommen, wir wollen eine Armee von 20 Zombies erschaffen. Es wäre unklug, für jeden Zombie eine eigene Member-Variable anzulegen – der Code würde dadurch unnötig aufgebläht werden. Stattdessen können wir mithilfe von Arrays unsere Sprites zusammenfassen. Objective-C bietet hierfür zwei Klassen nichtassoziativer Arrays an: NSArray NSMutableArray
88
3.7 Clipping und Animationen NSMutableArray ist von NSArray abgeleitet und bietet Unterstützung für eine variable Anzahl an Elementen.
dagegen muss mit einer fixen Größe angelegt werden, Elemente können danach nicht mehr hinzugefügt werden. Die Performance von NSArray ist daher einen Tick schneller, dennoch: Da es in Spielen oft sehr flexibel zugeht und wir uns das Leben nicht unnötig schwermachen wollen, ist das NSMutableArray die bessere Wahl. Später werden wir uns noch die assoziativen Varianten ansehen; assoziativ deshalb, weil wir hier Key-ValuePaare ablegen können: NSArray
NSDictionary NSMutableDictionary
Auch hier stellt das NSMutableDictionary wieder die veränderliche Variante der Elternklasse NSDictionary dar. Assoziative Arrays bieten sich an, wenn wir beispielsweise ein bestimmtes Objekt im Array suchen, wie das passende UIImage-Objekt (Value), das anhand eines NSStrings (Key) angelegt wurde: Sie müssen also nicht das gesamte Array in einer Schleife durchgehen, nur um ein bestimmtes Element zu finden. Für unsere Zombiearmee können wir auf die erste Variante (NSMutableArray) zurückgreifen: Wir werden seriell vorgehen und geben nicht einem bestimmten Zombie den Vorzug, alle Zombies werden gemeinsam über eine Schleife gehandhabt – und auf den Screen gerendert. Lassen wir den untoten Schicksalen also ihren Lauf und deklarieren im Header das Array: NSMutableArray *sprites;
Das Befüllen desselben und den Rendervorgang der Zombies erledigen wir komplett in der drawRect:-Methode der MainView-Klasse, der wir den folgenden Code hinzufügen: - (void) drawRect: (CGRect) rect { W = rect.size.width; H = rect.size.height; if (!sprites) { sprites = [[NSMutableArray alloc] initWithCapacity: 20]; for (int i=0; i<20; i++) { int fs = [self getRndBetween: 1 and: 10]; int sy = [self getRndBetween: -3 and: -1]; int px = [self getRndBetween: 0 and: W]; int py = [self getRndBetween: H and: H+100]; Sprite *sprite = [[Sprite alloc] initWithPic: @"zombie_4f.png" frameCnt: 4 frameStep: fs speed: CGPointMake(0, sy) pos: CGPointMake(px, py)]; [sprites addObject: sprite]; [sprite release]; } } for (Sprite *sprite in sprites){ [sprite draw]; } }
89
3 Spiele entwickeln – von 0 auf 180 Um die Zombie-Objekte mit unterschiedlichen Werten anzulegen, verwenden wir wieder die bekannte getRndBetween:and:-Methode. Sofern das sprites-Array noch nicht initialisiert wurde, verzweigen wir in die if-Abfrage und legen dieses mit einer erwarteten Größe von 20 Elementen an: sprites = [[NSMutableArray alloc] initWithCapacity: 20];
Diese Kapazitätsangabe dient der besseren Speicherverwaltung von Objective-C. Wir können nach Belieben später weitere Elemente hinzufügen oder löschen. In der nachfolgenden for-Schleife erfolgt nun einmalig das Befüllen des Arrays, indem wir zunächst 20-mal ein neues Sprite-Objekt mit zufälligen Werten erzeugen. Das Hinzufügen zum Array erfolgt über die addObject:-Methode: [sprites addObject: sprite]; [sprite release];
Achten Sie darauf, dass die Sprite-Objekte unmittelbar nach dem Hinzufügen released werden, um keine Speicher-Leaks zu erzeugen. Die Array-Klassen von Objektive-C erhöhen für jedes hinzugefügte Element deren retain-Count. Folglich müssen wir diesen sogleich um 1 vermindern, da wir das Objekt nur noch über das Array referenzieren. Ansonsten könnte das Objekt später in der dealloc-Methode nicht mehr abgeräumt werden: -(void) dealloc { [sprites release]; [super dealloc]; }
Der Aufruf von [sprites release] bewirkt lediglich, dass der retainCount aller im Array gespeicherten Objekte um 1 vermindert wird. Hätten wir zuvor den retainCount nicht um 1 vermindert, würden die Objekte im Array immer noch bei 1 stehen und könnten nicht freigegeben werden. Um vorzeitig Elemente aus einem Array zu entfernen, stehen Ihnen die removeObject:bzw. removeAllObjects:-Methoden zur Verfügung. Beispiel: [sprites removeObject: sprite]; [sprites removeAllObjects];
Um nun die Zombies auf den Screen zu rendern, genügt es, durch das Array zu iterieren und die draw:-Methode aller darin enthaltenen Zombies aufzurufen: for (Sprite *sprite in sprites){ [sprite draw]; }
Diese verkürzte Variante einer for-Schleife liefert uns der Reihe nach Zugriff auf alle Sprite-Objekte. Die Schleife wird automatisch verlassen, wenn das letzte Element gefunden wurde. Beachten Sie, dass Sie während der Iteration keine Objekte hinzufügen oder entfernen können, ansonsten erhalten Sie die Fehlermeldung: "Collection was mutated while being enumerated." Um trotzdem dynamisch Objekte verwalten zu können, müssen wir ein paar weitere Tricks anwenden, die wir später an einem etwas komplexeren Beispiel vorstellen werden.
90
3.8 Kollisionskontrolle, bitte!
3.8
Kollisionskontrolle, bitte! Was haben wir bis jetzt erreicht? Wir haben Autos in verschiedenen Farben auf dem Display angezeigt und eine kleine Zombiearmee aufmarschieren lassen. Nun, verbinden wir doch einfach beides und lassen die Autoindustrie gegen die Zombies antreten. Können die kleinen Pixelzombies der Übermacht der heranrasenden Automobile überstehen? Um festzustellen, ob ein Zombie von einem Auto angefahren wurde, brauchen wir den nächsten Baustein der Spieleentwicklung: die Kollisionskontrolle. In nahezu jedem Spiel gibt es Elemente, die mit anderen Elementen kollidieren können. Auch für Benutzeroberflächen werden Kollisionschecks gemacht: Um festzustellen, ob die Maus ein Element angeklickt hat, muss geprüft werden, ob sich der Punkt, an dem die Maus gedrückt wurde, innerhalb des Menüelementes befindet. Oder anders ausgedrückt, ob der Punkt mit dem Element "kollidiert". Die einfachste Form der Kollisionsüberprüfung besteht darin festzustellen, ob sich ein Punkt innerhalb eines Rechtecks befindet. Eine solche Prüfung ist leicht zu bewerkstelligen: //Kollision Punkt <-> Rechteck - (bool) checkColPoint: (CGPoint) p withRect: (CGRect) rect { if ( p.x > rect.origin.x && p.x < (rect.origin.x+rect.size.width) && p.y > rect.origin.y && p.y < (rect.origin.y+rect.size.height)) { return true; } return false; }
Da in der Spieleprogrammierung Sprites eingesetzt werden und Sprites eine rechteckige Form haben, ist die Kollisionsüberprüfung zwischen zwei Rechtecken ebenfalls sehr verbreitet. Dabei überprüft man den linken, oberen und den rechten, unteren Punkt des ersten Rechtecks mit den entsprechenden Punkten des zweiten Rechtecks (siehe Grafik).
Abb 3.11: Kollision von zwei Rechtecken
91
3 Spiele entwickeln – von 0 auf 180 Um diesen Kollisionscheck vorzustellen und in einem Projekt einzusetzen, verwenden wir das Animationsbeispiel aus dem letzten Kapitel als Template und setzen in Xcode ein neues Projekt namens "Kollision" auf. Da wir sehr häufig mit der rechteckigen Grundform eines Sprites arbeiten, fügen wir zunächst der Sprite-Klasse eine Methode hinzu, die uns jeweils das aktuelle Rechteck zurückliefert. Die Abmessungen des Sprites frameW und frameH haben wir in der Klasse als Member angelegt und in der init-Methode initialisiert mit: int picW = pic.size.width; frameW = picW/frameCnt; frameH = pic.size.height;
Dadurch fällt die Implementierung der getRect-Methode sehr einfach aus: - (CGRect) getRect { //Rechteck an der aktuellen Position des Sprites return CGRectMake(pos.x, pos.y, frameW, frameH); }
Ausgehend von der Rechteckstruktur können wir uns jeweils die aktuellen Eckpunkte besorgen und die Kollisionsprüfung durchführen: //Kollision Rechteck <-> Rechteck - (bool) checkColWithSprite: (Sprite) sprite { CGRect rect1 = [self getRect]; CGRect rect2 = [sprite getRect]; //Rect 1 int x1=rect1.origin.x; //Rect1: Punkt links oben int y1=rect1.origin.y; int w1=rect1.size.width; int h1=rect1.size.height; //Rect 2 int x3=rect2.origin.x; //Rect2: Punkt links oben int y3=rect2.origin.y; int w2=rect2.size.width; int h2=rect2.size.height; int x2=x1+w1, y2=y1+h1; int x4=x3+w2, y4=y3+h2;
//Rect1: Punkt rechts unten //Rect2: Punkt rechts unten
Die Methode fügen wir ebenfalls der Sprite-Klasse hinzu.
92
3.8 Kollisionskontrolle, bitte! Um zu signalisieren, dass eine Kollision zwischen Auto und Zombie stattfindet, lassen wir die untoten Gesellen aufheulen und ein höllisches "ARGGH" von sich geben – symbolisiert durch eine Sprechblase.
Abb 3.12: arggh.png
Dieses „individuelle“ Verhalten der Zombie-Sprites realisieren wir idealerweise in einer Unterklasse, die wir von Sprite ableiten. Für unseren Kollisionstest werden wir daher zwei verschiedene Sprite-Typen einsetzen, einerseits die Auto-Sprites und die von Sprite abgeleiteten Zombie-Sprites. Um die Sprites später voneinander unterscheiden zu können, fügen wir der Sprite-Klasse einen type-Member hinzu vom Typ int. Diesen belegen wir in der init-Methode der Sprites standardmäßig mit dem Wert –1. Die möglichen Sprite-Typen legen wir in einer enum-Liste an, die wir außerhalb des Sprite-Interface deklarieren: //Sprite-Typen/ enum types { CAR, ZOMBIE }; @interface Sprite : NSObject { ... } ... @end
Um einen expliziten Sprite-Typ zu setzen, fügen wir der Klasse noch die folgenden Setter/Getter hinzu: - (void) setType: (int) spriteType { type = spriteType; } - (int) getType { return type; }
Wie wir an der Implementierung der Zombie-Unterklasse sehen werden, kommt der Getter zum Einsatz, um die Kollisionsprüfung mit den Autos durchzuführen (alternativ können Sie natürlich auch Properties einsetzen). Doch zunächst der Header der Zombie-Klasse: Listing Zombie.h #import "Sprite.h" @interface Zombie : Sprite { UIImage *argghPic; }
93
3 Spiele entwickeln – von 0 auf 180 - (void) hitTest: (NSMutableArray *) sprites; @end
Die Unterklasse beinhaltet lediglich das ARGGH-Pic, das die Kollision verdeutlichen soll, und eine hitTest:-Methode, die die Kollisionsprüfung anhand der übergebenen SpriteListe vornimmt. Listing Zombie.m #import "Zombie.h" @implementation Zombie - (id) initWithPic: frameCnt: frameStep: speed: pos:
} - (void) hitTest: (NSMutableArray *) sprites { for (Sprite *sprite in sprites) { if ([sprite getType] == CAR) { if ([self checkColWithSprite: sprite]){ [argghPic drawAtPoint: CGPointMake(pos.x, Pic.size.height)]; } } } }
pos.y
-
arggh-
@end
überschreiben wir die initdamit sich die Initialisierung der Zombies formal nicht von den anderen Sprites unterscheidet. Innerhalb der Methode laden wir dann lediglich das ARGGH-Bild und rufen über super die überschriebene Fassung der Methode auf, um das Sprite-Objekt mit den üblichen Werten zu füllen.
In
der
Implementierung
der
Zombie-Klasse
WithPic:frameCnt:frameStep:speed:pos:-Methode,
94
3.8 Kollisionskontrolle, bitte! Da die Zombies den Kollisionstest selbstständig durchführen sollen, fügen wir die neue hitTest:-Methode hinzu, in der wir durch alle Sprites iterieren und über den Getter den jeweiligen Typ abfragen. Handelt es dabei um ein Auto, rufen wir den Kollisionscheck auf und rendern gegebenenfalls das ARGGH-PNG. Jetzt müssen wir nur noch für die Autoindustrie sorgen und den Fuhrpark anhand der drei Auto-PNGs aus dem vorletzten Kapitel erstellen. Zombies und Autos werden zusammen in der drawRect:-Methode der MainView-Klasse initialisiert, wobei die Autos von oben nach unten fahren und die Zombies von unten nach oben. Um die neue Zombie-Klasse einzusetzen, müssen wir in der MainView-Klasse natürlich noch den Header importieren. - (void) drawRect: (CGRect) rect { W = rect.size.width; H = rect.size.height; //Zombies + Autos initialisieren if (!sprites) { sprites = [[NSMutableArray alloc] initWithCapacity: 30]; //Zombies for (int i=0; i<20; i++) { int fs = [self getRndBetween: 1 and: 10]; int sy = [self getRndBetween: -3 and: -1]; int px = [self getRndBetween: 0 and: W]; int py = [self getRndBetween: H and: H+100]; Zombie *zombie = [[Zombie alloc] initWithPic: @"zombie_4f.png" frameCnt: 4 frameStep: fs speed: CGPointMake(0, sy) pos: CGPointMake(px, py)]; [zombie setType: ZOMBIE]; [sprites addObject: zombie]; [zombie release]; } //Autos for (int i=0; i<10; i++) { NSString *pic = @"car_blue.png"; if (i<3) pic = @"car_green.png"; else if (i<6) pic = @"car_red.png"; int sy = [self getRndBetween: 1 and: 3]; int px = [self getRndBetween: 0 and: W]; int py = [self getRndBetween: -100 and: 0]; Sprite *car = [[Sprite alloc] initWithPic: pic frameCnt: 1 frameStep: 0 speed: CGPointMake(0, sy) pos: CGPointMake(px, py)]; [car setType: CAR]; [sprites addObject: car]; [car release]; } } for (Sprite *sprite in sprites) { if ([sprite getType] == ZOMBIE) { [(Zombie *) sprite hitTest: sprites]; } [sprite draw]; } }
95
3 Spiele entwickeln – von 0 auf 180 Die Zombies werden wie im Animationsbeispiel angelegt. Diesmal verwenden wir jedoch die Zombie-Klasse und setzen über [zombie setType: ZOMBIE];
den passenden Typ. Wir hätten diesen natürlich auch in der überschriebenen initMethode setzen können, bleiben hier aber konform zu den Autos, die wir auf ähnliche Weise anlegen. Beachten Sie, dass wir für frameCnt den Wert 1 angeben, da das Auto nicht animiert ist. Da die Zombie-Klasse von Sprite abgeleitet ist, können wir die Zombies in der forSchleife wie die Autos behandeln – beide verfügen schließlich über die draw-Methode. Um die hitTest:-Methode aufzurufen, müssen wir das Sprite-Element jedoch im Array über (Zombie *) casten: if ([sprite getType] == ZOMBIE) { [(Zombie *) sprite hitTest: sprites]; }
Damit wäre unser erstes Kollisionsbeispiel komplett, die hitTest:-Methode durchläuft alle Sprites und prüft das jeweilige Zombie-Objekt auf Kollision mit einem Sprite-Objekt, falls dieses vom Typ CAR ist.
Abb 3.13: Open your mouth and say … ARGGH!
Wenn Sie das Beispiel ausführen, werden Sie feststellen, dass die Zombies von den Autos "überfahren" werden, da diese zuerst gezeichnet werden. Die Grafiken der Autos überlagern also die zuvor gerenderten Zombie-Sprites. Wenn Sie erreichen wollen, dass zwar der Zombie, nicht aber die Sprechblase unter dem Auto landet, müssten Sie die Sprechblase in einer weiteren Iteration nach dem Rendern der Autos anzeigen. Wie Sie sich vielleicht schon denken können, liefert die Rechteck-Kollisionsmethode nicht immer Ergebnisse, die der Optik der Grafiken entsprechen: Die transparenten Pixel an den Rändern werden ignoriert, da nur das umgebende Rechteck – man sagt dazu auch "Bounding Box" – auf Kollision geprüft wird. Sie können die Genauigkeit der Kollisionsprüfung
96
3.8 Kollisionskontrolle, bitte! erhöhen, indem Sie die Bounding Box enger um die Grafik legen oder aber mehrere Bounding Boxen pro Objekt verwenden. Eine weitere Variante besteht darin, einen Bounding Circle zu verwenden, d.h., die Kollision wird anhand eines gedachten Kreises durchgeführt, in den das Objekt eingepasst ist. Der Mittelpunkt des Objektes sollte dabei dem Mittelpunkt des Kreises entsprechen, und der Radius sollte der Ausdehnung der Grafik entsprechen. Um zu zeigen, wie man die Kollisionsberechnung mit Kreisen durchführt, müssen wir auf den berühmten Satz von Pythagoras zurückgreifen, den Sie sicherlich noch aus der Schule kennen:
a 2 + b2 = c2 Dieser Satz birgt für uns – neben der Grundlage für die Kollisionsberechnung – einen weiteren Anwendungsfall, der sich für die Spieleprogrammierung als nützlich erweisen kann, nämlich die Distanzberechnung von zwei Punkten. Distanz zwischen zwei Punkten Angenommen, Sie wollen die Entfernung zwischen P1 (x1, y1) und P2 (x2, y2) wissen, beispielsweise um zu prüfen, ob eine Kollisionsprüfung mit einem Objekt überhaupt Sinn macht, sprich in der Nähe ist. Sie brauchen sich nun nur die Strecke zwischen P1 und P2 als die längste Seite eines rechtwinkligen Dreiecks vorzustellen, die bekanntermaßen auch Hypotenuse genannt wird.
Abb 3.14: Rechtwinkliges Dreieck mit der Strecke zwischen P1 und P2
Damit können wir den Satz des Pythagoras anwenden, um die Länge von c zu bestimmen. Der dritte Eckpunkt des gedachten Dreiecks P3 ergibt sich aus P1 und P2 (denn das Dreieck soll ja rechtwinklig sein) und hat folglich die Koordinaten P3 (x2, y1). Somit können wir die Länge von a und b bestimmen:
a = x2 − x1 b = y2 − y1 Daraus folgt mit Pythagoras:
c 2 = (x2 − x1)2 + ( y2 − y1)2
97
3 Spiele entwickeln – von 0 auf 180
Nun müssen wir auf beiden Seiten die Quadratwurzel ziehen, und es ergibt sich:
(x2 − x1)2 + ( y2 − y1)2
c=
Über math.h bietet uns Objective-C die passenden Methoden an: •
Quadrieren (der zweite Wert gibt die Potenz an) extern double pow( double, double ); extern float powf( float, float );
Anstatt die pow-Funktion einzusetzen, können Sie auch direkt multiplizieren, also:
(x2 − x1)2 = (x2 − x1)(x2 − x1) Dies bringt einen minimalen Performanceschub mit sich, den wir nicht verschweigen wollen. Ebenso: Auf die höhere Präzision des double-Typs können wir verzichten, sodass wir die float-Variante sqrtf() wählen. Für die Distanzberechnung zwischen zwei Punkten ergibt sich damit die folgende Methode: - (int) getDistanceP1: (CGPoint) p1 andP2: (CGPoint) p2 { return sqrtf( (p2.x - p1.x)*(p2.x - p1.x) + (p2.y - p1.y)*(p2.y - p1.y) ); }
Und hier ein paar Testaufrufe: int dist = [self getDistanceP1: CGPointMake(10, 0) andP2: CGPointMake(8, 0)]; NSLog(@"distance: %i", dist); // = 2 dist = [self getDistanceP1: CGPointMake(7, 5) andP2: CGPointMake(4, 16)]; NSLog(@"distance: %i", dist); // = 11
Übrigens: Die Formel lässt sich ebenso einfach im dreidimensionalen Raum anwenden, Sie müssen lediglich die z-Koordinate hinzufügen. Distanz d (im 3D-Raum) zwischen P1 (x1, y1, z1) und P2 (x2, y2, z2):
d=
(x2 − x1)2 + ( y2 − y1)2 + (z2 − z1)2
Kollisionsberechnung mit Kreisen
Was hat nun die Distanzformel mit der Kollisionsprüfung von Kreisen zu tun? Sehr viel. Um es in Anlehnung an Sepp Herberger zu formulieren: "Kreise sind rund". Mit dieser famosen Erkenntnis halten wir bereits den Schlüssel der gesuchten Formel in unseren Händen – denn ein Kreis besitzt an jedem Punkt die gleiche Entfernung zum Mittelpunkt, den Radius.
98
3.8 Kollisionskontrolle, bitte!
Abb 3.15: Bounding Circles mit Distanz und Radien. Im unteren Beispiel gilt r1 + r2 = Distanz der Radien zueinander.
Zwei Kreise kollidieren daher genau dann, wenn die Distanz zwischen den Mittelpunkten kleiner als die Summe der Radien ist. Ein Kreis ist vollständig definiert durch seinen Mittelpunkt und den Radius. Da der Radius die Distanz zwischen Mittelpunkt und den Kreispunkten angibt, lässt sich ein Kreis ebenfalls durch den Satz von Pythagoras beschreiben. Beispiel: Ein Kreis mit MP (0, 0) und Radius = 3 ist im 2D-Koordinatensystem bestimmt durch die Gleichung:
x2 + y 2 = 3 * 3 Die Methode zur Kollisionsprüfung von zwei Kreisen können wir mit der Distanzformel implementieren: //Kollision Kreis <-> Kreis - (bool) checkColWith: (CGPoint) p1 radius1: (int) r1 andP2: (CGPoint) p2 radius2: (int) r2 { if ( (p2.x - p1.x)*(p2.x - p1.x) + (p2.y - p1.y)*(p2.y - p1.y) < (r1 + r2)*(r1 + r2) ) { return true; } return false; }
Da das Wurzelziehen für den Prozessor mehr Aufwand bedeutet als das Quadrieren und wir für den Kollisionscheck nur die Ungleichung prüfen müssen, können wir die Summe
99
3 Spiele entwickeln – von 0 auf 180
der Radien ebenfalls quadrieren, um die Wurzel wegzulassen. Damit ist die Kollisionsberechnung von Kreisen performanter als das Bestimmen der Distanz von zwei Punkten. Für den dreidimensionalen Raum müssen wir wiederum nur die z-Koordinate hinzufügen. Auch eine Kugel wird vollständig durch den Pythagoras-Satz beschrieben. Kollision Kugel <-> Kugel: Kugel 1: Mittelpunkt: MP1 (x1, y1, z1), Radius: r1 Kugel 2: Mittelpunkt: MP2 (x2, y2, z2), Radius: r2 Ein Kollision ist erfolgt, wenn:
(x2 − x1)2 + ( y2 − y1)2 + (z2 − z1)2 < (r1 + r 2)2 Für die 3D-Varianten der Kollisionsprüfung bzw. Distanzberechnung werden wir später noch ausführlichere Beispiele bringen. Für 2D-Spiele eignen sich Bounding Boxen aufgrund der rechteckigen Form der Grafiken sehr gut und werden auch häufig eingesetzt, während für 3D-Spiele die Kugel-Kollisionsberechnung ein schnelleres und daher sehr beliebtes Verfahren darstellt.
3.9
User-Input Wir wissen nun, wie wir Bewegtbilder auf dem Display anzeigen können und Kollisionsberechnungen durchführen. Um erste Spiele entwickeln zu können, fehlt nur noch ein wichtiger Baustein: Da Spiele interaktiv sind, müssen wir – anders als beim Film – dafür sorgen, dass der User in die "Handlung" eingreifen kann. Beispielsweise könnte der User in das Mikrofon des iPhones pusten und dadurch eine Reaktion auslösen. Oder aber die Eingabe erfolgt über das Touch-Display und/oder über den Bewegungssensor. Die iOSGeräte haben – mangels einer klassischen Game-Pad-Steuerung – viele neue und kreative Eingabemöglichkeiten etabliert. Erst mit dem iPhone wurden Touch-Displays so richtig populär, auch wenn es schon lange vorher Mobiltelefone gab, die sich per Stylus oder ausschließlich per Finger steuern ließen.4 Um sowohl Touch-Display als auch Bewegungssensor und deren Einsatz für die Steuerung eines Spielcharakters vorzustellen, legen wir ein neues Projekt namens "UserInput" an – auf Basis das Kollisionsprojektes, das wir als Template ablegen und beim Anwählen im
4 Auch der Bewegungssensor ist keine Erfindung von Apple. Das Spiel Marble Revolution (bit-side) gewann 2005 den Deutschen Entwicklerpreis in der Kategorie "Innovativste Technik", eben weil es zu den ersten Spielen gehörte, die die Bewegungssteuerung für Nokias Symbian-Plattform populär gemacht haben, lange vor Wii und EyeToy: Die Steuerung erfolgte in der ersten Version noch per Kamera, später wurde dann ein Motion-Sensor eingesetzt.
100
3.9 User-Input
Xcode-Wizard mit dem neuen Namen anlegen. Die Namensvergabe erfolgt wie bisher über "Projekt -> Edit Active Target -> Packaging -> Product Name". Den Quellcode lassen wir zunächst unverändert. Nur die drawRect:-Methode der MainView-Klasse entschlacken wir, indem wir lediglich ein Hintergrundbild anzeigen, damit sich unsere Spielfigur (und wir) nicht mehr mit dem grauen Hintergrund begnügen muss: - (void) drawRect: (CGRect) rect { W = rect.size.width; H = rect.size.height; if (!background) { background = [UIImage imageNamed: @"background.png"]; } else { [background drawAtPoint: CGPointMake(0, 0)]; } }
Das "background.png" ist 320x480 Pixel groß und bildet den Untergrund, auf dem wir unseren Spielcharakter umherschubsen wollen. Das Hintergrundbild mit dem runden Farbverlauf haben wir übrigens mit der Quartz API erzeugt und als Screenshot abgespeichert (aus Performance-Gründen sollte man es vermeiden, Gradients bzw. Farbverläufe zur Laufzeit eines Spiels zu rendern). Jeder View ist in der Lage, Touch-Events entgegenzunehmen. Das Handling ist denkbar einfach: Um die Koordinaten zu erhalten, an denen ein Finger das Display berührt hat, genügt es, die folgende Methode der MainView-Implementierung hinzuzufügen: - (void) touchesBegan: (NSSet *) touches withEvent: (UIEvent *) event { //Single Tap UITouch *touch = [touches anyObject]; CGPoint p = [touch locationInView:self]; int xt = p.x; int yt = p.y; NSLog(@"Touch at %i, %i", xt, yt); }
Die Methode liefert zum Beispiel den folgenden Output: 2010-06-07 13:17:25.160 UserInput[16579:207] Touch at 161, 235 2010-06-07 13:17:26.736 UserInput[16579:207] Touch at 23, 47 2010-06-07 13:17:27.896 UserInput[16579:207] Touch at 298, 450
Das iOS SDK bietet Unterstützung für vielfältige Gesten wie Pinch und Swipe. Die Grundlage dafür bilden die vier UIResponder-Methoden: -
Je nach Bedarf können wir in unserer Implementierung die gewünschte Methode in einem View überschreiben und so auf die Berührungen des Displays reagieren. Wir werden nicht
101
3 Spiele entwickeln – von 0 auf 180
auf alle Varianten eingehen, denn diese sind in der Dokumentation und der Literatur zum iOS SDK gut beschrieben. Für die meisten Spiele wird ohnehin der oben vorgestellte Code ausreichend sein. Die touchesBegan:withEvent:-Methode wird immer dann aufgerufen, wenn ein Finger das Display berührt. Ein solcher Single Tap entspricht einem Mausklick auf dem Desktop (werden weitere Finger erfasst, spricht man von Multi-Touch). Um auf die Berührung reagieren zu können, benötigen wir also lediglich die Koordinaten des Punktes, an denen der Finger das Display berührt hat. Diese liefert uns: UITouch *touch = [touches anyObject]; CGPoint p = [touch locationInView:self];
Die NSSet-Instanz enthält die Touch-Objekte, wobei wir nur an dem ersten Element interessiert sind, da dieses den Single Tap repräsentiert (= der erste Finger, der das Display berührt hat). UITouch liefert dann über die locationInView:-Methode, die mit unserem View als Parameter aufgerufen wird, den gewünschten Punkt. Bevor wir uns nun überlegen, was wir mit diesem Punkt anfangen und wie wir damit die Bewegungen eines Spielcharakters beeinflussen können, zeigen wir, wie Sie Multi-TouchEvents unterstützen können: - (void) touchesBegan: (NSSet *) touches withEvent: (UIEvent *) event { [self setMultipleTouchEnabled: YES]; NSSet *allTouches = [event allTouches]; NSLog(@"Tap count: %i", [allTouches count]); //Multi-Touch for (UITouch *touch in [allTouches allObjects]){ CGPoint p = [touch locationInView:self]; int x = p.x; int y = p.y; NSLog(@"Multi-Touch at %i, %i", x, y); } }
Auch hier überschreiben wir die touchesBegan:withEvent:-Methode, diesmal fragen wir jedoch den UIEvent-Parameter ab, der uns über die allTouches:-Methode Zugriff auf alle Touch-Events gibt. Damit wir überhaupt Multi-Touch-Events vernünftig abfragen können, müssen wir diese per [self setMultipleTouchEnabled: YES];
"einschalten". Ansonsten müssten alle fünf Finger (= die maximale Anzahl für TouchEvents) gleichzeitig das Display berühren. Multi-Touch ermöglicht dagegen, dass Sie die Finger nacheinander auf das Display legen können. Für jeden neuen Finger wird die Methode erneut aufgerufen. Dadurch können Sie über [allTouches count]
102
3.9 User-Input
die Anzahl der Finger bestimmen, die gerade das Display berühren (Tap Count). [allTouliefert dann ein NSArray, das Sie in einer for-Schleife durchgehen können, um die Koordinaten abzufragen:
Tap count: 5 Multi-Touch at Multi-Touch at Multi-Touch at Multi-Touch at Multi-Touch at
82, 326 41, 164 118, 82 207, 431 231, 28
Ebenso können Sie auch gezielt auf ein bestimmtes Element zugreifen, zum Beispiel das zuerst erfolgte Touch-Ereignis: UITouch *touch = [[allTouches allObjects] objectAtIndex: 0];
Leider bietet der iPhone-Simulator keine Unterstützung für Multi-Touch, sodass Sie den Code nur auf dem Device ausprobieren können. Immerhin: Einen Double Tap können Sie simulieren, wenn Sie vor dem Mausklick die Alt-Taste gedrückt halten. Mit etwas Kreativität können Sie sich verschiedene Varianten ausdenken, um eine Spielfigur mit den Fingern zu steuern. Für eine klassische Gamepad-Emulation brauchen Sie beispielsweise nur vier virtuelle Richtungstasten auf Kollision mit einem Single Tap zu kontrollieren. Viel interessanter ist es dagegen, eine Steuerung zu wählen, die sich nicht mit einem herkömmlichen Gamepad realisieren lassen würde. Angenommen, Sie wollen die Figur in jede mögliche Richtung laufen lassen und tippen dazu einfach nur einen Zielpunkt auf dem Display an. Die Figur läuft dann danach zu diesem Punkt. Tippen Sie den Screen über der Figur an, läuft sie nach oben, tippen Sie darunter, dann nach unten usw. Dies erinnert ein bisschen an einen Mausverfolger und lässt sich ebenso einfach realisieren. Als Spielfigur wählen wir wieder das kleine Pixelmonster aus dem letzten Kapitel. Da wir die Spielfigur nun aber selbst kontrollieren wollen, müssen wir das Verhalten der ZombieKlasse weiter spezialisieren. Wir legen dazu eine neue Player-Klasse an: Listing Player.h #import "Zombie.h" @interface Player : Zombie { CGPoint touchPoint; } - (void) setTouch: (CGPoint) touchPoint; @end Listing Player.m #import "Player.h" @implementation Player - (void) setTouch: (CGPoint) point {
Die Player-Klasse dient lediglich dazu, über einen Setter den Touch-Punkt an die PlayerInstanz zu übergeben.5 Bevor wir das Beispiel ausführen, müssen wir die Spielfigur noch initialisieren und rendern. Wie bisher erledigen wir dies der Einfachheit halber in der drawRect:-Methode der MainView-Klasse: if (!player) { player = [[Player alloc] initWithPic: frameCnt: frameStep: speed: pos: [player setType: PLAYER]; }
Weiterhin haben wir die Sprite-Typen um den Player-Typ erweitert, den Player-Header im MainView-Header importiert und dort ebenfalls die Player-Instanz mittels Player *player deklariert. Bevor der Zombie unseren Finger anknabbern kann, müssen wir zuletzt noch die Koordinaten, an denen der Finger das Display berührt, an die setTouch:-Methode übergeben: UITouch *touch = [touches anyObject]; CGPoint p = [touch locationInView: self]; [player setTouch: p];
Den Code platzieren wir, wie zu erwarten, im touchesBegan:withEvent:-Handler.
5 Natürlich können wir den Setter auch über die Properties vereinfacht anlegen, wie wir das bereits gesehen haben. Aber es schadet auch nichts, den herkömmlichen Weg zu wählen. Der Code wird dadurch leichter portierbar auf andere Systeme.
104
3.9 User-Input
Abb 3.16: Der Zombie läuft stets zum Finger (Tap).
Und nun kann's losgehen: Wo immer wir das Display berühren, das Pixelmännchen folgt unserem Finger auf dem direktesten Weg – also auch diagonal, wenn es sein muss: Ein klarer Vorteil gegenüber der klassischen 8-Wege-Gamepad-Steuerung. Außerdem bewegt sich die Figur beschleunigt und dadurch dynamisch: Längere Strecken werden schneller zurückgelegt, für kürzere Entfernungen flaniert unser Geschöpf genüsslich heran. Sehen wir uns genauer an, wie wir das hinbekommen haben. Die Magie verbirgt sich in der setTouch:-Methode der Player-Klasse: speed.x = (touchPoint.x - pos.x)/20; //deltaX speed.y = (touchPoint.y - pos.y)/20; //deltaY
Die draw-Methode des Sprites benötigt lediglich die speed-Angabe, um das Sprite in xbzw. y-Richtung pro Frame fortzubewegen. Um also die Richtung des Players zu verändern, brauchen wir nur speed.x und speed.y anzupassen. Hinter dem obigen Code versteckt sich die Zweipunkteform der Geradengleichung: y = mx + n.
105
3 Spiele entwickeln – von 0 auf 180
Eine Gerade wird mathematisch durch zwei Punkte (x1, y1) und (x2, y2) definiert, wobei sich die Steigung m der Geraden durch die Abstände der xy-Koordinaten ergibt:
m = Delta_Y / Delta_X = (y2 - y1) - (x2 - x1) Übertragen auf den Pixelzombie bedeutet dies, dass der erste Punkt (x1, x2) die Position des Sprites markiert und der zweite Punkt den Touch-Point. Da sich der Zombie auf einer Geraden von seinem aktuellen Aufenthaltsort zum Finger bewegen soll, brauchen wir nur die Steigung der Geraden (Delta_Y / Delta_X) für die Verschiebung der Figur in x- bzw. y-Richtung zu addieren. Da Ergebnis teilen wir dann noch durch den Faktor 20, damit die Bewegung nicht allzu schnell erfolgt. Allerdings gibt es noch einen kleinen Schönheitsfehler: Man würde erwarten, dass der Zombie stehen bleibt, wenn er den Finger erreicht hat (Zombies sind ja schließlich hungrig). Stattdessen schießt der Unhold übers Ziel hinaus und verschwindet gar vom Display. Dieses Problem können wir schnell lösen: Um die Bewegung zu stoppen, brauchen wir nur die speed-Werte auf 0 zu setzen. Dazu überschreiben wir die draw-Methode der PlayerKlasse und prüfen, ob die aktuelle Position des Sprites hinter dem Touch-Punkt liegt: - (void) draw { [super draw]; //Stop movement if (speed.x > 0 if (speed.x < 0 if (speed.y > 0 if (speed.y < 0
Nun kann der Spieler das sichtbare Spielfeld nicht mehr verlassen, denn es ist unmöglich, ein Touch-Event außerhalb des Displays auszulösen. Sobald die Figur den Finger erreicht, bleibt sie stehen. Allerdings läuft die Animationssequenz weiter. Dies können wir für alle Sprites anpassen, indem wir in der drawFrame-Methode der Sprite-Klasse die speedVariable auf 0 prüfen und gegebenenfalls das Standframe (frameNr = 0) anzeigen: frameNr = [self updateFrame]; if (speed.x == 0 && speed.y == 0) { frameNr = 0; }
Wir können die Bewegungssteuerung aber noch weiter verbessern. Um den Zombie zu bewegen, muss der Spieler den Finger stets absetzen und an einer anderen Stelle aufsetzen. Viel angenehmer wäre es, wenn man stattdessen seinen Finger über das Display ziehen könnte und der Zombie unmittelbar auf diesen Richtungswechsel seines Opfers reagieren würde. Auch dies lässt sich relativ einfach realisieren: Dazu brauchen wir nur die touchesMoved:withEvent:-Methode zu überschreiben, die für jeden Koordinatenwechsel eines Fingers aufgerufen wird. - (void) touchesMoved: (NSSet *) touches withEvent: (UIEvent *) event { UITouch *touch = [touches anyObject]; CGPoint p = [touch locationInView:self]; [player setTouch: p];
106
3.9 User-Input int x = p.x; int y = p.y; NSLog(@"Touch moves at %i, %i", x, y); }
Halten Sie den Finger auf dem Display, können Sie den Zombie hinter sich herziehen und so dauerhaft auf Trab halten. Aber wir haben noch mehr erreicht. Der Code zeigt außerdem eine mögliche Implementierung der Swipe-Geste. Sie können den Zombie nicht nur per Single Tap und Dauerbewegung steuern, sondern bereits swipen: Wenn Sie auf den Zombie tippen und dann den Finger schnell in eine andere Richtung ziehen, folgt die Spielfigur dem Finger so lange, bis dieser abgesetzt wurde. Für manche Spiele benötigen Sie alternativ eine Bewegungssteuerung, die nur dann aktiv ist, während Sie eine virtuelle Vorwärtstaste drücken (wie bei einer klassischen GamepadEmulation). Die passende Methode zur Umsetzung ist die touchesEnded:WithEvent:Methode, in der Sie die speed-Variable des Players auf 0 setzen könnten, sobald der Finger das Display nicht mehr berührt: - (void) touchesEnded: (NSSet *) touches withEvent: (UIEvent *) event { [player setSpeed: CGPointMake(0, 0)]; //Beispiel }
Bewegungssensor abfragen
Nicht immer gelingt bei einem iPhone-Spiel die Steuerung über das Touch-Display, und für manche Spiele ist die Bewegungssteuerung die bessere Alternative. Denken Sie beispielsweise an die Labyrinth-Serie, bei der eine Murmel durch ein Holzlabyrinth durch Kippen des Displays manövriert werden muss. Eine Steuerung per Sensor ist viel einfacher zu realisieren, als es auf den ersten Blick vielleicht erscheinen mag. Abgefragt werden kann der Bewegungssensor in jeder Klasse, die das UIAccelerometerimplementiert. Dieses Protokoll verlangt lediglich die Implementierung der accelerometer:didAccelerate:-Methode, die vom iPhone iOS aufgerufen wird – und zwar in regelmäßigen Zeitabständen, die wir als Entwickler selbst festlegen können.
Delegate-Protokoll
Über die Methode haben wir dann Zugriff auf die xyz-Werte, die uns die aktuelle Lage des Gerätes mitteilen. Um mit dem Bewegungssensor arbeiten zu können, müssen wir wissen, ob das Gerät nach links oder rechts geneigt wird oder nach oben oder unten. Darüber hinaus wollen wir noch wissen, wie stark das Gerät geneigt wird. Sie sehen schon, als praktisches Anwendungsbeispiel drängt sich eine Murmel in einem Labyrinth geradezu auf. Wir können die Steuerung aber auch auf den Zombie anwenden. Die Implementierung erfolgt in der MainView-Klasse. Dazu erweitern wir den Header um die Protokollangabe und fügen die Deklaration der UIAccelerometer-Instanz hinzu, über die wir den Bewegungssensor initialisieren können: @interface MainView : UIView { UIAccelerometer *accelerometer;
107
3 Spiele entwickeln – von 0 auf 180 UIImage *background; Player *player; }
Die einmalige Initialisierung führen wir in der drawRect:-Methode durch: if (!accelerometer) { accelerometer = [UIAccelerometer sharedAccelerometer]; accelerometer.delegate = self; accelerometer.updateInterval = 0.033; }
Die Klassenmethode sharedAccelerometer gewährt uns Zugriff auf den Sensor. Als Delegate geben wir unsere Klasse (self) an. Die über das Protokoll vereinbarte accelerometer:didAccelerate:-Methode wird dadurch vom iOS aufgerufen, sobald das nächste Update-Intervall erreicht ist und wir neue Sensordaten geliefert bekommen. Das UpdateIntervall wird in Sekunden angegeben. Doch Vorsicht: Ein zu niedriger Wert würde zu viele Ressourcen verschwenden und die Batterie schnell leer saugen. In der Praxis liefert 0.033 akkurate Ergebnisse, da die Spielschleife ebenfalls mit 0.033 FPS läuft. Um die Daten des Sensors unmittelbar für die Steuerung des Sprites einzusetzen, fügen wir mit - (void) setSpeed: (CGPoint) sxy { speed = sxy; }
noch einen Setter für die speed-Variable hinzu. So
weit
zu
den
Vorbereitungen. Nun können wir uns die acceleromeansehen, in der die eigentliche Behandlung des Sensors
ter:didAccelerate:-Methode
stattfindet: - (void) accelerometer: (UIAccelerometer *) sensor didAccelerate: (UIAcceleration *) acceleration { float x = acceleration.x; float y = acceleration.y; float z = acceleration.z; NSLog(@"Sensor: x: %f y: %f z: %f", x, y, z); int factor = 15; [player setSpeed: CGPointMake(x*factor, -y*factor)]; }
Das war es auch schon. Über den UIAcceleration-Parameter können wir direkt auf die aktuellen xyz-Werte zugreifen, die im Bereich –1 bis 1 liegen und – verstärkt um den Fak-
108
3.9 User-Input
tor 15 – direkt an die setSpeed:-Methode des Players gereicht werden. Für den y-Wert müssen wir lediglich das Vorzeichen ändern, da die y-Achse des Koordinatensystems der iOS-Geräte von oben nach unten verläuft. Der Faktor legt zudem die maximale Pixelgeschwindigkeit fest und bestimmt die Sensibilität, mit der unser Pixelmännchen auf den Sensor reagiert: Je kleiner der Faktor, desto träger. liefert die xyz-Orientierung als UIAccelerationValue-Typ, der als doubdefiniert ist. Für uns reicht float-Präzision, deshalb greifen wir die Werte auch entsprechend ab. Die Bedeutung von xyz ist wie folgt festgelegt:
Um die Bewegung einer Spielfigur in vier Richtungen zu leiten, reicht also bereits ein Blick auf das Vorzeichen. Der Grad der Neigung reicht jeweils von 0 (waagerecht, 0 Grad) bis 1 bzw. –1, was einem Winkel von 90 Grad entspricht. Wird das Gerät weiter gekippt, ist das Display nicht mehr sichtbar, es zeigt nach unten, und die Werte wandern von 1 bzw. –1 zurück bis nach 0. Daher benötigen Sie zusätzlich den z-Parameter, anhand dessen Sie ablesen können, ob der Neigungswinkel sich auf ein nach oben ausgerichtetes Display bezieht oder nicht. Für Spiele reicht in erster Linie die Auswertung der xy-Werte. Zusätzlich könnten Sie den z-Parameter verwenden, um das Spiel zu pausieren, sobald der User das Gerät umdreht. Nimmt er es wieder in die Hand, um weiter zu spielen, zeigt das Display nach oben (da die meisten Spieler das Gerät wahrscheinlich nicht wie Jimi Hendrix die Gitarre über dem Kopf halten und beim Spielen nach oben sehen): if (z < 0) NSLog(@"Display korrekt."); else NSLog(@"Schlafmodus einschalten.");
Da der iPhone-Simulator keine Möglichkeit bietet, den Bewegungssensor zu testen, müssen Sie das Programm auf einem Gerät ausprobieren. Wir sollten außerdem dafür sorgen, dass jeweils nur eine Steuerungsart aktiv ist: Die Default-Steuerung soll die TouchSteuerung sein. Tippt der User mit zwei Fingern gleichzeitig auf das Display, wird die aktuelle Steuerungsart umgeschaltet werden (Toggle). Dazu legen wir uns einfach eine neue Boole'sche Variable als Flag an: bool useSensor; Den Wert togglen wir dann in der touchesBegan:withEvent:-Methode: if ([allTouches count] == 2) { //Doubletap if (useSensor) { useSensor = NO; } else { useSensor = YES; } }
Nun brauchen wir den Flag nur noch in den jeweiligen Methoden abzufragen und die entsprechenden Code-Teile in den if-Block zu setzen. Denken Sie daran, die Steuerung per
109
3 Spiele entwickeln – von 0 auf 180
Double Tap umzuschalten, wenn Sie das Programm auf einem Gerät ausprobieren. Befinden Sie sich im Motion-Steuerungsmodus und die Figur ist über den Bildschirmrand hinausgewandert, brauchen Sie nur erneut einen Double Tap auszuführen. Die Figur wandert dann flugs zurück zum sichtbaren Bildschirmbereich. Dafür sorgt automatisch die gezeigte Touch-Control-Implementierung. Sie werden feststellen, dass beide Steuerungsarten jeweils über Vor- und Nachteile verfügen. Im Falle der Touch-Steuerung verdeckt der Finger manchmal die Spielfigur oder behindert den Blick auf das Display. Die Bewegungssteuerung ist dagegen sehr feinfühlig und daher manchmal auch etwas zu sensibel: Es ist mitunter schwer, die Figur unbewegt auf einem Fleck stehen zu lassen, da der Sensor selbst kleinste Bewegungsschwankungen registriert. Zumindest hier könnten Sie Abhilfe schaffen, indem Sie optional einen Schwellenwert (Threshold) definieren, ab dem die Bewegung der Spielfigur erfasst werden soll: float threshold = 0.1; if (x > threshold || x < -threshold) { [player setSpeed: CGPointMake(x*factor, ([player getSpeed]).y)]; } if (y > threshold || y < -threshold) { [player setSpeed: CGPointMake(([player getSpeed]).x, -y*factor)]; }
Sie müssen dabei natürlich darauf achten, dass Sie die xy-Geschwindigkeit getrennt steuern. Deshalb übergeben wir an die setSpeed:-Methode nur jeweils einen Wert und lassen den anderen unverändert, indem wir den aktuellen Wert über einen Getter erneut setzen. Den Threshold sollten Sie je nach Bedarf anpassen, ein Wert von 0.1 liefert einen guten Kompromiss zwischen Sensibilität und "Shock Resistance" (je nachdem, wie zittrig Ihre Hände sind).
3.10
Und jetzt alle(s) zusammen: GameManager und SpriteVerwaltung Die Bestandteile eines elementaren Spieleentwicklungsbaukastens haben wir bereits kennengelernt: Benutzereingaben verarbeiten, eine Spielschleife aufsetzen und Grafiken auf dem Display zur Anzeige bringen. Damit können Sie bereits viele Spielideen umsetzen. Doch aufgepasst: Für Spiele müssen sehr viele Daten unterschiedlichster Art verarbeitet werden, noch dazu in Echtzeit und interaktiv. Damit gehören Spiele mit zu den herausforderndsten Anwendungen der Informatik. Nicht ohne Grund gelten Spiele als Benchmark für neue Hardware und treiben die Entwicklung neuer CPUs und Grafikkarten voran. In diesem Kapitel legen wir den Schwerpunkt daher auch weniger auf neue Features des iOS SDK, sondern zeigen exemplarisch, wie Sie mit den bereits bekannten Mitteln die Komplexität von Spielen meistern können. Wichtige Themen der Spieleentwicklung sind bisher noch unberührt geblieben:
110
3.10 Und jetzt alle(s) zusammen: GameManager und Sprite-Verwaltung Dynamische Erzeugung und Verwaltung von Sprites Einmalige Animationseffekte, z.B. für Explosionen Ressourcen platzsparend speichern und performant laden Preloader (Daten vor Spielbeginn im Hintergrund laden) Einfache Benutzerführung mit Zustandsmaschinen Organisation des Quellcodes von größeren Projekten
All diese unterschiedlichen Aspekte der Spieleentwicklung lassen sich relativ überschaubar von einer zentralen Klasse aus steuern, der wir den bezeichnenden Namen "GameManager" geben werden. Selbst für sehr einfache Spiele ist es wichtig, dass Sie den Überblick behalten, um sich bei der Programmierung auf das eigentliche Spielgeschehen konzentrieren zu können. Kurzum, Sie müssen sich Gedanken darüber machen, wie Sie zum Beispiel mehrere Sprites in Ihrem Spiel leben und sterben lassen können und wie Sie die verschiedenen Zustände, in denen sich ein Spiel befinden kann (Held besiegt Gegner, Held stirbt usw. – oder anwendungsbezogen: Spiel gewonnen, Spiel verloren, Menü wird angezeigt usw.), mit vertretbarem Aufwand organisieren können. Da Theorie grau ist und wir es kaum erwarten können, erneut kleine Pixelmännchen über den Touch-Bildschirm zu schieben, verbinden wir die bisher vorgestellten Elemente zu einem neuen Spielkonzept. Unsere kleinen Zombies konnten sich ja bisher nicht wehren, wenn sie von den Autos angefahren bzw. überfahren wurden. Der Fachmann weiß natürlich, dass das einem Zombie an sich auch herzlich egal sein dürfte. Aber da wir nun wissen, wie man einen Untoten per Finger über den Bildschirm scheuchen kann, können wir auch gleich ein kleines Spiel daraus machen. Gerechtigkeit muss sein, lassen Sie uns also feststellen, ob unsere Zombiearmee den Kampf gegen die übermächtige Autoindustrie vielleicht doch noch gewinnen kann ... Das Spielkonzept legen wir wie folgt fest:
Sie steuern eine Zombiearmee per Finger und müssen dafür sorgen, dass diese möglichst unbehelligt an den Autos vorbei in Richtung Stadt marschieren kann. Das Spiel ist gewonnen, wenn Sie die erforderliche Anzahl an Untoten über eine grüne Demarkationslinie bugsiert haben. Spielelemente Hintergrundgrafik: Zeigt eine Straße aus Vogelperspektive. Master Zombie (grün): Sozusagen der Held des Spiels, der von Ihnen per SwipeSteuerung über den Bildschirm geschoben werden kann. Reanimator: Ein Item, das der Master-Zombie aktivieren kann, wenn er darauf steht. Erzeugt alle fünf Frames einen neuen Zombie. Symbolisiert durch einen Pi-
111
3 Spiele entwickeln – von 0 auf 180
xeltotenkopf umgeben von einem roten Kreis. Erzeugte Zombies wandern stets von unten nach oben. Zombies: werden vom Master Zombie zum (untoten) Leben erweckt. Marschieren stoisch mit einem Pixel pro Frame senkrecht nach oben zur Demarkationslinie. Demarkationslinie: Erreicht ein Zombie diese Linie, hat er das Ziel erreicht und kann unbehelligt die imaginäre Großstadt erreichen. Autos: Fahren stets von oben nach unten und hindern die Zombies so an der Invasion. Autos sind unzerstörbar. Gerät ein Zombie oder der Spieler (repräsentiert durch den Master-Zombie) unter ein Auto, verliert er Lebenspunkte. Gesundheit: Der Spieler hat 100 Lebenspunkte. Reanimierte Untote verfügen über zehn Lebenspunkte. Für die Dauer der Berührung mit einem Auto wird jeweils ein Lebenspunkt pro Frame abgezogen.
Abb 3.17: Alle Grafiken des Spiels in der Übersicht
Siegbedingung Das Spiel ist gewonnen, wenn der Spieler mindestens 20 Zombies über die Demarkationslinie gebracht hat. Um den Spieler zu motivieren, wird die Anzahl der verlorenen Zombies angezeigt. Niederlage Die Lebenspunkte des Spielers sind auf 0 gesunken. Sinken die Lebenspunkte eines Zombies auf 0, so wird dieser zurück in die Hölle geschickt, angezeigt durch eine Verpuffungsanimation (eine graue Rauchwolke).
112
3.10 Und jetzt alle(s) zusammen: GameManager und Sprite-Verwaltung
Steuerung Sie können sowohl die Zombies als auch den grünen Master-Zombie per Swipe-Steuerung über den Bildschirm bewegen. Der Master-Zombie bleibt stehen, sobald er den Endpunkt der Swipe-Bewegung erreicht hat. Die Zombies dagegen marschieren nach Beendigung des Swipes wieder mit einem Pixel pro Frame nach oben. Um Spielfiguren swipen zu können, muss der Berührungspunkt des Fingers auf der Spielfigur liegen. Multi-Touch wird nicht unterstützt, Sie können aber durch "Darüberfahren" mit dem Finger weitere Zombies aufsammeln und mitführen. Die Bewegung endet erst, wenn der Finger abgesetzt wurde. HUD (Head-up-Display) Am oberen Bildschirmrand werden die folgenden Informationen angezeigt: •
HLT: Gesundheitsanzeige des Master-Zombies (Health).
•
SVD: Anzahl der bereits 'geretteten' Zombies (Saved).
•
LST: Anzahl der 'überfahrenen' Zombies (Lost).
Menüführung 1. Während des Ladens der Spielressourcen wird der Splash-Screen angezeigt. 2. Sind alle Ressourcen geladen, startet das Spiel mit einem kleinen Begrüßungs- bzw. Anleitungstext. Ein Tap startet das Spiel. 3. Spielen! 4. Stirbt der Held, wird das Spielergebnis angezeigt. Ein erneuter Tap führt zurück zum Willkommenstext (2.). 5. Gewinnt der Spieler, wird das Spielergebnis ebenfalls angezeigt. Ein erneuter Tap führt zurück zum Willkommenstext (2.). Der Clou und gleichzeitig die Schwierigkeit des Spieles bestehen also darin, möglichst lange auf dem Reanimator, symbolisiert durch den Totenschädel, zu verweilen, den Autos auszuweichen und trotzdem die bereits erschaffenen Zombies vor den Autos zu schützen und möglichst weit per Swipe nach oben zu befördern. Da die Autos mit unterschiedlichen Geschwindigkeiten nach unten fahren, ergeben sich Lücken. Doch Vorsicht: Ohne Ihr Zutun ist der Weg zur rettenden grünen Linie zu weit, und Ihre untoten Kreationen hätten keine Chance gegen die Übermacht der Autos. Durch die Geschwindigkeit der Autos, der Zombies, der Anzahl der Reanimatoren, der Lebenspunkte und der Anzahl der zu rettenden Zombies haben Sie mehrere Möglichkeiten, den Schwierigkeitsgrad des Spiels zu steuern und weitere Level zu bauen. Zombiespiele sind generell sehr beliebt im App Store, wie zum Beispiel der erfolgreiche Fieldrunner-Clone Plants vs. Zombies (PopCap). Warum also nicht mal die Seiten wechseln und den ewig wehrlosen und tumben Gesellen unter die Arme greifen? Durch die Touch-Steuerung der iOS-Geräte ergibt sich so ein relativ neuartiges Spielkonzept, das
113
3 Spiele entwickeln – von 0 auf 180
sich vielfältig ausbauen lässt (gerne auch mit anderem Personal). Und dennoch ist das Spiel einfach genug, um nicht den Blick auf die zu erlernenden Grundlagen zu verstellen. Dieses Kapitel lesen Sie am besten, wenn Sie sich parallel dazu den Source-Code des Beispielprojektes ansehen. Dieses heißt schlicht "GameManager" und befindet sich wie immer im Download-Ordner auf der Website zum Buch. Das Projekt haben wir auf der Basis des UserInput-Beispiels aus dem letzten Kapitel aufgesetzt, das wir als Template verwendet haben. Die bereits vorgestellten Grafiken befinden sich im „Resources“-Ordner, die Quelltexte unter „Classes“. Wie Sie sehen, haben wir in diesem Ordner per Rechtsklick die neue Gruppe „GameElements“ erzeugt, in die wir alle Sprite-Klassen verschoben haben. Sie müssen dazu nicht die import-Anweisungen der Klassen ändern. Die Gruppendarstellung dient lediglich einer übersichtlicheren Xcode-Ansicht, die tatsächliche File-Struktur bleibt unverändert. Für jedes der fünf Spielelemente haben wir eine Sprite-Klasse angelegt, wobei Sprite die allen gemeinsame Oberklasse bildet. Sprite: Definiert die generellen Eigenschaften der Sprites. Individuelles Verhalten legen wir durch neue Methoden in den Kindklassen oder durch Überschreiben der Elternmethoden fest. Zombie: Unsere untoten Gesellen, die dadurch charakterisiert sind, dass sie stets hinter die grüne Linie wollen. Player: Dabei handelt es sich um den Master-Zombie, der fast alle Eigenschaften eines normalen Zombies besitzt (und ebenfalls auf Berührungen reagiert), aber nicht stets nach oben will. Car: Die Autos. Sobald der untere Bildschirmbereich verlassen wird, lassen wir das jeweilige Auto erneut von oben herabfahren. Animation: Sorgt für Animationseffekte, die in der Regel zentriert über einem (rechteckigen) Sprite abgespielt werden sollen. Reanimator: Ein unheilvolles Gerät, das in der Lage ist, mit etwas OOP untote Heerscharen zu erzeugen, digital und unbegrenzt – sollte nicht in die falschen Hände geraten.
Mit dieser Übersicht verlassen wir die Sprites vorerst und sehen uns den Rest der Klassenstruktur an, die mit dem bereits bekannten Delegaten, der MainView-Klasse und dem neuen GameManager lediglich drei Klassen enthält. Der Delegate sorgt wie bisher für die Taktung der Spielschleife und ruft die drawRect:Methode der MainView-Klasse auf. Die Klasse ist unverändert gegenüber den bisherigen Beispielen. Die MainView-Klasse haben wir dagegen entschlackt, da wir alle spielbezogenen Eigenschaften in den neuen GameManager verlagern wollen. Listing MainView.h #import #import "GameManager.h"
114
3.10 Und jetzt alle(s) zusammen: GameManager und Sprite-Verwaltung
Wie Sie sehen, stellt die MainView-Klasse nun nur noch eine Durchgangsstation dar, die die Eigenheiten der iOS-Plattform von der eigentlichen Spiellogik fernhält.
115
3 Spiele entwickeln – von 0 auf 180
Die Methoden zur Touch-Steuerung informieren den Manager lediglich darüber, wie die Koordinaten (CGPoint) der aktuellen Fingerberührung des Displays lauten und ob der Finger abgesetzt wurde (touchEnded). Den zentralen Einstiegspunkt können Sie in der drawRect:-Methode erkennen. Pro Frame wird die drawStatesWithFrame:-Methode des Managers aufgerufen, der die aktuellen Dimensionen des Displays in Form einer CGRect-Struktur erhält. Zuvor wird der Manager einmalig initialisiert: if (!gameManager) { gameManager = [GameManager getInstance]; }
Da wir den GameManager in allen Klassen einsetzen wollen und darüber hinaus wissen, dass es nur einen GameManager geben kann, legen wir diesen als Singleton an. Die Klassenmethode getInstance liefert uns daher stets die gleiche Objektinstanz zurück. Diese Methode stellt gleichzeitig sicher, dass es zur Laufzeit des Programms nur eine Manager-Instanz geben kann. Ein weiterer Vorteil der Klassenmethode getInstance ist, dass wir in jeder Klasse Zugriff auf den Manager erhalten, die den GameManager-Header importiert. Die Implementierung der getInstance-Methode in der GameManager-Klasse basiert auf dem Singleton-Pattern: + (GameManager*) getInstance { static GameManager* gameManager; if (!gameManager) { gameManager = [[GameManager alloc] init]; NSLog(@"gameManager Singleton angelegt!"); [gameManager preloader]; } return gameManager; }
Das +-Zeichen spezifiziert die Methode als Klassenmethode, das heißt, sie benötigen keine Objektinstanz, um die Methode aufzurufen, es genügt der Klassenname. Dennoch bekommen Sie eine Instanz zurückgeliefert, die beim erstmaligen Aufruf der getInstanceMethode angelegt wird. Die gameManager-Instanz ist statisch deklariert und daher nur einmal vorhanden. Die if-Abfrage sorgt dann schließlich dafür, dass die Instanz erstmalig angelegt wird. Dies ist außerdem ein guter Ort, um über einen Preloader die benötigten Ressourcen für ein Spiel zu laden. Wie Sie sehen, rufen wir hier die preloader-Methode des Managers auf, welche einmalig globale Ressourcen lädt. Damit unser Spiel ohne Verzögerungen starten kann, sorgt die Methode über state = LOAD_GAME;
noch dafür, dass die aktuellen Spieldaten in der loadGame-Methode geladen werden. Da dieser Vorgang während des erstmaligen Aufrufs der drawRect:-Methode der MainViewKlasse stattfindet, wird der Splash-Screen der App so lange angezeigt, bis der Ladevorgang abgeschlossen ist. Aber wir wollen nicht vorgreifen. Sehen wir uns zunächst die Struktur der GameManagerKlasse an, die eine ganze Reihe von Aufgaben zu bewältigen hat und dementsprechend
116
3.10 Und jetzt alle(s) zusammen: GameManager und Sprite-Verwaltung
auch etwas größer ausfällt. Xcode bietet hier mit den Pragma-Direktiven eine vorzügliche Möglichkeit an, den Code zu strukturieren. Codestrukturierung
Wenn Sie im Quelltextfenster der Manager-Implementierung auf das Funktionsmenü in der Navigationsbar klicken, sehen Sie alle Methoden der Klasse auf einen Blick.
Abb 3.18: Methoden-Navigation in Xcode
Achten Sie darauf, dass in den Xcode-Preferences unter "Codes Sense" die Checkbox neben "Sort list alphabetically" nicht aktiviert ist. Das Popup zeigt uns nun alle Methoden der Klasse an, und wir können direkt zu der gewünschten Methode springen, ohne lange scrollen zu müssen. Außerdem sehen Sie, dass die GameManager-Klasse in drei Bereiche unterteilt ist. Dies erfolgt über eine Pragma-Direktive, die im Quelltext gesetzt wurde: #pragma mark ====== Game Handler ======
Natürlich sehen Sie die Methodenübersicht auch im Header. Das Funktionsmenü jedoch bietet zusätzlich die Möglichkeit der direkten Navigation zu den jeweiligen Methoden – ein Feature, das gerade bei großen Klassen unnötiges Scrollen vermeidet. Für unser Beispiel kommen wir mit den folgenden Unterteilungen aus: Init Methods: Hier liegen alle Methoden, die mit der Erzeugung von Objekten zu tun haben. Insbesondere neue Sprites werden hier angelegt. Auch der Preloader findet hier seinen Platz. Game Handler: Der Game Handler-Bereich dient der eigentlichen Steuerung des Spielverlaufes, nicht nur der User-Input wird hier behandelt, sondern auch die Sta-
117
3 Spiele entwickeln – von 0 auf 180
te-Machine sorgt für eine saubere Trennung der Spielzustände. In der playGameMethode findet dann das eigentliche Spiel statt. Helper Methods: Kein Spiel kommt ohne Tools aus bzw. Hilfsmethoden, die triviale, spielbezogene Aufgaben erfüllen oder aber unveränderliche und immer wiederkehrende Arbeiten erledigen, die unabhängig vom jeweiligen Spielkonzept gemacht werden; wie zum Beispiel das Verwalten der Ressourcen und Sprites. Dieser Bereich ist daher am ehesten dafür geeignet, später in eine eigene Klasse gekapselt zu werden. Ressourcen verwalten
Sehen wir uns zunächst eine Aufgabe des GameManagers an, ohne die jedes noch so unscheinbare Spiel unweigerlich baden geht: Die Ressourcenverwaltung von Grafiken, Sounds oder auch Leveldaten – für unser Spiel ist bereits eine stattliche Anzahl von neun Grafiken zusammengekommen. Jede Grafik belegt Arbeitsspeicher und benötigt eine gewisse Zeit zum Laden.6 Bisher haben wir uns bei unseren ersten Sprite-Gehversuchen nicht weiter darum gekümmert. Aber nun wird es Zeit, das Ganze etwas professioneller anzugehen und die Sprite-Klasse nochmals ein wenig aufzubohren. Warum? Jedes Sprite verfügt über ein oder mehrere PNGs, die mithilfe der UIImage-Klasse aus dem Filemanager des iPhone iOS geladen werden: UIImage *pic = [UIImage imageNamed: picName];
Die Variable pic (vom Typ UIImage) stellt dabei lediglich einen Zeiger auf den Speicherbereich dar, in dem das PNG geladen wurde. Während das PNG-Format die komprimierte und damit platzsparende Speicherung der Bildpunkte erlaubt, liegt ein Bild im Arbeitsspeicher unkomprimiert vor. So ist der Straßenhintergrund background.png bei einer Größe von 320x480 Pixeln lediglich rund 8 KB groß. Viele Bereiche des Bildes sind einfarbig, es werden nur wenige Farben verwendet usw. Im Arbeitsspeicher wird das Bild jedoch pro Pixel abgelegt. Unabhängig vom Motiv belegt ein Bild im Speicher maximal den folgenden Platz:
Platzverbrauch in Bytes = Breite x Höhe x 4 Mit anderen Worten: Pro Pixel wird die Farbinformation (RGB plus Alpha) mit 4 Bytes gespeichert. Damit belegt unser Hintergrundbild bereits 600 KB. Hinzu kommt: Egal wie oft Sie das gleiche PNG mithilfe der Klassenmethode imageNamed laden, es wird jedes Mal ein neuer Zeiger auf einen neuen Speicherbereich angelegt. Wenn wir also ein 10 KB großes Bild für ein Sprite benötigen und dieses Sprite 100 Mal erzeugen, belegt das Sprite bereits fast 1 Megabyte. Sie sehen, es lohnt sich, sich Gedanken über die Bildressourcen zu machen.
6 Auf vielen mobilen Spieleplattformen sind das Erzeugen und Zerstören von Objekten zur Laufzeit keine gute Idee: Da für diesen Vorgang kurzzeitig Speicher bereitgestellt bzw. freigegeben werden muss, kommt es zu einem kurzen Stottereffekt. Nicht so beim iPhone OS: Die Plattform reagiert hier relativ robust, solange wir das Erzeugen von Grafiken oder anderen aufwändigeren Operationen während der Objektallokation vermeiden.
118
3.10 Und jetzt alle(s) zusammen: GameManager und Sprite-Verwaltung
Wir müssen also nun dafür sorgen, dass ein Bild, das bereits geladen wurde, wiederverwendet werden kann. Dadurch sparen wir zusätzlich den Ladevorgang ein. Da wir während des Spiels neue Sprites anlegen und löschen wollen, müssen wir alle aufwändigeren Routinen aus der Initialisierung fernhalten. Abhilfe schafft eine Hashtable, die uns im Gegensatz zu einem Array direkten Zugriff auf eine Ressource bietet (Key/Value). Wir verwenden dafür das NSMutableDictionary des Foundation Frameworks. Das Dictionary (bzw. die Hashtable) sollte veränderlich sein (mutable), weil wir später vielleicht andere Level mit anderen Ressourcen laden wollen und die aktuell geladenen Ressourcen löschen müssen, falls der Speicherplatz nicht ausreichen sollte. Im GameManager haben wir das Dictionary wie folgt angelegt: - (NSMutableDictionary *) getDictionary { if (!dictionary) { dictionary = [[NSMutableDictionary alloc] init]; NSLog(@"Dictionary angelegt!"); } return dictionary; }
Über die if-Abfrage stellen wir sicher, dass die Ressourcentabelle nur einmal angelegt wird. Um nun Ressourcen darin abzulegen, legen wir pro Ressourcentyp eine weitere Hilfsmethode an. Da wir für unser Spiel verschiedene Grafiken benötigen, begnügen wir uns mit einer getPic:-Methode: - (UIImage *) getPic: (NSString*) picName { @try { UIImage *pic = [[self getDictionary] objectForKey: picName]; if (!pic) { pic = [UIImage imageNamed: picName]; [[self getDictionary] setObject: pic forKey: picName]; int memory = pic.size.width*pic.size.height*4; NSLog(@"%@ gespeichert, Size: %i KB", picName, memory/1024); [pic release]; } return pic; } @catch (id theException) { NSLog(@"ERROR: %@ not found!", picName); } return nil; }
Über die NSDictionary-Methoden objectForKey: und setObject:forKey: können vorhandene Einträge abgerufen bzw. neue angelegt werden. Als Parameter verwenden wir den Bildnamen. Ist das Bild noch nicht im Dictionary vorhanden, gibt die objectForKey:Methode nil zurück, und wir können im if-Teil das Bild anlegen und in der Tabelle ablegen. Als Key verwenden wir hier natürlich wiederum den Bildnamen. Zusätzlich lassen wir uns noch den Speicherverbrauch des Bilds ausgeben. Das Ganze haben wir zusätzlich
119
3 Spiele entwickeln – von 0 auf 180
in eine try-catch-Klausel verpackt, um Fehler abzufangen – beispielsweise falls der Bildname falsch geschrieben wurde. Um Speicher-Leaks zu vermeiden, ist es wichtig, dass Sie wissen, dass das NSDictionary den retainCount der abgelegten Objekte um 1 erhöht. Wird das Bild angelegt, ist retainCount = 1. Fügen wir es der Tabelle hinzu, liegt der schon bei 2. Sollte das Dictionary nun released werden, wird der retainCount aller darin enthaltenen Objekte wiederum um 1 vermindert. Unser Pic würde dann wieder bei 1 stehen und nicht gelöscht werden. Wie Sie wissen, dealloziert das iOS SDK alle Objekte automatisch, deren Retain-Wert = 0 ist. Deshalb rufen wir nach Hinzufügen des Bildes die release-Methode auf, und in der dealloc-Methode des Managers sorgen wir mit
retainCount
[[self getDictionary] release];
dafür, dass nicht nur das Dictionary, sondern auch alle darin enthaltenen Objekte abgeräumt werden, sofern ihr Retain-Wert zuvor 1 betrug. Sie können sich per NSLog(@"%@ retain count: %i", picName, [pic retainCount]);
den Wert jederzeit anzeigen lassen. Ein weiterer Vorteil der NSDictionary-Klasse: Sie brauchen sich beim Verwenden von Bildern keine Gedanken mehr über den release zu machen, da dies die Aufgabe des Managers ist. Statt nun ein neues UIImage in der Sprite-Klasse anzulegen, rufen wir stattdessen die getPic:-Methode im Initialisierer auf: pic = [[GameManager getInstance] getPic: picName];
Der Zeiger pic der Sprite-Instanz verweist nun stets an die gleiche Speicherstelle, egal wie häufig das Bild von anderen Klassen geladen wurde. Weiterhin muss die getPic:Methode nur den Zeiger auf das aktuelle Bild zurückgeben, sofern es bereits zuvor geladen wurde. Dadurch erhöht sich die Performance nicht unwesentlich. Sprite-Verwaltung
An die Sprite-Verwaltung stellen wir nun erweiterte Anforderungen: Bisher mussten wir einfach das Array, in dem die Sprite-Objekte abgelegt wurden, durchlaufen, um alle Sprites anzuzeigen. Nun wollen wir jedoch neue Sprites während des Spiels erzeugen oder löschen. Der Reanimator versinnbildlicht dies in unserem Spielkonzept. Aber auch für fast alle anderen Spielkonzepte wollen Sie gegebenenfalls neue Sprites on the fly erzeugen: beispielsweise neue Gegner, Items zum Aufsammeln, bewegliche Hindernisse usw. Auch einmalig stattfindende Ereignisse, wie Explosionen, lassen sich am einfachsten über Sprites realisieren, die erzeugt werden, um die Explosionsanimation ablaufen zu lassen, und aus dem Speicher gelöscht werden, wenn die Explosionssequenz abgelaufen ist. Eigentlich sollte dies doch mit Arrays ohne Probleme machbar sein. Der Haken ist nur, dass
120
3.10 Und jetzt alle(s) zusammen: GameManager und Sprite-Verwaltung
zwar dynamisch Elemente hinzufügen oder löschen können, dies allerdings nicht erfolgen darf, während sie ein Array durchlaufen.7
NSMutableArrays
Versuchen Sie es dennoch, erhalten Sie die schöne Fehlermeldung: "Arrays cannot be mutated while being enumerated." – der Grund: Die gespeicherten Indices würden sich sonst verändern, und das Array wäre nicht mehr vernünftig iterierbar. Abhilfe verschafft uns hier ein kleiner Trick. Wir legen zwei zusätzliche Arrays an, in denen wir jeweils die neuen oder die zu löschenden Objekte ablegen: //Aktive Sprites, die gerendert werden sollen NSMutableArray *sprites; //Neue Sprites, die der sprites-Liste hinzugefügt werden sollen NSMutableArray *newSprites; //Inaktive Sprites, die gelöscht werden sollen NSMutableArray *destroyableSprites;
Die Arrays legen wir dann im Preloader des Managers einmalig an: sprites = [[NSMutableArray alloc] initWithCapacity:20]; newSprites = [[NSMutableArray alloc] initWithCapacity:20]; destroyableSprites = [[NSMutableArray alloc] initWithCapacity:20];
Doch was nun? Sehen Sie sich die manageSprite-Methode an, die wir zu Beginn eines jeden Rendervorgangs aufrufen: - (void) manageSprites { //Cleanup for (Sprite *destroyableSprite in destroyableSprites) { for (Sprite *sprite in sprites) { if (destroyableSprite == sprite) { [sprites removeObject: sprite]; break; } } } //Neue Sprites hinzufügen for (Sprite *newSprite in newSprites){ [sprites addObject: newSprite]; } [destroyableSprites removeAllObjects]; [newSprites removeAllObjects]; }
Um die Sprites zu rendern, verwenden wir nach wie vor das sprites-Array. In der manawerden jedoch pro Frame-Durchlauf die beiden neuen Arrays durchlaufen. Zunächst prüfen wir, ob sich in destroyableSprites Objekte befinden, die gelöscht werden sollen. Für jedes dieser Objekte steigen wir dann erneut in das sprites-
geSprites-Methode
7 Sowohl für Arrays als auch das Dictionary stehen unveränderliche Varianten zur Verfügung ohne den Zusatz "Mutable" im Klassennamen. Diese Varianten arbeiten grundsätzlich schneller als ihre veränderbaren Geschwister, allerdings nicht schnell genug, um in der Praxis auf den zusätzlichen Programmierkomfort verzichten zu wollen.
121
3 Spiele entwickeln – von 0 auf 180
Array ein und löschen das Objekt mithilfe der removeObject-Methode, sobald wir das Objekt gefunden haben. Der Gleichheitsoperator prüft hier nicht die Objekte auf Gleichheit, sondern deren Zeiger und arbeitet daher sehr schnell. Doch Obacht: Sobald das Objekt gelöscht wurde, müssen wir die innere for-Schleife mittels break verlassen, da wir uns ansonsten erneut die bereits erwähnte Exception einfangen würden. Danach geht es dann in der äußeren for-Schleife mit dem nächsten destroyable-Objekt weiter. Neue Sprites werden auf ähnliche Weise dem sprites-Array hinzugefügt. Da wir hier jedoch die addObjekt-Methode auf dem sprites-Array aufrufen, das aktuell nicht durchlaufen wird, können wir die Angelegenheit in einem Rutsch erledigen. Nach Abschluss der Anlege- und Löscharbeiten können wir optional noch beide HilfsArrays mit removeAllObjects komplett entleeren, falls sich fremde Objekte eingeschlichen haben sollten (man weiß ja nie, wie groß ein Projekt später wird und welche Programmierer Unfug treiben). Um die Arbeitsweise des Sprite-Managers zu verfolgen, können Sie sich den aktuellen Inhalt der drei Arrays wie folgt ausgeben lassen: NSLog(@"Sprites: %i destroyable: %i new: %i", [sprites count], [destroyableSprites count], [newSprites count]);
Nun müssen wir natürlich noch dafür sorgen, dass die beiden Hilfs-Arrays auch eingesetzt werden. Wie sorgen wir beispielsweise dafür, dass ein Sprite dem destroyableSpritesArray hinzugefügt werden soll? Zunächst stellen wir fest, dass in der playGame:-Methode das gesamte Handling der Sprites in zwei Zeilen abgehandelt wird: - (void) playGame { ... [self manageSprites]; [self renderSprites]; ... }
Nachdem die manageSprite-Methode durchlaufen ist, rufen wir die renderSpritesMethode auf, die gegenüber den bisherigen Beispielen nur geringfügig erweitert wurde: - (void) renderSprites { for (Sprite *sprite in sprites) { if ([sprite isActive]) { [self checkSprite: sprite]; [sprite draw]; } else { [destroyableSprites addObject: sprite]; } } }
Einerseits iterieren wir wie bisher das sprites-Array und rufen die draw-Methode der Sprite-Klasse auf, andererseits prüfen wir jedes Element, ob es aktiv ist. Die isActive-Methode gibt den aktuellen Wert des active-Members der Sprite-Klasse zurück. Wir haben die Sprite-Klasse um diesen Boole'schen Wert zuvor erweitert. Wann
122
3.10 Und jetzt alle(s) zusammen: GameManager und Sprite-Verwaltung
immer wir der Meinung sind, dass ein Sprite nicht mehr gebraucht wird, setzen wir active = false. Die renderSprites-Methode braucht den Render-Vorgang nun nur noch für die aktiven Sprites anzustoßen, inaktive Sprites werden unverzüglich dem destroyableSprites-Array zugeführt und aus dem Speicher gelöscht. Bevor die draw-Methode eines aktiven Sprites aufgerufen wird, inspizieren wir das Sprite etwas genauer: Wir haben dafür die checkSprite:-Methode hinzugefügt, in der wir spielbezogene Aufgaben mit dem Sprite durchführen können. Während die renderSpritesMethode unabhängig vom Spielkonzept stets gleich arbeitet und daher in den HelperBereich gehört, unterstützt die checkSprite:-Methode den aktuellen Spielverlauf: - (void) checkSprite: (Sprite *) sprite { if ([sprite getType] == ZOMBIE || [sprite getType] == PLAYER) { for (Sprite *sprite2test in sprites) { if ([sprite2test getType] == CAR) { if ([sprite checkColWithSprite: sprite2test]) { [sprite hit]; } } if ([sprite getType] == PLAYER && [sprite2test getType] == REANIMATOR) { if ([sprite checkColWithSprite: sprite2test]) { [(Reanimator *) sprite2test reanimate]; } } } } }
Wie Sie an der Implementierung sehen können, führen wir für jedes Sprite anhand seines Typs bestimmte Arbeiten durch: Zombies und der Spieler werden auf Kollision mit den Auto-Sprites geprüft. Findet eine Kollision statt, rufen wir die neue Sprite-Methode hit auf. Diese haben wir in der Zombie- und der Player-Klasse überschrieben, um zu definieren, was passieren soll, wenn ein Zombie bzw. der Player angefahren wurde. Der Spieler wird zusätzlich auf Kollision mit dem Reanimator-Item geprüft. Ist das der Fall, casten wir das Reanimator-Sprite, um die reanimate-Methode der Reanimator-Klasse aufrufen zu können. Was in dieser unheilvollen Methode passiert, sehen wir uns weiter unten genauer an.
Wir wissen, wie Sprites „on the fly“ aus dem Speicher gelöscht werden können. Aber wie erzeugen wir sie? Nichts leichter als das! Wir haben ja bereits mehrere Sprites erzeugt. Was wir nun bei der Verwendung des GameManagers noch leisten müssen, ist lediglich eine einheitliche Schnittstelle: - (void) createSprite: (int) type speed: (CGPoint) sxy pos: (CGPoint) pxy;
123
3 Spiele entwickeln – von 0 auf 180
Jedes Sprite unseres Spiels verfügt über einen Typ, eine Geschwindigkeit und eine Anfangsposition. Um individuellere Eigenschaften festzulegen, verzweigen wir in der Methode auf den jeweiligen Typ. Doch Achtung: Wir können dabei kein switch-caseStatement einsetzen. Objective-C verbietet das Erzeugen von Objekten in einer caseAnweisung, deshalb verzweigen wir hier über if-Blöcke. Der Ablauf sieht dann pro Typ mehr oder weniger ähnlich aus: Zombie *zombie = [[Zombie alloc] initWithPic: frameCnt: frameStep: speed: pos: [zombie setType: ZOMBIE]; [newSprites addObject: zombie]; [zombie release];
@"zombie_4f.png" 4 3 sxy pxy];
Wichtig ist hierbei, dass Sie die neue Sprite-Instanz dem newSprites-Array hinzufügen. Dieses wird dann wie zuvor beschrieben in der manageSprites-Hilfsmethode dem eigentlichen sprites-Array hinzugefügt. Um nun ein neues Sprite anzulegen, sehen wir uns an, wie der Reanimator arbeitet. Dieser verfügt lediglich über eine Methode und einen Counter, der pro Frame hochgezählt wird und sicherstellt, dass alle fünf Frames nur ein Zombie erschaffen wird: - (void) reanimate { counter++; if (counter % 5 == 0) { int px = [[GameManager getInstance] getRndBetween: 10 and: W-30]; int py = [[GameManager getInstance] getRndBetween: H and: H+100]; [[GameManager getInstance] createSprite: ZOMBIE speed: CGPointMake(0, -1) pos: CGPointMake(px, py)]; [[GameManager getInstance] drawString: @"Re-Animate!" at: CGPointMake(pos.x-50, pos.y-30)]; } }
Kinderleicht, oder? Beachten Sie, wie wir mithilfe des GameManagers noch zwei weitere Hilfsmethoden aufrufen. Einmal, um die zufällige Position eines neuen Unholds zu bestimmen, und einmal, um mit der drawString:-Methode eine flackernde "ReAnimate!"-Aktivitätsanzeige auf das Display zu rendern. Für unser Spiel werden nur die Zombies zur Laufzeit neu erzeugt. Die anderen Objekte (Reanimator-Items, Player, Car) werden einmalig zu Beginn eines neuen Spiels in der loadGame-Methode des Managers angelegt. Bis auf eine Ausnahme: Wird ein Zombie überfahren, soll dieser nicht nur auf inaktiv gesetzt werden – zusätzlich soll eine Verpuffungsanimation abgespielt werden, um anzuzeigen, dass dieser an seinen ursprünglichen Aufenthaltsort zurückgekehrt ist.8 Auch bei dieser einmalig stattfindenden
124
3.10 Und jetzt alle(s) zusammen: GameManager und Sprite-Verwaltung
Aufenthaltsort zurückgekehrt ist.8 Auch bei dieser einmalig stattfindenden Animation handelt es sich um ein Sprite, das sich nach Ablauf der Animationssequenz selbst über active = false löscht. Doch der Reihe nach. Einmalige Animationsabläufe (One-Shots)
Häufig sollen Animationen nicht endlos geloopt abgespielt werden. Manchmal braucht man nur eine einmalig stattfindende Animation, beispielsweise um das Aufsammeln eines Gegenstandes zu animieren oder ein gegnerisches Raumschiff explodieren zu lassen. Für unser Spiel benötigen wir eine Frame-Sequenz, die wir abspielen, sobald der Zombie keine Lebensenergie mehr hat. Derartige Explosionseffekte werden in Spielen sehr häufig benötigt. Durch Veränderung von Alpha- und Farbwerten in einem Partikelsystem lassen sich diese auch in Echtzeit erzeugen. Oftmals werden aus Performance-Gründen alternativ vorgerenderte Animationsphasen verwendet, die wie die Laufsequenz unseres kleinen Zombies aus einzelnen Frames bestehen. Diese Variante stellen wir nun vor. Die Grundlage der Verpuffungsanimation bildet eine sechsphasige Einzelbildsequenz:
Abb 3.19: smoke_6f.png
Das Prinzip einer einmalig ablaufenden Animation ist immer gleich:
1. Erzeuge ein neues Animationsobjekt. 2. Spiele dieses an der Position xy ab. 3. Lösche das Animationsobjekt, sobald die Animation beendet ist. Punkt 2 + 3 haben wir bereits durch die Sprite-Verwaltung gelöst (dies ist auch der Grund, warum wir das One-Shot-Thema erst jetzt aufgreifen). Um festzustellen, wann die Animationssequenz zu Ende ist, müssen wir jedoch eine kleine Änderung an der Sprite-Klasse vornehmen: - (void) drawFrame { frameNr = [self updateFrame]; ... [self renderSprite]; } - (void) renderSprite { ... } - (int) updateFrame {
8 Man verzeihe uns an dieser Stelle das martialische Vorgehen gegen die pixeligen Gesellen. Wir wissen ja bereits, wie wir diese später reanimieren können.
125
3 Spiele entwickeln – von 0 auf 180 if (frameStep == cnt) { cnt = 0; frameNr++; if (frameNr > frameCnt-1) { frameNr = 0; cycleCnt++; } } cnt++; return frameNr; }
Zunächst haben wir die drawFrame-Methode aufgesplittet in die Methoden renderSprite und updateFrame – dies ermöglicht uns, die Methoden bei Bedarf zu überschreiben. Weiterhin haben wir in der updateFrame-Methode den Zähler cycleCnt hinzugefügt. Dieser wird immer dann um 1 erhöht, sobald ein Animationszyklus vollständig durchgelaufen ist. Für die Verpuffungsanimation benötigen wir lediglich einen Durchlauf, da die Rauchanimation ja nur einmal ablaufen soll. Wir brauchen also nur noch die drawFrame-Methode in der Animation-Klasse zu überschreiben: - (void) drawFrame { frameNr = [self updateFrame]; if (cycleCnt == 1) { active = false; } if (active) { [self renderSprite]; } }
In dieser prüfen wir lediglich den Wert von cycleCnt: Ist die Rauchsequenz bereits vollständig durchgelaufen mit der eingestellten Anzahl an frameSteps, dann setzen wir active auf false, und der GameManager erledigt den Rest für uns. Allerdings müssen wir uns noch überlegen, wo die Animation platziert werden soll. Insbesondere Explosionseffekte oder eben auch unser Verpuffungsrauch haben oftmals eine andere Größe als die Spielfiguren, sollen aber dennoch mittig über diesen platziert werden. Deshalb haben wir der Animationsklasse eine weitere Klassenmethode spendiert, die von diesem Mittelpunkt aus die linke, obere Ecke ermittelt, an der die Animation schließlich gezeichnet werden soll: + (CGPoint) getOriginBasedOnCenterOf: andPic: withFrameCnt: UIImage *picSlave = [[GameManager
//Mittelpunkt Master int xmm = rectMaster.origin.x + rectMaster.size.width/2; int ymm = rectMaster.origin.y + rectMaster.size.height/2; //Origin der Animation int xs = xmm-picSlave.size.width/2/fcnt; int ys = ymm-picSlave.size.height/2;
126
3.10 Und jetzt alle(s) zusammen: GameManager und Sprite-Verwaltung return CGPointMake(xs, ys); }
Damit sich der Aufruf der Animation trotzdem noch unkompliziert gestaltet, kapseln wir die Funktionalität im GameManager: - (void) createExplosionFor: (Sprite *) sprite { CGPoint p = [Animation getOriginBasedOnCenterOf: [sprite getRect] andPic: @"smoke_6f.png" withFrameCnt: 6]; [self createSprite: ANIMATION speed: CGPointMake(0, 0) pos: p]; }
Bevor wir also den standardisierten Erzeugungsmechanismus für die Sprites aufrufen, bemühen wir die Klassenmethode getOriginBasedOnCenterOf:, um die exakte Position zu ermitteln – und zwar anhand des Animationsbildes und des Sprite-Rechteckes, über das die Animation zentriert gelegt werden soll. Der eigentliche Aufruf der Rauchsequenz gestaltet sich dann wesentlich einfacher: [[GameManager getInstance] createExplosionFor: self];
Da die createExplosionFor:-Methode als Parameter ein Sprite erwartet, kann der Aufruf beliebig in einer Sprite-Klasse eingesetzt werden. Für unser Spiel platzieren wir den Aufruf naheliegenderweise in der hit-Methode der Zombie-Klasse. State-Machines: Organisation leicht gemacht
Die Frameworks Core Graphics und OpenGL arbeiten mit State-Machines, aber auch für ein Spiel bietet es sich an, einen einfachen Zustandsautomaten einzusetzen. Wie nicht anders zu erwarten, definieren wir diesen im Header des GameManagers: enum states { LOAD_GAME, START_GAME, PLAY_GAME, GAME_OVER, GAME_WON };
//Spieldaten werden geladen //Spiel kann gestartet werden //Spiel läuft //Spiel wurde verloren //Spiel wurde gewonnen
Natürlich sind noch weitere Zustände denkbar, aber um zu demonstrieren, wie man den Programmablauf mithilfe einer State-Machine festlegen kann, sollte dieses Beispiel genügen. Die Behandlung der Zustände erfolgt in der zentralen Einstiegsmethode drawStatesWithFrame:, die von der MainView-Klasse pro Frame aufgerufen wird. - (void) drawStatesWithFrame: (CGRect) frame { W = frame.size.width; H = frame.size.height; switch (state) { case LOAD_GAME: [self loadGame]; state = START_GAME; break;
127
3 Spiele entwickeln – von 0 auf 180 case START_GAME: [background drawAtPoint: CGPointMake(0, 0)]; [self drawString: @"Welcome!" at: CGPointMake(5, 5)]; ... [self drawString: @"Tap screen to start!" at: CGPointMake(5, 145)]; break; case PLAY_GAME: [self playGame]; break; case GAME_OVER: [background drawAtPoint: CGPointMake(0, 0)]; [self drawString: @"G A M E OVER" at: CGPointMake(5, 5)]; ... [self drawString: @"Tap screen!" at: CGPointMake(5, 85)]; break; case GAME_WON: [background drawAtPoint: CGPointMake(0, 0)]; [self drawString: @"Y O U M A D E I T !" at: CGPointMake(5, 5)]; ... [self drawString: @"Tap screen!" at: CGPointMake(5, 85)]; break; default: NSLog(@"ERROR: Unbekannter Spielzustand: %i", state); break; } }
Für jeden Zustand können wir nun definieren, was passieren soll. Nachdem der Zustand LOAD_GAME beendet wurde, wechselt das Programm automatisch in den START_GAMEZustand. Für den PLAY_GAME-Zustand wird lediglich die playGame-Methode aufgerufen, und der START_GAME-Zustand wartet schließlich auf eine Benutzereingabe. Um einen Zustandswechsel zu erzwingen, können wir den GameManager extern aufrufen, zum Beispiel in der Player-Klasse, wenn der Spieler seine Lebenspunkte verloren hat: - (void) hit { [argghPic drawAtPoint: CGPointMake(pos.x, pos.y - argghPic.size.height)]; health--; if (health == 0) { [[GameManager getInstance] setState: GAME_OVER]; } }
Benutzereingaben
Um zu bestimmen, wann der Benutzer mit dem GameManager interagiert, haben wir drei Schnittstellen deklariert, die von der MainView-Klasse wie eingangs beschrieben mit Input gefüttert werden:
Sobald die touchBegan:-Methode aufgerufen wurde, wissen wir, dass der User einen Tap auf das Display ausgeführt hat. Wir können diese Information für die State-Machine verwenden, um vom Willkommens-Screen zum Spiel-Screen zu wechseln: - (void) handleStates { if (state == START_GAME) { state = PLAY_GAME; } else if (state == GAME_OVER || state == GAME_WON) { state = LOAD_GAME; } }
Die handleStates-Methode braucht dafür nur in der touchBegan:-Methode aufgerufen zu werden. Befindet sich das Spiel beispielsweise im Zustand GAME_OVER, sorgt ein Single Tap dafür, dass die handleStates-Methode in den LOAD_GAME-Zustand und danach automatisch in den START_GAME-Zustand umschaltet. Aber die drei Touch-Methoden leisten noch mehr. Für unser Spiel soll nicht nur die Spielfigur per Swipe gesteuert werden, sondern auch jeder neu erschaffene Zombie. Wir müssen in den Methoden also jeweils durch alle Sprites iterieren und die Nachricht an diese weiterleiten: - (void) touchBegan: (CGPoint) p { [self handleStates]; if (state == PLAY_GAME) { for (Sprite *sprite in sprites) { if ([sprite isActive]) { if ([sprite getType] == ZOMBIE || [sprite getType] == PLAYER) { [(Zombie *) sprite setTouch: p]; } } } } } - (void) touchMoved: (CGPoint) p { if (state == PLAY_GAME) { [self touchBegan: p]; } } - (void) touchEnded { if (state == PLAY_GAME) { for (Sprite *sprite in sprites) { if ([sprite isActive]) { if ([sprite getType] == ZOMBIE
Der Touch-Punkt wird so bis zu der setTouch:-Methode der Zombie-Klasse durchgereicht: - (void) setTouch: (CGPoint) point { if ([self checkColWithPoint: point]) { touchAction = true; } if (touchAction && !saved) { touchPoint = point; speed.x = (touchPoint.x - pos.x)/20; //deltaX speed.y = (touchPoint.y - pos.y)/20; //deltaY } } - (void) touchEnded { touchAction = false; }
Sie sehen, genauso hatten wir den Swipe bereits im UserInput-Beispiel eingesetzt, mit dem Unterschied, dass wir den Swipe hier nur noch für einen bestimmten Sprite-Typ erlauben. Deshalb haben wir der Sprite-Klasse eine weitere Kollisionsmethode in Anlehnung an das bereits vorgestellte Kollisionsbeispiel spendiert: if ([self checkColWithPoint: point]) { touchAction = true; }
Der Flag touchAction wird nur dann auf true gesetzt, wenn der Finger mit dem Sprite kollidiert. Ansonsten haben wir der Zombie-Klasse nur noch einen saved-Flag hinzugefügt, der aktiviert wird, sobald das Sprite die grüne Demarkationslinie erreicht. Damit der Zombie nach Beendigung der Swipe-Geste seine ursprüngliche Marschrichtung aufnimmt, wird die draw-Methode wie folgt überschrieben: - (void) draw { [super draw]; //Stop movement if (speed.x > 0 if (speed.x < 0 if (speed.y > 0 if (speed.y < 0
3.10 Und jetzt alle(s) zusammen: GameManager und Sprite-Verwaltung if (pos.y < -30) { active = false; [[GameManager getInstance] savedZombie]; } } else { saved = false; } }
Außerdem sehen Sie hier die Behandlung der grünen Linie: getTargetY liefert die y-Position. Befindet sich das Sprite oberhalb dieses Wertes, wird der saved-Flag auf true gesetzt, und das Sprite wird beschleunigt, um möglichst schnell mit der Invasion beginnen zu können. Den saved-Flag benutzen wir übrigens auch in der hit-Methode der ZombieKlasse, um eine weitere Verletzung des Zombies durch die Autos auszuschließen. Schließlich haben wir die Untoten doch bereits gerettet! Die Swipe-Behandlung für den Player erfolgt analog – mit dem Unterschied, dass sich der auch oberhalb der grünen Linie noch in der Gefahrenzone befindet und nach Beendigung der Swipe-Geste stehen bleibt. Deshalb haben wir die draw-Methode in der PlayerKlasse nochmals überschrieben. Außerdem: Um zu verhindern, dass die Spielfigur im Eifer des Gefechts über die Bildschirmgrenzen hinausgeschoben wird, können wir optional in der draw-Methode noch den folgenden Check einbauen:
Dieser Check bewirkt, dass die Spielfigur stets in voller Größe sichtbar bleibt, auch an den Rändern. Eine nur halb sichtbare Figur wäre nur noch schwer berühr- und steuerbar. HUD und Punktezählung
Damit hätten wir die Hauptaufgaben des GameManagers beschrieben. Sowohl die SpriteBehandlung als auch die State-Machine sind dabei bewusst allgemein gehalten, sodass diese auch leicht in anderen Spielen wiederverwendet werden können. Auf individuelle Eigenheiten wie zum Beispiel die Swipe-Steuerung sind wir nochmals eingegangen. Mithilfe der State-Machine lässt sich auch eine einfache Menüführung realisieren. Und natürlich gehört zu einem Spiel auch eine ordentliche Punkteanzeige, damit der Spieler stets weiß, wie gut oder schlecht es gerade um ihn steht. Sehen wir uns abschließend noch einmal die finale playGame-Methode an: - (void) playGame { [background drawAtPoint: CGPointMake(0, 0)]; [self manageSprites]; [self renderSprites]; [self drawTargetLine]; NSString *hud = [NSString stringWithFormat: @"HLT: %i SVD: %i/%i LST: %i",
131
3 Spiele entwickeln – von 0 auf 180 [player getHealth], saved, savedMax, lost]; [self drawString: hud at: CGPointMake(5, 5)]; if (saved >= savedMax) { state = GAME_WON; } }
Das gesamte Spiel wird auf nicht einmal zehn Zeilen abgehandelt. Für die Anzeige des HUD setzen wir erneut die drawString:-Hilfsmethode ein. Es steht Ihnen frei, das HUD aufwändiger zu gestalten. Wenn Sie mit Strings arbeiten, können Sie die stringWithFormat-Klassenmethode der NSString-Klasse einsetzen, um ähnlich der Konsolenausgabe Zahlenwerte in den String zu integrieren. Die Punktezählung haben wir in die jeweiligen Sprite-Klassen integriert, die wiederum auf den GameManager zugreifen.
Abb 3.20: Screenshots des fertigen Spiels
Wenn Sie Lust haben, können Sie nun anhand der geretteten Zombies einen Bonus-Score vergeben und eine niedrige Anzahl an verlorenen Zombies mit Extrapunkten belohnen. Wie auch immer Sie sich entscheiden: Sie sollten zu diesem Zeitpunkt in der Lage sein, das Spiel nach Ihren eigenen Wünschen auszubauen bzw. als Ausgangspunkt für eigene Experimente zu nutzen. Auf das in diesem Kapitel entstandene Grundgerüst werden wir für die nächsten größeren Projekte erneut aufsetzen.
132
3.11 Zappp, Brzzz, Booom, Pennng! Hintergrundmusik und Sound-Effekte
3.11
Zappp, Brzzz, Booom, Pennng! Hintergrundmusik und Sound-Effekte Musik und Sound-Effekte sind für Spiele sehr wichtig. Vor gar nicht allzu langer Zeit war das zeitunkritische Abspielen von Hintergrund-MIDIs das verbreitetste akustische Gestaltungsmittel für Handyspiele – Echtzeit-Sound-Effekte fand man nur selten: Zu langsam waren die Geräte, zu schlecht und fehlerhaft die Implementierung der Schnittstellen. Wer sich noch an die Spielhallenklassiker aus den 80ern erinnert, der weiß, dass dadurch einiges an Atmosphäre verloren geht. Auch ruhmreiche Heimcomputer wie der C64 gaben den Spielen mit einem separaten Soundchip wie dem legendären SID-Chip erst den rechten Schliff. Für die Geräte der iOS-Familie stehen zum Glück mehrere Möglichkeiten zur Verfügung, um das Spielgeschehen gebührend zu vertonen. Die direkteste Lösung funktioniert über die sogenannten Systemsounds. Diese werden auch vom iOS UI eingesetzt und können über das AudioToolbox-/AudioServices-Framework eingebunden werden. Nach dem Erstellen eines Sounds mit einer eigenen ID kann der Sound über AudioServicesPlaySystemSound(soundID) jederzeit abgespielt werden. Der Nachteil: Polyphone Sounds sind nicht möglich, das heißt, es kann immer nur ein Sound gleichzeitig abgespielt werden. Und die Länge eines Sounds ist zudem auf 30 Sekunden begrenzt. Bessere Möglichkeiten bietet da schon OpenAL, insbesondere im Hinblick auf die realistische 3D-Positionierung von Sounds (vor allem wenn mit Kopfhörern gespielt wird). Die umfangsreichsten Möglichkeiten findet man jedoch über das Core Audio Framework und die Audio Queue Services. Damit ist sogar der Echtzeitzugriff auf den Audio-Buffer möglich. Wer Musiksoftware mit virtuellen Synthesizern entwickeln will, kommt hieran nicht vorbei. Da das Framework sehr nah an der Hardware ansetzt und daher viele LowLevel-Möglichkeiten bietet, ist der Code, um einen einfachen Sound abzuspielen, schon sehr umfangreich. Deshalb bietet das iOS SDK seit der Version 2.2 den AVAudioPlayer an, der ebenfalls Teil des Core Audio Frameworks ist, von diesem aber abstrahiert und daher mit vergleichsweise geringem Aufwand für die Spieleentwicklung eingesetzt werden kann. Vorbereitungen: Framework einbinden
Um Zugriff auf die Audio-Schnittstelle zu bekommen, müssen wir zunächst das AVFoundaeinbinden. Zunächst legen wir wieder ein neues Projekt namens "Sound" an, auf der Basis des GameManager-Projektes aus dem letzten Kapitel. Nicht benutzte Klassen und Ressourcen löschen wir (weiter unten werden wir die relevanten Teile genauer vorstellen). Um Sounds abzuspielen, soll, wie für Grafiken auch, die GameManagerKlasse den zentralen Rahmen bieten. Sie können den folgenden Code aber natürlich auch in jeder anderen Klasse Ihrer Wahl einbinden.
tion-Framework
Das generelle Vorgehen beim Einbinden eines Frameworks: Machen Sie einen Rechtsklick auf den Frameworks-Ordner. Wählen Sie: "Add -> Existing Frameworks"
133
3 Spiele entwickeln – von 0 auf 180 Nun sollten Sie die verfügbaren Frameworks des iOS SDK sehen. Wählen Sie das AVFoundation.framework aus. Importieren
Sie
das
Framework
im
Header:
#import
tion/AVFoundation.h>
Der absolute Pfad zum Framework lautet:
Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOSx.x.sdk/ System/Library/Frameworks/AVFoundation.framework Nun können Sie Ihre eigenen Sounds in jeder Klasse abspielen, die den AVFoundationFramework-Header einbindet: NSString *path = [[NSBundle mainBundle] pathForResource: @"track1.wav" ofType: nil]; AVAudioPlayer *sound = [[AVAudioPlayer alloc] initWithContentsOfURL: [NSURL fileURLWithPath: path] error: nil]; [sound play];
Wie Sie sehen, ist die Klasse etwas weniger komfortabel aufgebaut als beispielsweise UIImage: Um den Sound aus dem Ressourcen-Ordner zu holen, müssen Sie explizit den Pfad zur Datei angeben – die NSBundle- bzw. NSURL-Klassen unterstützen Sie dabei jedoch. Sie müssen nun nur noch dafür sorgen, dass die Sound-Datei track1.wav richtig in das Projekt über "Add -> Exising Files" eingebunden wird (wie bisher bei den Bildern auch). Danach können Sie innerhalb von Xcode nach Belieben die Sound-Files in Unterordner gruppieren. Die Typangabe ist für die NSBundle-Klasse übrigens unbedeutend. Der AVAudioPlayer liest anhand des Datei-Headers die Formatangaben und wirft eine Exception, falls ein nicht unterstütztes Audioformat referenziert wird.9 Wir werden später noch zeigen, wie man solche Fehler abfängt. Möglichkeiten des AVAudioPlayers
Ist der Player einmal erzeugt, stehen Ihnen mehrere Methoden zur Verfügung, um den Sound weiterzubehandeln: play: Spielt den Sound einmal ab (siehe obiges Beispiel). prepareToPlay: Bereitet den Sound zum Abspielen vor. Erfolgt bereits implizit beim Aufrufen von play. pause: Wiedergabe wird angehalten, der Sound bleibt abspielbereit. stop: Stoppt die Wiedergabe, der Player wird in seinen Ursprungszustand versetzt.
Die play- und die prepareToPlay-Methode geben noch einen Boole'schen Wert zurück, der anzeigt, ob der Sound abgespielt werden konnte. Pro Sound benötigen Sie genau eine 9 Hinweis: Statt der initWithContentsOfURL:-Methode können Sie den Player auch mit Ihren eigenen Daten befüttern. Dafür können Sie die initWithData:-Methode verwenden, die als Parameter ein NSData-Objekt entgegennimmt. In diesem könnten Sie dann zum Beispiel ein on the fly generiertes WAV-File ablegen, das Sie per Software-Synthese im Programm erzeugt haben könnten. Das iPhone besitzt keinen Synthesizer-Chip wie die berühmten Heimcomputer der 80er oder der GameBoy. Über die initWithData:-Methode könnten Sie die Sounds zumindest emulieren, wenn auch nicht in Echtzeit erzeugen. Die Latenzen wären beim Einsatz des AVAudioPlayers zu hoch. Für Echtzeitmusikanwendungen sollten Sie ohnehin auf das Low Level Core Audio Framework und die Audio Queue Services ausweichen.
134
3.11 Zappp, Brzzz, Booom, Pennng! Hintergrundmusik und Sound-Effekte AVAudioPlayer-Instanz.
Sie können nicht mehrere Sounds mit der gleichen Instanz zum
Abspielen vorbereiten. Bevor Sie den Sound abspielen, können Sie noch ein paar interessante Properties setzen bzw. abfragen: sound.volume: Als float-Wert im Bereich 0.0 (lautlos) bis 1.0 (maximale Lautstärke) können Sie die Abspiellautstärke angeben. Diese kann auch während des Abspielen eines Sounds verändert werden, zum Beispiel um einen Sound ein- oder auszufaden. Werte größer 1.0 verzerren den Sound. sound.numberOfLoops: –1 bedeutet unendliche Wiederholung, Werte größer 0 geben die Anzahl der Wiederholungen an. Sauber geschnittene Loops werden nahtlos wiederholt. sound.duration: Gibt die Länge des Sounds zurück (double-Präzision vom Typ NSTimeInterval). sound.currentTime: Hier können Sie festlegen, wann der Sound gestartet werden soll, ebenfalls als double-Wert. Ist der Wert größer als sound.duration hören Sie natürlich nichts. sound.playing: Gibt einen Boole'schen Wert zurück, der angibt, ob der Sound aktuell noch abgespielt wird.
Die Länge eines Sounds hängt allein vom Speicher des Gerätes ab, lange Sounds benötigen natürlich mehr Zeit beim Laden. Polyphonie und Soundformate
Um polyphone Sounds abzuspielen, müssen mehrere Instanzen des AVAudioPlayers mit jeweils einem Sound angelegt werden. Doch Achtung: Die gleichzeitige Wiedergabe von mehreren unterschiedlichen Sounds erfordert ein unkomprimiertes Soundformat. Vom AVAudioPlayer werden unter anderem die folgenden Soundformate unterstützt: WAV, MP3, AIF, AAC, ALAC, CAF
Diese Formate enthalten einzelne Samples (Höhe der Amplitude pro Zeitabschnitt) in unterschiedlicher Genauigkeit bzw. Abtastrate. Daher wird auch das MIDI-Format vom Player nicht unterstützt, da es sich dabei um kein Audioformat handelt, sondern nur um Steuerungsdaten für eine Sound-Datenbank (also im Prinzip nur die Noten), während Audioformate die zu hörende Toninformation festhalten. Deshalb lassen sich hierüber auch naturalistisch klingende Sounds wiedergeben, wie zum Beispiel menschliche Stimmen – ganz im Gegensatz zur Ära des C64: Der SID-Chip ist aufgebaut wie ein Synthesizer und bietet Oszillatoren, Filter und Hüllkurven, um einen Klang zu generieren, kann aber keine gespeicherten Sample-Daten wiedergeben. Um mehrere Sounds auf dem iPhone gleichzeitig wiedergeben zu können, setzen Sie am besten das WAV-Format ein (8 Bit – 24 Bit, 8 Hz – 44,1 Hz), da dieses die Amplitudenausschläge unkomprimiert abspeichert (Linear PCM).
135
3 Spiele entwickeln – von 0 auf 180
Für komprimierte Sounds wie AAC, MP3 oder ALAC (Apple Lossless) setzt Core Audio einen Hardware-beschleunigten Decodierer ein, der allerdings nur einen Sound gleichzeitig bearbeiten kann. Das bedeutet: Die gleichzeitige Wiedergabe von WAV-Sounds ist nur durch die Leistungsfähigkeit der CPU und des Speichers begrenzt. Die Amplitudeninformation kann direkt an den Lautsprecher weitergegeben werden, ohne zuvor dekodiert zu werden. Es kann nur ein MP3-Sound gleichzeitig wiedergegeben werden (zusätzlich zu WAV-Sounds).
Warum überhaupt MP3? Längere Musikstücke können im WAV-Format bei CDtauglicher Auflösung schnell mehrere Megabyte groß werden. MP3 ist da deutlich platzsparender. Da das iPhone iOS aber nur einen MP3-Sound in den Dekodierer speisen kann, bietet sich das MP3-Format für längere Hintergrundmusik an. Sound-Effekte sind in der Regel nicht so lang und können daher im WAV-Format mit vertretbarer Größe aufgenommen werden. Natürlich können Sie auch ausschließlich WAV-Sounds einsetzen. Allerdings: Da MP3-Sounds über den Hardware-Dekodierer in hörbare Amplitudenschwingungen umgesetzt werden, kostet dies keine zusätzliche Rechenzeit – die Performance verändert sich nicht, und Sie verschwenden lediglich Speicherplatz.10 Woher bekommt man geeignete Sounds?
Nun wissen wir, wie Sounds abgespielt werden und welche Formate geeignet sind. Im Web finden Sie zahlreiche Seiten mit Open Source Samples, Sounds, die frei verfügbar sind – oder gar komplette Tracks, die ohne Lizenz verwendet werden können. Eine Auswahl an Effekt-Sounds und Geräuschen findet sich zum Beispiel auf www.pacdv.com/ sounds/index.html. Fragen Sie aber sicherheitshalber immer beim Anbieter nach, welche Nutzungsrechte für Ihr Projekt gelten. Alternativ können Sie sich Sample-CDs besorgen – bestückt mit vorgefertigten Loops und mitunter einzeln gesampleten Instrumenten, mit denen sich komplette Tracks basteln lassen. Das Audio-Material auf Sample-CDs ist in der Regel frei für eigene Produktionen verwendbar, der Preis für die CDs ist daher entsprechend hoch. Billiger wird es, wenn Sie die Sounds selbst erstellen: Das iPhone iOS hat sich inzwischen zu einer professionell einsetzbaren Musikproduktionsplattform entwickelt. Warum also nicht die Sounds mit einem geeigneten Tool selbst erstellen? Ein gut klingender monophoner Synthesizer mit leider etwas kryptischer Bedienung ist Noise.io. Das Programm Nanoloop stammt dagegen ursprünglich vom GameBoy, sorgt aber auch auf dem iPhone für typische 80er-Bleep-Sounds – und der Sequenzer verfügt über ein leicht zu erlernendes Bedienkonzept. Etwas moderner und elektronischer wird es
10 Weitere Informationen zur Audio-Wiedergabe und zu den unterstützten Formaten finden Sie unter “Audio Playback and Recording Formats”:
3.11 Zappp, Brzzz, Booom, Pennng! Hintergrundmusik und Sound-Effekte
mit der perfekten TB-303/TR-909-Emulation TechnoBox oder aber gleich ganz stilecht: mit Rebirth, dem PC-Klassiker von Propellerheads, neu aufgelegt für das iPhone.
Abb 3.21: Korg iElectribe
Typische Gamesounds sind meistens kurz und prägnant, daher eignen sich auf Drumsounds spezialisierte Synthesizer hierfür besonders gut. Für das iPad hat Korg den iElectribe veröffentlicht, eine Emulation der ersten Hardware-Fassung des berühmten virtuell analogen "Rhythm Synthesizers" von 1999. Das Programm besitzt darüber hinaus den Vorteil, dass die erstellten Tracks direkt als WAV-File nach iTunes exportiert werden können. Und von dort natürlich direkt in unser Xcode-Projekt wandern können. Für das Beispielprojekt "Sound" haben wir fünf Effekt-Sounds und ein Drum-Loop mit dem iElectribe erstellt und unbearbeitet in das Projekt über "Bounce Pattern" exportiert. Die Geschwindigkeit des Sequenzers haben wir für die Effekt-Sounds so eingestellt, dass das Ausklingen des Sounds ziemlich genau mit der Länge eines Patterns übereinstimmt. So vermeiden wir unnötiges Nachbearbeiten mit einer Audio-Schnittsoftware wie WaveLab. Den zweitaktigen Drum-Loop haben wir ebenfalls über "Bounce Pattern" exportiert. Der iElectribe sorgt automatisch dafür, dass das WAV-File beat-genau geschnitten ist. Die sechs Sounds track1.wav und sound1.wav - sound5.wav finden Sie im Beispielprojekt im Ordner Resources -> Sounds.
137
3 Spiele entwickeln – von 0 auf 180 Game-Sounds richtig in Spiele einbinden
Okay, die passenden Sounds haben wir. Für den problemlosen Einsatz in unseren Spieleprojekten fehlt aber noch eine passende Abspiel-Engine. Wir haben die folgenden Anforderungen: Latenzzeiten: Die Sounds sollen möglichst verzögerungsfrei jederzeit abgespielt werden können. Wir müssen deshalb dafür sorgen, dass die Sounds bereits geladen sind. Loops: Wiederholtes Abspielen eines Sounds. Soll ein bereits abgespieltes Loop erneut gespielt werden, soll dieser Request ignoriert werden, um unschöne Überlagerungen zu vermeiden. Polyphonie: Die Sounds sollen polyphon abgespielt werden können. Beenden: Sounds sollen jederzeit gestoppt werden können.
Die Polyphonie erreichen wir durch den Einsatz von WAV-Files – aus den anderen Anforderungen ergeben sich neben einer obligatorischen playSound-Methode drei weitere Methoden, die wir im Header der GameManager-Klasse als Schnittstellen definieren: -
Die getSound:-Methode arbeitet wie die getPic:-Methode mit einer Hash-Tabelle, in der die Sounds als AVAudioPlayer-Instanz gespeichert sind. Die drei Methoden zum Abspielen/Beenden der Sounds rufen dann die getSound:-Methode auf, sodass die getSound:Methode in der Regel nicht separat aufgerufen werden muss. Um einen Sound einmalig abzuspielen, genügt die folgende Zeile: [[GameManager getInstance] playSound: @"sound1.wav"];
Mehrmaliges Aufrufen, beispielsweise innerhalb der Spielschleife führt dazu, dass der Sound für jeden Aufruf von vorne anfängt: Ein genretypischer Stakkatoeffekt entsteht, der bei Schüssen durchaus gewünscht sein kann. Dennoch sollten Sie die playSound:Methode eher für ereignisbezogene Geräusche einsetzen: Der Spieler wird verwundet, oder aber die Spielfigur springt nach oben, boxt den Gegner usw. Falls Sie den gleichen Sound polyphon abspielen wollen, müssten Sie das WAV-File in mehrere Player einbinden. Für geloopte Sounds und daher auch ideal für Hintergrundmusik genügt der Aufruf von: [[GameManager getInstance] loopSound: @"track1.wav"];
Der Sound wird endlos wiederholt. Ist das Ende des Sounds erreicht, wird der Sound wieder von vorne abgespielt. Im Gegensatz zur playSound:-Methode verhält sich die loopSound:-Methode auch innerhalb der Spielschleife gleich – falls der Sound bereits geloopt wird, wird ein erneuter Aufruf ignoriert. Um einen Sound zu stoppen, unabhängig ob dieser geloopt wird oder nicht oder eventuell gar nicht mehr abgespielt wird, setzen Sie die folgende Code-Zeile ein (in jeder Klasse, die den GameManager-Header importiert):
Bevor wir ein praktisches Beispiel zum Testen der Methoden vorstellen, werfen wir noch einen Blick auf die Implementierung der Methoden im GameManager: - (AVAudioPlayer *) getSound: (NSString *) soundName { @try { AVAudioPlayer *sound = [[self getDictionary] objectForKey: soundName]; if (!sound) { NSError *error; NSString *path = [[NSBundle mainBundle] pathForResource: soundName ofType: nil]; sound = [[AVAudioPlayer alloc] initWithContentsOfURL: [NSURL fileURLWithPath: path] error: &error]; if (!sound) { NSLog(@"ERROR: Wrong sound format: %@. Description: %@", soundName, [error localizedDescription]); } else { sound.volume = 0.7; int len = sound.duration; [[self getDictionary] setObject: sound forKey: soundName]; NSLog(@"%@ loaded, duration: %i sec", soundName, len); [sound release]; } } return sound; } @catch (id theException) { NSLog(@"ERROR: %@ not found!", soundName); } return nil; }
Sie sehen, ähnlich der getPic:-Methode setzen wir wieder das NSMutableDictionary ein. Ist der Sound bereits vorhanden, geben wir direkt die Player-Instanz zurück. Diese enthält dann bereits den geladenen Sound, der über die play-Methode abgespielt werden kann. Die prepareToPlay-Methode können wir bei kurzen Effekt-Sounds für die latenzfreie Wiedergabe ignorieren. Dennoch sollten wir dafür sorgen, dass alle Sound-Instanzen vor der ersten Verwendung im Spiel bereits geladen sind. Dies erledigen wir idealerweise im Preloader des GameManagers: - (void) preloader { [self getSound: @"sound1.wav"]; [self getSound: @"sound2.wav"]; [self getSound: @"sound3.wav"]; [self getSound: @"sound4.wav"]; [self getSound: @"sound5.wav"]; [self getSound: @"track1.wav"]; ... }
139
3 Spiele entwickeln – von 0 auf 180
Für jedes WAV-File wird nur ein Player angelegt, daher können wir die einzelnen Player polyphon abspielen, aber das gleichzeitige Abspielen wäre nur über eine zusätzliche Player-Instanz möglich. Aber zurück zur getSound:-Implementierung: Wir haben den gesamten Code in einen try-catch-Block gelegt, der uns eine Fehlermeldung ausgibt, falls der Sound im FileSystem nicht gefunden wurde. Wenn der Sound in einem Format vorliegt, das der AVAudioPlayer nicht unterstützt, gibt die initWithContentsOfURL der AVAudioPlayer-Klasse ein nil-Objekt zurück. Für die Fehlerbeschreibung setzen wir ein NSError-Objekt ein, das uns über dessen localizedDesription-Methode den Grund für das nil-Objekt liefert. Ist alles glatt gegangen, setzen wir die Default-Lautstärke des Players von 1.0 auf 0.7 zurück. Für gut ausgesteuerte Sounds reicht mitunter auch 0.3, da die Lautsprecher der älteren iPhoneGenerationen recht laut und schnarrend klingen. Der User kann den Klang ohnehin über den externen Lautstärkehebel der iOS-Geräte nachregeln. Außerdem lassen wir uns über int len = sound.duration;
die Dauer des Sounds, gerundet auf Sekunden, über die Konsole ausgeben. Die eigentliche Genauigkeit des duration-Felds ist zwar vom Typ double, wird aber hier von uns nicht gebraucht. Die Implementierung der drei Methoden zum punktgenauen Abspielen eines Sounds ist unspektakulär: - (void) playSound: (NSString *) soundName { AVAudioPlayer *sound = [self getSound: soundName]; if (sound) { sound.currentTime = 0; if (!sound.playing) { sound.numberOfLoops = 0; [sound play]; } } } - (void) loopSound: (NSString *) soundName { AVAudioPlayer *sound = [self getSound: soundName]; if (sound) { if (!sound.playing) { sound.currentTime = 0; sound.numberOfLoops = -1; [sound play]; } } } - (void) stopSound: (NSString *) soundName { AVAudioPlayer *sound = [self getSound: soundName]; if (sound && sound.playing) { [sound stop]; } }
140
3.11 Zappp, Brzzz, Booom, Pennng! Hintergrundmusik und Sound-Effekte
In der playSound:-Methode prüfen wir, ob der Sound bereits abgespielt wird, und falls nein, spielen wir den Sound ab. Unabhängig davon setzen wir über currentTime die Startposition des Sounds stets auf 0, um so den bereits beschriebenen Stottereffekt zu erzeugen. Dadurch stellen wir sicher, dass jeder Aufruf der Methode zu einem hörbaren Ergebnis führt. Die loopSound:-Methode spielt den Sound dagegen nur, sofern dieser nicht bereits abgespielt wird, und fragt dafür den Boole'schen Wert sound.playing ab. Auch hier wird vor jedem Abspielvorgang die Startposition des Sounds auf 0 gesetzt. Die Anzahl der Wiederholungen setzen wir mit –1 auf unendlich. Sehen wir uns abschließend ein Einsatzszenario an, bei dem wir einen Sound im Hintergrund loopen und die Effekt-Sounds pro Touch einmal abspielen wollen. Damit testen wir die Belastbarkeit der Sound-Wiedergabe auf dem iPhone – ganz so, wie es auch in einem Spiel der Fall wäre, die Sounds sollen jederzeit spielbereit sein. Für diesen Härtetest eignet sich eine Soundboard-Applikation, die jeden Sound nach Druck auf der (virtuellen) Taste abspielt. Der App Store bietet eine ganze Reihe von Soundboard-Applikationen an. Eine interessante Variante stellt das iPhone-/iPad-Programm Bloom dar, das von dem experimentellen Musiker Brian Eno zusammen mit Peter Chilvers (arbeitete am Spielehit Creatures mit) entwickelt wurde. Jede Touch-Berührung auf dem Display spielt ein Audio-File ab, das Programm merkt sich die zeitliche Abfolge der Berührungen und spielt diese erneut ab – ähnlich einem Sequenzer. Allerdings werden die Töne algorithmisch umgebogen, um so ganz im Sinne der generativen Musik stets neue Tonfolgen zu wiederholen. Für unser Praxisbeispiel wollen wir nicht ganz so weit gehen, aber zumindest das Grundkonzept beibehalten: Unsere fünf mit dem iElectribe erzeugten Effekt-Sounds sollen nach Berührung des Displays abgespielt werden. Die Sounds werden gleichmäßig auf der y-Achse verteilt, was in der BloomAnwendung der Tonhöhe entspräche. Die Spielschleife regelt den zeitlichen Verlauf. Das Programm soll sich den Zeitpunkt eines Sounds merken und diesen erneut abspielen. Der Loopbereich beträgt 100 Frames. Jedes Antippen des Displays erzeugt einen sich vergrößernden Kreis, der langsam durchsichtiger wird und schließlich ganz verschwindet. Drückt man unten links auf das Display, soll der geloopte Hintergrund-Sound anbzw. ausgeschaltet werden können. Drückt man unten rechts auf das Display, werden alle bereits gespeicherten SoundEvents gelöscht. Der Sequenzer wird in seinen Anfangszustand versetzt.
Das hört sich vielleicht etwas kompliziert an, ist mit unserem bisherigen Wissen aber sehr leicht umzusetzen. Alle Touch-Events, die entlang der Zeitachse registriert wurden, werden im Sequenzer gespeichert und erneut abgespielt, sobald der nächste Durchlauf die entsprechende Zeitmarke platziert. Die Synchronisierung der Effekt-Sounds mit dem Hinter-
141
3 Spiele entwickeln – von 0 auf 180
grund-Loop werden wir für das Beispielprogramm jedoch ignorieren, da wir für die Umsetzung des Mini-Sequenzers lediglich die Frame-Anzahl des Loop-Bereichs einsetzen wollen. Da das Abspielen eines Sounds mit dem Rendern eines Kreises verbunden ist, setzen wir hierfür natürlich wieder die Sprite-Klasse ein. Und da das Sprite nach dem Ausfaden verschwindet, haben wir es hier außerdem mit einer Animation zu tun – mit dem Unterschied, dass das Faden des Alphawertes nicht von den Frames eines Filmstreifens abhängt. Listing Sprite.h #import "Sprite.h" @interface Circle : Sprite { int radius; float alpha; } - (id) initWithRadius: (int) rds pos: (CGPoint) pxy; @end
Listing Sprite.m #import "Circle.h" #import "GameManager.h" @implementation Circle - (id) initWithRadius: (int) rds pos: (CGPoint) pxy { if (self = [super init]) { radius = rds; pos = pxy; alpha = 1; active = true; NSString *sound = @"sound1.wav"; int step = H/5; //5 sounds if (pos.y < step*1) { sound = @"sound1.wav"; } else if (pos.y < step*2) { sound = @"sound2.wav"; } else if (pos.y < step*3) { sound = @"sound3.wav"; } else if (pos.y < step*4) {
Für den Sound-Kreis werden keine PNG-Grafiken benötigt, daher legen wir eine eigene init-Methode an. Die einzigen Parameter, die wir für den Kreis benötigen, sind der Anfangsradius und die Position, an der der Bildschirm berührt wurde – diese Position wird in der draw-Methode als Mittelpunkt des Kreises gesetzt. Der jeweilige Sound hängt lediglich von der y-Position ab, sodass wir diesen in der initanhand des Touch-Points auswählen und abspielen können. Die y-Achse wird dabei gleichmäßig in Abschnitte unterteilt, in Abhängigkeit der ScreenHöhe und der Anzahl der Sounds (= step), sodass Sie das Programm ohne Anpassungsaufwand auch für andere iOS-Auflösungen umsetzen können. Da wir die x-Position ignorieren, verfügen wir über Spielraum, um die App später optional zu erweitern – zum Beispiel könnten Sie die Lautstärke von der x-Position abhängig machen und die SoundMethoden im GameManager entsprechend anpassen. WithRadius:pos:-Methode
Das Abspielen eines Sounds erfolgt bereits im init-Konstruktor der Circle-Klasse, deshalb brauchen wir die von Sprite geerbte draw-Methode nur noch zu überschreiben, um die Kreisanimation zeitgleich zu starten: Der Radius wird pro Frame um 2 Pixel vergrößert und der Alphawert um 0.03 verringert. Erreicht der Alphawert 0, ist der Kreis volltrans-
143
3 Spiele entwickeln – von 0 auf 180
parent und damit nicht mehr sichtbar – wir können den active-Flag auf false setzen, und das Sprite wird vom GameManager beim nächsten Durchlauf aus dem Speicher entfernt. Das Erzeugen eines neuen Circle-Objektes erledigen wir wie bisher in der createSprite:speed:pos:-Methode des GameManagers. Da wir für die Sound-App derzeit nur ein Sprite verwenden, fällt der Code entsprechend übersichtlich aus: - (void) createSprite: (int) type speed: (CGPoint) sxy pos: (CGPoint) pxy { if (type == CIRCLE) { Circle *circle = [[Circle alloc] initWithRadius: 10 pos: pxy ]; [circle setType: CIRCLE]; [newSprites addObject: circle]; [circle release]; } else { NSLog(@"ERROR: Unbekannter Sprite-Typ: %i", type); } }
Bevor wir diese Methode für einen neuen Touch aufrufen, müssen wir uns noch um die Implementierung des Sequenzers kümmern, der sich lediglich die Zeit und die TouchPosition merken muss. Zur Speicherung dieser Daten legen wir eine neue Klasse an: Listing SoundEvent.h @interface SoundEvent : NSObject { int frameTime; CGPoint pos; } - (id) initWithTime: (int) frt pos: (CGPoint) pxy; - (int) getTime; - (CGPoint) getPos; @end Listing SoundEvent.m #import "SoundEvent.h" @implementation SoundEvent - (id) initWithTime: (int) frt pos: (CGPoint) pxy { if (self = [super init]) { frameTime = frt;
Der Aufbau der Klasse muss nicht weiter erläutert werden. Statt der expliziten Formulierung des Setter-Getter-Pärchens können Sie natürlich auch die für Objective-C typische @property-Direktive einsetzen. Den Sequenzer legen wir nach dem Vorbild eines Sprites ebenfalls als veränderbares Array in der GameManager-Klasse an: int time; //Frame Counter NSMutableArray *sequencer; ... sequencer = [[NSMutableArray alloc] initWithCapacity:99];
Die time-Variable stellt einen simplen Zähler dar, der die Durchläufe der Spielschleife festhält. Da wir einen Loop-Bereich von 100 Frames angesetzt haben, wird der Zähler nach 100 wieder auf 0 gesetzt. Die vollständige Implementierung des Sequenzers kann daher in der playGame-Methode der GameManagers erfolgen: - (void) playGame { //Frame Counter time ++; if (time > 100) { //Loop-Bereich time = 0; } for (SoundEvent *event in sequencer) { if ([event getTime] == time) { [self createSprite: CIRCLE speed: CGPointMake(0, 0) pos: [event getPos]]; } } ... }
Das sequencer-Array durchläuft pro Frame alle Elemente und vergleicht das gespeicherte Frame mit dem aktuellen time-Frame. Sind beide gleich, wird ein neues Circle-Objekt
145
3 Spiele entwickeln – von 0 auf 180
erzeugt, dem die gespeicherte Position des SoundEvents mitgegeben wird, da dieses ja für den zu spielenden Sound von Bedeutung ist. Die speed-Angabe ignorieren wir, da sich die Kreise nicht bewegen sollen. Um nun die neuen Touch-Events in den Sequenzer einzuspeisen, brauchen wir lediglich die touchBegan:-Methode anzupassen: - (void) touchBegan: (CGPoint) p { [self handleStates]; if (p.x < 130 && p.y > H-50) { static bool playMusic = true; if (playMusic) { [[GameManager getInstance] stopSound: @"track1.wav"]; playMusic = false; NSLog(@"Loop off"); } else { [[GameManager getInstance] loopSound: @"track1.wav"]; playMusic = true; NSLog(@"Loop on"); } } else if (p.x > W-60 && p.y > H-50) { NSLog(@"Clear sequencer"); [sequencer removeAllObjects]; } else { [self createSprite: CIRCLE speed: CGPointMake(0, 0) pos: p]; SoundEvent *event = [[SoundEvent alloc] initWithTime: time pos: p]; [sequencer addObject: event]; [event release]; } } - (void) touchMoved: (CGPoint) p {} - (void) touchEnded {}
Zunächst sorgen wir hier für die Behandlung der beiden Fake-Buttons, die den LoopSound togglen bzw. das Sequenzer-Array leeren. Ob ein Button gedrückt wurde, entscheiden wir anhand der Touch-Position. Sie können natürlich auch eigene Buttons kreieren, um das Programm zu verschönern: Dafür können Sie entweder die vorgefertigten UI-Komponenten des UIKits einsetzen oder aber den Button als unbewegtes Sprite realisieren. Befindet sich der Touch-Point nicht über einer der beiden Schaltflächen, spielen wir den Sound über die createSprite:speed:pos:-Methode ab und speichern die aktuelle Position zusammen mit der time-Variablen in einem SoundEvent-Objekt. Fertig!
146
3.12 Datenspeicherung
Abb 3.22: Die Sound-App in Aktion
Wenn Sie Lust haben, können Sie die Anwendung mit Ihren eigenen Sounds bestücken oder aber die Beschränkung des Loop-Bereichs verändern. Sie werden feststellen, dass selbst das Ur-iPhone eine stattliche Anzahl an Sound-Events gleichzeitig abspielen kann. Doch irgendwann ist Schluss: Zu viele Events zwingen selbst den stärksten Prozessor in die Knie. Neuere Geräte der iPhone-Familie wie das iPad oder das iPhone 4 halten natürlich länger durch als die Geräte der ersten Generation. Aber für Spiele brauchen Sie in der Regel nicht mehr als zwei gleichzeitige Effekt-Sounds und einen Hintergrund-Soundtrack. Mit der entstandenen App können Sie immerhin testen, welche Sound-FX und Tracks zusammenpassen und welche nicht. Und vielleicht machen Sie daraus ja auch ein innovatives Musikspiel jenseits der x-ten Neuauflage von Senso (Ralph H. Baer, Milton Bradley)?
3.12
Datenspeicherung Handyspiele werden häufig unterwegs gespielt und müssen daher schon mal während eines laufenden Spieles unterbrochen werden – zum Beispiel, wenn endlich der lang ersehnte Schienenersatzverkehr eintrifft (Berliner, die im Sommer 2010 mit der U2 unterwegs waren, wissen, was ich meine!). Kurzum, Sie müssen dafür sorgen, dass Sie den Spielstand speichern und beim nächsten Start wieder abrufen können. Daneben wollen Sie vielleicht auch den erreichten Highscore oder die aktuelle Spielstufe festhalten. Wie das funktioniert? Nun, zunächst müssen wir wissen, wann gespeichert werden soll: Sobald der User auf den Home-Button drückt, wird die applicationWillResignActive:-
147
3 Spiele entwickeln – von 0 auf 180
Methode des Delegates ausgeführt – hier können Sie den Speichervorgang anstoßen. Doch wie werden die Daten überhaupt gespeichert und in welchem Format? Wie bereits häufiger gesehen, so stellt die iPhone iOS-Plattform auch hier mehrere Möglichkeiten zur Verfügung: Über das App-Filesystem: Für jede App steht ein eigener Speicherbereich zur Verfügung. Über den NSFileManager können Sie darauf zugreifen. Sie setzen die NSUserDefaults-Klasse ein, die auch für das Verwalten von Optionen über das iOS-Settings-Menü (Settings Bundle) die erste Wahl darstellt. Für Datenbankanwendungen steht eine SQLite-Datenbank zur Verfügung. Die API Core Data bietet Zugang auf Systemebene, Core Data wird ebenfalls von der SQLite-Datenbank verwendet. Schließlich haben Sie noch die Möglichkeit, Daten über den UIWebView und eine darin eingebettete HTML-Seite zu speichern. Diese wird über den integrierten WebKit-Browser dargestellt, der den HTML5-Standard unterstützt und damit auch das dauerhafte Speichern von Daten – unter anderem über das localStorageObjekt (JavaScript).
Um Highscores oder Spielstände zu speichern, bietet sich die NSUserDefaults-Klasse an: Neben der Unterstützung für einfache Datentypen wie NSInteger, float, BOOL ermöglicht die Klasse auch das Speichern von Objekten. Sie können also, wenn Sie wollen, den aktuellen Zustand eines Spieles jederzeit einfrieren. Um ein Beispiel zu zeigen, legen wir das neue Projekt "Datenspeicherung" an, das Sie auch im Download-Ordner finden – als Template verwenden wir wieder das GameManager-Projekt. Zum Lesen und Speichern von Objekten genügen uns zwei Methoden, die wir der GameManager-Klasse hinzufügen: - (void) saveObject: (id) object key: (NSString *) key { NSUserDefaults *storage = [NSUserDefaults standardUserDefaults]; [storage setObject: object forKey: key]; } - (id) readObjectForKey: (NSString *) key { NSUserDefaults *storage = [NSUserDefaults standardUserDefaults]; return [storage objectForKey: key]; }
Der Zugriff auf die NSUserDefaults-Instanz kann jederzeit über die Klassenmethode standardUserDefaults erfolgen. Da wir Objekte unterschiedlicher Typen speichern wollen, wählen wir den generischen Typ id. Für jeden gespeicherten Eintrag müssen Sie einen Schlüssel in Form eines NSStrings vergeben. Über diesen können Sie dann den gespeicherten Wert wieder auslesen. Natürlich können Sie Einträge auch löschen: [storage removeObjectForKey: key];
148
3.12 Datenspeicherung
Einen primitiven Datentyp, wie zum Beispiel int, können Sie so speichern und auslesen: //Speichern [storage setInteger: 22 forKey: @"eineZahl"]; //Lesen int val = [storage integerForKey: @"eineZahl"]; //val = 22
Der Schlüssel, um den Eintrag zu identifizieren, ist in diesem Fall der String @"eineZahl". In der Praxis werden Sie aber eher selten lediglich einzelne Zahlenwerte oder andere einfache Datentypen speichern: Statt mehrere separate Speichervorgänge anzustoßen, ist es effizienter, die zu speichernden Daten in einem Objekt zu lagern und dieses dann dauerhaft innerhalb eines einzigen Schreibvorgangs im Persistance-Speicher des Gerätes abzulegen. Für das nachfolgende Beispiel speichern wir ein Zitat von Friedrich Schiller (aus der berühmten Elegie "Der Spaziergang" von 1795). Da Strings Objekte sind, setzen wir die zuvor implementierten Methoden saveObject und readObjectForKey ein: //Speichern NSString *schillerQuote = @"Und die Sonne Homers, siehe! sie lächelt auch uns."; NSString *schillerKey = @"schillerKey"; [self saveObject: schillerQuote key: schillerKey]; //Lesen NSString *result = [self readObjectForKey: schillerKey]; if (result) { NSLog(@"Storage output: %@", result); }
Den Code platzieren wir in der loadGame-Methode des GameManagers. Da wir wissen, dass der schillerKey ein NSString-Objekt zurückliefert, können wir dieses direkt der resultInstanz zuweisen. Beispiel: Highscores
Um mehrere Daten in einem Rutsch zu speichern, bietet sich erneut die NSMutableDictioan. Für jeden neuen Highscore soll eine entsprechende Erfolgsmeldung über die Konsole ausgegeben werden, wobei alle Highscores dauerhaft in der NSMutableDictionary-Instanz abgelegt werden. Mit jedem neuen Highscore vergrößert sich die Anzahl der Einträge. Als Key für einen neuen Highscore-Eintrag verwenden wir das aktuelle Datum. Da Spiele auf mobilen Geräten in der Regel nur vom Besitzer gespielt werden, bietet es sich an, die Highscore-Liste nicht mit einem User-Namen zu verknüpfen, sondern stattdessen den Zeitpunkt anzugeben, wann die Höchstpunktzahl erreicht wurde. Den Highscore lassen wir für das Beispiel über den bereits implementierten Zufallsgenerator bestimmen. Die Umsetzung erfolgt vollständig in der loadGame-Methode des GameManagers, sodass bei Start der App die alten Highscores und der möglicherweise neue Highscore ausgegeben werden. nary-Klasse
Wie setzen wir eine solche Highscore-Liste um? Zunächst einmal müssen wir an das aktuelle Datum über die NSDate-Klasse herankommen:
Damit wir das Datum in einem lesbaren Format erhalten, setzen wir den NSDateFormatter ein. Dieser formatiert das Datum länderspezifisch anhand der Spracheinstellung des jeweiligen Gerätes (über das Settings-Menü erreichbar). Sowohl für das Datum als auch die Uhrzeit können wir zusätzlich noch einen Stil angeben: Der NSDateFormatterLongStyle liefert das Datum in der Form "25. Juni 2010" – der von uns eingesetzte Stil erzeugt dagegen lediglich: "25.06.10". Auch für die Uhrzeit können Sie einen Stil setzen. Der verwendete "Long"-Style liefert zusätzlich noch die Sekundenangabe der aktuellen Uhrzeit. Dies ist nützlich, da wir den Highscore später anhand des Datums identifizieren wollen und ein Spiel kürzer als eine Minute dauern kann. Das einmalige Anlegen und Auslesen der Highscore-Tabelle über eine NSMutableDictionary-Instanz stellt uns vor keine Herausforderungen: //Highscore-Table anlegen NSString *highscoresKey = @"highscoresKey"; NSMutableDictionary *highscores = [self readObjectForKey: highscoresKey]; if (!highscores) { highscores = [[NSMutableDictionary alloc] initWithCapacity: 10]; } //Scores lesen NSArray *keys = [highscores keysSortedByValueUsingSelector: @selector(compare:)]; NSNumber *score = [NSNumber numberWithInt: 0]; for (id key in keys) { score = [highscores objectForKey: key]; NSLog(@"Date: %@, Score: %@", key, score); }
Beim Auslesen der Highscores wollen wir jedoch die Werte alphanumerisch sortieren. Dies erledigt die vom iOS bereitgestellte compare:-Methode, da wir lediglich den NSInteger-Wert sortieren wollen. Um eine eigene Sortiermethode zu verwenden, müssten wir die Methode überschreiben. Die NSDictionary-Klasse bietet praktischerweise die keysSortedByValueUsingSelector-Methode, der wir über die @selector-Direktive die compare:Methode übergeben können. Als Ergebnis erhalten wir die Datumsschlüssel sortiert nach den Highscore-Einträgen zurück. Wir brauchen also nur noch über eine for-Schleife alle Einträge auszugeben. Das Ergebnis könnte beispielsweise so aussehen: 2010-06-25 14:55:41.139 Datenspeicherung[14632:207] Date: 25.06.10 14:53:04 MESZ, Score: 333 2010-06-25 14:55:41.140 Datenspeicherung[14632:207] Date: 25.06.10 14:55:41 MESZ, Score: 337 2010-06-25 14:55:41.141 Datenspeicherung[14632:207] Date: 25.06.10 14:53:24 MESZ, Score: 429
150
3.13 Verbindung gesucht: Multiplayer-Spiele mit Game Kit und Game Center 2010-06-25 14:55:41.141 Datenspeicherung[14632:207] Date: 25.06.10 14:53:20 MESZ, Score: 536 2010-06-25 14:55:41.142 Datenspeicherung[14632:207] Date: 25.06.10 14:53:29 MESZ, Score: 672
Das Speichern eines neuen Highscore-Eintrages ist nun nur noch reine Formsache: //Score speichern int newScore = [self getRndBetween: 1 and: 9999]; if (newScore > [score intValue]) { NSLog(@"NEW HIGHSCORE: %i!", newScore); NSNumber *scoreObj = [NSNumber numberWithInt: newScore]; [highscores setObject: scoreObj forKey: dateString]; [self saveObject: highscores key: highscoresKey]; } else { NSLog(@"Score: %i. Leider kein neuer Highscore.", newScore); }
Ist der zufällige, neue Highscore größer als der zuletzt ausgegebene Highscore der gespeicherten Liste, legen wir einen neuen Eintrag an und geben eine entsprechende Erfolgsmeldung über die Konsole aus. Sie sehen, dass wir den einfachen int-Wert zuvor in ein NSNumber-Objekt gewrappt haben, damit dieser als Objekt im Dictionary abgelegt werden kann (numberWithInt:). Umgekehrt wandeln wir diesen nun wieder in einen int-Wert um, um die zuletzt gespeicherte Höchstpunktzahl mit dem aktuellen Highscore vergleichen zu können (intValue). Praktisch ist dabei, dass die um den neuen Highscore erweiterte Liste komplett als Objekt über die NSUserDefaults-Instanz festhalten werden kann.
3.13
Verbindung gesucht: Multiplayer-Spiele mit Game Kit und Game Center iPhone, iPod touch und das iPad sind vernetzte Geräte und prädestiniert dafür, Multiplayer-Spiele auszutragen. Da Apps von den meisten Anwendern über WLAN heruntergeladen werden, kann man davon ausgehen, dass für Multiplayer-Spiele ebenfalls eine WLAN-Verbindung vorhanden ist – diese braucht man, um die Latenzzeiten möglichst gering zu halten, gerade bei schnellen Action-Titeln. Unabhängig von der Güte der Verbindung können Sie über das WebKit Framework Verbindungen zum Internet herstellen – dabei werden die Spieler nicht direkt miteinander verbunden, sondern kommunizieren über einen zwischengeschalteten Server miteinander, der die Game-Paketdaten entgegennimmt und an den jeweiligen Spielpartner weiterleitet. Dass das Aufsetzen eines derartigen Servers mit zusätzlichem Aufwand verbunden ist, versteht sich von selbst, vor allem, wenn mehrere Tausend Nutzer gleichzeitig auf das System zugreifen wollen. Wenn Sie also ein Spiel um Multiplayer-Funktionalitäten erweitern wollen, macht es Sinn, sich nach einem Drittanbieter umzusehen, der Ihnen diese Arbeit abnimmt. Populäre APIs werden unter anderem von OpenFeint, ngmoco („Plus+“) oder Chillingo („Crystal“) bereitgestellt. Die APIs decken die gängigen Aufgaben ab:
151
3 Spiele entwickeln – von 0 auf 180 Freunde einladen Matchmaking Versand/Empfang von Paketdaten Online-Highscores
Auch Apple selbst bietet seit iOS 4.1 mit der Game Center App (hierüber kann man sich direkt auf dem Device einen Account anlegen und Multiplayer-Spiele starten) und der dazugehörigen Game Kit API ähnliche Funktionalitäten an und versteht sich als „Social Gaming Network“.11 Neben Internetverbindungen können die iOS-Geräte aber auch untereinander über Bluetooth vernetzt werden (Peer-to-Peer). Solche Ad-hoc-Netzwerke setzen zwar die räumliche Anwesenheit der Spieler am gleichen Ort voraus, benötigen dafür aber keinen Internetzugang und keine zwischengeschalteten Server – die Spieler sind direkt untereinander vernetzt. Apple unterstützt Bluetooth und Voice-Übertragungen ebenfalls über das Game Kit Framework:
http://developer.apple.com/iphone/library/documentation/GameKit/Reference/ GameKit_Collection/ Anders als der Name vermuten lässt, bietet die API keine speziellen Hilfestellungen für die Spieleentwicklung, sondern dient lediglich der Verwaltung der Verbindungen und der Anbindung an die Game Center App (Anmeldung der Spieler, Leaderboards, Matchmaking etc.). Deshalb werden wir an dieser Stelle nicht weiter darauf eingehen. Die API ist gut dokumentiert und enthält viele Code-Beispiele. Aktuelle Informationen finden Sie auf der Game Center-Homepage: http://developer.apple.com/iphone/gamecenter/.
11 Zur Drucklegung des Buches ist das Game Center nur über den iOS 4.1 GM seed verfügbar gewesen, der offizielle Release ist für Herbst 2010 geplant. Den vorläufigen Programming Guide finden Sie unter: https://developer.apple.com/iphone/prerelease/library/documentation/NetworkingInternet/Conceptual/GameKit_Guide/Introd uction/Introduction.html
152
4 4 OpenGL ES – der Turbo-Gang 4.1
Warum OpenGL ES? So, jetzt heißt es anschnallen! OpenGL ES sorgt durch seine Nähe zur Grafik-Hardware auf den iOS-Geräten für Höchstgeschwindigkeit. Dieser zusätzliche Performance-Schub kann mit Core Graphics so nicht erreicht werden. Zudem sorgt die Schnittstelle insbesondere im 3D-Bereich für natives Rendering und realistische Grafikeffekte. Das bisher Gelernte müssen wir dabei nicht über Bord werfen: Von der Sprite-Verwaltung über die Touch-Steuerung bis hin zur Programmierung von Sound-Effekten – diese Bausteine bleiben weiterhin erhalten, denn OpenGL ES (= Open Graphics Library for Embedded Systems) ist eine reine Programmierschnittstelle zur Entwicklung von 2D- und 3D-Grafik basierten Apps (erweiterte Bibliotheken wie GLU/GLUT stehen auf dem iPhone nicht zur Verfügung, und die Audio-Erweiterung OpenAL stellt für Spiele in den meisten Fällen zu viel Overhead dar). Mit dem UIKit konnten wir zwar bisher schon ganz flott eigene Ideen verwirklichen, aber mit steigender Anzahl gleichzeitig dargestellter Sprites nimmt natürlich irgendwann auch die Spielgeschwindigkeit ab. Ganz davon abgesehen: UIKit und Core Graphics bieten keinerlei Unterstützung für 3D-Grafik. Übrigens sind einige der UI-Animationseffekte des iOS SDK intern ebenfalls über OpenGL ES implementiert worden. Interessant ist, dass OpenGL und die auf mobile Geräte zugeschnittene Variante OpenGL ES bereits seit einigen Jahren – völlig unabhängig von Apples iPhone & Co. – existieren: OpenGL ES wird nicht nur in Spielkonsolen wie der Playstation 3 erfolgreich eingesetzt, sondern auch für viele mobile Plattformen wie etwa Googles Android oder das Symbian Betriebssystem. Auch die neue Linux-basierte Maemo-/MeeGo-Plattform mit Geräten wie dem Nokia N900 unterstützt OpenGL ES. Erinnern Sie sich noch an den Spätsommer 2008? Das iPhone SDK war erst wenige Monate als Beta verfügbar, und trotzdem gab es im App Store bereits eine große und stetig
153
4 OpenGL ES – der Turbo-Gang wachsende Anzahl aufwändig produzierter 3D-Spiele: Viele Entwickler waren mit der OpenGL-Schnittstelle von anderen Systemen her bereits vertraut und konnten daher bestehenden Code relativ schnell an die neue iPhone-Plattform adaptieren. Tatsächlich ist OpenGL ES C-basiert, und da das iOS SDK ebenfalls C/C++ unterstützt, kann der Kontakt zu Objective-C auf ein Minimum reduziert werden, auch wenn dies nicht unbedingt zu empfehlen ist – denn schließlich basieren das iOS SDK und damit auch das Betriebssystem der iOS Geräte auf Objective-C. Andererseits ist OpenGL ES nicht das einzige C-Reservat innerhalb des SDK: Mit Core Graphics haben wir schon eine C-API kennengelernt. Und ebenso wie Core Graphics arbeitet auch OpenGL ES als State-Machine: Es gibt zahlreiche "Schalter", wie zum Beispiel "Licht AN", die, einmal aktiviert, dauerhaft gültig bleiben, so lange, bis man den Befehl "Licht AUS" erteilt. In OpenGL ES lauten die entsprechenden Anweisungen übrigens: glEnable(GL_LIGHTING); ... glDisable(GL_LIGHTING);
Doch dazu später mehr, wir wollen nicht vorgreifen (auch wenn wir es kaum erwarten können!).
4.2
Was ist OpenGL ES, und wie ist es aufgebaut? Die OpenGL- bzw. OpenGL ES-Spezifikation wird von einem Non-Profit-Konsortium geleitet, das auf den etwas martialisch klingenden Namen Khronos Group, Inc., hört und an dem sich praktisch alle namhaften Firmen der Mobilindustrie beteiligen, wie zum Beispiel Nokia, Sony Computer Entertainment, Samsung Electronics, Motorola, Google und natürlich auch Apple Inc., um nur einige zu nennen. Kein Wunder also, dass OpenGL ES den Anspruch hat, eine plattformunabhängige API bereitzustellen. Ob Sie nun für Symbian, Android oder das iPhone entwickeln, die Arbeit mit der Schnittstelle ist auf allen Plattformen nahezu identisch. OpenGL ES bietet einen sehr direkten Zugang zur jeweiligen Grafik-Hardware – damit die API dennoch einheitlich auf den unterschiedlichen System eingesetzt werden kann, wird eine weitere, abstrakte Schnittstelle benötigt, die den individuellen Zugriff kapselt: EGL. Obwohl EGL optional ist, wird eine Variante für fast alle Systeme angeboten. Für das iOS SDK ist diese Schnittstelle die EAGL-API, die unter anderem dafür zuständig ist, den Zugriff auf den Bildschirm zu gewährleisten bzw. den UIView mit einem CAEAGLLayer (anstelle des CALayers) zu verknüpfen und so das Rendern darauf zu ermöglichen. Vereinfacht kann man sich das so vorstellen:
154
4.2 Was ist OpenGL ES, und wie ist es aufgebaut?
Abb. 4.1: Zusammenspiel von OpenGL ES, EAGL und der Grafik-Hardware
Während die Desktop-Variante von OpenGL unter anderem mit GLU (OpenGL Utility Library) und GLUT (OpenGL Utility Toolkit) erweiterte Bibliotheken bereitstellt, die sich um systemübergreifende Basisfunktionalitäten wie zum Beispiel Fensterzugriffe, InputHandling oder aber auch die Positionierung einer Kamera im virtuellem Raum kümmern, ist OpenGL ES auf das Wesentliche beschränkt. Das bedeutet de facto, dass wir uns um alles selbst kümmern müssen. Aber keine Sorge, die meisten dieser Aufgaben müssen nur einmalig erledigt werden, sodass wir nicht gezwungen sind, allzu sehr in die Tiefe einzusteigen. Ebenso werden wir für die nachfolgenden Beispiele nicht die gesamte API vorstellen, sondern uns auf das konzentrieren, was für uns als Spieleentwickler Bedeutung hat. Zunächst einmal stellen wir fest, dass es derzeit zwei unterschiedliche Varianten von OpenGL ES gibt: OpenGL ES 1.0/1.1: Wird von allen Geräten der iPhone-Familie unterstützt. Version 1.0 ist aufwärtskompatibel zu 1.1. OpenGL ES 2.0: Unterstützt ab dem iPhone 3GS. OpenGL ES 2.0 wurde bereits 2007 spezifiziert, wird aber erst seit Erscheinen des iPhone 3GS (2009) unterstützt. Dies bedeutet, dass die Mehrheit der derzeit auf dem Markt verfügbaren Geräte nicht kompatibel zur Version 2.0 ist. Gleichwohl unterstützen alle 2.0Geräte auch die 1.1er-Version. OpenGL 2.0 ist nur lauffähig auf Grafikchips, deren Hardware individuell programmiert werden kann über die sogenannte Shader-Programmierung. Damit können Sie Einfluss auf die Render-Pipeline nehmen und für jeden Vertex und jedes Fragment eigene Attribute festlegen. Diese Flexibilität hat aber auch ihren Preis und erfordert eine höhere Einarbeitungszeit. Viele Funktionen, die unter OpenGL ES 1.1 vorhanden sind, müssen unter OpenGL ES 2.0 durch eigene Programmierung ersetzt werden. Gleichwohl haben Sie die Möglichkeit, zur Laufzeit einer App zu testen, ob 2.0 verfügbar ist, und falls nicht, auf 1.1 auszuweichen. Dennoch stellt sich die Frage: Welche OpenGL ES-Version soll ich wählen? Die Frage kann man auch umdrehen und sich fragen, warum man 2.0 unterstützen sollte? Nun, Sie werden kaum ein Programm finden, das ausschließlich für die Version 2.0 programmiert ist (einige exklusive iPad-Titel einmal ausgenommen). Die Vorteile von OpenGL ES 2.0 werden eher in High-End-Spielen sichtbar und sind für den Laien kaum auszumachen. Vielmehr unterscheidet sich 2.0 durch die Architektur der API als durch überragende visuelle Möglichkeiten. Insofern ist OpenGL ES 2.0 nicht unbedingt die
155
4 OpenGL ES – der Turbo-Gang aktuellere API, sondern bietet einen alternativen Ansatz. Beide Varianten der API liegen deutlich je unter 100 KB, sodass OpenGL ES 1.1 auch in Zukunft parallel auf jedem Gerät vorhanden sein dürfte. Umgekehrt gibt es derzeit kein einziges Device, das ausschließlich 2.0 unterstützt. Bedenken Sie auch, dass aufgrund der geringeren Marktdurchdringung ein Spiel mindestens 1.1 unterstützen sollte – wenn Sie Zeit und Muße haben, können Sie später immer noch 2.0-Features nachrüsten. Der Fokus für dieses Buch liegt daher auf OpenGL ES 1.1. Wenn Sie die Grundlagen von 1.1 verstanden haben, sollte es Ihnen nicht schwerfallen, später den Aufbau von OpenGL ES 2.0 nachzuvollziehen (sofern Sie dafür ausreichend motiviert sind).
4.3
OpenGL ES – grundlegende Fragen Anstatt die gesamte API en détail vorzustellen, werden wir zügig auf die für uns als Spieleentwickler relevanten Aspekte eingehen. Um sich dennoch einen Überblick zu verschaffen, ist es sinnvoll, die Navigationsmöglichkeiten innerhalb von Xcode zu nutzen: An jeder Stelle des Source-Codes können Sie über Rechtsklick -> "Jump to Definition" oder "Find Text in Documentation" erlaubte Parameter und Methodensignaturen einsehen. Die offizielle Dokumentation von OpenGL ES 1.1 finden Sie unter:
http://www.khronos.org/opengles/sdk/1.1/docs/man/ Bei Fragen und Problemen rund um OpenGL ES ist ein Besuch des offiziellen Hilfeforums von Khronos empfehlenswert: http://www.khronos.org/message_boards. Aber auch Apple selbst hat mit dem "OpenGL ES Programming Guide" ein Tutorial ins Netz gestellt:
http://developer.apple.com/iphone/library/documentation/3DDrawing/Conceptual/ OpenGLES_ProgrammingGuide/Introduction/Introduction.html Leider sind die Beispiele (und Anleitungen) nicht unbedingt auf das Wesentliche beschränkt und zeigen eher die generellen Möglichkeiten auf. Überhaupt ist die Literatur zum Thema OpenGL ES 1.1, vor allem im Hinblick auf die Spieleentwicklung, so gut wie kaum vorhanden (auch im englischen Sprachraum). Lassen Sie uns daher möglichst einfach und mit grundlegenden Fragestellungen beginnen: Wie zeichne ich eine Linie, ein Dreieck, ein Rechteck? Wie werden Texturen eingesetzt? Wie kann ich ein Sprite animieren? Wie funktioniert die Kollisionskontrolle? Wie scrolle ich den Bildschirm? Woher bekomme ich 3D-Modelle, und wie binde ich diese ein?
156
4.4 Ein Template erstellen – OpenGL ES richtig einbinden Davon ausgehend werden wir später eine etwas komplexere Game-Architektur vorstellen.1 Prinzipiell können Sie aber auch nur den folgenden Einstiegsteil lesen und dann selbstständig experimentieren. Doch zunächst müssen wir erst einmal ein OpenGL ES-Projekt aufsetzen – das Vorgehen ist dabei sowohl für 2D- als auch 3D-Projekte identisch. Unterschiede ergeben sich nur durch die Wahl der Perspektive (2D oder 3D) und durch den Einsatz des Tiefenpuffers (3D).
4.4
Ein Template erstellen – OpenGL ES richtig einbinden Wir erinnern uns, dass wir bisher unsere Spiele über die drawRect:-Methode eines UIgerendert haben. Für OpenGL ES ist dies nicht anders. Wir starten daher mit einem ähnlichen Grundgerüst, das aus einem Delegate, einem View und dem GameManager besteht. Das finale Grundgerüst finden Sie im Projektordner zum Buch unter "OGL_Basics". Es besteht aus den drei (bekannten) Klassen:
Views
1. MyGameAppDelegate: Initialisiert den MainView und startet die Spielschleife. 2. MainView: Initialisiert OpenGL, initialisiert den GameManager, ruft periodisch die drawStatesWithFrame:-Methode des GameManagers auf und leitet den Touch-Input an den GameManager weiter. 3. GameManager: Bestimmt die OGL-Perspektive, sorgt über die drawStatesWithFrame:-Methode für das Rendern des Spiels, verarbeitet den Touch-Input und kümmert sich um die Ressourcenverwaltung des Spiels. Die Verwendung der Klassen unterscheidet sich nicht von den bisherigen Beispielen. Um die OpenGL ES-Programmbibliothek verfügbar zu machen, müssen wir das über Xcode einbinden. Klicken Sie per Rechtsklick auf den Frameworks-Ordner und wählen über "Add -> Existing Frameworks" das OpenGlES.framework aus. OpenGlES.framework
1 Die nachfolgenden Beispiele wurden auf dem Ur-iPhone, dem langsamsten Gerät der iPhone-Familie, getestet. Insofern stellen die OpenGL ES-Beispiele funktionierenden und (hoffentlich gut lesbaren) Code dar, der dennoch Potenzial für Optimierungen bereithält. So haben wir zum Beispiel auf den Einsatz von PVRTC-Texturen (ein spezielles Kompressionsformat für PNGs) und Vertex Buffer Objects (VBO) verzichtet, wobei umstritten ist, ob VBOs tatsächlich schneller auf dem iPhone zu rendern sind – oft hängt dies von den Modelldaten und Texturkoordinaten ab.
157
4 OpenGL ES – der Turbo-Gang
Abb. 4.2: OpenGL ES-Framework über Xcode einbinden
Wenn Sie den OpenGLES-Ordner aufklappen, sehen Sie, dass sowohl die ES1- und ES2Header bereits eingebunden sind. Sie könnten also umgehend auch die OpenGL ES 2.0 Funktionen im Simulator nutzen (was wir aber nicht tun werden). Ebenso sehen Sie die Definition der EAGL-Schnittstelle, die wir gleich brauchen werden, um die MainViewKlasse OpenGL ES-tauglich zu machen. Damit das Framework eingesetzt werden kann, müssen Sie die Header importieren – im Import-Anweisungen hinzu:
MainView-Header fügen wir daher die folgenden #import #import #import #import
Das QuartzCore-Framework wird für den CAEAGLLayer benötigt. Ansonsten müssen Sie nur noch den ES1/gl.h in jeder Klasse einbinden, die auf die OpenGL-Funktionen zugreifen muss. Fügen Sie also die folgende Zeile dem GameManager-Header hinzu: #import
Die drawStatesWithFrame:-Methode des Managers soll wie bisher auch von der Mainaufgerufen werden, wobei der Takt des Game Loops von der Delegate-Klasse vorgegeben wird: Die MyGameAppDelegate-Klasse unterscheidet sich nur minimal von den bisherigen Beispielen und sorgt nun neben der Initialisierung der MainView-Klasse für den Aufruf der setupOGL-Methode:
View-Klasse
mainView = [[MainView alloc] initWithFrame:
158
4.4 Ein Template erstellen – OpenGL ES richtig einbinden [UIScreen mainScreen].applicationFrame]; mainView.backgroundColor = [UIColor grayColor]; [mainView setupOGL];
Ansonsten startet die Klasse wie gewohnt die Spielschleife und ruft periodisch die loopMethode auf: - (void) loop { [mainView drawRect:[UIScreen mainScreen].applicationFrame]; }
Die MainView-Klasse wird zwar weiterhin von UIView abgeleitet, stellt aber dank EAGL eine direkte Verbindung zur Grafik-Hardware her: Wir müssen dafür sorgen, dass der View neu gezeichnet und der zuvor gezeigte Inhalt gelöscht wird. Um konsistent zu den bisherigen Beispielen zu bleiben, rufen wir die drawRect:-Methode manuell in der Schleife auf und übergeben per applicationFrame die aktuellen Screen-Abmessungen. Im Ergebnis bedeutet dies für die MainView-Klasse, dass die drawRect:-Methode wie gewohnt pro Frame aufgerufen wird und über den rect-Parameter die W,H-Dimensionen an den Manager weiterreicht: - (void) drawRect: (CGRect) rect { ... [gameManager drawStatesWithFrame: rect]; ... }
An den drei Pünktchen sehen Sie, dass die Implementierung so noch nicht vollständig ist. Doch der Reihe nach. Um OpenGL einzubinden, müssen Sie die Hauptarbeit in einer von UIView abgeleiteten Instanz vornehmen, in unserem Fall also die MainView-Klasse. Listing MainView.h #import #import "GameManager.h" #import #import #import #import @interface MainView : UIView { GameManager *gameManager; EAGLContext *eaglContext; GLuint renderbuffer; GLuint framebuffer; GLuint depthbuffer; GLint viewportWidth; GLint viewportHeight; } - (void) setupOGL; @end
159
4 OpenGL ES – der Turbo-Gang Sehen wir uns zunächst die Header-Deklaration an. Es gibt nur eine neue Methode: Die bereits erwähnte setupOGL-Methode. Als Member benötigen wir neben dem GameManager außerdem den EAGLContxt, über den wir Zugriff auf den OpenGL-Grafikkontext erhalten (genauer: die Render-Pipeline) und drei Variablen für die Buffer- sowie die ScreenAbmessungen viewportWidth bzw. viewportHeight. Wie Sie sehen, sind für OpenGL eigene Datentypen definiert, um die Plattformunabhängigkeit zu gewährleisten. Natürlich können Sie anstatt GLint auch einfach int verwenden, aber das wäre schlechter Stil und sollte vermieden werden – zumal wir später an einigen Stellen explizit den passenden OpenGL-Datentyp angeben müssen. Über Rechtsklick "Jump to Definition" sehen Sie, welche Datentypen für OpenGL ES deklariert sind – unter anderem: GLbyte, GLfloat, GLshort. Wir werden nicht alle Datentypen brauchen. Was hat es mit den drei Buffern auf sich? Ein Buffer ist zunächst einfach nur ein für OpenGL ES reservierter Speicherbereich, in den unsere App Daten schreiben und auslesen kann. Da sich OpenGL um die Verwaltung des Buffers kümmert, können die Platzierung der Daten und das Format intern optimiert werden.
Abb. 4.3: Drei Buffer: Renderbuffer, Depthbuffer und Framebuffer
Alle Zeichenvorgänge werden zunächst in den Renderbuffer geschrieben, der die 2DFarbinformation einer Szene bzw. eines Frames vorhält. Für 3D-Apps wird zusätzlich noch mit dem z-Index die Tiefeninformation pro Pixel im Depthbuffer gespeichert. Beides zusammen landet dann im Framebuffer, der als Endpunkt der Grafik-Pipeline für die perspektivisch korrekte Darstellung sorgt. Dies klingt komplizierter, als es ist, aber wir müssen uns zum Glück nicht intensiv damit beschäftigen. Die MainView-Klasse kümmert sich darum, alle drei Buffer sowie den Grafikkontext anzulegen und – das war es auch schon. Sobald wir diese Hürde genommen haben, können wir mit der eigentlichen Render-Arbeit beginnen, die konsequenterweise in den GameManager ausgelagert wird – ebenso wie das Bestimmen der OGL-Perspektive (2D oder 3D). Es schadet trotzdem nicht, sich die Implementierung der MainView-Klasse genauer anzusehen: Listing MainView.m #import "MainView.h" @implementation MainView
Die setupOGL- und die drawRect:-Methode haben wir zunächst ausgespart, um einen besseren Überblick über die MainView-Klasse zu bekommen. Wie Sie sehen: Wenig Neues. Über die vier bekannten Methoden nehmen wir den Touch-Input entgegen und leiten diesen an den GameManager weiter. Damit aus unserem View ein "OGL-View" wird, überschreiben wir lediglich die layerClass-Klassenmethode: + (Class) layerClass { return [CAEAGLLayer class]; }
161
4 OpenGL ES – der Turbo-Gang Anstelle des CALayers (= Core-Animation-Layer), der unseren View als herkömmlichen UIView identifizieren würde, geben wir nun die CAEAGLLayer-Klasse zurück und weisen uns damit bereits vor der Initialisierung als OpenGL-View aus. Sehen wir uns nun die Implementierung der setupOGL-Methode an: - (void) setupOGL { CAEAGLLayer *eaglLayer = (CAEAGLLayer *) self.layer; eaglLayer.opaque = YES; eaglContext = [[EAGLContext alloc] initWithAPI: kEAGLRenderingAPIOpenGLES1]; if (!eaglContext || ![EAGLContext setCurrentContext: eaglContext]) { [self release]; } else { //Renderbuffer glGenRenderbuffersOES(1, &renderbuffer); glBindRenderbufferOES(GL_RENDERBUFFER_OES, renderbuffer); //Framebuffer glGenFramebuffersOES(1, &framebuffer); glBindFramebufferOES(GL_FRAMEBUFFER_OES, framebuffer); glFramebufferRenderbufferOES(GL_FRAMEBUFFER_OES, GL_COLOR_ATTACHMENT0_OES, GL_RENDERBUFFER_OES, renderbuffer); //Graphic context [eaglContext renderbufferStorage: GL_RENDERBUFFER_OES fromDrawable: eaglLayer]; glGetRenderbufferParameterivOES(GL_RENDERBUFFER_OES, GL_RENDERBUFFER_WIDTH_OES, &viewportWidth); glGetRenderbufferParameterivOES(GL_RENDERBUFFER_OES, GL_RENDERBUFFER_HEIGHT_OES, &viewportHeight); //Depthbuffer (3D only) glGenRenderbuffersOES(1, &depthbuffer); glBindRenderbufferOES(GL_RENDERBUFFER_OES, depthbuffer); glRenderbufferStorageOES(GL_RENDERBUFFER_OES, GL_DEPTH_COMPONENT16_OES, viewportWidth, viewportHeight); glFramebufferRenderbufferOES(GL_FRAMEBUFFER_OES, GL_DEPTH_ATTACHMENT_OES, GL_RENDERBUFFER_OES, depthbuffer); glBindRenderbufferOES(GL_RENDERBUFFER_OES, renderbuffer); //rebind if (!gameManager) { gameManager = [GameManager getInstance]; } } }
162
4.4 Ein Template erstellen – OpenGL ES richtig einbinden Abgesehen davon, dass wir am Ende der Methode den GameManager initialisieren, wird zunächst über die CAEAGLLayer-Instanz des eigenen Layers der opaque-Parameter auf YES gesetzt. Apple empfiehlt dies aus Performance-Gründen: Ein undurchsichtiger Layer benötigt weniger Rechenzeit. Danach initialisieren wir den OGL-Grafikkontext über: eaglContext = [[EAGLContext alloc] initWithAPI: kEAGLRenderingAPIOpenGLES1];
Wie Sie sehen, handelt es sich hier um die OpenGL ES 1.1-Variante. Für OpenGL ES 2.0 müsste stattdessen die kEAGLRenderingAPIOpenGLES2-Konstante übergeben werden. Falls die Zuweisung des Kontextes geklappt hat, initialisieren wir die drei Buffer – die entsprechenden Funktionen dafür sind von Apple als _OES-Erweiterung vorgegeben. Sobald die Variablen für den Farb-Renderbuffer und Framebuffer verankert und beide Buffer erzeugt wurden, können wir den EAGL-Grafikkontext mit den Buffern bekannt machen und die Variablen für die Viewport-Abmesungen einbinden. Den Tiefenpuffer legen wir abschließend auch noch gleich an, sodass wir später für 3DSpiele keine weiteren Anpassungen an der MainView-Klasse vornehmen müssen. Achten Sie darauf, nach Erzeugung des Tiefenpuffers den Renderbuffer erneut zu binden. Wir werden den Tiefenpuffer später beim Setzen der OGL-Perspektive für unsere ersten 2DGehversuche deaktivieren. Die verwendeten Parameter zur Erzeugung der Buffer sind für alle Beispiele dieses Buches geeignet – auch hier müssen wir später keine weiteren Anpassungen vornehmen. Abschließend sollten Sie noch die erzeugten Buffer und den EAGL-Layer in der deallocMethode abräumen: if (eaglContext) { glDeleteRenderbuffersOES(1, &depthbuffer); glDeleteFramebuffersOES(1, &framebuffer); glDeleteRenderbuffersOES(1, &renderbuffer); [eaglContext release]; }
Damit können wir uns nun der Implementierung der drawRect:-Methode zuwenden und zeigen, wie die Buffer dort angesprochen werden: - (void) drawRect: (CGRect) rect { glViewport(0, 0, viewportWidth, viewportHeight); glClearColor(0.5, 0.5, 0.5, 1.0); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); [gameManager drawStatesWithFrame: rect]; [eaglContext presentRenderbuffer: GL_RENDERBUFFER_OES]; }
Zunächst bestimmen wir den Viewport über die glViewport()-Funktion. Die DefaultWerte beziehen sich auf einen Fullscreen – Sie können über diese Methode den Viewport aber auch verkleinern.
163
4 OpenGL ES – der Turbo-Gang Damit wir pro Frame eine saubere Zeichenfläche vorfinden, löschen wir den sichtbaren Screenbereich explizit: glClearColor(0.5, 0.5, 0.5, 1.0); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
Über glClearColor() legen wir die Löschfarbe fest, mit der der Viewport einfarbig übermalt werden soll (hier: Grau), dann löschen wir die aktuelle Farbinformation aus dem Renderbuffer und dem Depthbuffer. Für 2D-Spiele reicht es, lediglich den GL_COLOR_ BUFFER_BIT-Flag an die glClear()-Funktion zu übergeben, da der z-Wert stets 0 ist. Sobald der Renderbuffer von allen Altlasten befreit ist, kann ein neuer Rendervorgang gestartet werden: [gameManager drawStatesWithFrame: rect]; [eaglContext presentRenderbuffer: GL_RENDERBUFFER_OES];
Den eigentlichen Rendervorgang haben wir in die bereits erwähnte GameManager-Methode drawStatesWithFrame: verlagert. Um die Szene zu rendern, muss abschließend der Renderbuffer über den EAGL-Kontext und den Framebuffer perspektivisch dargestellt werden, dies erledigt die presentRenderbuffer:-Methode anhand der zuvor festgelegten Perspektive. Hinweis: Um sicherzustellen, dass wir pro Frame den passenden Grafikkontext anwählen, können wir den EAGL-Kontext optional über die setCurrentContext:-Methode erneut pro Frame setzen und die verwendeten Buffer nochmals binden. Da wir nur einen einzigen Kontext und je einen Buffer verwenden, ist dies für unsere Beispiele nicht notwendig. Über die UIView-Methode layoutSubviews können Sie sich zusätzlich noch darüber informieren lassen, falls sich die Layer-Eigenschaften geändert haben – in diesem Fall müssen Sie den EAGL-Grafikkontext erneut anlegen und den Renderbuffer nochmals binden. Apple zeigt in seinem OpenGL ES Application-Template, das Sie über Xcode starten können, wie dies realisiert werden kann. Die Implementierung des Views wird dadurch natürlich etwas umfangreicher.2
4.5
Das OpenGL-Koordinatensystem OpenGL unterstützt sowohl die 2D- als auch die 3D-Programmierung. Eine erste Unterscheidung ergibt sich anhand der gewählten Perspektive, die über die folgenden beiden Funktionen festgelegt werden kann:
2 Es besteht außerdem die Möglichkeit, einen klassischen UIView (CALayer) mit einem OGL-View (CAEAGLLayer) zu kombinieren. Zum Beispiel, um NSStrings auf dem OGL-View zu rendern – Apple empfiehlt dies aus Performance-Gründen jedoch nicht. Falls Sie keine Lust haben, Ihre Menü-Screens ebenfalls mit OpenGL ES zu rendern, dann können Sie natürlich problemlos zwischen einem UIView und dem OGL-View wechseln. Das UIKit ist dafür ausgelegt, mehrere UIViews zu unterstützen (Stichwort: UIViewController).
Die 3D-Perspektive werden wir uns später ansehen – für die nachfolgenden Beispiele werden wir uns zunächst auf eine reine 2D-Szenerie beschränken und den Einsatz der glOrthof()-Funktion zeigen. Das Bestimmen der Perspektive ist der letzte neue Baustein, den wir zum Aufsetzen eines OpenGL ES-Grundgerüstes benötigen. Die Implementierung der Perspektive erfolgt naheliegenderweise in der GameManagerKlasse, die dem bereits bekannten Aufbau entspricht. Wir haben ja bereits gesehen, dass die MainView-Klasse nach wie vor die zentrale drawStatesWithFrame:-Methode des GameManagers aufruft. In dieser verzweigen wir auf den PLAY_GAME-State und rufen dort die playGame-Methode auf. Damit stellt die playGame-Methode für die nächsten Beispiele unsere Spielwiese dar, auf der wir unsere ersten OpenGL ES-Gehversuche wagen wollen. Sie erinnern sich noch, dass die GameManager-Klasse statisch initialisiert wird und danach die preloader-Methode aufruft. In dieser können wir einmalig über [self setOGLProjection];
die gewünschte Perspektive festlegen: - (void) setOGLProjection { //Set View glMatrixMode(GL_PROJECTION); glLoadIdentity(); glOrthof(0, W, H, 0, 0, 1); //2D-Perspektive //Enable Modelview: Auf das Rendern von Vertex-Arrays umschalten glMatrixMode(GL_MODELVIEW); glEnableClientState(GL_VERTEX_ARRAY); glDisable(GL_DEPTH_TEST); //2D only }
Dieser Aufruf bewirkt über die glOrthof()-Funktion, dass sich der Koordinatenursprung in der linken, oberen Screen-Ecke befindet. Die positive x-Achse zeigt nach rechts, die positive y-Achse nach unten.
165
4 OpenGL ES – der Turbo-Gang
Abb. 4.4: 2D-Koordinatensystem unter OpenGL ES
Obwohl die glOrthof()-Funktion, im Gegensatz zu ihrem 3D-Pendant glFrustumf(), bewirkt, dass die z-Koordinaten bei der Berechnung des Framebuffers ignoriert werden, sollten wir trotzdem den Depthbuffer explizit ausschalten über glDisable (GL_DEPTH_TEST). Die zNear- bzw. zFar-Parameter können für die 2D-Perspektive ignoriert werden und werden standardmäßig auf 0 bzw. 1 gesetzt. Mit left/top (0/0) bzw. right/bottom (W/H) bestimmen Sie die linke, obere bzw. die rechte, untere Ecke des Viewports. Der Aufruf von glOrthof(0, W, H, 0, 0, 1);
erzeugt daher ein klassisches 2D-Koordinatensystem wie in der Computergrafik üblich – mit vertauschter y-Achse, die nach unten zeigt. Sie können die Parameter der glOrthof()Funktion aber auch dahingehend ändern, dass Sie das aus der Schule bekannte Koordinatensystem mit Mittelpunkt in der Screen-Mitte und nach oben zeigender y-Achse erhalten, ein Beispiel dafür folgt weiter unten. Die Perspektive wird mathematisch über die sogenannte Matrizenrechnung bestimmt. OpenGL ES verfügt über zwei für uns relevante Arten der Matrizenberechnung, die wir über die glMatrixMode()-Funktion aktivieren können. Jeweils nur ein Mode ist gleichzeitig möglich: GL_PROJECTION: Um die Perspektive festzulegen, müssen wir den GL_PROJECTIONMode aktivieren. Danach sollte die aktuelle Matrix durch die Einheitsmatrix ersetzt werden. Dies wird durch den Aufruf von glLoadIdentity() erledigt. Nun können wir über glOrthof() bzw. glFrustumf() die Art der Perspektive bzw. die Projektion der Szene auf das 2D-Display angeben. GL_MODELVIEW: Nachdem wir einmalig die Perspektive bestimmt haben, werden für die nachfolgenden Render-Vorgänge nur noch Modelle zur Anzeige gebracht. Wir können daher dauerhaft auf den GL_MODELVIEW-Mode umschalten – und alle weiteren Matrizenoperationen beziehen sich nur noch auf den Model-View. Doch halt, was meinen wir eigentlich mit Modellen? Nun, für 2D- bzw. 3D-Spiele unter OpenGL ES können wir nicht einfach angeben, dass wir ein Bild an der Position xyz an-
166
4.6 Einfache Zeichenoperationen zeigen wollen. Wir müssen zuvor genau festlegen, wo die einzelnen Bildpunkte liegen sollen, kurz: Wir brauchen ein Modell, das aus einzelnen Eckpunkten zusammengesetzt ist. Triangulieren Sie wissen vielleicht, dass in Computerspielen Polygone gerendert werden, die aus zahlreichen kleinen Dreiecken zusammengesetzt sind. Dies hat einen einfachen Grund. Um eine Fläche (2D oder 3D) mathematisch zu beschreiben, braucht man mindestens drei Punkte. Und die Fläche, die sich aus drei Punkten ergibt ist – ergo: ein Dreieck (englisch: Triangle). Unabhängig davon, ob wir nun ein 2D-Dreieck oder ein 3D-Dreieck auf dem Screen anzeigen wollen – wir müssen die Eckpunkte des Dreiecks angeben. Einen einzigen Eckpunkt nennt man auch Vertex (Plural: Vertices). Später können Sie noch ein Bild angeben, das auf dieser Dreiecksfläche angezeigt werden soll, eine sogenannte Textur. Doch zunächst besteht jedes grafische Gebilde, das wir auf dem Screen rendern können, lediglich aus ein paar Eckpunkten – dem Modell. Deshalb aktivieren wir dauerhaft den GL_MODELVIEW-Matrix-Mode, da wir wissen, dass ab jetzt nur noch Modelle in Form von Eckpunkten gerendert werden sollen. Diese Modelle übergibt man typischerweise als Array – deshalb haben wir zusätzlich den nächsten OpenGL ES-State gesetzt: glEnableClientState(GL_VERTEX_ARRAY);
Es gibt noch weitere Client-States, die wir parallel aktivieren können, aber der GL_VERTEX_ARRAY-State wird von uns immer benötigt: Arrays, bestehend aus einzelnen Eckpunkten, das ist alles, was wir rendern können – für OpenGL ES besteht die Welt nur aus Eckpunkten. Aus diesem Grund steht uns zunächst auch keine drawImage:-Methode zur Verfügung – auch Text können wir nicht einfach so ausgeben. Die API konzentriert sich auf den direkten Hardware-Zugriff, und Methoden wie drawImage: oder drawString: müssen wir schon selbst implementieren. Aber keine Sorge, das ist gar nicht so schwer.
4.6
Einfache Zeichenoperationen Obwohl das OpenGL ES-Grundgerüst bisher nur einen grauen Screen anzeigt – die höchste Hürde haben wir bereits genommen. Und die gute Nachricht ist, dass wir uns vorerst nicht weiter mit den jeweiligen Buffern, Matrizen oder der Perspektive beschäftigen müssen. Dies alles können wir (zunächst) hinter uns lassen und uns der playGame-Methode zuwenden, über die wir endlich ein paar erste Grafiken ausgeben wollen. Das Hello World der OpenGL-Programmierung besteht darin, die einfachste Flächenform auszugeben – ein weißes Dreieck: - (void) playGame { [self drawOGLTriangle]; } - (void) drawOGLTriangle {
Den Code zum Anzeigen des Dreiecks haben wir in eine eigene Methode ausgelagert und die Eckpunkte des Dreiecks in einem Array vom Typ GLshort abgelegt. Es gibt drei Eckpunkte, bestehend aus je zwei xy-Koordinaten – macht insgesamt sechs Array-Elemente. Die Reihenfolge der Punkte wird entgegen dem Uhrzeigersinn angegeben – beginnend beim linken, unteren Punkt des Dreiecks (x = 0, y = 250). Sagten wir, alles besteht aus Dreiecken? Na ja, nicht ganz – neben Dreiecken gibt es auch Punkte und Linien, aber das war es auch schon, alle anderen 2D-/3D-Formen müssen Sie aus diesen Primitiven ableiten. Die Basis bildet immer ein Vertex, der aus zwei (2D, xy), drei (3D, xyz) oder gar vier (4D, homogene Koordinaten) Komponenten bestehen kann. Für unsere 2D-Perspektive haben wir das Koordinatensystem so geeicht, dass die Einheit der Koordinaten in Pixeln angegeben werden kann. Der Punkt x = 10, y = 20 liegt also 10 Pixel vom rechten Bildschirmrand entfernt und 20 Pixel vom oberen. Für ein 3DKoordinatensystem spricht man dagegen nur noch von Einheiten, da eine feste Bezugsgröße fehlt: Der Punkt x = 10, y = 20, z = –100 kann sich überall auf dem Display befinden, je nachdem, von welcher Perspektive man auf die Szenerie blickt. Doch zurück zu unseren Dreiecken im 2D-Raum. Gut, wir haben drei Eckpunkte angegeben, aber was passiert dann? Wie wird aus den drei Punkten eine weiße Fläche? Okay, die Farbe haben wir nicht angegeben, also rendert OpenGL erst mal alles in Weiß. Um eine andere Farbe anzugeben, braucht man lediglich die glColor4f()-Funktion vor dem Zeichnen aufzurufen. Da OpenGL als State Machine arbeitet, bleibt diese Farbgebung so lange
168
4.6 Einfache Zeichenoperationen erhalten, bis eine andere Farbe gesetzt wird. Die Parameter sind: R-G-B plus Alpha, wobei die Werte jeweils von 0 bis 1 reichen. Um das Dreieck grün einzufärben, würden Sie zum Beispiel die folgende Zeile in den Code einfügen: glColor4f(0, 1, 0, 1);
Aber wie steht es nun mit der Fläche? Was dem Eskimo sein Schnee, sind für OpenGL ES die Eckpunkte: Es gibt mehrere Arten, die Eckpunkte sichtbar zu machen, sprich: zu rendern. OpenGL ES kennt die folgenden Primitiven: GL_POINTS GL_LINES GL_LINE_STRIP GL_LINE_LOOP GL_TRIANGLES GL_TRIANGLE_STRIP GL_TRIANGLE_FAN
In der Grafik können Sie sehen, wie die einzelnen Punkte im Array abgelegt werden müssen, um die jeweilige Form zu erzeugen.
Abb. 4.6: Übersicht der verfügbaren Primitiven
Den gewünschten Parameter übergeben Sie dann der glDrawArrays()-Funktion. Um aus einem Array mit beliebigen Elementen eine grafische Ausgabe zu erzeugen, sind also grundsätzlich drei Schritte notwendig: 4. Das Array mit den gewünschten Eckpunkten anlegen. 5. Das Array der OpenGL ES Render-Pipeline zuführen. Eine Array-Variable ist nichts anderes als ein Pointer, der auf das erste Element zeigt. OpenGL ES stellt die Funktion glVertexPointer (GLint size, GLenum type, GLsizei stride, const GLvoid
bereit. Der erste Parameter gibt an, wie viele Komponenten ein Eckpunkt besitzt (dies geht so erst mal nicht aus dem Array hervor, also müssen wir das explizit angeben). Mögliche Werte sind 2, 3 oder 4. Danach geben Sie den Datentyp der Elemente an. Für jeden Datentyp stellt OpenGL die passende Konstante bereit. Da wir das Array als GLshort deklariert haben, wählen wir die Konstante GL_SHORT. Sie wissen
*pointer)
169
4 OpenGL ES – der Turbo-Gang vielleicht, dass sich aus dem Datentyp ergibt, wie viele Bytes ein Element im Speicher belegt. Damit OpenGL durch alle Elemente des Arrays iterieren kann, müssen wir angeben, wie viele Bytes ein Element im Speicher belegt. Die Elemente des Arrays liegen hintereinander im Speicher. Normalerweise jedenfalls, über den stride-Parameter können Sie einen Offset zwischen den Elementen angeben. Für uns ist dieser Wert jedoch einfach 0. Zu guter Letzt geben wir dann den Array-Zeiger an: glVertexPointer(2, GL_SHORT, 0, vertices). 6. Nun weiß OpenGL, wo das Array mit den Eckpunkten liegt. Jetzt brauchen wir nur noch den Render-Vorgang auszulösen. Der glDrawArrays()-Funktion geben wir dabei noch drei zusätzliche Parameter mit auf den Weg, um zu bestimmen, wie die Eckpunkte visualisiert werden sollen: glDrawArrays (GLenum mode, GLint first, GLsizei count). Den mode-Parameter haben wir ja oben schon vorgestellt. Um das Dreieck ungefüllt zu zeichnen, würden wir statt GL_TRIANGLES den Parameter GL_LINE_LOOP übergeben – und um nur die Eckpunkte anzuzeigen: GL_POINTS. Über die beiden nächsten Parameter first und count bestimmen wir weitere Eigenschaften der Eckpunkte:
first: Index des ersten Array-Elements, das gerendert werden soll. Um das gesamte Array zu rendern, fangen wir also bei 0 an. Sie können aber auch Elemente überspringen, um so nur Teile des Arrays zu rendern.
Noch weiß OpenGL nicht, wie lang unser Array ist. Anstatt nun aber einfach die Länge des Arrays anzugeben, erwartet OpenGL hier die Anzahl der Eckpunkte, die wir rendern wollen. Also: Wir haben zwei Koordinaten (xy) pro Eckpunkt und wollen drei Eckpunkte rendern, beginnend beim ersten Element 0 (unser Array hat eine Länge von 2*3 Elementen): glDrawArrays(GL_TRIANGLES, 0, 3).
count:
Sehen wir uns ein weiteres Beispiel an. Um eine Linie zwischen zwei beliebigen Punkten zu definieren, können wir die folgende Methode schreiben: - (void) drawOGLLineFrom: (CGPoint) p1 to: (CGPoint) p2 { GLshort vertices[ ] = { p1.x, p1.y, p2.x, p2.y }; glVertexPointer(2, GL_SHORT, 0, vertices); glColor4f(1, 0, 0, 1); glDrawArrays(GL_LINES, 0, 2); }
Ein Aufruf von [self drawOGLLineFrom: CGPointMake(0, H/2) to: CGPointMake(W, H/2)];
bewirkt dann eine horizontale, rote Linie. Allerdings hat dieses Beispiel einen kleinen Haken: Wir übergeben der Methode die Eckpunkte der Linie als Parameter. Nun, darüber können wir die Linie über den Screen bewegen und die Koordinaten zur Laufzeit verändern. Aber: Wie soll das für ein komplexes 3D-Modell funktionieren, das aus mehreren Tausend Eckpunkten besteht? Um dieses über den Screen zu bewegen, müssten wir alle Punkte manuell verschieben und das so geänderte Array rendern. OpenGL bietet hier eine wesentlich elegantere Lösung, mit der wir alle Eckpunkte, die aktuell in unserer Szene im
170
4.6 Einfache Zeichenoperationen Speicher liegen, skalieren, rotieren oder verschieben können. Wie diese Matrixoperationen mathematisch funktionieren, müssen wir dabei nicht wissen, da OpenGL ES die entsprechenden Funktionen zur Verfügung stellt: //Verschieben glTranslatef (GLfloat x, GLfloat y, GLfloat z); //Rotieren glRotatef (GLfloat angle, GLfloat x, GLfloat y, GLfloat z); //Skalieren glScalef (GLfloat x, GLfloat y, GLfloat z);
Die unteren beiden Funktionen werden wir uns später ansehen. Damit wir die Modelle gefahrlos verschieben können – ohne Auswirkungen auf andere Modelle fürchten zu müssen –, stellt OpenGL ES einen Mechanismus bereit, der uns bekannt vorkommen dürfte: Um Rotationen mit Core Graphics durchzuführen, haben wir den alten Zustand des Grafikkontextes gespeichert, dann die Rotationen durchgeführt und schließlich den Ursprungszustand wiederhergestellt. Auch für OpenGL ES gibt es einen solchen Ursprungszustand, den wir jederzeit herstellen können, wenn wir die glLoadIdentity()-Funktion aufrufen und damit alle aktuellen Matrizenoperationen durch die Einheitsmatrix ersetzen – allerdings würden wir dadurch auch die zuvor gemachten Änderungen zurücksetzen. Deshalb speichert OpenGL Änderungen an der Matrix in einem Stack – bis zu 16 Matrizen können im GL_MODELVIEW-Mode auf dem Stapel liegen. Aber so viele brauchen wir gar nicht. Wir müssen lediglich dafür sorgen, dass wir eine Kopie der aktuellen Matrix (mit allen zuvor gemachten Transformationen) erhalten – auf dieser können wir dann die gewünschten Änderungen durchführen und rendern. Danach löschen wir die aktuelle Matrix vom Stack, und das System befindet sich wieder in seinem Ursprungszustand. Für diesen Zweck stellt OpenGL ES die folgenden beiden Funktionen zur Verfügung: glPushMatrix(); //... Transformieren der Matrix //... Rendern glPopMatrix();
Durch die Transformation wird das gesamte Koordinatensystem verändert: Um ein Modell, bestehend aus einzelnen Eckpunkten, zu verschieben, verschieben wir also eigentlich nicht das Modell, sondern stattdessen das Koordinatensystem. Nachdem das Modell gerendert ist, können wir das Koordinatensystem zurück an die alte Position verschieben – oder aber das zuvor gespeicherte Koordinatensystem herstellen: Die Funktion glPushMatrix() setzt ein Duplikat der aktuellen Matrix auf den Stapel. Damit können wir dann die gewünschten Änderungen vornehmen, zum Beispiel unser Modell verschieben, rotieren oder skalieren und dann dem Renderbuffer zuführen. Danach sorgt die glPopMatrix()Funktion dafür, dass das oberste Element vom Stapel gelöscht und damit das darunter liegende, vorherige Element zum obersten Element wird (und so gleichsam die aktuelle Matrix ersetzt): Die aktuelle Matrix befindet sich nun wieder in genau dem gleichen Zustand wie vorher. Natürlich können wir diesen Vorgang mit beliebig vielen weiteren Modellen
171
4 OpenGL ES – der Turbo-Gang durchführen – für jedes Modell gilt die gleiche Ausgangslage, sprich: Alle Modelle operieren mit der gleichen Matrix. So viel zur Theorie, in der Praxis funktioniert das ganz einfach, wie wir gleich sehen werden. Wir implementieren eine neue Methode, über die wir ein Rechteck rendern wollen: - (void) drawOGLRect: (CGRect) rect { GLshort vertices[ ] = { 0, rect.size.height, //links unten rect.size.width, rect.size.height, //rechts unten 0, 0, //links oben rect.size.width, 0 //rechts oben }; glVertexPointer(2, GL_SHORT, 0, vertices); glColor4f(1, 1, 0, 1); glPushMatrix(); glTranslatef(rect.origin.x, rect.origin.y, 0); glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); glPopMatrix(); }
Anstatt nun die Eckpunkte direkt in das Array zu packen, positionieren wir das Rechteck am Ursprung (0, 0) und verändern nur die Größe (width/height). Um nun das Rechteck an der gewünschten xy-Position zu rendern, rufen wir die glTranslatef()-Funktion auf – da die Methode immer auch einen z-Wert erwartet, setzen wir diesen auf 0. Damit die Funktion nicht fortlaufend das Koordinatensystem verschiebt, klammern wir den Aufruf durch das glPushMatrix()/glPopMatrix()-Pärchen ein und können die Methode so mehrmals aufrufen: Die Translation wirkt sich dadurch scheinbar immer nur auf das aktuelle Array aus.
Abb. 4.7: Zufällig gerenderte gelbe Rechtecke (10x10 Pixel) und eine rote Linie
Tatsächlich rendern wir das Rechteck stets an der Position (0, 0), verschieben aber kurzzeitig das Koordinatensystem, indem wir die aktuelle Matrix mit der Translationsmatrix
172
4.7 Exkurs: Mathe-Plotter multiplizieren (dies erledigt die glTranslatef()-Funktion für uns). Da die alte Matrix danach wieder hergestellt wird, finden nachfolgende Operationen ein unverändertes Koordinatensystem vor: Unser Rechteck befindet sich aber trotzdem an der gewünschten Position, da wir das Array ja bereits in den Renderbuffer geschoben haben.
Abb. 4.8: Jedes Rechteck ist aus zwei Dreiecken zusammengesetzt.
Wie Sie vielleicht bemerkt haben, besteht das Rechteck aus vier Eckpunkten. Dies liegt daran, dass wir den Array-Mode GL_TRIANGLE_STRIP gewählt haben – denn das Rechteck besteht eigentlich aus zwei Dreiecken, die sich aber zwei Eckpunkte teilen. Dadurch können wir das Array von sechs Eckpunkten auf vier verkürzen. Jeder neue Punkt bildet mit den beiden vorhergehenden Punkten so automatisch eine neue Dreiecksfläche.
4.7
Exkurs: Mathe-Plotter Wer sich daran stört, dass unser Koordinatensystem seinen Ursprung in der linken, oberen Ecke hat – vielleicht weil er das aus der Schule anders gewöhnt ist –, braucht nicht zu verzweifeln. Wie bereits erwähnt, können wir die Parameter der glOrthof()-Funktion ändern, um das klassische, aus dem Mathe-Unterricht bekannte Koordinatensystem zu erhalten. Wir haben hierzu ein neues Beispielprojekt auf der Basis unseres Grundgerüstes angelegt, das wir "OGL_MathPlotter" genannt haben. Ziel des Projektes ist es, einen beliebigen Funktionsgraphen anzuzeigen. Um das Koordinatensystem einzurichten, müssen wir die setOGLProjection-Methode anpassen: - (void) setOGLProjection { //Set View glMatrixMode(GL_PROJECTION); glLoadIdentity(); glOrthof(-W/2, W/2, -H/2, H/2, 0, 1); glMatrixMode(GL_MODELVIEW); glEnableClientState(GL_VERTEX_ARRAY); glDisable(GL_DEPTH_TEST); //2D only }
173
4 OpenGL ES – der Turbo-Gang Sie sehen, die einzige Änderung besteht in den Funktionsparametern der glOrthof()Funktion. Left/Top verweist auf den Punkt (-W/2, H/2) Right/Bottom verweist auf den Punkt (W/2, -H/2) Damit befindet sich der Koordinatenursprung exakt in der Display-Mitte, und die (sichtbaren) Achsen haben je eine Gesamtlänge, die den Screen-Abmessungen entspricht. Durch die Vorzeichen ergibt sich, dass die positive x-Achse nach rechts zeigt und die positive y-Achse nach oben.
Abb. 4.9: Der Graphenplotter mit einer Funktion dritten Grades
Nach dieser Vorarbeit ist es kein Problem mehr, den Graphen einer beliebigen mathematischen Funktion darzustellen: - (void) draw2DGraphPlotter { //x-Achse [self drawOGLLineFrom: CGPointMake(W/2, 0) to: CGPointMake(-W/2, 0)]; //y-Achse [self drawOGLLineFrom: CGPointMake(0, H/2) to: CGPointMake(0, -H/2)]; for (float x = -W/2; x < W/2; x += 0.01) { //Wertebereich von x //Die Funktion float y = 0.01 * x*x; //Gestauchte quadratische Funktion GLfloat point[ ] = { x, y }; glVertexPointer(2, GL_FLOAT, 0, point);
174
4.8 Und Bilder? Wie wär's mal mit Texturen! glDrawArrays(GL_POINTS, 0, 1); } }
Nachdem wir über die drawOGLLineFrom-Methode die beiden Achsen eingezeichnet haben, können wir in einer for-Schleife durch den Wertebereich einer mathematischen Funktion iterieren:
y = 0.01* x2 Das Ergebnis der Gleichung rendern wir danach als Punkt-Array vom Typ GL_POINTS. Für jeden x-Wert wird also ein Punkt gerendert. Um eine Funktion dritten Grades der Form:
y = a * x3 zu rendern, tauschen Sie einfach die quadratische Gleichung aus: float y = 0.0001 * x*x*x;
Im Ergebnis erhalten Sie dann den im Screenshot gezeigten Graphen. Da die Achsen mit ± 160 bzw. ± 240 Pixeln deutlich länger als von der Schule gewohnt sind, haben wir a möglichst klein gewählt, um den Graphen entsprechend zu stauchen. Alternativ können Sie die Graphenansicht vergrößern, indem Sie zum Beispiel die Parameter der glOrthof()-Funktion proportional verkleinern. Der Darstellungsbereich wird dadurch vergrößert – oder aber Sie skalieren das Koordinatensystem mit der glScalef()Funktion. Den Einsatz dieser Funktion werden wir später noch an einem Beispiel zeigen.
4.8
Und Bilder? Wie wär's mal mit Texturen! Als Spieleentwickler können wir uns natürlich nicht mit eingefärbten Flächen begnügen. Sowohl für 2D-Spiele als auch für 3D-Spiele lassen sich jedoch Bilder, zum Beispiel im PNG-Format, nicht einfach so anzeigen. Dies geht nur über sogenannte Texturen. OpenGL ES kümmert sich als plattformunabhängige API natürlich nicht um spezielle Bildformate – um eine Textur zu erzeugen, muss man stattdessen die einzelnen Pixel eines Bildes bereitstellen. Hierbei stellt sich zunächst die Frage, wie man an die Bytes eines Bildes herankommt. Wir werden dieses Problem mithilfe von Core Graphics lösen. Eine Textur ist im Prinzip nichts anderes als ein normales Bild auch: Die Pixeldaten werden in einen von OpenGL ES reservierten Speicherbereich geschrieben und können dann genutzt werden. Die Abmessungen der Ausgangsbildes spielen keine Rolle, die Textur wird auf die durch die Vertices bestimmte Fläche gemappt und dabei skaliert und beschnitten – oder aber auch gewrappt. Beim Texturieren gibt es verschiedene Möglichkeiten. Normalerweise legt ein Grafikdesigner beim Entwerfen eines 3D-Models auch eine Wicklung der Textur fest, die seinen Vorstellungen entspricht. Die Textur muss man sich dabei vorstellen wie einen ausgerollten Teppich, der dann später anhand von Texturkoordinaten auf die Vertices gerendert wird. Zu jedem Vertex gehört genau eine Texturkoordinate.
175
4 OpenGL ES – der Turbo-Gang
Für 2D-Spiele vereinfacht sich der Prozess. Denn sowohl die Textur als auch unsere Fläche haben zwei Dimensionen – mit einer rechteckigen Grundform. Hinweis: Damit die Grafik-Hardware mit den Texturen des Modells effizient arbeiten kann, muss die Abmessung der Textur einer Zweier-Potenz entsprechen. Ansonsten wird statt der Textur eine weiße Fläche angezeigt. Breite und Höhe müssen allerdings nicht identisch sein. Die maximale Breite bzw. Höhe einer Textur beträgt auf den Geräten der iPhone-Familie 2048 Pixel. Erlaubte Größen sind zum Beispiel 32 * 64, 128 * 128, 512 * 32 oder 1024 * 2048. Transparente Flächen sind erlaubt, das heißt, der Alphakanal wird voll unterstützt. Beginnen wir damit, unsere erste eigene Textur auf dem Display zu rendern. Das Ziel ist ja generell, beliebige PNGs über eine einfache Methode zu rendern, wie zum Beispiel: [self drawOGLImg: @"player.png" at: CGPointMake(x, y)];
Die Implementierung einer solchen Methode wollen wir nun in Angriff nehmen. Wir legen also ein neues Projekt an, wieder auf Basis des Grundgerüstes, und nennen es "OGL_Texture" – unter diesem Namen finden Sie das fertige Projekt auch auf der Download-Seite zum Buch. Als Grafik, die wir anzeigen wollen, wählen wir ein 64x64 Pixel großes Raumschiff:
Abb. 4.10: player.png
Wie alle Grafiken besitzt auch diese eine rechteckige Grundform (genauer: quadratisch). Wir haben ja bereits gesehen, wie man eine rechteckige Grundform als 2D-Fläche in OpenGL ES definieren kann: GLshort imageVertices[ ] = { 0, height, //links unten width, height, //rechts unten 0, 0, //links oben width, 0 //rechts oben };
So weit, so gut. Doch wie läuft das nun mit dem Texturieren? width/height können wir im Array durch 64 ersetzen, dann entspricht unsere Fläche exakt der Größe des PNG. Da zu jedem Eckpunkt auch eine Texturkoordinate gehört, brauchen wir also ein weiteres Array mit genau vier Texturkoordinaten: GLshort textureCoords[ ] = { 0, 1, //links unten 1, 1, //rechts unten 0, 0, //links oben 1, 0 //rechts oben };
176
4.8 Und Bilder? Wie wär's mal mit Texturen!
Gar nicht so schwer, oder? Doch Achtung: Texturkoordinaten können beliebig gestreckt oder gestaucht werden, deshalb macht es keinen Sinn, hier tatsächliche Koordinaten anzugeben. Infolgedessen reicht der Wertebereich einer 2D-Textur per Definition immer von 0 bis 1.
Abb. 4.11: Die Textur wird auf die Eckpunkte gemappt, links: 1:1, rechts: gestreckt.
Um deutlich zu machen, dass die Texturkoordinaten nicht mit normalen Koordinaten zu verwechseln sind, tragen diese die Bezeichnung st-Koordinaten (manchmal auch uv-Koordinaten), wobei s dem x-Wert entspricht und t dem y-Wert. Da die Abmessungen aller Texturvorlagen, ebenso wie die player.png-Grafik, einer Zweier-Potenz entsprechen müssen und damit durch 2 teilbar sind, können wir die Textur leicht unterteilen: Wenn wir zum Beispiel nur die linke Hälfte des Raumschiffs auf eine Fläche mappen wollen, würden die Texturkoordinaten von 0 bis 0.5 reichen: GLfloat textureCoords[ ] = { 0, 1, //links unten 0.5, 1, //rechts unten 0, 0, //links oben 0.5, 0 //rechts oben };
Achtung: Als Datentyp müssen wir nun natürlich GLfloat wählen. Da unsere VertexFläche immer noch die Abmessungen width * height hat, wird die Textur in der Breite gestreckt auf die Fläche gemappt. Wollen Sie dagegen die Proportionen erhalten und nur die linke Hälfte des Flugzeuges unverzerrt darstellen, müssten Sie auch das Vertex-Array halbieren. Sie merken, dass es für die Texturkoordinaten völlig unerheblich ist, welche tatsächlichen Abmessungen die Ausgangsgrafik hat, die Abmessung der Textur beträgt immer 1 x 1. Aber fürs Erste kümmern wir uns nicht weiter um die Unterteilung der Textur: Um Grafiken anzuzeigen, benötigen wir lediglich die vier Eckpunkte der Grafik (abhängig von width, height) und die vier Eckpunkte der Textur (stets 1, 1). Hat man in OpenGL ES erst einmal eine Textur erzeugt, so liegt diese als Integer-ID im Speicher und kann beliebig oft eingesetzt werden. Da der Code zur Erzeugung einer Textur recht umfangreich ist, lagern wir diesen in einer neuen Klasse namens Tex aus. Die gute Nachricht vorneweg: Das Erzeugen von Texturen für 2D-Spiele unterscheidet sich nicht
177
4 OpenGL ES – der Turbo-Gang
von der Erzeugung von Texturen für 3D-Spiele, sodass wir die Klasse später erneut einsetzen können. Listing Tex.h #import @interface Tex : NSObject { GLuint textureID; int width; int height; } //Initialisierer - (void) createTexFromImage: (NSString *) picName; //Texturerzeugung - Helper - (GLubyte *) generatePixelDataFromImage: (UIImage *) pic; - (void) generateTexture: (GLubyte *) pixelData; //Textur rendern - (void) drawAt: (CGPoint) p; //Getter - (GLuint) getTextureID; - (int) getWidth; - (int) getHeight; @end
Wie Sie sehen, speichert die Tex-Klasse lediglich die Textur-ID und die Abmessungen der Texturvorlage als Member ab. Neben ein paar Gettern kommen wir damit auf vier Methoden: - (void) createTexFromImage: (NSString *) picName; Um eine neue Textur zu erzeugen, soll lediglich der Name der Bilddatei genügen. Diese Methode ruft intern die beiden Helper generatePixelDataFromImage: und generateTexture: auf, um die Textur tatsächlich zu erzeugen. - (void) drawAt: (CGPoint) p; Danach sollten wir auch schon in der Lage sein, die Textur an einem beliebigen Punkt zu zeichnen. - (GLubyte *) generatePixelDataFromImage: (UIImage *) pic; Um die Textur zu erzeugen, müssen wir zunächst an die Pixel-Rohinformation herankommen. Diese Methode liefert uns zu einem beliebigen UIImage dessen Bytes als Array.
178
4.8 Und Bilder? Wie wär's mal mit Texturen! - (void) generateTexture: (GLubyte *) pixelData; Die Byte-Repräsentation können wir dann an diese Methode weiterreichen, die aus den Pixeldaten eine OpenGL-Textur macht. Da Texturen innerhalb eines reservierten Speicherbereichs von OpenGL liegen, liefert die Methode nichts zurück, sondern speichert nur die neue Textur-ID in der Member-Variablen textureID ab. Textur-IDs fangen in der Regel bei 1 an und werden dann für jede neue Textur hochgezählt.
Bevor wir an die Implementierung der Methoden gehen, sehen wir uns zunächst das Grundgerüst der Tex-Klasse an: Listing Tex.m
4 OpenGL ES – der Turbo-Gang [super dealloc]; } @end
In der dealloc-Methode haben wir bereits die glDeleteTextures()-Funktion implementiert. Über diese kann eine Textur dauerhaft aus dem OpenGL-Speicherbereich gelöscht werden – da auch mehrere Texturen gelöscht werden können, lautet der erste Parameter 1: Wir wollen nur eine Textur mit der ID textureID löschen. Die Implementierung der Einstiegsmethode createTexFromImage: stellt uns vor keine großen Herausforderungen: - (void) createTexFromImage: (NSString *) picName { UIImage *pic = [UIImage imageNamed: picName]; if (pic) { //Texturabmessungen festlegen width = pic.size.width; height = pic.size.height; if ( (width & (width-1)) != 0 || (height & (height-1)) != 0 || width > 2048 || height > 2048) { NSLog(@"ERROR: %@ width und/oder height ist keine 2er Potenz oder > 2048!", picName); } //Pixeldaten erzeugen GLubyte *pixelData = [self generatePixelDataFromImage: pic]; //Aus den Pixeldaten die Textur erzeugen und als ID speichern [self generateTexture: pixelData]; //Cleanup int memory = width*height*4; NSLog(@"%@-Pic-Textur erzeugt, Size: %i KB, ID: %i", picName, memory/1024, textureID); free(pixelData); [pic release]; } else { NSLog(@"ERROR: %@ nicht gefunden, Textur nicht erzeugt.", picName); } }
Anhand des übergebenen Strings erzeugen wir ein ganz normales UIImage. Außerdem prüfen wir, ob die Textur über gültige Abmessungen verfügt. Die if-Abfrage prüft mithilfe des &-Bit-Operators, ob es sich um eine Zweier-Potenz handelt. Die eigentliche Erzeugung der Textur erfolgt dann durch die beiden Aufrufe von: GLubyte *pixelData = [self generatePixelDataFromImage: pic]; [self generateTexture: pixelData];
180
4.8 Und Bilder? Wie wär's mal mit Texturen!
Danach räumen wir dann noch ein bisschen den Speicher auf und geben den maximalen Speicherverbrauch der Textur aus. Da wir keine PVRTC-Texturen verwenden, werden pro Bildpunkt vier Bytes benötigt (RGBA). Die größtmögliche Textur schlägt damit mit einem Speicherverbrauch von
2048 x 2048 * 4 = 16,384 MB zu Buche. Unsere 64er-Textur verschlingt dagegen gerade mal 16 KB. Sehen wir uns nun an, wie wir aus dem UIImage die Bytes herauslesen können. Wie bereits erwähnt, ist dies nicht die Aufgabe von OpenGL ES, sodass wir hier auf die Hilfe von Core Graphics zurückgreifen: - (GLubyte *) generatePixelDataFromImage: (UIImage *) pic { GLubyte *pixelData = (GLubyte *) calloc( width*height*4, sizeof(GLubyte) ); CGColorSpaceRef imageCS = CGImageGetColorSpace(pic.CGImage); CGContextRef gc = CGBitmapContextCreate( pixelData, width, height, 8, width*4, imageCS, kCGImageAlphaPremultipliedLast); CGContextDrawImage(gc, CGRectMake(0, 0, width, height), pic.CGImage); //Render pic auf gc CGColorSpaceRelease(imageCS); CGContextRelease(gc); return pixelData; }
Wie jede gute Grafik-API bietet auch Core Graphics die Möglichkeit, ein Bild offline zu rendern. Im Prinzip wenden wir also einen kleinen Trick an: Wir besorgen uns einen neuen Grafikkontext und rendern auf diesem unser Bild. Das war's schon, alles, was wir auf diesen Kontext gezeichnet haben, landet automatisch im dafür reservierten Speicherbereich, den wir dann nur noch zurückgeben müssen. Doch der Reihe nach: Zunächst reservieren wir uns über die calloc-Funktion Speicherplatz, der exakt der zuvor erwähnten Formel entspricht: w*h*4. Nun können wir auch schon einen neuen Bitmap-Kontext erzeugen. Core Graphics stellt dafür die Funktion CGContextRef CGBitmapContextCreate( void *data, size_t width, size_t height, size_t bitsPerComponent, size_t bytesPerRow, CGColorSpaceRef space, CGBitmapInfo bitmapInfo);
zur Verfügung. Alles, was auf diesen Kontext gerendert wird, wandert in den durch den Pointer data angegebenen Speicherbereich. Weiterhin übergeben wir noch die Abmessungen, legen 8 Bits
181
4 OpenGL ES – der Turbo-Gang
pro Komponente fest, geben mit der Konstante kCGImageAlphaPremultipliedLast ein Standard-RGBA-Verhalten vor (wir rendern ja nur ein Image) und geben schließlich noch den Farbraum des Bildes an die Funktion. Diesen haben wir uns über die CGImageGetColorSpace()-Funktion geholt, der wir die Core Graphics-Repräsentation des UIImages übergeben. Sobald der neue Grafikkontext steht, können wir auf diesem rendern, was das Zeug hält – nun ja, in unserem Fall reicht ein Aufruf von CGContextDrawImage(). Das Bild wird natürlich an die Position (0, 0) gezeichnet, da der Kontext der Größe des Bildes entspricht. Danach müssen wir nur noch etwas Speicher freigeben (für den Grafikkontext und den Farbraum) und können die Pixeldaten zurückliefern. Dieses Verfahren ermöglicht es uns, auch aus anderen Elementen eine Textur zu erzeugen, wie zum Beispiel Strings. Sie wissen ja, dass OpenGL ES von Haus aus keine Textausgabe unterstützt. Doch dazu später mehr. Um aus den Pixeldaten eine einsatzbereite Textur zu machen, müssen wir wieder auf OpenGL ES zurückgreifen: - (void) generateTexture: (GLubyte *) pixelData { glGenTextures(1, &textureID); glBindTexture(GL_TEXTURE_2D, textureID); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixelData); //Textur-bezogene States global aktivieren glEnable(GL_BLEND); glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); glEnableClientState(GL_TEXTURE_COORD_ARRAY); }
Bevor wir die Textur erzeugen, verankern und binden wir zunächst die textureID, setzen passende Parameter, die das Wickelverhalten der Textur steuern, und erzeugen die Textur schließlich über die glTExImage2D()-Funktion, der wir die Pixeldaten übergeben – zusammen mit anderen Parametern wie den Abmessungen und dem Farbmodell inklusive Alphakanal. Über die Texturparameter können Sie alternativ festlegen, das die Textur in s- bzw. t-Richtung gewrappt werden soll (GL_TEXTURE_WRAP_S, GL_TEXTURE_WRAP_T). Wir benötigen dieses Feature jedoch nicht: Die Texturkoordinaten sollen 1:1 auf die Vertices gemappt werden. Natürlich bietet OpenGL ES noch weitere Möglichkeiten, um das Texturverhalten zu steuern – mit den gezeigten Standardeinstellungen sollten Sie jedoch die häufigsten Anwendungsfälle abdecken können. Schließlich aktivieren wir noch die passende Blend-Funktion, die bestimmt, wie mit transparenten Flächen verfahren werden soll: Die Konstante GL_ONE_MINUS_SRC_ALPHA bewirkt, dass transparente Flächen entsprechend ihres Alphawertes mit anderen transparenten Flächen überblendet werden. Außerdem aktivieren wir über die Konstante GL_TEXTURE_ COORD_ARRAY dauerhaft die Nutzung des Texturkoordinaten-Arrays während des Render-
182
4.8 Und Bilder? Wie wär's mal mit Texturen!
Vorgangs – dadurch legen wir jedoch nicht fest, dass alle Flächen von nun an texturiert werden sollen, sondern ermöglichen lediglich die Nutzung eines solchen Arrays. Nach Beendigung der generateTexture:-Methode sollte, wenn alles geklappt hat, die textureID auf eine neue Textur verweisen, die wir sodann während des Render-Vorgangs nutzen können: - (void) drawAt: (CGPoint) p { GLshort imageVertices[ ] = { 0, height, //links unten width, height, //rechts unten 0, 0, //links oben width, 0 //rechts oben }; GLshort textureCoords[ ] = { 0, 1, //links unten 1, 1, //rechts unten 0, 0, //links oben 1, 0 //rechts oben }; glEnable(GL_TEXTURE_2D); //alle Flaechen werden nun texturiert glColor4f(1, 1, 1, 1); glBindTexture(GL_TEXTURE_2D, textureID); glVertexPointer(2, GL_SHORT, 0, imageVertices); glTexCoordPointer(2, GL_SHORT, 0, textureCoords); glPushMatrix(); glTranslatef(p.x, p.y, 0); glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); glPopMatrix(); glDisable(GL_TEXTURE_2D); }
In den beiden Arrays imageVertices und textureCoords speichern wir, wie bereits gezeigt, die Abmessungen des Original-Rechteckes, ausgerichtet am Ursprung (0, 0), und die Texturkoordinaten (gültig für alle Bilder). Danach wird dann über glEnable(GL_TEXTURE_2D) der Textur-Flag eingeschaltet. So lange, bis wir diesen nicht wieder ausschalten (am Ende der Methode), werden alle Flächen mit der aktuellen Textur überzogen, die wir über glBindTexture(GL_TEXTURE_2D, textureID);
aktivieren. Natürlich ist es möglich, pro Textur andere Texturkoordinaten zu verwenden, also verweisen wir im nächsten Schritt auf die beiden zuvor angelegten Arrays (imageVertices und textureCoords).
183
4 OpenGL ES – der Turbo-Gang
Danach können wir dann wie gewohnt in den Render-Prozess einsteigen und die Textur an dem gewünschten Punkt rendern, der sich stets auf die linke, obere Ecke des Bildes bezieht. Voilà, das wäre geschafft. Über Tex *tex = [[Tex alloc] init]; [tex createTexFromImage: @"player.png"]; [tex drawAt: CGPointMake(100, 100)];
können wir dann unsere erste Textur auf dem Bildschirm an der Position (100, 100) anzeigen.3 Wie versprochen wollen wir nun noch zeigen, wie Sie aus einem NSString eine Textur erzeugen, um Texte ausgeben zu können. Doch Achtung: Natürlich eignet sich dieses Verfahren nicht für besonders lange Texte oder etwa die Score-Anzeige: Für jeden String wird hierbei eine neue Textur erzeugt – wenn Sie die neue Textur nicht wieder löschen, wird der Speicherverbrauch schnell einen kritischen Bereich erreichen. Aber um einfache Meldungen wie zum Beispiel "Game Over" auszugeben, reicht diese Technik vollkommen aus. Sie können mit dieser Methode natürlich auch einen eigenen Bitmap-Font-Zeichensatz erzeugen, sodass Sie die Texturen wiederverwenden können: Um beispielsweise einen Punktestand anzuzeigen, brauchen Sie nur zehn Texturen, sodass Sie den Score aus den Zahlen 0 bis 9 zusammensetzen können. Damit wir die Tex-Klasse um die Unterstützung für Strings erweitern können, brauchen wir nur zwei neue Methoden hinzuzufügen: - (void) createTexFromString: (NSString *) text; - (GLubyte *) generatePixelDataFromString: (NSString *) text;
Die erste Methode ist ähnlich aufgebaut wie das zuvor gezeigte UIImage-Pendant, legt aber die Texturabmessungen selbstständig anhand der Textlänge fest. Wir unterstützen nur einzeilige Texte. Aufgrund der Beschränkungen der Texturgröße wird unsere Textur zwangsläufig größer als der dargestellte Text sein: - (void) createTexFromString: (NSString *) text { //Texturabmessungen festlegen int len = [text length]*20; if (len < 64) width = 64; else if (len < 128) width = 128; else if (len < 256) width = 256; else width = 512; //max width text height = 32; //Pixeldaten erzeugen GLubyte *pixelData = [self generatePixelDataFromString: text];
3 Anstatt die Vertices und Texturkoordinaten als Array zu übergeben, steht auch eine OES-Extension zur Verfügung, mit der 2DTexturen direkt gerendert werden können: glDrawTexiOES (GLint x, GLint y, GLint z, GLint width, GLint height)– hier als Beispiel die Integer-Variante. Die Funktion arbeitet pro Texel und bietet daher weniger Flexibilität hinsichtlich der Matrixoperationen.
184
4.8 Und Bilder? Wie wär's mal mit Texturen! //Aus den Pixeldaten die Textur erzeugen und als ID speichern [self generateTexture: pixelData]; //Cleanup int memory = width*height*4; NSLog(@"%@-Text-Textur erzeugt, Size: %i KB, ID: %i", text, memory/1024, textureID); free(pixelData); }
Sie sehen, die maximale Länge beträgt 512 Pixel. Außerdem legen wir die Texturhöhe mit 32 Pixeln fest, das heißt, wir unterstützen keine unterschiedlichen Font-Größen. Es sollte dennoch mit dem jetzigen Wissen für Sie möglich sein, bei Bedarf diese Funktionalität nachzuimplementieren. Je nach Font-Größe müssen Sie dabei aufgrund der ZweierPotenzen-Beschränkung unter Umständen verschenkten Platz in Kauf nehmen. In der zweiten neuen Methode rendern wir den String, wie zuvor das UIImage, auf einen Bitmap-Kontext: - (GLubyte *) generatePixelDataFromString: (NSString *) text { const char *cText = [text cStringUsingEncoding: NSASCIIStringEncoding]; GLubyte *pixelData = (GLubyte *) calloc( width*height*4, sizeof(GLubyte) ); CGColorSpaceRef rgbCS = CGColorSpaceCreateDeviceRGB(); CGContextRef gc = CGBitmapContextCreate( pixelData, width, height, 8, width*4, rgbCS, kCGImageAlphaPremultipliedLast); int size = 22; //Font-Groesse, kleiner als height CGContextSetRGBFillColor(gc, 0,1,0,1); //Schriftfarbe CGContextSelectFont(gc, "Verdana", size, kCGEncodingMacRoman); int ys = height-size; //swapped y-axis //Render text auf gc: CGContextShowTextAtPoint(gc, 0, ys, cText, strlen(cText)); CGColorSpaceRelease(rgbCS); CGContextRelease(gc); return pixelData; }
Da Core Graphics keine direkte Unterstützung für NSString-Objekte bietet, erzeugen wir zunächst eine char-Zeichenkette, die wir dann über die CGContextShowTextAtPoint()Funktion rendern können. Wie Sie sehen, geben wir der Einfachheit halber die Schriftgröße (22) und den Font (Verdana) vor, ebenso wie die Schriftfarbe (0,1,0,1 = Grün). Aber auch hier sollte es kein Problem für Sie darstellen, die Methode bei Bedarf diesbezüglich anzupassen. Achten Sie beim Positionieren der Grafik darauf, dass die y-Achse des Grafikkontextes nach oben zeigt (wir befinden uns schließlich nicht im OpenGLKoordinatensystem).
185
4 OpenGL ES – der Turbo-Gang
Wenn Sie einen HelloWorld-Text ausgeben wollen, können Sie das nun folgendermaßen tun: Tex *tex = [[Tex alloc] init]; [tex createTexFromString: @"Hallo Welt!"]; [tex drawAt: CGPointMake(100, 100)];
Allerdings, wie Sie vielleicht zu Recht bemerken, sind wir noch nicht ganz am Ziel: So ganz optimal ist das Handling der Texturen noch nicht. Meistens wollen wir einen Text oder ein Bild doch eher über eine Spielschleife ausgeben – und da wäre es unschön, wenn wir die Textur pro Durchlauf erneut erzeugen würden. Wir sollten daher abschließend eine saubere Ressourcenverwaltung einrichten. Praktischerweise können wir dabei auf die bisherige Hashtable-Lösung zurückgreifen. Erweitern Sie die GameManager-Klasse also um ein NSMutableDictionary, in dem wir die Texturen ablegen: - (NSMutableDictionary *) getDictionary { if (!dictionary) { //Hashtable dictionary = [[NSMutableDictionary alloc] init]; NSLog(@"Dictionary angelegt!"); } return dictionary; } - (void) removeFromDictionary: (NSString*) name { [[self getDictionary] removeObjectForKey: name]; }
Zur Erzeugung der Texturen verwenden wir eine weitere Hilfsmethode, die über den zusätzlichen Parameter imgFlag darüber entscheidet, ob ein Text oder ein Bild als Vorlage für die Textur verwendet werden soll: - (Tex *) getTex: (NSString*) name isImage: (bool) imgFlag { Tex *tex = [[self getDictionary] objectForKey: name]; if (!tex) { tex = [[Tex alloc] init]; if (imgFlag) { [tex createTexFromImage: name]; } else { [tex createTexFromString: name]; } [[self getDictionary] setObject: tex forKey: name]; NSLog(@"%@ als Tex im Dictionary abgelegt.", name); [tex release]; } return tex; }
Nun brauchen wir nur noch zwei passende Render-Methoden zu implementieren: - (void) drawOGLImg: (NSString*) picName at: (CGPoint) p { Tex *tex = [self getTex: picName isImage: YES]; if (tex) {
186
4.8 Und Bilder? Wie wär's mal mit Texturen! [tex drawAt: p]; } } - (void) drawOGLString: (NSString*) text at: (CGPoint) p { Tex *tex = [self getTex: text isImage: NO]; if (tex) { [tex drawAt: p]; } }
Um ein Bild zu rendern, genügt jetzt ein Aufruf von: [self drawOGLImg: @"player.png" at: CGPointMake(100, 100)];
Die drawOGLImg-Methode fragt die Hashtable ab, ob bereits eine Texturklasse mit dem Namen des Bildes existiert, sodass wir die Methode gefahrlos in einer Spielschleife einsetzen können. In manchen Situationen benötigen wir zusätzlich die Abmessungen des Bildes – dies können wir über die folgende Convenience-Methode erreichen: - (CGSize) getOGLImgDimension: (NSString*) picName { Tex *tex = [self getTex: picName isImage: YES]; if (tex) { return CGSizeMake([tex getWidth], [tex getHeight]); } return CGSizeMake(0, 0); }
Sehen wir uns ein praktisches Beispiel der neu entstandenen Methoden an: - (void) playGame { CGSize textureSize = [self getOGLImgDimension: @"player.png"]; int w = textureSize.width; int h = textureSize.height; static int yStep = 0; yStep -= 1; for (int x = 0; x < W; x += w) { for (int y = 0; y < H; y += h) { [self drawOGLImg: @"player.png" at: CGPointMake(x, y + yStep)]; } } [self drawOGLString: @"OpenGL ES rules!" at: CGPointMake(60, 100)]; }
187
4 OpenGL ES – der Turbo-Gang
Abb. 4.11: 100 Fighter-Texturen und eine Texttextur
Über zwei verschachtelte for-Schleifen geben wir 40-mal die player.png-Textur auf dem Bildschirm aus, die Raumschiffe fliegen langsam nach oben – darüber rendern wir dann noch eine Textmeldung als Beweis, dass alles so funktioniert, wie wir uns das vorgestellt haben. Na, das war doch gar nicht so schwer, oder? Nun sollten wir auch unter OpenGL ES in der Lage sein, ein Spiel aus beliebigen 2D-Grafiken zusammenzubasteln. Am Ende dieses Kapitels haben wir die Texturerzeugung und das Anzeigen einer Grafik auf eine einzige Methode reduziert, sodass Sie sich zukünftig nicht weiter mit den Feinheiten der Texturierung beschäftigen müssen.
4.9
Ab in die Matrix: die Transformationsfunktionen Bevor wir im nächsten Kapitel animierte Texturen mittels Clipping (über die Texturkoordinaten) erzeugen, widmen wir uns erneut der Fortbewegung bzw. der Transformation von (texturierten) Objekten. Keine Angst, dies wird ein ganz harmloses Kapitel, vor dem niemand Angst zu haben braucht. Versprochen. Die benötigten Funktionen zum Transformieren von Objekten haben wir ja bereits kennengelernt und die glTranslatef()-Funktion im Einsatz gesehen. Die Methode lässt sich auch für Texturen verwenden: - (void) translate { static int x = 160-32; static int y = 480-32; y -= 1; glPushMatrix(); glTranslatef(x, y, 0);
188
//1. //2.
4.9 Ab in die Matrix: die Transformationsfunktionen [self drawOGLImg: @"player.png" at: CGPointMake(0, 0)]; //3. glPopMatrix(); //4. }
Diesen Code-Ausschnitt finden Sie auch im Beispielprojekt zu diesem Kapitel: "OGL_Movement". Das Grundprinzip für die Transformation von Objekten – dem Verschieben, Verzerren oder Rotieren von Eckpunkten – ist stets gleich: 1.
Eine Kopie des aktuellen Matrix-Stacks holen.
2.
Auf der Kopie des aktuellen Matrix-Stacks die Operation durchführen.
3.
Rendern.
4.
Die alte Matrix reaktivieren.
Diese Schritte haben wir bereits ausführlich besprochen. Sie sehen, dies klappt auch im Zusammenspiel mit der neuen drawOGLImg-Methode. Achten Sie darauf, dass wir für dieses Beispiel das Image an der Position (0, 0) rendern. Im Ergebnis wird das Raumschiff nach oben bewegt (y -= 1). Rotationen lassen sich ebenso einfach bewerkstelligen: - (void) rotate { static int a = 0; a -= 10; glPushMatrix(); glTranslatef(160, 240, 0); //Mittig platzieren glRotatef(a, 0, 0, 1); [self drawOGLImg: @"player.png" at: CGPointMake(0, 0)]; glPopMatrix(); }
Der erste Parameter gibt den Rotationswinkel in Grad an – da wir diesen kontinuierlich hochzählen, ergibt sich eine Kreisbewegung um den Koordinatenursprung. Das Koordinatensystem haben wir per glTranslatef() in die Screen-Mitte verschoben (160, 240), dadurch dreht sich auch das Objekt um die Screen-Mitte. Um das Objekt stattdessen um sich selbst zu drehen, müssten wir dieses mittig auf dem Nullpunkt positionieren. Dies werden wir im nächsten Kapitel zeigen. Nach dem Rotationswinkel wird angegeben, um welche Achse (x-y-z) gedreht werden soll: Für ein 2D-System macht nur die z-Achse Sinn (0-0-1), die senkrecht vom Betrachter fort zeigt. Für 3D-Systeme können Sie auch um mehrere Achsen gleichzeitig drehen. Eine Rotation um alle drei Achsen würden Sie so erzeugen: glRotatef(a, 1, 1, 1);
Doch aufgepasst: Die Reihenfolge der Transformationen spielt für die Matrizenberechnung eine große Rolle. Ob Sie ein Modell erst rotieren und dann verschieben oder erst verschieben und dann rotieren, führt zu unterschiedlichen Ergebnissen. Als Faustregel kann man sich merken, dass die letzte Transformation zuerst ausgeführt wird, danach die anderen – bevor das Array schließlich in den Renderbuffer geschoben wird. Dies bedeutet, dass wir im obigen Beispiel eigentlich zuerst das Objekt um (0, 0) rotieren und dann erst verschieben. Im Quelltext müssen Sie natürlich die umgekehrte Reihenfolge beibehalten.
189
4 OpenGL ES – der Turbo-Gang
Die dritte und letzte Transformationsfunktion erlaubt uns die Skalierung des Objektes: - (void) scale { static float s = 1; static int sf = 1; //Vorzeichen s -= 0.01 * sf; if (s < 0 || s > 1) { sf *= -1; } glPushMatrix(); glTranslatef(160, 240, 0); glScalef(s, s, 1); [self drawOGLImg: @"player.png" at: CGPointMake(0, 0)]; glPopMatrix(); }
Die Größe der Skalierung wird pro Achse (x-y-z) angegeben: 1 bedeutet: Keine Skalierung (Originalgröße) 0 < 1 bedeutet: Verkleinern > 1 bedeutet: Vergrößern
Um die Grafik proportional in xy-Ausdehnung zu skalieren, müssen also die ersten beiden Parameter gleich sein. Eine Ausdehnung in Richtung der z-Achse macht für ein 2D-Spiel keinen Sinn. Im oben gezeigten Code oszilliert der s-Wert zwischen 0 und 1. Da die Grafik stets am Screen-Mittelpunkt positioniert wird, liegt die Grafik bei einer Ausdehnung von 0 direkt auf der Screen-Mitte. Um die Grafik zentriert über ihrem Mittelpunkt zu skalieren, müssten Sie die Grafik zuvor verschieben. Für 3D-Spiele ergibt die Skalierung nur selten sinnvolle Resultate, da die optische Wirkung dem Verschieben in Richtung der z-Achse ähnelt – für 2D-Spiele kann die Skalierung jedoch zu interessanten Effekten führen und dem Spiel den Eindruck räumlicher Tiefe verleihen.
Abb. 4.12: Bewegungsarten
190
4.10 Animationen mit Textur-Clipping
Natürlich können Sie auch alle drei Transformationsfunktionen gemeinsam einsetzen. Ein Beispiel: - (void) allTogether { static int x = 160; static int y = 480; static int a = 0; static float s = 1; static int sf = 1; //Vorzeichen y -= 1; a -= 10; s -= 0.01 * sf; if (s < 0 || s > 1) { sf *= -1; } glPushMatrix(); glTranslatef(x, y, 0); glRotatef(a, 0, 0, 1); glScalef(s, s, 1); [self drawOGLImg: @"player.png" at: CGPointMake(0, 0)]; glPopMatrix(); }
Achten Sie dabei unbedingt auf die Reihenfolge von Translation und Rotation, da sonst unerwartete Ergebnisse die Folge sind. Wir werden zukünftig Translation und Rotation innerhalb der Tex-Klasse anwenden und uns so wieder den bereits bekannten Spielmechanismen nähern, die wir in den ersten Kapiteln dieses Buches kennengelernt haben – die Positionierung der Texturen am Ursprung (0, 0) wurde in diesem Kapitel lediglich zur besseren Veranschaulichung der Beispiele vorgenommen.
4.10
Animationen mit Textur-Clipping Sowohl Skalierung als auch Rotation werden meist objektzentriert eingesetzt, damit ein realistischer Eindruck entsteht. Am Beispiel der Rotation werden wir dies nun innerhalb der Tex-Klasse einbauen. Weiterhin werden wir in einem späteren Spieleprojekt zeigen, wie sich Sprite-Objekte auf Kreisbahnen fortbewegen lassen – dies erfolgt dann typischerweise außerhalb der Tex-Klasse. Da wir Sprites nicht nur rotieren, sondern auch das Prinzip des Textur-Clippings vorstellen wollen, legen wir eine neue Methode in der Tex-Klasse an (das fertige Projekt lautet: "OGL_Animation"): - (void) drawFrame: frameWidth: angle: at:
(int) frameNr (int) fw (int) degrees (CGPoint) p;
191
4 OpenGL ES – der Turbo-Gang
Als Basis für die Animation dient uns wieder ein Filmstreifen-PNG, in dem die verschiedenen Animationsphasen nebeneinander angeordnet sind.
Abb. 4.13: octo_8f.png mit Frame-Nummern und s-Texturkoordinaten
Die Methode erwartet die aktuelle Frame-Nummer des Filmstreifens – bei acht Frames hat das erste Frame die Nummer 0 und das letzte die Nummer 7. Weiterhin übergeben wir die Frame-Breite in Pixeln. Für ein PNG mit einer Breite von 128 Pixeln und zwei Frames beträgt die Frame-Breite 64 Pixel. Außerdem übergeben wir noch den gewünschten Rotationswinkel in Grad. Bei 0 Grad würde die Textur ungedreht gerendert werden, für 90 Grad würde Sie nach rechts zeigen und so weiter. Schließlich geben wir noch den Punkt an, an dem die obere, linke Ecke des Frames gezeichnet werden soll. Anders als bei den bisherigen Core Graphics-Beispielen erledigen die Texturkoordinaten bereits für uns das Clipping – wir brauchen keine dedizierte Funktion dafür. Das TexturClipping basiert darauf, dass Sie den Filmstreifen horizontal in gleichmäßige Abschnitte unterteilen. Da alle Frames die gleiche Höhe haben, brauchen wir dabei pro Frame nur die s-Koordinaten (entlang der x-Achse) anzupassen. Die tatsächliche Breite des Filmstreifens spielt dabei keine Rolle. Unser Filmstreifen besteht aus acht Frames. Die Textur-Frame-Breite beträgt damit 1/8. Daraus ergibt sich:
Frame 0: Von x1 = 0 bis x2 = 1/8 Frame 1: Von x1 = 1/8*1 bis x2 = 1/8*2 Frame 2: Von x1 = 1/8*2 bis x2 = 1/8*3 Frame 3: Von x1 = 1/8*3 bis x2 = 1/8*4 usw. Diesen Zusammenhang können wir natürlich leicht in eine Formel gießen: GLfloat txW = 1.0/(width/fw); //Textur-Breite GLfloat x1 = frameNr*txW; GLfloat x2 = x2 = x1 + txW; //oder: x2 = (frameNr+1)*txW;
Damit lauten die Texturkoordinaten des aktuellen Frames: GLfloat textureCoords[ ] = { x1, 1, //links unten
192
4.10 Animationen mit Textur-Clipping x2, x1, x2,
1, //rechts unten 0, //links oben 0 //rechts oben
};
Anhand der Texturkoordinaten können wir also direkt bestimmen, welcher Teil des Filmstreifens auf die rechteckige Grundfläche gemappt werden soll. Mit diesem Wissen können wir nun die Methode implementieren: - (void) drawFrame: frameWidth: angle: at:
(int) frameNr (int) fw (int) degrees (CGPoint) p {
Die Umsetzung der Methode unterscheidet sich, abgesehen von den geänderten Texturkoordinaten, nur bezüglich der Rotation von der drawAt:-Methode: glTranslatef(p.x+fw/2, p.y+height/2, 0);
193
4 OpenGL ES – der Turbo-Gang glRotatef(degrees, 0, 0, 1); //angle = 0 = keine Rotation glTranslatef(0-fw/2, 0-height/2, 0);
Wie zuvor bereits erwähnt, soll die Grafik um ihren eigenen Mittelpunkt herum gedreht werden. Dieser befindet sich am Punkt: (p.x+fw/2, p.y+height/2, 0)
Um die Rotation nun wie gewünscht durchzuführen, müssen wir das System zweimal verschieben. Erinnern Sie sich dabei an die umgekehrte Reihenfolge bei der Ausführung der Matrixberechnung: 1. Verschieben an den Mittelpunkt der Grafik: glTranslatef(0-fw/2, 0-height/2, 0);
2. Die Rotation erfolgt nun um Koordinatenursprung herum, die Grafik wird um sich selbst gedreht, da Koordinatenursprung und Grafik-Mittelpunkt identisch sind: glRotatef(degrees, 0, 0, 1);
3. Das Objekt wird an die Zielposition verschoben, die erste Translation wird rückgängig gemacht, und die Grafik wird wieder an der linken, oberen Ecke ausgerichtet: glTranslatef(p.x+fw/2, p.y+height/2, 0);
Abb. 4.14: Drehung um die eigene Achse und diagonale Fortbewegung nach rechts unten
Nun, um unsere neue Methode in Aktion zu zeigen, müssen wir natürlich noch dafür sorgen, dass die jeweiligen Frame-Nummern in passenden Abständen an die Methode übergeben werden. Später werden wir dies in die Sprite-Klasse verlagern. Damit wir den Ablauf schon jetzt testen können, legen wir eine neue Tex-Member-Variable im GameManager an, die wir in der preloader-Methode initialisieren: Tex *octoTexture; ... octoTexture = [self getTex: @"octo_8f.png" isImage: YES];
Das Bestimmen der Frame-Nummer erledigen wir nach altbewährtem Muster über eine if-Abfrage und einen Modulo-Operator, der für die Abstände sorgt: - (void) playGame { timer++; static static static static
194
int int int int
frameNr = 0; frameW = 64; angle = 0; x = 0;
4.11 Unendliche Weiten: Scrolling und Parallax-Scrolling static int y = 0; if (timer % 3 == 0) { frameNr ++; if (frameNr > 7) { frameNr = 0; } } angle++; x++; y++; [octoTexture drawFrame: frameWidth: angle: at:
frameNr frameW angle CGPointMake(x, y)];
}
Die timer-Variable entspricht der aktuellen Anzahl an Durchläufen der Spielschleife. Sie sehen, es werden daneben noch weitere Variablen benötigt, sodass sich die Verlagerung in eine eigene Klasse geradezu aufdrängt. Im übernächsten Kapitel werden wir dies anhand eines konkreten Spielprojektes demonstrieren.
4.11
Unendliche Weiten: Scrolling und Parallax-Scrolling Die Level eines Spiels können aus einzelnen Hintergrundbildern zusammengesetzt sein (den sogenannten Tiles) und/oder aus mehreren Ebenen bestehen, die übereinandergelegt werden und in Abhängigkeit der Bewegung des Spielers gegeneinander verschoben werden können – um so den Eindruck von Tiefe zu vermitteln ("Parallax-Scrolling"). Der Mechanismus des Scrollings lässt sich gut aus der Vogelperspektive heraus erläutern: Die Spielfigur scheint scheinbar in der Spielmitte festzuhängen – da sich aber der Hintergrund bewegt, entsteht der Eindruck, dass sich die Figur vorwärts bewegt. Der Trick besteht nun darin, dass sich die Figur tatsächlich vorwärts bewegt und die gesamte Spielwelt in Abhängigkeit vom Spieler verschoben wird, sodass der Spieler stets zentriert in der Spielmitte erscheint. Bevor wir dieses Prinzip demonstrieren können, brauchen wir einen Hintergrund, auf dem sich die Spielfigur fortbewegen kann. Als Spielfigur wählen wir dabei wieder das Raumschiff aus den letzten Kapiteln.
195
4 OpenGL ES – der Turbo-Gang
Abb. 4.15: background.png, clouds.png
Der Hintergrund für das Raumschiff soll aus zwei übereinandergelegten Texturen bestehen, die parallax gegeneinander verschoben werden. Die Wolkengrafik clouds.png ist halbtransparent, sodass die background.png-Grafik darunter trotzdem noch zu sehen ist. Damit die Spielfläche vollständig mit beiden Grafiken bekachelt werden kann, muss die Grafik oben, unten, links und rechts anschlussfähig zu sich selbst sein. Kacheln können Sie aus jeder Vorlage erzeugen, zum Beispiel mit dem Pattern-Generator von Photoshop. Bevor wir nun zum eigentlichen Scrolling kommen, sehen wir uns erst einmal an, wie man die Hintergründe relativ zur Spielerposition verschieben kann (das fertige Projekt finden Sie übrigens wie immer unter dem Namen "OGL_Parallax" im Download-Ordner). Das Ziel ist dabei zunächst, eine beliebige Hintergrundgrafik flächendeckend auf das Display zu bringen und anschließend zu verschieben. Da wir beliebig viele Parallax-Ebenen übereinander schichten können, legen wir hierfür am besten eine neue Klasse an (auch wenn wir unser Beispiel nur auf zwei Hintergrundebenen beschränken): Listing ParallaxLayer.h #import "Tex.h" @interface ParallaxLayer : NSObject { //Layer-Textur Tex *tex; //Abmessungen des Layers int layerW; int layerH; //Referenzpunkt der Parallaxebene, relativ zur Spielerposition float refX; float refY; //Vorherige Spielerposition float oldPx; float oldPy; }
196
4.11 Unendliche Weiten: Scrolling und Parallax-Scrolling
Da die Hintergrundgrafik als Textur gerendert wird, müssen wir natürlich auch unsere TexKlasse importieren. Bevor wir zu den Membern kommen, sehen wir uns zunächst die Methodensignaturen an. Die initWithPic:-Methode dürfte klar sein: Hier erzeugen wir eine neue Layer-Instanz. Angezeigt wird der Parallax-Layer dann über die Methode: - (void) drawWithFactor: (float) factor relativeTo: (CGPoint) pos atOrigin: (CGPoint) o;
Die Parameter: (float) factor: Der Faktor bestimmt, wie schnell die Ebene im Verhältnis zum Spieler bewegt werden soll. Ein Faktor von 1 würde die Ebene mit der gleichen Geschwindigkeit wie den Player bewegen, nur in der entgegengesetzten Richtung (schließlich soll die Ebene ja die Vorwärtsbewegung des Spielers veranschaulichen). Ein Faktor von 2 würde hingegen die Ebene halb so schnell bewegen – je größer der Faktor, desto langsamer wird die Ebene bewegt. Da sich der Spieler bewegt und nicht der Boden unter seinen Füßen, wird die Parallax-Ebene niemals schneller fortbewegt als der Spieler. (CGPoint) pos: Hierüber teilen wir der Methode die aktuelle Position des Spielers mit. (CGPoint) o: Solange wir die Spielwelt nicht scrollen, können wir hier den Koordinatenursprung übergeben (0, 0). Dies ändert sich, wenn wir später den ScrollingMechanismus hinzufügen. Warum das so ist, werden wir noch zeigen.
So viel erst mal zum Aufbau der Klasse – machen wir nun ein paar theoretische Überlegungen, wie wir die Methode mit Leben füllen können.
Abb. 4.16: Parallax-Scrolling
197
4 OpenGL ES – der Turbo-Gang
Um das Display mit Kacheln zu füllen, wählen wir am besten einen Referenzpunkt, von dem ausgehend wir die umgebenden Kacheln rendern können – so lange, bis der Screen komplett bedeckt ist. Um alle Ebenen relativ zum Spieler zu verschieben, brauchen wir also nur einen einzigen Punkt zu verschieben, der – da ja die Kacheln nahtlos aneinander liegen – innerhalb eines Bereiches verschoben wird, der exakt der Kachelgröße entspricht. Die Koordinaten dieses Referenzpunktes (refX, refY) ergeben sich aus der Fortbewegung des Spielers in xy-Richtung. Diese können wir ermitteln, wenn wir die aktuelle Position (pos.x, pos.y) mit der vorherigen vergleichen (oldPx, oldPy). Nun müssen wir nur noch auf den Bereich achten, innerhalb dessen der Referenzpunkt verschoben wird: Sobald der Punkt eine Seitengrenze erreicht, wird er auf die entgegengesetzte Grenze verschoben. Dadurch sollten wir eine nahtlos fließende Bewegung erhalten, da der zulässige Bereich exakt der Kachelgröße entspricht. Lassen Sie uns dies anhand der Implementierung überprüfen: Listing ParallaxLayer.m #import "ParallaxLayer.h" #import "GameManager.h" @implementation ParallaxLayer - (id) initWithPic: (NSString *) picName { self = [super init]; tex = [[GameManager getInstance] getTex: picName isImage: YES]; layerW = [tex getWidth]; layerH = [tex getHeight]; refX = 0; refY = 0; oldPx = 0; oldPy = 0; return self; } - (void) drawWithFactor: (float) factor relativeTo: (CGPoint) pos atOrigin: (CGPoint) o { //Positionsaenderung des Spielers //gegenueber dem vorherigen Frame ermitteln float px = pos.x; float py = pos.y; float diffX = px - oldPx; float diffY = py - oldPy; oldPx = px; oldPy = py;
198
4.11 Unendliche Weiten: Scrolling und Parallax-Scrolling //Parallaxebene relativ zum Spieler verschieben //factor = 1 -> speed = actor //factor = 2 -> speed = actor/2 (halb so schnell) //usw. refX -= diffX/factor; refY -= diffY/factor; //Innerhalb dieser Grenzen wird der Referenzpunkt verschoben if (refX > layerW) refX = 0; if (refX < 0) refX = layerW; if (refY > layerH) refY = 0; if (refY < 0) refY = layerH; //Viewport vollstaendig mit Layer bekacheln, ausgehend vom Referenzpunkt for (float x = o.x + refX-layerW; x < o.x + W; x+=layerW) { for (float y = o.y + refY-layerH; y < o.y + H; y+=layerH) { [tex drawAt: CGPointMake(x, y)]; } } } @end
In der initWithPic:-Methode legen wir die Textur an und initialisieren die Member. Die Implementierung der drawWithFactor:relativeTo:atOrigin:-Methode folgt dem zuvor beschriebenen Verfahren. In zwei verschachtelten for-Schleifen bekacheln wir dann schließlich das Display mit der Textur. Sie sehen, dass sich die xy-Koordinaten, an denen die Texturen gerendert werden, auf den Referenzpunkt und den Origin beziehen. Die Bekachelung endet, wenn die Screen-Breite bzw. Screen-Höhe erreicht ist. Um die Klasse im Einsatz zu zeigen, legen wir im GameManager eine neue Player-Textur und die beiden Parallax-Layer an: //Player Tex *playerTexture; int playerX; int playerY; //Parallax-Layer ParallaxLayer *back; ParallaxLayer *clouds; ... //Player-Textur playerTexture = [self getTex: @"player.png" isImage: YES]; int playerW = [playerTexture getWidth]; int playerH = [playerTexture getHeight]; //Spieler zentrieren
199
4 OpenGL ES – der Turbo-Gang playerX = W/2 - playerW/2; playerY = H/2 - playerH/2; //Parallax-Layer back = [[ParallaxLayer alloc] initWithPic: @"background.png"]; clouds = [[ParallaxLayer alloc] initWithPic: @"clouds.png"];
Die Player-Textur wird initial mittig auf den Screen platziert. In der Spielschleife (playGame) des Managers bewegen wir den Player dann nach oben und rendern zunächst einmal einen Hintergrund: //Player nach oben bewegen playerX += 0; playerY -= 1; //Background rendern [back drawWithFactor: playerY relativeTo: CGPointMake(playerX, playerY) atOrigin: CGPointMake(0, 0)]; //Player rendern [playerTexture drawAt: CGPointMake(playerX, playerY)];
Was wird passieren? Richtig, das Player-Raumschiff bewegt sich konstant mit einem Pixel pro Frame nach oben – die Hintergrund-Kachel bewegt sich aber nicht vom Fleck. Als Faktor haben wir nämlich die aktuelle y-Koordinate des Players playerY übergeben. Noch ein Beispiel: Um die Hintergrundtextur gleichzeitig zusammen mit dem Player fortzubewegen, müssten wir als Faktor –1 übergeben, da sich der Player ebenfalls um diesen Wert fortbewegt. Nun aber zum realistischen Anwendungsfall: //Player nach oben bewegen playerX += 0; playerY -= 1; //Parallax-Ebenen rendern [back drawWithFactor: 2 relativeTo: CGPointMake(playerX, playerY) atOrigin: CGPointMake(0, 0)]; [clouds drawWithFactor: 1 relativeTo: CGPointMake(playerX, playerY) atOrigin: CGPointMake(0, 0)];
200
4.11 Unendliche Weiten: Scrolling und Parallax-Scrolling
Abb. 4.17: Die beiden Parallax-Ebenen werden nach unten verschoben, der Player fliegt nach oben.
Der Player wird wieder konstant nach oben bewegt. Die beiden Parallax-Ebenen back und clouds werden entgegengesetzt dazu bewegt: back bewegt sich halb so schnell wie der Player nach unten (Faktor = 2) und wirkt dadurch so, als ob die Ebene etwas tiefer liegt. clouds wird mit der Geschwindigkeit des Players nach unten bewegt (Faktor = 1), dadurch wirkt die Ebene so, als ob sie über der back-Ebene liegt, aber immer noch unterhalb des Players.
Das korrekte Verhalten können Sie testweise auch mit anderen Werten für playerX und testen – die beiden Parallax-Ebenen passen ihre Scrollgeschwindigkeit der Richtung des Players (entgegengesetzt) und dessen Geschwindigkeit an.
playerY
Dass sich der Player nach oben fortbewegt, während die beiden Layer nach unten schweben, ist zwar physikalisch stimmig, doch als Spieler wollen Sie ja auch wissen, wohin die Reise geht. Kurz, wir müssen dafür sorgen, dass sich der Player in der Screen-Mitte befindet. Und hier kommt nun endlich die Scrolling-Technik zum Einsatz. Sie können sich das so vorstellen, als ob eine virtuelle Kamera den Spieler von oben filmt. Das Kamerabild sehen wir dann auf dem Display – und die Strecke, die der Spieler (und die Kamera) dabei zurücklegt, können wir an der vorbeifliegenden Landschaft erkennen (unseren beiden Parallax-Ebenen).
201
4 OpenGL ES – der Turbo-Gang
Abb. 4.18: Scrolling
Tatsächlich ist die Technik aber wesentlich einfacher zu realisieren. Wir benötigen keine virtuelle Kamera oder Ähnliches, stattdessen müssen wir – wieder einmal – nur das Koordinatensystem verschieben. Die Verschiebung findet nun dauerhaft statt, deshalb speichern wir die aktuelle Translation in den beiden Membern xt und yt: float xt; float yt;
Außerdem fügen wir einen Getter hinzu, der uns stets den Screen-Origin zurückliefert: - (CGPoint) getViewportOrigin { return CGPointMake(-xt, -yt); }
Da die gesamte Spielwelt an die Position (xt, yt) verschoben wird, hat der Punkt links oben auf dem Display die entgegengesetzten Koordinaten (-xt, -yt). Diesen sich verändernden Origin müssen wir nun als Bezugspunkt an die Parallax-Ebenen weiterreichen. That's it. Das Scrolling erledigt die neue scrollWorld-Methode für uns. Diese rufen wir in der Spielschleife auf, bevor die restlichen Render-Vorgänge erfolgen: - (void) playGame { [self scrollWorld]; //Player nach oben bewegen playerX += 0; playerY -= 1; //Parallax-Ebenen rendern [back drawWithFactor: 2 relativeTo: CGPointMake(playerX, playerY) atOrigin: [self getViewportOrigin]]; [clouds drawWithFactor: 1 relativeTo: CGPointMake(playerX, playerY) atOrigin: [self getViewportOrigin]];
202
4.11 Unendliche Weiten: Scrolling und Parallax-Scrolling
Abb. 4.19: Die beiden Ebenen bewegen sich parallax verschoben nach unten, der Player verbleibt in der Screen-Mitte. Die y-Koordinaten des Players verdeutlichen die Vorwärtsbewegung (aus Sicht des Players).
Sie sehen, im Ergebnis funktioniert nun alles wie geplant: Die Koordinaten des Players ändern sich kontinuierlich. Ausgehend von seiner (mittigen) Position wird die Spielwelt verschoben – und das Player-Raumschiff verbleibt in der Screen-Mitte, während die Parallax-Ebenen die Flugrichtung veranschaulichen. Da sich die scrollWorld-Methode auf die gesamte Spielwelt auswirken soll, laden wir mit glLoadIdentity() die Einheitsmatrix, sodass sich für jedes neue Frame die Welt im Ursprungszustand befindet und der Koordinatenursprung zunächst wieder bei (0, 0) in der linken, oberen Ecke liegt. Nach dem Laden der Einheitsmatrix verschieben wir das System in Abhängigkeit von der aktuellen Spielerposition. Diese Translation bleibt dann für die nachfolgenden Render-Vorgänge erhalten, sodass ein weiteres Spielobjekt an der Position (4200, 6400) auch tatsächlich an dieser Position
203
4 OpenGL ES – der Turbo-Gang
gerendert wird. Befindet sich der Spieler zufällig in der Nähe, so ist dieses Spielobjekt auf dem Screen zu sehen, ansonsten nicht. An der scrollWorld-Methode können Sie außerdem erkennen, wie die aktuelle Translation pro Frame aus den Spielerkoordinaten hergeleitet wird: xt = W/2 - playerW/2 - playerX; yt = H/2 - playerH/2 - playerY;
Die aktuelle Position des Spielers wird von der Screen-Mitte subtrahiert, wobei sich die Screen-Mitte hier auf die ungescrollte Position bezieht, an der der Player gerendert werden müsste, um zentriert auf dem Bildschirm zu erscheinen. Der eigentliche Scrolling-Mechanismus besteht lediglich aus sechs Zeilen Code und ermöglicht uns als weiteren Vorteil die Beibehaltung der ursprünglichen Koordinaten aller Spielobjekte. Dies macht die Entwicklung von Spielen für uns natürlich einfacher, da wir uns unter anderem beim Entwerfen von Animationspfaden für unsere Sprites nicht darum zu kümmern brauchen, ob und wie das Spielfeld gescrollt wird.
4.12
Lassen wir's krachen: ein OpenGL ES-Shooter Unseren OpenGL ES-Baukasten haben wir nun mit mächtigen Zutaten angefüllt. Lassen Sie uns jetzt mit einem neuen Spieleprojekt durchstarten und das Erlernte mit den zuvor erarbeiteten Basistechniken der Spieleentwicklung (Sprite-Management, Input-Handling, Kollisionskontrolle usw.) verbinden. Wir haben ja bereits gesehen, wie man Sprites um sich selbst rotieren kann – verknüpfen wir dies doch mit dem Scrolling-Mechanismus: Das Player-Raumschiff aus dem letzten Kapitel soll sich in alle Richtungen drehen können, verbleibt aber trotzdem in der Mitte des Screens und kann so vorwärtsfliegend die gesamte Spielwelt 360 Grad erkunden. Für diese Spielmechanik gibt es einige berühmte Vorbilder. Das erste Spiel dieser Art dürfte der Arcade-Automat Bosconian (Namco, 1981) gewesen sein, gefolgt von Time Pilot (Konami, 1982) und dessen hervorragendem Nachfolger Time Pilot '84 (Konami, 1984). Ebenfalls erwähnenswert sind Sinistar (Williams, 1982) und Vanguard II (SNK, 1984). Das eher unbekannte Arcade-Spiel Cerberus (Cinematronics, 1985) wandelt das Steuerungsprinzip auf ungewöhnliche Weise ab und geht mehr in Richtung Asteroids (Atari, 1979) und Spacewar! (MIT, 1961).
Abb. 4.20: Bosconian (Namco), Time Pilot (Konami) und Time Pilot '84 (Konami)
204
4.12 Lassen wir's krachen: ein OpenGL ES-Shooter
Aufgrund der in den 80ern üblichen 8-Wege-Joysticks sind wir natürlich herausgefordert, das Steuerungskonzept für das neue Jahrtausend und die Touch-Steuerung des iPhones zu adaptieren. Wir teilen dazu den Bildschirm in zwei Hälften: Berührt der Spieler die linke Hälfte, dreht sich das Raumschiff links herum, berührt er die rechte Hälfte, dann dreht sich das Raumschiff rechts herum. Lässt der Spieler den Bildschirm los, fliegt das Raumschiff entsprechend dem zuletzt eingestellten Winkel weiter geradeaus (aus Sicht des Piloten).
Abb. 4.20: Das Steuerungskonzept
Das Spielkonzept ist genretypisch:
Als Pilot müssen Sie versuchen, möglichst lange am Leben zu bleiben. Ihr Schiff verfügt dabei über unbegrenztes Dauerfeuer. Jede Berührung mit dem Feind endet tödlich. Steuern Sie nach links oder nach rechts, und erledigen Sie möglichst viele Gegner. Folgende Gegner bevölkern die Spielwelt: Minen: Unbewegt, unzerstörbar, rotierend um sich selbst. Fighter: Einzeln oder in Schwärmen unterwegs, bewegt sich wie der Spieler ebenfalls 360 Grad. Octo: Mechanischer Oktopus, eine Art Drohne, auch der Greifer genannt. Den tödlichen Tentakeln gilt es auszuweichen.
205
4 OpenGL ES – der Turbo-Gang
Abb. 4.21: Die Grafiken des Spiels auf einen Blick
Sie finden das fertige Spielprojekt bereits im Download-Ordner unter dem Namen "OGL_Shooter". Während wir das Spiel exemplarisch zusammenbauen, kann es nicht schaden, wenn Sie sich parallel dazu den finalen Source-Code ansehen und die einzelnen Bausteine im Zusammenhang betrachten. Wir haben versucht, den Aufbau des Spiels so einfach und übersichtlich wie möglich zu gestalten. Grundgerüst vervollständigen
Als Basis wählen wir das Projekt "OGL_Parallax" aus dem letzten Kapitel, sodass unser Spiel bereits über die folgenden (unveränderten) Klassen verfügt: MyGameAppDelegate: Initialisiert den MainView, triggert die Spielschleife. MainView: Initialisiert den GameManager, reicht den Touch-Input weiter an den GameManager, initialisiert OpenGL ES und ruft periodisch den GameManager auf. GameManager: Verwaltet die Ressourcen, steuert und rendert das Spiel. Tex: Texturen erzeugen und rendern. ParallaxLayer: Parallax verschobene Spielhintergründe.
206
4.12 Lassen wir's krachen: ein OpenGL ES-Shooter
Bis auf den GameManager müssen wir an den Klassen keine Änderungen mehr vornehmen. Um das Spiel zu komplettieren, legen wir im Ordner "GameElements" acht neue Klassen an, wobei die Sprite-Klasse die Elternklasse bildet und die sieben anderen Klassen direkt oder indirekt davon abgeleitet sind: Sprite: Elternklasse aller Sprites Player: Der Spieler, abgeleitet von Sprite Octo: Animierter Basisfeind, abgeleitet von Sprite Mine: Eine Mine, abgeleitet von Sprite Fighter: Gegner, abgeleitet von Octo Bullet: Die Munition des Spielers, abgeleitet von Sprite Gear: Antriebsgrafik des Spielers, abgeleitet von Bullet Animation: Animationsklasse – wird verwendet zur Darstellung einer Explosionssequenz, abgeleitet von Sprite
Sie sehen, die Vererbungsverhältnisse sind recht übersichtlich. Die Animationsklasse ist identisch zum Zombiespiel, nur dass hier Texturen statt eines UIImages eingesetzt werden. Bevor jedoch genauer auf die Implementierung der Sprite-Klassen eingehen, müssen wir das Grundgerüst der GameManager-Klasse vervollständigen. Zur Ressourcenverwaltung setzen wir neben dem NSDictionary wieder unsere SpriteArrays ein. Zusätzlich legen wir den Player ebenfalls als Member im GameManager.h an: Player *player; NSMutableArray *sprites; //Aktive Sprites NSMutableArray *newSprites; //Neue Sprites NSMutableArray *destroyableSprites; //Inaktive Sprites NSMutableDictionary* dictionary; //Ressourcen-Hashtable
Außerdem legen wir im Header die folgenden Game-States fest: enum states { LOAD_GAME, START_GAME, PLAY_GAME, GAME_OVER };
In der preloader-Methode können wir dann die Ressourcenbehälter einmalig anlegen: - (void) preloader { sprites = [[NSMutableArray alloc] initWithCapacity:20]; newSprites = [[NSMutableArray alloc] initWithCapacity:20]; destroyableSprites = [[NSMutableArray alloc] initWithCapacity:20]; //Preload OGL-Textures [self getTex: @"fighter.png" isImage: YES]; [self getTex: @"octo_8f.png" isImage: YES]; [self getTex: @"explosion_8f.png" isImage: YES]; [self getTex: @"player.png" isImage: YES]; [self getTex: @"mine.png" isImage: YES];
207
4 OpenGL ES – der Turbo-Gang [self getTex: @"bullets.png" isImage: YES]; [self getTex: @"gear.png" isImage: YES]; //Parallax-Layer back = [[ParallaxLayer alloc] initWithPic: @"background.png"]; clouds = [[ParallaxLayer alloc] initWithPic: @"clouds.png"]; [self setOGLProjection]; state = LOAD_GAME; }
Da die Texturen unabhängig von den Sprites verwaltet werden, können wir hier alle verwendeten Texturen vorab anlegen, um später Wartezeiten zu umgehen. Auch Textbausteine können optional vorab geladen werden, Beispiel: [self getTex: @"abc" isImage: NO];
Aufgrund der geringen Anzahl an Texten können wir an dieser Stelle jedoch darauf verzichten. Weiter geht es mit der loadGame-Methode, die vor jedem neuen Spiel aufgerufen wird und für einen frischen Startzustand der Objekte sorgt: - (void) loadGame { [sprites removeAllObjects]; [newSprites removeAllObjects]; [destroyableSprites removeAllObjects]; //Player [self createSprite: PLAYER speed: CGPointMake(0, 0) pos: CGPointMake(0, 0)]; }
In dieser Methode können Sie optional auch Texturen für ein weiteres Level laden und nicht mehr benötigte Texturen löschen, Beispiel: [self removeFromDictionary: @"myTex1.png"];
Da wir nur ein Level implementiert haben, startet das Spiel nach "Game Over" unverändert erneut mit den gleichen Texturen – sodass wir hier keine Texturen aus dem Speicher entfernen oder neue laden müssen. Die Erzeugung neuer Sprites funktioniert nach dem Muster des Zombiespiels und sollte keine Überraschungen für Sie bereithalten: - (id) createSprite: (int) type speed: (CGPoint) sxy pos: (CGPoint) pxy { if (type == PLAYER) { player = [[Player alloc] initWithPic: frameCnt: frameStep: speed:
Jedes Sprite verfügt über einen Typ (type), eine Geschwindigkeit (speed) und eine Anfangsposition (pos). Die individuelle Fortbewegungsart des Sprites wird dann in der jeweiligen Unterklasse geregelt. Sie sehen außerdem, dass jedes Sprite eine einheitliche Initialisierungsmethode initWithPic:frameCnt:frameStep:speed:pos: besitzt, die wir bereits mit den entsprechenden Werten aufrufen. Die Parameter ergeben sich aus den Grafiken (Name der Grafik, Anzahl der Frames im Filmstreifen). Nicht animierte Sprites verfügen nur über ein Frame, und der frameStep beträgt demzufolge 0. Wir werden uns die Implementierung der Sprites gleich noch ansehen. Zuvor wollen wir noch die Methode zum Anzeigen einer einmalig ablaufenden Explosion hinzufügen – diese entspricht ebenfalls der Implementierung des Zombiespiels und bedarf daher keiner weiteren Erläuterung: - (void) createExplosionFor: (Sprite *) sprite { CGPoint p = [Animation getOriginBasedOnCenterOf: [sprite getRect] andPic: @"explosion_8f.png" withFrameCnt: 8]; [self createSprite: ANIMATION speed: CGPointMake(0, 0) pos: p]; }
Sehen wir uns nun die Behandlung der Touch-Events im GameManager an: - (void) touchBegan: (CGPoint) p { [self handleStates]; if (state == PLAY_GAME && player) { [player setTouch: p];
210
4.12 Lassen wir's krachen: ein OpenGL ES-Shooter } } - (void) touchMoved: (CGPoint) p { if (state == PLAY_GAME) { [self touchBegan: p]; } } - (void) touchEnded { if (state == PLAY_GAME && player) { [player touchEnded]; } } - (void) handleStates { if (state == START_GAME) { state = PLAY_GAME; } else if (state == GAME_OVER) { state = LOAD_GAME; } }
Nach dem bewährten Muster reichen wir den Berührungspunkt des Touch-Displays direkt an die Player-Instanz weiter – sofern sich das Spiel im PLAY_GAME-State befindet. Ansonsten steuern wir über handleStates das Umschalten zwischen Start-Screen und GameOverScreen. Beide Screens dienen übrigens nur der Darstellung einiger Hilfetexte, wie Sie an der Implementierung der zentralen Einstiegsmethode sehen können: - (void) drawStatesWithFrame: (CGRect) frame { W = frame.size.width; H = frame.size.height; CGPoint o = [self getViewportOrigin]; switch (state) { case LOAD_GAME: [self loadGame]; state = START_GAME; break; case START_GAME: [self drawOGLString: @"Tap screen to start!" at: CGPointMake(o.x, o.y)]; [self drawOGLString: @"How to control the ship:" at: CGPointMake(o.x, o.y + 50)]; [self drawOGLString: @"Tap left - turn left." at: CGPointMake(o.x, o.y + 75)]; [self drawOGLString: @"Tap right - turn right." at: CGPointMake(o.x, o.y + 100)]; break; case PLAY_GAME:
211
4 OpenGL ES – der Turbo-Gang [self playGame]; break; case GAME_OVER: [self playGame]; [self drawOGLString: @"G A M E O V E R" at: CGPointMake(o.x, o.y)]; break; default: NSLog(@"ERROR: Unbekannter Spielzustand: %i", state); break; } }
Da sich unser Spiel auch in einem gescrollten Zustand befinden kann, müssen wir die Texte am oberen, linken Display-Rand ausrichten. Dies muss nicht immer (0, 0) sein, wie wir bereits gesehen haben. An der drawStatesWithFrame:-Methode sehen Sie auch, dass das eigentliche Spiel in der playGame-Methode stattfindet, die für uns aber immer noch keine großen Überraschungen offenbart: - (void) playGame { timer++; [self scrollWorld]; //Parallax-Ebenen [back drawWithFactor: 2 relativeTo: [player getPos] atOrigin: [self getViewportOrigin]]; [clouds drawWithFactor: 1 relativeTo: [player getPos] atOrigin: [self getViewportOrigin]]; [self generateNewEnemies]; [self manageSprites]; [self renderSprites]; }
Recht übersichtlich, oder? Die bekannten Sprite-Manager-Methoden sorgen wie zuvor im Zombiespiel dafür, dass die Sprites ohne Anpassungen eingebunden werden können. Die manageSprites-Methode dient dem Erzeugen und Löschen der Sprites und unterscheidet sich nicht von der bisherigen Implementierung im Zombiespiel – ebenso wie die renderSprites-Methode, die alle aktiven Sprites durchläuft und deren draw-Methode aufruft. Lediglich die draw-Methode des Players rufen wir separat am Schluss der Methode auf, damit das Spielerraumschiff nicht von anderen Sprites verdeckt werden kann. Da sich die Implementierung ansonsten nicht unterscheidet, drucken wir die Methoden hier nicht erneut ab. Game Play
Eine neue Methode haben wir der playGame-Methode dennoch hinzugefügt: Die generasorgt dafür, dass unser Spieler fortlaufend mit neuen Gegnern
teNewEnemies-Methode
212
4.12 Lassen wir's krachen: ein OpenGL ES-Shooter
konfrontiert wird. Diese müssen in bestimmten Zeitabständen und an bestimmten Orten erscheinen: Zeit, Ort und Anzahl der Feinde sorgen zudem für den Schwierigkeitsgrad des Spieles und definieren daher auch das Game Play des Shooters. Wie legen wir jedoch fest, wo die Feinde erscheinen sollen? Was wir möglichst vermeiden wollen, ist, dass die Feinde zufällig irgendwo auftauchen – entweder zu weit weg vom Spieler, sodass dieser den Gegner niemals trifft, oder aber zu nah, sodass die Feinde urplötzlich auf dem Bildschirm auftauchen und zu unfairen Spielsituationen führen. Ein möglicher Lösungsansatz besteht darin, einen Bereich außerhalb des Viewports zu definieren, in dem die Feinde erzeugt bzw. positioniert werden.
Abb. 4.22: Die Gegner werden in den vier Zonen TOP, LEFT, RIGHT und BOTTOM erzeugt, die sich stets außerhalb des Viewports befinden.
Diesen Bereich dimensionieren wir anhand von Screen-Breite und Screen-Höhe, wobei wir eine Pufferzone einrichten, die der maximalen Ausrichtung des größten Sprites entspricht, in unserem Fall ist das der mechanische Oktopus mit 128 Pixeln: - (CGPoint) getRndStartPos { int px, py; //Positionierung ausserhab des Screens int f = 128; //Pufferzone (frameW bzw. frameH) int flag = [self getRndBetween: 0 and: 3]; switch (flag) { case 0: //Top px = [self getRndBetween: -f-W and: f+W*2]; py = [self getRndBetween: -f-H and: -f]; break; case 1: //Left
213
4 OpenGL ES – der Turbo-Gang px = [self py = [self break; case 2: //Right px = [self py = [self break; case 3: //Bottom px = [self py = [self break;
Die getRndStartPos-Methode liefert uns eine zufällige Position innerhalb des vorgesehenen Bereiches außerhalb des Viewports. Da sich der Viewport in Abhängigkeit des Spielers verändert, beziehen wir den aktuellen Origin mit ein.4 Zum Erzeugen eines neuen Feindes benötigen wir nun nur noch den Typ und die Geschwindigkeit in xy-Richtung, sodass wir eine neue Hilfsmethode implementieren können: - (void) generateEnemy: (int) type speedX: (int) sx speedY: (int) sy { CGPoint startPos = [self getRndStartPos]; [self createSprite: type speed: CGPointMake(sx, sy) pos: startPos]; }
Damit können wir dann auch recht überschaubar die generateNewEnemies-Methode zur Steuerung des Game Plays entwerfen: - (void) generateNewEnemies { //Octos if (timer % 12 == 0) { int sx = [self getRndBetween: -3 and: 3]; int sy = [self getRndBetween: -7 and: -1]; [self generateEnemy: OCTO speedX: sx speedY: sy]; } //Mines if (timer % 5 == 0) { [self generateEnemy: MINE speedX: 0 speedY: 0]; }
4 Diese Art der Positionierung setzt eine kontinuierliche Bewegungsänderung des Spielers voraus. Verbleibt der Spieler längere Zeit am gleichen Ort (oder dreht Loopings), dann steigt die Feindeszahl außerhalb des Viewports immer weiter an. Die beste Strategie, um das Spiel zu meistern, ist also, möglichst weiträumig umherzufliegen.
Zeitlicher Abstand und dadurch Anzahl der Feinde werden durch einen Modulo-Operator bestimmt. Die zufällige Geschwindigkeit der Octos sorgt für weitere Überraschungsmomente. Die Minen sind dagegen unbewegt und stellen dadurch gleichsam Orientierungspunkte innerhalb der scrollenden Umgebung dar. Die Geschwindigkeit bzw. die Geschwindigkeitsrichtung für den Fighter ist als Startwert zu verstehen – wir werden den Bewegungspfad später innerhalb der Klasse justieren. Nun, unser Spielfeld ist nun mit Feinden bevölkert – zur Spielsteuerung gehört aber natürlich noch die Kollisionskontrolle. Wie Sie sich vielleicht schon denken können, verfügt die Sprite-Klasse (siehe unten) über eine checkColWithSprite:-Methode, die die Kollision zwischen zwei beliebigen Sprites überprüft, und eine hit-Methode, die für den Fall der Kollision aufgerufen wird. Mit diesem Wissen können wir bereits die komplette checkSprite:-Methode implementieren: - (void) checkSprite: (Sprite *) sprite { if ([sprite getType]==PLAYER || [sprite getType]==BULLET) { for (Sprite *sprite2test in sprites) { if ([sprite2test getType]==OCTO || [sprite2test getType]==FIGHTER) { if ([sprite checkColWithSprite: sprite2test] && state != GAME_OVER) { [sprite hit]; [sprite2test hit]; } } if ([sprite getType]==BULLET && [sprite2test getType]==MINE) { if ([sprite checkColWithSprite: sprite2test]) { //Mines sind unzerstörbar [sprite hit]; [[GameManager getInstance] createExplosionFor: sprite]; } } if ([sprite getType]==PLAYER && [sprite2test getType]==MINE) { if ([sprite checkColWithSprite: sprite2test]) { [sprite hit]; } } } } }
Wir haben folgende Sprite-Typen zu berücksichtigen: PLAYER, BULLETS, OCTO, FIGHTER und MINES. Wie Sie an der if-Abfrage sehen, wird für den Player- und für jede Bullet-
215
4 OpenGL ES – der Turbo-Gang
Instanz das gesamte Sprite-Array iteriert und mit dem Player bzw. einer Bullet auf Kollision geprüft. Octos und Fighter können wir dabei gleich behandeln. Da Mine-Objekte unzerstörbar sind, aber dennoch ein unüberwindliches Hindernis darstellen, werden diese separat für den Player und pro Bullet behandelt. Jede Bullet, die auf eine Mine trifft, soll eine Explosion auslösen. Ebenso soll der Player explodieren, wenn er auf ein Mine-Objekt trifft, während die Mine selbst nicht explodiert.5 Die Sprite-Klasse
Nachdem wir das Grundgerüst des Spiels innerhalb der GameManager-Klasse vervollständigt haben, können wir uns nun den Spielelementen widmen, die alle direkt oder indirekt von der Sprite-Klasse abgeleitet sind. Die Sprite-Klasse orientiert sich stark an der bereits bekannten Implementierung aus dem Zombiespiel. Aufgrund einiger Besonderheiten, die sich durch das Spielprinzips und natürlich durch die Verwendung von OpenGL ES ergeben, wollen wir die Klasse hier dennoch vollständig abdrucken: Listing Sprite.h #import #import #import "Tex.h" //Sprite-Typen enum types { PLAYER, BULLET, GEAR, OCTO, MINE, FIGHTER, ANIMATION }; @interface Sprite : NSObject { Tex *tex; //Textur-Filmstreifen CGPoint speed; //Pixelgeschwindigkeit pro Frame in x-, y-Richtung CGPoint pos; //aktuelle Position int cnt; //interner Zaehler int frameNr; //aktuelles Frame int frameCnt; //Anzahl Frames im Filmstreifen int frameStep; //Anzahl Frames pro Durchlauf int frameW; //Breite eines Frames int frameH; //Höhes eines Frames
5 Sie können natürlich, wenn Sie Lust haben, dieses Verhalten der Minen ändern. Wir finden jedoch, dass die Minen als unzerstörbare Blindgänger für einen interessanteren Spielverlauf sorgen.
216
4.12 Lassen wir's krachen: ein OpenGL ES-Shooter int angle; int type; int tolBB; int cycleCnt; bool forceIdleness; bool active; bool autoDestroy;
//Winkel in Grad, um den das Sprite gedreht wird //Sprite-Typ //Bounding Box - Toleranz //Anzahl der Wiederholungen des Filmstreifens //keine Animation, wenn Sprite stillsteht //inaktive Sprites werden vom GameManager gelöscht //!Toleranzbereich -> active = false
Lassen Sie uns kurz auf einige Besonderheiten eingehen: Anstelle einer UIImage-Instanz verfügt jedes Sprite über eine Tex-Instanz. Diese wird zusammen mit den anderen Membern in der initWithPic:frameCnt: frameStep:speed:pos:-Methode initialisiert. Die Bestimmung der Breite eines Frames (frameW) setzt voraus, dass die Texturvorlage in der Breite vollständig genutzt wird. Dies funktioniert nur, wenn die Frame-Breite einer Zweier-Potenz entspricht. Um auch andere Breiten zuzulassen, müssen Sie den Code entsprechend anpassen, Sie verschenken dabei aber natürlich Platz im Filmstreifen. Um die sinus()- bzw. cos()-Funktion zu nutzen, müssen wir den Winkel vom Gradmaß ins Bogenmaß (Rad) umrechnen. 180 Grad entsprechen 3,1415... Rad (= Pi). Unsere Methode zur Umrechnung lautet getRad:. Für die Kollisionsberechnung (Rechteck <-> Rechteck) übergeben wir eine CGRectStruktur als Bounding Box. Über den Parameter tolBB ziehen wir die Bounding Box etwas enger, um den Sprites mehr Spielraum zu geben. Nicht alle Sprites passen in die rechteckige Idealform. Sie können den Parameter individuell pro Sprite ändern – oder aber optional mehrere kleinere Bounding Boxen verwenden, die Sie möglichst genau am Umriss des Sprites anpassen. Aus Gründen der Übersichtlichkeit haben wir hier aber darauf verzichtet.
In der renderSprite-Methode - (void) renderSprite { int tolBBBkp = tolBB; tolBB = 0;
werden zwei zusätzliche Kollisionschecks durchgeführt:
CGPoint o = [[GameManager getInstance] getViewportOrigin]; if ([self checkColWithRect: CGRectMake(o.x, o.y, W, H)]) { [tex drawFrame: frameNr
221
4 OpenGL ES – der Turbo-Gang frameWidth: frameW angle: angle at: CGPointMake(pos.x, pos.y)]; } else if (autoDestroy) { int dist = H*3; if (![self checkColWithRect: CGRectMake(o.x-dist, o.y-dist, W+dist*2, H+dist*2)]) { active = false; } } tolBB = tolBBBkp; }
Ein Sprite wird nur dann gerendert, wenn es auf dem aktuellen Viewport sichtbar ist. Dies ist der Fall, wenn es mit dem Viewport-Rechteck kollidiert (o.x, o.y, W, H). Wird eine Kollision festgestellt, wird die Textur gerendert, ansonsten nicht – um den Grafikprozessor zu entlasten. Findet keine Kollision statt, wird nach Verlassen einer Toleranzzone – der dreifachen Screen-Höhe (dist = H*3) – das Sprite deaktiviert und damit vom Sprite-Manager aus dem Speicher gelöscht. Um dies festzustellen, wird ein erneuter Kollisionscheck mit dem Rechteck des Toleranzbereiches (o.x-dist, o.y-dist, W+dist*2, H+dist*2) durchgeführt. Dieser Check wird nur gemacht, sofern der Flag autoDestroy des jeweiligen Sprites auf true gesetzt wurde. Da beide Kollisionsprüfungen die tatsächliche Frame-Breite bzw. Frame-Höhe und nicht die enger gezogene Bounding Box berücksichtigen sollen, müssen wir den tolBBParameter für die Dauer der Prüfung auf 0 hochsetzen. Die Spielelemente
Nun, da unsere Sprite-Klasse steht, können wir mit den Spielelementen beginnen. Sehen wir uns zuerst die Player-Klasse an. Listing Player.h #import "Sprite.h" @interface Player : Sprite { CGPoint touchPoint; bool touchAction; bool moveLeft; int speedScalar; bool dead; } - (void) setTouch: (CGPoint) touchPoint; - (void) touchEnded; - (void) fire; @end
Die Klasse hat, neben der Aufgabe, den GAME_OVER-State auszulösen, wenn der Spieler getroffen wurde (siehe hit-Methode), im Wesentlichen drei Anforderungen zu bewältigen: 1. Steuerung/Bewegung des Spielers
224
4.12 Lassen wir's krachen: ein OpenGL ES-Shooter
2. Dauerfeuer 3. Antriebsgrafik rendern Gehen wir die Punkte der Reihe nach durch. Über die setTouch:-Methode erhalten wir den aktuellen Berührungspunkt auf dem Touch-Display. Gemäß unseres Steuerungskonzeptes soll die linke Hälfte des Screens zu einer Linksdrehung führen und die rechte Hälfte zu einer Rechtsdrehung. Dies erledigt eine einfache if-Abfrage für uns. Damit die Drehung aktiv bleibt, solange der Finger das Display berührt, speichern wir die gegenwärtige Richtung in der moveLeft-Variablen vom Typ bool. Die eigentliche Drehung des Raumschiffs erfolgt dann in der draw-Methode. Über die Member-Variable angle der Sprite-Elternklasse können wir den Drehungswinkel des Sprites angeben. Da wir aber zusätzlich auch das Sprite selbst auf einer Kreisbahn bewegen wollen, müssen wir die Geschwindigkeitsrichtung speed anpassen. Vielleicht erinnern Sie sich noch an den Einheitskreis aus der Schule, über den Sinus und Kosinus definiert sind.
Abb. 4.23. Einheitskreis mit Sinus und Kosinus
Wir wissen ja bereits, dass die speed-Variable eine unabhängige Fortbewegung in xy-Richtung ermöglicht. Andererseits soll der Drehungswinkel des Raumschiffes auch der Fortbewegungsrichtung entsprechen. Wir müssen den Winkel also lediglich in eine xyKomponente zerlegen, die der neuen Richtung des Raumschiffes entspricht. Nun, genau dieses erledigt bereits der Einheitskreis für uns: Die Hypotenuse hat stets die Länge 1 (Punkt O bis Punkt P). Wenn sich unser Raumschiff am Punkt O befindet, kann es sich in jede Richtung fortbewegen (360 Grad). Wenn wir uns am Ende der Hypotenuse am Punkt P einen Pfeil denken, so zeigt die Hypotenuse in jede der möglichen Richtungen. Mehr noch, das rechtwinklige Dreieck, das durch die Hypotenuse aufgespannt wird, gibt uns die Entfernungen an, die wir in x- bzw. y-Richtung zurücklegen müssen, um vom
225
4 OpenGL ES – der Turbo-Gang
Punkt O zum Punkt P zu gelangen. Diese beiden anderen Seiten des Dreiecks (Gegenkathete und Ankathete) bzw. deren Länge definieren den Sinus und Kosinus. Das ist doch äußerst praktisch für uns, nicht wahr? Damit können wir die Richtungsänderung direkt aus dem Drehungswinkel berechnen: speed.x = sin([self getRad: angle]); speed.y = -cos([self getRad: angle]);
Da Sinus und Kosinus stets zwischen –1 und 1 oszillieren, wir das Raumschiff aber gerne etwas schneller fortbewegen wollen, müssen wir den Wert mit einer festen Größe multiplizieren. Da es sich bei der speed-Variablen mathematisch um einen Vektor handelt, nennt man diesen Faktor auch einen Skalar (speedScalar). In Abhängigkeit des Wahrheitswertes von moveLeft können wir dann den Drehungswinkel erhöhen oder verringern. Damit sich die Drehbewegung etwas feinfühliger gestaltet, erhöhen wir den Drehungswinkel in Abhängigkeit von der Dauer der Berührung: Die Drehung erfolgt anfangs etwas langsamer (angleStep = 3) und später etwas schneller (Maximaler Wert: angleStep = 10). Beide Werte können Sie je nach Geschmack anpassen.
angle
Kommen wir zum Dauerfeuer. Das Ziel ist dabei, in regelmäßigen Abständen eine neue zu erzeugen, die Richtung der Bullet soll dabei der aktuellen Richtung des Players entsprechen. Bullet-Instanz
Die Steuerung erfolgt vollständig in der fire-Methode. Die zeitliche Abfolge der Bullets haben wir wieder über einen Modulo-Operator gelöst (cnt % 5). Hier können Sie natürlich auch mit anderen Werten experimentieren. Anstatt die Geschosse automatisch abzufeuern, können Sie alternativ einen zusätzlichen Button auf dem Display anbringen – nur wenn dieser gedrückt wird, wird die fire-Methode des Players aufgerufen. Bevor wir die createSprite-Methode des GameManagers aufrufen können, müssen wir natürlich das Geschoss mittig auf dem Player positionieren. Da die createSprite-Methode die Übergabe des Drehungswinkels nicht erlaubt, rufen wir auf der zurückgelieferten Instanz stattdessen einen Getter auf. Über [bullet setAngle: angle];
sorgen wir dafür, dass die neu erzeugte Instanz in die gleiche Richtung gedreht ist wie momentan der Spieler. Die Richtung des Geschosses können wir direkt über den speedRichtungsvektor übergeben. Die Antriebsanimation haben wir, wie Sie vielleicht bereits erkennen können, nach dem gleichen Prinzip, nur in umgekehrter Richtung, gelöst. Bei der gear.png-Grafik handelt es sich um transparenten weißen Rauch, der aus den beiden Antriebsdüsen des Raumschiffes ausgestoßen werden soll. Mit cnt % 2 erzeugen Sie eine Flackeranimation des Antriebs – um diesen stattdessen dauerhaft anzuzeigen, setzen Sie einfach cnt % 1. Wie Sie vielleicht bemerken, haben wir den Antrieb nicht einfach statisch entsprechend der aktuellen Richtung an das Raumschiff "geklebt", sondern einen schweifartigen Effekt erzeugt: Der Antriebsrauch ändert sich entsprechend der Richtungsänderung des Spielers leicht verzögert und sorgt so für eine realistischere Antriebssimulation.
226
4.12 Lassen wir's krachen: ein OpenGL ES-Shooter
Dies haben wir über einen kleinen Trick realisiert. Die Bullets werden ja – sofern sie auf kein Hindernis treffen – automatisch bei Erreichen des Toleranzbereiches in der renderSprite-Methode aus dem Speicher gelöscht (so wie alle anderen Sprites auch). Im Prinzip gilt dies auch für den Antriebsrauch: Dieser müsste, da wir diesen ja ebenso wie die Bullets mittig auf dem Spieler platziert haben, viel häufiger zu sehen sein. Warum das nicht so ist, werden wir gleich sehen. Zunächst stellen wir die Bullet-Klasse vor, die keine besonderen Geheimnisse enthält: Listing Bullet.h #import "Sprite.h" @interface Bullet : Sprite { } - (void) setAngle: (int) degree; @end Listing Bullet.m #import "Bullet.h" #import "GameManager.h" @implementation Bullet - (void) setAngle: (int) degree { angle = degree; autoDestroy = true; } - (void) hit { active = false; } @end
Die Gear-Klasse, die für den Antriebsschweif zuständig ist, haben wir von Bullet abgeleitet: Listing Gear.h #import "Bullet.h" @interface Gear : Bullet { } @end
227
4 OpenGL ES – der Turbo-Gang Listing Gear.m #import "Gear.h" @implementation Gear - (void) draw { if (active) { pos.x+=speed.x; pos.y+=speed.y; [self drawFrame]; if (cnt > 3) { active = false; } } } @end
Sie sehen, wir haben hier die draw-Methode überschrieben, um anhand der internen cntVariable, die konstant hochgezählt wird, das Sprite zu deaktivieren. Dies ist auch schon der zuvor erwähnte Trick. Das Gear-Sprite wird konstant neu erzeugt, deaktiviert sich aber nach mehr als drei Spielschleifen-Durchläufen selbst (cnt > 3). Tatsächlich wird die Gear-Textur nicht nur einmal auf dem Screen gerendert, sondern mehrmals. Wir können nur das letzte Teilstück sehen, weil die anderen vom Spieler-Sprite verdeckt werden. Da die Gear-Sprites zeitversetzt erzeugt werden, erscheint die Richtung ebenfalls leicht zeitversetzt zur aktuellen Spielerrichtung. Wenn Sie den cnt-Grenzwert erhöhen (zum Beispiel cnt > 40), sehen Sie, wie der Rauch schlangenhaft dem Spieler hinterherwedelt (Abb. 4.24). Lassen Sie uns kurz noch einen weiteren Test durchführen. Unser Spieler bewegt sich ja dank Sinus und Kosinus auf Kreisbahnen durch das Level – sehen können wir das aber nur indirekt, anhand der gescrollten Parallax-Hintergründe. Kommentieren Sie im GameManager testweise die scrollWorld-Methode in der playGameMethode des GameManagers aus: Da die Spielfläche nun nicht mehr gescrollt wird, können Sie die Kreisbahn des Spielers leichter erkennen – manövrieren Sie jedoch schnell genug, damit Ihr Raumschiff nicht von der Bildfläche verschwindet (Abb. 4.25).
228
4.12 Lassen wir's krachen: ein OpenGL ES-Shooter
Abb. 4.24: Links: Normale Antriebsanimation, rechts mit cnt > 40
Abb. 4.25: Links: Scrolling ist deaktiviert, rechts: mit Scrolling
229
4 OpenGL ES – der Turbo-Gang
Diese Art der Fortbewegung werden wir gleich noch für den Fighter-Gegner verwenden. Zuvor werfen wir aber noch einen Blick auf die anderen beiden Gegnertypen. Listing Octo.h #import "Sprite.h" @interface Octo : Sprite { } @end Listing Octo.m #import "Octo.h" #import "GameManager.h" @implementation Octo - (void) additionalSetup { autoDestroy = true; } - (void) hit { active = false; [[GameManager getInstance] createExplosionFor: self]; } @end
An diesem Beispiel ist gut zu erkennen, dass die Sprite-Klasse eine ganze Menge an Logik aus der individuellen Implementierung der Sprites heraushält. Für den mechanischen Oktopus müssen wir lediglich dafür sorgen, dass dieser automatisch gelöscht wird, indem wir den autoDestroy-Flag aktivieren. Außerdem sorgen wir in der hit-Methode für eine Explosion. Die Tex-Klasse unterstützt ja bereits animierte Texturen, welche über die Sprite-Klasse aufgerufen werden, sodass wir uns in der Octo-Klasse nicht weiter um die Animation des Sprites kümmern müssen. Auch die Explosion basiert auf einer animierten Frame-Sequenz, die allerdings nur einmalig abläuft. Wie das funktioniert, haben wir ja bereist beim Zombiespiel gesehen: Wir müssen in der Octo-Klasse lediglich die eigene Instanz per self übergeben, sodass die Klasse die Sequenz mittig über dem Oktopus positionieren kann. Listing Mine.h #import "Octo.h" @interface Mine : Sprite { int sign; }
Auch die Mine-Klasse ist direkt von Sprite abgeleitet und verfügt als Besonderheit lediglich über ein zufällig gesetztes Vorzeichen (sign), das die Richtung der Rotation der Mine festlegt. Kommen wir nun zu dem interessantesten Gegner, der sich wie der Spieler auf einer Kreisbahn fortbewegen soll. Listing Fighter.h #import "Octo.h" @interface Fighter : Octo { int angleOffset; int pathCnt; int speedScalarX; int speedScalarY; } @end Listing Fighter.m #import "Fighter.h" @implementation Fighter
Die Fortbewegung funktioniert hier genauso wie beim Spieler: Über die Sinus-/KosinusFunktion bestimmen wir ausgehend vom aktuellen Drehungswinkel den Richtungsvektor speed. Doch woher kommt der aktuelle Drehungswinkel? Eine Nutzereingabe wie in der Spieler-Klasse fehlt hier ja. Die Lösung ist – wie immer – recht simpel: Wir geben den Winkel einfach vor. Dazu haben wir mit animationPath ein Array angelegt, das die vorab festgelegten Richtungsänderungen enthält. Natürlich können Sie die Änderungen in diskreten Zeitabständen auch zufällig vornehmen, aber ein solcher vordefinierter Animationspfad hat noch einen weiteren schönen Nebeneffekt, wie wir gleich noch sehen werden. Dieser Animationspfad wird endlos wiederholt, und Sie können unschwer erkennen, dass wir die Zeitabstände für die Richtungsänderung wieder einmal durch einen ModuloOperator bestimmt haben.
232
4.12 Lassen wir's krachen: ein OpenGL ES-Shooter
Abb. 4.26: Die Fighter bewegen sich wie der Spieler auf einer kreisähnlichen Flugbahn.
Experimentieren Sie mit verschiedenen Animationspfaden – jedes Element des Arrays enthält einen Winkel, der die aktuelle Richtung des Sprites vorgibt. Natürlich kann das Array beliebig lang sein. Über sizeof(animationPath)/sizeof(int) können wir die Länge des C-Arrays bestimmen (es bringt keine Vorteile, hier ein NSArray einzusetzen). Vielleicht erinnern sich noch an die eindrucksvollen Gegnerbewegungen aus dem Spieleklassiker Galaga (Namco, 1981)?
Abb. 4.27: Gegnerische Angriffsmuster bei Galaga (Namco)
Und vielleicht haben Sie sich schon immer einmal gefragt, wie das gemacht wurde?
233
4 OpenGL ES – der Turbo-Gang
Nun, die insektenartigen Gegner bilden verschiedene Formationen, die sich während des Spielverlaufs mehrmals wiederholen. Der interessante Effekt entsteht durch die Reihenbildung der Feindesschwärme, die wie Zugabteile hintereinander aufgereiht sind. Bei genauerer Betrachtung stellt sich heraus, dass sich alle Gegner auf dem gleichen Pfad bewegen, nur zeitversetzt. Erinnert Sie das an etwas? Genau, wir haben dieses Verhalten bereits implementiert. Um den Fighter so wie in Galaga umherfliegen zu lassen, müssen wir lediglich weitere Fighter zeitversetzt erzeugen – und zwar stets an der gleichen Ausgangsposition. Ein weiterer Timer muss dafür sorgen, dass nicht unbegrenzt viele Fighter erzeugt werden. Der Ablauf ist also der folgende:
1. Ist es Zeit, neue Fighter zu erzeugen? 2. Wenn ja, dann erzeuge neuen Fighter an der Position xy. 3. Erzeuge weiter neue Fighter, bis die maximale Anzahl erreicht ist. 4. Ende. Ganz einfach, oder? Um aus unserem Fighter einen Galaga-Schwarm entstehen zu lassen, bauen wir den oben beschriebenen Algorithmus zusätzlich in der generateNewEnemiesMethode des GameManagers ein: static int fighterCnt = 15; //Anzahl Fighter pro Reihe static CGPoint startP; if (fighterCnt == 15 && timer % 20 == 0) { //Neue Reihe erzeugen? fighterCnt = 0; startP = [self getRndStartPos]; } if (timer % 9 == 0 && fighterCnt < 15) { //timer bestimmt die Abstaende fighterCnt++; [self createSprite: FIGHTER speed: CGPointMake(7, 7) pos: startP]; }
Als timer verwenden wir den internen Counter des GameManagers zusammen mit einem Modulo-Operator. Die Anzahl der Fighter pro Schwarmreihe wird auf 15 Fighter begrenzt (fighterCnt). Während der Erzeugungsphase der Fighter zählen wir diesen Wert hoch. Gleichzeitig dient uns dieser Wert als Prüfstein, um zu entscheiden, ob bereits neue Fighter-Reihen erzeugt werden sollen (es soll stets nur eine neue Reihe erzeugt werden). Für den gleichmäßigen Verlauf der Formation sorgt dann schließlich der Animationspfad, den wir ja bereits in der Fighter-Klasse angelegt haben.
234
4.12 Lassen wir's krachen: ein OpenGL ES-Shooter
Abb. 4.28: Screenshots des fertigen Spiels
235
5 5 Die dritte Dimension: 3D-Spiele 5.1
Wie sind 3D-Spiele aufgebaut? Der Sprung in die dritte Dimension ist – nachdem wir bereits OpenGL ES für 2D-Spiele eingesetzt haben – nur ein kleiner Schritt: Wir haben gesehen, dass die Transformationsfunktionen standardmäßig drei Koordinaten (x-y-z) erwarten – für 2D-Spiele brauchten wir lediglich die unbenutzte z-Koordinate auf 0 zu setzen. Auch die MainView-Klasse, die die Einrichtung des OpenGL ES-Grafikkontextes erledigt und die die drei benötigten Buffer (Renderbuffer, Framebuffer und Depthbuffer) initialisiert, unterstützt bereits die 3DDarstellung von Modellen. Daher sind für den Sprung in die dritte Dimension nur drei Anpassungen nötig: 1. Umstellung der Perspektive (über setOGLProjection) 2. Aktivierung des Tiefenpuffers (z-Achse) 3. Modelle müssen über drei statt zwei Koordinaten verfügen (Vertex-Arrays) Das hört sich gar nicht so kompliziert an – und ist es auch nicht. Dass 3D-Spiele trotzdem als technisch anspruchsvoll gelten, hat einen anderen Grund: Stellen Sie sich typische Genre-Vertreter vor, wie zum Beispiel einen Ego-Shooter, ein Autorennspiel oder ein Third Person Action Adventure. Wie sind diese Spiele gemacht? Der Spieler findet eine frei begehbare Landschaft vor, die er von den verschiedensten Blickwinkeln betrachten kann. Dazu kommen die darin enthaltenen 3D-Modelle (Autos, Menschen, Monster usw.), die ebenfalls von allen Seiten gut aussehen sollen. Durch die räumliche Ausdehnung der Objekte müssen nicht nur verschiedene Texturen erarbeitet werden, sondern auch deren Formen über hochkomplexe externe Editoren modelliert werden – die Erstellung selbst einfacher 3D-Szenen übersteigt damit häufig das zeitliche und finanzielle Budget eines Hobby-Entwicklers. Neben der Modellierung der Szenen und Modelle kommen natürlich noch weitere Hürden hinzu: Die räumliche Darstellung sorgt für mehr Realismus – dadurch sollten sich auch die
237
5 Die dritte Dimension: 3D-Spiele Bewegungen der Modelle physikalisch möglichst realistisch verhalten. Und vielleicht wollen Sie zusätzlich noch verschiedene Lichtquellen und Materialeigenschaften berücksichtigen.
Abb. 5.1: Hero of Sparta (Gameloft), Need For Speed Undercover (Electronic Arts), Mirror's Edge (Electronic Arts)
Bevor Sie ein 3D-Spiel planen, sollten Sie sich vorher gut überlegen, was alles auf Sie zukommt und ob Sie diesen Aufwand wirklich allein bewältigen können. Sie haben nicht ewig Zeit: Zwei Jahre und mehr an einem 3D-Spiel im stillen Kämmerlein zu werkeln ist im Hinblick auf den schnelllebigen App Store keine gute Idee: Das Risiko, dass Ihr Spielkonzept längst veraltet ist und/oder technisch mit den neuesten Produktionen großer Studios nicht mithalten kann, sollten Sie nicht unterschätzen. Dennoch – das Thema ist andererseits viel zu spannend, um es einfach so zu übergehen. Und Sie werden sehen, die Grundlagen lassen sich immerhin recht schnell erarbeiten. Wir wissen ja bereits, dass für OpenGL ES alles aus Eckpunkten besteht: Ob Sie nun eine Gebirgslandschaft, ein Kellerverlies oder ein Automodell darstellen wollen – 3D-Spiele werden aus einzelnen Modellen zusammengesetzt, die wiederum aus zahlreichen Eckpunkten, den Vertices, bestehen. Unabhängig vom Spielkonzept müssen Sie die folgenden Kernaufgaben bewältigen: Erzeugen der Modelle (über eine externe Modellierungssoftware oder auf Basis einfacher geometrischer Grundformen) Positionierung dieser Modelle im Raum Bewegung/Animation der Modelle Kollisionsprüfung der Modelle untereinander Positionierung einer Kamera Wie ließe sich nun – ausgehend von diesen Bausteinen – ein 3D-Spiel erzeugen? Zwei Anregungen: Beispiel 1: Für ein einfaches Autorennspiel benötigen Sie mindestens eine zweidimensionale Grundfläche (Plane). Auf dieser können Sie den Straßenverlauf mit Texturen darstellen. Wenn Sie die dritte Achse (Höhenverlauf der Strecke) aus pragmatischen Gründen ausklammern, können Sie die Kollision mit dem Straßenverlauf über eine 2D-Tilemap prüfen. Die Rennautos bestehen, ebenso wie Hindernisse oder Häuser am Straßenrand, aus
238
5.2 Das Grundgerüst 3D-Modellen, die Sie auf dieser Ebene platzieren und (im Falle der Autos) entlang der Rennstrecke fortbewegen können. Beispiel 2: Ein Doom-Shooter bzw. ein Labyrinth-Spiel: Wieder legen Sie eine zweidimensionale Grundfläche an – die Wände bestehen aus mehreren 3D-Quadern, die Sie anhand einer 2D-Tilemap aufstellen. Erhöhte Ebenen oder Stufen werden ausgeschlossen. Ihre Spielfigur bewegt sich auf der 2D-Tilemap, die Kollision erfolgt ebenfalls über die Tilemap, die festlegt, wo die Wände aufgestellt sind. Sie sehen: Auch hier müssen Sie die Kollisionskontrolle nicht zwischen Spieler und senkrechter Wand vornehmen, sondern übertragen lediglich die Konzepte aus der 2D-Spieleentwicklung. Auch für frei bewegliche Spielobjekte (Spieler, Monster, Geschosse) können Sie die Bodenposition vergleichen (2D-Kollision) – oftmals kann es unter Umständen aber auch einfacher sein, die Modelle direkt gegeneinander auf Kollision zu prüfen. Sie sehen, auch wenn viele 3D-Spiele auf den ersten Blick sehr eindrucksvoll wirken, verbergen sich mitunter einfache Grundkonzepte dahinter. Auch Spieleentwickler kochen nur mit Wasser. Beginnen wir also zunächst damit, die passenden Lösungen auf die oben benannten Kernaufgaben zu finden. Im Anschluss werden wir anhand der Ergebnisse exemplarisch einen 3D-Weltraum-Shooter entwickeln.
5.2
Das Grundgerüst Zum Erstellen des ersten 3D-Projektes verwenden wir als Basis das Grundgerüst aus dem 2D-Kapitel, das bekanntermaßen aus drei Klassen besteht: MyGameAppDelegate MainView GameManager Sie finden das fertige Projekt im Download-Ordner unter dem Namen "OGL3D_Basics". Um nun die 3D-Perspektive einzurichten und den Tiefenpuffer zu aktivieren, der bereits in der MainView-Klasse erzeugt wurde, müssen wir lediglich die setOGLProjection-Methode des GameManagers anpassen: - (void) setOGLProjection { glLoadIdentity(); //Set View glMatrixMode(GL_PROJECTION); zNear = 0.1; zFar = 2500; fieldOfViewAngle = 45; float top = zNear * tan(M_PI * fieldOfViewAngle / W); float bottom = -top; float left = bottom * W / H; //Seitenverhaeltnis: W / H;
239
5 Die dritte Dimension: 3D-Spiele float right = top * W / H; glFrustumf(left, right, bottom, top, zNear, zFar); //3D-Perspektive //Enable Modelview: Auf das Rendern von Vertex-Arrays umschalten glMatrixMode(GL_MODELVIEW); glEnableClientState(GL_VERTEX_ARRAY); glEnable(GL_DEPTH_TEST); glDepthFunc(GL_LESS); }
Wie bereits beim Einrichten der 2D-Perspektive müssen wir OpenGL ES zuerst in den Matrix-Modus GL_PROJECTION versetzen. Anstelle der glOrthof()-Funktion kommt nun die glFrustumf()-Funktion zum Einsatz, die ansonsten aber die gleichen Parameter erwartet. Über diese Methode legen wir die Perspektive auf das Spielgeschehen fest, nicht jedoch die Kameraposition. Um diese werden wir uns später kümmern. Gleichwohl sorgt auch diese Funktion bereits dafür, dass es eine Kamera gibt – nämlich den Betrachter, der auf die Szene blickt: Dieser befindet sich direkt über dem Koordinatenursprung und blickt entlang der negativen z-Achse in die Tiefe. Der Blickwinkel beträgt dabei 45 Grad (fieldofViewAngle, entspricht einem Kameraobjektiv). Da der Betrachter das Geschehen räumlich wahrnehmen soll, benötigen wir auch eine Tiefenangabe: zFar. Alle Objekte, die sich hinter dieser Grenze befinden, sind nicht zu sehen, ebenso wie Objekte, die sich vor zNear befinden. Daher bezeichnet man beide Parameter auch als Clipping Panes. Anhand dieser Parameter kann man die Abmessungen left/right/bottom/top – wie in der Implementierung gezeigt – berechnen.
Abb. 5.2: Frustum mit zNear- und zFar-Ebene
Wenn wir später eine Kamera hinzufügen, werden wir sehen, dass sich zNear und zFar relativ zur Kameraposition verändern. Das heißt, der Betrachter wird nie Objekte sehen können, die weiter als 2500 Einheiten (= zFar) entfernt sind. Die Parameter für zNear und zFar müssen per Definition stets beide größer als 0 sein. Es ist üblich, zNear möglichst klein zu wählen, um den blinden Bereich zwischen Betrachter und zNear möglichst schmal
240
5.3 Das 3D-Koordinatensystem zu halten und zFar möglichst groß zu wählen – aber auch nicht zu groß, da sonst die Performance einbrechen könnte, wenn zu viele Objekte gerendert werden müssen. Zu guter Letzt aktivieren wir in der setOGLProjection-Methode noch den Tiefenpuffer (GL_DEPTH_TEST), bestimmen die Standard-Alpha-Funktion (GL_LESS) und ermöglichen dauerhaft die Übergabe von Vertex-Arrays über den Client-State GL_VERTEX_ARRAY. Sie kennen dies ja bereits aus dem 2D-Kapitel.
5.3
Das 3D-Koordinatensystem Mit den gezeigten Projektionseinstellungen verfügen wir damit über das folgende Koordinatensystem: Der Koordinatenursprung (Origin) befindet sich in der Screen-Mitte. Die positive x-Achse zeigt nach rechts. Die positive y-Achse zeigt nach oben. Die positive z-Achse zeigt zum Betrachter, die negative z-Achse zeigt in den Bildschirm hinein.
Abb. 5.3: 3D-Koordinatensystem unter OpenGL ES
Es handelt sich dabei um ein "rechtshändiges" kartesisches Koordinatensystem (der Mittelfinger (= z-Achse) zeigt zum Betrachter) – im Gegensatz beispielsweise zu DirectX: Hier verläuft die positive z-Achse entgegengesetzt in den Bildschirm hinein.
241
5 Die dritte Dimension: 3D-Spiele Anders als bei einer 2D-Perspektive gibt es keine Einheiten für die Koordinaten. Der Punkt P (10, 30, –20) bezieht sich daher nicht auf Pixelangaben – sondern man spricht stattdessen schlicht von "Einheiten". Der Punkt P befindet sich also 10 Einheiten parallel zur x-Achse vom Ursprung entfernt, 30 Einheiten parallel zur y-Achse vom Ursprung entfernt und –20 Einheiten parallel zur z-Achse vom Ursprung entfernt. Der Grund für die Einheiten liegt einfach darin, dass sich aufgrund der Perspektive und der Kameraposition die tatsächliche Pixelposition auf dem Bildschirm erst nach der Projektionsberechnung der Eckpunkte im Framebuffer ergibt. Eine näher beim Betrachter verlaufende Linie erscheint somit länger als eine weiter entfernte Linie der gleichen Länge.
5.4
Einfache Formen zeichnen Mit dem Bild dieses Koordinatensystems im Kopf können wir uns bereits vorstellen, wo sich Objekte befinden müssen, damit sie von uns gesehen werden können. Da wir noch keine Kamera aufgestellt haben, befinden wir uns direkt am Ursprung und blicken die negative z-Achse entlang. Ein paar Beispiele: Der Punkt P1 (0, 0, –6) befindet sich vor dem Betrachter, die Entfernung beträgt: -zNear-6 Einheiten. Der Punkt liegt damit innerhalb des sichtbaren Bereiches. Der Punkt P2 (0, 0, –60) befindet sich vor dem Betrachter, die Entfernung beträgt: -zNear-60 Einheiten. Der Punkt liegt damit innerhalb des sichtbaren Bereiches, ist aber weiter entfernt als der vorherige Punkt P1. Der Punkt P3 (0, 0, 6) liegt dagegen im Rücken des Betrachters und ist nicht sichtbar. Wie auch schon für die 2D-Programmierung erläutert, stehen uns verschiedene Konstanten zur Verfügung, um aus Eckpunkten eine (räumliche) Fläche zu rendern. Wir setzen dabei dieselben Funktionen wie zuvor ein – und können auf dieselben Konstanten zurückgreifen (GL_POINTS, GL_LINES, GL_TRIANGLES, usw.). Der Unterschied zu 2D besteht darin, dass wir nun drei Koordinaten pro Eckpunkt angeben können – aber nicht müssen. Sehen wir zuerst ein Beispiel, wie wir eine Linie im Raum rendern, die nur aus xy-Koordinaten besteht: - (void) drawLine { GLbyte vertices[ ] = { 0, 0, //Vertex 1 1, 0 //Vertex 2 }; glPushMatrix(); glColor4f(1, 1, 0, 1); //Zeichenfarbe setzen glVertexPointer(2, GL_BYTE, 0, vertices); //2 Werte pro Eckpunkt glTranslatef(0, 0, -zNear-0.0001); glDrawArrays(GL_LINES, 0, 2); //2 = Anzahl Eckpunkte
242
5.4 Einfache Formen zeichnen glPopMatrix(); }
Da wir eine räumliche Perspektive gesetzt haben, wird die z-Koordinate automatisch ergänzt und beträgt schlicht 0.
Abb. 5.4: Eine gelbe Linie, sonst nichts!
Der erste Punkt der Linie befindet sich bei (0, 0), also im Ursprung. Der zweite Punkt (1, 0) verläuft auf der x-Achse 1 Einheit nach rechts. Da der z-Wert bei beiden Punkten 0 beträgt, können wir die Linie erst sehen, wenn wir das Koordinatensystem temporär nach hinten verschieben – wie bisher benutzen wir dafür das glPushMatrix()/glPopMatrix()-Pärchen. Die Translation der z-Achse beträgt: -zNear-0.0001. Wir haben die Linie also 0.0001 Einheiten vor die zNear-Clipping Pane verschoben – können die Linie also gerade so sehen. Nach rechts verläuft die Linie noch viel weiter über den Display-Rand hinaus: Die Strecke bis zum rechten Bildrand hängt von der Perspektive, der Screen-Auflösung und von der z-Position ab und beträgt ungefähr 0.0315 Einheiten.1 Aber Achtung: Wenn Sie eine Linie dieser Länge rendern wollen, müssen Sie in der drawLine-Methode natürlich zuvor den passenden Datentyp wählen, statt GLbyte/GL_BYTE nun Glfloat/GL_FLOAT. Wenn wir später mit 3D-Modellen arbeiten, werden wir grundsätzlich den GLfloatDatentyp verwenden. Als Nächstes steht das OpenGL ES-Hello World an, wieder ein Dreieck, diesmal geben wir die z-Koordinate aber explizit an. - (void) drawTriangle { GLbyte vertices[ ] = { -1, 1, 0, //Vertex 1 1, -1, 0, //Vertex 2 1, 1, 0 //Vertex 3 1 Sie können den Wert durch Ausprobieren ermitteln oder rechnerisch, indem Sie die Screen-Koordinaten auf die 3DWeltkoordinaten umrechnen: Also der umgekehrte Weg, um die Daten in den Framebuffer zu schieben. Eine Beschreibung finden Sie unter anderem bei [Fournier et. al.].
243
5 Die dritte Dimension: 3D-Spiele }; glPushMatrix(); glColor4f(0, 1, 0, 1); glVertexPointer(3, GL_BYTE, 0, vertices); //3 Werte pro Eckpunkt glTranslatef(0, 0, -6); //Etwas weiter nach hinten schieben glDrawArrays(GL_TRIANGLE_STRIP, 0, 3); //3 = Anzahl Eckpunkte glPopMatrix(); }
Wir haben ja eben bereits gesehen, dass direkt vor der zNear-Clipping Pane der Abstand vom Ursprung zum seitlichen Rand knapp 0.0315 Einheiten beträgt (bezogen auf das aktuelle Environment). Um das Dreieck zu sehen, verschieben wir das Modell sechs Einheiten nach hinten. Beachten Sie auch, dass wir nun beim VertexPointer drei Werte pro Eckpunkt geltend machen müssen. Noch befindet sich das Dreieck parallel zur xy-Ebene, es könnte sich auch um eine 2DGrafik handeln. Verändern Sie doch mal testweise die z-Koordinate(n) und beobachten die perspektivische Veränderung. Noch deutlicher können wir die 3D-Perspektive machen, wenn wir eine Fläche rotieren lassen: - (void) drawRectangle { //Rechteck aus zwei Dreiecken zusammengesetzt GLbyte vertices[ ] = { -1, 1, 0, //links oben 1, 1, 0, //rechts oben -1, -1, 0, //links unten 1, -1, 0 //rechts unten }; glPushMatrix(); glColor4f(0, 0, 1, 1); glVertexPointer(3, GL_BYTE, 0, vertices); //3 Werte pro Eckpunkt glTranslatef(0, 0, -6); static int angle = 0; angle += 2; glRotatef(angle, 1, 0, 0); //Um die x-Achse rotieren lassen glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); //4 = Anzahl Eckpunkte glPopMatrix(); }
244
5.5 Texturierung von Flächen
Abb. 5.5: Rotation einer Rechteckfläche
Das Rechteck – bestehend aus zwei Dreiecken – besitzt vier Eckpunkte, die die Rechteckfläche mittig über dem Ursprung aufspannen. Die Rotation erfolgt um die x-Achse herum. Ein derartiges Rechteck können wir natürlich auch mit einer Textur bespannen und so aus jedem 2D-Spiel flugs ein 3D-Spiel machen. Natürlich besitzt die im Beispiel gerenderte Fläche kein Volumen – Modelle mit räumlicher Ausdehnung werden wir uns weiter unten ansehen. Das Grundprinzip bleibt dabei gleich – die Form des Objektes wird allein durch die Eckpunkte vorgegeben.
5.5
Texturierung von Flächen Aus Performance-Gründen ist es durchaus sinnvoll, ein 3D-Spiel nicht vollständig aus 3DModellen mit Volumen aufzubauen. Vielleicht kennen Sie aus einigen 3D-Autorennspielen diese seltsam flachen Bäume am Straßenrand, die zwar irgendwie realistisch aussehen, aber andererseits auch von jeder Seite gleich. Dies liegt daran, dass es sich dabei um, rechteckige, texturierte Flächen ohne z-Ausdehnung handelt, deren Fläche stets dem Betrachter zugewandt ist. Derartige Sprites nennt man auch Billboards, und wir werden später, wenn wir über eine Kamera verfügen, Billboards zusammen mit 3D-Modellen einsetzen. Texturierte Flächen wirken aber auch interessant, wenn deren Fläche nicht immer dem Betrachter zugewandt ist. Stellen Sie sich doch mal den 2D-Fighter aus dem letzten Spiel vor, der im 3D-Raum Pirouetten drehen könnte. Um dies zu verwirklichen, müssen wir lediglich die rechteckige Grundfläche des Fighters mit der Fighter-Textur bespannen. Mit den bereits vorgenommenen Einstellungen innerhalb des Grundgerüstes ist die Textur automatisch von beiden Seiten der Fläche zu sehen (Vorder- und Rückseite).
245
5 Die dritte Dimension: 3D-Spiele Als Basis wählen wir das Projekt aus dem letzten Kapitel, dem wir die bereits bekannte Tex-Klasse hinzufügen. Das fertige Beispielprojekt hierzu lautet "OGL3D_Texture". Da wir den Render-Vorgang bei 3D-Modellen flexibler gestalten wollen, streichen wir die draw-Methoden aus der Tex-Klasse, sodass lediglich die Methoden zur Textur-Erzeugung unverändert in der Klasse verbleiben. Außerdem fügen wir dem GameManager wieder die bekannte Ressourcenverwaltung aus dem 2D-Teil hinzu. Damit ergibt sich für den rotierenden 2D-Fighter der folgende Code, den wir der GameManager-Klasse hinzufügen und in der playGame-Methode kontinuierlich aufrufen: - (void) drawTexture { Tex *tex = [[GameManager getInstance] getTex: @"fighter.png" isImage: YES]; GLuint textureID = [tex getTextureID]; //Seitenverhaeltnis beibehalten, bei einer Hoehe von 1 int w = [tex getWidth] / [tex getHeight]; int h = 1; GLfloat vertices[ ] = { //Mittig auf dem Ursprung positionieren -w, h, 0, //links oben w, h, 0, //rechts oben -w, -h, 0, //links unten w, -h, 0 //rechts unten }; GLfloat textureCoords[ ] = { 1, 0, //rechts unten -> gemappt auf den letzten Vertex (w, -h, 0) 0, 0, //links unten 1, 1, //rechts oben 0, 1 //links oben -> gemappt auf den ersten Vertex (-w, h, 0) }; int verticesLength = sizeof(vertices) / sizeof(GLfloat); //Render obj-Modell glEnable(GL_TEXTURE_2D); //alle Flaechen werden nun texturiert glVertexPointer(3, GL_FLOAT, 0, vertices); glBindTexture(GL_TEXTURE_2D, textureID); glTexCoordPointer(2, GL_FLOAT, 0, textureCoords); glColor4f(1, 1, 1, 1); //Keine Extrafarbe //Matrix-Operations glPushMatrix(); //Nach oben fliegen, z-Tiefe = -16 Einheiten static float y = -8; y += 0.2;
246
5.5 Texturierung von Flächen if (y > 8) y = -8; glTranslatef(0, y, -16); //Pirouetten drehen static int angle = 0; angle += 10; glRotatef(angle, 0, 1, 0); glDrawArrays(GL_TRIANGLE_STRIP, 0, verticesLength/3); glPopMatrix(); //Cleanup glDisable(GL_TEXTURE_2D); }
Zunächst besorgen wir uns nach dem bekannten Muster die Textur-ID für die FighterGrafik. Damit wir die Eckpunkte proportional zur tatsächlichen Grafik wählen können, leiten wir die Breite aus dem Seitenverhältnis der Originalgrafik ab. Sie wissen ja, dass in der 3D-Welt keine Pixel-Einheiten existieren. Allerdings wird uns die spätere Positionierung der Objekte erleichtert, wenn wir diese mittig über dem Ursprung platzieren (und damit nicht mehr ausgehend von der linken, oberen Ecke – dies würde spätestens bei 3D-Körpern zu Verwirrung führen). Außerdem können wir so die Rotation direkt durchführen, ohne das Objekt zuvor zu verschieben. Für die Texturkoordinaten wählen wir dieselben Parameter wie bisher, es handelt sich ja stets um eine Fläche mit w = 1 und h = 1, jedoch müssen wir die Reihenfolge der Koordinaten umdrehen, da die y-Achse in unserem 3D-Koordinatensystem nicht mehr von oben nach unten verläuft, sondern umgekehrt. Der Rest des Codes dürfte Ihnen bekannt vorkommen und unterscheidet sich nicht vom Texturieren der bisher besprochenen 2D-Grafiken. Der Vertex-Pointer verweist auf unsere Rechteck-Eckpunkte, die je aus xyz-Koordinaten bestehen, auf denen dann die 2D-Textur gemappt wird. Als Farbe wählen wir RGBA = 1, 1, 1, 1 – das bedeutet, die Textur wird nicht eingefärbt und unverändert dargestellt (die Farbe der Texturpixel wird mit der Farbe Weiß multipliziert), eine eventuell zuvor gesetzte Farbe wird so überschrieben.
247
5 Die dritte Dimension: 3D-Spiele
Abb. 5.6: Der 2D-Fighter schraubt sich nach oben.
Um den Fighter nach oben fliegen zu lassen, rufen wir die glTranslate()-Methode auf. Die z-Tiefe des Fighters haben wir auf –16 Einheiten festgesetzt – in dieser Tiefe entsprechen die Vertex-Punkte der Fläche in Abhängigkeit von der Perspektive am ehesten der Originalgröße der Grafik. Testweise können Sie diesen Wert natürlich verkleinern und so den Fighter weiter in die Tiefe des Raumes schicken. Da sich die Fläche bereits zentriert auf dem xy-Ursprung befindet, können wir das Modell direkt um die y-Achse rotieren lassen – die y-Achse des Modells entspricht bereits der y-Achse des Koordinatensystems.
5.6
Texturierung von 3D-Körpern Natürlich wollen wir für 3D-Spiele nicht nur mit texturierten 2D-Flächen arbeiten. Um dreidimensionale Körper zu erzeugen, setzt man normalerweise einen 3D-Editor ein, mit dem die benötigten Eckpunkte erzeugt bzw. modelliert werden können. Um das Prinzip zu verstehen, erzeugen wir aber zunächst programmatisch einen eigenen Körper. Der einfachste 3D-Körper besteht aus einem Würfel – und mit rechteckigen Flächen haben wir ja bereits Erfahrung.
248
5.6 Texturierung von 3D-Körpern
Abb. 5.7: Ein Würfel mit Kantenlänge 2 mittig im Koordinatensystem platziert.
Als Kantenlänge des Würfels wählen wir zwei Einheiten, sodass die Koordinaten – wenn wir den Würfel mittig auf dem Ursprung anlegen – von –1 bis 1 reichen. Der Würfel hat sechs Seiten (vorne, hinten, oben, unten, links, rechts) und benötigt daher pro Seite eine Textur. Als Textur wählen wir die Hintergrundgrafik aus unserem 2D-Shooter. Da die Texturkoordinaten für jede Seite des Würfels auf die gleiche quadratische Fläche gemappt werden sollen, müssen wir die Koordinaten im Array sechsmal wiederholen. Der Würfel verfügt damit über sechs Flächen mit je vier Eckpunkten, also insgesamt 24 Eckpunkte, bestehend aus je drei Koordinaten (xyz), sodass das Array eine Länge von 72 Elementen besitzt. Das Texturkoordinaten-Array verfügt dagegen ebenfalls über 72 Eckpunkte – zu jedem Eckpunkt gehört eine Texturkoordinate –, besitzt aber insgesamt nur 48 Elemente, da die z-Angabe für die Texturkoordinaten überflüssig ist. Um einen texturierten Cube zu rendern, können wir somit die folgende Methode einsetzen: - (void) drawCube { GLfloat vertices[ ] = { -1, -1, 1, //Vorne 1, -1, 1, -1, 1, 1, 1, 1, 1, -1, -1, -1, //Hinten
Diesmal lassen wir das Modell um alle drei Achsen gleichzeitig rotieren, müssen aber noch dafür sorgen, dass die Rückseiten des Modells nicht gerendert werden – wir wollen ja schließlich keine Rechenzeit verschwenden: Diese Technik nennt man Backface Culling, und wir können diese ganz leicht anwenden: glEnable(GL_CULL_FACE); glCullFace(GL_BACK);
Im ersten Schritt aktivieren wir das Culling, und im zweiten bestimmen wir die Rückseite, die nicht gerendert werden soll. GL_BACK bezieht sich hier auf die Rückseite des Würfels – die Rückseite ist ja bereits automatisch durch die Perspektive festgelegt. Alternativ können Sie auch die Vorderseite "cullen" – was aber praktisch in den wenigsten Fällen Sinn ergibt. GL_BACK ist übrigens ohnehin per Default eingestellt. Backface Culling kann aber auch unerwünscht sein: Erinnern Sie sich an den sich drehenden Fighter aus dem letzten Kapitel: Hier wollten wir sowohl Vorder- als auch Rückseite sehen. Deshalb deaktivieren wir den GL_CULL_FACE-Schalter am Ende der drawCubeMethode, um anderen Render-Methoden einen sauberen State zu liefern. Unser Würfel verfügt zwar über stattliche 72 Eckpunkte – dennoch lassen sich diese relativ gut durch Nachdenken selbst bestimmen. Anders sieht dies bei anderen geometrischen Grundformen aus: Eine Pyramide würden wir vielleicht auch noch hinbekommen, aber eine Kugel? Natürlich lässt sich eine Kugel mathematisch definieren, doch eine mathematisch perfekte Kugel gibt es in keiner 3D-Welt: Sie wissen ja, dass alle 3D-Modelle aus polygonalen Grundformen zusammengesetzt sein müssen. Insofern kann es nur eine Annäherung an die runde Form einer Kugel geben. Die Technik, mit der man Körper in Polygone zerlegt, nennt man Tesselation, wobei die entstandenen Polygone dann wiederum durch Triangulation in Dreiecke zerlegt werden müssen. Wenn Sie im Netz nach dem Stichwort „Tesselierung“ suchen, werden Sie verschiedene Algorithmen finden, mit denen sich programmseitig derartige Grundformen erzeugen lassen.
252
5.7 Es werde Licht Das Prinzip besteht dabei stets darin, eine Formel aufzustellen, die die Eckpunkte des Körpers liefert. Auch aus der Würfelform, die wir bereits erstellt haben, können wir durch Deformation beliebig viele weitere 3D-Objekte formen. Eine Art Rappel-Effekt erzeugen wir, wenn wir pro Aufruf der drawCube-Methode die Eckpunkte minimal verändern: for (int i = 0; i < verticesLength; i++) { vertices[i] += ([self getRndBetween: -1 and: 1] * 0.02); }
Im Ergebnis wackelt die Kiste dabei heftig hin und her, so als wollte gleich ein kleiner Kastenteufel herausspringen.2
5.7
Es werde Licht Vielleicht ist es Ihnen bereits aufgefallen – unser Würfel wird von allen Seiten gleichmäßig beleuchtet. Dass wir trotzdem die Würfelform erkennen, liegt lediglich an der Textur. Würden wir die Textur weglassen, sähen wir nur einen gleichförmigen, weißen Fleck mit sich verändernden Konturen auf dem Display. Damit wir die Würfelform auch ohne Textur erkennen können, müssen wir den Würfel beleuchten – durch die Schatten des Lichts können wir dann die Konturen erkennen. Natürlich ist Licht auch für texturierte Modelle hilfreich, da Licht- und Schatteneffekte für noch mehr Realismus sorgen. Neben Licht können Sie auch verschiedene Materialien angeben, die bestimmen, wie das Licht reflektiert werden soll. Für unsere Beispiele reicht dagegen die Verwendung einer Standard-Materialeigenschaft, die wir über den Flag GL_COLOR_MATERIAL dauerhaft aktivieren können. OpenGL ES unterstützt verschiedene Lichtarten (GL_AMBIENT, GL_DIFFUSE, GL_SPECULAR ...) und bis zu acht verschiedene Lichtquellen (GL_LIGHT0 – GL_LIGHT7). Pro Lichtquelle können Sie auch eine Farbe angeben, in der das Licht die Szenerie erleuchten soll – so können Sie einen weißen Würfel zum Beispiel gelb erscheinen lassen. Außerdem können Sie über einen Richtungsvektor den Ort der Lichtquelle bestimmen. Für unsere Beispiele genügt die Verwendung eines Standardlichtes: - (void) setLight { glEnable(GL_LIGHTING); //Licht ermoeglichen glEnable(GL_COLOR_MATERIAL); //Standard-Material-Reflexion glEnable(GL_LIGHT0); //Lichtquelle 0 einschalten (8 Quellen maximal) GLfloat color0[ ] = {1, 1, 1, 1}; //Farbe des Lichts glLightfv(GL_LIGHT0, GL_DIFFUSE, color0); //Art des Lichts = "diffus" GLfloat light_position0[ ] = {0, 1, 0, 0}; //Licht kommt von oben
2 Vielleicht haben Sie auch schon mal etwas von Skin-Deformation gehört. Dabei handelt es sich im Prinzip um das gleiche Verfahren, nur dass das Model nach bestimmten mathematischen Verfahren fortlaufend deformiert wird. Ansonsten würden 3DSpielfiguren ziemlich eckig aussehen: Skin Deformation bewirkt z.B. die realistische Veränderung eines Arm-Meshes, ohne den Arm dabei aus starren Grundformen zusammenzusetzen.
Die Methode rufen wir am Ende der setOGLProjection-Methode einmalig auf, damit die 3D-Szene dauerhaft erleuchtet wird. Im Richtungsvektor light_position0 haben wir x = 0, y = 1 und z = 0 gesetzt, sodass das Licht entlang der y-Achse nach unten scheint. Unsere Modelle werden also von oben erleuchtet. Die Lichtfarbe ist Weiß, damit wir die ursprüngliche Farbe der Modelle nicht verfälschen. Bevor wir nun texturierte oder untexturierte 3D-Modelle beleuchten können, gibt es noch ein weiteres Problem zu lösen. Denn noch funktioniert das Licht nicht so wie erwartet. Dies liegt daran, dass unsere 3D-Modelle bisher noch unvollständig sind. Wie das? Nun, OpenGL ES braucht, um eine Fläche beleuchten zu können, eine Angabe darüber, wohin diese Fläche zeigt. Genauer: Wir müssen pro Fläche einen Vektor angeben, der rechtwinklig von der Fläche weg zeigt. Erst dann kann der Lichteinfall auf unsere Szene realistisch berechnet werden. Diesen rechtwinkligen Vektor nennt man auch die Normale. Wenn dieser Vektor zusätzlich noch die Länge 1 hat, handelt es sich um eine normalisierte Normale. Sie wissen ja, drei Punkte bilden bereits eine Fläche – es wäre daher mathematisch für OpenGL ES kein Problem, den Normalenvektor anhand der Eckpunkte zu berechnen. Allerdings würde dies zu viel Performance verschlingen, sodass wir den Normalenvektor besser selbst angeben – und zwar wieder in Form eines Arrays. Aufgrund der einfachen Form unseres Würfels können wir die Normale pro Würfelseite durch Überlegung bestimmen: 0, 0, 1, //Vorne 0, 0, -1, //Hinten 0, 1, 0, //Oben 0, -1, 0, //Unten -1, 0, 0, //Links 1, 0, 0, //Rechts
Sie sehen, die Normale der Vorderseite zeigt auf der z-Achse eine Einheit nach vorne (0, 0, 1) usw. – für komplexere Modelle wird es dann schon schwieriger, die Normale zu finden. Glücklicherweise unterstützen alle Programme zum Modellieren von 3D-Objekten auch den Export der Normalen, sodass wir uns hier später keine weiteren Gedanken machen müssen. Um die Normale in den Render-Vorgang mit einzubeziehen, müssen wir – ähnlich den Texturkoordinaten – die Normale pro Eckpunkt angeben. Das heißt, wir stellen ein Normalen-Array mit 24 Vektoren bereit – bestehend aus je einer xyz-Angabe = 72 Elemente. Um das Normalen-Array zu aktivieren, müssen wir natürlich wieder erst mal einen Schalter umlegen und dann den Pointer auf das aktuelle Normalen-Array legen: glEnableClientState(GL_NORMAL_ARRAY); glEnable(GL_NORMALIZE); ... glNormalPointer(GL_FLOAT, 0, normals);
254
5.7 Es werde Licht Über die Konstante GL_NORMALIZE legen wir zusätzlich fest, dass alle Normalen automatisch normalisiert werden sollen, also auf die Einheitslänge 1 gebracht werden sollen, falls das nicht bereits der Fall sein sollte. Wenn man sich sicher ist, dass die Normalen bereits normalisiert sind, kann man aus Performance-Gründen diese Angabe auch weglassen – aber der Gewinn ist auch nicht übermäßig hoch, sodass wir den Schalter für unsere Beispiele ohne Bedenken umlegen können. Um nun einen nichttexturierten Würfel mit Normalen und blauer Farbe zu rendern, implementieren wir die folgende Methode: - (void) drawCube_noTexture { GLfloat vertices[ ] = { -1, -1, 1, //Vorne 1, -1, 1, -1, 1, 1, 1, 1, 1, -1, -1, -1, //Hinten -1, 1, -1, 1, -1, -1, 1, 1, -1, -1, 1, -1, 1,
Abb. 5.9: Rotierender, blauer Würfel ohne Textur mit Licht von oben
Sie sehen, bis auf die Tatsache, dass wir die Textur nun weggelassen und ein NormalenArray aktiviert haben, ist der Code gleich geblieben. Um auch texturierte Modelle zu beleuchten, müssen Sie natürlich ebenfalls die Normalen pro Eckpunkt des Modells angeben. Ab jetzt werden wir alle Modelle mit Normalen versehen – das Licht bleibt angeschaltet.
5.8
3D-Modelle erzeugen, laden und einbinden Um eigene 3D-Spiele entwickeln zu können, kommen Sie kaum um den Einsatz einer 3DModellierungssoftware herum. Natürlich können Sie alternativ selbst berechnete Grundformen, wie Quader, Pyramiden oder Kreise einsetzen, aber auch diese Grundformen lassen sich über eine externe Software wesentlich leichter erstellen und exportieren. Da OpenGL ES als reine Grafikschnittstelle fungiert, ist eine Importfunktion von 3DModellen nicht vorgesehen. Außerdem würde dadurch die Plattformunabhängigkeit eingeschränkt werden, da bestimmten Formaten der Vorzug gegeben würde – ganz davon abgesehen, dass das fehlerfreie und flexible Einlesen von Modelldaten zusätzliche Anforderungen an die API stellen würde. Warum also nicht die Arbeit auf die Entwickler verlagern? Da stehen wir also nun und müssen uns die folgenden Fragen stellen: Mit welcher Software soll ich die Modelle überhaupt modellieren? In welchem Format soll ich die Modelle exportieren? Wie kann ich das Format einlesen und nutzen?
257
5 Die dritte Dimension: 3D-Spiele Fangen wir mit der zweiten Frage an: Dies ist eine sehr kritische Frage, da heutige 3DProgramme äußerst komplexe und realistische Modelle erzeugen können – sodass das Parsen eines proprietären Formates viel Zeit in Anspruch nehmen kann. Eine bessere Alternative wäre ein frei verfügbares und offenes Format, das nicht allzu kompliziert aufgebaut sein sollte und von möglichst vielen 3D-Programmen unterstützt wird. Tatsächlich existiert ein solches Format, das zudem auf ASCII basiert: das Obj-Format. Ursprünglich von Wavefront Technologies entwickelt, unterstützt Obj eine Reihe fortgeschrittener Features – die wir allerdings nicht nutzen müssen. Stattdessen gehen wir pragmatisch vor und konzentrieren uns auf das, was wir bisher gelernt haben: Vertex-Arrays, Texturkoordinaten und Normal-Arrays. Damit wäre auch die erste Frage beantwortet – Sie können Ihre Modelle mit nahezu jedem Werkzeug Ihrer Wahl erstellen, vom kostenlosen Blender bis hin zu Maya oder 3ds Max: Jedes dieser Programme unterstützt *.obj-Files als Export. Alternativ können Sie gänzlich auf den Einsatz einer Modellierungssoftware verzichten und im Netz nach frei verwendbaren Obj-Modellen suchen – professionelle Game-Studios kaufen die eingesetzten Modelle oftmals ohnehin über kostenpflichtige Angebote ein. Faces Da wäre allerdings noch etwas. Bevor wir das Obj-Format einsetzen können, müssen wir noch klären, was unter Faces zu verstehen ist. Faces sind keine Besonderheit von Obj, sondern spielen auch in anderen Formaten eine Rolle. Im Prinzip geht es dabei darum, die Daten platzsparender zu speichern. Daher werden Faces glücklicherweise direkt von OpenGL ES unterstützt. Für den Würfel hatten wir 24 Eckpunkte definiert. Tatsächlich besteht der Würfel aber nur auch acht verschiedenen Eckpunkten – da pro Seite mehrmals dieselben Eckpunkte referenziert werden. Die Idee hinter den Faces ist nun, im Vertex-Array nur diese acht Eckpunkte abzulegen. Damit OpenGL ES trotzdem weiß, aus welchen Punkten ein Dreieck erzeugt werden soll, wird ein zweites Array verwendet, das Indices auf das VertexArrays enthält – und zwar pro Eckpunkt eines Dreiecks ein Index, der auf den Eckpunkt im Vertex-Array verweist. In der Praxis ist nicht immer nachzuweisen, dass die Performance durch Faces steigt, dies hängt unter anderem auch vom Modell selbst ab, aber zumindest sinkt der Speicherplatzverbrauch – insbesondere bei Modellen mit Tausenden von Eckpunkten.3 Um Faces in den Render-Vorgang einzubeziehen, gehen Sie wie bisher vor, das heißt, Sie übergeben das Vertex-Array als Pointer, rufen aber statt glDrawArrays(GL_TRIANGLE_STRIP, 0, verticesLength/3);
nun glDrawElements(GL_TRIANGLES, facesLength, GL_UNSIGNED_SHORT, faces);
5.8 3D-Modelle erzeugen, laden und einbinden auf, um die Faces zu rendern. Das faces-Array enthält die Indices der Dreiecke, und über die Konstante GL_TRIANGLES geben wir an, dass das Array pro Face drei Eckpunkte enthält (= Dreieck). Die glDrawElements()-Funktion erwartet daneben nur noch die Angabe der Länge des Arrays (facesLength) und den Datentyp. Achtung: Da das Array nur Indices enthält, sind nur die beiden Datentypkonstanten GL_UNSIGNED_BYTE oder GL_UNSIGNED_SHORT zugelassen. Ab jetzt werden wir nur noch mit Modellen arbeiten, die wir über ein externes Tool erstellt haben. Um die Modelle zu rendern, benötigen wir die folgenden Arrays: Modell ohne Textur: •
Vertex-Array
•
Normal-Array
•
Faces-Array
Modell mit Textur: •
Vertex-Array
•
Normal-Array
•
Texturkoordinaten-Array
•
Faces-Array
Aufbau eines .obj-Files Unser Ziel ist es, die obigen Array-Informationen zu erhalten. Sobald wir diese haben, können wir das Modell mit den bisher gezeigten Mitteln rendern – unabhängig davon, ob es sich dabei um ein Auto, ein Gebirge oder einen Tetris-Klotz handelt.4 Bevor wir den Aufbau eines Obj-Files analysieren können, müssen wir zunächst einmal ein einfaches Modell mit einem 3D-Tool erzeugen. Wir setzen hierfür 3ds Max von Autodesk ein, da dieses Programm mit am häufigsten für Spiele eingesetzt wird und viele kostenlose Programme in der Bedienung 3ds Max nachempfunden sind. Wir werden ohnehin nicht tiefer auf die Bedienung des Programms eingehen. Einfache Grundformen lassen sich ohne viel Vorwissen über "Create -> Standard Primitives" unproblematisch erzeugen. Um ein erstes Obj-File zu erzeugen, wählen wir einen "Cube" als Grundform. Diesen können Sie mit wenigen Mausklicks aufziehen. Achten Sie darauf, dass Sie den Würfel möglichst in der Mitte des Koordinatensystems positionieren.
4 Wenn Sie bereits Erfahrung mit einem anderen Format haben, können Sie natürlich auch dieses einsetzen. Sie sollten nur wissen, wie Sie an die benötigten Arrays gelangen.
259
5 Die dritte Dimension: 3D-Spiele
Abb. 5.10: 3ds Max mit Cube und OBJ Exporter
Danach wählen Sie "File -> Export" und als Dateityp "Wavefront Object (*.OBJ)". Nun sehen Sie ein kleines Fenster – den OBJ Exporter. Unter "Faces" geben Sie hier "Triangles" an – denn wir wollen die Faces als Triangles rendern. Machen Sie außerdem noch das Häkchen bei "Normals". Für die Anzahl der Nachkommastellen reicht uns die Genauigkeit von einer Nachkommastelle ("# of Digits: 1"). Sie sehen, dass Sie für jedes 3DModell automatisch Texturkoordinaten erzeugen können – die passgenaue Umwicklung des Objektes mit der Textur sollten Sie dennoch mit der jeweiligen Modellierungssoftware selbst vornehmen. An dieser Stelle verzichten wir jedoch darauf. Als Ergebnis erhalten Sie ein Text-File namens "cube.obj", das Sie auch unter "Resources -> gfx" im Beispielprojekt zu diesem Kapitel finden (Name: "OGL3D_obj_Models"). Listing cube.obj # Max2Obj Version 4.0 Mar 10th, 2001 # # object Box01 to come ... # v -7.5 -7.2 -7.8 v 7.3 -7.2 -7.8 v -7.5 7.6 -7.8 v 7.3 7.6 -7.8 v -7.5 -7.2 7.0 v 7.3 -7.2 7.0 v -7.5 7.6 7.0 v 7.3 7.6 7.0 # 8 vertices
260
5.8 3D-Modelle erzeugen, laden und einbinden
vn vn vn vn vn vn vn vn # 8 g f f f f f f f f f f f f #
Der Aufbau des Obj-Files ist unschwer erkennbar – vor jedem Eckpunkt sehen Sie ein Kürzel, das beschreibt, wofür der Eckpunkt steht. Wir verarbeiten die folgenden Kürzel: •
v – Vertex
•
vt – Texturkoordinate
•
vn – Normale
•
f – Face
Texturkoordinaten lassen sich auch mit drei Elementen pro Koordinate angeben, wobei der letzte Wert dann in der Regel 0 ist. Beim Verarbeiten der Texturkoordinaten müssen wir dies beim Setzen des Pointers natürlich berücksichtigen (drei statt zwei Komponenten pro Koordinate). Außerdem werden die Faces noch einmal unterteilt in:
Vertex-Face / Textur-Face / Normal-Face Fehlt einer dieser Werte, weil Sie zum Beispiel keine Texturkoordinaten exportiert haben, wird der Wert einfach weggelassen, Beispiel:
Vertex-Face / / Normal-Face
261
5 Die dritte Dimension: 3D-Spiele Da ein Face aus drei Eckpunkten besteht, müssen pro Face auch drei Indices angegeben werden. In unserem Obj-File besteht das erste Dreieck daher aus den Indices 1, 3, und 4 (f 1//1 3//3 4//4). Der Würfel besitzt 12 Faces, da die sechs Seiten aus je zwei Dreiecken zusammengesetzt sind (6 * 2 = 12). Wir interessieren uns nur für den Vertex-Index – falls die anderen Faces-Indices (Textur und Normals) ungleich dem Vertex-Array sind, können wir die glDrawElements()Funktion nicht mehr einsetzen. Diese geht davon aus, dass alle Faces-Elemente (Vertices, Texturkoordinaten, Normals) in gleicher Abfolge im Array angeordnet sind. Ist das nicht der Fall, müssen Sie auf die Faces verzichten und die glDrawArrays()-Funktion einsetzen – dies bedeutet aber auch, dass Sie zuvor alle Arrays (Vertices, Textur, Normals) erneut anhand der Indices zusammensetzen müssen. Achten Sie also besser beim Exportieren darauf, dass die Indices stets den gleichen Wert haben. Achtung: Das Obj-Format beginnt die Zählung der Indices bei 1 anstatt 0. Wenn wir die Daten des Obj-Files später einsetzen wollen, müssen wir darauf achten und den Index um 1 vermindern. Damit wir die im Obj-Format enthaltenen Arrays leichter im Quelltext einfügen können, haben wir unter der URL http://www.qioo.de/objformatter/ einen einfachen Formatierer ins Netz gestellt. Sie können natürlich auch Ihren eigenen Formatierer verwenden oder aber das Obj-Format direkt über das iOS SDK als File einlesen und parsen.
Abb. 5.11: Der Online-Formatter mit den Obj-Daten des exportierten Würfels
262
5.8 3D-Modelle erzeugen, laden und einbinden Die Verwendung des Skriptes ist selbsterklärend – pasten Sie einfach den ASCII-Text des Obj-Files in das Eingabefeld, und drücken Sie auf "Run formatter!". Danach erhalten Sie auf der Website den folgenden Output: The .obj-file has been formatted: 27.07.10 - 17:52:12 Formatted .obj: # Max2Obj Version 4.0 Mar 10th, 2001 # # object Box01 to come ... # Vertices: -7.5, -7.2, -7.8, 7.3, -7.2, -7.8, -7.5, 7.6, -7.8, 7.3, 7.6, -7.8, -7.5, 7.2, 7.0, 7.3, -7.2, 7.0, -7.5, 7.6, 7.0, 7.3, 7.6, 7.0, # 8 vertices Vertex normals: 0.0, 0.0, -1.6, 0.0, 0.0, -1.6, 0.0, 0.0, -1.6, 0.0, 0.0, -1.6, 0.0, 0.0, 1.6, 0.0, 0.0, 1.6, 0.0, 0.0, 1.6, 0.0, 0.0, 1.6, # 8 vertex normals g Box01 Faces (index starts with 0): 0, 2, 3, 3, 1, 0, 4, 5, 7, 7, 6, 4, 0, 1, 5, 5, 4, 0, 1, 3, 7, 7, 5, 1, 3, 2, 6, 6, 7, 3, 2, 0, 4, 4, 6, 2, # 12 faces Vertex x-Range from: -7.5 to: 7.3 Vertex y-Range from: -7.2 to: 7.6 Vertex z-Range from: -7.8 to: 7.0 Suggested radius for bounding sphere: 7.6333333333333 Warning: Supplied faces might be too complex. Supported format example: 22/22/22 7/7/7 10/10/10
Sie sehen, Vertices, Normals und Faces sind nun so angeordnet, dass wir diese per Copy & Paste ohne weitere Anpassungen in unseren Quelltexten verwenden können. Außerdem sorgt das Skript dafür, dass die Faces bei 0 beginnen (selbstverständlich geben wir nur den Vertex-Index der Faces aus – alles andere wird von OpenGL ES nicht unterstützt, und den Einsatz der glDrawArrays()-Funktion als Fallback wollen wir vermeiden, siehe oben). Auf diesen Umstand bezieht sich übrigens auch die obige Warnmeldung: Da die Texturkoordinate beim Exportieren fehlt, entspricht das Faces-Format nicht dem erwarteten Format, bei dem alle drei Indices (Vertex, Textur, Normale) gleich sein müssen. Das Skript prüft lediglich auf Gleichheit – fehlt eine Komponente, wird die gezeigte Warnung ausgegeben. Aber ein kurzer Blick in das Obj-File genügt, und wir stellen fest, dass tatsächlich Vertex und Normale stets auf den gleichen Index verweisen – also alles im grünen Bereich.
263
5 Die dritte Dimension: 3D-Spiele Zusätzlich liefert das Skript noch nützliche Angaben für die Erstellung von Bounding Boxen bzw. Bounding Spheres: Anhand der Vertices-Daten wird die maximale Entfernung der Eckpunkte zueinander in xyz-Richtung angegeben: Entlang der x-Achse reicht die Ausdehnung im obigen Beispiel von –7.5 bis 7.3, was einer Länge von 14.8 Einheiten entspricht. An den Kommastellen können Sie feststellen, dass wir den Cube im Editor manuell und daher nicht exakt mittig positioniert haben – diese leichte Ungenauigkeit ist vertretbar, da das Objekt später ohnehin frei verschoben werden kann. Anhand der Maximalwerte können wir einen Mittelwert bilden und so den Radius für eine umgebende Bounding Sphere vorschlagen: 7.6333333333333. Natürlich sind in der Praxis nicht alle Modelle so gleichmäßig aufgebaut wie unser Würfel, sodass der Radius nur einen ungefähren Wert liefern kann, der das Objekt nicht immer vollständig umschließt. Für komplexere 3D-Modelle kann es dennoch sinnvoll sein, mehrere Bounding Boxen/Spheres um das Objekt zu legen (zum Beispiel: Kopf, linker Arm, rechter Arm, Rumpf, Beine). Für die meisten Elemente eines Spiels sollten Sie jedoch mit einer Bounding Sphere oder einer Bounding Box gut auskommen. Aber zurück zu unserem Obj-File. Sehen wir uns an, ob wir alles richtig gemacht haben, und übertragen die Eckpunkte und Indices in unseren Quelltext: - (void) drawCube { //Daten stammen aus dem obj-File, //konvertiert mit www.qioo.de/objformatter/ GLfloat vertices[ ] = { //8 Eckpunkte -7.5, -7.2, -7.8, 7.3, -7.2, -7.8, -7.5, 7.6, -7.8, 7.3, 7.6, -7.8, -7.5, -7.2, 7.0, 7.3, -7.2, 7.0, -7.5, 7.6, 7.0, 7.3, 7.6, 7.0 }; GLfloat normals[ ] = { //8 Normals 0.0, 0.0, -1.6, 0.0, 0.0, -1.6, 0.0, 0.0, -1.6, 0.0, 0.0, -1.6, 0.0, 0.0, 1.6, 0.0, 0.0, 1.6, 0.0, 0.0, 1.6, 0.0, 0.0, 1.6 }; GLushort faces[ ] = { //12 Faces = 6 Wuerfelseiten mit je zwei Dreiecken 0, 2, 3, 3, 1, 0, 4, 5, 7, 7, 6, 4, 0, 1, 5, 5, 4, 0, 1, 3, 7, 7, 5, 1, 3, 2, 6, 6, 7, 3, 2, 0, 4, 4, 6, 2 }; //Anzahl der Array-Elemente //int verticesLength = sizeof(vertices) / sizeof(GLfloat); int facesLength = sizeof(faces) / sizeof(GLushort); //Render obj-Modell glEnableClientState(GL_NORMAL_ARRAY); //normal Arrays aktivieren glEnable(GL_NORMALIZE); glEnable(GL_CULL_FACE); //Nicht sichtbare Flaechen ignorieren glCullFace(GL_BACK);
Wie Sie sehen, unterscheidet sich der obige Code von den bisherigen Beispielen nur durch den Einsatz der glDrawElements()-Funktion, die auf die Faces zugreift (anstelle von glDrawArrays()), die wiederum auf die Vertices verweisen. Da wir das faces-Array direkt in der glDrawElements()-Funktion als Zeiger angeben, brauchen wir keine extra Pointer-Funktion für die Faces aufzurufen – so wie wir das noch immer für die Vertices und die Normals tun müssen. Damit hätten wir unser erstes 3D-Modell mit einem Editor erstellt, geladen und angezeigt. Wagen wir uns doch nun an komplexere Modelle heran: Das Standardbeispiel in diesem Bereich ist wohl der berühmte 3D-Teekessel, den Sie – wenn Sie Lust dazu haben – probeweise auch gerne einbinden können. Aber 3ds Max bietet noch eine ganze Reihe weiterer interessanter Formen, die Sie mit zwei bis drei Mausklicks erstellen können. Wir entscheiden uns für den "Torus Knot", den Sie unter "Create -> Extended Primitives" finden.
265
5 Die dritte Dimension: 3D-Spiele
Abb. 5.12: 3ds Max mit Torus Knot
Das passende Obj-File dazu finden Sie ebenfalls im Beispielprojekt unter "gfx". Das Modell besteht aus 1440 Vertices, 1440 Normals und 2880 Faces: - (void) drawTorusKnot { //Daten stammen aus dem obj-File //konvertiert mit www.qioo.de/objformatter/ GLfloat vertices[ ] = { 20.4, 10.9, -0.5, 21.1, 12.4, 1.0, 20.7, 14.3, 2.1, 19.5, 16.1, 2.5, 17.7, 17.4, 2.1, ... }; GLfloat normals[ ] = { 0.7, -0.7, -0.0, 0.9, -0.3, 0.4, 0.8, 0.1, 0.6, 0.5, 0.5, 0.7, 0.1, 0.8, 0.6, -0.4, ... }; GLushort faces[ ] = { 0, 12, 13, 0, 13, 1, 1, 13, 14, 1, 14, 2, 2, 14, 15, 2, 15, 3, 3, 15, 16, 3, 16, 4, 4, ... }; //Anzahl der Array-Elemente //int verticesLength = sizeof(vertices) / sizeof(GLfloat); int facesLength = sizeof(faces) / sizeof(GLushort); //Render obj-Modell glEnableClientState(GL_NORMAL_ARRAY); //normal Arrays aktivieren glEnable(GL_NORMALIZE);
Abb. 5.13: Cube und Torus Knot rotierend im iPhone-Simulator
5.9
Weitere 3D-Modelle mit Textur Im Hinblick auf ein erstes 3D-Spielprojekt legen wir noch zwei weitere 3D-Modelle an, diesmal exportieren wir auch die Texturkoordinaten mit.
267
5 Die dritte Dimension: 3D-Spiele
Abb. 5.14: asteroid.png, obstacle.png
Beim ersten Objekt handelt es sich um einen Asteroiden, das zweite Objekt ist eine Aufklärungseinheit einer gegnerischen Macht, die es später zu zerstören gilt. Als Textur verwenden wir je zwei Grafiken mit 128x128 Pixeln. Beide Objekte modellieren wir in 3ds Max und exportieren diese als Obj-File inklusive Texturkoordinaten, wie in den Screenshots dargestellt.
Abb. 5.15: 3ds Max mit Asteroid und Obstacle
Nachdem wir die Obj-Files über unseren Online-Formatierer in Form gebracht haben, binden wir die Daten in ein neues Projekt ein namens: "OGL3D_obj_withTexture".
268
5.9 Weitere 3D-Modelle mit Textur Dort legen wir zum Rendern beider Modelle je eine eigene Methode an, die wir in der playGame-Methode aufrufen: [self drawAsteroid]; [self drawObstacle]; Listing drawAsteroid-Methode - (void) drawAsteroid { Tex *tex = [[GameManager getInstance] getTex: @"asteroid.png" isImage: YES]; GLuint textureID = [tex getTextureID]; //Daten stammen aus dem obj-File, //konvertiert mit www.qioo.de/objformatter/ GLfloat vertices[ ] = { -0.207126, -0.066316, 17.863581, 23.264721, -1.502157, -0.192209, -0.207126, ... }; GLfloat textureCoords[ ] = { 0.511933, 0.499667, 0.000000, 0.753190, 0.915463, 0.000000, 0.054368, 0.673368, ... }; GLfloat normals[ ] = { 0.003697, -0.091516, 0.995797, 0.960635, -0.009128, 0.277662, 0.000000, 0.960620, ... }; GLushort faces[ ] = { 0, 6, 8, 56, 57, 30, 56, 30, 58, 58, 30, 59, 7, 1, 22, 57, 60, 30, 30, 60, 61, 30, 61, ... }; //Anzahl der Array-Elemente //int verticesLength = sizeof(vertices) / sizeof(GLfloat); int facesLength = sizeof(faces) / sizeof(GLushort); //Render obj-Modell glEnable(GL_TEXTURE_2D); //alle Flaechen werden nun texturiert glEnableClientState(GL_NORMAL_ARRAY); //normal Arrays aktivieren glEnable(GL_NORMALIZE); glEnable(GL_CULL_FACE); //Nicht sichtbare Flaechen ignorieren glCullFace(GL_BACK); glVertexPointer(3, GL_FLOAT, 0, vertices); glBindTexture(GL_TEXTURE_2D, textureID); glTexCoordPointer(3, GL_FLOAT, 0, textureCoords);
Natürlich macht es Sinn, einen Importierer zu schreiben, der das Obj-File direkt aus dem Ressourcen-Ordner einliest – doch das hieße, dass wir uns bereits auf das Obj-Format festlegen, und wir wollen hier stattdessen nur eine allgemeingültige Vorgehensweise zeigen. Nachdem wir nun die Daten der 3D-Modelle eingebunden haben und anzeigen, stellen wir fest, dass sich das Obstacle-Modell vor dem Asteroiden-Modell befindet. Beide Modelle sind unbewegt, rotieren jedoch um die xyz-Achsen. Wie wäre es, wenn wir durch beide Objekte hindurchfliegen könnten? Anstatt nun die Modelle per glTranslatef() auf den Betrachter zuzubewegen (dies würde optisch dem Hindurchfliegen entsprechen), wäre es eleganter, wenn wir die Kameraposition verändern könnten und uns mit der Kamera – gleichsam in Ego-Perspektive – vorwärts bewegen könnten.
5.10
Ego-Perspektive: Kamera erstellen und einsetzen Zu jeder vernünftigen 3D-API gehört die Implementierung einer Kamerafunktion. Auch für OpenGL existiert eine derartige Funktion – allerdings ausgelagert in die GLU-Library und die gluLookAt()-Methode. Der Grund dafür ist offensichtlich: Die Erstellung einer
271
5 Die dritte Dimension: 3D-Spiele Kamera erfolgt rein softwareseitig, und da OpenGL als Hardware-Schnittstelle angelegt ist, passt die Implementierung einer solchen Funktionalität nicht ganz ins Konzept. Hinzu kommt, dass unterschiedliche Implementierungen möglich sind, je nach konkretem Anwendungsfall kann dies Auswirkungen auf die Performance haben. Da für die mobile Variante von OpenGL die GLU-Bibliothek nicht verfügbar ist, müssen wir uns selbst um die Implementierung kümmern. Unter einer Kamera verstehen wir dabei gleichsam das Auge des Betrachters, das sich an verschiedenen Orten in unserer 3D-Welt befinden und in verschiedene Richtungen blicken kann. Wir müssen also zwei Kriterien erfüllen: 1. Die Kamera soll an jeder beliebigen xyz-Position aufgestellt werden können. 2. Ausgehend von dieser Position wollen wir die Blickrichtung in xyz-Richtung festlegen. Außerdem sollen die Position und die Blickrichtung pro Frame veränderbar sein, sodass wir die Kamera-Methode kontinuierlich mit verschiedenen Werten aufrufen können. Dadurch lassen sich alle für Spiele typischen Perspektiven verwirklichen – von der EgoPerspektive, bei der die Kamera dem Betrachter entspricht, bis hin zur Third-PersonPerspektive, bei der die Kamera etwas versetzt hinter dem Spielcharakter aufgestellt ist. Diese Anforderungen lassen sich mathematisch über Matrizenoperationen lösen – die Idee dahinter ist, die gesamte 3D-Szene so zu transformieren, dass sich der Koordinatenursprung verschiebt. Statt also die Kamera durch die Szene zu bewegen, werden die 3DObjekte in der Szene bewegt. Die Erstellung einer solchen Kamera ist ein typischer Anwendungsfall in der 3DMathematik, deshalb können Sie im Internet viele unterschiedliche Implementierungen finden. Wir orientieren uns hier an einem Algorithmus, der der freien Mesa 3D-GrafikBibliothek entlehnt ist (http://www.mesa3d.org/). Listing Kamera-Implementierung - (void) setCameraX: (GLfloat) cameraY: (GLfloat) cameraZ: (GLfloat) lookAtX: (GLfloat) lookAtY: (GLfloat) lookAtZ: (GLfloat)
camX camY camZ atX atY atZ {
glLoadIdentity(); GLfloat matrix[16]; //Die Multiplikationsmatrix //Vektoren GLfloat forward[3]; //Blickrichtung GLfloat upward[3]; //Rechtwinklig zur Blickrichtung GLfloat sideward[3]; //Rechtwinklig zur Ebene von forward und upward //Rotation der Kamera, Standardausrichtung ist "stehend"
Da wir die Vektoren mehrmals normalisieren müssen, haben wir noch die Hilfsfunktion normalizeVector: hinzugefügt. Beide Methoden fügen wir dem GameManager hinzu.
Abb. 5.16: Kamera mit den Vektoren forward, upward, sideward
Ausgehend von der Kameraposition und der Blickrichtung erhalten wir, wenn wir eine aufrechte Kameraposition annehmen (upward), eine Fläche, die von zwei Vektoren aufgespannt wird (forward und upward). Über das Kreuzprodukt finden wir den Vektor, der sich rechtwinklig zu dieser Ebene befindet (sideward). Nachdem alle drei Vektoren normalisiert sind, können wir schließlich die aktuelle Matrix mit der aus den drei Einheitsvektoren gebildeten Matrix über die glMultMatrix()-Funktion multiplizieren und entgegengesetzt zur aktuellen Kameraposition verschieben. Damit ist die Kamera aufgestellt bzw. die Welt aus der Sicht der Kamera an einen neuen Ort bewegt worden. Wie Sie bemerken, kommt hier einiges an Vektorrechnung zum Einsatz – die jeweiligen Stichwörter (Kreuzprodukt, Matrize, Einheitsvektor usw.) sind in jedem Mathe-Buch für die Oberstufe enthalten. Für uns ist an dieser Stelle nur wichtig, dass wir die Methode anwenden können. Der Einsatz der neuen Methode gestaltet sich zum Glück übersichtlich: - (void) playGame { static float z = 0;
274
5.11 Spaceflight: ein 3D-Spiel entsteht z -= 0.5; [self setCameraX: 0 cameraY: 0 cameraZ: z lookAtX: 0 lookAtY: 0 lookAtZ: z-1]; [self drawAsteroid]; [self drawObstacle]; }
Die Kamera-xyz-Werte bestimmen die Position, während wir über lookAtX: die Blickrichtung angeben – die xyz-Werte stellen einerseits einen Vektor dar, andererseits den Punkt im Raum, den das Auge der Betrachters fixiert. Über die playGame-Methode verändern wir kontinuierlich die z-Position der Kamera und fliegen so entlang der z-Achse in den Bildschirm hinein – die Blickrichtung bestimmen wir mit z-1, sodass wir stets entlang der Flugrichtung blicken. Es macht übrigens keinen Unterschied, ob Sie die Kamera vor oder nach dem Rendern der Objekte aufrufen. Die Kamera verschiebt die gesamte Welt in Relation zu sich selbst. "Welt" meint dabei das Koordinatensystem und alles, was sich bereits oder noch nicht darin befindet. lookAtY:lookAtZ:
Abb. 5.17: Screenshots: Flug durch Obstacle und Asteroid
5.11
Spaceflight: ein 3D-Spiel entsteht Die nötigen Zutaten für 3D-Spiele (Kamera, 3D-Modelle, Texturen, Transformationen) haben wir jetzt beisammen – sorgen wir also endlich wieder für Action! Unser nächstes 3D-Spieleprojekt soll unter anderem verdeutlichen, wie Sie einen Spieler in EgoPerspektive durch die 3D-Welt navigieren können. In der Welt sollen verschiedene Objekte platziert werden – einige davon können aufgesammelt werden, anderen muss man aus-
275
5 Die dritte Dimension: 3D-Spiele weichen, und wieder andere sollen per Laser-Beschuss zerstört werden. Mit diesem Spielkonzept können wir die Mechanismen aufzeigen, die für nahezu alle Arten von 3D-Spielen Gültigkeit besitzen: Positionierung von Elementen im dreidimensionalen Raum, Einsetzen einer Kamera und Kollisionsprüfung mit den Elementen der 3D-Welt. Also genau das, was wir bereits gelernt haben – nun kommt es nur noch auf das richtige Zusammenspiel an. Verlieren wir also keine Zeit und beginnen wir damit, zunächst den groben Rahmen für das Spiel zu definieren. Spielkonzept Spaceflight
Sie werden als Pilot eines Aufklärers mit der Aufgabe betreut, vor dem Mond von Jarson einen Asteroidengürtel von feindlichen Spionagesatelliten zu säubern. Ausgestattet ist Ihr Schiff mit unbegrenzter Feuerkraft. Vermeiden Sie Kollisionen mit den Asteroiden, aber auch mit den Satelliten. Ihr Schiff kann drei Treffer aushalten, danach ist Ihre Mission beendet. Wenn Sie Energiekapseln aufsammeln, können Sie den letzten Schaden an Ihrem Schiff ausbessern. Steuerung Das Spiel wird aus der Ego-Perspektive gesteuert. Der Bildschirm ist diagonal in vier Bereiche unterteilt, mit denen das Raumschiff durch Berührung des jeweiligen Bereiches nach oben, nach unten, nach links und nach rechts gesteuert werden kann. Die Bewegung erfolgt dabei ohne Drehung stets parallel zur xy-Achse. Das Raumschiff fliegt automatisch konstant nach vorne (entlang der negativen z-Achse). Spielelemente Player: Übernimmt die Steuerung der Kameraperspektive und die Darstellung des Cockpits als 2D-Billboard-Grafik, die parallel zur xy-Achse ausgerichtet ist. Verfügt über Dauerfeuer. Bullet: Laser-Geschosse, die in regelmäßigen Abständen vom Spieler links und rechts unterhalb seiner aktuellen Position abgefeuert werden. Die Flugrichtung ist parallel zur z-Achse. Asteroid: Werden zufällig im Raum kurz vor der zFar-Clipping Pane in Abhängigkeit der Spielerposition erzeugt. Alle Asteroiden bewegen sich auf der z-Achse dem Spieler entgegen. Spionagesatellit: Muss vom Spieler per Laser-Beschuss vernichtet werden. Energiekapsel: Kann aufgesammelt werden, um den letzten Schaden zu reparieren. Textmeldungen: Schadensmeldungen werden per Textmeldung angezeigt, ebenso wie weitere Meldungen zum Spielverlauf. Beginnen wir mit der Umsetzung: Wir bauen das Spiel auf der Basis des letzten Kapitels, sodass das Grundgerüst bereits über die Texturklasse, eine Kamera und die Texturen für Asteroid und Satellit verfügt.
276
5.11 Spaceflight: ein 3D-Spiel entsteht Die Laser-Geschosse modellieren wir mit 3ds Max aus einer gestreckten Pyramidengrundform ohne Textur. Für die Energiekapsel wählen wir die Grundform "Capsule". Wir exportieren das Modell ebenfalls im Obj-Format ohne Textur.
Abb. 5.18: 3ds Max mit Laser-Pyramide und Capsule
Die beiden Files bullet.obj und capsule.obj finden Sie bereits im Projektordner (das fertige Projekt trägt den Namen "Spaceflight"). Übrigens: Sie können, wenn Sie nicht mit 3ds Max arbeiten, die Obj-Files über fast jeden anderen 3D-Editor Ihrer Wahl importieren und dort bei Bedarf umgestalten – das ObjFormat enthält alle benötigten Daten, sodass ein beliebiger Editor damit zurechtkommen sollte. Unser Grundgerüst verfügt über die folgenden (bekannten) Klassen: MyGameAppDelegate MainView GameManager Tex
277
5 Die dritte Dimension: 3D-Spiele Neben der Sprite-Klasse, die wir als Oberklasse für alle 3D-Objekte einsetzen wollen, werden wir die folgenden davon abgeleiteten Klassen erstellen: Sprite: Elternklasse aller Sprites Player: Der Spieler Bullet: Laser-Geschoss Asteroid: Kugelförmiges, unzerstörbares Hinderniss Obstacle: Spionagesatellit Capsule: Energiekapsel Text: Alle Textmeldungen werden als Textur auf dem Display ausgegeben. Zur einfacheren Handhabung wird Text ebenfalls als Sprite realisiert. Damit ergeben sich die folgenden Sprite-Konstanten, die wir bereits im Header der ansonsten noch leeren Sprite-Klasse anlegen können. //Sprite-Typen enum types { PLAYER, ASTEROID, OBSTACLE, CAPSULE, BULLET, TEXT };
Außerdem fügen wir der Sprite-Klasse eine eigene Vertex-Struktur hinzu: struct Vertex { float x, y, z; }; typedef struct Vertex Vertex;
Der Grund dafür: Alle 3D-Sprites verfügen über eine Position (oder einen Richtungsvektor oder einen Rotationsvektor) im 3D-Raum – da wir hier nicht mehr mit der CGPointStruktur arbeiten können, legen wir uns kurzerhand eine Struktur für die xyz-Werte an. Ein neue Vertex-Struktur können wir dann beispielsweise so erzeugen: Vertex position; position.x = 20; position.y = 111; position.z = -45871;
Nach diesen Vorarbeiten beginnen wir nun damit, das Grundkonzept des Spieles über den GameManager zu implementieren. Danach werden wir die Sprite-Klassen mit Leben füllen. Zunächst vervollständigen wir den Manager anhand der Sprite-Verwaltung, die wir bereits für den 2D-OGL-Shooter verwendet haben – auch für die dritte Dimension lassen sich 3D-Objekte auf diese Art komfortabel verwalten: NSMutableArray *sprites; //Aktive Sprites NSMutableArray *newSprites; //Neue Sprites NSMutableArray *destroyableSprites; //Inaktive Sprites NSMutableDictionary* dictionary; //Ressourcen-Hashtable
278
5.11 Spaceflight: ein 3D-Spiel entsteht In der preloader-Methode laden wir die später verwendeten Texturen in den Speicher: - (void) preloader { sprites = [[NSMutableArray alloc] initWithCapacity:20]; newSprites = [[NSMutableArray alloc] initWithCapacity:20]; destroyableSprites = [[NSMutableArray alloc] initWithCapacity:20]; //Preload OGL-Textures [self getTex: @"asteroid.png" isImage: YES]; [self getTex: @"obstacle.png" isImage: YES]; [self getTex: @"cockpit.png" isImage: YES]; [self setOGLProjection]; state = LOAD_GAME; }
Und über die loadGame-Methode, die vor jedem Neustart eines Spieles aufgerufen wird, sorgen wir dafür, dass der Weltraum über einen Anfangszustand mit zufällig erzeugten Spielelementen verfügt: - (void) loadGame { [sprites removeAllObjects]; [newSprites removeAllObjects]; [destroyableSprites removeAllObjects]; [self createSprite: PLAYER]; //Startzustand for (int i=0; i<20; i++) { [self createSprite: ASTEROID]; } for (int i=0; i<10; i++) { [self createSprite: OBSTACLE]; } for (int i=0; i<5; i++) { [self createSprite: CAPSULE]; } }
Für dieses Spiel können wir eine recht einfach aufgebaute createSprite:Methodensignatur verwenden – die Angabe des zu erzeugenden Typs reicht bereits, da wir diesmal die Zuweisung der individuellen Eigenschaften über Setter/Getter nachträglich steuern. Parameter wie Position, Geschwindigkeit und Rotation werden einheitlich vor Initialisierung des Sprites zufällig erzeugt: - (id) createSprite: (int) type { Player *p = [self getPlayer]; Vertex posPlayer = [p getPos]; Vertex pos; pos.x = [self getRndBetween: posPlayer.x - zFar/4 and: posPlayer.x + zFar/4]; pos.y = [self getRndBetween: posPlayer.y - zFar/2
Weiterhin holen wir uns über eine getPlayer-Methode die aktuelle Spielerinstanz, die wir in der loadGame-Methode bereits angelegt haben: - (Player *) getPlayer { for (Sprite *sprite in sprites) { if ([sprite isActive]) { if ([sprite getType] == PLAYER) { return (Player *) sprite; } } } return nil; }
Wir benötigen die aktuelle Spielerinstanz innerhalb der createSprite:-Methode, um die aktuelle Position des Spielers festzustellen. Haben wir diese, erzeugen wir eine neue Vertex-Struktur mit zufälligen xyz-Werten (für die Erzeugung des Spielers selbst werden diese nicht benötigt – Sie wissen ja, Aufrufe auf nil sind in Objective-C erlaubt). Der z-Wert der Sprites soll am Ende des sichtbaren Bereichs liegen. Dieser ergibt sich aus der z-Position des Players und der zFar-Distanz plus einem zufälligen Bereich: pos.z = [self getRndBetween: posPlayer.z + zFar+10 and: posPlayer.z + zFar+50];
Ebenso verfahren wir für die xy-Position, wobei wir hier keine klare Begrenzung des Sichtfeldes haben und den Bereich proportional zu zFar nach beiden Seiten hin aufteilen.
281
5 Die dritte Dimension: 3D-Spiele Wenn Sie sich den Erzeugungsmechanismus der jeweiligen Sprites genauer ansehen, dann stellen Sie die folgenden Eigenschaften fest: Asteroid: Wird mit einer Geschwindigkeit von 20 Einheiten auf der z-Achse dem Spieler entgegen bewegt. Die Rotationsgeschwindigkeit liegt zwischen –10 und 10 Grad pro Frame. Satellit/Obstacle: Unbewegt. Rotationsgeschwindigkeit zwischen –5 und 5 Grad. Energiekapsel/Capsule: Bewegt sich mit zwei Einheiten entlang der z-Achse in der gleichen Richtung wie der Spieler. Um eine Kapsel zu erreichen, muss der Spieler also schneller als die Kapsel sein. Rotationsgeschwindigkeit: zwischen –10 und 10 Grad. Player: In der createSprite:-Methode werden keine Parameter für den Spieler gesetzt, da es ohnehin nur eine Player-Instanz gibt. Laser-Geschoss/Bullet: Die Anfangsposition befindet sich 40 Einheiten unter dem Spieler. Die Geschwindigkeit beträgt 100 Einheiten in der gleichen Richtung wie der Spieler entlang der z-Achse. Es werden stets zwei Geschosse abgefeuert, beide Geschosse sind auf der x-Achse jeweils 20 Einheiten links/rechts vom Mittelpunkt des Spielers entfernt. Das Touch-Handling erfolgt in der GameManager-Klasse, wie bei den bisherigen Spielen auch: Der Berührungspunkt wird an die setTouch:- und die touchEnded:-Methode der Player-Klasse weitergereicht. Das Spiel verfügt über drei States (LOAD_GAME, PLAY_GAME, GAME_OVER), die in der drawStatesWithFrame:-Methode wie folgt behandelt werden: - (void) drawStatesWithFrame: (CGRect) frame { W = frame.size.width; H = frame.size.height; switch (state) { case LOAD_GAME: [self loadGame]; [self createOGLText: @"GOOD LUCK" offsetX: 2 offsetY: 2 selfDestroy: YES]; state = PLAY_GAME; break; case PLAY_GAME: [self playGame]; break; case GAME_OVER: [self playGame]; break; default: NSLog(@"ERROR: Unbekannter Spielzustand: %i", state); break; } }
282
5.11 Spaceflight: ein 3D-Spiel entsteht Die playGame-Methode kümmert sich lediglich um die konstante Erzeugung neuer Sprites und setzt dabei den Modulo-Operator ein. Außerdem werden die bereits bekannten manageSprites- und renderSprites-Methoden aufgerufen: - (void) playGame { timer++; if (timer [self } if (timer [self } if (timer [self }
Der Viewport ändert sich mit der Änderung der Spielerposition, die wiederum der Kameraposition entspricht und damit die jeweilige Screen-Mitte repräsentiert: - (Vertex) getViewportOrigin { Vertex v = [[self getPlayer] getPos]; return v; }
Wir werden die getViewportOrigin-Methode später einsetzen, um beispielsweise Text mittig auf dem Display auszurichten. Die checkSprite-Methode kümmert sich wie bisher um die Kollisionsüberprüfung und außerdem um die Ausgabe von Textmeldungen (da diese ebenfalls als bewegtes Texturobjekt und damit als Sprite-Instanz realisiert werden): - (void) checkSprite: (Sprite *) sprite { Player *player = [self getPlayer]; if ([sprite getType] == ASTEROID || [sprite getType] == OBSTACLE) { if ([player checkColWithSprite: sprite]) { [self createOGLText: @"DAMAGED!!" offsetX: 2 offsetY: 1 selfDestroy: YES]; [sprite hit]; [player hit]; } } if ([sprite getType] == OBSTACLE) { for (Sprite *bullet in sprites) { if ([bullet getType] == BULLET) { if ([bullet checkColWithSprite: sprite]) {
Alle Asteroiden und Satelliten (Obstacle) werden mit dem Spieler auf Kollision geprüft, wobei die Kollision mit einer Energiekapsel (Capsule) separat geprüft wird. Alle BulletInstanzen werden wiederum nur mit allen Satelliten auf Kollision geprüft, da nur die Satelliten zerstörbar sein sollen. Zur Anzeige der Textmeldungen verwenden wir die neue createOGLText:offsetX:offsetY:selfDestroy:-Methode: - (id) createOGLText: (NSString *) txt offsetX: (float) x offsetY: (float) y selfDestroy: (bool) destroy { Text *text = [[Text alloc] init]; [text setText: offsetX: offsetY: selfDestroy:
Da wir die Textausgabe diesmal als Sprite realisiert haben, dient diese Methode lediglich als Helfer zur einfacheren Erzeugung der Textmeldungen. Die Text-Klasse ist von der Sprite-Klasse abgeleitet, da der Text in Abhängigkeit des Viewport-Origins auf dem Dis-
284
5.11 Spaceflight: ein 3D-Spiel entsteht play gerendert werden soll – dennoch handelt es bei Text um eine Textur ohne körperliche Ausdehnung, aber mit einer festen Position im Raum. Dies gibt uns optional die Möglichkeit, zusätzliche Texteffekte einzusetzen, zum Beispiel den Text langsam in der Tiefe des Raumes verschwinden zu lassen – wie zum Beispiel beim berühmten Star Wars-Intro. Damit wir die Text-Sprites leichter dem Release-Handling des Sprite-Managers überlassen können, sorgt der destroy-Parameter (wenn true) dafür, dass der Text nach einem in der Klasse definierten Timeout automatisch deaktiviert wird. Wir verfahren so bei allen Texten, mit einer Ausnahme: Die "GAME OVER"-Meldung werden wir dauerhaft anzeigen, so lange, bis der Spieler das Spiel neu startet – dabei werden über die loadGame-Methode dann ohnehin alle Sprites aus dem Speicher geräumt. Da wir das Text-Objekt als Textur erzeugen und nur die Texturbreite, nicht aber die tatsächliche Breite des Textes im 3D-Raum kennen, können wir die mittige Position des Textes über die offsetXY-Parameter nachjustieren.5 Um zu verhindern, dass eine Textmeldung von anderen Sprites verdeckt wird, sorgen wir zusätzlich in der renderSprites-Methode dafür, dass der Text erst nach den anderen Sprites gerendert wird – wie wir das bereits beim letzten Spiel für den Player durchgeführt haben. Beachten Sie, dass wir das Text-Objekt nur einmal erzeugen und dann der Sprite-Pipeline über die obige Methode zuführen müssen. Was genau in der Text-Klasse passiert, werden wir uns ansehen, wenn wir die Spielelemente (= Sprites) vorstellen. Die letzte Anpassung, die wir in der GameManager-Klasse vornehmen, bezieht sich auf die Hier fügen wir noch eine zweite Lichtquelle hinzu, um das Cockpit von hinten anzuleuchten:
Das Cockpit besteht nur aus einer Fläche ohne z-Ausdehnung – da die erste Lichtquelle konstant von oben auf die Szenerie scheint, wäre das Cockpit ansonsten kaum zu sehen. 3D-Sprites Wir haben an der GameManager-Klasse ja bereits gesehen, dass wir unsere 3D-Modelle für das Spaceflight-Spiel als Sprite-Objekte verwalten. Daher müssen wir die Sprite-Klasse nun für die Verwendung im 3D-Raum anpassen. Zunächst stellen wir die vollständige Klasse vor:
5 Die implementierte Textlösung zeigt nur einen prinzipiellen Ansatz, wie man Text mit OpenGL ES erzeugen kann. Für den häufigeren Einsatz werden Sie sicher lieber auf einzeln gerenderte Bitmap-Fonts (ebenfalls als Textur) ausweichen wollen und so beliebige Texte aus einzelnen Buchstaben zusammensetzen. Eine derartige Text-Render-Engine würde aber den Rahmen dieses Buches sprengen.
285
5 Die dritte Dimension: 3D-Spiele Listing Sprite.h #import #import #import "Tex.h" //Sprite-Typen enum types { PLAYER, ASTEROID, OBSTACLE, CAPSULE, BULLET, TEXT }; struct Vertex { float x, y, z; }; typedef struct Vertex Vertex; @interface Sprite : NSObject { GLuint textureID; //Textur-ID Vertex pos; //Aktuelle xyz-Position Vertex speed; //Bewegung pro Frame in xyz-Richtung Vertex rotation; //Rotation um xyz-Achse float angle; //aktueller Rotationswinkel float angleStep; //Aenderung des Winkels pro Frame float radius; //Radius, fuer Bounding Sphere int type; //Sprite-Typ bool active; //inaktive Sprites werden vom GameManager gelöscht bool autoDestroy; //!Toleranzbereiches -> active = false //Arrays mit den .obj-Daten GLfloat *vertices; GLfloat *textureCoords; GLfloat *normals; GLushort *faces; //Laenge der Arrays int verticesLength; int facesLength; } -
Die Klasse enthält wenig Neues – wir haben lediglich dafür gesorgt, diese an unsere neuen Anforderungen zur Verwaltung von 3D-Modellen anzupassen. Daher verfügen nun alle Sprites über eine Vertex-Struktur (wie bereits eingangs gezeigt), um die Position (pos), die Geschwindigkeit (speed) und die Rotationsachse (rotation) zu speichern. Zur Rotation gehört außerdem noch der aktuelle Winkel (angle) und der Wert, um den wir den Winkel pro Frame verändern (angleStep). Da wir den Render-Prozess der Textur diesmal in die Sprite-Klasse verlegen, speichern wir in der Klasse anstelle einer Tex-Instanz lediglich die Textur-ID. Zu einem 3D-Modell gehören neben einer Textur aber auch noch die vier Arrays für die Eckpunkte des Modells (vertices), die Texturkoordinaten (textureCoords), die Normalen (normals) und die Faces (faces). Wenn Sie sich die renderSprite-Methode ansehen, werden Sie feststellen, dass wir den Render-Prozess über zwei if-Abfragen etwas flexibler gestaltet haben – falls zum Beispiel ein Modell über keine Texturkoordinaten (Capsule, Bullet) oder keine Faces (Text) verfügt. Da wir mindestens eine Lichtquelle einsetzen, muss jedoch jedes Modell über ein normals-Array verfügen. Deshalb wird der Client-State GL_NORMAL_ARRAY, ebenso wie GL_VERTEX_ARRAY, in der setOGLProjection-Methode dauerhaft aktiviert. Die Transformation des Modells haben wir – analog zu unseren bisherigen Beispielen – in die move-Methode ausgelagert. Hier wird das Modell um sich selbst rotiert und in xyzRichtung verschoben. Außerdem prüft die Methode, ob ein Sprite deaktiviert werden kann, wenn es den sichtbaren Bereich verlässt. Da alle Sprites entweder unbewegt sind oder sich entlang der z-Achse auf den Spieler zu bewegen, vergleichen wir lediglich, ob sich das Sprite hinter der aktuellen z-Position des Spielers befindet (die zNear-Pane können wir hier vernachlässigen): if (autoDestroy) { Player * p = [[GameManager getInstance] getPlayer]; Vertex posPlayer = [p getPos]; if (pos.z < posPlayer.z) { active = false; } }
Um die Kollision zwischen den 3D-Modellen zu überprüfen, setzen wir diesmal eine Kugel <–> Kugel-Kollisionskontrolle ein. Den Algorithmus dafür haben wir bereits besprochen. Um die Prüfung durchzuführen, benötigen wir lediglich den Radius einer ge-
291
5 Die dritte Dimension: 3D-Spiele dachten Kugel um das Sprite herum und die aktuelle Position. Beim Radius handelt es sich um einen Näherungswert, den wir den Sprites anhand unseres Formatter-Skriptes individuell zuweisen werden. Abschließend sei noch auf die setVertices:size:setTexCoords:size:setNormals:size:setFaces:size:-Methode
hingewiesen. Wie wir gleich noch sehen werden, werden wir die Modelldaten in der jeweiligen Sprite-Unterklasse anlegen. Da es sich dabei um C-Arrays handelt, können wir nicht direkt die bereits im Header deklarierten Member initialisieren. Deshalb übergeben wir die maximal vier Arrays über diesen Setter, müssen dabei aber darauf achten, dass C-Arrays zwar als Call by Reference übergeben werden, die Setter-Methode jedoch lediglich einen Zeiger auf das erste Element des Arrays enthält. Die Länge des Arrays müssen wir also explizit mit übergeben (size). Über die malloc()-Funktion legen wir dann den Speicherbereich für die Sprite-Member fest und kopieren das in der Unterklasse nur lokal angelegte Array über die memcpy()-Funktion. Die Implementierung der Spielelemente Sehen wir uns anhand der Asteroid-Klasse ein erstes Beispiel an, wie Sprite- und Kindklasse zusammenarbeiten: Listing Asteroid.h #import "Sprite.h" @interface Asteroid : Sprite { } @end Listing Asteroid.m #import "Asteroid.h" #import "GameManager.h" @implementation Asteroid /* Vertex x-Range from: -23.678974 to: 23.264721 Vertex y-Range from: -21.068602 to: 21.969690 Vertex z-Range from: -20.450109 to: 17.863581 Suggested radius for bounding sphere: 22.032924333333 */ - (void) additionalSetup { type = ASTEROID; Tex *tex = [[GameManager getInstance] getTex: @"asteroid.png" isImage: YES];
Die Asteroid-Klasse legt in der einzigen Methode additionalSetup also lediglich den Typ, die Texture-ID und den Radius fest. Danach werden dann die vier Arrays mit Inhalt gefüllt, anhand der Daten aus dem Obj-File. Ebenso einfach ist die Obstacle-Klasse für den Satelliten aufgebaut: Listing Obstacle.h #import "Sprite.h" @interface Obstacle : Sprite { }
Für die Player-Klasse müssen wir dagegen noch etwas mehr Code implementieren. Einerseits müssen wir das Cockpit passgenau auf dem Screen platzieren, andererseits verarbeiten wir den Touch-Input und steuern darüber die Kamera. Listing Player.h #import "Sprite.h" @interface Player : Sprite { CGPoint touchPoint; bool touchAction; float speedOffset; int w, h; //Abmessungen Cockpit-Textur int hitCnt; //Anzahl der Beschaedigungen int timer; } - (void) setTouch: (CGPoint) p; - (void) touchEnded; - (void) hitCapsule; @end Listing Player.m #import "Player.h"
296
5.11 Spaceflight: ein 3D-Spiel entsteht #import "GameManager.h" @implementation Player - (void) additionalSetup { type = PLAYER; Tex *tex = [[GameManager getInstance] getTex: @"cockpit.png" isImage: YES]; textureID = [tex getTextureID]; radius = 15; speedOffset = 0; hitCnt = 0; //Nicht die Orginalgroesse nutzen, damit Textur direkt //vor der Kamera positioniert werden kann (sonst zu gross). //Und: Seitenverhaeltnis beibehalten, bei einer Hoehe von 1 w = [tex getWidth] / [tex getHeight]; h = 1; GLfloat verticesObjData[ ] = { //Cockpit 0, h, 0, //links oben w, h, 0, //rechts oben 0, 0, 0, //links unten w, 0, 0 //rechts unten }; GLfloat textureCoordsObjData[ ] = { 1, 0, 0, //rechts unten, Achtung: y-Achse zeigt nach oben 0, 0, 0, //links unten 1, 1, 0, //rechts oben 0, 1, 0 //links oben }; GLfloat normalsObjData[ ] = { 0, 0, 1, //Vorne 0, 0, 1, 0, 0, 1, 0, 0, 1 }; [self setVertices: size: setTexCoords: size: setNormals: size: setFaces: size:
Das Cockpit wird wie ein Billboard über eine 2D-Fläche gelegt, deshalb benötigen wir hier keine Obj-Daten. Außerdem geben wir keine Faces an. Damit das Cockpit angeleuchtet werden kann, dürfen wir die Normalen nicht vergessen, die pro Eckpunkt in Richtung des Betrachters zeigen (0, 0, 1). Die 512x512 Pixel große Cockpit-Textur wird mit einer Breite/Höhe von jeweils einer Einheit auf das 2D-Modell gelegt. Es macht hier keinen Sinn, die Originalabmessungen der Texturvorlage zu verwenden, diese wäre viel zu groß und müsste daher viel zu weit von der Kamera entfernt positioniert werden – und würde dadurch von Sprites, die sich weiter vorne auf der z-Achse befinden, verdeckt werden. Die Positionierung des Cockpits erledigen wir über die glTranslatef()-Funktion in der überschriebenen move()-Methode – ausgehend von der aktuellen Position des Spielers (pos = Origin). Pro Parameter verschieben wir das Cockpit minimal, um dieses deckungsgleich auf dem Display anzeigen zu können. Die Verschiebung hängt einerseits von der Größe des Modells (1x1), andererseits aber auch von der gewählten Perspektive (45°Blickwinkel) ab.
299
5 Die dritte Dimension: 3D-Spiele Steuerung Den Touch-Input (touchPoint) verarbeiten wir in der draw()-Methode der Player-Klasse. Wir verändern lediglich die xy-Position des Spielers und bewegen diesen stets parallel zur z-Achse nach vorne. Die Geschwindigkeit beträgt fünf Einheiten (und ist damit schneller als die Energiekapsel, wir könnten diese sonst nie einholen). Über den ansteigenden speedOffset-Parameter sorgen wir dafür, dass die Bewegung nicht allzu ruckartig erfolgt und erst mit der Dauer der Berührung an Geschwindigkeit zunimmt.
Abb. 5.19: Steuerungsübersicht mit den Bereichen UP, DOWN, LEFT, RIGHT. Links die Geradengleichungen und die Schnittpunktbestimmungen.
Gemäß unseres zuvor festgelegten Steuerungskonzeptes soll der Screen in vier Bereiche unterteilt werden (siehe Grafik). Wie stellen wir aber nun fest, in welchem Bereich der Touch-Point liegt? Hier greifen wir auf die Geradengleichung zurück:
y = mx + n Wobei m die Steigung darstellt und für die Diagonalen entweder den Wert 1 oder –1 hat. Der Schnittpunkt mit der y-Achse wird durch n markiert. Bitte nicht vergessen: Die y-Achse verläuft von oben nach unten – anders als in der Schule. Auch durch den Touch-Point können wir uns zwei gedachte Diagonalen vorstellen. Damit haben wir vier Geraden, wobei n jeweils bestimmt, ob die Gerade ober- oder unterhalb einer anderen Geraden verläuft. Dies ermöglicht es uns, durch Vergleich der vier n-Werte
300
5.11 Spaceflight: ein 3D-Spiel entsteht (n1, n2, np1, np2) den Bereich zu bestimmen, in dem der Touch-Point liegt. Dieses recht einfache Verfahren können Sie beispielsweise auch einsetzen, um einen 4-Wege-ArcadeJoystick zu simulieren. Ego-Perspektive Da die Kamera mit dem Betrachter deckungsgleich sein soll, verändern wir in der drawMethode ebenfalls die Kameraposition anhand der aktuellen Spielerposition: [[GameManager getInstance] setCameraX: pos.x cameraY: pos.y cameraZ: pos.z lookAtX: pos.x lookAtY: pos.y lookAtZ: pos.z+1];
Die Blickrichtung (lookAt) ist dabei stets parallel zur z-Achse, sodass die xy-Werte der aktuellen Position entsprechen und der z-Wert (pos.z+1) die Richtung bestimmt. Laser Über die aktuelle Position des Spielers wird gleichzeitig auch der Origin bestimmt, deshalb brauchen wir für die Erzeugung neuer Bullets über die createSprite:-Methode des GameManagers keine Position festzulegen. Über einen Timer bestimmen wir lediglich die Frequenz des Dauerfeuers. Schadensmeldung Schließlich speichern wir im hitCnt-Member der Player-Klasse die Anzahl der Kollisionen mit Asteroiden und Obstacles. Ist der kritische Wert erreicht, wechselt das Spiel in der hit-Methode in den GAME_OVER-Zustand, und eine entsprechende Textmeldung wird auf dem Display ausgegeben. Die hitCapsule-Methode sorgt dafür, dass Schäden auch ausgebessert werden können: Hierzu wird einfach der hitCnt-Parameter um 1 vermindert. Weitere Sprites Sehen wir uns nun noch die Bullet-Klasse an. Ebenso wie für die Energiekapseln werden hier keine Texturdaten benötigt, stattdessen geben wir die Farbe des Models über die setColor-Methode an. Da sich die Bullets ziemlich schnell vom Spieler fortbewegen, prüfen wir in der draw-Methode, ob sich die Bullets hinter der zFar-Clipping Pane befinden. Ist das der Fall, wird das Sprite-Objekt deaktiviert. Wenn Sie mehrere Sprites einsetzen, die sich vom Spieler fortbewegen, können Sie diese Prüfung natürlich auch allgemeingültiger in der Sprite-Klasse selbst vornehmen (siehe autoDestroy). Listing Bullet.h #import "Sprite.h" @interface Bullet : Sprite { } @end
5.11 Spaceflight: ein 3D-Spiel entsteht - (void) draw { Player * p = [[GameManager getInstance] getPlayer]; Vertex posPlayer = [p getPos]; if (pos.z > posPlayer.z + [[GameManager getInstance] getzFar]) { active = false; //Sprite außerhalb des Sichtfeldes } [super draw]; } @end
Damit hätten wir bis auf die Text-Klasse alle Sprite-Klassen vorgestellt. Diese wollen wir nun nachreichen: Listing Text.h #import "Sprite.h" @interface Text : Sprite { int timer; int w, h; //Abmessungen der Text-Textur float offsetX; //Abweichung vom Origin float offsetY; bool selfDestroy; //nach Ablauf des Timers wird das Objekt geloescht } - (void) setText: offsetX: offsetY: selfDestroy:
(NSString *) text (float) x (float) y (bool) destroy;
@end Listing Text.m #import "Text.h" #import "GameManager.h" @implementation Text //Hinweis: Die Schriftfarbe wird beim Erzeugen der Textur festgelegt - (void) additionalSetup { type = TEXT; } - (void) setText: offsetX: offsetY: selfDestroy:
(NSString *) text (float) x (float) y (bool) destroy {
offsetX = x; //Textur breiter als Text, daher manuelle Ausrichtung offsetY = y; selfDestroy = destroy; } - (void) move { //Text positionieren Vertex o = [[GameManager getInstance] getViewportOrigin]; glTranslatef(o.x - w + offsetX, o.y - h + offsetY, o.z + 10);
304
5.11 Spaceflight: ein 3D-Spiel entsteht if (selfDestroy) { timer++; if (timer > 30) { active = false; } } } @end
Auf die Funktionsweise der Klasse und deren Einsatz sind wir ja bereits eingegangen. Da es sich wie beim Cockpit um eine 2D-Fläche handelt, auf die die Text-Textur gemappt wird, geben wir die Objektdaten direkt an und benötigen keine Faces. Die Ausrichtung des Textes nehmen wir in der move()-Methode vor, ausgehend vom Origin und von den offsetX, offsetY-Parametern. Ist der Flag selfDestroy auf true gesetzt, wird in der ifAbfrage das Text-Objekt nach mehr als 30 Frames deaktiviert und damit vom SpriteManager aus dem Speicher gelöscht. Wie bereits erwähnt, können Sie Textmeldungen gleichen Inhalts erneut erzeugen, ohne die Textur neu generieren zu müssen. Diese wird über den Textur-Manager des GameManagers verwaltet. Dies bedeutet umgekehrt aber auch, dass Sie nicht benötigte Textbausteine explizit über [[GameManager getInstance] removeFromDictionary: @"Mein Text"];
löschen können. Somit wird dann auch die Textur entfernt und nicht nur das Text-Objekt.
Abb. 5.20: Screenshots des fertigen Spiels
305
6 6 Waiting round to be a millionaire 6.1
Das Tor zur Welt – iTunes Connect So weit, so gut: Wir wissen nun, wie man Spiele im Emulator zum Laufen bringt. Doch um ein Spiel hinaus in die Welt zu schicken (und darauf zu warten, dass man Millionär wird), bedarf es noch einiger weiterer Schritte, die allesamt von Apple vorgegeben sind und daher auch gut auf den Apple Developer-Seiten dokumentiert sind. Dabei liegt der Fokus auf den folgenden beiden Zielen:
Deployment auf einem Device
Distribution über den App Store
Voraussetzung für beide Aktivitäten ist der kostenpflichtige Zugang zum Apple Developer-Programm. Sie können diesen über die Startseite des Developer-Programms einrichten:
http://developer.apple.com/iphone/program/ Der Zugang kostet 99 $ pro Jahr (79 Euro) und muss nach Ablauf eines Jahres erneuert werden, ansonsten verfällt der Account, und die bereits eingestellten Spiele verschwinden aus dem App Store. Sobald Sie den Zugang eingerichtet haben, können Sie über das Apple Dev Center und iTunes Connect alle weiteren Schritte unternehmen. Unter anderem finden Sie hier auch die wöchentlich aktualisierten Verkaufszahlen pro Land pro Spiel.
307
6 Waiting round to be a millionaire
6.2
Testen, Testen, Testen: Aber wie kommt das Spiel auf mein Gerät? Selbstverständlich sollten Sie alle Spiele, die Sie im App Store veröffentlichen, vorher auch auf einem Gerät der iPhone-Familie getestet haben. Als schwächstes Glied der Kette empfiehlt sich hier das Ur-iPhone von 2007, das Sie auch heute noch gebraucht im Internet ersteigern können. Läuft das Spiel auf diesem Gerät, dann können Sie sich relativ sicher sein, dass das Programm auch auf allen anderen Modellen lauffähig ist. Als Basisversion können Sie über das Deployment Target eine möglichst niedrige iOS-Version wählen, um die Reichweite Ihres Spiels zu steigern. Bevor die Anwendung auf das Testgerät überspielt werden kann, müssen Sie die Anwendung signieren. Außerdem müssen Sie ebenfalls ein lokales Zertifikat auf Ihrem Rechner anlegen. Danach kann dann über Xcode die App an das per USB-Kabel angeschlossene Gerät überspielt werden. Die einzelnen Schritte sind recht umfangreich, werden aber über den „Development Provisioning Assistant“ gut aufbereitet. Apple stellt hier eine übersichtliche Step-by-Step-Anleitung bereit, die Sie unter
http://developer.apple.com/iphone/manage/overview/index.action erreichen können.
6.3
Release und Distribution Bevor das Spiel von Apple in den App Store eingestellt und damit weltweit vertrieben wird, müssen Sie Ihr Spiel durch den „Approval Process“ schicken. Hierbei wird die App von Apple-Mitarbeitern hinsichtlich Qualität, Funktionalität und Sicherheit getestet. Neben begleitenden Ressourcen wie Screenshots, Kontakt-Website und Beschreibungstext müssen Sie das Spielprojekt zuvor für den Release vorbereiten. Wechseln Sie die Xcode-Einstellungen für Ihr Projekt von „Debug“ auf „Release“, und testen Sie erneut auf einem Device. Hat alles geklappt, benötigen Sie ein weiteres Zertifikat. Das bisherige Developer-Zertifikat dient nur dem Deployment, das nun benötigte Distribution-Zertifikat erstellen Sie ebenfalls über das Provisioning Center unter
https://developer.apple.com/iphone/manage/distribution/index.action Wählen Sie nun zum Bauen den „Distribution“-Modus. Aber Achtung: Da Ihr Projekt nun nicht mehr lokal auf einem Device bereitgestellt wird, sondern ausschließlich über den App Store vertrieben werden soll, klicken Sie statt „Build and Go“ nur auf „Build“. Um das Build zu verifizieren, können Sie sich die Build Logs ansehen. Diese finden Sie, wenn Sie auf die Succeeded-Meldung am unteren rechten Rand klicken. Achten Sie darauf, dass die App mit dem Distributionszertifikat signiert wurde.
308
6.3 Release und Distribution Um nun das finale Binary zu erstellen, müssen Sie lediglich die App-Dateien im Build-Ordner zippen. Über iTunes Connect können Sie nun die App einstellen. Der eigentliche Upload-Vorgang des Binarys erfolgt seit Juli 2010 nicht mehr direkt über iTunes Connect. Stattdessen rufen Sie ein bereits installiertes Programm auf Ihrem Mac auf, den „Application Loader“. Diesen finden Sie (ab iPhone SDK 3.2) unter
/Developer/Applications/Utilities/Application Loader.app Diese Anwendung sorgt unter anderem dafür, dass das Binary bereits vor dem Upload validiert wird. Wichtig ist, dass die App in iTunes Connect vor Upload des Binarys den Status „Waiting for Upload“ erhalten hat.
Abb. 6.1: Der Weg zum Provisioning Portal und zu iTunes Connect führt über die Dev CenterStartseite (ein kostenpflichtiger Account ist Zugangsvoraussetzung).
309
6 Waiting round to be a millionaire
6.4
Marketing-Pläne? Nun, das wäre geschafft. Das Spiel hat den „Approval Process“ bestanden und kann über die App Store-Suche gefunden werden. Den passenden Suchwörtern kommt dabei entscheidende Bedeutung zu. Da die Keywords nur einmalig festgelegt werden können, sollten Sie hier so spezifisch wie möglich sein und gängige Phrasen eher vermeiden. Dies müssen aber nicht die einzigen Schritte bleiben, um die Aufmerksamkeit potenzieller Käufer zu erregen: Denken Sie über eine Lite-Version nach. Eine kostenlose App wird eher heruntergeladen und sorgt für einen höheren Bekanntheitsgrad. Alternativ können Sie eine kostenlose Version des Spiels erstellen, bei der beispielsweise alle weiteren Level über In-App-Purchases zugekauft werden können (In-App-Purchases werden ebenfalls vollständig über Apple bzw. den App Store abgewickelt).1 Der Preis einer App kann jederzeit angepasst werden: Während der Startphase können Sie das Spiel zu einem günstigeren Preis oder gar kostenlos anbieten, um mehr Bewertungen zu generieren. Auch ein anfänglicher 50%-Discount kann unentschlossene User zum Kauf verleiten. Natürlich können Sie auch den klassischen Weg über das Internet gehen und MarketingMaterialien an Webzines und Blogs schicken oder ein Video auf YouTube einstellen. Apple bietet drei Varianten zur Verlinkung Ihres Spiels an: iTunes Direct Link zu einer App mittels http://itunes.com/apps/appname iTunes Direct Link zu einer App mit Entwicklername über
http://itunes.com/apps/developername/appname iTunes Direct Link zu all Ihren Apps via http://itunes.com/apps/developername Falls iTunes nicht auf dem Rechner des Users installiert ist, öffnet sich stattdessen ein Link mit einer Installationsanleitung.
1 Erinnern Sie sich noch an den 3D-Klassiker Doom? Die ersten beiden Level konnten 1993 umsonst über das Internet heruntergeladen werden. 15 Millionen Nutzer taten dies, und 150.000 Nutzer kauften dann schließlich die Vollversion, dies entspricht zwar nur einer eher durchschnittlichen Konversionsrate von 1%, aber 150.000 Verkäufe waren dennoch genug, um John Romero, den Gründer von id software, zum Millionär zu machen.
310
7 7 Literaturverzeichnis [Aarnio et al.]
Tomi Aarnio, Ville Miettinen, Kari Pulli: Mobile 3D Graphics with OpenGL ES and M3G, Academic Press 2007.
[Apetri1]
Marius Apetri: 3D-Grafik Programmierung. Alle mathematischen Grundlagen. Von einfachen Rasteralgorithmen bis hin zu Landscape Generation, mitp 2007.
[Apetri2]
Marius Apetri: 3D-Grafik mit OpenGL, mitp 2010.
[Feldman]
Ari Feldman: Designing Arcade Computer Game Graphics, Wordware Publishing, Inc. 2000. Kostenlos online unter: http://wiki.yoyogames.com/index.php/Ari_Feldman%27s_Book_on_Ga me_Graphics
[Fournier et al.] Alain Fournier, Donald Fussell: On the power of the frame buffer, in: ACM Transactions on Graphics, S. 103 – 128, Volume 7, Issue 2, ACM 1988. [Gamma et. al.] Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides: Entwurfsmuster. Elemente wiederverwendbarer objektorientierter Software, Addison-Wesley 2009. [Kochan]
Stephen G. Kochan: Objective-C 2.0. Anwendungen entwickeln für Mac und iPhone, Addison-Wesley 2009.
311
7 Literaturverzeichnis [Lengyel]
Eric Lengyel: Math for 3D Game Programming & Computer Graphics, Course Technology Dezember 2003.
[Lobacher]
Patrick Lobacher: iPhone OS Webentwicklung. Professionelle Applikationen für WebKit-Browser, Open Source Press 2009.
[NFGMan]
NFGman: Character Design for Mobile Devices, Focal Press 2006.
[Reminder]
Wolfgang Reminder: Spieleprogrammierung mit Cocoa und OpenGL, SmartBooks Publishing AG 2009.
[Rideout]
Philip Rideout: iPhone 3D Programming. Developing Graphical Applications with OpenGL ES, O'Reilly Media 2010. Kostenlos online unter
MAC DEVELOPER Entwickler-Magazin für iPhone, iPad und Mac
Xcode
4/2010 September, Oktober, November
d 14,95
Deutschland d 14,95 Österreich, Belgien, Niederlande, Luxemburg d 16,45 Schweiz sfr 29,90
●
Grundlagen & individuelle Anpassungen
●
Debugging-Tipps aus der Praxis
●
Neue Werkzeuge in Xcode 4
S. 14
S. 30
iOS 4
●
iPhone 4
●
HTML5
●
Objective-C
●
Shop-Apps
●
Best Practices mit Xcode S. 36
Profi-CD: Test- und Demo-Versionen (Auswahl)
Special ●
E-Book „Become an Xcoder“ Deutschsprachiges Einsteigertutorial
Bonus-Artikel ●
Mit dem iPhone auf Netzwerke zugreifen
●
Google Maps in iPhone-Apps einbetten
Alles über iOS 4 und iPhone 4 Entwickler-Werkzeuge ● ●
Jabaco 1.5.2
●
KBasic
●
Mono Develop 2.4
●
●
jQTouch 1.0b2
●
UiUIKit Framework 2.1
04
Nicht jugendbeeinträchtigend
4 191741 414952
geprüft:
S. 38
Werbung integriert: Geld verdienen mit iAd
S. 100
Webanwendungen mit HTML5
Ul-Frameworks
DT-Control
Im Überblick: Neue APIs und Frameworks
●
Flash-Alternative: Animationen mit Canvas
●
Interaktiver Videoplayer selbst gebaut
S. 68
S.74
Expertenwissen Mac-Programmierung ●
Datenbankentwicklung mit Cocoa und MySQL
●
Multicore-Profi Grand Central Dispatch im Einsatz
S. 84 S. 92
U2_Aboanzeige_iphone&co_A4+4v3.qxd:Layout 1
09.08.2010
14:34 Uhr
Seite U2
Jetzt im Abo sichern! Bestellen Sie jetzt Ihr Vorzugs-Jahresabo mit 15% Rabatt gegenüber dem Preis im Einzelhandel, und Sie erhalten von uns als Willkommensgeschenk das edle Lederetui Vinga Pouch in Schwarz von Krusell im Wert von rund 20,– € für Ihr iPhone 3GS/3G.
& Schützen Sie Ihr iPhone 3GS/3G mit diesem edlen und funktionellen Etui. An der Verschlusslasche ist ein strapazierfähiges Nylonband angebracht, das die schnelle Entnahme des Geräts ermöglicht.
Ihre Vorteile:
(iPhone in Lieferumfang nicht enthalten)
• Sie erhalten die nächsten 5 Ausgaben von iPhone & Co und sparen 15%. • Sie erhalten mit jeder Ausgabe einen topaktuellen Datenträger. • Sie erhalten jede Ausgabe mit pünktlicher, bequemer Frei-Haus-Zustellung. • Sie erhalten als Willkommensgeschenk das edle Lederetui Vinga Pouch in Schwarz von Krusell.
Mehr unter:
www.macup.com/iphone-vinga
003_Edi_0410_ef_ea.qxp
10.08.2010
10:38 Uhr
Seite 3
Editorial
Im Westen viel Neues A
ntenne, iPhone 4 – und was noch? Den ganzen Sommer über schien es nur ein Thema zu geben. Auch die mac-developer geht darüber nicht hinweg (lesen Sie auf Seite 52 und 130). Den Ab- und Umsatzzahlen hat die zweifellos nicht perfekte Hardware jedoch nichts anhaben können. Eher im Gegenteil: „Bad news are good news“, lautet ein Grundsatz in der Medienpsychologie … quod erat demonstrandum. Für mich auch ein Zeichen dafür, dass es hauptsächlich die Innovationskraft der Software ist, die für den kommerziellen Erfolg des iPhones – iPod touch und iPad eingeschlossen – verantwortlich ist. Und dazu gibt es nicht zuletzt seit der diesjährigen Ausgabe der Leitkonferenz WWDC in San Francisco (Seite 10) jede Menge Berichtenswertes. Das neue Betriebssystem iOS 4 (Seite 38) inklusive der Apple-eigenen Werbesoftware für mobile Geräte iAd (Seite 100) wurde vorgestellt, die kommende
„Beste Voraussetzungen für mehr Apps“ Version 4 der Entwicklungsumgebung Xcode erstmals gezeigt (Seite 36). Alles in allem sind die Voraussetzungen für Entwickler, bald noch mehr erfolgreiche Apps produzieren und auf den Markt bringen zu können, bestens. Sie konnten nicht live in Kalifornien sein? Nun, auch Deutschland hat seinen – vielleicht sogar wilderen ;-) – Westen, und genau dort, in Köln, im Rheinland, findet Anfang Dezember wieder die iPhone Developer Conference statt (Seite 21). Viele der heißen Themen bekommen Sie auch dort präsentiert. Ich lade Sie jetzt schon ein, die Redaktion und viele unserer Autoren vor Ort zu treffen. Ein Hinweis noch in eigener Sache: Mit dieser Ausgabe, der vierten insgesamt, ist der erste Jahrgang der mac-developer komplett. Von Start weg sind wir auf ein großes Echo gestoßen und viele Leser forderten bald mehr! Wir sagen danke: Ab 2011, beginnend mit Heft 1 im November, wird Ihre mac-developer sechs Mal im Jahr erscheinen. Viel Spaß beim Lesen, heute und in Zukunft, wünscht
Technik & Trends Neues aus der Mac- und iPhone-Welt
10
WWDC-Rückschau Die Entwicklerkonferenz in San Francisco
12
Neue Mac-Bücher Ausgewählte Neuerscheinungen
Titelthema 14
Xcode für Individualisten Tricks und Anpassungen für eine effiziente Arbeitungsumgebung
25
Daten im Griff Xcode im Zusammenspiel mit Cocoa und dem iPhone SDK
30
Dem Fehler auf der Spur Debugging-Techniken und Expertentipps für die Fehlersuche in Xcode
36
Neue Werkzeuge und Ansichten Erster Blick auf Xcode 4
IOSDev 38
Neue APIs braucht die App Alle Änderungen in iOS 4 für Entwickler
44
Sprachhilfe für Umsteiger Die Syntax von Objective-C für Programmierer aus anderen Welten
52
Superstar ohne Empfang iPhone 4: Was Entwickler über die neue Hardware wissen müssen
59
Tipps & Tricks Versteckte Funktionen, unbekannte Shortcuts und nützliche Erweiterungen
WebDev 60
Graphenmeister Visualisierungen mit Graphviz für Mac-OS-X- und iPhone-Anwendungen
64
Ist Flash am Ende? Die Gründe für Apples Ablehnung
68
HTML5 bewegt Animationen mit dem Canvas-Element
72
Kosmetik fürs Web Websites für iPhone 4 und iPad anpassen
74
Videoplayer mit Extras Interaktiver Player mit dem Video-Element von HTML5
4
Titelthema: Xcode-Expertenwissen Xcode ist das tägliche Brot des Mac- und iPhone-Entwicklers. In der Titelstrecke stellt mac-developer die Entwicklungsumgebung von Apple im Detail vor, verrät Tricks und Funktionen, die Entwickler produktiver machen, und geht auf praktische Anwendungsszenarien wie das Debugging in Xcode ein. Lesen Sie alles über die neuen Werkzeuge in der kommenden Version Xcode 4, die alle Entwickler-Tools deutlich besser verzahnt und die AppleWelt einen großen Schritt nach vorn bringt. Ab Seite 14
iOS 4 für Entwickler Apple stellt in iOS 4 und dem iPhone SDK eine Fülle an neuen Programmierschnittstellen und Frameworks bereit. Die bekannteste Neuheit ist Multitasking. Außerdem dürfen App-Entwickler in iOS 4 auf neue Services aus den Bereichen Audio, Video und Navigation zugreifen. Der Workshop zeigt, wie sich diese Funktionen in bestehende und neue Anwendungen integrieren lassen. Seite 38
004_Inhalt_0410_ef_jp_ea.qxp
09.08.2010
16:25 Uhr
Seite 5
Inhalt mac-developer 4/2010 ●
Datenbankprogrammierung mit Objective-C MySQL ist eine gute Wahl für die Datenhaltung in Mac-Anwendungen. Wie Sie den Zugriff auf die Datenbank mit Objective-C und Cocoa steuern und welche Module Sie einbinden müssen, verrät dieser Workshop. Seite 84
MacDev 78 Filemaker und Konsorten Übersicht: Datenbank-Alternativen für alle Einsatzgebiete 84 MySQL für Mac-Programme Datenbankzugriff mit Objective-C in der Praxis 88 Auf Kommando Open-Source-Schatz: Freie Software mit MacPorts installieren 92 Abarbeitung in Blöcken Grand Central Dispatch für die MulticoreEntwicklung einsetzen
HTML5 für Animationen und Videos Die HTML5-Elemente Canvas und Video sind ressourcenschonende Flash-Alternativen für mobile Apps. Zwei Workshops stellen HTML5-Lösungen für Animationen und einen interaktiven Videoplayer vor. Seiten 68 und 74
iPad als lukrativer Verkaufskanal Mit Shopping-Apps für das iPad erschließen Händler neue Zielgruppen und haben die Chance, mit neuen Verkaufskonzepten zu punkten. Wie Anbieter vom iPad als Vertriebskanal profitieren, lesen Sie auf Seite 105
97 Projekte und Lebenszyklen Tools Corner: Nützliche Software für die Projektverwaltung im Team
Mobile Business 100 Zukunftsmarkt mit Fragezeichen Was beim Einstieg in Apples Werbeprogramm iAd zu beachten ist 105 Angebot überschaubar – noch Mit Shopping-Apps auf dem iPad neue Kunden erschließen 108 Suchfunktion deluxe Der besondere App-Tipp: FACT-Finder für die Suche und Navigation in Webshops
BackGround 112 Kategorische Erweiterung Objective-C-Grundlagen: Strukturen und Funktionserweiterungen für Klassen 118 Beschleunigungsfaktor Modularisierung durch komponentenbasierte Entwicklung
Highlights der Heft-CD Ser vice ❚ E-Book (rund 80 Seiten): „Become an Xcoder“
Deutschsprachiges Einsteigertutorial ❚ Bonusartikel „Mit dem iPhone auf Netzwerke zugreifen“ und „Google
Maps in iPhone-Apps einbinden“ (Buchauszüge von Addison-Wesley) ❚ Entwickler-Werkzeuge: Jabaco 1.5.2, KBasic, Mono Develop 2.4,
Mono Framework 2.6.7, Mono Touch 3.0.11, Netbeans 6.9 und mehr ❚ UI-Frameworks: jQTouch 1.0b2, UiUIKit Framework 2.1,
Kurzmeldungen HP BSMobile 1.0: BusinessAnwendungen per iPhone steuern Mit einer kostenlosen App von HP haben iPhone-Anwender ihr Business Service Management im Griff. Die App „HP BSMobile“ spricht Unternehmen an, die HP BAC 8.0 oder HP BSM 9.0 im Einsatz haben. Anwender steuern mit dem iPhone unternehmenskritische Anwendungen und haben alle Monitoring-Daten im Blick. Sie stellen sicher, dass die Verfügbarkeit gewährleistet ist, und lösen Performance-Knoten. Die HPApp macht auch Gebrauch von den Funktionen des iPhones. Benachrichtigungen lassen sich per E-Mail oder SMS auf das iPhone schicken, sobald ein Problem mit einer Anwendung auftritt. Ist das Problem nur vor Ort zu lösen oder ist eine bestimmte Abteilung zuständig, versendet der iPhoneUser eine Nachricht mit allen nötigen Informationen. 3 HP BSMobile • www.hp.com
Apple TV: iOS und Apps? Angeblich will Apple seiner Set-Top-Box für Film-, Serien- und Musik-Streams mit neuen Preisen und Apps auf die Sprünge helfen. Das Nachrichtenportal Newteevee berichtet, dass Apple den Preis deutlich senken will: Pro Folge sollen Zuschauer nur noch 99 US-Cent zahlen. Außerdem überarbeitet der Hersteller offenbar seine Set-Top-Box. Beim neuen Modell schrumpft Apple laut Newteevee den internen Speicher von bislang 160 GByte auf 16 GByte. Dafür fällt auch der Preis für Apple TV auf 100 Dollar. Den Berichten zufolge wird Apple TV auf auf iOS umsteigen und damit grundsätzlich auch Apps für die Set-TopBox ermöglichen. 3 Apple TV • www.apple.com/de/appletv
Buffalo Dualie jetzt für iPhone 4 Buffalo hat seine Dockingstation Dualie aktualisiert. Der Doppelpack aus Backup-Festplatte für den Mac oder PC und einer iPhone/iPod-Docking-Station ist jetzt mit dem iPhone 4 kompatibel. Dualie besitzt einen Steckplatz für ein iPhone oder einen iPod und trägt huckepack eine USB-Festplatte, auf der Backups des Rechners angelegt werden können. iPhone und iPod lassen sich im Dock laden und synchronisieren. Dualie kostet 170 Euro. Im Paket ist bereits eine für den Mac vorformatierte 500-GByteFestplatte im 2,5-Zoll-Format mit USB-2.0und Firewire-400/800-Ports enthalten. 3 Dualie • www.dualie.de
6
Neue Mac-Desktops
Mehr Leistung Der aktuelle Mac Pro rechnet jetzt mit den neuesten Quad-Coresowie Hexa-Core-Intel-Xeon-Prozessoren und kann in den stärksten Ausführungen mit zwei Prozessoren auf bis zu zwölf Prozessorkerne zurückgreifen. Mit einem neuen ATI-Grafikchip bringt es der Mac Pro auf bis zu 50 Prozent mehr Grafikleistung gegenüber der Vorgängergeneration. Außerdem lassen sich die Desktops mit bis zu vier Solid State Drives (SSD) zu je 512 GByte Kapazität bestücken. Der All-in-One-Rechner iMac bekommt ebenfalls neuere Prozessoren der Intel-Core-i7-, -i5- und -i3-Reihe mit bis zu 3,6 GHz Taktrate spendiert. Für die Grafik ist jetzt eine ATI Radeon HD 5750 zuständig. Den neuen Mac Pro und iMac liegt standardmäßig zwar eine Magic Mouse bei, optional kann aber für 69 Euro auch ein neues Multitouch-Trackpad namens Magic Trackpad dazubestellt werden.
Upgrade: Bessere Prozessoren, Ladegerät für MacBooks im Display Zu guter Letzt präsentiert Apple noch ein 27-Zoll-LED-Cinema-Display mit 2560 x 1440 Pixel Auflösung, das ebenfalls mit einer Neuheit überrascht: einem eingebauten MagSafe-Ladegerät. Damit eignet sich der Monitor nicht nur als Ergänzung zu den neuen Mac Pro, sondern auch für Apple-Notebooks im stationären Betrieb. Eingebaut sind außerdem USB-Anschlüsse, eine iSight-Videokamera, ein Mikrofon und ein Lautsprechersystem. 3 Apple • www.apple.com/de
Filemaker Go
Datenbank-Zugriff Filemaker bietet zwei neue Apps im App Store an: Filemaker Go for iPhone und Filemaker Go for iPad erlauben den mobilen Datenbankzugriff mit dem Smartphone oder Tablet. Anwender können die Daten nicht nur abrufen und einsehen, sondern auch unterwegs bearbeiten. Alle Änderungen werden sofort auf dem Server aktualisiert. Auf den mobilen Geräten stehen Suchfunktionen, Sortiermöglichkeiten und verschiedene Ansichten wie Listen-, Formular- und Tabellendarstellung zur Auswahl. Auf beiden Geräten können die Datenbanken im Hoch- oder im Querformat betrachtet werden. Mobile Nutzer haben über WLAN oder 3G Zugang zu Datenbanken, die mit Filemaker Pro erstellt und von Filemaker Server oder Filemaker Pro auf einem Mac oder PC gehostet werden. Filemaker Go for iPhone und Filemaker Go for iPad sind mit allen Filemaker-Datenbanken ab Ver-
Filemaker Go erlaubt mobilen Zugriff auf Datenbanken sion 7 kompatibel. Unterwegs stehen viele der gewohnten Datenbankfunktionen zur Verfügung, darunter Tab Control, Web Viewer sowie externe SQL-Datenbankquellen, Container, Portale und Skripts. Filemaker Go für iPhone und iPod Touch benötigt iOS 4 und kostet 16 Euro. Filemaker Go für das iPad mit iOS 3.2 gibt es für 32 Euro. 3 Filemaker • www.filemaker.de
mac-developer 4/2010
006_News1_ef_mjh_ef_ea.qxp
09.08.2010
16:40 Uhr
Seite 7
Mac Insider News ●
iTunes Connect Mobile
iPhone Developer Conference 2010 bietet über 50 Vorträge an zwei Tagen
Verkäufe im Blick
Entwickler trifft Manager
iTC Mobile gibt Entwicklern unterwegs Auskunft darüber, wie sich ihre Apps im App Store schlagen. App-Anbieter rufen damit Statistiken der Downloads und Umsätze ab, filtern nach Zeitraum, Art des Downloads wie beispielsweise bezahlte Apps, Gratis-Apps oder kostenlose Updates und sehen sich die Entwicklung der In-App-Käufe an. Die Daten lassen sich auch grafisch auf dem iPhone darstellen, um Trends abzulesen, und nach verschiedenen Märkten aufschlüsseln. Die App iTunes Connect Mobile steht kostenlos im App Store zum Download bereit. Ein Wartungsupdate auf Version 1.0.1 bereinigt bereits einen Fehler, durch den sich einige Entwickler nicht einloggen konnten. iTC Mobile: Statisti3 Apple • ken für App-Anbieter www.apple.com
Drei Jahre ist das iPhone auf ihrem Motto „Die Entwicklerdem Markt, und bereits zum und Business-Konferenz für dritten Mal in Folge treffen iPhone, iPad und iPod touch“ sich zum Jahresausklang auf teilt sich die Veranstaltung in der iPhone Developer Confeausgewiesene Entwicklerrence, der größten deutschund Business-Tracks: Über die sprachigen Veranstaltung in beiden Tage verteilen sich jediesem Themenbereich, iPhoweils drei Themenstränge mit ne-, iPad- und Mac-Enthusiaüber 50 Vorträgen von rund 40 sten, um sich über aktuelle Referenten, dazu kommt ein Trends und Technologien zu offenes Expertenforum. Proinformieren. Auch der Stadt grammpunkte wie Best PracKöln bleibt der Veranstalter, tices mit iOS und Tools und die Neue Mediengesellschaft „Auf nach Köln“, heißt es Methoden der Profis richten Ulm Messe und Kongresse, für iPhone- und iPad-Ensich eher an fortgeschrittene nach dem positiven Feedback thusiasten im Dezember Programmierer, doch kom2009 treu, doch erfordert das men am App Starter Day diesWachstum von Programm und Ausstelmal auch Einsteiger auf ihre Kosten. lung den Umzug vom edlen Hotel im Die Konferenz-Site steht bereits online, Wasserturm ins eher volksnahe und weauf der Website finden Sie auch alle wichsentlich größere Veranstaltungszentrum tigen Informationen zur Anmeldung und Gürzenich. Programm-Updates. Bis zum 13. Oktober Am bewährten Programmkonzept aus gewährt der Veranstalter 100 Euro Frühden beiden Vorjahren kann sich der Besubucherrabatt. cher auch 2010 wieder orientieren: Gemäß 3 www.iphonedevcon.de
Toshiba Canvio
Google App Inventor
Mac-OS-X-Backups
App-Baukasten
Toshiba hat seine Canvio-Reihe jetzt für den Mac herausgebracht. Die USB-2.0Festplatte kommt in Größen mit 500 GByte, 750 GByte und 1 TByte. Auf dem Laufwerk befindet sich eine Backup-Software für Mac OS X. Dabei handelt es sich um die leicht zu bedienende Lösung NTI Shadow 5 for Mac. Die mit Canvio for Mac gesicherten Daten sind passwortgeschützt mit 256-Bit-Verschlüsselung vor fremden Augen verborgen. Die Toshiba-Festplatte ist mit einem Shock-Sensor ausgerüstet, um Stöße oder einen Fall zu erkennen und Schäden am Laufwerk zu vermeiden. Die Festplatten gibt es vorerst nur in den USA zu Preisen von 100, 120 beziehungsweise 140 Dollar. 3 Toshiba Storage • http://sdd.toshiBackup-Spezialist: ba.com Toshiba Canvio for Mac
App Inventor for Android ist ein neues Baukastensystem von Google, das die wichtigsten Funktionen für eigene mobile Anwendungen als Module zum einfachen Zusammensetzen bereitstellt. Damit soll jeder Anwender mit ein paar Klicks eine Android-App entwickeln können, ohne sich mit Code zu beschäftigen. Google hat ein Jahr lang an App Inventor gebastelt, jetzt wurde die Anwendung für alle User freigegeben. Die Möglichkeiten von App Inventor sind natürlich durch die Module des Baukastens beschränkt, versetzen jedoch Nichtprogrammierer erstmals in die Lage, eigene Handy-Software zu entwickeln. Harold Abelson, der App Inventor entwickelt hat, gibt zu, dass die Anwendungen vielleicht nicht die schönsten Programme der Welt sind. Dafür lassen sich Informationen einfach durch das Zusammenschieben von Icons verknüpfen, per Drag and Drop Funktionen auslösen und Eingabeblöcke und Buttons zusammensetzen.
4/2010 www.mac-developer.de
Die Software kann auf Dienste im Web zugreifen, Informationen auf dem Smartphone speichern und verschiedene Kommunikationswege nutzen. Mit App Inventor bauen Anwender beispielsweise Twitter-Apps, Quizanwendungen oder SMS-Benachrichtigungsdienste. Außerdem kann der Anwender auf Sensoren des Smartphones zugreifen und damit auch GPS-Standortdaten, die Bewegungssensoren und Ähnliches nutzen. 3 Google • http://appinventor.googlelabs.com
Baukastensystem: Google App Inventor
7
006_News1_ef_mjh_ef_ea.qxp
09.08.2010
16:40 Uhr
Seite 8
Mac Insider News ●
Web-Fundstücke Marketing für App-Entwickler App-Developer haben alle oft dasselbe Problem: Wie kommt mein brandneues Produkt zu meinen potenziellen Kunden? Wie gut, dass die Macher von Appentwickler.de genau zu dieser Frage die passenden Antworten parat zu haben scheinen. Ob Marketingplan, Presseverteiler, Blogverzeichnis oder Twitterund Facebooks-Tipps – hier sollten Entwickler die passende Unterstützung und Hilfe finden. Und App-Reviews und App-Boosting sollen den Bekanntheitsgrad der eigenen Apps weiter erhöhen. 3 App Entwicker • http://appentwickler.de
Trotz iPad
Safari 5
Macs sind gefragt
Neue Entwickler-Tools
Apple verzeichnet beim Mac-Absatz in den USA im Mai ein 37-prozentiges Plus im Vergleich zum Vorjahr. Im April betrug der Zuwachs 39 Prozent. Die befürchteten Einbußen durch das iPad, auf das viele Anwender umsteigen könnten, fallen damit sehr gering aus. Zu diesem Schluss kommt Gene Munster von Piper Jaffray mit Marktdaten der NPD Group. „Apple hat die iPad-Funktionalität erfolgreich auf den Konsum von Inhalten eingeschränkt, im Gegensatz zur Generierung von Inhalten mit dem Mac“, sagt Munster. Der Analyst geht davon aus, dass im zweiten Quartal 3,1 bis 3,2 Millionen Macs über die Ladentheken gehen. Neue Modelle und Upgrades von Produkten wie dem MacBook und dem Mac mini sorgen für ein stabiles Wachstum beim Mac-Absatz. 3 Piper Jaffray • www.piperjaffray.com
iPad-Blog für Liebhaber Wer sich dieser Tage auf die Suche nach einem guten iPad-Blog mit regelmäßig neuen Beiträgen macht, wird ziemlich schnell enttäuscht sein. So groß der Hype um das Apple-Tablet ist, so wenig und vor allem so wenig fundiert wird darüber gebloggt. Anders auf iPad-Mag.de: Dort findet man tägliche Updates aus der Welt des Tablets und nicht nur News, sondern auch App-Reviews, kleinere Workshops und eine Top-10-App-Liste für das Apple iPad. Und der Twitter-Stream ist auch nicht ohne. 3 iPad Mag • www.ipad-mag.de
Neue Modelle des Mac mini kurbeln das Interesse an Mac-Systemen an
Apple lässt Erweiterungen für Safari zu Mit Webtechnologien wie HTML5, CSS und Javascript können Entwickler ab sofort den Safari-Browser erweitern. Der neue Extension Builder in Safari 5 hilft bei der Entwicklung, beim Packen und beim Installieren von Erweiterungen. Safari-Erweiterungen laufen aus Sicherheitsgründen in einer Sandbox. Apple hat weitere HTML5-Funktionen implementiert, um Webentwicklern den Einsatz der neuen Webtechnologie schmackhaft zu machen. Dazu zählen die Unterstützung der HTML-Funktionen für Videos im Vollbildmodus, Ruby, Geo-Lokalisierung, Formularvalidierung und Ajax-History. 3 Safari • www.apple.com/de/safari
Mac-Macken aufdecken und lösen Ganz schön pfiffig: In Zeiten von User Generated Content hatte Martin Steiger die ziemlich geniale Idee, die Inhalte für sein Blog von seinen Lesern generieren zu lassen, indem diese ihre ganz persönlichen Mac-Probleme beschreiben, die Martin dann auf seinem Blog aufgreift und zur Diskussion freigibt. Und das Konzept scheint aufzugehen. Wie gesagt: ziemlich clever. 3 Mac Macken • http://macmacken.com
App Stores
Lukrative iPad-Apps Im Distimo Report für Juni 2010 stellt der Analyseanbieter die App-Läden von Google, Apple, Nokia, RIM, Palm und Microsoft mit Daten aus den USA einander ge-
Dass Microsoft nicht nur in Windows-Kategorien denkt, dürfte hinlänglich bekannt sein. Weil es eine genügend große Zahl von MacAnhängern gibt, die sich freiwillig (oder auch nicht) ein Microsoft-Produkt wie Office auf ihren Apple-Rechner packen, pflegt der Konzern aus Redmond die entsprechende Software wie Office:mac 2011 oder Messenger für Mac. Wer sich als Interessent schon einmal vorab mit Design und Funktionsumfang vertraut machen will, findet auf einer stylischen Webseite passende Informationen über alle Microsoft-Mac-Produkte. 3 Mactopia • http://microsoft.com/mac
8
Quelle: Distimo
Mac-Produkte von Microsoft
Mit iPad-Apps verdienen Apple-Entwickler mehr als mit iPhone-Apps
genüber. Im Android Market finden Anwender den größten Anteil kostenloser Apps (57 Prozent), bei Windows Marketplace for Mobile sind es die wenigsten (22 Prozent). Die Mehrzahl der Apps kostet in allen Stores unter 2 Dollar. Ausnahmen sind die Blackberry App World und der Windows Marketplace for Mobile mit einem geringeren Anteil günstiger Apps. Im Apple App Store liegt der Durchschnittspreis für bezahlte Apps fürs iPad mit 4,65 Dollar etwas höher als beim iPhone mit 4,01 Dollar. Bei den Top 100 fällt die Differenz noch größer aus. Neun von zehn der populärsten kostenlosen Apps und acht von zehn der populärsten kostenpflichtigen Apps in Apples App Store sind übrigens Spiele. 3 Distimo • www.distimo.com
mac-developer 4/2010
006_News1_ef_mjh_ef_ea.qxp
09.08.2010
16:40 Uhr
Seite 9
Mac Insider News
Bluefish-Editor
Mobiler Markt
Nativ auf Mac OS X
Top: iOS, Android & Symbian
Die Entwickler von Bluefish haben den Editor erstmals als natives Paket für Mac OS X veröffentlicht. Bluefish unterstützt alle wichtigen Sprachen wie C/C++, Java, Ruby, PHP und XML und greift Entwicklern mit Syntax-Highlighting, Code-Folding, Autovervollständigung, komfortablem Suchen und Ersetzen, unbegrenztem Undo und der Synchronisation von Projektdateien unter die Arme. 3 Bluefish • http://bluefish.openoffice.nl
Bluefish gibt es jetzt erstmals als natives Package für Mac OS X
90 Prozent aller SmartphoneZugriffe auf das Admob-Netzwerk kommen von drei Plattformen: iOS, Android und Symbian. Die Statistik für Mai 2010 verrät, dass iOS bei der weltweiten Verbreitung die Nase vorn hat. Bei den getrackten Apps entfallen 40 Prozent der Aufrufe auf das Apple-Betriebssystem. Google Android hat sich allerdings in kürzester Zeit mit einer Fülle an Herstellern und Endgeräten einen iOS ist Spitzenreiter, Android holt auf, Quelle: Admob Marktanteil von 26 Prozent er15 Prozent aus Asien. Die beliebtesten Gearbeitet. Symbian kommt auf 24 Prozent. räte nach dem iPhone sind übrigens das Interessante Zahlen meldet Admob Motorola Droid (6,8 Prozent), das HTC auch zur globalen Verbreitung der mobiMagic (2,9 Prozent) und das Nokia N70 len Plattformen. Das iPhone ist außerhalb (2,5 Prozent). Der Mobile Metrics Report der USA deutlich besser aufgestellt als für Mai 2010 von Admob, dem WerbeAndroid. 44 Millionen iOS-Geräte haben netzwerk von Google, erfasst Anzeigenim Mai auf Apps im Admob-Netzwerk aufrufe von 23.000 Apps und mobilen zugegriffen, 57 Prozent davon von außerWebseiten. halb der USA. 28 Prozent der Anfragen 3 Admob • www.admob.com kommen zum Beispiel aus Westeuropa,
Via iPad
Mac OS X vs. iOS
Windows-Zugriff
Künftig zwei WWDCs?
Array Networks hat ein System aus Hardund Software entwickelt, um mit einem iPad oder iPhone auf Desktop-Rechner zuzugreifen, auf denen Windows läuft. Dabei überträgt die Applikation lediglich Video-, Tastatur- und Maussignale geschützt über einen Webbrowser auf das Mobilgerät. Alle Anwendungen und Daten verbleiben auf dem Desktop. Darüber hinaus ermöglicht die iPad-Applikation den simultanen Zugriff auf mehrere Desktops. Die Hardware besteht aus sogenannten Universal Access Controllern der Array-NetworksSPX-Serie, auf denen die RemoteSoftware Desktop Direct arbeitet. 3 Array Networks • www.arraynetArray Networks: Fernzugriff auf Windows-PCs works.net
Ein Apple-Mitarbeiter hat Gerüchte über eine Aufteilung der Apple-Entwicklerkonferenz in zwei WWDCs gestreut – eine für die Mac-OS-X-Umgebung und eine für die mobile Plattform iOS. In der Apple-Entwicklergemeinde wird immer häufiger die Befürchtung laut, dass der Mac durch den Erfolg des iPads, des iPhones und der iOS-Plattform für Apple an Bedeutung verliert und damit auch die MacEntwickler auf der Strecke bleiben. Auf der WWDC spielte der Mac bei Steve Jobs’ Vorstellung der Apple-Neuheiten keine Rolle. Angesichts der vielen Neuheiten wie iPhone 4, iPad, iOS 4 fielen die vergleichsweise kleinen Updates für den Mac unter den Tisch. Jetzt wird darüber spekuliert, dass Apple in Zukunft statt einer gemeinsamen
4/2010 www.mac-developer.de
Konferenz die Mac-und die iOS-Welt voneinander trennen und zwei Konferenzen für Apple-Entwickler abhalten könnte. Eine Apple-Quelle hat Hardmac mitgeteilt, dass diese Möglichkeit besteht. Apple könnte eine Entwicklerkonferenz ganz auf seine mobilen Produkte iPhone, iPod, iPad und iOS zuschneiden, während eine zweite Veranstaltung neue Mac-Modelle und Mac OS X zum Thema hat. So würde der Mac-Bereich deutlich gestärkt. 3 Hardmac • www.hardmac.com
Mac OS X und iOS könnten getrennte Entwicklerkonferenzen bekommen
9
Quelle: Admob
●
010_WWDC_ef_ea.qxp
09.08.2010
16:03 Uhr
Seite 10
Mac Insider WWDC-Special ●
Rückblick: Apple Worldwide Developers Conference 2010
iOS-Entwickler in der Überzahl Über 5000 Apple-Entwickler kamen in San Francisco zusammen, um sich über die Neuigkeiten zu iPhone und iPad zu informieren. Die Mac-Plattform spielte diesmal eine untergeordnete Rolle. Ortwin Gentz
Ortwin Gentz ist Geschäftsführer der FutureTap GmbH in Seefeld bei München. Nach seinem Informatikstudium an der TU München gründete er 1999 die equinux AG, die sich im Bereich MacSoftware einen Namen gemacht hat. Das enorme Potenzial des iPhone-Marktes bewegte ihn 2008 zur Gründung der FutureTap GmbH. Er arbeitet mit Leidenschaft an der Perfektion von Bedienoberflächen und wurde dafür bereits zweimal mit dem Apple Design Award ausgezeichnet.
Bild: Cyril Godefroy
Beim WWDC-Bash lassen die Teilnehmer zu Livemusik die Woche ausklingen
10
B
ereits seit vielen Jahren findet die WWDC einmal jährlich im Moscone Center in San Francisco statt. Die Location ist das größte Konferenzzentrum in der Umgebung, und dennoch finden bei Weitem nicht alle Apple-Entwickler Platz. Bereits nach acht Tagen meldet Apple „Ausverkauft!“ – sämtliche 5200 Tickets waren weg. 190 Stück davon gingen an deutsche Entwickler, die damit das größte Kontingent nach ihren US-Kollegen stellen. Selbst die Preiserhöhung um 20 Prozent und eine extrem kurze Vorankündigungszeit von nur fünf Auf einer Video-Wall aus 30 Apple-Cinema-24-Zoll-Displays werden die Wochen halten die Entwickler nicht aktuellen App-Store-Downloads visualisiert davon ab, in Scharen nach San Franteilnahme sparen und die Vorträge auf Video cisco zu pilgern. Im vorigen Jahr dauerte es noch ansehen. In den Mittelpunkt rücken damit die etwas über einen Monat bis zum vollständigen direkten Kontakte zu Apple-Engineers und EntVerkauf aller Tickets. wicklerkollegen (siehe Kasten Seite 11). Gab es zur WWDC 2008 noch einen Vortrag Videos in Rekordzeit „iPhone-Entwicklung für Mac-Entwickler“, änApple scheint selbst zu bedauern, dass viele Entderte sich das bereits im letzten Jahr, als stattdeswickler draußen bleiben mussten. So werden die sen ein Vortrag „Mac-Entwicklung für iPhoneVideos der Vorträge bereits nach einer Woche Entwickler“ angeboten wurde. Der Trend setzt zur Verfügung gestellt – kostenfrei für alle Entsich in diesem Jahr weiter fort, die iPhone-Entwickler. In den vergangenen Jahren mussten wickler sind deutlich in der Überzahl. Apple Entwickler dafür noch mehrere hundert Euro trägt allerdings selbst dazu bei, da bereits im berappen. Der Informationsvorsprung durch Vorfeld klare Signale gesendet wurden, dass in die Vorträge entfällt damit künftig. Entwickler, diesem Jahr dem Mac kein allzu großer Stellendie ihre Zeit während der Konferenz hauptsächwert eingeräumt werden würde. So werden die lich mit den zahlreichen Vorträgen verbracht begehrten Apple Design Awards erstmals aushaben, werden sich möglicherweise im nächsten schließlich für iPhone- und iPad-Apps vergeben Jahr die Kosten für Flug, Hotel und Konferenzund der Mac-Vortrags-Track wird stark zusammengestrichen. Im kommenden Jahr könnte die Situation anders aussehen, wenn Mac OS X 10.7 bis dahin zur Beta-Reife gelangt. Ein schleichendes Ende des Macs können sich die meisten Entwickler jedenfalls derzeit nicht vorstellen. Werner Jainek von Cultured Code, Entwickler von Things für iOS und Mac: „Apples Events sind seit einiger Zeit immer sehr stark fokussiert auf ein Thema. Derzeit liegt der Hauptfokus – auch wegen der wachsenden Konkurrenz im Mobile-Bereich – sehr stark auf dem iPhone. Aber ich denke, dass Apple uns auch weiterhin mit dem Mac beglücken wird. Ich glaube, dass wir hier nächstes Jahr ein Betriebssystem-Update sehen werden.“
mac-developer 4/2010
Bild: Ortwin Gentz
Autor
010_WWDC_ef_ea.qxp
09.08.2010
16:03 Uhr
Seite 11
Mac Insider WWDC-Special ●
Unter den Entwicklern wird das neue iPhone 4 begeistert aufgenommen. „Das iPhone 4 sieht fantastisch aus. Ähnlich wie das iPhone 3G und 3GS ist es seiner Zeit weit voraus“, meint Keith Shepherd, Gründer von Imangi Studios und Entwickler von Spielen wie Harbor Master. „Das Spannendste ist das hochauflösende Retina-Display, mit dem beeindruckende grafische Effekte möglich werden.“ So verwundert es wenig, dass die Unterstützung der höheren iPhone-4-Auflösung zu den Top-Themen der Woche gehörte. Things-Entwickler Werner Jainek: „Ich bin umgehauen vom neuen iPhone 4. Wir werden natürlich alle Grafiken updaten. Unser Designer arbeitet schon eine Weile daran, wir haben die Gerüchte einfach mal geglaubt.“ Für viele Teilnehmer ist die höhere Auflösung also keine große Überraschung. Neu ist dagegen für die meisten das Gyroskop im iPhone 4, das auch Drehungen senkrecht zur Schwerkraft präzise erfassen kann. Über das neue Core Motion API bekommen Entwickler komfortablen Zugriff auf die Bewegungsdaten.
Fazit: Erfolgserlebnisse durch persönlichen Kontakt Nach einer anstrengenden, aber lohnenden Woche ist die Resonanz der Entwickler auf der WWDC 2010 durchweg positiv. Dustin Barker
Bild: Michael Jurewitz
Heißestes Thema: iPhone 4
Videos der Vorträge erhalten alle registrierten Entwickler kostenlos eine Woche nach der Konferenz von Agile Partners kann sich nicht nur über einen Apple Design Award für die Musik-App „TabToolkit“ freuen: „Im letzten Jahr gingen wir mit viel neuem technischen Wissen nach Hause aber mit wenig Inspiration. Das war dieses Jahr anders: Diesmal ging es mehr darum, wie man bessere Produkte entwickelt. Besonders die UISessions waren fantastisch.“ Werner Jainek von Cultured Code pflichtet bei: „Die Sessions haben eine sehr hohe Qualität. Man sieht, wie viel Arbeit Apple da reinsteckt. Da fühlt man sich als Entwickler gewürdigt. Auch die Möglichkeit, in den Labs mit den Apple-Entwicklern direkt reden zu können, finde ich sehr wichtig. Mit Hilfe eines Apple-Entwicklers konnten wir einen Bug, der uns schon seit einem Jahr beschäftigt, in fünf Minuten fixen.“ Solche Erfolgserlebnisse sind es, die mit Sicherheit auch 2011 zu einem Ausverkauf in Rekordzeit führen werden. [jp]
Fünf Tipps für eine erfolgreiche WWDC Mit den Apple-Engineers sprechen Die WWDC ist die einzige Gelegenheit im Jahr, bei der man unkompliziert Kontakt zu den Entwicklern von iOS, Mac OS X und Co. bekommt. Besonders für Fragen „zwischen den Zeilen“, die so nicht in der Dokumentation beantwortet werden, sind solche direkten Kontakte in den Labs Gold wert. Manchmal ist es nicht ganz einfach, einen kompetenten Ansprechpartner für dieses oder jene Problem zu finden. Doch kann man es öfters probieren: Die Labs sind über die ganze Woche verteilt. Fragen vorbereiten Während der Arbeit stehen die meisten Entwickler immer wieder einmal vor unlösbaren Problemen oder unerklärlichen Bugs. Die WWDC ist der beste Ort, solche Fragen zu klären. Man sollte sich allerdings vorher gut vorbereiten, um während der Konferenz nicht wertvolle Zeit damit zu verbringen, Bugs erst zu reproduzieren oder Beispielprojekte zu erstellen. Kontakte zu Entwicklerkollegen knüpfen Neben der rein fachlichen Seite ist die WWDC auch der Treffpunkt schlechthin für die Apple-Entwick-
4/2010 www.mac-developer.de
ler-Community und damit eine tolle Möglichkeit, neue Kontakte zu knüpfen. Das bereitgestellte Boxed Lunch ist zwar nicht besonders hochwertig, aber eine prima Gelegenheit, an einen der Tische mit unbekannten Gesichtern zu gehen und Hallo zu sagen. Oft ergeben sich sehr interessante Gespräche dabei. Abends finden während der Woche diverse Partys statt, auf denen es sich auch super networken lässt. Wichtig ist nur, gleich die Kontaktdaten auszutauschen, da man sich bei der Menge an Teilnehmern leicht aus dem Auge verliert. Die gute alte Visitenkarte ist dafür immer noch praktisch. Wer es moderner mag, kann Bump fürs iPhone verwenden. Netz-Abdeckung Das gesamte Konferenzzentrum ist mit freiem WLAN gut abgedeckt. Leider litt die Zuverlässigkeit in diesem Jahr stark unter diversen MiFi-Routern (Mobile WiFi), die als eigene WLAN-Basisstationen die knappen Funkkanäle blockieren. Höhepunkt war die Keynote selbst, in der Steve Jobs mit dem neuen iPhone 4 keinen WLAN-Zugang mehr bekam. Im Konferenzzentrum kann man zum Arbeiten am Mac auf
die großzügig vorhandenen Ethernet-Anschlüsse ausweichen. Außerhalb der Konferenz bietet es sich an, einen Datenplan fürs iPhone mit einer lokalen SIM-Karte zu aktivieren. Natürlich ist dies nur für Unlocked-Geräte möglich. Als lokale SIM-Karten kommen in den USA nur AT&T (3G) und T-Mobile (nur EDGE) infrage. Sinnvollerweise sollten Entwickler sich vor der Reise über aktuelle Angebote informieren. Keynote Die WWDC wird mit der traditionell von Steve Jobs gehaltenen Keynote eröffnet. Der Vortrag findet im größten Raum namens Presidio statt, in dem neben den knapp 1000 Pressevertretern jedoch nicht alle 5200 Teilnehmer Platz finden.Wer zu spät dran ist, muss mit einem Platz in einem Overflow-Room vorlieb nehmen. Das führt dazu, dass viele Teilnehmer bereits im frühen Morgengrauen Schlange stehen, um einen möglichst guten Platz zu ergattern. Da allerdings durchaus die meisten Teilnehmer in Presidio Platz finden, sollte man sich nicht verrückt machen. Es sei denn, man kann durch den Jetlag ohnehin nicht schlafen.
11
012_Buecher_la_ef_ea.qxp
09.08.2010
15:55 Uhr
Seite 12
Mac Insider Buchbesprechungen ●
iPhone & iPad Programmieren für Einsteiger
3Autor: Ingo Böhme Untertitel: Programmierung für Einsteiger Verlag: Markt+Technik, 2010, 320 Seiten ISBN: 978-3-8272-4595-3 Preis: 29,95 Euro
r!
Online meh
Autor Ingo Böhme will mit dem Buch „iPhone & iPad“ Einsteigern das Entwickeln von Apps für die mobilen Geräte von Anfang an beibringen. Damit das auf 320 Seiten klappen kann, wählt der Autor einen schmalen Pfad durch alle wichtigen Wissensgebiete und erläutert die Themen auf diesem Weg. Das grundsätzliche Rüstzeug wird dem Leser mitgegeben; was am Ende fehlt, ist der große Überblick über die Frameworks des iPhone SDK – den kann allerdings auch ein einzelnes Buch kaum vermitteln. Der Weg zum Entwickler für iPhoneund iPad-Anwendungen beginnt damit, dass die Hard- und Software vorgestellt wird, die der Leser braucht, um dem Autor zu folgen. Auch das Anlegen eines kostenlosen Entwickler-Accounts bei Apple begleitet der Autor. Die Grundlagen der Programmiersprache Objective-C werden auf knapp 50 Seiten gelegt und dabei auch die Frameworks Cocoa und Cocoa Touch kurz vorgestellt.
Der Leser lernt, wie er sein Programm anweist, auf Events zu reagieren, und übt den Umgang mit dem Debugger. Es folgt eine Anleitung zum Entwickeln einer eigenen App. Alle wichtigen Bestandteile werden dabei Schritt für Schritt vorgeführt und erläutert. Auch Switches und Slider, Listen, Tabellen sowie Views und Animationen werden thematisiert und ein Test auf einem echten iPhone durchgeführt. In Kapitel 11 wird eine komplette Beispiel-App entwickelt. Wieder geht es Schritt für Schritt vom Konzept bis zur fertigen App. Darauf folgt die Beschreibung, wie die App im App Store eingereicht wird. Weitere rund 20 Seiten beschäftigen sich mit den Besonderheiten des iPads. Das Buch verwendet eine serifenlose Schrift, die recht groß und gut lesbar ist. Allerdings sind die Seitenränder eher knapp gehalten. Sehr gut sind die stets ausreichend großen Bildschirmfotos, auf denen man das, was der Autor zeigen will, auch wirklich gut erkennen kann. [bl]
www.mac-developer.de/Aktuell/Buchvorstellungen
Entwickeln mit dem iPhone SDK Kompetente Einführung
3Autoren: Bill Dudney, Chris Adamson Untertitel: – Verlag: O’Reilly, 2010, 604 Seiten ISBN: 978-3-89721-951-9 Preis: 39,90 Euro
12
Die Autoren, Software-Entwickler Bill Dudney und Entwickler und Berater Chris Adamson, erklären in diesem Buch, wie man zu einer ersten selbstgeschriebenen iPhone-App kommt und sie in den App-Store bringt. Die vom Leser geforderten Voraussetzungen beschränken sich auf einen Mac mit OS X Leopard, auf welchem das iPhone SDK lauffähig ist, sowie Kenntnisse in einer der Programmiersprachen C, C++, Java, Ruby oder einer der anderen „Curly Brace“-Programmiersprachen. Auch Leser, die nur Programmierkenntnisse in Skriptsprachen (JavaScript, AppleScript, PHP) haben, laden die Autoren zur Lektüre ein, prophezeien ihnen aber einige zusätzliche Schwierigkeiten. Das erste der mehr als 50 Beispielprogramme ist das unvermeidliche HalloWelt-Programm, diesmal in der Ausprägung „Hallo iPhone“. Darauf folgen erst einmal die Grundlagen der iPhone-Entwicklung mit dem Basiswissen zu den Tools (Xcode, Interface Builder) und zu
Objective-C, der Programmiersprache für Cocoa Touch und damit fürs iPhone. View Controllern und Views sind die nachfolgenden Kapitel gewidmet. Der Leser erfährt darin, wie er Bedienoberflächenelemente wie Tab-Leisten oder Tabellenansichten aufbaut. Nach diesen grundlegenden Informationen werden Lerninhalte und die Beispiele schrittweise spezieller. Behandelt werden unter anderem die Datenbank SQLite, Peer-to-Peer-Netzwerke, der Zugriff auf die iPod-Bibliothek, das Abspielen von Videos und der Zugriff auf das Adressbuch sowie das Location-API. Auch Fehlersuche und Performance-Tuning werden behandelt. Dank der gelungenen deutschen Übersetzung von Thomas Demmig ist der Text leicht zu lesen. Viele Screenshots illustrieren die Inhalte. Sehr gut sind die eingestreuten Textkästen „Joe fragt“. Darin beantworten die Autoren naheliegende Leserfragen. Die Quellcodes kann man auf der Webseite zum Buch laden. [bl] mac-developer 4/2010
012_Buecher_la_ef_ea.qxp
09.08.2010
15:55 Uhr
Seite 13
Mac Insider Buchbesprechungen ●
iPhone Apps mit HTML, CSS & JavaScript Jonathan Stark ist Webprogrammierer und wollte seine Webanwendungen auch für das iPhone verfügbar machen. Den Aufwand, dafür Objective-C zu lernen, und das Risiko, Anwendungen ausschließlich für Apples App Store zu entwickeln, scheute er. So hat er nach einer Lösung gesucht, mit seinen Kenntnissen als Webentwickler und geeigneter Software Apps zu entwickeln. Sie besteht darin, eine Webanwendung mit den frei verfügbaren Technologien wie HTML5, CSS und JavaScript aufzubauen und diese anschließend mit Hilfe des Open-SourceEntwickler-Tools PhoneGap in eine native iPhone App zu konvertieren. Stark versucht, sogar Leser ohne Vorkenntnisse mitzunehmen, indem er mit einem Crashkurs in Webprogrammierung beginnt. Daraufhin startet die Schritt-fürSchritt-Anleitung für den Aufbau einer Web-App, die später auf das iPhone portiert wird. Eingebaut werden auch eine Animation sowie die Datenspeicherung
r!
Online meh
auf dem Client, und auch die für den Offline-Betrieb nötigen Funktionen werden hinzugefügt. Ist die Web-App fertig, bringt der Autor PhoneGap ins Spiel. PhoneGap wurde entwickelt, um eine Brücke zwischen Webanwendungen und mobilen Geräten zu bauen. Auch der Einsatz von PhoneGap wird Schritt für Schritt beschrieben. Dabei kommen dann auch Erweiterungen zur Positionsbestimmung und die Nutzung der iPhone-Sensoren zur Sprache. Den Abschluss bildet ein kurzes Kapitel, das beschreibt, wie Sie die fertige App nach iTunes übertragen. Das Layout des Buches ist übersichtlich, viele Screenshots illustrieren den Text. Gelungen ist, dass viele Listing-Zeilen mit Ziffern versehen sind. Die Ziffern findet man auch im Text wieder, so dass die Zuordnung leicht gelingt. Das Buch ist leicht zu lesen und die Übersetzung durch Ingo Dellwig nimmt auch auf deutsche Besonderheiten Rücksicht, wie etwa die HTMLCodierung von Umlauten. [bl]
Apps ohne Objective-C aufbauen
3Autor: Jonathan Stark Untertitel: – Verlag: O’Reilly, 2010, 185 Seiten ISBN: 978-3-89721-603-7 Preis: 29,90 Euro
www.mac-developer.de/Aktuell/Buchvorstellungen
Mac OS X für Profis Das Buch „Mac OS X für Profis“ ist exklusiv im E-Book-Eigenverlag des Autors Michael Kofler erschienen. Die E-BookReihe beschränkt sich auf kurze Texte ohne Ballast. Koflers Formel dabei: 100 Seiten – 10 Euro. Allerdings will er nicht bereits gedruckte Bücher, oder Auszüge daraus als E-Book verkaufen, sondern originäre Titel, die von Konzept und Layout für die Lektüre am Bildschirm optimiert sind. Das E-Book behandelt Mac OS X 10.6 (Snow Leopard). Zielgruppe sind fortgeschrittene Mac-OS-X-Anwender, und wie im Konzept versprochen werden weniger wichtige Themen nicht ausführlich behandelt. So wird beispielsweise die Bedienoberfläche von Mac OS X kaum thematisiert. Die Kernthemen des E-Books sind die effiziente Nutzung des Terminals, die Grundlagen des Betriebssystems inklusive des Dateisystems HFS und der Werkzeugsammlung Xcode, die Installation wichtiger Open-Source-Programme, die Verwendung von MacPorts und Fink
4/2010 www.mac-developer.de
sowie die Ausführung virtueller Maschinen mit Virtual Box. Anhang A gibt einen Überblick über die wichtigsten Tastenkürzel unter Mac OS X, und ein weiterer Anhang zählt diverse andere kostenlose und kommerzielle Werkzeuge auf, die der Autor nützlich findet. Das Seitenlayout des E-Books ist übersichtlich, etliche Screenshots illustrieren den Inhalt. Überschriften, Hinweistexte und Listings sind in gut lesbarer dunkelblauer Schrift hervorgehoben. Hin und wieder gibt der Autor auch einen Tipp, der auf seinen praktischen Erfahrungen aufbaut und dem Leser hilft, Zeit und Nerven zu sparen, beispielsweise bei der Auswahl einer Emacs-Variante oder bei der Arbeit mit dem kostenlosen Bildbearbeitungsprogramm Gimp. Alles in allem ist das E-Book eine kostengünstige Möglichkeit, sich einen schnellen Überblick zu Mac OS X 10.6 zu verschaffen, wenn man nicht gerade eine Einführung zur grafischen Bedienoberfläche sucht. [bl]
Xcode für Individualisten Die Xcode-Entwicklungsumgebung ist das tägliche Brot des Entwicklers. Lesen Sie, wie Sie mehr Effizienz aus Ihrem Entwicklungssystem herauskitzeln und welche Tricks die Programmierung erleichtern. Ivo Wessel Auf einen Blick Mit einigen Handgriffen gestalten Entwickler ihre Xcode-Arbeitsumgebung effizienter. Lesen Sie hier, wie Sie sich mit bestimmten Voreinstellungen, eigenen Tastenkürzeln, individuellen Templates und Skripts das Leben erleichtern.
S
oftware-Entwickler sollten bei der Auswahl ihres Entwicklungssystems und ihrer Arbeitsumgebung äußerst wählerisch sein. Sie müssen ihr Werkzeug schließlich jeden Tag sehen, fühlen und hören. Empfehlenswert ist ein Zweitmonitor oder gleich ein Dreißigzöller, und dazu ein Schreibmaschinenkurs. Letzteres geht ja leider sehr gegen die Entwicklerehre, obschon das auf lange Sicht das Arbeitspensum am besten steigert. Neben der reinen Arbeitsgeschwindigkeit gibt es aber bei einem Entwicklungssystem weitere Faktoren wie bequeme Navigation, geringe Frustration, Fehlervermeidung und Denkentlastung, die zu einer gesteigerten Codequalität führen (Bild 1).
Xcode – Werkzeuge und Voreinstellungen Xcode umfasst erheblich mehr Tools als den Editor, wovon man sich im Ordner /Developer/ Applications überzeugen kann. Hier soll es aber
14
erst einmal um den Editor gehen. Wer den Editor aus dem Effeff und vor allem mit der Tastatur beherrscht, ist schon gut gerüstet. Richten Sie sich’s bequem ein – Ihr erster Gang sollte Sie über Xcode, Preferences … zu den Voreinstellungen führen. Drücken Sie [Befehl ,] – und schon haben Sie die erste Tastenkombination gelernt, die übrigens generell für Mac-Applikationen gilt. Ach ja: Falls Sie sich nicht als Switcher aus einer anderen Welt outen wollen, nennen Sie die Wahl-Taste nie „Alt“ – auch wenn sie auf den neueren Tastaturen so beschriftet ist. Ich empfehle das standardmäßig leider nicht aktivierte Layout All-In-One im Menü General. Es gleicht am ehesten einem aufgeräumten Schreibtisch und pflastert den Bildschirm nicht mit Fenstern zu. Möchten Sie trotzdem einmal ein Modul von mehreren Seiten aus betrachten, können Sie per Doppelklick in Groups & Files ein unabhängiges Fenster öffnen. Zu diesem Unterfenster gelangen Sie – wie wiederum generell beim Mac – üb-
mac-developer 4/2010
014_Grundlagen_jp_ea.qxp
09.08.2010
16:44 Uhr
Seite 15
Titelthema Xcode-Grundlagen ●
rigens mit der wenig bekannten Kombination [Befehl <]. Das gilt auch im Finder bei mehreren geöffneten Fenstern. Jedes Editor-Fenster besitzt rechts oben einen Split-Button, der die Ansicht teilt. Sie kennen ihn sicher von anderen Applikationen – sehr praktisch für Cut-and-Paste-Operationen innerhalb eines Textes und damit nützlich für Code-Refactoring. Aber wussten Sie, dass mit gedrückter Wahl-Taste das Fenster vertikal geteilt wird? Im zweiten Teil können Sie auch ein anderes Modul bearbeiten, etwa links die Implementierung einer Klasse, rechts die sie benutzende Methode oder Header und Implementierung gleichzeitig. Ein derart vertikal geteilter, dreispaltiger Arbeitsplatz – links der Group & Files-Baum, in der Mitte und rechts je eine Sourcecode-Ansicht – füllt einen mindestens 1600 x 1200 Pixel großen Bildschirm optimal aus. Ein Doppelklick auf die Trennlinie zwischen Group & Files und Editor maximiert Letzteren. Noch schneller geht das mit [Umschalt Wahl Befehl E].
Schriften anpassen
Der Schalter [X] Show page guide. Display at column 100 sorgt für eine senkrechte Linie, die Sie stets ermahnt, Zeilen spätestens an dieser Stelle sinnvoll zu umbrechen. Schließlich wollen Sie Sourcecode ja auch mal drucken, und die horizontale Scrollbar kompliziert das Editieren sehr. Lange Strings können in Objective-C sehr einfach mehrere Zeilen umfassen – C sei Dank: NSString *sText = @"Ich bin 3 ein langer" @"Text über" @"drei Zeilen"; Dank benannter, also quasi „sprechender“ Parameter bleibt auch der Umbruch von Methoden mit mehreren Parametern übersichtlich, da die Zeilen automatisch unterhalb der Doppelpunkte ausgerichtet werden. Die aktuelle Selektion wird manuell durch [Wahl Befehl 5] beziehungsweise [Wahl Befehl 6] und automatisch durch [Ctrl I] ein- und ausgerückt. So bleibt bei einem Parameter pro Zeile rechts davon meist noch Platz für einen Kommentar oder einen Beispielwert. Wenn Sie sich die Shortcuts für das Komprimieren und Dekomprimieren von Code nicht merken wollen – [Ctrl Befehl] plus Cursortasten – klicken Sie in den Rand zwischen Zeilennummern und Editor. Die Anzeige von Zeilennummern wird durch den Schalter [X] Show gutter mit [X] Show line numbers aktiviert. Ich finde Zeilennummern zur schnellen Lokalisierung von Codezeilen und zur Orientierung innerhalb eines Moduls sehr nützlich.
iPhone-Symbole in Xcode einsetzen (Bild 2)
▲
Mein Lieblings-Font ist der kostenlose Envy Code R von Damien Guard, der dank eines sehr ausgewogenen Höhe-Breite-Verhältnisses jede Courier schlägt. Öffnen Sie unter Preferences die Lasche Fonts & Colors, markieren Sie alle Kategorien mit [Befehl A] und wählen anschließend mit [Befehl T] die gewünschte Schrift aus. Apropos Font: Wenn Sie mal die Symbole benötigen, die Apple für das User Interface des iPhones definiert und freundlicherweise gleich in Apple Symbol integriert hat, wählen Sie Edit, Special Characters … oder [Wahl Befehl T], klicken auf Darstellung: Glyph, Schrift: Apple Symbols und scrollen ganz ans Ende – das sollte dann so aussehen wie in Bild 2.
Leitplanke im Editor
Ein effizientes Entwicklungssystem fördert die Codequalität (Bild 1)
4/2010 www.mac-developer.de
15
014_Grundlagen_jp_ea.qxp
09.08.2010
16:44 Uhr
Seite 16
Titelthema Xcode-Grundlagen ●
Neue Key Bindings Move to Beginning of Document: [Ctrl Pos1] ● Move to Beginning of Line: [Pos1] ● Move to End of Document: [Ctrl Ende] ● Move to End of Line: [Ende] ● Move Word Backward: [Ctrl Pfeil links] ● Move Word Forward: [Ctrl Pfeil rechts] ● Pop Loaded Files PopUp: [Befehl 1] ● Pop Symbols PopUp: [Befehl 2] ● Pop Bookmarks PopUp: [Befehl 3] ● Pop Included Headers PopUp: [Befehl 4] ●
Einrückungen mache ich sozusagen seit 30 Jahren mit drei Leerzeichen. Programmierer stehen nun einmal fortwährend vor if-Entscheidungen, und die Spaltenausrichtung der folgenden Klammer mit dem Code in der nächsten Zeile finde ich mit drei Leerzeichen schon optimal. Außerdem markierte ich gern Namen von Klassen und Methoden durch Unterstreichen. Außerdem verwende ich mit Vorliebe die einfachen und doppelten Linien (aus - und =), um Sourcecode zu strukturieren und zusammengehörende Methoden zu gruppieren. Die Zeile #pragma mark sorgt in der Auswahlliste für eine Trennlinie. Die „handgemalten“ Linien gliedern den Code. Methoden innerhalb einer funktionalen Gruppe – etwa System-, Action- oder Button-Methoden – werden durch eine einfache Linie getrennt.
Eigene Skripts Vielleicht ist das Menü-Icon inmitten der Textoptionen einfach zu unauffällig – jedenfalls kann die eher versteckte Möglichkeit, eigene Skripts zu definieren, die dann auf Knopfdruck ausgeführt werden, bei der Eingabe sehr viel Zeit sparen. Hier die Vorgehensweise, um „meine“ beiden Kommentar-Trennlinien durch [Befehl -] beziehungsweise [Wahl Befehl -] an der aktuellen Eingabeposition einzufügen: Wählen Sie im Script-Menü die Option Edit User Scripts …, dann New Seperator, New Submenu (hier: „Ivo Wessel“), New Shell Script und in der Spalte Befehl [Befehl -]:
Eigene Benutzerskripts in Xcode (Bild 3)
#!/bin/sh echo -n "// ----------------------------------------------------------------------------------------------" Wählen Sie Duplicate Script und als Hotkey [Wahl Befehl -] (Bild 3).
Key Bindings ändern Nur ausgesprochene Apple-Fans geben sich mit der Tastenkombination [Befehl Pfeil rechts] zufrieden, um das Eingabezeichen ans Zeilenende zu bewegen. Wenn Sie, wie ich, die Taste 3 auf dem Ziffernblock für schräg unten rechts bevorzugen, können Sie das in den Text Key Bindings einstellen. Möglicherweise ist das bei der normalen Tastatur sinnfälliger als bei der Notebook-Variante, bei der ohnehin immer zwei Tasten zu drücken sind. Beim ersten Mal müssen Sie über die Schaltfläche Duplicate … eine Kopie der Standard-Belegung herstellen. Dadurch können Sie aber auch schnell wieder zum Xcode-Standard wechseln. Vielleicht befindet sich Ihr Lieblingseditor bereits in der Auswahlliste. Meine wichtigsten Neubelegungen sehen Sie im nebenstehenden Kasten und in Bild 4. Viele weitere Einstellungen lassen sich über die Kommandozeile ändern, etwa automatisch eingefügte Leerzeichen in Parameterlisten. Allerdings ist das eher etwas für Spezialisten. Mehr dazu finden Sie in Apples Xcode User Default Reference.
Templates anpassen Beim Erzeugen eines neuen Projekts kann man bekanntlich aus mehreren Templates wählen. Diese können nicht nur modifiziert, sondern auch um weitere ergänzt werden. Die vorgegebenen finden sich unter /Developer/Platforms/ iPhoneOS.platform/Developer/Library/Xcode/Project Templates/Application. Kopieren Sie das am besten passende Template unter /Users//Library/Application Support/Developer/Shared /Xcode/Project Templates. Alle Ordner in diesem Verzeichnis, die Applikations-Templates enthalten, werden im Dialog File, New Project, User Templates angeboten. Nachdem Sie das Template angepasst haben, vergessen Sie nicht, den Build-Folder sowie alle .*-Dateien zu löschen. Diese sollen schließlich nicht Bestandteil des Templates sein. Auch die bei der Neuanlage von Dateien verwendeten Muster für Klassenableitungen wie NSObject sind über die jeweiligen Dateien konfigurierbar.
Eigene Text-Makros Selbst die praktischen Text-Makros (tippen Sie etwa if und drücken dann die [Esc]-Taste) lassen sich modifizieren und erweitern. Kopieren Sie dazu die Datei ObjectiveC.xctxtmakro aus dem Xcode-Applikations-Bundle Xcode.app, das im Ordner /Developer/Applications liegt. Falls Ihnen Windows-EXE-Dateien vertrauter sein sollten: Mac-Apps sind nichts anderes als Verzeichnisse,
16
mac-developer 4/2010
014_Grundlagen_jp_ea.qxp
09.08.2010
16:44 Uhr
Seite 17
Titelthema Xcode-Grundlagen ●
die die Dateien einer Applikation enthalten. Öffnen Sie Xcode.app im Finder mit Rechtsklick und Paketinhalt zeigen und suchen Sie hier die folgende Datei: /Contents/PlugIns/TextMacros.xctxt macro/Contents/Resources/ObjectiveC.xctxtmacro. Diese Datei kopieren Sie in das passende (und beschreibbare) Benutzer-Verzeichnis: /Users//Library/Application Support/Developer/Sha red/Xcode/Specifications. Dort können Sie die Datei nach Belieben ändern. Nähere Beschreibungen finden Sie im Xcode Workspace Guide.
Editieren eines Moduls Die Pop-up-Tasten erlauben den schnellen Zugriff auf bestimmte Listen, etwa die der Dateien, der Methoden und Properties eines Moduls. Für eine schnelle Navigation, besonders mit Hilfe der Tastatur, sind sie unverzichtbar. Der Wechsel zwischen Header- (*.h) und Implementierungs-Datei (*.m) gelingt am schnellsten mit [Wahl Befehl Pfeil hoch].
menhängende Wörter markieren möchten, halten Sie dabei [Ctrl Befehl] gedrückt. Klick und [Umschalt]-Klick markieren einen „Von-bis-Bereich“ – den man in Xcode auch per Drag and Drop direkt verschieben kann. Wortweise springen: Ich finde diese Art der Navigation mit [Ctrl Pfeil links] und [Ctrl Pfeil rechts] beziehungsweise [Wahl Pfeil links] und [Wahl Pfeil rechts] überaus augenfreundlich und „lesekompatibel“. Bei Verwendung der großen Tastatur liegt die rechte [Ctrl]-Taste direkt links neben den Cursortasten – man benötigt also nur die rechte Hand. Diese beiden Änderungen in Text Key Bindings lohnen sich in jedem Fall. Bookmarks: Mit [Befehl D] erzeugen Sie ein neues Lesezeichen. Ich habe mir angewöhnt, vor dem [Befehl D] per Doppelklick und [Befehl C]
Favorites Bar: nützlich und versteckt Dateien lassen sich also über Groups & Files und das Loaded Files PopUp editieren. Häufig benötigte Dateien kann man aber auch in die Favorites Bar ziehen, die allerdings über View, Layout, Favorites Bar erst eingeblendet werden muss. Gemessen an ihrer Nützlichkeit scheinen erstaunlich wenige Entwickler diese Option zu kennen oder zumindest zu nutzen. Häufig dagegen zu sehen und mir gar nicht einsichtig ist die Verwendung des Detail ViewFensters. In dieser Liste erscheinen die Dateien des aktuellen Groups & Files-Ordners. Ich denke, dass man auf einem der anderen beschriebenen Wege schneller zu dem gewünschten Modul gelangt und diesen Platz lieber für den Editor nutzen sollte – wie ja überhaupt der verfügbare Platz in der Vertikalen eher knapp und kostbar ist, so dass ich beispielsweise das Dock stets an der Seite platziere. Dateien, die man nicht ganz so oft benötigt, deren Namen man aber schnell tippen kann, lassen sich durch die Quick Open-Option [Umschalt Befehl D] vermutlich am schnellsten öffnen.
Navigation in einem Modul
4/2010 www.mac-developer.de
▲
Jump to Definition – Doppelklickereien: [Befehl] und Doppelklick auf eine Property oder Methode wechselt zwischen Prototyp und Implementierung. Das funktioniert sowohl in der .H- wie auch in der .M-Datei – und sogar im Interface Builder, wenn Sie etwa schnell in den Sourcecode der Klasse des File’s Owner gelangen wollen. Dass Sie per Doppelklick ein Wort markieren, wissen Sie. Wenn Sie mehrere, nicht zusam-
zum Beispiel den Namen der Methode in die Zwischenablage zu kopieren und diesen – eingefügt mit [Befehl V] – auch zur Identifikation des Lesezeichens zu benutzen. [Befehl 3] (beziehungsweise [Ctrl 4] in der Xcode-Default-Einstellung) öffnet das Pop-up rechts oben. In Groups & Files gibt es einen Ordner mit den aktuellen Bookmarks, die hier auch gelöscht werden können. Außerdem umfasst diese Liste sämtliche Lesezeichen. Im Pop-up erscheinen nur jene des aktuellen Moduls. Fehlt dieser Ordner, blenden Sie ihn über [Umschalt Befehl M] oder die Option Preferences, Bookmarks im Kontextmenü wieder ein. Verschieben Sie den Ordner nach oben, um die Einträge leichter zu erreichen. Zeilennummern: Vielleicht mag es absurd erscheinen, aber für zwei oder drei nur kurz in
Eigene Tastenkürzel erleichtern den Zugriff auf Funktionen (Bild4)
17
014_Grundlagen_jp_ea.qxp
09.08.2010
16:44 Uhr
Seite 18
Titelthema Xcode-Grundlagen ●
Autor
mac-developer-Autor Ivo Wessel ist freier Software-Entwickler und hat über „seine“ Programmiersprachen zahlreiche Bücher und Artikel verfasst. Seit zwei Jahren widmet er sich ausschließlich der Programmierung für das iPhone und iPad und schreibt gerade ein Buch darüber.
Bearbeitung befindliche Stellen innerhalb eines Moduls finde ich die Orientierung an den Zeilennummern ganz praktisch. Kommentare: Lesezeichen in Form von Kommentaren haben den Vorteil, dass sie im Code verbleiben und „personalisiert“ werden können. Arbeiten mehrere Personen an einem Projekt, sollte man Namenskürzel definieren, die genügend eindeutig sind, um Bestandteil einer Suche sein zu können. *iw etwa ist mein seit 25 Jahren benutztes Kürzel. Verwendet man es zusammen mit einem der vordefinierten Kommentare wie beispielsweise // TODO: *iw so taucht es, ähnlich den #pragma-Zeilen, in der Funktionen-Liste auf, die mit [Befehl 2] beziehungsweise als Xcode-Default mit [Ctrl 2] vom Editor aus geöffnet wird. Es gibt ein paar weitere Kommentarsymbole, die je nach Temperament für unterschiedliche Zwecke eingesetzt werden können: // // // // //
Angepasste Toolbar Project/Debug [Befehl 9]/ [Befehl 0] ● Build and Run [Befehl Eingabe] ● Build [Befehl B] ● Clean All ● Info [Befehl I] ● Class Browser [Umschalt Befehl C] ● Snapshot ● Organizer [Ctrl Befehl O] ● Find [Umschalt Befehl F] ● Editor [Umschalt Befehl E] ●
MARK: xxx TODO: xxx FIXME: xxx ???: !!!:
Beachten Sie den Doppelpunkt – und die ersten beiden erscheinen nur in der Symbolliste, wenn ihnen ein Text folgt. // MARK: ist dabei identisch mit dem vorgenannten #pragma; diese Zeile erzeugt wiederum eine Trennlinie im Pop-up: // MARK: Das Funktions-Pop-up [Befehl 2], eine am oberen Rand eingeblendete Liste der Funktionen und Methoden eines Moduls, ist sicher die am häufigsten benutzte Art der schnellen Navigation. Halten Sie beim Mausklick [Wahl] gedrückt, erscheint die Liste alphabetisch sortiert.
Reihenfolge der Methoden Werden Methoden in einem Modul oberhalb ihres Aufrufs implementiert, müssen sie nicht deklariert werden. Man spart sich also die Einträge der Prototypen in der .H-Datei, wenn man bei der Reihenfolge der Methoden etwas Acht gibt. Typischerweise landen dann Systemmethoden wie viewWillAppear, viewDidLoad et cetera am Ende eines Moduls. Wenngleich es Code-Puristen vielleicht etwas grausen mag, genügt es dann, im Header-File nur noch jene Public-Prototypen zu deklarieren, die von außerhalb ihrer eigenen Klasse aufgerufen werden.
18
Konfiguration der Toolbar Aus diesem Grunde habe ich in der Toolbar extra den Button mit dem Tooltipp Toggle Embedded Editor eingefügt, der die unter bestimmten Voraussetzungen automatisch geöffnete DetailListe wieder schließt. „Meine“ Toolbar-Buttons sehen Sie im nebenstehenden Kasten. Clean All mag sich auf den ersten Blick nicht so recht erschließen – aber es scheint auch heutzutage noch selbst für komfortable Entwicklungsumgebungen ein Problem zu sein, extern geänderte Bitmap-Dateien als modifiziert zu erkennen. Gelegentlich sorgt erst ein Klick auf Clean All für die notwendige Aktualisierung der kompilierten Ressourcen. Ich habe mir angewöhnt, vor jedem endgültigen Build durch Clean All sicherzustellen, dass alle Dateien auch wirklich auf dem aktuellen Stand sind. Außerdem gibt es eine Art Warnungen-Cache, der einmal angezeigte Meldungen unterdrückt. Erst Clean All macht dann nachdrücklich auf diese Stellen aufmerksam.
Code-Refactoring Klicken Sie auf ein Symbol – etwa eine lokale Variable – und warten Sie, bis rechts daneben ein kleiner Pfeil erscheint. Wenn Sie diesen klicken, die Option Edit All in Scope auswählen und einen neuen Namen schreiben, werden alle Vorkommnisse angepasst. Ist Ihnen das Warten auf den Pfeil zu umständlich, drücken Sie [Ctrl Befehl T]. Für viele Code-Optimierungen – Umbenennungen sind sicher die simpelsten, aber auch am häufigsten benötigten – gibt es Funktionen im Dialog Edit, Refactor …, die man direkt im Code über das Kontextmenü oder [Umschalt Befehl J] aufrufen kann. Umbenennungen haben übrigens gegebenenfalls auch Auswirkungen auf den mit dem Interface Builder erstellten Code.
Hilfe! Neben [Befehl] und Doppelklick sollte man sich [Wahl] und Doppelklick merken – diese Kombination führt bei der Anwendung zum Beispiel auf einen Klassennamen im Editor oder Interface Builder direkt zur passenden Hilfeseite. Für den Anfang mag es nützlich sein, die Quick Help mit [Ctrl Befehl ?] geöffnet zu lassen. Steht das Eingabezeichen auf einem Symbol einer Systemklasse, bietet das verschiebbare Fenster Informationen und weitere nützliche Links. In der Hilfe [Befehl ?] sollte man die Auswahlliste Jump to … nicht vergessen – sie erlaubt schnelles Navigieren und bietet etwa auch die Zugriffe auf die Adopted Protocols. Gelegentlich ist es sehr hilfreich, sich die Hilfe als – lokal speicherbares und druckbares – PDF anzuschauen.
mac-developer 4/2010
014_Grundlagen_jp_ea.qxp
09.08.2010
16:44 Uhr
Seite 19
Titelthema Xcode-Grundlagen ●
Snapshots vergleichen in Xcode (Bild 5) Natürlich lassen sich Methoden-Prototypen ausschneiden und im Editor einfügen. Ich ändere allerdings immer die Standardnamen der Parameter, weil ich dessen Variable und dessen Namen unterscheiden möchte. Als Beispiel: ... tableView:(UITableView *) tableView ... tableView:(UITableView *) oTableView
Datensicherung und Snapshots Am besten und sichersten ist noch immer die Benutzung eines Versionskontrollsystems. Subversion gehört zum Lieferumfang und ist direkt in Xcode integriert. Dessen Beschreibung würde allerdings diesen Artikel sprengen. Ein Versionskontrollsystem lohnt sich übrigens durchaus auch dann, wenn Sie allein an einem Projekt arbeiten. Unschätzbar ist der Luxus, mal eben eine ältere Version zurückzuholen oder Versionen vergleichen zu können. Wer diesen Aufwand scheut, sollte – den Finder haben Sie hoffentlich stets parat – vor größeren Änderungen Module oder auch mal das komplette Projekt duplizieren. Wenn Sie zusätzlich noch den Build-Ordner im Projekt löschen, sollte das fix genug gehen. Fehlende Build-Ordner werden automatisch angelegt. Eine vielleicht etwas zu sehr versteckte und wenig genutzte Möglichkeit ist die Anfertigung von Snapshots. Der Dialog ist mit [Ctrl Befehl S] schnell zur Hand und erzeugt eine Sicherung des aktuellen Standes. Snapshots lassen sich nebeneinanderstellen und vergleichen (Bild 5).
Mehrere Xcode-Versionen und ältere Plattformen
4/2010 www.mac-developer.de
Logging und Debugging Die Benutzung eines Debuggers ist sicherlich nicht nur eine Temperamentsache. Zur schnellen Ausgabe von Variablen, zur schnellen Prüfung, ob etwa eine Methode überhaupt aufgerufen wird, ist aber das Log-Fenster häufig ein schnellerer Weg. Über eine Option in den Preferences lässt sich das Fenster automatisch beim Start einer App öffnen: Debugging, On Start: Show Console. Ansonsten schalten Sie mit den beiden Buttons ganz links (zeigen Sie unter den Toolbar-Icons auch den Text, steht dort Page) zwischen Projekt und Debug hin und her. Es gibt dafür sogar eine Tastenkombination – allerdings ist eine
▲
Wenn es nach Apple ginge, würde jeder stets mit der allerneusten Xcode-Version arbeiten. Gelegentlich kann es aber durchaus sinnvoll sein, verschiedene Versionen unabhängig voneinander auf einem Rechner zu halten. Duplizieren Sie dazu den Developer-Folder, vergeben Sie ei-
nen sinnvollen Namen (zum Beispiel Developer 3.1.2) und erzeugen Sie für die Xcode.app im Verzeichnis Applications einen Alias, den Sie zum Beispiel im Dock ablegen. Sie können auch mit der neuesten Version 3.2.3 durchaus noch Applikationen generieren, die auf älteren Geräten (zum Beispiel mit Version 3.0) laufen. Zwar neigt der Apple-User dazu, gern umgehend die jeweils neueste Version zu installieren, aber beispielsweise beim iPod Touch findet man immer noch Geräte mit älteren Versionsnummern – vor allem, da das 3.0Update nicht kostenlos war. Öffnen Sie hierzu auf dem obersten Projektelement in Groups & Files das Info-Fenster [Befehl I] und wechseln Sie auf die Lasche Build. Unter Architectures, Base SDK werden Sie nur neuere Versionen wie iPhone Device 3.2 oder 4.0 finden. Unter Deployment, iPhone OS Deployment Target lassen sich aber ohne Weiteres noch ältere Versionen wie iPhone OS 3.0 wählen. Natürlich können Sie dann keine der neuen Framework-Funktionen nutzen. Wählen Sie also mit Bedacht. iPhone OS 3.1.3 etwa findet sich aber durchaus noch auf vielen Geräten. Ein großer Schnitt war vor längerer Zeit beispielsweise die Einführung von Core Data in 3.0.
19
014_Grundlagen_jp_ea.qxp
09.08.2010
16:44 Uhr
Seite 20
Titelthema Xcode-Grundlagen ●
Weitere Informationen Apples iPhone OS Reference Library enthält einige Dokumentes, die man sich durchaus einmal ansehen sollte. Suchen Sie nach den folgenden Stichwörtern: ● Xcode User Default Reference ● Xcode Workspace Guide ● Xcode Project Management Guide ● A Tour of Xcode
der beiden recht umständlich: Project, Project: [Befehl 0], Run, Debugger: [Umschalt Befehl Y]. Um mir die zweite nicht merken zu müssen, habe ich sie unter Preferences, Key Bindings, Menu Key Bindings auf [Befehl 0] und Erstere auf [Befehl 9] und damit wie in der Toolbar links davon umgelegt – beide gehören jetzt zu meinen am häufigsten benutzten Tastenkürzeln. Die Funktion NSLog() erhält als Parameter einen Format-String nebst einer Liste der einzusetzenden Werte. Hier ein paar Tipps: Um die Funktion schnell einzugeben, tippen Sie log, drücken die [Esc]-Taste und bestätigen die Auswahl der Liste mit [Eingabe]. [Esc] listet generell alle Symbole auf, die mit dem aktuellen Text beginnen. Objekte, Arrays, Dictionaries und ähnliches lassen sich durch das Formatzeichen @"%@" ausgeben. Definieren Sie für eigene Klassen eine Property Description, wird diese bei @"%@" benutzt. Floats ohne Nachkommastellen werden durch @"%.0f" ausgegeben. Ersetzen Sie gegebenenfalls die Null durch die Anzahl der gewünschten Nachkommastellen. Das Log-Fenster kann übrigens auch jederzeit mit [Ctrl Wahl Befehl R] gelöscht werden. Der Name der aktuellen Methode wird durch __PRETTY_FUNCTION__ über das Formatzeichen @"%s" ausgegeben. Kritische Methoden besitzen bei mir als erste Zeile etwas in der Art: NSLog(@"%s", __PRETTY_FUNCTION__); Damit der Produktionscode diese Zeile nicht enthält, lässt sie sich durch bedingtes Kompilieren unterdrücken, so dass sie nur bei Verwendung des Simulators aufgerufen wird. Angezeigt werden die Methode, die Zeilennummer sowie die gewünschten Werte (Listing 1). Solche allgemein und überall gültigen Dinge lassen sich gut in der _Prefix.pch-Datei unterbringen. Haben Sie etwa Klassen erweitert, können diese automatisch inkludiert werden, was den Aufruf in einzelnen Modulen spart. Solche sogenannte Kategorien sind eine wunderbare Möglichkeit, häufig benötigte Properties oder Methoden direkt einer System-Klasse wie NSString oder UIView beizubringen.
Listing 2: Auskommentieren #if false /* * Kommentar */ // Wird nicht ausgeführt ... #endif
Quellcode-Kommentare Sobald man neues Modul erzeugt, generiert Xcode einen Kommentarblock als Header, der auch eine Copyright-Zeile enthält. Der dort eingesetzte Name wurde in früheren Versionen dem Adressbuch entnommen. Es handelte sich dabei um den Datensatz, dessen Bild durch „Ich“ gekennzeichnet ist. Neuerdings kann der Name im Info-Dialog des Projekts auf der Lasche General unter Organization Name eingegeben werden. Markieren Sie in Groups & Files das Wurzel-Element und drücken Sie [Befehl I]. Durch bedingtes Kompilieren lassen sich übrigens weitere Kommentar-Ebenen erzeugen. Möchte man Zeilen auskommentieren, die ihrerseits bereits Kommentare nach der Art /* ... */ enthalten, die man leider nicht schachteln kann, bleibt für das Auskommentieren von Quellcode immer noch der Precompiler (Listing 2). #if true oder Löschen der beiden „Klammerzeilen“ reaktiviert die Codeausführung wieder.
Nützlich und gratis: Zusatz-Tool Finder Der Finder sollte für Dateioperationen immer zur Hand sein. Ein Rechtsklick auf ein Modul in Groups & Files und die Wahl der Option Reveal in Finder öffnet ihn an der richtigen Stelle. Ein allgemein verwendbarer Trick, bei einem Öffnenoder Speichern unter …-Dialog zu einem Pfad zu gelangen, der im Finder bereits geöffnet ist, besteht darin, das Icon der Finder-Titelleiste in den Dialog hineinzuziehen. Module lassen sich auch im Finder schnell erstellen und duplizieren: [Befehl D] und dann per Drag and Drop an die gewünschte Stelle innerhalb der Groups & FilesSektion ziehen. Apropos [Befehl D]: Falls Sie die Abfrage beim Schließen eines Dialog Speichern …? mit Nicht speichern und der Tastatur beantworten wollen, drücken Sie einfach [Befehl D]. Hintergrund: Hier gilt meist die englische Belegung, und die Option heißt dort „Don’t save“. Und mit [Umschalt Befehl D] gelangen Sie im Finder und in jedem Öffnen-/Speichern-Dialog direkt zum Desktop. Diese kleine Tour ist damit beendet. Sie haben hoffentlich Anregungen gefunden, Ihr Werkzeug effizient zu nutzen. Und denken Sie daran: A shortcut a day keeps the mouse away. [jp]
mac-developer 4/2010
021_Eindruck_iPhone_2010_A4_v4.qxd:Layout 3
06.08.2010
12:20 Uhr
Seite 21
Frühbuc
€ 100,–
01./02. Dezember 2010 Köln, Gürzenich
Entwickler-Panels
h er rab a
bis 13. O
ktober 2
tt
010
Business-Panels
01. Dezember 2010
plattfor mifend e r g über
➔
Best Practices mit iOS
➔
Marketingtool App
➔
Tools und Methoden für Profis
➔
M-Commerce
➔
AppStarter Day: iPhone vs. Android
➔
Von der Idee zur App
➔
AppStarter Day: Bada vs. MeeGo
➔
Finanzierungsmodelle und Marktchancen
02. Dezember 2010 ➔
Hybrid vs. Native Apps
➔ iPhone
➔
Lösungen für Fortgeschrittene
➔
Social Networks
➔
Mobile Gaming – Die Idee zum Erfolg
➔
➔
Mobile Games in der Übersicht
iPhone vs. iPad – Eine Entscheidungshilfe
➔
Blick über den Tellerrand
im Unternehmenseinsatz
Programm und Referenten unter www.iphonedevcon.de Präsentiert von:
Aussteller und Sponsoren:
Veranstalter:
021_Eindruck_iPhone_2010_A4_v4.qxd:Layout 3
06.08.2010
12:20 Uhr
Seite 22
01. Dezember 2010: Entwickler-Panels 09.00 – 09.45
10.00 – 13.30
Keynote
Eine Bestandsaufnahme: iOS vs. Mac OS – wohin entwickeln sich die Apple-Betriebssysteme? N.N.
Ein Trend war 2010 offensichtlich: Mit dem Erscheinen von iOS 4 verschwimmen die Grenzen zwischen Apples Computer- und HandyBetriebssystemen zusehends. Zeit für eine Bestandsaufnahme. Macht
eine Unterscheidung in Desktop-PCs, Notebooks und mobile Geräte überhaupt noch Sinn? Wird es 2011 vielleicht ein Betriebssystem für alle Apple-Devices geben? Ein Blick in die Glaskugel des Apple-Universums.
Developer 1
Developer 2
Best Practices mit iOS
Migration zu OS 4 – Überblick der neuen APIs | Markus Franz Vom Multitasking bis zum Zugriff auf die Fotogalerie iPhone/iPad SDK | Ivo Wessel Praxisbewährte Lösungen für die Datenhaltung auf einem mobilen Gerät Nebenläufigkeit in iPhone OS 4 | Tammo Freese Verschiedene Möglichkeiten der Ausführung im Hintergrund. Ein Überblick
14.30 – 18.00
ab 18.00
Developer 1
Tools und Methoden für Profis
App Starter Day: Android vs. iOS
Mobile Development – Ein Überblick | Markus Stäuble Ein Überblick über die wichtigsten Plattformen und Tools für den Einstieg in die Entwicklung mobiler Anwendungen Android – Wo steht es, wie geht es? | N.N. Was kann die aktuellste Androidversion und welche Hürden muss der Entwickler für einen Einstieg nehmen iOS – Wo steht es, wie geht es? | Markus Stäuble Fähigkeiten der neuesten Version und Hinweise für den Einstieg in die Entwicklung
Developer 2
App Starter Day: Bada vs. MeeGo
Verteilte Versionskontrolle mit Git | Michael Johann Live-Coding Session: Arbeiten mit verteilten Repositories und Git anhand eines kleinen iPad-Projektes
Samsung bada – Wo steht es, wie geht es? | Jens Weller Was kann die Plattform und wie können Entwickler eigene Apps für diese Plattform entwickeln
Modellgetriebene App-Entwicklung | Peter Friese Mit etablierten Open-Source Technologien in Minuten datengetriebene Anwendungen für die Plattformen iPhone und Android entwickeln
MeeGo – Wo steht es, wie geht es? | Jens Weller Diese Session stellt die Plattform MeeGo vor und zeigt Einsteigern wie eine App für MeeGo entwickelt wird.
Datengetriebene iPhone-Anwendungen mit Core Data | Sönke Matz Basisaspekte und Live-Demo: Von der Modellierung bis hin zum Speichern, Laden und Umgang mit den Core Data-Objekten
Plattformübergreifende Entwicklung | Heiko Behrens Eine Anwendung mehrfach zu entwickeln. Die wichtigsten Werkzeuge und Techniken für plattformübergreifende Entwicklung
Abendveranstaltung
Preisverleihung der besten deutschen iPhone App 2010 durch die Redaktionen
Programmänderung vorbehalten
02. Dezember 2010: Entwickler-Panels 09.00 – 09.45
10.00 – 13.30
Keynote
Das iPhone in Ausbildung und Lehre N.N.
Die Integration von iPhone und iPod in existierende e-learning Plattformen bietet einen Weg, das Studienerlebnis durch Podcasts, interaktive Lernmodule und kleine ad-hoc Lerngruppen zeitlich
und räumlich zu flexibilisieren. Die europaweite Einführung des ECTS-Systems bringt aber auch erhöhte Anforderungen an die präzise Bewertung von Studienleistungen.
Developer 3
Developer 4
Hybrid vs. Native Apps
Hybrid Apps – das Beste aus 2 Welten? | Roland Gülle Stärken und Schwächen der Nativen und Web Technologie und Argumente für die Entscheidungsfindung hin zur Entscheidungsmatrix
OpenGL ES 1.1 vs. 2.0 | Marc Hehmeyer Vor- und Nachteile von Shadern im mobilen Einsatz sowie alternative Lösungen unter OpenGL ES 1.1
Mobile Web-Apps – eine Alternative? Matthias Lübken, Philipp Friesen Anhand der Web-App »popula« werden Vor- und Nachteile aufgezeigt so wie praktische Erfahrungen bei der Umsetzung und dem Betrieb
Denn das Ohr hört mit | Gero Goerlich Episches Sounddesign auf iOS mit fmod
Best Practices – Hybrid Apps für iPhone & Co | Salvatore Sferrazza Effiziente Realisierung von Hybrid Apps auf übergreifenden Plattformen
14.30 – 18.00
Developer 3
Lösungen für Fortgeschrittene
Screencasts für iPhone und iPad Apps | Heiko Behrens Wie professionelle Screencasts mit kostengünstigen Werkzeugen und ein wenig Übung selbst erstellt werden können SproutCore-nativer Look & Feel ohne SDK | Johannes Fahrenkrug Eine Einführung in SproutCore mit live Aufbau eines RSS Reader der auf dem iPad nur mit JavaScript, HTML und CSS läuft Spring-Backends für iPhone Apps | Stefan Scheidt Wie mit Hilfe von Spring Roo ein REST-basierter Webservice für eine iPhone-Os-Applikation implementiert wird.
Programmänderung vorbehalten
Kooperationspartner:
Mobile Gaming – Mit der Idee fängt alles an
Performance Optimierung unter iOS | Marc Hehmeyer Von den Großen abgeguckt: Von Batching über LOD bis Texturkompression
Developer 4
Mobile Games in der Übersicht
Game Center im Fronteinsatz | N.N. Das Apple Social Game Network im Vergleich zu anderen Plattformen Mehr Bewegung im Spiel | N.N. Einsatzmöglichkeiten und Ansteuerung des Gyroscope Fliegende Menues leicht gemacht | N.N. Menuegestaltung und -animation mit Hilfe von UI Kit
021_Eindruck_iPhone_2010_A4_v4.qxd:Layout 3
06.08.2010
12:20 Uhr
Seite 23
01. Dezember 2010: Business-Panels 09.00 – 09.45
Business 1
Business 2
Marketingtool Apps
Mobile Trends | Florian Gmeinwieser Aktuelle Trends und neue Technologien im Bereich Mobile Marketing Virtuelle Produkt- und Markeinszenierung | Steffen Trenkle Fortschrittliche Definierung von Trends mit Blick auf die Early Adopters. Richtungsweisende Handlungsempfehlungen Möglichkeiten und Erfolgsfaktoren des iPhones | Alex Sutter Über das persönlichste aller Medien effizient kommunizieren
Business 1
Spracherkennung übertragen auf das iPhone | Reimund Schmald Wie die Idee einer Speech-To-Text Anwendung entstand und wie Entwickler diese Möglichkeit für sich nutzen können POI-Apps - das iPad als Alternative zum PC | Dr. Volker Redder Vom ersten Kundenkontakt über die Zielvorgaben bis zur Implementierung
Finanzierungsmodelle und Marktchancen
M-Commerce ist wie E-Commerce. Nur mehr. | Martin Cserba Die Kluft zwischen uns und den Dingen die wir jetzt im Moment haben wollen, schafft uns die größten Chancen im M-Commerce
Geschäftsmodelle für mobile Applikationen | Carsten Frien Konkrete Fallbeispiele, Tools und Lösungsansätze um Applikationen von Mitbewerbern abzuheben und für tragfähige Geschäftsmodelle
Markteintrittsstrategien | Christian Lupp Strategien für den erfolgreichen Markteintritt innerhalb des AppStore, ausgehend vom exklusiv für das iPad entwickelten Twitter-Client »TweetStrip«
Eine App, alle Shops | Shopgate Einmal registrieren und in 400 Shops einkaufen. Über 2 Millionen Produkte immer und überall dabei
10.00 – 13.30
Konzeption einer App | Bernd Lindemann Die Planungsstufen bei der Konzepterstellung einer App damit diese ein Erfolg wird.
Business 2
M-Commerce
Von der Idee zur App
14.30 – 18.00
Über Erfolg und Misserfolg im Ausland | Thorsten Rauser In einem Markt mitspielen, dessen Sprache und Kultur man nicht kennt
Die Gewinner der besten deutschen iPhone App werden im Rahmen der Abendveranstaltung bekannt gegeben
ab 18.00
02. Dezember 2010: Business-Panels 09.00 – 09.45
Business 3
iPhone im Unternehmenseinsatz
Business 4
iPhone vs. iPad - Eine Entscheidungshilfe
iPhone im Unternehmen | Thomas Bröckers Integration neuer Hardware in die vorhandene Infrastruktur. Ein Überblick über die Möglichkeiten der Absicherungsmaßnahmen
iPhone vs. iPad | Ingo Dellwig Das iPad aus mehreren Blickwinkeln: Unterschiede und Gemeinsamkeiten
Hybrid Apps für iPhone & Co | Steffen Schlimmer Vorteile von Hybrid Apps hinsichtlich Wirtschaftlichkeit, Management und Technologie
9,7 Zoll – Usability und Konzeption fürs iPad | Benno Bartels Usability-Do's und Dont's auf dem iPad. Wertvolle Konzeptideen für eigene iPad-Apps
Applikationen und Recht | Dr. Thomas Sassenberg Fragen aus dem Wettbewerbs- und Urheberecht so wie Beispiele zur Berücksichtigung bei der Integration von iAds
iPad als Hoffnungsträger der Verlagsbranche? | Steffen Trenkle Die Rolle des iPads aus analystischer Sicht und das Vertriebsmodell iTunes Store als profitable Zukunft für digitales Publishing
Business 3
Business 4
Mobile Social Networks
Blick über den Tellerrand
Soziale Apps – Menschen finden zusammen | Markus Franz Strategien für das junge Segment der Social Apps (Case Study von match2blue enthalten)
Ein Blick über den iPhone-App Tellerrand | Felix Heimbrecht Die mobile Welt jenseits des iPhones: mobile Portale, Multi-Plattform Apps, Mobile-Application Server und das entstehende Web of Things
Mobile Business Networking – XING anywhere | Philipp Mühlenkord Anforderungen an die mobile Version eines geschäftlichen Netzwerks
The Developer Ecosystgem – Nokia’s Role in the Game Jens Dissmann
Gowalla – Eine kleine Reise durch Köln | Jürgen Walleneit Die steigende Akzeptanz und Wichtigkeit von Location Based Services präsentiert anhand einer virtuellen Reise durch die Stadt
Windows Phone 7 – Das etwas andere Smartphone Dr. Frank Prengel Als Geschäftskunde, Inhalte-Anbieter, Entwickler oder Designer vom künftigen Erfolg von Windows Phone 7 profitieren
10.00 – 13.30
14.30 – 18.00
Änderungen vorbehalten
021_Eindruck_iPhone_2010_A4_v4.qxd:Layout 3
06.08.2010
12:20 Uhr
Seite 24
Anmeldung iPhone developer conference im Gürzenich Köln, 01./02. Dezember 2010
Konferenzticket Ich bestelle ein Zwei-Tagesticket für die iPhone developer conference inkl. Fachmessebesuch am 01. und 02. Dezember 2010 für € 699,– zzgl. MwSt. (Frühbucherpreis bis 13. Oktober 2010, danach € 799,– zzgl. MwSt.) Ich bestelle ein Ein-Tages-Ticket für die iPhone developer conference 2010 inkl. Fachmessebesuch für € 399,– zzgl. MwSt. (Frühbucherpreis bis 13. Oktober 2010, danach € 499,– zzgl. MwSt.) für den 01. Dezember 2010
Neue Mediengesellschaft Ulm mbh Kongresse & Messen Claudia Fink Bayerstraße 16a 80335 München
Ja, ich möchte mich nur für den kostenlosen Fachmessebesuch registrieren (Ohne Vorabregistrierung ist der Besuch kostenpflichtig)
1. Anmeldung Wir bestätigen Ihre Anmeldung per E-Mail.
Pre-Conference Ja, ich möchte mich nur für die kostenlose Pre-Conference am 30. November 2010 in der Universität zu Köln anmelden.
Ja, senden Sie mir kostenlos die nächste erreichbare Ausgabe der vierteljährlich erscheinenden Fachzeitschrift MAC DEVELOPER. Soll sich das Kennenlernabonnement nicht in ein reguläres Abonnement umwandeln, teilen Sie uns das bitte bis spätestens 14 Tage nach Erhalt der Leseprobe schriftlich mit. Ansonsten erhalten Sie das MAC-DEVELOPER-Abo zum Vorzugspreis von z.Zt. 12,70 Euro je Ausgabe oder 50,80 Euro im Jahr jeweils inklusive MwSt. und Versand. So sparen Sie fast 15% gegenüber dem Kauf am Kiosk. In Österreich betragen die entsprechenden Preise 13,95 Euro je Ausgabe oder 55,80 Euro im Jahr, in der Schweiz 25,25 Franken bzw. 101,00 Franken im Jahr. Das Abonnement verlängert sich automatisch um ein Jahr, wenn es nicht sechs Wochen vor Ablauf der Bezugszeit schriftlich gekündigt wird.
Anmeldedaten (* Pflichtfelder) Vorname:*
Nachname:*
Firma:
Abteilung:
Land:
Telefon:
Fax:
E-Mail:* Hiermit melde ich mich zu der vorgenannten Veranstaltung an und bestätige, dass ich die AGBs gelesen und akzeptiert habe.
Datum / Unterschrift: Wir machen Sie ausdrücklich auf Ihr Widerrufsrecht gegen die Speicherung und Verwendung Ihrer personenbezogenen Daten zu Werbe- und Marketingzwecken aufmerksam. Sie können Ihre Einwilligung zur Verwendung Ihrer persönlichen Daten zu den vorgenannten Zwecken jederzeit für die Zukunft widerrufen. Hierzu genügt eine E-Mail an [email protected] oder eine Mitteilung in Textform (Fax, Brief) an die vorstehend genannte Adresse.
3. Leistungserbringung und Rücktrittsvorbehalt Wir behalten uns vor, inhaltliche und zeitliche Änderungen im Veranstaltungsprogramm und bei der Besetzung der Referenten vorzunehmen. NMG ist berechtigt vom Vertrag zurückzutreten, wenn die für eine wirtschaftliche Durchführung der Veranstaltung erforderliche Zahl an Ausstellern und Sponsoren nicht erreicht wird, der Hauptveranstalter die Veranstaltung nicht durchführt oder sonstige nicht im Verantwortungsbereich der NMG liegende Gründe vorliegen, die die Durchführung der Veranstaltung unmöglich machen. In diesem Falle wird der Besucher unverzüglich be-nachrichtigt und die bereits geleistete Zahlung unverzüglich erstattet. Weitergehende Ansprüche des Besuchers sind ausgeschlossen, soweit NMG nicht Vorsatz oder grobe Fahrlässigkeit zur Last liegt. 4. Stornierung/Umbuchung Sie können ihre Anmeldung nur bis 30 Tage vor Beginn der Veranstaltung stornieren; bereits entrichtete Teilnahmegebühren werden in diesem Fall innerhalb von 30 Tagen rückerstattet. Die Stornierung hat schriftlich an Neue Mediengesellschaft Ulm mbH, Bayerstrasse 16a, D-80335 München oder per Fax an +49 (0)89-74117-448 zu erfolgen. Die Benennung eines Ersatzteilnehmers ist jederzeit kostenfrei möglich.
Straße, Nr.:* PLZ/Ort:*
2. Zahlungsbedingungen Der Rechnungsbetrag ist 14 Tage nach Erhalt der Rechnung, spätestens am Tag des Besuchs der ersten gebuchten Veranstaltung fällig und ab dann mit 8 %-Punkten über dem Basiszinssatz zu verzinsen.
5. Datenschutzhinweise Wir weisen darauf hin, dass personenbezogene Daten des Ausstellers nach den Bestimmungen des Bundesdatenschutzgesetzes (BDSG) sowie des Telemediengesetzes (TMG) erhoben, verarbeitet und genutzt werden. Alle über unsere Webseite erhobenen personen-bezogenen Daten werden entsprechend den gesetzlichen Vorgaben behandelt und nicht an Dritte weitergegeben. Externe Dienstleister, die in unserem Auftrag Daten verarbeiten, sind ebenfalls den gesetzlichen Vorschriften verpflichtet, gelten jedoch nicht als Dritte. Ihre bei der Anmeldung erhobenen personenbezogenen Daten werden an die Aussteller und Sponsoren der von uns durchgeführten Veranstaltungen weitergegeben. Von dort können Sie weiterführende Marketinginformationen erhalten. Wir machen Sie auf Ihr Widerrufsrecht gegen die Speicherung und Verwendung Ihrer personenbezogenen Daten zu Werbe- und Marketingzwecken aufmerksam. Sie können Ihre Einwilligung zur Verwendung Ihrer persönlichen Daten zu den vorgenannten Zwecken jederzeit für die Zukunft widerrufen. Hierzu genügt eine E-Mail an [email protected] oder eine Mitteilung in Textform (Fax, Brief) an uns.
www.iphonedevcon.de
025_X_Code_2_jp_ea.qxp
09.08.2010
17:00 Uhr
Seite 25
Titelthema Xcode im Zusammenspiel mit dem iPhone SDK ●
iPhone-Entwicklung mit Core Data
Daten im Griff Mit Core Data steht ein Framework für die Datenverwaltung im iPhone SDK zur Verfügung. Wie Sie damit effizient in Xcode arbeiten, verrät Ihnen dieser Workshop. Erica Sadun Auf einen Blick In diesem Workshop zeigt Erica Sadun, wie Entwickler in Xcode mit Core Data arbeiten, einem Framework, das erstmals im iPhone SDK 3.0 vorgestellt wurde. Damit ist die Datenverwaltung auf dem iPhone mit Objective-C statt mit SQL-Anfragen möglich. Die Listings zum Artikel finden Sie auf unter www.macdeveloper.de im Download-Bereich.
Im Modell-Editor legen Sie Objektdefinitionen für Core-Data-Anwendungen an (Bild 1)
C
ore Data vereinfacht die Erstellung und Verwaltung von Objekten in einer Anwendung. Vor dem SDK 3.0 erfolgten die Datenverwaltung und der SQL-Zugriff ausschließlich über sehr systemnahe Bibliotheken. Das war weder elegant noch einfach anzuwenden. Jetzt ist Core Data in die Framework-Familie von Cocoa Touch aufgenommen worden und ermöglicht eine einfachere Datenverwaltung auf dem iPhone. Core Data bietet eine flexible Infrastruktur für die Objektverwaltung, Werkzeuge für die Arbeit mit dauerhaften Datenspeichern und Lösungen für den gesamten Lebenszyklus eines Objekts. Im MVC-Programmierprinzip (Model–View– Controller) gehört Core Data zum Modell. Die anwendungsspezifischen Daten werden außerhalb der grafischen Bedienoberfläche definiert und gesteuert. Daher sind die Anwendungsdelegierung, die Instanzen des Ansichtscontrollers und die maßgeschneiderten Modellklassen die Orte, an denen die Funktionen von Core Data Gestalt annehmen. Wen Sie zum Besitzer der Objekte machen, hängt davon ab, wie Sie die Daten verwenden. In der Regel besitzt die Anwendungsdelegierung gewöhnlich alle gemeinsam genutzten Datenbanken, die in der Anwendung eingesetzt werden. In einfacheren Anwendungen kann ein einziger Ansichtscontroller ausreichen, der sich auch um den Datenzugriff kümmert. Entscheidend ist, dass der Besitzer den gesamten Datenzugriff steuert – das Lesen, Schreiben und Ak-
tualisieren. Jeder Teil der Anwendung, der auf Core Data zurückgreift, muss sich mit diesem Besitzer koordinieren. Das Datenmodell der Anwendung ist von der grafischen Oberfläche getrennt, doch die Daten existieren nicht im luftleeren Raum. Das SDK 3.0 arbeitet nahtlos mit UITableView-Instanzen zusammen. Die Controller-Klasse für abgerufene Ergebnisse in Cocoa Touch wurde für die Verwendung mit Tabellen entworfen und bietet praktische Eigenschaften und Methoden für die Aufnahme der Daten in Tabellen.
Modelldateien erstellen und bearbeiten
▲
Modelldateien definieren, wie Core-Data-Objekte strukturiert sind. Jedes Projekt, das mit dem Framework Core Data verknüpft ist, enthält mindestens eine Modelldatei. Diese XCDATAMODEL-Dateien definieren die Objekte sowie deren Attribute und Beziehungen. Jedes Objekt kann eine Reihe sogenannter Attribute als Eigenschaften aufweisen. Mögliche Attributtypen sind Strings, Datumsangaben, Zahlen und Daten. Jedes Objekt kann außerdem über Beziehungen verfügen, also über Verknüpfungen zu anderen Objekten. Dabei kann es sich um einzelne (1:1-Beziehung) oder mehrfache (1:n-Beziehung) Verbindungen handeln. Diese Beziehungen verlaufen in einer Richtung, können aber auch umgekehrt werden, um eine inverse Beziehung anzuzeigen. Das Modell definieren Sie in Xcode, indem Sie eine neue Datenmodelldatei gestalten. Dazu wählen Sie File, New File, iPhone OS, Resource, Data Model, Next. Geben Sie einen Namen für die neue Datei ein, und klicken Sie auf Next und dann auf Finish. Xcode fügt die neue Modelldatei zu Ihrem Projekt hinzu. Klicken Sie doppelt auf die XCDATAMODEL-Datei, um sie in einem Editorfenster zu öffnen (Bild 1). In der Liste oben links fügen Sie neue Objektentitäten hinzu (im Grunde genommen Klassen), und oben in der Mitte definieren Sie Attribute und Beziehungen (letzten Endes sind das Instanzvariablen). Oben rechts finden Sie ein kontextabhängiges Informationsfeld. Darunter erscheint ein Objektdiagramm, das die von Ihnen definierten Entitäten in einem Raster grafisch darstellt. Um dem Modell eine neue Entität hinzuzufügen, klicken Sie unten links in
4/2010 www.mac-developer.de
25
025_X_Code_2_jp_ea.qxp
09.08.2010
17:00 Uhr
Seite 26
Titelthema Xcode im Zusammenspiel mit dem iPhone SDK ●
der Liste Entity auf die Schaltfläche +. Standardmäßig werden alle neuen Entitäten zur Laufzeit als Instanzen der Klasse NSManagedObject instanziert. Um dem Objekt einen Namen zu geben, zum Beispiel Person, ändern Sie den hier vorgeschlagenenen Namen Entity.
Listing 2: Im Kontext - (void) initCoreData { NSError *error; // Pfad zur SQLite-Datei NSString *path = [NSHomeDirectory() 3 stringByAppendingString:@"/Documents/cdintro_00.sqlite"]; NSURL *url = [NSURL fileURLWithPath:path]; // Initialisiert das Modell NSManagedObjectModel *managedObjectModel = 3 [NSManagedObjectModel mergedModelFromBundles:nil]; // Richtet den Koordinator für den dauerhaften Speicher ein NSPersistentStoreCoordinator *persistentStoreCoordinator = 3 [[NSPersistentStoreCoordinator alloc] 3 initWithManagedObjectModel:managedObjectModel]; if (![persistentStoreCoordinator 3 addPersistentStoreWithType:NSSQLiteStoreType 3 configuration:nil URL:url options:nil error:&error]) 3 NSLog(@"Error %@", [error localizedDescription]); else { // Erstellt den Kontext und weist den Koordinator zu self.context = 3 [[[NSManagedObjectContext alloc] init] autorelease]; [self.context 3 setPersistentStoreCoordinator:persistentStoreCoordinator]; } [persistentStoreCoordinator release]; }
26
Wenn eine Entität markiert ist, können Sie ihr Attribute hinzufügen. Wie bei der Definition von Instanzvariablen geben Sie auch einem Attribut einen Namen und einen Typ. Beziehungen sind Zeiger zu anderen Objekten. Sie können einen einzelnen Zeiger für eine 1:1-Beziehung (zum Beispiel ein Leiter für eine Abteilung) oder einen Satz von 1:n-Beziehungen (alle Mitglieder einer Abteilung) einrichten. Beachten Sie das Informationsfeld oben rechts. Hier können Sie den Namen des Objekts ändern, seinen Typ festlegen, seinen Standardwert angeben und noch einiges mehr. Das Diagramm in der unteren Hälfte des Editors zeigt die definierten Entitäten und die Beziehungen zwischen ihnen durch Pfeile an. In dem Modell aus der Abbildung gehört jede Person zu einer Abteilung. Eine Abteilung hat einen Leiter (1:1-Beziehung) und eine beliebige Anzahl von Mitgliedern (1:n-Beziehung).
Header-Dateien erstellen Nachdem Sie das Modell gestaltet und gespeichert haben, können Sie Header-Dateien für die einzelnen Entitäten erstellen. Diese Dateien sind nicht erforderlich, erlauben Ihnen aber, in Ihren Anwendungen die Punktschreibweise zu nutzen, anstatt valueForKey:-Aufrufe einzusetzen, um die Attribute verwalteter Objekte abzurufen. Markieren Sie eine Entität, und wählen Sie File, New File, iPhone OS, Cocoa Touch Class, Managed Object Class, Next, Next, Finish. Xcode erstellt ein Paar aus einer H- und einer M-Datei für die Entität und fügt sie dem Projekt hinzu. Die Header-Datei für die Abteilung aus dem Projekt von Bild 1 sieht beispielsweise wie Listing 1 aus. Sie können erkennen, dass der Gruppenname ein String ist, dass der Abteilungsleiter (manager) auf ein anderes verwaltetes Objekt zeigt und dass die 1:n-Beziehungen der Mitglieder als ein Satz definiert sind. Diese Datei sieht wie ein normaler KlassenHeader in Objective-C aus, doch gibt es keine Implementierungen. Das erledigt Core Data mit dem Compiler-Schlüsselwort @dynamic. @implementation Department @dynamic groupName; @dynamic manager; @dynamic members; @end Der Hauptgrund dafür, Core-Data-Dateien zu erstellen, liegt darin, dass sie dem Projekt neue Verhaltensweisen und flüchtige Attribute hinzufügen können, also Attribute, die nicht im dauerhaften Speicher abgelegt werden. Beispielsweise können Sie das Attribut fullName erstellen, das aus den Attributen firstName und
mac-developer 4/2010
025_X_Code_2_jp_ea.qxp
09.08.2010
17:00 Uhr
Seite 27
Titelthema Xcode im Zusammenspiel mit dem iPhone SDK ●
lastName einer Person zusammengesetzt wird. Außerdem hindert Sie nichts daran, eine Klasse für verwaltete Objekte wie jede andere Klasse zu nutzen, also alle Arten von Daten zu verwenden und zu bearbeiten. Sie können jegliches Objective-C-Verhalten in eine Instanz verwalteter Objekte aufnehmen, indem Sie die Implementierungsdatei bearbeiten. Fügen Sie Instanz- und Klassenmethoden nach Bedarf hinzu.
Listing 3: Objekte hinzufügen - (void) addObjects { // Fügt eine neue Abteilung hinzu Department *department = (Department *)[NSEntityDescription 3 insertNewObjectForEntityForName:@"Department" 3 inManagedObjectContext:self.context]; department.groupName = @"Office of Personnel Management"; // Fügt eine Person hinzu Person *person1 = (Person *)[NSEntityDescription 3 insertNewObjectForEntityForName:@"Person" 3 inManagedObjectContext:self.context];
Einen Core-Data-Kontext erstellen Nachdem Sie das Modell für die verwalteten Objekte erstellt haben, ist es an der Zeit, den Code zu schreiben, der auf eine Datendatei zugreift. Um mit Core Data zu arbeiten, müssen Sie im Programm einen Kontext für verwaltete Objekte erstellen. Dieser Kontext führt alle Zugriffs- und Aktualisierungsfunktionen aus, die erforderlich sind, um das Modell und die Datei aufeinander abzustimmen. Die Methode in Listing 2 initialisiert den Kontext für eine Anwendung. Wenn Sie eine vorgefertigte Core-Data-Vorlage verwenden, ist diese Arbeit für Sie schon erledigt. Anhand der hier vorgestellten Methode können Sie ablesen, wie Sie diese Aufgabe manuell lösen. Als Erstes werden sämtliche Modelldateien aus dem Anwendungs-Bundle gelesen und zu einem zentralen Modell zusammengefasst. Anschließend initialisiert die Methode einen Koordinator für den dauerhaften Speicher. Dieser Koordinator bietet einen systemnahen Dateizugriff über das zentrale Modell. Dazu geben Sie einen URL an, der auf die Datei zeigt, in der die Modelldaten gespeichert werden sollen. Abschließend initialisiert die Methode mit Hilfe des Koordinators den neuen Kontext und speichert ihn als beibehaltene Instanzvariable. Es ist wichtig, eine Kontextinstanz zu unterhalten, auf die Sie von einem Ansichts-Controller (für einfache Core-Data-Anwendungen) oder von der Anwendungsdelegierung (für kompliziertere Anwendungen) aus verweisen können. Der Kontext wird für sämtliche Lese-, Schreib- und Aktualisierungsvorgänge in Ihrer Anwendung eingesetzt.
Neue Objekte hinzufügen
4/2010 www.mac-developer.de
person1.birthday = [self dateFromString:@"12-1-1901"]; person1.department = department; // Fügt eine weitere Person hinzu Person *person2 = (Person *)[NSEntityDescription 3 insertNewObjectForEntityForName:@"Person" 3 inManagedObjectContext:self.context]; person2.name = @"Jane Doe"; person2.birthday = [self dateFromString:@"4-13-1922"]; person2.department = department; // Richtet die Abteilungsbeziehung ein department.manager = person1; // Legt die Daten im dauerhaften Speicher ab NSError *error; if (![self.context save:&error]) 3 NSLog(@"Error %@", [error localizedDescription]); }
jede Abteilung hat eine Reihe von Mitgliedern und einen Leiter. Der Code folgt wieder dem abgebildeten Entwurf. Für eine Abteilung müssen Sie nicht explizit die Mitglieder angeben, denn wenn Sie das Attribut department einer Person festlegen, fügt die inverse Beziehung sie als Mitglied zu der Abteilung hinzu. Änderungen am dauerhaften Speicher erfolgen erst, wenn Sie speichern. Der Speichervorgang bringt die Datenbankdatei auf den Stand des Modells im Arbeitsspeicher. Die einzige Speicheranforderung in diesem Code weist den Kontext an, seinen Status mit dem dauerhaften Speicher zu synchronisieren und alle Änderungen in die Datenbankdatei zu schreiben. Wenn Sie diesen Code im Simulator ausführen, können Sie die dadurch erstellte SQLITEDatei auf einfache Weise einsehen. Wechseln Sie zum Simulator-Ordner (~/Library/Application Support/iPhone Simulator/User/Applications) und dort zum Ordner für die Anwendung selbst. In einem Dokumentenordner, dessen Pfad von dem URL abhängt, den Sie zum Erstellen des dauerhaften Speichers verwendet haben, finden Sie eine SQLITE-Datei mit der Datenbankdarstellung, die Sie generiert haben. Mit dem Kommandozeilenwerkzeug sqlite3 können Sie den Inhalt dieser Datei untersu-
▲
Neue Objekte erstellen Sie, indem Sie Entitäten in den verwalteten Kontext einfügen. Im Codefragment in Listing 3 werden drei neue Elemente erzeugt, nämlich eine Abteilung und zwei Personen. Nachdem Sie das verwaltete Objekt eingefügt haben, wodurch eine neue Instanz zurückgegeben wird, legen Sie seine Eigenschaften (die Attribute und Beziehungen) durch Zuweisung fest. Jede Person gehört zu einer Abteilung,
person1.name = @"John Smith";
27
025_X_Code_2_jp_ea.qxp
09.08.2010
17:00 Uhr
Seite 28
Titelthema Xcode im Zusammenspiel mit dem iPhone SDK ●
chen, indem Sie den Befehl .dump verwenden. Im Download-Bereich von mac-developer finden Sie ein Beispiel für die Definition von zwei SQL-Tabellen (department und manager), in denen die Informationen der einzelnen Objekte abgelegt werden, sowie die notwendigen Einfügebefehle, um die im Code erstellten Instanzen zu speichern.
Die Datenbank abfragen Um Objekte aus der Datenbank abzurufen, führen Sie eine fetch-Anforderung durch, in der Sie die Suchkriterien angeben. Diese Anforderung wird weitergeleitet und zur Initialisierung eines Ergebnisobjekts mit einem Zeiger auf die verwalteten Objekte verwendet, die den Kriterien entsprechen. Der Ergebniscontroller führt den Abruf durch, bevor er dieses Array passender verwalteter Objekte zurückgibt.
Die Methode fetchObjects erstellt eine Anforderung und setzt den Entitätstyp dabei auf Person, so dass bei der Suche nach Person-Objekten im gemeinsam genutzten verwalteten Speicher geforscht wird. Jede Anforderung muss mindestens einen Sortierungsdeskriptor enthalten. In Listing 4 wird eine Liste von Person-Datensätzen zurückgegeben, die in aufsteigender Reihenfolge nach dem Namensfeld geordnet sind. Der Code zeigt nur die einfachste Form einer Anfrage: „Gib alle verwalteten Elemente eines bestimmten Typs zurück!“. Sie können auch weit vielschichtigere Abfragen aufstellen. Die fetch-Anforderung wird verwendet, um ein NSFetchedResultsController-Objekt zu initialisieren. Diese Klasse verwaltet die von einem Core-Data-Abruf zurückgegebenen Daten. Der Ergebniscontroller wird über eine beibehaltene Klasseneigenschaft (self.results) verfügbar gemacht. Abrufergebnisse bieten konkreten Zugriff auf Objekte des Datenmodells. Nachdem die Daten abgerufen sind, listet die Methode listPeople: die einzelnen Personen nach Name und Abteilung auf. Dazu zieht sie die Eigenschaft fetchedObjects des Ergebnisses heran.
[fetchRequest setEntity:[NSEntityDescription 3 entityForName:@"Person" inManagedObjectContext:self.context]]; // Fügt einen Sortierungsdeskriptor hinzu. Dies ist erforderlich. NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] 3 initWithKey:@"name" ascending:YES selector:nil]; NSArray *descriptors = [NSArray arrayWithObject:sortDescriptor]; [fetchRequest setSortDescriptors:descriptors]; [sortDescriptor release]; // Initialisiert den Controller für abgerufene Ergebnisse NSError *error; self.results = [[NSFetchedResultsController alloc] 3 initWithFetchRequest:fetchRequest managedObjectContext:self.context 3 sectionNameKeyPath:nil cacheName:@"Root"];
Im iPhone SDK steht Entwicklern auf Grundlage von Cocoa Touch eine Möglichkeit zur Verfügung, Daten auf dem iPhone zu verwalten. Sie haben gesehen, wie Sie in Xcode über dieses Framework Objekte hinzufügen, speichern und abfragen. Allerdings kann auch das Entfernen von Objekten schwieriger sein, als Sie denken mögen, vor allem dann, wenn diese Objekte nicht nur einfache Eigenschaften, sondern Beziehungen aufweisen. Das Buch von Erica Sadun hält auch hier die passenden Lösungen bereit. [jp]
self.results.delegate = self; if (![[self results] performFetch:&error]) 3 NSLog(@"Error %@", [error localizedDescription]); [self.results release]; [fetchRequest release]; } - (void) listPeople { [self fetchObjects]; if (!self.results.fetchedObjects.count) { NSLog(@"Database has no people at this time"); return; } NSLog(@"People:"); for (Person *person in self.results.fetchedObjects) 3 NSLog(@"%@ : %@", person.name, person.department.groupName); }
28
Die Autorin 3 Erica Sadun ist Computerwissenschaftlerin und erfahrene Entwicklerin. Sie sammelt Gadgets aller Art und programmiert vor allem für die Mac-Plattform und das iPhone. Sie hat dazu auch schon mehrere Fachbücher verfasst. Der vorliegende Artikel enthält Auszüge aus dem aktuellen Standardwerk „Das große iPhone Entwicklerbuch“. Titel: Das große iPhone Entwicklerbuch Autorin: Erica Sadun ISBN: 978-3-82732917-2 Verlag: Addison-Wesley Umfang: 960 Seiten Preis: 59,80 Euro
mac-developer 4/2010
029_AZ_Dell_A4_V2.qxd:Layout 1
06.08.2010
14:27 Uhr
Seite 29
Gewinnen Sie einen Dell -Laserdrucker! TM
Machen Sie mit beim großen Dell TM-Gewinnspiel und gewinnen Sie einen von 10 hochwertigen Dell TM-Laserdruckern! Einfach unter www.druckergewinnen.de 10 Fragen beantworten und schon sind Sie mit dabei!
r rucke d r e s a ll -L ! 10 De innen w e g zu TM
chluss s e d n Einse tober 15. Ok
7.-10. Preis: Je ein Laserdrucker Dell 1130 TM
4.-6. Preis: Je ein Farblaserdrucker Dell 1320c TM
2.-3. Preis: Je ein Farblaserdrucker Dell 3130 cn TM
1. Preis: Dell 3115 cn TM
Leistungsstarker Multifunktions-Farblaserdrucker für Netzwerkdruck, Scannen, Faxen und als Einzelkopiergerät. 30 s/w-Seiten bzw. 17 Farbseiten pro Minute.
Eine gemeinsame Aktion von
www.druckergewinnen.de
030_Debugging_jp_ea.qxp
09.08.2010
16:12 Uhr
Seite 30
Titelthema Debugging mit Xcode ●
Die Debugging-Werkzeuge von Xcode
Dem Fehler auf der Spur Bei der Entwicklung schleichen sich immer wieder logische Fehler ein, die sich erst in der Programmausführung bemerkbar machen. Diesen kommen Sie am besten mit Hilfe des Debuggers auf die Schliche. Andreas Hitzig Auf einen Blick Die Fehlersuche ist bei der Programmierung eine zeitaufwendige Angelegenheit. Xcode unterstützt Sie dabei mit zahlreichen Werkzeugen und dem Sichten auf Variablen, Funktionen und Methoden. Im mac-developerWorkshop verrät Ihnen Andreas Hitzig mehr über das Debugging innerhalb von Xcode und die verschiedenen Debugger-Ansichten.
Sie können direkt aus der Entwicklungsumgebung im Texteditor debuggen (Bild 1)
M
it einem Debugger führen Sie ein Programm schrittweise aus und können zu jedem beliebigen Zustand einzelne Variablen oder interne Tabellen untersuchen. Innerhalb von Xcode haben Sie eine größere Auswahl, in welchem Umfeld Sie Ihre Programmzeilen untersuchen möchten. Für das Debugging gibt es insgesamt vier verschiedene Umfelder (siehe nebenstehenden Kasten „Debugging-Möglichkeiten in Xcode“), unter anderem den Texteditor (Bild 1). Die Programmentwicklung und Kompilierung hat in letzter Konsequenz das Ziel, ein fertiges Programm oder Produkt zu generieren. Bis Sie jedoch dahin gelangt sind, benötigen Sie einige Zwischenschritte, wie etwa die Fehlersuche, wenn es nicht wie geplant funktioniert. Für diesen Zweck sollten Sie in Ihrer Entwicklungsumgebung einige Voreinstellungen vornehmen. Die Konfiguration der Debugging-Umgebung nehmen Sie in den Einstellungen über die Option Debugging vor (Bild 2). Dort legen Sie die Schriftart und Farbe des Textes in der Konsole fest, die Farbe des Instruction Pointers – ein farbiger Zeiger beim Debuggen – und das Verhalten beim Start einer Anwendung aus Xcode heraus. Dabei definieren Sie verschiedene Aktionen (siehe Kasten „Debugging-Aktionen“, Seite 31). Weiter legen Sie in diesem Fenster das Ausgabeverzeichnis der GDB-Logdateien (GNU Project Debugger) fest, das Verhalten beim Laden von Symbolen, das Format bei der Disassemblierung, das Aussehen des Texteditors und das Verhalten beim Start einer Debugging-Session.
Debugging-Möglichkeiten in Xcode Texteditor: Analyse nahe an der Programmierung – mit diesem Werkzeug ist die Untersuchung von Werten einzelner Variablen ebenso möglich wie das schrittweise Abarbeiten von Programmzeilen. Mini Debugger: Die Fehlersuche wird in einem zusätzlichen Fenster durchgeführt, wobei der Blick auf die Applikation gerichtet ist. Debugger: Grafischer Debugger mit dem größten Leistungsumfang. Konsole: Puristischer Ansatz über ein Konsolenfenster.
Wenn Sie die Option In-Editor Debugger Controls aktiviert haben, erscheint beim Debugging im Texteditor eine Symbolleiste mit den wichtigsten Funktionen. Wenn Sie beim Start des Debuggings eine leere Konsole sehen möchten, aktivieren Sie die Option Auto Clear Debug Console. Stellen Sie für die Fehlersuche Xcode am besten auf den Ein-Fenster-Modus um – dies geschieht über Xcode, Preferences, General, Layout, All-in-One. Damit Sie diese Einstellung vornehmen können, darf aktuell allerdings kein Projekt geöffnet sein. Anschließend steht Ihnen auch in Ihrem Projektfenster an der linken oberen Ecke ein Umschalter zwischen der normalen Entwicklungsansicht und einer Debugger-Sicht zur Verfügung.
Code fürs Debugging vorbereiten Damit Sie innerhalb eines Programms auf Fehlersuche gehen können, müssen Sie zusätzlich in der Symbolleiste des Projekts innerhalb von Active Configuration die Konfiguration von Release auf Debug umstellen. Die Fehlersuche starten Sie am besten, indem Sie entweder am Start Ihrer Anwendung oder an einer kritischen Stelle einen Breakpoint setzen.
Breakpoints setzen und löschen Dazu haben Sie mehrere Optionen: Am einfachsten bewegen Sie Ihren Cursor im Texteditor in die Spalte mit den Zeilennummern und klicken diese an. Der Breakpoint wird durch einen farbigen Pfeil gekennzeichnet. Außerdem steht Ihnen innerhalb des Menüs Run auch der
30
mac-developer 4/2010
030_Debugging_jp_ea.qxp
09.08.2010
16:12 Uhr
Seite 31
Titelthema Debugging mit Xcode ●
Punkt Manage Breakpoint, Add Breakpoint zur Verfügung, über den Sie weitere Haltepunkte definieren können. Der Texteditor bietet Ihnen zusätzlich noch eine Reihe weiterer Funktionen, unter anderem auch zum Setzen und Löschen von Breakpoints (Bild 3). Darüber erfahren Sie auf den folgenden Seiten mehr. Es gibt insgesamt drei Arten von Breakpoints, die Sie setzen können. Dies sind zum einen die klassischen Breakpoints, die über die Zeilennummer gesetzt werden und das Programm genau an dieser Stelle zur weiteren Untersuchung stoppen. Darüber hinaus gibt es symbolische Breakpoints, die beim Aufruf einer Routine die Ausführung anhalten, und einen letzten Typ, der beim Auftreten einer definierten Ausnahme die Ausführung stoppt. Beim letzten Fall gibt es zwei Ausprägungen für die Programmiersprachen C++ und Objective-C. Abhängig von der Art des Projekts bekommen Sie jeweils die passende Alternative angeboten. Diese Breakpoints setzen Sie wie oben beschrieben über das Menü Run.
Verwaltung von Breakpoints
In der Übersicht der Breakpoints (Bild 4) finden Sie immer den Namen des Breakpoints, die Position, eventuelle Bedingungen und eine Information darüber, um welchen Typ von Haltepunkt es sich handelt – dies wird durch ein Symbol dargestellt: Mit einem Dateisymbol wird eine Ausnahme oder ein Haltepunkt an einer bestimmten Programmzeile gekennzeichnet, die blaue Box dagegen weist auf einen symbolischen Breakpoint hin.
Autor
Setzen von erweiterten Haltepunkten Einen symbolischen Breakpoint nutzen Sie für die Überwachung des Aufrufs einer Methode oder Funktion. Dieser Aufruf kann sowohl statisch als auch dynamisch sein. Wählen Sie zum Setzen des Breakpoints den Menüpunkt Run, Manage Breakpoints, Add Symbolic Breakpoint. Geben Sie im Dialog den Namen der Methode oder der Funktion an, die den Auslöser darstellt. Wenn Sie den Aufruf einer Objective-C-Methode überwachen möchten, müssen Sie bei der Eingabe den Namen der Methode in eckige Klammern setzen und vor den Ausdruck ein Minuszeichen. Mit dem Debugger kommen Sie nicht nur den fehlerhaften Methoden und Funktionen auf die Spur, sondern auch Abbrüchen, die durch
Andreas Hitzig arbeitet als Leiter Anwendungen in einem großen deutschen Buchverlag und ist seit Jahren als Autor von Fachartikeln und Büchern tätig.
▲
Für die Breakpoints gibt es ein eigenes Fenster, in dem sie verwaltet werden. Dies rufen Sie über Run, Show, Breakpoints auf. Das Fenster ist in zwei verschiedene Bereiche unterteilt: eine Baumstruktur auf der linken Seite, in der die verschiedenen Breakpoints gruppiert sind, und eine Liste im Hauptfenster, in der die Haltepunkte aufgelistet sind. Xcode unterscheidet bei den Gruppen in projektbezogene Breakpoints und globale Breakpoints. Im ersten Fall werden diese in der Projektdatei des Benutzers gespeichert und stehen innerhalb des Projekts zur Verfügung. Globale Haltepunkte werden an den übergreifenden Routinen gespeichert und können auch außerhalb der Projekte beim Zugriff auf die jeweiligen Programmzeilen genutzt werden. In der Übersicht erfahren Sie eine Menge Details zu jedem Breakpoint, wobei in der Regel die Informationen aus den ersten beiden Ebenen ausreichend sind. Der Zugriff auf diese zusätzlichen Daten ist zur Laufzeit des Programms möglich.
Xcode bietet zahlreiche Einstellungen für den Debugging-Modus (Bild 2)
Debugging-Aktionen Normaler Programmablauf (Do Nothing) Anzeige der Konsole (Show Console) ● Start des Debuggers (Show Debugger) ● Start des Mini Debuggers (Show Mini Debugger) ● Start in Konsole und Debugger (Show Console & Debugger) ● ●
4/2010 www.mac-developer.de
Einen Breakpoint können Sie direkt im Texteditor setzen (Bild 3)
31
030_Debugging_jp_ea.qxp
09.08.2010
16:12 Uhr
Seite 32
Titelthema Debugging mit Xcode ●
Debugging-Leiste Liste der Threads ● Breakpoints: Aktivieren und Deaktivieren von Breakpoints ● Fortsetzen (Continue): Ein angehaltenes Programm wird fortgesetzt ● Step Over: Funktion wird als Ganzes ausgeführt ● Step Into: Einstieg in Funktion oder Methode ● Step Out: Ausstieg aus Funktion oder Methode ● Debugger: Aufruf des Debuggers ● Console: Aufruf der Konsole ● Call List: Aufrufliste der Funktionen und Methoden des aktuellen Programms – Weg zur aktuellen Stelle
In der Breakpoint-Übersicht sehen Sie alle festgelegten Haltepunkte (Bild 4) Ausnahmen hervorgerufen werden. Entweder haben Sie diese mit Hilfe von passenden Routinen abgefangen und können auf diese Weise die Abarbeitung einfach kontrollieren, oder Sie nutzen Breakpoints beim Auftreten einer Ausnahme. Diese Fehlerauswertung müssen Sie für Objective-C und C++ lediglich aktivieren. Für Objective-C finden Sie die Einstellung Stop on Objective-C Exceptions unter Run, Activate/Deactivate. Für C++ setzen Sie den Schalter analog unter Run, Manage Breakpoints, Add C++ Exception, All Exceptions.
Breakpoints deaktivieren und löschen
Xcode erlaubt für jeden Breakpoint die Definition einer Folgeverarbeitung (Bild 5)
32
Wird ein Haltepunkt nicht mehr benötigt, haben Sie zwei Möglichkeiten, um diesen loszuwerden. Entweder Sie löschen ihn oder Sie deaktivieren ihn lediglich. Bei der zweiten Option können Sie in einer späteren Prüfung wieder auf den Breakpoint zurückgreifen und müssen ihn nicht wieder neu setzen. Zum Löschen eines Breakpoints gibt es mehrere Vorgehensweisen. Bei einem Breakpoint, der die Ausführung des Programms an einer speziellen Zeile stoppt, ziehen Sie im Texteditor einfach die Markierung nach links aus der Spalte. Alternativ dazu können Sie diesen auch über
Run, Manage Breakpoints, Remove Breakpoint at Current Line löschen. Eine weitere Möglichkeit ist die Übersicht der Breakpoints: Markieren Sie an dieser Stelle die Zeile und drücken Sie die [Entf]-Taste. Wie bereits erwähnt ist die zweite Alternative das An- und Ausschalten von Breakpoints, was vor allem bei Schleifen oder wiederholten Ausführungen bestimmter Segmente sehr hilfreich ist. Haben Sie mehrfach an einer Stelle gestoppt, die Sie sicher nicht weiter untersuchen möchten, deaktivieren Sie den Breakpoint einfach für diesen Durchgang. Rufen Sie dazu die Übersicht über alle Breakpoints auf und entfernen Sie den Haken des Breakpoints rechts nach der Beschreibung. Damit ist der Haltepunkt zwar noch vorhanden, es wird jedoch nicht mehr an dieser Stelle gestoppt.
Folgeaktionen auf Breakpoints Bis jetzt haben Sie an einem Haltepunkt lediglich zur weiteren Untersuchung des Sachverhalts angehalten. Xcode erlaubt darüber hinaus jedoch auch weitere automatisierte Aktionen, sollte eine solche Stelle erreicht werden. Dafür stehen Ihnen unterschiedliche Aktionen zur Verfügung, wie etwa die Ausführung des Debuggers oder eines Shell-Kommandos, das Abspielen einer Sounddatei als Hinweis oder das Protokollieren von zusätzlichen Informationen in der Konsole. Die Folgeaktionen setzen Sie über die Breakpoint-Übersicht (Bild 5). Öffnen Sie den Breakpoint und fügen Sie eine zusätzliche Aktion hinzu, indem Sie mit dem Pfeil die nächste Ebene unterhalb des Breakpoints aufklappen. Ihnen steht dann ein Feld mit der Beschriftung Click add Button to incorporate breakpoint action
mac-developer 4/2010
030_Debugging_jp_ea.qxp
09.08.2010
16:12 Uhr
Seite 33
Titelthema Debugging mit Xcode ●
zur Verfügung. Mit dem Pluszeichen am linken Rand fügen Sie die Aktion hinzu. Eine ausführliche Übersicht über die verschiedenen Alternativen finden Sie online in der Xcode-Dokumentation (http://developer.apple.com) unter dem Abschnitt Managing Program Execution.
Stelle, und Sie können die Programmzeilen schrittweise ausführen. Sind Sie – nachdem Sie die kritische Stelle passiert haben – sicher, dass innerhalb der Funktion oder Methode nichts mehr geschieht, können Sie mit Step Out wieder zurück zur aufrufenden Stelle springen.
Anzeige von Variablenwerten
Debugging-Ansicht
Nachdem Sie an einem Breakpoint angehalten haben, stehen Ihnen verschiedene Optionen zur Verfügung. Als Erstes können Sie alle aktuell gültigen lokalen und globalen Variablen sowie deren Werte betrachten. Der Zugriff auf die aktuellen Werte kann in unterschiedlicher Art und Weise erfolgen. Befinden Sie sich gerade an der passenden Programmzeile im Texteditor, dann bewegen Sie einfach den Mauszeiger über die Variable, und Sie erhalten den aktuellen Wert in einem Tooltipp angezeigt (Bild 6). Wenn Sie sich im Debugger befinden, steht Ihnen ein Fenster mit allen Variablen, Argumenten und Registern des jeweiligen Programms zur Verfügung. Neben dem aktuellen Wert der Variablen enthält diese Ansicht noch zahlreiche weitere technische Details zur Variablen.
Zum Debugging eignen sich am besten die beiden Ansichten Debugger und Texteditor. Im folgenden Abschnitt erfahren Sie mehr über die unterschiedlichen Steuerungen innerhalb der beiden Varianten. Innerhalb des Texteditors können Sie eine Vielzahl von Debugging-Aktivitäten durchführen und haben über eine Symbolleiste, den Tool-
Den aktuellen Wert sehen Sie entweder im Tooltipp oder in der Übersicht der Variablen (Bild 6)
Überwachung Eine Verfeinerung von Breakpoints sind Watchpoints. Dabei handelt es sich um Breakpoints, denen Sie eine Bedingung mitgeben. Nur wenn die Bedingung eintritt, wird die Ausführung des Programms an dieser Stelle gestoppt. Wählen Sie dazu die Variable aus dem Überwachungsfenster aus. Den Watchpoint setzen Sie über Run, Variables Watch, Watch Variable. Sobald sich der Wert der Variablen ändert, stoppt der Debugger an der Programmzeile, und Sie können der Ursache auf den Grund gehen.
Durchmarsch durchs Programm
4/2010 www.mac-developer.de
tipp und die Leiste mit den Zeilennummern auch genug Unterstützung an der Hand. In der Debugging-Leiste stehen Ihnen die wichtigsten Werkzeuge für die Fehlersuche zur Verfügung. Von links nach rechts sind in der Symbolleiste neun Funktionen und Informationen angeordnet. Darüber hinaus enthält die Spalte mit den Zeilennummern einige zusätzliche Funktionen (siehe Kasten „Debugging-Leiste“ auf Seite 32 sowie den unten stehenden Kasten „Funktionen der Zeilennummernspalte“). Die meisten Debugging-Aktionen lassen sich somit innerhalb des Texteditors erledigen (Bild 8). Wenn Sie trotz allem einmal weiterreichende Informationen benötigen, sollten Sie auf den Debugger zurückgreifen. Diese Ansicht zeigt Ih-
▲
Nachdem Sie sich einen Überblick über die Variablen und deren Werte verschafft haben und optionale Watchpoints definiert sind, sollten Sie sich auf den Weg durch das Programm machen, damit Sie dem Fehler auf die Schliche kommen. Dazu stehen Ihnen im Debugger die drei Schaltflächen Step Over, Step Into und Step Out zur Verfügung (Bild 7). Wenn Sie sich sicher sind, dass der Programmablauf innerhalb einer Funktion korrekt ist, können Sie die detaillierte Ansicht getrost überspringen und die Programmzeilen mit Hilfe der Schaltfläche Step Over als Ganzes ausführen. Anders sieht es aus, wenn Sie innerhalb einer Funktion oder Methode ein Problem vermuten – die Schaltfläche Step Into verzweigt direkt an diese
Mit den Step-Tasten debuggen Sie durch den Programmcode (Bild 7)
Funktionen der Zeilennummernspalte Continue to here: Programm wird an der Stelle, an welcher sich der Cursor befindet, fortgesetzt. ● Add Breakpoint: Fügt Breakpoint hinzu. ● Add & Edit Breakpoint: Breakpoint wird hinzugefügt und das Breakpoint-Fenster zur weiteren Bearbeitung geöffnet. ● Built-in Breakpoints: Vordefinierter Break●
point wird hinzugefügt. Enable Breakpoint: Deaktivierter Breakpoint wird aktiviert. ● Disable Breakpoint: Aktivierter Breakpoint wird deaktivert. ● Reveal in Breakpoint: Breakpoint der aktuellen Zeile wird im Breakpoint-Fenster geöffnet. ●
33
030_Debugging_jp_ea.qxp
09.08.2010
16:12 Uhr
Seite 34
Titelthema Debugging mit Xcode ●
nen neben dem Texteditor eine Aufrufliste aller aktiven Threads sowie eine Übersicht aller Variablen an. Außerdem haben Sie die notwendigen Sprungfunktionen, den Aufruf der Konsole sowie die Übersicht der Breakpoints über Schaltflächen im direkten Zugriff. Die Fenster des Debuggers können Sie entweder horizontal oder vertikal anordnen. Die nötigen Funktionen finden Sie unter Run, Debugger Display.
erfüllen. Die Kompilierung muss mit GCC 3.3 oder höher erfolgen, sie muss ohne Optimierung und mit Debugging-Symbolen erfolgen. Haben Sie nichts an den Standardeinstellungen der Kompilierung innerhalb von Xcode verändert, sind die Bedingungen von Haus aus erfüllt. Änderungen während des Debuggings können nur nacheinander erfolgen. Wenn Sie Korrekturen an mehreren Dateien durchführen müssen, muss dies für jede Datei separat erfolgen. Nur auf diese Weise kann Xcode die Änderungen auch umsetzen. Die Anpassung starten Sie über Run, Fix zur Laufzeit des Programms in Xcode. Nachdem Sie die Korrekturen durchgeführt haben, kompiliert Xcode die Änderungen und fügt sie in den Programmablauf ein. Anschließend setzen Sie das Programm an der Stelle fort, an der Sie zuvor pausiert haben. Führen Sie Änderungen an einer Stelle durch, die bereits durchlaufen wurde, so sehen Sie das geänderte Verhalten erst beim nächsten Durchlauf der Programmzeilen. Alternativ können Sie während einer Pause auch die aktuelle Stelle des Cursors verschieben, damit die geänderten Programmzeilen erneut ausgeführt werden. Ziehen Sie dazu den roten Pfeil auf die neue Zeile und starten Sie die Ausführung.
Die Konsole
Schritt für Schritt
Im Zusammenhang mit dem Debugging unter Xcode soll auch die Konsole nicht unerwähnt bleiben. An einigen Stellen in diesem Workshop wurde bereits auf diese Ansicht hingewiesen. Es gibt einige Situationen, beispielsweise wenn Sie die direkte Ausgabe Ihrer Anwendung betrachten wollen, in denen Sie auf die Konsole anstelle der grafischen Oberfläche angewiesen sind. Ebenso werden alle Debugging-Meldungen, die über stderr erzeugt werden, in der Konsole ausgegeben. Die Konsole können Sie entweder wie beschrieben über die jeweiligen Symbolleisten aufrufen oder über Run, Console (Bild 9). In diesem Workshop soll nicht näher auf die Konsole eingegangen werden, da das Thema sehr umfangreich ist. Apple widmet dem Debugging innerhalb der Konsole unter dem Stichwort Debugging with GDB eine komplette Rubrik in der Online-Dokumentation.
Für eine Korrektur zur Laufzeit sind somit folgende Schritte notwendig: Starten Sie eine Debugging-Session für Ihr Programm. Arbeiten Sie mit dem Debugger und prüfen Sie Ihre Anwendungen auf Fehler, bis Sie auf die notwendige Korrektur stoßen. Ändern Sie die Stelle in Xcode und speichern Sie die Änderungen. Rufen Sie Run, Fix zur Übernahme der Korrekturen auf. Setzen Sie das Programm an der gewünschten Stelle mit den Änderungen fort. [jp]
Das Debugging kann direkt aus dem Texteditor heraus erfolgen (Bild 8)
In der Konsole sehen Sie wichtige Systemmeldungen (Bild 9)
Programmänderungen zur Laufzeit Eine sehr hilfreiche Funktion ist die Änderung von Programmzeilen zur Laufzeit. Damit können Sie direkt testen, ob eine vermutete Korrektur das Problem beseitigt – für diese Vorgehensweise ist auch kein Neustart Ihres Debuggers notwendig. Für die Verwendung der Funktion sind jedoch auch einige Voraussetzungen zu
34
Weiterführende Informationen Wenn Sie weiterführende Informationen zum Debugger benötigen, sollten Sie einen Blick in die OnlineDokumentation von Apple werfen. Dort finden Sie die verschiedenen Varianten des Debuggings ausführlich beschrieben und erhalten an der einen oder anderen Stelle noch weitergehende Hinweise. Das Handbuch ist gut gegliedert, und über die Suche kommen Sie auch schnell zu weiterführenden Seiten innerhalb der Entwickler-Dokumentationen: http://developer.apple.com/mac/library/documenta tion/DeveloperTools/Conceptual/XcodeDebugging Wenn Sie viel mit Shortcuts arbeiten, ist die Übersicht von Colin Wheeler zu empfehlen – er hat auf einer DIN-A4-Seite die wichtigsten Tastaturbefehle für die Bedienung von Xcode zusammengestellt: www.1729.us/xcode/Xcode%20Shortcuts.pdf
mac-developer 4/2010
35_PHPWorld_Anz_A4_v3.qxd:Layout 1
09.08.2010
14:40 Uhr
Seite 35
09.-11. November 2010 Holiday Inn Munich City Centre, München
att b a r r e h c Frühbu
€ 100b,–er 2010
eptem bis 21. S
09. November 2010: Kongress
10. November 2010: Intensiv-Workshops
11. November 2010: PHP Master Class: Live Coding
Cloud Computing
OOP
Usability
SEO Performance
Security
Unit Testing
Web Services Entwurfsmuster
Frameworks Zukunft von PHP
www.phpworld-kongress.de Medienpartner:
CMS
Ein Kongress des: Veranstalter:
036_XCode4_jp_ea.qxp
09.08.2010
15:07 Uhr
Seite 36
Titelthema Xcode 4 ●
Erster Blick auf die kommende Version
Neue Werkzeuge und Ansichten Zunächst war es Besuchern der WWDC vorbehalten, sich Apples neuestes Programmier-Tool anzuschauen. Seit Ende Juli können nun alle registrierten Entwickler Xcode 4 am eigenen Mac live erleben. Ingo Böhme Auf einen Blick mac-developer-Autor Ingo Böhme wirft einen Blick auf die neuen Werkzeuge und Ansichten in Version 4 von Apples Entwicklungswerkzeug Xcode.
L
ange haben wir darauf warten müssen. Xcode machte bisher den Anschein eines zusammengewürfelten Pakets. Unter einer Oberfläche werden bei Xcode zahlreiche Tools kombiniert. Muss ein Entwickler bei anderen IDEs lediglich ein Steuerelement platzieren und das passende Ereignis mit Code füllen, um etwa eine Schaltfläche mit Leben zu erfüllen, waren bei Xcode ein halbes Dutzend Aktionen, Mausbewegungen und Code-Änderungen in unterschiedlichen Programmen und Dateien nötig. Damit ist jetzt Schluss! Alle Tools sind bei Xcode 4 harmonisch
kennen und anzusprechen. Zudem waren Interface Builder und Xcode in zahlreiche Fenster unterteilt. Insbesondere für Einsteiger war dieses Wirrwarr undurchschaubar. Das neue Layout von Xcode 4 packt alle Informationen und Funktionen nun in ein einziges Fenster. Aufgeteilt ist die Oberfläche in ein bis vier vertikal getrennte Bereiche, in denen die Projektverwaltung, das Dokumentenfenster für Code oder das XIB-Layout sowie sämtliche InspectorFenster und Info-Tabs liegen (Bild 1). Jedes Element kann ein- oder ausgeblendet werden.
Navigatoren und Utilities
Das neue Xcode 4 stellt sämtliche Funktionen und Informationen übersichtlich geordnet in einem Fenster dar (Bild 1)
in einer Oberfläche kombiniert, und die AppleEntwicklungsumgebung hat endlich den Stand erreicht, den die RAD-Tools unter Windows bereits seit fast einer Dekade aufweisen.
Eine Oberfläche Bislang waren Codebereich und visuelle Gestaltung der Oberfläche aufgesplittet auf die Module Xcode und Interface Builder. Für die Verbindung gab es zahlreiche Direktiven, um die sogenannten Outlets vom Code aus zu er-
Die Oberfläche teilt sich in drei Bereiche: Links und rechts befinden sich die Navigations- und die Utilities-Randspalte, im Zentrum zeigt sich der Content. Das kann – wie bei Xcode 3 – das Codefenster, aber auch das Layout einer XIBDatei sein. Neu ist der Assistent, eine Zweiteilung des Content-Bereichs, wobei der Assistent immer rechts die passenden Informationen zu dem Objekt, Modul oder Media-Element anzeigt, das im linken Teilfenster geöffnet wird. Also beispielsweise die Header-Datei zu einem Modul oder das zugehörige Modul zu einem Interface-Builder-XIB-Layout. Im Navigationsbereich am linken Fensterrand findet der Entwickler Zugang zu seinem Code aus vielfältiger Perspektive: über die Projektdateien, die Suche, den Debugger-Trace, Logs oder das Aufsplitten in einzelne Objektteile. Zusätzlich steht die hierarchische Navigationsleiste über dem Code zur Verfügung (Bild 2), die stets den kompletten Objekthierarchie-Pfad anzeigt, also in welchem Modul oder sogar welchem Objekt man sich gerade mit dem Cursor befindet. Jede einzelne Hierarchie kann dabei separat ausgewählt werden. Der Wechsel zwischen den einzelnen Dateien wird dadurch um ein Vielfaches beschleunigt.
Die Navigationsleiste über dem Programmcode ist ein echter Turbo für den Wechsel der Projektdatei (Bild 2)
36
mac-developer 4/2010
036_XCode4_jp_ea.qxp
09.08.2010
15:07 Uhr
Seite 37
Titelthema Xcode 4 ●
Per Drag and Drop erzeugt Xcode 4 automatisch den Outlet- und den IBAction-Code in der Header- und in der Moduldatei (Bild 3)
Am rechten Fensterrand stellt Xcode die Utility-Spalte dar. Hier finden sich die Objektbibliothek der Steuerelemente, der Inspector, der bekannte Connection- und Size-Manager sowie die Verwaltung der verwendeten Medien. Beim Inspector wird auf die Lokalisierung für verschiedene Länder besonderer Wert gelegt. Auch neue Dateien aus der Template-Bibliothek müssen nicht mehr umständlich übers Menü ausgewählt werden. Sie werden einfach aus dem Utility-Bereich in den Projektnavigator gezogen.
Automatisierter Code Um ein Steuerelement vom Code aus zu bearbeiten, musste man in Xcode 3 Folgendes tun: Outlet deklarieren, IBAction deklarieren und in der Moduldatei anlegen. Gegebenenfalls Getterund Setter-Methoden oder zumindest die Property festlegen. Alles old-fashioned! Xcode 4 erledigt das eigenständig. Alles, was der Programmierer tun muss, ist, das Steuerelement mit der rechten Maustaste in den Interface-Abschnitt der Header-Datei zu ziehen (Bild 3). Sofort stehen IBOutlet-Deklaration und IBAction-Methode in der Header-Datei und im zugehörigen Modul.
Code Library Oft vermisst: eine Bibliothek mit Code-Schnipseln, in der man komplizierte Delegate-Deklarationen, aber auch kleine Lösungscodeteile zusammenfassen kann. Eine solche Codebibliothek ist endlich Bestandteil von Xcode (Bild 4). Einige Beispiele sind bereits vorgelegt. Neue werden einfach per Drag and Drop aus dem Code ins Snippet-Fenster gezogen. Dabei können wie bei nativem Objective-C und der CodeVervollständigung Platzhalter für die Parameter oder variablen Bereiche definiert werden. Zudem lässt sich jeder Codeschnipsel mit einem Textkürzel versehen, das wiederum mit [Ctrl Leertaste] im Code verwendet werden kann.
4/2010 www.mac-developer.de
Projektmanagement Mit dem Workspace-Konzept trägt Apple dem Bedürfnis Rechnung, dass Entwickler zumeist an mehreren zusammenhängenden Projekten arbeiten. Mit einem Workspace kann der Programmierer mehrere Xcode-Projekte und zusätzliche Dateien in einer virtuellen Aktenmappe sammeln. Das spart viel Zeit, insbesondere weil die verschiedenen Einzelprojekte nicht nur komplett geladen werden, sondern sich auch noch ein gemeinsames Build-Verzeichnis teilen. Dennoch bleibt alles virtuell. Die einzelnen Projekte verändert die Arbeit im Workspace nicht.
Unter der Haube Der LLVM-Compiler (Low-Level Virtual Machine) wird zum Standard erhoben und ersetzt die GNU Compiler Collection (GCC). LLVM unterstützt auch C++-Code und soll in der finalen Version zweimal so schnell kompilieren wie der Vorgänger. Zudem soll der erstellte Code durch eine bessere Optimierung um bis zu 60 Prozent schneller laufen. Der Entwickler profitiert auch direkt vom neuen Compiler, der nicht nur Fehler aufspürt, sondern auch gleich die Korrekturen vornehmen kann.
Die Code Library erweitert Xcode um Snippets und Code-Bausteine mit variablen Elementen (Bild 4)
Zeitmaschine inklusive Die neue Versionskontrolle ist besonders bemerkenswert, steht allerdings in der Preview-Version noch nicht zur Verfügung. Sie wird – ähnlich wie die Time Machine am Mac – eine Zeitleiste besitzen und sämtliche Änderungen am Code minutiös nachverfolgbar machen. In der Programmierung definitiv ein Novum. [jp]
37
038_iOS4_neu_ef_ea.qxp
09.08.2010
15:27 Uhr
Seite 38
iOSDev iOS 4 ●
Überblick über die neuen Features von iOS 4
Neue APIs braucht die App iOS 4 stellt APIs und Frameworks für Entwickler bereit, die man sich bei der App-Entwicklung zunutze machen kann. Neu ist Multitasking, aber auch die Core Services wurden erweitert. Kay Glahn Auf einen Blick Mit Einführung des aktuellen mobilen Betriebssystems iOS 4 stellt Apple seinen SoftwareEntwicklern mehr als 1500 neue APIs und zahlreiche Frameworks bereit. mac-developer-Autor Kay Glahn zeigt, wie sich neue Funktionen aus den Bereichen Audio, Video, Navigation und Hintergrund-Jobs in bestehende und neue Apps integrieren lassen und wie Anwender davon profitieren können.
A
m 7. Juni 2010 stellte Steve Jobs auf der WWDC die vierte iPhone-Generation vor. Ein wichtiger Bestandteil des neuen Geräts ist das inzwischen in iOS umgetaufte iPhone OS 4. Laut Apple verfügt das neue Betriebssystem über mehr als 1500 neue APIs, mit denen sich die Funktionalität von iPhone- und iPod-touch-Applikationen verbessern lässt. Registrierte AppleEntwickler können das neue iOS SDK 4 im iPhone Dev Center herunterladen, um mit der Entwicklung zu beginnen. Das neue iOS-4-System läuft allerdings nicht ausschließlich auf dem iPhone 4, sondern ist inzwischen auch als kostenloses Update für das iPhone 3G und das iPhone 3GS verfügbar. Auch ein Update für den iPod touch der zweiten und dritten Generation ist inzwischen erhältlich. Außen vor bleiben zurzeit iPad-Besitzer: Dort läuft momentan nur iOS 3.2. Apple hat aber versprochen, bis zum Herbst ein entsprechendes Update auf iOS 4 auch für das iPad auszuliefern. Die Version 3.2 des iOS, die ausschließlich auf dem iPad verfügbar ist, stellt gewissermaßen einen Zwischenschritt zwischen dem letzten iOS 3.0 des iPhones und dem iOS 4 dar und enthält einige der Neuerungen. Eines der bei vielen Benutzern und Entwicklern lang ersehnten Features von iOS 4 ist die Multitasking-Fähigkeit, die das parallele Ausführen von Applikationen auf dem iPhone erlaubt. Doch dieses Feature wird längst nicht auf allen Geräten unterstützt. Besitzer des iPhone 3G und des iPod touch der zweiten Generation müssen trotz iOS 4 auf Multitasking verzichten.
Kontrolliertes Multitasking Apple ermöglicht durch die Einführung der Multitasking-Services, Applikationen im Hintergrund auszuführen. Gleichzeitig sollen aber auch die Akkulaufzeit und die Ausführung der Vordergrundapplikation nicht beeinträchtigt werden. Hierfür hat man ein MultitaskingFramework eingeführt, das der Applikation bei der Gestaltung seiner Hintergrundaktivitäten einige Einschränkungen auferlegt. Hiermit soll vermieden werden, dass Anwendungen, die im Hintergrund laufen, beliebig Systemressourcen verschwenden dürfen und somit die Performance und den Energiehaushalt des gesamten Systems negativ beeinflussen.
38
Mittels Multitasking hat man ständigen Zugriff auf die im Hintergrund laufenden Programme (Bild 1) Vor der Version 4 von iOS durfte auf dem iPhone immer nur eine Applikation gleichzeitig ausgeführt werden. Dies hatte zur Folge, dass vor dem Start einer neuen Applikation die aktuelle zuerst beendet werden musste. Hieraus resultierte ein Application Lifecycle, bei dem Applikationen immer von Null aus gestartet wurden und wieder aus dem Speicher entfernt wurden, sobald man sie beendete. Ab iOS 4 bleiben Applikationen nun standardmäßig im Speicher, selbst nachdem sie beendet wurden. Für den Benutzer macht sich das zum Beispiel dadurch bemerkbar, dass die laufende Anwendung per Betätigen des Home-Buttons nicht mehr beendet wird, sondern im Background Execution Context weiter ausgeführt wird. Als Folge davon ist für ein erneutes Ausführen der Anwendung nicht immer einen Neustart von null aus erforderlich (Bild 1). Für Entwickler bedeutet das allerdings, dass man nun verschiedene Zustände und Zustandsübergänge der Applikation berücksichtigen und die Applikation entsprechend darauf reagieren lassen muss. Für die meisten Applikationen heißt das, dass sie in den Suspended-Zustand wechseln, sobald sie in den Hintergrund gelan-
mac-developer 4/2010
038_iOS4_neu_ef_ea.qxp
09.08.2010
15:28 Uhr
Seite 39
iOSDev iOS 4 ●
gen. Obwohl die Applikation dann in der Regel zwar keine Aktivitäten im Hintergrund mehr ausführt, hat das den Vorteil, dass sie bei erneutem Start wesentlich schneller wieder reaktiviert werden kann und dadurch die User Experience verbessert wird. Außerdem nehmen dadurch die Hintergrund-Applikationen den Vordergrund-Applikation weniger Ressourcen weg. Als Entwickler sollte man sich aber immer darüber im Klaren sein, dass für eine Applikation, die sich gerade im Suspended-Modus befindet, keine Garantie besteht, dass diese nicht vollständig beendet und aus dem Speicher entfernt wird. Dies kann zum Beispiel der Fall sein, wenn der Arbeitsspeicher knapp wird. Daher sollten Applikationen so programmiert werden, dass sie jederzeit bereit sind, beendet zu werden. Aufgaben wie das Speichern von Daten sollten daher immer vor dem Wechsel in den Suspended-Zustand erledigt werden.
Background-Zustände im Detail
4/2010 www.mac-developer.de
Autor
Kay Glahn ist unabhängiger ITBerater mit den Schwerpunkten Mobile Applications und Services. Er berät internationale Kunden bei der Umsetzung von Projekten im Mobile-Bereich.
Zentrale Thread-Verwaltung mit Grand Central Dispatch Eine weitere Neuerung, die das System selbst betrifft, sind die beiden Core Services Block Objects und Grand Central Dispatch (GCD). Beide sind nicht nur in iOS 4.0 verfügbar, sondern werden auch von Mac OS X 10.6 (Snow Leopard) unterstützt. Bei den Block Objects handelt es sich um ein Sprachkonstrukt auf C-Ebene, das in C- und in Objective-C-Code verwendet werden kann und ab der Version 2.0 von Objective-C verfügbar ist. Dieses Sprachkonstrukt, das in anderen Sprachen auch als Closure oder Lamb-
▲
Obwohl die meisten Applikationen lediglich in den Suspended-Zustand gehen und nicht wirklich Aufgaben im Hintergrund erledigen, gibt es diese Option doch, allerdings nur unter ganz klar definierten Rahmenbedingungen. Grundsätzlich hat eine Applikation drei verschiedene Möglichkeiten, Aufgaben im Hintergrund auszuführen. Sie kann entweder einen begrenzten Zeitraum anfordern, in dem sie eine wichtige bereits angefangene Aufgabe beendet, während sie im Hintergrund läuft. Sie kann aber auch per Deklaration einen von mehreren vorgegebenen Diensten unterstützen, die Ausführungszeit im Hintergrund zugeteilt bekommen. Dies geschieht, indem die Applikation dem Key UIBackgroundModes in der Datei Info.plist einen der drei Werte audio, location oder voip zuweist. Hieraus ergeben sich verschiedene Anwendungsbereiche für den Multitasking-Betrieb, die von Applikationen umgesetzt werden können, indem sie die Hintergrunddienste nutzen. So kann eine Anwendung mit Hilfe des Dienstes „Background Audio“ Audio-Inhalte auch dann wiedergeben, wenn sie sich im Hintergrund befindet. Hierdurch kann der Nutzer beispielsweise im Web surfen, während er Musik hört. Durch den Dienst „Background Location“ können Navigationsanwendungen oder SocialNetworking-Applikationen Ortsinformationen erhalten und weiterverarbeiten, während sie sich im Hintergrund befinden. Mit Hilfe des Dienstes „Voice over IP“ wird es Applikationen gestattet, VoIP-Anrufe entgegenzunehmen, während Sie andere Applikationen nutzen oder während sich das Gerät im Locked-Zustand in der Hosentasche befindet. Diese Dienste werden zentral vom System bereitgestellt, so dass sie
von mehreren Applikationen genutzt werden können und nicht jede Applikation durch eigene Hintergrunddienste das System belastet. Eine weitere Variante, eine Applikation im Hintergrund laufen zu lassen, ist der Einsatz von Notifications. Während Push Notifications bereits in iOS 3.0 eingeführt wurden, kommen nun die sogenannten Local Notifications hinzu, mit deren Hilfe sich Alerts lokal generieren lassen. Dies kann entweder durch eine Applikation erfolgen, die sich gerade im Hintergrund befindet, oder zu einem vorgegebenen Zeitpunkt, auch wenn die Applikation nicht mehr ausgeführt wird. Im Gegensatz zu Push Notifications ist für Local Notifications kein Server erforderlich, da sie von einer Applikation auf dem Gerät ausgelöst beziehungsweise geplant werden. Für den Benutzer präsentieren sich beide Arten der Notifications identisch: Sie zeigen entweder einen Alert an oder können das Icon einer Applikation mit Hilfe eines Badges markieren. Das Ganze kann zusätzlich durch ein akustisches Signal unterstützt werden. Der Benutzer kann dann die jeweilige Applikation starten, um weitere Informationen zu erhalten, oder die Notification ignorieren. Da selbst bei iOS 4 nicht auf allen Geräten Multitasking unterstützt wird, sollte man als Entwickler immer prüfen, ob das System auch Multitasking-fähig ist. Dies geschieht durch die in Listing 1 gezeigten Codezeilen. Eine Applikation kann allerdings auch durch ein Opt-out explizit auf die Unterstützung von Multitasking verzichten, selbst wenn das System dazu in der Lage wäre. In diesem Fall wird die Applikation beim Beenden vollständig terminiert und aus dem Arbeitsspeicher entfernt.
Listing 1: Überprüfung, ob das iOS-Device Multitasking-fähig ist
39
038_iOS4_neu_ef_ea.qxp
09.08.2010
15:28 Uhr
Seite 40
iOSDev iOS 4 ●
da bezeichnet wird, stellt im Prinzip eine anonyme Funktion und die dazugehörigen Daten dar, die als Argumente übergeben und gespeichert werden oder von mehreren Threads gleichzeitig verwendet werden können. Block Objects werden bevorzugt als Callbacks eingesetzt oder dienen dazu, auf einfache Weise Code mit den dazugehörigen Daten zu kombinieren. In iOS finden Block Objects vor allem in folgenden Szenarien Verwendung: ● als Ersatz für Delegates und Delegate-Methoden, ● als Ersatz für Callback-Funktionen, ● um Completion Handler für One-Time-Operationen zu implementieren, ● um auf einfache Weise einen Task auf alle Elemente einer Collection auszuführen, ● um einen asynchronen Task im Zusammenhang mit Dispatch Queues auszuführen. Bei Grand Central Dispatch handelt es sich um eine Technologie auf BSD-Ebene, die das Ausführen von Tasks innerhalb einer Applikation verwaltet. Hierdurch soll eine einfachere und effizientere Alternative zu Threads bereitgestellt werden, die auf einem asynchronen Programmiermodell basiert. Es wurde eigentlich für Mehrprozessorsysteme entwickelt, aber bietet auch bei iOS eine einfach zu handhabende Alternative für viele Low-Level-Tasks wie das Lesen und Schreiben von File-Descriptoren, das Implementieren von Timern oder das Verarbeiten von Events. Während man bei bisherigen Versionen von iOS auf Operation Objects und Threads angewiesen war, so kann man ab der Version 4.0 GCD verwenden, um mehrere Tasks in einer Anwendung pseudo-parallel auszuführen. Falls Sie mehr zu GCD wissen wollen, sollten Sie einen Blick in mac-developer 1/2010 ab Seite 62 werfen. Dort haben wir das Thema ausführlich besprochen.
Mit dem Event Kit Framework lassen sich Kalendereinträge mittels API direkt manipulieren (Bild 2)
40
das Event Kit UI Framework wird auch ein View Controller bereitgestellt, der das Standard System Interface zum Anzeigen und Bearbeiten von Kalendereinträgen innerhalb der eigenen Applikation anzeigt. Ein weiterer View Controller ist auch für das Verfassen von SMS-Nachrichten innerhalb der eigenen Applikation verfügbar. Während in iOS 3.0 bereits ein View Controller für das Schreiben von E-Mails vorhanden war, steht jetzt durch die Klasse MFMessageComposeViewController auch das Standard System Interface zum Verfassen und Versenden von SMS-Nachrichten zur Verfügung. Bisher war dies nur durch den Aufruf der SMS-Applikation mit Hilfe eines speziell formatierten URL möglich, wozu allerdings die aufrufende Applikation verlassen werden musste. Durch das Core Telephony Framework erhalten Applikationen Zugriff auf telefonbasierte Informationen, falls das Gerät über eine Telefoneinheit verfügt. Die Applikation kann so zum Beispiel Informationen über den Netzwerkanbieter abfragen oder sich über eingehende Anrufe per Notification benachrichtigen lassen. Anwendungen haben auch direkten Zugriff auf die Fotos und Videos des Benutzers mit Hilfe der Media Library APIs. Über das Assets Library Framework wird eine Query-basierte Schnittstelle bereitgestellt, über die die Fotos und Videos des jeweiligen Benutzers abgefragt werden können. Auf diese Weise erhält die iOS4-Applikation Zugriff auf dieselben Daten wie die iPhone-Fotoapplikation. Hierzu gehören auch alle Bilder in dem Album Saved Photos des Benutzers sowie Fotos und Videos, die auf das Gerät importiert wurden. Neben dem Zugriff erlaubt das Framework auch das Speichern von neuen Fotos und Videos in vorhandene Alben.
Ein Sack voller neuer APIs
Erweiterte Frameworks für Audio, Video und Karten
Neben den Neuheiten auf Systemebene stellt iOS 4 eine ganze Reihe an weiteren APIs bereit (Tabelle 1). Die meisten dieser APIs betreffen die Interaktion mit Systemapplikationen oder erlauben den Zugriff auf Systemfunktionen, die bisher für „externe“ Anwendungsentwickler nicht zugänglich waren. So erlaubt das Event Kit Framework Applikationen den direkten und vollständigen Zugriff auf Kalendereinträge in der Kalender-Applikation des Geräts. Hiermit können in allen Kalendern des Geräts neue Kalendereinträge angelegt oder bestehende manipuliert werden (Bild 2). Über
Auch die Aufnahme- und Wiedergabe-Fähigkeiten von Videos wurden mit Hilfe des AV Foundation Frameworks wesentlich erweitert. So stellt das neue Framework zahlreiche Funktionen bereit, mit denen beispielsweise Videos editiert werden können und sich Video- und Audiospuren synchronisieren lassen. Es gibt jetzt aber auch eine Objective-C-Schnittstelle, mit deren Hilfe man Details über Sounddateien wie das Datenformat, die Sample Rate und die Anzahl der Kanäle ermitteln kann. Das AV Foundation Framework basiert auf dem Core Media Framework sowie dem Core Video Framework, auf die man als Entwickler ebenfalls Zugriff hat, die aber für die meisten Anwendungsszenarien nie direkt benutzt werden müssen, da das AV Foundation Framework
mac-developer 4/2010
038_iOS4_neu_ef_ea.qxp
09.08.2010
15:28 Uhr
Seite 41
iOSDev iOS 4 ●
bereits alle wichtigen Funktionen bereitstellt. Während Core Media die Low-Level-Mediatypen für die AV Foundation bereitstellt, liefert Core Video entsprechende Buffer und Buffer Pools für Core Media. Auch an dem Map Kit zur Darstellung von Kartendaten wurden einige Verbesserungen vorgenommen. Diese bestehen im Wesentlichen aus verschiebbaren Kartenanmerkungen (Draggable Map Annotations) sowie der Unterstützung von Map Overlays. Mit Hilfe der Overlays können nun Regionen auf einer Karte identifiziert und somit Annotations erstellt werden, die sich nicht nur auf einen einzelnen Punkt, sondern auf eine komplette Region beziehen. Dadurch lassen sich beispielsweise Busrouten, Wahlergebnisse, Parkgrenzen oder Wetterkarten mit Hilfe eines separaten Layers über die Kartendaten legen. Draggable Map Annotations erlauben ein einfaches Verschieben der Anmerkungen, nachdem diese zur Karte hinzugefügt wurden. Mit Hilfe der Annotations können beispielsweise Routen auf der Karte mit individuellen Anmerkungen versehen werden.
Tabelle 1: Neue Frameworks in iOS 4 Name
Beschreibung
Accelerate
Stellt mathematische Funktionen für DSP, Fast-Fourier-Transformationen, Standard-Vektor- und Matrix-Operationen sowie Funktionen zur Matritzenfaktorierung und zum Lösen linearer Gleichungssysteme bereit.
AssetsLibrary
Query-basierte Schnittstelle, über die die Fotos und Videos des jeweiligen Benutzers abgefragt werden können.
CoreMedia
Stellt die Low-Level-Media-Typen für die AV Foundation bereit und wird im Normalfall vom Entwickler nicht direkt aufgerufen.
CoreMotion
Einheitliche Schnittstelle zu allen Bewegungsdaten des Geräts wie Rohdaten und vorverarbeiteten Daten von Beschleunigungsund Gyroskopsensoren.
CoreTelephony
Ermöglicht den Zugriff auf Phone-basierte Informationen und Events, falls das Gerät über ein Mobiltelefon verfügt.
CoreVideo
Stellt entsprechende Buffer und Buffer-Pools für das Core Media Framework bereit und wird im Normalfall vom Entwickler nicht direkt aufgerufen.
EventKit
Erlaubt Applikationen den direkten Zugriff auf Kalendereinträge in der Kalender-Applikation des Gerätes.
EventKitUI
View Controller, der das Standard System Interface zum Anzeigen und Bearbeiten von Kalendereinträgen innerhalb der eigenen Applikation bereitstellt.
iAd
Stellt eine View zum Integrieren von iAd-Werbebannern in eigene Applikationen bereit.
ImageIO
Ermöglicht das Lesen und Schreiben von Bilddaten in zahlreichen Formaten von Standard-Webformaten bis zu Kamera-RAWDateien.
QuickLook
Ermöglicht einer Applikation, eine Vorschau von ihr unbekannten Dateiformaten wie iWorks, MS Office oder PDF anzuzeigen
Vorschau per Quick Look Neu bei iOS 4 sind auch die Quick LookAPIs, die über das Quick Look Framework bereitgestellt werden. Hierdurch wird eine Applikation in die Lage versetzt, eine Vorschau von ihr unbekannten Dateiformaten anzuzeigen (Bild 3). Die Funktion eignet sich besonders für Dateien, die aus dem Netz geladen werden, oder für nicht unterstützte E-Mail-Anhänge. Nachdem die Datei auf das iPhone oder den iPod touch geladen wurde, kann mit Hilfe des entsprechenden View Controllers der Inhalt der Datei direkt innerhalb der eigenen Applikation dargestellt werden. Es ist kein eigener Code erforderlich, um die Datei anzeigen zu lassen. Unterstützt werden folgende Dateiformate: ● iWork-Dokumente, ● Microsoft-Office-Dokumente (ab Office 97), ● Rich-Text-Format-Dokumente (RTF), ● PDF-Dateien, ● Bilddateien, ● Textdateien, ● Comma-Separated-Value-Dateien (CSV).
4/2010 www.mac-developer.de
▲
Für mathematische Operationen wird das Accelerate Framework als Core Service zur Verfügung gestellt. Es enthält Hunderte von mathematischen Funktionen, die auf die Hardware des iPhone und iPod touch optimiert sind. Hierzu zählen Signal-Processing-Routinen (DSP), Fast-Fourier-Transformationen, Standard-Vektor- und Matrix-Operationen sowie Funktionen zur Matritzenfaktorierung und zum Lösen linearer Gleichungssysteme.
Anstatt die mathematischen Funktionen jeweils selbst zu implementieren, kann auf ein einheitliches API zurückgegriffen werden, dessen Implementierung auf die jeweilige Hardware abgestimmt ist. Für den einfachen Gebrauch von Bewegungsund Beschleunigungssensoren steht unter iOS 4 das Core Motion Framework zur Verfügung. Es ermöglicht das Abrufen von Rohdaten und bereits vorhandenen Daten von Beschleunigungsund Gyroskopsensoren. Durch das Framework wird eine einheitliche Schnittstelle zu allen Bewegungsdaten des Geräts bereitgestellt, die auf der Verwendung von Block-Objekten basiert. Das Core Location Framework ist zwar nicht neu in iOS 4, es wurde allerdings um einige wichtige Funktionen erweitert. So wird jetzt ein Location Monitoring Service integriert, der signifikante Änderungen der aktuellen Position auch anhand von Informationen aus dem Mobilfunknetz ermitteln kann. Im Gegensatz zur bisherigen Überwachung auf Basis von GPS-Daten kann so erheblich Energie gespart werden, da der GPS-Empfänger nicht kontinuierlich eingeschaltet sein muss (Bild 4). Außerdem ist das Framework nun in der Lage, beliebige geografische Regionen zu definieren und Grenzüber-
41
038_iOS4_neu_ef_ea.qxp
09.08.2010
15:28 Uhr
Seite 42
iOSDev iOS 4 ●
Unter Mac OS X schon seit Jahren bekannt, jetzt auch unter iOS 4: Quick Look (Bild 3)
Location Monitoring Services erlauben permanente GPS-Updates im Hintergrund (Bild 4)
schreitungen in diese Regionen hinein oder aus diesen Regionen heraus festzustellen und Applikationen mit Hilfe eines Events darüber zu informieren. Hierdurch kann eine Applikation auf Bewegungen oder Annäherungen an eine geografische Region reagieren, selbst wenn sie zu diesem Zeitpunkt nicht läuft oder nur im Hintergrund aktiv ist. Vertrauliche Daten lassen sich in Zukunft über einen integrierten Verschlüsselungsmechanismus schützen. Wenn eine Applikation eine Datei als geschützt ausweist, wird diese auf Geräten, die diese Funktion unterstützen, vom System automatisch in einem verschlüsselten Format gespeichert. Während das Gerät gelockt ist, ist der Inhalt der Datei weder für die Applikation noch für einen potenziellen Angreifer zugänglich. Erst wenn das Gerät wieder entsperrt ist, kann die Applikation auf die Datei zugreifen. Bei der Verwendung dieses Data-Protection-Mechanismus ist es erforderlich, dass dies bei der Entwicklung der Applikation berücksichtigt wird, damit sie nicht versucht, auf die gesperrten Daten zuzugreifen, solange das Gerät gesperrt ist.
iOS mit integrierter Werbung In Verbindung mit iOS 4 hat Apple die mobile Werbeplattform iAd angekündigt. Bisher gab es bei mobilen Werbebannern innerhalb von Anwendungen immer das Problem, dass der Be-
So sieht Bannerwerbung bis dato auf dem iPhone aus. Mit iAd soll alles anders werden (Bild 5)
nutzer beim Anklicken des Banners meist den Anwendungskontext verlassen musste und die Seite des Werbenden in einem Webbrowser angezeigt wurde. Es war dann für den Benutzer oft schwierig, wieder an die gleiche Stelle der Applikation zurückzukehren. Apple löst dieses Problem bei iAd, indem Fullscreen-Videos und interaktive Inhalte angezeigt werden, ohne dass die ursprüngliche Applikation verlassen werden muss (Bild 5). Entwickler von Applikationen für iOS 4 können sich das zunutze machen und iAd-Werbebanner in ihre Applikationen integrieren. Die Werbung wird dabei transparent für den Entwickler dynamisch über die Mobilfunkverbindung im Hintergrund auf das Gerät übertragen. Apple verkauft und liefert die Werbung selbst aus und beteiligt den Entwickler mit 60 Prozent an den Einnahmen aus dem Werbegeschäft. Alles, was man als Entwickler hierfür tun muss, ist das Einbinden der iAds in die eigenen Applikationen. Das iAD Framework stellt hierfür die notwendigen APIs zur Verfügung. Man muss lediglich in die Standard-Views des User Interfaces der Applikation entsprechende iAd-Views integrieren, die sich um alles Weitere kümmern. Einzige Voraussetzung ist, dass der Screen des Unser Interfaces von einem View Controller verwaltet wird, also einer von UIViewController abgeleiteten Klasse. Das Einbetten des iAd-Werbebanners erfolgt dann durch den in Listing 2 dargestellten Code. [mjh]
Listing 2: Einbetten von iAd-Werbung in die eigene App ADBannerView *adView = [[ADBannerView alloc] initWithFrame:CGRectZero]; adView.currentContentSizeIdentifier = ADBannerContentSizeIdentifier320x50; [self.view addSubview:adView];
42
mac-developer 4/2010
038_iOS4_neu_ef_ea.qxp
09.08.2010
15:28 Uhr
Seite 43
iOSDev iOS 4 ●
Eine Bestandsaufnahme: iOS vs. Mac OS – wohin werden sich Apples Betriebssysteme entwickeln? Mit dem Erscheinen des iPhone OS 3.2 und dessen Nachfolger iOS 4 verschwimmen die Grenzen zwischen Apples Computer- und Handy-Betriebssystemen zusehends. Im iPhone Software Development Kit (SDK) stehen mehr und mehr Funktionen bereit, die man zuvor nur von klassischen Betriebssystemen kannte und die nur auf Desktop- oder Laptop-Rechnern liefen. Umgekehrt sind Funktionen aus dem iPhone-Betriebssystem mittlerweile im MacOS X zu finden – zum Beispiel das Core Location Framework. Dieses Vermischen von zuvor streng getrennten Systemen ruft einige Fragen in der Apple-Entwicklergemeinde hervor: Wird es noch ein Mac OS 10.7 geben? Wird iOS auch auf iMac, Mac Pro und MacBook Einzug halten? Diese Diskussionen wurden durch den starken Fokus auf das iOS auf der letzten WWDC Anfang Juni 2010 in San Francisco noch weiter geschürt. Höchste Zeit also, eine Bestandsaufnahme vorzunehmen. Apple-Hardware: klassisch und mobil Das Apple-Hardware-Produktportfolio lässt eine starke Zweiteilung erkennen. Einerseits gibt es die klassischen Computer wie iMac, Mac Pro und deren mobile Derivate MacBook und MacBook Pro sowie das MacBook Air. Auf der anderen Seite haben wir es mit mobilen Geräten wie iPhone, iPad und iPhone touch zu tun. Diese Unterscheidung anhand der Mobilität gilt es aber zu überdenken und wohl bald über Bord zu werfen: Apple scheint hier eine andere Einteilung zu treffen und Strategie zu verfolgen. Auf der einen Seite haben wir es mit Devices zu tun, deren primäres Ziel es ist, Inhalte zu konsumieren. Ich möchte sie „Content Consumer Devices“ nennen. In diese Kategorie fallen das iPhone, das iPad, der iPod (touch) und auch Apple TV. Diese Geräte erlauben es dem Benutzer, digitale Inhalte – also Musik, Filme, Spiele, Internetseiten et cetera – zu konsumieren. Hierbei gibt es die Option von hinreichenden Eingabemöglichkeiten durch Onscreen-Keyboards. Der Benutzer kann also E-Mails schreiben und Webadressen oder Suchbegriffe eingeben. Für das Schreiben längerer Textdokumente sind diese User-Interfaces hingegen ungeeignet. Auch das Apple TV fällt in diese Kategorie und es ist zu erwarten, dass die nächste Version von dessen Betriebssystems ebenfalls iOS sein wird. Als Eingabegerät wird hier vermutlich nicht mehr die Apple Remote zum Einsatz kommen, sondern das noch recht neue Magic Trackpad. Diese Möglichkeit der fingergesteuerten Eingabe hat Apple bereits in der aktuellen Version der iPhone-App „Remote“ gezeigt: Hier ist es möglich, mittels einer Oberfläche das Apple TV zu steuern. Der Sprung zum Magic Trackpad ist somit nicht weit.
Der Nutzungskontext entscheidet: Content Consumer Devices versus Content Producer Devices Die andere Gerätekategorie sind die „klassischen Computer“ - Desktops und Laptops. Für diese Kategorie bietet sich der Sammelbegriff „Content Producer Devices“. Damit werden Filme produziert, Musik geschnitten und Software entwickelt. Diese Arbeiten auf einem iPad zu erledigen ist sicher nicht sehr effektiv. Hier sind etablierte Eingabegeräte wie die Tastatur immer noch die erste Wahl. Die bisherige Einteilung in mobile und stationäre Geräte ist also hinfällig. In Zukunft muss also der Anwendungszweck von Geräten in den Fokus gerückt werden und auf dieser Basis eine neue Einteilung vorgenommen werden. Es ist daher davon auszugehen, dass es einen Nachfolger (Mac OS X 10.7) für Snow Leopard (Mac OS X 10.6) für die Content Producer Devices geben wird. Es wird natürlich immer eine Überschneidung von Funktionen und Anwendungsbereichen zwischen den beiden Geräteklassen geben, da der Benutzer sowohl auf dem Content Consumer Device als auch auf dem Content Producer Device arbeiten wird. Seamless Integration ist notwendig Dies wird er je nach Nutzungskontext tun – ist er im Büro, wird er unter Mac OS X diese Aufgaben erledigen – daheim vermutlich auf seinem iPad oder Apple TV. Hierbei ist eine nahtlose Integration (Seamless Integration) dieser Geräte und deren Benutzer notwendig: Alle Inhalte – seien sie selbst produziert oder erworben – müssen zu jeder Zeit auf allen Geräten sofort verfügbar sein. An dieser Baustelle muss Apple allerdings aktuell noch hart arbeiten: Der derzeitige Synchronisationsmechanismus – insbesondere die notwendige Kabelverbindung zwischen einem Content Consumer Device und einem Content Producer Device mittels USB Kabel – kann nur als anachronistisch bezeichnet werden. Dass dieser Synchronisationsprozess prinzipiell auch „over the air“ geht, zeigt Apple selbst bereits seit geraumer Zeit mit der Apple-TV-Software.
„Der derzeitige Synchronisationsmechanismus über USB-Kabel ist anachronistisch“
4/2010 www.mac-developer.de
Marc Schlüpmann ist Geschäftsführer der SMP IT-Media GmbH, die sich auf die Entwicklung von iPhone- und iPad-Anwendungen und deren Backend-Systeme fokussiert. Er ist seit vielen Jahren in der Software-Entwicklung und IT-Beratung tätig, entwickelt seit 2008 Software für das iPhone und seit 1997 in der Programmiersprache Java vornehmlich webbasierte Systeme.
In Zukunft ein einziges Apple OS? Es ist deshalb damit zu rechnen, dass Apple in naher Zukunft ein neues Apple TV mit iOS vorstellen wird. Den klassischen Computer-Nutzern wird sicher vermutlich auch im nächsten Jahr ein neues Mac OS X in der Version 10.7 präsentiert. Ob es eine Vereinigung der zwei Betriebssystemvarianten – iOS und Mac OS X – zu einem einzigen Apple OS geben wird, ist derzeit nicht vorhersagbar, da gute Gründe dafür, aber auch dagegen sprechen. [Marc Schlüpmann/ef]
43
044_Einsteiger2_jp_ea.qxp
09.08.2010
15:33 Uhr
Seite 44
iOSDev iPhone-Einstieg für Software-Entwickler, Teil 2 ●
Umstieg aufs iPhone für Delphi-, Visual Basic-, Java-, PHP- und C++-Entwickler
Sprachhilfe für Umsteiger Sie sieht völlig anders aus als alles, was man sonst unter Windows kennt: Die Syntax von Objektive-C wirkt verwirrend. Doch das Prinzip ist dasselbe wie bei Delphi, C, Java, PHP oder selbst Visual Basic. Ingo Böhme Auf einen Blick Dieser Beitrag ergänzt den Artikel „Willkommen an Bord“ aus Ausgabe 3/2010 von mac-developer. Im zweiten Teil geht es um die sprachlichen Konzepte von Xcode. Anhand des Vergleichs wird gezeigt, dass sich Objective-C trotz völlig anders wirkender Syntax im Grunde nur wenig von anderen Sprachen wie Java, C++, Delphi, PHP oder Visual Basic unterscheidet. Das Ziel dieses Beitrags ist, die Grundkonzepte der Sprache kennenzulernen, ohne alles komplett neu lernen zu müssen.
W
er Objective-C-Code zum ersten Mal sieht, versteht sicher nur Bahnhof, selbst wenn Delphi, PHP und Visual Basic oder sogar C++ so in Fleisch und Blut übergegangen sind, dass man sie getrost als Muttersprache bezeichnen kann. Der Grund dafür ist, dass sämtliche genannten Beispiele aus der prozeduralen Ecke der Programmierung kommen. Natürlich rühmen sich Delphi und C++ genauso wie PHP und die beliebte Einsteigersprache Visual Basic oder VBA damit, sie seien objektorientiert. Diese Objektorientiertheit hat man den Sprachen jedoch übergestülpt. Objective-C hingegen ist den anderen Weg gegangen. Hier haben die Entwickler die Nachrichten- und objektorientierte Sprache Smalltalk genommen und sie um C-Sprachelemente erweitert. So stammen Variablendeklarationen, Datentypen, Kontrollstrukturen und Zuweisungen von der Mutter aller Sprachen. Die Ereignissteuerung und alles, was sich um Objektorientierung und Vererbung dreht, stammen jedoch von Smalltalk und haben rein gar nichts mit dem objektorientierten Ansatz von C++ gemein.
Variablendeklarationen Wie bei allen Hochsprachen müssen Variablen, von der einfachen ganzzahligen Variante bis hin zu Instanzen der einzelnen Klassen, den Objekten, zunächst einmal deklariert werden. Auch in Visual Basic und in PHP ist dies möglich, aber nicht zwingend erforderlich, weil der Interpre-
ter dort die Variablen erst zur Laufzeit anlegt. In C und auch in Objective-C ist die Deklaration hingegen zwingend erforderlich. Zudem muss nicht nur erklärt werden, dass es eine Variable vom Namen XY gibt, sondern auch, von welchem Datentyp sie ist. Auf diese Weise reserviert der Compiler bereits beim Start der App den nötigen Speicherplatz. In Tabelle 1 sehen Sie den Speicherplatz, den einfache Datentypen belegen. Objekte hingegen werden als Zeiger deklariert. Zur Laufzeit erzeugt der Entwickler ein Objekt, indem er aus der Klasse – also der Bauanleitung eines Objekts, vergleichbar mit den Blaupausen eines Autos – eine echte Instanz, also in der Analogie ein reales Auto – erzeugt. Dieser Speicherplatz ist um einiges umfangreicher als jener der deklarierten Objektvariablen, die dann lediglich den Verweis auf das dynamisch erzeugte Objekt enthalten. Somit ist für den Entwickler, der mit Objekten arbeitet, die Kontrolle des zu Verfügung stehenden Speichers und der Freigabe des von den Objekten belegten Speichers ein ständiger Begleiter. Bei der Benennung der Variablen gibt es nur wenige Einschränkungen. So dürfen sie aus Buchstaben, Ziffern und dem Unterstrich _ bestehen, aber nicht mit einer Ziffer beginnen. Insofern sind auch Exemplare wie _1_2, Cu_l8er, flugentenshowcrash oder _______ theoretisch gültige Variablennamen. Besonders leserlich sind sie hingegen nicht. Und aussagekräftig schon gar nicht. Dazu kommt auch noch, dass sowohl C als auch Objective-C bei allen Bezeichnungen
Tabelle 1: Standard-Datentypen in Objective-C Name char unsigned char short unsigned short int unsigned int long unsigned long long long unsigned long long float double
44
Beschreibung Buchstaben oder Sonderzeichen. Wertebereich: -128 bis 127 Buchstaben oder Sonderzeichen. Wertebereich: 0 bis 255 Ganzzahlen: Wertebereich: -32.768 bis 32.767 Ganzzahlen: Wertebereich: 0 bis 65.538 Ganzzahlen: Wertebereich: -2.147.483.648 bis 2.147.483.647 Ganzzahlen: Wertebereich: 0 bis 4.294.967.295 siehe int siehe unsigned int Ganzzahlen: Wertebereich: -9.223.372.036.854.775.808 bis 9.223.372.036.854.775.807 Ganzzahlen: Wertebereich: 0 bis 18.446.744.073.709.551.615 Fließkommazahlen: 1.2E-38 bis 3.4E+38, Genauigkeit 6 Stellen Fließkommazahlen: 2.3E-308 bis 1.7E+308, Genauigkeit 15 Stellen
iOSDev iPhone-Einstieg für Software-Entwickler, Teil 2 ●
Voraussetzungen für die Entwicklung Zunächst einmal benötigen Sie einen Mac mit IntelProzessor und möglichst das Betriebssystem Snow Leopard (OS X 10.6), wobei bislang auch noch Leopard (10.5) unterstützt wird. Die Entwicklungsumgebung, die für viele Programmierer aus der Windows-Ecke verwirrenderweise nur iPhone SDK genannt wird, gibt es bei Apple kostenlos, nachdem man sich als Apple Developer registriert hat (http://developer.apple.com/ programs/register). Wer allerdings die entwickelten
zwischen Groß- und Kleinschreibung unterscheidet. Das ist insbesondere für Programmierer aus anderen Sprachen, wie Basic oder Pascal, völlig fremd. Daher haben sich bei C-Programmierern zwei Arten der Variablennotation durchgesetzt: „camelCasing“ und „underscore_ spacing“. Bei beiden Varianten geht es darum, Variablen möglichst einheitlich und gut beschreibend zu benennen. Dazu werden sämtliche beschreibenden Attribute aneinandergereiht. Beim camelCasing beginnt der Variablenname immer mit einem Kleinbuchstaben und alle Folgeattribute mit einem Großbuchstaben. Hat man beispielsweise eine Variable, in der die Kapazität der Ladung eines Akkus gespeichert werden soll, könnte man sie beispielsweise Akkukapazitaetladung – Umlaute und das ß sind natürlich auch in C ein No-go – nennen. Beim camelCasing würde die Notation aber akkuKapazitaetLadung lauten. Das Prinzip ist beim underscore_spacing dasselbe, nur werden hier ausschließlich Kleinbuchstaben verwendet und die Attribute mit einem Unterstrich getrennt. Eine Variable wird in der Form datentyp variablenname = wert; deklariert und sofort mit einem Wert vorbelegt. Beispiele sind:
Autor Apps nicht nur auf dem Simulator – als Teil des iPhone SDK –, sondern auch auf der tatsächlichen Hardware testen oder gar über den App Store vertreiben will, der benötigt einen iPhone Developer Account, der jährlich mit 99 Dollar zu Buche schlägt. Fertig! Das war’s! Wenn Sie sich einen günstigen Mac mini zulegen, sind Sie mit knapp 500 Euro dabei. Das kosten unter Windows schon die meisten Entwicklungsumgebungen.
blen ganzeZahl in dem Pointer *daIstSie. Wenn Sie noch nie mit einem Pointer gearbeitet haben, werden Sie sich fragen, wozu man diesen Kram denn überhaupt braucht. Nun, auch wenn Sie es nicht bewusst getan haben, so doch sicher in Ihrer Pascal-, PHP- oder Visual-Basic-Programmierung. Nur dass es dort nicht direkt als Pointer erkennbar war. Denn sobald Sie eine Variable – gleich welchen Typs – an eine Unterroutine, also eine Funktion oder Prozedur, übergeben und in dieser Routine der Inhalt der Variablen verändert werden soll – man spricht hier von Call by Reference –,wird nicht wirklich die Variable und deren Wert übergeben, sondern ihre Adresse (Bild 1). Also der Pointer. Nun ist die Sprache C recht nah an der Maschine (einmal ausgenommen der objektorientierte Teil von Objective-C), und deshalb sagen C-Programmierer auch das, was sie meinen, nämlich: „Ich übergebe dir, Funktion XY, die Adresse einer Integer-Variable und möchte, dass du daran Änderungen vornehmen kannst.“ Dieselben Operationen gibt es eben auch in anderen Sprachen. Eine Prozedur in Visual Basic, zum Beispiel
Ingo Böhme arbeitet als freier Journalist im IT-Bereich. Er programmiert seit drei Jahrzehnten und hat sich seither schon mit nahezu allen Programmiersprachen bis hin zu Assembler, ADA und Cobol herumgeschlagen. Seit dem ersten Pilot – später als Palm bekannt – ist er dem Mobile Computing verfallen. Er hat zahlreiche Bücher zu Palm OS und Windows Mobile verfasst. Sein aktuelles Buch (www.iho.me/book) handelt vom schnellen Ein- und Umstieg in die iPhone-Programmierung.
sub XY(A as Integer, ByVal B As Integer) int ganzeZahl = 4711; char einBuchstabe = 'x'; float gleitKommaZahl = 123.45;
▲
Während man in Visual Basic oder PHP nahezu überhaupt nichts damit zu tun hat und man sich in Pascal oder Delphi fast immer darum drücken kann, sind Pointer, also Zeiger oder Verweise auf Speicheradressen, für CProgrammierer das tägliche Brot. Pointer werden wie Variablen deklariert. Dem Namen wird nur ein * vorangestellt. Ein Zeiger auf die Integer-Variable ganzeZahl sieht dann so aus:
übergibt zwei Parameter. Während der erste (A) innerhalb der Prozedur verändert werden kann, wird der zweite (B) nur als Wert übergeben und sämtliche Operationen auf die Variable/den Parameter B haben keine Auswirkungen auf den Teil des Programms, der XY aufruft. Somit ist
Zeiger auf Variablen und Objekte enthalten Hauptspeicheradressen, die mit NSLog sichtbar gemacht werden können (Bild 1)
int *daIstSie = &ganzeZahl; Das Zeichen & steht dabei für Adresse von und speichert in diesem Beispiel die Hauptspeicheradresse der Varia-
4/2010 www.mac-developer.de
45
044_Einsteiger2_jp_ea.qxp
09.08.2010
15:33 Uhr
Seite 46
iOSDev iPhone-Einstieg für Software-Entwickler, Teil 2 ●
die erste Variable in Wirklichkeit ein Referenzaufruf, bei dem die Adresse, also der Pointer, übergeben wird. Von B hingegen wird nur der Wert, also der Inhalt der Variablen übergeben (ByVal = by value = Wert-Übergabe). Genau umgekehrt ist es in Pascal/Delphi. Hier wird der Referenzparameter ausgezeichnet: procedure XY(var A:integer; B:integer); Auch dabei ist wieder der Fall, dass der Parameter A als Referenz, also als Pointer, übergeben wird und somit innerhalb der Prozedur geändert werden kann und bei B nur die Integer-Zahl auf dem Übergabestack landet. Sowohl in Delphi/Pascal als auch in Visual Basic sieht man beim Aufruf der Prozedur keinen Unterschied, ob es sich um einen Referenzoder um einen Wert-Parameter handelt. XY ersteVariable, zweiteVariable ' 3 Visual Basic XY(ersteVariable, zweiteVariable); /* 3 Delphi/Pascal */
Debugger-Ausgabe von Variableninhalten In Ausgabe 3/2010 der mac-developer haben Sie bereits gesehen, wie man im Programm Debugger-Ausgaben von konstanten Zeichenketten realisieren kann. Die Funktion NSLog bietet jedoch eine sehr flexible Möglichkeit, Variableninhalte formatiert auszugeben. NSLog braucht mindestens einen Parameter. Das ist die Formatierungszeichenkette. Im einfachsten Fall ist das eine konstante Zeichenkette. In dieser Zeichenkette können jedoch Platzhalter für Variablen stehen, beispielsweise %i für den Inhalt einer Integer-Ganzzahl-Variablen oder %f für eine Gleitpunktzahl. Sobald Sie einen Platzhalter in der ersten Zeichenkette haben, erwartet NSLog als weiteren Parameter die Variable. Haben Sie zwei Platzhalter, braucht es zwei Variablen und so fort. Erstellen Sie ein neues Xcode-Window-basedProjekt, öffnen die Startdatei main.m und deklarieren nach der Zeile int main( ... zwei Variablen
Im Gegensatz dazu wird in PHP sowohl bei der Deklaration als auch beim Aufruf angegeben, dass hier eine Referenz-Übergabe einer Variablen stattfindet. Die Deklaration der Prozedur lautet function XY(&$A, $B)
int ganzeZahl = 4711; float pi=3.1415926; Mit dem NSLog-Aufruf NSLog (@"Hallo Debugger, ganzeZahl=%i 3 und pi=%f", ganzezahl, pi);
und der dazugehörige Funktionsaufruf in PHP: Mit der Funktion NSLog zeigen Sie Variableninhalte und Zeitstempel im Debugger-Fenster an (Bild 2)
XY(&$ersteVariable, $zweiteVariable); // PHP
direkt im Anschluss erhalten Sie nach dem Start (Build, Build and Run) das Ergebnis im Konsolenfenster (Run, Console) angezeigt (Bild 2). Tipp: Eine ausführliche Liste der VariablenPlatzhalter finden Sie in der Xcode-Hilfe (Tastenkürzel [Befehl Ctrl ?]) unter dem Suchbegriff „Format Specifiers“.
Tücken bei Berechnungen und Datentypen meistern Wie andere Sprachen auch, lässt natürlich auch C arithmetische Ausdrücke zu. Neben den vier Grundrechenarten gibt es noch den ModuloOperator %, der den Rest einer Division als Ergebnis liefert (Tabelle 2). Die Zuweisung erfolgt wie in PHP oder Visual Basic über das Zeichen =. int int int c = c =
iOSDev iPhone-Einstieg für Software-Entwickler, Teil 2 ●
c = b / a; NSLog(@"c = %i ", c);
i = i + 1;
Dass in den ersten beiden Varianten die Ausgabe 50 respektive 2 ist, ist ja noch ganz einsichtig. Pascal-Programmierer werden spätestens bei der dritten Variante die Augen zusammenkneifen. Hier wird einer ganzzahligen Variable das Ergebnis einer Division zugewiesen. In Pascal/Delphi ein Unding. In C hingegen bekommt die Integer-Variable eben genau den ganzzahligen Wert der Division zugewiesen, in diesem Falle also den Wert 0. Nun könnte man meinen, dass es ausreicht, eine float-Variable zu verwenden, um zum gewünschten Ergebnis 0,5 zu gelangen:
Dies kann man sich in C erleichtern, indem man den Operator direkt zum = stellt, also
float f; f = b / a; NSLog(@"f = %f“, f); Aber leider ist das (noch) nicht die Lösung. Zwar wird das ganzzahlige Ergebnis – nämlich die 0 – in Fließkommaform als 0.00000 angezeigt. Doch weiterhin geht der Compiler davon aus, dass die Division zweier ganzer Zahlen wieder eine ganze Zahl sein soll. An dieser Stelle kommt das sogenannte Typecasting ins Spiel. So ist das Ergebnis einer Division genau dann auch wieder eine Fließkommazahl, wenn eines der beteiligten Elemente eine Fließkommazahl ist. Und dem Compiler kann man durch die Notation (DATENTYP) AUSDRUCK mitteilen, dass AUSDRUCK – dies kann wahlweise ein konstanter Wert oder auch eine Variable sein – als Wert vom Typ DATENTYP sein soll. Im vorgenannten Beispiel würde also float f; f = (float) b / a; NSLog(@"f = %f“, 3 f);
Bedeutung Addiert zwei Werte Subtrahiert zwei Werte Multipliziert zwei Werte Dividiert zwei Werte Modulo (Rest einer Division)
4/2010 www.mac-developer.de
das funktioniert auch mit allen anderen Operatoren (Tabelle 3). Aber damit nicht genug, lassen sich Variablen auch noch direkt um den Wert 1 inkrementieren und dekrementieren. So bedeutet i++, dass der Wert der Variable i um eins erhöht, und i--, dass er um eins vermindert wird. So weit ist es ja noch ganz nett. Für Entwickler aus anderen Programmiersprachen setzt das Stirnrunzeln ein, wenn einmal von ++i und ein anderes Mal von i++ (analog natürlich auch von --i und i--) in einem Code zu lesen ist. Auch hier ist es wieder so, dass Programmierer von Natur aus faul sind und mit möglichst wenig Code möglichst viel erreichen wollen. C-Programmierer scheinen hier die Vorreiter zu sein. In jedem Fall bedeutet der vorgestellte Operator ++ oder --, dass das Inkrementieren beziehungsweise das Dekrementieren stattfindet, bevor das Ergebnis an die Variable auf der linken Seite weitergegeben wird. So hat bei i = 10; c = ++i; die Variable c nach der Wertzuweisung den Wert 11. Und die Variable i genauso. Steht allerdings der ++-Operator nach dem Variablennamen, wird die Zuweisung an c zuerst vorgenommen und danach erst der Variablenwert von i erhöht. Somit hat nach i = 10; c = i++; die Variable c den Wert 10 und i wie auch im vorherigen Beispiel den Wert 11.
Verzweigungen und Bedingungen Auch im Zeitalter der objektorientierten und ereignisgesteuerten Programmierung kommt man als Programmierer um Verzweigungen, also Fallunterscheidungen und Schleifen, nicht herum. Im Grunde sind diese Konstrukte aber genauso aufgebaut wie in allen anderen Pro-
▲
dazu führen, dass die Variable b als Fließkommazahl angesehen wird. Und es wird auch tatsächlich zum gewünschten Ergebnis 0.500000 führen. Eine Besonderheit von C, die ansonsten lediglich bei Java und PHP bekannt ist, ist die verkürzte Schreibweise für Operationen. Diese erspart Arbeit bei häufig anfallenden Ausdrücken So ist etwa das Hochzählen einer Laufvariablen das tägliche Brot in einer Schleife:
Bedeutung a+=b ist gleichwertig zu a=a+b a-=b ist gleichwertig zu a=a-b a*=b ist gleichwertig zu a=a*b a/=b ist gleichwertig zu a=a/b a%=b ist gleichwertig zu a=a%b
47
044_Einsteiger2_jp_ea.qxp
09.08.2010
15:33 Uhr
Seite 48
iOSDev iPhone-Einstieg für Software-Entwickler, Teil 2 ●
} In dem Beispiel drängt es sich nahezu auf: Man benötigt natürlich auch eine Möglichkeit, den umgekehrten, den else-Zweig darzustellen. if (BEDINGUNG) ANWEISUNG; else ANWEISUNG; also im Beispiel:
Die Code-Vervollständigung vermindert die Fehleranfälligkeit beim Tippen und spart viel Zeit (Bild 3)
grammiersprachen auch – lediglich die grundsätzliche Notation ist ein wenig unterschiedlich. Wie in PHP, Visual Basic und Delphi auch, lautet die Verzweigung if. Und lediglich bei der Gleichheit, die mit ==, und der Ungleichheit, die mit != geprüft wird, gibt es zu Visual Basic und Delphi einen Unterschied. if (BEDINGUNG) ANWEISUNG; Hierbei gilt, dass die ANWEISUNG nur dann ausgeführt wird, wenn die BEDINGUNG den Wahrheitswert TRUE hat. Die Bedingung kann aus einer Aussage, wie beispielsweise b==0 bestehen oder aber gemäß der Boole’schen Algebra mit && (logisches Und) oder || (logisches Oder) verknüpft oder gegebenenfalls mit ! negiert sein, also beispielsweise (b==0) && (!(a<=10)) Es ist immer empfehlenswert, Klammern um die Ausdrücke zu setzen. Es gibt zwar eine Operatoren-Hierarchie und Sie können sich etwas Schreibarbeit sparen, mit den Klammern sind Sie jedoch auf der sicheren Seite und müssen sich keine Reihenfolge merken und diese bei jedem komplizierten Ausdruck auch noch versuchen anzuwenden. Ansonsten gelten sämtliche Regeln der Boole’schen Algebra, also beispielsweise, dass !(A && B) dasselbe ist wie !A || !B (De Morgan’sche Regeln). Die ANWEISUNG; in der if-Verzweigung kann wahlweise eine einzelne Anweisung sein oder eine Reihe von Anweisungen, die dann aber in geschweifte Klammern, also {}, eingebettet sein müssen. Dies entspricht in Delphi/Pascal dem begin … end. In Visual Basic endet der Anweisungen-Block ja mit endif. if (b !=0) { f = (float) a / b; 3 NSLog(@"Ergebnis der Division ist 3 %f", f);
48
if (b !=0) { f = (float) a / b; 3 NSLog(@"Ergebnis der Division ist 3 %f", f); } else NSLog(@"Division durch Null!");
Schleifen Wie in den meisten Programmiersprachen gibt es auch in C zwei wichtige Formen der Schleifen: Die Zähl- oder for-Schleife dient dazu, eine festgelegte Anzahl von Läufen durch den Schleifenkörper auszuführen. Die Notation ist ähnlich wie in PHP. Und auch Basic-Programmierer werden die Syntax schnell erkennen. Lediglich Pascal-Entwickler verstehen unter einer forSchleife ein Konstrukt, das eine Variable in Einser-Schritten durchläuft. Hier gibt C definitiv mehr Flexibilität: for ( INITIALISIERUNG; BEDINGUNG; 3 ENDANWEISUNG ) ANWEISUNG; Die INITIALISIERUNG ist eine normale Anweisung, die am Anfang, also vor dem ersten Durchlauf des Schleifenkörpers, durchgeführt wird. Sollen beispielsweise alle Zahlen von 1 bis 100 ausgegeben werden, lautet die Initialisierung: i = 1 Die BEDINGUNG ist wieder ein Boole’scher Ausdruck, der wahlweise wahr (TRUE) oder falsch (FALSE) sein kann. Im Beispiel wäre es i<=100 Und die ENDANWEISUNG ist eine Anweisung, die nach jedem Durchlauf des Schleifenkörpers durchgeführt wird. Dies wird zumeist für das Inkrementieren der Laufvariablen verwendet, also in unserem Beispiel i = i+1 oder in guter CManier i++. Die gesamte Schleife würde dann wie folgt aussehen:
mac-developer 4/2010
044_Einsteiger2_jp_ea.qxp
09.08.2010
15:33 Uhr
Seite 49
iOSDev iPhone-Einstieg für Software-Entwickler, Teil 2 ●
for (i=1; i<=100; i++) NSLog(@"$d", i);
rufenden Code zurückliefert. Eine Funktion hatten wir bereits. Die in allen C-Programmen enthaltene Funktion main:
Das ist der normale Sinn der for-Schleife. Durch die flexible Syntax lässt dieses Schleifenkonstrukt jedoch auch sehr gewagte Varianten zu, die überhaupt keine Ähnlichkeit mehr mit den Zählschleifen in anderen Sprachen haben. Aber das werden Sie in der Praxis schon selbst herausbekommen. Wenn eine Initialisierung nicht sinnvoll ist, kommt wahlweise eine kopfgesteuerte oder eine fußgesteuerte Schleife in Betracht. Bei Ersterer wird die Bedingung geprüft, bevor die erste Anweisung des Schleifenkörpers ausgeführt wird. Bei Zweiterer wird der Schleifenkörper mindestens einmal durchlaufen und am Ende geprüft, ob ein weiterer Durchlauf vonnöten ist. Schematisch sehen kopfgesteuerte und fußgesteuerte Schleife wie folgt aus:
int main(int argc, char *argv[]) { ... return retVal; } Die Parameterliste wird nicht anders notiert als die normale Variablendeklaration. Der Unterschied ist nur, dass die Parameter in den Klammern der Funktion nur durch Kommata getrennt sind. Nach dem Zeichen { kommt beliebiger Programmcode, von der Deklaration lokaler Variablen, die lediglich innerhalb der Funktion gelten, bis hin zu den Anweisungen, die iterativ abgearbeitet werden. An irgendeiner Stelle sollte dann das Statement return WERT
while (BEDINGUNG) // Kopfgesteuerte 3 Schleife ANWEISUNG; beziehungsweise do ANWEISUNG; while (BEDINGUNG); // Schleife
Fußgesteuerte 3
kommen. An dieser Stelle endet die Verarbeitung der Funktion. Und der WERT wird an den aufrufenden Code zurückgegeben, sofern er gebraucht wird. Hat man beispielsweise void halloWelt() { NSLog(@"Hallo Welt!"); } so genügt der Aufruf
Das Gute an Xcode ist, dass der Editor Code-Vervollständigung unterstützt (Bild 3). In dem Moment, wo Sie die beiden Buchstaben wh tippen, erscheint sofort die schematische Notation der While-Schleife. Gleiches gilt natürlich für alle anderen Konstrukte auch. Passt der Vorschlag von Xcode, können Sie mit den Pfeiltasten zu den entsprechenden Platzhaltern für die Bedingungen respektive für die Anweisungen navigieren. Auf diese Weise gibt es viel weniger Fehler durch vergessene Klammern oder versehentlich groß geschriebene Bezeichner.
halloWelt(); ohne Zuweisung eines Wertes. Hat man hingegen eine echte Funktion mit einem Rückgabewert, wie beispielsweise float PI() { return 3.1415926; } so kann man den Rückgabewert in Zuweisungen oder auch Berechnungen nutzen:
Prozeduren und Funktionen Prozeduren und Funktionen sind in C im Grunde dasselbe. Was man in Visual Basic und in Delphi als procedure oder Sub kennt, ist in C lediglich eine Funktion, die den Rückgabewert void besitzt. Grundsätzlich ist der Aufbau einer Funktion DATENTYP name(PARAMETER) { return RÜCKGABEWERT; } Der DATENTYP ist jener, den der Wert RÜCKGABEWERT durch den Befehl return an den auf-
4/2010 www.mac-developer.de
float flaeche, radius=20; flaeche = 2 * radius * PI(); Tipp: Die Codevervollständigung funktioniert übrigens nicht nur bei Elementen von ObjectiveC oder den Standard-C-Bibliotheken, sondern auch bei Ihren eigenen Variablen und Funktionen. Tippen Sie einfach nach der Definition von void halloWelt() die Buchstaben ha. Sofort erscheint der Aufruf der Funktion halloWelt in Grau und Sie müssen lediglich die Pfeiltaste rechts drücken, um ihn in den Quellcode zu übernehmen. So vermeiden Sie Tippfehler und es geht schneller! [jp]
Ausblick In der nächsten Ausgabe von mac-developer erfahren Sie mehr über Parameter, Funktionsdeklarationen, Header-Dateien, Konstanten sowie den objektorientierten Ansatz von Objective C, das Prinzip der Nachrichten und wie man sich im Dschungel der Apple-Klassen zurechtfinden kann, ohne sich zu verirren.
49
050_CD_Inhalt_0410_ef_ea.qxp
10.08.2010
10:48 Uhr
Seite 50
Service Heft-CD ●
E-Book, Bonus-Artikel, Entwickler-Tools
Become an Xcoder Zum Themenschwerpunkt dieser Ausgabe erhalten Sie mit der Heft-CD ein zusätzliches Tutorial – rund 80 Seiten für Einsteiger.
E
s gibt viel zu lernen. Aber erst einmal keine Sorge, dieses Buch geht es ruhig an. Doch ist das nicht der ganze zusätzliche Lesestoff: Dazu kommen zwei ausgewählte Buchkapitel von Erica Sadun und Lee S. Barney, beide Bestsellerautoren beim renommierten Verlag AddisonWesley (www.addison-wesley.de). Einmal geht es um den Zugriff auf Netzwerke mit dem
E-Book „Become an Xcoder“
iPhone, des Weiteren haben wir ein Kapitel für Sie ausgewählt, das die Einbettung von Google Maps in iPhone-Applikationen beschreibt. Auch die Software-Auswahl kann sich wieder sehen lassen: Unter anderem stellen wir Ihnen drei UI-Frameworks zur Verfügung, die das iPhone entweder nachbilden oder den Bau einer eigenen Bedienoberfläche ermöglichen. [ef]
Buchauszug: „Google Maps einbetten“
Das Buch führt Sie in die Grundlagen der Programmierung, insbesondere mit Objective-C, unter Verwendung von Xcode ein. Schon bald werden Sie in der Lage sein, ein einfaches Programm ohne eine grafische Benutzerschnittstelle (GUI) zu erstellen. Become an Xcoder – Programmieren mit dem Mac in Objective-C , 80 Seiten, Altenberg, Clarke und Moghin; kostenlos (CC 3.0)
Buchauszug: „Netzwerke“ Mit der umfassenden Neuauflage ihres iPhoneEntwicklerbuchs legt Erica Sadun das Standardwerk zur Programmierung mit dem iPhone SDK vor – erweitert um Einführungen in Xcode, Interface Builder und Objective-C. Das große iPhone Entwicklerbuch – Rezepte für Anwendungsprogrammierung mit dem iPhone SDK, Erica Sadun, Addison-Wesley 2010, 960 Seiten, 59,80 Euro, ISBN: 978-3-8273-2917-2
Ab sofort ist es nicht mehr notwendig, tief in Objective-C einzutauchen, um hochkarätige Anwendungen für das iPhone zu entwickeln. Sie nutzen einfach die bekannten Entwicklungs-Tools. Das ausgewählte Kapitel hat 20 Seiten. Dynamische iPhone-Anwendungen entwickeln – Anwendungsentwicklung mit HTML, CSS und JavaScript, Lee S. Barney, Addison-Wesley 2010, 224 Seiten, 34,80 Euro, ISBN: 978-3-8273-2918-9
Netbeans IDE 6.9 Programmiersprachen sollen die Programmierung nicht nur ermöglichen, sondern auch erleichtern. So vollzog sich die Entwicklung der Programmiersprachen im Spannungsfeld von Übersetzbarkeit und Bequemlichkeit (Lesbarkeit, Knappheit, Sicherheit). Die Entwicklungsumgebung Netbeans wurde für Java entwickelt und unterstützt unter anderem C, C++ und dynamische Sprachen.
Keine CD mehr da? Unsere Zusammenstellung der Software erspart Ihnen mühsame Internetrecherche. Vielleicht hat aber schon ein Leser vor Ihnen genau das auf der CD gefunden, was er unbedingt brauchte und schon lange gesucht hat – und die CD nicht mehr zurück- beziehungsweise weitergegeben. Kein Problem: Anruf, Fax oder Mail an unsere ServiceAdresse genügt, und gern senden wir Ihnen Ihre persönliche Scheibe zu: mac-developer-Leserservice E-Mail: [email protected] Tel. +49 (0)89 741 17-205 Fax +49 (0)89 741 17-101
50
Der Startscreen Ihrer Heft-CD: Klicks auf die Produktfotos führen Sie direkt ins Installationsmenü
mac-developer 4/2010
051_macDeveloper-Pizza_A4_hellblau.qxd:Layout 1
06.08.2010
14:15 Uhr
Seite 1
Wissenshungrig? Lassen Sie sich Ihr Programmier-Wissen doch direkt nach Hause liefern.
Frei Haus und 15% Preisvorteil
MAC DEVELOPER ist das erste deutschsprachige Entwicklermagazin für Mac- und iPhone-Software. Nutzwertige Beiträge, solides Insider-Wissen und viel Know-how. Von professionellen Software-Entwicklern geschrieben. 4 x im Jahr im praktischen Abo frei Haus. Mit einem Preisvorteil von 15 % gegenüber dem Kiosk.
Der erste Homeservice für Programmier-Wissen: www.mac-developer.de/abo
052_iPhone-4_jp_ea.qxp
09.08.2010
16:19 Uhr
Seite 52
iOSDev Das taugt das iPhone 4 ●
iPhone 4: Erfolgsstart mit Bauchschmerzen
Superstar ohne Empfang Obwohl das iPhone 4 einen Rekordstart hingelegt hat, ist nicht alles Gold, was glänzt. Der Artikel fasst die ersten Eindrücke und Testergebnisse der neuen Handy-Generation zusammen. Boris Boden Auf einen Blick Das neue iPhone 4 ist zwar ein Renner im Verkauf, es kämpft aber mit Empfangsproblemen, die Apple über eine zusätzliche Hülle beheben will. Diese Schwierigkeiten trüben das Bild eines ansonsten starken Geräts mit leistungsfähiger Hardware. Vor allem das Display, der Akku und die Kamera zeigen sich gegenüber den Vorgängermodellen stark verbessert.
E
s hätte die vierte große iPhone-Party für Apple werden können: Schon vor dem Verkaufsstart liefen wieder Hunderttausende Vorbestellungen ein, laut Apple hat sich das iPhone 4 in den ersten Tagen so gut verkauft wie keiner der drei Vorgänger. Doch die Freude über den vierten Streich wurde schon nach wenigen Tagen getrübt, denn im alltäglichen Einsatz zeigte der Neuling ungewohnte Schwächen.
Wer hat Schuld? So berichteten Käufer von Empfangsproblemen des Telefons, wenn sie es auf eine bestimmte Weise greifen würden, bei der die Hand unten links am Gehäuses anlag. Vor allem für Linkshänder ist das eine typische Haltung beim Telefonieren. Dann geht der Empfang um mehrere Balken zurück, Gespräche können sogar ganz abbrechen. Was zunächst mit einigen Foreneinträgen begann, entwickelte sich im Juli zur großen Welle, auf die Apple mit stückweisen Eingeständnissen oder Gegenangriffen eher dünnhäutig reagierte. Zunächst hieß es, es gebe keine Empfangsprobleme oder diese würden durch schlechte Netze verursacht. Einige Wochen später erklärte der Hersteller, dass ein Fehler in der Berechnung der Feldstärke für die Anzeige im Display vorliege, der besseren Empfang als in der Realität vorhanden vorgaukele. Dieses Problem sollte durch ein Update behoben werden. Mitte Juli hieß es dann, dass es tatsächlich Probleme gebe, die durch einen kostenlosen „Bumper“ – eine bunte Gummihülle für die Kanten des Gerätes, die im Apple Store sonst 29,90 Euro kostet – gelöst würden.
Die Realität Bei allen Gerüchten und Meldungen im Netz gibt es doch einige sichere Erkenntnisse zu den Problemen: So zeigt auch unser Testgerät im eigentlich sehr guten Netz von T-Mobile einen schwachen Empfang, wenn es zum Telefonieren wie ein klassisches Handy in die Hand genommen wird (Bild 1). Wenn es dagegen im Freisprechmodus auf dem Tisch liegt, veränderte sich die Gesprächsqualität bei Anrufen nicht. Gesprächsabbrüche haben wir allerdings nicht zu verzeichnen, so dass manche Berichte, die das Gerät gleich für untauglich erklären, über-
52
iPhone 4: Empfangsstörungen treten bei falschen Handgriffen – unten links – auf (Bild 1) trieben erscheinen. Dass es definitiv Empfangsprobleme gibt, haben auch Labormessungen der US-Verbraucherschutzorganisation und der Fachzeitschrift Connect belegt. Das SoftwareUpdate auf das Betriebssystem iOS 4.0.1 verändert übrigens nur wie angekündigt die Anzeige der Balken, sonst nichts. Der Bumper verschafft dagegen tatsächlich Abhilfe, schade nur, dass die in poppigen Farben verfügbare Gummihülle optisch wenig ansprechend ist. Noch uneleganter dürfte nur das Aufbringen eines Klebestreifens auf die linke untere Ecke des iPhone sein, der das Problem ebenfalls behebt. Möglicherweise werden zukünftig produzierte Geräte auch ohne Modifikationen durch den User einwandfrei funktionieren, wenn Apple das Problem beheben kann. Deshalb ist es vielleicht nicht die schlechteste Lösung, als Interessent mit dem Kauf des iPhone 4 noch etwas abzuwarten. So mancher wird sogar ausharren müssen, denn noch immer gibt es eine Warteliste für das iPhone 4 – das Theater um den Empfang hat den Verkäufen bisher offenbar kaum geschadet. In
mac-developer 4/2010
052_iPhone-4_jp_ea.qxp
09.08.2010
16:19 Uhr
Seite 53
iOSDev Das taugt das iPhone 4 ●
Deutschland wird es wie die Vorgänger exklusiv über T-Mobile vertrieben und ist nicht ohne Vertrag erhältlich. Wer es dort bestellt, muss derzeit mindestens acht Wochen warten. Die Version mit dem weißen Gehäuse soll sogar erst Ende des Jahres weltweit verfügbar sein, wofür Apple Produktionsprobleme verantwortlich macht. Auch die Preise der vierten Generation sind kein Sonderangebot, denn das neue Modell kostet je nach Tarif mit Vertrag zwischen 40 und 80 Euro mehr als das 3GS. Ohne Bindung oder mit einer Prepaid-Karte ist das iPhone 4 nur über den freien Markt erhältlich, wobei entsperrte Geräte bei Händlern in der kleinen 16-GByte-Version mindestens 1000 Euro kosten. Ein Eigenimport aus Ländern, wie Frankreich oder Großbritannien, wo das Gerät ohne Simlock für weniger als 700 Euro erhältlich ist, erscheint als Alternative möglich, allerdings muss für den Kauf der dort ebenfalls stark nachgefragten Geräte eine Adresse im jeweiligen Land vorliegen, und der Erwerb ist möglicherweise nicht ohne Vorbestellung möglich.
Noch eine Eigenheit: das MicroSIM-Format
ten Smartphones ist. Mit 137 Gramm ist es kein Leichtgewicht, und auch die deutlicher als beim Vorgänger ausgeprägten Kanten werden nicht jedem gefallen. Auf der Vorder- und Rückseite verwendet Apple nun gehärtetes Glas, wobei es bisher nur die schwarze Ausführung gibt. Das Material wirkt auf jeden Fall sehr hochwertig und scheint auch vor Kratzern gefeit zu sein. Apple verspricht auch, dass es weniger anfällig für Fingerabdrücke ist, das konnten wir im Test aber nicht nachvollziehen. An der Unterseite des Telefons befindet sich der Port Connector von Apple, der das iPhone an den Strom oder den Computer anschließt. Damit lässt sich grundsätzlich auch älteres Zubehör nutzen, sofern nicht dessen Aufnahme genau auf das weniger eckige Gehäuse des iPhone 3 abgestimmt ist.
Boris Boden setzt sich bei der Fachzeitschrift „Telecom Handel“ seit über zahn Jahren mit Handys und Mobile Computing auseinander. Die Smartphone- und die iPhone-Welt begleitet er seit dem Beginn.
Neue A4-CPU: Ganz schön schnell Auch wenn Apple offiziell nicht mitteilt, welche Taktung der A4 genannte Prozessor hat, ist die Leistung gegenüber den Vorgängermodellen klar gestiegen und kann an die 1-GHz-Geschwindigkeit anderer neuer Geräte heranreichen. Es gibt kaum Verzögerungen im Betrieb, und auch grafisch ansprechende Spiele laufen schnell ab. Das von Apple angepriesene Multitasking ist aber nur bei Apps möglich, die dafür auch vorgesehen sind, andere Anwendungen bleiben nur stehen und stehen so schneller wieder zur Verfügung. Durch einen Doppelklick auf die Home-Taste des iPhones sieht der Anwender, welche Apps gerade aktiv sind, und kann diese durch einen langen Druck auf das Symbol schließen. In der Realität verliert man aber schnell den Überblick und es ist möglich, dass zum Beispiel die immer noch aktive Navigation plötzlich in der Tasche ungewollt Anweisungen gibt. Auch stößt der eigentlich üppig bemessene Arbeitsspeicher von 512 Megabyte an seine Grenzen, wenn zu viele Apps im Hintergrund arbeiten.
Bessere Bilder, schnellere Kamera Das höhere Tempo des Prozessors kommt auch der Kamera des iPhone 4 zugute, denn deren Auslöseverzögerung ist mit weniger als einer Sekunde für ein Handy extrem gering. Die Bildqualität der Fünf-Megapixel-Kamera ist in dieser Leistungsklasse überdurchschnittlich und
▲
Wer nach dem Kauf einfach seine alte SIM-Karte ins iPhone 4 einsetzen will, erlebt eine Überraschung: Denn wie schon das iPad verwendet auch das Smartphone das MicroSIM-Format, bei dem die Karte rund 30 Prozent kleiner wird. Von T-Mobile gibt es diese Karten bereits, alte, große SIM-Karten lassen sich mit etwas Geschick nach einer Anleitung im Internet zuschneiden. Platz finden sie dann in einem Schacht an der rechten Seite des Telefons. Zwei weitere, weniger schöne iPhone-Spezialitäten hat auch das „Vierer“: Der Akku ist wieder fest eingebaut und kann nicht vom Anwender gewechselt werden. Außerdem gibt es keinen Steckplatz für Speicherkarten, den fast alle anderen Smartphones haben. Intern steht je nach Version mit 16 oder 32 Gigabyte auf den ersten Blick genug Speicher zur Verfügung, doch wer viele Videos und Apps aufspielen will, die das hochauflösende Display ausnutzen, könnte hier an seine Grenzen stoßen. Platz für eine Speicherkarte gäbe es am Gehäuse eigentlich genug, denn das neue iPhone ist kaum kleiner und auch nicht leichter geworden. Lediglich die Höhe hat sich auf 9,3 Millimeter reduziert, Die Videotelefonie Facetime funktioniert womit es eines der schlanks- nicht über Mobilfunk (Bild 2)
4/2010 www.mac-developer.de
Autor
53
052_iPhone-4_jp_ea.qxp
09.08.2010
16:19 Uhr
Seite 54
iOSDev Das taugt das iPhone 4 ●
der neu dazugekommene LED-Blitz leuchtet den Nahbereich von ein bis zwei Metern sehr hell aus. Ohnehin ist die Aufhellung der Kamera recht stark. Gut funktioniert auch der Zoom per Finger auf dem Display. Bei Videos ist das iPhone 4 mit einer HD-Auflösung von 720p und 30 Frames pro Sekunde ebenfalls auf der Höhe der Zeit, ohne aber neue Maßstäbe zu setzen. Erstmals ist über eine kleine VGA-Kamera über dem Display Videotelefonie mit einem iPhone möglich. Der zugehörige Apple-Dienst Facetime funktioniert aber nur zwischen zwei iPhone 4 und über WLAN – und nicht im Mobilfunknetz (Bild 2). Durch die hohen Datenraten erscheint der Gesprächspartner sehr scharf im Display, und auch seine Bewegungen sind weniger verschwommen als bei anderen Geräten.
nicht abstellen. Trotzdem gehört das Display zum Besten, was derzeit verfügbar ist, zumal der kapazitive Touchscreen sehr schnell und präzise auf Fingerdruck reagiert. An Qualität gewonnen hat auch der Akku, der für ein Touchscreen-Smartphone überdurchschnittliche Leistungen bietet. Bei geringer Beanspruchung des Displays sind mehrere Tage Betrieb realistisch, und einige Stunden Spielen oder Videogenuss sind kein Problem. Viel Strom scheint der leistungsfähige GPS-Empfänger zu benötigen, denn mehr als fünf Stunden Navigation mit Ansagen können wir nicht erreichen. Die quälend langen Ladezeiten über USB nerven zudem den Anwender.
Tolles Display, kräftiger Akku
Trotzdem bleibt am Ende ein sehr guter Eindruck: Die Hardware ist deutlich leistungsfähiger, die Kamera kann mit den Konkurrenten mithalten und der bessere Akku erleichtert das Alltagsleben. Das alles würde das Vierer zum bisher besten iPhone machen, wenn nicht das gravierende Empfangsproblem wäre, das sich auch nicht mit Bumpern und iOS-Bugs wegreden lässt. Wer so viel Geld investiert, möchte ohne Qualitätseinbußen telefonieren. [jp]
Den neuen Maßstab soll das Display des Smartphones setzen, das mit 960 x 640 Pixel eine sehr hohe Auflösung bietet – der Vorgänger hatte nur 480 x 320 Pixel. Texte wirken auf der Anzeige wie gedruckt, was das iPhone 4 auch zum brauchbaren E-Book-Reader macht. Dass die Anzeige in der Sonne stark spiegelt, konnte Apple aber leider auch bei dieser Generation
Nicht ohne Tadel
Kampf der mobilen Betriebssysteme Keine Frage: Den großen Wow-Effekt, den noch das erste iPhone 2007 auslöste, bedingt die vierte Version nicht mehr. Denn die Konkurrenz hat das Bedienkonzept längst aufgegriffen und teilweise optimiert. Zudem werden viele aktuelle Smartphones preisgünstiger angeboten als das iPhone.
Anwendungen im Market, derzeit sind es über 90.000. Davon sind 60 Prozent kostenlos. Ins Hintertreffen geraten ist dagegen Windows Mobile, das bald Windows Phone heißen wird. In der aktuellen Version 6.5 fehlen TouchscreenFeatures und auch ein Software-Shop. Deshalb geht das Warten auf die siebte Ausgabe weiter, die frühestens im Herbst kommen soll. Dafür haKlar auf den Massenmarkt zielt dabei das Anben mehrere Hersteller wie HTC oder Samsung droid-Betriebssystem von Google, für das einige Geräte angekündigt, die dann wohl leistungsfäHersteller Geräte bieten, die es schon für rund hige Prozessoren und viel Speicher brauchen, 200 Euro ohne Vertrag gibt. Auch wenn Google damit das System mit seinen Multimedia-Featuden Verkauf seines Nexus One unter eigenem Nares auch sein Potenzial entfalten kann. men einstellen wird, nimmt die Zahl Noch immer das meistgenutzte der Produkte ständig zu. Bei den akmobile Betriebssystem ist Symbian, tuellen Modellen ist etwa das Samdas fast nur noch Nokia in der Versung Galaxy S zu nennen, das einen sion Series-60 verwendet. Es ist Gigahertz-Prozessor und viel Ausnicht optimal auf Touchscreens abstattung hat (Bild 3). Sein Supergestimmt und auch nicht allzu Amoled-Display ist sogar noch etschnell. Abhilfe könnte die neue was größer als beim iPhone und stark verbesserte Version Symbian 3 weist ebenfalls eine brillante Darschaffen, die Nokia in seinem stellung auf. Auch Sony Ericsson mit Smartphone-Spitzenmodell N8 im dem Xperia X10 und dem X10 Mini, September zum ersten Mal einsetzen HTC mit den Hero und Legend sowie wird (Bild 4). Außerdem entwickelt Motorola mit dem Backflip jagen das Das Samsung Galaxy Nokia zusammen mit Intel das BeiPhone. Ein weiteres Plus für Android S i9000 setzt auf An- triebssystem Meego, das auf Linux basiert. Geräte hat dafür noch nieist die ständig wachsende Zahl der droid (Bild 3)
54
Das Nokia N8 mit Symbian 3 (Bild 4) mand gezeigt, doch noch dieses Jahr soll es erste Netbooks und Tablets geben. Rein proprietär ist dagegen Blackberry OS, das Betriebssystem von Research in Motion, das nur in den Modellen des kanadischen Herstellers zum Einsatz kommt. Mit einem eigenen SoftwareShop und verschiedenen Geräten vom Tastaturbis zum Touchscreen-Telefon erreicht RIM immerhin den dritten Platz auf dem weltweiten Smartphone-Markt. Schließlich gibt es noch WebOS vom PDA-Pionier Palm. Das Pre Plus ist ein Smartphone mit Tastatur und Touchscreen, dessen Bedienung große Parallelen zum iPhone aufweist. Da Palm dieses Jahr von HP übernommen wurde, könnte das eigene Betriebssystem eine Wiedergeburt erleben und auch in HP-Geräten zum Einsatz kommen. Der Kampf der Betriebssysteme bleibt also spannend.
mac-developer 4/2010
055_DPP_MacDeveloper_Eindr_A4_v1.qxd:Layout 1
06.08.2010
14:01 Uhr
Seite 55
19. - 20. Oktober 2010 Meistersingerhalle Nürnberg
2 Konferenztage zu
Datenbanken in der Cloud
Für Leser de r
E 100,– Ermäßigung auf die Teilnahm e. Ihr Anmelde code: DBPD10mac
20. Oktober 2010
19. Oktober 2010
SimpleDB, CouchDB & Co.
SQL Azure „Database as a Service“ in der Microsoft-Welt
Jenseits der SQL-Server – Datenbanken für das Web 2.0
Content-Manager und Moderator: .. .. Markus Eilers, Geschaftsfuhrer, runtime software GmbH
Content-Manager und Moderator: .. Prof. Dr. Stefan Edlich, Beuth Hochschule fur Technik Berlin
• SQL-Azure-Projekte aufsetzen, verwalten,
• Grundlagen: NoSQL und Map-Reduce für
• Die richtigen Architekturmuster zur
• Replikation und Persistenzstrategien
• Sicherheit garantieren mit Daten-
• Zukunftssicher: Skalieren von
mit der richtigen Architektur einbinden
Performancesteigerung kennenlernen
verschlüsselung und Zugriffskontrolle
Information und Anmeldung : Veranstalter:
Ihre Anwendung
für Cloud-Datenbanken
nichtrelationalen Datenbanken
www.databasepro-powerdays.de Präsentiert von:
055_DPP_MacDeveloper_Eindr_A4_v1.qxd:Layout 1
06.08.2010
SQL Azure Microsofts CloudDatenbank steht im Mittelpunkt von Tag 1 der databasepro powerdays. Hohe Skalierbarkeit Markus Eilers und Zuverlässigkeit Content Manager zeichnen zwar alle nicht-relationalen Datenbanken in der Cloud aus, doch stößt man in bestehenden Anwendungen und komplexen Datenmodellen schnell an deren Grenzen. Mit SQL Azure steht jetzt erstmals eine relationale Datenbank ohne diese Nachteile in der Cloud bereit. Aber auch mit SQL Azure, obwohl für Kenner von SQL-Server schnell und einfach nutzbar, muss man umgehen lernen. Wie geht man um mit LINQ, MultiTenancy und Partitioning? Wie migriert man eine bestehende Anwendung möglichst reibungslos in ein SQL-Azure-Umfeld? Sie erhalten am 19. Oktober in Nürnberg konkrete Anleitungen und lernen die wichtigsten Do’s and Dont’s zu Architektur, Sicherheit und den noch vorhandenen Einschränkungen kennen.
Begrüßung durch Erik Franz, Chefredakteur databasepro und Markus Eilers, Geschäftsführer runtime Software GmbH
09.15 – 10.15 Uhr
Golo Roden Relationale Daten – Eine Wiederauferstehung in der Cloud Cloud Computing wird oft gleichgesetzt mit relationsloser Datenhaltung. Wo die wirtschaftliche Optimierung von Software und High-Performance-Szenarien im Vordergrund stehen, haben Large-Scale-Webanwendungen wie Facebook, Amazon, twitter und Co.
10.20 – 11.20 Uhr
Tim Fischer
Tim Fischer Portierung bestehender Anwendungen auf SQL Azure Wenn Sie mit Ihrer Datenbankanwendung nicht auf der grünen Wiese starten, ist ein abgesicherter Umstieg erfolgskritisch. Der
13.50 – 15.05 Uhr
Wer sollte teilnehmen?
15.10 – 16.10 Uhr
16.10 – 16.40 Uhr
16.40 – 17.40 Uhr
Ronny Leger
17.45 Uhr
gen, ob mit isolierten Datenbereichen oder ohne, bringt SQL Azure hervorragende Unterstützung. Der Vortrag zeigt, wie Sie mit der richtigen Architektur und dem passenden Konzept Ihre Datenbank elastisch machen.
Pilotprojekte – Ein Erfahrungsbericht Mittelstandsanwendung. Wie ist es um die Praxistauglichkeit des Dienstes gestellt? Wie setzen die verschiedenen Unternehmen die Datenbank in der Praxis ein? Dieser Vortrag liefert wertvolle Einsichten.
Kaffeepause und Besuch der Ausstellung
Markus Eilers Outlook: Was bringt die nächste Version von SQL Azure? Welche Neuerungen und Features bringen die nächsten Versionen von SQL Azure mit sich? Wie sieht Microsoft die langfristige Strategie und die Einsatzszenarien für das Produkt und welche Einschränkungen und Sicherheitsüberlegungen werden Kunden
Die Teilnehmer der databasepro powerdays haben die Möglichkeit, kostenlos daran teilzunehmen. Registrieren Sie sich unter www.databasepro-powerdays.de/Anmeldung. Das Kontingent ist begrenzt.
Markus Eilers Mach’s Maxi: SQL Azure im Livebetrieb richtig partitionieren und skalieren
Gut ein halbes Jahr nach der Markteinführung berichtet der SQL-Azure-Berater und CTO des Startups pulsd von Erfahrungen mit SQL Azure aus Sicht einer SaaS-Geschäftsanwendung, eines Social Games und einer
Abendprogramm
Vortrag klärt wichtige Fragen vorab und zeigt die Migration einer Datenbank und Anwendung am Beispiel.
Mittagspause und Besuch der Ausstellung
Ihre Anwendung hat Fans? Sie bedienen viele hundert oder gar tausende User? Sie müssen für transaktionale Schwergewichte vorbereitet sein? Wenn Sie zuverlässige Performance bei massiver Skalierung benöti-
Die Veranstaltung richtet sich an Datenbankenwickler und -administratoren, IT-Verantwortliche, Sys-Admins, CIOs, IT-Consultants und Fachinformatiker Systemintegration.
und Sicherheit optimiert. Ferner geht der Vortrag darauf ein, wie man NoSQL-Datenbanken wie Azure Table Storage und SQLServer-Datenbanken mit dem gleichen LINQ-basiertem API ansteuern kann und so flexibel bleibt.
Kaffeepause und Besuch der Ausstellung
11.20 – 11.50 Uhr
11.50 – 12.50 Uhr
einen Trend zu NoSQL, Denormalisierung und flacher, redundanter Speicherung begründet. Anhand typischer Szenarien zeigt der Übersichtsvortrag, wann sich NoSQL- und FullSQLDatenhaltung qualifizieren und welche Vorbedingungen nach hybriden Ansätzen verlangen.
Arbeiten mit SQL Azure
SQL Azure kommt mit den wichtigsten Funktionalitäten vom SQL-Server in die Cloud. Das gibt Entwicklern Sicherheit bei der Entscheidung für den Datenbankservice. Im Vortrag wird gezeigt, was SQL Azure leistet und wie man einige der Einschränkungen überwindet sowie die Performance
12:50 – 13:50 Uhr
Am 19. Oktober findet ab 19:30 das Abendprogramm der prio.conference in der Meistersingerhalle Nürnberg statt.
Seite 56
und Entwickler durch die nächsten Releases begleiten? Spannende Hintergründe, Lerneffekte aus den frühen Tagen des Dienstes und Rückmeldungen der weltweiten Kundenbasis prägen die Roadmap und die weitere Entwicklung.
Abschlussdiskussion: Die Grenzen von SQL Azure Programmänderungen vorbehalten
Mehr Informationen und Anmeldung unter: Medienpartner:
Begrüßung durch Erik Franz, Chefredakteur databasepro und Prof. Dr. Stefan Edlich, Beuth Hochschule für Technik Berlin
09.15 – 10.00 Uhr
Achim Friedland Graph-Daten in der Cloud mit .NET beherrschen Anders als bisherige Datenbanken stellen Graph-Datenbanken vernetzte semi-strukturierte Informationen und deren effizientes Management in ihren Mittelpunkt. CloudInfrastrukturen bieten sich somit als idealer Partner beim Betrieb verteilter Graph-
10.05 – 11.05 Uhr
Tim Lossen Key-Value-Stores: Der Schlüssel zur Skalierung Cassandra, Riak, Redis, Tokyo Tryrant .... viele NoSQL-Projekte werden in die Kategorie 'Key-Value-Store' eingeordnet. Was unterscheidet diese Projekte von anderen NoSQL-Ansätzen? Tim Lossen erläutert
11.05 – 11.35 Uhr
11.35 – 12.35 Uhr
13.50 – 14.50 Uhr
Mathias Meyer Document Databases in der Cloud
15.20 – 16.20 Uhr
Marc Boeker Skalierst du noch oder nutzt du schon MongoDB?
Teilnahmegebühr und Leistungen Die Teilnahmegebühr für die databasepro powerdays beträgt pro Tag € 499,– zzgl. MwSt. Bis zum 13. September 2010 gilt der Frühbucherpreis in Höhe von € 399,– zzgl. MwSt. In der Teilnahmegebühr der databasepro powerdays sind enthalten:
Jonathan Weiss Skalierbare Anwendungen mit SimpleDB und RDS die Datenbank-Angebote SimpleDB und RDS von Amazon eingegangen, die es Entwicklern ermöglichen, hoch-verfügbare und skalierende Anwendungen in der Cloud zu betreiben.
Andreas Widmann Performance Tests in der Cloud Performance testen ist wesentlicher Bestandteil des Application Lifecycle. Doch nicht alle Firmen verfügen über das notwendige Know-How und die erforderliche Infrastruktur, um solche Tests effizient
17.30 Uhr
lastintensive Apps entwickeln. Der Vortrag klärt alle Fragen von der Installation, über die ersten Gehversuche bis hin zur "8 Mio. Operations pro Sekunde"-Konfiguration.
Kaffeepause und Besuch der Ausstellung
Amazon stellt eine breite Pallette an Infrastruktur für das Cloud Computing zur Verfügung. Von Speicher über MessagingSysteme bis zu virtuellen Server auf Abruf reicht das Angebot. In dem Vortrag wird auf 16.25 – 17.25 Uhr
struktur in der Cloud, Skalierung über mehrere Geo-Locations und Data-Center hinweg sind Herausforderungen, denen sich CouchDB und Riak mit interessanten, aber völlig unterschiedlichen Ansätzen stellen.
Am Tag 2 der databasepro powerdays geht es um eine neue Generation Datenbanken, die die Welt in den letzten Jahren im Sturm erobert hat. Prof. Dr. Stefan Edlich Content Manager Im Vordergrund steht dabei die Fixierung auf eine perfekte horizontale Skalierung der Datenbank mit handelsüblicher Hardware und nicht primär das Datenmodell. Die meisten dieser sogenannten NoSQL-Datenbanken sind Open-Source, besitzen einfache APIs – wie beispielsweise REST-Schnittstellen – und sind ideal für eine agile Softwareentwicklung. Dieser powerday hat zwei zentrale Zielrichtungen: Einerseits geht es um eine praxisnahe ‘Hands-On’ Einführung von Experten, die aufzeigt, wie man solche Mega-Scale Systeme aufbaut. Auf der anderen Seite werden die NoSQL-Dimensionen wie Datenbank-Kategorien oder CAP-Gewichtung analysiert, um die richtige Datenbank für das vorliegende Softwareproblem auszuwählen. Dabei wird auch auf viele andere Datenbankzugriffsformen außerhalb der .NET-Welt eingegangen.
Mittagspause und Besuch der Ausstellung
SQL nein Danke! Wer keine Lust mehr auf starre SQL-Schemata und ewige Skalierungsworkshops hat, dem hilft MongoDB. Schnell und unkompliziert lassen sich damit einfache aber auch komplexe und 14.50 – 15.20 Uhr
anhand von konkreten Beispielen aus der Praxis, wo die Stärken und Schwächen von Key-Value-Stores liegen und wie man die richtige Lösung für die eigenen Anforderungen auswählt.
Kaffeepause und Besuch der Ausstellung
Dieser Vortrag zeigt, wie sich Dokumenten-orientierte Datenbanken anschicken, in Sachen Produktivität und Skalierbarkeit den großen Playern der SQL-Welt Besitztümer streitig zu machen: Die ständige Fluktuation der Infra12:35 – 13:50 Uhr
Datenbanken an. Der Vortrag soll hierzu eine kurze Einführung in das Thema liefern und einen Überblick über die derzeitigen praktischen Lösungen verschiedener Hersteller geben.
SimpleDB, CouchDB & Co.
durchzuführen. Im Vortrag wird aufgezeigt, wie HP Loadrunner die Integrität von Applikationen überprüft und Möglichkeiten zur Performanceoptimierung aufzeigt.
Abschlussdiskussion und Ende des powerdays
der Vorträge und der beglei• Besuch tenden Fachausstellung Mittagessen und Kaffeepausen • Zugang Download der Vorträge • nach derzumVeranstaltung Zeitgleich findet am 19. und 20. Oktober 2010 in der Meistersingerhalle Nürnberg die prio.conference (www. prioconference.de) zum Thema Verteilte Architektur statt. Ein All-AreaTicket für € 1.190,– zzgl. MwSt. ermöglicht den Eintritt zu beiden Konferenzen. Der Frühbucherpreis für das All-AreaTikket beträgt € 1.090,– zzgl. MwSt. bis 13. September 2010.
Programmänderungen vorbehalten
www.databasepro-powerdays.de
055_DPP_MacDeveloper_Eindr_A4_v1.qxd:Layout 1
06.08.2010
Anmeldung databasepro powerdays 19. - 20. Oktober 2010 Meistersingerhalle Nürnberg, Münchener Str. 21, 90478 Nürnberg
14:01 Uhr
Seite 58
Nutzen Sie als Leser der MacDeveloper Sonderkonditionen
Hiermit melde ich mich verbindlich zur Teilnahme an: SQL Azure am 19. Oktober 2010
All-Area-Ticket databasepro powerdays und prio.conference am 19. und 20. Oktober 2010 € 1.090,– zzgl. MwSt. (statt € 1.190,– zzgl. MwSt.)
Bitte geben Sie unter www.databasepro-powerdays.de/Anmeldung folgenden Code ein, um die Sonderkonditionen zu nutzen: DBPD10mac Informationen zu Anreise und Übernachtungen finden Sie unter www.databasepro-powerdays.de/Anreise-Uebernachtung
www.databasepro-powerdays.de
Per Fax: +49 (0)89 74117-448
Per Post: Neue Mediengesellschaft Ulm mbH Kongresse & Messen Claudia Fink Bayerstraße 16a 80335 München Teilnahmebedingungen / AGBs 1. Anmeldung Wir bestätigen Ihre Anmeldung per E-Mail.
Ja, senden Sie mir kostenlos die nächste Ausgabe der zweimonatlich erscheinenden Fachzeitschrift databasepro zu. Wenn ich von databasepro überzeugt bin und nicht 10 Tage nach Erhalt der Leseprobe schriftlich abbestelle, dann beziehe ich danach databasepro für ein Jahr zum Vorzugspreis von z.Z. 12,70 Euro je Ausgabe oder 76,20 Euro im Jahr inkl. MwSt. und Versand. So spare ich fast 15% gegenüber dem Kauf am Kiosk. In Österreich betragen die entsprechenden Preise 13,95 Euro je Ausgabe oder 83,70 Euro im Jahr, in der Schweiz 25,34 Franken bzw. 152,00 Franken im Jahr. Das Abonnement verlängert sich automatisch um 1 Jahr, wenn es nicht 6 Wochen vor Ablauf der Bezugszeit schriftlich gekündigt wird. Teil des Heftabonnements ist der 14tägliche Newsletter, der Sie mit praktischem Wissen für professionelle Datenbank-Architekten, -Administratoren, Consultants, Anwender und IT-Manager, die sich mit der Auswahl von Technologien, Plattformen, Datenbanken und Entwicklungsumgebungen beschäftigen, versorgt.
Anmeldedaten: Vorname:*
Nachname:*
Firma:
Abteilung:
Straße, Nr.:*
PLZ/Ort:*
Land: Telefon:
Fax:
E-Mail:*
*sind Pflichtfelder
Hiermit melde ich mich zu der vorgenannten Veranstaltung an und bestätige, dass ich die AGBs gelesen und akzeptiert habe.
Datum / Unterschrift: Wir machen Sie ausdrücklich auf Ihr Widerrufsrecht gegen die Speicherung und Verwendung Ihrer personenbezogenen Daten zu Werbe- und Marketingzwecken aufmerksam. Sie können Ihre Einwilligung zur Verwendung Ihrer persönlichen Daten zu den vorgenannten Zwecken jederzeit für die Zukunft widerrufen. Hierzu genügt eine E-Mail an [email protected] oder eine Mitteilung in Textform (Fax, Brief) an die vorstehend genannte Adresse.
Buchung Aussteller und Sponsoren: Marketing Projekt 2000, Birgit und Axel Reber, Tel.: +49 (0)8205 962 33, E-Mail: [email protected]
2. Zahlungsbedingungen Der Rechnungsbetrag ist 14 Tage nach Erhalt der Rechnung, spätestens am Tag des Besuchs der ersten gebuchten Veranstaltung fällig und ab dann mit 8 %-Punkten über dem Basiszinssatz zu verzinsen. 3. Leistungserbringung und Rücktrittsvorbehalt Wir behalten uns vor, inhaltliche und zeitliche Änderungen im Veranstaltungsprogramm und bei der Besetzung der Referenten vorzunehmen. NMG ist berechtigt vom Vertrag zurückzutreten, wenn die für eine wirtschaftliche Durchführung der Veranstaltung erforderliche Zahl an Ausstellern und Sponsoren nicht erreicht wird, der Hauptveranstalter die Veranstaltung nicht durchführt oder sonstige nicht im Verantwortungsbereich der NMG liegende Gründe vorliegen, die die Durchführung der Veranstaltung unmöglich machen. In diesem Falle wird der Besucher unverzüglich benachrichtigt und die bereits geleistete Zahlung unverzüglich erstattet. Weitergehende Ansprüche des Besuchers sind ausgeschlossen, soweit NMG nicht Vorsatz oder grobe Fahrlässigkeit zur Last liegt. 4. Stornierung/Umbuchung Sie können ihre Anmeldung nur bis 30 Tage vor Beginn der Veranstaltung stornieren; bereits entrichtete Teilnahmegebühren werden in diesem Fall innerhalb von 30 Tagen rückerstattet. Die Stornierung hat schriftlich an Neue Mediengesellschaft Ulm mbH, Bayerstrasse 16a, D-80335 München oder per Fax an +49 (0)89-74117-448 zu erfolgen. Die Benennung eines Ersatzteilnehmers ist jederzeit kostenfrei möglich. 5. Datenschutzhinweise Wir weisen darauf hin, dass personenbezogene Daten des Ausstellers nach den Bestimmungen des Bundesdatenschutzgesetzes (BDSG) sowie des Telemediengesetzes (TMG) erhoben, verarbeitet und genutzt werden. Alle über unsere Webseite erhobenen personenbezogenen Daten werden entsprechend den gesetzlichen Vorgaben behandelt und nicht an Dritte weitergegeben. Externe Dienstleister, die in unserem Auftrag Daten verarbeiten, sind ebenfalls den gesetzlichen Vorschriften verpflichtet, gelten jedoch nicht als Dritte. Ihre bei der Anmeldung erhobenen personenbezogenen Daten werden an die Aussteller und Sponsoren der von uns durchgeführten Veranstaltungen weitergegeben. Von dort können Sie weiterführende Marketinginformationen erhalten. Wir machen Sie auf Ihr Widerrufsrecht gegen die Speicherung und Verwendung Ihrer personenbezogenen Daten zu Werbe- und Marketingzwecken aufmerksam. Sie können Ihre Einwilligung zur Verwendung Ihrer persönlichen Daten zu den vorgenannten Zwecken jederzeit für die Zukunft widerrufen. Hierzu genügt eine E-Mail an [email protected] oder eine Mitteilung in Textform (Fax, Brief) an uns.
059_TT_ef_jp_ea.qxp
09.08.2010
17:04 Uhr
Seite 59
iOSDev Tricks für Entwickler ●
Nützliche Entwicklungshelfer
Tipps & Tricks D
ie folgenden Tricks von macdeveloper-Autor Ingo Böhme helfen Ihnen, in Xcode schneller ans Ziel zu kommen, gut versteckte Funktionen aufzuspüren oder nützliche Hilfsmittel und Erweiterungen zu finden.
Wechsel zwischen Headerund Moduldatei
Sie sich gut, wo der Button ist, denn sehen werden Sie ihn nicht mehr. Insofern sollten Sie erst den ganzen Akt mit der IBOutlet- und IBAction-Verknüpfung machen, solange er noch sichtbar ist. Sollten Sie ihn später suchen und nicht finden, hilft [Befehl L] für das Anzeigen von Rechtecken um Steuerelemente. Sie werden nur zur Designzeit angezeigt, nicht zur Laufzeit.
Die wohl wichtigste Xcode-Tas- Mit dem Completion Dictionary verwenden Sie eigene Code-Vervollständitenkombination ist [Wahl Befehl gungen in Xcode (Bild 1) Pfeil hoch]. Damit wechseln Sie tionsschluss 4.1.1). Aus dem Archiv nehvon Header- zu Moduldatei und wieder Komplette Xcode-Shortcut-Liste men Sie die Datei ODCompletionDictionary zurück. Wer ein neueres MacBook hat, das Eine hervorragende Zusammenstellung .xcplugin und kopieren sie in die beiden Mehrfingergesten auf dem Trackpad unteraller Xcode-Shortcuts – noch dazu ästheOrdner ~/Library/Application Support/Destützt, kann den Wechsel mit einem Witisch gelayoutet – hat Colin Wheeler erveloper/Shared/Xcode/Plug-ins/, wobei die schen nach oben mit drei Fingern erledigen. stellt. Unter www.iho.me/xcode finden Sie Tilde ~ für das Benutzerverzeichnis steht, die PDF-Datei in verschiedenen Farb-Vasowie in den Ordner /Library/Application Code-Vervollständigung mit rianten zum Download (Bild 2). Support/Developer/Shared/Xcode/Plug-ins/. Completion Dictionary Da es sich bei Letzterem um ein SystemXcode kann mit Plug-ins erweitert werverzeichnis handelt, müssen Sie den VoriPhone oder Simulator den. Eines der sinnvollsten ist Completion gang per Admin-Passwort bestätigen. Mit den Direktiven Dictionary (www.iho.me/dict). Dieses stellt Starten Sie dann Xcode neu. Im Menü eine einfache Art dar, Codeteile mit ParaEdit sehen Sie nun den neuen Menüpunkt #if TARGET_IPHONE_SIMULATOR meter-Platzhaltern zu erzeugen und diese Completion Dictionary. Mit Edit Macros könebenso wie die Codevervollständigung nen Sie die vorhandenen Textersetzungen beziehungsweise von Xcode selbst zu verwenden. verwalten, löschen oder neue hinzufügen Laden Sie von www.iho.me/dict die Datei (Bild 1). Mit Expand Macro erweitern Sie die #if TARGET_OS_IPHONE CompletionDictionary-xxx.zip (xxx steht Buchstabenkombination links vom Cursymbolisch für die Version; zum Redaksor im Editorfenster zum zugeordneten können Sie Code abhängig vom Build-ErTextblock. Das Besondere ist, gebnis einbinden. dass Sie in den Textbausteinen Platzhalter im Stil von <#hier eingeben#> verwenden können, Splashscreen für 1 Sekunde die im Quellcode als FormularWenn Ihre App zu schnell startet, Sie aber felder erscheinen. auf dem Splash-Screen gern etwas Eigenwerbung anzeigen wollen, bietet es sich UILabels zum Anklicken an, ein Päuschen einzulegen. Der optiEin UILabel hat keine Standardmale Ort ist in die Delegate-Moduldatei Events wie beispielsweise eine in der Startmethode application:didFinishSchaltfläche. So ist es schwieLaunchingWithOptions direkt nach dem rig, etwa eine Webadresse anAufruf von [window makeKeyAndVisible];. klickbar in einem Label darzuGeben Sie dort stellen. Ein kleiner Trick ist, das NSDate *future = [NSDate date3 Label zu formatieren und dann WithTimeIntervalSinceNow: 1.0 ]; einen UIButton drüber zu le[NSThread sleepUntilDate:future]; gen. Für diesen wiederum wählen Sie im Attribute Inein, so wird der Splash-Screen genau 1,0 spector als Type den Eintrag Die gelayoutete Xcode-Shortcut-Liste von Colin Wheeler gibt es Sekunden lang angezeigt. [jp] Custom. Aber Achtung: Merken als PDF im Internet kostenlos zum Download (Bild 2)
4/2010 www.mac-developer.de
59
060_Graphviz_jp_ea.qxp
05.08.2010
16:18 Uhr
Seite 60
WebDev Visualisierung von Graphen ●
Graphviz für Mac-OS-X- und iPhone-Anwendungen
Graphenmeister Die Welt wird immer vernetzter: Soziale Netze, Graphendatenbanken und komplexe Netzwerkinfrastrukturen setzen sich durch. Um solche Netzwerke zu visualisieren, bietet sich Graphviz an. Oliver Brüning Auf einen Blick Wie Entwickler Graphen mit dem Open-Source-Programmpaket Graphviz darstellen und verschiedene Ausgabeformate wie beispielsweise SVG erzeugen, zeigt mac-developer-Autor Oliver Brüning in seinem Artikel. Außerdem werden die wichtigsten Zusatz-Tools und Beispielanwendungen vorgestellt.
G
raphviz ist ein von AT&T entwickeltes Open-Source-Programmpaket zur Visualisierung von gerichteten und ungerichteten Graphen. In guter Unix-Tradition besteht es aus mehreren kleinen Kommandozeilen-Programmen, die hintereinander geschaltet werden können (Piping). Das Paket enthält auch eine Reihe von Viewern und interaktiven Tools, die allerdings teilweise schon etwas in die Jahre gekommen sind. Die Software verarbeitet DOT-Dateien, in denen die Struktur der zu visualisierenden Graphen definiert ist. DOT ist eine deklarative Programmiersprache, mit der ausschließlich Objekte und deren Beziehungen zueinander beschrieben werden. In DOT wird nicht festgelegt, in welcher Form die Visualisierung erfolgen soll. Für die eigentliche Visualisierung hält Graphviz unterschiedliche Layout-Verfahren bereit, die je nach Anwendungsfall gewählt werden können (siehe Kasten „Layout-Algorithmen“, Seite 62). Eine besondere Stärke von Graphviz sind die vielen Ausgabeformate. Neben den gängigen Bitmap-Formaten wie JPEG, PNG, GIF und TIF stehen auch eine Vielzahl von Vektorformaten wie Postscript oder PDF zur Verfügung. Für den Einsatz im Web ist vor allem das SVG-Format interessant. Selbst Imagemaps können erzeugt werden.
Plattformen und Sprachanbindungen für Graphviz
Hello World als Graph (Bild 1)
60
Graphviz wurde für verschiedene Plattformen portiert, unter anderem auch für Mac OS X. Wer über die nötigen C-Kenntnisse verfügt, kann Graphviz-Funktionalität auch in seine eigenen Applikationen integrieren. Die Libraries und der Quellcode stehen auf der Graphviz-Projektseite www.graphviz.org zum Download bereit. Selbst das iPhone ist eine passende Plattform für Graphviz: Der mobile Safari-Browser kann von Haus aus SVG in hoher Qualität darstellen. Im App Store findet sich darüber hinaus die kostenpflichtige App Instaviz, eine Art interaktiver mobiler Mindmap-Manager (siehe Kasten „Instaviz“, Seite 63). Graphviz kann von fast jeder Programmiersprache aus angesprochen werden. Für Programmiersprachen, die auf C aufsetzen, gibt es
meist native Bindings. Das hat den Vorteil, dass Graphviz direkt in die Applikation eingebettet ist und somit im gleichen Prozess läuft. Lose Bindings erfüllen aber ebenfalls ihren Zweck. So stellt das Ruby-Gem Ruby-Graphviz ein Ruby-API zur Verfügung, um programmatisch DOT-Strukturen zu erzeugen. Das eigentliche Rendern erfolgt dann einfach über den Aufruf der Unix-Tools. In der Praxis wird man lose Bindings eher für serverseitige Anwendungen verwenden. Java-Anhänger können einen Blick auf das Grappa-Projekt von AT&T werfen. Grappa ist eine Java-Portierung von Graphviz, in der allerdings viele Features noch nicht implementiert sind, zum Beispiel aus dem Bereich Layout. Wer nicht unbedingt auf Java angewiesen ist, sollte zum Original greifen.
Einsatzgebiete: Von Netzstrukturen bis UML Die Einsatzgebiete von Graphviz sind vielfältig. Die Resources-Seite listet fast 100 Tools und Projekte auf. Klassische Anwendungsfälle aus der IT-Welt sind die Darstellung von Netzstrukturen und Datenbankschemata. Hier findet sich eine große Anzahl verschiedener Tools. Auch UML-Applikationen wie ArgoUML nutzen die Möglichkeiten von Graphviz zur Visualisierung von Modellen und Klassenhierarchien. Für Textsatzsysteme wie LaTeX oder auch AsciiDoc gibt es Skripts und Erweiterungen, die eine Integration von Graphen in Dokumente ermöglichen. Doch auch Programme mit einem geringer ausgeprägten akademischen Charakter wie OmniGraffle nutzen für ihre Diagrammfunktio-
Listing 3: Gerichteter Graph digraph directed_example { start -> a; start -> c; a -> b; c -> d; b -> end; d -> end; }
Listing 4: Cluster digraph cluster_example { subgraph cluster_0 { label = "c0"; a -> b; } subgraph cluster_1 { label = "c1"; c -> d; } start -> a; start -> c; b -> end; d -> end; }
nen Graphviz. Seit Neuestem gibt es sogar eine Graphviz-Erweiterung für MS Visio, allerdings von einem Drittanbieter.
Graphviz im Browser Für den Einsatz im Web stehen verschiedene Viewer zur Verfügung. An vorderster Stelle ist hier Canviz zu nennen, ein Javascript-basierter Viewer. Canviz zeigt allerdings noch einige Darstellungsprobleme im Internet Explorer. Der Flash-Viewer VizierFX könnte für interaktive Webapplikationen interessant sein. Wenn man einen Viewer braucht, der ohne viel Experimentieren in den meisten gängigen Browsern funktioniert, dann kommt man allerdings an Svgweb von Google nicht vorbei (siehe Kasten „SVG für alle“, Seite 63).
Hello World in der DOT-Edition
4/2010 www.mac-developer.de
▲
Die DOT-Sprache ist rein deklarativ und in ihren Grundzügen schnell erklärt. Man unterscheidet zwischen ungerichteten (graph) und gerichteten (digraph) Graphen. Das klassische „Hello World“ sieht in Graphviz so aus wie in Listing 1 gezeigt.
Um diesen einfachen Graphen in eine EPSAutor Grafik (Bild 1) zu transformieren, benötigen Sie das DOT-Programm (Listing 2). Die Option -K gibt den Layout-Algorithmus an, der verwendet werden soll (siehe Kasten „Layout-Algorithmen“, Seite 62). Das Ausgabeformat legen Sie über die Option -T fest. Geben Sie kein Ausgabeformat an, wird ein sogenanntes Attributed DOT erzeugt. Dabei handelt es sich um ein Zwischenformat, das die Knoten und Kanten eines Graphen mit PositionsangaOliver Brüning arbeitet seit über ben anreichert. Das kann zum Beispiel dann zehn Jahren als Software-Entsinnvoll sein, wenn man den Output an andere wickler für ein Finanzinstitut. Er Programme weiterleiten will (Piping). war an der Entwicklung mehreWenn Sie nicht extra einen Dateinamen für die rer J2ME-Projekte beteiligt und Ausgabedatei angeben wollen, hilft die Option programmiert zurzeit Inhouse-O, die den Namen der Ausgabedatei aus dem Lösungen in Ruby on Rails. Namen der Eingabedatei herleitet. Der VerboseModus, der mit der Option -v aktiviert wird, liefert interessante Einblicke in die Arbeitsweise von Graphviz. Vor allem bei größeren Graphen kann man verfolgen, welche Anstrengungen Graphviz unternimmt, um ein möglichst kreuzungsfreies Routing zu erreichen. Das Beispiel in Listing 3 zeigt einen gerichteten Graphen mit sechs Knoten. Um zu verdeutlichen, dass bestimmte Knoten zusammengehören, kann man sie zu sogenannten Clustern gruppieren (Listing 4). Für jeden Cluster wird ein Subgraph definiert, der zusammengehörige Knoten enthält. Beim Rendern werden diese Knoten dann mit einem Rechteck umgeben (Bild 2). Nicht alle Layout-Algorithmen beherrschen Clustering. Es ist auch unbedingt erforderlich, die Namenskonventionen (cluster_0, cluster_1 et cetera) einzuhalten. Bis jetzt beschränkten sich die gezeigten Graphen auf das absolut Notwendige. Knoten und Kanten können jedoch fast beliebig gestaltet werden. Kanten dürfen zum Beispiel auch mit einer Beschriftung versehen werden. Schriftarten, Farben und Pfeilformen sind frei wählbar. Bei Knoten hat man die Auswahl aus über 30 Formen (Shapes). Wem das nicht reicht, der kann eigene Shapes definieren (Listing 5). Bei Knoten mit dem Shape record erhält das label-Attribut eine besondere Bedeutung: Knoten können in verschiedene Regionen aufgeteilt werden, die über einen Identifier (ID) in spitzen Klammern benannt werden. Bei den Verbindungsdefinitionen kann man dann auf diese IDs Bezug nehmen. Bild 3 zeigt das Endergebnis. Gerichteter Graph mit zwei Clustern (Bild 2)
61
060_Graphviz_jp_ea.qxp
05.08.2010
16:18 Uhr
Seite 62
WebDev Visualisierung von Graphen ●
Spezialwerkzeuge für den Umgang mit Graphen
In Regionen aufgeteilte Knoten (Bild 3)
In der Praxis wird man es hauptsächlich mit dem DOT-Programm zu tun haben. In der Graphviz-Werkzeugkiste finden sich allerdings noch einige andere Tools, die nicht unerwähnt bleiben sollten. Gc zählt die Graphen, Knoten und Kanten in einer DOT-Datei. Nop kommt als Prettyprinter für DOT-Quelltexte zum Einsatz. Gvpr ist eine Art DOT-Postprocessor, der über alle Graphen, Knoten und Kanten einer DOTDatei iteriert. In einem Gvpr-Programm ist definiert, welche Aktionen ausgeführt werden sollen, wenn zum Beispiel ein Knoten mit einer bestimmten Eigenschaft gefunden wird. So könnte man die Farbe aller blauen Knoten auf Rot ändern oder bestimmte Knoten aus Graphen löschen.
Fazit
Mit dem Neato-Algorithmus gerendertes ER-Diagramm (Bild 4)
Graphviz ist ein sehr leistungsfähiges und flexibles Programm-Paket. Es implementiert hochwertige Layout-Algorithmen und kann nahezu alle Image-Ausgabeformate erzeugen. Wenn es darum geht, Graphen zu visualisieren, führt kein Weg an Graphviz vorbei.
Durch vielfältige Gestaltungsmöglichkeiten kann man den technischen Charakter vieler Graphen auflockern, wie man beispielsweise bei OmniGraffle gut erkennen kann. Apps wie Instaviz zeigen, dass nicht nur Viewer, sondern auch einfach zu bedienende Grapheneditoren realisierbar sind. Browserbasierte Editoren auf SVG-Basis sind die nächste große Herausforderung. [jp]
Layout-Algorithmen DOT-Dateien können mit verschiedenen Layout-Algorithmen gerendert werden. Für gerichtete Graphen verwendet man in der Regel den DOT-Algorithmus. Dieser Algorithmus ist in der Hinsicht hierarchisch, dass er versucht, alle Kanten möglichst in die gleiche Richtung zeigen zu lassen, also entweder von oben nach unten oder von links nach rechts. Darüber hinaus wird eine möglichst kreuzungsarme Darstellung angestrebt. Der Neato-Algorithmus kommt vor allem bei ungerichteten Graphen zum Einsatz (Bild 4). Hier ist die Idee, dass alle Kanten in etwa die gleiche Länge haben sollen. Man kann sich die Funktionsweise dieses Algorithmus ansatzweise erklären, indem man sich die Kanten als Sprungfedern vorstellt, die zwischen den Knoten eingehängt sind. Die wirkenden Kräfte führen dann zu einem stabilen Zustand. Da dieser Algorithmus iterativ ist und mit zufälligen Startwerten arbeitet, sind die Ergebnisse bei jedem Lauf minimal unterschiedlich, was in der Praxis kein großes Problem darstellen sollte. Kreisförmige Layouts sind mit dem Circo-Algorithmus möglich. Ebenfalls kreisförmige Layouts erzeugt der Twopi-Algorithmus. Hier werden ausgehend von einem Root-Knoten weitere Knoten je nach ihrer Entfernung vom Root-Knoten auf konzentrischen Kreisen angeordnet.
Listing 7: SVG mit Link
62
mac-developer 4/2010
060_Graphviz_jp_ea.qxp
05.08.2010
16:18 Uhr
Seite 63
WebDev Visualisierung von Graphen ●
SVG für alle SVG bietet sich als Ausgabeformat für Graphviz an. Zum einen sind SVG-Grafiken stufenlos skalierbar, zum anderen kann man sehr leicht Elemente von Graphen mit Links hinterlegen und damit klickbar machen. Interaktive Anwendungen sind dadurch schnell und ohne großen Aufwand realisierbar. Die Unterstützung von SVG ist in den Browsern sehr unterschiedlich. Der Internet Explorer weiß gar nichts mit diesem Format anzufangen. Er war bisher auf das inzwischen eingestellte Adobe-SVG-Viewer-Plug-in angewiesen. Firefox und Safari unterstützen SVG, aber die Einbindung in HTML-Seiten ist nicht einheitlich. Auch in der Darstellung gibt es Abweichungen zwischen den beiden Browsern. Google hat sich dieses Problems angenommen und bietet mit Svgweb eine Integrationslösung für alle gängigen Browser an. Listing 6 zeigt die grundsätzliche Verwendung des Svgweb-Viewers. Die Referenz auf die Javascript-Datei svg.js muss vor allen anderen Skripts auf der Seite stehen. Falls sich die HTML-Seite und svg.js in verschiedenen Verzeichnissen befinden, ist die Angabe des data-path-Attri-
buts erforderlich. Der SVG-Content wird in ein eigenes Skript-Tag mit dem Typ image/svg+xml gepackt. Interessant ist das Meta-Tag svg.render.forceflash. Setzt man das Attribut content auf true, so wird zur Darstellung von SVG immer Flash herangezogen, unabhängig davon, ob der Browser selbst mit SVG umgehen kann. Setzt man content auf false, wird zunächst überprüft, ob der Browser SVG-fähig ist. Wenn ja, wird das Rendern dem Browser überlassen, wenn nicht, wird Flash genommen. Für Anwendungen, die in möglichst allen Browsern gleich aussehen und sich gleich verhalten sollen, sollte man content auf true setzen. Anders verhält es sich mit den mobilen SafariBrowsern auf iPhone und iPad. Da diese bekanntlich auch weiterhin auf Flash verzichten müssen, sollten Entwickler eine Browser-Erkennung durchführen und das content-Attribut auf false setzen. Um den Kreis myCircle im Beispiel-SVG klickbar zu machen, würde man ihn mit einem Link-Tag umgeben (Listing 7). Man beachte, dass wirklich nur der Kreis klickbar ist, es gibt also keine Bounding Box.
Instaviz Die App Instaviz ist ein interessantes und ziemlich einmaliges Beispiel für die Nutzung von Graphviz auf einem Smartphone. Das beginnt schon bei der Bedienung. Um einen Knoten zu erstellen, malt man den gewünschten Umriss – zum Beispiel einen Kreis oder ein Dreieck – auf den Touchscreen (Bild 5, Bild 6). Instaviz erkennt die gemalte Form und baut einen passenden Knoten in den Graphen ein. Verbindungen werden einfach dadurch hergestellt, dass eine Linie zwischen zwei
Knoten gezogen wird. Nach Benutzerinteraktionen rendert Instaviz den Graphen sofort neu, ein manuelles Anpassen der Positionen von Knoten ist nicht erforderlich und auch nicht möglich. Man kann allerdings den Layout-Algorithmus ändern. Unterstützt werden der DOT- und der Neato-Algorithmus. Mit dem Programm Instavue lassen sich mit Instaviz erstellte Graphen auf den Mac oder PC exportieren. Instaviz ist für 7,99 Euro im App Store erhältlich.
Apple-Chef verteidigt seine ablehnende Haltung gegenüber Flash
Ist Flash am Ende? In näherer Zukunft wird es kein Flash-Plug-in für mobile Apple-Geräte geben. Dieser Artikel beleuchtet die Hintergründe. Außerdem erfahren Sie mehr über die Flash-Technik und Flash-Alternativen. Ingo Dellwig Auf einen Blick Eine Stellungnahme von AppleChef Steve Jobs gibt Entwicklern wichtige Anhaltspunkte, mit welchen Technologien künftig auf den iOS-Geräten gerechnet werden darf – und mit welchen nicht. mac-developer-Autor Ingo Dellwig analysiert die Gründe für die Flash-Verweigerung und stellt mögliche Alternativen vor.
B
ekanntlich ist ein Flash-Plugin für mobile Apple-Geräte nicht geplant. Viele Anwender vermissen diese Technik so sehr, dass sich Apple-Chef Steve Jobs genötigt fühlte, Ende April einen offenen Brief mit seinen Überlegungen zu Flash zu veröffentlichen (Bild 1). Er betont darin, dass es sich bei der Entscheidung gegen Flash nicht um firmenpolitische, sondern um technologische Gründe handle. Dazu führt er sechs Gründe aus.
Jobs’ Grund Nummer 1: Flash ist nicht offen
Steve Jobs nennt in einem offenen Brief seine Gründe für das im iOS fehlende Flash-Plug-in (Bild 1)
Jobs wehrt sich gegen die Vorwürfe von Adobe, dass Apple seine technologischen Spezifikationen nicht veröffentlichen würde, Flash dagegen ein offenes System sei. Er schreibt, dass Flash hundertprozentig geschlossen sei, da Adobe die alleinige Autorität über künftige Erweiterungen und die Preisgestaltungen habe. Die mobilen Apple-Geräte würden dagegen auf offene Standards wie HTML5, CSS und Javascript setzen.
SWF vs. HTML5 Wenn man in Flash (Listing 1) eine Animation für die Wiedergabe im Flash Player abspeichern möchte, erhält man eine SWF-Datei. Bis 2008
Flash-Historie 1997: Macromedia bringt Flash auf den Markt und veröffentlicht noch im selben Jahr die zweite Version. ● 1998: Flash 3 erscheint. Neu sind Aktionen, die man Schlüsselbildern zuweisen kann. ● 1999: Actionscript wird in Macromedias Flash 4 umfangreich erweitert. ● 2000: Mit Flash 5 wird der Zugriff auf externe Datenquellen vereinfacht. ● 2002: Macromedia nennt die sechste Version Flash MX und integriert erstmals Video in den Flash Player. ● 2003: Mit Flash MX 2004 wird das objekt●
64
orientierte Actionscript 2 eingeführt. 2005: Adobe übernimmt Macromedia und integriert Flash Professional 8 in die Programmsammlung Studio 8. ● 2006: Zur Jahresmitte erscheint der Flash Player 9. ● 2007: Es erscheint Adobe Flash CS3, das direkte Importe aus anderen Adobe-Produkten wie etwa Photoshop zulässt. ● 2008: Mit dem Flash Player 10 erscheint auch Adobe Flash CS4. Außerdem werden die Spezifikationen von Flash offen gelegt. ● 2010: Die aktuelle Version Adobe Flash CS5 erscheint. ●
war das SWF-Dateiformat nicht offiziell offengelegt. Bis dahin war es für Suchmaschinen nicht möglich, Texte aus Flash-Inhalten auszulesen. Heute geht das, da die Spezifikationen bekannt sind. HTML5 ist die neuste Version der Metasprache, mit der der Aufbau von Webseiten definiert wird. Hier gibt es erstmals ein sogenanntes Canvas-Element (Listing 2), das per Javascript zur Laufzeit mit grafischen Elementen gefüllt werden kann. Außerdem bietet es eine einfache Möglichkeit, Videos in die Seite einzubinden.
Jobs’ Grund Nummer 2: Das „ganze Web“ Adobe vertritt laut Jobs den Standpunkt, dass die mobilen Apple-Geräte nicht „das ganze Web“ nutzen könnten, da 75 Prozent der Filme im Web über Flash bereitgestellt würden. Der Apple-Chef hält dagegen: „Ein Großteil der Videos sind auch im moderneren H.264-Format verfügbar.“ Jobs bestätigt, dass man mit seinen mobilen Geräten keine Flash-Spiele starten kann, verweist aber gleichzeitig auf den App Store, der über 50.000 Spiele bereithalte, von denen viele kostenlos seien.
Video im Web Videos kamen im Web erst so richtig in Mode, als Dienste wie Youtube Tausende Nutzer begeisterten. Bislang setzte der Branchenprimus auf Flash für die Wiedergabe der Filme. Seit einigen
mac-developer 4/2010
064_Flash_jp_ea.qxp
05.08.2010
16:43 Uhr
Seite 65
WebDev Flash-Perspektiven ●
Wochen werden die Videos bei Youtube aber auch direkt per H.264 ausgeliefert (Bild 2). Für Webentwickler gilt: Das Einbinden eines Videos ist mit HTML5 viel einfacher geworden. Das doppelte Übergeben aller Parameter per object und embed (Listing 3) für die verschiedenen Browsertypen entfällt (Listing 4). Außerdem kann man sich den umständlichen Video-Codierungsvorgang von Flash sparen.
Autor
Flash-Spiele und der App Store Spiele im Internet sind ebenfalls immer wichtiger geworden. Es gibt beispielsweise Millionen von Facebook-Nutzern, die ihre Zeit in das Flash-Spiel Farmville investieren. Wenn man ein solches Angebot betreibt und im Moment die iOS-User nicht ausschließen möchte, besteht die Möglichkeit, eine native App zu erstellen und diese im App Store anzubieten. Die FarmvilleEntwickler haben sich erst vor Kurzem zu diesem Schritt entschieden. Was Steve Jobs bei seinem Verweis auf den App Store nicht bedacht hat, ist der zusätzliche Aufwand, der durch die Umwandlung in eine native App entsteht. Adobe hatte da eine interessante Idee und wollte Flash-Entwicklern eine Export-Funktion an die Hand geben, mit der Flash-Inhalte in eine native iOS-App verpackt werden konnten, um sie so schnell und unkompliziert im App Store veröffentlichen zu können (Bild 3). Mit den jüngsten Änderungen der App-Store-Guidelines werden solche Flash-Apps allerdings vertraglich ausgeschlossen.
Wie gut ein Browser HTML5 unterstützt, verrät Html5test .com. Der Gewinner ist momentan Safari 5 (Bild 5)
Jobs’ Grund Nummer 4: Akkulaufzeit
Ingo Dellwig (36) ist seit 1997 Unternehmer im Web- und Software-Umfeld. Er hat viele Fachbücher zu Internet- und Programmierthemen geschrieben und ist seit der Einführung des iPhone SDKs begeisterter AppEntwickler. Klar, dass seine Firma web’n’apps heißt (www.webn-apps.de).
▲
Das vierte Thema ist mit dem dritten verwandt. Es geht um die Akkulaufzeit. Jobs schreibt, dass man mit einem iPhone zehn Stunden lang H.264-Videos abspielen könne, da hier Hardware zum Decodieren genutzt wird. Wenn man allerdings auf die Software-Variante per Flash setzen würde, halbiere sich die Wiedergabezeit. Sie können es selbst ausprobieren: Sie brauchen nur einen Computer mit CPU-Lüfter, einen Browser mit veraltetem Flash-Player, eine Webseite, die ein Flash-Video enthält und eine Minute Geduld. Selbst wenn die Lautsprecher abgeschaltet sind, macht sich Flash über stärkere
Jobs’ Grund Nummer 3: Zuverlässigkeit, Sicherheit und Leistung Im dritten Punkt führt Jobs die Zuverlässigkeit des Flash-Plug-ins an. Er schreibt, dass der Absturz eines Macs meist auf Flash zurückzuführen sei. Außerdem sei die Leistung nicht für mobile Geräte optimiert. Adobe arbeite hier nicht genug mit Apple zusammen und verschiebe Release-Termine immer weiter.
Listing 1: Ein Quadrat mit Actionscript var myRectangle:Shape = new Shape(); myRectangle.graphics.lineStyle(1,0x00000000); myRectangle.graphics.beginFill(0xff000000); myRectangle.graphics.drawRect(10, 10, 50, 50); myRectangle.graphics.endFill(); addChild(myRectangle);
Listing 2: Ein Quadrat mit HTML5 Listing 2.2: Ein Quadrat mit HTML 5 und JavaScript zeichnen 3 (JavaScript-Teil) function drawRectangle(){ var canvas = document.getElementById('quadrat'); if(canvas.getContext){ var context = canvas.getContext('2d'); context.fillStyle = "rgb(255, 0, 0)"; context.fillRect(0, 0, canvas.width, canvas.height); }
Kaum Punkte auf Html5test.com: IE 8 (Bild 6)
4/2010 www.mac-developer.de
65
064_Flash_jp_ea.qxp
05.08.2010
16:43 Uhr
Seite 66
WebDev Flash-Perspektiven ●
bel sind (zum Beispiel Rollover-Effekte). Wenn diese Inhalte also für die Bedienung per Touch ohnehin angepasst werden müssen, könnten Entwickler auch gleich auf die von Jobs bevorzugten offenen Standards wie HTML5 setzen.
Touch vs. Maus
Inzwischen kann man mit dem iPad Youtube-Videos auch im H.264-Format im Browser ansehen (Bild 2)
Lüftergeräusche bemerkbar. Für dieses Verhalten ist allerdings ein Detail wichtig: das Adjektiv „veraltet“. Die benötigte CPU-Leistung hat sich mit dem neusten Flash Player zumindest bei PCs deutlich verbessert. Die Mac-Gemeinde klagt aber immer noch über eine erhöhte Geräuschkulisse. Der Zusammenhang zwischen CPU-Last und Akkulaufzeit bei mobilen Geräten ist unbestritten. Da es aber keinen FlashPlayer auf iOS-Geräten gibt, kann man hier leider nicht selbst nachmessen und muss Apples Aussagen ungeprüft hinnehmen.
Jobs’ Grund Nummer 5: Touch Apples mobile Geräte setzen auf Touch-Displays. Jobs führt aus, dass Flash für die Bedienung per Maus konzipiert wurde. Viele der bestehenden Flash-Inhalte im Web würden auf Konzepte setzen, die nicht mit Touch kompati-
Dieser Punkt ist nicht zu unterschätzen. Geräte mit Touch-Bedienung und mausgesteuerte Geräte sind sehr unterschiedlich. Hier geht es nicht nur um Mauszeiger und Rollover-Effekte, sondern auch um Gesten und Multitouch-Ereignisse. Allerdings gibt es auch einige Konzepte, die sich gleichen. Sowohl der Klick als auch der Doppelklick und das Drag-and-Drop-Prinzip sind bei beiden Technologien vorhanden. Wer sich also auf diese minimalen Konzepte beschränkt, läuft nicht Gefahr, einen Gerätetyp auszuschließen. Der neue Flash Player 10.1 unterstützt übrigens Multitouch-Gesten und bietet Flash-Entwicklern mehr Möglichkeiten, auf die Bedürfnisse von Maus-Verweigerern einzugehen. Hier könnte Jobs seinen Standpunkt überdenken.
Jobs’ Grund Nummer 6: Cross Plattform Software Das letzte Thema, dem sich Jobs widmet, ist die Philosophie von Adobe, den Entwicklern Werkzeuge an die Hand geben zu wollen, mit denen man gleich für mehrere Plattformen entwickeln kann. Hier würde Adobe nur die Schnittmenge der Funktionen aller Geräte abdecken und neue Funktionen, die zum Beispiel dem iPhone hinzugefügt wurden, gar nicht oder erst sehr spät umsetzen. Jobs möchte für seine mobilen Geräte „die besten Apps“, und die könnten Programmierer mit Drittanbieter-Programmier-Umgebungen nicht erzeugen.
In Adobe Flash CS5 ist der Export als native iOS-App vorgesehen. Allerdings haben so erzeugte Apps keine Chance, im App Store angeboten zu werden (Bild 3)
66
mac-developer 4/2010
064_Flash_jp_ea.qxp
05.08.2010
16:43 Uhr
Seite 67
WebDev Flash-Perspektiven ●
Apps für mehrere Plattformen
Flash-Support
Adobe hat bekannt gegeben, dass bis Ende 2012 sehr viele mobile Plattformen Flash unterstützen können (siehe nebenstehenden Kasten). Die mobile Apple-Welt ist dabei ausgeschlossen. Adobe würde das gern ändern und schaltete nach Steve Jobs’ offenem Brief eine Anzeige mit einem „Liebesbeweis“ an Apple (Bild 4). Jobs’ Haltung gegenüber Cross-PlattformApps ist nicht gerade konsequent, denn es lassen sich auch mit anderen Werkzeugen Apps für verschiedene Plattformen erzeugen. Ein Beispiel ist PhoneGap (siehe mac-developer 2/2010, Seite 80). Mit diesem Framework lassen sich WebApps in native Apps umwandeln. Das funktioniert nicht nur für iOS, sondern auch für Android, Blackberry, Palm, Symbian und Windows Mobile.
iOS: nein (nicht geplant) Android: geplant ● Blackberry: geplant ● Symbian OS: geplant ● Palm WebOS: geplant ● Windows Phone 7: geplant ● Windows Mobile: geplant ● Mac OS: ja, Flash Player 10.1 ● Windows: ja, Flash Player 10.1 ● Linux: ja, Flash Player 10.1 ● ●
HTML5-Support Die HTML5-Unterstützung eines Browsers prüfen Sie mit http:// html5test.com (Bild 5 und 6). Hier werden 300 Punkte plus Bonuspunkte vergeben.
Jobs’ Schlussfolgerung Flash ist für Jobs eine veraltete Technologie, die noch auf Mäuse setzt und nicht ausreichend für mobile Geräte optimiert wurde. Die 200.000 Apps im App Store würden beweisen, dass Flash nicht notwendig sei. Ergo: Adobe solle sich mehr auf die Entwicklung von HTML5-Werkzeugen konzentrieren, anstatt Apple Vorwürfe zu machen.
Die Zukunft von Flash: 3D als Anreiz Adobe lässt sich dadurch aber nicht entmutigen, sondern setzt anscheinend auf eine Erweiterung der Flash-Funktionalität. Für die nächste MAXKonferenz wurde zum Beispiel eine Session mit dem Titel „Flash Player 3D Future“ angekündigt. Ob Flash allerdings je den Weg auf iOS-Geräte finden wird, ist ungewiss. Vielleicht ändert eine performantere, stabilere und sicherere Version des Flash Players doch noch die Meinung des Apple-Chefs.
Safari 5.0: 208+7 Google Chrome 5.0: 197+7 ● Mozilla Firefox 4.0 Beta: 189+9 ● Mobiler Safari (iOS 4.0.1): 185+7 ● Opera 10.6: 159+7 ● Mozilla Firefox 3.6: 139+4 ● Mobiler Safari (iOS 3.2.1): 127+7 ● Microsoft Internet Explorer 9.0 Preview: 84+1 ● Microsoft Internet Explorer 8.0: 27 ● ●
„Wir lieben Apple“: Adobe versucht durch diese Anzeige, doch in der mobilen Apple-Welt Fuß zu fassen (Bild 4) geht. Schließlich bleibt noch die Gruppe der „amüsierten Zuschauer“ zu erwähnen, die nichts beachten muss, sondern sich wahrscheinlich schon bald auf eine weitere Runde im Schlagabtausch zwischen Apple und Adobe freuen kann. [jp]
Listing 3: Flash-Video einbinden
Listing 4: HTML5-Video einbinden
67
068_Canvas_jp_ea.qxp
06.08.2010
10:12 Uhr
Seite 68
WebDev Animationen ohne Flash ●
Mit Canvas und SVG gegen Flash
HTML5 bewegt Mit dem Canvas-Element von HTML5 steht Entwicklern eine Alternative zu Flash zur Verfügung, bei der die Animationen direkt von der Browser-Engine erledigt werden. Alexander Ebner Auf einen Blick Wie Sie mit Canvas und SVG Animationen erstellen können, beschreibt mac-developer-Autor Alexander Ebner. Der vorliegende Artikel enthält Themen aus dem Buch „iPhone OS Webentwicklung – Professionelle Applikationen für Webkit-Browser“ von Patrick Lobacher. Die Beispiele aus diesem Artikel finden Sie vollständig im DownloadBereich unter www.mac-develo per.de.
E
ines der am häufigsten anzusehen lohnt. Animatioinstallierten Browsernen mit der Browser-Engine Plug-ins ist Flash. Adobe hat sind schon heute möglich. mit diesem Format einen Die Webkit-Engine des Quasi-Standard geschaffen Safari Browsers ist eine leisum Animationen auf Webtungsfähige, wenn nicht die seiten darzustellen. Aber so leistungsfähigste Browservielseitig Flash auch ist, es Engine, die zurzeit verfüghat es auch Schattenseiten: bar ist. Sie ist Open Source, Man benötigt ein Plug-in, wird aber von Apple aktiv um dieses proprietäre Forund stetig weiter entwickelt. mat abzuspielen. Und Flash Die Engine wird in zahlreihat einen Ressourcenhunger, Ein rotes und ein halbtransparentes Rechtchen anderen Browsern der sich deutlich bemerkbar eck, ein Rahmen und ein Rechteck, das ebenfalls eingesetzt. Dazu den Bereich löscht (Bild 1) macht. Eine CPU-Auslasgehören Googles Chrometung von 100 Prozent, nur Browser, Android, Nokia weil eine Webseite ein animiertes Flash-Banner Symbian OS (S60) und andere. Auch die Geckoenthält, kommt durchaus vor. Das ist auch einer Engine des Mozilla-Projekts beherrscht HTML5. der Gründe, warum Apple auf dem iPhone und Der Internet Explorer tut sich allerdings auch in dem iPad keine Flash-Unterstützung bietet. Mit der aktuellen Version 8 noch schwer damit und den Möglichkeiten des Canvas-Elements von unterstützt HTML5 nicht. Erst in der kommenHTML5 stehen Alternativen bereit, die es sich den Version 9 soll der IE auch HTML5 beherrschen. Damit fällt der IE vorerst für den Einsatz von Canvas aus.
Listing 1: HTML-Seite für die Beispiele
Canvas Beispiel <meta name="viewport" content="width=320; initial-scale=1.0; 3 user-scalable=0;"> <script type="text/javascript"> function draw() { var canvas = document.getElementById(’myCanvas’); if (canvas.getContext){ var ctx = canvas.getContext(„2d“); // hier kommt der Javascript-Code zum Zeichnen. } }
Animationen mit Canvas Canvas wurde von Apple im Rahmen der Webkit-Entwicklung eingeführt und später von der Web Hypertext Application Technology Working Group (WHATWG) standardisiert. Damit ist es Bestandteil von HTML5. Mit Canvas erstellen Entwickler und Webdesigner VektorGrafiken und Animationen. Das Canvas-Element lässt sich über CSS genauso stylen wie das Image-Element. Wenn Sie ein Canvas-Element einsetzen, sollten Sie es immer mit einem Identifier (ID) versehen, damit Sie über Javascript leichter darauf zugreifen können.
<style type="text/css"> canvas {
border: 1px solid black; }
68
In der ursprünglichen Spezifikation war das Schluss-Tag nicht vorgesehen. Sie sollten es dennoch verwenden, da die Gecko-Engine im Gegensatz zur Webkit-Engine das Schluss-Tag zwingend benötigt. Webkit wird das SchlussTag einfach ignorieren. Wie bereits erwähnt kann der Internet Explorer mit dem Canvas-Tag nichts anfangen. Auch
mac-developer 4/2010
068_Canvas_jp_ea.qxp
06.08.2010
10:12 Uhr
Seite 69
WebDev Animationen ohne Flash ●
einige andere Browser und ältere Versionen von Firefox und Co. können Probleme bekommen. Wenn Sie innerhalb des Canvas-Tags einen Inhalt hinzufügen, wird bei Browsern, die Canvas nicht darstellen können, stattdessen folgender Inhalt angezeigt.
Rechteck zu zeichnen, können Sie folgende Befehle einsetzen:
strokeRect(x,y,,)
var canvas = document.getElementById3 ('myCanvas'); var ctx = canvas.getContext("2d");
fillRect(x,y,,) zeichnet ein gefülltes Rechteck.
zeichnet die Kanten eines Rechtecks. clearRect(x,y,,) löscht das Rechteck und macht alles darunter transparent. Um einem Rechteck eine Farbe zuzuweisen, übergeben Sie diese vor dem Zeichnen-Befehl mit der Funktion fillStyle() als RGB- oder RGBAWert. Der RGBA-Wert wurde mit CSS3 eingeführt und definiert zusätzlich den Alphakanal, der die Transparenz des Objekts beschreibt. Das Beispiel in Listing 2 zeichnet alle Arten eines Rechtecks mit verschiedenen Farben und Alphakanälen. Das Ergebnis sehen Sie in Bild 1. Eine weitere Grundform sind Kreise, die mit der Funktion arc() gezeichnet werden. Die Syntax für Arc sieht so: arc(x, y, Radius, Startwinkel, 3 Endwinkel, Uhrzeigersinn)
Alexander Ebner ist Typo3-Entwickler beim Reiseveranstalter FTI Frosch Touristik und beschäftigt sich neben den Typo3-Portalen auch mit dem mobilen Internet. Er veröffentlicht regelmäßig Fachartikel und Bücher zum Thema Typo3 und iPhone-Webentwicklung. Darüber hinaus hält er über diese Themen auch Vorträge auf Fachmessen.
▲
Ein Browser, der Canvas darstellen kann, wird diesen Fallback-Inhalt ignorieren. Browser, die Canvas nicht kennen, machen das, was Browser in der Regel tun, wenn sie ein Tag nicht kennen: Sie verneinen seine bloße Existenz und zeigen einfach den Inhalt dazwischen an. Ein Canvasfähiger Browser zeichnet also ein Feld in den angegebenen Maßen. Dieses Feld zeigt außer einer weißen Fläche noch nichts an. Um die Fläche zu füllen, wird Javascript eingesetzt. Aus diesem Grund wird dem Canvas-Feld ein ID mitgegeben, da es dann einfach über getElementById() angesprochen werden kann. Die Verwendung von Javascript ermöglicht Ihnen den Einsatz vorhandener Javascript-Methoden und – viel wichtiger – den Zugriff auf das DOM und globale Variablen. Um die Beispiele durcharbeiten, sollten Sie sich einen kleinen Spielplatz einrichten, also eine einfache HTML-Seite mit einem CanvasElement auf der Seite (Listing 1). Diese sehr einfache Seite reicht bereits aus. Es befindet sich das Canvas-Element darin und ein einfaches Stylesheet, das dem Element einen schwarzen Rahmen zuweist. Als Erstes wird dem Canvas-Element ein Kontext zugewiesen. Zurzeit existiert nur der Wert 2d, in Zukunft werden aber weitere Werte wie 3d hinzu kommen. Die Zuweisung des Kontexts erfolgt mit den Zeilen
Autor
Listing 2: Rechtecke zeichnen <script type="text/javascript"> function draw(){ var canvas = document.getElementById(’myCanvas’); if 3 (canvas.getContext){ var ctx = canvas.getContext("2d"); // Erstes Rechteck mit roter Farbe ctx.fillStyle = "rgb(200,0,0)"; ctx.fillRect (10, 10, 100, 100); //Zweites Rechteck mit blauer Farbe und 50% Transparenz ctx.fillStyle = "rgba(0, 0, 200, 0.5)"; ctx.fillRect (50, 50, 100, 100); // Drittes Rechteck nur als Rand ctx.strokeRect (100, 100, 100, 100); // Viertes Rechteck löscht aus allen vorherigen Rechtecken einen Teil 3 heraus ctx.clearRect(90, 90, 70, 70); } }
Es ist auch möglich, den Kontext auf folgende Weise zuzuweisen: var ctx = document.getElementById3 ("myCanvas").getContext("2d")
Listing 3: Ein einfacher Kreis ctx.beginPath();
Jetzt ist das Canvas-Element bereit. Um auf dem Element zu zeichnen, können Sie aus mehreren Grundformen wählen, die durch einfache Befehle aufgerufen werden. Um beispielsweise ein
Die Punkte x und y bezeichnen die Koordinaten des Kreismittelpunkts, der dritte Wert den Radius des Kreises. Der Start- und der Endwinkel wird an der x-Achse gemessen und der boolesche Wert Uhrzeigersinn gibt an, in welche Richtung der Kreis gezeichnet wird. Ein normaler Kreis wird mit Listing 3 erstellt. Es wird also ein neuer Pfad begonnen, der Kreis definiert, gefüllt und der Pfad beendet. Auf diese Weise lässt sich beispielsweise auch ein Smilie (Listing 4) zeichnen.
beginnt einen neuen Pfad innerhalb des Canvas mit Startpunkt (0,0) schließt den Pfad und verbindet den End- mit dem Startpunkt füllt das Innere des Pfades aus und verbindet den End- mit dem Startpunkt; closePath() kann daher entfallen. zieht einen Rahmen um den Pfad versetzt den Startpunkt zeichnet eine Linie vom Startpunkt zum angegebenen Endpunkt zeichnet Kreise zeichnet einen Kreis an einem Subpath mit Hilfe eines Radius und zwei Tangenten zeichnet Bezierkurven zeichnet quadratische Kurven zeichnet ein Rechteck als Pfad dreht das Koordinatensystem um den angegebenen Wert um den Ursprungspunkt verschiebt den Ursprungswert des Koordinatensystems speichert den Zustand des Canvas stellt den gespeicherten Zustand wieder her erstellt einen linearen Verlauf erstellt einen radialen Verlauf
Als Ergebnis grinst uns ein netter gelber Smilie entgegen (Bild 2). Somit wäre es bereits möglich, Rechtecke und Kreise zu zeichnen. Komplexe Formen werden als Pfade gezeichnet. Dazu steht eine Reihe von Befehlen zur Verfügung. In Tabelle 1 finden Sie alle Befehle, die Sie einsetzen können.
Linien und Kurven Eine einfache Linie zu zeichnen ist sehr einfach. Sie benötigen dazu den Befehl lineTo(), der als Parameter die x- und y-Koordinate erwartet. Diese Koordinaten bestimmen den Endpunkt. Der Startpunkt ist die momentane Position. Möchten Sie den Startpunkt ändern, verschieben Sie die aktuelle Position mit moveTo() an die gewünschte Koordinate. Auch dieser Befehl erwartet dazu die x- und y-Koordinate. Das Beispiel in Listing 5 zeichnet eine Raute (Bild 3). Organischere Formen werden mit der Bezierkurve möglich. Allerdings ist ein gutes Vorstellungsvermögen nötig, um aus den sechs erforderlichen Werten die gewünschte Kurve zu erzeugen. Ausgehend von der aktuellen Position wird eine Line zum Endpunkt auf der x- und yKoordinate gezeichnet. Die übrigen zwei Koordinaten bestimmen die beiden Kontrollpunkte. Mit Canvas können auch Pixel-Bilder verarbeitet werden. Dadurch werden interessante Effekte möglich. Dazu muss ein neuer Kontext für das Bild erstellt werden. Dieser kann dann gezeichnet werden. Der Pfad zu dem Bild kann relativ oder absolut sein. Der Befehl drawImage() hat noch weitere Parameter, mit denen auch Teilausschnitte des Bildes erzeugt werden können. Die Syntax sieht so aus: drawImage(imgref, sourceX, sourceY, sourceW, sourceH, destX, destY, destW, destH)
mac-developer 4/2010
068_Canvas_jp_ea.qxp
06.08.2010
10:12 Uhr
Seite 71
WebDev Animationen ohne Flash ●
Listing 7: Bilder transformieren var img = new Image();
ctx.translate(150,150);
img.src = "desk.jpg";
var img = new Image();
img.onload = function() {
img.src = "desk.jpg";
ctx.drawImage(img,0,0);
img.onload = function() {
} ctx.translate(100,-10); ctx.rotate(120);
Das Bild wird gedreht (Bild 4) Die Parameter sind so zu verstehen: Sie beginnen mit der Bildressource, dann folgen die linke obere Ecke des gewünschten Ausschnitts (sourceX und sourceY), die Breite und Höhe des Ausschnitts (sourceW und sourceH), die linke obere Ecke, an der im Canvas der Ausschnitt platziert werden soll (destX und destY) sowie die Breite und Höhe, in der der Ausschnitt angezeigt werden soll (destw und destH). Wie bereits angesprochen, können die Bilder auch mit Canvas-Funktionen beeinflusst werden. Im Beispiel in Listing 7 verschieben Sie den Nullpunkt auf die Position (100,-10) und drehen das Bild um 60 Grad (Bild 4). So lassen sich schöne Effekte erzielen. Beispielsweise lässt sich das Bild mehrfach anordnen. Ein einfaches Beispiel in Listing 8 zeichnet
Listing 9: Eine einfache Animation <script type="text/javascript"> var canvas, ctx; var img = new Image(); var i=1; function draw() { canvas = document.getElementById( "myCanvas"); ctx = canvas.getContext("2d"); ctx.translate(150,150); img.src = "desk.jpg"; setInterval(doit,100); } function doit() { ctx.save(); ctx.translate(10,20); ctx.rotate(10*i); ctx.drawImage(img,40,40,30,30); ctx.restore(); i++; }
4/2010 www.mac-developer.de
Listing 8: Anordnen
ctx.drawImage(img,10,3 3 10,30,30); }
das Bild stark verkleinert und dann noch 50-mal drumherum. Das Ergebnis (Bild 5) sieht ein wenig wie eine Blüte aus. Dazu wird mittels save() der Zustand des Canvas gespeichert, der Nullpunkt durch translate() verschoben und das Koordinatensystem mit rotate() gedreht. Nun wird das Bild wieder gezeichnet. Da der Nullpunkt verschoben wurde, wird das Bild immer an den selben Koordinaten gezeichnet. Danach wird der ursprüngliche Zustand wieder hergestellt. Um den Aufbau zu animieren, machen Sie es sich zunutze, dass die Funktionen, die die Zeichnungen durchführen, auch als Handler eines Event-Triggers arbeiten können. Ein Event-Trigger könnte daher setIntervall() sein, der zu einem festgesetzten Intervall eine Funktion aufruft. Im Listing 9 ist die obere Funktion so umgebaut, dass eine Animation daraus wird. Es ist klar, dass komplexere Bilder und Animationen eine aufwendigere Programmierung erfordern. Ein Nachteil gegenüber Flash ist hier eine fehlende Entwicklungsumgebung, die eine einfache Erstellung der Animationen ermöglicht. Auch die (noch) fehlende Unterstützung des Internet Explorers verhindert im Moment noch den breiten Einsatz dieser Technologie.
for (var i = 0; i< 50; 3 i++) { ctx.save(); ctx.translate(20,20); ctx.rotate(10*i); ctx.drawImage(img,40,3 3 40,30,30); ctx.restore(); }
Fazit Der Einsatz von Canvas zeigt, wohin die Entwicklung geht. Wenn Animationen direkt von der Browser-Engine ausgeführt werden können, kann man sich zukünftig die Installation von proprietären Fremd-Plug-ins sparen. Vor allem die Möglichkeiten, Animationen und Bilder auf der Grundlage mathematischer oder logischer Operationen zu erstellen, bieten einige faszinierende Lösungen. Die im Gegensatz zu Flash schwierigere Zugänglichkeit, fehlende IDEs und nicht zuletzt die fehlende Unterstützung im Internet Explorer verhindern im Moment noch die weitere Verbreitung. Dennoch wird der Einsatz von Canvas in Zukunft zunehmen. Gerade bei Webseiten für das iPhone kann mit Canvas das fehlende Flash-Plug-in sehr gut ersetzt werden. Um einen Überblick zu bekommen, was mit Canvas und Javascript möglich ist, lohnt sich ein Blick auf die Seite www.hongkiat.com/blog/ 48-excellent-html5-demos. Die dort vorgestellten Demos stehen Flash in nichts nach. [jp]
Das Bild wird vervielfacht und im Kreis angeordnet (Bild 5)
71
072_iPadWeb_jp_ea.qxd
06.08.2010
10:43 Uhr
Seite 72
WebDev Webseiten für iPad und iPhone anpassen ●
Webentwicklung für iPad und iPhone 4
Kosmetik fürs Web Obwohl die Website-Darstellung beim iPhone 4 mit der der Vorgänger kompatibel ist und das iPad sowieso dazu taugt, normale Webseiten anzuzeigen, sind kleine Korrekturen empfehlenswert. Alexander Ebner Auf einen Blick
ein anderes Mobiltelefon hat dem mobilen Web einen solchen Schub gegeben wie Apples iPhone. Vor wenigen Jahren war das mobile Websurfen noch eine Randerscheinung. Zu kompliziert, zu teuer und zu umständlich war das Navigieren durch die virtuelle Welt des Netzes. Erst das iPhone brachte durch seine durchdachte Bedienung in Verbindung mit einem leistungsfähigen mobilen Browser, der diesen Namen auch verdiente, die Menschen dazu, das Web auch unterwegs zu erkunden. Für die Webentwickler war das iPhone ein Segen. Sie konnten mit wenig Aufwand jede Webseite zu einem mobilen Surf-Vergnügen machen und Webapplikationen entwickeln, bei denen sie sich um Inkompatibilitäten keine großen Sorgen machen mussten. Die Hardware war im Grunde bei allen Geräten gleich. Doch mit der Einführung von iPad und iPhone 4 gehen ein paar Änderungen einher. Das iPhone Classic hatte dieselbe Display-Größe und -Auflösung wie das iPhone 3G und das iPhone 3GS. Somit konnte man fix positionieren und auch innerhalb von CSS-Dokumenten Conditions einsetzen, die auf die Display-Größe prüfen. Jetzt ist das iPhone 4 erschienen und ändert alles. Wieder einmal. Nun, zumindest die Display-Größe. Und auch das iPad kommt mit einer für die Größe des Geräts angepassten Auflösung. Daher muss hier nachgebessert werden. Wenn Sie sich den User-Agent-String beim iPhone 4 genauer ansehen, stellen Sie fest, dass keine genauen Angaben über den HardwareTyp gemacht werden. Foto: Apple, iFixit
Wie Sie Webseiten optimal an das iPad und das iPhone 4 anpassen, beschreibt mac-developer-Autor Alexander Ebner. Der Artikel behandelt Themen aus dem Buch „iPhone OS Webentwicklung – Professionelle Applikationen für Webkit-Browser“ von Patrick Lobacher.
K
Mozilla/5.0 (iPhone; U; CPU iPhone OS 3 4_0 like Mac OS X; en-us) AppleWebKit/3 532.9 (KHTML, like Gecko) Version/3 4.0.5 Mobile/8A293 Safari/6531.22.7 Im Vergleich der User-Agent-String des iPads:
Tabelle 1: Display-Auflösungen iPhone classic, 3G, 3GS 480 x 320 Pixel bei 163 ppi, 3,5 Zoll
72
iPhone 4 960 x 640 Pixel bei 326 ppi, 3,5 Zoll
iPad 1024 x 768 Pixel bei 132 ppi, 9,7 Zoll
Das 9,7-Zoll-Display des iPads stellt normale Webseiten dar (Bild 1) Mozilla/5.0 (iPad; U; CPU OS 3_2 like 3 Mac OS X; en-us) AppleWebKit/531.21.3 10 (KHTML, like Gecko) Version/4.0.4 3 Mobile/7B334b Safari/531.21.10 Natürlich fallen die unterschiedlichen Versionsnummern der Webkit-Engine auf. Das iPad arbeitet im Gegensatz zum iPhone 4 noch mit der iOS-Version 3.2. Die Version 4.0 für das iPad wird erst im Herbst erscheinen. Der einzige wirklich signifikante Unterschied liegt aber tatsächlich bei der Gerätebezeichnung, die der Hardware entspricht. Da aber sowohl bei iPhone 4, iPhone 3G(S) und iPhone classic jeweils nur „iPhone“ als Hardware-Kennung übertragen wird, können Sie die Geräte nur vom iPad, aber nicht untereinander unterscheiden. Bisher war das kein Problem, da die Hardware-Unterschiede für die mobilen Webseiten nicht relevant waren. Mit der höheren Auflösung des Retina-Displays jedoch ändert sich dieser Sachverhalt. Unglaubliche 960 x 640 Pixel bei 326 ppi bringt der Zulieferer LG Display auf 3,5 Zoll unter. Die Tabelle 1 zeigt die verschiedenen Display-Auflösungen im Überblick. Nun stellt sich die Frage, inwieweit Anpassungen überhaupt nötig sind. Für das iPad (Bild 1) muss keine mobile Webseite ausgeliefert werden, da es aufgrund seiner Display-Größe von 9,7 Zoll bestens dafür gerüstet ist, normale Webseiten darzustellen. Das iPhone 4 hat auch we-
mac-developer 4/2010
072_iPadWeb_jp_ea.qxd
06.08.2010
10:43 Uhr
Seite 73
WebDev Webseiten für iPad und iPhone anpassen ●
nig Probleme damit, Webseiten komplett darzustellen, aufgrund der Display-Größe von 3,5 Zoll ist das Lesen und Navigieren darin allerdings ungleich schwerer. Hier ergibt es also aus ergonomischer Sicht noch Sinn, eine eigene mobile Variante auszuliefern. Die existierenden Webseiten, die für die vorherigen iPhones erstellt wurden, können auch vom iPhone 4 problemlos dargestellt werden, da der Viewport unabhängig von der Hardware ist. Wurde eine Webseite mit einer Breite von 320 Pixeln entworfen und der Viewport auf 320 Pixel gesetzt, kann auch das iPhone 4 diese Webseite ganz normal darstellen. Der Inhalt wird vom Browser auf die physikalischen 640 Pixel Breite skaliert. Warum dann anpassen? Der wichtigste Grund ist wohl, dass der Besucher einer Webseite immer die optimale Surferfahrung machen sollte, denn dann wird er auch eher zu einer Seite zurückkehren. Natürlich stellt das iPhone 4 alle Grafiken genauso gut dar wie die vorherigen Generationen – es kann das aber besser. Nutzen Sie die Fähigkeiten des Geräts, um noch brillantere Grafiken darzustellen (Bild 2). Gerade Produktbilder oder Bildergalerien profitieren davon. Um die höhere Auflösung des iPhone 4 zu unterstützen, sind einige Änderungen nötig.
Der Viewport Zuerst muss der Viewport neu gesetzt werden. Er sollte nicht mehr fest auf 320 Pixel gesetzt sein, da der Content auf dem iPad mit 1024 Pixeln Breite dargestellt werden soll. Dazu setzen Sie den Viewport einfach auf eine Konstante, die Ihnen die Display-Größe liefert.
aufgerufen. Um einem iPhone ein eigenes Stylesheet zu geben, wurde häufig die Abfrage nach dem Wert max-device-width verwendet. Allerdings achtet das iPhone 4 nicht darauf und verwendet das Stylesheet auch, obwohl das Display eine höhere Breite als 480 Pixel aufweist. Das iPhone 4 handelt hier also genauso wie die älteren Modelle. Würde es auf die Condition vermeintlich richtig reagieren und das Stylesheet nicht verwenden, würde ein Großteil der iPhone-optimierten Webseiten nicht mehr funktionieren. Stattdessen kann man aber den Wert -webkit-min-device-pixel-ratio dafür verwenden, der beim iPhone 4 bei 2 liegt. Der entsprechende Befehl sieht dann so aus: Alternativ können Sie nur eine CSS-Datei ausliefern und innerhalb dieser die Überprüfung durchführen. @media screen and 3 (-webkit-min-device-pixel-ratio:2) { .content {background: url3 ('images/bg640.png') no-repeat;} }
Das Webclip-Icon <meta name="viewport" 3 content="width=320" /> Stattdessen wird ein Wert verwendet, der die Display-Breite vorhält: <meta name="viewport" 3 content="width=device-width" /> Die Konstante device-width enthält die Breite des physikalischen Displays. Für das iPhone 4 kann der Wert content="width=640" eingestellt werden. Ältere iPhones werden die Webseite wieder auf 320 Pixel herunterskalieren. Allerdings wird die Webseite noch immer mit 320 Pixeln Breite ausgeliefert. Auch Bilder sind nur 320 Pixel breit. Da die Vermaßung grundsätzlich in CSS erfolgen soll, benötigen Sie also ein Stylesheet für das iPhone 4. Grundsätzlich könnten Sie einfach einen Container mit der Breite 100% einsetzen und alle Positionierungen relativ dazu vornehmen, aber die Bilder wären immer noch zu klein. Diese werden über das zweite Stylesheet
4/2010 www.mac-developer.de
Das Webclip-Icon wird verwendet, wenn ein Bookmark zu einer Webseite auf dem Homescreen abgelegt wird. Ist kein Webclip-Icon hinterlegt, wird stattdessen ein kleiner Screenshot der Webseite verwendet. Natürlich funktionieren die bisherigen Webclip-Icons auch mit dem iPhone 4, sie sehen aber wegen der geringeren Auflösung nicht so schön aus. Daher sollten Sie stattdessen das Icon in der Größe 114 x 114 Pixel hinterlegen statt als 57 x 57 Pixel. Ältere iPhones werden das Icon einfach herunterskalieren. Das Webclip-Icon binden Sie wie gehabt über folgende Zeile im Head-Bereich der Seite ein:
Das hochauflösende RetinaDisplay verlangt nach Grafik-Upgrades (Bild 2)
Werden runde Kanten und Glanz-Effekt nicht erwünscht, verwenden Sie statt apple-touch-icon einfach apple-touch-icon-precomposed. Damit wird die automatische Nachbearbeitung des Icons deaktiviert. [jp]
73
074_Videoplayer_jp_ea.qxp
06.08.2010
11:46 Uhr
Seite 74
WebDev Interaktiver Videoplayer ●
HTML5 in der Praxis: Das video-Tag
Videoplayer mit Extras Gegenüber Flash bietet HTML5 bereits heute einige Vorzüge, die in einem kleinen Workshop vorgestellt werden. Sie bauen hier einen interaktiven Videoplayer mit nützlichen Zusatzfunktionen. Tilman Hampl Auf einen Blick Mit dem HTML5-Tag video zeigen Entwickler Videos direkt im Browser an. Wie Sie einen HTML5-Videoplayer einbinden und diesen mit eigenen Kontrollen und Sprungmarken anpassen, erläutert mac-developerAutor Tilman Hampl.
Videoplayer mit grafisch ansprechenden Kontrollen (Bild 1)
S
tellen Sie sich vor, Sie haben ein Video, vielleicht zwölf Minuten lang und beim Konzert Ihrer Lieblingsband mit dem iPhone aufgenommen. Dieses Video soll auf Ihr Blog. Dazu kommt ein Artikel, in dem Sie auf die drei Highlights des Konzerts, die natürlich in Ihrem Video perfekt eingefangen wurden, hinweisen. Wäre es nicht toll, wenn Sie mit Hyperlinks in Ihrem Blogbeitrag direkt auf die Highlights im Video verlinken könnten? Nun, nach dem Lesen dieses kleinen Tutorials werden Sie das können. Ich prophezeie einmal, angesichts der drastischen Verbreitung von Videos im Web werden Sie dem Betrachter immer häufiger die Auswahl lassen müssen, ob er der Film ganz oder nur zum Teil ansehen möchte. Direkt-Links zu bestimmten Abschnitten erhöhen die Chance, dass Ihr Video beachtet wird, ungemein. Nehmen Sie ein Interview mit einem Politiker, in dem dieser zu zehn oder mehr Fragen Stellung nimmt. Nur sein persönlicher Berater oder karrierebewusste Parteifreunde werden alle zehn Antworten sehen wollen, Sie interessiert vielleicht nur eine. Da wäre es doch sehr angenehm, wenn die Fragen in Form von Buttons oder ganz einfach Hyperlinks auf der gleichen Seite platziert wären. Sie werden sich in diesem Tutorial mit einem ähnlichen Beispiel auseinandersetzen. Hier geht es um die Aufnahme eines Konzerts. Sie programmieren Buttons, mit denen man direkt an die Anfänge bestimmter Songs springen kann.
Tools und Skills Wenn Sie diesen Workshop nachvollziehen wollen, sollten Sie einen Texteditor wie BBedit oder TextMate am Start haben und ein MPEG4-Video, komprimiert mit H.264 und Theora, bereitlegen. Der Kasten auf Seite 76 geht auch auf die Vorbereitung und Kompression ein. Hierzu benötigen Sie auf einem Mac das Programm Handbrake und das Firefox-Plug-in Firefogg.
Interaktive Filme, Quicktime und Flash Wenn man vor ein paar Jahren (in der Zeitrechnung des Internets reden wir von „vor der letzten Eiszeit“) einen Film mit verschiedenen Sprungmarken machen wollte, einen interaktiven Film also, bot sich außer dem damals noch sehr komplexen Authoring eines DVD-Menüs die Software Quicktime von Apple an. Damals brachten viele Entwickler Tools heraus, die eine Oberfläche für die bis zur letzten Version der Software unterlegte Skriptsprache boten. Mit zunehmender Geschwindigkeit des Netzes kamen dann auch neue Technologien an den Start. Man kann sicher sagen, dass im Lauf der Zeit Flash das Rennen gemacht hat. Es ist nicht nur die Grundlage für interaktive Websites mit animierten Menüs, sondern hat sich mit dem Einsatz als Videotechnologie unter anderem beim Videoportal Youtube auch schnell zum De-facto-Standard bei der Darstellung von Bewegtbildern im Web etabliert. Die bei Flash eingesetzte mächtige Skriptsprache vereinfacht es Entwicklern, Videos mit Tasten interaktiv zu machen. Dies reicht bis zu komplexen mathematischen Berechnungen, die zur
Nützliche Links Laden Sie sich das hier verwendete Video zum Experimentieren herunter: http://files.qtvr.com/tears.zip Verschiedene Kompressions-Tools gibt es hier: Theora – Online Encoder http://firefogg.org Theora – Offline Encoder (Command-Line) http://v2v.cc/~j/ffmpeg2theora/download.html H.264 – Handbrake: http://handbrake.fr/downloads.php
74
mac-developer 4/2010
074_Videoplayer_jp_ea.qxp
06.08.2010
11:46 Uhr
Seite 75
WebDev Interaktiver Videoplayer ●
Darstellung eines 360-Grad-Panoramabilds und -films reichen. An der verhältnismäßig einfachen Art und Weise, wie die Programmierung innerhalb der aktuellen Version von Flash vonstatten geht, muss sich heute jedes andere Tool messen lassen. Ein Beispiel: video.currentTime = xy
Jetzt kommt HTML5
Autor
Tilman Hampl bringt die Objekte von Verlagen auf den Markt der mobilen Applikationen. Der 47-jährige Journalist konzipiert seit dem Jahr 2000 Applikationen für mobile Geräte.
▲
Wichtig bei der Bedienung von Videoplayern ist immer die Ermittlung der Filmlänge, der aktuellen Position im Film und das Bewegen des Abspielkopfs zu einer bestimmten Position. Diese Idee liegt auch dem hier gezeigten Beispiel zugrunde. Und HTML? Natürlich lässt sich diese sehr einfache Art der Interaktivität auch mit gutem, alten und kompatiblem HTML darstellen. Es ist hierzu aber nötig, den Film in mehrere Abschnitte zu teilen. Die Ladezeiten nach dem Aufruf des Films sind lästig, aber erträglich. Das Videomenü kann mit einer Imagemap ein beliebiges Aussehen haben. Elegant ist diese Methode nicht, wer aber nur einmal ein Stück Video ins Netz stellen und dem Nutzer verschiedene Einstiege erlauben will, der kann diese Methode, die grundsätzlich auch auf iPhone und Co. funktioniert, wählen.
unterschiedlichen Kompressionsformaten bereitzulegen (Listing 2). Live sieht das Ganze dann aus wie unter http://qtvr.com/workspace/mdevelo per/v2.html gezeigt. Gratulation, Sie haben Ihren ersten Videoplayer in HTML5 erstellt. Wenn man sieht, wie schlank und einfach das ist, kann man gut verstehen, wieso Youtube und Co. so eifrig dabei sind, alles von Flash auf diese neue Technologie umzustellen. Im Beispiel fehlen Ihnen noch einige sehr wichtige Elemente, etwa ein auf allen Browsern einheitlich aussehender Controller, Lade- und Abspielanzeige, Lautstärke, und das Ganze in ansprechender Optik (Bild 1). Diese Elemente borgen Sie sich aus einem sehr hilfreichen Tutorial im Web: http://blog.steveheffernan.com/2010/ 04/how-to-build-an-html5-video-player.