Thứ Bảy, 2 tháng 2, 2013

Bài 4 – Đa phương tiện và MIDP 2.0



MIDP 2.0 cùng với Mobile Media API 1.1 (MMAPI) đưa ra các khả năng đa phương tiện cho các thiết bị di động, bao gồm việc chơi và ghi lại các dữ liệu âm thanh và hình ảnh từ nhiều nguồn khác nhau. Dĩ nhiên, không phải tất cả các thiết bị di động đều hỗ trợ hết, và MMAPI được thiết kế theo một cách để tận dụng các lợi thế sẵn có và phớt lờ đi những gì nó không hỗ trợ. MIDP 2.0 kèm theo một tập con MMAPI để đảm bảo rằng nếu gặp một thiết bị không hỗ trợ MMAPI, bạn vẫn có thể sử dụng phiên bản gọn hơn. Phiên bản gọn này chỉ hỗ trợ âm thanh (kể cả các giao điệu) và loại trừ mọi thứ liên quan đến hình ảnh hay video.
Trong bài học này, bạn sẽ học cách kết hợp các khả năng đa phương tiện trong MIDlet của bạn. Bạn sẽ học cách truy vấn một thiết bị để lấy về các thông tin khả năng hỗ trợ của thiết bị. Bạn cũng sẽ học cách playback từ các vị trí khác nhau. Nhưng trước tiên, một lý thuyết nhỏ được yêu cầu là hiểu cơ bản các MMAPI và các tập con của nó trong MIDP 2.0.
1.Đằng sau Mobile Media API (MMAPI)
MMAPI định nghĩa superset các khả năng đa phương tiện hiện diện trong MIDP 2.0. Bắt đầu là JSR 135 và hiện hành là phiên bản 1.1. Phiên bản hiện hành bao gồm thêm một số thay đổi tài liệu và các cập nhật an ninh, và được phân phối dưới một file jar trong J2ME wireless toolkit 2.2. Mặc dù với các mô tả phát hành về trạng thái toolkit mà MMAPI 1.1 kèm theo, phiên bản thực sự là 1.0.
MMAPI được xây dựng trên một trừu tượng mức cao với tất cả các thiết bị đa phương tiện có thể được trên một thiết bị bị giới hạn tài nguyên (resource-limited). Sự trừu tượng này rõ ràng ở trong 3 class hình thành các khối thao tác mà bạn làm với API này. Những lớp này là các giao diện Player và Control, và lớp Manager. Một lớp khác nữa, lớp trừu tượng DataSource, được dùng để định vị các tài nguyên, nhưng trừ phi bạn muốn định nghĩa một cách mới để đọc dữ liệu bạn chắc chắn sẽ không bao giờ cần sử dụng nó trực tiếp.
Nói tóm lại, bạn sử dụng lớp Manager để tạo các thể hiện Player cho phương tiện khác nhau bằng cách chỉ rõ các thể hiện DataSource. Các thể hiện Player vì thế được tạo ra là có khả năng cấu hình bằng cách sử dụng các thể hiện Control. Chẳng hạn, hầu hết mọi thể hiện Player sẽ hỗ trợ về mặt lý thuyết một VolumeControl để điều khiển volume của Player. Hình 1 cho thấy quá trình này.
Hình 1 – Quá trình tạo và quản lý Player
Lớp Manager là lớp trung tâm cho việc tạo ra các player – bộ chơi và nó cung cấp 3 phương thức để cho biết nguồn của phương tiện. Những phương thức này, tất cả đều là phương thức tĩnh, là createPlayer(DataSource source)createPlayer(InputStream stream, String type) và createPlayer(String locator). Phương thức cuối cùng được ưa thích vì nó cung cấp một cú pháp dạng URI cho việc định vị phương tiện. Chẳng hạn, nếu bạn muốn tạo một thể hiện Player trên một file âm thanh nền web, bạn có thể sử dụngcreatePlayer(“http://www.yourweb.com/audio/song.wav”). Tương tự, để tạo một Player media thu lấy âm thanh, bạn có thể sử dụng createPlayer(“capture://audio”), và còn nhiều nữa. Bảng sau cho biết các cú pháp hỗ trợ với các ví dụ mẫu:
Kiểu phương tiệnCú pháp điển hình
Capture audiocapture://audio” để bắt lấy audio trên thiết bị ghi lại audio mặc định hoặc “capture://devmic0?encoding=pcm” bắt lại audio trên thiết bị devmic0 khi PCM encoding
Capture videocapture://video” để ghi lại video từ thiết bị ghi video mặc định hoặc “capture://devcam0?encoding=rgb888&width=100&height=50” để ghi lại từ camera thứ hai, trong chế độ rgb888 và với chiều rộng và chiều cao chỉ định
Nghe radio trong thiết bịcapture://radio?f=105.1&st=stereo” để chỉnh đến tần số FM 105.1 FM và chế độ stereo
Bắt đầu streaming video/audio/text từ một nguồn bên ngoàirtp://host:port/type” trong đó type là một trong số: audio, video hoặc text
Chơi các giai điệu và MIDIdevice://tone” sẽ cho bạn một player bạn có thể sử dụng để chạy các giai điệu hoặc
device://midi” sẽ cho bạn một player bạn có thể sử dụng để chạy MIDI
Một danh sách các giao thức được hỗ trợ cho một kiểu nội dung xác định có thể được lấy về bằng cách gọi phương thức getSupportedProtocols(String contentType) sẽ trả về một mảng String. Chẳng hạn, nếu bạn gọi phương thức này với đối số “audio/x-wav” thì phương thức sẽ trả về một mảng với 3 giá trị: httpfile vàcapture. Điều này cho bạn biết rằng bạn có thể lấy về kiểu nội dung “audio/x-wav”, bằng các giao thức http vàfile, và bắt lấy nó bằng giao thức capture. Tương tự, một danh sách các kiểu nội dung được hỗ trợ cho một giao thức xác định có thể được truy cập bằng cách gọi phương thức getSupportedContentTypes(String protocol). Vì thế, việc gọi getSupportedContentTypes(“capture”) sẽ trả về audio/x-wav vàvideo/vnd.sun.rgb565, cho biết rằng bạn có thể bắt lấy audio chuẩn và video nén rgb565. Chú ý rằng việc chuyển giá trị null vào trong những phương thức này sẽ trả về tất cả giao thức và kiểu nội dung được hỗ trợ.
Một khi một thể hiện Player được tạo ra sử dụng các phương thức lớp Manager, thể hiện Player đó cần đi qua nhiều giai đoạn trước khi được sử dụng. Trong lúc được tạo ra, player ở trạng thái UNREALIZED và phải làREALIZED và PREFETCHED trước khi có thể STARTED. REALIZE là một quá trình player xem xét nguồn hay các tài nguyên media cuối và có đủ thông tin để bắt đầu việc thu thập chúng. PREFETCH xảy ra sau REALIZE và player thực sự yêu cầu những tài nguyên media này. Cả hai quá trình REALIZE và PREFETCH có thể tiêu tốn thời gian và tài nguyên, nhưng việc chúng thực hiện trước khi player bắt đầu đảm bảo rằng không có độ trễ khi thực sự bắt đầu xảy ra. Một khi player được bắt đầu, bằng phương thức start(), và đang xử lý dữ liệu media, có thể đi vào trạng thái PREFETCHED lại khi quá trình xử lý media tự dừng lại (chẳng hạn như khi đến đoạn cuối của media), bạn gọi thẳng phương thức stop() trên thể hiện Player, hoặc start() trên thể hiện Player để khởi động lại từ điểm dừng trước (nếu player đến cuối media, nghĩa là nó sẽ bắt đầu lại từ điểm bắt đầu).
Phong cách lập trình tốt nhất yêu cầu bạn gọi các phương thức realize() và prefetch() trước khi gọi phương thức start() để tránh bất kỳ độ trễ nào khi bạn muốn player bắt đầu. Phương thức start() gọi thẳng phương thức prefetch() (nếu player không ở trong trạng thái PREFETCHED), hoặc gọi phương thức realize() (nếu player không ở trạng thái REALIZED), nhưng nếu bạn gọi thẳng những phương thức này trước tiên, bạn sẽ có một thể hiện Player sẽ bắt đầu play ngay khi bạn gọi start(). Một player có thể đi vào trạng thái CLOSED nếu bạn gọi phương thức close() trên nó, sau khi thể hiện Player không thể được tái sử dụng lại. Thay vì đóng lại, bạn có thể hủy phân bổ một player bằng cách gọi phương thức deallocate(), sẽ trả về player tương ứng với trạng tháiREALIZED, do đó giải phóng tất cả tài nguyên sẽ được yêu cầu.
Hình 2 cho thấy các trạng thái và quá trình chuyển tiếp giữa chúng
Hình 2 – Các trạng thái media player và quá trình chuyển tiếp giữa chúng
Sự thông báo các chuyển tiếp giữa các trạng thái khác nhau có thể được chuyển giao để gắn các listener trên một player. Đến lúc kết thúc, một thể hiện Player cho phép bạn gắn một PlayerListener bằng cách sử dụng phương thức addPlayerListener(PlayerListener listener). Hầu hết tất cả các trạng thái chuyển tiếp đều được thông báo cho listener qua phương thức playerUpdate(Player player, String event, Object eventData).
Một player cũng cho phép kiểm soát các thuộc tính của media mà nó đang play bằng cách sử dụng các control. Một control là một chức năng xử lý media điển hình cho mỗi dạng media cụ thể. Chẳng hạn, VideoControl điều khiển hiển thị của video, trong khi MIDIControl cung cấp truy cập đến các thuộc tính của các thiết bị MIDI. Dĩ nhiên có vài control có thể phổ biến qua nhiều thiết bị khác nhau, VolumeControl là một ví dụ. Bởi vì giao diệnPlayer kế thừa giao diện Controllable, cung cấp các phương tiện để truy vấn danh sách các control có sẵn. Bạn có thể làm việc này bằng cách gọi phương thức getControls(), trả về một mảng các thể hiện Control, haygetControl(String controlType), trả về một Control (null nếu controlType không được hỗ trợ).
Như đã nói trước đây, MIDP 2.0 chứa một tập con của MMAPI 1.1. Điều này đảm bảo rằng thiết bị đó chỉ hỗ trợ MIDP 2.0 vẫn có thể một phương thức phát hiện và sử dụng phù hợp có thể tăng cường nếu rộng hơn API hiện tại. Tập con chỉ hỗ trợ các giai điệu và audio chỉ với 2 điều khiển, ToneControl và VolumeControl. Thêm nữa, nguồn tài nguyên không được hỗ trợ, và kể từ đây, lớp Manager trong MIDP 2.0 được rút gọn đi và không cung cấp createPlayer(DataSource source) nữa.
Trong các mục tiếp đây, bạn sẽ học cách chơi audio và video từ nhiều nguồn khác nhau trong MIDlet đa phương tiện của bạn.
2.Sử dụng Mobile Media API (MMAPI)
Có lẽ cách dễ nhất để học MMAPI là bắt đầu thử play một file audio đơn giản. Tất cả các thao tác multimedia, dù đó là audio playback hay video capture đều sẽ theo các khuôn mẫu tương tự nhau. Lớp Manager sẽ được dùng để tạo một thể hiện Player sử dụng một String locator. Sau đó Player sẽ được REALIZE, PREFETCH và PLAY cho đến lúc đóng nó lại. Có nhiều khác biệt nhỏ, và tôi sẽ chỉ ra trong quá trình học.
Hình 3 cho thấy các thao tác cho audio file playback đơn giản.
Hình 3 – Audio file playback đơn giản
Khi người dùng chạy MIDlet, anh ta đã được xác định play chỉ các bài hát trong danh sách, như là bài “Siren from jar”. Khi chọn bài hát này, màn hình thay đổi với văn bản “Playing media” và có 2 lệnh xuất hiên bên dưới: Stop và Pause, cho người dùng lựa chọn. Media đang được chơi và người dùng có thể tạm dừng lại hay dừng hẳn và trở về danh sách bài hát.
Mã tương ứng quá trình trên:
package com.j2me.part4;

import java.util.Hashtable;
import java.util.Enumeration;

import javax.microedition.lcdui.Item;
import javax.microedition.lcdui.List;
import javax.microedition.lcdui.Form;
import javax.microedition.midlet.MIDlet;
import javax.microedition.lcdui.Display;
import javax.microedition.lcdui.Command;
import javax.microedition.lcdui.Displayable;
import javax.microedition.lcdui.CommandListener;

import javax.microedition.media.Player;
import javax.microedition.media.Control;
import javax.microedition.media.Manager;
import javax.microedition.media.PlayerListener;

public class MediaMIDlet extends MIDlet
  implements CommandListener, PlayerListener {

  private Display display;
  private List itemList;
  private Form form;

  private Command stopCommand;
  private Command pauseCommand;
  private Command startCommand;

  private Hashtable items;
  private Hashtable itemsInfo;

  private Player player;

  public MediaMIDlet() {
    display = Display.getDisplay(this);
    // creates an item list to let you select multimedia files to play
    itemList = new List("Select an item to play", List.IMPLICIT);

    // stop, pause and restart commands
    stopCommand = new Command("Stop", Command.STOP, 1);
    pauseCommand = new Command("Pause", Command.ITEM, 1);
    startCommand = new Command("Start", Command.ITEM, 1);

    // a form to display when items are being played
    form = new Form("Playing media");

    // the form acts as the interface to stop and pause the media
    form.addCommand(stopCommand);
    form.addCommand(pauseCommand);
    form.setCommandListener(this);

    // create a hashtable of items
    items = new Hashtable();

    // and a hashtable to hold information about them
    itemsInfo = new Hashtable();

    // and populate both of them
    items.put("Siren from jar", "file://siren.wav");
    itemsInfo.put("Siren from jar", "audio/x-wav");
  }

  public void startApp() {

    // when MIDlet is started, use the item list to display elements
    for(Enumeration en = items.keys(); en.hasMoreElements();) {
    itemList.append((String)en.nextElement(), null);
  }

    itemList.setCommandListener(this);

    // show the list when MIDlet is started
    display.setCurrent(itemList);
  }

  public void pauseApp() {
    // pause the player
    try {
      if(player != null) player.stop();
    } catch(Exception e) {}
  }

  public void destroyApp(boolean unconditional) {
    if(player != null) player.close(); // close the player
  }

  public void commandAction(Command command, Displayable disp) {

    // generic command handler

    // if list is displayed, the user wants to play the item
    if(disp instanceof List) {
      List list = ((List)disp);

      String key = list.getString(list.getSelectedIndex());

      // try and play the selected file
      try {
        playMedia((String)items.get(key), key);
      } catch (Exception e) {
        System.err.println("Unable to play: " + e);
        e.printStackTrace();
      }
    } else if(disp instanceof Form) {

      // if showing form, means the media is being played
      // and the user is trying to stop or pause the player
      try {

        if(command == stopCommand) { // if stopping the media play

          player.close(); // close the player
          display.setCurrent(itemList); // redisplay the list of media
          form.removeCommand(startCommand); // remove the start command
          form.addCommand(pauseCommand); // add the pause command

        } else if(command == pauseCommand) { // if pausing

          player.stop(); // pauses the media, note that it is called stop
          form.removeCommand(pauseCommand); // remove the pause command
          form.addCommand(startCommand); // add the start (restart) command
        } else if(command == startCommand) { // if restarting

          player.start(); // starts from where the last pause was called
          form.removeCommand(startCommand);
          form.addCommand(pauseCommand);
        }
      } catch(Exception e) {
        System.err.println(e);
      }
    }

  }

  /* Creates Player and plays media for the first time */
  private void playMedia(String locator, String key) throws Exception {

    // locate the actual file, we are only dealing
    // with file based media here
    String file = locator.substring(
      locator.indexOf("file://") + 6,
      locator.length());

    // create the player
    // loading it as a resource and using information about it
    // from the itemsInfo hashtable
    player = Manager.createPlayer(
        getClass().getResourceAsStream(file), (String)itemsInfo.get(key));

    // a listener to handle player events like starting, closing etc
    player.addPlayerListener(this);

    player.setLoopCount(-1); // play indefinitely
    player.prefetch(); // prefetch
    player.realize(); // realize

    player.start(); // and start
  }

  /* Handle player events */
  public void playerUpdate(Player player, String event, Object eventData) {

    // if the event is that the player has started, show the form
    // but only if the event data indicates that the event relates to newly
    // stated player, as the STARTED event is fired even if a player is
    // restarted. Note that eventData indicates the time at which the start
    // event is fired.
    if(event.equals(PlayerListener.STARTED) &&
      new Long(0L).equals((Long)eventData)) {

      display.setCurrent(form);
    } else if(event.equals(PlayerListener.CLOSED)) {

      form.deleteAll(); // clears the form of any previous controls
    }
  }
}
Code 1 – Audio playback đơn giản
Bây giờ bạn có một audio player đơn giản. Để bắt đầu, MIDler hiển thị một danh sách các media có thể play được. Lúc đó, như trong hình ảnh, chỉ có một media có tên “Siren from jar”. Chú ý rằng trong mã, “Siren from jar” tương ứng với một truy cập đến file audio. Điều này ngụ ý rằng vị trí thực của media sẽ là trong file jar. Khi người dùng chọn bài hát này, một đối tượng Player được tạo ra theo phương thức playMedia(). Phương thức này nạp player, gắn một listener cho nó, tìm nạp trước, realize và cuối cùng, khởi động player. Và cũng chú ý rằng player này chơi media liên tục.
Bởi vì listener cho Player chính là lớp MIDlet, phương thức playerUpdate() bắt lấy các sự kiện player. Vì thế, khi người dùng bắt đầu nghe bài hát, Form được hiển thị, cho phép người dùng stop hay pause lại. Việc chọn Stopđưa người dùng trở lại danh sách, trong khi chọn Pause chỉ tạm dừng chơi và chơi lại từ điểm đánh dấu khi được khởi động lại.
Bằng việc tạo ra lớp generic này, bây giờ dễ dàng để thêm các dạng media khác cho player. Bên cạnh audio, video là các kiểu media chính sẽ được play. Để cho phép MIDlet play video, chỉ cần thay đổi trong phương thứcplayerUpdate(), để tạo một video screen. Việc này được thực hiện như trong đoạn mã sau, những phần thay đổi được tô đậm:
/* Handle player events */
  public void playerUpdate(Player player, String event, Object eventData) {

    // if the event is that the player has started, show the form
 // but only if the event data indicates that the event relates to newly
 // stated player, as the STARTED event is fired even if a player is
 // restarted. Note that eventData indicates the time at which the start
 // event is fired.
 If(event.equals(PlayerListener.STARTED) &&
   new Long(0L)Equals((Long)eventData)) {

        // see if we can show a video control, depending on whether the media
  // is a video or not
  VideoControl vc = null;
  if((vc = (VideoControl)player.getControl("VideoControl")) != null) {
    Item videoDisp =
   (Item)vc.initDisplayMode(vc.USE_GUI_PRIMITIVE, null);
    form.append(videoDisp);
  }

  display.setCurrent(form);
 } else if(event.equals(PlayerListener.CLOSED)) {

   form.deleteAll(); // clears the form of any previous controls
 }
  }
Các thay đổi trên cho phép bạn chơi các file video với sự trợ giúp đắc lực của MediaMIDlet này. Nếu phương thức quyết định cho player đó có một VideoControl, có thể trình bày ra bằng cách tạo một GUI cho nó. GUI này sau đó được gắn vào form hiện hành. Dĩ nhiên, bây giờ bạn cần gắn một video vào danh sách để bạn có thể thử nghiệm nó.
Nhớ lại rằng không phải tất cả các thiết bị di động đều chơi tất cả các file video (hay các file audio). Để xem danh sách các file video được hỗ trợ theo từng thiết bị, hãy sử dụng phương thứcManager.getSupportedContentTypes(null). Trong trường hợp của Wireless Toolkit, video/mpeg được hỗ trợ sẵn, nếu bạn có một file video có tên huetoday.mpg, hãy thêm vào trong danh sách như sau:
items.put("Promo Video from jar", "file://huetoday.mpg");
itemsInfo.put("Promo Video from jar", "video/mpeg");
Đặt video huetoday.mpg vào trong thư mục res, và bây giờ bạn có thể chọn và play file video đó khi MIDlet chạy lên. Kết quả có thể tương tự như hình 4 sau:
Hình 4 – Video playback với MediaMIDlet
3.Truyền tải stream media qua đường truyền mạng
Cũng giống như mọi file media khác, đặc biệt là các file video, sẽ không được phân bổ với MIDlet của bạn, trừ phi chúng thực sự có kích thước nhỏ. Với một ứng dụng MIDlet thành công, khả năng stream media qua đường mạng là một yếu tố cần thiết. MediaMIDlet có thể chơi các media qua đường mạng một cách dễ dàng bằng cách chỉ rõ một file dựa trên giao thức HTTP. Tuy nhiên, có hai vấn đề cần được làm sáng tỏ ngay sau đây.
Thứ nhất, truy cập media qua đường mạng yêu cầu các quyền truy cập từ phía người dùng cuối. Sau hết, người dùng phải chịu một mức phí mạng nào đó. Có nhiều cách để lấy được các quyền truy cập này và lưu các kết quả bên trong môi trường MIDlet, nhưng tôi sẽ không đi sâu chi tiết ở đây.
Thứ hai, truy cập media qua đường mạng có thể là một hành động tiêu tốn khá nhiều thời gian. Hành động này sẽ không được thực hiện trong thread chính của ứng dụng, trong trường hợp bị ràng buộc vào một mạng bị giới hạn tốc độ. Tất cả các truy cập mạng sẽ được thực hiện trong một thread riêng rẽ.
Hãy nhớ kỹ 2 vấn đề này, Code 2 cho thấy một phần Code 1 đã được thay đổi (mã được thêm vào là để play các file video). Vấn đề đầu tiên đã được chăm sóc cẩn thận bởi AMS bên dưới. Nó hỏi thẳng quyền truy cập người dùng một khi truy cập mạng được yêu cầu. Vấn đề thứ hai lại được chăm sóc cẩn thận bằng cách phân tách mã truy cập media mạng trong một thread riêng.
package com.j2me.part4;

import java.util.Hashtable;
import java.util.Enumeration;

import javax.microedition.lcdui.Item;
import javax.microedition.lcdui.List;
import javax.microedition.lcdui.Form;
import javax.microedition.lcdui.Alert;
import javax.microedition.midlet.MIDlet;
import javax.microedition.lcdui.Display;
import javax.microedition.lcdui.Command;
import javax.microedition.lcdui.Displayable;
import javax.microedition.lcdui.CommandListener;

import javax.microedition.media.Player;
import javax.microedition.media.Control;
import javax.microedition.media.Manager;
import javax.microedition.media.PlayerListener;
import javax.microedition.media.control.VideoControl;

public class MediaMIDletV2 extends MIDlet
  implements CommandListener {

  private Display display;
  private List itemList;
  private Form form;

  private Hashtable items;

  public MediaMIDletV2() {

    display = Display.getDisplay(this);

    // creates an item list to let you select multimedia files to play
    itemList = new List("Select an item to play", List.IMPLICIT);

    // a form to display when items are being played
    form = new Form("Playing media");

    // create a hashtable of items
    items = new Hashtable();

    // and populate both of them
    items.put("Siren from web", "http://www.craftbits.com/j2me/siren.wav");
    items.put(
      "Promo Video from web",
      "http://www.craftbits.com/j2me/promo.mpg");
  }

  public void startApp() {

    // when MIDlet is started, use the item list to display elements
    for(Enumeration en = items.keys(); en.hasMoreElements();) {
      itemList.append((String)en.nextElement(), null);
    }

    itemList.setCommandListener(this);

    // show the list when MIDlet is started
    display.setCurrent(itemList);
  }

  public void pauseApp() {
  }

  public void destroyApp(Boolean unconditional) {
  }

  public void commandAction(Command command, Displayable disp) {

    // generic command handler

    // if list is displayed, the user wants to play the item
    if(disp instanceof List) {
      List list = ((List)disp);

      String key = list.getString(list.getSelectedIndex());

      // try and play the selected file
      try {
        playMedia((String)items.get(key));
      } catch (Exception e) {
        System.err.println("Unable to play: " + e);
        e.printStackTrace();
      }
    }

  }

  /* Creates Player and plays media for the first time */
  private void playMedia(String locator) throws Exception {

    PlayerManager manager =
      new PlayerManager(form, itemList, locator, display);
    form.setCommandListener(manager);
    Thread runner = new Thread(manager);
    runner.start();
  }
}

class PlayerManager implements Runnable, CommandListener, PlayerListener {

  Form form;
  List list;
  Player player;
  String locator;
  Display display;

  private Command stopCommand;
  private Command pauseCommand;
  private Command startCommand;

  public PlayerManager(Form form, List list, String locator, Display display) {
  this.form = form;
  this.list = list;
  this.locator = locator;
  this.display = display;

    // stop, pause and restart commands
    stopCommand = new Command("Stop", Command.STOP, 1);
    pauseCommand = new Command("Pause", Command.ITEM, 1);
    startCommand = new Command("Start", Command.ITEM, 1);

    // the form acts as the interface to stop and pause the media
    form.addCommand(stopCommand);
    form.addCommand(pauseCommand);
  }

  public void run() {

    try {
      // since we are loading data over the network, a delay can be
      // expected
      Alert alert = new Alert("Loading. Please wait ....");
      alert.setTimeout(Alert.FOREVER);
      display.setCurrent(alert);

      player = Manager.createPlayer(locator);

      // a listener to handle player events like starting, closing etc
      player.addPlayerListener(this);

      player.setLoopCount(-1); // play indefinitely
      player.prefetch(); // prefetch
      player.realize(); // realize

      player.start(); // and start
    } catch(Exception e) {
      System.err.println(e);
      e.printStackTrace();
    }
  }

  public void commandAction(Command command, Displayable disp) {
    if(disp instanceof Form) {
      // if showing form, means the media is being played
      // and the user is trying to stop or pause the player
      try {
        if(command == stopCommand) { // if stopping the media play
          player.close(); // close the player
          display.setCurrent(list); // redisplay the list of media
          form.removeCommand(startCommand); // remove the start command
          form.removeCommand(pauseCommand); // remove the pause command
          form.removeCommand(stopCommand);  // and the stop command
        } else if(command == pauseCommand) { // if pausing
          player.stop(); // pauses the media, note that it is called stop
          form.removeCommand(pauseCommand); // remove the pause command
          form.addCommand(startCommand); // add the start (restart) command
        } else if(command == startCommand) { // if restarting
          player.start(); // starts from where the last pause was called
          form.removeCommand(startCommand);
          form.addCommand(pauseCommand);
        }
      } catch(Exception e) {
        System.err.println(e);
      }
    }
  }

  /* Handle player events */
  public void playerUpdate(Player player, String event, Object eventData) {

    // if the event is that the player has started, show the form
    // but only if the event data indicates that the event relates to newly
    // stated player, as the STARTED event is fired even if a player is
    // restarted. Note that eventData indicates the time at which the start
    // event is fired.
    if(event.equals(PlayerListener.STARTED) &&
      new Long(0L)Equals((Long)eventData)) {

      // see if we can show a video control, depending on whether the media
      // is a video or not
      VideoControl vc = null;
      if((vc = (VideoControl)player.getControl("VideoControl")) != null) {
        Item videoDisp =
          (Item)vc.initDisplayMode(vc.USE_GUI_PRIMITIVE, null);
        form.append(videoDisp);
      }

      display.setCurrent(form);
    } else if(event.equals(PlayerListener.CLOSED)) {

      form.deleteAll(); // clears the form of any previous controls
    }
  }

}
Code 2 – Truy cập media qua đường mạng trong một thread riêng
Như bạn có thể thấy, tất cả mã tương tác với media đã được di chuyển vào lớp PlayerManager, và chạy trong một thread riêng. Hình 5 cho thấy cách tương tác với MIDlet.
Hình 5 – Quá trình truy cập qua đường mạng
Chú ý cách MIDlet hỏi quyền truy cập người dùng vào mạng trước khi player được tạo ra. Quyền truy cập được cập một lần và có giá trị trong suốt thời gian MIDlet chạy; vì thế, các truy cập mạng sau đó không yêu cầu màn hình hỏi quyền truy cập này nữa.
Như vậy là đến phần cuối của series 1 về J2ME. Tôi chỉ có thể đưa ra một tổng quan ngắn gọn về Mobile Media API và các tập con của nó trong MIDP 2.0 cùng một số ví dụ đơn giản. Có vài thứ khác mà bạn có thể làm thêm với API này, như tạo ra và play các tone – giai điệu, ghi âm hay ghi hình hay radio streaming qua đường mạng. Hãy khám phá thêm tài liệu API và sử dụng các ví dụ đã có trong các bài học để cải thiện các khả năng hoạt động trong MIDlet của bạn. Chúc các bạn thành công.
Dịch từ today.java.net

Bài 3 – Khám phá Game API trong MIDP 2.0



J2ME là một nền tảng phổ biến cho cho việc phát triển game cho các thiết bị không dây. Trong bài học này, tôi sẽ giới thiệu cho bạn gói Game API và giúp bạn phát triển một game đơn giản có sử dụng tất cả các lớp thuộc gói này như là một công cụ học tập. Gói này có tên là javax.microedition.lcdui.game, và nó được xây dựng dựa trên các khái niệm mà bạn đã học.
1.Tổng quan J2ME Game API
Chỉ có 5 class trong gói javax.microedition.lcdui.game: GameCanvas, Layer, Sprite, TiledLayer vàLayerManager. 5 class này đủ để cung cấp một nền tảng hoàn hảo cho việc phát triển các trò chơi.
Lớp Layer là superclass của lớp Sprite và TiledLayer. Lớp Layer trừu tượng hành vi của một thành phần trực quan trong một game. Thành phần này có thể là một sprite – thể hiện một đồ họa độc lập (hay một tập hợp các hoạt hình) có thể di chuyển được quanh màn hình game, hay một tiled layer – thể hiện một đồ họa có thể được dùng để tạo ra nền game rộng chỉ với một ít image. Bạn sử dụng các lớp Layer cho việc định vị và khả năng nhìn thấy các đối tượng. Các subclass ghi đè phương thức paint(Graphics g) có nhiệm vụ render các thành phần lên màn hình.
Lớp LayerManager cung cấp một cơ chế tiện lợi cho quản lý nhiều thành phần trực quan của một game (các sprite và tiled layer) bằng cách render lớp thích hợp theo đúng trình tự thích hợp.
Lớp GameCanvas được tạo ra một cách hữu ích bằng cách mở rộng chức năng lớp Canvas. Lớp GameCanvascung cấp một bộ đệm off-screen, để cho mọi thao tác render được thực hiện trước khi đẩy chúng lên màn hình thiết bị. Lớp GameCanvas cũng cung cấp. Nó cũng cung cấp một cơ chế dễ dùng để truy vấn các phím key hiện hành đang được nhấn bởi người dùng.
Cách tốt nhất để giới thiệu cho bạn những class này là đưa ra môt ví dụ, từ đó ta sẽ hiểu dần dần được mọi khía cạnh của một game.
2.Hướng dẫn vỡ lòng đầu tiên khi xây dựng Game
Một game hay hoạt hình được xây dựng dựa theo nguyên lý việc thực thi có tính chất lặp lại một đoạn mã. Đoạn mã này theo dõi giá trị các biến thể hiện và cập nhật trạng thái game một cách phù hợp. Dựa trên trạng thái game, đoạn mã sau đó draw/paint/repaint màn hình game với các thành phần tạo nên game. Giá trị các biến thể hiện có thể thay đổi bởi vì do tương tác của người dùng hoặc do hành vi bên trong của game.
Việc thực thi có tính lặp lại được tác động bằng cách đặt đoạn mã lặp trong một vòng lặp vô tận. Trước khi vào vòng lặp, một biến thể hiện có thể được kiểm tra kỹ lưỡng nếu game vẫn đang chạy, còn nếu không, vòng lặp có thể bị đẩy ra. Đoạn mã trong vòng lặp sẽ cho phép thread việc thực thi hiện hành ngủ đi một vài giây để kiểm soát tốc độ lúc cập nhật cho các biến thể hiện đã được thực hiện (thực vậy, game screen nhanh như thế nào tùy thuộc vào cách làm tươi).
Trong đoạn mã của bạn phải tuân theo một số điều kiện:
// main class

public MainClass {

   private GameCanvas canvas = new MyGameCanvas();

   public MainClass() {

      // bắt đầu một thread chạy một cách vô tận

      canvas.start();

   }

   // mã của bạn

}

// lớp thực sự làm công việc vẽ - the actual drawing class

public MyGameCanvas extends GameCanvas implements Runnable {

   public MyGameCanvas() {

      // mã khởi tạo - instantiation code

   }

   public void start() {

      // thực hiện việc khởi tạo - do initialization

      // và sau đó bắt đầu một thread - and then start a thread

      Thread runner = new Thread(this);

      runner.start();

   }

   private void run() {

      // or while(keeprunning = true)

      // where keeprunning is an instance variable

      while(true) {

         // checks if the game has reached

         // some boundary states or special conditions

         verifyGameState();

         // gets input from user and

         // updates instance variables

         // that describe the games elements

         checkUserInput();

         // paints elements on screen to reflect

         // the current game state using the current

         // graphics object

         updateGameScreen(getGraphics());

         // control the rate at which screen updates are done

         Thread.sleep(milliseconds);

      }

   }

}
Ta sẽ sử dụng cấu trúc này để phát triển game trong các mục sau.
3.Xây dựng một J2ME Game: Bắt đầu với GameCanvas
Một lớp GameCanvas là một subclass chuyên biệt hóa của lớp Canvas mà bạn đã biết trong bài trước.GameCanvas được tối ưu cho làm game bởi vì nó cung cấp một bộ đệm off-screen đặc biệt phục vụ mọi thao tác vẽ. Khi mọi công tác vẽ trên bộ đệm này hoàn thành, bộ đệm được render trên màn hình thiết bị bằng cách gọi phương thức flushGraphics(). Việc làm đệm kép này tận dụng lợi thế của việc đem lại các chuyển tiếp mượt mà của các thành phần chuyển động trên màn hình. Kích thước của bộ đệm bằng với kích thước của màn hình thiết bị, và chỉ có một bộ đệm trên mỗi thể hiện GameCanvas.
Lớp GameCanvas cung cấp một cơ chế lưu trữ trạng thái các phím game, đó là một phương pháp có ích để lấy vế tương tác người dùng trong game. Cơ chế này cung cấp một cách đơn giản để theo dõi số lần người dùng đã nhấn một phím cụ thể nào đó. Việc gọi phương thức getKeyStates() trả về một dãy bit thể hiện tất cả các phím game vật lý, cụ thể là 1 cho các phím đã nhấn và 0 là cho các phím chưa được nhấn từ lần cuối cùng phương thức này được gọi. Chỉ những trạng thái game sau là được nhận diện, và được định nghĩa bởi lớp Canvas:DOWN_PRESSED, UP_PRESSED, RIGHT_PRESSED, LEFT_PRESSED, FIRE_PRESSED, GAME_A_PRESSED, GAME_B_PRESSED, GAME_C_PRESSED,  GAME_D_PRESSED.
Hãy cùng tạo một game bằng cách kế thừa lớp GameCanvas. Code 1 cho thấy nỗ lực đầu tiên này, còn Code 2 cho thấy MIDlet sẽ được sử dụng để chạy các ví dụ.
package com.j2me.part3;

import javax.microedition.lcdui.Image;

import javax.microedition.lcdui.Graphics;

import javax.microedition.lcdui.game.GameCanvas;

import java.io.IOException;

public class MyGameCanva extends GameCanvas implements Runnable {

   public MyGameCanvas() {

      super(true);

   }

   public void start() {

      try {

         // create and load the couple image

         // and then center it on screen when

         // the MIDlet starts

         coupleImg = Image.createImage("/couple.gif");

         coupleX = CENTER_X;

         coupleY = CENTER_Y;

      } catch(IOException ioex) { System.err.println(ioex); }

         Thread runner = new Thread(this);

         runner.start();

   }

   public void run() {

      // the graphics object for this canvas

      Graphics g = getGraphics();

      while(true) { // vòng lặp vô tận

         // based on the structure

         // first verify game state

         verifyGameState();

         // check user's input

         checkUserInput();

         // update screen

         updateGameScreen(getGraphics());

         // and sleep, this controls

         // how fast refresh is done

         try {

            Thread.currentThread().sleep(30);

         } catch(Exception e) {}

     }

   }

   private void verifyGameState() {

      // không làm gì cả

   }

   private void checkUserInput() {

      // lấy trạng thái các phím

      int keyState = getKeyStates();

      // tính toàn vị trí của trục x

      calculateCoupleX(keyState);

   }

   private void updateGameScreen(Graphics g) {

      // the next two lines clear the background

      g.setColor(0xffffff);

      g.fillRect(0, 0, getWidth(), getHeight());

      // draws the couple image according to current

      // desired positions

      g.drawImage(

         coupleImg, coupleX,

         coupleY, Graphics.HCENTER | Graphics.BOTTOM);

      // đẩy bộ đệm ra màn hình

      flushGraphics();

   }

   private void calculateCoupleX(int keyState) {

      // determines which way to move and changes the

      // x coordinate accordingly

      if((keyState & LEFT_PRESSED) != 0) {

         coupleX -= dx;

      }

      else if((keyState & RIGHT_PRESSED) != 0) {

         coupleX += dx;

     }

   }

   // the couple image

   private Image coupleImg;

   // the couple image coordinates

   private int coupleX;

   private int coupleY;

   // the distance to move in the x axis

   private int dx = 1;

   // the center of the screen

   public final int CENTER_X = getWidth()/2;

   public final int CENTER_Y = getHeight()/2;

}
Code 1 – MyGameCanvas: Xây dựng một game canvas
Code 2 cho thấy MIDlet sẽ sử dụng game canvas này:
package com.j2me.part3;

import javax.microedition.midlet.MIDlet;

import javax.microedition.lcdui.Display;

public class GameMIDlet extends MIDlet {

   MyGameCanvas gCanvas;

   public GameMIDlet() {

      gCanvas = new MyGameCanvas();

   }

   public void startApp() {

      Display display = Display.getDisplay(this);

      gCanvas.start();

      display.setCurrent(gCanvas);

   }

   public void pauseApp() {

   }

   public void destroyApp(boolean unconditional) {

   }

}
Code 2 – Lớp MIDlet thử nghiệm
Sử dụng cả hai lớp này, tạo ra một dự án với Toolkit của bạn và sau đó build và chạy ứng dụng. Bạn sẽ cần file ảnh này: , tên là couple.gif, trong thư mục res của MIDlet, hoặc bạn có thể sử dụng một ảnh có kích thước tương tự. Hình 1 cho thấy kết xuất mong muốn.
Hình 1 – Sử dụng GameCanvas
Hình ảnh ở giữa màn hình có thể được di chuyển qua trái hay phải bằng các phím game trái phải tương ứng. Trong đoạn mã Code 1, điều này có thể thực hiện được bằng cách truy vấn trạng thái game trong phương thứccheckUserInput() và sau đó gọi phương thức calculateCoupleX() với trạng thái game này. Như bạn có thể thấy, theo phép so sánh bit OR trạng thái với các hằng được cung cấp trong lớp GameCanvas, bạn có thể dễ dàng quyết định người dùng đã nhấn phím nào và hành động tương ứng. Vị trí tọa độ x của ảnh được di chuyển qua trái hay phải vị trí hiện hành bằng cách cộng thêm hay trừ đi delta x (dx) từ vị trí hiện hành.
Ảnh được render lên màn hình trong phương thức updateGameScreen(). Phương thức này được chuyển cho đối tượng Graphics hiện hành. Đối tượng này được tạo ra cho bạn bởi lớp GameCanvas, và chỉ có một đối tượng như thế trên GameCanvas. Phương thức xóa bộ đệm graphic của mọi render trước đó, vẽ cặp ảnh dựa trên biếncoupleX hiện hành (và cả biến coupleY không thay đổi) và sau đó đẩy bộ đệm này lên màn hình thiết bị.
Vòng lặp vô hạn trong phương thức run() theo cấu trúc game tôi đã mô tả ở trên. Vòng lặp này ngủ 30ms trước khi vào vòng lặp khác nhằm xác định user input và làm tươi bộ đệm. Bạn có thể thử nghiệm giá trị này làm chậm hay tăng tốc tỉ lệ làm tươi.
Cuối cùng, chú ý rằng phương thức dựng MyGameCanvas gọi bộ dựng của lớp con GameCanvas với một giá trị tham số là true. Điều này cho biết rằng cơ chế sự kiện phím thông thường, được kế thừa từ lớp Canvas, sẽ bị chặn, vì mã này không yêu cầu các thông báo. Trạng thái được xử lý một cách thỏa đáng bởi thông tin trạng thái phím, được lấy về từ phương thức getKeyStates(). Bằng cách khử cơ chế thông báo cho “phím đã ấn”, “phím đã nhả ra”, và “phím lặp lại”, hiệu suất game được cải thiện.
4.Định rõ các đặc điểm của game
Những gì bạn làm trong một game là phải di chuyển nhân vật chính cứ qua trái hay phải thì chả có gì là thú vị cả. Hãy thay đổi một vài thứ trong thân phát triển game trong Code 1 để cải thiện game tốt hơn một chút. Để bắt đầu, hãy chỉ rõ một giới hạn trong game của bạn. Đó là yếu tố cần thiết để thực hiện, bởi vì nó làm cho game của bạn có kích thước phù hợp qua các thiết bị khác nhau. Để làm điều này, bắt đầu bằng việc định nghĩa một số hằng như trong đoạn mã sau:
// the game boundary

public static final int GAME_WIDTH = 160;

public static final int GAME_HEIGHT = 160;

// the shifted x,y origin of the game

public final int GAME_ORIGIN_X = (getWidth() - GAME_WIDTH)/2;

public final int GAME_ORIGIN_Y = (getHeight() - GAME_HEIGHT)/2;

// the height of sections below and above the couple

public final int SECTION_HEIGHT = 64;

// the base on which the couple will move

public final int BASE = GAME_ORIGIN_Y + GAME_HEIGHT - SECTION_HEIGHT;

// the max height the couples can jump

public final int MAX_HEIGHT = 32;
(Chý ý rằng tôi đã giới thiệu một đặc điểm game cho biết hai nhân vật người có thể nhảy trên màn hình, với hỗ trợ của hằng MAX_HEIGHT). Trên màn hình, những hằng này giúp định nghĩa các giới hạn của game và các thành phần duy nhất của nó (hai nhân vật người), như minh họa trong hình sau:
Hình 2 – Định nghĩa các ranh giới bằng cách sử dụng các hằng game
Dĩ nhiên, bây giờ bạn cần thay đổi phần còn lại của code để sử dụng được những hằng này. Thêm một phương thức mới vào Code 1 tên là buildGameScreen(Graphics g), như sau:
private void buildGameScreen(Graphics g) {

   // set the drawing color to black

   g.setColor(0x000000);

   // draw the surrounding rectangle

   g.drawRect(GAME_ORIGIN_X, GAME_ORIGIN_Y, GAME_WIDTH, GAME_HEIGHT);

   // draw the base line

   g.drawLine(GAME_ORIGIN_X, BASE, GAME_ORIGIN_X + GAME_WIDTH, BASE);

  // draw the maximum line to where the couple can jump to

   g.drawLine(GAME_ORIGIN_X, BASE - MAX_HEIGHT,

      GAME_ORIGIN_X + GAME_WIDTH, BASE - MAX_HEIGHT);

}
Ta cũng thêm một lời gọi đến phương thức này trong phương thức updateGameScreen(), trước khi ảnh hai nhân vật người được vẽ ra. Các ranh giới game đã được định nghĩa và chỉ còn một việc còn lại phải làm là chuẩn bị vị trí bắt đầu cho ảnh hai nhân vật người là BASE và not CENTER_Y. Thay đổi việc này trong phương thức start()bằng cách thiết lập coupleY = BASE;.
Ảnh hai nhân vật người có thể di chuyển qua trái hay phải với các phím game trái hay phải, nhưng bây giờ chúng ta phải đảm bảo rằng không di chuyển qua ranh giới game. Đây là một vấn đề tồn tại trong Code 1, nhưng trong trường hợp đó, đơn giản là hình ảnh biến mất khỏi màn hình, và ranh giới là cạnh của màn hình. Có vẻ rất là kỳ cục nếu ảnh đi qua các ranh giới. Do đó, việc thay đổi các hành động nhấn phím trái và phải trong phương thứccalculateCoupleX() giới hạn việc di chuyển quá các ranh giới. Phương thức được thay đổi như sau:
private void calculateCoupleX(int keyState) {

   // determines which way to move and changes the

   // x coordinate accordingly

   if((keyState & LEFT_PRESSED) != 0) {

      coupleX =

         Math.max(

         GAME_ORIGIN_X + coupleImg.getWidth()/2,

         coupleX - dx);

   } else if((keyState & RIGHT_PRESSED) != 0) {

      coupleX =

         Math.min(

         GAME_ORIGIN_X + GAME_WIDTH - coupleImg.getWidth()/2,

          coupleX + dx);;

   }

}
Bây giờ phương thức này sử dụng các phương thức Math.max() mà Math.min() để giới hạn ảnh hai nhân vật người trong phạm vi các ranh giới game. Chú ý rằng việc đó cũng kết hợp chặt chẽ với độ rộng của ảnh trong các tính toán này.
Như đã nói trước đây về việc làm cho ảnh hai nhân vật người nhảy quanh màn hình. Hãy xem cách thực hiện việc này bằng cách thêm một phương thức để di chuyển ảnh theo trục Y, một cách độc lập với người chơi game.
Thêm 3 biến thể hiện mới vào Code 1, gọi là upjumpHeight, và random, như sau:
// a flag to indicate which direction the couple are moving

private Boolean up = true;

// indicates the random jump height, calculated for every jump

private int jumpHeight = MAX_HEIGHT;

// random number generator

public Random random = new Random();
Như bạn có thể thấy, jumpHeight được khởi tạo là MAX_HEIGHT. Biến jumpHeight sẽ được tính toán cho mỗi lần nhảy của hai nhân vật người đó và nó sẽ được thiết lập là một giát trị ngẫu nhiên mỗi lúc. Điều này được thể hiện trong phương thức calculateCoupleY() sau đây:
private void calculateCoupleY(int keyState) {

   // check if the couple were on the way up

   if(up) {

      // if yes, see if they have reached the current jump height

      if((coupleY > (BASE - jumpHeight + coupleImg.getHeight()))) {

         // if not, continue moving them up

         coupleY -= dy;

      } else if(coupleY == (BASE - jumpHeight + coupleImg.getHeight())) {

         // if yes, start moving them down

         coupleY += dy;

         // and change the flag

         up = false;

      }

   } else {

       // the couple are on their way down, have they reached base?

      if(coupleY < BASE) {

          // no, so keep moving them down

          coupleY += dy;

      } else if(coupleY == BASE) {

          // have reached base, so calculate a new

          // jump height which is not more than MAX_HEIGHT

          int hyper = random.nextInt(MAX_HEIGHT + 1);

          // but make sure that this it is atleast greater than the image height

          if(hyper > coupleImg.getHeight()) jumpHeight = hyper;

          // move the image up

          coupleY -= dy;

          // and reset the flag

          up = true;

      }

   }

}
Chú ý rằng vì phương thức này không phụ thuộc vào việc người dùng nhấn các phím lên hay xuống, nó không sử dụng thông tin keyState. Nhưng dù sao giá trị này đã được chuyển cho nó, để duy trì sự phù hợp với phương thức calculateCoupleX(). Phương thức này bắt đầu di chuyển ảnh hai nhân vật người bằng cách thay đổi biếncoupleY theo hướng đi lên cho đến khi chạm tới độ cao bước nhảy hiện hành (là MAX_HEIGHT lúc bắt đầu). Một khi phương thức chạm tới độ cao bước nhảy này, nó bắt đầu di chuyển hướng ngược lại cho đến khi nó chạm tớiBASE. Tại vị trí này, một giá trị độ cao bước nhảy mới, giữa MAX_HEIGHT và độ cao ảnh hai nhân vật người, được tính toán một cách ngẫu nhiên và hai nhân vật người lại tiếp tục nhảy.
Toàn bộ tác động có thể được thực hiện bằng cách ấn phím trái hay phải từ phía người dùng. Một snapshot minh họa:
Hình 3 – Một game snapshot
5.Xây dựng một J2ME Game: Tạo các nền sau sử dụng lớp TiledLayer
Trong mục này, bạn sẽ thêm một số màu sắc vào game bằng cách tạo ra một nền sau (background) sử dụng lớpTiledLayer. Game được chia ra thành 3 phần: phần đầu có thể tưởng tượng là bầu trời, phần giữa là hình hai nhân vật người nhảy trên mặt đất, và phần dưới là biển. Ba phần này có thể được thiết kế dễ dàng bằng việc sử dụng 3 ảnh kích thước 32×32 pixel, mỗi ảnh cho mỗi phần. Tuy nhiên, mỗi vùng thường lớn hơn 32×32 pixel, và lớp TiledLayer được dùng để định nghĩa các vùng rộng như thế với các bức ảnh nhỏ.
Để bắt đầu, chia màn hình game thành các ô vuông 32×32 px và đánh số các hàng và cột, bắt đầu từ chỉ số index là 0. Hình sau cho thấy một nền sau 5×5 ô:
Hình 4 – Chia màn hình game thành các ô riêng biệt
Vì vậy, các ô (0, 0) đến (1, 4) được vẽ bằng ảnh bầu trời; các ô (2, 0) đến (2, 4) vẽ mặt đất, và các ô (3, 0) đến (4, 4) vẽ biển. Bạn sẽ làm việc này với các ảnh sau:
Hình 5 – Các ảnh nền sau
Ô 32×32 px đầu tiên thể hiện ảnh mặt đất, ô thứ hai thể hiện biển, và ô thứ 3 thể hiện bầu trời. Khi bạn sử dụng lớp TiledLayer, ba ảnh được đánh số từ index 1 (không phải 0; vì thế, mặt đất là 1, biển là 2, và bầu trời là 3). Lớp TiledLayer sẽ sử dụng một bức ảnh này và chia nó ra thành 3 ảnh riêng biệt dùng cho việc render nền sau của game. Trong trường hợp của chúng ta, ta muốn TiledLayerrender một ô nền sau 5×5 bằng việc sử dụng các ô 32×32 px. Ta làm điều đó như sau:
// load the image

backgroundImg = Image.createImage("/tiledLayer1.gif");

// create the tiledlayer background

background = new TiledLayer(5, 5, backgroundImg, 32, 32);
Như bạn có thể thấy, hai tham số đầu tiên cho phương thức dựng TiledLayer thể hiện kích thước tổng của nền sau, tham số tiếp theo là ảnh bạn dùng để cắt, và hai tham số cuối cùng thể hiện kích thước cho mỗi ô. Kích thước này sẽ được sử dụng bởi lớp TiledLayer để cắt ảnh thành các ô nền sau độc lập.
Việc còn lại bây giờ là đặt vào mỗi ô với ảnh tương ứng của nó. Mã thực hiện tạo nền sau được cho bên dưới trong một phương thức tên là createBackground(). Bạn sẽ cần thêm một lời gọi đến phương thức này từ phương thức start() thuộc lớp MyGameCanvas. Một khi thực hiện xong, thêm một lời gọi để vẽ background này sử dụng background.paint(g) ở cuối phương thức buildGameScreen().
// creates the background using TiledLayer

private void createBackground() throws IOException {

   // load the image

   backgroundImg = Image.createImage("/tiledlayer1.gif");

   // create the tiledlayer background

   background = new TiledLayer(5, 5, backgroundImg, 32, 32);

   // array that specifies what image goes where

   int[] cells = {

      3, 3, 3, 3, 3, // sky

      3, 3, 3, 3, 3, // sky

      1, 1, 1, 1, 1, // earth

      2, 2, 2, 2, 2, // sea

      2, 2, 2, 2, 2  // sea

   };

   // set the background with the images

   for (int i = 0; i < cells.length; i++) {

      int column = i % 5;

      int row = (i - column)/5;

      background.setCell(column, row, cells[i]);

   }

   // set the location of the background

   background.setPosition(GAME_ORIGIN_X, GAME_ORIGIN_Y);

}
Kết quả cuối cùng như sau:
Hình 6 – Game với background đã được thêm vào
6.Xây dựng một J2ME Game: Sprite và lớp LayerManager
Cho đến lúc này tôi đã sử dụng một ảnh hình hai nhân vật người để cho bạn thấy một thành phần điển hình của game. Thành phần này được render hiện hành như là một ảnh; nhưng ta có cảm giác như nó được render ra một hình sprite. Một sprite là một thuật ngữ theo cách nói của game nói đến bất kỳ thành phần trực quan nào, thường là một ảnh hoạt hình, có thể được di chuyển quanh màn hình một cách độc lập với các thành phần khác. Lớp Sprite được dùng để thể hiện các sprite trên các game MIDP 2.0 API. Lớp này cung cấp các phương thức để làm hoạt hình sprite dựa trên một số ảnh, tương tự với cách các background được tạo ra khi sử dụng lớpTiledLayer. Quan trọng hơn, nó cung cấp các phương thức kiểm tra các va chạm với các thành phần game khác, bao gồm các ảnh, các sprite hay các lớp tiledlayer.
Hãy cùng bắt đầu bằng cách chuyển đổi đoạn mã có ảnh hình hai nhân vật người có sẵn thành một sprite. Để thấy được hoạt hình, tôi sẽ sử dụng ảnh trong Hình 7, là ảnh hai nhân vật người được sao chép với một màu khác thành 2 frame khác nhau, mỗi frame 10×10 px.
Hình 7 – Các frame cho hoạt hình Sprite
Tương tự với lớp TiledLayer, lớp Sprite yêu cầu kích thước cho mỗi frame, được chuyển vào phương thức khởi tạo của nó:
coupleSprite = new Sprite(coupleImg, 10, 10);
Đoạn mã này được thêm vào sau đoạn mã tạo ra ảnh hai nhân vật người, tạo ra một sprite hai nhân vật người với 2 frame 10x10px, được đánh số từ 0. Vì vậy, để luân phiên giữa các ảnh sprite, bạn có thể gọi phương thứcnextFrame(), phương thức này lấy bức ảnh tiếp theo đúng tuần tự hiện hành. Vì chỉ có 2 ảnh trong tuần tự sprite này, cái này sẽ được xuất hiện sau mỗi cái trước. Nếu bạn muốn tạo một sprite có tuần tự frame dài hơn, bạn có thể sử dụng phương thức setFrame(int sequenceNo). Trong trường hợp này, thêmcoupleSprite.nextFrame() trong phương thức updateGameScreen().
Bây giờ bạn không muốn ảnh hai nhân vật người được vẽ ra trên màn hình. Trước khi sprite hai nhân vật người có thể được vẽ ra trên màn hình, bạn cần định nghĩa một điểm pixel tham chiếu đến nó. Hãy nghĩ rằng đó là một điểm bắt đầu – điểm gốc, mà mọi thao tác vẽ được thực hiện. Theo mặc định, sprite được vẽ ra có góc trên bên trái đóng vai trò là điểm bắt đầu của nó. Tương tự với cách bạn thiết lập tham chiếu đến ảnh hai nhân vật người khi sử dụng mã Graphics.HCENTER | Graphics.BOTTOM, bạn cần định nghĩa một điểm pixel tham chiếu cho sprite, như sau:
coupleSprite.defineReferencePixel(coupleSprite.getWidth()/2, coupleSprite.getHeight());
Thêm dòng mã này vào sau mã tạo ra sprite như đã nói ở trước. Bây giờ, thay vì định vị sprite dựa trên điểm gốc, bạn sẽ định vị nó dựa trên pixel tham chiếu này, như sau:
coupleSprite.setRefPixelPosition(coupleX, coupleY);

coupleSprite.paint(g);
Dòng cuối cùng của đoạn mã trên vẽ ra sprite trên đối tượng đồ họa được chuyển cho nó (g). Như mong đợi, bạn sẽ cần chèn hai dòng mã này vào phương thức updateGameScreen() thay thế vào dòng mã vẽ hình hai nhân vật người. Kết quả cuối cùng sẽ gần giống với kết quả trước, ngoại trừ hai nhân vật người nhảy trên màn hình sẽ được thay thế với hai nhân vật người nhấp nháy lung linh!
Trước khi tiếp tục, hãy đảm bảo rằng tất cả những thay đổi của bạn cho biến coupleImg vào coupleSprite trong các phương thức calculateX() và calculateY().
+ Quản lý các Layer sử dụng lớp LayerManager
Nhớ lại cả hai lớp Sprite và TiledLayer kế thừa từ lớp Layer. Một lớp chỉ có thể chứa ít nhất một lớp TiledLayervà vài lớp Sprite. Khi có quá nhiều layer để kiểm soát, lớp LayerManager xuất hiện lúc cần thiết. Lớp này cung cấp các phương thức thêm, xóa, hay chèn các layer trong một game, và cũng cung cấp một phương thức đơn để vẽ tất cả những layer này vào đối tượng Graphics bên dưới. Điều này có nghĩa là bạn không cần gọi phương thứcpaint() một cách độc lập cho mỗi layer của một game.
Một thể hiện của lớp LayerManager được tạo ra sử dụng bộ khởi tạo không đối số của nó. Các layer sau đó được thêm vào, loại bỏ đi, hoặc chèn vào bằng các sử dụng phương thức append(Layer layer), remove(Layer layer), và insert(Layer layer, int index). Thứ tự mà các layer được thêm vào rất quan trọng, bởi vì thứ tự này quyết định layer nào được vẽ ra đầu tiên, và thứ tự này theo thứ tự trục z-order. Layer ở index 0 được vẽ ra trên hết các layer khác, và từ đó, layer này là nằm gần người dùng nhất, và cứ tiếp tục như thế.
Trong game của chúng ta, phương thức start() bây giờ cần được thay đổi, như sau:
// creates the layermanager

manager = new LayerManager();

// and adds layers to it

manager.append(coupleSprite);

// creates the game background

createBackground();

manager.append(background);
Như bạn có thể thấy, coupleSprite sẽ là layer gần người dùng nhất và layer background sẽ là layer xa người dùng nhất, dựa theo chỉ số của chúng. Phương thức buildGameScreen() bây giờ không cần vẽ background (vì bây giờ LayerManager sẽ vẽ background), và vì thế dòng lệnh background.paint(g) cần được loại bỏ từ phương thức này. Cuối cùng, như trong mục trước, bạn đã dùng coupleSprite để vẽ ra màn hình thay vìcoupleImage. Bây giờ dù điều đó không được yêu cầu, vì LayerManager sẽ làm điều đó cho bạn. Loại bỏcoupleSprite.paint(g) ra khỏi phương thức updateGameScreen() và thay thế nó với manager.paint(g, 0, 0). Như bạn có thể thấy, tất cả các lời gọi đến các phương thức paint() của layer riêng rẽ đều được thay thế với một lời gọi đơn vào phương thức paint() của LayerManager. Hai tham số cuối cùng thể hiện vị trí mà manage sẽ vẽ. Khi đó background và carSprite chịu trách nhiệm cho vị trí của riêng chúng, bạn có thể bỏ qua những tham số này nếu có (tức là, vẽ từ vị trí gốc thiết bị).
Code 3 cho thấy phương thức updateGameScreen() đã được sửa lại. Các dòng bị loại bỏ vẫn được giữ lại trong các comment để dễ dàng nhận thấy các thay đổi:
private void updateGameScreen(Graphics g) {

   // the next two lines clear the background

   g.setColor(0xffffff);

   g.fillRect(0, 0, getWidth(), getHeight());

   // creates the game borders

   buildGameScreen(g);

   // draws the couple image according to current

   // desired positions

   /*g.drawImage(

      coupleImg, coupleX,

      coupleY, Graphics.HCENTER | Graphics.BOTTOM);*/

   // animates the sprite

   coupleSprite.nextFrame();

   // moves the sprite based on its reference pixel

   coupleSprite.setRefPixelPosition(coupleX, coupleY);

   // paints it on the buffer

   // coupleSprite.paint(g);

   // the manager paints all the layers

   manager.paint(g, 0, 0);

   // this call paints off screen buffer to screen

   flushGraphics();

}
Code 3 – Cập nhật lại phương thức updateGameScreen()
7.Thêm nhiều Sprite hơn nữa và nhận diện va chạm
Một sprite mà chỉ có nhảy qua nhảy lại thì đâu có vui phải không các bạn? Đã đến lúc thêm một sprite khác, ví dụ như là một chiếc xe xuất hiện vài nơi trên màn hình. Hai nhân vật người cần va chạm vào chiếc xe này! Càng nhiều xe bị hai người này chạm vào từng lượt, điểm số càng cao hơn.
Với các mục tiêu game đã rõ ràng, trước tiên hãy tạo một class sẽ theo dõi thời gian để game có thể dừng lại một khi hết thời gian. Code 4 cho thấy mã cho lớp Clock:
package com.j2me.part3;

import java.util.TimerTask;

public class Clock extends TimerTask {

   int timeLeft;

   public Clock(int maxTime) {

      timeLeft = maxTime;

   }

   public void run() {

      timeLeft--;

   }

   public int getTimeLeft() { return this.timeLeft; }

}
Code 4 – Lớp Clock sẽ theo dõi thời gian game
Lớp Clock kế thừa lớp TimerTask, có phương thức run() được thực hiện sau mỗi khoảng thời gian định trước. Ở đây, phương thức này giảm bớt biến maxTime qua mỗi giây đồng hồ, nhằm giúp chúng ta theo dõi thời gian. Để sử dụng lớp Clock, tạo ra và khởi động nó trước vòng lặp vô tận bên trong phương thức run() của lớpMyGameCanvas khi được thực thi, như sau:
// before going in the loop, start the timer clock with a

// 30 seconds countdown

clock = new Clock(30);

new Timer().schedule(clock, 0, 1000);
Dĩ nhiên, bây giờ vòng lặp vô tận phải được kiểm soát với một flag nhằm dừng vòng lặp khi đang chạy khi hết thời gian. Để làm điều này, định nghĩa một flag có tên là stop, như dưới đây:
// the flag that tells the game to stop

private Boolean stop = false;
Đưa nó vào vòng lặp while với while(!stop) và nhập các dòng mã đầu tiên trong phương thứcverifyGameState():
private void verifyGameState() {

   if(clock.getTimeLeft() == 0) {

      stop = true;

      return;

   }

}
Cuối cùng, người dùng cần được biết thời gian còn lại trong game. Để làm điều này, thêm một phương thức tên làshowTimeLeft(Graphics g) như dưới đây:
private void showTimeLeft(Graphics g) {

   // what does the clock say

   int timeLeft = clock.getTimeLeft();

   // if less than 6 seconds left

   // flicker time with red and black

   if(timeLeft < 6) {

      if((timeLeft % 2) == 0)

         g.setColor(0xff0000);

      else

         g.setColor(0x000000);

   }

   // draw the time left string

   g.drawString("Time Left: " + timeLeft + " seconds", 0, 0, 0);

   // reset the color

   g.setColor(0x000000);

}
Phương thức này được gọi ở cuối phương thức buildGameSreen(). Hình 8 cho thấy một snapshot của game:
Hình 8 – Game có thông báo thời gian còn lại
Giờ là lúc thêm một vài sprite mới vào trong game. Code 5 là đoạn mã cho chiếc xe trong một class tên làCarSprite. Đoạn mã này sử dụng bức ảnh một chiếc xe trong hình 9.
Hình 9 – Hình ảnh một car sprite
package com.j2me.part3;

import java.util.Random;

import javax.microedition.lcdui.Image;

import javax.microedition.lcdui.game.Sprite;

import javax.microedition.lcdui.game.LayerManager;

public class CarSprite implements Runnable {

   public CarSprite(MyGameCanvas parent) {

      this.parent = parent;

      this.manager = parent.getManager()
   }

   public void start() {

      // first load the car image

      try {

         carImage = Image.createImage("/car.gif");

      } catch(Exception e) { System.err.println(e); return; }

      // next start the thread that will display cars

      // are random locations

      runner = new Thread(this);

      runner.start()
   }

   public void run() {

      try {

         while(true) {

            // create a random car

            randomCar();

            // wait before creating another one

            Thread.currentThread()Sleep(500);

         }

      } catch(Exception e) { System.err.println(e); }

   }

   // creates and displays a car at a random location

   private void randomCar() {

      // if maximum cars are being shown return

      if(currentCars == MAX_CARS) return;

      // create a new car sprite

      carSprite = new Sprite(carImage, 10, 10);

      // generate the random places where cars may appear

      int randomCarX = parent.getRandom().nextInt(parent.GAME_WIDTH);

      int randomCarY =

         (parent.BASE -

         parent.getRandom().nextInt(parent.MAX_HEIGHT + 1) -

         carSprite.getHeight());

      // make sure that these places are within bounds

      if(randomCarX < parent.GAME_ORIGIN_X) 
         randomCarX = parent.CENTER_X;

      if(randomCarY < (parent.BASE - parent.MAX_HEIGHT)
         randomCarY = parent.CENTER_Y;

      // set this newly created car sprite in its random position

      carSprite.setPosition(randomCarX, randomCarY);

      // add it to the manager at index 0

      manager.insert(carSprite, 0);

      // increase the no of cars created

      currentCars++;

   }

   public void checkForCollision() {

      // if there are no cars being shown (only background and couple)

      if(manager.getSize() == 2) return;

      // iterate through the layers, remember don't worry about

      // the last two because they are the couple and background

      for(int i = 0; i < (manager.getSize() - 2); i++) {

         // if collision occurs

         if(parent.getCoupleSprite().collidesWith(

            (Sprite)manager.getLayerAt(i), true)) {

            // remove the offending car

            manager.remove(manager.getLayerAt(i));

            // reduce the no of cars on display

            currentCars--;

            // and increase the no of cars hit

            carsHit++;

         }

      }

   }

   // the no of cars hit

   public int getCarsHit() {

      return carsHit;

   }

   // the car sprite

   private Sprite carSprite;

   // the car image

   private Image carImage;

   // the no of current cars in display

   private int currentCars;

   // the parent canvas

   private MyGameCanvas parent;

   // the parent canvas's layer manager

   private LayerManager manager;

   // runner

   private Thread runner;

   // tracks the no of cars hit

   private int carsHit;

   // the maximum no of cars to create

   private static final int MAX_CARS = 20;

}
Code 5 – Đoạn mã tạo thêm vài car sprite
Lớp CarSprite cài đặt giao diện Runnable, để sinh ra vài car sprite mới cứ mỗi nửa giây. Phương thức run() gọi phương thức randomCar() sau khi chờ 500ms. Phương thức randomCar() kiểm tra nếu số car sprite đang có không được vượt quá giới hạn, sau đó tạo ra một sprite mới sử dụng hình ảnh được nạp lên trước đó. Sau đó nó tính toán một vị trí ngẫu nhiên cho sprite này xuất hiện, và đảm bảo rằng vị trí ngẫu nhiên này ở trong phạm vi của game. Nó còn thiết lập sprite vào LayerManager ở mức index là 0, vì thế làm cho sprite đó gần với người dùng nhất.
Lớp này cũng cung cấp một phương thức để kiểm tra va chạm của hai nhân vật người với các chiếc xe ngẫu nhiên. Phương thức checkForCollision() lặp qua các car sprite đang được trình bày bởi LayerManager, và sử dụng phương thức collidesWidth() trong lớp Sprite để kiểm tra va chạm. Phương thức này trả về true khi va chạm xảy ra, và chấp nhận một layer, một hình ảnh, hoặc một thứ khác khi có va chạm. Phương thức này cũng chấp nhận một flag cho biết nếu có nhận ra va chạm thì sẽ chú ý tới các pixel trong suốt quanh một hình ảnh, hay chỉ cho các pixel mờ. Khi một va chạm được nhận diện, số car bị chạm vào được tăng lên và số car thấy bị giảm đi.
Để sử dụng lớp CarSprite, chèn các dòng mã sau vào cuối phương thức start() trong lớp MyGameCanvas.
// create the car sprite thread and start it

carSprite = new CarSprite(this);

carSprite.start();
Và cũng thêm dòng mã sau vào cuối phương thức verifyGameState():
carSprite.checkForCollision();
Như thế, thread CarSprite bắt đầu đẻ ra nhiều car mới, với tối đa là MAX_CARS chiếc xe. Một khi người dùng chạm vào một chiếc xe bằng cách di chuyển hai nhân vật người đến chiếc xe đó, thì chiếc xe biến mất. Điều này được kiểm tra trong phương thức verifyGameState() bằng cách gọi phương thức checkForCollision() trên thread CarSprite. Nhiều chiếc xe hơn vẫn cứ xuất hiện trong khi thời gian hết dần. Hình 10 cho thấy một game đang trong quá trình như vậy.
Hình 10 – Một game điển hình đang chạy sau khi thêm một số sprite
Những gì còn lại bây giờ là thông báo cho người dùng số chiếc xe mà họ đã phá được. Sau khi vòng lặp while() thoát ra, thêm một lời gọi vào một phương thức mới tên là showGameScore(getGraphics()) như dưới đây:
// at the end of the game show the score

private void showGameScore(Graphics g) {

   // create a base rectangle

   g.setColor(0xffffff);

   g.fillRect(0, CENTER_Y - 20, getWidth(), 40);

   g.setColor(0x000000);

   // and show the score

   g.drawString("You hit " +

      carSprite.getCarsHit() + " cars.",

      CENTER_X, CENTER_Y,

      Graphics.HCENTER | Graphics.BASELINE);

   flushGraphics();

}
Phương thức này vẽ một hình chữ nhật nhỏ ở giữa màn hình lúc kết thúc game, hiện ra số xe đã bị phá bởi người chơi. Một game điển hình kết thúc như sau:
Hình 11 – Một kết thúc điển hình và thông điệp hiển thị cuối cùng
Dĩ nhiên bạn có thể hiển thị thông tin này theo định dạng và vị trí bất kỳ mà bạn muốn.
8.Kết luận
Tuy bài 3 này hơi dài, nhưng bạn đã học được cách sử dụng các lớp MIDP 2.0 API với một ví dụ tương đối tổng quan nhất và game đã chạy thành công. Bạn cũng học được những cơ bản về quá trình xây dựng game qua ví dụ đó.
Trong phần tiếp theo của series này, bạn sẽ học cách thêm multimedia vào MIDlet của bạn,thậm chí một số thứ có thể hữu ích trong J2ME game. Bạn cũng sẽ được học cách sử dụng record-store management API để lưu trữ thông tin vào thiết bị.
Dịch từ today.java.net