Vorgestern wars soweit. Ich habe in Vertretung des AntMe! Teams (Dominik, Patric, Tom) eine ordentliche Firma gegründet – wir sind jetzt also offiziell eine GmbH in Gründung. Und ab jetzt brauche ich eure Hilfe! Doch zuvor erstmal mein Masterplan:

Wie alles begann…

Wer das Projekt schon etwas länger verfolgt, der weiß, dass wir, Wolfgang und ich, bereits 2006 die allererste Version für die Games Convention in Leipzig zusammen geklöppelt hatten. Es ging uns damals darum, die technischen Hintergründe hinter Spielen leichter verständlich zu machen. Auf einer Spiele-Messe ergibt der spielerische Ansatz deswegen besonders viel Sinn.

Der Erfolg vor Ort hat uns damals aber wirklich überwältigt. Ich erinnere mich an einige Besucher, die quasi von der Security vom Stand entfernt werden mussten, weil sie sich in der Aufgabenstellung des Spiels verbissen hatten und nicht mehr locker lassen wollten. Aus heutiger Sicht weiß ich, dass das mit Abstand das Schönste ist, was einem Spieleentwickler passieren kann.

CeBIT TeamDoch mit der Games Convention vor 8 Jahren sollte nicht Schluss sein. Dank einer frühen Kooperation mit Microsoft (Danke an Uwe Baumann, der das Potential des Spiels sehr schnell erkannt hatte) bekamen wir die Chance auf vielen weiteren Messen (darunter viele Games Conventions, CeBITs, unzählige Girlsdays und Thyssenkrupp IdeenParks,…) auf denen sich dieses Schauspiel der Begeisterung einfach immer und immer wieder wiederholt hat. Leuchtende Augen überall.

So kamen dann nach und nach auch weitere Helfer mit ins Team, die vollkommen ehrenamtlich viel Herzblut investiert haben, um das Projekt weiter zu bringen. Leute wie Maike Reiner und Sebastian Loers, sowie unsere akademischen Freunde Christine Helfer, Matthias Utesch und Andreas Judt haben enorm viel bewegt.

Gerade wenn viel Ehrenamt im Spiel ist, stellt man sich früher oder später die Frage, ob es eine Möglichkeit gibt, Helfer in irgend einer Form zu entschädigen – und seien es nur Reisekosten. Leider haben uns da ein paar Gespräche mit Gründerberatern ernüchtert. Mit unseren idealistischen Werten lies sich kein Business Modell finden, das uns finanzieren könnte und so verliefen sich unsere Aktivitäten so langsam, da sich früher oder später jeder in einem vereinnehmenden Arbeitsverhältnis wieder fand.

Warum der neue Versuch?

Die vergangenen 2 Jahre habe mir ein paar interessante Erkenntnisse verschafft, die mich jetzt ein weiteres mal viel Geld und Zeit in das Projekt investieren lassen. Ihr solltet wissen, dass wir, auch wenn wir nicht mehr aktiv am Projekt gearbeitet haben, niemals die Website offline genommen haben. Die Software – wenn auch inzwischen 6 Jahre alt – war und ist weiterhin verfügbar.

Unsere Besucher- und Download-Zahlen sind (trotz aktuell furchtbarer Website – sorry dafür, ich arbeite dran) weitestgehend stabil. Ebenso erreichen mich regelmäßig Nachrichten von Wettbewerben und aktivem Einsatz an einigen großen deutschen Hochschulen. Die Ameisen finden also weiterhin Einsatz an wichtigen Bildungseinrichtungen, obwohl die grafische Ausführung durchaus etwas Tuning vertragen könnte – auch daran arbeite ich.

Hinzu kommt, dass ich bei nahezu jedem Vortrag, den ich in den vergangenen 2-3 Jahren gehalten habe, nach meiner „Ich habe AntMe! gemacht“-Slide immer ein paar Leute aufhüpfen und schreihen „Wegen DEINEM SPIEL hab ich Informatik studiert!“. Ok. Es lief nicht ganz so dramatisch ab, aber den Satz habe ich inzwischen tatsächlich schon oft gehört. Und ich bilde mir ein, dass es bei den Absolventen-Zahlen der MINT-Fächer in den Jahren 2011 und 2012 (also etwa 8 Semester nach unserer aktiven Phase) einen erkennbaren Zuwachs gibt – ich nenne ihn liebevoll den AntMe!-Peak ;)

AntMe! bleibt relevant!

Mein Glaube an das Produkt AntMe! gründet aber weniger in der Nostalgie der Geschehnisse, als vielmehr in der weiterhin brisanten Nachwuchs-Situation der ganzen naturwissenschaftlichen Disziplinen. Jedem ist inzwischen klar, dass IT so ziehmlich in jedem einzelnen (Industrie-)Zweig unserer Gesellschaft nicht mehr wegzudenken ist. Trotzdem ist und bleibt der Informatiker oder Programmierer eher unattraktiv für einen Großteil der Menschheit. Dementsprechend flach ist die Kurve der nachwachsenden Absolventen in diesem Bereich – ich selbst traue mich beim Kennenlernen von Leuten ja auch nicht direkt zu sagen, dass ich zu den Nerds gehöre.

AnonietteIn unseren Augen liegt das mangelnde Interesse größtenteils an der irrsinnigen Abstraktion des Themas und die Unfähigkeit, die Berührungsängste in den entscheidenden Altersgruppen zu nehmen. Ich denke mal, ich brauche hier niemandem vor Augen zu führen, wie Informatik-Unterricht (sofern er denn stattfindet) in heutiger Zeit immernoch aussieht. Man überzeugt einen heutigen jugendlichen nicht damit, zwei Zahlen auf der Konsole addieren zu können. Hier geht es vielmehr um die Grundbegeisterung, die man wecken muss.

