|
| 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