Mehr Stärke, Geschwindigkeit und Verteidigung durch PowerUps

So gut wie jedes Spiel enthält PowerUps, die dem Spieler temporär, oder permanent Boni auf Statuswerte gewährt, oder die einen anderen Effekt auf das Spielgeschehen anwendet.

In diesem Beiträg beschäftigen wir uns mit Gegenständen, die unser Raumschiff mit mehr Angriffskraft, Feuergeschwindigkeit und sogar einem Schild ausstattet, der uns gegen Schaden schützt.

Im letzten Beitrag Game Over haben wir ein Spritesheet mit einem Herzen und den 3 PowerUp-Bildern in unser Spiel geladen.

Wir können unsere CSS-Klasse ".life" für die unterschiedlichen PowerUps duplizieren und kleinere Anpassungen anwenden. Dazu verschieben wir das Hintergrund-Bild so weit nach links (X-Koordinate in den negativen Bereich), bis das PowerUp in unserem HTML-Tag erscheint.

Wir unterscheiden bei den PowerUps zwischen Power (Rot), um den Schaden unserer Projektile zu erhöhen, Speed (Grün), um die Feuerrate der Projektile zu erhöhen und Shield (Blau), um unserem Raumschiff einen Schild zu geben der den nächsten Angriff blockt.

Du darfst natürlich gerne die Farben Deines Spielst so anpassen wie Du möchtest. Wenn Du andere Bilder für die PowerUps im Internet findest, die Du verwenden darfst, kannst Du das natürlich gerne machen.

.powerup,
.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;
	
	width: 25px;
	height: 25px;
	position: absolute;
	left: 0;
	top: 10px;
	z-index: 98;
}
.life {
	background-position: 0px 0px;
}

.powerup {
	border-radius: 50%;
}
.powerup[data-type="power"] {
	background-position: -25px 0px;
	box-shadow: 0px 0px 20px #da0000;
}
.powerup[data-type="shield"] {
	background-position: -50px 0px;
	box-shadow: 0px 0px 20px #0076da;
}
.powerup[data-type="speed"] {
	background-position: -75px 0px;
	box-shadow: 0px 0px 20px #1eda00;
}

Bleiben wir gleich in unserer CSS-Datei. Die Projektile sollen Ihre Farbe ändern, sobald der Spieler ein rotes PowerUp eingesammelt hat. Der visuelle Effekt dient als Feedback für den Spieler, damit er erkennt, wann er mehr Schaden verursacht und wann der Effekt des roten PowerUps nachlässt.

Zudem erstellen wir einen Schild, der unser Schiff schmücken soll, sobald der Spieler ein blaues PowerUp einsammelt.

Kenney.nl hat für den Schild bereits Grafiken bereitgestellt und die Projektile gibt es nicht nur in Grün, sondern auch in Rot - Passend zu unserer PowerUp-Farbe.

.bullet {
	/* ... */
}
.bullet {
	background-image: url( 'laserGreen.png' );
}
.bullet[data-powerup="true"] {
	background-image: url( 'laserRed.png' );
}

.bullet_shot {
	/* ... */
}
.bullet_shot {
	background-image: url( 'laserGreenShot.png' );
}
.bullet_shot[data-powerup="true"] {
	background-image: url( 'laserRedShot.png' );
}

#shield[data-active="true"] {
	content: '';
	display: block;
	
	position: absolute;
	margin-left: -13px;
	margin-top: -18px;
	
	background-image: url( 'shield.png' );
	background-size: 75.5px 59px;
	background-repeat: no-repeat;
	
	width: 75.5px; 
	height: 59px;
	z-index: 10;
}

Eine Array soll uns als Verwaltung für alle PowerUps dienen. Damit können wir unserem Raumschiff beibringen, ob und wie viele PowerUps eingesammelt wurden. Die Array der Projektile oder der Meteore dienen uns dafür als Vorlage. Achte darauf, dass die Array wieder beim initialisieren unseres Spiels deklariert wird, damit wir von überall aus auf die PowerUps zugreifen können.

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

Die Erstellung der JavaScript-Klasse

