Reaali Robootika.COM

NXT robotimaailm ja programmeerimine C-keeles

10. 9 klass 4 õppetundi Ping-Pong kahele üle Bluetoothi

Tiigrihype_logoSee materjal on loodud Tiigrihüppe Sihtasutuse programmi ProgeTiiger raames.

Selle projekti juures saame teada, kuidas nxt-d omavahel üle bluetoothi reaalajas suhtlevad ja kuidas arvutada palli põrkumine ekraanil juhuslikult etteantud arvu põhjal.

 

Ping-pong mäng kahe NXT vahel toimub üle Bluetooth ühenduse, nii et mängijad võivad asuda teineteisest kuni 30 m kaugusel. Kummalgi mängijal on oma NXT, mille ekraanilt saab jälgida kogu mängu kulgu.

Mõlemad mängijad saavad liigutada oma väravat NXT külge ühendatud mootori abil. Mängu alustava mängija NXT-l on küljes ka teine mootor, millest saab sujuvalt muuta mängu kiirust.

Roboti ehitus

NXT külge peab olema mugaval viisil ühendatud mootor koos rattaga, mille abil saab väravat juhtida. Ühel NXT-l peab olema lisatud ka teine mootor, mille abil saab muuta mängu kiirust.

Programmeerimine

Kummagi NXT jaoks on omaette programm, kuid neil on ka mõned ühised programmiosad. Nagu näiteks Bluetooth ühenduse loomine, värava liigutamine ja info kuvamine ekraanil. Seetõttu koosneb antud mängu programm neljast eraldi failist.

1)       NXT 1 ehk mängija 1 programm master_gamer.nxc

2)       NXT 2 ehk mängija 2 programm slave_gamer.nxc

3)       Mõlema NXT jaoks ühised funktsioonid CommonFunctions.nxc

4)       Bluetooth ühenduse programm BluetoothCheck.nxc

Iga fail on jagatud omakorda alametappideks, et lihtsustada arendust ja mängu toimimise põhimõttest arusaamist. Bluetooth ühenduse programmi pole siinkohal eraldi välja toodud ega kirjeldatud, kuna tegemist on standardse ühenduse loomisega, mis on kirjeldatud käesoleva raamatu juhendi Bluetoothi alapeatükis.

Mängija 1 programm ehk master_gamer.nxc fail koosneb järgmistest peamistest funktsioonidest:

1)       Bluetooth ühenduse loomine kahe NXT vahel

2)       Mängu loogika GameLoop

3)       Palli põrkumise ja võitja tuvastamise funktsioon BallCollide

4)       Palli liigutamise protseduur MoveBall

5)       Mängu kiiruse juhtimine ChangeGameSpeed

6)       Bluetooth info saatmine/vastuvõtmine SendReceiveBluetoothInfo

7)       Mängu alginfo initsialiseerimine InitData

8)       Mängu võitja kuvamine ShowWinner

Mängija 2 programm ehk slave_gamer.nxc fail koosneb järgmistest peamistest funktsioonidest:

1)       Bluetooth ühenduse loomine kahe NXT vahel

2)       Mängu loogika GameLoop

3)       Bluetooth info saatmine/vastuvõtmine SendReceiveBluetoothInfo

Ühised funktsioonid ehk CommonFunctions.nxc fail koosneb järgmistest funktsioonidest

1)       Värava liigutamise funktsioon PlayerPosition

2)       Ekraanile kuvamise funktsioon Render

 

clip_image002

Mängija 1 mängu loogika

Mängu käivitamisel kontrollitakse Bluetoothi kaudu teise mängija olemasolu. Kui ka teine mängija on mängu käima pannud, saadetakse teisele mängijale info palli suuruse BallWidth ja värava laiuse GateWidth kohta ja algväärtustatakse mängu parameetrid protseduuriga InitData.  Seejärel pannakse käima mäng protseduuriga GameLoop ja kui mängus tekib võitja, kuvatakse tulemus ekraanil funktsiooniga ShowWinner.

Protseduur GameLoop käib while-tsükli sees seni, kuni tingimus on tõene while(!Winner), ehk kuni võitjat ei ole selgunud. Winner muutuja on vaikimisi võrdne nulliga. Kui tekib võitja, on selle väärtus vastavalt 1 või 2, sõltuvalt võitjast.

Protseduuri GameLoop sees omistatakse muutujale Winner võitja väärtus, mis on palli põrkamise ja võitja tuvastamise funktsiooni BallCollide tagastatav väärtus. Pärast seda käivitatakse palli liigutamise protseduur MoveBall, mille ülesandeks on uute palli koordinaatide omistamine globaalsele muutujale BallPos. Seejärel käivitatakse funktsioon PlayerPosition(GateWidth), mis liigutab väravat ja tagastab väärtuse mängija 1 värava koordinaadiga, mis omistatakse muutujale Player1Pos. Funktsiooni sisendparameeter on värava laius.

Pärast seda omistatakse muutujale GameData kolm erinevat väärtust, palli x ja y koordinaadid ning mängija 1 värava koordinaat. Kuna Bluetooth on kahe NXT vahel võrdlemisi aeglane, siis eelnimetatud parameetrid pannakse kokku üheks muutujaks GameData, et minimeerida Bluetooth ühenduste arvu kahe NXT vahel. Et mängija 2 NXT programmis oleks andmeid lihtsam „lahti võtta“, on koordinaatide x ja y vahele pandud eraldajana kaldkriips ning värava koordinaadi ette sidekriips.

Seejärel käivitatakse funktsioon SendReceiveBluetoothInfo(GameData), mis saadab äsja loodud faili teisele NXT-le ja tagastab väärtuse mängija 2 värava koordinaadiga, mis omistatakse muutujale Player2Pos. Funktsiooni sisendparameeter on GameData.

Viimasena käivitatakse protseduur Render(Player1Pos, Player2Pos, BallPos, GateWidth, BallWidth), mille ülesanne on joonistada ekraanile mõlema värava asukohad ja pall. Selle sisendparameetriteks on esimese ja teise mängija väravate koordinaadid, palli koordinaadid ning palli ja värava laiused.

Seejärel oodatakse 40 ms, mis on ühtlasi ka mängu kiiruseks, ning kogu tsükkel algab uuesti algusest, kuni võitjat ei ole selgunud.

