Die Programmierung von Leben und Tod

Im Beitrag Objekte kollidieren lassen haben wir die Klasse Meteor erstellt und die Klasse Bullet mit einer Kollisions-Abfrage versehen, die überprüft ob ein Projektil mit einem Meteor-Objekt kollidiert. Der Spieler soll aber nicht nur reine Zielübungen meistern, sondern den Meteoren ausweichen, bevor er getroffen wird.

Der aktuelle Stand des Spiels lässt die Meteore nur über unser Raumschiff hinweg fliegen. Da Meteore aber bekanntlich Dinge zerstören die sie treffen, sollten wir das mithilfe einer Kollisions-Abfrage an unserem Raumschiff berücksichtigen. Dadurch bekommt der Spieler einen Anreiz die vorbei fliegenden Objekte nicht nur zu zerstören, sondern diesen auch auszuweichen, wenn ein Offensiv-Angriff keine Option mehr darstellt.

Wir benötigen also eine Abfrage, ob Meteore in unserer Array myMeteors mit dem Raumschiff kollidieren und sollten auch testen ob unser Raumschiff zu viel Schaden abbekommen hat. Der Schaden wird durch die Anzahl der Leben visualisiert, die wir bereits im Beitrag Vorbereitung und Planung in unserer Klasse Spaceship erstellt und deklariert haben.

Dank der update()-Funktion unseres Raumschiffs können wir eine neue Funktion zur Timeline der Main-Loop hinzufügen und diese in unserem Schiff unterbringen. Die Funktion collision() wird unser Prüf-Element, das mit einer Schleife durch die Positionen aller Meteore iteriert um eine mögliche Kollision mithilfe von checkCollision() festzustellen. Sollte eine Kollision zwischen einem Meteor und dem Raumschiff stattfinden wird zuerst Leben vom Raumschiff abgezogen und der Meteor wird im Anschluss darauf zerstört, indem dessen Variable Destroyed einen bool'schen Wert true erhält. Durch die Integration beider Algorithmen in der gleichen Funktion verhindern wir eine mögliche Doppel-Kollision.

/* ... */
class Spaceship {
	constructor( args ) {
		/* ... */
	}
	update() {
		if( !GameOver ) {
			if( typeof myInput !== "undefined" ) {
				this.move();
				this.shoot();
				this.collision();
			}
			/* ... */
		}
	}
	move() {
		/* ... */
	}
	shoot() {
		/* ... */
	}
	collision() {
		
		for( var i=0; i < myMeteors.length; i++ )
			if( !myMeteors[ i ].Destroyed )
				if( checkCollsion( this, myMeteors[ i ] ) ) {
					
					this.Life -= 1;
					if( this.Life <= 0 )
						GameOver = true;
					
					myMeteors[ i ].Destroyed = true;
					
				}
		
	}
}
/* ... */

Unser Raumschiff kann nun zwar Schaden erleiden, kann aber immer noch nicht zerstört werden. Denn der Algorithmus für den Status GameOver wurde noch nicht in unserem Code implementiert.

Für diesen Status benötigen wir eine Erweiterung der Renderer-Klasse. Sie wird zukünftig überprüfen, ob das Raumschiff noch Leben besitzt und andernfalls eine Nachricht auf unsere Leinwand projizieren, die dem Spieler mitteilt, dass das Spiel beendet ist.

Die Ausgabe wird durch ein neues HTML-Element gesetzt, das eine eindeutige ID besitzt, damit wir noch ein paar spezielle CSS-Formatierungen vornehmen können. Ich habe für Dich eine Formatierung erstellt, die Du gerne für Dein eigenes Spiel verwenden kannst. Du kannst gerne die Typografie und Farben nach Belieben ändern.

