Das ABC der Android-Spieleentwicklung: Erkennen Sie Kollisionen

Das Ziel des Android-Spiels, das wir in dieser Serie entwickeln, ist es, den Schwung der fliegenden Untertasse anzupassen, um zu vermeiden, dass der Asteroid ihn in Stücke zerschmettert. Das Projekt ist zwar simpel, enthält jedoch alle wesentlichen Aspekte eines Videospiels: Leinwand, Sprites, Animation, Kollisionserkennung und Benutzereingaben.

In unserem letzten Tutorial haben wir es geschafft, unsere Sprites auf dem Bildschirm zu bewegen. (Wenn Sie neu in dieser Serie sind, lesen Sie die Teile eins und zwei: Bereiten Sie die Leinwand vor und laden und zeigen Sie Sprites an.) Jetzt müssen wir erkennen können, wann der Asteroid und das Schiff kollidieren. Grundsätzlich gibt es zwei Techniken zur Kollisionserkennung: Begrenzungsalgorithmen und Bildmaskierung. Das Bounding ist einfacher und nimmt weniger CPU-Zyklen in Anspruch als die Bildmaskierung. Für Arcade-Spiele ist die Begrenzung jedoch normalerweise nicht genau genug. Was ist der Unterschied? Betrachten Sie unsere beiden Bilder. Ich habe beide auf einem 50x50-Pixel-Hintergrund, aber der Inhalt dieser Sprites füllt nicht das gesamte 50x50-Pixel-Raster. Schauen Sie sich die Abbildungen in Abbildung A und Abbildung B an . Abbildung A.

Unser UFO-Sprite mit transparenten Bereichen.
Abbildung B.

Das Asteroiden-Sprite mit seinen transparenten Bereichen.
Wenn wir zur Erkennung von Kollisionen eine Begrenzung verwenden würden, würden wir einfach jedes Mal durch unsere Schleife prüfen, ob sich die 50x50-Rechtecke überlappen. Wenn sie sich überlappen, betrachten wir dies als Kollision. Anhand der obigen Bilder können Sie sehen, dass diese Technik für Kollisionen auf der X-Achse verfälscht werden kann. Kollisionen auf der Y-Achse sind nicht so trocken. ( Abbildung C ) Abbildung C.

Grundlegende Erkennung von Grenzkollisionen und deren mangelnde Genauigkeit.

Wie ich bereits sagte, führt der Vergleich der Nähe der beiden Sprites auf Pixel-für-Pixel-Basis (manchmal als Bitmaskierung bezeichnet und manchmal als pixelgenaue Kollisionserkennung bezeichnet) zu einem viel genaueren Kollisionserkennungsmechanismus. Dies kann zeitaufwändig sein, da Sie grundsätzlich die einzelnen Pixel in beiden Bildern scannen und nach Überlappungen suchen müssen. In den meisten Spielen wird eine Kombination aus beiden verwendet, und das ist der Ansatz, den wir auch verwenden werden.

Solange sich die äußeren Rechtecke nicht überlappen, markieren wir das Kollisionsflag als falsch. Wenn sich die Rechtecke überlappen, werden nur die Pixel im überlappenden Bereich gescannt. Jedes Mal, wenn wir im überlappenden Bereich ein Pixel haben, das in beiden Bildern nicht transparent ist, sind unsere Sprites ineinander gestürzt.

Kollisionen erkennen

Dieses Tutorial baut auf dem auf, was wir in Teil drei erstellt haben. Um wirklich zu verstehen, was los ist, ist es hilfreich, den Code im Kontext des Ganzen zu sehen. Daher ist die Codeliste im Tutorial unsere vollständige Arbeitsbasis, wobei der neue Code inline kommentiert wird. Sie können den schrittweisen Anweisungen folgen oder das gesamte Projekt herunterladen und in Eclipse importieren.

1. Erstellen Sie ein neues Android-Projekt in Eclipse. Ziel Android 2.1 oder höher. Stellen Sie sicher, dass Sie die Startaktivität Main.java und das entsprechende Layout in main.xml umbenennen.