Näide. Protseduur GameLoop ehk mänguloogika

void GameLoop()

{

  //muutujasse Winner salvestatakse mängu staatus

  Winner = BallCollide();

  //liigutatakse palli

  MoveBall();

  //CommonFunctions failist käivitatakse funktsioon

  //mis liigutab player1 värava asukohta, sisendiks värava laius

  Player1Pos = PlayerPosition(GateWidth);

  //luuakse string kolmest parameetrist: palli x ja y

  //koordinaadid ning player1 mängija värava koordinaat

  GameData = StrCat(NumToStr(BallPos.x), "/",

            NumToStr(BallPos.y), "-",

            NumToStr(Player1Pos));

  //vahetatakse Bluetooth infot, edastatakse mängu info

  //tagasi saadakse player2 mängija värava koordinaat

  Player2Pos = SendReceiveBluetoothInfo(GameData);

  //CommonFunctions failist käivitatakse palli ja

  //väravate kuvamise protseduur

  Render(Player1Pos, Player2Pos,BallPos, GateWidth, BallWidth);

  Wait(MS_40);  //oodatakse 40ms, mängu kiirus

}

Mängija 1 palli põrkumine ja võitja tuvastamine

clip_image004Funktsioon BallCollide kannab hoolt selle eest, et pall saaks alati ekraani servadest tagasi põrgatatud ning tuvastatud väravate olemasolu ja palli põrkumine vastu väravat. See funktsioon tagastab võitja numbri. Kui võitjat ei ole, tagastab see funktsioon alati nulli. Kui võitja on mängija 1, tagastab funktsioon vastavalt ühe ja kui võitja on mängija 2, siis kahe.

Käesolevas projektis toimub palli põrkamise kontrollimine täpselt samal viisil, nagu projektis Ping-Pong ühele. Tingimus (BallPos.y > DISPLAY_HEIGHT – BallWidth) saab tõeseks siis, kui pall on jõudnud ekraani ülemisse serva. Kuna pallil on teatud suurus, tuleb kontrollimiseks lahutada palli suurus ekraani kõrgusest. Kui seda mitte teha, kontrollitaks palli alumist serva ekraani kõrgusega ja siis on visuaalselt enamus palli ekraani taha kadunud. Ka parema serva vastu põrkumist kontrollitakse tingimusega (BallPos.x > DISPLAY_WIDTH - BallWidth),kus lahutatakse ekraani laiusest palli laius, et pall ei kaoks põrkumisel ekraani serva taha. Vaata ülemise ja alumise põrke kohta joonist.

Samal moel toimub ka palli kontrollimine vastu alumist (BallPos.y <= 0) ja vasakut serva (BallPos.x <= 2), need on lihtsamad kontrollid, kuna koordinaadid on nii pallil kui ka ekraanil samas kohas. Vasaku serva korral kontrollitakse palli x-koordinaati number 2 suhtes, et jätta väravale 1 piksel ruumi liikumiseks.

 

clip_image006

Vasaku ja parema serva kontrollile lisandub alati ka järgmine kontroll, palli võrdlus värava koordinaadiga (BallPos.y < Player1Pos + GateWidth && BallPos.y > Player1Pos - BallWidth). Palli koordinaate kontrollitakse värava ülemise ja alumise serva suhtes, kuid alumise serva kontrolli korral lahutatakse palli laius, et pall saaks ka ülemise nurgaga väravalt tagasi põrgata. Kui seda lahutamistehet ei kasutaks, oleks pall väravas isegi juhul, kui pool palli on värava kohal. Vaata selle põrke kohta allolevat joonist.

Joonisel on kujutatud kaks palli positsiooni, üks kujutab seda kui pall põrkub vastu ülemist värava serva ja teine, kui palll põrkub vastu alumist värava serva. Mõlemal juhul on tegemist palli nö. viimase piksli põrkumisega.

Kui värava tingimuse kontroll pole täidetud, siis kaotas mängija, kelle poolelt pall välja kukkus, ning funktsioon tagastab võitnud mängija numbri:  return 2.

Peale palli põrkumist muudetakse palli liikumise suund ja omistatakse talle uus koordinaat. Uus koordinaat on ühe piksli võrra suunatud ekraani keskele, et järgmise tsükli käigus ei tuvastataks palli jätkuvalt seina vastas olevaks.

Kui pall on põrganud alumiselt või ülemiselt servalt, siis muudetakse palli liikumise nurk vastupidiseks. Kui enne oli 60 kraadi, siis peale põrkumist on -60 kraadi. Kui aga põrkumine on toimunud parema-vasaku külje suunas, lahutatakse palli põrkumise nurk 180-st. Saadud numbrit kasutatakse palli liikumise protseduuri juures ning selle tehte vajaduse selgitamine toimub seal.

clip_image008

Näide. Palli põrkumine ja võitja tuvastamine

//palli põrkumise funktsioon, tagastab väärtused 0, 1, 2

short BallCollide()

{

  //pall põrkub vastu ülemist serva

  if(BallPos.y > DISPLAY_HEIGHT - BallWidth)

  {

    BallAngleDeg = -BallAngleDeg;

    BallPos.y = DISPLAY_HEIGHT - BallWidth - 1;

  }

  //pall põrkub vastu alumist serva

  else if(BallPos.y <= 0)

  {

    BallAngleDeg = -BallAngleDeg;

    BallPos.y = BallWidth;

  }

  //pall põrkub vastu vasakut serva (Player1)

  if(BallPos.x <= 2)

  {

    //kontrollitakse, kas palli põrkekohal on värav ees

    if (BallPos.y < Player1Pos + GateWidth &&

        BallPos.y > Player1Pos - BallWidth)

    {

      BallAngleDeg = 180 - BallAngleDeg;

      BallPos.x = BallWidth;

    }

    //kui väravat ees ei olnud, siis on mäng läbi

    else

    {

      return 2; //võitis mängija 2

    }

  }

  //pall põrkub vastu paremat serva (Player2)

  else if(BallPos.x > DISPLAY_WIDTH - BallWidth)

  {

    //kontrollitakse, kas palli põrkekohal on värav ees

    if(BallPos.y < Player2Pos + GateWidth &&

      BallPos.y > Player2Pos - BallWidth)

    {

      BallAngleDeg = 180 - BallAngleDeg;

      BallPos.x = DISPLAY_WIDTH - BallWidth - 1;

    }

    //kui väravat ees ei olnud, siis on mäng läbi

    else

    {

      return 1; //võitis mängija 1

    }

  }

  return 0;  //mäng jätkub

}

