Header Ads

[Android Game] Tự tạo Game Flappy Bird cho riêng mình

Thấy bài viết này trên tinh tế hay quá, share về cho bạn nào cần.....Tut rất rõ ràng, dễ hiểu 

Make your own "Flappy Bird" game with libGDX


I. Phân tích bài toán..

A. Kịch bản:
- Có một màn hình chơi game với...
- background là một nền trời nằm yên... mặt đất phía dưới chạy liên tục từ phải sang trái.
- Một con chim vỗ cánh liên tục.
- Nếu "tap" vào màn hình:
Con chim tại vị trí khi "tap" sẽ bay ngửa mặt lên một đoạn và rơi cắm đầu xuống đất... Trong quá trình thực hiện chuỗi thao tác trên nếu xảy ra sự kiện "tap" màn hình.. con chim sẽ thực hiện huỷ bỏ chuôic thao tác trước và tại vị trí khi "tap" sẽ thực hiện chuỗi thao tác mới...

- Trong một khoảng thời gian cố định sẽ tuần tự xuất hiện hai ống nước trên và dưới chừa một khoảng hở cao thấp ngẫu nhiên cho con chim bay qua...

- Nếu con chim bay qua được khoảng hở hai ống này sẽ được cộng một điểm.
- Nếu con chim đụng vào hai ống này thì GAME OVER.

B. Xây dựng đối tượng.

1 nền trời: Image background nạp cố định lên màn hình

2 mặt đất: Land (kế thừa Image):
- hành động:
* actionMoveLeft() trôi liện tục về trái - được gọi ngay khi bắt đầu

3 con chim: Bird (kế thừa Image)
- thuộc tính:
* boolean isDie (cho biết chim sống hay chết - chết là HẾT PHIM)
* int score (điểm con chim đạt được khi bay qua các ống)
- hành động:
* đập cánh liên tục (nếu isDie thì dùng đập cánh) - được gọi ngay khi bắt đầu.
* tapMe() - hàm thực hiện chuỗi hành động nhảy lên và rơi xuống - đươc gọi khi sự kiên "tap" xảy ra.
* hitMe() - hàm thực hiện cắm đầu xuống đất và chết - được gọi khi ống đụng con chim
* updateScore() - hàm công score của con chim thêm một điểm - được gọi khi ống thấy con chim bay qua.