2. Obwohl seit dem dritten Teil keine Änderungen an unserer Manifestdatei oder unserem Layout vorgenommen wurden, habe ich der Vollständigkeit halber beide unten aufgeführt.

AndroidManifest.xml

 "http://schemas.android.com/apk/res/android" 
 package = "com.authorwjf.gamedevtut04" 
 android: versionCode = "1" 
 android: versionName = "1.0" > 
 android: minSdkVersion = "7" 
 android: targetSdkVersion = "15" /> 
 android: icon = "@ drawable / ic_launcher" 
 android: label = "@ string / app_name" 
 android: theme = "@ style / AppTheme" > 
 android: name = ".Main" 
 android: label = "@ string / title_activity_main" 
 android: screenOrientation = "Porträt" android: configChanges = "Ausrichtung | Tastatur versteckt " > 
 "android.intent.action.MAIN" /> 
 "android.intent.category.LAUNCHER" /> 
 main.xml 
 "http://schemas.android.com/apk/res/android" 
 android: layout_width = "fill_parent" 
 android: layout_height = "fill_parent" 
 android: orientierung = "vertikal" > 
 android: layout_width = "wrap_content" 
 android: layout_height = "wrap_content" 
 android: layout_gravity = "top | center" 
 android: text = "ABC's von Android Game Dev" /> 
 android: id = "@ + id / the_button" 
 android: layout_width = "wrap_content" 
 android: layout_height = "wrap_content" 
 android: layout_gravity = "center" 
 Android: Schwerkraft = "Zentrum" 
 android: enabled = "false" 
 android: text = "Zurücksetzen" /> 
 android: layout_width = "wrap_content" 
 android: layout_height = "wrap_content" 
 android: layout_gravity = "center" 
 android: text = "Sprite-Geschwindigkeit (?, ?)" 
 android: id = "@ + id / the_label" /> 
 android: layout_width = "wrap_content" 
 android: layout_height = "wrap_content" 
 android: layout_gravity = "center" 
 android: text = "Letzte Kollision XY (?, ?)" 
 android: id = "@ + id / the_other_label" /> 
 android: layout_width = "fill_parent" 
 android: layout_height = "fill_parent" 
 android: layout_margin = "20dip" 
 android: id = "@ + id / the_canvas" /> 

3. Wie in den vorherigen Tutorials haben wir einen Ordner / drawable im Verzeichnis / res erstellt, in dem wir unsere beiden Sprite-Images ablegen: ufo.png und asteroid.png. Jetzt, da wir die Kollisionserkennung durchführen, wird die Größe wichtiger. Wenn Sie also Ihre eigenen Bilder verwenden, empfehle ich, beide auf 50 x 50 Pixel zu skalieren. Außerdem müssen Sie sicherstellen, dass die Bilder mit einem transparenten Hintergrund gespeichert wurden.

4. Der größte Teil der Magie findet diesmal in der Datei com.authorwjf.drawable.GameBoard.java statt. Hier werden wir jedes Mal durch die On-Draw-Schleife nach einer Kollision suchen. Wenn eine Kollision auftritt, aktualisieren wir das Flag und die Koordinaten und zeichnen ein rotes x, das angibt, wo die Kollision aufgetreten ist.

Wenn Ihnen die Überprüfung der Kollisionsfunktion zu diesem Zeitpunkt noch verwirrend erscheint, ist dies in Ordnung. Sie können sie in Ihren Projekten weitgehend durch Ausschneiden und Einfügen verwenden. Mit der Zeit werden Sie sich mit der Technik allmählich besser vertraut machen.

