分享

利用简单游戏项目教你如何用java如何画对象

 好汉勃士 2024-03-19 发布于广东

画对象只需三个步骤:

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.arraycopyint[] 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
            }

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多