J2ME - animáció tárolása
2009-09-01T11:24:22+02:00
2009-09-08T20:40:20+02:00
2022-08-13T11:20:34+02:00
zsoltsandor
Sziasztok!

Egy j2me-s játékot fejlesztünk, és belefutottunk egy problémába.

Adott sok animációs fázis (kb. 30 karakterenként) a karakterek mérete 100x100 pixel körül van. Épp ezért a memóriában nagyon sok helyet: 10k*2*30: 600k-t elfoglal, hogy ha image-ként tárolom. A 2-es szorzó a transzparencia miatt jön be. Ebből következik, hogy Image-ként az animációkat nem tudjuk tárolni.

Amennyiben csak a png-ket tároljuk byte[] tömbben, akkor sokkal barátibb, framenként 5k körüli a heap foglalás, tehát a 30 frame az csak 150k, ez kezelhető. Viszont azt vettük észre, hogy ha a paint()-ben kódoljuk át a png-t image-gé, akkor az elég lassú, egy egyszerű image sorozat kirakásánál 20 frame/sec-et tudunk hozni, viszont így csak 10-et.

Volt egy ötletünk, hogy úgy spórolunk, hogy az animációk közötti diffet letároljuk, és csinálunk egy nagy képet, amiben benne van a kezdőfázis, illetve a diff-ek. Ezt tegnap lekódoltam, az ötlet jól is működik, csak sajnos a transzparencia miatt megbukott az elképzelés, ugyanis képzeljétek el a következőt. Van két fázis, A és B. Az A fázisban előre néz a fej, a B fázisban hátra. kirakjuk az A fázist, majd rá a B-t. mivel B-ben a transzparencia a fej előtt van, ezért végeredményként 2 fejet fogunk látni. Ahogy láttam, a j2me nem támogat semmiféle and,or,xor akármit, tehát maximum pixelről pixelre végigmenve tudnánk megoldani, hogy jó legyen, ami attól tartok, megint csak megenné a prociidőt.

Még egy olyan ötletünk van, hogy saját RLE enkódert írunk, reménykedve abba, hogy a kép kikódolása gyorsabb lesz, mint a J2ME-be épített png dekódoló. Egy délelőtt alatt lekódolt nagyon buta RLE enkóder azt mutatja, hogy a helyfoglalás a png kétszerese (5k helyett 10k) lett (rajzolt cartoon képeink vannak)

