Thứ Sáu, 31 tháng 8, 2012

Sử dụng Custom Fonts hay Bitmap Fonts trong MIDP 2.0 Part II


Đến phần: 1 | 2

Trong phần trước, chúng ta đã tạo lớp clsFont để vẽ chữ bằng bitmap font. Bài học hôm nay sẽ sử dụng class này để xuất chữ lên màn hình.

Sử dụng Bitmap Font

Hãy mở code lớp clsCanvas để bắt đầu. Chúng ta sẽ thêm biến toàn cục myFont làm đối tượng lớp clsFont.


private midMain fParent;

private clsFont myFont;
    /** Creates a new instance of clsCanvas */
    public clsCanvas(midMain m) {

Nếu bạn mở folder images ra sẽ thấy ảnh fonts.png trong đó. Đấy chính là font chúng ta sử dụng trong phương thức clsFont.load( ). Thêm 2 dòng sau vào cuối phương thức load( ) của lớp clsCanvas.



        }
        
        myFont = new clsFont();
        myFont.load("/images/fonts.png");
    }

Thêm lời gọi phương thức unload của clsFont vào cuối phương thức unload( ) của clsCanvas



    public void unload(){
        // make sure the object get's destroyed

        myFont.unload();
        myFont = null;
    }

Cuối cùng ta dùng phương thức drawString( ) của clsFont để vẽ chữ lên màn hình. Thêm các dòng sau trước lời gọi phương thức flushGraphics( ) trong vòng lặp chính:



           g.fillRect(0, 0, screenW, screenH);
           
           myFont.drawString(g, "Hello Neo...", 10, 50);
           myFont.drawString(g, "1234567890", 10, 70);
           myFont.drawString(g, "ABCDEFG abcdefg", 10, 90);           
           flushGraphics();

Sau đó chúng ta test những gì vừa làm được:


Bitmap Font on WTK 2.5.1

Sau đây là code hoàn chỉnh lớp clsCanvas:

package MyGame;

import javax.microedition.lcdui.Graphics;
import javax.microedition.lcdui.Image;
import javax.microedition.lcdui.game.GameCanvas;

public class clsCanvas extends GameCanvas implements Runnable {

// key repeat rate in milliseconds
public static final int keyDelay = 250;    

//key constants
public static final int upKey = 0;
public static final int leftKey = 1;
public static final int downKey = 2;
public static final int rightKey = 3;
public static final int fireKey = 4;

//key states for up, left, down, right, and fire key
private boolean[] isDown = {
    false, false, false, false, false
};

//last time the key changed state
private long[] keyTick = {
    0, 0, 0, 0, 0
};

//lookup table for key constants :P
private int[] keyValue = {
    GameCanvas.UP_PRESSED, GameCanvas.LEFT_PRESSED,
    GameCanvas.DOWN_PRESSED, GameCanvas.RIGHT_PRESSED, 
    GameCanvas.FIRE_PRESSED
};

private boolean isRunning = true;    
private Graphics g;
private midMain fParent;

private clsFont myFont;
    /** Creates a new instance of clsCanvas */
    public clsCanvas(midMain m) {
        super(true);
        fParent = m;
        setFullScreenMode(true);
    }
    
    public void start(){
        Thread runner = new Thread(this);
        runner.start();
    }
    
    public void load(){
        try{
            // load the images here
            
        }catch(Exception ex){
            // exit the app if it fails to load the image
            isRunning = false;
            return;
        }
        
        myFont = new clsFont();
        myFont.load("/images/fonts.png");
    }
    
    public void unload(){
        // make sure the object gets destroyed

        myFont.unload();
        myFont = null;
    }
    
    public void checkKeys(int iKey, long currTick){
        long elapsedTick = 0;
        //loop through the keys
        for (int i = 0; i < 5; i++){ 
            // by default, key not pressed by user
            isDown[i] = false;
            // is user pressing the key
            if ((iKey & keyValue[i]) != 0){ 
                elapsedTick = currTick - keyTick[i];
                //is it time to toggle key state?
                if (elapsedTick >= keyDelay){ 
                    // save the current time
                    keyTick[i] = currTick;  
                    // toggle the state to down or pressed
                    isDown[i] = true; 
                }
            }
        }
    }

    public void run() {
       int iKey = 0;
       int screenW = getWidth();
       int screenH = getHeight();
       long lCurrTick = 0; // current system time in milliseconds;
       
       load();
       g = getGraphics();
       while(isRunning){
           
           lCurrTick = System.currentTimeMillis();
           iKey = getKeyStates();
           
           checkKeys(iKey, lCurrTick);
           
           if (isDown[fireKey]){
               isRunning = false;    
           }
           
           //restore the clipping rectangle to full screen
           g.setClip(0, 0, screenW, screenH);
           //set drawing color to black
           g.setColor(0x000000);
           //fill the screen with blackness
           g.fillRect(0, 0, screenW, screenH);
           
           myFont.drawString(g, "Hello Neo...", 10, 50);
           myFont.drawString(g, "1234567890", 10, 70);
           myFont.drawString(g, "ABCDEFG abcdefg", 10, 90);           
           flushGraphics();
           
           try{
               Thread.sleep(30);
           } catch (Exception ex){
               
           }
       }
       g = null;
       unload();
       fParent.destroyApp(false);
       fParent = null;
    }
}


Xem xét vấn đề
Bạn cũng có thể xem xét thêm phương thức wrappers trong lớp clsFont để vẽ kiểu int như sau:



    public void drawInt(Graphics g, int num, int x, int y){
        drawString(g, Integer.toString(num), x, y);
    }

    public void drawLong(Graphics g, long num, int x, int y){
        drawString(g, Long.toString(num), x, y);
    }

Một biến thể của phương thức drawString( ) vẽ ra chuỗi với lề bên phải có thể được tạo một cách dễ dàng với đoạn code sau:



    //draws string from right to left starting at x,y
    public void drawStringRev(Graphics g, String sTxt, int x, int y){
        // get the strings length
        int len = sTxt.length();

        // set the starting position
        int cx = x;
        
        // if nothing to draw return
        if (len == 0) {
            return;
        }
        
        // our fail-safe
        if (useDefault){
            g.drawString(sTxt, x, y, Graphics.TOP | Graphics.RIGHT);
            return;
        }

        // loop through all the characters in the string      
        for (int i = (len - 1); i >= 0; i--){

           // get current character 
           char c = sTxt.charAt(i);

           // get ordinal value or ASCII equivalent
           int cIndex = (int)c;

           // lookup the width of the character
           int w = charW[cIndex];

           // go to the next drawing position
           cx -= (w + charS);

           // draw the character
           drawChar(g, cIndex, cx, y, w, charH);
        }
    }

