Meteore erstellen und diese zerstörbar machen

Im letzten Beitrag Projektile abfeuern haben wir die Klasse Bullet erstellt und beim Betätigen der Leertaste ein neues Projektil in die Array myBullets geladen, damit unser Raumschiff für ein paar Zielübungen vorbereitet ist. Dieser Beitrag befasst sich mit den Zielscheiben, die wir mit den Projektilen zerstören werden.

Unsere Objekte sollen sich an das gewählte Setting „Weltall“ richten. Deshalb werden wir die Zielscheiben aussehen lassen wie Meteore, die ziellos durch das Universum schweben. Damit der Effekt bestehen bleibt, dass das Raumschiff durch das All fliegt lassen wir die Position der Meteore auf der Y-Achse nach unten wandern.

Du kannst dir bestimmt denken, dass wir auch in unserer Klasse Meteor einige Variablen benötigen werden, wie die Position, die Geschwindigkeit (Speed) in Pixel, eine ID, sowie die Variable Destroyed. Damit wir ein wenig Variation in diese Objekte einfließen lassen bekommen die Meteore zusätzlich eine Variable Type, die angibt ob der Meteor groß (Big) oder klein (Small) ist. Je nach Typ erhält das Objekt eine andere Geschwindigkeit und unterschiedlich viel Leben, die wir in einer weiteren Variable Life sichern.

Für die Meteore verwenden wir 2 Bilder, die per CSS geladen werden sollen.

In der CSS-Formatierung des neuen Elements hinterlegen wir die bereits verfügbare Animation ROTATE, die wir für die initiale Feuer-Animation der Projektile erstellt haben. Da wir die Animationen ROTATE und FADEOUT in 2 unterschiedlichen Keyframes implementiert haben können wir den Meteor rotieren lassen, ohne ihn dabei auszublenden.

.meteor {
	background-repeat: no-repeat;
	position: absolute;
	left: -100px;
	top: -100px;
	animation: ROTATE 10s linear 0s infinite;
	transform-origin: center center;
}
.meteor[data-type="small"] {
	background-image: url( '/wp-content/themes/atomik_theme/scheme/post/game/games/SpaceShooter/img/meteorSmall.png' );
	background-size: 44px 42px;
	width: 44px;
	height: 42px;
}
.meteor[data-type="big"] {
	background-image: url( '/wp-content/themes/atomik_theme/scheme/post/game/games/SpaceShooter/img/meteorBig.png' );
	background-size: 68px 55.5px;
	width: 68px;
	height: 55.5px;
}

Damit der Spieler kein Schema beim erscheinen neuer Meteore feststellen kann, benötigen wir neben den Variablen ein paar Zufälle die im Konstruktor aufgerufen werden. Dadurch können wir die initiale Position und die Richtung in die der Meteor steuert zufällig bestimmen lassen.

Die Positionsänderung in der move()-Funktion wird etwas dynamischer als die vom Raumschiff oder vom Projektil. Da wir eine zufällige Richtung verwenden möchten müssen wir diese Richtung mit der Geschwindigkeit des Meteors multiplizieren und danach mit der aktuellen Position addieren.

class Meteor {
	constructor( args ) {
		args = args || {};
		
		this.Destroyed = false;
		
		// Die ID des Meteors, um die Bewegung des HTML-Tags zu steuern
		if( typeof args.ID !== "undefined" )
			this.ID = args.ID;
		else
			this.ID = myMeteors.length;
		
		// Die initiale Position des Meteors kann mit der übergabe des Parameters "Position" gesetzt werden
		this.Position = { X: 0, Y: 0, H: 0, W: 0 };
		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;
		}
		
		// Sollte die Position nicht gesetzt sein wird ein zufälliger Wert im oberen Bereich gewählt
		if( this.Position.X == 0 && this.Position.Y == 0 ) {
			var randomPositionX = Math.floor( ( Math.random() * Field.Width ) + 1 );
			this.Position.X = randomPositionX;
			this.Position.Y = -120;
		}
		
		// Über die Parameter des Konstruktors können wir übergeben, ob der Meteor klein oder groß sein soll. Sollte der Parameter nicht gesetzt sein wird ein kleiner Meteor erstellt
		if( typeof args.Type !== "undefined" )
			this.Type = args.Type;
		else {
			// Die Größe des Meteors soll zufällig erstellt werden
			var randMeteorType = Math.floor( ( Math.random() * 10 ) + 1 );
			if( randMeteorType >= 8 ) this.Type = "Big";
			else this.Type = "Small";
		}
		
