前一末节,咱们完成了一个包括能够制作坦克图片的绘图板的游戏窗体小程序。这一末节,咱们要完成的方针如下:

  1. 规划坦克的基类
  2. 完成各种类型带血条坦克的制作
  3. 完成坦克移动
  4. 完成经过方向键操控玩家坦克移动

先调整上一末节的类规划,将MyPanel作为MyFrame的成员变量,在MyFrame无参结构中对其进行实例化和赋值;而MyPanel中也持有对MyFrame的依赖,调整如下:

package com.pf.java.tankbattle;
​
import ...
​
public class MyFrame extends JFrame {
  
  private MyPanel panel;
​
  public MyFrame() {
     ...
​
    // 将面板组件添加到窗口目标的内容面板中
    this.panel = new MyPanel(this);
    getContentPane().add(panel);
​
     ...
​
   }
}
package com.pf.java.tankbattle;
​
import ...
​
public class MyPanel extends JPanel {
  
  private MyFrame frame;
​
  public MyPanel(MyFrame frame) {
    this.frame = frame;
     ...
   }
}

这样主类就简化为:

package com.pf.java.tankbattle;
​
import ...
​
public class GameMain {
​
  public static void main(String[] args) {
     ...
    // 创立窗体目标
    new MyFrame();
   }
​
}

以上的操作仍是遵从面向目标封装的思想,客户端(游戏主类)不需要关心游戏窗体组件内部的部件,这也不应该对客户端暴露出来。复习了下Java中面向目标的封装思想,咱们再来看看面向目标中的继承。

坦克类规划

运用Java多线程实现坦克移动

这儿咱们首先考虑一个坦克有哪些特点和行为,为其规划一个基类。然后再扩展两个详细的坦克类:玩家的英豪坦克和电脑的敌军坦克来继承这个基类。一起来看下基类中的特点(这儿省略了getter和setter办法)和办法(省略了特定的结构器)。

根本的特点:

  • x、y坐标

    代表坦克在绘图板中被制作时的左上角的坐标方位,坦克在行进时会导致某个方向的坐标值改变,转向时也可能导致坐标点的改变。

  • speed

    坦克行进的速度,也便是每1000毫秒坦克移动的像素数,假如坦克的速度是40,则移动一个像素需要25毫秒。假如经过多线程来操控坦克移动,则只要每休眠25毫秒让坦克往前移动一个像素即可。

  • direction

    枚举类型,坦克行进的方向。

  • blood

    坦克的血点,表现坦克血条的长度,被敌方坦克炮弹击中后会掉血,掉到0则坦克会被炸毁(调用其die()办法)。

  • picIndex

    在制作坦克时要确定的索引方位,取值范围0至13。

    运用Java多线程实现坦克移动

  • gearToggle

    记录履带交替改动的布尔变量,坦克每向前移动一个像素,就会在两个只有履带纹样不同的坦克图片之间进行切换:

    运用Java多线程实现坦克移动

  • frame

    游戏窗体目标

关于坦克根本的行为,这儿咱们暂时供给几个办法:

/**
 * 坦克移动的办法,每次移动一个像素的间隔
 * @return 是否被阻挠的布尔值,假如被阻挠则不会往前移动一个像素
 */
public boolean move() {
  // todo 待完成
  return false;
}
​
/**
 * 坦克转向的办法
 * @param direction 调转的方向
 */
public void turnRound(Direction direction) {
  // 调用direction特点的setter办法设置新的方向
  setDirection(direction);
}
​
/**
 * 坦克被制作的办法
 * @param g 绘图板的画笔目标
 */
public void paint(Graphics g) {
    // todo 待完成
}
​
/**
 * 坦克被炸毁的办法
 */
public void die() {
    // todo 待完成
}

接下来咱们侧重完成paint(Graphics g)move()办法。

完成paint办法

首先咱们封装一个制作各种类型坦克的办法,完成代码如下:

public void paint(Graphics g) {
  // 依据坦克的方向获取其索引 ↓1处
  int index = direction.ordinal();
  // 核算截取坦克图片的开始方位 ↓2处
  int subX = (picIndex + 28 * index + (gearToggle ? 14 : 0)) * SIZE;
  // 抠图并制作 ↓3处
  g.drawImage(ResourceMgr.tank.getSubimage(subX, 0, SIZE, SIZE), x, y, null);
}

