Sunday, January 11, 2009

JAVA 2D Graphics dan Animation (Part 2) - Games Engineering

Images
Menggambar text pada screen pada tulisan sebelumnya adalah sesuatu yang menyenangkan, tetapi akan sangat menyenangkan ketika kita bisa menambahkan image pada game yang kita develop, bener ga? bener!. Sebelum memulai menggambarkan image pada screen ada baiknya jika kita mempelajari beberapa hal fundamental yang berkaitan dengan image, seperti tipe transparansi dan format file.

Transparensi
Oke sekarang bayangkan kita punya gambar seperti di bawah ini :

gambar image di atas mempunyi backgroun berwarna putih, tetapi perlu diingat bahwa background adalah bagian dari image juga, dan apakah background tersebut akan ditampilkan juga? tergantung dari transparansi image. Kita bisa menggunakan tiga tipe image tranparansi: opaque, transparent, dan translucent.
  • Opaque: Setiap pixel pada image akan ditampilkan.

  • Transparent: Setiap image juga akan ditampilakan, tetapi background putih akan dibuat transparan.

  • Translucent: Pixel dapat ditransparansi secara terpisah, seperti ketika ingin menampilkan effect ghost/hantu.


File Format
Ada dua tipe format dasar image yaitu raster dan vector. Raster image format akan ditampilkan sesuai dengan ukuran kedalaman pixel, jika diperbesar image akan pecah. Vector image akan ditampilakan secara geometri dan dapat diresize tanpa mengurangi kualitas image.
Java API tidak mempunyai built in vector format, maka kita akan memfokuskan pada raster image, jika ingin mengetahui manipulasi tentang vector image bisa memanfaatkan SVG punya apache yang disebut batik http://xml.apache.org/batik/.

Java runtime mempuyai tiga format file raster yang berbeda yaitu GIF, PNG, dan JPEG:
  • GIF, GIF image bisa opaque atau transparent, dan dapat mempunyai warna 8 bit atau bahkan kurang. Saat kita sudah mengenal format PNG dan mempunyai kemampuan seperti GIF, sehingga disarankan menggunakan PNG.

  • PNG, PNG image mempunyai semua tipe transparansi: opaque, transparent, atau translucent. PNG image bisa mempunyai kedalaman warna sampai 24 bit.

  • JPEG, JPEG image hanya bersifat 24 bit opaque. JPEG mempunyai kompresi yang tinggi untuk fotografi, tetapi bersifat lossy commpression, jadi image tidak akan sama persis dengan replika.


Membaca Image
Jadi bagaimana kita mentranslate GIF, PNG, atau JPEG file kedalam sesuatu yang dapat kita tampilkan? Dengan memanfaatkan API java Toolkit kita bisa memanggil method getImage(), yang akan memparsing file image dan akan mengembalikan image object. Contohnya sebagai berikut:
Toolkit toolkit = Toolkit.getDefaultToolkit();
Image image = toolkit.getImage(fileName);

Dengan kode diata sebernarnya image belum benar2 diload karena image akan diload pada thread yang lain, jika kita langsung menampilakan image tersebut dan image belum selesai diload maka hanya sebagian image saja yang akan ditampilkan atau malah tidak sama sekali.
Untuk mengatasi hal tersebut kita bisa memanfaatkan MediaTracker object untuk melihat apakah image sudah selesai diload, tetapi ada cara yang lebih mudah yaitu memanfaatkan ImageIcon class yang akan secara otomatis menggunkan MediaTracker dan menunggu sampai proses loading selesai. ImageIcon class di package javax.swing akan melakukan load pada image menggunakan Toolkit kemudian akan menunggu sampai proses loading selesai. Sebagai contoh sebagai berikut:
ImageIcon icon = new ImageIcon(fileName);
Image image = icon.getImage();