Mängija 1 pallile uute koordinaatide andmine

Protseduur MoveBall on programmi üks lühemaid, kuid vaatamata sellele kõige olulisem palli korrektse liikumise seisukohast. Antud protseduuri tulemuseks on palli x ja y koordinaadid.

Alguses kutsutakse välja funktsioon ChangeGameSpeed, mille abil saab jooksvalt mängu kiirust muuta. ChangeGameSpeed tagastab ujukomaarvu vahemikus 0,1 kuni lõpmatus. Algne palli liikumise kiirus on 1.

Pall pannakse liikuma täisnurkse kolmnurga siinus ja koosinus funktsioone kasutades. X-telje koordinaat ehk lähiskaatet saadakse koosinuse abil ning y-telje koordinaat ehk vastaskaatet siinuse abil. Vaadates alltoodud valemeid, on hüpotenuus siin programmis kasutusel kiiruse muutujana nimega Speed, mis mängu alguses on võrdne ühega.

Tuletame siinkohal meelde vajalikud valemid.

Täisnurkse kolmnurga lähiskaateti pikkuse arvutamine teravnurga ning hüpotenuusi abil

clip_image010

Täisnurkse kolmnurga vastaskaateti pikkuse arvutamine teravnurga ning hüpotenuusi abil

clip_image012

Valemis toodud tähis teravnurk vastab programmis muutujale BallAngleDeg ehk see on suurus, mis saadakse mängu alguses juhusliku valiku teel ning võib olla vahemikus -60..60 (pall liigub paremale) ja 120..240 (pall liigub vasakule). Need vahemikud tulenevad mängu parameetrite algväärtustamisest ja sellised suurused on valitud seetõttu, et välistada mängujuhuste tekkimine, kus pall hakkab otse üles-alla põrkama ja ei jõuagi seeläbi mängija väravani.

Järgnev joonis näitab, kuidas mängu alghetkel liikumissuunda määrav koordinaatteljestik ekraani suhtes paikneb. Viirutatud alad on need, millistes suundades pall kunagi ei stardi. Palli liikumist toetav joonis on toodud edaspidi, pärast kirjeldusi.

clip_image014

Palli liikumine sirgjooneliselt

Võtame eelduseks, et mängu alguses startis pall keskelt ja suunaga 60 kraadi, seega üles-paremale täpselt punase viirutatud ala servas. Esimese tsükli käigus

x-koordinaat =1 * cos(60) = 0,5 ja

y-koordinaat = 1 * sin(60) = 0,86

ning kuna palli liikumise kiirus on alguses 1, siis jätame selle edaspidi kirjutamata. Ekraani keskkoht on 50x32 pikslit, seega tegelikult on pärast esimest tsüklit x=50,5 ja y=32,86.

Järgmise tsükli käigus (ehk 40 ms pärast) liidetakse kummalegi väärtusele juurde uuesti 0,5 ja 0,86 ja siis on tulemuseks:

x=51

y=33,52

Kuna ekraani koordinaadid on täisarvud, siis programm ümardab eelnimetatud numbrid täisarvudeks ja pall kuvatakse ekraanil koordinaatidega x= 51 ja y= 34.

Palli põrkumine ekraani ülemisest servast

Jätkates eelmist rida, saame ekraani serva jõudnud palli koordinaatideks:

x=68,5

y=64

Nüüd rakendub tehe, mis muudab palli liikumise nurga märgi vastupidiseks -BallAngleDeg = -60. Järgmise tsükli käigus on palli koordinaatide arvutamise nurgaparameetriks -60. Arvutades selle numbriga pallile järgmised koordinaadid, saame:

x=x+cos(-60)=68,5+0,5=69

y=y+sin(-60)=64+(-0,86)=63,13

Seega näeme, et x koordinaat suureneb jätkuvalt, aga y hakkab vähenema. See tähendab, et pall liigub nüüd alla paremale.

Palli põrkumine ekraani paremast servast

Jätkates eelmist rida, tuleb meil arvutada, kas pall jõuab enne alla või põrkub vastu paremat seina. Kuna eelmise näite lõpus oli pall 68,5 piksli kaugusel, jääb sealt ekraani parema servani 31,5 pikslit. Võttes selle aluseks x-telje suunalise kolmnurga küljepikkusena, saame välja arvutada, kui kaugele alla pall selle jooksul jõuab. Tulemus on x=100 (ekraani parem serv) ja y=9,5. Seega põrkub pall enne alumise servani jõudmist vastu paremat serva. Ekraani kõrgus on 64 pikslit, st. pall jõuab paremasse serva 9 piksli juures. Nüüd tuleb palli põrkamise hetk, seega 180-st lahutatakse palli nurk 180-(-60)=240. Pannes selle numbri palli asukoha koordinaatide arvutamise tehtesse, saame:

x=x+cos(240)=100-0,5=99,5

y=y+sin(240)=9,5+(-0,86)=8,6.

Seega saame numbrite põhjal öelda, et pall liigub nüüd jätkuvalt alla, kuid on vahetanud x-teljel suunda ja liigub vasakule.

Palli põrkumine ekraani alumisest servast

Palli x-telje koordinaat on 100 ja y koordinaat 9,5, seega ekraani alumise servani on 9 pikslit. Arvutades välja ekraani allserva jõudva palli x koordinaadi, saame x=94,5 kui y=0, st. pall on alla jõudnud. Nüüd on taas märgimuutmise arvutustehe ja uueks nurga väärtuseks on -240. Kuna positiivseid arvusid on lihtsam mõista, siis võime öelda ka et -240 kraadi = 120 kraadi. Vaata jooniselt.

Palli koordinaadid on:

x=x+cos(120)=94,5+(-0,5)=94

y=y+sin(120)=0+0,86=0,86