代码详解:

  1. 1处获取枚举项地点的索引值,咱们在界说方向枚举时是依照上、右、下、左的顺序界说的:

    package com.pf.java.tankbattle.enums;
    ​
    /**
     * 方向枚举类
     */
    public enum Direction {
      UP, RIGHT, DOWN, LEFT;
    }
    

    方向和索引的关系如下:

    运用Java多线程实现坦克移动

    因而,假如坦克的方向为DOWN,则经过direction.ordinal()咱们将得到索引值2

  2. 2处核算要制作的坦克的开始方位

    从下图中不难发现,当picIndex确定后,即要制作的坦克类型确定后,假设picIndex0,咱们发现每经过28个SIZE的像素单位后坦克的方向发生了改变,自然依照第一步确定的index核算出的偏移量28 * index,再加上操控履带改变的变量,索引的偏移量为28 * index + (gearToggle ? 14 : 0),再算上picIndex和坦克的SIZE,最终得到核算坦克抠图的开始方位的表达式为:

    (picIndex + 28 * index + (gearToggle ? 14 : 0)) * SIZE
    

    运用Java多线程实现坦克移动

  3. 3处依照第二步核算出来的坦克图片的扣取区域的起点方位,扣取坦克SIZE宽高的区域,并以坦克当时的(x, y)坐标点进行制作。

下面测验下坦克的制作:

package com.pf.java.tankbattle;
​
import ...
​
public class MyPanel extends JPanel {
​
  // 暂时在绘图板中界说一个英豪
  private HeroTank heroTank;
  
   ...
​
  public MyPanel(MyFrame frame) {
     ...
​
    // 实例化咱们的英豪
    heroTank = new HeroTank(Direction.DOWN, 32, 32, 80, 0, frame);
​
   }
​
  @Override
  protected void paintComponent(Graphics g) {
     ...
​
    // 测验坦克的制作,因为该办法会被调用多次,为避免被覆盖,x坐标每次都设置下  
    heroTank.setX(32);
    heroTank.paint(g);
​
    // 改动履带和x坐标方位再制作一次
    heroTank.setGearToggle(!heroTank.isGearToggle());
    heroTank.setX(64);
    heroTank.paint(g);
   }
}

程序运转截图:

运用Java多线程实现坦克移动

现在咱再给坦克安上血条,首先咱们编写一个工具类,以不同的色彩代表不同的血量范围,工具类代码如下:

package com.pf.java.tankbattle.util;
​
import ...
​
public class LifeColorUtil {
​
  /**
   * 依据血量核算出要显示的血条色彩
   * @param blood
   * @return
   */
  public static Color parseColor(int blood) {
    Color c;
    if (blood >= 90) {
      c = new Color(127, 255, 0);
     } else if (blood >= 80) {
      c = new Color(118, 238, 0);
     } else if (blood >= 60) {
      c = new Color(179, 238, 58);
     } else if (blood >= 50) {
      c = new Color(238, 238, 0);
     } else if (blood >= 40) {
      c = new Color(238, 220, 130);
     } else if (blood >= 30) {
      c = new Color(255, 193, 37);
     } else if (blood >= 15) {
      c = new Color(255, 127, 36);
     } else {
      c = new Color(255, 48, 48);
     }
    return c;
   }
​
}

好在idea支撑色值展示,咱们能够看到随着血量的削减,相应的色值的改变:

运用Java多线程实现坦克移动

Tank类的paint办法的最终再制作上血条:

public void paint(Graphics g) {
   ...
​
  // 依据血量核算出血条色彩
  g.setColor(LifeColorUtil.parseColor(blood));
  // 制作血条
  g.fillRect(x, y == 0 ? y : y - 2, 32 * blood / 100, 2);
}

MyPanel类中完善测验代码:

package com.pf.java.tankbattle;
​
import ...
​
public class MyPanel extends JPanel {
​
   ...
​
  @Override
  protected void paintComponent(Graphics g) {
     ...
​
     ...
    // 设置血量
    heroTank.setBlood(80);
    heroTank.paint(g);
​
     ...
    // 设置血量
    heroTank.setBlood(40);
    heroTank.paint(g);
    
     ...
    heroTank.setBlood(12);
    heroTank.paint(g);
   }
}

