Chủ Nhật, 2 tháng 9, 2012

Dùng lớp TiledLayer hiển thị Tile Map

Một tile map là map game hay quang cảnh làm nền để nhân vật đi lên trên. Tile map có trong game như Diablo, Fallout, Command and Conquer, StarCraft, Final Fantasy, Pokemon, và hầu hết các game RPG/MMORPG và game chiến thuật 2D. Sau đây là ví dụ trong sử dụng Tile map:

Herbarrio Screenshot
Screenshot of the game Herbarrio

Gọi là tile map bởi nó được tạo nên từ nhiều ảnh nhỏ gọi là tile. Tile được vẽ nối nhau rất phù hợp và tạo nên map cho game. Một bộ tile cùng chủ đề gọi là tileset. 

Lí do phải dùng tile map là để game không phải load ảnh map lớn mà chỉ phải load một tileset nhỏ và dùng lại nó để vẽ map. Ta có thể dùng cùng một tileset tạo nhiều map khác nhau. Việc này giúp chúng ta tiết kiệm bộ nhớ và làm game load nhanh hơn.

Trong bài học hôm nay, chúng ta sẽ sử dụng lại code của bài trước: Sử dụng lớp Sprite và di chuyển Sprite trong MIDP 2.0. Giờ ta sẽ làm một map cho chiếc xe tải chạy và chướng ngại vật chắn đường xe bằng lớp TiledLayer của MIDP 2.0, được tạo ra đặc biệt để vẽ map.

Map chúng ta làm sẽ có kích thước 176x208 (độ phân giải của hầu hết máy Nokia dòng s60v2 như N70, N72...). Thôi không dài dòng nữa, đây là ví dụ giải thích cho các bạn dễ hiểu (hay càng khó hiểu hơn nhỉ, hahaha)
TileMap Diagram
Giải thích về Tile Map

Khung chữ nhật xanh là tile map, những ô trong nó là các tile vẽ nên map. Kích thước tile map dựa vào số hàng và cột của các tile trong đó. Như ví dụ trên thì map này có kích thước 11x13 tile và mỗi tile có kích thước 16x16 pixel. Rất vừa vặn cho màn hình 176x208.

Tạo và hiển thị TiledLayer

Devlin đã chuẩn bị sẵn cho chúng ta một tileset dưới đây. Bạn có thể down về dùng hoặc có thể tự vẽ nếu vẽ đẹp (share cho tôi dùng với ^^).
Tileset
Tileset Image
Dimensions: 160x16 pixels
Tile Size: 16x16 pixels


Đây là hình đã phóng to gấp đôi

Tileset : larger view
Tileset Image zoomed in at 2x