Oke, sekarang kita bisa mencona untuk menampilkan image dengan full-screen dengan memanfaatkan SimpleClassManager yang telah kita bahas pada key topic Full-Screen Graphics. Sekarang kita akan buat ImageTest.java dengan listing sebagai berikut:
package brain.left.games;

import java.awt.*;
import javax.swing.ImageIcon;
import javax.swing.JFrame;

public class ImageTest extends JFrame {
/**
*
*/
private static final long serialVersionUID = -4134922005163510957L;

public static void main(String[] args) {
DisplayMode displayMode;
if (args.length == 3) {
displayMode = new DisplayMode(Integer.parseInt(args[0]), Integer
.parseInt(args[1]), Integer.parseInt(args[2]),
DisplayMode.REFRESH_RATE_UNKNOWN);
} else {
displayMode = new DisplayMode(800, 600, 16,
DisplayMode.REFRESH_RATE_UNKNOWN);
}
ImageTest test = new ImageTest();
test.run(displayMode);
}

private static final int FONT_SIZE = 24;
private static final long DEMO_TIME = 10000;
private SimpleScreenManager screen;
private Image bgImage;
private Image opaqueImage;
private Image transparentImage;
private Image translucentImage;
private Image antiAliasedImage;
private boolean imagesLoaded;

public void run(DisplayMode displayMode) {
setBackground(Color.blue);
setForeground(Color.white);
setFont(new Font("Dialog", Font.PLAIN, FONT_SIZE));
imagesLoaded = false;
screen = new SimpleScreenManager();
try {
screen.setFullScreen(displayMode, this);
loadImages();
try {
Thread.sleep(DEMO_TIME);
} catch (InterruptedException ex) {
}
} finally {
screen.restoreScreen();
}
}

public void loadImages() {
bgImage = loadImage("images/tile-games/background.jpg");
opaqueImage = loadImage("images/tile-games/opaque.png");
transparentImage = loadImage("images/tile-games/transparent.png");
translucentImage = loadImage("images/tile-games/translucent.png");
antiAliasedImage = loadImage("images/tile-games/antialiased.png");
imagesLoaded = true;
// signal to AWT to repaint this window
repaint();
}

private Image loadImage(String fileName) {
return new ImageIcon(fileName).getImage();
}

public void paint(Graphics g) {
// set text anti-aliasing
if (g instanceof Graphics2D) {
Graphics2D g2 = (Graphics2D) g;
g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
}
// draw images
if (imagesLoaded) {
g.drawImage(bgImage, 0, 0, null);
drawImage(g, opaqueImage, 0, 0, "Opaque");
drawImage(g, transparentImage, 320, 0, "Transparent");
drawImage(g, translucentImage, 0, 300, "Translucent");
drawImage(g, antiAliasedImage, 320, 300,
"Translucent (Anti-Aliased)");
} else {
g.drawString("Loading Images...", 5, FONT_SIZE);
}
}

public void drawImage(Graphics g, Image image, int x, int y, String caption) {
g.drawImage(image, x, y, null);
g.drawString(caption, x + 5, y + FONT_SIZE + image.getHeight(null));
}
}

Benchmarking pada Image-Drawing
Oke kita akan lakukan benchmarking pada kecepatan image drawing, kita akan memodifikasi pada listing program sebelumnya menjadi ImageSpeedTest.java, idenya adalah sebagai berikut kita akan melakukan drawing secara berulang dengan periode waktu sebesar 1500 milidetik dan kemudian menghitung berapa kali image akan ditampilkan setiap second.

PERHTIAN!
Oke sebagai peringatan saja jangan lakukan hal semacam ini pada pembuatan game yang sesungguhnya, karena ini tidak akan bekerja. AWT event dispacth thread memanggil method paint(), tetapi disamping itu AWT event juga menangani keyboard, mouse dan masih banyak lagi event yang ditangani. Kita akan membahas hal ini pada pembuatan animasi.

listing ImageSpeesTest.java :
package brain.left.games;