Und genau da bringt AntMe! ein paar entscheidende Faktoren mit, die auch ähnliche Produkte wie Alice, Cara und auch Code Combat nicht leisten können:

  • Das Spiel schafft es sehr schnell, dem Spieler ein grobes Bild davon zu vermitteln, mit welchen Problemen ein Programmierer täglich zu kämpfen hat und mit welchem Handwerkszeug er umgehen muss. Sogar die Analogie zur Bug-Bekämpfung passt. Natürlich bändigt ein „echter“ Programmierer selten Ameisen, die verwendeten Mechanismen sind der echten Welt aber erschreckend ähnlich.
  • Das Spiel steigt mit einem Szenario ein, das jedem Kleinkind bekannt ist: Ameisen müssen Zucker sammeln, bauen Ameisenstraßen und arbeiten mithilfe von Duftmarken zusammen. Man muss für eine glaubwürdige Aufgabenstellung nichts fiktives konstruieren. Hinzu kommt, dass die Ameisen in der jeweiligen Landessprache (also „GeheGeradeaus()“ in Deutsch, „GoAhead()“ in Englisch) programmiert werden können. Das klingt für einen ausgewachsenen Programmierer erstmal grausam, baut aber unnötige Hürden beim Erstkontakt ab.
  • Was der Spieler im Spiel lernt, lässt sich 1:1 in der Praxis anwenden. Er verwendet eine professionelle Entwicklungsumgebung (Visual Studio), eine marktrelevante Sprache (C#) und aktuelle Programmiermodelle (OOP, Events, Asynchronität, Lambda-Expressions,…).
  • Ein Spieler kann mit dem passenden Begleitmaterial auch gleich die kompexeren Theorien hinter seiner Ameise verstehen. So wendet er automatisch den objektorientierten Ansatz an, indem er eine Ameise programmiert und 100 Instanzen im Spiel bekommt. Hand aufs Herz: Wie lange habt ihr gebraucht, um OOP wirklich zu begreifen ohne es in einem sinnvollen Umfeld angewendet zu haben?

Und darin besteht nun der Zauber hinter den Ameisen. Denn egal wem man das Spiel gibt, er wird seinen Nutzen daraus ziehen. Angefangen bei jemandem, der sich wirklich überhaupt nicht für Technik interessiert. Auch er wird nach dem Spiel mit AntMe! nicht zum Informatiker. Aber das Spiel gibt diesem Menschen die Chance auf einen Einblick in dieses Aufgabengebiet. Damit kann er sich dann etwas objektiver dagegen entscheiden, wird aber in Zukunft sicher etwas mehr Grundverständnis mitbringen.

Für Unentschlossene ist es eine perfekte Möglichkeit den Umgang mit einer Entwicklungsumgebung zu trainieren, sicheren Umgang mit Code zu lernen und dabei auch noch Spaß zu haben.

Studenten, für die die Theorie dahinter wesentlich wichtiger als der Code ist, können verschiedenste Patterns und Techniken zuerst anwenden, um danach die Theorie umfassender zu begreifen.

Und selbst für alteingesessene Programmierer kann das Spielen mit den Ameisen eine echte Herausforderung sein – sozusagen E-Sports für Nerds. Viele Grüße an dieser Stelle an meine Freunde der .NET Usergroups in Karlsruhe, Braunschweig, NRW, Dresden und München.

Business-Modell

Beim Schreiben der ersten Zeile AntMe! haben Wolfgang und ich beschlossen, dass die Software für Bildung und Forschung immer frei sein soll. Dem Grundsatz will ich auch weiterhin treu bleiben, weil das nun mal Sinn und Zweck der Entwicklung war.

Logo

Wie also lässt sich in ein funktionerendes Business-Modell umwandeln? Grundsätzlich sieht unser Konzept 3 unterschiedliche Produkte/Leistungen vor:

  1. Der AntMe!-Client (das eigentliche Spiel), der für Bildung und Forschung, sowie den privaten Gebrauch weiterhin frei verwendbar sein soll. Darin angebunden natürlich ein API-Wiki, Forum und Dokumentation.
  2. AntMe! Online. Eine Plattform, die das freundliche Miteinander in AntMe! erlaubt. Angemeldete Spieler können ihre programmierten Ameisen hochladen und andere Spieler herausfordern. Die Simulation läuft dann in der Cloud.
  3. Die Bildungsplattform. Das ist eine Mischung aus ausgearbeitetem Unterrichtsmaterial und der Online-Plattform, die im Klassenverband zentralisiert Programmier-Aufgaben stellen kann. Hinzu kommt hier professioneller Support und die Möglichkeit Trainer zu buchen.

Wie man sofort erkennen kann, ist die Wertschömpfung primär auf Dienstleistung und Services ausgelegt, weniger auf das Produkt selbst. Damit das Grundprodukt aber überhaupt enstehen kann, braucht es noch etwas Power.

Das letzte halbe Jahr habe ich darauf verwendet, einen soliden Prototypen zu bauen, den ich als Alpha 1 zur Verfügung gestellt habe. Das war der Punkt, an dem ich beschlossen habe, das jetzt etwas ernster anzugehen und mir einen Finanzierungsplan zu überlegen, um das Spiel fertig zu stellen. Inzwischen bin ich etwas weiter und habe ein grafisch ansprechendes Zwischenprodukt namens „Alpha 2“, die ich auch in Bälde mal online stellen werde. (Hier gerne in den Beta Newsletter eintragen). Eine bodenständige Version soll also noch dieses Jahr fertig werden.

Was kannst du tun?

Inzwischen ist der Entwicklungsplan recht stabil. Und da kommt wieder die GmbH ins Spiel. Das ist nämlich der Punkt, an dem ich eure Begeisterung und auch finanzielle Unterstützung brauche. Ich selbst bin nur Programmierer und möchte für eine ansprechende Aufmachung des Spiels professionelle Artists und Illustratoren haben. Ebenso sollen gute Autoren und Pädagogen die Geschichte schreiben. Immerhin haben wir aus den vergangenen Jahren enorm viel Verbesserungspotential erkannt.

Wenn du das gut findest, solltest du dir folgende Frage stellen: Bist du ein erfolgreicher IT-ler, der in seinem Studium Ameisen gespielt hat oder sogar durch AntMe! inspiriert worden? Oder vielleicht Teil einer IT-Firma die Azubis mit Ameisen lockt oder in der Mittagspause gerne mal den Ameisenhügel verteidigt? Oder auch Lehrer, Professor, Dozent oder Ausbilder an einer der zahlreichen Bildungsstätten dieses Landes? Aber vielleicht auch Einfach jemand, der die Wichtigkeit von Weiterbildung im IT-Bereich erkannt hat?

Dann gibt es zahlreiche Möglichkeiten uns in unserem Vorhaben zu unterstützen.

Schritt 1: Unterstütze uns mit deinem Daumen auf Facebook und bleibe über den Newsletter am Ball. Damit stellst du sicher, dass du immer auf dem Laufenden bleibst. Außerdem zeigst du uns dadurch, wie viele Leute das Thema als relevant erachten und unsere Lösung gut finden.

Schritt 2: Schick das hier an alle, die damit was anfangen können. Ihr wisst, wie wichtig Reichweite ist. Wenn es ein gutes Spiel werden soll, ist ein direkter Draht zu möglichst vielen Leuten Gold wert. Außerdem motiviert Zuspruch!

Schritt 3: Werde Sponsor, Unterstützer, Botschafter!

Ich weiß, dass es euch gibt da draußen. Generationen von IT-Anwärtern werden euch dankbar sein!

Ants Business

Veröffentlicht: 19 Juni 2014 in AntMe!
Schlagwörter:, , ,

BoogieAktuell arbeite ich an den ersten Levels für AntMe! 2.0. Es geht dabei nicht nur darum eine flache, braune Spielfläche zur Verfügung zu stellen, auf der ein Spieler gegen automatisierte und dämliche Wanzen spielen muss.

Das Spielkonzept bietet hier wesentlich mehr, mit dem ein Level-Designer arbeiten kann. Wie ich schon in vorherigen Blog-Einträgen verraten habe, wird das Spielfeld in Zukunft über diverse topologische Eigenschaften verfügen und unterschiedliche Oberflächenbeschaffenheiten aufweisen können. Dazu kommen weitere Spielelelemente, mit denen die Ameise interagieren kann.

Eines der mächtigsten Tools für einen Level-Designer ist aber die Möglichkeit eines ausgeklügelten Scriptings. Es erlaubt es, die Spielumgebung interaktiv zu gestalten und auf Aktionen des Spielers zu reagieren. Betritt eine Ameise beispielsweise einen bestimmten Spielbereich, können neue Einheiten Spawnen, Hindernisse verschwinden oder Dialoge gestartet werden – schlicht Geschichten erzählt werden.

Verbindet man die kleinen Geschichten einzelner Aufgaben und Levels zu einem fortlaufenden Ereignis-Strang, erhält man eine packende Geschichte mit Höhen und Tiefen unserer neuen Protagonisten, die den Spieler ständig motivieren auch noch die nächste Aufgabe zu meistern und dadurch mehr über die Informatik zu erfahren! Ich freue mich schon sehr auf die Reaktionen der ersten Beta-Tester.

Die passende Mailing-Liste ist weiterhin offen und freut sich über interessierte Teilnehmer. An alle die sich kürzlich eingetragen haben und nun auf Post warten: Ich arbeite auf Hochtouren an einer neuen Alpha, damit ihr was zu spielen habt. Bitte schaut auch gerne immer mal wieder in unserer Facebook Gruppe vorbei.

Howto play AntMe! 2.0

Veröffentlicht: 31 März 2014 in AntMe!, Development

tomEine der wichtigsten Fragen, wenn es um AntMe! 2.0 geht, ist sicher die Frage nach dem Gameplay. Ich kann alle beruhigen, die mir gesagt haben, dass es weiterhin so simpel bleiben muss wie bisher. „Bitte kein Neustart“, „bitte nicht alles anders“ haben mir viele Lehrer und Trainer gesagt, als ich ihnen von der neuen Version erzählt habe. Das habe ich mir sehr zu Herzen genommen. Daher wird einem die neue Ameisen-API sehr, sehr vertraut vorkommen. Die Ameisen werden weiterhin mit Events funktionieren. Allerdings bekommt der Spieler wesentlich mehr Informationen über seine Umgebung, sodass die aktuelle Situation wesentlich besser analysiert werden kann. Allerdings kommt mit AntMe! 2.0 auch die Möglichkeit hinzu, die Wanzen zu programmieren. Damit das nicht langweilig wird, arbeiten diese Kollegen nach einem komplett anderen Programmier-Konzept. Man darf gespannt sein. In diesem Artikel gehts nämlich erstmal nur um die Ameisen.

Templates

Besonders wichtig bei der Neuumsetzung des Spiels war die Überarbeitung des Erstellungsprozesses von neuen Ameisen. Bisher wars ja so, dass der Spieler händisch das Ameisen-Projekt kopieren musste. Das ist recht heikel, weil der Spieler im Normalfall ja nicht wirklich weiß, was er da tut. Mal ganz abgesehen davon, dass die Runtime diverse technische Schwierigkeiten bekommt, da alle Spieler-Dateien ja scheinbar aus dem selben Projekt stammen, aber unterschiedliche Inhalte aufweisen. Diese kleine Hürde wird dem Spieler in Zukunft genommen. Ein Generator für diese Ameisen-Templates erstellt dem Spieler erstmal ein Grundgerüst für jegliche unterstützte Kombination an Entwicklungsumgebung, Sprache und Programmiersprache. Der Spieler wählt also „Visual Studio 2013“, „C#“, dann „Deutsch“ und am Ende fällt ein fertiges Visual Studio Projekt heraus mit dem es dann los gehen kann. Realisiert wird das über Code-Generatoren. Darüber schreibe ich aber mal, wenn es um die Erweiterbarkeit von AntMe! 2.0 geht.

Lokalisiert Spielen

Bekannt aus vorherigen AntMe! Versionen spielt der Spieler wieder lokalisiert in seiner Muttersprache. Das ist und bleibt eines der Grundkonzepte der Ameisen, weil wir extem positive Erfahrungen damit gemacht haben, den Spieler nicht gleichzeitig mit der Programmier-Herausforderung auch noch mit einer Sprachbarriere zu konfrontieren. Die Ameisen reagieren also weiterhin auf ein GeheGeradeaus() oder Nimm(zucker) und verfügt über Events wie Sieht(Zucker zucker). Kontrollstrukturen und Schlüsselworte bleiben natürlich weiter C#-üblich.

Ameisen produzieren

Eine kleine Änderung gibts dann aber doch – zu Gunsten einer korrekten Umsetzung der OOP. Der Generator erzeugt nämlich direkt zwei Klassen im Projekt. Neben der Klasse, die die Ameise repräsentiert, gibt es noch eine übergeordnete Rolle, die die komplette Kolonie darstellt. Diese zweite Klasse übernimmt die Aufgabe der bisher statischen BestimmeTyp()-Methode der alten Version. Sie gliedert die Aufgabe der Erzeugung neuer Ameisen in eigenen Aufgabenbereich aus, damit der Spieler bei der Erstellung seiner Ameisen etwas freier ist. Eine ganze Reihe von Informationen über das aktuelle Volk (Punktestand, Ameisen-Typ Verteilung, Kampf- und Ressourcen-Statistiken) unterstützen den Spieler bei der Entscheidung, welche Ameise als nächstes erzeugt werden soll. Ganz nebenbei erfüllt diese Klasse auch direkt die Funktion eines Fabric-Patterns, wodurch hier auch schon die ersten Informatik-Pattern äußerst plastisch dargestellt werden.

Ameisen spezialisieren

Die Auslagerung der Erzeugungsklasse hat aber nicht nur positive Auswirkungen auf die Code-Struktur, sondern erlaubt es auch, dem Spieler etwas mehr Freiheit bei der Spezialisierung seiner Ameisen zu machen. Ameisen konnten ja bislang mit Hilfe der Kasten-Attribute in Gruppen unterteilt werden. Der Code, der alles steuert, befand sich aber weiterhin in einer einzigen Datei. Wollte man den „Sammler“ anders agieren lassen, als den „Kämpfer“, so musste man als Programmierer in jeder Methode eine Weiche einbauen. Das ist nicht ganz im Sinne der objektorientierten Programmierung. AntMe! 2.0 löst dieses Problem weitaus besser: Die zentrale Klasse des Volkes wird in Zukunft nicht mehr die Ameise, sondern die Ameisen-Fabrik, die Colony-Klasse, sein. Die Implementierung der Ameise verläuft dann vollkommen frei und kann beliebig vererbt werden. Das Kaste-Attribut gibt es zwar weiterhin, aber nur um eine konkrete Implementierung mit der Fertigkeitsverteilung zu belegen – also nutzen wir nur noch die deklarative Funktionsweise eines Attributs. Es wird also möglich sein für sein Volk eine Basisameise zu implementieren, die Grundfunktionen wie die Wegfindung, Dekodierung von Markierungen,… übernimmt. Die Spezialisierung zum Kämpfer oder Sammler kann dann in einer spezialisierten Klasse passieren – ganz im Sinne der OOP.

Wahrnehmung der Welt

Wesentliche Verbesserungen sind bei der Wahrnehmung der Umgebung vorgenommen worden. Wo die alte Version pro Event immer nur das erstbeste Objekt geliefert hat, kann die Ameise in Zukunft ihre Umwelt vollkommen frei analysieren. Richtig kritisch ist dieses Problem bei der Verfolgung einer Markierungsspur. Die alte API liefert bei Riecht(Markierung) lediglich die Markierung, die zuerst gesichtet wurde – und das auch nur ein einziges mal bei Sichtung. Es ist also wirklich schwer eine Ameisenstraße zu analysieren um die Herkunft zu ermitteln. Ein paar Hilfsvariablen wie AmeisenInSichtweite, die die Menge der benachbarten Ameisen zurück geliefert hat, war leider nur selten hilfreich. In Zukunft wird dem Spieler eine Liste von sichtbaren Elementen zur Verfügung gestellt. Eine Variable RiechbareMarkierungen liefert alle wahrgenommenen Markierungen. Das ist der Moment, in dem Schleifen und Lambdas in Spiel kommen. So lässt sich mit RiechbareMarkierungen.OrderBy((m) => (m.Entfernung)). FirstOrDefault() die Markierung ermitteln, die der Ameise am nächsten steht.

So funktionieren die Ameisen im Hintergrund

Alles in allem hat sich die Architektur von Version 2.0 wesentlich generischer entwickelt, um wirklich sehr flexibel bei zukünftigen Entwicklungen zu bleiben und ein Höchstmaß an Erweiterbarkeit zu sichern. Ich will mich bei der Beschreibung hier aber erstmal auf den Ablauf bei den Ameisen beschränken. Im Übrigen funktioniert bei den Wanzen auch wieder alles ganz anders ;) Alles fängt damit an, dass es eine generische Implementierung von Factions, also Fraktionen, Parteien, Rassen,… gibt die von beiden Rassen (Ameisen und Wanzen) gleichermaßen verwendet werden. Die konkrete Implementierung bestimmt dann, wie der Ablauf dieser Rasse im Spiel abläuft (Wann neue Einheiten erstellt werden, wie neue Einheiten erstellt werden,…). Im Ameisenfall ist festgelegt, dass es eine Colony-Klasse geben muss – mehr erstmal nicht. Die Colony-Klasse verfügt in Zukunft auch über das Spieler-Attribut, das Auskunft über den Kolonie-Namen und die Autoren-Referenz gibt. Geht eine Simulation los, wird beim erzeugen einer Ameise diese Colony gefragt, welche neue Ameise erzeugt werden soll. Der Rückgabewert ist hier ein schlichter Type der von der AntFaction untersucht wird. Handelt es sich hierbei um eine gültige Ameisen-Klasse (erbt sie von einer Basisklasse, verfügt sie über ein Kasten-Attribut), wird eine Instanz davon erzeugt und die Simulation erhält einen Repräsentaten in Form einer Ameise. Erlebt die Ameise innerhalb der Simulation irgendetwas spannendes (sieht Zucker oder so), wird dem Programmierer mit Hilfe eines Interop-Objektes ein Ereignis geworfen, das letztendlich zur Ausführung der entsprechenden Methode führt.

