Tetrisklone - Lektion 2
Nun wollen wir die Steine mal zum Fallen bringen.Als erstes erstellen wir uns einen Controller. In diesem wird unsere Hauptlogik implementiert.
Unser Controller besitzt einen Mover Actor. Über diesen wird unser Spielstein bewegt und gedreht.
Ebenso wichtig ist hier die Methode nextStone(). Sie wird vom Mover Actor aufgerufen, wenn ein Stein am Boden angelangt ist. Durch sie werden die Blöcke eines Steines dem Feld zugewiesen, es wird auf aufgelöste Linien geprüft, es wird auf ein GameOver geprüft und natürlich wird ein neuer zufälliger Stein zugewiesen.
Eine weitere wichtige Methode ist die checkCollision Methode die überprüft, ob eine Kollision von Blöcken stattgefunden hat. Sie wird nur vom Mover Actor oder der CheckGameOver Methode aufgerufen.
Controller: ( Control.scala )
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128
package de.blauerfalke.tetris.control import java.awt.event.{ActionListener, ActionEvent} import swing.Dialog import scala.actors.Actor import scala.actors.Actor._ import de.blauerfalke.tetris.view.Block import de.blauerfalke.tetris.view.Stone import de.blauerfalke.tetris.view.Field import de.blauerfalke.tetris.view.Main object Control { } class Control(val field:Field) { var speed = 700 var timer = new javax.swing.Timer(speed , new ActionListener { def actionPerformed(ae:ActionEvent) { Mover ! "down" } }) timer.start def nextStone { for(c <- field.stone.blocks) field.blocks = Block((field.stone.p._1+c.p._1,field.stone.p._2+c.p._2),c.c) :: field.blocks var line = Field.fieldHeight for(c <- field.blocks) if(c.p._2<line) line = c.p._2 while(line < Field.fieldHeight) { var colCount=0 for(c <- field.blocks) if(c.p._2 == line) colCount = colCount + 1 if(colCount>=Field.fieldWidth) delLine(line) else line = line + 1 } field.stone = Stone(0) checkGameOver } def delLine(line:Int) { field.blocks = field.blocks.filter(s => s.p._2 != line) field.blocks = field.blocks.map(s => if(s.p._2<line) Block((s.p._1, s.p._2+1), s.c) else s) } def checkGameOver() { if(checkCollision) { //if respawn in a block the game is over ... timer.stop Dialog.showMessage(field,"GAME OVER\n\nThe Game is over.","Game Over") System.exit(0); } } def checkCollision() :Boolean = { for(b <- field.stone.blocks) { val x1 = b.p._1+field.stone.p._1 val x2 = b.p._2+field.stone.p._2 if(x1<0 || x1>Field.fieldWidth-1) return true if(x2<0 || x2>Field.fieldHeight-1) return true for(c <- field.blocks) if(x1==c.p._1 && x2==c.p._2) return true } return false } val Mover = actor { loop { react { case "left" => { field.stone.left if(checkCollision) { field.stone.back } } case "right" => { field.stone.right if(checkCollision) { field.stone.back } } case "down" => { field.stone.down if(checkCollision) { field.stone.back if(field.stone.ground) { nextStone } else { field.stone.ground=true } } } case "turnl" => { field.stone.turnl if(checkCollision) { field.stone.left if(checkCollision) { field.stone.back field.stone.turnr } } } case "turnr" => { field.stone.turnr if(checkCollision) { field.stone.left if(checkCollision) { field.stone.back field.stone.turnl } } } } } } Mover.start }
Mit diesen Methoden wird der fallende Stein beweegt und gedreht.
Die Methoden turnl() und turnr() drehen die Koordinaten der einzelnen Blöcke via einer einfachen Drehungsmatrix. Danach werden die Blockkoordinaten wieder genormt, damit die Positionen nicht verändert werden. Man könnte das auch mit einer Drehungsmatrix um den Mittelpunkt machen, allerdings müsste man dann erst den Mittelpunkt berechnen.
Die Methode Back ist wichtig, denn sollte beim Bewegen des Steines eine Kollision auftreten, so muss der Stein in seine ursprüngliche Position gebracht werden.
Außerdem erweitern wir die apply Methode des Stone Objektes noch um einen weiteren Fall: Mit Stone(0) wird uns nun ein zufälliger Stein zurückgegeben.
Steine: ( Stone.scala )
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72
package de.blauerfalke.tetris.view import java.awt.Graphics import java.awt.Color import java.util.Random object Stone { def apply(typ:Int) = { var t = typ match { case 0 => new Random(System.currentTimeMillis()).nextInt(7) + 1; case _ => typ } t match { case -1 => new Stone(Array()) case 1 => new Stone(Array( Block(0,0,Color.blue) ,Block(0,1,Color.blue) ,Block(0,2,Color.blue) ,Block(1,2, Color.blue) )) case 2 => new Stone(Array( Block(1,0,Color.green) ,Block(1,1,Color.green) ,Block(1,2,Color.green) ,Block(0,2,Color.green) )) case 3 => new Stone(Array( Block(0,0,Color.red) ,Block(0,1,Color.red) ,Block(1,1,Color.red) ,Block(1,2,Color.red) )) case 4 => new Stone(Array( Block(1,0,Color.orange),Block(1,1,Color.orange),Block(0,1,Color.orange),Block(0,2,Color.orange))) case 5 => new Stone(Array( Block(1,0,Color.pink) ,Block(1,1,Color.pink) ,Block(0,1,Color.pink) ,Block(1,2,Color.pink) )) case 6 => new Stone(Array( Block(0,0,Color.yellow),Block(1,0,Color.yellow),Block(0,1,Color.yellow),Block(1,1,Color.yellow))) case 7 => new Stone(Array( Block(0,0,Color.cyan) ,Block(0,1,Color.cyan) ,Block(0,2,Color.cyan) ,Block(0,3,Color.cyan) )) } } } class Stone(var blocks:Array[Block]) { var p = (4,0) //position var lp = (0,0) //last position var ground = false //was the last time on the ground? def matrixMult(p:(Int,Int),m:((Int,Int, Int,Int))) = { (p._1*m._1+p._2*m._2,p._1*m._3+p._2*m._4) } def turnl() { var min = (1000,1000) for(i <- 0 until blocks.size) { blocks(i).p = matrixMult(blocks(i).p, (0,-1, 1,0) ) if(blocks(i).p._1 < min._1) min = (blocks(i).p._1,min._2) if(blocks(i).p._2 < min._2) min = (min._1, blocks(i).p._2) } for(i <- 0 until blocks.size) blocks(i).p = (blocks(i).p._1 - min._1, blocks(i).p._2 - min._2) } def turnr() { var min = (1000,1000) for(i <- 0 until blocks.size) { blocks(i).p = matrixMult(blocks(i).p, (0,1, -1,0) ) if(blocks(i).p._1 < min._1) min = (blocks(i).p._1,min._2) if(blocks(i).p._2 < min._2) min = (min._1, blocks(i).p._2) } for(i <- 0 until blocks.size) blocks(i).p = (blocks(i).p._1 - min._1, blocks(i).p._2 - min._2) } def down() { lp = p p = (p._1, p._2+1) } def left() { lp = p p = (p._1-1, p._2) } def right() { lp = p p = (p._1+1, p._2) } def back = p = lp def paint(g:Graphics) { for(b <- blocks) b.paint(g, p) } }
Diesen brauchen wir um den Block relativ zu seiner Position zu malen.
Block: ( Block.scala )
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
package de.blauerfalke.tetris.view import java.awt.Color import java.awt.Graphics object Block { val blockSize = 10 def apply(b:(Int,Int), c:Color) = new Block(b, c) def apply(x:Int, y:Int, c:Color) = new Block((x,y), c) } class Block(var p:(Int,Int),var c:Color) { def paint(g:Graphics) { paint(g,(0,0)) } def paint(g:Graphics, gp:(Int,Int)) { g.setColor(c); g.fillRect(p._1*Block.blockSize + gp._1*Block.blockSize, p._2*Block.blockSize + gp._2*Block.blockSize, Block.blockSize, Block.blockSize) } }
Des Weiteren fügen wir der Komponente einen KeyListener hinzu, der bei den gegebenen KeyEvents über den Actor Mover den Stein bewegt. Spielfeld: ( Field.scala )
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
package de.blauerfalke.tetris.view import scala.swing.Component import java.awt.Graphics2D import java.awt.Color import java.awt.event.{ActionListener, ActionEvent} import de.blauerfalke.tetris.control.Control import java.awt.event.KeyListener import java.awt.event.KeyEvent object Field { val fieldWidth = 11 val fieldHeight = 18 } class Field extends Component { val control = new Control(this) var stone = Stone(0) var blocks:List[Block] = List() peer.setFocusable(true); //Keyboard Input peer.addKeyListener(new KeyListener() { def keyTyped(e: KeyEvent) = { } def keyPressed(e: KeyEvent) = { if(e.getKeyCode==KeyEvent.VK_LEFT ) control.Mover ! "left" if(e.getKeyCode==KeyEvent.VK_RIGHT ) control.Mover ! "right" if(e.getKeyCode==KeyEvent.VK_DOWN ) control.Mover ! "down" if(e.getKeyCode==KeyEvent.VK_SPACE ) control.Mover ! "turnl" if(e.getKeyCode==KeyEvent.VK_ENTER ) control.Mover ! "turnr" } def keyReleased(e: KeyEvent) = {} }) //Repaint all 50 msec new javax.swing.Timer(50 , new ActionListener { def actionPerformed(ae:ActionEvent) { repaint } }).start //var t:Actor = actor { loop { reactWithin(50) { case "repaint" => {repaint; t ! "repaint" } } } }.start; t ! "repaint" override def paintComponent(g: Graphics2D) { g.setColor(Color.black) g.fillRect(0,0, Field.fieldWidth*Block.blockSize ,Field.fieldHeight*Block.blockSize) for(b <- blocks) b.paint(g) stone.paint(g) } }
In unserem Hauptfenster ( Main.scala ) hat sich nichts verändert.
So, nun sehen wir, dass unser Tetris im Prinzip schon voll funktionsfähig ist. Allerdings fehlen noch viele kleine Extras, die das Spiel erst ansprechend machen. Diese fügen wir in der nächsten Lektion hinzu.
Hier nochmal die komplette Lektion 2: TetrisLection2.zip