Joonistuskäsud, lõimed, mälupilt, heli
Javas on püütud joonistusvahendid teha sihtpinnast sõltumatuks, nii et sama programmilõigu abil võib joonistada nii ekraanile, mällu kui printerisse ning sobiva draiveri abil ka muude vahendite abil. Programmi poolt saadetakse joonistatavale pinnale teateid graafilise konteksti abil, milleks on enamasti klassi Graphics või Graphics2D oskustega isend. Kontekstile antakse üldisi käsklusi joone, ringi, teksti jm. joonistamise kohta, viimase ülesandeks on tulemus kanda üle sihtpinnale. Kas tegemist on mälupildi joonistamisel massiivi väärtuste arvutamisega või põletuspulga liigutamisel plotteris, sellele ei pea programm tähelepanu pöörama. Programmi kirjutaja isegi ei pea teadma, millise sihtseadmeni tema käskude mõju lõpuks ulatub. Et iga lõigu või punkti joonistamisel ei peaks eraldi kaasa lisama joonistamisel vajalikke parameetreid, hoitakse neid graafilise konteksti juures ning koos joonistuskäsklustega edastatakse sihtseadmele. Graphics hoiab eneses joonistusvärvi, -piirkonda ning nihet, Graphics2D juures aga tulevad juurde joone laius, pildi keere, kalle, suurendus, joonte ühendus. Samuti võib joonistusvärviks valida mustri või värviülemineku. Sellisel juhul arvutades mälus asuvale pildile joonistatavat ringi iga punkti puhul vaadatakse, milline koht mustrist vastab joone alla jäävale kohale sihtpinnal.
Välja paistva graafikakomponendi joonistuskäsud koondatakse üldiselt meetodisse nimega paint. Meetodile antakse parameetrina graafiline kontekst, mis oma väljundi suunab pinnale, kus kasutaja arvab komponenti asuvat, üldjuhul ekraanile. Nii õnnestub lihtsustada komponenti näitava kesta tööd. Igal korral kui leitakse, et komponendi sisu on kaduma läinud või vajab lihtsalt uuendamist, kutsutakse taas välja paint, mis komponendi arvates sobiva oleku taastab. Kui komponendi omanik vaatab, et uuendamist vajab vaid osa komponendi pinnast, võidakse ressursside kokkuhoiu huvides saata paint-meetodisse pügatud (vähendatud, clip) joonistusalaga Graphics, mis kannab sihtpinnale edasi vaid nende joonistuskäskude tulemused, mis jäävad konteineri (komponendi omaniku) arvates uuendamist vajavasse piirkonda. Nõnda võib mõnikord veebilehe kerimisel avastada, et konteiner on uuendamist vajavat osa valesti hinnanud ning valgeks jäetakse ka osa komponendi pinnast, kus peaks midagi muud leiduma. Soovides kogu komponendi pinda uuendada, tuleb anda käsklus repaint(). See käsk käivitab pinnauuenduse eraldi lõimes. Ta ootab kõigepealt, et virtuaalmasinal jaguks piisavalt ressursse joonistamiseks. Edasi kutsutakse välja komponendi meetod update parameetrina graafiline kontekst komponendi asukohaseadme pinnale joonistamiseks, mis klassist java.awt.Component päritud vaikimisi oskuste alusel kõigepealt katab komponendi pinna taustavärviga ning seejärel kutsub välja meetodi paint, kus kirjeldatud käskude abil peaks kompomendi arvatav pinnavälimus kasutajani jõudma. Nõndamoodi saab hoolitseda, et vanast välimusest uude juhuslike jupikeste näol rudimente ei jääks, sest kui vana pind ühtlaselt taustavärvi plaadiga katta, siis ei tohi ju põhja midagi alla silma häirima jääda. Samas võib suurema pinna uuendamisel tekkida ekraanile vilgatus, kui platsi tühjendamise ning uue sisu joonistamise vahele piisavalt suur vahe jääb, et inimsilm seda märkab. Lahenduseks leitakse, et tuleb komponendi kohal näidatav pilt eelnevalt valmis arvutada ning siis ühekorraga või rida-realt ekraanile joonistada. Nõnda saab teha, kui pilt koostatakse mällu ning paint-meetodi sisuks oleks vaid selle pildi ekraanile manamine, update aga loobuks taustavärviga katmisest (taustavärv oleks ekraanile joonistatava pildi põhjaks) ning kutsuks kohe välja paint-i. update't annab muuta ülekatte abil: kui kirjutatada
public void update(Graphics g){
paint(g)
}
siis jääbki taustaga katmine ära ning kogu töö usaldatakse paint'ile. Sellist joonistusviisi nimetatakse topeltpuhverduseks ning Swingi juures võib sama skeemi joonistamise juures ühe lisaparameetriga sisse lülitada.
Ekraanile joonistada saab ka mujal kui paint-meetodis, kuid sellisel juhul konteineri poolt kutsutud pinnauuenduse korral läheb kõik muu kaduma, mis paint'is kirjas pole. Sellegipoolest on mõnikord mugav kohe hiirevajutuse peale küsida pinna graafiline kontekst ning sealtkaudu soovitud ringike ekraanile manada, mitte asuda andmeid muutujates säilitama ning paint'i kaudu ekraanile tooma. Tõsisemate programmide juures tuleks aga valida viimane tee ning pea alati on siiski vaja ka omal teada, mida ja kus peaks ekraanile ilmuma. Ka liikumise puhul on mõistlik jätta pinna uuendamine paint-meetodi hooleks, kuigi vahel on mugavam otse leitud uute andmete järgi ka pilt ekraanile joonistada.
Järgnevas näites koos kasutaja hiire lohistamisega liigub ekraanil ring. Liikumise taustaks on eelnevalt hiire lahtilaskmise kohas taustapildile joonistatud ringid ning iga uue lahtilaskmisega tekib taustapildile üks ring juurde. Joonistusmeetod paint viib ekraanile vaid taustapildi, liikumismulje jätab mouseDragged, kus alla joonistatakse taust ning peale hiire parajatist asukohta tähistav ring. Et taustapilt oleks kõikjalt Liigu2 isendist kätte saadav, selleks on deklareeritud
Image pilt;
klassi algusse. Pilt saab enesele väärtuse ning võimaluse andmeid sisestada aga alles joonistusmeetodis
public void
paint(Graphics g){
if(pilt==null)looPilt();
g.drawImage(pilt, 0, 0, this);
}
, sest varem pole kindlustatud, et createImage suudaks Liigu2 komponendi omadustele vastava pildi luua. Koos pildi loomisega küsitakse ka selle graafiline kontekst, et edaspidi oleks hea lihtne mälupildi peale andmeid saata.
void looPilt(){
pilt=createImage(getSize().width,
getSize().height);
piltg=pilt.getGraphics();
}
Java uuemates versioonides on võimalik pilt ka varem luua BufferedImage abil. Hiire sündmustele reageerimiseks on rakendile külge pandud kuular nii hiirevajutuste (MouseListener) kui hiire liikumise (MouseMotionListener) tarvis.
public Liigu2(){
addMouseMotionListener(this);
addMouseListener(new Liigu2kuular(this));
}
Liikumise ja vedamise tarvis kaks meetodit on realiseeritud Liigu2 enese sees, vajutuste püüdmiseks on aga loodud eraldi adapterklass, kust hiirenupu lahtilaskmise peale mälupildile ring joonistatakse.
import
java.awt.*;
import java.awt.event.*;
import
java.applet.Applet;
public class
Liigu2 extends Applet
implements MouseMotionListener{
Image pilt;
Graphics piltg;
public Liigu2(){
addMouseMotionListener(this);
addMouseListener(new Liigu2kuular(this));
}
public void paint(Graphics g){
if(pilt==null)looPilt();
g.drawImage(pilt, 0, 0, this);
}
void looPilt(){
pilt=createImage(getSize().width,
getSize().height);
piltg=pilt.getGraphics();
}
public void mouseMoved(MouseEvent e){}
public void mouseDragged(MouseEvent e){
Graphics g=this.getGraphics();
g.drawImage(pilt, 0, 0, this);
g.drawOval(e.getX()-5, e.getY()-5, 10,
10);
}
public static void main(String
argumendid[]){
Frame f=new Frame();
f.add(new Liigu2());
f.setSize(200, 200);
f.setVisible(true);
}
}
Et hiire vajutamise teateid püüdev suudaks Liigu2 juurde kuuluvale pildile midagi joonistada, selleks peab siin olema ligipääs ekraanile joonistatava pildi graafilisele kontekstile. Üheks võimaluseks on lisada kuularile osuti, mille kaudu sihtisendile ligi pääseda. Selle muutuja nimeks on siin peremees. Objektorienteeritud programmeerimises on viisakas isendimuutujatele anda väärtusi meetodite kaudu, siin saadakse andmed kohale konstruktori kaudu. Kui üleval kirjutati kuulari lisamiseks
addMouseListener(new
Liigu2kuular(this));
, siis see this Liigu2kuulari loomisel tähendabki osutit Liigu2 eksemplarile.
class Liigu2kuular extends MouseAdapter{ Liigu2
peremees; public
Liigu2kuular(Liigu2 l2){ peremees=l2; } public
void mouseReleased(MouseEvent e){
peremees.piltg.drawOval(
e.getX()-5, e.getY()-5, 10, 10); } } |
|
Järgnevas näites on lõime alamklass Pall2. Igal pallil on koordinaadid x ja y ning iga sammuga muutuvad need dx ja dy võrra. Iga pall liigub omaette lõimes, s.t. programmi muudest osadest sõltumatult. Pall alandab enese prioriteeti, s.t. et protsessori tööaja jagamisel loeb ta end keskmisest vähem tähtsamaks. Prioriteedi alandamine on vajalik selleks, et pallide liigutamine ei hakkaks märkimisväärselt aeglustama teisi lõimi, näiteks ekraanile joonistamist. Muidu võib juhtuda, et suure hulga pallide puhul jäävad ülejäänud tegevused suhteliselt unarusse. Käsk yield tähendab, et lõim annab oma tööjärje üle järgmisele ning asub ise järjekorda uuesti protsessori aega ootama.
Raami alamklass Loim2 loob enesele viis palli isendit ning laseb nad liikuma. Loim2 ise realiseerib liidest Runnable, s.t., et tema run-meetodit on samuti võimalik panna tööle eraldi lõimes. Selle lõime ülesandeks on joonistada iga natukese aja tagant uus ekraanipilt vastavalt pallide uutele koordinaatidele.
import java.awt.*;
public class Loim2 extends Frame implements Runnable{
Pall2[] pallid;
public Loim2(){
pallid=new Pall2[5];
for(int i=0;
i<pallid.length; i++)
pallid[i]=new Pall2();
setSize(200, 200);
setVisible(true);
new Thread(this).start();
}
public void joonista(){
Image pilt=createImage(200,
200);
Graphics
piltg=pilt.getGraphics();
for(int i=0;
i<pallid.length; i++){
piltg.drawOval(pallid[i].x()-10, pallid[i].y()-10, 20, 20);
}
this.getGraphics().drawImage(pilt, 0, 0, this);
}
public void run(){
while(true){
joonista();
Thread.yield();
try{Thread.sleep(100);}catch(Exception
e){}
}
}
public static void main(String
argumendid[]){
new Loim2();
}
}
class Pall2 extends Thread{
double x=200, y=200, dx, dy;
int vasak=20, ulal=50,
parem=200, all=200;
public Pall2(){
dx=5*Math.random();
dy=5*Math.random();
start();
}
public int x(){
return (int)x;
}
public int y(){
return (int)y;
}
public void run(){
setPriority(Thread.NORM_PRIORITY-2);
while(true){
if(x>parem)dx=-Math.abs(dx);
if(y>all)dy=-Math.abs(dy);
if(x<vasak)dx=Math.abs(dx);
if(y<ulal)dy=Math.abs(dy);
x+=dx;
y+=dy;
yield();
try{Thread.sleep(100);}catch(Exception e){}
}
}
}
Järgnevalt
vaadeldalse näidete abil teekond, mida läheb tarvis enamiku liikumisega seotud
rakenduste puhul. Alustatakse tausta liigutamisest ning ükshaaval lisatakse
elemente. Kas tulemuseks on mäng või juhitav simulaator sõltub rohkem
vaatenurgast.
Küllalt mugav on
tausta tekitada korduva pildiga, kuid see pole sugugi ainuke võimalus. Mustri
võib ka pidevalt mõne kavala valemi abil välja arvutada. Mugavaks ümberkäimiseks peaks taustapilt
olema nähtavast alast suurem, kuigi
mälu kokkuhoiu huvides võib vajadusel proovida ka sama väiksemat pilti mitu
korda üksteise kõrvale joonistada. Samuti on nõnda võimalik panna rakendus
tausta joonistamisel ka nähtava ala suurust arvestama. Siin näites on
taustapildi suuruseks 320x480 punkti, eeldatav vaatamissuurus aga 300x300
punkti. Tausta muster kordub iga 160 punkti tagant. Taustapilti joonistatakse
iga kaadri haaval niikaua allapoole, kuni on läbitud kogu mustripikkus (siin
160 punkti). Siis alustatakse järgmisel korral jälle mustri joonistamist
ülevalt. Nii tekibki pilt liikumisest, sest hüpe mustri kõrguse jagu üles ei
ole kasutajale nähtav.
Muudetavad parameetrid on koondatud
programmi algusse, et neid saaks ka siis oma vajaduste järgi paika sättida, kui
programmi sisust suuremat ülevaadet pole. Lähem seletus:
Taustapildimuutuja
on toodud meetoditest välja, et selle abil oleks võimalik pidevalt pildi poole
pöörduda ning ei peaks iga joonistamisvajaduse eel pilti asuma failist uuesti
välja lugema.
Mustripikkus on
eraldi muutujas, sest taustapildi vahetamisel teistsuguse vastu on tarvis
teada, kui pika hüppe peab üles tegema, et pilt kasutaja silmadele taas
samasugune välja näeks.
Samm hoiab
meeles, mitme ekraanipunkti jagu iga korraga pilti allapoole nihutatakse, paus
teatab joonistamise vahel oodatavate millisekundite arvu. Sammu suurendamisel
või ooteaja lühenemisel liikumiskiirus kasvab. Liialt suur samm muudab aga
liikumise hüplikuks, liialt väike ooteaeg aga ei pruugi lasta masinal tulemust
korralikult välja joonistada.
Image taust;
int mustripikkus=160;
int samm=3;
int paus=50;
int nihe=0;
boolean veel=false;
Plinkimise vältimiseks on üle kaetud meetod update, paludes sel kohe paint
välja kutsuda. Muul juhul tahetaks igal korral ekraan enne pildi joonistamist
taustavärviga katta ning selle tulemusena hakkaks silmeesine virvendama. Kuna aga
taustapilt nagunii kogu nähtava ala katab, siis ei jää vana pilt nagunii näha
ning taustavärviga katmine oleks mõttetu.
Pildi laadimiseks on loodud eraldi
alamprogramm, kuna rakend ja rakendus saavad pildi kätte erinevalt. See meetod
proovib kõigepealt pilti saada rakendi moel. Kui nii ei õnnestu, siis
üritatakse Toolkiti abil failist lugeda. Kui fail leidub ja formaadid sobivad,
siis üks võimalus nendest võiks õnnestuda.
Pilt laetakse sisse, kui
käivitusjärg jõuab esimest korda paint-alamprogammini. Loogiline võiks tunduda
pildi laadimine otse muutujate deklareerimise ajal, aga rakendi getImage pole
selleks ajaks veel töövõimeline. Nii ongi tehtud paint'i algusesse valik, kus
muutuja taust null-väärtuse korral (mis tal algselt on) palutakse pilt sisse
laadida.
Iga paint-meetodi väljakutse korral
liigutatakse taustapilti sammu jagu allapoole. Nihe näitab, palju ollakse
algsest asendist edasi liikunud. Kui pärast arvutust ületab nihe mustripikkust,
siis hüpatakse mustri pikkuse jagu ülespoole, et oleks taas võimalik rahumeeles
alla liikuda.
Joonistamiskäskluses on
x-koordinaadiks 0, sest tausta joonistatakse alates vasakust servast.
Y-koordinaadi väärtus nihe-mustripikkus näitab, et igal korral määrab pildi
asukoha nihke suurenemine. Samas konstantne mustripikkuse muutuja väärtus hoiab
pilti pidevalt niipalju kõrgel, et pildi ülaserv ei hakkaks kasutajale paistma.
Kui kood vaid sellega piirduks, siis
püsiks pilt enamiku ajast paigal. Ning vaid juhul, kui mõnd teist akent me
rakenduse peale ja sealt ära liigutada või natukese aja tagant meie akna
suurust muuta võiksime märgata mingit liikumist. Et aga saaksime silme ees näha
pidevalt liikuvat pilti, peaksid üksikud hüpped toimuma piisavalt sageli.
Sarnase ülejoonistuse nagu toimub
akna suuruse muutuse juures, saab esile kutsuda ka käsu repaint()
käivitamisega. Püsivat pilti joonistavates programmides paigutati see enamasti
tekstivälja või hiiresündmuse töötlusossa. Siin aga soovime, et meetodit
kutsutaks välja pidevalt iga natukese aja tagant. Leidub ka klass, mille abil
peaks võimalik olema täpselt soovitud ajavahemike tagant etteantud meetodit
käivitada, kuid lihtsamal juhul piisab lahendusest, kus pärast iga joonistust
peetakse soovitud aja pikkune paus. Selline vaikselt põksuv süda on ka siia rakendusse
sisse ehitatud ning võlusõnaks on lõim.
Kuigi traditsioonilise
programmeerimise juures on programmi tööjärg kogu aeg kindlas kohas ning ilma eelmist sammu lõpetamata edasi ei
liiguta, siis pole see sugugi ainus tava rakendusi töös hoida. Näiteks
liigutamise, ehk praegusel juhul repaint'i väljakutsumise saab jätta
suhteliselt iseseisvalt töötava lõime hooleks. Selle käivitamise, tööshoidmise
ja seiskamise tarvis on näha järgnevas koodis hulk sõnu.
Lõime abil omaette käivitatav kood
paigutatakse meetodi run sisse. Selline nimi on määratud liideses Runnable ning
selle järgi teab Thread-tüüpi objekt, mida käivitada. Meetodi run ülesandeks
selles programmis on repaint-käsklust korduvalt välja kutsuda. Selleks on
loodud tsükkel milles käsklus repaint() ning ootamiseks Thread.sleep(paus).
Viimane on paigutatud try-catch katsendiplokki, kuna võib mõnes olukorras
(lõimede omavahelise suhtlemise korral) heita erindeid. Siin koodis taolist
võimalust pole, kuid sellest hoolimata nõuab Java kompilaator potentsiaalselt
erindiohtliku käsu ümbritsemist vastava plokiga. Muutuja veel while-tsükli
päises võimaldab lõimel oma töö lõpetada, kui too tõeväärtusmuutuja väärtuseks
antakse false. See juhtub näiteks käsus stop, mis kutsutakse välja seilur
lahkub rakendit sisaldavalt lehelt.
Tööle lükatakse lõim meetodist
start. Näites võib märgata kaht käsklust start, mis aga omavahel kuigivõrd
seotud pole. Esimene kuulub klassi Pildike1 külge ning katab üle klassi Applet
vastavat meetodit. Rakendikäitur (seilur) teatab selle kaudu Pildike1
eksemplarile, et isend on lehel avanenud. Siis on paras aeg liikumislõime
käivitamiseks. Käsk new Thread(this).start() teatab, et loodagu uus klassi
Thread eksemplar, millele antakse ette
this ehk klassi Pildike1 eksemplar. Ning et etteantud eksemplaris pandagu
iseseisvana käima run-meetod. Et run just käivitada tuleb, seda teab juba
Thread. Iseseisva programmina käsurealt käivitades on näha main-meetodis käsku
ap.start() – see käivitab Pildike1 start-käsu samuti nagu rakendikäiturgi veebis.
Ning tulemusena õnnestub liikuvat tausta vaadata.
import
java.applet.Applet;
import
java.awt.*;
public
class Pildike1 extends Applet implements Runnable{
Image taust;
int mustripikkus=160;
int samm=3;
int paus=50;
int nihe=0;
boolean veel=false;
public void paint(Graphics g){
if(taust==null)taust=laePilt("rohetaust320x480.gif");
nihe=nihe+samm;
if(nihe>mustripikkus)nihe=nihe-mustripikkus;
g.drawImage(taust, 0, nihe-mustripikkus,
this);
}
public void update(Graphics g){
paint(g);
}
public void start(){
veel=true;
new Thread(this).start();
}
public void run(){
while(veel){
repaint();
try{Thread.sleep(paus); }catch(Exception
e){}
}
}
public void stop(){
veel=false;
}
Image laePilt(String
failinimi){ try{ return
getImage(getCodeBase(), failinimi); }catch(Exception e){} return
Toolkit.getDefaultToolkit().
getImage(failinimi); } public static void
main(String argumendid[]){ Frame f=new
Frame("Pildiraam"); Pildike1 ap=new
Pildike1(); f.add(ap); f.setSize(300, 300); f.setVisible(true); ap.start(); } } |
|
Nagu iga keerukama ülesande kallale tuleb asuda üksikute osade kaupa, nii
ka siin on järgmiseks ette võetud lillede paigutamine taustale. Ning
hoolitsetud, et lilled suhteliselt ühtlase tihedusega, kuid samas juhuslikult
pinnale jaotuksid ning pinna suhtes paigal püsides inimese eest läbi liiguksid.
Juurde on tulnud muutujad lilledega seotud andmete hoidmiseks. Lille kõrguse
järgi saab arvestada millal ta ekraanile paistma hakkab, millal kaob ning
hiljem ka leida, kas lill mõne muu alaga kattuma juhtub. Reaalarvuline
lillelisamistõenäosus näitab, mitu lille luuakse keskmiselt iga sammu korral.
Et väärtuseks on praegu 0.03*samm, tähendab, et iga kõrguse ekraanipunkti kohta
tuleb keskmiselt 0.03 lille, ehk siis ligikaudu üks lill iga 33 punkti kohta.
Lilli endeid hoitakse Vector'is; kollektsioonis, mille elemente võib vabalt
lisada ja eemaldada ilma, et peaks ise muret tundma mälu eraldamise või
vabastamise pärast. Aega arvutatakse ekraanipunktides. Et liikumine on ühtlase
kiirusega, siis leidub ka kulunud ajaga võrdeline seos. Samahästi võiks
muutujat nimetada kogunihkeks, ehk maapinna liikumise kogu punktide arvuks.
Lillede haldamise tarbeks on loodud mitu alamprogrammi: lisamise,
eemaldamise ja joonistamise tarbeks.
Lillelisamistõenäosus näitas, mitu
lille tuleb ühe joonistustsükli eel lisada. Kui see väärtus on näiteks 2,7,
siis kaks lille lisatakse kindlasti, kolmas aga tõenäosusega 0,7 ehk 70%.
Muutuja lt liigub tõenäosuse algsest väärtusest kuni nullini. Kuni lt väärtus
on suurem kui 1, lisatakse lill kindlasti, sest Math.random()-i väljastatu on
vahemikus 0<=x<1. Jääb aga lt väärtus alla ühe, siis sõltub Math.random'i
tegevusest, kas tuleb veel lill või mitte. Lill luuakse uue objektina, millele
jäetakse meelde tema x-koordinaat (muutumatu) ning aeg ehk maapinna liikumise
teepikkus ekraanipunktides lille lisamise ajaks. Selle järgi on võimalik pärast
arvutada, kus lill ekraanil peaks asuma, sest kõik lilled luuakse vaikimisi
ülaserva. X-koordinaat leitakse juhuslikult pea kogu nähtava laiuse ulatuses.
Kümme punkti võetakse laiusest x-i leidmisel maha, et ei tekiks lille, mis
ekraanil sugugi näha poleks. Meeldejäetud x tähistab lille vasakut serva.
void lisaUusiLilli(){
for(double lt=lillelisamistoenaosus;
lt>0; lt=lt-1){
if(Math.random()<lt){
lillekesed.addElement(new
Lilleke2((int)((laius-10)*Math.random()), aeg));
}
}
}
Lille klass näeb välja suhteliselt lihtne. Vaid kaks muutujat ning
konstruktor algandmete sisestamiseks. Siin näites on ta paigutatud eraldi
klassina samasse faili, kuid selle võiks paigutada ka sootuks omaette faili samasse
kataloogi või siis hoopis sisemise klassina Pildike2 sisse. Eraldi failis saaks
Lilleke2-te kasutada soovi korral otse ka teised klassid. Sisemise klassina
paigutamise puhul aga pole muret et mõni muu samanimeline klass kusagil
segadust tekitama hakkaks.
class
Lilleke2{
int x, algaeg;
public Lilleke2(int x1, int algaeg1){
x=x1; algaeg=algaeg1;
}
}
Iga sammu juures
kontrollitakse kõik lilled läbi ning eemaldatakse alt üle ääre sattunud. Käsuga
elementAt küsitakse lillekeste vektorist välja
järjekorranumbrile vastav lill. Tüübimuundus on tarvilik, kuna Vector
hoiab kõiki andmeid ülemklassina Object, meil on aga tarvis lillelt küsida vaid
temale omase välja algaeg väärtust.
void eemaldaVanadLilled(){
for(int i=0; i<lillekesed.size(); i++){
Lilleke2
l=(Lilleke2)lillekesed.elementAt(i);
if(aeg-l.algaeg>korgus+lillekorgus){
lillekesed.removeElementAt(i);
i--;
}
}
}
Joonistamine läks
pikemaks. Ehkki paint näeb välja ilus lühike, on tema ülesannet täitma tulnud
hulk abilisi.
public void paint(Graphics g){
koostaPilt();
g.drawImage(pilt, 0, 0, this);
}
Lisaks taustale tuleb sisse lugeda ka lille pilt. Samuti ei joonistata
tausta enam otse ekraanile, vaid pannakse mälus enne taust ja lilled uue pildi
peale kokku ning alles seejärel näidatakse tulemus ekraanile. Mälupildi
koostamiseks on createImage, mis jällegi rakendi puhul pole enne võimeline
käivituma kui esimese paint-i ajal. Loodud pildi käest küsitud piltg jääb
globaalsena üle kogu isendi kättesaadavaks, et ei pea muudele joonistavatele
alamprogrammidele seda eraldi kätte jagama. Nagu näha, pärast iga sammu
eemaldatakse vanad lilled, lisatakse uusi, joonistatakse taust mälupildi
põhjaks ning lilled ükshaaval sellele.
void koostaPilt(){
if(taust==null)taust=laePilt("rohetaust320x480.gif");
if(lill==null)lill=laePilt("lill1.gif");
if(pilt==null){
pilt=createImage(laius, korgus);
piltg=pilt.getGraphics();
}
nihe=nihe+samm;
aeg=aeg+samm;
if(nihe>mustripikkus)nihe=nihe-mustripikkus;
eemaldaVanadLilled();
lisaUusiLilli();
piltg.drawImage(taust, 0,
nihe-mustripikkus, this);
joonistaLilled();
}
Lillede joonistamisel käiakse lihtsalt läbi kõik vektoris olevad lilled
ning paigutatakse nende andmete järgi lillepilt taustapildile. Arvutus
aeg-lille algaeg näitab lille asukoha ekraanil. Lisaks võetakse maha veel
lillekõrgus, et lill asuks nähtavasse alasse sisenema oma alumise poolega ja
mitte ei tekiks järsku tervikuna inimese vaatevälja.
void joonistaLilled(){
for(int i=0; i<lillekesed.size(); i++){
Lilleke2
l=(Lilleke2)lillekesed.elementAt(i);
piltg.drawImage(lill, l.x,
aeg-l.algaeg-lillekorgus, this);
}
}
Ja tervikuna
liikuva tausta ja juhuslike lilledega rakenduse kood.
import
java.applet.Applet;
import
java.awt.*;
import
java.util.Vector;
public
class Pildike2 extends Applet implements Runnable{
Image taust;
Image pilt;
Graphics piltg;
Image lill;
int lillekorgus=100;
int mustripikkus=160;
int samm=3;
int paus=50;
int nihe=0;
int aeg=0;
int laius=300, korgus=300;
Vector lillekesed=new Vector();
double lillelisamistoenaosus=0.03*samm;
boolean veel=false;
public void paint(Graphics g){
koostaPilt();
g.drawImage(pilt, 0, 0, this);
}
public void update(Graphics g){
paint(g);
}
void koostaPilt(){
if(taust==null)taust=laePilt("rohetaust320x480.gif");
if(lill==null)lill=laePilt("lill1.gif");
if(pilt==null){
pilt=createImage(laius, korgus);
piltg=pilt.getGraphics();
}
nihe=nihe+samm;
aeg=aeg+samm;
if(nihe>mustripikkus)nihe=nihe-mustripikkus;
eemaldaVanadLilled();
lisaUusiLilli();
piltg.drawImage(taust, 0,
nihe-mustripikkus, this);
joonistaLilled();
}
void lisaUusiLilli(){
for(double lt=lillelisamistoenaosus;
lt>0; lt=lt-1){
if(Math.random()<lt){
lillekesed.addElement(new
Lilleke2((int)((laius-10)*Math.random()), aeg));
}
}
}
void eemaldaVanadLilled(){
for(int i=0; i<lillekesed.size(); i++){
Lilleke2
l=(Lilleke2)lillekesed.elementAt(i);
if(aeg-l.algaeg>korgus+lillekorgus){
lillekesed.removeElementAt(i);
i--;
}
}
}
void joonistaLilled(){
for(int i=0; i<lillekesed.size(); i++){
Lilleke2
l=(Lilleke2)lillekesed.elementAt(i);
piltg.drawImage(lill, l.x,
aeg-l.algaeg-lillekorgus, this);
}
}
public void start(){
veel=true;
new Thread(this).start();
}
public void run(){
while(veel){
repaint();
try{Thread.sleep(paus); }catch(Exception
e){}
}
}
public void stop(){
veel=false;
}
Image laePilt(String failinimi){
try{
return getImage(getCodeBase(),
failinimi);
}catch(Exception e){}
return Toolkit.getDefaultToolkit().getImage(failinimi);
}
public static void main(String argumendid[]){ Frame f=new
Frame("Pildiraam"); Pildike2 ap=new
Pildike2(); f.add(ap); f.setSize(300, 300); f.setVisible(true); ap.start(); } } class Lilleke2{ int x, algaeg; public Lilleke2(int x1, int
algaeg1){ x=x1; algaeg=algaeg1; } } |
|
Ehkki lõpupoole soovin näha nii lilli kui putukaid koos liiklemas, on hea
väiksemaid osi alustuseks eraldi poovida. Järgnevalt saab putukat noolte abil
liigutada omasoodu liikuva tausta kohal. Putuka tarvis on juurde tulnud
muutujad tema asukoha kohta, samuti sammu pikkus, mõõtmed ja pilt.
Joonistatakse sarnaselt lilledega, st., et kõigepealt koostatakse mälus pilt taustast
ja putukast ning siis kantakse tulemus tervikuna ekraanile.
Põhiline lisandus on putuka liigutamine klaviatuuri abil. Klahvisündmuste
kuulamiseks on realiseeritavaks liideseks lisandunud KeyListener paketist
java.awt.event. Liidesega koos kolm kohustuslikult realiseeritavat meetodit:
keyPressed, keyReleased ja keyTyped. Et reageerida soovime vaid esimesele, on
ülejäänute koodiosa tühi.
Klahvivajutusele reageerimisel
küsitakse kõigepealt klahvi kood, et oleks võimalik asuda võrdlema, millisele
klahvile vajutati. Vasaku noole puhul kontrollitakse kõigepealt, et putukas
oleks vasakust äärest vähemalt oma sammupikkuse kaugusel ning sel puhul
vähendatakse putuka x-koordinaadi väärtust sammu jagu. Nõnda võin vasakut noolt
vajutades liikuda putukaga vasaku serva lähedale, kuid mitte kaugemale. Pole
karta, et putukas ekraani pealt lahkuks. Sarnane kontroll on ka teiste külgede
juures, kuid paremal ja all arvestatakse lisaks veel putuka mõõtmetega, et ka
parem ega alumine külg üle piiri ei läheks.
public void keyPressed(KeyEvent e){
int kood=e.getKeyCode();
if((kood==KeyEvent.VK_LEFT) &&
(putukax>putukasamm))putukax‑=putukasamm;
if((kood==KeyEvent.VK_RIGHT) &&
(putukax<laius-putukasamm-putukalaius))
putukax+=putukasamm;
if(kood==KeyEvent.VK_UP &&
putukay>putukasamm)putukay-=putukasamm;
if(kood==KeyEvent.VK_DOWN &&
putukay<korgus-putukasamm-putukakorgus)
putukay+=putukasamm;
}
Muu kood on
küllalt sarnane tausta liikumise näitega. Lõim hoolitseb omasoodu sagedase pildiuuenduse
eest, lisaks taustapildile on aga tarvis lisada ka putukas vastavalt oma
koordinaatidele.
import
java.applet.Applet;
import
java.awt.*;
import
java.awt.event.*;
public
class Pildike3 extends Applet implements Runnable, KeyListener{
Image taust;
Image pilt;
Graphics piltg;
Image putukas;
int putukax=100, putukay=150;
int putukalaius=30, putukakorgus=15;
int putukasamm=4;
int mustripikkus=160;
int samm=3;
int paus=50;
int nihe=0;
int aeg=0;
int laius=300, korgus=300;
boolean veel=false;
public Pildike3(){
addKeyListener(this);
requestFocus();
}
public void paint(Graphics g){
koostaPilt();
g.drawImage(pilt, 0, 0, this);
}
public void update(Graphics g){
paint(g);
}
void koostaPilt(){
if(taust==null)taust=laePilt("rohetaust320x480.gif");
if(putukas==null)putukas=laePilt("sirelane.gif");
if(pilt==null){
pilt=createImage(laius, korgus);
piltg=pilt.getGraphics();
}
nihe=nihe+samm;
aeg=aeg+samm;
if(nihe>mustripikkus)nihe=nihe-mustripikkus;
piltg.drawImage(taust, 0,
nihe-mustripikkus, this);
piltg.drawImage(putukas, putukax, putukay,
this);
}
public void start(){
veel=true;
new Thread(this).start();
}
public void run(){
while(veel){
repaint();
try{Thread.sleep(paus); }catch(Exception
e){}
}
}
public void stop(){
veel=false;
}
public void keyPressed(KeyEvent e){
int kood=e.getKeyCode();
if((kood==KeyEvent.VK_LEFT) &&
(putukax>putukasamm))putukax-=putukasamm;
if((kood==KeyEvent.VK_RIGHT) &&
(putukax<laius-putukasamm-putukalaius))
putukax+=putukasamm;
if(kood==KeyEvent.VK_UP &&
putukay>putukasamm)putukay-=putukasamm;
if(kood==KeyEvent.VK_DOWN &&
putukay<korgus-putukasamm-putukakorgus)
putukay+=putukasamm;
}
public void
keyReleased(KeyEvent e){} public void
keyTyped(KeyEvent e){} Image laePilt(String
failinimi){ try{ return
getImage(getCodeBase(), failinimi); }catch(Exception e){} return
Toolkit.getDefaultToolkit().
getImage(failinimi); } public static void
main(String argumendid[]){ Frame f=new
Frame("Pildiraam"); Pildike3 ap=new
Pildike3(); f.add(ap); f.setSize(300, 300); f.setVisible(true); ap.start(); } } |
|
Liikuvad lilled ja liigutatav
putukas omaette läbi proovitud, nüüd kannatab nad ühte kesta kokku panna.
Juures on omadus, kus putukas lilleni jõudmisel selle nektarist tühjaks imeb,
nii et viimane pärast imemist seest tühjemana paistab. Ka lilleklassi sai veidi
täiendatud: nüüd on sel lisaks oma x-koordinaadile ja algusajale meeles, kas on
ta juba nektarist tühjaks imetud või veel mitte. Ning nii nagu
objektorienteeritud programmile kohane, sai tühjuse määramiseks ja küsimiseks
koostatud eraldi meetodid. Nii on näiteks võimalik oleku muutumisel
kontrollida, kas uus väärtus sobib või soovi korral toimingud kuhugile testiks
logida. Samuti võiks praeguse programmi korral olla lubatud tühi vaid tõeseks
muuta, sest iseenesest juba tühjaks imetud õied siin enam mesimahla ei tekita.
class
Lilleke4{
int x, algaeg;
boolean tyhi=false;
public Lilleke4(int x1, int algaeg1){
x=x1; algaeg=algaeg1;
}
void paneTyhi(boolean kasTyhi){
tyhi=kasTyhi;
}
boolean kasTyhi(){
return tyhi;
}
}
Teadmaks, kas putukas mõne lille
pihta satub, kontrollitakse läbi kõikide loetelus olevate lillede kaugused
putukast. Kontrollimisel abiks põhikoolist tuttav Pythagorase teoreem, et
täisnurkse kolmnurga külgede ruutude summa on võrdne pikima külje ruuduga.
Imemiskaugus on paigutatud eraldi muutujasse, et oleks võimalik määrata, kui
kaugelt suudab putukas nektari kätte saada. Esimene mõte imemisvõimaluse
leidmisel tõenäoliselt tuleb, et peaks leidma putuka ja lille vahelise kauguse
ning siis võrdlema, kas leitud suurus on imemiskaugusest väiksem. Et aga
ruutjuure arvutamine nõuab arvutilt küllalt palju protsessoritehteid ning iga
sammu ajal on tarvilik leida hulga lillede ja putuka vaheline kaugus, siis
annab programmi sujuvamaks teha, kui vaid võrrelda külgede ruutude summat
imemiskauguse ruuduga. Viimane ei muutu ning selle saab vähemasti enne sammu
algust valmis arvutada.
Kui leitakse, et lill putukale
piisavalt lähedale sattus, siis antakse lillele teada, et mingu ta tühjaks.
Samuti palutakse Toolkit'il tekitada väikene kõll.
void kontrolliImemisi(){
int kauguseruut=imemiskaugus*imemiskaugus;
for(int i=0; i<lillekesed.size(); i++){
Lilleke4
l=(Lilleke4)lillekesed.elementAt(i);
int lilley=aeg-l.algaeg-lillekorgus/2;
int xkaugus=putukax-l.x;
int
ykaugus=putukay-(lilley-lillekorgus/2);
if(!l.kasTyhi() &&
(xkaugus*xkaugus+ykaugus*ykaugus)<kauguseruut){
l.paneTyhi(true);
Toolkit.getDefaultToolkit().beep();
}
}
}
Joonistamise puhul tuleb siis iga lille puhul otsustada,
milline pilt lillest välja näidata. Nektarit täis lille pilt on nime all lill,
tühjaks imetu oma lill2. Avaldis
(l.kasTyhi())?lill2:lill väljastab sulgudes oleva tõese tingimuse puhul
küsimärgile kohe järgneva väärtuse, ehk pildi lill2. Kui aga lill pole veel
tühjaks imetud, võetakse tulemus pärast koolonit ehk pilt muutujast lill.
void joonistaLilled(){
for(int i=0; i<lillekesed.size(); i++){
Lilleke4 l=(Lilleke4)lillekesed.elementAt(i);
Image
lillepilt=(l.kasTyhi())?lill2:lill;
piltg.drawImage(lillepilt, l.x,
aeg-l.algaeg-lillekorgus, this);
}
}
Ja taas kogu peaklassi kood tervikuna.
import
java.applet.Applet;
import
java.awt.*;
import
java.awt.event.*;
import
java.util.Vector;
public
class Pildike4 extends Applet implements Runnable, KeyListener{
Image taust;
Image pilt;
Graphics piltg;
Image lill, lill2;
int lillekorgus=100;
Image putukas;
int putukax=100, putukay=150;
int putukalaius=30, putukakorgus=15;
int putukasamm=4;
int imemiskaugus=10;
int mustripikkus=160;
int samm=1;
int paus=50;
int nihe=0;
int aeg=0;
int laius=300, korgus=300;
Vector lillekesed=new Vector();
double lillelisamistoenaosus=0.03*samm;
boolean veel=false;
public Pildike4(){
addKeyListener(this);
}
public void paint(Graphics g){
koostaPilt();
g.drawImage(pilt, 0, 0, this);
}
public void update(Graphics g){
paint(g);
}
void koostaPilt(){
if(taust==null)taust=laePilt("rohetaust320x480.gif");
if(putukas==null)putukas=laePilt("sirelane.gif");
if(lill==null)lill=laePilt("lill1a.gif");
if(lill2==null)lill2=laePilt("lill1.gif");
if(pilt==null){
pilt=createImage(laius, korgus);
piltg=pilt.getGraphics();
}
nihe=nihe+samm;
aeg=aeg+samm;
if(nihe>mustripikkus)nihe=nihe-mustripikkus;
eemaldaVanadLilled();
lisaUusiLilli();
kontrolliImemisi();
piltg.drawImage(taust, 0, nihe-mustripikkus,
this);
joonistaLilled();
piltg.drawImage(putukas, putukax, putukay,
this);
}
void lisaUusiLilli(){
for(double lt=lillelisamistoenaosus;
lt>0; lt=lt-1){
if(Math.random()<lt){
lillekesed.addElement(new
Lilleke4((int)((laius-10)*Math.random()), aeg));
}
}
}
void eemaldaVanadLilled(){
for(int i=0; i<lillekesed.size(); i++){
Lilleke4
l=(Lilleke4)lillekesed.elementAt(i);
if(aeg-l.algaeg>korgus+lillekorgus){
lillekesed.removeElementAt(i);
i--;
}
}
}
void kontrolliImemisi(){
int kauguseruut=imemiskaugus*imemiskaugus;
for(int i=0; i<lillekesed.size(); i++){
Lilleke4
l=(Lilleke4)lillekesed.elementAt(i);
int lilley=aeg-l.algaeg-lillekorgus/2;
int xkaugus=putukax-l.x;
int
ykaugus=putukay-(lilley-lillekorgus/2);
if(!l.kasTyhi() &&
(xkaugus*xkaugus+ykaugus*ykaugus)<kauguseruut){
l.paneTyhi(true);
Toolkit.getDefaultToolkit().beep();
}
}
}
void joonistaLilled(){
for(int i=0; i<lillekesed.size(); i++){
Lilleke4
l=(Lilleke4)lillekesed.elementAt(i);
Image
lillepilt=(l.kasTyhi())?lill2:lill;
piltg.drawImage(lillepilt, l.x,
aeg-l.algaeg-lillekorgus, this);
}
}
public void start(){
veel=true;
new Thread(this).start();
}
public void run(){
while(veel){
repaint();
try{Thread.sleep(paus); }catch(Exception
e){}
}
}
public void stop(){
veel=false;
}
public void keyPressed(KeyEvent e){
int
kood=e.getKeyCode();
if((kood==KeyEvent.VK_LEFT) &&
(putukax>putukasamm))putukax-=putukasamm;
if((kood==KeyEvent.VK_RIGHT) &&
(putukax<laius-putukasamm-putukalaius))
putukax+=putukasamm;
if(kood==KeyEvent.VK_UP &&
putukay>putukasamm)putukay-=putukasamm;
if(kood==KeyEvent.VK_DOWN &&
putukay<korgus-putukasamm-putukakorgus)
putukay+=putukasamm;
}
public void keyReleased(KeyEvent e){}
public void keyTyped(KeyEvent e){}
Image laePilt(String failinimi){
try{
return getImage(getCodeBase(), failinimi);
}catch(Exception e){}
return
Toolkit.getDefaultToolkit().getImage(failinimi);
}
public static void main(String
argumendid[]){
Frame f=new Frame("Pildiraam");
Pildike4 ap=new Pildike4();
f.add(ap);
f.setSize(300, 300);
f.setVisible(true);
ap.start();
}
}
Sirelane mängu
alguses |
Esimesed lilled |
Kaks tühjaks imetud
lille
Nähtavalt on juurde tulnud kerimisribad putuka
liikumistundlikkuse, maapinna liikumiskiiruse ja lillede tiheduse määramiseks.
Näha veel väike statistika ning klahve vajutades tunda, et putukas mitte ei
hüppa klahvivajutuse peale, vaid klahvidega saab pigem putuka suunda ja kiirust
määrata. Ühes sellega on muutunud või lisandunud hulga koodi.
Kõigepealt tuleb kerimisribad mälus valmis luua. Sulgude
sees näha neil hulga parameetreid. Scrollbar.HORIZONTAL näitab, et riba on
pikali. Sooviks püstist riba, oleks vastavaks konstandiks Scrollbar.VERTICAL.
Järgnevad arvud: algne väärtus, nupu laius, vähim ja suurim võimalik väärtus.
Nagu siit aimata võib, määratakse ja küsitakse kerimisriba väärtusi arvude
abil.
Scrollbar sbtaustasamm=new
Scrollbar(Scrollbar.HORIZONTAL, 10, 5, 0, 100);
Et kerimisriba sündmustele annaks reageerida, selleks on
klassi realiseeritavate liideste loendisse lisandunud AdjustmentListener
public
class Pildike5 extends Applet implements Runnable, KeyListener,
AdjustmentListener{
Liidesega käib koos meetod adjustmentValueChanged, mis on
võimalik kerimisriba liigutamise peale käivitada. Iga käivituse puhul küsitakse
kõigist kolmest kerimisribast väärtused ja paigutatakse vastavatesse
muutujatesse. Iseenesest oleks võimalik küsida meetodi parameetriks tulnud
AdjustmentEvent'i käest käsuga e.getSource(), et millist riba just liigutati.
Kuna aga kerimisribast väärtuse küsimine ei nõua kuigivõrd ressurssse, siis
pole eristust tehtud. Samuti oleks võimalik läbi ajada sootuks ilma
sammupikkuse või mõne muu muutujata, küsides igal vajaminemiskorral väärtuse
otse kerimisribast.
Kerimisribast
saab väärtuse küsida vaid täisarvuna. Et aga liikumisel saaks asukohti
sujuvamalt arvutada, on liikumisega seotud väärtused salvestatud reaalarvudena.
Putuka ja maapinna liikumise puhul on kerimisribalt väärtus jagatud kümnega.
Lillelisamistõenäosuse puhul aga, kus väärtused väiksemad ning kümnendkohtadel
suurem tähtsus - tuhandega. Lõpuks on palutud lõuend fookusesse kutsuda, et
kasutaja klahvivajutused taas lõuendilt kinni püütavad oleksid. Lõuendil asuvad
taust, lilled ja putukas.
public void
adjustmentValueChanged(AdjustmentEvent e){
samm=sbtaustasamm.getValue()/10.0;
kiirusesamm=sbputukatundlikkus.getValue()/10.0;
lillelisamistoenaosus=sblilletoenaosus.getValue()/1000.0*samm;
louend.requestFocus();
}
Graafikakomponendid paigutatakse konstruktoris. Kui eelmises
näites joonistati rakendi enese pinnale, siis nüüd kasutatakse selleks eraldi
lõuendit. Nõnda on kergem hoolitseda, et kerimisribad sisestusfookust enesele
ei haarakse. Veel taseme jagu viisakam oleks kogu liikumine ja joonistamine
jätta eraldi komponendi sisse ning komponendi külge ehitada meetodid, mille
abil saab parameetreid muuta. Siis luua kestprogramm, mis paigutaks enese peale
nii tolle joonistava komponendi kui kerimisribad ning kas vahendaks
kerimisribade teated loodud komponendile või paluks ribadel otse oma teated
sinna saata.
Pildike5
paigutushalduriks on valitud BorderLayout. Nii saab paigutada (alla) serva
juhtribad ning joonistuskomponendi venitada üle muu pinna, nii et rakend oleks
ühtlaselt kaetud.
Alumised
kerimisribad koos nende sisu selgitavate siltidega paigutati omaette paneeli.
Nii saab valmis ehitatud ploki pärast tervikuna rakendi allserva paigutada.
Paneeli paigutushalduriks määrati GridLayout kolme rea ja kahe veeruga. Ning
edasi paigutati sinna järgemööda sisse nii kirjeldavad sildid kui ribad ise.
public Pildike5(){
setLayout(new BorderLayout());
Panel p1=new Panel(new GridLayout(3, 2));
p1.add(new Label("Maapinna
kiirus")); p1.add(sbtaustasamm);
p1.add(new Label("Putuka liikumistundlikkus"));
p1.add(sbputukatundlikkus);
p1.add(new Label("Lillede
sagedus")); p1.add(sblilletoenaosus);
add(p1, BorderLayout.SOUTH);
add(louend, BorderLayout.CENTER);
louend.addKeyListener(this);
sbtaustasamm.addAdjustmentListener(this);
sbputukatundlikkus.addAdjustmentListener(this);
sblilletoenaosus.addAdjustmentListener(this);
}
Asukohtade
arvutamisel on juurde tulnud putuka liikumise arvutus. Nii nagu maapind, nii ka
putukas liigub üldjuhul ühtlase kiirusega. Enne putuka sammu võrra liikumist
kontrollitakse, kas uus soovitav asukoht asub liikumiseks sobiliku pinna sees.
Meetodi esimesed kaks koordinaati tähistavad uuritavat kohta, viimased kaks
lubatud ala laiust ja kõrgust. Eeldatakse, et ala vasak ja ülemine serv
hakkavad nullist.
boolean sees(double x, double y, double x2,
double y2){
if(x>=0 && x<x2 &&
y>=0 && y<y2) return true;
return false;
}
Kui aga juhtub, et putukas on sattunud lubatud ala serva
lähedale ning edasi pole võimalik liikuda, siis seatakse ta liikumiskiirus
(sammu pikkus ühe joonistuse jooksul) nulliks.
void arvutaAsukohad(){
nihe=nihe+samm;
aeg=aeg+samm;
if(nihe>mustripikkus)nihe=nihe-mustripikkus;
if(sees(putukax+putukaxkiirus, putukay+putukaykiirus,
laius-putukalaius,
korgus-putukakorgus)){
putukax+=putukaxkiirus;
putukay+=putukaykiirus;
} else {
putukaxkiirus=putukaykiirus=0;
}
}
Reageering klahvivajutusele on mõnevõrra muutunud. Kui
ennist muutus vajutusel putuka asukoht, siis nüüd püütakse muuta ta liikumise
kiirust. Näitena näha vasak nooleklahv. Kui putukas liikus ennist vasakule (st.
putukaxkiirus oli nullist väiksem) sel juhul klahvile vajutades vasakule
liikumise kiirus kasvab kiirusesammu võrra, ehk kiirusest lahutatakse
kiirusesammu väärtus. Kui aga putukas juhtus enne paigal seisma või paremale
liikuma, siis määratakse uueks kiiruseks -kiirusesamm, ehk sama palju, kui
muidu iga vajutamisega kiirust juurde tuleb. Nõnda ei pea kiiruse suuna
muutmisel liialt aega kulutama pidurdamisele, vaid võib küllalt järsku teises
suunas liikuma hakata, nagu putuka lennu puhul ikka näha võib.
public void keyPressed(KeyEvent e){
int kood=e.getKeyCode();
if(kood==KeyEvent.VK_LEFT){
if(putukaxkiirus<0)putukaxkiirus-=kiirusesamm;
else putukaxkiirus=-kiirusesamm;
}
//...
}
Juurde tuli mõningane statistika, et kasutajal oleks näha,
kaugele ta mänguga jõudnud on. Kulunud aja arvutamiseks küsitakse käivitamise
ajal eraldi muutujasse alghetke väärtus. Tegemist küllalt suure ja väheütleva
arvuga: millisekundeid alates aastast 1970.
long alghetk=new Date().getTime();
Kui nüüd aga millalgi uuesti süsteemi käest aeg küsida, siis
nende kahe väärtuse vahe ütleb, palju mängu algusest aega kulunud on.
void joonistaStatistika(){
String st="Lilli: "+lilli+"
Imetud: "+imetud+
" Kadunud: "+kadunud+"
Aeg: "+(new Date().getTime()-alghetk)/1000;
piltg.setColor(Color.white);
piltg.drawString(st, 30, korgus-10);
}
Rakendusele pandi kaasa heli. Pidevalt korduv linnutaust, et
veidigi tekiks mulje niidul lendavast putukast ning märku andev sahin, kui
putukas lillest nektari kätte sai. Taustaga on lihtsam. Kui esimest korda
joonistama asutakse, siis palutakse muutujasse laadida linnutaust ning käsu
loop abil pannakse too korduvalt end ketrama.
if(linnutaust==null){
linnutaust=laeKlipp("linnutaust.au");
linnutaust.loop();
}
Lille tabamise sahina saaks ka lihtsamal juhul laadida ning
käsuga play mängima panna. Kui aga lilli palju ning sahinaheli vähegi pikem,
siis kipuvad järjestikustel tabamustel helid kattuma ning üksikud tabamused ei
eristu kõrvale ilusti ja loendatavalt. Sahinahelide eristamiseks loodi omaette
klass. Lähem kirjeldus näha juba kommentaarides.
/**
*
Lõimeklass, mille abil saab meloodiajuppe mängida. Sünkroniseerimise abil
hoolitsetakse,
*
et mängualguste vahel oleks vähemalt poolesekundiline paus. Luku
*
(loa)ga sünkroniseeritud plokki pääseb korraga vaid üks lõim. Ülejäänud
*
peavad selle ees ootama, kuni eelmine on plokist väljunud.
*/
class
Piiksuja extends Thread{
static Object lukk=new Object();
static java.applet.AudioClip sahin;
public void run(){
synchronized(lukk){
sahin.play();
try{Thread.sleep(500);}catch(Exception
e){}
}
}
}
Nagu näha, on Piiksuja nii lukk kui sahin staatilised
muutujad. See tähendab, et neile on võimalik ligi pääseda sõltumata klassi
eksemplaride arvust. Lukuobjekt luuakse programmi käivitamisel ning sama isend
on kättesaadav kõigile klassi isenditele - nõndamoodi saab selle järgi
sünkroniseerimisel otsustada, et korraga ei juhtuks mitu sahinat mängima.
Piiksuja sahin laetakse koos muude klippidega ja paigutatakse sinna staatilisse
muutujasse.
if(Piiksuja.sahin==null)
Piiksuja.sahin=laeKlipp("sahin.au");
Kui nüüd käivitamise juures jõutakse niikaugele, et on paras
aeg sahistada, siis luuakse lõimeklassist Piiksuja uus eksemplar ning palutakse
start abil eraldi lõimes ta run käivitada.
if(!l.kasTyhi() &&
(xkaugus*xkaugus+ykaugus*ykaugus)<kauguseruut){
l.paneTyhi(true);
imetud++;
new Piiksuja().start();
}
Kui eelmist
mängijat pooleli pole, siis asutakse pea kohe mängima. Kui aga eelmine lõime
eksemplar peatub luku abil sünkroniseeritud ploki sees, siis jääb uus mängija
järjekorda, kuni eelmine on oma töö lõpetanud ning plokist väljunud. Kui nüüd
liikuda putukaga läbi tiheda lillesalu, võib järgemööda eristatult kuulda mitut
sahinat - iga tabatud lille kohta üht.
synchronized(lukk){
sahin.play();
try{Thread.sleep(500);}catch(Exception
e){}
}
Nagu koodist näha, pole rakendiklassil Pildike5 enam
main-meetodit. Lähem seletus klassi kommentaarides.
/**
*
Alamklass liikuvate lilledega rakendile Pildike5. Meetod laeKlipp on siin üle
kaetud,
*
sest rakendil saadakse heliklipi andmed käsust getAudioClip, rakendusel aga
samaotstarbeliseks
*
käsuks Applet.newAudioClip. Viimane kuulub aga JDK koosseisu alates versioonist
1.2 ning
*
varasema versiooni seilurid annavad neile tundmatu meetodi sisse lugemisel
klassi kohta veateate
*
ning keelduvad vastava klassiga edaspidi tegelemast kartes turvamuresid. Kui
aga vastav meetod
*
siin üle katta, siis ülemklass on sellest meetodist prii ja võib rahus rakendis
töötada,
*
käsurealt käivitades aga võetakse siinse alamklassi üle kaetud käsklus ning
pannakse
*
tööle.
*/
import
java.awt.*;
import
java.awt.event.*;
import
java.applet.*;
import
java.io.*;
class
Pildike5Raam extends Pildike5{
AudioClip laeKlipp(String failinimi){
try{return Applet.newAudioClip(new
File(failinimi).toURL());}catch(Exception e){}
return null;
}
public static void main(String
argumendid[]){
Frame f=new Frame("Pildiraam");
Pildike5Raam ap=new Pildike5Raam();
f.add(ap);
f.setSize(300, 400);
f.setVisible(true);
ap.start();
f.addWindowListener(
new WindowAdapter(){
public void windowClosing(WindowEvent
e){
System.exit(0);
}
}
);
}
}
Nõnda on tehtud muudatused/täiendused üle vaadatud ning
rakenduse terviku kood loodetavasti arusaadav.
import
java.applet.*;
import
java.awt.*;
import
java.awt.event.*;
import
java.util.*;
public
class Pildike5 extends Applet implements Runnable, KeyListener,
AdjustmentListener{
Image taust;
Image pilt;
AudioClip linnutaust;
Graphics piltg;
Image lill, lill2;
int lillekorgus=100, lillelaius=50;
Image putukas;
double putukax=100, putukay=150;
double putukaxkiirus=0, putukaykiirus=0;
double kiirusesamm=0.2;
int putukalaius=35, putukakorgus=35;
int imemiskaugus=10;
int mustripikkus=160;
double samm=1;
int paus=50;
double nihe=0;
double aeg=0;
int laius=300, korgus=300;
int lilli=0, imetud=0, kadunud=0;
long alghetk=new Date().getTime();
Vector lillekesed=new Vector();
double lillelisamistoenaosus=0.02*samm;
Canvas louend=new Canvas(){
public boolean isFocusTraversable(){
return true;
}
};
Scrollbar sbtaustasamm=new
Scrollbar(Scrollbar.HORIZONTAL, 10, 5, 0, 100);
Scrollbar sbputukatundlikkus=new
Scrollbar(Scrollbar.HORIZONTAL, 2, 1, 1, 20);
Scrollbar sblilletoenaosus=new
Scrollbar(Scrollbar.HORIZONTAL, 20, 1, 0, 100);
boolean veel=false;
public Pildike5(){
setLayout(new BorderLayout());
Panel p1=new Panel(new GridLayout(3, 2));
p1.add(new Label("Maapinna
kiirus")); p1.add(sbtaustasamm);
p1.add(new Label("Putuka
liikumistundlikkus")); p1.add(sbputukatundlikkus);
p1.add(new Label("Lillede
sagedus")); p1.add(sblilletoenaosus);
add(p1, BorderLayout.SOUTH);
add(louend, BorderLayout.CENTER);
louend.addKeyListener(this);
sbtaustasamm.addAdjustmentListener(this);
sbputukatundlikkus.addAdjustmentListener(this);
sblilletoenaosus.addAdjustmentListener(this);
}
public void adjustmentValueChanged(AdjustmentEvent
e){
samm=sbtaustasamm.getValue()/10.0;
kiirusesamm=sbputukatundlikkus.getValue()/10.0;
lillelisamistoenaosus=sblilletoenaosus.getValue()/1000.0*samm;
louend.requestFocus();
}
public void joonista(){
koostaPilt();
louend.getGraphics().drawImage(pilt, 0, 0,
this);
}
void laeKlipid(){
if(taust==null)taust=laePilt("rohetaust320x480.gif");
if(putukas==null)putukas=laePilt("sirelane.gif");
if(lill==null)lill=laePilt("lill1a.gif");
if(lill2==null)lill2=laePilt("lill1.gif");
if(pilt==null){
pilt=createImage(laius, korgus);
piltg=pilt.getGraphics();
}
if(Piiksuja.sahin==null)
Piiksuja.sahin=laeKlipp("sahin.au");
if(linnutaust==null){
linnutaust=laeKlipp("linnutaust.au");
linnutaust.loop();
}
korgus=louend.getSize().height;
laius=louend.getSize().width;
louend.requestFocus();
}
void arvutaAsukohad(){
nihe=nihe+samm;
aeg=aeg+samm;
if(nihe>mustripikkus)nihe=nihe-mustripikkus;
if(sees(putukax+putukaxkiirus,
putukay+putukaykiirus,
laius-putukalaius,
korgus-putukakorgus)){
putukax+=putukaxkiirus;
putukay+=putukaykiirus;
} else {
putukaxkiirus=putukaykiirus=0;
}
}
void koostaPilt(){
if(taust==null)laeKlipid();
arvutaAsukohad();
eemaldaVanadLilled();
lisaUusiLilli();
kontrolliImemisi();
piltg.drawImage(taust, 0,
(int)nihe-mustripikkus, this);
joonistaLilled();
joonistaStatistika();
piltg.drawImage(putukas, (int)putukax,
(int)putukay, this);
}
void lisaUusiLilli(){
for(double lt=lillelisamistoenaosus;
lt>0; lt=lt-1){
if(Math.random()<lt){
lillekesed.addElement(new
Lilleke4((int)((laius-lillelaius)*Math.random()), (int)aeg));
lilli++;
}
}
}
void eemaldaVanadLilled(){
for(int i=0; i<lillekesed.size(); i++){
Lilleke4
l=(Lilleke4)lillekesed.elementAt(i);
if(aeg-l.algaeg>korgus+lillekorgus){
if(!l.tyhi)kadunud++;
lillekesed.removeElementAt(i);
i--;
}
}
}
void kontrolliImemisi(){
int kauguseruut=imemiskaugus*imemiskaugus;
for(int i=0; i<lillekesed.size(); i++){
Lilleke4
l=(Lilleke4)lillekesed.elementAt(i);
int lilley=(int)aeg-l.algaeg-lillekorgus/2;
int xkaugus=(int)putukax-l.x;
int
ykaugus=(int)putukay-(lilley-lillekorgus/2);
if(!l.kasTyhi() &&
(xkaugus*xkaugus+ykaugus*ykaugus)<kauguseruut){
l.paneTyhi(true);
imetud++;
new Piiksuja().start();
}
}
}
void joonistaLilled(){
for(int i=0; i<lillekesed.size(); i++){
Lilleke4
l=(Lilleke4)lillekesed.elementAt(i);
Image
lillepilt=(l.kasTyhi())?lill2:lill;
piltg.drawImage(lillepilt, l.x,
(int)aeg-l.algaeg-lillekorgus, this);
}
}
void joonistaStatistika(){
String st="Lilli: "+lilli+"
Imetud: "+imetud+ " Kadunud: "+kadunud+" Aeg: "+(new
Date().getTime()-alghetk)/1000;
piltg.setColor(Color.white);
piltg.drawString(st, 30, korgus-10);
}
public void start(){
veel=true;
new Thread(this).start();
if(linnutaust!=null)linnutaust.loop();
}
public void run(){
while(veel){
joonista();
try{Thread.sleep(paus); }catch(Exception
e){}
}
}
public void stop(){
veel=false;
linnutaust.stop();
}
boolean sees(double x, double y, double x2,
double y2){
if(x>=0 && x<x2 &&
y>=0 && y<y2) return true;
return false;
}
public void keyPressed(KeyEvent e){
int kood=e.getKeyCode();
if(kood==KeyEvent.VK_LEFT){
if(putukaxkiirus<0)putukaxkiirus-=kiirusesamm;
else putukaxkiirus=-kiirusesamm;
}
if(kood==KeyEvent.VK_RIGHT){
if(putukaxkiirus>0)putukaxkiirus+=kiirusesamm;
else putukaxkiirus=kiirusesamm;
}
if(kood==KeyEvent.VK_UP){
if(putukaykiirus<0)putukaykiirus-=kiirusesamm;
else putukaykiirus=-kiirusesamm;
}
if(kood==KeyEvent.VK_DOWN){
if(putukaykiirus>0)putukaykiirus+=kiirusesamm;
else putukaykiirus=kiirusesamm;
}
}
public void keyReleased(KeyEvent e){}
public void keyTyped(KeyEvent e){}
Image laePilt(String failinimi){
try{
return getImage(getCodeBase(),
failinimi);
}catch(Exception e){}
return
Toolkit.getDefaultToolkit().getImage(failinimi);
}
AudioClip laeKlipp(String failinimi){
return getAudioClip(getDocumentBase(),
failinimi);
}
}
Esimesed lilled |
Kaks imetud, kaks
silmist mööda läinud |
Tihedaks määratud
lilleväli
Loodud rakendus küll töötab ning on mõningase
näpuharjutusena mängitav, kuid pole ta veel ei terviklik mäng ega saa seda
kuigivõrd asjalikult ka mõneks muuks otstarbeks kasutada. Olemasolevat koodi
vähemalt osalt mõistes on võimalik küllalt vähese vaeva abil koodi toimimist ja
rakenduse väljanägemist täiesti märkimisväärselt muuta. Lihtsaimaks asenduseks
ehk pildid ja heliklipid. Kui joonistada või leida muru ja lillede abil miskit
muud, siis annab kergesti kokku panna koristaja maanteel või elevandi
portselanikaupluses. Üks üliõpilane aga kujundas piltide muutmise abil sellest
samast näitest aga näiteks küllaltki verise sõjamängu.
Kui näidet
mänguks kujundada, siis tõenäoliselt tuleb üsna pea lisada punktidearvutus ja
mängu lõpp, ehk ka tasemed. Punkte kannatab kuvada statistikafunktsiooni veidi
muutes. Iseseisva rakenduse puhul annab vahetulemusi faili salvestada. Rakendi
puhul on turvapiirangute tõttu see veidi keerulisem, kuid kel PHP või muu
veebiserveripoolse programmeerimisvahendiga kokkupuuteid ja kasutusvõimalusi,
siis on ka see täiesti tehtav.
Ilus
võistlusmoment tekib, kui ühe putuka asemel lendleb kaks, kummagi juhtimiseks
omaette klahvid. Ning samuti võib saabuvaid pilte olla mitut tüüpi. Nii
magusaid mesikaid, õiteta okaspuid kui muude vahele ära eksinud kärbsepabereid.
Näide ei
pea aga mitte ainult mängu aluseks olema. Mõningase muutmise teel saab selle
põhjal koostada katseseadme pidurdusteekonna pikkuse või molekulide kaootilise
liikumise näitamiseks. Kõige rohkem läheb vaja pealehakkamist.
Sirelasemäng
Alustuseks on kasutada mängu põhi
· Tutvu mänguga
· Lae lähtekood, pildid ja helilõigud oma arvutisse, kompileeri ja käivita.
· Otsi Internetist pilte ning kujunda nende abil mäng oma silma järgi.
· Lisa punktidearvutus.
· Loo mängule lõpp.
· Luba samaaegselt liikuda kahel mängijal
Klahvidega liigutamine.
· Nooleklahvidega saab liigutada ekraanil olevat ringi.
· Ekraanil juhuslikus kohas paikneb rist. Jõudes ringiga selleni, hüppab rist uude juhuslikku kohta.
· Liigutatavaid ringe on kaks, kummalgi oma klahvid.
· Risti asemel on pisike pilt. All servas on kirjas, mitu korda kumbki mängija on ristini jõudnud.
Tennis
· Võrguga tenniseväljakul liigub pall (ring) vasakult paremale.
· Samaaegselt ringi liikumisega saab ekraanil liigutada reketit.
· Kummalgi serval on liigutatav reket, mille tabamisel pall tagasi põrkab. Loetakse punkte.
Ussimäng
· 10*10 ruudustikul asub vasakul ülaservas neljalüliline uss, keda saab nooleklahviga paremale liigutada.
· Uss liigub pidevalt, nooleklahvidega saab tema liikumise suunda määrata. Seinani jõudmisel uss seiskub.
· Ussiga püütakse juhuslikus kohas asuvat kuldmuna. Muna kättesaamisel uss pikeneb kahe lüli võrra ning muna tekib uude kohta. Seina või enese hammustamisel uss lüheneb nelja lüli võrra.
Tilgapüüdja
· All servas saab liigutada kaussi
· Ekraanil kukub tilk. Selle kaussi püüdmisel saab punkti.
· Tilku võib korraga kukkuda mitu. Mäng on meeldivalt kujundatud ning töötab märgatavate vigadeta.
Nooltega ralli.
· Noolte abil saab muuta ekraanil liikuva ringi kiirust.
· Külgmiste nooltega saab muuta liikumise suunda, teiste nooltega kiirust.
· Korraga võib üle võrgu sõita kaks kasutajat. Aetakse taga juhuslikus kohas paiknevat aaret, mis selleni jõudmisel hüppab uude kohta.