一、创建一个画框
让TankFrame类继承Frame类,设置标题位置大小,
最后在main里面实例化,设置setVisible为true就可以了;
二、创建一个黑方块
重写paint方法,使用g,也就是awt自动初始化的一个变量,相当于一只画笔,调用fillReact,设置位置和长宽;
三、创建Tank类
为了体现封装性,需要将画笔封装入Tank类中;
四、让黑框框动起来
在main函数中,改变黑框框位置的同时,通过while死循环和repaint()不断地刷新画框,黑框框就动起来了;
五、按下键盘的方向键使黑框框移动
在TankFrame中加入一个自己写的监听器TankKeyListener(监听器继承了KeyAdapter而没有继承KeyListener,是因为KeyListener是一个接口,如果说要让我写的监听器继承它的话,需要重写它的所有的抽象方法,而抽象类KeyAdapter继承了它,这时让我的监听器继承KeyAdapter的话,就可以不重写它的所有方法,要什么功能就重写什么功能),
而键盘输入由于是坦克的事,所以需要放在Tank类里;
基本实现:通过重写KeyPressed函数,将读取到的KeyEvent通过TankFrame传入Tank中,再通过getKeyCode将KeyEvent读取为一个整型值;将整型值传入switch进行虚拟值(VK_DIR)判断,通过横坐标x和纵坐标y的加减进行黑框框的移动;
暴露的问题:
1.移动不够平滑
2.同时按下两个键,再抬起一个键时,方块停止运动
原因:
1.每次按下一个方向键的时候,黑方框都会先走一格,再停一阵子,再连续移动,而这种现象在快速随意乱按方向键的时候非常明显;
改进方法:
1.将移动功能相关代码另起一个方法,并放入paint方法里,可以在按下键位的同时刷新画框(有疑惑!!!)
2.编写键位抬起方法,使用四个boolean变量记录按下的键位,同时改写键位按下方法,再编写一个判断方向的方法即可
3.由于STOP不是方向,是一个状态,所以不能把它DIR枚举类里,这里通过一个boolean变量代替STOP
至此黑框移动的功能大致写完,现在来总结一下移动黑框框的代码:
在paint函数中用TankFrame传递过来的画笔创建一个黑框框;
除开paint方法外,有四个负责控制方向的方法:
keyPressed()、keyReleased()、setMainDir()、move()
在TankFrame里有一个继承了KeyAdapter(它继承了KeyListener)的监听器(内部类),通过重写keyPressed()和keyReleased()来获取监听值KeyValue,通过myTank.keyPressed(e)和myTank.keyReleased(e)将监听值传入Tank类中,将监听值用getKeyValue()获取为整型变量,对此变量进行switch操作,如果这个值满足了一个虚拟值,则将对应的boolean类型标志位置为true;
(对应的KeyReleased方法,前面操作一样,不过是把对应的boolean类型标志位置为false)
在这个两个方法里调用setMainDir方法;
int key = e.getKeyCode();
switch (key){
case KeyEvent.VK_UP:
bU = true;
break;
................
setMainDir();
}
setMainDir方法的功能:通过上述两个方法设定的boolean类型标志位,对表示方向和运动状态的枚举类、布尔类进行赋值;
首先要判断黑框是否移动,如果四个标志位都为false,就代表没有按键,黑框不动;
那如果有一个是true,则将运动状态设置为true同时将代表方位的枚举变量设置为相对应的枚举值
if (!bD && !bL && !bR && !bU) {
moving = false;
}else if (bD && !bL && !bR && !bU) {
moving = true;
dir = Dir.D;
}
................
在paint方法中调用了move方法,move方法的功能:通过前述的setMainDir方法设置的布尔值对黑框所在的位置进行x轴和y轴方向上的加减,配合repaint方法调用的paint方法进行画框的刷新,形成黑框移动的效果;
首先要先判断是否移动,再进行对dir的switch挑选,如果未移动则直接结束这次的move方法调用
if (moving == false)
return;
switch (dir){
case U:
y -= SPEED;
break;
至此移动功能总结完毕,下面进入图片插入模块
六、图片插入
首先要从硬盘里面将图片读取到内存当中,
ImageIO.read负责读取图片,Tank.class是Tank类的一个对象,Tank.class.getClassLoader()获取到了将Tank.class加载到内存里面的ClassLoader,再通过此ClassLoader将图片(资源)通过流的方式加载到内存当中;
其次是在画框中画出这个图片,
往画笔g中传入图片、坐标和监听器(null)
try {
BufferedImage tankL = ImageIO.read(Tank.class.getClassLoader().getResourceAsStream("images\\GoodTank1.png"));
g.drawImage(tankL, x, y, null);
} catch (IOException e) {
e.printStackTrace();
}
而这样就会暴露出一个问题:这段代码是写在paint方法里的,在main调用repaint方法对paint进行刷新时,需要不断的加载图片,一秒钟60次,效率非常的低下,所以我把需要加载的资源封装在另外一个叫ResourceMgr的类里面,通过一个静态代码块对资源进行初始化;
由于坦克的基本形态有前后左右四种,可以为每种形态配一张图片,也可以将一张图片旋转不同的角度,本人采用的是第二种,所以要写一个翻转方法;
翻转方法
方转方法呢,本人才疏学浅不做解释~
(不过可以复习javaSE基础,这个类里调用的翻转方法是写在ImageUtil里面的静态方法,可直接通过 类名.方法名 进行调用)
通过翻转方法生成了基本的四种坦克方向图片;
七、双缓冲问题
将它加到TankFrame里面
//解决双缓冲问题
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.WHITE);
gOffScreen.fillRect(0, 0, GAME_WIDTH, GAME_HEIGHT);
gOffScreen.setColor(c);
paint(gOffScreen);
g.drawImage(offScreenImage, 0, 0, null);
}
八、子弹
创建子弹类,子弹应该有坐标,方向,速度,分组(我方发射还是敌方发射);
调用画笔对子弹进行方向选择以及移动;
基本的子弹就创建好了;
由于是坦克射出的子弹,所以在坦克中设置fire方法;此方法里new出子弹,将坦克的坐标、方向和好坏作为子弹的坐标、方向和好坏;
由于需要在TankFrame里面画出子弹,所以new出来的这个子弹需要放在TankFrame里面,以下提供两种方法:
1.通过TankFrame的引用
解释:在TankFrame里增添一个方法add,负责接收子弹,创建坦克的时候通过this,将TankFrame的对象传递到Tank里,在Tank类里的fire方法中通过这个对象调用add方法来接收这个子弹;
(已上传此版本:通过TankFrame引用显示子弹)
2.通过单例模式
首先,在TankFrame里,构造方法之前先new出一个static final的TankFrame对象,然后将TankFrame设置为private,即谁也不能new我,最后让new出来的那个不变对象去干TankFrame该干的事;
(已上传此版本:通过单例模式显示子弹)
此时发射子弹出现了几个问题,由于在TankFrame里面只new出了一个子弹,所以屏幕上只会同时存在一个子弹,解决方法是new一个子弹就把它放在集合里面,在画图的时候,对这个集合进行增强for循环遍历时再画出每一个子弹即可;
另外一个问题就是,子弹需要边界检测,否则子弹越来越多越来越多;
九、子弹边界检测
private void boundsCheck() {
if (x < 0 || y < 30 || x > TankFrame.GAME_WIDTH || y > TankFrame.GAME_HEIGHT) {
live = false;
}
}
十、击毁敌军坦克!!!
当子弹和敌军坦克碰撞的时候敌军坦克就嗝儿屁了~
其实就是子弹模型和坦克模型相交的时候就删除敌军坦克
(因为以前写的没保存,就直接给出完整版的)
首先取出子弹和坦克的方块儿,他们两个相交的时候子弹和敌坦一起带~
前面的两个判断语句是这样的,如果坦克带了,那可就千万别执行这个方法了,不然它带了后还像一个黑洞一样吞噬着子弹;
那如果说是坦克自己打出来的子弹,也就是坏坦克就打出坏子弹,肯定不能伤到自己啊,所以如果说自己打到自己的话那就退出该方法;
那在画坦克和子弹的时候就需要进行判断是否存活
(已上传子弹完整版)(55555~55555前面写的没保存55555555555~)
public void collidedWithTank(Enemy tank){
if (!tank.isLive()){
return;
}
if (this.group == tank.getGroup()){
return;
}
Rectangle rectBullet = new Rectangle(x, y, ResourceMgr.bulletD.getWidth(), ResourceMgr.bulletD.getHeight());
Rectangle rectTank = new Rectangle(tank.getX(), tank.getY(),
ResourceMgr.goodTankD.getWidth(), ResourceMgr.goodTankD.getHeight());
if (rectBullet.intersects(rectTank)){
this.die();
tank.die();
}
}
十一、起飞吧!敌坦!
(其实就是让敌坦自己动,自己动嗷自己动 ~)
十二、坦克就要在擂台上战斗!(坦克边界检测)
代码解释:random.nextInt(100)生成的是[0 , 100)内的一个整数,如果它大于90的话,就让方向改变
改变语句解释:Dir.values()整个代表一个数组,随机返回一个数组长度以内的一个值对应的方向给dir,让它去move,从而实现了随机方向的转换;
private void randomDirection() {
if (random.nextInt(100) > 90)
this.dir = Dir.values()[random.nextInt(Dir.values().length)];
}
十三、我坦,你不能再为所欲为了!(我方坦克的碰撞问题)
注意哦,我这里可用了重载喔,我可牛逼了哦~
(已上传:基本完整的游戏功能)
十四、一点儿也不艺术的爆炸
在ResourseMgr里面加入爆炸资源,用一个数组来存放;
撰写爆炸类,需要有它的爆炸地点x和y,还需要有检测是否爆炸结束的标志位,还有它自己的paint函数;
(因为爆炸爆得bug特别多,所以我直接给出)爆炸最终方案:
1.当坦克爆炸的时候,把当前坦克的坐标传入new出来的爆炸,通过单例模式接收到TankFrame里面的爆炸集合里;
2.在paint方法里不断的对这个爆炸集合进行重画,当某个爆炸已经炸完了,就把它移除掉,没炸完就给我继续画;
3.爆炸类里,每调用一次paint,给我画一张图片,再次调用时给我画下一张图片,全部画完之后标志位置true(它真的over了),那如果over了呢,就不画了,在TankFrame里面,当知道这次爆炸真的over了,那就把它移出掉;