VIDI Project X #59:
multiplayer game TIC-TAC-TOE

Radi savladavanja korisnički mnogo jednostavnije UDP mrežne komunikacije, demonstrirat ćemo vam kako pomoću UDP protokola od klasične igre križić-kružić napraviti mrežnu igru za dva igrača

Križić-kružić igra je koju ste sigurno znali zaigrati u vašim mlađim danima. Nakon što ste je savladali do nepobjedivosti, pala je u zaborav, no zbog svoje je jednostavnosti vrlo dobar primjer računalne igre tj. koda za učenje mnogočega. Kada biste npr. igrali protiv računala, mogli biste ga pokušati naučiti postati nepobjedivo.

Mi smo vam pak u ovoj radionici odlučiti pokazati osnove mrežne komunikacije kako biste istu metodu znali primijeniti i na druge mrežne igre.

Za ovu će vam radionicu trebati VIDI X mikroračunalo i pristupna točka za Wi-Fi, ne nužno povezana s internetom, te dijeljena s mobitela ili rutera.

Kako bismo se pozabavili mrežnim dijelom koda, pri izradi smo ove radionice prvo potražili postoji li negdje na internetu već isprogramiran križić-kružić. Tako smo među prvim rezultatima pronašli kod za Arduino UNO na linku: https://create.arduino.cc/projecthub/nickthegreek82/arduino-touch-tic-tac-toe-game-792816.

Valjalo je preuzeti kod preurediti za pokretanje na VIDI X mikroračunalu.

Prvo treba maknuti biblioteke ekrana i dodati one korištene na VIDI X mikroračunalu. Uz njih je potrebno definirati varijable i objekt zaslona te objekt za senzor dodira. Njemu je potrebno postaviti granične vrijednosti očitane na rubu igraće ploče.

Unutar glavne ćete petlje loop() pronaći dio koda kojim se na serijski monitor ispisuju translatirane koordinate dodira putem varijabli p.x i p.y, pa s njim pokrenite i „Serial Monitor“ u Arduino IDE razvojnoj okolini kako biste odmah provjerili postoji li potreba za prilagođavanjem vrijednosti TS_MINX i TS_MINY te TS_MAXX i TS_MAXY varijabli za ispravljanje točnost dodirne točke.

Unutar glavne petlje, ali i u ostalim dijelovima koda, bilo je potrebno prilagoditi linije u kojima je korištena os z dodirnog ekrana. Ona služi za detekciju jačine pritiska, pa tako naša p točka, uz p.x i p.y, ima i koordinate p.z.

Osjetljivost dodira u ovome kodu možete povećati ili smanjiti promjenom Z_THRESHOLD varijable.

Izgled koda prilagođenog za VIDI X mikroračunalo provjerite na poveznici: https://github.com/VidiLAB-com/Vidi-X/tree/master/Krizic-Kruzic u skici Krizic-Kruzic.ino.

Nakon što smo osigurali dovoljno precizan i osjetljiv križić-kružić čiji kod može odrediti početak, kraj i ishod igre, krenimo s dodavanjem koda potrebnog za mrežnu komunikaciju.

 

Prvi je na potezu igrač kojemu na početnom ekranu piše „Player One“

 

Koristit ćemo AsyncUDP.h biblioteku. Kako biste se pobliže upoznali s UDP komunikacijom, proučite biblioteci priložene primjere. Otvorite ih klikom na Datoteka → Primjeri → ESP32 Async UDP te odaberite Client, MulticastServer i Server primjere.

Asinkrona serijska komunikacija preko UDP-a pruža mogućnost inkapsuliranja asinkronih podataka u User Datagram Protocol (UDP) pakete i zatim njihovo nepouzdano slanje bez potrebe za uspostavljanjem veze s uređajem primateljem.

Pri tome prvo treba razumjeti asinkronu komunikaciju. Sinkronost označava istovremenost radnji, dok je asinkronost neistovremenost. Dakle, pri asinkronoj komunikaciji jedan uređaj sluša dok drugi odašilje. Nadalje treba razumjeti značenje nepouzdanog slanja. Ono se odnosi na odašiljanje poruke za koju pošiljatelj ne zna uspješnost dospijeća tj. cjelovitost primitka. Najčešće se UDP poruke šalju s jedinstvenog izvora na jedno odredište, no mogu se služiti i multicasting slanjem kojim se poruke šalju s jedinstvenog pošiljatelja na veći popis primatelja.