4. Ống: Pipe (kế thừa Image)
- thuộc tính:
* boolean getScore; (cho biết con chim bay qua có tính điểm cho chim hay không)
* Bird bird; (con chim mà ống sẽ kiểm tra "bay qua" hay "đụng vào"
- hành động:
* actionMoveLeft() - trôi liên tục về trái (nếu vượt quá màn hình thì tự động REMOVE) - được gọi ngay trong khi
* bypass() - kiểm tra và cộng điểm cho con chim - được gọi liên tục
Nếu getScore true và có chim bay qua thì gọi hàm updateScore() của chim.
* checkCollision() - kiểm tra va chạm với chim - được gọi liên tục.
Nếu có va chạm thì gọi hàm hitMe() của chim.

5. Màn hình Sân khấu: MyStage (kế thừa stage)
- hành động:
Override hàm touchDown() - được gọi khi có sự kiện touch vào màn hình.
thực hiện hàm tapMe() của con chim
hoặc gọi resetGame() nếu con chim chết.

6 Màn hình chơi game: PlayScreen (kế thừa Screen)
- thuộc tính:
* MyStage stage: nhận touch, chứa các đối tượng của game, gọi actions và vẽ chúng...
* Bird bird: nhân vật chính của game
* Land land: mặt đất trôi.
* static Label labelScore: hiển thị điểm của con chim (static để con chim cập nhật)
- hành động:
* nap background, nạp Land, nạp Bird, nạp labelScore - gọi khi reset lại Game
* addPipe(); nạp hai ống có khoảng hở ngẫu nhiên - được gọi tuần tự sau một khoảng thời gian cho trước.

II. Code với libGDX:

A. Chuẩn bị:

1. resource gồm:
- flappy.txt + flappy.png
*background.png
*land.png
*bird1.png, bird2.png, bird3.png (3 frame hoạt hình vỗ cánh)
*pipe1.png, pipe2.png (ống trên và ống dưới)

- flappyfont.fnt + flappyfont.png
*BitMap Font gồm ký tự 0,1,2,3,4,5,6,7,,8,9

- sound
*sfx_hit.mp3, sfx_point.mp3, sfx_wing.mp3
phát tiếng khi va chạm, ghi điểm và vỗ cánh (tap).

2. Tạo Project:
- Sử dụng gdx-setup-ui.jar để tạo 3 project chính FlappyBird, FlappyBird_android, FlappyBird_desktop
Name: FlappyBird
Package: com.tai.flappy
Game Class: FlappyBird


- Dùng eclipse import 3 project FlappyBird, FlappyBird_android, FlappyBird_desktop
- Copy toàn bộ resource vào thư mục assets/data của project FlappyBird_android


B. Coding:

Chú Ý: mọi hướng dẫn sau đây sẽ là:
- Coding trong project FlappyBird
- run/debug trong project FlappBird_destop.

1. Định nghĩa các biến hằng
new một file config.java

Code:
public class config {

   //key de nap cac sound vao mang va lay sound ra de play
   public static String SoundJump = "jump";
   public static String SoundScore = "score";  
   public static String SoundHit = "hit";
  
   //chieu cao va chieu dai thiet ke cua hinh land.png
   public static int kLandHeight = 112;
   public static float kLandWidth = 336;
  
   //thoi gian move sang trai khoang kLandWidth
   public static float kmoveLeftDura = 3f;

   //chieu cao va thoi gian khi bay len do "tap"
   public static int kjumpHeight = 60;
   public static float kjumpDura = 0.2f;
  
   //khoang thoi gian them ong nuoc vao man hinh
   public static float kTimeAddPipe = 2;
  
   //khoang ho giua hai ong nuoc
   public static float kHoleBetweenPipes = 150;
  
   //ham tinh random mot so trong pham vi tu min den max
   public static int random(int min, int max)
   {
     Random random = new Random();
     return random.nextInt(max - min + 1) + min;
   }
}
2. Thay ApplicationListener bằng Game
- Vào project FlappyBird mở file FlappyBird.java
xoá hết và thay vào với nội dung sau:
Code:
public class FlappyBird  extends Game {

   //design viewport
   public static final Vector2 VIEWPORT = new Vector2(320, 480);
  
   //Quan ly textureAtals va sound
     public AssetManager manager = new AssetManager();
   public static HashMap<String, Sound> sounds = new HashMap<String, Sound>();
  
   public PlayScreen getPlayScreen()
   {
     return new PlayScreen(this);
   }
  
  
   @Override
   public void create() {
    
     //nap danh sach cac sound, de bat ky dau cuxng co the goi va "play"
     sounds.put(config.SoundJump, Gdx.audio.newSound(Gdx.files.internal("data/sounds/sfx_wing.mp3")));
     sounds.put(config.SoundScore, Gdx.audio.newSound(Gdx.files.internal("data/sounds/sfx_point.mp3")));
     sounds.put(config.SoundHit, Gdx.audio.newSound(Gdx.files.internal("data/sounds/sfx_hit.mp3")));

   }

   @Override
     public void resize(int width, int height )
     {
     super.resize( width, height );
  
     if( getScreen() == null ) {
       setScreen( getPlayScreen() );
  }
  };

  @Override
  public void render()
  {
  super.render();
  }

  @Override
  public void pause()
  {
  super.pause();

  }

  @Override
  public void resume()
  {
  super.resume();
  }
  
  @Override
  public void setScreen(Screen screen )
  {
  super.setScreen( screen );
  }

  @Override
  public void dispose()
  {
     //giai phong sounds
   for (String key: sounds.keySet())
   {
     sounds.get(key).dispose();
   }
    
   //giai phong texture
   manager.dispose();
    
  super.dispose();
  }  
}
Trong đó ta tạo hai biến static manager và sounds
Và nạp màn hình Game đầu tiên vào là màn hình PlayScreen - lúc này sẽ bị lỗi phần get và nạp PlayScreen vì chưa code :)


3. Custom Stage để override hàm TouchDown() - MyStage
Code:
public class MyStage extends Stage {
  