Làm việc nào. Mở clsCanvas, thêm biến cho đối tượng TiledLayer và ảnh tileset 
private Image imgTileset;
private TiledLayer roadMap;
private TiledLayer blockMap;
    /** Creates a new instance of clsCanvas */
    public clsCanvas(midMain m) {

biến imgTileset dùng để lưu trữ ảnh tileset bạn vừa down. Biến roadMap kiểu TiledLayer dùng vẽ đường, nơi xe có thể chạy. Biến blockMap kiểu TiledLayer dùng hiển thị các chướng ngại vật, cũng dùng để phát hiện va chạm.

Tiếp theo, chúng ta thêm phương thức loadRoadMap( ) khởi tạo cho biến roadMap. Thêm đoạn code sau dưới phương thức start( )

    public void loadRoadMap(){
        //initialize the roadMap
        roadMap = new TiledLayer(11, 13, imgTileset, 16, 16);

        // Create a new Random for randomizing numbers
        Random Rand = new Random();
        
        //loop through all the map cells
        for (int y = 0; y < 13; y++){
            for (int x = 0; x < 11; x++){
                // get a random tile index between 2 and 5
                int index = (Math.abs(Rand.nextInt()>>>1) % (3)) + 2;

                // set the tile index for the current cell
                roadMap.setCell(x, y, index);
            }
        }
        // mark Rand for clean up
        Rand = null;
    }

Phương thức loadRoadMap( ) tạo đối tượng TiledLayer và xác định số hàng, cột, ảnh tileset sẽ dùng, kích thước tile. Sau đó nó gán các chỉ số tile ngẫu nhiên từ 2 đến 5 đến mỗi ô của map bằng phương thức setCell( )

Chú ý rằng chỉ số tile bắt đầu từ 1 chứ không phải 0. Bạn có thể để tile đầu trong suốt, để nó không hiển thị được.

Làm tương tự với blockMap và thêm phương thức loadBlockMap( ). Thêm đoạn code sau vào dưới phương thức loadRoadMap( )
    public void loadBlockMap(){
        // define the tile indexes to be used for each map cell
        byte[][] blockData = {
            {10, 8 , 7 , 6 , 10, 9 , 8 , 7 , 6 , 10, 9 },  
            {6 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 8 },  
            {7 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 7 },  
            {8 , 0 , 0 , 10, 6 , 0 , 0 , 7 , 0 , 0 , 6 },  
            {9 , 0 , 0 , 0 , 0 , 0 , 0 , 8 , 0 , 0 , 10},  
            {10, 0 , 0 , 0 , 0 , 0 , 0 , 9 , 0 , 0 , 9 },  
            {6 , 0 , 0 , 8 , 0 , 0 , 0 , 0 , 0 , 0 , 8 },  
            {7 , 0 , 0 , 7 , 0 , 0 , 0 , 0 , 0 , 0 , 7 },  
            {8 , 0 , 0 , 6 , 0 , 0 , 0 , 10, 0 , 0 , 6 },  
            {9 , 0 , 0 , 10, 0 , 0 , 7 , 6 , 0 , 0 , 10},  
            {10, 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 9 },  
            {6 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 8 },  
            {7 , 8 , 9 , 10, 6 , 7 , 8 , 9 , 10, 6 , 7 }
        };
        
        //initialize blockMap
        blockMap = new TiledLayer(11, 13, imgTileset, 16, 16);
        
        //loop through all the map cells
        for (int y = 0; y < 13; y++){
            for (int x = 0; x < 11; x++){
                // set the tile index for the current cell
                // take note of the reversed indexes for blockData
                blockMap.setCell(x, y, blockData[y][x]);
            }
        }
        
        blockData = null;
    }

Phương thức này cũng như loadRoadMap( ) ngoại trừ việc chỉ số mỗi ô dựa vào mảng đã cho trước.

Bây giờ chúng ta chỉnh sửa phương thức load( ) để load ảnh tileset và gọi phương thức chúng ta đã thêm để khởi tạo đối tượng TiledLayer. Ta thay đổi cả nơi bắt đầu hiển thị xe để tránh việc nó kẹt vào chướng ngại vật.
    public void load(){
        try{
            // load the images here
            imgVan = Image.createImage("/images/van.png");
            
            // load the tileset      
            imgTileset = Image.createImage("/images/tileset1.png");
        }catch(Exception ex){
            // exit the app if it fails to load the image
            isRunning = false;
            return;
        }
        
        // initialize the Sprite object
        Van = new Sprite(imgVan, 18, 18);
        
        // show the frame 1 - the second frame
        Van.setFrame(1);
        
        // move to 16, 16 (X, Y)
        Van.setPosition(16, 16);
        
        //initialize the TiledLayers
        loadRoadMap();
        loadBlockMap();        
    }

Ta chỉnh sửa cả phương thức unload( ) như sau
    public void unload(){
        // make sure the object gets destroyed
        blockMap = null;
        roadMap = null;
        Van = null;
        imgTileset = null;
        imgVan = null;
    }

Giờ chúng ta vẽ map lên màn hình. Thêm mấy dòng sau vào phương thức run( ) trước khi xe được vẽ:
           //draw the road
           roadMap.paint(g);
           
           //draw the blocks
           blockMap.paint(g);           
           // draw the sprite
           Van.paint(g);
           
           flushGraphics();

Kiểm tra va chạm

Nếu bây giờ bạn chạy project thì sẽ thấy xe vẫn đi được lên trên các chướng ngại vật. Rất may là hai class TiledLayer và Sprite có phương thức kiểm tra va chạm.

Đầu tiên ta thay đổi tốc độ chiếc xe. Khai báo biến toàn cục vanSpeed như sau
private Sprite Van;

private int vanSpeed = 2;
private Image imgTileset;

Sau đó, thay đổi phương thức run( ) sử dụng biến vanSpeed
           if ((iKey & GameCanvas.UP_PRESSED) != 0){
               // show the van facing up
               Van.setFrame(0);

               // move the van upwards
               cy -= vanSpeed;
           } else if ((iKey & GameCanvas.DOWN_PRESSED) != 0){
               // show the van facing down
               Van.setFrame(1);

               // move the van downwards
               cy += vanSpeed;
           } else if ((iKey & GameCanvas.LEFT_PRESSED) != 0){
               // show the van facing left
               Van.setFrame(2);

               // move the van to the left
               cx -= vanSpeed;
           } else if ((iKey & GameCanvas.RIGHT_PRESSED) != 0){
               // show the van facing right
               Van.setFrame(3);

               // move the van to the right
               cx += vanSpeed;
           }

Bây giờ đến phần kiểm tra va chạm. Chúng ta lưu vị trí hiện thời của xe vào hai biến txty, để có thể vẽ lại vị trí chiếc xe về nơi trước khi nó chạm chướng ngại vật.
           // get the current position of the van
           int cx = Van.getX();
           int cy = Van.getY();
           
           // save the current position in temporary vars
           // so we can restore it when it hits a block
           int tx = cx;
           int ty = cy;           
           if ((iKey & GameCanvas.UP_PRESSED) != 0){

Đoạn code sau đây kiểm tra va chạm nếu xe chạm vào chướng ngại vật và vẽ lại xe về chỗ cũ trước khi nó đâm vào chướng ngại vật. Thêm đoạn code sau vào sau đoạn code update vị trí xe.
           // update the vans position
           Van.setPosition(cx, cy);
           
           //check if the van hits a block
           if (Van.collidesWith(blockMap, true)){
             //reset the van to the original position  
             Van.setPosition(tx, ty);
           }           
           //restore the clipping rectangle to full screen
           g.setClip(0, 0, screenW, screenH);

Phương thức Sprite.collodesWith( ) dùng để phát hiện va chạm giữa xe với các đối tượng blockMap. Tham số thứ 2, giá trị "true" chỉ ra rằng nó phải kiểm tra va chạm ở "mức độ pixel". Tức là chỉ kiểm tra va chạm nếu phần pixel có màu của xe tải chạm vào phần pixel có màu của chướng ngại vật. Bạn cũng có thể dùng phương thức collidesWith( ) để kiểm tra va chạm giữa các đối tượng Sprite.

Nhận tiện, vì map chiếm hết cả màn hình, ta không cần đoạn code fill màn hình màu đen nữa. Hãy xóa hoặc chú thích nó đi.
           //restore the clipping rectangle to full screen
           g.setClip(0, 0, screenW, screenH);
           
           /* comment out or remove this code
            
           set drawing color to black
           g.setColor(0x000000);
            
           fill the screen with blackness
           g.fillRect(0, 0, screenW, screenH);

            */           
           //draw the road
           roadMap.paint(g);

Khi chạy project  bạn sẽ thấy như sau:
TiledLayer Demo

Và sau đây là code lớp clsCanvas hoàn chỉnh cho các bạn đối chiếu và tham khảo:
package MyGame;

import java.util.Random;
import javax.microedition.lcdui.Graphics;
import javax.microedition.lcdui.Image;
import javax.microedition.lcdui.game.GameCanvas;
import javax.microedition.lcdui.game.Sprite;
import javax.microedition.lcdui.game.TiledLayer;

public class clsCanvas extends GameCanvas implements Runnable {

// key repeat rate in milliseconds
public static final int keyDelay = 250;    

//key constants
public static final int upKey = 0;
public static final int leftKey = 1;
public static final int downKey = 2;
public static final int rightKey = 3;
public static final int fireKey = 4;

//key states for up, left, down, right, and fire key
private boolean[] isDown = {
    false, false, false, false, false
};

//last time the key changed state
private long[] keyTick = {
    0, 0, 0, 0, 0
};

//lookup table for key constants :P
private int[] keyValue = {
    GameCanvas.UP_PRESSED, GameCanvas.LEFT_PRESSED,
    GameCanvas.DOWN_PRESSED, GameCanvas.RIGHT_PRESSED, 
    GameCanvas.FIRE_PRESSED
};

private boolean isRunning = true;    
private Graphics g;
private midMain fParent;

private Image imgVan;
private Sprite Van;

private int vanSpeed = 2;

private Image imgTileset;
private TiledLayer roadMap;
private TiledLayer blockMap;
    public clsCanvas(midMain m) {
        super(true);
        fParent = m;
        setFullScreenMode(true);
    }
    
    public void start(){
        Thread runner = new Thread(this);
        runner.start();
    }
    
    public void loadRoadMap(){
        //initialize the roadMap
        roadMap = new TiledLayer(11, 13, imgTileset, 16, 16);
        
        // Create a new Random for randomizing numbers
        Random Rand = new Random();
        
        //loop through all the map cells
        for (int y = 0; y < 13; y++){
            for (int x = 0; x < 11; x++){
                // get a random tile index between 2 and 5
                int index = (Math.abs(Rand.nextInt()>>>1) % (3)) + 2;
                
                // set the tile index for the current cell
                roadMap.setCell(x, y, index);
            }
        }
        
        Rand = null;
    }
    
    public void loadBlockMap(){
        // define the tile indexes to be used for each map cell
        byte[][] blockData = {
            {10, 8 , 7 , 6 , 10, 9 , 8 , 7 , 6 , 10, 9 },  
            {6 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 8 },  
            {7 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 7 },  
            {8 , 0 , 0 , 10, 6 , 0 , 0 , 7 , 0 , 0 , 6 },  
            {9 , 0 , 0 , 0 , 0 , 0 , 0 , 8 , 0 , 0 , 10},  
            {10, 0 , 0 , 0 , 0 , 0 , 0 , 9 , 0 , 0 , 9 },  
            {6 , 0 , 0 , 8 , 0 , 0 , 0 , 0 , 0 , 0 , 8 },  
            {7 , 0 , 0 , 7 , 0 , 0 , 0 , 0 , 0 , 0 , 7 },  
            {8 , 0 , 0 , 6 , 0 , 0 , 0 , 10, 0 , 0 , 6 },  
            {9 , 0 , 0 , 10, 0 , 0 , 7 , 6 , 0 , 0 , 10},  
            {10, 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 9 },  
            {6 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 8 },  
            {7 , 8 , 9 , 10, 6 , 7 , 8 , 9 , 10, 6 , 7 }
        };
        
        //initialize blockMap
        blockMap = new TiledLayer(11, 13, imgTileset, 16, 16);
        
        //loop through all the map cells
        for (int y = 0; y < 13; y++){
            for (int x = 0; x < 11; x++){
                // set the tile index for the current cell
                // take note of the reversed indexes for blockData
                blockMap.setCell(x, y, blockData[y][x]);
            }
        }
        
        blockData = null;
    }    
    public void load(){
        try{
            // load the images here
            imgVan = Image.createImage("/images/van.png");

            imgTileset = Image.createImage("/images/tileset1.png");
        }catch(Exception ex){
            // exit the app if it fails to load the image
            isRunning = false;
            return;
        }
        
        // initialize the Sprite object
        Van = new Sprite(imgVan, 18, 18);
        
        // show the frame 1 - the second frame
        Van.setFrame(1);
        
        // move to 16, 16 (X, Y)
        Van.setPosition(16, 16);
        
        loadRoadMap();
        loadBlockMap();        
    }
    
    public void unload(){
        // make sure the object gets destroyed
        blockMap = null;
        roadMap = null;
        Van = null;
        imgTileset = null;
        imgVan = null;
    }
    
    public void checkKeys(int iKey, long currTick){
        long elapsedTick = 0;
        //loop through the keys
        for (int i = 0; i < 5; i++){ 
            // by default, key not pressed by user
            isDown[i] = false;
            // is user pressing the key
            if ((iKey & keyValue[i]) != 0){ 
                elapsedTick = currTick - keyTick[i];
                //is it time to toggle key state?
                if (elapsedTick >= keyDelay){ 
                    // save the current time
                    keyTick[i] = currTick;  
                    // toggle the state to down or pressed
                    isDown[i] = true; 
                }
            }
        }
    }

    public void run() {
       int iKey = 0;
       int screenW = getWidth();
       int screenH = getHeight();
       long lCurrTick = 0; // current system time in milliseconds;
       
       load();
       g = getGraphics();
       while(isRunning){
           
           lCurrTick = System.currentTimeMillis();
           iKey = getKeyStates();
           
           checkKeys(iKey, lCurrTick);
           
           if (isDown[fireKey]){
               isRunning = false;    
           }
           
           // get the current position of the van
           int cx = Van.getX();
           int cy = Van.getY();
           
           // save the current position in temporary vars
           // so we can restore it when we hit a block
           int tx = cx;
           int ty = cy;           
           if ((iKey & GameCanvas.UP_PRESSED) != 0){
               // show the van facing up
               Van.setFrame(0);
               
               // move the van upwards
               cy -= vanSpeed;
           } else if ((iKey & GameCanvas.DOWN_PRESSED) != 0){
               // show the van facing down
               Van.setFrame(1);
               
               // move the van downwards
               cy += vanSpeed;
           } else if ((iKey & GameCanvas.LEFT_PRESSED) != 0){
               // show the van facing left
               Van.setFrame(2);
               
               // move the van to the left
               cx -= vanSpeed;
           } else if ((iKey & GameCanvas.RIGHT_PRESSED) != 0){
               // show the van facing right
               Van.setFrame(3);
               
               // move the van to the right
               cx += vanSpeed;
           }
           
           // update the vans position
           Van.setPosition(cx, cy);
           
           //check if the van hits a block
           if (Van.collidesWith(blockMap, true)){
             //reset the van to the original position  
             Van.setPosition(tx, ty);
           }           
           //restore the clipping rectangle to full screen
           g.setClip(0, 0, screenW, screenH);
           
           /* comment out or remove this code
            
           set drawing color to black
           g.setColor(0x000000);
            
           fill the screen with blackness
           g.fillRect(0, 0, screenW, screenH);

            */

           //draw the road
           roadMap.paint(g);
           
           //draw the blocks
           blockMap.paint(g);           
           // draw the sprite
           Van.paint(g);
           
           flushGraphics();
           
           try{
               Thread.sleep(30);
           } catch (Exception ex){
               
           }
       }
       g = null;
       unload();
       fParent.destroyApp(false);
       fParent = null;
    }
}

Về Tilemap, các bạn có thể tìm trên internet với từ khóa "tile map" để tham khảo. Chúc các bạn thành công !!

1 nhận xét:

  1. I feel really happy to have seen your webpage and look forward to so many more entertaining times reading here. Thanks once more for all the details. fallout 4 teleport console command

    Trả lờiXóa