Nun zum Hauptteil unserer Erweiterung: der JavaScript-Klasse Powerup. Wir können wieder einige Strukturen unserer Klasse Bullet oder Meteor verwenden, wie die ID, die Position oder die Direction.

Zu diesen Werten kommen noch die Variablen Type, die eine unserer 3 PowerUp-Typen speichert, die Variable Duration, um dem Powerup zu sagen, wie lange dieser aktiv sein soll und die Variable Multiplier, um die Feuerrate oder die Kraft des Projektils um einen X-Fachen Wert erhöht.

Der Schild benötigt zwar keinen Multiplier, aber wir setzen diese trotzdem und beachten sie einfach nicht in unseren weiteren Algorithmen.

Die Bewegung der PowerUps soll sich genauso verhalten, wie bei den Meteoren. Die Funktion move() kann einfach aus der Klasse Meteor wiederverwendet werden.

Tipp für fortgeschrittene JavaScript-Entwickler: Ihr merkt bestimmt, dass bei vielen Bereichen auch eine Vererbung angewandt werden kann. Da sich diese Anleitung eher an JavaScript-Anfänger richtet möchtet ich die Polymorphie an dieser Stelle nicht behandeln.

class Powerup {
	constructor( args ) {
		args = args || {};
		
		this.Destroyed = false;
		
		if( typeof args.ID !== "undefined" )
			this.ID = args.ID;
		else
			this.ID = myPowerups.length;
		
		if( typeof args.Type !== "undefined" )
			this.Type = args.Type;
		else {
			var randomType = Math.floor( ( Math.random() * 3 ) + 1 );
			switch( randomType ) {
				case 1: this.Type = "Speed"; break;
				case 2: this.Type = "Power"; break;
				case 3: this.Type = "Shield"; break;
				default: this.Type = "Speed"; break;
			}
		}
		
		if( typeof args.Duration !== "undefined" )
			this.Duration = args.Duration;
		else 
			this.Duration = 1000;
		
		if( typeof args.Multiplier !== "undefined" )
			this.Multiplier = args.Multiplier;
		else 
			this.Multiplier = 2.0;
		
		
		this.Position = { X: 0, Y: 0, H: 25, W: 25 };
		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;
		}
		
		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;
		}
		
		if( typeof args.Speed !== "undefined" )
			this.Speed = args.Speed;
		else
			this.Speed = 1;
	}
	update() {
		this.move();
	}
	move() {
		
		this.Position.X += this.Direction.X * this.Speed;
		this.Position.Y += this.Direction.Y * this.Speed;
		
	}
}

Als nächstes erstellen wir eine globale Funktion, die ein Powerup auf unser Spielfeld setzen soll. Da wir diese Funktion aufrufen werden, sobald ein großer Meteor zerstört wurde, können wir den Index des Meteors übergeben und prüfen, ob der abgeschossene Meteor vom Typ "Big" ist.

Danach erstellen wir eine zufällige Zahl zwischen 1 und 100. Wenn die Variable kleiner gleich 20 ist soll ein Powerup erscheinen. Dadurch hat der Spieler eine 20%-ige Chance, dass ein PowerUp erscheint. Der Typ des Powerups wird in unserer Klasse zufällig generiert, deshalb müssen wir den Typen nicht in unserer Argumenten-Liste angeben.

In den Argumenten benötigen wir die Position, an der sich unser PowerUp initial befinden soll. Die Position wird so errechnet, dass sie sich genau in der Mitte des Meteors befindet. Zudem übergeben wir dem PowerUp die Geschwindigkeit und die Richtung des Meteors.

function newPowerup( i ) {
	if( myMeteors[ i ].Type == "Big" ) {
		var randomSpawn = Math.floor( ( Math.random() * 100 ) + 1 );
		if( randomSpawn <= 20 ) {
			
			powerupArguments = {
				Position: {
					X: ( myMeteors[ i ].Position.X + myMeteors[ i ].Position.W/2 ) - 12.5,
					Y: ( myMeteors[ i ].Position.Y + myMeteors[ i ].Position.H/2 ) - 12.5
				},
				Speed: myMeteors[ i ].Speed,
				Direction: {
					X: myMeteors[ i ].Direction.X,
					Y: myMeteors[ i ].Direction.Y
				}
			};
			
			var powerup_outof_area = false;
			for( var i=0; i < myPowerups.length; i++ )
				if( myPowerups[ i ].Position.Y >= Field.Height + 20 || myPowerups[ i ].Destroyed ) {
					powerupArguments.ID = myPowerups[ i ].ID;
					myPowerups[ i ] = new Powerup( powerupArguments );
					powerup_outof_area = true;
					break;
				}
			if( !powerup_outof_area )
				myPowerups.push( new Powerup( powerupArguments ) );
			
		}
	}
}

