本教程会举例用Java实现简单的小球碰壁反弹游戏,效果如图所示:
JFrame: 窗体部分
首先创建一个窗体界面,下面举一个简单的例子(Jframe是自己定义的类名,注意和JFrame的区别)
import javax.swing.JFrame; //没导入就没卵用
public class Jframe {
public static void main(String[] args)
{
JFrame frame = new JFrame("小球碰壁"); //创建Jframe对象,参数是窗体标题
frame.setSize(300, 300); //设置窗口大小
frame.setVisible(true); //设置可见性,没设置的话就看不见
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); //这个是退出时的操作,没加这一行可能关闭后会继续运行
}
}
通过继承JFrame也可以
import javax.swing.JFrame;
public class Game extends JFrame //继承父类Jframe
{
public static void main(String[] args) {
Game tennis=new Game(); //创建game对象
}
Game() //自己的构造函数,同时也会调用父类的构造函数来创建对象
{
this.setTitle("碰壁球游戏");
this.setSize(500,500);
this.setVisible(true);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}
}
如图:
JPanel: 绘图部分
有窗体接下来就是绘图部分,绘图就需要用一个画布JPanel(Jpanel是自己定义的类名,注意和JPanel的区别)
import javax.swing.JPanel;
import javax.swing.JFrame;
import java.awt.*;
public class Jpanel extends JPanel{
public void paint(Graphics g) //paint方法中的Graphics g是从Graphic继承的对象
{
g.setColor(Color.BLUE); //设置颜色
g.fillOval(0, 0, 30, 30); //参数分别为坐标x,y,球的宽度,高度
g.drawString("这是一个小球", 0, 50); //参数分别为字符串,坐标x,y
}
public static void main(String[] args) {
JFrame frame = new JFrame("Tennis");
frame.add(new Jpanel());
frame.setSize(300, 300);
frame.setVisible(true);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}
}
如图:
最后整合一下前面两部分所学的知识创建一个Game类。因为Java不支持多继承所以创建多一个继承JPanel的Draw类。
import javax.swing.JFrame;
import javax.swing.JPanel;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.geom.Ellipse2D;
public class Game extends JFrame //继承父类Jframe
{
public static void main(String[] args) {
Game tennis=new Game(); //创建game对象
}
Game() //自己的构造函数,同时也会调用父类的构造函数来创建对象
{
Draw ball=new Draw();
this.add(ball);
this.setTitle("碰壁球游戏");
this.setSize(600,600);
this.setVisible(true);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}
}
class Draw extends JPanel //这里创建一个继承JPanel的子类
{
public void paint(Graphics g) {
super.paint(g); //调用父类的paint
Graphics2D g2d = (Graphics2D) g; //进行对象转型,因为Graphic2D的方法比较好也比较丰富,所以这里用Graphics2D,Graphics是旧的类
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON); //这个是防止边缘有锯齿的
g2d.fillOval(0, 0, 60, 60);
}
}
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)的效果图.
接下来要让球移动,球的位置是根据当前所赋值的坐标决定的,所以每次绘制小球的时候我们需要通过更改x,y让小球达到移动的效果,这样的话我们就把刚才的代码改成g2d.fillOval(x, y, 30, 30);然后再通过repaint()方法重新绘制。
原理就是一直更新并重新绘制新的小球使其达到移动的效果。
因此这里打算写一个moveBall()的方法来不断改变x,y的值。
稍微修改一下之前的代码:
import javax.swing.JFrame;
import javax.swing.JPanel;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.geom.Ellipse2D;
public class Game extends JFrame //继承父类Jframe
{
public static void main(String[] args) throws InterruptedException //这个是抛出异常用来防止线程异常的,需要加
{
Game tennis=new Game(); //创建game对象
Draw ball=new Draw(); //绘制一个新的小球,因为要循环调用所以放在Draw类里面x,y会一直重新定义成0所以在这里定义
while (true) {
tennis.add(ball); //循环添加新的小球
ball.moveBall(); //更新小球的位置(只是更新位置,还没绘制);
tennis.repaint(); //重新绘制
Thread.sleep(5); //延迟5毫秒再绘制,不然小球会移动很快一闪而过
}
}
Game()
{
this.setTitle("碰壁球游戏");
this.setSize(600,600);
this.setVisible(true);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}
}
class Draw extends JPanel
{
int x=0; //小球的默认位置
int y=0;
void moveBall() //这个方法就是不断更新小球的位置
{
x++;
y++;
}
public void paint(Graphics g) {
super.paint(g);
Graphics2D g2d = (Graphics2D) g;
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
g2d.fillOval(x, y, 60, 60);
}
}
运行一下小球开始移动了,可以看见小球不断循环的向右下角移动.
然后再也回不来了~
然而并没有什么卵用,我们要让小球碰壁再弹回来,改变小球的方向和速度。这里只要改一下moveBall()方法便可以实现。
class Draw extends JPanel
{
int x=0; //小球的默认位置
int y=0;
int incx=1; //这是小球位置要移动的方向
int incy=1;
void moveBall() //这个方法就是不断更新小球的位置
{
if(x+incx>getWidth()-60) //如果小球移动后的位置超出窗体范围的话,移动方向就是一直-1;因为要考虑球的大小所以-60
incx=-1;
if(x+incx<0) //同理
incx=1;
if(y+incy>getHeight()-60)
incy=-1;
if(y+incy<0)
incy=1;
x+=incx;
y+=incy;
}
public void paint(Graphics g) {
super.paint(g);
Graphics2D g2d = (Graphics2D) g;
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
g2d.fillOval(x, y, 60, 60);
}
}
现在小球碰到窗体边缘就会改变方向了,看起来就像是碰壁反弹。
为了使代码看起来更简洁这里另外创建一个Ball类用来存放小球的各种属性方法。这里改了一下窗体高度。
现在总共有两个类。
Game类
import javax.swing.JFrame;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
public class Game extends JFrame //继承父类Jframe
{
Ball ball=new Ball(this); //这里创建一个Ball对象,this作为参数让Ball类可以获取Game的成员信息
private void move() //这里是move方法用来调用Ball类中的moveBall
{
ball.moveBall();
}
public static void main(String[] args) throws InterruptedException //这个是抛出异常用来防止线程异常的,需要加
{
Game tennis=new Game(); //创建game对象
while (true) {
tennis.move(); //因为访问限制所以通过move方法来调用Ball类中的moveBall方法
tennis.repaint(); //重新绘制
Thread.sleep(5); //延迟5毫秒再绘制,不然小球会移动很快一闪而过
}
}
Game()
{
this.setTitle("碰壁球游戏");
this.setSize(600,800);
this.setVisible(true);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}
public void paint(Graphics g) {
super.paint(g); //不添加这个的话旧的小球不会被擦除
Graphics2D g2d = (Graphics2D) g;
ball.paint(g2d); //调用ball类中的paint方法
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
}
}
Ball类
import javax.swing.*;
import java.awt.Graphics2D;
public class Ball extends JPanel{
int x=0; //小球的默认位置
int y=0;
int incx=1; //这是小球位置要移动的方向
int incy=1;
private Game tennis;
public Ball(Game tennis)
{
this.tennis=tennis;
}
void moveBall() //这个方法就是不断更新小球的位置
{
if(x+incx>tennis.getWidth()-60) //如果小球移动后的位置超出窗体范围的话,移动方向就是一直-1;因为要考虑球的大小所以-60
incx=-1;
if(x+incx<0) //同理
incx=1;
if(y+incy>tennis.getHeight()-60)
incy=-1;
if(y+incy<0)
incy=1;
x+=incx;
y+=incy;
}
public void paint(Graphics2D g)
{
g.fillOval(x, y, 60, 60);
}
}
运行结果跟刚才的一样。
小球碰壁游戏有一个长形状方块可以由玩家控制,碰到小球的话可以让小球反弹回去。所以接下来就是事件监听部分,监听键盘的事件才可以控制方块要移动的方向。
这里举出一个简单的键盘监听事件的例子
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import javax.swing.JFrame;
import javax.swing.JPanel;
@SuppressWarnings("serial")
public class Keyboard extends JPanel {
String str="还没按";
public Keyboard()
{
KeyListener listener = new MyKeyListener(this); //创建一个键盘监听器对象
addKeyListener(listener); //注册
setFocusable(true); //
}
public static void main(String[] args) {
JFrame frame = new JFrame("键盘监听器");
Keyboard keyboardExample = new Keyboard();
frame.add(keyboardExample); //添加对象
frame.setSize(200, 200);
frame.setVisible(true);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}
public void paint(Graphics g) {
super.paint(g);
Graphics2D g2d = (Graphics2D) g;
g2d.setColor(Color.black);
g.setFont(new Font("宋体",Font.BOLD,60));
g2d.drawString(str,100,70);
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
}
public class MyKeyListener implements KeyListener //这个是实现接口,所以必须重新定义里面的方法
{
private Keyboard kb;
MyKeyListener (Keyboard kb)
{
this.kb=kb;
}
@Override //检查下面的方法在父类中是否存在
public void keyTyped(KeyEvent e)
{
}
@Override
public void keyPressed(KeyEvent e) //按下键盘事件
{
str=KeyEvent.getKeyText(e.getKeyCode());
kb.repaint();
//System.out.println("按下的按键"+KeyEvent.getKeyText(e.getKeyCode()));
}
@Override
public void keyReleased(KeyEvent e) //放开键盘事件
{
//System.out.println("松开的按键="+KeyEvent.getKeyText(e.getKeyCode()));
}
}
}
运行结果如图:
所按下的按键对应的文本都会显示在窗体上。
现在按的按键都能监听到了,因为上面的MyKeyListener类中只用了一次,因此我们可以改用为匿名对象并简化代码。
package Tutorial;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import javax.swing.JFrame;
import javax.swing.JPanel;
@SuppressWarnings("serial")
public class Keyboard2 extends JPanel {
public Keyboard2() {
KeyListener listener = new KeyListener() {
@Override
public void keyTyped(KeyEvent e) {
}
@Override
public void keyPressed(KeyEvent e) {
System.out.println("你按下了"+KeyEvent.getKeyText(e.getKeyCode()));
}
@Override
public void keyReleased(KeyEvent e) {
System.out.println("你松开了"+KeyEvent.getKeyText(e.getKeyCode()));
}
};
addKeyListener(listener);
setFocusable(true);
}
public static void main(String[] args) {
JFrame frame = new JFrame("键盘监听器");
Keyboard2 keyboardExample = new Keyboard2();
frame.add(keyboardExample);
frame.setSize(200, 200);
frame.setVisible(true);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}
}
然后是创建球拍了,跟ball类一样,创建一个球拍Racquet类,所做的更动只有以下两个类,Game类添加一个键盘监听和Racquet对象,Ball类不变.
Racquet类
import java.awt.Graphics2D;
import java.awt.event.KeyEvent;
import javax.swing.JPanel;
public class Racquet extends JPanel{
int x=0;
int xa=0;
private Game game;
public Racquet(Game game)
{
this.game=game;
}
public void move()
{
if(x+xa<game.getWidth()-120 && x+xa>0)
x+=xa;
}
public void paint(Graphics2D g)
{
g.fillRect(x, 660, 120, 20);
}
public void KeyReleased(KeyEvent e)
{
xa=0;
}
public void KeyPressed(KeyEvent e)
{
if(e.getKeyCode()==KeyEvent.VK_LEFT)
xa=-2;
if(e.getKeyCode()==KeyEvent.VK_RIGHT)
xa=2;
}
}
Game类
import javax.swing.JFrame;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.event.*;
public class Game extends JFrame //继承父类Jframe
{
Ball ball=new Ball(this); //这里创建一个Ball对象,this作为参数让Ball类可以获取Game的成员信息
Racquet racquet=new Racquet(this);
public Game()
{
this.setTitle("碰壁球游戏");
this.setSize(600,800);
this.setVisible(true);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
KeyListener listener=new KeyListener()
{
@Override
public void keyPressed(KeyEvent e) {
// TODO Auto-generated method stub
racquet.KeyPressed(e);
}
@Override
public void keyReleased(KeyEvent e) {
// TODO Auto-generated method stub
racquet.KeyReleased(e);
}
@Override
public void keyTyped(KeyEvent arg0) {
// TODO Auto-generated method stub
}
};
addKeyListener(listener);
setFocusable(true);
}
private void move() //这里是move方法用来调用Ball类中的moveBall
{
ball.moveBall();
racquet.move();
}
public static void main(String[] args) throws InterruptedException //这个是抛出异常用来防止线程异常的,需要加
{
Game tennis=new Game(); //创建game对象
while (true) {
tennis.move(); //因为访问限制所以通过move方法来调用Ball类中的moveBall方法
tennis.repaint(); //重新绘制
Thread.sleep(5); //延迟5毫秒再绘制,不然小球会移动很快一闪而过
}
}
public void paint(Graphics g) {
super.paint(g); //不添加这个的话旧的小球不会被擦除
Graphics2D g2d = (Graphics2D) g;
ball.paint(g2d); //调用ball类中的paint方法
racquet.paint(g2d);
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
}
}
运行如图:
这里我们先在Game类中添加GameOver()函数。
public void gameOver() {
JOptionPane.showMessageDialog(this, "Game Over", "Game Over", JOptionPane.YES_NO_OPTION);
System.exit(ABORT);
}
作用就是当游戏结束会弹出一个提示窗口,点击后并退出游戏。
现在按键盘方向键可以控制球拍的运行了,那么问题来了,球和方块之间并不会产生碰撞。
我们会用两个矩形来检测物体之间的碰撞。java.awt.Rectangle里面有提供一个intersects方法可以用来判断两个方形是否相交,如图3,4会返回true。在图四中小球是没有碰到球拍的,但是看做方形的话有碰到球拍,所以还是会返回true。可以不用在意这些细节,因为本教程的目的主要是用来学习。
改一下Racquet类
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.event.KeyEvent;
import javax.swing.JPanel;
public class Racquet extends JPanel{
private static final int y= 660; //添加三个球拍属性的final变量,因为已经确定下来了不会再改
private static final int WIDTH = 120;
private static final int HEIGHT = 20;
int x=0;
int xa=0;
private Game game;
public Racquet(Game game)
{
this.game=game;
}
public void move()
{
if(x+xa<game.getWidth()-120 && x+xa>0)
x+=xa;
}
public void paint(Graphics2D g)
{
g.fillRect(x, y, WIDTH, HEIGHT);
}
public void KeyReleased(KeyEvent e)
{
xa=0;
}
public void KeyPressed(KeyEvent e)
{
if(e.getKeyCode()==KeyEvent.VK_LEFT)
xa=-2;
if(e.getKeyCode()==KeyEvent.VK_RIGHT)
xa=2;
}
public Rectangle getBounds() //返回当前Rectangle类型的球拍
{
return new Rectangle(x, y, WIDTH, HEIGHT);
}
public int getTopY() //返回球拍所在的水平线
{
return y;
}
}
再改一下Ball类
import javax.swing.*;
import java.awt.Graphics2D;
import java.awt.Rectangle;
public class Ball extends JPanel{
private static final int ballsize=60;
int x=0; //小球的默认位置
int y=0;
int incx=1; //这是小球位置要移动的方向
int incy=1;
private Game tennis;
public Ball(Game tennis)
{
this.tennis=tennis;
}
void moveBall() //这个方法就是不断更新小球的位置
{
if(x+incx>tennis.getWidth()-ballsize) //如果小球移动后的位置超出窗体范围的话,移动方向就是一直-1;因为要考虑球的大小所以-60
incx=-1;
if(x+incx<0) //同理
incx=1;
if(y+incy>tennis.getHeight()-ballsize)
tennis.gameOver(); // 如果碰到底部就游戏结束
if(y+incy<0)
incy=1;
if (collision()) //如果检测到碰撞就改变方向
{
incy = -1;
y = tennis.racquet.getTopY() - ballsize;//这个是矫正球的位置,为了防止碰撞导致的就球拍和小球重叠
}
x+=incx;
y+=incy;
}
public void paint(Graphics2D g)
{
g.fillOval(x, y, ballsize, ballsize);
}
public Rectangle getBounds()//返回Rectangle类型的小球
{
return new Rectangle(x,y,ballsize,ballsize);
}
private boolean collision() //这个是检测碰撞
{
return tennis.racquet.getBounds().intersects(getBounds()); //用intersects方法判断小球是否和球拍相交
}
}
运行玩玩一下,效果图:
到这里基本大功告成了。接下来就是分数部分,稍微添加些代码就可以了。在Game中定义一个score变量用来计数,在collision为true时分数递增,然后通过paint绘制出来。
最终成果:
Class Game
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.event.*;
public class Game extends JFrame //继承父类Jframe
{
Ball ball=new Ball(this); //这里创建一个Ball对象,this作为参数让Ball类可以获取Game的成员信息
Racquet racquet=new Racquet(this);
static int score;
public Game()
{
this.setTitle("碰壁球游戏");
this.setSize(600,800);
this.setVisible(true);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
KeyListener listener=new KeyListener()
{
@Override
public void keyPressed(KeyEvent e) {
// TODO Auto-generated method stub
racquet.KeyPressed(e);
}
@Override
public void keyReleased(KeyEvent e) {
// TODO Auto-generated method stub
racquet.KeyReleased(e);
}
@Override
public void keyTyped(KeyEvent arg0) {
// TODO Auto-generated method stub
}
};
addKeyListener(listener);
setFocusable(true);
}
private void move() //这里是move方法用来调用Ball类中的moveBall
{
ball.moveBall();
racquet.move();
}
public static void main(String[] args) throws InterruptedException //这个是抛出异常用来防止线程异常的,需要加
{
Game tennis=new Game(); //创建game对象
while (true) {
tennis.move(); //因为访问限制所以通过move方法来调用Ball类中的moveBall方法
tennis.repaint(); //重新绘制
Thread.sleep(2); //延迟5毫秒再绘制,不然小球会移动很快一闪而过
}
}
public void gameOver() {
JOptionPane.showMessageDialog(this, "Game Over", "Game Over", JOptionPane.YES_NO_OPTION);
System.exit(ABORT);
}
public void paint(Graphics g) {
super.paint(g); //不添加这个的话旧的小球不会被擦除
Graphics2D g2d = (Graphics2D) g;
ball.paint(g2d); //调用ball类中的paint方法
racquet.paint(g2d);
g2d.setColor(Color.GRAY);
g2d.setFont(new Font("Verdana", Font.BOLD, 50));
g2d.drawString(String.valueOf(score), 20, 120);
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
}
}
Class Ball
import javax.swing.*;
import java.awt.Graphics2D;
import java.awt.Rectangle;
public class Ball extends JPanel{
private static final int ballsize=60;
int x=0; //小球的默认位置
int y=0;
int incx=1; //这是小球位置要移动的方向
int incy=1;
private Game tennis;
public Ball(Game tennis)
{
this.tennis=tennis;
}
void moveBall() //这个方法就是不断更新小球的位置
{
if(x+incx>tennis.getWidth()-ballsize) //如果小球移动后的位置超出窗体范围的话,移动方向就是一直-1;因为要考虑球的大小所以-60
incx=-1;
if(x+incx<0) //同理
incx=1;
if(y+incy>tennis.getHeight()-ballsize)
tennis.gameOver();
if(y+incy<0)
incy=1;
if (collision()) //如果检测到碰撞就改变方向
{
incy = -1;
y = tennis.racquet.getTopY() - ballsize;//这个是矫正球的位置,为了防止碰撞导致的就球拍和小球重叠
tennis.score++;
}
x+=incx;
y+=incy;
}
public void paint(Graphics2D g)
{
g.fillOval(x, y, ballsize, ballsize);
}
public Rectangle getBounds()//返回Rectangle类型的小球
{
return new Rectangle(x,y,ballsize,ballsize);
}
private boolean collision() //这个是检测碰撞
{
return tennis.racquet.getBounds().intersects(getBounds()); //用intersects方法判断小球是否和球拍相交
}
}
Class Racquet
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.event.KeyEvent;
import javax.swing.JPanel;
public class Racquet extends JPanel{
private static final int y= 660; //添加三个球拍属性的final变量,因为已经确定下来了不会再改
private static final int WIDTH = 120;
private static final int HEIGHT = 20;
int x=0;
int xa=0;
private Game game;
public Racquet(Game game)
{
this.game=game;
}
public void move()
{
if(x+xa<game.getWidth()-120 && x+xa>0)
x+=xa;
}
public void paint(Graphics2D g)
{
g.fillRect(x, y, WIDTH, HEIGHT);
}
public void KeyReleased(KeyEvent e)
{
xa=0;
}
public void KeyPressed(KeyEvent e)
{
if(e.getKeyCode()==KeyEvent.VK_LEFT)
xa=-2;
if(e.getKeyCode()==KeyEvent.VK_RIGHT)
xa=2;
}
public Rectangle getBounds() //返回当前Rectangle的球拍
{
return new Rectangle(x, y, WIDTH, HEIGHT);
}
public int getTopY() //返回球拍所在的水平线
{
return y;
}
}
到这里大家就可以根据自己的喜好对小球,球拍的速度做调整,也可以自己添加一个开始按钮来开始游戏。
希望生成可运行文件的可以通过File>Export>Java>Runnable Jar File 来导出jar文件。
本教程到此结束,讲解中的不足之处请见谅,谢谢大家了~