See 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
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
Funktsioon 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.
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.
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
Täisnurkse kolmnurga vastaskaateti pikkuse arvutamine teravnurga ning hüpotenuusi abil
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.
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.
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
Protseduur 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.
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();
}
}