Skip to content

GameSession

damios edited this page Oct 28, 2018 · 12 revisions

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.

GameSession

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.


SlaveSession und AuthoritativeSession

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":

  1. Der Client C teilt dem Server mit, dass er Haus H anzünden möchte
  2. Der Server prüft das (und bejaht es) und teilt anschließend allen Clienten mit, dass das Haus H brennt
  3. 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.
  4. 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.


GameServer und GameClient

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 geht's weiter

Wie das Processing des Spiels innerhalb der fixedUpdate()-Methode abläuft, wird hier erläutert.