《Java小游戏实现》:坦克大战
前面写了一个简单的聊天小程序,今天开始就写一个坦克大战的游戏,算是对Java相关小知识点的一个应用。
这个游戏的完成,我们也是分步完成,逐步累加,一个一个小功能的添加,最后直至完成整个游戏的开发。
第一步:写一个界面
public class TankClient extends JFrame{
public static void main(String[] args) {
new TankClient().launchFrame();
}
public void launchFrame(){
this.setTitle("坦克大战");
this.setLocation(300, 400);
this.setSize(300, 400);
//this.setBackground(Color.RED);
this.getContentPane().setBackground(Color.RED);
//为关闭窗口添加响应
this.addWindowListener(new WindowAdapter(){
@Override
public void windowClosing(WindowEvent e) {
System.exit(0);
}
});
//设置是否允许用户改变窗口的大小,这里限制下,不允许
this.setResizable(false);
this.setVisible(true);
}
}
以上就是为坦克大战写了一个界面,这是我们的第一步,算是完成了。
在完成这个界面时,遇到了一点小小的问题,这里有必要说明下,本来我想将这个界面设置一个背景颜色,但是使用
this.setBackground(Color.RED);就是不能成功,于是百度了下,这是由于我这里继承的是JFrame,而不是Frame,如果是继承的是Frame,则就可以通过this.setBackground(Color.RED)来设置颜色。如果我们选择的是继承JFrame,则需要使用this.getContentPane().setBackground(Color.RED)来设置背景颜色。
出现上面的原因如下:Frame和JFrame的窗口层次结构不同,JFrame的窗口包括:JFrame、Root Pane、Layered pane、Content Pane、Glass Pane;而Frmae窗口包括:Frame、Content Pane。
其次,窗口背景颜色是指直接调用JFrame或者Frame的setBackground(Color color)方法设置后显示出来的颜色。其实,JFrame在你直接调用这个方法后,你的确设置了背景颜色,而你看到的却不是直接的JFrame或者Frame,而是JFrame.getContentPane().而JFrame上的contentPane默认是Color.WHITE的,所以,无论你对JFrame或者Frame怎么设置背景颜色,你看到的都只是contentPane.因此,
this.setBackground(Color.RED);
his.getContentPane().setVisible(false);//得到contentPane容器,设置为不可见
也可以解决JFram设置的背景颜色的问题。
第二步:在界面上显示一个坦克图标
有界面,没有坦克在上面,肯定不是我们想要的,是吧,因此,我们就先完成这个功能。
在这里我们用一个圆来代表一个坦克。绘图代码如下:
@Override
public void paint(Graphics g) {
Color c = g.getColor();
g.setColor(Color.RED);
g.fillOval(50, 50, 30, 30);
g.setColor(c);
}
完整的代码如下:
public class TankClient extends Frame{
public static void main(String[] args) {
new TankClient().launchFrame();
}
@Override
public void paint(Graphics g) {
Color c = g.getColor();
g.setColor(Color.RED);
g.fillOval(50, 50, 30, 30);
g.setColor(c);
}
public void launchFrame(){
this.setTitle("坦克大战");
this.setLocation(300, 400);
this.setSize(600, 400);
//this.getContentPane().setBackground(Color.GRAY);
this.setBackground(Color.GRAY);
//为关闭窗口添加响应
this.addWindowListener(new WindowAdapter(){
@Override
public void windowClosing(WindowEvent e) {
System.exit(0);
}
});
//设置是否允许用户改变窗口的大小,这里限制下,不允许
this.setResizable(false);
this.setVisible(true);
}
}
细心的朋友可能会发现,第一个版本窗口是继承的是JFrame,而我这个版本是继承的是Frame,是出现了这样一个问题,在我重写了paint方法之后,继承JFrame实现的窗口使用this.getContentPane().setBackground(Color.GRAY);来设置颜色又不是我们所期望的。
下面左边的图是使用继承Frame+this.setBackground(Color.GRAY);
右边的图是使用继承JFrame+this.getContentPane().setBackground(Color.GRAY);结果居然是白色。
所以说Frame和JFrame还是有一定的差别的,这里不再仔细研究,而是直接使用Frame来完成。
第三步:控制坦克移动
经过第二步的处理,可以在界面上显示一个坦克了,那么应该如何来控制坦克移动了。下面我们来研究并完成。
我们再实现通过键盘的方向键来控制坦克移动之前,我们实现这样一个功能,什么功能呢??就是:坦克在某一个方向上移动。
坦克移动,简单来说,就是坦克所在的位置。即每次改变坦克的位置之后重画调用paint函数。因此,我们将坦克的位置用x/y表示。然后写一个线程专门用于重画。
实现代码如下:
public class TankClient extends Frame{
public static void main(String[] args) {
new TankClient().launchFrame();
}
//坦克所在的位置的坐标
private int x=50;
private int y=50;
@Override
public void paint(Graphics g) {
Color c = g.getColor();
g.setColor(Color.RED);
g.fillOval(x, y, 30, 30);
g.setColor(c);
y+=5;//该表坦克的位置
}
public void launchFrame(){
this.setTitle("坦克大战");
this.setLocation(300, 400);
this.setSize(600, 400);
this.setBackground(Color.GRAY);
//为关闭窗口添加响应
this.addWindowListener(new WindowAdapter(){
@Override
public void windowClosing(WindowEvent e) {
System.exit(0);
}
});
//设置是否允许用户改变窗口的大小,这里限制下,不允许
this.setResizable(false);
this.setVisible(true);
new Thread(new MyRepaint()).start();
}
private class MyRepaint implements Runnable{
@Override
public void run() {
while(true){
//每50ms重画一次
repaint();
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
通过开启一个线程以及在paint函数中改变坦克的位置(y+=5),这样,坦克就会一直往下面移动。
实现了坦克一直往下移动之后,我们就可以实现通过键盘来控制坦克的移动了。
通过键盘来实现坦克的移动,我们只需要添加一个键盘按键的监听事件即可,根据键盘按键的不同,做相应的一个处理。
1、首先先写一个键盘按键监听类,键盘按键监听类,有两种实现方式,一种是继承KeyAdapter(重写我们需要的方法,比较方便);另一种是实现KeyListener接口(需要实现3个抽象方法)。
2、然后使用组件的 addKeyListener 方法将从该类所创建的侦听器对象向该组件注册。按下、释放或键入键时生成键盘事件。
private class KeyMonitor extends KeyAdapter{
@Override
public void keyPressed(KeyEvent e) {
int key=e.getKeyCode();
switch(key){
case KeyEvent.VK_LEFT:
x -= 5;
break;
case KeyEvent.VK_UP:
y -= 5;
break;
case KeyEvent.VK_RIGHT:
x += 5;
break;
case KeyEvent.VK_DOWN:
y += 5;
break;
}
}
}
完整代码如下:
public class TankClient extends Frame{
public static void main(String[] args) {
new TankClient().launchFrame();
}
//坦克所在的位置的坐标
private int x=50;
private int y=50;
@Override
public void paint(Graphics g) {
Color c = g.getColor();
g.setColor(Color.RED);
g.fillOval(x, y, 30, 30);
g.setColor(c);
}
public void launchFrame(){
this.setTitle("坦克大战");
this.setLocation(300, 400);
this.setSize(600, 400);
this.setBackground(Color.GRAY);
//为关闭窗口添加响应
this.addWindowListener(new WindowAdapter(){
@Override
public void windowClosing(WindowEvent e) {
System.exit(0);
}
});
//设置是否允许用户改变窗口的大小,这里限制下,不允许
this.setResizable(false);
this.setVisible(true);
new Thread(new MyRepaint()).start();
this.addKeyListener(new KeyMonitor());
}
private class MyRepaint implements Runnable{
@Override
public void run() {
while(true){
//每50ms重画一次
repaint();
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
private class KeyMonitor extends KeyAdapter{
@Override
public void keyPressed(KeyEvent e) {
int key=e.getKeyCode();
switch(key){
case KeyEvent.VK_LEFT:
x -= 5;
break;
case KeyEvent.VK_UP:
y -= 5;
break;
case KeyEvent.VK_RIGHT:
x += 5;
break;
case KeyEvent.VK_DOWN:
y += 5;
break;
}
}
}
}
以上就通过键盘的上下左右键来移动界面上的坦克了。
第四步:面向对象的思想构造坦克类
对于面向对象编程的思想,我们首先要考虑的事情就是有哪些类和对象,每个类中有哪些属性和方法。
上面的版本都还没有利用面向对象的思想。这一小节,我们就来构造坦克类,这样便于面向对象编程。
首先,我们构造一个Tank类,Tank类中有两个属性,分别为坦克位置x和y。包含的方法应该有:draw方法和keyMonitor方法。即自己画图的方法和自己对键盘的响应的方法。
Tank类的代码如下:
public class Tank {
//坦克所在的位置坐标
private int x;
private int y;
public Tank(int x, int y) {
super();
this.x = x;
this.y = y;
}
public int getX() {
return x;
}
public void setX(int x) {
this.x = x;
}
public int getY() {
return y;
}
public void setY(int y) {
this.y = y;
}
//绘图
public void draw(Graphics g){
Color c = g.getColor();
g.setColor(Color.RED);
g.fillOval(x, y, 30, 30);
g.setColor(c);
}
//响应键盘按键的方法
public void keyMonitor(KeyEvent e){
int key=e.getKeyCode();
switch(key){
case KeyEvent.VK_LEFT:
x -= 5;
break;
case KeyEvent.VK_UP:
y -= 5;
break;
case KeyEvent.VK_RIGHT:
x += 5;
break;
case KeyEvent.VK_DOWN:
y += 5;
break;
}
}
}
TankClient类的方法重构后的代码如下:
public class TankClient extends Frame{
private Tank tk=new Tank(50,50);
public static void main(String[] args) {
new TankClient().launchFrame();
}
@Override
public void paint(Graphics g) {
//直接调用坦克至圣的draw方法
tk.draw(g);
}
public void launchFrame(){
this.setTitle("坦克大战");
this.setLocation(300, 400);
this.setSize(600, 400);
this.setBackground(Color.GRAY);
//为关闭窗口添加响应
this.addWindowListener(new WindowAdapter(){
@Override
public void windowClosing(WindowEvent e) {
System.exit(0);
}
});
//设置是否允许用户改变窗口的大小,这里限制下,不允许
this.setResizable(false);
this.setVisible(true);
new Thread(new MyRepaint()).start();
this.addKeyListener(new KeyMonitor());
}
private class MyRepaint implements Runnable{
@Override
public void run() {
while(true){
//每50ms重画一次
repaint();
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
private class KeyMonitor extends KeyAdapter{
@Override
public void keyPressed(KeyEvent e) {
tk.keyMonitor(e);
}
}
}
解决闪烁现象
什么是闪烁现象呢??可以理解为丢帧。即在视觉上少了一些中间过程的变化。
当使用graphics对象在窗体中绘制多种图像时,每次窗体改变都要调用repaint()函数,相当于把所有图像按顺序重新绘制一次,所以就出现了闪烁现象。 这一点在棋类游戏更加明显(想象一下每次落子都需要重新绘制棋盘上的几十上百个棋子是什么概念)
解决的方法是使用双缓冲技术。也就是说,先将所有图像都绘制在一个缓冲区中,形成一张图片,这样每次调用repaint()只需要将该缓冲图片画到窗体上就可以了。
在本例中,只需要引入一个画布,并重写update方法
private Image offScreenImage = null;
@Override
public void update(Graphics g) {
if (offScreenImage == null) {
offScreenImage = this.createImage(GAME_WIDTH, GAME_HEIGHT);
}
Graphics goffScreen = offScreenImage.getGraphics();// 重新定义一个画虚拟桌布的画笔//
Color c = goffScreen.getColor();
goffScreen.setColor(Color.GRAY);
goffScreen.fillRect(0, 0, GAME_WIDTH, GAME_HEIGHT);
goffScreen.setColor(c);
//调用paint方法
paint(goffScreen);
//最后,将虚拟画布作为整体直接画上去,避免闪烁的发生
g.drawImage(offScreenImage, 0, 0, null);
}
@Override
public void paint(Graphics g) {
//直接调用坦克至圣的draw方法
tk.draw(g);
}
未完,待续