GameStates

Eichelhäer

Bekanntes Mitglied
Hallo,
ich versuch grade ein GameState System zu implementieren. Ich habe einen fixedthread pool von 4 threads und abhängig davon meine 4 Gamestates. Jeder thread funktioniert abhängig von meiner enum state Wahl einwandfrei, allerdings das umschalten zwischen den states innerhalb der applikation nicht. Es gibt zwar mit dem executor eine shutdown methode allerdings kann ich dann den nächsten thread nicht mehr starten und es soll immer nur ein thread laufen. Gibts da iwie eine mögzlichkeit um elegant zwischen den states zu switchen?

[CODE lang="java" title="GameStates"]switch(states) {

case INTRO:
gamescreen.removeAll();
intro = new Intro(gamescreen,new ImageIcon("res/menu/backgrounds/intro_video.gif"));
audio.playIntroMusic("INTROGM");
audio.playIntroVoice("intro_audio");
gamescreen.add(intro);
executor.execute(intro);
break;

case LOADINGSCREEN:
gamescreen.removeAll();
loadingScreen = new LoadingScreen(gamescreen);
gamescreen.add(loadingScreen);
executor.execute(loadingScreen);
break;

case MAINMENU:
gamescreen.removeAll();
mainmenu = new MainMenu(gamescreen);
//audio.playMainMenuMusic("");
gamescreen.add(mainmenu);
executor.execute(mainmenu);
break;

case GAME:
gamescreen.removeAll();
game = new Game(gamescreen);
//audio.playGameMusic("");
gamescreen.add(game);
executor.execute(game);
break;

case MAPEDITOR:
gamescreen.removeAll();
mapeditor = new MapEditor(gamescreen);
gamescreen.add(mapeditor);
executor.execute(mapeditor);
break;

}[/CODE]
 
K

kneitzel

Gast
Wenn immer nur ein Thread laufen soll, dann brauchst du nicht mehrere Threads gleichzeitig und somit auch keinen Threadpool.

Einen beendeten Thread kannst du nicht erneut starten:
It is never legal to start a thread more than once. In particular, a thread may not be restarted once it has completed execution.

Somit musst Du immer einen neuen Thread erstellen.
 
K

kneitzel

Gast
Wobei ich erst im nachhinein sehe: Ist Dein executor ein ThreadPool und Deine Klassen Game, MapEditor, ... implementieren Runnable?
Dann sollte das so wie Du es geschrieben hast zumindest funktionieren. Hast Du eine Fehlermeldung? Was genau passiert?

Wenn neue Threads nicht ausgeführt werden, dann könnte es daran liegen, dass sich Deine Threads nicht beenden. Dann hat der ThreadPool seine x laufenden Threads und alle neuen Threads landen in der Warteschlange.
 

Eichelhäer

Bekanntes Mitglied
Hm. Der executorservice bietet die methode shutdown an, allerdings sagt die api, dass danach keine weiteren tasks mehr ausgeführt werden können. Dann gibts noch die Methode awaitTermination die wartet auf das beenden. Beides lässt aber ein wiederholtes starten nicht zu.
Ich denke halt ich muss die vorherigen Threads zunächst pausieren oder beenden bevor ich nen neuen ausführe. Bin da aber nicht so fit mit den threads.
 

Eichelhäer

Bekanntes Mitglied
Habe keine fehlermeldung und ja alle implemntieren runnable ( bei mainmenu eig. nicht notwendig).
Am Ende des ladevorgangs beispielsweise bleibt alles stehen keine fehlermeldung, obwohl ich nachdem die ladetask beendet ist den state wechsel.
 
K

kneitzel

Gast
Das sind Methoden vom ThreadPool. Du willst den ThreadPool doch nicht beenden!

Threads sollen sich selbst beenden! Bei Runable bedeutet das, dass die run Methode sich beenden soll.
 
K

kneitzel

Gast
Ja genau:

Wobei die Dokumentation jetzt fast so klingt, als ob der Status nicht gesetzt wird, wenn der Thread in einem speziellen Zustand war, der eine InterruptedException erlaubt:
If none of the previous conditions hold then this thread's interrupt status will be set.
 
K

kneitzel

Gast
Da mich das auch interessiert hat, habe ich es mal getestet:
Java:
public class ThreadTest implements Runnable {
    public static void main(String[] args) {
        Thread thread = new Thread(new ThreadTest());
        thread.start();
        thread.interrupt();
    }

    public void run() {
        try {
            Thread.sleep(10000);
        } catch (InterruptedException ex) {
            System.out.println("Interrupted!");
            System.out.println("isInterrupted() = " + Thread.interrupted());
        }
    }
}

Es ist so, wie die Dokumentation es aussagt: Nur wenn der Thread nicht in einem Status war, dass ein interrupt nicht gesendet werden konnte, wird das Flag gesetzt.

[CODE title="Ausgabe"]Interrupted!
isInterrupted() = false[/CODE]


Ich selbst nutze für sowas auch meine eigene Logik. Ich Kapsel Threads in der Regel (das ist ein Implementierungsdetail, das niemanden etwas angeht!) und haben dann sowas wie eine shutdown Methoder oder so, die dann etwas verändern kann. Dann würde ein internes Flag zielsicher gesetzt. Der interrupt() Aufruf wäre dann nur intern um ggf. ein wait und co abzubrechen.
 

Eichelhäer

