Feuer frei - Mit dem Raumschiff Projektile abfeuern

Bevor wir uns mit Gegnern und böswillige Objekte beschäftigen, die unser Raumschiff zerstören möchten bereiten wir es zuerst für den Kampf vor. Unser Raumschiff kann schnell zu Weltraum-Schrott werden, wenn es keine Fähigkeit besitzt sich offensiv zu Verteidigen. Für diesen Angriff erstellen wir nun Projektile, die die Gegner unseres Schiffs zerstören sollen.

Zur Vorbereitung benötigen wir als aller erstes die notwendigen Bilder, damit der Spieler auch einen Schuss erkennen kann. Dafür habe ich bereits zwei Bilder herausgesucht, die Du in Dein Spiel integrieren kannst. Falls du eigene Grafiken erstellen möchtest oder einen Künstler kennst, der die Grafiken für Dich erstellt kannst du diese natürlich gerne verwenden. Je mehr Individualisierungen in deinem Spiel sind desto besser!

Wir verwenden 2 Bilder für die Schüsse. Die visuelle Darstellung des Projektils und ein Bild für eine initiale Schuss-Animation, die wir mit CSS-Formatierungen und -Animationen dynamisch gestalten.

Nachdem Du die Bilder in dein Ziel-Verzeichnis gesichert hast können wir die CSS-Formatierungen für den Schusswechsel erstellen. Um Zeit zu sparen kann das Animations-Schema der Sterne dupliziert und angepasst werden. Die „Feuer“-Animation soll langsam ausgeblendet und gleichzeitig gedreht werden. Wir könnten beide Animationen in ein und dasselbe Keyframe legen, jedoch können wir dann die beiden Animationen nicht mehr getrennt voneinander verwenden.

Die Alternative wäre die Animationen getrennt zu erstellen und die Ziel-Keyframes im Element mit einem Komma separiert zu setzen. Die beiden Keyframes können dadurch auch eine unterschiedliche Ablauf-Geschwindigkeit erhalten und wenn gewünscht eine Verzögerung beinhalten.

Gesetzt werden diese Animationen in einer neuen Klasse "bullet" und "bullet_shot" die wie alle anderen Elemente auf dem Spielfeld hauptsächlich aus einer Hintergrundgrafik und einer absoluten Positionierung bestehen.

.bullet {
	background-size: 4.5px 16.5px;
	background-repeat: no-repeat;
	
	width: 4.5px;
	height: 16.5px;
	
	position: absolute;
	left: -100px;
	top: -100px;
}
.bullet {
	background-image: url( '/wp-content/themes/atomik_theme/scheme/post/game/games/SpaceShooter/img/laserGreen.png' );
}

.bullet_shot {
	background-size: 28px 27px;
	background-repeat: no-repeat;
	
	width: 28px;
	height: 27px;
	
	position: absolute;
	left: -100px;
	top: -100px;
	
	opacity: 0;
	animation: FADEOUT .1s linear 0s 1, ROTATE 1s linear 0s 1;
}
.bullet_shot {
	background-image: url( '/wp-content/themes/atomik_theme/scheme/post/game/games/SpaceShooter/img/laserGreenShot.png' );
}
@-webkit-keyframes FADEOUT {
	from {
		opacity: 1;
	}
	to { 
		opacity: 0;
	}
}
@-webkit-keyframes ROTATE {
	from {
		transform: rotate( 0deg );
	}
	to { 
		transform: rotate( 360deg );
	}
}

[data-destroyed="true"] {
	display: none;
}

Du kannst Dich gerne entscheiden alle Elemente mit einem vordefinierten Set aus CSS-Formatierungen zu bestücken um wichtige Eigenschaften allen Elementen schon von Werk aus zu geben. Dazu benötigst du eine neue Klasse, die Du zum Beispiel "element" nennen kannst und alle Gemeinsamkeiten aus den gegebenen CSS-Beispielen beinhalten. In meinen Beispielen wirst du diese Standard-Klassen nicht sehen, da ich mich dagegen entschieden habe.