import java.awt.*;
import javax.swing.ImageIcon;
import javax.swing.JFrame;

public class ImageSpeedTest extends JFrame {
private static final long serialVersionUID = -223039028953577654L;

public static void main(String args[]) {
DisplayMode displayMode;
if (args.length == 3) {
displayMode = new DisplayMode(Integer.parseInt(args[0]), Integer
.parseInt(args[1]), Integer.parseInt(args[2]),
DisplayMode.REFRESH_RATE_UNKNOWN);
} else {
displayMode = new DisplayMode(800, 600, 16,
DisplayMode.REFRESH_RATE_UNKNOWN);
}
ImageSpeedTest test = new ImageSpeedTest();
test.run(displayMode);
}

private static final int FONT_SIZE = 24;
private static final long TIME_PER_IMAGE = 1500;
private SimpleScreenManager screen;
private Image bgImage;
private Image opaqueImage;
private Image transparentImage;
private Image translucentImage;
private Image antiAliasedImage;
private boolean imagesLoaded;

public void run(DisplayMode displayMode) {
setBackground(Color.blue);
setForeground(Color.white);
setFont(new Font("Dialog", Font.PLAIN, FONT_SIZE));
imagesLoaded = false;
screen = new SimpleScreenManager();
try {
screen.setFullScreen(displayMode, this);
synchronized (this) {
loadImages();
// wait for test to complete
try {
wait();
} catch (InterruptedException ex) {
}
}
} finally {
screen.restoreScreen();
}
}

public void loadImages() {
bgImage = loadImage("images/tile-games/background.jpg");
opaqueImage = loadImage("images/tile-games/opaque.png");
transparentImage = loadImage("images/tile-games/transparent.png");
translucentImage = loadImage("images/tile-games/translucent.png");
antiAliasedImage = loadImage("images/tile-games/antialiased.png");
imagesLoaded = true;
// signal to AWT to repaint this window
repaint();
}

private final Image loadImage(String fileName) {
return new ImageIcon(fileName).getImage();
}

public void paint(Graphics g) {
// set text anti-aliasing
if (g instanceof Graphics2D) {
Graphics2D g2 = (Graphics2D) g;
g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
}
// draw images
if (imagesLoaded) {
drawImage(g, opaqueImage, "Opaque");
drawImage(g, transparentImage, "Transparent");
drawImage(g, translucentImage, "Translucent");
drawImage(g, antiAliasedImage, "Translucent (Anti-Aliased)");
// notify that the test is complete
synchronized (this) {
notify();
}
} else {
g.drawString("Loading Images...", 5, FONT_SIZE);
}
}

public void drawImage(Graphics g, Image image, String name) {
int width = screen.getFullScreenWindow().getWidth()
- image.getWidth(null);
int height = screen.getFullScreenWindow().getHeight()
- image.getHeight(null);
int numImages = 0;
g.drawImage(bgImage, 0, 0, null);
long startTime = System.currentTimeMillis();
while (System.currentTimeMillis() - startTime < TIME_PER_IMAGE) {
int x = Math.round((float) Math.random() * width);
int y = Math.round((float) Math.random() * height);
g.drawImage(image, x, y, null);
numImages++;
}
long time = System.currentTimeMillis() - startTime;
float speed = numImages * 1000f / time;
System.out.println(name + ": " + speed + " images/sec");
}
}

Program ini tidaklah menjadi patokan, karena semuanya tergantung dari jenis image yang digunakan serta spesifikasi komputer yang digunakan. Pada saat pembuatan program ini saya menggunakan komputer dengan spesifikasi processor Intel Centrino 1.7G, VGA Intel 64M, Resolusi layar 800x600 dan kedalaman bit 16, hasilnya adalah sebagai berikut:
Opaque: 10985.074 images/sec
Transparent: 239.68042 images/sec
Translucent: 260.98535 images/sec
Translucent (Anti-Aliased): 264.31424 images/sec

