Celeste/PICO8

Typ_projektu
Arduino
Zdjecie główne
Krótki opis projektu

 

Projekt przedstawia grę platformową retro w stylu gier na konsolę Pico-8. Gra działa na mikrokontrolerze Arduino UNO z wykorzystaniem wyświetlacza OLED 128x64 piksele oraz shielda SIC Game Console. Rozgrywka polega na przeskakiwaniu między platformami w celu wyjścia z poziomu unikając przy tym kolców. Po uruchomieniu gracz pojawia się na ekranie startowym będącym jednocześnie planszą do nauki sterowania. Rozgrywka rozpoczyna się po wyjściu z mapy startowej.

Niezbędne elementy

1. Płytka Arduino UNO lub kompatybilna

2. Shield SIC Game Console Sprzęt

3.Komputer PC z zainstalowanym Arduino IDE

Opis projektu

 

OBSŁUGA WYŚWIETLACZA

Do wyświetlania elementów gry wykorzystywana jest biblioteka Adafruit SSD1306 – gra rysuje platformy i ściany jako prostokąty, kolce jako zygzaki, a gracz kontroluje postać o sylwetce człowieka.

ROZGRYWKA

Poziomy Gra ma wgrane 3 poziomy o różnie rozmieszczonych platformach, kolcach i wyjściu, oraz ekran startowy i końcowy z wynikiem.

Ekran początkowy

Na ekranie startwym znajduje się tytuł gry oraz twórcy. Napis PICO-A jest jednocześnie elementem mapy na którym można ćwiczyć sterowanie.

Ekran Końcowy

Na ekranie końcowym jest wyświetlany czas przejścia gry oraz liczba śmierci. Na tym ekranie też można się poruszać. Zasady wygranej/przegranej Do następnego poziomu się przechodzi poprzez dotarcie do wyjścia. Zakończenie gry polega na przejściu wszystkich poziomów. Śmierć następuje gdy postać dotknie kolców lub spadnie w przepaść. W momencie śmierci gracz zostaje przeniesiony na początek danego poziomu. Restart W dowolnym momencie rozgrywki można zresetować dany poziom naciskając naraz wszystkie przyciski kierunkowe a restart gry poprzez jednoczesne naciśnięcie wszystkich przycisków konsoli. Sterowanie ruch w poziomie - prawy/lewy przycisk kierunkowy skok - dolny boczny przycisk, przytrzymanie przycisku daje wyższy skok dash - górny boczny przycisk, dash jest wykonywany w kierunku obranym przyciskami kierunkowymi

DZIAŁANIE WAŻNYCH FUNKCJI

uint8_t getTile(int x, int y) - pobiera wartość danego pola ze skompresowanej mapy void levelStart(int num) - ustawia aktualny poziom oraz przenosi gracza na jego początek void readInput() - czyta wciśnięte przez użytkownika przyciski void movement() - odpowiada za ruch w zależności od wciśniętych przycisków oraz aktualnej pozycji na mapie void drawTimer() - wyświetla Timer void drawPlayer() - rysuje postać gracza oraz odpowiada za prostą animację ruchu void draw() - rysuje wyszystkie obiekty na ekranie void checkHitbox() - w zależności od pozycji gracza ustawia czy można skakać itp oraz sprawdza warunki śmierci i przejścia do następnego poziomu

NAPOTKANE TRUDNOŚCI

Mapa wa wymiary 64x32 i każde pole przechowuje 1 liczbę czyli mapa zajmuje 2kB pamięci. Na konsoli Pico-8 aktualna mapa jest przechowywana na pamięci RAM (Pico-8 ma 64kB RAMu). Arduino Uno ma jedynie 2kB RAMu więc żeby gra działałą płynnie mapy są przechowywane w pamięci Flash i są skompresowane do 2 bitów na komórkę żeby mapy zajmowały mniej pamięci a funkcja getTile dekompresuje i zwraca wartość przechowywaną w pojedynczej komórce co zajmuje mało RAMu.

Zdjęcia
kod programu

ZIP poniżej

#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SH110X.h>
#include <avr/pgmspace.h>
#include "level.h"
#include <stdint.h>


 

#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
#define i2c_Address 0x3C


 

Adafruit_SH1106G display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);



 