		// Hier werden verschiedene größenspezifische Werte gesetzt, wie die Pixelgröße des Meteors, das Leben und die Gewschwindigkeit
		if( this.Type == "Small" ) {
			this.Life = 1;
			this.Speed = 1;
			
			this.Position.W = 44;
			this.Position.H = 42;
		}
		else if( this.Type == "Big" ) {
			this.Life = 3;
			this.Speed = 0.5;
			
			this.Position.W = 68;
			this.Position.H = 55.5;
		}
		
		// Hier geben wir eine Richtung vor, in die der Meteor fliegen soll. Die Y-Koordinate sollte immer Positiv sein, da der Meteor von oben nach unten fliegt
		this.Direction = { X: 0, Y: 0 };
		if( typeof args.Direction !== "undefined" ) {
			if( typeof args.Direction.X !== "undefined" )
				this.Direction.X = args.Direction.X;
			if( typeof args.Direction.Y !== "undefined" )
				this.Direction.Y = args.Direction.Y;
		}
		
		// Falls die Richtung nicht gesetzt sein sollte, oder die Y-Koordinate in die falsche Richtung zeigt, in die sich der Meteor bewegen soll, wird ein zufälliger Wert ermittelt
		if( this.Direction.X == 0 || this.Direction.Y <= 0 ) {
			
			var positionX = 0.5;
			if( this.Position.X - this.Position.W/2 > Field.Width/2 )
				positionX = -0.5;
			
			this.Direction.Y = 1;
			this.Direction.X = positionX;
			
		}
		
	}
	update() {
		this.move();
	}
	move() {
		
		// Um den Meteor zu steuern verwenden wir die Richtung Mal die Geschwindigkeit des Meteors und addieren den Wert aus der Multiplikation mit der aktuellen Position
		this.Position.X += this.Direction.X * this.Speed;
		this.Position.Y += this.Direction.Y * this.Speed;
		
	}
}

Neben der Deklaration der Projektile soll auch eine weitere Variable myMeteors für die Verwaltung der Meteore dienen. Die Variable wir wie üblich im globalen Namespace gesetzt und in unserer Funktion $(document).ready() mit einer leeren Array befüllt.

var myMeteors;
(function($) {
	$( document ).ready( function() {
		myInput = new Input();
		myBullets = [];
		myMeteors = [];
		
		/* ... */
	});
})(jQuery);

Damit unsere neue Array befüllt werden kann erstellen wir eine Hilfs-Funktion, die ähnlich aufgebaut ist wie die Funktion shoot() aus unserer Klasse Spaceship. Leere Meteore die abgeschossen wurden oder aus dem sichtbaren Bereich des Main-Wrappers fliegen können mit einer kleinen Modifikation dieses Algorithmus neu befüllt werden. Dank dieser Art den Meteor zu erstellen und in unsere Array zu laden sparen wir wertvolle Ressourcen im Arbeitsspeicher und Zeit beim Rendern des Objekts. Wie bei den Projektilen soll erst ein neuer Meteor erstellt werden, wenn alle anderen noch aktiv sind, oder kein Meteor zuvor erstellt wurde.

function newMeteor() {
		
		// Hier verwenden wir den gleichen Algorithmus wie beim Projektil
		var meteor_outof_area = false;
		for( var i=0; i < myMeteors.length; i++ )
			if( myMeteors[ i ].Position.Y >= Field.Height + 20 ) {
				myMeteors[ i ] = new Meteor({ ID: myMeteors[ i ].ID });
				meteor_outof_area = true;
				break;
			}
		if( !meteor_outof_area )
			myMeteors.push( new Meteor() );
		
}

Da sich unser Meteor noch nicht auf unserer Leinwand befinden kann, müssen wir unsere Klasse Renderer mit den neuen Objekten erweitern. Dazu erstellen wir eine Funktion updateMeteors() und rufen diese in der update()-Funktion des Renderers auf. Die neue Funktion soll es uns erlauben alle Meteore, die sich in unserer Array myMeteors befinden im Main-Wrapper zu erstellen und die Position zu verändern.

Bei einer Kollision zwischen unserem Projektil und einem Meteor sollen Beide von unserem Main-Wrapper entfernt werden. Dazu prüfen wir mit einer neuen Funktion checkDestroyed(), ob einer der Objekte den Status Destroyed==true enthält. Die Funktion soll nur aufgerufen werden, wenn sich ein Meteor oder ein Projektil in unseren Arrays befinden.

