Full-Screen Exclusive Mode API
En bra och enkel genomgång av detta APIfinns i Java Tutorial,
http://java.sun.com/docs/books/tutorial/extra/fullscreen/index.html.
Den som tänker använda API:et bör absolut läsa igenom
den. Speciellt viktigt är det att förstå avsnitten
programming
tips.
Full-screen exclusive mode innebär att operativsystemets fönsterhantering
kopplas bort. Programmet tar istället kontroll över hela skärmen
och ritar direkt på grafikkortet. Detta gäller egentligen inte
på Solaris eller Linux eftersom de inte har något API för
detta. Det innebär ändå en prestandavinst att använda
full-screen exclusive mode även på dessa operativ.
Display Mode
Av prestandaskäl är det lämpligt att programmet tar reda
på information om den plattform det körs på. Klassen
java.awt.GraphicsEnvironment
symboliserar allting som går att rita på (skärmar och skrivare).
Varje enhet (tex skärm) symboliseras av en
java.awt.GraphicsDevice.
Varje skärm har en
java.awt.GraphicsConfiguration som beskriver
egenskaper hos grafikkortet och en
java.awt.DiplayMode som beskriver
skärmens egenskaper. Här kommer ett program som skriver ut information
om skärmen och grafikkortet,
ListScreenInfo.java.
Det kan också vara läge att ändra färgdjup och skärmstorlek.
Det görs genom att skapa ett objekt av DisplayMode med lämpliga
egenskaper och sätta det med
GraphicsDevice.setDisplayMode(DisplayMode
d).
Passiv och aktiv rendering
Passiv rendering är det sätt som normalt används
i AWT och Swing, dvs att vi skriver en metod
paint(Graphics g) eller
paintComponent(Graphics g) som anropas av ramverket (dvs AWT eller
Swing) när komponenten av någon anledning behöver ritas
om. Detta är bra för ett fönsterhanteringssystem eftersom
programmet själv inte kan veta när användaren gör något
som innebär att komponenten behöver ritas. Använaren kan till
exempel ha förstorat eller förminskat ett fönster eller tagit
bort något som tidigare skymde en del av fönstret.
Aktiv rendering innebär tvärtom att programmet inte väntar
på att bli anropat av fönsterhanterarens händelsehanteringstråd
utan själv, i en egen tråd, ritar på skärmen. Det är
alltså programmet, inte användaren (via fönsterhanteraren)
som bestämmer när skärmen ska ritas om. Denna strategi är
snabbare men fungerar bara om programmet har full koll på allt som
finns på hela skärmen. Så är fallet med full-screen
exclusive mode varför aktiv rendering används av det API:et.
Double Buffering och Page Flipping
Om vi ritar grafik pixel för pixel direkt på skärmen är
det högst sannolikt att det syns hur grafiken "växer fram" vilket
gör ett väldigt dåligt intryck på användaren.
Lösningen på detta är att använda
double buffering,
vilket innebär att hela skärmbilden ritas färdigt i en bild
som bara finns i minnet (
back buffer,
off-screen image). Sedan
flyttas hela denna buffert ut till skärmen samtidigt, se bild nedan.
Denna teknik används till exempel av Swing. I
avsnittet om Java 2D, stycket om bilder
finns det ett exempel på double buffering utan full-screen exclusive
mode.
Även med double buffering kan skärmen se konstig ut om bufferten
kopieras just när den håller på att ritas ut på skärmen.
Detta kan undvikas genom att använda
page flipping, då
flyttas inte bufferten utan videopekaren (dvs adressen till det minnesområde
som innehåller skärmbilden). Nästa gång skärmen
ritas om kommer det då att vara innehållet i bakbufferten som
ritas ut:
Notera att syftet med både double buffering och page
flipping är att det ska se bättre ut, inte att det ska gå
snabbare.
Klassen BufferStragtegy
Klassen BufferStrategy innehåller koden för aktiv rendering
i fullskärmsläge. Vi skaffar oss ett objekt av klassen med metoden
getBufferStrategy() som finns i
java.awt.Window (och därmed
även i
java.awt.Frame). Som namnet antyder innehåller
klassen också strategin (double buffering, page flipping osv) för
att rita på skärmen. Vill vi styra vilken strategi som ska användas
får vi skapa ett objekt av
BufferCapabilities som innehåller
de önskade egenskaperna och sedan skapa en
BufferStrategy med
metoden
Window.createBufferStrategy(int numBuffers, BufferCapabilities
bc). Med den metoden kan vi även ange hur många buffertar
vi vill ha. Att ha flera buffertar än två är meningsfullt
om det tar längre tid att skapa bilden off-sreen än att rita upp
den på skärmen.
BufferStrategy har en metod
getDrawGraphics() som returnerar
ett
Graphics-objekt för den "bakersta" bufferten. Det är
den vi ska rita grafik i. Den har också en metod
show() som
flyttar ut den "näst yttersta" bufferten till skärmen. Den metoden
ska användas när vi vill rita om skärmen. Animeringsloopen
kommer alltså att se ut något åt det här hållet:
BufferStrategy myStrategy = ...;
while (!done) {
Graphics g;
try {
g = myStrategy.getDrawGraphics();
render(g); //Min metod där grafiken ritas.
} finally {
g.dispose();
}
myStrategy.show();
}
Beroende på vilken strategi
BufferStrategy använder
kan bufferten vi får tag i med
getDrawGraphics() se olika ut.
Till exempel kan vi med metoden
BufferStrategy.contentsLost() få
reda på om hela buffertens innehåll är förlorat. Mer
exakt vad som händer får vi reda på genom att hämta
ett
BufferCapabilities-objekt med
BufferStrategy.getCapabilities()
och undersöka det. Syftet med allt detta är att få reda på
hur mycket som behöver ritas om (och eventuellt hur mycket vi hinner
rita om).
Här kommer ett exempel med fullskärmsanimering,
MultiBufferTest.java. Två viktiga
men
om programmet:
- Jag har inte haft tid att skriva ett exempel som följer alla
upptänkliga best practices. Programmet ovan har jag knyckt från
Java Tutorial, det är lite onödigt krångligt samtidigt som
det hoppar över viktiga detaljer. Läs noga alla programming
tips som finns längst ner på varje sida i avsnittet om full-screen
exclusive mode i Java Tutorial (se länk överst på denna sida)
för att skriva ett bättre program.
- Om plattformen inte stödjer exklusivt fullskärmsläge
(som är fallet med Linux) simuleras det genom att fönstret täcker
hela skärmen och saknar ram osv. I detta fall får vi dock inte
prestandavinsten med hårdvaruaccelererat videominne. Vi får heller
inte möjlighet att ändra skärmens färgdjup och upplösning
eller att använda strategier som page flipping. Slutligen är AWT:s
händelshantering fortfarande aktiv och kan hitta på bus vid oönskat
tillfälle.
Till sist: Det faktum att
BufferStrategy kan dölja allt som
har med skärmegenskaper, dubbla buffrar, page flip osv att göra
betyder
inte att vi kan strunta i det.
Det är
mycket viktigt att förstå vad som händer och hur vi kan påverka
det för att skriva en bra animering.
VolatileImage
Klassen
java.awt.image.VolatileImage används för att
representera en bild som finns i hårdvaruaccelererat minne på
grafikkortet. Det specifika med sådant minne är att dess innehåll
kan bli förstört. Det kan ske till exempel om användaren med
ALT-TAB lägger något annat ovanpå vår fullskärmsapplikation.
Även om
BufferStrategy döljer
VolatileImage kan
det vara bra att läsa om den i
ftp://ftp.java.sun.com/docs/j2se1.4/VolatileImage.pdf
Optimera med förnuft
Det är alltid nödvändigt att ta reda på vad som behöver
optimeras och varför och hur det ska göras
innan någon
som helst optimering görs. Vid animering är detta extra viktigt
eftersom beteendet blir extremt plattformberoende. Det är ofta lämpligt
att prova ut optimeringar på olika plattformar och sedan i koden testa
vilken plattform programmet körs på och köra kod som är
optimerad för just den plattformen. Vidare kan det vara lämpligt
att ha flera olika strategier klara och i koden välja den bästa
av dem som stöds av aktuell hårdvara.
Design
Här är ett förslag på objektorienterad design för
animering. Det handlar allstå inte om layout eller om kodmässiga
optimeringar. Enligt förslaget finns animeringsloopen i
AnimationEngine
och ser ut ungefär som följer. Koden är inte ens kompilerad,
den är bara ett försök att förtydliga diagrammen nederst
på sidan.
BufferStrategy strategy = ...;
Graphics graphics;
while (!done) {
try {
graphics = strategy.getDrawGraphics();
drawBackground(graphics); //Method where the background is drawed
for (int i=0; i<sprites.length; i++) {
sprites[i].move();
collisionDetect(sprites[i]);
sprites[i].drawYourself(graphics); //All sprites draw their symbol at the correct position.
}
} finally {
graphics.dispose();
}
strategy.show();
}
Metoden
collisionDetect() tänker jag mig ungefär så
här:
void collisionDetect(Sprite sprite) {
for(int i=0; i<sprites.length; i++) {
if (!sprites[i].equals(sprite)) {
if (sprites[i].isCollision(sprite)) {
sprites[i].collision(sprite);
sprite.collision(sprites[i]);
}
}
}
}
Obs! Med denna algoritm för collision detect får animeringen
en komplexitetsfunktion med O(n
2), där n är antalet
sprites. Hujedamej! Jag är ingen guru på animering och har tyvärr
inget bra förslag på någon bättre beprövad algoritm.
Ett bra ställe att kolla på är
www.javagaming.org.
Om en
Sprite ska vara animerad (alltså röra sig medans
den åker över skärmen) ska den ha en
Image-array
(objektet
symbols i kollaborationsdiagrammet nedan). Varje gång
Spriten ska rita upp sig tar den nästa bild i arrayen.
Kollaborationsdiagram för animering
Klassdiagram för animering