JAVA專案:Java實現飛揚的小鳥(Flappy Bird)

QF大資料發表於2020-09-25

飛揚的小鳥(Flappy Bird)

 

需求描述

遊戲載入完畢點選介面即可開始遊戲。

滑鼠點選控制小鳥飛行,或者通過鍵盤控制小鳥的前後上下也可以,不要撞到管道哦!

控制好小鳥越過障礙飛得更遠,獲得更高的積分。

 

使用的技術點

  • 變數
  • 分支語句
  • 迴圈語句
  • 物件導向
  • 異常處理
  • Random隨機數
  • StringBuffer字串操作
  • IO操作
  • 多執行緒
  • swing元件
  • 。。。。

 

需求分析


程式碼實現

1、實現介面背景

step1:首先新建一個class表示背景類BackGround。我們要在該類中,載入背景圖片。

建立一個包pics,裡面先存放背景圖:bg.png。

先定義一個常量類Constant,專門用於儲存程式中的常量。

程式碼實現:

package com.ruby.demo;
​
/**
 * 常量類
 * @author ruby
 *
 */
public class Constant {
​
  // 圖片路徑
  public static String PATH_PIC = "/pics/";
// 背景圖片路徑
  public static String PATH_BACKGROUND = PATH_PIC + "bg.png";
  
}
​

然後建立BackGround類:

程式碼實現:

package com.ruby.demo;
​
import java.awt.image.BufferedImage;
import java.io.IOException;
​
import javax.imageio.ImageIO;
​
/**
 * step1:背景類:單例模式
 * 
 * @author ruby
 *
 */
public class BackGround {
  public BufferedImage img = null;// 背景圖片
  public int width = 0;// 背景的寬度
  public int height = 0;// 背景的高度
  private static BackGround instance = null;
​
  private BackGround() {
    try {
      // ImageIO用於載入圖片資源
      // this.getClass().getResource根據當前路徑載入圖片
      img = ImageIO.read(this.getClass().getResource(Constant.PATH_BACKGROUND));
​
      // 獲取背景圖片長度和高度
      width = img.getWidth();// 獲取圖片的寬度
      height = img.getHeight();// 獲取圖片的寬度
​
      System.out.println("widthBg=" + width + ", heightBg=" + height);
​
    } catch (IOException e) {
      e.printStackTrace();
    }
  }
​
  // 實現執行緒安全的懶漢式
  public static BackGround getInstance() {
    if (instance == null) {
      instance = new BackGround();
    }
    return instance;
  }
}
​

程式碼實現:

package com.ruby.demo;
​
import java.awt.image.BufferedImage;
import java.io.IOException;
​
import javax.imageio.ImageIO;
​
/**
 * step1:背景類:單例模式
 * 
 * @author ruby
 *
 */
public class BackGround {
  public BufferedImage img = null;// 背景圖片
  public int width = 0;// 背景的寬度
  public int height = 0;// 背景的高度
  private static BackGround instance = null;
​
  private BackGround() {
    try {
      // ImageIO用於載入圖片資源
      // this.getClass().getResource根據當前路徑載入圖片
      img = ImageIO.read(this.getClass().getResource(Constant.PATH_BACKGROUND));
​
      // 獲取背景圖片長度和高度
      width = img.getWidth();// 獲取圖片的寬度
      height = img.getHeight();// 獲取圖片的寬度
​
      System.out.println("widthBg=" + width + ", heightBg=" + height);
​
    } catch (IOException e) {
      e.printStackTrace();
    }
  }
​
  // 實現執行緒安全的懶漢式
  public static BackGround getInstance() {
    if (instance == null) {
      instance = new BackGround();
    }
    return instance;
  }
}
​

說明:

  • 整個專案只有這一個背景,所以可以設計為單例模式。
  • 通過getResource()方法載入圖片資源。

 

step2:然後建立一個皮膚類,上面用於實現背景,小鳥等。GamePanel

這裡主要重寫paint()方法,將背景圖片,顯示到皮膚上。

程式碼實現:

package com.ruby.demo;
​
import java.awt.Graphics;
import java.awt.Image;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
​
import javax.imageio.ImageIO;
import javax.swing.JPanel;
​
/**
 * step2:自定義JPanel類的子類
 * 
 * @author ruby
 *
 */
