Lieber Besucher, herzlich willkommen bei: RPG Studio - Make your World real. Falls dies Ihr erster Besuch auf dieser Seite ist, lesen Sie sich bitte die Hilfe durch. Dort wird Ihnen die Bedienung dieser Seite näher erläutert. Darüber hinaus sollten Sie sich registrieren, um alle Funktionen dieser Seite nutzen zu können. Benutzen Sie das Registrierungsformular, um sich zu registrieren oder informieren Sie sich ausführlich über den Registrierungsvorgang. Falls Sie sich bereits zu einem früheren Zeitpunkt registriert haben, können Sie sich hier anmelden.

1

Donnerstag, 23. Dezember 2010, 03:01

F12 - Neustarten des Projekts ohne Komplikationen

Von dem Problem hört man ja oft. Man drückt in seinem Projekt die F12 Taste und bekommt prompt folgende Fehlermeldung:

Quellcode

1
SystemStackError: stack level too deep

Auch bekannt als Alias-Bug. Die Bezeichnung finde ich etwas unpassend, weil es eigentlich überhaupt kein Bug ist. Aliasing verhält sich genau so wie es sollte. Das Problem liegt woanders. Ein Drücken der F12 Taste löst den Neustart des Spiels aus. Dies geschieht, in dem der Scripteditor als Ganzes neu eingelesen und ausgeführt wird. Das wäre alles nicht weiter tragisch, würde auch der Interpreter neu gestartet werden. Das passiert aber nicht. Das heißt: Der gesamte Inhalt des Programms, alle Variablen, Objekte, Klassen, bleibt erhalten. Nur der Programmcode wird neu ausgeführt. Das führt zu unzähligen möglichen Problemen. Der SystemStackError ist nur einer davon, aber vermutlich der häufigste.
Angenommen man hat eine Methode foo. Man aliast diese unter einem neuen Namen foo_copy. Nun ersetzt man foo und fügt dort einen eigenen Code plus einen Aufruf von foo_copy ein. Dies ist eine typische Vorgehensweise beim Schreiben von Scripten, da man dadurch eigenen Code in Methoden einfügen kann, ohne dabei den Code anderer Scripte (die hoffentlich auch alias nutzen) zu überschreiben. Das Problem: Führt man noch einmal einen alias auf foo zu foo_copy aus, so erhält man eine Methode foo_copy, welche denselben Programmcode wie das zuvor veränderte foo enthält, vor allem den Aufruf von foo_copy. Man erhält also eine Methode die sich selbst aufruft. Das führt zu einer Endlosrekursion, bis irgendwann der Interpreter die Schnauze voll hat und das Programm mit einem SystemStackError beendet.

Wie verhindert man das? Am häufigsten trifft man folgendes Programmkonstrukt:

Ruby Quellcode

1
2
3
class MeineKlasse
  alias eine_methode_copy eine_methode unless method_defined? :eine_methode_copy
end

Noch hässlicher:

Ruby Quellcode

1
2
3
4
class MeineKlasse
  alias eine_methode_copy eine_methode unless $eine_methode_copy_defined
  $eine_methode_copy_defined = true
end


Mir gefallen beide Konstrukte nicht. Warum? Weil sie das Problem an der falschen Stelle angehen. Wenn es ins Haus reinregnet, weil das Dach kaputt ist, kann man zwar einen Eimer hinstellen. Das hilft für's erste. Aber besser ist natürlich, man lässt das Dach reparieren. Kurzum: Das Problem an der Wurzel packen.

Was ist die Wurzel? Na, das Neuladen des Scripteditors! Als erstes sollte man sich die Frage stellen, wie der Rubyinterpreter das überhaupt macht. Das Neustarten des Scripteditors ohne den Interpreter selbst neuzustarten. Das ist nämlich keinesfalls trivial. Er muss praktisch aus dem Script rausspringen. Das ließe sich über Threads lösen (auf eine sehr hässliche Art und Weise), aber die naheliegenste Lösung ist ein Sprungbefehl, der aus dem Programmcode rausspringt. Nun gibt es in Ruby nur einen einzigen Sprungbefehl, der sowas vermag: Die Exceptions! Also habe ich einfach mal geprüft ob beim Drücken der F12 Taste eine Exception geworfen wird. Zu diesem Zweck muss man einfach dem Main-Script sagen, es soll alle Exceptions abfangen, und beim Wurf einer Exception dessen Namen ausgeben. Und tatsächlich: Das Drücken der F12-Taste wirft die Exception Reset. Nun kann man sich darüber ärgern, dass dieser Umstand wie so vieles nicht in der Dokumentation des Makers zu finden ist. Man kann sich aber auch freuen, dass das Problem nun so einfach zu lösen ist.