void setup() {
  display.begin(i2c_Address, true);
  display.clearDisplay();
  display.setRotation(2);
  pinMode(7, INPUT_PULLUP);
  pinMode(8, INPUT_PULLUP);
  pinMode(9, INPUT_PULLUP);
  pinMode(10, INPUT_PULLUP);
  pinMode(4, INPUT_PULLUP);
  pinMode(5, INPUT_PULLUP);
  pinMode(13, OUTPUT);
}


 

int x=15, y=30, hy=28;
int vx = 0, vxTemp = 0;
int vy = 0;
int yDash = 0;
int levelNum = 0;
int deathCount = 0;
float time = 0;
int xDtemp1, yDtemp1, xDtemp2, yDtemp2, tDash;


 

bool onGround = false , ableToDash = true, ableToRun = true, ableToJump = false;
bool up, down, left, right, jump, dash, tempJump;
bool onWallLeft = false;
bool onWallRight = false;


 

const uint8_t* levels[] = {
  level0,
  level1,
  level2,
  level3,
  level4
};
const uint8_t *level = levels[0];


 

uint8_t getTile(int x, int y) {
  if (x < 0 || y < 0 || x >= WIDTH || y >= HEIGHT) return 1;


 

  int index = y * WIDTH + x;


 

  int byteIndex = index / 4;
  int shift = (index % 4) * 2;


 

  uint8_t data = pgm_read_byte(&level[byteIndex]);



 

  return (data >> shift) & 0b11;
}
void levelStart(int num){
  int startPointX[]={15,5,5,5,5};
  int startPointY[]={30,20,25,7,27};
  x = startPointX[num];
  y = startPointY[num];
  hy = y-2;
  level = levels[num];
  tDash = 0;
  xDtemp1 = 0;
  yDtemp1 = 0;
  xDtemp2 = 0;
  yDtemp2 = 0;
}
void readInput() {
  up = !digitalRead(7);
  right = !digitalRead(8);
  down = !digitalRead(9);
  left = !digitalRead(10);
  jump = !digitalRead(5);
  dash = !digitalRead(4);
  //game reset
    if(up&&down&&right&&left&&dash&&jump){
    deathCount = 0;
    time = 0;
    levelNum = 0;
    levelStart(levelNum);
  }
  if(up&&down&&right&&left){
    //level reset
    levelStart(levelNum);
  }
}
void movement() {
 
  yDash = 0;
  bool jumpPressed = jump && !tempJump;


 

  // normalny ruch
  if (vy <= 1) ableToRun = true;


 

  if (ableToRun){
    vx = 0;
    if (right) vx = 1;
    if (left) vx = -1;
  }


 

  // skok z ziemi
  if (ableToJump) {
    vy = 10;
    ableToJump = false;
  }


 

  // WALL JUMP
  if (!onGround && jumpPressed) {


 

    if (onWallLeft) {
        vx = 1;
        vy = 10;
        ableToRun = false;
    }


 

    if (onWallRight) {
      vx = -1;
      vy = 10;
      ableToRun = false;
    }
  }
  if (getTile(x+vx, y)!=1&&getTile(x+vx, hy)!=1) x += vx;
 
  if (vy>1){
    if (jump&&tempJump){
      if (getTile(x, y-1)!=1&&getTile(x, hy-1)!=1){
        y -= 1;
        hy -= 1;
      }else vy -= 10;
    }else
    if (!jump) vy = 1;


 

  }
  if (vy<0){
    y += 1;
    hy += 1;
    vy = -1;
  }
  tempJump = jump;
  if (dash && ableToDash){
    xDtemp1 = x; yDtemp1 = y;
    if(up) yDash = -1;
    if(down) yDash = 1;
    for (int d = 0; d < 12; d++){
      if(getTile(x+vx, y+yDash)!=1&&getTile(x+vx, hy+yDash)!=1){
        x = x + vx;
        y = y + yDash;
        hy = hy + yDash;
      }
      xDtemp2 = x; yDtemp2 = y;
      tDash = 5;
      checkHitbox();
    }
    ableToDash = false;
  }
}
void drawTimer() {
  long total = (long)(time * 100); // setne sekundy


 

  int minutes = total / 6000;
  int seconds = (total / 100) % 60;
  int centiseconds = total % 100;
  if(levelNum<4){
    display.fillRect(0, 0, 50, 10, SH110X_BLACK);
    display.setTextSize(1);
    display.setCursor(0, 0);
  }else{
    display.setTextSize(2);
    display.setCursor(14, 10);
  }


 

  display.setTextColor(SH110X_WHITE);


 

  if (minutes < 10) display.print("0");
  display.print(minutes);
  display.print(":");


 

  if (seconds < 10) display.print("0");
  display.print(seconds);
  display.print(".");


 

  if (centiseconds < 10) display.print("0");
  display.print(centiseconds);
}
void drawPlayer(){
  //draw player
  if(x%3==0||!onGround){


 

  display.drawPixel(2*x, 2*y, SH110X_WHITE);
  display.drawPixel(2*x, 2*y-1, SH110X_WHITE);
  display.drawPixel(2*x+1, 2*y+1, SH110X_WHITE);
  display.drawPixel(2*x-1, 2*y+1, SH110X_WHITE);
  display.drawPixel(2*x+1, 2*y, SH110X_WHITE);
  display.drawPixel(2*x-1, 2*y, SH110X_WHITE);
  display.drawPixel(2*x+1, 2*y-2, SH110X_WHITE);
  display.drawPixel(2*x-1, 2*y-2, SH110X_WHITE);
  display.drawPixel(2*x+1, 2*y-3, SH110X_WHITE);
  display.drawPixel(2*x-1, 2*y-3, SH110X_WHITE);
  display.drawPixel(2*x, 2*y-1, SH110X_WHITE);
  display.drawPixel(2*x, 2*y-2, SH110X_WHITE);
  display.drawPixel(2*x, 2*y-3, SH110X_WHITE);
  display.drawPixel(2*x, 2*y-4, SH110X_WHITE);
  }else{


 

  display.drawPixel(2*x, 2*y+1, SH110X_WHITE);
  display.drawPixel(2*x, 2*y, SH110X_WHITE);
  display.drawPixel(2*x+1, 2*y-2, SH110X_WHITE);
  display.drawPixel(2*x-1, 2*y-2, SH110X_WHITE);
  display.drawPixel(2*x+1, 2*y-3, SH110X_WHITE);
  display.drawPixel(2*x-1, 2*y-3, SH110X_WHITE);
  display.drawPixel(2*x, 2*y-1, SH110X_WHITE);
  display.drawPixel(2*x, 2*y-2, SH110X_WHITE);
  display.drawPixel(2*x, 2*y-3, SH110X_WHITE);
  display.drawPixel(2*x, 2*y-4, SH110X_WHITE);
  }
}
void drawSkull() {
  int ox = 40; // offset X
  int oy = 27; // offset Y


 

  //write death count
  display.setTextSize(2);
  display.setCursor(ox+22, oy);
  display.setTextColor(SH110X_WHITE);
  display.print(deathCount);


 

    // --- ZARYS
  display.drawLine(ox+4, oy+10, ox+11, oy+10, SH110X_WHITE);
  display.drawLine(ox+1, oy+8, ox+3, oy+8, SH110X_WHITE);
  display.drawLine(ox+12, oy+8, ox+14, oy+8, SH110X_WHITE);
  display.drawLine(ox+0, oy+4, ox+0, oy+7, SH110X_WHITE);
  display.drawLine(ox+15, oy+4, ox+15, oy+7, SH110X_WHITE);
  display.drawLine(ox+5, oy+0, ox+10, oy+0, SH110X_WHITE);
  display.drawPixel(ox+1, oy+3, SH110X_WHITE);
  display.drawPixel(ox+2, oy+2, SH110X_WHITE);
  display.drawPixel(ox+3, oy+1, SH110X_WHITE);
  display.drawPixel(ox+4, oy+1, SH110X_WHITE);
  display.drawPixel(ox+11, oy+1, SH110X_WHITE);
  display.drawPixel(ox+12, oy+1, SH110X_WHITE);
  display.drawPixel(ox+13, oy+2, SH110X_WHITE);
  display.drawPixel(ox+14, oy+3, SH110X_WHITE);


 

  // --- OCZY (+)
  // lewe
  display.drawPixel(ox+4, oy+4, SH110X_WHITE);
  display.drawPixel(ox+3, oy+5, SH110X_WHITE);
  display.drawPixel(ox+4, oy+5, SH110X_WHITE);
  display.drawPixel(ox+5, oy+5, SH110X_WHITE);
  display.drawPixel(ox+4, oy+6, SH110X_WHITE);


 

  // prawe
  display.drawPixel(ox+11, oy+4, SH110X_WHITE);
  display.drawPixel(ox+10, oy+5, SH110X_WHITE);
  display.drawPixel(ox+11, oy+5, SH110X_WHITE);
  display.drawPixel(ox+12, oy+5, SH110X_WHITE);
  display.drawPixel(ox+11, oy+6, SH110X_WHITE);


 

  // --- NOS (kwadrat)
  display.drawPixel(ox+7, oy+6, SH110X_WHITE);
  display.drawPixel(ox+7, oy+7, SH110X_WHITE);
  display.drawPixel(ox+8, oy+6, SH110X_WHITE);
  display.drawPixel(ox+8, oy+7, SH110X_WHITE);


 

  // --- ZĘBY
  display.drawPixel(ox+4, oy+9, SH110X_WHITE);
  display.drawPixel(ox+6, oy+9, SH110X_WHITE);
  display.drawPixel(ox+9, oy+9, SH110X_WHITE);
  display.drawPixel(ox+11, oy+9, SH110X_WHITE);


 

  // --- PISZCZEL
  display.drawLine(ox+3, oy+14, ox+12, oy+14, SH110X_WHITE);
  display.drawLine(ox+3, oy+12, ox+12, oy+12, SH110X_WHITE);
  display.drawPixel(ox+1, oy+11, SH110X_WHITE);
  display.drawPixel(ox+2, oy+11, SH110X_WHITE);
  display.drawPixel(ox+13, oy+11, SH110X_WHITE);
  display.drawPixel(ox+14, oy+11, SH110X_WHITE);
  display.drawPixel(ox+1, oy+15, SH110X_WHITE);
  display.drawPixel(ox+2, oy+15, SH110X_WHITE);
  display.drawPixel(ox+13, oy+15, SH110X_WHITE);
  display.drawPixel(ox+14, oy+15, SH110X_WHITE);
  display.drawPixel(ox+0, oy+12, SH110X_WHITE);
  display.drawPixel(ox+1, oy+13, SH110X_WHITE);
  display.drawPixel(ox+0, oy+14, SH110X_WHITE);
  display.drawPixel(ox+15, oy+12, SH110X_WHITE);
  display.drawPixel(ox+14, oy+13, SH110X_WHITE);
  display.drawPixel(ox+15, oy+14, SH110X_WHITE);
}
void draw(){
  display.clearDisplay();
 
  //draw map
  for (int i = 0; i<WIDTH; i++) {
    for (int j = 0; j < HEIGHT; j++) {
      uint8_t tile = getTile(i, j);
      if (tile==1||tile==2) display.drawPixel(2*i, 2*j, SH110X_WHITE);
      if (tile==1||tile==2) display.drawPixel(2*i, 2*j+1, SH110X_WHITE);
      if (tile==1||tile==2) display.drawPixel(2*i+1, 2*j, SH110X_WHITE);
      if (tile==1||tile==2) display.drawPixel(2*i+1, 2*j+1, SH110X_WHITE);
    }
  }
  drawPlayer();
  //draw dash trail
  if (tDash>0){
    display.drawLine(2*xDtemp1, 2*yDtemp1, 2*xDtemp2, 2*yDtemp2, SH110X_WHITE);
    tDash -=1;
  }
  if(levelNum>0){
    drawTimer();
  }else{
    display.setTextColor(SH110X_WHITE);
    display.setTextSize(1);
    display.setCursor(10, 45);
    display.print("by: PCh & Zgirak");
  }
  if(levelNum==4)drawSkull();
  display.display();
}
void checkHitbox(){
  //next level
  if (getTile(x, y)==3 ||getTile(x, hy)==3){
    levelNum += 1;
    levelStart(levelNum);
  }
  //death
  if (getTile(x, y)==2 ||getTile(x, hy)==2 || y==31){
    levelStart(levelNum);
    deathCount += 1;
  }
  //check for floor
  if (getTile(x, y+1)==1){
    onGround = true;
    ableToDash = true;
    ableToJump = true;
    tempJump = true;
    ableToRun = true;
    vy = 0;
  }else{
    onGround = false;
    ableToJump = false;
    vy -=1;
  }
  onWallLeft = (getTile(x-1, y-1)==1);
  onWallRight = (getTile(x+1, y-1)==1);
}



 

void loop() {
 
  checkHitbox();
  readInput();
  movement();
  draw();


 

  delay(1);
  if(levelNum!=4&&levelNum!=0)time += 0.1;


 

}
Pliki_projektu
Schemat
Youtube
Tagi
celeste PICO - 8 A