   PlayScreen screen;
  
   public MyStage(float width, float height, boolean keepAspecyRatio)
   {
     super(width, height, keepAspecyRatio);
   }
    
   public void setPlayScreen(PlayScreen screen)
   {
     this.screen = screen;
   }
  
   @Override
   public boolean touchDown(int x, int y, int pointer, int button) {

     if (screen != null)
     {
       if (screen.bird.isDie)
       {
         screen.resetGame();
       }
       else
       {
         screen.bird.tapMe();
         FlappyBird.sounds.get(config.SoundJump).play();
       }
     }
    
     return super.touchDown(x, y, pointer, button);
   }
  
   @Override
   public boolean touchDragged(int x, int y, int pointer) {

     return super.touchDragged(x, y, pointer);
   }
  
   @Override
   public boolean touchUp(int x, int y, int pointer, int button) {
    
     return super.touchUp(x, y, pointer, button);
   }
}
Trong đó có hàm setPlayScreen để lấy biến màn hình game nhằm trong touchDown sẽ thực hiên thao tác:
Nếu chim chết thì resetGame ngược lại "tap con chim"

4. Mặt đất - Land
Code:
public class Land extends  Image {

   public Land(TextureRegion region)
   {
     super(region);
     actionMoveLeft();
   }
  
   @Override
   public void act(float delta) {
     super.act(delta);
    
     if (getX() <= -config.kLandWidth) setX(0);
   }
  
   private void actionMoveLeft()
   {
     addAction(forever(moveBy(-config.kLandWidth, 0, config.kmoveLeftDura)));
   }
  
}
Trong đó constructor thực hiện actionMoveLeft ngay khi khai báo và act() thực hiện và "giật ngược" lại .
Tạo hiệu ứng trôi liên tục dài vô tận (mà hình thì không vô tận nên phải giật lùi)

5. Con chim - Bird
Code:
public class Bird extends Image {
  
   public int score;
   private Action curAction;

   Animation animation;
   TextureRegion curFrame;
   float dura;
  
   public boolean isDie;
  
   public Bird(TextureRegion[] regions)
   {
     super(regions[0]);
     setOrigin(getWidth()/2, getHeight()/2);
    
     animation = new Animation (0.03f, regions);
     dura = 0;
     isDie = false;
    
   }

   @Override
   public void act(float delta) {
     super.act(delta);
    
     if (isDie) return;
     //animation flying...
     dura += delta;
     curFrame = animation.getKeyFrame(dura, true);
     setDrawable(new TextureRegionDrawable(curFrame));
   }

   public void tapMe()
   {
     //huỷ bỏ action trước để bắt đầu chuỗi hành động của tapMe mới
     this.removeAction(curAction);

     float y = getY() + config.kjumpHeight;
    
     //fly up
  RotateToAction faceup = new RotateToAction();
  faceup.setDuration(config.kjumpDura);
  faceup.setRotation(30.0f);
  
  MoveToAction moveup = new MoveToAction();
  moveup.setDuration(config.kjumpDura);
  moveup.setPosition(getX(), y);
  moveup.setInterpolation(Interpolation.sineOut);

  Action fly  = parallel( faceup, moveup);
  
  //fall down
  float durafall = getDuraDown(y, config.kLandHeight);
  
  RotateToAction facedown = new RotateToAction();
  facedown.setDuration(durafall);
  facedown.setRotation(-90.0f);
  
  MoveToAction movedown = new MoveToAction();
  movedown.setDuration(durafall);
  movedown.setPosition(this.getX(), config.kLandHeight);
  movedown.setInterpolation(Interpolation.sineIn);
  
  Action fall  = parallel( facedown, movedown);
  
  curAction = sequence(fly, fall);
  this.addAction(curAction);    
   }

   public void hitMe()
   {
     isDie = true;
     this.removeAction(curAction);
    
  //fall down
  RotateToAction facedown = new RotateToAction();
  facedown.setDuration(config.kjumpDura);
  facedown.setRotation(-90.0f);
  
  MoveToAction movedown = new MoveToAction();
  movedown.setDuration(getDuraDown(getY(), config.kLandHeight) * 1/2);
  movedown.setPosition(this.getX(), config.kLandHeight);
  movedown.setInterpolation(Interpolation.sineIn);
  
  curAction  = parallel( facedown, movedown);
  this.addAction(curAction);    
   }

  
   public void updateScore()
   {
     score++;
     PlayScreen.labelScore.setText("" + score);
    
   }
  
