Chủ Nhật, 2 tháng 9, 2012

Kiểm soát nhập kí tự - hay nhập tên nhân vật

Game của bạn sẽ hay hơn nếu như nó cho phép người chơi nhập tên nhân vật. Việc này làm họ thấy họ là một phần trong trò chơi. Sau đó, tên của người chơi lại xuất hiện trong các đoạn hội thoại khiến họ cảm thấy như đang được nói chuyện. Tiếp nữa, họ có thể lưu điểm số với tên của mình làm bằng chứng chiến thắng...

Những thứ trên thế nào? Bạn có thể làm với textbox dễ dàng? No, my young padawan =))) . Đúng, bạn có thể làm vậy nhưng game bạn sẽ bị ẩn đi khi textbox xuất hiện. Khi đó nó sẽ dùng theme của điện thoại và trông không giống như textbox đó là một phần của game. Lại một lần nữa, đây là vấn đề về thẩm mỹ.

Chúng ta dùng code của bài này để hiển thị chữ bitmap. Nếu bạn chưa học thì hãy bắt đầu bài đó trước. Hay nếu không thì bạn cũng có thể dùng phương thức drawChar( ) của Graphics thay vì của clsFont chúng ta tự định nghĩa.

Thiếu phím bấm

Trong bài này chúng ta viết chữ như những game arcade console ngày xưa. Chúng chỉ có phím điều khiển 4 chiều, 6 phím chức năng, 1 nút start và 1 nút reset nhưng chúng vẫn có thể cho người chơi nhập tên. Còn chiếc điện thoại chúng ta đang có dù nhiều phím bấm hơn nhưng không phải máy nào cũng có mã phím giống nhau.

Rất may là lớp GameCanvas cho chúng ta các phím cơ bản giống nhau ở hầu hết các loại máy điện thoại:
  • Phím 2 - Nút UP
  • Phím 8 - Nút DOWN
  • Phím 4 - Nút LEFT
  • Phím 6 - Nút RIGHT

Chúng ta sẽ làm như sau:

  • LEFT - Chọn kí tự bên trái 
  • RIGHT - Chọn kí tự bên phải
  • UP - Cuộn kí tự lên trên
  • DOWN - Cuộn kí tự xuống dưới
  • FIRE - Chấp nhận thay đổi


Các giá trị

Bắt đầu sửa lớp clsCanvas. Thêm các biến toàn cục sau vào lớp này:
private clsFont myFont;

// this will hold the resulting text
private String prText = "AAAAAA";

// the currently selected character
private int prSelected = 0;

// width and height of the characters
private int prWidth = 9;
private int prHeight = 10;

// character spacing - includes width of character
private int prSpacing = 12;

