2007-07-31 01:15:14
Včera večer nám zase při bouřce vypadl proud a jelikož jsem měl vybitý notebook, tak mi nezbylo nic jinýho než relaxovat u PDAčka. Zkoušel jsem ve SmallBasicu naprogramovat něco na způsob známého spořiče Mystifikace, který bývá standardní součástí Windows. Určitě ho znáte - po obrazovce lítají pospojované čáry (tvořící mnohoúhelníky) a odráží se od krajů.
Úloha vychází z klasického zadání, kdy se po ploše pohybuje bod a odráží se od stěn obrazovky. Akorát tady se těch bodů pohybuje více a ještě k tomu jsou propojeny dohromady a tvoří jakýsi "n-úhelník" (tedy přesněji řečeno n bodů je propojeno přímkami tak, že bod Bi je spojen s bodem Bi+1 s tím, že poslední bod Bn je zase spojen s prvním bodem B1)

Svého času to býval celkem typický příklad na různých programátorských soutěžích. Úlohu jsem si ještě zpestřil tím, že jsem přidal možnost zadat počet bodů, které budou tvořit obrazec (konstanta edges může teoreticky nabývat hodnot od 2 do nekonečna, ale při číslech větších než 100 se ten mnohoúhelník nestíhá plynule překreslovat). Další vychytávkou je, že každý bod při odrazu získá náhodnou rychlost, takže body se mohou pohybovat různě rychle.
Obrazec je v paměti realizován 2 rozměrným polem, kde
A(i,1) udává polohu na ose X,
A(i,2) polohu na ose Y,
A(i,3) udává směr, kterým poletí
A(i,4) udává rychlost (což je vlastně o kolik pixelů se posune během jednoho iteračního cyklu)
Směry, kterými mohou body létat jsou jenom čtyři, jelikož od stěn se odráží pod úhlem 45°. Na obrázku jsou vyznačeny červeně. Jakmile se obrazec odrazí, směr se změní. (o to se stará řádek 41-50).

Změna směru po odrazu je řešena poněkud zvláštně. Pokud se dostane bod k hranici nebo za hranici plochy, přičte se k proměnné A(i,3) určitá konstanta. Možná vše osvětlí tato tabulka:
| Okraj, od kterého se odrazí | Podmínka, kterou se to zjistí | Jaký byl směr? | Jaký bude následovat směr? | O kolik se změní? |
|---|---|---|---|---|
| Horní | x <= 0 | 1 | 3 | +2 |
| 2 | 4 | |||
| Dolní | x >= XMAX | 3 | 1 | -2 |
| 4 | 2 | |||
| Vpravo | y <= 0 | 2 | 1 | -1 |
| 4 | 3 | |||
| Vlevo | y >= YMAX | 1 | 2 | +1 |
| 3 | 4 |
Z tabulky vyplývá, že nemusím řešit, jaký byl předchozí směr, abych mohl vypočítat směr nový. Stačí zjistit, do jaké stěny bod narazil a pak přičíst příslušnou hodnotu z posledního sloupce. Letěl-li nějaký bod například směrem vlevo nahoru (směrem č. 1) a narazí do horního okraje obrazovky, přičtu hodnotu +2 a bod po odrazu poletí směrem vlevo dolů (tedy směr č. 3). Možná to někomu připadá zbytečně složité, ale zminimalizuje to složité podmínky, jelikož je mi úplně jedno, jakým směrem se bod pohyboval před odrazem.

Zajímavý problém nastává pokud se bod dostane do rohu plochy, to pak může měnit směry, jak chce, a zůstane tam viset, takže to řeším tak, že při každém odrazu se bod posune o pixel vedle, aby se něco takového nestávalo... (tato záplata řeší i extrémní případ, kdy se při inicializaci stane, že náhodně rozmístěný bod bude na kraji a zárověň náhodně získá směr, který by normální cestou nikdy získat nemohl...)
Na začátku programu jsou kromě konstanty edges, která určuje počet hran, také dvě předvolby min_speed a max_speed. Určují minimální a maximální rychlost pohybu bodu, což je podstatě o kolik pixelů se nejméně, nebo nejvíce budou moci sunout. Rychlost získá bod při odrazu, bude to náhodně vybrané číslo přesně z tohoto intervalu. O to se stará procedura change_speed, která je definována na 8. řádku.
Samotné vykreslování je řešeno tak, že se prvně smaže předchozí obrazec, potom se přepočítá nová poloha, a pak se nakreslí nový přepočítaný obrazec, chvíli se počká a pak furt dokola.
Ještě bych se zastavil u toho, jakým způsobem je docíleno, že poslední bod je spojen přímkou s tím prvním. Je to řešeno zbytkem po celočíselném dělení, kdy se vždy spojí bod Bi s bodem B(i MOD edges)+1, kde edges je celkový počet bodů.
Tak a tohle je kód:
CONST edges = 4
CONST min_speed = 6
CONST max_speed = 30
DIM a(1 TO edges, 1 TO 4)
SUB change_speed(BYREF x)
x = floor(rnd *(max_speed-min_speed))+min_speed
END
'------------------------------- initialize
FOR i = 1 TO edges
a(i,1) = FLOOR(RND * (XMAX-6))+1 'x
a(i,2) = FLOOR(RND * (YMAX-6))+1 'y
a(i,3) = FLOOR(RND * 3)+1 'direction
change_speed a(i,4) 'speed
NEXT i
WHILE 1
'--------------------------- undrawing lines
FOR i = 1 TO edges
next_edge = ((i % edges) + 1)
LINE a(i,1), a(i,2), a(next_edge,1), a(next_edge,2) COLOR 15
NEXT i
'--------------------------- moving + rebounding
FOR i = 1 TO edges
FOR j = 1 TO a(i,4)
'--- moving
IF f a(i,3) = 1 THEN
a(i,1)=a(i,1)-1 : a(i,2)=a(i,2)-1
ELIF a(i,3) = 2 THEN
a(i,1)=a(i,1)+1 : a(i,2)=a(i,2)-1
ELIF a(i,3) = 3 THEN
a(i,1)=a(i,1)-1 : a(i,2)=a(i,2)+1
ELIF a(i,3) = 4 THEN
a(i,1)=a(i,1)+1 : a(i,2)=a(i,2)+1
FI
'--- rebound = change of direction
IF a(i,1) <= 0 THEN
a(i,3) = a(i,3) + 1: a(i,1) = 1 : change_speed a(i,4)
ELIF a(i,1) >= XMAX THEN
a(i,3) = a(i,3) - 1: a(i,1) = XMAX - 1 : change_speed a(i,4)
ELIF a(i,2) <= 0 THEN
a(i,3) = a(i,3) + 2: a(i,2) = 1 :change_speed a(i,4)
ELIF a(i,2) >= YMAX THEN
a(i,3) = a(i,3) - 2: a(i,2) = YMAX - 1: change_speed a(i,4)
FI
NEXT j
NEXT i
'--------------------------- drawing lines
FOR i = 1 TO edges
next_edge = ((i % edges) + 1)
LINE a(i,1), a(i,2), a(next_edge,1), a(next_edge,2) COLOR 0
NEXT i
DELAY 100
WEND
Netvrdím, že je to úplně bezchybnaté, určitě by se to dalo ještě nějak optimalizovat. Zdroják můžete sami otestovat i v interpretu SmallBasicu pro Windows, který lze stáhnout na Smallbasic.sourceforge.net. Určitě přijdete na nějaké vylepšení, do komentářů s ním...