Thứ Tư, 15 tháng 8, 2012

Input Handling : Keypress with Repeat Rate

Có rất nhiều cách để xử lí với user input trong game. Bài hướng dẫn này sẽ chỉ cho bạn một cách để dùng trong game của mình. Như khi bạn thông báo người chơi có quyết định thoát game hay không, khi ở trong menu chính, hay là khi bạn muốn user chọn vài lựa chọn trên màn hình. Chúng ta sẽ chỉ sử dụng hàm getKeyStates( ) để giới hạn code.

Vấn đề đặt ra: Kẹt phím
Bấm phím liên tọi trong game cũng tốt khi game vẫn có thể hồi đáp với tần suất nhấn phím của người chơi. Nhưng không phải là trong màn hình lựa chọn. Tại sao lại thế? Hãy giả sử game bạn có frame rate là 15 frame 1 giây. Đó cũng là tốc độ mà game hành động mỗi khi người chơi bấm phím.

Hãy tưởng tượng trong game của bạn, đầu tiên là New Game và thứ 2, ngay dưới nó là Instruction, phần đang được chọn sáng lên như hình dưới. Người chơi đang muốn xem phần Instruction và bấm phím xuống, và phần được chọn chuyển xuống với mức 15 lần 1 giây, tức chuyển cmn 15 phát thay vì 1 phát. Và họ sẽ không thể chọn được phần mình muốn.




Tiếp theo, tưởng tượng rằng bạn viết một đoạn chữ trong game, mỗi lần bạn bấm phím viết chữ "a", bạn sẽ thấy 15 chữ "a" xuất hiện trên màn hình. Như kiểu lái xe mà không có phanh vậy. (Lần này Devlin không vẽ minh họa cho chúng ta ^^)

Làm phím chuyển đổi
Trong phần này chúng ta sẽ thêm một số tương tác vào quả địa cầu quay trong bài trước. Bạn hãy bật IDE, mở project và lấy clsCanvas ra xem.

Khai báo vài biến và hằng mới nào. Hãy viết thêm mấy dòng code sau vào sau dòng khai báo lớp clsCanvas.


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
};

Giá trị gán cho hằng keyDelay cho ta biết tốc độ mà trạng thái của key chuyển từ đang được bấm sang không được bấm khi chúng ta nhấn giữ một phím. Nói cách khác đó là tần suất lặp lại. Số càng lớn thì tần suất lặp càng chậm và ngược lại.

5 hằng tiếp theo khai báo cho 5 phím chuẩn của game: Up, Down, Left, Right Fire hay OK. Chúng ta đặt như thế để code nhìn đỡ rắc rối hơn, tha vì số ta dùng chữ cho dễ hiểu dễ nhận. Cũng có người nói với tôi ( Devlins ) 12 - 14 năm về trước rằng nên dùng các hằng hay gí trị cố định để sử dụng lại trong code của mình.

Mảng boolean isDown[] sẽ được dùng để lưu trữ trạng thái các phím chúng ta vừa định nghĩa và mảng long keyTick[] lưu trữ thời gian cuối cùng mà phím chuyển từ trạng thái được bấm sang được nhả ra.

Mảng cuối cùng keyValue[] mang giá trị integer lưu trữ mã phím thực sự của mỗi phím. Chúng ta khai báo nó để lặp các phím trong code phát hiện phím (xem bên dưới).