GameBoard.java

 Paket com.authorwjf.drawing; 
 import java.util.ArrayList; 
 import java.util.List; 
 import java.util.Random; 
 import com.authorwjf.gamedevtut04.R; 
 android.content.Context importieren ; 
 android.graphics.Bitmap importieren ; 
 android.graphics.BitmapFactory importieren ; 
 android.graphics.Canvas importieren ; 
 android.graphics.Color importieren ; 
 android.graphics.Matrix importieren ; 
 android.graphics.Paint importieren ; 
 android.graphics.Point importieren ; 
 android.graphics.Rect importieren ; 
 import android.util.AttributeSet; 
 android.view.View importieren ; 
 public class GameBoard erweitert View { 
 Privatfarbe p; 
 private Liste starField = null ; 
 private int starAlpha = 80; 
 private int starFade = 2; 
 private Rect sprite1Bounds = new Rect (0, 0, 0, 0); 
 private Rect sprite2Bounds = new Rect (0, 0, 0, 0); 
 privater Punkt sprite1; 
 privater Punkt sprite2; 
 private Bitmap bm1 = null ; 
 private Matrix m = null ; 
 private Bitmap bm2 = null ; 
 // Kollisionsflagge und Punkt 
 private boolean collisionDetected = false ; 
 privater Punkt lastCollision = neuer Punkt (-1, -1); 
 private int sprite1Rotation = 0; 
 private static final int NUM_OF_STARS = 25; 
 // Erlaube unserem Controller, die Sprite-Positionen abzurufen und einzustellen 
 // Sprite 1 Setter 
 synchronisierte öffentliche Leere setSprite1 ( int x, int y) { 
 sprite1 = neuer Punkt (x, y); 
 }} 
 // Sprite 1 Getter 
 synchronized public int getSprite1X () { 
 return sprite1.x; 
 }} 
 synchronized public int getSprite1Y () { 
 return sprite1.y; 
 }} 
 // Sprite 2 Setter 
 synchronisierte öffentliche Leere setSprite2 ( int x, int y) { 
 sprite2 = neuer Punkt (x, y); 
 }} 
 // Sprite 2 Getter 
 synchronisierte öffentliche int getSprite2X () { 
 return sprite2.x; 
 }} 
 synchronized public int getSprite2Y () { 
 return sprite2.y; 
 }} 
 synchronisierte öffentliche Leere resetStarField () { 
 starField = null ; 
 }} 
 // Sprite-Grenzen dem Controller aussetzen 
 synchronisierte öffentliche int getSprite1Width () { 
 return sprite1Bounds.width (); 
 }} 
 synchronized public int getSprite1Height () { 
 return sprite1Bounds.height (); 
 }} 
 synchronisierte öffentliche int getSprite2Width () { 
 return sprite2Bounds.width (); 
 }} 
 synchronized public int getSprite2Height () { 
 return sprite2Bounds.height (); 
 }} 
 // den Punkt der letzten Kollision zurückgeben 
 synchronisierter öffentlicher Punkt getLastCollision () { 
 return lastCollision; 
 }} 
 // das Kollisionsflag zurückgeben 
 synchronisierter öffentlicher Boolescher Wert wasCollisionDetected () { 
 return collisionDetected; 
 }} 
 öffentliches GameBoard (Kontextkontext, AttributeSet aSet) { 
 super (Kontext, aSet); 
 p = neue Farbe (); 
 // lade unsere Bitmaps und setze die Grenzen für den Controller 
 sprite1 = neuer Punkt (-1, -1); 
 sprite2 = neuer Punkt (-1, -1); 
 // Definiere eine Matrix, damit wir den Asteroiden drehen können 
 m = neue Matrix (); 
 p = neue Farbe (); 
 bm1 = BitmapFactory. decodeResource (getResources (), R.drawable. asteroid ); 
 bm2 = BitmapFactory. decodeResource (getResources (), R.drawable. ufo ); 
 sprite1Bounds = new Rect (0, 0, bm1.getWidth (), bm1.getHeight ()); 
 sprite2Bounds = new Rect (0, 0, bm2.getWidth (), bm2.getHeight ()); 
 }} 
 synchronisierte private void initializeStars ( int maxX, int maxY) { 
 starField = new ArrayList (); 
 für ( int i = 0; i < NUM_OF_STARS ; i ++) { 
 Random r = new Random (); 
 int x = r.nextInt (maxX-5 + 1) +5; 
 int y = r.nextInt (maxY-5 + 1) +5; 
 starField.add ( neuer Punkt (x, y)); 
 }} 
 collisionDetected = false ; 
 }} 
 private boolean checkForCollision () { 
 if (sprite1.x <0 && sprite2.x <0 && sprite1.y <0 && sprite2.y <0) return false ; 
 Rect r1 = new Rect (sprite1.x, sprite1.y, sprite1.x + sprite1Bounds.width (), sprite1.y + sprite1Bounds.height ()); 
 Rect r2 = new Rect (sprite2.x, sprite2.y, sprite2.x + sprite2Bounds.width (), sprite2.y + sprite2Bounds.height ()); 
 Rechteck r3 = neues Rechteck (r1); 
 if (r1.intersect (r2)) { 
 für ( int i = r1.left; i 
 für ( int j = r1.top; j 
 if (bm1.getPixel (i-r3.left, j-r3.top)! = Farbe. TRANSPARENT ) { 
 if (bm2.getPixel (i-r2.left, j-r2.top)! = Farbe. TRANSPARENT ) { 
 lastCollision = neuer Punkt (sprite2.x + i-r2.left, sprite2.y + j-r2.top); 
 return true ; 
 }} 
 }} 
 }} 
 }} 
 }} 
 lastCollision = neuer Punkt (-1, -1); 
 return false ; 
 }} 
 @Override 
 synchronisierte öffentliche Leere onDraw (Canvas Canvas) { 
 p.setColor (Farbe. SCHWARZ ); 
 p.setAlpha (255); 
 p.setStrokeWidth (1); 
 canvas.drawRect (0, 0, getWidth (), getHeight (), p); 
 if (starField == null ) { 
 initializeStars (canvas.getWidth (), canvas.getHeight ()); 
 }} 
 p.setColor (Farbe. CYAN ); 
 p.setAlpha (starAlpha + = starFade); 
 if (starAlpha> = 252 || starAlpha <= 80) starFade = starFade * -1; 
 p.setStrokeWidth (5); 
 für ( int i = 0; i < NUM_OF_STARS ; i ++) { 
 canvas.drawPoint (starField.get (i) .x, starField.get (i) .y, p); 
 }} 
 if (sprite1.x> = 0) { 
 m.reset (); 
 m.postTranslate (( float ) (sprite1.x), ( float ) (sprite1.y)); 
 m.postRotate (sprite1Rotation, ( float ) (sprite1.x + sprite1Bounds.width () / 2.0), ( float ) (sprite1.y + sprite1Bounds.width () / 2.0)); 
 canvas.drawBitmap (bm1, m, null ); 
 sprite1Rotation + = 5; 
 if (sprite1Rotation> = 360) sprite1Rotation = 0; 
 }} 
 if (sprite2.x> = 0) { 
 canvas.drawBitmap (bm2, sprite2.x, sprite2.y, null ); 
 }} 
 // Die letzte Aufgabe besteht darin, nach einer Kollision zu suchen 
 collisionDetected = checkForCollision (); 
 if (collisionDetected) { 
 // Wenn es eines gibt, können wir ein rotes X zeichnen 
 p.setColor (Farbe. ROT ); 
 p.setAlpha (255); 
 p.setStrokeWidth (5); 
 canvas.drawLine (lastCollision.x - 5, lastCollision.y - 5, lastCollision.x + 5, lastCollision.y + 5, p); 
 canvas.drawLine (lastCollision.x + 5, lastCollision.y - 5, lastCollision.x - 5, lastCollision.y + 5, p); 
 }} 
 }} 
 }} 

