-
Notifications
You must be signed in to change notification settings - Fork 0
GameSession
Die eigentliche Arbeit des Spiels läuft in den GameSession
-Klassen ab. Die Mutterklasse GameSession
wird von SlaveSession
für den Clienten und von AuthoritativeSession
für den Server erweitert.
Die GameSession
-Klasse selbst hat drei Aufgaben: (a) das Updaten des Runden-Timers, (b) das Beinhalten des momentanen Spielstandes (in Form eines World-Objekts) und (c) das Verarbeiten des Spiels.
Die update()
-Methode ermittelt zuerst das Zeit-Delta seit dem letzten update()
-Aufruf. Ist die Runde zu Ende, so wird true zurückgegeben. Um mit dem Verarbeiten der nächsten Runde zu beginnen, muss die startNextRound()
-Methode aufgerufen werden (was der Fall ist, wenn alle Spieler im Runden-End-Bildschirm auf "Bereit" gedrückt haben). Bevor es mit dem Verarbeiten des Spiels weiter geht, werden zunächst alle Votes (bspw. Amtsernennungen) abgearbeitet.
Während einer Runde wird die vergangene Zeit über einen Tick-Counter in Ticks umgerechnet. Ticks sind die Zeiteinheiten nach denen ein Verarbeitungsschritt stattfindet. Bei normaler Spielgeschwindigkeit gibt es pro Sekunde 10 Ticks, d.h. die fixedUpdate()
-Methode, in der die Verarbeitung des Spiels stattfindet, wird 10 mal pro Sekunde aufgerufen. Wie das Game Processing in der fixedUpdate()
-Methode abläuft, wird im nächsten Eintrag näher beschrieben.
/**
* Updates the game session. Returns true once, when a round is over. To
* start the next round call {@link #startNextRound()}.
*
* @return Whether the ingame day is over (8 minutes).
*/
public synchronized boolean update() {
if (!holdVote) {
// NORMAL UPDATE CYCLE
if (tickCounter.update()) {
processRoundEnd();
return true;
}
return false;
} else {
// PROCESS VOTES
if (matterToVoteOn == null) {
matterToVoteOn = world.getMattersToHoldVoteOn().pollFirst();
onNewVote(matterToVoteOn);
if (matterToVoteOn == null) {
holdVote = false;
}
} else {
if (voteTimer.isRunning()) {
// 7 Sekunden nach Vote-Ende warten
if (voteTimer.update()) {
voteTimer.reset();
matterToVoteOn = null;
}
}
}
return false;
}
}
/**
* Called after a round ended to start the next round.
*/
protected synchronized void startNextRound() {
currentRound++;
// Reset the tick counter
tickCounter.reset();
// [...]
holdVote = !world.getMattersToHoldVoteOn().isEmpty();
}
Die update()-Methode der Sessions wird in der render(float)
-Methode der BaseGameScreen
-Klasse aufgerufen, d.h. solange ein Ingame-Screen angezeigt wird, wird die GameSession
geupdated. Wenn eine Runde zuende ist, also update()
true zurückgibt, wird auf den RoundEnd-Screen umgeleitet.
Außerdem beinhaltet die GameSession
noch die folgenden zwei wichtigen Variablen:
private GameSessionSetup sessionSetup;
private World world;
In diesen werden die (unveränderlichen) Eckdaten des Spiels (wie Karte und Schwierigkeit) und der momentane Stand der Stadt (welche Gebäude, Personen, Amtsträger gibt es) gespeichert.
Es gibt sowohl für den Server als auch für die Clienten jeweils eine eigene Child-Klasse der GameSession
. Grund dafür ist, dass die Klasse des Servers die Spielwelt nach der Aktion eines Nutzers (bspw. "Zünde Haus X an") direkt anpasst (= authoritative), während der Client seine Spielwelt nur anpasst, nachdem der Server im das Ergebnis der Aktion ("Das Haus X brennt") mitgeteilt hat (= slave).
Umgesetzt wird das (wie schon im Eintrag zur Netzwerkarchitektur beschrieben) durch Remote Method Invocation, d.h. der jeweils eine Endpunkt ruft Methoden des jeweils anderen auf. Das läuft in ProjektGG über zwei Interfaces: SlaveActionListener
und AuthoritativeResultListener
.
Das erstere wird von einem ServersideActionHandler
implementiert und liegt damit auf der Serverseite. Dieser nimmt Aktionen der Nutzer (die hauptsächlich durch das UI vorgenommen werden) entgegen und verarbeitet diese. Deutlich wird dies durch folgendes Beispiel:
public interface SlaveActionListener {
public boolean marrySomeone(short networkId, short personId);
}
public class ServersideActionHandler implements SlaveActionListener {
@Override
public boolean marrySomeone(short networkId, short spouseId) {
// 1. Überprüfen, ob das überhaupt geht, nicht das der entsprechende Spieler nur aufgrund
// von veralteten Daten davon ausgeht oder gar schummelt
// [...]
// 2. Die Aktion lokal anwenden
players.get(networkId).getPerson().setSpouse(city.getPersons(spouseId));
city.getPersons(spouseId).setSpouse(players.get(networkId).getPerson());
// 3. Die Aktion an die Clienten weitergeben
// Das wird (a) in einem extra Thread erledigt und (b) in einer Utility-Klasse, die
// eigenständig über alle Client-Verbindungen interiert.
(new AuthoritativeResultListenerThread() {
@Override
protected void informListener(AuthoritativeResultListener resultListener) {
resultListener.onPersonsMarried(players.get(networkId).getPerson().getId(), spouseId);
}
}).start();
return true; // false wird oben zurückgeben, falls die Aktion gar nicht durchgeführt werden darf
// der Client würde dann das UI neu laden, um die (hoffentlich mittlerweile empfangenen Daten)
// entsprechend anzuzeigen
}
}
Auf der Client-Seite kümmert sich ein ClientsideResultListener
um das Ergebnis der Aktion:
public class ClientsideResultListener implements AuthoritativeResultListener {
@Override
public synchronized void onPersonsMarried(short person1Id, short person2Id) {
city.getPersons(person1Id).setSpouse(city.getPersons(person2Id));
city.getPersons(person2Id).setSpouse(city.getPersons(person1Id));
}
}
Dass die Methode im AuthoritativeResultListener
genau dasselbe tut, wie die Methode im SlaveActionListener
ist nicht immer so. Anders wäre es beispielsweise bei einer Aktion "Haus anzünden":
- Der Client C teilt dem Server mit, dass er Haus H anzünden möchte
- Der Server prüft das (und bejaht es) und teilt anschließend allen Clienten mit, dass das Haus H brennt
- In seiner
update(long)
prüft der Server nun die nächste Zeit, ob ein Haus brennt. Das bejaht er für H und teilt dementsprechend allen Clienten mit, dass das Haus H X Schaden nimmt. - Die Clienten andererseits prüfen das in ihrer
update(long)
-Methode hingegen nicht (!), da die Entscheidungen vom Server getroffen werden und der Client als Slave letztlich nur die Ergebnisse des Servers umsetzt. Grund dafür ist folgendes: Hätte ein weiterer Client D in letzter Sekunde das Haus gelöscht (bevor es fatalen Schaden genommen und damit zerstört worden wäre), hätte es sein können, dass er dies zwar dem Server mitteilt, dieser das auch bejaht, bei sich umsetzt und allen andere mitteilt – die anderen Clienten aber bei sich schon den Schaden angewendet haben und das Haus damit bei ihnen nicht mehr existiert.
Die Faustregel ist hier: Wird ein Entity-Wert (bspw. das Geld eines Spielers) geändert, so geschieht das nur über RMI, geht also immer vom Server aus.
Die beiden Klassen, die das tatsächliche Networking, also das Aufsetzen von Client und Server über Kryonet sowie den Verbindungsvorgang zwischen den beiden übernehmen, sind GameServer
auf der Server-Seite und GameClient
auf der Client-Seite. Diese beiden Klassen beinhalten auch die jeweilige GameSession
sowie den AuthoritativeResultListener
bzw. den SlaveActionListener
und sind ihrerseits Variablen von ProjektGG
.
Wie das Processing des Spiels innerhalb der fixedUpdate()
-Methode abläuft, wird hier erläutert.