Nachdem wir nun die Darstellung des Projektils abgeschlossen haben widmen wir uns nun der Klasse Bullet um neue Projektile zu erstellen. Das Projektil erhält zunächst Variablen, wie eine ID, eine Position mit den Koordinaten und Maßen um später die Kollision zu prüfen, eine Geschwindigkeit (Speed) in Pixel, einen Angriff-Schaden (Damage), sowie eine Kontroll-Variable (Destroyed). Die Kontroll-Variable Destroyed enthält wie in unserem Raumschiff eine bool’sche Variable, die uns mit dem Wert "true" anzeigt, ob das Projektil zerstört wurde.

Damit unser Projektil bewegt werden kann benötigt es eine Funktion um sich bewegen zu können. Die Funtion move() subtrahiert die Bewegungsgeschwindigkeit pro Durchgang der Main-Loop zu der Y-Koordinate der eigenen Position. Es wird eine Subtraktion angewendet, da sich das Projektil nach oben bewegen soll. In unserer Main-Loop werden wir die update()-Funktion aufrufen, die wiederum die Funktion move() ausführt. Dadurch können wir im späteren Verlauf der Entwicklung weitere Funktionen aufrufen, die jeweils durch die Main-Loop ausgeführt werden sollen.

Sobald Du den Programmcode in diesen Beitrag umgesetzt hast kannst du die Variable Speed in der Klasse Bullet nach Belieben anpassen. Je höher Du den Wert dieser Variable setzt, desto schneller bewegt sich unser Projektil auf der Leinwand. (Spoiler: Setze die Variable nicht zu hoch, da wir die Projektile später durch aufsammelbare Gegenstände schneller werden lassen)

