Hướng dẫn Phát Triển Game Đơn Giãn Trên Mobile

Phát Triển Game Đơn Giãn Trên Mobile(P1)




Giới thiệu:
Bài này nhằm mục tiêu cung cấp hướng dẫn, giới thiệu về cách thức để phát triển một trò chơi đơn giãn trong J2ME. Trước khi đọc bài này, tôi khuyên bạn nên tìm hiểu sơ lược về platform J2ME. Đây là một trong 3 platform của Java được sử dụng để phát triển các ứng dụng trên các thiết bị nhúng, mobile…Bây giờ, hãy cùng tìm hiểu phần 1.

Phần một:

Ngõ vào ứng dụng(entry point):
Trong tất cả các ứng dụng trên J2ME, để chạy ứng dụng chúng ta phải extends từ lớp ảo(abstract) MIDlet trong gói javax.microedition.midlet, tương tự như tạo một applet phải extend từ lớp java.applet.Applet. Và entry point của nó là phương thức startApp(), tương tự như phương thức main() trong J2SE.
Ví dụ: Ta có một lớp MyMidlet. 
public class MyMidlet extends MIDlet{
// invoked when the application starts and each time is resumed
protected void startApp() throws MIDletStateChangeException {}
// invoked when the MIDlet needs to be destroyed
protected void destroyApp(boolean uncondicional) throws MIDletStateChangeException {}
// invoked when the MIDlet needs to be paused. (Some phones ignore pauseApp().)
protected void pauseApp() {}
}
Nếu bạn tạo project và chạy file này thì nó chỉ hiển thị màn hình blank.
Tiếp theo, để trình bày nội dung trong ứng dụng, bạn cần sử dụng lớp Display. Lớp Display điều khiển tất cả những gì xảy trong màn hình của MIDlet, mỗi MIDlet có một đối tượng Display và truy cập nó thông qua việc sử dụng phương thức tĩnh getDisplay().
Để biểu diễn một đối tượng nào đó trên màn hình, bạn cần phải sử dụng phương thức setCurrent() của lớp Displayable.
Ví dụ: ta có một lớp Alert, lớp này kế thừa từ lớp Displayable, trong ví dụ này sử dụng Alert để hiển thị một câu thông báo đơn giãn lên màn hình.
Alert alert;
//entry point for the application
protected void startApp() throws MIDletStateChangeException {
// creates alert
alert = new Alert("Hello World");
// shows alert in the screen.
Display.getDisplay(this).setCurrent(alert);
}

Nếu chạy chương trình, bạn sẽ thấy nó hiển thị ra một thông điệp như sau:
Tiếp theo, để thêm các command cho ứng dụng, như commad exit, bạn xử lý như sau:
Command comExit;
[...]
protected void startApp() throws MIDletStateChangeException {
[...]
// create command
comExit = new Command("Exit", Command.EXIT, 1);
// add a command to exit the Midlet
alert.addCommand(comExit);
[...]
}

Đoạn code trên show cho bạn command exit lên màn hình, nhưng khi bạn click vào, nó không làm gì cả bởi bạn cần thêm CommandListener vào Alert để đăng ký một listener.

public class MyMidlet extends MIDlet implements CommandListener{ public void startApp(){ [...] // adds a listener to the alert alert.setCommandListener(this); } public void commandAction(Command cmd, Displayable display) { // check what command was selected if (cmd == comExit) { notifyDestroyed(); } } }

Với thay đổi này, chúng ta có một MIDlet hoàn chỉnh. Ở phần tiếp theo, chúng ta sẽ có một cái nhìn chi tiết hơn về các phần tử giao diện người dùng như: Alert, Display, và CommandListener.
Code


Phát Triển Game Đơn Giãn Trên Mobile(P2)




Ở phần này, ta tìm hiểu về các thành phần giao diện người dùng(UI) có sẵn trong J2ME nhằm tạo ra sự tương tác giữa người dùng với điện thoại, đây là vấn đề quan trọng bởi kích thước của màn hình điện thoại được giới hạn. J2ME cung cấp đặc tả MIDP chứa các thành phần giao diện đồ họa, hiện nay đã có rất nhiều phiên bản MIDP 3.0, tuy nhiên trong bài này tôi chỉ xét MIDP 2.0. Ta cùng tìm hiểu sơ đồ phân lớp của nó:

 

 

MIDP 2.0 cung cấp các lớp UI trong một gói javax.microedition.lcdui, trong đó lcdui là viết tắt của liquid crystal display user interface(LCD UI). Để show một phần tử UI lên màn hình, bạn phải sử dụng một lớp Displayable. Ví dụ, một lớp Displayable có thể có một title, một ticker, và các command liên kết với nó.
Lớp Display quản lý những cái gì hiển thị lên màn hình. Phương thức static getDisplay(MIDlet midlet) cho phép bạn truy cập vào Display của MIDlet. Sau đó, bạn sử dụng phương thức setCurrent(Displayable element) để lựa chọn đối tượng nào extend từ Displayable được hiển thị lên màn hình. Tại một thời điểm, chỉ có một đối tượng Displayable được hiển thị trên màn hình. Hãy xem lại ví dụ từ bài trước:
Display.getDisplay(this).setCurrent(alert);

Trong MIDP, nó phân chia các lớp thành 2 thành phần: các thành phần giao diện cấp cao(high-level interface component) và cấp thấp(low-level).
Các thành phần high-level được thực thi thông qua các lớp extends từ class Screen, còn các thành phần low-level được thực thi thông qua các lớp extends từ class Canvas. Và tất cả chúng đều extends từ class Displayable.
Bất kỳ một ứng dụng nào cũng có thể kết hợp cả các thành phần giao diện high-level và low-level để phục vụ cho mục đích của ứng dụng. Ví dụ, trong một ứng dụng game, List và Form có thể được sử dụng để chọn lựa hay cấu hình game, trong khi Canvas (hay GameCanvas) được sử dụng cho các thành phần tương tác của game như tạo nhân vật chuyển động, ảnh background.

Lớp Command:
Một MIDlet tương tác với người dùng thông qua các Command. Một Command tương đương với một menu item trong một ứng dụng thông thường, và nó chỉ có thể kết hợp với một phần tử UI Displayable. Lớp Displayable cho phép người dùng attacth một Command bằng cách sử dung phương thức addCommand(Command command). Một đối tượng Displayable có thể cón nhiều Command bên trong nó. Lớp Command được nắm giữ các thông tin về command. Thông tin này được đóng gói trong 4 thuộc tính: short label, optional long label, command type và priority. Ví dụ, sử dụng lớp Command để tạo ra đối tượng command thông qua cung cấp các giá trị trong Constructor:
// adds a command to exit the MIDlet 
comExit = new Command("Exit", Command.EXIT, 1); 

Lưu ý rằng các command không thay đổi khi chúng được tạo ra.

Nếu xác định command type, bạn có thể cho phép thiết bị chạy MIDlet ánh xạ(map) bất kỳ phím định sẵn nào trên thiết bị vào command đó. Ví dụ, một command với kiểu OK được ánh xạ vào phím OK của thiết bị. Trong MIDP 2.0, nó có sẵn các kiểu command sau: BACK, CANCEL, EXIT, HELP, ITEM, SCREEN và STOP. Kiểu SCREEN liên quan đến một command được map trong ứng dụng cho màn hiền hiện thời. Cả SCREEN và ITEM không có bất kỳ các phím được ánh xạ trên thiết bị. Để nhận được phản hồi từ người dùng, bạn cần phải listen từ các command, điều này được thực hiện thông qua thực thi giao diện CommandListener. Trong ví dụ Hello World, Interface được thực thi thông qua phương thức commandAction().
Ví dụ:
alert.addCommand(comExit);
// adds a listener to the form  
alert.setCommandListener(this);
[...]
public void commandAction(Command cmd, Displayable display) {
  if (cmd == comExit) {
    exit();
  }             
}

Như bạn thấy, phương thức commandAction() nhận 2 tham số: đó là Command được thực thi và Displayable đang được hiển thị hiện tại. 

Giao diện người dùng cấp cao(High-level User Interface):
Các API của giao diện người dùng cấp cao được thiết kế cho các ứng dụng kinh doanh của các khách hàng mà các thành phần client của nó chạy trên các thiết bị di động. Đối với các loại ứng dụng này, tính linh động(portability) qua nhiều thiết bị là rất quan trọng. Để đạt được tính linh động như vậy, các API của nó ở mức cao được trừu tượng hóa(abstraction) và cung cấp ít các điều khiển hơn các look anh feel. Điều này cho phép thiết bị sử dụng look and feel giao diên người dùng tự nhiên(native) để hiển thị thay thế cho các thành phần giao diện MIDP high-level. Điều này có nghĩa khi một môt ứng dụng được viết bằng API high-level, nó look and feel một cách tự động sử dụng look and feel của thiết bị mà ứng dụng đang chạy, còn đối với người dùng cuối, điều này cung cấp sự tương tác với người dùng một cách liền mạch, đó là ứng dụng MIDP làm việc như là các ứng dụng native trên thiết bị.
Tóm lại, khi sử dụng API high-level, bạn có thể:

