Dziś mnie natchnęło na zrobienie prostej gierki – sapera, ale nie o samą grę tu chodzi, a o to, ze przy okazji przedstawię krok po kroku z przykładami jak taką grę napisać pokazując swoją metodę podchodzenia do czegoś takiego.
Założenia projektu:
- kod na tyle „uniwersalny”, żeby można było go dostosować do innych rzeczy
- wynikiem ma być w pełni funkcjonalna gra
- wyświetlanie pozostałej ilości bomb
- randomowe plansze – to w późniejszej wersji – wjedzie pewnie szum z ADC jako mechanizm generowania liczb pseudolosowych (czy wtedy już będą losowe?)
Jest to dość spory projekt – potrzebujemy więc plan działania co i jak.
1. wybór platformy sprzętowej
2. pierwotny wygląd planszy – funkcja rysująca
3. funkcje umożliwiające „zapełnienie” poszczególnej kratki jaką chcemy
4. funkcje umożliwiające przechodzenie pomiędzy poszczególnymi kratkami
5. sposób zapisu danych odnośnie planszy + funkcje
6. sposoby wyświetlania danych na planszy + funkcje
7. interakcje - czyli ogólnie gra - zaznaczanie pól, odkrywanie, wygrana/przegrana + funkcje
7. sposób przedstawienia wyniku/ilości pozostałych bomb itp. + funkcje
Roboty jest sporo więc zaczynamy.
1. wybór platformy sprzętowej
Tutaj w moim przypadku najlepszym wyborem jest płytka z projektu SUDOKU STM32 – zawiera mikrokontrolerek STM32F030C8T6, kolorowy wyświetlacz ILI9341 + przyciski. Dzięki temu też podstawowe funkcje miałem już napisane – teraz je poprzerabiałem na bardziej uniwersalne – więc nawet jakby ktoś chciał zrobić szachy na tej podstawie – nie ma problemu!
2. Pierwotny wygląd planszy – funkcja rysująca
Wszyscy zapewne wiemy jak wygląda saper. Plansza z kwadratami, jednak jej rozmiar nie jest do końca zdefiniowany. Pamiętając o założeniu z uniwersalnością najpierw zadeklarujmy sobie kilka rzeczy z których najważniejsze to ile pikseli ma mieć bok kratki, ile kratek ma być w osi X i ile w osi Y. Czyli prosty sposób:
Kod: Zaznacz cały
#define BOK_KRATKI 25
#define PLANSZA_X 9
#define PLANSZA_Y 11
#define PLANSZA_START_X 6
#define PLANSZA_START_Y 6
#define PLANSZA_GRUBOSC_LINII 3
#define KOLOR_OBRAMOWANIA ILI9341_BLUE
Możemy teraz napisać funkcję, która rysuje obramowania:
Kod: Zaznacz cały
// funkcja rysujaca obramowanie gry
void rysuj_obramowanie(void)
{
// linie poziome
for(uint8_t i = 0; i < PLANSZA_Y + 1; i++)
{
lcd_zapelnij_prostokat( PLANSZA_START_X,
PLANSZA_START_X + i * BOK_KRATKI,
BOK_KRATKI * PLANSZA_X + PLANSZA_GRUBOSC_LINII,
PLANSZA_GRUBOSC_LINII,
KOLOR_OBRAMOWANIA);
}
// linie pionowe
for(uint8_t i = 0; i < PLANSZA_X + 1; i++)
{
lcd_zapelnij_prostokat( PLANSZA_START_X + i * BOK_KRATKI,
PLANSZA_START_Y,
PLANSZA_GRUBOSC_LINII,
BOK_KRATKI * PLANSZA_Y + PLANSZA_GRUBOSC_LINII,
KOLOR_OBRAMOWANIA);
}
}
Efekt widzimy na zdjęciu:
Efekt dla ustawień:
Kod: Zaznacz cały
#define PLANSZA_X 6
#define PLANSZA_Y 6
#define PLANSZA_START_X 30
#define PLANSZA_START_Y 30
Jest taki:
Czyli ten punkt już ogarnięty!
3. funkcje umożliwiające „zapełnienie” poszczególnej kratki jaką chcemy
Co chcemy uzyskać – funkcje, która po podaniu numeru kratki i zawartości – zadba nam o jej wypełnienie. Dla ułatwienia wpiszmy teraz do niej jakąś cyferkę.
Wjeżdzaja kolejne definy – potrzebne do umiejscowienia liteki w kratce – czyli przesunięcie w osi X i Y od piksela z „początkiem” kratki:
Kod: Zaznacz cały
#define PRZESUNIECIE_TEKSTU_X 4
#define PRZESUNIECIE_TEKSTU_Y 4
Najwygodniej będzie, jeśli nasza funkja będzie przyjmować numer kratki, jej zawartość i kolor tła. Wygląda ona następująco:
Kod: Zaznacz cały
// funkcja wpisujaca w podana kratke okreslona liczbe
void zapelnij_kratke(uint8_t numer, uint8_t liczba,uint16_t kolor)
{
char buff[2] = "a";
buff[0] = liczba + 48;
// zamalowanie poprzedniej zawartosci kratki
lcd_zapelnij_prostokat( PLANSZA_START_X + PLANSZA_GRUBOSC_LINII + (numer%PLANSZA_X) * BOK_KRATKI,
PLANSZA_START_Y + PLANSZA_GRUBOSC_LINII + (numer/PLANSZA_X) * BOK_KRATKI,
BOK_KRATKI - PLANSZA_GRUBOSC_LINII,
BOK_KRATKI - PLANSZA_GRUBOSC_LINII,
kolor);
// wypelnienie literka
lcd_pisz_tekst_16( PLANSZA_START_X + PLANSZA_GRUBOSC_LINII + (numer%PLANSZA_X) * BOK_KRATKI + PRZESUNIECIE_TEKSTU_X,
PLANSZA_START_Y + PLANSZA_GRUBOSC_LINII + (numer/PLANSZA_X) * BOK_KRATKI + PRZESUNIECIE_TEKSTU_Y,
buff,
ILI9341_BLACK,
kolor);
}
Komentarze z kodu mówią same za siebie.
Przetestujmy teraz jej działanie za pomocą funkcji testowej:
Kod: Zaznacz cały
// funkcja testowa zapelniajaca kolejne komorki planszy
void test_zapelnienia()
{
uint8_t liczba = 0;
uint8_t kratka = 0;
while(1)
{
zapelnij_kratke(kratka,liczba,ILI9341_CYAN);
liczba++;
kratka++;
if( liczba == 10 )
{
liczba = 0;
}
if(kratka == PLANSZA_X * PLANSZA_Y)
{
kratka = 0;
}
_delay_ms(200);
}
}
Efekt – na filmiku:
https://youtu.be/M6DcSE_EoiE
Jak widać – wszystko działa ok! nawet dla różnych rozmiarów planszy.
4. funkcje umożliwiające przechodzenie pomiędzy poszczególnymi kratkami
Co chcemy uzyskać – możliwość przechodzenia pomiędzy kolejnymi kratkami za pomocą „strzałek na płytce”. Wiadomo strzałka w górę – do góry itp.
Wprowadźmy kilka zmiennych globalnych (globalnych dla wygody):
Kod: Zaznacz cały
// numer wybranej kostki - pozycja "kursora" na planszy - tam gdzie aktualnie się znajdujemy
uint8_t wybrana_kostka = 0;
// numer kostki z ktorej zrobilismy ruch
uint8_t poprzednia_kostka;
Zróbmy jedną funkcję ruchu, która jako parametr pobierze kierunek ruchu. Ułatwmy sobie sprawę enumem:
enum{ruch_lewo, ruch_prawo, ruch_gora,ruch_dol};
I tak wygląda nasza funkcja:
Kod: Zaznacz cały
// funkcja wyliczajaca nastepna pozycje kratki
void ruch_po_planszy( uint8_t kierunek )
{
switch( kierunek )
{
case ruch_lewo:
{
poprzednia_kostka = wybrana_kostka;
wybrana_kostka--;
if(wybrana_kostka == 255)
{
wybrana_kostka = ( PLANSZA_X * PLANSZA_Y ) - 1;
}
break;
}
case ruch_prawo:
{
poprzednia_kostka = wybrana_kostka;
wybrana_kostka++;
if(wybrana_kostka == ( PLANSZA_X * PLANSZA_Y ))
{
wybrana_kostka = 0;
}
break;
}
case ruch_gora:
{
poprzednia_kostka = wybrana_kostka;
wybrana_kostka -= PLANSZA_X;
if(wybrana_kostka > 200)
{
wybrana_kostka = poprzednia_kostka + ( PLANSZA_X * ( PLANSZA_Y - 1 ) );
}
break;
}
case ruch_dol:
{
poprzednia_kostka = wybrana_kostka;
wybrana_kostka += PLANSZA_X;
if(wybrana_kostka > ( PLANSZA_X * PLANSZA_Y ) - 1)
{
wybrana_kostka = wybrana_kostka%PLANSZA_X;
}
break;
}
}
}
Teraz kwestia pożenienia tego ze sprzętem – akurat ja stosuję bibliotekę do obsługi przycisków, która wywołuje podpięte callbacki do eventów przycisków. Musimy więc dla każdego przycisku napisać osobną funkcję:
Kod: Zaznacz cały
void button_left()
{
ruch_po_planszy( ruch_lewo );
zamaluj_pola_po_ruchu();
}
void button_right()
{
ruch_po_planszy( ruch_prawo );
zamaluj_pola_po_ruchu();
}
void button_up()
{
ruch_po_planszy( ruch_gora );
zamaluj_pola_po_ruchu();
}
void button_down()
{
ruch_po_planszy( ruch_dol );
zamaluj_pola_po_ruchu();
}
I podpiąć je do przycisków:
Kod: Zaznacz cały
// przypisanie funkcji do przyciskow
button_ustaw(0,5,200,50,button_up, NULL );
button_ustaw(1,5,200,50,button_left, NULL );
button_ustaw(2,5,200,50,button_ok, NULL );
button_ustaw(3,5,200,50,button_right,NULL );
button_ustaw(4,5,200,50,button_down, NULL );
Wyżej użyliśmy funkcji “zamaluj_pola_po_ruchu();” – co ona robi – dla pozycji ze zmiennej „wybrana_kostka” wpisuje cyferkę „1”, natomiast dla poprzedniej wpisuje cyferkę „0” – wtedy ładnie przetestujemy działanie.
Tak ona wygląda:
Kod: Zaznacz cały
void zamaluj_pola_po_ruchu()
{
zapelnij_kratke(poprzednia_kostka,0,ILI9341_WHITE);
zapelnij_kratke(wybrana_kostka,1,ILI9341_YELLOW);
}
Uruchamiamy całość i oto efekt:
https://youtu.be/S8M40DbdZ1Y
W sumie mamy już nasz „silnik gry” dalej skupimy się bardziej na logice gry.
CDN... komentarze uwagi itp mile widziane kody udostępnie na samym końcu