• 无swing,高级javaSE毕业之贪吃蛇游戏(含模块构建,多线程监听服务),已录制视频


    JavaSE,无框架实现贪吃蛇

    B站已发视频无swing,纯JavaSE贪吃蛇游戏设计构建

    本篇文章没有使用任何框架,纯JavaSE编写的贪吃蛇。主要探讨点为程序设计,比如流程绘制,模块划分。

    如果需要源代码,公众号’一只学Java的飞哥呀’,回复贪吃蛇,即可获取源码和讲义。此外附赠1年前用GUI写的贪吃蛇

    JavaSE无框架实现贪吃蛇效果

    贪吃蛇JavaSE无框架


    JavaGUI实现贪吃蛇效果,但文章内容并无涉及GUI代码编写,仅仅在公众号上提供相应代码

    贪吃蛇GUI

    1.整体思考

    • 游戏明面上组成部分有2。蛇、地图。在JavaSE的知识体系内。地图可以使用二维数组表示,蛇可以用一维数组表示
    • 通过在控制台打印数组的形式,来静态展示贪吃蛇游戏
    • 游戏本质上是一组连续的图片,每一秒打印一次数组,以此让游戏动起来
    • 游戏需要通过用户敲击键盘,实现方向移动。程序需要监听键盘输入,并将输入结果传递给蛇,以此操作蛇的移动

    2.可能的难点思考

    2.1 如何表示游戏界面

    public class GameMap{
        private static int row = 20;
        private static int col = 20;
    	// String的二维数组, 用来表示地图
        public static String[][] gameMap = new String[row][col];    
    
        // 初始化地图
        public GameMap() {
            // o 为地图, * 为蛇, @ 为食物
            for (int i = 0; i < gameMap.length; ++i) {
                for (int j = 0; j < gameMap[0].length; ++j) {
                    gameMap[i][j] = "o";
                }
            }
        }
        //...
    }
    
    // Node的一维列表, 用来表示蛇的坐标
    public class Node{
        int x;
        int y;
        public Node() {}
        public Node(int x, int y) {
            this.x = x;
    		this.y = y;
    	}
    }
    
    public class Snake{
    	Deque<Node> snakeLoc = new ArrayDeque<>(); 
    	// ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    2.2 如何渲染游戏界面

    打印地图,相当于渲染游戏界面

    void printMap() {
        // 循环打印map   
    }
    
    • 1
    • 2
    • 3

    2.3 如何让游戏动起来

    用循环,持续不断的打印界面,就可以形成动起来的效果

    while(true) {
    	// ...
        printMap(map, snake);
        // ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    2.4 蛇如何移动

    蛇的移动属于蛇对象的行为,因此我们可以在Snake类中封装move方法,移动的本质是:蛇尾移动到蛇头.

    public class Snake{
        // 返回尾坐标,头坐标
        public void move(int[] direction) {
            // 获取蛇尾坐标
            Node lastNode = snakeLoc.removeLast();
            // 移动
            Node newNode = moveTo(direction);
        	// 添加到蛇头
            snakeLoc.addFirst(newNode);
        }
        
        private Node moveTo(Node node, int[] direction) {
            // 获取头节点
            Node firstNode = snakeLoc.getFirst();
            // 执行移动逻辑
            int x = firstNode.getX();
            int y = firstNode.getY();
            x += direction[0];
            y += direction[1];
            firstNode.setX(x);
            firstNode.setY(y);
            return firstNode;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    3.流程图制作

    在这里插入图片描述

    4.模块划分

    在这里插入图片描述

    5.模块完善

    5.0常量优化

    public interface Constants {
        /**
         * 蛇的标记
         */
        String SNAKE_FLAG = "o";
        /**
         * 地图的标记
         */
        String MAP_FLAG = "*";
        /**
         * 食物的标记
         */
        String FOOD_FLAG = "@";
        /**
         * 地图行数
         */
        int row = 10;
        /**
         * 地图列数
         */
        int col = 10;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    5.1监听键盘服务

    考虑到还在JavaSE的范畴,swing的键盘监听功能我们不会去使用,而网上有没有找到合适的代替方案。因此,我们采用最原始的方法,Scanner输入,来代替监听功能。但scanner会有阻塞现象,一旦把主游戏进程阻塞,那么后续的流程都将无法进行。因此,我们需要开启子线程来监听用户输入

    i.输入存储
    /**
     * 存储用户的输入
     */
    public class StoreInput{
        private static String input = "a";
        /**    w
          * a  s  d
          */
        private static List<String> validDir = Arrays.asList("w", "a", "s", "d");
        
        public static void set(String in) {
            if (validDir.contains(in)) {
            	input = in;    
            }
        }
        
        public static int[] get() {
            if ("w".equals(input)) {
                int[] dir = {0, 1};
                return dir;
            }else if ("a".equals(input)) {
    			int[] dir = {-1, 0};
                return dir;    
            }else if ("s".equals(input)) {
                int[] dir = {0, -1};
                return dir;
            }else {
                int[] dir = {1, 0};
                return dir;
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    ii.键盘监听
    /**
     * 监听器, 监听输入
     */
    public class ScanerListener{
        public void start() {
            // 创建线程
            new Thread(new Runnable() {
                Scanner scanner = new Scanner(System.in);
    
                @Override
                public void run() {
                    while(true) {
                        if (scanner.hasNext()) {
                            // 存储最后一个字符
                            int length = scanner.next().length();
                            char inputChar = scanner.next().charAt(length - 1);
                        	StoreInput.set(String.valueOf(inputChar));
                        }
                    }
                }
            }).start();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    5.2棋盘类方法(地图)

    • 地图节点渲染(蛇/食物 坐标渲染)
    • 地图边界判断
    i.节点渲染
    // 地图坐标更新
    public static void updateMap(Node node, String type) {
    	gameMap[node.getX()][node.getY()] = type;
    }
    
    • 1
    • 2
    • 3
    • 4
    ii.边界判断
    // 判断是否到达地图边缘
    public static boolean isValid(Node node) {
    	if (node.getX() < 0 || node.getX() >= Constants.row || node.getY() < 0 || node.getY() >= Constants.col) {
    		// 非法
       		return false;         
    	}
        // 合法
        return true;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    iii.地图显示

    循环打印地图数组

    public void show() {	
    	for (int i = 0; i < gameMap.length; ++i) {
    		for (int j = 0; j < gameMap[0].length; ++j) {
        	    System.out.print(gameMap[i][j]);
                System.out.print(" ");
            }
            System.out.println();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    iV.食物生成
    private static Random random = new Random();
    
    private static Node food = new Node(1, 1);
    
    /**
     * 生成食物, 且保证不是在蛇的身体上
     */ 
    public static void generateFood() {
        // 循环生成成对坐标, 并且坐标不能落在蛇体上
        int x = 0;
        int y = 0;
        do {
            x = random.nextInt(Constants.row);
            y = random.nextInt(Constants.col);
        }while( isSnake(x, y) );
        food = new Node(x, y);
        updateMap(food, Constants.SNAKE_FLAG);
    }
    
    /**
     * 返回食物节点
     */
    public static Node getFood() {
        return food;
    }
    
    private static boolean isSnake(int x, int y) {
        return gameMap[x][y].equals(Constants.SNAKE_FLAG);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    V.地图初始化

    1.初始化食物,地图,蛇

    // 初始化地图
    public GameMap() {
        // o 为地图, * 为蛇, @ 为食物
        for (int i = 0; i < gameMap.length; ++i) {
        	for (int j = 0; j < gameMap[0].length; ++j) {
            	gameMap[i][j] = Constants.MAP_FLAG;
            }
        }
    	generateFood();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    5.3蛇类方法

    初步完善如下功能:

    • 位置移动
    • 自我碰撞监测
    • 食物监测
    i.蛇体初始化
    public class Snake{
        // 初始化贪吃蛇
    	public Snake() {
    		Node node = new Node(Constants.row / 2, Constants.col / 2);
    		snakeLoc.addFirst(node);
    		GameMap.updateMap(node, Constants.SNAKE_FLAG);
    
    		Node node1 = new Node(node.getX() + 1, node.getY());
    		snakeLoc.addLast(node1);
    		GameMap.updateMap(node1, Constants.SNAKE_FLAG);
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    ii.自定义异常
    public SnakeException extends RuntimeException{
        public SnakeException(String msg) {
            super(msg);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    iii.食物监测
    /**
     * 监测食物
     */
    public void detectFood(Node firstNode) {
    	boolean flag = isFood(firstNode);
    	if (flag) {
    		System.out.println("吃掉!");
    		// 长度增加
    		longgerSelf();
    		// 随机生成食物
            GameMap.generateFood();
    	}
    }
    
    /**
     * 增长自己
     */ 
    private void longgerSelf(){
    	// 获取当前方向
    	int[] dir = StoreInput.get();
    	// 方向取反, 获得尾巴需要添加的方向
    	int x = -1 * dir[0];
    	int y = -1 * dir[1];
    	// 在尾部添加节点
    	Node lastNode = snakeLoc.getLast();
    	Node newNode = new Node(lastNode.getX() + x, lastNode.getY() + y);
    	// 添加节点到尾部
    	snakeLoc.addLast(newNode);
    	// 更新节点
    	GameMap.updateMap(newNode, Constants.SNAKE_FLAG);
    }
    
    /**
     * 判断节点是否是食物
     * @param firstNode
     */
    private boolean isFood(Node firstNode) {
    	Node foodNode = GameMap.getFood();
    	return firstNode.getX() == foodNode.getX() && firstNode.getY() == foodNode.getY();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    iV.自我碰撞监测
    /**
     * 传入新的头节点, 判断是否和身体节点冲突
     */
    public boolean detectSelf(Node firstNode) {
        // 判断是否和余下的节点冲突
        for (Node node : snakeLoc) {
            if (node.getX() == firstNode.getX() && node.getY() == firstNode.getY()) {
                return true;
            }
        }
        return false;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    V.移动

    因为我们已经有输入存储模块,我们可以直接从中获取

    // 返回尾坐标,头坐标
    public void move() {
    	// 获取蛇尾坐标
    	Node lastNode = snakeLoc.removeLast();
    	// 获取方向
    	int[] direction = StoreInput.get();
    	// 移动
    	Node newNode = moveTo(direction);
    	// 墙体监测
    	if (!GameMap.isValid(newNode)) {
    		throw new SnakeException("撞墙!游戏结束");
    	}
    	// 自我碰撞监测
    	if (detectSelf(newNode)) {
    		throw new SnakeException("撞到自己!游戏结束");
    	}
    	// 返回更改坐标
    	GameMap.updateMap(lastNode, Constants.MAP_FLAG);
    	GameMap.updateMap(newNode, Constants.SNAKE_FLAG);
    	// 食物探测
    	detectFood(newNode);
    	// 添加到蛇头
    	snakeLoc.addFirst(newNode);
    }
        
    private Node moveTo(int[] direction) {
    	// 获取头节点
    	Node firstNode = snakeLoc.getFirst();
    	// 执行移动逻辑
    	int x = firstNode.getX();
    	int y = firstNode.getY();
    	x += direction[0];
    	y += direction[1];
    	// 创建新节点
    	return new Node(x, y);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    6.业务流程编写

    游戏类,主要控制全局的游戏流程

    public class SnakeGame {
    	// 创建地图
        private static GameMap map = new GameMap();
        // 创建蛇对象
        private static Snake snake = new Snake();
        // 创建监听服务
        private static ScanerListener listener = new ScanerListener();
    
        public static void main(String[] args) {
    		// 开启游戏
            // 启动键盘监听服务
        	listener.start();
            try {
            	while(true) {
                	// 绘图
                	map.show();
                	// 睡眠1秒
                    try {
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    // 移动蛇
                    snake.move();
    		        // 清空控制台
                    cls();
                }
            } catch(SnakeException e) {
                e.printStackTrace();
            }
        }
    
        private static void cls() {
    //        System.out.print("Everything on the console will cleared");
            System.out.print("\033[H\033[2J");
            System.out.flush();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
  • 相关阅读:
    tcpdump(五)命令行参数讲解(四)
    >>数据管理:DAMA简介
    [附源码]计算机毕业设计JAVA基于web鲜花销售系统论文2022
    C++ 构造函数
    零宽空格引发的问题
    npm 无法将“npm”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。请检查名称的拼写,如果包括路径,请确保路径正确,然后再试一次。
    2023秋招nlp笔试题-太初
    使用 Fail2ban 防止 ssh 暴力破解攻击
    visual studio下载安装
    ChatGPT Prompting开发实战(四)
  • 原文地址:https://blog.csdn.net/qq_62835094/article/details/132729576