作用:

运用Java多线程实现坦克移动

完成坦克移动

要完成坦克的移动很简单,暂时不考虑与边界和障碍物的碰撞检测,在Tank基类中完成如下:

public boolean move() {
  // 让坦克履带转动起来
  gearToggle = !gearToggle;
  // 完成在行进方向移动一个像素的间隔
  switch (direction) {
    case LEFT:
      x--;
      break;
    case UP:
      y--;
      break;
    case RIGHT:
      x++;
      break;
    case DOWN:
      y++;
   }
  return true;
}

为了让坦克在绘图板中“活”起来,咱们需要不断的刷新绘图板的画面,也便是先清除画布,再重新在新的方位制作坦克,这样坦克就动起来了。为此咱们在游戏窗体中创立一个线程,来对整个窗体进行不断的重绘:

package com.pf.java.tankbattle;
​
import ...
​
public class MyFrame extends JFrame {
​
  private Thread paintThread;
  
   ...
​
  public MyFrame() {
     ...
​
    // 创立一个线程,不停履行对游戏窗体进行重绘
    paintThread = new Thread(() -> {
      while (true) {
        try {
          // 刷新的频率越快,动画越流通,但也要考虑CPU的开销
          Thread.sleep(20);
         } catch (InterruptedException e) {
          e.printStackTrace();
         }
        repaint();
       }
     });
    
    paintThread.start();
​
   }
}

对当时的窗体目标进行repaint时,MyPanel中的paintComponent办法会主动被调用,因而该办法只要简化为如下即可:

protected void paintComponent(Graphics g) {
  super.paintComponent(g);
  heroTank.paint(g);
}

剩余的事则是,在游戏发动后,操控坦克移动(调用其move()办法)即可。

package com.pf.java.tankbattle.entity.tank;
​
import ...
​
public class HeroTank extends Tank {
​
  /** 坦克发动机线程 */
  private Thread moveThread;
​
  public HeroTank(Direction direction, int x, int y, int speed, int picIndex, MyFrame frame) {
    super(direction, x, y, speed, picIndex, frame);
​
    // 结构和发动坦克引擎
    moveThread = new Thread(() -> {
      // todo 这儿先不考虑坦克被炸毁的状况,引擎发动后就一向持续下去
      while (true) {
        // 只管向前冲
        move();
        try {
          // 核算每走一个像素花费的毫秒数,并以此作为休眠时刻
          Thread.sleep(1000 / speed);
         } catch (InterruptedException e) {
          e.printStackTrace();
         }
       }
     });
    
    moveThread.start();
​
   }
}

作用如下:

运用Java多线程实现坦克移动

经过方向键操控玩家坦克

是时分将咱们的意志注入给英豪的坦克了。接下来咱们要完成经过上下左右方向键操控玩家坦克移动。玩家能够一起按下多个方向键,最终按下的起作用,当松开一个方向键后,最近一次按下的起作用,而当悉数方向键都松开后,坦克停下来,能够参考下面的示意图:

运用Java多线程实现坦克移动

咱们将经过Java AWT组件供给的键盘事情监听器,再结合多线程来完成上述需求。详细代码如下:

package com.pf.java.tankbattle.entity.tank;
​
import ...
​
public class HeroTank extends Tank {
​
  /** 坦克发动机线程 */
  private Thread moveThread;
​
  public HeroTank(Direction direction, int x, int y, int speed, int picIndex, MyFrame frame) {
    super(direction, x, y, speed, picIndex, frame);
    // 注册键盘事情
    frame.addKeyListener(new MyKeyListener());
   }
​
  /**
   * 内部类,完成了键盘事情(键按下、键松开)的处理办法
   */
  class MyKeyListener extends KeyAdapter {
​
    /** 记录已按下的方向键的数值 */
    private LinkedList<Integer> oprs;
    /** 坦克是否处于静止状况,留意必需要确保多线程的可见性,用volatile修饰 */
    private volatile boolean stop = true;
​
    public MyKeyListener() {
      oprs = new LinkedList<>();
      moveThread = new Thread(() -> {
        // todo 这儿先不考虑坦克被炸毁的状况
        while (true) {
          // 假如坦克处于中止状况则将线程park住
          if (stop) {
            LockSupport.park();
           }
          // 在行进方向移动坦克
          move();
          try {
            // 核算每走一个像素花费的毫秒数,并以此作为休眠时刻
            Thread.sleep(1000 / getSpeed());
           } catch (InterruptedException e) {
            e.printStackTrace();
           }
         }
       });
      moveThread.start();
     }
​
    @Override
    public void keyPressed(KeyEvent e) {
      int key = e.getKeyCode();
      switch (key) {
        case KeyEvent.VK_LEFT:
        case KeyEvent.VK_UP:
        case KeyEvent.VK_RIGHT:
        case KeyEvent.VK_DOWN:
          break;
        default:
          return;
       }
      // 假如不包括在操控列表中则添加进来
      if (!oprs.contains(key)) {
        oprs.add(key);
       }
      // 预备发动坦克
      if (stop) {
        stop = false;
        LockSupport.unpark(moveThread);
       }
      // 设置坦克转向为最新按下的方向键
      setDirection(getDirectionByKey(key));
     }
​
    @Override
    public void keyReleased(KeyEvent e) {
      // 留意下面调用oprs.remove办法传入的参数有必要是包装类型
      Integer key = e.getKeyCode();
      switch (key) {
        case KeyEvent.VK_LEFT:
        case KeyEvent.VK_UP:
        case KeyEvent.VK_RIGHT:
        case KeyEvent.VK_DOWN:
          break;
        default:
          return;
       }
      // 移除松开的方向键
      oprs.remove(key);
      if (oprs.isEmpty()) {
        // 一切方向键都松开,则操控线程的状况变量设为中止
        stop = true;
       } else {
        // 否则取方向操控列表中最近一次添加的
        setDirection(getDirectionByKey(oprs.getLast()));
       }
     }
​
    private Direction getDirectionByKey(int key) {
      switch (key) {
        case KeyEvent.VK_LEFT:
          return Direction.LEFT;
        case KeyEvent.VK_UP:
          return Direction.UP;
        case KeyEvent.VK_RIGHT:
          return Direction.RIGHT;
        case KeyEvent.VK_DOWN:
          return Direction.DOWN;
        default:
          return null;
       }
     }
   }
}

阐明

  1. 这儿操控moveThread线程的运转和中止采用的是juc包中的LockSupport类,调用其pack()挂起当时线程,可是持有的锁不会被释放,和Thread.sleep(millis)相似,仅仅前者唤醒能够由其他线程操控,调用LockSupport.unpark(thread)即可唤醒从前被park住的线程。
  2. 这儿界说的stop变量会有多个线程访问,监听键盘事情的后台线程会对该变量进行读写,而咱们创立的moveThread也会读取它,因而有必要用volatile关键字来修饰它,确保其可见性。
  3. 对方向键的存取这儿采用的是LinkedList,而不是ArrayList,因为有频频的刺进和删去操作,自然链表结构完成的效率会更高。

运转程序,玩家能够顺利的操作方向键来灵活的操控玩家坦克,手感杠杠滴,作用如下:

运用Java多线程实现坦克移动

但存在一个很明显的瑕疵,当时间短的切换方向键时,无法操控坦克只转向而不移动,实践坦克仍是会移动一段间隔,作用如下:

运用Java多线程实现坦克移动

修复办法:当坦克由静止状况时,按下一个方向键,moveThread线程会持续履行LockSupport.park()后续的代码,此刻能够恰当休眠下,在这个时刻间隙里,坦克不会移动,而超过这个时刻间隔后才持续调用move()办法。添加的操控逻辑:

moveThread = new Thread(() -> {
  while (true) {
    if (stop) {
      LockSupport.park();
      // 操控坦克只转向而不移动
      try {
        // 这儿时间短休眠下再进行下一轮判别,以便完成时间短按键下只转向不移动
        Thread.sleep(100);
       } catch (InterruptedException e) {
        e.printStackTrace();
       }
      continue;
     }
     ...
   }
});

作用如下:

运用Java多线程实现坦克移动

经过这一末节的学习,相信大伙儿在敲代码中慢慢找到了学习Java的趣味,把多线程和调集的常识也运用进来了,对于面向目标也了解的更深入些了吧。不过这才是开始,后续咱们将逐步的过渡到规划形式的实操上来,大家加油!