var GameOver = false;
/* ... */
(function($) {
	/* ... */
	class Renderer {
		constructor() {
			/* ... */
		}
		update() {
			if( typeof this.Spaceship !== "undefined" ) {
				this.updateSpaceship();
				this.checkGameOver();
			}
			/* ... */
		}
		updateSpaceship() {
			/* ... */
		}
		updateBullets() {
			/* ... */
		}
		updateMeteors() {
			/* ... */
		}
		checkDestroyed() {
			/* ... */
		}
		checkGameOver() {
			
			if( GameOver && this.Spaceship.length ) {
				this.Spaceship.remove();
			}
			
		}
	}
})(jQuery);
#gameover {
	background-color: rgba( 0, 0, 0, .6 );
	color: #ffffff;
	position: absolute;
	height: 100%;
	width: 100%;
	left: 0;
	top: 0;
	z-index: 99;
	
	font-size: 50px;
	line-height: 100%;
	padding: 275px 0;
	text-align: center;
	font-family: monospace;
}

Verloren hat der Spieler, wenn seine 5 Leben von einem Meteor auf 0 dezimiert wurden. Leider hat der Spieler aber keine Möglichkeit anzusehen wie viele Leben sein Raumschiff noch besitzt. Lass uns das als nächstes ändern.

Unsere Oberfläche hat viel Platz um eine Benutzerfläche zu gestalten, in dem der Benutzer seine aktuellen Statistiken betrachten kann. Die Leben seines Schiffs sollten dazu gehören. Es gibt viele Möglichkeiten die Leben auszugeben, eine Zahl oder ein Balken sind zwei davon. Ich habe mich dafür entschieden die Anzahl an Leben als Herzen auszugeben. Dafür habe ich eine Grafik vorbereitet, die Du gerne auch für dein Projekt verwenden kannst. Wenn Du möchtest kannst Du gerne eine eigene Art ausdenken, wie Du die Leben des Raumschiffs darstellen möchtest.

Die HTML-Elemente werden Absolut positioniert in unseren Main-Wrapper einfügt. Das erreichen wir durch eine Grundformatierung per CSS, die das Hintergrundbild setzt, sowie die Größe und weitere wichtige Formatierungen angibt. Du hast beim Downloaden der PNG-Datei bestimmt bemerkt, dass die 4 Bilder nicht in unterschiedlichen Dateien liegen, sondern als gebündeltes Spritesheet erstellt wurden. Wir verwenden für diesen Beitrag nur das erste Bild in der Reihe, werden uns aber in einem kommenden Beitrag mit den 3 anderen Bildern beschäftigen.

Durch die Verwendung von einem Spritesheet sparen wir uns Dateivolumen und somit wertvolle Ladezeit beim Aufruf der Webseite. Wie bereits mit unserem Raumschiff im Beitrag Bewegung entwickeln können wir den Bildausschnitt des Spritesheets mit der Angabe der background-position verändern. Durch die Limitierung des Bildausschnitts und der Eigenschaft overflow: hidden wird dadurch nur der Bereich dargestellt, der auf der Leinwand erscheinen darf.

.life {
	background-image: url( '/wp-content/themes/atomik_theme/scheme/post/game/games/SpaceShooter/img/powerups.png' );
	background-size: 100px 25px;
	background-repeat: no-repeat;
	background-position: 0px 0px;
	
	width: 25px;
	height: 25px;
	position: absolute;
	left: 0;
	top: 10px;
	z-index: 98;
}

Der Renderer wird sich darum kümmern die Anzahl der Herzen immer gleich mit den Leben zu halten und an die richtige Stelle im GUI zu setzen. Dazu erweitern wir unsere Klasse erneut um die Funktion checkLifes(). In Ihr implementieren wir einen Algorithmus der pro Durchlauf feststellen soll, ob die Anzahl der Leben des Raumschiffs gleich der Herzen auf dem GUI ist. Andernfalls wird die Ausgabe der Leben erneuert, indem die Leben komplett entfernt und mit einer Schleife neu aufgebaut werden. Da die Herzen komplett erneuert werden könntest Du sogar ein Item erstellen, das die Leben des Raumschiffs wieder auffüllen kann. Das wird kein Bestandteil dieser Beitrags-Reihe, aber du solltest im Anschluss genügend Wissen haben, damit du dieses Item und noch mehr selbstständig integrieren kannst.