Tulemus näitab, et x väheneb ja y suureneb, seega pall liigub üles vasakule.

Sarnase arvutuse kohaselt saab leida ka liikumise pärast vasakpoolse seina vastu põrkumist.

clip_image016

Näide. palli liikumise ja nurga arvutamine

void MoveBall()

{

  //esmalt käivitatakse funktsioon, mille vahendusel

  //saab kiiruse, ehk kolmnurga hüpotenuusi

  Speed = ChangeGameSpeed();

  //arvutatakse x-telje koordinaat

  BallPos.x += cosd(BallAngleDeg) * Speed;

  //arvutatakse y-telje koordinaat

  BallPos.y += sind(BallAngleDeg) * Speed;

}

Mängija 1. Mängu kiiruse juhtimine

Kui mäng tundub liiga lihtne, saab jooksvalt mängu käigus muuta palli liikumise kiirust. Funktsioon ChangeGameSpeed kasutab mängu alguses initsialiseeritud mootor B väärtust MootorCenterB, seda kasutatakse mootori algse positsiooni määramiseks. Sellest lahutatakse mootori hetke pöörded, mis on mängu alguses sama kui algne positsioon, seega tulemus on null. Liidetakse juurde 100, et anda mängule esmane baaskiirus.

Kui keerata mootorit kiiruse vähendamise suunas, siis järgnev if-lause ei võimalda muutujal Kiirus minna väiksemaks kui 10. Enne tagastamist jagatakse väärtus 100-ga, et mootori pööramisel oleks kiiruse muutuse vähim samm 0,1 ühikut return Kiirus/100.

Näide. Mängu kiiruse muutmise funktsioon

float ChangeGameSpeed()

{

  long Mootor;

  float Kiirus;

  Mootor = MotorRotationCount(OUT_B);

  //muutuja Kiirus arvestab mootori algset asukohta

  //+100 liidetakse selleks, et anda mängule baaskiirus

  Kiirus = MootorCenterB - Mootor + 100;

  if (Kiirus <= 10)

    {

      MootorCenterB = Mootor;

      Kiirus = 10;

    }

  //tagastatav kiirus jagatakse 100-ga,

  //et kiiruse muutus oleks võimalikult väike

  return Kiirus/100;

}

Mängija 1. Bluetooth info saatmine/vastuvõtmine

Funktsiooni SendReceiveBluetoothInfo sisendiks on tekstitüüpi muutuja Data, milles sisaldub nii palli kui ka värava koordinaatide info. Funktsioon tagastab vastasmängija värava koordinaadi.

Saadetav andmestik edastatakse teisele NXT-le käsuga SendRemoteString(BTconn, MAILBOX5, Data).  Saatmine toimub aga alles pärast seda, kui ollakse mängija 2 käest korrektselt vastu võtnud värava koordinaatide info. Vastuvõtmise käsk on while-tsükli sees, mis käib seni, kuni tingimus on tõene. Tingimuseks on Bluetoothi kaudu info lugemise käsu ReceiveRemoteNumber tagastatav väärtus null (parametriseeritult ka kui NO_ERR) lugemise õnnestumise korral. Seni, kuni postkastis pole midagi, tagastab ReceiveRemoteNumber väärtuse, mis on nullist erinev (näiteks 64 tähistab tühja postkasti). Seejärel kontrollitakse tingimust 64 != 0, mis on tõene, sest 64 ju ei võrdu nulliga. Kui aga info saadakse, on tingimuseks 0 != 0, mis on väär, sest 0 on võrdne 0-ga. Selle tulemuse saamisel oleme kindlad, et vastasmängija värava koordinaat saadi kätte ning programmikood suundub järgmist käsku täitma.

Näide. Bluetooth info saatmise/vastuvõtmise funktsioon

int SendReceiveBluetoothInfo(string Data)

{

  //MASTER kirjutab ja loeb alati SLAVE mailboxidest

  //saadame info stringina, et kogu pakett korraga saata

  //seeläbi on BT ühendusi vähem ja mäng kiirem

  int PlayerPos;

  //ootab, kuni saab korrektselt teise mängija väravainfo

  while(ReceiveRemoteNumber

        (MAILBOX4, TRUE, PlayerPos)!=NO_ERR);

  //saadab oma värava ja palli info teisele poole

  SendRemoteString(BTconn, MAILBOX5, Data);

  //tagastab teise mängija värava koordinaadi

  return PlayerPos;

}

Mängija 1. Mängu alginfo initsialiseerimine

Protseduur InitData käivitatakse iga kord enne mängu käivitumist. Esmalt algväärtustatakse palli ja väravate koordinaadid, mis sätitakse alati keskele. Mootorite pöörded salvestatakse muutujatesse, et neid hiljem kasutada värava ja kiiruse juhtimiseks.

Seejärel luuakse muutuja RandomStart millele omistatakse juhuslik väärtus vahemikus 0..119 Random(120).  Pärast seda tehakse kaks uut muutujat RandomRight ja RandomLeft, mis omakorda väljendavad kraadide vahemikku paremale-vasakule, kuhu pall võib alustades startida. Vaata selle kohta selgitavat joonist käesolevas projektis, kus punasega märgitud ala viitab piirkondadele, kuhu pall ei saa startida. Seejärel tehakse taas juhuslikkuse alusel valik, kas pall stardib paremale või vasakule. Seda valik toimub ternary-tingimusega,  mille kontrolliks on käsk Random(2) väärtustega 0 või 1 ja vastavalt sellele otsustab programm, kas valida parem või vasak.

Viimase tegevusena saadetakse vastasmängijale info „RESET“, mida teise mängija programm käsitleb kui mängu alustamise käsklus.

Näide. Mängu andmete algväärtustamine

void InitData()

