1 CLDC, MIDP – verze a současnost Vydávání a revize takzvaných JSR (Java Specification Request), což jsou finální uvolněné specifikace pro platformu Java zajišťuje skupina JCP (Java Community Process), založená v roce 1998.
1.1 CLDC V současnosti existují dvě verze CLDC a to původní 1.0 (JSR-30) a novější 1.1 (JSR-139). Existuje ještě verze 1.0.4, ta však byla použita celkem pouze asi v sedmi zařízeních firmy Samsung, tudíž se o ní nebudu zmiňovat. Starší verze, CLDC 1.0, obsahuje, jak již bylo uvedeno definice pro VM, sítě, bezpečnost a základní knihovny. Podrobnější specifikace lze nalézt na internetu [4]. Verze 1.1 se příliš neliší, přidává však velmi důležitou věc a to sice podporu datových typů s plovoucí desetinnou čárkou (float, double). Ty v CLDC 1.0 neexistovaly, protože nebyly hardwarově podporovány. V praxi se tudíž používala (a používá) metoda „fixed point“, což znamená, že v programu jsou všechna čísla vynásobená např. 103 (od toho název fixed point – pevná desetinná čárka/přesnost), díky čemuž lze do 3 nejnižších řádů ukládat údaje o desetinných místech. V případě tisku na výstup je pak potřeba číslo správně zobrazit. Dalšími změnami v CLDC 1.1 jsou tzv. „weak references“1 nebo zvýšení minimální paměti ze 160kB na 192kB.
1.2 MIDP Tak jako u CLDC i u MIDP existují v současnosti dvě verze – 1.0 (JSR-37) a 2.0 (JSR-118). I zde existuje ještě meziverze MIDP 2.1, ta je ale teprve uváděna na trh a vyplňuje jakousi mezeru mezi MIDP 2.0 a dlouho očekávaným MIDP 3.0. MIDP 1.0 implementuje základní rozhraní pro práci s GUI, podporu sítí, RMS (Record Management System) – systém pro perzistentní ukládání dat a další. V praxi se však ukázalo, že trpí vážnými nedostatky: nemožnost přístupu k jednotlivým pixelům, nulová podpora pro zvuky a fullscreen (celoobrazovkové) zobrazení, sítě pouze pro bezestavový protokol HTTP či neexistující fronta pro stisk kláves. Tyto nedostatky řešily různá proprietární API výrobců telefonů, jejichž použitím se však aplikace stala nepřenositelnou. A tak 5. listopadu 2002 byla uvolněna specifikace MIDP verze 2.0, přidávající třídy: • javax.microedition.media – třídy pro podporu zvuku a videa, jsou podtřídou MMAPI 1
Více o weak references na Wikipedii: http://en.wikipedia.org/wiki/Weak_reference
-1-
(více v kapitole 7.1) • javax.microedition.lcdui.game – zvláštní API pro usnadnění vývoje 2D her – sprity, tile manažer, vrstvy, rozšířený canvas (zabudovaný double buffer, klávesová fronta) • javax.microedition.pki – API pro práci s certifikáty Dalšími přidanými vlastnostmi je například tzv. „push registr“, který umožňuje spouštění midletu při iniciaci příchozího spojení či sdílení perzistentního úložiště dat mezi aplikacemi. Profil MIDP 3.0 (JSR-271) je zatím stále ve vývoji, nicméně obsahuje mnoho novinek a proto by byla chyba ho zde opomenout. Protože MIDP zařízení jsou mnohem výkonnější než před lety a mají mnohem více paměti, vznikl tlak na požadavek zavedení běhu několika aplikací najednou, respektive běh midletů na pozadí. Dále je zde možnost spouštění midletů hned po startu zařízení, specifikuje komunikační kanál mezi midlety, vylepšuje UI, zavádí podporu více displejů, IPv6 a podstatně rozšiřuje možnosti připojení k síti. Stadium vývoje je možné sledovat na stránkách organizace JCP [5]. Verze CLDC a MIDP jsou na sobě nezávislé a mohou se libovolně kombinovat, tudíž existují zařízení jak s kombinací CLDC 1.1 a MIDP 2.0, tak i CLDC 1.0 a MIDP 2.0.
1.3 Současnost mobilních zařízení V současnosti jsou nejrozšířenějšími MIDP zařízeními mobilní telefony. Vývoj v nich stačil urazit už relativně dlouhou cestu, vždyť první telefony podporující Javu – Motorola i80s a i50sx – byly uvedeny na trh již v dubnu roku 2001. Černobílý displej s na tu dobu velkým rozlišením – 119x64, 256kB heap paměti, 50kB omezení na JAR soubor a žádný zvuk - to jsou parametry i80s. Dnes je nejrozšířenější platformou patrně Series 40 od firmy Nokia, která ve svých už pěti revizích zahrnuje širokou škálu zařízení, od nejstarších, jako je Nokia 2355 se standardním displejem o rozlišení 128x128 až po poslední modely typu Nokia 6301 s QVGA displejem (240x320). Tato platforma představuje dnes jakýsi minimální standard a proto se na ní také primárně vyvíjí aplikace, které se následnou portací pouze rozšiřují. Opačný postup by byl totiž násobně obtížnější a tím pádem i dražší. Standardem je velikost JAR archivu 64kB, heap paměť kolem 200kB u nejslabších modelů, 20kB místa na RMS a standardní rozlišení 128x96 (128x128 u fullscreen módu s použitím Nokia API).
2 Sestavení a nasazení aplikace Abychom mohli aplikaci distribuovat, nestačí nám jen přeložit zdrojové soubory. Poté co máme aplikaci napsanou a přeloženou, je potřeba class soubory preverifikovat. Jak již bylo zmíněno, ve „velké“ Javě se toto děje až při spouštění aplikace a stará se o to virtuální stroj -2-
(JVM), v MIDP ale potřebujeme svést co nejvíce těžké práce na počítač na kterém sestavujeme aplikaci. Po preverifikaci nám zbudou upravené class soubory, které konečně můžeme zabalit do JAR archivu. Svou strukturou se jedná o běžný archiv typu ZIP. V něm může být jeden či více midletů a jejich pomocných tříd, zdroje v podobě dalších souborů (texty, obrázky, apod.) a nakonec tzv. MANIFEST soubor. Ten musí být v JARu v adresáři META-INF a jedná se o textový soubor podobný deskriptoru. Deskriptor aplikace je textový soubor s příponou JAD a obsahuje popis soupravy MIDletů ve formátu atribut:hodnota. Atributů je relativně mnoho - některé jsou povinné (jméno aplikace, velikost v bytech, URL JAR souboru atd.) a některé ne. Programátor si může dokonce definovat vlastní, které lze v aplikaci číst metodou Midlet.getAppProperty(String atribut). Hlavní činnost deskriptoru je při stahování aplikací do telefonu, protože poskytuje všechny důležité informace ještě předtím, než se přenese samotná aplikace. Jelikož sestavování aplikace je komplikovaný a náročný proces, vznikly nástroje které ho zjednodušují. Od různých stupňů podpory automatizace ve vývojových prostředích (WTK, EclipseME, Netbeans – u těchto nástrojích pojednává kapitola 8) až po skripty.
2.1 Ant, Antenna a J2ME Jak již bylo řečeno, vývoj aplikací na mobilní zařízení a to především využívající multimédií a různých specifik daných modelů kladou velké požadavky na vývojáře. Desítky různých odlišností ve specifikacích zařízení vyžadují portaci (úpravu) aplikace na každý model zvlášť. Aby se vývoj zjednodušil a zefektivnil, vznikl Ant. Ant (respektive jeho odnož pro J2ME – Antenna1) je systém pro sestavování aplikací na základě XML skriptu. Podpora Antu (potažmo Antenny) je obvykle integrována ve většině vývojových nástrojů cílených na jazyk Java. Co
vlastně
Ant
umí:
základní
operace
se
soubory
a
adresáři
(vytvoření/přejmenování/mazání), spouštění externích programů, preprocessing (úprava kódu ještě před samotným překladem - jako v jazyce C), build, preverifikace, obfuskace, zabalení, vytvoření deskriptoru, spuštění emulátoru a další. Pro bližší informace v češtině je výborný zdroj [2]. Příklad 4.1 Ukázka Antenna skriptu <project name="TheGame" default="build" basedir=".">
1
Domovská stránka: http://antenna.sourceforge.net
-3-
<property name="wtk.home" value="C:\WTK22" /> <property name="wtk.midpapi" value="${wtk.home}\lib\midpapi.zip" /> <property name="midlet.name" value="TheGame" />
<wtkjad jadfile="${midlet.name}.jad" jarfile="${midlet.name}.jar" name="${midlet.name}"> <midlet name="${midlet.name}" class="Main" /> <wtkbuild srcdir="src_prep" destdir="classes" target="1.1" source="1.2" preverify="false" /> <wtkpackage jarfile="${midlet.name}.jar" jadfile="${midlet.name}.jad" obfuscate="true" preverify="true" basedir="classes" libclasspath="res" /> <wtkrun jadfile="${midlet.name}.jad" device="Nokia_S40_DP20_SDK_1_1" wait="true"/>
2.2 Bajtkód a obfuskace Zdrojový kód Javy se překládá do zvláštního instrukčního jazyka zvaného bajtkód (Bytecode). Díky tomu je možné jednou přeloženou aplikaci do bajtkódu spustit na libovolném virtuálním stroji (VM) Javy na téměř libovolné platformě (VM musí pro tuto platformu samozřejmě existovat), přenositelnost bez rekompilace je jedním ze základních principů jazyka Java. Všechny VM Javy jsou tak napůl interprety a napůl překladače. Nevýhodou bajtkódu je jeho 100% dekompilatovatelnost. S využitím vhodného decompileru (např. DJ Java Decompiler1) je možné z class souboru získat zpět zdrojový kód včetně všech názvů tříd, proměnných atd. Jednou z možností jak tomu zabránit, je tzv. obfuskace. Obfuskace je proces, při kterém program, obfuskátor (obfuscator – nejznámější: ProGuard, RetroGuard, JShrink) provede s bajtkódem několik operací: odstraní přebytečné (nepoužité) proměnné, metody a třídy, optimalizuje bajtkód, ale především „ořízne“ veškeré uživatelské názvy tříd, proměnných, metod atd. Takto upravený class soubor je až o 30% menší než původní. Toho se již standardně užívá právě u mobilních aplikací, protože nejenom znemožňuje dekompilaci (respektive znepřehlední dekompilovaný kód), ale zkrácením jmen také výrazně zmenší velikost výsledných class souborů. Ukázka metody před
1
Domovská stránka Decompileru: http://members.fortunecity.com/neshkov/dj.html
-4-
protected final void startApp() throws MIDletStateChangeException { display = Display.getDisplay(this); splash = new SplashScreen(); display.setCurrent(splash); splash.init(); gameCanvas = new MyGameCanvas(); gameCanvas.init(this); splash.run(); splash = null; menu = new Menu(this); menu.show(0); }
a po obfuskaci (zůstaly zachovány jen názvy tříd a výjimek z API) protected final void startApp() throws MIDletStateChangeException { a = Display.getDisplay(this); b = new g(); a.setCurrent(b); b.a(); d = new e(); d.a(this); b.b(); b = null; c = new c(this); c.a(0); }
2.3 Životní cyklus midletu Spouštění a zavádění MIDletu třídou javax.microedition.midlet.MIDlet probíhá v několika krocích. Po požadavku OS na spuštění se zavolá bezparametrický konstruktor třídy a je vytvořena nová instance. Aplikace se nachází v tzv. přerušeném stavu. Když je vše připraveno, metoda startApp() uvede midlet do stavu aktivního. Ten označuje normální běh MIDletu. Zavoláním metody notifyDestroyed() a následným spuštěním destroyApp(boolean unconditional) je možné MIDlet uvést do stavu zrušení a fakticky ho ukončit. V běžícím stavu je možno ho také přerušit a to buď z programu metodou notifyPaused() nebo se o to postará správce aplikací (např. při příchozím hovoru). V obou případech je zavolána metoda pauseApp(). Zpět do aktivního stavu MIDlet vrátí resumeRequest(). Všechny metody měnící stavy je možné (a vhodné) překrýt a postarat se v nich o alokaci/dealokaci zdrojů.
Obr. 4.1 Životní cyklus midletu, zdroj: přednášky Vybrané partie z jazyka Java, Petr Hnětynka, UK
2.4 Vysoko- a nízko-úrovňová API GUI (Graphics User Interface – grafické uživatelské rozhraní) se v MIDP skládá z tzv. -5-
vysokoúrovňových a nízkoúrovňových API.
2.4.1 Vysokoúrovňové API Vysokoúrovňové API jak už z názvu vyplývá operuje vysoko nad komponentami grafických a uživatelských rozhraní, je tedy více abstraktní. Jeho obsahem je základní balík grafických komponent podobný knihovně AWT1 z J2SE. Programátor nemá moc možností jak ovlivnit vzhled aplikace a programování vypadá ve stylu metod nakresliTlacitko() nebo coSeVybraloZMenu(). Barvy, fonty a celý vzhled aplikace je závislý na implementaci rozhraní výrobcem zařízení. Tím pádem může stejná aplikace na různých zařízeních vypadat různě, avšak právě díky tomu je zaručena absolutní přenositelnost mezi všemi mobilními informačními zařízeními implementujícími daný profil. Mnoho práce tak za nás obstarává již implementace obsluhy GUI v zařízení. Programování je pak nejen jednodušší, ale z hlediska uživatele je výsledek mnohem přívětivější, protože uživatel je „navyklý“ na určitý sjednocený grafický vzhled. Některé součásti, jako je například scrolling, uživatelský vstup, navigace apod. programátor vůbec neřeší, jsou totiž automaticky obstarávány zařízením. Následuje stručný popis tříd spadajících pod vysokoúrovňové API. Screen – základní třída reprezentující „obrazovku“; pro představu lze použít přirovnání k jedné statické webové stránce: může obsahovat následující prvky List – výběrový seznam; ekvivalent HTML prvků checkbox a radio TextBox – textové vstupní pole; ekvivalent input type=text Alert – dočasné okno s upozorněním Form – formulář; obalový prvek; může obsahovat další formulářové prvky: posuvníky, vstupní pole, texty, obrázky, seznamy apod.
2.4.2 Nízkoúrovňové API Nízkoúrovňové API je pak naprostý opak. Důraz je zde kladen na téměř nejnižší přístup ke komponentám zařízení (displej, tlačítka, stylus). Míra abstrakce je zde maximálně potlačena a metody vypadají typově jako nastavBarvuPixelu() nebo jeStisknutaKlavesa(). Dává nám tak maximální přístup k zařízení, avšak právě kvůli tomu se aplikace obvykle stávají závislými na zařízení a následně nepřenositelnými. Pro multimediální aplikace jsou důležitá obě rozhraní. Vysokoúrovňová pro implementaci navigací, menu, formulářových vstupů apod. a nízkoúrovňová pro samotný
1
Článek o grafických rozhraní v J2SE: http://www.neo.cz/~tomas/java.net/2004/05/swing-versus-swt.html
-6-
výkonný kód. Jelikož klademe důraz na multimediální možnosti mobilních zařízení, zaměříme se pouze na nízkoúrovňová API.
3 MIDP a 2D grafika V současné jsou grafické aplikace postaveny téměř výhradně na třídách Canvas a Image ze standardního balíčku javax.microedition.lcdui, případně na jejich kamarádkách z MIDP 2.0 – GameCanvas, Sprite, Layer a dalších z balíku javax.microedition.lcdui.game. Pro začátek věnujeme tedy kousek prostoru těm méně známým a méně používaným rozhraním pro práci s 2D grafikou.
3.1 M2G Mobile 2D Graphics (M2G) známý také jako Scalable 2D vector Graphics API (JSR-226) je nepovinný balíček doplňující profil MIDP. Slouží zásadně pro práci s vektorovou grafikou, jejíž přednosti oproti rastrové grafice jsou především velikost souborů (u mobilních zařízení podstatná výhoda) a snadná možnost transformace (velikosti, natočení atd.), protože vektorový formát je nezávislý na rozlišení. Rozhraní používá otevřený formát SVG-Tiny1, což je oficiální úprava „velkého“ formátu SVG2, který nepodléhá žádné licenci a je volně šiřitelný. Setkat se s ním lze například na webu, přičemž sám o sobě umožňuje i animace. Třídy a rozhraní lze nalézt v balíku javax.microedition.m2g, popř. org.w3c.dom.svg.
3.2 Canvas a ti ostatní v MIDP 1.0 Třída Canvas z balíku javax.microedition.lcdui je základní třídou pro aplikace vyžadující přímý přístup k displeji zařízení. Kromě zobrazování se stará také o zpracování vstupu od uživatele – poskytuje rozhraní k zachytávání událostí stisku kláves. Každá klávesa má přiřazen konstantní kód, pomocí kterého lze snadno zjistit o kterou klávesu se jedná. Tyto kódy jdou definovány pro standardní ITU-T telefonní klávesnici. Příklad 5.1 Ukázka čtení kláves protected void keyPressed(int keyCode) { if(keyCode > 0) switch(keyCode) { case Canvas.KEY_NUM4: // '4' // stisknuta klávesa s číslem 4 break;
1
Domovská stránka: http://www.w3.org/TR/SVGMobile/
2
Domovská stránka: http://www.w3.org/TR/SVG/
-7-
case Canvas.KEY_NUM6: // '6' // stisknuta klávesa s číslem 6 break; // ...
Canvas poskytuje také vstupní rozhraní k herním aplikacím. Zde jsou definovány abstraktní akce typu NAHORU, DOLEVA apod. Abstraktní jsou proto, že konstanta UP (nahoru) může na různých klávesnicích znamenat různé klávesy, čímž je ale zaručena přenositelnost. Implementace pak závisí na výrobci telefonu. Obsahem Canvasu jsou i metody na obsluhu stylusu v případě, že jej zařízení podporuje. Instancí této třídy (nebo tříd zděděných, což je obvyklejší) může být při běhu víc. Aktivní (viditelná) však může být v jeden okamžik maximálně jedna. O předávání řízení se v tomto případě starají metody showNotify a hideNotify, které zaregistrují instanci k zachytávání vstupních událostí. Standardně je zobrazovací plochou Canvasu celý displej, nicméně v některých implementacích může být část displeje vyhrazena na nabídku příkazů (třída Command) nebo na titulek. Proto byla v MIDP 2 přidána metoda setFullScreenMode, která explicitně nastaví, zda má třída pokrývat celou plochu displeje. V MIDP 1 bylo toto chování řešit proprietátními API, typicky pro telefony Nokia bylo potřeba při požadavku na fullscreen použít namísto standardního Canvasu třídu FullCanvas z balíku com.nokia.mid.ui. O samotný obsah toho co se má vykreslit se stará metoda protected abstract void paint(Graphics g), kterou je třeba překrýt. Kontext, do kterého se bude kreslit je předán v parametru g. Důležité je však vědět, že samotné vykreslení se neděje voláním této metody, ale
zprostředkovaně
a
to
sice
požadavkem
z midletu
na
překreslení
Display.getDisplay().setCurrent() nebo ze sebe sama pomocí metody repaint() popř. systémového volání serviceRepaints(), které se liší tím, že pozastaví ostatní systémové akce jako čtení kláves, stylusu apod. a provede pokud možno okamžité překreslení. Třída Graphics je základní třídou pro vykreslování 2D primitiv. Podporovány jsou 24-bitové barvy (RGB - 3x8 bitů) a škála metod pro základní kreslení – čar, obdélníků, polygonů, textu, zakřivených čar a dalších. To, zda se na barevnou škálu použije všech 24 bitů záleží samozřejmě na cílovém zařízení. Obecně však lze s touto barevnou hloubkou pracovat, pak je totiž úkolem koncového zařízení provést potřebnou konverzi. Graphics je víceméně grafickým kontextem, který se získá od požadované plochy na kterou chceme kreslit (obrazovka, off-screen) a získává se pomocí Image.getImage(), pokud chceme kreslit do offscreen obrázku (ten musí být vytvořen jako „muttable“, viz dále) nebo je předáván jako povinný parametr v metodě Canvas.paint(), pokud chceme kreslit přímo na displej. -8-
Souřadný systém je „zleva-doprava“ a „shora-dolů“, pixel v horním levém rohu má koordináty [0;0]. Zajímavé možná je, že koordináty neznamenají jednotlivé pixely, ale mřížku mezi nimi. Nicméně použití je jednoznačné. Jak zmiňuje dokumentace, ukázkový fillRect(0, 0, 3, 2) vyplní samozřejmě předpokládanou oblast 3x2 pixely. Bohužel, při testování jsem se přesvědčil, že někteří výrobci telefonů si dokumentaci vykládají různě a na některých zařízeních ten samý kód kreslil obrázek o pixel nahoru či dolů oproti ostatním. Nastavení barvy pro kreslení všech čar a textů provádí metoda setColor(int RGB), přičemž parametr lze zapsat obvyklým hexadecimálním zápisem 0x00RRGGBB, u čar lze navíc nastavit styl čáry pomocí setStrokeStyle(int style), v aktuální verzi MIDP 2.0 zatím jen ve dvou stylech – plná čára a tečkovaně. Tloušťka čáry je vždy jeden pixel. Clipping (ořezávání) je metoda sloužící k nastavení obdélníkové oblasti v objektu Graphics, do níž smí grafický kontext zakreslovat. Používá se převážně k optimalizaci vykreslování, kdy uzamčení části plochy a kreslení do jednoho místa je rychlejší, než když zařízení musí celou plochu zpřístupnit k vykreslování. Další výhodou je možnost tvorby výřezů (viz. příklad).
Příklad 5.2 Metoda nahrazující drawRegion z MIDP 2 pro MIDP 1 // Parametry // g – cílový kontext do kterého se má kreslit // img – zdrojový obrázek // xSrc, ySrc, width, heigth – hranice výřezu ze zdrojového obrázku // xDst, yDst, anchor – umístění výřezu v cíli public static void drawRegion(Graphics g, Image img, int xSrc, int ySrc, int width, int height, int xDst, int yDst, int anchor) { /* MIDP 1.0 */ g.setClip(xDst, yDst, width, height); g.drawImage(img, xDst - xSrc, yDst - ySrc, anchor); /* MIDP 2.0 g.drawRegion(img, xSrc, ySrc, width, height, 0, xDst, yDst, anchor); */ }
Ořezy nelze kombinovat a vytvářet tak složitější útvary. Každý objekt Graphics může mít jen jeden ořez. K nastavení se používá metoda setClip(). Zarovnání (anchor point) se používá při zobrazení obrázků a textů. Pomocí bitového OR lze snadno nastavit, zda vzhledem k zadanému bodu se má grafika vykreslit nalevo, napravo, uprostřed, nahoře či dole. Tato vlastnost slouží čistě k usnadnění práce programátora a zpřehlednění kódu.
Příklad 5.3 Vykreslení obrázku doprostřed displeje klasickou metodou a s využitím anchor // klasicky g.drawImage(img, resolution.x/2 - obrX/2, resolution.y/2 – obrY/2, 0); // a s anchor point
-9-
g.drawImage(img, resolution.x/2, resolution.y/2, Graphics.HCENTER | Graphics.VCENTER);
Třída Image z balíku javax.microedition.lcdui je určena k uchování grafických rastrových dat. Objekt typu Image může vzniknout dvojím způsobem a podle toho se typově dělí
na mutable a
immutable.
Obrazy mutable (měnitelné)
vznikají
zavoláním
Image.createImage(int width, int height). Tato metoda vytvoří prázdný obraz o rozměrech width x height do kterého lze po získání kontextu Graphics kreslit, kopírovat fragmenty či celé další obrázky. Narozdíl od toho obrazy typu immutable (neměnné) nelze po jejich vytvoření a iniciování měnit ani na ně nijak kreslit. Typicky se jedná o proměnné Image vytvořené nahráním ze zdrojového souboru či bloku dat v paměti. Více viz dokumentace na http://java.sun.com. Máme-li však požadavek na editaci grafiky získané ze souboru, lze immutable obrázek snadno přeměnit na muttable pomocí jednoduchého kódu, ostatně jak také zmiňuje dokumentace:
Příklad 5.4 Snadná konverze immutable objektu na objekt mutable Image source; // zdrojový obrázek source = Image.createImage(...); Image copy = Image.createImage(source.getWidth(), source.getHeight()); Graphics g = copy.getGraphics(); g.drawImage(source, 0, 0, TOP|LEFT);
Pozn.: častou chybou bývá získávání kontextu např. při každém průchodu cyklem. Nejenom že se kód citelně zpomalí, ale především bude nefunkční, protože získaný kontext je při každém volání getGraphics() jiný. Následující ukázkový kód nebude fungovat korektně: img.getGraphics().setColor(0x00FF0000); // nastavení barvy štětce na červenou img.getGraphics().drawLine(0, 0, 10, 10); // čára se vykreslí implicitní barvou (černou)
narozdíl od správného: Graphics g = img.getGraphics(); g.setColor(0x00FF0000); // nastavení barvy štětce na červenou g.drawLine(0, 0, 10, 10); // čára se již vykreslí červeně (pokud nemáme černobílý displej :)
Standardním obrazovým formátem je bezeztrátový kompresní formát PNG (Portable Network Graphics1) ve verzi 1.0. Podpora dalších typů je závislá na libovůli výrobce zařízení. Mezi obvyklé patří ještě GIF a BMP, nicméně oproti PNG nepřináší žádnou výhodu – BMP jako nekomprimovaný formát je datově příliš veliký pro nasazení na mobilních zařízeních, GIF je pak omezen na škálu maximálně 256 barev a navíc podléhá tvrdé licenční politice.
Průhlednost
1
specifikace: http://www.w3.org/TR/REC-png.html
- 10 -
Neměnné (immutable) obrazy mohou obsahovat kromě neprůhledných a plně průhledných i poloprůhledné pixely (narozdíl od mutable, které nemohou mít průhledný byť jediný pixel). Implementace Javy na konkrétním zařízení musím umět pracovat s plnou nebo žádnou průhledností. Podpora částečné průhlednosti (alpha blending) je opět implementačně závislá na výrobci a nelze se tedy na její podporu vždy spolehnout. Od MIDP 2 lze alespoň zjistit, kolik úrovní průhlednosti zařízení podporuje metodou Display.numAlphaLevels(). Vrácená hodnota bude vždy alespoň 2, větší číslo znamená podporu částečné průhlednosti.
3.3 Uživatelské rozhraní v MIDP Abstraktní třída Displayable je základním elementem zobrazitelným na displeji. Obsahuje v sobě mechanismus pro práci s příkazy (Commands). Protože třída je definována jako abstraktní, zaměříme se na její potomky. Displayable má dva přímé potomky: Canvas a Screen. Canvas, česky plátno, je zástupce low-level API, Screen, česky obrazovka, je zástupcem high-level API.
Obr. 5.1 Hierarchie tříd grafického uživatelského rozhraní MIDP zdroj: JAVA™ MIDP APPLICATION DEVELOPER’S GUIDE FOR NOKIA DEVICES [6] Příkazy (Commands) jsou mechanismem pro obsluhu vysokoúrovňových událostí. Filozofie práce s nimi je taková, že objektu, který je potomkem Displayable, případně potomkem Screen se přiřadí libovolný počet příkazů (Command), každý určeného typu a s určitou prioritou. Protože obsluha těchto příkazů probíhá obvykle pomocí kontextových kláves, kterých je omezené množství (obvykle dvě), pomáhá určení typu a priority sdělit
- 11 -
aplikačnímu rozhraní, které příkazy má zobrazit přímo a které se „schovají“ v podnabídce. O následnou obsluhu událostí generovaných těmito příkazy se stará metoda commandAction() z rozhraní CommandListener. Problematika událostí a jejich obsluhy je poměrně široká a není hlavní náplní této práce, případné zájemce lze odkázat na vyčerpávající dokumentaci [7] nebo výbornou příručku [6] začínajících vývojářů od fy Nokia, byť už trochu staršího data.
Příklad 5.5 Ukázka použití Commands v praxi public class Main extends MIDlet implements CommandListener { // základní proměnné private List sampleList; // seznam private Command exitCmd = new Command("Exit", Command.EXIT, 1); // příkaz private Command backCmd = new Command("Back", Command.BACK, 1); // příkaz // inicializace při spuštění aplikace public void startApp() { sampleList = new List("Samples", List.IMPLICIT); // inicializace seznamu for (int i = 0; i < SAMPLES.length; i++) { // naplnění seznamu SAMPLES[i].getDisplayable().addCommand(exitCmd); SAMPLES[i].getDisplayable().addCommand(backCmd); SAMPLES[i].getDisplayable().setCommandListener(this); sampleList.append(SAMPLES[i].getName(), null); } sampleList.addCommand(exitCmd); sampleList.setCommandListener(this); Display.getDisplay(this).setCurrent(sampleList); } // metoda na zachytávání akcí public void commandAction(Command command, Displayable displayable) { if (command == exitCmd) { // příkaz „konec aplikace“ destroyApp(true); notifyDestroyed(); } else if (command == backCmd) { // příkaz „krok zpět“ Display.getDisplay(this).setCurrent(sampleList); // zobrazení seznamu } else if (command == List.SELECT_COMMAND) { // zobrazení požadované třídy } } }
3.4 GameCanvas a ti ostatní v MIDP 2.0 S příchodem MIDP 2 se objevily i nové třídy určené primárně k usnadnění vývoje herních aplikací. Jak již bylo uvedeno, bezdrátová zařízení jsou tvrdě limitována výpočetním výkonem a pamětí, proto každý přesun kódu do API knihoven znamená zvýšení výkonu. Game API totiž nepřidává téměř nic, co by nebylo možná napsat pomocí MIDP 1, pouze přenáší obvykle používané konstrukce do standardních knihoven. Metody měnící vlastnosti (pozice, viditelnost...) následujících objektů nemají žádný přímý viditelný výsledek. Hodnoty jsou uloženy uvnitř objektů a změna se projeví až při - 12 -
překreslení metodou paint(). Tento přístup je vhodný právě pro herní aplikace, kde herní cyklus sestává obvykle ze sekvence čtení vstupu-výpočet-vykreslení. Celé API je balíkem pěti tříd: GameCanvas – podtřída třídy Canvas poskytuje základní rozhraní pro zobrazovací funkce. Obsahuje všechny zděděné metody a navíc přidává frontu událostí vstupního zařízení tak jako je tomu v desktopových OS (v MIDP 1 se metody pro čtení stavu kláves musely „trefit“ do okamžiku kdy klávesa byla zrovna stisknuta; pokud byla smyčka aplikace příliš dlouhá a uživatel stiskl tlačítko velmi krátce, mohlo dojít k tomu, že se stisk vůbec nezaznamenal) a nativní podporu pro double buffer. Třída je vytvořena děděním a je zpětně kompatibilní, lze tedy používat všechny konstrukce známé z normálního low-level API. Layer – abstraktní třída reprezentující viditelný objekt; je definována pozicí, rozměry a viditelností; dědí od ní třídy Sprite a TiledLayer LayerManager – zastřešující třída pro objekty typu Layer; usnadňuje vykreslování většího počtu objektů a poskytuje podporu pro snadné vytváření a úpravy pohledu na vykreslovanou scénu; vloženým objektům přiřazuje úroveň vnoření a tím řeší problematiku viditelnosti – jakýsi zjednodušený z-buffer Sprite – potomek Layer; reprezentuje základní grafický prvek; umožňuje vytváření animací a jednoduchou rotaci či zrcadlení; primární vlastnosti a dovednosti: • vytváření frame-sekvencí; obvyklá délka sekvence je rovna počtu framů, je však možné si nadefinovat vlastní sekvenci s libovolným množstvím framů, které se mohou opakovat, vynechávat jednotlivé snímky či vytvářet reverzní animaci; každý Sprite může obsahovat jen jednu libovolně dlouhou sekvenci • referenční pixel; chceme-li vykreslit Sprite v bodě [x;y], znamená to, že od tohoto bodu se vykreslí levý horní roh; toto chování lze změnit nastavením referenčního pixelu na libovolný bod v oblasti Spritu • transformace; existují zde jen výpočetně nenáročné transformace – otáčení při kroku 90° a/nebo zrcadlení • kolize; Sprite umožňuje vyhodnocování kolizí mezi Sprity, TiledLayer nebo objektem Image a to buď na úrovni kolizního obdélníku nebo metodou pixel-by-pixel, kde je detekována kolize pixelů s nulovou průhledností TiledLayer – třída pro vytváření čtvercových map pomocí tzv. „tiles“ (dlaždic); obvyklá technika pro vytváření různých pozadí; protože mobilní zařízení jsou paměťově výrazně omezená, není možné jako pozadí/mapu použít velký obrázek, proto se používá metoda, kdy
- 13 -
jsou do čtvercové sítě kladeny jednotlivé buňky, které na sebe motivem navazují a vytvářejí pocit jednolitého pozadí; princip je podobný jako u Sprite – jednotlivé framy mají svůj index a podle matice definující pozadí jsou skládány vedle sebe; jednotlivé buňky mohou být také animovány
Obr. 5.2 Matice pozadí a výsledný obraz, zdroj: dokumentace WTK [8]
Příklad 5.6 Animace rotace Zeměkoule pomocí Sprite a pomocí MIDP 1 // proměnné private Sprite _sprite1; // sprite podle MIDP1 (Sprite je třída ve standardním balíku private javax.microedition.lcdui.game.Sprite _sprite2; // sprite podle MIDP2 // řídící proměnná pro _sprite1 int _actualFrame=0; // inicializace _sprite1 _sprite1=new Sprite("/earth.png", 50, 50); // inicializace _sprite2 Image spriteImage=null; try { spriteImage = Image.createImage("/earth.png"); } catch(Exception e) { e.printStackTrace(); } _sprite2=new javax.microedition.lcdui.game.Sprite(spriteImage, 50, 50); // vykreslování // _sprite1 _gBuffer.drawImage(_sprite1.getFrame(_actualFrame++), getWidth(), 0, Graphics.TOP|Graphics.RIGHT); if(_actualFrame>=_sprite1.m_numFrames) _actualFrame=0; // _sprite2 _sprite2.paint(_gBuffer); _sprite2.nextFrame();
4 MIDP a 3D grafika 4.1 OpenGL ES OpenGL ES (JSR-239) se nachází ještě ve stádiu vývoje, nicméně už je implementován na některá výkonná zařízení. Je určen především pro vývojáře kteří znají OpenGL na PC.
- 14 -
4.2 M3G 4.2.1 Pozadí vzniku Vzhledem ke vzrůstajícímu výkonu mobilních zařízení se postupem času došlo k závěru, že jsou již připravena pro implementaci 3D rozhraní. Prvním byl renderovací engine Mascot Capsule na telefonech firmy SonyEricsson. Jen pro zajímavost, jeho výkon je i dnes obvykle vyšší než dále zmíněné M3G. Na jeho základě vznikla expertní komise předních vývojářů pro mobilní zařízení, která měla za úkol vytvořit standardizované API pod hlavičkou JCP. Tak 19. listopadu 2003 vznikla první finální specifikace JSR-184, nazvaná Mobile 3D Graphics API (M3G API). To je zaměřeno na zařízení s malým výpočetním výkonem a pamětí a která nemají hardwarovou podporu 3D grafiky (myšleno GPU). Implementace rozhraní může být celá dokonce čistě softwarová. Pokud však zařízení má k dispozici specializované hardwarové prostředky pro rychlejší výpočty, API je umí využít. Při návrhu se bralo v potaz i předpokládané využití – hry, vizualizace map, spořiče obrazovek, graficky bohatá uživatelská rozhraní a další. Z těchto rozdílných požadavků nakonec vzešlo řešení dvou různých úrovní přístupu – retained a immediate mode, viz níže. API vyžaduje podporu čísel s desetinnou čárkou, respektive CLDC 1.1.
4.2.2 Obecná teorie Systém M3G používá pravotočivý souřadný systém. 3D prostor je orientován tak, že horizontální posun značí osa x, vertikální osa y a posun v hloubce osa z. Toto rozložení používají oba majoritní 3D zobrazovací systémy i na platformě PC (Direct3D, OpenGL). Definice se někdy rozchází v tom, zda kladný přírůstek na ose z znamená přiblížení ke kameře (pozorovateli) nebo naopak oddálení. V M3G je osa z orientována (má kladný přírůstek) směrem ke kameře. Objekty v běžných 3D zobrazovacích systémech reprezentují shluky polygonů (núhelníků). Tyto polygony lze pak rozložit na menší jednotky – trojúhelníky, které tvoří nejmenší možnou zobrazitelnou plochu. Vrcholy každého trojúhelníku jsou body v prostoru definované třemi [X, Y, Z] souřadnicemi. Tyto vrcholy se nazývají vertexy.
- 15 -
Obr. 6.1 View frustum, zdroj: IBM developerWorks [9]
Již byl zmíněn pojem kamera. Kamera je okem pozorovatele. Je dána svojí pozicí, úhlem natočení (ve všech třech osách), pozorovacím úhlem (viewing angle) a řeznými rovinami. Pozorovací úhel a řezné roviny definují v prostoru jakýsi čtyřstěnný jehlan se seříznutou špicí (view frustum) a všechny objekty které se budou nacházet uvnitř tohoto jehlanu budou zobrazeny na výsledném zobrazovacím zařízení (v našem případě obvykle LCD mobilního zařízení).
Mobile 3D Graphics API
MIDP
CLDC 1.1
Obr. 6.2 vztah M3G, MIDP a CLDC
4.2.3 API Veškeré třídy a rozhraní se nacházejí v balíku javax. microedition.m3g. V aplikaci lze zjistit dostupnost rozhraní zavoláním System.getProperty("microedition.m3g.version"), které vrátí číslo verze, respektive null, pokud API není přítomno. Krom základních tříd jsou v balíku přítomny třídy popisující materiálové vlastnosti, klíčování animací, meshe, textury, systémy hierarchie scény a další. Důležité třídy, tak jak jsou popsány v dokumentaci [10]:
- 16 -
• Appearance – soubor komponent popisujících 3D objekt (textura, materiál, kompozice...) • Camera – uzel grafu scény popisující pozici a polohu pozorovatele a parametry projekce (pozorovací úhel, perspektiva...) • Graphics3D – grafický kontext (třída je jedináček – singleton, získává se statickou metodou getInstance()), nastavuje cíl renderingu, jeho parametry (antialiasing, dithering...) a realizuje samotné vykreslování pomocí jedné ze čtyř metod render (2 pro retained mode a 2 pro immediate mode) • Image2D – rastrový obrázek pro použití jako textura, pozadí nebo sprite; může být mutable i immutable (viz. Sprite v kapitole 5.4) • Loader – třída obsahující dvě statické metody pro vytváření známých datových struktur – obecně Object3D (což zahrnuje World, Image2D a další) • Mesh – uzel grafu scény popisující 3D objekt složený z polygonů • Node – abstraktní třída obecného uzlu • Object3D - abstraktní třída; základ všech objektů umístitelných do 3D prostoru • Transform – obecná transformační matice 4x4 • World – úložiště pro graf scény; potomek třídy Group nejvyšší úrovně (kořen scény) Scene graph1 Graf scény je stromovou strukturou - místo kde se dělí větev se nazývá uzel (node). Uzel na vrcholu této struktury se nazývá kořen (World node). Uzlem mohou být meshe (objekty), sprity, světla a celé skupiny těchto objektů zabalené do uzlu pomocí obalové třídy Group. S těmi se pak zachází jako s jedním objektem, což je výhodné pro skupiny prvků které spolu nějak logicky souvisejí. Přestože World je také typu uzel, nemůže být sám o sobě jeho součástí, tzn. World musí stát vždy na vrcholu pyramidy. Výhoda použití grafu scény je, že s malým programovacím úsilím lze snadno zobrazit komplexní scénu. Stromový typ grafu klade však některá omezení. Jeden uzel (node) může náležet vždy maximálně do jedné skupiny (Group). Graf musí být větvený, tzn. je třeba se vyhnout zacyklení větví.
1
Obecně o scene graph v encyklopedii Wikipedia: http://en.wikipedia.org/wiki/Scene_graph
- 17 -
Obr. 6.3 restrikce ve stromu grafu, zdroj: Mobile 3D Graphics API Specification [3]
Důležité je také přidělování a správa takzvaných „user ID“ jak uvidíme později na příkladu. Ty slouží ke rychlé a jednoznačné identifikaci objektů ve scéně. V modelovacím nástroji je možné používat vícero kamer než je jen jedna. Ty se vyexportují společně se zbytkem scény a lze mezi nimi snadno přepínat právě třeba pomocí „user ID“. Scene graph se snadno vytvoří exportem z běžného 3D modelovacího nástroje (3D Studio MAX1 – ve verzi 8 má exporter zabudován, Blender2 – pro export existuje Python skript3 atd.) a to včetně světel, textur a kamer. Zde prezentovaný příklad byl vytvořen právě pomocí open source nástroje Blender. Strukturu je samozřejmě možné vytvořit i programově, za běhu.
1
Domovské stránky 3D Studia MAX: http://www.autodesk.com/3dsmax
2
Domovské stránky Blenderu: http://www.blender.org
3
Blender Export for J2ME: http://www.nelson-games.de/bl2m3g/default.html
- 18 -
Obr. 6.4 Ukázka struktury grafu, zdroj: Mobile 3D Graphics API Specification [3]
4.2.4 Immediate a retained mode M3G obsahuje dva zobrazovací módy. Immediate, což je nízkoúrovňové API, založené na derivátu OpenGL ES1), používá se k zobrazování základních prvků – vertexů, trojúhelníků, jednotlivých světel atd. Umožňuje absolutní kontrolu nad zobrazovanou scénou, příkazy jsou okamžitě vyhodnoceny a zpracovány grafickou jednotkou. Oproti tomu retained mode představuje abstraktnější vysokoúrovňové API, kde se využívá především importu celých scén vytvořených externími modelovacími programy. Tyto scény v sobě obsahují stromové grafy sestávající z uzlů - meshů (logické celky objektů), systémů světel, virtuálních kamer a dalších objektů, ke kterým se v programu snadno přistupuje přes jednoznačný identifikátor (ID) jak již bylo výše řečeno. Standard dále definuje formát dat modelů, včetně texturování a systému animací. Oba módy lze kombinovat a používat najednou, avšak při použití retained módu a zavolání render(World) se použijí světla a kamera definovaná ve World, ignorují se tedy všechna předchozí nastavení. Toto neplatí při vykreslení pomocí render(Node, transform). Popsání alespoň hlavních mechanismů by zabralo více než je obsah celé této práce. Pro co nejrychlejší a nejsrozumitelnější zasvěcení zde budou proto uvedeny pečlivě okomentované příklady.
Příklad 6.1 Kostka, retained mode // ...inicializace // získání instance g3d = Graphics3D.getInstance(); // nahrání scény ze souboru Object3D[] parts=null; try { parts = Loader.load("/cube.m3g"); } catch(Exception e) { e.printStackTrace(); } _world = (World) parts[0]; // prvek na nultém indexu je vždy hlavní objekt World // získání odkazu na kostku (má definováno userID=12) // pro pozdější snazší manipulaci _cube=(Mesh)_world.find(12); // získání a nastavení kamery _camera = _world.getActiveCamera(); // nastavení projekce float[] lProjection = {0.0f, 0.0f, 0.0f, 0.0f}; _camera.getProjection(lProjection); float aspect = (float)getWidth()/(float)getHeight();
1
Domovské stránka OpenGL ES: http://www.khronos.org/opengles
- 19 -
// nastavení prespektivy _camera.setPerspective(
lProjection[0], // úhel pohledu aspect, // poměr stran lProjection[2], // první ořezávací rovina lProjection[3]); // druhá ořezávací rovina _world.setActiveCamera(_camera);
// ... // a vykreslení _cube.postRotate(2.0f,0.0f,0.0f,1.0f); // rotace kostky try { g3d.bindTarget(g); // nastavení cíle (g je odkaz na grafický kontext) g3d.render(_world); // render } finally { g3d.releaseTarget(); // uvolnění cíle }
Příklad 6.2 Kostka, immediate mode // definice proměnných; pozn.: pouze výňatky // vertexy private static final byte[] VERTEX_POSITIONS = { -1, -1, 1, 1, -1, 1, -1, 1, 1, 1, 1, 1, // front 1, -1, -1, -1, -1, -1, 1, 1, -1, -1, 1, -1, // back ... }; // indexy vertexů pro vytvoření triangle stripů private static final int[] TRIANGLE_INDICES = { 0, 1, 2, 3, // přední stěna 4, 5, 6, 7, // zadní stěna ... }; // délky triangle stripů v TRIANGLE_INDICES private static int[] TRIANGLE_LENGTHS = { 4, 4, 4, 4, 4, 4 }; // koordináty k namapování textury private static final byte[] VERTEX_TEXTURE_COORDINATES = { 0, 1, 1, 1, 0, 0, 1, 0, // přední 0, 1, 1, 1, 0, 0, 1, 0, // zadní ... }; // inicializace (zjednodušená) // získání instance graphics3d = Graphics3D.getInstance(); / vytvoření pozadí s barvou _background = new Background(); _background.setColor(0xFF113366); / vytvoření vertex bufferu _cubeVertexData = new VertexBuffer(); / vytvoření a nastavení pole vertexů VertexArray vertexPositions = new VertexArray(VERTEX_POSITIONS.length/3, 3, 1); VertexPositions.set(0, VERTEX_POSITIONS.length/3, VERTEX_POSITIONS); _cubeVertexData.setPositions(vertexPositions, 1.0f, null); / to samé pro textury VertexArray vertexTextureCoordinates = new VertexArray(VERTEX_TEXTURE_COORDINATES.length/2, 2, 1); VertexTextureCoordinates.set(0, VERTEX_TEXTURE_COORDINATES.length/2, VERTEX_TEXTURE_COORDINATES); cubeVertexData.setTexCoords(0, vertexTextureCoordinates, 1.0f, null); // vytvoření polygonů (trojúhelníků) tvořících kostku _cubeTriangles = new TriangleStripArray(TRIANGLE_INDICES, TRIANGLE_LENGTHS); // vytvoření a nastavení kamery Camera camera = new Camera(); float aspect = (float) getWidth() / (float) getHeight(); camera.setPerspective(30.0f, aspect, 1.0f, 1000.0f);
- 20 -
Transform cameraTransform = new Transform(); cameraTransform.postTranslate(0.0f, 0.0f, 10.0f); _graphics3d.setCamera(camera, cameraTransform); try { // vytvoření a nastavení textury Image2D image2D = (Image2D) Loader.load("/fel.png")[0]; // načtení ze zdroje _cubeTexture = new Texture2D(image2D); _cubeTexture.setBlending(Texture2D.FUNC_DECAL); // blending; viz. dokumentaci _cubeAppearance.setTexture(0, _cubeTexture); } catch (Exception e) { e.printStackTrace(); } // a vykreslení _cubeTransform.postRotate(2.0f, 0.0f, 1.0f, 0.0f); // rotace kostky (stejné jako u retained mode) try { _graphics3d.bindTarget(g); // nastavení cíle _graphics3d.clear(_background); // překreslení cíle pozadím _graphics3d.render(_cubeVertexData, _cubeTriangles, _cubeAppearance, _cubeTransform); // render } finally { _graphics3d.releaseTarget(); // uvolnění cíle }
4.2.5 Shrnutí M3G je vysokoúrovňové API a jako takové poskytuje rychlé výsledky. Pomocí modelovacího nástroje a několika řádek kódu je možné snadno vytvořit komplexní scénu. Na druhou stranu je prozatím výkon většiny současných přístrojů nedostačující na jakékoliv složitější scény. To je způsobeno především požadavkem na výpočty s plovoucí desetinnou čárkou, který pro uspokojivý běh vyžaduje matematický koprocesor. Stejně jako u 2D, i zde se však lze pomocí optimalizace dosáhnout rozumných výsledků.
5 Zvuky Podpora zvuku se v MIDP oficiálně objevila až od její druhé verze, nicméně už dřívější zařízení s podporou MIDP 1 byla schopna přehrávat zvuky s pomocí proprietárních API výrobců telefonů. Dnes na poli ozvučení existuje několik specifikací (standardů), které si následně rozebereme. Předtím by ještě bylo vhodné uvést několik málo poznámek o typech dat, jež je možno přehrávat. Defakto existují dvě hlavní skupiny lišící se principem, a to samply (datové proudy) a MIDI1. Hlavní rozdíl je ve způsobu uložení, zatímco samply (wav, amr, mp3 atd.) obsahují pomocí převodníků zdigitalizované zvuky, MIDI definuje pouze kombinaci čas-nota-rychlost-hraný nástroj. Přehrávající zařízení tedy kompletně rekonstruuje (generuje) skladbu. V oblasti grafiky bychom našli analogii – rastrová grafika (samply) versus vektorová grafika (MIDI). Z toho následně plynou výhody i nevýhody. Hlavní výhodou, 1
Musical Instrument Digital Interface: http://en.wikipedia.org/wiki/Musical_Instrument_Digital_Interface
- 21 -
zvláště v oblasti zařízení s malými pamětmi je velikost dat. Protože v midi jsou zaznamenány pouze noty, může i několikaminutová pasáž zabírat pouze několik kB paměti. Na druhou stranu je však zřejmé, že takto zahrát lze pouze na nástroje, jež zařízení podporuje. Z toho plyne, že na různých zařízeních může ta samá skladba znít různě, či se dokonce některé nástroje nemusí přehrát vůbec. MIDI též z principu není schopno přehrát nic jiného než hudbu, nedokáže tedy přehrát řeč, či různé ruchy, od toho jsou zde ostatní formáty.
5.1 Mobile Media API 5.1.1 Obecně MMAPI (JSR-135) obecně slouží k širšímu spektru akcí než je přehrávání zvuku. V závislosti na zařízení umožňuje též přehrávání videa nebo zachytávání zvuku či obrazu z videokamery. API bylo navrhováno jako vysokoúrovňové a dnes je v podstatě standardem, o čemž svědčí i seznam členů MMAPI Expert group: Nokia, Mitsibishi, Motorola, Philips, Siemens, Sun, Symbian, TI, Vodafone a další. Výsledkem jejich práce jsou ve skutečnosti dvě API: MMAPI a MIDP 2.0 Media API, které je pouze podmnožinou předchozího (je s ním tedy plně kompatibilní) a je zacíleno na zařízení s obecně sníženými schopnostmi multimediální reprodukce. Jak zmiňuje Lucie Rút Bittnerová v sérii článků „J2ME v kostce – jak na zvuk“ [11], MMAPI je součástí většiny současných zařízení s podporou J2ME – od telefonů až po PDA. API se nachází v balíku javax.microedition.media, u některých výrobců bylo ukryto pod vlastní balík (Siemens - com.siemens.mp.media). Protože mají mít knihovny záběr v široké paletě různorodých zařízení, bylo definováno jako vysokoúrovňové.
5.1.2 Základní koncept Architektura systému sestává ze tří hlavních částí: •
třídy Manager
•
rozhraní Player
•
rozhraní Control
- 22 -
Obr. 7.1 Architektura MMAPI, zdroj: developers.sun.com
Manager je přístupovým bodem ke všem možnostem přehrávání zvuku. Zodpovídá za dvě hlavní funkce: vytváření instancí Player a k přímému přehrávání tónů. Dále pak obsahuje metody pro zjištění podporovaných datových typů pro dané zařízení (povinná implementace je jen pro WAV a MIDI) metodou getSupportedContentTypes() a seznam podporovaných protokolů getSupportedProtocols(). Player je objekt, který je prostředníkem mezi zvukovým zdrojem (souborem) a jeho ovládacími prvky. Může být vytvořen třídou Manager buď ze zdroje typu InputStream, který je možno vytvořit například ze zdroje (resource) v JARu nebo z adresy URL. Po vytvoření metodou createPlayer() a zavolání start() se co nejrychleji alokují potřebné zdroje a spustí se přehrávání na pozadí, dokud skladba neskončí. To je nejjednodušší příklad použití. Podíváme se trochu hlouběji na kroky, které se provedou před samotným spuštěním skladby. Životní cyklus
vytvořeného
Player
sestává
z pěti
stavů:
UNREALIZED,
REALIZED,
PREFETCHED, STARTED a CLOSED. Jak se zmiňuje manuál [18], praktický význam těchto stavů je kvůli časové náročnosti jednotlivých kroků při požadavku na přehrání zvuku. Po zavolání createPlayer je Player v nerealizovaném stavu. Zavoláním realize() se stav změní na realizovaný a v případě, že zvuková data byla definována jako URL, dojde k jejich stažení. Nakonec zavoláním prefetch() dojde k připravenému stavu, kdy jsou alokována zvuková zařízení a Player je připraven k okamžitému přehrávání. Zavoláním start() se přehraje zvuk a stav se změní na běžící. Skončí-li přehrávání buď metodou stop() nebo tím, že se přehrávač dostane na konec skladby, přejde opět do stavu připravenosti. Uvolnění zdrojů se provede z kteréhokoli předchozího stavu zavoláním close(). Cyklus možná lépe vystihne schéma.
- 23 -
Obr. 7.2 stavy objektu Player a jeho přechody, zdroj: J2ME v kostce, jak na zvuk [11]
Zavoláním start() ze stavu UNREALIZED dojde k automatickému volání metod realize() a prefetch(), není tedy nutné všechny tyto metody exaktně volat. Pokud bychom vyžadovali zpětnou vazbu od přehrávače, je potřeba vytvořit třídu implementující rozhraní PlayerListener, respektive jeho metodu playerUpdate(Player player, String udalost, Object dataUdalosti) a tuto třídu u požadovaného přehrávače zaregistrovat. Ta se pak automaticky zavolá pokaždé, dojde-li k nějaké z událostí definovaných v rozhraní PlayerListener, například: konec skladby, vynucené stopnutí, změna hlasitosti, apod. Všechny události jsou podrobně rozepsány v dokumentaci [18]. Zajímavou pro praxi je schopnost přehrání několika zvukových vrstev současně. Tato vlastnost se bude jistě lišit přístroj od přístroje, nicméně emulátor i reálné zařízení typu Nokia series 40 toto zvládlo. Control je rozhraní pro implementaci ovládacích prvků objektu Player. Jedná se o předka, který má dva přímé potomky: •
ToneControl – užívá se pro přehrávání sekvencí jednoduchých tónů, více viz „Tónový generátor“
•
VolumeControl – slouží k nastavení hlasitosti
5.1.3 Tónový generátor Tónový generátor je důležitý pro zvukové aplikace jako jsou například hry. U velmi jednoduchých zařízení je to často jediný způsob jak z něj dostat zvuk. Vyvolat ho lze metodou Manager.playTone(int nota, int delka, int hlasitost). Pokud chceme přehrát celou sekvenci tónů (třeba krátkou písničku) je tento způsob nepohodlný. Lze využít vytvoření objektu Player s typem Manager.TONE_DEVICE_LOCATOR a následně přes rozhraní ToneControl nastavit sekvenci not třeba jako pole bytů, respektive datový typ audio/x-tone-seq1.
1
popsán v http://www.ietf.org/rfc/rfc2234
- 24 -
6 J2ME a vývojová prostředí 6.1 WTK Sun Java Wireless Toolkit (WTK1), známý také jako J2ME Wireless Toolkit, je základní sada nástrojů pro tvorbu aplikací na platformě J2ME. Obsahuje nástroje pro sestavení aplikace, pomocné utility a základní balík emulátorů. V aktuální verzi 2.5 podporuje OS Windows a OS Linux.
6.2 Eclipse Jak uvádí encyklopedie Wikipedia2, Eclipse3 je open source vývojová platforma, která je pro většinu lidí známá jako vývojové prostředí (IDE) určené pro programování v jazyce Java. Flexibilní návrh této platformy dovoluje rozšířit seznam podporovaných programovacích jazyků za pomoci pluginů, například o C++, PHP nebo o návrh UML, či zápis HTML nebo XML. Jedním z těchto pluginů je i EclipseME. Jak uvádí oficiální stránka4, EclipseME by za vás měl udělat „špinavou“ práci co se týče někdy velmi problémového propojení Eclipse s WTK. Veškerý kód v této práci byl vytvořen právě kombinací Eclipse+EclipseME a nutno podotknout, že šlo o kombinaci bezproblémovou. Druhým rozšířením jsou Mobile Tools for Java (MTJ)5, který měl být oficiálním rozšířením (EclipseME je software třetí strany) platformy Eclipse. V době psaní této práce (duben 2008) byla poslední verze 0.7 pozastavena a projekt bude reinkarnován a znovupostaven právě na pluginu EclipseME. Ten jako samostatný projekt poté zanikne.
6.3 NetBeans Pro úvodní popis prostředí si opět dovolím citovat Wikipedii: NetBeans6 je open source projekt s rozsáhlou uživatelskou základnou, komunitou vývojářů a s více než 100 partnery po celém světě. Pod firmu Sun Microsystems, která je hlavním sponzorem projektu, přešel na základě akvizice stejnojmenné české společnosti v říjnu 1999. Pod open-source licencí byl 1
http://java.sun.com/products/sjwtoolkit/
2
Wikipedia, otevřená encyklopedie: http://www.wikipedia.org
3
http://www.eclipse.org
4
http://eclipseme.org
5
http://www.eclipse.org/dsdp/mtj/
6
http://www.netbeans.org
- 25 -
produkt uvolněn v červnu 2000. NetBeans podporují mnoho technologií, mezi nimiž je i mobilní edice Javy. Ta na této platformě v posledním roce prožívá bouřlivý vývoj v podobě široké podpory a především obrovské škály pomocných nástrojů. Zatímco předchozí řešení umožňovaly „jen“ vytváření, testování a někdy i základní debugging, nový NetBeans nabídne mnohem víc: vizuální návrhář (obdoba Visual Editoru v Eclipse) pro snadné vytváření GUI metodou Drag&Drop, herní návrhář pro snadnou práci s Game API – sprity, vrstvami a čtvercovou sítí, výrazná podpora SVG a důležitá podpora pro portaci aplikací. Případný zájemce nalezne více na http://www.netbeans.org/features/javame/index.html.
- 26 -
7 Závěr Tato práce neměla být pouhou rešerší. Snažil jsem se v ní soustředit kromě nezbytných teoretických podkladů nutných k pochopení problematiky také praktické postupy a příklady ilustrující možnosti přístupu k věci. Jsou zde popsány metody práce s 2D a 3D grafikou za pomoci standardních API, základní práce se zvukem a přehled vývojových platforem. Pomalu nastává doba, kdy výkonnostní omezení přenosných zařízení přestávají hrát prim a kdy se programátor přestává zaměřovat na optimalizaci, ale může se zaměřit na samotný výkonný kód. Specifikem programování mobilních aplikací však nadále zůstává roztříštěnost cílových zařízení a nutnost portace, která vývoj mnohdy výrazně prodražuje. I tato nevýhoda postupem času však ztrácí na důležitosti a to jak díky stále se zlepšujícím se nástrojům, tak hlavně díky změnám ve verzích profilu MIDP. Aplikace na platformě J2ME mají jednu velkou výhodu – umí je spustit téměř jakýkoli mobilní telefon. Tím se otevírá obrovská základna potenciálních uživatelů. Nemá cenu zastírat, že tahounem mobilního byznysu jsou herní aplikace, které jsou majoritní náplní této platformy. Existuje však i mnoho „reálných“ aplikací: • navigační software; mapy • aplikace spolupracující s externím nebo interním GPS modulem • aplikace ovládající přes internet serverový program na jiném hostujícím počítači; namátkou projekt MobileMule1 • blogovací aplikace • monitoring serverů či jiných zařízení • komunikační software za použití Bluetooth či infraportu • slovníky • a mnoho dalších2. Nezřídka dochází k tomu, že pomocí Javových aplikací jsou nahrazovány již existující nativní aplikace předinstalované v zařízení. Trh s mobilními aplikacemi pomalu dospívá a zdaleka ještě neukázal svůj veškerý potenciál.
1
Domovské stránky projektu MobileMule: http://mobil.emule-project.net
2
k nalezení například na http://java.mob385.com/en/
- 27 -
Seznam příloh Příloha A - Obrázky ukázkové aplikace Příloha B - Zdrojové kódy ukázkové aplikace
- 28 -
Příloha A
A.1 Immediate mode
A.2 Retained mode
P-1
A.3 Synchronní animace
Příloha B B.1 Třída Anim.java public class Anim extends Canvas implements Sample, Runnable { private Sprite _sprite1; // sprite podle MIDP1 private javax.microedition.lcdui.game.Sprite _sprite2; // sprite podle MIDP2 // backbuffer private Image _buffer; private Graphics _gBuffer; private Thread _thread = null; // pomocné proměnné pro mechanismus řízení FPS long _startTime, _endTime; int _sleep; final int FPS=10; final int MSPERTICK=1000/FPS; // řídící proměnná pro _sprite1 int _actualFrame=0; // přehrávač hudby Player _player; // konstruktor Anim() { super(); } // pokyn pro zastavení public void stop() { _thread=null; // ukončení vlákna try { _player.stop(); // zastavení hudby } catch (MediaException e) { e.printStackTrace(); } } // pokyn pro spuštění public void showNotify() { init(); } // inicializace public void init() { // vytvoření backbufferu _buffer=Image.createImage(this.getWidth(),this.getHeight()); _gBuffer=_buffer.getGraphics(); // inicializace _sprite1 _sprite1=new Sprite("/earth.png",50,50); // inicializace _sprite2 Image spriteImage=null; try { spriteImage = Image.createImage("/earth.png"); } catch(Exception e) { e.printStackTrace(); } _sprite2=new javax.microedition.lcdui.game.Sprite(spriteImage,50,50); // spuštění vlákna _thread=new Thread(this); _thread.start(); // přehátí zvuku try { InputStream is = getClass().getResourceAsStream("Coldplay-Trouble.mid"); _player = Manager.createPlayer(is, "audio/midi");
P-2
_player.start(); } catch (Exception e) { e.printStackTrace(); } } // překreslení obrazovky protected void paint(Graphics g) { _gBuffer.setColor(0); _gBuffer.fillRect(0,0,_buffer.getWidth(),_buffer.getHeight()); // smazání // _sprite1 _gBuffer.drawImage(_sprite1.getFrame(_actualFrame++),getWidth(),0,Graphics.TOP|Graphics .RIGHT); if(_actualFrame>=_sprite1.m_numFrames) _actualFrame=0; // _sprite2 _sprite2.paint(_gBuffer); _sprite2.nextFrame(); // vykreslení na obrazovku z backbufferu g.drawImage(_buffer,0,0,0); } // metoda vlákna public void run() { while(_thread!=null) { // dokud vlákno nezrušíme _startTime=System.currentTimeMillis(); // zde je místo pro případnou herní logiku repaint(); // překreslení // a výpočet framerate _endTime=System.currentTimeMillis(); _sleep=(int)(MSPERTICK - (_endTime - _startTime)); if(_sleep<5) _sleep=5; // při 0 se zasekává vstup try { Thread.sleep(_sleep); } catch(Exception e) { e.printStackTrace(); } } } }
B.2 Třída ImmediateMode.java public class ImmediateMode extends Canvas implements Sample, Runnable { private Thread _thread = null; // vertexy private static final byte[] VERTEX_POSITIONS = { -1, -1, 1, 1, -1, 1, -1, 1, 1, 1, 1, 1, // front 1, -1, -1, -1, -1, -1, 1, 1, -1, -1, 1, -1, // back 1, -1, 1, 1, -1, -1, 1, 1, 1, 1, 1, -1, // right -1, -1, -1, -1, -1, 1, -1, 1, -1, -1, 1, 1, // left -1, 1, 1, 1, 1, 1, -1, 1, -1, 1, 1, -1, // top -1, -1, -1, 1, -1, -1, -1, -1, 1, 1, -1, 1 // bottom }; // indexy vertexů pro vytvoření triangle stripů private static final int[] TRIANGLE_INDICES = { 0, 1, 2, 3, // přední stěna 4, 5, 6, 7, // zadní stěna 8, 9, 10, 11, // pravá 12, 13, 14, 15, // levá 16, 17, 18, 19, // vrchní 20, 21, 22, 23, // spodní }; // délky triangle stripů v TRIANGLE_INDICES private static int[] TRIANGLE_LENGTHS = { 4, 4, 4, 4, 4, 4 };
P-3
// koordináty k namapování textury private static final byte[] VERTEX_TEXTURE_COORDINATES = { 0, 1, 1, 1, 0, 0, 1, 0, // přední 0, 1, 1, 1, 0, 0, 1, 0, // zadní 0, 1, 1, 1, 0, 0, 1, 0, // pravý 0, 1, 1, 1, 0, 0, 1, 0, // levý 0, 1, 1, 1, 0, 0, 1, 0, // vrchní 0, 1, 1, 1, 0, 0, 1, 0, // spodní }; // vertex buffer private VertexBuffer _cubeVertexData; // triangle stripy private TriangleStripArray _cubeTriangles; // transformační matice kostky private Transform _cubeTransform; // "appaerance" kostky private Appearance _cubeAppearance; // vlastnosti polygonu private PolygonMode _polygonMode; // proměnná textury private Texture2D _cubeTexture; // grafický kontext private Graphics3D _graphics3d; // barva na pozadí private Background _background; //pokyn pro spuštění public void showNotify() { init(); } //pokyn pro zastavení public void stop(){ _thread=null; // ukončení vlákna } // inicializace protected void init() { // získání instance _graphics3d = Graphics3D.getInstance(); // vytvoření pozadí s barvou _background = new Background(); _background.setColor(0xFF113366); // vytvoření vertex bufferu _cubeVertexData = new VertexBuffer(); // vytvoření a nastavení pole vertexů VertexArray vertexPositions = new VertexArray(VERTEX_POSITIONS.length/3, 3, 1); vertexPositions.set(0, VERTEX_POSITIONS.length/3, VERTEX_POSITIONS); _cubeVertexData.setPositions(vertexPositions, 1.0f, null); // to samé pro textury VertexArray vertexTextureCoordinates = new VertexArray(VERTEX_TEXTURE_COORDINATES.length/2, 2, 1); vertexTextureCoordinates.set(0, VERTEX_TEXTURE_COORDINATES.length/2, VERTEX_TEXTURE_COORDINATES); _cubeVertexData.setTexCoords(0, vertexTextureCoordinates, 1.0f, null); // vytvoření polygonů (trojúhelníků) tvořících kostku _cubeTriangles = new TriangleStripArray(TRIANGLE_INDICES, TRIANGLE_LENGTHS); // vytvoření a nastavení kamery Camera camera = new Camera(); float aspect = (float) getWidth() / (float) getHeight();
P-4
camera.setPerspective(30.0f, aspect, 1.0f, 1000.0f); Transform cameraTransform = new Transform(); cameraTransform.postTranslate(0.0f, 0.0f, 10.0f); _graphics3d.setCamera(camera, cameraTransform); // počáteční natočení kostky _cubeTransform = new Transform(); _cubeTransform.postRotate(20.0f, 1.0f, 0.0f, 0.0f); _cubeTransform.postRotate(45.0f, 0.0f, 1.0f, 0.0f); // nastavení vlastností (Appearance) _cubeAppearance = new Appearance(); _polygonMode = new PolygonMode(); _polygonMode.setPerspectiveCorrectionEnable(true); _cubeAppearance.setPolygonMode(_polygonMode);
try { // vytvoření a nastavení textury Image2D image2D = (Image2D) Loader.load("/fel.png")[0]; _cubeTexture = new Texture2D(image2D); _cubeTexture.setBlending(Texture2D.FUNC_DECAL); _cubeAppearance.setTexture(0, _cubeTexture); } catch (Exception e) { e.printStackTrace(); } // spuštění vlákna _thread = new Thread(this); _thread.start(); } // překreslení obrazovky protected void paint(Graphics g) { if(_thread==null) return; // pojistka try { _graphics3d.bindTarget(g); // nastavení cíle _graphics3d.clear(_background); // překreslení cíle pozadím _graphics3d.render(_cubeVertexData, _cubeTriangles, _cubeAppearance, _cubeTransform); // render } finally { _graphics3d.releaseTarget(); // uvolnění cíle } } // metoda vlákna public void run() { while(_thread!=null) { // dokud vlákno nezrušíme _cubeTransform.postRotate(2.0f, 0.0f, 1.0f, 0.0f); // rotace kostky repaint(); // překreslení try { Thread.sleep(20); } catch (InterruptedException ie) { ie.printStackTrace(); } } } }
B.3 Třída RetainedMode.java class RetainedMode extends Canvas implements Sample, Runnable { private Thread _thread = null; private private private private
Graphics3D g3d; World _world; Camera _camera; Mesh _cube;
// konstruktor RetainedMode() { super();
P-5
} // pokyn pro spuštění public void showNotify() { init(); } // pokyn pro zastavení public void stop() { _thread=null; } // metoda vlákna public void run() { while(_thread!=null) { // dokud vlákno nezrušíme _cube.postRotate(2.0f,0.0f,0.0f,1.0f); // rotace kostky repaint(); // překreslení try { Thread.sleep(20); } catch (InterruptedException ie) { ie.printStackTrace(); } } } // překreslení obrazovky protected void paint(Graphics g) { if(_thread==null) return; // pojistka try { g3d.bindTarget(g); // nastavení cíle g3d.render(_world); // render } finally { g3d.releaseTarget(); // uvolnění cíle } } // inicializace void init(){ // získání instance g3d = Graphics3D.getInstance(); // nahrání scény ze souboru Object3D[] parts=null; try { parts = Loader.load("/cube.m3g"); } catch(Exception e) { e.printStackTrace(); } _world = (World) parts[0]; // prvek na nultém indexu je vždy hlavní objekt // získání odkazu na kostku (má definováno userID=12) // pro pozdější snazší manipulaci _cube=(Mesh)_world.find(12); // získání a nastavení kamery _camera = _world.getActiveCamera(); // nastavení projekce float[] lProjection = {0.0f, 0.0f, 0.0f, 0.0f}; _camera.getProjection(lProjection); float aspect = (float)getWidth()/(float)getHeight(); // nastavení prespektivy _camera.setPerspective(lProjection[0], // úhel pohledu aspect, // poměr stran lProjection[2], // první ořezávací rovina lProjection[3]); // druhá ořezávací rovina _world.setActiveCamera(_camera); // spuštění vlákna _thread = new Thread(this); _thread.start(); } }
P-6