Bekanntes Mitglied
Hm. Klingt kompliziert. Also wäre es besser die Threads einzeln für jeden state einzeln zu erzeugen und zu starten und dann jeweils jeden thread für sich zu beenden abhängig von nem Button oder task der diesen dann beendet?
 
K

kneitzel

Gast
Im Augenblick glaube ich, dass Du es Dir nur unnötig kompliziert gemacht hast, denn du hast Dinge aus meiner Sicht nicht an den jeweiligen Orten, an die eben diese gehören. Aber es fehlen einfach zu viele Informationen zu dem, was Du da überhaupt im Detail machst. Und ich weiss auch nicht, mit was für Libraries du arbeitest - ggf. hast Du auch eine Library im Einsatz, bei der es genau so vorgesehen ist (warum auch immer).

Alles, was ich bisher weiss, ist ja:
- Du hast ein Spiel geschrieben, welche in unterschiedliche Phasen kommen kann, wobei immer nur eine Phase auf einmal aktiv ist:
Intro, LoadingScreen, Game und MapEditor.
- Jede dieser Phasen macht irgendwas und stellt etwas da.

Dein gamescreen ist sowas wie das Hauptfenster und das bekommt dann immer einen entsprechenden Inhalt.

Ich würde es deutlich einfacher halten von außen. Schnittstellen, die man nicht zwingend braucht und die gar Implementationsdetails betrifft, die außen niemanden zu interessieren haben, die sind rein intern!

Und dann kapseln die Elemente Dinge.

Intro soll Audio abspielen? Das gehört in die Intro. Das Verhalten der Intro gehört in die Intro und sonst nirgend wo hin.
Das trifft auf alles zu. Wenn die Intro einen Thread braucht, dann erstellt die Intro den Thread und hat die Verantwortung dafür, dass der Thread wieder beendet wird. Das sollte aber kein Problem sein, denn zum einen ist die Intro sein eigener Herr, d.h. der eigene Zustand ist wirklich bei ihm und kann ausgewertet werden: Ist die Intro beendet? Hat der User abgebrochen? (Oder was auch immer Du da abbildest). In so einem Fall wird der Thread beendet.

Oder beim LoadingScreen: Der wird aufgerufen und im Hintergrund lädt etwas (oder so ähnlich). Dann bietet der LoadingScreen eine Methode, die im Signalisiert: Du bist fertig. Und dann kann er den Thread beenden. Oder er lädt dann auch alles - dann weiss er ja, wann er fertig ist. (Das ist dann einfach - denn der Thread läuft einmal durch und hat keinerlei Schleife ...)

Unter dem Strich wird etwas - so man es sauber gegliedert hat und Verantwortlichkeiten richtig aufgeteilt hat - sehr einfach und übersichtlich. Komplexe Logik findet sich dann kaum. Selbst so ein Switch wird dann extrem trivial, denn das Enum kann dann Details kennen, so dass auf Deinem Code etwas wird wie:

Java:
gamescreen.removeAll();
gamescreen.add(states.createScreenContent());

(states sollte Singular sein, also state)

Dazu wird dein Enum etwas erweitert:
Java:
public enum State {
    // ...
    GAME(()->new Game());
    
    Suplier<WhatEver> factory;
    
    State(Suplier<WhateEver> factory) {
        this.factory = factory;
    }
    
    WhatEver createScreenContent() {
        return factory.get();
    }

Das nur einmal vereinfacht dargestellt - WhatEver muss natürlich zu der Basisklasse von Deinen Klassen Game, Intro, ... geändert werden (das was die add Methode von gamescreen halt haben will)

Es lässt sich sehr viel sehr einfach handhaben, wenn man Dinge so verschiebt, dass sie genau da sind, wo sie hingehören. Und dann dies auch Kapselt. Und Logik muss auch einfach sein. Wenn ich etwas wissen muss, dann hat die Information gezielt vorzuliegen.

Die Verantwortung, Daten sinnvoll zu liefern, liegt also an der Quelle!

Wenn da irgendwas komplexes gemacht werden muss, dann macht das zur Not ein Adapter. Aber ich arbeite nur mit einfachen Dingen. Das macht es auch extrem einfach für mich, Code zu schreiben und zu verstehen. Es wird etwas mehr an Klassen und Methoden, aber es ist einfach zu verstehen.

Du hast ein Flugzeug und eine Anzeige der Höhe über NN in Meter. Nun bekommst Du aber nur Geräte, die dir die Höhe in Fuß angeben. Kein Thema - dann gibt es einen Adapter, der das umrechnet. Total trivial. Bekommen auch Kinder hin: Smartphone passt nicht in die Steckdose. Hmm, nehmen wir ein "Adapter" ... sogar oft zwei Adapter ... erst ein Steckernetzteil und dann noch ein Kabel zum verbinden.
Einfaches Schema F, das wir im Leben tagtäglich machen, wenden wir auch bei der Software Entwicklung an.
 

Eichelhäer

Bekanntes Mitglied
Hallo nochmal, könnten wir uns in skype oder ähnlichem treffen keine sorge möchte dich nicht ausquetschen das ganze stellt sich trotz deiner Worte als kompliziert da. Also nur falls du zeit findest. Würde mich freuen. Sag einfach Bescheid. Bis dann.
 
K

kneitzel

Gast
Für so Skype Sessions habe ich keine Zeit. Maximal könnte man sowas im Projekt auf die schnelle mal umstellen, so dass du an Deinem Projekt siehst, wie es aussehen könnte.
 

Neue Themen


Oben