  • Vẽ để được hiển thị bởi hệ thống phần mềm của thiết bị. Ứng dụng không định nghĩa giao diện trực quan như hình dáng, màu sắc… của các thành phần high-level.

  • Điều hướng, cuộn, và các tương tác nguyên thủy khác với các thành phần giao diện người dùng được thực hiện bởi thiết bị. Tuy nhiên, ứng dụng không nhận biết được các tương tác này.

  • Ứng dụng không thể truy cập vào các kỹ thuật input cụ thể, như các phím nhấn cụ thể nào đó.


Alert:
Ứng dụng Hello World sử dụng một alert. Phần tử này đại diện cho một màn hình(Screen) dùng để show dữ liệu đến người dùng và đợi một thời gian trước khi xử lý đối tượng Displayable tiếp theo. Một Alert thì có thể chứa môt chuỗi text và một image. Thông thường, Alert được sử dụng để thông báo lỗi hay các ngoại lệ khác.

TextBox:
Kế thừa từ lớp Screen, cho phép người dùng nhập và chỉnh sửa text. Lớp này có thể được cấu hình để thích nghi với các nhu cầu của bạn. Bạn có thể giới hạn maximum các ký tự hiển thị trong TextBox. Ngoài ra, bạn có thể ràng buộc các kiểu nhập cho TextBox bằng cách sử dụng các flag được định nghĩa trong lớp TextField. Có 6 ràng buộc(constrain) để giới hạn nội dung hiển thị, đó là: ANY, EMAILADRR, NUMBERIC, PHONENUMBER, URL và DECIMAL. Có 6 ràng buộc ảnh hưởng tới kiểu nhập: PASSWORD, UNEDITABLE, SENSITIVE, NON_PREDICTIVE, INITIAL_CAPS_WORD, và INITIAL_CAPS_SENSITIVE. Ví dụ, chỉ cho phép địa chỉ email được phép nhập trong TextBox, bạn thiết lập flag TextField.EMAILADRR sử dụng phương thức setConstrains(), ngoài ra để kết hợp nhiều ràng buộc cùng lúc, bạn sử dụng toán tử OR giữa các flag. Ví dụ:
setConstraints(TextField.EMAILADDR | TextField.UNEDITABLE);

List:
Một List chứa một danh sách các chọn lựa. Khi List được biểu diễn trên màn hình, người dùng có thể tương tác với nó bằng cách chon lựa các phần tử, di chuyển qua lại giữa các phần tử của List. Nó có các kiểu cấu hình sau:

  • Choice.EXCLUSIVE: chỉ có 1 phần tử được chọn lựa.

  • Choice.MULTIPLE: có thể có nhiều phần tử được chọn lựa.

  • Choice.IMPLICIT: phần tử được hightlight được chọn lựa.


Form:
Nó chứa nhiều item, bất kỳ lớp nào extends từ lớp Item để có thể được chứa trong một Form. Việc thực thi xử lý các layout, traversal, và scrolling. Nội dung của Form có thể được cuộn lại với nhau.
Các loại Item có thể được thêm vào trong Form:

StringItem: là một label và không cho phép người dùng sửa lên nó. Item này có thể chứa một tiêu đề và một text, và cả hai đều có thể null.

DateField: cho phép người dùng nhập ngày date/time một trong 3 dạng sau: DATE, TIME và DATE_TIME.

TextField: Tương tự như TextBox.

ChoiceGroup: Tương tự như List.

Gauge: sử dụng để mô phỏng process bar, tuy nhiên nó có thể sử dụng trong kiểu tương tác bởi người dùng, ví dụ nếu bạn muốn dùng nó để show một Volume Control.

ImageItem: Nắm giữ một image.

CustomItem: là một lớp ảo abstract cho phép các subclass tạo ra giao diện riêng, tương tác riêng và cơ chế thông báo riêng của nó. Nếu bạn muốn một phần tử UI khác so với các phần tử được cung cấp thì bạn có thể tạo ra subclass.

Giao diện người dùng cấp thấp(Low-level User Interface): 
Các API của low-level user interface( như lớp Canvas) được thiết kế cho các ứng dụng cần sự sắp đặt và điều khiển các phần tử graphics một cách chính xác cũng như truy cập vào các low-level input event. Ví dụ điển hình là game board, một chart object hay một graph. Sử dụng low-level user interface, một ứng dụng có thể:

  • Kiểm soát những gì được vẽ trên màn hình.

  • Điều khiển được các sự kiện primitive như các sự kiện nhấn phím(key press) và nhả phím(key release)

