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:
  1. 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.
  2. 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(n2), 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