Với phương thức vẽ kiểu int:

    public void drawIntRev(Graphics g, int num, int x, int y){
        drawStringRev(g, Integer.toString(num), x, y);
    }

    public void drawLongRev(Graphics g, long num, int x, int y){
        drawStringRev(g, Long.toString(num), x, y);
    }

Những phương thức vừa thêm có thể dùng để vẽ điểm số và chữ số căn lề bên phải.

Bitmap Font với chức năng Word Wrap

Sau đây là các phương thức bổ sung để bạn có thể dùng chức năng word wrap với bitmap font


    // space between lines in pixels
    public int lineS = 2; 
    
    // draws words that wrap between x and x1
    public void drawStringWrap(Graphics g, String s, int x, int y, int x1){
        int len = s.length();
        
        // current x
        int tx = x;
        
        // current y
        int ty = y;
        
        /*
           word buffer contents width -
           I just thought it would be faster than 
           calling the String.length() method
        */
        int ww = 0; 
        
        // word buffer
        String sWord = "";
        
        for (int i = 0; i < len; i++){
            char c = s.charAt(i);
            int cIndex = (int)c;
            int cw = charW[cIndex];
            
            if ((cIndex > 32) && (cIndex < 127)){
              //if not a space and the character is printable 
                
              //add the character to the buffer
              sWord += String.valueOf(c);
              
              //compute the length of the current word
              ww += cw;
            } else {
               //if space or non-printable character
               
               // check if there is a word in the buffer 
               if (ww > 0) {
                   
                   //check if the word goes past the right margin
                   if ((tx + ww) > x1){
                       // carrage return
                       tx = x;
                       
                       // line feed
                       ty += (charH + lineS);
                   }
                   
                   // draw the contents of the word buffer
                   drawString(g, sWord, tx, ty);
               }
               
               //move to the next position
               tx += (ww + cw); 
               // clear the word buffer
               sWord = "";
               // word buffer width to zero
               ww = 0;
            }
        }

        // if there is a word remaining in the buffer then draw it
        if (ww > 0) {
            if ((tx + ww) > x1){
                tx = x;
                ty += (charH + lineS);
            }
            drawString(g, sWord, tx, ty);
        }
    }

Phương thức drawStringWrap( ) gồm các tham số sau
  • Graphics g - đối tượng Graphics dùng vẽ font
  • String s - chuỗi cần vẽ
  • int x - lề trái và tọa độ x bắt đầu vẽ
  • int y - tọa độ y dòng đầu tiên
  • int x1 - lề phải để chữ tự động xuống dòng
Phương thức drawStringWrap( ) sử dụng phương thức drawString( ) để vẽ các từ. Phương thức này xem "các kí tự không hiển thị được và dấu cách" là một "dấu phân cách giữa các từ". Bạn có thể thay đổi khoảng cách các dòng với nhau thông qua giá trị biến lineS.

Để thử nghiệm, bạn thêm đoạn code sau vào trước lời gọi flushGraphics( )


           myFont.drawString(g, "ABCDEFG abcdefg", 10, 90);

           myFont.drawStringWrap(g, "blah blah blah blah blah blah " +
                   "blah blah blah blah blah blah blah blah blah " +
                   "blah blah blah blah blah blah blah blah blah " +
                   "blah blah blah blah blah blah blah blah blah " +
                   "blah blah blah blah blah blah blah blah blah " +
                   "blah ", 10, 100, 166);
           flushGraphics();

Bạn sẽ thấy kết quả thế này


Bitmap Font With Wrapping on WTK 2.5.1

Sau đây là code hoàn chỉnh lớp clsFont sau khi thêm các phương thức mới

package MyGame;

import javax.microedition.lcdui.Graphics;
import javax.microedition.lcdui.Image;

public class clsFont {
    // additional space between characters
    public int charS = 0;
    
    // max clipping area
    public int screenW = 176;
    public int screenH = 208;
    
    // flag: set to true to use the Graphics.drawString() method
    // this is just used as a fail-safe
    public boolean useDefault = false;
    
    // height of characters
    public int charH = 10;
    
    // lookup table for character widths
    public int[] charW = {
    // first 32 characters    
    9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 
    9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 
    9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 
    9, 9,
    // space
    9,
    // everything else XD
    3, 5, 8, 8, 7, 8, 3, 5, 5, 6, 
    7, 3, 7, 3, 9, 6, 4, 6, 6, 6, 
    6, 6, 6, 6, 6, 3, 3, 6, 6, 6, 
    6, 9, 6, 6, 6, 6, 6, 6, 6, 6, 
    3, 6, 6, 6, 9, 6, 6, 6, 6, 6, 
    6, 7, 6, 6, 9, 6, 6, 6, 5, 9, 
    5, 4, 6, 4, 6, 6, 6, 6, 6, 6, 
    6, 6, 3, 4, 6, 3, 9, 6, 6, 6, 
    6, 6, 6, 6, 6, 6, 9, 6, 6, 6, 
    5, 3, 5, 4,
    // delete
    9};
    
    // the bitmap font image
    private Image imgFont;
    
    public clsFont() {
    }
    
    public boolean load(String imagePath){
        useDefault = false;
        try{
            // load the bitmap font
            if (imgFont != null){
                imgFont = null;
            }
            imgFont = Image.createImage(imagePath);
        } catch (Exception ex){
            // oohh we got an error then use the fail-safe
            useDefault = true;
        }
        return (!useDefault);
    }
    
    public void unload(){
        // make sure the object gets destroyed
        imgFont = null;
    }
    