Još je potrebno razumijeti koncept broadcasta, također korišten u UDP okruženju. Poruke se pri emitiranju (broadcasting) šalju s jedinstvenog izvora na neodređen popis odredišta, odnosno svatko s pristupom na tu mrežu može primati poruke, dok pošiljatelj ne zna njihov broj.

UDP komunikacija koristi se kad je brza komunikacija u stvarnom vremenu ključna, a gubitak određenog broja paketa u procesu prihvatljiv, a TCP komunikacija koristi se kad je prijenos svakog paketa bitan. Odnosno, UDP nudi manje kašnjenja podataka u usporedbi s pouzdanijim TCP-om.

 

Arduino IDE i borba s bibliotekama

 

Nemate li već instaliran Arduino IDE, to možete učiniti uz pomoć radionice na linku: https://vidi-x.org/radionice/vidi-project-x-91-arduino-ide/.

VIDI Project X #91: Arduino IDE – Windows instalacija

Nakon instalacije Arduino IDE razvojnog okruženja, potrebno je instalirati biblioteke koje nedostaju.
Putem „Library Manager“ izbornika pronađite verzije 1.5.6 „Adafruit_ILI9341“ biblioteke, odnosno 1.3.0 „XPT2046_Touchscreen“ biblioteke, te ih instalirajte.
AsyncUDP“ biblioteka već je instalirana uz podršku za ESP32 razvojno okruženje.

 

Kod

Govoreći o multiplayer igrama za veći broj (u našem slučaju dva) igrača, treba imati na umu kako jedno računalo mora biti server, a drugo klijent. U ovoj smo radionici izradili dva koda, tj. dvije minimalno različite skice. Prva u imenu sadrži riječ server, a druga client.
Server skica drugi je igrač po redu, tj. Player Two, kako piše na ekranu VIDI X mikroračunala, dok je client skica Player One te se od njega očekuje prvi potez.
Kako bismo inicirali korištenje UDP protokola, na je početak skice potrebno dodati biblioteke te definirati varijable za Wi-Fi i stvoriti UDP objekt.

 

#include "WiFi.h"
#include "AsyncUDP.h"

const char * ssid = "Wireless";
const char * password = "password za vaš wireless";

AsyncUDP udp;

 

Sada u setup() funkciji dodajemo naredbe za spajanje na Wi-Fi te putem if uvjeta provjeravamo je li ostvarena UDP konekcija.

 

WiFi.mode(WIFI_STA); // Make this the Wi-Fi client (the Wi-Fi router (or server) is WIFI_AP)
WiFi.begin(ssid, password);

if (WiFi.waitForConnectResult() != WL_CONNECTED) {
  Serial.println("WiFi Failed");
  TFT.println("WiFi Failed");
  while (1) {
    delay(1000);
  }
}

if (udp.connect(IPAddress(192, 168, 0, 104), 1234)) {
  Serial.print("UDP connected - My IP is: ");
  TFT.print("UDP connected - My IP is: ");
  Serial.println(WiFi.localIP());
  TFT.println(WiFi.localIP());
}

 

Osvrnite se na liniju koda s IPAddress naredbom. Njome se definira s kojim uređajem želimo komunicirati te na kojem portu. U našem slučaju koristimo port 1234, no može se koristiti i neki drugi u rasponu do 65535.

Pripazite tu IP adresu promijeniti u onu VIDI X mikroračunala s kojim želite komunicirati. Najbolje je stoga na drugom VIDI X mikroračunalu pokrenuti isti ovaj kod kako biste pronašli njegovu IP adresu.

Server, tj. Player Two kreće kroz skicu iz pozicije čekanja poruke dok klijent, tj. Player One mora napraviti prvi potez koji će pokrenuti dio skice za odašiljanje poruke.

Unutar petlje playGame() imamo uvjet if (moves % 2 == 1) kojim parne, odnosno neparne poteze preusmjerava na dio skice za primanje ili slanje poruke.

Nakon što igrač odigra potez, on se manifestira na igraću ploču smještenu u nizu board[], koja je, s obzirom na to da je križić-kružić igra na devet polja, niz od devet brojeva.

 