  • Truy cập vào cụ thể từng phím và các thiệt bị đầu vào khác.


Ngoài ra, trong MIDP 2.0 còn cung cấp javax.microediton.lcdui.game. Gói này bao gồm 5 class dùng để thiết kế cho các game, đó là : GameCanvas, LayerManger, Layer, Sprite và TiledLayer. Bạn có thể tìm hiểu phần này sau.
Ví dụ về giao diện người dùng:
Đối với mỗi màn hình game, chúng ta tạo một phương thức init[ScreenName] để khởi tạo màn hình và trả về đối tượng Displayable được tạo ra:
Đối với Main Menu, bạn sử dụng component List để biểu diễn main options. Ví dụ:
public Displayable initMainForm() { 
if (mainForm == null) {
    // creates a implicit List where the current element is
    // the selected
    mainForm = new List("Menu",   List.IMPLICIT);
    // append list options
    mainForm.append("New Game",   null);
    mainForm.append("Options",   null);
    mainForm.append("Scores",   null);
    mainForm.append("Help", null);
    mainForm.append("Exit", null);
    // adds a select Command
    comSelect = new   Command("Select", Command.ITEM, 1);
    mainForm.setSelectCommand(comSelect);
    // adds a listener to the form
    mainForm.setCommandListener(this);
  }
    return mainForm; 
}

Đối với Menu Settings, bạn chọn phần tử Form, và thêm một đối tượng ChoiceGroup để tùy chỉnh âm thanh:
public Displayable initSettingsForm() {
  // check if already created
  if (settingsForm == null) {
    settingsForm = new Form("Settings");
    settingsForm.addCommand(initBackCommand());
    settingsForm.setCommandListener(this);
    // creates a choice Group for sound options
    soundChoice = new ChoiceGroup("Sound", List.EXCLUSIVE);     
    soundChoice.append("On", null);
    soundChoice.append("Off", null);
    // appends the choice to the form
    settingsForm.append(soundChoice);     
  }
  return settingsForm;
}

Đối với Help Screen, bạn chọn phần tử Form với static message:
public Displayable initHelpForm() {
    if (helpForm == null) {
      helpForm = new Form("Help");
      helpForm
          .append("User cursors to move your pad, don't let "+
              "the ball go by you, hit all the bricks!");
      helpForm.setCommandListener(this);
      helpForm.addCommand(initBackCommand());
    }
    return helpForm;
  }

Để giới thiệu High Score, bạn sử dụng một Form với các item của nó là TextField và DateField:
public Displayable initNewHighScore(int score, int pos) {
    if (newHighScoreForm == null) {
      newHighScoreForm = new Form("New High Score");
      newHighScoreForm.setCommandListener(this);
      // create items
      highScoreName = new TextField("Name", "", 20, TextField.ANY);
      highScoreValue = new StringItem("Score", Integer.toString(score));
      highScorePosition = new StringItem("Position", Integer.toString(pos));
      // create save command
      highScoreSave = new Command("Save", Command.OK, 1);     
      // append command and itens to screen
      newHighScoreForm.addCommand(highScoreSave);
      newHighScoreForm.append(highScoreName);
      newHighScoreForm.append(highScoreValue);
      newHighScoreForm.append(highScorePosition);
    }
    // update score
    highScoreValue.setText(Integer.toString(score));
    // update pos
    highScorePosition.setText(Integer.toString(pos)+1);
    return newHighScoreForm;
  }

Màn hình game sẽ được đề cập trong bài tiếp theo. Bây giờ, hãy tạo một phương thức giả lập kết thúc game và sử dụng nó để thay thế:
public void endGame(int lifes, int score, int time) {
    Displayable nextScreen = initMainForm();
    String message;
    if (lifes == 0) {
      message = "Game Over!!";
    } else {
      message = "You Win!";
    }
    int pos = isHighScore(score);
    if (pos != -1) {
      nextScreen = initNewHighScore(score, pos);
    } 
    display(new Alert(message, message, null, AlertType.INFO), nextScreen);
  }

Bây giờ, tất cả các phương thức đã được tạo ra, bạn link chúng đến phương thức commandAction(). Rewrite code:
public void commandAction(Command cmd, Displayable display) {
  // check what screen is being displayed
  if (display == mainForm) {
    // check what command was used
    if (cmd == comSelect) {
      switch (mainForm.getSelectedIndex()) {
      case (0):
        // At the moment just go directly to the end of the game
        endGame(1, 200, 50);
        break;
      case (1):
        display(initSettingsForm());
        break;
      case (2):
        display(initScoreForm());
        break;
      case (3):
        display(initHelpForm());
        break;
      case (4):
        exit();
        break;
      }
    }
  } else if (display == highScoreForm) {
    if (cmd == comBack) {
      display(initMainForm());
    }
  } else if (display == settingsForm) {
    if (cmd == comBack) {
      soundOn = soundChoice.getSelectedIndex() == 0;
      display(initMainForm());
    }
  } else if (display == helpForm) {
    if (cmd == comBack) {
      display(initMainForm());
    }
  } else if (display == newHighScoreForm) {
      if (cmd == highScoreSave) {
        int pos = Integer.parseInt(highScorePosition.getText())-1;
        // advance all the scores
        for ( int i = scores.length-1; i > pos ; i--){
          scores[i].name  = scores[i-1].name;
          scores[i].value = scores[i-1].value;
          scores[i].when  = scores[i-1].when;
        }
        // insert new score
        scores[pos].name = highScoreName.getString();
        scores[pos].value = Integer.parseInt(highScoreValue.getText());
        scores[pos].when = new Date();       
        display(initScoreForm());
      }
    }
}

Tất cả các logic menu cho các MIDlet được xác định bên trong phương thức commandAction() này. Quyết định làm gì tiếp theo phụ thuộc vào màn hình hiển thị và command được chọn lựa. Từ menu chính, tôi chỉ đơn giãn chuyển hướng người dùng đến mỗi màn hình cụ thể. Các màn hình hiện tại chỉ có một back command, chỉ duy nhất một form NewHighScores có command save dùng để lưu thông tin về điểm số(score).
Bạn lưu ý cách thức sử dụng phương thức display(), vì đây là cách đơn giãn để kích hoạt đối tượng Displayable.
public void display(Displayable display) {
  // shows display  in the screen.
  Display.getDisplay(this).setCurrent(display);
}

Bài tiếp theo sẽ mô tả làm thế nào để cài đặt một Game Screen.
Code


Phát Triển Game Đơn Giãn Trên Mobile(P3)




Ở bài trước, chúng ta đã hoàn thành một giao diện menu cho game. Tuy nhiên, màn hình Game Screen vẫn chưa được tạo ra. Mục đích của bài này là sinh ra một Game Screen có giao diện như hình dưới:

 

 

Các thành phần giao diện cấp cao không thể sử dụng cho game screen bởi vì chúng ta phải điều khiển tất cả các phần tử trong game khi chúng được vẽ và làm thế nào để game tương tác trở lại với keypad. Để làm được điều này, chúng ta phải sử dụng các class giao diện low-level. Các class thuộc nhóm low-level cho phép bạn kiểm soát chi tiết các phần tử và sự kiện trên màn hình game. Sử dụng các class này, bạn có thể xác định rõ vị trí, màu sắc và kích thước. Cũng chính vì vậy, bạn phải thiết kế màn hình game screen cho mỗi loại màn hình ứng với mỗi thiết bị.

Diagram sau cho biết các lớp chính trong giao diện low-level :

 

 

Với entry point là lớp Canvas, nó cho phép bạn truy cập các sự kiện của hệ thống:

  • keyPressed(), keyReleased(), keyRepeated() thông báo tới Canvas khi keypad được sử dụng.

  • pointerPressed(), pointerDragged(), pointerReleased() thông báo tới Canvas khi pointer được sử dụng, đây là các phương thức xử lý trong các màn hình cảm ứng.

  • paint() thông báo đến Canvas khi nó cần vẽ lên màn hình, phương thức này dùng để truy cập vào đối tượng Graphics.

  • getWidth(), getHeight() truy cập vào kích thước hiện tại của màn hình được vẽ.


Lớp Graphics cung cấp các phương thức để vẽ trực tiếp lên màn hình:

  • drawLine() vẽ đường thẳng.

  • drawRect(), fillRect() vẽ hay đổ màu một rectangle lên màn hình.

  • drawArc() vẽ một cung(arc) lên màn hình, có thể dùng nó để vẽ đường tròn.

  • drawChar() vẽ một ký tự lên màn hình.

  • drawString() vẽ một chuỗi lên màn hình.

  • drawImage() vẽ một bitmap image lên màn hình

  • setFont() thiết lập Font chữ.

  • setColor() thiết lập màu.


Ngoài ra trong MIDP 2.0 còn nhiều phương thức khác, bạn tự tìm hiểu thêm.

Để tạo Game Screen, bạn phải tạo một lớp extends từ lớp Canvas và cài đặt phương thức paint().

import javax.microedition.lcdui.Canvas;
import javax.microedition.lcdui.Graphics;

public class MyCanvas extends Canvas{

int width;
int height;

public MyCanvas() {

}

protected void paint(Graphics g) {
// stores width and height
width = getWidth();
height = getHeight();
// set background color
g.setColor(0,0,0);
// clear screen
g.fillRect(0, 0, width, height);
// draw a red circle that represents a ball
g.setColor(255,0,0);
g.drawArc(100, 100, 5, 5, 0, 360);
// draws a blue rectangle for the pad
g.setColor(0,0,255);
g.fillRect(100, 200, 15, 15);
}
}

Để kích hoạt Canvas, tạo nó trong lớp MIDlet và hiển thị nó trong phương thức commandAction()
public Displayable initGameCanvas() {
  if (gameCanvas == null){
    gameCanvas = new MyCanvas();
    // add a back Command to return to the menu screen
    gameCanvas.addCommand(initBackCommand());
    // set the listener to our actions
    gameCanvas.setCommandListener(this);
  }
  return gameCanvas;
}

Vòng lặp game:

Trước khi bắt đầu, bạn phải hiểu những cách thức thông thường mà một ứng dụng game hoạt động. Một game hay animation được xây dựng dựa trên một mẫu(piece) code thực thi được lặp đi lặp lại. Mẫu code này theo dõi giá trị  của các biến và các trạng thái game. Dựa trên trạng thái game,  đoạn code sẽ vẽ hay vẽ lại các phần tử trên game. Các giá trị của biến có thể được thay đổi bởi các tương tác của người dùng hay các hành vi của game bên trong.

Điều này được tạo ra bằng cách đặt đoạn code đó lặp đi lặp lại trong một vòng lặp liên tục. Trước khi vào vòng lặp, một biến có thể sẽ được kiểm tra xem game còn chạy hay không. Nếu không, vòng lặp có thể được thoát. Các đoạn code trong vòng lặp nên cho phép thread thực thi hiện hành sleep vài giây để điều khiển tốc độ(rate) mỗi khi trạng thái game được cập nhật(đó là sau bao lâu thì màn hình game được refresh). Code có dạng sau:

public class MyCanvas extends GameCanvas implements Runnable{

public void start() {
run = true;
Thread t = new Thread(this);
t.start();
}

public void stop() {
run = false;
}

public void run(){
init();
while (run){
// update game elements, positions, collisions, etc..
updateGameState();
// check user input
checkUserInput();
// render screen
updateGameScreen();
// redraws screen
flushGraphics();
// controls at which rate the updates are done
Thread.sleep(10);
}
}

}

Ở đoạn code trên, lớp GameCanvas được sử dụng. Lớp GameCanvas là một trường hợp đặc biệt của lớp Canvas, được optimize cho các game. Nó sử dụng với các kỹ thuật sau:

  • Bộ đệm đôi(double buffer): GameCanvas sử dụng một image off-screen mà image này được sử dụng cho tất cả các thao tác vẽ. Khi thao tác vẽ hoàn tất, nó được vẽ lên màn hình nhờ sử dụng phương thức flushGraphics(). Điều này giúp tránh tình trạng màn hình bị flick và các chuyển động mượt mà hơn.