    public void drawChar(Graphics g, int cIndex, int x, int y, int w, int h){
         // non printable characters don't need to be drawn
        if (cIndex < 33){
            return;
        }

        // neither does the delete character 
        if (cIndex > 126){
            return;
        }

        // get the characters position
        int cx = cIndex * 9;

        // reset the clipping rectangle
        g.setClip(0, 0, screenW, screenH);

        // resize and reposition the clipping rectangle
        // to where the character must be drawn
        g.clipRect(x, y, w, h);

        // draw the character inside the clipping rectangle
        g.drawImage(imgFont, x - cx, y, Graphics.TOP | Graphics.LEFT);
    }
    
    public void drawString(Graphics g, String sTxt, int x, int y){
        // get the strings length
        int len = sTxt.length();

        // set the starting position
        int cx = x;
        
        // if nothing to draw return
        if (len == 0) {
            return;
        }
        
        // our fail-safe
        if (useDefault){
            g.drawString(sTxt, x, y, Graphics.TOP | Graphics.LEFT);
            return;
        }

        // loop through all the characters in the string      
        for (int i = 0; i < len; i++){

           // get current character 
           char c = sTxt.charAt(i);

           // get ordinal value or ASCII equivalent
           int cIndex = (int)c;

           // lookup the width of the character
           int w = charW[cIndex];

           // draw the character
           drawChar(g, cIndex, cx, y, w, charH);

           // go to the next drawing position
           cx += (w + charS);
        }
    }
    
    // extended methods ***************************************

    public void drawInt(Graphics g, int num, int x, int y){
        drawString(g, Integer.toString(num), x, y);
    }

    public void drawLong(Graphics g, long num, int x, int y){
        drawString(g, Long.toString(num), x, y);
    }
    
    // Right align methods ****************************************
    
    //draws string from right to left starting at x,y
    public void drawStringRev(Graphics g, String sTxt, int x, int y){
        // get the strings length
        int len = sTxt.length();

        // set the starting position
        int cx = x;
        
        // if nothing to draw return
        if (len == 0) {
            return;
        }
        
        // our fail-safe
        if (useDefault){
            g.drawString(sTxt, x, y, Graphics.TOP | Graphics.RIGHT);
            return;
        }

        // loop through all the characters in the string      
        for (int i = (len - 1); i >= 0; i--){

           // get current character 
           char c = sTxt.charAt(i);

           // get ordinal value or ASCII equivalent
           int cIndex = (int)c;

           // lookup the width of the character
           int w = charW[cIndex];

           // go to the next drawing position
           cx -= (w + charS);

           // draw the character
           drawChar(g, cIndex, cx, y, w, charH);

        }
    }

    public void drawIntRev(Graphics g, int num, int x, int y){
        drawString(g, Integer.toString(num), x, y);
    }

    public void drawLongRev(Graphics g, long num, int x, int y){
        drawString(g, Long.toString(num), x, y);
    }

    // Word wrap method ****************************************
    
    // space between lines in pixels
    public int lineS = 2; 
    
    // draws words that wrap between x and x1
    public void drawStringWrap(Graphics g, String s, int x, int y, int x1){
        int len = s.length();
        
        // current x
        int tx = x;
        
        // current y
        int ty = y;
        
        /*
           word buffer contents width -
           I just thought it would be faster than 
           calling the String.length() method
        */
        int ww = 0; 
        
        // word buffer
        String sWord = "";
        
        for (int i = 0; i < len; i++){
            char c = s.charAt(i);
            int cIndex = (int)c;
            int cw = charW[cIndex];
            
            if ((cIndex > 32) && (cIndex < 127)){
              //if not a space and the character is printable 
                
              //add the character to the buffer
              sWord += String.valueOf(c);
              
              //compute the length of the current word
              ww += cw;
            } else {
               //if space or non-printable character
               
               // check if there is a word in the buffer 
               if (ww > 0) {
                   
                   //check if it goes past the right margin
                   if ((tx + ww) > x1){
                       // carrage return
                       tx = x;
                       
                       // line feed
                       ty += (charH + lineS);
                   }
                   
                   // draw the contents of the word buffer
                   drawString(g, sWord, tx, ty);
               }
               
               //move to the next position
               tx += (ww + cw); 
               
               // clear the word buffer
               sWord = "";
               
               // word buffer width to zero
               ww = 0;
            }
        }
        
        // if there is a word remaining in the buffer then draw it
        if (ww > 0) {
            if ((tx + ww) > x1){
                tx = x;
                ty += (charH + lineS);
            }
            drawString(g, sWord, tx, ty);
        }
    }
}

Còn rất nhiều thứ có thể làm để thêm chức năng cho clsFont. Ví dụ như căn chỉnh chữ, tối ưu các code trên hay thêm các font chữ khác và viết code cho người chơi lựa chọn loại font họ thích...

Chúc các bạn thành công !!

Thứ Ba, 28 tháng 8, 2012

Sử dụng Custom Fonts hay Bitmap Fonts trong MIDP 2.0 Part I


Đến phần: 1 | 2

Đã bao giờ các bạn chơi game và tự hỏi tại sao họ lại hiển thị được điểm số và lời thoại bằng nhiều màu sắc? Bạn thử đủ cách nhưng không làm được? Đúng vậy, drawString( ) và setFont( ) không thể thực hiện điều bạn muốn. Sự thực là các chữ và số đó được vẽ bằng ảnh, chúng ta gọi là Bitmap Font.

Bitmap font giải quyết những vấn đề của các bạn gặp phải khi chỉ sử dụng phương thức drawString( ). Và dùng bitmap font cũng làm cho game của bạn trông đẹp hơn.

Trong bài học này chúng ta sẽ học về cách sử dụng bitmap font với sự giúp đỡ của phương thức setClip( ) và ép kiểu ( type casting ). Nếu bạn chưa biết setClip để làm gì thì hãy học qua bài này trước: Cắt hình ảnh hay hiển thị một phần của ảnh

Bạn có thể sử dụng project này dể thực hành:

Bảng mã ASCII

Như tôi đã đề cập, ảnh sẽ được dùng thay cho chữ và số. Thực ra, chúng là một chuỗi ảnh, mỗi phần tử chứa một chữ ( hoặc số ). Sau đây là một ví dụ cho các bạn:
Click to see actual size.
Bitmap font sample zoomed in 2x.
Frame size: 9x10 pixels.
Click on the image to see what it actually looks like.

Vì đó là ảnh nên các bạn muốn vẽ đẹp xấu thế nào cũng được. Chỉ cần nhớ là font quá xấu thì bạn có thể làm người chơi chảy máu mắt...