Die Funktion checkDestroyed() soll nur aufgerufen werden, wenn die Array myMeteors und die Array myBullets nicht leer sind. Du kannst die Bedingung auch gerne noch erweitern, indem du abfragst ob es mindestens einen aktiven Meteor und mindestens ein aktives Projektil gibt. Die Abfrage ist meistens nicht so Ressourcen-Intensiv wie das Prüfen aller Kollisionen.

class Renderer {
	constructor() {
		/* ... */
	}
	
	update() {
		/* ... */
		if( typeof myMeteors !== "undefined" && myMeteors.length > 0 ) this.updateMeteors();
		if( typeof myBullets !== "undefined" && myBullets.length > 0
		||	typeof myMeteors !== "undefined" && myMeteors.length > 0 )
			this.checkDestroyed();
	}
	
	updateSpaceship() {
		/* ... */
	}
	
	updateBullets() {
		/* ... */
	}
	
	updateMeteors() {
		
		for( var i=0; i < myMeteors.length; i++ ) {
			var $Meteor = $( '#meteor_'+myMeteors[ i ].ID );
			if( $Meteor.length ) {
				if( myMeteors[ i ].Position.Y <= Field.Height + 50 ) {
					$Meteor.css({ 
						'top' : myMeteors[ i ].Position.Y+'px',
						'left' : myMeteors[ i ].Position.X+'px'
					});
					if( myMeteors[ i ].Type.toLowerCase() !== $Meteor.attr( 'data-type' ) )
						$Meteor.attr( 'data-type', myMeteors[ i ].Type.toLowerCase() );
				}
			}
			else {
				$( '#main_wrapper' ).append( '
' ); } } } checkDestroyed() { for( var i=0; i < myBullets.length; i++ ) if( typeof myBullets[ i ] !== "undefined" ) if( typeof myBullets[ i ].Destroyed !== "undefined" && myBullets[ i ].Destroyed == true ) $( '#bullet_'+myBullets[ i ].ID ).attr( 'data-destroyed', 'true' ); else if( typeof myBullets[ i ].Destroyed !== "undefined" && myBullets[ i ].Destroyed == false ) $( '#bullet_'+myBullets[ i ].ID ).attr( 'data-destroyed', 'false' ); for( var i=0; i < myMeteors.length; i++ ) if( typeof myMeteors[ i ] !== "undefined" ) if( typeof myMeteors[ i ].Destroyed !== "undefined" && myMeteors[ i ].Destroyed == true ) $( '#meteor_'+myMeteors[ i ].ID ).attr( 'data-destroyed', 'true' ); else if( typeof myMeteors[ i ].Destroyed !== "undefined" && myMeteors[ i ].Destroyed == false ) $( '#meteor_'+myMeteors[ i ].ID ).attr( 'data-destroyed', 'false' ); } }

Ob Plattformer, RPG oder Minispiel. Es gibt kein Spiel das ohne eine Kollsionsabrage auskommt. Sie wird verwendet um zu prüfen ob sich zwei verschiedene Objekte berühren, oder ineinander liegen.

Die Theorie dahinter ist etwas schwierig erklärt, aber einfach auszuführen wenn man sie einmal verstanden hat. Im Grunde testet man die äußersten Kanten eines Rechtecks (Objekt 1), ob diese innerhalb eines anderen Rechtecks (Objekt 2) liegt. Zuerst werden die horizontalen Kanten von Objekt 1 mit den horizontalen Kanten des Objekts 2 überprüft, danach die vertikalen Kanten (es funktioniert auch andersrum).

Da wir in beiden Objekten (Projektil und Meteor) jeweils eine X- und Y-Koordinate, sowie die Höhe und die Breite in der Variable Position gesichert haben, können wir jeweils die äußeren Kanten aller Objekte mathematisch herausfinden.

Die X-Koordinate stellt unsere linke Kante dar. Um die rechte Kante zu erhalten addieren wir die X-Koordinate mit der Breite des Objekts. Die gleiche Formel verwenden wir auch in der Vertikalen. Um diese Formel in unserem Programm darzustellen und wir einen bool'schen Wert daraus erhalten können, implementieren wir eine globale Funktion checkCollision() in der wir die beiden zu prüfenden Objekte als Parameter übergeben können.

function checkCollision( a, b ) {
	
	return !(
		( ( a.Position.Y + a.Position.H ) < ( b.Position.Y ) ) ||
		( a.Position.Y > ( b.Position.Y + b.Position.H ) ) ||
		( ( a.Position.X + a.Position.W ) < b.Position.X ) ||
		( a.Position.X > ( b.Position.X + b.Position.W ) )
	);
	
}

In einem vorherigen Schritt der Meteor-Erstellung haben wir definiert, dass sich die Meteore in eine zufällige Richtung bewegen sollen. Beim Erstellen eines neuen Meteor-Objekts kann zusätzlich ein Zufall hinterlegt werden, der besagt dass ein Meteor mit einer bestimmten Wahrscheinlichkeit auf der Bildfläche erscheint.

Wir erweitern also unsere Main-Loop mit einer Zufalls-Variable, die einen Integer zwischen 1 und 100 setzt. Sollte dieser Wert unter 20 liegen wird ein neuer Meteor erstellt und in unsere Array myMeteors hinterlegt. Das geschieht mithilfe der Funktion newMeteor().

Ohne den Aufruf der update()-Funktion können sich unsere Meteore nicht bewegen. Deshalb wird direkt nach der zufälligen Erstellung eine Schleife erstellt, die alle Meteore durchläuft und diese Funktion aufruft.

function mainLoop() {
	/* ... */
	
	// Es soll zufällig ein Meteor erstellt werden
	var randNewMeteor = Math.floor( ( Math.random() * 1000 ) + 1 );
	if( randNewMeteor <= 5 ) {
		newMeteor();
	}
	
	// Hier werden alle Meteore Bewegt, indem die update()-Funktion aufgerufen wird
	for( var i=0; i < myMeteors.length; i++ )
		myMeteors[ i ].update();
	
	/* ... */
}

Die Meteore sollten sich nun auf unserem Spielfeld befinden und sich etwa in Richtung unseres Raumschiffs bewegen. Um die Meteore mit Projektilen zu zerstören müssen wir die Klasse Bullet ein wenig erweitern. Dazu hilft uns die Funktion checkCollision(), die das Projektil mit allen in der Variable myMeteors enthaltene Objekte auf eine Kollision prüfen soll. Eine Kollisionsprüfung soll nur stattfinden, wenn die Variable Destroyed des Meteors oder des Projektils nicht wahr ist. Falls eine Kollision stattfinden sollte wird diese Variable in beiden Objekten auf true gestellt.

class Bullet {
	constructor( args ) {
		/* ... */
	}
	
	update() {
		/* ... */
	}
	move() {
		/* ... */
	}
	collision() {
		
		if( !this.Destroyed )
			for( var i=0; i < myMeteors.length; i++ )
				if( !myMeteors[ i ].Destroyed )
					if( checkCollision( this, myMeteors[ i ] ) ) {
						myBullets[ this.ID ].Destroyed = true;
						myMeteors[ i ].Destroyed = true;
					}
		
	}
}

Dank der Funktion checkDestroyed() in der Klasse Renderer verschwinden der Meteor und das Projektil automatisch, wenn der Status Destroyed aktiviert wurde. Technisch sind beide noch in den Arrays vorhanden und bewegen sich bis zu einem bestimmten Punkt weiter, aber diese Objekte werden nicht mehr bei Kollisionsprüfungen beachtet und sind dadurch Deaktiviert.

Zum Schluss kannst Du nun die Geschwindigkeit und die Wahrscheinlichkeit des Erstellens eines Meteors justieren. Für die Geschwindigkeit musst Du nur die Variable Speed im Meteor verändern. Je größer der Wert, desto schneller bewegen sich die Meteore. Die Wahrscheinlichkeit des Aufrufs newMeteor() kannst du in der Bedingung beeinflussen. Umso kleiner die zu prüfende Zahl wird, desto geringer ist die Wahrscheinlichkeit das ein Meteor auf dem Spielfeld erscheint.

 

Ein neuer Meilenstein wurde erreicht! Der Spieler kann mit dem betätigen der Leertaste ein Projektil abfeuern, dass einen Meteor zerstören kann. Im folgenden Beitrag ergänzen wir einige Feinheiten in unserem Spiel. Unser Raumschiff wird eine bestimmte Anzahl an Leben erhalten und einen Score, der die erspielten Punkte beinhalten soll. Durch eine kleine GUI werden beide Variablen grafisch dargestellt.

Zudem testen wir die Kollision zwischen den Meteoren und dem Schiff, damit die Leben dezimiert werden können und der Status Game Over ausgelöst wird, wenn keine mehr übrig sind. Langsam wird unser Spiel spannend!

 

Codepalm
Spieleprogrammierung für Einsteiger
Teil 7: Objekte in Spielen kollidieren lassen