Java 保姆级教程——3.添加地图,地图根据人物移动

Java 保姆级教程——3.添加地图,地图根据人物移动

注:经过上期的学习,我们学习了如何创建JFrame窗体、如何添加监听事件。根据大家给我的反馈,这一期给大家讲述:如何添加地图、地图如何根据人物移动。

准备阶段

为了更直观的讲解,我们先看看我今晚写的小案例(为了更好的理解,大家要看过之后再浏览后续文章,闪烁问题使用双缓冲即可解决,我这里仅为演示,暂且没有写)

1.首先,我们需要了解几个概念

1.如何将自己的游戏合理的拆解成多个类?

答:
1.首先,分析我们这个小案例都有哪些部分组成:
(我这里直接将程序截图给大家,是便于大家的理解,实际情况,这个分析要在脑海里进行,并且借用一些工具如(微软自带的画图工具、记事本记录)进行记录)
分析过程:
首先肯定有游戏窗体,游戏窗体我们最好继承JFrame便于我们根据自己的需求去定制窗体。
其次根据我们的游戏,我们有地图,地图需不需要独立成一个类呢? 这取决于你要做什么游戏,若为棋牌类、泡泡堂等地图本身并无改变需求,反之像魂斗罗、超级玛丽等地图会发生改变的游戏,我们则需要将地图独立出来,便于我们后续更改。还需要有人物,因为人物有移动、停止等状态切换,所以人物类也许独立出来。
所以:
组成部分:1. 游戏的窗体 2. 人物 3. 地图 4. 边缘类(更类似于工具类)
我们这个小案例功能很小,但是不妨碍我们去分析,同时大家要训练自己举一反三的能力(任何能力均可以在重复中习得)。
Java 保姆级教程——3.添加地图,地图根据人物移动_第1张图片

如下图所示,我们的小案例分为以下部分,分别是:
1.MarioFrame继承自JFrame(游戏主窗体)
2.MarioMap (地图类)
3.Player (人物类)
4.一些边缘类:MarioImageUtils(图片加载类)、Dir(方向枚举类)、PlayerStatus(人物状态枚举)
Java 保姆级教程——3.添加地图,地图根据人物移动_第2张图片

2.每个类应该怎么做好自己的工作

在经过上一步的分析和拆解后,下一步我们需要思考每个类应该做好什么。
人物应该直接画在地图上吗? 假如我们将人物直接画在地图上(地图中创建一个Player对象),那么当我们需要更改地图时,难道我们再去创建一个Player对象吗?
显然:人物和地图是独立的类,需要思考他们之间的关系,在学习过程中,要尽量避免两个独立的类中持有一方的对象,除非他们之间有必然联系。
做好自己的工作(单一原则)这样做的好处:
1.代码耦合度降低(可百度耦合度)
2.提高类的可维护性和可读性
3.降低变更风险:当需要更改一个类的行为或者属性,只需要在这个类中去更改,不会影响到其他地方对此类的调用
所以
人物要做的是拥有自身的属性(x,y,width,height,image) 和画自己(paint)的行为
地图也拥有自身的属性和画自己的行为
那么,问题来了,人物和地图都有独立的方法和行为且不互相调用,那么怎么画到窗体上呢?
答案是:有MarioFrame进行调用,MarioFrame类中应当有人物和地图类的对象引用。
具体怎么实现:
在MarioFame初始化时,将人物和地图类的对象实例化
MarioFrame拥有paint方法,在paint方法中调用人物和地图类对象的paint方法,游戏具体的逻辑(人物与地图的关系)应该由MarioFrame进行控制

3.游戏不应该是人物在移动吗?为什么要移动地图

首先 游戏中确实是人物在移动,但只说对了一半,人物的移动是在地图只有切换无需延申的基础上,超级玛丽、魂斗罗类横板2D游戏当人物走到地图边界时就需要移动地图了。
地图是如何移动的呢?
非常简单,在一定的规则下改变地图paint时图片的X坐标即可 x+speed地图向右移动、x-speed地图向左边移动
那么地图怎么移动的规则是什么呢?
看下图↓
这是我们地图的图片 width 1000 height 358
Java 保姆级教程——3.添加地图,地图根据人物移动_第3张图片