Um die Funktion aufzurufen erweitern wir collision() in unserer Klasse Bullet, da diese Funktion bestimmt ob der Meteor zerstört wird. Im Parameter des Funktionsaufrufs newPowerup() wird der Index des zerstörten Meteors der Array myMeteors übergeben.

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 ].Life -= this.Damage;
						if( myMeteors[ i ].Life <= 0 ) {
							if( myMeteors[ i ].Type == "Big" )
								Score += 100;
							else 
								Score += 25;
							
							myMeteors[ i ].Destroyed = true;
							
							newPowerup( i );
						}
						
					}
		
	}
}

Da unsere Powerups nun im Umlauf sind können wir die update()-Funktion der PowerUps aufrufen. Durch das Update wird das PowerUp in Bewegung gebracht. Du kannst dir gerne aussuchen, ob du die Powerups bewegen lassen möchtest oder nicht. In diesem Fall empfehle ich dir jedoch die Items nach einer bestimmten Zeit verschwinden zu lassen, da der Browser nur eine bestimmte Anzahl an Collisions-Berechnungen schafft.

function mainLoop() {
	/* ... */
	
	for( var i=0; i < myPowerups.length; i++ )
		myPowerups[ i ].update();
	
	/* ... */
}

Damit sich PowerUps auch visuell auf dem Spielfeld befinden, müssen wir den Renderer erweitern. In folgendem Beispiel wurde die Funktion updateMeteors() dupliziert und für die Powerups angepasst. Die Funktion checkDestroyed() wurde danach erweitert, um die aufgesammelten (zerstörten) Powerups auszublenden.

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

Damit unser Raumschiff die PowerUps einsammeln kann benötigen wir eine Kollisionsabfrage in unserer Klasse Spaceship. Sobald ein PowerUp und das Raumschiff aufeinander treffen soll der Gegenstand "zerstört", werden und dem Raumschiff seine Werte übergeben. Dazu fragen wir den Typ, die Duration und den Multiplier ab und fügen diese zu einer Array PowerUps in unserem Schiff hinzu. Die "Duration" des PowerUps in dieser Array wird durch die update()-Funktion so lange nach unten gezählt, bis sie 0 erreicht. Ab diesem Zeitpunkt lässt der Effekt nach.

class Spaceship {
	constructor( args ) {
		/* ... */
		
		this.PowerUps = [];
	}
	update() {
		if( !GameOver ) {
			/* ... */
			
			for( var i=0; i < this.PowerUps.length; i++ ) {
				if( this.PowerUps[ i ].Duration > 0 )
					this.PowerUps[ i ].Duration -= 1;
			}
		}
	}
	move() {
		/* ... */
	}
	shoot() {
		/* ... */
	}
	collision() {
		/* ... */
		
		for( var i=0; i < myPowerups.length; i++ )
			if( !myPowerups[ i ].Destroyed )
				if( checkCollision( this, myPowerups[ i ] ) ) {
					
					myPowerups[ i ].Destroyed = true;
					var powerupArguments = {
						Type: myPowerups[ i ].Type,
						Multiplier: myPowerups[ i ].Multiplier,
						Duration: myPowerups[ i ].Duration
					};
					var empty_powerups = false;
					for( var i=0; i < this.PowerUps.length; i++ ) {
						if( this.PowerUps[ i ].Duration <= 0 ) {
							this.PowerUps[ i ] = powerupArguments;
							empty_powerups = true;
							break;
						}
					}
					if( !empty_powerups )
						this.PowerUps.push( powerupArguments );
					
				}
		
	}
}

