7. Oktober 2013
BREAK OUT – FREI ZUM ABSCH(L)USS Wir erhielten ein Template des Arcade-Klassikers „Break Out“ und die Aufgabe, dieses nach unseren Vorstellungen und Möglichkeiten zu neu gestalten. Mein oberstes Ziel war es, das altbekannte Gameplay abwechslungsreicher und komplexer zu machen. Mir war bewusst, dass dies meine Fähigkeiten schnell übersteigen konnte – der Code war mir von Anfang an schon komplex genug. Abwechslungsreichere Interaktionen und „aktivere Gegner“ (als statische Klötze) konnte ich alleine noch nicht programmieren. Mit einer simplen aber wirksamen Methode versuchte ich, dem Spieler trotzdem ein meist intelligent und komplex reagierendes Gegenüber zu schaffen: Ein zweiter User sollte ihn herausfordern können (fortan „Player2“). Dieser sollte den Angriffen des ehemaligen Einzelspielers („Player1“) ausweichen und ihm den Sieg streitig machen. Als Eingabegerät nutzte ich aus – aus Mangel an Kenntnis und Alternativen - schlicht die Pfeiltasten des Keyboards. Zu meinem Erfreuen gelang es mir, die Grundfunktion des Ausweichens in „Eigenregie“ zu schreiben. Einzig für das kontinuierliche Fahren bei gedrückt gehaltener Pfeilaste musste ich David um Rat Fragen. Es war mir grundsätzlich sehr wichtig, dass ich alle Programmierungen selbstständig umsetzten - oder notfalls zumindest durchschauen - konnte. Das Spiel hatte vorerst noch das Problem, dass sich ein findiger/feiger Player2 unversehrt aus der Bildfläche bewegen und wieder zurückkommen konnte, um den Schüssen auszuweichen. Nach unzähligen Fehlschlägen programmierte ich unabsichtlich eine Lösung die zwar nicht optimal, dafür aber wirksam und für mich umsetzbar war: Wenn Bricks den Bildrand überschritten, wurden sie gelöscht. Eine konsequente Bestrafung für das Ausnutzen eines Bugs. Erst jetzt machte ich mir Gedanken über die „Story“ des Spiels und deren grafische Umsetzung. Dieses Vorgehen ist bei mir - und bei richtigen Programmierern wohl auch – weder üblich noch sehr sinnvoll. Der Grund dafür war, dass ich der grössten Herausforderung – dem Ändern des Gameplays – hohe Priorität gab; solange ich nicht wusste, ob ich den gewünschten Spielmodus überhaupt hinkriegte, war es sinnlos dessen grafische Umsetzung zu planen. Ich interpretierte die ursprünglichen Bricks als Zähne und die beiden Spieler – da sie sehr unterschiedliche Aufgaben hatten – als zwei ungleiche Gegner: Einen Frosch und eine Fliege. Wie mir später auffiel, war diese Idee sicher nicht der Weisheit’ letzter Schluss. Zwar habe ich die Entscheidung nicht sehr schwer gewichtet und eher willkürlich gefällt, wie ich aber bei der Darstellung der Bricks als Zähne ausgerechnet auf einen Frosch kam, konnte ich mir schon in der Umsetzungsphase nicht mehr erklären. Passend zu der beschränkten Bedienung (2 Spieler an 1 PC) und der fragwürdigen Story (eben), wählte ich den grafischen Stil früher „in-browser games“ (e.g. Bubble Trouble, Gold Miner, etc.). Als ich das Spiel mit Klassenkameraden testete, merkte ich bald, dass hier mit ungleichen Spiessen gekämpft wurde: Noch immer war es Player1 fast nicht möglich die „Zähne“ des Froschs zu treffen, da sich der Ball nur sehr begrenzt Steuern liess. Um seine Waffe trickreicher zu machen, gab ich Player1 die Möglichkeit den Spielball durch Mausklicks im Flug anzuhalten. Doch wie sich herausstellte reichte das nicht, also beschloss ich anstelle eines Stopps, einen Richtungswechsel als Reaktion auf Mausklicks zu programmieren. Wie bei fast allen Änderungen die ich vornahm, verbrachte ich auch bei dieser sehr viel Zeit damit, den Code zu analysieren um die entscheidende Funktion zu finden. Trotz allem gelang es mir leider nicht, die Bewegung umzudrehen, da ich es hier mit Vektoren zu tun hatte welche ich nicht durch simples „*-1“ ins Negative kehren konnte. Ich erhielt Hilfe und muss eingestehen, dass ich wohl noch lange nicht selbstständig auf die richtige Lösung gekommen wäre. Ebenso wenig, schaffte ich es den letzten gravierenden Bug aus dem Programm zu kriegen: sobald alle „Froschzähne“ zerstört waren, sollte sich ein Bild mit Glückwünschen an die Fliege öffenen, was aber nicht geschah. Ich versuchte mit einer „count“ Funktion die Anzahl der übrigen Zähne zu eruieren, diese gab aber oft unsinnige Werte an. Die Zeitlimite und mein Ziel möglichst vieles selbst zu programmieren, verhinderten dass ich das Problem noch lösen konnte. Ich vermute aber dass es mit dem zuvor programmierten „Löschen der Zähne bei Verlassen des Spielfelds“ zusammenhängt. Dies ist wohl der Preis, den man für meine „unsauberen doch zweckmässigen“ Lösungen bezahlen musste. /* -------------------------------------------------------------------------- * BreakOut * -------------------------------------------------------------------------- * prog: Max Rheiner * date: 26/09/2012 (m/d/y) * ---------------------------------------------------------------------------- */ import ddf.minim.*; Minim minim; AudioSample ballHitSnd; AudioSample ballHitSchlaegerSnd; AudioPlayer froschtod; AudioPlayer naturpur; import java.util.*; HashSet keysDown = new HashSet(); PImage Kopf; PShape Fliege; PImage Tuempel; PImage Anleitung; PImage Gameover; int deltaBrickX; int halt; int BallRad = 7; BouncingBall ball; // ball der Klasse BouncingBall Racket schlaeger = null; Brick[] brickList = null; // ist ein Array //Gamemodi final static int GAME_MODE_MENU = 0; //"final static" heisst "nur Lesen" final static int GAME_MODE_PLAY = 1; final static int GAME_MODE_GAMEOVER = 2; int gameMode = GAME_MODE_MENU; // Variabel die den Gamemode speichert void setup() { Tuempel = loadImage("pond2.jpg"); Fliege = loadShape("Fliege.svg"); Kopf = loadImage("Grinsegrind.png"); Anleitung = loadImage("Anleitung.png"); Gameover = loadImage("Gameover.png"); size(800, 600); ball = new BouncingBall(width/2, height/2, BallRad); // spielball Position und Radius schlaeger = new Racket(0, height - 40, 300, 15); // sound init minim = new Minim(this); ballHitSnd = minim.loadSample("quack1.wav"); ballHitSchlaegerSnd = minim.loadSample("fly1.wav"); froschtod = minim.loadFile("froschtod.wav"); naturpur = minim.loadFile("naturpur.mp3"); smooth(); noCursor(); // macht cursor unsichtbar } void initGameScene() // kreiert ein neues Spielfeld { naturpur.play(); // setzte die start posititon des balles ball.set(new PVector(width/2, height/2), new PVector(random(-7, 7), -7), 1); // erzeuge die ziegel brickList = new Brick[(int)random(5, 9)]; // 3-12 Ziegel for (int i=0;i < brickList.length;i++) // Abstand der Ziegel { brickList[i] = new Brick(300 + i*30, 100, 60, 20); } } void draw() { background(0); if (keyDown(LEFT)) { deltaBrickX -=5; } else if (keyDown(RIGHT)) { deltaBrickX +=5; } image(Tuempel, 0, -120, width*1.3, height*1.3); // ghosting //fill(0, 0, 0, 100); //rect(0, 0, width, height); drawScene(); // DARSTELLUNG SPIELFELD } void drawScene() { switch(gameMode) { case GAME_MODE_MENU: drawMenu(); break; case GAME_MODE_PLAY: drawGame(); break; case GAME_MODE_GAMEOVER: drawGameOver(); break; } } void drawMenu() { image(Anleitung, 0, 0); /* pushStyle(); fill(0, 200, 200, 255); text("BreakOut", 350, 200); fill(0, 150, 200, 255); text("\'p\' : start game", 350, 230); fill(0, 100, 200, 255); text("Space : stop game", 350, 250); popStyle(); */ } void drawGameOver() { image(Gameover,0,0); } PVector kopfpos = new PVector(100, 100); void drawGame() // HIER GEHT DAS SPIEL AB { // update die daten, berechne kollisionen updateScene(); // zeichne ziegel fill(255); kopfpos.x += deltaBrickX; image(Kopf, kopfpos.x-85, kopfpos.y-55, 800, 300); for (int i=0;i 0 && brickList[i]._pos.x < width) { brickList[i]._pos.x += deltaBrickX; brickList[i].draw(); } } int count = brickList.length; for (int i = 0; i < count;i++) { if ( brickList[i] == null) { count -= 1; } } println(count); if (count height- ball._r) { gameMode = GAME_MODE_GAMEOVER; froschtod.play(1); return; } // update schlaeger pos schlaeger.move(mouseX, mouseY); PVector hitPos = new PVector(); float hitV; float speed; // teste ob ball den schlaeger trifft int retVal = schlaeger.checkCollision(ball._pos.x, ball._pos.y, ball._r, ball._dir, hitPos); switch(retVal) { case Brick.HIT_LEFT: // aendere flugrichtung ball ball._dir.x *= -1; // setzte ball buendig auf den schlaeger(dass er nicht im schlaeger ist) ball._pos.x = schlaeger._pos.x - schlaeger._w/2 - ball._r; // spiele sound ballHitSchlaegerSnd.trigger(); break; case Brick.HIT_RIGTH: // aendere flugrichtung ball ball._dir.x *= -1; // setzte ball buendig auf den schlaeger(das er nicht im schlaeger ist) ball._pos.x = schlaeger._pos.x + schlaeger._w/2 + ball._r; // spiele sound ballHitSchlaegerSnd.trigger(); break; case Brick.HIT_TOP: // aendere flugrichtung ball // anschnitt berechnen hitV = hitPos.x - (schlaeger._pos.x - schlaeger._w/2); hitV = 1.0 / schlaeger._w * hitV - 0.5; speed = ball._dir.mag(); ball._dir.x += hitV * 8; ball._dir.y *= -1; // ball soll die gleiche geschwindigkeit haben wie am anfang ball._dir.normalize(); ball._dir.mult(speed); // setzte ball buendig auf den schlaeger(das er nicht im schlaeger ist) ball._pos.y = schlaeger._pos.y - schlaeger._h/2 - ball._r ; // spiele sound ballHitSchlaegerSnd.trigger(); break; case Brick.HIT_BOTTOM: // aendere flugrichtung ball // anschnitt berechnen hitV = hitPos.x - (schlaeger._pos.x - schlaeger._w/2); hitV = 1.0 / schlaeger._w * hitV - 0.5; speed = ball._dir.mag(); ball._dir.x += hitV * 8; ball._dir.y *= -1; // ball soll die gleiche geschwindigkeit haben wie am anfang ball._dir.normalize(); ball._dir.mult(speed); // setzte ball buendig auf den schlaeger(das er nicht im schlaeger ist) ball._pos.y = schlaeger._pos.y + schlaeger._h/2 + ball._r ; // spiele sound ballHitSchlaegerSnd.trigger(); break; } // test ob ball einen ziegel trifft for (int i=0;i width) { _dir.x *= -1; _pos.x = width - _r; } else if(_pos.x - _r < 0) { _dir.x *= -1; _pos.x = _r; } // teste vertikal if(_pos.y - _r = _pos.x - _w/2 && x -r = _pos.y - _h/2 && y - r <= _pos.y + _h/2) { // teste welche kannte getroffen wurde PVector dir = dirObj.get(); dir.normalize(); // berechne eine linie PVector pObj = PVector.add(new PVector(x, y), PVector.mult(dir, r)); PVector pObjPrev = PVector.sub(new PVector(x, y), PVector.mult(dir, 50)); PVector lt = cornerLT(); PVector rt = cornerRT(); PVector lb = cornerLB(); PVector rb = cornerRB(); PVector vecTop = segIntersection(pObjPrev.x, pObjPrev.y, pObj.x, pObj.y, lt.x, lt.y, rt.x, rt.y); PVector vecBottom = segIntersection(pObjPrev.x, pObjPrev.y, pObj.x, pObj.y, lb.x, lb.y, rb.x, rb.y); PVector vecLeft = segIntersection(pObjPrev.x, pObjPrev.y, pObj.x, pObj.y, lt.x, lt.y, lb.x, lb.y); PVector vecRight = segIntersection(pObjPrev.x, pObjPrev.y, pObj.x, pObj.y, rt.x, rt.y, rb.x, rb.y); // teste kante oben if(vecTop != null) { hitPos.set(vecTop); return HIT_TOP; } else if(vecBottom != null) { hitPos.set(vecBottom); return HIT_BOTTOM; } else if (vecLeft != null) { hitPos.set(vecLeft); return HIT_LEFT; } else if (vecRight != null) { hitPos.set(vecRight); return HIT_RIGTH; } else return HIT_NO; } return HIT_NO; } } // checkout http://wiki.processing.org/w/Line-Line_intersection PVector lineIntersection(float x1, float y1, float x2, float y2, float x3, float y3, float x4, float y4) { float bx = x2 - x1; float by = y2 - y1; float dx = x4 - x3; float dy = y4 - y3; float b_dot_d_perp = bx*dy - by*dx; if (b_dot_d_perp == 0) { return null; } float cx = x3-x1; float cy = y3-y1; float t = (cx*dy - cy*dx) / b_dot_d_perp; return new PVector(x1+t*bx, y1+t*by); } PVector segIntersection(float x1, float y1, float x2, float y2, float x3, float y3, float x4, float y4) { float bx = x2 - x1; float by = y2 - y1; float dx = x4 - x3; float dy = y4 - y3; float b_dot_d_perp = bx * dy - by * dx; if(b_dot_d_perp == 0) { return null; } float cx = x3 - x1; float cy = y3 - y1; float t = (cx * dy - cy * dx) / b_dot_d_perp; if(t 1) { return null; } float u = (cx * by - cy * bx) / b_dot_d_perp; if(u 1) { return null; } return new PVector(x1+t*bx, y1+t*by); } // FUNKTION RACKET /* -------------------------------------------------------------------------- * Racket * -------------------------------------------------------------------------- * prog: Max Rheiner * date: 26/09/2012 (m/d/y) * ---------------------------------------------------------------------------- */ class Racket extends Brick { Racket(int posX,int posY,int w,int h) { super(posX,posY-100,w,h); } void move(int x,int y) { _pos.x = x; } void draw() { pushStyle(); rectMode(CENTER); fill(0,200,200,255); shape(Fliege, _pos.x-290, _pos.y-90, _w*2,260); popStyle(); } }