Skip to content

Commit 8203d14

Browse files
Merge pull request #124 from stefanhaustein/master
Add a simple maze game that scales down to 8x8 matrix displays
2 parents e092a3d + 1369522 commit 8203d14

6 files changed

Lines changed: 892 additions & 0 deletions

File tree

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
package com.pi4j.examples.games;
2+
3+
import com.pi4j.drivers.display.BitmapFont;
4+
import com.pi4j.drivers.display.graphics.Graphics;
5+
import com.pi4j.drivers.display.graphics.GraphicsDisplay;
6+
import com.pi4j.drivers.input.GameController;
7+
import com.pi4j.drivers.sound.SoundDriver;
8+
import com.pi4j.util.DeferredDelay;
9+
10+
import java.util.concurrent.locks.LockSupport;
11+
12+
/** Abstract base class for games tailored towards small displays. */
13+
public abstract class MiniGame {
14+
// Drivers and constructor params
15+
16+
protected final GraphicsDisplay display;
17+
protected final GameController controller;
18+
protected final SoundDriver soundDriver;
19+
protected final String gameName;
20+
protected final Graphics graphics;
21+
protected final DeferredDelay delay = new DeferredDelay();
22+
protected int backgroundColor;
23+
protected int borderColor;
24+
25+
// Sizing and scaling
26+
27+
protected int cellSize;
28+
protected int scale;
29+
protected int scaledCellSize;
30+
protected int size;
31+
protected int x0;
32+
protected int y0;
33+
34+
// Internals
35+
36+
private GameController.Direction previousDirection = GameController.Direction.NONE;
37+
private int textX;
38+
private int textY;
39+
private String scrollText;
40+
private int scrollPos;
41+
private boolean soundEnabled;
42+
private boolean running = false;
43+
private boolean exit = false;
44+
private int score = 0;
45+
private int highScore = 0;
46+
47+
/**
48+
* Adds the "delta" value to a value and returns the sum. If the distance to the next integer is less than
49+
* delta / 2, the returned value "snaps" to this integer.
50+
*/
51+
public static float addAndSnap(float value, float delta) {
52+
float newValue = value + delta;
53+
float rounded = Math.round(newValue);
54+
return (Math.abs(newValue - rounded) <= Math.abs(delta)/2) ? rounded : newValue;
55+
}
56+
57+
protected MiniGame(GraphicsDisplay display,
58+
GameController controller,
59+
SoundDriver soundDriver,
60+
String name,
61+
int backgroundColor,
62+
int borderColor) {
63+
this.display = display;
64+
this.controller = controller;
65+
this.soundDriver = soundDriver;
66+
this.gameName = name;
67+
this.backgroundColor = backgroundColor;
68+
this.borderColor = borderColor;
69+
this.soundEnabled = soundDriver != null;
70+
71+
graphics = display.getGraphics();
72+
73+
int minDim = Math.min(display.getWidth(), display.getHeight());
74+
int rawCellSize = minDim / 8;
75+
76+
if (rawCellSize < 8) {
77+
cellSize = 1;
78+
scale = cellSize;
79+
} else {
80+
int remainder10 = rawCellSize % 10;
81+
int remainder8 = rawCellSize % 8;
82+
83+
if (remainder10 <= remainder8) {
84+
cellSize = 10;
85+
} else {
86+
cellSize = 8;
87+
}
88+
scale = rawCellSize / cellSize;
89+
}
90+
scaledCellSize = scale * cellSize;
91+
size = 8 * scaledCellSize;
92+
93+
x0 = (display.getWidth() - size) / 2;
94+
y0 = (display.getHeight() - size) / 2;
95+
96+
System.out.println("cellSize: " + cellSize + " scale: " + scale + " x0: " + x0 + " y0: " + y0);
97+
98+
renderTitleScreen();
99+
}
100+
101+
public void addPoints(int points) {
102+
score += points;
103+
}
104+
105+
protected void clearScreen() {
106+
graphics.setColor(borderColor);
107+
graphics.fillRect(0, 0, display.getWidth(), display.getHeight());
108+
graphics.setColor(backgroundColor);
109+
graphics.fillRect(x0, y0, size, size);
110+
}
111+
112+
protected void clearScreen(int ms) {
113+
graphics.setColor(backgroundColor);
114+
115+
for (int i = 0; i < size; i++) {
116+
graphics.drawRect(i + x0, i + y0, size - 2 * i, size - 2 * i);
117+
sleep(Math.max(1, ms / size));
118+
}
119+
}
120+
121+
protected void println(String text) {
122+
if (cellSize >= 8) {
123+
graphics.renderText(textX, textY, text);
124+
textX = x0 + scaledCellSize / 2;
125+
textY += scaledCellSize;
126+
}
127+
scrollText += " -- " + text;
128+
}
129+
130+
protected void renderTitleScreen() {
131+
clearScreen();
132+
scrollText = "";
133+
if (score > highScore) {
134+
highScore = score;
135+
}
136+
137+
if (cellSize >= 8) {
138+
graphics.setTextScale(scale, scale);
139+
graphics.setColor(0xffffffff);
140+
graphics.setFont(cellSize == 10 ? BitmapFont.get5x10Font() : BitmapFont.get5x8Font());
141+
142+
textX = (display.getWidth() - gameName.length() * 6 * scale) / 2;
143+
textY = scaledCellSize * 5 / 4;
144+
} else {
145+
scrollText = "";
146+
}
147+
println(gameName);
148+
149+
textY += scaledCellSize / 4;
150+
151+
println("Hi: " + highScore);
152+
println("Sc: " + score);
153+
println("> Start");
154+
println("< Exit");
155+
if (soundDriver != null) {
156+
println("^ Sound: " + (soundEnabled ? "On" : "Off"));
157+
}
158+
scrollText += scrollText;
159+
}
160+
161+
protected void titleScreenStep() {
162+
GameController.Direction direction = controller.getDirection();
163+
if (direction == GameController.Direction.NONE) {
164+
switch (previousDirection) {
165+
case GameController.Direction.EAST -> {
166+
clearScreen();
167+
startGame();
168+
running = true;
169+
}
170+
case GameController.Direction.WEST -> {
171+
clearScreen();
172+
exit = true;
173+
}
174+
case GameController.Direction.NORTH -> {
175+
soundEnabled = !soundEnabled;
176+
renderTitleScreen();
177+
}
178+
}
179+
previousDirection = GameController.Direction.NONE;
180+
} else if (previousDirection == GameController.Direction.NONE) {
181+
previousDirection = direction;
182+
}
183+
184+
if (cellSize < 8) {
185+
graphics.setFont(BitmapFont.get5x8Font());
186+
graphics.setColor(backgroundColor);
187+
graphics.fillRect(0, 0, 1000, 64);
188+
graphics.setColor(0xffffffff);
189+
int len = graphics.renderText(scrollPos--/2, 8, scrollText);
190+
if (scrollPos < -len) {
191+
scrollPos = 0;
192+
}
193+
}
194+
}
195+
196+
public void run() {
197+
while (!exit) {
198+
delay.setDelayMillis(17);
199+
if (running) {
200+
running = step();
201+
if (!running) {
202+
renderTitleScreen();
203+
}
204+
} else {
205+
titleScreenStep();
206+
}
207+
display.flush();
208+
delay.materializeDelay();
209+
}
210+
}
211+
212+
public void play(String mml) {
213+
if (soundDriver != null && soundEnabled) {
214+
soundDriver.playNotes(mml);
215+
}
216+
}
217+
218+
public void sleep(int ms) {
219+
if (running) {
220+
LockSupport.parkNanos(ms * 1_000_000L);
221+
} else {
222+
try {
223+
Thread.sleep(ms);
224+
} catch (InterruptedException e) {
225+
Thread.currentThread().interrupt();
226+
throw new RuntimeException(e);
227+
}
228+
}
229+
}
230+
231+
/** Implement the logic for starting a new game here. */
232+
protected abstract void startGame();
233+
234+
/** Invoked every ~17ms. Implemenet the game logic and rendering here. */
235+
protected abstract boolean step();
236+
}
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
package com.pi4j.examples.games;
2+
3+
public class Sprite {
4+
private int color;
5+
private int[] bitMap;
6+
private int bitOffset;
7+
private int bitStride;
8+
private int[] palette;
9+
private int width;
10+
private int height;
11+
12+
public Sprite(int color, int width, int[] bitmap, int[] palette) {
13+
this.color = color;
14+
this.width = width;
15+
this.bitMap = bitmap;
16+
this.bitStride = 32;
17+
this.bitOffset = 32 - width * 4;
18+
this.height = bitmap.length * 32 / bitStride;
19+
this.palette = palette;
20+
}
21+
22+
public Sprite(int color, int width, long[] bitmap, int[] palette) {
23+
this.color = color;
24+
this.width = width;
25+
this.bitMap = long2intArray(bitmap);
26+
this.bitStride = 64;
27+
this.bitOffset = 64 - width * 4;
28+
this.height = this.bitMap.length * 32 / bitStride;
29+
this.palette = palette;
30+
}
31+
32+
public Sprite withColors(int color, int... palette) {
33+
Sprite result = new Sprite(color, this.width, bitMap, palette);
34+
result.bitStride = bitStride;
35+
result.bitOffset = bitOffset;
36+
result.height = height;
37+
return result;
38+
}
39+
40+
public void render(MiniGame game, float x, float y) {
41+
render(game, x, y, Transformation.NONE);
42+
}
43+
44+
public void fill(MiniGame game, float x, float y, Transformation transformation, int color) {
45+
game.graphics.setColor(color);
46+
int w;
47+
int h;
48+
if (game.cellSize < 8) {
49+
w = 1;
50+
h = 1;
51+
} else if (transformation == Transformation.ROTATE_90 || transformation == Transformation.ROTATE_270) {
52+
w = height;
53+
h = width;
54+
} else {
55+
w = width;
56+
h = height;
57+
}
58+
game.graphics.fillRect(
59+
game.x0 + (int) (x * game.scaledCellSize + (game.scaledCellSize - w * game.scale) / 2),
60+
game.y0 + (int) (y * game.scaledCellSize + (game.scaledCellSize - h * game.scale) / 2),
61+
w * game.scale,
62+
h * game.scale);
63+
}
64+
65+
public void render(MiniGame game, float x, float y, Transformation transformation) {
66+
if (game.cellSize < 8) {
67+
fill(game, x, y, transformation, color);
68+
return;
69+
}
70+
int stride;
71+
int increment;
72+
int offset;
73+
int w;
74+
int h;
75+
switch (transformation) {
76+
case ROTATE_90 -> {
77+
offset = bitStride * (height - 1) + bitOffset;
78+
increment = -bitStride;
79+
stride = 4;
80+
w = height;
81+
h = width;
82+
}
83+
case ROTATE_180 -> {
84+
offset = bitStride * height - 4;
85+
increment = -4;
86+
stride = -bitStride;
87+
w = width;
88+
h = height;
89+
}
90+
case ROTATE_270 -> {
91+
offset = bitStride - 4;
92+
increment = bitStride;
93+
stride = -4;
94+
w = height;
95+
h = width;
96+
}
97+
case MIRROR_H -> {
98+
offset = bitStride - 4;
99+
increment = -4;
100+
stride = bitStride;
101+
w = width;
102+
h = height;
103+
}
104+
case MIRROR_V -> {
105+
offset = bitStride * (height - 1) + bitOffset;
106+
increment = 4;
107+
stride = -bitStride;
108+
w = width;
109+
h = height;
110+
}
111+
default -> {
112+
offset = bitOffset;
113+
increment = 4;
114+
stride = bitStride;
115+
w = width;
116+
h = height;
117+
}
118+
}
119+
120+
game.graphics.setProcessAlpha(true);
121+
game.graphics.drawIndexed(
122+
game.x0 + (int) (x * game.scaledCellSize + (game.scaledCellSize - w * game.scale) / 2),
123+
game.y0 + (int) (y * game.scaledCellSize + (game.scaledCellSize - h * game.scale) / 2),
124+
w * game.scale,
125+
h * game.scale,
126+
bitMap,
127+
4,
128+
palette,
129+
offset,
130+
increment,
131+
stride,
132+
game.scale,
133+
game.scale);
134+
}
135+
136+
public void erase(MiniGame game, float x, float y, Transformation transformation) {
137+
fill(game, x, y, transformation, game.backgroundColor);
138+
}
139+
140+
public static int[] long2intArray(long[] data) {
141+
int[] result = new int[data.length * 2];
142+
for (int i = 0; i < data.length; i++) {
143+
result[2*i] = (int) (data[i] >>> 32);
144+
result[2*i+1] = (int) data[i];
145+
}
146+
return result;
147+
}
148+
149+
public enum Transformation {
150+
NONE,
151+
ROTATE_90,
152+
ROTATE_180,
153+
ROTATE_270,
154+
MIRROR_H,
155+
MIRROR_V,
156+
}
157+
}

0 commit comments

Comments
 (0)