  • Lưu trữ trạng thái phím trong một mảng: Thông qua phương thức getKeyStates(), bạn có thể truy cập vào một mảng bit tương ứng với trạng thái của mỗi phím sử dụng các giá trị hằng(constant) được định nghĩa trong Canvas.


Bên cạnh sử dụng GameCanvas, bạn cần sử dụng một Thread để giữ các chuyển động trong game độc lập với các event của MIDlet. Bằng cách này, các animation sẽ không cần chờ các event của hệ thống vẽ lại chính nó. Trong ví dụ này, game có 3 thực thể:

  • Pad: là một hình chữ nhật nhỏ di chuyển từ trái sang phải nằm phái dưới màn hình.

  • Ball: nằm trước Pad, khi bạn nhấn phím Fire, nó sẽ di chuyển với tốc độ theo chiều dọc ngang của Pad.

  • Brick: các khối tĩnh nằm phía trên màn hình, khi chúng trúng Ball thì chúng sẽ biến mất.


Mục đích của game là làm cho các Brick biến mất càng nhanh càng tốt, và không để cho Ball chạy ra khỏi phía dưới của màn hình. Game cần theo dõi 3 biến:

  • Điểm số của người chơi. Người chơi sẽ nhận được 10 điểm mỗi lần làm một Brick biến mất.

  • Số lượt chơi: mỗi lần Ball chạy ra khỏi phía dưới màn hình, người chơi mất một lượt.

  • Thời gian chơi game: người chơi phải hoàn tất game trong khoảng thời gian cho phép hay giới hạn.


Lớp Entity là supeclass của 3 thực thể trên, gồm có các thuộc tính và chức năng sau:

  • x, y: Xác định vị trí hiện tại của thực thể.

  • speedX, speedY: Xác định tốc độ của thực thể.

  • width, height: Xác định kích thước của thực thể.

  • update(): Xác định hành vi của các thực thể.

  • paint(): phương thức được lớp Graphics sử dụng để vẽ các thực thể.