Basis API

Das eben erwähnte Interop-Objekt ist – im Gegensatz zur vorherigen Version der Lokalisierung – die zentrale Schnittstelle zwischen dem Ameisen-Objekt der Simulation und der lokalisierten API für den Spieler. Dieses Interop-Objekt verfügt über wesentlich rohere Daten als die übersetzte API. Was bei der deutschen Lokalisierung noch SichtbareAmeisen, SichtbareWanzen,… heißt, ist im Interop-Objekt noch in VisibleItems zusammengefasst. Das kann in manchen Situationen sehr, sehr nützlich sein. Ebenso sind die Events, die eintreffen, etwas generischer. So gibt es nur ein Spots(Something)-Event – ohne Differenzierung des Element-Typs. Ameisenvererbung Profis können das für sich nutzen. Einerseits steht dem Programmierer bereits in den Standard-Templates ein Interop-Objekt zur Verfügung, auf das frei zugegriffen werden kann. Der Spieler kann aber auch von Anfang an über das Erben einer anderen, wesentlich niedrigeren Version der Basisameise Zugriff darauf verschaffen und seine API nach eigenem Ermessen basteln.

Willst du die Alpha testen?

So! Die erste spielbare Alpha steht bereits fast vor der Tür. Leider lässt hier der Komfort und Unterstützung durch Dokumentation und Hilfe-Dialoge noch etwas zu wünschen übrig, weshalb das hier sicher nur was für Leute ist, denen der Prozess des Erstellens eigener Projekte, Referenzierung von Assemblies und Einbinden von Namespaces nicht unbekannt ist. Da ich aber gerade jetzt, wenn es um die Gestaltung des Spiel-Interfaces geht, gaaaanz, ganz viel Feedback brauche, würde ich gerne ein paar Leute zum geschlossenen Test einladen. Fühlst du dich angesprochen, dann schreib mir doch bitte direkt eine Email (tom@antme.net) mit ein paar Worten dazu, was du so machst und in welchem Zusammenhang du zum Ameisen-Projekt stehst. Ich würde mir wünschen, wenn sich auf diesem Weg ein paar Lehrer, Trainer und Pädagogen bei mir melden, damit ich hier möglichst früh noch euer Feedback einbeziehen kann. Danke für euer Unterstützung!