Edit: Oder auch nicht ^^ Die Reset-Exception Klasse wird erst erzeugt, wenn sie gebraucht wird. Darum kommt es zum NameError wenn man das Programm über eine andere Exception verlässt. Scheinbar macht es aber auch nichts aus, wenn man sie vorher selbst manuell erzeugt. Das zweite Problem ist eher kosmetischer Natur: Die Sprites werden nicht disposed -_- Hier muss man manuell nachhelfen und alle Sprites per Hand löschen. Das habe ich, etwas unsauber, über den ObjectSpace gelöst. Auch hier wieder was neues gelernt: Obwohl in der RGSS-Doku steht, dass Viewport die Methode disposed? enthält, existiert diese Methode nicht ^^

Hier also die korrigierte, jetzt nicht mehr so hübsch aussehende Version. Sie sollte jetzt aber hoffentlich ohne Komplikationen funktionieren:

Ruby Quellcode

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#==============================================================================
# ** Main
#------------------------------------------------------------------------------
#  After defining each class, actual processing begins here.
#==============================================================================
class Reset < Exception
end
module RGSSGraphicsObject
end
[Viewport, Sprite, Plane, Window, Tilemap].each() {|klass| 
  klass.send(:include, RGSSGraphicsObject)
}
 
begin
  # Prepare for transition
  Graphics.freeze
  # Make scene object (title screen)
  $scene = Scene_Title.new
  # Call main method as long as $scene is effective
  while $scene != nil
    $scene.main
  end
  # Fade out
  Graphics.transition(20)
rescue Errno::ENOENT
  # Supplement Errno::ENOENT exception
  # If unable to open file, display message and end
  filename = $!.message.sub("No such file or directory - ", "")
  print("Unable to find file #{filename}.")
rescue Reset
  Graphics.freeze
  ObjectSpace.each_object(RGSSGraphicsObject) {|v| 
    v.dispose if v.respond_to?(:dispose) && (!v.respond_to?(:disposed?) || !v.disposed?)
  }
  RPG::Cache.clear
  GC.start
  retry
end


Damit müsste die F12 Funktion jetzt wieder einwandfrei funktionieren, ohne dass es zu Komplikationen mit den Scripten kommt. Wer die F12 Taste nicht braucht, kann natürlich diesen unschönen Hack mit dem ObjektSpace weglassen und das retry entfernen. Dann wird das Programm beim Drücken von F12 ganz normal beendet.

Ich weiß nicht ob das jedem bekannt ist (eventuell gähnt der eine oder andere beim Lesen des Threads) - Anlass für den Thread war eine entsprechende Anfrage im Quartier, bei der ich der Sache nachgegangen bin. Eventuell interessiert die Lösung ja auch hier den einen oder anderen.
Bild
RMXP Grundkurs
1 2 3
Ruby/RGSS-Kurs

Dieser Beitrag wurde bereits 2 mal editiert, zuletzt von »=Kai=« (23. Dezember 2010, 15:26) aus folgendem Grund: Blöde verbuggte RGSS ^^


Neo-Bahamut

Himmelsgleicher

Motto: Wer anderen eine Bratwurst brät, der hat ein Bratwurstbratgerät.

  • Nachricht senden

2

Donnerstag, 23. Dezember 2010, 08:36

Das ist natürlich interessant :o

Allerdings hätte ich das hier:

Ruby Quellcode

1
2
3
4
5
module RGSSGraphicsObject
end
[Viewport, Sprite, Plane, Window, Tilemap].each() {|klass| 
  klass.send(:include, RGSSGraphicsObject)
}

anders gelöst, nämlich einfach bei der Erstellung eines solchen Objekts zu einem Array hinzufügen und beim disposen wieder daraus entfernen.
Weil so werden Viewports (und Tilemaps ?) doch nicht gelöscht, oder?

Übrigens könnte es auch sinnvoll sein, vorher noch RPG::Cache.clear aufzurufen und Bitmap zu der Liste hinzuzufügen.
Es scheint nämlich so, dass ungelöschte Bitmaps Memory Leaks verursachen.
Spoiler: Wurstinator
zum Lesen den Text mit der Maus markieren

Spoiler: Lazer-Wurst
zum Lesen den Text mit der Maus markieren

Spoiler: Hallowurst
zum Lesen den Text mit der Maus markieren

3

