RGSS/Tutorials/Rubykurs 3 - RGSS Teil 2
| Rubykurs | |
|---|---|
| Autor | Kai D |
| Thematik | Ruby |
| Vorraussetzungen | Programme: RPG-Maker XP Fähigkeiten: Grundlagen des Eventscripting im RPG-Maker XP |
| Andere Teile | |
Inhaltsverzeichnis
Optimierung von Scripten
Nicht selten ist es einfacher, ein eigenes Script erst hinterher zu optimieren, wenn schon alle wichtigen Funktionen laufen. Dabei stellt sich die Frage, warum ein Script überhaupt optimiert werden sollte. Hauptsache es läuft, könnte man sagen. Nichtsdestotrotz kann man mit Optimieren viel Arbeit sparen und ein besseres Ergebnis bekommen. Das Ziel der Optimierung ist:
- Das Script soll weniger Rechenzeit verschwenden und schneller ausgeführt werden
- Das Script soll dynamischer werden um es später für andere Scripte wiederzuverwenden
- Das Script soll übersichtlicher werden, um später noch den eigenen Code verstehen zu können
Unser Script ist denkbar schlecht gecodet. Von Objektorientierter Programmierung ist nichts zu sehen. Wir wollen ein neues Script, in dem wir, gemäß dem OOP-Prinzip, alle Aufgaben auf verschiedene Klassen verteilen, lange Aufgaben in verschiedene Methoden zersplitten und statische Inhalte dynamisch machen oder per Konstanten regeln.
Auslagern von Code in Klassen
Im Idealfall sollte unsere Scene_Charakterauswahl nicht anders aussehen, als unsere Scene_Test. Alle Instanzvariablen werden erzeugt, geupdated und falls nötig disposed. Alle anderen Funktionen werden auf die Instanzvariablen selbst ausgelagert.
Im Normalfall sieht ein Script folgendermaßen aus:
Das heißt: Das Hauptprogramm Main führt die Scene aus. Die Scene erzeugt eine Funktions- und eine Grafikklasse. Danach werden beide Klassen geupdated und am Ende der Scene die Grafikklasse disposed.
Warum die Trennung von Grafik und Funktion? Nehmen wir ein Beispiel: Die Events des Makers. Sie werden im Scripteditor durch zwei Klassen realisiert: Game_Event und Sprite_Character. Game_Event enthält die eigentliche Funktionsweise des Events, Sprite_Character ist ihre grafische Repräsentation. Würden wir die Klasse Sprite_Character "leeren", dann würden alle Events (sowie der Spieler) nicht mehr angezeigt werden. Dennoch wird ihre Funktionsweise in keiner Weise gestört.
Ihr könnt es ja mal in einem neuen Projekt ausprobieren. Fügt über Main einfach mal diesen Code ein:
class Sprite_Character < RPG::Sprite def initialize(a, b=nil) end def update() end def dispose() end end
Daraufhin werden die Events nicht mehr angezeigt. Aber abgesehen davon, dass ihr leichte Orientierungsprobleme haben werdet, funktioniert das Spiel fehlerfrei weiter.
Der eigentliche Ablauf des Spiels ist also von der grafischen Repräsentation völlig getrennt. Und das ist auch gut so. Denn auf diese Weise können wir manche Spielelemente selbst dann noch ablaufen lassen, wenn wir gar nicht wollen das sie grafisch angezeigt werden. Beispielsweise könnten wir, um Rechenspeicher zu sparen, allen Events, die sich weit weg von der aktuellen Position des Spielers befinden, die Grafikklasse entfernen, ohne dass sie in ihrer Funktionsweise eingeschränkt wären. Auch wäre es möglich, dass die Events in einer Stadt sich weiterbewegen, auch während der Spieler ein Haus betritt.
Es ist also sinnvoll Grafik und Funktion zu trennen. Dennoch wird es nicht konsequent überall durchgeführt. In Menüs beispielsweise wäre es dem Spieler unmöglich eine Eingabe zu machen, wenn er das Menü nicht sieht. Aus dem Grund bilden in den Menüs Grafik- und Funktion immer eine Einheit.
Da auch wir ein Auswahlmenü haben, werden wir das Menü nicht in Grafik und Funktion unterteilen, sondern eine Klasse für das Auswahlmenü schreiben. Diese Klasse werden wir als Subklasse von Sprite schreiben. Dennoch werden wir versuchen so viele Methoden wie möglich aus unserem Auswahlmenü auszulagern.
Dabei gehen wir folgendermaßen vor: Wir fertigen eine Liste mit allen Arbeitsschritten unseres Menüs an. Kleinere Arbeitsschritte, die keine Auslagerung wert sind, werde ich kursiv hinterlegen. Die Liste könnte in etwa so aussehen:
Menü wird geladen
- Sprite + Bitmap werden erzeugt und positioniert
Menü wird geupdated
- Tastenabfrage:
- Wenn rechts/links: Cursor wird bewegt
- wenn Enter:
- Heldentruppe wird geleert und ausgewählter Charakter wird in die Heldentruppe gesetzt
- Scene wechselt auf die Map
Auswahlbox wird angezeigt
- Farbe wird bestimmt, je nachdem ob Box ausgewählt ist
- Eine blaue Box wird gezeichnet
- Ein Battler + Text wird gezeichnet und mittig platziert
Menü wird beendet
- Grafiken werden gelöscht
Als nächstes schreiben wir hinter jedem Arbeitsschritt, zu welcher Klasse wir ihn einordnen würden. Dabei überlegen wir uns, wo dieser Arbeitsschritt noch gebraucht werden könnte, bzw. ob wir irgendwann einmal ein anderes Script schreiben würden, in dem dieser Arbeitsschritt ebenfalls vorkommt.
- Tastenabfrage: Wenn rechts/links: Cursor wird bewegt: Jedes Menü braucht eine Tastenabfrage mit Cursorbewegung. Dieser Arbeitsschritt gehört also zu jedem Auswahlmenü
- Heldentruppe wird geleert und ausgewählter Charakter wird in die Heldentruppe gesetzt: Dieser Arbeitsschritt gehört zur Klasse Game_Party, da sie für die Heldentruppe verantwortlich ist
- Eine blaue Box wird gezeichnet: Eine blaue Box könnten wir häufiger gebrauchen. Vielleicht wollen wir ja mal eine Textnachricht da reinschreiben o.ä. Nicht nur Menüs könnten davon profitieren, sondern auch viele andere Scripte. Es handelt sich hier um eine Zeichenfunktion, die wir daher der Bitmap Klasse zuordnen.
- Ein Battler + Text wird gezeichnet und mittig platziert: Das mittig Platzieren ist durchaus eine praktische Sache, die wir in eine andere Klasse einordnen könnten. Nur wo? Beim platzieren handelt es sich ja um eine Bitmapmethode, doch beim mittig platzieren geht es eher um die Position. Und Positionen werden von Rechtecken festgelegt. Wir ordnen das mittige Platzieren der Rect Klasse zu.
Wir werden nun die ausgelagerten Methoden in die jeweiligen Klassen schreiben.
Fangen wir mit der Game_Party Klasse an.
class Game_Party #-------------------------------------------------------------------------- # * Verändert die Heldengruppe #-------------------------------------------------------------------------- def set(helden) @actors = helden $game_player.refresh end end
Nun können wir auch unsere update Methode in Scene_Auswahlcharakter kürzen:
elsif Input.trigger?(Input::C)
$game_party.set([@charaktere[@cursor]])
$scene = Scene_Map.new
end
Als nächstes kommt die Bitmap Klasse:
class Bitmap #-------------------------------------------------------------------------- # * Zeichnet eine Box mit Rahmen und transparenten Inhalt #-------------------------------------------------------------------------- def zeichne_box(rechteck, farbe) #Erzeuge eine Kopie von farbe, deren Alphawert wir später verändern werden farbe = farbe.dup # sowie eine Kopie des rects, weil wir diesen ebenfalls verändern möchten rechteck = rechteck.dup #Fülle das gesamte Bitmap mit der blauen Farbe aus fill_rect(rechteck, farbe) #Als nächstes wollen wir ein inneres Bitmap, an den Rändern je 1 Pixel kleiner, ausfüllen rechteck.x += 1 rechteck.y += 1 rechteck.width -= 2 rechteck.height -= 2 #Die Farbe soll auf dieser Fläche transparent sein farbe.alpha = 155 #Zeichne die Innenfläche! fill_rect(rechteck, farbe) end end
Beachtet hierbei, dass wir nicht mehr @sprite.bitmap.fill_rect schreiben, da wir uns ja nun in der Bitmap Klasse befinden. Daher schreiben wir nur noch fill_rect.
Am Anfang erzeugen wir mit der bereits bekannten Object#dup Methode eine Kopie unseres Farbobjektes. Warum? Nun, weiter unten verändern wir den alpha-Wert der Farbe. Da farbe nur eine lokale Variable ist, die auf ein Color-Objekt zeigt, verändern wir mit farbe.alpha = 155 ein Color-Objekt, welches vielleicht irgendwo im Code noch einmal gebraucht wird. Beispielsweise könnten wir zwei Boxen nacheinander zeichnen wollen:
farbe = Color.new(100,0,0) bitmap = Bitmap.new(640, 480) bitmap.zeichne_box(Rect.new(0,0,100,100), farbe) bitmap.zeichne_box(Rect.new(200,0,100,100), farbe)
Angenommen wir würden das Farbobjekt in unserer Bitmap#zeichne_box nicht kopieren, dann würden wir beim ersten Aufruf von zeichne_box die farbe verändern (ihren alpha-Wert auf 155) setzen und beim zweiten Aufruf mit dem veränderten Farbobjekt weiterarbeiten. Wir hätten also einmal eine nichttransparente und eine halbtransparente Box, obwohl wir eigentlich zwei gleichaussehende Boxen zeichnen wollten. Das Gleiche gilt auch für den Rect. Wir verändern seine Koordinaten und Breite/Höhe in unserer Methode, also müssen wir vorher eine Kopie erzeugen. Schließlich wissen wir nicht, wofür der übertragene Rect sonst noch gebraucht werden könnte.
Es ist also wichtig Kopien von Objekten, die als Parameter an eine Methode geschickt werden, zu erzeugen, wenn diese dort verändert werden und eben dies nicht das primäre Ziel der Methode ist.
Und nun das mittige Platzieren:
class Rect #-------------------------------------------------------------------------- # * Platziert einen Rect mittig in diesen Rect #-------------------------------------------------------------------------- def zentriere_in!(rechteck) self.x = rechteck.x + (rechteck.width - self.width)/2 self.y = rechteck.y + (rechteck.height - self.height)/2 self end def zentriere_in(rechteck) dup.zentriere_in!(rechteck) end def untere_mitte!(rechteck) self.x = rechteck.x + (rechteck.width - self.width)/2 self.y = rechteck.y + rechteck.height- self.height self end def untere_mitte(rechteck) dup.untere_mitte!(rechteck) end end
An dieser Stelle haben wir nun jeweils zwei Methoden geschrieben: Einmal mit ! und einmal ohne. Wo liegt nun der Unterschied? Wir haben das im ersten Kurs bereits angesprochen: Das Ausrufezeichen weist darauf hin dass das Objekt geändert wird. Die Methode ohne das Ausrufezeichen erzeugt eine Kopie des Rects und verändert diese. Der Rect selbst bleibt also unangetastet, was manchmal auch sehr nützlich sein kann.
Nun kürzen wir also unsere Methode zum Zeichnen der Box:
def zeige_auswahlbox(charakter, x, ausgewaehlt=false) #Erzeuge die Farbe und den Rect farbe = if ausgewaehlt then Color.new(255,255,255) else Color.new(25,25,255) end rechteck = Rect.new(x,0,150,200) #Fülle nun das gesamte Bitmap mit der blauen Farbe aus @sprite.bitmap.zeichne_box(rechteck, farbe) #Zeichne die Charaktergrafik mittig(!) in die Box grafik = RPG::Cache.battler(charakter.battler_name, 0) mittig = grafik.rect.zentriere_in(rechteck) @sprite.bitmap.blt(mittig.x, mittig.y, grafik, grafik.rect) #Schreibe nun den Namen des Charakters mittig in die Box texthoehe = @sprite.bitmap.font.size + 2 #sicherheitshalber erhöhen wir die Schrifthöhe nochmal um 2 textrect = Rect.new(x, 0, rechteck.width, texthoehe).untere_mitte(rechteck) @sprite.bitmap.draw_text(textrect, charakter.name, 1) end
Insgesamt ist unser Code eher länger denn kürzer geworden. Aber das ist unwichtig. Wir können später, wenn wir auf ähnliche Probleme stoßen, die ausgelagerten Methoden wieder benutzen. Je mehr Scripte wir einbauen, desto besser macht sich sinnvolles Auslagern bezahlt. Zudem wird unser Hauptscript, die Charakterauswahl, immer kürzer und übersichtlicher.
Nun trennen wir noch die Scene_Test von unserer neuen Sprite_CharakterAuswahl und wir haben den ersten Optimierungsschritt abgeschlossen. Wir erzeugen erst einmal das Grundgerüst der Klasse:
class Sprite_CharakterAuswahl < Sprite def initialize end def refresh end def update end def dispose end end
Aus diesen vier Methoden besteht eigentlich jede Grafikklasse. initialize ist für das Initialisieren der Instanzvariablen zuständig. Außerdem legt sie die Attribute der Grafik (Position, Sichtbarkeit usw.) fest. refresh zeichnet das Bitmap. update aktualisiert die Grafik und überprüft, ob das Bitmap neu gezeichnet werden muss. dispose löscht die Grafik wieder.
Wir brauchen größtenteils eigentlich nur die Methodeninhalte der Scene übernehmen. Bedenkt aber, dass wir eine Subklasse von Sprite haben. Wir schreiben also nicht mehr @sprite = Sprite.new, da das Objekt ja selber ein Sprite ist. Stattdessen schreiben wir einfach nur super um die initialize Methode der Spriteklasse aufzurufen. Auf das Bitmap greifen wir mit self.bitmap statt @sprite.bitmap zu.
class Sprite_CharakterAuswahl < Sprite def initialize(charaktere) super() #erzeuge den Sprite self.x = 10 self.y = 150 self.bitmap = Bitmap.new(640, 480) @cursor = 0 @charaktere = charaktere refresh end def refresh x = 0 @charaktere.each_index do |index| zeige_auswahlbox(@charaktere[index], x, (index==@cursor)) x+= 160 end end def zeige_auswahlbox(charakter, x, ausgewaehlt) #Zeichne Box farbe = if ausgewaehlt then Color.new(255,255,255) else Color.new(25,25,255) end bitmap.zeichne_box(rechteck = Rect.new(x,0,150,200), farbe) #Zeichne Battler in Box grafik = RPG::Cache.battler(charakter.battler_name, 0) mittig = grafik.rect.zentriere_in(rechteck) bitmap.blt(mittig.x, mittig.y, grafik, grafik.rect) #Schreibe Charnamen in Box texthoehe = bitmap.font.size + 2 textrect = Rect.new(x, 0, rechteck.width, texthoehe).untere_mitte(rechteck) bitmap.draw_text(textrect, charakter.name, 1) end def update if Input.trigger?(Input::RIGHT) then if (@cursor += 1) > 3 then @cursor = 0 end refresh elsif Input.trigger?(Input::LEFT) then if (@cursor -= 1) < 0 then @cursor = 3 end refresh elsif Input.trigger?(Input::C) $game_party.set([@charaktere[@cursor]]) $scene = Scene_Map.new end end def dispose bitmap.dispose super() end end
Und nun unsere Scene:
class Scene_CharacterAuswahl #-------------------------------------------------------------------------- # * Ablauf der Szene (durch Main-Script aufgerufen) #-------------------------------------------------------------------------- def main() # Initialisierung der Instanzvariablen lade_grafiken() # Ausführen des Überblendeffektes Graphics.transition # Hauptschleife loop do # Aktualisieren der Grafiken Graphics.update # Aktualisieren der Tasteneingabe Input.update # Aktualisieren der Instanzvariablen update # Abbruch der Schleife, wenn eine neue Szene beginnt if $scene != self break end end # Bildschirm einfrieren für Überblendeffekt Graphics.freeze # Disposing der verwendeten Grafiken loesche_grafiken() end #-------------------------------------------------------------------------- # * Initialisierung der Instanzvariablen und Laden der Grafiken #-------------------------------------------------------------------------- def lade_grafiken() @auswahlmenu = Sprite_CharakterAuswahl.new($game_party.actors) end #-------------------------------------------------------------------------- # * Aktualisierung der Instanzvariablen #-------------------------------------------------------------------------- def update @auswahlmenu.update end #-------------------------------------------------------------------------- # * Disposen der Grafiken #-------------------------------------------------------------------------- def loesche_grafiken() @auswahlmenu.dispose end end
Jetzt sieht unsere Scene_CharakterAuswahl schon wesentlich schlanker aus. Wir haben den Code, der vorher in einer Klasse stand, nun auf die Bitmap-, Game_Party-, Scene_CharakterAuswahl- und Sprite_CharakterAuswahl verteilt. Als nächstes beschäftigen wir uns mit der Abstrahierung und Dynamisierung des Codes.
Wer nicht ganz mitgekommen ist, kann sich hier noch einmal das vollständige Script ansehen: RGSS/Tutorials/Rubykurs 3 - RGSS CharakterAuwahl#Auslagern von Code in Klassen.
Abstraktion
Unser Charakterauswahlmenü hat noch eine entscheidende Schwäche: Die Boxen haben eine genaue Größe von 150*200 Pixeln. Was ist, wenn der zu zeichnende Charakter größer ist? Nun, er wird dann einfach nicht in die Box reinpassen. Das heißt für uns: Wenn wir irgendwann einen neuen Charakter einfügen mit einer sehr großen Grafik, so müssen wir unser Auswahlmenü umschreiben.
Das zweite Problem ist: Was ist wenn wir 8 Charaktere zur Auswahl geben? In das Menü passen doch nur gerade so 4 Charaktere rein. Bei weiteren Charakteren gibt es Platzprobleme. Dieses Problem werden wir allerdings nicht im Umfang des Grundlagenkurses lösen, da es doch etwas anspruchsvoller ist.
Aber wir sehen, unser Script funktioniert nur, wenn wir unser Spiel an das Script anpassen. Dabei sollte es genau andersherum sein: Das Script soll sich unseren Bedürfnissen anpassen.
Genau das wollen wir jetzt im nächsten Optimierungsschritt erreichen. Versuchen wir das Problem mit der statischen Größe der Grafikbox zu lösen. Auf fixe Werte sollte man in einem Script möglichst verzichten. Wir wollen das Script so schreiben, dass es selbst bei Bildschirm großen Grafiken noch richtig funktioniert. Das ist auch nicht weiter schwer. Wir verlangen von unserem Script, dass die Auswahlboxen so groß sein sollen, wie die größte Charaktergrafik.
Wie sollte dazu der Code aussehen? Eigentlich ganz einfach:
def ermittle_boxgroesse #Erzeuge Array mit allen Charaktergrafiken boxweiten = @charaktere.collect do |charakter| RPG::Cache.battler(charakter.battler_name, 0).width end boxhoehen = @charaktere.collect do |charakter| RPG::Cache.battler(charakter.battler_name, 0).height end [boxweiten.max, boxhoehen.max] end
Hier sind wir wieder bei unseren geliebten Iteratoren. Der Iterator collect erzeugt einen neuen Array aus einem Bestehenden. Wir haben einen Array @charaktere, welcher alle Charaktere, die im Auswahlmenü angezeigt werden sollen, enthält. Aus diesem erzeugen wir nun einen neuen Array boxweiten, der die Weite aller Battlergrafiken, sowie einen neuen Array boxhoehen, der die Höhe aller Battlergrafiken dieser Charaktere enthält.
Aus beiden Arrays suchen wir nun jeweils das Maximum. Dafür verwenden wir den Iterator max. Ihm brauchen wir keinen Codeblock anhängen, da er Fixnumwerte von alleine sortieren kann (dies ist mit allen Objekten möglich, die <=> kennen).
Die beiden ermitteln Werte geben wir in Form eines Arrays als Rückgabewert aus. Warum als Array? Weil jede Methode nur einen einzigen Rückgabewert haben darf. Wollen wir mehrere Werte schreiben, so müssen wir sie als Array zurückgeben. Wir hätten auch return boxweiten.max, boxhoehen.max schreiben können, dass wäre auf's Gleiche hinausgelaufen.
Wo fügen wir diesen Code nun ein? Natürlich in die initialize Methode, schließlich muss nur ein einziges Mal die größte Grafik ermittelt werden.
#============================================================================== # ** Sprite_CharakterAuswahl #------------------------------------------------------------------------------ # Ein Sprite-Menu zur Auswahl eines Charakters #============================================================================== class Sprite_CharakterAuswahl < Sprite #-------------------------------------------------------------------------- # * Initialisiert das Menü # charaktere: Ein Game_Actor-Array mit den zur Auswahl stehenden Charakteren #-------------------------------------------------------------------------- def initialize(charaktere) super() #erzeuge den Sprite self.x = 10 self.y = 150 self.bitmap = Bitmap.new(640, 480) @cursor = 0 @charaktere = charaktere @boxweite, @boxhoehe = ermittle_boxgroesse() refresh end #-------------------------------------------------------------------------- # * Ermittelt die größten Grafiken aller Charaktergrafiken # und passt die Boxgröße entsprechend dieser an #-------------------------------------------------------------------------- def ermittle_boxgroesse #Erzeuge Array mit allen Charaktergrafiken boxweiten = @charaktere.collect do |charakter| RPG::Cache.battler(charakter.battler_name, 0).width end boxhoehen = @charaktere.collect do |charakter| RPG::Cache.battler(charakter.battler_name, 0).height end [boxweiten.max, boxhoehen.max] end end
Hier haben wir wieder ein Beispiel für Parallelzuweisung:
@boxweite, @boxhoehe = ermittle_boxgroesse()
Angenommen die Methode ermittle_boxgroessen hat als Rückgabewert den Array [100, 120], so wird @boxweite auf 100 und @boxhoehe auf 120 gesetzt. Wir könnten die beiden Instanzvariablen auch direkt in der Methode ermittle_boxgroesse setzen, aber es ist der Übersicht dienlich, wenn wir versuchen alle Instanzvariablen in der initialize-Methode zu belegen.
Wir haben bisher festgelegt, dass jede Box einen Abstand von 160 Pixeln zur nächsten hat. Da eine Box bisher immer 150 Pixel breit war, bestand die Lücke zwischen zwei Boxen immer 10 Pixel. Jetzt aber ist die Breite einer Box abhängig von der Größe der Grafiken. Daher müssen wir die den Abstand einer Grafik wie folgt berechnen: @boxweite + LUECKE. Wir könnten natürlich als Lücke wieder 10 einsetzen, stattdessen aber schreiben wir eine Konstante, welche die Breite einer Lücke zwischen zwei Boxen festlegt. Wenn wir später das Script abändern wollen, müssen wir nicht mehr im Code selbst nach Werten suchen, sondern brauchen nur die Konstanten abändern.
class Sprite_CharakterAuswahl < Sprite LUECKE = 10 #Breite der Lücke zwischen zwei Boxen in Pixeln #-------------------------------------------------------------------------- # * Aktualisiert alle Grafiken des Menüs #-------------------------------------------------------------------------- def refresh x = 0 @charaktere.each_index do |index| zeige_auswahlbox(@charaktere[index], x, (index==@cursor)) x+= @boxweite + LUECKE end end end
Zuletzt ändern wir noch die Zeile um, welche den Rect für die zeichne_box Methode erzeugt. Wir finden sie in der Sprite_CharakterAuswahl#zeige_auswahlbox Methode.
bitmap.zeichne_box(rechteck = Rect.new(x,0,150,200), farbe)
Wir ändern sie ab in
bitmap.zeichne_box(rechteck = Rect.new(x,0,@boxweite,@boxhoehe), farbe)
Nun schauen wir uns noch kurz die update-Methode an. wir sind bisher davon ausgegangen, dass im Menü eine Auswahl aus 4 Charakteren bestand. Was aber wenn die Auswahl größer oder kleiner ist? Dies müssen wir auch noch berücksichtigen.
def update if Input.trigger?(Input::RIGHT) then if (@cursor += 1) > (@charaktere.size-1) then @cursor = 0 end refresh elsif Input.trigger?(Input::LEFT) then if (@cursor -= 1) < 0 then @cursor = @charaktere.size-1 end refresh elsif Input.trigger?(Input::C) $game_party.set([@charaktere[@cursor]]) $scene = Scene_Map.new end end
Wir erinnern uns: Vorher stand, wo nun @charaktere.size-1 steht, eine 3. Die 3 heißt: der Charakter mit dem Index 3 ist der letzte im @charaktere Array.
Nun errechnen wir diesen Wert dynamisch mit der Array#size Methode, welche die Anzahl der Elemente zurückgibt. Da ein Array immer mit 0 anfängt, müssen wir den size-Wert noch um 1 verringern, damit er den Index des letzten Elementes angibt.
Damit hätten wir diesen Optimierungsschritt beendet. Wir können nun die Anzahl der zur Auswahl stehenden Charaktere verändern, deren Grafiken vergrößern etc. ohne das unser Script dabei verändert werden muss.
Wer nicht ganz mitgekommen ist, kann sich hier noch einmal das vollständige Script ansehen: Ruby/RGSS/Tutorials/Rubykurs/3 - RGSS/CharakterAuwahl#Abstraktion.
Auslagern des Codes in Superklassen
Der letzte Optimierungsschritt wird recht schnell gehen. Wir hatten uns beim Auslagern des Codes in Klassen schon einmal die Frage gestellt: Welche Methoden gehören eher zu einer anderen Klasse. Diesmal stellen wir uns die Frage: Welche Methoden könnten zu einer Superklasse gehören.
Fangen wir doch einmal mit unserer Scene an. Vergleichen wir die Scene_CharakterAuswahl mit der Scene_Test. Beide haben eine völlig identische main-Methode. Warum also diese main-Methode nicht in einer gemeinsamen Superklasse auslagern?
#============================================================================== # ** Scene #------------------------------------------------------------------------------ # Die Superklasse für unsere Szenen #============================================================================== class Scene #-------------------------------------------------------------------------- # * Ablauf der Szene (durch Main-Script aufgerufen) #-------------------------------------------------------------------------- def main() # Initialisierung der Instanzvariablen lade_grafiken() # Ausführen des Überblendeffektes Graphics.transition # Hauptschleife loop do # Aktualisieren der Grafiken Graphics.update # Aktualisieren der Tasteneingabe Input.update # Aktualisieren der Instanzvariablen update # Abbruch der Schleife, wenn eine neue Szene beginnt if $scene != self break end end # Bildschirm einfrieren für Überblendeffekt Graphics.freeze # Disposing der verwendeten Grafiken loesche_grafiken() end #-------------------------------------------------------------------------- # * Initialisierung der Instanzvariablen und Laden der Grafiken #-------------------------------------------------------------------------- def lade_grafiken() end #-------------------------------------------------------------------------- # * Aktualisierung der Instanzvariablen #-------------------------------------------------------------------------- def update() end #-------------------------------------------------------------------------- # * Disposen der Grafiken #-------------------------------------------------------------------------- def loesche_grafiken() end end
Nun können wir uns die main-Methode für eigene Szenen sparen und müssen sie nicht jedes Mal neu aufschreiben. Wir entfernen also die main-Methode aus unserer Scene_CharakterAuswahl und definieren die Klasse als Subklasse von Scene:
#============================================================================== # ** Scene_CharacterAuswahl #------------------------------------------------------------------------------ # Die Scene für eine Charakterauswahl #============================================================================== class Scene_CharakterAuswahl < Scene #-------------------------------------------------------------------------- # * Initialisierung der Instanzvariablen und Laden der Grafiken #-------------------------------------------------------------------------- def lade_grafiken() @auswahlmenu = Sprite_CharakterAuswahl.new($game_party.actors) end #-------------------------------------------------------------------------- # * Aktualisierung der Instanzvariablen #-------------------------------------------------------------------------- def update() @auswahlmenu.update end #-------------------------------------------------------------------------- # * Disposen der Grafiken #-------------------------------------------------------------------------- def loesche_grafiken() @auswahlmenu.dispose end end
Kommen wir zur Sprite_CharakterAuswahl. Sie ist ein Auswahlmenü für Charaktere. Wir werden aber später auch noch andere Auswahlmenüs schreiben. z.B. ein Auswahlmenü für Skills, Quests oder Items. Wir stellen uns also die Frage: Was gehört in jedes Auswahlmenü, was gehört NUR zu einem Charakterauswahlmenü.
- die Auswahlboxen gehören in jedes Auswahlmenü
- die Tastenabfrage gehört in jedes Auswahlmenü
- die Battlergrafik in den Boxen gehört nur zu einem Charakterauswahlmenü
- der Name des Charakters in den Boxen gehört nur zu einem Charakterauswahlmenü
- eine Beschreibung der Auswahlbox passt in jedes Auswahlmenü
- eine Grafik in der Box sollte auch in jedes Auswahlmenü
- jedes Auswahlmenü reagiert auf einen Druck der Entertaste
- aber nur Charakterauswahlmenüs verändern bei einem Druck auf Enter die Heldenpartie
- die Boxfarbe ist in jedem Menü ggf. unterschiedlich
- nicht jedes Menü muss an der selben Position wie das Charakterauswahlmenü angezeigt werden
- nicht jedes Menü muss so groß wie der ganze Bildschirm sein
Nun, das sollte ausreichen. Versuchen wir also unser Menü entsprechend in ein Sprite_Auswahl und ein Sprite_CharakterAuswahlmenu zu teilen. Wir erstellen die Sprite_Auswahl in dem wir den Code der Sprite_CharakterAuswahl einfach kopieren. Als nächstens benennen wir in der ganzen Klasse die Instanzvariable @charaktere in @elemente um. Schließlich besteht ein allgemeines Menü nicht unbedingt aus Charakteren. Nun fangen wir an Methoden für all jene Dinge zu schreiben, die in jedem Menü unterschiedlich sind: box_grafik(), box_beschriftung(), box_farbe() und enter().
Diese Methoden geben irgendwelche Dummy-Werte zurück. Sie werden dann von den Subklassen, wie z.B. Sprite_CharakterAuswahl überschrieben.
class Sprite_Auswahl < Sprite #-------------------------------------------------------------------------- # * Gibt die Grafik für die Box zurück # element: Element, für welches die Grafik zurücgegeben wird #-------------------------------------------------------------------------- def box_grafik(element) Bitmap.new(1,1) #leere Grafik end #-------------------------------------------------------------------------- # * Gibt die Beschriftung für die Box zurück # element: Element, für welches die Beschriftung zurücgegeben wird #-------------------------------------------------------------------------- def box_beschriftung(element) "" #leerer String end #-------------------------------------------------------------------------- # * Gibt die Boxfarbe aus # auswahl: Boolean, wenn true, so zeigt der Cursor gerade auf die Box #-------------------------------------------------------------------------- def box_farbe(element) Color.new(0,0,0,0) #transparente Farbe end #-------------------------------------------------------------------------- # * Wird aufgerufen, wenn Enter-Taste gedrückt #-------------------------------------------------------------------------- def enter() end end
Nun suchen wir im Skript alle Stellen, an denen wir mit Werten gearbeitet haben, die nur im Charakterauswahlmenü vorkommen und ersetzen sie durch unsere Methoden.
Unsere Methode zeige_auswahlbox() sieht nun folgendermaßen aus:
class Sprite_Auswahl #-------------------------------------------------------------------------- # * Zeigt eine Auswahlbox des Menüs an # element: Element, das gezeichnet werden soll # x: Fixnum das die Position der Auswahlbox angibt # ausgewaehlt: Boolean, true wenn Cursor auf die Auswahlbox zeigt #-------------------------------------------------------------------------- def zeige_auswahlbox(element, x, ausgewaehlt) #Zeichne Box farbe = boxfarbe(ausgewaehlt) bitmap.zeichne_box(rechteck = Rect.new(x,0,@boxweite,@boxhoehe), farbe) #Zeichne Battler in Box grafik = box_grafik(element) mittig = grafik.rect.zentriere_in(rechteck) bitmap.blt(mittig.x, mittig.y, grafik, grafik.rect) #Schreibe Charnamen in Box texthoehe = bitmap.font.size + 2 textrect = Rect.new(x, 0, rechteck.width, texthoehe).untere_mitte(rechteck) bitmap.draw_text(textrect, box_beschriftung(element), 1) end end
Wir sehen, überall wo vorher eine bestimmte Farbe, oder eine bestimmte Grafik stand, wurden nun unsere allgemeinen Methoden eingesetzt. Überschreibt eine Subklasse diese Methoden, so wirkt sich das auf den Inhalt der Boxen aus.
Unsere Methode ermittle_boxgroesse() sieht nun folgendermaßen aus:
class Sprite_Auswahl < Sprite #-------------------------------------------------------------------------- # * Ermittelt die größte Grafik aller Charaktergrafiken # und passt die Boxgröße entsprechend dieser an #-------------------------------------------------------------------------- def ermittle_boxgroesse() #Erzeuge Array mit allen Charaktergrafiken grafiken = @elemente.collect do |element| box_grafik(element) end boxweiten = grafiken.collect do |grafik| grafik.width end boxhoehen = grafiken.collect do |grafik| grafik.height end [boxweiten.max, boxhoehen.max] end end
Ich habe unsere vorherige Methode geringfügig modifiziert, in dem ich ein drittes collect eingefügt habe. Zuerst werden die Grafiken aller Elemente in den Array grafiken gespeichert. Danach werden die Weite aller grafiken in den Array boxweiten gespeichert und die Höhen aller grafiken in den Array boxhoehen. Aber der Grundplan der Methode ist gleich geblieben.
Auch unsere initialize-Methode sieht etwas anders aus:
class Sprite_Auswahl < Sprite #-------------------------------------------------------------------------- # * Initialisiert das Menü # rect: x,y Position des Menüs sowie dessen Breite und Höhe # elemente: Ein Element-Array mit den zur Auswahl stehenden Elementen #-------------------------------------------------------------------------- def initialize(rect, elemente) super() #erzeuge den Sprite self.x = rect.x self.y = rect.y self.bitmap = Bitmap.new(rect.width, rect.height) @cursor = 0 @elemente = elemente ermittle_boxgroesse @boxweite, @boxhoehe = ermittle_boxgroesse() refresh end end
Wir nehmen diesmal als ersten Parameter einen Rect, welcher angibt wo auf dem Bildschirm das Menü angezeigt werden soll und wie viel Platz es einnimmt.
Wir haben in unserer Klasse eine Konstante LUECKE. Nun kann aber jedes Menü ggf. eine andere Lückengröße haben. Wir haben hier zwei Alternativen: Entweder wir schreiben eine Methode luecke() welche die Länge der Lücke zurückgibt, oder wir belassen es bei einer Konstante. Ich habe mich für letzteres entschieden. Wichtig ist jetzt aber, dass jede Subklasse eine eigene Konstante LUECKE haben darf.
class Sprite_Auswahl < Sprite #-------------------------------------------------------------------------- # * Konstanten #-------------------------------------------------------------------------- LUECKE = 10 #Breite der Lücke in Pixeln #-------------------------------------------------------------------------- # * Aktualisiert alle Grafiken des Menüs #-------------------------------------------------------------------------- def refresh() x = 0 @elemente.each_index do |index| zeige_auswahlbox(@elemente[index], x, (index==@cursor)) x+= @boxweite + self.class::LUECKE end end end
Was bedeutet nun in der refresh Methode self.class::LUECKE? Nun, self.class gibt die Klasse zurück, der das Objekt angehört. Eine Instanz der Klasse Sprite_Auswahl wird also Sprite_Auswahl zurückgeben. Eine Instanz der Klasse Sprite_CharakterAuswahl wird eben Sprite_CharakterAuswahl als Wert zurückgeben. Wir haben außerdem gelernt, dass Klasse::Konstante auf die Konstante der Klasse zugreift. self.class::LUECKE holt also den Wert der Konstante LUECKE der aktuellen Klasse. Eine Instanz von Sprite_CharakterAuswahl wird also in der Klasse Sprite_CharakterAuswahl nach einer Konstante suchen. Wenn sie dort keine Konstante LUECKE findet, sucht sie in der Superklasse weiter. Würden wir nur LUECKE hinschreiben, so würde die Instanz sofort in Sprite_Auswahl suchen, weil die Methode refresh() sich eben in dieser Superklasse befindet.
In der Methode update() rufen wir für den Fall, dass Enter gedrückt wird, die enter() Methode auf. Das wäre dann schon alles. Unsere Superklasse sieht nun folgendermaßen aus:
#============================================================================== # ** Sprite_Auswahl #------------------------------------------------------------------------------ # Ein Sprite-Menu zur Auswahl #============================================================================== class Sprite_Auswahl < Sprite #-------------------------------------------------------------------------- # * Konstanten #-------------------------------------------------------------------------- LUECKE = 10 #Breite der Lücke zwischen zwei Boxen in Pixeln #-------------------------------------------------------------------------- # * Initialisiert das Menü # rect: x,y Position des Menüs sowie dessen Breite und Höhe # elemente: Ein Element-Array mit den zur Auswahl stehenden Elementen #-------------------------------------------------------------------------- def initialize(rect, elemente) super() #erzeuge den Sprite self.x = rect.x self.y = rect.y self.bitmap = Bitmap.new(rect.width, rect.height) @cursor = 0 @elemente = elemente @boxweite, @boxhoehe = ermittle_boxgroesse() refresh end #-------------------------------------------------------------------------- # * Aktualisiert das Menü (Tastenabfrage) #-------------------------------------------------------------------------- def update if Input.trigger?(Input::RIGHT) then if (@cursor += 1) > (@elemente.size-1) then @cursor = 0 end refresh elsif Input.trigger?(Input::LEFT) then if (@cursor -= 1) < 0 then @cursor = @elemente.size-1 end refresh elsif Input.trigger?(Input::C) enter() end end #-------------------------------------------------------------------------- # * Aktualisiert alle Grafiken des Menüs #-------------------------------------------------------------------------- def refresh() x = 0 @elemente.each_index do |index| zeige_auswahlbox(@elemente[index], x, (index==@cursor)) x+= @boxweite + self.class::LUECKE end end #-------------------------------------------------------------------------- # * Löscht die Grafiken des Menüs #-------------------------------------------------------------------------- def dispose bitmap.dispose super() end #-------------------------------------------------------------------------- # * Zeigt eine Auswahlbox des Menüs an # element: Element, das gezeichnet werden soll # x: Fixnum das die Position der Auswahlbox angibt # ausgewaehlt: Boolean, true wenn Cursor auf die Auswahlbox zeigt #-------------------------------------------------------------------------- def zeige_auswahlbox(element, x, ausgewaehlt) #Zeichne Box farbe = box_farbe(ausgewaehlt) bitmap.zeichne_box(rechteck = Rect.new(x,0,@boxweite,@boxhoehe), farbe) #Zeichne Battler in Box grafik = box_grafik(element) mittig = grafik.rect.zentriere_in(rechteck) bitmap.blt(mittig.x, mittig.y, grafik, grafik.rect) #Schreibe Charnamen in Box texthoehe = bitmap.font.size + 2 textrect = Rect.new(x, 0, rechteck.width, texthoehe).untere_mitte(rechteck) bitmap.draw_text(textrect, box_beschriftung(element), 1) end #-------------------------------------------------------------------------- # * Ermittelt die größte Grafik aller Charaktergrafiken # und passt die Boxgröße entsprechend dieser an #-------------------------------------------------------------------------- def ermittle_boxgroesse() #Erzeuge Array mit allen Charaktergrafiken grafiken = @elemente.collect do |element| box_grafik(element) end boxweiten = grafiken.collect do |grafik| grafik.width end boxhoehen = grafiken.collect do |grafik| grafik.height end [boxweiten.max, boxhoehen.max] end #-------------------------------------------------------------------------- # * Gibt die Grafik für die Box zurück # element: Element, für welches die Grafik zurücgegeben wird #-------------------------------------------------------------------------- def box_grafik(element) Bitmap.new(1,1) #leere Grafik end #-------------------------------------------------------------------------- # * Gibt die Beschriftung für die Box zurück # element: Element, für welches die Beschriftung zurücgegeben wird #-------------------------------------------------------------------------- def box_beschriftung(element) "" #leerer String end #-------------------------------------------------------------------------- # * Gibt die Boxfarbe aus # auswahl: Boolean, wenn true, so zeigt der Cursor gerade auf die Box #-------------------------------------------------------------------------- def box_farbe(element) Color.new(0,0,0,0) #transparente Farbe end #-------------------------------------------------------------------------- # * Wird aufgerufen, wenn Enter-Taste gedrückt #-------------------------------------------------------------------------- def enter() end end
Nun können wir unsere Sprite_CharakterAuswahl neu schreiben. Keine Sorge, es geht sehr schnell. Was unterscheidet unser Charakterauswahlmenü von jedem anderen Auswahlmenü? Die verwendeten Battlergrafiken, die Boxfarbe, die Boxbeschriftung, die Größe und Position des Menüs, sowie die Reaktion auf die Entertaste. Also überschreiben wir nur diese Methoden:
#============================================================================== # ** Sprite_CharakterAuswahl #------------------------------------------------------------------------------ # Ein Sprite-Menu zur Auswahl eines Charakters #============================================================================== class Sprite_CharakterAuswahl < Sprite_Auswahl #-------------------------------------------------------------------------- # * Initialisiert das CharakterAuswahl-Menü # charaktere: Ein Game_Actor-Array mit den zur Auswahl stehenden Charakteren #-------------------------------------------------------------------------- def initialize(charaktere) super(Rect.new(10, 150, 640, 480), charaktere) #erzeuge den Sprite end #-------------------------------------------------------------------------- # * Gibt die Grafik für die Box zurück # element: Element, für welches die Grafik zurücgegeben wird #-------------------------------------------------------------------------- def box_grafik(element) RPG::Cache.battler(element.battler_name, 0) end #-------------------------------------------------------------------------- # * Gibt die Beschriftung für die Box zurück # element: Element, für welches die Beschriftung zurücgegeben wird #-------------------------------------------------------------------------- def box_beschriftung(element) element.name() end #-------------------------------------------------------------------------- # * Gibt die Boxfarbe aus # auswahl: Boolean, wenn true, so zeigt der Cursor gerade auf die Box #-------------------------------------------------------------------------- def box_farbe(auswahl) if auswahl then Color.new(255,255,255) else Color.new(25,25,255) end end #-------------------------------------------------------------------------- # * Wird aufgerufen, wenn Enter-Taste gedrückt #-------------------------------------------------------------------------- def enter() $game_party.set([@elemente[@cursor]]) $scene = Scene_Map.new() end end
Damit wären wir fertig. Unser fertiges Menü könnt ihr hier noch einmal in voller Länge sehen:
Ruby/RGSS/Tutorials/Rubykurs/3 - RGSS/CharakterAuwahl#Auslagern des Codes in Superklassen
Interessant ist, dass unser eigentliches Menü mit zugehöriger Scene nur noch aus 29 Zeilen Code besteht (der Rest sind Kommentare). Alles andere sind die Superklassen und ausgelagerten Klassen, die zwar von unserem Menü genutzt werden, aber nicht direkt etwas damit zu tun haben.
Einige werden sich jetzt fragen: Hat sich der ganze Aufwand überhaupt gelohnt? Nun, stellen wir uns vor, der Spieler hat einen Auftrag erfolgreich abgeschlossen und darf sich zur Belohnung eines von 5 verschiedenen Items aussuchen. Wir wollen dies in einem Itemauswahlmenü realisieren. Nun, gehen wir ans Werk:
#============================================================================== # ** Scene_ItemAuswahl #------------------------------------------------------------------------------ # Startet eine Auswahl an Items #============================================================================== class Scene_ItemAuswahl < Scene #-------------------------------------------------------------------------- # * Startet das Menü #-------------------------------------------------------------------------- def initialize(items) @items = items end #-------------------------------------------------------------------------- # * Initialisierung der Instanzvariablen und Laden der Grafiken #-------------------------------------------------------------------------- def lade_grafiken() @itemmenu = Sprite_ItemAuswahl.new(@items) end #-------------------------------------------------------------------------- # * Aktualisierung der Instanzvariablen #-------------------------------------------------------------------------- def update() @itemmenu.update end #-------------------------------------------------------------------------- # * Disposen der Grafiken #-------------------------------------------------------------------------- def loesche_grafiken() @itemmenu.dispose end end #============================================================================== # ** Sprite_ItemAuswahl #------------------------------------------------------------------------------ # Gibt eine Auswahl an Items. Das ausgewählte Item wird dem Spieler gegeben. #============================================================================== class Sprite_ItemAuswahl < Sprite_Auswahl #-------------------------------------------------------------------------- # * Initialisiert das Itemauswahl-Menü # items: Ein Item-Array mit den zur Auswahl stehenden Items #-------------------------------------------------------------------------- def initialize(items) super(Rect.new(10, 150, 640, 480), items) #erzeuge den Sprite end #-------------------------------------------------------------------------- # * Gibt die Grafik für die Box zurück # item: RPG::Item/Weapon/Armor, für welches die Grafik zurückgegeben wird #-------------------------------------------------------------------------- def box_grafik(item) RPG::Cache.icon(item.icon_name) end #-------------------------------------------------------------------------- # * Gibt die Boxfarbe aus # auswahl: Boolean, wenn true, so zeigt der Cursor gerade auf die Box #-------------------------------------------------------------------------- def box_farbe(auswahl) if auswahl then Color.new(255,255,255) else Color.new(25,25,255) end end #-------------------------------------------------------------------------- # * Wird aufgerufen, wenn Enter-Taste gedrückt #-------------------------------------------------------------------------- def enter() item = @elemente[@cursor] if item.kind_of?(RPG::Weapon) then $game_party.gain_weapon(item.id, 1) elsif item.kind_of?(RPG::Item) then $game_party.gain_item(item.id, 1) else $game_party.gain_armor(item.id, 1) end $scene = Scene_Map.new() end end
Das Menü ist ohne Kommentare genau 33 Zeilen lang - also nicht viel größer als unser Charakterauswahlmenü. Wir können auf diese Wiese beliebig viele Menüs schreiben, in kurzer Zeit und mit wenig Code, in dem wir uns auf die Klassen stützen, die wir zuvor erstellt haben.
Allerdings sieht unser Auswahlmenü noch etwas langweilig aus. Wird Zeit, dass wir es grafisch noch etwas aufpeppen.
Planes
Nachdem wir die ganze Zeit über Sprites und Bitmaps sinniert haben, wollen wir uns einer neuen Grafikklasse zuwenden: den Planes.
Planes lassen sich am ehesten mit Tapeten vergleichen. Sie füllen eine Fläche auf einem Bildschirm mit einem Bitmap. Im Gegensatz zu Sprites zeigen sie das Bitmap nicht einmal an, sondern so oft bis der Platz, dem man dem Plane zugewiesen hat, ausgefüllt ist.
Planes begegnen uns im Maker in Form von Panoramen und Fogs. Vor allem an Fogs sieht man den Nutzwert von Planes deutlich. Stellt man einen 30*30 Pixelgroßen Fog ein, so wird dieser so oft hintereinander angezeigt, bis er den ganzen Bildschirm ausfüllt. Bewegt man den Plane, so bleibt die Fläche trotzdem ausgefüllt.
Planes lassen sich sehr einfach erstellen. Zum Experimentieren erzeugen wir uns wieder eine Scene_Test. Diesmal nutzen wir gleich die Superklasse Scene:
class Scene_Test < Scene def lade_grafiken() @plane = Plane.new() @plane.bitmap = RPG::Cache.battler("001-Fighter01", 0) end def update() if Input.trigger?(Input::C) then $scene = nil #Beende Spiel end end def loesche_grafiken() @plane.dispose() end end #Starte Szene: $scene = Scene_Test.new $scene.main #beende nach Ende der Szene exit()
Fügt diesen Code einfach in den Scripteditor ein (beachtet aber, dass vorher die Klasse Scene aus dem Charakterauswahlmenü im Scripteditor eingefügt werden muss) und startet den Maker. Die Szene wird sofort gestartet. Mit Druck der Entertaste wird das Spiel beendet.
Object#exit() ist übrigens eine Methode, die das Spiel sofort beendet. Zum Testen ganz nützlich, im eigentlichen Projekt werden wir aber immer die bessere Alternative $scene = nil verwenden.
Wir sehen, die Grafik bzw. das zu wiederholende Muster eines Planes lässt sich wie auch beim Sprite mit der Methode Plane#bitmap=() auswählen. Als nächstes wollen wir versuchen, den Plane zu bewegen.
Der Plane hat im Gegensatz zum Sprite keine x/y Koordinaten. Der Grund hierfür ist einleuchtend: Da ein Plane immer den ganzen Bildschirm einnimmt, ist es für ihn sinnlos auf dem Bildschirm positioniert zu werden. Aber der Plane hat Ursprungskoordinaten ox und oy.
Sie legen fest mit welchem Pixel des Bitmaps an der obersten linken Ecke des Bildschirms das Muster angefangen werden soll.
Um den Plane zu bewegen, verändern wir einfach kontinuierlich seine ox und oy Koordinaten. Der Plane kann sich im Gegensatz zum Sprite nicht aus dem Bildschirm herausbewegen, da er ja per Definition immer den ganzen Bildschirm einnimmt.
class Scene_Test < Scene def update() #Beende Scene wenn Entertaste gedrückt if Input.trigger?(Input::C) then $scene = nil #Beende Spiel end #Bewege Plane @plane.ox += 1 @plane.oy += 1 end end
Die Richtung und Schnelligkeit der Bewegung bestimmen wir mit den Zahlenwerten. Steigt ox, bewegt sich der Plane nach links. Steigt oy, so bewegt sich der Plane nach oben. Je stärker wir beide Werte addieren oder subtrahieren, desto schneller ist die Bewegung.
Anmerkung: Je kleiner die Grafik ist, die wir dem Plane zuweisen, desto mehr Rechenzeit verbraucht er. Wir sollten es also möglichst vermeiden Grafiken zu verwenden, die nur ein paar Pixel groß sind.
Viewports
Wir haben nun die Planes mit einer Tapete und die Sprites mit einem Gemälde verglichen. Fehlt noch die Wand, auf der die Tapete aufgetragen oder das Gemälde aufgehängt wird.
Diese Wand bezeichnet man als Viewport. Sie ist die Fläche, auf der Grafiken angezeigt werden. Jede Grafik darf einen Viewport erhalten. Geben wir keinen Viewport an, so wird automatisch der Standardviewport verwendet: der volle 640*480 Bildschirm.
Viewports sind im einfachsten Sinne nur ein Rect in dem Grafiken angezeigt werden. Wir erzeugen einen Viewport in dem wir als Parameter einen Rect, der die Größe und Position des Viewports festlegt, angibt. Alternativ können wir diese Werte auch in vier Parametern übergeben.
groesse = Rect.new(50,50,150,150) mein_viewport = Viewport.new(groesse) #oder mein_viewport = Viewport.new(50,50,150,150)
Wir weisen Grafikklassen einem Viewport zu, in dem wir den Viewport beim Erstellen eines Grafikobjekts als Parameter mitgeben.
viewport = Viewport.new(50,50,150,150) sprite = Sprite.new(viewport) plane = Plane.new(viewport)
Eine Grafik darf nicht aus ihrem Viewport hinausragen. Genauso wenig wie ein Sprite aus dem Bildschirm hinausragen kann. Viewports stellen also eine Möglichkeit dar, Grafikklassen zu zwingen nur auf einem festgelegten Bereich des Bildschirms angezeigt zu werden.
Typisches Anwendungsbeispiel: Wir haben einen Zweispielermodus mit Split-Screen. In diesem Fall darf es natürlich nicht passieren, das der eine Spieler sich nach links bewegt und plötzlich im Bildschirm des anderen Spielers landet. Mit Viewports lässt sich das leicht verhindern. Wir teilen den Bildschirm in zwei Viewports auf. Den einen Viewport erhält Spieler 1, das heißt all seine Grafiken werden auf diesem Viewport angezeigt. Nun dürfen diese Grafiken den halben Splitscreen-Bildschirm nicht verlassen.
Viewports stellen auch eine Möglichkeit dar, Grafikklassen zu gruppieren. Grafiken werden immer in Relation zum Viewport angezeigt. Die Koordinaten 0/0 eines Sprites z.B. beziehen sich auf die Koordinaten des Viewports. Ein Sprite mit den koordinaten 0/0 muss sich also nicht unbedingt in der oberen, linken Ecke des Bildschirms befinden. Er befindet sich nur in der oberen, linken Ecke des Viewports.
Haben wir also ein Menü, dass aus vielen Sprites aufgebaut ist, so müssten wir, wollten wir das gesamte Menü an einer anderen Stelle des Bildschirms anzeigen, sämtliche Grafiken bewegen. Da ist es einfacher, wir weisen allen Grafiken denselben Viewport zu, den wir danach jederzeit mitsamt den enthaltenen Grafiken bewegen können.
Auch beim Löschen von Grafiken zeigt sich das Gruppenverhalten von Viewports. Wird ein Viewport mit dispose gelöscht, werden automatisch alle Grafikobjekte, die dem Viewport zugewiesen sind, gelöscht. Man muss also nicht für jedes Grafikobjekt die dispose-Methode aufrufen (aber Achtung: Selbst erstellte Bitmaps sind davon nicht betroffen).
Eine weitere wichtige Aufgabe von Viewports ist die Festlegung der z-Achse von Grafiken. Wir haben bisher Grafiken nur über X und Y Koordinaten angezeigt. Was also ist die Z-Achse?
Wollen wir im Maker mehrere Pictures per Show Picture Befehl an der selben Stelle anzeigen, so werden Pictures mit der höheren ID über den Pictures mit der niedrigeren ID angezeigt.
Arbeiten wir mit RGSS-Grafikklassen, so können wir selbst festlegen welche Sprites vor oder hinter anderen Sprites angezeigt werden. Dafür gibt es den Z-Wert. Er lässt sich genauso verwenden wie die X und Y Koordinaten.
Wir können den Z-Wert bei Sprites, Planes, Windows und Viewports festlegen. Hat eine Grafik einen höheren Z-Wert als eine andere, so wird sie vor diesem angezeigt (wenn beide Grafiken sich überlappen). Haben zwei Grafik einen gleich hohen Z-Wert, so wird die zuerst erstellte Grafik hinter der später erstellten angezeigt.
Sind Grafiken einem Viewport zugewiesen, so gelten für alle Grafiken dieses Viewports die selben Regeln. Bei Grafiken verschiedener Viewports aber zählt nur der Z-Wert des Viewports.
Beispiel: Wir haben mehrere Grafiken, die zum Viewport menu gehören. Die Grafik cursor ist eine solche Grafik in menu und hat einen Z-Wert von 100. 'menu' hat einen z-Wert von 3. Nun zeigen wir ein Lebensbalken über den Heldencharakter an. Der Lebensbalken balken ist ein Sprite mit einem Z-Wert 20, der zum Viewport charakterinfos gehört. Dieser Viewport hat einen Z-Wert von 5.
Angenommen unser menu und der balken werden an der selben Position angezeigt. Welche Grafik überdeckt die andere?
Obwohl menu einen wesentlich höheren Z-Wert als balken hat, wird balken vor menu angezeigt. Da beide Grafiken aus verschiedenen Viewports stammen, betrachten wir nämlich nicht mehr die Z-Werte der Grafiken, sondern nur noch die der Viewports. Und charakterinfos hat nunmal einen höheren Z-Wert als menu.
Was passiert wenn zwei Viewports den gleichen Z-Wert haben? Nun, das Gleiche wie bei Grafiken: Der zuerst erstellte Viewport hat Vorrang.
Was für einen Vorteil wir aus dieser Situation ziehen sollte auch klar sein. Wenn wir alle Grafiken eines Menüs in einen Viewport legen, entscheidet nur noch der Z-Wert des Viewports über ihre Z-Achse. Es kann also nicht mehr passieren das ein Lebensbalken zwischen dem Cursor und dem Menüpunkt angezeigt wird. Entweder liegt das ganze Menü unter dem Lebensbalken, oder das ganze Menü liegt über den Lebensbalken. Eine andere Möglichkeit gibt es nicht.
Anwendungsbeispiel: Lauftexte
Klassendefinition
Kommen wir zu einem, zugegeben weniger typischen, Anwendungsbeispiel für Viewports: Wir erweitern unser Menü um einen Lauftext.
Wir erinnern uns, dass man bei einem Plane nicht festlegen kann, welche Größe er hat. Er erstreckt sich eben über den ganzen Bildschirm, pardon, jetzt wissen wir es besser: er erstreckt sich über den ganzen Viewport. Genauso wie eine Tapete sich über die ganze Wand erstreckt.
Wir können also die Größe eines Planes festlegen, in dem wir den Plane in einen Viewport sperren. Schreiben wir also eine eigene Lauftextklasse.
#============================================================================== # ** Lauftext #------------------------------------------------------------------------------ # Zeigt einen Lauftext an #============================================================================== class Lauftext attr_accessor :geschwindigkeit #-------------------------------------------------------------------------- # * Initialisiert den Lauftext #-------------------------------------------------------------------------- def initialize(x, y, width, height, text="", textabstand=10) @text = text @viewport = Viewport.new(Rect.new(x,y,width,height)) @plane = Plane.new(@viewport) @plane.bitmap = Bitmap.new(1,1) @text_abstand = textabstand @geschwindigkeit = 1 end #-------------------------------------------------------------------------- # * Zeigt die Grafiken des Lauftextes an #-------------------------------------------------------------------------- def refresh() #Ermittle Größe des Textes text_rect = @plane.bitmap.text_size(@text) #Füge Abstand hinzu text_rect.width += @text_abstand #Logischerweise darf das Bitmap nicht weniger hoch sein als der Viewport #sonst würde der Text ja auch mehrmals übereinander angezeigt, wie #es für Planes üblich ist text_rect.height = @viewport.rect.height #Überprüfe ob der Text-Rect eine gewisse Mindestgröße einhält #Denn zu kleine Planebitmaps führen zu Performanceproblemen text_rect.width = 10 if text_rect.width < 10 text_rect.height = 10 if text_rect.height < 10 #überprübe ob Bitmap neu angepasst werden muss if @plane.bitmap.rect != text_rect then #Falls ja, dann dispose Bitmap und erzeuge ein neues @plane.bitmap.dispose @plane.bitmap = Bitmap.new(text_rect.width, text_rect.height) else #ansonsten leere aktuelles Bitmap einfach @plane.bitmap.clear end #Nun zeichne Text in das Bitmap @plane.bitmap.draw_text(text_rect, @text) end #-------------------------------------------------------------------------- # * Bewegt den Lauftext #-------------------------------------------------------------------------- def update if (@geschwindigkeit >= 0) then @plane.ox += @geschwindigkeit elsif (Graphics.frame_count % @geschwindigkeit.abs == 0) then @plane.ox += 1 end end #-------------------------------------------------------------------------- # * Löscht den Lauftext #-------------------------------------------------------------------------- def dispose @plane.bitmap.dispose @viewport.dispose end #-------------------------------------------------------------------------- # * Setter- und Gettermethoden für Koordinaten und Text #-------------------------------------------------------------------------- def x @viewport.rect.x end def x=(neuer_wert) @viewport.rect.x = neuer_wert end def y @viewport.rect.y end def y=(neuer_wert) @viewport.rect.y = neuer_wert end def text @text end #-------------------------------------------------------------------------- # * Wird der Text verändert, muss der Lauftext neu gezeichnet werden #-------------------------------------------------------------------------- def text=(neuer_wert) if (@text != neuer_wert) then @text = neuer_wert refresh end neuer_wert end end
Okay, das sieht wieder wie ein sehr langer Code aus, das meiste ist aber trivial.
Das Prinzip dieser Lauftextklasse ist folgendes: Wir erstellen ein Viewport und einen Plane, dem wir unser Viewport zuweisen. Wenn wir unser Viewport eine große Länge aber eine kleine Höhe geben, sieht der entstehende Plane dann wie ein Balken aus. Nun weisen wir dem Plane noch ein Bitmap zu und schreiben in das Bitmap einen Text. Wir updaten die Lauftextklasse jeden Frame und bewegen dabei die Ursprungskoordinaten des Planes. Das Resultat: Der Text im Plane wandert nach links, der Text bewegt sich also.
Gehen wir den Code Schritt für Schritt durch.
class Lauftext attr_accessor :geschwindigkeit #-------------------------------------------------------------------------- # * Initialisiert den Lauftext #-------------------------------------------------------------------------- def initialize(x, y, width, height, text="", textabstand=10) @text = text @viewport = Viewport.new(Rect.new(x,y,width,height)) @plane = Plane.new(@viewport) @plane.bitmap = Bitmap.new(1,1) @text_abstand = textabstand @geschwindigkeit = 1 end end
Das dürfte weitgehend trivial sein. Wir verlangen 5 Parameter, wobei der letzte optional ist. Dann definieren wir unsere Instanzvariablen und weisen ihnen die Werte aus den Parametern zu.
Wichtig ist natürlich, dass an der Stelle unser Viewport, unser Plane und dessen Bitmap erstellt werden. Dem Plane wird unser Viewport zugewiesen (wichtig! Das darf nur beim Erstellen des Planes geschehen. Danach gibt es keine Möglichkeit dem Grafikobjekt einen neuen Viewport zuzuweisen). Das Bitmap erstellen wir sicherheitshalber auch schon, obwohl es noch keine Rolle spielt und später eh neu erstellt werden muss, da wir nicht wissen wie groß unser Text ist. Aus diesem Grund erstellen wir auch nur solch ein kleines Bitmap - es soll ohnehin nur dazu dienen die Größe des Textes herauszufinden und muss daher nicht groß sein. Auch an dieser Stelle ein Hinweis: Ein Bitmap muss mindestens 1*1 Pixel groß sein, sonst gibt es eine Fehlermeldung.
Damit der Benutzer jederzeit die Textgeschwindigkeit anpassen kann, schreiben wir noch die attr_accessor Methode.
class Lauftext #-------------------------------------------------------------------------- # * Zeigt die Grafiken des Lauftextes an #-------------------------------------------------------------------------- def refresh() #Ermittle Größe des Textes text_rect = @plane.bitmap.text_size(@text) #Füge Abstand hinzu text_rect.width += @text_abstand #Logischerweise darf das Bitmap nicht weniger hoch sein als der Viewport #sonst würde der Text ja auch mehrmals übereinander angezeigt, wie #es für Planes üblich ist text_rect.height = @viewport.rect.height #Überprüfe ob der Text-Rect eine gewisse Mindestgröße einhält #Denn zu kleine Planebitmaps führen zu Performanceproblemen text_rect.width = 10 if text_rect.width < 10 text_rect.height = 10 if text_rect.height < 10 #überprübe ob Bitmap neu angepasst werden muss if @plane.bitmap.rect != text_rect then #Falls ja, dann dispose Bitmap und erzeuge ein neues @plane.bitmap.dispose @plane.bitmap = Bitmap.new(text_rect.width, text_rect.height) else #ansonsten leere aktuelles Bitmap einfach @plane.bitmap.clear end #Nun zeichne Text in das Bitmap @plane.bitmap.draw_text(text_rect, @text) end end
Diese Methode ist der eigentliche Kern unserer Klasse. Sie wird immer dann aufgerufen, wenn der anzuzeigende Text geändert wird. Am Anfang finden wir mit der Bitmap#text_size Methode heraus, wie groß unser Text ist. Zu der Länge des Textes fügen wir noch den Abstandswert hinzu, der zwischen dem Text und dessen Wiederholung steht.
Die so ermittelte Textgröße soll später angeben, wie groß das Bitmap unseres Planes sein soll. Doch was passiert, wenn das Bitmap nur halb so groß wie der Viewport wird? Nun, da ein Plane alles wiederholt, würde er das Bitmap dann zweimal untereinander anzeigen. Wir hätten dann zwei Texte untereinander, was wir natürlich nicht wollen. Daher setzen wir die Höhe unseres Textgrößerects auf die Höhe des Viewports.
Da allerdings zu kleine Planebitmaps Performanceprobleme erzeugen, sollten wir darauf achten dass unser Planebitmap mindestens 10*10 Pixel groß ist. Das fragen wir schnell mit zwei If-Sätzen ab.
Zuletzt vergleichen wir unseren Textgrößerect mit dem Rect des Planebitmaps. Sind die Rects verschieden groß, löschen wir das Bitmap unseres Planes und erstellen ein neues, welches sich der Größe des Textes angepasst hat
Nun, wo unser Bitmap die richtige Größe hat, brauchen wir nur noch den Text zeichnen zu lassen.
class Lauftext #-------------------------------------------------------------------------- # * Bewegt den Lauftext #-------------------------------------------------------------------------- def update if (@geschwindigkeit >= 0) then @plane.ox += @geschwindigkeit elsif (Graphics.frame_count % @geschwindigkeit.abs == 0) then @plane.ox += 1 end end end
Jeden Frame soll der Text sich nach links bewegen. Kein Problem, wir verändern einfach die Ursprungskoordinaten unsers Planes in der update-Methode. Damit der Benutzer auch auswählen kann, wie schnell der Text sich bewegen soll, geben wir die Erhöhung anhand unseres Textgeschwindigkeitsattributs an.
Das Problem an der Sache: Lassen wir den Geschwindigkeitswert bei 1, so bewegt sich der Text um 40 Pixel pro Sekunde. Das ist nicht gerade langsam. Wir müssen also eine Möglichkeit bieten, wie man den Text noch langsamer bewegen lassen kann. Ich habe hier die Möglichkeit implementiert, negative Geschwindigkeiten anzugeben. Was bewirken die?
Graphics.frame_count ist eine Methode, welche die Nummer des aktuellen Frames angibt. Diese Nummer steigt jeden Frame um 1 an. Bei einer Geschwindigkeit von -3 soll der Text nur noch jeden dritten Frame um 1 bewegt werden. Numeric#abs ist eine Methode, die den Betrag einer Zahl zurückgibt. (-5).abs ergibt also +5. +8.abs ergibt wieder +8. Sie wandelt also alle Zahlen ins positive um. Wir nutzen das, um statt jeden -3ten Frame jeden 3ten Frame zu warten. Doch wann ist jeder dritte Frame? Das finden wir problemlos durch die Modulo-Methode raus. Eine Zahl a ist immer dann durch eine Zahl b teilbar, wenn a ein Vielfaches von b ist. In unserem Fall heißt das, der Framezähler ist immer dann ohne Rest durch 3 teilbar, wenn er ein vielfaches von 3 ist. Und das ist immer nach 3 Frames der Fall.
Ob eine Zahl durch eine andere ohne Rest teilbar ist, überprüfen wir mit dem modulo-Operator. Ist dessen Ergebnis gleich 0, gibt es keinen Rest. In diesem Fall, der nur nach 3 Frames auftritt, oder wie auch immer der Wert unserer @geschwindigkeit Variable ist, erhöhen wir die Ursprungskoordinaten um 1.
class Lauftext #-------------------------------------------------------------------------- # * Löscht den Lauftext #-------------------------------------------------------------------------- def dispose @plane.bitmap.dispose @viewport.dispose end end
Wird der Lauftext disposed, so müssen wir nur das Bitmap und den Viewport löschen. Da der Plane im Viewport angezeigt wird, wird er automatisch zusammen mit dem Viewport gelöscht.
Die restlichen Methoden sorgen dafür, dass wir die Koordinaten des Lauftextes ändern können. Dabei ändert sich jeweils nur der Rect des Viewports.
class Lauftext #-------------------------------------------------------------------------- # * Wird der Text verändert, muss der Lauftext neu gezeichnet werden #-------------------------------------------------------------------------- def text=(neuer_wert) if (@text != neuer_wert) then @text = neuer_wert refresh end neuer_wert end end
Diese Settermethode ist dagegen etwas spezieller. Wenn wir nämlich den Text ändern, muss der Lauftext neu gezeichnet werden. Damit dies nur geschieht, wenn der neu zugewiesene Text auch wirklich anders als der bisherige ist, setzen wir dies in eine If-Abfrage.
Implementierung in unser Menü
Damit haben wir eine Klasse, die einen Lauftext anzeigt. Als nächstes bauen wir diesen Lauftext in unser Menü ein.
Zuerst erweitern wir die initialize Methode unsere Sprite_Auswahl Klasse.
#============================================================================== # ** Sprite_Auswahl #------------------------------------------------------------------------------ # Ein Sprite-Menu zur Auswahl #============================================================================== class Sprite_Auswahl < Sprite #-------------------------------------------------------------------------- # * Konstanten #-------------------------------------------------------------------------- LUECKE = 10 #Breite der Lücke zwischen zwei Boxen in Pixeln #-------------------------------------------------------------------------- # * Initialisiert das Menü # rect: x,y Position des Menüs sowie dessen Breite und Höhe # elemente: Ein Element-Array mit den zur Auswahl stehenden Elementen #-------------------------------------------------------------------------- def initialize(rect, elemente) super() #erzeuge den Sprite self.x = rect.x self.y = rect.y self.bitmap = Bitmap.new(rect.width, rect.height) @cursor = 0 @elemente = elemente @boxweite, @boxhoehe = ermittle_boxgroesse() @lauftext = Lauftext.new(x, y+@boxhoehe, rect.width, bitmap.font.size+2, lauftext_beschriftung(@elemente[@cursor])) refresh end end
Wir erzeugen dort eine neue Instanz der Klasse Lauftext und weisen ihr eine neue Instanzvariable, die wir @lauftext nennen, zu. Die x-Koordinate unseres Lauftexts entspricht der x-Koordinate des Menüs. Da wir aber den Lauftext direkt unter den Boxen angezeigt haben wollen, nehmen wir als Y-Koordinate die Y-Koordinate des Menüs addiert mit der Höhe der Boxen. Als Weite nehmen wir die Weite des ganzen Menüs, die Höhe entspricht der Höhe eines normalen Textes. Wir erhalten ihn aus der Font-Größe, wobei wir sicherhaltshalber noch 2 hinzuaddieren, falls doch mal ein Font größerer Buchstaben als erwartet haben sollte.
Was machen wir mit der Beschriftung? Nun, wir müssen eine Methode Sprite_Auswahl#lauftext_beschriftung definieren, die zu dem jeweiligen Element dessen Beschriftung ausgibt. Wir schreiben die Methode erst einmal hin und weisen ihr den Parameter des ersten Elements zu.
Wie könnte diese Methode aussehen? Naja, in der Superklasse nicht anders als unsere element_beschriftung() Methode, nämlich leer.
class Sprite_Auswahl < Sprite def lauftext_beschriftung(element) "" #leerer String end end
Wir können ihn dann in der Subklasse überschreiben. Als nächstes aber müssen wir dafür sorgen das unser Lauftext jeden Frame geupdatet und ihm der Text des aktuellen Elements zugewiesen wird.
class Sprite_Auswahl < Sprite #-------------------------------------------------------------------------- # * Aktualisiert das Menü (Tastenabfrage) #-------------------------------------------------------------------------- def update if Input.trigger?(Input::RIGHT) then if (@cursor += 1) > (@elemente.size-1) then @cursor = 0 end refresh elsif Input.trigger?(Input::LEFT) then if (@cursor -= 1) < 0 then @cursor = @elemente.size-1 end refresh elsif Input.trigger?(Input::C) enter() end @lauftext.text = lauftext_beschriftung(@elemente[@cursor]) @lauftext.update end end
Der Code sollte trivial sein. Wir weisen das neue Text-Element mit der Sprite_Auswahl#lauftext_beschriftung() Methode zu und updaten den Lauftext (damit dieser sich bewegt).
Zuletzt muss der Lauftext beim Beenden des Menüs disposed werden.
class Sprite_Auswahl < Sprite #-------------------------------------------------------------------------- # * Löscht die Grafiken des Menüs #-------------------------------------------------------------------------- def dispose bitmap.dispose @lauftext.dispose super() end end
Erweitern wir noch unsere Subklasse Sprite_CharakterAuswahl um eine spezifische Sprite_CharakterAuswahl#lauftext_beschriftung Methode.
class Sprite_CharakterAuswahl < Sprite_Auswahl def lauftext_beschriftung(element) case element.id when 1 then "Aluxes ist ein starker Krieger, der vor allem auf die Kraft seines Schwertes setzt" when 2 then "Basil ist ein gefürchteter Lanzenkämpfer mit starken Angriffen und guter Defensive" #when 3... when 4.. kann beliebig erweitert werden when 7 then "Gloria ist eine Klerikerin, die daher mehr auf passive und unterstützende Magie setzt" when 8 then "Hildas Magie ist offensiv veranlagt. Sie beherrscht mächtige flächendeckende Zauber" else super(element) #leerer String, falls gar nichts von oben zutrifft end end end
Zugegeben, das ist keine schöne Lösung. Wir müssten die Strings eigentlich als Konstanten der Klasse Game_Actor schreiben, aber belassen wir es erstmal dabei.
Zuletzt fügen wir noch einen coolen Nebeleffekt in unser Menü ein. Da er nicht wirklich etwas mit einem Auswahlmenü zu tun hat, erweitern wir lieber unsere Scene um diesen Effekt:
#============================================================================== # ** Scene_CharacterAuswahl #------------------------------------------------------------------------------ # Die Scene für eine Charakterauswahl #============================================================================== class Scene_CharakterAuswahl < Scene NEBEL = "001-Fog01" #-------------------------------------------------------------------------- # * Initialisierung der Instanzvariablen und Laden der Grafiken #-------------------------------------------------------------------------- def lade_grafiken() @nebel = Plane.new() @nebel.bitmap = RPG::Cache.fog(NEBEL, 0) @nebel.opacity = 100 @auswahlmenu = Sprite_CharakterAuswahl.new($game_party.actors) end #-------------------------------------------------------------------------- # * Aktualisierung der Instanzvariablen #-------------------------------------------------------------------------- def update() @nebel.ox -= 1 @nebel.oy -= 1 @auswahlmenu.update end #-------------------------------------------------------------------------- # * Disposen der Grafiken #-------------------------------------------------------------------------- def loesche_grafiken() @nebel.dispose @auswahlmenu.dispose end end
Der Code dürfte trivial sein (das ist in der Endphase eines Scriptes glücklicherweise fast immer der Fall). In der Scene_CharakterAuswahl#lade_grafiken Methode erzeugen wir eine Instanz der Klasse Plane und weisen ihr ein Bitmap aus dem RTP zu. Damit der Nebel nicht ganz so hell ist, geben wir ihm etwas Transparenz, in dem wir die Setter-Methode Plane#opacity auf 100 setzen (wir erinnern uns: 0 = unsichtbar, 255 = vollkommen sichtbar. Alles dazwischen = transparent).
Damit der Nebel sich nach unten rechts bewegt verringern wir seine ox/oy Werte jeden Frame um 1. Und zu guter Letzt disposen wir ihn, sobald das Menü beendet wurde.
So sieht unser Menü letztlich aus - na, das kann sich doch sehen lassen.