Das Projekt AntMe! 2.0

Veröffentlicht: 12 März 2014 in AntMe!, Development
Was bisher geschah

AntvatarWer AntMe! nicht kennt: Es handelt sich dabei um ein bereits 6 Jahre altes Projekt aus meiner Studien-Zeit. Zusammen mit Wolfgang Gallo stand ich damals auf der Games Convention in Leipzig am Stand der Universität Leipzig, um umherziehenden Gamer-Kids die technischen Hintergründe von Spielen zu zeigen. Gemeinsam entwickelten wir deshalb ein Spiel, das dies vernünftig veranschaulichen sollte.

Das Ergebnis war eine sehr, sehr einfache Art von Programmierspiel. In Phase 1 programmiert der Spieler „das Gehirn“ von Ameisen (Im Prinzip eine simple KI), die in der anschließenden Simulation (Phase 2) ihr Leben bestreiten muss, indem Sie Nahrungsmittel wie Zucker und Äpfel einsammeln, aber auch gegen den natürlichen Feind, die Wanze, kämpfen müssen.

Den Rest des Beitrags lesen »

XNA-Sample: MoonTaxi (Teil 3/3)

Veröffentlicht: 30 Dezember 2012 in Microsoft, Visual Studio, XNA
Schlagwörter:

Hohoho! An dieser Stelle startet Teil 3 des mittlerweile echt groß gewordenen Tutorials zu MoonTaxi. Die bisherigen Teile haben dir gezeigt, wie man Grafiken lädt und rendert, auf die Usereingabe reagiert und die Spielobjekte physikalisch korrekt bewegt. Außerdem wurde dir gezeigt, wie man einfache Kollisionsberechnung im 2D-Raum und rechteckigen Objekten durchführen kann. In einem kleinen Lade-Skript für Levels ist sogar das manuelle Editieren mit Notepad möglich. Am Ende der vorherigen Tutorials konnte der Spieler bereits durch das Level fliegen und an Hindernisse stoßen.

Wem die ersten beiden Tutorial-Teile noch fehlen, der kann sich der folgenden Links bedienen:

Was ich in Teil 3 zeigen will sind kleine Schönheitskorrekturen. Wir sammeln nämlich noch gar keine Fahrgäste ein. Danach möchte ich gerne mehrere Levels hinterlegen können, die bei Erfolg durchlaufen werden. Damit man dann eine Übersicht über den Spielfortschritt hat, fügen ich dann noch eine kleine Level- und Geschwindigkeitsanzeige im Spiel ein. Vielleicht schaffen wir auch noch ein paar Sounds und Musik einzufügen. Außerdem möchte ich gerne noch die Vibration des Controllers ansprechen, wenn eines der Triebwerke verwendet wird.

Schritt 1: Fahrgäste ein- und ausladen

Das Spiel macht zwar im aktuellen Stand schon recht viel Spaß, aber so ganz ohne Spielziel wird auch das bald recht öde. Das eigentliche Spielprinzip sieht ja vor, dass man Fahrgäste einsammelt und durch ein kompliziertes Level bis zum Zielort bringt.

Wie schaut die Dynamik beim ein- und ausladen von Fahrgästen aus? Ich würde das mal schnell damit zusammen fassen, dass das Taxi auf jeden Fall gelandet sein muss. Landet das Taxi in der Nähe eines Fahrgastes oder dem Ziel, so wird automatisch ein- oder ausgeladen.

Ob das Taxi den Fahrgast bereits eingeladen hat, müssen wir irgendwo speichern. Ein weiteres Feld in unserer Taxi-Klasse würde sich da anbieten. Aus diesem Grund füge doch dort einfach ein neues Feld vom Typ bool ein, das speichern soll, ob das Taxi den Fahrgast geladen hat.

public bool GuestLoaded { get; set; }