  • collided(): Kiểm tra va chạm giữa các thực thể.


Tiếp theo, mở rộng lớp Entity cho mỗi phần tử game và cài đặt phương thức update() và paint(). Đối với Ball:

public class Ball extends Entity {
public int radium = 2;

public Ball(int radium){
this.radium = radium;
width = radium * 2;
height = radium * 2;
// red color
this.color = 0x00FF0000;
}

/**
* Paints the ball using a circle
*/

public void paint(Graphics g) {
g.setColor(color);
g.fillArc(x, y, radium*2, radium*2, 0, 360);
}

/***
* Updates the ball position.
*/

public void update() {
// update position
oldX=x;
oldY=y;
x += speedX;
y += speedY;
}
}

Đối với Pad:

public class Pad extends Entity{
int minLimit = 0;
int maxLimit = 1;

public Pad(int width, int height) {
this.width = width;
this.height = height;
}

public void paint(Graphics g) {
g.setColor(0,0,255);
g.fillRect(x, y, width, height);
}

public void update() {
// change x position according the speed
x += speedX;
// check if world bounds are reached
if (x < minLimit) {
x = minLimit;
}
if (x+width > maxLimit){
x = maxLimit – width;
}
}
}

Đối với Brick:

public class Brick extends Entity {
boolean active = true;

public Brick(int color){
this.color = color;
}

public void paint(Graphics g) {
// only paints if still active
if (active){
g.setColor(color);
g.fillRect(x, y, width, height);
}

}

public void update() {
// the bricks don’t move
}
}

Bây giờ, tạo và cấu hình các lớp này trên lớp Canvas. Tạo phương thức init() trên lớp Canvas:

public void init(){
// resets lifes
lifes = 3;
// resets score
score = 0;
// resets time
time = 0;
// bricks hit
bricksHit = 0;
// create a pad
pad = new Pad(getWidth()/10,getWidth()/10/4);
pad.x = (this.getWidth()-pad.width) / 2;
pad.y = this.getHeight() – (2*pad.height);
pad.maxLimit = getWidth();
pad.minLimit = 0;

// create ball 
ball = new Ball(4);
ball.x = getWidth() / 2;
ball.y = getHeight() / 2;
ball.speedX = 1;
ball.speedY = 1;
// set collision limits
wallMinX = 0;
wallMaxX = getWidth();
wallMinY = 0;
// to allow to get out of screen
wallMaxY = getHeight() + 4 * ball.radium;

// create bricks
Brick brick;
bricks = new Vector();
for (int i=0; (i*(BRICK_WIDTH+2))<getwidth(); i++){<br="">brick = new Brick(Util.setColor(255,0,0));
brick.width  = BRICK_WIDTH;
brick.height = BRICK_HEIGHT;
brick.x = (i*(brick.width+2));
brick.y = 20;
bricks.addElement(brick);
}
}

Sau khi tất cả các đối tượng được tạo ra, cập nhật lại trạng thái và vẽ lại, được mô tả trong 2 phương thức updateGameState() và updateGameScreen():

// draws elements to the screen
protected void updateGameScreen(Graphics g) {
// stores width and height
width = getWidth();
height = getHeight();
// set background color
g.setColor(0,0,0);
// clear screen
g.fillRect(0, 0, width, height);
// draw score
g.setColor(255,255,255);
g.drawString(“Score:”+score+” Lifes:”+lifes+” Time: “+time, 0, 0, Graphics.TOP|Graphics.LEFT);
// draw game elements
pad.paint(g);
ball.paint(g);
// draw bricks stored in the Vector bricks
for (int i=0; i < bricks.size(); i++){
Brick brick = (Brick)(bricks.elementAt(i));
brick.paint(g);
}
}
// updates state of all elements in the game
public void updateGameState(){
pad.update();
ball.update();

checkBallCollisionWithWalls();
checkBallCollisionWihPad();
checkBallCollisionWithBricks();
checkBallOutOfReach();

// check if bricks ended
if (bricksHit == bricks.size()){
run = false;
}
}

Game cần tương tác trở lại với các sự kiện keypad để di chuyển pad của người chơi.
// update game entities according to use presses on keypad
  public void checkUserInput() {
    int state = getKeyStates();
    if ( (state & GameCanvas.LEFT_PRESSED) > 0) {
      // move left
      pad.speedX=-1;     
    } else if ( (state & GameCanvas.RIGHT_PRESSED) > 0) {
      // move right
      pad.speedX=1;
    } else {
      // don't move
      pad.speedX=0;
    }
  }

Bây giờ nếu bạn chạy trò chơi trên mô phỏng, các bạn sẽ có một màn hình trò chơi thực sự, nơi bạn có thể chơi trò chơi riêng của bạn: Di chuyển pad, trúng ball và xóa tất cả các brick.

Trong phần tiếp theo giải thích cách sử dụng hình ảnh để có được một trò chơi tìm kiếm tốt hơn.

Code


Phát Triển Game Đơn Giãn Trên Mobile(P4)




Trong bài trước, lớp GameCanvas được xây dựng với tất cả các tương tác giữa các element chính. Bây giờ, tất cả các element gameplay được xây dựng, đây là lúc cải thiện giao diện trực quan của trò chơi bằng cách sử dụng các tập tin hình ảnh thay vì sử dụng phương thức draw/fill trên đối tượng graphics để biểu diễn các thực thể trò chơi. Tôi đã tạo ra một số hình ảnh dựa trên các tài nguyên SpriteLib sẽ được sử dụng trong trò chơi.

 

 

Image

Để truy cập và hiển thị các hình ảnh trong MIDlet, ta phải sử dụng một phần tử hay lớp Image trong giao diện người dùng cấp thấp. Lớp này lưu trữ dữ liệu hình ảnh đồ họa độc lập với thiết bị hiển thị trong một bộ nhớ đệm off-screen. Các hình ảnh hoặc ở dạng Mutable hay Immutable phụ thuộc vào cách mà chúng được tạo ra. Thông thường, các hình ảnh dạng Immutable được ta ra thông qua việc load hình ảnh từ tài nguyên như file Jar hay mạng. Các hình ảnh dạng Immutable được tạo ra thông qua việc sử dụng phương thức tĩnh createImage() của lớp Image. Tuy nhiên, một khi ảnh Immutable đã được tạo ra thì nó không thể được sửa đổi. Các hình ảnh dạng Mutable được tạo ra thông qua phương thức khởi dựng(Constructor) của lớp Image và lúc đó nó chỉ chứa các pixel trắng. Ứng dụng có thể biểu diễn image Mutable bằng cách gọi phương thức getGraphics() trên Image để có được đối tượng Graphics cho mục đích này.

Trong game này, các hình ảnh Immutable đại diện cho các thực thể game. Bây giờ, ta hãy thay đổi lớp Pad và tạo ra đối tượng Image trong constructor của nó.

Image image;
public Pad() {
try {
image = Image.createImage(“/pad.png”);
width = image.getWidth();
height = image.getHeight();
catch (IOException e) {
e.printStackTrace();
}
}

Sau đó, cài đặt lại phương thức paint() của lớp Pad để vẽ hình ảnh:

public void paint(Graphics g) {
g.drawImage(image, x,y, Graphics.TOP | Graphics.LEFT);
}

Lưu ý: khi tạo hình ảnh, bạn nên sử dụng ảnh với định dạng PNG, bởi đây là định dạng thông dụng của các loại thiết bị. Những thiết bị đời mới có thể hỗ trợ nhiều định dạng khác như: JPEG, BMP…, tuy nhiên để an toàn bạn nên tạo ra file ảnh dạng PNG.

Tiếp theo, ta sửa đổi lớp Ball như sau:

Image image;
public Ball() {
try {
image = Image.createImage(“/ball.png”);
width = image.getWidth();
height = image.getHeight();
catch (IOException e) {
e.printStackTrace();
}
}

public void paint(Graphics g) {
g.drawImage(image, x,y, Graphics.TOP | Graphics.LEFT);
}

Bây giờ, nếu bạn chạy ứng dụng, bạn sẽ cố một Ball đẹp, tuy nhiên trong game sử dụng nhiều Brick với màu sắc khác nhau, và mỗi Brick được cắt từ một Image chính. Bạn có thể làm điều này thông qua sử dụng phương thức setClip() trong lớp Graphics, tuy nhiên từ MIDP 2.0 trở đi, nó còn một hỗ trợ một lớp Sprite để ta làm điều này.

Sprite

Một Sprite là một thuật ngữ chung trong game. Nó tham chiếu đến một phần tử trực quan được tạo ra bởi các Image, thường chuyển động và di chuyển xung quanh các phần tử khác một cách độc lập trong game. Lớp Sprite trong MIDP 2.0 đại diện cho khái niệm này. Nó cho phép tạo ra các Sprite dựa trên các hình ảnh với nhiều frame. Nó có thể thay đổi frame, điều khiển chuyển động và kiểm tra va chạm với các phần tử khác.

Tất cả các khả năng này được sử dụng trong các thực thể của game, và ta hãy xem cách xây dựng Brick:

public static int BRICK_FRAMES = 20;
Image image = null;
Sprite sprite = null;
public Brick(){
// load image
image = Image.createImage(“/bricks.png”);
// create the sprite with 20 frames, one for each brick
sprite = new Sprite(image,image.getWidth()/BRICK_FRAMES, image.getHeight());
width = sprite.getWidth();
height = sprite.getHeight();
}

Mã này tạo ra một Sprite với 20 khung hình(frame), một cho mỗi Brick có sẵn trên hình ảnh. Trước khi vẽ Brick bạn cần thay đổi frame sẽ được sử dụng.

public void paint(Graphics g) {
if (active){
sprite.nextFrame();
sprite.setPosition(x, y);
sprite.paint(g);
}
}

Bây giờ, nếu bạn chạy Midlet, bạn sẽ được các Brick thay đổi màu sắc vào tất cả thời gian. Tuy nhiên, vấn đề đặt ra ở đây là hình ảnh bricks.png được load cho mỗi Brick được tạo ra, điều này thật sự không tối ưu. Sửa đổi mã để nó tối ưu như sau:

public Brick(Image image, int numFrames, int frameSelected){
sprite = new Sprite(image,image.getWidth()/numFrames,image.getHeight());
// set frame
sprite.setFrame(frameSelected);
// get size for collision detection
width = sprite.getWidth();
height = sprite.getHeight();
}

public void paint(Graphics g) {
if (active){
sprite.setPosition(x, y);
sprite.paint(g);
}
}

Bây giờ chỉ cần load một lần trong phương thức init () và chọn một frame sẽ được sử dụng trong vòng đời của một Brick.

// create bricks
Image bricksFrames = null;
try {
bricksFrames = Image.createImage(“/bricks.png”);
catch (IOException e) {
e.printStackTrace();
}
Brick brick = new Brick(bricksFrames, 20, 0);
bricks = new Vector();
for (int i=0; (i*(brick.width+2)) < getWidth(); i++){
brick = new Brick(bricksFrames, 20, i);
brick.sprite.setFrame(i);
brick.x = (i*(brick.width+2));
brick.y = 20;
bricks.addElement(brick);
}

Bây giờ bạn có một hàng các Brick đẹp, mỗi hàng đều có mỗi Brick với một màu riêng. Trong bài tiếp theo, chúng ta sẽ thảo luận cách lưu điểm số của ngươi chơi(high-score).

Code


Phát Triển Game Đơn Giãn Trên Mobile(P5)




Để lưu điểm số high-score, trước tiên bạn cần hiểu Input Output trong MIDlet và cách sử dụng RecordStores.

Streams

Hãy bắt đầu với các operation IO đơn giãn. Các lớp mà thực thi chúng nằm trong gói java.io. Java ME chỉ có một ít các lớp sẵn có trong Java SE nhưng tất cả chúng đều rất hữu ích.

  • Input Stream, Output Stream: Các lớp cơ sở cho các byte streams kiểu binary.

  • ByteArrayInputStream, ByteArrayOutputStream: các mảng buffer stream trong bộ nhớ.

  • DataInputStream, DataOutputStream: Đọc và ghi các kiểu dữ liệu nguyên thủy Java(int, float, String, …) sang streams.

  • Reader, Writer: Các lớp cơ sở cho các stream character.

  • OutputStreamWriter, InputStreamReader: Các lớp đọc stream character sử dụng encodings.


Để học cách sử dụng các class này, ta tạo file settings để lưu trữ các options của game.

  • Số lượt chơi(life)

  • Tốc độ Ball

  • Thời gian hoàn tất một level

  • Số điểm khi ném trúng một Brick


Để thực thi file Settings này, ta tạo một file text settings.txt, nơi mà mỗi dòng của nó đại diện cho một setting. Tên setting và giá trị của nó được phân cách bởi dầu ‘=’.
ball_speed=2
start_lifes=4
level_time=100
brick_points=10

Thêm file này vào tài nguyên của project và đảm bảo rằng file này được lưu trong file JAR với các lớp. Để truy cập vào file này, bạn sử dụng hàm getResourceAsStream(String name) từ lớp Class. Phương thức này cho phép bạn truy cập vào bất kỳ file nào trong file JAR. Với đường dẫn root “/” tham chiếu tới root level của file JAR. Ví dụ, đoạn mã sau dùng để truy cập vào file settings.txt của file JAR:

InputStream is = this.getClass().getResourceAsStream(“/settings.txt”);
// read first byte
byte c= is.read();

Đoạn code trên trả về byte đầu tiên của file, nhưng cái thực sự bạn cần là đọc một dòng như là một stream character. Sử dụng InputStreamReader để thay thế và phân tích mỗi dòng cho mỗi cặp giá trị .

public class Settings {
public Hashtable values;

public Settings(String file) {
values = new Hashtable(10);
read(file);
}

public String getSetting(String key){
return (String)(values.get(key));
}

/**
* Opens the file and reads all the settings to the hashtable
* @param file, the name of the file to read
*/

public void read(String file){
// open file
InputStream is = this.getClass().getResourceAsStream(file);
InputStreamReader isr = new InputStreamReader(is);
// create a buffer to store lines
StringBuffer lineBuffer = new StringBuffer();
int c;
try {
c = isr.read();
while(c != -1){
lineBuffer.append((char)c);
c = isr.read();
// checks for end of line character
if ( c == 10 || c==-1){
// cleans extra spaces or end of lines chars
String line = lineBuffer.toString().trim();
// splits the string using the = character
int pos = line.indexOf(“=”);
if (pos != -1){
// adds a new setting
String key = line.substring(0,pos);
String value = line.substring(pos+1);
values.put(key, value);
}
// clean buffer
lineBuffer.setLength(0);
}
}
catch (IOException e) {
e.printStackTrace();
finally {
try {
is.close();
catch (IOException e) {
e.printStackTrace();
}
}
}
}

Bây giờ, bạn chỉ cần sử dụng lớp này trong phương thức init() của lớp Canvas:

public void initSettings(){
Settings setting = new Settings(“/settings.txt”);
BALL_SPEED = Integer.parseInt(setting.getSetting(“ball_speed”));
MAX_LIFES = Integer.parseInt(setting.getSetting(“start_lifes”));
MAX_TIME = Integer.parseInt(setting.getSetting(“level_time”));
BRICK_SCORE = Integer.parseInt(setting.getSetting(“brick_points”));
}

public void init(){
initSettings();
[...]
}

Lúc này, file cấu hình đã được tạo ra, tuy nhiên phương thức getResourceAsStream chỉ cho phép đọc file lưu trữ trong file JAR nhưng ko cho phép ghi vào chúng.

RecordStore

Để ghi vào tập tin, bạn cần sử dụng lớp RecordStore. Lớp này cho phép tạo một database nhỏ, nơi mà bạn có thể lưu trữ hay lấy dữ liệu liên tục mỗi khi start MIDlet.

Class này được cài đặt trong gói javax.microedition.rms và nó cung cấp những phương thức  constructor tĩnh sau đây:

  • openRecordStore(String recordStoreName, boolean createIfNecessary)

  • openRecordStore(String recordStoreName, boolean createIfNecessary, int authmode, boolean writable)

  • openRecordStore(String recordStoreName, String vendorName, String suiteName)


Những phương pháp này cho phép tạo ra và / hoặc mở một RecordStore. Tên của các RecordStore phân biệt hoa thường và có chiều dài tối đa 32 ký tự. Một trong những tính năng đặc biệt mà cần phải được đưa vào account là setting authmode. Thiết lập này có hai giá trị:

  • AUTHMODE_PRIVATE: trong chế độ này, chỉ có MIDlet tạo ra RecordStore mới được phép truy cập nó.

  • AUTHMODE_ANY: Trong chế độ này, bất kỳ MIDlet nào đều có thể mở RecordStore, đọc dữ liệu từ nó và nếu setting writable thiết lập là true, thì có thể ghi dữ liệu vào nó.Điều này cho phép chia sẽ dữ liệu giữa các MIDlet, vì thế có thể 2 game cùng chia sẽ một bảng high-score. Các RecordStore được xác định duy nhất bởi tên tạo ra nó, MIDlet-Name, MIDlet-Vendor trong file JAD.


Sau khi bạn mở RecordStore, bạn có thể thêm record vào nó. Mỗi record được làm bằng một mảng byte và nó có một ID duy nhất khi nó được tạo ra. Sau khi bạn tạo một record, bạn có thể lấy dữ liệu của nó, thay đổi dữ liệu, và xóa nó. Các phương thức sau đây hỗ trợ các thao tác đó:

  • addRecord(byte[] data, int offset, int numBytes)

  • deleteRecord(int recordId)

  • setRecord(int recordId, byte[] newData, int offset, int numBytes)


Kích thước tối đa của RecordStore phụ thuộc vào thiết bị. Bạn có thể sử dụng phương thức getSizeAvailable(), tuy nhiên giá trị trả về của phương thức này không thực sự chính xác trong hầu hết các loại thiết bị.

Sử dụng RecordStore để lưu trữ high score như sau:

public void saveData() {
try {
// open records store options
RecordStore options = RecordStore.openRecordStore(“options”, true);
byte[] data = saveOptions();
// check if record store not empty
if (options.getNumRecords() != 0) {
// update the settings
options.setRecord(1, data, 0, data.length);
else {
// adds the settings
options.addRecord(data, 0, data.length);
}
// closes the record store
options.closeRecordStore();
catch (RecordStoreException ex) {
}
}

public byte[] saveOptions() {
// create a byte array stream to store data temporarily 
ByteArrayOutputStream baos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(baos);
try {
dos.writeBoolean(soundOn);
// write scores
for (int i =0; i < scores.length; i++){
dos.writeInt(scores[i].value);
dos.writeUTF(scores[i].name);
dos.writeLong(scores[i].when.getTime());
}
// push all the data to the byte array stream
dos.flush();
catch (IOException ex) {

}
// returns bytes from stream
return baos.toByteArray();
}

Trong ví dụ trên, tên RecordStore là “options” được dùng để lưu trữ settings và high scores. Đối với quá trình lưu trữ, một DataOutputStream với một ByteArrayOutputStream để convert dữ liệu sang một mảng nhị phân.

Bây giờ bạn cần cài đặt việc đọc dữ liệu:

public void loadData() {
try {
RecordStore options = RecordStore.openRecordStore(“options”, true);
// check if record store not empty
if (options.getNumRecords() != 0) {
loadOptions(options.getRecord(1));
}
options.closeRecordStore();
catch (RecordStoreException ex) {
}
}

public void loadOptions(byte[] data) {
// create a byte array stream to store data temporarily
ByteArrayInputStream bais = new ByteArrayInputStream(data);
// creates a data input stream to read from
DataInputStream dis = new DataInputStream(bais);
try {
soundOn = dis.readBoolean();
// read scores
for (int i = 0; i < scores.length; i++) {
int value = dis.readInt();
String name = dis.readUTF();
Date date = new Date(dis.readLong());
scores[i] = new Score(value, name, date);
}
dis.close();
catch (IOException ex) {

}
}

Ở đoạn code trên, bạn chỉ cần mở một RecordStore và kiểm tra xem nó đã được tạo hay chưa. Nếu có, thì lấy dữ liệu và sử dụng DataInputStream kết hợp với ByteArrayInputStream.

Bây giờ, bạn chỉ cần đưa chúng vào phần startup và shutdown MIDlet.

public void initOptions() {
soundOn = true;
if (scores == null) {
scores = new Score[10];
for (int i = 0; i < scores.length; i++) {
scores[i] = new Score(0, “Empty”, new Date());
}
}
// loads data in record stores if available
loadData();
}

public void exit() {
// store high scores and setting to record store
saveData();
notifyDestroyed();
}

Đến lúc này, bạn đã có thể lưu trữ high score và settings của player. Trong bài tiếp theo, chúng ta sẽ được tìm hiểu cách sử dụng settings và thêm vào âm thanh cho game.

Download code ở đây.Chúc các bạn thành công.


Phát Triển Game Đơn Giãn Trên Mobile(P6)




Bài viết trước mô tả làm thế nào để lưu các cài đặt trò chơi, bao gồm cả âm thanh bật / tắt màn hình, nhưng nó không có bất kỳ âm thanh nào được phát ra. Bài viết này mô tả các Java Mobile Multimedia API (MMAPI) và giải thích làm thế nào để thêm âm thanh vào game.

MMAPI cung cấp một tập các khả năng đa phương tiện cho các thiết bị di động, bao gồm cả phát lại và ghi âm dữ liệu âm thanh và video từ nhiều nguồn khác nhau. Tất nhiên, không phải tất cả thiết bị di động hỗ trợ tất cả các tùy chọn này, nhưng MMAPI được thiết kế để tận dụng những khả năng có sẵn trên thiết bị và bỏ qua những cái mà nó không hỗ trợ.

Thông tin MMAPI

Các MMAPI được xây dựng dựa trên sự trừu tượng cấp cao của tất cả các thiết bị đa phương tiện. Sự trừu tượng này được cài đặt(implement) trong ba lớp  đã hình thành lõi(core) của các hoạt động mà bạn làm với các API này. Những lớp này là 2 interface Player và Control, và lớp Manager. Một lớp khác, lớp trừu tượng DataSource, được sử dụng để xác định vị trí tài nguyên, nhưng nếu bạn định nghĩa một phương pháp mới để đọc dữ liệu, bạn sẽ không cần sử dụng nó một cách trực tiếp.

Tóm lại, lớp Manager được sử dụng để tạo ra các thể hiện Player cho các media khác nhau bằng cách xác định các thể hiện DataSource. Như vậy, các thể hiện Player có thể tạo ra được cấu hình bằng cách sử dụng các thể hiện Control. Ví dụ, hầu hết các thể hiện Player hỗ trợ VolumeControl để điều khiển âm lượng của các Player. Kiểm tra sơ đồ sau đây:

 

 

Lớp Manager về cơ bản là một factory của các player được hỗ trợ bởi các phương thức sau đây:

  • createPlayer(DataSource source): tạo một player dựa trên DataSource.

  • createPlayer(InputStream stream, String type): tạo một player sử dụng input stream như là nguồn và giả định rằng các kiểu media đã được cung cấp. Để biết thêm các kiểu media, bạn có thể vào địa chỉ:http://www.iana.org/assignments/media-types/ để kiểm tra.

  • createLayer(String url): tạo một player sử dụng một url để xác định dữ liệu nguồn.


Phương thức cuối cùng cho phép bạn phân bổ các loại media khác nhau, tùy thuộc vào giao thức URL được chọn chọn. Các loại sau được hỗ trợ:

  • Midi Player – “device://midi”: tạo a midi Player.

  • Tone Player – “device://tone”: tạo a tone Player.

  • Capture Audio – “capture://audio”: cho phép capture audio từ thiết bị.

  • Capture Video – “capture://video”: cho phép capture video từ thiết bị.

  • Capture Radio – “capture://radio?f=105.1&st=stereo”: cho phép capture radio.


Để tìm hiểu các kiểu nội dung và giao thức được hỗ trợ trên thiết bị, bạn sử dụng các phương thức của lớp Manager:

  • getSupportedContentTypes(): cung cấp một danh sách các kiểu nội dung sẵn có cho tất cả các giao thức hay một giao thức cụ thể.

  • getSupportedProtocols(): cung cấp một danh sách các giao thức sẵn có cho tất cả các kiểu nội dung hay một kiểu nội dung cụ thể.


Sau khi bạn đã tạo ra một player, bạn có thể bắt đầu sử dụng nó bằng cách đơn giản gọi phương thức start (). Khi đạt đến sự kết thúc của media thì nó stop một cách tự động. Đây là một cái nhìn đơn giản của lớp Player. Trên thực tế lớp có năm tiểu trạng thái:

  • UNREALIZED: đây là trạng thái đầu tiên của Player thu được từ lớp Manager.

  • REALIZED: Khi phương thức realized() được gọi, Player chuyển sang trạng thái này để thu nhận thông tin cần thiết để có được các nguồn media.Quá tình realized một Player có thể là một tài nguyên và quá trình tiêu tốn thời gian. Player có thể phải giao tiếp với server, đọc một file hay tương tác với một tập các đối tượng.

  • PREFETCHED: Sau khi một player được realized, nó có thể vẫn cần các tài nguyên khan hiếm hay độc quyền, fill bộ đệm với dữ liệu media hay thực hiện các tiến trình start-up khác. Và điều này được thực hiện bằng cách gọi phương thức prefetch() để chuyển đổi sang trạng thái này.

  • STARTED: Khi gọi phương thức start(), Player bắt đầu play tài nguyên media cho đến khi nó đạt đến sự kết thúc của media.

  • CLOSED: khi gọi phương thức close(), Player chuyển đổi sang trạng thái này, giải phóng tất cả các tài nguyên nắm giữ. Và nó không thể sử dụng lại lần nữa.


Hình dưới đây cho biết nhiều trạng thái khác nhau và có thể chuyển tiếp giữa chúng:

 

 

Nếu ứng dụng của bạn cần thông tin về chuyển đổi trạng thái, bạn cần implements giao diện PlayerListener.

Play a sound

Trong game này, ý tưởng để play một sound mỗi khi Ball di chuyển chạm vào Brick hay Pad. Để làm được điều này, tạo một lớp gọi là Multimedia với phương thức playSound():

//multimedia libraries
import javax.microedition.media.Manager;
import javax.microedition.media.Player;
import javax.microedition.media.MediaException;
public class Multimedia {
public void playSound(String file, String format) {
try {
InputStream is = getClass().getResourceAsStream(file);
Player p = Manager.createPlayer(is, format);
p.start();
catch (IOException ioe) {
catch (MediaException me) {
}
}

Bây giờ chỉ cần sử dụng phương thức bên dưới mỗi khi va chạm giữa Ball và các thực thể khác được phát hiện. Cho mục đích này, tôi đã tạo một sound có tên click.wav và nó phải sẵn có trong tài nguyên của ứng dụng.

public void updateGameState(){

byte colision = ball.colided(pad);
if (colision != Entity.COLLISION_NONE){
if (midlet.soundOn){
midlet.multimedia.playSound(“click.wav”, “audio/X-wav”);
}
}

}

Nếu chạy ứng dụng, bạn sẽ nghe một vài âm thanh khi play game.

Capture Video

Bây giờ, game đã có âm thanh, bạn có thể chụp ảnh của người chơi mỗi khi người đó đạt điểm số cao nhất. Để làm được điều này, bạn cần truy cập vào camera video và show nó vào player. Phương thức dưới sẽ chụp ảnh video và lưu nó vào một item.

Player p;
VideoControl vc;
public Item showVideo(String url){
Item result = null;
try {
p = Manager.createPlayer(url);
p.realize();
// Grab the video control .
vc = (VideoControl)p.getControl(“VideoControl”);
if (vc != null) {
// create the Item with the video image
result =((Item)vc.initDisplayMode(VideoControl.USE_GUI_PRIMITIVE, null));
// add a label
result.setLabel(“Photo”);
}
// start capture
p.start();
catch (IOException ioe) {
catch (MediaException me) { }
return result;
}

VideoControl được sử dụng để tạo Item được sử dụng trong Form:
public Displayable initNewHighScore(int score, int pos) {
  ...
  newHighScoreForm.append(multimedia.showVideo("capture://video"));
  ...
}

Bây giờ, sử dụng VideoControl để chụp một image từ camera.

public Image captureVideo(){
Image result = null;
try {
// grab data
byte[] imageData = vc.getSnapshot(“encoding=png”);
// create image;
result = Image.createImage(imageData, 0, imageData.length);
catch (MediaException me) {
me.printStackTrace();
}
return result;
}

Sau đó, gọi phương thức này khi high score đã được lưu.
// we added an extra field to Score to store the image
  scores[pos].image = multimedia.captureVideo();

Sau đó, show các image trong màn hình high score.

Chạy ứng dụng để test chức năng mới này. Bài tiếp theo sẽ thảo luận về network trên mobile.

Download Code ở đây

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


Phát Triển Game Đơn Giãn Trên Mobile(P7 – Phần Cuối)




Ở bài trước, các bạn đã được hướng dẫn để tạo ra một game, ở phần này chúng ta sẽ bàn đến network. Một trong các đặc điểm chính của mobile, đó là khả năng được kết nối ở mọi nơi và mọi lúc. Bên cạnh việc bạn đạt được điểm số cao(high score); kết quả trả về có thể được chia sẽ trên internet. Với JavaME, bạn có thể dễ dàng truy cập vào những đặc điểm giao tiếp sau:

  • HTTP

  • Socket

  • SMS

  • Bluetooth


Bài viết này sẽ giải thích cách sử dụng giao thức HTTP để gửi dữ liệu high score đến server và làm thế nào để sử dụng chức năng gửi SMS để mời các người khác cùng tham gia game.

Connector

Đặc điểm chính khi sử dụng các chức năng network là lớp Connector. Lớp này cho phép tạo ra các đối tượng Connection thông qua các phương thức tĩnh:

  • open(String name)

  • open(String name, int mode)


Tham số name chính là một chuỗi URL để xác định kết nối mà bạn muốn tạo ra. Tương ứng với giao thức ở URL là một kiểu đối tượng Connection sẽ được tạo ra. Sau đây là danh sách các URL và các đối tượng Connection tương ứng:

  • http://www.server.com“: Trả về một kết nối HTTP sử dụng lớp HttpConnection.

  • “socket://server.com:8080″: trả về một kết nối TCP/IP.

  • “btsp://2343434d3434″: trả về một kết nối Bluetooth.

  • “sms://+351910000000″: trả về một kết nối SMS đến một số điện thoại +351910000000.


Trong các giao thức kể trên, thì giao thức HTTP được tất cả các thiết bị hỗ trợ JavaME hỗ trợ; các giao thức khác thì phụ thuộc vào thiết bị. Tuy nhiên hầu hết các loại điện thoại mới đều hỗ trợ tất cả các giao thức trên.

Lưu ý rằng khi bạn cố gắng mở bất kỳ loại kết nối nào, người dùng các ứng dụng được thông báo từ hệ thống quản lý ứng dụng và cung cấp tùy chọn để chấp nhận hoặc từ chối các yêu cầu kết nối. Nếu bị từ chối yêu cầu kết nối, ứng dụng sẽ nhận được một SecurityException và kết nối không được thiết lập.

HTTP

Có thể nói, kết nối HTTP là một loại kết nối phổ biến nhất trong tất cả các loại kết nối. Để tạo kết nối HTTP trong J2ME, bạn làm như sau:
import javax.microedition.io.Connector;
import javax.microedition.io.HttpConnection;

public class Network {
  public byte[] httpConnection(String url, byte[] dataToSend) throws IOException
  {
    HttpConnection hc = null;
    // Prepare Connection
    hc = (HttpConnection) Connector.open(url, Connector.READ_WRITE);
    [...]
  }
}

Các kết nối HTTP được đại diện bởi lớp HttpConnection. Lớp này có ba trạng thái:

  • Setup nơi mà các tham số yêu cầu được định nghĩa.

  • Connected nơi mà bạn có thể gửi và nhận dữ liệu. Bạn cần gửi dữ liệu trước khi bạn có thể nhận bất kỳ sự trả lời nào.

  • Closed sau khi bạn đọc dữ liệu và kết nối được kết thúc.


Khi bạn nhận đối tượng HttpConnection từ Connector, nó rơi vào trạng thái Setup. Sau đó, bạn có thể cấu hình các tham số yêu cầu. Một trong những lựa chọn chính đó là phương thức yêu cầu HTTP(POST, GET hoặc HEAD).

public byte[] httpConnection(String url, byte[] dataToSend) throws IOException {
[..]
if (dataToSend == null){
hc.setRequestMethod( HttpConnection.GET );
else {
hc.setRequestMethod( HttpConnection.POST );
}
[...]
}

Bạn cũng có thể cấu hình các thuộc tính yêu cầu, chẳng hạn như kiểu nội dung mà bạn sẽ gửi trong trường hợp POST:
hc.setRequestProperty("Content-type", "application/octet-stream" );

Sau khi bạn đã cấu hình kết nối, bạn có thể bắt đầu gửi dữ liệu. Khi bạn bắt đầu quá trình này, bạn không thể thay đổi các tham số yêu cầu. Nếu kết nối được thiết lập, bạn bắt đầu nhận được dữ liệu.

public byte[] httpConnection(String url, byte[] dataToSend) throws IOException {
[...]
if (dataToSend != null){
// Write Data
OutputStream os = hc.openOutputStream();
os.write( dataToSend );
os.close();
}
// gets answer from  server
int rc = hc.getResponseCode();
// check http response
if (rc == HttpConnection.HTTP_OK){
// Read Data 
InputStream in = hc.openInputStream();
ByteArrayOutputStream tmp = new ByteArrayOutputStream();
int ch;
while ((ch = in.read()) != -1) {
tmp.write(ch);
}
data = tmp.toByteArray();
in.close();
hc.close();
}

return data;
}

Xây dựng phương thức httpConnection() coi như đã xong, tiếp theo nó được cài đặt trong game để gửi high score đến server. Bạn hãy thêm một command vào màn hình high score:
public Displayable initScoreForm() {
  [...]
  cmdSendHighScore = new Command("Send to Server", Command.ITEM, 1);
  highScoreForm.addCommand(cmdSendHighScore);
  [...]
}

Tiếp theo, bạn tạo một phương thức sử dụng nó khi Command được lựa chọn:

public String sendScore(String user, int score) {
String result = “No Answer”;
// server to send data
String url = “http://www.sergioestevao.com/midpAdventures/post.php”;
// prepare http request
String urlTotal = url + “?user=” + user + “&score=” + score;
byte[] data = null;
try {
data = network.httpConnection(urlTotal, null);
catch (IOException e) {
result = “Communication Problems”;
e.printStackTrace();
catch (SecurityException s) {
// user denied access to communication
result = “You need to allow communications in order to send the highscore to server.”;
s.printStackTrace();
}
// check data return.
if (data != null) {
result = new String(data);
}
return result;
}

Và gọi nó khi Command được sử dụng:
if (cmd == comInviteSend) {
        result = sendScore(scores[0].name, scores[0].value);
        display(new Alert("Result", result, null, AlertType.INFO));
   }

Nếu bạn sử dụng đoạn code này, bạn sẽ nhận được một cảnh báo:

Warning: To avoid potential deadlock, operations that may block, such as networking, should be performed in a different thread than the commandAction() handler.

Hãy nhớ rằng bạn đang thực thi đoạn mã này như là một câu trả lời cho một sự kiện mà người sử dụng đang được thực thi trên thread UI. Nếu đoạn mã này block hoặc mất một thời gian dài để thực hiện, ứng dụng sẽ gặp khó khăn.

Để tránh vấn đề này, bạn nên sử dụng một thread để làm các công việc liên quan đến network. Hãy bắt đầu khai báo một thread và các biến trạng thái để điều khiển các hoạt động của thread.

private static final int ACTION_SEND_HIGHSCORE = 0;
public Thread workThread;
public boolean active = false;

Bất cứ lúc nào có một sự kiện làm tốn nhiều thời gian(như việc kết nối mạng có thể thành công hay thất bại, điều này phụ thuộc các yêu tố bên ngoài như đường truyền mạng…), thì bạn nên sử dụng một thread chuyên biệt để kích hoạt các hoạt động này.

public void doAction(int action) {
// stores action to do
this.action = action;
// check if thread is already created
if (workThread == null) {
workThread = new Thread(this);
workThread.start();
active = true;
}
// wakes up thread to work
synchronized (this) {
notify();
}
}

Thread này sẽ thực thi phương thức run():

public void run() {
while (active) {
// check what action to do
switch (action) {
case (ACTION_SEND_HIGHSCORE):
// send the first score to the server
result = sendScore(scores[0].name, scores[0].value);
commandAction(cmdReceiveHighScore, highScoreForm);
break;
}
// waits for action to do. 
synchronized (this) {
try {
wait();
catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

Để start thread này, cần khai báo một giao diện Runnable trong MIDlet:

public class MyMidlet extends MIDlet implements CommandListener, Runnable {

Bây giờ, bạn có thể gọi phương thức doAction() khi người dùng chọn command ‘Send high score’. Lưu ý rằng, phương thức run() gọi một command cmdReceiveHighScore. Điều này được sử dụng để hiển thị các kết quả của các giao tiếp với người dùng.
 if (cmd == cmdSendHighScore) {
        doAction(ACTION_SEND_HIGHSCORE);
      }
      if (cmd == cmdReceiveHighScore) {
        display(new Alert("Result", result, null, AlertType.INFO));
      }

SMS

Một trường hợp sử dụng cho giao tiếp trong Java ME là để mời bạn bè tham gia chơi game. Bạn cũng có thể có một cuộc cạnh tranh trong bảng điểm số cao.

Thứ nhất, cài đặt phương thức gửi tin nhắn SMS trong lớp network.

public boolean sendSms(String number, String message){
boolean result = true;
try {
//sets address to send message
String addr = “sms://”+number;
// opens connection
MessageConnection conn = (MessageConnection) Connector.open(addr);
// prepares text message
TextMessage msg =
(TextMessage)conn.newMessage(MessageConnection.TEXT_MESSAGE);
//set text
msg.setPayloadText(message);
// send message
conn.send(msg);
catch (Exception e) {
result = false;
}
return result;
}

Từ đoạn code ví dụ trên, bạn chỉ cần sử dụng phương thức open() của lớp Connector với định dạng URL là “sms://number”. Sau đó tạo một TextMessage và gửi nó thông qua Connection. Kích thước của message được giới hạn trong 160 ký tự.

Tiếp theo, bạn tạo một form để nhận tên và số điện thoại từ người dùng như sau:

public Displayable initInviteForm() {
if (inviteForm == null) {
inviteForm = new Form(“Invite”);
inviteForm.setCommandListener(this);
inviteForm.addCommand(initBackCommand());

inviteName = new TextField(“Name:”, “”, 20, TextField.ANY);
inviteNumber = new TextField(“Number:”, “”, 20, TextField.PHONENUMBER);
inviteForm.append(inviteName);
inviteForm.append(inviteNumber);
comInviteSend = new Command(“Invite”, Command.ITEM, 1);
inviteForm.addCommand(comInviteSend);
}

return inviteForm;
}

Tiếp theo, trong phương thức commandAction(), bạn cài đặt code để hiển thị form và gửi message:
public void commandAction(Command cmd, Displayable display) {
   [...]
   else if (display == inviteForm) {
      if (cmd == comBack) {
        display(initMainForm());
      }
      if (cmd == comInviteSend) {
        doAction(ACTION_SEND_INVITE);
      }
    }
  [...]
}

Cuối cùng, trong phương thức run(), bạn gọi phương thức sendSms() để gửi message:

public void run() {
[...]
case (ACTION_SEND_INVITE):
// invite another player to play
String inviteMessage = ” invites you to play Transnoid!”;
network.sendSms(inviteNumber.getString(), inviteName.getString()
+ inviteMessage);
break;
}
[..]
}

Download code ở đây

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

Post a Comment

Mới hơn Cũ hơn