5. In der Datei /src/Main.java müssen noch einige Änderungen vorgenommen werden. Zuerst möchten wir dem Frame-Update eine Überprüfung des Kollisionsflags hinzufügen. Wenn eine Kollision aufgetreten ist, aktualisieren wir das xy-Label und brechen es dann ab. Zweitens möchten wir sicherstellen, dass unsere Schaltfläche Zurücksetzen den Bildschirm ungültig macht. Dadurch wird sichergestellt, dass durch Drücken von Zurücksetzen das Spiel von vorne gestartet wird.

Main.java

 Paket com.authorwjf.gamedevtut04; 
 import java.util.Random; 
 import com.authorwjf.drawing.GameBoard; 
 import android.os.Bundle; 
 import android.os.Handler; 
 android.view.View importieren ; 
 android.view.View.OnClickListener importieren ; 
 android.widget.Button importieren ; 
 android.widget.TextView importieren ; 
 android.app.Activity importieren ; 
 android.graphics.Point importieren ; 
 public class Main erweitert Activity implementiert OnClickListener { 
 privater Handler-Frame = neuer Handler (); 
 // Geschwindigkeit beinhaltet die Geschwindigkeit und die Richtung unserer Sprite-Bewegung 
 private Point sprite1Velocity; 
 privater Punkt sprite2Velocity; 
 private int sprite1MaxX; 
 private int sprite1MaxY; 
 private int sprite2MaxX; 
 private int sprite2MaxY; 
 // Teilen Sie den Frame durch 1000, um zu berechnen, wie oft pro Sekunde der Bildschirm aktualisiert wird. 
 private static final int FRAME_RATE = 20; // 50 Bilder pro Sekunde 
 @Override 
 public void onCreate (Bundle savedInstanceState) { 
 super .onCreate (savedInstanceState); 
 setContentView (R.layout. main ); 
 Handler h = neuer Handler (); 
 ((Button) findViewById (R.id. the_button )). SetOnClickListener ( this ); 
 // Wir können die Grafiken nicht sofort initialisieren, da der Layout-Manager 
 // muss zuerst ausgeführt werden, also rufe in einer Sekunde zurück. 
 h.postDelayed ( new Runnable () { 
 @Override 
 public void run () { 
 initGfx (); 
 }} 
 }, 1000); 
 }} 
 privater Punkt getRandomVelocity () { 
 Random r = new Random (); 
 int min = 1; 
 int max = 5; 
 int x = r.nextInt (max-min + 1) + min; 
 int y = r.nextInt (max-min + 1) + min; 
 neuen Punkt zurückgeben (x, y); 
 }} 
 privater Punkt getRandomPoint () { 
 Random r = new Random (); 
 int minX = 0; 
 int maxX = findViewById (R.id. the_canvas ) .getWidth () - ((GameBoard) findViewById (R.id. the_canvas )). getSprite1Width (); 
 int x = 0; 
 int minY = 0; 
 int maxY = findViewById (R.id. the_canvas ) .getHeight () - ((GameBoard) findViewById (R.id. the_canvas )). getSprite1Height (); 
 int y = 0; 
 x = r.nextInt (maxX-minX + 1) + minX; 
 y = r.nextInt (maxY-minY + 1) + minY; 
 neuen Punkt zurückgeben (x, y); 
 }} 
 synchronisierte öffentliche Leere initGfx () { 
 ((GameBoard) findViewById (R.id. the_canvas )). ResetStarField (); 
 Punkt p1, p2; 
 do { 
 p1 = getRandomPoint (); 
 p2 = getRandomPoint (); 
 } while (Math. abs (p1.x - p2.x) <((GameBoard) findViewById (R.id. the_canvas )). getSprite1Width ()); 
 ((GameBoard) findViewById (R.id. the_canvas )). SetSprite1 (p1.x, p1.y); 
 ((GameBoard) findViewById (R.id. the_canvas )). SetSprite2 (p2.x, p2.y); 
 // Gib dem Asteroiden eine zufällige Geschwindigkeit 
 sprite1Velocity = getRandomVelocity (); 
 // Fixiere die Schiffsgeschwindigkeit vorerst auf eine konstante Geschwindigkeit 
 sprite2Velocity = neuer Punkt (1, 1); 
 // Setze unsere Grenzen für die Sprites 
 sprite1MaxX = findViewById (R.id. the_canvas ) .getWidth () - ((GameBoard) findViewById (R.id. the_canvas )). getSprite1Width (); 
 sprite1MaxY = findViewById (R.id. the_canvas ) .getHeight () - ((GameBoard) findViewById (R.id. the_canvas )). getSprite1Height (); 
 sprite2MaxX = findViewById (R.id. the_canvas ) .getWidth () - ((GameBoard) findViewById (R.id. the_canvas )). getSprite2Width (); 
 sprite2MaxY = findViewById (R.id. the_canvas ) .getHeight () - ((GameBoard) findViewById (R.id. the_canvas )). getSprite2Height (); 
 ((Button) findViewById (R.id. the_button )). SetEnabled ( true ); 
 frame.removeCallbacks (frameUpdate); 
 ((GameBoard) findViewById (R.id. the_canvas )). Invalidate (); 
 frame.postDelayed (frameUpdate, FRAME_RATE ); 
 }} 
 @Override 
 synchronisierte öffentliche Leere onClick (View v) { 
 initGfx (); 
 }} 
 private Runnable frameUpdate = new Runnable () { 
 @Override 
 synchronisierter öffentlicher void run () { 
 // Bevor wir etwas anderes tun, suchen Sie nach einer Kollision 
 if (((GameBoard) findViewById (R.id. the_canvas )). wasCollisionDetected ()) { 
 Punkt collisionPoint = ((GameBoard) findViewById (R.id. the_canvas )). GetLastCollision (); 
 if (collisionPoint.x> = 0) { 
 ((TextView) findViewById (R.id. the_other_label )). SetText ("Last 

Kollision XY

("+ Integer. ToString (collisionPoint.x) +", "+ Integer. ToString (collisionPoint.y) +") ");
 }} 
 // schalte die Animation aus, bis Reset gedrückt wird 
 zurück ; 
 }} 
 frame.removeCallbacks (frameUpdate); 
 Punkt sprite1 = neuer Punkt (((GameBoard) findViewById (R.id. the_canvas )). GetSprite1X (), 
 ((GameBoard) findViewById (R.id. the_canvas )). GetSprite1Y ()); 
 Punkt sprite2 = neuer Punkt (((GameBoard) findViewById (R.id. the_canvas )). GetSprite2X (), 
 ((GameBoard) findViewById (R.id. the_canvas )). GetSprite2Y ()); 
 sprite1.x = sprite1.x + sprite1Velocity.x; 
 if (sprite1.x> sprite1MaxX || sprite1.x <5) { 
 sprite1Velocity.x * = -1; 
 }} 
 sprite1.y = sprite1.y + sprite1Velocity.y; 
 if (sprite1.y> sprite1MaxY || sprite1.y <5) { 
 sprite1Velocity.y * = -1; 
 }} 
 sprite2.x = sprite2.x + sprite2Velocity.x; 
 if (sprite2.x> sprite2MaxX || sprite2.x <5) { 
 sprite2Velocity.x * = -1; 
 }} 
 sprite2.y = sprite2.y + sprite2Velocity.y; 
 if (sprite2.y> sprite2MaxY || sprite2.y <5) { 
 sprite2Velocity.y * = -1; 
 }} 
 ((GameBoard) findViewById (R.id. the_canvas )). SetSprite1 (sprite1.x, sprite1.y); 
 ((GameBoard) findViewById (R.id. the_canvas )). SetSprite2 (sprite2.x, sprite2.y); 
 ((GameBoard) findViewById (R.id. the_canvas )). Invalidate (); 
 frame.postDelayed (frameUpdate, FRAME_RATE ); 
 }} 
 }; 
 }} 

Laden Sie das Tutorial auf ein Gerät oder einen Emulator und warten Sie, bis das Schiff und der Asteroid kollidieren. Wenn es zu lange dauert, drücken Sie Reset und sowohl das UFO als auch der Asteroid erhalten neue Flugbahnen.

Zu diesem Zeitpunkt haben Sie ein Spiel, wenn auch das frustrierendste der Welt. Während beide Sprites herumspringen und kollidieren, haben Sie auch keine Kontrolle darüber. Der nächste Beitrag in dieser Reihe wird grundlegende Benutzereingaben behandeln und uns gerade genug Kontrolle über das außerirdische Raumschiff geben, um diesem eingehenden Asteroiden aus dem Weg zu gehen.

© Copyright 2021 | pepebotifarra.com