{

  Winner = 0;

  BallPos.x = DISPLAY_WIDTH/2;

  BallPos.y = DISPLAY_HEIGHT/2;

  Player1Pos = DISPLAY_HEIGHT/2 - GateWidth/2;

  Player2Pos = DISPLAY_HEIGHT/2 - GateWidth/2;

  MootorCenter=MotorRotationCount(OUT_A);

  MootorCenterB=MotorRotationCount(OUT_B);

  //luuakse juhuslik arv vahemikus 0..119

  int RandomStart = Random(120);

  //juhulikust arvust tehakse vahemik -60..60,

  //see vastab ekraani parempoolsele suunale

  int RandomRight = RandomStart - 60;

  //juhulikust arvust tehakse vahemik 120..240

  //see vastab ekraani vasakpoolsele suunale

  int RandomLeft = RandomStart + 120;

  //valitakse juhuslikult, kas alustada parem-vasakule

  BallAngleDeg = Random(2) ? RandomRight : RandomLeft;

  //saadetakse Player2-le info mängu alsutamise kohta

  SendRemoteString(BTconn, MAILBOX6, "RESET");

}

Mängija 1. Mängu võitja kuvamine

clip_image018Protseduur ShowWinner käivitatakse pärast seda, kui funktsioon BallCollide tagastab väärtuse 1 või 2, mille järel väljutakse mängu käigus hoidvast while-tsüklist, kuna muutuja Winner omab nullist erinevat väärtust.

ShowWinner kontrollib muutujat Winner. Kui see on võrdne 1-ga, saadetakse mängija 2 postkasti teade, et ta kaotas ja iseendale kuvatakse teade „Sa võitsid“ ning „Vajuta nuppu jätkamiseks“. Vastupidise võidu korral saadetakse mängijale 2 teade „Sa võitsid“ ning iseendale kuvatakse teade „Sa kaotasid“ ning „Vajuta nuppu jätkamiseks“.

Näide. Mängu võitja kuvamise protseduur

void ShowWinner()

{

  string Win = "Sa voitsid";

  string Lose = "Sa kaotasid";

 

  //kui võitja oli Player1

  if(Winner==1)

  {

    //saadetakse Player2-le teade kaotuse kohta

    SendRemoteString(BTconn, MAILBOX6, Lose);

    TextOut(10, LCD_LINE4, Win);

    TextOut(10, LCD_LINE6, "Vajuta nuppu");

    TextOut(10, LCD_LINE7, "jatkamiseks");

  }

  //kui võitja oli Player2

  else if(Winner==2)

  {

    //saadetakse Player2-le teade võidu kohta

    SendRemoteString(BTconn, MAILBOX6, Win);

    TextOut(10, LCD_LINE6, "Vajuta nuppu");

    TextOut(10, LCD_LINE7, "jatkamiseks");

    TextOut(10, LCD_LINE4, Lose);

  }

  while(!ButtonPressed(BTNCENTER, FALSE));

  while(ButtonPressed(BTNCENTER, FALSE));

}

Mängija 2 mängu loogika

Mängu käivitamisel kontrollitakse, kas Bluetooth töötab. Seejärel oodatakse mängija 1 NXT käest tegevuse sünkroniseerimise teadet Handshake. Kui see on kätte saadud, edastatakse teade OK.

Pärast seda jääb mängija 2 programm ootama while(!BallWidth || !GateWidth), kuni on kätte saanud palli ja värava suuruse info. Antud while-tsükli tingimuses kontrollitakse palli ja värava infot. Mõlemad on alguses võrdsed nulliga, kuid tingimuse kontrollis muudetakse hüüumärgiga nende väärtus vastupidiseks ehk üheks. Seega tingimuse kontroll näeb välja selline: „kas 1 või 1 on võrdne 1-ga“. See on ilmselgelt tõene ja tsükkel käivitatakse. Kui aga üks neist on väärtuse saanud, näeb kontroll välja järgmine „kas 1 või 0 on võrdne 1-ga“, mis on samuti tõene. Kui aga mõlemad muutujad on saanud väärtuse, on kontroll stiilis „kas 0 või 0 on võrdne 1-ga“, mis muidugi pole tõene ja väljutakse tsüklist.

Seejärel käivitub lõpmatult mängu loogika protseduur GameLoop. Esmalt käivitatakse funktsioon PlayerPosition(GateWidth), mis liigutab väravat ja tagastab väärtuse mängija 2 värava koordinaadiga, mis omistatakse muutujale Player2Pos. Sisendparameetriks on värava laius. Pärast seda käivitatakse Bluetooth postkastidest lugemise funktsioon SendReceiveBluetoothInfo. Selle sisendparameetriks on mängija 2 värava koordinaat, muutuja Player2Pos, mis edastatakse mängijale 1. Funktsioon tagastab väärtuse palli ja mängija 1 värava koordinaatidega ja omistab selle muutujale GameData.

Muutujast GameData saadakse stringitöötlusega kätte kõik koordinaadid. Esiteks omistatakse muutujale t kaldkriipsu asukoha väärtus antud stringi sees. Järgmise käsuga SubStr(GameData, 0, t) loetakse muutujast GameData kõik kohad kuni kaldkriipsuni ja tulemus salvestatakse palli x-koordinaadiks. Järgmisele muutujale u omistatakse sidekriipsu asukoha väärtus. Käsuga SubStr(GameData, t+1, u-t) loetakse muutjast GameData kõik kohad alates kaldkriipsu positsioonist kuni sidekriipsuni. Parameeter t+1 tähendab seda, et lugemist alustatakse üks koht pärast kaldkriipsu ning parameeter u-t väljendab loetava stringi pikkust. Värava koordinaat saadakse kätte samuti käsuga SubStr(GameData, u+1, v-u), kuid muutuja v tähistab siin kogu stringi pikkust.

Pärast seda käivitatakse protseduur Render(Player1Pos, Player2Pos, BallPos, GateWidth, BallWidth), mille ülesanne on joonistada ekraanile mõlema värava asukohad ja pall. Selle sisendparameetriteks on esimese ja teise mängija väravate koordinaadid, pallikoordinaadid ja palli ning värava laiused.

Viimase käsuna kuvatakse ekraanil võitja info, kuid alles seejärel, kui üks või teine pool võitis.

Näide. Mängu loogika

void GameLoop()

