See materjal on loodud Tiigrihüppe Sihtasutuse programmi ProgeTiiger raames.
Ping-pong mäng seisneb selles, et ekraanil põrkab edasi-tagasi ja paremale-vasakule pall mis võib ekraani altservast nö. maha kukkuda. Palli ekraanil hoidmiseks on allservas värav mida saab nooltega paremale vasakule liigutada ning mille pealt pall on võimeline tagasi põrkama.
Õppetundide jaotuse aluseks on arvestatud ühe tunni kestuseks 2x45 min.
1. Tund.
a. Töötava mängu tutvustamine NXT peal
b. Programmi algoritmi tutvustamine
c. Värava liigutamise funktsiooni loomine.
2. Tund
a. Palli liigutamise funktsiooni kirjutamine
b. Palli ja värava alamprogrammide kokkupanek
3. Tund
a. Raskusastme lisamine
b. Punktiarvestuse lisamine ja initsialiseerimine
c. Mäng valmis
Mängu algoritm
Vaata joonist. Programmi käivitudes küsitakse kasutajalt mängu keerukust, peale selle valmisit lähevad paralleelselt käima kaks alamprogrammi: värava liigutamine ja palli põrkamine. Need kestavad seni kuni pall kukup ekraani altservast välja. Selle peale katkestatakse mäng, kuvatakse kasutajale tema punktisumma, küsitakse uuesti mängu keerukust ja seepeale algab mäng otsast peale.
1. Värava liigutamise funktsioon
2. Palli liigutamise funktsioon (lõpmatult ekraanil põrkamas)
3. Palli ja värava liigutamise funktsioonid kokku pandud (kas pall põrkas?)
4. Raskusastme valimine
5. Punktiarvestuse lisamine ja initsialiseerimine
6. Lõplik mängu kood
Koos kommentaaride ja tühjade ridadega (koodi parema loetavuse nimel) on programmi pikkus ca 220 rida.
Värava liigutamise funktsioon
Antud funktsiooni eesmärgiks on liigutada väravat paremale ja vasakule. Kui nuppu hoitakse alla, liigub värav sõites ühte või teise serva.
int Dash;
//Gate protseduur joonistab ekraanile värava
//antud alamprotseduur kutsutakse välja värava liigutamise juurest
void Gate(int i){
ClearLine(LCD_LINE8);
TextOut(i, LCD_LINE8, "__");
}
//MoveTheGate funktsioon liigutab väravat paremale/vasakule
task MoveTheGate(){
while (TRUE) {
//liigutame värava paremale kuni lõpuni
if (ButtonPressed(BTNRIGHT, TRUE)){
//väravat liigutatakse 5 pikseli kaupa
//kui ollakse piksli 85 juures jääb seisma ning värav on paremal
if (Dash < 85)
Dash += 5;
else
Dash = 85;
}
//liigutame väravat vasakule kuni algusesse
else if (ButtonPressed(BTNLEFT, TRUE)){
//väravat liigutatakse 5 piksli kaupa vasakule
//kui ollakse 1 piksli juures jääb seisma ning värav on vasakul
if (Dash > 1)
Dash -= 5;
else
Dash = 1;
}
//kutsutakse välja alamprotseduur Gate mis joonistab värava asukoha
//vastavalt oma parameetrile dash
Gate(Dash);
//ootame 0,1 sekundit, et värav liiguks paraja kiirusega
Wait(100);
}
}
task main(){
StartTask(MoveTheGate);
}
Palli liigutamise funktsioon
Antud funktsioon paneb palli ekraanil põrkama ning tulemuseks on palli lõpmatu põrkumine ekraanil 45 kraadise nurga all servade suhtes.
Pall liigub igas suunas ühe pikseli ehk punkti kaupa. Ekraani kõrgus on 64 ja laius 100 pikselit ehk punkti. Ülemist serva kontrollime numbriga 56 ja paremat numbriga 93, kuna ekraanil liigub tegelikkuses „o“ täht ja NXT arvestab selle tähe asukohta vasaku-alumise nurga järgi.
int Ball_X; //palli asukoht x teljel
int Ball_Y; //palli asukoht y teljel
bool Yles; //palli liikumise suund üles-alla
bool Paremale; //palli liikumise suund paremale-vasakule
int Ball_Last_X; //palli eelmine asukoht
int Kiirus = 60; //palli liikumise kiirus, kasutatakse raskusastme määramisel hiljem
//funktsioon Ball joonistab ekraanile palli
void Ball(int k, int i, int j){
ClearLine(k);
TextOut(j, i, "o");
}
//MoveTheBall liigutab palli üles-alla paremal-vasakule
task MoveTheBall(){
while (TRUE)
{
//Ball_Last_X postisioon on vajalik, et õige ekraani rida ära puhastada
//seega, algselt salvestatakse palli asukoht ajutisse muutujasse
Ball_Last_X = Ball_X;
//pall liigub üles
if (Yles)
Ball_X += 1;
//pall liigub alla
else
Ball_X -= 1;
//pall liigutatakse paremale
if (Paremale)
Ball_Y += 1;
//pall liigutatakse vasakule
else
Ball_Y -= 1;
//kutsutakse välja palli asukohta muutmise funktsioon
//esimene parameeter määrab rea mis kustutatakse
//teine ja kolmas parameeter määravad palli asukoha
Ball(Ball_Last_X, Ball_X, Ball_Y);
//kontrollitakse kas pall on all või üleval
//vastavalt olekule põrkab pall järgmise tsükli käigus vastassuunas
if (Ball_X > 56)
Yles = FALSE;
else if (Ball_X <= 2)
Yles = TRUE;
//kontrollitakse/juhitakse, kas pall peab paremale-vasakule põrkama
if (Ball_Y > 93)
Paremale = FALSE;
else if (Ball_Y < 1)
Paremale = TRUE;
//palli liikumise kiirus, edaspidi kasutatakse raskusastme jaoks
Wait(Kiirus);
}
}
task main ()
{
StartTask(MoveTheBall);
}
Palli ja värava liigutamise funktsioonid koos
Palli ja värava liigutamise funktsioonid kokkupandult tähendavad seda, et pall saab alla jõudes aru, kas tema alla on värav või mitte. Kui palli all on värav, põrkab pall üles tagasi või muidu kukub ekraani alt läbi ja mäng on läbi.
Antud funktsioonide kokkupanekul muutub palli liigutamise alamprogramm, värava liigutamine jääb samaks, seal ei muutu midagi. Seega värava liigutamise alamprogrammi siinkohal kordama ei hakka.
Palli liigutamise alamprogrammis muutub ainult koht koodis, mis kontrollib kas pall on all, seega välja on ainult see osa toodud.
//kontrollitakse kas pall on all või üleval
//vastavalt olekule põrkab pall järgmise tsükli käigus vastassuunas
if (Ball_X > 56)
Yles = FALSE;
//kontrollitakse kas pall on all
if (Ball_X <= 2)
{
//kui pall põrkab vastu alust, saad punkti
//ja pall liigub järgmise tsükli käigus üles
//aluse kontroll on alusest 8 pikslit paremale või 5 vasakule
//seega värava laiuseks on tegelikkuses 13 pikselit
if (Ball_Y >= Dash-5 && Ball_Y <= Dash + 8)
{
Yles = TRUE;
}
//kui pall ei põrka vastu alust
else
//kui pall ei põrka vastu alust, on mäng läbi
{
PlayTone(500, MS_20);
StopTask(MoveTheGate);
ExitTo(main);
}
}
Muutub ka alamprogramm main(), kus peab välja kutsuma kaks alamprogrammi. Alamprogrammid MoveTheGate ja MoveTheBall käivad paralleelselt, kuid alljärgnevas koodis on näha, et kõigepealt käivitatakse MoveTheGate() ja seejärel väljutakse main()-st ja käivitatakse MoveTheBall(). Selline koodijärjestus on vajalik, kuna kui käivitades lihtsalt mõlemad alamprogrammid käsuga StartTask lõpeb mõlema käivitamise järel main() programm ning rakendus sulgub koheselt.
NB! Selle küsimuse võib jätta koduseks ülesandeks: Miks käivitatakse paralleelselt käivad alamprogrammid alljärgneval moel ja mitte ei käivitata mõlemaid käsuga StartTask?
task main ()
{
StartTask(MoveTheGate);
ExitTo(MoveTheBall);
}
Lisaks tuleb koodi algusesse kirjutada alljärgnev rida. See deklareerib kompilaatorile, et eksisteerib alamprogramm nimega main(), mida kasutatakse MoveTheBall() alamprogrammis. Kompilaator kompileerib koodi järjest ning kui kõige alguses ei oleks deklareeritud main(), tekiks kompileerimisel viga kui programmi koodis kasutatakse viidet main()-le, kuna kompilaator ei tea veel, et see alamprogramm eksisteerib.
task main();
Raskusastme valimine
Raskusastme valimisega muutuvad mängus 2 omadust.
1) Esiteks muutub palli liikumise kiirus, mis teeb mängimise raskemaks.
2) Teiseks muutub punktiarvestus: kui pall liigub kiiremini, saab iga tagasipõrgatatud palli eest rohkem punkte kui palli aeglasemalt liikumise eest ehk kergema raskusastme korral.
int Score; //loeb kokku mängu jooksul saadud punktid
int Raskus = 2; //raskusastmed on 1..3, vaikimisi on 2, st. keskmine
int Kiirus = 250; //mängu kiirus on vaikimisi 250, kuid erinevad raskusastmed mõjutavad kiirust
task Start(){
string msg;
bool ExitWhile = FALSE;
//mängu käivitamisel kuvatakse ekraanil allolevad kirjad
ClearScreen();
msg = "-= Ping Pong =-";
TextOut(1, LCD_LINE1, msg);
msg = "----------------";
TextOut(1, LCD_LINE2, msg);
msg = "Let's play it !";
TextOut(1, LCD_LINE3, msg);
msg = "Sinu punktid: ";
msg += NumToStr(Score);
TextOut(1, LCD_LINE5, msg);
msg = "< vali raskus >";
TextOut(1, LCD_LINE6, msg);
//alljärgnev tsükkel on raskusastme valimiseks ning väljutakse siis kui
//kasutaja vajutab keskmist ehk oranzi nuppu
while (!ExitWhile)
{
//nupuvajutus lisab raskusastmele 1-e juurde
if (ButtonPressed(BTNRIGHT, TRUE))
{
while(ButtonPressed(BTNRIGHT, TRUE));
Raskus ++;
}
//nupuvajutus lautab raskusastmest 1-e
if (ButtonPressed(BTNLEFT, TRUE))
{
while(ButtonPressed(BTNLEFT, TRUE));
Raskus --;
}
//järgmised if laused tagavad selle, et raskusastme valimine käiks ringiratast
if (Raskus > 3) Raskus = 1;
if (Raskus < 1) Raskus = 3;
ClearLine(LCD_LINE7);
//switchi abil kuvatakse ekraanil raskusastmele vastav nimetus
//lisaks muudetakse kiiruse muutujat, mis mõjutab mängu raskust
switch (Raskus)
{
case 1:
TextOut(1, LCD_LINE7, " ---Kerge---");
Kiirus = 350;
break;
case 2:
TextOut(1, LCD_LINE7, " --Keskmine--");
Kiirus = 250;
break;
case 3:
TextOut(1, LCD_LINE7, " ---Raske---");
Kiirus = 150;
break;
}
//kui kasutaja vajutab keskmist nuppu, hakkab mäng pihta
if (ButtonPressed(BTNCENTER, TRUE))
{
while(ButtonPressed(BTNCENTER, TRUE));
ExitWhile = TRUE;
}
//oodatakse 100ms et välistada juhuslikud nupuvajutused
Wait(100);
}
//programm ootab enne väljumist 3 sekundit
//see on vajalik ainult käesoleva mooduli testimiseks
//mängus seda ootamist tarvis pole
Wait(SEC_3);
}
task main ()
{
StartTask(Start);
}
Punktiarvestuse lisamine ja initsialiseerimine
Punktiarvestus toimub põhimõttel, et mitu palli suudeti tagasi põrgatada. Erinevate kiiruste korral saab iga tagasipõrgatamise eest erineva arvu punkte. See osa koodist tuleb lisada sinna, kus kontrollitakse kas pall põrkab tagasi või kukub läbi.
if (Ball_Y >= Dash-5 && Ball_Y <= Dash + 8)
{
Yles = TRUE;
//siin antakse vastavalt mängu raskusele punktid
switch (Raskus)
{
case 1:
Score ++;
break;
case 2:
Score += 5;
break;
case 3:
Score += 10;
break;
}
}
Initsialiseerimine tähendab seda, et enne iga mängu alustamist saaksid pall, värav ja punktiarvestus nullitud. Selleks otstarbeks käivitatakse alljärgnev alamprotseduur.
//See protseduur algväärtustab muutujad
void Initialize()
{
Raskus;
Dash = 50; //see on värav ning alguses seatakse see keskele
Ball_X = 9; //rida millelt pall alustab liikumist
Ball_Y = 20; //veerg millelt pall alustab liikumist
Yles = TRUE; //muutuja, mis määrab et pall liigub alguses üles
Paremale = TRUE; //muutuja, mis määrab et pall liigub alguses paremale
Score = 0; //punktid nulli
ClearScreen(); //puhastame ekraani
Gate(Dash); //see joonistab värava algasukoha
}
Lõplik mängu kood
Kõik eelnevalt läbi käidud programmikoodi komponendid on alljärgnevalt kokku pandud üheks töötavaks tervikuks.
Esimesel real olev task Start() on siin põhjusel, et seda kutsutakse koodis välja enne kui antud alamprogramm on deklareeritud. Seetõttu on tarvilik defineerida kompilaatori jaoks tühi alamprogramm.
task Start();
int Dash;
int Score; //loeb kokku mängu jooksul saadud punktid
int Raskus=2; //raskusastmed on 1..3, vaikimisi keskmine
int Ball_X; //palli asukoht x teljel
int Ball_Y; //palli asukoht y teljel
bool Yles; //palli liikumise suund üles-alla
bool Paremale; //palli liikumise suund paremale-vasakule
int Ball_Last_X; //palli eelmine asukoht
int Kiirus = 60; //palli liikumise kiirus, kasutatakse raskusastme määramisel hiljem
//Gate protseduur joonistab ekraanile värava
//antud alamprotseduur kutsutakse välja värava liigutamise juurest
void Gate(int i){
ClearLine(LCD_LINE8);
TextOut(i, LCD_LINE8, "__");
}
//See protseduur algväärtustab muutujad
void Initialize()
{
Raskus;
Dash = 50; //see on värav ning alguses seatakse see keskele
Ball_X = 9; //rida millelt pall alustab liikumist
Ball_Y = 20; //veerg millelt pall alustab liikumist
Yles = TRUE; //muutuja, mis määrab et pall liigub alguses üles
Paremale = TRUE; //muutuja, mis määrab et pall liigub alguses paremale
Score = 0; //punktid nulli
ClearScreen(); //puhastame ekraani
Gate(Dash); //see joonistab värava algasukoha
}
//MoveTheGate funktsioon liigutab väravat paremale/vasakule
task MoveTheGate(){
while (TRUE) {
//liigutame värava paremale kuni lõpuni
if (ButtonPressed(BTNRIGHT, TRUE)){
//väravat liigutatakse 5 pikseli kaupa
//kui ollakse piksli 85 juures jääb seisma ning värav on paremal
if (Dash < 85)
Dash += 5;
else
Dash = 85;
}
//liigutame väravat vasakule kuni algusesse
else if (ButtonPressed(BTNLEFT, TRUE)){
//väravat liigutatakse 5 piksli kaupa vasakule
//kui ollakse 1 piksli juures jääb seisma ning värav on vasakul
if (Dash > 1)
Dash -= 5;
else
Dash = 1;
}
//kutsutakse välja alamprotseduur Gate mis joonistab värava asukoha
//vastavalt oma parameetrile dash
Gate(Dash);
//ootame 0,1 sekundit, et värav liiguks paraja kiirusega
Wait(100);
}
}
//funktsioon Ball joonistab ekraanile palli
void Ball(int k, int i, int j){
ClearLine(k);
TextOut(j, i, "o");
}
//MoveTheBall liigutab palli üles-alla paremal-vasakule
task MoveTheBall(){
while (TRUE)
{
//Ball_Last_X postisioon on vajalik, et õige ekraani rida ära puhastada
//seega, algselt salvestatakse palli asukoht ajutisse muutujasse
Ball_Last_X = Ball_X;
//pall liigub üles
if (Yles)
Ball_X += 1;
//pall liigub alla
else
Ball_X -= 1;
//pall liigutatakse paremale
if (Paremale)
Ball_Y += 1;
//pall liigutatakse vasakule
else
Ball_Y -= 1;
//kutsutakse välja palli asukohta muutmise funktsioon
//esimene parameeter määrab rea mis kustutatakse
//teine ja kolmas parameeter määravad palli asukoha
Ball(Ball_Last_X, Ball_X, Ball_Y);
//kontrollitakse kas pall on all või üleval
//vastavalt olekule põrkab pall järgmise tsükli käigus vastassuunas
if (Ball_X > 56)
Yles = FALSE;
//kontrollitakse kas pall on all
if (Ball_X <= 2)
{
//kui pall põrkab vastu alust, saad punkti
//ja pall liigub järgmise tsükli käigus üles
//aluse kontroll on alusest 8 pikslit paremale või 5 vasakule
//seega värava laiuseks on tegelikkuses 13 pikselit
if (Ball_Y >= Dash-5 && Ball_Y <= Dash + 8)
{
Yles = TRUE;
//siin antakse vastavalt mängu raskusele punktid
switch (Raskus)
{
case 1:
Score ++;
break;
case 2:
Score += 5;
break;
case 3:
Score += 10;
break;
}
}
//kui pall ei põrka vastu alust
else
//kui pall ei põrka vastu alust, on mäng läbi
{
PlayTone(500, MS_20);
StopTask(MoveTheGate);
ExitTo(Start);
}
}
//kontrollitakse/juhitakse, kas pall peab paremale-vasakule põrkama
if (Ball_Y > 93)
Paremale = FALSE;
else if (Ball_Y < 1)
Paremale = TRUE;
//palli liikumise kiirus, edaspidi kasutatakse raskusastme jaoks
Wait(Kiirus);
}
}
task Start(){
string msg;
bool ExitWhile = FALSE;
//mängu käivitamisel kuvatakse ekraanil allolevad kirjad
ClearScreen();
msg = "-= Ping Pong =-";
TextOut(1, LCD_LINE1, msg);
msg = "----------------";
TextOut(1, LCD_LINE2, msg);
msg = "Let's play it !";
TextOut(1, LCD_LINE3, msg);
msg = "Sinu punktid: ";
msg += NumToStr(Score);
TextOut(1, LCD_LINE5, msg);
msg = "< vali raskus >";
TextOut(1, LCD_LINE6, msg);
//alljärgnev tsükkel on raskusastme valimiseks ning väljutakse siis kui
//kasutaja vajutab keskmist ehk oranzi nuppu
while (!ExitWhile)
{
//nupuvajutus lisab raskusastmele 1-e juurde
if (ButtonPressed(BTNRIGHT, TRUE))
{
while(ButtonPressed(BTNRIGHT, TRUE));
Raskus ++;
}
//nupuvajutus lautab raskusastmest 1-e
if (ButtonPressed(BTNLEFT, TRUE))
{
while(ButtonPressed(BTNLEFT, TRUE));
Raskus --;
}
//järgmised if laused tagavad selle, et raskusastme valimine käiks ringiratast
if (Raskus > 3) Raskus = 1;
if (Raskus < 1) Raskus = 3;
ClearLine(LCD_LINE7);
//switchi abil kuvatakse ekraanil raskusastmele vastav nimetus
//lisaks muudetakse kiiruse muutujat, mis mõjutab mängu raskust
switch (Raskus)
{
case 1:
TextOut(1, LCD_LINE7, " ---Kerge---");
Kiirus = 120;
break;
case 2:
TextOut(1, LCD_LINE7, " --Keskmine--");
Kiirus = 60;
break;
case 3:
TextOut(1, LCD_LINE7, " ---Raske---");
Kiirus = 40;
break;
}
//kui kasutaja vajutab keskmist nuppu, hakkab mäng pihta
if (ButtonPressed(BTNCENTER, TRUE))
{
while(ButtonPressed(BTNCENTER, TRUE));
ExitWhile = TRUE;
}
//oodatakse 100ms et välistada juhuslikud nupuvajutused
Wait(100);
}
//initialize algväärtustab kõik mängu vajalikud omadused
Initialize();
StartTask(MoveTheGate);
ExitTo(MoveTheBall);
}
task main ()
{
Initialize();
StartTask(Start);
}