​
public class GamePanel extends JPanel {
​
  private BackGround bg = null;// 宣告背景物件
​
  /*
   * 建構函式
   */
  public GamePanel() {
    // 單例模式宣告背景物件和地面物件
    bg = BackGround.getInstance();
  }
​
  /**
   * 當前皮膚中繪製元件(載入圖片等)
   * 
   * paint方法會在初始化以及最小和最大化時自動呼叫該方法(即視窗發生變化時,jvm都會自動呼叫該方法用於繪製皮膚)
   */
  @Override
  public void paint(Graphics g) {
    super.paint(g);
    System.out.println("paint方法被呼叫" + getCurrentTime());
    // Graphics物件繪製背景圖案
    g.drawImage(bg.img, 0, 0, null);
​
  }
​
  // 獲取當前時間
  public String getCurrentTime() {
    Date day = new Date();
    SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
    return df.format(day);
  }
}
​


step3:建立一個窗體類GameFrame,裡面新增剛剛建立的GamePanel物件。

但是首先要在Constant常量類中,設定一些常量:

程式碼:

// 介面引數
  public static String GAME_TITLE = "飛翔吧小鳥";
  public static int WINDOW_WIDTH = 432;
  public static int WINDOW_HEIGHT = 644;

程式碼實現:

package com.ruby.demo;
​
import javax.swing.JFrame;
/**
 * step3:窗體
 * @author ruby
 *
 */
public class GameFrame extends JFrame {
​
  // 初始化窗體
  public void initFrame() {
    // 設定視窗標題
    setTitle(Constant.GAME_TITLE);
    // 設定視窗大小
    setSize(Constant.WINDOW_WIDTH, Constant.WINDOW_HEIGHT);
​
    // 新增Panel
    GamePanel panel = new GamePanel();
    add(panel);
​
    // 設定視窗座標
    setLocationRelativeTo(null);
​
    // 設定視窗可見
    setVisible(true);
​
    // 設定視窗大小不可調整
    setResizable(false);
    
    // 監聽視窗關閉,程式結束
    setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
  }
}
​

step4:建立Main類,表示程式的入口:

程式碼實現:

package com.ruby.demo;
/**
 * 程式的入口
 * @author ruby
 *
 */
​
public class Main {
​
  public static void main(String[] args) {
    GameFrame frame = new GameFrame();
    frame.initFrame();
  }
​
}
​

執行效果:

2、實現地面移動

思路:

 

首先得先在Constant常量類中,新增地面的圖片路徑,並且將地面圖片拷貝到pics資源目錄下。

// 地面圖片路徑
  public static String PATH_GROUND = PATH_PIC + "ground.png";

然後建立Ground類:

然後建立Ground類:

package com.ruby.demo;
​
import java.awt.image.BufferedImage;
import java.io.IOException;
​
import javax.imageio.ImageIO;
​
/**
 * step1:地面類
 * 
 * @author ruby
 *
 */
public class Ground {
  public BufferedImage img = null;// 地面圖片
  public int x, y;// 地面繪製的起始座標
  public int width = 0;// 地面的寬度
  public int height = 0;// 地面的高度
  private static Ground instance = null;
​
  private Ground() {
    try {
      // 單例模式
      BackGround bg = BackGround.getInstance();
      // ImageIO用於載入圖片資源
      // this.getClass().getResource根據當前路徑載入圖片
      img = ImageIO.read(this.getClass().getResource(Constant.PATH_GROUND));
​
      // 獲取地面圖片的長度和高度
      width = img.getWidth();// 獲取圖片的寬度
      height = img.getHeight();// 獲取圖片的寬度
​
      x = 0;
      y = bg.height - height;// 背景高度與地面圖片高度的差值就是地面圖片的起始Y座標
      System.out.println("widthGround=" + width + ", heightGround=" + height);
      System.out.println("x=" + x + ", y=" + y);
​
    } catch (IOException e) {
      e.printStackTrace();
    }
  }
​
  // 實現懶漢式
  public static Ground getInstance() {
    if (instance == null) {
      instance = new Ground();
    }
    return instance;
  }
}
​

說明:在該類中要計算出地面的座標點x和y。

x為0即可,而y的值為背景圖片的高度減去地面圖片的高度。

然後新增一個地面移動的方法:

// 地面移動
  public void move(BackGround bg) {
    x--;
    if (x == bg.width +9 - width) {// 9為修正值,根據地面移動效果調整該數值,保證圖片移動自然流暢。是地面圖片中條紋間距的一半數值。
      x = 0;
    }
     System.out.println("x=" + x);
  }

所謂的運動地面,就是就是修改x的值,地面向左側移動,所以x--。

然後要修改GamePanel中的paint()方法,繪製地面:

@Override
  public void paint(Graphics g) {
    super.paint(g);
    System.out.println("paint方法被呼叫" + getCurrentTime());
    // Graphics物件繪製背景圖案
    g.drawImage(bg.img, 0, 0, null);
​
    // 繪製地面
    g.drawImage(ground.img, ground.x, ground.y, null);
  }

然後在GamePanel類中新增一個新增的方法action(),表示遊戲的動作,我們需要設定一個死迴圈,來讓地面不停的移動。

多久移動一次呢,我們可以設定一個速度,其實就表示遊戲的速度,可以初始化一個常量,每當過一關,遊戲的速度可以適當的增加。

現在定義一個常量:

public static int MOVE_SPEED = 40;// 地面及柱子移動初始速度。當積分累加,速度會遞增

在GamePanel類中新增一個變數speed,表示速度:

private int speed = 0;// 柱子和地面的移動速度

修改GamePanel()構造方法:

// 初始化速度
speed = Constant.MOVE_SPEED;
​

然後新增一個action()方法,

public void action() {
    // 設定滑鼠監聽
​
    // 設定鍵盤監聽
​
    // 通過監聽滑鼠事件,監聽到state的變化,無限迴圈,不斷切換狀態
    while (true) {
      // 地面移動
      ground.move(bg);
​
      // 執行緒休眠(因為是無限迴圈,下一次迴圈開始需要一段休息時間,這樣才能讓程式有緩衝的執行時間)
      try {
        Thread.sleep(1000 / speed);// 調節遊戲速度
        // 重新繪製(重新呼叫皮膚paint方法)
        this.repaint();
      } catch (Exception e) {
        e.printStackTrace();
      }
    }
​
  }

為了不讓地面移動太快,頻繁的繪製介面,我們需要讓程式睡眠一下,這個睡眠的時間,其實表示遊戲的速度,就是地面和柱子移動的速度。

然後在GameFrame的initFrame()方法中,呼叫action()方法:

// 皮膚執行
panel.action();

執行效果:


3、顯示開始和結束

思路:整個遊戲分為3個狀態:遊戲開始前,開始玩,遊戲結束。定義一個變數state,0表示未開始,1表示玩ing,2表示game over,遊戲結束。

當遊戲未開始狀態,顯示開始圖片。

點選開始遊戲後,可以玩,當小鳥撞到地面或者天空或者柱子,遊戲結束。

遊戲結束時,顯示結束圖片。

在GamePanel類中,新增一個變數state

private static int state = 0;// 遊戲狀態,0表示遊戲未開始,1表示遊戲正在進行,2表示遊戲結束

然後在構造方法中,初始化state的狀態,以及載入開始和結束的圖片:

/*
   * 建構函式
   */
  public GamePanel() {
​
    // 初始化資料
​
    // 單例模式宣告背景物件和地面物件
    bg = BackGround.getInstance();
    ground = Ground.getInstance();
    // state = 0表示遊戲未開始
    state = 0;
​
    try {
​
      // 載入開始和結束圖片
      imgStart = ImageIO.read(this.getClass().getResource(Constant.PATH_START));
      imgOver = ImageIO.read(this.getClass().getResource(Constant.PATH_GAMEOVER));
​
    } catch (IOException e) {
      e.printStackTrace();
    }
​
  }

同時將兩張圖片,新增到pics目錄下,並且在Constant類中,新增常量值:

  public static String PATH_START = PATH_PIC + "start.png";
  public static String PATH_GAMEOVER = PATH_PIC + "gameover.png";

修改繪圖的方法paint(),先繪製背景,然後根據狀態不同,繪製不同的圖案,最後繪製地面:

@Override
  public void paint(Graphics g) {
    super.paint(g);
    System.out.println("paint方法被呼叫" + getCurrentTime());
    // Graphics物件繪製背景圖案
    g.drawImage(bg.img, 0, 0, null);
​
    // 根據狀態,繪製不同的圖案
    if (state == 0) {//遊戲未開始
      // 遊戲未開始時,繪製開始圖案及小鳥
      g.drawImage(imgStart, 0, 0, null);
​
    } else if (state == 1) {// 遊戲開始後
      
    } else if (state == 2) {// 遊戲結束
      // 遊戲結束時,繪製結束圖案
      g.drawImage(imgOver, 0, 0, null);
    }
​
    // 繪製地面
    g.drawImage(ground.img, ground.x, ground.y, null);
  }

修改action()方法,新增滑鼠事件:

public void action() {
    // 設定滑鼠監聽
    this.addMouseListener(new MouseAdapter() {
      //點選滑鼠後
      @Override
      public void mouseReleased(MouseEvent e) {
        super.mouseReleased(e);
        switch (state) {
        case 0://遊戲未開始
          // 切換到狀態1時的資料
          state = 1;
          break;
          
        case 1://開始遊戲
          state = 2;//...
          break;
          
        case 2://遊戲結束
          //遊戲結束後,更改狀態為0,可以繼續下一次遊戲
          state = 0;
          break;
​
        default:
          break;
        }
        
        
        
      }
    });
    
    // 設定鍵盤監聽
  //....
​
  }
​

執行效果:

點選一下,開始遊戲,再點一下結束遊戲,效果如圖:

 

開始遊戲的時候,小鳥是灰色的,我們希望小鳥能一直扇動翅膀。

小鳥翅膀扇動,其實就是小鳥的8張圖迴圈輪播。我們可以通過陣列來實現。

現在Constant類中定義小鳥的圖片數量,以及小鳥的初始位置:

public static int BIRD_PIC_COUNT = 8;// 小鳥皮膚個數
public static int BIRD_POSITION_X = 190;// 小鳥初始化座標
public static int BIRD_POSITION_Y = 220;

然後建立一個小鳥類Bird:

package com.ruby.demo;
​
import java.awt.image.BufferedImage;
import java.io.IOException;
​
import javax.imageio.ImageIO;
​
/**
 * 小鳥類
 * 
 * @author ruby
 *
 */
public class Bird {
  public BufferedImage img = null;// 小鳥圖片
  public BufferedImage imgs[] = new BufferedImage[Constant.BIRD_PIC_COUNT];// 陣列,儲存所有小鳥圖案
  public static int index = 0;// 當前皮膚的序號
  public int x, y;// 初始座標
  public int width = 0;// 小鳥的寬度
  public int height = 0;// 小鳥的高度
​
  public Bird() {
    try {
      for (int i = 0; i < 8; i++) {
        imgs[i] = ImageIO.read(getClass().getResource(Constant.PATH_PIC + i + ".png"));
      }
      img = imgs[0];
      // 獲取小鳥的寬度和高度
      width = img.getWidth();
      height = img.getHeight();
​
      // 初始化小鳥的座標位置
      x = Constant.BIRD_POSITION_X;
      y = Constant.BIRD_POSITION_Y;
    } catch (IOException e) {
      e.printStackTrace();
    }
  }
​
  
}
​

新增一個小鳥扇動翅膀的方法:

// 小鳥飛翔的圖片切換
  public void fly() {
    index++;
    // 小鳥圖形切換的頻率,index/x,x越大,翅膀切換頻率越慢
    img = imgs[index / 6 % Constant.BIRD_PIC_COUNT];
    if (index == 6 * Constant.BIRD_PIC_COUNT) {
      index = 0;
    }
  }

然後在GamePanel類中新增小鳥物件,

private Bird bird = null;// 宣告小鳥物件

並在構造方法中初始化:

// 宣告小鳥物件
    bird = new Bird();

修改paint()方法,在未開始遊戲的時候,就要繪製小鳥了:

// 根據狀態,繪製不同的圖案
    if (state == 0) {// 遊戲未開始
      // 遊戲未開始時,繪製開始圖案及小鳥
      g.drawImage(imgStart, 0, 0, null);
      g.drawImage(bird.img, bird.x, bird.y, null);
​
    } else if (state == 1) {// 遊戲開始後
​
    } else if (state == 2) {// 遊戲結束
      // 遊戲結束時,繪製結束圖案
      g.drawImage(imgOver, 0, 0, null);
    }

修改action()方法,在迴圈中,除了移動地面外,還要讓小鳥扇動翅膀:

// 通過監聽滑鼠事件,監聽到state的變化,無限迴圈,不斷切換狀態
    while (true) {
      // 地面移動
      ground.move(bg);
      bird.fly();
​
      // 執行緒休眠(因為是無限迴圈,下一次迴圈開始需要一段休息時間,這樣才能讓程式有緩衝的執行時間)
      try {
        Thread.sleep(1000 / speed);// 調節遊戲速度
        // 重新繪製(重新呼叫皮膚paint方法)
        this.repaint();
      } catch (Exception e) {
        e.printStackTrace();
      }
    }

執行結果:


4、遊戲開始

遊戲開始後,小鳥就要移動了。還要新增上下兩根柱子,柱子向左側移動,通過點選滑鼠,讓小鳥上下移動,來躲避柱子。

小鳥上下飛動

我們現在實現小鳥的部分:

遊戲開始時,小鳥在距離螢幕左側120左右的位置就可以:

在Constant類中定義,遊戲開始時小鳥的位置:

  public static int BIRD_FLY_POSITION_X = 120;// 小鳥開始飛翔時初始座標

首先修改action()中,case 0裡,先修改小鳥的位置

public void action() {
    // 設定滑鼠監聽
    this.addMouseListener(new MouseAdapter() {
      // 點選滑鼠後
      @Override
      public void mouseReleased(MouseEvent e) {
        super.mouseReleased(e);
        switch (state) {
        case 0:// 遊戲未開始
            // 切換到狀態1時的資料
          state = 1;
          bird.x = Constant.BIRD_FLY_POSITION_X;// 小鳥飛的初始x座標
          break;
          .....
}

然後在paint()方法中,state如果為1,代表遊戲開始,應該繪製小鳥和兩根柱子,我們先畫小鳥:

else if (state == 1) {// 遊戲開始後
      //繪製小鳥和兩根柱子
      g.drawImage(bird.img, bird.x, bird.y, null);
​
    }

執行效果:

小鳥預設會向下掉,要考慮重力加速度。當點選滑鼠的時候,會向上移動。

先在Constant提供常量:

public static double GRAVITATIONAL_ACCELERATION = 9.8;
public static double DOWN_TIME = 0.18; // 小鳥自然下降的時長


在小鳥Bird類中,提供一些變數:

public double g = Constant.GRAVITATIONAL_ACCELERATION; // 重力加速度
  public double v = 0;// 下降速度
  public double t = Constant.DOWN_TIME;// 下降時間
  public double h;// 下降的距離

再新增兩個方法:down()表示下降

// 小鳥自然下降
  public void down() {
    v = v - g * t; // 末速度Vt=Vo-gt
    h = v * t - g * t * t / 2; // 位移h=Vot-gt²/2
    y = y - (int) h;
  }

然後修改action()方法:

while (true) {
      // 地面移動
      ground.move(bg);
      bird.fly();
      if(state == 0){
        
      }else if(state == 1){
        bird.down();//小鳥下降
      }else if(state == 2){
        
      }
      
      // 執行緒休眠(因為是無限迴圈,下一次迴圈開始需要一段休息時間,這樣才能讓程式有緩衝的執行時間)
      try {
        Thread.sleep(1000 / speed);// 調節遊戲速度
        // 重新繪製(重新呼叫皮膚paint方法)
        this.repaint();
      } catch (Exception e) {
        e.printStackTrace();
      }
    }

執行結果:


當點選滑鼠時,小鳥會向上飛,每次向上20。先在Constant中新增一個常量:

public static double UP_SPEED = 20;// 上升的速度

在Bird中,再新增一個方法:

// 上升,點滑鼠或點鍵盤向上鍵
  public void up() {
    v = Constant.UP_SPEED;
  }

修改action(),在滑鼠抬起的事件中,如果state為1,那麼要呼叫up()方法,讓小鳥上升:

 

...
public void mouseReleased(MouseEvent e) {
        super.mouseReleased(e);
        switch (state) {
        case 0:// 遊戲未開始
            // 切換到狀態1時的資料
          state = 1;
          bird.x = Constant.BIRD_FLY_POSITION_X;// 小鳥飛的初始x座標
          break;
​
        case 1:// 開始遊戲
          // 當狀態1時,小鳥點選向上移動
          bird.up();
          break;
...

柱子左右移動

首先在pics下放圖片資源pillar.png。然後在Constant類中,新增常量:

// 柱子引數
  public static String PATH_PILLAR = PATH_PIC + "pillar.png";
  public static int PILLAR_GAP = 144;// 柱子通道距離
  public static int PILLAR_DISTANCE = 244;// 柱子間距

然後建立柱子類Pillar類:

package com.ruby.demo;
​
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.Random;
​
import javax.imageio.ImageIO;
​
/**
 * 柱子類 其中構造方法中需要背景物件和地面物件
 * 
 * @author ruby
 *
 */
public class Pillar {
  public BufferedImage img;// 圖片
  public int x, y;// 座標
  public int width = 0;// 柱子寬度
  public int height = 0;// 柱子高度
  Random random = new Random();// 一個生成隨機數的物件
  private int max, min = 0;// 為了保證柱子通道能夠完全顯示在螢幕上,所以存在柱子在Y座標的最大值和最小值
​
  public Pillar(BackGround bg, Ground ground) {
    try {
      img = ImageIO.read(getClass().getResource(Constant.PATH_PILLAR));
      width = img.getWidth();
      height = img.getHeight();
      System.out.println("柱子width=" + width + ",height=" + height);
      x = bg.width;
​
      max = (height - Constant.PILLAR_GAP) / 2;
      min = (height - Constant.PILLAR_GAP) / 2 - (bg.height - Constant.PILLAR_GAP - ground.height);
      y = -(min + random.nextInt(max - min));
      // System.out.println("y=" + y);
​
    } catch (IOException e) {
      e.printStackTrace();
    }
  }
​
​
​
}
​

小鳥闖關的柱子,每隔244間距,就要再產生一根柱子。柱子的高度通過隨機數來產生,先計算出柱子的高度:柱子圖片高度-柱子通道距離114,然後除以2。柱子的最小高度,就是柱子的高度減去背景圖高度-地面高度-通道距離。

柱子的y座標,就因該是柱子的高度和柱子最小高度之間的隨機數。

再新增一個移動柱子的方法:

  // 柱子移動,遊戲一旦開始則柱子移動
  public void move(BackGround bg) {
    x--;
    if (x == -width) {
      x = bg.width;
      y = -(min + random.nextInt(max - min));
      // System.out.println("y=" + y);
    }
  }

 

然後修改GamePanel類,建立2個柱子物件,因為遊戲介面中,最多出現2根柱子。然後再構造方法中,例項化兩個柱子物件,並設定x座標,柱子是從遊戲介面右側,移入到遊戲介面上,所以第一個柱子的x值為遊戲介面的寬度,第二個柱子要再加柱子間距。

// 宣告兩個柱子,並分別設定柱子的起始X座標
    p1 = new Pillar(bg, ground);
    p1.x = bg.width;
    p2 = new Pillar(bg, ground);
    p2.x = bg.width + Constant.PILLAR_DISTANCE;
​

修改paint()方法,遊戲開始後,繪製柱子:

 else if (state == 1) {// 遊戲開始後
      // 繪製小鳥和兩根柱子
      g.drawImage(bird.img, bird.x, bird.y, null);
      g.drawImage(p1.img, p1.x, p1.y, null);
      g.drawImage(p2.img, p2.x, p2.y, null);
​
    } 


然後再action()的迴圈裡,呼叫兩個柱子的移動方法:

while (true) {
      // 地面移動
      ground.move(bg);
      bird.fly();
      if (state == 0) {
​
      } else if (state == 1) {
        // 遊戲開始。地面移動、柱子移動、小鳥飛並自然下降
        bird.down();
        p1.move(bg);
        p2.move(bg);
      } else if (state == 2) {
        
      }
​
      // 執行緒休眠(因為是無限迴圈,下一次迴圈開始需要一段休息時間,這樣才能讓程式有緩衝的執行時間)
      try {
        Thread.sleep(1000 / speed);// 調節遊戲速度
        // 重新繪製(重新呼叫皮膚paint方法)
        this.repaint();
      } catch (Exception e) {
        e.printStackTrace();
      }
    }

執行結果:

5、計算得分

然後在Constant類中,新增和得分相關的常量:

// 得分資訊的字型大小及座標
  public static int FONT_SIZE = 20;
  public static int SCORE_X = 20;
  public static int SCORE_Y = 40;

在GamePanel中定義score,表示分數,然後在paint()方法中,繪製分數:

// 繪製文字
    Font font = new Font(Font.SERIF, Font.ITALIC, Constant.FONT_SIZE);//字型,傾斜,大小
    g.setFont(font);
    g.setColor(Color.white);// 這裡font和color導包都導java.awt
    g.drawString("得分:" + score, Constant.SCORE_X, Constant.SCORE_Y);

 

要想統計分數,得先計算小鳥的各種碰撞,首先在小鳥的類中,新增一個是否碰撞地面的方法,其實就是檢測小鳥y的值:

// 碰撞檢測
  // 掉落到地面時
  public boolean hitGround(BackGround bg, Ground ground) {
    if (y + height >= (bg.height - ground.height)) {
      return true;
    }
    return false;
  }

再新增一個檢測是否碰撞天空的方法,就是遊戲介面的頂部:

// 碰撞到舞臺頂部時
  public boolean hitSky() {
    if (y <= 0) {
      return true;
    }
    return false;
  }

再新增一個檢測碰撞柱子的方法:

// 碰到柱子時的檢測
  public boolean hitPillar(Pillar p) {
    // x方向小鳥和柱子碰撞的條件
    if ((x + width) >= p.x && x <= p.x + p.width) {
      if (y <= p.y + (p.height - Constant.PILLAR_GAP) / 2
          || y >= p.y + (p.height + Constant.PILLAR_GAP) / 2 - height) {
        return true;
      }
    }
    return false;
  }

要判斷小鳥的橫向上,碰撞柱子。

在小鳥類裡新增一個方法:

// 碰到柱子時的檢測
  public boolean hitPillar(Pillar p) {
    // x方向小鳥和柱子碰撞的條件
    if ((x + width) >= p.x && x <= p.x + p.width) {
      if (y <= p.y + (p.height - Constant.PILLAR_GAP) / 2
          || y >= p.y + (p.height + Constant.PILLAR_GAP) / 2 - height) {
        return true;
      }
    }
    return false;
  }

再新增一個積分的方法:

// 增加積分,通過柱子通道後呼叫該方法
  public boolean addScore(Pillar p) {
    // System.out.println("x=" + x + ",p.x=" + p.x);
    if (x == p.x + p.width) {
      return true;
    }
    return false;
  }

然後在GamePanel類中修改action()方法:

while (true) {
      if (state == 0) {
        // 遊戲未開始。地面移動,小鳥展翅
        ground.move(bg);
        bird.fly();
      } else if (state == 1) {
        // 遊戲開始。地面移動、柱子移動、小鳥飛並自然下降
        ground.move(bg);
        p1.move(bg);
        p2.move(bg);
        bird.fly();
        bird.down();
        // 碰到地面、天空、柱子都顯示遊戲結束。
        if (bird.hitGround(bg, ground) || bird.hitSky() || bird.hitPillar(p1) || bird.hitPillar(p2)) {
          state = 2;
        } else {
          // 小鳥每通過一個竹子通道,累計積分,並提高柱子和地面移動速度。
          if (bird.addScore(p1) || bird.addScore(p2)) {
            score++;
            // 每通過一個柱子,速度會遞增
            speed += 2;
            // System.out.println("speed=" + speed);
          }
        }
      }

然後修改滑鼠事件的監聽:

public void mousePressed(MouseEvent e) {
        // TODO Auto-generated method stub
        super.mousePressed(e);
        switch (state) {
        case 0:
          // 切換到狀態1時的資料
          state = 1;
          bird.x = Constant.BIRD_FLY_POSITION_X;// 小鳥飛的初始x座標
          musicThread = new MusicThread();
          musicThread.start();
          
          break;
        case 1:
          // 當狀態1時,小鳥點選向上移動
          bird.up();
          break;
        case 2:
          // 切換到狀態0時的資料
          musicThread.stopBGM();
          state = 0;
          score = 0;
          // 重置小鳥的位置
          bird.x = Constant.BIRD_POSITION_X;
          bird.y = Constant.BIRD_POSITION_Y;
          bird.v = 0;
          // 重置柱子的座標
          p1.x = bg.width;
          p2.x = bg.width + Constant.PILLAR_DISTANCE;
          // System.out.println("p1==" + p1 + ", p2==" + p2);
          break;
        default:
          break;
        }
      }

當遊戲結束的時候,要初始化小鳥和柱子的資料。


6、新增小鳥的鍵盤事件

在Constant中新增常量:

public static int DISTANCE_PER_PRESS = 10;// 每點一次滑鼠或鍵盤,移動的位置

然後在小鳥類中,新增鍵盤的上下左右事件方法:

// 後退,點鍵盤向左鍵
    public void backward() {
      x -= Constant.DISTANCE_PER_PRESS;
    }
​
    // 前進,點鍵盤向右鍵
    public void foward() {
      x += Constant.DISTANCE_PER_PRESS;
    }
​
    // 點選鍵盤下降,點鍵盤向下鍵
    public void pressdown() {
      y += Constant.DISTANCE_PER_PRESS;
    }

然後在action()中,新增滑鼠事件監聽:

// 設定鍵盤監聽事件
    this.addKeyListener(new KeyAdapter() {
      @Override
      public void keyPressed(KeyEvent e) {
        super.keyPressed(e);
        switch (e.getKeyCode()) {
        case KeyEvent.VK_UP:
          bird.up();
          break;
        case KeyEvent.VK_RIGHT:
          bird.foward();
          break;
        case KeyEvent.VK_LEFT:
          bird.backward();
          break;
        case KeyEvent.VK_DOWN:
          bird.pressdown();
          break;
        }
      }
    });

注意,最後要在GameFrame中,可以響應鍵盤事件:

// 讓該Frame中的panel聚焦,可以響應鍵盤事件
    panel.requestFocus();

7、新增背景音樂

先準備一首背景音樂,然後在src上建立一個音樂的資源目錄music,並將音樂檔案拷貝進去:

 

然後倒入音訊播放的jar包:

在Constant中新增常量:

//音樂路徑
  public static String PATH_MUSIC = "/music/";
  
  public static String PATH_BGM = PATH_MUSIC + "Ari_Pulkkinen-Funky_Theme.mp3";//music/Ari Pulkkinen-Funky Theme.mp3
​

然後建立一個執行緒類,播放音樂,再提供一個停止播放的方法:

package com.ruby.demo;
​
import java.io.InputStream;
​
import javazoom.jl.decoder.JavaLayerException;
import javazoom.jl.player.Player;
​
// 播放音樂
class MusicThread extends Thread {
  Player player = null;
  
  @Override
  public void run() {
    // 繼承執行緒類後,重寫run方法,播放音樂的程式碼
    // 1.載入音訊檔案得到輸入流
    InputStream inputStream = this.getClass().getResourceAsStream(Constant.PATH_BGM);
    try {
      // 2.建立Player物件,播放音樂
      player = new Player(inputStream);
      player.play();
      
​
    } catch (JavaLayerException e) {
      e.printStackTrace();
    }
  }
  
  
  public void stopBGM(){
    if(player != null){
      player.close();
    }
  }
}

 

然後在GamePanel裡建立MusicThread物件,用於播放和停止音樂,在action()中修改程式碼:

this.addMouseListener(new MouseAdapter() {
      // 點選滑鼠後
      @Override
      public void mouseReleased(MouseEvent e) {
        super.mouseReleased(e);
        switch (state) {
        case 0:// 遊戲未開始
            // 切換到狀態1時的資料
          state = 1;
          bird.x = Constant.BIRD_FLY_POSITION_X;// 小鳥飛的初始x座標
​
          musicThread = new MusicThread();
          musicThread.start();
          break;
​
        case 1:// 開始遊戲
            // 當狀態1時,小鳥點選向上移動
          bird.up();
          break;
​
        case 2:// 遊戲結束
            // 遊戲結束後,更改狀態為0,可以繼續下一次遊戲
          musicThread.stopBGM();
          state = 0;
​
          score = 0;
          // 重置小鳥的位置
          bird.x = Constant.BIRD_POSITION_X;
          bird.y = Constant.BIRD_POSITION_Y;
          bird.v = 0;
          // 重置柱子的座標
          p1.x = bg.width;
          p2.x = bg.width + Constant.PILLAR_DISTANCE;
          break;
​
        default:
          break;
        }
​
      }
    });


在state為0時,開始播放音樂,當時遊戲結束state為2時,停止音樂。

相關文章