Ở 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++){
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

Các bài liên quan:
Phát triển game đơn giãn trên Mobile(P1)
Phát triển game đơn giãn trên Mobile(P2)
Phát triển game đơn giãn trên Mobile(P4)
Phát triển game đơn giãn trên Mobile(P5)
Phát triển game đơn giãn trên Mobile(P6)
Phát triển game đơn giãn trên Mobile(P7)