JavaSE,无框架实现贪吃蛇
B站已发视频 :无swing,纯JavaSE贪吃蛇游戏设计构建
文章目录
本篇文章没有使用任何框架,纯JavaSE编写的贪吃蛇。主要探讨点为程序设计,比如流程绘制,模块划分。
如果需要源代码,公众号'一只学Java的飞哥呀',回复贪吃蛇,即可获取源码和讲义。此外附赠1年前用GUI写的贪吃蛇
JavaSE无框架实现贪吃蛇效果
贪吃蛇JavaSE无框架
JavaGUI实现贪吃蛇效果,但文章内容并无涉及GUI代码编写,仅仅在公众号上提供相应代码
贪吃蛇GUI
1.整体思考
- 游戏明面上 的组成部分 有2。蛇、地图。在JavaSE的知识体系内。地图可以使用
二维数组
表示,蛇可以用一维数组
表示 - 通过在控制台打印数组 的形式,来静态展示贪吃蛇游戏
- 游戏本质上是一组连续的图片 ,每一秒打印一次数组,以此让游戏动起来
- 游戏需要通过用户敲击键盘,实现方向移动。程序需要监听键盘输入,并将输入结果传递给蛇,以此操作蛇的移动
2.可能的难点思考
2.1 如何表示游戏界面
java
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<>();
// ...
}
2.2 如何渲染游戏界面
打印地图,相当于渲染游戏界面
java
void printMap() {
// 循环打印map
}
2.3 如何让游戏动起来
用循环,持续不断的打印界面,就可以形成动起来的效果
java
while(true) {
// ...
printMap(map, snake);
// ...
}
2.4 蛇如何移动
蛇的移动属于蛇对象的行为,因此我们可以在Snake类中封装move方法,移动的本质是:蛇尾移动到蛇头.
java
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;
}
}
3.流程图制作
4.模块划分
5.模块完善
5.0常量优化
java
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.输入存储
java
/**
* 存储用户的输入
*/
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;
}
}
}
ii.键盘监听
java
/**
* 监听器, 监听输入
*/
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();
}
}
5.2棋盘类方法(地图)
- 地图节点渲染(蛇/食物 坐标渲染)
- 地图边界判断
i.节点渲染
java
// 地图坐标更新
public static void updateMap(Node node, String type) {
gameMap[node.getX()][node.getY()] = type;
}
ii.边界判断
java
// 判断是否到达地图边缘
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.地图显示
循环打印地图数组
java
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.食物生成
java
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.初始化食物,地图,蛇
java
// 初始化地图
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.蛇体初始化
java
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);
}
}
ii.自定义异常
java
public SnakeException extends RuntimeException{
public SnakeException(String msg) {
super(msg);
}
}
iii.食物监测
java
/**
* 监测食物
*/
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.自我碰撞监测
java
/**
* 传入新的头节点, 判断是否和身体节点冲突
*/
public boolean detectSelf(Node firstNode) {
// 判断是否和余下的节点冲突
for (Node node : snakeLoc) {
if (node.getX() == firstNode.getX() && node.getY() == firstNode.getY()) {
return true;
}
}
return false;
}
V.移动
因为我们已经有输入存储
模块,我们可以直接从中获取
java
// 返回尾坐标,头坐标
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.业务流程编写
游戏类,主要控制全局的游戏流程
java
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();
}
}