Chúng ta sẽ thêm phương thức checkKeys( ) cho lớp clsCanvas, thêm đoạn code sau ngay trên phương thức run( )



   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() {

Phương thức checkKey( ) lấy giá trị trả về của biến iKey sử dụng hàm getKeyStates( ) và lấy giá trị thời gian hiện tại tính theo milli giây của biến currTick. Nó sẽ lặp qua tất cả các phím trong mảng keyValue[ ] xem phím nào vừa được nhấn. Sau đó nó sẽ chuyển giá trị của các yếu tố thích hợp trong mảng isDown[ ] tùy thuộc vào thời gian phím được nhấn. Nó cũng sẽ chắc chắn rằng mảng isDown[ ] được cập nhật với các phím không được nhấn. (Đoạn này dịch hơi lủng củng, ai dịch được làm ơn comment dưới bài viết nhé. Sau đây là nguyên văn tiếng Anh)
The checkKeys() method takes the value returned by the getKeyStates() method in theiKey parameter and the current time in milliseconds in the currTick parameter. It then loops through all the keys in the keyValue[] array too see if a certain key is being pressed. It then toggles the value of the appropriate element in the isDown[] array depending on how long it's been since the key was in the pressed state. It also makes sure that the isDown[] array is updated as to which keys are not being pressed.

Hãy chỉnh sửa phương thức run() của chúng ta để khi người chơi nhấn phím sang trái thì tốc độ quay tăng lên, còn bấm sang phải thì tốc độ quay giảm đi. Đầu tiên, hãy chú thích hoặc xóa để vô hiệu hóa đoạn code mà khi chúng ta nhấn phím OK thì chương trình thoát ra.
       /*
       if ((iKey & GameCanvas.FIRE_PRESSED) != 0){
           isRunning = false;
       }
       */

Sau đó thêm đoạn code sau đây vào bên dưới dòng code đặt giá trị cho biến lCurrTick để chúng ta có thể sử dụng giá trị đó cho phương thức checkKey( )
       lCurrTick = System.currentTimeMillis();
       
       checkKeys(iKey, lCurrTick);
   
          if (isDown[leftKey]){
              if (lDelay > 0){
                  lDelay-=10;
              }
          } else if (isDown[rightKey]){
              if (lDelay < 1000){
                  lDelay+=10;
              }
          } else if (isDown[fireKey]){
              isRunning = false;   
          }

Thêm đoạn code sau vào ngay trước lời gọi setClip( ) để chúng ta có thể hiển thị thông tin phản hồi khi phím được nhấn.
       g.drawString("lDelay : " + Long.toString(lDelay), 2, 108, Graphics.TOP | Graphics.LEFT);
       if (isDown[upKey]) {
         g.drawString("up key pressed", 2, 128, Graphics.TOP | Graphics.LEFT);
       } else if (isDown[leftKey]) {
         g.drawString("left key pressed", 2, 128, Graphics.TOP | Graphics.LEFT);
       } else if (isDown[downKey]) {
         g.drawString("down key pressed", 2, 128, Graphics.TOP | Graphics.LEFT);
       } else if (isDown[rightKey]) {
         g.drawString("right key pressed", 2, 128, Graphics.TOP | Graphics.LEFT);
       
       }
       //clip the drawing area to a single frame
       g.setClip(50, 50, 16, 16);

Đoạn code cuối cùng chúng ta hiển thị giá trị của lDisplay để thấy nó thay đổi như thế nào. Nó cũng hiển thị khi có phím di chuyển nào được nhấn, trừ nút OK vì nhấn OK thì game out rồi còn gì. Sau đây là code lớp clsCanvas 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 imgEarth;

   public clsCanvas(midMain m) {
       super(true);
       fParent = m;
       setFullScreenMode(true);
   }
  
   public void start(){
       Thread runner = new Thread(this);
       runner.start();
   }
  
   public void load(){
       try{
           // try to load the image file
           imgEarth = Image.createImage("/images/earthstrip.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
       imgEarth = 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 imgX = 50; // x coordinate of the image
      int frameIndex = 0; // current frame to be drawn
      long lDelay = 250; //time to pause between frames in milliseconds
      long lStart = 0; //time we last changed frames in milliseconds
      long lCurrTick = 0; // current system time in milliseconds;
     
      load();
      g = getGraphics();
      while(isRunning){
         
          iKey = getKeyStates();
           /*
          if ((iKey & GameCanvas.FIRE_PRESSED) != 0){
              isRunning = false;
          }
          */
          lCurrTick = System.currentTimeMillis();
          
          checkKeys(iKey, lCurrTick);
         
          if (isDown[leftKey]){
              if (lDelay > 0){
                  lDelay-=10;
              }
          } else if (isDown[rightKey]){
              if (lDelay < 1000){
                  lDelay+=10;
              }
          } else if (isDown[fireKey]){
              isRunning = false;   
          }          
          if ((lCurrTick-lStart) >= lDelay){
              lStart = lCurrTick; // save the current time
              if (frameIndex < 8) {
                  frameIndex++; // skip to the next frame
              } else {
                  frameIndex = 0; // go back to first frame
              }
              imgX = 50 - (frameIndex * 16); // compute x relative to clip rect
          }
         
          //restore the clipping rectangle to full screen
          g.setClip(0, 0, getWidth(), getHeight());
         
          //set drawing color to black
          g.setColor(0x000000);
          //fill the whole screen
          g.fillRect(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);

          g.drawString("Frame : " + Integer.toString(frameIndex), 2, 68, Graphics.TOP | Graphics.LEFT);
          g.drawString("X : " + Integer.toString(imgX), 2, 88, Graphics.TOP | Graphics.LEFT);
         
          g.drawString("lDelay : " + Long.toString(lDelay), 2, 108, Graphics.TOP | Graphics.LEFT);
          if (isDown[upKey]) {
            g.drawString("up key pressed", 2, 128, Graphics.TOP | Graphics.LEFT);
          } else if (isDown[leftKey]) {
            g.drawString("left key pressed", 2, 128, Graphics.TOP | Graphics.LEFT);
          } else if (isDown[downKey]) {
            g.drawString("down key pressed", 2, 128, Graphics.TOP | Graphics.LEFT);
          } else if (isDown[rightKey]) {
            g.drawString("right key pressed", 2, 128, Graphics.TOP | Graphics.LEFT);
             
          }          
          //clip the drawing area to a single frame
          g.setClip(50, 50, 16, 16);
          //draw the image          
          g.drawImage(imgEarth, imgX, 50, Graphics.TOP | Graphics.LEFT);
         
          flushGraphics();
         
          try{
              Thread.sleep(30);
          } catch (Exception ex){
             
          }
      }
      g = null;
      unload();
      fParent.destroyApp(false);
      fParent = null;
   }
}

Xong !! Giờ thì bạn hãy bật giả lập lên xem thành quả của mình...
Output on Sun Java WTK 2.5.1


Có vài thứ chúng ta có thể làm với ví dụ trên. Bạn có thể dùng các phím khác,hay chỉnh sửa thông số phương thức checkKey( ) hay có thể đặt nó làm phương thức tĩnh. Hoặc, thay đổi giá trị keyDelay theo ý thích. Cuối cùng, bạn có thể dùng các kỹ thuật trên với phương thức keyPressed( ) vào keyRealeased( )

Chúc các bạn học tốt !! À quên, có thắc mắc gì các bạn có thể hỏi, mình sẽ cố gắng giải đáp trong phạm vi kiến thức. Hoặc các bạn hỏi Devlin trong blog của anh ta: http://devlinslab.blogspot.com/

Không có nhận xét nào:

Đăng nhận xét