Natürlich muss auch diese Variable wieder in Reset zurück gesetzt werden, damit bei einem Neustart nicht plötzlich noch der Gast an Board bleibt.

Im nächsten Schritt ist die Interaktion mit der Umwelt zu implementieren. Am klügsten ist es, zuerst eine Prüfung darauf zu machen, ob das Taxi gelandet ist. Wenn das zutrifft, können wir die Entfernung zu dem Gast und dem Ziel berechnen (je nach Zustand des Taxis). Ist die Entfernung klein genug, dann wird der Gast ein-, bzw. ausgeladen.

Distanzberechnung

Abb. 1: Entfernungsberechnung zwischen zwei Punkten im Koordinatensystem.

Um die Entfernung der beiden Objekte zu berechnen, bedienen wir uns einfachen Vektor-Berechnungen. Abbildung 1 zeigt den schematischen Aufbau in einem Koordinatensystem. P1 repräsentiert hier das Taxi, P2 den Fahrgast und die blauen Vektoren sind gleichzusetzen mit den Positionen der beiden Objekte. Um nen den Vektor zwischen P1 und P2 zu ermitteln, muss man die beiden Vektoren einfach voneinander subtrahieren. Da uns nur die Länge des Vektors interessiert, spielt es keine Rolle welchen Vektor wir von welchem anderen Vektor subtrahieren. Legen wir also fest, dass wir immer P2 minus P1 berechnen und die Länge des resultierenden Vektors ermitteln.

Den folgenden Code fügen wir einfach dort ein, wo wir im zweiten Teil des Tutorials die Start-Sequenz platziert haben. Das war in der Update-Methode gleich unter der Deklaration der delta-Variablen.

// Abhebesequenz 
if (taxi.Landed) 
{ 
    if (forceY > 0) 
        taxi.Landed = false;

    // Gastbehandlung 
    Vector2 taxiPoint = taxi.Position + 
        new Vector2(Taxi.WIDTH / 2, Taxi.HEIGHT / 2); 
    if (taxi.GuestLoaded) 
    { 
        Vector2 goalPoint = level.GoalPoint * BLOCKSIZE + 
            new Vector2(BLOCKSIZE / 2, BLOCKSIZE / 2); 
        float distGoal = (taxiPoint - goalPoint).LengthSquared(); 
        if (distGoal < MAXPICKUPDISTANCE * MAXPICKUPDISTANCE) 
        { 
            // Nahe genug am Ziel - Level neu starten 
            Reset(); 
        } 
    } 
    else 
    { 
        Vector2 guestPoint = level.GuestPoint * BLOCKSIZE + 
            new Vector2(BLOCKSIZE / 2, BLOCKSIZE / 2); 
        float distGuest = (taxiPoint - guestPoint).LengthSquared(); 
        if (distGuest < MAXPICKUPDISTANCE * MAXPICKUPDISTANCE) 
        { 
            // Nahe genug am Gast - Gast aufnehmen 
            taxi.GuestLoaded = true; 
        } 
    } 
}

Im Code wurde hier schon wieder eine neue Konstante eingeführt (MAXPICKUPDISTANCE) die auch weiter oben noch initialisiert werden sollte. Ich habe das mit dem Wert 50f gemacht. Also 50 Pixel Entfernung zwischen Taximittelpunkt und dem Zielpunkt, bzw. Gast.

Damit wäre die Logik zum Aufsammeln von Fahrgästen geschrieben. In der Ansicht wird der Fahrgast aber immernoch gezeichnet, auch wenn er bereits im Taxi sitzt. Das lässt sich mit einer kleinen Bedingung in der Draw-Methode beheben. Vor die Zeile, die den Fahrgast malt, wird einfach die Bedingung “if (!taxi.GuestLoaded)” platziert.

Wir können jetzt also vollständig spielen. Das Taxi taucht auf, kann Fahrgäste einladen, gegen Levelgegenstände stoßen. Bei Erfolg wird das Level aber neu gestartet. Irgendwie unbefriedigend. Im nächsten Schritt führen wir einfach mal mehrere Levels ein. Die Level-Dateien dazu haben wir ja auch schon.

Schritt 2: Mehrere Levels

Um das zu realisieren gehört garnicht so viel. Das System, das ich gerne implementieren würde, basiert auf einem konsistenten Namenskonzept für Level-Dateien. Level 1 heißt “level1.txt”, Level 2 dann “level2.txt” usw.. Um diese Dateien jetzt richtig anzusprechen, verwende ich einfach einen Level-Zähler, der bei Erfolg eins nach oben zählt und damit das neue Level startet.

Die neue Variable “levelNumber” findet ihren Platz in der zentralen Game-Klasse und direkt auf 1 initialisiert. Bei Erfolgreichem Abschluss des Levels wird der Zähler einfach um eins erhöht und der Ladepfad für die Leveldatei in der Reset-Methode entsprechend modifiziert.

int levelNumber = 1;

if (distGoal < MAXPICKUPDISTANCE * MAXPICKUPDISTANCE) 
{ 
    // Nahe genug am Ziel - Level neu starten 
    levelNumber++; 
    Reset(); 
}

level = new Level(@"Levels\level" + levelNumber + ".txt");

Danach gehts rund! Nach einem erfolgreichen Level werden dynamisch neue Levels nachgeladen! jetzt liegt’s an dir auch genügend neue Levels bereit zu stellen!

Schritt 3: HUD

Da wir nun mehrere Levels haben und es auch Dinge gibt, auf die der Spieler achten muss, macht es langsam wirklich Sinn auch einen kleinen Head Up Display (kurz HUD) einzubauen. Ich verzichte dabei erstmal vollständig auf grafischen Schnickschnack. Es soll links oben einfach eine textbasierte Anzeige des aktuellen Levels und der aktuellen Taxi-Geschwindigkeit geben.

Grundsätzlich werden diese Anzeigen genauso gezeichnet wie der Rest des Spieles auch. Hintergrundgrafiken sind Texturen, die ganz am Ende über die ganze Szene gezeichnet werden. Erschreckenderweise gilt das auch für Schriftzeichen. Wer aus anderen Render-Technologien gewohnt ist einfach ein Label zu verwenden oder einen Befehl wie “print” zu verwenden, der muss etwas umdenken.

Schriften werden in Spielen genauso gezeichnet wie die restlichen Grafiken auch: aus Texturen heraus. Dazu muss die Schrift aber vorher in eine Textur umgewandelt werden. Üblicherweise stellt dazu ein Grafiker eine Textur zusammen, die alle benötigten Buchstaben enthält und dann von der Engine hübsch zu einem sinnvollen Satz/Wort zusammen gebastelt werden können. Man spricht hier von sog. Sprite Fonts.

Sprite Font

Abb. 2: Beispieltextur einer Sprite Font

Bei XNA muss man das zwar nicht mehr selbst machen, sollte aber wissen, was da im Hintergrund passiert. Möchte man für XNA Schrift verwenden, kann man das Ganze ohne Vorbereitung im Content-Ordner generieren. Ein Rechtsklick auf den Content-Ordner, Add, New Item erlaubt die Auswahl “Sprite Font”. Ich habe die daraufhin erstellte Datei einfach mal “moontaxi.spritefont” genannt. Macht man diese Datei mit einem Doppelklick auf, so stellt man fest, dass es sich dabei um eine XML-formatierte Textdatei handelt. Man stellt hier ein paar Rahmenbedingungen ein, unter denen die Textur erzeugt werden soll. Man legt Schriftart, Schriftgröße und Stil fest, anschließend, wenn man will, noch den Wertebereich der benötigt wird – das wars. Der Wertebereich reicht normalerweise über die ganzen Standard-Buchstaben. Leider fehlen noch Sonderzeichen wie z.B. das “Ä”. Ich verwende hier gerne die Schriftart “Verdana” in Größe 12.

