這是一篇學習分享博客,這篇博客將會介紹以下幾項內容:
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);
- 第一個游戲案例的分享差不多就到這里,如果有什么描述不夠清楚的地方歡迎大家在評論區留言,也可以點擊下方鏈接,下載我這三個游戲的全部源代碼和游戲素材進行參考
游戲源代碼及游戲素材鏈接——提取碼:hjzd
- 這個游戲目前來說做得還非常粗糙,還有一些小漏洞和可以優化的地方,如果有小伙伴下載了我的代碼,發現有什么好的建議歡迎私信或在評論區中指出,歡迎交流,您的評論將給我的學習之路帶來巨大幫助,
可以優化的地方:
- 有的子彈會穿過怪物,或者有時候碰到怪物沒有死,說明判斷碰撞和物體移動的方法還有缺陷,
- 怪物血量減少到一定程度時出現破損效果,這樣看起來對怪物剩余血量更直觀
- 游戲玩法比較單一,可以給飛機適當增加新的技能,增加關卡和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);
}
}
}
游戲源代碼及游戲素材鏈接——提取碼:hjzd
游戲三:雙線挑戰(雙人游戲)
游戲截圖:

游戲介紹:
- 這個游戲和上面兩個游戲不太一樣,它是一個使用鍵盤操控的雙人小游戲,操作方法有點類似貪吃蛇,兩個人分別操控一條線,當觸碰到游戲邊界或者自身及對手的線時,游戲結束,所以在游戲程序中,雙方可以盡可能地把對方包圍在一個比較小的空間里,使自己成為最后的贏家,
- 雖然這個游戲的畫面設計比較粗糙,但是這次增加了游戲開始界面、游戲背景音樂的播放功能,仍然非常有意思,
- 這個游戲一共做了三個版本

游戲皮膚:
雙線挑戰制作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();
- 就這么簡單!
游戲源代碼及游戲素材鏈接——提取碼:hjzd
一點點總結心得:
實作一個程式的步驟——
- 我想要實作什么效果?
- 為了實作這樣的效果我要怎么做?
(開干!) - 做好的效果和我的預期符合嗎?如果不符合我要怎么修改?
- 搜集資料,撰寫博客,和同學交流,對程式進一步優化
寫在最后:
java給了我一種前所未有的體驗,或者說一種前所未有的快感,只需要敲擊鍵盤,就可以像在廣闊的平原,憑空升起一座城堡,
復雜紛繁的代碼,從我的手中獲得了意義,獲得了生氣,在這個世界里,猶如掌握了“生殺大權”,游戲的一切都由我來定義,
飛機長什么樣子,怪物又長什么樣子;飛機一次打多少發子彈,怪物吃多少子彈會被殺死;怪物以什么姿態出生,又以什么姿態死去……
每一個程式的活潑生動,都是用一條條樸實無華的代碼堆砌的,手握代碼,我們就是這個世界的造物主!
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/256365.html
標籤:java
上一篇:微信二維碼支付