案例:
Java 保姆级教程——3.添加地图,地图根据人物移动_第4张图片

案例中 窗体的大小为width500 height358 ,左边界为X(0-100),右边界X(400-500),自由移动区域X(100-400)
当我们在width=500的窗体上画width=1000的图片时,会根据图片的x坐标来显示图片,所以对比地图和窗体的两张图片发现,当地图的x=0时,地图只显示了一半。
1.当人物在自由移动区域时,地图无需移动
2.当人物抵达左右边界X坐标时,地图移动。
具体规则(在MarioFrame的paint方法中进行控制):
人物在自由移动区域移动时,地图不移动
当人物抵达左边界时,判断地图的坐标是否<=0(即:判断人物是否已经到达地图的左尽头),向右移动原理相同,判断条件做出更改地图x坐标>= - (1000-speed)
Java 保姆级教程——3.添加地图,地图根据人物移动_第5张图片

以上就是地图移动的思想了,剩下要做的就是如何将思想转化为代码了。

2.代码实现

1.创建窗体类

public class MarioFrame extends JFrame {
   public MarioFrame() {
       // 
      init();
   }
   // 初始化窗体函数
   public void init() {
      // 清空默认布局管理器
      this.setLayout(null);
      // 设置窗体的大小
      this.setSize(GameConstant.GAME_CLIENT_WIDTH, GameConstant.GAME_CLIENT_HEIGHT);
      // 设置窗体显示的位置,这里
      this.setLocation((int) (Toolkit.getDefaultToolkit().getScreenSize().getWidth() - this.getWidth()) / 2,
            (int) (Toolkit.getDefaultToolkit().getScreenSize().getHeight() - this.getHeight()) / 2);
      // 设置窗体关闭按钮点击时窗体的状态
      this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
      // 设置不可更改大小
      this.setResizable(false);
   }
}

2.创建图片加载类(边缘类) 用以存储游戏中所用到的图片

/**
* 图片加载类
*/
public class MarioImageUtils {
   // 地图
   public static BufferedImage MAP_IMG = null;
   // 人物向左
   public static BufferedImage PLAYER_LEFT_IMG = null;
   // 人物向右
   public static BufferedImage PLAYER_RIGHT_IMG = null;
   // 加载图片
   static {
      try {
         MAP_IMG = ImageIO.read(MarioImageUtils.class.getResource("/images/backGround_img.png"));
         PLAYER_LEFT_IMG = ImageIO.read(MarioImageUtils.class.getResource("/images/player_left.png"));
         PLAYER_RIGHT_IMG = ImageIO.read(MarioImageUtils.class.getResource("/images/player_right.png"));
      } catch (IOException e) {
         e.printStackTrace();
      }
   }
}

3.游戏窗体常量类(边缘类)存储游戏中的常量

public class GameConstant {
   // 窗体宽度
   public static final int GAME_CLIENT_WIDTH=500;
   // 窗体高度
   public static final int GAME_CLIENT_HEIGHT=358;
}

4.创建地图类

/**
 * 地图类
 */
public class MarioMap {
   // 地图属性
   public int speed = 5, x = 0, y = 0, width = GameConstant.GAME_CLIENT_WIDTH, height = GameConstant.GAME_CLIENT_HEIGHT;

   // 画自身,由MarioFrame调用
   public void paint(Graphics g) {
      // 使用窗体的画笔,将自身画到窗体上
      g.drawImage(MarioImageUtils.MAP_IMG, x, y, 1000, height, null);
   }

   // 地图的移动方法,右MarioFrame 调用
   public void move(Dir dir) {
      switch (dir) {
         case RIGHT:// 人物向右移动,图片向左位移
            // 判断地图是否在右尽头
            if (x > -(width - speed))
               x -= speed;
            break;
         case LEFT:// 人物向左移动,图片向右位移
            // 判断地图是否在左尽头
            if (x <= 0)
               x += speed;
            break;
      }
   }
}

5.创建人物类及所需的边缘类
1.人物方向枚举类

/**
 * 方向类
 */
public enum Dir {
   RIGHT,LEFT
}

2.人物状态枚举类

/**
 * 人物状态类
 */
public enum PlayerStatus {
   RUN, STOP
}

3.人物类,当人物走到窗体(注意!不是地图的左右边界)的左右边缘时,人物停止移动

