簡介
我們仿照 QQ遊戲“”雷電這個風靡全球的遊戲,來把所有知識點串接起來了
多線程用來實現動畫效果、容器實現對於多發炮彈的存取和處理、常用類等等的應用。
但跟隨本文只能做出雷電的雛形,但你可以在這個基礎上改造出屬於你的“雷電”
這是最後效果圖:
第一階段:遊戲界面的繪畫及圖片加載
遊戲開發中,圖片加載是最常見的技術。我們在此處使用ImageIO類實現圖片加載,並且爲了代碼的複用,將圖片加載的方法封裝到GameUtil工具類中,便於我們以後直接調用。
我們要先將項目用到的圖片拷貝到項目的src下面,我們可以建立新的文件夾images存放所有圖片
1.添加背景圖片
方法1:通過在JFrame中添加一個JPanel,背景圖片放在JPanel上來實現
方法2:我們用JLayeredPane,JLayeredPane 爲 JFC/Swing 容器添加了深度,允許組件在需要時互相重疊。Integer
對象指定容器中每個組件的深度,其中編號較高的組件位於其他組件之上。常用的幾個層如下圖:
之前我再添加背景圖片時一貫使用絕對路徑,每次添加修改極爲麻煩,本次我們學習如何使用相對路徑,首先通過GameUtil.class.getClaaaLoader().getResource來獲得資源的根目錄,從而獲取相對位置
paint會被自動被調用,g相當於一隻畫筆
代碼如下:
/**
* 2019年9月21日
*/
package Thunder;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.event.KeyListener;
import java.util.ArrayList;
import javax.swing.JFrame;
/**
* 飛機大戰雛形:可以發炮彈,但是沒有解決連發炮彈的問題
*
* 之後有兩個方向:畫面上有很多炮彈,控制飛機移動躲避炮彈
* 或者經典的雷電遊戲,擊落敵機
*
* 2019年9月21日
*/
public class GameUI extends JFrame {
// 將背景和飛機圖片定義爲成員變量
Image background = GameUtil.getImage("images/back3.jpg");
Image planeImg = GameUtil.getImage("images/plane1.png");
Image shootImg = GameUtil.getImage("images/shoot1.png");
public static int UIWidth = 1000;
public static int UIHeigh = 1000;
//ArrayList<Shell> shellList = new ArrayList<Shell>();
Plane plane1 = new Plane(planeImg,200,200,2,100,100);
Shell shoot1 = new Shell(shootImg, 237,170,2,25,35);
public void GFrame() {
this.setTitle("雷電");
this.setSize(UIWidth, UIHeigh);
this.setLocationRelativeTo(null);
this.setDefaultCloseOperation(3);
this.addKeyListener(plane1);
this.addKeyListener(shoot1);
this.setVisible(true);
//啓動線程
PaintThread pt =new PaintThread();
pt.start();
}
// paint方法作用是:畫出整個窗口及內部內容。被系統自動調用。
public void paint(Graphics g) {
g.drawImage(background, 0, 0, 1000, 1000, null);
plane1.drawMySelf(g);
shoot1.drawMySelf(g);
}
// 定義內部類
class PaintThread extends Thread {
// 重寫
public void run() {
while (true) {
repaint();// 重畫
try {
Thread.sleep(10);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
//雙緩衝解決閃爍
private Image offScreenImage = null;
public void update(Graphics g) {
if(offScreenImage == null)
offScreenImage = this.createImage(500,500);//這是遊戲窗口的寬度和高度
Graphics gOff = offScreenImage.getGraphics();
paint(gOff);
g.drawImage(offScreenImage, 0, 0, null);
}
// 主方法
public static void main(String[] args) {
// 類裏面調用窗體
GameUI gu = new GameUI();
gu.GFrame();
}
}
【要點】:
1.繼承Frame類,畫出窗口
2. 瞭解座標系,窗口座標以左上角爲(0,0)點
3. 物體就是矩形,物體的位置就是所在矩形左上角頂點的座標
4. 窗口關閉,居中等我們需要自己添加功能,如下
在JFrame中我們可以這樣寫:
drawframe.setDefaultCloseOperation(3);//關閉時程序結束
drawframe.setLocationRelativeTo(null);
但在Frame中,我們需要
/ 增加關閉窗口監聽,這樣用戶點擊右上角關閉圖標,可以關閉遊戲程�?
this.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
System.exit(0);
}
});
第二階段:使用多線程讓動畫動起來
第三階段:鍵盤操控飛機
第四階段:炮彈和飛機碰撞,爆炸
第五階段:顯示分數
項目結構如圖:
所有代碼:
/**
* 2019年9月21日
*/
package Thunder;
import java.awt.Graphics;
import java.awt.Image;
import javax.swing.JFrame;
/**
*
*
* 2019年9月21日
*/
public class GameUI extends JFrame {
// 將背景和飛機圖片定義爲成員變量
Image background = GameUtil.getImage("images/back3.jpg");
Image planeImg = GameUtil.getImage("images/plane1.png");
public static int UIWidth = 1000;
public static int UIHeigh = 1000;
Plane plane1 = new Plane(planeImg,200,200,2,100,100);
public void GFrame() {
this.setTitle("雷電");
this.setSize(UIWidth, UIHeigh);
this.setLocationRelativeTo(null);
this.setDefaultCloseOperation(3);
//添加監聽器
//Listener li = new Listener();
//this.addKeyListener(li);
this.addKeyListener(plane1);
this.setVisible(true);
//啓動線程
PaintThread pt =new PaintThread();
pt.start();
}
// paint方法作用是:畫出整個窗口及內部內容。被系統自動調用。
public void paint(Graphics g) {
g.drawImage(background, 0, 0, 1000, 1000, null);
plane1.drawMySelf(g);
}
// 定義內部類
class PaintThread extends Thread {
// 重寫
public void run() {
while (true) {
repaint();// 重畫
try {
Thread.sleep(10);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
//雙緩衝解決閃爍
private Image offScreenImage = null;
public void update(Graphics g) {
if(offScreenImage == null)
offScreenImage = this.createImage(500,500);//這是遊戲窗口的寬度和高度
Graphics gOff = offScreenImage.getGraphics();
paint(gOff);
g.drawImage(offScreenImage, 0, 0, null);
}
// 主方法
public static void main(String[] args) {
// 類裏面調用窗體
GameUI gu = new GameUI();
gu.GFrame();
}
}
package Thunder;
import java.awt.Image;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.net.URL;
import javax.imageio.ImageIO;
/**
* GameUtil類:加載圖片代碼
*GameUtil獲得程序運行類加載器,加載資源的根目錄,
* 2019年9月22日
*/
public class GameUtil {
//工具類一般將構造器私有化
public GameUtil() {
}
public static Image getImage(String path){
BufferedImage bi = null;
try {
URL u = GameUtil.class.getClassLoader().getResource(path);
bi = ImageIO.read(u);
} catch (IOException e) {
e.printStackTrace();
}
return bi;
}
}
/**
* 2019年9月22日
*/
package Thunder;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.Rectangle;
/**
* 遊戲中所有物體的父類
*
* 2019年9月22日
*/
public class GameObject {
Image img; //該物體對應的圖片對象
double x,y; //該物體的座標
int speed; //該物體的運行速度
int width,height; //該物體所在矩形區域的寬度和高度
public void drawMySelf(Graphics g){
g.drawImage(img, (int)x, (int)y, (int)width, (int)height, null);
}
public GameObject(Image img, double x, double y) {
this.img = img;
this.x = x;
this.y = y;
if(img!=null){
this.width = img.getWidth(null);
this.height = img.getHeight(null);
}
}
public GameObject(Image img, double x, double y, int width,
int height) {
this.img = img;
this.x = x;
this.y = y;
this.width = width;
this.height = height;
}
public GameObject(Image img, double x, double y, int speed, int width,
int height) {
this.img = img;
this.x = x;
//這只是構造方法初始化化時的值,並不能作爲約束
this.y = y;
this.speed = speed;
this.width = width;
this.height = height;
}
public GameObject() {
}
/**
* 返回物體對應矩形區域,便於後續在碰撞檢測中使用
* @return
*/
public Rectangle getRect(){
return new Rectangle((int)x,(int) y, width, height);
}
}
package Thunder;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
/**
*
*
* 2019年9月22日
*/
public class Plane extends GameObject implements KeyListener {
boolean left, right, up, down,shoot;
boolean live = true;
public void drawMySelf(Graphics g) {
super.drawMySelf(g);
// 這裏可移動,但是下面不行,應該是判斷的問題
this.x+=2;
System.out.println("此時的座標"+x);
//System.out.println(left);
if (left) {
System.out.println("此時的座標"+x);
x -= speed;
if(x<=0){
x=0;
}
}
if (right) {
x += speed;
if(x>=900){
x=900;
}
}
if (up) {
y -= speed;
if(y<=10){
y=10;
}
}
if (down) {
y += speed;
if(y>=900){
y=900;
}
}
}
public Plane(Image img, double x, double y, int speed, int width, int height) {
super(img, x, y,width, height);
//System.out.println("有被重畫");
//this.speed = speed;
}
public void keyTyped(KeyEvent e) {
}
public void keyPressed(KeyEvent e) {
System.out.println(e.getKeyCode());
int key = e.getKeyCode();
switch (key) {
case 37:
left = true;
break;
case 38:
up = true;
break;
case 39:
right = true;
break;
case 40:
down = true;
break;
case 32:
shoot = true;
break;
default:
break;
}
}
public void keyReleased(KeyEvent e) {
//System.out.println(e.getKeyCode());
int key = e.getKeyCode();
switch (key) {
case 37:
left = false;
break;
case 38:
up = false;
break;
case 39:
right = false;
break;
case 40:
down = false;
break;
case 32:
shoot = true;
break;
default:
break;
}
}
}
我原來打算共享飛機座標,這樣就可以將子彈和飛機關聯起來,但是這個語句導致 飛機完全不能動,因爲座標與圖片沒有關聯了,重繪只畫了一次
該怎麼把子彈放入數組列表中呢?
現在子彈是一個新的類,而且有構造器,得隨着飛機移動,但是
應該直接放入監聽就可以了,點一下,延遲幾秒,畫一個。
現在到這個方向有兩種結果可以走:
第一種:屏幕隨機生成炮彈,我們控制飛機去躲避,生存時間越長,得分越高
第二鍾:和遊戲雷電一樣,對面有敵機,我們在躲避敵機炮彈的同時擊落敵機,獲取分數。
在炮彈的任意角度飛行時,如果觸碰到邊界就反轉角度,模擬碰撞效果。
同時爲了效果美觀,我們需要在一些細節上注意:例如碰撞邊界時要減去球 的直徑,碰撞標題欄時需要減去標題欄的寬度。
但是一個炮彈遠遠不夠,我們希望增大一些挑戰難度,有很多炮彈,就可以用到數組了。
分爲三步:
1)定義數組
2)初始化五十個炮彈
3)畫出炮彈
當使用Frame時,屏幕閃爍如圖:
當使用JFrame解決屏幕閃爍問題時:
最後我們採用了frame+雙緩衝的技術來解決,解決效果如圖:
只需要把這段代碼放入界面的類中(任意位置即可)
//雙緩衝解決閃爍
private Image offScreenImage = null;
public void update(Graphics g) {
if(offScreenImage == null)
offScreenImage = this.createImage(1000,1000);//這是遊戲窗口的寬度和高度
Graphics gOff = offScreenImage.getGraphics();
paint(gOff);
g.drawImage(offScreenImage, 0, 0, null);
}
在Jfram和frame切換中可能的報錯:
當我們使用JFrame時可以調用這行語句關閉窗口
但是在Frame裏面沒有,所以我們需要添加監聽器
//this.setDefaultCloseOperation(3);
//增加關閉窗口監聽,這樣用戶點擊右上角關閉圖標,可以關閉遊戲程序
this.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
System.exit(0);
}
});
遊戲中,多個元素是否碰到一起,實際上,通常是用“矩形檢測”原理實現的。 我們在前面提到,遊戲中所有的物體都可以抽象成“矩形”,我們只需判斷兩個矩形是否相交即可。對於一些複雜的多邊形、不規則物體,實際上是將他分解成多個矩形,繼續進行矩形檢測。
Java的API中,爲我們提供了Rectangle類來表示矩形相關信息,並且提供了intersects()方法,直接判斷矩形是否相交。
我們使用GameObject裏面的矩形檢測來判斷,飛機有,炮彈也有,這也是繼承的隱形好處.
爆炸圖片輪播:
圖片加載一次很耗費資源,所以我們將他設爲static,只初始化一次,之後直接用就可以,而不需要new了,
爆炸類如下
package Thunder_two;
import java.awt.Graphics;
import java.awt.Image;
/**
*
*
*
*/
public class Explode {
double x,y;
static Image[] imgs = new Image[16];
static {
for(int i=0;i<16;i++){
imgs[i] = GameUtil.getImage("images/explode/e"+(i+1)+".gif");
imgs[i].getWidth(null);
}
}
int count;
public void draw(Graphics g){
if(count<=15){
g.drawImage(imgs[count], (int)x, (int)y, null);
count++;
}
}
public Explode(double x,double y){
this.x = x;
this.y = y;
}
}
到這一步,雷電遊戲的雛形大概就完成了,但是我們卻少了最重要的獎勵機制,玩家不知道自己的得分,自然也不會有興趣繼續挑戰了,所有我們加入時間類,在玩家死亡之後,在屏幕上顯示獲得的分數。
先定義兩個成員變量
Date startTime = new Date(); //遊戲起始時刻
Date endTime; //遊戲結束時刻
如果飛機死亡,調用顯示的方法
if(!plane.live){
if(endTime==null){
endTime = new Date();
}
int period = (int)((endTime.getTime()-startTime.getTime())/1000);
printInfo(g, "時間:"+period+"秒", 50, 120, 260, Color.white);
}
//顯示的方法
public void printInfo(Graphics g,String str,int size,int x,int y,Color color){
Color c = g.getColor();
g.setColor(color);
Font f = new Font("宋體",Font.BOLD,size);
g.setFont(f);
g.drawString(str,x,y);
g.setColor(c);
}
至此,雷電小遊戲的第一個方向就完成了,但大家也許還有點意猶未盡呢吧!這和我們兒時玩的雷電差距太大了吧,下一篇文章中,我們就來開發自動發射+擊落敵機的部分,翹首以待吧。
以下是雷電Ⅰ的全部代碼
項目結構如圖:
package Thunder_one;
import java.awt.Graphics;
import java.awt.Image;
/**
*
*
*
*/
public class Explode {
double x,y;
static Image[] imgs = new Image[16];
static {
for(int i=0;i<16;i++){
imgs[i] = GameUtil.getImage("images/explode/e"+(i+1)+".gif");
imgs[i].getWidth(null);
}
}
int count;
public void draw(Graphics g){
if(count<=15){
g.drawImage(imgs[count], (int)x, (int)y, null);
count++;
}
}
public Explode(double x,double y){
this.x = x;
this.y = y;
}
}
/**
* 2019年9月22日
*/
package Thunder_one;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.Rectangle;
/**
* 遊戲中所有物體的父類
*
* 2019年9月22日
*/
public class GameObject {
Image img; //該物體對應的圖片對象
double x,y; //該物體的座標
int speed; //該物體的運行速度
int width,height; //該物體所在矩形區域的寬度和高度
public void drawMySelf(Graphics g){
g.drawImage(img, (int)x, (int)y, (int)width, (int)height, null);
}
public GameObject(Image img, double x, double y) {
this.img = img;
this.x = x;
this.y = y;
if(img!=null){
this.width = img.getWidth(null);
this.height = img.getHeight(null);
}
}
public GameObject(Image img, double x, double y, int width,
int height) {
this.img = img;
this.x = x;
this.y = y;
this.width = width;
this.height = height;
}
public GameObject(Image img, double x, double y, int speed, int width,
int height) {
this.img = img;
this.x = x;
//這只是構造方法初始化化時的值,並不能作爲約束
this.y = y;
this.speed = speed;
this.width = width;
this.height = height;
}
public GameObject() {
}
/**
* 返回物體對應矩形區域,便於後續在碰撞檢測中使用
* @return
*/
public Rectangle getRect(){
return new Rectangle((int)x,(int) y, width, height);
}
}
package Thunder_one;
import java.awt.Color;
import java.awt.Font;
import java.awt.Frame;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import javax.swing.Timer;
import javax.swing.JFrame;
import javax.swing.JLabel;
/**
* :雷電第一個版本:
* 屏幕有很多隨機生成的炮彈,我們需要控制飛機來躲避炮彈
* 存活時間越長,得分越高
*
*
*/
public class GameUI extends Frame {
Date startTime = new Date(); // 遊戲起始時刻
Date endTime; // 遊戲結束時刻
private int score = 0;// �?獲分�?
// 界面寬和�?
public static int UIWidth = 1000;
public static int UIHeigh = 1000;
// 炮彈的數量
int ShellNum = 20;
// 飛機的速度
int PlSpeed = 20;
// 將背景和飛機圖片定義爲成員變量
Image background = GameUtil.getImage("images/back3.jpg");
Image planeImg = GameUtil.getImage("images/plane1.png");
Image shootImg = GameUtil.getImage("images/shoot1.png");
Plane plane1 = new Plane(planeImg, 500, 800, PlSpeed, 100, 100);
// 炮彈的數�?
Shell[] shellArr = new Shell[ShellNum];
// 創建爆炸對象
Explode bao;
// 爆炸的輪播圖
Image[] imgs = new Image[16];
// 主界面
public void GFrame() {
this.setTitle("雷電");
this.setSize(UIWidth, UIHeigh);
this.setLocationRelativeTo(null);
// 增加關閉窗口監聽,這樣用戶點擊右上角關閉圖標,可以關閉遊戲程�?
this.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
System.exit(0);
}
});
this.addKeyListener(plane1);
// 初始化,生成炮彈
for (int i = 0; i < ShellNum; i++) {
shellArr[i] = new Shell();
}
this.setVisible(true);
// 啓動線程
PaintThread pt = new PaintThread();
pt.start();
}
// paint方法作用是:畫出整個窗口及內部內容�被系統自動調用�
public void paint(Graphics g) {
g.drawImage(background, 0, 0, 1000, 1000, null);
plane1.drawMySelf(g);
// 畫出容器中所有的子彈
for (int i = 0; i < shellArr.length; i++) {
shellArr[i].draw(g);
// 檢測碰撞
boolean peng = shellArr[i].getRect().intersects(plane1.getRect());
if (peng) {
plane1.live = false;// 飛機死掉
endTime = new Date();
if (bao == null) {
bao = new Explode(plane1.x, plane1.y);
}
bao.draw(g);
}
}
if (!plane1.live) {
if (endTime == null) {
endTime = new Date();
}
int period = (int) ((endTime.getTime() - startTime.getTime()) / 1000);
printInfo(g, "您的得分爲:" + period + "�?", 50, 120, 260, Color.white);
}
}
// �?後計�?
public void printInfo(Graphics g, String str, int size, int x, int y, Color color) {
Color c = g.getColor();
g.setColor(color);
Font f = new Font("宋體", Font.BOLD, size);
g.setFont(f);
g.drawString(str, x, y);
g.setColor(c);
}
// 定義內部�?
class PaintThread extends Thread {
// 重寫
public void run() {
while (true) {
repaint();// 重畫
try {
Thread.sleep(10);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
// 雙緩衝解決閃�?
private Image offScreenImage = null;
public void update(Graphics g) {
if (offScreenImage == null)
offScreenImage = this.createImage(1000, 1000);
// 這是遊戲窗口的寬度和高度
Graphics gOff = offScreenImage.getGraphics();
paint(gOff);
g.drawImage(offScreenImage, 0, 0, null);
}
// 主方�?
public static void main(String[] args) {
// 類裏面調用窗�?
GameUI gu = new GameUI();
gu.GFrame();
}
}
package Thunder_one;
import java.awt.Image;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.net.URL;
import javax.imageio.ImageIO;
/**
* GameUtil類:加載圖片代碼
*GameUtil獲得程序運行類加載器,加載資源的根目錄,
* 2019年9月22日
*/
public class GameUtil {
//工具類一般將構造器私有化
public GameUtil() {
}
public static Image getImage(String path){
BufferedImage bi = null;
try {
URL u = GameUtil.class.getClassLoader().getResource(path);
bi = ImageIO.read(u);
} catch (IOException e) {
e.printStackTrace();
}
return bi;
}
}
package Thunder_one;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
/**
*
*
* 2019年9月22日
*/
public class Plane extends GameObject implements KeyListener {
boolean left, right, up, down, shoot;
boolean live = true;
public void drawMySelf(Graphics g) {
if(live){
super.drawMySelf(g);
// 這裏可移動,但是下面不行,應該是判斷的問題
// this.x+=2;
// System.out.println(left);
if (left) {
x -= speed;
if (x <= 0) {
x = 0;
}
}
if (right) {
x += speed;
if (x >= 900) {
x = 900;
}
}
if (up) {
y -= speed;
if (y <= 10) {
y = 10;
}
}
if (down) {
y += speed;
if (y >= 900) {
y = 900;
}
}
}
}
public Plane(Image img, double x, double y, int speed, int width, int height) {
super(img, x, y, width, height);
this.speed = speed;
}
public void keyTyped(KeyEvent e) {
}
public void keyPressed(KeyEvent e) {
//System.out.println(e.getKeyCode());
int key = e.getKeyCode();
switch (key) {
case 37:
left = true;
break;
case 38:
up = true;
break;
case 39:
right = true;
break;
case 40:
down = true;
break;
default:
break;
}
}
public void keyReleased(KeyEvent e) {
// System.out.println(e.getKeyCode());
int key = e.getKeyCode();
switch (key) {
case 37:
left = false;
break;
case 38:
up = false;
break;
case 39:
right = false;
break;
case 40:
down = false;
break;
default:
break;
}
}
}
/**
* 2019年9月26日
*/
package Thunder_one;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Image;
import java.util.Random;
import Thunder.GameUtil;
/**
*
*
* 2019年9月26日
*/
public class Shell extends GameObject{
double degree;
Random ra = new Random();
public Shell(){
degree = Math.random()*Math.PI*2;
x = ra.nextInt(700)+250;
y = ra.nextInt(700)+150;
width = ra.nextInt(15)+10;
height = ra.nextInt(15)+10;
speed = ra.nextInt(3)+2;
}
public void draw(Graphics g){
//將外部傳入對象g的狀態保存好
Color c = g.getColor();
g.setColor(Color.yellow);
Image shootImg = GameUtil.getImage("images/shoot1.png");
g.drawImage(shootImg, (int)x, (int)y, width, height, null);
//g.fillOval((int)x, (int)y, width, height);
//炮彈沿着任意角度飛行
x += speed*Math.cos(degree);
y += speed*Math.sin(degree);
//如下代碼,用來實現碰到邊界,炮彈反彈回來(原理和打檯球遊戲一樣)
if(y>GameUI.UIHeigh-height||y<40){
degree = -degree;
}
if(x<0||x>GameUI.UIWidth-width){
degree = Math.PI-degree;
}
//返回給外部,變回以前的顏色
//g.setColor(c);
}
}