Instanziert wird alles wieder über den Content-Loader, nachdem wir eine Variable vom Typ SpriteFont zur Verfügung gestellt haben. Wo passierts? Natürlich wieder in LoadContent.

SpriteFont font;

font = Content.Load<SpriteFont>(“moontaxi”);

Nun gehts ans Zeichnen. Interessant wäre eine Anzeige des aktuellen Levels. Außerdem möchte ich die aktuelle Taxi-Geschwindigkeit anzeigen. Diese Anzeige ist wichtig, da der Spieler vorsichtig auf der Plattform landen muss und dabei eine gewisse Geschwindigkeit nicht überschreiben darf. Aus diesem Grund soll die Geschwindigkeitsanzeige auch farbig darauf hinweisen, ob das Taxi gerade zu schnell zum landen ist. Grün soll bedeuten, dass die Geschwindigkeit passt, rot ist zu schnell.

Das HUD

Abb. 3: Simples HUD

Gezeichnet wird natürlich in der Draw-Methode. Da es sich um die Anzeige handelt, die immer sichtbar sein soll, wird sie auch ganz am Ende gezeichnet, damit sie wirklich über allen restlichen Objekten gezeichnet wird. Darum wird unser HUD-Zeichenblock auch unter den Zeichenblock für das Taxi platziert.

Der Spritebatch, den wir bislang zum Zeichnen unserer Texturen verwendet haben, verfügt auch über eine Methode zum Zeichnen von Zeichenketten: “DrawString”. Diese verwenden wir auch zum erstellen unserer HUD-Texte. Die erste Zeile soll das Label “Level:” enthalten und anschließend die Nummer des aktuellen Levels ausgeben.

Zeile 2 wird etwas komplizierter, da sie aus 2 Zeichenketten zusammengestellt wird. Die Schwierigkeit hier ist nämlich die Position des hinteren, eingefärbten Teils der Zeile zu ermitteln, da die Breite der ersten Hälfte nicht so ganz klar ist. Zu unserem Glück stellt uns die SpriteFont eine Methode zur Verfügung, die uns erlaubt, die Länge von Zeichenketten zu ermitteln. Mit font.MeasureString(“”) erhalte ich einen Vector2, der die Ausmaße der angegebenen Zeichenkette enthält und kann somit vorher schon berechnen, wo ich mit der zweiten Zeichenkette anknüpfen kann. In unserem Fall könnte man einfach durch herumexperimentieren die Position der zweiten Zeichenkette ermitteln, aber manchmal ist die Länge der Zeichenketten natürlich variabel, dann wird das schon schwieriger.

// HUD zeichnen 
spriteBatch.DrawString(font, "Level: " + levelNumber, 
    new Vector2(10, 10), Color.White); 
Vector2 speedSize = font.MeasureString("Speed:"); 
float taxiSpeed = taxi.Speed.Length(); 
spriteBatch.DrawString(font, "Speed:", new Vector2(10, 30), Color.White); 
spriteBatch.DrawString(font, taxiSpeed.ToString("0") + " px/s", 
    new Vector2(speedSize.X + 15, 30), 
    taxiSpeed > MAXLANDINGSPEED ? Color.Red : Color.Green);

Schritt 4: Sounds und Musik

Das Spiel läuft und der Spieler kann schon richtig Spaß haben. Alles in allem ist das Spiel aber noch recht ruhig, dafür, dass wir hier ein Taxi mit Raketenantrieb haben! Da fehlt ganz klar noch Sound und Musik.

XNA kennt eine Reihe von Möglichkeiten auf Sounds zuzugreifen. Glücklicherweise sieht die API dazu immer gleich aus. Zur Verarbeitung von Audio stehen die beiden Namespaces Microsoft.Xna.Framework.Media und Microsoft.Xna.Framework.Audio zur Verfügung.

Beginnen wir am besten damit, dem Taxi Raketensounds zu verpassen. Ich habe dazu ein passendes Geräusch erzeugt und dem Projekt beigelegt (jet.wav). Mit XNA kann man nun sehr abgefahrene Sound-Manipulationen machen, indem man ein mitgeliefertes Werkzeug (XACT) verwendet und die verwendeten Sounds dort importiert und nachbearbeitet. In diesem Tutorial werde ich einen sehr simplen weg ohne dieses Tool gehen. Wer sich also über komplexere Sound-Arbeiten informieren will, ist mit dem Suchwort XACT gut bedient.

In diesem Tutorial werden wir allerdings einfach nur den Raketensound laden und im Loop abspielen, sobald die Triebwerke benutzt werden. Die Handhabung unterscheidet sich von den Texturen kaum. Den Sound kann man auch einfach in den Content-Ordner legen und per Content.Load laden. Datentyp eines Sounds ist SoundEffect. Der Soundeffect selbst ist aber nur die Soundvorlage. Um das Ding abspielen zu können, benötigt man eine Instanz dieses Sounds für jede Stimme, die parallel gestartet werden soll. Da wir unseren Sound aber nur einstimmig starten, reicht uns eine einzelne Instanz, die wir auch bereits im LoadContent-Block erstellen und in einer Klassenweiten Variable “jetSoundInstance” speichern

SoundEffect jetSound; 
SoundEffectInstance jetSoundInstance;

jetSound = Content.Load<SoundEffect>("jet"); 
jetSoundInstance = jetSound.CreateInstance();

Diese eine Instanz kann jetzt beliebig gestartet, pausiert und gestoppt werden. Nachträglich lassen sich dann jederzeit Lautstärke, Balance und sogar das Pitching nachregeln. Sogar das Anwenden von 3D-Parametern ist möglich. Wir verwenden hier aber nur eine Sache: an und aus :)

Da der Status des Geräusches vollständig vom Status der Usereingabe abhängt, platziere ich die Steuerung direkt an die Stelle in Update, an der wir die Controller- Tastatur-Eingabe abgegriffen haben. Also kurz bevor wir mit der Berechnung der Bewegungsänderung beginnen. Ist eine der beiden Variablen “forceX” oder “forceY” ungleich 0, so bedeutet das, dass der Spieler gerade in irgend eine Richtung steuert und somit Jets benutzt. Solange also eine der beiden nicht 0 ist, wird der Sound gespielt, ansonsten gestoppt. XNA ignoriert einen “Play”-Aufruf, wenn der Sound schon abgespielt wird. Wir müssen also nicht vorher noch den Status prüfen.

// Jet-Sound 
if (forceX != 0f || forceY != 0f) 
{ 
    // Sound muss an sein 
    jetSoundInstance.Play(); 
} 
else 
{ 
    // Sound muss aus sein 
    jetSoundInstance.Stop(); 
}

Das ist auch schon der ganze Zauber. So einfach ist es, einfache Sounds abzuspielen. Mit Musik geht das grundsätzlich genau so. Einfach den Soundtrack in den Content-Ordner werfen (gerne auch im MP3-Format), den Inhalt vom Typ “Song” laden und abspielen. Etwas galanter wäre es aber doch, wenn das Spiel einen beliebigen Song aus der Media-Library des Media-Players – oder auf der XBox der Musiksammlung – abspielen würde. Oder?