Nachdem wir die Powerups nun aufsammeln können wird es Zeit die Funktionen zu erstellen, um das Raumschiff für eine bestimmte Zeit lang zu verbessern.

Die Feuerrate erhöhen

Um die Feuerrate zu erhöhen müssen wir in unserer Klasse Spaceship alle aktiven Powerups nach dem Typ "Speed" filtern und diese in einer Kontroll-Variable addieren. Der initiale Timer-Wert (in unserem Beispiel 80) wird dann durch den addierten Multiplikator-Wert dividiert.

Um Einbrüche in der Performance zu vermeiden wird emfohlen den Timer nicht zu stark abzusenken. Das kann mit einer Bedingung vermieden werden. Dazu frägt man den Timer, ob er unter einem bestimmten Wert liegt und setzt in diesem Fall eine Konstante Zahl als Wert in die Variable für die Geschwindigkeit ein.

class Spaceship {
	constructor( args ) {
		/* ... */
	}
	update() {
		/* ... */
	}
	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 ) {
				/* ... */
				
				var speedMultiplier = 1;
				for( var i=0; i < this.PowerUps.length; i++ ) {
					if( this.PowerUps[ i ].Type == "Speed" ) {
						if( this.PowerUps[ i ].Duration > 0 )
							speedMultiplier *= this.PowerUps[ i ].Multiplier;
					}
				}
				if( speedMultiplier >= 1 )
					this.shootTimer = 80 / speedMultiplier;
				if( this.shootTimer < 10 )
					this.shootTimer = 10;
				
			}
		}
	}
	collision() {
		/* ... */
	}
}

Den Projektilschaden erhöhen

 

Unsere Klasse Bullet besitzt eine Variable, die den Schaden des Projektils definiert. Diese Variable werden wir beim Initialisieren des Projektils erweitern.

Dazu müssen wir alle PowerUps mit einer Schleife durchlaufen und den Typen "Power" herausfiltern. Alle Multiplier der Powerups mit diesem Typ werden addiert und im Anschluss mit dem initialen Schaden des Projektils Multipliziert. Zudem können wir die Schuss-Animation um das Attribut data-powerup="true" erweitern, um die richtige CSS-Animation des verstärkten Projektils zu verwenden.

