Closed cagix closed 1 year ago
Siehe https://github.com/Programmiermethoden/Dungeon/pull/560/files#r1184735167:
Der TextureHandler
wird in Game#setup
"aufgerufen", d.h. dort zum ersten Mal initialisiert. Da wir dabei eine checked exception werfen könnten, muss man in Game#setup
damit umgehen. Der aktuelle Workaround ist, die Exception in eine Runtime-Exception zu konvertieren und weiter nach oben zu reichen.
Game#setup
beim Auftreten einer IOException
nochmal loggen und dann direkt aussteigen?Game#render
prüft in jedem Frame, ob ein Setup gemacht werden muss?! Wenn Game#setup
die checked exception (also die IOException
) weitergeben würde, dann müsste sich Game#render
darum kümmern. Das erscheint irgendwie unsinnig; ein Weiterleiten geht nicht wg. der Ableitung von ScreenAdapter
... Wäre es nicht sinnvoller, man macht explizit ein Setup für das Game (also einmalig) und steigt dann in die Game-Loop ein, ohne dort immer wieder zu prüfen, ob man vielleicht doch nochmal ein Setup machen möchte? TextureHandler
kennen und explizit aufrufen? Wäre es nicht besser, wenn man das Game nach den Texturen befragen könnte? D.h. wenn diese Schnittstelle nach Game
verschoben wird, also sowas wie Game.getTexturePaths(regex)
? (Game könnte das intern immer noch an den TextureHandler
weitergeben oder ein Attribut vom Typ TextureHandler
haben.)Wäre es nicht sinnvoller, man macht explizit ein Setup für das Game (also einmalig) und steigt dann in die Game-Loop ein, ohne dort immer wieder zu prüfen, ob man vielleicht doch nochmal ein Setup machen möchte?
Die stelle ärgert mich auch jeden Tag im Schlaf. Das Kernproblem ist, dass libGDX erst irgendwelche Magie machen muss, damit wir in setup
auf bestimmte Sachen von libGDX zugreifen können.
Man müsste sich das Problem mal systematisch vorknöpfen:
Setup
wirklich etwas von dieser libGDX Magie? Wenn ja, wo? Kann man das auslagern? render
denn wirklich der erste Einstiegspunkt dafür? Können wir uns vielleicht auch mit setup
woanders anschließen?` Bitte mal https://github.com/gurkenlabs/litiengine-docs/blob/master/tutorials/creating-a-platformer.md#our-main-class anschauen. Das nenne ich mal ne saubere Schnittstelle (naja, aber irgendwie intuitiver und stimmiger).
Hmm... ich glaube so groß anders als wir machen die das gar nicht. Die verteilen das einfach nur "besser" (wobei ich mir nicht sicher bin, ob ich das besser finde).
Wenn wir unsere Klasse Game
wieder (im alten Framework war das ja so) abstract
machen, dann könnte das irgendwie so aussehen
package content;
import configuration.Configuration;
import configuration.KeyboardConfig;
import ecs.entities.Hero;
import java.io.IOException;
public class MyGame extends Game {
public static void main(String[] args) {
// start the game
try {
Configuration.loadAndGetConfiguration("dungeon_config.json", KeyboardConfig.class);
} catch (IOException e) {
throw new RuntimeException(e);
}
DesktopLauncher.run(new MyGame());
}
@Override
public void myOnLevelLoad() {
// z.B spawne Monster und Schatzkisten
}
@Override
public void mySetup() {
hero = new Hero();
}
@Override
public void mySystems() {
// new WuppiSystem();
}
}
Wenn man richtig fancy ist, könnte mann vielleicht in das systems
package gucken und für jedes System das dort liegt eine Instanz erzeugen. Quasi automatisiert. Dann brächte es mySystems
gar nicht mehr.
@AMatutat Ich meinte eher das Beispiel direkt in "Our Main Class".
Die machen mehrere Aufrufe ala Game.info().setName("wuppie");
um die allgemeinen Metainformationen zu setzen. Dito sowas wie Game.init(args);
oder Game.window().setIcon(Resources.images().get("icon.png"));
oder Game.world().loadEnvironment("level1");
. Das finde ich vom Frameworkgedanken her wesentlich lesbarer als eine Klasse im Framework überschreiben zu müssen.
Ich glaube interessanter ist dieses Szenario, denn da wird zumindest Logik implementiert:
// add default game logic for when a level was loaded
Game.world().onLoaded(e -> {
// spawn the player instance on the spawn point with the name "enter"
Spawnpoint enter = e.getSpawnpoint("enter");
if (enter != null) {
enter.spawn(Player.instance());
}
});
das wäre jetzt quasi unser, onLevelLoad
Ich denke man könnte das auf alle Methoden anwenden, die man ansonsten @Override
würde.
Vorteil:
Nachteil:
Der Metainfo Kram könnte man auch übernehmen/integrieren, sehe ich jetzt aber nicht als wirkliches Problem bei uns, da sich die Werte einfach austauschen lassen und die default Werte eigentlich recht gut eingestellt sind.
Genau. Man erbt nicht von einer Klasse und überschreibt dabei Methoden und muss das dann irgendwie wieder in das Framework zurücktun, sondern man hat einen festen Ansprechpartner im Framework (Game
), wo man über Methodenaufrufe die ganzen Einstellungen macht und auch CallBacks hinterlegt oder Objekte (Systems, ...) übergibt/registriert.
Die Frage ist, wie weit man damit kommt. Jetzt haben wir mit render()
ja komplette Narrenfreiheit. Klappt das auch noch mit Lambda-Ausdrücken?
Die Studis müssen lambdas verstehen
Ist nicht so schlimm aus meiner Sicht - das Thema ist eh in der ersten Woche mit dran und gehört auch in ein fortführendes Programmiermodul prima mit rein. Und ich muss nächstes Jahr eh im Praktikum "kleiner" anfangen, d.h. das würde aus der Perspektive passen.
Ich Bau mal einen PoC. Dann sehen wir am ehesten wo es knallt.
Hier mal ein vereinfachtes Beispiel für die Methode onLevelLoad
In Game
private Runnable myOnLevelLoad;
public void myOnLevelLoad(Runnable function) {
myOnLevelLoad = function;
}
@Override
public void onLevelLoad() {
currentLevel = levelAPI.getCurrentLevel();
entities.clear();
getHero().ifPresent(this::placeOnLevelStart);
myOnLevelLoad.run();
}
in Main
public static void main(String[] args) {
Game game = new Game();
game.myOnLevelLoad(() -> System.out.println("Test"));
// start the game
try {
Configuration.loadAndGetConfiguration("dungeon_config.json", KeyboardConfig.class);
} catch (IOException e) {
throw new RuntimeException(e);
}
Game.LibgdxSetup.run(game);
}
@cagix hast du dir das so vorgestellt? Und ist Runnable
hier wirklich der richtige Typ? Mir stößt das irgendwie auf. aber das Internet sagt mir das und Reflection will ich nun echt nicht machen.
Runnable
ist was für die Erzeugung von Threads. Ich dachte, wir haben hier ne Game-Loop? Wieso kommt IJ auf den Trichter? Vielleicht wg. der run
-Methode?
Runnable ist was für die Erzeugung von Threads. Ich dachte, wir haben hier ne Game-Loop?
Yep. Deswegen bin ich da auch drüber gestolpert. Im Grunde will ich eigentlich einen Consumer
ohne Parameter.
Runnable ist was für die Erzeugung von Threads. Ich dachte, wir haben hier ne Game-Loop?
Yep. Deswegen bin ich da auch drüber gestolpert. Im Grunde will ich eigentlich einen
Consumer
ohne Parameter.
Naja, eigentlich willst Du eine Function<Void,Void>
:). ...
Tatsächlich ist java.lang.Runnable
ein funktionales Interface, d.h. das könntest Du hier verwenden. Allerdings wird es üblicherweise im Zusammenhang mit Multithreading eingesetzt, und das könnte die Leser verwirren.
Und warum auch immer ist in java.util.function
nichts Äquivalentes definiert. But that's Java for you :/
Ich habe sogar eine Diskussion gefunden: https://stackoverflow.com/questions/14319787/how-to-specify-function-types-for-void-not-void-methods-in-java8#comment66743981_14338333 ...
Aber im Prinzip wäre das so ungefähr der Mechanismus: Game
wird nicht mehr abgeleitet (könnte sogar final
sein) und man ruft auf einem Objekt vom Typ Game
ein paar Builder-Methoden auf, u.a. auch vielleicht myOnLevelLoad()
. Und dann wird das Ding gestartet.
Edit: Ob das in main()
dann aber wirklich Game.LibgdxSetup.run(game);
sein muss und nicht einfach Game.run()
sein könnte, sollten wir nochmal besprechen. Die run()
-Methode könnte der letzte Aufruf im Sinne des Builder-Patterns sein.
Bitte mal https://github.com/gurkenlabs/litiengine-docs/blob/master/tutorials/creating-a-platformer.md#our-main-class anschauen. Das nenne ich mal ne saubere Schnittstelle (naja, aber irgendwie intuitiver und stimmiger).
Bitte das Konzept übertragen.