void printBoardPM()
{
int i = 0;
Serial.println("SBoard: [");
String Poruka = "SBoard:";

for (i = 0; i < 9; i++)
{
  Serial.print(board[i]);
  Serial.print(",");
  Poruka = String (Poruka + String(board[i]));
}

Serial.print("]");
Serial.print("S Poruka:");
Serial.println(Poruka);

udp.connect(IPAddress(192,168,0,104), 1234); // IP adresa klijenta
udp.print(Poruka);
}

 

Uočite kako smo funkciju printBoard() iz originalnog koda preimenovali u printBoardPM(), odnosno printBoardCM(), kako bismo pri interakciji igrača prvom funkcijom UDP protokolom poslali poruku, dok drugom nismo trebali jer smo je neposredno prije njenog izvršavanja primili od protivničkog igrača.
Odašiljanoj poruci izvor iz servera označavamo dodavanjem SBoard riječi na početak, dok u drugom slučaju CBoard kreira klijentovu poruku na početku.
Time smo željeli pokazati na koji je način moguće označiti dio UDP poruke kako bi primatelj mogao razlučiti gdje su za njega bitni podaci. Nadalje, putem for petlje na poruku dodajemo niz u kojem se nalazi stanje igraće ploče, putem linije koda:

 

Poruka = String (Poruka + String(board[i]));

 

u kojoj vidimo kako vrijednost niza (koja je integer, tj. cijeli broj) pretvaramo u string ne bismo li je dodali na poruku istoga oblika.
Uz pomoć naredbe Serial.print pratimo stanje na serijskoj konzoli kako bismo znali što se s našom porukom događa. Naredbama:

 

udp.connect(IPAddress(192,168,0,104), 1234); // IP adresa klijenta
udp.print(Poruka);

 

ostvarujemo konekciju te šaljemo pripremljenu poruku.

Kod može odrediti izjednačenje ili pobjednika pojedinog igrača

 

Za to se vrijeme s druge strane mreže odvija kod funkcije computerMove() u kojemu većinu vremena, tj. dokle god putem varijable b ne dobijemo potvrdu o primitku očekivane poruke, vrtimo funkciju commListen().

 

void commListen() {
if (udp.listen(1234)) {

  udp.onPacket([](AsyncUDPPacket packet) {
    PorukaP = String( (char*) packet.data()); // Board: 211021212

    if (PorukaP.startsWith("CBoard:", 0)) {
      Serial.println("Got an #CBoard:# message from the server");
      PorukaP = PorukaP.substring(7); // Rješavamo se prvog dijela "Board: " kako bi ostalo samo "211021212"
      Serial.println(PorukaP);
      translateMessage();
      Serial.println("translateMessage done!");
      b = 1; // Oznaka da smo primili poruku
    }

  }

  );
}
}

 

Ova funkcija provjerava primitak poruke na portu 1234 te pretvorbu paketnih podataka iz packet.data() u String kako bi se mogli pospremiti u varijablu PorukaP.
Nakon toga je potrebno provjeriti sadržaj poruke. Novim if uvjetom provjeravamo počinje li poruka očekivanim pošiljateljevim stringom, u kojem slučaju mičemo njen početni identifikatorski dio kako bi nam ostao samo onaj s prikazom stanja igraće ploče.
Potom pozivamo funkciju translateMessage() zaduženu za prevođenje string poruke u niz cijelih brojeva kojima se stanje igraće ploče mijenja novim stanjem iz poruke.

 

void translateMessage()
{
  int i = 0;
  Serial.println("Board: [");
  Poruka = "Board: ";
  Serial.println(PorukaP);
  for (i = 0; i < 9; i++)
  {
    String P = String(PorukaP[i]);
    board[i] = P.toInt();
    Serial.print(board[i]);
    Serial.print(",");
    Poruka = Poruka + String(board[i]);
  }
  Serial.print("]");
  Serial.print("Poruka:");
  Serial.println(Poruka);
}

 

U toj je funkciji najbitnija for petlja u kojoj putem dvije naredbe:

 

String P = String(PorukaP[i]);
board[i] = P.toInt();

 

pripremamo poruku za kreiranje novog stanja igraće ploče.

Po završetku se programski kod vraća u commListen() funkciju te varijabla b postaje 1.

Potom se kod vraća u computerMove() funkciju koja između ostaloga iscrtava novonastali potez.