Donnerstag, 23. Dezember 2010, 12:28

Interessant .-.
Das der Druck auf F12 die Reset-Exception wirft, war mir schon bekannt, aus dem RGSS-Player Modul Script, wo man F12 und F1 deaktivieren kann.

Neos Einwand zu den Bitmaps teile ich, wieso hast Du die nicht auch in deine Verwaltung aufgenommen?
Aber der Text ist gut geschrieben und verständlich, aber für ein Tutorial hätte ich mir ein bisschen mehr Hintergrundwissen und Erklärung deiner Korrektur gewünscht.

4

Donnerstag, 23. Dezember 2010, 15:24

Zitat

nämlich einfach bei der Erstellung eines solchen Objekts zu einem Array hinzufügen und beim disposen wieder daraus entfernen.
Äh, nee, der Code fügt einfach nur ein leeres Modul in alle RGSS-Grafikklassen ein, so dass man sie leicht über den ObjectSpace iterieren kann. Im ObjektSpace rufe ich dann die dispose Methode auf. Die Objekte werden ja sowieso gelöscht (gibt ja keine Referenz mehr darauf). Nur das dauert eben, bis der GC sich dazu bequemt.
Das mit dem Array ist nur bedingt eine Alternative. Denn der Array würde zugleich dafür sorgen, dass die Objekte vom GC nicht mehr gelöscht werden. Das wäre dann ein fataler MemoryLeak. An der Stelle könnte man mit WeakRef arbeiten (Pointer, die vom GC nicht berücksichtigt werden und so Objekte referenzieren können, ohne diese am Leben zu halten). Aber ich finde da die Variante mit dem ObjectSpace ehrlich gesagt leichter (elegant sind sie beide nicht).

Zitat

Übrigens könnte es auch sinnvoll sein, vorher noch RPG::Cache.clear aufzurufen und Bitmap zu der Liste hinzuzufügen.
Es scheint nämlich so, dass ungelöschte Bitmaps Memory Leaks verursachen.
Gute Idee. Das sollte man hinzufügen.

Zitat

Neos Einwand zu den Bitmaps teile ich, wieso hast Du die nicht auch in deine Verwaltung aufgenommen?
Weil es dann passieren kann, dass Bitmaps vor ihren Eigentümern gelöscht werden. Wenn die Eigentümer in ihrer dispose Methode dann auf das Bitmap zugreifen (warum sollten sie sowas bescheuertes machen? Na das fragt mal die Macher der RGSS -_-) gibt es eine Exception. Man kann natürlich nach dem Löschen der Viewports und Sprites immer noch die Bitmaps löschen. Aber wie bereits gesagt: Die werden vom GC irgendwann eh gelöscht. Der ganze Sinn des Löschens der Grafikobjekte war nicht den Speicher freizuräumen, sondern lediglich die Grafikartefakte zu entfernen, die erscheinen, wenn man im Spiel oft F12 drückt.

Zitat

aber für ein Tutorial hätte ich mir ein bisschen mehr Hintergrundwissen und Erklärung deiner Korrektur gewünscht.
Das ist richtig. Eigentlich isses auch weniger ein Tutorial, sondern eher ein Hinweis. Ich wusste nur nicht, wohin mit dem Thread. Eventuell kann ja ein Mod den in den Alias-Thread reinschieben.

btw. noch eine alternative Lösung, die weniger hacky ist:

Man fügt am Anfang des Scripteditors folgenden Code ein:

Ruby Quellcode

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Main.start if defined? Main
module Main
  def self.start
    # Prepare for transition
    Graphics.freeze
    # Make scene object (title screen)
    $scene = Scene_Title.new
    # Call main method as long as $scene is effective
    while $scene != nil
      $scene.main
    end
    # Fade out
    Graphics.transition(20)
    # exit program
    exit()
  rescue Errno::ENOENT
    # Supplement Errno::ENOENT exception
    # If unable to open file, display message and end
    filename = $!.message.sub("No such file or directory - ", "")
    print("Unable to find file #{filename}.")
  end    
end


Und nun fügt man an Stelle seines Main-Scripts einfach den Code

Ruby Quellcode

1
Main.start

ein. Dann wird der Scripteditor ebenfalls nicht neu eingelesen, weil schon im obersten Script das Spiel gestartet wird. Dafür hat der Interpreter die Gelegenheit alle Grafiken auf saubere Art und Weise zu löschen.
Bild
RMXP Grundkurs
1 2 3
Ruby/RGSS-Kurs

Ähnliche Themen

Social Bookmarks