Dapat disimpulkan dari hasil benchmarking bahwa image dengan opaque paling cepat..!!, siip saatnya sekarang melangkah lebih lanjut kita akan membahas tentang animasi.

Animation
Animasi yang pertama adalah cartoon-style animation. Animasi jenis ini akan menampilkan urutan gambar secara bergantian. Sebagai contoh kita akan buat animasi dengan urutan sebagai berikut :

Urutan tersebut kita analogikan sebagai urutan frame, setiap frames akan ditampilkan dengan waktu tertentu, tetapi frame akan ditampilakan dengan waktu yang berbeda-beda. Sebagai contoh, misal frame pertama ditampilkan selama 200milisecond, frame kedua akan ditampilkan 75milisecond, dan seterusnya.
Kita akan menampilkan dengan urutan sebagai berikut :

Oke sekarang kita implementasikan urutan animasi tersebut pada programming. Sekarang kita akan membuat class Animation yang mempunyai tiga method penting: addFrame(), update(), dan getImage(). Method addFrame digunakan untuk menambahkan image kedalam animasi dengan waktu dalam milisecond. Method update() akan memberitahukan bahwa bahwa waktu penampilan telah selesai. Yang terakhir getImage() digunakan untuk mendapatkan image yang seharusnya akan ditampilakan setelah suatu waktu terlewati.
Listing Animation.java adalah sebagai berikut:
package brain.left.games;

import java.awt.Image;
import java.util.ArrayList;

/**
* Animation class digunakan untuk menangani urutan image yang akan
* ditampilakan pada frame untuk waktu tertentu.
*/
public class Animation {
private ArrayList frames;
private int currFrameIndex;
private long animTime;
private long totalDuration;

/**
* Konstruktor untuk Animasi.
*/
public Animation() {
frames = new ArrayList();
totalDuration = 0;
start();
}

/**
* Menambahkan image ke frame dengan waktu tertentu
*/
public synchronized void addFrame(Image image, long duration) {
totalDuration += duration;
frames.add(new AnimFrame(image, totalDuration));
}

/**
* Memulai animasi mulai dari awal frame.
*/
public synchronized void start() {
animTime = 0;
currFrameIndex = 0;
}

/**
* Melakukan update pada image pada animasi.
*/
public synchronized void update(long elapsedTime) {
if (frames.size() > 1) {
animTime += elapsedTime;
if (animTime >= totalDuration) {
animTime = animTime % totalDuration;
currFrameIndex = 0;
}
while (animTime > getFrame(currFrameIndex).endTime) {
currFrameIndex++;
}
}
}

/**
* Mendapatkan image yang sedang ditampilkan pada animasi yang sedang berlangsung,
* akan mengembalikan nilai null jika image tidak ada
*/
public synchronized Image getImage() {
if (frames.size() == 0) {
return null;
} else {
return getFrame(currFrameIndex).image;
}
}

private AnimFrame getFrame(int i) {
return frames.get(i);
}

private class AnimFrame {
Image image;
long endTime;

public AnimFrame(Image image, long endTime) {
this.image = image;
this.endTime = endTime;
}
}
}

Active Rendering
Untuk mengimplementasikan animation, kita harus secara terus-menerus melakukan update secara eficient. Sebelum melakukan rendering dengan memanggil paint(), kita harus memanggil method repaint() terlebih dahulu untuk memberikan signal AWT event dispatch thread untuk me-repaint screen, tetapi ini akan menyebabkan delay karena AWT thread mungkin sedang melakukan hal lain.
Cara lain untuk melakukan active rendering, active rendering akan melakukan drawing secara langsung pada screen di main thread. Dengan cara ini kita dapat mengkontrol yang sedang digambarkan pada screen saat ini, dan lebih mempersingkat kode.
Untuk menggunakan teknik active rendering, kita gunakan method getGraphic() dari class Componen untuk mendapatkan graphic contex.
Graphics g = screen.getFullScreenWindow().getGraphics();
draw(g);
g.dispose();