Nakon toga se vraćamo u playGame() funkciju zaduženu za provjeru pobjednika te se, ako on još ne postoji, moves varijabla povećava za jedan kako bi kroz novi prolazak playGame() funkcijom igra čekala potez igrača kojim se vraćamo na ranije objašnjeni if (moves % 2 == 1) uvjet koji parne odnosno neparne poteze tj. vrijednosti moves varijable preusmjerava na dio skice za primanje ili slanje poruke.

moves % 2 znači da ostatak dijeljenja moves varijable određujemo brojem dva pri čemu je on nula ako je broj paran i jedan ako nije. To je najlakši način za izvršavanje nekog dijela koda svaki drugi put.

Taj se dio koda ponavlja dok netko od igrača ne pobijedi ili dok broj poteza ne preraste broj polja na igraćoj ploči.

U našim testiranjima križić-kružić igre nije dolazilo do gubljenja paketa, no ponekada se znalo desiti da poruka ima dodatak u obliku smetnji ili nekih slučajnih znakova, no to nam s obzirom na način obrađivanja poruke nije smetalo za predviđeno izvršavanje koda.

Kod pronađite na našem GitHub kanalu na adresi: https://github.com/VidiLAB-com/Vidi-X/tree/master/Krizic-Kruzic,
server skica nalazi se u mapi: https://github.com/VidiLAB-com/Vidi-X/tree/master/Krizic-Kruzic/Krizic-Kruzic-server,
a client skica u mapi: https://github.com/VidiLAB-com/Vidi-X/tree/master/Krizic-Kruzic/Krizic-Kruzic-client.

 

Korisni savjeti

Kako smo istovremeno radili (programirali) sa dvije skice, a Arduino IDE nije u stanju istovremeno na dva različita COM porta raditi s dva uređaja, koristili smo Arduino IDE 2.0.0.-beta.4 za klijent skicu, a klasični Arduino IDE za server skicu. Tako smo kompajliranja i upload skica mogli istodobno pokrenuti u oba IDE sučelja čime smo si ubrzali debugging.

 

Arduino IDE 2.0.0.-beta.4 ima i tamnu temu, no najveći je benfit serijska konzola koja uz klasični Arduino može prikazati komunikaciju s drugog COM porta

 

Spajanjem vanjske Wi-Fi antene na VIDI X mikroračunalo možete povećati domet VIDI X mikroračunala prema Wi-Fi pristupnoj točki. Kako ne biste kupovali dodatnu antenu, pokušajte je reciklirati iz staroga laptopa koji više nije u stanju raditi ozbiljan posao, ili iz od vremena pregaženoga beskorisnog Wi-Fi rutera.

 

Antenu smo izvadili iz starog CISCO rutera. Malenih je dimenzija i ima odličan signal.

 

Što možemo učiniti nakon što igra proradi?

Zadatak 1: Za vježbu pokušajte napraviti zamjenu redoslijeda igrača nakon kraja partije.

Zadatak 2: Kako pri UDP komunikaciji može doći do ispadanja dijela paketa, moguće je da odaslana poruka ne bude primljena. Pokušajte napraviti provjeru primitka odašiljanjem poruke sve po primitka povratnog signala od primatelja, možda u obliku string poruke „Poruka primljena“.

Zadatak 3: Manji bi izazov mogao biti server VIDI X mikroračunala postaviti u Wi-Fi mod access pointa kako vam za komunikaciju ne bi trebao dodatni ruter.

PRIJTELJI PROJEKTA

bez kojih sve ovo ne bi bilo moguće.

SVA PRAVA PRIDRŽANA - VIDI TO 2020.

Niti jedan dio ovog web site-a ne smije se u bilo kojem obliku ili radi bilo koje namjene reproducirati bez prethodne pismene suglasnosti izdavača, Svi tekstovi objavljeni na www.vidi-x.com pripremljeni su s osobitom pažnjom i kontrolirani na više razina. Redakcija www.vidi-x.com međutim, ni u kojem slučaju ne može odgovarati za moguće štete bilo kakve vrste nastale na osnovu savjeta, tekstova, slika ili drugog redakcijskog ili oglašivačkog materijala objavljenog na www.vidi-x.com ili na drugi način datog od strane zaposlenika ili suradnika izdavača.