public class Player {
   // 人物的属性
   public int x, y, speed, width, height;
   // 人物的方向
   public Dir dir = Dir.LEFT;
   // 人物的状态
   public PlayerStatus playerStatus = PlayerStatus.STOP;
   // 人物的图片
   private BufferedImage playerImage = MarioImageUtils.PLAYER_RIGHT_IMG;

   // 初始化对象
   public Player() {
      speed = 5;
      width = 32;
      height = 32;
      x = (GameConstant.GAME_CLIENT_WIDTH - width) / 2;
      y = GameConstant.GAME_CLIENT_HEIGHT - 77;
   }

   // 画自身
   public void paint(Graphics g) {
      // 画的同时,调用人物移动的方法
      move();
      // 将自身画在屏幕上
      g.drawImage(playerImage, x, y, width, height, null);
   }

   // 自身移动
   public void move() {
      // 若自身的状态为RUN移动,这个状态由MarioFrame控制
      if (playerStatus == PlayerStatus.RUN) {
         // 根据方向进行移动
         switch (dir) {
            case LEFT:
               // 设置人物左面向 图
               playerImage = MarioImageUtils.PLAYER_LEFT_IMG;
               // 向左时,判断自身是否已经到达了最左边
               if (x > 0)
                  x -= speed;
               break;
            case RIGHT:
               // 设置人物右面向 图
               playerImage = MarioImageUtils.PLAYER_RIGHT_IMG;
               // 向右时,要判断自身位置是不是到达了最右边,所以是窗体宽度减去自身宽度
               if (x < GameConstant.GAME_CLIENT_WIDTH - width)
                  x += speed;
               break;
         }
      }
   }
}

6.在MarioFrame中实现我们之前的思想
地图、人物、边缘类均已准备就绪,而人物、地图又是要画在窗体上的,那么我们的MarioFrame窗体类是不是应该持有人物、地图的对象引用,以便我们画图。
有了这个思想基础,那么我们需要先做两步事情
1.在我们的MarioFrame中创建地图、人物对象,作为MarioFrame的属性

// 地图 对象
private MarioMap marioMap;
// 人物 对象
private Player player;

2.在MarioFrame的init(初始化方法中)将地图类、人物类对象进行初始化

public void init() {
    // 初始化地图
    marioMap = new MarioMap();
    // 创建人物
    player = new Player();
}

而后怎么做呢?我们的地图、人物类都有画自己的方法,那么画的方法在哪里调用呢?
我们的MarioFrame类继承自JFrame,而JFrame有paint方法,所以在MarioFrame中重写继承自JFrame的paint方法,并在MarioFrame的paint方法中调用地图、人物的paint方法。
现在,我们知道在哪里调用地图、人物的paint了,那么这个调用paint方法有没有什么原则呢?或者说到底是先画地图还是先画人物呢?
paint画笔原则:paint 中画笔的层级顺序,和栈相同(先进后出),先画底显,就是一层一层的画,先画的显示在底层
我们是不是应该先把地图画到窗体上?
如果我们画人物,再画地图,根据paint画笔的原则,人物会被地图覆盖。
违反原则的展示

// 重写JFrame的paint方法
@Override
public void paint(Graphics g) {
    // 先画人物
    player.paint(g);
    // 再画地图
    marioMap.paint(g);
}

运行:
其实地图、人物都画到了窗体,只是人物的图片先画,地图后画,先画的人物被后画的地图遮挡住了
Java 保姆级教程——3.添加地图,地图根据人物移动_第6张图片

遵循原则的展示:

// 重写JFrame的paint方法
@Override
public void paint(Graphics g) {
    // 再画地图
    marioMap.paint(g);
    // 先画人物
    player.paint(g);
}

运行:
ok,达到了我们的预期需求
Java 保姆级教程——3.添加地图,地图根据人物移动_第7张图片

让人物动起来
我们的人物类中,拥有move方法,且被自身的paint方法调用,move方法中,我们制定了移动的规则,这个规则的关键是人物的状态(PlayerStatus)、人物的方向(Dir),且在人物类中,我们已经将这两个枚举类的对象设置为public。
接下来要做的就是在MarioFrame中通过改变人物对象的这两个状态来控制人物了
我们知道怎么让人物动起来,那么在MarioFrame的哪一部分去控制人物的这两个状态呢?
显然,我们需要获取用户的输入,即添加监听器
在MarioFrame的init方法中添加键盘监听

