這是一篇學習分享博客,這篇博客將會介紹以下幾項內容:
1、如何讓一個程序同時做多件事?(多線程的創建、多線程的應用)
2、如何讓小球在畫面中真實地動起來?(賦予小球勻速直線、自由落體、上拋等向量運動)
3、多線程遊戲仿真實例分享(飛機大戰、接豆人、雙線挑戰三個遊戲實例)
- 涉及的知識點有:多線程的應用、雙緩衝繪圖、小球的向量運動、遊戲的邏輯判斷、鍵盤監聽器的使用、二維數組的使用、添加音樂效果等
遊戲效果:
怎麼樣?如果覺得還不錯的話就請繼續看下去吧!
熱身
第一步:創建畫布
- 心急吃不了熱豆腐,我們先從最簡單的創建畫布開始。
首先我們創建一個窗體,然後設置一些參數,從窗體中取得畫筆,嘗試在畫布中心畫一個圖形,以下是參考代碼:
import java.awt.FlowLayout;
import java.awt.Graphics;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JButton;
import javax.swing.JFrame;
public class Frame {
//聲明畫布對象
public Graphics g;
//主函數
public static void main(String[] args) {
//創建Frame類,然後運行showFrame函數
Frame fr=new Frame();
fr.showFrame();
}
//編寫窗體顯示的函數
public void showFrame(){
//創建窗體
JFrame jf=new JFrame();
jf.setTitle("小球演示");//設置窗體標題
jf.setSize(900,900);//設置窗體大小
jf.setDefaultCloseOperation(3);//設置點擊窗體右上角的叉叉後做什麼操作,這裏的3代表點擊叉叉後關閉程序
jf.setLocationRelativeTo(null);//設置窗體居中顯示
FlowLayout flow=new FlowLayout();//設置窗體佈局爲流式佈局
jf.setLayout(flow);
Mouse mou=new Mouse();//創建監聽器對象
JButton jbu=new JButton("START");//創建按鈕,按下按鈕後可以在畫布中間畫一個圓
jbu.addActionListener(mou);//爲按鈕添加事件監聽器
jf.add(jbu);
//設置窗體可見
jf.setVisible(true);
//從窗體獲取畫布
g=jf.getGraphics();
}
//創建內部類監聽器(也可以重新創建一個文件編寫該類)
class Mouse implements ActionListener{
//重寫按鈕監聽方法
public void actionPerformed(ActionEvent e){
//按下按鈕後會執行這裏的代碼,下面這條代碼指的是在畫布中心畫一個圓
g.fillOval(300,300,300,300);
}
}
}
- 我們可以試着運行一下,出現以下圖片所示效果第一步就成功了。
第二步:讓小球動起來
- 用一段循環代碼重複地畫小球,每次循環讓小球偏移一點距離
我們在上述代碼中的監聽器類Mouse的按鈕監聽器方法actionPerformed(ActionEvent e)下加這樣一段代碼
//重複畫100次小球,每次橫縱座標分別加1
for(int i=0;i<100;i++){
g.fillOval(300+i,300+i,30,30);
/*下面這段代碼的意思是每執行一次循環,系統暫停30毫秒,否則畫的
太快我們就觀察不到小球在動了*/
try{
Thread.sleep(30);
}catch(Exception ef){
}
}
- 運行程序並點擊START按鍵後,我們可以看到一個圓往右下角方向緩緩移動了一段距離,並且留下了痕跡。同時我們還可以發現,每次點擊START鍵後,START鍵會保持被按下的狀態,直至整個繪製小球的循環代碼執行結束後纔會彈起。這是因爲我們現在寫的程序只有一個線程在運行,所以只有當前任務執行完後按鈕才能重新接收響應。想要解決這一點,可以利用下面將要講到的多線程的原理。
那麼,熱身結束,下面讓我們一起進入多線程的世界吧!
一、如何讓一個程序同時做多件事情?
創建線程對象
- 創建線程對象我們需要用到Thread類,該類是java.lang包下的一個類,所以調用時不需要導入包。下面我們先創建一個新的子類來繼承Thread類,然後通過重寫run()方法(將需要同時進行的任務寫進run()方法內),來達到讓程序同時做多件事情的目的。
import java.awt.Graphics;
import java.util.Random;
public class ThreadClass extends Thread{
public Graphics g;
//用構造器傳參的辦法將畫布傳入ThreadClass類中
public ThreadClass(Graphics g){
this.g=g;
}
public void run(){
//獲取隨機的x,y座標作爲小球的座標
Random ran=new Random();
int x=ran.nextInt(900);
int y=ran.nextInt(900);
for(int i=0;i<100;i++){
g.fillOval(x+i,y+i,30,30);
try{
Thread.sleep(30);
}catch(Exception ef){
}
}
}
}
- 然後我們在主類的按鈕事件監聽器這邊插入這樣一段代碼,即每按一次按鈕則生成一個ThreadClass對象
public void actionPerformed(ActionEvent e){
ThreadClass thc=new ThreadClass(g);
thc.start();
}
- 在這裏我們生成ThreadClass對象並調用start()函數後,線程被創建並進入準備狀態,每個線程對象都可以同時獨立執行run()方法中的函數,當run()方法中的代碼執行完畢時線程自動停止。
接下來我們試着運行一下吧!
加入清屏功能,讓小球真正的動起來
- 從上面的畫圖示範我們可以看出,小球在移動過程中是留下了軌跡的,那如果我只想看到小球的運動,不想看到小球的軌跡怎麼辦?
- 很簡單,我們只需要每次畫新的小球之前先給整個畫布畫上一個大的背景色矩形,把原來的圖案覆蓋即可。
讓我們試着把run()方法中的代碼改爲下面這樣:
public void run(){
//獲取一個隨機數對象
Random ran=new Random();
//生成一對隨機的x,y座標設爲小球的座標,範圍都在0-399
int x=ran.nextInt(400);
int y=ran.nextInt(400);
for(int i=0;i<100;i++){
//畫一個能夠覆蓋畫面中一塊區域的白色矩形來清屏(把原來的筆跡都覆蓋掉)
g.setColor(Color.white);
g.fillRect(300,300,300,300);
g.setColor(Color.black);
g.fillOval(200+x+i,200+y+i,30,30);
try{
Thread.sleep(30);
}catch(Exception ef){
}
}
}
讓我們試着運行一下
- 我們運行後發現,小球確實在中間的白色矩形區域內實現了不留軌跡的運動,但是小球閃爍的非常厲害。這其中的原因有兩個,一個是多個線程對象同時運行會產生衝突,另一個是IO設備使用頻率過高。後者我們在稍後的部分講到用雙緩衝繪圖去解決,前者則通過下面的方法解決。
- 我們只在主類中創建一次ThreadClass對象。然後再創建一個列表,每次按按鈕時將一組座標存到這個列表中,最後通過run()方法中依次讀出這個列表中的每一項並畫出。
在主類中創建ThreadClass對象並運行(主類的showFrame方法中插入以下代碼)
先創建座標類
public class Location {
public int x;
public int y;
public Location(int x,int y){
this.x=x;
this.y=y;
}
}
然後在主類和ThreadClass類中創建列表
public ArrayList<Location> locs=new ArrayList<Location>();
然後在按鈕監聽器的方法下寫入這段代碼
public void actionPerformed(ActionEvent e){
Random ran=new Random();
int x=ran.nextInt(400);
int y=ran.nextInt(400);
Location loc=new Location(x,y);
locs.add(loc);
System.out.println(locs.size());
}
然後將畫布g和列表locs傳入創建的線程對象中,在主類的showFrame方法插入以下代碼。
ThreadClass thc=new ThreadClass(g,locs);
thc.start();
重載Thread Class的run()方法
public void run(){
while(true){
g.setColor(Color.white);
g.fillRect(300,300,300,300);
for(int i=0;i<locs.size();i++){
g.setColor(Color.black);
//每次給小球座標偏移一下
int x=locs.get(i).x++;
int y=locs.get(i).y++;
g.fillOval(200+x,200+y,30,30);
}
try{
Thread.sleep(30);
}catch(Exception ef){
}
}
}
讓我們再來試一下!
這下小球就沒有閃爍的那麼厲害了。
二、如何讓小球在畫面中真實地動起來?
- 衆所周知,要想描述物體的運動狀態,需要知道物體的三個物理量——位置、速度和加速度。我們只需要找到方法描述這三個物理量,便可以很好的模擬真實小球的運動。
在這裏我們可以創建一個Vector類來描述位置、速度和加速度這三個物理量
public class Vector {
public int x;
public int y;
public Vector(int x,int y){
this.x=x;
this.y=y;
}
//向量的加和運算
public void add(Vector vec){
this.x+=vec.x;
this.y+=vec.y;
}
}
然後我們再創建一個Ball類來代表小球(move函數是本部分的關鍵)
public class Ball {
public Vector location;//位置
public Vector speed;//速度
public Vector acce;//加速度
//構造器傳參,設定小球的基本參數
public Ball(Vector location,Vector speed,Vector acce){
this.location=location;
this.speed=speed;
this.acce=acce;
}
//小球移動,這是整個部分的關鍵!!!每畫完一次小球就調用一次move函數,讓小球依據速度和加速度來改變一次位置
public void move(){
this.speed.x+=acce.x;//每調用一次move函數小球的速度就和加速度做一次加法
this.speed.y+=acce.y;
this.location.x+=speed.x;//每調用一次move函數小球的位置座標就和速度做一次加法
this.location.y+=speed.y;
}
}
有了這兩個類,我們就可以表示任意二維的向量運動了
- 比如說從原點出發,向右速度爲5,向下加速度爲10的平拋運動可以表示爲
Vector location=new Vector(0,0);
Vector speed=new Vector(5,0);
Vector acce=new Vector(10,0);
- 從原點出發,向右速度爲5,向上速度爲10,向下加速度爲10的上拋運動可以表示爲
Vector location=new Vector(0,0);
Vector speed=new Vector(5,10);
Vector acce=new Vector(10,0);
- 利用這個原理,我們已經可以做出一點好玩的東西了!
試想一下,我們可以先給窗體添加一個鼠標監聽器,然後獲取鼠標按下和鬆開的點的座標,然後沿着按下和鬆開的點連成的直線方向丟出一個小球,這樣是不是就可以做一個投籃遊戲了呢。 - 具體操作:給窗體加上鼠標監聽器👉在mousePress函數下獲取鼠標按下的點的座標x1,y1👉在mouseRelease函數下獲取鼠標鬆開的點的座標x2,y2👉生成一個小球對象,以(x2,y2)作爲小球座標,(x2-x1)作爲x方向上的速度,(y2-y1)作爲y方向上的速度,y方向上加速度爲1。然後把這個小球放入到傳入ThreadClass的列表中,讓線程將這個小球畫出。
需要改變的是主類中Mouse類的代碼和ThreadClass類中run方法的代碼
- Mouse類
//創建內部類監聽器(也可以重新創建一個文件編寫該類)
class Mouse implements ActionListener,MouseListener{
int prx=0;
int pry=0;//記錄按下鼠標的點的座標
//重寫按鈕監聽方法
public void actionPerformed(ActionEvent e){
}
public void mouseClicked(MouseEvent e) {
}
public void mousePressed(MouseEvent e) {
prx=e.getX();
pry=e.getY();//獲取按下鼠標的點的座標
}
public void mouseReleased(MouseEvent e) {
int speedx=(int)((e.getX()-prx)/10);
int speedy=(int)((e.getY()-pry)/10);
Vector location=new Vector(e.getX(),e.getY());
Vector speed=new Vector(speedx,speedy);
Vector acce=new Vector(0,1);
Ball ball=new Ball(location,speed,acce);
balls.add(ball);
}
public void mouseEntered(MouseEvent e) {
}
public void mouseExited(MouseEvent e) {
}
}
- ThreadClass下的run方法
public void run(){
while(true){
g.setColor(Color.white);
g.fillRect(300,0,600,900);
for(int i=0;i<balls.size();i++){
g.setColor(Color.black);
g.fillOval(balls.get(i).location.x,balls.get(i).location.y,30,30);
balls.get(i).move();
}
try{
Thread.sleep(30);
}catch(Exception ef){
}
}
}
執行效果如圖所示(爲了顯示小球向量運動的效果,這裏省去了清屏操作)
完整代碼放到這裏:
https://pan.baidu.com/s/10HcOSvuov14moes1jPe9JQ
提取碼:z8ii
三、多線程遊戲仿真實例分享
(三個遊戲的源代碼、圖片素材鏈接在下文中獲取)
遊戲一:飛機大戰
遊戲演示:
Java遊戲製作
遊戲說明:
飛機大戰簡介:
- 飛機大戰整個程序一共用了五個類GameUIFrame(遊戲界面窗體顯示及主程序)、ThreadClass(線程類,進行圖片繪製、生成怪物、判斷碰撞、刷新分數等一系列功能,是程序的主體部分)、FlyObject(創建所有飛行物對象的類,可以定義飛行物的位置、速度、加速度、顯示圖片等)、Vector(上文中介紹過的向量類)以及Listener(鼠標監聽器,負責獲取鼠標在屏幕上的點繪製我方飛船和生成飛船發出的子彈)。
要實現飛機大戰主要完成這幾件事:
- 繪製我方飛機、不斷髮射子彈
- 不斷隨機生成怪物、寶箱
- 判斷子彈與怪物、怪物與我方飛機之間是否碰撞
- 爆炸動效、刷新分數
由於時間關係,目前博主所製作的遊戲暫時只具有以上這些功能,有興趣的夥伴還可以試着增加關卡、Boss、新的怪物(比如會發射子彈的怪物)、劇情等等。
我們在遊戲進行的過程中,難免會生成大量的圖片對象。前面我們講到,當我們需要在屏幕上繪製的圖像過多時會出現卡頓閃屏現象,第二個解決方法就是雙緩衝繪圖,下面我來簡單的介紹一下。
雙緩衝繪圖解決閃屏
- 我們正常畫圖的時候,是從窗體對象中直接獲取Graphics類對象來繪畫,然後每一次把需要畫的圖形傳輸到我們的屏幕上時,都需要佔用一定的輸入輸出(IO)設備通道。所以當我們需要繪製的圖像過多時將導致IO設備使用頻率過高,屏幕就會出現閃屏現象。
- 雙緩衝繪圖就是一次性把所有要畫的對象先畫到內存中,最後再把內存中的圖片用窗體對象中直接獲取Graphics類對象畫出來。
- 打個比方,比如說我要把地上的落葉全部掃進垃圾桶裏,我把地上的樹葉一片一片直接撿到垃圾桶裏,這就是不用雙緩衝繪圖的情況;如果說我有一個垃圾鏟,先把樹葉撿到垃圾鏟裏,然後再一次性倒進垃圾桶,效率是不是高多了?這就有點類似雙緩衝繪圖的原理。
要實現雙緩衝繪圖,首先我們要創建BufferedImage對象,然後從這個緩存對象中獲取畫布:
//創建緩存
BufferedImage bufImg=new BufferedImage(1200,1200,BufferedImage.TYPE_INT_ARGB);
//最後的TYPE_INT_ARGB代表創建的是具有合成整數像素的 8 位 RGBA 顏色分量的圖像,也可以選擇其他類型,詳見Java的API文件
//獲取緩存上的畫布
Graphics bufg=bufImg.getGraphics();
獲取了Graphics bufg後,我們所有的繪圖操作先在bufg上完成,等一輪圖像畫完之後,再把bufg上的圖像畫到原本的Graphics對象中
g.drawImage(bufImg,0,0,null);
飛機大戰製作Step1:飛行物
- 遊戲中真正的飛行物一共只有三種,我方飛機、怪物和子彈(均需要在創建FlyObject對象時設定位置、速度、加速度、圖片、血量)。但是因爲寶箱、爆炸特效需要定義的參數比真正的飛行物要少(只需要位置和圖片),所以寶箱和爆炸特效也可以使用FlyObject類創建。
- 我們可以爲上面五類飛行物各創建一個列表,用於存放其對象;每次需要生成一架飛機或者一個怪物時,就往對應的列表中放入一個對象。然後在線程的run方法中依次將每個列表中的所有對象全部畫出。
- 在FlyObject類中,最重要的是FlyObject的構造方法、move方法(負責計算飛行物的下一個座標)和drawFO方法(傳入畫布,將飛行物圖片畫到飛行物的座標上)。
//有圖片、有血量的飛行物
public FlyObject(Vector location,Vector speed,Vector acce,String imgName,int HP){
this.location=location;//位置
this.speed=speed;//速度
this.acce=acce;//加速度
this.HP=HP;//血量
this.imgName=fileAddress+imgName;//圖片地址
ImageIcon imgicon=new ImageIcon(this.imgName);//如果我們想要在畫布上畫一張圖片,可以先用圖片地址創建一個ImageIcon對象,然後再從這個對象中獲取Image對象
img=imgicon.getImage();
}
//前面介紹過的move方法
public void move(){
speed.add(acce);
location.add(speed);
}
//將飛行物的圖片畫到畫布上
public void drawFO(Graphics g){
//如果被繪製的對象有圖片就畫圖片,沒圖片就畫一個圓
if(imgName!=null){
// System.out.println(imgName);
g.drawImage(img,location.x, location.y,null);
}else{
g.fillOval(location.x, location.y,10,10);
}
}
- 遊戲過程中,我方飛船是始終跟隨着鼠標共同移動的。要實現這一點,我們需要在Listener類中實現MouseMotionListener,然後重寫鼠標移動mouseMoved方法(記得最後要給窗體添加MouseMotionListener監聽器)。當鼠標在窗體中進行移動時,該方法會不斷地獲取鼠標在窗體中的座標,參考下面這段代碼重寫mouseMoved方法:
public void mouseMoved(MouseEvent e){
Vector location=new Vector(e.getX(),e.getY());
FlyObject mp=new FlyObject(location,null,null,"我機.png");
mps.add(mp);//mps是存放我方飛機對象的列表ArrayList<FlyObject> mps
}
- 這樣一來,在移動鼠標的過程該方法會被不停的調用,並且不停的往mps列表中存放我放飛機對象。在線程類的run方法中,我們每一次只需要獲取該列表的最後一項(我方飛機的最新座標)將其畫出即可。
- 因爲子彈是我方飛機發射出來的,所以子彈生成座標只需要取我方飛機的座標即可。
//不斷髮射子彈
public void generateBullet(){
//隔一段時間就生成一些子彈(int len是一個計數器,它記錄的是run方法中的運行次數,所有代碼跑完一次就加一)
if(len%5==0){
for(int i=0;i<4;i++){
//設定子彈座標
Vector location_fo=new Vector(mps.get(mps.size()-1).location.x,mps.get(mps.size()-1).location.y+20*i);
Vector speed_fo=new Vector(100,0);//設定子彈速度
Vector acce_fo=new Vector(0,0);//設定子彈加速度(這裏把加速度設爲0意思就是讓子彈做勻速運動)
FlyObject fo=new FlyObject(location_fo,speed_fo,acce_fo,"子彈.png",1);
fos.add(fo);
}
}
}
- 怪物的生成就更簡單了,在遊戲設定中,怪物會從界面的最右邊被生成,一直往窗體的最左邊走,縱座標和速度是隨機的。
public void generateEnemy(){
if(len%20==0){
Random ran=new Random();
//怪物的橫座標是固定的(窗體的最右邊),縱座標是隨機的
int loc_y=ran.nextInt(900)+100;
//怪物只在x方向有速度
int spd_x=-ran.nextInt(10)-10;
Vector location=new Vector(1200,loc_y);
Vector speed=new Vector(spd_x,0);
Vector acce=new Vector(0,0);
FlyObject enemy=new FlyObject(location,speed,acce,"怪物.png",5);
enemys.add(enemy);
}
}
飛機大戰製作Step2:判斷碰撞
- 判斷兩個物體是否碰撞,我這裏用到的原理是判斷兩個飛行物座標的距離是否小於一定的值(比如說圖片寬度)。而且我們每一輪都要判斷每一個子彈和每一個怪物的距離,都要判斷我方飛機和怪物的距離等等。我們的所有飛行物都被放入了列表中,所以我們需要建立循環拆解列表,將其中的元素逐個取出,逐個比較。
//判斷子彈是否擊中怪物、怪物是否觸碰我機、是否拾得寶箱
public void judgeAttack(Graphics bufg_judgeAttack){
//判斷子彈是否擊中怪物
for(int i=0;i<enemys.size();i++){
//取出怪物對象
FlyObject en=enemys.get(i);
for(int j=0;j<fos.size();j++){
//取出子彈對象
FlyObject fo=fos.get(j);
//獲取子彈和怪物的座標位置
int fo_x=fo.location.x;
int fo_y=fo.location.y;
int en_x2=en.location.x;
int en_y2=en.location.y;
//計算怪物和子彈之間的距離(也可以採用if(橫座標的差值<某數&縱座標的差值<某數)
int distance_fo_en=(int)Math.sqrt(Math.pow((fo_x-en_x2),2)+Math.pow((fo_y-en_y2),2));
if(distance_fo_en<=50){
//這裏en(怪物)的HP是血量,fo(子彈)的HP是傷害值。
en.HP-=fo.HP;
//在該子彈位置添加一個子彈爆炸效果,後面會介紹
explosion(fos.get(j));
//將該子彈從列表中移除
fos.remove(j);
if(en.HP<=0){
//怪物爆炸效果
explosion(enemys.get(i));
//這裏如果直接用enemys.remove(i)會導致循壞for(int j=0;j<fos.size();j++)繼續執行,誤刪其他元素
enemys.get(i).img=null;
//把怪物圖片去除(每次畫圖就不畫該怪物了),然後把它移出屏幕
enemys.get(i).location=new Vector(-1000,0);
if(en.imgName.equals(fileAddress+"怪物.png")){
score+=10;
}else if(en.imgName.equals(fileAddress+"怪物2.png")){
score+=50;
}
}
}
}
}
}
飛機大戰製作Step3:爆炸動效
-
前面說到過爆炸動效也可以放進FlyObject類列表中,完成爆炸動效需要寫兩個方法,一個方法生成爆炸動效對象,一個方法繪製爆炸動效。因爲爆炸動效一般都是在子彈或者怪物消失的時候纔會生成,所以只生成一次;但是繪製爆炸動效需要多次繪製,所以生成爆炸動效對象和繪製爆炸動效需要分成兩個方法來寫。
-
生成爆炸動效的方法傳入的是飛行物的對象,因爲繪製爆炸動效至少需要兩個元素:爆炸發生在哪裏,生成什麼爆炸效果(怪物的爆炸效果和子彈的爆炸效果不同)。所以首先我們對該飛行物的圖片名稱進行一個判斷(判斷是什麼東西爆炸),然後取出它的座標。最後生成一個對應的爆炸效果對象放入列表中。
-
爆炸效果是一種動態效果,所以還涉及到切換圖片的操作。我們可以將預先準備好的幾張圖片同意文件名格式並編好序號,方便每畫完一次圖片就切換一張。
//爆炸動效
public void explosion(FlyObject flo){
//判斷是什麼對象爆炸
if(flo.imgName.equals(fileAddress+"怪物.png")|flo.imgName.equals(fileAddress+"怪物2.png")){
//獲取爆炸對象的座標
int x_explo=flo.location.x;
int y_explo=flo.location.y;
Vector location=new Vector(x_explo,y_explo);
//生成爆炸動效對象
FlyObject explo=new FlyObject(location,null,null,"爆炸_1.png",10);
//將爆炸動效對象添加到列表中
explotions.add(explo);
}
}
//繪製爆炸動效
public void drawExplo(Graphics bufg_explotion){
//依次將列表中的每個爆炸圖像畫出
for(int i=0;i<explotions.size();i++){
explotions.get(i).drawFO(bufg_explotion);
//這裏的HP表示的是這個爆炸效果持續的時間,每畫一次效果HP減一,當HP等於0時停止繪製該爆炸效果
explotions.get(i).HP--;
if(explotions.get(i).imgName.equals(fileAddress+"爆炸_1.png")){
//下面這條代碼的作用是每畫完一次圖像就更換一次圖片,以此達到動態變化的效果
ImageIcon imgicon=new ImageIcon(fileAddress+"爆炸_"+((explotions.get(i).HP%3)+1)+".png");//因爲我繪製的爆炸效果圖片一共有三張,所以這裏取除以三的餘數來設定圖片的文件名
explotions.get(i).img=imgicon.getImage();
}
//當爆炸動效對象的HP等於0時移除該對象
if(explotions.get(i).HP==0){
explotions.remove(i);
}
}
}
飛機大戰製作Step4:遊戲暫停/繼續,判定遊戲結束
-
我們想要的效果:在遊戲畫面的左下角有一個暫停鍵,我們點擊暫停鍵時遊戲會進入暫停狀態,再點擊開始遊戲會恢復到暫停之前的狀態。
-
我們需要做的操作:我們可以創建一個布爾值對象gameRest(布爾值只有true和false兩種狀態),初始值設定爲false,每一輪線程運行時都需要先判斷一下gameRest值是否爲false,如果爲true,則跳過繪製飛行物、判斷碰撞等操作;如果爲false,則繼續正常運行。
-
接着我們寫一個方法來改變gameRest的值,這樣我們每調用一次方法就切換一次gameRest的值
//遊戲暫停/開始
public void on_off(){
gameRest=!gameRest;
}
- 在鼠標監聽器中添加一個監聽事件,在mouseReleased方法下我們可以判斷一下鼠標鬆開的座標是否落在畫面左下角這塊區域,如果是就調用on_off方法來改變gameRest的值。
- 當我們的飛船與怪物相碰時,飛船墜落,遊戲結束,這裏同樣用到了一個布爾值gameOver
//判斷遊戲是否結束
public void judgeGameOver(Graphics g_judgeGameOver){
for(int i=0;i<enemys.size();i++){
FlyObject en=enemys.get(i);
FlyObject mp=mps.get(mps.size()-1);
int mp_x=mp.location.x;
int mp_y=mp.location.y;
int en_x=en.location.x;
int en_y=en.location.y;
int distance_mp_en=(int)Math.sqrt(Math.pow((mp_x-en_x),2)+Math.pow((mp_y-en_y),2));
if(distance_mp_en<=60){
//繪製gameOver圖片
ImageIcon imgicon_gamover=new ImageIcon(fileAddress+"gameover.png");
Image img_gamover=imgicon_gamover.getImage();
g_judgeGameOver.drawImage(img_gamover,0,0,null);
gameOver=true;
}
}
}
- 最後在run方法中插一段判斷gameOver的代碼
if(gameOver==true){
break;}
飛機大戰製作Step5:刷新分數
- 將遊戲分數的萬位、千位、百位和十位和個位分別取出,然後每個數字對應顯示一張圖片,將刷新分數的方法寫入run方法中,每一輪刷新一次分數。(Java中的符號“/”代表整除)
//獲取萬位
int number_5=score/10000;
//獲取千位
int number_4=(score-number_5*10000)/1000;
//獲取百位
int number_3=(score-number_5*10000-number_4*1000)/100;
//獲取十位
int number_2=(score-number_5*10000-number_4*1000-number_3*100)/10;
//獲取個位
int number_1=score-number_5*10000-number_4*1000-number_3*100-number_2*10;
- 同樣的,將每個數字的圖片素材同一文件名格式並編號
- 這裏的fileAddress是我存放圖片素材的目錄,這樣當我更換圖片目錄時只需要更改這一個值就可以了。
//生成圖片對象
ImageIcon imgicon_score=new ImageIcon(fileAddress+"Score.png");
Image img_score=imgicon_score.getImage();
ImageIcon imgicon5=new ImageIcon(fileAddress+number_5+".png");
Image img5=imgicon5.getImage();
ImageIcon imgicon4=new ImageIcon(fileAddress+number_4+".png");
Image img4=imgicon4.getImage();
ImageIcon imgicon3=new ImageIcon(fileAddress+number_3+".png");
Image img3=imgicon3.getImage();
ImageIcon imgicon2=new ImageIcon(fileAddress+number_2+".png");
Image img2=imgicon2.getImage();
ImageIcon imgicon1=new ImageIcon(fileAddress+number_1+".png");
Image img1=imgicon1.getImage();
//bufg_score是該方法導入的Graphics類畫布
bufg_score.drawImage(img_score, 340,50,null);
bufg_score.drawImage(img5, 590,50,null);
bufg_score.drawImage(img4, 650,50,null);
bufg_score.drawImage(img3, 710,50,null);
bufg_score.drawImage(img2, 770,50,null);
bufg_score.drawImage(img1, 830,50,null);
- 第一個遊戲案例的分享差不多就到這裏,如果有什麼描述不夠清楚的地方歡迎大家在評論區留言。也可以點擊下方鏈接,下載我這三個遊戲的全部源代碼和遊戲素材進行參考
- 這個遊戲目前來說做得還非常粗糙,還有一些小漏洞和可以優化的地方,如果有小夥伴下載了我的代碼,發現有什麼好的建議歡迎私信或在評論區中指出。歡迎交流,您的評論將給我的學習之路帶來巨大幫助。
可以優化的地方:
- 有的子彈會穿過怪物,或者有時候碰到怪物沒有死,說明判斷碰撞和物體移動的方法還有缺陷。
- 怪物血量減少到一定程度時出現破損效果,這樣看起來對怪物剩餘血量更直觀
- 遊戲玩法比較單一,可以給飛機適當增加新的技能,增加關卡和Boss,豐富玩法
- 缺少遊戲開始界面、背景音樂、音效等
- 遊戲玩到後期比較卡頓,因爲飛出窗體的子彈、怪物等仍然存在列表中,每次繪製圖片時都要將這些看不見的對象重新再畫一遍,十分消耗性能。
遊戲二:接豆人
遊戲演示:
Java原創遊戲分享
遊戲介紹:
-
接豆人遊戲和飛機大戰玩法雖然差異比較大,但是用到的代碼原理其實是類似的。
-
黃色的喫豆人的移動,同樣是依靠鼠標監聽器的mouseMoved方法不斷獲取鼠標的座標然後繪製接豆人的圖像。只不過這次我們只獲取鼠標的橫座標,縱座標設定爲一個定值,這樣就可以實現我們的接豆人只做水平方向的運動了。
-
接豆人喫到寶石和道具的判斷,和飛機大戰中的判斷碰撞是類似的;接豆人中隨機掉落的寶石、炸彈和道具,與飛機大戰中刷新怪物是類似的。
-
兩個遊戲比較不同的地方是,在接豆人遊戲中如果喫到了蜘蛛或者金幣禮包是會觸發新事件的。而且在接豆人中也增加了玩家的生命值。
-
大致總結一下,實現接豆人需要完成這幾件事:讓接豆人在水平方向跟隨鼠標移動👉隨機生成寶石、炸彈蜘蛛和道具,並且賦予下落物體一個垂直方向的加速度,增加真實感👉判斷接豆人是否接到了掉落物👉給金幣禮包和蜘蛛添加觸發效果(下金幣雨和接豆人進入眩暈)👉遊戲暫停、遊戲結束後重新開始👉間隔一段時間清理一下飛行物列表,提高遊戲流暢度
-
和飛機大戰類似的地方就不再贅述,這裏介紹一些不同的地方
接豆人制作Step1:金幣禮包和蜘蛛的觸發效果
- 首先我們需要定義四個變量
public int rewardTime;//獎勵時間
public Boolean pause=false;//是否進入眩暈狀態
public int pauseTime;//眩暈時間
public FlyObject mp_pause;//用於繪製眩暈時接豆人的圖片
- 在判斷碰撞的方法下面,如果接豆人碰到的是禮物,則給rewardTime加上200,如果是蜘蛛,則給pauseTime加上200,且將pause的值改爲true。
- 在生成下落物的方法中,我們先對rewardTime進行一個判斷,如果rewardTime大於0,就下金幣,如果小於等於0,就生成其他掉落物。
//生成下落物
public void generateDrop(){
if(rewardTime>0){
if(len%1==0){
rewardTime--;//每次rewardTime遞減
Random ran=new Random();
Vector location=new Vector(ran.nextInt(750)+50,50);
Vector speed=new Vector(0,ran.nextInt(1)+10);
Vector acce=new Vector(0,2);
FlyObject fo=new FlyObject(location,speed,acce,"金幣1.png");
fos.add(fo);
}
}else{
//生成其他掉落物
}
- 當接豆人進入眩暈狀態時,身邊的掉落物還是正常掉落的,但是接豆人在眩暈狀態下不能移動、不能接取掉落物。所以我們需要在繪製接豆人和判斷碰撞的方法下分別先對pause的值進行一個判斷,如果pause爲false則正常運行。
//繪製我機
public void draw_mp(){
if(pause==false){
if(mps.size()-5>=0){
FlyObject mp=mps.get(mps.size()-5);
mp.drawFO(bufg);
}
}else{
//當pause爲true時執行
pauseTime--;//pauseTime遞減
mp_pause.drawFO(bufg);//繪製接豆人眩暈時的圖片
if(pauseTime==0){
pause=false;//當pauseTime減少到0時將pause改回爲false
}
}
}
接豆人制作Step2:遊戲重新開始
- 這個功能的實現和飛機大戰中說過的暫停功能非常類似,我們需要創建一個布爾值gameOver,然後當生命值減少到0時將gameOver改爲true。然後屏幕上顯示gameOver的圖像
- 當我們點擊該區域時,將gameOver的值改回爲false,並且將所有的飛行物列表、分數、生命值等全部恢復到遊戲剛開始的狀態。
if(thc.gameOver){
if(e.getX()>340&e.getX()<540&e.getY()>630&e.getY()<710){
thc.life=3;
thc.fos.removeAll(fos);
thc.mps.removeAll(mps);
thc.score=0;
thc.gameOver=false;
}
}
接豆人制作Step3:清理列表數據,提升流暢度
- 我們每間隔一段時間就把超出窗體可見範圍的飛行物都從列表中刪去,防止遊戲後期需要畫的飛行物太多導致卡頓。
//清理緩存(在run方法中調用該方法)
public void clear(){
//每500輪清理一次
if(len%500==0){
System.out.println("清理前:");
System.out.println("fos size is"+fos.size());
System.out.println("mps size is"+mps.size());
System.out.println("exps size is"+explotions.size());
clearList(fos,0);
clearList(mps,1);
clearList(explotions,0);
System.out.println("清理後:");
System.out.println("fos size is"+fos.size());
System.out.println("mps size is"+mps.size());
System.out.println("exps size is"+explotions.size());
}
}
//清理列表(傳入需要清理的列表,並傳入清理類型)
public void clearList(ArrayList<FlyObject> fos,int flag){
int fos_size=fos.size();
//其他飛行物類型的清理
if(flag==0){
for(int i=fos.size()-10;i>-1;i--){
//判斷一下從哪個飛行物開始超出窗體可見範圍(在它之前的飛行物一定是超過了)
if(fos.get(i).location.y>1000){
for(int j=0;j<i;j++){
fos.remove(0);//不斷刪除列表的第一個元素,直到刪到開始超出窗體範圍的那一個
}
break;
}
}
//接豆人的列表的清理
}else if(flag==1){
//只保留列表中最後一百個元素,前面的全部刪除
for(int i=0;i<fos_size-100;i++){
fos.remove(0);
}
}
}
遊戲三:雙線挑戰(雙人遊戲)
遊戲截圖:
遊戲介紹:
- 這個遊戲和上面兩個遊戲不太一樣,它是一個使用鍵盤操控的雙人小遊戲。操作方法有點類似貪喫蛇,兩個人分別操控一條線,當觸碰到遊戲邊界或者自身及對手的線時,遊戲結束。所以在遊戲過程中,雙方可以儘可能地把對方包圍在一個比較小的空間裏,使自己成爲最後的贏家。
- 雖然這個遊戲的畫面設計比較粗糙,但是這次增加了遊戲開始界面、遊戲背景音樂的播放功能,仍然非常有意思。
- 這個遊戲一共做了三個版本
遊戲皮膚:
雙線挑戰製作Step1:鍵盤監聽器的使用
- 鍵盤監聽器的接口是KeyListener,我們主要用到keyPress和keyReleased兩個方法,他們分別在鍵盤按下和鍵盤松開時被調用。
class Listener implements KeyListener{
public void keyTyped(KeyEvent e) {
}
public void keyPressed(KeyEvent e) {
//獲取按下鍵的keycode
int keyc=e.getKeyCode();
System.out.println(keyc+" is pressed!");
//也可以使用String press=e.getKeyChar()+"";這樣獲取到的就是鍵盤的字符
}
public void keyReleased(KeyEvent e) {
//獲取鬆開鍵的keycode
int keyc=e.getKeyCode();
System.out.println(keyc+" is released!");
}
}
- 然後一定要記得給窗體加上監聽器對象!而且鍵盤的監聽相較於鼠標監聽器還有一個特殊的地方,鍵盤的監聽器需要焦點,鍵盤監聽器需要獲取焦點發生的動作事件。比如說我們的qq登陸界面上有兩個輸入框,如果我們直接敲擊鍵盤,此時電腦是不知道我們需要輸入的是賬號還是密碼。只有我們點擊賬號文本框後,才能讓賬號文本框得到焦點,從而順利輸入我們的賬號。
- 同時,窗體獲取焦點的代碼也必須放在窗體可見之後,否則無法正常監聽鍵盤事件
//設置窗體可見,jf是創建的JFrame對象
jf.setVisible(true);
jf.addKeyListener(mou);//爲窗體添加鍵盤監聽器
jf.requestFocusInWindow();//窗體獲得焦點,記得要放在窗體可見之後
- 現在讓我們一起來做幾個小實驗,對鍵盤監聽器的原理進一步瞭解(記得在實驗前將鍵盤調爲英文輸入模式,否則無法正常監聽英文鍵的輸入):連續敲擊F鍵;長按F鍵;慢速交替敲擊F和D鍵;同時按下F和D鍵;快速交替敲擊F和D鍵。
- 下面是博主測出的結果(F鍵的keycode爲70,D鍵的keycode爲68):
- 看出來這幾種按鍵方式的特點了嗎?我們使用鍵盤與程序交互時,這些按鍵方式反饋的差異會給我們帶來很大幫助。比如說有的遊戲中同時按下W和D鍵是向右上跳,只按W鍵是向上跳,只按D鍵是向右走。我們就可以在鍵盤監聽器中先判斷用戶是同時按了W,D鍵還是先按了D鍵再按W鍵,從而決定讓角色向右上跳,還是先向右走再向垂直上跳。
雙線挑戰製作Step2:用鍵盤控制線條的走向
- 因爲在雙線挑戰遊戲中,我們是需要線條留下軌跡的,所以我們的遊戲背景圖片只需要畫一次(否則就會把軌跡覆蓋了)。那怎麼讓圖片只畫一次呢?
//我們可以先定義一個整數flag1
public int flag1=0;
- 在畫圖之前先判斷一下這個值是不是0,是0的話說明沒有被畫過;在畫圖的代碼中,記得將flag1改爲除0以外的數,表示這個圖已經被畫過一次了。
//只畫一次圖片
public void draw_just_once(int type){
//如果說flag1爲0,則開始畫圖
if(flag1==0){
ImageIcon imgic=new ImageIcon(fileAddress+"遊戲背景_2.png");
Image img=imgic.getImage();
g.drawImage(img, 0,0,null);
flag1++;//更改flag1的值,表示圖已畫過
}
}
- 當我們的背景只畫一次,而小方塊又在不停的移動時,小方塊自然就留下了軌跡;對於實現小方塊移動的方法,和飛機大戰中的FlyObject類、Vector類差不多。
- 我們先創建一個從窗體的左上角出發的小方塊,速度向右爲1(這裏的LineBall類和FlyObject原理及代碼基本相同(move方法、Vector類的使用等),不太清楚的小夥伴可以回到飛機大戰Step1看一看)
lb_blue=new LineBall(new Vector(0,0),new Vector(1,0));
- LineBall的drawLB方法和FlyObject的drawFO方法稍有不同。因爲我這裏用的小方塊的圖片是5個像素,所以說小方塊的location每加1,我就讓小方塊的座標向右移5個像素。
public void drawLB(Graphics g){
if(imgName==null){
g.fillRect(location.x*10+50, location.y*10+50, 10,10);
}else{
ImageIcon imgic=new ImageIcon(fileAddress+imgName);
Image img=imgic.getImage();
g.drawImage(img,location.x*10+50, location.y*10+70, null);
}
}
- 然後在線程中運行這一段代碼
lb_blue.imgName="藍_4.png";
lb_blue.drawLB(g);//畫完以後讓小方塊move移動一次
lb_blue.move();
try{
Thread.sleep(50);
}catch(Exception ef){
}
- 運行效果大概是這樣的
- 現在如果我們想讓小方塊改變它的移動方向,只需要改變對象lb_blue的speed值即可(比如說想讓小方塊往下走,那speed就改成(0,1);想往左走,那就改成(-1,0)。
- 因爲我們要使用鍵盤操控,所以我們必須獲取WASD和上下左右鍵的keycode(可以使用雙線挑戰製作Step1中的方法自己試驗一下,把這8個按鍵都按一遍就知道它們的keycode了,需要知道其他按鍵的keycode也可用此方法)。
- 在鍵盤監聽器的keyReleased方法下去判斷按鍵及做出響應
public void keyReleased(KeyEvent e) {
int keyc=e.getKeyCode();
System.out.println(keyc+" is released!");
int speed=1;
if(lb_blue.len!=0){
if(lb_blue.speed.y==0){
if(keyc==87){
//w
lb_blue.len=0;
lb_blue.speed=new Vector(0,-speed);
}
if(keyc==83){
//s
lb_blue.len=0;
lb_blue.speed=new Vector(0,speed);
}
}
if(lb_blue.speed.x==0){
if(keyc==65){
//a
lb_blue.len=0;
lb_blue.speed=new Vector(-speed,0);
}
if(keyc==68){
//d
lb_blue.len=0;
lb_blue.speed=new Vector(speed,0);
}
}
}
}
- 博主這裏還用到了幾個判斷,在這裏我給大家解釋一下。
if(lb_blue.speed.y==0)/if(lb_blue.speed.x==0)
:這裏的判斷是,假如小方塊目前正在往左走或者往右走(即y方向速度爲0)時,纔可以向上或者向下拐(不然就會出現本來在往上走,按了向下鍵後突然原地掉頭,在這個遊戲設定中是不符合規則的)後面的判斷x方向速度同理。if(lb_blue.len!=0)
:這裏的len代表的是小方塊在當前方向行走的距離,每更改一次方向len就清零一次。它的意思是小方塊更改方向後,必須往更改後的方向至少前進一格才能再次更改方向,否則仍然有可能出現“原地掉頭”的操作。
雙線挑戰製作Step3:利用二維數組設定“棋盤”
- 回顧一下雙線挑戰最重要的遊戲規則:玩家線條不能夠觸碰到邊界、不能觸碰對方和自身的線條。
- 大家覺得這種判定方法是不是像在下棋?兩個玩家就像順着小方塊的移動方向不停的擺棋子(小方塊),當下一個要擺的棋子超出了邊界,或者擺在了原來有棋子的格子上時,遊戲結束。
- 二維數組的特點就十分符合我們的需求,比如說我們創建了一個大小爲70*70的棋盤chessBoard。(橫縱座標範圍均爲0-69)
public static int[][] chessBoard=new int [70][70];
- 我們可以用chessBoard[x][y]=?來表示棋盤上座標爲(x,y)的格子裏面裝的是什麼,這裏我們用0代表空,1代表這裏有棋子。那麼chessBoard[8][9]=0就代表座標(8,9)的格子裏沒有棋子;chessBoard[7][6]=1就代表(7,6)的格子裏已經放有棋子了。(二維數組在創建的時候默認每個位置的值都是0,也就是沒有棋子)
//判斷遊戲是否結束
public Boolean judge_gameover(){
//判斷棋子是否超出邊界
if(location.x>69|location.y>69|location.x<0|location.y<0){
gameOver=true;
return true;
//判斷棋子要放下的位置上原本有沒有棋子
}else if(chessBoard[location.x][location.y]==1){
gameOver=true;
return true;
//如果上面兩種情況都不是,則返回false
}else{
gameOver=false;
return false;
}
}
- 同時,我們需要修改一下drawLB的方法
public void drawLB(Graphics g){
if(imgName==null){
//當棋子走到某一格時,將棋盤的這一格狀態改爲“有棋子”
chessBoard[location.x][location.y]=1;
g.fillRect(location.x*10+50, location.y*10+50, 10,10);
}else{
//當棋子走到某一格時,將棋盤的這一格狀態改爲“有棋子”
chessBoard[location.x][location.y]=1;
ImageIcon imgic=new ImageIcon(fileAddress+imgName);
Image img=imgic.getImage();
g.drawImage(img,location.x*10+50, location.y*10+70, null);
}
}
- 最後在run方法中:
雙線挑戰製作Step4:給遊戲添加背景音樂
- 首先我們需要創建一個PlayMusic類來載入音樂文件,準備播放
import java.applet.AudioClip;
import java.net.MalformedURLException;
import java.net.URL;
import javax.swing.JApplet;
public class PlayMusic {
public AudioClip music = loadSound("此處輸入需要播放的音樂文件路徑(文件格式必須爲WAV格式)");
public static AudioClip loadSound(String filename) {
URL url = null;
try {
url = new URL("file:" + filename);
}
catch (MalformedURLException e) {
;}
return JApplet.newAudioClip(url);
}
//音樂播放
public void play() {
//音樂播放
music.play();
//循環播放
music.loop();
}
}
- 然後在需要播放音樂和音效的地方,插入這一段代碼
PlayMusic p=new PlayMusic();
p.play();
- 就這麼簡單!
一點點總結心得:
實現一個程序的步驟——
- 我想要實現什麼效果?
- 爲了實現這樣的效果我要怎麼做?
(開幹!) - 做好的效果和我的預期符合嗎?如果不符合我要怎麼修改?
- 蒐集資料,撰寫博客,和同學交流,對程序進一步優化
寫在最後:
java給了我一種前所未有的體驗,或者說一種前所未有的快感。只需要敲擊鍵盤,就可以像在廣闊的平原,憑空升起一座城堡。
複雜紛繁的代碼,從我的手中獲得了意義,獲得了生氣。在這個世界裏,猶如掌握了“生殺大權”,遊戲的一切都由我來定義。
飛機長什麼樣子,怪物又長什麼樣子;飛機一次打多少發子彈,怪物喫多少子彈會被殺死;怪物以什麼姿態出生,又以什麼姿態死去……
每一個程序的活潑生動,都是用一條條樸實無華的代碼堆砌的。手握代碼,我們就是這個世界的造物主!