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

JavaSE,无框架实现贪吃蛇

文章目录

  • JavaSE,无框架实现贪吃蛇
    • 1.整体思考
    • 2.可能的难点思考
      • 2.1 如何表示游戏界面
      • 2.2 如何渲染游戏界面
      • 2.3 如何让游戏动起来
      • 2.4 蛇如何移动
    • 3.流程图制作
    • 4.模块划分
    • 5.模块完善
      • 5.0常量优化
      • 5.1监听键盘服务
        • i.输入存储
        • ii.键盘监听
      • 5.2棋盘类方法(地图)
        • i.节点渲染
        • ii.边界判断
        • iii.地图显示
        • iV.食物生成
        • V.地图初始化
      • 5.3蛇类方法
        • i.蛇体初始化
        • ii.自定义异常
        • iii.食物监测
        • iV.自我碰撞监测
        • V.移动
    • 6.业务流程编写

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

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

JavaSE无框架实现贪吃蛇效果

贪吃蛇JavaSE无框架


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

贪吃蛇GUI

1.整体思考

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

2.可能的难点思考

2.1 如何表示游戏界面

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的一维列表, 用来表示蛇的坐标
class Node{
    int x;
    int y;
    public Node() {}
    public Node(int x, int y) {
        this.x = x;
		this.y = y;
	}
}

class Snake{
	Deque<Node> snakeLoc = new ArrayDeque<>(); 
	// ...
}

2.2 如何渲染游戏界面

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

void printMap() {
    // 循环打印map   
}

2.3 如何让游戏动起来

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

while(true) {
	// ...
    printMap(map, snake);
    // ...
}

2.4 蛇如何移动

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

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;
    }
}

3.流程图制作

无swing,高级javaSE毕业之贪吃蛇游戏(含模块构建,多线程监听服务)_第1张图片

4.模块划分

无swing,高级javaSE毕业之贪吃蛇游戏(含模块构建,多线程监听服务)_第2张图片

5.模块完善

5.0常量优化

public interface Constants {
    /**
     * 蛇的标记
     */
    String SNAKE_FLAG = "o";
    /**
     * 地图的标记
     */
    String MAP_FLAG = "*";
    /**
     * 食物的标记
     */
    String FOOD_FLAG = "@";
    /**
     * 地图行数
     */
    int row = 10;
    /**
     * 地图列数
     */
    int col = 10;
}

5.1监听键盘服务

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

i.输入存储

/**
 * 存储用户的输入
 */
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;
        }
    }
}

ii.键盘监听

/**
 * 监听器, 监听输入
 */
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();
    }
}

5.2棋盘类方法(地图)

  • 地图节点渲染(蛇/食物 坐标渲染)
  • 地图边界判断

i.节点渲染

// 地图坐标更新
public static void updateMap(Node node, String type) {
	gameMap[node.getX()][node.getY()] = type;
}

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;
}

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();
    }
}

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);
}

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();
}

5.3蛇类方法

初步完善如下功能:

  • 位置移动
  • 自我碰撞监测
  • 食物监测

i.蛇体初始化

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);
	}
}

ii.自定义异常

public SnakeException extends RuntimeException{
    public SnakeException(String msg) {
        super(msg);
    }
}

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();
}

iV.自我碰撞监测

/**
 * 传入新的头节点, 判断是否和身体节点冲突
 */
public boolean detectSelf(Node firstNode) {
    // 判断是否和余下的节点冲突
    for (Node node : snakeLoc) {
        if (node.getX() == firstNode.getX() && node.getY() == firstNode.getY()) {
            return true;
        }
    }
    return false;
}

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);
}

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);
                    // 清空控制台
                    cls();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 移动蛇
                snake.move();
            }
        } 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();
    }
}

你可能感兴趣的:(JavaSE,无框架,贪吃蛇)