public void init(){
        this.addKeyListener(new KeyAdapter() {
       // 当键盘按下时
       @Override
       public void keyPressed(KeyEvent e) {
          switch (e.getKeyCode()) {
             case KeyEvent.VK_LEFT:// 左键
                player.playerStatus = PlayerStatus.RUN;
                player.dir = Dir.LEFT;
                break;
             case KeyEvent.VK_RIGHT:// 右键
                player.playerStatus = PlayerStatus.RUN;
                player.dir = Dir.RIGHT;
                break;
          }
       }
    
       @Override
       public void keyReleased(KeyEvent e) {
          // 键盘抬起左或右键时,人物的状态设置为STOP
          switch (e.getKeyCode()) {
             case KeyEvent.VK_LEFT:
             case KeyEvent.VK_RIGHT:
                player.playerStatus = PlayerStatus.STOP;
                break;
          }
       }
    });
}

Nice! 经过一番努力,我们知道了:
1.要在MarioFrame的paint方法中调用地图、人物的paint
2.地图、人物画的顺序:根据画笔原则先画地图在画人物
3.在人物类(Player)中制定了移动(move)的规则
4.在MarioFrame中添加键盘监听器
哈哈哈哈哈~~!这下我们的人物该动起来了吧!!
运行吧! 冲啊!!马里奥! 运行----->
????????????????????????????
Java 保姆级教程——3.添加地图,地图根据人物移动_第8张图片

啊!!马里奥,你怎么了,你的腿摔断了吗???

为什么马里奥还是动不了?
这个问题的答案,我在第一期:什么是游戏中有具体讲述。
当我们运行程序时,java确实调用了MarioFrame的paint方法 ,MarioFrame.panit方法中也确实调用了地图、人物的paint方法。
关键在于,仅仅画了一次(一帧)!
人物的paint方法也只调用了一次,paint中的move方法也只调用了一次,窗体上也只有一帧画面(帧在第一期中也有讲述)那画面怎么可能动起来呢?
所以我们要将画面的每一次改变都画到窗体上,这时候就需要用到线程,持续的去重画整个窗体,也就是把每帧都显示出来
解决:在MarioFrame的init方法中添加线程,重画窗体

public void init(){
   // 保证游戏的流畅性,每30毫秒刷新一次
   new Thread(() -> {
      while (true) {
         try {
            // 重画窗体
            repaint();
            // 每30ms执行一次
            Thread.sleep(30);
         } catch (InterruptedException e) {
            e.printStackTrace();
         }
      }
   }).start();
}

这下,我们的人物终于能移动了!!!
剩下的就是将我们左右边界、地图移动的思想(人物与地图的关系)添加到MarioFrame中了

  1. 当人物向左移动,且抵达右边界,而地图并没有到右尽头,此时,地图向右移动
    2.当人物向左移动,且抵达左边界,而地图并没有到左尽头,此时,地图向左移动
    3.当人物到达右边界,且地图到了右尽头,人物可以移动至右边界外
    4.当人物到达左边界,且地图到了左尽头,人物可以移动至左边界外
    因为由了重复刷新调用paint方法,我们就可将人物与地图的关系添加到MarioFrame的paint方法中了
// 判断人物的状态
if (player.playerStatus == PlayerStatus.RUN) {
   // 当人物向右,且抵达右边界时
   if (player.x + player.width > 400 && player.dir == Dir.RIGHT) {
      // 地图移动
      marioMap.move(Dir.RIGHT);
      // 因为地图和人物的速度都是5,同时移动就是10速度了,所以将人物的速度设置为0,仅移动地图即可
      player.speed = 0;
      // 如果向右的同时,地图到达了右尽头时,人物的速度设置为5
      if (marioMap.x <= -(GameConstant.GAME_CLIENT_WIDTH - 5)) player.speed = 5;
   } else if (player.x < 100 && player.dir == Dir.LEFT) {// 当人物向左,且抵达左边界时
      marioMap.move(Dir.LEFT);
      player.speed = 0;
      if (marioMap.x >= 0) player.speed = 5;
   } else player.speed = 5; //自由移动区域
}

