桌面弹球游戏制作总结
1 创建游戏对象
本人在做这个游戏时,没有完全按照书上的做法来做,相反的是我根据以前做过的坦克大战游戏思路对类的具体实现及其设计做了很大的改动并纠正书上明显的错误之处,有幸的是游戏可以运行,不免小小激动了一下,但当挡板吃掉道具时还有一部分小问题,就是当吃了道具以后变长,当在吃另一个变长的道具是就没有反应,问题是由于在处理道具的时候增加了一条if判断语句,但是本人改了半天仍然没有的到完好的实现,所以就到此作罢。当然,除了书上所说
用时间控制器来控制画板的重画之外,在这里我个人又用了多线程的方法,让线程睡眠100ms绘画一次,同样实现了游戏的功能
在这个游戏中,有挡板(Stick),小球(Ball),砖块(Brick)(障碍物),道具(Magic)等物品,把这些物品都抽象成具体的类,类名见括号里的红体字,这些物品都有自己的属性,但是它们也都有公共属性:有属于自己的位置(横坐标x和纵坐标y),有图片image属性,还有运动速度speed属性。所以可以设计一个基类BallComponent包含些共有的属性和相关的方法,让其子类继承。其中道具类Magic的作用就是让挡板变长或者变短。它是一个抽象类,该类中有一个用于道具功能实现的抽象方法,供其子类LongMagic(让道具变长的道具类)和ShortMagic(让道具变短的道具类)实现
。同时游戏中还有一个画板(BallFrame),用于绘制图片,此类还负责完成界面的初始化,监听用户的键盘,而游戏的相关的业务逻辑,比如判断输赢,或者弹球的运动,挡板的运动等,都放在BallService中去处理。最后提供一个单独的类(Main),里面只有main方法,这是个人的习惯问题,当然也可以把它放在BallFrame中去
注意,在平时的开发中,如果发现多个对象之间一些共有的特性或者行为,并且觉得可以使用这些特性或者行为构造一个对象,那么可以考虑建立一个新的对象作为这些对象的父类
2 编辑各个类
BallComponent(父类)
前面说了,这个类是Stick, Ball, Brick 和Magic的父类,拥有自己的位置坐标x,y,还有图片image,速度speed属性,所以该类的代码到目前为止暂且如下(后面还有具体的添加代码的操作)
public class BallComponent {
//imagePath为图片文件路径
public BallComponent(String imagePath,int x,int y) throws IOException{
// 读取图片
this.image = ImageIO.read(new File(imagePath));
// 设置x y 坐标
this.x = x;
this.y = y;
}
//得到图片
public Image getImage() {
return image;
}
// 得到横坐标
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 int getSpeed() {
return speed;
}
private Image image = null;
private int speed = 5;
private int x = -1;
private int y = -1;
}
弹球类(Ball)
Ball是BallComponent的子类,由于小球在运动的时候除了横竖方向上的运动,还有各个角度的斜方向,所以在此把小球的速度分解成横向速度SpeedX和竖向速度SpeedY。也就是说Ball类提供了SpeedX和SpeedY属性,此外游戏开始时,小球处于静止状态(也可以说是死亡状态),所以用一个布尔变量started来表示小球是否开始运动,初始值为false(当然在做游戏时,我沿用的坦克大战里面的思路定义的是isLife,来判断是否活着)。游戏结束后,小球也是处于静止状态,但是不能移动,同样再定义一个布尔变量stop属性来标识小球能否在移动。当然仍然为这些属性定义了相应的getter,setter方法
同时为该类提供一个构造器,所以其代码如下:
public class BallComponent {
// public BallComponent(int panelWidth,int panelHeight,String imagePath) throws IOException {
// this.image = ImageIO.read(new File(imagePath));//读取图片
// //设置x坐标,在这里是把图片设置到画板中间的位置
// this.x =(int)( panelWidth - image.getWidth(null))/2;
// }
// //用图片来构造一个BallComponent
// public BallComponent(String imagePath) throws IOException{
读取图片
// image = ImageIO.read(new File(imagePath));
// }
public BallComponent(String imagePath,int x,int y) throws IOException{
// 读取图片
this.image = ImageIO.read(new File(imagePath));
// 设置x y 坐标
this.x = x;
this.y = y;
}
//得到图片
public Image getImage() {
return image;
}
// 得到横坐标
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 int getSpeed() {
return speed;
}
/**
* 获得包围图片的矩形
* @return Rectangle 矩形
*/
public Rectangle getRect() {
return new Rectangle(this.getX(),this.getY(),this.getImage().getWidth(null),this.getImage().getHeight(null));
}
private Image image = null;
private int speed = 5;
private int x = -1;
private int y = -1;
}
道具类Magic
道具类Magic是一个抽象类,它是BallComponent的子类,又是LongMagic和ShortMagic的父类,此类之提供一个方法magicDo.道具是来出来挡板Stick的,所以magicDo方法里传一个Stick 类型的参数用来完成道具的功能,使得挡板在吃了道具之后变长或者变短,变长变短的处理当然首先需要获得挡板的原有宽度preWidth,在进行处理,这也是在Stick里提供getPreWidth的原因之一。在此游戏中,道具图片随机地隐藏在砖块中,当弹球和砖块撞击时砖块消失,如果该砖块有道具,那么就在画板中画出来并且随即向下运动。
这个类提供了一个使用图片路径和x y坐标的参数的构造器使其子类继承,代码如下
public abstract class Magic extends BallComponent{
/**
* 提供子类调用的构造器
* @param imagePath图片路径
* @param x 横坐标
* @param y纵坐标
* @throws IOException
*/
public Magic(String imagePath, int x,int y) throws IOException{
super(imagePath,x,y);
}
/**
* 道具的功能
* @param stick
* Stick
* @return void
*/
public abstract void magicDo(Stick stick);
}
LongMagic 代码
主要是使得挡板的宽度变成原来的二倍
public class LongMagic extends Magic{
/**
* 让挡板变长的道具
* @param imagePath图片路径
* @param x 横坐标
* @param y 纵坐标
* @throws IOException
*/
public LongMagic(String imagePath, int x, int y) throws IOException{
super(imagePath, x,y);
}
@Override
public void magicDo(Stick stick) {
double sticklength = stick.getImage().getWidth(null);
//如果挡板没变长过
if(stick.getPreWidth() <= sticklength)
stick.setPreWidth((int)stick.getPreWidth()*2);
}
}
ShortMagic 代码
使得挡板变为原来的一半
public class ShortMagic extends Magic {
/**
* 让挡板变短的道具
* @param imagePath图片路径
* @param x 横坐标
* @param y纵坐标
* @throws IOException
*/
public ShortMagic(String imagePath, int x, int y) throws IOException {
super(imagePath, x, y);
// TODO Auto-generated constructor stub
}
@Override
public void magicDo(Stick stick) {
double stickImageLength = stick.getImage().getWidth(null);
//如果挡板没有变短过
if(stick.getPreWidth()>=stickImageLength)
stick.setPreWidth((int)(stick.getPreWidth()*0.5));
}
}
砖块类 Brick
Brick类是BallComponent的一个子类,在游戏中当小球和挡板碰撞时砖块就被消灭不在画板上显示,变成无效的,所以在本类中用一个布尔变量disable属性来标志对象是否有效的效果,其中还包含一个Magic类型的属性magic,
其代码如下
public class Brick extends BallComponent{
public Brick(String imagePath,int magicType, int x, int y) throws IOException{
super(imagePath,x,y);
if(magicType==MAGIC_LONG_TYPE){
//长道具
magic = new LongMagic("img/long.gif",x,y);
}else if(magicType == MAGIC_SHORT_TYPE){
//短道具
magic = new ShortMagic("img/short.gif",x,y);
}
}
/**
* 判断砖块是否有生命
* @return booelean 是否有生命
*/
public boolean isDisable() {
return disable;
}
/**
* 设置砖块的生命状态
* @param isLive
*/
public void setDisable(boolean disable) {
this.disable = disable;
}
/**
* 获得道具
* @return Magic
*/
public Magic getMagic() {
return magic;
}
/**
* 设置砖块要用到的道具
* @param magic
*/
public void setMagic(Magic magic) {
this.magic = magic;
}
private boolean disable = false;
private static final int MAGIC_LONG_TYPE = 1;
private static final int MAGIC_SHORT_TYPE = 2;
private Magic magic = null;
}
挡板类Stick
此类提供了一个以画板的宽,高,和挡板的图片路径为参数的构造器,首先调用父类的构造器,初始化位置并用javax.ImageIO.的read方法读取磁盘中的图片,然后把y坐标设置到画板的底部和中部。
由于挡板在游戏中吃了道具(Magic)会改变,所以该类有个int类型的preWidth属性,代表挡板的长度,并提供了gettet和setter方法,并定义一个final int 类型的SPEED的属性,代表挡板移动的速度
public class Stick extends BallComponent {
public Stick(int panelWidth,int panelHeight,String imagePath) throws IOException{
super(imagePath,panelWidth,panelHeight);
//把挡板设置到画板中央
this.setX((int)( panelWidth - this.getImage().getWidth(null))/2);
//把挡板设置到画板底部
this.setY(panelHeight - super.getImage().getHeight(null));//设置y坐标
// 获得挡板原本的宽度,也就是挡板图片的宽度,把挡板的宽度设置为第一次调用图片的宽度
this.preWidth = super.getImage().getWidth(null);
}
// 得到挡板的宽度
public int getPreWidth() {
return preWidth;
}
//设置挡板的宽度
public void setPreWidth(int preWidth) {
this.preWidth = preWidth;
}
public static final int SPEED = 20;//设置挡板的运动速度
private int preWidth = 0;//挡板的宽度
}
到此位置,游戏需要用到的角色(对象类)都已经全部设计完成,现在该是处理游戏功能(逻辑业务)的时候了,包括游戏的开始,游戏的结束,处理小球的运动,挡板的移动,初始化砖块和道具,判断游戏中的图片元素是否有碰撞以及把图片绘制到画板等功能,在这里单独设计一个类BallService来完成这个工作。
由于该类需要维护界面中所有的组件:小球对象,挡板对像。砖块的二维数组等下面重点将将这个工作。
根据面向对象的思维方式,既然是处理小球和挡板还有砖块的功能,该类中显然有Ball Stick 和Brick属性。这些属性在该类的构造器里进行初始化
代码如下
// 提供一个挡板
private Stick stick = null;
// 提供一个弹球
private Ball ball = null;
// 定义一个砖块数组
private Brick[][] brick = null;
private int panelWidth ;//游戏界面的宽度
private int panelHeight;//游戏界面的高度
注意:确定一个砖块对象以后,在界面中由于要画出许多的砖块,在这里提供一个Brick的二维数组,来表示界面中所有的砖块,二维数组的所有元素都是每个Birck对象,在画障碍物砖块的时候就可以遍历这个二维数组来进行绘制。
游戏中主要实现的是是否发生撞击,包括弹球和砖块的撞击,弹球和挡板的撞击,挡板和道具的撞击,为了判断是否发生撞击。在这里分别提供isHitBrick,isHitStick,和stickEatMagic三个布尔类型的方法,仔细分析一下,“弹球”撞击“砖块”,当然该方法得提供一个砖块的对象作为参数,不然撞谁去呢,所以同理isHitStick也要提供一个挡板对象作为参数,stiickEatMagic要提供一个Magic作为参数;
到这里先停一下。按照以前我做的坦克大战的游戏的思路,发生撞击时是两个图片所包围的矩形进行碰撞,所以仿照坦克大战的游戏的方法,需要”获得”所有这些对象的图片所包围的矩形Rectangle, 就需要为Ball Stick 和Brick Magic 提供一Rectangle类型的getRect()方法,很明显,因为都得发生碰撞,所以可以看出每个类里都需要这个方法,因此可以把这个方法放在这些类的父类BallComponent里面去,所以在这里在BallComponent类里添加了下面代码
/**
* 获得包围图片的矩形
* @return Rectangle 矩形
*/
public Rectangle getRect() {
return new Rectangle(this.getX(),this.getY(),this.getImage().getWidth(null),this.getImage().getHeight(null));
}
当然需要注意的是,如果球和砖块撞击,那么砖块就变得无效了,所以在处理砖块和弹球转机的同时需要判断和设置一下砖块的disable属性。因此判断撞击的方法可以写成如下所示
/**
* 判断是否和砖块相撞,既然是和砖块相撞,必须提供砖块,所以参数里面传了一个Brick
* @param brick 判断要撞的砖块,这里判断是否相撞,我采取了坦克大战里面的方法
* @return 如果撞击了,返回true 否则返回false
*/
public boolean isHitBrick(Brick brick){
// 如果砖块无效,由前面知道界面中就不会画出砖块来了,所以肯定撞不住,所以返回false
if(brick.isDisable()){
return false;
}
// 判断小球是否开始运动
if(ball.isStarted()&&ball.getRect().intersects(brick.getRect())){
// 设置砖块的生命状态为false,也就是砖块死了
brick.setDisable(true);//如果撞击了就设置砖块为无效状态
return true;
}
return false;
}
/**
* 判断是否和格挡板相撞,同理得参数提供一个Stick
* @param stick Stick挡板
* @return 如果撞击了返回true否则返回false
*/
public boolean isHitStick(Stick stick){
if(ball.isStarted()&&ball.getRect().intersects(stick.getRect())){
return true;
}
return false;
}
/**
* 判断挡板是否吃了道具,同理需提供一个道具Magic
* @param magic 道具
* @return 如果吃了返回true 否则返回false
*/
public boolean stickEatMagic(Magic magic){
if(stick.getRect().intersects(magic.getRect())){
return true;
}
return false;
}
注:查一下api intersects(Retangle r)方法就是判断当前的矩形和另一个矩形是否相交,如果相交说明发生了撞击
下面来设置挡板的左右移动的位置setStickPos。这个位置的处理是用键盘上的左右键来处理的。在本类中提供了一个KeyEvent ke作为此方法的参数用来监听键盘的动作。当然当挡板运动的时候还得把小球的运动状态设置为true
Ball.setStarted(true);
同时在这里如果按下F2时需要重新开始,重新开始的处理很简单,只要重新绘制一下画板Frame即可,所以我们要为该类在提供一个BallFrame,并且在该类的构造器里初始化,然后就可以调用该类的init()方法得到开始时的画面
所以此时完成的该类构造器的操作
public BallService(int panelWidth, int panelHeight, BallFrame frame) throws IOException {
super();
this.panelWidth = panelWidth;
this.panelHeight = panelHeight;
this.frame = frame;
// 初始化挡板
stick = new Stick(panelWidth,panelHeight,"img/stick.jpg");
// 初始化弹球
ball = new Ball(panelWidth,panelHeight,stick.getImage().getHeight(null),"img/ball.gif");
// 初始化砖块数组
brick = this.creatBricks("img/brick.gif", 11, 6);
// 游戏结束图片
gameOver = new BallComponent("img/over.gif",0,0);
// 赢图片
won = new BallComponent("img/win.gif",0,0);
}
/**
* 控制挡板的移动方向
* @param ke KeyEvent
* @return void
* @throws IOException
*/
public void setStickPos(KeyEvent ke) throws IOException{
// 把弹球的运动状态设为true
ball.setStarted(true);
// 如果是按的左键,
if(ke.getKeyCode() == KeyEvent.VK_LEFT){
// 判断是否到达游戏界面的最左边,当二者之差大于零时说明还没有到达最左边,如果按下left建,仍然向左运动
if(stick.getX() - Stick.SPEED >=0)
// 挡板向左运动
stick.setX(stick.getX()-Stick.SPEED);
// 如果按的是右键
}
if(ke.getKeyCode()==KeyEvent.VK_RIGHT){
// 判断挡板是否到达游戏界面的最右边,同理如果if语句里面成立,说明还没有到达最右边,可以继续向右运动
if(stick.getX()+ Stick.SPEED<=panelWidth)
// 挡板向右运动
stick.setX(stick.getX()+Stick.SPEED);
}
// 如果按的是F2就重新开始
if(ke.getKeyCode()==KeyEvent.VK_F2){
// 重新初始化游戏界面
frame.init();
}
}
由于挡板吃了道具以后,它的长度会随着改变,所以需要提供一个setStickWidth的方法。来设置其宽度。
}
/**
* 设计挡板的宽度,注意多态性的应用
* @param magic道具
* @return void
*/
public void setStickWidth(Magic magic){
如果挡板吃了道具
if(this.stickEatMagic(magic)){
改变挡板的长度
magic.magicDo(stick);
}
}
需要注意的是,挡板的长度是增加了,怎么在游戏的过程之中,小球可以从挡板之中穿过呢?而没有发生撞击,看了getRect()类的处理方式我知道了原因的所在。看一下getRect()的代码
public Rectangle getRect() {
return new Rectangle(this.getX(),this.getY(),this.getImage().getWidth(null),this.getImage().getHeight(null));
}
黑体部分是包围图片的矩形的宽度,由于getImage()得到的只是stick.gif的宽度,所以在preWidth已经改变的情况下,得到的矩形的宽度应该随之改变才对,但是其恰恰没有改变,所以我在Stick里重写了这个方法,把黑体部分改成this.getPreWidth()或者preWidth得到完美解决。
处理完了挡板,现在处理砖块,如下代码,注在画砖块的时候并不是每一个二维数组的元素都有砖块,。另外道具里也随机包含有道具,这一点要注意
* 创建一个数组类型为Brick的数组,用来存储砖块图片
* @param imagePath 图片路径
*
* @param xSize二维数组的横向宽度
* @param ySize二维数组的纵向宽度
* @return Brick[][] 砖块的二维数组
* @throws IOException
*/
public Brick[][] creatBricks(String imagePath, int xSize,int ySize) throws IOException{
Brick[][] bricks = new Brick[xSize][ySize];
int x = 0;//砖块的横坐标
int y = 0;//砖块的纵坐标
int randomType = 0;//随机产生道具的类型
int imageSize = 28;
boolean isDisable = false;
for(int i=0;i<xSize;i++){
for(int j = 0;j<ySize;j++){
randomType = (int)(Math.random()*3);//随机生成道具类型
x = i*imageSize;//第i个砖块的横坐标位置
y = j * imageSize;//第i个砖块的纵坐标位置
isDisable = Math.random()>0.8? true:false;//随机为砖块产生道具说
// 如果界面数组中某一位置没有砖块,即isDisable为true没有砖块。则不提供道具,具体见运行时的结果
if(isDisable){
randomType = 0;//不提供道具
}
Brick brick = new Brick(imagePath,randomType,x,y);
// 随机设置砖块的是否有生命,如果没有生命则不在游戏界面上面显示,或者画出来,如果不设置会出现每一个二维数组中都有brick元素
brick.setDisable(isDisable);
bricks[i][j] = brick;
}
}
return bricks;
}
当砖块被消灭的时候,里面如果有道具的话,道具就会下降,所以要设计一下怎么下降,只需设计一下它的纵坐标即可,在这里需要遍历砖块的二维数组,并且判断每一个元素位置的magic道具是否为空。间下面代码
/**
* 设置道具位置,因为道具是向下运动的,所以只需要设置道具的纵坐标即可
*
*/
public void setMagicPos(){
for(int i=0;i<brick.length;i++){
for(int j=0;j<brick[i].length;j++){
// 获得道具
Magic magic = brick[i][j].getMagic();
// 如果道具不为空
if(magic != null){
if(brick[i][j].isDisable()&&magic.getY()<panelHeight){
magic.setY(magic.getY()+ magic.getSpeed());
// //如果挡板吃了砖块
this.setStickWidth(magic);
}
}
}
}
}
现在来判断是否输赢,如果在小球死亡的情况下界面中只有有一个砖块,那么就没有赢,直到砖块都没有的情况下才能赢,这就需要遍历砖块数组判断每一个砖块的有效性
public boolean isWon(){
for(int i=0;i<brick.length;i++){
for(int j=0;j<brick[i].length;j++){
if(!brick[i][j].isDisable())//如果有一个砖块还活着,说明你没有胜利
return false;
}
}
return true;
}
下面设置小球的运动,由于小球的运动是自动了,是不受玩家控制的,在整个游戏中玩家能控制的只有挡板的作用移动,在小球运动的时候需要判断是否碰到边界,包括左边界,右边界,上边界,下边界。这都要进行相应的处理。
public void setBallPos(){
int absSpeedX = Math.abs(ball.getSpeedX());
int absSpeedY = Math.abs(ball.getSpeedY());
// 如果小球开始运动并且游戏还没有结束
if(ball.isStarted()){
// 如果碰到左边界,即是球的横坐标小于0
if(ball.getX()-absSpeedX<0){
// 重新设定弹球的位置,注意画图分析一下怎么设置,很有技巧
ball.setX(ball.getImage().getWidth(null));
// 让弹球反方向弹回
ball.setSpeedX(-ball.getSpeedX());
}
// 如果碰到右边界,即是球的的横坐标大于panelWidth-ball.getImage().getWidth(null),画图分析之
if(ball.getX() + absSpeedX>panelWidth-ball.getImage().getWidth(null)){
// 重新设置弹球的位置,注意画图分析一下怎么设置,很有技巧
ball.setX(panelWidth - ball.getImage().getWidth(null));
// 让弹球放方向弹回
ball.setSpeedX(-ball.getSpeedX());
}
// 如果碰到上边界
if(ball.getY() - absSpeedY<0){
// 重新设置弹球的Y位置
ball.setY(ball.getImage().getWidth(null));
// 让弹球放方向弹回
ball.setSpeedY(-ball.getSpeedY());
}
// 如果碰到下边界
if(ball.getY() + absSpeedY>panelHeight - stick.getImage().getHeight(null)){
if(this.isHitStick(stick)){//如果和挡板有碰撞;只需把纵坐标速度改成相反即可,横坐标速度不变,根据速度的合成,即可得到合速度
ball.setSpeedY(-ball.getSpeedY());
}
}
// 与砖块碰撞后的运动
for (int i = brick.length - 1; i > -1; i--) {
for (int j = brick[i].length - 1; j > -1; j--) {
// 如果小球与砖块有碰撞
if (isHitBrick(brick[i][j])) {
if (ball.getSpeedY() > 0) {
//反方向弹回
ball.setSpeedY(-ball.getSpeedY());
}
}
}
}
// 如果小球碰到底部结束游戏
if (ball.getY() > panelHeight) {
ball.setStop(true);
}
// 设置x坐标,让小球子自动运动.开始时是让小球横坐标向左运动的。注意
ball.setX(ball.getX() - (int) (Math.random() * 2)
-ball.getSpeedX());
// 设置y坐标
ball.setY(ball.getY() - (int) (Math.random() * 2)
- ball.getSpeedY());
}
}
下面定义了一个让游戏开始的方法,run
public void run(){
// 弹球坐标改变
setBallPos();
// 道具坐标改改变
setMagicPos();
}
下面提供一个方法,把游戏的组件化的界面上去
public void draw(Graphics g){
if(this.isWon()){
// 绘制赢的图片
g.drawImage(won.getImage(), won.getX(), won.getY(), panelWidth,
panelHeight - 10, null);
} else if (ball.isStop()) {
// 绘制游戏结束图像
g.drawImage(gameOver.getImage(), gameOver.getX(), gameOver.getY(),
panelWidth, panelHeight - 10, null);
}else{
// 清除所有图像
g.clearRect(0, 0, panelWidth,panelHeight);
// 绘制档板图像
g.drawImage(stick.getImage(), stick.getX(), stick.getY(), stick
.getPreWidth(), stick.getImage().getHeight(null), null);
// 绘制弹球图像
g.drawImage(ball.getImage(), ball.getX(), ball.getY(), null);
// 迭代绘制砖块图像
for (int i = 0; i < brick.length; i++) {
for (int j = 0; j < brick[i].length; j++) {
BallComponent magic = brick[i][j].getMagic();
// 如果这个砖块图像对像是有效的
if (!brick[i][j].isDisable()) {
// 里面的数字1为砖块图像间的间隙
g.drawImage(brick[i][j].getImage(), brick[i][j]
.getX(), brick[i][j].getY(), brick[i][j]
.getImage().getWidth(null) - 1, brick[i][j]
.getImage().getHeight(null) - 1, null);
} else if (magic != null && magic.getY() < panelHeight) {
g.drawImage(magic.getImage(), magic.getX(), magic
.getY(), null);
}
}
}
}
}
游戏面板BallFrame
我们在BallService里面已经处理过了小球,挡板,砖块,和挡板,面板的作用就是所以把处理的结果显示出来让我们看即可,所以,只需为该类提供了一个BallService即可,当然还需要对面板添加键盘监听器,用来监听挡板的运动方向。还有一个时间控制器
ublic class BallFrame extends JFrame {
/**
*
*/
private static final long serialVersionUID = 1L;
public class BallPanel extends JPanel{
/**
*
*/
private static final long serialVersionUID = 1L;
@Override
public void paint(Graphics g) {
service.draw(g);//在面板上画图
}
}
/**
* 得到有一个BallPanel
* @return BallPanel
*/
public BallPanel getBallPanel(){
if(ballPanel == null){
ballPanel = new BallPanel();
// 设置ballPanel的大小,使得其大小和frame一样
ballPanel.setPreferredSize(new Dimension(WITDH,HEIGHT));
}
return ballPanel;
}
/**
* 初始化界面
* @throws IOException
*/
public void init() throws IOException{
this.setTitle("ballGame");
this.setResizable(false);
this.setBackground(Color.black);
ballPanel = this.getBallPanel();
// 初始化service
service = new BallService(WITDH,HEIGHT,this);
/**
* 添加一个事件监听器
*/
ActionListener task = new ActionListener(){
@Override
public void actionPerformed(ActionEvent e) {
// 游戏开始
service.run();
// 刷新游戏面板
ballPanel.repaint();
}
};
// 提供时间控制器
if(timer!=null){
timer.restart();
}else{
// 每100ms控制一次事件监听器
timer = new Timer(100,task);
timer.start();
}
this.add(ballPanel);
// 添加键盘监听器,用来控制挡板的移动
KeyListener[] klarr = this.getKeyListeners();
if(klarr.length==0){
// 定义一个键盘监听适配器
KeyListener keyAdapter = new KeyAdapter(){
@Override
public void keyPressed(KeyEvent ke) {
try {
service.setStickPos(ke);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
};
this.addKeyListener(keyAdapter);
}
this.setVisible(true);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.pack();
}
/**
* 默认构造器
*/
public BallFrame() throws IOException {
super();
// 初始化
init();
}
private BallPanel ballPanel = null;
private final int WITDH = 307;
private final int HEIGHT = 400;
private BallService service = null;
private Timer timer = null;
}
注意一下这个paint方法,查询api可知:由 Swing 调用,以绘制组件。应用程序不应直接调用 paint
,而是应该使用 repaint
方法来安排重绘组件。
为了使弹球达到运动的效果,用来Timer这个时间控制器,设置每隔0.1秒监听一下任务,下面是一些关于Timer的api说明
设置计时器的过程包括创建一个 Timer 对象,在该对象上注册一个或多个动作侦听器,以及使用 start 方法启动该计时器。例如,以下代码创建并启动一个每秒(该时间由 Timer 构造方法的第一个参数指定)触发一次动作事件的计时器。Timer 构造方法的第二个参数指定接收计时器动作事件的侦听器。
int delay = 1000; //milliseconds
ActionListener taskPerformer = new ActionListener() {
public void actionPerformed(ActionEvent evt) {
//...Perform a task...
}
};
new Timer(delay, taskPerformer).start();
构造 Timer 时要指定一个延迟参数和一个 ActionListener。延迟参数用于设置初始延迟和事件触发之间的延迟(以毫秒为单位)。启动了计时器后,它将在向已注册侦听器触发第一个 ActionEvent 之前等待初始延迟。第一个事件之后,每次超过事件间延迟时它都继续触发事件,直到被停止。
构造之后,可以单独更改初始延迟和事件间延迟,并且可以添加其他 ActionListener。
如果希望计时器只在第一次时触发然后停止,可以对计时器调用 setRepeats(false)。
其实,也可以不用事件监听器,直接用多线程的知识的知识,直接让现成睡眠100ms然后继续刷新画板,然后代码就可以改成如下所示情况
为BallFrame类里面添加一个线程内部类
private class PaintThread implements Runnable {
public void run() {
while(true) {
service.run();
ballPanel.repaint();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
这样在init里面添加一个线程对象然后启动线程即可
public void init() throws IOException{
this.setTitle("ballGame");
this.setResizable(false);
this.setBackground(Color.black);
ballPanel = this.getBallPanel();
// 初始化service
service = new BallService(WITDH,HEIGHT,this);
this.add(ballPanel);
// 添加键盘监听器,用来控制挡板的移动
// 定义一个键盘监听适配器
KeyListener keyAdapter = new KeyAdapter(){
@Override
public void keyPressed(KeyEvent ke) {
try {
service.setStickPos(ke);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
};
this.addKeyListener(keyAdapter);
new Thread(new PaintThread()).start();
this.setVisible(true);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.pack();
}
至于图片文件,要是想要的话,请联系feixiangdebaiyun@yeah.net