Simplekan ???, jangan lupa untuk memnggil dispose() setelah selesai melakukan draw(g), karena dengan memanggil dispose() akan melepaskan resourse yang sudah tidak dipakai.

Animasi Loop
Sekarang kita akan gunakan active rendering untuk melakukan draw pada sebuah loop. Animasi loop mempunyai step sebagai berikut:
  • Melakukan update pada animasi

  • Melakukan draw pada screen

  • Pilihan optional bisa menggunakan sleep untuk periode tertentu

  • Mulai dari step pertama kembali


jika dikodingkan maka loop akan menjadi sebagai berikut:
while (true) {
// Melakukan update pada animasi
updateAnimations();
// Melakukan draw pada screen
Graphics g = screen.getFullScreenWindow().getGraphics();
draw(g);
g.dispose();
// Optionl ---> melakukan sleep pada periode tertentu
try {
Thread.sleep(20);
}
catch (InterruptedException ex) { }
}

Pada kenyataannya loop pada animasi tidak akan berkjalan secara terus menerus, pada contoh yang akan kita buat kali ini, animasi akan berhenti setelah beberapa detik.
Listing program AnimationTest sebagai berikut:
package brain.left.games;

import java.awt.*;
import javax.swing.ImageIcon;
import javax.swing.JFrame;

public class AnimationTest1 {
public static void main(String args[]) {
DisplayMode displayMode;
if (args.length == 3) {
displayMode = new DisplayMode(Integer.parseInt(args[0]), Integer
.parseInt(args[1]), Integer.parseInt(args[2]),
DisplayMode.REFRESH_RATE_UNKNOWN);
} else {
displayMode = new DisplayMode(800, 600, 16,
DisplayMode.REFRESH_RATE_UNKNOWN);
}
AnimationTest1 test = new AnimationTest1();
test.run(displayMode);
}

private static final long DEMO_TIME = 5000;
private SimpleScreenManager screen;
private Image bgImage;
private Animation anim;

public void loadImages() {
// load images
bgImage = loadImage("images/tile-games/background.jpg");
Image player1 = loadImage("images/tile-games/hero1.png");
Image player2 = loadImage("images/tile-games/hero2.png");
Image player3 = loadImage("images/tile-games/hero3.png");
// create animation
anim = new Animation();
anim.addFrame(player1, 250);
anim.addFrame(player2, 150);
anim.addFrame(player1, 150);
anim.addFrame(player2, 150);
anim.addFrame(player3, 200);
anim.addFrame(player2, 150);
}

private Image loadImage(String fileName) {
return new ImageIcon(fileName).getImage();
}

public void run(DisplayMode displayMode) {
screen = new SimpleScreenManager();
try {
screen.setFullScreen(displayMode, new JFrame());
loadImages();
animationLoop();
} finally {
screen.restoreScreen();
}
}

public void animationLoop() {
long startTime = System.currentTimeMillis();
long currTime = startTime;
while (currTime - startTime < DEMO_TIME) {
long elapsedTime = System.currentTimeMillis() - currTime;
currTime += elapsedTime;
// update animation
anim.update(elapsedTime);
// draw to screen
Graphics g = screen.getFullScreenWindow().getGraphics();
draw(g);
g.dispose();
// take a nap
try {
Thread.sleep(20);
} catch (InterruptedException ex) {
}
}
}

public void draw(Graphics g) {
// draw background
g.drawImage(bgImage, 0, 0, null);
// draw image
g.drawImage(anim.getImage(), 0, 0, null);
}
}

Pada contoh program diatas animasi akan berjalan tetapi akan kelihatan ficker pada animasi, untuk mengatasi masalah ini maka akan dibahas pada Get Rid of Flicker and Tearing.

1 comment:

  1. Bagus mas... saya tunggu karya selanjutnya buat tambah-tambah ilmu. salam

    ReplyDelete