   //tính thời gian rớt xuống tỉ lệ thuận với kJumHeight và kJumDura.
   float getDuraDown(float up, float down)
   {
    
    float dy = up - down;
    float duraDown;
    
    //neu dy <= kjumheight => thoi gian khong doi
    if (dy <= config.kjumpHeight) duraDown = config.kjumpDura;
    else //neu cao hon thi tinh theo ti le thuan
    {
    duraDown = dy * (config.kjumpDura) / config.kjumpHeight;
    }
    
    return duraDown;
   }
  
}

Trong đó thực hiện vỗ cánh trong act()
và sẵn sàng các hàm public tapMe(), hitMe() và updateScore() cho người ta gọi khi gặp tình huống thích hợp....

Chú ý: bay lên có: thời gian bay một đoạn kJumHeight là kJumpDura
=> rơi xuống cao hơn => tính tỉ lệ thuận lại thời gian rơi.
=> rơi cắm đầu khi chết thì được tăng gấp đôi nhanh hơn.

6. Ống Nước - Pipe
Code:
public class Pipe extends Image {

   boolean getScore;
  
   Bird bird;
  
   public Pipe(TextureRegion region, Bird bird, boolean getScore)
   {
     super(region);
     this.bird = bird;
     this.getScore = getScore;
    
    
     actionMoveLeft();
   }
  
   @Override
   public void act(float delta) {
     super.act(delta);
    
     if (bird.isDie)
     {
       clearActions();
       return;
     }
    
     //ra khoi man hinh remove...
     if (getX() < -getWidth())
     {
       remove();
     }
    
  bypass();
     checkCollision();
   }
  
   private void actionMoveLeft()
   {
    
    MoveByAction moveleft = new MoveByAction();
    moveleft.setDuration(config.kmoveLeftDura);
    moveleft.setAmountX(-config.kLandWidth);
    
    addAction(forever(moveleft));

   }
  
   private void bypass()
   {
  if (getX() <= bird.getX())
  {
     if (getScore)
     {
       getScore = false; //chi tinh diem mot lan....
       bird.updateScore();
       FlappyBird.sounds.get(config.SoundScore).play();
     }
  }
   }
  
   private void checkCollision()
   {
     if (isCollision())
     {
       bird.hitMe();
       FlappyBird.sounds.get(config.SoundHit).play();
     }
   }
  
   private boolean isCollision()
   {
     float d = 4; //gia giam de chim dung vao pipe nhieu hon....
  
     float maxx1 = getX() + getWidth() - d;
     float minx1 = getX() + d;
     float maxy1 = getY() + getHeight() - d;
     float miny1 = getY() + d;
  
     float maxx2 = bird.getX() + bird.getWidth() - d;
     float minx2 = bird.getX() + d;
     float maxy2 = bird.getY() + bird.getHeight() - d;
     float miny2 = bird.getY() + d;
  
     return !(maxx1 < minx2 ||
         maxx2 < minx1 ||
         maxy1 < miny2 ||
         maxy2 < miny1);
  
   }
  
}
Trong đó constructor thực hiện actionMoveLeft ngay khi khai báo.
act() thực hiện
- remove bản thân khi vượt quá màn hình
- bypass - kiểm tra cho điểm con chim - gọi bird.updateScore() và phát sound nếu có cho điểm
- checkColiision - kiểm tra va chạm con chim - gọi bird.hitMe() và phát sound nếu có va chạm

7. Màn hình chơi Game - PlayScreen
...
gọi reset Game khi bắt đầu...
Code:
   public void resetGame()
   {
     stage.clear();
    duraAddPipe = 0;
    addBackground();
    addLand();
    addBird();
    addLabelScore();
   }
Trong đó clear tất tần tật mọi thứ đang có trong stage.
nạp lai background, land, bird và labelScore.


