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
}
In unserem Mover Actor werden die Bewegungsmethoden von Stone aufgerufen. Diese wollen wir jetzt implementieren: left() right() down() turnl() turnr() back()

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)	
	}
}
Als nächstes erweitern wir die paint Methode um den weiteren Parameter "position".
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)
	}
}
Auch in unserem Spielfeld ist einiges dazu gekommen. Jedes Spielfeld hat eine Instanz des Controllers. Des Weiteren ist eine Liste von Blöcken hinzugekommen. In dieser Liste sind alle Blöcke gespeichert, die noch auf dem Spielfeld sind. Wenn ein Stein gefallen ist, werden seine Blöcke dieser Liste hinzugefügt.
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)
	}
}
Für unser Spielfeld zeichnen wir einen schwarzen Hintergrund mit der Feldbreit x Feldhöhe, die jeweils noch mit der Blockgröße multipliziert werden.

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