Java遊戲開發——flappy bird
遊戲介紹
在《FlappyBird》這款遊戲中,玩家滑鼠點選螢幕,小鳥就會往上飛,不斷的點選就會不斷的往高處飛。不點選的話則會快速下降。所以玩家要控制小鳥一直向前飛行,然後注意躲避途中高低不平的管子。
1、在遊戲開始後,滑鼠點選螢幕,要記住是有間歇的點選螢幕,不要讓小鳥掉下來。
2、儘量保持平和的心情,點的時候不要下手太重,儘量注視著小鳥。
3、遊戲的得分是,小鳥安全穿過一個柱子且不撞上就是1分。撞上柱子就直接掛掉,只有一條命。
本篇博文開發了一個《flappy bird》遊戲,執行效果如下:
使用素材資料夾:
素材及完整工程連結:https://pan.baidu.com/s/1D1eCMNzVrVl8XeOz6vDSkg 提取碼: xbc1
遊戲設計思路
使用場景相對小鳥移動的過程間接實現小鳥在水平方向的位移,小鳥實際上只在垂直方向上進行了位置的改變,呼叫執行緒,每次迴圈使小鳥的y值自動增加以達到重力效果,玩家點選滑鼠按鍵時,減少小鳥y軸座標以達到跳躍效果,當小鳥位於某根水管中間時,判斷小鳥是否與該水管的上側或者下側發生了碰撞,如果沒有,當小鳥的x座標>水管左上角x座標+水管寬度時,分數+1;如果發生了碰撞,遊戲結束。
遊戲具體實現
Ⅰ資訊的儲存
遊戲使用兩張背景圖片平鋪的形式達到背景迴圈效果,需要使用backgroundX0和backgroundX1兩個變數記錄背景1和背景2兩張圖片左上角的x座標,使用birdX和birdY記錄小鳥左上角的x座標和y座標,使用barXArrays陣列記錄各個水管左上角的x座標,使用barUpArrays陣列記錄各個水管上半部分底部的y座標,使用barDownArrays陣列記錄各個水管下半部分頂部的y座標,使用score變數記錄分數,使用width和height變數記錄螢幕長寬,使用nowStep表示當前跳躍狀態y值改變的大小,flag表示小鳥是否在跳躍中:
Image[] pics = new Image[5];//儲存圖片 int birdX;//小鳥左上角x,y座標 int birdY; int width;//螢幕長寬 int height; int backgroundX0 = 0;//背景1的x軸起點 int backgroundX1 = 750;//背景2的x軸起點 int nowStep = 40;//當前跳躍狀態改變的y值大小 int flag = 0;//是否跳躍中 int score = 0;//記錄分數 int[] barXArrays = new int[5];//記錄各個水管左上角的x座標 int[] barUpArrays = new int[5];//記錄各個水管上半部分底部的y座標 int[] barDownArrays = new int[5];//記錄各個水管下半部分頂部的y座標
Ⅱ資訊初始化
初始時backgroundX0的值為0,backgroundX1的值為width,小鳥垂直位於螢幕中間,水平方向靠左1/3處。初始化水管x值位於螢幕右側,每隔400畫素出現一根水管,水管寬度為100畫素,每根水管上半部分底部的y值位於150~350之間,每根水管下半部分頂部的y值是上半部分底部的y值+250,小鳥初始狀態未跳躍,分數為0:
public GamePanel(){
width = 750;
height = 750;
birdX = width/3-50;//小鳥始終位於螢幕左邊1/3處
birdY = height/2;//只通過重力去改變小鳥縱座標
this.setPreferredSize(new Dimension(width,height));
this.setVisible(true);
getPics();
initData();
this.addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
flag = 1;
repaint();
}
});
new Thread(this).start();
}
private void initData(){
for(int i=0;i<5;i++){
int barUp = (int) (Math.random()*200+150);//barUp取值範圍是150~350
int barDown = barUp+250;//barDown取值範圍是450~600
barXArrays[i] = width+400*i;
barUpArrays[i] = barUp;
barDownArrays[i] = barDown;
// System.out.println(barXArrays[i]+","+barUpArrays[i]+","+barDownArrays[i]);
}
}
Ⅲ場景相對位移
執行緒每次迴圈,背景1和背景2兩圖片左上角x座標減去10,當背景1或者背景2的左上角x值為-width時(該背景完全位於螢幕左側),將x值賦值為width,達到背景迴圈輪播的效果;執行緒每次迴圈還要對每根水管左上角的x值進行減去10的操作,如果某根水管完全位於螢幕左側時,獲取當前場景最後一根水管的陣列下標值,根據最後一根水管的x值,在新的位置生成新的一根水管:
@Override
public void run() {
while(true){
try {
backgroundX0-=10;
backgroundX1-=10;
省略...
for(int i=0;i<5;i++){
barXArrays[i]-=10;
省略...
if(barXArrays[i]<-100){
int index = i-1<0?4:i-1;//獲取當前位於最後的一根水管的下標
int barUp = (int) (Math.random()*200+150);//barUp取值範圍是150~350
int barDown = barUp+250;//barDown取值範圍是450~600
barXArrays[i] = barXArrays[index]+400;
barUpArrays[i] = barUp;
barDownArrays[i] = barDown;
}
}
repaint();
省略...
if(backgroundX0==-width){
backgroundX0=width;
}
if(backgroundX1==-width){
backgroundX1=width;
}
Thread.sleep(50);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
Ⅳ小鳥的跳躍
為了增加小鳥跳躍的流暢性,可以將小鳥的跳躍過程分為多幀數處理,即每幀向上跳一定距離nowStep,nowStep逐漸減小,然後跟重力效果抵消,當nowStep為0時,跳躍狀態結束;在這裡可以為小鳥新增一個標記flag,表示小鳥是否在跳躍過程中;當玩家點選滑鼠按鍵時,flag=1;當跳躍狀態結束時,flag=0並將nowStep的值初始化;執行緒每次迴圈將增加小鳥的y值;
//滑鼠監聽時間,有滑鼠按鍵時跳躍
this.addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
flag = 1;
repaint();
}
});
@Override
public void run() {
while(true){
try {
省略...
if(flag==1){//如果是跳躍過程中
if(birdY>=0){//沒有觸碰到遊戲螢幕頂部
birdY-=nowStep;
}
nowStep-=4;
if(nowStep==0){
nowStep=40;
flag = 0;
}
}
省略...
Thread.sleep(50);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
Ⅴ碰撞檢測
迴圈遍歷水管左上角x座標陣列,如果小鳥左上角x值+小鳥寬度>某根水管左上角x值 並且 小鳥左上角x值+小鳥寬度<該根水管左上角x值+水管寬度,說明小鳥位於該根水管的中間,可能發生碰撞;如果前面兩個條件滿足,再判斷小鳥左上角y值是否小於該水管上半部分底部y值 或者 小鳥左上角y值+小鳥高度是否大於該水管下半部分頂部的y值,如果是,說明發生了碰撞;簡單說,碰撞檢測需要滿足下列條件:
①小鳥左上角x值+小鳥寬度>某根水管左上角x值
②小鳥左上角x值+小鳥寬度<該根水管左上角x值+水管寬度
③小鳥左上角y值<該水管上半部分底部y值 或者 小鳥左上角y值+小鳥高度>該水管下半部分頂部的y值
for(int i=0;i<5;i++){
if(birdX>barXArrays[i]-50&&birdX<barXArrays[i]+100&&(birdY<=barUpArrays[i]||birdY+50>=barDownArrays[i])){//碰撞檢測
int best = GameClient.helpPanel.getRecord();
int choice;
if(score>best){
GameClient.helpPanel.writeRecord(score);
choice = JOptionPane.showConfirmDialog(null, "你的分數是"+score+",更新了歷史記錄"+best+"\n是否重新開始?","遊戲結束",JOptionPane.YES_NO_OPTION);//獲取使用者選擇
}else{
choice = JOptionPane.showConfirmDialog(null, "你的分數是"+score+"是否重新開始?","遊戲結束",JOptionPane.YES_NO_OPTION);//獲取使用者選擇
}
if(choice==1){//否
System.exit(0);//退出
}else if(choice == 0){//是,重置遊戲資料
initData();
birdY = height/2;
flag = 0;
score = 0;
nowStep = 40;
backgroundX0 = 0;
backgroundX1 = width;
GameClient.helpPanel.getRecord();
GameClient.helpPanel.setScore(score);
}
break;
}
}
Ⅵ圖片的獲取及顯示
圖形化程式設計基礎不多解釋。。。
private void getPics() {
for(int i=0;i<4;i++){
pics[i] = Toolkit.getDefaultToolkit().getImage("D://Game//FlappyBirdGame//pic"+i+".png");
}
}
public void paint(Graphics g){
g.clearRect(0, 0, width, height);//清屏
g.drawImage(pics[0],backgroundX0,0,width,height,this);//背景1
g.drawImage(pics[0],backgroundX1,0,width,height,this);//背景2
g.drawImage(pics[3],birdX,birdY,50,50,this);
for(int i=0;i<5;i++){//畫水管
g.drawImage(pics[1], barXArrays[i], 0, 100, barUpArrays[i], this);
g.drawImage(pics[2], barXArrays[i],barDownArrays[i], 100,height-barDownArrays[i],this);
}
}
Ⅶ歷史記錄讀取及更新
常用IO操作,如果不存在則新建歷史記錄文字;
//獲取歷史記錄
public int getRecord(){
File file = new File("D://GameRecordAboutSwing");
if(!file.exists()){
file.mkdirs();
}
File record = new File("D://GameRecordAboutSwing//flappyBirdGame.txt");
try{
if(!record.exists()){//如果不存在,新建文字
record.createNewFile();
fos = new FileOutputStream(record);
dos = new DataOutputStream(fos);
String s = "0";
dos.writeBytes(s);
System.out.println(record.isFile());;
}
//讀取記錄
fis = new FileInputStream(record);
dis = new DataInputStream(fis);
String str = dis.readLine();
best = Integer.parseInt(str);
bestLabel.setText(""+best);
}catch(Exception e){
e.printStackTrace();
}finally{
try {
if(fis!=null)
fis.close();
if(dis!=null)
dis.close();
if(fos!=null)
fos.close();
if(dos!=null)
dos.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
return best;
}
//更新關卡歷史記錄
public void writeRecord(int score){
File record = new File("D://GameRecordAboutSwing//flappyBirdGame.txt");
try {
//清空原有記錄
FileWriter fileWriter =new FileWriter(record);
fileWriter.write("");
fileWriter.flush();
fileWriter.close();
//重新寫入文字
fos = new FileOutputStream(record);
dos = new DataOutputStream(fos);
String s = score+"";
dos.writeBytes(s);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}finally{
try {
if(fos!=null)
fos.close();
if(dos!=null)
dos.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
到這遊戲的主要實現步驟已經介紹完了,完整原始碼篇幅不多,這次貼下,自己實現的話素材需自備:
GamePanel類:
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.Toolkit;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
public class GamePanel extends JPanel implements Runnable{
Image[] pics = new Image[5];//儲存圖片
int birdX;//小鳥左上角x,y座標
int birdY;
int width;//螢幕長寬
int height;
int backgroundX0 = 0;//背景1的x軸起點
int backgroundX1 = 750;//背景2的x軸起點
int nowStep = 40;//當前跳躍狀態改變的y值大小
int flag = 0;//是否跳躍中
int score = 0;//記錄分數
int[] barXArrays = new int[5];//記錄各個水管左上角的x座標
int[] barUpArrays = new int[5];//記錄各個水管上半部分底部的y座標
int[] barDownArrays = new int[5];//記錄各個水管下半部分頂部的y座標
public GamePanel(){
width = 750;
height = 750;
birdX = width/3-50;//小鳥始終位於螢幕左邊1/3處
birdY = height/2;//只通過重力去改變小鳥縱座標
this.setPreferredSize(new Dimension(width,height));
this.setVisible(true);
getPics();
initData();
this.addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
flag = 1;
repaint();
}
});
new Thread(this).start();
}
private void initData(){
for(int i=0;i<5;i++){
int barUp = (int) (Math.random()*200+150);//barUp取值範圍是150~350
int barDown = barUp+250;//barDown取值範圍是450~600
barXArrays[i] = width+400*i;
barUpArrays[i] = barUp;
barDownArrays[i] = barDown;
// System.out.println(barXArrays[i]+","+barUpArrays[i]+","+barDownArrays[i]);
}
}
private void getPics() {
for(int i=0;i<4;i++){
pics[i] = Toolkit.getDefaultToolkit().getImage("D://Game//FlappyBirdGame//pic"+i+".png");
}
}
public void paint(Graphics g){
g.clearRect(0, 0, width, height);//清屏
g.drawImage(pics[0],backgroundX0,0,width,height,this);//背景1
g.drawImage(pics[0],backgroundX1,0,width,height,this);//背景2
g.drawImage(pics[3],birdX,birdY,50,50,this);
for(int i=0;i<5;i++){//畫水管
g.drawImage(pics[1], barXArrays[i], 0, 100, barUpArrays[i], this);
g.drawImage(pics[2], barXArrays[i],barDownArrays[i], 100,height-barDownArrays[i],this);
}
}
@Override
public void run() {
while(true){
try {
backgroundX0-=10;
backgroundX1-=10;
birdY+=10;
for(int i=0;i<5;i++){
barXArrays[i]-=10;
if(barXArrays[i]+100>birdX-5&&barXArrays[i]+100<=birdX+5){
score++;
GameClient.helpPanel.setScore(score);
}
if(barXArrays[i]<-100){
int index = i-1<0?4:i-1;//獲取當前位於最後的一根水管的下標
int barUp = (int) (Math.random()*200+150);//barUp取值範圍是150~350
int barDown = barUp+250;//barDown取值範圍是450~600
barXArrays[i] = barXArrays[index]+400;
barUpArrays[i] = barUp;
barDownArrays[i] = barDown;
}
}
repaint();
for(int i=0;i<5;i++){
if(birdX>barXArrays[i]-50&&birdX<barXArrays[i]+100&&(birdY<=barUpArrays[i]||birdY+50>=barDownArrays[i])){//碰撞檢測
int best = GameClient.helpPanel.getRecord();
int choice;
if(score>best){
GameClient.helpPanel.writeRecord(score);
choice = JOptionPane.showConfirmDialog(null, "你的分數是"+score+",更新了歷史記錄"+best+"\n是否重新開始?","遊戲結束",JOptionPane.YES_NO_OPTION);//獲取使用者選擇
}else{
choice = JOptionPane.showConfirmDialog(null, "你的分數是"+score+"是否重新開始?","遊戲結束",JOptionPane.YES_NO_OPTION);//獲取使用者選擇
}
if(choice==1){//否
System.exit(0);//退出
}else if(choice == 0){//是,重置遊戲資料
initData();
birdY = height/2;
flag = 0;
score = 0;
nowStep = 40;
backgroundX0 = 0;
backgroundX1 = width;
GameClient.helpPanel.getRecord();
GameClient.helpPanel.setScore(score);
}
break;
}
}
if(flag==1){
if(birdY>=0){
birdY-=nowStep;
}
nowStep-=4;
if(nowStep==0){
nowStep=40;
flag = 0;
}
}
if(backgroundX0==-width){
backgroundX0=width;
}
if(backgroundX1==-width){
backgroundX1=width;
}
Thread.sleep(50);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
HelpPanel類:
import java.awt.Color;
import java.awt.Dimension;
import java.awt.GridLayout;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import javax.swing.*;
//輔助面板
public class HelpPanel extends JPanel{
int score = 0;
int best = 0;
JLabel scoreLabel = new JLabel("0");
JLabel bestLabel = new JLabel("0");
FileInputStream fis = null;
FileOutputStream fos = null;
DataInputStream dis = null;
DataOutputStream dos = null;
public HelpPanel(){
this.setPreferredSize(new Dimension(100,750));
this.setVisible(true);
this.setLayout(new GridLayout(2,2,10,10));
this.add(new JLabel("score:"));
this.add(scoreLabel);
this.add(new JLabel("best:"));
this.add(bestLabel);
getRecord();
}
public void setScore(int score){
this.score = score;
scoreLabel.setText(score+"");
}
//獲取歷史記錄
public int getRecord(){
File file = new File("D://GameRecordAboutSwing");
if(!file.exists()){
file.mkdirs();
}
File record = new File("D://GameRecordAboutSwing//flappyBirdGame.txt");
try{
if(!record.exists()){//如果不存在,新建文字
record.createNewFile();
fos = new FileOutputStream(record);
dos = new DataOutputStream(fos);
String s = "0";
dos.writeBytes(s);
System.out.println(record.isFile());;
}
//讀取記錄
fis = new FileInputStream(record);
dis = new DataInputStream(fis);
String str = dis.readLine();
best = Integer.parseInt(str);
bestLabel.setText(""+best);
}catch(Exception e){
e.printStackTrace();
}finally{
try {
if(fis!=null)
fis.close();
if(dis!=null)
dis.close();
if(fos!=null)
fos.close();
if(dos!=null)
dos.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
return best;
}
//更新關卡歷史記錄
public void writeRecord(int score){
File record = new File("D://GameRecordAboutSwing//flappyBirdGame.txt");
try {
//清空原有記錄
FileWriter fileWriter =new FileWriter(record);
fileWriter.write("");
fileWriter.flush();
fileWriter.close();
//重新寫入文字
fos = new FileOutputStream(record);
dos = new DataOutputStream(fos);
String s = score+"";
dos.writeBytes(s);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}finally{
try {
if(fos!=null)
fos.close();
if(dos!=null)
dos.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
GameClient類:
import java.awt.BorderLayout;
import java.awt.Container;
import javax.swing.JFrame;
import javax.swing.JPanel;
public class GameClient extends JFrame{
static HelpPanel helpPanel;
public GameClient(){
GamePanel gamePanel = new GamePanel();//例項化主面板物件
helpPanel = new HelpPanel();//例項化輔助面板物件
Container container = this.getContentPane();//獲取窗體內建容器
container.setLayout(new BorderLayout());//設定佈局
container.add(gamePanel,BorderLayout.CENTER);//新增遊戲主面板到內建容器
container.add(helpPanel,BorderLayout.EAST);//新增遊戲輔助面板到內建容器
this.setSize(850,750);//設定窗體大小
pack();
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);//當用戶點選窗體右上角的x時,自動退出程式
this.setTitle("flappy bird");//設定窗體標題
this.setLocationRelativeTo(null);//讓窗體顯示在螢幕正中間
this.setVisible(true);//展示窗體
gamePanel.requestFocus();
}
public static void main(String[] args) {
new GameClient();
}
}