Da kommt der Namespace Media ins Spiel. Dieser ist nämlich genau für solche Zwecke da: Der Zugriff auf die eigene Musik-, Video- und Bilder-Bibliothek. In diesem Beispiel möchte ich einen Zufälligen Song aus der Bibliothek abspielen. Aus diesem Grund benötigen wir zwei weitere Variablen innerhalb der Game-Klasse. Eine, um einen Verweis auf die Bibliothek zu speichern. Eine andere, um den aktuellen Song zu halten.

MediaLibrary library = new MediaLibrary(); 
Song music;

Die Bibliothek wird direkt instanziert. Der Verweis auf den Song soll sich allerdings mit jedem Level-Start neu auswählen. Das passiert am besten in der Reset-Methode, weil wir dort bei jedem Neustart eines Levels vorbei kommen – egal ob eine Kollision stattgefunden oder der Spieler das Level erfolgreich beendet hat. Dort habe ich einen Zufallsgenerator erstellt, der aus den vorhandenen Songs einen auswählen soll. die Library erlaubt uns den Zugriff auf eine Auflistung aller Songs über ihr Member “Songs”. Die Songs lassen sich über einen Index abrufen.

Sobald wir einen Song haben, kann der direkt über die statische Klasse “MediaPlayer” abgespielt werden. Songs, die nicht aus der Library kommen sondern aus dem Content-Ordner geladen werden, werden auf dem selben Weg abgespielt – einfach über den MediaPlayer abspielen.

// Musik abspielen 
Random rand = new Random(); 
music = library.Songs[rand.Next(0, library.Songs.Count)]; 
MediaPlayer.Play(music);

Und schon hat unser Spiel sowohl Sounds, als auch Hintergrund-Musik und das Spielen macht gleich doppelt so viel Spaß!

Schritt 5: Jet-Flammen und winkende Gäste (Textur-Akrobatik)

Das Taxi macht ja schon ne Menge krach, wenn es Gebrauch von seinen Jets macht. Sehen kann man aber leider noch nicht so viel. Das sollte sich doch bitteschön noch ändern. Anhand dieses kleinen Beispiels würde ich auch gleich zeigen wollen, wie man weiterhin mit Texturen umgehen kann.

Als kleine Finger-Übung fangen wir an mit einem animierten Taxi-Gast. Im Moment steht dieser nur statisch da und wartet auf das Taxi. In Zukunft soll dieser aber im Sekundentalk winken. Das realisieren wir mit einer Art Daumenkino für 2D-Flächen. Der Trick ist nämlich für jeden Animationsschritt eine eigene Textur zu haben. Damit da der Ladevorgang aber nicht zu Umfangreich wird, macht man der Einfachheit halber alle Animationsschritte in eine einzige Textur. Diese Textur habe ich unter dem Namen “guest2.png” vorbereitet.

Der Fahrgast

Abb. 4: 2 Frames für eine Animation

In dieser Textur sind nun plötzlich 2 Taxigäste zu finden. Und ersetzt man einfach die erste Textur, so tauchen auch im Spiel plötzlich 2 Gäste auf. Nicht gut. Was wir jetzt nämlich machen müssen, ist den Bereich innerhalb der Textur, der tatsächlich gezeichnet wird, weiter einzuschränken. Mit der Standard-Überladung von Draw wird die komplette Textur gezeichnet. Es existiert aber auch eine, die die Angabe eines SourceRectangles erlaubt. Das brauchen wir. Mit diesem Rectangle lässt sich ein Bereich innerhalb der Textur benennen, die anschließend auf dem Bildschirm landet. Wir ersetzen also den Codeabschnitt zum Zeichnen des Gastes durch folgenden Code.

// Code mit Animation (Ab Schritt 5) 
int frame = gameTime.TotalGameTime.Seconds % 2; 
spriteBatch.Draw(guestTexture, 
    new Vector2(x * BLOCKSIZE, y * BLOCKSIZE), 
    new Rectangle(frame * BLOCKSIZE, 0, BLOCKSIZE, BLOCKSIZE), Color.White);

in der ersten Zeile bestimmen wir den Animationsframe in Abhängigkeit der Spielzeit. Modulo ermittelt mir hier eine Zahl zwischen 0 und 1, im Sekundentakt wechselnd. Weiter unten verwende ich den Wert dann im Source Rectangle, um einmal von Position 0 oder von Position 20 jeweils ein Rechteck von 20×20 auszuschneiden und zu zeichnen.

Etwas komplizierter wird’s mit dem Triebwerksstrahl. Zur Verfügung steht uns nur ein einziges Flammenbild. Daraus sollen die animierten Triebwerksstrahlen für alle Seiten gemalt werden. Außerdem soll die Flamme in Abhängigkeit der Schubkraft ihre Größe verändern.

Als erste fange ich damit an neue Felder in die Taxi-Klasse zu bauen. Die Kraft der Triebwerke ermitteln wir nämlich in Update, zeichnen werden wir aber in Draw. Wir müssen also die Infos irgendwo zwischen speichern. Da das eine Eigenschaft des Taxis ist, ist auch dort der geeignete Platz das zu speichern. Unsere Taxi-Klasse bekommt 3 neue Member.

public float JetLeft { get; set; } 
public float JetRight { get; set; } 
public float JetBottom { get; set; }

Ermittelt werden die Werte genau da wo wir sie auch berechnen. In der Update-Methode. Dort, wo wir eben auch die Sounds entsprechend abgespielt haben.

// Jet-Settings 
taxi.JetLeft = Math.Max(forceX, 0f); 
taxi.JetRight = Math.Abs(Math.Min(forceX, 0f)); 
taxi.JetBottom = Math.Max(forceY, 0f);

Anschließend kann für jeden Jet einzeln die Stärke ermittelt werden. Mit Hilfe der Math.Min/Max-Methoden lässt sich das schnell aus den force-Floats heraus ermitteln. Auf zur Draw-Methode. Hier müssen wir jetzt irgendwie aus einem einzigen Feuerbildchen eine Animation erzeugen und gleichzeitig auch an 3 verschiedene Treibwerke malen. Ein perfektes Beispiel um die Möglichkeiten der Draw-Methode zu zeigen.

In unserem Fall besteht das Bild aus einem nahezu achsensymethrischen Bild einer Flamme. Damit es nach einer kleinen Animation aussieht, spiegeln wir das Bild einfach alle 1/10 Sekunden. Welches Bild gerade dran ist, wird auf dem selben Weg ermittelt wie wir das eben schon beim Fahrgast gemacht haben. Einziger Unterschied: Wir nehmen Millisekunden (1000 pro Sekunde) und dividieren den Wert aber nochmal durch 100. Damit ergeben sich zehntel Sekunden, die zusammen mit dem Modulo 2 wieder einen Flip pro zehntel Sekunde ergeben.

Animation durch Spiegelung

Abb. 5: Spiegeln mit negativen Werten.

Um die Textur jetzt gespiegelt darzustellen, bedienen wir uns einem mathematischen Trick. Ähnlich wie beim Fahrgast verwenden wir Source Rectangle, um den Quellbereich des Bildes auszuwählen. Indem wir aber das ausschneidende Rechteck nicht nach rechts, sondern nach links spannen, wir die Textur anschließend umgedreht dargestellt. Abbildung 5 zeigt die zwei Varianten. Oben wir der Startpunkt 0/0 verwendet und in positiver Richtung aufgespannt (10/10) und die Grafik wird wieder genau so dargestellt, wie sie in der Textur hinterlegt ist. Beispiel 2 (unten) zeigt das Resultat, wenn man den Ursprung bei 10/0 platziert und das Rechteck in den Negativen Bereich aufspannt (-10/10). Das Ergebnis ist eine achsengespiegelte Version der Textur.