... và xóa game của bạn luôn !!!


Các kí tự của bitmap font được sắp xếp theo bảng mã ASCII

ASCII table

Điều này làm chúng ta dễ dàng hơn trong việc sử dụng bitmap font bằng công thức sau:
positionX = ((int)theCharacter) * frameWidth;

Ép kiểu một char thành kiểu int cho kết quả là giá trị gốc của kí tự, tức giá trị thập phân trong bảng ASCII.

Bạn sẽ thấy các hình vuông có màu đen và xám trong ảnh ( ảnh project ). Đó là những kí tự không in ra. Bởi vì 32 kí tự đầu của bảng mã ASCII là không in ra được, có nghĩa là không thể xuất hiện trên màn hình. Hay như kí tự cuối - Del cũng không in ra được. Bởi vậy chỉ những kí tự có mã thập phân từ 31 đến 127 là vẽ lên màn hình được.

Vẽ Bitmap Font : lớp clsFont

Chúng ta tạo lớp mới tên là "clsFont". Khi xong các bạn sẽ thấy như sau:
package MyGame;

public class clsFont {

    
    /** Creates a new instance of clsFont */

    public clsFont() {
    }
    
}

Tiếp đó ta khai báo các biến toàn cục dưới lời khai báo lớp:
public class clsFont {

    // additional space between characters

    public int charS = 0;
    
    // max clipping area

    public int screenW = 176;

    public int screenH = 208;
    
    // flag: set to true to use the Graphics.drawString() method

    // this is just used as a fail-safe

    public boolean useDefault = false;

    
    // height of characters

    public int charH = 10;
    
    // lookup table for character widths

    public int[] charW = {

    // first 32 characters    
    9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 

    9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 

    9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 

    9, 9,

    // space
    9,

    // everything else :P
    3, 5, 8, 8, 7, 8, 3, 5, 5, 6, 

    7, 3, 7, 3, 9, 6, 4, 6, 6, 6, 

    6, 6, 6, 6, 6, 3, 3, 6, 6, 6, 

    6, 9, 6, 6, 6, 6, 6, 6, 6, 6, 

    3, 6, 6, 6, 9, 6, 6, 6, 6, 6, 

    6, 7, 6, 6, 9, 6, 6, 6, 5, 9, 

    5, 4, 6, 4, 6, 6, 6, 6, 6, 6, 

    6, 6, 3, 4, 6, 3, 9, 6, 6, 6, 

    6, 6, 6, 6, 6, 6, 9, 6, 6, 6, 

    5, 3, 5, 4,

    // delete character
    9};
    
    // the bitmap font image

    public Image imgFont;

Bấm ALT+Shift+F hoặc chọn "Fix Imports" từ menu "Source" để Netbean tự import các gói còn thiếu.

Biến charS để định nghĩa khoảng trống giữa các kí tự. Bạn có thể diều chỉnh nó nếu thấy các kí tự quá gần hay quá xa nhau. It can also be useful if you want to do a spring effect animation and make the characters bounce sideways.

Hai biến screenWscreenH định nghĩa khoảng màn hình lớn nhất chúng ta có thể vẽ lên. Chúng cũng chắc chắn rằng các khung cắt luôn rơi vào vùng đã định. Các bạn sẽ nhận ra điều này sau.

Tiếp theo chúng ta có biến useDefault. Điều này là bắt buộc và chúng ta sẽ dùng nó để báo hiệu khi hình ảnh không load được. Các bạn sẽ có thông tin về vấn đề này sau.

Biến charH lưu trữ chiều cao cực đại của các kí tự và dùng để điều chỉnh chiều cao khung cắt.

Mảng số nguyên charW[ ] lưu trữ độ rộng được tính toán trước của mỗi kí tự. Chúng ta dùng nó để tính toán vị trí chính xác các kí tự từ chuỗi đã cho và bao nhiêu không gian mỗi kí tự phải chiếm. Bằng cách này, chuỗi vẽ ra trông sẽ tự nhiên hơn. Việc này cũng giúp tiết kiệm không gian màn hình. Không giống như các phông chữ theo phong cách đơn khối ( mono block ) nơi mà mỗi kí tự sử dụng cùng một chiều rộng ngay cả khi có những kí tự to và kí tự nhỏ hơn. Một điều nữa cần chú ý về giá trị trong charW[ ] là chúng đã bao gồm khoảng cách giữa các kí tự rồi. Nhưng bạn vẫn có thể dùng biến charS để điều chỉnh khoảng cách nếu cần.

Biến cuối cùng imgFont sẽ lưu giữ ảnh bitmap font.

Bây giờ ta thêm phương thức load( ) để load ảnh chứa font và phương thức unload( ) để dọn dẹp khi không dùng đến class này nữa.
    /** Creates a new instance of clsFont */
    public clsFont() {
    }
    
    public boolean load(String imagePath){
        useDefault = false;
        try{
            // load the bitmap font
            if (imgFont != null){
                imgFont = null;
            }
            imgFont = Image.createImage(imagePath);
        } catch (Exception ex){
            // oohh we got an error then use the fail-safe
            useDefault = true;
        }
        return (!useDefault);
    }
    
    public void unload(){
        // make sure the object get's destroyed
        imgFont = null;
    }

Phương thức load( ) lấy tham số imagePath - đường dẫn đến bitmap font chúng ta cần load. Nếu load thất bại thì useDefault được set giá trị true. Bạn có thể in giá trị này ra màn hình để biết botmap font có được load thành công hay không. Phương thức load () cũng trả về giá trị nghịch đảo của useDefault, do đó bạn có thể sử dụng phương pháp này để vừa load vừa kiểm tra xem bitmap font có được load hay không:
    if (!myFont.load("/images/fonts.png")){
       /*
           ...do something to handle the error
           when the image fails to load...
           ... 
           ... 
      */
    }

Tiếp theo chúng ta thêm phương thức drawChar( ) dùng để vẽ một kí tự lên màn hình. Thêm phương thức này vào dưới phương thức unload( ) nhé.