最终MarioFrame类的所有代码

public class MarioFrame extends JFrame {
   // 地图 对象
   private MarioMap marioMap;
   // 人物 对象
   private Player player;

   public MarioFrame() {
      // 初始化窗体
      init();
   }

   public void init() {
      // 清空默认布局管理器
      this.setLayout(null);
      // 设置窗体的大小
      this.setSize(GameConstant.GAME_CLIENT_WIDTH, GameConstant.GAME_CLIENT_HEIGHT);
      // 设置窗体显示的位置,这里
      this.setLocation((int) (Toolkit.getDefaultToolkit().getScreenSize().getWidth() - this.getWidth()) / 2,
            (int) (Toolkit.getDefaultToolkit().getScreenSize().getHeight() - this.getHeight()) / 2);
      // 设置窗体关闭按钮点击时窗体的状态
      this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
      // 设置不可更改大小
      this.setResizable(false);
      // 初始化地图
      marioMap = new MarioMap();
      // 创建人物
      player = new Player();
      this.setVisible(true);
      // 添加键盘监听事件
      this.addKeyListener(new KeyAdapter() {
         // 当键盘按下时
         @Override
         public void keyPressed(KeyEvent e) {
            switch (e.getKeyCode()) {
               case KeyEvent.VK_LEFT:// 左键
                  player.playerStatus = PlayerStatus.RUN;
                  player.dir = Dir.LEFT;
                  break;
               case KeyEvent.VK_RIGHT:// 右键
                  player.playerStatus = PlayerStatus.RUN;
                  player.dir = Dir.RIGHT;
                  break;
            }
         }

         @Override
         public void keyReleased(KeyEvent e) {
            // 键盘抬起左或右键时,人物的状态设置为STOP
            switch (e.getKeyCode()) {
               case KeyEvent.VK_LEFT:
               case KeyEvent.VK_RIGHT:
                  player.playerStatus = PlayerStatus.STOP;
                  break;
            }
         }
      });
      // 保证游戏的流畅性,每30毫秒刷新一次
      new Thread(() -> {
         while (true) {
            try {
               repaint();
               Thread.sleep(30);
            } catch (InterruptedException e) {
               e.printStackTrace();
            }
         }
      }).start();
   }

   // 重写JFrame的paint方法
   @Override
   public void paint(Graphics g) {
      marioMap.paint(g);
      player.paint(g);
      // 判断人物的状态
      if (player.playerStatus == PlayerStatus.RUN) {
         // 当人物向右,且抵达右边界时
         if (player.x + player.width > 400 && player.dir == Dir.RIGHT) {
            // 地图移动
            marioMap.move(Dir.RIGHT);
            // 因为地图和人物的速度都是5,同时移动就是10速度了,所以将人物的速度设置为0,仅移动地图即可
            player.speed = 0;
            // 如果向右的同时,地图到达了右尽头时,人物的速度设置为5
            if (marioMap.x <= -(GameConstant.GAME_CLIENT_WIDTH - 5)) player.speed = 5;
         } else if (player.x < 100 && player.dir == Dir.LEFT) {// 当人物向左,且抵达左边界时
            marioMap.move(Dir.LEFT);
            player.speed = 0;
            if (marioMap.x >= 0) player.speed = 5;
         } else player.speed = 5; //自由移动区域
      }
   }
   // 测试
   public static void main(String[] args) {
      MarioFrame marioFrame = new MarioFrame();
   }
}

注:以上就是本期的所有内容了,我看到大家的反馈中有许多关于碰撞检测的,这篇文章虽然在将地图的移动,其中也已涉及到了碰撞检测。
java游戏阶段的主要作用是:帮助大家编程入门,这个入门是指:培养程序思维、发现及解决问题的能力、自学能力,所以,在日常的学习中,要多思多想,多回顾。教程上尽可能的给大家讲详细些,是为了我把如何思考,如何解决问题的过程给大家拿出来,供大家参考,大家要看到思想,而不仅仅是看到代码。
若发现文章中存在错误还请及时联系我,有什么问题欢迎大家钉钉我,或在本文章下评论——zwk

你可能感兴趣的:(Java小游戏制作,java,游戏)