{

  //CommonFunctions failist käivitatakse funktsioon,

  //mis liigutab värava asukohta, sisendiks värava laius

  Player2Pos = PlayerPosition(GateWidth);

  //vahetatakse Bluetooth infot, edastatakse värava info

  //tagasi saadakse player1 värava ja palli koordinaadid

  GameData = SendReceiveBluetoothInfo(Player2Pos);

 

  //stringitöötluse abil taastatakse

  //värava ja palli koordinaadid 

  int t = Pos("/", GameData);

  BallPos.x = StrToNum(SubStr(GameData, 0, t));

  int u = Pos("-", GameData);

  BallPos.y = StrToNum(SubStr(GameData, t+1, u-t));

  int v = StrLen(GameData);

  Player1Pos = StrToNum(SubStr(GameData, u+1, v-u));

 

  //CommonFunctions failist käivitatakse palli ja

  //väravate kuvamise protseduur

  Render(Player1Pos, Player2Pos,

        BallPos, GateWidth, BallWidth);

  //võitja info kuvatakse ekraanil

  TextOut(10, LCD_LINE4, ShowWinner);

 

  Wait(40);

}

Mängija 2. Bluetooth info saatmine/vastuvõtmine

Funktsiooni SendReceiveBluetoothInfo sisendparameetriks on mängija 2 positsioon, mis saadetakse mängijale 1 ning see tagastab väärtuseks Data, milles sisalduvad palli ja mängija 1 värava koordinaadid.

Postkastist 6 loetakse võitja info. Kui antud postkastis sisaldub info, kontrollitakse tingimust TempShowWinner != "" ja sõnumi sisu kuvatakse mängijale. Kui aga sealt saabub info „RESET“, ei kuvata ekraanil mängijale ühtegi sõnumit.

Näide. Bluetooth info saatmine/vastuvõtmine

string SendReceiveBluetoothInfo(int PlayerPos)

{

  string Data;

  //slave loeb oma lokaalsest mailboxist

  //palli ja värava koordinaadid

  ReceiveRemoteString(MAILBOX5, FALSE, Data);

  //SLAVE kirjutab oma värava koordinaadi MAILBOX4-a

  SendResponseNumber(MAILBOX4, PlayerPos);

  string TempShowWinner = "";

  //SLAVE loeb mailbox6-st võitja info

  ReceiveRemoteString(MAILBOX6, TRUE, TempShowWinner);

  //kui saadi võitja info, kuvatakse see ekraanil

  if(TempShowWinner != "")

    ShowWinner = TempShowWinner;

  //kui saadi nullimise info, siis

  //ei kuvata võitja infot ekraanil

  if(TempShowWinner == "RESET")

    ShowWinner = "";

  return Data;

}

Ühised funktsioonid, CommonFunctions

Funktsioon PlayerPos liigutab väravat, selle sisendparameetriks on värava laius ning see tagastab väärtuse värava koordinaat. Selle funktsiooni kirjeldus on esitatud ping-pong ühele mängu juures, seega siin seda enam kordama ei hakata.

Protseduuri Render kasutatakse selleks, et ekraanile joonistada pall ja väravad. Sisendparameetriteks on palli ja väravate koordinaadid ning palli ja värava laiused. Nii pall kui väravad joonistatakse ekraanile nelinurkadena, funktsiooni RectOut abil. Kui selle funktsiooni laiuseks on 0, siis tulemuseks joonistatakse 1 piksli laiune ristkülik.

Näide. Fail CommonFunctions.NXC

int MootorCenter;

 

//funktsioon PlayerPosition liigutab väravat

//antud funktsiooni sisendiks on värava laius ning

//tagastatav väärtus on värava koordinaat

int PlayerPosition(byte Gate)

{

  int PlayerPos;

  //pöörded jagatakse kahega,

  //et värava liikumine poleks nii tundlik

  int Mootor=MotorRotationCount(OUT_A)/2;

  //algsest mootori väärtusest lahutatakse mootoripöörded

  //pööretele liidetakse pool ekraani kõrgusest,

  //et mängu alguses asuks värav keskel

  PlayerPos = (MootorCenter-Mootor)+DISPLAY_HEIGHT/2;

 

  //kui värav on jõudnud ekraani ulatusest välja

  //omistatakse mootori algväärtusele uus suurus

  //värav jõuab ekraani alumisest serva juurest välja

  if (PlayerPos<=0)

  {

    MootorCenter = Mootor-DISPLAY_HEIGHT/2;

    PlayerPos = 1;

  }

  //värav jõuab ekraani ülemise serva juurest välja

  if (PlayerPos>=DISPLAY_HEIGHT-Gate)

  {

    MootorCenter = Mootor+DISPLAY_HEIGHT/2-Gate;

    PlayerPos = DISPLAY_HEIGHT-Gate;

  }

  return PlayerPos;

}

 

//struktuurne muutuja palli koordinaatide hoidmiseks

struct Position

{

  float x;

  float y;

};

//ekraanil kuvamise protseduur, sisendiks on

//*mängijate värava koordinaadid

//*palli koordianaadid

//*palli ja värava suurused

void Render(int PlayerPosition1,

      int PlayerPosition2, Position BallPos,

      byte Gate, byte Ball)

{

  ClearScreen();

  //ekraanile joonistatakse player1 värav

  RectOut(0, PlayerPosition1, 0, Gate);

  //ekraanile joonistatakse player2 värav

  RectOut(DISPLAY_WIDTH - 1, PlayerPosition2, 0, Gate);

  //ekraanile joonistatakse pall

  RectOut(BallPos.x, BallPos.y, Ball, Ball);

}

Mängija 1 programmikood

Mängija 1 jaoks kirjeldatud funktsioonid ja protseduurid kokku panduna annab esimese mängija NXT programmikoodi. See NXT on mängu juht, esimängija, info edastaja ja Bluetooth master.

Näide. Mängija 1 programmikood

//Bluetooth mäng, MASTER NXT, Mängija 1

 

#include "BluetoothCheck.nxc"

#include "CommonFunctions.nxc"

 

const byte BTconn=1;

const string SlaveNXTName = "NXT2";

const byte GateWidth = 10;  //värava laius

const byte BallWidth = 3;   //palli suurus

 

int Player1Pos;     //värava koordinaat

int Player2Pos;     //värava koordinaat

int BallAngleDeg;   //palli põrkenurk

float Speed;        //palli põrkekiirus

string GameData;    //paarilisele saadetavad andmed

Position BallPos;   //palli positsioon

 

//0: no winner, 1: winner Player1, 2: winner Player2

byte Winner;

long MootorCenterB; //mängu kiiruse muutmise mootor

 

