Zręcznościowa gra na Arduino inspirowana grą "Geometry Dash".
1. Płytka Arduino UNO
2. Konsola z wyświetlaczem 128x64 i przynajmniej 1 działającym przyciskiem
"Mini Dash" to uproszczona wersja popularnej gry "Geometry Dash". Gra została zaprojektowana z użyciem wyłącznie 1 przycisku, w celu zwiększenia stabilności programu. Program po uruchomieniu pozwala wybrać 1 z 3 podstawowych poziomów trudności, różniących się szybkością gry. W trakcie gry postać o kształcie pustego w środku kwadratu porusza się na dwuwymiarowej mapie, a podczas skoków wykonuje płynną animację obrotu. Przeszkodami są trójkątne kolce w 2 różnych rozmiarach, których dotknięcie kończy grę. Dodatkowo dodano bloki w kształcie pełnych kwadratów, po których postać może się bezpiecznie poruszać - mogą one leżeć na ziemi, lewitować w powietrzu lub tworzyć wysokie ściany. W celu poprawy płynności rozgrywki dodano możliwość ciągłego skakania poprzez przytrzymanie przycisku. Podczas gry wyświetlony jest licznik punktów. W miarę postępu gry postać porusza się coraz szybciej, co utrudnia dalszą rozgrywkę. Po zakończeniu tury wyświetla się napis "Game over" oraz polecenie, aby wcisnąć klawisz, by zacząć nową grę, gra wraca wtedy dom ekranu wyboru poziomu trudności. W przypadku braku aktywności gracza przez 10 sekund, gra wraca do ekranu startowego.
/*********************************************************************
Mini Dash - większe przeszkody (kwadraty i podstawy kolców)
OLED SH1106 128x64 I2C
Przycisk na pinie D4
*********************************************************************/
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SH110X.h>
#include <math.h>
#define I2C_ADDRESS 0x3C
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
#define BUTTON_PIN 4
Adafruit_SH1106G display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
/* =========================================================
PARAMETRY GRY I FIZYKI
========================================================= */
const int GROUND_Y = 56;
const int PLAYER_X = 18;
const int PLAYER_SIZE = 8;
const int BLOCK_SIZE = 10;
const float GRAVITY = 0.8f;
const float JUMP_VELOCITY = -6.5f;
const int OBSTACLE_COUNT = 4;
const unsigned long FRAME_TIME_MS = 10;
const int ROTATION_FRAMES = 12;
const unsigned long GAME_OVER_TIMEOUT_MS = 10000;
/* =========================================================
STANY GRY
========================================================= */
enum GameState {
START_SCREEN,
DIFFICULTY_SELECT,
PLAYING,
GAME_OVER
};
GameState gameState = START_SCREEN;
/* =========================================================
ZMIENNE GRACZA
========================================================= */
float playerY = GROUND_Y - PLAYER_SIZE;
float prevPlayerY = playerY;
float playerVY = 0.0f;
bool playerOnGround = true;
int coyoteFrames = 0;
float playerAngle = 0.0f;
float rotationStartAngle = 0.0f;
float rotationTargetAngle = 0.0f;
int rotationFrame = 0;
bool rotationActive = false;
/* =========================================================
PRZESZKODY
========================================================= */
struct Obstacle {
int x;
int w;
int h;
int type;
int cols;
int rows;
int altitude;
uint8_t spikesOnTop;
};
Obstacle obstacles[OBSTACLE_COUNT];
/* =========================================================
ZMIENNE POSTĘPU
========================================================= */
float distanceTravelled = 0.0f;
float gameSpeed = 3.0f;
float nextSpeedUpDist = 150.0f;
int difficultyLevel = 1;
float baseSpeed = 3.0f;
unsigned long lastMenuInteraction = 0;
unsigned long gameOverStartTime = 0;
bool lastButtonState = HIGH;
unsigned long lastFrameTime = 0;
/* =========================================================
PROTOTYPY FUNKCJI
========================================================= */
void resetGame();
void applyDifficulty();
void initObstacles();
void spawnObstacle(int index, int newX);
int getRightmostObstacleEdge();
void readInput();
void triggerJump();
void startHalfFlip();
void updateRotation();
void updateGame();
bool checkCollision();
void drawStartScreen();
void drawDifficultyScreen();
void drawGame();
void drawGameOverScreen();
void drawPlayer(int x, int y, int size, float angle);
void drawObstacle(Obstacle& obs);
int rotatePointX(float localX, float localY, float angle);
int rotatePointY(float localX, float localY, float angle);
/* =========================================================
SETUP
========================================================= */
void setup() {
pinMode(BUTTON_PIN, INPUT_PULLUP);
delay(250);
Wire.begin();
Wire.setClock(100000);
if (!display.begin(I2C_ADDRESS, true)) {
while (1) { }
}
display.setRotation(2);
display.clearDisplay();
display.display();
randomSeed(analogRead(A0));
resetGame();
}
/* =========================================================
LOOP
========================================================= */
void loop() {
unsigned long now = millis();
if (now - lastFrameTime < FRAME_TIME_MS) {
return;
}
lastFrameTime = now;
readInput();
if (gameState == PLAYING) {
updateGame();
drawGame();
}
else if (gameState == START_SCREEN) {
drawStartScreen();
}
else if (gameState == DIFFICULTY_SELECT) {
drawDifficultyScreen();
}
else {
drawGameOverScreen();
}
}
void applyDifficulty() {
if (difficultyLevel == 0) baseSpeed = 2.0f;
else if (difficultyLevel == 1) baseSpeed = 3.2f;
else baseSpeed = 4.5f;
}
void resetGame() {
playerY = GROUND_Y - PLAYER_SIZE;
prevPlayerY = playerY;
playerVY = 0.0f;
playerOnGround = true;
coyoteFrames = 0;
playerAngle = 0.0f;
rotationStartAngle = 0.0f;
rotationTargetAngle = 0.0f;
rotationFrame = 0;
rotationActive = false;
distanceTravelled = 0.0f;
gameSpeed = baseSpeed;
nextSpeedUpDist = 150.0f;
gameOverStartTime = 0;
initObstacles();
}
void initObstacles() {
int startX = SCREEN_WIDTH + 20;
for (int i = 0; i < OBSTACLE_COUNT; i++) {
spawnObstacle(i, startX);
startX += obstacles[i].w + random(70, 110);
}
}
void spawnObstacle(int index, int newX) {
obstacles[index].x = newX;
int r = random(0, 100);
if (r < 25) {
// KOLEC NA ZIEMI
obstacles[index].type = 0;
obstacles[index].w = BLOCK_SIZE;
obstacles[index].h = (random(0, 2) == 0) ? BLOCK_SIZE : BLOCK_SIZE / 2;
obstacles[index].cols = 1;
obstacles[index].rows = 1;
obstacles[index].altitude = 0;
obstacles[index].spikesOnTop = 0;
} else if (r < 55) {
// PLATFORMA NA ZIEMI
obstacles[index].type = 1;
obstacles[index].cols = random(4, 7);
obstacles[index].rows = 1;
obstacles[index].altitude = 0;
obstacles[index].w = obstacles[index].cols * BLOCK_SIZE;
obstacles[index].h = BLOCK_SIZE;
obstacles[index].spikesOnTop = 0;
for (int i = 1; i < obstacles[index].cols - 1; i++) {
if (random(0, 3) == 0) obstacles[index].spikesOnTop |= (1 << i);
}
} else if (r < 75) {
// LEWITUJĄCE BLOKI
obstacles[index].type = 1;
obstacles[index].cols = random(1, 4);
obstacles[index].rows = 1;
obstacles[index].altitude = random(2, 3) * BLOCK_SIZE;
obstacles[index].w = obstacles[index].cols * BLOCK_SIZE;
obstacles[index].h = BLOCK_SIZE;
obstacles[index].spikesOnTop = 0;
} else {
// WYSOKA ŚCIANA
obstacles[index].type = 1;
obstacles[index].cols = random(1, 3);
// Na łatwym wykluczona ściana z 3 kwadratów pionowo
if (difficultyLevel == 0) {
obstacles[index].rows = 2;
} else {
obstacles[index].rows = random(2, 4);
}
obstacles[index].altitude = 0;
obstacles[index].w = obstacles[index].cols * BLOCK_SIZE;
obstacles[index].h = obstacles[index].rows * BLOCK_SIZE;
obstacles[index].spikesOnTop = 0;
}
}
int getRightmostObstacleEdge() {
int maxEdge = obstacles[0].x + obstacles[0].w;
for (int i = 1; i < OBSTACLE_COUNT; i++) {
if (obstacles[i].x + obstacles[i].w > maxEdge) {
maxEdge = obstacles[i].x + obstacles[i].w;
}
}
return maxEdge;
}
/* =========================================================
ODCZYT WEJŚCIA
========================================================= */
void readInput() {
bool currentState = digitalRead(BUTTON_PIN);
bool jumpPressed = (lastButtonState == HIGH && currentState == LOW);
bool jumpHeld = (currentState == LOW);
lastButtonState = currentState;
if (gameState == START_SCREEN) {
if (jumpPressed) {
gameState = DIFFICULTY_SELECT;
lastMenuInteraction = millis();
}
}
else if (gameState == DIFFICULTY_SELECT) {
if (jumpPressed) {
difficultyLevel = (difficultyLevel + 1) % 3;
lastMenuInteraction = millis();
}
else if (millis() - lastMenuInteraction > 2000) {
applyDifficulty();
resetGame();
gameState = PLAYING;
}
}
else if (gameState == PLAYING) {
if (jumpHeld && (playerOnGround || coyoteFrames > 0)) {
triggerJump();
coyoteFrames = 0;
}
}
else if (gameState == GAME_OVER) {
if (jumpPressed) {
gameState = DIFFICULTY_SELECT;
lastMenuInteraction = millis();
}
else if (millis() - gameOverStartTime >= GAME_OVER_TIMEOUT_MS) {
gameState = START_SCREEN;
}
}
}
void triggerJump() {
playerVY = JUMP_VELOCITY;
playerOnGround = false;
startHalfFlip();
}
void updateGame() {
prevPlayerY = playerY;
playerVY += GRAVITY;
playerY += playerVY;
for (int i = 0; i < OBSTACLE_COUNT; i++) {
obstacles[i].x -= (int)gameSpeed;
if (obstacles[i].x + obstacles[i].w < 0) {
int newX = getRightmostObstacleEdge() + random(70, 110);
spawnObstacle(i, newX);
}
}
distanceTravelled += gameSpeed;
if (distanceTravelled >= nextSpeedUpDist && gameSpeed < 7.5f) {
gameSpeed += 0.25f;
nextSpeedUpDist += 150.0f;
}
int currentFloorY = GROUND_Y;
for (int i = 0; i < OBSTACLE_COUNT; i++) {
if (obstacles[i].type == 1) {
int ox = obstacles[i].x;
int ow = obstacles[i].w;
int boxTop = GROUND_Y + 1 - obstacles[i].altitude - obstacles[i].h;
int px = PLAYER_X + 1;
int pw = PLAYER_SIZE - 2;
if (px + pw > ox && px < ox + ow) {
if (playerVY >= 0 && (playerY + PLAYER_SIZE >= boxTop) && (prevPlayerY + PLAYER_SIZE <= boxTop + 8)) {
if (boxTop < currentFloorY) {
currentFloorY = boxTop;
}
}
}
}
}
if (playerY >= currentFloorY - PLAYER_SIZE) {
playerY = currentFloorY - PLAYER_SIZE;
playerVY = 0.0f;
playerOnGround = true;
coyoteFrames = 5;
playerAngle = 0.0f;
rotationActive = false;
} else {
playerOnGround = false;
if (coyoteFrames > 0) coyoteFrames--;
}
updateRotation();
if (checkCollision()) {
gameState = GAME_OVER;
gameOverStartTime = millis();
}
}
void startHalfFlip() {
rotationStartAngle = playerAngle;
rotationTargetAngle = playerAngle + PI;
rotationFrame = 0;
rotationActive = true;
}
void updateRotation() {
if (!rotationActive) return;
rotationFrame++;
float t = (float)rotationFrame / (float)ROTATION_FRAMES;
if (t >= 1.0f) {
playerAngle = rotationTargetAngle;
rotationActive = false;
} else {
playerAngle = rotationStartAngle + (rotationTargetAngle - rotationStartAngle) * t;
}
}
/* =========================================================
KOLIZJE
========================================================= */
bool checkCollision() {
int px = PLAYER_X;
int py = (int)playerY;
int pw = PLAYER_SIZE;
int ph = PLAYER_SIZE;
for (int i = 0; i < OBSTACLE_COUNT; i++) {
int ox = obstacles[i].x;
int ow = obstacles[i].w;
if (obstacles[i].type == 0) {
int oh = obstacles[i].h;
int oy = GROUND_Y + 1 - oh;
bool overlapX = (px < ox + ow - 2) && (px + pw > ox + 2);
bool overlapY = (py < oy + oh) && (py + ph > oy + 2);
if (overlapX && overlapY) return true;
} else if (obstacles[i].type == 1) {
int boxTop = GROUND_Y + 1 - obstacles[i].altitude - obstacles[i].h;
int boxBottom = GROUND_Y + 1 - obstacles[i].altitude;
bool overlapX = (px < ox + ow - 2) && (px + pw > ox + 2);
bool overlapY = (py < boxBottom - 2) && (py + ph > boxTop + 2);
if (overlapX && overlapY) return true;
for (int j = 0; j < obstacles[i].cols; j++) {
if ((obstacles[i].spikesOnTop >> j) & 1) {
int sx = ox + j * BLOCK_SIZE;
int sw = BLOCK_SIZE;
int sh = BLOCK_SIZE / 2;
int sy = boxTop - sh;
bool soverlapX = (px < sx + sw - 2) && (px + pw > sx + 2);
bool soverlapY = (py < sy + sh) && (py + ph > sy + 2);
if (soverlapX && soverlapY) return true;
}
}
}
}
return false;
}
void drawStartScreen() {
display.clearDisplay();
display.setTextColor(SH110X_WHITE);
display.setTextSize(2);
display.setCursor(18, 8);
display.print(F("MINI"));
display.setCursor(18, 28);
display.print(F("DASH"));
display.setTextSize(1);
display.setCursor(14, 50);
display.print(F("KLIKNIJ BY GRAC"));
display.display();
}
void drawDifficultyScreen() {
display.clearDisplay();
display.setTextColor(SH110X_WHITE);
display.setTextSize(1);
display.setCursor(20, 10);
display.print(F("WYBIERZ POZIOM"));
display.setTextSize(2);
display.setCursor(15, 25);
if (difficultyLevel == 0) display.print(F("LATWY"));
else if (difficultyLevel == 1) display.print(F("SREDNI"));
else display.print(F("TRUDNY"));
display.setTextSize(1);
display.setCursor(5, 52);
display.print(F("Czekaj by zaczac..."));
display.display();
}
void drawGame() {
display.clearDisplay();
display.drawLine(0, GROUND_Y + 1, SCREEN_WIDTH - 1, GROUND_Y + 1, SH110X_WHITE);
for (int x = 0; x < SCREEN_WIDTH; x += 8) {
display.drawFastHLine(x, GROUND_Y + 4, 4, SH110X_WHITE);
}
for (int i = 0; i < OBSTACLE_COUNT; i++) {
drawObstacle(obstacles[i]);
}
drawPlayer(PLAYER_X, (int)playerY, PLAYER_SIZE, playerAngle);
display.setTextSize(1);
display.setTextColor(SH110X_WHITE);
display.setCursor(2, 2);
display.print(F("PKT:"));
display.print((unsigned long)(distanceTravelled / 10.0f));
display.display();
}
void drawGameOverScreen() {
display.clearDisplay();
display.setTextSize(2);
display.setTextColor(SH110X_WHITE);
display.setCursor(10, 10);
display.print(F("GAME"));
display.setCursor(18, 30);
display.print(F("OVER"));
display.setTextSize(1);
display.setCursor(20, 52);
display.print(F("WYNIK: "));
display.print((unsigned long)(distanceTravelled / 10.0f));
display.display();
}
void drawPlayer(int x, int y, int size, float angle) {
int cx = x + size / 2;
int cy = y + size / 2;
float h = size / 2.0f;
int x1 = cx + rotatePointX(-h, -h, angle);
int y1 = cy + rotatePointY(-h, -h, angle);
int x2 = cx + rotatePointX( h, -h, angle);
int y2 = cy + rotatePointY( h, -h, angle);
int x3 = cx + rotatePointX( h, h, angle);
int y3 = cy + rotatePointY( h, h, angle);
int x4 = cx + rotatePointX(-h, h, angle);
int y4 = cy + rotatePointY(-h, h, angle);
display.drawLine(x1, y1, x2, y2, SH110X_WHITE);
display.drawLine(x2, y2, x3, y3, SH110X_WHITE);
display.drawLine(x3, y3, x4, y4, SH110X_WHITE);
display.drawLine(x4, y4, x1, y1, SH110X_WHITE);
}
int rotatePointX(float localX, float localY, float angle) {
return (int)round(localX * cos(angle) - localY * sin(angle));
}
int rotatePointY(float localX, float localY, float angle) {
return (int)round(localX * sin(angle) + localY * cos(angle));
}
/* =========================================================
RYSOWANIE
========================================================= */
void drawObstacle(Obstacle& obs) {
if (obs.type == 0) {
int leftX = obs.x;
int rightX = obs.x + obs.w;
int topX = obs.x + obs.w / 2;
int baseY = GROUND_Y + 1;
int topY = baseY - obs.h;
display.fillTriangle(leftX, baseY, topX, topY, rightX, baseY, SH110X_WHITE);
} else if (obs.type == 1) {
int groupTopY = GROUND_Y + 1 - obs.altitude - obs.h;
for (int c = 0; c < obs.cols; c++) {
for (int r = 0; r < obs.rows; r++) {
int bx = obs.x + c * BLOCK_SIZE;
int by = groupTopY + r * BLOCK_SIZE;
display.drawRect(bx, by, BLOCK_SIZE, BLOCK_SIZE, SH110X_WHITE);
display.fillRect(bx + 2, by + 2, BLOCK_SIZE - 4, BLOCK_SIZE - 4, SH110X_WHITE);
}
}
for (int c = 0; c < obs.cols; c++) {
if ((obs.spikesOnTop >> c) & 1) {
int sx = obs.x + c * BLOCK_SIZE;
int sw = BLOCK_SIZE;
int sh = BLOCK_SIZE / 2;
int sy = groupTopY;
display.fillTriangle(sx, sy, sx + sw / 2, sy - sh, sx + sw, sy, SH110X_WHITE);
}
}
}
}