class Renderer {
	constructor() {
		/* ... */
	}
	update() {
		if( typeof this.Spaceship !== "undefined" ) {
			this.updateSpaceship();
			this.checkGameOver();
			this.checkLifes();
		}
		/* ... */
	}
	updateSpaceship() {
		/* ... */
	}
	updateBullets() {
		/* ... */
	}
	updateMeteors() {
		/* ... */
	}
	checkDestroyed() {
		/* ... */
	}
	checkGameOver() {
		/* ... */
	}
	checkLifes() {
		
		var spaceshipLifes = mySpaceship.Life;
		var currentGUILifes = $( '#main_wrapper .life' ).length;
		if( spaceshipLifes != currentGUILifes ) {
			$( '#main_wrapper .life' ).remove();
			for( var i=0; i < spaceshipLifes; i++ ) {
				$( '#main_wrapper' ).append( '
' ); } } } }

Da unsere Meteore im letzten Beitrag ebenfalls Leben erhalten haben, die wir bisher aber noch nicht verwenden, werden wir jetzt unsere Klasse Bullet um einen kleinen Code-Schnipsel erweitern, der den Meteor nicht sofort zerstört, sondern erst Schwächt bevor er eliminiert wird. Eine Abfrage, ob das Leben des Meteors kleiner oder gleich 0 beträgt, wird nach der Subtraktion des Projektil-Schadens vom Leben durchgeführt.

class Bullet {
	constructor( args ) {
		/* ... */
	}
	
	update() {
		/* ... */
	}
	move() {
		/* ... */
	}
	collision() {
		
		if( !this.Destroyed )
			for( var i=0; i < myMeteors.length; i++ )
				if( !myMeteors[ i ].Destroyed )
					if( checkCollsion( this, myMeteors[ i ] ) ) {
						myBullets[ this.ID ].Destroyed = true;
						
						// Um den Schaden zu berechnen müssen wir nur das Leben des Meteors mit dem verursachenden Schaden des Projektils subtrahieren
						myMeteors[ i ].Life -= this.Damage;
						if( myMeteors[ i ].Life <= 0 ) {
							// Danach prüfen wir ob das Leben des Meteors kleiner gleich 0 ist und setzen die Variable "Destroyed" erst dann auf "true"
							myMeteors[ i ].Destroyed = true;
						}
						
					}
		
	}
	
}

In vielen Fällen möchte man dem Spieler die Möglichkeit geben sich mit anderen zu messen, um einen Wett. Welche Möglichkeit ist dafür besser geeignet als ein Score?

Mit Score bezeichnet man die Punkte, die der Spieler in einer Sitzung erlangt hat. Wir als Programmierer des Spiels müssen festlegen, wie viele Punkte der Spieler für welche Ereignisse erhält. Unser SpaceShooter gibt schon fast vor, dass der Spieler Punkte erhält, wenn er Meteore abschießt. Da wir zwei unterschiedliche Meteore haben können wir für diese 2 Typen auch unterschiedlich viele Punkte verteilen. Ich habe definiert, dass kleine Meteore 25 Punkte und große Meteore 100 Punkte einbringen. Diese beiden Werte kannst Du aber gerne nach Belieben individualisieren.

Für den Score benötigen wir zunächst eine globale Variable, die beim zerstören des Meteors mit dem definierten Wert addiert wird. Das passiert in der Funktion collision() unseres Projektils.

var Score = 0;
/* ... */
(function($) {
	/* ... */
	class Bullet {
		constructor( args ) {
			/* ... */
		}
		
		update() {
			/* ... */
		}
		move() {
			/* ... */
		}
		collision() {
			
			if( !this.Destroyed )
				for( var i=0; i < myMeteors.length; i++ )
					if( !myMeteors[ i ].Destroyed )
						if( checkCollsion( this, myMeteors[ i ] ) ) {
							myBullets[ this.ID ].Destroyed = true;
							
							myMeteors[ i ].Life -= this.Damage;
							if( myMeteors[ i ].Life <= 0 ) {
								myMeteors[ i ].Destroyed = true;
								
								// Es wird geprüft, von welchem Typ der Meteor ist und dementsprechend wird der Score um 25 oder 100 Punkte erhöht 
								if( myMeteors[ i ].Type == "Big" )
									Score += 100;
								else 
									Score += 25;
							}
							
						}
			
		}
		
	}
	/* ... */
})(jQuery);

Außerdem soll der Renderer unsere Variable auf der GUI ausgeben, damit der Spieler seinen Score immer im Überblick behält. Die update()-Funktion wird um einen neuen Aufruf der Funktion changeScore() erweitert, die den Score ändern soll, sobald die angezeigte Punktausgabe nicht mit der Variable übereinstimmt. jQuery besitzt bereits eine Funktion, mit der Inhalte von HTML-Elementen in der Laufzeit bearbeitet werden können. Das HTML-Element wird per ID selektiert und mit dem .html()-Befehl kann der Inhalt des Elements, also den Score des Spielers, eingefügt werden.

class Renderer {
	constructor() {
		/* ... */
	}
	update() {
		/* ... */
	}
	updateSpaceship() {
		/* ... */
	}
	updateBullets() {
		/* ... */
	}
	updateMeteors() {
		/* ... */
	}
	checkDestroyed() {
		/* ... */
	}
	checkGameOver() {
		
		if( GameOver )
			if( $( '#spaceship' ).length ) {
				this.Spaceship.remove();
				$( '#main_wrapper' ).append( '
GameOver
Score: '+Score+'
' ); } } checkLifes() { /* ... */ } changeScore() { var $Score = $( '#main_wrapper #score' ); if( $Score.length ) { if( $Score.html() !== Score ) $Score.html( Score ); } else { $( '#main_wrapper' ).append( '
'+Score+'
' ); } } }

Zu guter Letzt können wir den Score noch einmal ausgeben lassen, sobald der Spieler besiegt wurde und es GameOver heißt. Der Score wird in ein HTML-Element gepackt und im Wrapper der GameOver-Nachricht angehängt. Mit CSS können wir die Formatierung des Scores ein wenig verändern um eine kleinere Schriftgröße zu erhalten, als der Text „Game Over“. Du hast natürlich wieder die Möglichkeit deine eigene Formatierung zu setzen, wenn Du das möchtest.

#score {
	position: absolute;
	right: 25px;
	top: 10px;
	z-index: 98;
	
	font-size: 22px;
	color: #ffffff;
	font-weight: 600;
	font-family: monospace;
}
/* ... */
#gameover .score {
	font-size: 32px;
}

Nun müssen wir nur noch den Renderer ein klein wenig erweitern, indem wir in der Funktion checkGameOver() den Score in den String der html()-Funktion hinzufügen. Eine CSS-Klasse .score hilft dem Score seine CSS-Formatierung zu erhalten.

 

Mit diesen Änderungen können sich unsere Spieler messen. Wenn Du Dich mit Datenbanken auskennen solltest kannst du die Scores pro Spieler sichern und einen Highscore entwickeln, der die besten Spieler neben Deinem Spiel ausgibt. Damit können sich die Spieler sogar auf deiner Webseite verewigen.

Der nächste Beitrag behandelt temporäre PowerUps, die es ermöglichen werden das Raumschiff für einen kurzen Zeitraum zu verbessern. Diese Beispiele kannst du verwenden um Deine eigenen PowerUps zu entwickeln, um den Spieler mit noch mächtigeren Boni zu belohnen.

 

Codepalm
Spieleprogrammierung für Einsteiger
Teil 8: Der Game Over Status in Browser-Spielen