//SendReceiveBluetoothInfo saadab ja võtab vastu infot

//funktsioni sisendiks on palli ja värava info

//mis edastatakse teisele NXT-le

//funktsioon tagastab teise mängija värava koordinaadi

int SendReceiveBluetoothInfo(string Data)

{

  //MASTER kirjutab ja loeb alati SLAVE mailboxidest

  //saadame info stringina, et saata kogu pakett korraga

  //seeläbi on BT ühendusi vähem ja mäng kiirem

  int PlayerPos;

  //ootab, kuni saab korrektselt teise mängija väravainfo

  while(ReceiveRemoteNumber

        (MAILBOX4, TRUE, PlayerPos)!=NO_ERR);

  //saadab oma värava ja palli info teisele poole

  SendRemoteString(BTconn, MAILBOX5, Data);

  //tagastab teise mängija värava koordinaadi

  return PlayerPos;

}

 

//mängu kiiruse muutmise funktsioon

float ChangeGameSpeed()

{

  long Mootor;

  float Kiirus;

  Mootor = MotorRotationCount(OUT_B);

  //muutuja Kiirus arvestab mootori algset asukohta

  //+100 liidetakse selleks, et anda mängule baaskiirus

  Kiirus = MootorCenterB - Mootor + 100;

  if (Kiirus <= 10)

    {

      MootorCenterB = Mootor;

      Kiirus = 10;

    }

  //tagastatav kiirus jagatakse 100-ga,

  //et kiiruse muutus oleks võimalikult väike

  return Kiirus/100;

}

 

//palli liikumise ja nurga arvutamine

void MoveBall()

{

  //esmalt käivitatakse funktsioon, mille vahendusel

  //saab kiiruse, ehk kolmnurga hüpotenuusi

  Speed = ChangeGameSpeed();

  //arvutatakse x-telje koordinaat

  BallPos.x += cosd(BallAngleDeg) * Speed;

  //arvutatakse y-telje koordinaat

  BallPos.y += sind(BallAngleDeg) * Speed;

}

 

//palli põrkamise funktsioon, tagastab väärtused 0, 1, 2

//0: mäng jätkub, 1: Player1 võitis, 2: Player2 võitis

short BallCollide()

{

  //pall põrkab vastu ülemist serva

  if(BallPos.y > DISPLAY_HEIGHT - BallWidth)

  {

    BallAngleDeg = -BallAngleDeg;

    BallPos.y = DISPLAY_HEIGHT - BallWidth - 1;

  }

  //pall põrkub vastu alumist serva

  else if(BallPos.y <= 0)

  {

    BallAngleDeg = -BallAngleDeg;

    BallPos.y = BallWidth;

  }

  //pall põrkab vastu vasakut serva (Player1)

  if(BallPos.x <= 2)

  {

    //kontrollitakse, kas palli põrkekohal on värav ees

    if (BallPos.y < Player1Pos + GateWidth &&

        BallPos.y > Player1Pos - BallWidth)

    {

      BallAngleDeg = 180 - BallAngleDeg;

      BallPos.x = BallWidth;

    }

    //kui väravat ees ei olnud, siis on mäng läbi

    else

    {

      //võitis mängija 2

      return 2;

    }

  }

  //pall põrkab vastu paremat serva (Player2)

  else if(BallPos.x > DISPLAY_WIDTH - BallWidth)

  {

    //kontrollitakse, kas palli põrkekohal on värav ees

    if(BallPos.y < Player2Pos + GateWidth &&

        BallPos.y > Player2Pos - BallWidth)

    {

      BallAngleDeg = 180 - BallAngleDeg;

      BallPos.x = DISPLAY_WIDTH - BallWidth - 1;

    }

    //kui väravat ees ei olnud, siis on mäng läbi

    else

    {

      //võitis mängija 1

      return 1;

    }

  }

  //mäng jätkub

  return 0;

}

 

//mängu funktsioonide väljakutsumine

void GameLoop()

{

  //muutujasse Winner salvestatakse mängu staatus

  Winner = BallCollide();

  //liigutatakse palli

  MoveBall();

  //CommonFunctions failist käivitatakse funktsioon,

  //mis liigutab värava asukohta, sisendiks värava laius

  Player1Pos = PlayerPosition(GateWidth);

  //luuakse string kolmest parameetrist:

  //palli x ja y koordinaadid ning player1 värava koordinaat

  GameData = StrCat(NumToStr(BallPos.x), "/",

            NumToStr(BallPos.y), "-",

            NumToStr(Player1Pos));

  //vahetatakse bluetooth infot, edastatakse mängu info

  //tagasi saadakse player2 mängija värava koordinaat

  Player2Pos = SendReceiveBluetoothInfo(GameData);

  //CommonFunctions failist käivitatakse palli

  //ja väravate kuvamise protseduur

  Render(Player1Pos, Player2Pos,

          BallPos, GateWidth, BallWidth);

  //oodatakse 40ms

  Wait(MS_40);

}

 

//võitja kuvamise protseduur

void ShowWinner()

{

  string Win = "Sa voitsid";

  string Lose = "Sa kaotasid";

 

  //kui võitja oli Player1

  if(Winner==1)

  {

    //saadetakse Player2-le teade kaotuse kohta

    SendRemoteString(BTconn, MAILBOX6, Lose);

    TextOut(10, LCD_LINE4, Win);

    TextOut(10, LCD_LINE6, "Vajuta nuppu");

    TextOut(10, LCD_LINE7, "jatkamiseks");

  }

  //kui võitja oli Player2

  else if(Winner==2)

  {

    //saadetakse Player2-le teade võidu kohta

    SendRemoteString(BTconn, MAILBOX6, Win);

    TextOut(10, LCD_LINE6, "Vajuta nuppu");

    TextOut(10, LCD_LINE7, "jatkamiseks");

    TextOut(10, LCD_LINE4, Lose);

  }

  while(!ButtonPressed(BTNCENTER, FALSE));

  while(ButtonPressed(BTNCENTER, FALSE));

}

 

//algse andmestiku initsialiseerimine

void InitData()