Eine solche Achsenspiegelung kann der Sprite Batch auch von Haus aus mit den SpriteEffects. Aber ich wollte das gerne von Hand machen, weil man mit solchen Tricks auch später noch im 3D-Raum für interessante Effekte sorgen kann und man das unbedingt mal von Hand gemacht haben sollte.

Fehlt uns noch die Rotation der Textur. Für den Strahl unten muss sich nichts ändern. Die Strahle rechts und links müssen aber jeweils um 90 Grad rotiert werden. Dazu gibt man bei der Methode einen Rotationspunkt an (wir nehmen hier erstmal 0/0) und einen Rotationswinkel.

Rotation von Texturen

Abb. 6: Rotation von Texturen

Der Kringel in Abbildung 6 symbolisiert die Drehachse um die das Bild im angegebenen Winkel rotiert wird. Die Angabe wird nicht in Grad, sondern im Bogenmaß angegeben. Eine 90 Grad-Rotation im Uhrzeigersinn ist also Pi/2. MathHelper liefer dafür sogar die Konstante PiOver2.

Als letzte Modifikation manipulieren wir jetzt noch die Flammengröße. Je stärker der Gamepad-Knüppel gedrückt wird, umso stärker ist der Schub der Düse. Entsprechend soll auch der Feuerstrahl skaliert werden. Volle Kraft, also 1, soll die Flamme in voller Größe anzeigen, mit halber Stärke aber bitte nur mit halber Höhe. Skalierungen dieser Art werden auch über das Source Rectangle und Destination Rectangle umgesetzt. In diesem Fall wird das Ziel-Rechteck einfach etwas flacher gezeichnet als das Quell-Rechteck. Die Textur wird dabei nicht, wie vielleicht erwartet, abgeschnitten, sondern einfach getaucht.

Skalierung von Texturen

Abb. 7: Verzerrung der Flammen-Textur.

Indem man beim Quell-Rechteck eine Höhe von 20 angibt, das Ziel-Rechteck aber nur mit 10 Pixel Höhe angibt, verursacht man eine Verzerrung der Textur. Will man die Textur lieber abschneiden, muss man natürlich das Quell-Rechteck entsprechend auf den Bereich beschränken, den man ausgeschnitten haben möchte.

Alles in allem lassen sich diese Bildmanipulationen in einer einzigen Zeile Code pro Triebwerksstrahl verpacken. Hier beispielhaft aufgelistet die Zeile Code, die den linken Triebwerksstrahl zeichnet. Sie wird unmittelbar vor der Taxitextur gezeichnet, damit sie zwar hinter dem Taxi liegt, aber vor allen anderen Spielelementen gezeichnet wird. Wir benötigen noch eine neue Konstante “FLAMESIZE”, die die Texturausmaße der Flamme angibt. Außerdem muss auch hier wieder über die Game-Zeit und einem Modulo der aktuelle Animationsframe ermittelt werden.

public const int FLAMESIZE = 10;

int flameFrame = (gameTime.TotalGameTime.Milliseconds / 100) % 2;

spriteBatch.Draw(flame, 
    new Rectangle( 
        (int)(taxi.Position.X), 
        (int)(taxi.Position.Y + 10), 
        FLAMESIZE, 
        (int)(taxi.JetLeft * FLAMESIZE)), 
    new Rectangle( 
        flameFrame == 0 ? 0 : FLAMESIZE, 
        0, 
        flameFrame == 0 ? FLAMESIZE : -FLAMESIZE, 
        FLAMESIZE), 
    Color.White, 
    MathHelper.PiOver2, 
    Vector2.Zero, 
    SpriteEffects.None, 
    0f);

Die Parameter von spriteBatch.Draw sind teilweise bekannt. Als erstes erwartet diese Methode immer die Textur, die gemalt werden soll. Zweiter Parameter ist das Destination Rectangle. Das ist das Rechteck auf der Zeichenfläche, in das die Textur gepresst werden soll. Natürlich ist das die Position des Taxis plus ein paar Korrektur-Pixel, damit der Strahl nicht auf dem Taxi, sondern am Triebwerks-Ausgang gemalt wird. Die Skalierung der Flamme wird hier von JetLeft abhängig gemacht. Da dort ein Wert zwischen 0 und 1 enthalten ist, reicht hier die Multiplikation mit FLAMESIZE aus, um die richtige Größe zu malen. Dazu ist noch nicht mal eine Abfrage zu machen, ob der Triebwerksstrahl überhaupt aktiv ist. Sollte das nämlich der Fall sein, ist JetLeft = 0 und damit wird die Textur mit der Höhe 0 gezeichnet – also garnicht. Das zweite Rechteck definiert den Ausschnitt aus der Quelltextur. Hier wird in Abhängigkeit des Animationsframes entweder richtig herum oder gespiegelt gewählt. Frame 0 würde hier 0/0/FLAMESIZE/FLAMESIZE verwendet werden, Frame 1 produziert hier den Ausschnitt FLAMESIZE/0/-FLAMESIZE/FLAMESIZE. Weiter hinten ist noch die Angabe der Rotation mit MathHelper.PiOver2 interessant. Dies bedeutet eine Rotation um 90 Grad im Uhrzeigersinn. Drehpunkt ist hier Vector2.Zero – das ist die linke, obere Ecke. Unter anderen Umständen (vielleicht bei der Rotation von Raumschiffen oder so) ist eine Rotation im Ursprung sinnvoller. Der wird dann einfach als Drehpunkt “new Vector2(FLAMESIZE / 2, FLAMESIZE / 2)” angegeben und schon passiert die Drehung im Mittelpunkt der Textur.

Schritt 6: Controller-Vibration

Als letzte Spielerei in diesem Tutorial möchte ich gerne alle beglücken, die über einen XBox-Controller am Computer verfügen. Dieser Controller kann nämlich auch vibrieren um dem Spieler Feedback über Spielabläufe zu liefern. Der Controller soll vibrieren, sobald die Jets am Taxi aktiviert wurden. Kinderspiel.

Die richtige Stelle dafür ist die Update-Methode – kurz nachdem wir die Jetpower für die einzelnen Düsen festgelegt haben. Daraus generieren wir nun auch die Stärke der Vibration. GamePad bietet uns nun eine Methode zum Setzen der Vibrationsstärke. Kleiner Hinweis: Das Ding vibriert solange, bis wir über diese Methode wieder alles auf 0 setzen (oder das Spiel beendet wird). Wir setzen daher immer die Vibration, auch wenn der Controller nicht mehr vibrieren sollte. Der erste Parameter bestimmt die Controller-Nummer. Ist nur ein Controller angeschlossen, betriefft es PlayerIndex.One. Sollte dein Spiel mit mehreren Controllern arbeiten, musst du hier natürlich darauf achten auch den richtigen Controller anzusprechen.

// Rumble-Controller 
float rumble = Math.Max(0,  
    Math.Max(taxi.JetRight,  
        Math.Max(taxi.JetBottom, taxi.JetLeft))); 
GamePad.SetVibration(PlayerIndex.One, rumble, rumble);

Und schon vibriert auch alles hübsch mit den Düsen zusammen! :)

So! Wir sind am Ende einer langen, langen Story von Spaß, Texturen, Sounds und was weiß ich alles. Ich hoffe, ihr hattet Spaß beim Durcharbeiten und habt schon 1000 eigene Ideen, was man mit dem neuen Wissen so alles anstellen kann! Lasst mich von euren Projekten wissen, stellt mir gerne Fragen, Anregungen und üble Worte zum Tutorial. Ich versuche das dann in zukünftigen Texten zu berücksichtigen.

Source zum Tutorial