Skip to content

Commit d3686aa

Browse files
committed
add BlockGame
1 parent 8bdcb5d commit d3686aa

File tree

1 file changed

+182
-0
lines changed

1 file changed

+182
-0
lines changed
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
package introprog
2+
3+
import java.awt.Color
4+
5+
/** A class for creating games with block-based graphics.
6+
* @constructor Create a new game.
7+
* @param title the title of the window
8+
* @param dim the (width, height) of the window in number of blocks
9+
* @param blockSize the side of each square block in pixels
10+
* @param background the color used when clearing pixels
11+
* @param framesPerSecond the desired update rate in the gameLoop
12+
* @param messageAreaHeight the height in pixels of the message area
13+
* @param messageAreaBackground the color of the message area background
14+
*/
15+
abstract class BlockGame(
16+
val title: String = "BlockGame",
17+
val dim: (Int, Int) = (50, 50),
18+
val blockSize: Int = 15,
19+
val background: Color = Color.black,
20+
var framesPerSecond: Int = 50,
21+
val messageAreaHeight: Int = 2,
22+
val messageAreaBackground: Color = Color.gray.darker.darker
23+
) {
24+
import introprog.PixelWindow
25+
26+
/** Called when a key is pressed. Override if you want non-empty action.
27+
* @param key is a string representation of the pressed key
28+
*/
29+
def onKeyDown(key: String): Unit = ()
30+
31+
/** Called when a key is released. Override if you want non-empty action.
32+
* @param key is a string representation of the released key
33+
*/
34+
def onKeyUp(key: String): Unit = ()
35+
36+
/** Called when mouse is pressed. Override if you want non-empty action.
37+
* @param pos the mouse position in underlying `pixelWindow` coordinates
38+
*/
39+
def onMouseDown(pos: (Int, Int)): Unit = ()
40+
41+
/** Called when mouse is released. Override if you want non-empty action.
42+
* @param pos the mouse position in underlying `pixelWindow` coordinates
43+
*/
44+
def onMouseUp(pos: (Int, Int)): Unit = ()
45+
46+
/** Called when window is closed. Override if you want non-empty action. */
47+
def onClose(): Unit = ()
48+
49+
/** Called in each `gameLoop` iteration. Override if you want non-empty action. */
50+
def gameLoopAction(): Unit = ()
51+
52+
/** Called if no time is left in iteration to keep frame rate.
53+
* Default action is to print a warning message.
54+
*/
55+
def onFrameTimeOverrun(elapsedMillis: Long): Unit =
56+
println(s"Warning: Unable to handle $framesPerSecond fps. Loop time: $elapsedMillis ms")
57+
58+
/** Returns the gameLoop delay in ms implied by `framesPerSecond`.*/
59+
def gameLoopDelayMillis: Int = (1000.0 / framesPerSecond).round.toInt
60+
61+
/** The underlying window used for drawing blocks and messages. */
62+
protected val pixelWindow: PixelWindow =
63+
new PixelWindow(width = dim._1 * blockSize, height = (dim._2 + messageAreaHeight) * blockSize, title, background)
64+
65+
/** Internal buffer with block colors. */
66+
private val blockBuffer: Array[Array[Color]] = Array.fill(dim._1, dim._2)(background)
67+
68+
/** Internal buffer with update flags. */
69+
private val isBufferUpdated: Array[Array[Boolean]] = Array.fill(dim._1, dim._2)(false)
70+
71+
/** Internal buffer for post-update actions. */
72+
private val toDoAfterBlockUpdates = collection.mutable.Buffer.empty[() => Unit]
73+
74+
/** Max time for awaiting events from underlying window in ms. */
75+
protected val MaxWaitForEventMillis = 1
76+
77+
clearWindow() // erase all blocks
78+
79+
/** The game loop that continues while not `stopWhen` is true.
80+
* It draws only updated blocks aiming at the desired frame rate.
81+
* It calls each `onXXX` method if a corresponding event is detected.
82+
*/
83+
protected def gameLoop(stopWhen: => Boolean): Unit = while (!stopWhen) {
84+
import PixelWindow.Event
85+
val t0 = System.currentTimeMillis
86+
pixelWindow.awaitEvent(MaxWaitForEventMillis.toLong)
87+
while (pixelWindow.lastEventType != PixelWindow.Event.Undefined) {
88+
pixelWindow.lastEventType match {
89+
case Event.KeyPressed => onKeyDown(pixelWindow.lastKey)
90+
case Event.KeyReleased => onKeyUp(pixelWindow.lastKey)
91+
case Event.WindowClosed => onClose()
92+
case Event.MousePressed => onMouseDown(pixelWindow.lastMousePos)
93+
case Event.MouseReleased => onMouseUp(pixelWindow.lastMousePos)
94+
case _ =>
95+
}
96+
pixelWindow.awaitEvent(1)
97+
}
98+
gameLoopAction()
99+
drawUpdatedBlocks()
100+
val elapsed = System.currentTimeMillis - t0
101+
if ((gameLoopDelayMillis - elapsed) < MaxWaitForEventMillis) {
102+
onFrameTimeOverrun(elapsed)
103+
}
104+
Thread.sleep((gameLoopDelayMillis - elapsed) max 0)
105+
}
106+
107+
/** Draw updated blocks and carry out post-update actions if any. */
108+
private def drawUpdatedBlocks(): Unit = {
109+
for (x <- blockBuffer.indices) {
110+
for (y <- blockBuffer(x).indices) {
111+
if (isBufferUpdated(x)(y)) {
112+
val pwx = x * blockSize
113+
val pwy = y * blockSize
114+
pixelWindow.fill(pwx, pwy, blockSize, blockSize, blockBuffer(x)(y))
115+
isBufferUpdated(x)(y) = false
116+
}
117+
}
118+
}
119+
toDoAfterBlockUpdates.foreach(_.apply())
120+
toDoAfterBlockUpdates.clear()
121+
}
122+
123+
/** Erase all blocks to background color. */
124+
def clearWindow(): Unit = {
125+
pixelWindow.clear
126+
clearMessageArea()
127+
for (x <- blockBuffer.indices) {
128+
for (y <- blockBuffer(x).indices) {
129+
blockBuffer(x)(y) = background
130+
}
131+
}
132+
}
133+
134+
/** Paint a block in color `c` at (`x`,`y`) in block coordinates. */
135+
def drawBlock(x: Int, y: Int, c: Color): Unit = {
136+
if (blockBuffer(x)(y) != c) {
137+
blockBuffer(x)(y) = c
138+
isBufferUpdated(x)(y) = true
139+
}
140+
}
141+
142+
/** Erase the block at (`x`,`y`) to `background` color. */
143+
def eraseBlock(x: Int, y: Int): Unit = {
144+
if (blockBuffer(x)(y) != background) {
145+
blockBuffer(x)(y) = background
146+
isBufferUpdated(x)(y) = true
147+
}
148+
}
149+
150+
/** Write `msg` in `color` in the middle of the window.
151+
* The drawing is postponed until the end of the current game loop
152+
* iteration and thus the text drawn on top of any updated blocks.
153+
*/
154+
def drawCenteredText(msg: String, color: Color = pixelWindow.foreground, size: Int = blockSize): Unit = {
155+
toDoAfterBlockUpdates.append( () =>
156+
pixelWindow.drawText(
157+
msg,
158+
(pixelWindow.width / 2 - msg.length * size / 3) max size,
159+
pixelWindow.height / 2 - size, color,
160+
size
161+
)
162+
)
163+
}
164+
165+
/** Write `msg` in `color` in the message area at ('x','y') in block coordinates. */
166+
def drawTextInMessageArea(msg: String, x: Int, y: Int, color: Color = pixelWindow.foreground, size: Int = blockSize): Unit = {
167+
require(y < messageAreaHeight && y >= 0, s"not in message area: y = $y")
168+
require(x < dim._1 * blockSize && x >= 0, s"not in message area: x = $x")
169+
pixelWindow.drawText(msg, x * blockSize, y + dim._2 * blockSize, color, size)
170+
}
171+
172+
/** Clear a rectangle in the message area in block coordinates. */
173+
def clearMessageArea(x: Int = 0, y: Int = 0, width: Int = dim._1, height: Int = messageAreaHeight): Unit = {
174+
require(y < messageAreaHeight && y >= 0, s"not in message area: y = $y")
175+
require(x < dim._1 * blockSize && x >= 0, s"not in message area: x = $x")
176+
pixelWindow.fill(
177+
x * blockSize, (y + dim._2) * blockSize,
178+
width * blockSize, messageAreaHeight * blockSize,
179+
messageAreaBackground
180+
)
181+
}
182+
}

0 commit comments

Comments
 (0)