class Bullet {
	constructor( args ) {
		args = args || {};
		
		this.Destroyed = false;
		this.PowerupShot = false;
		
		/* ... */
		
		if( typeof args.Damage !== "undefined" ) this.Damage = args.Damage;
		else {
			this.Damage = 1;
			var damageMultiplier = 1;
			for( var i=0; i < mySpaceship.PowerUps.length; i++ ) {
				if( mySpaceship.PowerUps[i].Duration > 0 && mySpaceship.PowerUps[i].Type == "Power" )
					damageMultiplier += mySpaceship.PowerUps[i].Multiplier;
			}
			if( damageMultiplier > 1 ) {
				this.Damage *= damageMultiplier;
				this.PowerupShot = true;
			}
		}
		
		/* ... */
		
		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() { /* ... */ } move() { /* ... */ } collision() { /* ... */ } }

Um unseren Projektilen zu zeigen, dass sie stärker sind als üblich überprüfen wir im Renderer, ob das abzufeuernde Projektil ein Power-Schuss ist. Sollte das der Fall sein wird das Attribut data-powerup="true" wie in der Schuss-Animation gesetzt.

class Renderer {
	constructor() {
		/* ... */
	}
	update() {
		/* ... */
	}
	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'
					});
					if( myBullets[ i ].PowerupShot == false && $Bullet.attr( 'data-powerup' ) != false ) 
						$Bullet.attr( 'data-powerup', false );
					else if( myBullets[ i ].PowerupShot == true && $Bullet.attr( 'data-powerup' ) != true ) 
						$Bullet.attr( 'data-powerup', true );
				}
			}
			else {
				$( '#main_wrapper' ).append( '
' ); } } } updateMeteors() { /* ... */ } updatePowerups() { /* ... */ } checkDestroyed() { /* ... */ } checkGameOver() { /* ... */ } checkLifes() { /* ... */ } changeScore() { /* ... */ } }

Ein Schild erstellen

Es gibt 2 Möglichkeiten, wann der eingesammelte Schild unseres Raumschiffs verschwinden soll. Die erste Möglichkeit ist, wenn das Schiff Schaden durch einen Meteor oder anderes genommen hat. Die andere Möglichkeit ist, wenn der Timer des jeweiligen Schilds auf 0 gefallen ist.

Um das zu erreichen müssen wir zuerst unseren Renderer erweitern, damit dieser unser Schild anzeigen kann. Damit der Schild vor unserem Raumschiff schwebt, setzen wir die Position des Schilds gleich der Position des Raumschiffs. Falls Du möchtest kannst du den Schild auch etwas verschieben. Zudem sollte der Schild auch wieder ausgeblendet werden, sobald er nicht mehr aktiv ist.

class Renderer {
	constructor() {
		/* ... */
		this.Shield = $( '#main_wrapper #shield' );
	}
	update() {
		/* ... */
	}
	updateSpaceship() {
		/* ... */
		
		if( this.Shield.length ) {
			var shield_active = false;
			for( var i=0; i < mySpaceship.PowerUps.length; i++ ) {
				if( mySpaceship.PowerUps[ i ].Type == "Shield" && mySpaceship.PowerUps[ i ].Duration > 0 ) {
					if( this.Shield.attr( 'data-active' ) != true )
						this.Shield.attr( 'data-active', true );
					shield_active = true;
					break;
				}
			}
			if( !shield_active && this.Shield.attr( 'data-active' ) != false ) {
				this.Shield.attr( 'data-active', false );
			}
			this.Shield.css({
				'left' : mySpaceship.Position.X + 'px',
				'top' : mySpaceship.Position.Y + 'px'
			});
		}
		else {
			$( '#main_wrapper' ).append( '
' ); this.Shield = $( '#main_wrapper #shield' ); } } updateBullets() { /* ... */ } updateMeteors() { /* ... */ } updatePowerups() { /* ... */ } checkDestroyed() { /* ... */ } checkGameOver() { /* ... */ } checkLifes() { /* ... */ } changeScore() { /* ... */ } }

Damit unser Schild nicht nur zur gut aussieht, geben wir ihm eine Funktion. Dazu navigieren wir zurück zu unserer Klasse Spaceship um die Funktion collision() zu erweitern.

An dieser Stelle fragen wir zusätzlich ab, ob eine Kollision zwischen dem Schiff und einem Meteor stattfindet und ob ein Powerup mit dem Typ "Shield" aktiv ist. Wenn das der Fall ist verschwindet das Schild, das zuerst aufgesammelt wurde. In diesem Fall soll auch kein Leben abgezogen werden.

class Spaceship {
	constructor( args ) {
		/* ... */
	}
	update() {
		/* ... */
	}
	move() {
		/* ... */
	}
	shoot() {
		/* ... */
	}
	collision() {
		
		for( var i=0; i < myMeteors.length; i++ )
			if( !myMeteors[ i ].Destroyed )
				if( checkCollision( this, myMeteors[ i ] ) ) {
					
					var shield_exists = false;
					for( var j=0; j < this.PowerUps.length; j++ ) 
						if( this.PowerUps[ j ].Type == "Shield" && this.PowerUps[ j ].Duration > 0 ) {
							this.PowerUps[ j ].Duration = 0;
							shield_exists = true;
							break;
						}
					
					if( !shield_exists ) {
						this.Life -= 1;
						if( this.Life <= 0 )
							GameOver = true;
					}
					
					/* ... */
					
				}
		
		/* ... */
	}
}

Nun haben wir unser Spiel mit den PowerUps erweitert. Unser Raumschiff kann nun temporär verbessert werden um uns für den Angriff der Alien-Raumschiffe zu wappnen.

 

In den nächsten Beiträgen erstellen wir richtige Gegner, die versuchen werden unser Raumschiff zu zerstören. Durch eine kleine KI werden die Aliens unser Raumschiff sogar verfolgen oder in Deckung gehen.

 

Codepalm
Spieleprogrammierung für Einsteiger
Teil 9: Power Ups in Videospielen programmieren