Grundlagen der Spieleprogrammierung – Objekte kollidieren lassen

Objekte kollidieren lassen

 

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 kollidieren lassen

Abonniere die Fanpage von Codepalm und verpasse keine BeitrÀge mehr

Codepalm auf Facebook
 
 
 
 
Das könnte Dir gefallen:

Spieleprogrammierung mit Cocoa und OpenGL

EUR 49,70

Zu Amazon Mehr erfahren

Spieleprogrammierung fĂŒr AnfĂ€nger

EUR 39,41

Zu Amazon Mehr erfahren

Unity5 Spieleentwicklung: Umfassendes Training

EUR 34,99

Zu Amazon Mehr erfahren

Kommentare

Sei der Erste, der einen Kommentar erstellt!

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.