{

  Winner = 0;

  BallPos.x = DISPLAY_WIDTH/2;

  BallPos.y = DISPLAY_HEIGHT/2;

  Player1Pos = DISPLAY_HEIGHT/2 - GateWidth/2;

  Player2Pos = DISPLAY_HEIGHT/2 - GateWidth/2;

  MootorCenter=MotorRotationCount(OUT_A);

  MootorCenterB=MotorRotationCount(OUT_B);

  //luuakse juhuslik arv vahemikus 0..119

  int RandomStart = Random(120);

  //juhulikust arvust tehakse vahemik -60..60,

  //mis vastab ekraani parempoolsele suunale

  int RandomRight = RandomStart - 60;

  //juhulikust arvust tehakse vahemik 120..240,

  //mis vastab ekraani vasakpoolsele suunale

  int RandomLeft = RandomStart + 120;

  //valitakse juhuslikult, kas algus on parem-vasak

  BallAngleDeg = Random(2) ? RandomRight : RandomLeft;

  //saadetakse Player2-le info mängu alustamise kohta

  SendRemoteString(BTconn, MAILBOX6, "RESET");

}

task main()

{

  //käivitatakse BT ühenduse kontroll ja/või loomine

  //kui ühendamine ei õnnestunud, väljutakse programmist

  if(!BluetoothConnect(BTconn, SlaveNXTName)==NO_ERR)

    Stop(TRUE);    

 

  //kuvame kasutajale ühe sekundi jooksul teate

  Wait(SEC_1);

 

  string shakemessage = "";

  while(shakemessage != "GO")

  {

    ReceiveRemoteString(MAILBOX1, TRUE, shakemessage);

    SendRemoteString(BTconn, MAILBOX1, "Handshake");

  }

  Wait(20);

  //saadetakse palli suuruse info

  SendRemoteNumber(BTconn, MAILBOX2, BallWidth);

  Wait(20);

  //saadetakse värava laiuse info

  SendRemoteNumber(BTconn, MAILBOX3, GateWidth);

  while(TRUE)

  {

    //nullitakse mängu andmed

    InitData();

    //mäng käib, kuni võitjat pole selgunud

    while(!Winner)

    {

      GameLoop();

    }

    //kui võitja on selgunud, kuvatakse see ekraanil

    ShowWinner();

  }

}

Mängija 2 programmikood

Mängija 2 jaoks kirjeldatud funktsioonid ja protseduurid kokku panduna on teise mängija NXT programmikood. See NXT on rollis Bluetooth slave ja tegeleb ainult info vastuvõtmise ja kuvamisega.

Näide. Mängija 2 programmikood

//SLAVE

#include "BluetoothCheck.nxc"

#include "CommonFunctions.nxc"

 

byte GateWidth;     //värava laius

byte BallWidth;     //palli suurus

int Player1Pos;     //mängija 1 värav

int Player2Pos;     //mängija 2 värav

string GameData;    //vastuvõetud mänguandmed

string ShowWinner;  //võitja kuvamine

Position BallPos;   //palli positsioon

 

//SendReceiveBluetoothInfo saadab ja võtab vastu infot

//funktsiooni sisendiks on värava info,

//mis on vaja edastada teisele NXT-le

//funktsioon tagastab mängija1 värava ja palli koordinaadid

//mängu võiduteade omistatakse globaalsele muutujale ShowWinner

string SendReceiveBluetoothInfo(int PlayerPos)

{

  string Data;

  //slave loeb oma lokaalsest mailboxist

  //palli ja värava koordinaadid

  ReceiveRemoteString(MAILBOX5, FALSE, Data);

  //SLAVE kirjutab oma värava koordinaadi MAILBOX4-a

  SendResponseNumber(MAILBOX4, PlayerPos);

  string TempShowWinner = "";

  //SLAVE loeb mailbox6-st võitja info

  ReceiveRemoteString(MAILBOX6, TRUE, TempShowWinner);

  //kui saadi võitja info, kuvatakse see ekraanil

  if(TempShowWinner != "")

    ShowWinner = TempShowWinner;

  //kui saadi nullimise info, ei kuvata võitja infot

  if(TempShowWinner == "RESET")

    ShowWinner = "";

  return Data;

}

 

//mängu funktsioonide väljakutsumine

void GameLoop()

{

  //CommonFunctions failist käivitatakse funktsioon

  //mis liigutab värava asukohta, sisendiks värava laius

  Player2Pos = PlayerPosition(GateWidth);

  //vahetatakse bluetooth infot, edastatakse värava info

  //tagasi saadakse player1 värava ja palli koordinaadid

  GameData = SendReceiveBluetoothInfo(Player2Pos);

 

  //stringitöötluse abil taastatakse

  //värava ja palli koordinaadid 

  int t = Pos("/", GameData);

  BallPos.x = StrToNum(SubStr(GameData, 0, t));

  int u = Pos("-", GameData);

  BallPos.y = StrToNum(SubStr(GameData, t+1, u-t));

  int v = StrLen(GameData);

  Player1Pos = StrToNum(SubStr(GameData, u+1, v-u));

 

  //CommonFunctions failist käivitatakse

  //palli ja väravate kuvamise protseduur

  Render(Player1Pos, Player2Pos,

          BallPos, GateWidth, BallWidth);

  //võitja info kuvatakse ekraanil

  TextOut(10, LCD_LINE4, ShowWinner);

  Wait(40);

}

task main()

{

  //käivitatakse BT ühenduse kontroll

  //kui ühendamine ei õnnestunud, väljutakse programmist

  if(!BluetoothCheck()==NO_ERR)

    Stop(TRUE);

 

  string shakemessage;

  while(shakemessage != "Handshake")

  {

    ReceiveRemoteString(MAILBOX1, TRUE, shakemessage);

  }

  TextOut(0,LCD_LINE1, "Waiting for bluetooth");

 

  SendResponseString(MAILBOX1, "GO");

  while(!BallWidth || !GateWidth)

  {

    ReceiveRemoteNumber(MAILBOX2, FALSE, BallWidth);

    ReceiveRemoteNumber(MAILBOX3, FALSE, GateWidth);

  }

  MootorCenter = MotorRotationCount(OUT_A);

  //mäng käib, kuni see katkestatakse

  while(TRUE)

  {

    GameLoop();

  }

}

 

Add comment

Loading