Nap backgrond, nạp land, nạp bird và nạp Label
Code:
   private void addBackground()
   {
  Image bg = new Image(atlas.findRegion("background"));
  stage.addActor(bg);
   }
  
   private void addLand()
   {
     land = new Land(atlas.findRegion("land"));
     stage.addActor(land);
   }
  
   private void addBird()
   {
     TextureRegion[] regions = new TextureRegion[] { atlas.findRegion("bird1"),
         atlas.findRegion("bird2"), atlas.findRegion("bird3") };
    
     bird = new Bird(regions);
     bird.setPosition(FlappyBird.VIEWPORT.x/2 - bird.getWidth()/2, FlappyBird.VIEWPORT.y/2);
    
     stage.addActor(bird);
   }

   private void addLabelScore()
   {
     LabelStyle textStyle = new LabelStyle();
     textStyle.font = new BitmapFont(Gdx.files.internal("data/flappyfont.fnt"), Gdx.files.internal("data/flappyfont.png"), false);

     labelScore = new Label("0",textStyle);
     labelScore.setPosition(FlappyBird.VIEWPORT.x/2 - labelScore.getWidth()/2, FlappyBird.VIEWPORT.y - labelScore.getHeight());
    
     stage.addActor(labelScore);
   }

Render
Code:
  @Override
  public void render(
  float delta )
  {
    
     if (bird.isDie)
     {
       land.clearActions();
     }
     else
     {
       duraAddPipe += delta;
       if (duraAddPipe > config.kTimeAddPipe)
       {
         duraAddPipe = 0;
         addPipe();
       }
     }
    
  // update the action of actors
  stage.act( delta );

  // clear the screen with the given RGB color (black)
  Gdx.gl.glClearColor( 0f, 0f, 0f, 1f );
  Gdx.gl.glClear( GL20.GL_COLOR_BUFFER_BIT );

  // draw the actors
  stage.draw();
  }

Trong đó.. nếu chim chết thì land dừng trôi, dừng nạp ống
ngược lại chim chưa chết thì sau mỗi kTimeAddPipe giây thì goi nạp ống addPipe() một lần.


Nap Ống
Code:
    //random cho Hole lech len va xuong
    //random lech
    int r = config.random(0, 4);
    float dy = r * 10; //random 0, 10, 20, 30, 40
    
    //random lech len hay xuong
    r = config.random(0, 1);
    if (r==0) dy = -dy;
    
    
    //add pipe1 ben tren vaf pipe2 ben duoi...khoang cach giua co dinh la kHoleBetweenPipe px
    //tam hole giua canh tren man hinh va canh tren Land...
    //sau nay random lech len , lech xuong sau...
    Pipe pipe1 = new Pipe(atlas.findRegion("pipe1"), bird, true);
    pipe1.setZIndex(1);
    float x = FlappyBird.VIEWPORT.x;
    float y = (FlappyBird.VIEWPORT.y - config.kLandHeight)/2 + config.kLandHeight + config.kHoleBetweenPipes/2;
    pipe1.setPosition(x, y + dy);
    
    Pipe pipe2 = new Pipe(atlas.findRegion("pipe2"), bird, false);
    pipe2.setZIndex(1);
    y = (FlappyBird.VIEWPORT.y - config.kLandHeight)/2 + config.kLandHeight - pipe2.getHeight() - config.kHoleBetweenPipes/2;
    pipe2.setPosition(x, y + dy);
    
    stage.addActor(pipe1);
    stage.addActor(pipe2);
    
     land.setZIndex(pipe2.getZIndex());
     bird.setZIndex(land.getZIndex());
     labelScore.setZIndex(bird.getZIndex());

   }

Trong đó.. nạp ống trên (pipe1) và ống dưới (pipe2) có khoảng hở ở giữa màn hình (tính từ top đến mặt đất).
Sau đó + thêm dy là số ngẫu nhiên lệch lên trên hay lệch xuống dưới..

source code

CHÚC MỌI NGƯỜI - MỌI NHÀ "TAP TAP" CON CHIM - NHƯNG MÀ LÀ CON CHIM CỦA MÌNH CHỨ KHÔNG PHẢI CỦA NGƯỜI TA....

No comments:

Powered by Blogger.