    public void drawChar(Graphics g, int cIndex, int x, int y, int w, int h){
         // non printable characters don't need to be drawn
        if (cIndex < 33){
            return;
        }

        // neither does the delete character 
        if (cIndex > 126){
            return;
        }

        // get the characters position
        int cx = cIndex * 9;

        // reset the clipping rectangle
        g.setClip(0, 0, screenW, screenH);

        // resize and reposition the clipping rectangle
        // to where the character must be drawn
        g.clipRect(x, y, w, h);

        // draw the character inside the clipping rectangle
        g.drawImage(imgFont, x - cx, y, Graphics.TOP | Graphics.LEFT);
    }

...phương thức drawChar( ) có các tham số sau:

  • Graphics g - đối tượng dùng để vẽ kí tự
  • int cIndex - giá trị kí tự được vẽ ra
  • int x - tọa độ x khung cắt dành cho kí tự
  • int y - tọa độ y khung cắt dành cho kí tự
  • int w - chiều rộng kí tự và khung cắt
  • int h - chiều cao kí tự và khung cắt
Phương thức drawChar( ) kiểm tra xem kí tự sắp vẽ có thể in ra hay không, thông qua giá trị cIndex. Sau đó nó tính toán vị trí của kí tự trong ảnh, lưu trữ lại để dùng. Phương thức cũng reset khung cắt trở lại toàn màn hình và điều chỉnh chính xác vị trí khung cắt đến nơi kí tự phải được vẽ ra và làm phù hợp với kích thước của nó bằng phương thức clipRect( ). Sau đó, nó sẽ vẽ kí tự lên màn hình trong khung cắt.

Sử dụng setClip( ) kết hợp với clipRect( ) là một cách tốt để đảm bảo rằng khung cắt ở trong màn hình để các phương thức vẽ sẽ không vẽ ra ngoài màn hình. Một số điện thoại gặp lỗi khi bạn vẽ ra bên ngoài màn hình.

Hãy thêm phương thức cuối cùng cho lớp clsFont, phương thức drawString( ). Đây là phương thức dùng để vẽ lời thoại, điểm số, vân vân... Thêm vào dưới phương thức drawChar()

    public void drawString(Graphics g, String sTxt, int x, int y){

        // get the strings length
        int len = sTxt.length();

        // set the starting position
        int cx = x;
        
        // if nothing to draw return
        if (len == 0) {
            return;
        }
        
        // our fail-safe
        if (useDefault){
            g.drawString(sTxt, x, y, Graphics.TOP | Graphics.LEFT);
            return;
        }

        // loop through all the characters in the string      
        for (int i = 0; i < len; i++){

           // get current character 
           char c = sTxt.charAt(i);

           // get ordinal value or ASCII equivalent
           int cIndex = (int)c;

           // lookup the width of the character
           int w = charW[cIndex];

           // draw the character
           drawChar(g, cIndex, cx, y, w, charH);

           // go to the next drawing position
           cx += (w + charS);
        }
    }

Các tham số của phương thức drawString()

  • Graphics g - đối tượng Graphics để vẽ chuỗi
  • String sTxt - chuỗi cần vẽ
  • int x - tọa độ x của chuỗi
  • int y - tọa độ y của chuỗi
Phương thức drawString( ) sẽ lấy độ dài chuỗi và tọa độ X nơi kí tự đầu tiên được vẽ. Nó sẽ kiểm tra độ dài xem chuỗi có rỗng không, nếu không thì thoát khỏi phương thức.

Bây giờ bạn sẽ biết useDefault dùng để làm gì. Nếu nó được set giá trị true, có nghĩa là bitmap font không được load, thì phương thức drawString của đối tượng Graphics g sẽ được dùng để vẽ chuỗi lên thay vì dùng bitmap font.

Phương thức drawString( ) lặp qua từng kí tự trong chuỗi và lấy thứ tự của mỗi kí tự. Số thứ tự này được dùng để lấy chiều rộng của từng kí tự trong bảng charW[ ]. Nó cũng thông qua phương thức drawChar( ) để xác định chiều rộng của khung cắt. Sau khi kí tự vừa được vẽ ra, độ rộng được cộng thêm vào tọa độ vẽ hiện thời và tiếp tục với kí tự kế tiếp. Nếu bạn đặt giá trị khác 0 vào charS, nó vẫn sẽ được cộng vào.

Và sau đây là lớp clsFont hoàn chỉnh cho các bạn đối chiếu:

package MyGame;

import javax.microedition.lcdui.Graphics;
import javax.microedition.lcdui.Image;

public class clsFont {
    // additional space between characters
    public int charS = 0;
    
    // max clipping area
    public int screenW = 176;
    public int screenH = 208;
    
    // flag: set to true to use the Graphics.drawString() method
    // this is just used as a fail-safe
    public boolean useDefault = false;
    
    // height of characters
    public int charH = 10;
    
    // lookup table for character widths
    public int[] charW = {
    // first 32 characters    
    9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 
    9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 
    9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 
    9, 9,
    // space
    9,
    // everything else XD
    3, 5, 8, 8, 7, 8, 3, 5, 5, 6, 
    7, 3, 7, 3, 9, 6, 4, 6, 6, 6, 
    6, 6, 6, 6, 6, 3, 3, 6, 6, 6, 
    6, 9, 6, 6, 6, 6, 6, 6, 6, 6, 
    3, 6, 6, 6, 9, 6, 6, 6, 6, 6, 
    6, 7, 6, 6, 9, 6, 6, 6, 5, 9, 
    5, 4, 6, 4, 6, 6, 6, 6, 6, 6, 
    6, 6, 3, 4, 6, 3, 9, 6, 6, 6, 
    6, 6, 6, 6, 6, 6, 9, 6, 6, 6, 
    5, 3, 5, 4,
    // delete
    9};
    
    // the bitmap font image
    private Image imgFont;
    
    /** Creates a new instance of clsFont */
    public clsFont() {
    }
    
    public boolean load(String imagePath){
        useDefault = false;
        try{
            // load the bitmap font
            if (imgFont != null){
                imgFont = null;
            }
            imgFont = Image.createImage(imagePath);
        } catch (Exception ex){
            // oohh we got an error then use the fail-safe
            useDefault = true;
        }
        return (!useDefault);
    }
    
    public void unload(){
        // make sure the object get's destroyed
        imgFont = null;
    }
    