// vars for timing the blinking cursor
private long prStart = 0;
private long prDelay = 100;
private boolean prShow = true;
    /** Creates a new instance of clsCanvas */
    public clsCanvas(midMain m) {

Biến String prText chứa đoạn text người chơi sẽ nhập vào và đồng thời phản hồi các kí tự người chơi chọn. Độ dài chuỗi gán cho prText cũng là độ dài tối đa lí tự có thể nhập và là các kí tự xuất hiện mặc định.

Biến nguyên prSelected giữ chỉ số của kí tự đang được chọn từ chuỗi prText.

Các biến prWidth và prHeight giữ chiều dài và chiều cao của kí tự. Chúng cũng được dùng trong clsCanvas.drawChar( ) để resize khung cắt.

Biến nguyên prSpacing xác định khoảng cách tính theo pixel giữa các kí tự. Giá trị được gán cho psSpacing là tổng độ rộng các kí tự và khoảng cách giữa chúng.

Thay thế một kí tự đơn

Tiếp theo, chúng ta viết một phương thức là replaceCharAt( ) cho phép chúng ta thay đổi một kí tự đơn lẻ trong chuỗi cho trước.

    public String replaceCharAt(String s, int pos, char c) {
       if (pos < (s.length() - 1)){
          return s.substring(0, pos) + c + s.substring(pos + 1);
       } else {
          return s.substring(0, pos) + c;
       }
    }    
    public void run() {

Phương thức này có 3 tham số:

  • String s - chuỗi đã cho
  • int pos - vị trí kí tự được thay đổi
  • Char c - kí tự thay thế

Người chơi điều khiển

Chúng ta cũng cần phương thức nhận và phản hồi phím người dùng nhấn và chuyển đổi hiển thị con trỏ khi cần thiết. Thêm phương thức updatePompt( ) như sau

    public void updatePrompt(long currTick){
        // get text length
        int len = prText.length();
        // get current selected characters ASCII value
        int prOrd = (int)prText.charAt(prSelected);
        
        // is it time to change the cursors visibility
        if ((currTick - prStart) >= prDelay){
            // update starting time
            prStart = currTick;
            // toggle cursor visibility
            prShow = !prShow;
        }
        
        if (isDown[leftKey]){
            // if not first character
            if (prSelected > 0){
                // select previous character in string
                prSelected--;
            }
        } else if (isDown[rightKey]){
            // if not last character
            if (prSelected < (len - 1)){
                // select next character in string
                prSelected++;
            }
        } else if (isDown[upKey]){
            if (prOrd == 97){ // small leter a
                prOrd = 90; // jump to capital letter Z
            } else if (prOrd == 65){ // capital letter A
                prOrd = 32; // jump to space character
            } else if (prOrd == 32){ // space character
                prOrd = 122; // jump to small leter z
            } else if ((prOrd > 97) || (prOrd > 65)){
                prOrd--; // previous character
            }
            // replace the selected character with the new character
            prText = replaceCharAt(prText, prSelected, (char)prOrd);
        } else if (isDown[downKey]){
            if (prOrd == 32){  // space character
                prOrd = 65; // jump to capilat letter A
            } else if (prOrd == 90){ // capital letter Z
                prOrd = 97; // jump to small letter a
            } else if (prOrd == 122){ // small letter z
                prOrd = 32; // jump to space character
            } else if ((prOrd < 90) || (prOrd < 122)){
                prOrd++; // next character
            }
            // replace the selected character with the new character
            prText = replaceCharAt(prText, prSelected, (char)prOrd);
        }
    }
    public void run() {

Phương thức này sử dụng giá trị Long currTick là thời gian hiện tại tính bằng milli giây. Giá trị này dùng kiểm tra xem thời gian qua giữa prStart và currTick có lớn hơn khoảng thời gian delay prDelay hay không. Nếu có, biến boolean prShow sẽ điều khiển con trỏ ẩn hay hiện qua giá trị true/false.

Chú ý rằng phương thức updatePrompt( ) chấp nhận chữ hoa, chữ nhỏ, và dấu cách. Không khó để thêm số vào hay thậm chí là toàn bộ các kí tự. Nhưng trong trường hợp này chỉ có chữ hoa.

Bạn cũng sẽ muốn giữ kí tự "dấu cách", vì người chơi có thể dùng nó khi tên họ ngắn mà kí tự bạn cho quá dài. Bằng cách này bạn có thể làm gọn các khoảng trống từ giá trị của prText khi người chơi đã nhập tên xong.

Hiển thị kí tự

Cuối cùng chúng ta viết phương thức drawPrompt( ) hiện chữ và con trỏ.

    public void drawPrompt(Graphics g, int x, int y){
        // get length of the string
        int len = prText.length();
        
        // loop through the characters
        for (int i = 0; i < len; i++){
            // get ASCII value
            int cIndex = (int)prText.charAt(i);
            
            // compute next drawing position - X 
            int cx = x + (i * prSpacing);
            
            // draw the character
            myFont.drawChar(g, cIndex, cx, y, prWidth, prHeight);
            
            // compute cusor position - Y
            int cy = y + 6;

            // if current char is selected
            if (i == prSelected){
                // if cursor shoud be visible
                if (prShow) {
                    //draw the cursor using char 95 or underscore character
                    myFont.drawChar(g, 95, cx, cy, prWidth, prHeight);
                }
            } else {
                //draw the cursor using char 95 or underscore character
                myFont.drawChar(g, 95, cx, cy, prWidth, prHeight);
            }
        }
    }
    public void run() {

Phương thức này vẽ từng chữ trong prText và gạch dưới mỗi chữ. Con trỏ nhấp nháy dưới chữ đang được chọn.

Phương thức này có 3 tham số:
  • Graphics g - Đối tượng Graphics dùng vẽ chữ.
  • int x - Tọa độ x bắt đầu vẽ
  • int y - Tọa độ y bắt đầu vẽ

Thực hiện

Hãy chỉnh sửa code phương thức run( ) để dùng các phương thức vừa viết. Đầu tiên, lời gọi phương thức updatePrompt( ) bên dưới lời gọi checkKey( )
           checkKeys(iKey, lCurrTick);
           
           updatePrompt(lCurrTick);           
           if (isDown[fireKey]){

Sau đó sử dụng:
           g.fillRect(0, 0, screenW, screenH);
           
           myFont.drawString(g, "Enter your name:", 10, 20);

           drawPrompt(g, 10, 40);

           myFont.drawString(g, "LEFT/RIGHT - move cursor", 10, 80);
           myFont.drawString(g, "UP/DOWN - change letter", 10, 100);

           myFont.drawString(g, "Hello " + prText.trim() + "!", 10, 140);           
           flushGraphics();

...và chạy thử để xem kết quả:
User input sample output.

Cuối cùng là lớp clsCanvas hoàn chỉnh
package MyGame;

import javax.microedition.lcdui.Graphics;
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;

// this will hold the resulting text
private String prText = "AAAAAA";

// the currently selected character
private int prSelected = 0;

// width and height of the characters
private int prWidth = 9;
private int prHeight = 10;

// character spacing - includes width of character
private int prSpacing = 12;

// vars for timing the blinking cursor
private long prStart = 0;
private long prDelay = 100;
private boolean prShow = true;
    /** 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 String replaceCharAt(String s, int pos, char c) {
       if (pos < (s.length() - 1)){
          return s.substring(0, pos) + c + s.substring(pos + 1);
       } else {
          return s.substring(0, pos) + c;
       }
    }    
       
    public void updatePrompt(long currTick){
        // get text length
        int len = prText.length();
        // get current selected characters ASCII value
        int prOrd = (int)prText.charAt(prSelected);
        
        // is it time to change to cursors visibility
        if ((currTick - prStart) >= prDelay){
            // update starting time
            prStart = currTick;
            // toggle cursor visibility
            prShow = !prShow;
        }
        
        if (isDown[leftKey]){
            // if not first character
            if (prSelected > 0){
                // select previous character in string
                prSelected--;
            }
        } else if (isDown[rightKey]){
            // if not last character
            if (prSelected < (len - 1)){
                // select next character in string
                prSelected++;
            }
        } else if (isDown[upKey]){
            if (prOrd == 97){ // small leter a
                prOrd = 90; // jump to capital letter Z
            } else if (prOrd == 65){ // capital letter A
                prOrd = 32; // jump to space character
            } else if (prOrd == 32){ // space character
                prOrd = 122; // jump to small leter z
            } else if ((prOrd > 97) || (prOrd > 65)){
                prOrd--; // previous character
            }
            // replace the selected character with the new character
            prText = replaceCharAt(prText, prSelected, (char)prOrd);
        } else if (isDown[downKey]){
            if (prOrd == 32){  // space character
                prOrd = 65; // jump to capilat letter A
            } else if (prOrd == 90){ // capital letter Z
                prOrd = 97; // jump to small letter a
            } else if (prOrd == 122){ // small letter z
                prOrd = 32; // jump to space character
            } else if ((prOrd < 90) || (prOrd < 122)){
                prOrd++; // next character
            }
            // replace the selected character with the new character
            prText = replaceCharAt(prText, prSelected, (char)prOrd);
        }
    }
    
    public void drawPrompt(Graphics g, int x, int y){
        // get length of the string
        int len = prText.length();
        
        // loop through the characters
        for (int i = 0; i < len; i++){
            // get ASCII value
            int cIndex = (int)prText.charAt(i);
            
            // compute next drawing position - X 
            int cx = x + (i * prSpacing);
            
            // draw the character
            myFont.drawChar(g, cIndex, cx, y, prWidth, prHeight);
            
            // compute cusor position - Y
            int cy = y + 6;

            // if current char is selected
            if (i == prSelected){
                // if cursor shoud be visible
                if (prShow) {
                    //draw the cursor using char 95 or underscore character
                    myFont.drawChar(g, 95, cx, cy, prWidth, prHeight);
                }
            } else {
                //draw the cursor using char 95 or underscore character
                myFont.drawChar(g, 95, cx, cy, prWidth, prHeight);
            }
        }
    }
    public void run() {
       int iKey = 0;
       int screenW = getWidth();
       int screenH = getHeight();
       long lCurrTick = 0; // current system time in milliseconds;
       
       String sName = "";
       
       load();
       g = getGraphics();
       while(isRunning){
           
           lCurrTick = System.currentTimeMillis();
           iKey = getKeyStates();

           checkKeys(iKey, lCurrTick);

           updatePrompt(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, "Enter your name:", 10, 20);

           drawPrompt(g, 10, 40);

           myFont.drawString(g, "LEFT/RIGHT - move cursor", 10, 80);
           myFont.drawString(g, "UP/DOWN - change letter", 10, 100);

           myFont.drawString(g, "Hello " + prText.trim() + "!", 10, 140);           
           flushGraphics();
           
           try{
               Thread.sleep(30);
           } catch (Exception ex){
               
           }
       }
       g = null;
       unload();
       fParent.destroyApp(false);
       fParent = null;
    }
}

Đến đây là hết các bài học của Devlin.


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

Tải dữ liệu Tile Map từ một file

Có một trò chơi loại giải đố trong các ví dụ demo của NetBeans. Đó là trò đẩy hộp. Trò chơi này cho bạn ví dụ về load dữ liệu map từ file text. Nhưng thứ mà tôi nhớ nhất đó là code game đó làm tôi hoa hết cả mắt, heh.

Sau đây là giải pháp của tôi về việc này. Thay vì load dữ liệu map từ text file chúng ta sẽ load từ file dạng nhị phân.

Bài này sử dụng tiếp code của bài trước Dùng lớp TiledLayer hiển thị tilemap. Nếu chưa học qua bạn nên bắt đầu từ bài đó trước và hãy nắm chắc trước khi sang bài này.

Bạn hãy down map dạng SMP sau về. Đó là map tạo từ phần mềm Simple Tile Map Editor của Devlin.
samplemap.smp (155 bytes) - Download

Tạo folder maps trong folder src và save map vào đó.

Bạn có thể mở ra xem, sửa map bằng phần mềm của Devlin, download tại đây.

Để xem trước map, bấm nút Open và chọn đến file map trên bạn vừa down về. Kế tiếp, chọn tileset1.png là file tileset của bạn (nếu chưa biết là gì thì hãy đọc bài trước để biết thêm chi tiết). Bạn sẽ thấy như sau:
Click to Enlarge

Khi đã xong mọi việc, bắt tay vào chỉnh sửa code lớp clsCanvas. Thêm phương thức sau vào trước phương thức loadBlockMap( ):

    public TiledLayer getMap(String fpath, Image ftiles){
        TiledLayer tMap = null;
        try {
            // open the file
            InputStream is = this.getClass().getResourceAsStream(fpath);
            DataInputStream ds = new DataInputStream(is);
            try {
                // skip the descriptor
                ds.skipBytes(8);
                
                // read map width
                int mW = ds.readByte();
                
                // read map height
                int mH = ds.readByte();
                
                // read tile width
                int tW = ds.readByte();
                
                // read tile height
                int tH = ds.readByte();
                
                // create a new tiled layer
                tMap = new TiledLayer(mW, mH, ftiles, tW, tH);
                
                // loop through the map data
                for (int rCtr = 0; rCtr < mH; rCtr++){
                  for (int cCtr = 0; cCtr < mW; cCtr++){
                    // read a tile index 
                    byte nB = ds.readByte();  
                    
                    // if tile index is non-zero
                    // tile index 0 is usually a blank tile
                    if (nB > 0) {
                        // assign (tile index + 1) to the current cell
                        // TiledLayer objects start tile index at 1
                        // instead of 0
                        tMap.setCell(cCtr, rCtr, nB + 1);  
                    }
                  }  
                }

           } catch (Exception ex) {
               tMap = null;
               System.err.println("map loading error : " + ex.getMessage());
           }
           // close the file
           ds.close();
           ds = null;
           is = null;
        } catch (Exception ex) {
            tMap = null;
            System.err.println("map loading error : " + ex.getMessage());
        }
        
        // return the newly created map or null if loading failed
        return tMap;
    }

Bấm Alt+Shift+F nếu dùng NetBeans để import các statements còn thiếu.

Chúng ta thêm một phương thức nữa là loadBlockMapFile( ) dùng getMap( ) để load dữ liệu map lên và khởi tạo cho tiled layer blockMap. Việc này giúp bạn bảo vệ phương thức loadBlockMap( ) để tham khảo sau. Thêm đoạn code sau vào dưới phương thức loadBlockMap( ):

    public void loadBlockMapFile(){
        //initialize blockMap
        blockMap = getMap("/maps/samplemap.smp", imgTileset);
    }

Giờ ta thay lời gọ phương thức loadBlockMap( ) trong phương thức load( ) bằng loadBlockMapFile( )
        loadRoadMap();
        loadBlockMapFile();
        
    }

Bạn chạy thử chương trình sẽ thấy như sau:
Screen shot of project running in the emulator

Map này chỉ khác với ví dụ trước ở vị trí chướng ngại vật và màu sắc, không có gì đáng kể.

Sau đây là clsCanvas hoàn chỉnh cho các bạn tham khảo:
package MyGame;

import java.io.DataInputStream;
import java.io.InputStream;
import java.util.Random;
import javax.microedition.lcdui.Graphics;
import javax.microedition.lcdui.Image;
import javax.microedition.lcdui.game.GameCanvas;
import javax.microedition.lcdui.game.Sprite;
import javax.microedition.lcdui.game.TiledLayer;

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 imgVan;
private Sprite Van;

private int vanSpeed = 2;

private Image imgTileset;
private TiledLayer roadMap;
private TiledLayer blockMap;

    /** 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 loadRoadMap(){
        //initialize the roadMap
        roadMap = new TiledLayer(11, 13, imgTileset, 16, 16);
        
        // Create a new Random for randomizing numbers
        Random Rand = new Random();
        
        //loop through all the map cells
        for (int y = 0; y < 13; y++){
            for (int x = 0; x < 11; x++){
                // get a random tile index between 2 and 5
                int index = (Math.abs(Rand.nextInt()>>>1) % (3)) + 2;
                
                // set the tile index for the current cell
                roadMap.setCell(x, y, index);
            }
        }
        
        Rand = null;
    }
    
    public void loadBlockMap(){
        // define the tile indexes to be used for each map cell
        byte[][] blockData = {
            {10, 8 , 7 , 6 , 10, 9 , 8 , 7 , 6 , 10, 9 },  
            {6 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 8 },  
            {7 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 7 },  
            {8 , 0 , 0 , 10, 6 , 0 , 0 , 7 , 0 , 0 , 6 },  
            {9 , 0 , 0 , 0 , 0 , 0 , 0 , 8 , 0 , 0 , 10},  
            {10, 0 , 0 , 0 , 0 , 0 , 0 , 9 , 0 , 0 , 9 },  
            {6 , 0 , 0 , 8 , 0 , 0 , 0 , 0 , 0 , 0 , 8 },  
            {7 , 0 , 0 , 7 , 0 , 0 , 0 , 0 , 0 , 0 , 7 },  
            {8 , 0 , 0 , 6 , 0 , 0 , 0 , 10, 0 , 0 , 6 },  
            {9 , 0 , 0 , 10, 0 , 0 , 7 , 6 , 0 , 0 , 10},  
            {10, 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 9 },  
            {6 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 8 },  
            {7 , 8 , 9 , 10, 6 , 7 , 8 , 9 , 10, 6 , 7 }
        };
        
        //initialize blockMap
        blockMap = new TiledLayer(11, 13, imgTileset, 16, 16);
        
        //loop through all the map cells
        for (int y = 0; y < 13; y++){
            for (int x = 0; x < 11; x++){
                // set the tile index for the current cell
                // take note of the reversed indexes for blockData
                blockMap.setCell(x, y, blockData[y][x]);
            }
        }
        
        blockData = null;
    }
    
    public void loadBlockMapFile(){
        //initialize blockMap from a binary file
        blockMap = getMap("/maps/samplemap.smp", imgTileset);
    }

    public TiledLayer getMap(String fpath, Image ftiles){
        TiledLayer tMap = null;
        try {
            // open the file
            InputStream is = this.getClass().getResourceAsStream(fpath);
            DataInputStream ds = new DataInputStream(is);
            try {
                // skip the descriptor
                ds.skipBytes(8);
                
                // read map width
                int mW = ds.readByte();
                
                // read map height
                int mH = ds.readByte();
                
                // read tile width
                int tW = ds.readByte();
                
                // read tile height
                int tH = ds.readByte();
                
                // create a new tiled layer
                tMap = new TiledLayer(mW, mH, ftiles, tW, tH);
                
                // loop through the map data
                for (int rCtr = 0; rCtr < mH; rCtr++){
                  for (int cCtr = 0; cCtr < mW; cCtr++){
                    // read a tile index 
                    byte nB = ds.readByte();  
                    
                    // if tile index is non-zero
                    // tile index 0 is usually a blank tile
                    if (nB > 0) {
                        //assign (tile index + 1) to the current cell
                        tMap.setCell(cCtr, rCtr, nB + 1);  
                    }
                  }  
                }

           } catch (Exception ex) {
               tMap = null;
               System.err.println("map loading error : " + ex.getMessage());
           }
           // close the file
           ds.close();
           ds = null;
           is = null;
        } catch (Exception ex) {
            tMap = null;
            System.err.println("map loading error : " + ex.getMessage());
        }
        
        // return the newly created map or null if loading failed
        return tMap;
    }    
    
    public void load(){
        try{
            // load the images here
            imgVan = Image.createImage("/images/van.png");
            imgTileset = Image.createImage("/images/tileset1.png");
        }catch(Exception ex){
            // exit the app if it fails to load the image
            isRunning = false;
            return;
        }
        
        // initialize the Sprite object
        Van = new Sprite(imgVan, 18, 18);
        
        // show the frame 1 - the second frame
        Van.setFrame(1);
        
        // move to 50, 50 (X, Y)
        Van.setPosition(16, 16);
        
        loadRoadMap();
        loadBlockMapFile();
        
    }
    
    public void unload(){
        // make sure the object gets destroyed
        blockMap = null;
        roadMap = null;
        Van = null;
        imgTileset = null;
        imgVan = 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;    
           }
           
           // get the current position of the van
           int cx = Van.getX();
           int cy = Van.getY();
           
           // save the current position in temporary vars
           // so we can restore it when we hit a block
           int tx = cx;
           int ty = cy;
           
           if ((iKey & GameCanvas.UP_PRESSED) != 0){
               // show the van facing up
               Van.setFrame(0);
               
               // move the van upwards
               cy -= vanSpeed;
           } else if ((iKey & GameCanvas.DOWN_PRESSED) != 0){
               // show the van facing down
               Van.setFrame(1);
               
               // move the van downwards
               cy += vanSpeed;
           } else if ((iKey & GameCanvas.LEFT_PRESSED) != 0){
               // show the van facing left
               Van.setFrame(2);
               
               // move the van to the left
               cx -= vanSpeed;
           } else if ((iKey & GameCanvas.RIGHT_PRESSED) != 0){
               // show the van facing right
               Van.setFrame(3);
               
               // move the van to the right
               cx += vanSpeed;
           }
           
           // update the vans position
           Van.setPosition(cx, cy);
           
           //check if the van hits a block
           if (Van.collidesWith(blockMap, true)){
             //reset the van to the original position  
             Van.setPosition(tx, ty);
           }
           
           //restore the clipping rectangle to full screen
           g.setClip(0, 0, screenW, screenH);
           
           /* comment out or remove this code
            
           set drawing color to black
           g.setColor(0x000000);
            
           fill the screen with blackness
           g.fillRect(0, 0, screenW, screenH);

            */
           
           //draw the road
           roadMap.paint(g);
           
           //draw the blocks
           blockMap.paint(g);
           
           // draw the sprite
           Van.paint(g);
           
           flushGraphics();
           
           try{
               Thread.sleep(30);
           } catch (Exception ex){
               
           }
       }
       g = null;
       unload();
       fParent.destroyApp(false);
       fParent = null;
    }
}

Bạn cũng có thể tìm các phần mềm tạo tilemap trên mạng, thậm chí có thể save dữ liệu map vào file xml, vân vân...

Link vài phần mềm làm map:
Cuối cùng, bạn cũng có thể dùng chức năng Game Builder trong NetBeans 6 và gói Mobility.

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

Dùng lớp TiledLayer hiển thị Tile Map

Một tile map là map game hay quang cảnh làm nền để nhân vật đi lên trên. Tile map có trong game như Diablo, Fallout, Command and Conquer, StarCraft, Final Fantasy, Pokemon, và hầu hết các game RPG/MMORPG và game chiến thuật 2D. Sau đây là ví dụ trong sử dụng Tile map:

Herbarrio Screenshot
Screenshot of the game Herbarrio

Gọi là tile map bởi nó được tạo nên từ nhiều ảnh nhỏ gọi là tile. Tile được vẽ nối nhau rất phù hợp và tạo nên map cho game. Một bộ tile cùng chủ đề gọi là tileset. 

Lí do phải dùng tile map là để game không phải load ảnh map lớn mà chỉ phải load một tileset nhỏ và dùng lại nó để vẽ map. Ta có thể dùng cùng một tileset tạo nhiều map khác nhau. Việc này giúp chúng ta tiết kiệm bộ nhớ và làm game load nhanh hơn.

Trong bài học hôm nay, chúng ta sẽ sử dụng lại code của bài trước: Sử dụng lớp Sprite và di chuyển Sprite trong MIDP 2.0. Giờ ta sẽ làm một map cho chiếc xe tải chạy và chướng ngại vật chắn đường xe bằng lớp TiledLayer của MIDP 2.0, được tạo ra đặc biệt để vẽ map.

Map chúng ta làm sẽ có kích thước 176x208 (độ phân giải của hầu hết máy Nokia dòng s60v2 như N70, N72...). Thôi không dài dòng nữa, đây là ví dụ giải thích cho các bạn dễ hiểu (hay càng khó hiểu hơn nhỉ, hahaha)
TileMap Diagram
Giải thích về Tile Map

Khung chữ nhật xanh là tile map, những ô trong nó là các tile vẽ nên map. Kích thước tile map dựa vào số hàng và cột của các tile trong đó. Như ví dụ trên thì map này có kích thước 11x13 tile và mỗi tile có kích thước 16x16 pixel. Rất vừa vặn cho màn hình 176x208.

Tạo và hiển thị TiledLayer

Devlin đã chuẩn bị sẵn cho chúng ta một tileset dưới đây. Bạn có thể down về dùng hoặc có thể tự vẽ nếu vẽ đẹp (share cho tôi dùng với ^^).
Tileset
Tileset Image
Dimensions: 160x16 pixels
Tile Size: 16x16 pixels


Đây là hình đã phóng to gấp đôi

Tileset : larger view
Tileset Image zoomed in at 2x

Làm việc nào. Mở clsCanvas, thêm biến cho đối tượng TiledLayer và ảnh tileset 
private Image imgTileset;
private TiledLayer roadMap;
private TiledLayer blockMap;
    /** Creates a new instance of clsCanvas */
    public clsCanvas(midMain m) {

biến imgTileset dùng để lưu trữ ảnh tileset bạn vừa down. Biến roadMap kiểu TiledLayer dùng vẽ đường, nơi xe có thể chạy. Biến blockMap kiểu TiledLayer dùng hiển thị các chướng ngại vật, cũng dùng để phát hiện va chạm.

Tiếp theo, chúng ta thêm phương thức loadRoadMap( ) khởi tạo cho biến roadMap. Thêm đoạn code sau dưới phương thức start( )

    public void loadRoadMap(){
        //initialize the roadMap
        roadMap = new TiledLayer(11, 13, imgTileset, 16, 16);

        // Create a new Random for randomizing numbers
        Random Rand = new Random();
        
        //loop through all the map cells
        for (int y = 0; y < 13; y++){
            for (int x = 0; x < 11; x++){
                // get a random tile index between 2 and 5
                int index = (Math.abs(Rand.nextInt()>>>1) % (3)) + 2;

                // set the tile index for the current cell
                roadMap.setCell(x, y, index);
            }
        }
        // mark Rand for clean up
        Rand = null;
    }

Phương thức loadRoadMap( ) tạo đối tượng TiledLayer và xác định số hàng, cột, ảnh tileset sẽ dùng, kích thước tile. Sau đó nó gán các chỉ số tile ngẫu nhiên từ 2 đến 5 đến mỗi ô của map bằng phương thức setCell( )

Chú ý rằng chỉ số tile bắt đầu từ 1 chứ không phải 0. Bạn có thể để tile đầu trong suốt, để nó không hiển thị được.

Làm tương tự với blockMap và thêm phương thức loadBlockMap( ). Thêm đoạn code sau vào dưới phương thức loadRoadMap( )
    public void loadBlockMap(){
        // define the tile indexes to be used for each map cell
        byte[][] blockData = {
            {10, 8 , 7 , 6 , 10, 9 , 8 , 7 , 6 , 10, 9 },  
            {6 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 8 },  
            {7 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 7 },  
            {8 , 0 , 0 , 10, 6 , 0 , 0 , 7 , 0 , 0 , 6 },  
            {9 , 0 , 0 , 0 , 0 , 0 , 0 , 8 , 0 , 0 , 10},  
            {10, 0 , 0 , 0 , 0 , 0 , 0 , 9 , 0 , 0 , 9 },  
            {6 , 0 , 0 , 8 , 0 , 0 , 0 , 0 , 0 , 0 , 8 },  
            {7 , 0 , 0 , 7 , 0 , 0 , 0 , 0 , 0 , 0 , 7 },  
            {8 , 0 , 0 , 6 , 0 , 0 , 0 , 10, 0 , 0 , 6 },  
            {9 , 0 , 0 , 10, 0 , 0 , 7 , 6 , 0 , 0 , 10},  
            {10, 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 9 },  
            {6 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 8 },  
            {7 , 8 , 9 , 10, 6 , 7 , 8 , 9 , 10, 6 , 7 }
        };
        
        //initialize blockMap
        blockMap = new TiledLayer(11, 13, imgTileset, 16, 16);
        
        //loop through all the map cells
        for (int y = 0; y < 13; y++){
            for (int x = 0; x < 11; x++){
                // set the tile index for the current cell
                // take note of the reversed indexes for blockData
                blockMap.setCell(x, y, blockData[y][x]);
            }
        }
        
        blockData = null;
    }

Phương thức này cũng như loadRoadMap( ) ngoại trừ việc chỉ số mỗi ô dựa vào mảng đã cho trước.

Bây giờ chúng ta chỉnh sửa phương thức load( ) để load ảnh tileset và gọi phương thức chúng ta đã thêm để khởi tạo đối tượng TiledLayer. Ta thay đổi cả nơi bắt đầu hiển thị xe để tránh việc nó kẹt vào chướng ngại vật.
    public void load(){
        try{
            // load the images here
            imgVan = Image.createImage("/images/van.png");
            
            // load the tileset      
            imgTileset = Image.createImage("/images/tileset1.png");
        }catch(Exception ex){
            // exit the app if it fails to load the image
            isRunning = false;
            return;
        }
        
        // initialize the Sprite object
        Van = new Sprite(imgVan, 18, 18);
        
        // show the frame 1 - the second frame
        Van.setFrame(1);
        
        // move to 16, 16 (X, Y)
        Van.setPosition(16, 16);
        
        //initialize the TiledLayers
        loadRoadMap();
        loadBlockMap();        
    }

Ta chỉnh sửa cả phương thức unload( ) như sau
    public void unload(){
        // make sure the object gets destroyed
        blockMap = null;
        roadMap = null;
        Van = null;
        imgTileset = null;
        imgVan = null;
    }

Giờ chúng ta vẽ map lên màn hình. Thêm mấy dòng sau vào phương thức run( ) trước khi xe được vẽ:
           //draw the road
           roadMap.paint(g);
           
           //draw the blocks
           blockMap.paint(g);           
           // draw the sprite
           Van.paint(g);
           
           flushGraphics();

Kiểm tra va chạm

Nếu bây giờ bạn chạy project thì sẽ thấy xe vẫn đi được lên trên các chướng ngại vật. Rất may là hai class TiledLayer và Sprite có phương thức kiểm tra va chạm.

Đầu tiên ta thay đổi tốc độ chiếc xe. Khai báo biến toàn cục vanSpeed như sau
private Sprite Van;

private int vanSpeed = 2;
private Image imgTileset;

Sau đó, thay đổi phương thức run( ) sử dụng biến vanSpeed
           if ((iKey & GameCanvas.UP_PRESSED) != 0){
               // show the van facing up
               Van.setFrame(0);

               // move the van upwards
               cy -= vanSpeed;
           } else if ((iKey & GameCanvas.DOWN_PRESSED) != 0){
               // show the van facing down
               Van.setFrame(1);

               // move the van downwards
               cy += vanSpeed;
           } else if ((iKey & GameCanvas.LEFT_PRESSED) != 0){
               // show the van facing left
               Van.setFrame(2);

               // move the van to the left
               cx -= vanSpeed;
           } else if ((iKey & GameCanvas.RIGHT_PRESSED) != 0){
               // show the van facing right
               Van.setFrame(3);

               // move the van to the right
               cx += vanSpeed;
           }

Bây giờ đến phần kiểm tra va chạm. Chúng ta lưu vị trí hiện thời của xe vào hai biến txty, để có thể vẽ lại vị trí chiếc xe về nơi trước khi nó chạm chướng ngại vật.
           // get the current position of the van
           int cx = Van.getX();
           int cy = Van.getY();
           
           // save the current position in temporary vars
           // so we can restore it when it hits a block
           int tx = cx;
           int ty = cy;           
           if ((iKey & GameCanvas.UP_PRESSED) != 0){

Đoạn code sau đây kiểm tra va chạm nếu xe chạm vào chướng ngại vật và vẽ lại xe về chỗ cũ trước khi nó đâm vào chướng ngại vật. Thêm đoạn code sau vào sau đoạn code update vị trí xe.
           // update the vans position
           Van.setPosition(cx, cy);
           
           //check if the van hits a block
           if (Van.collidesWith(blockMap, true)){
             //reset the van to the original position  
             Van.setPosition(tx, ty);
           }           
           //restore the clipping rectangle to full screen
           g.setClip(0, 0, screenW, screenH);

Phương thức Sprite.collodesWith( ) dùng để phát hiện va chạm giữa xe với các đối tượng blockMap. Tham số thứ 2, giá trị "true" chỉ ra rằng nó phải kiểm tra va chạm ở "mức độ pixel". Tức là chỉ kiểm tra va chạm nếu phần pixel có màu của xe tải chạm vào phần pixel có màu của chướng ngại vật. Bạn cũng có thể dùng phương thức collidesWith( ) để kiểm tra va chạm giữa các đối tượng Sprite.

Nhận tiện, vì map chiếm hết cả màn hình, ta không cần đoạn code fill màn hình màu đen nữa. Hãy xóa hoặc chú thích nó đi.
           //restore the clipping rectangle to full screen
           g.setClip(0, 0, screenW, screenH);
           
           /* comment out or remove this code
            
           set drawing color to black
           g.setColor(0x000000);
            
           fill the screen with blackness
           g.fillRect(0, 0, screenW, screenH);

            */           
           //draw the road
           roadMap.paint(g);

Khi chạy project  bạn sẽ thấy như sau:
TiledLayer Demo

Và sau đây là code lớp clsCanvas hoàn chỉnh cho các bạn đối chiếu và tham khảo:
package MyGame;

import java.util.Random;
import javax.microedition.lcdui.Graphics;
import javax.microedition.lcdui.Image;
import javax.microedition.lcdui.game.GameCanvas;
import javax.microedition.lcdui.game.Sprite;
import javax.microedition.lcdui.game.TiledLayer;

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 imgVan;
private Sprite Van;

private int vanSpeed = 2;

private Image imgTileset;
private TiledLayer roadMap;
private TiledLayer blockMap;
    public clsCanvas(midMain m) {
        super(true);
        fParent = m;
        setFullScreenMode(true);
    }
    
    public void start(){
        Thread runner = new Thread(this);
        runner.start();
    }
    
    public void loadRoadMap(){
        //initialize the roadMap
        roadMap = new TiledLayer(11, 13, imgTileset, 16, 16);
        
        // Create a new Random for randomizing numbers
        Random Rand = new Random();
        
        //loop through all the map cells
        for (int y = 0; y < 13; y++){
            for (int x = 0; x < 11; x++){
                // get a random tile index between 2 and 5
                int index = (Math.abs(Rand.nextInt()>>>1) % (3)) + 2;
                
                // set the tile index for the current cell
                roadMap.setCell(x, y, index);
            }
        }
        
        Rand = null;
    }
    
    public void loadBlockMap(){
        // define the tile indexes to be used for each map cell
        byte[][] blockData = {
            {10, 8 , 7 , 6 , 10, 9 , 8 , 7 , 6 , 10, 9 },  
            {6 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 8 },  
            {7 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 7 },  
            {8 , 0 , 0 , 10, 6 , 0 , 0 , 7 , 0 , 0 , 6 },  
            {9 , 0 , 0 , 0 , 0 , 0 , 0 , 8 , 0 , 0 , 10},  
            {10, 0 , 0 , 0 , 0 , 0 , 0 , 9 , 0 , 0 , 9 },  
            {6 , 0 , 0 , 8 , 0 , 0 , 0 , 0 , 0 , 0 , 8 },  
            {7 , 0 , 0 , 7 , 0 , 0 , 0 , 0 , 0 , 0 , 7 },  
            {8 , 0 , 0 , 6 , 0 , 0 , 0 , 10, 0 , 0 , 6 },  
            {9 , 0 , 0 , 10, 0 , 0 , 7 , 6 , 0 , 0 , 10},  
            {10, 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 9 },  
            {6 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 8 },  
            {7 , 8 , 9 , 10, 6 , 7 , 8 , 9 , 10, 6 , 7 }
        };
        
        //initialize blockMap
        blockMap = new TiledLayer(11, 13, imgTileset, 16, 16);
        
        //loop through all the map cells
        for (int y = 0; y < 13; y++){
            for (int x = 0; x < 11; x++){
                // set the tile index for the current cell
                // take note of the reversed indexes for blockData
                blockMap.setCell(x, y, blockData[y][x]);
            }
        }
        
        blockData = null;
    }    
    public void load(){
        try{
            // load the images here
            imgVan = Image.createImage("/images/van.png");

            imgTileset = Image.createImage("/images/tileset1.png");
        }catch(Exception ex){
            // exit the app if it fails to load the image
            isRunning = false;
            return;
        }
        
        // initialize the Sprite object
        Van = new Sprite(imgVan, 18, 18);
        
        // show the frame 1 - the second frame
        Van.setFrame(1);
        
        // move to 16, 16 (X, Y)
        Van.setPosition(16, 16);
        
        loadRoadMap();
        loadBlockMap();        
    }
    
    public void unload(){
        // make sure the object gets destroyed
        blockMap = null;
        roadMap = null;
        Van = null;
        imgTileset = null;
        imgVan = 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;    
           }
           
           // get the current position of the van
           int cx = Van.getX();
           int cy = Van.getY();
           
           // save the current position in temporary vars
           // so we can restore it when we hit a block
           int tx = cx;
           int ty = cy;           
           if ((iKey & GameCanvas.UP_PRESSED) != 0){
               // show the van facing up
               Van.setFrame(0);
               
               // move the van upwards
               cy -= vanSpeed;
           } else if ((iKey & GameCanvas.DOWN_PRESSED) != 0){
               // show the van facing down
               Van.setFrame(1);
               
               // move the van downwards
               cy += vanSpeed;
           } else if ((iKey & GameCanvas.LEFT_PRESSED) != 0){
               // show the van facing left
               Van.setFrame(2);
               
               // move the van to the left
               cx -= vanSpeed;
           } else if ((iKey & GameCanvas.RIGHT_PRESSED) != 0){
               // show the van facing right
               Van.setFrame(3);
               
               // move the van to the right
               cx += vanSpeed;
           }
           
           // update the vans position
           Van.setPosition(cx, cy);
           
           //check if the van hits a block
           if (Van.collidesWith(blockMap, true)){
             //reset the van to the original position  
             Van.setPosition(tx, ty);
           }           
           //restore the clipping rectangle to full screen
           g.setClip(0, 0, screenW, screenH);
           
           /* comment out or remove this code
            
           set drawing color to black
           g.setColor(0x000000);
            
           fill the screen with blackness
           g.fillRect(0, 0, screenW, screenH);

            */

           //draw the road
           roadMap.paint(g);
           
           //draw the blocks
           blockMap.paint(g);           
           // draw the sprite
           Van.paint(g);
           
           flushGraphics();
           
           try{
               Thread.sleep(30);
           } catch (Exception ex){
               
           }
       }
       g = null;
       unload();
       fParent.destroyApp(false);
       fParent = null;
    }
}

Về Tilemap, các bạn có thể tìm trên internet với từ khóa "tile map" để tham khảo. Chúc các bạn thành công !!

Thứ Bảy, 1 tháng 9, 2012

Làm việc với lớp TiledLayer

Có chuyện gì xảy ra với blog của Devlin không biết, sáng nay tôi vào không được. Hi vọng blog không sao chứ nếu có sao thì tiếc lắm... Nếu còn vào được tôi sẽ copy hết sang rồi dịch sau. Đúng là dại quá. Sau đây là bài học về TiledLayer, tôi chôm được trên mạng.

P/S: À mà thôi, đã vào lại được Devlinsblog, tôi tranh thủ copy bài luôn. Bài này cứ để Tiếng Anh, khi nào xong của Devlin tôi sẽ dịch sau.

A common technique used in video games is to have a large scrolling background formed by a grid of smaller, reusable images. In MIDP 1.0, such a feature would need to be implemented from scratch or with a third-party library. However, MIDP 2.0 provides the convenient TiledLayer class to address this specific need. In this article we will look at a simple example of how to use the TiledLayer class in a Java ME application.
TiledLayer is part of the javax.microedition.lcdui.game package that was introduced with MIDP 2.0. The constructor for the TiledLayer class accepts five parameters: the number of columns and rows in the grid, an Image object containing the tiles, and the width and height in pixels of each tile.
TiledLayer(int columns, int rows, Image image, int tileWidth, int tileHeight)
The image can have multiple rows of tiles. The tileWidth and tileHeight properties are used to break up the image into individual tiles. The tiles are numbered starting with 1 and incrementing in row-major order. Index 0 is reserved for transparency; TiledLayer won’t draw anything for cells with index 0.



For our example application, we’ll start by creating a class that extends GameCanvas. Extending GameCanvas will allow us to take advantage of the off-screen buffer that this class provides.
import javax.microedition.lcdui.*;
import javax.microedition.lcdui.game.*;
public class DemoCanvas extends GameCanvas {
Next, we’ll add an instance variable for our TiledLayer object, along with the customary setter and getter methods.
public TiledLayer getTiledLayer() {
    return tiledLayer;
}
public void setTiledLayer(TiledLayer tiledLayer) {
    this.tiledLayer = tiledLayer;
}
// Instance variables
private TiledLayer tiledLayer;  // The TiledLayer object
We will also add some constants to store the size of our tiles in pixels, and the background color. Remember that the 0 index tiles are transparent, so we will be responsible for clearing the background ourselves.
// Constants
public static final int TILE_WIDTH = 16;
public static final int TILE_HEIGHT = 16;
public static final int BACKGROUND_COLOR = 0x00000000;
We will need to provide some data for the grid. For the purposes of this example, we can hard code some data into a two-dimensional array. In a real-life application, you would probably read this data from a resource, generated from some sort of map editor software.
public byte[][] getGridData() {      // Create and populate a two-dimensional
    // array with sample grid data
    byte gridData[][] = {
        {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
        {0,0,0,1,2,2,2,3,0,0,0,0,10,10,0,0},
        {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
        {0,0,0,10,10,0,0,0,1,2,2,3,0,0,0,0},
        {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
        {0,0,1,2,2,3,0,0,0,0,0,4,5,6,0,0},
        {0,0,0,0,0,0,0,0,0,0,0,0,12,0,0,0},
        {7,8,8,8,8,8,8,8,8,8,8,8,8,8,8,9}
    };      
    return gridData;
}
Now, we will add a method to create and initialize our TiledLayer object. To initialize the cells, we will simply iterate through the byte array we created above and call the TiledLayer.setCell() for each value.
public void initTiledLayer() {

    // Get the grid data
    byte[][] gridData = getGridData();

    // Derive number of columns and rows
    // from grid data array sizes
    int rows = gridData.length;
    int columns = gridData[0].length;

    // Get the tile Image object
    Image tileImage;
    try {
        tileImage =
        Image.createImage("/tiledlayer-tiles.png");
    } catch(java.io.IOException e) {
        System.err.println("Unable to create image");
        e.printStackTrace();
        return;
    }

    // Create a TiledLayer object
    TiledLayer tiledLayer = new TiledLayer(
    columns, rows, tileImage,
    TILE_WIDTH, TILE_HEIGHT);

    // Transfer grid data to TiledLayer object
    for(int y = 0;  y < rows; y++) {
        for(int x = 0; x < columns; x++) {
            int tileIndex = gridData[y][x];
            if(tileIndex > 0)
                tiledLayer.setCell(x, y, tileIndex);
        }
    }

    // Set the TiledLayer instance variable
    setTiledLayer(tiledLayer);

}
Next, we need a method to draw the background. In addition to rendering the TiledLayer object, it will also need to clear the background. Note that since GameCanvas provides an off-screen buffer, there is no need to override the paint() method; GameCanvas already provides an adequate implementation.
public void drawBackground() {
    
    // Get off-screen buffer
    Graphics g = getGraphics();

    // Clear the background
    g.setColor(BACKGROUND_COLOR);
    g.fillRect(0, 0, getWidth(), getHeight());

    // Paint the tiled layer
    TiledLayer tiledLayer = getTiledLayer();
    if(tiledLayer != null) {
        tiledLayer.paint(g);
    }

}
Now we’ll add a custom constructor to call our initialization method, and to perform the initial drawing of the off-screen buffer. Since we’ll be overriding the keyPressed() method in the next step, we will pass false for the suppressKeyEvents parameter of the GameCanvas constructor.
protected DemoCanvas() {

    // We will be overriding keyPressed,
    // so do not suppress key events
    super(false);

    // Initialize the TiledLayer
    initTiledLayer();

    // Draw to the off-screen buffer
    drawBackground();

}
Lastly, we’ll implement an extremely simple keyPressed() method that will scroll the background when the direction keys are pressed using the convenient TiledLayer.move() method. It will also force a repaint of the off-screen buffer and flush it to the display.
protected void keyPressed(int keyCode) {

    // Handle scrolling in response to key presses
    TiledLayer tiledLayer = getTiledLayer();
    if(tiledLayer != null) {

        // Get game action from key code
        int gameAction = getGameAction(keyCode);

        switch(gameAction) {
            case UP:
                tiledLayer.move(0, -16);
                break;
            case DOWN:
                tiledLayer.move(0, 16);
                break;
            case LEFT:
                tiledLayer.move(-16, 0);
                break;
            case RIGHT:
                tiledLayer.move(16, 0);
                break;
        }

        // Repaint the off-screen buffer
        paintBackground(getGraphics());

        // Flush the off-screen buffer to the display
        flushGraphics();

    }

}

TiledLayer Example

Sử dụng lớp Sprite và di chuyển Sprite trong MIDP 2.0

Bài học ngày hôm nay sẽ giúp các bạn sử dụng được class Sprite và di chuyển đối tượng Sprite quanh màn hình.

Chúng ta sẽ sử dụng ví dụ có sẵn ở đây. Bạn hãy download nó về và sử dụng.

Tôi đã chuẩn bị một ảnh  cho chúng ta trong bài này. Đó là ảnh một chiếc xe tải trắng có kích thước 18x18 pixel với 4 hướng lên, xuống, trái, phải.

It's an 18x18 pixel white van!
White Van Image
Frame Size:18x18 pixels

Hãy bật Netbeans, mở project ví dụ ra và chúng ta bắt đầu.

Thêm vào hai biến, một cho ảnh và một cho sprite dưới hàm constructor của clsCanvas



private Image imgVan;
private Sprite Van;
    /** Creates a new instance of clsCanvas */
    public clsCanvas(midMain m) {

Giờ chúng ta load ảnh và khởi tạo đối tượng Sprite, hãy chỉnh sửa phương thức load( ) như sau


    public void load(){
        try{
            // load the images here
            imgVan = Image.createImage("/images/van.png");
        }catch(Exception ex){
            // exit the app if it fails to load the image
            isRunning = false;
            return;
        }
        
        // initialize the Sprite object
        Van = new Sprite(imgVan, 18, 18);
        
        // show the frame 1 - the second frame
        Van.setFrame(1);
        
        // move to 50, 50 (X, Y)
        Van.setPosition(50, 50);
    }

Constructor của Sprite( ) có 3 tham số:
  1. Image image - ảnh có chứa các frame hình
  2. int frameWidth - chiều rộng mỗi frame
  3. int frameHeight - chiều cao mỗi frame
Bạn phải chắc chắn rằng chiều rộng và chiều cao bạn khai báo đúng bằng với chiều rộng và chiều cao của mỗi frame trong ảnh. Trong ví dụ này, mỗi frame có kích thước 18 pixel ngang và 18 pixel dọc.

Sau khi khởi tạo sprite, chúng ta dùng phương thức setFrame( ) của lớp Sprite để đặt frame hiện tại là 1. Frame tính từ bên trái qua và chỉ số bắt đầu từ 0. Chúng ta cũng khởi tạo vị trí cho sprite với vị trí 50, 50 (X, Y) trên màn hình.

Sau đó chúng ta viết phương thức unload( ) để dùng khi không còn sử dụng ảnh và sprite nữa:



    public void unload(){
        // make sure the object gets destroyed

        Van = null;
        imgVan = null;
    }

Và phương thức vẽ được gọi trước flushGraphics( ):

           // draw the sprite
           Van.paint(g);           
           flushGraphics();

Để test thành quả, chúng ta cho chạy thử:

Van Sprite on Emulator

Tiếp theo là làm cho chiếc xe chạy được 4 hướng: Lên, xuống, trái, phải. Đầu tiên chúng ta lấy tọa độ của sprite và lưu vào 2 biến. Thêm hai dòng sau vào trong phương thức run( ):


           // get the current position of the van
           int cx = Van.getX();
           int cy = Van.getY();           
           //restore the clipping rectangle to full screen
           g.setClip(0, 0, screenW, screenH);

Chúng ta thay đổi giá trị đó tùy theo phím được nhấn. Thêm những dòng code sau vào dưới lời khai báo hai biến vừa viết:

           // get the current position of the van
           int cx = Van.getX();
           int cy = Van.getY();
           
           if ((iKey & GameCanvas.UP_PRESSED) != 0){
               // show the van facing up
               Van.setFrame(0);
               // move the van upwards
               cy--;
           } else if ((iKey & GameCanvas.DOWN_PRESSED) != 0){
               // show the van facing down
               Van.setFrame(1);
               // move the van downwards
               cy++;
           } else if ((iKey & GameCanvas.LEFT_PRESSED) != 0){
               // show the van facing left
               Van.setFrame(2);
               // move the van to the left
               cx--;
           } else if ((iKey & GameCanvas.RIGHT_PRESSED) != 0){
               // show the van facing right
               Van.setFrame(3);
               // move the van to the right
               cx++;
           }
           
           // update the vans position
           Van.setPosition(cx, cy);
           //restore the clipping rectangle to full screen
           g.setClip(0, 0, screenW, screenH);

Đoạn code trên kiểm tra phím nào vừa được nhấn và thay đổi frame của chiếc xe cho phù hợp với hướng đi. Sau đó nó tính toán vị trí cho chiếc xe và vẽ lại lên vị trí mới.
  • Lên - giảm giá trị của Y
  • Xuống - tăng giá trị của Y
  • Trái - giảm giá trị của X
  • Phải - tăng giá trị của X
Bây giờ bạn chạy thử project sẽ thấy chiếc xe có thể đi 4 hướng them phím nhấn.

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;
import javax.microedition.lcdui.game.Sprite;

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 imgVan;
private Sprite Van;
    /** 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
            imgVan = Image.createImage("/images/van.png");
        }catch(Exception ex){
            // exit the app if it fails to load the image
            isRunning = false;
            return;
        }
        
        // initialize the Sprite object
        Van = new Sprite(imgVan, 18, 18);
        
        // show the frame 1 - the second frame
        Van.setFrame(1);
        
        // move to 50, 50 (X, Y)
        Van.setPosition(50, 50);
    }
    
    public void unload(){
        // make sure the object gets destroyed
        Van = null;
        imgVan = 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;    
           }
           
           // get the current position of the van
           int cx = Van.getX();
           int cy = Van.getY();
           
           if ((iKey & GameCanvas.UP_PRESSED) != 0){
               // show the van facing up
               Van.setFrame(0);
               // move the van upwards
               cy--;
           } else if ((iKey & GameCanvas.DOWN_PRESSED) != 0){
               // show the van facing down
               Van.setFrame(1);
               // move the van downwards
               cy++;
           } else if ((iKey & GameCanvas.LEFT_PRESSED) != 0){
               // show the van facing left
               Van.setFrame(2);
               // move the van to the left
               cx--;
           } else if ((iKey & GameCanvas.RIGHT_PRESSED) != 0){
               // show the van facing right
               Van.setFrame(3);
               // move the van to the right
               cx++;
           }
           
           // update the vans position
           Van.setPosition(cx, cy);           
           //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);
           
           // draw the sprite
           Van.paint(g);           
           flushGraphics();
           
           try{
               Thread.sleep(30);
           } catch (Exception ex){
               
           }
       }
       g = null;
       unload();
       fParent.destroyApp(false);
       fParent = null;
    }
}

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

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 !!