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

3 nhận xét:

  1. chào bạn! rất cảm ơn bạn có những bài dịch bổ ích này, nhưng hiện tại hầu hết hình ảnh và các link download trong các bài dịch của bạn đều bị hỏng rồi, bạn có thể cập nhật lại được không. cảm ơn bạn

    Trả lờiXóa
  2. Nhận xét này đã bị tác giả xóa.

    Trả lờiXóa
  3. Mình cũng muốn lắm nhưng khi dịch thì link của Devlin đã die nên bó tay. Tuy nhiên mình vẫn làm được, có lẽ bạn cũng thế. Đây là yahoo của mình, có gì bạn cứ pm
    scorpion06_vs_knight(@yahoo.com)

    Trả lờiXóa