    public void drawChar(Graphics g, int cIndex, int x, int y, int w, int h){
         // non printable characters don't need to be drawn
        if (cIndex < 33){
            return;
        }

        // neither does the delete character 
        if (cIndex > 126){
            return;
        }

        // get the characters position
        int cx = cIndex * 9;

        // reset the clipping rectangle
        g.setClip(0, 0, screenW, screenH);

        // resize and reposition the clipping rectangle
        // to where the character must be drawn
        g.clipRect(x, y, w, h);

        // draw the character inside the clipping rectangle
        g.drawImage(imgFont, x - cx, y, Graphics.TOP | Graphics.LEFT);
    }
    
    public void drawString(Graphics g, String sTxt, int x, int y){
        // get the strings length
        int len = sTxt.length();

        // set the starting position
        int cx = x;
        
        // if nothing to draw return
        if (len == 0) {
            return;
        }
        
        // our fail-safe
        if (useDefault){
            g.drawString(sTxt, x, y, Graphics.TOP | Graphics.LEFT);
            return;
        }

        // loop through all the characters in the string      
        for (int i = 0; i < len; i++){

           // get current character 
           char c = sTxt.charAt(i);

           // get ordinal value or ASCII equivalent
           int cIndex = (int)c;

           // lookup the width of the character
           int w = charW[cIndex];

           // draw the character
           drawChar(g, cIndex, cx, y, w, charH);

           // go to the next drawing position
           cx += (w + charS);
        }
    }
    
}


Thứ Hai, 27 tháng 8, 2012

Thêm icon cho game

Tôi sẽ vào đề luôn không lằng nhằng gì thêm. Bạn có nhớ, khi tạo một MIDlet trong Netbean hay Java ME SDK các bạn sẽ thấy như hình dưới?

Name and Location Screen

Ô thứ 3 từ trên xuống chính là ô dành cho icon. Trước đó bạn nhớ add resource cho project của bạn bằng cách chuột phải vào Resource chọn Add Folder.

...sau đó chỉ đến folder chứa ảnh resource của bạn.

Một cách nữa, khi bạn lỡ tạo MIDlet xong rồi sẽ không làm được như trên. Bạn hãy làm như hình sau, chọn File -> "Tên project" Properties:
Project Properties via File Menu

Hoặc chuột phải vào project chon Properties cũng được.


Project Properties via Projects Panel Context Menu

Trước khi bạn bắt đầu thêm icon cho game, hãy nhìn bảng dưới đây. Đó là kích thước icon riêng cho từng dòng máy. Nên dùng ảnh định dạng PNG 8bit transparent ( trong suốt ).
Phone/
Model
Resolution
128x128128x160176x208208x176208x208240x320320x240352x416416x352
BlackBerry45x45
Motorola15x15
16x16
32x32
Nokia S4016x1618x18
24x24
29x29
n/an/a46x4646x48
42x29
n/an/an/a
Nokia S60
1st/2nd Edition
n/an/a42x29
29x29
n/an/an/an/a76x76n/a
Nokia S60
3rd Edition
n/an/a31x31
42x29
37x3737x3753x53
55x55
64x64
52x52
54x54
64x6476x76
84x58
Sagem18x18
Samsung16x16
29x29
32x32
Sanyo24x24
Sharp27x27
Siemens18x18
Sony Ericsson16x16
32x32


Sau đây là một số tool các bạn có thể dùng để vẽ icon PNG
Sau đây là một số screenshot mà Devlin chụp khi vẽ icon bằng phần mềm Fireworks

Đây là khi anh ta vẽ nền trong suốt ( transparent )

Settings for Icon with Alpha Transparency

...còn đây, không trong suốt
Settings for Icon with No Transparency

Đây là một icon có nền transparent với kích thước 42x29 pixel anh ta vẽ sẵn, các bạn có thể dùng thử cho bài học này. Nếu bạn vẽ đẹp, hãy thử xem ^^
game icon
42x29 pixels

Hãy quay lại bài học, mở Project Properties lên, chọn MIDlet trong menu bên trái. Bấm chọn midMain và bấm Edit. Một cửa sổ mới hiện ra, trong phần MIDlet Icon bạn có thể chọn  icon cho game của mình. Sau đó thì OK.
Edit MIDlet Dialog with Icons List

Giờ bạn chạy thử game trên IDE hoặc cài game vào điện thoại, sẽ thấy kết quả tương tự như sau

View of MIDlet with the Icon on the Emulator


Chúc các bạn thành công !!

Thứ Bảy, 18 tháng 8, 2012

No commands: Delicious graphical menus

Đôi lời muốn nói
Tôi viết thế cho oai ấy mà, chứ thực ra cũng chẳng có gì quan trọng lắm với người nào chăm chỉ. Vấn đề với người lười là, bài trước, tức là bài kiểm soát phím nhấn, có liên quan chặt chẽ với bài này. Vì nếu không viết code kiểm soát thì thế nào các bạn cũng bị tình trạng bấm phím mỗi một lần mà menu lựa chọn chạy liên tục.

Mĩ thuật là một phần quan trọng trong game bạn viết. Làm cho menu chính trông vừa vặn với game cũng rất quan trọng. Nó có thể làm cho người chơi muốn chơi game ngay khi vừa nhìn vào.

Chúng ta hãy bắt đầu bài học ngày hôm nay. Không nên dụng Command Listener và đối tượng cho menu. Đối tượng Command trông không giống như một thành phần của Game. Ở một vài dòng điện thoại, game bị ẩn đi khi menu Command được hiển thị. Thậm chí điện thoại cũng không hiển thị được nhãn của phím bấm bạn cần để bật menu khi game ở trạng thái full màn hình. Menu đồ họa ( graphical menu ) sẽ giải quyết cho chúng ta vấn đề trên, và làm game trông đẹp hơn.

Kỹ thuật clipping ( tạm dịch: cắt khung màn hình ) học trong bài quả địa cầu quay được sử dụng trong bài hướng dẫn này. Nếu bạn chưa học, hãy đọc lại bài đó: Cắt hình ảnh (hay hiển thị một phần của hình ảnh)

Load các ảnh
Trong bài học này chúng ta sẽ làm một menu dọc theo màn hình. MIDlet này sẽ được thiết kế cho thiết bị có màn hình 176x208. Nếu bạn muốn viết cho màn hình lớn hơn thì hãy vẽ hình khác và điều chỉnh thông số cho phù hợp.



Sample Vertical Menu

Tôi đã chuẩn bị một project cho các bạn thực hành. Đó là project trong bài trước chúng ta đã học: Input Handling: Keypress with Repeat Rate. Project cũng có sẵn ảnh chúng ta sẽ sử dụng cho bài học hôm nay. Các bạn có thể down về dùng ( chạy trên Netbean ):
Khi bạn mở project sẽ thấy 2 file ảnh như hình dưới đây:
logo.png - 176x208 pixels

logo.png

menuitems.png - 82x80 pixels

menuitems.png

Đầu tiên chúng ta cần load ảnh trước. Mở clsCanvas.java ra và khai báo các biến như sau trước hàm constructor:
private midMain fParent;

private Image imgBG;
private Image imgMenu;
 public clsCanvas(midMain m) {

Thêm lời gọi load ảnh vào phương thức load(). Nhớ để chúng trong try..catch hoặc dùng ngoại lệ.
 public void load(){
     try{
         // load the images here
         imgBG = Image.createImage("/images/logo.png");
         imgMenu = Image.createImage("/images/menuitems.png");
         
     }catch(Exception ex){

Sau đó gán giá trị cho imgMenu imgBG bằng null vào phương thức unload( ) để xóa đi khi không sử dụng nữa.
 public void unload(){
     // make sure the object get's destroyed
     imgMenu = null;
     imgBG = null;
 }

Giờ thì thay lời gọi vẽ logo vào lời gọi fillRect( ) để logo được vẽ lên màn hình thiết bị. Hãy xóa hoặc chú thích để vô hiệu hóa nó đi.
        //restore the clipping rectangle to full screen
        g.setClip(0, 0, getWidth(), getHeight());
        
        /* start - delete lines
        //set drawing color to black
        g.setColor(0x000000);
        //fill the whole screen
        g.fillRect(0, 0, getWidth(), getHeight());
        */ end - delete lines
        
        // set drawing color to white
        g.setColor(0xffffff);

...đây là lời gọi vẽ logo lên màn hình
        //restore the clipping rectangle to full screen
        g.setClip(0, 0, getWidth(), getHeight());

        g.drawImage(imgBG, 0, 0, Graphics.TOP | Graphics.LEFT);

Vẽ Menu

Chúng ta cần biến để lưu trữ phần đang được chọn trên menu. Hãy gọi nó là menuIndex và khai báo nó:
private Image imgMenu;

private int menuIndex = 0;
 public clsCanvas(midMain m) {

Đoạn code vẽ menu sẽ được đặt trong phương thức mới drawMenu( ). Phương thức này được đặt trên phương thức run( ).

 public void drawMenu(Graphics g){
     int cy = 0;
     for (int i = 0; i < 5; i++){
         //compute the Y position of the menu item
         cy = 64 + (i * 22);
         //set the clipping rectangle to where the item will be drawn
         g.setClip(47, cy, 82, 20);
         if (menuIndex == i){
           //draw the light button if the item is focused
           g.drawImage(imgMenu, 47, cy - 20, Graphics.TOP | Graphics.LEFT);
         } else {
           //draw the dark button if the item is not focused
           g.drawImage(imgMenu, 47, cy, Graphics.TOP | Graphics.LEFT);
         }
         //offset of the label is 6 pixels from the top of the button
         cy += 6;
         //set the clipping rectangle to where the label will be drawn
         g.setClip(47, cy, 82, 8);
         //draw the label so that it is inside the clipping rectangle
         g.drawImage(imgMenu, 47, cy - (40 + (i * 8)), Graphics.TOP | Graphics.LEFT);
     }
 }
 public void run() {

Menu sẽ được vẽ cách cạnh trên màn hình 64 pixel, mỗi lựa chọn trong menu cao 20 pixel, dài 82 pixel với 2 pixel làm biên. Tùy thuộc vào giá trị của menuIndex mà nút màu xanh đậm hay xanh nhạt được vẽ ra. Cuối cùng, nhãn ( chữ ) trong nút thấp hơn nút 6 pixel.

The menu will be drawn 64 pixels from the top of the screen and the menu items are drawn at 22 pixel intervals and since each of the menu items is only 20 pixels in height, a 2 pixelspace will be left between the menu items acting as a margin. Depending on the value ofmenuIndex, either a light-blue or a dark-blue button will be drawn. Finally, the label of each menu item is drawn 6 pixels lower than the menu items position.


Để hiển thị menu lên màn hình, hãy thêm đoạn code sau vào sau lời gọi logo
        //restore the clipping rectangle to full screen
        g.setClip(0, 0, getWidth(), getHeight());
        //draw the logo
        g.drawImage(imgBG, 0, 0, Graphics.TOP | Graphics.LEFT);
        
        //draw the menu
        drawMenu(g);
        //restore the clipping rectangle to full screen again
        g.setClip(0, 0, getWidth(), getHeight());        
        // set drawing color to white

Chú ý rằng chúng ta gọi setClip( ) 2 lần để cắt đầy màn hình. Lời gọi thứ 2 là cần thiết bởi vì khi drawMenu( ) được gọi xong, phương thức sẽ thay đổi kích cỡ và vị trí khung cắt cho nhãn cuối cùng trong menu. Reset khung cắt giúp ta vẽ được thêm hình trên màn hình.

Cuối cùng, làm cho menu tương tác được với phím được nhấn. Chỉnh sửa lệnh tương tác với phím nhấn bên dưới phương thức checkKeys( ) như sau:
        checkKeys(iKey, lCurrTick);

        if (isDown[upKey]){
            //move focus up
            if (menuIndex > 0){
                menuIndex--;
            } else {
                menuIndex = 4;
            }
        } else if (isDown[downKey]){
            //move focus down
            if (menuIndex < 4){
                menuIndex++;
            } else {
                menuIndex = 0;
            }
        } else if (isDown[fireKey]){
            //do action depending on the menu item selected
            if (menuIndex == 4){
                isRunning = false;
            }
        }

Đoạn code trên cho phép di chuyển highlight chọn hay nói cách khác là lựa chọn item trên menu. Bấm phím UP, lựa chọn sẽ di chuyển lên và ngược lại. Bấm FIRE sẽ là hành động, ở đây ta cho hành động đó là exit game với bất kì lựa chọn nào trên menu.

Dưới đây là code lớp clsCanvas.java hoàn chỉnh
package MyGame;

import javax.microedition.lcdui.Graphics;
import javax.microedition.lcdui.Image;
import javax.microedition.lcdui.game.GameCanvas;

public class clsCanvas extends GameCanvas implements Runnable {
// key repeat rate in milliseconds
public static final int keyDelay = 250; 

//key constants
public static final int upKey = 0;
public static final int leftKey = 1;
public static final int downKey = 2;
public static final int rightKey = 3;
public static final int fireKey = 4;

//key states for up, left, down, right, and fire key
private boolean[] isDown = {
 false, false, false, false, false
};
//last time the key changed state
private long[] keyTick = {
 0, 0, 0, 0, 0
};
//lookup table for key constants :P
private int[] keyValue = {
 GameCanvas.UP_PRESSED, GameCanvas.LEFT_PRESSED,
 GameCanvas.DOWN_PRESSED, GameCanvas.RIGHT_PRESSED,
 GameCanvas.FIRE_PRESSED
};

private boolean isRunning = true; 
private Graphics g;
private midMain fParent;

private Image imgBG;
private Image imgMenu;
//stores the focused menu item
private int menuIndex = 0;
 public clsCanvas(midMain m) {
     super(true);
     fParent = m;
     setFullScreenMode(true);
 }

 public void start(){
     Thread runner = new Thread(this);
     runner.start();
 }

 public void load(){
     try{
            // load the images here
         imgBG = Image.createImage("/images/logo.png");
         imgMenu = Image.createImage("/images/menuitems.png");         
     }catch(Exception ex){
         // exit the app if it fails to load the image
         isRunning = false;
         return;
     }
 }

 public void unload(){
     // make sure the object get's destroyed
     imgMenu = null;
     imgBG = null;
 }

 public void checkKeys(int iKey, long currTick){
     long elapsedTick = 0;
     //loop through the keys
     for (int i = 0; i < 5; i++){
         // by default, key not pressed by user
         isDown[i] = false;
         // is user pressing the key
         if ((iKey & keyValue[i]) != 0){
             elapsedTick = currTick - keyTick[i];
             //is it time to toggle key state?
             if (elapsedTick >= keyDelay){
                 // save the current time
                 keyTick[i] = currTick;
                 // toggle the state to down or pressed
                 isDown[i] = true;
             }
         }
     }
 }
 
 public void drawMenu(Graphics g){
     int cy = 0;
     for (int i = 0; i < 5; i++){
         //compute the Y position of the menu item
         cy = 64 + (i * 22);
         //set the clipping rectangle to where the item will be drawn
         g.setClip(47, cy, 82, 20);
         if (menuIndex == i){
           //draw the light button if the item is selected
           g.drawImage(imgMenu, 47, cy - 20, Graphics.TOP | Graphics.LEFT);
         } else {
           //draw the dark button if the item is not selected
           g.drawImage(imgMenu, 47, cy, Graphics.TOP | Graphics.LEFT);
         }
         //offset of the label is 6 pixels from the top of the button
         cy += 6;
         //set the clipping rectangle to where the label will be drawn
         g.setClip(47, cy, 82, 8);
         //draw the label so that it is inside the clipping rectangle
         g.drawImage(imgMenu, 47, cy - (40 + (i * 8)), Graphics.TOP | Graphics.LEFT);
     }
 }
 public void run() {
    int iKey = 0;
    long lCurrTick = 0; // current system time in milliseconds;
 
    load();
    g = getGraphics();
    while(isRunning){
     
        lCurrTick = System.currentTimeMillis();
        iKey = getKeyStates();
     
        checkKeys(iKey, lCurrTick);
        
        if (isDown[upKey]){
            //move focus up
            if (menuIndex > 0){
                menuIndex--;
            } else {
                menuIndex = 4;
            }
        } else if (isDown[downKey]){
            //move focus down
            if (menuIndex < 4){
                menuIndex++;
            } else {
                menuIndex = 0;
            }
        } else if (isDown[fireKey]){
            //do action depending on the menu item selected
            if (menuIndex == 4){
                isRunning = false;
            }
        }        
        //restore the clipping rectangle to full screen
        g.setClip(0, 0, getWidth(), getHeight());

        //draw the logo
        g.drawImage(imgBG, 0, 0, Graphics.TOP | Graphics.LEFT);
     
        //draw the menu
        drawMenu(g);
        //restore the clipping rectangle to full screen again
        g.setClip(0, 0, getWidth(), getHeight());        
        // set drawing color to white
        g.setColor(0xffffff);
        //display the key code last pressed
        g.drawString(Integer.toString(iKey), 2, 2, Graphics.TOP | Graphics.LEFT);
     
        flushGraphics();
     
        try{
            Thread.sleep(30);
        } catch (Exception ex){
         
        }
    }
    g = null;
    unload();
    fParent.destroyApp(false);
    fParent = null;
 }
}

Giờ thì nhấn F6 xem chúng ta làm được những gì.


Dưới đây là link một video chất lượng thấp về MIDlet chạy trên N70 ( tôi - Red Scorpion không xem được, có lẽ link đã vẹo rồi, haha ):

Bấm để xem clip

Đó là kỹ thuật vẽ menu dọc cổ điển. Tôi ( ở đây là Devlin ) đã từng sử dụng trong những game DOS viết bằng Turbo Pascal, DirectX game viết bằng VB 6.0 và C# với XNA Game Studio Express. Một điều cần nhớ là bạn bị hạn chế về việc sử dụng menu dọc. Tôi dùng nó dể ví dụ vì nó dễ làm. Bạn có thể thay đổi hình trong menu tùy ý thích, nhớ dùng ảnh có nền trong suốt và phối màu phù hợp. Dưới đây là ví dụ chọn nhân vật của một game giải đố:


Character sprites came from the MMORPG Trickster.

Làm menu bằng hình ảnh sẽ sử dụng khả năng sáng tạo và chỉ bị giới hạn bởi sức tưởng tượng của bạn. Nhưng cần nhớ rằng dù cho trí tưởng tượng của bạn có bay cao bay xa như uống Fristy thì chiếc di động cũng có rất nhiều mặt hạn chế.

John Constantine:

           "There's always a catch... damn cellphones!"