1.解释器在MyGameFrame中去调用静态的main方法而不需要创建一个对象。new MyGameFrame()对象,新创建一个窗口,f.launchFrame()用来对窗口的标题(setTitle)、可视化(setVisible)、窗口大小(setSize)、窗口位置(setLocation)信息的初始化以及重写windowClosing方法,确保点击关闭按钮后程序能够完全退出。this指代的是MyGameFrame继承自Frame类中对应的方法。
public class MyGameFrame extends JFrame {
/**
*初始化窗口
*/
public void launchFrame() {
this.setTitle("程序员作品");
this.setVisible(true);
this.setSize(500,500);
this.setLocation(500, 300);
this.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
System.exit(0);
}
});
}
public static void main(String[] args) {
MyGameFrame f = new MyGameFrame();
f.launchFrame();
}
}
2.接下来可以导入背景图片和飞机图片,这里需要定义一个工具类用来返回指定路径文件的图片对象。重写paint方法导入图片
Image plane = GameUtil.getImage("images/plane.png");
Image bg = GameUtil.getImage("images/bg.png");
@Override
public void paint(Graphics g) {//自动被调用,g相当于一只画笔
g.drawImage(bg, 0, 0, null);
g.drawImage(plane, planeX, planeY, null);
}
public class GameUtil {
// 工具类最好将构造器私有化。
private GameUtil() {
}
/**
* 返回指定路径文件的图片对象
* @param path
* @return
*/
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;
}
}
代码效果如下:
3.我们要想看见飞机动起来,就要对飞机图片的位置进行更改,并且要不断刷新飞机的位置。这里我们会用到多线程。
//帮助我们反复重画窗口
class PaintThread extends Thread{
@Override
public void run() {
while(true) {
System.out.println("窗口画一次");//在需要检测的位置调用打印的方法,便于查看功能是否实现。
repaint();//重画
try {
Thread.sleep(40);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
在launchFrame()方法中还要添加下面的代码,来启动窗口线程。
new PaintThread().start();//启动窗口的线程
此时我们运行程序发现反复画窗口的功能已经实现,但是窗口不停闪烁。所以我们还要在MyGameFrame类中加上如下代码解决窗口闪烁问题。具体原因有兴趣的可以查一下资料。
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);
}
最终是如下效果:
4.下面开始进入正式环节,首先我们知道Java是一门面向对象的语言,不想C语言那样,所有函数、变量在一块。在做Java项目时我们要有面向对象的思想,将具有某一功能或者说某一个可以看做是一体的代码,我们要学会封装起来,一方面偏于调用,另一方面便于维护。这里我们可以创建一个GameObject类,这个类是游戏中所有物体的父类,其具有所有物体的共有属性。比如位置、大小、速度等。其子类继承其属性,这样可以减少很多代码量。换个角度理解,这里的父类就比作动物类,猫和狗都具有动物的特征,但它们又有自己独有的属性,我们让猫和狗继承动物的属性,再单独添加自己的方法。这样代码看上去更简洁,有序。
/**
* 游戏物体的父类
* @author hp
*
*/
public class GameObject {
Image img;
double x,y;
int speed;
int width,height;
public void drawSelf(Graphics g) {
g.drawImage(img, (int)x, (int)y, null);
}
public GameObject(Image img, double x, double y, int speed, int width, int height) {
super();
this.img = img;
this.x = x;
this.y = y;
this.speed = speed;
this.width = width;
this.height = height;
}
public GameObject(Image img, double x, double y) {
super();
this.img = img;
this.x = x;
this.y = y;
}
public GameObject() {
}
/**
* 返回物体所在的矩形,便于碰撞检测
* @return
*/
public Rectangle getRect() {//返回类型是某个类的实例。
return new Rectangle((int)x,(int)y,width,height);
}
}
5.对于飞机我们可以单独写一个类,来编写飞机的各种参数,飞机要动起来首先得有一个速度,其次我们还要用键盘去控制飞机的移动,这就涉及到按键的读取,用boolean类型的变量去存放此时按键的情况。由于飞机是移动的,所以我们飞机的图片不能像背景一样直接放在那。我们创建一个方法用来画出飞机每时每刻的位置。由于之前我们用了多线程,图片是一直刷新的,所以我们一按下一个键,图片相应的坐标值就会减去速度从而达到图片移动的效果。
public class Plane extends GameObject{
int speed = 3;
boolean left,up,right,down;
//如果出现问题,调试时追溯往回在相应的位置调用打印方法
public void drawSelf(Graphics g) {
g.drawImage(img, (int)x, (int)y, null);
if(left) {
x-=speed;
}
if(right) {
x+=speed;
}
if(up) {
y-=speed;
}
if(down) {
y+=speed;
}
}
public Plane(Image img,double x,double y) {
this.img = img;
this.x = x;
this.y = y;
}
//按下某个键,增加对应的方向
public void addDirection(KeyEvent e) {
switch(e.getKeyCode()) {
case KeyEvent.VK_LEFT:
left = true;
break;
case KeyEvent.VK_UP:
up = true;
break;
case KeyEvent.VK_RIGHT:
right = true;
break;
case KeyEvent.VK_DOWN:
down = true;
break;
default:
break;
}
}
//按下某个键,取消对应的方向
public void minusDirection(KeyEvent e) {
switch(e.getKeyCode()) {
case KeyEvent.VK_LEFT:
left = false;
break;
case KeyEvent.VK_UP:
up = false;
break;
case KeyEvent.VK_RIGHT:
right = false;
break;
case KeyEvent.VK_DOWN:
down = false;
break;
default:
break;
}
}
}
由于程序是按顺序执行的,在调用了按下键改变位置的方法后,立刻又将方向键的boolean值变为false。这里并不冲突。
设置了飞机的类,我们还要在paint方法中加入画飞机的调用。
plane.drawSelf(g);//画飞机
Plane的构造方法是用来给对应参数初始化赋值,this指代的变量继承自父类GameObject类。
drawImage方法继承自父类Object。
这里介绍一下,Java的按键事件KeyEvent
按键事件可以利用键盘来控制和执行一些动作,或者从键盘上获取输入,只要按下,释放一个键或者在一个组件上敲击,就会触发按键事件。KeyEvent对象描述事件的特性(按下,放开,或者敲击一个键)和对应的值。java提供KeyListener接口处理按键事件。
当按下一个键时会调用KeyPressed处理器,当松开一个键时会调用KeyReleased处理器,当输入一个统一编码时会调用KeyTyped处理器。如果这个键不是统一码(如功能键,修改键,动作键和控制键)
每个按键事件有一个相关的按键字符和按键代码,分别由KeyEvent中的getKeyChar()和getKeyCode()方法返回。
getKeyChar(): char 返回这个事件中和键相关的字符
getKeyCode(): int 返回这个事件中和键相关的整数键
keyPressed(e: KeyEvent) 在源组件上按下一个键后被调用
KeyReleased(e: KeyEvent) 在源组件上释放一个键后被调用
KeyTyped(e: KeyEvent) 在源组件上按下一个键然后释放该键后被调用
按键常量
按键常量 | 含义 | 按键常量 | 含义 |
---|---|---|---|
VK_HOME | Home键 | VK_CONTROL | 控制键 |
VK_END | End键 | VK_SHIFT | shift键 |
VK_PGUP | page up键 | VK_BACK_SPACE | 退格键 |
VK_PGDN | page down键 | VK_CAPS_LOCK | 大小写锁定键 |
VK_UP | 上箭头 | VK_NUM_LOCK | 小键盘锁定键 |
VK_DOWN | 下箭头 | VK_ENTER | 回车键 |
VK_LEFT | 左箭头 | VK_UNDEFINED | 未知键 |
VK_RIGHT | 右箭头 | VK_F1–VK_F12 | F1 – F12 |
VK_ESCAPE | Esc键 | VK_0 --VK_9 | 0 — 9 |
VK_TAB | Tab键 | VK_A --VK_Z | A----Z |
在MyGameFrame类中,我们添加一个键盘监听的内部类,用来获取键盘的按下与释放。有关内部类的相关知识,欢迎浏览我转载的博客https://blog.csdn.net/weixin_44139445/article/details/87894705。
//定义键盘监听的内部类
class KeyMonitor extends KeyAdapter {
@Override
public void keyPressed(KeyEvent e) {
plane.addDirection(e);
}
@Override
public void keyReleased(KeyEvent e) {
plane.minusDirection(e);
}
}
同时在初始化窗口的方法中添加addKeyListener方法的调用。
addKeyListener(new KeyMonitor());
当然在类的开始我们还要new一个Plane对象。只有当类被实例化之后,我们才能去调用其中的非静态方法(静态属性和静态方法一般可直接调用,非静态的一般都要实例化对象后才可以使用 静态方法里不能用static )。有关静态方法的相关知识欢迎浏览我转载的 对java中public、static的理解
Plane plane = new Plane(planeImg,250,250);
另外,这里提一点建议,对于项目中经常使用到的字面量,我们可以用变量来代替。写一个存放变量的类Constant ,我们把窗口的高度和宽度放进去。
public class Constant {
public static final int GAME_WIDTH = 500;
public static final int GAME_HEIGHT = 500;
}
6.下面我们开始设置炮弹,最简单一个点向四周发射炮弹,炮弹碰到窗口边缘会反弹,碰到飞机会使游戏结束。
/**
* 炮弹类
* @author hp
*
*/
public class Shell extends GameObject{
double degree;
public Shell() {//炮弹的构造方法用来初始化炮弹
x = 200;
y = 200;
width = 10;
height = 10;
speed = 2;
degree = Math.random()*Math.PI*2;
}
public void draw(Graphics g) {
Color c = g.getColor();
g.setColor(Color.yellow);
g.fillOval((int)x, (int)y, width, height);//画一个实心的圆
//炮弹沿着任意角度去飞
x += speed*Math.cos(degree);
y += speed*Math.sin(degree);
if(x<0||x>Constant.GAME_WIDTH-width) {
degree = Math.PI-degree;
}
if(y<40||y>Constant.GAME_HEIGHT-height) {
degree = -degree;
}
g.setColor(c);
}
}
有了炮弹的基础数据我们开始实例化一定数量的炮弹。实例化只是生成了一定数量具有某些属性的炮弹。而draw方法在窗口的具体位置显示有颜色的炮弹,并且判断了炮弹的飞行方向和边界判断。
Shell[] shells = new Shell[50];
//画出所有的炮弹
for(int i=0;i<shells.length;i++) {
shells[i].draw(g);
}
在初始化方法中,我们还要加入炮弹的初始化。
//初始化50个炮弹
for(int i=0;i<shells.length;i++) {
shells[i] = new Shell();
}
此时的效果图为:
7.现在已经完成了一大半了,还剩下炮弹与飞机的碰撞检测、爆炸效果和计时功能。首先是碰撞检测,我们要知道,飞机也好,炮弹也好都可以看做一个个矩形区域。此时我们可以调用之前在GameObject类添加的Rectangle类 getRect()方法。返回的Rectangle类的实例,即对应物体的矩形选区。我们将飞机和炮弹的碰撞检测放在画炮弹的地方。
boolean peng = shells[i].getRect().intersects(plane.getRect());
上面的代码是当炮弹与飞机所在的矩形相交时返回true,否则返回false。
//画出所有的炮弹
for(int i=0;i<shells.length;i++) {
shells[i].draw(g);
//飞机和炮弹的碰撞检测
boolean peng = shells[i].getRect().intersects(plane.getRect());
}
然后是爆炸效果,我们准备了爆炸的图片,当这些图片在短时间连续显示时,我们可以看到爆炸的效果。其实游戏中的很多动画都是图片一帧帧连起来显示得到的。像拳皇的各种动作招式也是图片短时间显示的结果。
/*
* 爆炸类
*/
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;
}
}
用static声明的局部变量存在于程序的整个执行期间,分配在全局变量区,但是只能在函数体内访问。
用static声明局部数组可避免在每次调用程序时都建立和初始化数组以及在每次退出函数时撤销数组,每次操作完成都会修改全局区的数据。
用static声明的局部变量和数组,如果程序员没有明确地初始化,那么编译器把它初始化为0.
一般情况下,如果有些代码必须在项目启动的时候就执行的时候,需要使用静态代码块,这种代码是主动执行的 。
实例化对象时构造方法是为了在创建对象时就初始化对象。
为了在飞机碰到炮弹时立刻爆炸,我们在画炮弹的后面还要加上爆炸图片的显示。
Explode bao;
if(peng) {
plane.live = false;
if(bao == null) {
bao = new Explode(plane.x, plane.y);
}
bao.draw(g);
}
最后是游戏时间的显示。要在类一加载时就new Date();记录开始时间,游戏结束时,endTime = new Date();period就是结束时间减去开始时间,即游戏经过时间/1000(毫秒化为秒)。
Date startTime = new Date();
Date endTime;
int period;//游戏持续时间
if(peng) {
plane.live = false;
if(bao == null) {
bao = new Explode(plane.x, plane.y);
endTime = new Date();
period = (int)((endTime.getTime()-startTime.getTime())/1000);
}
bao.draw(g);
}
if(!plane.live) {
g.setColor(Color.red);
g.setFont(new Font("宋体",Font.BOLD,50));
g.drawString("时间:"+period+"秒", 200, 200);
}
g.setColor(c);