class Bullet {
	constructor( args ) {
		args = args || {};
		
		this.Destroyed = false;
		
		// Die ID des Projektils, um gleich die Bewegung des HTML-Tags zu steuern
		if( typeof args.ID !== "undefined" )
			this.ID = args.ID;
		else
			this.ID = myBullets.length;
		
		// Der Schaden, der verursacht wird, wenn das Projektil auf einen Gegner oder einen Meteoriten trifft
		if( typeof args.Damage !== "undefined" ) this.Damage = args.Damage;
		else this.Damage = 1;
		
		// Die Geschwindigkeit in Pixel
		if( typeof args.Speed !== "undefined" ) this.Speed = args.Speed;
		else this.Speed = 2.5;
		
		// Wird später wichtig, sobald die Alien-Schiffe implementiert werden
		if( typeof args.Type !== "undefined" ) this.Type = args.Type;
		else this.Type = 'Player';
		
		// Die Position des Projektils
		this.Position = { X: -100, Y: -100, W: 4.5, H: 16.5 };
		if( typeof args.Position !== "undefined" ) {
			if( typeof args.Position.X !== "undefined" )
				this.Position.X = args.Position.X;
			if( typeof args.Position.Y !== "undefined" )
				this.Position.Y = args.Position.Y;
			if( typeof args.Position.W !== "undefined" )
				this.Position.W = args.Position.W;
			if( typeof args.Position.H !== "undefined" )
				this.Position.H = args.Position.H;
		}
		
		// Hier wird das Projektil zum Main-Wrapper hinzugefügt. Zudem wird eine initiale Animation gesetzt, die nach einer Sekunde entfernt wird. Den Fading-Effekt haben wir bereits in der CSS-Formatierung gesetzt, dadurch bekommen wir eine bessere Performance, als z.B. durch eine jQuery-Animation
		if( this.Position.X != -100 && this.Position.Y != -100 ) {
			$( '#main_wrapper' ).append( '
' ); $( '#main_wrapper .bullet_shot:last-of-type' ).delay( 1000 ).queue( function() { $(this).remove(); } ); } } update() { this.move(); } move() { // Hier wird die Bewegungsgeschwindigkeit zur Position abgezogen, da das Projektil nach oben geschossen wird this.Position.Y -= this.Speed; } }

Das Raumschiff soll nicht nur ein einzelnes Projektil abfeuern können, sondern mehrere hintereinander. Das bedeutet, dass wir eine Array erstellen müssen, die alle abgefeuerten Projektile beinhaltet. Um dieses Array von jeder Stelle aus aufzurufen müssen wir sie in den globalen Namespace hinzufügen, also außerhalb unseres jQuery-Wrappers. Dadurch können wir die Array myBullets in unserer $(document).ready() Funktion deklarieren und in der Main-Loop die update()-Funktionen jedes Projektils mit einer Schleife aufrufen.

/* ... */

var myBullets;
$( document ).ready( function() {
	/* ... */
	
	// Hier wird die Array für die Projektile deklariert
	myBullets = [];
	
	/* ... */
});
function mainLoop() {
	/* ... */
	
	// Mit einer for-Schleife können einfach und performant alle update-Funktionen der Projektile aufgerufen werden
	for( var i=0; i < myBullets.length; i++ )
		myBullets[ i ].update();
	
	/* ... */
}

/* ... */

Der Algorithmus um ein neues Projektil zu erstellen wird in der Funktion shoot() unseres Raumschiffs hinterlegt. Ein Timer hilft uns dabei die Projektile nicht durchgehend, sondern in einem bestimmten Intervall abfeuern zu lassen. In der update()-Funktion unseres Raumschiffs können wir den Timer shootTimer ablaufen lassen, indem die Variable pro Durchlauf der Main-Loop um den Wert 1 abnimmt, falls der Wert größer 0 ist.

Da die Timer-Variable nach einer bestimmten Zeit immer 0 erreicht können wir in der Funktion shoot() danach prüfen. Sollte dieser Fall eintreffen wird ein neues Projektil erstellt, zu der Array myBullets hinzugefügt und der Timer wieder auf einen positiven Wert gesetzt. Mit diesem Wert kannst Du gerne experimentieren. Umso kleiner die Zahl festgelegt wird, desto schneller kann das Raumschiff feuern.

Damit unser Renderer später nicht immer wieder ein neues Projektil in unseren Main-Wrapper hinzufügen und löschen muss überprüfen wir beim Erstellen eines neuen Geschosses, ob bereits ein Objekt mit dem Wert Detroyed==true existiert. Dieses "zerstörte" Objekt kann mit einem neuen Projektil ersetzt werden. Durch dieses Recycling der vorhandenen Objekte verringern wir den Rechenaufwand unseres Spiels.

class Spaceship {
	constructor( args ) {
		/* ... */
		
		// Der Timer soll nach einer kurzer Abklingzeit einen Schuss abfeuern
		this.shootTimer = 0;
	}
	update() {
		if( typeof myInput !== "undefined" ) {
			this.move();
			this.shoot();
		}
		// Hier wird der Timer heruntergezählt, bis er "0" erreicht hat
		if( this.shootTimer > 0 ) this.shootTimer--;
	}
	move() {
		/* ... */
	}
	shoot() {
		if( myInput.Space ) {
			// Sobald der Timer "0" erreicht hat und die Leertaste betätigt wurde beginnt der Algorithmus, um ein neues Projektil zu erstellen
			if( this.shootTimer <= 0 ) {
				
				// Hier bereiten wir die initiale Position des Projektils vor
				var bulletPosition = {

					X: this.Position.X + this.Position.W / 2,
					Y: this.Position.Y

				};
				
				// Damit das Programm nicht ständig die Projektile löschen und wieder platzieren muss fragen wir an dieser Stelle ab, ob ein Projektil außerhalb des sichtbaren Bereichs liegt und ersetzen diesen, falls dies der Fall sein sollte
				var bullet_on_negative = false;
				for( var i=0; i < myBullets.length; i++ )
					if( myBullets[ i ].Position.Y < -20 ) {
						$( '#bullet_'+myBullets[ i ].ID ).remove();
						myBullets[ i ] = new Bullet({ ID: i, Position: bulletPosition });
						bullet_on_negative = true;
						break;
					}
				
				// Falls mehr Projektile benötigt werden, als bereits auf dem Spielfeld platziert wurden wird an dieser Stelle ein neuer hinzugefügt
				if( !bullet_on_negative )
					myBullets.push( new Bullet({ Position: bulletPosition }) );
				
				// Nun wird noch der Timer zurückgesetzt. Umso höher der Timer, desto länger benötigt das Schiff den nächsten Schuss zu platzieren
				this.shootTimer = 80;
				
			}
		}
	}
}

Das Projektil kann sich nun bewegen und unser Raumschiff hat auch schon die Fähigkeit erhalten Projektile abzufeuern. Zu guter Letzt benötigen wir noch eine visuelle Darstellung des Projektils, damit der Benutzer auch merkt, wenn er eines abgefeuert hat. Dazu erweitern wir die Klasse Renderer.

Die Steuereinheit dieser Darstellung wird eine neue Funktion updateBullets() übernehmen, die sich darum kümmert die Geschosse auf unsere Leinwand zu bringen, die Position mit jedem Durchlauf der Schleife zu verändern und aus dem Spielgeschehen zu entfernen, falls die Variable Destroyed des Projektils den Rückgabewert "true" liefert.

Um das Projektil auf unsere Leinwand zu bekommen verwenden wir die jQuery-Funktion append(), die es erlaubt innerhalb unseres Main-Wrappers ein neues Element während der Laufzeit am Schluss anzuhängen. Die Darstellung übernimmt unsere bereits erstellte CSS-Formatierung. Alles was wir hierbei beachten müssen, ist das wir die richtigen Werte in das HTML-Element hinterlegen. Auch das "entfernen" des Projektils haben wir in unserer CSS definiert. Dazu muss der Renderer lediglich das Attribut data-destroyed an unser HTML-Projektil mit dem Wert "true" setzen. Zudem wird die Position jedes Mal verändert, sofern sich das Projektil im sichtbaren Bereich befindet.

/* ... */

class Renderer {
	constructor() {
		/* ... */
	}
	update() {
		if( typeof this.Spaceship !== "undefined" ) this.updateSpaceship();
		if( typeof myBullets !== "undefined" && myBullets.length > 0 ) this.updateBullets();
	}
	updateSpaceship() {
		/* ... */
	}
	updateBullets() {
		
		for( var i=0; i < myBullets.length; i++ ) {
			var $Bullet = $( '#bullet_'+myBullets[ i ].ID );
			if( $Bullet.length ) {
				if( myBullets[ i ].Position.Y >= -200 ) {
					$Bullet.css({ 
						'top' : myBullets[ i ].Position.Y+'px',
						'left' : myBullets[ i ].Position.X+'px'
					});
				}
			}
			else {
				$( '#main_wrapper' ).append( '
' ); } } } } /* ... */

Mit der Fertigstellung dieser Klasse wurde unser Raumschiff mit der Eigenschaft erweitert Projektile abzufeuern. Da es aber noch kein Objekt gibt, das wir zerstören können werden wir im nächsten Beitrag Meteore erstellen, die uns als Zielscheiben dienen sollen. Durch eine Kollisionsprüfung zwischen den Meteoren und den Projektilen werden wir feststellen können, ob sich die beiden Elemente treffen.

In der Zwischenzeit kannst du gerne die Variablen, wie in diesem Beitrag beschrieben, nach Deiner Vorstellung justieren um die Feuerrate des Raumschiffs und die Geschwindigkeit der Projektile zu verändern.

 

Codepalm
Spieleprogrammierung für Einsteiger
Teil 6: Projektile in Browser-Spielen programmieren