Van esetleg valami ötletetek, hogy mit lehetne még kipróbálni?
Mutasd a teljes hozzászólást!

  • A png-ket byte[] tömbben tároljátok. Adott két kép tömb, ismert a kódolás, ismert a méret. Pofonegyszerűen össze lehet xor-olni őket kézzel. (11000000 ^ 10100001 -> 01100001)
    Mutasd a teljes hozzászólást!
  • Még azt is kipróbálnám, hogy a Canvast bővítve a paint() -ban a figura csak egy bizonyos területét rajzolnám át.
    Megszerezném pl a terület háttér pixeleit és a fázis kép átlátszó pixeleit kivéve rárajzolnám a fázis karaktert.
    Mutasd a teljes hozzászólást!
  • A gond a következő. Az összes png-t nem tudjuk byte[] tömbben KIKÓDOLVA tárolni, mert túl sok memóriát foglal, ahogy említettem az elején is.
    Mutasd a teljes hozzászólást!
  • A canvas paint bővítéses dolog viszont nem tűnik annyira rossznak, a lényeg, hogy össze kellene merge-elni a háttér részt illetve a kis képrészletet, és azt kirakni. Gondolkodok rajta, hogy jó elképzelés-e, de elsőre jónak tűnik...
    Mutasd a teljes hozzászólást!
  • Hát..
    Régen úgy is spóroltak, hogy volt mondjuk egy 100x100-as figura. Ezt felosztották mondjuk 10x10 mezőre, mindegyik egy 10x10-es kép lesz.
    Mivel a figurák nem teli "kockák", így az üres helyeket egyrészt rajzolni sem kell, másrészt tárolni sem. Illetve az esetleges közös részek is "kiesnek". Pl. ha üt a főszereplő akkor csak deréktól fölfelé változik a képe, az alsó része nem foglal plusz memóriát. Ha két szereplőnek csak a pólója, feje különbözik akkor meg főleg sok memória spórolható.
    Csak akkor minden animációs fázishoz kell egy 10x10-es tömb ami megmondja, hogy az adott helyre melyik képet kell kirajzolni.

    Nem mondom hogy kódold le, de papíron végezhetsz számítást, hogy ezzel nyersz e helyet, és ha igen, mennyit. Szerintem egy 30-50% helyet takaríthatsz vele, de ez programfüggő is. Sebességben lassabb lesz, de az üres kockák miatt lehet hogy visszakapod ezt.
    Mutasd a teljes hozzászólást!
  • Mekkelek5: köszi, ez egy nagyon jó ötlet. Abban reménykedünk, hogy ha felosztjuk x * y - ra a képet, akkor a frame-ek ismétlődése miatt lesznek hasonló dobozok, így ezzel további optimalizációra is lehetőség lesz.
    Mutasd a teljes hozzászólást!
  • Sziasztok!

    Elkészítettem a kép feldarabolóst, és csináltam egy kis mérést.
    70 frame-en teszteltem, a következő felosztásokat használva:

    8,10,11,12,13,14,15,16,17,18,19,20,23,25,30 sorra és oszlopra osztottam a frameket, és megnéztem, hogy az átlagos fill count (1 framet átlagosan hány részből kell összerakni) illetve a végén mennyi kilobájt memória foglalást jelent az adott felosztás.

    Az eredmény:

    avg fill count: lineárisan emelkedik, 8-ról 56-ra. Az 56 nagyon soknak tűnik, szerintem a mobil procija nem bírná. A memória foglalás ennek megfelelően, 922k-ról kezdi (x*y*2 az alpha miatt), majd szépen leesik: 18x18-nál 25 fill count mellett már csak 484k, majd kis huplikban hol több, hol kevesebb, a legkisebb a 30x30-asnál lesz, ott 417k.

    Figyelem, becsapós dolog, a 30x30 azt jelenti, hogy 900 részre osztottam fel a képet, ami nyilván egy nagyon brutális felosztás.

    Eredmény: nem tudom, a mobil processzora mit bír, de olyan 550k-ra le tudtuk törni a memóriafoglalás, amennyiben azt mondjuk, hogy a 2 karakter összesen 70 frame-mel rendelkezik, tehát karakterenként 30-35-tel. Ez szerintem egy elég elfogadható kompromisszum lett, az algoritmus pedig nagyon-nagyon primitív.



    Mutasd a teljes hozzászólást!
  • Mindenki okulására a forráskód, amely jelenleg az e:/z könyvtárból felolvassa a képeket, majd feldarabolja őket,
    és kiírja az eredményeket. Ha excelbe akarjátok importálni, akkor az excel boolean változót állítsátok true-ba, és akkor elég copy-pastelni.

    Apache commons lib szükséges, mert egy nem túl szép megoldással vizsgáltam (kép->string->md5), hogy az adott kép blokk egyedi-e.

    package com.test.cropper; import java.awt.Color; import java.awt.Rectangle; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; import java.util.Hashtable; import javax.imageio.ImageIO; import org.apache.commons.codec.digest.DigestUtils; public class Main { int BOX_HNUM = 8; int BOX_WNUM = 15; int emptyCount = 0; int filledCount = 0; BufferedImage image; Hashtable<String, Integer> table = new Hashtable<String, Integer>(); int cell_w = 0; int cell_h = 0; boolean EXCEL = true; public Main() { File root = new File("e:/z"); for (File file : root.listFiles()) { calcImage(file); } int sameCount = 0; for (Integer value : table.values()) { if (value > 1) { sameCount++; } } if (EXCEL) { System.out.println(BOX_HNUM + " x " + BOX_WNUM); System.out.println(cell_w); System.out.println(cell_h); System.out.println(sameCount); System.out.println(emptyCount); System.out.println(filledCount); System.out.println(filledCount / root.list().length); System.out.println((sameCount + filledCount) * cell_w * cell_h * 2 / 1024); } else { System.out.println("Splitting a frame to " + BOX_HNUM + " x " + BOX_WNUM); System.out.println("cell_w: " + cell_w); System.out.println("cell_h: " + cell_h); System.out.println("same count = " + sameCount); System.out.println("empty count = " + emptyCount); System.out.println("filled count = " + filledCount); System.out.println("average filled count = " + filledCount / root.list().length + " / frame"); System.out.println("Consuming: " + (sameCount + filledCount) * cell_w * cell_h * 2 / 1024 + "k"); } } void calcImage(File file) { try { System.out.println("Working on file :" + file); image = ImageIO.read(file); int width = image.getWidth(); int height = image.getHeight(); cell_w = width / BOX_WNUM; cell_h = height / BOX_HNUM; for (int j = 0; j < BOX_HNUM; j++) { for (int i = 0; i < BOX_WNUM; i++) { Rectangle rect = new Rectangle(i * cell_w, j * cell_h, cell_w, cell_h); int calc = calcEmpty(rect); // 0: empty, 1: már volt, 2: még nem volt } } } catch (IOException e) { e.printStackTrace(); } } int calcEmpty(Rectangle rect) { boolean empty = true; StringBuffer buffer = new StringBuffer(); for (int j = 0; j < rect.height; j++) { for (int i = 0; i < rect.width; i++) { Color pixelColour = new Color(image.getRGB(rect.x + i, rect.y + j), true); if (pixelColour.getAlpha() > 0) { empty = false; } buffer.append(pixelColour.getAlpha()); buffer.append(","); buffer.append(pixelColour.getRed()); buffer.append(","); buffer.append(pixelColour.getGreen()); buffer.append(","); buffer.append(pixelColour.getBlue()); buffer.append(","); } } String md5 = DigestUtils.md5Hex(buffer.toString()); if (empty) { emptyCount++; return 0; } else { if (table.get(md5) != null) { Integer count = table.get(md5); table.put(md5, new Integer(count + 1)); return 1; } else { table.put(md5, 1); filledCount++; return 2; } } } public static void main(String[] args) { new Main(); } }
    Mutasd a teljes hozzászólást!
  • Szintén spórolási módszer lehet, ha nem 24/32 biten tárolod a képeket.

    2D játékoknál általában (szerintem minden esetben) 256 szín elegendő (egy figurán), amivel már háromszor-négyszer annyi animáció tárolható. De akár 16 színű (4bit) vagy 4 színű (2bit) képeket is lehet tárolni. 16 szín is elég lehet és akkor már akár 8x annyi animációt tárolhat az ember, 4 színnel már 16x annyit. 4 szín kevésnek tűnik, de mondjuk egy lobogó lánghoz, vagy kifröccsenő vérhez elég lehet.
    Vagy lehet egyedi tárolást alkalmazni, mondjuk 32 vagy 64 színnel vagy ahogy tetszik. Nem tudom a Java mennyire támogatja ezt, lehetséges hogy saját kép kirajzoló eljárást kell írni, ami lassíthat a dolgon.
    A "kockázós" módszerrel alkalmazva már egész jó lehet.
    Mutasd a teljes hozzászólást!
  • Sziasztok!

    Nagyon hasonló szituációba keveredtem én is a minap. A lényeg, hogy transparency-vel rendelkező PNG-kel dolgozom és ezt a Java paint metódusa elég szépen és kellő gyorsasággal tudja kezelni és megjeleníteni, így nem is mentem bele, hogy egyéni megoldást alkalmazzak. Ám ennek ára, hogy a png file-ok elég nagyok lettek, 40x40-es egység kb 65 frame-el már 66k, a midletek jar file-ainak maximalizásálsával, így elég kevés lehetőség van több egység beillesztére, így mégis rávettem magam, hogy egy egyéni megoldást alkalmazzak. A jó öreg colorkey technikát, a png-kről eltávolítottuk az átlátszó pixeleket és egy valószínűleg ritkán előforduló pixel-t használtunk (0x0000ff00). Egy sima átalakítással a png-n ezt a színt megtaláljuk és átalakítjuk egy átlátszó színre (pl.: 0x00ffffff). Ezzel óriási megtakarítást nyertünk, az addig 66k-s png 15k-s lett a játék többi komponensére használva, az egész game grafikai forrása mindsösze 150k-ra csökkent így még maradt tovább 150 a forráskódnak és az egyébb média forrásoknak.

    Ám sajnos, mint már itt olvasni lehetett. A craeteRGBImage által png-t feldolgozzuk és byte[] ( vagy alhpachanneles támogatás esetén int[]) folyamba elhelyezzük, akkor a kirajzolási sebességnél hatalmas visszaesést tapasztaltunk. Átlagos (Thread.sleep(10)) 38-40 FPS- ról konstans 14 FPS- re csökkent ami elviselhetetlen játékmenetet ad :(.

    Ki mit tud erről, miért lassú ennyire a PNG helyett az byte[] folyamok vagy int[] folyamok megjelenítése. Milyen technikát javasoltok az átlátszóság megvalósítására figyelembe véve a MIDLET 300k-s maximális méretét.

    Köszi
    Bence
    Mutasd a teljes hozzászólást!
  • Szia!

    A probléma szerintem a png dekódolás sebességével van, ugyanis ha egy sima image-et kirajzolsz, az gyors, viszont ha a paint() metódusban png-ből először image-et csinálsz, majd azt rajzolod ki, az viszont lassú lesz.

    Két dologra kell optimalizálni:

    - jar méret
    - heap méret

    Mi 1mb jar, 2 mb heap-re optimalizálunk, úgy vettem ki, hogy nálatok ezek a korlátok alacsonyabbak. Fontos lenne tudni, hogy pontosan mi a gond nálad, túl nagy lesz a jar, vagy pedig amikor image-et csinálsz, akkor elfogy a heap.

    A lehetőségeid szerintem azok, amiket fent említettem:

    - animáció diff létrehozás, png-be pakolás, png betöltés, png-ből adott darabok kirajzolása
    - animációs frame-ek kis dobozokra vágása, és így tárolás, majd a png betöltése, valamint png-ből adott darabok kirajzolása
    Mutasd a teljes hozzászólást!
  • A probléma az, hogy a program betöltésekor egy image factory osztály kezelek, melyben minden szükséges képet ID-szerint betöltök úgy,hogy ha szükséges akkor a megjelölt háttérszínt átlátszó pixelekké alakítom. Ezeket lementem Image-be, majd később kirajzolásnál már csak az imagek-kel dolgozok, a kirajzolást pedig game package Sprite osztályával valósítom meg a Canvas paint() függvényében. A különleges jelenség, hogy ha a frame-eket feldarabolom 40x40-es fileokba és betöltök akár 100-at akkor telejsen gyors, ha egyszerre töltöm be és dolgozom át a pixeleket átlátszóvá akkor lassú.

    Kipróbálom a javasolt technikát. Mi 300K-os jar-ra optimalizálunk, főként az alacsony kategóriás eszközökön is szeretnénk futattni a progit.

    Bence
    Mutasd a teljes hozzászólást!
  • Tehát mindkét esetben Image-eid vannak, és csak a graphics.drawImage()-et hivod? Mert akkor viszont
    lehetséges, hogy egy profile-ingot lenne érdemes
    futtatni, viszont vigyázz, hogy a legújabb, 3-as
    sdk-ban elég gáz a profile-ing (nem igazán működik),
    próbálkozz a 2-essel.

    Esetleg ha gondolod, szúrj be egy kódrészletet.
    Mutasd a teljes hozzászólást!
  • Profiling-ról még nem hallottam, az mit takar. Miért kell használni? stb.

    A jelenség a következő, ha ugyan azt a png-t betöltöm Image-be majd kirajzolom akkor gyors, ha betöltöm Image-be majd átalakítom az Image-t úgy, hogy a megfelelő színeket átlátszóvá teszem, akkor lassú. Mindekét esetben a drawImage függvényt használom. Az átalakítás nem valósidőben, hanem a betöltéskor megy végbe.

    Az átalakításért felelős függvényem:


    static public Image createAlphaMap(Image Immutable, int _r, int _g, int _b){ int a,r,g,b; // lementjük egy tömbe a forrás fileből nyert pixeleket int[] colors = new int[Immutable.getWidth()*Immutable.getHeight()]; Immutable.getRGB(colors, 0, Immutable.getWidth(), 0, 0, Immutable.getWidth(), Immutable.getHeight()); // átalakítjuk a szükséges pixeleket és elhelyezzük egy új mutable image-ben for (int i=0; i<colors.length; i++){ // átalakítjuk az int-ben tárolt színkódot ARGB komponensekre a = (colors[i] >> 24) & 0xff; r = (colors[i] >> 16) & 0xff; g = (colors[i] >> 8) & 0xff; b = (colors[i] >> 0) & 0xff; // az átlátszóvá alakaítani kívánt színt maskoljuk if (r == _r && g == _g && b == _b){ colors[i] &= 0x00000000; } else { colors[i] = (a << 24) + (r << 16) + (g << 8) + b; } } Image Mutable = Image.createRGBImage(colors, Immutable.getWidth(), Immutable.getHeight(), true); return Mutable; }
    Mutasd a teljes hozzászólást!
  • A profiling arra jó, hogy mutatja a memória foglaltságot (milyen élő objektumaid vannak, azok mennyi helyet foglalnak a memóriában) /processzor használatot az alkalmazás futása közben.

    Miért kell használni: pont azért, amit most leírtál :)

    ugye a paint-ben nem valami olyasmit csinálsz, hogy

    Image image = Cache.getImage(xxx);
    Image finalImage = StaticClass.createAlphaImage(image,r,g,b);
    graphics.drawImage(finalImage)

    ?
    Mutasd a teljes hozzászólást!
  • Igen igen, csak két részre bontva :

    Image image = Cache.getImage(xxx);
    Image finalImage = StaticClass.createAlphaImage(image,r,g,b);

    a betöltésénél szerepel, és egy factory osztály belső tárolójába kerül mentésre. Mely adott elemét előhívhatjuk a paint metódusba.

    Így tulajdonképpen, egy sima Graphics.drawImge(finalImage) metódust hívunk egy már átalakított és elkészített képpel.

    Mutasd a teljes hozzászólást!
  • Akkor én megnézném a factory osztályt, hátha mégis ott van a turpisság, mert ebben az esetben nem kellene ilyen
    teljesítmény csökkenésnek lennie.

    Még arra tudok gondolni, hogy a mobilnak valamiért át kell kódolnia az általad gyártott képet más bpp-re...

    Kipróbáltad mobilon is, vagy csak emulátorban?
    Mutasd a teljes hozzászólást!
  • Köszönöm a segítséget,de hál isten sikerült megoldani a dolgot. Mivel a Java dedikált osztályait nehéz lenne kritizálni, ezért valószínű nálam volt a probléma, mindenesetre leírom, mert lehet, hogy más is találkozik vele.

    A lényeg, hogy az eredeti nagyméretű 40db 40x40-es frame-t tartalmazó png méretének csökkentésére, azt találtuk, ki, hogy nem használjuk a png alpha csatornáját, hanem egy ritkán alkalmazott színnel töltjük ki azt, majd a betöltéskor egy algoritmus segítségével ezt a színt kicseréljük átltátszóra, így a png-ből létrehozott Image-t kicseréltük egy saját magunk által létrehozott int[] folyammá. A jaxax.microedition.lcdui.game.Sprite osztály használtam a frame-k feldarabolására és kezelésére. Különös módon a Sprite osztálynak amint átadtuk a saját magunk által átalakított Image-t úgy belassult, hogy 40 FPS-ről 10 FPS-re csökkent a képfrissítés. Nagyjából 6-7 esetet végig próbálva kezdett körvonalazódni, hogy a Sprite osztály képtelen hatákonyan dolgozni nagyobb int[] folyamok által generált Image-ekkel, természetesen fent tartom azt, hogy tévedek és az általam eszközölt eljárásokban van a hiba, de átlátszó csatornát használó png-k betöltése után mindez a jelenség nem lépett fel. A megoldás, hogy létrehoztam egy egyéni Sprite osztály-t amely az összes eredeti dedikált függvénnyel rendelkezik, működési lényege, hogy a teljes frame sorozat alapján feldarabolja a képet és külön külön frame méretű image-ekbe rakja, majd lefuttatja az átlátszó pixelek beszúrását. Ezeket az Imageket lehet elérni a frame ID-jának megadásával. A Sprite osztály által nyújtott nagyon hanszos ütközés funkción kívül működik az összes sprite osztály által használt függvény, így nem kell módosítani a programot. Az eredeti 39-40 FPS helyett viszont 47-50 FPS lett a végleges sebesség, amin csodálkoztam.

    Zsoltsandor, ti milyen game-t fejlesztetek?
    Mutasd a teljes hozzászólást!
  • Szia!

    Mi kapásból dobtuk a Sprite osztályt, mert számunkra
    nem volt használható.

    Mi egy verekedős játékot fejlesztünk, sok animációval, sok karakterrel, háttérben (ha belefér) parallax scrollerrel.
    Mutasd a teljes hozzászólást!
  • Nem tudom pontosan ez az ütközés figyelés hogy zajlik, de el tudom képzelni, és ez lehet lassú dolog. Főleg ha több objektumot figyel pixelről pixelre.

    Tapasztalataim szerirint fölösleges pixel pontos ütközést készíteni, nem nagyon van olyan sas szemű egyén aki egy akciójáték közben pixel pontosan látja, hogy ütközött e valamivel vagy sem. Minél nagyobb a felbontás annál jobban "el lehet nagyolni" ezt a részt.
    Sőt, én most mászkálós játékot készítek és az ütközést csak úgy kérdezem le, hogy két objektum távolságát mérem. Nem tökéletes, de "tökéletesen" játszható a játék, úgyse fogom látni, hogy a főszereplő haja hozzá ért e a madár szárnyához, azt látom hogy közeledik valami, akkor tudom hogy el kell kerülni, vagy "meg kell ölni".
    Ha lesz kedvem/időm pontos ütközést készíteni akkor is a valódi felbontás felével fogok dolgozni. Régebben azt csináltam (Pascalban), hogy a 320x200-as képernyő mellé ("mögé") készítettem egy 160x100-as "képet" amire a figurák kicsinyített képét rajzoltam, pontosabban ahol figura volt oda a sprite sorszámát írtam. így mielőtt kirajzoltam a főszereplől (lövedékeket stb.) ezen a hátsó képen megnéztem, hogy van e valami alatta, és ha igen akkor mi az. Ez elég gyors volt sok száz objektumnál is, +sprite-háttér ütközést is tudtam vele kezelni.

    Ha okosak voltak akkro a Java sprite kezelőjében is lehet állítani, hogy milyen finoman érzékelje az ütközést, pixel pontosan vagy csak minden második-harmadik pixelre.
    Mutasd a teljes hozzászólást!
  • Ütközés vizsgálatot lehet bounding box-szal
    csinálni, első körben. Ha a bounding box ütközik,
    akkor jöhet szóba a pixeles vizsgálat.

    Viszont, azt meg lehet csinálni, hogy egy bitsorozatban
    tároljuk minden egyes animációs fázishoz, hogy hol van
    ütközés rész, és csak az ütköző dobozokhoz tartozó
    bitekkel bűvészkedünk.
    Mutasd a teljes hozzászólást!
  • Igen, az is egy módszer, hogy az ember vizsgálja, hogy a két téglalapnak van e közös területe, ha igen akkor lehet pixelenkénti vizsgálatot végezni.

    Nekem ez azért nem tetszik, mert ha mondjuk 100 sprite van velem, 200 ellenség a képernyőn akkor 200x100=20000 vizsgálatot kell végezni, +esetleg sprite-háttér ütközést figyelni. Szerintem sok elem esetén lassú lehet.
    Ha minden fázishoz tárolunk egy "bitsorozatot" ami jelzi, hogy hol ütközik a sprite akkor az nem rossz ötlet. Mondjuk megtehetjük, hogy pl. a 32x32-es képhez 16x16-os maszkot készítünk, ezzel is helyet-időt nyerünk, meg vizsgálatkor a sorokat egy-egy számba (változóba) tölthetjük, a megfelelő eltolás után egy "A and B" megmondja, hogy van e közös pontjuk. Ha lesz időm rá akkor ki is próbálom valamikor.
    Mutasd a teljes hozzászólást!
abcd