画对象只需三个步骤:
1.对象的图片 2.绘制到窗口的x坐标 3.绘制到窗口的y坐标
@Override
public void paint(Graphics g) { //Jpanle提供的绘制图片的方法
//1.对象图片 2.需要绘制的 x 和y坐标
ImageIcon img = Images.battleship;//获取战舰图片
//1.填null 2. g 3.x坐标 4.y坐标
img.paintIcon(null,g,270,124);
}
贴切到项目中画对象
需要5个步骤 第一个步骤定义获取对象图片的抽象方法: 每个对象都需要有自己的图片,意味着每个类都需要写一个获取图片的方法.如果都要写一个获取图片的方法,那么代码冗余,那么就需要将当前getImage方法提取到SeaObject父类中.因为子类的具体返回的图片不一样,所以父类需要做一个抽象方法,约束子类必须返回自己的图片.
public abstract ImageIcon getImage();
第二个步骤
获取图片的具体逻辑,每个对象都有状态, 例如 活着的状态和死亡的状态,我们认为对象都有状态,且状态的固定的.所以需要在SeaObject父类去定义两个常量的死亡和活着状态 , 还应该有一个表示当前状态的变量.
public static final int LIVE = 0;//活着的状态
public static final int DEAD = 1;//死亡的状态
public int currentState = LIVE; //当前状态默认为活着的
在SeaObject类中添加两个方法,一个是用来判断当前对象是否是活着的方法,一个是用来判断当前对象是否是死亡的方法
/** 判断当前调用这个方法的对象状态 是否是死亡状态 */
public boolean isDead(){
return currentState == DEAD;
}
/** 判断当前调用这个方法的对象状态 是否是活着状态 */
public boolean isLive(){
return currentState ==LIVE;
}
第三个步骤
子类重写实现父类的getImage方法
战舰类
@Override //战舰比较特殊,并不是一打就死亡,所以可以直接返回图片.
public ImageIcon getImage() {
return Images.battleship; //返回战舰图片
}
--------------------
深水炸弹类
@Override
public ImageIcon getImage() {
if(this.isLive()){//如果当前
对象是活着的状态
return Images.bomb;//返回深水炸弹图片
}
return null;//如果能走到这里 说明当前对象死亡 则不再返回图片
}
--------------------
水雷类
@Override
public ImageIcon getImage() {
if(this.isLive()){ //如果是活着的状态
return Images.mine;//返回图片
}
return null;//返回null 代表不是活着的意思
}
--------------------
水雷潜艇类
@Override
public ImageIcon getImage() {
if(this.isLive()){ //如果是活着的状态
return Images.minesubm;//返回图片
}
return null;//返回null 代表不是活着的意思
}
--------------------
侦察潜艇类
@Override
public ImageIcon getImage() {
if(this.isLive()){ //如果是活着的状态
return Images.obsersubm;//返回图片
}
return null;//返回null 代表不是活着的意思
}
---------------------
鱼雷类
@Override
public ImageIcon getImage() {
if(this.isLive()){ //如果是活着的状态
return Images.torpedo;//返回图片
}
return null;//返回null 代表不是活着的意思
}
----------------------
鱼雷潜艇类
@Override
public ImageIcon getImage() {
if(this.isLive()){ //如果是活着的状态
return Images.torpesubm;//返回图片
}
return null;//返回null 代表不是活着的意思
}
第四个步骤
获取对象图片功能有了,因为每个对象都需要绘制,那么可以设计一个绘制的方法,设计SeaObject中,因为每个对象绘制的行为具体逻辑是一样的,所以设计一个普通方法.
/**
* 因为每个子类都需要绘制图片 那么就将逻辑写到父类中,子类可以复用
* 需要外部传递一个画笔 所以做成一个形式参数
* */
public void paintImage(Graphics g){
if(this.getImage()!=null){//如果当前对象图片不为空
ImageIcon icon = this.getImage();//获取当前对象图片
//1.填null 2. g 3.x坐标 4.y坐标
icon.paintIcon(null,g,this.x+100,this.y);
}
}
第五个步骤
在GameWorld的paint方法中测试
@Override
public void paint(Graphics g) { //Jpanle提供的绘制图片的方法
ship.paintImage(g);
for (int i = 0; i <submarines.length ; i++) {
submarines[i].paintImage(g);
}
}
问题:运行游戏以后,潜艇能够生成并绘制出来的,但是并不能够移动,而移动是动态自动移动的。 对象的动态移动要先学习 成员内部类 和 匿名内部类。
成员内部类(应用率不高)
类中套个类!外层的类称之为外部类 ,内层称之为内部类 1.内部类对外不具备可见性,除了外部类的其它类不可直接访问! 2.内部类对象 可以在外部类中创建。 3.内部类可以共享外部类的属性和行为(包括私有)。 4.内部类访问外部类的成员会有一个隐式写法:外部类名.this.xx;
package oo.day05;
/**
* 用于测试内部类和外部类的语法规则:
* */
public class TestDemo {
public static void main(String[] args) {
// Baby a = new Baby(); 编译错误: 内部类 无法直接被外部其它直接使用
Mama m = new Mama();
}
}
class Mama{ //外部类------ 妈妈类
int a;
private int b;
void action(){
Baby b = new Baby();//外部类可以创建内部类对象
}
class Baby{//内部类 ------ 宝宝类
int c;
void test(){
this.c = 10;//使用本类的内容会有隐式的this
Mama.this.a = 10;//内部类可以共享外部类的属性和行为 隐式写法:外部类名.this.a
b = 20;//可以访问外部类私有的内容
}
}
}
匿名内部类(应用率高)
没有名字的内部类叫做匿名内部类。 适用性:如果一个子类,仅仅只是想重写父类的某个功能(方法),其它类根本不需要用到这个子类,那么可以直接用匿名内部类 来便捷的快速实现。 1.匿名内部类只会存在于子类要重写父类的(抽象或普通)方法时 才会使用!! 2.匿名内部类无法修改外部类的值! 因为规定在匿名内部类使用外部类的属性时,会自动修饰为final。
package oo.day05;
/**
* 匿名内部类的测试:
* */
public class NstInnerClassDemo { //外部类
public static void main(String[] args) {
//常规的子类实现重写的方法
//1.创建Boo对象
//2.把Boo对象地址 赋值 给b
Boo b = new Boo();
b.show();
int d = 10;//局部变量
//使用匿名内部类来创建子类并重写父类的功能实现:
//1.创建了Aoo的子类 只不过没有名字
//2.将当前这个子类的地址 赋值给 a
//3.花括号就是子类的类体
Aoo a = new Aoo(){ //匿名的Aoo子类 是 NstInnerClassDemo 的内部类
@Override
public void show() {
// d= 10; 编译错误 内部类 无法修改外部类的内容 默认在内部类中会将外部类内容修饰为final
System.out.println(d);//内部类可以访问外部类的成员
System.out.println('匿名Aoo的子类重写父类的show方法');
}
};
a.show();
}
}
class Aoo{//父类
public void show(){
System.out.println('父类Aoo中的show方法....');
}
}
class Boo extends Aoo{ //常规的子类实现重写的方法
@Override
public void show() {
System.out.println('Boo子类中重写父类的show方法');
}
}
面试题:
问:内部类是否有独立的.class 字节码文件吗? 有!!
成员内部类
外部类的名字 字节码文件 内部类 成员内部类字节码文件 Mama.java Mama.class Baby Mama$Baby.class
匿名内部类
外部类的名字 字节码文件 内部类 成员内部类字节码文件 NstInnerClassDemo NstInnerClassDemo.class 1 NstInnerClassDemo$1.class
动态入场
潜艇动态入场 ----------------------------- 自动发生的 水雷和鱼雷动态入场---------------------自动发生的
如何自动发生:
1.定时器 2.线程(后面会讲)
1.给闹钟制定一个任务 2.开始延时多久执行这个任务的时间 3.闹钟任务执行一次后据下次执行的间隔时间
定时器:
1.具体什么任务 2.延时多久开始执行 3.执行后下一次的间隔时间 Java提供的两个类 任务类 和 定时器类 TimerTask 任务类 定时类 Timer 在GameWorld类上方导入这两个功能。
import java.util.Timer;//定时器类
import java.util.TimerTask;//任务模板类
在action方法中写上:
Timer t = new Timer();//创建定时器对象
TimerTask task = new TimerTask() { //创建 TimerTask匿名子类 实现重写父类的run方法
@Override
public void run() { //自定义的任务逻辑
System.out.println('叮叮叮~~~');
}
};
//1.要执行的具体任务对象 2. 延时时间(毫秒) 3.间隔时间(毫秒)
t.schedule(task, 5000,1000);
潜艇动态生成对象后需要 将对象存给潜艇数组,所以需要将数组先扩容。
数组扩容:
package oo.day05;
import java.util.Arrays;
/**
* 数组的拷贝的方式:
* 1. Arrays.copyof ---------------更多是基于源数组 扩容 或者 缩容 的使用方式。
*
* 2. System.arraycopy --------------更多是基于存在 两个数组的拷贝方式。(把A 数组元素拷贝 给B数组)
*/
public class ArrayCopyDemo {
public static void main(String[] args) {
// int[] array = {};
// System.out.println('扩容之前数组的空间' + array.length);//0
// int a = 10;
//
// //1.要操作的数组 2.扩容 (基于要扩容的数组长度上加1)
// array = Arrays.copyOf(array, array.length + 1);
// System.out.println('扩容之后数组的空间' + array.length);//1
// //代表数组空间下最后一个索引 的写法 array.length-1
// array[array.length-1] = a;
// for (int i = 0; i < array.length; i++) {
// System.out.println(array[i]);
// }
//2.System.arraycopy
int[] arrA = {10,50,5,6,1};
int[] arrB = {1 ,1 ,1,1,1};
/*
* 1.要拷贝的数组
* 2.从要拷贝的数组的哪个索引开始拷贝
* 3.要拷贝到的目标数组
* 4.从目标数组的哪个索引开始装
* 5.拷贝过来的数量是多少
* */
System.arraycopy(arrA,0,arrB,0,arrA.length);
for (int i = 0; i < arrB.length; i++) {
System.out.println(arrB[i]);
}
}
}
在GameWorld类中再去写一个方法,代表潜艇入场的方法,方法名submarineEnterAction
/**
* 生成潜艇对象的方法 返回值可以写具体的潜艇类型吗?不可以,如果写了具体的子潜艇类型
* 该方法还通用吗?不能!
* 返回值应该写 父类型
*/
public SeaObject nextSubmarine() {
int type = (int) (Math.random() * 20);//随机数 0 ~ 20
if (type < 10) { //生成的随机数在 0~9区间
return new ObserverSubmarine();//返回侦查潜艇
} else if (type < 15) {//生成的随机数在 10~14区间
return new TorpedoSubmarine();//返回鱼雷潜艇
} else { //生成的随机数在 15~19区间
return new MineSubmarine();//返回水雷潜艇
}
}
private int subEnterIndex = 0;//用来控制潜艇产生的速度
/**
* 潜艇入场的具体实现方法 放到run中调用
*/
public void submarineEnterAction() { //10毫秒调用一次 (0.01s)
subEnterIndex++;//每10毫秒自增一次
if (subEnterIndex % 40 == 0) {//每400毫秒走一次 (0.4s)
//潜艇生成s
SeaObject obj = nextSubmarine();//调用创建潜艇对象的方法 接收返回的潜艇对象
submarines = Arrays.copyOf(submarines, submarines.length + 1);//扩容
//将obj 装到数组中的最后一个元素
submarines[submarines.length - 1] = obj;
}
}
在run中调用:
//做创建对象的事情
private void action() {
Timer timers = new Timer();//创建具体的定时器
//schedule方法 参数 1.具体任务 2.延时多久开始执行 3.开始执行后的间隔时间
TimerTask task = new TimerTask() {//创建匿名内部类
@Override
public void run() {//在run中去写 你要执行的任务
submarineEnterAction();//调用潜艇入场的方法
repaint();//每隔0.01s做一次绘制刷新
}
};
//延时5秒后执行任务,执行任务以后下次执行任务时看间隔时间
// 任务 延时5秒 //间隔执行任务时间
timers.schedule(task, 5000, 10);//间隔0.01s
}
水雷 和鱼雷分别由水雷潜艇和鱼雷潜艇发射,因为发射的逻辑基本一样。所以在父类中写一个shootThunder方法来进行复用。
/** 生成雷的方法
* 因为这个方法要被鱼雷潜艇或水雷潜艇对象所调用。
* 所以返回值 不能写具体的子类型,否则方法不通用,可以填父类来代表。
* */
public SeaObject shootThunder(){
int x = this.x + this.width;//雷的x坐标
int y = this.y - 5;//雷的y坐标
// instanceof 是java提供的判断类型的语法
//判断当前调用方法的对象 是不是 鱼雷潜艇
if(this instanceof TorpedoSubmarine){
return new Torpedo(x,y);//返回鱼雷对象
}else if( this instanceof MineSubmarine){//判断当前调用方法的对象 是不是 水雷潜艇
return new Mine(x,y);//返回水雷对象
}
return null;//如果方法能执行到这一行 说明是侦察潜艇对象使用了方法,则返回null
}
水雷和鱼雷的入场也是自动发生,那么也需要GameWorld类中去做一个雷入场的方法,thunderEnterAction,也要在run中去调用。
private int thunderEnterIndex = 0;//用来控制雷的生成速度
/**
* 当前方法是雷入场的方法 ,需要放到run中去调用。
*/
public void thunderEnterAction() {//每10毫秒调用一次(0.01s)
thunderEnterIndex++;//每次方法被调用自增1
if (thunderEnterIndex % 100 == 0) {//每1000毫秒执行一次(1s)
// 1.循环遍历当前潜艇数组,并依次调用数组潜艇对象的shootThunder方法 会返回一个对象(雷,null) 接收。
for (int i = 0; i < submarines.length; i++) {
SeaObject obj = submarines[i].shootThunder();
if (obj != null) { //2.判断接收的对象 是否不等于null
//3.如果条件成立,给雷数组扩容
thunders = Arrays.copyOf(thunders, thunders.length + 1);
//4.将对象装到雷数组的最后一个位置
thunders[thunders.length-1] = obj;
}
}
}
}
所有的潜艇、水雷和鱼雷 的移动行为。
所有潜艇都是向右运动,所以所有潜艇类的step方法内写上 x+=speed; 水雷和鱼雷都是向上运动,所以水雷和鱼雷类的step方法内写上 y-=speed; 深水炸弹向下运动,所以深水炸弹类的step方法内写上 y+=speed; 当前水雷、鱼雷,深水炸弹,所有潜艇 运动都自动发生的,所以在GameWorld写一个方法,stepAction方法主要就是用来遍历潜艇数组和雷数组,深水炸弹数组 并依次调用他们的移动的方法。然后将stepAction 放到run中进行调用。
/**
* 所有潜艇、水雷、鱼雷 深水炸弹 调用移动方法的 方法。
* */
public void stepAction(){
for (int i = 0; i < submarines.length; i++) {
submarines[i].step();//依次调用潜艇数组里每个对象的移动方法
}
for (int i = 0; i < thunders.length; i++) {
thunders[i].step();//依次调用雷数组里每个对象的移动方法
}
for (int i = 0; i < bombs.length; i++) {
bombs[i].step();//依次调用深水炸弹数组里每个对象的移动方法
}
}
深水炸弹入场是当按下空格键来进行入场的。 因为发射深水炸弹的行为是战舰的,所以应该在战舰类里面写一个shootBomb的方法。
/**
*发射深水炸弹的方法 返回值就是深水炸弹对象
* 由战舰对象进行调用。
* */
public Bomb shootBomb(){
return new Bomb(this.x,this.y);
}
深水炸弹发射是基于一个事件—当玩家按下了键盘的空格键 再进行发射。 事件:发生了一件事 (在餐厅点了一份餐) 事件处理:发生事件以后要做的事情 (点完餐让服务员出餐了通知我取餐) **侦听:**用来检测要做的事情有没有完成 (服务员在侦听 有没有出餐) 深水炸弹发射也是由事件产生的,所以需要用到事件和侦听的功能,Java中已经提供了键盘相关的事件和侦听的功能。 在GameWorld类上方导入这两个包
import java.awt.event.KeyAdapter;//键盘侦听器
import java.awt.event.KeyEvent;//键盘事件
在GameWorld类加上深水炸弹入场的方法:
/**
* 深水炸弹入场的方法 ,放到按下键盘空格键以后进行调用
*/
public void bombEnterAction() {
// 1.战舰对象打点调用shootBombs方法,返回一个深水炸弹对象并接收
Bomb obj = ship.shootBomb();
// 2.给深水炸弹数组扩容
bombs = Arrays.copyOf(bombs, bombs.length + 1);
// 3.将产生的深水炸弹对象 装到深水炸弹数组的最后一个位置
bombs[bombs.length-1] = obj;
}
在Action方法里面加上相关需要侦听的逻辑代码并调用深水炸弹入场的方法:
void action() {
//创建了键盘侦听匿名子类 具体实现按下的时 所需要执行的代码逻辑
KeyAdapter key = new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) { //重写父类键盘按下的方法
if (e.getKeyCode() == KeyEvent.VK_SPACE) {//判断用户按下的是否是空格的意思
//调用深水炸弹入场的方法
bombEnterAction();
}
}
};
this.addKeyListener(key);//将创建好的具体侦听逻辑的子类对象 添加到检测当中
Timer t = new Timer();//创建定时器对象
TimerTask task = new TimerTask() { //创建 TimerTask匿名子类 实现重写父类的run方法
@Override
public void run() { //自定义的任务逻辑
submarinesEnterAction();//调用潜艇入场的方法...
thunderEnterAction();//调用雷入场的方法
stepAction();//调用移动的方法
repaint();//重新绘制一次 0.01s
}
};
//1.要执行的具体任务对象 2. 延时时间(毫秒) 3.间隔时间(毫秒)
t.schedule(task, 5000, 10);//间隔0.01s
}
实现战舰的左右移动,在战舰类中添加左右移动的方法:
public void moveLeft() {//左移动的方法
this.x -= speed;
}
public void moveRight() {//右移动的方法
this.x += speed;
}
在GameWorld类的Action方法中进行调用
void action() {
//创建了键盘侦听匿名子类 具体实现按下的时 所需要执行的代码逻辑
KeyAdapter key = new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) { //重写父类键盘按下的方法
if (e.getKeyCode() == KeyEvent.VK_SPACE) {//判断用户按下的是否是空格的意思
//调用深水炸弹入场的方法
bombEnterAction();
}
if(e.getKeyCode() == KeyEvent.VK_LEFT){//判断用户按下的是否是左←键的意思
ship.moveLeft();//调用左移的方法
System.out.println(123);
}
if(e.getKeyCode() == KeyEvent.VK_RIGHT){//判断用户按下的是否是右→键的意思
ship.moveRight();//调用右移的方法
System.out.println(456);
}
}
};
this.addKeyListener(key);//将创建好的具体侦听逻辑的子类对象 添加到检测当中
Timer t = new Timer();//创建定时器对象
TimerTask task = new TimerTask() { //创建 TimerTask匿名子类 实现重写父类的run方法
@Override
public void run() { //自定义的任务逻辑
submarinesEnterAction();//调用潜艇入场的方法...
thunderEnterAction();//调用雷入场的方法
stepAction();//调用移动的方法
repaint();//重新绘制一次 0.01s
}
};
//1.要执行的具体任务对象 2. 延时时间(毫秒) 3.间隔时间(毫秒)
t.schedule(task, 5000, 10);//间隔0.01s
}
分析:有一些潜艇、鱼雷水雷、深水炸弹当已经移出了屏幕外,还需要进行移动和绘制吗??具体点来说,还需要将这些已经除出了屏幕外的对象留在对应的数组里面吗?
优化内存:
400毫秒 1个潜艇 1秒----- 2个潜艇 + 2个雷 --------------共4个对象 1分钟 ---------240个对象 10分钟 --------2400个对象 每10毫秒-------移动的方法里循环遍历 2400个对象 绘制的方法里面循环遍历 2400个对象 -----4800个对象 1秒 -------处理…4800000个对象。
内存泄漏:指的是一直在创建对象。 内存溢出:指的是内部没有地方可以用于创建对象了。 优化海洋对象出界的问题,在父类中写一个判断是否越界的方法,isOutBounds 。
/** 是否是越界的方法
* 为什么不做抽象方法而做普通方法:
* 当前3个潜艇类判断是否越界的行为是一样的,可以进行复用。
* 其它3个类鱼雷类、水雷类、深水炸弹 自行进行重写实现既可。
* */
public boolean isOutBounds(){
return this.x >= GameWorld.WIDTH;//判断潜艇对象是否越界的标准
}
其它3个类鱼雷类、水雷类、深水炸弹 自行进行重写实现既可。
鱼雷类:
@Override
public boolean isOutBounds() {
return this.y <= -height;//y如果小于或等于 -一个图片的高度,说明超过了上方窗口了
}
水雷类:
@Override
public boolean isOutBounds() {
return this.y <= 150 -height;//判断是否在水平面上
}
深水炸弹:
@Override
public boolean isOutBounds() {//重写父类判断是否越界的标准
return this.y >= GameWorld.HEIGHT;//是否超过屏幕的高
}
删除越界对象的行为是自动发生的,在GameWorld类里面定义一个方法,outOfBounds。
/**
* 删除越界对象的行为逻辑: 放到run中 移动的方法后面调用。
*/
public void outOfBounds() {
//循环遍历潜艇数组的每个对象 并依次调用每个对象判断是否越界的标准方法
for (int i = 0; i < submarines.length; i++) {
if (submarines[i].isOutBounds()) { //2.判断如果当前对象越界
//3.将最后一个元素的对象 覆盖给当前越界对象的位置
submarines[i] = submarines[submarines.length - 1];
//4.将数组最后一个元素删除掉(缩容)
submarines = Arrays.copyOf(submarines, submarines.length - 1);
}
}
//2.循环遍历thunders数组的每个对象 并依次调用每个对象判断是否越界的标准方法
for (int i = 0; i < thunders.length; i++) {
if (thunders[i].isOutBounds()) { //2.判断如果当前对象越界
//3.将最后一个元素的对象 覆盖给当前越界对象的位置
thunders[i] = thunders[thunders.length - 1];
//4.将数组最后一个元素删除掉(缩容)
thunders = Arrays.copyOf(thunders, thunders.length - 1);
}
}
//3.循环遍历bombs数组的每个对象 并依次调用每个对象判断是否越界的标准方法
for (int i = 0; i < bombs.length; i++) {
if (bombs[i].isOutBounds()) { //2.判断如果当前对象越界
//3.将最后一个元素的对象 覆盖给当前越界对象的位置
bombs[i] = bombs[bombs.length - 1];
//4.将数组最后一个元素删除掉(缩容)
bombs = Arrays.copyOf(bombs, bombs.length - 1);
}
}
}
放到run中进行调用判断是否越界的方法
public void run() { //自定义的任务逻辑
submarinesEnterAction();//调用潜艇入场的方法...
thunderEnterAction();//调用雷入场的方法
stepAction();//调用移动的方法
outOfBounds();//判断对象是否需要越界删除的方法
System.out.println('潜艇数组里的对象' + submarines.length + ' 雷的数组里的对象' + thunders.length);
repaint();//重新绘制一次 0.01s
}
|